feat: web-11 캘린더 일정 조회 페이지#18
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces an administrative calendar and schedule management system, adding the AdminSchedulePage along with components like AdminCalendarView, AllScheduleView, and associated dropdowns, checkboxes, and tabs. The review feedback highlights several critical bugs and usability improvements: using schedule.id as a unique key in AllScheduleView to prevent selection and duplicate key issues, adding a "Cancel" button for editing and adding schedules, keying AdminCalendarView with the current year and month to reset selection states on navigation, extending the year options to allow planning for the next year, removing redundant Date instantiations in utility functions, and replacing a hardcoded width with w-full in the dropdown list.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| function getScheduleKey(schedule) { | ||
| return `${schedule.startDate}-${schedule.endDate}-${schedule.title}`; | ||
| } |
There was a problem hiding this comment.
getScheduleKey 함수에서 startDate, endDate, title 조합으로 키를 생성하고 있습니다. 만약 동일한 날짜에 동일한 제목을 가진 일정이 중복 등록될 경우, React key 중복 경고가 발생할 뿐만 아니라 체크박스 선택 및 삭제 시 중복된 모든 일정이 함께 선택되거나 삭제되는 버그가 발생할 수 있습니다.\n\nlocalSchedules 생성 시 고유한 id를 부여하고 있으므로, schedule.id가 존재한다면 이를 우선적으로 키로 사용하도록 개선하는 것이 안전합니다.
| function getScheduleKey(schedule) { | |
| return `${schedule.startDate}-${schedule.endDate}-${schedule.title}`; | |
| } | |
| function getScheduleKey(schedule) { | |
| return schedule.id || `${schedule.startDate}-${schedule.endDate}-${schedule.title}`; | |
| } |
| {/* 수정/완료 버튼: shrink-0 으로 항상 하단 고정 */} | ||
| <div className="flex shrink-0 justify-end pt-8"> | ||
| <button | ||
| type="button" | ||
| onClick={handleConfirm} | ||
| className="h-[32px] w-[96px] rounded-[6px] bg-brand px-[14px] py-[6px] text-[14px] text-white transition-opacity hover:opacity-80" | ||
| > | ||
| {isEditMode || isAddingSchedule ? '완료' : '수정'} | ||
| </button> | ||
| </div> |
There was a problem hiding this comment.
일정 수정 모드(isEditMode) 또는 일정 추가 모드(isAddingSchedule)일 때, 사용자가 작업을 취소하고 이전 상태로 되돌릴 수 있는 "취소" 버튼이 없습니다. 이로 인해 사용자는 원치 않는 수정을 강제로 완료하거나 페이지를 새로고침해야 하는 불편함이 있습니다.\n\n"완료" 버튼 왼쪽에 변경 사항을 폐기하고 모드를 종료할 수 있는 "취소" 버튼을 추가하는 것을 권장합니다.
{/* 수정/완료 버튼: shrink-0 으로 항상 하단 고정 */}
<div className="flex shrink-0 justify-end gap-2 pt-8">
{(isEditMode || isAddingSchedule) && (
<button
type="button"
onClick={() => {
setIsEditMode(false);
setIsAddingSchedule(false);
setEditedSchedules([]);
setNewScheduleTitle('');
}}
className="h-[32px] w-[96px] rounded-[6px] border border-brand bg-white px-[14px] py-[6px] text-[14px] text-brand transition-opacity hover:opacity-80"
>
취소
</button>
)}
<button
type="button"
onClick={handleConfirm}
className="h-[32px] w-[96px] rounded-[6px] bg-brand px-[14px] py-[6px] text-[14px] text-white transition-opacity hover:opacity-80"
>
{isEditMode || isAddingSchedule ? '완료' : '수정'}
</button>
</div>| <AdminCalendarView | ||
| calendar={{ year, month, yearOptions, calendarDates, todayKey }} | ||
| view={{ viewMode, onViewModeChange: handleViewModeChange }} | ||
| schedules={{ scheduleDateKeys, visibleSchedules }} | ||
| monthDropdown={{ | ||
| isOpen: isMonthDropdownOpen, | ||
| handlers: { | ||
| onToggle: () => setOpenedDropdown(isMonthDropdownOpen ? null : DROPDOWN_TYPE.MONTH), | ||
| onSelect: (monthOption) => { setMonth(monthOption); setOpenedDropdown(null); }, | ||
| onScroll: handleMonthScroll, | ||
| }, | ||
| scrollRefs: { listRef: monthListRef, trackRef: monthTrackRef, thumbRef: monthThumbRef }, | ||
| }} | ||
| yearDropdown={{ | ||
| isOpen: isYearDropdownOpen, | ||
| handlers: { | ||
| onToggle: () => setOpenedDropdown(isYearDropdownOpen ? null : DROPDOWN_TYPE.YEAR), | ||
| onSelect: (yearOption) => { setYear(yearOption); setOpenedDropdown(null); }, | ||
| onScroll: handleYearScroll, | ||
| }, | ||
| scrollRefs: { listRef: yearListRef, trackRef: yearTrackRef, thumbRef: yearThumbRef }, | ||
| }} | ||
| onAddSchedule={handleAddSchedule} | ||
| onDeleteSchedule={handleDeleteSchedule} | ||
| onSaveEdits={handleSaveEdits} | ||
| /> |
There was a problem hiding this comment.
사용자가 특정 월에서 일정을 선택한 상태(예: selectedIndexes에 인덱스가 저장된 상태)에서 드롭다운을 통해 다른 월이나 연도로 이동할 경우, AdminCalendarView 내부의 selectedIndexes 상태가 초기화되지 않고 그대로 유지됩니다. 이로 인해 새로 바뀐 월의 동일한 인덱스에 있는 일정이 의도치 않게 선택되어 표시되거나, 삭제 시 엉뚱한 일정이 삭제되는 심각한 버그가 발생할 수 있습니다.\n\nAdminCalendarView 컴포넌트에 key={${year}-${month}}를 부여하면, 연도나 월이 변경될 때 컴포넌트가 자동으로 언마운트 후 재마운트되면서 내부 상태(selectedIndexes, isEditMode, isAddingSchedule 등)가 깔끔하게 초기화되어 이 문제를 우아하게 해결할 수 있습니다.
<AdminCalendarView
key={`${year}-${month}`}
calendar={{ year, month, yearOptions, calendarDates, todayKey }}
view={{ viewMode, onViewModeChange: handleViewModeChange }}
schedules={{ scheduleDateKeys, visibleSchedules }}
monthDropdown={{
isOpen: isMonthDropdownOpen,
handlers: {
onToggle: () => setOpenedDropdown(isMonthDropdownOpen ? null : DROPDOWN_TYPE.MONTH),
onSelect: (monthOption) => { setMonth(monthOption); setOpenedDropdown(null); },
onScroll: handleMonthScroll,
},
scrollRefs: { listRef: monthListRef, trackRef: monthTrackRef, thumbRef: monthThumbRef },
}}
yearDropdown={{
isOpen: isYearDropdownOpen,
handlers: {
onToggle: () => setOpenedDropdown(isYearDropdownOpen ? null : DROPDOWN_TYPE.YEAR),
onSelect: (yearOption) => { setYear(yearOption); setOpenedDropdown(null); },
onScroll: handleYearScroll,
},
scrollRefs: { listRef: yearListRef, trackRef: yearTrackRef, thumbRef: yearThumbRef },
}}
onAddSchedule={handleAddSchedule}
onDeleteSchedule={handleDeleteSchedule}
onSaveEdits={handleSaveEdits}
/>| const yearOptions = useMemo( | ||
| () => Array.from({ length: currentYear - 2022 + 1 }, (_, index) => 2022 + index), | ||
| [currentYear] | ||
| ); |
There was a problem hiding this comment.
현재 yearOptions는 currentYear까지만 생성하도록 제한되어 있습니다. 연말이나 학기 말에는 다음 연도(내년)의 일정을 미리 계획하고 등록해야 하는 경우가 많으므로, 관리자가 내년 일정을 조회하고 추가할 수 있도록 currentYear + 1까지 연도 옵션을 제공하는 것이 좋습니다.
| const yearOptions = useMemo( | |
| () => Array.from({ length: currentYear - 2022 + 1 }, (_, index) => 2022 + index), | |
| [currentYear] | |
| ); | |
| const yearOptions = useMemo( | |
| () => Array.from({ length: (currentYear + 1) - 2022 + 1 }, (_, index) => 2022 + index), | |
| [currentYear] | |
| ); |
| for (const current = new Date(start); current <= end; current.setDate(current.getDate() + 1)) { | ||
| keys.add(formatDateKey(new Date(current))); | ||
| } |
There was a problem hiding this comment.
current 변수는 이미 new Date(start)로 생성된 Date 객체 인스턴스입니다. 따라서 formatDateKey를 호출할 때 굳이 new Date(current)로 새로운 객체를 중복 생성할 필요가 없습니다. 불필요한 객체 생성을 줄이기 위해 current를 직접 전달하도록 수정하는 것이 효율적입니다.
| for (const current = new Date(start); current <= end; current.setDate(current.getDate() + 1)) { | |
| keys.add(formatDateKey(new Date(current))); | |
| } | |
| for (const current = new Date(start); current <= end; current.setDate(current.getDate() + 1)) { | |
| keys.add(formatDateKey(current)); | |
| } |
| <ul | ||
| ref={listRef} | ||
| onScroll={onScroll} | ||
| className="h-full w-[376px] divide-y divide-[#F1F3F7] overflow-y-auto" |
There was a problem hiding this comment.
ul 태그의 너비가 w-[376px]로 하드코딩되어 있습니다. 상위 패널의 너비는 panelWidthClass 프로프를 통해 동적으로 전달받고 있으므로, 내부 리스트인 ul은 하드코딩된 고정 너비 대신 w-full을 사용하여 패널 크기에 맞게 유연하게 늘어나도록 설정하는 것이 더 견고하고 유지보수하기 좋습니다.
| className="h-full w-[376px] divide-y divide-[#F1F3F7] overflow-y-auto" | |
| className="h-full w-full divide-y divide-[#F1F3F7] overflow-y-auto" |
# Conflicts: # src/components/schedule/AllScheduleView.jsx # src/components/schedule/ScheduleDropdown.jsx # src/components/schedule/ScheduleTabs.jsx # src/constants/schedule.js # src/styles/ScheduleStyles.jsx # src/utils/schedule.js
# Conflicts: # src/App.jsx # src/components/schedule/AllScheduleView.jsx # src/components/schedule/ScheduleTabs.jsx # src/constants/schedule.js # tailwind.config.js
|
constants/schedule.js에 상수가 정의돼 있고 방문자 SchedulePage.jsx는 이 상수를 쓰는데, 관리자 페이지만 리터럴을 하드코딩:
|
|
@takjinwu |





📌 작업 내용
🔗 관련 이슈
📸 스크린샷 / 화면 녹화
🧪 테스트 방법
✅ 체크리스트
npm run lint통과를 확인했습니다npm run format을 적용했습니다console.log를 제거했습니다develop인지 확인했습니다💬 리뷰어에게 한 마디