Skip to content

feat(web): UI 개선 - #64

Merged
ehlung merged 12 commits into
developfrom
feature/improve-ui
Jun 24, 2026
Merged

feat(web): UI 개선#64
ehlung merged 12 commits into
developfrom
feature/improve-ui

Conversation

@meteorqz6

@meteorqz6 meteorqz6 commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

개요

웹 관리자 화면의 전반적인 UI를 정리하고, @moyeorak/design-system 0.3.0 적용에 맞춰 전역 스타일 import 구조와 공용 관리 UI 컴포넌트를 개선했습니다.

변경 유형

  • UI/스타일 개선
  • 공용 컴포넌트 리팩터링
  • 접근성 개선
  • 의존성/전역 스타일 설정 변경

변경 사항

디자인 시스템 및 전역 스타일

  • @moyeorak/design-system 버전을 ^0.3.0으로 업데이트했습니다.
  • 전역 CSS import 순서를 layers.csstailwindcsstailwind-pluginstyles.css로 정리했습니다.
  • 기존 layout.tsx@moyeorak/design-system/dist/style.css 전역 import를 제거했습니다.
  • Pretendard 폰트 로딩을 CDN @font-face에서 next/font/local + 로컬 폰트 asset 방식으로 변경했습니다.
  • tsconfig.json의 JSX 설정을 Next.js 빌드 권장값인 preserve로 정리했습니다.

관리자 레이아웃 및 사이드바

  • 관리자 사이드바 메뉴를 단일 리스트로 정리했습니다.
  • 사이드바 메뉴명을 조정했습니다.
    • 근무자근무자 관리
    • 가능 시간가능 시간 관리
  • 사이드바 하단에 인증 사용자 정보와 로그아웃 버튼을 표시하고, 로그아웃 시 세션 폐기 후 /login으로 이동하도록 변경했습니다.
  • /organization/new 온보딩 화면에서도 관리자 레이아웃을 사용하되, 사이드바 메뉴는 숨기고 로그아웃 영역만 노출하도록 변경했습니다.
  • 사이드바 트리거 및 메뉴 hover 스타일을 디자인 시스템 토큰 기반으로 정리했습니다.
  • 관리자 레이아웃에 위로가기 FloatingButton을 추가했습니다.

공용 관리 UI 컴포넌트

  • 관리 화면에서 재사용할 공용 컴포넌트를 추가했습니다.
    • AdminTableSection
    • CrudFormDialog
    • DeleteConfirmDialog
    • EmptyState
    • RowActions
  • CrudFormDialog를 Radix Dialog 기반으로 전환해 Escape 닫기, focus trap, focus restore 동작을 보강했습니다.
  • 근무자 / 최소 인원 조건 테이블의 수정·삭제 액션을 공용 RowActions + IconButton 기반으로 정리했습니다.

화면별 UI 정리

  • 대시보드 요약 문구와 CSV export 액션 표시를 정리했습니다.
  • 대시보드 CSV export 버튼 아이콘을 공유 아이콘에서 다운로드 아이콘으로 변경했습니다.
  • 보관함 월 필터 문구를 간결하게 조정했습니다.
  • 가능 시간 선택 셀에 aria-pressed와 체크 아이콘을 추가해 선택 상태를 색상만으로 표현하지 않도록 보완했습니다.
  • 근무자 / 최소 인원 조건 테이블의 액션 열에 sr-only 라벨을 추가해 표 접근성을 보강했습니다.
  • 인증 브랜드 compact 표시의 배경색을 정리했습니다.

영향 범위

  • 웹 관리자 화면 전반
    • 대시보드
    • 근무자 관리
    • 가능 시간 관리
    • 최소 인원 조건
    • 보관함
    • 조직 생성 온보딩
  • 웹 전역 스타일 및 폰트 로딩
  • @moyeorak/design-system, Radix Dialog 관련 웹 의존성

