Skip to content

feat:매장 운영 업무 스케줄 위젯 추가(#28) - #29

Merged
seongjinss555 merged 4 commits into
developfrom
feat/#28/dashboard-layout-store-schedule
Jul 9, 2026
Merged

feat:매장 운영 업무 스케줄 위젯 추가(#28)#29
seongjinss555 merged 4 commits into
developfrom
feat/#28/dashboard-layout-store-schedule

Conversation

@seongjinss555

@seongjinss555 seongjinss555 commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Pull Request

작업 내용

  • 매장 운영 대시보드에서 업무 스케줄 위젯을 추가할 수 있도록 구성했습니다.
  • 대시보드 위젯 렌더러에 workspaceId 컨텍스트를 전달해 위젯별 데이터 스코프를 맞췄습니다.
  • 최근 자료 위젯이 전체 목업 자료가 아니라 현재 워크스페이스 자료만 보여주도록 수정했습니다.
  • 레이아웃 추가/삭제 시 저장 서버 액션이 state updater 안에서 호출되지 않도록 저장 시점을 분리했습니다.

작업 결과

  • store-operation 대시보드 편집 모드의 위젯 추가 목록에 업무 스케줄이 표시됩니다.
  • 업무 스케줄 위젯은 현재 워크스페이스 멤버 기준 오늘 근무 인원과 근무 유형별 요약을 보여줍니다.
  • 자료 위젯은 workspaceId 기준으로 필터링되어 다른 워크스페이스 자료가 섞이지 않습니다.
  • 위젯 추가/삭제 중 React 렌더 단계에서 서버 액션이 호출되던 경고 원인을 제거했습니다.

변경 사항

Added

  • 매장 운영용 dashboard-work-schedule 위젯 추가
  • 위젯 렌더 컨텍스트에 workspaceId 추가

Changed

  • 대시보드 그리드가 각 위젯 렌더러에 워크스페이스 컨텍스트를 전달하도록 변경
  • store-operation 템플릿의 추가 가능 위젯 목록에 업무 스케줄 추가
  • 최근 자료 위젯이 워크스페이스 단위로 자료를 필터링하도록 변경

Fixed

  • 레이아웃 저장 서버 액션 호출을 useEffect로 이동해 state updater 내부 부수효과 제거

실행화면

  • 대시보드 레아이웃
스크린샷 2026-07-09 오전 11 11 28 스크린샷 2026-07-09 오전 11 17 46

테스트

  • 로컬 실행 확인
  • 주요 시나리오 확인
  • 영향 범위 확인

검증 명령:

  • npm run lint
  • npm run typecheck
  • npm run build
  • git diff --check

리뷰 체크리스트

  • PR base branch가 올바릅니다. (feature/* -> develop, 배포 시 develop 또는 release/* -> main)
  • 브랜치명이 Type/#issue-number/description 형식을 따릅니다.
  • 커밋 메시지가 컨벤션을 따릅니다.
  • 불필요한 console.log, 주석, 임시 코드를 제거했습니다.
  • 타입 에러와 린트 에러를 확인했습니다.
  • CodeRabbit 1차 리뷰를 확인했습니다.
  • CodeRabbit 리뷰 반영 후 Discord에 공유했습니다.
  • 최소 1명 이상의 approve 후 merge합니다.

리뷰 요청사항

  • 대시보드 위젯 렌더 컨텍스트를 workspaceId로 확장한 방향이 이후 공지/자료/스케줄 DB 연동에도 적절한지 봐주세요.
  • 업무 스케줄 위젯의 정보 밀도가 기존 최근 공지/최근 자료 위젯과 어울리는지 봐주세요.

관련 이슈

Closes #28
ref #2

@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

대시보드 위젯 렌더에 workspaceId를 전달하도록 흐름을 확장하고, 매장 운영용 업무 스케줄 위젯을 추가해 카탈로그와 템플릿에 등록했습니다. 최근 자료 위젯은 워크스페이스 기준으로 필터링되며, 레이아웃 저장은 useEffect 기반으로 바뀌었습니다.

Changes

대시보드 위젯 컨텍스트와 위젯 구성

Layer / File(s) Summary
렌더 컨텍스트 전달
src/shared/dashboard/model/widget.types.ts, src/views/dashboard/ui/DashboardGrid.tsx, src/views/dashboard/ui/DashboardView.tsx
WidgetRenderContextWidgetDefinition.render(size, context)를 추가하고, DashboardViewDashboardGridworkspaceId를 위젯 렌더러에 전달한다.
위젯 카탈로그와 템플릿 갱신
src/views/dashboard/config/widget-catalog.tsx, src/views/dashboard/config/template-widgets.ts, src/widgets/store-operation/dashboard-recent-resources/ui/RecentResources.tsx
work-schedule 위젯을 카탈로그와 store-operation 템플릿에 추가하고, RecentResourcesworkspaceId 기준으로 자료를 필터링해 렌더한다.
업무 스케줄 위젯 구현
src/widgets/store-operation/dashboard-work-schedule/ui/WorkScheduleSummary.tsx, src/widgets/store-operation/dashboard-work-schedule/index.ts, src/views/store-operation/work-schedule/ui/WorkScheduleView.tsx
WorkScheduleSummary가 workspaceId 기준 멤버를 집계해 크기별 요약 UI를 렌더하고, WorkScheduleView가 워크스페이스별 멤버 데이터를 초기 스케줄과 보드에 전달한다.

레이아웃 저장 시점 변경

Layer / File(s) Summary
레이아웃 저장을 useEffect로 이동
src/features/dashboard/edit-layout/model/useDashboardLayout.ts
useEffectuseRef를 추가하고, 초기 마운트 이후에만 saveDashboardLayout을 호출하며, handleLayoutChange/addWidget/removeWidget에서 즉시 커밋 호출을 제거한다.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant DashboardView
  participant DashboardGrid
  participant WorkScheduleSummary
  participant RecentResources

  DashboardView->>DashboardGrid: workspaceId 전달
  DashboardGrid->>WorkScheduleSummary: render(size, { workspaceId })
  WorkScheduleSummary-->>DashboardGrid: 요약 UI 반환
  DashboardGrid->>RecentResources: render(size, { workspaceId })
  RecentResources-->>DashboardGrid: 필터링된 자료 UI 반환
Loading

Possibly related PRs

  • TeampleRun/syncly#12: workspaceId를 받아 WorkScheduleView를 렌더링하는 업무 스케줄 화면 변경이 이번 위젯 구성과 직접 연결됩니다.
  • TeampleRun/syncly#18: useDashboardLayout의 저장 시점 변경이 같은 레이아웃 저장 흐름과 맞물립니다.

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 11.11% 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
Linked Issues check ✅ Passed 업무 스케줄 위젯 추가, workspaceId 컨텍스트, 최근 자료 필터링, 레이아웃 저장 분리 요구를 모두 반영했습니다.
Out of Scope Changes check ✅ Passed 요구사항과 무관한 변경은 보이지 않으며, 추가된 코드도 위젯과 레이아웃 관련 범위에 머뭅니다.
Title check ✅ Passed 제목은 매장 운영 업무 스케줄 위젯 추가라는 변경의 핵심을 간결하게 잘 요약합니다.
Description check ✅ Passed 필수 섹션들이 대부분 채워져 있고 작업 내용, 결과, 변경 사항, 테스트, 리뷰 체크리스트, 이슈가 포함되어 있습니다.
✨ 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/#28/dashboard-layout-store-schedule

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

@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/features/dashboard/edit-layout/model/useDashboardLayout.ts`:
- Line 31: The `useDashboardLayout` save flow currently implies persisting on
every `onLayoutChange`, which will hammer the server during drag/resize. Update
the layout persistence logic in `useDashboardLayout` to debounce saves or defer
them until drag/resize ավարտ/completion, and keep the change localized to the
`onLayoutChange`/DB-sync path so frequent intermediate events do not trigger
server actions.
- Around line 29-39: The current save trigger in useDashboardLayout relies only
on didMountRef, which can still fire on StrictMode remounts or duplicate initial
onLayoutChange events. Update the useEffect in useDashboardLayout to compare the
current layout against the previous layout before calling saveDashboardLayout,
and only persist when the layout has a real change. Keep the guard logic near
didMountRef/saveDashboardLayout so the initial or duplicate layout emissions do
not cause an unnecessary save.

In
`@src/widgets/store-operation/dashboard-work-schedule/ui/WorkScheduleSummary.tsx`:
- Around line 47-53: `getNextWeekdays` is including today in the “next 4 days”
range, which can duplicate the current day in the lg summary grid. Update the
logic in `WorkScheduleSummary` so the sequence starts from the day after `today`
(not `todayIndex` itself), then returns the next 4 weekdays; check the callers
that render the top “오늘 근무 인원” and the lower grid to keep their day sets
non-overlapping.
- Around line 82-86: `WorkScheduleSummary`의 `primaryShift` 선택 로직이 배열에서 처음으로 근무
인원이 0보다 큰 시프트를 고르는 문제를 수정하세요. `workingShifts.find(...)` 대신 `todayCounts`를 기준으로
가장 많은 인원이 배정된 시프트를 계산해 대표 시프트로 사용하고, 동률일 때의 우선순위도 명확히 유지되도록 하세요.
`totalWorkingMembers` 계산은 그대로 두고, `primaryShift`를 만드는 부분만 `workingShifts`와
`todayCounts`를 함께 사용해 최다 근무 인원을 가진 시프트를 선택하도록 변경하세요.
🪄 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: 26043281-8ed6-4835-8213-ad3a1bb774c2

📥 Commits

Reviewing files that changed from the base of the PR and between 376f240 and 5e749e7.

📒 Files selected for processing (9)
  • src/features/dashboard/edit-layout/model/useDashboardLayout.ts
  • src/shared/dashboard/model/widget.types.ts
  • src/views/dashboard/config/template-widgets.ts
  • src/views/dashboard/config/widget-catalog.tsx
  • src/views/dashboard/ui/DashboardGrid.tsx
  • src/views/dashboard/ui/DashboardView.tsx
  • src/widgets/store-operation/dashboard-recent-resources/ui/RecentResources.tsx
  • src/widgets/store-operation/dashboard-work-schedule/index.ts
  • src/widgets/store-operation/dashboard-work-schedule/ui/WorkScheduleSummary.tsx

Comment on lines +29 to +39
const didMountRef = useRef(false);

// TODO: DB 연동 — 변경 저장 (드래그 중 잦은 호출은 debounce 예정)
const commit = useCallback(
(next: Layout) => {
void saveDashboardLayout(workspaceId, pageType, { layout: next });
},
[workspaceId, pageType],
);
useEffect(() => {
if (!didMountRef.current) {
didMountRef.current = true;
return;
}

void saveDashboardLayout(workspaceId, pageType, { layout });
}, [layout, workspaceId, pageType]);

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

🧩 Analysis chain

🌐 Web query:

react-grid-layout onLayoutChange fires without position change v2

💡 Result:

The issue of onLayoutChange firing without a tangible position change is a known behavioral characteristic in react-grid-layout, often stemming from how the component handles internal state updates, mount events, or interaction events [1][2]. While version 2 (v2) includes significant architectural improvements, such as immutable layout updates, to address previous mutation-related bugs [3][4], users may still encounter unexpected triggers due to the following common patterns: 1. Lifecycle and Initialization: The onLayoutChange callback can fire during the component's initial mount or when the layout prop is first initialized, as the grid computes its internal layout state [1]. 2. Interaction Events: Features like droppable regions (dragenter/dragleave events) can trigger onLayoutChange even if no item is dropped, as the library may recalculate layout state in response to external drag interactions [2]. 3. Prop and Data Handling: Applying certain props (like data-grid) or managing layout state in external stores (e.g., Redux) without careful memoization can lead to redundant triggers or double-firing of callbacks [5][6]. 4. External Factors: In some environments, layout recalculations may occur due to external events like page scrolling or parent container resizing if the grid's dimensions are bound to these factors [7]. To mitigate these issues: - Implement a comparison check: Inside your onLayoutChange handler, verify if the new layout actually differs from your current state before triggering side effects (e.g., API calls or state updates) [1][6]. A deep comparison or a stringified check can be effective [6]. - Use specific event handlers: For drag and resize operations, prioritize using specific callbacks like onDragStop or onResizeStop to update your state, rather than relying solely on onLayoutChange if it is firing too frequently for your use case [2]. - Upgrade to v2: Ensure you are using the latest version (v2.2.0+), as it features a complete rewrite that utilizes immutable layout updates, significantly improving the accuracy of reference comparisons and reducing bugs related to layout mutation [3][4].

Citations:


초기 저장은 실제 레이아웃 변경 시에만 수행하도록 좁혀주세요.
didMountRef만으로는 StrictMode의 재마운트나 react-grid-layout의 초기/중복 onLayoutChange 호출을 막지 못합니다. 저장 전에 이전 레이아웃과 실질적 diff를 비교해, 변경이 있을 때만 saveDashboardLayout을 호출하는 쪽이 안전합니다.

🤖 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/dashboard/edit-layout/model/useDashboardLayout.ts` around lines
29 - 39, The current save trigger in useDashboardLayout relies only on
didMountRef, which can still fire on StrictMode remounts or duplicate initial
onLayoutChange events. Update the useEffect in useDashboardLayout to compare the
current layout against the previous layout before calling saveDashboardLayout,
and only persist when the layout has a real change. Keep the guard logic near
didMountRef/saveDashboardLayout so the initial or duplicate layout emissions do
not cause an unnecessary save.

const [editMode, setEditMode] = useState(false);
const didMountRef = useRef(false);

// TODO: DB 연동 — 변경 저장 (드래그 중 잦은 호출은 debounce 예정)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

TODO: 저장 debounce. 드래그/리사이즈 중 onLayoutChange가 매우 자주 발생하므로, DB 연동 시 각 이벤트마다 서버 액션이 호출됩니다. debounce(또는 드래그 종료 시점 저장) 도입이 필요합니다.

원하시면 debounce 적용 구현을 작성하거나 추적용 이슈를 생성해 드릴까요?

🤖 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/dashboard/edit-layout/model/useDashboardLayout.ts` at line 31,
The `useDashboardLayout` save flow currently implies persisting on every
`onLayoutChange`, which will hammer the server during drag/resize. Update the
layout persistence logic in `useDashboardLayout` to debounce saves or defer them
until drag/resize ավարտ/completion, and keep the change localized to the
`onLayoutChange`/DB-sync path so frequent intermediate events do not trigger
server actions.

Comment on lines +47 to +53
function getNextWeekdays(today: WeekdayKey, count: number) {
const todayIndex = weekdays.findIndex((weekday) => weekday.key === today);
return Array.from({ length: count }, (_, index) => {
const weekdayIndex = (todayIndex + index) % weekdays.length;
return weekdays[weekdayIndex];
});
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

getNextWeekdays가 "다음 4일"에 오늘을 포함해, lg 뷰 하단 그리드 첫 칸이 상단 "오늘 근무 인원"과 중복 표시될 수 있습니다.

(todayIndex + index) % weekdays.lengthindex === 0일 때 오늘 자신을 반환합니다. 주석/UI 상 "다음 4일 근무 인원"이라는 의도라면 오늘을 제외하고 다음날부터 4일을 보여줘야 할 것으로 보입니다.

🐛 오늘을 제외한 다음 4일을 반환하도록 하는 수정안
   const todayIndex = weekdays.findIndex((weekday) => weekday.key === today);
   return Array.from({ length: count }, (_, index) => {
-    const weekdayIndex = (todayIndex + index) % weekdays.length;
+    const weekdayIndex = (todayIndex + index + 1) % weekdays.length;
     return weekdays[weekdayIndex];
   });

Also applies to: 122-122, 158-182

🤖 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/widgets/store-operation/dashboard-work-schedule/ui/WorkScheduleSummary.tsx`
around lines 47 - 53, `getNextWeekdays` is including today in the “next 4 days”
range, which can duplicate the current day in the lg summary grid. Update the
logic in `WorkScheduleSummary` so the sequence starts from the day after `today`
(not `todayIndex` itself), then returns the next 4 weekdays; check the callers
that render the top “오늘 근무 인원” and the lower grid to keep their day sets
non-overlapping.

Comment on lines +82 to +86
const totalWorkingMembers = workingShifts.reduce(
(total, shift) => total + (todayCounts[shift.id] ?? 0),
0,
);
const primaryShift = workingShifts.find((shift) => (todayCounts[shift.id] ?? 0) > 0);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

primaryShift가 최다 근무 인원 시프트가 아닌 배열 순서상 첫 매칭 시프트를 선택합니다.

workingShifts.find(...)mockWorkScheduleConfig.shifts 배열 순서상 인원이 0보다 큰 첫 시프트를 반환합니다. 실제로 가장 많은 인원이 근무하는 시프트가 아니라도 "대표" 시프트로 표시될 수 있습니다(예: 첫 시프트에 1명, 다른 시프트에 8명이면 1명짜리가 대표로 노출).

🐛 최다 인원 시프트를 대표로 선택하는 수정안
-  const primaryShift = workingShifts.find((shift) => (todayCounts[shift.id] ?? 0) > 0);
+  const primaryShift = workingShifts.reduce<(typeof workingShifts)[number] | undefined>(
+    (best, shift) => {
+      const count = todayCounts[shift.id] ?? 0;
+      const bestCount = best ? todayCounts[best.id] ?? 0 : 0;
+      return count > bestCount ? shift : best;
+    },
+    undefined,
+  );
📝 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 totalWorkingMembers = workingShifts.reduce(
(total, shift) => total + (todayCounts[shift.id] ?? 0),
0,
);
const primaryShift = workingShifts.find((shift) => (todayCounts[shift.id] ?? 0) > 0);
const totalWorkingMembers = workingShifts.reduce(
(total, shift) => total + (todayCounts[shift.id] ?? 0),
0,
);
const primaryShift = workingShifts.reduce<(typeof workingShifts)[number] | undefined>(
(best, shift) => {
const count = todayCounts[shift.id] ?? 0;
const bestCount = best ? todayCounts[best.id] ?? 0 : 0;
return count > bestCount ? shift : best;
},
undefined,
);
🤖 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/widgets/store-operation/dashboard-work-schedule/ui/WorkScheduleSummary.tsx`
around lines 82 - 86, `WorkScheduleSummary`의 `primaryShift` 선택 로직이 배열에서 처음으로 근무
인원이 0보다 큰 시프트를 고르는 문제를 수정하세요. `workingShifts.find(...)` 대신 `todayCounts`를 기준으로
가장 많은 인원이 배정된 시프트를 계산해 대표 시프트로 사용하고, 동률일 때의 우선순위도 명확히 유지되도록 하세요.
`totalWorkingMembers` 계산은 그대로 두고, `primaryShift`를 만드는 부분만 `workingShifts`와
`todayCounts`를 함께 사용해 최다 근무 인원을 가진 시프트를 선택하도록 변경하세요.

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

고생하셨습니다~ 대시보드 위젯에 workspaceId 같이 내려주는게 확장성 면에서도 좋을 것 같습니다~
지금 위젯에 추가해두신 형태로 추후 pr파서 다른 위젯들도 수정해두겠습니다~
다만 위젯별 데이터를 각각 위젯 내부에서 워크스페이스 아이디로 페칭할지 아니면 대시보드에서 일괄 페칭해서 내려줄지는 추후 고민해봤으면 좋겠습니다!

@seongjinss555
seongjinss555 merged commit 2672f79 into develop Jul 9, 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