Skip to content

fix: statements submittedAt 멱등성 보장, 에러 로깅, moderation JSON 파싱 에러 처리 - #56

Merged
evenif99 merged 2 commits into
devfrom
fix/statements-moderation-review
Jun 18, 2026
Merged

evenif99 merged 2 commits into
devfrom
fix/statements-moderation-review

Conversation

@juahcheon

@juahcheon juahcheon commented Jun 18, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • statements/route.ts: submittedAt 미설정으로 인한 재제출 방지 로직 무력화 수정
  • statements/route.ts: 외부 catch 블록 에러 로깅 추가
  • moderation.ts: JSON.parse 파싱 실패 시 try-catch로 명확한 에러 전파

변경 상세

submittedAt 멱등성

  • fail-open(Gemini 실패) 경로와 approved 경로 모두 submittedAt: new Date() 설정
  • 기존에는 항상 null이라 line 146의 중복 제출 방지 체크(existingStatement?.submittedAt)가 동작하지 않았음
  • StatementData 인터페이스 submittedAt: null → string | null 수정

에러 로깅

  • 기존 빈 catch → console.error('[disputes/statements] api error', { message }) 패턴 적용

JSON 파싱

  • JSON.parse(jsonMatch[0]) 실패 시 catch로 감싸 'Gemini moderation returned unparseable JSON' 에러 throw

Test plan

  • 진술 저장 후 submittedAt DB 컬럼에 값 설정 확인
  • 동일 진술 재제출 시 409 CONFLICT 반환 확인
  • Gemini 응답이 비정상 JSON일 때 AI_PARSE_ERROR로 처리되는지 확인

🤖 Generated with Claude Code

Summary by CodeRabbit

Bug Fixes

  • 분쟁 진술 제출 시 응답에 정확한 제출 시간이 ISO 형식 타임스탬프로 포함되도록 개선했습니다(승인됨 및 모더레이션 대기 상태 모두 반영).
  • 모더레이션 서비스 응답을 파싱할 수 없는 경우, 원인 파악이 쉬운 명시적 오류 메시지를 제공하도록 개선했습니다.

…러 처리

- statements: fail-open/approved 경로 모두 submittedAt 설정 → 재제출 방지 멱등성 보장
- statements: StatementData.submittedAt 타입 string | null로 수정
- statements: 외부 catch 블록에 console.error 에러 로깅 추가
- moderation: JSON.parse를 try-catch로 감싸 파싱 실패 시 명확한 에러 전파

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

vercel Bot commented Jun 18, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
talky-owl Ready Ready Preview, Comment Jun 18, 2026 6:09am

@coderabbitai

coderabbitai Bot commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

진술 저장 API에서 upsert 패턴을 제거하고 createupdateMany(where submittedAt: null) 패턴으로 교체하여 submittedAt을 fail-open·approved 두 경로 모두에서 new Date()로 원자적으로 설정하고 ISO 문자열로 응답하도록 수정했다. 중복 제출 충돌은 StatementConflictError로 감지되어 409 상태로 반환된다. 모더레이션 모듈의 JSON 파싱을 try/catch로 감싸 명시적 오류를 발생시키도록 변경했다.

Changes

진술 저장 및 모더레이션 오류 처리 개선

Layer / File(s) Summary
모더레이션 JSON 파싱 오류 처리
src/lib/ai/moderation.ts
JSON.parsetry/catch로 감싸 파싱 실패 시 "Gemini moderation returned unparseable JSON" 메시지로 예외를 재발생시키도록 변경.
진술 타입 및 오류 클래스 정의
src/app/api/disputes/[id]/statements/route.ts
StatementConflictError 클래스를 추가하고, StatementData.submittedAt 타입을 string | null로 확장하며, Prisma 관련 타입을 import.
fail-open pending 저장 로직
src/app/api/disputes/[id]/statements/route.ts
모더레이션 호출 실패 시 create/updateMany(where submittedAt: null) 패턴으로 pending 상태를 저장하고 submittedAt: new Date()를 설정. 응답에서 submittedAt을 ISO 문자열로 매핑.
approved 저장 및 오류 처리 개선
src/app/api/disputes/[id]/statements/route.ts
모더레이션 성공 시 create/updateMany(where submittedAt: null) 패턴으로 approved 상태를 저장하고 submittedAt: new Date()를 설정. 충돌 시 StatementConflictError를 발생시키고, 최상단 catch에서 이를 409로 반환하며 기타 오류를 로깅 후 500으로 응답.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • I5-Project/TALKY-OWL#49: 진술 제출 엔드포인트를 신규 추가하며 모더레이션 호출 및 submittedAt 필드 처리를 포함하는 PR으로, 이번 변경과 직접적으로 동일한 코드 경로를 개선함.