테스트 방법

  • pnpm --filter @fragment/web typecheck
  • pnpm --filter @fragment/web build
  • 웹 앱 실행 후 주요 관리자 화면 UI 확인
    • 대시보드
    • 근무자 관리
    • 가능 시간 관리
    • 최소 인원 조건
    • 보관함
    • 조직 생성 온보딩
  • 사이드바 펼침/접힘 상태에서 메뉴, 계정 정보, 로그아웃 버튼 표시 확인
  • /organization/new에서 사이드바 메뉴가 숨겨지고 로그아웃 영역만 사용 가능한지 확인
  • 테이블 수정·삭제 버튼 및 다이얼로그 닫기/저장 동작 확인
  • 위로가기 버튼 클릭 시 페이지 상단 이동 확인

체크리스트

  • 주요 관리자 화면 UI 확인
  • 사이드바 펼침/접힘 표시 확인
  • 로그아웃 버튼 동작 확인
  • Radix Dialog 전환 후 타입체크/빌드 확인
  • 접근성 리뷰 반영 사항 확인

스크린샷/영상

  • 별도 첨부 없음

리뷰 포인트

  • CrudFormDialog를 Radix Dialog 기반으로 바꾼 부분의 키보드 접근성 동작
  • /organization/new 온보딩 화면에서 사이드바 메뉴를 숨기고 로그아웃만 노출하는 UX
  • @moyeorak/design-system 0.3.0 전역 CSS import 순서
  • 근무자/최소 인원 조건 폼의 React Hook Form + shared Zod 전환은 이번 UI 개선 PR 범위를 넘는 리팩터로 보고 후속 작업으로 분리 예정

- Pretendard 폰트 CDN 경로를 gh에서 npm으로 변경 (gh 경로 용량 제한으로 404)
- 사이드바 메뉴/트리거 호버 색상을 디자인 시스템 토큰으로 변경
- 근무자/최소 인원 조건 테이블의 수정·삭제 버튼을 IconButton으로 교체
- 메뉴/운영 설정 그룹 구분 제거, 6개 항목을 하나의 목록으로 통합
- 근무자 -> 근무자 관리, 가능 시간 -> 가능 시간 관리로 명칭 변경
- tsconfig.json 포맷팅 정리
@coderabbitai

coderabbitai Bot commented Jun 23, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

@moyeorak/design-system을 v0.3.0으로 업그레이드하면서 전역 CSS 진입점을 globals.css로 정리했고, 어드민 셸과 사이드바를 재구성했습니다. 공용 테이블·다이얼로그 컴포넌트를 추가해 staffing rules와 workers 페이지에 적용했으며, 일부 피처 페이지의 아이콘과 표시 문구도 조정했습니다.

Changes

design-system v0.3 업그레이드 및 UI 개선

