@gaonjs/cli 0.64.0 → 0.65.2
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/dist/commands/check.js +34 -18
- package/dist/commands/g.d.ts +1 -0
- package/dist/commands/g.js +11 -2
- package/dist/commands/gen.js +1 -1
- package/dist/dev.d.ts +13 -7
- package/dist/dev.js +30 -22
- package/dist/doctor/fixers/i18n-layout.d.ts +2 -2
- package/dist/doctor/fixers/i18n-layout.js +119 -25
- package/dist/doctor/fixers/index.d.ts +1 -1
- package/dist/doctor/fixers/index.js +13 -3
- package/dist/doctor/fixers/types.d.ts +17 -2
- package/dist/doctor/i18n-app-scope.d.ts +3 -0
- package/dist/doctor/i18n-app-scope.js +94 -65
- package/dist/doctor/i18n-layout.d.ts +1 -1
- package/dist/doctor/i18n-layout.js +157 -32
- package/dist/doctor/i18n-server-scope.d.ts +2 -0
- package/dist/doctor/i18n-server-scope.js +124 -0
- package/dist/doctor/locale-parity.js +29 -68
- package/dist/doctor/redundant-index.d.ts +7 -0
- package/dist/doctor/redundant-index.js +67 -0
- package/dist/doctor/schema-relations.d.ts +13 -0
- package/dist/doctor/schema-relations.js +5 -1
- package/dist/doctor/types.d.ts +1 -1
- package/dist/doctor.d.ts +3 -1
- package/dist/doctor.js +42 -10
- package/dist/i18n-config.d.ts +9 -9
- package/dist/i18n-config.js +8 -15
- package/dist/messages-gen.d.ts +10 -10
- package/dist/messages-gen.js +42 -54
- package/dist/scaffold/app.d.ts +10 -1
- package/dist/scaffold/app.js +12 -2
- package/dist/templates/project/AGENTS.md.tpl +8 -6
- package/dist/templates/project/agents/data.md.tpl +6 -0
- package/dist/templates/project/agents/frontend.md.tpl +218 -75
- package/dist/templates/project/agents/i18n.md.tpl +165 -70
- package/dist/templates/project/agents/web.md.tpl +4 -3
- package/dist/templates/project/{locales → apps/web/locales}/en/frontend.json.tpl +1 -0
- package/dist/templates/project/{locales → apps/web/locales}/ko/frontend.json.tpl +1 -0
- package/dist/templates/project/gaon.config.ts.tpl +6 -5
- package/package.json +7 -7
- package/dist/templates/project/apps/web/locales/en.json.tpl +0 -3
- package/dist/templates/project/apps/web/locales/ko.json.tpl +0 -3
- /package/dist/templates/project/{locales → domain/locales}/en/backend.json.tpl +0 -0
- /package/dist/templates/project/{locales → domain/locales}/ko/backend.json.tpl +0 -0
|
@@ -340,83 +340,204 @@ import PageShell from '@shared/components/ui/PageShell.vue'
|
|
|
340
340
|
| 원자 (18) | Button · Input · Label · Badge · Card · CardHeader · CardTitle · CardDescription · CardContent · CardFooter · Alert · AlertTitle · AlertDescription · Form · FormField · FormMessage · Dialog · Sheet |
|
|
341
341
|
| 블록 (4 · 결정 106) | PageShell · PageHeader · EmptyState · Pagination |
|
|
342
342
|
|
|
343
|
-
|
|
344
|
-
소스에서 찾아야
|
|
345
|
-
|
|
346
|
-
|
|
343
|
+
**컴포넌트 사용법 — 킷 21종 전수 (첫 시도용 · 소스 안 읽어도 되게 · O-2).** 카탈로그가
|
|
344
|
+
이름만이면 슬롯/prop 을 소스에서 찾아야 한다 — 표면 전부를 여기 못박는다. 아래 값은
|
|
345
|
+
`gaon g ui-kit` 이 심는 **템플릿 소스의 `defineProps`·`defineEmits`·`<slot>` 에서 확인한
|
|
346
|
+
것**이다(그래도 정본은 프로젝트에 복사된 여러분의 파일이다 — 고쳤다면 고친 쪽이 맞다).
|
|
347
|
+
**주의: named slot 이름이 비대칭이다** — `PageHeader` 는 `#actions`(**복수**), `EmptyState`
|
|
348
|
+
는 `#action`(**단수**). 슬롯 이름을 틀리면 오류 없이 **조용히 안 그려진다**.
|
|
347
349
|
|
|
348
|
-
|
|
350
|
+
*블록 (4 · 결정 106) — 화면 골격.*
|
|
351
|
+
|
|
352
|
+
| 컴포넌트 | props (기본값) | slots | emits |
|
|
349
353
|
|---|---|---|---|
|
|
350
|
-
| **PageShell** | `size?: 'default'\|'narrow'\|'wide'\|'full'` | 기본 | — |
|
|
351
|
-
| **PageHeader** | `title
|
|
352
|
-
| **EmptyState** | `title
|
|
353
|
-
| **Pagination** | `page`(필수) · `pageCount`(필수) · `siblings
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
|
358
|
-
|
|
359
|
-
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
- **`
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
`
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
354
|
+
| **PageShell** | `size?: 'default'\|'narrow'\|'wide'\|'full'` (`'default'`) | 기본 | — |
|
|
355
|
+
| **PageHeader** | `title?: string` · `description?: string` | 기본 없음 · `#title` · `#description` · **`#actions`**(복수) | — |
|
|
356
|
+
| **EmptyState** | `title?: string` · `description?: string` | 기본 없음 · `#icon` · `#title` · `#description` · **`#action`**(단수) | — |
|
|
357
|
+
| **Pagination** | `page: number`(필수) · `pageCount: number`(필수) · `siblings?: number` (`1`) | — | `update:page` (= `v-model:page`) |
|
|
358
|
+
|
|
359
|
+
*원자 (18) — 표면·컨트롤.*
|
|
360
|
+
|
|
361
|
+
| 컴포넌트 | props (기본값) | slots | emits |
|
|
362
|
+
|---|---|---|---|
|
|
363
|
+
| **Button** | `variant?: 'default'\|'secondary'\|'destructive'\|'outline'\|'ghost'\|'link'` (`'default'`) · `size?: 'default'\|'sm'\|'lg'\|'icon'` (`'default'`) · `type?: 'button'\|'submit'\|'reset'` (`'button'`) · `href?: string` · `external?: boolean` · `target?: string` | 기본 | 네이티브(예 `@click`) |
|
|
364
|
+
| **Input** | `modelValue?: string \| number` | — | `update:modelValue` (= `v-model`) |
|
|
365
|
+
| **Label** | — | 기본 | — |
|
|
366
|
+
| **Badge** | `variant?: 'default'\|'secondary'\|'destructive'\|'outline'` (`'default'`) | 기본 | — |
|
|
367
|
+
| **Alert** | `variant?: 'default'\|'destructive'` (`'default'`) | 기본 | — |
|
|
368
|
+
| **AlertTitle** / **AlertDescription** | — | 기본 | — |
|
|
369
|
+
| **Card** / **CardHeader** / **CardTitle** / **CardDescription** / **CardContent** / **CardFooter** | — | 기본(조합) | — |
|
|
370
|
+
| **Form** | — | 기본 | `submit` (`@submit` 를 `useForm` 의 post/put/delete 로 넘긴다) |
|
|
371
|
+
| **FormField** | `label?: string` · `error?: string` | 기본(컨트롤) | — |
|
|
372
|
+
| **FormMessage** | `message?: string` | — | — |
|
|
373
|
+
| **Dialog** | `open: boolean`(필수) | 기본 | `update:open` (= `v-model:open`) |
|
|
374
|
+
| **Sheet** | `open: boolean`(필수) · `side?: 'left'\|'right'` (`'right'`) | 기본 | `update:open` (= `v-model:open`) |
|
|
375
|
+
|
|
376
|
+
**쓸 때 자주 틀리는 것:**
|
|
377
|
+
|
|
378
|
+
- **`PageHeader`·`EmptyState` 는 기본 슬롯이 없다** — 제목/설명은 prop 이거나 named slot
|
|
379
|
+
이다. `<PageHeader>제목</PageHeader>` 는 아무것도 안 그린다(오류도 안 난다).
|
|
380
|
+
- **`Dialog`·`Sheet` 는 `v-model:open` 이다** — `:open` 만 주면 열리기만 하고 안 닫힌다
|
|
381
|
+
(닫기 요청이 `update:open` 으로 나가는데 받는 쪽이 없다).
|
|
382
|
+
- **`Pagination` 은 `v-model:page` 이거나 `@update:page`** — 실제 이동(`router.get`)은
|
|
383
|
+
**페이지가** 한다. 킷은 라우트를 모른다(결정 25·105).
|
|
384
|
+
- **`Input` 의 `update:modelValue` 는 `string`** 이다. 숫자를 받아야 하면 페이지에서
|
|
385
|
+
변환한다(`Number(...)`) — 킷이 타입을 추측하지 않는다.
|
|
386
|
+
- **`Button` 의 variant 는 6종뿐이다** — `dangerSoft`·`plain` 같은 이름은 이 킷에 없다.
|
|
387
|
+
더 필요하면 여러분의 `Button.vue` 에 직접 더한다(복사-소유).
|
|
388
|
+
- **`class` 는 그냥 넘기면 루트로 폴스루된다** — `<Button class="w-full">`. 별도
|
|
389
|
+
`class` prop 이 없다.
|
|
390
|
+
|
|
391
|
+
> **왜 여기 코드 예시가 없나:** 문서 예시 컴파일 게이트(결정 458)의 픽스처
|
|
392
|
+
> 워크스페이스에는 킷이 `Button`·`Card`·`PageShell` 만 있고 나머지(`PageHeader`·
|
|
393
|
+
> `EmptyState`·`Pagination`·`Form`·`FormField`·`Input`·`Dialog`·`Badge`)가 없어,
|
|
394
|
+
> 이들을 쓰는 예시는 검증할 수 없다. 검증 못 하는 예시는 싣지 않는다 — 위 표가
|
|
395
|
+
> 표면의 정본이고, 조합 예시는 `gaon new` 스캐폴드의 실제 페이지가 정본이다.
|
|
396
|
+
|
|
397
|
+
**킷을 넓힐 때 — 관리 화면 블록 관례.** 위 카탈로그(원자 18 + 블록 4)는 `gaon new` 가
|
|
398
|
+
심는 **최소 한 벌**이다. 관리 화면을 만들면 목록·표·트리 같은 블록이 곧 필요해지는데
|
|
399
|
+
이것들은 **프레임웍이 주지 않는다** — 여러분 프로젝트의 `shared/components/ui/` 에
|
|
400
|
+
직접 만들어 소유한다(위 "예약" 항목). 그때의 관례는 아래와 같다. 이름은 예시일 뿐
|
|
401
|
+
프레임웍 API 가 아니다 — 규칙만 가져가고 이름은 프로젝트가 정한다.
|
|
402
|
+
|
|
403
|
+
- **목록 화면은 순서를 고정한다** — 헤더 → 필터 바 → 목록 카드(편집 바 + 본문) →
|
|
404
|
+
페이지네이션. 화면마다 순서가 다르면 같은 콘솔 안에서 손이 헤맨다. 블록 하나가
|
|
405
|
+
이 순서를 소유하고 각 자리를 named slot 으로 연다. **빈 슬롯은 그 줄째 사라지게**
|
|
406
|
+
한다 — 자리를 남기려고 공백을 넣지 않는다.
|
|
407
|
+
- **목록의 표는 CSS 그리드로 짠다** — `display:grid` + `grid-template-columns`.
|
|
408
|
+
컬럼 폭·정렬·좁은 화면 숨김이 **컬럼 정의 한 곳**에 모여, 열을 하나 넣고 뺄 때
|
|
409
|
+
`colgroup`·`th`·`td` 세 군데를 고칠 일이 없다. 체크 열·액션 열을 앞에 붙이는
|
|
410
|
+
계산도 문자열 하나로 끝난다. **대신 `role` 을 직접 붙인다** — `<table>` 이 공짜로
|
|
411
|
+
주던 행·열 관계가 그리드엔 없어서, `role="grid"/"row"/"columnheader"/"gridcell"` 과
|
|
412
|
+
`aria-sort` 가 없으면 스크린리더에는 표가 아니라 글자 더미로 읽힌다. `<table>` 은
|
|
413
|
+
본문 안에 끼우는 짧은 표에만 남긴다.
|
|
414
|
+
- **정렬 헤더는 목록 전체가 클라이언트에 있을 때만 연다.** 서버 페이지네이션
|
|
415
|
+
(`paginate()`) 목록에서 헤더 정렬을 열면 **현재 쪽만** 정렬돼 "가격 높은 순" 이
|
|
416
|
+
전체가 아니라 그 쪽 안에서만 맞는 거짓말이 된다. 서버 정렬을 붙이기 전까지는 그
|
|
417
|
+
컬럼의 정렬을 막아 둔다.
|
|
418
|
+
- **상태 문자열 → 색 매핑은 한 곳에 둔다** — `shared/lib/` 에 `statusTone(status)`
|
|
419
|
+
같은 함수 하나를 두고 배지가 그것만 쓴다. 화면마다 삼항 연산으로 색을 고르면 같은
|
|
420
|
+
"미처리" 가 화면에 따라 다른 색이 된다. 모르는 값은 중립색으로 떨어뜨려 화면이
|
|
421
|
+
깨지지 않게 한다.
|
|
422
|
+
- **떠 있는 패널(드롭다운·달력·빠른 동작)은 `Teleport` 여부를 "자르는 조상" 으로
|
|
423
|
+
판단한다.** ① 카드처럼 `overflow` 로 자르는 조상 안에서 열리면 → `Teleport to="body"`
|
|
424
|
+
+ 좌표 계산(안 그러면 패널이 잘린다). ② 헤더·사이드바처럼 자르는 조상이 없고 앵커에
|
|
425
|
+
붙어 스크롤을 따라가야 하면 → Teleport 하지 않는다. **②의 경우 그 패널을 감싸는
|
|
426
|
+
상자에 `overflow-hidden` 을 주지 않는다** — 모서리를 둥글리려고 무심코 준
|
|
427
|
+
`overflow-hidden` 하나가 패널을 통째로 안 보이게 만든다(모서리는 안쪽 면에 직접
|
|
428
|
+
둥글리기를 준다).
|
|
429
|
+
- **트리거의 여닫기는 쓰는 쪽이 붙인다.** 드롭다운 블록은 열림 상태와 "바깥 클릭·Esc
|
|
430
|
+
로 닫기" 만 갖고 `@click` 은 쓰는 쪽이 준다 — 상단 바처럼 여럿이 나란히 있을 때
|
|
431
|
+
"여는 쪽이 나머지를 닫는" 규칙을 블록이 대신 정해 버리면 그 규칙을 바꿀 수 없다.
|
|
432
|
+
- **셸(사이드바·상단 바)이 든 상태는 슬롯 프롭으로 내린다.** 레이아웃은 셸의 **부모**라
|
|
433
|
+
`provide`/`inject` 로는 셸이 든 상태(접힘·콘텐츠 넓이)를 받지 못한다 — `inject` 가
|
|
434
|
+
조용히 `undefined` 가 되고 "코드는 맞는데 화면만 안 바뀐다" 로 나타난다.
|
|
435
|
+
- **사이드바 활성 판정은 "가장 긴 접두 하나" 다.** 정확 일치만 보면 상세 화면
|
|
436
|
+
(`/admin/members/1`)에서 아무 메뉴도 안 켜지고, 그냥 `startsWith` 면 `/admin/posts`
|
|
437
|
+
와 `/admin/posts/trash` 가 **함께** 켜진다. 후보 중 현재 경로의 접두이면서 가장 긴
|
|
438
|
+
것 하나만 켠다(경계는 세그먼트 단위 — `/admin/postscript` 가 `/admin/posts` 를 먹지
|
|
439
|
+
않게). 루트 항목(`/admin`)은 접두로 이기지 않게 정확 일치로 둔다. 이 판정은 **앱이**
|
|
440
|
+
한다 — 킷은 라우트를 모른다(결정 25·105).
|
|
441
|
+
- **shared 블록이 문구를 직접 번역하면 그 키는 전 앱 카탈로그에 있어야 한다** —
|
|
442
|
+
`shared/` 는 어느 앱 번들에도 실릴 수 있어서다. 한 앱에만 있으면 다른 앱 화면에서만
|
|
443
|
+
키 문자열이 뜨는 조용한 실패가 된다(doctor **i18n-app-scope** 가 잡는다 ·
|
|
444
|
+
`agents/i18n.md`). 문구를 props 로 끌어올려 회피하지 말고 키를 복제한다.
|
|
445
|
+
|
|
446
|
+
**콘솔 블록 표면 정본 (프로젝트가 만들어 쓰는 확장 킷).** 아래는 관리 콘솔을 실제로
|
|
447
|
+
만들며 굳은 표면이다. **`gaon new` 는 이것들을 심지 않는다** — 여러분 프로젝트의
|
|
448
|
+
`shared/components/ui/` 에 만들어야 있고, 만들 때 이 표면을 그대로 쓰면 화면 코드가
|
|
449
|
+
프로젝트 사이에서 옮겨 다닌다. 이미 만들어져 있다면 **여러분 파일이 정본**이다(고쳤다면
|
|
450
|
+
고친 쪽이 맞다). 값은 실제 `defineProps`·`defineEmits`·`<slot>` 에서 확인한 것이다.
|
|
451
|
+
|
|
452
|
+
*목록 화면.*
|
|
453
|
+
|
|
454
|
+
| 컴포넌트 | props (기본값) | slots | emits |
|
|
455
|
+
|---|---|---|---|
|
|
456
|
+
| **ListPage** | `title?` · `description?` · `total?: number`(편집 바 "N건") · `listTitle?`(목록 카드 제목) · `listHint?` · `asideWidth?: number` (`260`) | 기본(목록 본문) · `#actions` · `#aside` · `#filters` · `#filter-bulk` · `#filter-actions` · `#list-actions` · `#pagination` | `search` |
|
|
457
|
+
| **DataGrid** | `columns: GridColumn[]`(필수) · `rows: Record<string,unknown>[]`(필수) · `rowKey: string`(필수) · `selectable?` (`false`) · `selected?: string[]` (`[]`) · `actions?: 'none'\|'detail'\|'full'` (`'none'`) · `labelKey?` · `sortKey?` · `sortDir?: 'asc'\|'desc'` (`'asc'`) · `minWidth?: number` (`640`) | `#cell-<컬럼키>`(셀마다) | `sort(key)` · `toggleAll(on)` · `toggleOne(id,on)` · `detail(row)` · `edit(row)` · `remove(row)` |
|
|
458
|
+
| **FilterBar** | `as?: string` (`'form'`) | 기본(필터 격자) · `#bulk`(아래 줄 좌) · `#actions`(아래 줄 우) | `submit` |
|
|
459
|
+
| **ListBulk** | `sizeLabel: string`(필수) · `selectedCount: number`(필수) · `statuses?: string[]`(없으면 셀렉트 자체가 안 나옴) | — | `update:sizeLabel` · `status(value)` · `remove` |
|
|
460
|
+
| **RowActions** | `detailOnly?` (`false`) · `label?`(스크린리더용 대상 이름) | — | `detail` · `edit` · `remove` |
|
|
461
|
+
| **UserCell** | `user: UserCellUser`(필수) · `interactive?` (`true`) · `statusOptions?` · `gradeOptions?` | — | `action(key, user, value?)` |
|
|
462
|
+
| **TitleCell** | `title: string`(필수) · `prefix?`(앞 배지) · `prefixTone?: 'accent'\|'danger'\|'warning'\|'neutral'` (`'accent'`) · `count?`·`likes?`·`files?` (`0` · 0이면 안 나옴) · `depth?` (`0`) · `secret?` (`false`) | — | — |
|
|
463
|
+
|
|
464
|
+
`GridColumn` = `{ key, label, w?, num?, mono?, hideMobile?, noSort? }` — `w` 는
|
|
465
|
+
`grid-template-columns` 조각(없으면 `minmax(0,1fr)`) · `num` 은 우측 정렬 + `tabular-nums`
|
|
466
|
+
· `hideMobile` 은 좁은 화면 숨김 · `noSort` 는 정렬 헤더를 막는다(서버 페이지네이션).
|
|
467
|
+
|
|
468
|
+
*설정·상세 화면.*
|
|
469
|
+
|
|
470
|
+
| 컴포넌트 | props (기본값) | slots | emits |
|
|
471
|
+
|---|---|---|---|
|
|
472
|
+
| **Splitter** | `width?: number` (`230`) · `minLeft?` (`230`) · `minRight?` (`220`) · `height?: string`(비우면 화면 높이) · `stackAt?: number` (`768`) | `#left` · `#right` | `update:width` |
|
|
473
|
+
| **TreeView** | `nodes: TreeNode[]`(필수) · `title?` · `selected?` · `addLabel?: string\|null`(null 이면 추가 버튼 숨김) · `searchPlaceholder?` · `totalLabel?` · `editable?` (`false`) · `reorderable?` (`false`) | — | `select(node, path)` · `add` · `edit(node, depth)` · `reorder(from, to, 'before'\|'after')` |
|
|
474
|
+
| **DetailFilter** | `selects: DetailSelect[]`(필수) · `values: string[]`(필수) · `range: DateRange`(필수) · `query: string`(필수) · `searchPlaceholder: string`(필수) | — | `update:values` · `update:range` · `update:query` · `reset` · `search` |
|
|
475
|
+
| **PermissionMatrix** | `modelValue: Record<string,string>`(필수) · `rows: MatrixRow[]`(필수 · `{key,label,desc}` — **`desc` 도 필수**) · `targets: string[]`(필수) · `disabled?` | — | `update:modelValue` |
|
|
476
|
+
| **SettingRow** | `setting: Setting`(필수) · `typeLabel: string`(필수) · `editMode?` (`false`) | — | `change` · `revert` · `remove` · `copyKey` · `toggleScope` |
|
|
477
|
+
|
|
478
|
+
*컨트롤·표시.*
|
|
479
|
+
|
|
480
|
+
| 컴포넌트 | props (기본값) | slots | emits |
|
|
481
|
+
|---|---|---|---|
|
|
482
|
+
| **FloatingField** | `label: string`(필수) · `modelValue?: string\|number` · `control?: 'input'\|'textarea'\|'select'` (`'input'`) · `type?` (`'text'`) · `rows?` (`4`) · `error?` · `hint?` · `surface?: 'background'\|'card'\|'popover'` (`'background'`) | 기본(select 의 `<option>`) | `update:modelValue` |
|
|
483
|
+
| **DateRangeField** | `modelValue?: DateRange` · `label?` · `surface?` (`'card'`) | — | `update:modelValue` |
|
|
484
|
+
| **Segmented** | `modelValue: string`(필수) · `options: {value,label}[]`(필수) · `label?` · `disabled?` | — | `update:modelValue` |
|
|
485
|
+
| **Switch** | `modelValue?: boolean` · `label?` · `disabled?` | — | `update:modelValue` |
|
|
486
|
+
| **Checkbox** | `modelValue?: boolean` · `label?` | 기본 | `update:modelValue` |
|
|
487
|
+
| **Select** / **Textarea** | `modelValue?` (Textarea 는 `rows?` 추가) | Select: 기본(`<option>`) | `update:modelValue` |
|
|
488
|
+
| **Icon** | `name: string`(필수 · 레지스트리 키) · `size?: number` (`15`) · `strokeWidth?: number` (`1.75`) | — | — |
|
|
489
|
+
| **Avatar** | `name: string`(필수) · `src?` · `size?: 'sm'\|'default'\|'lg'` (`'default'`) | — | — |
|
|
490
|
+
| **StatCard** | `label: string`·`value: string`(필수) · `delta?` · `trend?: 'up'\|'down'\|'flat'` (`'flat'`) · `icon?` · `clickable?` (`false`) | — | — |
|
|
491
|
+
| **Tooltip** | `text: string`(필수) · `side?: 'top'\|'bottom'` (`'top'`) | 기본 | — |
|
|
492
|
+
| **Skeleton** | `variant?: 'text'\|'block'\|'circle'` (`'block'`) | — | — |
|
|
493
|
+
| **Spinner** | `size?: 'sm'\|'default'\|'lg'` (`'default'`) · `label?` | — | — |
|
|
494
|
+
|
|
495
|
+
*떠 있는 것 · 셸.*
|
|
496
|
+
|
|
497
|
+
| 컴포넌트 | props (기본값) | slots | emits |
|
|
498
|
+
|---|---|---|---|
|
|
499
|
+
| **Dropdown** | `open: boolean`(필수) · `align?: 'left'\|'right'` (`'right'`) · `up?` (`false`) · `width?` (`'192px'`) · `label?` | `#trigger`(**여닫기 `@click` 은 여기에 직접 붙인다**) · 기본(패널 내용) | `update:open` |
|
|
500
|
+
| **ConfirmDialog** | `open: boolean`·`title: string`(필수) · `description?` · `confirmLabel?` · `cancelLabel?` | — | `update:open` · `confirm` |
|
|
501
|
+
| **Toast** | `title: string`(필수) · `description?` · `variant?: 'default'\|'success'\|'warning'\|'destructive'` (`'default'`) | `#action` | — |
|
|
502
|
+
| **Banner** | `title: string`(필수) · `description?` · `tone?: 'neutral'\|'accent'\|'success'\|'warning'\|'danger'\|'info'` (`'info'`) · `action?`(없으면 링크 자체가 안 나옴) · `dismissible?` (`false`) | — | `action` · `dismiss` |
|
|
503
|
+
| **Editor** | `modelValue?: string`(HTML) · `placeholder?` · `minHeight?: number` (`220`) | — | `update:modelValue` |
|
|
504
|
+
| **Tabs** | `modelValue: string`(필수) | 기본(TabList·TabPanel 조합) | `update:modelValue` |
|
|
505
|
+
| **Collapse** | `open: boolean`(필수) | 기본 | — |
|
|
506
|
+
| **AdminShell** | `size?: 'default'\|'wide'\|'shell'\|'full'` (`'shell'`) · `align?: 'center'\|'start'` (`'start'`) · `sidebar?: 'expanded'\|'mini'\|'off'` (`'expanded'`) · `collapsible?` (`true`) | 기본 · `#brand`(슬롯 프롭 `{mini}`) · `#nav` · `#topbar` · `#topbar-actions`(슬롯 프롭 `{fullWidth,setFullWidth}`) · `#account` · `#sidebar-footer` · `#footer` | — |
|
|
507
|
+
|
|
508
|
+
*레이아웃 원자(간격을 값이 아니라 이름으로 준다).*
|
|
509
|
+
|
|
510
|
+
| 컴포넌트 | props (기본값) |
|
|
511
|
+
|---|---|
|
|
512
|
+
| **Container** | `size?: 'narrow'\|'default'\|'wide'\|'shell'\|'full'` (`'default'`) · `gutter?` (`true`) · `align?: 'center'\|'start'` (`'center'`) |
|
|
513
|
+
| **Stack** | `gap?: 'none'\|'inline'\|'stack'\|'group'\|'section'` (`'stack'`) · `as?` (`'div'`) · `align?` (`'stretch'`) |
|
|
514
|
+
| **Cluster** | `gap?: 'none'\|'inline'\|'stack'\|'group'` (`'inline'`) · `justify?: 'start'\|'center'\|'end'\|'between'` (`'start'`) · `align?` (`'center'`) · `wrap?` (`true`) |
|
|
515
|
+
| **Grid** | `cols?: 'auto'\|1\|2\|3\|4` (`'auto'`) · `gap?` (`'group'`) |
|
|
516
|
+
| **Section** | `gap?: 'stack'\|'group'\|'section'` (`'group'`) · `label?` |
|
|
517
|
+
| **Separator** | `orientation?: 'horizontal'\|'vertical'` (`'horizontal'`) · `decorative?` (`true`) |
|
|
518
|
+
|
|
519
|
+
*함께 쓰는 컴포저블 (`shared/lib/`).*
|
|
520
|
+
|
|
521
|
+
| 이름 | 인자 | 반환 |
|
|
522
|
+
|---|---|---|
|
|
523
|
+
| **`useSort(rows)`** | `ComputedRef<T[]>`(필터까지 끝난 행) | `{ sortKey, sortDir, sorted, toggle(key) }` — 같은 키 재클릭이면 방향만 뒤집는다 |
|
|
524
|
+
| **`useListPage({ rows, key, size? })`** | `rows`=정렬까지 끝난 행 · `key(row)=>string` · `size`=초기 표시 개수 | `{ page, sizeLabel, pageSize, pageCount, paged, selected, allChecked, selectionNote, toggleAll, toggleOne, clear }` |
|
|
525
|
+
| **`statusTone(status)`** | 상태 문자열 | `Tone` — 모르는 값은 `neutral` 로 떨어진다 |
|
|
526
|
+
| **`useToast()`** | — | `{ toasts, toast, success, warning, error, dismiss }` — 렌더는 레이아웃에 한 번 둔 `ToastRegion` 이 한다 |
|
|
527
|
+
|
|
528
|
+
**조립 순서(외우기).** `ListPage`(골격) → `#filters` 에 `FloatingField`·`DateRangeField` →
|
|
529
|
+
`#filter-bulk` 에 `ListBulk` → 본문에 `DataGrid`(셀은 `#cell-<키>` 로 `UserCell`·
|
|
530
|
+
`TitleCell`·`Badge`) → `#pagination` 에 `Pagination`. 상태는
|
|
531
|
+
`useSort` → `useListPage` 순서로 감는다(정렬한 뒤 쪽을 자른다 · 반대로 하면 현재 쪽
|
|
532
|
+
안에서만 정렬된다).
|
|
533
|
+
|
|
534
|
+
**킷 사용법 문서는 데이터로 두고 화면이 렌더한다.** 블록이 열 개를 넘어가면 "이게 무슨
|
|
535
|
+
컴포넌트인지" 를 매번 소스에서 읽게 된다. 항목(설명·props·사용 예·규칙)을 **모듈 하나**에
|
|
536
|
+
모으고 그것을 렌더하는 페이지를 하나 두면, 컴포넌트를 더할 때 항목만 더하면 된다.
|
|
537
|
+
그 페이지에는 **실물 미리보기**를 함께 낸다 — 살아 있는 컴포넌트를 그대로 렌더하면
|
|
538
|
+
컴포넌트가 바뀔 때 미리보기도 같이 바뀌어 문서가 늙지 않는다(스크린샷은 늙는다).
|
|
539
|
+
props 표에는 **실제 `defineProps` 에서 확인한 것만** 적는다 — 없는 prop 을 그럴듯하게
|
|
540
|
+
적어 두느니 행을 비우는 편이 낫다.
|
|
420
541
|
|
|
421
542
|
### 9. 클라이언트 환경변수 — `env` (결정 198 · F-9 옵션 ②)
|
|
422
543
|
|
|
@@ -539,6 +660,28 @@ async function runSearch(q: string) {
|
|
|
539
660
|
(결정 37).
|
|
540
661
|
- **`v-html` 은 XSS 탈출구** — 사용자 입력을 넣지 않는다
|
|
541
662
|
(`agents/security.md`).
|
|
663
|
+
- **`gaon dev` 에 Vue HMR 은 없다 — 저장 후 브라우저를 새로고침한다.** dev 는 Vite
|
|
664
|
+
dev 서버를 띄우지 않고 **`vite build --watch`** 로 번들을 다시 만들며(앱마다 ·
|
|
665
|
+
결정 146), 서버는 `dist/<앱>` 을 서빙한다. `.vue`·`.ts`·`theme/*.css` 를 고치면
|
|
666
|
+
재빌드가 돌지만(실측 1.1~1.3초) **화면은 새로고침해야 바뀐다** — Inertia 탓이
|
|
667
|
+
아니라 v1 어댑터에 Vite 통합이 없어서다(§6.4). "고쳤는데 화면이 그대로" 를 코드
|
|
668
|
+
문제로 오진하지 말 것. 콘솔의 `[vite] built in …` 이 재빌드 완료 신호다.
|
|
669
|
+
- **`tailwind.config.ts` 를 고치면 `gaon dev` 를 재시작한다** — watch 프로세스가
|
|
670
|
+
Tailwind 설정을 물고 있어 **재빌드가 돌아도 새 유틸이 생성되지 않는다**(실측
|
|
671
|
+
2026-08-07: `theme.extend.fontSize` 에 계단을 더했는데 `.text-<이름>` 이 CSS 에
|
|
672
|
+
아예 안 나옴 → 클래스가 조용히 무효). 증상이 "클래스를 썼는데 스타일이 하나도
|
|
673
|
+
안 먹는다" 라 오진하기 쉽다. **토큰 값(`theme/*.css`)만 고칠 때는 새로고침으로
|
|
674
|
+
충분하고, 토큰↔유틸 매핑(설정)을 고칠 때만 재시작**한다.
|
|
675
|
+
- **웹폰트·정적 자산은 CSS 에서 상대경로로 참조한다** — `theme/fonts/*.woff2` 처럼
|
|
676
|
+
두고 `@font-face { src: url('./fonts/…') }` 로 가리키면 Vite 가 해시해
|
|
677
|
+
`dist/<앱>/assets/` 로 내보내 코어의 `/assets/*` 서빙과 CSP `font-src 'self'` 를
|
|
678
|
+
그대로 통과한다. **Vite `public/` 은 쓸 수 없고**(코어는 `dist/<앱>/assets/*` 만
|
|
679
|
+
서빙 — dist 루트 산출물은 라우트가 없어 404), **`apps/<앱>/static/` 도
|
|
680
|
+
`index.html` 에서 참조하면 안 된다**(런타임은 서빙하지만 `gaon build` 산출
|
|
681
|
+
검증(결정 146)이 "index.html 이 참조하는 에셋이 dist 에 없다" 로 막는다).
|
|
682
|
+
`index.html` 에서 스크립트를 하나 더 실어야 하면 **`<script type="module"
|
|
683
|
+
src="./x.js">`**(번들 대상)로 쓴다 — **인라인 `<script>` 는 기본 CSP
|
|
684
|
+
`script-src 'self'` 가 차단한다**(규칙 8 · 콘솔 위반).
|
|
542
685
|
|
|
543
686
|
## 관련 결정 번호
|
|
544
687
|
|