Poem

🐰 upsert는 이제 작별하고,
create와 updateMany가 안겨주네.
submittedAt, 이제 원자적으로 빛나고,
JSON도 단단히 감싸졌으니,
중복 제출? 409로 막아버리지! 🛡️

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning PR 설명이 변경 내용을 구조적으로 잘 설명하고 있으나, 제공된 템플릿의 필수 섹션들이 대부분 채워지지 않았습니다. 작업 영역, Issue 번호, 요구사항 ID, 테스트 결과, 작업 범위 확인, 보안 검토 등의 필수 항목이 누락되었습니다. 템플릿의 모든 필수 섹션(담당 작업 영역, 관련 Issue, 테스트 결과 체크박스, 작업 범위 확인, 보안/개인정보 확인)을 작성하고, 각 항목에 대한 구체적인 정보를 입력하세요.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.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 제목이 변경 내용의 핵심 세 가지(submittedAt 멱등성, 에러 로깅, JSON 파싱 에러 처리)를 명확하게 요약하고 있으며, 개발자 관점에서 중요한 수정 사항들을 효과적으로 전달하고 있습니다.
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 fix/statements-moderation-review

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: 1

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]/statements/route.ts (1)

176-189: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

fail-open 응답에도 저장된 submittedAt를 반환하세요.

Line 176 및 Line 178에서 submittedAt를 저장하지만 Line 189는 여전히 null을 반환합니다. Gemini 실패 경로에서 클라이언트가 방금 제출된 진술을 미제출 상태로 해석할 수 있습니다.

수정 예시
-            submittedAt: null,
+            submittedAt: statement.submittedAt?.toISOString() ?? null,
🤖 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 176 - 189, The
code saves submittedAt with a new Date() value in the create/update operation
(lines 176 and 178), but the API response in the NextResponse.json return
statement is hardcoding submittedAt to null (line 189). Replace the null value
with the actual submittedAt timestamp from the statement object to ensure the
response accurately reflects the data that was just saved to the database,
preventing the client from misinterpreting the submission status.
🤖 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:
- Line 178: The update operations at lines 178 and 237 are not atomic and allow
concurrent requests to bypass the duplicate submission check at line 150. To fix
this, replace the unconditional `update` operation with a conditional
`updateMany` that only succeeds when `submittedAt: null`, ensuring idempotency.
When the updateMany returns zero updated records (indicating the statement was
already submitted), catch this scenario and return a 409 Conflict status code.
Alternatively, implement a unique constraint on the relevant identifier field
and handle the unique constraint violation error to return 409, preventing
duplicate moderation logs and ensuring atomic processing of the submission.

---

Outside diff comments:
In `@src/app/api/disputes/`[id]/statements/route.ts:
- Around line 176-189: The code saves submittedAt with a new Date() value in the
create/update operation (lines 176 and 178), but the API response in the
NextResponse.json return statement is hardcoding submittedAt to null (line 189).
Replace the null value with the actual submittedAt timestamp from the statement
object to ensure the response accurately reflects the data that was just saved
to the database, preventing the client from misinterpreting the submission
status.
🪄 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: f69269e8-5bef-4b34-932a-cfcb9802023b

📥 Commits

Reviewing files that changed from the base of the PR and between 162e88b and d29f375.

📒 Files selected for processing (2)
  • src/app/api/disputes/[id]/statements/route.ts
  • src/lib/ai/moderation.ts

Comment thread src/app/api/disputes/[id]/statements/route.ts Outdated
upsert가 submittedAt 여부와 무관하게 update를 실행해 동시 요청이 모두 통과하는
경쟁 조건을 제거한다.

- isNew: create 사용, P2002 unique 충돌 → StatementConflictError → 409
- !isNew: updateMany(where: submittedAt: null) 사용, count === 0 → StatementConflictError → 409
- fail-open/approved 경로 모두 동일 패턴 적용
- outer catch에서 StatementConflictError 분기하여 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.

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]/statements/route.ts (1)

143-163: ⚠️ Potential issue | 🔴 Critical

삭제된 dispute로의 진술 제출 차단 필요

코딩 가이드라인 "Deleted rooms/disputes must be blocked from access"에 따라 현재 코드는 dispute의 삭제 상태를 검증해야 합니다.

disputeParticipant.findFirst()를 호출하기 전에 dispute를 조회하여 deletedAt: null을 확인하세요. 다른 모든 dispute API 엔드포인트(disputes/[id]/route.ts, disputes/[id]/judge/route.ts, disputes/[id]/result/route.ts 등)가 동일한 패턴으로 검증하고 있습니다.