Layer / File(s) Summary
CSS 진입점 재구성
apps/web/package.json, apps/web/src/app/globals.css, apps/web/src/app/layout.tsx, apps/web/tsconfig.json
@moyeorak/design-system 의존성을 ^0.3.0으로 올리고, globals.css에서 layers.csstailwindcsstailwind-pluginstyles.css 순서로 import를 재구성했다. Pretendard 폰트 CDN 경로를 npm/pretendard@1.3.9로 바꿨고, layout.tsxdist/style.css 직접 import를 제거했다. tsconfig.jsonjsx 옵션은 preserve로 변경됐다.
문서 지침 갱신
apps/web/CLAUDE.md, docs/stack.md
CLAUDE.md의 스타일링 규칙과 design-system 도입 지침이 v0.3 기준으로 갱신됐고, docs/stack.md에는 버전 표기와 Tailwind CSS v4 전역 import 순서가 추가됐다.
AdminShell과 AdminSidebar 정리
apps/web/src/components/layout/admin-shell.tsx, apps/web/src/components/layout/admin-sidebar.tsx
AdminShell에 상단 이동 버튼과 smooth scroll 핸들러가 추가됐다. AdminSidebar는 단일 navigationItems 배열, currentUser 표시, 아이콘 전용 로그아웃 링크, SidebarMenuButton hover 스타일로 정리됐다.
공용 테이블와 다이얼로그 컴포넌트 추가
apps/web/src/components/admin/admin-table-section.tsx, apps/web/src/components/admin/crud-form-dialog.tsx, apps/web/src/components/common/delete-confirm-dialog.tsx, apps/web/src/components/common/empty-state.tsx, apps/web/src/components/common/row-actions.tsx
목록 섹션, CRUD 폼, 삭제 확인, 빈 상태, 행 액션 UI가 공용 컴포넌트로 분리됐다. AdminTableSection은 empty state와 테이블 래퍼를 분기하고, CrudFormDialogDeleteConfirmDialog는 각각 폼 모달과 삭제 확인 모달을 렌더링한다. RowActions는 수정/삭제 아이콘 버튼을 제공한다.
Staffing Rules와 Workers 페이지 전환
apps/web/src/features/staffing-rules/components/mvp-staffing-rules-page.tsx, apps/web/src/features/workers/components/mvp-workers-page.tsx
두 페이지의 목록, 행 액션, 생성·수정 폼, 삭제 확인 UI가 새 공용 컴포넌트로 교체됐다. 헤더의 액션 텍스트는 sr-only 라벨로 바뀌었고, 삭제 시 배열 필터링과 선택 해제 흐름은 유지됐다.
피처 페이지 표시와 아이콘 조정
apps/web/src/features/availability/components/mvp-availability-page.tsx, apps/web/src/features/dashboard/components/mvp-dashboard-page.tsx, apps/web/src/features/auth/components/auth-brand.tsx, apps/web/src/features/schedule-history/components/mvp-schedule-history-page.tsx
availability의 선택 셀은 "가능" 텍스트 대신 체크 아이콘을 표시하고 aria-pressed를 추가했다. dashboardIcon 기반으로 바뀌고 Store/Download 아이콘 구성이 수정됐다. auth-brand compact 모드의 배경색이 제거됐고, schedule-history의 월 필터 텍스트가 단축됐다.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

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.
Title check ❓ Inconclusive 제목은 관련되지만 'UI 개선'이 너무 포괄적이라 변경 핵심을 바로 파악하기 어렵습니다. 대상 범위와 핵심 변경을 넣어 더 구체적으로 작성하세요. 예: 'web 관리자 사이드바 정리 및 design-system 0.3.0 적용'.
✅ Passed checks (3 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description check ✅ Passed 템플릿의 주요 섹션을 대부분 채웠고 변경 사항, 테스트 방법, 리뷰 포인트까지 구체적으로 작성했습니다.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/improve-ui

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@meteorqz6
meteorqz6 marked this pull request as draft June 23, 2026 06:32
@meteorqz6 meteorqz6 self-assigned this Jun 23, 2026
@meteorqz6 meteorqz6 changed the title Feature/improve UI feat(web): improve UI Jun 23, 2026
@meteorqz6 meteorqz6 changed the title feat(web): improve UI feat(web): UI 개선 Jun 24, 2026
@meteorqz6
meteorqz6 marked this pull request as ready for review June 24, 2026 15:07

@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: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/web/src/features/availability/components/mvp-availability-page.tsx (1)

473-483: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

선택 상태를 색상만으로 표현하지 않도록 보완이 필요합니다.

텍스트 제거 후 셀 상태가 색상 의존적으로 바뀌었습니다. 체크 아이콘/점 표시 같은 비색상 단서를 함께 넣어주세요.

수정 예시
 <button
   key={`${date}-${time}`}
   type="button"
   disabled={disabled}
+  aria-pressed={selected}
   aria-label={`${date} ${time} 가능 시간 ${selected ? "해제" : "선택"}`}
   className={cn(
     "h-8 rounded-md border text-[10px] transition-colors",
 ...
   )}
   onClick={() => toggleSlot(date, time)}
-></button>
+>
+  {selected ? <span aria-hidden="true">●</span> : null}
+</button>
🤖 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 `@apps/web/src/features/availability/components/mvp-availability-page.tsx`
around lines 473 - 483, The slot button in mvp-availability-page.tsx now
communicates selection only through color, so update the button rendered around
the toggleSlot handler to include a non-color visual cue such as a check icon or
dot when selected. Keep the existing aria-label and selected/disabled styling,
but make the selected state distinguishable in the button content or adjacent
markup so users can recognize it without relying on color alone.
🤖 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 `@apps/web/CLAUDE.md`:
- Around line 81-82: 문서의 import 순서 설명이 실제 `globals.css` 구성과 맞지 않습니다.
`CLAUDE.md`에서 `src/app/globals.css`를 설명하는 부분을 `layers.css → tailwindcss →
tailwind-plugin → styles.css`의 canonical 순서로 수정해, `tailwindcss` 누락과
`styles.css`/`tailwind-plugin` 순서 오류가 없도록 정리하세요. 해당 설명은 `src/app/globals.css`와
관련된 import 안내 문구를 기준으로 함께 맞춰 주세요.

In `@apps/web/src/app/globals.css`:
- Line 11: The `@font-face` in globals.css still points to a remote jsDelivr URL;
switch Pretendard to a local source instead. Update the font setup using
next/font/local or a local static asset and replace the existing src reference
in the font-face so the app no longer depends on the external CDN.

In `@apps/web/src/components/layout/admin-sidebar.tsx`:
- Around line 58-65: The logout control in admin-sidebar.tsx currently only
navigates to /login, so it never calls the session revocation endpoint. Update
the logout action around the Link/LogOut usage to perform a POST to
/api/auth/logout (or the defined logout route that revokes the current refresh
token) first, then redirect the user to /login after a successful response. Keep
the existing admin-sidebar component structure, but replace the direct
navigation behavior with an explicit logout handler so the server-side refresh
token is actually invalidated.

In `@apps/web/src/features/staffing-rules/components/mvp-staffing-rules-page.tsx`:
- Line 217: The action-column table header in mvp-staffing-rules-page.tsx is
currently empty, which removes its meaning for assistive technologies. Update
the <th> in the staffing rules table to keep an accessible label while remaining
visually hidden, using the existing table header structure near the action
column. Preserve the right-aligned styling, but replace the blank header with
screen-reader-only text so the column purpose stays available to non-visual
users.

In `@apps/web/src/features/workers/components/mvp-workers-page.tsx`:
- Line 169: The workers table action column currently has an empty header, which
removes useful table semantics for assistive technologies. Update the header
cell in mvp-workers-page.tsx to keep an accessibility-only label by adding
sr-only text inside the existing th for the action column, so the table
structure remains understandable without changing the visual layout.

---

Outside diff comments:
In `@apps/web/src/features/availability/components/mvp-availability-page.tsx`:
- Around line 473-483: The slot button in mvp-availability-page.tsx now
communicates selection only through color, so update the button rendered around
the toggleSlot handler to include a non-color visual cue such as a check icon or
dot when selected. Keep the existing aria-label and selected/disabled styling,
but make the selected state distinguishable in the button content or adjacent
markup so users can recognize it without relying on color alone.
🪄 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: 65c0234c-54bd-4347-a04d-e872935f0267

📥 Commits

Reviewing files that changed from the base of the PR and between 67c41c5 and 87e8cca.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (14)
  • apps/web/CLAUDE.md
  • apps/web/package.json
  • apps/web/src/app/globals.css
  • apps/web/src/app/layout.tsx
  • apps/web/src/components/layout/admin-shell.tsx
  • apps/web/src/components/layout/admin-sidebar.tsx
  • apps/web/src/features/auth/components/auth-brand.tsx
  • apps/web/src/features/availability/components/mvp-availability-page.tsx
  • apps/web/src/features/dashboard/components/mvp-dashboard-page.tsx
  • apps/web/src/features/schedule-history/components/mvp-schedule-history-page.tsx
  • apps/web/src/features/staffing-rules/components/mvp-staffing-rules-page.tsx
  • apps/web/src/features/workers/components/mvp-workers-page.tsx
  • apps/web/tsconfig.json
  • docs/stack.md
💤 Files with no reviewable changes (1)
  • apps/web/src/app/layout.tsx

Comment thread apps/web/CLAUDE.md Outdated
Comment thread apps/web/src/app/globals.css Outdated
Comment thread apps/web/src/components/layout/admin-sidebar.tsx Outdated
Comment thread apps/web/src/features/staffing-rules/components/mvp-staffing-rules-page.tsx Outdated
Comment thread apps/web/src/features/workers/components/mvp-workers-page.tsx Outdated

@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: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/web/src/features/dashboard/components/mvp-dashboard-page.tsx (1)

160-162: 🎯 Functional Correctness | 🟡 Minor

CSV export 버튼 아이콘을 export/download 의미로 맞춰 주세요.

현재 share 아이콘은 공유 동작으로 읽혀서, 확정 스케줄 CSV 내려받기와 의미가 어긋납니다. 다운로드/내보내기 의미가 더 분명한 아이콘으로 바꾸는 게 좋습니다.

🤖 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 `@apps/web/src/features/dashboard/components/mvp-dashboard-page.tsx` around
lines 160 - 162, The CSV export button in MVPDashboardPage is using a share icon
that suggests sharing rather than downloading/exporting. Update the leadingIcon
on the CSV export action to a download/export-appropriate icon in the same
component, keeping the button label and behavior unchanged while making the
intent clearer.

Source: Path instructions

♻️ Duplicate comments (1)
apps/web/src/features/workers/components/mvp-workers-page.tsx (1)

191-249: 📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift

폼 상태를 React Hook Form + Zod(packages/shared)로 전환해 주세요.

staffing-rules 페이지와 동일하게, 이 폼도 useState 기반 draft + 직접 검증을 사용합니다. RHF + Zod 조합과 packages/shared의 공유 스키마로 통일해 주세요.

As per coding guidelines, 모든 폼은 React Hook Form + Zod로 작성하고 스키마는 packages/shared에서 가져와야 합니다.

🤖 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 `@apps/web/src/features/workers/components/mvp-workers-page.tsx` around lines
191 - 249, This form still uses local draft state and manual validation instead
of the shared React Hook Form + Zod pattern. Update the worker form in
mvp-workers-page to use useForm with a Zod resolver, wire the fields through RHF
controls instead of draft/setDraft, and move validation to the shared schema
from packages/shared so it matches the staffing-rules page. Keep the existing
CrudFormDialog, saveWorker, and field rendering logic, but adapt them to consume
RHF form state, errors, and submission handling.

Source: Coding guidelines

🤖 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 `@apps/web/src/components/admin/crud-form-dialog.tsx`:
- Around line 29-66: `CrudFormDialog` is implemented as a manual overlay with
only `role="dialog"`, so it lacks Radix Dialog behavior such as Escape-to-close,
focus trapping, and focus restore. Update this component to use the same
Radix-based dialog pattern already used by the delete confirmation modal, moving
the current markup inside the shared Dialog content/wrapper and wiring
close/save actions through the Dialog primitives so keyboard and focus
management are handled consistently.

In `@apps/web/src/features/staffing-rules/components/mvp-staffing-rules-page.tsx`:
- Around line 240-326: The staffing rule dialog is still using local draft state
and a custom validator instead of the required React Hook Form + shared Zod
schema flow. Refactor the form in mvp-staffing-rules-page.tsx to use React Hook
Form for field state/submit handling, and import the staffing rule Zod schema
from packages/shared rather than defining validation in the web app. Wire the
existing fields in CrudFormDialog to the form controller, keep the
day/start/end/minimumWorkers validations in the shared schema, and remove
validateStaffingRuleDraft plus the useState-based draft logic.

---

Outside diff comments:
In `@apps/web/src/features/dashboard/components/mvp-dashboard-page.tsx`:
- Around line 160-162: The CSV export button in MVPDashboardPage is using a
share icon that suggests sharing rather than downloading/exporting. Update the
leadingIcon on the CSV export action to a download/export-appropriate icon in
the same component, keeping the button label and behavior unchanged while making
the intent clearer.

---

Duplicate comments:
In `@apps/web/src/features/workers/components/mvp-workers-page.tsx`:
- Around line 191-249: This form still uses local draft state and manual
validation instead of the shared React Hook Form + Zod pattern. Update the
worker form in mvp-workers-page to use useForm with a Zod resolver, wire the
fields through RHF controls instead of draft/setDraft, and move validation to
the shared schema from packages/shared so it matches the staffing-rules page.
Keep the existing CrudFormDialog, saveWorker, and field rendering logic, but
adapt them to consume RHF form state, errors, and submission handling.
🪄 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: 122e9d8a-0215-4e0d-a7c1-5e2b7402fda2

📥 Commits

Reviewing files that changed from the base of the PR and between 87e8cca and 522a929.

📒 Files selected for processing (10)
  • apps/web/CLAUDE.md
  • apps/web/src/components/admin/admin-table-section.tsx
  • apps/web/src/components/admin/crud-form-dialog.tsx
  • apps/web/src/components/common/delete-confirm-dialog.tsx
  • apps/web/src/components/common/empty-state.tsx
  • apps/web/src/components/common/row-actions.tsx
  • apps/web/src/features/availability/components/mvp-availability-page.tsx
  • apps/web/src/features/dashboard/components/mvp-dashboard-page.tsx
  • apps/web/src/features/staffing-rules/components/mvp-staffing-rules-page.tsx
  • apps/web/src/features/workers/components/mvp-workers-page.tsx

Comment thread apps/web/src/components/admin/crud-form-dialog.tsx
Comment on lines +240 to +326
<CrudFormDialog
open={formOpen}
title={formTitle}
titleId="staffing-rule-form-title"
description="특정 요일과 시간대에 필요한 최소 근무 인원을 입력하세요."
onClose={closeForm}
onSave={saveRule}
>
<div className="space-y-2">
<Label htmlFor="staffing-rule-day">요일</Label>
<Select
value={draft.dayOfWeek}
onValueChange={(value) =>
setDraft((current) => ({ ...current, dayOfWeek: value as DayOfWeek }))
}
>
<SelectTrigger
id="staffing-rule-day"
aria-invalid={submitted && Boolean(errors.dayOfWeek)}
>
<SelectValue placeholder="요일을 선택하세요" />
</SelectTrigger>
<SelectContent>
{Object.entries(DAY_LABELS).map(([value, label]) => (
<SelectItem key={value} value={value}>
{label}
</SelectItem>
))}
</SelectContent>
</Select>
{submitted && errors.dayOfWeek ? (
<p className="text-sm text-destructive">{errors.dayOfWeek}</p>
) : null}
</div>

{rules.length > 0 ? (
<div className="overflow-x-auto">
<table className="w-full min-w-2xl border-collapse text-left text-sm">
<thead className="bg-surface-secondary text-xs font-semibold uppercase text-muted-foreground">
<tr>
<th className="px-6 py-3">요일</th>
<th className="px-6 py-3">시작 시간</th>
<th className="px-6 py-3">종료 시간</th>
<th className="px-6 py-3">최소 인원</th>
<th className="px-6 py-3 text-right">액션</th>
</tr>
</thead>
<tbody className="divide-y divide-border">
{rules.map((rule) => (
<tr key={rule.id} className="bg-card">
<td className="px-6 py-4 font-medium text-foreground">
{DAY_LABELS[rule.dayOfWeek]}
</td>
<td className="px-6 py-4 text-muted-foreground">{rule.startTime}</td>
<td className="px-6 py-4 text-muted-foreground">{rule.endTime}</td>
<td className="px-6 py-4 text-muted-foreground">{rule.minimumWorkers}명</td>
<td className="px-6 py-4">
<div className="flex justify-end gap-2">
<Button type="button" variant="ghost" onClick={() => openEditForm(rule)}>
<Pencil className="size-4" aria-hidden="true" />
수정
</Button>
<Button
type="button"
variant="outline"
onClick={() => setDeleteTarget(rule)}
>
<Trash2 className="size-4" aria-hidden="true" />
삭제
</Button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
) : (
<div className="px-6 py-16 text-center">
<p className="text-sm font-medium text-foreground">등록된 최소 인원 조건이 없습니다.</p>
<p className="mt-2 text-sm text-muted-foreground">
조건을 추가하면 추천 생성 시 해당 시간대의 필요 인원으로 사용됩니다.
</p>
<div className="grid gap-4 sm:grid-cols-2">
<div className="space-y-2">
<Label htmlFor="staffing-rule-start-time">시작 시간</Label>
<Input
id="staffing-rule-start-time"
type="time"
value={draft.startTime}
aria-invalid={submitted && Boolean(errors.startTime)}
onChange={(event) =>
setDraft((current) => ({ ...current, startTime: event.target.value }))
}
/>
{submitted && errors.startTime ? (
<p className="text-sm text-destructive">{errors.startTime}</p>
) : null}
</div>
)}
</section>

{formOpen ? (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 px-4">
<section
role="dialog"
aria-modal="true"
aria-labelledby="staffing-rule-form-title"
className="w-full max-w-md rounded-xl border border-border bg-card p-6 shadow-modal"
>
<div className="flex items-start justify-between gap-4">
<div>
<h2 id="staffing-rule-form-title" className="text-lg font-semibold text-foreground">
{formTitle}
</h2>
<p className="mt-2 text-sm leading-6 text-muted-foreground">
특정 요일과 시간대에 필요한 최소 근무 인원을 입력하세요.
</p>
</div>
<button
type="button"
className="rounded-md p-1 text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
aria-label="닫기"
onClick={closeForm}
>
<X className="size-5" aria-hidden="true" />
</button>
</div>

<div className="mt-6 space-y-5">
<div className="space-y-2">
<Label htmlFor="staffing-rule-day">요일</Label>
<Select
value={draft.dayOfWeek}
onValueChange={(value) =>
setDraft((current) => ({ ...current, dayOfWeek: value as DayOfWeek }))
}
>
<SelectTrigger
id="staffing-rule-day"
aria-invalid={submitted && Boolean(errors.dayOfWeek)}
>
<SelectValue placeholder="요일을 선택하세요" />
</SelectTrigger>
<SelectContent>
{Object.entries(DAY_LABELS).map(([value, label]) => (
<SelectItem key={value} value={value}>
{label}
</SelectItem>
))}
</SelectContent>
</Select>
{submitted && errors.dayOfWeek ? (
<p className="text-sm text-destructive">{errors.dayOfWeek}</p>
) : null}
</div>

<div className="grid gap-4 sm:grid-cols-2">
<div className="space-y-2">
<Label htmlFor="staffing-rule-start-time">시작 시간</Label>
<Input
id="staffing-rule-start-time"
type="time"
value={draft.startTime}
aria-invalid={submitted && Boolean(errors.startTime)}
onChange={(event) =>
setDraft((current) => ({ ...current, startTime: event.target.value }))
}
/>
{submitted && errors.startTime ? (
<p className="text-sm text-destructive">{errors.startTime}</p>
) : null}
</div>

<div className="space-y-2">
<Label htmlFor="staffing-rule-end-time">종료 시간</Label>
<Input
id="staffing-rule-end-time"
type="time"
value={draft.endTime}
aria-invalid={submitted && Boolean(errors.endTime)}
onChange={(event) =>
setDraft((current) => ({ ...current, endTime: event.target.value }))
}
/>
{submitted && errors.endTime ? (
<p className="text-sm text-destructive">{errors.endTime}</p>
) : null}
</div>
</div>

<div className="space-y-2">
<Label htmlFor="minimum-workers">최소 인원</Label>
<Input
id="minimum-workers"
type="number"
min={1}
value={draft.minimumWorkers}
placeholder="예: 2"
aria-invalid={submitted && Boolean(errors.minimumWorkers)}
onChange={(event) =>
setDraft((current) => ({ ...current, minimumWorkers: event.target.value }))
}
/>
{submitted && errors.minimumWorkers ? (
<p className="text-sm text-destructive">{errors.minimumWorkers}</p>
) : null}
</div>
</div>
<div className="space-y-2">
<Label htmlFor="staffing-rule-end-time">종료 시간</Label>
<Input
id="staffing-rule-end-time"
type="time"
value={draft.endTime}
aria-invalid={submitted && Boolean(errors.endTime)}
onChange={(event) =>
setDraft((current) => ({ ...current, endTime: event.target.value }))
}
/>
{submitted && errors.endTime ? (
<p className="text-sm text-destructive">{errors.endTime}</p>
) : null}
</div>
</div>

<div className="mt-8 flex justify-end gap-2">
<Button type="button" variant="outline" onClick={closeForm}>
취소
</Button>
<Button type="button" variant="brand" onClick={saveRule}>
저장
</Button>
</div>
</section>
<div className="space-y-2">
<Label htmlFor="minimum-workers">최소 인원</Label>
<Input
id="minimum-workers"
type="number"
min={1}
value={draft.minimumWorkers}
placeholder="예: 2"
aria-invalid={submitted && Boolean(errors.minimumWorkers)}
onChange={(event) =>
setDraft((current) => ({ ...current, minimumWorkers: event.target.value }))
}
/>
{submitted && errors.minimumWorkers ? (
<p className="text-sm text-destructive">{errors.minimumWorkers}</p>
) : null}
</div>
) : null}
</CrudFormDialog>

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 | 🏗️ Heavy lift

폼 상태를 React Hook Form + Zod(packages/shared)로 전환해 주세요.

이 다이얼로그 폼은 useState 기반 draft와 직접 작성한 validateStaffingRuleDraft로 동작합니다. 프로젝트 규칙상 모든 폼은 React Hook Form + Zod 조합을 쓰고, Zod 스키마는 packages/shared에서 import해야 합니다(웹 내부 별도 정의 금지). 요일/시작·종료 시간/최소 인원(1 이상) 검증을 shared 스키마로 옮기면 API와 검증 규칙도 일관되게 유지됩니다.

As per coding guidelines, "모든 폼은 React Hook Form + Zod로 작성합니다. Zod 스키마는 packages/shared에서 가져옵니다. 웹에서 별도로 정의하지 않습니다."

🤖 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 `@apps/web/src/features/staffing-rules/components/mvp-staffing-rules-page.tsx`
around lines 240 - 326, The staffing rule dialog is still using local draft
state and a custom validator instead of the required React Hook Form + shared
Zod schema flow. Refactor the form in mvp-staffing-rules-page.tsx to use React
Hook Form for field state/submit handling, and import the staffing rule Zod
schema from packages/shared rather than defining validation in the web app. Wire
the existing fields in CrudFormDialog to the form controller, keep the
day/start/end/minimumWorkers validations in the shared schema, and remove
validateStaffingRuleDraft plus the useState-based draft logic.

Source: Coding guidelines

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

수고하셨습니다!

@ehlung
ehlung merged commit a074bac into develop Jun 24, 2026
2 checks passed
@ehlung
ehlung deleted the feature/improve-ui branch June 24, 2026 18:21
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