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 => (
+
+ ))}
+
+
+ )}
+
+ )
+}