feat(web): UI 개선 - #64
Conversation
- Pretendard 폰트 CDN 경로를 gh에서 npm으로 변경 (gh 경로 용량 제한으로 404) - 사이드바 메뉴/트리거 호버 색상을 디자인 시스템 토큰으로 변경 - 근무자/최소 인원 조건 테이블의 수정·삭제 버튼을 IconButton으로 교체
- 메뉴/운영 설정 그룹 구분 제거, 6개 항목을 하나의 목록으로 통합 - 근무자 -> 근무자 관리, 가능 시간 -> 가능 시간 관리로 명칭 변경 - tsconfig.json 포맷팅 정리
Walkthrough
Changesdesign-system v0.3 업그레이드 및 UI 개선
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes 🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (14)
apps/web/CLAUDE.mdapps/web/package.jsonapps/web/src/app/globals.cssapps/web/src/app/layout.tsxapps/web/src/components/layout/admin-shell.tsxapps/web/src/components/layout/admin-sidebar.tsxapps/web/src/features/auth/components/auth-brand.tsxapps/web/src/features/availability/components/mvp-availability-page.tsxapps/web/src/features/dashboard/components/mvp-dashboard-page.tsxapps/web/src/features/schedule-history/components/mvp-schedule-history-page.tsxapps/web/src/features/staffing-rules/components/mvp-staffing-rules-page.tsxapps/web/src/features/workers/components/mvp-workers-page.tsxapps/web/tsconfig.jsondocs/stack.md
💤 Files with no reviewable changes (1)
- apps/web/src/app/layout.tsx
There was a problem hiding this comment.
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 | 🟡 MinorCSV 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
📒 Files selected for processing (10)
apps/web/CLAUDE.mdapps/web/src/components/admin/admin-table-section.tsxapps/web/src/components/admin/crud-form-dialog.tsxapps/web/src/components/common/delete-confirm-dialog.tsxapps/web/src/components/common/empty-state.tsxapps/web/src/components/common/row-actions.tsxapps/web/src/features/availability/components/mvp-availability-page.tsxapps/web/src/features/dashboard/components/mvp-dashboard-page.tsxapps/web/src/features/staffing-rules/components/mvp-staffing-rules-page.tsxapps/web/src/features/workers/components/mvp-workers-page.tsx
| <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> |
There was a problem hiding this comment.
📐 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
개요
웹 관리자 화면의 전반적인 UI를 정리하고,
@moyeorak/design-system0.3.0 적용에 맞춰 전역 스타일 import 구조와 공용 관리 UI 컴포넌트를 개선했습니다.변경 유형
변경 사항
디자인 시스템 및 전역 스타일
@moyeorak/design-system버전을^0.3.0으로 업데이트했습니다.layers.css→tailwindcss→tailwind-plugin→styles.css로 정리했습니다.layout.tsx의@moyeorak/design-system/dist/style.css전역 import를 제거했습니다.@font-face에서next/font/local+ 로컬 폰트 asset 방식으로 변경했습니다.tsconfig.json의 JSX 설정을 Next.js 빌드 권장값인preserve로 정리했습니다.관리자 레이아웃 및 사이드바
근무자→근무자 관리가능 시간→가능 시간 관리/login으로 이동하도록 변경했습니다./organization/new온보딩 화면에서도 관리자 레이아웃을 사용하되, 사이드바 메뉴는 숨기고 로그아웃 영역만 노출하도록 변경했습니다.FloatingButton을 추가했습니다.공용 관리 UI 컴포넌트
AdminTableSectionCrudFormDialogDeleteConfirmDialogEmptyStateRowActionsCrudFormDialog를 Radix Dialog 기반으로 전환해 Escape 닫기, focus trap, focus restore 동작을 보강했습니다.RowActions+IconButton기반으로 정리했습니다.화면별 UI 정리
aria-pressed와 체크 아이콘을 추가해 선택 상태를 색상만으로 표현하지 않도록 보완했습니다.sr-only라벨을 추가해 표 접근성을 보강했습니다.영향 범위
@moyeorak/design-system, Radix Dialog 관련 웹 의존성테스트 방법
pnpm --filter @fragment/web typecheckpnpm --filter @fragment/web build/organization/new에서 사이드바 메뉴가 숨겨지고 로그아웃 영역만 사용 가능한지 확인체크리스트
스크린샷/영상
리뷰 포인트
CrudFormDialog를 Radix Dialog 기반으로 바꾼 부분의 키보드 접근성 동작/organization/new온보딩 화면에서 사이드바 메뉴를 숨기고 로그아웃만 노출하는 UX@moyeorak/design-system0.3.0 전역 CSS import 순서