Skip to content

Feat: 오답 다중 등록으로 수정#169

Open
Dubabbi wants to merge 22 commits intodevelopfrom
feat/#168/wrong-create
Open

Feat: 오답 다중 등록으로 수정#169
Dubabbi wants to merge 22 commits intodevelopfrom
feat/#168/wrong-create

Conversation

@Dubabbi
Copy link
Copy Markdown
Collaborator

@Dubabbi Dubabbi commented Apr 15, 2026

🔗 관련 이슈

Closes #168

💡 작업 내용

이번 작업에서는 기존 오답 등록 플로우를 단건, 다단계 등록 중심 구조에서 스캔 그룹 기반 다중 등록 플로우로 개편했어요. 변경사항이 너무 많아서 리뷰하는 데 시간이 꽤 걸릴 것 같아 미안한 마음..

1. 오답 생성 플로우 단순화

기존 wrong/create의 4단계(step-1~4) 플로우를 걷어내고, 업로드 후 AI 분류 결과를 기반으로 바로 스캔 목록과 스캔 상세 편집으로 이어지는 흐름으로 변경했어요.

  • step-2, step-3, step-4 및 관련 hook들을 삭제했습니다.
  • wrong/create/page.tsx는 업로드 + 그룹 요약 대기 역할만 담당하도록 단순화했습니다.
  • 업로드 이후에는 그룹 단위 스캔 결과를 세션에 저장하고, /wrong/create/scans로 진입하도록 변경했습니다.

결과적으로, 기존처럼 여러 단계에서 URL/로컬 상태를 계속 동기화하는 대신 업로드 -> AI 분류 완료 대기 -> 스캔 목록 확인 -> 각 스캔 상세 수정 -> 일괄 등록 완료 흐름으로 정리했어요.

2. 스캔 그룹 기반 화면 추가

업로드된 여러 문제를 한 번에 관리할 수 있도록 새로운 화면들을 추가했어요.

/wrong/create/scans

  • 업로드된 문제들을 카드 형태로 보여주는 목록 페이지를 추가했습니다.
  • 전체 / 분류 완료 / 검토 필요 탭으로 스캔 상태를 나눠 볼 수 있도록 했습니다.
  • 각 카드에서 과목/단원/썸네일 정보를 확인할 수 있습니다.
  • 검토 필요(needsReview) 여부를 그룹 요약 응답 기준으로 표시할 수 있도록 구성했습니다.

/wrong/scans/[id]

  • 개별 스캔 상세 확인 및 수정 페이지를 추가했습니다.
  • 문제 이미지/단원/유형/정답 입력을 한 화면에서 다룰 수 있도록 구성했습니다.
  • 이전/다음 문제 이동 내비게이션을 추가해서 여러 문제를 연속으로 검토할 수 있게 했습니다.
  • 편집 모달에서 과목/단원/유형을 수정할 수 있도록 했습니다.
  • 커스텀 유형 직접 추가, 비활성화, 정렬 변경 흐름도 상세 페이지 안에서 처리하도록 옮겼습니다.

3. 다중 등록 API 흐름 추가

백엔드의 그룹 생성/그룹 요약/벌크 생성 흐름을 프론트에서 처리할 수 있도록 API 레이어를 확장했습니다.

  • problemScanApi.createGroup
  • problemScanApi.getGroupSummary
  • createBulkWrongAnswerCards
  • useCreateProblemScanGroupMutation
  • useCreateBulkWrongAnswerCardsMutation

또 스캔 그룹 생성 응답과 그룹 요약 응답의 shape를 정규화할 수 있도록 problem-scan-api.ts, problem-scan-types.ts를 보강했어요.

4. 그룹 컨텍스트를 세션 스토리지에 저장

다중 등록 과정에서 각 스캔의 AI 분류 결과와 사용자가 수정한 값을 유지할 수 있도록 세션 스토리지 기반 그룹 컨텍스트를 추가했습니다.

  • group-context.ts 추가
  • 그룹별 scanId, finalUnitId, finalTypeIds, answerFormat, imageUrl, subjectName, unitName, typeNames, needsReview
    등을 저장
  • 목록/상세/완료 페이지에서 동일 그룹 문맥을 이어서 사용할 수 있도록 처리

이 구조로 바꾸면서, 기존 step 기반의 복잡한 URL 파라미터 전달과 선택 상태 관리 코드를 비교적 많이 제거할 수 있었습니다.

5. App Bar를 스캔 상세 흐름에 맞게 확장

스캔 상세 페이지 진입에 맞춰 app bar 라우팅과 UI를 확장했어요.

  • scanDetail variant를 추가했습니다.
  • 현재 문제 index를 기준으로 문제 (n/total) 형태 타이틀을 표시합니다.
  • 타이틀 드롭다운에서 같은 그룹 내 다른 문제로 바로 이동할 수 있도록 했습니다.
  • /wrong/create/scans, /wrong/scans/[id] 라우팅을 app bar에서 인식하도록 보강했습니다.

드롭다운에 사용한 triangle-scan.svg도 함께 추가했어요. 기존 triangle 아이콘을 재사용하려고 했는데 radius가 조금 달라서,
스타일로 억지로 맞추기보다 전용 svg를 추가하는 쪽으로 정리했습니다. 직접 구현하려면 결국 clip-path 등으로 형태를 다시 맞춰야 해서 svg 분리가 더 명확하다고 판단했습니다.

6. 기타 정리

  • 오답 등록 완료 페이지 문구/동선을 현재 플로우에 맞게 수정했습니다.
  • 일부 마이 페이지 관련 파일은 app bar/라우팅 영향 범위에서 함께 정리됐습니다.
  • 아이콘 상수 및 sprite 등록도 신규 아이콘에 맞게 업데이트했습니다.

💬 원하는 리뷰 방식(선택)

변경 파일도 많고 삭제, 추가한 파일이 너무 많아서, 이번에는 CodeRabbit CLI로 전체 리뷰를 한 번 먼저 돌리고 반영해둔 상태입니다. 매도 미리 맞는 게 낫다고 생각해서 ㅎㅎ 가능한 기본 피드백은 먼저 정리해뒀습니다.

그래서 이번 리뷰에서는 다음 사항들을 특히 봐주시면 좋겠어요!

  • 다중 등록 플로우가 사용자 입장에서 자연스럽게 이어지는지
  • 세션 스토리지 기반 그룹 컨텍스트 설계가 현재 요구사항에 맞는지
  • bulk create 시점과 저장 단위가 적절한지
  • app bar가 스캔 상세 요구사항을 수용하면서도 너무 과도하게 커지지 않았는지

개인적으로는 헤더가 점점 복잡해지고 있어서, 장기적으로 app bar 구조를 어떻게 분리하면 좋을지 고민해보면 좋을 것 같아요. 지금은 라우트별 분기와 variant가 계속 늘어나는 구조라, 추가 기능이 붙을수록 유지보수 난이도가 올라갈 수 있겠다고 느껴서,,

📸 Screenshots or Video(선택)

내일 아침에 추가할게유

Summary by CodeRabbit

Summary by CodeRabbit

주요 변경사항

  • New Features

    • 다중 이미지 파일 동시 업로드 및 그룹 처리 지원
    • 스캔 그룹 목록(검토 페이지) 및 스캔 상세 화면 추가 — 편집 모달, 정답(객관식/주관식) 편집, 이전/다음 네비게이션 포함
    • 그룹 컨텍스트 기반 일괄 생성(버클 생성) 및 관련 UI 흐름 도입
  • Refactor

    • 기존 다단계 생성 흐름을 단순화하고 스캔 기반 그룹 워크플로로 재구성

@vercel
Copy link
Copy Markdown

vercel bot commented Apr 15, 2026

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

Project Deployment Actions Updated (UTC)
semo Ready Ready Preview, Comment Apr 16, 2026 5:32am

@github-actions
Copy link
Copy Markdown

🔺 세모-STORYBOOK

세모 Storybook 배포 완료!!

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 15, 2026

Walkthrough

다중 파일 기반의 문제 스캔 그룹 생성 및 편집 흐름이 도입되었습니다. Step1에서 여러 파일을 업로드해 그룹을 생성하고 그룹 요약을 통해 각 스캔을 개별 편집·검토한 뒤 일괄 생성API로 문제를 등록하는 구조로 변경되었습니다. 기존 Step2–4와 관련 훅/스타일은 제거되었습니다.

Changes

