-
Notifications
You must be signed in to change notification settings - Fork 3
feat: 매장 운영 업무 스케줄 구현 (#11) #12
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -26,6 +26,10 @@ | |
|
|
||
| - | ||
|
|
||
| ### 실행화면 | ||
|
|
||
| - | ||
|
|
||
| ## 테스트 | ||
|
|
||
| - [ ] 로컬 실행 확인 | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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" | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 <WorkspaceShell workspaceId={workspaceId}>{children}</WorkspaceShell>; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 <WorkScheduleView workspaceId={workspaceId} />; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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'; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<string, number> { | ||
| const counts = Object.fromEntries(config.shifts.map((shift) => [shift.id, 0])); | ||
|
|
||
| const uniqueEntries = new Map<string, WorkScheduleEntry>(); | ||
|
|
||
| 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; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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, | ||
| })), | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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]; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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]; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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)); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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, | ||
| }, | ||
|
Comment on lines
+22
to
+29
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win 🧩 Analysis chain🌐 Web query:
💡 Result: In date-fns, the handling of "24:00" depends on the parsing function used [1][2][3]. For the Citations:
🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '\n## file outline\n'
ast-grep outline src/entities/work-schedule/model/mock-work-schedule-config.ts --view expanded || true
printf '\n## file contents\n'
cat -n src/entities/work-schedule/model/mock-work-schedule-config.ts | sed -n '1,120p'
printf '\n## search for endTime usage and time parsing\n'
rg -n "endTime|startTime|24:00|parseISO|parse\\(|format\\(|HH:mm|time" src | sed -n '1,220p'Repository: TeampleRun/syncly Length of output: 4027 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '\n## related files\n'
fd -a ".*work-schedule.*|.*schedule.*" src || trueRepository: TeampleRun/syncly Length of output: 1043 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '\n## related files by exact name\n'
git ls-files 'src/**' | rg 'work-schedule|schedule' || trueRepository: TeampleRun/syncly Length of output: 1270 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '\n## work-schedule types\n'
cat -n src/entities/work-schedule/model/work-schedule.types.ts | sed -n '1,120p'
printf '\n## settings panel\n'
cat -n src/features/manage-work-schedule/ui/WorkShiftSettingsPanel.tsx | sed -n '1,220p'
printf '\n## board and legend\n'
cat -n src/features/manage-work-schedule/ui/WorkScheduleBoard.tsx | sed -n '1,220p'
printf '\n---\n'
cat -n src/features/manage-work-schedule/ui/WorkShiftLegend.tsx | sed -n '1,220p'
printf '\n## create initial config\n'
cat -n src/entities/work-schedule/lib/create-initial-work-schedule.ts | sed -n '1,220p'
printf '\n## find any time validation/parsing libraries around schedule\n'
rg -n "parseISO|parse\\(|isValid|HH:mm|type=\"time\"|endTime|startTime" src/entities src/features src/views | sed -n '1,240p'Repository: TeampleRun/syncly Length of output: 21847 🌐 Web query:
💡 Result: In HTML, the value 24:00 is not a valid time string for an element [1]. According to the HTML specification, a valid time string uses a 24-hour clock where the hour must be in the range 00 through 23 [1]. The value 00:00 represents midnight at the beginning of a day, and there is no representation of "end of day" as 24:00 within the standard time input format [1][2]. If you attempt to set the value attribute or the min/max attributes to 24:00, it will not be recognized as a valid time [3][4]. Consequently, if a browser performs client-side validation on an , it will treat 24:00 as invalid [4]. To handle intervals that include midnight or the end of a day, developers typically use a checkbox to indicate "entire day" or alternative input methods (such as text inputs with custom validation) [2][5]. Citations:
🤖 Prompt for AI Agents |
||
| { | ||
| id: 'shift-off', | ||
| name: '휴무', | ||
| startTime: null, | ||
| endTime: null, | ||
| color: 'slate', | ||
| isOff: true, | ||
| }, | ||
| ], | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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']; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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'; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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', | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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', | ||
| }, | ||
| ]; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| // Supabase 데이터가 연결되기 전까지 사용하는 목업 일정용 워크스페이스 멤버 형태입니다. | ||
| export interface WorkspaceMember { | ||
| workspaceId: string; | ||
| userId: string; | ||
| workspaceNickname: string; | ||
| avatarLabel: string; | ||
| role: 'owner' | 'member'; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| // 목업 워크스페이스 데이터와 타입의 공개 API입니다. | ||
| export type { Workspace } from './model/workspace.types'; | ||
| export { mockWorkspace } from './model/mock-workspace'; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| // 워크스페이스 API 연결 전 사이드바에 표시하는 현재 워크스페이스 목업입니다. | ||
| import type { Workspace } from './workspace.types'; | ||
|
|
||
| export const mockWorkspace: Workspace = { | ||
| id: 'test', | ||
| name: '카페 그레이 운영', | ||
| purpose: 'store-operation', | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| // Supabase 워크스페이스 데이터 연결 전 shell에서 사용하는 워크스페이스 타입입니다. | ||
| export interface Workspace { | ||
| id: string; | ||
| name: string; | ||
| purpose: 'store-operation' | 'team-project' | 'side-project'; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| // 근무 일정 관리 기능의 공개 API입니다. | ||
| export { WorkScheduleBoard } from './ui/WorkScheduleBoard'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
Repository: TeampleRun/syncly
Length of output: 2867
🏁 Script executed:
Repository: TeampleRun/syncly
Length of output: 2375
빈
shifts가드를 추가하세요.shifts가 비어 있으면shifts[nextIndex]가undefined가 되어WorkShiftOption반환 계약을 깨뜨립니다.getDefaultWorkShiftOption도 같은 문제가 있으니 두 헬퍼 모두 빈 배열을 먼저 처리해야 합니다.🤖 Prompt for AI Agents