작성 이력
| 버전 | 날짜 | 내용 |
|---|---|---|
| v0.1 | 2026-09-28 | 초안 • 테이블 11개 • 관계도 • 컬럼 정의 • 상태 전이 규칙 • 결정 필요 사항 |
| v1.0 | 2026-09-28 | 결정 사항 확정 • 결정 #1~#6 확정(4장) • judgment 구현 규칙 추가(7장) • experience에 attempt_no · member_id 추가 및 유니크 제약 변경(재체험·회원 연결 대비) • penalty_rule을 법정형 / 선고 가능 범위로 분리 |
| v1.1 | 2026-09-28 | 비교 분석 실시간 생성(시퀀스 v0.2 안 B) 반영 • comparison_analysis 테이블 추가(확장 단계, 체험당 1개) • 관계도 · 서버 규칙 갱신 |
| v1.3 | 2026-09-29 | 문서 정합성 점검 결정 반영 (COMMON-4) • DB를 PostgreSQL로 확정(기술 스택 2장): 타입을 jsonb · timestamptz로, MySQL 관련 문구 삭제• judgment.references → reference_tags (SQL 예약어 회피)• 무죄( NOT_GUILTY) MVP 제외• 경합범 서비스 제외에 따라 예시 데이터(6장)를 단일 범행 사건으로 교체 • legal_case.thumbnail_url · deidentified_items(확장), factor.summary_tag, judgment.summary 추가, 범죄 분류명은 코드 상수 |
| v1.4 | 2026-09-30 | 대표 사건(살인) 가공 결정 반영 (BE-13) • penalty_type에 사형(DEATH) · 무기징역(LIFE) 추가. LIFE · DEATH 행의 allowed_min ~ allowed_max는 작량감경해 징역으로 선고할 때의 범위(무기 → 10 ~ 50년, 사형 → 20 ~ 50년)• judgment.reduced_to(감경 후 형벌) 추가, 형벌 종류별 CHECK · 선고 가능 범위 검증 규칙 갱신• sentence_range_option.kind에 LIFE · DEATH 추가• extra_dispositions.type 값 목록 명시, 몰수(CONFISCATION) 추가• penalty_rule 예 3(살인) 추가• reduced_to는 형벌 종류가 바뀌는 감경만 기록한다고 명시, 살인은 sentence_range_option에 FINE 구간을 두지 않음 |
| v1.5 | 2026-09-30 | 예시 사건을 가상 살인 사건으로 교체 (COMMON-11) • 6장 예시 데이터를 "빌린 돈 문제로 찾아온 지인을 살해한 사건"(가상)으로 전면 교체: penalty_rule 3행(사형 · 무기 · 징역), 살인용 sentence_range_option 8개, 판단 요소 11개, 세 판결 · 매트릭스 · 변화 유형• case_section · factor 설명의 사기 예시 문구 교체 |
| v1.2 | 2026-09-28 | 전체 문서 교차 검토 반영 • case_section.stage에 SUMMARY 추가 · 섹션 • 출처 명시, 공개 판단 유일 조건을 (case_id, subject_type)별로 정정 • 형벌 종류별 CHECK 제약 추가 • judgment.references 추가 (v1.3에서 reference_tags로 변경)• 선고 가능 하한 정의 명확화(법률상 감경 + 작량감경) · 벌금 예시 하한 25,000원 • last_reviewed_step 규칙 • 선택 FK 관계선 표기 • 서버 규칙 표 보완 |
| v1.6 | 2026-09-30 | BE-16 시드 반영 • 6장 sentence_range_option 예시를 V1이 넣은 실제 id(8 ~ 15) · display_order(2 ~ 9)로 정정, 사용자 사전 판단 예시 구간을 11로 정정 |
| v1.5 | 2026-09-30 | BE-2 마이그레이션 반영 • penalty_rule 유니크 (case_id, penalty_type), DEATH · LIFE 행 CHECK(법정형 NULL, 집행유예 불가)• experience CHECK(last_reviewed_step 0 ~ 4, attempt_no 1 이상), comparison_analysis.fail_reason CHECK 명시• judgment CHECK: 형량 값 양수(prison_months · fine_amount · suspension_months > 0), 사용자 판단은 항상 공개(PR #26 리뷰 반영)• 7장에 마이그레이션 공통 규칙 추가 • penalty_rule 컬럼 표 중간의 DEATH · LIFE 설명을 표 아래로 옮김(표가 끊겨 마지막 3개 컬럼이 표로 보이지 않던 문제) |
| v1.7 | 2026-10-01 | BE-3 엔티티 반영 — 7장 마이그레이션 공통 규칙에 jsonb 형식(배열 컬럼, case_section.data 원소 형식, 부가 처분 type 값) 추가 |
| v1.8 | 2026-10-01 | BE-3 리뷰 반영 — case_source 최종 확정 판결 부분 유니크(V6 마이그레이션) 추가, comparison_analysis는 PENDING일 때만 DONE · FAILED로 바뀜(엔티티 규칙) |
| v1.9 | 2026-10-02 | 사건 정보 섹션 표시 방식 팀 결정 반영 (BE-17) • case_section.content는 항목 하나를 한 줄로 쓰고 줄바꿈(\n)으로 나눈다. 화면은 줄바꿈을 그대로 보여 준다(7장 규칙 추가)• 양측 주장( PROSECUTOR · DEFENSE) 섹션 위 안내 문구는 DB에 두지 않고 화면 고정 문구로 둔다. 컬럼을 추가하지 않는다• penalty_rule.display_order 규칙 추가: 법조문 표기 순서대로 무거운 형벌부터(사형 → 무기 → 징역 → 벌금). 예시 · 가상 시드는 이미 이 순서이고 실제 사건 시드(비공개)를 맞춤 |
| v1.10 | 2026-10-06 | 판단 요소별 요약어 기준 확정 반영 (BE-26) • factor.summary_tag 설명을 "요소를 묶는 분류명"에서 "요소마다 붙이는 요약어"로 정정• 6장 factor 예시의 summary_tag 11개 값을 요소별 요약어로 교체(가상 시드 · 프론트 목과 같은 값) |
| v1.11 | 2026-10-07 | AI 판결 자동 파이프라인 · 후검수 반영 (BE-31) • ai_generation.generation_report(jsonb) 추가(V8 마이그레이션): 자동 생성 정보(검증 경고 · 사전 학습 점검 판정 · 회차 선택 · 실행 식별자 runKey)• 자동 적재는 AI 판결을 비공개( is_published=false) · PENDING으로, 사건을 DRAFT로 넣고 관리자가 검수 후 공개한다 |
| v1.12 | 2026-10-08 | COMMON-19 반영 (AI 판결 파이프라인 BE-31 ~ BE-45) • legal_case.title · factor(case_id, display_order) 유일 제약 설명 추가(V7, 적재 SQL의 조회 키)• legal_case.incident_date: 자동 파이프라인이 원문 · 선고일과 대조해 확인한 값을 넣는다(BE-38)• 자동 적재는 재판부 판결(COURT)도 비공개( is_published=false)로 넣고, 다시 돌리면 비공개 후보가 쌓일 수 있음을 명시(BE-38)• generation_report 내용 구체화: 점검 판정 · 분류별 개수 · 기준(criteria) 등. 코드 변경 없음, 설명만 정정 |
| v1.13 | 2026-10-08 | 판단 요소 가치관 축 추가 (BE-47) • factor.value_axis 추가(V9 마이그레이션, nullable): 판결 체험에서 고르는 판단 요소를 사용자 성향과 매칭하는 가치관 축 4개(APOLOGY_SINCERITY · FAULT_STANDARD · PRINCIPLE_RELATION · ORDER_OPPORTUNITY) 또는 NULL• 사전 판단( OVERVIEW)과 형량 선택은 성향 계산에 쓰지 않는다(계산하는 쪽에서 제외) |
| v1.14 | 2026-10-08 | 11 · 12차 회의 반영 (COMMON-20) • comparison_analysis: AI 비교 분석(REQ-063) 제외로 보류. V5 마이그레이션의 테이블은 그대로 두고 쓰지 않는다(삭제 여부 미정)• 8장 신설: 판결 성향 테스트 테이블 제안(미확정) — 문항 · 선택지 · 유형 · 결과 · 체험별 성향 기여. 판단 요소별 가치관 축은 새로 만들지 않고 factor.value_axis(v1.13, BE-47)를 쓴다 |
| v1.15 | 2026-10-08 | 판단 요소 가치관 축 후검수 반영 (BE-48) • factor.value_axis_status 추가(V10 마이그레이션, 기본 AUTO): AUTO(AI 투표 · 사람 초안 기본값) / CONFIRMED(관리자 확정, NULL 확정 포함). 시드 · 적재 SQL은 CONFIRMED 행을 덮어쓰지 않는다• factor.value_axis_votes(jsonb) 추가: 축 분류 투표 기록(BE-49). 최다표가 요청 횟수의 과반이 아니면(동률 · 유효 응답 부족 포함) needsReview |
- 판단은 하나의 테이블로. 사용자의 사전 판단·최종 판결, AI 판결, 재판부 판결을 모두
judgment한 테이블에subject_type+timing으로 구분해 저장한다(DR-1, DR-3). 배심원 의견 등은subject_type값만 추가하면 된다. - 판단 요소 기록도 하나의 형식으로. 누가 판단했든
judgment_factor한 테이블에 같은 형식(요소 · 방향)으로 저장한다(DR-2). 그래서 비교 매트릭스는 조회 한 번으로 만들 수 있다. - 사건마다 달라지는 내용은 유연하게. 범죄 유형별로 필드가 달라지는 사건 정보(피해 금액, 범행 기간 등)는 컬럼으로 고정하지 않고
case_section의 본문 + JSON 데이터로 담는다. - 진행 상태는 서버가 강제한다. 체험 진행은
experience.status로 관리하고, 상태는 앞으로만 이동한다(IA 9장). - 원본 판결문 정보는 내부 전용. 사건번호·법원명 등은
case_source에 따로 두고 사용자 응답에는 절대 포함하지 않는다(REQ-074, FR-5-3). - 나중에 열 기능은 컬럼만 미리. 재체험, 회원 연결, 배심원 관점처럼 MVP 이후 기능은 스키마를 바꾸지 않고 켤 수 있도록 컬럼과 값만 준비해 둔다.
erDiagram
SENTENCING_GUIDELINE |o--o{ LEGAL_CASE : "적용"
LEGAL_CASE ||--o{ CASE_SOURCE : "원본 판결문"
LEGAL_CASE ||--o{ CASE_SECTION : "사건 정보"
LEGAL_CASE ||--o{ PENALTY_RULE : "선고 가능 범위"
LEGAL_CASE ||--o{ FACTOR : "판단 요소 목록"
LEGAL_CASE ||--o{ JUDGMENT : "판단"
LEGAL_CASE ||--o{ EXPERIENCE : "체험"
SENTENCE_RANGE_OPTION |o--o{ JUDGMENT : "사전 판단 선택"
ANONYMOUS_USER ||--o{ EXPERIENCE : "체험"
EXPERIENCE |o--o{ JUDGMENT : "사용자 판단"
JUDGMENT ||--o{ JUDGMENT_FACTOR : "요소 평가"
FACTOR ||--o{ JUDGMENT_FACTOR : "평가 대상"
JUDGMENT ||--o| AI_GENERATION : "생성 기록"
EXPERIENCE ||--o| COMPARISON_ANALYSIS : "비교 분석 (확장)"
LEGAL_CASE {
bigint id PK
varchar title "사건 제목"
varchar crime_type "MURDER FRAUD INJURY"
varchar charge_name "죄명"
text overview "S-03 개요"
varchar thumbnail_url "S-02 카드 이미지"
varchar status "DRAFT REVIEW PUBLISHED"
bigint guideline_id FK
}
PENALTY_RULE {
bigint id PK
bigint case_id FK
varchar penalty_type "DEATH LIFE PRISON FINE"
bigint statutory_min "법정형 하한"
bigint statutory_max "법정형 상한"
bigint allowed_min "선고 가능 하한"
bigint allowed_max "선고 가능 상한"
}
FACTOR {
bigint id PK
bigint case_id FK
varchar label "판단 요소 문구"
varchar reveal_stage "OVERVIEW DETAIL ARGUMENT LAW"
varchar summary_tag "요약 태그"
varchar value_axis "가치관 축 (nullable)"
varchar value_axis_status "AUTO CONFIRMED"
jsonb value_axis_votes "축 분류 투표 기록 (nullable)"
int display_order
}
ANONYMOUS_USER {
uuid id PK
bigint member_id "회원 연결 1:N"
timestamptz created_at
timestamptz last_seen_at
}
EXPERIENCE {
bigint id PK
uuid anonymous_user_id FK
bigint case_id FK
int attempt_no "회차"
bigint member_id "체험 시작 시 로그인"
varchar status "STARTED ~ COMPLETED"
int last_reviewed_step
}
JUDGMENT {
bigint id PK
bigint case_id FK
varchar subject_type "USER AI COURT"
varchar timing "PRE FINAL"
bigint experience_id FK "USER만"
bigint range_option_id FK "PRE만"
varchar penalty_type "FINAL만"
varchar reduced_to "감경 후 형벌 LIFE PRISON"
int prison_months
bigint fine_amount
int suspension_months
varchar summary "카드 한 줄 요약 AI COURT"
}
COMPARISON_ANALYSIS {
bigint id PK
bigint experience_id FK "unique"
varchar status "PENDING DONE FAILED"
jsonb content "검증 통과한 분석"
varchar prompt_version
}
JUDGMENT_FACTOR {
bigint id PK
bigint judgment_id FK
bigint factor_id FK
varchar direction "UP DOWN 또는 NULL"
text evidence "재판부 근거 문장"
}
case는 SQL 예약어라legal_case로 둔다.
| 컬럼 | 타입 | 필수 | 설명 | 관련 |
|---|---|---|---|---|
| id | bigint PK | ✓ | ||
| title | varchar(100) | ✓ | 사건 제목 | REQ-006 |
| crime_type | varchar(20) | ✓ | MURDER / FRAUD / INJURY |
FR-1-2 |
| charge_name | varchar(100) | ✓ | 죄명 (예: 사기) | REQ-015 |
| short_intro | varchar(200) | ✓ | 목록 카드용 짧은 소개 | REQ-006 |
| keywords | jsonb | 목록 카드용 중립 키워드 배열 | FR-1-5 | |
| difficulty | varchar(10) | LOW / MID / HIGH |
FR-1-5 | |
| estimated_minutes | int | 예상 소요 시간 | FR-1-5 | |
| overview | text | ✓ | S-03 사건 개요 (뉴스 수준, 중립 표현) | REQ-015, 094 |
| thumbnail_url | varchar(300) | S-02 사건 카드 이미지 경로 (v1.3). 없으면 화면이 범죄 유형별 기본 이미지를 쓴다 | REQ-006 | |
| deidentified_items | jsonb | (확장) 비식별화한 항목 종류 배열 (예: ["인명", "지명", "사건번호", "업체명"]). 원래 값은 넣지 않는다 (v1.3) |
FR-5-2, REQ-055 | |
| applied_law | varchar(200) | ✓ | 적용 법조문 (예: 형법 제250조 제1항) — 고정 입력값 | REQ-042 |
| statutory_penalty_text | varchar(200) | ✓ | 법정형 안내 문구 (예: 10년 이하의 징역 또는 2천만 원 이하의 벌금) | REQ-023 |
| recommended_min_months | int | 권고 형량 하한 (개월) | REQ-034 | |
| recommended_max_months | int | 권고 형량 상한 (개월) | REQ-034 | |
| recommended_basis | text | 권고 범위 산출 근거 문구 | REQ-035 | |
| guideline_id | bigint FK | 적용 양형기준 버전 | REQ-080 | |
| incident_date | date | 사건 발생일 (양형기준 버전 판단 근거). 자동 파이프라인은 모델이 찾은 날짜를 판결문 원문 · 선고일과 대조해 확인된 값만 넣고, 원문에 날짜가 없으면 비운다(v1.12, BE-38) | FR-3-5-1 | |
| status | varchar(20) | ✓ | DRAFT / REVIEW / PUBLISHED. PUBLISHED만 사용자에게 노출 |
REQ-047, 075 |
| published_at | timestamptz | |||
| created_at, updated_at | timestamptz | ✓ |
- 권고 범위는 MVP에서 팀이 계산해 입력한 값을 그대로 쓴다. 자동 계산 로직은 확장 단계(결정 #4).
title은 유일하다(uk_legal_case_title, V7 · BE-15, v1.12). 적재 SQL이 제목으로 사건을 찾아 환경마다 id가 달라도 같은 SQL을 쓰기 위해서다. 모델이 만든 중립 제목이 다른 공개 · 검토 중 사건과 겹치면 자동 적재가 멈춘다(BE-31).- 화면에 보이는 범죄 분류명(예: "사기 / 재산범죄")은 컬럼으로 두지 않고
crime_type별 코드 상수로 둔다(MURDER→ 생명범죄,FRAUD→ 재산범죄,INJURY→ 신체범죄). API는crimeCategoryLabel로 내려준다(v1.3).
S-04 · S-05에 보여 줄 사건 정보를 섹션 단위로 저장한다(결정 #3). S-03 개요와 S-04 섹션 ①은 legal_case.overview 하나를 함께 쓰고, 이 테이블에 중복 저장하지 않는다.
| 컬럼 | 타입 | 필수 | 설명 |
|---|---|---|---|
| id | bigint PK | ✓ | |
| case_id | bigint FK | ✓ | |
| stage | varchar(20) | ✓ | 화면 단계: DETAIL / ARGUMENT / LAW / SUMMARY(S-05 전용) |
| section_type | varchar(30) | ✓ | 섹션 종류 (아래 표) |
| title | varchar(100) | 섹션 라벨 (예: 주요 사실관계) | |
| content | text | 본문. 항목이 여럿이면 한 줄에 하나씩 쓰고 줄바꿈(\n)으로 나눈다(v1.9) |
|
| data | jsonb | 구조화 데이터 (피해 결과 카드 값, 용어 설명 목록 등) | |
| display_order | int | ✓ | 같은 단계 안에서의 순서 |
| section_type | stage | 내용 | data 예시 |
|---|---|---|---|
FACTS |
DETAIL | 주요 사실관계 | — |
DAMAGE |
DETAIL | 피해 결과 요약 카드 | [{"label":"피해자 수","value":"1명"}, {"label":"피해 결과","value":"사망"}, {"label":"범행 도구","value":"집에 있던 흉기"}, ...] |
DEFENDANT |
DETAIL | 피고인 관련 주요 사실 | — |
SETTLEMENT |
DETAIL | 합의 · 피해 회복 | — |
PROSECUTOR |
ARGUMENT | 검사 측 주장 | — |
DEFENSE |
ARGUMENT | 피고인 · 변호인 측 주장 | — |
LAW_TERM |
LAW | 법률 · 양형기준 용어 설명 | [{"term":"기본영역","desc":"..."}] |
SUMMARY |
SUMMARY | S-05 핵심 사실 요약 | ["집으로 찾아온 지인 1명을 살해", "다투던 중 집에 있던 흉기를 사용", ...] |
- 적용 법률 · 법정형 · 권고 범위는
legal_case컬럼에서, 선고 가능 범위는penalty_rule에서 꺼내 LAW 단계에 함께 보여 준다. - 범죄 유형마다 섹션 종류가 달라지면
section_type값만 추가한다. PROSECUTOR·DEFENSE섹션 위의 안내 문구(판결문의 양형 이유에서 정리한 내용이라는 안내)는 이 테이블에 두지 않는다. 화면이 섹션 종류별 고정 문구로 보여 준다(v1.9, 정보 구조 S-04). 사건마다 문구가 달라져야 하면 그때 칸을 추가한다.
S-06에서 보여 줄 형벌 선택지, 그리고 선고 가능 범위 밖 판결을 막는 기준이다(FR-3-3 ②).
| 컬럼 | 타입 | 필수 | 설명 |
|---|---|---|---|
| id | bigint PK | ✓ | |
| case_id | bigint FK | ✓ | |
| penalty_type | varchar(20) | ✓ | 법정형에서 고르는 형벌: DEATH(사형) / LIFE(무기징역) / PRISON(유기징역) / FINE(벌금). 법정형에 있는 형벌만 행을 둔다. DEATH · LIFE는 v1.4. 무죄(NOT_GUILTY)는 MVP에서 뺐다(v1.3, 요구사항 15장) |
| statutory_min | bigint | 법정형 하한 (징역: 개월, 벌금: 원). 없으면 NULL. DEATH · LIFE는 NULL |
|
| statutory_max | bigint | 법정형 상한. DEATH · LIFE는 NULL |
|
| allowed_min | bigint | 선고 가능 하한 — 법정형 하한에 사실관계로 인정되는 법률상 감경(자수 · 심신미약 등)과 작량감경(재판상 감경, 형법 제53조)을 모두 적용했을 때의 값 | |
| allowed_max | bigint | 선고 가능 상한 — 법정형 상한. 누범 등 사실관계로 정해지는 가중이 있으면 반영한 값 | |
| allowed_basis | varchar(300) | 선고 가능 범위 산출 근거 (예: 작량감경 시 하한 1/2) | |
| suspension_allowed | boolean | ✓ | 이 형벌에 집행유예 입력을 보여 줄지 |
| display_order | int | ✓ | 형벌 표시 순서. 법조문 표기 순서대로 무거운 형벌부터 DEATH → LIFE → PRISON → FINE(v1.9). API 8 · S-06 형벌 버튼이 이 순서를 그대로 쓴다 |
DEATH·LIFE행 (v1.4):allowed_min~allowed_max는 그 형벌을 고른 뒤 작량감경해 징역으로 선고할 때의 범위(개월)다. 무기징역은 10년 ~ 50년(120 ~ 600, 형법 제55조 제1항 제2호), 사형은 20년 ~ 50년(240 ~ 600, 같은 항 제1호). 법률상 감경 사유도 있으면 그만큼 더 넓힌다. 사형을 감경해 무기징역으로 선고하는 경우는 형법 규정이라 코드 상수로 판단한다. 두 행 모두suspension_allowed = false(감경해도 징역 10년 이상).
- 예 1 (사기, 조문상 하한 없음):
PRISON법정형 NULL ~ 120 / 선고 가능 1 ~ 120,FINE법정형 NULL ~ 20,000,000 / 선고 가능 25,000 ~ 20,000,000statutory_min이 NULL이면 조문에 하한이 없다는 뜻이다. 이때도 형법의 일반 하한(징역 1개월, 제42조 / 벌금 5만 원, 제45조)이 적용된다.- 징역은 1개월보다 낮게 선고할 수 없어 1이다. 벌금은 감경하면 5만 원 미만으로 할 수 있고(제45조 단서) 감경 시 1/2이 되므로(제55조 제1항 제6호) 25,000이다. 사건 등록 시 법조문으로 다시 확인한다.
- 예 2 (법정형 1년 이상 10년 이하, 법률상 감경 사유 없음):
PRISON법정형 12 ~ 120 / 선고 가능 6 ~ 120 (작량감경 1/2). 법률상 감경 사유도 있으면 3 ~ 120 - 예 3 (살인, 사형 · 무기 또는 5년 이상 징역, 법률상 감경 · 가중 사유 없음, v1.4):
PRISON법정형 60 ~ 360 / 선고 가능 30 ~ 360,LIFE법정형 NULL / 감경 시 120 ~ 600,DEATH법정형 NULL / 감경 시 240 ~ 600. 유기징역을 고르면 징역 상한은 30년이고, 무기 · 사형을 고른 뒤 감경하면 50년이 된다 - 재판부가 실제로 감경했는지와 관계없이 가장 넓은 범위를 쓴다. 판결 전 화면에서 재판부의 감경 여부가 드러나지 않게 하기 위해서다.
- 경합범 가중(형법 제38조)은 반영하지 않는다. 경합범(같은 죄를 여러 번 저지른 동종 경합범 포함) 사건은 서비스 대상에서 뺐으므로(REQ-081, v1.3) 상한은 단일 범행 기준이다.
- MVP는 팀이 판결문 · 법조문을 확인해 계산한 값을 입력한다. 자동 계산은 확장 단계.
- 집행유예 가능 조건(선고형 3년 이하 징역 또는 500만 원 이하 벌금, 기간 1~5년, 형법 제62조 본문)은 법 규정이라 테이블이 아니라 코드 상수로 둔다. 같은 조 단서의 결격 사유(금고 이상 형 확정 후 집행 종료 · 면제 뒤 3년 안에 범한 죄)는 사건마다 다르므로
suspension_allowed에 반영해 입력한다. - 유니크 제약: (
case_id,penalty_type) — 사건마다 형벌 종류별 규칙은 1개 (API 8 · 9가 형벌 종류로 규칙 하나를 찾는다, v1.5) - CHECK:
DEATH·LIFE이면statutory_min·statutory_max가 NULL이고suspension_allowed = false(v1.5)
사용자 · AI · 재판부가 함께 쓰는 공통 목록(FR-3-4).
| 컬럼 | 타입 | 필수 | 설명 | 관련 |
|---|---|---|---|---|
| id | bigint PK | ✓ | ||
| case_id | bigint FK | ✓ | REQ-031 | |
| label | varchar(100) | ✓ | 판단 요소 문구 (예: 다투던 중 집에 있던 흉기를 집어 들었다) | REQ-076 |
| pre_label | varchar(100) | 사전 판단용 짧은 문구 (예: 피해 금액이 수천만 원이다). OVERVIEW 요소만 |
REQ-093 | |
| reveal_stage | varchar(20) | ✓ | 처음 알게 되는 단계: OVERVIEW / DETAIL / ARGUMENT / LAW |
REQ-095 |
| summary_tag | varchar(20) | ✓ | 요약 태그 (v1.3 추가, v1.10 확정). 여러 요소를 묶는 분류명이 아니라 요소마다 붙이는 짧은 요약어다(예: 요소 "다투던 중 집에 있던 흉기를 집어 들었다" → 흉기 사용). S-09 "내 판결" 한 줄 요약과 세 판결 비교 규칙 문장(API 14 ruleSentences)에 쓴다 |
REQ-060 |
| value_axis | varchar(20) | 가치관 축 (v1.13 추가, BE-47). 사용자가 이 요소를 고르는 것으로 드러나는 성향을 매칭하는 축: APOLOGY_SINCERITY(사과와 진정성: 반성 · 자수 · 수사 협조 · 사후 정황) / FAULT_STANDARD(잘잘못의 기준: 범행 동기 · 수단 · 방법 · 계획성 · 결과의 중대성) / PRINCIPLE_RELATION(원칙과 관계: 피해 회복 · 합의 · 처벌불원 · 피해자 과실) / ORDER_OPPORTUNITY(질서와 기회: 전과 · 연령 · 가족 · 부양 · 직업 · 사회적 유대). 어느 축에도 맞지 않으면 NULL |
||
| value_axis_status | varchar(20) | ✓ | 가치관 축 후검수 상태 (v1.15 추가, BE-48): AUTO(기본값. AI 투표 · 사람 초안, 아직 관리자가 확정하지 않음) / CONFIRMED(관리자 확정, NULL로 확정한 것 포함). 기본 AUTO |
|
| value_axis_votes | jsonb | 축 분류 투표 기록 (v1.15 추가, BE-49). 예: {"runs": 5, "counts": {"FAULT_STANDARD": 3, "NONE": 2}, "needsReview": false}. counts는 표를 받은 축만, NULL 표는 NONE 키. requestedRuns(선택)는 요청한 횟수로, runs(유효 응답 수)와 다를 수 있다. needsReview는 최다표가 요청 횟수의 과반이 아니면(동률 · 유효 응답 부족 포함) true. 사람 초안 · 투표 없이 정한 값은 NULL |
||
| display_order | int | ✓ |
(case_id, display_order)는 유일하다(uk_factor_case_display_order, V7 · BE-15, v1.12). 적재 SQL이 요소를 번호와 문구로 찾기 때문이다.value_axis는 AI(사건 추출기 · 파이프라인 축 분류 투표) 또는 사람 초안(비공개 시드)이 기본값을 정하고, 관리자가 후검수로 확정한다(CHECK는 네 값 또는 NULL). 값이 NULL인 요소는 성향 계산에서 빠진다. 사전 판단(OVERVIEW)과 형량 선택은 성향 계산에 쓰지 않는다.- 값만으로는 "관리자가 일부러 NULL로 둔 요소"와 "아직 채우지 않은 요소"를 구분할 수 없어서
value_axis_status를 둔다. 시드(R__15)와 파이프라인 적재 SQL(이미 적재된 사건이면 번호 · 라벨이 같은 요소의 축 · 투표 기록만 갱신)은AUTO행만 바꾸고, 관리자가 고치거나 그대로 승인하면CONFIRMED가 된다(BE-43). AUTO행의 우선순위: 투표 기록이 있는 값이 우선이다. 시드(R__15)는 투표 기록이 없는 행만 사람 초안으로 맞추고, 적재 SQL은 투표 기록이 있는 값만 이미 적재된 사건에 반영한다. 그래서 축 단계를 건너뛴 적재나 시드 재실행이 투표 결과를 되돌리지 않는다. 사람이 값을 바꾸는 경로는 관리자 확정 하나다.summary_tag는 사건별로 팀이 붙인다. 요소마다 다른 요약어를 붙이는 것이 기본이지만, 같은 사건 안에 뜻이 겹치는 요소가 있으면 같은 태그를 쓸 수도 있다(그때는 UserSummarySentence · RuleSentences가 중복을 한 번만 쓴다). 태그 문구도 판단 요소와 같이 중립적으로 쓴다(FR-3-4).
| 컬럼 | 타입 | 필수 | 설명 |
|---|---|---|---|
| id | bigint PK | ✓ | |
| case_id | bigint FK | ✓ | |
| court_level | varchar(20) | ✓ | FIRST / APPEAL / SUPREME |
| case_number | varchar(50) | ✓ | 사건번호 |
| court_name | varchar(50) | 법원명 | |
| decided_at | date | 선고일 | |
| is_final | boolean | ✓ | 최종 확정 판결 여부 (REQ-054) |
| source_org | varchar(50) | ✓ | 출처 기관 (사용자에게 노출 가능한 유일한 컬럼) |
| original_text | text | 판결문 원문 또는 저장 위치 | |
| note | text | 가공 · 검수 메모 |
- 1심과 항소심 판결문을 모두 쓰는 사건은 행을 2개 둔다(FR-2-2).
- 부분 유니크 제약:
case_idWHEREis_final = true— 사건마다 최종 확정 판결은 1건 (V6, v1.8)
| 컬럼 | 타입 | 필수 | 설명 |
|---|---|---|---|
| id | bigint PK | ✓ | |
| crime_category | varchar(50) | ✓ | 예: 살인범죄 |
| version_name | varchar(50) | ✓ | 예: 2024 개정 |
| effective_date | date | ✓ | 시행일 |
| source_url | varchar(300) |
S-03의 객관식 선택지. 범죄 유형별로 정의한다(FR-2-8).
| 컬럼 | 타입 | 필수 | 설명 |
|---|---|---|---|
| id | bigint PK | ✓ | |
| crime_type | varchar(20) | ✓ | |
| label | varchar(50) | ✓ | 예: 실형 3년 이상 ~ 5년 미만 |
| kind | varchar(20) | ✓ | FINE / SUSPENDED / PRISON / LIFE / DEATH (LIFE · DEATH는 v1.4, 살인 등 법정형에 있는 범죄 유형만) |
| min_months | int | 실형 구간 하한 | |
| max_months | int | 실형 구간 상한 | |
| display_order | int | ✓ |
kind와 개월 범위가 있어야 S-09에서 "처음 생각보다 가벼운/무거운 판결"을 계산할 수 있다.- 법정형에 벌금이 없는 범죄 유형(살인)은
FINE구간을 두지 않는다. 살인은 집행유예부터 사형까지 8개다(요구사항 FR-2-8, v1.4). - 무겁기 순서:
FINE<SUSPENDED<PRISON(개월 순) <LIFE<DEATH(v1.4). 최종 판결은reduced_to가 있으면 그 값으로 비교한다.
| 컬럼 | 타입 | 필수 | 설명 |
|---|---|---|---|
| id | uuid PK | ✓ | 브라우저에 저장하는 익명 ID |
| member_id | bigint | 이 브라우저에서 로그인한 회원. 회원 1명에 익명 ID 여러 개가 연결될 수 있다(1:N). MVP에서는 비워 둠 | |
| created_at | timestamptz | ✓ | |
| last_seen_at | timestamptz | ✓ |
- 회원 기능(이후 단계)에서 로그인하는 순간 해당 브라우저의
member_id를 채운다. 그러면 비로그인 때 한 체험이 회원 기록으로 이어진다. member테이블은 회원 기능 설계 때 추가한다.
IA 9장의 진행 상태를 저장한다.
| 컬럼 | 타입 | 필수 | 설명 |
|---|---|---|---|
| id | bigint PK | ✓ | |
| anonymous_user_id | uuid FK | ✓ | |
| case_id | bigint FK | ✓ | |
| attempt_no | int | ✓ | 이 사건의 몇 번째 체험인지. 기본값 1. MVP에서는 항상 1 |
| member_id | bigint | 체험을 시작할 때 로그인 상태였다면 회원 ID. MVP에서는 비워 둠 | |
| status | varchar(30) | ✓ | STARTED / PRE_JUDGED / REVIEWING / REVIEWED / VERDICT_CONFIRMED / AI_REVEALED / COMPLETED |
| last_reviewed_step | int | ✓ | S-04에서 확인을 마친 마지막 섹션 번호 (0 ~ 4). 기본값 0(STARTED). 사전 판단 제출 시 1(섹션 ① 개요는 S-03에서 본 것으로 처리), 섹션 ② ~ ④ 확인 시 2 ~ 4. 새로고침 복원용 |
| started_at | timestamptz | ✓ | |
| pre_judged_at | timestamptz | ||
| reviewed_at | timestamptz | ||
| verdict_confirmed_at | timestamptz | ||
| ai_revealed_at | timestamptz | ||
| completed_at | timestamptz | ||
| updated_at | timestamptz | ✓ |
- 유니크 제약: (
anonymous_user_id,case_id,attempt_no) - CHECK:
last_reviewed_step은 0 ~ 4,attempt_no는 1 이상 (v1.5) - MVP에서는
attempt_no = 1만 만든다. 같은 브라우저에서 이미 체험을 시작한 사건에 들어오면 새 체험을 만들지 않고 기존 체험의 진행 단계로 보낸다. 완료(COMPLETED)한 사건이면 S-09 결과 화면으로 보낸다. - 통계와 참여자 비교에는
attempt_no = 1인 체험만 쓴다. 두 번째 체험부터는 실제 판결을 이미 알고 있는 상태이기 때문이다. - 체험 당시 로그인 여부 구분:
| 상황 | anonymous_user.member_id | experience.member_id | 해석 |
|---|---|---|---|
| 비로그인으로 체험, 가입 전 | 비어 있음 | 비어 있음 | 익명 체험 |
| 비로그인으로 체험 → 나중에 로그인 | 채워짐 | 비어 있음 | 로그인 전에 한 체험 (회원 기록으로 이어짐) |
| 로그인한 상태로 체험 | 채워짐 | 채워짐 | 로그인 후 체험 |
| 컬럼 | 타입 | 필수 | 설명 |
|---|---|---|---|
| id | bigint PK | ✓ | |
| case_id | bigint FK | ✓ | |
| subject_type | varchar(20) | ✓ | USER / AI / COURT (이후 JURY 등) |
| timing | varchar(10) | ✓ | PRE / FINAL. PRE는 USER만 |
| experience_id | bigint FK | USER일 때만 |
|
| range_option_id | bigint FK | PRE일 때만 (사전 판단 형량 구간) |
|
| penalty_type | varchar(20) | 법정형에서 고른 형벌: DEATH / LIFE / PRISON / FINE (DEATH · LIFE는 v1.4). FINAL일 때 필수 (무죄는 MVP 제외, v1.3) |
|
| reduced_to | varchar(20) | (v1.4) 감경 후 형벌. DEATH를 감경하면 LIFE 또는 PRISON, LIFE를 감경하면 PRISON. 감경하지 않았거나 PRISON · FINE이면 NULL. 형벌 종류가 바뀌는 감경만 기록한다(유기징역 안의 작량감경은 기록하지 않는다). 최종 선고 형벌 = reduced_to가 있으면 그 값, 없으면 penalty_type |
|
| prison_months | int | 징역 개월 | |
| fine_amount | bigint | 벌금 (원) | |
| suspension_months | int | 집행유예 기간 (개월). 없으면 NULL | |
| extra_dispositions | jsonb | 부가 처분 (예: [{"type":"COMMUNITY_SERVICE","value":"80시간"}]). 주로 COURT. type 값: COMMUNITY_SERVICE(사회봉사), CONFISCATION(몰수, v1.4). 필요하면 값을 추가한다 |
|
| summary | varchar(100) | S-09 판결 카드의 한 줄 요약 (v1.3). AI · COURT는 팀이 입력. USER는 저장하지 않고 서버가 factor.summary_tag로 규칙 문장을 만들어 응답한다(API 판결 응답 공통 형식) |
|
| reasoning | text | 판결 이유 요약 (AI · COURT) | |
| plain_explanation | text | 쉬운 설명 (COURT) | |
| excerpt | text | 판결문 발췌 (COURT, 비식별화 적용) | |
| free_opinion | text | 자유 의견 (USER, 비교 대상 아님) | |
| reference_tags | jsonb | 참고 자료 태그 (AI, 예: ["형법 제250조", "살인범죄 양형기준", "유사 판례 5건"]). S-07에 표시 (v1.2 추가, v1.3에서 references → reference_tags: REFERENCES는 SQL 예약어) |
|
| is_published | boolean | ✓ | AI · COURT는 검수 후 true만 노출. USER는 항상 true |
| created_at | timestamptz | ✓ |
제약 조건
| 제약 | 내용 | 지키는 규칙 |
|---|---|---|
유니크 (experience_id, timing) |
한 체험에 사전 판단 1개, 최종 판결 1개 | 수정·재제출 불가 (IA 결정 #4) |
CHECK: timing = PRE |
subject_type = USER, range_option_id 필수, penalty_type · prison_months · fine_amount · suspension_months는 NULL |
사전 판단 형식 |
CHECK: timing = FINAL |
penalty_type 필수, range_option_id는 NULL |
최종 판결 형식 |
| CHECK: 형벌 종류별 값 (v1.2) | PRISON → prison_months 필수 · fine_amount NULL / FINE → fine_amount 필수 · prison_months NULL (v1.3: NOT_GUILTY 조건 삭제) |
AI · COURT 판결을 SQL로 넣을 때도 형식 보장 |
| CHECK: 사형 · 무기 (v1.4) | reduced_to는 penalty_type = DEATH일 때 LIFE · PRISON, LIFE일 때 PRISON만 가능하고 그 밖에는 NULL / 최종 선고 형벌이 DEATH · LIFE면 prison_months · fine_amount · suspension_months NULL / 최종 선고 형벌이 PRISON이면 prison_months 필수 · fine_amount NULL / penalty_type이 DEATH · LIFE면 reduced_to와 관계없이(감경해 PRISON이 돼도) suspension_months NULL |
사형 · 무기 판결 형식 |
| CHECK: 주체와 체험 (v1.2) | subject_type = USER ⇔ experience_id NOT NULL |
사용자 판단은 체험에 속함 |
| CHECK: 형량 값 양수 (v1.5) | prison_months · fine_amount · suspension_months는 NULL이거나 0보다 큼 |
AI · COURT 판결을 SQL로 넣을 때 0 · 음수 방지 (집행유예 1 ~ 5년 등 법 규정은 코드 상수) |
| CHECK: 사용자 판단 공개 (v1.5) | subject_type = USER이면 is_published = true |
사용자 판단은 검수 대상이 아님 |
| 공개 판단 1개 | subject_type ∈ {AI, COURT} 이고 is_published = true인 행은 (case_id, subject_type)별로 1개 — 사건마다 공개 AI 판결 1개, 공개 실제 판결 1개. PostgreSQL 부분 유니크 인덱스로 구현(기술 스택 2장 uk_judgment_published) |
모든 사용자에게 같은 AI 판결 (REQ-046) |
| 수정 없음 | USER 판단은 INSERT만 하고 UPDATE API를 두지 않음 |
IA 결정 #4 |
| 선고 가능 범위 | USER FINAL 저장 시 penalty_rule.allowed_min ~ allowed_max 밖이면 거절 (서비스 로직). 비교하는 행은 penalty_type(고른 형벌)의 행이다. 무기 · 사형을 감경해 징역으로 선고하면 그 행의 감경 범위로 검사한다 (v1.4) |
FR-3-3 ② |
| 컬럼 | 타입 | 필수 | 설명 |
|---|---|---|---|
| id | bigint PK | ✓ | |
| judgment_id | bigint FK | ✓ | |
| factor_id | bigint FK | ✓ | |
| direction | varchar(10) | UP / DOWN. 사전 판단(PRE)은 방향 없이 NULL |
|
| evidence | text | 재판부 판단의 근거 문장 (COURT, REQ-077) |
- 유니크 제약: (
judgment_id,factor_id) - 행이 없으면 "고려하지 않음"(—)으로 본다(결정 #1). 요구사항 DR-2의 "고려 여부"는 행의 존재 여부로 표현한다.
- 사전 판단(
PRE)에는reveal_stage = OVERVIEW인 요소만 저장할 수 있다.
AI 판결 1건을 어떤 조건으로 만들었는지 남긴다(REQ-047, 079). MVP에서는 오프라인으로 생성한 결과를 넣을 때 최소 항목만 채운다.
| 컬럼 | 타입 | 필수 | 설명 |
|---|---|---|---|
| id | bigint PK | ✓ | |
| judgment_id | bigint FK, unique | ✓ | 대상 AI 판단 |
| model_name | varchar(50) | ✓ | |
| prompt_version | varchar(30) | ✓ | |
| input_snapshot | jsonb | AI에 넣은 입력 (실제 판결이 없는지 검증용, REQ-041) | |
| raw_output | jsonb | 모델 원본 출력 | |
| review_status | varchar(20) | ✓ | PENDING / APPROVED / REJECTED |
| reviewed_by | varchar(50) | 검수자 | |
| reviewed_at | timestamptz | ||
| generation_report | jsonb | 자동 생성 정보 (v1.11, BE-31): 검증 경고 · 사전 학습 점검(판정 · 근거 · 분류별 개수 · 기준 criteria, 점검을 안 했으면 SKIPPED, BE-37) · 회차 선택(전략 · 이유 · 점수) · 모델(요청 모델 · 실제 응답 모델) · 토큰 · 실행 식별자(runKey, 같은 적재 SQL 중복 실행 방지, 부분 유니크 인덱스 ux_ai_generation_run_key). 예측 형량 · 실제 판결 값은 넣지 않는다. 사람이 검수해 넣은 행은 NULL |
|
| created_at | timestamptz | ✓ |
- 자동 파이프라인(BE-31)은 재판부 판결(COURT)도 비공개(
judgment.is_published=false)로 넣는다(BE-38). 기존 공개 판결은 건드리지 않고, 파이프라인을 다시 돌리면 같은 사건에 비공개 COURT 후보가 하나 더 쌓일 수 있다(기존 행은 지우지 않음, REQ-079와 같은 원칙). 공개할 후보를 고르는 일은 후검수(BE-33 이후, 미구현)가 한다. COURT 판결에는 검수 상태 컬럼이 아직 없다. - 자동 파이프라인(BE-31)은 AI 판결을 비공개(
judgment.is_published=false) ·review_status='PENDING'으로 넣는다. 관리자가 검수해APPROVED로 바꾸고 공개 판단을 교체한다(후검수).REJECTED는 공개하지 않는다. - 모델·프롬프트가 바뀌면 새
judgment+ 새ai_generation을 만들고, 검수 후 공개 판단을 교체한다. 기존 행은 지우지 않는다(REQ-079).
(v1.13) 보류 — AI 비교 분석(REQ-062 · 063)을 11차 회의에서 확장 범위에서 제외했다. V5 마이그레이션으로 만든 테이블은 그대로 두고 쓰지 않는다. 아래 정의는 결정 기록으로 남긴다.
체험 1건의 세 판결을 AI가 비교한 결과를 저장한다(FR-6-3). 사용자 판결이 체험마다 다르므로 체험별로 실시간 생성한다(시퀀스 8장). AI 판결(ai_generation)과 달리 공개 전 검수가 없으므로, 서버 검증을 통과한 결과만 DONE으로 저장한다.
| 컬럼 | 타입 | 필수 | 설명 |
|---|---|---|---|
| id | bigint PK | ✓ | |
| experience_id | bigint FK, unique | ✓ | 대상 체험. 체험당 1개 |
| status | varchar(20) | ✓ | PENDING(생성 중) / DONE(검증 통과) / FAILED(검증 실패 · 오류 · 시간 초과 → 화면은 규칙 문장 유지) |
| content | jsonb | 검증을 통과한 분석. 항목마다 근거 요소 ID(factor.id) 포함 |
|
| fail_reason | varchar(30) | INVALID_FACTOR / FORBIDDEN_EXPRESSION / INVALID_FORMAT / TIMEOUT / API_ERROR |
|
| model_name | varchar(50) | ✓ | |
| prompt_version | varchar(30) | ✓ | |
| input_snapshot | jsonb | AI에 넣은 입력 (사건 원문 · 판결문 전문이 없는지 점검용) | |
| raw_output | jsonb | 모델 원본 출력 (실패 원인 분석 · 샘플 점검용) | |
| created_at | timestamptz | ✓ | 생성 시작 (S-08 진입, AI_REVEALED) |
| completed_at | timestamptz | DONE · FAILED가 된 시각 |
- 행은 실제 판결 공개(
AI_REVEALED) 시PENDING으로 만들고, 생성 작업이 끝나면DONE또는FAILED로 바꾼다. 유니크 제약으로 동시 요청이 와도 한 번만 생성한다. - CHECK:
fail_reason은 위 표의 5개 값만 허용 (v1.5) FAILED는 자동으로 다시 만들지 않는다(MVP와 같은 규칙 문장이 보이므로 체험은 정상). 재시도 정책은 기술 설계에서 정한다.- 비교 분석은
COMPLETED상태에서만 응답한다. content.perspectives.USER는 확장 단계의 AI "내 판결" 요약이다(REQ-062, v1.3).DONE이면 S-09 내 판결 카드의 한 줄 요약(MVP:summary_tag규칙 문장)을 이 값으로 바꾼다. 따로 테이블을 두지 않고 이 행에 함께 저장한다.
| # | 항목 | 결정 | 비고 |
|---|---|---|---|
| 1 | "고려하지 않음" 저장 방식 | 고려한 요소만 judgment_factor 행으로 저장. 행이 없으면 "—" |
"명시적으로 고려하지 않음"을 구분할 필요가 생기면 considered 컬럼 추가 |
| 2 | 사전 판단 저장 위치 | judgment에 timing = PRE로 저장 |
빈 컬럼은 CHECK 제약과 구현 규칙으로 관리 (7장) |
| 3 | 사건 정보 저장 방식 | case_section (본문 + JSON) |
대표 판례 확정 후 필요하면 재검토 |
| 4 | 권고 형량 범위 저장 위치 | legal_case 컬럼에 팀이 계산한 값 입력 |
자동 계산은 확장 단계. 선고 가능 범위(penalty_rule)도 같은 방식 |
| 5 | 사건당 체험 횟수 | MVP는 1회 (같은 브라우저 기준). attempt_no · member_id로 재체험 · 회원 연결 대비 |
통계는 attempt_no = 1만 사용 |
| 6 | JSON 컬럼 사용 | 사용 (keywords, deidentified_items, data, extra_dispositions, reference_tags, input_snapshot, raw_output, content) | v1.3: DB는 PostgreSQL로 확정(기술 스택 2장). JSON 컬럼은 jsonb, 시각은 timestamptz |
| 항목 | 결정 |
|---|---|
| 범위 밖 판결 | 확정 불가 — 화면에서 확정 버튼 비활성 + 서버에서 저장 거절 |
| 기준 | 법정형이 아니라 선고할 수 있는 가장 넓은 범위 (penalty_rule.allowed_min ~ allowed_max) |
| 하한 | 법정형 하한에 사실관계로 인정되는 법률상 감경과 작량감경(재판상 감경)을 모두 적용한 값 |
| 상한 | 법정형 상한. 누범처럼 사실관계로 정해지는 가중이 있으면 반영 |
| 사형 · 무기 (v1.4) | 법정형에 있으면 선택지로 둔다. 그대로 선고하거나 작량감경해 징역으로 선고할 수 있고, 이때 징역 범위는 무기 10 ~ 50년, 사형 20 ~ 50년이다. 유기징역을 고른 경우보다 상한이 높아진다 |
| 이유 | 재판부가 실제로 감경했는지가 판결 전에 드러나지 않게 하고, 나중에 가중·감경 로직을 붙이기 쉽게 하기 위해 |
| 주의 | 계산 규칙은 사건 등록 시 판결문 · 법조문 · 양형기준으로 다시 확인한다 |
IA 9장의 규칙을 어느 테이블·제약이 책임지는지 정리한다.
| 규칙 | 담당 |
|---|---|
| 상태는 앞으로만 이동 | experience.status 전이를 서비스 로직에서 검증 (이전 상태로 되돌리는 API 없음) |
| 사전 판단은 한 번만 | status = STARTED일 때만 저장 허용 + 유니크 (experience_id, timing) |
| 최종 판결은 한 번만 | status = REVIEWED일 때만 저장 허용 + 유니크 (experience_id, timing) |
| 선고 가능 범위 밖 판결 거절 | penalty_rule.allowed_min ~ allowed_max 검증 (고른 형벌의 행 기준, v1.4) + reduced_to 허용 조합 검증 |
| AI 판결은 확정 후에만 | status ≥ VERDICT_CONFIRMED일 때만 AI judgment 응답 |
| 비교 분석 체험당 1회 생성 (확장) | comparison_analysis.experience_id 유니크, COMPLETED일 때만 응답 |
| 실제 판결은 AI 다음에 | status ≥ AI_REVEALED일 때만 COURT judgment 응답 |
| 사전 판단은 비교 화면에서만 | status = COMPLETED일 때만 PRE 판단 응답 |
| 새로고침 복원 | experience.status + last_reviewed_step |
| 섹션 확인 순서 (서버 강제) | last_reviewed_step + 1번 섹션만 확인 가능. 잠긴 섹션 본문은 응답하지 않음 (REQ-019) |
| 체험 중복 방지 | 같은 (anonymous_user_id, case_id)의 체험이 있으면 새로 만들지 않고 기존 체험으로 이동. 완료했으면 S-09 |
| 원본 판결문 비노출 | case_source는 사용자 API에서 조회하지 않음 (source_org만 예외) |
| 검수된 결과만 노출 | legal_case.status = PUBLISHED, judgment.is_published = true |
설명용으로 지어낸 가상 사건이다(빌린 돈 문제로 찾아온 지인 1명을 살해, 범행 1회). 실제 사건 데이터는 저장소에 두지 않고 배포 때 따로 넣는다. v1.3 ~ v1.4의 예시(지인 투자금 편취, 사기)는 MVP 최초 목표가 살인 사건 1건 완성이라 v1.5에서 바꿨다. 와이어프레임 v2.1은 아직 사기 예시 기준이다.
법정형과 선고 가능 범위는 형법 조문 그대로이고, 형량 · 판단 요소 방향 · 권고 범위는 지어낸 값이다. 실제 사건을 등록할 때 팀이 다시 계산한다.
사건 설정
| 항목 | 값 |
|---|---|
title |
빌린 돈 문제로 찾아온 지인을 살해한 사건 |
crime_type / charge_name |
MURDER / 살인 |
applied_law |
형법 제250조 제1항 |
statutory_penalty_text |
사형, 무기 또는 5년 이상의 징역 |
overview |
피고인이 빌린 돈을 갚지 못해 오래 다투던 지인이 집으로 찾아오자, 말다툼 끝에 집에 있던 흉기로 피해자를 살해하고 구호 조치 없이 집을 나간 사건이다. |
| 권고 범위 | 84 ~ 144 (징역 7년 ~ 12년) |
recommended_basis |
살인범죄 제2유형(보통 동기 살인). 특별감경인자 1개(실질적 피해 회복 — 5,000만 원 공탁)가 있고 특별가중인자는 없어 감경영역을 적용한다. 계획 없이 다투던 중 벌어진 범행이라 특별가중인자인 '계획적 살인 범행'에 해당하지 않는다. |
penalty_rule
| penalty_type | statutory_min | statutory_max | allowed_min | allowed_max | allowed_basis | suspension_allowed |
|---|---|---|---|---|---|---|
| DEATH | NULL | NULL | 240 | 600 | 사형을 작량감경하면 무기 또는 징역 20 ~ 50년 (형법 제55조 제1항 제1호) | false |
| LIFE | NULL | NULL | 120 | 600 | 무기징역을 작량감경하면 징역 10 ~ 50년 (같은 항 제2호) | false |
| PRISON | 60 | 360 | 30 | 360 | 유기징역 선택, 작량감경 시 하한 1/2. 법률상 감경 · 가중 사유 없음 | true |
→ 벌금은 법정형에 없어 FINE 행을 두지 않는다. 징역 31년(372개월)을 입력하면 allowed_max 360을 넘으므로 확정 불가(S-06c). 무기징역을 고른 뒤 감경해 징역 50년으로 선고하면 penalty_type = LIFE, reduced_to = PRISON, prison_months = 600이고, LIFE 행의 120 ~ 600으로 검사한다.
sentence_range_option (crime_type = MURDER, 8개)
| id | label | kind | min_months | max_months | display_order |
|---|---|---|---|---|---|
| 8 | 징역형 집행유예 | SUSPENDED | NULL | NULL | 2 |
| 9 | 실형 3년 미만 | PRISON | NULL | 36 | 3 |
| 10 | 실형 3년 이상 ~ 5년 미만 | PRISON | 36 | 60 | 4 |
| 11 | 실형 5년 이상 ~ 10년 미만 | PRISON | 60 | 120 | 5 |
| 12 | 실형 10년 이상 ~ 20년 미만 | PRISON | 120 | 240 | 6 |
| 13 | 실형 20년 이상 | PRISON | 240 | NULL | 7 |
| 14 | 무기징역 | LIFE | NULL | NULL | 8 |
| 15 | 사형 | DEATH | NULL | NULL | 9 |
→ 살인은 법정형에 벌금이 없어 FINE 구간을 빼고 LIFE · DEATH를 더해 8개다(요구사항 FR-2-8).
→ id · display_order는 V1 마이그레이션이 넣은 실제 값이다. V1은 모든 범죄 유형의 구간을 사기(1 ~ 7) → 살인(8 ~ 15) → 상해(16 ~ 22) 순서로 넣고, 공통 7개 구간의 순서(벌금형 = 1)를 그대로 쓴 뒤 살인에서 벌금형(1)만 빼므로 살인의 display_order는 2부터 시작한다. 화면은 display_order 오름차순으로 보여 주면 된다.
factor
| id | label | reveal_stage | summary_tag |
|---|---|---|---|
| 1 | 빌린 돈을 갚지 못해 오래 다툼이 있었다 | OVERVIEW | 채무 다툼 |
| 2 | 다투던 중 집에 있던 흉기를 집어 들었다 | OVERVIEW | 흉기 사용 |
| 3 | 범행 뒤 구호 조치 없이 현장을 떠났다 | OVERVIEW | 구호 조치 없음 |
| 4 | 사건 3개월 전부터 변제 문제로 여러 차례 다퉜다 | DETAIL | 반복된 다툼 |
| 5 | 유족이 엄벌을 원한다 | DETAIL | 유족의 엄벌 의사 |
| 6 | 피해자에게는 부양하던 어린 자녀 2명이 있다 | DETAIL | 피해자의 부양 가족 |
| 7 | 수사 초기부터 범행을 인정하고 반성하고 있다 | DETAIL | 범행 인정 · 반성 |
| 8 | 형사처벌 전력이 없다 | DETAIL | 전과 없음 |
| 9 | 피해 회복을 위해 5,000만 원을 공탁했다 | DETAIL | 피해 회복 공탁 |
| 10 | 피고인은 우발적 범행이라고 주장한다 | ARGUMENT | 우발적 범행 주장 |
| 11 | 피고인은 오랜 채무로 정신적으로 지쳐 있었다고 주장한다 | ARGUMENT | 정신적 피로 주장 |
- 피해자의 사망은 살인죄의 구성요건 결과라 판단 요소로 두지 않는다(이중평가 방지).
pre_label: 1 = "돈 문제로 오래 다툼이 있었다", 2 = "다투던 중 흉기를 집어 들었다", 3 = "범행 뒤 현장을 떠났다"
judgment
| id | subject_type | timing | experience_id | range_option_id | penalty_type | reduced_to | prison_months | summary |
|---|---|---|---|---|---|---|---|---|
| 10 | USER | PRE | 100 | 11 (실형 5년 이상 ~ 10년 미만) | NULL | NULL | NULL | NULL |
| 11 | USER | FINAL | 100 | NULL | PRISON | NULL | 180 | NULL (응답 시 규칙 문장 생성) |
| 20 | AI | FINAL | NULL | NULL | PRISON | NULL | 144 | 다투다 벌어진 범행과 공탁 · 반성을 함께 저울질한 판단 |
| 30 | COURT | FINAL | NULL | NULL | PRISON | NULL | 120 | 유족의 처벌 의사를 무겁게 보면서도 공탁과 반성을 감안한 판단 |
suspension_months는 모두 NULL이다(선고형이 3년을 넘어 집행유예 대상이 아니다).- 재판부
extra_dispositions:[{"type":"CONFISCATION","value":"범행에 사용한 흉기"}] - 내 판결 180개월(15년)은 권고 범위(84 ~ 144) 밖이지만 선고 가능 범위(30 ~ 360) 안이라 그대로 확정된다. 권고 범위 이탈 안내는 확장 단계다(REQ-036).
judgment_factor
| judgment_id | factor_id | direction |
|---|---|---|
| 10 | 1, 2 | NULL |
| 11 | 2, 6 | UP |
| 11 | 7 | DOWN |
| 20 | 2, 3, 5 | UP |
| 20 | 7, 8, 9 | DOWN |
| 30 | 2, 3, 5, 6 | UP |
| 30 | 7, 8, 9 | DOWN |
→ S-09 매트릭스(API 14): 요소 11개 × (USER FINAL · AI · COURT)를 조회해 행이 없으면 "—"로 표시한다.
| 요소 | 내 판결 | AI | 재판부 | 분류 |
|---|---|---|---|---|
| 1 | — | — | — | 세 주체 모두 고려하지 않아 매트릭스에서 뺌 |
| 2 | ↑ | ↑ | ↑ | ALL_SAME 셋 모두 같게 본 요소 |
| 3 | — | ↑ | ↑ | ONLY_ME_MISSED 나만 고려하지 않은 요소 |
| 4 | — | — | — | 매트릭스에서 뺌 |
| 5 | — | ↑ | ↑ | ONLY_ME_MISSED |
| 6 | ↑ | — | ↑ | DIVERGED 판단이 엇갈린 요소 |
| 7 | ↓ | ↓ | ↓ | ALL_SAME |
| 8 | — | ↓ | ↓ | ONLY_ME_MISSED |
| 9 | — | ↓ | ↓ | ONLY_ME_MISSED |
| 10, 11 | — | — | — | 매트릭스에서 뺌 |
→ 내 판결 한 줄 요약(MVP 규칙 문장, 요소별 요약어 기준 v1.10): ↑ 요소 2 · 6의 요약어 흉기 사용 · 피해자의 부양 가족, ↓ 요소 7의 요약어 범행 인정 · 반성 → "흉기 사용 · 피해자의 부양 가족을 무겁게 보고 범행 인정 · 반성을 감안한 판단". 확장 단계에서는 AI 비교 분석의 perspectives.USER로 바꾼다.
→ 판단 이유 변화(REQ-096, 확장): PRE의 요소(1, 2 — 모두 OVERVIEW)와 FINAL의 요소(2, 6, 7)를 비교한다. 2는 양쪽에 있으므로 "처음부터 알던 요소"(API KEPT), 1은 PRE에만 있으므로 "이미 알던 요소의 무게가 바뀜"(API WEIGHT_CHANGED — 사전 판단에서는 골랐지만 최종 판결에서는 고르지 않았다는 뜻이다. PRE 기록에는 방향이 없어 처음 판단의 강도는 알 수 없다), 6 · 7은 reveal_stage = DETAIL이고 FINAL에만 있으므로 "새로 알게 된 요소"(API NEWLY_LEARNED)다. 요소 1처럼 매트릭스에서 빠지는 요소도 사전 판단에서 골랐다면 변화 유형은 보여 준다(API 14 matrix 규칙).
→ 사전 판단 구간 11(60 ~ 120)과 최종 판결 180개월을 비교하면 preToFinal.direction은 HEAVIER다.
스키마는 Flyway 파일(backend/src/main/resources/db/migration)로만 만든다. 모든 테이블에 아래 규칙을 적용한다.
| 규칙 | 내용 |
|---|---|
| 파일 구성 | V1 기준 데이터(양형기준 버전 · 형량 구간 + 고정값) / V2 사건 콘텐츠 / V3 사용자 체험 / V4 판단 / V5 확장 단계 테이블. 새 변경은 V6부터 새 파일로 추가한다 |
| 열거값 | varchar + 허용 값 CHECK. 값이 계속 늘어나는 case_section.section_type, factor.summary_tag만 CHECK를 두지 않는다 |
| ID | bigint GENERATED BY DEFAULT AS IDENTITY (JPA GenerationType.IDENTITY). 시드 데이터는 ID를 지정하지 않고 넣는다 (지정하면 자동 번호가 올라가지 않아 이후 번호가 겹친다) |
| 익명 ID | anonymous_user.id(uuid)는 쿠키를 발급하는 서버 코드에서 만든다. DB 기본값 없음 |
| 필수 시각 | created_at · started_at · last_seen_at · updated_at은 DEFAULT now(). updated_at 갱신은 애플리케이션이 한다 |
| 이름 | pk_ · fk_{테이블}_{참조 테이블} · uk_{테이블}_{내용} · chk_{테이블}_{내용} · idx_{테이블}_{컬럼} |
| FK 인덱스 | FK 컬럼마다 인덱스를 만든다. 유니크 제약의 첫 컬럼과 겹치면 생략한다 |
| FK 삭제 동작 | 기본값(NO ACTION) |
| jsonb 형식 (v1.7) | 아래 컬럼은 반드시 배열로 넣는다. 엔티티가 배열 타입으로 읽어서, 형식이 다르면 그 사건의 조회 전체가 실패한다 • legal_case.keywords · deidentified_items, judgment.reference_tags: 문자열 배열• case_section.data: 배열. 원소는 섹션마다 다르다 — DAMAGE {label, value} · LAW_TERM {term, desc} 객체, SUMMARY 문자열• judgment.extra_dispositions: 객체 배열 {type, value}, type은 COMMUNITY_SERVICE · CONFISCATION만 |
| 본문 줄바꿈 (v1.9) | case_section.content의 항목은 줄바꿈(\n)으로 나눈다. PostgreSQL에서는 E'첫 항목\n둘째 항목'처럼 쓴다. 문장 끝에 마침표를 두어 줄바꿈을 그리지 않는 곳에서도 한 문단으로 읽히게 한다 |
| 적용 방식 | SQL 파일을 직접 실행하지 않고 서버 기동 시 Flyway가 적용한다. ddl-auto는 모든 환경에서 validate |
| 수정 규칙 | develop에 Merge된 파일은 수정하지 않는다. 오픈 전 불가피하면 팀 합의 후 DB 초기화하고 수정, 오픈 후에는 새 마이그레이션만 추가한다 |
엔티티 매핑 시 timestamptz는 OffsetDateTime · Instant, jsonb는 @JdbcTypeCode(SqlTypes.JSON)을 쓴다. validate라서 타입이 맞지 않으면 서버가 뜨지 않는다.
- DB CHECK 제약을 건다. 3-3장의
timing별 CHECK 제약을 Flyway 마이그레이션 DDL에 포함한다(PostgreSQL). - 조회는 목적별 Repository 메서드로만 한다.
timing·subject_type조건을 서비스 코드에 직접 쓰지 않는다.- 예:
findPreJudgment(experienceId),findUserFinalJudgment(experienceId),findPublishedJudgment(caseId, subjectType) - 통계 쿼리도
timing = FINAL·attempt_no = 1조건을 메서드 안에 둔다.
- 예:
- 요청·응답 DTO를 분리한다. 사전 판단 제출 DTO(형량 구간 + 작용 요소)와 최종 판결 제출 DTO(형벌 · 감경 후 형벌 · 형량 · 집행유예 · 판단 요소 · 방향)를 따로 둔다. 테이블은 하나지만 API에서는 서로 다른 모양으로 다룬다.
- 익명 ID가 없으면 발급한다.
- (
anonymous_user_id,case_id)의 체험이 있으면 그 체험을 돌려준다. 없으면attempt_no = 1로 새로 만든다. - 화면은 돌려받은
status에 맞는 화면으로 이동한다(IA 5장).
미확정 제안이다. 12차 회의 "DB에 성향 테스트 관련 내용 추가"를 위한 초안으로, 문항 산식 · 매핑 로직(요구사항 15장)이 정해지면 확정한다. 요구사항 DR-11, 기능 명세 REQ-112 ~ 119 · 127.
판단 요소 ↔ 가치관 축은 이 장에서 새로 만들지 않는다. 요소가 어느 축에 속하는지는 이미 있는
factor.value_axis(v1.13, BE-47)를 그대로 쓴다. 값은 파이프라인의 AI 분류(BE-49)와 관리자 후검수(BE-48 · BE-43)로 정해진다. 축 이름도 같은 키(APOLOGY_SINCERITY·FAULT_STANDARD·PRINCIPLE_RELATION·ORDER_OPPORTUNITY)를 쓰므로 성향 테스트 점수와 체험 기여분을 같은 키로 합칠 수 있다.형량 ↑ / ↓로 고른 것이 어느 글자(E/I 등)로 가는지는 가치관 축마다 정하는 규칙이므로 테이블 없이 산식 문서(WBS 6.1.4)로 정한다. 요소마다 글자 · 가중치가 달라야 한다는 결론이 나오면 그때
factor옆에 보조 테이블을 추가한다.
erDiagram
anonymous_user ||--o{ personality_result : "응시"
personality_type ||--o{ personality_result : "결과 유형"
personality_question ||--|{ personality_choice : "선택지"
experience ||--o| experience_personality : "체험별 성향 기여"
| 테이블 | 주요 컬럼 (제안) | 설명 |
|---|---|---|
personality_question |
id, axis(APOLOGY_SINCERITY / FAULT_STANDARD / PRINCIPLE_RELATION / ORDER_OPPORTUNITY, factor.value_axis와 같은 값), scenario(FRIEND / FAMILY / WORK / NEIGHBOR), content, display_order, is_active |
일상 시나리오 문항. 축당 3 ~ 4개. 법률 용어 금지 |
personality_choice |
id, question_id FK, content, letter(E·I / S·N / T·F / J·P), score, display_order |
선택지가 어느 글자 쪽에 몇 점을 주는지 |
personality_type |
code PK(char(4), 예: INFP), name(예: 너그러운 판다형), animal, one_liner, tolerance(0 ~ 4), description, image_url |
16유형 콘텐츠. 코드 상수로 둘 수도 있다 |
personality_result |
id, anonymous_user_id FK, type_code FK, axis_scores jsonb(키는 위 4개 축 이름), answers jsonb, is_current, created_at | 응시 결과. 재응시 시 새 행 + 이전 행 is_current=false(제안). 회원 연결은 anonymous_user.member_id로 따라간다 |
experience_personality |
id, experience_id FK unique, axis_scores jsonb(키는 위 4개 축 이름), type_code, created_at | 판결 체험이 COMPLETED가 될 때 계산한 성향 기여분. 성향 변화 흐름 · 종합 성향(평균) 계산에 쓴다. 성향 테스트 전에 한 체험도 남겨 둔다 |
- 사용자 응답에 원본 판결문 정보가 섞이지 않는다는 원칙(5장)은 그대로다. 성향 기여는 사용자의
USER·FINAL판단 요소 기록(judgment_factor)에서만 계산한다. - 공유 링크는
personality_type만 읽는다.personality_result의 응답 · 점수는 공유 응답에 넣지 않는다. factor.value_axis가NULL인 판단 요소는 성향 계산에서 빠진다(4장 규칙과 동일).