Skip to content

feat:매장 운영 근무 스케줄 DB 연동(#42) - #42

Merged
seongjinss555 merged 4 commits into
developfrom
feat/#41/store-operation-schedule-db
Jul 13, 2026
Merged

feat:매장 운영 근무 스케줄 DB 연동(#42)#42
seongjinss555 merged 4 commits into
developfrom
feat/#41/store-operation-schedule-db

Conversation

@seongjinss555

@seongjinss555 seongjinss555 commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Pull Request

작업 내용

  • 매장 운영 근무 스케줄을 목업에서 Supabase DB 기반으로 전환했습니다.
  • 워크스페이스 UUID 기준 공통 멤버 조회 API와 DB 워크스페이스 조회를 추가했습니다.
  • 근무유형, 주간 일정, 기본 일정 생성, 셀·근무유형 자동 저장과 삭제 대체 처리를 연결했습니다.
  • 대시보드 레이아웃 저장과 업무 스케줄 위젯을 실제 DB 데이터로 전환했습니다.

작업 결과

  • 실제 워크스페이스 UUID URL에서 멤버·근무유형·일정을 조회합니다.
  • 이번 주에 없는 멤버·요일 조합은 기본 근무유형으로 DB에 생성하며, 기존 배정값은 유지합니다.
  • 셀 클릭과 근무유형 편집 결과가 새로고침 후에도 유지됩니다.
  • 대시보드 위젯은 실제 근무 일정으로 오늘 근무 인원을 집계하고, 위젯 배치는 사용자·워크스페이스별로 저장됩니다.

변경 사항

Added

  • work_shift_types, shift_type_id 이전, RLS, 삭제 RPC를 포함한 Supabase migration
  • 워크스페이스 멤버·근무유형·주간 일정 조회 API
  • 근무 스케줄 및 근무유형 Server Action
  • 현재 인증 사용자/개발 테스트 사용자를 일관되게 반환하는 서버 헬퍼

Changed

  • 워크스페이스 라우팅을 목업 ID 조회에서 DB 조회로 전환
  • 근무 스케줄 화면과 대시보드 업무 스케줄 위젯을 DB 데이터 기반으로 전환
  • 대시보드 레이아웃을 user_dashboard_layouts로 조회·upsert
  • 서버 전용 대시보드 API를 entity 배럴에서 제거해 클라이언트 번들 경계를 분리

Fixed

  • 개발 시드 UUID가 Zod UUID 검증에서 거부되던 문제 수정
  • 화면 기본 오픈값과 대시보드 DB 집계가 달라 근무 인원이 불일치하던 문제 수정
  • 대시보드 레이아웃이 새로고침 후 사라지는 문제 수정

실행화면

  • 근무 스케줄 페이지에서 셀 변경 후 새로고침해 저장값 유지 확인
  • 대시보드 업무 스케줄 위젯에서 실제 근무 인원 집계 확인

테스트

  • npm run lint
  • npm run typecheck
  • npm run build
  • 근무 스케줄 셀 변경 후 Supabase work_schedule_entries 반영 확인
  • 대시보드 레이아웃 저장 후 새로고침 유지 확인
  • 소셜 로그인 연동 및 실제 RLS 정책 전환 후 권한 검증

리뷰 요청사항

  • 공통 getWorkspaceMembersByWorkspaceId 반환 타입이 다른 도메인에서도 충분한지 확인 부탁드립니다.(src/entities/workspace-member/api/get-workspace-members-by-id.ts)
  • work_shift_types migration 및 근무유형 삭제 RPC의 권한·데이터 정합성 검토를 부탁드립니다.
  • 현재 user_dashboard_layouts 스키마에는 page_type이 없어 워크스페이스별 개인 레이아웃 하나만 저장합니다. 페이지별 레이아웃 확장이 필요한지 확인 부탁드립니다.

관련 이슈

Closes #42

Summary by CodeRabbit

  • 새로운 기능
    • 워크스페이스 정보와 대시보드 레이아웃을 실제 데이터로 불러오고 저장합니다.
    • 주간 근무 일정과 근무유형을 실제 데이터로 관리합니다.
    • 근무유형 추가/수정/순서 변경/대체 후 삭제를 지원합니다.
    • 근무 일정 변경을 서버에 저장합니다.
    • 주간 일정이 없으면 기본 일정이 자동으로 생성됩니다.
  • 개선 사항
    • 근무 일정 요약이 실제 멤버와 근무유형을 반영합니다.
    • 존재하지 않는 워크스페이스는 404 화면으로 안내합니다.
    • 데이터 로딩 및 오류 상태가 화면에 표시됩니다.

@coderabbitai

coderabbitai Bot commented Jul 13, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 852cb2d6-3c9c-4527-843e-83a89976226d

📥 Commits

Reviewing files that changed from the base of the PR and between ab83d00 and 90e2c9e.

📒 Files selected for processing (7)
  • src/entities/work-schedule/api/ensure-weekly-work-schedule-entries.ts
  • src/entities/work-schedule/api/work-schedule-actions.ts
  • src/entities/work-schedule/lib/work-date.ts
  • src/features/dashboard/edit-layout/model/useDashboardLayout.ts
  • src/shared/api/supabase/current-user.ts
  • src/widgets/store-operation/dashboard-work-schedule/ui/WorkScheduleSummary.tsx
  • supabase/migrations/20260713000000_add_work_shift_types.sql
💤 Files with no reviewable changes (1)
  • supabase/migrations/20260713000000_add_work_shift_types.sql

📝 Walkthrough

Walkthrough

워크스페이스와 대시보드 레이아웃을 Supabase 데이터로 전환하고, 근무유형·주간 스케줄 조회와 편집을 서버 액션에 연결했습니다. 스케줄 모델은 shiftTypeIdworkDate 기반으로 변경되었습니다.

Changes

워크스페이스 및 근무 스케줄 실데이터 연동

Layer / File(s) Summary
워크스페이스 및 대시보드 데이터 조회
src/app/workspaces/[workspaceId]/*, src/entities/workspace/..., src/entities/dashboard-layout/..., src/features/dashboard/...
워크스페이스와 대시보드 레이아웃을 실제 사용자·워크스페이스 데이터로 조회·저장합니다.
주간 스케줄 API와 데이터베이스 계약
src/entities/work-schedule/api/*, src/entities/workspace-member/api/*, src/entities/work-schedule/lib/work-date.ts, src/shared/model/database.types.ts, supabase/migrations/*
근무유형·멤버·주간 엔트리 조회, 기본 엔트리 보장, 서버 액션과 관련 DB 스키마 및 RPC를 추가합니다.
스케줄 상태와 편집 UI
src/entities/work-schedule/model/*, src/entities/work-schedule/lib/*, src/features/manage-work-schedule/*, src/views/store-operation/work-schedule/*
날짜 및 근무유형 ID 기반 상태 관리와 근무유형 편집·삭제 대체·셀 저장을 구현합니다.
대시보드 근무 요약
src/widgets/store-operation/dashboard-work-schedule/*
근무 요약 위젯이 API 데이터를 로드하고 요일별 근무 인원을 집계합니다.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

Suggested reviewers: wjswlgh96, jiwoonge, 0011810

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed 제목이 매장 운영 근무 스케줄의 DB 연동이라는 핵심 변경을 정확히 요약합니다.
Description check ✅ Passed 필수 섹션이 대부분 채워졌고, 일부 리뷰 체크리스트만 비어 있어도 충분히 구체적입니다.
Linked Issues check ✅ Passed 직접 이슈 #42의 DB 연동, 주간 일정 생성, 자동 저장, 삭제 대체, 대시보드 저장 요구사항을 충족합니다.
Out of Scope Changes check ✅ Passed 변경 사항은 근무 스케줄 DB 연동과 대시보드 저장·조회 범위에 맞고, 명백한 무관 변경은 보이지 않습니다.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/#41/store-operation-schedule-db

Comment @coderabbitai help to get the list of available commands.

@seongjinss555
seongjinss555 force-pushed the feat/#41/store-operation-schedule-db branch from 88ddcd8 to ab83d00 Compare July 13, 2026 01:14
@seongjinss555 seongjinss555 changed the title feat:매장 운영 근무 스케줄 DB 연동(#41) feat:매장 운영 근무 스케줄 DB 연동(#42) Jul 13, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 11dbbc0 and 88ddcd8.

📒 Files selected for processing (33)
  • src/app/workspaces/[workspaceId]/dashboard/page.tsx
  • src/app/workspaces/[workspaceId]/layout.tsx
  • src/app/workspaces/[workspaceId]/page.tsx
  • src/app/workspaces/[workspaceId]/settings/page.tsx
  • src/app/workspaces/[workspaceId]/work-schedule/page.tsx
  • src/entities/dashboard-layout/api/get-dashboard-layout.ts
  • src/entities/dashboard-layout/api/save-dashboard-layout.ts
  • src/entities/dashboard-layout/index.ts
  • src/entities/work-schedule/api/ensure-weekly-work-schedule-entries.ts
  • src/entities/work-schedule/api/get-dashboard-work-schedule.ts
  • src/entities/work-schedule/api/get-work-schedule-entries-by-week.ts
  • src/entities/work-schedule/api/get-work-shift-types-by-workspace-id.ts
  • src/entities/work-schedule/api/work-schedule-actions.ts
  • src/entities/work-schedule/index.ts
  • src/entities/work-schedule/lib/count-schedules-by-weekday.ts
  • src/entities/work-schedule/lib/create-initial-work-schedule.ts
  • src/entities/work-schedule/lib/get-next-work-shift-option.ts
  • src/entities/work-schedule/lib/get-work-members-by-weekday.ts
  • src/entities/work-schedule/lib/work-date.ts
  • src/entities/work-schedule/model/mock-work-schedule-config.ts
  • src/entities/work-schedule/model/work-schedule.types.ts
  • src/entities/workspace-member/api/get-workspace-members-by-id.ts
  • src/entities/workspace/api/get-workspace-by-id.ts
  • src/features/dashboard/edit-layout/model/useDashboardLayout.ts
  • src/features/manage-work-schedule/model/use-work-schedule-state.ts
  • src/features/manage-work-schedule/ui/WorkScheduleBoard.tsx
  • src/features/manage-work-schedule/ui/WorkShiftSettingsPanel.tsx
  • src/shared/api/supabase/current-user.ts
  • src/shared/model/database.types.ts
  • src/views/dashboard/ui/DashboardView.tsx
  • src/views/store-operation/work-schedule/ui/WorkScheduleView.tsx
  • src/widgets/store-operation/dashboard-work-schedule/ui/WorkScheduleSummary.tsx
  • supabase/migrations/20260713000000_add_work_shift_types.sql
💤 Files with no reviewable changes (1)
  • src/entities/dashboard-layout/index.ts

Comment thread src/app/workspaces/[workspaceId]/settings/page.tsx
Comment thread src/entities/work-schedule/api/work-schedule-actions.ts Outdated
Comment thread src/entities/work-schedule/api/work-schedule-actions.ts
Comment on lines +85 to +113
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();

Copy link
Copy Markdown

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

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 반환 동작은 유지하세요.

Comment thread src/widgets/store-operation/dashboard-work-schedule/ui/WorkScheduleSummary.tsx Outdated
Comment thread src/widgets/store-operation/dashboard-work-schedule/ui/WorkScheduleSummary.tsx Outdated
Comment on lines +73 to +99
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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 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 VALID CHECK 제약을 먼저 검증한 뒤 전환하면 완전 스캔을 피할 수 있습니다.
  • 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

Comment thread supabase/migrations/20260713000000_add_work_shift_types.sql
Comment on lines +124 to +166
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;
$$;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ 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 -S

Repository: 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 -S

Repository: 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 -n

Repository: 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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 88ddcd8 and ab83d00.

📒 Files selected for processing (33)
  • src/app/workspaces/[workspaceId]/dashboard/page.tsx
  • src/app/workspaces/[workspaceId]/layout.tsx
  • src/app/workspaces/[workspaceId]/page.tsx
  • src/app/workspaces/[workspaceId]/settings/page.tsx
  • src/app/workspaces/[workspaceId]/work-schedule/page.tsx
  • src/entities/dashboard-layout/api/get-dashboard-layout.ts
  • src/entities/dashboard-layout/api/save-dashboard-layout.ts
  • src/entities/dashboard-layout/index.ts
  • src/entities/work-schedule/api/ensure-weekly-work-schedule-entries.ts
  • src/entities/work-schedule/api/get-dashboard-work-schedule.ts
  • src/entities/work-schedule/api/get-work-schedule-entries-by-week.ts
  • src/entities/work-schedule/api/get-work-shift-types-by-workspace-id.ts
  • src/entities/work-schedule/api/work-schedule-actions.ts
  • src/entities/work-schedule/index.ts
  • src/entities/work-schedule/lib/count-schedules-by-weekday.ts
  • src/entities/work-schedule/lib/create-initial-work-schedule.ts
  • src/entities/work-schedule/lib/get-next-work-shift-option.ts
  • src/entities/work-schedule/lib/get-work-members-by-weekday.ts
  • src/entities/work-schedule/lib/work-date.ts
  • src/entities/work-schedule/model/mock-work-schedule-config.ts
  • src/entities/work-schedule/model/work-schedule.types.ts
  • src/entities/workspace-member/api/get-workspace-members-by-id.ts
  • src/entities/workspace/api/get-workspace-by-id.ts
  • src/features/dashboard/edit-layout/model/useDashboardLayout.ts
  • src/features/manage-work-schedule/model/use-work-schedule-state.ts
  • src/features/manage-work-schedule/ui/WorkScheduleBoard.tsx
  • src/features/manage-work-schedule/ui/WorkShiftSettingsPanel.tsx
  • src/shared/api/supabase/current-user.ts
  • src/shared/model/database.types.ts
  • src/views/dashboard/ui/DashboardView.tsx
  • src/views/store-operation/work-schedule/ui/WorkScheduleView.tsx
  • src/widgets/store-operation/dashboard-work-schedule/ui/WorkScheduleSummary.tsx
  • supabase/migrations/20260713000000_add_work_shift_types.sql
💤 Files with no reviewable changes (1)
  • src/entities/dashboard-layout/index.ts

Comment on lines +2 to +5
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';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

getDashboardWorkSchedule와 로직이 중복됩니다.

getCurrentWeekRangeensureWeeklyWorkScheduleEntriesPromise.all(멤버/근무유형/스케줄) 흐름이 src/entities/work-schedule/api/get-dashboard-work-schedule.tsgetDashboardWorkSchedule와 동일합니다. 기존 함수를 재사용해 두 곳이 어긋나지 않도록 하는 것을 권장합니다.

♻️ 제안 리팩터
-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.

Comment thread src/entities/work-schedule/api/get-dashboard-work-schedule.ts
Comment on lines +19 to 44
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,
};
}),
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 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.

Suggested change
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.

Comment thread src/entities/work-schedule/lib/work-date.ts

@Kwon812 Kwon812 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

확인했습니다~
레이아웃 테이블에 페이지 타입은 어차피 워크스페이스아이디별 대시보드페이지에서만 레이아웃 수정하기로 했었으니까 따로 없어도 될 것 같습니다~ 나중에 다른 페이지에서도 레이아웃 편집 가능하게 확장 할 경우에 그때가서 추가하면 좋을 것 같습니다~
유저목록 반환값 제 템플릿에서도 잘 맞을 것 같습니다~

@seongjinss555
seongjinss555 merged commit 259b8ed into develop Jul 13, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants