Skip to content

feat: AI 판결 스키마 분리 및 판결 로직 구현 - #70

Merged
evenif99 merged 7 commits into
devfrom
refactor/ai-judgment-schema-split
Jun 19, 2026
Merged

feat: AI 판결 스키마 분리 및 판결 로직 구현#70
evenif99 merged 7 commits into
devfrom
refactor/ai-judgment-schema-split

Conversation

@juahcheon

@juahcheon juahcheon commented Jun 19, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • AiJudgment 스키마 필드 분리: reasoning/adviceaFault/bFault/aSuggestedLine/bSuggestedLine (역할별)
  • src/lib/ai/judgment.ts 신규 구현: Step1 extractDisputeMeta (진술 저장 시 title/summary 추출), Step2 generateAiJudgment (판결받기 시 전체 판결 JSON)
  • POST /api/disputes/[id]/judge TODO 제거 후 실제 AI 판결 로직 연결 (DB 갈등유형 조회 → Gemini 호출 → AiJudgment 저장)
  • POST /api/disputes/[id]/statements/submit 신규 라우트 (멱등성 보장)
  • GET /api/users/me 경로 스펙 수정 (/api/user/me/api/users/me)
  • dispute.api.ts / dispute.hooks.ts 신규 — TanStack Query 기반 도메인 훅
  • disputes/[id]/page.tsx raw fetch → useDispute / useRequestJudgment 훅으로 교체
  • layout.tsx<Toast /> 전역 마운트, 판결 에러 시 토스트 연결

Test plan

  • POST /api/disputes/[id]/statements/submit — 진술 미작성 시 404, 중복 요청 시 동일 응답 반환 확인
  • POST /api/disputes/[id]/judge — 1인(WAITING_OPPONENT) / 2인(BOTH_SUBMITTED) 각각 판결 요청 후 JUDGED 상태 전환 확인
  • AI 판결 실패(timeout / JSON 파싱 오류) 시 dispute 상태 원복 및 에러 토스트 노출 확인
  • GET /api/users/me 경로 변경 후 기존 호출부 정상 동작 확인
  • disputes/[id] 페이지 — TanStack Query 캐시 갱신 후 상태 반영 확인

🤖 Generated with Claude Code

Summary by CodeRabbit

릴리스 노트

  • 새로운 기능

    • 분쟁 진술 “최종 제출” API가 추가되었습니다.
    • 로그인 사용자 정보(마이 정보) 조회 API가 추가되었습니다.
    • AI 분쟁 판결 요청이 실제로 처리되어 결과 생성/저장됩니다.
  • 개선 사항

    • AI 판결 근거/제안 표현이 A/B별 별도 필드로 제공됩니다.
    • 분쟁/진술 상태에 따른 수정·제출 가능 여부와 에러 처리가 더 엄격해졌습니다.
    • 판결 요청/갱신 흐름이 훅 기반으로 정리되었고 전역 토스트가 표시됩니다.

juahcheon and others added 3 commits June 19, 2026 12:44
- AiJudgment 스키마: reasoning/advice → aFault/bFault/aSuggestedLine/bSuggestedLine 분리
- prisma generate 반영 및 seed.ts 필드 업데이트
- src/lib/ai/judgment.ts: extractDisputeMeta(Step1), generateAiJudgment(Step2) 구현
- judge route: TODO 제거 후 실제 AI 판결 로직 연결 (DB 갈등유형 조회 → AI 호출 → AiJudgment 저장)
- judgment.mapper.ts / types/judgment.ts: 새 필드 반영

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- POST /api/disputes/[id]/statements/submit: 진술 최종 제출 (멱등성 보장)
- GET /api/users/me: 경로 스펙 수정 (/api/user/me → /api/users/me)
- StatementSubmitResponse 타입 추가 (src/types/dispute.ts)
- dispute.api.ts: fetchDispute, saveStatement, submitStatement, requestJudgment
- dispute.hooks.ts: useDispute, useSaveStatement, useSubmitStatement, useRequestJudgment

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- page.tsx: useEffect+raw fetch → useDispute/useRequestJudgment 훅으로 교체
- runJudge onError에 useToastStore.show 연결
- layout.tsx: <Toast /> 전역 마운트 추가

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@vercel

vercel Bot commented Jun 19, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
talky-owl Error Error Jun 19, 2026 5:43am
talky-owl-xqdp Ready Ready Preview, Comment Jun 19, 2026 5:43am

