feat: 사건조회 페이지 분기 UI 및 AI 판결 프롬프트 개선 - #79
Conversation
- 초대 배너 추가 (isSolo && !isCompleted 조건) - 푸터 사건종료 + 판결받기 두 버튼으로 변경 - 판결 완료 후 진술/판결 탭 표시 분기 처리 - Solo/Duo 판결 프롬프트 객관성 강화 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…at/judgment-logic-ai-prompt
…at/judgment-logic-ai-prompt
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthrough분쟁 상세 페이지에 단독 참여자 대상 초대 배너와 사건종료 버튼을 추가하고, 진술서 저장 페이지를 Changes분쟁 페이지 UI·진술서·AI 판결 통합 개선
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
Deployment failed with the following error: Learn More: https://vercel.com/lsgs-projects-34d31fd6?upgradeToPro=build-rate-limit |
…at/judgment-logic-ai-prompt
…esponse로 분리 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/lib/ai/judgment.ts (1)
189-192:⚠️ Potential issue | 🟠 Major | ⚡ Quick win듀오 판결에 필수적인 scoreA와 scoreB 필드 검증을 추가하세요.
callJudgmentAi함수는summary와conflictType.code만 검증하고 있습니다. 듀오 판결의 경우scoreA와scoreB가 필수 필드임에도 검증하지 않아, AI가 해당 필드를 반환하지 않으면 Lines 236-237에서 0으로 처리되고, 정규화 로직(Lines 250-252)을 거쳐 부정확한 결과(예: scoreA=0, scoreB=100)가 생성될 수 있습니다.듀오 케이스에서
scoreA,scoreB가 숫자인지, 그리고 합이 100에 가까운지 검증하여 잘못된 AI 응답을 조기에 탐지해야 합니다.제안하는 수정
async function callJudgmentAi( model: ReturnType<InstanceType<typeof GoogleGenerativeAI>['getGenerativeModel']>, prompt: string, + isDuo: boolean = false, ): Promise<Record<string, unknown>> { const result = await model.generateContent(prompt) const text = result.response.text().trim() const jsonMatch = text.match(/\{[\s\S]*\}/) if (!jsonMatch) throw new Error('JSON') let parsed: Record<string, unknown> try { parsed = JSON.parse(jsonMatch[0]) } catch { throw new Error('JSON') } const conflictType = parsed.conflictType as { code?: unknown } | undefined if (typeof parsed.summary !== 'string' || typeof conflictType?.code !== 'string') { throw new Error('JSON') } + + if (isDuo) { + if (typeof parsed.scoreA !== 'number' || typeof parsed.scoreB !== 'number') { + throw new Error('JSON: scoreA and scoreB must be numbers for duo judgment') + } + } return parsed }그리고 호출부를 수정:
- const parsed = await callJudgmentAi(model, prompt) + const parsed = await callJudgmentAi(model, prompt, !isSolo)- attempt = await callJudgmentAi(model, prompt) + attempt = await callJudgmentAi(model, prompt, true)🤖 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/ai/judgment.ts` around lines 189 - 192, The callJudgmentAi function currently only validates summary and conflictType.code fields, but for duo judgment cases it must also validate that scoreA and scoreB are present as numbers and their sum is approximately 100. Add validation logic after the existing typeof checks to ensure scoreA and scoreB are valid numbers for duo cases, and throw an appropriate error if they are missing or invalid. This will catch incorrect AI responses early instead of allowing them to be processed with default values of 0 through the normalization logic that follows.
🧹 Nitpick comments (3)
src/app/(page)/disputes/[id]/DisputePage.module.scss (1)
229-241: ⚡ Quick win접근성 개선: 키보드 포커스 스타일 추가를 고려하세요.
초대 배너가 버튼 역할을 하므로 키보드 탐색 사용자를 위해
:focus-visible스타일을 추가하는 것이 좋습니다.♻️ 제안하는 개선안
.inviteBanner { display: flex; align-items: center; justify-content: space-between; padding: fn.r(16) fn.r(20); margin: 0 fn.r(20) fn.r(12); background: #{v.$color-primary-100}; border-radius: fn.r(8); border: none; cursor: pointer; width: calc(100% - fn.r(40)); text-align: left; + + &:focus-visible { + outline: 2px solid var(--border-focus); + outline-offset: 2px; + } }🤖 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]/DisputePage.module.scss around lines 229 - 241, The inviteBanner class is styled as a button with cursor: pointer but lacks keyboard focus styles, which impacts accessibility for keyboard navigation users. Add a :focus-visible pseudo-class rule to the inviteBanner selector that provides clear visual feedback (such as an outline or border style) when the banner receives keyboard focus, ensuring keyboard users can see which element is currently focused.src/types/dispute.ts (1)
45-49: ⚡ Quick win
SaveStatementResponse.role타입을ParticipantRole로 좁혀 계약 일관성을 유지해주세요.현재
role: string은 잘못된 값도 통과시켜, 이후 분기/매핑 오류를 컴파일 타임에 놓칠 수 있습니다.제안 diff
export interface SaveStatementResponse { id: string disputeId: string - role: string + role: ParticipantRole content: string submittedAt: string | null hasPersonalInfo: boolean }🤖 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/types/dispute.ts` around lines 45 - 49, The role property in the SaveStatementResponse interface is typed as string, which allows invalid values and prevents compile-time type checking. Change the type of the role field from string to ParticipantRole in the SaveStatementResponse interface to ensure type safety and catch invalid role values at compile time rather than runtime.src/lib/ai/judgment.ts (1)
189-192: ⚡ Quick winconflictType.code가 허용된 목록에 포함되는지 검증하는 것을 권장합니다.
현재
conflictType.code가 문자열인지만 확인하지만,input.conflictTypes에서 제공된 허용 코드 목록에 포함되는지는 검증하지 않습니다. AI가 잘못된 코드를 반환하면 데이터베이스에 저장되어 하위 시스템에서 문제를 일으킬 수 있습니다.제안하는 수정
callJudgmentAi함수 시그니처를 변경하여 허용 코드 목록을 받도록 하고:async function callJudgmentAi( model: ReturnType<InstanceType<typeof GoogleGenerativeAI>['getGenerativeModel']>, prompt: string, + allowedCodes: string[], ): Promise<Record<string, unknown>> { // ... existing code ... const conflictType = parsed.conflictType as { code?: unknown } | undefined - if (typeof parsed.summary !== 'string' || typeof conflictType?.code !== 'string') { + if ( + typeof parsed.summary !== 'string' || + typeof conflictType?.code !== 'string' || + !allowedCodes.includes(conflictType.code) + ) { throw new Error('JSON') }그리고 호출부를 수정:
+ const allowedCodes = input.conflictTypes.map((t) => t.code) - const parsed = await callJudgmentAi(model, prompt) + const parsed = await callJudgmentAi(model, prompt, allowedCodes)- attempt = await callJudgmentAi(model, prompt) + attempt = await callJudgmentAi(model, prompt, allowedCodes)🤖 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/ai/judgment.ts` around lines 189 - 192, The current validation in the callJudgmentAi function only checks if conflictType.code is a string but does not verify if it matches one of the allowed codes from the input.conflictTypes list. Modify the callJudgmentAi function signature to accept the allowed conflictTypes as a parameter, then update the validation condition alongside the existing typeof checks to ensure that conflictType.code is actually contained within the allowed codes list. Finally, update all invocations of callJudgmentAi to pass the allowed conflictTypes so that AI-generated codes are validated against the permitted values before being stored.
🤖 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:
- Line 193: The Button component displaying "사건종료" on line 193 is missing an
onClick handler, making it non-functional when clicked. Add an onClick handler
to the Button variant="outline" component that implements the case closure
functionality, or if the feature is not yet ready to implement, add the
disabled={true} attribute to the button to indicate it is currently unavailable.
Choose the appropriate approach based on whether the implementation is ready or
still pending.
- Around line 141-153: The router.push call in the invite banner button's
onClick handler is attempting to navigate to a non-existent route path at
/rooms/${dispute.roomId}/invite. Since all client routes in this project are
organized under src/app/(page)/ and there is no rooms directory, you need to
either create the missing invite page at
src/app/(page)/rooms/[id]/invite/page.tsx to match the current routing
structure, or update the router.push path to reference an existing route that
aligns with the project's routing organization.
In `@src/lib/ai/judgment.ts`:
- Around line 160-161: The Duo judgment schema for aFault and bFault fields does
not specify null allowance conditions, while the Solo judgment schema (around
Line 124) explicitly states "해당 없으면 null" for similar fields. Since the code at
Lines 265-266 allows null values for aFault and bFault, update the field
descriptions in the Duo schema to include explicit null allowance conditions
(e.g., "null일 수 있습니다" or similar phrasing) to provide clear guidance to the AI
model about when these fields can be null.
---
Outside diff comments:
In `@src/lib/ai/judgment.ts`:
- Around line 189-192: The callJudgmentAi function currently only validates
summary and conflictType.code fields, but for duo judgment cases it must also
validate that scoreA and scoreB are present as numbers and their sum is
approximately 100. Add validation logic after the existing typeof checks to
ensure scoreA and scoreB are valid numbers for duo cases, and throw an
appropriate error if they are missing or invalid. This will catch incorrect AI
responses early instead of allowing them to be processed with default values of
0 through the normalization logic that follows.
---
Nitpick comments:
In `@src/app/`(page)/disputes/[id]/DisputePage.module.scss:
- Around line 229-241: The inviteBanner class is styled as a button with cursor:
pointer but lacks keyboard focus styles, which impacts accessibility for
keyboard navigation users. Add a :focus-visible pseudo-class rule to the
inviteBanner selector that provides clear visual feedback (such as an outline or
border style) when the banner receives keyboard focus, ensuring keyboard users
can see which element is currently focused.
In `@src/lib/ai/judgment.ts`:
- Around line 189-192: The current validation in the callJudgmentAi function
only checks if conflictType.code is a string but does not verify if it matches
one of the allowed codes from the input.conflictTypes list. Modify the
callJudgmentAi function signature to accept the allowed conflictTypes as a
parameter, then update the validation condition alongside the existing typeof
checks to ensure that conflictType.code is actually contained within the allowed
codes list. Finally, update all invocations of callJudgmentAi to pass the
allowed conflictTypes so that AI-generated codes are validated against the
permitted values before being stored.
In `@src/types/dispute.ts`:
- Around line 45-49: The role property in the SaveStatementResponse interface is
typed as string, which allows invalid values and prevents compile-time type
checking. Change the type of the role field from string to ParticipantRole in
the SaveStatementResponse interface to ensure type safety and catch invalid role
values at compile time rather than runtime.
🪄 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: 7d4ab4b3-cd3a-4194-ac43-a6cfb921698f
📒 Files selected for processing (7)
src/app/(page)/disputes/[id]/DisputePage.module.scsssrc/app/(page)/disputes/[id]/page.tsxsrc/app/(page)/disputes/[id]/statement/page.tsxsrc/app/api/disputes/[id]/statements/route.tssrc/domains/dispute/dispute.api.tssrc/lib/ai/judgment.tssrc/types/dispute.ts
| {/* 초대 배너 (단독 + 미완료) */} | ||
| {isSolo && !isCompleted && ( | ||
| <button | ||
| className={styles.inviteBanner} | ||
| onClick={() => router.push(`/rooms/${dispute.roomId}/invite`)} | ||
| > | ||
| <div className={styles.inviteBannerText}> | ||
| <span className={styles.inviteBannerTitle}>상대를 초대해</span> | ||
| <span className={styles.inviteBannerDesc}>더 좋은 판결 결과를 얻어보세요</span> | ||
| </div> | ||
| <span className={styles.inviteBannerPlus}>+</span> | ||
| </button> | ||
| )} |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Description: /rooms/[id]/invite 페이지 라우트 존재 확인
# 동적 라우트 패턴으로 검색
fd -t f -e tsx -e ts 'page\.(tsx|ts)$' src/app | rg 'rooms/\[.*\]/invite'
# 또는 디렉토리 구조 확인
fd -t d 'invite' src/app | rg 'rooms'Repository: I5-Project/TALKY-OWL
Length of output: 95
🏁 Script executed:
#!/bin/bash
# Check for the client page route
fd -p 'page\.(tsx|ts)$' src/app/rooms
# Also show the directory structure
find src/app/rooms -type f -name '*.tsx' -o -name '*.ts' 2>/dev/null | head -20Repository: I5-Project/TALKY-OWL
Length of output: 165
🏁 Script executed:
#!/bin/bash
# Check the app directory structure
ls -la src/app/
# Search for any invite page routes
find src/app -type f -name '*invite*' 2>/dev/null
# List all page.tsx files to understand routing
find src/app -type f -name 'page.tsx' | head -30Repository: I5-Project/TALKY-OWL
Length of output: 1438
/rooms/[id]/invite 클라이언트 라우트가 구현되지 않았습니다.
초대 배너(141-153줄)가 router.push(\/rooms/${dispute.roomId}/invite`)로 이동하려 하지만, 해당 페이지 라우트가 존재하지 않습니다. 현재 프로젝트의 모든 클라이언트 라우트는 src/app/(page)/라우트 그룹 하에 있으며,rooms` 디렉토리 자체가 없습니다.
초대 페이지를 구현하거나 네비게이션 경로를 수정해야 합니다.
🤖 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 141 - 153, The
router.push call in the invite banner button's onClick handler is attempting to
navigate to a non-existent route path at /rooms/${dispute.roomId}/invite. Since
all client routes in this project are organized under src/app/(page)/ and there
is no rooms directory, you need to either create the missing invite page at
src/app/(page)/rooms/[id]/invite/page.tsx to match the current routing
structure, or update the router.push path to reference an existing route that
aligns with the project's routing organization.
| 판결받기 | ||
| </Button> | ||
| <div className={styles.footerRow}> | ||
| <Button variant="outline">사건종료</Button> |
There was a problem hiding this comment.
치명적: "사건종료" 버튼에 onClick 핸들러가 없습니다.
"사건종료" 버튼이 클릭해도 아무 동작을 하지 않습니다. onClick 핸들러를 구현하거나, 아직 구현 예정인 기능이라면 버튼을 임시로 비활성화하거나 제거해야 합니다.
🐛 제안하는 수정안
옵션 1: 핸들러 구현이 준비되었다면 추가
-<Button variant="outline">사건종료</Button>
+<Button variant="outline" onClick={handleCloseCase}>사건종료</Button>옵션 2: 아직 미구현이라면 비활성화 표시
-<Button variant="outline">사건종료</Button>
+<Button variant="outline" disabled>사건종료</Button>옵션 3: 구현 전까지 임시로 주석 처리
-<Button variant="outline">사건종료</Button>
+{/* <Button variant="outline" onClick={handleCloseCase}>사건종료</Button> */}📝 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.
| <Button variant="outline">사건종료</Button> | |
| <Button variant="outline" disabled>사건종료</Button> |
🤖 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 at line 193, The Button component
displaying "사건종료" on line 193 is missing an onClick handler, making it
non-functional when clicked. Add an onClick handler to the Button
variant="outline" component that implements the case closure functionality, or
if the feature is not yet ready to implement, add the disabled={true} attribute
to the button to indicate it is currently unavailable. Choose the appropriate
approach based on whether the implementation is ready or still pending.
| "aFault": "A측 표현이나 행동 중 갈등을 키운 부분 (100자 이내)", | ||
| "bFault": "B측 표현이나 행동 중 갈등을 키운 부분 (100자 이내)", |
There was a problem hiding this comment.
듀오 판결 스키마에서 aFault, bFault의 null 허용 여부를 명시하세요.
솔로 판결 스키마(Line 124)는 "해당 없으면 null"을 명시하지만, 듀오 스키마에는 aFault/bFault가 null일 수 있다는 설명이 없습니다. 코드(Lines 265-266)에서는 null을 허용하므로, AI 모델에게 명확한 지침을 제공하기 위해 프롬프트에도 null 허용 조건을 추가해야 합니다.
제안하는 수정
- "aFault": "A측 표현이나 행동 중 갈등을 키운 부분 (100자 이내)",
- "bFault": "B측 표현이나 행동 중 갈등을 키운 부분 (100자 이내)",
+ "aFault": "A측 표현이나 행동 중 갈등을 키운 부분 (100자 이내, 책임이 없으면 null)",
+ "bFault": "B측 표현이나 행동 중 갈등을 키운 부분 (100자 이내, 책임이 없으면 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/lib/ai/judgment.ts` around lines 160 - 161, The Duo judgment schema for
aFault and bFault fields does not specify null allowance conditions, while the
Solo judgment schema (around Line 124) explicitly states "해당 없으면 null" for
similar fields. Since the code at Lines 265-266 allows null values for aFault
and bFault, update the field descriptions in the Duo schema to include explicit
null allowance conditions (e.g., "null일 수 있습니다" or similar phrasing) to provide
clear guidance to the AI model about when these fields can be null.
Summary
disputes/[id]) JUDGED 전/후 분기 UI 처리isSolo && !isCompleted조건)judged) 후 진술/판결 탭 표시isLoading중복 선언 제거 및useSaveStatementhook으로 교체DisputeStatementDto에hasPersonalInfo필드 추가Test plan
judged상태에서 진술/판결 탭 전환 확인judged이후 미노출 확인🤖 Generated with Claude Code
Summary by CodeRabbit
Release Notes
새로운 기능
개선 사항