Cohort / File(s) Summary
이미지 선택 핸들러 업데이트
apps/web/src/app/login/info/components/sections/profile-section/profile-section.tsx, apps/web/src/app/my/edit/components/my-profile-section.tsx, apps/web/src/app/wrong/create/hooks/step1/use-image-source-picker.ts
useImageSourcePicker의 onSelect 시그니처를 단일 File → File[]로 변경, 선택 빈 배열 방어 로직 추가 및 관련 호출자들 배열 처리로 변경
Step1: 다중 업로드 및 요약 전환
apps/web/src/app/wrong/create/components/steps/step-1.tsx, apps/web/src/app/wrong/create/hooks/step1/use-step1-summary-transition.ts
파일 다중선택 지원, 최대 개수 검증, 그룹 생성 뮤테이션 사용, 그룹 요약 폴링/준비 판정 및 그룹 컨텍스트 생성·내비게이션 로직으로 대체
다단계 UI/훅/스타일 제거
apps/web/src/app/wrong/create/components/steps/step-2.tsx, .../step-3.tsx, .../step-4.tsx, .../step.css.ts, .../wrong-create-steps/*, apps/web/src/app/wrong/create/hooks/step2/*, .../step3/*, .../step4/*, use-pointer-sort-ids.ts, use-wrong-create-submit.ts
Step2–4 관련 컴포넌트, 훅, 스타일 일괄 삭제로 클라이언트 다단계 흐름 제거
라우팅·페이지 재구성 (Step1 중심)
apps/web/src/app/wrong/create/page.tsx, apps/web/src/app/wrong/create/hooks/use-wrong-create-route.ts, apps/web/src/app/wrong/create/utils/search-params.ts
페이지가 Step1만 렌더링하도록 단순화, search params에서 scanIds/groupId 중심으로 변경, 관련 유틸 추가(readScanIds 등)
그룹 리스트 및 상세 편집 페이지 추가
apps/web/src/app/wrong/create/scans/page.tsx, .../scans/scans.css.ts, apps/web/src/app/wrong/scans/[id]/page.tsx, .../page.css.ts
그룹 기반 스캔 목록(탭 필터) 페이지와 스캔 상세 편집 페이지 추가 — 이미지, 답안 편집, 편집 모달, 하단 네비게이션 포함
그룹 컨텍스트·페이로드 유틸리티
apps/web/src/app/wrong/create/utils/group-context.ts, apps/web/src/app/wrong/scans/[id]/payload.ts, .../type-actions.ts, .../utils.ts
세션스토리지 기반 그룹 컨텍스트 생성/저장/복구, 문제 생성 일괄 페이로드 빌더와 타입/유틸 함수 추가
API 확장: 스캔 그룹·일괄 생성
apps/web/src/shared/apis/problem-scan/problem-scan-api.ts, .../problem-scan-types.ts, .../hooks/use-create-problem-scan-group-mutation.ts, apps/web/src/shared/apis/problem-create/problem-create-api.ts, .../problem-create-types.ts, .../hooks/use-create-bulk-wrong-answer-cards-mutation.ts, apps/web/src/shared/apis/constants/api-paths.ts
문제 스캔 그룹 생성·그룹 요약 API 추가, 문제 일괄 생성 API 및 타입/뮤테이션 추가, API 경로 상수 확장
AppBar 및 라우팅 확장
apps/web/src/shared/components/app-bar/*, apps/web/src/shared/components/app-bar/types/app-bar.ts, .../utils/app-bar-routing.ts, app-bar-gate.tsx
scanDetail 변형(중앙 제목 드롭다운 메뉴, 좌/우 슬롯) 추가 및 라우트 매칭 확장, useAppBar 옵션에 그룹 컨텍스트 getter 주입
스캔 상세 관련 컴포넌트·훅
apps/web/src/app/wrong/scans/[id]/components/*, apps/web/src/app/wrong/scans/[id]/hooks/*
ScanDetailHero, ScanAnswerSection, ScanBottomNav, ScanEditModal 등 컴포넌트와 useWrongScanDetail, useScanAnswerActions 훅 추가
소소한 UI/프로필/스타일 변경
apps/web/src/app/my/page.tsx, apps/web/src/app/login/info/.../profile-section.tsx, apps/web/src/app/wrong/create/components/direct-add-button/*, apps/web/src/shared/components/chip/chip.tsx, apps/web/src/shared/constants/icons.ts, apps/web/src/shared/constants/routes.ts
마이페이지 기본 이름 처리 개선, DirectAddButton 스타일·타이포·아이콘 변경, Chip에 as prop 추가, 아이콘/라우트 상수 확장

Sequence Diagram(s)

sequenceDiagram
    autonumber
    participant User as User
    participant Client as Client(UI)
    participant API as API Server
    participant Storage as sessionStorage
    participant BulkAPI as Bulk Create API

    User->>Client: 여러 파일 선택 (Step1)
    Client->>API: POST /problem-scans/groups (files[])
    API-->>Client: { groupId, scanIds[] }
    Client->>Storage: saveWrongCreateGroupContext(groupId + items)
    Client->>Client: navigate /wrong/create/scans?group=groupId
    Client->>API: GET /problem-scans/groups/{groupId}/summary
    API-->>Client: group summary (summaries...)
    loop 사용자 편집 (각 scan)
      User->>Client: 상세 열기 → /wrong/scans/{id}?group=groupId
      Client->>Storage: readWrongCreateGroupContext(groupId)
      Client->>Client: edit subject/unit/types/answer
      Client->>Storage: persistGroupItem(updatedItem)
    end
    User->>Client: 완료 → 일괄 생성 요청 준비
    Client->>BulkAPI: POST /problems/bulk (ProblemCreateRequest[])
    BulkAPI-->>Client: { problems[] }
    Client->>User: 등록 완료 / redirect
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • kimkimjunjun

Poem

여러 파일이 모여 한 행렬을 이루네 ✨
업로드 한 번에 꿈틀대던 스캔들,
그룹으로 모여 각자 빛을 다듬고,
편집 끝엔 일괄 생성의 박수 소리—
작지만 견고한 흐름, 새로 선다 🎉

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 제목 '오답 다중 등록으로 수정'은 PR의 핵심 변경사항(단건 방식에서 그룹 기반 다중 등록으로의 전환)을 명확하게 요약합니다.
Linked Issues check ✅ Passed Issue #168의 오답 다중 등록 구현 요구사항(그룹 기반 API, 다중 등록 흐름, UI 개선)이 코드에서 모두 충족됩니다.
Out of Scope Changes check ✅ Passed 모든 변경사항이 다중 등록 흐름 구현과 직접 관련된 범위 내에 있으며, 불필요한 수정은 없습니다.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ 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 feat/#168/wrong-create

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 17

Caution

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

⚠️ Outside diff range comments (1)
apps/web/src/shared/components/app-bar/utils/app-bar-routing.ts (1)

39-46: ⚠️ Potential issue | 🟡 Minor

/wrong/scans가 상세 라우트로 오인식될 수 있습니다.

Line 39의 분기 때문에 /wrong/scans 경로가 { type: "detail", id: "scans" }로 매칭됩니다. scans가 예약 세그먼트가 된 만큼 예외 가드를 추가해 app-bar 상태 오인을 막는 게 안전합니다.

개선 예시
   // /wrong/[id]
   if (segments.length === 2) {
+    if (segments[1] === "scans") return { type: "none" as const };
     return { type: "detail" as const, id: segments[1]! };
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/shared/components/app-bar/utils/app-bar-routing.ts` around lines
39 - 46, The route parser currently treats any two-segment path as a generic
detail (if segments.length === 2) which causes "/wrong/scans" to be
misclassified as { type: "detail", id: "scans" }; update the guard in that
branch to exclude reserved segment names (at least "scans") before returning a
detail type. Specifically, in the branch checking segments.length === 2 (and the
same code path that constructs { type: "detail" as const, id: segments[1]! }),
add a conditional that skips returning a detail when segments[1] === "scans" (or
other reserved tokens), falling through so the subsequent scans-specific branch
(segments.length === 3 && segments[1] === "scans") can match correctly. Ensure
tests or callers using the function that depends on segments and the returned
type still behave as expected.
🧹 Nitpick comments (8)
apps/web/src/shared/apis/problem-create/problem-create-types.ts (1)

20-22: Bulk 응답 컬렉션을 불변 타입으로 고정해 주세요.

현재 problems가 가변 배열이라 후속 코드에서 실수로 mutate될 수 있습니다. 응답 타입을 readonly로 잠가두면 캐시 안정성이 좋아집니다.

🔧 제안 변경
 export type ProblemBulkCreateResponse = {
-  problems: ProblemCreateResponse[];
+  readonly problems: ReadonlyArray<ProblemCreateResponse>;
 };

As per coding guidelines, API 리뷰 시 "타입 안정성 확보(응답 제네릭, 불변 데이터)"를 확인해야 합니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/shared/apis/problem-create/problem-create-types.ts` around lines
20 - 22, Change the mutable `problems: ProblemCreateResponse[]` in the
`ProblemBulkCreateResponse` type to an immutable collection so callers can't
accidentally mutate cached responses; update the field to use a readonly type
such as `readonly ProblemCreateResponse[]` (or
`ReadonlyArray<ProblemCreateResponse>`) in `ProblemBulkCreateResponse` and
adjust any call sites that rely on mutation to avoid in-place changes.
apps/web/src/shared/components/chip/chip.tsx (1)

23-41: as="span" 모드에서 버튼 전용 props를 타입으로 차단해 주세요.

지금은 as="span"이어도 onClick, disabled, type 전달이 가능하지만 렌더 시 무시됩니다. 호출부에서 인터랙션 가능 컴포넌트로 오해하기 쉬워서, ChipProps를 판별 유니온으로 분리하는 편이 안전합니다.

As per coding guidelines, **/src/shared/components/**에서는 "TypeScript 타입 정의의 정확성"과 "일관된 API 디자인과 명명 규칙"을 우선해야 합니다.

Also applies to: 65-82

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/shared/components/chip/chip.tsx` around lines 23 - 41, Split the
existing ChipProps into a discriminated union based on the as prop so that
interactive button props are only allowed when as is a button: replace the
current single ChipProps (which mixes Pick<ButtonHTMLAttributes, "disabled" |
"onClick" | "type">) with two variants (e.g., ButtonChipProps having as?:
"button" | undefined plus onClick/disabled/type, and SpanChipProps having as:
"span" and no onClick/disabled/type), keep shared fields (label, icon, tone,
etc.) in a common base, and update usages of ChipProps/Chip component to use the
new union; ensure the discriminant is the as property so TypeScript will block
passing onClick/disabled/type when as="span".
apps/web/src/shared/components/app-bar/app-bar-gate.tsx (1)

9-15: shared 계층에 feature 의존성이 직접 들어와 결합도가 높아졌습니다.

Line 9처럼 @/app/wrong/create/...를 shared 컴포넌트에서 직접 참조하면, app-bar의 재사용성과 계층 경계가 약해집니다. getWrongCreateGroupContext 주입은 상위 레이어(페이지/레이아웃)에서 연결하고 shared 쪽은 인터페이스만 받도록 분리하는 편이 유지보수에 유리합니다.

As per coding guidelines "도메인, 페이지, 특정 피처 의존성 최소화(결합도 낮게 유지)".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/shared/components/app-bar/app-bar-gate.tsx` around lines 9 - 15,
AppBarGate currently imports the feature-specific readWrongCreateGroupContext
(from "@/app/wrong/create/...") which increases coupling; instead modify
AppBarGate to accept the dependency from its parent: remove the direct import
and have AppBarGate accept a prop (e.g., getWrongCreateGroupContext) and pass
that into useAppBar; update callers (pages/layouts) to import
readWrongCreateGroupContext and inject it when rendering AppBarGate so the
shared component only depends on the interface used by useAppBar and not the
concrete feature implementation.
apps/web/src/app/wrong/scans/[id]/components/scan-detail-hero.tsx (1)

35-42: 접근성: 편집 버튼에 aria-label 추가 권장

버튼 내부에 아이콘과 텍스트가 있지만, 스크린 리더 사용자를 위해 명시적인 aria-label을 추가하면 더 좋습니다.

♻️ 제안
             <button
               type="button"
               className={s.editButton}
               onClick={onEditClick}
+              aria-label="단원 수정하기"
             >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/app/wrong/scans/`[id]/components/scan-detail-hero.tsx around
lines 35 - 42, The edit button rendered in the ScanDetailHero component lacks an
explicit aria-label; update the <button> that uses className s.editButton,
onClick onEditClick and contains <Icon name="edit-scan"> to include a
descriptive aria-label (e.g., aria-label="단원 수정하기" or a localized equivalent) so
screen readers announce the control clearly while keeping visible text
unchanged.
apps/web/src/app/wrong/create/page.tsx (1)

30-39: 사용하지 않는 pathname 제거 가능

useWrongCreateRoute에서 pathname을 반환하지만 이 페이지에서는 사용하지 않습니다. 불필요한 destructuring을 제거하면 코드가 더 깔끔해집니다.

참고: Context snippet에 pathname이 반환값에 포함되어 있으나 실제 훅 코드(Line 46-54)에서는 반환하고 있습니다. 페이지에서 사용하지 않으므로 destructure에서 제외 가능합니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/app/wrong/create/page.tsx` around lines 30 - 39, The component
WrongCreatePage unnecessarily destructures pathname from useWrongCreateRoute;
remove pathname from the destructuring so the line becomes const { router,
params, scanIds, groupId, goStep } = useWrongCreateRoute(); leaving the rest
(copy = WRONG_CREATE_STEP_COPY[1] and the call to useStep1SummaryTransition)
unchanged to keep behavior identical while cleaning up the unused variable.
apps/web/src/shared/components/app-bar/app-bar.tsx (1)

207-233: 드롭다운 메뉴 접근성 개선 필요

현재 드롭다운에 키보드 탐색(Escape로 닫기, 화살표 키 이동)과 외부 클릭 시 닫기 기능이 없습니다. 접근성 가이드라인 준수를 위해 개선이 필요합니다.

♻️ 개선 방향
  1. Escape 키로 닫기: onKeyDown 핸들러 추가
  2. 외부 클릭 시 닫기: useClickOutside 훅 또는 overlay 활용
  3. 포커스 관리: 열릴 때 첫 아이템으로 포커스 이동
// 예시: Escape 키 처리
useEffect(() => {
  if (!props.titleMenu.isOpen) return;
  const handleEscape = (e: KeyboardEvent) => {
    if (e.key === "Escape") props.titleMenu.onToggle();
  };
  document.addEventListener("keydown", handleEscape);
  return () => document.removeEventListener("keydown", handleEscape);
}, [props.titleMenu.isOpen, props.titleMenu.onToggle]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/shared/components/app-bar/app-bar.tsx` around lines 207 - 233,
The dropdown rendered when props.titleMenu.isOpen lacks keyboard and
outside-click handling; add an effect and handlers to close on Escape and close
on outside clicks, plus move focus to the first item when opened. Specifically,
in the component that renders the div with className s.titleDropdown and buttons
with s.titleDropdownItem, add a useEffect that listens for "keydown" (handle
Escape to call props.titleMenu.onToggle) and for clicks outside the
s.titleDropdown element (use a useClickOutside hook or a document click listener
that calls props.titleMenu.onToggle), and when props.titleMenu.isOpen becomes
true programmatically set focus to the first item (e.g., the first button from
props.titleMenu.items) so arrow/keyboard navigation starts there; ensure you
clean up listeners on unmount and when isOpen changes and preserve existing
onClick handlers on each item.
apps/web/src/app/wrong/create/hooks/use-wrong-create-route.ts (1)

22-44: goStep 함수 useCallback 래핑 고려

goStep이 매 렌더마다 새로 생성되어, 이를 의존성으로 사용하는 하위 컴포넌트/훅에서 불필요한 재실행이 발생할 수 있습니다.

♻️ 제안
+import { useCallback, useMemo } from "react";
...
-  const goStep = (nextStep: number, extra?: Record<string, string | null>) => {
+  const goStep = useCallback((nextStep: number, extra?: Record<string, string | null>) => {
     const safe = clamp(nextStep, 1, Number.MAX_SAFE_INTEGER);
     ...
     router.replace(`${pathname}?${nextQuery}`, { scroll: false });
-  };
+  }, [spString, pathname, router]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/app/wrong/create/hooks/use-wrong-create-route.ts` around lines
22 - 44, The goStep function is recreated on every render causing unnecessary
downstream updates; wrap goStep with React's useCallback to memoize it and list
its external dependencies (spString, pathname, router, and clamp if it's not a
stable import) in the dependency array so it only changes when those values
change; ensure useCallback is imported and keep the same signature (nextStep,
extra?) and behavior inside the memoized function.
apps/web/src/app/wrong/create/components/steps/step-1.tsx (1)

35-41: 파일 검증 실패 시 사용자 피드백 개선 고려

현재 첫 번째 오류 파일에서 window.alert로 중단됩니다. 여러 파일 중 어떤 파일이 문제인지 명확히 안내하면 UX가 향상됩니다.

💡 제안
       for (const file of files) {
         const error = validateImageFile(file);
         if (error) {
-          window.alert(error);
+          window.alert(`${file.name}: ${error}`);
           return;
         }
       }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/app/wrong/create/components/steps/step-1.tsx` around lines 35 -
41, The current loop over files calls validateImageFile(file) and immediately
calls window.alert(error) and returns on the first error, which hides which
file(s) failed; instead, collect validation results for all files (use the
existing files variable and validateImageFile) and build a user-friendly
aggregated message that includes each failing file name and its specific error,
then present that consolidated feedback (e.g., via component state and a visible
error list or a single alert with all entries) rather than returning on the
first error; update the handler that currently uses window.alert to display the
aggregated message or render the error list in the component UI.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/web/src/app/wrong/create/hooks/step1/use-step1-summary-transition.ts`:
- Around line 175-187: Handle group-level failure from
problemScanApi.getGroupSummary before inspecting individual summaries: after
fetching groupSummary (from problemScanApi.getGroupSummary) check if
groupSummary.status === "FAILED" and if so call toastError with an appropriate
message and call resetFlow (similar to per-summary failure handling), then
return; keep existing flowKeyRef.current and isMountedRef.current guards intact
and only proceed to check summaries/isFailedSummary when group-level status is
not FAILED.
- Around line 112-163: createGroupContext currently builds items only from
payloads, which drops summaries whose buildPayloadFromSummary returned null and
prevents those scans from advancing; change createGroupContext (used with
createWrongCreateGroupId, saveWrongCreateGroupContext, router.push to
ROUTES.WRONG.CREATE_SCANS) to iterate over summaries instead, lookup payloads by
scanId, and always include an item per summary: if payload is present use its
fields, otherwise set finalUnitId/finalTypeIds/answer* to null and derive
subjectName/unitName/imageUrl from the summary so the scan proceeds to
/wrong/create/scans for manual classification; ensure typeNames handles null
finalTypeIds and needsReview is set from summary.classification.needsReview.

In `@apps/web/src/app/wrong/create/scans/page.tsx`:
- Around line 28-31: The effect exits too early when groupId is missing so users
get a permanent blank (page returns null at the bottom); update the useEffect in
page.tsx to treat missing group or missing groupId as a redirect condition
(i.e., when !groupId || !group call router.replace(ROUTES.WRONG.ROOT)
immediately) and remove the early-return that leaves the component rendering
null; also ensure the render path shows a loading/fallback while routing (or
immediately navigates) instead of returning null so users can recover.
- Around line 61-65: handleNext currently always uses items[0] and enables the
CTA even when the current tab’s filtered list is empty; change it to use the
filtered list (visibleItems) instead: compute const firstVisible =
visibleItems[0], return early if !firstVisible, and call
router.push(buildProblemHref(firstVisible)); also update the equivalent handler
at the other location (the one referenced around lines 149-156) to use
visibleItems and to only enable the button when visibleItems.length > 0 so the
CTA matches the tab’s visible items.

In `@apps/web/src/app/wrong/create/utils/group-context.ts`:
- Around line 50-60: The function readWrongCreateGroupContext currently casts
JSON.parse(raw) directly to WrongCreateGroupContext which can crash later;
update readWrongCreateGroupContext to validate the parsed object shape before
returning: after parsing, check required top-level fields (e.g. id is a string,
createdAt is a valid date/string, and items is an array) and ensure items
elements have the minimal shape your UI expects (e.g. item.id and any fields
used by items.filter/render). If any check fails, return null. Use
buildStorageKey and the existing try/catch but replace the unchecked cast with
this defensive validation so downstream calls like items.filter are safe.

In `@apps/web/src/app/wrong/create/utils/search-params.ts`:
- Around line 8-13: The parsing currently allows non-integer positive numbers
and zeros/negatives for legacy scan IDs; update the logic in the scanIds parsing
helper (the function that maps raw -> scanIds in search-params.ts) to only
accept positive integers: trim and split as before, parse each value using
Number or parseInt then reject any value that is not an integer (e.g.,
Number.isInteger), and also reject values <= 0 so decimals and zero/negative IDs
are filtered out; apply the same stricter integer > 0 rule to the legacy scanId
parsing branch mentioned (lines ~20-23) so both code paths use identical
positive-integer validation.

In `@apps/web/src/app/wrong/scans/`[id]/components/scan-bottom-nav.tsx:
- Around line 19-37: Wrap the bottom navigation div (s.bottomNav) in a semantic
nav landmark with an appropriate aria-label (e.g., "Scan navigation") instead of
just a div, and update the component that renders the Icon inside the buttons
(Icon usage on lines with "chevron") to include aria-hidden="true" so screen
readers ignore decoration; ensure the interactive buttons (using prevItem,
nextItem and onMove) retain clear accessible labels (or add explicit
aria-labels) if their visible text isn't sufficient for assistive tech.

In `@apps/web/src/app/wrong/scans/`[id]/components/scan-edit-modal.tsx:
- Around line 138-177: The custom type list only supports drag-and-drop, which
blocks keyboard users; update the custom type item rendering (the mapped block
using customSelectedTypes and element class s.customTypeItem) to add
keyboard-accessible reordering: add visible or visually-hidden "Move up" and
"Move down" buttons (or arrow key handlers) that call onCustomTypeMove with the
current type.id and the target sibling id, ensure the remove button
(onCustomTypeRemove) remains a focusable control, add appropriate aria-labels
and role="listitem" on each item and aria-live or aria-relevant notifications
for order changes, and add onKeyDown handlers on the item to support Enter/Space
to open move controls and ArrowUp/ArrowDown to trigger the same onCustomTypeMove
behavior; keep draggedTypeIdRef logic for mouse drag but ensure keyboard actions
invoke the same onCustomTypeMove function so both input methods update order
consistently.
- Around line 64-69: The modal container currently rendered as <div
className={s.sheet}> lacks ARIA dialog attributes; update the element in
scan-edit-modal.tsx (the element with className s.sheet and the header with
className s.sheetHeader) to include role="dialog" and aria-modal="true", and
wire aria-labelledby to the header by giving the header a stable id (or use
aria-label on the sheet) so screen readers recognize it as a dialog; ensure the
overlay onClick/onClose logic remains unchanged.

In `@apps/web/src/app/wrong/scans/`[id]/page.css.ts:
- Around line 259-269: 현재 sheet 스타일에서 height가 고정된 "54rem"으로 설정되어 있어 작은 뷰포트에서 상단이
잘릴 수 있습니다; 파일의 sheet 스타일 선언을 수정하여 고정 높이를 제거하거나 유지하되 뷰포트 기반 제한을 추가하세요 (예: set
height to min(54rem, 100dvh) or keep height: "54rem" and add maxHeight:
"100dvh") so the bottom sheet never exceeds the viewport; update the export
const sheet style block accordingly to include a maxHeight/height expression
using 100dvh to ensure responsive behavior.

In `@apps/web/src/app/wrong/scans/`[id]/page.tsx:
- Around line 363-370: handleCustomTypeRemove currently both removes the type
from the local selection and calls setProblemTypeActiveMutation to set
active:false, which globally disables the problem type; update
handleCustomTypeRemove to only call setSelectedTypes(prev => prev.filter(name =>
name !== type.name)) and remove the mutateAsync call, and instead move the
global deactivation logic into a separate confirmed flow (e.g.,
handleGlobalDisableProblemType or a "delete/disable" button) that invokes
setProblemTypeActiveMutation.mutateAsync({ typeId: type.id, body: { active:
false }}) after an explicit user confirmation; ensure any optimistic UI updates
are handled in the separate flow rather than in handleCustomTypeRemove.
- Around line 130-134: The initial answerMode is hardcoded to "objective", so
saved subjective items reopen as objective and a submit can overwrite the stored
answer; change the initialization of answerMode to derive from
groupItem?.answerType (map saved types like "TEXT"/"NUMBER"/"EXPRESSION" to the
subjective AnswerMode and "CHOICE"/"CHOICE_MULTIPLE" to objective), e.g.
setAnswerMode(groupItem ? mapAnswerTypeToMode(groupItem.answerType) :
"objective"), and update the submit logic around the payload (the code that
builds the CHOICE payload in the handler used at lines ~195-207) to respect the
current answerMode and existing groupItem values (use groupItem?.answerChoiceNo
as fallback, and only emit CHOICE with a non-null answerChoiceNo; for subjective
modes emit the correct answerType and answerValue) so saved answers are
preserved and not overwritten with nulls.

In `@apps/web/src/shared/apis/problem-create/problem-create-api.ts`:
- Around line 68-79: The normalizeBulkCreateResponse function must not swallow
unexpected server shapes by returning { problems: [] }; instead throw a
descriptive error so callers surface failure and trigger error UI/retry. Replace
the fallback return in normalizeBulkCreateResponse with throwing an Error
(including context like "[problem-create] normalizeBulkCreateResponse:
unexpected response" and the raw data or serialized shape) so the caller's error
handling can run; keep the thrown error type consistent with existing API error
handling path and update any upstream call sites if they assume a successful
empty result.

In
`@apps/web/src/shared/apis/problem-scan/hooks/use-create-problem-scan-group-mutation.ts`:
- Around line 10-18: Remove the redundant per-scan invalidation in the onSuccess
handler: the call to qc.invalidateQueries({ queryKey: problemScanQueryKeys.all
}) already invalidates child keys, so delete the data.scanIds.forEach(...) loop
and keep only the single qc.invalidateQueries({ queryKey:
problemScanQueryKeys.all }) call inside the onSuccess of the
use-create-problem-scan-group-mutation hook; ensure references are to
problemScanQueryKeys and qc.invalidateQueries and that onSuccess still accepts
the data parameter.

In `@apps/web/src/shared/components/app-bar/app-bar.css.ts`:
- Around line 221-234: titleDropdown 드롭다운에 높이 제한과 세로 스크롤이 없어 큰 그룹에서 항목이 잘리는 문제
해결: titleDropdown 스타일 객체에 maxHeight(예: maxHeight: "min(60vh, 20rem)" 또는 프로젝트의
반응형 변수 사용)와 overflowY: "auto"를 추가하고 필요하면 WebKit 터치 스크롤 지원을 위한
"-webkit-overflow-scrolling": "touch" 및 overscrollBehavior: "contain"을 추가해 뷰포트
내에서 스크롤 가능한 메뉴가 되도록 수정하세요.

In `@apps/web/src/shared/components/app-bar/app-bar.tsx`:
- Around line 237-246: The API ambiguity arises because props.rightLabel and
props.onRightClick are both optional so a disabled button can be rendered;
update the prop types for the AppBar component (where props.rightLabel and
props.onRightClick are declared) to enforce a clear contract: either make
onRightClick required whenever rightLabel is present (e.g., use a
discriminated/conditional type or overload so rightLabel implies onRightClick)
or collapse them into a single object prop (e.g., rightAction: { label: string;
onClick: () => void }) and update the rendering logic around
props.rightLabel/props.onRightClick (the JSX that renders the button with class
s.rightLabelButton) to use the new type shape.

In `@apps/web/src/shared/components/app-bar/hooks/use-app-bar.ts`:
- Around line 280-283: The onBack handler currently always pushes to
`${ROUTES.WRONG.CREATE_SCANS}?group=${encodeURIComponent(groupId ?? "")}`, which
yields an invalid URL when groupId is empty; update the onBack logic in
useAppBar (the onBack handler that calls router.push) to check groupId first
and, if missing, fall back to a safe route or behavior (e.g., call router.back()
or navigate to ROUTES.CREATE) instead of building a URL with an empty group
query param so the flow can recover without group context.

---

Outside diff comments:
In `@apps/web/src/shared/components/app-bar/utils/app-bar-routing.ts`:
- Around line 39-46: The route parser currently treats any two-segment path as a
generic detail (if segments.length === 2) which causes "/wrong/scans" to be
misclassified as { type: "detail", id: "scans" }; update the guard in that
branch to exclude reserved segment names (at least "scans") before returning a
detail type. Specifically, in the branch checking segments.length === 2 (and the
same code path that constructs { type: "detail" as const, id: segments[1]! }),
add a conditional that skips returning a detail when segments[1] === "scans" (or
other reserved tokens), falling through so the subsequent scans-specific branch
(segments.length === 3 && segments[1] === "scans") can match correctly. Ensure
tests or callers using the function that depends on segments and the returned
type still behave as expected.

---

Nitpick comments:
In `@apps/web/src/app/wrong/create/components/steps/step-1.tsx`:
- Around line 35-41: The current loop over files calls validateImageFile(file)
and immediately calls window.alert(error) and returns on the first error, which
hides which file(s) failed; instead, collect validation results for all files
(use the existing files variable and validateImageFile) and build a
user-friendly aggregated message that includes each failing file name and its
specific error, then present that consolidated feedback (e.g., via component
state and a visible error list or a single alert with all entries) rather than
returning on the first error; update the handler that currently uses
window.alert to display the aggregated message or render the error list in the
component UI.

In `@apps/web/src/app/wrong/create/hooks/use-wrong-create-route.ts`:
- Around line 22-44: The goStep function is recreated on every render causing
unnecessary downstream updates; wrap goStep with React's useCallback to memoize
it and list its external dependencies (spString, pathname, router, and clamp if
it's not a stable import) in the dependency array so it only changes when those
values change; ensure useCallback is imported and keep the same signature
(nextStep, extra?) and behavior inside the memoized function.

In `@apps/web/src/app/wrong/create/page.tsx`:
- Around line 30-39: The component WrongCreatePage unnecessarily destructures
pathname from useWrongCreateRoute; remove pathname from the destructuring so the
line becomes const { router, params, scanIds, groupId, goStep } =
useWrongCreateRoute(); leaving the rest (copy = WRONG_CREATE_STEP_COPY[1] and
the call to useStep1SummaryTransition) unchanged to keep behavior identical
while cleaning up the unused variable.

In `@apps/web/src/app/wrong/scans/`[id]/components/scan-detail-hero.tsx:
- Around line 35-42: The edit button rendered in the ScanDetailHero component
lacks an explicit aria-label; update the <button> that uses className
s.editButton, onClick onEditClick and contains <Icon name="edit-scan"> to
include a descriptive aria-label (e.g., aria-label="단원 수정하기" or a localized
equivalent) so screen readers announce the control clearly while keeping visible
text unchanged.

In `@apps/web/src/shared/apis/problem-create/problem-create-types.ts`:
- Around line 20-22: Change the mutable `problems: ProblemCreateResponse[]` in
the `ProblemBulkCreateResponse` type to an immutable collection so callers can't
accidentally mutate cached responses; update the field to use a readonly type
such as `readonly ProblemCreateResponse[]` (or
`ReadonlyArray<ProblemCreateResponse>`) in `ProblemBulkCreateResponse` and
adjust any call sites that rely on mutation to avoid in-place changes.

In `@apps/web/src/shared/components/app-bar/app-bar-gate.tsx`:
- Around line 9-15: AppBarGate currently imports the feature-specific
readWrongCreateGroupContext (from "@/app/wrong/create/...") which increases
coupling; instead modify AppBarGate to accept the dependency from its parent:
remove the direct import and have AppBarGate accept a prop (e.g.,
getWrongCreateGroupContext) and pass that into useAppBar; update callers
(pages/layouts) to import readWrongCreateGroupContext and inject it when
rendering AppBarGate so the shared component only depends on the interface used
by useAppBar and not the concrete feature implementation.

In `@apps/web/src/shared/components/app-bar/app-bar.tsx`:
- Around line 207-233: The dropdown rendered when props.titleMenu.isOpen lacks
keyboard and outside-click handling; add an effect and handlers to close on
Escape and close on outside clicks, plus move focus to the first item when
opened. Specifically, in the component that renders the div with className
s.titleDropdown and buttons with s.titleDropdownItem, add a useEffect that
listens for "keydown" (handle Escape to call props.titleMenu.onToggle) and for
clicks outside the s.titleDropdown element (use a useClickOutside hook or a
document click listener that calls props.titleMenu.onToggle), and when
props.titleMenu.isOpen becomes true programmatically set focus to the first item
(e.g., the first button from props.titleMenu.items) so arrow/keyboard navigation
starts there; ensure you clean up listeners on unmount and when isOpen changes
and preserve existing onClick handlers on each item.

In `@apps/web/src/shared/components/chip/chip.tsx`:
- Around line 23-41: Split the existing ChipProps into a discriminated union
based on the as prop so that interactive button props are only allowed when as
is a button: replace the current single ChipProps (which mixes
Pick<ButtonHTMLAttributes, "disabled" | "onClick" | "type">) with two variants
(e.g., ButtonChipProps having as?: "button" | undefined plus
onClick/disabled/type, and SpanChipProps having as: "span" and no
onClick/disabled/type), keep shared fields (label, icon, tone, etc.) in a common
base, and update usages of ChipProps/Chip component to use the new union; ensure
the discriminant is the as property so TypeScript will block passing
onClick/disabled/type when as="span".
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: e305acca-0ae8-45ee-bdac-700c53fd00f0

📥 Commits

Reviewing files that changed from the base of the PR and between 00c37fa and e4382a8.

⛔ Files ignored due to path filters (2)
  • apps/web/src/shared/assets/icons/edit-scan.svg is excluded by !**/*.svg, !**/src/shared/assets/**
  • apps/web/src/shared/assets/icons/triangle-scan.svg is excluded by !**/*.svg, !**/src/shared/assets/**
📒 Files selected for processing (58)
  • apps/web/src/app/login/info/components/sections/profile-section/profile-section.tsx
  • apps/web/src/app/my/edit/components/my-profile-section.tsx
  • apps/web/src/app/my/page.tsx
  • apps/web/src/app/wrong/create/components/direct-add-button/direct-add-button.css.ts
  • apps/web/src/app/wrong/create/components/direct-add-button/direct-add-button.tsx
  • apps/web/src/app/wrong/create/components/steps/step-1.tsx
  • apps/web/src/app/wrong/create/components/steps/step-2.tsx
  • apps/web/src/app/wrong/create/components/steps/step-3.tsx
  • apps/web/src/app/wrong/create/components/steps/step-4.tsx
  • apps/web/src/app/wrong/create/components/steps/step.css.ts
  • apps/web/src/app/wrong/create/components/wrong-create-steps/wrong-create-steps.tsx
  • apps/web/src/app/wrong/create/done/page.tsx
  • apps/web/src/app/wrong/create/hooks/step1/use-image-source-picker.ts
  • apps/web/src/app/wrong/create/hooks/step1/use-step1-summary-transition.ts
  • apps/web/src/app/wrong/create/hooks/step2/use-step-2-selection.ts
  • apps/web/src/app/wrong/create/hooks/step3/use-active-type-view.ts
  • apps/web/src/app/wrong/create/hooks/step3/use-async-id-lock.ts
  • apps/web/src/app/wrong/create/hooks/step3/use-custom-type-order.ts
  • apps/web/src/app/wrong/create/hooks/step3/use-optimistic-problem-types.ts
  • apps/web/src/app/wrong/create/hooks/step3/use-recommended-types.ts
  • apps/web/src/app/wrong/create/hooks/step3/use-sanitized-url-type-ids.ts
  • apps/web/src/app/wrong/create/hooks/step3/use-selected-type-ids.ts
  • apps/web/src/app/wrong/create/hooks/step3/use-step-3-selection.ts
  • apps/web/src/app/wrong/create/hooks/step3/use-type-add-flow.ts
  • apps/web/src/app/wrong/create/hooks/step3/use-type-ids-param.ts
  • apps/web/src/app/wrong/create/hooks/step3/use-type-maintenance.ts
  • apps/web/src/app/wrong/create/hooks/step4/use-step4-form.ts
  • apps/web/src/app/wrong/create/hooks/use-pointer-sort-ids.ts
  • apps/web/src/app/wrong/create/hooks/use-wrong-create-route.ts
  • apps/web/src/app/wrong/create/hooks/use-wrong-create-submit.ts
  • apps/web/src/app/wrong/create/page.tsx
  • apps/web/src/app/wrong/create/scans/page.tsx
  • apps/web/src/app/wrong/create/scans/scans.css.ts
  • apps/web/src/app/wrong/create/utils/group-context.ts
  • apps/web/src/app/wrong/create/utils/search-params.ts
  • apps/web/src/app/wrong/scans/[id]/components/scan-answer-section.tsx
  • apps/web/src/app/wrong/scans/[id]/components/scan-bottom-nav.tsx
  • apps/web/src/app/wrong/scans/[id]/components/scan-detail-hero.tsx
  • apps/web/src/app/wrong/scans/[id]/components/scan-edit-modal.tsx
  • apps/web/src/app/wrong/scans/[id]/page.css.ts
  • apps/web/src/app/wrong/scans/[id]/page.tsx
  • apps/web/src/shared/apis/constants/api-paths.ts
  • apps/web/src/shared/apis/problem-create/hooks/use-create-bulk-wrong-answer-cards-mutation.ts
  • apps/web/src/shared/apis/problem-create/problem-create-api.ts
  • apps/web/src/shared/apis/problem-create/problem-create-types.ts
  • apps/web/src/shared/apis/problem-scan/hooks/use-create-problem-scan-group-mutation.ts
  • apps/web/src/shared/apis/problem-scan/problem-scan-api.ts
  • apps/web/src/shared/apis/problem-scan/problem-scan-types.ts
  • apps/web/src/shared/components/app-bar/app-bar-gate.tsx
  • apps/web/src/shared/components/app-bar/app-bar.css.ts
  • apps/web/src/shared/components/app-bar/app-bar.tsx
  • apps/web/src/shared/components/app-bar/hooks/use-app-bar.ts
  • apps/web/src/shared/components/app-bar/types/app-bar.ts
  • apps/web/src/shared/components/app-bar/utils/app-bar-routing.ts
  • apps/web/src/shared/components/chip/chip.tsx
  • apps/web/src/shared/constants/icons.ts
  • apps/web/src/shared/constants/routes.ts
  • apps/web/src/shared/constants/sprite.ts
💤 Files with no reviewable changes (20)
  • apps/web/src/app/wrong/create/components/steps/step-3.tsx
  • apps/web/src/app/wrong/create/hooks/step3/use-type-add-flow.ts
  • apps/web/src/app/wrong/create/hooks/step3/use-recommended-types.ts
  • apps/web/src/app/wrong/create/hooks/step3/use-optimistic-problem-types.ts
  • apps/web/src/app/wrong/create/hooks/step3/use-async-id-lock.ts
  • apps/web/src/app/wrong/create/components/steps/step-2.tsx
  • apps/web/src/app/wrong/create/hooks/step4/use-step4-form.ts
  • apps/web/src/app/wrong/create/hooks/step3/use-sanitized-url-type-ids.ts
  • apps/web/src/app/wrong/create/hooks/use-pointer-sort-ids.ts
  • apps/web/src/app/wrong/create/hooks/step3/use-active-type-view.ts
  • apps/web/src/app/wrong/create/components/wrong-create-steps/wrong-create-steps.tsx
  • apps/web/src/app/wrong/create/hooks/step3/use-type-maintenance.ts
  • apps/web/src/app/wrong/create/hooks/use-wrong-create-submit.ts
  • apps/web/src/app/wrong/create/components/steps/step-4.tsx
  • apps/web/src/app/wrong/create/hooks/step3/use-type-ids-param.ts
  • apps/web/src/app/wrong/create/hooks/step3/use-custom-type-order.ts
  • apps/web/src/app/wrong/create/hooks/step3/use-selected-type-ids.ts
  • apps/web/src/app/wrong/create/hooks/step3/use-step-3-selection.ts
  • apps/web/src/app/wrong/create/hooks/step2/use-step-2-selection.ts
  • apps/web/src/app/wrong/create/components/steps/step.css.ts

Comment thread apps/web/src/app/wrong/create/utils/group-context.ts
Comment thread apps/web/src/app/wrong/create/utils/search-params.ts Outdated
Comment thread apps/web/src/app/wrong/scans/[id]/components/scan-edit-modal.tsx Outdated
Comment thread apps/web/src/app/wrong/scans/[id]/components/scan-edit-modal.tsx Outdated
Comment thread apps/web/src/app/wrong/scans/[id]/page.tsx Outdated
Comment thread apps/web/src/shared/apis/problem-create/problem-create-api.ts Outdated
Comment on lines +221 to +234
export const titleDropdown = style([
bgColor["grayscale-0"],
{
position: "absolute",
top: "calc(100% + 1.2rem)",
left: "50%",
transform: "translateX(-50%)",
width: "14rem",
borderRadius: vars.radius.r16,
boxShadow: vars.shadow.e400,
overflow: "hidden",
zIndex: vars.zIndex.contentOverlayHigh,
},
]);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

드롭다운에 높이 제한이 없어 큰 그룹에서 항목이 잘립니다.

다중 등록에서는 문제 수가 많아질 수 있는데, 현재 드롭다운은 maxHeight/overflowY가 없어서 하단 항목이 뷰포트 밖으로 밀리면 선택할 수 없습니다. 그룹 내 빠른 이동이 핵심 UX라서 여기서는 스크롤 가능한 메뉴가 필요합니다.

💡 제안 수정안
 export const titleDropdown = style([
   bgColor["grayscale-0"],
   {
     position: "absolute",
     top: "calc(100% + 1.2rem)",
     left: "50%",
     transform: "translateX(-50%)",
     width: "14rem",
+    maxHeight: "min(32rem, calc(100vh - 8rem))",
     borderRadius: vars.radius.r16,
     boxShadow: vars.shadow.e400,
     overflow: "hidden",
+    overflowY: "auto",
+    overscrollBehavior: "contain",
     zIndex: vars.zIndex.contentOverlayHigh,
   },
 ]);

As per coding guidelines "반응형 스타일 구현 방식".

📝 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
export const titleDropdown = style([
bgColor["grayscale-0"],
{
position: "absolute",
top: "calc(100% + 1.2rem)",
left: "50%",
transform: "translateX(-50%)",
width: "14rem",
borderRadius: vars.radius.r16,
boxShadow: vars.shadow.e400,
overflow: "hidden",
zIndex: vars.zIndex.contentOverlayHigh,
},
]);
export const titleDropdown = style([
bgColor["grayscale-0"],
{
position: "absolute",
top: "calc(100% + 1.2rem)",
left: "50%",
transform: "translateX(-50%)",
width: "14rem",
maxHeight: "min(32rem, calc(100vh - 8rem))",
borderRadius: vars.radius.r16,
boxShadow: vars.shadow.e400,
overflow: "hidden",
overflowY: "auto",
overscrollBehavior: "contain",
zIndex: vars.zIndex.contentOverlayHigh,
},
]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/shared/components/app-bar/app-bar.css.ts` around lines 221 -
234, titleDropdown 드롭다운에 높이 제한과 세로 스크롤이 없어 큰 그룹에서 항목이 잘리는 문제 해결: titleDropdown
스타일 객체에 maxHeight(예: maxHeight: "min(60vh, 20rem)" 또는 프로젝트의 반응형 변수 사용)와
overflowY: "auto"를 추가하고 필요하면 WebKit 터치 스크롤 지원을 위한 "-webkit-overflow-scrolling":
"touch" 및 overscrollBehavior: "contain"을 추가해 뷰포트 내에서 스크롤 가능한 메뉴가 되도록 수정하세요.

Comment thread apps/web/src/shared/components/app-bar/app-bar.tsx Outdated
Comment thread apps/web/src/shared/components/app-bar/hooks/use-app-bar.ts Outdated
@Central-MakeUs Central-MakeUs deleted a comment from coderabbitai bot Apr 15, 2026
@Central-MakeUs Central-MakeUs deleted a comment from coderabbitai bot Apr 15, 2026
@Central-MakeUs Central-MakeUs deleted a comment from coderabbitai bot Apr 15, 2026
@Central-MakeUs Central-MakeUs deleted a comment from coderabbitai bot Apr 15, 2026
@Central-MakeUs Central-MakeUs deleted a comment from coderabbitai bot Apr 15, 2026
@Central-MakeUs Central-MakeUs deleted a comment from coderabbitai bot Apr 15, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

Caution

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

⚠️ Outside diff range comments (1)
apps/web/src/app/wrong/create/hooks/step1/use-step1-summary-transition.ts (1)

244-255: ⚠️ Potential issue | 🟠 Major

groupId 없는 업로드 응답은 성공으로 처리하면 안 됩니다.

여기서 scanIds만 저장되면 isStep1Blockedtrue가 되는데, 실제 폴링은 !groupId에서 바로 종료됩니다. 그 결과 화면이 영구히 막힐 수 있으니 res.groupId가 없으면 즉시 실패 처리하고 복구 경로를 열어 주세요.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/app/wrong/create/hooks/step1/use-step1-summary-transition.ts`
around lines 244 - 255, The handler treats responses without res.groupId as
success which leaves scanIds saved but polling exits on !groupId and the UI gets
stuck; update the callback in use-step1-summary-transition (the arrow handler
that currently sets createdFlowRef.current and calls goStep) to explicitly treat
a missing res.groupId as a failure: when res.groupId is falsy, clear
createdFlowRef.current, invoke the existing failure/recovery flow (e.g., set the
step error state or call the step-failure handler used elsewhere), and do NOT
call goStep(1, {...}) with a null groupId; only call goStep when res.groupId is
present to guarantee polling and isStep1Blocked remain consistent.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/web/src/app/wrong/create/hooks/step1/use-step1-summary-transition.ts`:
- Around line 204-208: The catch block after pollSummariesRef.current currently
retries indefinitely by clearing and scheduling timerRef with POLL_INTERVAL_MS;
change it to track retry attempts or elapsed time (e.g., a retryCount or
startTime stored alongside timerRef/pollSummariesRef), stop retrying after a
maxRetry or timeout threshold, call resetFlow() and show a user-facing
toast/error notification when threshold is exceeded, and ensure authentication
errors (401/403) are treated as terminal by short-circuiting to resetFlow()
immediately rather than scheduling another poll; update the catch in
use-step1-summary-transition to consult these counters/response status and
perform the terminal path.

In `@apps/web/src/app/wrong/scans/`[id]/hooks/use-scan-answer-actions.ts:
- Around line 20-22: The hook keeps stale answer state because
useState(initial*) only uses the initial values once; update
use-scan-answer-actions.ts (in the hook using answerMode/answerChoice/answerText
and initialMode/initialChoice/initialText) to reset those states whenever the
initial props change by adding a useEffect that calls
setAnswerMode(initialMode), setAnswerChoice(initialChoice) and
setAnswerText(initialText) with [initialMode, initialChoice, initialText] as the
dependency array so the hook syncs when the scan changes.

In `@apps/web/src/app/wrong/scans/`[id]/hooks/use-wrong-scan-detail.ts:
- Around line 96-103: The current mapping in use-wrong-scan-detail.ts uses
resolvedTypeIds.length > 0 ? resolvedTypeIds : groupItem.finalTypeIds which
preserves previous groupItem.finalTypeIds when the user clears all types; change
it to always assign finalTypeIds: resolvedTypeIds (so an empty selection becomes
an empty array) and remove the fallback to groupItem.finalTypeIds; apply the
same change to the analogous block referenced around the other instance (lines
~172-180) so both places persist an explicit empty array when the user clears
all types.

In `@apps/web/src/app/wrong/scans/`[id]/payload.ts:
- Around line 32-58: resolveFinalTypeIds can trigger duplicate createType calls
for the same new type across a batch; fix by introducing a shared promise cache
keyed by the normalized type name so concurrent requests reuse one creation
promise/result. Update resolveFinalTypeIds (and the other similar block at
123-145) to: normalize+trim the typeName into a key, check a shared Map<string,
Promise<ProblemTypeItem> | string> (or two maps) for an existing promise/id
before calling createType, store the promise in the map immediately when
creating so subsequent callers await the same promise, and return the resolved
id; ensure you still consult existing problemTypes first and use normalized keys
consistently.
- Around line 123-149: The code only validates and resolves
finalUnitId/finalTypeIds for nextCurrentItem and serializes other items via
buildStoredProblemPayload, which can let incomplete review-needed items with
empty unit/type slip into the bulk payload; update the payload construction so
you first iterate over all items (the items array) and for each item (compare
using scanId with nextCurrentItem) compute/resolve finalUnitId via
resolveItemFinalUnitId and finalTypeIds via resolveFinalTypeIds (using the same
fallback and problemTypes/createType inputs) before serializing, use
buildProblemPayload for the current item and buildStoredProblemPayload for
others but only after resolution, and if any item fails resolution or yields
missing/invalid unit/type, prevent the bulk create (return an error or throw)
instead of proceeding.

---

Outside diff comments:
In `@apps/web/src/app/wrong/create/hooks/step1/use-step1-summary-transition.ts`:
- Around line 244-255: The handler treats responses without res.groupId as
success which leaves scanIds saved but polling exits on !groupId and the UI gets
stuck; update the callback in use-step1-summary-transition (the arrow handler
that currently sets createdFlowRef.current and calls goStep) to explicitly treat
a missing res.groupId as a failure: when res.groupId is falsy, clear
createdFlowRef.current, invoke the existing failure/recovery flow (e.g., set the
step error state or call the step-failure handler used elsewhere), and do NOT
call goStep(1, {...}) with a null groupId; only call goStep when res.groupId is
present to guarantee polling and isStep1Blocked remain consistent.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 6bf03b84-3418-4940-9b80-47ab115ad078

📥 Commits

Reviewing files that changed from the base of the PR and between e4382a8 and 62d1a8f.

📒 Files selected for processing (18)
  • apps/web/src/app/wrong/create/components/steps/step-1.tsx
  • apps/web/src/app/wrong/create/hooks/step1/use-step1-summary-transition.ts
  • apps/web/src/app/wrong/create/scans/page.tsx
  • apps/web/src/app/wrong/create/utils/group-context.ts
  • apps/web/src/app/wrong/create/utils/search-params.ts
  • apps/web/src/app/wrong/scans/[id]/components/scan-edit-modal.tsx
  • apps/web/src/app/wrong/scans/[id]/hooks/use-scan-answer-actions.ts
  • apps/web/src/app/wrong/scans/[id]/hooks/use-wrong-scan-detail.ts
  • apps/web/src/app/wrong/scans/[id]/page.css.ts
  • apps/web/src/app/wrong/scans/[id]/page.tsx
  • apps/web/src/app/wrong/scans/[id]/payload.ts
  • apps/web/src/app/wrong/scans/[id]/type-actions.ts
  • apps/web/src/app/wrong/scans/[id]/utils.ts
  • apps/web/src/shared/apis/problem-create/problem-create-api.ts
  • apps/web/src/shared/apis/problem-scan/hooks/use-create-problem-scan-group-mutation.ts
  • apps/web/src/shared/components/app-bar/app-bar.tsx
  • apps/web/src/shared/components/app-bar/hooks/use-app-bar.ts
  • apps/web/src/shared/components/app-bar/types/app-bar.ts
🚧 Files skipped from review as they are similar to previous changes (11)
  • apps/web/src/shared/apis/problem-scan/hooks/use-create-problem-scan-group-mutation.ts
  • apps/web/src/app/wrong/create/utils/search-params.ts
  • apps/web/src/shared/apis/problem-create/problem-create-api.ts
  • apps/web/src/app/wrong/scans/[id]/page.tsx
  • apps/web/src/app/wrong/create/scans/page.tsx
  • apps/web/src/shared/components/app-bar/types/app-bar.ts
  • apps/web/src/shared/components/app-bar/app-bar.tsx
  • apps/web/src/app/wrong/create/utils/group-context.ts
  • apps/web/src/app/wrong/create/components/steps/step-1.tsx
  • apps/web/src/app/wrong/scans/[id]/components/scan-edit-modal.tsx
  • apps/web/src/app/wrong/scans/[id]/page.css.ts

Comment on lines +204 to +208
} catch {
clearTimer();
timerRef.current = setTimeout(() => {
pollSummariesRef.current();
}, POLL_INTERVAL_MS);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

네트워크 오류 시 여기서 무기한 폴링에 갇힙니다.

catch는 모든 예외를 같은 방식으로 재시도만 해서, 인증 오류나 지속적인 5xx에서도 Step 1이 끝나지 않습니다. 사용자는 계속 막힌 상태만 보게 되니 최대 재시도/타임아웃 후 토스트를 띄우고 resetFlow()로 빠져나오는 경로가 필요합니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/app/wrong/create/hooks/step1/use-step1-summary-transition.ts`
around lines 204 - 208, The catch block after pollSummariesRef.current currently
retries indefinitely by clearing and scheduling timerRef with POLL_INTERVAL_MS;
change it to track retry attempts or elapsed time (e.g., a retryCount or
startTime stored alongside timerRef/pollSummariesRef), stop retrying after a
maxRetry or timeout threshold, call resetFlow() and show a user-facing
toast/error notification when threshold is exceeded, and ensure authentication
errors (401/403) are treated as terminal by short-circuiting to resetFlow()
immediately rather than scheduling another poll; update the catch in
use-step1-summary-transition to consult these counters/response status and
perform the terminal path.

Comment on lines +20 to +22
const [answerMode, setAnswerMode] = useState<AnswerMode>(initialMode);
const [answerChoice, setAnswerChoice] = useState<number | null>(initialChoice);
const [answerText, setAnswerText] = useState(initialText);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
fd 'page.tsx' apps/web/src/app/wrong/scans -x sed -n '1,220p' {}
echo '---'
rg -n -C3 'useWrongScanDetail|key=' apps/web/src/app/wrong/scans

Repository: Central-MakeUs/Delta-front

Length of output: 8947


🏁 Script executed:

cat -n apps/web/src/app/wrong/scans/[id]/hooks/use-scan-answer-actions.ts

Repository: Central-MakeUs/Delta-front

Length of output: 2435


🏁 Script executed:

cat -n apps/web/src/app/wrong/scans/[id]/hooks/use-wrong-scan-detail.ts | head -100

Repository: Central-MakeUs/Delta-front

Length of output: 5195


🏁 Script executed:

cat -n apps/web/src/app/wrong/scans/[id]/hooks/use-wrong-scan-detail.ts | sed -n '100,200p'

Repository: Central-MakeUs/Delta-front

Length of output: 3878


스캔이 바뀌어도 답안 state가 초기화되지 않습니다.

useState(initial*)는 첫 렌더에서만 적용되므로, moveTo(nextScanId)로 다른 스캔으로 이동할 때 이전 문제의 answerMode/answerChoice/answerText가 남습니다. 같은 hook 인스턴스에서 스캔만 바뀌므로 state가 동기화되지 않고, 이전 답안이 새로운 문제에 표시될 수 있습니다.

initialMode, initialChoice, initialText가 변경될 때 state를 초기화하는 useEffect를 추가해 주세요:

제안된 수정 방안
useEffect(() => {
  setAnswerMode(initialMode);
  setAnswerChoice(initialChoice);
  setAnswerText(initialText);
}, [initialMode, initialChoice, initialText]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/app/wrong/scans/`[id]/hooks/use-scan-answer-actions.ts around
lines 20 - 22, The hook keeps stale answer state because useState(initial*) only
uses the initial values once; update use-scan-answer-actions.ts (in the hook
using answerMode/answerChoice/answerText and
initialMode/initialChoice/initialText) to reset those states whenever the
initial props change by adding a useEffect that calls
setAnswerMode(initialMode), setAnswerChoice(initialChoice) and
setAnswerText(initialText) with [initialMode, initialChoice, initialText] as the
dependency array so the hook syncs when the scan changes.

Comment on lines +96 to +103
const resolvedTypeIds = resolveKnownTypeIds(appliedTypes, problemTypes);
return {
...groupItem,
finalUnitId:
resolveUnitId(appliedSubject, appliedUnit) ?? groupItem.finalUnitId,
finalTypeIds:
resolvedTypeIds.length > 0 ? resolvedTypeIds : groupItem.finalTypeIds,
subjectName: appliedSubject,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

유형을 전부 해제해도 이전 finalTypeIds가 남습니다.

지금 로직은 선택된 유형이 0개이면 []를 유지하지 않고 기존 groupItem.finalTypeIds로 되돌립니다. 그래서 사용자가 유형을 모두 지워도 실제 저장/전송 값은 예전 유형 그대로 남고, review-needed 상태로 되돌릴 수도 없습니다. 빈 선택은 그대로 빈 배열로 저장하고, 완료 단계에서 검증하는 쪽이 맞습니다.

Also applies to: 172-180

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/app/wrong/scans/`[id]/hooks/use-wrong-scan-detail.ts around
lines 96 - 103, The current mapping in use-wrong-scan-detail.ts uses
resolvedTypeIds.length > 0 ? resolvedTypeIds : groupItem.finalTypeIds which
preserves previous groupItem.finalTypeIds when the user clears all types; change
it to always assign finalTypeIds: resolvedTypeIds (so an empty selection becomes
an empty array) and remove the fallback to groupItem.finalTypeIds; apply the
same change to the analogous block referenced around the other instance (lines
~172-180) so both places persist an explicit empty array when the user clears
all types.

Comment on lines +32 to +58
export const resolveFinalTypeIds = async ({
typeNames,
fallbackTypeIds,
problemTypes,
createType,
}: {
typeNames: string[];
fallbackTypeIds: string[];
problemTypes: ProblemTypeItem[];
createType: (name: string) => Promise<ProblemTypeItem>;
}) => {
if (typeNames.length === 0) return fallbackTypeIds;

return dedupe(
await Promise.all(
typeNames.map(async (typeName) => {
const existing = problemTypes.find(
(type) => normalize(type.name) === normalize(typeName)
);

if (existing?.id) return existing.id;

const created = await createType(typeName.trim());
return created.id;
})
)
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

같은 새 커스텀 유형이 배치 안에서 중복 생성될 수 있습니다.

resolveFinalTypeIds()가 item마다 독립적으로 problemTypes만 보고 createType()를 호출해서, 여러 스캔이 같은 새 유형명을 갖고 있으면 같은 이름으로 생성 요청이 여러 번 나갈 수 있습니다. 이름 기준의 promise cache/shared map을 두고 한 번 만든 결과를 같은 배치 전체에서 재사용하는 쪽이 안전합니다.

Also applies to: 123-145

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/app/wrong/scans/`[id]/payload.ts around lines 32 - 58,
resolveFinalTypeIds can trigger duplicate createType calls for the same new type
across a batch; fix by introducing a shared promise cache keyed by the
normalized type name so concurrent requests reuse one creation promise/result.
Update resolveFinalTypeIds (and the other similar block at 123-145) to:
normalize+trim the typeName into a key, check a shared Map<string,
Promise<ProblemTypeItem> | string> (or two maps) for an existing promise/id
before calling createType, store the promise in the map immediately when
creating so subsequent callers await the same promise, and return the resolved
id; ensure you still consult existing problemTypes first and use normalized keys
consistently.

Comment on lines +123 to +149
const payloadItems = await Promise.all(
items.map(async (item) => {
const effectiveItem =
item.scanId === nextCurrentItem.scanId ? nextCurrentItem : item;

if (effectiveItem.scanId === nextCurrentItem.scanId) {
return buildProblemPayload(
nextCurrentItem,
currentFinalUnitId,
currentFinalTypeIds,
answerMode,
answerChoice,
answerText
);
}

const finalUnitId = resolveItemFinalUnitId(effectiveItem);
const finalTypeIds = await resolveFinalTypeIds({
typeNames: effectiveItem.typeNames,
fallbackTypeIds: effectiveItem.finalTypeIds,
problemTypes,
createType,
});

return buildStoredProblemPayload(effectiveItem, finalUnitId, finalTypeIds);
})
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

현재 문제만 검증하고 나머지 스캔은 그대로 전송됩니다.

여기서는 currentItemfinalUnitId/finalTypeIds를 확인하고, 나머지 항목은 buildStoredProblemPayload()로 바로 직렬화합니다. Step 1에서 review-needed 항목이 빈 단원/유형 값으로 저장될 수 있어서, 한 문제만 수정한 뒤 완료해도 invalid bulk payload가 섞여 나갈 수 있습니다. 완료 전에 items 전체를 검증하거나, 미완료 항목이 있으면 bulk create를 막아야 합니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/app/wrong/scans/`[id]/payload.ts around lines 123 - 149, The
code only validates and resolves finalUnitId/finalTypeIds for nextCurrentItem
and serializes other items via buildStoredProblemPayload, which can let
incomplete review-needed items with empty unit/type slip into the bulk payload;
update the payload construction so you first iterate over all items (the items
array) and for each item (compare using scanId with nextCurrentItem)
compute/resolve finalUnitId via resolveItemFinalUnitId and finalTypeIds via
resolveFinalTypeIds (using the same fallback and problemTypes/createType inputs)
before serializing, use buildProblemPayload for the current item and
buildStoredProblemPayload for others but only after resolution, and if any item
fails resolution or yields missing/invalid unit/type, prevent the bulk create
(return an error or throw) instead of proceeding.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

오답 다중 등록 구현

1 participant