@coderabbitai

coderabbitai Bot commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro Plus

Run ID: 82487240-146a-4c48-856e-336f45f47376

📥 Commits

Reviewing files that changed from the base of the PR and between 2706c8e and b3eaa7b.

📒 Files selected for processing (4)
  • prisma/seed.ts
  • src/app/api/disputes/[id]/judge/route.ts
  • src/domains/judgement/judgment.mapper.ts
  • src/types/judgment.ts

📝 Walkthrough

Walkthrough

Prisma 스키마에서 일부 모델 제거 및 신규 enum 7종 추가, AI 판결 DTO 필드 재구성(A/B 역할별 분리), Gemini 기반 AI 판결 생성 함수 신규 구현, 진술 제출/저장 API 개선, 분쟁 도메인 API 래퍼 및 React Query 훅 도입, 프론트엔드 훅 전환 및 글로벌 Toast 마운트가 이루어졌습니다.

Changes

AI 판결 실행 및 분쟁 도메인 레이어 구현

Layer / File(s) Summary
Prisma 스키마, AiJudgmentDto 타입 및 시드 재정의
prisma/schema.prisma, prisma/seed.ts, src/types/judgment.ts, src/types/dispute.ts, src/domains/judgement/judgment.mapper.ts
Session/VerificationToken/AuditLog/UserDeletionLog 모델 제거, 다수 @relation onDelete 옵션 제거, 7종 신규 enum(ai_error_type, ai_log_type, deletion_type, point_transaction_type, request_status, shop_item_type, statistics_source_type) 추가. AiJudgmentDtoreasoning/adviceaFault/bFault/aSuggestedLine/bSuggestedLine으로 교체하고, StatementSubmitResponse DTO 추가. seed와 mapper도 새 필드 형태로 갱신.
Gemini AI 판결 생성 로직
src/lib/ai/judgment.ts
DisputeMetaResult, ConflictTypeOption, JudgmentInput, JudgmentResult 타입 정의. extractDisputeMeta(META_PROMPT로 제목/요약 추출), generateAiJudgment(SOLO/DUO 프롬프트 분기, Gemini 호출, JSON 파싱, scoreA+scoreB 검증 및 정규화, moreResponsibleRole 필터링) 신규 구현.
POST /api/disputes/[id]/judge 실제 AI 판결 실행
src/app/api/disputes/[id]/judge/route.ts
generateAiJudgment import 활성화하여 503/AI_NOT_IMPLEMENTED 반환 블록 제거. conflictTypeDetail 조회 → A측 진술 필수 검증(누락 시 422) → generateAiJudgment 호출 → 트랜잭션으로 판결 레코드 생성 및 dispute.status JUDGED 전환.
PUT/POST /api/disputes/[id]/statements 진술 저장/관리 개선
src/app/api/disputes/[id]/statements/route.ts
extractDisputeMeta import 추가, StatementConflictError 제거. dispute 존재/상태 동시 검증(미존재 404, 불변 상태 409). 모더레이션 경로에서 pending 상태 저장 및 조건부 상태 갱신. 승인 경로에서 비동기 extractDisputeMeta 호출로 title/description 업데이트(실패는 로그만).
POST /api/disputes/[id]/statements/submit 신규 진술 제출 API
src/app/api/disputes/[id]/statements/submit/route.ts
인증(401) → 참여자/진술 조회(403/404) → 분쟁 상태 검증(불변 상태 409, ROLE_B 제약 422) → 멱등 응답(이미 제출된 경우) → 미제출 시 역할 기반 상태 계산(WAITING_OPPONENT/BOTH_SUBMITTED) → 트랜잭션으로 submittedAt과 status 원자적 업데이트 → StatementSubmitResponse 반환.
분쟁 도메인 API 래퍼 및 React Query 훅
src/domains/dispute/dispute.api.ts, src/domains/dispute/dispute.hooks.ts
fetchDispute/saveStatement/submitStatement/requestJudgment fetch 래퍼 신규 추가, parseJson 공통 헬퍼. disputeKeys, useDispute(useQuery), useSaveStatement/useSubmitStatement/useRequestJudgment(useMutation, 성공 시 캐시 invalidate) 신규 추가.
프론트엔드 통합: 훅 전환, 사용자 API, 글로벌 Toast
src/app/(page)/disputes/[id]/page.tsx, src/app/api/users/me/route.ts, src/app/layout.tsx
disputes 상세 페이지의 로컬 상태 기반 로직을 useDispute/useRequestJudgment/useToastStore 훅 기반으로 교체. GET /api/users/me 신규 추가(세션 → UserMeDto[id, nickname, mbti] 반환). RootLayout에 <Toast /> 전역 마운트.

