feat:매장 운영 근무 스케줄 DB 연동(#42) - #42
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (7)
💤 Files with no reviewable changes (1)
📝 WalkthroughWalkthrough워크스페이스와 대시보드 레이아웃을 Supabase 데이터로 전환하고, 근무유형·주간 스케줄 조회와 편집을 서버 액션에 연결했습니다. 스케줄 모델은 Changes워크스페이스 및 근무 스케줄 실데이터 연동
Estimated code review effort: 5 (Critical) | ~120 minutes Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
88ddcd8 to
ab83d00
Compare
There was a problem hiding this comment.
Actionable comments posted: 14
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/features/manage-work-schedule/ui/WorkScheduleBoard.tsx (1)
79-166: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift서버 액션 실패 시 낙관적 로컬 상태가 되돌아가지 않고, 동시 요청 순서도 보장되지 않습니다.
handleCommitShift,handleMoveShift,handleCycleCell은 모두 서버 액션을 호출하기 전에 이미 로컬 상태(scheduleConfig/schedule)를 변경한 뒤, 실패하면toast.error만 띄우고 이전 값으로 되돌리거나 서버 데이터로 재동기화하지 않습니다. 예를 들어handleCommitShift가 실패해도 입력창에는 방금 편집한 값이 그대로 남아 있어, 사용자는 저장되지 않은 편집을 저장된 것으로 오인할 수 있습니다.반면
confirmDeleteShift(101-116줄)와handleAddShift(60-68줄)는 서버 호출 성공 후에만 로컬 상태를 갱신해 이 문제가 없습니다.추가로, 세 핸들러 모두 특정 대상(shift/셀)에 대해 진행 중인 요청을 추적하지 않아, 동일 대상에 빠르게 연속 상호작용(연속 클릭, 필드 간 빠른 탭 이동 등)이 발생하면 여러 서버 호출이 동시에 진행될 수 있습니다. 이때 먼저 보낸(더 오래된 상태의) 요청이 나중에 완료되면, 최신 로컬 상태보다 오래된 스냅샷이 DB에 최종적으로 기록되는 out-of-order 쓰기가 발생할 수 있습니다(세 핸들러 모두 부분 필드가 아닌 전체 스냅샷을 전송하므로 실제로 영향이 있습니다).
같은 대상에 대해 요청이 진행 중일 때 컨트롤을 비활성화하거나, 요청별 시퀀스 번호로 오래된 응답을 무시하는 가드를 두고, 실패 시에는 이전 상태로 롤백(또는 서버 값으로 재조회)하는 처리를 추가하는 것을 권장합니다.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/features/manage-work-schedule/ui/WorkScheduleBoard.tsx` around lines 79 - 166, Update handleCommitShift, handleMoveShift, and handleCycleCell to prevent concurrent requests for the same shift or cell, preserving request ordering through per-target disabling or sequence guards. Capture each handler’s previous local state before its optimistic update, and on server-action failure restore that state or resynchronize from the server instead of only showing a toast; ensure stale responses cannot overwrite newer local changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/app/workspaces/`[workspaceId]/settings/page.tsx:
- Line 26: Update the settings page around getWorkspaceById so members,
currentUserId, and currentNickname are derived from the loaded workspace’s
actual member and current-user data rather than mock values. Ensure member
management and profile tabs render information belonging to the requested
workspace, preserving the existing UI contracts for these values.
In `@src/entities/work-schedule/api/ensure-weekly-work-schedule-entries.ts`:
- Around line 10-40: Optimize ensureWeeklyWorkScheduleEntries by checking the
existing work_schedule_entries count for the workspace and requested week before
constructing or upserting entries. If the count equals members.length multiplied
by weekdays.length, return without performing the upsert; otherwise preserve the
existing default-shift guard and upsert behavior for missing entries.
In `@src/entities/work-schedule/api/work-schedule-actions.ts`:
- Around line 150-170: Update reorderWorkShiftTypes to replace the parallel
per-row updates with a single RPC call, following the
replace_and_delete_work_shift_type pattern, so all sort_order changes execute
atomically in one transaction. Pass the workspaceId and shiftTypeIds through the
RPC, preserve validation and error propagation, and revalidate the workspace
only after a successful call.
- Around line 85-113: createWorkShiftType의 마지막 sort_order 조회 후 +1을 계산해 insert하는
비원자적 흐름을 제거하세요. 동시 호출에서도 workspace별 sort_order가 중복되지 않도록 데이터베이스 트랜잭션, 원자적
RPC/시퀀스, 또는 충돌 재시도 가능한 방식으로 계산 및 삽입을 처리하고, 기존 WorkShiftOption 반환 동작은 유지하세요.
- Around line 55-83: Update saveWorkScheduleEntry so existing rows matched by
workspace_id,user_id,work_date update only shift_type_id, while newly inserted
rows set created_by from getCurrentUserId(). Replace the current upsert payload
behavior with a conflict-safe insert/update flow that preserves the original
created_by value.
- Around line 12-17: Replace the custom regex in uuidSchema with z.guid().
Strengthen timeSchema to validate HH:MM ranges, allowing hours 00–23 and minutes
00–59 instead of accepting any two-digit values.
In `@src/entities/workspace-member/api/get-workspace-members-by-id.ts`:
- Around line 11-34: Update the workspace-member retrieval flow to replace the
separate profiles query with one Supabase query from workspace_members using an
embedded profiles relation through workspace_members_user_id_fkey, selecting the
existing membership fields plus id, email, and real_name. Preserve workspaceId
filtering, joined_at ordering, empty-result behavior, and the existing error
handling semantics while mapping the nested profile data into the current
response shape.
In `@src/features/dashboard/edit-layout/model/useDashboardLayout.ts`:
- Around line 8-9: Update the autosave flow in useDashboardLayout to prevent
overlapping saveDashboardLayout requests from allowing stale layouts to
overwrite newer ones, using debouncing or serialized latest-request-wins
handling. Replace the unhandled void invocation with explicit failure handling,
exposing save errors to the UI or retrying failed saves while preserving the
latest layout.
In `@src/shared/api/supabase/current-user.ts`:
- Around line 5-12: Update getCurrentUserId so the DEV_USER_ID fallback is only
available outside production; when no authenticated user exists in production,
throw an authentication error instead of returning DEV_USER_ID, while preserving
the existing development fallback.
In
`@src/widgets/store-operation/dashboard-work-schedule/ui/WorkScheduleSummary.tsx`:
- Around line 79-102: Replace the manual useEffect/useState loading flow around
getDashboardWorkSchedule with TanStack Query’s useQuery, using workspaceId in
the query key and getDashboardWorkSchedule as the query function. Derive
loading, error, and data rendering from the query result, and remove the
cancelled flag plus manual setData/setHasError handling.
- Around line 82-102: Reset the previous workspace state at the start of the
useEffect tied to workspaceId by clearing data and setting hasError to false
before calling getDashboardWorkSchedule. Keep the existing cancellation handling
and success/error updates unchanged so the UI shows the loading state until the
new workspace request completes.
In `@supabase/migrations/20260713000000_add_work_shift_types.sql`:
- Around line 101-121: Remove the unconditional dev_full_access policy from the
work_shift_types RLS migration, or restrict it to an explicitly non-production
development environment so it cannot override the member and owner policies.
Preserve work_shift_types_select_member and work_shift_types_write_owner as the
effective access controls, and ensure the temporary-policy removal is tracked if
it remains necessary during development.
- Around line 73-99: Update the work_schedule_entries migration to use a
zero-downtime sequence: add the shift_type_id foreign key as NOT VALID and defer
VALIDATE CONSTRAINT to a later migration, replace immediate SET NOT NULL with a
validated NOT VALID CHECK constraint before the not-null transition, and defer
dropping shift_type until older application versions no longer reference it.
- Around line 124-166: Update replace_and_delete_work_shift_type so the DELETE
result is validated instead of always succeeding. After the delete from
public.work_shift_types, capture ROW_COUNT with GET DIAGNOSTICS and raise an
exception when zero rows were deleted, preserving the existing successful path
when the target shift type is removed.
---
Outside diff comments:
In `@src/features/manage-work-schedule/ui/WorkScheduleBoard.tsx`:
- Around line 79-166: Update handleCommitShift, handleMoveShift, and
handleCycleCell to prevent concurrent requests for the same shift or cell,
preserving request ordering through per-target disabling or sequence guards.
Capture each handler’s previous local state before its optimistic update, and on
server-action failure restore that state or resynchronize from the server
instead of only showing a toast; ensure stale responses cannot overwrite newer
local changes.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: afa962c2-5f44-4d3d-9d7c-a92182d61bd9
📒 Files selected for processing (33)
src/app/workspaces/[workspaceId]/dashboard/page.tsxsrc/app/workspaces/[workspaceId]/layout.tsxsrc/app/workspaces/[workspaceId]/page.tsxsrc/app/workspaces/[workspaceId]/settings/page.tsxsrc/app/workspaces/[workspaceId]/work-schedule/page.tsxsrc/entities/dashboard-layout/api/get-dashboard-layout.tssrc/entities/dashboard-layout/api/save-dashboard-layout.tssrc/entities/dashboard-layout/index.tssrc/entities/work-schedule/api/ensure-weekly-work-schedule-entries.tssrc/entities/work-schedule/api/get-dashboard-work-schedule.tssrc/entities/work-schedule/api/get-work-schedule-entries-by-week.tssrc/entities/work-schedule/api/get-work-shift-types-by-workspace-id.tssrc/entities/work-schedule/api/work-schedule-actions.tssrc/entities/work-schedule/index.tssrc/entities/work-schedule/lib/count-schedules-by-weekday.tssrc/entities/work-schedule/lib/create-initial-work-schedule.tssrc/entities/work-schedule/lib/get-next-work-shift-option.tssrc/entities/work-schedule/lib/get-work-members-by-weekday.tssrc/entities/work-schedule/lib/work-date.tssrc/entities/work-schedule/model/mock-work-schedule-config.tssrc/entities/work-schedule/model/work-schedule.types.tssrc/entities/workspace-member/api/get-workspace-members-by-id.tssrc/entities/workspace/api/get-workspace-by-id.tssrc/features/dashboard/edit-layout/model/useDashboardLayout.tssrc/features/manage-work-schedule/model/use-work-schedule-state.tssrc/features/manage-work-schedule/ui/WorkScheduleBoard.tsxsrc/features/manage-work-schedule/ui/WorkShiftSettingsPanel.tsxsrc/shared/api/supabase/current-user.tssrc/shared/model/database.types.tssrc/views/dashboard/ui/DashboardView.tsxsrc/views/store-operation/work-schedule/ui/WorkScheduleView.tsxsrc/widgets/store-operation/dashboard-work-schedule/ui/WorkScheduleSummary.tsxsupabase/migrations/20260713000000_add_work_shift_types.sql
💤 Files with no reviewable changes (1)
- src/entities/dashboard-layout/index.ts
| export async function createWorkShiftType(workspaceId: string): Promise<WorkShiftOption> { | ||
| const parsedWorkspaceId = uuidSchema.parse(workspaceId); | ||
| const supabase = await createSupabaseServerClient(); | ||
| const { data: lastShift, error: sortOrderError } = await supabase | ||
| .from('work_shift_types') | ||
| .select('sort_order') | ||
| .eq('workspace_id', parsedWorkspaceId) | ||
| .order('sort_order', { ascending: false }) | ||
| .limit(1) | ||
| .maybeSingle(); | ||
|
|
||
| if (sortOrderError) | ||
| throw new Error(`근무 유형 순서 조회에 실패했습니다: ${sortOrderError.message}`); | ||
|
|
||
| const { data, error } = await supabase | ||
| .from('work_shift_types') | ||
| .insert({ | ||
| workspace_id: parsedWorkspaceId, | ||
| code: `custom-${crypto.randomUUID()}`, | ||
| name: '새 근무', | ||
| start_time: '09:00', | ||
| end_time: '18:00', | ||
| ends_next_day: false, | ||
| color: 'emerald', | ||
| is_off: false, | ||
| sort_order: (lastShift?.sort_order ?? -1) + 1, | ||
| }) | ||
| .select('id, code, name, start_time, end_time, ends_next_day, color, is_off') | ||
| .single(); |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
sort_order 산출에 TOCTOU 경쟁 조건이 있습니다.
마지막 sort_order를 조회한 뒤 +1로 insert하는 두 단계가 원자적이지 않습니다. 두 명이 동시에 "근무유형 추가"를 호출하면 둘 다 같은 sort_order 값으로 insert될 수 있습니다(DB에 (workspace_id, sort_order) unique 제약이 없어 에러 없이 통과). 결과적으로 정렬 순서가 중복되어 UI 표시 순서가 불명확해질 수 있습니다.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/entities/work-schedule/api/work-schedule-actions.ts` around lines 85 -
113, createWorkShiftType의 마지막 sort_order 조회 후 +1을 계산해 insert하는 비원자적 흐름을 제거하세요.
동시 호출에서도 workspace별 sort_order가 중복되지 않도록 데이터베이스 트랜잭션, 원자적 RPC/시퀀스, 또는 충돌 재시도 가능한
방식으로 계산 및 삽입을 처리하고, 기존 WorkShiftOption 반환 동작은 유지하세요.
| alter table public.work_schedule_entries | ||
| add column shift_type_id uuid references public.work_shift_types(id) on delete restrict; | ||
|
|
||
| -- 기존 open/middle/close/off 문자열을 같은 워크스페이스의 근무유형 FK로 옮긴다. | ||
| update public.work_schedule_entries entry | ||
| set shift_type_id = shift.id | ||
| from public.work_shift_types shift | ||
| where shift.workspace_id = entry.workspace_id | ||
| and shift.code = entry.shift_type; | ||
|
|
||
| do $$ | ||
| begin | ||
| if exists ( | ||
| select 1 | ||
| from public.work_schedule_entries | ||
| where shift_type_id is null | ||
| ) then | ||
| raise exception '근무 스케줄의 shift_type_id 이전에 실패했습니다.'; | ||
| end if; | ||
| end; | ||
| $$; | ||
|
|
||
| alter table public.work_schedule_entries | ||
| alter column shift_type_id set not null; | ||
|
|
||
| alter table public.work_schedule_entries | ||
| drop column shift_type; |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
FK 추가/NOT NULL 설정/컬럼 삭제가 무중단 마이그레이션 관례를 따르지 않습니다.
Static analysis(Squawk)가 지적한 대로:
- 74행: 기존 테이블
work_schedule_entries에 FK 제약을 바로 추가하면 양쪽 테이블에SHARE ROW EXCLUSIVE락과 전체 스캔이 필요해 쓰기가 블로킹됩니다.NOT VALID로 추가한 뒤 별도 마이그레이션에서VALIDATE CONSTRAINT하는 패턴을 권장합니다. - 96행:
SET NOT NULL도 스캔 중 읽기를 블로킹합니다.NOT VALIDCHECK 제약을 먼저 검증한 뒤 전환하면 완전 스캔을 피할 수 있습니다. - 99행:
shift_type컬럼 드롭은 배포 롤링 윈도우 중 이전 버전 앱 인스턴스가 여전히 해당 컬럼을 참조한다면 에러를 유발할 수 있습니다.
현재는 서비스 초기 단계라 실질 트래픽/데이터 규모가 작을 수 있지만, 이후 마이그레이션에서도 이 패턴이 반복될 수 있으므로 짚어둡니다.
🧰 Tools
🪛 Squawk (2.59.0)
[warning] 74-74: Adding a foreign key constraint requires a table scan and a SHARE ROW EXCLUSIVE lock on both tables, which blocks writes to each table. Add NOT VALID to the constraint in one transaction and then VALIDATE the constraint in a separate transaction.
(adding-foreign-key-constraint)
[warning] 96-96: Setting a column NOT NULL blocks reads while the table is scanned. Make the field nullable and use a CHECK constraint instead.
(adding-not-nullable-field)
[warning] 99-99: Dropping a column may break existing clients.
(ban-drop-column)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@supabase/migrations/20260713000000_add_work_shift_types.sql` around lines 73
- 99, Update the work_schedule_entries migration to use a zero-downtime
sequence: add the shift_type_id foreign key as NOT VALID and defer VALIDATE
CONSTRAINT to a later migration, replace immediate SET NOT NULL with a validated
NOT VALID CHECK constraint before the not-null transition, and defer dropping
shift_type until older application versions no longer reference it.
Source: Linters/SAST tools
| create function public.replace_and_delete_work_shift_type( | ||
| p_workspace_id uuid, | ||
| p_deleted_shift_type_id uuid, | ||
| p_replacement_shift_type_id uuid | ||
| ) | ||
| returns void | ||
| language plpgsql | ||
| security invoker | ||
| set search_path = '' | ||
| as $$ | ||
| begin | ||
| if p_deleted_shift_type_id = p_replacement_shift_type_id then | ||
| raise exception '삭제할 근무유형과 대체 근무유형은 달라야 합니다.'; | ||
| end if; | ||
|
|
||
| if not exists ( | ||
| select 1 | ||
| from public.work_shift_types | ||
| where id = p_deleted_shift_type_id | ||
| and workspace_id = p_workspace_id | ||
| ) then | ||
| raise exception '삭제할 근무유형을 찾을 수 없습니다.'; | ||
| end if; | ||
|
|
||
| if not exists ( | ||
| select 1 | ||
| from public.work_shift_types | ||
| where id = p_replacement_shift_type_id | ||
| and workspace_id = p_workspace_id | ||
| ) then | ||
| raise exception '대체 근무유형을 찾을 수 없습니다.'; | ||
| end if; | ||
|
|
||
| update public.work_schedule_entries | ||
| set shift_type_id = p_replacement_shift_type_id | ||
| where workspace_id = p_workspace_id | ||
| and shift_type_id = p_deleted_shift_type_id; | ||
|
|
||
| delete from public.work_shift_types | ||
| where id = p_deleted_shift_type_id | ||
| and workspace_id = p_workspace_id; | ||
| end; | ||
| $$; |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== file list =="
git ls-files 'supabase/migrations/20260713000000_add_work_shift_types.sql' || true
echo
echo "== relevant slice =="
nl -ba supabase/migrations/20260713000000_add_work_shift_types.sql | sed -n '1,260p'
echo
echo "== policy searches =="
rg -n "work_shift_types_select_member|work_shift_types_write_owner|dev_full_access|replace_and_delete_work_shift_type|work_shift_types" supabase -SRepository: TeampleRun/syncly
Length of output: 293
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== relevant slice =="
sed -n '1,260p' supabase/migrations/20260713000000_add_work_shift_types.sql | cat -n
echo
echo "== policy searches =="
rg -n "work_shift_types_select_member|work_shift_types_write_owner|dev_full_access|replace_and_delete_work_shift_type|work_shift_types" supabase -SRepository: TeampleRun/syncly
Length of output: 8137
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== auth RLS policy context =="
sed -n '1,260p' supabase/migrations/20260709025820_add_auth_rls_policies.sql | cat -n
echo
echo "== row-count / diagnostics patterns =="
rg -n "GET DIAGNOSTICS|ROW_COUNT|raise exception .*0 rows|update .*;|delete .*;" supabase/migrations -S
echo
echo "== function usage =="
rg -n "replace_and_delete_work_shift_type" -S .Repository: TeampleRun/syncly
Length of output: 6930
🏁 Script executed:
#!/bin/bash
set -euo pipefail
sed -n '150,240p' src/entities/work-schedule/api/work-schedule-actions.ts | cat -nRepository: TeampleRun/syncly
Length of output: 2034
멤버 호출 시 0 rows를 실패로 처리하세요.
work_shift_types_select_member로 존재 확인은 통과하지만, work_shift_types_write_owner 때문에 일반 멤버의 DELETE는 막힙니다. 이 RPC는 UPDATE/DELETE 결과가 0건이어도 성공으로 끝나서, dev_full_access가 제거되면 “삭제됨”으로 보이는데 실제로는 shift type이 남는 부분 성공이 발생합니다. GET DIAGNOSTICS ... ROW_COUNT로 0건이면 예외를 던지세요.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@supabase/migrations/20260713000000_add_work_shift_types.sql` around lines 124
- 166, Update replace_and_delete_work_shift_type so the DELETE result is
validated instead of always succeeding. After the delete from
public.work_shift_types, capture ROW_COUNT with GET DIAGNOSTICS and raise an
exception when zero rows were deleted, preserving the existing successful path
when the target shift type is removed.
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/app/workspaces/`[workspaceId]/work-schedule/page.tsx:
- Around line 2-5: Update the work-schedule page’s data-loading flow to reuse
getDashboardWorkSchedule instead of duplicating getCurrentWeekRange,
ensureWeeklyWorkScheduleEntries, and the parallel member/work-type/schedule
requests. Remove the now-unused direct API imports and preserve the page’s
existing returned data shape and behavior.
In `@src/entities/work-schedule/api/get-dashboard-work-schedule.ts`:
- Around line 10-20: Update getDashboardWorkSchedule to obtain the week range
from getCurrentWeekRange using an explicit Asia/Seoul timezone, ensuring
startDate and endDate are calculated from KST rather than server-local time
before calling ensureWeeklyWorkScheduleEntries and getWorkScheduleEntriesByWeek.
In `@src/entities/work-schedule/lib/create-initial-work-schedule.ts`:
- Around line 19-44: Replace the duplicated date calculations in the initial
work-schedule creation flow with the exported getCurrentWeekRange and
getWorkDateByWeekday helpers from work-date.ts. Update the weekdays mapping to
derive each workDate through these helpers while preserving the existing member,
weekday, workspaceId, userId, and shiftTypeId values.
In `@src/entities/work-schedule/lib/work-date.ts`:
- Around line 18-31: Update getCurrentWeekRange to calculate the weekday and
week boundaries in the Asia/Seoul timezone instead of the server’s local
timezone, especially when now uses its default value. Preserve the existing
Monday–Sunday range and YYYY-MM-DD output through toDateString, using
Intl.DateTimeFormat or the project’s timezone utility.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 9d50d2ef-3039-4b05-ba82-6c50f0111135
📒 Files selected for processing (33)
src/app/workspaces/[workspaceId]/dashboard/page.tsxsrc/app/workspaces/[workspaceId]/layout.tsxsrc/app/workspaces/[workspaceId]/page.tsxsrc/app/workspaces/[workspaceId]/settings/page.tsxsrc/app/workspaces/[workspaceId]/work-schedule/page.tsxsrc/entities/dashboard-layout/api/get-dashboard-layout.tssrc/entities/dashboard-layout/api/save-dashboard-layout.tssrc/entities/dashboard-layout/index.tssrc/entities/work-schedule/api/ensure-weekly-work-schedule-entries.tssrc/entities/work-schedule/api/get-dashboard-work-schedule.tssrc/entities/work-schedule/api/get-work-schedule-entries-by-week.tssrc/entities/work-schedule/api/get-work-shift-types-by-workspace-id.tssrc/entities/work-schedule/api/work-schedule-actions.tssrc/entities/work-schedule/index.tssrc/entities/work-schedule/lib/count-schedules-by-weekday.tssrc/entities/work-schedule/lib/create-initial-work-schedule.tssrc/entities/work-schedule/lib/get-next-work-shift-option.tssrc/entities/work-schedule/lib/get-work-members-by-weekday.tssrc/entities/work-schedule/lib/work-date.tssrc/entities/work-schedule/model/mock-work-schedule-config.tssrc/entities/work-schedule/model/work-schedule.types.tssrc/entities/workspace-member/api/get-workspace-members-by-id.tssrc/entities/workspace/api/get-workspace-by-id.tssrc/features/dashboard/edit-layout/model/useDashboardLayout.tssrc/features/manage-work-schedule/model/use-work-schedule-state.tssrc/features/manage-work-schedule/ui/WorkScheduleBoard.tsxsrc/features/manage-work-schedule/ui/WorkShiftSettingsPanel.tsxsrc/shared/api/supabase/current-user.tssrc/shared/model/database.types.tssrc/views/dashboard/ui/DashboardView.tsxsrc/views/store-operation/work-schedule/ui/WorkScheduleView.tsxsrc/widgets/store-operation/dashboard-work-schedule/ui/WorkScheduleSummary.tsxsupabase/migrations/20260713000000_add_work_shift_types.sql
💤 Files with no reviewable changes (1)
- src/entities/dashboard-layout/index.ts
| 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'; |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win
getDashboardWorkSchedule와 로직이 중복됩니다.
getCurrentWeekRange → ensureWeeklyWorkScheduleEntries → Promise.all(멤버/근무유형/스케줄) 흐름이 src/entities/work-schedule/api/get-dashboard-work-schedule.ts의 getDashboardWorkSchedule와 동일합니다. 기존 함수를 재사용해 두 곳이 어긋나지 않도록 하는 것을 권장합니다.
♻️ 제안 리팩터
-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
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/app/workspaces/`[workspaceId]/work-schedule/page.tsx around lines 2 - 5,
Update the work-schedule page’s data-loading flow to reuse
getDashboardWorkSchedule instead of duplicating getCurrentWeekRange,
ensureWeeklyWorkScheduleEntries, and the parallel member/work-type/schedule
requests. Remove the now-unused direct API imports and preserve the page’s
existing returned data shape and behavior.
| const today = new Date(); | ||
| const mondayOffset = (today.getDay() + 6) % 7; | ||
| const monday = new Date(today); | ||
| monday.setDate(today.getDate() - mondayOffset); | ||
|
|
||
| const toDateString = (date: Date): string => { | ||
| const year = date.getFullYear(); | ||
| const month = String(date.getMonth() + 1).padStart(2, '0'); | ||
| const day = String(date.getDate()).padStart(2, '0'); | ||
| return `${year}-${month}-${day}`; | ||
| }; | ||
|
|
||
| return members.flatMap((member) => | ||
| weekdays.map((weekday) => ({ | ||
| workspaceId, | ||
| userId: member.userId, | ||
| weekday: weekday.key, | ||
| shiftOptionId: defaultShift.id, | ||
| })), | ||
| weekdays.map((weekday, index) => { | ||
| const workDate = new Date(monday); | ||
| workDate.setDate(monday.getDate() + index); | ||
|
|
||
| return { | ||
| workspaceId, | ||
| userId: member.userId, | ||
| weekday: weekday.key, | ||
| workDate: toDateString(workDate), | ||
| shiftTypeId: defaultShift.id, | ||
| }; | ||
| }), | ||
| ); |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win
날짜 계산 로직이 work-date.ts와 중복됩니다.
getCurrentWeekRange(월요일 오프셋 계산)와 getWorkDateByWeekday(요일별 날짜 문자열 계산)가 이미 work-date.ts에 존재하고 src/entities/work-schedule/index.ts에서 공개 export 되어 있습니다. 이 파일에서 동일한 로직을 별도로 재구현하면 두 구현이 어긋날 위험이 있습니다.
♻️ 기존 헬퍼 재사용 제안
+import { getCurrentWeekRange, getWorkDateByWeekday } from './work-date';
+
export function createInitialWorkSchedule({
workspaceId,
members,
config,
}: CreateInitialWorkScheduleParams): WorkScheduleEntry[] {
const defaultShift = getDefaultWorkShiftOption(config.shifts);
- const today = new Date();
- const mondayOffset = (today.getDay() + 6) % 7;
- const monday = new Date(today);
- monday.setDate(today.getDate() - mondayOffset);
-
- const toDateString = (date: Date): string => {
- const year = date.getFullYear();
- const month = String(date.getMonth() + 1).padStart(2, '0');
- const day = String(date.getDate()).padStart(2, '0');
- return `${year}-${month}-${day}`;
- };
-
- return members.flatMap((member) =>
- weekdays.map((weekday, index) => {
- const workDate = new Date(monday);
- workDate.setDate(monday.getDate() + index);
-
- return {
- workspaceId,
- userId: member.userId,
- weekday: weekday.key,
- workDate: toDateString(workDate),
- shiftTypeId: defaultShift.id,
- };
- }),
- );
+ const { startDate } = getCurrentWeekRange();
+
+ return members.flatMap((member) =>
+ weekdays.map((weekday) => ({
+ workspaceId,
+ userId: member.userId,
+ weekday: weekday.key,
+ workDate: getWorkDateByWeekday(startDate, weekday.key),
+ shiftTypeId: defaultShift.id,
+ })),
+ );
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const today = new Date(); | |
| const mondayOffset = (today.getDay() + 6) % 7; | |
| const monday = new Date(today); | |
| monday.setDate(today.getDate() - mondayOffset); | |
| const toDateString = (date: Date): string => { | |
| const year = date.getFullYear(); | |
| const month = String(date.getMonth() + 1).padStart(2, '0'); | |
| const day = String(date.getDate()).padStart(2, '0'); | |
| return `${year}-${month}-${day}`; | |
| }; | |
| return members.flatMap((member) => | |
| weekdays.map((weekday) => ({ | |
| workspaceId, | |
| userId: member.userId, | |
| weekday: weekday.key, | |
| shiftOptionId: defaultShift.id, | |
| })), | |
| weekdays.map((weekday, index) => { | |
| const workDate = new Date(monday); | |
| workDate.setDate(monday.getDate() + index); | |
| return { | |
| workspaceId, | |
| userId: member.userId, | |
| weekday: weekday.key, | |
| workDate: toDateString(workDate), | |
| shiftTypeId: defaultShift.id, | |
| }; | |
| }), | |
| ); | |
| import { getCurrentWeekRange, getWorkDateByWeekday } from './work-date'; | |
| const defaultShift = getDefaultWorkShiftOption(config.shifts); | |
| const { startDate } = getCurrentWeekRange(); | |
| return members.flatMap((member) => | |
| weekdays.map((weekday) => ({ | |
| workspaceId, | |
| userId: member.userId, | |
| weekday: weekday.key, | |
| workDate: getWorkDateByWeekday(startDate, weekday.key), | |
| shiftTypeId: defaultShift.id, | |
| })), | |
| ); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/entities/work-schedule/lib/create-initial-work-schedule.ts` around lines
19 - 44, Replace the duplicated date calculations in the initial work-schedule
creation flow with the exported getCurrentWeekRange and getWorkDateByWeekday
helpers from work-date.ts. Update the weekdays mapping to derive each workDate
through these helpers while preserving the existing member, weekday,
workspaceId, userId, and shiftTypeId values.
Kwon812
left a comment
There was a problem hiding this comment.
확인했습니다~
레이아웃 테이블에 페이지 타입은 어차피 워크스페이스아이디별 대시보드페이지에서만 레이아웃 수정하기로 했었으니까 따로 없어도 될 것 같습니다~ 나중에 다른 페이지에서도 레이아웃 편집 가능하게 확장 할 경우에 그때가서 추가하면 좋을 것 같습니다~
유저목록 반환값 제 템플릿에서도 잘 맞을 것 같습니다~
Pull Request
작업 내용
작업 결과
변경 사항
Added
work_shift_types,shift_type_id이전, RLS, 삭제 RPC를 포함한 Supabase migrationChanged
user_dashboard_layouts로 조회·upsertFixed
실행화면
테스트
npm run lintnpm run typechecknpm run buildwork_schedule_entries반영 확인리뷰 요청사항
getWorkspaceMembersByWorkspaceId반환 타입이 다른 도메인에서도 충분한지 확인 부탁드립니다.(src/entities/workspace-member/api/get-workspace-members-by-id.ts)work_shift_typesmigration 및 근무유형 삭제 RPC의 권한·데이터 정합성 검토를 부탁드립니다.user_dashboard_layouts스키마에는page_type이 없어 워크스페이스별 개인 레이아웃 하나만 저장합니다. 페이지별 레이아웃 확장이 필요한지 확인 부탁드립니다.관련 이슈
Closes #42
Summary by CodeRabbit