λ¬Έμ λ²μ : 2026-01-10 (v2.2 - MEMBER μν μΈλΆν & μ§λ¬Έ μ°μ μμ λμ )
erDiagram
FAMILY ||--o{ MEMBER : has
FAMILY ||--o{ FAMILY_REPORT : archives
MEMBER ||--o{ MEMBER_QUESTION : receives
MEMBER_QUESTION ||--o{ ANSWER : has
ANSWER ||--o{ REACTION : receives
ANSWER ||--o{ COMMENT : has
FAMILY {
bigint id PK
text name
text lifecycle_status
timestamptz created_at
timestamptz updated_at
}
MEMBER {
bigint id PK
bigint family_id FK
text nickname
text role_type
text role_name
int order_in_role
text label
date birthday
timestamptz created_at
timestamptz updated_at
}
QUESTION {
bigint id PK
text content
int level
text category
timestamptz created_at
}
MEMBER_QUESTION {
bigint id PK
bigint member_id FK
text content
int level
int priority
date planned_date
text status
int shuffle_count
jsonb metadata
timestamptz answered_at
}
ANSWER {
bigint id PK
bigint member_question_id FK
text answer_type
jsonb content
timestamptz created_at
}
REACTION {
bigint id PK
bigint answer_id FK
bigint member_id FK
text emoji
timestamptz created_at
}
COMMENT {
bigint id PK
bigint answer_id FK
bigint commenter_member_id FK
bigint parent_id FK
text content
timestamptz created_at
timestamptz edited_at
timestamptz deleted_at
}
FAMILY_REPORT {
bigint id PK
bigint family_id FK
text type
date start_date
date end_date
text summary
jsonb data
timestamptz created_at
}
κ°μ‘± λ¨μμ λ£¨νΈ μν°ν°
| μ»¬λΌ | νμ | μ μ½ | μ€λͺ |
|---|---|---|---|
id |
BIGSERIAL | PK | κ°μ‘± κ³ μ ID |
name |
TEXT | NOT NULL | κ°μ‘± μ΄λ¦ |
lifecycle_status |
TEXT | κ°μ‘± μν: λ°μ¨ | νμ | νμ¬ | λ³ν |
|
created_at |
TIMESTAMPTZ | DEFAULT now() | μμ± μκ° |
updated_at |
TIMESTAMPTZ | DEFAULT now() | μμ μκ° |
CREATE TABLE family (
id BIGSERIAL PRIMARY KEY,
name TEXT NOT NULL,
lifecycle_status TEXT,
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now()
);κ°μ‘± ꡬμ±μ
| μ»¬λΌ | νμ | μ μ½ | μ€λͺ |
|---|---|---|---|
id |
BIGSERIAL | PK | ꡬμ±μ κ³ μ ID |
family_id |
BIGINT | FK, NOT NULL | μμ κ°μ‘± |
nickname |
TEXT | NOT NULL | νλ©΄ νμμ© (곡주λ, μΈμλΉ λ±) |
role_type |
TEXT | μμ€ν λ‘μ§μ© (PARENT, CHILD λ±) | |
role_name |
TEXT | μ κ·νλ νΈμΉ (μλΉ , μλ§, λΈ λ±) | |
order_in_role |
INT | DEFAULT 1 | μμ (첫째, λμ§Έ λ±) |
label |
TEXT | κ΄κ³ λ§₯λ½ μμΆ (첫째 λΈ, λ§λ΄, μμΈ ν°μλ§ λ±) | |
birthday |
DATE | μλ μμΌ | |
created_at |
TIMESTAMPTZ | DEFAULT now() | κ°μ μκ° |
updated_at |
TIMESTAMPTZ | DEFAULT now() | μμ μκ° |
CREATE TABLE member (
id BIGSERIAL PRIMARY KEY,
family_id BIGINT NOT NULL REFERENCES family(id) ON DELETE CASCADE,
nickname TEXT NOT NULL,
role_type TEXT,
role_name TEXT,
order_in_role INT DEFAULT 1,
label TEXT,
birthday DATE,
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now()
);
CREATE INDEX idx_member_family ON member(family_id);
CREATE INDEX idx_member_role_type ON member(role_type);μ§λ¬Έ ν νλ¦Ώ (μ΄μμκ° λ±λ‘νλ μλ λ°μ΄ν°, μ°Έμ‘°μ©)
| μ»¬λΌ | νμ | μ μ½ | μ€λͺ |
|---|---|---|---|
id |
BIGSERIAL | PK | μ§λ¬Έ κ³ μ ID |
content |
TEXT | NOT NULL | μ§λ¬Έ λ³Έλ¬Έ |
level |
INT | DEFAULT 1 | λμ΄λ 1 |
category |
TEXT | μΉ΄ν
κ³ λ¦¬: μΌμ | μΆμ΅ | κ°μ | λ―Έλ | μ·¨λ―Έ | κ΄κ³ |
|
created_at |
TIMESTAMPTZ | DEFAULT now() | μμ± μκ° |
CREATE TABLE question (
id BIGSERIAL PRIMARY KEY,
content TEXT NOT NULL,
level INT DEFAULT 1 CHECK (level BETWEEN 1 AND 4),
category TEXT,
created_at TIMESTAMPTZ DEFAULT now()
);
CREATE INDEX idx_question_level ON question(level);
CREATE INDEX idx_question_category ON question(category);μ°Έκ³ : QUESTIONμ ν νλ¦Ώ νλ‘λ§ μ¬μ©λ©λλ€. μ κ· λ©€λ² κ°μ μ MEMBER_QUESTIONμ 볡μ¬λ©λλ€.
λ©€λ²λ³ μ€μ λ°μ‘ μ§λ¬Έ (QUESTIONμμ λ³΅μ¬ λλ AI μμ±)
| μ»¬λΌ | νμ | μ μ½ | μ€λͺ |
|---|---|---|---|
id |
BIGSERIAL | PK | μ§λ¬Έ κ³ μ ID |
member_id |
BIGINT | FK, NOT NULL | μμ λ©€λ² (μ£ΌμΈκ³΅) |
content |
TEXT | NOT NULL | μ§λ¬Έ λ΄μ© (ν νλ¦Ώ λ³΅μ¬ λλ AI κ°μΈν) |
level |
INT | DEFAULT 1 | λμ΄λ 1~4 |
priority |
INT | DEFAULT 1 | μ°μ μμ (1:ν νλ¦Ώ, 2:λ³ΈμΈλ΅λ³ κΈ°λ° νμ, 3:10:1 κ°μ‘±λ΅λ³ κΈ°λ° νμ) |
planned_date |
DATE | NOT NULL | λ°μ‘ μμ μΌ |
status |
TEXT | DEFAULT 'scheduled' | μν: scheduled | sent | answered | skipped | passed |
shuffle_count |
INT | DEFAULT 0 | μ ν νμ (μ΅λ 3ν) |
metadata |
JSONB | AI μμ± μ 보 λ± | |
answered_at |
TIMESTAMPTZ | λ΅λ³ μλ£ μκ° |
metadata ꡬ쑰:
// AI μμ± μ§λ¬Έ
{"generated_by": "ai", "model": "gpt-5-nano", "confidence": 0.9}
// ν
νλ¦Ώ 볡μ¬
{"generated_by": "template", "source_question_id": 42}
// μλ μ
λ ₯
{"generated_by": "manual"}
// 1:1 νμ (P2)
{"logic_type": "follow_up", "parent_answer_id": 102}
// 10:1 ν©μ± (P3)
{"logic_type": "synthesis", "source_answer_ids": [101, 102, 108], "ai_insight": "..."}μ κ· λ©€λ² κ°μ μ ν νλ¦Ώ 볡μ¬:
INSERT INTO member_question (member_id, content, level, priority, planned_date, status, metadata)
SELECT
:new_member_id,
q.content,
q.level,
1,
:planned_date,
'scheduled',
jsonb_build_object('generated_by', 'template', 'source_question_id', q.id)
FROM question q
WHERE q.level <= 2
ORDER BY RANDOM()
LIMIT 30;CREATE TABLE member_question (
id BIGSERIAL PRIMARY KEY,
member_id BIGINT NOT NULL REFERENCES member(id) ON DELETE CASCADE,
content TEXT NOT NULL,
level INT DEFAULT 1 CHECK (level BETWEEN 1 AND 4),
priority INT DEFAULT 1 CHECK (priority BETWEEN 1 AND 3),
planned_date DATE NOT NULL,
status TEXT DEFAULT 'scheduled'
CHECK (status IN ('scheduled', 'sent', 'answered', 'skipped', 'passed')),
shuffle_count INT DEFAULT 0,
metadata JSONB,
answered_at TIMESTAMPTZ,
UNIQUE(member_id, planned_date)
);
CREATE INDEX idx_member_question_member ON member_question(member_id);
CREATE INDEX idx_member_question_status ON member_question(status);
-- λμ€ν¨μΉ μ°μ μμ λ°μ μΈλ±μ€ (priority μ°μ , planned_date λ€μ)
CREATE INDEX idx_mq_dispatch ON member_question(priority DESC, planned_date ASC);μ§λ¬Έμ λν λ΅λ³ (μμ±μ = member_question.member_id)
| μ»¬λΌ | νμ | μ μ½ | μ€λͺ |
|---|---|---|---|
id |
BIGSERIAL | PK | λ΅λ³ κ³ μ ID |
member_question_id |
BIGINT | FK, NOT NULL, UNIQUE | μ§λ¬Έ ID (1:1) |
answer_type |
TEXT | NOT NULL | λ΅λ³ μ ν: text | image | audio | video | mixed |
content |
JSONB | NOT NULL | λ΅λ³ λ΄μ© (μλ ꡬ쑰 μ°Έμ‘°) |
created_at |
TIMESTAMPTZ | DEFAULT now() | μμ± μκ° |
content ꡬ쑰:
// ν
μ€νΈ
{"text": "νν°νν° μ€νλ λ£λ μ€"}
// μ΄λ―Έμ§
{"url": "https://...", "mime": "image/jpeg"}
// νΌν©
{"text": "μ€λ λ¨Ήμ λΌλ©΄!", "url": "https://..."}CREATE TABLE answer (
id BIGSERIAL PRIMARY KEY,
member_question_id BIGINT NOT NULL UNIQUE REFERENCES member_question(id) ON DELETE CASCADE,
answer_type TEXT NOT NULL DEFAULT 'text'
CHECK (answer_type IN ('text', 'image', 'audio', 'video', 'mixed')),
content JSONB NOT NULL,
created_at TIMESTAMPTZ DEFAULT now()
);μμ±μ μ‘°ν:
JOIN member_question mq ON a.member_question_id = mq.idβmq.member_idκ° μμ±μ
μ΄λͺ¨μ§ 리μ‘μ (μ΄μ λ§μ°° 1μ΄ λ°μ)
| μ»¬λΌ | νμ | μ μ½ | μ€λͺ |
|---|---|---|---|
id |
BIGSERIAL | PK | 리μ‘μ κ³ μ ID |
answer_id |
BIGINT | FK, NOT NULL | λμ λ΅λ³ ID |
member_id |
BIGINT | FK, NOT NULL | 리μ‘μ λ¨κΈ΄ λ©€λ² |
emoji |
TEXT | NOT NULL | μ΄λͺ¨μ§: heart | check | smile λ± |
created_at |
TIMESTAMPTZ | DEFAULT now() | 리μ‘μ μκ° |
CREATE TABLE reaction (
id BIGSERIAL PRIMARY KEY,
answer_id BIGINT NOT NULL REFERENCES answer(id) ON DELETE CASCADE,
member_id BIGINT NOT NULL REFERENCES member(id) ON DELETE CASCADE,
emoji TEXT NOT NULL,
created_at TIMESTAMPTZ DEFAULT now(),
UNIQUE(answer_id, member_id)
);
CREATE INDEX idx_reaction_answer ON reaction(answer_id);λ΅λ³μ λν λκΈ (λλκΈ μ§μ)
| μ»¬λΌ | νμ | μ μ½ | μ€λͺ |
|---|---|---|---|
id |
BIGSERIAL | PK | λκΈ κ³ μ ID |
answer_id |
BIGINT | FK, NOT NULL | λμ λ΅λ³ ID |
commenter_member_id |
BIGINT | FK, NOT NULL | μμ±μ ID |
parent_id |
BIGINT | FK | λΆλͺ¨ λκΈ (λλκΈ) |
content |
TEXT | NOT NULL | λκΈ λ΄μ© |
created_at |
TIMESTAMPTZ | DEFAULT now() | μμ± μκ° |
edited_at |
TIMESTAMPTZ | μμ μκ° | |
deleted_at |
TIMESTAMPTZ | μμ μκ° (μννΈ μμ ) |
CREATE TABLE comment (
id BIGSERIAL PRIMARY KEY,
answer_id BIGINT NOT NULL REFERENCES answer(id) ON DELETE CASCADE,
commenter_member_id BIGINT NOT NULL REFERENCES member(id) ON DELETE CASCADE,
parent_id BIGINT REFERENCES comment(id) ON DELETE CASCADE,
content TEXT NOT NULL,
created_at TIMESTAMPTZ DEFAULT now(),
edited_at TIMESTAMPTZ,
deleted_at TIMESTAMPTZ
);
CREATE INDEX idx_comment_answer ON comment(answer_id);μ£Όκ°/μκ° κ°μ‘± 리ν¬νΈ
| μ»¬λΌ | νμ | μ μ½ | μ€λͺ |
|---|---|---|---|
id |
BIGSERIAL | PK | 리ν¬νΈ κ³ μ ID |
family_id |
BIGINT | FK, NOT NULL | κ°μ‘± ID |
type |
TEXT | NOT NULL | μ ν: WEEKLY | MONTHLY |
start_date |
DATE | NOT NULL | κΈ°κ° μμμΌ |
end_date |
DATE | NOT NULL | κΈ°κ° μ’ λ£μΌ |
summary |
TEXT | AI μμ± μμ½ | |
data |
JSONB | νμ΄λΌμ΄νΈ λ°μ΄ν° (μλ ꡬ쑰 μ°Έμ‘°) | |
created_at |
TIMESTAMPTZ | DEFAULT now() | μμ± μκ° |
data ꡬ쑰:
{
"answer_ids": [101, 105, 112],
"top_keywords": ["νν°νν°", "νΈλ‘νΈ"],
"total_answers": 7,
"total_reactions": 23
}CREATE TABLE family_report (
id BIGSERIAL PRIMARY KEY,
family_id BIGINT NOT NULL REFERENCES family(id) ON DELETE CASCADE,
type TEXT NOT NULL CHECK (type IN ('WEEKLY', 'MONTHLY')),
start_date DATE NOT NULL,
end_date DATE NOT NULL,
summary TEXT,
data JSONB,
created_at TIMESTAMPTZ DEFAULT now(),
UNIQUE(family_id, type, start_date)
);
CREATE INDEX idx_family_report_family ON family_report(family_id);FAMILY ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β lifecycle_status (λ°μ¨/νμ/νμ¬/λ³ν) β
β β
βββ MEMBER β
β ββ nickname, role_type, role_name, order_in_role, label, birthday β
β β
βββ FAMILY_REPORT β
β ββ type (WEEKLY/MONTHLY), summary, data β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
QUESTION (ν
νλ¦Ώ ν, μ°Έμ‘°μ©)
β content, level, category
β
β μ κ· λ©€λ² κ°μ
μ 볡μ¬
β ββββββββββββββββββββββΊ
β
MEMBER_QUESTION ββββββββββββββββββββββββββββββββββββββββββββββ
β content (μ§λ¬Έ λ΄μ©) β
β level (λμ΄λ 1~4) β
β priority (μ°μ μμ 1~3) β
β planned_date, status, shuffle_count β
β metadata (AI μμ±/νμ/ν©μ± μ 보) β
β β
βββ ANSWER βββββββββββββββββββββββββββββββββββββββββββββββββββ€
β content (λ΅λ³ λ΄μ©, μμ±μ=μ§λ¬Έ μ£ΌμΈκ³΅) β
β β
βββ REACTION (μ΄λͺ¨μ§) β
β β
βββ COMMENT (λκΈ) β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
| ν μ΄λΈ | μ»¬λΌ μ | μν |
|---|---|---|
| FAMILY | 5 | κ°μ‘± λ¨μ |
| MEMBER | 10 | κ°μ‘± ꡬμ±μ |
| QUESTION | 5 | μ§λ¬Έ ν νλ¦Ώ (μ°Έμ‘°μ©) |
| MEMBER_QUESTION | 10 | λ©€λ²λ³ μ€μ μ§λ¬Έ |
| ANSWER | 5 | λ΅λ³ |
| REACTION | 5 | μ΄λͺ¨μ§ 리μ‘μ |
| COMMENT | 9 | λκΈ |
| FAMILY_REPORT | 8 | μ£Όκ°/μκ° λ¦¬ν¬νΈ |
| μ μ½ | μ€λͺ |
|---|---|
member_question(member_id, planned_date) UNIQUE |
λ©€λ²λΉ ν루 1μ§λ¬Έ |
member_question.priority CHECK (1~3) |
μ°μ μμ λ²μ |
reaction(answer_id, member_id) UNIQUE |
λ΅λ³λΉ λ©€λ² 1리μ‘μ |
family_report(family_id, type, start_date) UNIQUE |
리ν¬νΈ μ€λ³΅ λ°©μ§ |
question.level CHECK (1~4) |
λμ΄λ λ²μ |
member_question.level CHECK (1~4) |
λμ΄λ λ²μ |
member_question.status CHECK |
μνκ° μ ν |
| λ²μ | λ μ§ | λ³κ²½ λ΄μ© |
|---|---|---|
| v1.0 | 2025-08-20 | μ΄κΈ° μ€ν€λ§ |
| v2.0 | 2026-01-02 | κ°κ²°ν - QUESTIONμ ν νλ¦Ώ μ°Έμ‘°μ©μΌλ‘ λ³κ²½, MEMBER_QUESTIONμ΄ μ€μ μ§λ¬Έ μ μ₯, ANSWER_ANALYSIS μμ , REACTION/FAMILY_REPORT μΆκ° |
| v2.1 | 2026-01-02 | forbidden_keywords 보λ₯, MEMBER_PROFILE μμ , MEMBER.birthday μΆκ° |
| v2.2 | 2026-01-10 | MEMBER μ»¬λΌ μΈλΆν(nickname, role_type, label λ±) λ° MEMBER_QUESTION μ°μ μμ(priority) μ μ± λ°μ |