fix: disputes/[id] 페이지 UI 및 판결 플로우 수정 - #91
Conversation
- extractDisputeMeta 전용 모델을 gemini-2.0-flash-lite로 분리 - moderation 모델도 gemini-2.0-flash-lite로 변경 - 제목 30자→20자, 요약 100자→50자로 축소 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- PATCH /api/disputes/[id]/status 엔드포인트 추가 - closeDispute API 함수 및 useCloseDispute 훅 추가 - DisputePage 사건종료 버튼에 훅 연결 및 로딩/에러 처리 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- 진술 제출 시 dispute 상태를 waiting_opponent로 업데이트 - fire-and-forget 방식으로 judge API 호출 추가 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- AbortController 30초 타임아웃 방식으로 충돌 해결 (dev 기준) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- statement 제출 후 fire-and-forget judge 호출 제거 - 스피너를 DB status 기반이 아닌 isJudging (API 호출 중) 기준으로만 표시 - 판결 완료 후 query invalidation으로 자동 UI 전환 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- extractDisputeMeta fire-and-forget → await으로 변경 (title/summary 추출 완료 후 응답) - 저장 중 fullscreen 스피너 화면 추가 - 중복 PATCH /status 호출 제거 (statements route에서 이미 처리) - next/image 적용 및 카카오 이미지 도메인 허용 (next.config.ts) - avatarFallback → 기본 이미지(thumbnail-default.png) 폴백 - categoryChip inline background-color 제거 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthrough사건 종료( Changes사건 종료 기능 및 DisputePage UI 개선
진술 저장 중 로딩 오버레이 UI
AI 모델 조정 및 statements 라우트 비동기 처리
Sequence Diagram(s)sequenceDiagram
participant DisputePage
participant useCloseDispute
participant closeDispute as closeDispute()
participant StatusRoute as PATCH /api/disputes/:id/status
participant Prisma
DisputePage->>useCloseDispute: 사건종료 버튼 클릭
useCloseDispute->>closeDispute: mutationFn 호출
closeDispute->>StatusRoute: PATCH 요청 { status: "closed" }
StatusRoute->>Prisma: dispute + participant 조회
Prisma-->>StatusRoute: 결과 반환
StatusRoute->>Prisma: dispute.status 업데이트
Prisma-->>StatusRoute: 갱신된 status
StatusRoute-->>closeDispute: { success: true }
closeDispute-->>useCloseDispute: void 반환
useCloseDispute->>DisputePage: onSuccess → invalidateQueries(disputeKeys.detail)
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 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: 5
🧹 Nitpick comments (4)
src/app/(page)/disputes/[id]/page.tsx (2)
133-142: ⚡ Quick win이미지 로드 실패 시 fallback 처리 부재
next/image의src가 유효하지 않거나 로드에 실패할 경우 브라우저 기본 broken image 아이콘이 표시됩니다.onError핸들러를 추가하여 로드 실패 시 기본 이미지로 fallback 처리하는 것이 좋습니다.♻️ 이미지 로드 실패 처리 추가
+const [imageErrors, setImageErrors] = React.useState<Set<string>>(new Set()) + {dispute.participants.slice(0, 2).map((p) => { - const imgSrc = p.profileImageUrl + const imgSrc = imageErrors.has(p.id) + ? '/images/common/thumbnail-default.png' + : p.profileImageUrl ?? (p.userId === userMe?.id ? userMe?.profileImageUrl : null) ?? '/images/common/thumbnail-default.png' return ( <div key={p.id} className={styles.avatar}> - <Image src={imgSrc} alt="" fill className={styles.avatarImg} /> + <Image + src={imgSrc} + alt="" + fill + className={styles.avatarImg} + onError={() => setImageErrors(prev => new Set(prev).add(p.id))} + /> </div> ) })}🤖 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 133 - 142, The Image component rendering participant avatars in the dispute.participants.slice(0, 2).map function lacks error handling for image load failures, which will display a broken image icon if the image URL is invalid or fails to load. Add an onError handler to the Image component that sets the source to the fallback default image path '/images/common/thumbnail-default.png' when the initial image fails to load, ensuring graceful fallback behavior.
115-123: ⚡ Quick win새로고침 버튼이 쿼리 무효화만 수행하고 UI 업데이트를 보장하지 않음
queryClient.invalidateQueries는 쿼리를 stale로 표시하지만, 컴포넌트가 마운트된 상태에서 자동으로 refetch되지 않을 수 있습니다. 사용자가 새로고침 버튼을 클릭했을 때 즉시 데이터를 다시 가져오려면refetch또는invalidateQueries와 함께refetch: true옵션을 사용하는 것이 좋습니다.♻️ 즉시 refetch를 보장하는 수정안
<AutorenewRoundedIcon sx={{ fontSize: 24, color: 'var(--icon-secondary)', flexShrink: 0, cursor: 'pointer' }} - onClick={() => queryClient.invalidateQueries({ queryKey: disputeKeys.detail(id) })} + onClick={() => queryClient.invalidateQueries({ queryKey: disputeKeys.detail(id), refetchType: 'active' })} />또는 useDispute 훅의 refetch를 직접 호출:
-const { data: dispute, isLoading: fetchLoading } = useDispute(id) +const { data: dispute, isLoading: fetchLoading, refetch } = useDispute(id) // ... <AutorenewRoundedIcon sx={{ fontSize: 24, color: 'var(--icon-secondary)', flexShrink: 0, cursor: 'pointer' }} - onClick={() => queryClient.invalidateQueries({ queryKey: disputeKeys.detail(id) })} + onClick={() => refetch()} />🤖 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 onClick handler for the AutorenewRoundedIcon refresh button only invalidates the query but does not guarantee an immediate refetch of the data. Update the onClick handler to include the refetch: true option in the invalidateQueries call to ensure the data is immediately refetched when the user clicks the refresh button, or alternatively call the refetch function directly from the useDispute hook if available. This ensures the UI reflects fresh data immediately after the refresh action.next.config.ts (1)
19-22: 💤 Low value*중복 패턴 - .kakaocdn.net이 k.kakaocdn.net을 포함함
Line 17의
k.kakaocdn.net패턴은 Line 21의*.kakaocdn.net와일드카드 패턴에 포함됩니다. 두 패턴을 모두 유지해도 동작에는 문제가 없지만, 불필요한 중복을 제거하여 설정을 단순화할 수 있습니다.♻️ 중복 제거 수정안
images: { remotePatterns: [ { protocol: 'https', hostname: '*.supabase.co', pathname: '/storage/v1/object/public/**', }, - { - protocol: 'https', - hostname: 'k.kakaocdn.net', - }, { protocol: 'https', hostname: '*.kakaocdn.net', }, ], },🤖 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 `@next.config.ts` around lines 19 - 22, The specific hostname pattern k.kakaocdn.net is redundant because it is already matched by the wildcard pattern *.kakaocdn.net shown in the diff. Remove the specific k.kakaocdn.net configuration object (the one appearing before the wildcard pattern) to eliminate the duplication and simplify the configuration, as the wildcard pattern will cover all subdomains of kakaocdn.net including k.kakaocdn.net.src/app/api/disputes/[id]/status/route.ts (1)
92-97: ⚡ Quick wincatch 블록의 에러 처리가 너무 일반적임
모든 예외를 500 Internal Server Error로 처리하면 Prisma 제약 조건 위반이나 특정 데이터베이스 에러의 컨텍스트가 손실됩니다. 클라이언트가 에러 원인을 파악하기 어려워질 수 있습니다.
♻️ 구체적인 에러 타입 처리 예시
} catch (error) { + // Prisma 관련 에러는 더 구체적으로 처리 가능 + if (error instanceof Error && error.message.includes('Record to update not found')) { + return NextResponse.json<ApiResponse>( + { success: false, error: { code: 'DISPUTE_NOT_FOUND', message: '사건을 찾을 수 없습니다.' } }, + { status: 404 }, + ) + } return NextResponse.json<ApiResponse>( { success: false, error: { code: 'INTERNAL_SERVER_ERROR', message: '서버 오류가 발생했습니다.' } }, { status: 500 }, ) }🤖 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]/status/route.ts around lines 92 - 97, The catch block at the end of the dispute status update route is handling all exceptions generically and returning a 500 error without distinguishing between different error types. To fix this, replace the generic catch block with specific error type handling that checks for Prisma errors (such as constraint violations using PrismaClientKnownRequestError), validation errors, and other specific database exceptions, then return appropriate HTTP status codes and error messages for each error type while keeping a fallback handler for unexpected errors. This will allow the client to understand whether the error is due to invalid data, constraint violations, or actual server errors.
🤖 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]/statement/page.tsx:
- Around line 77-81: The loading screen container needs accessibility attributes
to announce the loading status to screen readers. Add role="status" and
aria-live="polite" attributes to the paragraph element with
className={styles.savingText} (or alternatively to the parent div with
className={styles.savingContent}). This will ensure the loading message is
properly announced to assistive device users as a status update rather than
being visually hidden from them.
In `@src/app/`(page)/disputes/[id]/statement/StatementPage.module.scss:
- Around line 135-141: The `.savingContent` class has a fixed width value of
fn.r(362) which can cause horizontal scrolling or content truncation on smaller
screens. Add a responsive constraint by including max-width: 100% alongside the
existing width property, or alternatively replace the width with min(100%,
fn.r(362)) to ensure the element does not exceed the available viewport width
while maintaining the intended size on larger screens.
In `@src/app/api/disputes/`[id]/statements/route.ts:
- Around line 316-324: The extractDisputeMeta function call lacks a timeout
mechanism, which can cause the POST request to hang indefinitely if the Gemini
API is slow or unresponsive. Wrap the await extractDisputeMeta(content) call
with a timeout wrapper (such as Promise.race or a utility function that rejects
after a specified duration like 5-10 seconds) to ensure the request doesn't
block indefinitely. If the timeout is exceeded, the existing catch block will
handle it gracefully by logging the error and allowing the response to continue
without blocking.
In `@src/app/api/disputes/`[id]/status/route.ts:
- Around line 75-81: The status update endpoint does not implement idempotency
for terminal dispute statuses. Modify the terminalStatuses check in the route
handler to allow requests when the requested status matches the current
dispute.status (e.g., closed to closed should succeed, not return 409). Extract
the requested status from the request body and compare it with the current
dispute.status before returning the 409 error response. Only return the 409
conflict error if the current status is terminal AND the requested status
differs from the current status, allowing identical status change requests to
succeed and maintain idempotency as required by the coding guidelines.
In `@src/lib/ai/judgment.ts`:
- Around line 94-96: The title and summary fields are being truncated using
String.prototype.slice, which operates on UTF-16 code units and can break
Unicode characters like emojis with surrogate pairs. Replace the slice
operations on parsed.title and parsed.summary with an Array.from based approach
that converts the string to an array of properly handled Unicode characters,
then slices at the desired character count and rejoins the result. This will
prevent surrogate pairs and basic emojis from being split mid-character.
---
Nitpick comments:
In `@next.config.ts`:
- Around line 19-22: The specific hostname pattern k.kakaocdn.net is redundant
because it is already matched by the wildcard pattern *.kakaocdn.net shown in
the diff. Remove the specific k.kakaocdn.net configuration object (the one
appearing before the wildcard pattern) to eliminate the duplication and simplify
the configuration, as the wildcard pattern will cover all subdomains of
kakaocdn.net including k.kakaocdn.net.
In `@src/app/`(page)/disputes/[id]/page.tsx:
- Around line 133-142: The Image component rendering participant avatars in the
dispute.participants.slice(0, 2).map function lacks error handling for image
load failures, which will display a broken image icon if the image URL is
invalid or fails to load. Add an onError handler to the Image component that
sets the source to the fallback default image path
'/images/common/thumbnail-default.png' when the initial image fails to load,
ensuring graceful fallback behavior.
- Around line 115-123: The onClick handler for the AutorenewRoundedIcon refresh
button only invalidates the query but does not guarantee an immediate refetch of
the data. Update the onClick handler to include the refetch: true option in the
invalidateQueries call to ensure the data is immediately refetched when the user
clicks the refresh button, or alternatively call the refetch function directly
from the useDispute hook if available. This ensures the UI reflects fresh data
immediately after the refresh action.
In `@src/app/api/disputes/`[id]/status/route.ts:
- Around line 92-97: The catch block at the end of the dispute status update
route is handling all exceptions generically and returning a 500 error without
distinguishing between different error types. To fix this, replace the generic
catch block with specific error type handling that checks for Prisma errors
(such as constraint violations using PrismaClientKnownRequestError), validation
errors, and other specific database exceptions, then return appropriate HTTP
status codes and error messages for each error type while keeping a fallback
handler for unexpected errors. This will allow the client to understand whether
the error is due to invalid data, constraint violations, or actual server
errors.
🪄 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: a978f124-7543-4458-8590-20e75a7191d2
📒 Files selected for processing (11)
next.config.tssrc/app/(page)/disputes/[id]/DisputePage.module.scsssrc/app/(page)/disputes/[id]/page.tsxsrc/app/(page)/disputes/[id]/statement/StatementPage.module.scsssrc/app/(page)/disputes/[id]/statement/page.tsxsrc/app/api/disputes/[id]/statements/route.tssrc/app/api/disputes/[id]/status/route.tssrc/domains/dispute/dispute.api.tssrc/domains/dispute/dispute.hooks.tssrc/lib/ai/judgment.tssrc/lib/ai/moderation.ts
| <div className={styles.savingScreen}> | ||
| <div className={styles.savingContent}> | ||
| <Spinner /> | ||
| <p className={styles.savingText}>{'사건 정보를 분석하고 있어요\n잠시만 기다려주세요'}</p> | ||
| </div> |
There was a problem hiding this comment.
로딩 상태를 스크린리더에 공지하도록 ARIA 속성을 추가해주세요.
현재는 시각적 텍스트만 있어 보조기기에서 저장 진행 상태를 놓칠 수 있습니다. 로딩 컨테이너(또는 텍스트)에 role="status"와 aria-live="polite"를 부여하는 것을 권장합니다.
제안 패치
- <div className={styles.savingScreen}>
+ <div className={styles.savingScreen} role="status" aria-live="polite" aria-busy="true">
<div className={styles.savingContent}>
<Spinner />
<p className={styles.savingText}>{'사건 정보를 분석하고 있어요\n잠시만 기다려주세요'}</p>
</div>
</div>📝 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.
| <div className={styles.savingScreen}> | |
| <div className={styles.savingContent}> | |
| <Spinner /> | |
| <p className={styles.savingText}>{'사건 정보를 분석하고 있어요\n잠시만 기다려주세요'}</p> | |
| </div> | |
| <div className={styles.savingScreen} role="status" aria-live="polite" aria-busy="true"> | |
| <div className={styles.savingContent}> | |
| <Spinner /> | |
| <p className={styles.savingText}>{'사건 정보를 분석하고 있어요\n잠시만 기다려주세요'}</p> | |
| </div> | |
| </div> |
🤖 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]/statement/page.tsx around lines 77 - 81, The
loading screen container needs accessibility attributes to announce the loading
status to screen readers. Add role="status" and aria-live="polite" attributes to
the paragraph element with className={styles.savingText} (or alternatively to
the parent div with className={styles.savingContent}). This will ensure the
loading message is properly announced to assistive device users as a status
update rather than being visually hidden from them.
| .savingContent { | ||
| display: flex; | ||
| flex-direction: column; | ||
| align-items: center; | ||
| gap: fn.r(20); | ||
| width: fn.r(362); | ||
| } |
There was a problem hiding this comment.
저장 오버레이 컨텐츠 폭을 반응형으로 제한해주세요.
width: fn.r(362) 고정값은 작은 화면에서 가로 스크롤/잘림을 만들 수 있습니다. max-width: 100%(또는 min(100%, fn.r(362)))를 같이 두는 편이 안전합니다.
제안 패치
.savingContent {
display: flex;
flex-direction: column;
align-items: center;
gap: fn.r(20);
- width: fn.r(362);
+ width: min(100%, fn.r(362));
}📝 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.
| .savingContent { | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| gap: fn.r(20); | |
| width: fn.r(362); | |
| } | |
| .savingContent { | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| gap: fn.r(20); | |
| width: min(100%, fn.r(362)); | |
| } |
🤖 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]/statement/StatementPage.module.scss around
lines 135 - 141, The `.savingContent` class has a fixed width value of fn.r(362)
which can cause horizontal scrolling or content truncation on smaller screens.
Add a responsive constraint by including max-width: 100% alongside the existing
width property, or alternatively replace the width with min(100%, fn.r(362)) to
ensure the element does not exceed the available viewport width while
maintaining the intended size on larger screens.
| try { | ||
| const meta = await extractDisputeMeta(content) | ||
| await prisma.dispute.update({ | ||
| where: { id: disputeId }, | ||
| data: { title: meta.title, description: meta.summary }, | ||
| }) | ||
| } catch (err) { | ||
| console.error('[statements] extractDisputeMeta failed:', err) | ||
| } |
There was a problem hiding this comment.
메타 추출 await 경로에 타임아웃이 없어 요청이 장시간 블로킹될 수 있습니다.
이제 메타 추출을 동기 대기하므로, Gemini 지연/무응답 시 POST 응답이 과도하게 늦어질 수 있습니다. 타임아웃을 두고 실패 시 현재처럼 로그만 남기고 계속 반환하는 게 안전합니다.
제안 수정안
if (participant.role === 'ROLE_A' && isNew) {
try {
- const meta = await extractDisputeMeta(content)
+ const meta = await Promise.race([
+ extractDisputeMeta(content),
+ new Promise<never>((_, reject) =>
+ setTimeout(() => reject(new Error('extractDisputeMeta timeout')), 3000),
+ ),
+ ])
await prisma.dispute.update({
where: { id: disputeId },
data: { title: meta.title, description: meta.summary },
})
} catch (err) {
console.error('[statements] extractDisputeMeta failed:', err)
}
}🤖 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 316 - 324, The
extractDisputeMeta function call lacks a timeout mechanism, which can cause the
POST request to hang indefinitely if the Gemini API is slow or unresponsive.
Wrap the await extractDisputeMeta(content) call with a timeout wrapper (such as
Promise.race or a utility function that rejects after a specified duration like
5-10 seconds) to ensure the request doesn't block indefinitely. If the timeout
is exceeded, the existing catch block will handle it gracefully by logging the
error and allowing the response to continue without blocking.
| const terminalStatuses = ['JUDGED', 'CLOSED', 'DELETED', 'EXPIRED'] | ||
| if (terminalStatuses.includes(dispute.status)) { | ||
| return NextResponse.json<ApiResponse>( | ||
| { success: false, error: { code: 'DISPUTE_NOT_MODIFIABLE', message: '이미 종료된 사건입니다.' } }, | ||
| { status: 409 }, | ||
| ) | ||
| } |
There was a problem hiding this comment.
멱등성 구현 누락 - 이미 종료된 상태에 대한 재요청 처리 필요
코딩 가이드라인에서 "사건 종료/삭제/익명화 요청은 멱등성을 구현해야 한다"고 명시되어 있습니다. 현재 코드는 이미 CLOSED 상태인 사건에 대해 409 에러를 반환하지만, 멱등성 관점에서는 동일한 상태 변경 요청(예: closed → closed)에 대해 성공 응답을 반환해야 합니다.
🔧 멱등성을 보장하는 수정안
const terminalStatuses = ['JUDGED', 'CLOSED', 'DELETED', 'EXPIRED']
-if (terminalStatuses.includes(dispute.status)) {
+if (terminalStatuses.includes(dispute.status) && dispute.status !== newStatus.toUpperCase()) {
return NextResponse.json<ApiResponse>(
{ success: false, error: { code: 'DISPUTE_NOT_MODIFIABLE', message: '이미 종료된 사건입니다.' } },
{ status: 409 },
)
}
+
+// 이미 요청한 상태면 멱등성 보장을 위해 성공 반환
+if (dispute.status === newStatus.toUpperCase()) {
+ return NextResponse.json<ApiResponse<{ status: DisputeStatus }>>({
+ success: true,
+ data: { status: dispute.status.toLowerCase() as DisputeStatus },
+ })
+}📝 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 terminalStatuses = ['JUDGED', 'CLOSED', 'DELETED', 'EXPIRED'] | |
| if (terminalStatuses.includes(dispute.status)) { | |
| return NextResponse.json<ApiResponse>( | |
| { success: false, error: { code: 'DISPUTE_NOT_MODIFIABLE', message: '이미 종료된 사건입니다.' } }, | |
| { status: 409 }, | |
| ) | |
| } | |
| const terminalStatuses = ['JUDGED', 'CLOSED', 'DELETED', 'EXPIRED'] | |
| if (terminalStatuses.includes(dispute.status) && dispute.status !== newStatus.toUpperCase()) { | |
| return NextResponse.json<ApiResponse>( | |
| { success: false, error: { code: 'DISPUTE_NOT_MODIFIABLE', message: '이미 종료된 사건입니다.' } }, | |
| { status: 409 }, | |
| ) | |
| } | |
| // 이미 요청한 상태면 멱등성 보장을 위해 성공 반환 | |
| if (dispute.status === newStatus.toUpperCase()) { | |
| return NextResponse.json<ApiResponse<{ status: DisputeStatus }>>({ | |
| success: true, | |
| data: { status: dispute.status.toLowerCase() as DisputeStatus }, | |
| }) | |
| } |
🤖 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]/status/route.ts around lines 75 - 81, The status
update endpoint does not implement idempotency for terminal dispute statuses.
Modify the terminalStatuses check in the route handler to allow requests when
the requested status matches the current dispute.status (e.g., closed to closed
should succeed, not return 409). Extract the requested status from the request
body and compare it with the current dispute.status before returning the 409
error response. Only return the 409 conflict error if the current status is
terminal AND the requested status differs from the current status, allowing
identical status change requests to succeed and maintain idempotency as required
by the coding guidelines.
Source: Coding guidelines
| title: parsed.title.slice(0, 20), | ||
| summary: parsed.summary.slice(0, 50), | ||
| modelName: META_MODEL, |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
JavaScript String.prototype.slice가 surrogate pair(이모지)를 중간에서 자를 수 있는지, 그리고 Array.from 기반 문자 단위 절단이 안전한지 확인해줘.
💡 Result:
JavaScript의 String.prototype.slice 메서드는 UTF-16 코드 유닛(code unit) 단위로 동작하므로, surrogate pair(이모지 등)의 중간을 잘라 잘못된(broken) 문자열을 생성할 수 있습니다 [1][2][3]. 따라서 이모지 처리가 필요한 경우 slice를 직접 사용하는 것은 권장되지 않습니다 [2][4][3]. Array.from이나 스프레드 연산자([...str])를 사용하여 문자열을 배열로 변환하는 방식은 String 객체의 내장 이터레이터([Symbol.iterator])를 활용하며, 이는 코드 유닛이 아닌 유니코드 코드 포인트(code point) 단위로 동작합니다 [5][6][7]. 이 방식은 단순 surrogate pair를 안전하게 유지해주므로 slice보다 훨씬 안전합니다 [1][4][8]. 하지만 주의할 점은 '안전하다'는 기준이 '코드 포인트' 단위라는 점입니다 [7][9]. 다수의 유니코드 문자로 구성된 복합 이모지(예: ZWJ 시퀀스, 피부색 수정자, 결합형 이모지)는 여전히 여러 코드 포인트로 나뉠 수 있습니다 [5][6][7]. 요약하자면: 1. String.prototype.slice: 코드 유닛 단위 절단이 발생하여 surrogate pair가 깨질 위험이 큼 [1][2][3]. 2. Array.from: 코드 포인트 단위로 분할하여 surrogate pair는 보호하지만, 복합 이모지(grapheme clusters)는 분리될 수 있음 [5][6][7]. 3. 권장 사항: 복합 이모지를 포함한 실제 '사용자가 인식하는 문자(grapheme clusters)' 단위로 정확하게 자르고 싶다면 ES2023에 도입된 Intl.Segmenter API를 사용하는 것이 가장 정확하고 안전합니다 [10].
Citations:
- 1: https://unicodefyi.com/guide/unicode-in-javascript/
- 2: https://jsguides.dev/reference/string-methods/slice/
- 3: https://javascript.info/unicode
- 4: https://attio.com/engineering/blog/javascript-string-slice-considered-harmful
- 5: https://stackoverflow.com/questions/79330809/why-does-array-from-work-differently-on-strings-than-split
- 6: https://dev.to/lionelrowe/gotchas-when-converting-strings-to-arrays-in-js-1n31
- 7: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/Symbol.iterator
- 8: https://unicodefyi.com/series/modern-web/javascript-strings/
- 9: https://symbolfyi.com/guides/javascript-unicode-strings/
- 10: https://stackoverflow.com/questions/35223206/how-to-split-unicode-string-to-characters-in-javascript
🏁 Script executed:
fd -t f judgment.tsRepository: I5-Project/TALKY-OWL
Length of output: 109
🏁 Script executed:
wc -l src/lib/ai/judgment.tsRepository: I5-Project/TALKY-OWL
Length of output: 91
🏁 Script executed:
cat -n src/lib/ai/judgment.ts | head -110 | tail -30Repository: I5-Project/TALKY-OWL
Length of output: 1091
UTF-16 코드 유닛 기반 절단으로 일부 유니코드 문자가 깨질 수 있습니다.
String.prototype.slice는 UTF-16 코드 유닛 단위로 동작하므로, 이모지 같은 서로게이트 쌍(surrogate pair)을 중간에서 잘라 손상된 문자열을 생성할 수 있습니다.
제안된 Array.from 기반 접근은 대부분의 경우 개선이 되어 기본 이모지와 서로게이트 쌍을 보호합니다. 다만 ZWJ 시퀀스나 피부색 수정자 같은 복합 이모지는 여전히 분리될 수 있으므로, 더 정확한 처리가 필요하면 Intl.Segmenter 사용을 고려하세요.
제안 수정안
+function sliceByChars(value: string, maxChars: number): string {
+ return Array.from(value).slice(0, maxChars).join('')
+}
+
return {
- title: parsed.title.slice(0, 20),
- summary: parsed.summary.slice(0, 50),
+ title: sliceByChars(parsed.title, 20),
+ summary: sliceByChars(parsed.summary, 50),
modelName: META_MODEL,
}📝 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.
| title: parsed.title.slice(0, 20), | |
| summary: parsed.summary.slice(0, 50), | |
| modelName: META_MODEL, | |
| function sliceByChars(value: string, maxChars: number): string { | |
| return Array.from(value).slice(0, maxChars).join('') | |
| } | |
| return { | |
| title: sliceByChars(parsed.title, 20), | |
| summary: sliceByChars(parsed.summary, 50), | |
| modelName: META_MODEL, | |
| } |
🤖 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 94 - 96, The title and summary fields
are being truncated using String.prototype.slice, which operates on UTF-16 code
units and can break Unicode characters like emojis with surrogate pairs. Replace
the slice operations on parsed.title and parsed.summary with an Array.from based
approach that converts the string to an array of properly handled Unicode
characters, then slices at the desired character count and rejoins the result.
This will prevent surrogate pairs and basic emojis from being split
mid-character.
- StatementPage: savingContent 폭 반응형 처리 (min(100%, 362px)) - statements/route: extractDisputeMeta에 10초 타임아웃 추가 - status/route: 동일 상태 재요청 시 멱등 성공 반환 - judgment.ts: Array.from 기반 유니코드 안전 문자열 절단 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
@evenif99 머지 부탁드립니다 |
Summary
PATCH /api/disputes/[id]/status엔드포인트 추가 및useCloseDispute훅 연결extractDisputeMetaawait 처리로 title/summary 추출 완료 후 이동useUserMe캐시 활용 + 기본 이미지 폴백,next/image적용gemini-2.0-flash-lite적용, 프롬프트 출력 길이 축소Test plan
🤖 Generated with Claude Code
Summary by CodeRabbit
릴리스 노트
새 기능
개선 사항
기타