feat: integrate latest HMDesign design system updates

This commit is contained in:
DESKTOP-KPCP6AL\ㅅㅎ committed 2026-10-11 11:09:03 +09:00
1 parent 99faec189e
commit 1c4e0bb9a1
209 files changed
+8845 -552

No files matched your search

+1
View File
@@ -0,0 +1 @@
<!doctype html><html lang="ko"><head><meta charset="UTF-8"/><meta name="viewport" content="width=device-width, initial-scale=1.0"/><link rel="icon" href="data:,"/><title>사용자 목록</title></head><body><div id="root"></div><script type="module" src="./main.tsx"></script></body></html>
+6
View File
@@ -0,0 +1,6 @@
import { createRoot } from "react-dom/client"
import { SearchList } from "../shared/SearchList"
import { GNBFrame } from "../shared/GNBFrame"
import "../shared/index.css"
import "../shared/hm-theme.css"
createRoot(document.getElementById("root")!).render(<GNBFrame title="사용자 목록"><SearchList title="사용자 목록"/></GNBFrame>)
+22
View File
@@ -0,0 +1,22 @@
{
"id": "01",
"title": "사용자 목록",
"difficulty": "Easy",
"mode": "GNB_ONLY",
"actions": [
"조회",
"초기화"
],
"patterns": [
"FilterBar",
"DataTable"
],
"generation": {
"model": "GPT-6 (session identity)",
"reasoningLevel": "UNAVAILABLE",
"randomSeed": null,
"seedReason": "Session sampling seed unavailable",
"condition": "Generated in current Codex session, shared compositions, no previous experiment solution source supplied",
"comparability": "NON-COMPARABLE"
}
}
+1
View File
@@ -0,0 +1 @@
<!doctype html><html lang="ko"><head><meta charset="UTF-8"/><meta name="viewport" content="width=device-width, initial-scale=1.0"/><link rel="icon" href="data:,"/><title>현장 업무</title></head><body><div id="root"></div><script type="module" src="./main.tsx"></script></body></html>
+6
View File
@@ -0,0 +1,6 @@
import { createRoot } from "react-dom/client"
import { SearchList } from "../shared/SearchList"
import { GNBFrame } from "../shared/GNBFrame"
import "../shared/index.css"
import "../shared/hm-theme.css"
createRoot(document.getElementById("root")!).render(<GNBFrame title="현장 업무"><SearchList title="현장 업무" cards/></GNBFrame>)
+21
View File
@@ -0,0 +1,21 @@
{
"id": "02",
"title": "현장 업무",
"difficulty": "Medium",
"mode": "GNB_ONLY",
"actions": [
"조회",
"초기화"
],
"patterns": [
"FilterBar"
],
"generation": {
"model": "GPT-6 (session identity)",
"reasoningLevel": "UNAVAILABLE",
"randomSeed": null,
"seedReason": "Session sampling seed unavailable",
"condition": "Generated in current Codex session, shared compositions, no previous experiment solution source supplied",
"comparability": "NON-COMPARABLE"
}
}
+1
View File
@@ -0,0 +1 @@
<!doctype html><html lang="ko"><head><meta charset="UTF-8"/><meta name="viewport" content="width=device-width, initial-scale=1.0"/><link rel="icon" href="data:,"/><title>승인 결재</title></head><body><div id="root"></div><script type="module" src="./main.tsx"></script></body></html>
+6
View File
@@ -0,0 +1,6 @@
import { createRoot } from "react-dom/client"
import { SearchList } from "../shared/SearchList"
import { GNBFrame } from "../shared/GNBFrame"
import "../shared/index.css"
import "../shared/hm-theme.css"
createRoot(document.getElementById("root")!).render(<GNBFrame title="승인 결재"><SearchList title="승인 결재" detail="dialog"/></GNBFrame>)
+25
View File
@@ -0,0 +1,25 @@
{
"id": "03",
"title": "승인 결재",
"difficulty": "Hard",
"mode": "GNB_ONLY",
"actions": [
"조회",
"초기화",
"결재 확인",
"취소",
"승인"
],
"patterns": [
"FilterBar",
"DataTable"
],
"generation": {
"model": "GPT-6 (session identity)",
"reasoningLevel": "UNAVAILABLE",
"randomSeed": null,
"seedReason": "Session sampling seed unavailable",
"condition": "Generated in current Codex session, shared compositions, no previous experiment solution source supplied",
"comparability": "NON-COMPARABLE"
}
}
+1
View File
@@ -0,0 +1 @@
<!doctype html><html lang="ko"><head><meta charset="UTF-8"/><meta name="viewport" content="width=device-width, initial-scale=1.0"/><link rel="icon" href="data:,"/><title>채용 관리</title></head><body><div id="root"></div><script type="module" src="./main.tsx"></script></body></html>
+6
View File
@@ -0,0 +1,6 @@
import { createRoot } from "react-dom/client"
import { SearchList } from "../shared/SearchList"
import { GNBFrame } from "../shared/GNBFrame"
import "../shared/index.css"
import "../shared/hm-theme.css"
createRoot(document.getElementById("root")!).render(<GNBFrame title="채용 관리"><SearchList title="채용 관리" detail="sheet"/></GNBFrame>)
+24
View File
@@ -0,0 +1,24 @@
{
"id": "04",
"title": "채용 관리",
"difficulty": "Hard",
"mode": "GNB_ONLY",
"actions": [
"조회",
"초기화",
"채용 상세",
"확인"
],
"patterns": [
"FilterBar",
"DataTable"
],
"generation": {
"model": "GPT-6 (session identity)",
"reasoningLevel": "UNAVAILABLE",
"randomSeed": null,
"seedReason": "Session sampling seed unavailable",
"condition": "Generated in current Codex session, shared compositions, no previous experiment solution source supplied",
"comparability": "NON-COMPARABLE"
}
}
+1
View File
@@ -0,0 +1 @@
<!doctype html><html lang="ko"><head><meta charset="UTF-8"/><meta name="viewport" content="width=device-width, initial-scale=1.0"/><link rel="icon" href="data:,"/><title>업무 요청</title></head><body><div id="root"></div><script type="module" src="./main.tsx"></script></body></html>
+6
View File
@@ -0,0 +1,6 @@
import { createRoot } from "react-dom/client"
import { RequestForm } from "../shared/RequestForm"
import { GNBFrame } from "../shared/GNBFrame"
import "../shared/index.css"
import "../shared/hm-theme.css"
createRoot(document.getElementById("root")!).render(<GNBFrame title="업무 요청"><RequestForm/></GNBFrame>)
+18
View File
@@ -0,0 +1,18 @@
{
"id": "05",
"title": "업무 요청",
"difficulty": "Medium",
"mode": "GNB_ONLY",
"actions": [
"제출"
],
"patterns": [],
"generation": {
"model": "GPT-6 (session identity)",
"reasoningLevel": "UNAVAILABLE",
"randomSeed": null,
"seedReason": "Session sampling seed unavailable",
"condition": "Generated in current Codex session, shared compositions, no previous experiment solution source supplied",
"comparability": "NON-COMPARABLE"
}
}
+1
View File
@@ -0,0 +1 @@
<!doctype html><html lang="ko"><head><meta charset="UTF-8"/><meta name="viewport" content="width=device-width, initial-scale=1.0"/><link rel="icon" href="data:,"/><title>권한 설정</title></head><body><div id="root"></div><script type="module" src="./main.tsx"></script></body></html>
+6
View File
@@ -0,0 +1,6 @@
import { createRoot } from "react-dom/client"
import { Permissions } from "../shared/Permissions"
import { LNBFrame } from "../shared/LNBFrame"
import "../shared/index.css"
import "../shared/hm-theme.css"
createRoot(document.getElementById("root")!).render(<LNBFrame title="권한 설정"><Permissions/></LNBFrame>)
+18
View File
@@ -0,0 +1,18 @@
{
"id": "06",
"title": "권한 설정",
"difficulty": "Medium",
"mode": "LNB_ONLY",
"actions": [
"저장"
],
"patterns": [],
"generation": {
"model": "GPT-6 (session identity)",
"reasoningLevel": "UNAVAILABLE",
"randomSeed": null,
"seedReason": "Session sampling seed unavailable",
"condition": "Generated in current Codex session, shared compositions, no previous experiment solution source supplied",
"comparability": "NON-COMPARABLE"
}
}
+1
View File
@@ -0,0 +1 @@
<!doctype html><html lang="ko"><head><meta charset="UTF-8"/><meta name="viewport" content="width=device-width, initial-scale=1.0"/><link rel="icon" href="data:,"/><title>업무 Dashboard</title></head><body><div id="root"></div><script type="module" src="./main.tsx"></script></body></html>
+5
View File
@@ -0,0 +1,5 @@
import { createRoot } from "react-dom/client"
import { Dashboard } from "../shared/Dashboard"
import "../shared/index.css"
import "../shared/hm-theme.css"
createRoot(document.getElementById("root")!).render(<Dashboard/>)
+18
View File
@@ -0,0 +1,18 @@
{
"id": "07",
"title": "업무 Dashboard",
"difficulty": "Medium",
"mode": "DASHBOARD_ONLY",
"actions": [],
"patterns": [
"DashboardLayout"
],
"generation": {
"model": "GPT-6 (session identity)",
"reasoningLevel": "UNAVAILABLE",
"randomSeed": null,
"seedReason": "Session sampling seed unavailable",
"condition": "Generated in current Codex session, shared compositions, no previous experiment solution source supplied",
"comparability": "NON-COMPARABLE"
}
}
+1
View File
@@ -0,0 +1 @@
<!doctype html><html lang="ko"><head><meta charset="UTF-8"/><meta name="viewport" content="width=device-width, initial-scale=1.0"/><link rel="icon" href="data:,"/><title>통계 Dashboard</title></head><body><div id="root"></div><script type="module" src="./main.tsx"></script></body></html>
+5
View File
@@ -0,0 +1,5 @@
import { createRoot } from "react-dom/client"
import { Dashboard } from "../shared/Dashboard"
import "../shared/index.css"
import "../shared/hm-theme.css"
createRoot(document.getElementById("root")!).render(<Dashboard statistics/>)
+19
View File
@@ -0,0 +1,19 @@
{
"id": "08",
"title": "통계 Dashboard",
"difficulty": "Medium",
"mode": "DASHBOARD_ONLY",
"actions": [],
"patterns": [
"DashboardLayout",
"DataTable"
],
"generation": {
"model": "GPT-6 (session identity)",
"reasoningLevel": "UNAVAILABLE",
"randomSeed": null,
"seedReason": "Session sampling seed unavailable",
"condition": "Generated in current Codex session, shared compositions, no previous experiment solution source supplied",
"comparability": "NON-COMPARABLE"
}
}
+1
View File
@@ -0,0 +1 @@
<!doctype html><html lang="ko"><head><meta charset="UTF-8"/><meta name="viewport" content="width=device-width, initial-scale=1.0"/><link rel="icon" href="data:,"/><title>작업 변경</title></head><body><div id="root"></div><script type="module" src="./main.tsx"></script></body></html>
+6
View File
@@ -0,0 +1,6 @@
import { createRoot } from "react-dom/client"
import { OverlayWorkflow } from "../shared/OverlayWorkflow"
import { GNBFrame } from "../shared/GNBFrame"
import "../shared/index.css"
import "../shared/hm-theme.css"
createRoot(document.getElementById("root")!).render(<GNBFrame title="작업 변경"><OverlayWorkflow/></GNBFrame>)
+20
View File
@@ -0,0 +1,20 @@
{
"id": "09",
"title": "작업 변경",
"difficulty": "Hard",
"mode": "GNB_ONLY",
"actions": [
"작업 변경 열기",
"취소",
"적용"
],
"patterns": [],
"generation": {
"model": "GPT-6 (session identity)",
"reasoningLevel": "UNAVAILABLE",
"randomSeed": null,
"seedReason": "Session sampling seed unavailable",
"condition": "Generated in current Codex session, shared compositions, no previous experiment solution source supplied",
"comparability": "NON-COMPARABLE"
}
}
+1
View File
@@ -0,0 +1 @@
<!doctype html><html lang="ko"><head><meta charset="UTF-8"/><meta name="viewport" content="width=device-width, initial-scale=1.0"/><link rel="icon" href="data:,"/><title>처리 상태</title></head><body><div id="root"></div><script type="module" src="./main.tsx"></script></body></html>
+6
View File
@@ -0,0 +1,6 @@
import { createRoot } from "react-dom/client"
import { StateWorkflow } from "../shared/StateWorkflow"
import { LNBFrame } from "../shared/LNBFrame"
import "../shared/index.css"
import "../shared/hm-theme.css"
createRoot(document.getElementById("root")!).render(<LNBFrame title="처리 상태"><StateWorkflow/></LNBFrame>)
+21
View File
@@ -0,0 +1,21 @@
{
"id": "10",
"title": "처리 상태",
"difficulty": "Easy",
"mode": "LNB_ONLY",
"actions": [
"로딩",
"오류",
"빈 결과",
"다시 시도"
],
"patterns": [],
"generation": {
"model": "GPT-6 (session identity)",
"reasoningLevel": "UNAVAILABLE",
"randomSeed": null,
"seedReason": "Session sampling seed unavailable",
"condition": "Generated in current Codex session, shared compositions, no previous experiment solution source supplied",
"comparability": "NON-COMPARABLE"
}
}
+32
View File
@@ -0,0 +1,32 @@
# AI 생성 품질 실험
신규 React 화면 10개. 같은 Codex 세션에서 작성한 공통 composition을 사용한다. 이전 실험 소스를 정답으로 사용하지 않았다. `requirements.json`에 난이도·Navigation·허용 액션·Pattern·모델·추론/Seed 가용성을 기록한다. 이전 실험 또는 독립 모델 비교와 NON-COMPARABLE이다.
```sh
node scripts/prepare-quality-experiments.mjs
npm run build:packages
node node_modules/typescript/bin/tsc -p examples/ai-quality/tsconfig.json
node node_modules/vite/bin/vite.js build --config examples/ai-quality/vite.config.ts
node node_modules/vite/bin/vite.js preview --config examples/ai-quality/vite.config.ts --host 127.0.0.1 --port 6321
```
`http://127.0.0.1:6321/01/index.html`부터 `10/index.html`까지 각각 독립 React bootstrap이다. 신규 Story·추가 운영 메뉴는 없다. 생성 스크립트는 entry·요구사항만 재생성하며 공통 화면 소스는 덮어쓰지 않는다.
브라우저 검사는 설치된 Playwright 모듈과 Chrome 실행 경로를 `HM_PLAYWRIGHT_DIR`, `HM_CHROME_PATH` 환경 변수로 제공한 뒤 `node scripts/verify-quality-runtime.mjs`를 실행한다. 결과·원본 Guard·40개 캡처는 `_local/quality-20261010`에 기록된다. 실패한 화면만 재검사할 때는 `node scripts/verify-quality-runtime.mjs 03 04`처럼 ID를 전달한다. 미검증 환경에는 임의 PASS를 기록하지 않는다.
| ID | 화면 | 난이도 | Navigation | 선택 이유 |
|---|---|---|---|---|
| 01 | 사용자 목록 | Easy | GNB | 서비스 헤더 + FilterBar/DataTable |
| 02 | 현장 업무 | Medium | GNB | FilterBar + 카드 결과, Table 불필요 |
| 03 | 승인 결재 | Hard | GNB | FilterBar/DataTable + Dialog 확인 |
| 04 | 채용 관리 | Hard | GNB | FilterBar/DataTable + Sheet 상세 |
| 05 | 업무 요청 | Medium | GNB | 별도 Form Pattern 없음, Label/Input/Checkbox composition |
| 06 | 권한 설정 | Medium | LNB | 설정 영역 단일 메뉴, Checkbox/Switch composition |
| 07 | 업무 Dashboard | Medium | Dashboard | 전용 48px Header·960px frame·기간별 지표 |
| 08 | 통계 Dashboard | Medium | Dashboard | Tabs/Progress 지표 + DataTable 분류별 집계, 공개 Chart API 없음 |
| 09 | 작업 변경 | Hard | GNB | Drawer 확인·적용 |
| 10 | 처리 상태 | Easy | LNB | Skeleton/Alert/빈 결과 composition, 검색 요구 없음 |
Navigation은 실험 요구사항에서 명시한 모드이며 하나의 화면 제목만 메뉴로 제공한다. 이 선택을 일반 업무 화면의 기본 모드로 일반화하지 않는다. DataTable의 페이지 크기·페이지 이동, 검색 Clear, Overlay Close는 선택한 공개 API의 기본 기능이며 새로운 업무 메뉴/CSV가 아니다. Dashboard Status는 해당 도메인 상태 필터 요청이 없으므로 강제하지 않는다. 공통 HM 토큰·기존 CSS entry를 소비하며 임의 token은 없다.
스크린리더 발화, 실기기 touch/drag, 실제 backend, 교차 모델과 모든 상태 조합은 NOT VERIFIED다. 점수 rubric을 실행하지 않았으므로 디자인 점수는 NOT VERIFIED이며 PASS 비율과 혼용하지 않는다.
+14
View File
@@ -0,0 +1,14 @@
import { useMemo, useState } from "react"
import { DashboardLayout, DashboardHeader, DashboardMain, DashboardContent, Card, CardHeader, CardTitle, CardContent, Progress, Tabs, TabsList, TabsTrigger, TabsContent, DataTable, useDataTable, type DataTableColumnDef } from "@hm-design/ui"
import { Heading } from "./Heading"
type Summary = { category: string; total: number; completed: number }
const columns: DataTableColumnDef<Summary>[] = [{ accessorKey: "category", header: "업무 분류" }, { accessorKey: "total", header: "전체 건수" }, { accessorKey: "completed", header: "완료 건수" }]
export function Dashboard({ statistics = false }: { statistics?: boolean }) {
const [period, setPeriod] = useState("week")
const data = useMemo(() => ["설계", "시공", "안전"].map((category, i) => ({ category, total: (period === "week" ? 12 : 48) + i, completed: (period === "week" ? 8 : 32) + i })), [period])
const table = useDataTable({ columns, data })
const title = statistics ? "통계 Dashboard" : "업무 Dashboard"
return <DashboardLayout header={<DashboardHeader><strong>{title}</strong></DashboardHeader>}><DashboardMain><DashboardContent><Heading title={title} description="기간별 건수와 진행률을 확인합니다."/><Tabs value={period} onValueChange={setPeriod}><TabsList aria-label="조회 기간"><TabsTrigger value="week">이번 주</TabsTrigger><TabsTrigger value="month">이번 달</TabsTrigger></TabsList>{["week", "month"].map(p => <TabsContent key={p} value={p}><div className="quality-metrics">{["진행", "대기", "완료"].map((label, i) => <Card key={label}><CardHeader><CardTitle>{label}</CardTitle></CardHeader><CardContent><p>{(p === "week" ? 12 : 48) + i}건</p><Progress value={40 + i * 20} aria-label={`${label} 진행률`}/></CardContent></Card>)}</div></TabsContent>)}</Tabs>{statistics && <Card><CardHeader><CardTitle>업무 분류별 집계</CardTitle></CardHeader><CardContent><DataTable table={table}/></CardContent></Card>}</DashboardContent></DashboardMain></DashboardLayout>
}
+6
View File
@@ -0,0 +1,6 @@
import { type ReactNode } from "react"
import { GNB, GNBBrand, GNBMobileMenu, GNBMobileLink } from "@hm-design/ui"
export function GNBFrame({ title, children }: { title: string; children: ReactNode }) {
return <><GNB><GNBMobileMenu title={`${title} 메뉴`}><GNBMobileLink href="#content" active>{title}</GNBMobileLink></GNBMobileMenu><GNBBrand href="#content"><span><small>HMDesign</small><strong>{title}</strong></span></GNBBrand></GNB><main id="content" className="quality-main">{children}</main></>
}
+5
View File
@@ -0,0 +1,5 @@
import { PageHeader, PageHeaderContent, PageHeaderTitle, PageHeaderDescription } from "@hm-design/ui"
export function Heading({ title, description }: { title: string; description: string }) {
return <PageHeader><PageHeaderContent><PageHeaderTitle>{title}</PageHeaderTitle><PageHeaderDescription>{description}</PageHeaderDescription></PageHeaderContent></PageHeader>
}
+6
View File
@@ -0,0 +1,6 @@
import { type ReactNode } from "react"
import { LNB, LNBBrand, LNBNav, LNBItem, LNBMobileHeader, LNBMobileBrand, LNBMobileMenu, LNBMobileLink } from "@hm-design/ui"
export function LNBFrame({ title, children }: { title: string; children: ReactNode }) {
return <div className="quality-lnb"><LNB><LNBBrand><span><small>HMDesign</small><strong>{title}</strong></span></LNBBrand><LNBNav aria-label={`${title} 메뉴`}><LNBItem href="#content" active>{title}</LNBItem></LNBNav></LNB><div className="quality-lnb-content"><div className="quality-mobile-header"><LNBMobileHeader><LNBMobileMenu title={`${title} 메뉴`}><LNBMobileLink href="#content" active>{title}</LNBMobileLink></LNBMobileMenu><LNBMobileBrand href="#content">{title}</LNBMobileBrand></LNBMobileHeader></div><main id="content" className="quality-main">{children}</main></div></div>
}
@@ -0,0 +1,8 @@
import { useState } from "react"
import { Card, CardContent, Button, Drawer, DrawerTrigger, DrawerContent, DrawerHeader, DrawerTitle, DrawerDescription, DrawerFooter, DrawerClose } from "@hm-design/ui"
import { Heading } from "./Heading"
export function OverlayWorkflow() {
const [status, setStatus] = useState("변경 전")
return <><Heading title="작업 변경" description="Drawer에서 작업 변경을 확인하고 적용합니다."/><Card><CardContent><p role="status">{status}</p><Drawer><DrawerTrigger asChild><Button>작업 변경 열기</Button></DrawerTrigger><DrawerContent><DrawerHeader><DrawerTitle>작업 변경</DrawerTitle><DrawerDescription>확인 후 적용하면 변경 상태를 갱신합니다.</DrawerDescription></DrawerHeader><DrawerFooter><DrawerClose asChild><Button variant="cancel" appearance="solid">취소</Button></DrawerClose><DrawerClose asChild><Button onClick={() => setStatus("변경 적용 완료")}>적용</Button></DrawerClose></DrawerFooter></DrawerContent></Drawer></CardContent></Card></>
}
@@ -0,0 +1,14 @@
import { useId, useState } from "react"
import { Card, CardContent, Button, Label, Input, Checkbox, Switch } from "@hm-design/ui"
import { Heading } from "./Heading"
export function Permissions() {
const id = useId()
const [edit, setEdit] = useState(false)
const [notice, setNotice] = useState(true)
const [status, setStatus] = useState("")
return <><Heading title="권한 설정" description="편집 권한과 알림 설정을 저장합니다."/><Card><CardContent><div className="quality-fields"><Label htmlFor={`${id}-account`}>계정</Label><Input id={`${id}-account`} readOnly value="hm-design"/>
<div className="flex items-center gap-2"><Checkbox id={`${id}-edit`} checked={edit} onCheckedChange={v => setEdit(v === true)}/><Label htmlFor={`${id}-edit`}>편집 권한</Label></div>
<div className="flex items-center gap-2"><Switch id={`${id}-notice`} checked={notice} onCheckedChange={setNotice}/><Label htmlFor={`${id}-notice`}>알림 수신</Label></div><Button onClick={() => setStatus(`저장 완료 · 편집 ${edit ? "허용" : "불가"} · 알림 ${notice ? "켜짐" : "꺼짐"}`)}>저장</Button><p role="status">{status}</p>
</div></CardContent></Card></>
}
@@ -0,0 +1,18 @@
import { useId, useRef, useState } from "react"
import { Card, CardContent, Button, Label, Input, Textarea, Checkbox } from "@hm-design/ui"
import { Heading } from "./Heading"
export function RequestForm() {
const id = useId()
const ref = useRef<HTMLInputElement>(null)
const [name, setName] = useState("")
const [description, setDescription] = useState("")
const [agreed, setAgreed] = useState(false)
const [status, setStatus] = useState("")
return <><Heading title="업무 요청" description="필수 이름과 동의를 확인한 뒤 제출합니다."/><Card><CardContent><form className="quality-fields" noValidate onSubmit={e => { e.preventDefault(); if (!name.trim()) { setStatus("이름을 입력하세요."); ref.current?.focus(); return } if (!agreed) { setStatus("동의가 필요합니다."); document.getElementById(`${id}-agree`)?.focus(); return } setStatus("요청이 제출되었습니다.") }}>
<Label htmlFor={`${id}-name`}>요청자 이름</Label><Input id={`${id}-name`} ref={ref} required aria-invalid={status === "이름을 입력하세요."} aria-describedby={`${id}-status`} value={name} onChange={e => setName(e.target.value)}/>
<Label htmlFor={`${id}-description`}>요청 내용</Label><Textarea id={`${id}-description`} value={description} onChange={e => setDescription(e.target.value)}/>
<div className="flex items-center gap-2"><Checkbox id={`${id}-agree`} checked={agreed} onCheckedChange={v => setAgreed(v === true)}/><Label htmlFor={`${id}-agree`}>처리에 동의합니다</Label></div>
<p id={`${id}-status`} role="status">{status}</p><Button type="submit">제출</Button>
</form></CardContent></Card></>
}
+25
View File
@@ -0,0 +1,25 @@
import { useId, useMemo, useState } from "react"
import { Card, CardHeader, CardTitle, CardContent, Button, Label, FilterBar, FilterBarSearch, FilterBarActions, Select, SelectTrigger, SelectValue, SelectContent, SelectItem, DataTable, useDataTable, type DataTableColumnDef, Badge, Dialog, DialogTrigger, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, DialogClose, Sheet, SheetTrigger, SheetContent, SheetHeader, SheetTitle, SheetDescription, SheetBody, SheetFooter, SheetClose } from "@hm-design/ui"
import { Heading } from "./Heading"
type Row = { id: string; name: string; group: string; status: string }
const records: Row[] = [{ id: "1", name: "김한맥", group: "설계", status: "진행" }, { id: "2", name: "이한맥", group: "시공", status: "대기" }, { id: "3", name: "박한맥", group: "설계", status: "완료" }]
const columns: DataTableColumnDef<Row>[] = [{ accessorKey: "name", header: "이름" }, { accessorKey: "group", header: "부서" }, { accessorKey: "status", header: "상태" }]
export function SearchList({ title, cards = false, detail = "none" }: { title: string; cards?: boolean; detail?: "none" | "dialog" | "sheet" }) {
const [draft, setDraft] = useState("")
const [group, setGroup] = useState("all")
const [applied, setApplied] = useState({ query: "", group: "all" })
const [confirmed, setConfirmed] = useState(false)
const rows = useMemo(() => records.filter(r => r.name.includes(applied.query) && (applied.group === "all" || r.group === applied.group)), [applied])
const table = useDataTable({ columns, data: rows, getRowId: row => row.id })
const id = useId()
return <><Heading title={title} description="부서와 이름 조건은 조회할 때 적용됩니다."/><Card><CardContent><div className="grid min-w-0 gap-4"><form onSubmit={e => { e.preventDefault(); setApplied({ query: draft, group }); table.setPageIndex(0) }}><FilterBar layout="list-basic">
<Select value={group} onValueChange={setGroup}><SelectTrigger size="medium" aria-label="부서"><SelectValue/></SelectTrigger><SelectContent><SelectItem value="all">전체 부서</SelectItem><SelectItem value="설계">설계</SelectItem><SelectItem value="시공">시공</SelectItem></SelectContent></Select>
<Label htmlFor={id} className="sr-only">이름 검색</Label><FilterBarSearch id={id} value={draft} onValueChange={setDraft} placeholder="이름 검색"/>
<FilterBarActions><Button type="submit" variant="primary" appearance="solid">조회</Button><Button type="button" variant="cancel" appearance="outline" onClick={() => { setDraft(""); setGroup("all"); setApplied({ query: "", group: "all" }); table.setPageIndex(0) }}>초기화</Button></FilterBarActions>
</FilterBar></form><p role="status">조회 결과 {rows.length}건{confirmed ? " · 처리 완료" : ""}</p>
{cards ? <div className="grid gap-3">{rows.map(row => <Card key={row.id}><CardHeader><CardTitle>{row.name}</CardTitle></CardHeader><CardContent>{row.group} · <Badge variant="outline">{row.status}</Badge></CardContent></Card>)}{!rows.length && <p>조건에 맞는 업무가 없습니다.</p>}</div> : <DataTable table={table} emptyMessage="조건에 맞는 항목이 없습니다."/>}
{detail === "dialog" && <Dialog><DialogTrigger asChild><Button>결재 확인</Button></DialogTrigger><DialogContent><DialogHeader><DialogTitle>결재 확인</DialogTitle><DialogDescription>현재 조회한 결재를 승인합니다.</DialogDescription></DialogHeader><DialogFooter><DialogClose asChild><Button variant="cancel" appearance="solid">취소</Button></DialogClose><DialogClose asChild><Button onClick={() => setConfirmed(true)}>승인</Button></DialogClose></DialogFooter></DialogContent></Dialog>}
{detail === "sheet" && <Sheet><SheetTrigger asChild><Button>채용 상세</Button></SheetTrigger><SheetContent><SheetHeader><SheetTitle>채용 상세</SheetTitle><SheetDescription>공고의 상세 내용과 확인 상태입니다.</SheetDescription></SheetHeader><SheetBody>설계 부문 채용 · 서류 검토</SheetBody><SheetFooter><SheetClose asChild><Button onClick={() => setConfirmed(true)}>확인</Button></SheetClose></SheetFooter></SheetContent></Sheet>}
</div></CardContent></Card></>
}
@@ -0,0 +1,12 @@
import { useState } from "react"
import { Card, CardContent, Button, Alert, AlertTitle, AlertDescription, Skeleton } from "@hm-design/ui"
import { Heading } from "./Heading"
export function StateWorkflow() {
const [state, setState] = useState("loading")
return <><Heading title="처리 상태" description="로딩, 오류, 빈 결과를 확인하는 작업입니다."/><Card><CardContent><div className="grid gap-4"><div className="flex flex-wrap gap-2"><Button onClick={() => setState("loading")}>로딩</Button><Button onClick={() => setState("error")}>오류</Button><Button onClick={() => setState("empty")}>빈 결과</Button></div>
{state === "loading" && <div role="status" aria-label="처리 중"><Skeleton className="h-10 w-full"/></div>}
{state === "error" && <Alert variant="destructive"><AlertTitle>처리 실패</AlertTitle><AlertDescription>다시 시도하세요.</AlertDescription><Button onClick={() => setState("empty")}>다시 시도</Button></Alert>}
{state === "empty" && <p role="status">등록된 항목이 없습니다.</p>}
</div></CardContent></Card></>
}
+12
View File
@@ -0,0 +1,12 @@
@import "@hm-design/ui/theme.css";
@import "@hm-design/ui/dashboard-status.css";
html, body { margin: 0; font-family: var(--hm-font-family-base); background: var(--hm-sem-bg-content); color: var(--hm-sem-fg-primary); }
.quality-main { max-width: 1180px; min-width: 0; margin: auto; padding: 24px; display: grid; gap: 20px; }
.quality-fields { display: grid; gap: 8px; max-width: 480px; }
.quality-metrics { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 16px; }
.quality-lnb { display: flex; min-height: 100vh; background: var(--hm-sem-bg-brand); }
.quality-lnb-content { flex: 1; min-width: 0; background: var(--hm-sem-bg-content); border-radius: var(--hm-lnb-join) 0 0 var(--hm-lnb-join); }
.quality-mobile-header { display: none; }
@media (max-width: 1023px) { .quality-mobile-header { display: block; } .quality-lnb-content { border-radius: 0; } }
@media (max-width: 640px) { .quality-main { padding: 16px; } .quality-metrics { grid-template-columns: 1fr; } }
+2
View File
@@ -0,0 +1,2 @@
@import "tailwindcss" source("../");
@import "@hm-design/ui/tailwind.css";
+4
View File
@@ -0,0 +1,4 @@
{
"compilerOptions": { "target": "ES2022", "lib": ["ES2022", "DOM", "DOM.Iterable"], "module": "ESNext", "moduleResolution": "Bundler", "jsx": "react-jsx", "strict": true, "skipLibCheck": false, "noEmit": true, "types": ["node", "vite/client"], "noUnusedLocals": true },
"include": ["**/*.tsx", "vite.config.ts"]
}
+6
View File
@@ -0,0 +1,6 @@
import path from "node:path"
import { defineConfig } from "vite"
import react from "@vitejs/plugin-react"
import tailwindcss from "@tailwindcss/vite"
const root = path.resolve(import.meta.dirname)
export default defineConfig({ root, plugins: [react(), tailwindcss()], publicDir: "../../public", server: { host: "127.0.0.1", port: 6320 }, build: { outDir: path.resolve(root, "../../_local/quality-20261010/screens-dist"), emptyOutDir: true, rolldownOptions: { input: Object.fromEntries(Array.from({ length: 10 }, (_, i) => { const id = String(i + 1).padStart(2, "0"); return [id, path.join(root, id, "index.html")] })) } } })