작성 이력
| 버전 | 날짜 | 내용 |
|---|---|---|
| v0.1 | 2026-09-28 | 초안 — 공통 규칙(경로 · 익명 ID 쿠키 · 에러 형식 · 에러 코드), MVP API 14개와 확장 API 1개의 요청 · 응답 · 거절 조건, 상태별 호출 가능 API 표 |
| v0.2 | 2026-09-28 | 전체 문서 교차 검토 반영 — API 6 · 7 허용 상태 정정, 예시 요소 ID를 ERD 예시(1 ~ 7)와 통일, 섹션 ① 데이터 출처 명시, 벌금 선고 가능 하한 25,000원(형법 제45조 단서), 무죄 선택지 · references 출처 · perspectives 근거 요소 · changeType(KEPT 추가) · 형벌 무게 순서 보완, 사기 양형기준 유형 표기 정정(제1유형), 시퀀스 후보 대응표 |
| v0.3 | 2026-09-29 | 문서 정합성 점검 결정 반영 (COMMON-4) — 6장 #1 확정(화면 진입 시 상태 조회 없이 각 API의 INVALID_STATE로 이동, 1-6 · 5장), 무죄 선택지 MVP 제외(API 8 · 9 · 14, 6장 #5), 예시 사건을 단일 범행 사건(지인 투자금 편취)으로 교체(경합범 서비스 제외), 판결 카드 한 줄 요약 summary 추가(내 판결은 요약 태그 규칙 문장, 확장에서 AI 요약으로 교체), 사건 목록 crimeCategoryLabel · thumbnailUrl, (확장) 실제 판결 deidentifiedItems, 참고 자료 태그 출처 컬럼명 reference_tags, (확장) 쿠키 삭제 안내(REQ-108). (낮음 항목) 같은 도메인 배포 확정(1-2 · 6장 #2), 시퀀스 후보 대응표 삭제, 톤 규칙 참조를 요구사항 11장으로 정정 |
| v0.4 | 2026-09-30 | 대표 사건(살인) 가공 결정 반영 (BE-13) — 형벌 종류에 사형(DEATH) · 무기징역(LIFE) 추가(API 6 · 8 · 9, 판결 응답 공통 형식), 판결 제출에 reducedTo(감경 후 형벌) 추가, diffFromMine 비교 기준을 최종 선고 형벌로 명시, 부가 처분 CONFISCATION(몰수) 추가 (ERD v1.4), API 14 형벌 무게 순서에 LIFE < DEATH 추가, reducedTo는 형벌 종류가 바뀌는 감경만 기록한다고 명시, 최종 선고 형벌이 DEATH · LIFE일 때 형량 값이 있으면 VALIDATION_ERROR로 명시 |
| v0.5 | 2026-09-30 | 예시 사건을 가상 살인 사건으로 교체 (COMMON-11) — API 1 · 4 · 6 · 8 · 9 · 10 · 12 · 14 예시를 "빌린 돈 문제로 찾아온 지인을 살해한 사건"(가상)으로 교체, 사전 판단 구간 예시를 살인용 8개(벌금형 제외 · 무기 · 사형 추가)로, 형벌 선택지 예시를 사형 · 무기 · 징역 3종으로 교체 (ERD v1.5) |
| v0.6 | 2026-09-30 | BE-16 시드 반영 — API 4 · 5 · 14 예시의 사전 판단 구간 rangeOptionId를 DB 실제 값(살인 8 ~ 15, 예시 선택 11)으로 정정 (ERD v1.6) |
| v0.7 | 2026-09-30 | API 4 · 5 구현 반영 (BE-7) — API 4 형량 구간 개수를 범죄 유형별로 명시(살인 8개, 사기 · 상해 7개), 두 API의 404 · 400 거절 조건 추가, API 5 검사 순서 · 거절 시 저장 없음 · factorIds 규칙 순서 명시 |
| v0.8 | 2026-10-01 | BE-8 리뷰 반영 — API 6의 선고 가능 범위는 하한·상한이 모두 있는 규칙만 포함, 범위 표시 문구를 API 8 형식으로 통일, LAW_TERM은 law.terms로만 전달하도록 명시 |
| v0.9 | 2026-10-01 | 후속 정리 (COMMON-14) — 1-1에 숫자 입력 규칙 추가(정수 필드의 소수는 VALIDATION_ERROR, 문자열 숫자는 허용, BE-22), API 9에 열거값에 없는 penaltyType도 INVALID_PENALTY_TYPE으로 거절한다고 명시(1-5 열거값 규칙보다 우선, BE-9) |
| v0.10 | 2026-10-02 | API 10 ~ 13 구현 · 리뷰 반영 (BE-10) — diffFromMine은 최종 선고 형벌에 해당하는 값만 내려가고 집행유예 여부가 다르면 다른 형벌로 본다고 명시하고 유예 기간 차이 suspensionMonthsDiff 추가(API 10), 내 판결 한 줄 요약에서 같은 태그가 ↑ · ↓ 양쪽에 있으면 먼저 고른 방향에만 남기고 조사는 끝의 한글 · 숫자 기준으로 을 · 를을 고른다고 명시(판결 응답 공통 형식), 공개 요청(API 11 · 13)은 상태를 옮기기 전에 검수한 AI · 재판부 판결이 등록돼 있는지 먼저 확인한다고 명시, 공개 사건에 그 판결이 없으면 500 INTERNAL_ERROR로 둔다고 명시(API 10 · 12) |
| v0.11 | 2026-10-02 | 사건 정보 섹션 표시 방식 팀 결정 반영 (BE-17) — API 6 content에는 항목마다 줄바꿈(\n)이 들어갈 수 있고 화면은 그대로 보여 준다고 명시, 양측 주장 섹션 위 안내 문구는 응답에 없고 화면 고정 문구라고 명시 (ERD v1.9 · 정보 구조 v1.10) |
| v0.12 | 2026-10-06 | 기획 단계 리뷰 준비 회의 결정 반영 (COMMON-16) — 내 판결 한 줄 요약 문장 틀을 확정(6장 #7, 판결 응답 공통 형식) |
| v0.13 | 2026-10-06 | 판단 요소별 요약어 기준 확정 반영 (BE-26, 구현 BE-11) — preToFinal.direction 세부 규칙 확정(6장 #3), ruleSentences 문장 틀을 요소별 요약어 기준으로 확정(6장 #4), 내 판결 한 줄 요약 방향별 태그 수 제한 없음으로 정정(6장 #7), API 14 예시 응답의 judgments.USER.summary · matrix(요소 7 ALL_SAME 추가) · ruleSentences를 요약어 기준 · 서로 일치하도록 갱신, 판결 응답 공통 형식의 USER(MVP) 예시 문구도 요약어 기준으로 정정(PR #67 리뷰) |
| v0.14 | 2026-10-08 | 11 · 12차 회의 반영 (COMMON-20) — API 15(AI 비교 분석)를 보류로 표시(REQ-063 확장 범위 제외, 4 · 5장 · 6장 #6), 판결 성향 테스트 · 마이페이지 · 관리자 API는 설계 전이라 7장에 후보만 기록 |
| 항목 | 규칙 |
|---|---|
| 기본 경로 | /api/v1 |
| 형식 | JSON (Content-Type: application/json), 문자 UTF-8 |
| 필드 이름 | camelCase |
| 날짜 · 시각 | ISO 8601 (2026-09-28T15:30:00+09:00) |
| 형량 단위 | 징역 · 집행유예는 개월(int), 벌금은 원(long). 화면에서 "2년 6개월"로 바꿔 보여 준다 |
| 숫자 입력 | (v0.9) 정수 필드(형량 개월 · 금액 · step · ID 등)에 소수(36.7, 36.0)가 오면 400 VALIDATION_ERROR로 거절한다. 소수점을 버려 다른 값으로 저장하지 않기 위해서다. "36"처럼 숫자로 읽을 수 있는 문자열은 숫자로 받고, "삼십육"처럼 읽을 수 없는 문자열은 400이다. 본문 형식 오류라 사건 · 체험 조회보다 먼저 거절한다 (BE-22) |
| 열거값 | ERD 값을 그대로 쓴다 (PRISON, UP, COMPLETED 등) |
| 인증 | MVP는 로그인 없음. 익명 ID 쿠키로 사용자를 구분한다 (1-2) |
| 항목 | 값 (제안) |
|---|---|
| 이름 | NLNB_AID |
| 값 | anonymous_user.id (UUID) |
| 발급 시점 | 체험 시작(POST /cases/{caseId}/experience) 때 쿠키가 없거나 DB에 없는 값이면 새로 발급 |
| 속성 | HttpOnly, Secure, SameSite=Lax, Path=/ |
| 유효 기간 | 1년 (요구사항 15장 "익명 ID 유지 방식 · 기간" 결정 시 확정) |
- 프론트와 API는 같은 도메인으로 배포한다(6장 #2 확정, 기술 스택 8장). EC2의 Nginx가 프론트 빌드 파일을 서빙하고
/api요청을 백엔드 컨테이너로 프록시하므로SameSite=Lax그대로 쓰고 CORS 설정은 필요 없다. - 요청마다
anonymous_user.last_seen_at을 갱신한다. - (확장, REQ-108) 쿠키를 지우거나 브라우저를 바꾸면 진행 상태를 이어갈 수 없다는 안내를 둔다. 표시 화면과 문구는 미정이다(요구사항 15장). 서버 API 변경은 없다.
화면 경로가 /cases/{caseId}/...(IA 4장)이므로, API도 사건 ID + 쿠키로 체험을 찾는다. 체험 ID를 화면이 따로 들고 다닐 필요가 없다.
/api/v1/cases/{caseId}/experience/...
- 서버는 (쿠키의 익명 ID,
caseId, 가장 최근attempt_no)로 체험을 찾는다. MVP에서는attempt_no가 항상 1이다. - 체험이 없거나 쿠키가 없으면
404 EXPERIENCE_NOT_FOUND. 다른 사람의 체험은 이 경로로 접근할 수 없다.
성공 응답은 결과 객체를 그대로 돌려준다(감싸는 객체 없음). 에러는 아래 형식으로 통일한다.
json
{ "code": "INVALID_STATE", "message": "지금 단계에서는 할 수 없는 요청입니다.", "currentStatus": "PRE_JUDGED", "details": null}| 필드 | 설명 |
|---|---|
code |
에러 코드 (1-5). 화면 분기는 이 값으로 한다 |
message |
사람이 읽는 설명. 화면에 그대로 띄우지 않는다 |
currentStatus |
체험 관련 에러일 때 현재 체험 상태. 화면은 이 값으로 이동할 화면을 정한다 (1-6) |
details |
입력 검증 에러일 때 필드별 사유 배열 [{ "field": "prisonMonths", "reason": "OUT_OF_ALLOWED_RANGE" }] |
| HTTP | code | 언제 | 화면 대응 |
|---|---|---|---|
| 400 | VALIDATION_ERROR |
필수값 누락, 형식 오류 (음수, 잘못된 열거값 등) | 개발 오류. 일반 오류 안내 |
| 404 | CASE_NOT_FOUND |
사건이 없거나 PUBLISHED가 아님 |
S-14 |
| 404 | EXPERIENCE_NOT_FOUND |
이 사건의 내 체험이 없음 (쿠키 없음 포함) | S-02로 이동 |
| 409 | INVALID_STATE |
지금 상태에서 할 수 없는 요청 (이미 제출 · 확정, 순서가 맞지 않음, 아직 공개 전) | currentStatus의 화면으로 이동 |
| 409 | STEP_OUT_OF_ORDER |
섹션 확인 순서를 건너뜀 | 현재 섹션 다시 불러오기 |
| 422 | INVALID_RANGE_OPTION |
사전 판단 구간이 이 사건 범죄 유형의 선택지가 아님 | 개발 오류 |
| 422 | INVALID_FACTOR |
이 사건의 판단 요소가 아님, 사전 판단에 OVERVIEW가 아닌 요소, 중복 요소, 방향 누락 |
개발 오류 |
| 422 | TOO_MANY_FACTORS |
사전 판단 작용 요소가 2개를 넘음 (확장) | 개발 오류 |
| 422 | INVALID_PENALTY_TYPE |
이 사건에서 허용되지 않은 형벌, 허용되지 않은 감경 조합(reducedTo) |
개발 오류 |
| 422 | OUT_OF_ALLOWED_RANGE |
형량이 선고할 수 있는 범위 밖 | S-06c 경고 유지 (정상 화면이면 버튼이 비활성이라 오지 않음) |
| 422 | INVALID_SUSPENSION |
집행유예를 허용하지 않는 형벌 · 형량인데 값이 있음, 기간이 1 ~ 5년 밖 | 입력 안내 |
| 500 | INTERNAL_ERROR |
서버 오류 | 일반 오류 안내 |
- "이미 제출함"과 "순서가 맞지 않음"을 따로 나누지 않고
INVALID_STATE하나로 둔다. 화면이 할 일은 둘 다 "currentStatus의 화면으로 이동"으로 같기 때문이다.
화면은 진입할 때 상태를 따로 조회하지 않고 그 화면의 API를 바로 부른다. 허용되지 않은 화면이면 API가 409 INVALID_STATE와 currentStatus를 돌려주므로, 화면은 아래 표의 "보낼 화면"으로 이동한다(IA 5장 · 9장, 6장 #1 확정). API 3은 상태만 알아야 하는 경우에 쓴다.
| status | 보낼 화면 | 허용 화면 |
|---|---|---|
STARTED |
S-03 | S-03 |
PRE_JUDGED, REVIEWING |
S-04 | S-04 |
REVIEWED |
S-05 | S-04, S-05, S-06 |
VERDICT_CONFIRMED |
S-07 | S-07 |
AI_REVEALED |
S-08 | S-07, S-08 |
COMPLETED |
S-09 | S-07, S-08, S-09 |
| # | 메서드 | 경로 | 설명 | 허용 상태 | 상태 변경 | 화면 | 단계 |
|---|---|---|---|---|---|---|---|
| 1 | GET | /cases |
사건 목록 | — | — | S-02 | MVP |
| 2 | POST | /cases/{caseId}/experience |
체험 시작 (있으면 기존 체험) | — | 없으면 → STARTED |
S-02 | MVP |
| 3 | GET | /cases/{caseId}/experience |
내 체험 상태 | 전체 | — | 전체 | MVP |
| 4 | GET | /cases/{caseId}/experience/overview |
사건 개요 · 사전 판단 선택지 | STARTED |
— | S-03 | MVP |
| 5 | POST | /cases/{caseId}/experience/pre-judgment |
사전 판단 제출 | STARTED |
→ PRE_JUDGED |
S-03 | MVP |
| 6 | GET | /cases/{caseId}/experience/review |
사건 정보 (열린 섹션까지) | PRE_JUDGED ~ REVIEWED |
— | S-04, S-05 | MVP |
| 7 | POST | /cases/{caseId}/experience/review-steps |
섹션 확인 기록 | PRE_JUDGED ~ REVIEWED (REVIEWED는 상태 유지) |
→ REVIEWING / REVIEWED |
S-04 | MVP |
| 8 | GET | /cases/{caseId}/experience/verdict-form |
판결 입력 정보 | REVIEWED |
— | S-06 | MVP |
| 9 | POST | /cases/{caseId}/experience/verdict |
판결 제출 | REVIEWED |
→ VERDICT_CONFIRMED |
S-06 | MVP |
| 10 | GET | /cases/{caseId}/experience/judgments/ai |
AI 판결 | VERDICT_CONFIRMED 이상 |
— | S-07 | MVP |
| 11 | POST | /cases/{caseId}/experience/court-reveal |
실제 판결 공개 | VERDICT_CONFIRMED 이상 |
처음이면 → AI_REVEALED |
S-07 | MVP |
| 12 | GET | /cases/{caseId}/experience/judgments/court |
실제 판결 | AI_REVEALED 이상 |
— | S-08 | MVP |
| 13 | POST | /cases/{caseId}/experience/comparison-reveal |
비교 공개 | AI_REVEALED 이상 |
처음이면 → COMPLETED |
S-08 | MVP |
| 14 | GET | /cases/{caseId}/experience/comparison |
세 판결 비교 | COMPLETED |
— | S-09 | MVP |
| 15 | GET | /cases/{caseId}/experience/comparison/analysis |
AI 비교 분석 | COMPLETED |
— | S-09 | 확장 |
공개(11, 13)와 조회(12, 14)를 나눈 이유: 상태를 바꾸는 요청은 POST로 두고, 조회는 몇 번을 불러도 상태가 바뀌지 않게 한다. 결과 화면을 새로고침하면 조회만 다시 부르면 된다. 공개 요청은 이미 공개된 상태에서 다시 와도 거절하지 않고 성공으로 돌려준다(시퀀스 7장).
요청
| 위치 | 이름 | 필수 | 설명 |
|---|---|---|---|
| query | crimeType |
MURDER / FRAUD / INJURY. 없으면 전체 |
응답 200
json
{ "summary": { "total": 9, "byCrimeType": { "MURDER": 3, "FRAUD": 3, "INJURY": 3 } }, "cases": [ { "caseId": 1, "title": "빌린 돈 문제로 찾아온 지인을 살해한 사건", "crimeType": "MURDER", "crimeCategoryLabel": "생명범죄", "shortIntro": "빌린 돈 문제로 찾아온 지인과 다투다 흉기로 살해한 사건입니다.", "keywords": ["돈 문제", "집으로 찾아옴"], "difficulty": "HIGH", "estimatedMinutes": 15, "participantCount": 1284, "thumbnailUrl": null } ]}PUBLISHED사건만.summary는 필터와 관계없이 전체 기준(칩 옆 건수 표시용).crimeCategoryLabel:crime_type별 표시용 분류명(예:FRAUD→ "재산범죄"). DB 컬럼이 아니라 서버 코드 상수에서 만든다(ERD 3-1).thumbnailUrl:legal_case.thumbnail_url. 없으면null이고, 화면은 범죄 유형별 기본 이미지를 쓴다.participantCount:attempt_no = 1이고COMPLETED인 체험 수.- 정렬:
published_at최신순 고정 (정렬 선택은 이후 단계, REQ-009). - (확장, REQ-010) 쿠키가 있으면 카드마다
myStatus(null/IN_PROGRESS/COMPLETED)를 넣어 배지를 표시할 수 있다.
요청: 본문 없음. 쿠키가 있으면 함께 보낸다.
응답 — 새로 만들면 201, 이미 있으면 200. 본문은 API 3과 같다.
json
{ "caseId": 1, "attemptNo": 1, "status": "STARTED", "lastReviewedStep": 0, "startedAt": "2026-09-28T15:30:00+09:00"}- 쿠키가 없거나 DB에 없는 익명 ID면
anonymous_user를 만들고Set-Cookie로 내려준다. - 이미 체험이 있으면 새로 만들지 않고 기존 체험을 돌려준다. 화면은 1-6 표대로 이동한다(완료한 사건이면 S-09, REQ-011 · 012).
- 동시에 두 번 와도 유니크 제약(
anonymous_user_id,case_id,attempt_no)으로 하나만 생긴다. 제약 위반이 나면 서버가 기존 체험을 다시 읽어200으로 돌려준다.
| 거절 | 조건 |
|---|---|
404 CASE_NOT_FOUND |
사건 없음 · 비공개 |
응답 200: API 2와 같은 형식.
| 거절 | 조건 |
|---|---|
404 CASE_NOT_FOUND |
|
404 EXPERIENCE_NOT_FOUND |
체험 없음 (쿠키 없음 포함) → 화면은 S-02로 |
응답 200
json
{ "case": { "caseId": 1, "title": "빌린 돈 문제로 찾아온 지인을 살해한 사건", "crimeType": "MURDER", "crimeCategoryLabel": "생명범죄", "chargeName": "살인", "overview": "피고인이 빌린 돈을 갚지 못해 오래 다투던 지인이 집으로 찾아오자, 말다툼 끝에 집에 있던 흉기로 피해자를 살해하고 구호 조치 없이 집을 나간 사건이다." }, "rangeOptions": [ { "rangeOptionId": 8, "label": "징역형 집행유예" }, { "rangeOptionId": 9, "label": "실형 3년 미만" }, { "rangeOptionId": 10, "label": "실형 3년 이상 ~ 5년 미만" }, { "rangeOptionId": 11, "label": "실형 5년 이상 ~ 10년 미만" }, { "rangeOptionId": 12, "label": "실형 10년 이상 ~ 20년 미만" }, { "rangeOptionId": 13, "label": "실형 20년 이상" }, { "rangeOptionId": 14, "label": "무기징역" }, { "rangeOptionId": 15, "label": "사형" } ], "preFactors": [ { "factorId": 1, "label": "돈 문제로 오래 다툼이 있었다" }, { "factorId": 2, "label": "다투던 중 흉기를 집어 들었다" }, { "factorId": 3, "label": "범행 뒤 현장을 떠났다" } ]}rangeOptions: 이 사건crime_type의sentence_range_option을display_order순으로. 사기 · 상해는 공통 7개, 살인은 벌금형을 빼고 무기징역 · 사형을 더한 8개다.preFactors:reveal_stage = OVERVIEW인 요소의pre_label. 확장(REQ-093) — MVP 화면은 쓰지 않아도 된다.- 법정형 · 선고 가능 범위 · 권고 범위 · 실제 판결은 넣지 않는다(FR-2-8).
| 거절 | 조건 |
|---|---|
404 CASE_NOT_FOUND |
사건 없음 · 비공개 |
404 EXPERIENCE_NOT_FOUND |
이 사건의 내 체험이 없음 (쿠키 없음 포함) → 화면은 S-02로 |
409 INVALID_STATE |
STARTED가 아님 (이미 제출) → currentStatus의 화면 (1-6) |
요청
json
{ "rangeOptionId": 11, "factorIds": [1, 2] }| 필드 | 필수 | 규칙 |
|---|---|---|
rangeOptionId |
✓ | 이 사건 범죄 유형의 선택지 |
factorIds |
0 ~ 2개, OVERVIEW 요소, 중복 없음. 확장 — MVP는 빈 배열이나 생략 |
응답 200
json
{ "status": "PRE_JUDGED", "lastReviewedStep": 1 }- 한 트랜잭션:
judgment(USER,PRE) +judgment_factor(방향 NULL) 저장, 상태STARTED→PRE_JUDGED,last_reviewed_step = 1(섹션 ① 개요는 S-03에서 본 것으로 처리). - 상태 변경은 조건부 갱신("
status = STARTED일 때만")으로 한다. 동시에 두 번 오면 하나만 성공하고 나머지는INVALID_STATE(currentStatus포함). 패자가 판단 저장에서 유니크 제약에 걸려도 같은 응답으로 바꿔 돌려준다. - 응답에 사전 판단 내용을 되돌려주지 않는다(S-09 전까지 다시 보여 주지 않음).
- 검사 순서: 사건 · 체험(404) → 상태(409) → 형량 구간(422) → 판단 요소(422: 개수 → 유효성). 첫 번째로 걸린 사유 하나로 거절하며, 거절하면 상태 · 판단 · 판단 요소 어느 것도 저장하지 않는다. 요청 형식 오류(400)는 그 앞에서 걸러진다.
factorIds(확장)의 유효성은 "이 사건의OVERVIEW요소이고 중복이 없다"이다. 하나라도 어긋나면 전체를INVALID_FACTOR로 거절한다.
| 거절 | 조건 |
|---|---|
400 VALIDATION_ERROR |
rangeOptionId 누락 · 형식 오류, factorIds 형식 오류 |
404 CASE_NOT_FOUND |
사건 없음 · 비공개 |
404 EXPERIENCE_NOT_FOUND |
이 사건의 내 체험이 없음 (쿠키 없음 포함) → 화면은 S-02로 |
409 INVALID_STATE |
STARTED가 아님 |
422 INVALID_RANGE_OPTION |
없는 구간이거나 이 사건 범죄 유형의 구간이 아님 |
422 INVALID_FACTOR |
이 사건의 요소가 아님, OVERVIEW가 아닌 요소, 중복, 없는 요소 |
422 TOO_MANY_FACTORS |
2개 초과 (확장) |
S-04와 S-05가 함께 쓴다.
섹션 번호: ① 개요 = 1, ② 상세 사실관계 = 2, ③ 양측 주장 = 3, ④ 법률 · 양형기준 = 4. lastReviewedStep은 확인을 마친 마지막 번호다.
응답 200 (예: 섹션 ②까지 확인, ③이 열린 상태)
json
{ "status": "REVIEWING", "lastReviewedStep": 2, "openStep": 3, "sections": [ { "step": 1, "stage": "OVERVIEW", "confirmed": true, "items": [ { "sectionType": "OVERVIEW", "title": "사건 개요", "content": "피고인이 빌린 돈을 갚지 못해 오래 다투던 지인이 집으로 찾아오자 …" } ] }, { "step": 2, "stage": "DETAIL", "confirmed": true, "items": [ { "sectionType": "FACTS", "title": "주요 사실관계", "content": "사건 3개월 전부터 변제 문제로 여러 차례 다툼이 있었고 …" }, { "sectionType": "DAMAGE", "title": "피해 결과", "data": [ { "label": "피해자 수", "value": "1명" }, { "label": "피해 결과", "value": "사망" }, { "label": "피해자와의 관계", "value": "지인 (돈을 빌린 사이)" }, { "label": "범행 도구", "value": "집에 있던 흉기" } ] }, { "sectionType": "DEFENDANT", "title": "피고인 관련 사실", "content": "30대이고 형사처벌을 받은 전력이 없다." }, { "sectionType": "SETTLEMENT", "title": "합의 · 피해 회복", "content": "피해 회복을 위해 5,000만 원을 공탁했으나 합의에 이르지 못했고, 유족은 엄벌을 원한다." } ] }, { "step": 3, "stage": "ARGUMENT", "confirmed": false, "items": [ { "sectionType": "PROSECUTOR", "title": "검사", "content": "…" }, { "sectionType": "DEFENSE", "title": "피고인 · 변호인", "content": "…" } ] } ], "lockedSteps": [4], "law": null, "summary": null}- step 1의 항목은
case_section이 아니라legal_case.overview로 만든다(sectionType: "OVERVIEW"는 응답용 값). step 2 ~ 4는case_section의stage별 항목이다. - 열린 섹션(
lastReviewedStep + 1)까지만 본문을 넣는다. 잠긴 섹션은 번호만lockedSteps에 넣는다(개발자 도구로 미리 보기 방지). law: 섹션 ④가 열리면 채운다.- 용어 설명(
case_sectionLAW_TERM)은 섹션 ④items에 넣지 않고law.terms로만 보낸다. content에는 항목마다 줄바꿈(\n)이 들어갈 수 있다. 화면은 줄바꿈을 그대로 보여 준다(v0.11, ERD v1.9).- 양측 주장(
PROSECUTOR·DEFENSE) 섹션 위 안내 문구는 응답에 넣지 않는다. 화면 고정 문구다(정보 구조 S-04).
json
"law": { "appliedLaw": "형법 제250조 제1항 살인", "statutoryPenaltyText": "사형, 무기 또는 5년 이상의 징역", "allowedRanges": [ { "penaltyType": "DEATH", "allowedMin": 240, "allowedMax": 600, "text": "사형 (감경하면 무기징역 또는 징역 20년 ~ 50년)" }, { "penaltyType": "LIFE", "allowedMin": 120, "allowedMax": 600, "text": "무기징역 (감경하면 징역 10년 ~ 50년)" }, { "penaltyType": "PRISON", "allowedMin": 30, "allowedMax": 360, "text": "징역 2년 6개월 ~ 30년" } ], "allowedRangeNote": "감경·가중 사유를 반영해 법률상 선고할 수 있는 가장 넓은 범위예요.", "recommended": { "minMonths": 84, "maxMonths": 144, "basis": "살인범죄 제2유형(보통 동기 살인), 감경영역. 특별감경인자 1개(실질적 피해 회복), 특별가중인자 없음" }, "terms": [ { "term": "감경영역", "desc": "형을 가볍게 할 특별한 사정이 있어 기본 권고 형량보다 낮은 구간이 적용되는 구간" } ]}- 권고 범위 예시 값(84 ~ 144개월)은 설명용이다. 살인 제2유형(보통 동기 살인) 감경영역(7년 ~ 12년)을 가정한 가상 값이며, 대표 판례 등록 시 팀이 양형기준으로 다시 계산한다. (와이어프레임 v2.1의 예시 값은 예전 사기 예시 사건 기준이라 이 명세와 다르다.)
summary:REVIEWED일 때 S-05용 핵심 사실 요약(case_sectionSUMMARY)을 채운다.allowedRanges:penalty_rule중 범위(allowed_min·allowed_max)가 모두 있는 규칙만 넣는다. 화면에는 징역만 보여 줘도 된다.- (v0.4) 사형 · 무기가 법정형에 있는 사건은
DEATH·LIFE항목도 넣는다. 이때allowedMin~allowedMax는 작량감경해 징역으로 선고할 때의 범위이고,text에 그대로 선고할 수 있다는 내용을 함께 쓴다. 예:{ "penaltyType": "LIFE", "allowedMin": 120, "allowedMax": 600, "text": "무기징역 (감경하면 징역 10년 ~ 50년)" },{ "penaltyType": "DEATH", "allowedMin": 240, "allowedMax": 600, "text": "사형 (감경하면 무기징역 또는 징역 20년 ~ 50년)" } allowedRangeNote는 서버 고정 문구다.penalty_rule.allowed_basis(산출 근거)는 내부용이라 응답하지 않는다.allowedRanges.text는 서버가 만들어 주는 표시 문구다. 화면마다 "1개월 ~ 10년" 표기가 달라지지 않게 하기 위해서다. 형식은 API 8options[].text와 같다.- 사전 판단은 넣지 않는다.
| 거절 | 조건 |
|---|---|
409 INVALID_STATE |
STARTED (사전 판단 전) → S-03, VERDICT_CONFIRMED 이상 → 결과 화면 (1-6) |
요청
json
{ "step": 3 }응답 200
json
{ "status": "REVIEWING", "lastReviewedStep": 3, "openStep": 4 }| 요청 step | 처리 |
|---|---|
lastReviewedStep + 1이고 2 또는 3 |
last_reviewed_step 갱신, 상태 REVIEWING |
lastReviewedStep + 1이고 4 |
last_reviewed_step = 4, 상태 REVIEWED. 응답 openStep: null |
lastReviewedStep 이하 |
이미 확인한 섹션. 거절하지 않고 현재 상태를 그대로 돌려준다 (버튼 두 번 누름 대비) |
| 그 밖 (건너뜀) | 409 STEP_OUT_OF_ORDER |
- 확인 후 다음 섹션 본문은 API 6을 다시 불러 받는다.
| 거절 | 조건 |
|---|---|
409 INVALID_STATE |
STARTED, 또는 VERDICT_CONFIRMED 이상 |
409 STEP_OUT_OF_ORDER |
|
400 VALIDATION_ERROR |
step이 2 ~ 4가 아님 |
REVIEWED에서 step 4 이하가 오면 현재 상태를 그대로 돌려준다.
응답 200
json
{ "penaltyOptions": [ { "penaltyType": "DEATH", "allowedMin": 240, "allowedMax": 600, "text": "사형 (감경하면 무기징역 또는 징역 20년 ~ 50년)", "suspensionAllowed": false, "reducibleTo": ["LIFE", "PRISON"] }, { "penaltyType": "LIFE", "allowedMin": 120, "allowedMax": 600, "text": "무기징역 (감경하면 징역 10년 ~ 50년)", "suspensionAllowed": false, "reducibleTo": ["PRISON"] }, { "penaltyType": "PRISON", "allowedMin": 30, "allowedMax": 360, "text": "징역 2년 6개월 ~ 30년", "suspensionAllowed": true, "reducibleTo": [] } ], "statutoryPenaltyText": "사형, 무기 또는 5년 이상의 징역", "allowedRangeNote": "감경·가중 사유를 반영해 법률상 선고할 수 있는 가장 넓은 범위예요.", "recommended": { "minMonths": 84, "maxMonths": 144, "basis": "…" }, "suspensionRule": { "maxPrisonMonths": 36, "maxFineAmount": 5000000, "minMonths": 12, "maxMonths": 60 }, "factors": [ { "factorId": 1, "label": "빌린 돈을 갚지 못해 오래 다툼이 있었다" }, { "factorId": 2, "label": "다투던 중 집에 있던 흉기를 집어 들었다" }, { "factorId": 3, "label": "범행 뒤 구호 조치 없이 현장을 떠났다" }, { "factorId": 4, "label": "사건 3개월 전부터 변제 문제로 여러 차례 다퉜다" }, { "factorId": 5, "label": "유족이 엄벌을 원한다" }, { "factorId": 6, "label": "피해자에게는 부양하던 어린 자녀 2명이 있다" }, { "factorId": 7, "label": "수사 초기부터 범행을 인정하고 반성하고 있다" }, { "factorId": 8, "label": "형사처벌 전력이 없다" }, { "factorId": 9, "label": "피해 회복을 위해 5,000만 원을 공탁했다" }, { "factorId": 10, "label": "피고인은 우발적 범행이라고 주장한다" }, { "factorId": 11, "label": "피고인은 오랜 채무로 정신적으로 지쳐 있었다고 주장한다" } ]}penaltyOptions:penalty_rule의display_order순. 화면은 여기 있는 형벌만 보여 준다(FR-3-1). 버튼 비활성 판단(S-06c)도 이 값으로 한다.- 무죄는 MVP 선택지에서 뺐다(요구사항 15장, v0.3).
penalty_rule에도 무죄 행을 두지 않는다. - (v0.4) 사형 · 무기가 법정형에 있는 사건은
penaltyOptions에DEATH·LIFE가 들어간다. 항목마다reducibleTo(감경해서 선고할 수 있는 형벌)를 함께 준다:DEATH→["LIFE", "PRISON"],LIFE→["PRISON"],PRISON·FINE→[].allowedMin~allowedMax는 감경해 징역으로 선고할 때의 범위이고,suspensionAllowed는false다. 예(살인):{ "penaltyType": "LIFE", "allowedMin": 120, "allowedMax": 600, "text": "무기징역 (감경하면 징역 10년 ~ 50년)", "suspensionAllowed": false, "reducibleTo": ["PRISON"] } - (v0.4) 선고할 수 있는 범위 막대(FR-3-2)는 고른 형벌 항목의
allowedMax를 상한으로 쓴다. 유기징역이면 30년, 무기 · 사형을 감경하면 50년이다. - 벌금 선고 가능 하한 25,000원은 형법 제45조 단서(감경 시 5만 원 미만 가능)와 제55조 제1항 제6호(벌금 감경 시 1/2)를 적용한 값이다(ERD 6장).
suspensionRule: 집행유예 가능 조건(코드 상수, ERDpenalty_rule비고). 화면에서 집행유예 입력을 보여 줄지 판단한다.factors: 사건의 판단 요소 전체(label). 공개 단계와 관계없이 모두.- 판결 입력값은 서버에 저장하지 않는다. S-04에 갔다 돌아오면 화면이 메모리에 기억한 값을 복원하고, 새로고침하면 빈 폼이다(임시 저장은 이후 단계, REQ-039).
| 거절 | 조건 |
|---|---|
409 INVALID_STATE |
REVIEWED가 아님 → currentStatus의 화면 (1-6) |
요청
json
{ "penaltyType": "PRISON", "reducedTo": null, "prisonMonths": 180, "fineAmount": null, "suspensionMonths": null, "factors": [ { "factorId": 2, "direction": "UP" }, { "factorId": 6, "direction": "UP" }, { "factorId": 7, "direction": "DOWN" } ], "freeOpinion": null}| 필드 | 필수 | 규칙 |
|---|---|---|
penaltyType |
✓ | 이 사건 penalty_rule에 있는 형벌 (법정형에서 고른 형벌: DEATH / LIFE / PRISON / FINE) |
reducedTo |
(v0.4) 감경 후 형벌. DEATH → LIFE · PRISON, LIFE → PRISON만 가능. 감경하지 않으면 null. 그 밖의 조합은 INVALID_PENALTY_TYPE. 형벌 종류가 바뀌는 감경만 기록한다. 유기징역(PRISON) 안의 작량감경은 reducedTo에 기록하지 않는다 |
|
prisonMonths |
최종 선고 형벌이 PRISON일 때 ✓ |
고른 형벌(penaltyType) 항목의 allowedMin ~ allowedMax. 최종 선고 형벌이 DEATH · LIFE면 null |
fineAmount |
FINE일 때 ✓ |
allowedMin ~ allowedMax |
suspensionMonths |
형벌이 집행유예 허용이고, 징역 36개월 이하 또는 벌금 500만 원 이하일 때만. 12 ~ 60. DEATH · LIFE를 고르면 감경해도 불가 |
|
factors |
0개 이상, 이 사건 요소, 중복 없음, direction 필수(UP / DOWN) |
|
freeOpinion |
확장(REQ-033). 최대 1,000자. 비교 대상 아님 |
- 선택하지 않은 형벌의 값(
PRISON인데fineAmount)은null이어야 한다. 값이 있으면VALIDATION_ERROR. 최종 선고 형벌(reducedTo가 있으면 그 값)이DEATH·LIFE인데prisonMonths·fineAmount·suspensionMonths에 값이 있어도VALIDATION_ERROR다. - (v0.4) 최종 선고 형벌은
reducedTo가 있으면 그 값, 없으면penaltyType이다. 예: 무기징역 그대로{ "penaltyType": "LIFE", "reducedTo": null, "prisonMonths": null }, 무기징역을 감경해 징역 40년{ "penaltyType": "LIFE", "reducedTo": "PRISON", "prisonMonths": 480 } - 무죄는 MVP에서 받지 않는다(요구사항 15장, v0.3).
penalty_rule에 없는 형벌이므로INVALID_PENALTY_TYPE으로 거절된다. - (v0.9)
penaltyType이 형벌 열거값(DEATH·LIFE·PRISON·FINE)에 없는 문자열(NOT_GUILTY, 오타 등)이어도 400이 아니라 422INVALID_PENALTY_TYPE이다. 무죄를 위 규칙대로 거절하기 위해서이며, 1-5의 "잘못된 열거값 →VALIDATION_ERROR"보다 이 규칙이 우선한다. 그래서 서버는penaltyType·reducedTo를 문자열로 받는다.
응답 200
json
{ "status": "VERDICT_CONFIRMED" }- 한 트랜잭션:
judgment(USER,FINAL) +judgment_factor저장, 상태REVIEWED→VERDICT_CONFIRMED(조건부 갱신). - 검사 순서: 상태 → 형벌(감경 조합 포함) → 형량 범위 → 집행유예 → 요소. 첫 번째로 걸린 사유 하나로 거절한다.
| 거절 | 조건 |
|---|---|
409 INVALID_STATE |
REVIEWED가 아님 (이미 확정 포함) |
422 INVALID_PENALTY_TYPE |
|
422 OUT_OF_ALLOWED_RANGE |
details에 { "field": "prisonMonths", "reason": "OUT_OF_ALLOWED_RANGE" } |
422 INVALID_SUSPENSION |
|
422 INVALID_FACTOR |
|
400 VALIDATION_ERROR |
사용자 · AI · 실제 판결을 subjectType 기준의 같은 형식으로 돌려준다(요구사항 16장, DR-1).
json
{ "subjectType": "AI", "penaltyType": "PRISON", "reducedTo": null, "prisonMonths": 144, "fineAmount": null, "suspensionMonths": null, "extraDispositions": [], "summary": "다투다 벌어진 범행과 공탁 · 반성을 함께 저울질한 판단", "reasoning": "흉기를 집어 들어 피해자를 공격하고 구호 조치 없이 자리를 떠난 점은 무겁지만, 공탁으로 피해 회복을 시도했고 범행을 인정하며 전력이 없는 점을 고려했다.", "factors": [ { "factorId": 2, "label": "다투던 중 집에 있던 흉기를 집어 들었다", "direction": "UP", "evidence": null } ]}| 필드 | USER | AI | COURT |
|---|---|---|---|
summary |
판단 요소 요약 태그로 만든 규칙 문장 (MVP) | 팀 입력 (judgment.summary) |
팀 입력 (judgment.summary) |
reasoning |
null |
판결 이유 | 재판부 판단 근거 요약 |
excerpt · plainExplanation |
없음 | 없음 | 판결문 발췌 · 쉽게 말하면 |
extraDispositions |
[] |
[] |
부가 처분 (예: 사회봉사 80시간, 몰수). type: COMMUNITY_SERVICE · CONFISCATION (ERD v1.4) |
reducedTo |
제출값 | 등록값 | 등록값. 감경 후 형벌 (v0.4, ERD judgment.reduced_to) |
factors[].evidence |
null |
null |
판결문 근거 문장 |
factors에는 고려한 요소만 들어간다. 없는 요소는 "—"(ERD 결정 #1).summary는 S-09 판결 카드의 한 줄 요약이다.- USER (MVP): 사용자가 고른 요소의 요약 태그(
factor.summary_tag)를 방향별로 모아 서버가 규칙 문장으로 만든다. AI를 부르지 않는다. 문장 틀 확정 (COMMON-16, 2026-10-06): "{↑ 태그들}을 무겁게 보고{↓ 태그들}을 감안한 판단". ↑만 있으면 "{↑ 태그들}을 무겁게 본 판단", ↓만 있으면 "{↓ 태그들}을 감안한 판단", 고른 요소가 없으면 "판단 요소를 고르지 않은 판단". 같은 태그는 한 번만 쓴다. 같은 태그를 가진 요소를 하나는 ↑, 다른 하나는 ↓로 골랐으면 먼저 고른(표시 순서가 앞선) 방향에만 남긴다 — 한 사건 안의 여러 요소가 같은 태그를 가질 수 있어(ERD) 그대로 두면 "A를 무겁게 보고 A를 감안한 판단"처럼 스스로 모순되는 문장이 된다 (BE-10). 태그 뒤 조사는 받침에 따라을·를을 고른다. 태그가 괄호 · 문장부호로 끝날 수 있으므로(반성(자백)) 끝에서부터 거슬러 올라가 처음 만나는 한글 또는 숫자로 판정한다(숫자는 한글로 읽은 소리 기준,전과 3→을) (BE-10). 예: 요소 2(흉기 사용, ↑) · 6(피해자의 부양 가족, ↑) · 7(범행 인정 · 반성, ↓) → "흉기 사용 · 피해자의 부양 가족을 무겁게 보고 범행 인정 · 반성을 감안한 판단" - USER (확장, REQ-062): AI 비교 분석이
DONE이면perspectives.USER.text(API 15)로 교체해 보여 준다. 실패하거나 생성 중이면 MVP 규칙 문장을 그대로 쓴다. - AI · COURT: 판결을 등록할 때 팀이 입력한 값. 없으면
null이고 카드에서 한 줄 요약을 숨긴다.
- USER (MVP): 사용자가 고른 요소의 요약 태그(
응답 200
json
{ "judgment": { "subjectType": "AI", "penaltyType": "PRISON", "prisonMonths": 144, "suspensionMonths": null, "…": "…" }, "myJudgment": { "subjectType": "USER", "penaltyType": "PRISON", "prisonMonths": 180, "suspensionMonths": null, "…": "…" }, "diffFromMine": { "samePenaltyType": true, "prisonMonthsDiff": -36, "fineAmountDiff": null, "suspensionMonthsDiff": null }, "references": ["형법 제250조", "살인범죄 양형기준", "유사 판례 5건"]}- 공개 AI 판결(
AI,is_published = true)을 DB에서 읽기만 한다. AI를 호출하지 않는다(시퀀스 6장). diffFromMine: MVP는 숫자 차이만. 형벌 종류는 최종 선고 형벌(reducedTo가 있으면 그 값, v0.4)로 비교한다. 형벌 종류가 같아도 그 종류에 해당하는 값만 내려간다 —PRISON은prisonMonthsDiff,FINE은fineAmountDiff,DEATH·LIFE는 형량 값이 없어 둘 다null이다(BE-10). 집행유예 여부가 다르면 같은 형벌로 보지 않는다 — 징역 2년 집행유예 3년과 징역 2년 실형은 개월 수가 같아도 같은 판결이 아니다(API 14 형벌 무게 순서SUSPENDED<PRISON, BE-10). 둘 다 집행유예면 유예 기간도suspensionMonthsDiff로 비교한다 — 징역 2년 집행유예 1년과 징역 2년 집행유예 3년은 징역 개월 수가 같아도 같은 판결이 아니다. 둘 다 집행유예가 아니면null이다 (BE-10). 형벌 종류가 다르면prisonMonthsDiff·fineAmountDiff모두null이고samePenaltyType: false다. 화면이 "내 판결보다 6개월 짧음"으로 표시한다. 문구 규칙은 미정(요구사항 15장).references: 참고 자료 태그.judgment.reference_tags(AI 판결 등록 시 팀이 입력, ERD v1.3)에서 읽는다. 값이 없으면 빈 배열. (컬럼명은 SQL 예약어REFERENCES를 피해reference_tags로 두고, 응답 필드명은references그대로 쓴다.)- 공개 사건인데 검수한 AI 판결(
AI,is_published = true)이 등록돼 있지 않으면500 INTERNAL_ERROR다. 사용자가 고칠 수 없는 데이터 문제라 거절 조건으로 두지 않는다 (BE-10).
| 거절 | 조건 |
|---|---|
409 INVALID_STATE |
VERDICT_CONFIRMED 전 → currentStatus의 화면 (1-6) |
요청: 본문 없음. 응답 200: { "status": "AI_REVEALED" }
VERDICT_CONFIRMED면AI_REVEALED로 바꾼다. 이미AI_REVEALED·COMPLETED면 상태를 그대로 두고 현재 상태를 돌려준다.- (확장) 처음 공개할 때
comparison_analysis를PENDING으로 만들고 AI 비교 분석 생성을 비동기로 시작한다(시퀀스 8장). 응답은 생성을 기다리지 않는다. - 상태를 옮기기 전에 그다음 화면이 읽을 판결(공개 AI · 재판부 판결)이 등록돼 있는지 먼저 확인한다. 확인하지 않으면 판결이 없는 사건에서 상태만 넘어가고 그 뒤 조회가 매번 500이 되어 되돌릴 방법 없이 결과 화면에 갇힌다 (BE-10).
| 거절 | 조건 |
|---|---|
409 INVALID_STATE |
VERDICT_CONFIRMED 전 → currentStatus의 화면 (1-6) |
응답 200
json
{ "judgment": { "subjectType": "COURT", "penaltyType": "PRISON", "reducedTo": null, "prisonMonths": 120, "suspensionMonths": null, "extraDispositions": [ { "type": "CONFISCATION", "value": "범행에 사용한 흉기" } ], "summary": "유족의 처벌 의사를 무겁게 보면서도 공탁과 반성을 감안한 판단", "reasoning": "…", "excerpt": "…", "plainExplanation": "…", "factors": [ { "factorId": 2, "label": "다투던 중 집에 있던 흉기를 집어 들었다", "direction": "UP", "evidence": "…" } ] }, "myJudgment": { "subjectType": "USER", "prisonMonths": 180, "suspensionMonths": null, "…": "…" }, "aiJudgment": { "subjectType": "AI", "prisonMonths": 144, "suspensionMonths": null, "…": "…" }, "source": { "sourceOrg": "법원 판결서 인터넷열람 서비스" }, "deidentifiedItems": ["인명", "지명", "사건번호", "날짜"]}source는 확장(REQ-055).case_source중is_final = true인 행의source_org. 사건번호 · 법원명 · 선고일은 절대 넣지 않는다(FR-5-3).deidentifiedItems는 확장(FR-5-2 "이 사건에 대하여").legal_case.deidentified_items에서 읽는다. 비식별화한 항목 종류만 내려가고 원래 값은 넣지 않는다. 값이 없으면 빈 배열.- 공개 사건인데 검수한 재판부 · AI 판결이 등록돼 있지 않으면
500 INTERNAL_ERROR다 (API 10과 같은 이유, BE-10).
| 거절 | 조건 |
|---|---|
409 INVALID_STATE |
AI_REVEALED 전 → currentStatus의 화면 (1-6, 보통 S-07) |
요청: 본문 없음. 응답 200: { "status": "COMPLETED" }
AI_REVEALED면COMPLETED로 바꾼다. 이미COMPLETED면 그대로 돌려준다.- 상태를 옮기기 전에 그다음 화면이 읽을 판결(공개 AI · 재판부 판결)이 등록돼 있는지 먼저 확인한다. 확인하지 않으면 판결이 없는 사건에서 상태만 넘어가고 그 뒤 조회가 매번 500이 되어 되돌릴 방법 없이 결과 화면에 갇힌다 (BE-10).
| 거절 | 조건 |
|---|---|
409 INVALID_STATE |
AI_REVEALED 전 |
응답 200
json
{ "preToFinal": { "preJudgment": { "rangeOptionId": 11, "label": "실형 5년 이상 ~ 10년 미만", "factorIds": [1, 2] }, "finalJudgmentText": "징역 15년", "direction": "HEAVIER", "summaryText": "사건을 모두 확인한 뒤, 처음 생각보다 무거운 판결을 내렸어요." }, "judgments": { "USER": { "subjectType": "USER", "summary": "흉기 사용 · 피해자의 부양 가족을 무겁게 보고 범행 인정 · 반성을 감안한 판단", "…": "…" }, "AI": { "subjectType": "AI", "summary": "다투다 벌어진 범행과 공탁 · 반성을 함께 저울질한 판단", "…": "…" }, "COURT": { "subjectType": "COURT", "summary": "유족의 처벌 의사를 무겁게 보면서도 공탁과 반성을 감안한 판단", "…": "…" } }, "matrix": [ { "factorId": 2, "label": "다투던 중 집에 있던 흉기를 집어 들었다", "revealStage": "OVERVIEW", "user": "UP", "ai": "UP", "court": "UP", "category": "ALL_SAME" }, { "factorId": 6, "label": "피해자에게는 부양하던 어린 자녀 2명이 있다", "revealStage": "DETAIL", "user": "UP", "ai": null, "court": "UP", "category": "DIVERGED" }, { "factorId": 7, "label": "수사 초기부터 범행을 인정하고 반성하고 있다", "revealStage": "DETAIL", "user": "DOWN", "ai": "DOWN", "court": "DOWN", "category": "ALL_SAME" }, { "factorId": 9, "label": "피해 회복을 위해 5,000만 원을 공탁했다", "revealStage": "DETAIL", "user": null, "ai": "DOWN", "court": "DOWN", "category": "ONLY_ME_MISSED" } ], "ruleSentences": { "common": ["세 판결 모두 흉기 사용을 형량을 높이는 요소로, 범행 인정 · 반성을 형량을 낮추는 요소로 봤어요."], "differences": ["AI와 재판부는 피해 회복 공탁을 고려했지만, 내 판결에서는 고려하지 않았어요.", "피해자의 부양 가족에 대한 판단이 세 판결 사이에서 엇갈렸어요."] }, "analysisAvailable": false}preToFinal.direction — 사전 판단 구간과 최종 판결을 비교한다(sentence_range_option.kind · 개월 범위 사용). 확정 (BE-11, 2026-10-06)
| 값 | 기준 |
|---|---|
HEAVIER |
최종 판결이 구간보다 무거움 (예: 구간은 집행유예인데 실형, 실형 구간 상한보다 긺) |
SAME |
최종 판결이 구간 안 |
LIGHTER |
최종 판결이 구간보다 가벼움 (예: 구간은 실형인데 집행유예) |
- 형벌 종류의 무게는
FINE(벌금, 벌금 집행유예 포함) <SUSPENDED(징역 집행유예) <PRISON(실형) <LIFE(무기징역) <DEATH(사형) 순으로 먼저 비교하고(sentence_range_option.kind선언 순서), 같은 종류가PRISON이면 구간의min_months(이상) ·max_months(미만)로 더 비교한다(하한 미만은LIGHTER, 상한 이상은HEAVIER). 그 외 같은 종류(FINE·SUSPENDED·LIFE·DEATH)는 더 세분하지 않고SAME이다. 최종 판결은reducedTo가 있으면 그 값(최종 선고 형벌)으로 비교한다(ERDsentence_range_option무겁기 순서와 같다). 무죄는 MVP에서 뺐으므로 비교 대상이 아니다(v0.3).
matrix[].category — 와이어프레임 S-09의 분류 태그(FR-6-4).
| 값 | 화면 문구 | 기준 |
|---|---|---|
ALL_SAME |
셋 모두 같게 본 요소 | 세 값이 모두 같음 (셋 다 null, 즉 아무도 고려하지 않은 요소는 매트릭스에서 뺀다. ERD 6장과 같음. 단, (확장, REQ-096) 사전 판단에서 고른 요소는 changeType을 보여 주기 위해 남기고 category는 null로 둔다) |
ONLY_ME_MISSED |
나만 고려하지 않은 요소 | 나는 null, AI와 재판부는 같은 방향 |
DIVERGED |
판단이 엇갈린 요소 | 그 밖 |
ruleSentences: MVP의 공통점 · 차이점. 매트릭스로 만드는 규칙 문장이며 AI를 부르지 않는다. 문장은matrix[].label(완전한 서술문)이 아니라factor.summary_tag(요소별 요약어)로 만든다 — 라벨을 그대로 쓰면 "~다"로 끝나는 서술문 뒤에 조사가 붙어 비문이 되기 때문이다. 문장 틀은 6장 #4 확정 (BE-26 · BE-11, 2026-10-06) 참고. "정답 · 틀렸다 · 이중 잣대" 표현은 쓰지 않는다(요구사항 11장 톤 원칙 · FR-6-3).judgments.*.summary: 판결 카드의 한 줄 요약(판결 응답 공통 형식 참고).USER는 MVP 규칙 문장이다.analysisAvailable: (확장) AI 비교 분석을 API 15로 불러올 수 있는지. MVP에서는 항상false.- (확장, REQ-096)
matrix[].changeType에 판단 이유 변화 유형(NEWLY_LEARNED새로 알게 된 요소 /KEPT처음부터 알던 요소 · 판단에 그대로 반영 /WEIGHT_CHANGED이미 알던 요소의 무게가 바뀜 /null)을 넣는다. 판별 기준은 요구사항 FR-6-6:OVERVIEW이후 요소가 최종 판결에만 있으면NEWLY_LEARNED,OVERVIEW요소가 사전 · 최종 양쪽에 있으면KEPT, 한쪽에만 있으면WEIGHT_CHANGED. 사전 판단 작용 요소가 있을 때만. - 사전 판단은 이 API에서 처음 내려간다.
| 거절 | 조건 |
|---|---|
409 INVALID_STATE |
COMPLETED 전 → currentStatus의 화면 (1-6, 보통 S-08) |
(v0.14) 보류 — AI 비교 분석(REQ-062 · 063)을 11차 회의에서 확장 범위에서 제외했다. 구현하지 않으며, 아래 정의는 결정 기록으로 남긴다. S-09는 API 14의 규칙 문장 · 한 줄 요약을 계속 쓴다.
응답 200
json
{ "status": "DONE", "analysis": { "common": [ { "text": "세 판결 모두 피해 금액이 큰 점을 무겁게 봤어요.", "factorIds": [1] } ], "differences": [ { "text": "재판부는 피해자가 처벌을 원한다는 점을 형량을 높이는 요소로 봤지만, 나와 AI는 고려하지 않았어요.", "factorIds": [5] } ], "perspectives": { "USER": { "text": "돈을 받은 방식과 피해 규모를 무겁게 보고, 반성은 조금만 반영한 판단이에요.", "factorIds": [1, 3, 6] }, "AI": { "text": "…", "factorIds": [1, 3, 4] }, "COURT": { "text": "…", "factorIds": [4, 6, 7] } } }}| status | analysis | 화면 |
|---|---|---|
PENDING |
null |
"분석 중" 표시, 2 ~ 3초 뒤 다시 조회 (최대 횟수는 기술 설계에서 정함) |
DONE |
분석 | 공통점 · 차이점 영역을 교체 |
FAILED |
null |
규칙 문장 유지 (fail_reason은 응답하지 않음) |
perspectives를 포함해 항목마다factorIds가 있다. 서버 검증(목록 안의 요소인가, 금지 표현이 없는가)을 통과한 것만DONE이다(시퀀스 8장 안전장치).perspectives.USER.text는 확장 단계의 AI "내 판결" 요약이다(REQ-062).DONE이면 화면은 S-09 내 판결 카드의 한 줄 요약(API 14judgments.USER.summary, MVP 규칙 문장)을 이 문장으로 바꾼다.perspectives.USER.factorIds는 사용자가 고른 요소 안에서만 나와야 한다.
| 거절 | 조건 |
|---|---|
409 INVALID_STATE |
COMPLETED 전 |
404 EXPERIENCE_NOT_FOUND |
분석 행이 없으면(확장 배포 전에 공개된 체험) FAILED로 응답한다.
| status | 호출 가능 (조회) | 호출 가능 (변경) |
|---|---|---|
STARTED |
3, 4 | 5 |
PRE_JUDGED |
3, 6 | 7 |
REVIEWING |
3, 6 | 7 |
REVIEWED |
3, 6, 8 | 7 (그대로 통과), 9 |
VERDICT_CONFIRMED |
3, 10 | 11 |
AI_REVEALED |
3, 10, 12 | 11 (그대로 통과), 13 |
COMPLETED |
3, 10, 12, 14 (15 보류) | 11 · 13 (그대로 통과) |
API 1 · 2는 상태와 관계없이 부를 수 있다. 표에 없는 조합은 409 INVALID_STATE다.
| 화면 | 진입 시 | 버튼 |
|---|---|---|
| S-02 | 1 | 체험 시작 → 2 → 응답 status로 이동 (1-6) |
| S-03 | 4 | 판단 제출 → 5 → S-04 |
| S-04 | 6 | 읽었습니다 → 7 → 6 다시 조회. 마지막 섹션 후 S-05 |
| S-05 | 6 (summary 포함) | 판결 내리러 가기 → S-06 |
| S-06 | 8 | 확정하기 → 9 → S-07 |
| S-07 | 10 | 실제 판결 확인하기 → 11 → S-08 |
| S-08 | 12 | 세 판결 비교 보기 → 13 → S-09 |
| S-09 | 14 (15는 v0.14 보류) | 다른 사건 체험 → S-02 |
- 모든 화면은 진입 API가
409 INVALID_STATE를 돌려주면currentStatus로 1-6 표의 "보낼 화면"으로 이동한다.404 EXPERIENCE_NOT_FOUND면 S-02,404 CASE_NOT_FOUND면 S-14로 간다. 진입 전에 API 3을 따로 부르지 않는다(6장 #1 확정, 요청 1회 절약). - S-04 · S-05는 같은 API 6을 쓰므로,
PRE_JUDGED·REVIEWING에서 S-05에 들어오면 API 6이 성공한다. 이때는 응답의status가REVIEWED가 아니면 화면이 S-04로 보낸다.
| # | 내용 | 제안 | 영향 |
|---|---|---|---|
| 1 | 체험 상태 확인을 API 3으로 먼저 할지, 각 API의 INVALID_STATE로 처리할지 |
확정 (v0.3): 각 API의 INVALID_STATE로 처리 (요청 수 절약, 1-6 · 5장) |
프론트 라우팅 구조 (기술 스택 5장) |
| 2 | 쿠키 이름 · 기간, 프론트 · API 도메인 구조 | 도메인 구조 확정 (v0.3): 같은 도메인, Nginx /api 프록시(기술 스택 8장). 쿠키 NLNB_AID · 1년은 제안 유지(요구사항 15장) |
CORS · SameSite 설정 |
| 3 | preToFinal.direction 세부 비교 규칙 |
확정 (BE-11, 2026-10-06): 형벌 종류를 FINE < SUSPENDED < PRISON < LIFE < DEATH 순(sentence_range_option.kind 선언 순서)으로 먼저 비교한다. 종류가 다르면 그 순서로 HEAVIER/LIGHTER, 같은 종류가 PRISON이면 구간의 min_months/max_months(상한 미만 기준)로 더 비교하고, 그 외 종류는 세분하지 않고 SAME이다. 감경이 있으면 최종 선고 형벌(reducedTo)로 비교한다 |
S-09 한 줄 요약 문구 |
| 4 | 규칙 문장(ruleSentences) 문장 틀 |
확정 (BE-26 · BE-11, 2026-10-06): 요소 라벨(완전한 서술문)이 아니라 요약어(factor.summary_tag)로 만든다. ALL_SAME은 방향별로 요약어를 모아 "세 판결 모두 {↑ 요약어들}을 형량을 높이는 요소로[, {↓ 요약어들}을 형량을 낮추는 요소로] 봤어요.", ONLY_ME_MISSED는 "AI와 재판부는 {요약어들}을 고려했지만, 내 판결에서는 고려하지 않았어요.", DIVERGED는 "{요약어들}에 대한 판단이 세 판결 사이에서 엇갈렸어요."로 한 문장씩 만든다. 해당 분류가 없으면 각각 "세 판결이 똑같이 본 판단 요소는 없어요." / "세 판결 사이에 판단이 엇갈린 점은 없어요."를 넣는다 |
S-09 공통점 · 차이점 |
| 5 | 무죄 선택 처리 (요구사항 15장) | 확정 (v0.3): MVP에서 무죄 선택지를 뺀다. 이후 도입 여부는 추후 검토 | API 8 · 9 · 14 |
| 6 | API 15 재조회 간격 · 최대 횟수, AI 생성 시간 초과 기준 | 확장 단계 | |
| 7 | 내 판결 한 줄 요약(summary) 규칙 문장 틀 · 태그 수 제한 |
확정 (COMMON-16 · BE-26, 2026-10-06): 423줄 문장 틀({↑ 태그들}을 무겁게 보고 {↓ 태그들}을 감안한 판단 등)을 확정. 방향별 태그 수는 제한하지 않는다(BE-26) — 요소마다 서로 다른 요약어가 붙어 많이 고르면 문장이 길어질 수 있지만, S-09에서 실제로 문제가 되면 상한(예: 최대 3개 + "외 N개")을 다시 검토한다 |
S-09 판결 카드 |
11 · 12차 회의에서 나온 확장 기능에 필요한 API 후보다. 경로 · 요청 · 응답은 아직 설계하지 않았다. 흐름은 user-flow.md 3장, 데이터는 ERD 8장(제안)을 따른다.
| 후보 | 용도 | 화면 | 관련 REQ |
|---|---|---|---|
| 성향 테스트 문항 조회 | 문항 · 선택지 목록 | S-15 | 112 |
| 성향 테스트 응답 제출 | 응답 저장 → 4글자 코드 계산 · 결과 저장(익명 ID) | S-15 → S-16 | 112 · 113 · 117 |
| 내 성향 결과 조회 | 현재 결과 · 축별 점수 | S-16 · S-17 | 113 · 127 |
| 유형 설명 조회 (공개) | 공유 링크용. 개인 응답 · 점수 없음 | S-16 공유 | 116 |
| 마이페이지 조회 | 성향 · 성향 변화 흐름 · 종합 성향 · 체험한 사건 목록 | S-17 | 119 · 127 |
| 관리자 검수 | 사건(DRAFT) · AI 판결(PENDING) 목록 · 상세 · 승인 · 반려 · 공개 | S-19 | 047 · 133 (BE-33) |
- 판결 체험의 성향 갱신(REQ-118)은 별도 API 없이 API 13(비교 공개)이
COMPLETED로 바꿀 때 서버가 함께 처리하는 안을 제안한다.