feat: integrate latest HMDesign design system updates
This commit is contained in:
1 parent
99faec189e
commit
1c4e0bb9a1
209 files changed
+8845
-552
No files matched your search
@@ -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>
|
||||
@@ -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>)
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>)
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>)
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>)
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>)
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>)
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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/>)
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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/>)
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>)
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>)
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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 비율과 혼용하지 않는다.
|
||||
@@ -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>
|
||||
}
|
||||
@@ -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></>
|
||||
}
|
||||
@@ -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>
|
||||
}
|
||||
@@ -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></>
|
||||
}
|
||||
@@ -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></>
|
||||
}
|
||||
@@ -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; } }
|
||||
@@ -0,0 +1,2 @@
|
||||
@import "tailwindcss" source("../");
|
||||
@import "@hm-design/ui/tailwind.css";
|
||||
@@ -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"]
|
||||
}
|
||||
@@ -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")] })) } } })
|
||||
Reference in new issue
Block a user