fix: 사건 생성 플로우 개선 및 UI/성능 수정 - #107
Conversation
- aFault/bFault에서 'A가/B가' 알파벳 지칭 제거, 실제 사용자 이름 사용 - mbtiNote 별도 필드 제거, aFault/bFault 내용에 MBTI 인사이트 통합 - judge route에서 user nickname/name 조회 후 프롬프트에 전달 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- detailCode 영어 원문 의미 기준으로 구체적 유형 선택하도록 지시 추가 - expectation_mismatch 등 포괄적 유형은 최후 수단으로만 선택하도록 제한 - SOLO/DUO 프롬프트 모두 동일하게 적용 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- JudgmentResult, JudgmentTypeResult에서 useJudgment 제거, page.tsx에서 props로 전달 - 탭 전환 시 불필요한 API 재호출 방지 - closed 상태에서 판결 결과 없음 모달 버그 수정 (isJudged 조건 적용) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- 홈에서 방 생성만 하고, 진술저장 클릭 시 dispute 생성 + AI 추출 원자적 처리 - POST /api/disputes: title 하드코딩 제거, content 받아 AI 추출 후 제목 자동 설정 - /rooms/[roomId]/statement 페이지 신규 추가 (취소/진술저장 버튼) - 취소 버튼: keepalive fetch로 방 삭제 후 즉시 홈 이동 (3s 블로킹 제거) - 진술저장 중 spinner 오버레이 + 멘트 순환 표시 - 판결받기 중 멘트 단일→순환 (5개, 2.5s 간격) - statementCardEditable hover 시 border 색상 변경 제거 - getCachedSession: getServerSession → JWT decode로 교체 (DB 조회 제거) - JudgmentResult/JudgmentTypeResult: useJudgment 중복 호출 → prop으로 수신 - dispute status judged 일 때만 judgment fetch 활성화 (closed 오작동 수정) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository UI (base), Organization UI (inherited) Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (5)
📝 WalkthroughWalkthrough사건 작성 흐름을 ChangesStatement 제출 및 AI 판결 파이프라인 전면 개편
Sequence Diagram(s)sequenceDiagram
participant User
participant NewCaseButton
participant RoomsAPI as POST /api/rooms
participant StatementPage
participant DisputesAPI as POST /api/disputes
participant extractDisputeMeta
participant moderateContent
participant Router
User->>NewCaseButton: 카테고리 선택
NewCaseButton->>RoomsAPI: POST /api/rooms
RoomsAPI-->>NewCaseButton: { roomId }
NewCaseButton->>Router: /rooms/:roomId/statement?category=...
User->>StatementPage: 진술 작성 후 저장
StatementPage->>DisputesAPI: POST { roomId, categoryGroup, content }
DisputesAPI->>moderateContent: content 모더레이션 (병렬)
DisputesAPI->>extractDisputeMeta: title/summary 추출 (타임아웃 병렬)
moderateContent-->>DisputesAPI: 결과 (fail open)
extractDisputeMeta-->>DisputesAPI: 메타 or 에러코드
DisputesAPI-->>StatementPage: 201 { dispute, hasPersonalInfo }
alt hasPersonalInfo
StatementPage->>User: 개인정보 경고 모달
else
StatementPage->>Router: /disputes/:id
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 9
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/route.ts (1)
12-18:⚠️ Potential issue | 🟠 Major | ⚡ Quick win서버에서도 진술 공백과 길이를 제한하세요.
현재
min(1)은 공백 문자열과 과도하게 큰 본문을 통과시켜 Gemini 호출/DB 저장 비용을 키울 수 있습니다. 페이지의Textarea가 1000자로 제한되므로 API 스키마도 같은 상한을 강제하는 편이 안전합니다.수정 예시
+const MAX_STATEMENT_LENGTH = 1000 + const createDisputeSchema = z.object({ roomId: z.string().uuid('유효하지 않은 방 ID입니다.'), categoryGroup: z.enum(VALID_CATEGORY_GROUPS, { errorMap: () => ({ message: '카테고리는 romance, family, friend, work 중 하나여야 합니다.' }), }), - content: z.string().min(1, '진술 내용을 입력해주세요.'), + content: z + .string() + .trim() + .min(1, '진술 내용을 입력해주세요.') + .max(MAX_STATEMENT_LENGTH, `진술 내용은 ${MAX_STATEMENT_LENGTH}자 이내로 입력해주세요.`), sourceConversationId: z.string().uuid('유효하지 않은 대화 ID입니다.').optional(), })🤖 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/route.ts` around lines 12 - 18, The content field validation in createDisputeSchema currently only enforces a minimum length of 1 character, which allows whitespace-only strings and excessively long content to pass through, potentially increasing costs for Gemini API calls and database storage. Update the content field validation to both trim whitespace (to reject strings with only spaces) and enforce a maximum length of 1000 characters to match the UI constraints, ensuring consistent validation between client and server.
🤖 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/`(page)/disputes/[id]/page.tsx:
- Around line 115-123: The condition determining when to fetch judgment data
only checks for `dispute.status === 'judged'`, but the judgment tab should be
visible and data should be fetched for both `judged` and `closed` dispute
statuses. Update the `isJudged` condition to include both completed states so
that the `useJudgment` hook is triggered whenever the dispute status is either
`judged` or `closed`, not just when it is `judged` alone.
In `@src/app/`(page)/rooms/[roomId]/statement/page.tsx:
- Around line 87-91: The POST request to /api/disputes endpoint in the fetch
call is missing the MBTI value in the request body. Identify where the MBTI
state or input value is being captured in the component, and add it to the JSON
body being sent alongside the existing roomId, categoryGroup (from category),
and content properties. This fix should be applied consistently at all locations
where the /api/disputes POST request is made (including the location at lines
155-160 mentioned in the comment).
In `@src/app/`(page)/rooms/[roomId]/statement/StatementPage.module.scss:
- Around line 55-58: The stylelint rule `declaration-empty-line-before` requires
a blank line before property declarations that follow an `@include` statement.
In the .categoryLabel class, add a blank line between the `@include
m.text-caption;` statement and the `font-weight` property declaration. Apply the
same fix to the similar issue mentioned at lines 104-106 where an `@include`
statement should be separated from following properties by a blank line.
In `@src/app/api/disputes/route.ts`:
- Around line 187-190: The duplicate check for disputes using findFirst on
roomId (around line 189) is separated from the actual dispute creation logic
(around lines 270-281), creating a race condition where concurrent requests with
the same roomId could both pass validation and create duplicates. To fix this,
wrap the entire duplicate check and creation flow in a serialized Prisma
transaction to make it atomic, or alternatively add a unique constraint on the
roomId column in the dispute table and use upsert instead of the separate
findFirst check followed by create, then handle the P2002 unique constraint
violation error appropriately to ensure idempotency and prevent duplicate
dispute records.
- Around line 212-221: The moderateContent call in the Promise.allSettled array
lacks a timeout wrapper, unlike extractDisputeMeta which is protected by a
Promise.race with a META_TIMEOUT_MS timeout. Apply the same timeout pattern to
the moderateContent call by wrapping it with Promise.race and a timeout promise,
ensuring both calls have consistent timeout protection. Consider creating a
common timeout helper function to wrap both moderateContent and
extractDisputeMeta to avoid duplication and to ensure proper cleanup of timers
on successful completion to prevent memory leaks.
- Around line 224-239: The moderation failure and blocking logs are not
following security guidelines. For the console.error call in the
moderationResult.status !== 'fulfilled' branch, replace logging the full
moderationResult.reason with only safe metadata like confidenceScore and
modelName that won't expose original text or personal information. Additionally,
add a logging statement before the return in the moderation.isBlocked branch to
record the blocking action with the same safe metadata approach, ensuring that
original content and personal data are never logged as per the coding
guidelines.
In `@src/lib/ai/judgment.ts`:
- Around line 234-252: The nameA and nameB variables are assigned default values
of 'A' and 'B' when not provided, which conflicts with an instruction
prohibiting alphabet references in the prompt. Additionally, if the input names
contain template placeholders like {statementA} or newline characters, the
subsequent replaceAll() calls will corrupt the prompt structure. Normalize nameA
and nameB by sanitizing them to remove or escape newlines and template
placeholders before they are used in the replaceAll() operations with
JUDGMENT_PROMPT_SOLO and JUDGMENT_PROMPT_DUO.
In `@src/lib/auth/getSession.ts`:
- Line 14: The decode function call in getSession.ts uses a non-null assertion
on process.env.NEXTAUTH_SECRET which can lead to unpredictable runtime errors if
the environment variable is not set. Add explicit validation before calling
decode to check if NEXTAUTH_SECRET exists, and throw a clear, descriptive error
immediately if it is not defined. This provides fail-fast behavior with a
meaningful error message instead of relying on the non-null assertion operator.
- Around line 17-25: The user.id property in the Session object returned from
the getSession function is being set to decoded.sub without validation, which
results in undefined when the JWT sub claim is missing. Add validation before
constructing the return object to ensure decoded.sub exists and is not
undefined; if the sub claim is missing, the function should either throw an
error or return early to prevent downstream code from receiving a Session with
an undefined user.id that could cause unexpected behavior in authorization
checks and database queries.
---
Outside diff comments:
In `@src/app/api/disputes/route.ts`:
- Around line 12-18: The content field validation in createDisputeSchema
currently only enforces a minimum length of 1 character, which allows
whitespace-only strings and excessively long content to pass through,
potentially increasing costs for Gemini API calls and database storage. Update
the content field validation to both trim whitespace (to reject strings with
only spaces) and enforce a maximum length of 1000 characters to match the UI
constraints, ensuring consistent validation between client and server.
🪄 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: d6c7797e-dc38-4a2d-ae2f-254d603e68cf
📒 Files selected for processing (11)
src/app/(page)/disputes/[id]/DisputePage.module.scsssrc/app/(page)/disputes/[id]/page.tsxsrc/app/(page)/rooms/[roomId]/statement/StatementPage.module.scsssrc/app/(page)/rooms/[roomId]/statement/page.tsxsrc/app/api/disputes/[id]/judge/route.tssrc/app/api/disputes/route.tssrc/components/home/NewCaseButton.tsxsrc/components/judgement/JudgmentResult.tsxsrc/components/judgement/JudgmentTypeResult.tsxsrc/lib/ai/judgment.tssrc/lib/auth/getSession.ts
💤 Files with no reviewable changes (1)
- src/app/(page)/disputes/[id]/DisputePage.module.scss
| // 판결 완료/종료 상태일 때만 fetch — 불필요한 API 호출 방지 | ||
| const isJudged = !!dispute && dispute.status === 'judged'; | ||
|
|
||
| const { | ||
| data: judgment, | ||
| isLoading: judgmentLoading, | ||
| isError: judgmentError, | ||
| error: judgmentErrorData, | ||
| } = useJudgment(id, isCompleted); | ||
| } = useJudgment(id, isJudged); |
There was a problem hiding this comment.
closed 상태에서 판결 결과 조회가 막히는 회귀가 있습니다.
판결 탭은 judged/closed 모두에서 노출되는데, 현재 조회 조건이 status === 'judged'로 제한되어 closed 사건은 useJudgment가 실행되지 않습니다. 완료 상태 전체와 조회 조건을 일치시켜야 합니다.
제안 패치 예시
- // 판결 완료/종료 상태일 때만 fetch — 불필요한 API 호출 방지
- const isJudged = !!dispute && dispute.status === 'judged';
+ // 판결 완료/종료 상태에서 결과 조회
+ const shouldFetchJudgment =
+ !!dispute && (dispute.status === 'judged' || dispute.status === 'closed');
...
- } = useJudgment(id, isJudged);
+ } = useJudgment(id, shouldFetchJudgment);📝 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.
| // 판결 완료/종료 상태일 때만 fetch — 불필요한 API 호출 방지 | |
| const isJudged = !!dispute && dispute.status === 'judged'; | |
| const { | |
| data: judgment, | |
| isLoading: judgmentLoading, | |
| isError: judgmentError, | |
| error: judgmentErrorData, | |
| } = useJudgment(id, isCompleted); | |
| } = useJudgment(id, isJudged); | |
| // 판결 완료/종료 상태에서 결과 조회 | |
| const shouldFetchJudgment = | |
| !!dispute && (dispute.status === 'judged' || dispute.status === 'closed'); | |
| const { | |
| data: judgment, | |
| isLoading: judgmentLoading, | |
| isError: judgmentError, | |
| error: judgmentErrorData, | |
| } = useJudgment(id, shouldFetchJudgment); |
🤖 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/`(page)/disputes/[id]/page.tsx around lines 115 - 123, The condition
determining when to fetch judgment data only checks for `dispute.status ===
'judged'`, but the judgment tab should be visible and data should be fetched for
both `judged` and `closed` dispute statuses. Update the `isJudged` condition to
include both completed states so that the `useJudgment` hook is triggered
whenever the dispute status is either `judged` or `closed`, not just when it is
`judged` alone.
| const res = await fetch('/api/disputes', { | ||
| method: 'POST', | ||
| headers: { 'Content-Type': 'application/json' }, | ||
| body: JSON.stringify({ roomId, categoryGroup: category, content }), | ||
| }) |
There was a problem hiding this comment.
MBTI 선택값이 저장 요청에 반영되지 않습니다.
MBTI를 수정할 수 있는 UI가 있는데 POST /api/disputes 요청 본문에는 mbti가 없어 사용자 입력이 저장되지 않습니다. 저장 대상이면 API 스키마와 함께 요청에 포함하고, 저장 대상이 아니면 이 입력 UI를 제거하는 편이 맞습니다.
제안 패치 예시
- body: JSON.stringify({ roomId, categoryGroup: category, content }),
+ body: JSON.stringify({
+ roomId,
+ categoryGroup: category,
+ content,
+ mbti: mbti || null,
+ }),Also applies to: 155-160
🤖 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/`(page)/rooms/[roomId]/statement/page.tsx around lines 87 - 91, The
POST request to /api/disputes endpoint in the fetch call is missing the MBTI
value in the request body. Identify where the MBTI state or input value is being
captured in the component, and add it to the JSON body being sent alongside the
existing roomId, categoryGroup (from category), and content properties. This fix
should be applied consistently at all locations where the /api/disputes POST
request is made (including the location at lines 155-160 mentioned in the
comment).
| .categoryLabel { | ||
| @include m.text-caption; | ||
| font-weight: v.$font-weight-bold; | ||
| color: var(--text-primary); |
There was a problem hiding this comment.
Stylelint 공백 규칙을 맞춰 주세요.
Line 57, Line 106에서 @include 뒤 선언 전에 빈 줄이 없어 declaration-empty-line-before가 실패합니다.
수정 예시
.categoryLabel {
`@include` m.text-caption;
+
font-weight: v.$font-weight-bold;
color: var(--text-primary);
white-space: nowrap;
}
@@
.modalText {
`@include` m.text-body-m;
+
color: var(--text-primary);
margin: 0;
text-align: center;
}Also applies to: 104-106
🧰 Tools
🪛 Stylelint (17.13.0)
[error] 57-57: Expected empty line before declaration (declaration-empty-line-before)
(declaration-empty-line-before)
🤖 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/`(page)/rooms/[roomId]/statement/StatementPage.module.scss around
lines 55 - 58, The stylelint rule `declaration-empty-line-before` requires a
blank line before property declarations that follow an `@include` statement. In
the .categoryLabel class, add a blank line between the `@include
m.text-caption;` statement and the `font-weight` property declaration. Apply the
same fix to the similar issue mentioned at lines 104-106 where an `@include`
statement should be separated from following properties by a blank line.
Source: Linters/SAST tools
|
|
||
| if (!token) return null | ||
|
|
||
| const decoded = await decode({ token, secret: process.env.NEXTAUTH_SECRET! }) |
There was a problem hiding this comment.
NEXTAUTH_SECRET 환경변수 미설정 시 런타임 에러 발생
non-null assertion(!)으로 인해 NEXTAUTH_SECRET이 미설정된 환경에서 decode 호출 시 예측하기 어려운 에러가 발생할 수 있습니다. 명시적 검증으로 빠른 실패(fail-fast)와 명확한 에러 메시지를 제공하세요.
🛡️ 제안: 환경변수 검증 추가
+ const secret = process.env.NEXTAUTH_SECRET
+ if (!secret) {
+ console.error('NEXTAUTH_SECRET is not configured')
+ return null
+ }
+
- const decoded = await decode({ token, secret: process.env.NEXTAUTH_SECRET! })
+ const decoded = await decode({ token, secret })📝 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.
| const decoded = await decode({ token, secret: process.env.NEXTAUTH_SECRET! }) | |
| const secret = process.env.NEXTAUTH_SECRET | |
| if (!secret) { | |
| console.error('NEXTAUTH_SECRET is not configured') | |
| return null | |
| } | |
| const decoded = await decode({ token, secret }) |
🤖 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/lib/auth/getSession.ts` at line 14, The decode function call in
getSession.ts uses a non-null assertion on process.env.NEXTAUTH_SECRET which can
lead to unpredictable runtime errors if the environment variable is not set. Add
explicit validation before calling decode to check if NEXTAUTH_SECRET exists,
and throw a clear, descriptive error immediately if it is not defined. This
provides fail-fast behavior with a meaningful error message instead of relying
on the non-null assertion operator.
- disputes POST: 중복 체크를 Serializable 트랜잭션 내부로 이동 (race condition 방지) - disputes POST: moderateContent에도 8s 타임아웃 적용 (withTimeout 공통 헬퍼) - disputes POST: 모더레이션 실패/차단 로그에서 원문 제거, 안전한 메타데이터만 기록 - judgment.ts: AI 프롬프트에서 실명 제거, A/B 레이블 고정 (표시 시 user.name으로 치환) - judge/route.ts: generateAiJudgment 호출에서 nameA/nameB 제거 - getSession.ts: decoded.sub 없을 시 null 반환으로 undefined user.id 방지 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Summary
/rooms/[roomId]/statement페이지에서 진술 작성 후POST /api/disputes호출로 플로우 분리POST /api/disputes에서 AI 추출로 제목 자동 설정 (새 사건하드코딩 제거)getCachedSession을getServerSession(DB 조회) -> JWTdecode(로컬 파싱)로 교체JudgmentResult,JudgmentTypeResult에서 각각useJudgment호출하던 것을 page에서 단일 호출 후 prop 전달dispute.status === 'closed'진입 시 판결 결과 없음 모달 오작동 수정 (isJudged조건 사용)Changes
src/app/(page)/rooms/[roomId]/statement/page.tsx신규 사건 작성 페이지src/app/api/disputes/route.ts: content 파라미터 추가, 모더레이션 + AI 추출 병렬 처리, 트랜잭션으로 dispute + participant + statement 원자적 생성src/components/home/NewCaseButton.tsx: POST /api/disputes 제거, 방 생성 후 statement 페이지로 이동src/app/(page)/disputes/[id]/page.tsx: 판결 중 멘트 순환 추가 (5개, 2.5s 간격)src/app/(page)/disputes/[id]/DisputePage.module.scss: statementCardEditable hover border 제거src/lib/auth/getSession.ts: JWT decode 기반으로 교체Test plan
Generated with Claude Code
Summary by CodeRabbit
Release Notes
New Features
Improvements
UI/UX