[fix] 스케줄 컴포넌트 UI 수정#26
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 Walkthrough📋 요약Walkthrough달력 인터페이스에 월/연도 선택기 기능을 추가하고, 주말 처리 로직을 토요일/일요일로 분리하며, 예상 노무비 추정 및 표시 기능을 도입했습니다. Changes월/연도 선택기 통합
캘린더 데이터 형태 및 표시 개선
🎯 시니어 리뷰 포커스
|
| Check name | Status | Explanation | Resolution |
|---|---|---|---|
| Docstring Coverage | Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. | Write docstrings for the functions missing them to satisfy the coverage threshold. | |
| Title check | ❓ Inconclusive | PR 제목이 변경 사항의 핵심인 드럼 피커 모달, 예상 인건비, 요일 색상 개선을 모두 포괄하지 못하고 '스케줄 컴포넌트 UI 수정'으로만 표현되어 구체성이 부족합니다. | 제목을 더욱 구체적으로 작성하세요. 예: '스케줄 캘린더 월/년 선택 피커 및 예상 인건비 표시 기능 추가' 또는 '[feat] 드럼 피커 모달 추가 및 캘린더 UI 개선' |
✅ Passed checks (3 passed)
| Check name | Status | Explanation |
|---|---|---|
| Linked Issues check | ✅ Passed | Check skipped because no linked issues were found for this pull request. |
| Out of Scope Changes check | ✅ Passed | Check skipped because no linked issues were found for this pull request. |
| Description check | ✅ Passed | PR 설명이 템플릿의 모든 필수 섹션을 완벽하게 따르고 있습니다. ID, 변경 내용, 구현 사항이 명확하고 상세하며, 구현 시연(데모 비디오)과 참고 사항까지 포함되어 있습니다. |
✏️ Tip: You can configure your own custom pre-merge checks in the settings.
✨ Finishing Touches
🧪 Generate unit tests (beta)
- Create PR with unit tests
- Commit unit tests in branch
fix/ALT-211
Tip
💬 Introducing Slack Agent: The best way for teams to turn conversations into code.
Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.
- Generate code and open pull requests
- Plan features and break down work
- Investigate incidents and troubleshoot customer tickets together
- Automate recurring tasks and respond to alerts with triggers
- Summarize progress and report instantly
Built for teams:
- Shared memory across your entire org—no repeating context
- Per-thread sandboxes to safely plan and execute work
- Governance built-in—scoped access, auditability, and budget controls
One agent for your entire SDLC. Right inside Slack.
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 @coderabbitai help to get the list of available commands and usage tips.
There was a problem hiding this comment.
Actionable comments posted: 4
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/features/home/common/schedule/hooks/useMonthlyCalendarViewModel.ts (1)
28-41:⚠️ Potential issue | 🟠 Major | ⚡ Quick win
weekStartsOn: 1→weekStartsOn: 0으로 변경 필요 — 요일 헤더와 날짜 컬럼 미정렬
WEEKDAY_LABELS는 일요일 기준(['일','월','화',...])이지만getMonthlyCells의startOfWeek/endOfWeek에서weekStartsOn: 1(월요일)을 사용하므로 그리드의 첫 컬럼(실제는 월요일)에 '일' 헤더가 표시되는 레이아웃 버그가 발생합니다. 다음과 같이 수정하세요:- const intervalStart = startOfWeek(monthStart, { weekStartsOn: 1 }) - const intervalEnd = endOfWeek(monthEnd, { weekStartsOn: 1 }) + const intervalStart = startOfWeek(monthStart, { weekStartsOn: 0 }) + const intervalEnd = endOfWeek(monthEnd, { weekStartsOn: 0 })🤖 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/home/common/schedule/hooks/useMonthlyCalendarViewModel.ts` around lines 28 - 41, The weekday header vs. grid misalignment is caused by using weekStartsOn: 1 (Monday) when computing intervalStart/intervalEnd in useMonthlyCalendarViewModel, while WEEKDAY_LABELS are Sunday-first; update the startOfWeek and endOfWeek calls that set intervalStart and intervalEnd to use weekStartsOn: 0 so eachDayOfInterval(...).map(...) (which builds dateKey/dayText/isCurrentMonth/weekDay) aligns with the Sunday-first WEEKDAY_LABELS.
🤖 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/home/common/schedule/hooks/useMonthlyDateCellsState.ts`:
- Around line 15-16: The isSaturday/isSunday checks in
useMonthlyDateCellsState.ts are reversed because date.getDay() returns
0=Sunday..6=Saturday; change the predicates so isSaturday uses cell.weekDay ===
6 and isSunday uses cell.weekDay === 0 (update the declarations of isSaturday
and isSunday that reference cell.weekDay in the useMonthlyDateCellsState hook).
In `@src/features/home/common/schedule/hooks/useMonthYearPickerViewModel.ts`:
- Around line 17-20: The years array in useMonthYearPickerViewModel is computed
from currentDate (which is MonthlyCalendar's baseDate) so the displayed year
range shifts when the user navigates to other months; change the reference to a
fixed "today" instead of currentDate—either call getYear(new Date()) (or use the
injectable/testable today value if available) when building years and remove
currentDate from that useMemo dependency so the range is always currentYear ±5;
update the code around the years constant in useMonthYearPickerViewModel.ts to
use getYear(today) (or new Date()) and adjust the dependency list accordingly.
In `@src/features/home/common/schedule/ui/MonthYearPickerModal.tsx`:
- Around line 16-99: DrumPicker currently only handles pointer events so
keyboard and assistive-tech users cannot change month/year; update DrumPicker to
provide keyboard-accessible controls by either (A) adding visible/hidden
increment/decrement <button> elements around the drum (or inside the modal)
wired to onChange (use functions handling +1/-1) with proper aria-labels,
focusable attributes and focus styling, or (B) convert the items list into an
ARIA listbox/option pattern: make the container focusable (tabIndex=0), add
role="listbox" and role="option" on items, manage aria-selected on the currently
selected item, and implement onKeyDown on the container to handle
ArrowUp/ArrowDown/PageUp/PageDown/Home/End and Enter to call
onChange(snappedIndex); keep existing pointer handlers
(handlePointerDown/Move/Up) but ensure keyboard actions update dragDelta/reset
state as needed and that buttons/options are reachable by keyboard and have
accessible names. Ensure focus management (focus stays on the listbox or moves
to buttons) and include sufficient aria labels (e.g., aria-label or
aria-labelledby) to describe the picker to screen readers.
- Around line 124-165: The modal (MonthYearPickerModal) lacks focus management
and Escape handling so keyboard focus can escape to the background; fix by
converting this to a proper dialog: when the modal opens move focus to a
sensible internal control (e.g., the first DrumPicker or the confirm button),
implement a focus trap that keeps Tab/Shift+Tab cycling within the modal, and
handle Escape key to call onClose; optionally replace the markup with the shared
Dialog component if one exists to get these behaviors. Ensure role="dialog"
remains, mark background content inert/aria-hidden while open, and wire up focus
management in the MonthYearPickerModal mount/unmount lifecycle (and ensure
onClose is used for Escape and background click).
---
Outside diff comments:
In `@src/features/home/common/schedule/hooks/useMonthlyCalendarViewModel.ts`:
- Around line 28-41: The weekday header vs. grid misalignment is caused by using
weekStartsOn: 1 (Monday) when computing intervalStart/intervalEnd in
useMonthlyCalendarViewModel, while WEEKDAY_LABELS are Sunday-first; update the
startOfWeek and endOfWeek calls that set intervalStart and intervalEnd to use
weekStartsOn: 0 so eachDayOfInterval(...).map(...) (which builds
dateKey/dayText/isCurrentMonth/weekDay) aligns with the Sunday-first
WEEKDAY_LABELS.
🪄 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: 384ee81d-0a81-462f-914a-f1532a4b7894
📒 Files selected for processing (13)
src/features/home/common/schedule/hooks/useMonthYearPickerViewModel.tssrc/features/home/common/schedule/hooks/useMonthlyCalendarViewModel.tssrc/features/home/common/schedule/hooks/useMonthlyDateCellsState.tssrc/features/home/common/schedule/types/calendarView.tssrc/features/home/common/schedule/types/monthlyCalendar.tssrc/features/home/common/schedule/ui/MonthYearPickerModal.tsxsrc/features/home/common/schedule/ui/MonthlyCalendar.tsxsrc/features/home/common/schedule/ui/MonthlyDateCell.tsxsrc/features/home/manager/hooks/useManagerHomeViewModel.tssrc/features/home/manager/hooks/useMonthlySchedulesViewModel.tssrc/features/home/user/schedule/ui/HomeScheduleCalendar.tsxsrc/pages/manager/home/index.tsxtailwind.config.js
| function DrumPicker({ items, selectedIndex, onChange }: DrumPickerProps) { | ||
| const [dragDelta, setDragDelta] = useState(0) | ||
| const [isDragging, setIsDragging] = useState(false) | ||
| const startY = useRef(0) | ||
|
|
||
| const baseTranslate = (CENTER - selectedIndex) * ITEM_HEIGHT | ||
| const rawTranslate = baseTranslate + dragDelta | ||
| const minTranslate = (CENTER - (items.length - 1)) * ITEM_HEIGHT | ||
| const maxTranslate = CENTER * ITEM_HEIGHT | ||
| const currentTranslate = Math.max( | ||
| minTranslate, | ||
| Math.min(maxTranslate, rawTranslate) | ||
| ) | ||
|
|
||
| const handlePointerDown = (e: React.PointerEvent<HTMLDivElement>) => { | ||
| setIsDragging(true) | ||
| startY.current = e.clientY | ||
| setDragDelta(0) | ||
| e.currentTarget.setPointerCapture(e.pointerId) | ||
| } | ||
|
|
||
| const handlePointerMove = (e: React.PointerEvent<HTMLDivElement>) => { | ||
| if (!isDragging) return | ||
| setDragDelta(e.clientY - startY.current) | ||
| } | ||
|
|
||
| const handlePointerUp = (e: React.PointerEvent<HTMLDivElement>) => { | ||
| if (!isDragging) return | ||
| setIsDragging(false) | ||
| const totalDelta = e.clientY - startY.current | ||
| const rawIndex = selectedIndex - totalDelta / ITEM_HEIGHT | ||
| const snapped = Math.round( | ||
| Math.max(0, Math.min(items.length - 1, rawIndex)) | ||
| ) | ||
| setDragDelta(0) | ||
| onChange(snapped) | ||
| } | ||
|
|
||
| return ( | ||
| <div | ||
| className="relative overflow-hidden select-none touch-none cursor-grab active:cursor-grabbing" | ||
| style={{ height: ITEM_HEIGHT * VISIBLE }} | ||
| onPointerDown={handlePointerDown} | ||
| onPointerMove={handlePointerMove} | ||
| onPointerUp={handlePointerUp} | ||
| onPointerCancel={handlePointerUp} | ||
| > | ||
| <div | ||
| className="pointer-events-none absolute inset-x-0 z-10 rounded-lg bg-gray-100 opacity-40" | ||
| style={{ top: CENTER * ITEM_HEIGHT, height: ITEM_HEIGHT }} | ||
| /> | ||
| <div | ||
| className="will-change-transform" | ||
| style={{ | ||
| transform: `translateY(${currentTranslate}px)`, | ||
| transition: isDragging ? 'none' : 'transform 0.2s ease-out', | ||
| }} | ||
| > | ||
| {items.map((item, i) => ( | ||
| <div | ||
| key={item} | ||
| className={cn( | ||
| 'flex items-center justify-center typography-body01-regular', | ||
| i === selectedIndex | ||
| ? 'text-text-100 font-semibold' | ||
| : 'text-text-40' | ||
| )} | ||
| style={{ height: ITEM_HEIGHT }} | ||
| > | ||
| {item} | ||
| </div> | ||
| ))} | ||
| </div> | ||
| <div | ||
| className="pointer-events-none absolute inset-x-0 top-0 z-20 bg-gradient-to-b from-white to-transparent" | ||
| style={{ height: CENTER * ITEM_HEIGHT }} | ||
| /> | ||
| <div | ||
| className="pointer-events-none absolute inset-x-0 bottom-0 z-20 bg-gradient-to-t from-white to-transparent" | ||
| style={{ height: CENTER * ITEM_HEIGHT }} | ||
| /> | ||
| </div> | ||
| ) | ||
| } |
There was a problem hiding this comment.
키보드만으로는 연/월을 변경할 수 없습니다.
DrumPicker가 포인터 이벤트만 처리하고 포커스 가능한 조작 요소도 없어서, 키보드 사용자나 보조기기 사용자는 모달을 열어도 값을 바꿀 수 없습니다. 이 플로우에서는 날짜 선택 자체가 막히므로 button 기반 증감 UI나 listbox 패턴으로 바꿔 방향키/Enter 조작을 지원해주세요. As per coding guidelines, **/*.tsx: a11y: 서비스를 막는 수준(의미 있는 버튼/폼/이미지, 키보드 트랩 등)만.
🤖 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/home/common/schedule/ui/MonthYearPickerModal.tsx` around lines
16 - 99, DrumPicker currently only handles pointer events so keyboard and
assistive-tech users cannot change month/year; update DrumPicker to provide
keyboard-accessible controls by either (A) adding visible/hidden
increment/decrement <button> elements around the drum (or inside the modal)
wired to onChange (use functions handling +1/-1) with proper aria-labels,
focusable attributes and focus styling, or (B) convert the items list into an
ARIA listbox/option pattern: make the container focusable (tabIndex=0), add
role="listbox" and role="option" on items, manage aria-selected on the currently
selected item, and implement onKeyDown on the container to handle
ArrowUp/ArrowDown/PageUp/PageDown/Home/End and Enter to call
onChange(snappedIndex); keep existing pointer handlers
(handlePointerDown/Move/Up) but ensure keyboard actions update dragDelta/reset
state as needed and that buttons/options are reachable by keyboard and have
accessible names. Ensure focus management (focus stays on the listbox or moves
to buttons) and include sufficient aria labels (e.g., aria-label or
aria-labelledby) to describe the picker to screen readers.
| return ( | ||
| <div className="fixed inset-0 z-50 flex items-center justify-center"> | ||
| <button | ||
| type="button" | ||
| className="absolute inset-0 bg-black/50" | ||
| aria-label="닫기" | ||
| onClick={onClose} | ||
| /> | ||
| <div | ||
| role="dialog" | ||
| aria-modal="true" | ||
| aria-label="날짜 선택" | ||
| className="relative w-[280px] rounded-2xl bg-white px-6 pb-5 pt-6" | ||
| > | ||
| <p className="typography-body01-semibold text-text-90 mb-4 text-center"> | ||
| 날짜 선택 | ||
| </p> | ||
| <div className="flex gap-2"> | ||
| <div className="flex-1"> | ||
| <DrumPicker | ||
| items={yearItems} | ||
| selectedIndex={yearIndex} | ||
| onChange={onYearIndexChange} | ||
| /> | ||
| </div> | ||
| <div className="flex-1"> | ||
| <DrumPicker | ||
| items={MONTH_ITEMS} | ||
| selectedIndex={monthIndex} | ||
| onChange={onMonthIndexChange} | ||
| /> | ||
| </div> | ||
| </div> | ||
| <button | ||
| type="button" | ||
| className="mt-4 w-full rounded-xl bg-main py-3 typography-body01-semibold text-white" | ||
| onClick={onConfirm} | ||
| > | ||
| 확인 | ||
| </button> | ||
| </div> | ||
| </div> |
There was a problem hiding this comment.
모달 포커스가 배경으로 새어 나갈 수 있습니다.
role="dialog"와 aria-modal만으로는 포커스가 모달 안에 머물지 않습니다. 지금 구현은 열릴 때 초기 포커스 이동도 없고 Escape 처리도 없어서, 탭 이동이 배경 UI로 빠지며 키보드 탐색이 깨집니다. 공용 Dialog를 쓰거나, 열릴 때 포커스를 이동하고 내부에 가두는 처리가 필요합니다. As per coding guidelines, **/*.tsx: a11y: 서비스를 막는 수준(의미 있는 버튼/폼/이미지, 키보드 트랩 등)만.
🤖 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/home/common/schedule/ui/MonthYearPickerModal.tsx` around lines
124 - 165, The modal (MonthYearPickerModal) lacks focus management and Escape
handling so keyboard focus can escape to the background; fix by converting this
to a proper dialog: when the modal opens move focus to a sensible internal
control (e.g., the first DrumPicker or the confirm button), implement a focus
trap that keeps Tab/Shift+Tab cycling within the modal, and handle Escape key to
call onClose; optionally replace the markup with the shared Dialog component if
one exists to get these behaviors. Ensure role="dialog" remains, mark background
content inert/aria-hidden while open, and wire up focus management in the
MonthYearPickerModal mount/unmount lifecycle (and ensure onClose is used for
Escape and background click).
ID
변경 내용
구현 사항
onClick연결,onMonthChangeprop 추가하여 날짜 선택 시 상위 컴포넌트에 변경 전달useMonthlySchedulesViewModel에서estimatedLaborCost필드를 API 응답으로부터 파싱해useMonthlyCalendarViewModel에서 텍스트로 포맷(약 N원)하여 캘린더에 노출구현 시연 (필요 시)
2026-05-06.11.13.37.mov
참고 사항 (필요 시)
Summary by CodeRabbit
릴리스 노트
새로운 기능
스타일