[REFACTOR] 강사 새 외주 작성 수정사항 반영#43
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워크스루강사 외주 작성 페이지의 UT 수정사항을 반영하여, 카테고리/컨셉/색상/파일업로드/결제 모달 섹션의 UI 및 로직을 변경한다. 공유 UI 컴포넌트(Chip, Radio, Toast, Sidebar)를 확장하고, 파일 검증 유틸, 날짜 드롭다운 즉시 확정 인터랙션, CSS 토큰을 추가한다. 변경 사항강사 외주 작성 페이지 UT 수정사항 반영
추정 코드 리뷰 노력🎯 4 (Complex) | ⏱️ ~60 minutes 연관 가능성이 있는 PR
추천 레이블
추천 리뷰어
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ 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: 6
🧹 Nitpick comments (5)
src/widgets/instructor/write/ui/AttachFileSection.tsx (1)
25-42: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win중복 로직을 공유 훅으로 추출하는 것을 고려해주세요.
handleValidatedFilesAdded와 두 모달 상태/렌더링이ReferenceSection과 거의 동일합니다(차이는MAX_FILE_COUNT와 일부 문구뿐). 공유 훅(예:useValidatedFileUpload)으로 추출하면 중복을 줄이고 검증 규칙 변경 시 일관성을 유지할 수 있습니다.🤖 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/write/ui/AttachFileSection.tsx` around lines 25 - 42, `handleValidatedFilesAdded` and its invalid-file/count modal state are duplicating nearly the same upload-validation flow used in `ReferenceSection`; extract the shared validation and modal handling into a reusable hook such as `useValidatedFileUpload`, then have `AttachFileSection` consume it with its own `MAX_FILE_COUNT` and UI text overrides. Keep the existing `handleValidatedFilesAdded` entry point and wire its validation, modal open/close state, and file-add callback through the shared hook so both sections stay consistent when rules change.src/widgets/instructor/write/ui/DesignConceptSection.tsx (2)
16-17: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win토스트/부제의 "5개"가
MAX_CONCEPT_SELECT와 중복되어 드리프트 위험이 있습니다.
LIMIT_TOAST_MESSAGE(라인 17)와 부제 "최대 5개까지"(라인 57)에 숫자가 하드코딩되어 있습니다.MAX_CONCEPT_SELECT는 이미2→5로 한 번 변경된 이력이 있어, 다음 변경 시 문구가 어긋날 수 있습니다. 상수에서 파생하도록 변경을 권장합니다.♻️ 제안
-const LIMIT_TOAST_MESSAGE = - "컨셉은 5개까지 선택할 수 있습니다. 추가적인 내용은 하단 토글을 열어 작성해주세요."; +const LIMIT_TOAST_MESSAGE = `컨셉은 ${MAX_CONCEPT_SELECT}개까지 선택할 수 있습니다. 추가적인 내용은 하단 토글을 열어 작성해주세요.`;🤖 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/write/ui/DesignConceptSection.tsx` around lines 16 - 17, The “5개” text is duplicated in both LIMIT_TOAST_MESSAGE and the subtitle, creating drift risk from MAX_CONCEPT_SELECT. Update DesignConceptSection so the toast message and the subtitle are derived from MAX_CONCEPT_SELECT instead of hardcoding the number, using the existing MAX_CONCEPT_SELECT symbol to keep the copy in sync when the limit changes.
77-85: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value조건부 클래스는
cn()사용을 제안합니다.라인 78, 83의 조건부 클래스 문자열을 템플릿 리터럴로 직접 이어붙이고 있습니다.
cn()유틸리티 사용을 권장합니다.As per path instructions: "조건부 클래스 문자열을 직접 이어붙이는 경우 cn() 유틸리티 사용을 제안해주세요."
🤖 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/write/ui/DesignConceptSection.tsx` around lines 77 - 85, The conditional class strings in DesignConceptSection should use the shared cn() utility instead of inline template concatenation. Update the className handling on the ArrowDownIcon and the collapsible container to build classes through cn(), keeping the same isAdditionalOpen-dependent styling while making the JSX consistent with the codebase’s class composition pattern.Source: Path instructions
src/widgets/instructor/write/ui/CategorySection.tsx (2)
59-77: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value활성화 조건이 문자열 하드코딩에 묶여 있습니다.
disabled={item !== "교재 외지/내지"}로 인해 확장된CATEGORIES의 나머지 항목들이 전부 비활성화됩니다. 의도된 동작(현재는 단일 항목만 활성)일 수 있으나, 매직 스트링이 UI 로직에 직접 박혀 있어 향후 라벨 변경 시 조용히 깨질 수 있습니다. 활성 항목을 config(예:CATEGORIES의 item 메타데이터)로 끌어올리는 것을 고려해주세요.현재 단일 항목만 활성화하는 것이 의도된 사양인지 확인 부탁드립니다.
🤖 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/write/ui/CategorySection.tsx` around lines 59 - 77, The item enable/disable logic in CategorySection is hardcoded to a label string, so the currently active option will break if the text changes or if CATEGORIES expands. Move the enabled-state decision out of the JSX condition into config-driven metadata on CATEGORIES (or a similar item property) and use that in the Radio rendering, keeping the current single-active-item behavior only if that is truly intended. Use the CategorySection component, the CATEGORIES list, and the Radio props as the main points to update.
28-30: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value조건부 클래스 문자열은
cn()사용을 제안합니다.템플릿 리터럴로 조건부 클래스를 직접 이어붙이고 있습니다(라인 29, 44). 가독성과 일관성을 위해
cn()유틸리티 사용을 권장합니다(같은 PR의ColorChooseCard.tsx/ColorChooseSection.tsx는 이미cn()사용).As per path instructions: "조건부 클래스 문자열을 직접 이어붙이는 경우 cn() 유틸리티 사용을 제안해주세요."
Also applies to: 43-45
🤖 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/write/ui/CategorySection.tsx` around lines 28 - 30, 조건부 클래스 문자열을 템플릿 리터럴로 직접 이어붙이고 있으므로 `cn()` 유틸리티로 통일하세요. `CategorySection`의 해당 `div`와 함께 `openIndex`에 따라 달라지는 클래스 조합을 `cn()` 호출로 옮겨 가독성과 일관성을 높이고, 같은 PR의 `ColorChooseCard`/`ColorChooseSection`처럼 패턴을 맞추세요. `CategorySection`의 JSX에서 조건부 `className`을 구성하는 부분을 찾아 직접 문자열 결합을 제거하면 됩니다.Source: Path instructions
🤖 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/features/instructor/write/ui/ConceptResult.tsx`:
- Around line 10-22: In ConceptResult, the container currently uses a fixed
height that can clip wrapped chips when selectedKeywords spans multiple rows.
Update the wrapper in ConceptResult.tsx to avoid a hard height by switching to a
minimum height or, if you want to keep a single row, enforce no wrapping with
horizontal overflow; keep the existing Chip rendering and onRemove behavior
unchanged.
In `@src/features/instructor/write/ui/PaymentModal/Step1.tsx`:
- Around line 127-135: The auto-dismiss logic in Step1’s toast state conflicts
with the CTA disable logic because `showError` only controls visibility while
the button still depends on `!!errorMessage`, and `dismissedMessage` can
suppress re-showing the same error. Update the `Step1` component so the error
display and CTA disabled state use consistent conditions: either keep the toast
visible whenever `errorMessage` is present, or separate the transient toast
state from the actual blocking error state used by the button. Make sure the
`useEffect` timeout and `showError`/CTA logic are aligned so the user can still
understand why the action is blocked and the same error can reappear when
appropriate.
In `@src/shared/ui/Sidebar.tsx`:
- Around line 13-25: The Sidebar effect leaves a stale global CSS variable
behind because only ResizeObserver cleanup runs; update the cleanup in Sidebar’s
useEffect to also clear `--sidebar-w` from document.documentElement when the
component unmounts. Keep the existing updateWidth/ResizeObserver behavior, but
ensure the teardown removes the variable so consumers like DesignConceptSection
can fall back to 0 correctly when Sidebar is not mounted.
In `@src/shared/ui/Toast.tsx`:
- Around line 12-22: The Toast component is missing live region semantics, so
add an appropriate accessibility announcement to the main container in Toast so
screen readers are notified when the message appears. Update the div in
Toast.tsx to use role="alert" or role="status" with a matching aria-live value,
and make sure the hidden state used by show does not prevent the announcement
from being conveyed to assistive technology. Keep the change localized to the
Toast component and preserve the existing visual behavior while adding the
accessibility attributes.
In `@src/widgets/instructor/write/ui/AttachFileSection.tsx`:
- Around line 25-42: handleValidatedFilesAdded currently opens both
isInvalidFileModalOpen and isFileCountExceededModalOpen in the same call path,
so make these cases mutually exclusive. In AttachFileSection.tsx, update
handleValidatedFilesAdded to branch on invalid files first and return
immediately after opening the invalid-file modal, or otherwise ensure the
MAX_FILE_COUNT check only runs when there are no invalid files. Keep the
existing behavior in handleFilesAdded, isAllowedFileType, MAX_FILE_SIZE_BYTES,
and MAX_FILE_COUNT, but prevent both modals from being set true together.
In `@src/widgets/instructor/write/ui/NecessaryPageChooseSection.tsx`:
- Around line 31-33: The user-facing copy in NecessaryPageChooseSection needs a
spacing correction: change the phrase with “페이지 별” to “페이지별” in the heading text
rendered by that component. Update the string inside the h2 in
NecessaryPageChooseSection so the visible copy is polished and consistent with
the intended Korean spacing.
---
Nitpick comments:
In `@src/widgets/instructor/write/ui/AttachFileSection.tsx`:
- Around line 25-42: `handleValidatedFilesAdded` and its invalid-file/count
modal state are duplicating nearly the same upload-validation flow used in
`ReferenceSection`; extract the shared validation and modal handling into a
reusable hook such as `useValidatedFileUpload`, then have `AttachFileSection`
consume it with its own `MAX_FILE_COUNT` and UI text overrides. Keep the
existing `handleValidatedFilesAdded` entry point and wire its validation, modal
open/close state, and file-add callback through the shared hook so both sections
stay consistent when rules change.
In `@src/widgets/instructor/write/ui/CategorySection.tsx`:
- Around line 59-77: The item enable/disable logic in CategorySection is
hardcoded to a label string, so the currently active option will break if the
text changes or if CATEGORIES expands. Move the enabled-state decision out of
the JSX condition into config-driven metadata on CATEGORIES (or a similar item
property) and use that in the Radio rendering, keeping the current
single-active-item behavior only if that is truly intended. Use the
CategorySection component, the CATEGORIES list, and the Radio props as the main
points to update.
- Around line 28-30: 조건부 클래스 문자열을 템플릿 리터럴로 직접 이어붙이고 있으므로 `cn()` 유틸리티로 통일하세요.
`CategorySection`의 해당 `div`와 함께 `openIndex`에 따라 달라지는 클래스 조합을 `cn()` 호출로 옮겨 가독성과
일관성을 높이고, 같은 PR의 `ColorChooseCard`/`ColorChooseSection`처럼 패턴을 맞추세요.
`CategorySection`의 JSX에서 조건부 `className`을 구성하는 부분을 찾아 직접 문자열 결합을 제거하면 됩니다.
In `@src/widgets/instructor/write/ui/DesignConceptSection.tsx`:
- Around line 16-17: The “5개” text is duplicated in both LIMIT_TOAST_MESSAGE and
the subtitle, creating drift risk from MAX_CONCEPT_SELECT. Update
DesignConceptSection so the toast message and the subtitle are derived from
MAX_CONCEPT_SELECT instead of hardcoding the number, using the existing
MAX_CONCEPT_SELECT symbol to keep the copy in sync when the limit changes.
- Around line 77-85: The conditional class strings in DesignConceptSection
should use the shared cn() utility instead of inline template concatenation.
Update the className handling on the ArrowDownIcon and the collapsible container
to build classes through cn(), keeping the same isAdditionalOpen-dependent
styling while making the JSX consistent with the codebase’s class composition
pattern.
🪄 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: d37763e9-d3b0-46d9-9cbd-4e73f3d7ddd4
📒 Files selected for processing (28)
src/app/globals.csssrc/app/instructor/write/layout.tsxsrc/features/instructor/write/config/write.tssrc/features/instructor/write/model/writeFormStore.tssrc/features/instructor/write/ui/ColorChooseCard.tsxsrc/features/instructor/write/ui/ConceptKeywordCard.tsxsrc/features/instructor/write/ui/ConceptResult.tsxsrc/features/instructor/write/ui/PaymentModal/Step1.tsxsrc/shared/lib/utils/dropdown.tssrc/shared/lib/utils/file.tssrc/shared/ui/Chip.tsxsrc/shared/ui/Header.tsxsrc/shared/ui/Radio.tsxsrc/shared/ui/Sidebar.tsxsrc/shared/ui/Toast.tsxsrc/shared/ui/Toggle.tsxsrc/shared/ui/dropdown/DateDropdownMenu.tsxsrc/shared/ui/dropdown/WheelColumn.tsxsrc/widgets/instructor/write/ui/AttachFileSection.tsxsrc/widgets/instructor/write/ui/BasicInfoTypingSection.tsxsrc/widgets/instructor/write/ui/CategorySection.tsxsrc/widgets/instructor/write/ui/ColorChooseSection.tsxsrc/widgets/instructor/write/ui/DeadlineChooseSection.tsxsrc/widgets/instructor/write/ui/DesignConceptSection.tsxsrc/widgets/instructor/write/ui/NecessaryPageChooseSection.tsxsrc/widgets/instructor/write/ui/PlanChooseSection.tsxsrc/widgets/instructor/write/ui/ReferenceSection.tsxsrc/widgets/instructor/write/ui/SizeSection.tsx
| <div className="rounded-12 bg-gray-5 flex h-12.5 flex-row gap-8 px-4 py-2"> | ||
| <p className="text-gray-80 text-body1-m flex items-center">선택한 컨셉</p> | ||
| <div className="flex flex-row flex-wrap gap-2"> | ||
| {selectedKeywords.map(keyword => ( | ||
| <Chip | ||
| key={keyword} | ||
| label={keyword} | ||
| variant="removable" | ||
| onRemove={() => onRemove(keyword)} | ||
| /> | ||
| ))} | ||
| </div> | ||
| </div> |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n## ConceptResult.tsx\n'
sed -n '1,220p' src/features/instructor/write/ui/ConceptResult.tsx
printf '\n## Chip component references\n'
rg -n "export .*Chip|function Chip|const Chip|<Chip" src -g '*.{ts,tsx}' -A 3 -B 3
printf '\n## h-12.5 / rounded-12 utility definitions if present\n'
rg -n "h-12\\.5|rounded-12|text-body1-m|bg-gray-5|text-gray-80" src -g '*.{ts,tsx,css,scss,less}' -A 2 -B 2Repository: Ditda-Official/Ditda-Frontend
Length of output: 50385
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n## Chip.tsx (relevant slices)\n'
sed -n '1,220p' src/shared/ui/Chip.tsx
printf '\n## ConceptResult usage and surrounding layout\n'
rg -n "ConceptResult|selectedKeywords" src/features/instructor/write src/widgets/instructor/write -g '*.{ts,tsx}' -A 5 -B 5
printf '\n## Nearby container widths in write flow\n'
rg -n "w-[0-9]|max-w-|min-w-|flex-wrap|overflow" src/features/instructor/write src/widgets/instructor/write -g '*.{ts,tsx}' -A 2 -B 2Repository: Ditda-Official/Ditda-Frontend
Length of output: 33087
src/features/instructor/write/ui/ConceptResult.tsx: 고정 높이는 제거하는 편이 안전합니다.
selectedKeywords가 최대 5개라 좁은 폭에서는 칩이 2줄로 래핑될 수 있는데, 현재 h-12.5로 고정돼 있어 두 번째 줄이 박스 밖으로 밀려나 아래 영역과 겹칠 수 있습니다. min-h-12.5로 바꾸거나, 한 줄만 유지하려면 flex-nowrap + overflow-x-auto로 맞춰주세요.
🤖 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/features/instructor/write/ui/ConceptResult.tsx` around lines 10 - 22, In
ConceptResult, the container currently uses a fixed height that can clip wrapped
chips when selectedKeywords spans multiple rows. Update the wrapper in
ConceptResult.tsx to avoid a hard height by switching to a minimum height or, if
you want to keep a single row, enforce no wrapping with horizontal overflow;
keep the existing Chip rendering and onRemove behavior unchanged.
| const [dismissedMessage, setDismissedMessage] = useState<string | null | undefined>(null); | ||
| const showError = !!errorMessage && errorMessage !== dismissedMessage; | ||
|
|
||
| useEffect(() => { | ||
| if (!errorMessage || errorMessage === dismissedMessage) return; | ||
| const timeout = setTimeout(() => setDismissedMessage(errorMessage), 2500); | ||
| return () => clearTimeout(timeout); | ||
| }, [errorMessage, dismissedMessage]); | ||
|
|
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
토스트 자동 숨김과 CTA 비활성 조건이 충돌합니다.
지금 구현은 2.5초 뒤 토스트만 사라지고 버튼은 계속 !!errorMessage 때문에 비활성화됩니다. 그래서 사용자는 막힌 이유를 더 이상 볼 수 없고, 같은 문구가 다시 들어와도 dismissedMessage 때문에 토스트가 재노출되지 않습니다. errorMessage가 살아있는 동안은 토스트를 유지하거나, 반대로 토스트를 일시적으로만 보여줄 거라면 버튼 비활성 조건을 분리해야 합니다.
Also applies to: 193-194
🤖 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/features/instructor/write/ui/PaymentModal/Step1.tsx` around lines 127 -
135, The auto-dismiss logic in Step1’s toast state conflicts with the CTA
disable logic because `showError` only controls visibility while the button
still depends on `!!errorMessage`, and `dismissedMessage` can suppress
re-showing the same error. Update the `Step1` component so the error display and
CTA disabled state use consistent conditions: either keep the toast visible
whenever `errorMessage` is present, or separate the transient toast state from
the actual blocking error state used by the button. Make sure the `useEffect`
timeout and `showError`/CTA logic are aligned so the user can still understand
why the action is blocked and the same error can reappear when appropriate.
| useEffect(() => { | ||
| const node = asideRef.current; | ||
| if (!node) return; | ||
|
|
||
| const updateWidth = () => { | ||
| document.documentElement.style.setProperty("--sidebar-w", `${node.offsetWidth}px`); | ||
| }; | ||
|
|
||
| updateWidth(); | ||
| const observer = new ResizeObserver(updateWidth); | ||
| observer.observe(node); | ||
| return () => observer.disconnect(); | ||
| }, []); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
언마운트 시 --sidebar-w를 제거하지 않아 전역 변수가 남습니다.
--sidebar-w는 document.documentElement(전역)에 설정되는데, 클린업에서 observer.disconnect()만 하고 변수는 그대로 남습니다. Sidebar가 없는 페이지로 이동하면 이전 값이 유지되어, 이를 소비하는 DesignConceptSection의 left-[calc(50%+var(--sidebar-w,0)/2)]에서 0 폴백이 적용되지 않고 잘못된 오프셋이 발생합니다.
🧹 제안
updateWidth();
const observer = new ResizeObserver(updateWidth);
observer.observe(node);
- return () => observer.disconnect();
+ return () => {
+ observer.disconnect();
+ document.documentElement.style.removeProperty("--sidebar-w");
+ };📝 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.
| useEffect(() => { | |
| const node = asideRef.current; | |
| if (!node) return; | |
| const updateWidth = () => { | |
| document.documentElement.style.setProperty("--sidebar-w", `${node.offsetWidth}px`); | |
| }; | |
| updateWidth(); | |
| const observer = new ResizeObserver(updateWidth); | |
| observer.observe(node); | |
| return () => observer.disconnect(); | |
| }, []); | |
| useEffect(() => { | |
| const node = asideRef.current; | |
| if (!node) return; | |
| const updateWidth = () => { | |
| document.documentElement.style.setProperty("--sidebar-w", `${node.offsetWidth}px`); | |
| }; | |
| updateWidth(); | |
| const observer = new ResizeObserver(updateWidth); | |
| observer.observe(node); | |
| return () => { | |
| observer.disconnect(); | |
| document.documentElement.style.removeProperty("--sidebar-w"); | |
| }; | |
| }, []); |
🤖 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/Sidebar.tsx` around lines 13 - 25, The Sidebar effect leaves a
stale global CSS variable behind because only ResizeObserver cleanup runs;
update the cleanup in Sidebar’s useEffect to also clear `--sidebar-w` from
document.documentElement when the component unmounts. Keep the existing
updateWidth/ResizeObserver behavior, but ensure the teardown removes the
variable so consumers like DesignConceptSection can fall back to 0 correctly
when Sidebar is not mounted.
| <div | ||
| className={cn( | ||
| "rounded-8 bg-gray-90 text-body1-sb shadow-banner z-toast flex items-center gap-2.5 p-4 text-white transition-all duration-300", | ||
| show ? "translate-y-0 opacity-100" : "pointer-events-none -translate-y-2 opacity-0", | ||
| className, | ||
| )} | ||
| > | ||
| <ExclamationMarkCircleIcon className="size-5 shrink-0 text-white" /> | ||
| <span>{message}</span> | ||
| </div> | ||
| ); |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
스크린리더 안내를 위한 라이브 리전 속성 추가를 권장합니다.
토스트는 동적으로 노출되는 알림이므로 role="alert"(또는 role="status" + aria-live="assertive"/"polite")가 없으면 보조기술 사용자에게 메시지가 전달되지 않습니다. 또한 show=false일 때도 DOM에 남아 있어 항상 읽히지 않도록 라이브 리전 적용이 필요합니다.
♿ 제안
<div
+ role="alert"
+ aria-live="assertive"
className={cn(📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <div | |
| className={cn( | |
| "rounded-8 bg-gray-90 text-body1-sb shadow-banner z-toast flex items-center gap-2.5 p-4 text-white transition-all duration-300", | |
| show ? "translate-y-0 opacity-100" : "pointer-events-none -translate-y-2 opacity-0", | |
| className, | |
| )} | |
| > | |
| <ExclamationMarkCircleIcon className="size-5 shrink-0 text-white" /> | |
| <span>{message}</span> | |
| </div> | |
| ); | |
| <div | |
| role="alert" | |
| aria-live="assertive" | |
| className={cn( | |
| "rounded-8 bg-gray-90 text-body1-sb shadow-banner z-toast flex items-center gap-2.5 p-4 text-white transition-all duration-300", | |
| show ? "translate-y-0 opacity-100" : "pointer-events-none -translate-y-2 opacity-0", | |
| className, | |
| )} | |
| > | |
| <ExclamationMarkCircleIcon className="size-5 shrink-0 text-white" /> | |
| <span>{message}</span> | |
| </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/Toast.tsx` around lines 12 - 22, The Toast component is missing
live region semantics, so add an appropriate accessibility announcement to the
main container in Toast so screen readers are notified when the message appears.
Update the div in Toast.tsx to use role="alert" or role="status" with a matching
aria-live value, and make sure the hidden state used by show does not prevent
the announcement from being conveyed to assistive technology. Keep the change
localized to the Toast component and preserve the existing visual behavior while
adding the accessibility attributes.
| const handleValidatedFilesAdded = (files: File[]) => { | ||
| const validFiles = files.filter( | ||
| file => isAllowedFileType(file, [".png"]) && file.size <= MAX_FILE_SIZE_BYTES, | ||
| ); | ||
|
|
||
| if (validFiles.length < files.length) { | ||
| setIsInvalidFileModalOpen(true); | ||
| } | ||
|
|
||
| if (validFiles.length === 0) return; | ||
|
|
||
| if (uploadedFiles.length + validFiles.length > MAX_FILE_COUNT) { | ||
| setIsFileCountExceededModalOpen(true); | ||
| return; | ||
| } | ||
|
|
||
| handleFilesAdded(validFiles); | ||
| }; |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
무효 파일과 개수 초과가 동시에 발생하면 두 모달이 함께 열립니다.
validFiles.length < files.length로 isInvalidFileModalOpen을 켠 뒤, 같은 호출에서 개수 초과 조건이 참이면 isFileCountExceededModalOpen도 켜져 두 모달이 동시에 렌더링됩니다. 두 상황을 상호 배타적으로 처리하는 것이 좋습니다.
🛠️ 제안 수정
const handleValidatedFilesAdded = (files: File[]) => {
const validFiles = files.filter(
file => isAllowedFileType(file, [".png"]) && file.size <= MAX_FILE_SIZE_BYTES,
);
- if (validFiles.length < files.length) {
- setIsInvalidFileModalOpen(true);
- }
-
- if (validFiles.length === 0) return;
-
- if (uploadedFiles.length + validFiles.length > MAX_FILE_COUNT) {
+ if (validFiles.length < files.length) {
+ setIsInvalidFileModalOpen(true);
+ return;
+ }
+
+ if (validFiles.length === 0) return;
+
+ if (uploadedFiles.length + validFiles.length > MAX_FILE_COUNT) {
setIsFileCountExceededModalOpen(true);
return;
}
handleFilesAdded(validFiles);
};📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const handleValidatedFilesAdded = (files: File[]) => { | |
| const validFiles = files.filter( | |
| file => isAllowedFileType(file, [".png"]) && file.size <= MAX_FILE_SIZE_BYTES, | |
| ); | |
| if (validFiles.length < files.length) { | |
| setIsInvalidFileModalOpen(true); | |
| } | |
| if (validFiles.length === 0) return; | |
| if (uploadedFiles.length + validFiles.length > MAX_FILE_COUNT) { | |
| setIsFileCountExceededModalOpen(true); | |
| return; | |
| } | |
| handleFilesAdded(validFiles); | |
| }; | |
| const handleValidatedFilesAdded = (files: File[]) => { | |
| const validFiles = files.filter( | |
| file => isAllowedFileType(file, [".png"]) && file.size <= MAX_FILE_SIZE_BYTES, | |
| ); | |
| if (validFiles.length < files.length) { | |
| setIsInvalidFileModalOpen(true); | |
| return; | |
| } | |
| if (validFiles.length === 0) return; | |
| if (uploadedFiles.length + validFiles.length > MAX_FILE_COUNT) { | |
| setIsFileCountExceededModalOpen(true); | |
| return; | |
| } | |
| handleFilesAdded(validFiles); | |
| }; |
🤖 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/write/ui/AttachFileSection.tsx` around lines 25 - 42,
handleValidatedFilesAdded currently opens both isInvalidFileModalOpen and
isFileCountExceededModalOpen in the same call path, so make these cases mutually
exclusive. In AttachFileSection.tsx, update handleValidatedFilesAdded to branch
on invalid files first and return immediately after opening the invalid-file
modal, or otherwise ensure the MAX_FILE_COUNT check only runs when there are no
invalid files. Keep the existing behavior in handleFilesAdded,
isAllowedFileType, MAX_FILE_SIZE_BYTES, and MAX_FILE_COUNT, but prevent both
modals from being set true together.
| <h2 className="text-body2-m text-gray-70"> | ||
| 작업물에 필수적으로 들어갈 페이지를 선택해주세요 | ||
| 선택한 페이지에 대한 페이지 별 추가 디자인 요청사항과 원하시는 레이아웃이 있다면 | ||
| 작성해주세요. |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
문구 띄어쓰기를 수정해 주세요.
Line 32의 페이지 별은 페이지별이 맞습니다. 사용자에게 바로 보이는 문구라 이번 텍스트 정리 범위에서 같이 바로잡는 편이 좋겠습니다.
수정 예시
- 선택한 페이지에 대한 페이지 별 추가 디자인 요청사항과 원하시는 레이아웃이 있다면
+ 선택한 페이지에 대한 페이지별 추가 디자인 요청사항과 원하시는 레이아웃이 있다면📝 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.
| <h2 className="text-body2-m text-gray-70"> | |
| 작업물에 필수적으로 들어갈 페이지를 선택해주세요 | |
| 선택한 페이지에 대한 페이지 별 추가 디자인 요청사항과 원하시는 레이아웃이 있다면 | |
| 작성해주세요. | |
| <h2 className="text-body2-m text-gray-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/write/ui/NecessaryPageChooseSection.tsx` around lines
31 - 33, The user-facing copy in NecessaryPageChooseSection needs a spacing
correction: change the phrase with “페이지 별” to “페이지별” in the heading text
rendered by that component. Update the string inside the h2 in
NecessaryPageChooseSection so the visible copy is polished and consistent with
the intended Korean spacing.
KOJ50
left a comment
There was a problem hiding this comment.
새 외주 작성하기 step2에서 선택으로 넣을 수 있는 파일을 넣으면 아래 설명이 필수로 바뀌는데 설명을 입력하지 않아도 다음 스텝으로 넘어갈 수 있습니다..! 확인부탁드립니당
step1->2 , step2->3으로 넘어가는 플로우들은 API 연동하면서 한꺼번에 잡는게 편할 것 같아서 아직 진행하지 않았습니다! 🥲 |
… into feature/#40-instructor_write-refactor
📢 PR 유형
어떤 변경 사항이 있었나요?
📌 관련 이슈번호
✅ Key Changes
UT 이후 강사 새 외주 작성 부분의 UI 수정사항들을 반영했습니다.
long배리언트 추가📸 스크린샷 or 실행영상
2026-06-30.235804.mov
🎸 기타 사항 or 추가 코멘트
Summary by CodeRabbit