Sequence Diagram(s)

sequenceDiagram
  participant Client as 클라이언트
  participant DisputePage as disputes/[id]/page.tsx
  participant SubmitRoute as POST /statements/submit
  participant JudgeRoute as POST /judge
  participant GenAI as generateAiJudgment
  participant DB as Prisma DB

  rect rgba(59, 130, 246, 0.5)
    note over Client, DB: 진술 최종 제출 흐름
    Client->>DisputePage: 진술 제출 버튼 클릭
    DisputePage->>SubmitRoute: submitStatement(disputeId)
    SubmitRoute->>DB: 참여자/진술/분쟁 검증
    alt submittedAt 이미 존재
      SubmitRoute-->>DisputePage: 200 멱등 응답
    else 미제출
      SubmitRoute->>DB: $transaction(submittedAt, status 갱신)
      SubmitRoute-->>DisputePage: 200 StatementSubmitResponse
    end
    DisputePage->>DisputePage: disputeKeys.detail 캐시 invalidate
  end

  rect rgba(16, 185, 129, 0.5)
    note over Client, DB: AI 판결 요청 흐름
    Client->>DisputePage: 판결받기 버튼 클릭
    DisputePage->>JudgeRoute: requestJudgment(disputeId)
    JudgeRoute->>DB: conflictTypeDetail, A/B 진술 조회
    alt A 진술 누락
      JudgeRoute-->>DisputePage: 422 STATEMENT_NOT_FOUND
    else 진술 존재
      JudgeRoute->>GenAI: generateAiJudgment(JudgmentInput)
      GenAI-->>JudgeRoute: JudgmentResult
      JudgeRoute->>DB: $transaction(judgmentResultCard, aiJudgment, dispute JUDGED)
      JudgeRoute-->>DisputePage: 200 AiJudgmentDto
      DisputePage->>DisputePage: router.refresh()
    end
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • I5-Project/TALKY-OWL#49: src/app/api/disputes/[id]/statements/route.ts의 진술 저장(모더레이션/응답 DTO) 로직과 src/app/api/disputes/[id]/judge/route.tspreviousStatus/솔로 판단 조건을 직접 변경하므로 코드 레벨에서 밀접하게 연관됩니다.
  • I5-Project/TALKY-OWL#53: src/app/(page)/disputes/[id]/page.tsx의 분쟁 상세 화면(판결 요청 흐름 및 상태별 UI) 구현/동작을 바꿔 서로 직접적으로 맞닿아 있습니다.
  • I5-Project/TALKY-OWL#56: src/app/api/disputes/[id]/statements/route.tssubmittedAt 멱등성 처리 및 진술 저장 로직이 동일한 진술 제출 코드 경로를 공유합니다.

Poem

🐰 토끼가 코드를 뛰어다니며,
Gemini에게 판결을 물어봤네~
A측 B측 잘못을 나눠 담고,
트랜잭션으로 쏙 저장했지! 🎉
이제 분쟁은 AI가 해결해요,
멱등성까지 챙긴 토끼 만세! 🐾

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning PR 설명이 주요 변경 사항은 포함하나, 필수 섹션 대부분이 누락되어 있습니다. 작업 내용, 담당 영역, 관련 이슈/요구사항, 테스트 결과, DB/API 변경 여부 등 필수 섹션을 템플릿에 따라 작성해주세요.
Docstring Coverage ⚠️ Warning Docstring coverage is 10.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목이 주요 변경 사항인 AI 판결 스키마 분리 및 판결 로직 구현을 명확하게 요약하고 있습니다.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/ai-judgment-schema-split

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/app/api/disputes/[id]/judge/route.ts (1)

121-126: ⚠️ Potential issue | 🔴 Critical | 🏗️ Heavy lift

JUDGING 잠금이 원자적이지 않아 상태가 역행할 수 있습니다.

