本アプリは SwiftDataではなく Supabase をバックエンドに採用。各モデルは struct + Codable で定義し、Supabase クライアント経由で読み書き。RLS(Row Level Security)でユーザー権限を制御。

ER 図(主要テーブル)

erDiagram AppUser ||--o{ Ranking : creates AppUser ||--o{ Vote : casts AppUser ||--o{ PointTransaction : has AppUser ||--o{ AppNotification : receives AppUser ||--o{ Follow : "follows/followed_by" Ranking ||--o{ Choice : contains Ranking ||--o{ Vote : "voted on" Ranking ||--o{ Quiz : has Ranking ||--o{ ChoiceRequest : "requested for" Ranking ||--o{ Report : reported Choice ||--o{ Vote : "selected by" AppUser { UUID id String displayName Bool isPremium Int points Int loginStreak Int freeCreationRemaining } Ranking { UUID id UUID creatorId String title String majorTag String minorTag String mode "single|threePoint" String status "draft|active|ended|held|suspended|deleted" Bool hasQuiz Int voteCount String voteTier "G..S" Date startsAt Date endsAt Bool isPublic Bool isAnonymous } Choice { UUID id UUID rankingId String label String shortLabel String imageUrl String imageStatus Int displayOrder Int voteCount } Vote { UUID id UUID rankingId UUID userId UUID choiceId Int points Int voteNumber String ipHash String deviceHash } Quiz { UUID id UUID rankingId String question String correctAnswer String answerHint Int displayOrder }

主要モデル

AppUser

struct AppUser: Codable, Identifiable {
    let id: UUID
    var displayName: String
    var isPremium: Bool
    var points: Int
    var loginStreak: Int
    var freeCreationRemaining: Int
    // ... 他プロフィール項目
}

Ranking

struct Ranking: Codable, Identifiable {
    let id: UUID
    let creatorId: UUID
    var title: String
    var majorTag: String
    var minorTag: String?
    var mode: RankingMode       // single / threePoint
    var status: RankingStatus   // draft, active, ended, ...
    var hasQuiz: Bool
    var voteCount: Int
    var voteTier: VoteTier      // G〜S
    var startsAt: Date
    var endsAt: Date
    var isPublic: Bool
    var isAnonymous: Bool
    var deletedAt: Date?
    var pointsConsumed: Int
}

Choice / Vote / Quiz

struct Choice: Codable, Identifiable {
    let id: UUID
    let rankingId: UUID
    var label: String
    var shortLabel: String?
    var imageUrl: String?
    var imageStatus: String?    // pending, approved 等
    var displayOrder: Int
    var voteCount: Int
}

struct Vote: Codable, Identifiable {
    let id: UUID
    let rankingId: UUID
    let userId: UUID
    let choiceId: UUID
    let points: Int             // 投票時の消費ポイント
    let voteNumber: Int         // 1=初投票, 2=revote1, 3=revote2
    let ipHash: String?
    let deviceHash: String?
}

struct Quiz: Codable, Identifiable {
    let id: UUID
    let rankingId: UUID
    let question: String
    let correctAnswer: String
    let answerHint: String?
    let displayOrder: Int
}

その他のモデル

主要 Enum

Enumケース
RankingStatusdraft / active / ended / held / suspended / deleted
RankingModesingle(シングル) / threePoint(TOP3)
VoteTierG / F / E / D / C / B / A / S(投票数帯)
PointActionloginBonus / adReward / createRanking / revote1 / revote2 / viewResult / cancelRefund 他15種
ReportReasondiscrimination / violence / selfHarm / sexual / illegal / personalInfo / spam / other
ChoiceRequestStatuspending / approved / rejected

ポイント消費表

アクション消費ポイント
ランキング作成(基本)20pt
クイズ付与+15pt
TOP3ランキング20pt
選択肢拡張 +1010pt
選択肢拡張 +1520pt
期間延長 +7日10pt
期間延長 +14日20pt
1回目の再投票3pt
2回目の再投票7pt
結果閲覧(条件付)1〜3pt

整合性に関する注意

非アトミック操作: RankingService.createRanking()VoteService.castVote() は複数の Supabase RPC/INSERT をシーケンシャル実行する。途中失敗時の整合性を保証するため、本番運用では Edge Function 化を検討すること。
二重付与防止: PointService.claimLoginBonus() はクライアント側 lastLoginDate + DB point_transactions の二層チェックで日次ボーナスの二重付与を防いでいる。

関連ファイル

変更履歴

バージョン日付変更内容
1.02026-05-09初版作成(ソースコードからリバース)