@t3q-ui/design-system 0.0.0-stage → 1.56.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1977 -0
- package/LICENSE.md +88 -0
- package/README.md +464 -2
- package/cli/doctor.mjs +196 -0
- package/cli/lib.mjs +425 -0
- package/cli/setup.mjs +181 -0
- package/cli/t3q-design.mjs +78 -0
- package/dist/chunk-IPF3K4BS.js +4274 -0
- package/dist/chunk-IPF3K4BS.js.map +1 -0
- package/dist/chunk-MHAO45AL.cjs +4427 -0
- package/dist/chunk-MHAO45AL.cjs.map +1 -0
- package/dist/fonts.css +12 -0
- package/dist/index-Ci1Wh5sA.d.cts +253 -0
- package/dist/index-Ci1Wh5sA.d.ts +253 -0
- package/dist/index.cjs +10998 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.css +332 -0
- package/dist/index.css.map +1 -0
- package/dist/index.d.cts +2554 -0
- package/dist/index.d.ts +2554 -0
- package/dist/index.js +10354 -0
- package/dist/index.js.map +1 -0
- package/dist/patterns/index.cjs +885 -0
- package/dist/patterns/index.cjs.map +1 -0
- package/dist/patterns/index.css +265 -0
- package/dist/patterns/index.css.map +1 -0
- package/dist/patterns/index.d.cts +373 -0
- package/dist/patterns/index.d.ts +373 -0
- package/dist/patterns/index.js +873 -0
- package/dist/patterns/index.js.map +1 -0
- package/dist/styles.css +2014 -0
- package/package.json +212 -4
- package/skills/tds/SKILL.md +91 -0
- package/skills/tds/examples/dashboard-page.tsx +344 -0
- package/skills/tds/examples/detail-page.tsx +388 -0
- package/skills/tds/examples/form-page.tsx +460 -0
- package/skills/tds/examples/list-page.tsx +275 -0
- package/skills/tds/reference/components.md +1424 -0
- package/skills/tds/rules.md +28 -0
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TDS 상세 화면 예제 — "헤더(제목·상태·메타·행동) → 탭 → 본문 + 오른쪽 정보 패널 → 삭제 확인" 흐름.
|
|
3
|
+
* 이 파일은 TDS 저장소의 타입 검사(`pnpm typecheck`)와 렌더 테스트를 통과한 코드다. 새 상세 화면은 이 구조에서 시작한다.
|
|
4
|
+
*
|
|
5
|
+
* 이 예제가 보여 주는 것 (자주 틀리는 곳):
|
|
6
|
+
* - DetailPage 가 헤더·탭·행동 버튼·본문/패널 2단·로딩·오류·없음 상태를 그린다. 헤더·탭을 직접 만들지 않는다
|
|
7
|
+
* - 행동 버튼은 `deleteAction`·`secondaryActions`·`primaryAction` 슬롯 → 패턴이 제목 줄 오른쪽에 [더보기 ⋯][보조 최대 2개][주 행동] 으로 놓는다.
|
|
8
|
+
* 삭제는 항상 ⋯ 메뉴 안, 보조 3번째부터도 ⋯ 로, 좁으면 [⋯][주 행동]만. 버튼을 탭 줄이나 본문 위에 직접 두지 않는다(2026-10-19 14차)
|
|
9
|
+
* - `primaryAction` 은 되돌릴 수 있는 생산적 주 행동 **하나**(배포·실행·저장). 중지·삭제·초기화처럼 되돌리기 어려운 행동은 `secondaryActions`(회색) 또는 ⋯ 메뉴(`deleteAction`) —
|
|
10
|
+
* primary 파랑도 빨간 채움도 주지 않는다(5차엔 빨간 버튼이, 14-codex 세트 2 엔 파란 "실험 중지" 가 감점, 2026-10-19 15차)
|
|
11
|
+
* - 개요 탭의 블록은 **전부 `Panel` 안에** — 표 블록도 `Panel`(제목 줄 `titleRight` 전체 보기) 안에 넣고 제목 + 표를 맨몸으로 두지 않는다(2026-10-21 18차 "sits outside a card unlike its neighbors"). 카드 안 표는 아래처럼 `Table`(가벼운 표) — `AgGridTable` 을 카드 안에 넣지 않는다(19차 "bordered table header inside the jobs card"). 개요 탭(2/3 열)의 표는 **4열·5행까지**(이름·상태·핵심 지표 1·시각) + 패널 제목 줄 `titleRight` 에 '전체 보기' 링크로 전용 탭 이동. 열이 더 필요한 표는 전용 탭에서 —
|
|
12
|
+
* 패턴이 `aside` 없는 탭을 전체 폭으로 그린다(GitHub PR 의 Conversation 2단 / Files changed 전체 폭 식). 열 5~7개 표를 2/3 열에 넣으면 이름·날짜가 줄바꿈해 두 심판 모두 감점(15차)
|
|
13
|
+
* - 상태 단어·tone 은 아래 `STATUS`·`DEPLOY_STATUS` 한 표에서만 꺼내 쓴다 — 헤더 뱃지·표·탭이 같은 단어("진행 중" vs "실행 중" 처럼 갈리면 감점, 15차)
|
|
14
|
+
* - 진행률은 분수 대신 풀어 쓴다: "완료 10 / 전체 12 · 실패 2" ("10/12 완료 · 실패 2" 는 실패가 완료에 드는지 모호, 15차)
|
|
15
|
+
* - 중지 아이콘은 `circle-stop`(네모 `square` 는 체크박스로 읽힘). 태그·뱃지 글자는 한국어 — 영문은 고유명사만("Best run" 같은 장식 영문 감점, 15차)
|
|
16
|
+
* - 탭의 건수 `badge`·진행률·"최근 N행" 표의 행 수는 **같은 배열에서 계산**한다(`data.versions.length` 처럼) — 서로 다른 숫자를 지어 쓰면 심사자가 모순으로 본다(탭 18 vs 진행 15+3+1/24)
|
|
17
|
+
* - `banner`(Alert)는 오류·경고에만(실패·중단·권한). "진행 중입니다" 같은 상태 안내를 큰 배너로 올리지 않는다 — 상태는 제목 옆 상태 Badge 가 이미 보여 준다
|
|
18
|
+
* - 시간·기간 표기는 0 단위를 생략한다("0시간 41분" → "41분", "1시간 0분" → "1시간")
|
|
19
|
+
* - 비활성 버튼(DEPRECATED 면 배포 불가)은 비활성으로만 둔다 — 안내 한 줄(`actionsNote`)은 쓰지 않는다(본문 위에 떠 보여 4라운드 연속 감점, 2026-10-18 11차). 상태 자체의 안내는 `banner`
|
|
20
|
+
* - 개요 탭은 요약 패널(`주요 정보`) + 최근 N행 표(`최근 배포`) — 패널 하나만 두면 오른쪽 열 아래가 비어 "large empty area" 감점(2026-10-18 11차)
|
|
21
|
+
* - 탭 항목의 건수는 `badge` 로 → "버전 4" 처럼 탭에 뱃지가 붙는다
|
|
22
|
+
* - 불러오기 실패는 `error` + `onRetry`, 항목 없음은 `notFound` → 패턴이 아이콘·제목·설명·다시 시도 상자로 그린다.
|
|
23
|
+
* Alert 줄이나 `<div>` 로 직접 만들지 않고, 헤더만 채운 채 본문에 "찾을 수 없음" 을 그리지 않는다
|
|
24
|
+
* - 핵심 정보는 헤더 `meta` 한 곳에 `MetaGroup columns={3}` + `MetaItem`(탭을 바꿔도 보임). 메타 자리를 두 개로 나누지 않는다(2026-10-30 사용자 결정 — 요약 상자 없음). 오른쪽 패널 `aside` 는 쓰지 않는다(첫 화면 아래가 빈다)
|
|
25
|
+
* - 상태는 `Badge` 에 아이콘 + 글자(학습 중처럼 진행 중인 상태는 정적 아이콘 `loader-circle` — 스피너는 정지 화면에서 점 뭉치로 보인다). 별도 상태 컴포넌트를 만들지 않는다.
|
|
26
|
+
* 실행 중·활성처럼 건강하게 돌아가는 상태는 `primary`(▶), 완료·준비됨만 `success`(✓) — 실행 중에 ✓ 를 붙이면 끝난 것으로 읽힌다(2026-10-20 17차)
|
|
27
|
+
* - 본문 묶음은 `Panel`(제목 + 1px 테두리 상자)로 나눈다 — 옆 패널(`aside`)도 패턴이 같은 Panel 로 그려 본문과 패널이 한 규칙. Card·Separator 로 나누지 않는다
|
|
28
|
+
* - 설명 문장은 헤더 `description` 한 곳에만 — 본문에 "설명" 구역을 또 만들지 않는다(같은 문장이 두 번 보이면 심사자가 중복으로 본다)
|
|
29
|
+
* - 헤더 `meta` 에 쓴 값(ID·버전·소유자)은 본문 Panel·`aside` 에 다시 쓰지 않는다 — 같은 값이 두 번 보여도 심사자가 중복으로 본다
|
|
30
|
+
* - 키워드·분류 태그는 `Tag variant='secondary'`(기본 각진 4px) — 상태 뱃지·칩·입력과 같은 라운드 계열. pill(`shape='rounded'`)로 그리지 않는다(2026-10-18 10차)
|
|
31
|
+
* - 삭제는 `AlertDialog`(variant danger)로 확인받고 끝나면 `toast`
|
|
32
|
+
*/
|
|
33
|
+
import { type ReactNode, useCallback, useEffect, useState } from 'react'
|
|
34
|
+
import {
|
|
35
|
+
Alert,
|
|
36
|
+
AlertDescription,
|
|
37
|
+
AlertDialog,
|
|
38
|
+
AlertDialogAction,
|
|
39
|
+
AlertDialogCancel,
|
|
40
|
+
AlertDialogContent,
|
|
41
|
+
AlertDialogDescription,
|
|
42
|
+
AlertDialogFooter,
|
|
43
|
+
AlertDialogHeader,
|
|
44
|
+
AlertDialogTitle,
|
|
45
|
+
AlertTitle,
|
|
46
|
+
Button,
|
|
47
|
+
LucideIcon,
|
|
48
|
+
MetaGroup,
|
|
49
|
+
MetaItem,
|
|
50
|
+
MetaList,
|
|
51
|
+
MetaRow,
|
|
52
|
+
Panel,
|
|
53
|
+
Badge,
|
|
54
|
+
type BadgeProps,
|
|
55
|
+
Table,
|
|
56
|
+
TableBody,
|
|
57
|
+
TableCell,
|
|
58
|
+
TableHead,
|
|
59
|
+
TableHeader,
|
|
60
|
+
TableRow,
|
|
61
|
+
Tag,
|
|
62
|
+
toast,
|
|
63
|
+
} from '@t3q-ui/design-system'
|
|
64
|
+
import { DetailPage } from '@t3q-ui/design-system/patterns'
|
|
65
|
+
|
|
66
|
+
type Status = 'READY' | 'TRAINING' | 'FAILED' | 'DEPRECATED'
|
|
67
|
+
type Model = {
|
|
68
|
+
id: string
|
|
69
|
+
name: string
|
|
70
|
+
version: string
|
|
71
|
+
framework: string
|
|
72
|
+
task: string
|
|
73
|
+
accuracy: number
|
|
74
|
+
sizeMb: number
|
|
75
|
+
status: Status
|
|
76
|
+
owner: string
|
|
77
|
+
description: string
|
|
78
|
+
tags: string[]
|
|
79
|
+
dataset: string
|
|
80
|
+
license: string
|
|
81
|
+
createdAt: string
|
|
82
|
+
updatedAt: string
|
|
83
|
+
}
|
|
84
|
+
type ModelVersion = {
|
|
85
|
+
version: string
|
|
86
|
+
accuracy: number
|
|
87
|
+
sizeMb: number
|
|
88
|
+
status: Status
|
|
89
|
+
createdAt: string
|
|
90
|
+
}
|
|
91
|
+
type Deployment = {
|
|
92
|
+
id: string
|
|
93
|
+
endpoint: string
|
|
94
|
+
replicas: number
|
|
95
|
+
status: 'RUNNING' | 'STOPPED' | 'FAILED'
|
|
96
|
+
updatedAt: string
|
|
97
|
+
}
|
|
98
|
+
/** 목 API 응답. null 이면 "없음" */
|
|
99
|
+
export type ModelDetail = {
|
|
100
|
+
model: Model
|
|
101
|
+
versions: ModelVersion[]
|
|
102
|
+
deployments: Deployment[]
|
|
103
|
+
} | null
|
|
104
|
+
|
|
105
|
+
// 상태값 → 글자·variant·아이콘 매핑은 한 곳에(Badge 에 아이콘 + 글자). 진행 중인 상태는 정적 loader-circle
|
|
106
|
+
type StatusView = { label: string; variant: NonNullable<BadgeProps['variant']>; icon: ReactNode }
|
|
107
|
+
const STATUS: Record<Status, StatusView> = {
|
|
108
|
+
READY: {
|
|
109
|
+
label: '준비됨',
|
|
110
|
+
variant: 'success',
|
|
111
|
+
icon: <LucideIcon iconComponentName='circle-check' />,
|
|
112
|
+
},
|
|
113
|
+
TRAINING: {
|
|
114
|
+
label: '학습 중',
|
|
115
|
+
variant: 'info',
|
|
116
|
+
icon: <LucideIcon iconComponentName='loader-circle' />,
|
|
117
|
+
},
|
|
118
|
+
FAILED: { label: '실패', variant: 'danger', icon: <LucideIcon iconComponentName='circle-x' /> },
|
|
119
|
+
DEPRECATED: {
|
|
120
|
+
label: '사용 중단',
|
|
121
|
+
variant: 'secondary',
|
|
122
|
+
icon: <LucideIcon iconComponentName='circle-slash' />,
|
|
123
|
+
},
|
|
124
|
+
}
|
|
125
|
+
const DEPLOY_STATUS: Record<Deployment['status'], StatusView> = {
|
|
126
|
+
RUNNING: { label: '실행 중', variant: 'primary', icon: <LucideIcon iconComponentName='play' /> },
|
|
127
|
+
STOPPED: {
|
|
128
|
+
label: '중지',
|
|
129
|
+
variant: 'secondary',
|
|
130
|
+
icon: <LucideIcon iconComponentName='circle-pause' />,
|
|
131
|
+
},
|
|
132
|
+
FAILED: { label: '실패', variant: 'danger', icon: <LucideIcon iconComponentName='circle-x' /> },
|
|
133
|
+
}
|
|
134
|
+
type Tab = 'overview' | 'versions' | 'deployments'
|
|
135
|
+
|
|
136
|
+
export function ModelDetailPage({ fetchModel }: { fetchModel: () => Promise<ModelDetail> }) {
|
|
137
|
+
const [data, setData] = useState<ModelDetail | undefined>() // undefined = 로딩 중, null = 없음
|
|
138
|
+
const [error, setError] = useState<string | null>(null)
|
|
139
|
+
const [tab, setTab] = useState<Tab>('overview')
|
|
140
|
+
const [confirmDelete, setConfirmDelete] = useState(false)
|
|
141
|
+
|
|
142
|
+
const load = useCallback(() => {
|
|
143
|
+
setError(null)
|
|
144
|
+
setData(undefined)
|
|
145
|
+
fetchModel().then(setData, () =>
|
|
146
|
+
setError('모델 정보를 불러오지 못했습니다. 잠시 후 다시 시도해 주세요.'),
|
|
147
|
+
)
|
|
148
|
+
}, [fetchModel])
|
|
149
|
+
useEffect(load, [load])
|
|
150
|
+
|
|
151
|
+
const model = data?.model
|
|
152
|
+
const deprecated = model?.status === 'DEPRECATED'
|
|
153
|
+
const activeEndpoints = data?.deployments.filter((d) => d.status === 'RUNNING').length ?? 0
|
|
154
|
+
|
|
155
|
+
return (
|
|
156
|
+
<>
|
|
157
|
+
<DetailPage
|
|
158
|
+
title={model?.name ?? '모델 상세'}
|
|
159
|
+
status={
|
|
160
|
+
model ? (
|
|
161
|
+
<Badge variant={STATUS[model.status].variant}>
|
|
162
|
+
{STATUS[model.status].icon}
|
|
163
|
+
{STATUS[model.status].label}
|
|
164
|
+
</Badge>
|
|
165
|
+
) : undefined
|
|
166
|
+
}
|
|
167
|
+
description={model?.description}
|
|
168
|
+
meta={
|
|
169
|
+
model ? (
|
|
170
|
+
<MetaGroup columns={3}>
|
|
171
|
+
<MetaItem label='ID' value={model.id} />
|
|
172
|
+
<MetaItem label='버전' value={model.version} />
|
|
173
|
+
<MetaItem label='소유자' value={model.owner} />
|
|
174
|
+
<MetaItem label='최근 수정일' value={model.updatedAt} />
|
|
175
|
+
<MetaItem label='사용 중인 엔드포인트' value={`${activeEndpoints}개`} />
|
|
176
|
+
<MetaItem label='학습 데이터셋' value={model.dataset} />
|
|
177
|
+
<MetaItem label='라이선스' value={model.license} />
|
|
178
|
+
</MetaGroup>
|
|
179
|
+
) : undefined
|
|
180
|
+
}
|
|
181
|
+
tabs={
|
|
182
|
+
data
|
|
183
|
+
? [
|
|
184
|
+
{ label: '개요', value: 'overview' },
|
|
185
|
+
{ label: '버전', value: 'versions', badge: data.versions.length },
|
|
186
|
+
{ label: '배포 이력', value: 'deployments', badge: data.deployments.length },
|
|
187
|
+
]
|
|
188
|
+
: undefined
|
|
189
|
+
}
|
|
190
|
+
activeTab={tab}
|
|
191
|
+
onTabChange={(v) => setTab(v as Tab)}
|
|
192
|
+
deleteAction={
|
|
193
|
+
data ? { children: '삭제', onClick: () => setConfirmDelete(true) } : undefined
|
|
194
|
+
}
|
|
195
|
+
secondaryActions={
|
|
196
|
+
data
|
|
197
|
+
? [
|
|
198
|
+
{
|
|
199
|
+
children: '편집',
|
|
200
|
+
onClick: () => toast({ title: '편집 화면은 준비 중입니다', variant: 'info' }),
|
|
201
|
+
},
|
|
202
|
+
]
|
|
203
|
+
: undefined
|
|
204
|
+
}
|
|
205
|
+
primaryAction={
|
|
206
|
+
data
|
|
207
|
+
? {
|
|
208
|
+
children: '배포',
|
|
209
|
+
disabled: deprecated,
|
|
210
|
+
onClick: () => toast({ title: '배포를 요청했습니다', variant: 'success' }),
|
|
211
|
+
}
|
|
212
|
+
: undefined
|
|
213
|
+
}
|
|
214
|
+
// 상태 자체의 안내(왜 중단됐고 무엇을 하면 되는지)는 배너로
|
|
215
|
+
banner={
|
|
216
|
+
deprecated ? (
|
|
217
|
+
<Alert variant='warning'>
|
|
218
|
+
<AlertTitle>사용 중단된 모델은 배포할 수 없습니다</AlertTitle>
|
|
219
|
+
<AlertDescription>
|
|
220
|
+
이 모델은 DEPRECATED 상태라 "배포" 버튼이 비활성화되어 있습니다. 새로 배포하려면
|
|
221
|
+
사용 가능한 버전의 모델을 선택하세요.
|
|
222
|
+
</AlertDescription>
|
|
223
|
+
</Alert>
|
|
224
|
+
) : undefined
|
|
225
|
+
}
|
|
226
|
+
loading={data === undefined && !error}
|
|
227
|
+
error={error}
|
|
228
|
+
onRetry={load}
|
|
229
|
+
notFound={data === null ? true : undefined}
|
|
230
|
+
>
|
|
231
|
+
{model && tab === 'overview' ? (
|
|
232
|
+
<div className='flex flex-col gap-[16px]'>
|
|
233
|
+
<Panel title='주요 정보'>
|
|
234
|
+
<MetaList>
|
|
235
|
+
<MetaRow align='start' label='프레임워크' value={model.framework} />
|
|
236
|
+
<MetaRow align='start' label='태스크' value={model.task} />
|
|
237
|
+
<MetaRow
|
|
238
|
+
align='start'
|
|
239
|
+
label='정확도'
|
|
240
|
+
value={`${(model.accuracy * 100).toFixed(1)}%`}
|
|
241
|
+
/>
|
|
242
|
+
<MetaRow align='start' label='크기' value={`${model.sizeMb.toLocaleString()} MB`} />
|
|
243
|
+
<MetaRow align='start' label='등록일' value={model.createdAt} />
|
|
244
|
+
<MetaRow
|
|
245
|
+
align='start'
|
|
246
|
+
label='태그'
|
|
247
|
+
value={
|
|
248
|
+
<span className='flex flex-wrap gap-[4px]'>
|
|
249
|
+
{model.tags.map((t) => (
|
|
250
|
+
<Tag key={t} variant='secondary'>
|
|
251
|
+
{t}
|
|
252
|
+
</Tag>
|
|
253
|
+
))}
|
|
254
|
+
</span>
|
|
255
|
+
}
|
|
256
|
+
/>
|
|
257
|
+
</MetaList>
|
|
258
|
+
</Panel>
|
|
259
|
+
{/* 개요 탭의 두 번째 패널: 최근 배포 — 4열·3행만, 전체는 '전체 보기' 로 배포 이력 탭(전체 폭). 패널 하나만 두면 오른쪽 열 아래가 비어 보인다 */}
|
|
260
|
+
<Panel
|
|
261
|
+
title='최근 배포'
|
|
262
|
+
titleRight={
|
|
263
|
+
<Button variant='primaryLink' onClick={() => setTab('deployments')}>
|
|
264
|
+
전체 보기
|
|
265
|
+
</Button>
|
|
266
|
+
}
|
|
267
|
+
>
|
|
268
|
+
{data && data.deployments.length > 0 ? (
|
|
269
|
+
<Table>
|
|
270
|
+
<TableHeader>
|
|
271
|
+
<TableRow>
|
|
272
|
+
<TableHead>엔드포인트</TableHead>
|
|
273
|
+
<TableHead align='right'>레플리카</TableHead>
|
|
274
|
+
{/* 짧은 열은 폭을 준다 — 폭 없는 열은 남는 폭을 똑같이 나눠 상태 열이 비어 보인다(table-fixed) */}
|
|
275
|
+
<TableHead className='w-32'>상태</TableHead>
|
|
276
|
+
<TableHead className='w-44'>갱신일</TableHead>
|
|
277
|
+
</TableRow>
|
|
278
|
+
</TableHeader>
|
|
279
|
+
<TableBody>
|
|
280
|
+
{data.deployments.slice(0, 3).map((d) => (
|
|
281
|
+
<TableRow key={d.id}>
|
|
282
|
+
<TableCell>{d.endpoint}</TableCell>
|
|
283
|
+
<TableCell align='right'>{d.replicas}</TableCell>
|
|
284
|
+
<TableCell>
|
|
285
|
+
<Badge variant={DEPLOY_STATUS[d.status].variant}>
|
|
286
|
+
{DEPLOY_STATUS[d.status].icon}
|
|
287
|
+
{DEPLOY_STATUS[d.status].label}
|
|
288
|
+
</Badge>
|
|
289
|
+
</TableCell>
|
|
290
|
+
<TableCell>{d.updatedAt}</TableCell>
|
|
291
|
+
</TableRow>
|
|
292
|
+
))}
|
|
293
|
+
</TableBody>
|
|
294
|
+
</Table>
|
|
295
|
+
) : (
|
|
296
|
+
<p className='text-base-regular text-sys-text-subtle'>
|
|
297
|
+
아직 배포된 엔드포인트가 없습니다.
|
|
298
|
+
</p>
|
|
299
|
+
)}
|
|
300
|
+
</Panel>
|
|
301
|
+
</div>
|
|
302
|
+
) : null}
|
|
303
|
+
{data && tab === 'versions' ? (
|
|
304
|
+
<Table>
|
|
305
|
+
<TableHeader>
|
|
306
|
+
<TableRow>
|
|
307
|
+
<TableHead>버전</TableHead>
|
|
308
|
+
<TableHead>정확도</TableHead>
|
|
309
|
+
<TableHead>크기</TableHead>
|
|
310
|
+
<TableHead>상태</TableHead>
|
|
311
|
+
<TableHead>등록일</TableHead>
|
|
312
|
+
</TableRow>
|
|
313
|
+
</TableHeader>
|
|
314
|
+
<TableBody>
|
|
315
|
+
{data.versions.map((v) => (
|
|
316
|
+
<TableRow key={v.version}>
|
|
317
|
+
<TableCell>{v.version}</TableCell>
|
|
318
|
+
<TableCell>{(v.accuracy * 100).toFixed(1)}%</TableCell>
|
|
319
|
+
<TableCell>{v.sizeMb.toLocaleString()} MB</TableCell>
|
|
320
|
+
<TableCell>
|
|
321
|
+
<Badge variant={STATUS[v.status].variant}>
|
|
322
|
+
{STATUS[v.status].icon}
|
|
323
|
+
{STATUS[v.status].label}
|
|
324
|
+
</Badge>
|
|
325
|
+
</TableCell>
|
|
326
|
+
<TableCell>{v.createdAt}</TableCell>
|
|
327
|
+
</TableRow>
|
|
328
|
+
))}
|
|
329
|
+
</TableBody>
|
|
330
|
+
</Table>
|
|
331
|
+
) : null}
|
|
332
|
+
{data && tab === 'deployments' ? (
|
|
333
|
+
<Table>
|
|
334
|
+
<TableHeader>
|
|
335
|
+
<TableRow>
|
|
336
|
+
<TableHead>엔드포인트</TableHead>
|
|
337
|
+
<TableHead>레플리카</TableHead>
|
|
338
|
+
<TableHead>상태</TableHead>
|
|
339
|
+
<TableHead>갱신일</TableHead>
|
|
340
|
+
</TableRow>
|
|
341
|
+
</TableHeader>
|
|
342
|
+
<TableBody>
|
|
343
|
+
{data.deployments.map((d) => (
|
|
344
|
+
<TableRow key={d.id}>
|
|
345
|
+
<TableCell>{d.endpoint}</TableCell>
|
|
346
|
+
<TableCell>{d.replicas}</TableCell>
|
|
347
|
+
<TableCell>
|
|
348
|
+
<Badge variant={DEPLOY_STATUS[d.status].variant}>
|
|
349
|
+
{DEPLOY_STATUS[d.status].icon}
|
|
350
|
+
{DEPLOY_STATUS[d.status].label}
|
|
351
|
+
</Badge>
|
|
352
|
+
</TableCell>
|
|
353
|
+
<TableCell>{d.updatedAt}</TableCell>
|
|
354
|
+
</TableRow>
|
|
355
|
+
))}
|
|
356
|
+
</TableBody>
|
|
357
|
+
</Table>
|
|
358
|
+
) : null}
|
|
359
|
+
</DetailPage>
|
|
360
|
+
|
|
361
|
+
<AlertDialog open={confirmDelete} onOpenChange={setConfirmDelete}>
|
|
362
|
+
<AlertDialogContent variant='danger'>
|
|
363
|
+
<AlertDialogHeader>
|
|
364
|
+
<AlertDialogTitle>{model?.name} 을(를) 삭제할까요?</AlertDialogTitle>
|
|
365
|
+
<AlertDialogDescription>
|
|
366
|
+
버전 {data?.versions.length ?? 0}개와 배포 이력이 함께 삭제되며 되돌릴 수 없습니다.
|
|
367
|
+
</AlertDialogDescription>
|
|
368
|
+
</AlertDialogHeader>
|
|
369
|
+
<AlertDialogFooter>
|
|
370
|
+
<AlertDialogCancel>취소</AlertDialogCancel>
|
|
371
|
+
<AlertDialogAction
|
|
372
|
+
onClick={() => {
|
|
373
|
+
setData(null)
|
|
374
|
+
toast({
|
|
375
|
+
title: '모델을 삭제했습니다',
|
|
376
|
+
description: `${model?.name} 이(가) 삭제되었습니다.`,
|
|
377
|
+
variant: 'success',
|
|
378
|
+
})
|
|
379
|
+
}}
|
|
380
|
+
>
|
|
381
|
+
삭제
|
|
382
|
+
</AlertDialogAction>
|
|
383
|
+
</AlertDialogFooter>
|
|
384
|
+
</AlertDialogContent>
|
|
385
|
+
</AlertDialog>
|
|
386
|
+
</>
|
|
387
|
+
)
|
|
388
|
+
}
|