diff --git a/src/assets/icons/schedule/schedule_calendar.svg b/src/assets/icons/schedule/schedule_calendar.svg new file mode 100644 index 0000000..0786b6a --- /dev/null +++ b/src/assets/icons/schedule/schedule_calendar.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/features/home/common/schedule/constants/calendar.ts b/src/features/home/common/schedule/constants/calendar.ts index 6c4a0ae..ea47ca0 100644 --- a/src/features/home/common/schedule/constants/calendar.ts +++ b/src/features/home/common/schedule/constants/calendar.ts @@ -1,19 +1,3 @@ -export const WEEKDAY_LABELS = [ - '일', - '월', - '화', - '수', - '목', - '금', - '토', -] as const - -/** 월요일 시작 주( date-fns weekStartsOn: 1 )와 그리드 열 순서를 맞춤 */ -export const WEEKDAY_LABELS_MONDAY_FIRST = [ - ...WEEKDAY_LABELS.slice(1), - WEEKDAY_LABELS[0], -] as const - export const DATE_KEY_FORMAT = 'yyyy-MM-dd' export const MONTH_LABEL_FORMAT = 'yyyy년 M월' diff --git a/src/features/home/common/schedule/hooks/useMonthlyCalendarViewModel.ts b/src/features/home/common/schedule/hooks/useMonthlyCalendarViewModel.ts index ab4565d..6c125fa 100644 --- a/src/features/home/common/schedule/hooks/useMonthlyCalendarViewModel.ts +++ b/src/features/home/common/schedule/hooks/useMonthlyCalendarViewModel.ts @@ -1,45 +1,19 @@ -import { - addDays, - eachDayOfInterval, - endOfMonth, - endOfWeek, - format, - isSameMonth, - startOfDay, - startOfMonth, - startOfWeek, -} from 'date-fns' +import { addDays, format, startOfDay } from 'date-fns' +import { getCalendarCells } from '@/shared/lib/calendarUtils' import { useMemo } from 'react' import { DATE_KEY_FORMAT, MONTH_LABEL_FORMAT, - WEEKDAY_LABELS, } from '@/features/home/common/schedule/constants/calendar' +import { WEEKDAY_LABELS } from '@/shared/constants/calendar' import { useMonthlyDateCellsState } from '@/features/home/common/schedule/hooks/useMonthlyDateCellsState' import type { MonthlyCalendarViewModel, - MonthlyCellInput, MonthlyDayMetrics, MonthlyCalendarPropsBase, } from '@/features/home/common/schedule/types/monthlyCalendar' import type { CalendarViewData } from '@/features/home/common/schedule/types/calendarView' -function getMonthlyCells(baseDate: Date): MonthlyCellInput[] { - const monthStart = startOfMonth(baseDate) - const monthEnd = endOfMonth(baseDate) - const intervalStart = startOfWeek(monthStart, { weekStartsOn: 0 }) - const intervalEnd = endOfWeek(monthEnd, { weekStartsOn: 0 }) - - return eachDayOfInterval({ start: intervalStart, end: intervalEnd }).map( - date => ({ - dateKey: format(date, DATE_KEY_FORMAT), - dayText: format(date, 'd'), - isCurrentMonth: isSameMonth(date, baseDate), - weekDay: date.getDay(), - }) - ) -} - type MinuteRange = [number, number] function mergeMinuteRanges(ranges: MinuteRange[]) { @@ -116,7 +90,16 @@ export function useMonthlyCalendarViewModel({ workspaceName, selectedDateKey, }: MonthlyCalendarPropsBase): MonthlyCalendarViewModel { - const cells = useMemo(() => getMonthlyCells(baseDate), [baseDate]) + const cells = useMemo( + () => + getCalendarCells(baseDate, 0).map(({ date, isCurrentMonth }) => ({ + dateKey: format(date, DATE_KEY_FORMAT), + dayText: format(date, 'd'), + isCurrentMonth, + weekDay: date.getDay(), + })), + [baseDate] + ) const selectedKey = selectedDateKey ?? format(baseDate, DATE_KEY_FORMAT) const dayMetricsByDate = useMemo(() => getDayMetricsByDate(data), [data]) diff --git a/src/features/home/common/schedule/types/monthlyCalendar.ts b/src/features/home/common/schedule/types/monthlyCalendar.ts index 586086d..515ad46 100644 --- a/src/features/home/common/schedule/types/monthlyCalendar.ts +++ b/src/features/home/common/schedule/types/monthlyCalendar.ts @@ -1,4 +1,4 @@ -import type { WEEKDAY_LABELS } from '@/features/home/common/schedule/constants/calendar' +import type { WEEKDAY_LABELS } from '@/shared/constants/calendar' import type { BaseCalendarProps } from '@/features/home/common/schedule/types/calendarBase' export interface MonthlyCellInput { diff --git a/src/features/home/common/schedule/ui/MonthlyCalendar.tsx b/src/features/home/common/schedule/ui/MonthlyCalendar.tsx index 2e32540..fffc7aa 100644 --- a/src/features/home/common/schedule/ui/MonthlyCalendar.tsx +++ b/src/features/home/common/schedule/ui/MonthlyCalendar.tsx @@ -94,7 +94,7 @@ export function MonthlyCalendar({ className={layout === 'manager' ? 'mt-5 px-[11px] pb-4' : 'mt-4'} >
- {weekdayLabels.map((label, index) => ( + {weekdayLabels.map((label: string, index: number) => ( ( DEFAULT_SELECTED_DAYS ) + const [startHour, setStartHour] = useState('') + const [startMinute, setStartMinute] = useState('') + const [endHour, setEndHour] = useState('') + const [endMinute, setEndMinute] = useState('') + const [selectedWorkerIndex, setSelectedWorkerIndex] = useState(0) - const workTimeRangeLabel = useMemo( - () => - `${DEFAULT_TIME.startHour}:${DEFAULT_TIME.startMinute} ~ ${DEFAULT_TIME.endHour}:${DEFAULT_TIME.endMinute}`, - [] - ) + const workTimeRangeLabel = useMemo(() => { + const sh = startHour || '00' + const sm = startMinute || '00' + const eh = endHour || '00' + const em = endMinute || '00' + return `${sh}:${sm} ~ ${eh}:${em}` + }, [startHour, startMinute, endHour, endMinute]) function toggleDay(day: string) { setSelectedDays(prev => @@ -29,17 +37,21 @@ export function useWorkerScheduleManageViewModel() { } return { - worker: { - name: '이름임', - role: 'manager' as const, - }, + worker: MOCK_WORKERS[selectedWorkerIndex], + workers: MOCK_WORKERS, + selectedWorkerIndex, + setSelectedWorkerIndex, workdayOptions: WORKDAY_OPTIONS, selectedDays, workTimeRangeLabel, - startHour: DEFAULT_TIME.startHour, - startMinute: DEFAULT_TIME.startMinute, - endHour: DEFAULT_TIME.endHour, - endMinute: DEFAULT_TIME.endMinute, + startHour, + startMinute, + endHour, + endMinute, + setStartHour, + setStartMinute, + setEndHour, + setEndMinute, toggleDay, } } diff --git a/src/features/manager/index.ts b/src/features/manager/index.ts index 891fdea..3312e30 100644 --- a/src/features/manager/index.ts +++ b/src/features/manager/index.ts @@ -3,3 +3,5 @@ export { WorkspaceChangeList } from '@/features/manager/home/ui/WorkspaceChangeL export { WorkspaceChangeCard } from '@/features/manager/home/ui/WorkspaceChangeCard' export { TodayWorkerList } from '@/features/manager/home/ui/TodayWorkerList' export { useManagerHomeViewModel } from '@/features/manager/home/hooks/useManagerHomeViewModel' +export type { ScheduleTab } from '@/features/manager/schedule/types/workerSchedule' +export { SCHEDULE_TABS } from '@/features/manager/schedule/constants/workerSchedule' diff --git a/src/features/manager/schedule/constants/workerSchedule.ts b/src/features/manager/schedule/constants/workerSchedule.ts new file mode 100644 index 0000000..939f6ed --- /dev/null +++ b/src/features/manager/schedule/constants/workerSchedule.ts @@ -0,0 +1,3 @@ +import type { ScheduleTab } from '@/features/manager/schedule/types/workerSchedule' + +export const SCHEDULE_TABS: ScheduleTab[] = ['고정', '일반'] diff --git a/src/features/manager/schedule/types/workerSchedule.ts b/src/features/manager/schedule/types/workerSchedule.ts new file mode 100644 index 0000000..992889a --- /dev/null +++ b/src/features/manager/schedule/types/workerSchedule.ts @@ -0,0 +1 @@ +export type ScheduleTab = '고정' | '일반' diff --git a/src/features/manager/worker-schedule/types/scheduleColor.ts b/src/features/manager/worker-schedule/types/scheduleColor.ts new file mode 100644 index 0000000..cbea0d9 --- /dev/null +++ b/src/features/manager/worker-schedule/types/scheduleColor.ts @@ -0,0 +1,12 @@ +export const ScheduleColor = { + Pink: '#EB98AD', + Purple: '#9FA4F8', + Blue: '#9EC9FD', + Yellow: '#FCD680', + LightPink: '#F0BFC0', + LightPurple: '#CCBAF9', + Gray: '#C8D2E1', + DarkGray: '#9E9EA3', +} as const + +export type ScheduleColor = (typeof ScheduleColor)[keyof typeof ScheduleColor] diff --git a/src/features/user/home/schedule/api/schedule.ts b/src/features/user/home/schedule/api/schedule.ts index fb4b3ea..e12cd8b 100644 --- a/src/features/user/home/schedule/api/schedule.ts +++ b/src/features/user/home/schedule/api/schedule.ts @@ -11,7 +11,7 @@ import { getDurationHours, toDateKey, toTimeLabel, -} from '@/features/user/home/schedule/lib/date' +} from '@/features/home/common/schedule/lib/date' import type { SelfScheduleQueryParams } from '@/shared/types/schedule' function mapToCalendarEvent( diff --git a/src/features/user/home/schedule/constants/calendar.ts b/src/features/user/home/schedule/constants/calendar.ts index 5678e0b..e8451b4 100644 --- a/src/features/user/home/schedule/constants/calendar.ts +++ b/src/features/user/home/schedule/constants/calendar.ts @@ -1,7 +1,4 @@ -// constants는 common으로 이동 — 하위 호환 re-export export { - WEEKDAY_LABELS, - WEEKDAY_LABELS_MONDAY_FIRST, DATE_KEY_FORMAT, MONTH_LABEL_FORMAT, DAILY_TIMELINE_HEIGHT, diff --git a/src/features/user/home/schedule/lib/date.test.ts b/src/features/user/home/schedule/lib/date.test.ts index 88b39a2..c5f1649 100644 --- a/src/features/user/home/schedule/lib/date.test.ts +++ b/src/features/user/home/schedule/lib/date.test.ts @@ -6,15 +6,18 @@ import { StatusEnum } from '@/shared/types/enums' import { getDailyHourTicks, getDayHours, - getDurationHours, getMonthlyDateCells, getScheduleParamsByMode, getWeekRangeLabel, getWeeklyDateCells, moveDateByMode, +} from './date' + +import { toDateKey, toTimeLabel, -} from './date' + getDurationHours, +} from '@/features/home/common/schedule/lib/date' describe('toDateKey', () => { it('ISO 문자열에서 날짜 부분만 반환한다', () => { diff --git a/src/features/user/home/schedule/lib/date.ts b/src/features/user/home/schedule/lib/date.ts index a76d78b..871d000 100644 --- a/src/features/user/home/schedule/lib/date.ts +++ b/src/features/user/home/schedule/lib/date.ts @@ -14,16 +14,13 @@ import type { ScheduleDataDto } from '@/features/user/home/schedule/types/schedu import type { HomeCalendarMode } from '@/features/user/home/schedule/types/schedule' import type { SelfScheduleQueryParams } from '@/shared/types/schedule' import type { ScheduleListItem } from '@/features/user/home/schedule/types/scheduleList' -import { WEEKDAY_LABELS } from '@/features/user/home/schedule/constants/calendar' +import { WEEKDAY_LABELS } from '@/shared/constants/calendar' import { toDateKey, toTimeLabel, getDurationHours, } from '@/features/home/common/schedule/lib/date' -// 순수 날짜 유틸은 common으로 이동 — 하위 호환 re-export -export { toDateKey, toTimeLabel, getDurationHours } - export function getMonthlyDateCells(baseDate: Date) { const monthStart = startOfMonth(baseDate) const monthEnd = endOfMonth(baseDate) diff --git a/src/features/user/home/workspace/api/workspaceSchedule.ts b/src/features/user/home/workspace/api/workspaceSchedule.ts index 5aaf1ec..9c973f7 100644 --- a/src/features/user/home/workspace/api/workspaceSchedule.ts +++ b/src/features/user/home/workspace/api/workspaceSchedule.ts @@ -4,11 +4,11 @@ import type { CalendarViewData, } from '@/features/user/home/schedule/types/schedule' import { + getDurationHours, toDateKey, toTimeLabel, - getDurationHours, - formatScheduleTimeRange, -} from '@/features/user/home/schedule/lib/date' +} from '@/features/home/common/schedule/lib/date' +import { formatScheduleTimeRange } from '@/features/user/home/schedule/lib/date' import type { WorkspaceScheduleApiResponse, WorkspaceScheduleQueryParams, diff --git a/src/pages/manager/worker-schedule/index.tsx b/src/pages/manager/worker-schedule/index.tsx index 860d783..82ca5e7 100644 --- a/src/pages/manager/worker-schedule/index.tsx +++ b/src/pages/manager/worker-schedule/index.tsx @@ -1,29 +1,69 @@ -import { useNavigate } from 'react-router-dom' +import { useState } from 'react' import { WorkerRoleBadge } from '@/shared/ui/home/WorkerRoleBadge' import { useWorkerScheduleManageViewModel } from '@/features/manager/home/hooks/useWorkerScheduleManageViewModel' -import chevronLeftIcon from '@/assets/icons/chevron-left.svg' import chevronDownIcon from '@/assets/icons/home/chevron-down.svg' +import calendarIcon from '@/assets/icons/schedule/schedule_calendar.svg' +import { Navbar } from '@/shared/ui/common/Navbar' +import { ScheduleCalendar } from '@/shared/ui/common/ScheduleCalendar' +import { ColorPickerDropdown } from '@/shared/ui/schedule/ColorPickerDropdown' +import type { ScheduleTab } from '@/features/manager' +import { SCHEDULE_TABS } from '@/features/manager' +import { ScheduleColor } from '@/features/manager/worker-schedule/types/scheduleColor' interface TimeSelectBoxProps { value: string unit: string + onChange: (value: string) => void } -function TimeSelectBox({ value, unit }: TimeSelectBoxProps) { +function TimeSelectBox({ value, unit, onChange }: TimeSelectBoxProps) { + const handleChange = (e: React.ChangeEvent) => { + let inputValue = e.target.value.replace(/\D/g, '') + + if (!inputValue) { + onChange('') + return + } + + if (inputValue.length > 2) { + inputValue = inputValue.slice(-2) + } + + const maxValue = unit === '시' ? 23 : 59 + const num = Math.min(parseInt(inputValue, 10), maxValue) + onChange(num.toString().padStart(2, '0')) + } + return (
- {value} - - {unit} - + + {unit}
) } export function ManagerWorkerSchedulePage() { - const navigate = useNavigate() + const [activeTab, setActiveTab] = useState('고정') + const [showCalendar, setShowCalendar] = useState(false) + const [selectedDate, setSelectedDate] = useState(null) + const [isWorkerDropdownOpen, setIsWorkerDropdownOpen] = useState(false) + const [isColorPickerOpen, setIsColorPickerOpen] = useState(false) + const [selectedColor, setSelectedColor] = useState( + ScheduleColor.Pink + ) const { worker, + workers, + selectedWorkerIndex, + setSelectedWorkerIndex, workdayOptions, selectedDays, workTimeRangeLabel, @@ -31,34 +71,45 @@ export function ManagerWorkerSchedulePage() { startMinute, endHour, endMinute, + setStartHour, + setStartMinute, + setEndHour, + setEndMinute, toggleDay, } = useWorkerScheduleManageViewModel() return (
-
- -

- 근무자 스케줄 관리 -

-
+ +
+ {SCHEDULE_TABS.map(tab => { + const isActive = activeTab === tab + return ( + + ) + })} +
-
+

근무자 선택

-
+ -
-
+
+ -
-

근무일 선택

-
- {workdayOptions.map(day => { - const selected = selectedDays.includes(day) - return ( + {isWorkerDropdownOpen && ( +
+ {workers.map((w, index) => ( - ) - })} -
+ ))} +
+ )}
-
-

- 근무 시간 선택 -

-

- {workTimeRangeLabel} -

+ {activeTab === '고정' ? ( +
+

근무일 선택

+
+ {workdayOptions.map(day => { + const selected = selectedDays.includes(day) + return ( + + ) + })} +
+
+ ) : ( + <> +
+

날짜 선택

+ + + {showCalendar && ( +
+ { + setSelectedDate(date) + setShowCalendar(false) + }} + /> +
+ )} +
+
+ setIsColorPickerOpen(!isColorPickerOpen)} + /> +
+ + )} + {!isColorPickerOpen && ( +
+

+ 근무 시간 선택 +

+

+ {workTimeRangeLabel} +

-
- - 출근 시간 - -
- -
-
-
-
-
- - 퇴근 시간 - -
- -
-
-
-
-
+
+ )}
diff --git a/src/shared/constants/calendar.ts b/src/shared/constants/calendar.ts new file mode 100644 index 0000000..fd9570d --- /dev/null +++ b/src/shared/constants/calendar.ts @@ -0,0 +1,14 @@ +export const WEEKDAY_LABELS = [ + '일', + '월', + '화', + '수', + '목', + '금', + '토', +] as const + +export const WEEKDAY_LABELS_MONDAY_FIRST = [ + ...WEEKDAY_LABELS.slice(1), + WEEKDAY_LABELS[0], +] as const diff --git a/src/shared/lib/calendarUtils.ts b/src/shared/lib/calendarUtils.ts new file mode 100644 index 0000000..c932465 --- /dev/null +++ b/src/shared/lib/calendarUtils.ts @@ -0,0 +1,30 @@ +import { + eachDayOfInterval, + endOfMonth, + endOfWeek, + isSameMonth, + startOfMonth, + startOfWeek, +} from 'date-fns' + +export interface CalendarCell { + date: Date + isCurrentMonth: boolean +} + +export function getCalendarCells( + baseDate: Date, + weekStartsOn: 0 | 1 = 1 +): CalendarCell[] { + const monthStart = startOfMonth(baseDate) + const monthEnd = endOfMonth(baseDate) + const intervalStart = startOfWeek(monthStart, { weekStartsOn }) + const intervalEnd = endOfWeek(monthEnd, { weekStartsOn }) + + return eachDayOfInterval({ start: intervalStart, end: intervalEnd }).map( + date => ({ + date, + isCurrentMonth: isSameMonth(date, baseDate), + }) + ) +} diff --git a/src/shared/ui/common/ScheduleCalendar.tsx b/src/shared/ui/common/ScheduleCalendar.tsx new file mode 100644 index 0000000..b30693d --- /dev/null +++ b/src/shared/ui/common/ScheduleCalendar.tsx @@ -0,0 +1,144 @@ +import { useEffect, useState } from 'react' +import { isSameDay } from 'date-fns' +import chevronLeftIcon from '@/assets/icons/nav/chevron-left.svg' +import { getCalendarCells } from '@/shared/lib/calendarUtils' +import { WEEKDAY_LABELS } from '@/shared/constants/calendar' + +interface ScheduleCalendarProps { + selectedDate?: Date | null + onDateChange: (date: Date) => void +} + +export function ScheduleCalendar({ + selectedDate, + onDateChange, +}: ScheduleCalendarProps) { + const today = new Date() + const [viewYear, setViewYear] = useState( + selectedDate?.getFullYear() ?? today.getFullYear() + ) + const [viewMonth, setViewMonth] = useState( + selectedDate?.getMonth() ?? today.getMonth() + ) + + useEffect(() => { + if (selectedDate) { + const year = selectedDate.getFullYear() + const month = selectedDate.getMonth() + if (viewYear !== year || viewMonth !== month) { + setViewYear(year) + setViewMonth(month) + } + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [selectedDate]) + + const baseDate = new Date(viewYear, viewMonth, 1) + const cells = getCalendarCells(baseDate, 0) + const weeks = [] + for (let i = 0; i < cells.length; i += 7) { + weeks.push(cells.slice(i, i + 7)) + } + + function prevMonth() { + if (viewMonth === 0) { + setViewYear(y => y - 1) + setViewMonth(11) + } else setViewMonth(m => m - 1) + } + + function nextMonth() { + if (viewMonth === 11) { + setViewYear(y => y + 1) + setViewMonth(0) + } else setViewMonth(m => m + 1) + } + + return ( +
+ {/* Header */} +
+ + + {viewYear}년 {viewMonth + 1}월 + + +
+ + {/* Weekday headers */} +
+ {WEEKDAY_LABELS.map(day => ( +
+ + {day} + +
+ ))} +
+ + {/* Date grid */} +
+ {weeks.map((week, wi) => ( +
+ {week.map(({ date, isCurrentMonth }, di) => { + const isSelected = selectedDate + ? isSameDay(date, selectedDate) + : false + + let textColor = 'text-text-90' + if (!isCurrentMonth) textColor = 'text-text-50' + else if (di === 0) textColor = 'text-error' + else if (di === 6) textColor = 'text-subBlue' + + return ( + + ) + })} +
+ ))} +
+
+ ) +} diff --git a/src/shared/ui/schedule/ColorPickerDropdown.tsx b/src/shared/ui/schedule/ColorPickerDropdown.tsx new file mode 100644 index 0000000..6029d03 --- /dev/null +++ b/src/shared/ui/schedule/ColorPickerDropdown.tsx @@ -0,0 +1,75 @@ +import chevronDownIcon from '@/assets/icons/home/chevron-down.svg' +import { ScheduleColor } from '@/features/manager/worker-schedule/types/scheduleColor' + +interface ColorPickerDropdownProps { + selectedColor: ScheduleColor + onColorChange: (color: ScheduleColor) => void + isOpen: boolean + onToggle: () => void +} + +export function ColorPickerDropdown({ + selectedColor, + onColorChange, + isOpen, + onToggle, +}: ColorPickerDropdownProps) { + const colors = Object.values(ScheduleColor) + + return ( +
+ + + {isOpen && ( +
+
+ {colors.map(color => ( + + ))} +
+
+ )} +
+ ) +}