Line 121-126은 읽기 후 갱신(TOCTOU)이라 동시 요청이 함께 JUDGING으로 진입할 수 있고, 이후 Line 199-202의 무조건 원복이 성공 요청의 JUDGED 상태를 BOTH_SUBMITTED로 되돌릴 수 있습니다(데이터 불일치).

예시 수정안
-    await prisma.dispute.update({
-      where: { id },
-      data: { status: 'JUDGING' },
-    })
+    const lock = await prisma.dispute.updateMany({
+      where: {
+        id,
+        deletedAt: null,
+        status: isSolo ? 'WAITING_OPPONENT' : 'BOTH_SUBMITTED',
+      },
+      data: { status: 'JUDGING' },
+    })
+    if (lock.count === 0) {
+      return NextResponse.json<ApiResponse>(
+        {
+          success: false,
+          error: { code: 'JUDGMENT_IN_PROGRESS', message: '이미 판결이 진행 중이거나 상태가 변경되었습니다.' },
+        },
+        { status: 409 },
+      )
+    }
@@
-      await prisma.dispute
-        .update({ where: { id }, data: { status: previousStatus } })
-        .catch(() => {})
+      await prisma.dispute
+        .updateMany({ where: { id, status: 'JUDGING' }, data: { status: previousStatus } })
+        .catch(() => {})

As per coding guidelines, src/app/api/**/*.{ts,tsx}: AI judgment request must implement idempotency to prevent duplicate processing.

Also applies to: 199-202

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/api/disputes/`[id]/judge/route.ts around lines 121 - 126, The status
update to JUDGING in the dispute update operation (lines 121-126) is not atomic
and allows multiple concurrent requests to enter the JUDGING state, causing a
race condition. Additionally, the unconditional status rollback at lines 199-202
can revert a successful JUDGED status back to BOTH_SUBMITTED. To fix this,
modify the prisma.dispute.update call to include a conditional where clause that
checks the current status is BOTH_SUBMITTED before transitioning to JUDGING
(using atomic update with status validation), and change the rollback logic at
lines 199-202 to only revert the status if the current state is still JUDGING,
not unconditionally, ensuring idempotent behavior where duplicate requests do
not corrupt the final judgment state.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@prisma/schema.prisma`:
- Around line 53-59: The Account model in the Prisma schema has field names in
snake_case (refresh_token, access_token, id_token, session_state) which violates
the coding guideline requiring camelCase field names with `@map` annotations.
Convert these four field names to camelCase (refreshToken, accessToken, idToken,
sessionState) and add a `@map` annotation to each field that maps the snake_case
database column name, ensuring the database schema remains unchanged while
aligning the Prisma field naming convention.

In `@src/app/api/disputes/`[id]/judge/route.ts:
- Around line 134-143: The code validates that statementA exists but does not
validate that statementB exists, which can cause duo dispute cases to be
incorrectly processed as solo cases when passed to generateAiJudgment. After the
existing check for missing statementA, add a similar validation check for
statementB to ensure both statements are present before proceeding. When
statementB is missing, update the dispute status back to the previousStatus, set
statusSetToJudging to false, and return an error response with an appropriate
error code and message for the B-side statement not found condition.

In `@src/app/api/disputes/`[id]/statements/submit/route.ts:
- Around line 28-31: The dispute status validation is missing before allowing
statement submissions. In the section where you fetch the participant using
prisma.disputeParticipant.findFirst, you need to also validate that the dispute
exists and is in a valid state (not deleted, closed, expired, or judged). Add a
check to retrieve the dispute first and verify its status, then return an
appropriate error response (409 or 410) if the dispute is in an invalid state.
Apply this same validation logic to the other location mentioned in the comment
(lines 70-73 range) to ensure consistent protection across all endpoints.
- Around line 49-74: The idempotency check on statement.submittedAt at line 50
occurs outside the transaction, creating a race condition with concurrent
requests. Additionally, the newDisputeStatus determination at lines 62-64 uses
only participant.role to decide between WAITING_OPPONENT and BOTH_SUBMITTED, but
this should instead be based on the actual count of submitted statements in the
database. Move the idempotency logic inside the transaction and replace the
role-based status assignment with logic that counts how many dispute statements
have been submitted (submittedAt is not null) for the given disputeId, setting
the status to BOTH_SUBMITTED only when both participants have actually submitted
their statements. This ensures the state transition reflects actual submission
counts rather than role assumptions.

