[FEAT] 강사 수정사항 요청 페이지 퍼블리싱#30
Conversation
|
Important Review skippedAuto incremental reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
📝 Walkthrough둘러보기강사 수정 사항 요청 페이지를 API 스펙 기반으로 완전히 구현합니다. 디자인 토큰 추가, 드래그 가능한 스크롤바 인프라, 이미지 상세 모달, 카테고리 선택 및 코멘트 입력 UI, 그리고 홈 화면에서의 네비게이션 통합을 포함합니다. 변경 사항강사 수정 요청 기능 구현
연관된 PR
추천 레이블
추천 리뷰어
예상 코드 리뷰 노력🎯 3 (Moderate) | ⏱️ ~20 minutes 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (8)
src/widgets/instructor/revision/model/revision.ts (1)
14-14: ⚡ Quick winexported 상수의 네이밍 컨벤션을 프로젝트 표준에 맞춰주세요.
프로젝트 learnings에 따르면 exported constants는 SCREAMING_SNAKE_CASE를 사용해야 합니다. 같은 widgets/instructor/revision 폴더의
config/revision.ts에서REVISION_CATEGORIES,MAX_SELECTABLE_COUNT가 이미 이 컨벤션을 따르고 있으므로, 일관성을 위해 이 파일의 mock data 상수들도 동일한 네이밍을 적용하는 것이 좋습니다.♻️ 권장하는 네이밍 변경
-export const draftRevisionDetailData: DraftRevisionDetail[] = [ +export const DRAFT_REVISION_DETAIL_DATA: DraftRevisionDetail[] = [-export const draftFilesData: DraftFiles[] = [ +export const DRAFT_FILES_DATA: DraftFiles[] = [이 변경을 적용하면 상수를 import하는 모든 곳(
src/app/instructor/revision/[commissionId]/page.tsx등)에서도 동일하게 업데이트해야 합니다.Based on learnings: "In Ditda-Frontend (TypeScript/Next.js), exported constants should be named in SCREAMING_SNAKE_CASE (e.g., BASIC_INFO_FIELDS, PAGE_OPTIONS)."
Also applies to: 70-70
🤖 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/widgets/instructor/revision/model/revision.ts` at line 14, The exported mock constant draftRevisionDetailData should follow the project's SCREAMING_SNAKE_CASE convention; rename draftRevisionDetailData to DRAFT_REVISION_DETAIL_DATA (and update all imports/usages such as in src/app/instructor/revision/[commissionId]/page.tsx) and ensure the exported type DraftRevisionDetail references remain unchanged; update any tests or files importing this constant to use the new name so compilation and imports stay consistent.Source: Learnings
src/shared/lib/hooks/useDragScrollbar.ts (1)
19-25: ⚡ Quick win모달 접근성 개선을 고려해보세요.
현재 구현은 기본적인 키보드 닫기(Escape)와 백드롭 클릭을 지원하지만, 다음 접근성 패턴이 누락되어 있습니다:
- Focus management: 모달이 열릴 때 첫 번째 포커스 가능한 요소로 focus 이동, 닫힐 때 원래 요소로 복원
- Focus trap: Tab/Shift+Tab으로 모달 내부에서만 focus 순환
이러한 패턴은 키보드 사용자와 스크린 리더 사용자의 경험을 개선합니다.
react-focus-lock이나@radix-ui/react-dialog같은 라이브러리를 활용하거나, 직접 구현을 고려해보세요.Also applies to: 30-36
🤖 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/shared/lib/hooks/useDragScrollbar.ts` around lines 19 - 25, Modal lacks focus management and a focus trap; when opened move focus to the first focusable element inside the modal (use modalRef or the component that renders the dialog) and save/restore the previously focused element on close (store previousActiveElement and restore in the close handler like closeModal or onClose); implement a focus trap so Tab/Shift+Tab cycles inside the modal (use react-focus-lock or focus-trap-react around the modal content or implement keydown handling on modalRef to constrain focus); ensure Escape and backdrop-click still call the existing closeModal/onClose and that focus is restored after close.src/shared/ui/modal/DraftModal.tsx (2)
52-59: 💤 Low value이미지 다운로드 방지 구현 확인
onContextMenu과draggable={false}로 이미지 다운로드를 방지하고 있습니다. 이는 기본적인 우클릭/드래그를 막지만 완벽한 보안 수단은 아닙니다:
- 개발자 도구, 스크린샷, 브라우저 확장 등으로 여전히 이미지 저장 가능
pointer-events-none과select-none으로 추가 보호현재 구현이 의도한 사용 사례(일반 사용자의 우연한 저장 방지)에는 적절하지만, 중요한 저작권 보호가 필요하다면 워터마크나 서버 사이드 처리를 고려하세요.
🤖 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/shared/ui/modal/DraftModal.tsx` around lines 52 - 59, The Image element in DraftModal.tsx should also prevent the context menu and text selection to better block casual downloads: add an onContextMenu handler that calls preventDefault (e.g., onContextMenu={e => e.preventDefault()}) and include the "select-none" utility in the className (alongside "pointer-events-none object-cover") on the Image component so the element blocks right-click and selection in the browser UI.
46-61: ⚡ Quick win개선 제안: key 속성에 index 사용 지양
Line 48에서
key={${fileUrl}-${index}}로 index를 포함하고 있습니다. 만약fileUrls배열의 순서가 변경되거나 항목이 추가/삭제되면 React의 재조정(reconciliation)에 문제가 발생할 수 있습니다.
fileUrl이 고유하다면 index를 제거하고, 고유하지 않다면 각 파일에 고유 ID를 부여하는 것이 좋습니다.🔑 개선 제안
fileUrl이 고유한 경우:
- key={`${fileUrl}-${index}`} + key={fileUrl}fileUrl이 중복될 수 있는 경우:
// 데이터 모델에 id 추가 interface DraftFile { id: string; url: string; } // 컴포넌트 fileUrls.map((file) => ( <div key={file.id} ...> <Image src={file.url} ... /> </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/shared/ui/modal/DraftModal.tsx` around lines 46 - 61, The mapped elements in DraftModal use key={`${fileUrl}-${index}`} which can break React reconciliation when the array order changes; update the mapping to use a stable unique key instead: if fileUrl is guaranteed unique, use fileUrl alone as the key for the div inside the fileUrls.map, otherwise change the data model used by the map to include a stable id (e.g., DraftFile.id) and use that id as the key for the div and reference the file's url for the Image src; ensure you update references in the map callback (fileUrls.map, the div key and Image src) to reflect the new shape.src/shared/ui/Thumbnail.tsx (2)
28-35: ⚡ Quick win접근성 개선: 버튼에 명시적인 레이블 추가
"자세히 보기" 버튼의 텍스트가 시각적으로는 표시되지만, 아이콘(
SearchIcon)에 대한 스크린리더 처리가 없습니다. 아이콘은 장식 요소이므로aria-hidden="true"를 추가하는 것이 좋습니다.♿ 접근성 개선 제안
<button type="button" onClick={onDetailClick} className="backdrop-blur-button text-body2-m rounded-12 absolute top-1/2 left-1/2 flex -translate-x-1/2 -translate-y-1/2 cursor-pointer flex-row items-center gap-2.5 bg-white/18 px-4 py-2 text-white opacity-0 transition-opacity group-hover:opacity-100" + aria-label="썸네일 자세히 보기" > 자세히 보기 - <SearchIcon className="size-5" /> + <SearchIcon className="size-5" aria-hidden="true" /> </button>🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/shared/ui/Thumbnail.tsx` around lines 28 - 35, The SearchIcon used inside the "자세히 보기" button is decorative and needs to be hidden from screen readers: update the SearchIcon element in Thumbnail (the button with onDetailClick) to include aria-hidden="true"; if the SearchIcon component doesn't accept DOM props, wrap it in a span with aria-hidden="true" or update the SearchIcon implementation to forward aria-hidden to the underlying svg so the icon is ignored by assistive tech while the button text remains the accessible label.
26-26: ⚡ Quick win반응형 고려: Image sizes 속성 개선
sizes="250px"고정값은 모든 뷰포트에서 동일한 크기의 이미지를 로드합니다. Thumbnail이 다양한 크기로 사용될 수 있다면 반응형 sizes 값을 고려하세요.💡 반응형 sizes 예시
- <Image src={src} alt={alt} fill sizes="250px" loading="eager" className="object-cover" /> + <Image + src={src} + alt={alt} + fill + sizes="(max-width: 768px) 150px, 250px" + loading="eager" + className="object-cover" + />🤖 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/shared/ui/Thumbnail.tsx` at line 26, The fixed sizes="250px" on the Image component in Thumbnail.tsx forces the same image size across all viewports; update the Image props (the Image element using src, alt, fill, sizes) to provide a responsive sizes value (e.g. media-query based like "(max-width: 600px) 100vw, 250px") or compute sizes based on Thumbnail usage so the browser requests appropriately sized images; locate the Image in Thumbnail.tsx and replace the hardcoded sizes string with a responsive sizes expression or remove it and supply explicit width/height variants where appropriate.src/shared/ui/CommentCard.tsx (2)
9-10: ⚡ Quick win시맨틱 HTML 개선: 제목 레벨 조정
현재
h1과h2를 사용하고 있지만, 이는 페이지의 주제목/부제목이 아니라 카드 내부의 레이블과 텍스트입니다:
- Line 9:
h1은 페이지당 하나만 사용해야 합니다.h3또는div가 적절합니다.- Line 10: comment 텍스트는 제목이 아니므로
<p>태그가 더 시맨틱합니다.🏷️ 시맨틱 개선 제안
return ( <div className="bg-purple-5 border-purple-10 rounded-12 flex w-full flex-col gap-2.5 border px-6 py-4"> - <h1 className="text-main-main text-body1-sb">{title}</h1> - <h2 className="text-gray-80 text-body2-r scrollbar-hide h-16.5 overflow-y-auto">{comment}</h2> + <h3 className="text-main-main text-body1-sb">{title}</h3> + <p className="text-gray-80 text-body2-r scrollbar-hide h-16.5 overflow-y-auto">{comment}</p> </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/shared/ui/CommentCard.tsx` around lines 9 - 10, In CommentCard.tsx, replace the semantic misuse of heading tags: change the element rendering title (currently using <h1> with variable title) to a lower-level heading like <h3> or a <div> (preserving className "text-main-main text-body1-sb"), and change the comment text element (currently <h2> rendering variable comment) to a paragraph <p> (preserving className "text-gray-80 text-body2-r scrollbar-hide h-16.5 overflow-y-auto") so the card content is semantically correct while retaining existing styling.
10-10: ⚡ Quick winUX 고려: 스크롤바 숨김 처리
scrollbar-hide와overflow-y-auto를 함께 사용하면 스크롤 가능 여부가 시각적으로 명확하지 않아 사용자가 혼란스러울 수 있습니다. 특히 긴 코멘트의 경우 더 많은 내용이 있다는 것을 알기 어렵습니다.다음 대안을 고려하세요:
- 스크롤바를 표시하되 스타일링 (
::-webkit-scrollbar등)- Fade gradient를 하단에 추가하여 더 많은 콘텐츠가 있음을 시각적으로 표시
- "더보기" 버튼 추가
🤖 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/shared/ui/CommentCard.tsx` at line 10, The h2 in CommentCard rendering {comment} uses both "scrollbar-hide" and "overflow-y-auto", which hides visual affordance for additional content; update the CommentCard component to remove "scrollbar-hide" and instead implement one of the suggested affordances: (a) enable visible, styled scrollbars via CSS (target the h2 selector or its class and add ::-webkit-scrollbar rules), (b) keep overflow-y-auto and add a bottom fade gradient overlay to the same element to indicate more content, or (c) replace the fixed-height scrollable view with a collapsible "Show more"/"Show less" control that toggles a max-height style; ensure changes are applied where the h2 is defined (the element with className containing "text-gray-80 text-body2-r h-16.5 overflow-y-auto") and keep accessibility in mind (keyboard focus and aria-expanded for the toggle).
🤖 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/instructor/revision/`[commissionId]/page.tsx:
- Around line 88-90: The Button currently rendering "수정사항 전달하기" lacks an onClick
handler; add a handler (e.g., handleSubmit or onSubmitRevisions) in the same
component and attach it to the Button so clicks do something, have the handler
first check isSubmitActive, gather the selected category state and comment input
state used in this page, call the existing submission API/function (or create
one like submitRevision) and handle loading/success/error states (disable button
or show spinner while submitting and show feedback on result). Ensure you
reference the Button element and isSubmitActive when wiring the onClick and
implement the handler near the component logic so it can access
selectedCategories/comment state and any API helper.
In `@src/shared/ui/DragScrollbar.tsx`:
- Around line 17-31: The custom scrollbar only supports mouse input; add
keyboard and screen-reader support by making the thumb focusable (add a thumbRef
and tabindex=0 on the thumb element) and giving the track or thumb appropriate
ARIA attributes (e.g., role="scrollbar" or role="slider", aria-valuemin="0",
aria-valuemax="100", aria-valuenow={numericPercent}, and aria-label or
aria-valuetext). Implement a key handler (e.g., handleThumbKeyDown) on the thumb
to respond to ArrowLeft/ArrowRight (and optionally Home/End/PageUp/PageDown) to
update percent and call the same update logic used by handleTrackMouseDown;
ensure percent is kept as a numeric value for aria-valuenow and converted to a
string for style left/width, and ensure trackRef/handleTrackMouseDown logic
updates the thumb position and aria attributes consistently.
In `@src/shared/ui/modal/DraftModal.tsx`:
- Around line 19-25: The useEffect in DraftModal that registers handleKeyDown
currently depends on onClose causing add/remove of the keydown listener every
render if the parent doesn't memoize onClose; fix by memoizing the close handler
in the parent (e.g., create handleCloseDraftModal with useCallback and pass it
as onClose to DraftModal) or, if you prefer the change inside DraftModal,
stabilize the callback by storing onClose in a ref and reading it from the ref
inside handleKeyDown (keep useEffect deps minimal so you don’t repeatedly
remove/add the listener). Ensure you reference the existing
handleKeyDown/useEffect/onClose/DraftModal symbols when applying the change.
- Around line 29-41: Add standard dialog ARIA attributes and focus management to
DraftModal: give the modal container (the inner div that currently has className
"rounded-24 ...") role="dialog", aria-modal="true" and aria-labelledby pointing
to the title element (the h1 rendering {title}), add an aria-label or
aria-labelledby to the CloseCircleIcon button (and ensure it is a focusable
button element that calls onClose), implement a focus trap so Tab/Shift+Tab
cannot escape the modal and move initial focus to the close button (or first
focusable child) when DraftModal mounts, and prevent background scrolling by
toggling document.body.style.overflow when the modal opens/closes; update
mounting/unmounting logic in DraftModal to clean up focus and body-scroll
changes on close.
In `@src/widgets/instructor/revision/ui/RevisionCategorySection.tsx`:
- Around line 42-44: The button shown when remainingRevisionCount === 0 has no
onClick handler, so add an onClick prop to the button in RevisionCategorySection
(the JSX snippet rendering "수정 횟수를 추가하시겠어요?") and wire it to a handler: either
accept a callback prop (e.g., onAddRevisionClick) on the RevisionCategorySection
component and call that, or import/use the navigation/modal-opening function
used elsewhere (e.g., openRevisionModal or router.push) to perform the desired
action; ensure the component's props/type definition is updated to include the
new callback and that the button uses onClick={onAddRevisionClick} (or the
selected handler).
---
Nitpick comments:
In `@src/shared/lib/hooks/useDragScrollbar.ts`:
- Around line 19-25: Modal lacks focus management and a focus trap; when opened
move focus to the first focusable element inside the modal (use modalRef or the
component that renders the dialog) and save/restore the previously focused
element on close (store previousActiveElement and restore in the close handler
like closeModal or onClose); implement a focus trap so Tab/Shift+Tab cycles
inside the modal (use react-focus-lock or focus-trap-react around the modal
content or implement keydown handling on modalRef to constrain focus); ensure
Escape and backdrop-click still call the existing closeModal/onClose and that
focus is restored after close.
In `@src/shared/ui/CommentCard.tsx`:
- Around line 9-10: In CommentCard.tsx, replace the semantic misuse of heading
tags: change the element rendering title (currently using <h1> with variable
title) to a lower-level heading like <h3> or a <div> (preserving className
"text-main-main text-body1-sb"), and change the comment text element (currently
<h2> rendering variable comment) to a paragraph <p> (preserving className
"text-gray-80 text-body2-r scrollbar-hide h-16.5 overflow-y-auto") so the card
content is semantically correct while retaining existing styling.
- Line 10: The h2 in CommentCard rendering {comment} uses both "scrollbar-hide"
and "overflow-y-auto", which hides visual affordance for additional content;
update the CommentCard component to remove "scrollbar-hide" and instead
implement one of the suggested affordances: (a) enable visible, styled
scrollbars via CSS (target the h2 selector or its class and add
::-webkit-scrollbar rules), (b) keep overflow-y-auto and add a bottom fade
gradient overlay to the same element to indicate more content, or (c) replace
the fixed-height scrollable view with a collapsible "Show more"/"Show less"
control that toggles a max-height style; ensure changes are applied where the h2
is defined (the element with className containing "text-gray-80 text-body2-r
h-16.5 overflow-y-auto") and keep accessibility in mind (keyboard focus and
aria-expanded for the toggle).
In `@src/shared/ui/modal/DraftModal.tsx`:
- Around line 52-59: The Image element in DraftModal.tsx should also prevent the
context menu and text selection to better block casual downloads: add an
onContextMenu handler that calls preventDefault (e.g., onContextMenu={e =>
e.preventDefault()}) and include the "select-none" utility in the className
(alongside "pointer-events-none object-cover") on the Image component so the
element blocks right-click and selection in the browser UI.
- Around line 46-61: The mapped elements in DraftModal use
key={`${fileUrl}-${index}`} which can break React reconciliation when the array
order changes; update the mapping to use a stable unique key instead: if fileUrl
is guaranteed unique, use fileUrl alone as the key for the div inside the
fileUrls.map, otherwise change the data model used by the map to include a
stable id (e.g., DraftFile.id) and use that id as the key for the div and
reference the file's url for the Image src; ensure you update references in the
map callback (fileUrls.map, the div key and Image src) to reflect the new shape.
In `@src/shared/ui/Thumbnail.tsx`:
- Around line 28-35: The SearchIcon used inside the "자세히 보기" button is
decorative and needs to be hidden from screen readers: update the SearchIcon
element in Thumbnail (the button with onDetailClick) to include
aria-hidden="true"; if the SearchIcon component doesn't accept DOM props, wrap
it in a span with aria-hidden="true" or update the SearchIcon implementation to
forward aria-hidden to the underlying svg so the icon is ignored by assistive
tech while the button text remains the accessible label.
- Line 26: The fixed sizes="250px" on the Image component in Thumbnail.tsx
forces the same image size across all viewports; update the Image props (the
Image element using src, alt, fill, sizes) to provide a responsive sizes value
(e.g. media-query based like "(max-width: 600px) 100vw, 250px") or compute sizes
based on Thumbnail usage so the browser requests appropriately sized images;
locate the Image in Thumbnail.tsx and replace the hardcoded sizes string with a
responsive sizes expression or remove it and supply explicit width/height
variants where appropriate.
In `@src/widgets/instructor/revision/model/revision.ts`:
- Line 14: The exported mock constant draftRevisionDetailData should follow the
project's SCREAMING_SNAKE_CASE convention; rename draftRevisionDetailData to
DRAFT_REVISION_DETAIL_DATA (and update all imports/usages such as in
src/app/instructor/revision/[commissionId]/page.tsx) and ensure the exported
type DraftRevisionDetail references remain unchanged; update any tests or files
importing this constant to use the new name so compilation and imports stay
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 Plus
Run ID: 74fbd326-9764-47b1-8543-8063d2c9263d
⛔ Files ignored due to path filters (1)
public/images/thumbnail_mock.jpgis excluded by!**/*.jpg
📒 Files selected for processing (18)
src/app/globals.csssrc/app/instructor/layout.tsxsrc/app/instructor/revision/[commissionId]/page.tsxsrc/app/instructor/write/page.tsxsrc/features/instructor/home/model/home.tssrc/features/instructor/home/ui/ModifyingCommissionsRow.tsxsrc/shared/lib/hooks/useDragScrollbar.tssrc/shared/ui/CommentCard.tsxsrc/shared/ui/DragScrollbar.tsxsrc/shared/ui/SidebarMenu.tsxsrc/shared/ui/Thumbnail.tsxsrc/shared/ui/modal/DraftModal.tsxsrc/shared/ui/modal/Modal.tsxsrc/widgets/instructor/revision/config/revision.tssrc/widgets/instructor/revision/index.tssrc/widgets/instructor/revision/model/revision.tssrc/widgets/instructor/revision/ui/RevisionCategorySection.tsxsrc/widgets/instructor/revision/ui/RevisionCommentSection.tsx
| <Button className="w-fit" variant={isSubmitActive ? "medium_primary" : "medium_disabled"}> | ||
| 수정사항 전달하기 | ||
| </Button> |
There was a problem hiding this comment.
"수정사항 전달하기" 버튼에 onClick 핸들러가 누락되었습니다.
isSubmitActive 로직으로 버튼 활성화 상태는 제어하고 있지만, 실제로 수정사항을 제출하는 onClick 핸들러가 구현되지 않았습니다. 사용자가 카테고리를 선택하고 코멘트를 입력한 후 이 버튼을 클릭해도 아무 동작이 발생하지 않습니다.
🔧 onClick 핸들러 추가 예시
+ const handleSubmitRevision = () => {
+ // TODO: API 호출로 수정사항 전달
+ console.log('Submit revision:', { selectedCategories, comments });
+ };
<Button
className="w-fit"
- variant={isSubmitActive ? "medium_primary" : "medium_disabled"}
+ variant={isSubmitActive ? "medium_primary" : "medium_disabled"}
+ onClick={isSubmitActive ? handleSubmitRevision : undefined}
>
수정사항 전달하기
</Button>🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/app/instructor/revision/`[commissionId]/page.tsx around lines 88 - 90,
The Button currently rendering "수정사항 전달하기" lacks an onClick handler; add a
handler (e.g., handleSubmit or onSubmitRevisions) in the same component and
attach it to the Button so clicks do something, have the handler first check
isSubmitActive, gather the selected category state and comment input state used
in this page, call the existing submission API/function (or create one like
submitRevision) and handle loading/success/error states (disable button or show
spinner while submitting and show feedback on result). Ensure you reference the
Button element and isSubmitActive when wiring the onClick and implement the
handler near the component logic so it can access selectedCategories/comment
state and any API helper.
There was a problem hiding this comment.
@waldls, 이해했습니다. 나중에 API 연결 시 isSubmitActive 조건 체크와 함께 핸들러를 붙여주시면 됩니다.
추후 작업 추적을 위해 GitHub 이슈를 생성해 드릴까요?
🧠 Learnings used
Learnt from: waldls
Repo: Ditda-Official/Ditda-Frontend PR: 28
File: src/widgets/instructor/my/model/my.ts:14-17
Timestamp: 2026-06-07T17:37:21.449Z
Learning: In Ditda-Frontend, `myInfoData` in `src/widgets/instructor/my/model/my.ts` and `commissionHistoryData` in `src/features/instructor/my/model/my.ts` are independent mock/dummy data objects. Their stat fields (e.g., `totalCommissionCount`, `ongoingCommissionCount`) do not need to match the actual length or contents of the commission history mock array — they are placeholder values only.
Learnt from: waldls
Repo: Ditda-Official/Ditda-Frontend PR: 18
File: src/constants/write.ts:65-82
Timestamp: 2026-05-27T08:26:49.054Z
Learning: In Ditda-Frontend (TypeScript/Next.js), exported constants should be named in SCREAMING_SNAKE_CASE (e.g., BASIC_INFO_FIELDS, PAGE_OPTIONS). The camelCase naming guideline should apply to functions and non-exported/regular variables, but not to exported constants.
| return ( | ||
| <div className={cn("flex h-12 items-center", className)}> | ||
| <div | ||
| ref={trackRef} | ||
| className="bg-purple-10 relative h-2 w-full cursor-pointer" | ||
| onMouseDown={e => handleTrackMouseDown(e.clientX)} | ||
| > | ||
| <div className="bg-purple-30 absolute inset-y-0 left-0" style={{ width: percent }} /> | ||
| <div | ||
| className="rounded-48 border-main-main absolute top-1/2 size-12 -translate-x-1/2 -translate-y-1/2 cursor-grab border-4 bg-white active:cursor-grabbing" | ||
| style={{ left: percent }} | ||
| /> | ||
| </div> | ||
| </div> | ||
| ); |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major | 🏗️ Heavy lift
접근성 개선 필요: 키보드 탐색 및 스크린리더 지원
커스텀 스크롤바가 마우스 인터랙션만 지원하고 키보드 접근성과 스크린리더 지원이 없습니다:
- 스크롤바 트랙과 썸에 적절한 ARIA role이 없습니다 (예:
role="scrollbar",aria-valuemin,aria-valuemax,aria-valuenow) - 키보드로 포커스하고 조작할 수 없습니다 (화살표 키로 스크롤 조정)
- 스크린리더 사용자가 현재 스크롤 위치를 알 수 없습니다
♿ 접근성 개선 제안
return (
<div className={cn("flex h-12 items-center", className)}>
<div
ref={trackRef}
className="bg-purple-10 relative h-2 w-full cursor-pointer"
onMouseDown={e => handleTrackMouseDown(e.clientX)}
+ role="scrollbar"
+ aria-orientation="horizontal"
+ aria-valuemin={0}
+ aria-valuemax={100}
+ aria-valuenow={Math.round(progress * 100)}
+ aria-label="수평 스크롤"
+ tabIndex={0}
+ onKeyDown={e => {
+ // 화살표 키로 스크롤 조정 로직 추가
+ }}
>
<div className="bg-purple-30 absolute inset-y-0 left-0" style={{ width: percent }} />
<div
className="rounded-48 border-main-main absolute top-1/2 size-12 -translate-x-1/2 -translate-y-1/2 cursor-grab border-4 bg-white active:cursor-grabbing"
style={{ left: percent }}
+ aria-hidden="true"
/>
</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/shared/ui/DragScrollbar.tsx` around lines 17 - 31, The custom scrollbar
only supports mouse input; add keyboard and screen-reader support by making the
thumb focusable (add a thumbRef and tabindex=0 on the thumb element) and giving
the track or thumb appropriate ARIA attributes (e.g., role="scrollbar" or
role="slider", aria-valuemin="0", aria-valuemax="100",
aria-valuenow={numericPercent}, and aria-label or aria-valuetext). Implement a
key handler (e.g., handleThumbKeyDown) on the thumb to respond to
ArrowLeft/ArrowRight (and optionally Home/End/PageUp/PageDown) to update percent
and call the same update logic used by handleTrackMouseDown; ensure percent is
kept as a numeric value for aria-valuenow and converted to a string for style
left/width, and ensure trackRef/handleTrackMouseDown logic updates the thumb
position and aria attributes consistently.
| useEffect(() => { | ||
| const handleKeyDown = (e: KeyboardEvent) => { | ||
| if (e.key === "Escape") onClose(); | ||
| }; | ||
| if (isOpen) document.addEventListener("keydown", handleKeyDown); | ||
| return () => document.removeEventListener("keydown", handleKeyDown); | ||
| }, [isOpen, onClose]); |
There was a problem hiding this comment.
잠재적 성능 이슈: useEffect 의존성 배열의 onClose
onClose가 의존성 배열에 포함되어 있는데, 만약 부모 컴포넌트에서 onClose를 메모이제이션하지 않으면 매 렌더마다 이벤트 리스너가 제거/재등록됩니다.
🔧 해결 방법
방법 1: 부모에서 useCallback 사용 (권장)
// RevisionCategorySection.tsx에서
const handleCloseDraftModal = useCallback(() => {
setIsDraftModalOpen(false);
}, []);
<DraftModal onClose={handleCloseDraftModal} ... />방법 2: useCallback으로 onClose 래핑
const DraftModal = ({ isOpen, onClose, title, fileUrls }: DraftModalProps) => {
const scrollRef = useRef<HTMLDivElement>(null);
+ const handleClose = useCallback(() => onClose(), [onClose]);
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
- if (e.key === "Escape") onClose();
+ if (e.key === "Escape") handleClose();
};
- if (isOpen) document.addEventListener("keydown", handleKeyDown);
+ if (isOpen) document.addEventListener("keydown", handleKeyDown);
return () => document.removeEventListener("keydown", handleKeyDown);
- }, [isOpen, onClose]);
+ }, [isOpen, handleClose]);🤖 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/shared/ui/modal/DraftModal.tsx` around lines 19 - 25, The useEffect in
DraftModal that registers handleKeyDown currently depends on onClose causing
add/remove of the keydown listener every render if the parent doesn't memoize
onClose; fix by memoizing the close handler in the parent (e.g., create
handleCloseDraftModal with useCallback and pass it as onClose to DraftModal) or,
if you prefer the change inside DraftModal, stabilize the callback by storing
onClose in a ref and reading it from the ref inside handleKeyDown (keep
useEffect deps minimal so you don’t repeatedly remove/add the listener). Ensure
you reference the existing handleKeyDown/useEffect/onClose/DraftModal symbols
when applying the change.
| return ( | ||
| <div | ||
| className="fixed inset-0 z-50 flex items-center justify-center bg-black/60" | ||
| onClick={onClose} | ||
| > | ||
| <div | ||
| className="rounded-24 flex h-191 w-303.5 flex-col bg-white py-10" | ||
| onClick={e => e.stopPropagation()} | ||
| > | ||
| <div className="flex justify-between px-14 pb-10"> | ||
| <h1 className="text-heading1-sb flex items-center text-black">{title}</h1> | ||
| <CloseCircleIcon className="text-gray-70 size-12 cursor-pointer" onClick={onClose} /> | ||
| </div> |
There was a problem hiding this comment.
접근성 필수 개선: 모달 ARIA 속성 및 포커스 관리
모달 컴포넌트에 필수적인 접근성 기능이 누락되었습니다:
- ARIA 속성:
role="dialog",aria-modal="true",aria-labelledby누락 - 닫기 버튼 레이블: CloseCircleIcon 버튼에
aria-label없음 - 포커스 트랩: 모달이 열렸을 때 포커스가 모달 내부에 갇혀야 하는데 현재는 Tab 키로 배경 콘텐츠로 이동 가능
- 초기 포커스: 모달이 열릴 때 닫기 버튼이나 첫 번째 포커스 가능 요소로 포커스 이동 필요
- Body 스크롤 방지: 모달이 열렸을 때 배경 스크롤 방지 필요
♿ 접근성 개선 제안
+ import { useEffect, useRef, useCallback } from "react";
+
const DraftModal = ({ isOpen, onClose, title, fileUrls }: DraftModalProps) => {
const scrollRef = useRef<HTMLDivElement>(null);
+ const modalRef = useRef<HTMLDivElement>(null);
+ const closeButtonRef = useRef<HTMLButtonElement>(null);
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === "Escape") onClose();
};
if (isOpen) document.addEventListener("keydown", handleKeyDown);
return () => document.removeEventListener("keydown", handleKeyDown);
}, [isOpen, onClose]);
+ // Body 스크롤 방지
+ useEffect(() => {
+ if (isOpen) {
+ document.body.style.overflow = "hidden";
+ return () => {
+ document.body.style.overflow = "";
+ };
+ }
+ }, [isOpen]);
+
+ // 초기 포커스 설정
+ useEffect(() => {
+ if (isOpen && closeButtonRef.current) {
+ closeButtonRef.current.focus();
+ }
+ }, [isOpen]);
+
+ // 포커스 트랩 (간단한 구현 - 라이브러리 사용 권장)
+ const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
+ if (e.key === "Tab" && modalRef.current) {
+ const focusableElements = modalRef.current.querySelectorAll(
+ 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
+ );
+ const firstElement = focusableElements[0] as HTMLElement;
+ const lastElement = focusableElements[focusableElements.length - 1] as HTMLElement;
+
+ if (e.shiftKey && document.activeElement === firstElement) {
+ e.preventDefault();
+ lastElement.focus();
+ } else if (!e.shiftKey && document.activeElement === lastElement) {
+ e.preventDefault();
+ firstElement.focus();
+ }
+ }
+ }, []);
if (!isOpen) return null;
return (
<div
className="fixed inset-0 z-50 flex items-center justify-center bg-black/60"
onClick={onClose}
+ role="presentation"
>
<div
+ ref={modalRef}
className="rounded-24 flex h-191 w-303.5 flex-col bg-white py-10"
onClick={e => e.stopPropagation()}
+ role="dialog"
+ aria-modal="true"
+ aria-labelledby="draft-modal-title"
+ onKeyDown={handleKeyDown}
>
<div className="flex justify-between px-14 pb-10">
- <h1 className="text-heading1-sb flex items-center text-black">{title}</h1>
+ <h1 id="draft-modal-title" className="text-heading1-sb flex items-center text-black">{title}</h1>
- <CloseCircleIcon className="text-gray-70 size-12 cursor-pointer" onClick={onClose} />
+ <button
+ ref={closeButtonRef}
+ type="button"
+ onClick={onClose}
+ aria-label="모달 닫기"
+ className="text-gray-70"
+ >
+ <CloseCircleIcon className="size-12 cursor-pointer" />
+ </button>
</div>참고: 프로덕션 환경에서는 focus-trap-react 같은 라이브러리 사용을 권장합니다.
📝 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.
| return ( | |
| <div | |
| className="fixed inset-0 z-50 flex items-center justify-center bg-black/60" | |
| onClick={onClose} | |
| > | |
| <div | |
| className="rounded-24 flex h-191 w-303.5 flex-col bg-white py-10" | |
| onClick={e => e.stopPropagation()} | |
| > | |
| <div className="flex justify-between px-14 pb-10"> | |
| <h1 className="text-heading1-sb flex items-center text-black">{title}</h1> | |
| <CloseCircleIcon className="text-gray-70 size-12 cursor-pointer" onClick={onClose} /> | |
| </div> | |
| import { useEffect, useRef, useCallback } from "react"; | |
| const DraftModal = ({ isOpen, onClose, title, fileUrls }: DraftModalProps) => { | |
| const scrollRef = useRef<HTMLDivElement>(null); | |
| const modalRef = useRef<HTMLDivElement>(null); | |
| const closeButtonRef = useRef<HTMLButtonElement>(null); | |
| useEffect(() => { | |
| const handleKeyDown = (e: KeyboardEvent) => { | |
| if (e.key === "Escape") onClose(); | |
| }; | |
| if (isOpen) document.addEventListener("keydown", handleKeyDown); | |
| return () => document.removeEventListener("keydown", handleKeyDown); | |
| }, [isOpen, onClose]); | |
| // Body 스크롤 방지 | |
| useEffect(() => { | |
| if (isOpen) { | |
| document.body.style.overflow = "hidden"; | |
| return () => { | |
| document.body.style.overflow = ""; | |
| }; | |
| } | |
| }, [isOpen]); | |
| // 초기 포커스 설정 | |
| useEffect(() => { | |
| if (isOpen && closeButtonRef.current) { | |
| closeButtonRef.current.focus(); | |
| } | |
| }, [isOpen]); | |
| // 포커스 트랩 (간단한 구현 - 라이브러리 사용 권장) | |
| const handleKeyDown = useCallback((e: React.KeyboardEvent) => { | |
| if (e.key === "Tab" && modalRef.current) { | |
| const focusableElements = modalRef.current.querySelectorAll( | |
| 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])' | |
| ); | |
| const firstElement = focusableElements[0] as HTMLElement; | |
| const lastElement = focusableElements[focusableElements.length - 1] as HTMLElement; | |
| if (e.shiftKey && document.activeElement === firstElement) { | |
| e.preventDefault(); | |
| lastElement.focus(); | |
| } else if (!e.shiftKey && document.activeElement === lastElement) { | |
| e.preventDefault(); | |
| firstElement.focus(); | |
| } | |
| } | |
| }, []); | |
| if (!isOpen) return null; | |
| return ( | |
| <div | |
| className="fixed inset-0 z-50 flex items-center justify-center bg-black/60" | |
| onClick={onClose} | |
| role="presentation" | |
| > | |
| <div | |
| ref={modalRef} | |
| className="rounded-24 flex h-191 w-303.5 flex-col bg-white py-10" | |
| onClick={e => e.stopPropagation()} | |
| role="dialog" | |
| aria-modal="true" | |
| aria-labelledby="draft-modal-title" | |
| onKeyDown={handleKeyDown} | |
| > | |
| <div className="flex justify-between px-14 pb-10"> | |
| <h1 id="draft-modal-title" className="text-heading1-sb flex items-center text-black">{title}</h1> | |
| <button | |
| ref={closeButtonRef} | |
| type="button" | |
| onClick={onClose} | |
| aria-label="모달 닫기" | |
| className="text-gray-70" | |
| > | |
| <CloseCircleIcon className="size-12 cursor-pointer" /> | |
| </button> | |
| </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/shared/ui/modal/DraftModal.tsx` around lines 29 - 41, Add standard dialog
ARIA attributes and focus management to DraftModal: give the modal container
(the inner div that currently has className "rounded-24 ...") role="dialog",
aria-modal="true" and aria-labelledby pointing to the title element (the h1
rendering {title}), add an aria-label or aria-labelledby to the CloseCircleIcon
button (and ensure it is a focusable button element that calls onClose),
implement a focus trap so Tab/Shift+Tab cannot escape the modal and move initial
focus to the close button (or first focusable child) when DraftModal mounts, and
prevent background scrolling by toggling document.body.style.overflow when the
modal opens/closes; update mounting/unmounting logic in DraftModal to clean up
focus and body-scroll changes on close.
| <button className="text-gray-80 text-body2-m cursor-pointer underline underline-offset-2"> | ||
| 수정 횟수를 추가하시겠어요? | ||
| </button> |
There was a problem hiding this comment.
onClick 핸들러가 누락되었습니다.
remainingRevisionCount === 0일 때 표시되는 "수정 횟수를 추가하시겠어요?" 버튼에 onClick 핸들러가 없어 클릭해도 아무 동작이 발생하지 않습니다. 이 버튼이 특정 동작(예: 페이지 이동, 모달 열기 등)을 수행해야 한다면 핸들러를 추가해야 합니다.
🔧 onClick 핸들러 추가 예시
- <button className="text-gray-80 text-body2-m cursor-pointer underline underline-offset-2">
+ <button
+ onClick={() => {/* TODO: 수정 횟수 추가 로직 */}}
+ className="text-gray-80 text-body2-m cursor-pointer underline underline-offset-2"
+ >
수정 횟수를 추가하시겠어요?
</button>📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <button className="text-gray-80 text-body2-m cursor-pointer underline underline-offset-2"> | |
| 수정 횟수를 추가하시겠어요? | |
| </button> | |
| <button | |
| onClick={() => {/* TODO: 수정 횟수 추가 로직 */}} | |
| className="text-gray-80 text-body2-m cursor-pointer underline underline-offset-2" | |
| > | |
| 수정 횟수를 추가하시겠어요? | |
| </button> |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/widgets/instructor/revision/ui/RevisionCategorySection.tsx` around lines
42 - 44, The button shown when remainingRevisionCount === 0 has no onClick
handler, so add an onClick prop to the button in RevisionCategorySection (the
JSX snippet rendering "수정 횟수를 추가하시겠어요?") and wire it to a handler: either accept
a callback prop (e.g., onAddRevisionClick) on the RevisionCategorySection
component and call that, or import/use the navigation/modal-opening function
used elsewhere (e.g., openRevisionModal or router.push) to perform the desired
action; ensure the component's props/type definition is updated to include the
new callback and that the button uses onClick={onAddRevisionClick} (or the
selected handler).
There was a problem hiding this comment.
@waldls, 알겠습니다! 추후 외부 링크 연결 시 onClick 핸들러 및 필요하다면 onAddRevisionClick prop도 함께 추가해 주시면 좋을 것 같습니다.
원하신다면 이 작업을 추적하기 위한 후속 이슈를 생성해 드릴까요?
🧠 Learnings used
Learnt from: waldls
Repo: Ditda-Official/Ditda-Frontend PR: 18
File: src/constants/write.ts:65-82
Timestamp: 2026-05-27T08:26:49.054Z
Learning: In Ditda-Frontend (TypeScript/Next.js), exported constants should be named in SCREAMING_SNAKE_CASE (e.g., BASIC_INFO_FIELDS, PAGE_OPTIONS). The camelCase naming guideline should apply to functions and non-exported/regular variables, but not to exported constants.
📢 PR 유형
어떤 변경 사항이 있었나요?
📌 관련 이슈번호
✅ Key Changes
수정 요청 페이지 구현 (/instructor/revision/[commissionId])
commissionId동적 라우팅으로 강사가 진행 중 외주 홈에서 "확인하기"를 누르면 해당 시안의 수정 요청 페이지로 이동하도록 연결RevisionCategorySection과, 선택한 카테고리별로 수정 방향을 입력하는RevisionCommentSection위젯 추가시안 확인(원본 이미지) 모달 DraftModal 추가
onContextMenu,draggable={false})커스텀 드래그 스크롤바 (DragScrollbar, useDragScrollbar)
공용 컴포넌트 추가
Thumbnail: 호버 시 오버레이와 "자세히 보기" 버튼이 노출되는 썸네일 컴포넌트CommentCard: 디자이너 코멘트를 표시하는 카드 컴포넌트Modal을shared/ui하위에서shared/ui/modal폴더로 이동하고 import 경로 정리기타
SidebarMenu에 matchPrefix prop을 추가해/instructor/revision/*하위 경로에서도 "진행 중 외주" 메뉴가 활성 상태로 표시되도록 처리overlay-hover), 블러 토큰(blur-hover,blur-button), radius-24 추가modifyingStatusData,draftRevisionDetailData,draftFilesData) 정비 및 시안 목업 이미지 추가📸 스크린샷 or 실행영상
revision.mp4
🎸 기타 사항 or 추가 코멘트
Summary by CodeRabbit
릴리스 노트
새로운 기능
스타일