Feat(#95): 백오피스 대시보드 페이지 퍼블리싱#103
Conversation
- BO02 대시보드 화면을 기존 backoffice UI 컴포넌트 기반으로 구성(page.tsx) - 추이 분석, 비용 및 시스템 상태, 어뷰징 모니터링, 추가 분석 섹션 추가(page.tsx) - 차트 축/범례, 테이블 caption, progressbar aria 속성 등 접근성 보강(page.tsx) - 대시보드 배경이 하단까지 유지되도록 body 기본 배경색 지정(layout.tsx)
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Warning Review limit reached
More reviews will be available in 22 minutes and 29 seconds. Learn how PR review limits work. Your organization has run out of usage credits. Purchase more in the billing tab. ⌛ How to resolve this issue?After more reviews become available, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available. Please see our Fair Usage Limits Policy for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (6)
📝 WalkthroughWalkthrough도메인 타입·정적 데이터부터 레이아웃, 페이지 조성, 섹션 위젯, Recharts 차트 컴포넌트를 포함하는 완전한 Back-office admin 대시보드가 추가됩니다. ChangesAdmin Dashboard Feature
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 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 |
컴포넌트 분리 권장현재 페이지 컴포넌트 책임이 너무 크기때문에 FSD 아키텍처 공식문서에 따라 분리해주세요.
차트의경우 적절한 라이브러리를 추가해주세요. |
- BO02 대시보드 라우트 엔트리를 얇게 정리하고 화면 조립을 src/pages/dashboard로 이동 - 대시보드 데이터와 타입을 entities/admin-dashboard로 분리 - 사이드바 레이아웃과 대시보드 섹션을 widgets 계층으로 분리 - 직접 SVG로 구현된 추이 차트를 Recharts 기반 컴포넌트로 교체 - Next App Router와 FSD src/pages 충돌 방지를 위한 루트 pages README 추가
There was a problem hiding this comment.
Actionable comments posted: 12
🤖 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/back-office/src/entities/admin-dashboard/model/dashboard-data.ts`:
- Around line 149-157: The dashboard data currently includes raw email addresses
in the title fields (e.g., the object literals with title:
'user-7f3k2@example.com' and title: 'admin@testcompany.io'); replace these raw
emails with masked identifiers or internal IDs by applying a masking step to the
title property (implement or call a util like maskEmail and use it when
constructing the objects or replace the literal values with masked strings such
as 'user-****' / 'admin-****'), and ensure any other occurrences in the same
exported array or constant are updated to avoid PII exposure (update the title
properties in the same data structure and any helper/code that consumes title to
accept masked form).
In `@apps/back-office/src/widgets/back-office-layout/ui/back-office-layout.tsx`:
- Line 28: Replace the brittle label-based current-menu check (item.label ===
'대시보드') with a pathname/key/href comparison: obtain the current pathname (e.g.,
via useRouter().pathname or window.location.pathname), then set isCurrent on
NavItem by comparing item.href (or item.key) to that pathname so aria-current is
driven by item.href/key rather than item.label; update the places using
isCurrent (the checks at the spots referencing item.label, e.g., the expressions
around isCurrent on lines like 28 and 33) to use this new href/key vs pathname
comparison.
- Line 40: The anchor uses href={isCurrent ? '`#dashboard-main`' : '#'} which
causes top-jump for inactive items; update the BackOfficeLayout menu rendering
to avoid href="#" for non-current entries: when a real route/path is available
set the actual path instead of '#', and for unimplemented/disabled items render
a <button> (or an anchor with role="button") with aria-disabled="true" and
keyboard focus handling; locate the conditional using isCurrent in the component
(e.g., the element that sets href={isCurrent ? '`#dashboard-main`' : '#'}) and
replace the false-branch with either the real route value or a disabled control
using aria-disabled so screen reader and keyboard UX are preserved.
In `@apps/back-office/src/widgets/dashboard/ui/additional-analysis-section.tsx`:
- Around line 30-41: The progress values used from item[2] are not clamped or
normalized, so invalid or out-of-range inputs break both the visual width and
screen-reader attributes; update the rendering for the progress bar(s) that use
item[2] (both the outer progress container attributes
aria-valuenow/aria-valuetext and the inner div style width) to parse the value
to a number, clamp it to the 0–100 range, and use the clamped numeric value for
aria-valuenow and aria-valuetext and the clamped value suffixed with "%" for the
inner div style.width; apply the same normalization wherever item[2] is used for
progress (the two blocks around the progressbar rendering).
In `@apps/back-office/src/widgets/dashboard/ui/alerts-section.tsx`:
- Around line 10-11: The map over alerts currently uses alert.title as the React
key which can collide for items with identical titles; update the key in the
alerts.map callback to use a stable unique identifier (e.g., alert.id) or, if no
id exists, combine title with the iteration index (e.g.,
`${alert.title}-${index}`) so the JSX element rendered in the alerts.map (the
<article> created in alerts-section.tsx) has a non-colliding, stable key.
In `@apps/back-office/src/widgets/dashboard/ui/cost-spike-section.tsx`:
- Around line 42-45: The table cell uses key={value} inside the project.map
callback which can duplicate when values repeat; update the key to combine a
stable row identifier with the index (or the map index) to guarantee uniqueness
— e.g., in the project.map callback change key={value} to a composite key like
`${someRowId}-${index}` or `${value}-${index}` (locate the project.map rendering
in cost-spike-section.tsx and the JSX that currently uses key={value}).
In `@apps/back-office/src/widgets/dashboard/ui/cost-system-section.tsx`:
- Around line 69-81: Clamp the progress value before rendering to ensure aria
and visual width stay within 0–100: compute a clamped value (e.g., const
clampedPercent = Math.min(100, Math.max(0, item.percent))) and use
clampedPercent wherever item.percent is currently used in this component —
specifically replace usages in aria-valuenow and the inner div style width (the
element that sets style={{ width: `${item.percent}%` }}) and any other
references to item.percent in this JSX.
In `@apps/back-office/src/widgets/dashboard/ui/dashboard-header.tsx`:
- Around line 24-26: The hardcoded "7분 전" must be replaced with a dynamic value:
accept a lastUpdatedAt prop (e.g., Date | string) in the DashboardHeader
component and render a computed time string instead of the literal; compute
either a relative time (using a helper like formatDistanceToNow from date-fns or
an in-house timeAgo util) or an absolute timestamp (e.g., new
Date(lastUpdatedAt).toLocaleString) and place that string inside the existing
<span aria-live="polite"> so it updates accessibly; ensure you
validate/normalize lastUpdatedAt and handle null/invalid dates by showing a
fallback like "알 수 없음".
In `@apps/back-office/src/widgets/dashboard/ui/metrics-section.tsx`:
- Around line 14-15: The current key for items in the metrics.map render uses
metric.label which can collide; update the key on the <div> inside the
metrics.map in metrics-section.tsx to use a stable unique identifier (prefer an
explicit id field like metric.id) and if no id exists, construct a deterministic
fallback such as combining label with the map index (e.g.
`${metric.label}-${index}`) to avoid duplicate keys and ensure proper React
reconciliation.
In `@apps/back-office/src/widgets/dashboard/ui/select-styles.ts`:
- Around line 1-10: The Tailwind classes use the deprecated prefix important
syntax; update all occurrences in selectTriggerClassName, selectValueClassName,
selectContentClassName, and selectItemClassName to the postfix important form
(move each leading/trailing "!" to the end of the utility token, e.g.
"!border-border" -> "border-border!"), keeping the same utilities and states
(hover:, focus-visible:, data-[state=...], data-[highlighted],
data-[state=checked]) so the semantics are identical; do this consistently
across the four exported constants to align with Tailwind v4 recommended syntax.
In `@apps/back-office/src/widgets/dashboard/ui/trend-analysis-section.tsx`:
- Around line 78-99: The Select control (Select.Root / Select.Trigger /
Select.Value) currently has no onValueChange handler so the chosen aggregation
never updates the data at the chart usage around the code referenced at line
107; wire Select.Root's onValueChange to a React state (e.g., setAggregation)
and use that state to compute/choose the data transformation passed to the chart
(or the variable used at the chart render site), ensuring values
"누적"/"일별"/"주간"/"월간" map to the proper data selector; alternatively, if you
cannot implement the transformation now, disable or remove the Select control
until the behavior is implemented.
In `@apps/back-office/src/widgets/dashboard/ui/trend-charts.tsx`:
- Around line 30-33: The YAxis components in trend-charts.tsx currently use
fixed domains (e.g., domain={[0, 4]}) which can truncate real data; update each
YAxis instance (the YAxis usages around the tick/axisStyle occurrences) to use a
data-driven domain such as ['dataMin', 'dataMax'] and add a small upper padding
(e.g., multiply/add a margin to dataMax or use a function domain like (dataMin,
dataMax) => [dataMin, dataMax * 1.05]) so charts never clip values; adjust ticks
if needed to remain consistent with the new dynamic domain.
🪄 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
Run ID: 0966fd8a-774b-4d9a-9934-6b30f34754df
⛔ Files ignored due to path filters (3)
apps/back-office/app/layout.tsxis excluded by none and included by noneapps/back-office/app/page.tsxis excluded by none and included by noneapps/back-office/pages/README.mdis excluded by none and included by none
📒 Files selected for processing (18)
apps/back-office/src/entities/admin-dashboard/index.tsapps/back-office/src/entities/admin-dashboard/model/dashboard-data.tsapps/back-office/src/entities/admin-dashboard/model/types.tsapps/back-office/src/pages/dashboard/index.tsapps/back-office/src/pages/dashboard/ui/dashboard-page.tsxapps/back-office/src/widgets/back-office-layout/index.tsapps/back-office/src/widgets/back-office-layout/ui/back-office-layout.tsxapps/back-office/src/widgets/dashboard/index.tsapps/back-office/src/widgets/dashboard/ui/abuse-monitoring-section.tsxapps/back-office/src/widgets/dashboard/ui/additional-analysis-section.tsxapps/back-office/src/widgets/dashboard/ui/alerts-section.tsxapps/back-office/src/widgets/dashboard/ui/cost-spike-section.tsxapps/back-office/src/widgets/dashboard/ui/cost-system-section.tsxapps/back-office/src/widgets/dashboard/ui/dashboard-header.tsxapps/back-office/src/widgets/dashboard/ui/metrics-section.tsxapps/back-office/src/widgets/dashboard/ui/select-styles.tsapps/back-office/src/widgets/dashboard/ui/trend-analysis-section.tsxapps/back-office/src/widgets/dashboard/ui/trend-charts.tsx
| title: 'user-7f3k2@example.com', | ||
| description: '프로젝트: AI 챗봇 테스트', | ||
| value: '47,234회', | ||
| helper: '오늘', | ||
| tone: 'border-red-200 bg-red-50 text-red-600', | ||
| }, | ||
| { | ||
| title: 'admin@testcompany.io', | ||
| description: '프로젝트: E-commerce QA', |
There was a problem hiding this comment.
사용자 식별자(이메일) 원문 노출을 제거해 주세요.
Line 149, Line 156에서 이메일 형태 식별자를 그대로 렌더링하고 있어 운영/데모 환경에서 PII 노출 리스크가 있습니다. 마스킹된 식별자(예: user-****)나 내부 ID로 대체하는 편이 안전합니다.
수정 예시
- title: 'user-7f3k2@example.com',
+ title: 'user-7f3k2',
...
- title: 'admin@testcompany.io',
+ title: 'admin-ops-01',📝 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.
| title: 'user-7f3k2@example.com', | |
| description: '프로젝트: AI 챗봇 테스트', | |
| value: '47,234회', | |
| helper: '오늘', | |
| tone: 'border-red-200 bg-red-50 text-red-600', | |
| }, | |
| { | |
| title: 'admin@testcompany.io', | |
| description: '프로젝트: E-commerce QA', | |
| title: 'user-7f3k2', | |
| description: '프로젝트: AI 챗봇 테스트', | |
| value: '47,234회', | |
| helper: '오늘', | |
| tone: 'border-red-200 bg-red-50 text-red-600', | |
| }, | |
| { | |
| title: 'admin-ops-01', | |
| description: '프로젝트: E-commerce QA', |
🤖 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/back-office/src/entities/admin-dashboard/model/dashboard-data.ts` around
lines 149 - 157, The dashboard data currently includes raw email addresses in
the title fields (e.g., the object literals with title: 'user-7f3k2@example.com'
and title: 'admin@testcompany.io'); replace these raw emails with masked
identifiers or internal IDs by applying a masking step to the title property
(implement or call a util like maskEmail and use it when constructing the
objects or replace the literal values with masked strings such as 'user-****' /
'admin-****'), and ensure any other occurrences in the same exported array or
constant are updated to avoid PII exposure (update the title properties in the
same data structure and any helper/code that consumes title to accept masked
form).
| </div> | ||
| <nav aria-label="Back office 주요 메뉴" className="flex flex-col gap-1 px-3 py-4 text-sm"> | ||
| {navItems.map((item) => { | ||
| const isCurrent = item.label === '대시보드'; |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win
현재 메뉴 판별을 라벨 문자열에 의존하지 않는 구조로 바꿔주세요.
Line 28처럼 item.label === '대시보드'에 의존하면 라벨 변경/번역 시 aria-current가 쉽게 깨집니다. NavItem에 key/href/isCurrent(또는 현재 pathname 비교 기준)를 두고 판별하는 방식이 안전합니다.
Also applies to: 33-33
🤖 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/back-office/src/widgets/back-office-layout/ui/back-office-layout.tsx` at
line 28, Replace the brittle label-based current-menu check (item.label ===
'대시보드') with a pathname/key/href comparison: obtain the current pathname (e.g.,
via useRouter().pathname or window.location.pathname), then set isCurrent on
NavItem by comparing item.href (or item.key) to that pathname so aria-current is
driven by item.href/key rather than item.label; update the places using
isCurrent (the checks at the spots referencing item.label, e.g., the expressions
around isCurrent on lines like 28 and 33) to use this new href/key vs pathname
comparison.
| ? 'bg-[#155DFC]/10 text-[#155DFC]' | ||
| : 'text-text-secondary hover:text-text-primary hover:bg-gray-100', | ||
| ].join(' ')} | ||
| href={isCurrent ? '#dashboard-main' : '#'} |
There was a problem hiding this comment.
비활성 메뉴에 href="#"를 사용하지 마세요.
Line 40에서 현재 페이지가 아닌 메뉴 클릭 시 실제 이동 없이 상단 점프만 발생합니다. 키보드/스크린리더 사용자 UX도 나빠집니다. 실제 경로를 연결하거나, 미구현 항목은 button + aria-disabled로 처리해 주세요.
🤖 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/back-office/src/widgets/back-office-layout/ui/back-office-layout.tsx` at
line 40, The anchor uses href={isCurrent ? '`#dashboard-main`' : '#'} which causes
top-jump for inactive items; update the BackOfficeLayout menu rendering to avoid
href="#" for non-current entries: when a real route/path is available set the
actual path instead of '#', and for unimplemented/disabled items render a
<button> (or an anchor with role="button") with aria-disabled="true" and
keyboard focus handling; locate the conditional using isCurrent in the component
(e.g., the element that sets href={isCurrent ? '`#dashboard-main`' : '#'}) and
replace the false-branch with either the real route value or a disabled control
using aria-disabled so screen reader and keyboard UX are preserved.
| <div | ||
| className="h-3 overflow-hidden rounded-full bg-gray-100" | ||
| role="progressbar" | ||
| aria-label={`${item[0]} 전환율`} | ||
| aria-valuemin={0} | ||
| aria-valuemax={100} | ||
| aria-valuenow={Number.parseFloat(item[2])} | ||
| aria-valuetext={item[2]} | ||
| > | ||
| <div | ||
| className="h-full rounded-full bg-[#155DFC]" | ||
| style={{ width: item[2] }} |
There was a problem hiding this comment.
퍼널/스토리지 진행률도 0~100 보정이 필요합니다.
Line 3637, Line 7879 및 Line 41/83에서 원본 값을 그대로 쓰고 있어, 비정상 퍼센트 입력 시 시각 표시와 스크린리더 값이 깨집니다. 두 블록 모두 정규화된 퍼센트를 기준으로 aria-valuenow와 width를 함께 맞춰주세요.
Also applies to: 72-83
🤖 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/back-office/src/widgets/dashboard/ui/additional-analysis-section.tsx`
around lines 30 - 41, The progress values used from item[2] are not clamped or
normalized, so invalid or out-of-range inputs break both the visual width and
screen-reader attributes; update the rendering for the progress bar(s) that use
item[2] (both the outer progress container attributes
aria-valuenow/aria-valuetext and the inner div style width) to parse the value
to a number, clamp it to the 0–100 range, and use the clamped numeric value for
aria-valuenow and aria-valuetext and the clamped value suffixed with "%" for the
inner div style.width; apply the same normalization wherever item[2] is used for
progress (the two blocks around the progressbar rendering).
| {alerts.map((alert) => ( | ||
| <article key={alert.title} className={`rounded-lg border px-5 py-4 ${alert.tone}`}> |
There was a problem hiding this comment.
리스트 key를 표시 문자열(title)에만 의존하지 마세요.
동일 제목 알림이 들어오면 key 충돌이 발생할 수 있습니다. 안정적인 식별자를 쓰거나 최소한 인덱스를 조합해 충돌을 피하는 편이 안전합니다.
수정 예시
- {alerts.map((alert) => (
- <article key={alert.title} className={`rounded-lg border px-5 py-4 ${alert.tone}`}>
+ {alerts.map((alert, index) => (
+ <article key={`${alert.title}-${index}`} className={`rounded-lg border px-5 py-4 ${alert.tone}`}>📝 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.
| {alerts.map((alert) => ( | |
| <article key={alert.title} className={`rounded-lg border px-5 py-4 ${alert.tone}`}> | |
| {alerts.map((alert, index) => ( | |
| <article key={`${alert.title}-${index}`} className={`rounded-lg border px-5 py-4 ${alert.tone}`}> |
🤖 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/back-office/src/widgets/dashboard/ui/alerts-section.tsx` around lines 10
- 11, The map over alerts currently uses alert.title as the React key which can
collide for items with identical titles; update the key in the alerts.map
callback to use a stable unique identifier (e.g., alert.id) or, if no id exists,
combine title with the iteration index (e.g., `${alert.title}-${index}`) so the
JSX element rendered in the alerts.map (the <article> created in
alerts-section.tsx) has a non-colliding, stable key.
| <span className="text-text-secondary text-sm" aria-live="polite"> | ||
| 마지막 갱신: 7분 전 | ||
| </span> |
There was a problem hiding this comment.
갱신 시각 문구를 하드코딩하지 마세요.
Line 25의 7분 전은 곧바로 잘못된 정보가 됩니다. lastUpdatedAt 값을 받아 상대시간을 계산하거나 절대시각(예: 2026-05-19 16:20)으로 표시해 정확도를 유지해 주세요.
🤖 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/back-office/src/widgets/dashboard/ui/dashboard-header.tsx` around lines
24 - 26, The hardcoded "7분 전" must be replaced with a dynamic value: accept a
lastUpdatedAt prop (e.g., Date | string) in the DashboardHeader component and
render a computed time string instead of the literal; compute either a relative
time (using a helper like formatDistanceToNow from date-fns or an in-house
timeAgo util) or an absolute timestamp (e.g., new
Date(lastUpdatedAt).toLocaleString) and place that string inside the existing
<span aria-live="polite"> so it updates accessibly; ensure you
validate/normalize lastUpdatedAt and handle null/invalid dates by showing a
fallback like "알 수 없음".
| {metrics.map((metric) => ( | ||
| <div key={metric.label} className="border-border shadow-1 rounded-lg border bg-white p-5"> |
There was a problem hiding this comment.
metric.label 단독 key 사용은 충돌 위험이 있습니다.
지표명이 중복되면 렌더링 갱신이 비정상 동작할 수 있습니다. key를 더 안정적으로 구성해 주세요.
수정 예시
- {metrics.map((metric) => (
- <div key={metric.label} className="border-border shadow-1 rounded-lg border bg-white p-5">
+ {metrics.map((metric, index) => (
+ <div key={`${metric.label}-${index}`} className="border-border shadow-1 rounded-lg border bg-white p-5">📝 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.
| {metrics.map((metric) => ( | |
| <div key={metric.label} className="border-border shadow-1 rounded-lg border bg-white p-5"> | |
| {metrics.map((metric, index) => ( | |
| <div key={`${metric.label}-${index}`} className="border-border shadow-1 rounded-lg border bg-white p-5"> |
🤖 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/back-office/src/widgets/dashboard/ui/metrics-section.tsx` around lines
14 - 15, The current key for items in the metrics.map render uses metric.label
which can collide; update the key on the <div> inside the metrics.map in
metrics-section.tsx to use a stable unique identifier (prefer an explicit id
field like metric.id) and if no id exists, construct a deterministic fallback
such as combining label with the map index (e.g. `${metric.label}-${index}`) to
avoid duplicate keys and ensure proper React reconciliation.
| export const selectTriggerClassName = | ||
| '!border-border !bg-white !text-text-primary hover:!border-[#155DFC] focus-visible:!outline-[#155DFC] data-[state=open]:!border-[#155DFC] data-[state=open]:!outline-[#155DFC] [&>svg]:!text-text-primary'; | ||
|
|
||
| export const selectValueClassName = | ||
| '!text-text-primary data-[state=filled]:!text-text-primary data-[state=empty]:!text-text-secondary'; | ||
|
|
||
| export const selectContentClassName = '!border-border !bg-white !text-text-primary'; | ||
|
|
||
| export const selectItemClassName = | ||
| '!text-text-primary data-[highlighted]:!bg-[#155DFC]/10 data-[highlighted]:!text-text-primary data-[state=checked]:!text-[#155DFC]'; |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# 목적: Tailwind v4 사용 여부와 legacy leading-! 유틸리티 존재 확인
rg -n --iglob '**/package.json' '"tailwindcss"\s*:\s*"[^"]*4\.[^"]*"'
rg -nP "(^|\\s)![-\\w:\\[\\]#./%]+" apps/back-office/src/widgets/dashboard/ui/select-styles.tsRepository: DataArts-Studio/web-mvp-front
Length of output: 486
🌐 Web query:
Tailwind CSS v4 important modifier syntax prefix postfix breaking changes
💡 Result:
In Tailwind CSS v4, there are significant changes to how the important modifier and prefixes are handled compared to v3. Important Modifier Syntax In v3, the important modifier was placed at the beginning of a utility class (e.g.,!flex). In v4, this syntax has changed: you must now place the! at the end of the class name (e.g., flex!) [1]. While the old v3 syntax is still supported for backward compatibility, it is deprecated [1]. Global Important Flag If you need to make all utility declarations!important, v4 provides a new way to do this via the CSS-first configuration. You can add the important flag after the @import statement for Tailwind utilities in your CSS file [2]: @import "tailwindcss/utilities" important; This is the v4 equivalent of setting important: true in the old tailwind.config.js file [2]. Note that when this global flag is enabled, it also affects declarations generated by the @apply directive [3]. Prefix Syntax Prefixes in v4 function differently than in v3. They are now treated as variants and must be placed at the beginning of the class name, followed by a colon (e.g., tw:flex) [4][5]. To configure a prefix, you use the prefix function when importing Tailwind in your CSS [4][5]: @import "tailwindcss" prefix(tw); Key differences include: - Position: Prefixes are always the first part of the utility, even before other variants (e.g., tw:hover:bg-white) [5]. - Scope: Prefixes apply to both utility classes and generated CSS variables (where they are separated by a dash, e.g., --tw-color-white) [5]. - Constraints: Only lowercase alpha characters (a-z) are allowed for prefixes [5]. These changes are part of the broader shift in v4 toward a CSS-first configuration model, moving away from the reliance on tailwind.config.js [6][7].
Citations:
- 1: https://tailwindcss.com/docs/upgrade-guide
- 2: Add support for
importantin v4 tailwindlabs/tailwindcss#14448 - 3: [v4] Breaking change: When using the important flag, @apply now also make all of the declarations !important tailwindlabs/tailwindcss#17448
- 4: https://v3.tailwindcss.com/docs/v4-beta
- 5: Add support for prefixes tailwindlabs/tailwindcss#14501
- 6: https://tailwindcss.com/blog/tailwindcss-v4
- 7: Upgrading to Tailwind v4: Missing Defaults, Broken Dark Mode, and Config Issues tailwindlabs/tailwindcss#16517
🏁 Script executed:
# Check for Tailwind configuration files and migration notes
fd -e 'js|ts|json' -p 'tailwind.config'
find . -name 'CHANGELOG*' -o -name 'MIGRATION*' | head -5Repository: DataArts-Studio/web-mvp-front
Length of output: 55
🏁 Script executed:
# Search for Tailwind configuration files
find . -type f \( -name 'tailwind.config.*' -o -name 'tailwind.config.js' -o -name 'tailwind.config.ts' \) | head -10
# Check for CSS imports in main entry files
fd -e 'css' -p 'global|index|main|app' apps/back-office/src 2>/dev/null | head -5Repository: DataArts-Studio/web-mvp-front
Length of output: 116
🏁 Script executed:
# Search for `@import` statements in CSS files that might set important flag
rg -g '*.css' '`@import`.*important|important.*`@import`' apps/back-officeRepository: DataArts-Studio/web-mvp-front
Length of output: 55
🏁 Script executed:
cat apps/back-office/src/app-shell/globals.cssRepository: DataArts-Studio/web-mvp-front
Length of output: 8892
Tailwind v4 호환성: prefix 문법은 deprecated이지만 여전히 동작합니다.
현재 클래스는 Tailwind v3 prefix 문법(!utility)을 사용 중입니다. v4에서도 하위 호환성으로 지원되어 스타일이 정상 적용되지만, 공식 문서에서 postfix 문법(utility!)으로의 마이그레이션을 권장합니다. 전역 important 플래그가 설정되지 않은 상태이므로 현재 명시적 수정자는 필요하며, 향후 코드베이스 마이그레이션 시 일괄 전환 권장.
🤖 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/back-office/src/widgets/dashboard/ui/select-styles.ts` around lines 1 -
10, The Tailwind classes use the deprecated prefix important syntax; update all
occurrences in selectTriggerClassName, selectValueClassName,
selectContentClassName, and selectItemClassName to the postfix important form
(move each leading/trailing "!" to the end of the utility token, e.g.
"!border-border" -> "border-border!"), keeping the same utilities and states
(hover:, focus-visible:, data-[state=...], data-[highlighted],
data-[state=checked]) so the semantics are identical; do this consistently
across the four exported constants to align with Tailwind v4 recommended syntax.
| <Select.Root defaultValue="누적" size="md"> | ||
| <Select.Trigger | ||
| aria-label="콘텐츠 생산량 집계 기준" | ||
| className={selectTriggerClassName} | ||
| > | ||
| <Select.Value placeholder="집계 기준" className={selectValueClassName} /> | ||
| </Select.Trigger> | ||
| <Select.Content className={selectContentClassName}> | ||
| <Select.Item value="누적" className={selectItemClassName}> | ||
| 누적 | ||
| </Select.Item> | ||
| <Select.Item value="일별" className={selectItemClassName}> | ||
| 일별 | ||
| </Select.Item> | ||
| <Select.Item value="주간" className={selectItemClassName}> | ||
| 주간 | ||
| </Select.Item> | ||
| <Select.Item value="월간" className={selectItemClassName}> | ||
| 월간 | ||
| </Select.Item> | ||
| </Select.Content> | ||
| </Select.Root> |
There was a problem hiding this comment.
집계 기준 Select가 현재 동작하지 않습니다.
Line 78~99에서 집계 기준을 고를 수 있지만, Line 107에 전달되는 데이터는 항상 동일해서 차트가 바뀌지 않습니다. 사용자 입장에서 기능 오동작으로 보이므로, 선택값 상태(onValueChange)를 연결해 데이터 변환을 적용하거나 구현 전에는 비활성/제거 처리해 주세요.
Also applies to: 107-107
🤖 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/back-office/src/widgets/dashboard/ui/trend-analysis-section.tsx` around
lines 78 - 99, The Select control (Select.Root / Select.Trigger / Select.Value)
currently has no onValueChange handler so the chosen aggregation never updates
the data at the chart usage around the code referenced at line 107; wire
Select.Root's onValueChange to a React state (e.g., setAggregation) and use that
state to compute/choose the data transformation passed to the chart (or the
variable used at the chart render site), ensuring values "누적"/"일별"/"주간"/"월간" map
to the proper data selector; alternatively, if you cannot implement the
transformation now, disable or remove the Select control until the behavior is
implemented.
| <YAxis | ||
| domain={[0, 4]} | ||
| ticks={[0, 1, 2, 3, 4]} | ||
| tick={axisStyle} |
There was a problem hiding this comment.
Y축 고정 도메인으로 데이터가 잘릴 수 있습니다.
Line 3033, Line 5052, Line 93~94의 고정 도메인은 실제 값이 상한을 넘는 순간 차트를 잘라서 보여줍니다. 대시보드 해석 정확도에 직접 영향이 있으니 데이터 기반 도메인(예: ['dataMin', 'dataMax'] + 상단 패딩)으로 바꿔주세요.
Also applies to: 50-52, 93-94
🤖 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/back-office/src/widgets/dashboard/ui/trend-charts.tsx` around lines 30 -
33, The YAxis components in trend-charts.tsx currently use fixed domains (e.g.,
domain={[0, 4]}) which can truncate real data; update each YAxis instance (the
YAxis usages around the tick/axisStyle occurrences) to use a data-driven domain
such as ['dataMin', 'dataMax'] and add a small upper padding (e.g., multiply/add
a margin to dataMax or use a function domain like (dataMin, dataMax) =>
[dataMin, dataMax * 1.05]) so charts never clip values; adjust ticks if needed
to remain consistent with the new dynamic domain.
| export const navItems: NavItem[] = [ | ||
| { | ||
| label: '대시보드', | ||
| iconPath: 'M3 13h8V3H3v10Zm10 8h8V3h-8v18ZM3 21h8v-6H3v6Z', | ||
| }, | ||
| { | ||
| label: '프로젝트 관리', | ||
| iconPath: | ||
| 'M3 7.5A2.5 2.5 0 0 1 5.5 5H10l2 2h6.5A2.5 2.5 0 0 1 21 9.5v7A2.5 2.5 0 0 1 18.5 19h-13A2.5 2.5 0 0 1 3 16.5v-9Z', | ||
| }, | ||
| { | ||
| label: '사용자 관리', | ||
| iconPath: | ||
| 'M16 21v-2a4 4 0 0 0-4-4H7a4 4 0 0 0-4 4v2M9.5 11a4 4 0 1 0 0-8 4 4 0 0 0 0 8Zm8.5 3 2 2 3-4', | ||
| }, | ||
| { label: '통계', iconPath: 'M4 19V9m5 10V5m5 14v-7m5 7V3' }, | ||
| { | ||
| label: '설정', | ||
| iconPath: | ||
| 'M12 15.5A3.5 3.5 0 1 0 12 8a3.5 3.5 0 0 0 0 7.5Zm0-12v2m0 13v2m8.5-8.5h-2m-13 0h-2m14.5-6.5-1.4 1.4m-9.2 9.2-1.4 1.4m0-12 1.4 1.4m9.2 9.2 1.4 1.4', | ||
| }, | ||
| ]; |
There was a problem hiding this comment.
entities/admin-dashboard/model/types.ts L14-17, dashboard-data.ts L40-61 의 navItems에서 iconPath를 SVG path 문자열로 직접 들고 있는 부분은 lucide-react 아이콘 컴포넌트를 참조하는 방식으로 빼는 게 좋을 것 같아요.
지금처럼 path string을 raw로 데이터에 박아두면 viewBox, stroke-width, fill 같은 메타 정보가 함께 따라오지 않아서 아이콘 하나 교체할 때마다 사용처까지 같이 신경 써야 하고, 다중 path나 group이 있는 아이콘은 구조적으로 표현이 안 됩니다. 실제로 사용자 관리 항목은 이미 두 개의 path를 한 문자열에 우겨넣은 형태로 들어가 있는데, 이런 케이스는 stroke나 group을 분리해서 다루기가 어려워져요. 거기에 IDE에서 미리보기가 되지 않다 보니 데이터만 봐서는 어떤 아이콘인지 알기 어렵고, 같은 아이콘을 다른 곳에서 재사용할 때도 path를 복붙해야 한다는 점도 부담입니다.
import type { LucideIcon } from 'lucide-react';
export type NavItem = {
label: string;
icon: LucideIcon;
};import { LayoutDashboard, FolderKanban, Users, BarChart3, Settings } from 'lucide-react';
export const navItems: NavItem[] = [
{ label: '대시보드', icon: LayoutDashboard },
{ label: '프로젝트 관리', icon: FolderKanban },
{ label: '사용자 관리', icon: Users },
{ label: '통계', icon: BarChart3 },
{ label: '설정', icon: Settings },
];| <h2 id="abuse-monitoring-title" className="tracking-zero text-lg font-bold"> | ||
| 어뷰징 및 이상 행동 모니터링 | ||
| </h2> |
There was a problem hiding this comment.
<h2 id="abuse-monitoring-title">이 <section aria-labelledby="..."> 바깥에 있습니다.
동작은 하지만 시멘틱 구조상 heading이 section을 라벨링하는 게 아니라 형제로 떠 있는 형태고, 스크린리더 동선이나 문서 outline 관점에서 부자연스러워요. heading은 section 안으로 들어가야 합니다.
<section aria-labelledby="abuse-monitoring-title" className="...">
<h2 id="abuse-monitoring-title" className="...">어뷰징 및 이상 행동 모니터링</h2>
<div className="grid gap-6 xl:grid-cols-2">
{/* 카드들 */}
</div>
</section>There was a problem hiding this comment.
각 카드의 제목 {signal.title}, 가입 및 IP 모니터링, Rate Limit 위반 이 전부 그냥 <div className="text-xl font-bold">로 들어가 있습니다. 이건 명백히 heading 역할인데 div로 처리되어 있어서 document outline에 안 잡히고, "헤딩으로 점프" 같은 보조 기술 사용도 막힙니다. h3으로 바꾸세요. 큰 섹션 안의 서브섹션이니까 h3가 자연스럽습니다.
같은 맥락에서 각 카드도 section 또는 article로 감싸는 게 맞아요. 지금은 카드 = div라서 의미상 그냥 "박스" 이상의 정보가 없습니다.
There was a problem hiding this comment.
<div className="text-xl font-bold">로 된 카드 제목들은 h2 다음 heading 위계가 비어, 스크린리더에서 카드 단위 탐색이 안 됩니다. h3로 올리고, 카드별로 section aria-labelledby까지 묶으면 더 또렷합니다.
JangHwanPark
left a comment
There was a problem hiding this comment.
entities가 도메인 모델이 아니라 목 데이터 저장소가 됨
model/dashboard-data.ts가 사실상 목 데이터 덩어리이고, index.ts 배럴로
alerts/metrics/projectTrend...가 엔티티 공개 API처럼 나가고 있는데요. FSD에서 entities는 도메인 모델(types + 이후 api/) 레이어라, 지금 구조면 실제 API 붙일 때 위젯들이 전부 정적 픽스처에 묶일 것 같습니다.
목 데이터는 *.mock.ts나 __mocks__로 따로 빼두고, 위젯은 page/api에서 주입받는 형태로 가는 게 나중에 편하지 않을까요?
그리고 navItems(SVG path 포함)는 소비처가 BackOfficeLayout이니 레이아웃 위젯쪽으로 옮기는 게 레이어상 더 맞을 것 같아요.
수치가 위젯 JSX에 하드코딩돼 데이터와 이중화됨 (cost-system-section.tsx)
$1,858, 93%, 예산: $2,000, 남은 예산: $142, w-[93%], aria-valuenow={93} 처럼 props 없이 정적 텍스트로 들어가 있는데, 같은 값이 dashboard-data.ts에도 있어서 둘이 어긋날 여지가 있어 보입니다. 다른 섹션(resourceUsages/systemStatuses 받는 식)처럼 props 기반으로 맞추고 진행률·aria 값도 데이터에서 파생시키면, 나중에 API 연동이 fetcher 교체만으로 끝날 것 같아요.
색상이 디자인 토큰 대신 raw 값으로 들어감 (select-styles.ts 등)
select-styles.ts에 하드코딩 + !important 오버라이드로 들어가 있는데, 이건 공용 Select를 위젯 로컬에서 강제로 덮는 모양새라 토큰이 안 맞으면 디자인 시스템 레벨에서 잡는 게 맞을 것 같습니다(위젯마다 override 쌓이면 관리가 어려워서요).
위젯들의 text-red-600/bg-amber-50 같은 raw 팔레트도 text-text-primary/border-border 토큰과 섞여 있어서 의미 색은 토큰으로 통일하면 좋겠습니다. 그리고 alerts의 tone: 'bg-red-50 text-red-700 ...'는 스타일 클래스가 데이터로 들어간 케이스라, status enum 정도만 데이터에 두고 클래스 매핑은 위젯에서 하는 게 깔끔할 것 같아요.
웹 view vs 백오피스 pages+widgets (레이어 어휘 분기)
웹은 view 레이어를 쓰는데 백오피스는 pages+widgets를 도입했네요. 정규 FSD상 문제는 없는데 두 앱 레이어 어휘가 갈립니다. 이런 부분은 작업전에 논의해주세요.
widgets/dashboard 책임 경계가 모호함
widgets/dashboard/ui/의 섹션들이 독립 위젯이라기보다 대시보드 페이지 한 장을 잘게 쪼갠 표현 단편으로 보입니다. (alerts-section.tsx, metrics-section.tsx 등)
FSD에서 위젯은 자기완결·재사용 가능한 블록인데, 지금은 이 페이지 전용 UI 조각으로 보입니다. 페이지의 UI 세그먼트로 두고, 정말 재사용/자기완결인 것만 위젯으로 승격하는 편이 경계가 더 또렷할 것 같습니다.
책임이 한 위젯 안에서 갈리는 곳도 있어요. cost-system-section.tsx는resourceUsages/systemStatuses를 props로 받으면서 동시에 $1,858/93%를 직접 박고 있어서, 한 위젯이 데이터 주도와 정적 콘텐츠 반반입니다(2번과 연결).
그리고 select-styles.ts가 widgets/dashboard/ui/ 안에 있는 것도 공용 Select 오버라이드는 위젯 책임이라기보단 디자인 시스템 책임이라, 위젯 안에 두면 책임이 샙니다.(3번과 연결)
BackOfficeLayout 위젯이 내비를 스스로 정의하지 않고 대시보드 엔티티에서 주입받는 것도 레이아웃 위젯 입장에선 의존 방향이 거꾸로라 같이 보면 좋겠습니다(1번과 연결).
| <main | ||
| id="dashboard-main" | ||
| aria-labelledby="dashboard-title" | ||
| className="text-text-primary min-h-dvh flex-1 bg-gray-50" | ||
| > | ||
| <div className="min-h-dvh bg-gray-50 lg:pl-[240px]"> | ||
| <aside | ||
| aria-label="Back office 사이드바" | ||
| className="border-border fixed inset-y-0 left-0 hidden w-[240px] border-r bg-white lg:block" | ||
| > |
There was a problem hiding this comment.
<main> 안에 <aside>/<nav>가 들어가 랜드마크가 섞입니다.
<main>이 사이드바와 주 콘텐츠를 모두 감싸고 있어, 스크린리더 랜드마크 탐색에서 본문으로 건너뛰기가 사이드바까지 포함하게 되고 main 안에 navigation이 중첩됩니다.
또 aria-labelledby="dashboard-title"이 다른 위젯(dashboard-header.tsx L17의 h1) id를 참조해, 헤더가 렌더되지 않거나 id가 바뀌면 main의 접근 가능한 이름이 조용히 사라집니다.
레이아웃을 <div class="layout"><aside/><main>{children}</main></div>형태로 바꿔 aside/nav를 main의 형제로 두고, main은 대시보드 페이지 title을 빌리지 말고 자체 라벨(sr-only 제목 또는 aria-label="백오피스")을 갖도록 분리해 주세요.
| <div className="border-border absolute bottom-0 hidden w-[239px] border-t px-6 py-4 text-sm lg:block"> | ||
| <div className="font-semibold">관리자</div> | ||
| <div className="text-text-secondary mt-1">admin@testea.com</div> | ||
| </div> |
There was a problem hiding this comment.
관리자 정보가 마크업에 하드코딩되어 있습니다.
관리자 / admin@testea.com가 정적이라 로그인 주체가 달라도 항상 같은 값이 노출됩니다. 세션/props(user: {name, email })로 주입받고, 미연동 단계면 스켈레톤이나 빈 상태로 자리만 잡아두는 편이 안전합니다.
| <span className="text-text-secondary text-sm" aria-live="polite"> | ||
| 마지막 갱신: 7분 전 | ||
| </span> |
There was a problem hiding this comment.
정적 텍스트에 aria-live="polite"가 걸려 있습니다.
마지막 갱신: 7분 전은 갱신되지 않는데 aria-live가 있어, 스크린리더는 변경 알림 영역으로 인식하지만 실제로는 아무 일도 일어나지 않습니다.
또 상대 시간("7분 전")이라 시간이 지나도 재계산되지 않아 부정확해집니다. 실시간 갱신을 구현하기 전까지는 aria-live를 제거하고, 구현 시에는 로 절대 시각을 함께 두고 갱신마다 텍스트를 교체해 주세요.
| <header className="border-border sticky top-0 z-10 border-b bg-white/95 px-5 py-4 backdrop-blur lg:px-8"> | ||
| <div className="flex flex-col gap-4 xl:flex-row xl:items-center xl:justify-between"> | ||
| <div> | ||
| <p className="text-xs font-semibold text-[#155DFC]">[BO02] 사용자 및 분석 대시보드</p> |
There was a problem hiding this comment.
내부 스펙 코드 [BO02]가 관리자 화면에 노출됩니다.
사용자에게 보이는 UI 텍스트에서 내부 식별자는 제거해 주세요.
| <Button variant="outlined" className="bg-white"> | ||
| 내보내기 | ||
| </Button> |
There was a problem hiding this comment.
내보내기 버튼이 동작하지 않습니다.
onClick/핸들러가 없어 클릭해도 반응이 없습니다. 미구현이면 disabled(+필요 시 사유 툴팁)로 의도를 드러내고, 구현 예정이면 핸들러를 연결해 주세요.
There was a problem hiding this comment.
<div className="text-xl font-bold">로 된 카드 제목들은 h2 다음 heading 위계가 비어, 스크린리더에서 카드 단위 탐색이 안 됩니다. h3로 올리고, 카드별로 section aria-labelledby까지 묶으면 더 또렷합니다.
| <div className="rounded-lg border border-red-200 bg-red-50 p-4"> | ||
| <div className="font-mono text-sm">203.0.113.45</div> | ||
| <div className="text-text-secondary mt-2 text-sm"> | ||
| 5개 계정 · 최근 2시간 내 생성 · <b className="text-red-600 underline">차단</b> |
There was a problem hiding this comment.
<b>차단</b>이 클릭 가능한 액션처럼 보입니다. text-red-600 underline이라 링크/버튼처럼 보이지만 b 태그라 키보드·스크린리더에는 액션으로 노출되지 않고 마우스 사용자만 클릭을 시도하게 됩니다. 실제 차단 액션이면 button으로, 단순 상태 표시면 underline을 제거하고 상태 텍스트(예: "차단됨")로 바꿔 affordance를 일치시켜 주세요.
There was a problem hiding this comment.
튜플 인덱스 접근이 마크업에 노출돼 깨지기 쉽습니다
item[0..3], project[0..2]로 위치 기반 접근을 하는데, 특히 item[2]는 표시 텍스트·width·aria-valuenow(parseFloat)·aria-valuetext로 네 번 재사용됩니다. 데이터 형식이 한 곳만 바뀌어도("92%"→0.92) 막대 폭과 aria 값이 동시에 어긋나고 가독성도 낮습니다.
FunnelStep/StorageProject를 객체({ label, value, rate, drop })로 바꾸고, aria-valuenow·width는 숫자 필드에서 파생시켜 주세요.
| <div className="rounded-lg bg-gray-50 p-5"> | ||
| <div className="text-text-secondary text-sm">총 용량</div> | ||
| <div className="mt-3 text-3xl font-bold">14.76 GB</div> | ||
| </div> | ||
| <div className="rounded-lg bg-gray-50 p-5"> | ||
| <div className="text-text-secondary text-sm">총 Row 수</div> | ||
| <div className="mt-3 text-3xl font-bold">4,559,234</div> | ||
| </div> |
There was a problem hiding this comment.
다른 항목과 달리 총 용량/Row 수가 하드코딩이라 데이터 경로가 갈립니다. props화해 통일해 주세요.
| <h2 id="additional-analysis-title" className="tracking-zero text-lg font-bold"> | ||
| 추가 분석 | ||
| </h2> | ||
| <section aria-labelledby="additional-analysis-title" className="grid gap-6 xl:grid-cols-2"> |
There was a problem hiding this comment.
h2가 section 바깥입니다 (cost-spike와 동일 패턴)
26cccda to
8123aeb
Compare
26cccda to
eb632fa
Compare
…ard-ui # Conflicts: # apps/back-office/app/layout.tsx
작업 유형
요약
BO02 사용량 및 분석 대시보드 화면을 backoffice 앱에 추가했습니다.
주요 지표, 추이 분석, 비용 및 시스템 상태, 비용 급증 프로젝트, 어뷰징 모니터링, 추가 분석 섹션을 구성했습니다.
배경 / 동기
관리자가 서비스 사용량, AI 비용, 인프라 사용량, 이상 사용 패턴을 한 화면에서 확인할 수 있는 backoffice 대시보드가 필요했습니다.
변경 사항
Button,Select컴포넌트 활용title/desc, 테이블caption,progressbararia 속성 등 접근성 보강스크린샷 / 데모
테스트 방법
pnpm --filter back-office lintpnpm --filter back-office build영향 범위 / 주의사항
체크리스트