In `@src/domains/dispute/dispute.api.ts`:
- Around line 9-12: The current implementation directly calls res.json() without
checking the HTTP response status (res.ok) or handling JSON parsing errors,
which causes raw SyntaxErrors when the server returns non-JSON responses like
HTML error pages. Create a common parser function that first checks if res.ok is
true, wraps the res.json() call in a try-catch block to gracefully handle JSON
parsing failures, and throws a consistent error message in both cases. Update
all fetch call sites in the dispute.api.ts file (the ones at lines 9-12, 21-23,
30-32, and 37-38 mentioned in the comment) to use this common parser function
instead of directly calling res.json().

In `@src/lib/ai/judgment.ts`:
- Line 156: The solo judgment detection logic in the isSolo variable assignment
is using a falsy check on input.statementB which incorrectly treats empty
strings as solo cases. Replace the falsy check with an explicit check for
undefined or null values to properly distinguish between a missing statementB
(true solo case) and an empty string statementB (which should be treated as a
two-person case). This ensures that actual two-person judgment requests are not
misrouted to the SOLO prompt.
- Around line 203-204: The scoreA and scoreB assignments lack validation for
ensuring the scores are within the valid 0-100 range and that they sum to
exactly 100, which can compromise data integrity. After rounding the parsed
values in the scoreA and scoreB assignments, add validation checks to ensure
both scores are between 0 and 100 inclusive, and that their sum equals 100. If
validation fails, provide sensible defaults (such as setting both scores to 0)
or implement appropriate error handling to prevent invalid judgment data from
being stored.

---

Outside diff comments:
In `@src/app/api/disputes/`[id]/judge/route.ts:
- Around line 121-126: The status update to JUDGING in the dispute update
operation (lines 121-126) is not atomic and allows multiple concurrent requests
to enter the JUDGING state, causing a race condition. Additionally, the
unconditional status rollback at lines 199-202 can revert a successful JUDGED
status back to BOTH_SUBMITTED. To fix this, modify the prisma.dispute.update
call to include a conditional where clause that checks the current status is
BOTH_SUBMITTED before transitioning to JUDGING (using atomic update with status
validation), and change the rollback logic at lines 199-202 to only revert the
status if the current state is still JUDGING, not unconditionally, ensuring
idempotent behavior where duplicate requests do not corrupt the final judgment
state.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro Plus

Run ID: 49dca64f-23d4-4669-802e-60e8adf0b485

📥 Commits

Reviewing files that changed from the base of the PR and between b8d7623 and 69bd306.

📒 Files selected for processing (13)
  • prisma/schema.prisma
  • prisma/seed.ts
  • src/app/(page)/disputes/[id]/page.tsx
  • src/app/api/disputes/[id]/judge/route.ts
  • src/app/api/disputes/[id]/statements/submit/route.ts
  • src/app/api/users/me/route.ts
  • src/app/layout.tsx
  • src/domains/dispute/dispute.api.ts
  • src/domains/dispute/dispute.hooks.ts
  • src/domains/judgement/judgment.mapper.ts
  • src/lib/ai/judgment.ts
  • src/types/dispute.ts
  • src/types/judgment.ts

Comment thread prisma/schema.prisma
Comment on lines +53 to +59
refresh_token String?
access_token String?
expires_at Int?
token_type String?
scope String?
id_token String?
session_state String?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Account 모델의 Prisma 필드 네이밍이 규칙과 충돌합니다.

Line 53-59의 refresh_token, access_token, id_token, session_state는 Prisma 필드명으로 snake_case를 직접 사용하고 있어, camelCase 필드 + @map 규칙과 맞지 않습니다. DB 컬럼은 snake_case로 유지하고 Prisma 필드만 camelCase로 바꿔주세요.

예시 수정안
-  refresh_token     String?
-  access_token      String?
+  refreshToken      String?   `@map`("refresh_token")
+  accessToken       String?   `@map`("access_token")
   expires_at        Int?
   token_type        String?
   scope             String?
-  id_token          String?
-  session_state     String?
+  idToken           String?   `@map`("id_token")
+  sessionState      String?   `@map`("session_state")

