diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 9bea199..19739d5 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -26,6 +26,10 @@ - +### 실행화면 + +- + ## 테스트 - [ ] 로컬 실행 확인 diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..d07d31f --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,9 @@ +{ + "editor.formatOnSave": true, + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.codeActionsOnSave": { + "source.fixAll.eslint": "explicit" + }, + "typescript.preferences.importModuleSpecifier": "non-relative", + "javascript.preferences.importModuleSpecifier": "non-relative" +} diff --git a/public/images/header/logo.svg b/public/images/header/logo.svg new file mode 100644 index 0000000..8ae72ad --- /dev/null +++ b/public/images/header/logo.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/public/landing/avatar-1.png b/public/images/landing/avatar-1.png similarity index 100% rename from public/landing/avatar-1.png rename to public/images/landing/avatar-1.png diff --git a/public/landing/avatar-2.png b/public/images/landing/avatar-2.png similarity index 100% rename from public/landing/avatar-2.png rename to public/images/landing/avatar-2.png diff --git a/public/landing/avatar-3.png b/public/images/landing/avatar-3.png similarity index 100% rename from public/landing/avatar-3.png rename to public/images/landing/avatar-3.png diff --git a/public/images/landing/bg.svg b/public/images/landing/bg.svg new file mode 100644 index 0000000..79a79da --- /dev/null +++ b/public/images/landing/bg.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/public/images/landing/hero-bg.svg b/public/images/landing/hero-bg.svg new file mode 100644 index 0000000..dffcdf1 --- /dev/null +++ b/public/images/landing/hero-bg.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/public/landing/hero-bg.png b/public/landing/hero-bg.png deleted file mode 100644 index 8c0bee3..0000000 Binary files a/public/landing/hero-bg.png and /dev/null differ diff --git a/src/app/workspaces/[workspaceId]/layout.tsx b/src/app/workspaces/[workspaceId]/layout.tsx new file mode 100644 index 0000000..a7cc4a9 --- /dev/null +++ b/src/app/workspaces/[workspaceId]/layout.tsx @@ -0,0 +1,15 @@ +// 워크스페이스 공통 사이드바와 헤더를 적용하는 라우트 레이아웃입니다. +import { WorkspaceShell } from '@/widgets/workspace-shell'; + +interface WorkspaceLayoutProps { + children: React.ReactNode; + params: Promise<{ + workspaceId: string; + }>; +} + +export default async function WorkspaceLayout({ children, params }: WorkspaceLayoutProps) { + const { workspaceId } = await params; + + return {children}; +} diff --git a/src/app/workspaces/[workspaceId]/work-schedule/page.tsx b/src/app/workspaces/[workspaceId]/work-schedule/page.tsx new file mode 100644 index 0000000..d2710c6 --- /dev/null +++ b/src/app/workspaces/[workspaceId]/work-schedule/page.tsx @@ -0,0 +1,14 @@ +// 워크스페이스 근무 일정 페이지의 라우트 진입점입니다. +import { WorkScheduleView } from '@/views/store-operation/work-schedule'; + +interface WorkSchedulePageProps { + params: Promise<{ + workspaceId: string; + }>; +} + +export default async function WorkSchedulePage({ params }: WorkSchedulePageProps) { + const { workspaceId } = await params; + + return ; +} diff --git a/src/entities/work-schedule/index.ts b/src/entities/work-schedule/index.ts new file mode 100644 index 0000000..8eba50b --- /dev/null +++ b/src/entities/work-schedule/index.ts @@ -0,0 +1,15 @@ +// 근무 일정 도메인 타입, 목업 설정, 헬퍼 함수의 공개 API입니다. +export type { + WorkScheduleConfig, + WorkScheduleEntry, + WorkShiftColor, + WorkShiftOption, +} from './model/work-schedule.types'; +export type { WeekdayKey } from './model/weekdays'; +export { weekdays } from './model/weekdays'; +export { countSchedulesByWeekday } from './lib/count-schedules-by-weekday'; +export { createInitialWorkSchedule } from './lib/create-initial-work-schedule'; +export { getDefaultWorkShiftOption } from './lib/get-default-work-shift-option'; +export { getWorkMembersByWeekday } from './lib/get-work-members-by-weekday'; +export { getNextWorkShiftOption } from './lib/get-next-work-shift-option'; +export { mockWorkScheduleConfig } from './model/mock-work-schedule-config'; diff --git a/src/entities/work-schedule/lib/count-schedules-by-weekday.ts b/src/entities/work-schedule/lib/count-schedules-by-weekday.ts new file mode 100644 index 0000000..8dd0dae --- /dev/null +++ b/src/entities/work-schedule/lib/count-schedules-by-weekday.ts @@ -0,0 +1,31 @@ +// 특정 요일의 각 근무 옵션에 몇 명의 멤버가 배정되어 있는지 계산합니다. +import type { WeekdayKey } from '../model/weekdays'; +import type { WorkScheduleConfig, WorkScheduleEntry } from '../model/work-schedule.types'; + +interface CountSchedulesByWeekdayParams { + schedule: WorkScheduleEntry[]; + config: WorkScheduleConfig; + weekday: WeekdayKey; +} + +export function countSchedulesByWeekday({ + schedule, + config, + weekday, +}: CountSchedulesByWeekdayParams): Record { +const counts = Object.fromEntries(config.shifts.map((shift) => [shift.id, 0])); + + const uniqueEntries = new Map(); + + schedule + .filter((entry) => entry.weekday === weekday) + .forEach((entry) => { + uniqueEntries.set(`${entry.userId}-${entry.weekday}`, entry); + }); + + uniqueEntries.forEach((entry) => { + counts[entry.shiftOptionId] = (counts[entry.shiftOptionId] ?? 0) + 1; + }); + + return counts; +} diff --git a/src/entities/work-schedule/lib/create-initial-work-schedule.ts b/src/entities/work-schedule/lib/create-initial-work-schedule.ts new file mode 100644 index 0000000..c4a3688 --- /dev/null +++ b/src/entities/work-schedule/lib/create-initial-work-schedule.ts @@ -0,0 +1,28 @@ +// 모든 워크스페이스 멤버에 대한 기본 요일별 근무 일정을 생성합니다. +import type { WorkspaceMember } from '@/entities/workspace-member'; +import { getDefaultWorkShiftOption } from './get-default-work-shift-option'; +import { weekdays } from '../model/weekdays'; +import type { WorkScheduleConfig, WorkScheduleEntry } from '../model/work-schedule.types'; + +interface CreateInitialWorkScheduleParams { + workspaceId: string; + members: WorkspaceMember[]; + config: WorkScheduleConfig; +} + +export function createInitialWorkSchedule({ + workspaceId, + members, + config, +}: CreateInitialWorkScheduleParams): WorkScheduleEntry[] { + const defaultShift = getDefaultWorkShiftOption(config.shifts); + + return members.flatMap((member) => + weekdays.map((weekday) => ({ + workspaceId, + userId: member.userId, + weekday: weekday.key, + shiftOptionId: defaultShift.id, + })), + ); +} diff --git a/src/entities/work-schedule/lib/get-default-work-shift-option.ts b/src/entities/work-schedule/lib/get-default-work-shift-option.ts new file mode 100644 index 0000000..3c84884 --- /dev/null +++ b/src/entities/work-schedule/lib/get-default-work-shift-option.ts @@ -0,0 +1,8 @@ +// 기본 셀 값으로 첫 번째 근무 중인 근무 옵션을 선택합니다. +import type { WorkShiftOption } from '../model/work-schedule.types'; + +export function getDefaultWorkShiftOption(shifts: WorkShiftOption[]): WorkShiftOption { + const firstWorkingShift = shifts.find((shift) => !shift.isOff); + + return firstWorkingShift ?? shifts[0]; +} diff --git a/src/entities/work-schedule/lib/get-next-work-shift-option.ts b/src/entities/work-schedule/lib/get-next-work-shift-option.ts new file mode 100644 index 0000000..4662394 --- /dev/null +++ b/src/entities/work-schedule/lib/get-next-work-shift-option.ts @@ -0,0 +1,17 @@ +// 설정된 배열 순서에 따라 다음 근무 옵션을 반환합니다. +import type { WorkShiftOption } from '../model/work-schedule.types'; + +interface GetNextWorkShiftOptionParams { + shifts: WorkShiftOption[]; + currentShiftOptionId: string; +} + +export function getNextWorkShiftOption({ + shifts, + currentShiftOptionId, +}: GetNextWorkShiftOptionParams): WorkShiftOption { + const currentIndex = shifts.findIndex((shift) => shift.id === currentShiftOptionId); + const nextIndex = currentIndex === -1 ? 0 : (currentIndex + 1) % shifts.length; + + return shifts[nextIndex]; +} diff --git a/src/entities/work-schedule/lib/get-work-members-by-weekday.ts b/src/entities/work-schedule/lib/get-work-members-by-weekday.ts new file mode 100644 index 0000000..5bdae88 --- /dev/null +++ b/src/entities/work-schedule/lib/get-work-members-by-weekday.ts @@ -0,0 +1,28 @@ +// 휴무 옵션을 제외하여 특정 요일에 근무 중인 멤버를 판별합니다. +import type { WorkspaceMember } from '@/entities/workspace-member'; +import type { WeekdayKey } from '../model/weekdays'; +import type { WorkScheduleConfig, WorkScheduleEntry } from '../model/work-schedule.types'; + +interface GetWorkMembersByWeekdayParams { + schedule: WorkScheduleEntry[]; + members: WorkspaceMember[]; + config: WorkScheduleConfig; + weekday: WeekdayKey; +} + +export function getWorkMembersByWeekday({ + schedule, + members, + config, + weekday, +}: GetWorkMembersByWeekdayParams): WorkspaceMember[] { + const offShiftIds = new Set(config.shifts.filter((shift) => shift.isOff).map((shift) => shift.id)); + + const workingUserIds = new Set( + schedule + .filter((entry) => entry.weekday === weekday && !offShiftIds.has(entry.shiftOptionId)) + .map((entry) => entry.userId), + ); + + return members.filter((member) => workingUserIds.has(member.userId)); +} diff --git a/src/entities/work-schedule/model/mock-work-schedule-config.ts b/src/entities/work-schedule/model/mock-work-schedule-config.ts new file mode 100644 index 0000000..b25ee94 --- /dev/null +++ b/src/entities/work-schedule/model/mock-work-schedule-config.ts @@ -0,0 +1,39 @@ +// 워크스페이스별 설정이 저장되기 전까지 사용하는 기본 목업 근무 설정입니다. +import type { WorkScheduleConfig } from './work-schedule.types'; + +export const mockWorkScheduleConfig: WorkScheduleConfig = { + shifts: [ + { + id: 'shift-open', + name: '오픈', + startTime: '09:00', + endTime: '14:00', + color: 'sky', + isOff: false, + }, + { + id: 'shift-middle', + name: '미들', + startTime: '14:00', + endTime: '19:00', + color: 'violet', + isOff: false, + }, + { + id: 'shift-close', + name: '마감', + startTime: '19:00', + endTime: '24:00', + color: 'amber', + isOff: false, + }, + { + id: 'shift-off', + name: '휴무', + startTime: null, + endTime: null, + color: 'slate', + isOff: true, + }, + ], +}; diff --git a/src/entities/work-schedule/model/weekdays.ts b/src/entities/work-schedule/model/weekdays.ts new file mode 100644 index 0000000..a914d73 --- /dev/null +++ b/src/entities/work-schedule/model/weekdays.ts @@ -0,0 +1,12 @@ +// 첫 번째 근무 일정 목업에서 사용하는 요일 컬럼입니다. +export const weekdays = [ + { key: 'monday', label: '월' }, + { key: 'tuesday', label: '화' }, + { key: 'wednesday', label: '수' }, + { key: 'thursday', label: '목' }, + { key: 'friday', label: '금' }, + { key: 'saturday', label: '토' }, + { key: 'sunday', label: '일' }, +] as const; + +export type WeekdayKey = (typeof weekdays)[number]['key']; diff --git a/src/entities/work-schedule/model/work-schedule.types.ts b/src/entities/work-schedule/model/work-schedule.types.ts new file mode 100644 index 0000000..58c2532 --- /dev/null +++ b/src/entities/work-schedule/model/work-schedule.types.ts @@ -0,0 +1,24 @@ +// 목업 근무 일정 모듈의 설정과 항목에 대한 핵심 타입입니다. +import type { WeekdayKey } from './weekdays'; + +export type WorkShiftColor = 'sky' | 'violet' | 'amber' | 'slate' | 'emerald' | 'rose'; + +export interface WorkShiftOption { + id: string; + name: string; + startTime: string | null; + endTime: string | null; + color: WorkShiftColor; + isOff: boolean; +} + +export interface WorkScheduleConfig { + shifts: WorkShiftOption[]; +} + +export interface WorkScheduleEntry { + workspaceId: string; + userId: string; + weekday: WeekdayKey; + shiftOptionId: string; +} diff --git a/src/entities/workspace-member/index.ts b/src/entities/workspace-member/index.ts new file mode 100644 index 0000000..1897eff --- /dev/null +++ b/src/entities/workspace-member/index.ts @@ -0,0 +1,4 @@ +// 목업 워크스페이스 멤버 데이터와 타입의 공개 API입니다. +export type { WorkspaceMember } from './model/workspace-member.types'; +export { mockCurrentWorkspaceMember } from './model/mock-current-workspace-member'; +export { mockWorkspaceMembers } from './model/mock-workspace-members'; diff --git a/src/entities/workspace-member/model/mock-current-workspace-member.ts b/src/entities/workspace-member/model/mock-current-workspace-member.ts new file mode 100644 index 0000000..1908b7b --- /dev/null +++ b/src/entities/workspace-member/model/mock-current-workspace-member.ts @@ -0,0 +1,10 @@ +// 인증/멤버 API 연결 전 shell 푸터와 헤더에 표시하는 현재 멤버 목업입니다. +import type { WorkspaceMember } from './workspace-member.types'; + +export const mockCurrentWorkspaceMember: WorkspaceMember = { + workspaceId: 'test', + userId: 'user-1', + workspaceNickname: '김민서', + avatarLabel: '김', + role: 'owner', +}; diff --git a/src/entities/workspace-member/model/mock-workspace-members.ts b/src/entities/workspace-member/model/mock-workspace-members.ts new file mode 100644 index 0000000..0b2cc39 --- /dev/null +++ b/src/entities/workspace-member/model/mock-workspace-members.ts @@ -0,0 +1,47 @@ +import type { WorkspaceMember } from './workspace-member.types'; + +// mock-member data, 실제 서버와 연동이 되면 수정될 예정 +export const mockWorkspaceMembers: WorkspaceMember[] = [ + { + workspaceId: 'store-workspace', + userId: 'user-1', + workspaceNickname: '김민서', + avatarLabel: '김', + role: 'owner', + }, + { + workspaceId: 'store-workspace', + userId: 'user-2', + workspaceNickname: '이준혁', + avatarLabel: '이', + role: 'member', + }, + { + workspaceId: 'store-workspace', + userId: 'user-3', + workspaceNickname: '박소연', + avatarLabel: '박', + role: 'member', + }, + { + workspaceId: 'store-workspace', + userId: 'user-4', + workspaceNickname: '최다은', + avatarLabel: '최', + role: 'member', + }, + { + workspaceId: 'store-workspace', + userId: 'user-5', + workspaceNickname: '정우진', + avatarLabel: '정', + role: 'member', + }, + { + workspaceId: 'store-workspace', + userId: 'user-6', + workspaceNickname: '이서영', + avatarLabel: '이', + role: 'member', + }, +]; diff --git a/src/entities/workspace-member/model/workspace-member.types.ts b/src/entities/workspace-member/model/workspace-member.types.ts new file mode 100644 index 0000000..cff1994 --- /dev/null +++ b/src/entities/workspace-member/model/workspace-member.types.ts @@ -0,0 +1,8 @@ +// Supabase 데이터가 연결되기 전까지 사용하는 목업 일정용 워크스페이스 멤버 형태입니다. +export interface WorkspaceMember { + workspaceId: string; + userId: string; + workspaceNickname: string; + avatarLabel: string; + role: 'owner' | 'member'; +} diff --git a/src/entities/workspace/index.ts b/src/entities/workspace/index.ts new file mode 100644 index 0000000..a44d0a6 --- /dev/null +++ b/src/entities/workspace/index.ts @@ -0,0 +1,3 @@ +// 목업 워크스페이스 데이터와 타입의 공개 API입니다. +export type { Workspace } from './model/workspace.types'; +export { mockWorkspace } from './model/mock-workspace'; diff --git a/src/entities/workspace/model/mock-workspace.ts b/src/entities/workspace/model/mock-workspace.ts new file mode 100644 index 0000000..e482c6e --- /dev/null +++ b/src/entities/workspace/model/mock-workspace.ts @@ -0,0 +1,8 @@ +// 워크스페이스 API 연결 전 사이드바에 표시하는 현재 워크스페이스 목업입니다. +import type { Workspace } from './workspace.types'; + +export const mockWorkspace: Workspace = { + id: 'test', + name: '카페 그레이 운영', + purpose: 'store-operation', +}; diff --git a/src/entities/workspace/model/workspace.types.ts b/src/entities/workspace/model/workspace.types.ts new file mode 100644 index 0000000..2560d7a --- /dev/null +++ b/src/entities/workspace/model/workspace.types.ts @@ -0,0 +1,6 @@ +// Supabase 워크스페이스 데이터 연결 전 shell에서 사용하는 워크스페이스 타입입니다. +export interface Workspace { + id: string; + name: string; + purpose: 'store-operation' | 'team-project' | 'side-project'; +} diff --git a/src/features/manage-work-schedule/index.ts b/src/features/manage-work-schedule/index.ts new file mode 100644 index 0000000..5d306c6 --- /dev/null +++ b/src/features/manage-work-schedule/index.ts @@ -0,0 +1,2 @@ +// 근무 일정 관리 기능의 공개 API입니다. +export { WorkScheduleBoard } from './ui/WorkScheduleBoard'; diff --git a/src/features/manage-work-schedule/model/use-work-schedule-state.ts b/src/features/manage-work-schedule/model/use-work-schedule-state.ts new file mode 100644 index 0000000..3fe4135 --- /dev/null +++ b/src/features/manage-work-schedule/model/use-work-schedule-state.ts @@ -0,0 +1,121 @@ +'use client'; + +// 목업 UI에서 일정 셀의 로컬 상태와 근무 옵션 교체 동작을 관리합니다. +import { useState } from 'react'; +import { + getDefaultWorkShiftOption, + getNextWorkShiftOption, + weekdays, + type WeekdayKey, + type WorkScheduleConfig, + type WorkScheduleEntry, +} from '@/entities/work-schedule'; +import type { WorkspaceMember } from '@/entities/workspace-member'; + +interface UseWorkScheduleStateParams { + initialSchedule: WorkScheduleEntry[]; + members: WorkspaceMember[]; + config: WorkScheduleConfig; +} + +function completeScheduleEntries({ + schedule, + members, + config, +}: { + schedule: WorkScheduleEntry[]; + members: WorkspaceMember[]; + config: WorkScheduleConfig; +}): WorkScheduleEntry[] { + const defaultShift = getDefaultWorkShiftOption(config.shifts); + const existingEntryIds = new Set( + schedule.map((entry) => `${entry.workspaceId}:${entry.userId}:${entry.weekday}`), + ); + + const missingEntries = members.flatMap((member) => + weekdays.flatMap((weekday) => { + const entryId = `${member.workspaceId}:${member.userId}:${weekday.key}`; + + if (existingEntryIds.has(entryId)) { + return []; + } + + return { + workspaceId: member.workspaceId, + userId: member.userId, + weekday: weekday.key, + shiftOptionId: defaultShift.id, + }; + }), + ); + + if (missingEntries.length === 0) { + return schedule; + } + + return [...schedule, ...missingEntries]; +} + +export function useWorkScheduleState({ initialSchedule, members, config }: UseWorkScheduleStateParams) { + const [schedule, setSchedule] = useState(() => + completeScheduleEntries({ + schedule: initialSchedule, + members, + config, + }), + ); + + const cycleCell = (userId: string, weekday: WeekdayKey): void => { + setSchedule((current) => { + const completedSchedule = completeScheduleEntries({ + schedule: current, + members, + config, + }); + + return completedSchedule.map((entry) => { + if (entry.userId !== userId || entry.weekday !== weekday) { + return entry; + } + + const nextShift = getNextWorkShiftOption({ + shifts: config.shifts, + currentShiftOptionId: entry.shiftOptionId, + }); + + return { + ...entry, + shiftOptionId: nextShift.id, + }; + }); + }); + }; + + const replaceShiftOption = (fromShiftOptionId: string, toShiftOptionId: string): void => { + setSchedule((current) => { + const completedSchedule = completeScheduleEntries({ + schedule: current, + members, + config, + }); + + return completedSchedule.map((entry) => + entry.shiftOptionId === fromShiftOptionId + ? { ...entry, shiftOptionId: toShiftOptionId } + : entry, + ); + }); + }; + + const completedSchedule = completeScheduleEntries({ + schedule, + members, + config, + }); + + return { + schedule: completedSchedule, + cycleCell, + replaceShiftOption, + }; +} diff --git a/src/features/manage-work-schedule/ui/WorkScheduleBoard.tsx b/src/features/manage-work-schedule/ui/WorkScheduleBoard.tsx new file mode 100644 index 0000000..87301f6 --- /dev/null +++ b/src/features/manage-work-schedule/ui/WorkScheduleBoard.tsx @@ -0,0 +1,209 @@ +'use client'; + +// 수정 가능한 요일별 근무 일정표, 근무 설정, 요일별 요약을 렌더링합니다. +import { useState } from 'react'; +import { + countSchedulesByWeekday, + getWorkMembersByWeekday, + weekdays, + type WorkScheduleConfig, + type WorkScheduleEntry, +} from '@/entities/work-schedule'; +import type { WorkspaceMember } from '@/entities/workspace-member'; +import { useWorkScheduleState } from '../model/use-work-schedule-state'; +import { WorkScheduleCell } from './WorkScheduleCell'; +import { WorkShiftSettingsPanel } from './WorkShiftSettingsPanel'; +import { WorkShiftLegend } from './WorkShiftLegend'; + +interface WorkScheduleBoardProps { + members: WorkspaceMember[]; + config: WorkScheduleConfig; + initialSchedule: WorkScheduleEntry[]; +} + +export function WorkScheduleBoard({ members, config, initialSchedule }: WorkScheduleBoardProps) { + const [scheduleConfig, setScheduleConfig] = useState(config); + const [isSettingsOpen, setIsSettingsOpen] = useState(false); + const { schedule, cycleCell, replaceShiftOption } = useWorkScheduleState({ + initialSchedule, + members, + config: scheduleConfig, + }); + + const handleAddShift = (): void => { + setScheduleConfig((current) => ({ + shifts: [ + ...current.shifts, + { + id: `shift-${crypto.randomUUID()}`, + name: '새 근무', + startTime: '09:00', + endTime: '18:00', + color: 'emerald', + isOff: false, + }, + ], + })); + }; + + const handleUpdateShift = ( + shiftId: string, + nextShift: WorkScheduleConfig['shifts'][number], + ): void => { + setScheduleConfig((current) => ({ + shifts: current.shifts.map((shift) => (shift.id === shiftId ? nextShift : shift)), + })); + }; + + const handleDeleteShift = (shiftId: string): void => { + if (scheduleConfig.shifts.length <= 1) return; + + const nextShifts = scheduleConfig.shifts.filter((shift) => shift.id !== shiftId); + const fallbackShift = nextShifts.find((shift) => !shift.isOff) ?? nextShifts[0]; + + replaceShiftOption(shiftId, fallbackShift.id); + setScheduleConfig({ shifts: nextShifts }); + }; + + const handleMoveShift = (shiftId: string, direction: 'up' | 'down'): void => { + setScheduleConfig((current) => { + const currentIndex = current.shifts.findIndex((shift) => shift.id === shiftId); + const targetIndex = direction === 'up' ? currentIndex - 1 : currentIndex + 1; + + if (currentIndex === -1 || targetIndex < 0 || targetIndex >= current.shifts.length) { + return current; + } + + const nextShifts = [...current.shifts]; + const currentShift = nextShifts[currentIndex]; + nextShifts[currentIndex] = nextShifts[targetIndex]; + nextShifts[targetIndex] = currentShift; + + return { + shifts: nextShifts, + }; + }); + }; + + return ( +
+
+ + +
+ + {isSettingsOpen ? ( + + ) : null} + +
+
+
+
직원
+ {weekdays.map((weekday) => ( +
+ {weekday.label} +
+ ))} +
+ + {members.map((member) => ( +
+
+ + {member.avatarLabel} + + + {member.workspaceNickname} + +
+ + {weekdays.map((weekday) => { + const entry = schedule.find( + (item) => item.userId === member.userId && item.weekday === weekday.key, + ); + const shift = scheduleConfig.shifts.find( + (item) => item.id === entry?.shiftOptionId, + ); + + if (!entry || !shift) return null; + + return ( + cycleCell(member.userId, weekday.key)} + /> + ); + })} +
+ ))} + +
+
합계
+ {weekdays.map((weekday) => { + const counts = countSchedulesByWeekday({ + schedule, + config: scheduleConfig, + weekday: weekday.key, + }); + + return ( +
+ {scheduleConfig.shifts.map((shift) => ( +

+ {shift.name} {counts[shift.id] ?? 0} +

+ ))} +
+ ); + })} +
+
+
+ +
+ {weekdays.map((weekday) => { + const workMembers = getWorkMembersByWeekday({ + schedule, + members, + config: scheduleConfig, + weekday: weekday.key, + }); + + return ( +
+

{weekday.label} 근무자

+
+ {workMembers.map((member) => ( + + {member.avatarLabel} + + ))} +
+

{workMembers.length}명 근무

+
+ ); + })} +
+
+ ); +} diff --git a/src/features/manage-work-schedule/ui/WorkScheduleCell.tsx b/src/features/manage-work-schedule/ui/WorkScheduleCell.tsx new file mode 100644 index 0000000..7544bf5 --- /dev/null +++ b/src/features/manage-work-schedule/ui/WorkScheduleCell.tsx @@ -0,0 +1,21 @@ +// 클릭하면 설정된 근무 옵션을 순서대로 전환하는 일정표 셀입니다. +import type { WorkShiftOption } from '@/entities/work-schedule'; +import { WorkShiftBadge } from './WorkShiftBadge'; + +interface WorkScheduleCellProps { + shift: WorkShiftOption; + onCycle: () => void; +} + +export function WorkScheduleCell({ shift, onCycle }: WorkScheduleCellProps) { + return ( + + ); +} diff --git a/src/features/manage-work-schedule/ui/WorkShiftBadge.tsx b/src/features/manage-work-schedule/ui/WorkShiftBadge.tsx new file mode 100644 index 0000000..2c8f7e8 --- /dev/null +++ b/src/features/manage-work-schedule/ui/WorkShiftBadge.tsx @@ -0,0 +1,26 @@ +// 설정된 근무 옵션 하나를 시각적으로 보여주는 배지입니다. +import type { WorkShiftOption } from '@/entities/work-schedule'; + +const colorClassName: Record = { + sky: 'bg-sky-100 text-sky-700', + violet: 'bg-violet-100 text-violet-700', + amber: 'bg-amber-100 text-amber-700', + slate: 'bg-slate-100 text-slate-400', + emerald: 'bg-emerald-100 text-emerald-700', + rose: 'bg-rose-100 text-rose-700', +}; + +interface WorkShiftBadgeProps { + shift: WorkShiftOption; +} + +export function WorkShiftBadge({ shift }: WorkShiftBadgeProps) { + return ( + + {shift.name} + {shift.startTime ? {shift.startTime} : null} + + ); +} diff --git a/src/features/manage-work-schedule/ui/WorkShiftLegend.tsx b/src/features/manage-work-schedule/ui/WorkShiftLegend.tsx new file mode 100644 index 0000000..18e5c3e --- /dev/null +++ b/src/features/manage-work-schedule/ui/WorkShiftLegend.tsx @@ -0,0 +1,24 @@ +// 활성화된 근무 옵션과 설정된 시간을 표시합니다. +import type { WorkScheduleConfig } from '@/entities/work-schedule'; +import { WorkShiftBadge } from './WorkShiftBadge'; + +interface WorkShiftLegendProps { + config: WorkScheduleConfig; +} + +export function WorkShiftLegend({ config }: WorkShiftLegendProps) { + return ( +
+ {config.shifts.map((shift) => ( +
+ + {shift.startTime && shift.endTime ? ( + + {shift.startTime}-{shift.endTime} + + ) : null} +
+ ))} +
+ ); +} diff --git a/src/features/manage-work-schedule/ui/WorkShiftSettingsPanel.tsx b/src/features/manage-work-schedule/ui/WorkShiftSettingsPanel.tsx new file mode 100644 index 0000000..3a0e59a --- /dev/null +++ b/src/features/manage-work-schedule/ui/WorkShiftSettingsPanel.tsx @@ -0,0 +1,169 @@ +// 근무 옵션을 추가, 제거, 정렬하고 시간을 설정할 수 있는 인라인 편집기입니다. +import { ChevronDown, ChevronUp } from 'lucide-react'; +import type { + WorkScheduleConfig, + WorkShiftColor, + WorkShiftOption, +} from '@/entities/work-schedule'; + +const shiftColors: WorkShiftColor[] = ['sky', 'violet', 'amber', 'slate', 'emerald', 'rose']; + +interface WorkShiftSettingsPanelProps { + config: WorkScheduleConfig; + onAddShift: () => void; + onDeleteShift: (shiftId: string) => void; + onMoveShift: (shiftId: string, direction: 'up' | 'down') => void; + onUpdateShift: (shiftId: string, nextShift: WorkShiftOption) => void; +} + +export function WorkShiftSettingsPanel({ + config, + onAddShift, + onDeleteShift, + onMoveShift, + onUpdateShift, +}: WorkShiftSettingsPanelProps) { + return ( +
+
+
+

