# 데이터 모델 (제안)

표 이름·필드는 제안이에요. 핵심은 **"누가 썼는지"를 저장은 하되, 응답에서는 익명 규칙대로 걸러낸다**는 점이에요.

## 계정

| 표 | 주요 필드 | 메모 |
|---|---|---|
| `users` | id, email(unique), password_hash(null 가능), nickname, avatar_url, plan(FREE/PRO), plan_expires_at, created_at, deleted_at | 소셜 전용 계정은 password_hash 없음 |
| `user_identities` | id, user_id, provider(google/kakao), provider_user_id | 소셜 연결 |
| `user_profiles` | user_id, strength, desired_role, skills(text[]), updated_at | 3 · A3 |
| `user_settings` | user_id, notify_session_invite, notify_report_ready, notify_marketing | A5 |
| `agreements` | id, user_id, type(terms/privacy/marketing), version, agreed(bool), agreed_at | 가입 시 동의 기록 |
| `refresh_tokens` | id, user_id, token_hash, remember(bool), expires_at, used_at, replaced_by, revoked_at | 회전 방식 · 로그아웃·비밀번호 변경 시 무효화 · 재사용 감지 |
| `legal_documents` | type, version, title, html, effective_date | 약관 원문 |
| `subscriptions` / `payments` | … | 결제 확정은 PG 웹훅으로 (미정) |

## 세션 · 참가자

| 표 | 주요 필드 | 메모 |
|---|---|---|
| `sessions` | id, code(대문자·숫자 6자리, 끝나지 않은 방끼리 unique), host_user_id, topic, criteria, duration_min(null=제한 없음), max_members, status(lobby/running/ended), stage_id, sub_step, started_at, ends_at, confirmed_idea_id | |
| `participants` | id, session_id, user_id(**not null**), role(host/participant), nickname, alias(A/B/C/D, 시작 시 무작위), row_order(무작위), online, last_seen_at, joined_at, kicked_at | (session_id, user_id) unique — 재접속은 같은 행을 그대로 씀 |
| `participant_profiles` | participant_id, strength, desired_role, skills(text[]) | **처음 입장할 때 스냅샷** (재접속 때는 그대로) |

## 7 아이스브레이킹

| 표 | 주요 필드 | 메모 |
|---|---|---|
| `interview_messages` | id, participant_id, role(ai/user), label, style(fq/explain/good/wait), text, question_no, client_message_id, created_at | **본인만 조회** |
| `interview_states` | participant_id, step(1~5), followup_used(bool), done(bool) | |
| `news_cards` | id, session_id, category(시장/기술/규제), title, plain, for_team, source_name, source_url, published_at, search_query | 세션당 3장 |
| `news_reactions` | card_id, participant_id, reaction(new/heard/know) | 진행자에겐 합계만 |
| `explain_requests` | id, participant_id, card_id, term, answer | |
| `materials` | id, session_id, participant_id, text, avoid(bool), kind(내부용) | 응답에 participant_id 넣지 않기 |
| `material_groups` | id, session_id, title, description, ai_pick(bool), focus(true/false/null), version | 다시 묶기 때 version 증가 |
| `material_group_items` | group_id, text, count, avoid | 표시용 요약(같은 재료 합침) |

## 8 발산

| 표 | 주요 필드 | 메모 |
|---|---|---|
| `ideas` | id, session_id, participant_id(주인), rank(1~3), text, source(own/ai), recommendation_id, created_at | 주인은 8-7 전까지 비공개 |
| `idea_recommendations` | id, participant_id, title, reason, page, picked(bool) | |
| `comments` | id, idea_id, author_participant_id, concern(text, 필수), praise(text, null) , created_at | **작성자 끝까지 비공개** · (idea_id, author) unique |
| `reviews` | idea_id(또는 ai_idea_id), grade(go/fix/re), exists_label, exists_summary, search_url, feasibility_level(상/중/하), feasibility_summary, missing_skill_count, missing_skill_summary, need_label, need_summary, timeline_label, timeline_summary, comment_summary(json), status(pending/done/failed) | AI 작업 결과 |
| `ai_ideas` | id, session_id, title, grade, sources(json: idea_id + taken_point), fixes(json) | "AI가 모음" · 최대 2개 |
| `common_threads` | id, session_id, title, why, sources(json: question + summary), candidate_ids(json) | 2명 이상 겹칠 때만 |
| `common_thread_members` | thread_id, participant_id | "내 답도 들어 있어요" 계산용, 응답 금지 |
| `common_thread_reactions` | thread_id, participant_id, reaction(didntKnow/knew) | |
| `votes` | session_id, participant_id, target_id(idea 또는 ai_idea), round(1=본투표, 2=재투표) | 1인 2표 · **누가 투표했는지 비공개** |
| `vote_finishes` | session_id, participant_id, round, finished_at | |

## 9 파트 나누기 · 배치 · 보고서

| 표 | 주요 필드 | 메모 |
|---|---|---|
| `sessions` (추가 필드) | confirmed_idea_id, leader_participant_id, assignment_version, assignment_confirmed_at | 팀장 = 배치를 확정하는 사람 (기본값 진행자) |
| `team_parts` | id, session_id, tier(core/normal/small), name, description, skill, effort(1~5, AI 추정 작업량), status(overlap/single/none), alternative, sort_order | 주제 확정 직후 AI가 만듦 |
| `part_candidates` | part_id, participant_id | 프로필 스냅샷 스킬로 고른 후보 |
| `part_questions` | id, part_id, qid(q1/q2), type(choice/text), text, options(json), context, hint | 파트마다 2개 · 같은 파트 후보는 같은 질문 |
| `part_answers` | id, part_id, participant_id, answers(json), created_at | **본인과 AI만** · (part_id, participant_id) unique |
| `part_decisions` | part_id, chosen_participant_id, method(question/single/balance/excluded), score(json, 비공개), decided_at | 공개하는 건 method 뿐 |
| `assignments` | id, session_id, part_id, assignee_participant_id, ai_assignee_participant_id, changed_by_leader(bool), version | 버전이 올라갈 때마다 team.assignment.updated |
| `part_marks` | part_id, participant_id, created_at | "이야기해 보자" 표시 · 팀장에게만 이름 공개 |
| `assignment_suggestions` | id, session_id, part_id, from_participant_id, to_participant_id, reason, status(open/accepted/dismissed), based_on_version | 분량 쏠림 제안 1개 |
| `reports` | id, session_id, version, content(json), created_at | A4 2쪽 내용 전체 · 담당이 바뀌면 새 version |

## 인덱스 · 제약 체크리스트

- `sessions.code`: 끝나지 않은 방(status≠ended) 안에서 unique
- `participants`: (session_id, user_id) unique
- `ideas`: (participant_id, rank) unique, 한 사람 최대 3개
- `comments`: (idea_id, author_participant_id) unique, 자기 아이디어 금지(서버 검사)
- `comments.praise`: 한 사람당 세션 전체 2개 이하(서버 검사)
- `votes`: 한 사람당 라운드별 2개 이하(재투표는 설정값), 숨은 공통점 id 금지
- `part_answers`: (part_id, participant_id) unique · 그 파트의 후보만 저장 가능(서버 검사)
- `assignments`: (session_id, part_id) unique · 담당은 그 세션 참가자만
- `part_marks`: (part_id, participant_id) unique
- `sessions.leader_participant_id`: 그 세션의 참가자여야 함 · 배치를 고치고 확정하는 API는 이 사람만
