-
Notifications
You must be signed in to change notification settings - Fork 3
feat:매장 운영 근무 스케줄 DB 연동(#42) #42
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 |
|---|---|---|
| @@ -1,4 +1,9 @@ | ||
| // 워크스페이스 근무 일정 페이지의 라우트 진입점입니다. | ||
| // 현재 주의 멤버, 근무유형, 스케줄 데이터를 병렬 조회해 근무 스케줄 화면에 전달하는 서버 페이지입니다. | ||
| import { getWorkspaceMembersByWorkspaceId } from '@/entities/workspace-member/api/get-workspace-members-by-id'; | ||
| import { getWorkScheduleEntriesByWeek } from '@/entities/work-schedule/api/get-work-schedule-entries-by-week'; | ||
| import { getWorkShiftTypesByWorkspaceId } from '@/entities/work-schedule/api/get-work-shift-types-by-workspace-id'; | ||
| import { ensureWeeklyWorkScheduleEntries } from '@/entities/work-schedule/api/ensure-weekly-work-schedule-entries'; | ||
|
Comment on lines
+2
to
+5
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. 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win
♻️ 제안 리팩터-import { getWorkspaceMembersByWorkspaceId } from '`@/entities/workspace-member/api/get-workspace-members-by-id`';
-import { getWorkScheduleEntriesByWeek } from '`@/entities/work-schedule/api/get-work-schedule-entries-by-week`';
-import { getWorkShiftTypesByWorkspaceId } from '`@/entities/work-schedule/api/get-work-shift-types-by-workspace-id`';
-import { ensureWeeklyWorkScheduleEntries } from '`@/entities/work-schedule/api/ensure-weekly-work-schedule-entries`';
+import { getDashboardWorkSchedule } from '`@/entities/work-schedule/api/get-dashboard-work-schedule`';
import { getCurrentWeekRange } from '`@/entities/work-schedule`';
import { WorkScheduleView } from '`@/views/store-operation/work-schedule`';
...
const { workspaceId } = await params;
- const { startDate, endDate } = getCurrentWeekRange();
- await ensureWeeklyWorkScheduleEntries(workspaceId, startDate);
- const [members, shifts, schedule] = await Promise.all([
- getWorkspaceMembersByWorkspaceId(workspaceId),
- getWorkShiftTypesByWorkspaceId(workspaceId),
- getWorkScheduleEntriesByWeek(workspaceId, startDate, endDate),
- ]);
+ const { startDate } = getCurrentWeekRange();
+ const { members, shifts, schedule } = await getDashboardWorkSchedule(workspaceId);Also applies to: 17-23 🤖 Prompt for AI Agents |
||
| import { getCurrentWeekRange } from '@/entities/work-schedule'; | ||
| import { WorkScheduleView } from '@/views/store-operation/work-schedule'; | ||
|
|
||
| interface WorkSchedulePageProps { | ||
|
|
@@ -9,6 +14,21 @@ interface WorkSchedulePageProps { | |
|
|
||
| export default async function WorkSchedulePage({ params }: WorkSchedulePageProps) { | ||
| const { workspaceId } = await params; | ||
| const { startDate, endDate } = getCurrentWeekRange(); | ||
| await ensureWeeklyWorkScheduleEntries(workspaceId, startDate); | ||
| const [members, shifts, schedule] = await Promise.all([ | ||
| getWorkspaceMembersByWorkspaceId(workspaceId), | ||
| getWorkShiftTypesByWorkspaceId(workspaceId), | ||
| getWorkScheduleEntriesByWeek(workspaceId, startDate, endDate), | ||
| ]); | ||
|
|
||
| return <WorkScheduleView workspaceId={workspaceId} />; | ||
| return ( | ||
| <WorkScheduleView | ||
| workspaceId={workspaceId} | ||
| members={members} | ||
| shifts={shifts} | ||
| schedule={schedule} | ||
| weekStartDate={startDate} | ||
| /> | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,18 +1,44 @@ | ||
| // 대시보드 레이아웃 조회 — DB 연동 자리. | ||
| // 저장분이 없으면(신규) 빈 레이아웃으로 시작한다 — 템플릿 기반 기본값/폴백은 두지 않는다. | ||
| // 현재 사용자의 워크스페이스별 대시보드 레이아웃을 조회하고, 저장값이 없으면 빈 레이아웃을 반환합니다. | ||
| import { getCurrentUserId } from '@/shared/api/supabase/current-user'; | ||
| import { createSupabaseServerClient } from '@/shared/api/supabase/server'; | ||
| import type { Layout, LayoutItem } from 'react-grid-layout'; | ||
|
|
||
| import type { DashboardLayoutState } from '../model/dashboard-layout.types'; | ||
|
|
||
| // TODO: DB 연동 — WORKSPACE_LAYOUTS에서 (workspace_id, user_id(세션), page_type) 기준 select | ||
| function isLayoutItem(value: unknown): value is LayoutItem { | ||
| if (!value || typeof value !== 'object') return false; | ||
|
|
||
| const item = value as Record<string, unknown>; | ||
| return ( | ||
| typeof item.i === 'string' && | ||
| typeof item.x === 'number' && | ||
| typeof item.y === 'number' && | ||
| typeof item.w === 'number' && | ||
| typeof item.h === 'number' | ||
| ); | ||
| } | ||
|
|
||
| function toDashboardLayout(value: unknown): Layout { | ||
| return Array.isArray(value) && value.every(isLayoutItem) ? value : []; | ||
| } | ||
|
|
||
| export async function getDashboardLayout( | ||
| workspaceId: string, | ||
| pageType: string, | ||
| ): Promise<DashboardLayoutState> { | ||
| void workspaceId; | ||
| const supabase = await createSupabaseServerClient(); | ||
| const userId = await getCurrentUserId(); | ||
| const { data, error } = await supabase | ||
| .from('user_dashboard_layouts') | ||
| .select('layout') | ||
| .eq('workspace_id', workspaceId) | ||
| .eq('user_id', userId) | ||
| .maybeSingle(); | ||
|
|
||
| if (error) throw new Error(`대시보드 레이아웃 조회에 실패했습니다: ${error.message}`); | ||
|
|
||
| void pageType; | ||
| // 임시 목 저장분 — DB의 layout jsonb를 흉내낸다. 위치(i,x,y,w,h)만 담고, | ||
| // 제약(minW/minH)은 저장하지 않는다(렌더 시 카탈로그에서 머지됨). | ||
| return { | ||
| layout: [], | ||
| layout: toDashboardLayout(data?.layout), | ||
| }; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,16 +1,31 @@ | ||
| // 대시보드 레이아웃 저장 — DB 연동 자리(서버액션). | ||
| // layout jsonb 한 행 = DashboardLayoutState 통째. 드래그 중 잦은 호출은 debounce 필요. | ||
| // 현재 사용자의 워크스페이스별 대시보드 레이아웃을 JSONB 한 행으로 upsert하는 서버 액션입니다. | ||
| 'use server'; | ||
|
|
||
| import { getCurrentUserId } from '@/shared/api/supabase/current-user'; | ||
| import { createSupabaseServerClient } from '@/shared/api/supabase/server'; | ||
| import type { LayoutItem } from 'react-grid-layout'; | ||
| import type { DashboardLayoutState } from '../model/dashboard-layout.types'; | ||
|
|
||
| // TODO: DB 연동 — WORKSPACE_LAYOUTS upsert (workspace_id, user_id(세션), page_type, layout) | ||
| function toStoredLayout(layout: DashboardLayoutState['layout']) { | ||
| return layout.map(({ i, x, y, w, h }: LayoutItem) => ({ i, x, y, w, h })); | ||
| } | ||
|
|
||
| export async function saveDashboardLayout( | ||
| workspaceId: string, | ||
| pageType: string, | ||
| state: DashboardLayoutState, | ||
| ): Promise<void> { | ||
| void workspaceId; | ||
| const supabase = await createSupabaseServerClient(); | ||
| const userId = await getCurrentUserId(); | ||
| const { error } = await supabase.from('user_dashboard_layouts').upsert( | ||
| { | ||
| user_id: userId, | ||
| workspace_id: workspaceId, | ||
| layout: toStoredLayout(state.layout), | ||
| }, | ||
| { onConflict: 'user_id,workspace_id' }, | ||
| ); | ||
|
|
||
| if (error) throw new Error(`대시보드 레이아웃 저장에 실패했습니다: ${error.message}`); | ||
| void pageType; | ||
| void state; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,2 @@ | ||
| // dashboard-layout 엔티티의 Public API — 개인 대시보드 레이아웃 조회/저장. | ||
| export type { DashboardLayoutState } from './model/dashboard-layout.types'; | ||
| export { getDashboardLayout } from './api/get-dashboard-layout'; | ||
| export { saveDashboardLayout } from './api/save-dashboard-layout'; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| // 이번 주에 아직 배정되지 않은 멤버·요일 조합을 기본 근무유형으로만 생성해 화면과 DB 기준을 맞춥니다. | ||
| import { getCurrentUserId } from '@/shared/api/supabase/current-user'; | ||
| import { createSupabaseServerClient } from '@/shared/api/supabase/server'; | ||
| import { getDefaultWorkShiftOption } from '../lib/get-default-work-shift-option'; | ||
| import { getWorkDateByWeekday } from '../lib/work-date'; | ||
| import { weekdays } from '../model/weekdays'; | ||
| import { getWorkShiftTypesByWorkspaceId } from './get-work-shift-types-by-workspace-id'; | ||
| import { getWorkspaceMembersByWorkspaceId } from '@/entities/workspace-member/api/get-workspace-members-by-id'; | ||
|
|
||
| export async function ensureWeeklyWorkScheduleEntries( | ||
| workspaceId: string, | ||
| weekStartDate: string, | ||
| ): Promise<void> { | ||
| const [members, shifts] = await Promise.all([ | ||
| getWorkspaceMembersByWorkspaceId(workspaceId), | ||
| getWorkShiftTypesByWorkspaceId(workspaceId), | ||
| ]); | ||
| const defaultShift = getDefaultWorkShiftOption(shifts); | ||
|
|
||
| if (!defaultShift || members.length === 0) return; | ||
|
|
||
| const supabase = await createSupabaseServerClient(); | ||
| const weekEndDate = getWorkDateByWeekday(weekStartDate, 'sunday'); | ||
| const { count, error: countError } = await supabase | ||
| .from('work_schedule_entries') | ||
| .select('id', { count: 'exact', head: true }) | ||
| .eq('workspace_id', workspaceId) | ||
| .gte('work_date', weekStartDate) | ||
| .lte('work_date', weekEndDate); | ||
|
|
||
| if (countError) throw new Error(`근무 스케줄 수 조회에 실패했습니다: ${countError.message}`); | ||
| if (count === members.length * weekdays.length) return; | ||
|
|
||
| const createdBy = await getCurrentUserId(); | ||
| const entries = members.flatMap((member) => | ||
| weekdays.map((weekday) => ({ | ||
| workspace_id: workspaceId, | ||
| user_id: member.userId, | ||
| work_date: getWorkDateByWeekday(weekStartDate, weekday.key), | ||
| shift_type_id: defaultShift.id, | ||
| created_by: createdBy, | ||
| })), | ||
| ); | ||
|
|
||
| const { error } = await supabase.from('work_schedule_entries').upsert(entries, { | ||
| onConflict: 'workspace_id,user_id,work_date', | ||
| ignoreDuplicates: true, | ||
| }); | ||
|
|
||
| if (error) throw new Error(`기본 근무 스케줄 생성에 실패했습니다: ${error.message}`); | ||
| } | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| 'use server'; | ||
|
|
||
| // 대시보드 근무 스케줄 위젯이 현재 주의 멤버, 근무유형, 일정 데이터를 한 번에 조회하는 서버 액션입니다. | ||
| import { getWorkspaceMembersByWorkspaceId } from '@/entities/workspace-member/api/get-workspace-members-by-id'; | ||
| import { getCurrentWeekRange } from '../lib/work-date'; | ||
| import { getWorkScheduleEntriesByWeek } from './get-work-schedule-entries-by-week'; | ||
| import { getWorkShiftTypesByWorkspaceId } from './get-work-shift-types-by-workspace-id'; | ||
| import { ensureWeeklyWorkScheduleEntries } from './ensure-weekly-work-schedule-entries'; | ||
|
|
||
| export async function getDashboardWorkSchedule(workspaceId: string) { | ||
| const { startDate, endDate } = getCurrentWeekRange(); | ||
| await ensureWeeklyWorkScheduleEntries(workspaceId, startDate); | ||
| const [members, shifts, schedule] = await Promise.all([ | ||
| getWorkspaceMembersByWorkspaceId(workspaceId), | ||
| getWorkShiftTypesByWorkspaceId(workspaceId), | ||
| getWorkScheduleEntriesByWeek(workspaceId, startDate, endDate), | ||
| ]); | ||
|
|
||
| return { members, shifts, schedule }; | ||
| } | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| // 워크스페이스의 지정된 한 주 스케줄을 조회하고, DB 날짜를 월~일 UI 키로 변환하는 서버 조회 함수입니다. | ||
| import { cache } from 'react'; | ||
| import { createSupabaseServerClient } from '@/shared/api/supabase/server'; | ||
| import { getWeekdayFromWorkDate } from '../lib/work-date'; | ||
| import type { WorkScheduleEntry } from '../model/work-schedule.types'; | ||
|
|
||
| export const getWorkScheduleEntriesByWeek = cache( | ||
| async (workspaceId: string, startDate: string, endDate: string): Promise<WorkScheduleEntry[]> => { | ||
| const supabase = await createSupabaseServerClient(); | ||
| const { data, error } = await supabase | ||
| .from('work_schedule_entries') | ||
| .select('workspace_id, user_id, work_date, shift_type_id') | ||
| .eq('workspace_id', workspaceId) | ||
| .gte('work_date', startDate) | ||
| .lte('work_date', endDate); | ||
|
|
||
| if (error) { | ||
| throw new Error(`근무 스케줄 조회에 실패했습니다: ${error.message}`); | ||
| } | ||
|
|
||
| return (data ?? []).map((entry) => ({ | ||
| workspaceId: entry.workspace_id, | ||
| userId: entry.user_id, | ||
| workDate: entry.work_date, | ||
| weekday: getWeekdayFromWorkDate(entry.work_date), | ||
| shiftTypeId: entry.shift_type_id, | ||
| })); | ||
| }, | ||
| ); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| // 워크스페이스별 근무유형을 정렬 순서대로 조회해 화면용 타입으로 변환하는 서버 조회 함수입니다. | ||
| import { cache } from 'react'; | ||
| import { createSupabaseServerClient } from '@/shared/api/supabase/server'; | ||
| import type { WorkShiftColor, WorkShiftOption } from '../model/work-schedule.types'; | ||
|
|
||
| function toTime(value: string | null): string | null { | ||
| return value ? value.slice(0, 5) : null; | ||
| } | ||
|
|
||
| export const getWorkShiftTypesByWorkspaceId = cache( | ||
| async (workspaceId: string): Promise<WorkShiftOption[]> => { | ||
| const supabase = await createSupabaseServerClient(); | ||
| const { data, error } = await supabase | ||
| .from('work_shift_types') | ||
| .select('id, code, name, start_time, end_time, ends_next_day, color, is_off') | ||
| .eq('workspace_id', workspaceId) | ||
| .order('sort_order'); | ||
|
|
||
| if (error) { | ||
| throw new Error(`근무 유형 조회에 실패했습니다: ${error.message}`); | ||
| } | ||
|
|
||
| return (data ?? []).map((shift) => ({ | ||
| id: shift.id, | ||
| code: shift.code, | ||
| name: shift.name, | ||
| startTime: toTime(shift.start_time), | ||
| endTime: toTime(shift.end_time), | ||
| endsNextDay: shift.ends_next_day, | ||
| color: shift.color as WorkShiftColor, | ||
| isOff: shift.is_off, | ||
| })); | ||
| }, | ||
| ); |
Uh oh!
There was an error while loading. Please reload this page.