근무 유형 설정

+

+ 매장 운영 방식에 맞게 교대 유형과 근무 시간을 조정합니다. +

+
+ + +
+ +
+ {config.shifts.map((shift, index) => ( +
+ + + + + + + + + + +
+ + + + + +
+
+ ))} +
+
+ ); +} \ No newline at end of file diff --git a/src/views/store-operation/work-schedule/index.ts b/src/views/store-operation/work-schedule/index.ts new file mode 100644 index 0000000..42a747c --- /dev/null +++ b/src/views/store-operation/work-schedule/index.ts @@ -0,0 +1,2 @@ +// 매장 근무 일정 뷰 슬라이스의 공개 API입니다. +export { WorkScheduleView } from './ui/WorkScheduleView'; diff --git a/src/views/store-operation/work-schedule/ui/WorkScheduleView.tsx b/src/views/store-operation/work-schedule/ui/WorkScheduleView.tsx new file mode 100644 index 0000000..be8b5ef --- /dev/null +++ b/src/views/store-operation/work-schedule/ui/WorkScheduleView.tsx @@ -0,0 +1,33 @@ +'use client'; + +// 워크스페이스의 목업 매장 근무 일정 화면을 구성합니다. +import { createInitialWorkSchedule, mockWorkScheduleConfig } from '@/entities/work-schedule'; +import { mockWorkspaceMembers } from '@/entities/workspace-member'; +import { WorkScheduleBoard } from '@/features/manage-work-schedule'; + +interface WorkScheduleViewProps { + workspaceId: string; +} + +export function WorkScheduleView({ workspaceId }: WorkScheduleViewProps) { + const initialSchedule = createInitialWorkSchedule({ + workspaceId, + members: mockWorkspaceMembers, + config: mockWorkScheduleConfig, + }); + + return ( +
+
+

업무 스케줄

+

셀을 클릭하면 근무 유형이 변경됩니다

+
+ + +
+ ); +} diff --git a/src/widgets/landing/landing-header/ui/LandingHeader.tsx b/src/widgets/landing/landing-header/ui/LandingHeader.tsx index e6b2943..8e4ba37 100644 --- a/src/widgets/landing/landing-header/ui/LandingHeader.tsx +++ b/src/widgets/landing/landing-header/ui/LandingHeader.tsx @@ -2,8 +2,9 @@ // 랜딩 상단 네비게이션 — 스크롤 시 배경 블러와 그림자가 나타나는 sticky 헤더 import { useEffect, useState } from 'react'; +import Image from 'next/image'; import Link from 'next/link'; -import { ArrowRight, Boxes } from 'lucide-react'; +import { ArrowRight } from 'lucide-react'; import { cn } from '@/shared/lib/utils'; const SCROLL_THRESHOLD = 8; @@ -33,10 +34,7 @@ export default function LandingHeader() { >