As per coding guidelines, **/*.prisma: Prisma field names must use camelCase with @map annotation for snake_case column mapping.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
refresh_token String?
access_token String?
expires_at Int?
token_type String?
scope String?
id_token String?
session_state String?
refreshToken String? `@map`("refresh_token")
accessToken String? `@map`("access_token")
expires_at Int?
token_type String?
scope String?
idToken String? `@map`("id_token")
sessionState String? `@map`("session_state")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@prisma/schema.prisma` around lines 53 - 59, The Account model in the Prisma
schema has field names in snake_case (refresh_token, access_token, id_token,
session_state) which violates the coding guideline requiring camelCase field
names with `@map` annotations. Convert these four field names to camelCase
(refreshToken, accessToken, idToken, sessionState) and add a `@map` annotation to
each field that maps the snake_case database column name, ensuring the database
schema remains unchanged while aligning the Prisma field naming convention.

Source: Coding guidelines

Comment on lines +134 to +143
const statementA = dispute.statements.find((s) => s.role === 'ROLE_A' && s.submittedAt)?.content
const statementB = dispute.statements.find((s) => s.role === 'ROLE_B' && s.submittedAt)?.content

if (!statementA) {
await prisma.dispute.update({ where: { id }, data: { status: previousStatus } })
statusSetToJudging = false
return NextResponse.json<ApiResponse>(
{ success: false, error: { code: 'STATEMENT_NOT_FOUND', message: 'A측 진술을 찾을 수 없습니다.' } },
{ status: 422 },
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

2인 판결에서 B측 진술 누락 검증이 빠져 있습니다.

Line 134-143은 statementA만 검증하고 statementB는 검증하지 않습니다. 이 상태로 generateAiJudgment에 넘기면 duo 사건이어도 단독 분기 처리될 수 있어 결과가 왜곡됩니다.

예시 수정안
       if (!statementA) {
         await prisma.dispute.update({ where: { id }, data: { status: previousStatus } })
         statusSetToJudging = false
         return NextResponse.json<ApiResponse>(
           { success: false, error: { code: 'STATEMENT_NOT_FOUND', message: 'A측 진술을 찾을 수 없습니다.' } },
           { status: 422 },
         )
       }
+      if (!isSolo && !statementB) {
+        await prisma.dispute.update({ where: { id }, data: { status: previousStatus } })
+        statusSetToJudging = false
+        return NextResponse.json<ApiResponse>(
+          { success: false, error: { code: 'STATEMENT_NOT_FOUND', message: 'B측 진술을 찾을 수 없습니다.' } },
+          { status: 422 },
+        )
+      }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/api/disputes/`[id]/judge/route.ts around lines 134 - 143, The code
validates that statementA exists but does not validate that statementB exists,
which can cause duo dispute cases to be incorrectly processed as solo cases when
passed to generateAiJudgment. After the existing check for missing statementA,
add a similar validation check for statementB to ensure both statements are
present before proceeding. When statementB is missing, update the dispute status
back to the previousStatus, set statusSetToJudging to false, and return an error
response with an appropriate error code and message for the B-side statement not
found condition.

Comment thread src/app/api/disputes/[id]/statements/submit/route.ts Outdated
Comment on lines +49 to +74
// 이미 제출된 경우 멱등성 처리
if (statement.submittedAt) {
const dispute = await prisma.dispute.findUniqueOrThrow({ where: { id: disputeId } })
return NextResponse.json<ApiResponse<StatementSubmitResponse>>({
success: true,
data: {
id: statement.id,
submittedAt: statement.submittedAt.toISOString(),
disputeStatus: dispute.status.toLowerCase() as DisputeStatus,
},
})
}

const newDisputeStatus =
participant.role === 'ROLE_A' ? ('WAITING_OPPONENT' as const) : ('BOTH_SUBMITTED' as const)

const [updatedStatement, updatedDispute] = await prisma.$transaction([
prisma.disputeStatement.update({
where: { id: statement.id },
data: { submittedAt: new Date() },
}),
prisma.dispute.update({
where: { id: disputeId },
data: { status: newDisputeStatus },
}),
])

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

멱등성과 상태 전이를 같은 트랜잭션에서 실제 제출 수 기준으로 계산해야 합니다.

Line 50의 선조회(statement.submittedAt)는 동시 요청에서 TOCTOU가 발생하고, Line 62-64의 role 기반 상태 결정은 실제 양측 제출 여부와 불일치할 수 있습니다. 결과적으로 both_submitted가 조기 기록되어 후속 판결 흐름에서 불일치 상태를 만들 수 있습니다.

수정 제안 (interactive transaction + 조건부 업데이트)
-    // 이미 제출된 경우 멱등성 처리
-    if (statement.submittedAt) {
-      const dispute = await prisma.dispute.findUniqueOrThrow({ where: { id: disputeId } })
-      return NextResponse.json<ApiResponse<StatementSubmitResponse>>({
-        success: true,
-        data: {
-          id: statement.id,
-          submittedAt: statement.submittedAt.toISOString(),
-          disputeStatus: dispute.status.toLowerCase() as DisputeStatus,
-        },
-      })
-    }
-
-    const newDisputeStatus =
-      participant.role === 'ROLE_A' ? ('WAITING_OPPONENT' as const) : ('BOTH_SUBMITTED' as const)
-
-    const [updatedStatement, updatedDispute] = await prisma.$transaction([
-      prisma.disputeStatement.update({
-        where: { id: statement.id },
-        data: { submittedAt: new Date() },
-      }),
-      prisma.dispute.update({
-        where: { id: disputeId },
-        data: { status: newDisputeStatus },
-      }),
-    ])
+    const now = new Date()
+    const result = await prisma.$transaction(async (tx) => {
+      // 멱등성: 아직 제출되지 않은 경우에만 갱신
+      const updated = await tx.disputeStatement.updateMany({
+        where: { id: statement.id, submittedAt: null },
+        data: { submittedAt: now },
+      })
+
+      const currentStatement = await tx.disputeStatement.findUniqueOrThrow({
+        where: { id: statement.id },
+        select: { id: true, submittedAt: true },
+      })
+
+      // 중복 요청이면 현재 분쟁 상태 그대로 반환
+      if (updated.count === 0) {
+        const currentDispute = await tx.dispute.findUniqueOrThrow({
+          where: { id: disputeId },
+          select: { status: true },
+        })
+        return { statement: currentStatement, dispute: currentDispute }
+      }
+
+      // 실제 제출 수 기준으로 상태 계산
+      const submittedCount = await tx.disputeStatement.count({
+        where: { disputeId, submittedAt: { not: null } },
+      })
+      const nextStatus = submittedCount >= 2 ? 'BOTH_SUBMITTED' : 'WAITING_OPPONENT'
+
+      const currentDispute = await tx.dispute.update({
+        where: { id: disputeId },
+        data: { status: nextStatus },
+        select: { status: true },
+      })
+      return { statement: currentStatement, dispute: currentDispute }
+    })
@@
-        id: updatedStatement.id,
-        submittedAt: updatedStatement.submittedAt!.toISOString(),
-        disputeStatus: updatedDispute.status.toLowerCase() as DisputeStatus,
+        id: result.statement.id,
+        submittedAt: result.statement.submittedAt!.toISOString(),
+        disputeStatus: result.dispute.status.toLowerCase() as DisputeStatus,
       },
     })

As per coding guidelines, "AI judgment request, statement completion, invitation participation, and deletion/anonymization requests must implement idempotency to prevent duplicate processing."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/api/disputes/`[id]/statements/submit/route.ts around lines 49 - 74,
The idempotency check on statement.submittedAt at line 50 occurs outside the
transaction, creating a race condition with concurrent requests. Additionally,
the newDisputeStatus determination at lines 62-64 uses only participant.role to
decide between WAITING_OPPONENT and BOTH_SUBMITTED, but this should instead be
based on the actual count of submitted statements in the database. Move the
idempotency logic inside the transaction and replace the role-based status
assignment with logic that counts how many dispute statements have been
submitted (submittedAt is not null) for the given disputeId, setting the status
to BOTH_SUBMITTED only when both participants have actually submitted their
statements. This ensures the state transition reflects actual submission counts
rather than role assumptions.

Source: Coding guidelines

Comment thread src/domains/dispute/dispute.api.ts
Comment thread src/lib/ai/judgment.ts Outdated
Comment thread src/lib/ai/judgment.ts Outdated
juahcheon and others added 2 commits June 19, 2026 13:00
- statements/route.ts: 수정 허용 (CONFLICT 체크 제거, 조건부 상태 전이)
  - 최초 저장 시에만 DRAFT→WAITING_OPPONENT / OPPONENT_JOINED→BOTH_SUBMITTED 전이
  - 욕설 필터링 통과 후 extractDisputeMeta 비동기 호출 → dispute.title/description 업데이트
  - StatementConflictError 제거
- statements/submit/route.ts: ROLE_B 제출 시 OPPONENT_JOINED 상태 검증 추가
- lib/ai/judgment.ts: categoryGroup 한글 매핑(ROMANCE→연애 등) AI 프롬프트 적용
- judge/route.ts: cardTitle을 dispute.title로 교체 (summary.slice 제거)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- judgment.ts: isSolo 판별에 빈 문자열 케이스 추가 (statementB.trim() === '')
- dispute.api.ts: res.json() 직접 호출 제거, parseJson 헬퍼로 교체
  - 비JSON/빈 본문 응답에서 SyntaxError 대신 일관된 에러 메시지 반환
- submit/route.ts: 삭제·종료·판결 완료 상태 선검증 추가 (DELETED/CLOSED/EXPIRED/JUDGED/JUDGING → 409)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/app/api/disputes/[id]/statements/route.ts (1)

199-210: 💤 Low value

P2002 catch 블록이 에러를 다시 throw만 하고 있어 실질적으로 불필요한 코드입니다.

StatementConflictError 제거 후 P2002 에러에 대한 특별 처리 로직이 사라졌습니다. 현재 throw e는 외부 catch로 전달되어 500 응답으로 처리되는데, 이는 catch 블록 없이도 동일한 동작입니다.

♻️ 불필요한 catch 블록 제거 제안
       if (isNew) {
-        try {
-          statement = await prisma.disputeStatement.create({
-            data: {
-              disputeId,
-              participantId: participant.id,
-              userId,
-              role: participant.role,
-              content,
-              moderationStatus: 'pending',
-              submittedAt: new Date(),
-            },
-          })
-        } catch (e) {
-          if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === 'P2002') {
-            throw e
-          }
-          throw e
-        }
+        statement = await prisma.disputeStatement.create({
+          data: {
+            disputeId,
+            participantId: participant.id,
+            userId,
+            role: participant.role,
+            content,
+            moderationStatus: 'pending',
+            submittedAt: new Date(),
+          },
+        })
       } else {

Lines 276-281의 트랜잭션 내 동일 패턴도 함께 정리 필요합니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/api/disputes/`[id]/statements/route.ts around lines 199 - 210, The
catch block in the Prisma create operation (checking for
Prisma.PrismaClientKnownRequestError with code P2002) is redundant because it
throws the error in both the if and else branches, providing no special error
handling. Since the StatementConflictError special handling was removed, this
entire catch block serves no purpose and should be deleted entirely. The same
cleanup pattern should be applied to the similar catch block in the transaction
section around lines 276-281.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/app/api/disputes/`[id]/statements/route.ts:
- Around line 310-318: The extractDisputeMeta function call is executed whenever
any statement is saved, which causes ROLE_B's statements to overwrite the title
and description originally created by ROLE_A. To fix this, wrap the
extractDisputeMeta call with a conditional check to ensure it only executes when
the statement is being saved by ROLE_A (the dispute initiator). This preserves
the original dispute metadata based on the initiator's statement while allowing
ROLE_B to save statements without overwriting the dispute title and summary.

---

Nitpick comments:
In `@src/app/api/disputes/`[id]/statements/route.ts:
- Around line 199-210: The catch block in the Prisma create operation (checking
for Prisma.PrismaClientKnownRequestError with code P2002) is redundant because
it throws the error in both the if and else branches, providing no special error
handling. Since the StatementConflictError special handling was removed, this
entire catch block serves no purpose and should be deleted entirely. The same
cleanup pattern should be applied to the similar catch block in the transaction
section around lines 276-281.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro Plus

Run ID: 47ca6980-b2ab-4be7-a000-491649bf3dd7

📥 Commits

Reviewing files that changed from the base of the PR and between 69bd306 and a8bc195.

📒 Files selected for processing (5)
  • src/app/api/disputes/[id]/judge/route.ts
  • src/app/api/disputes/[id]/statements/route.ts
  • src/app/api/disputes/[id]/statements/submit/route.ts
  • src/domains/dispute/dispute.api.ts
  • src/lib/ai/judgment.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/domains/dispute/dispute.api.ts
  • src/app/api/disputes/[id]/judge/route.ts
  • src/lib/ai/judgment.ts

Comment thread src/app/api/disputes/[id]/statements/route.ts Outdated
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants