@gaonjs/cli 0.65.4 → 0.66.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.
@@ -30,10 +30,14 @@ export function controllerScaffold(names, app) {
30
30
  ``,
31
31
  `export default controller({`,
32
32
  ` // GET /${plural} — 목록 페이지 (Inertia render).`,
33
- ` // E-4 체이닝 예시: orderBy · limit · all.`,
33
+ ` // 목록 계약(결정 464): 표준 쿼리 이름을 this.listQuery() 로 읽고, 봉투 하나를`,
34
+ ` // this.lazy() 로 싣는다. 쪽을 넘길 때(부분 리로드) 지명되지 않은 prop 은`,
35
+ ` // 전송도 안 되고 **클로저도 안 돈다** — 목록만 다시 조회된다(결정 462·463).`,
34
36
  ` async index() {`,
35
- ` const items = await ${pascal}.orderBy('createdAt', 'desc').limit(20).all()`,
36
- ` return this.render('${pascal}/Index', { items })`,
37
+ ` const q = this.listQuery()`,
38
+ ` return this.render('${pascal}/Index', {`,
39
+ ` ${plural}: this.lazy(() => ${pascal}.latest().paginate(q.page, q.size)),`,
40
+ ` })`,
37
41
  ` },`,
38
42
  ``,
39
43
  ` // GET /${plural}/count.json — JSON 액션 (errata E-3).`,
@@ -29,6 +29,12 @@ export function pageScaffold(pagePath, app) {
29
29
  // 않는 키가 나와 pageProps 가 해상되지 않는다(gaon g page BlogPosts/EditForm 실측).
30
30
  const routeKey = `${app}:${toCamel(parent)}#${toCamel(last)}`;
31
31
  const filePath = `apps/${app}/pages/${trimmed}.vue`;
32
+ // 결정 465: `<리소스>/Index` 는 **목록 화면**이다 — 짝이 되는 컨트롤러 스캐폴드가
33
+ // 봉투 prop(`this.lazy(() => …paginate())`)을 내므로 여기서 usePagedList 로 받는다.
34
+ // 이 파일이 AI 가 목록을 처음 모방하는 코드라, 정본 패턴이 여기 있어야 한다.
35
+ if (segments.length >= 2 && last === 'Index') {
36
+ return { path: filePath, contents: listPage(routeKey, toCamel(parent)) };
37
+ }
32
38
  const lines = [
33
39
  `<script setup lang="ts">`,
34
40
  `import { pageProps } from 'gaonjs/vue'`,
@@ -48,3 +54,55 @@ export function pageScaffold(pagePath, app) {
48
54
  ];
49
55
  return { path: filePath, contents: lines.join('\n') };
50
56
  }
57
+ /**
58
+ * 목록 화면(`<리소스>/Index`)의 정본 골격 — 서버 페이지네이션 + 부분 리로드(결정 465).
59
+ * `only` 를 손으로 쓰지 않는 것이 핵심이다: 봉투 prop 이름 하나가 곧 요청 이름이라
60
+ * "적어 놨는데 안 도는" 사고도, "4개 중 하나를 빠뜨려 건수만 옛 값" 도 날 수 없다.
61
+ */
62
+ function listPage(routeKey, envelopeKey) {
63
+ return [
64
+ `<script setup lang="ts">`,
65
+ `import { pageProps, usePagedList } from 'gaonjs/vue'`,
66
+ ``,
67
+ `// 컨트롤러 ${routeKey} 의 render props 타입이 그대로 흐른다(§6.2).`,
68
+ `// 라우트 키가 다르면 이 리터럴을 바꾼다 — .gaon/routes.d.ts 가 유효한 키를 알려준다.`,
69
+ `const props = pageProps<'${routeKey}'>()`,
70
+ ``,
71
+ `// 두 번째 인자는 목록 봉투 prop 의 이름이고, 그대로 부분 리로드 대상이 된다 —`,
72
+ `// 쪽을 넘기면 서버는 이 prop 만 다시 조회한다(결정 462·464·465).`,
73
+ `const list = usePagedList(props, '${envelopeKey}')`,
74
+ ``,
75
+ `function goTo(item: number | '…'): void {`,
76
+ ` if (typeof item === 'number') list.page = item`,
77
+ `}`,
78
+ `</script>`,
79
+ ``,
80
+ `<template>`,
81
+ ` <div>`,
82
+ ` <h1>${envelopeKey}</h1>`,
83
+ ``,
84
+ ` <input v-model="list.q" placeholder="검색" />`,
85
+ ` <p>전체 {{ list.total }}건 · {{ list.page }}/{{ list.pageCount }}쪽</p>`,
86
+ ``,
87
+ ` <!-- 행의 실제 컬럼으로 바꾸세요. :key 도 고유 식별자(보통 row.id)로 바꿉니다. -->`,
88
+ ` <ul>`,
89
+ ` <li v-for="(row, i) in list.rows" :key="i"><pre>{{ row }}</pre></li>`,
90
+ ` </ul>`,
91
+ ``,
92
+ ` <!-- 이동은 컴포저블이 소유하고 UI 는 그리기만 한다(결정 25·105·106).`,
93
+ ` 킷 Pagination 을 쓸 땐 <Pagination v-model:page="list.page" :page-count="list.pageCount" /> -->`,
94
+ ` <nav>`,
95
+ ` <button`,
96
+ ` v-for="(item, i) in list.pageItems"`,
97
+ ` :key="i"`,
98
+ ` :disabled="item === '…' || item === list.page || list.loading"`,
99
+ ` @click="goTo(item)"`,
100
+ ` >`,
101
+ ` {{ item }}`,
102
+ ` </button>`,
103
+ ` </nav>`,
104
+ ` </div>`,
105
+ `</template>`,
106
+ ``,
107
+ ].join('\n');
108
+ }
@@ -98,6 +98,66 @@ const props = pageProps<'web:posts#index'>()
98
98
  - **`shared/` 밖에서만 사용** — `shared/` 안 `pageProps` 사용은 §4 대칭 표에서
99
99
  금지 (라우트를 모른다는 순수 규칙).
100
100
 
101
+ ### 1.1 목록 화면 — `usePagedList(props, '<봉투키>')` (결정 462~465)
102
+
103
+ 서버 페이지네이션 목록의 배선(쪽 이동 · URL 동기화 · 검색 디바운스 · 정렬 · 부분
104
+ 리로드)은 컴포저블 하나가 소유한다. 화면이 쓰는 것은 **두 줄**이다.
105
+
106
+ 컨트롤러는 목록 계약(`agents/web.md` §4.5)대로 봉투 prop 하나를 `this.lazy` 로 낸다:
107
+ `products: this.lazy(() => Product.latest().paginate(q.page, q.size))`.
108
+
109
+ ```vue
110
+ <!-- apps/web/pages/Products/Index.vue -->
111
+ <script setup lang="ts">
112
+ import { pageProps, usePagedList } from 'gaonjs/vue'
113
+
114
+ const props = pageProps<'web:products#index'>()
115
+
116
+ // 두 번째 인자 = 목록 봉투 prop 의 이름. 이 이름이 그대로 부분 리로드 대상이 되므로
117
+ // `only` 를 손으로 적을 일이 없다(오타는 컴파일 에러 · 자동완성은 봉투 키만 보여 준다).
118
+ const list = usePagedList(props, 'products')
119
+ </script>
120
+
121
+ <template>
122
+ <div>
123
+ <input v-model="list.q" placeholder="검색" />
124
+ <p>전체 {{ list.total }}건 · {{ list.page }}/{{ list.pageCount }}쪽</p>
125
+
126
+ <ul>
127
+ <li v-for="row in list.rows" :key="row.id">{{ row.name }}</li>
128
+ </ul>
129
+
130
+ <button :disabled="list.loading || list.page >= list.pageCount" @click="list.page = list.page + 1">
131
+ 다음 쪽
132
+ </button>
133
+ </div>
134
+ </template>
135
+ ```
136
+
137
+ | 읽는 값 | 뜻 |
138
+ |---|---|
139
+ | `list.rows` · `list.total` · `list.pageCount` · `list.perPage` | 서버 봉투 그대로(읽기 전용) |
140
+ | `list.loading` | 부분 리로드 진행 중(스켈레톤·버튼 비활성화) |
141
+ | `list.pageItems` | `[1,'…',4,5,6,'…',20]` 쪽 번호 띠 — 직접 만들면 늘 틀리는 자리 |
142
+
143
+ | 쓰는 값 | 일어나는 일 |
144
+ |---|---|
145
+ | `list.page = n` | 그 쪽으로 이동(서버가 클램프한 값이 되돌아온다) |
146
+ | `list.size = n` · `list.filters.<이름> = v` | **1쪽으로 되돌리고** 재조회 |
147
+ | `list.q = '…'` | 320ms 디바운스 후 1쪽으로 재조회 |
148
+ | `list.sort('price')` | 같은 키면 방향 뒤집기, 다른 키면 그 키 오름차순 → 서버 정렬 |
149
+ | `list.reload()` | 지금 조건 그대로 재조회(저장·삭제 뒤) |
150
+
151
+ - **UI 킷은 그대로다.** `Pagination` 은 `v-model:page` 이고 라우트를 모른다(결정 25·105·106) —
152
+ `<Pagination v-model:page="list.page" :page-count="list.pageCount" />` 로 물리면 끝이다.
153
+ **이동은 컴포저블이 소유하고 킷은 그리기만 한다.**
154
+ - **주소가 상태다.** 조건은 쿼리(`?page=2&q=김&sort=price&dir=desc`)에 실려 링크가 공유
155
+ 가능하고, 기본값인 항목은 주소에서 빠진다. 화면이 쓰는 다른 파라미터(`?tab=…`)는 보존된다.
156
+ - **히스토리 기본값은 `replace`** — 쪽 넘김마다 히스토리가 쌓이면 뒤로가기가 "이전 화면" 이
157
+ 아니라 "이전 쪽" 이 된다. 공개 목록에서 쪽마다 주소를 남기려면
158
+ `usePagedList(props, 'products', { history: 'push' })`.
159
+ - 한 화면에 목록이 둘이면 `usePagedList` 를 둘 부른다 — 봉투가 prop 하나씩이라 서로 간섭하지 않는다.
160
+
101
161
  ### 2. API 클라이언트 (`api()`) 호출 (errata E-3 §C)
102
162
 
103
163
  페이지와 무관한 JSON 액션(루트 데이터 경로 판단표 3행)은 `gaonjs/vue` 의 `api()` 로
@@ -202,9 +262,13 @@ import { Product } from '../../../domain/models/Product.js'
202
262
 
203
263
  export default controller({
204
264
  async index() {
205
- const list = await Product.orderBy('createdAt', 'desc').limit(20).all()
265
+ const q = this.listQuery()
266
+ // 목록 계약(§1.1) — 봉투 하나를 this.lazy 로 싣고, 식별자는 그 안에서 String() 으로 못박는다.
206
267
  return this.render('Products/Index', {
207
- products: list.map((p) => ({ id: String(p.id), name: p.name, price: p.price })),
268
+ products: this.lazy(async () => {
269
+ const r = await Product.latest().paginate(q.page, q.size)
270
+ return { ...r, rows: r.rows.map((p) => ({ id: String(p.id), name: p.name, price: p.price })) }
271
+ }),
208
272
  })
209
273
  },
210
274
  })
@@ -413,10 +477,11 @@ import PageShell from '@shared/components/ui/PageShell.vue'
413
477
  주던 행·열 관계가 그리드엔 없어서, `role="grid"/"row"/"columnheader"/"gridcell"` 과
414
478
  `aria-sort` 가 없으면 스크린리더에는 표가 아니라 글자 더미로 읽힌다. `<table>` 은
415
479
  본문 안에 끼우는 짧은 표에만 남긴다.
416
- - **정렬 헤더는 목록 전체가 클라이언트에 있을 때만 연다.** 서버 페이지네이션
417
- (`paginate()`) 목록에서 헤더 정렬을 열면 **현재 쪽만** 정렬돼 "가격 높은 순" 이
418
- 전체가 아니라 그 쪽 안에서만 맞는 거짓말이 된다. 서버 정렬을 붙이기 전까지는
419
- 컬럼의 정렬을 막아 둔다.
480
+ - **정렬 헤더는 서버로 보낸다** (결정 464·465). 서버 페이지네이션(`paginate()`) 목록에서
481
+ **클라이언트 배열만 정렬하면 현재 쪽만** 정렬돼 "가격 높은 순" 이 전체가 아니라 그 쪽
482
+ 안에서만 맞는 거짓말이 된다. 정렬은 목록 계약의 일부다 — 헤더 클릭은 `list.sort('price')`
483
+ (§1.1)로 `sort`·`dir` 쿼리에 실어 서버가 정렬한 쪽을 받는다. 클라 배열
484
+ `.sort()` 는 목록 **전체가 이미 클라에 있을 때만** 쓴다.
420
485
  - **상태 문자열 → 색 매핑은 한 곳에 둔다** — `shared/lib/` 에 `statusTone(status)`
421
486
  같은 함수 하나를 두고 배지가 그것만 쓴다. 화면마다 삼항 연산으로 색을 고르면 같은
422
487
  "미처리" 가 화면에 따라 다른 색이 된다. 모르는 값은 중립색으로 떨어뜨려 화면이
@@ -710,6 +775,10 @@ async function runSearch(q: string) {
710
775
  | 결정 166 | `api()` CSRF 자동 부착 = data-page `props.csrf`(결정 116 과 같은 단일 출처) · `<meta name="csrf-token">` 은 레거시 폴백(§2 · `packages/vue/src/api.ts`) |
711
776
  | 결정 150 | 앱 전역 공유 키 확장 — `app.config` sharedProps 등록 → useShared 로 읽기(코어 3종 고정 · 선언 병합 타입 · hidden 미유출 · `agents/web.md` §4.2) |
712
777
  | 결정 119 | `Pagination` 블록이 `chain.paginate()` 결과에 정합(`:page`·`:pageCount` 필드 그대로 · 매핑 0 · `agents/data.md`) |
778
+ | 결정 462 | Inertia 부분 리로드 서버 지원 — `router.reload/get` 의 `only` 가 **비로소 실제로 동작**한다(종전엔 서버가 헤더를 무시해 무동작) · 빠진 prop 은 클라가 이전 값으로 병합(`agents/web.md` §4.5) |
779
+ | 결정 463 | `this.lazy`/`this.optional` — 부분 리로드에서 지명 안 된 prop 은 전송도 평가도 안 된다 · `optional` 은 페이지 타입이 `T \| undefined`(`agents/web.md` §4.5) |
780
+ | 결정 464 | 목록 계약 — 봉투 prop 하나 + 표준 쿼리 이름 8종(`page`·`size`·`q`·`searchType`·`from`·`to`·`sort`·`dir`) · 클라/서버가 같은 이름을 쓴다(§1.1) |
781
+ | 결정 465 | `usePagedList(props, '<봉투키>')` — 이동·URL·디바운스·정렬·`only` 를 컴포저블이 소유 · 킷 `Pagination` 은 `v-model:page` 그대로(결정 106 무변경) · **서버 정렬이 계약에 들어와 §8 의 «정렬 헤더는 클라에 전체가 있을 때만» 규칙이 바뀌었다**(§1.1·§8) |
713
782
  | 결정 128 | `useChannel` 자동 재연결(지수 백오프 1s·2s·5s·10s·지터 · `onReconnect` 로 놓친 데이터 따라잡기 · 미인가 4401 은 재연결 안 함 · `agents/realtime.md` §4) |
714
783
  | 결정 198 | 클라 환경변수 접근자 `env`(gaonjs/vue · `.vue` 의 import.meta.env TS1470 회피) · VITE_* 접두만 노출·접두 제거 · `.gaon/env.d.ts`(.env 스캔) 타입 브리지 · doctor no-import-meta-env(§9) |
715
784
  | 결정 206 | UI 킷 §8 슬롯·props 요약표(카탈로그가 이름만이라 소스 열람 유발 · O-2 해소) · named slot 비대칭 명시(PageHeader `#actions` 복수 vs EmptyState `#action` 단수) |
@@ -431,10 +431,7 @@ shared.locale // 로그인/로그아웃·플래시로
431
431
  // ❌ 컨트롤러가 검색 교집합·태그 필터를 인라인 조립
432
432
  // ✅ const rows = await Post.searchPublished(term).latest().offset(o).limit(n).all()
433
433
  // ✅ 페이지네이션은 스코프 체인 종단 paginate — 컨트롤러는 여전히 한 줄(결정 119)
434
- // (결정 294: this.query('page') 같은 단일 접근 표면은 없다 애드혹 폼으로 받는다)
435
- // const { page } = this.query({ _row: {} as { page?: string } })
436
- // const result = await Post.searchPublished(term).latest().paginate(Number(page ?? 1), 20)
437
- // return this.render('Posts/Index', { page: result }) // 통째로 안전(rows Serialized · 나머지 number)
434
+ // 목록 액션의 정본 형태는 §4.5(목록 계약) 따른다 표준 쿼리 + 봉투 하나 + this.lazy.
438
435
  ```
439
436
 
440
437
  ### 4.4 클라이언트 IP · 헤더는 `this.request` (FastifyRequest 탈출구 · 결정 120)
@@ -457,6 +454,60 @@ shared.locale // 로그인/로그아웃·플래시로
457
454
  위협 모델 · `compose.prod.yaml`).
458
455
  - 잘못된 설정(빈 헤더 이름 등)은 **부팅 에러**로 즉시 잡힌다(→ 수리 안내 포함).
459
456
 
457
+ ### 4.5 목록 화면 — 부분 리로드 · `this.lazy` · `this.listQuery` (결정 462~465)
458
+
459
+ 목록 화면에서 **2쪽으로 넘어갈 때 다시 조회돼야 하는 것은 목록뿐**이다. 게시판 트리·
460
+ 셀렉트 후보·통계 카드까지 매번 다시 뽑으면 쪽 이동이 첫 로드만큼 무거워진다.
461
+ Inertia 프로토콜의 **부분 리로드**가 그 자리인데, 서버가 무엇을 게을리 할지 알아야
462
+ 성립한다 — 그 선언이 `this.lazy` 다.
463
+
464
+ **세 가지 선언, 세 가지 뜻:**
465
+
466
+ | 선언 | 값 평가 | 첫 로드 | 쪽 이동(미지명) | 이름으로 요청됨 | 페이지 타입 |
467
+ |---|---|---|---|---|---|
468
+ | `posts` (평범한 값) | 즉시(항상) | 포함 | **포함**(재계산·재전송) | 포함 | `T` |
469
+ | `this.lazy(() => …)` | 포함될 때만 | 포함 | **생략 + 클로저 미호출** | 포함 | `T` |
470
+ | `this.optional(() => …)` | 포함될 때만 | **미포함** | 미포함 | 포함 | `T \| undefined` |
471
+
472
+ - **`lazy` = 평가를 미룬다.** 첫 화면엔 늘 있으므로 페이지 타입이 `T` 로 유지된다
473
+ (옵셔널 체이닝이 안 늘어난다). **목록 화면의 정답**이다.
474
+ - **`optional` = 기본으로 안 보낸다.** "버튼을 눌러야 오는 상세·통계" 용. 타입이
475
+ `T | undefined` 라 그 성질이 에디터에서 바로 보인다.
476
+ - async 클로저는 1급이다(`paginate()` 가 async). 여러 lazy 는 **동시에** 평가된다.
477
+
478
+ **목록 계약** — 봉투는 **prop 하나**(`{ rows, total, page, pageCount, perPage }` =
479
+ `chain.paginate()` 반환 그대로)다. 4개 최상위 prop 으로 펴지 않는다: 이름이 하나라야
480
+ 클라가 `only` 를 틀릴 수 없고(하나 빠뜨리면 쪽만 바뀌고 건수는 옛 값), 한 화면에
481
+ 목록 둘을 놓을 수 있다.
482
+
483
+ **쿼리 이름은 고정**이다 — `page`·`size`·`q`·`searchType`·`from`·`to`·`sort`·`dir`.
484
+ `this.listQuery()` 가 정수화·클램프(`page ≥ 1` · `1 ≤ size ≤ 100`)까지 해서 읽고,
485
+ 클라 `usePagedList` 가 **같은 이름으로** 싣는다. where/order 조립(의미)은 앱이 한다.
486
+
487
+ ```ts controller-action
488
+ // apps/web/controllers/posts.ts — 쪽을 넘길 때 authors·stats 는 쿼리 자체가 안 나간다
489
+ async index() {
490
+ const q = this.listQuery()
491
+ return this.render('Posts/Index', {
492
+ posts: this.lazy(() => Post.latest().paginate(q.page, q.size)),
493
+ authors: this.lazy(() => User.select(['id', 'name']).limit(500).all()),
494
+ // 첫 로드엔 안 온다 — 화면이 이름으로 요청할 때만(결정 463).
495
+ stats: this.optional(() => Post.count()),
496
+ })
497
+ },
498
+ ```
499
+
500
+ **경계 규칙(외우지 말고 알아만 둘 것):**
501
+
502
+ - 클라가 다른 페이지로 이동 중이면(`X-Inertia-Partial-Component` 불일치) 서버는
503
+ 필터를 **무시하고 전량**을 보낸다 — 반쪽 페이지가 안 생긴다.
504
+ - 부분 응답에서 빠진 prop 은 클라가 **이전 값으로 채운다**(Inertia 프로토콜). 그래서
505
+ 서버는 빼기만 하고, 무엇을 빼도 화면이 안 깨진다.
506
+ - 단 **`flash`·`errors` 는 항상 실린다** — 세션에서 한 번 읽고 지워지는 소비성 값이라
507
+ 빼면 그대로 유실된다("저장했습니다" 가 영영 안 뜬다).
508
+ - **`this.lazy` 는 JSON 액션·`this.json` 에 쓸 수 없다**(게으를 이유가 없는 경로다) —
509
+ 쓰면 응답 경계에서 수리 안내와 함께 throw 한다. 값을 그대로 `await` 해서 넘긴다.
510
+
460
511
  ### 5. 비밀번호 해싱 — `hashPassword` · `verifyPassword` (`gaonjs/web`)
461
512
 
462
513
  회원가입·로그인에서 비밀번호를 다룰 때는 **직접 crypto/bcrypt 를 import 하거나
@@ -788,6 +839,10 @@ export default controller({
788
839
  | 결정 117 | render props 에 예약 공유 키 = 컴파일 에러 + 런타임 방어(자동 주입값 조용한 덮어쓰기 금지 · §4.2) |
789
840
  | 결정 150 | 앱 전역 공유 키 확장 — `app.config` sharedProps → 모든 렌더 자동 주입 · 코어 3종 예약(덮으면 throw) · 선언 병합 타입 · hidden 미유출 · useShared 로 읽기(§4.2) |
790
841
  | 결정 119 | 목록 액션 페이지네이션 = `chain.paginate(page, perPage)` 종단(§4.3 · `agents/data.md`) · 손 조립 반정본 · result 통째로 render props 안전 |
842
+ | 결정 462 | Inertia **부분 리로드** 서버 지원 — `X-Inertia-Partial-Data`/`-Except` 를 존중해 요청된 prop 만 전송 · `-Component` 불일치면 전량 · **소비성 prop(flash·errors)만 항상 포함** · 버전 불일치 409 는 필터보다 먼저(§4.5) |
843
+ | 결정 463 | 평가 시점/포함 여부 분리 — `this.lazy(fn)`(첫 로드 포함 · 미지명 시 생략 + **클로저 미호출**) · `this.optional(fn)`(기본 미포함 · `T \| undefined`) · **원시 함수 prop 금지**(컴파일 에러 + 런타임 수리 안내 — 종전엔 조용히 사라졌다) · JSON 경계에 새면 throw(§4.5) |
844
+ | 결정 464 | 목록 계약 — 봉투 **prop 하나**(`{rows,total,page,pageCount,perPage}`) · 표준 쿼리 이름 8종 · `this.listQuery()`(정수화·클램프 `size ≤ 100`)(§4.5) |
845
+ | 결정 465 | 클라 목록 표면 = `usePagedList(props, '<봉투키>')`(`gaonjs/vue`) — `only`·URL·디바운스·정렬을 컴포저블이 소유 · 킷 `Pagination` 은 `v-model:page` 그대로(결정 106 무변경 · `agents/frontend.md`) |
791
846
  | 결정 120 | 클라이언트 IP = `this.request.ip`(별도 표면 없음) · `web.clientIp` direct/proxy/header 로 rate limit·로깅과 같은 산출 배선(§4.4 · `agents/security.md`) |
792
847
  | 결정 133 | 멀티파트 업로드(`this.file()`) CSRF 검사는 `x-csrf-token` 헤더로만 — 바디 `_csrf` 는 스트리밍 파싱이라 검사 시점에 없다(§3 · 헤더 부재 시 403 + 수리 안내 · 부착은 결정 342 로 자동화) |
793
848
  | 결정 342 | CSRF 부착 The One Way — `useForm`/`router` 상태 변경 visit 에 프레임웍이 `X-CSRF-Token` 자동 부착(라이브 페이지 props 출처 · 결정 341 · api() 와 단일 출처) · 수동 `_csrf` 바디/헤더 보일러플레이트 제거(스캐폴드 동기) · 명시 헤더는 존중(탈출구) |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gaonjs/cli",
3
- "version": "0.65.4",
3
+ "version": "0.66.0",
4
4
  "description": "Gaon CLI — 스캐폴딩·제너레이터·마이그레이션·dev/serve/work/hub·doctor·check (bin: gaon)",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -32,13 +32,13 @@
32
32
  "@modelcontextprotocol/sdk": "^1.29.0",
33
33
  "typescript": "^5.9.0",
34
34
  "vite": "^7.0.0",
35
- "@gaonjs/config": "0.26.2",
36
35
  "@gaonjs/async": "0.22.0",
37
- "@gaonjs/i18n": "0.5.0",
36
+ "@gaonjs/config": "0.27.0",
38
37
  "@gaonjs/core": "0.3.0",
39
- "@gaonjs/mail": "0.5.4",
38
+ "@gaonjs/i18n": "0.5.0",
40
39
  "@gaonjs/data": "0.26.4",
41
- "@gaonjs/web": "0.33.0"
40
+ "@gaonjs/mail": "0.5.4",
41
+ "@gaonjs/web": "0.34.0"
42
42
  },
43
43
  "scripts": {
44
44
  "build": "node ../../node_modules/typescript/bin/tsc -p tsconfig.json && node -e \"const fs=require('fs');fs.rmSync('dist/templates/project',{recursive:true,force:true});fs.cpSync('src/templates','dist/templates',{recursive:true,filter:(s)=>!s.endsWith('.ts')});fs.rmSync('dist/templates/index.ts',{force:true})\""