권장 수정
const dispute = await prisma.dispute.findFirst({
  where: { id: disputeId, deletedAt: null }
})
if (!dispute) {
  return NextResponse.json<ApiResponse>(
    { success: false, error: { code: 'DISPUTE_NOT_FOUND', message: '사건을 찾을 수 없습니다.' } },
    { status: 404 }
  )
}

이후 participant 조회를 수행하세요.

🤖 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 143 - 163, Add a
validation check for deleted disputes before querying for the participant.
Before the `prisma.disputeParticipant.findFirst()` call in the route handler,
first query the dispute using `prisma.dispute.findFirst()` with a condition to
ensure `deletedAt: null`, checking that the dispute exists and is not deleted.
If the dispute is not found, return a 404 response with error code
DISPUTE_NOT_FOUND. Only proceed with the existing participant query and
subsequent logic after confirming the dispute exists and has not been deleted.

Source: Coding guidelines

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

203-209: 💤 Low value

fail-open 경로와 approved 경로 간 statement 재조회 방식 불일치

approved 경로(line 285)에서는 updateManyfindUniqueOrThrow로 실제 저장된 값을 조회하지만, fail-open 경로에서는 수동으로 객체를 재구성합니다. submittedAt의 경우 new Date()를 별도로 호출하므로 DB에 저장된 타임스탬프와 응답 값이 미세하게 다를 수 있습니다.

일관성을 위해 approved 경로처럼 findUnique로 조회하거나, 최소한 updateMany 호출 전에 timestamp를 변수로 캡처하여 재사용하는 것이 좋습니다.

♻️ 타임스탬프 일관성 개선 예시
       } else {
+        const now = new Date()
         const { count } = await prisma.disputeStatement.updateMany({
           where: { id: existingStatement.id, submittedAt: null },
-          data: { content, moderationStatus: 'pending', submittedAt: new Date() },
+          data: { content, moderationStatus: 'pending', submittedAt: now },
         })
         if (count === 0) throw new StatementConflictError()
         statement = {
           ...existingStatement,
           content,
           moderationStatus: 'pending',
-          submittedAt: new Date(),
-          updatedAt: new Date(),
+          submittedAt: now,
+          updatedAt: now,
         }
       }
🤖 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 203 - 209, The
fail-open path reconstructs the statement object manually with new Date() calls
for submittedAt and updatedAt fields, which creates a timestamp mismatch with
the actual database-stored values. To fix this, either query the statement from
the database using findUnique after the updateMany call (consistent with the
approved path approach at line 285), or capture the timestamp in a variable
before the updateMany operation and reuse it in the manually reconstructed
statement object instead of calling new Date() separately for each field
assignment.
🤖 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.

Outside diff comments:
In `@src/app/api/disputes/`[id]/statements/route.ts:
- Around line 143-163: Add a validation check for deleted disputes before
querying for the participant. Before the `prisma.disputeParticipant.findFirst()`
call in the route handler, first query the dispute using
`prisma.dispute.findFirst()` with a condition to ensure `deletedAt: null`,
checking that the dispute exists and is not deleted. If the dispute is not
found, return a 404 response with error code DISPUTE_NOT_FOUND. Only proceed
with the existing participant query and subsequent logic after confirming the
dispute exists and has not been deleted.

---

Nitpick comments:
In `@src/app/api/disputes/`[id]/statements/route.ts:
- Around line 203-209: The fail-open path reconstructs the statement object
manually with new Date() calls for submittedAt and updatedAt fields, which
creates a timestamp mismatch with the actual database-stored values. To fix
this, either query the statement from the database using findUnique after the
updateMany call (consistent with the approved path approach at line 285), or
capture the timestamp in a variable before the updateMany operation and reuse it
in the manually reconstructed statement object instead of calling new Date()
separately for each field assignment.

ℹ️ Review info
⚙️ Run configuration

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

Review profile: CHILL

Plan: Pro Plus

Run ID: 3d168fd1-8256-4967-b22a-f7ad91aa3fed

📥 Commits

Reviewing files that changed from the base of the PR and between d29f375 and eea063a.

📒 Files selected for processing (1)
  • src/app/api/disputes/[id]/statements/route.ts

@juahcheon
juahcheon marked this pull request as draft June 18, 2026 06:12
@evenif99
evenif99 marked this pull request as ready for review June 18, 2026 06:13
@evenif99
evenif99 merged commit 0c033b3 into dev Jun 18, 2026
3 checks passed
@evenif99
evenif99 deleted the fix/statements-moderation-review branch June 18, 2026 06:19
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