[Feat] WTH-255: 어드민 일정 관리 UI 구현#47
Conversation
📝 WalkthroughWalkthrough일정 관리를 위한 새로운 관리자 인터페이스 기능을 추가합니다. 여러 새로운 일정 관련 UI 컴포넌트, 날짜/시간 선택기, 생성/편집 모달, 타입 정의 및 유틸리티를 포함합니다. Changes
Sequence DiagramsequenceDiagram
participant User as 사용자
participant SchedulePage as SchedulePage
participant SchedulePageContent as SchedulePageContent
participant FilterUI as FilterUI<br/>(기수, 탭, 검색)
participant ScheduleList as ScheduleList
participant Modal as Modal<br/>(Create/Edit)
User->>SchedulePage: 일정 관리 페이지 방문
SchedulePage->>SchedulePageContent: 렌더링
SchedulePageContent->>FilterUI: 기수/탭/월/검색 상태 제공
User->>FilterUI: 필터/검색 선택
FilterUI->>SchedulePageContent: 필터 값 변경
SchedulePageContent->>SchedulePageContent: 일정 필터링 및 정렬<br/>(기수 → 월/년 → 탭 → 검색어)
SchedulePageContent->>ScheduleList: 정렬된 일정 전달
ScheduleList->>User: 필터된 일정 목록 표시
User->>ScheduleList: "생성" 버튼 클릭
ScheduleList->>Modal: CreateScheduleModal 열기
User->>Modal: 일정 입력 및 저장
Modal->>SchedulePageContent: onOpenChange(false) 콜백
User->>ScheduleList: "수정" 버튼 클릭
ScheduleList->>SchedulePageContent: 일정 선택
SchedulePageContent->>Modal: EditScheduleModal 열기
User->>Modal: 일정 수정 및 저장
Modal->>SchedulePageContent: 수정 완료 콜백
SchedulePageContent->>ScheduleList: 변경사항 반영
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 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 |
🤖 Claude 테스트 제안
변경된 컴포넌트에 대해 Claude가 생성한 테스트 코드입니다. 검토 후 적합한 부분만 사용하세요.
|
PR 테스트 결과✅ Jest: 통과 🎉 모든 테스트를 통과했습니다! |
PR 검증 결과✅ TypeScript: 통과 |
|
구현한 기능 Preview: https://weeth-239odj5fz-weethsite-4975s-projects.vercel.app |
There was a problem hiding this comment.
Actionable comments posted: 13
🧹 Nitpick comments (8)
src/components/admin/schedule/CalendarPicker.tsx (1)
92-92: Popover 그림자에 임의값 대신 디자인 토큰 클래스를 사용해 주세요.
shadow-[0px_4px_14px_0px_rgba(0,0,0,0.25)]는 토큰 기반 스타일 정책과 맞지 않습니다.As per coding guidelines "Always use design token classes first (text-, bg-, typo-, p-, gap-*) — no hardcoded values; ask before adding new tokens".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/admin/schedule/CalendarPicker.tsx` at line 92, The popover uses a hardcoded shadow utility "shadow-[0px_4px_14px_0px_rgba(0,0,0,0.25)]" inside the CalendarPicker component's className; replace that hardcoded value with the appropriate design token shadow class (e.g., the project's shadow token like shadow-elevation-XX or shadow-lg) to comply with token-based styling, or if no suitable token exists, request/add a new design token and use its class name instead; update the className on the element in CalendarPicker.tsx accordingly.src/components/alert/CustomAlertDialog.tsx (1)
50-50: 다이얼로그 크기/그림자에 하드코딩 값이 포함되어 있습니다.
w-[339px],shadow-[0px_10px_40px_0px_rgba(0,0,0,0.5)]는 토큰 기반 스타일 정책과 충돌합니다. 토큰 클래스 사용 또는 토큰 정의 후 적용이 필요합니다.As per coding guidelines "Always use design token classes first (text-, bg-, typo-, p-, gap-*) — no hardcoded values; ask before adding new tokens".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/alert/CustomAlertDialog.tsx` at line 50, The component CustomAlertDialog contains hardcoded utility classes ('w-[339px]' and 'shadow-[0px_10px_40px_0px_rgba(0,0,0,0.5)]') inside the dialog className string; replace those with design token classes (or add new tokens) and apply them instead. Locate the className string in CustomAlertDialog and swap 'w-[339px]' for the appropriate width token class (e.g., a dialog width token like w-dialog) and replace the custom shadow with a shadow token (e.g., shadow-dialog); if tokens don’t exist, add the new token definitions to the design tokens and use those token class names in the same className string. Ensure you only change the className in CustomAlertDialog and run style linting to confirm no hardcoded values remain.src/components/admin/schedule/ScheduleFormField.tsx (1)
1-18: LGTM: 재사용 가능한 폼 필드 래퍼로 잘 분리되었습니다.
label/children구조와cn기반 클래스 병합이 명확해서 모달 폼 구성에 적합합니다.As per coding guidelines, "Use
classNamemerge utility from@/lib/cn".🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/admin/schedule/ScheduleFormField.tsx` around lines 1 - 18, Ensure all class strings are merged via the cn utility: in ScheduleFormField keep the outer wrapper using cn('flex flex-col', className) and change the inner label container to use cn as well (e.g., cn('flex h-12 items-center px-400')) so all className usage consistently goes through the '@/lib/cn' merge utility; update the JSX in the ScheduleFormField function accordingly.src/components/admin/schedule/ScheduleList.tsx (1)
33-39: 고정 픽셀값(h-[150px],w-[308px])은 토큰 기반 클래스로 치환해 주세요.현재 값은 디자인 토큰 우선 정책과 충돌합니다. 기존 spacing/size 토큰 조합으로 맞추거나 필요한 경우 토큰 추가 후 사용해 주세요.
As per coding guidelines, "Always use design token classes first (text-, bg-, typo-, p-, gap-*) — no hardcoded values; ask before adding new tokens".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/admin/schedule/ScheduleList.tsx` around lines 33 - 39, The ScheduleList component uses hardcoded utility classes h-[150px] and w-[308px] (inside the div and the Button with onCreateClick) which violate the design-token-first rule; replace those fixed pixel classes with the appropriate design token classes (e.g., existing height/width tokens like h-*, w-* or spacing tokens that match the intended size) or request/add new tokens if no suitable token exists, and update the className values on the div and the Button to use those token classes instead.src/components/admin/schedule/TimePicker.tsx (1)
68-68: 임의 그림자값(shadow-[...]) 대신 토큰/프리셋 shadow 클래스를 사용해 주세요.디자인 토큰 정책을 따르려면 bracket arbitrary value는 피하는 편이 안전합니다.
As per coding guidelines, "Always use design token classes first (text-, bg-, typo-, p-, gap-*) — no hardcoded values; ask before adding new tokens".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/admin/schedule/TimePicker.tsx` at line 68, In TimePicker.tsx replace the arbitrary bracket shadow class "shadow-[0px_4px_14px_0px_rgba(0,0,0,0.25)]" in the className on the root container with the project’s shadow design token/preset (e.g., shadow-md/shadow-lg or the specific token like shadow-elevation-*) so it uses the standardized token-based shadow class; update the className string in the TimePicker component accordingly and remove the bracketed arbitrary value to comply with design token policy.src/components/admin/schedule/CreateScheduleModal.tsx (1)
65-65: 임의 픽셀값(max-w-[860px],max-h-[700px],h-[150px])은 토큰 기반 클래스로 교체해 주세요.디자인 시스템 기준으로는 arbitrary value보다 토큰/스케일 클래스 우선이 적합합니다.
As per coding guidelines, "Always use design token classes first (text-, bg-, typo-, p-, gap-*) — no hardcoded values; ask before adding new tokens".
Also applies to: 92-92, 142-142
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/admin/schedule/CreateScheduleModal.tsx` at line 65, Replace arbitrary pixel tailwind classes in CreateScheduleModal.tsx (specifically the className containing max-w-[860px] at the element tied to the modal, plus occurrences of max-h-[700px] and h-[150px] noted elsewhere) with the corresponding design-token/scale classes from our design system (use existing max-w-*, max-h-*, h-* token classes like the spacing/size scale); if no suitable token exists, request addition of a new token instead of adding an arbitrary value. Locate the affected JSX by the component name CreateScheduleModal and the className strings (the modal container and the two other elements referenced) and swap pixel-based arbitrary values for token/scale equivalents or open a token request before introducing new tokens.src/components/admin/schedule/SchedulePageContent.tsx (1)
172-185: 검색 영역 고정값(w-[492px],pl-14)은 토큰 기반으로 정리해 주세요.레이아웃 일관성과 디자인 시스템 준수를 위해 arbitrary value 대신 토큰/스케일 클래스를 우선 사용해 주세요.
As per coding guidelines, "Always use design token classes first (text-, bg-, typo-, p-, gap-*) — no hardcoded values; ask before adding new tokens".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/admin/schedule/SchedulePageContent.tsx` around lines 172 - 185, The fixed-width and spacing in the search block use arbitrary classes (w-[492px], pl-14 and left-400) instead of design tokens; in SchedulePageContent replace these with the project’s scale/token classes (e.g., use a width token class like w-<token> or p-/px- token and a left-<token> utility) so the Image and input use consistent token-based sizing and padding; update the containing div, the Image wrapper class (absolute left-400) and the input padding (pl-14) to the appropriate token equivalents while preserving the existing hooks/props (searchValue, setSearchValue, SearchIcon) and visual alignment.src/components/admin/schedule/EditScheduleModal.tsx (1)
118-123: 아이콘 버튼은 shadcn/ui 프리미티브로 통일을 권장합니다.메뉴/닫기 버튼을 raw
<button>으로 직접 스타일링하기보다, 프로젝트 공통 접근성 패턴(포커스 상태/키보드 동작)이 보장된 프리미티브로 맞추는 편이 유지보수에 유리합니다.As per coding guidelines "Use Radix UI and shadcn/ui for accessible component primitives".
Also applies to: 133-140
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/admin/schedule/EditScheduleModal.tsx` around lines 118 - 123, The raw <button> wrappers around the menu/close icons (see the icon element using Icon with AdminMeatballIcon and the similar button at lines 133-140) should be replaced with the project's shadcn/ui accessible primitive (e.g., the Button/IconButton component used across the app) so focus/keyboard behavior and a11y patterns are consistent; locate the component that renders Icon src={AdminMeatballIcon} and swap the raw button with the shadcn/ui primitive, preserving existing classNames/props (size, aria-label) and move any custom styling into the primitive's props or className.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/components/admin/schedule/CalendarPicker.tsx`:
- Around line 34-37: The viewYear and viewMonth state (initialized from
parsedDate) are only set on first render and thus fall out of sync when the
external prop value changes; add an effect that watches the value prop (or
parsedDate) and calls setViewYear(parsedDate.getFullYear()) and
setViewMonth(parsedDate.getMonth() + 1) to synchronize the calendar view
whenever value updates so prefill/reset actions update the displayed month/year.
In `@src/components/admin/schedule/CreateScheduleModal.tsx`:
- Line 63: The modal's form only resets in handleClose, so closing via outside
click/ESC (Dialog onOpenChange) skips resetForm; update the Dialog's
onOpenChange handler in CreateScheduleModal.tsx (the Dialog at the top and the
similar usage around lines ~49-52) to detect when open becomes false and call
resetForm (or call handleClose) before delegating to the passed onOpenChange
callback, ensuring the form state is cleared on all close paths; locate
resetForm and handleClose in this component and wire them into the Dialog
onOpenChange handler accordingly.
- Around line 54-60: The form currently only checks title and cardinalNumber;
add a start/end time validation so schedules with end < start are rejected by
both isValid and handleSubmit. Locate the start/end state variables (e.g.,
startTime and endTime or startDate/endDate) and update isValid to include a
check like !(endTime < startTime) (or endTime >= startTime), and add the same
guard in handleSubmit before calling handleClose (return early and surface an
error/validation message). Ensure you reference the same variables in both
handleSubmit and isValid so the UI and submit logic stay consistent.
- Line 35: The initial endTime state in CreateScheduleModal is set to '23:59',
which conflicts with the TimePicker's 5-minute step domain; change the default
in the useState call (endTime / setEndTime) to a valid value such as '23:55' (or
compute and round to the nearest 5-minute increment) so the initial selected
value matches the TimePicker options and avoids selection mismatch.
In `@src/components/admin/schedule/EditScheduleModal.tsx`:
- Around line 50-62: The content state is incorrectly initialized to '' and
hasChanges only checks content !== '', which breaks prefill and change detection
for schedules with existing descriptions; initialize content from the incoming
schedule (e.g., useState(() => schedule.description || '') or add a useEffect
that calls setContent(schedule.description || '') when the schedule prop
opens/changes) and update the hasChanges expression to compare content !==
(schedule.description || '') instead of content !== ''. Ensure you update
references to content/state initialization and the hasChanges boolean in
EditScheduleModal to use schedule.description (or the correct schedule field for
description) so existing descriptions are preserved and change detection works.
- Line 101: The className on EditScheduleModal contains arbitrary pixel token
classes (e.g., "max-w-[860px]", "max-h-[700px]", "h-[150px]") which violate the
design-token rule; update these to use existing design token classes (or agreed
new tokens) instead — locate the className string in EditScheduleModal.tsx (and
the other occurrences noted around the other className usages at the same
component locations) and replace max-w-[860px], max-h-[700px], h-[150px] (and
any other bracketed pixel classes found at the noted areas) with the equivalent
token classes (e.g., max-w-*, max-h-*, h-* tokens) or add a new design token if
no equivalent exists, ensuring you follow the project's token naming conventions
and verify with design before introducing new tokens.
In `@src/components/admin/schedule/ScheduleItem.tsx`:
- Around line 32-35: The UI can break when schedule.startDateTime fails to
parse; update the logic around getDayOfMonth, getDayLabel and
formatScheduleDateTime calls to validate schedule.startDateTime first and
provide safe fallbacks (e.g., default strings like "-" or "Invalid date") for
day, dayLabel and dateTimeText when parsing fails; ensure you centralize the
validation (check Date.isNaN or equivalent) before assigning
day/dayLabel/dateTimeText so components consuming those variables never receive
undefined or malformed values.
In `@src/components/admin/schedule/SchedulePageContent.tsx`:
- Around line 70-80: The selectedCardinalId/activeCardinal is only used for
label display and not applied to the schedule filtering pipeline; update the
pipeline that builds monthFiltered (and the subsequent tab/search filters around
the code handling lines 86-93) to also filter schedules by cardinal when
selected by adding a condition like s.cardinalId === selectedCardinalId (or
include all when selectedCardinalId is undefined), so the final displayed list
is filtered by currentYear/currentMonth plus selectedCardinalId and the existing
tab/search criteria; reference selectedCardinalId, activeCardinal, schedules,
monthFiltered, currentYear, currentMonth and the later filter block to locate
where to add this check.
- Around line 117-119: The delete handler (handleDelete) is currently a no-op so
clicking delete doesn't remove items; implement it to perform an optimistic
local update by removing the passed Schedule from the component state (update
the schedules array via setSchedules or equivalent) and close any confirmation
dialog, then call the real delete API (deleteSchedule) or a mock delete function
and handle errors by reverting the state if the API fails; also apply the same
behavior to the other delete spots referenced (the other delete
handlers/confirmation locations) so the UI reflects deletion immediately even
before the backend completes.
In `@src/components/admin/schedule/TimePicker.tsx`:
- Around line 29-35: The current parsing of value in TimePicker (const [h, m] =
value ? value.split(':').map(Number) : [0, 0]) accepts minute values like 59 and
can desync the UI; update the parsing to normalize minutes to the nearest
5-minute step (or fallback to 0 if invalid) before using them in state and
before calling handleSelect/onChange, and apply the same normalization logic to
the other similar block around the handleSelect usage (lines showing minute list
handling) so displayed minute selection always matches the 5-minute increments
used by handleSelect.
In `@src/hooks/mutations/admin/useAdminScheduleMutations.ts`:
- Line 13: The mutation functions in useAdminScheduleMutations.ts call
adminScheduleApi.createSchedule / adminScheduleApi.deleteSchedule with a
non-guarded clubId (using clubId!), which can be undefined if useClubId() is not
ready; update the mutationFn and the corresponding delete mutation to explicitly
guard clubId (from useClubId) before calling adminScheduleApi: check if clubId
is present and if not reject/throw or return a failed Promise with a clear error
message so the mutation never invokes the API with undefined clubId; locate the
mutationFn and delete mutation references to adminScheduleApi.createSchedule and
adminScheduleApi.deleteSchedule and add the guard there.
In `@src/hooks/queries/admin/useAdminScheduleQueries.ts`:
- Around line 11-15: The current queryFn calls
adminScheduleApi.getSchedules(clubId!, cardinalId!) using non-null assertions
and relies only on enabled, which is unsafe because TanStack Query v5 may call
queryFn even when enabled is false; modify the queryFn in
useAdminScheduleQueries to explicitly validate clubId and cardinalId at the
start (e.g., check for null/undefined and throw a clear error or return a
rejected promise) before calling adminScheduleApi.getSchedules, so you never
pass invalid values to adminScheduleApi.getSchedules and the function fails fast
with a descriptive message.
In `@src/utils/admin/scheduleUtils.ts`:
- Around line 3-23: The three functions getDayLabel, getDayOfMonth and
formatScheduleDateTime currently call new Date(dateString) directly which can
mis-interpret timezone-less ISO strings and produce Invalid Date values; update
them to (1) parse the input robustly by detecting timezone-less ISO strings and
appending 'Z' (or otherwise normalizing to UTC) before creating the Date object,
(2) validate the resulting Date (isNaN(date.getTime())) and handle invalid input
by returning a safe fallback (e.g., empty string or null/placeholder) instead of
calling getDay/getDate/getHours on an invalid date, and (3) ensure consistent
use of UTC or local getters (choose getUTC* if you normalize to UTC) so
getDayLabel, getDayOfMonth and formatScheduleDateTime all produce consistent,
timezone-safe output.
---
Nitpick comments:
In `@src/components/admin/schedule/CalendarPicker.tsx`:
- Line 92: The popover uses a hardcoded shadow utility
"shadow-[0px_4px_14px_0px_rgba(0,0,0,0.25)]" inside the CalendarPicker
component's className; replace that hardcoded value with the appropriate design
token shadow class (e.g., the project's shadow token like shadow-elevation-XX or
shadow-lg) to comply with token-based styling, or if no suitable token exists,
request/add a new design token and use its class name instead; update the
className on the element in CalendarPicker.tsx accordingly.
In `@src/components/admin/schedule/CreateScheduleModal.tsx`:
- Line 65: Replace arbitrary pixel tailwind classes in CreateScheduleModal.tsx
(specifically the className containing max-w-[860px] at the element tied to the
modal, plus occurrences of max-h-[700px] and h-[150px] noted elsewhere) with the
corresponding design-token/scale classes from our design system (use existing
max-w-*, max-h-*, h-* token classes like the spacing/size scale); if no suitable
token exists, request addition of a new token instead of adding an arbitrary
value. Locate the affected JSX by the component name CreateScheduleModal and the
className strings (the modal container and the two other elements referenced)
and swap pixel-based arbitrary values for token/scale equivalents or open a
token request before introducing new tokens.
In `@src/components/admin/schedule/EditScheduleModal.tsx`:
- Around line 118-123: The raw <button> wrappers around the menu/close icons
(see the icon element using Icon with AdminMeatballIcon and the similar button
at lines 133-140) should be replaced with the project's shadcn/ui accessible
primitive (e.g., the Button/IconButton component used across the app) so
focus/keyboard behavior and a11y patterns are consistent; locate the component
that renders Icon src={AdminMeatballIcon} and swap the raw button with the
shadcn/ui primitive, preserving existing classNames/props (size, aria-label) and
move any custom styling into the primitive's props or className.
In `@src/components/admin/schedule/ScheduleFormField.tsx`:
- Around line 1-18: Ensure all class strings are merged via the cn utility: in
ScheduleFormField keep the outer wrapper using cn('flex flex-col', className)
and change the inner label container to use cn as well (e.g., cn('flex h-12
items-center px-400')) so all className usage consistently goes through the
'@/lib/cn' merge utility; update the JSX in the ScheduleFormField function
accordingly.
In `@src/components/admin/schedule/ScheduleList.tsx`:
- Around line 33-39: The ScheduleList component uses hardcoded utility classes
h-[150px] and w-[308px] (inside the div and the Button with onCreateClick) which
violate the design-token-first rule; replace those fixed pixel classes with the
appropriate design token classes (e.g., existing height/width tokens like h-*,
w-* or spacing tokens that match the intended size) or request/add new tokens if
no suitable token exists, and update the className values on the div and the
Button to use those token classes instead.
In `@src/components/admin/schedule/SchedulePageContent.tsx`:
- Around line 172-185: The fixed-width and spacing in the search block use
arbitrary classes (w-[492px], pl-14 and left-400) instead of design tokens; in
SchedulePageContent replace these with the project’s scale/token classes (e.g.,
use a width token class like w-<token> or p-/px- token and a left-<token>
utility) so the Image and input use consistent token-based sizing and padding;
update the containing div, the Image wrapper class (absolute left-400) and the
input padding (pl-14) to the appropriate token equivalents while preserving the
existing hooks/props (searchValue, setSearchValue, SearchIcon) and visual
alignment.
In `@src/components/admin/schedule/TimePicker.tsx`:
- Line 68: In TimePicker.tsx replace the arbitrary bracket shadow class
"shadow-[0px_4px_14px_0px_rgba(0,0,0,0.25)]" in the className on the root
container with the project’s shadow design token/preset (e.g.,
shadow-md/shadow-lg or the specific token like shadow-elevation-*) so it uses
the standardized token-based shadow class; update the className string in the
TimePicker component accordingly and remove the bracketed arbitrary value to
comply with design token policy.
In `@src/components/alert/CustomAlertDialog.tsx`:
- Line 50: The component CustomAlertDialog contains hardcoded utility classes
('w-[339px]' and 'shadow-[0px_10px_40px_0px_rgba(0,0,0,0.5)]') inside the dialog
className string; replace those with design token classes (or add new tokens)
and apply them instead. Locate the className string in CustomAlertDialog and
swap 'w-[339px]' for the appropriate width token class (e.g., a dialog width
token like w-dialog) and replace the custom shadow with a shadow token (e.g.,
shadow-dialog); if tokens don’t exist, add the new token definitions to the
design tokens and use those token class names in the same className string.
Ensure you only change the className in CustomAlertDialog and run style linting
to confirm no hardcoded values remain.
🪄 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: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: d8884bdd-937d-4193-ae67-a51f690df609
⛔ Files ignored due to path filters (3)
src/assets/icons/admin/ic_admin_calendar_edit.svgis excluded by!**/*.svgsrc/assets/icons/admin/ic_admin_square_left.svgis excluded by!**/*.svgsrc/assets/icons/admin/ic_admin_square_right.svgis excluded by!**/*.svg
📒 Files selected for processing (26)
src/app/(private)/admin/schedule/page.tsxsrc/assets/icons/admin/index.tssrc/components/admin/index.tssrc/components/admin/layout/Header.tsxsrc/components/admin/schedule/CalendarPicker.tsxsrc/components/admin/schedule/CreateScheduleModal.tsxsrc/components/admin/schedule/DateTimeInput.tsxsrc/components/admin/schedule/EditScheduleModal.tsxsrc/components/admin/schedule/MonthNavigator.tsxsrc/components/admin/schedule/ScheduleFormField.tsxsrc/components/admin/schedule/ScheduleItem.tsxsrc/components/admin/schedule/ScheduleList.tsxsrc/components/admin/schedule/SchedulePageContent.tsxsrc/components/admin/schedule/ScheduleTag.tsxsrc/components/admin/schedule/TimePicker.tsxsrc/components/alert/CustomAlertDialog.tsxsrc/components/alert/index.tssrc/components/ui/Button.tsxsrc/hooks/mutations/admin/index.tssrc/hooks/mutations/admin/useAdminScheduleMutations.tssrc/hooks/queries/admin/index.tssrc/hooks/queries/admin/useAdminScheduleQueries.tssrc/lib/apis/adminSchedule.tssrc/lib/apis/index.tssrc/types/admin/schedule.d.tssrc/utils/admin/scheduleUtils.ts
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
PR 테스트 결과✅ Jest: 통과 🎉 모든 테스트를 통과했습니다! |
🤖 Claude 테스트 제안
변경된 컴포넌트에 대해 Claude가 생성한 테스트 코드입니다. 검토 후 적합한 부분만 사용하세요.
|
PR 검증 결과✅ TypeScript: 통과 |
|
구현한 기능 Preview: https://weeth-2pzjxmsh9-weethsite-4975s-projects.vercel.app |
- adminSchedule API, query/mutation hooks, barrel export 변경 되돌림 - CreateScheduleBody 타입 제거 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- CalendarPicker, TimePicker, DateTimeInput → components/ui/ 이동 - CreateScheduleModal, EditScheduleModal → admin/schedule/modal/ 이동 - barrel export 경로 업데이트 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
🤖 Claude 테스트 제안
변경된 컴포넌트에 대해 Claude가 생성한 테스트 코드입니다. 검토 후 적합한 부분만 사용하세요.
|
PR 테스트 결과✅ Jest: 통과 🎉 모든 테스트를 통과했습니다! |
PR 검증 결과✅ TypeScript: 통과 |
|
구현한 기능 Preview: https://weeth-lopxuipqd-weethsite-4975s-projects.vercel.app |
| return ( | ||
| <div className={cn('flex flex-col', className)} {...props}> | ||
| <div className="flex h-12 items-center px-400"> | ||
| <span className="typo-sub2 text-text-normal">{label}</span> |
There was a problem hiding this comment.
<span>으로 label 텍스트를 표시해도 큰 문제는 없지만,, <label> 태그로 가져오면 스크린 리더가 읽을 수 잇어 접근성 측면에서 더 좋다고 합니당!
dalzzy
left a comment
There was a problem hiding this comment.
수고하셨습니다~!!! 👍🏻 열일하는 직장인,,,, 파이팅..
🤖 Claude 테스트 제안
변경된 컴포넌트에 대해 Claude가 생성한 테스트 코드입니다. 검토 후 적합한 부분만 사용하세요.
|
PR 테스트 결과✅ Jest: 통과 🎉 모든 테스트를 통과했습니다! |
PR 검증 결과✅ TypeScript: 통과 🎉 모든 검증을 통과했습니다! |
|
구현한 기능 Preview: https://weeth-hny4dgade-weethsite-4975s-projects.vercel.app |
There was a problem hiding this comment.
Actionable comments posted: 3
♻️ Duplicate comments (2)
src/components/admin/schedule/SchedulePageContent.tsx (1)
122-124:⚠️ Potential issue | 🟠 Major삭제 핸들러가 비어 있어 실제 삭제가 반영되지 않습니다.
ScheduleList와EditScheduleModal둘 다 이 핸들러를 호출하지만, 현재는 어떤 상태 변경도 일어나지 않아 사용자는 삭제가 된 것처럼 보였다가 목록이 그대로 남습니다. mock 단계여도 로컬 목록에서 즉시 제거하거나, 실제 mutation 성공 후에만 닫히도록 연결이 필요합니다.수정 예시
- const schedules = MOCK_SCHEDULES; + const [schedules, setSchedules] = useState<Schedule[]>(MOCK_SCHEDULES); - const handleDelete = (_schedule: Schedule) => { - // TODO: API 연동 시 deleteSchedule(_schedule) 호출 - }; + const handleDelete = (schedule: Schedule) => { + setSchedules((prev) => prev.filter((item) => item.scheduleId !== schedule.scheduleId)); + // TODO: API 연동 시 deleteSchedule(schedule) 호출 + };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/admin/schedule/SchedulePageContent.tsx` around lines 122 - 124, The handleDelete function is currently a no-op so deletions never propagate; implement it to call the deletion flow (call the existing deleteSchedule API/mutation if available or a mock delete) and upon success update the local schedules state used by ScheduleList and EditScheduleModal by filtering out the deleted Schedule (use its id), ensure the EditScheduleModal is closed only after successful deletion, and add basic error handling/logging to revert UI or show an error if the API fails; locate and change the handleDelete function referenced by ScheduleList and EditScheduleModal and update the component's schedules state updater accordingly.src/components/admin/schedule/modal/EditScheduleModal.tsx (1)
33-42:⚠️ Potential issue | 🟠 Major설명 필드가 항상 빈 값으로 초기화되어 기존 설명을 잃습니다.
toInitialForm()이content를''로 고정해서 수정 모달을 열 때마다 설명 프리필이 깨지고, 변경 감지도 설명 필드에서는 항상 왜곡됩니다. 게다가 현재src/types/admin/schedule.d.ts:1-11의Schedule에도 설명 필드가 없어 이 모달이 기존 설명을 복원할 방법이 없습니다.수정 방향 예시
function toInitialForm(schedule: Schedule): ScheduleFormState { return { title: schedule.title, startDate: schedule.startDateTime.slice(0, 10), startTime: schedule.startDateTime.slice(11, 16), endDate: schedule.endDateTime.slice(0, 10), endTime: schedule.endDateTime.slice(11, 16), location: schedule.location, - content: '', + content: schedule.description ?? '', }; }// src/types/admin/schedule.d.ts export interface Schedule { scheduleId: number; title: string; type: ScheduleType; startDateTime: string; endDateTime: string; location: string; cardinalNumber: number; description?: string; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/admin/schedule/modal/EditScheduleModal.tsx` around lines 33 - 42, The toInitialForm function wipes existing descriptions by setting content to '' and the Schedule type lacks a description field; update the Schedule interface to include an optional description?: string and modify toInitialForm (the function named toInitialForm) to set content = schedule.description ?? '' so the edit modal pre-fills the existing description while still defaulting to empty when absent.
🧹 Nitpick comments (4)
src/components/ui/TimePicker.tsx (1)
10-13: UI 컴포넌트에classNameprop이 노출되지 않았습니다.
CalendarPicker와 동일하게classNameprop을 노출하여 외부에서 스타일을 확장할 수 있도록 해야 합니다.♻️ className 지원 추가
interface TimePickerProps { value: string; onChange: (value: string) => void; + className?: string; }그리고 trigger 버튼에
cn()으로 병합:-function TimePicker({ value, onChange }: TimePickerProps) { +function TimePicker({ value, onChange, className }: TimePickerProps) { // ... <button type="button" - className="bg-container-neutral data-[state=open]:border-brand-primary ..." + className={cn("bg-container-neutral data-[state=open]:border-brand-primary ...", className)}As per coding guidelines: "Always expose
classNameprop in component Props" forsrc/components/ui/**/*.{ts,tsx}.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/ui/TimePicker.tsx` around lines 10 - 13, TimePickerProps currently lacks a className prop so external styles can't be applied; add className?: string to the TimePickerProps interface and accept it in the TimePicker component props, then pass it into the top-level container (or trigger button) by merging with existing classes using the cn(...) utility (same approach as CalendarPicker) so external className values are preserved and combined.src/utils/shared/date.ts (1)
64-68:formatDateDisplay가 월/일의 앞자리 0을 제거하지 않습니다.함수 목적이
"YYYY. M. D."형식이라면, 입력"2026-04-05"는"2026. 4. 5."로 출력되어야 하지만 현재는"2026. 04. 05."를 반환합니다.♻️ 앞자리 0 제거가 필요한 경우
export function formatDateDisplay(dateStr: string): string { if (!dateStr) return ''; const [year, month, day] = dateStr.split('-'); - return `${year}. ${month}. ${day}.`; + return `${year}. ${Number(month)}. ${Number(day)}.`; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/utils/shared/date.ts` around lines 64 - 68, The function formatDateDisplay currently preserves leading zeros in month/day; update it to remove them by converting month and day to numbers (e.g., using parseInt or Number) or by stripping a leading '0' before formatting so "2026-04-05" becomes "2026. 4. 5."; modify the logic inside formatDateDisplay (referenced by its name) to coerce or trim month and day values prior to building the return string while keeping the existing YYYY. M. D. output structure.src/components/ui/CalendarPicker.tsx (1)
12-15: UI 컴포넌트에classNameprop이 노출되지 않았습니다.
src/components/ui/내의 컴포넌트는classNameprop을 노출해야 합니다.♻️ className 지원 추가
interface CalendarPickerProps { value: string; onChange: (value: string) => void; + className?: string; } -function CalendarPicker({ value, onChange }: CalendarPickerProps) { +function CalendarPicker({ value, onChange, className }: CalendarPickerProps) {그리고 trigger 버튼에 적용:
<button type="button" - className="bg-container-neutral data-[state=open]:border-brand-primary ..." + className={cn("bg-container-neutral data-[state=open]:border-brand-primary ...", className)}As per coding guidelines: "Always expose
classNameprop in component Props" forsrc/components/ui/**/*.{ts,tsx}.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/ui/CalendarPicker.tsx` around lines 12 - 15, The CalendarPicker component props (CalendarPickerProps) currently omit className; update the CalendarPickerProps interface to accept an optional className?: string and thread that prop into the CalendarPicker component so consumers can pass classes; also apply the className to the root element (or the component's outer container) and ensure the trigger button inside CalendarPicker (the trigger element) receives any relevant classes or composes them (e.g., merge with existing classes) so styling can be applied from outside.src/components/admin/schedule/SchedulePageContent.tsx (1)
177-177:w-[492px]는 현재 토큰 규칙을 우회합니다.이 검색 영역 폭은 임의 픽셀값 대신 기존 사이즈 토큰으로 표현하는 편이 맞습니다. 대응 토큰이 없다면 여기서 arbitrary value를 늘리기보다 토큰 추가 합의를 먼저 하는 편이 안전합니다.
As per coding guidelines "Always use design token classes first (text-, bg-, typo-, p-, gap-*) — no hardcoded values; ask before adding new tokens".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/admin/schedule/SchedulePageContent.tsx` at line 177, The div in SchedulePageContent.tsx currently uses an arbitrary width class "w-[492px]" which violates the token-first rule; replace this hardcoded width by using an existing design token width class (e.g., a w- token from your size scale) for the search area or, if no suitable token exists, open a token addition RFC/PR and use a new token class (e.g., w-{new-token}) once agreed; update the JSX className on the div (the element with className containing "relative w-[492px]") to the tokenized width and remove the arbitrary bracketed value.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/components/admin/schedule/modal/CreateScheduleModal.tsx`:
- Line 35: resetForm currently calls setEndTime('23:59'), which conflicts with
the TimePicker's 5-minute step and the component's initial endTime value of
'23:55'; update resetForm to use '23:55' instead (or better, reference a single
DEFAULT_END_TIME constant used by the initial state) so resetForm, the initial
state, and the TimePicker step are consistent (refer to resetForm and setEndTime
in CreateScheduleModal).
- Line 51: isValid currently uses startDate < endDate which can fail to consider
time components (or compare non-Date values); update the validation to compare
timestamps instead by using startDate.getTime() < endDate.getTime() (or, if
startDate/endDate may be strings, first parse them to Date and then compare
.getTime()), keeping the other checks (title.trim() and cardinalNumber !== null)
and referencing the isValid expression and the startDate/endDate/cardinalNumber
variables.
In `@src/components/admin/schedule/modal/EditScheduleModal.tsx`:
- Around line 78-82: handleSubmit currently only validates title and calls
handleClose, so changes never propagate; update it to call the parent save
handler or API mutation (e.g., an onSave prop or updateSchedule mutation) with
the current form state, await the result, and only call handleClose on success;
also surface errors (keep modal open on failure) and toggle a local saving flag
(e.g., isSaving) to disable the save button while the request is in flight;
reference the handleSubmit function, handleClose, the form state (form.title and
other fields), and the parent onSave/updateSchedule mutation to locate the
changes.
---
Duplicate comments:
In `@src/components/admin/schedule/modal/EditScheduleModal.tsx`:
- Around line 33-42: The toInitialForm function wipes existing descriptions by
setting content to '' and the Schedule type lacks a description field; update
the Schedule interface to include an optional description?: string and modify
toInitialForm (the function named toInitialForm) to set content =
schedule.description ?? '' so the edit modal pre-fills the existing description
while still defaulting to empty when absent.
In `@src/components/admin/schedule/SchedulePageContent.tsx`:
- Around line 122-124: The handleDelete function is currently a no-op so
deletions never propagate; implement it to call the deletion flow (call the
existing deleteSchedule API/mutation if available or a mock delete) and upon
success update the local schedules state used by ScheduleList and
EditScheduleModal by filtering out the deleted Schedule (use its id), ensure the
EditScheduleModal is closed only after successful deletion, and add basic error
handling/logging to revert UI or show an error if the API fails; locate and
change the handleDelete function referenced by ScheduleList and
EditScheduleModal and update the component's schedules state updater
accordingly.
---
Nitpick comments:
In `@src/components/admin/schedule/SchedulePageContent.tsx`:
- Line 177: The div in SchedulePageContent.tsx currently uses an arbitrary width
class "w-[492px]" which violates the token-first rule; replace this hardcoded
width by using an existing design token width class (e.g., a w- token from your
size scale) for the search area or, if no suitable token exists, open a token
addition RFC/PR and use a new token class (e.g., w-{new-token}) once agreed;
update the JSX className on the div (the element with className containing
"relative w-[492px]") to the tokenized width and remove the arbitrary bracketed
value.
In `@src/components/ui/CalendarPicker.tsx`:
- Around line 12-15: The CalendarPicker component props (CalendarPickerProps)
currently omit className; update the CalendarPickerProps interface to accept an
optional className?: string and thread that prop into the CalendarPicker
component so consumers can pass classes; also apply the className to the root
element (or the component's outer container) and ensure the trigger button
inside CalendarPicker (the trigger element) receives any relevant classes or
composes them (e.g., merge with existing classes) so styling can be applied from
outside.
In `@src/components/ui/TimePicker.tsx`:
- Around line 10-13: TimePickerProps currently lacks a className prop so
external styles can't be applied; add className?: string to the TimePickerProps
interface and accept it in the TimePicker component props, then pass it into the
top-level container (or trigger button) by merging with existing classes using
the cn(...) utility (same approach as CalendarPicker) so external className
values are preserved and combined.
In `@src/utils/shared/date.ts`:
- Around line 64-68: The function formatDateDisplay currently preserves leading
zeros in month/day; update it to remove them by converting month and day to
numbers (e.g., using parseInt or Number) or by stripping a leading '0' before
formatting so "2026-04-05" becomes "2026. 4. 5."; modify the logic inside
formatDateDisplay (referenced by its name) to coerce or trim month and day
values prior to building the return string while keeping the existing YYYY. M.
D. output structure.
🪄 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: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 20cc85d8-27b3-4a76-9e24-b18a9a510305
📒 Files selected for processing (19)
src/components/admin/index.tssrc/components/admin/schedule/ScheduleFormField.tsxsrc/components/admin/schedule/ScheduleItem.tsxsrc/components/admin/schedule/ScheduleItem.tsx.__tmpsrc/components/admin/schedule/ScheduleList.tsxsrc/components/admin/schedule/SchedulePageContent.tsxsrc/components/admin/schedule/ScheduleTag.tsxsrc/components/admin/schedule/modal/CreateScheduleModal.tsxsrc/components/admin/schedule/modal/EditScheduleModal.tsxsrc/components/alert/CustomAlertDialog.tsxsrc/components/ui/CalendarPicker.tsxsrc/components/ui/DateTimeInput.tsxsrc/components/ui/TimePicker.tsxsrc/components/ui/index.tssrc/constants/admin/schedule.constants.tssrc/constants/shared/date.tssrc/types/admin/schedule.d.tssrc/utils/admin/scheduleUtils.tssrc/utils/shared/date.ts
✅ Files skipped from review due to trivial changes (2)
- src/components/ui/index.ts
- src/types/admin/schedule.d.ts
🚧 Files skipped from review as they are similar to previous changes (6)
- src/components/admin/schedule/ScheduleFormField.tsx
- src/components/admin/schedule/ScheduleItem.tsx
- src/components/admin/index.ts
- src/utils/admin/scheduleUtils.ts
- src/components/admin/schedule/ScheduleList.tsx
- src/components/alert/CustomAlertDialog.tsx
| setStartDate(toDateInputValue()); | ||
| setStartTime('00:00'); | ||
| setEndDate(toDateInputValue()); | ||
| setEndTime('23:59'); |
There was a problem hiding this comment.
resetForm에서 endTime이 '23:59'로 설정되어 TimePicker 5분 단위 도메인과 충돌합니다.
초기값(line 26)은 '23:55'로 올바르게 설정되어 있지만, resetForm에서는 '23:59'로 리셋됩니다. 일관성을 위해 동일한 값을 사용해야 합니다.
🔧 수정 제안
const resetForm = () => {
setTitle('');
setStartDate(toDateInputValue());
setStartTime('00:00');
setEndDate(toDateInputValue());
- setEndTime('23:59');
+ setEndTime('23:55');
setLocation('');
setContent('');
};📝 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.
| setEndTime('23:59'); | |
| const resetForm = () => { | |
| setTitle(''); | |
| setStartDate(toDateInputValue()); | |
| setStartTime('00:00'); | |
| setEndDate(toDateInputValue()); | |
| setEndTime('23:55'); | |
| setLocation(''); | |
| setContent(''); | |
| }; |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/admin/schedule/modal/CreateScheduleModal.tsx` at line 35,
resetForm currently calls setEndTime('23:59'), which conflicts with the
TimePicker's 5-minute step and the component's initial endTime value of '23:55';
update resetForm to use '23:55' instead (or better, reference a single
DEFAULT_END_TIME constant used by the initial state) so resetForm, the initial
state, and the TimePicker step are consistent (refer to resetForm and setEndTime
in CreateScheduleModal).
| handleClose(); | ||
| }; | ||
|
|
||
| const isValid = title.trim().length > 0 && startDate < endDate && cardinalNumber !== null; |
There was a problem hiding this comment.
같은 날짜의 일정을 생성할 수 없습니다.
현재 startDate < endDate 비교는 같은 날짜인 경우(예: 오전 10시~오후 2시) 항상 false를 반환합니다. 시간을 포함한 비교가 필요합니다.
🔧 수정 제안
- const isValid = title.trim().length > 0 && startDate < endDate && cardinalNumber !== null;
+ const isValid = (() => {
+ if (!title.trim() || cardinalNumber === null) return false;
+ const start = new Date(`${startDate}T${startTime}`);
+ const end = new Date(`${endDate}T${endTime}`);
+ return start < end;
+ })();📝 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 isValid = title.trim().length > 0 && startDate < endDate && cardinalNumber !== null; | |
| const isValid = (() => { | |
| if (!title.trim() || cardinalNumber === null) return false; | |
| const start = new Date(`${startDate}T${startTime}`); | |
| const end = new Date(`${endDate}T${endTime}`); | |
| return start < end; | |
| })(); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/admin/schedule/modal/CreateScheduleModal.tsx` at line 51,
isValid currently uses startDate < endDate which can fail to consider time
components (or compare non-Date values); update the validation to compare
timestamps instead by using startDate.getTime() < endDate.getTime() (or, if
startDate/endDate may be strings, first parse them to Date and then compare
.getTime()), keeping the other checks (title.trim() and cardinalNumber !== null)
and referencing the isValid expression and the startDate/endDate/cardinalNumber
variables.
| const handleSubmit = () => { | ||
| if (!form.title.trim()) return; | ||
| // TODO: API 연동 시 수정 요청 | ||
| handleClose(); | ||
| }; |
There was a problem hiding this comment.
저장이 수정값을 반영하지 않고 모달만 닫습니다.
현재 handleSubmit()은 제목만 확인한 뒤 바로 닫기 때문에, 사용자가 바꾼 값이 부모 목록이나 서버 어디에도 반영되지 않습니다. 결과적으로 “저장”이 실제로는 discard와 거의 같은 동작이 됩니다. 최소한 onSave/mutation 성공 이후에만 닫히도록 연결이 필요합니다.
수정 방향 예시
interface EditScheduleModalProps {
open: boolean;
onOpenChange: (open: boolean) => void;
schedule: Schedule;
+ onSave: (schedule: Schedule, form: ScheduleFormState) => Promise<void> | void;
onDelete?: (schedule: Schedule) => void;
}
-function EditScheduleModal({ open, onOpenChange, schedule, onDelete }: EditScheduleModalProps) {
+function EditScheduleModal({ open, onOpenChange, schedule, onSave, onDelete }: EditScheduleModalProps) {
const initialForm = toInitialForm(schedule);
const [form, setForm] = useState<ScheduleFormState>(initialForm);
const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false);
const [discardSource, setDiscardSource] = useState<'close' | 'cancel' | null>(null);
- const handleSubmit = () => {
+ const handleSubmit = async () => {
if (!form.title.trim()) return;
- // TODO: API 연동 시 수정 요청
+ await onSave(schedule, form);
handleClose();
};🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/admin/schedule/modal/EditScheduleModal.tsx` around lines 78 -
82, handleSubmit currently only validates title and calls handleClose, so
changes never propagate; update it to call the parent save handler or API
mutation (e.g., an onSave prop or updateSchedule mutation) with the current form
state, await the result, and only call handleClose on success; also surface
errors (keep modal open on failure) and toggle a local saving flag (e.g.,
isSaving) to disable the save button while the request is in flight; reference
the handleSubmit function, handleClose, the form state (form.title and other
fields), and the parent onSave/updateSchedule mutation to locate the changes.
✅ PR 유형
어떤 변경 사항이 있었나요?
📌 관련 이슈번호
✅ Key Changes
어드민 일정 관리
center,top-right,bottom-right등) 커스텀 확인 다이얼로그기반 코드
📸 스크린샷 or 실행영상
🎸 기타 사항 or 추가 코멘트
CustomAlertDialog는src/components/alert/에 별도로 생성했으며,positionprop으로 뜨는 위치를 지정할 수 있습니다.작업사항이 커질 거 같애서 일정이랑 세션은 나눠서 작업하겠습니다!
Summary by CodeRabbit
릴리스 노트
New Features
UI Enhancement