@gaonjs/cli 0.21.2 → 0.24.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/README.md +10 -3
- package/dist/commands/db.js +2 -2
- package/dist/db.d.ts +7 -2
- package/dist/db.js +30 -17
- package/dist/doctor/async-offload.d.ts +23 -0
- package/dist/doctor/async-offload.js +236 -0
- package/dist/doctor/csrf-wiring.d.ts +5 -0
- package/dist/doctor/csrf-wiring.js +72 -0
- package/dist/doctor/internal-anchor.d.ts +6 -0
- package/dist/doctor/internal-anchor.js +122 -0
- package/dist/doctor/pageprops-destructure.d.ts +5 -0
- package/dist/doctor/pageprops-destructure.js +84 -0
- package/dist/doctor/types.d.ts +1 -1
- package/dist/doctor/types.js +2 -4
- package/dist/doctor.d.ts +4 -0
- package/dist/doctor.js +22 -2
- package/dist/generate.d.ts +8 -0
- package/dist/generate.js +51 -7
- package/dist/index.d.ts +6 -2
- package/dist/index.js +27 -21
- package/dist/serve.js +7 -0
- package/dist/templates/auth/Dashboard.vue.tpl +5 -4
- package/dist/templates/auth/Login.vue.tpl +9 -7
- package/dist/templates/auth/Signup.vue.tpl +7 -6
- package/dist/templates/project/AGENTS.md.tpl +33 -4
- package/dist/templates/project/agents/async.md.tpl +55 -0
- package/dist/templates/project/agents/frontend.md.tpl +21 -1
- package/dist/templates/project/agents/security.md.tpl +7 -0
- package/dist/templates/project/agents/web.md.tpl +47 -4
- package/dist/templates/project/apps/web/app.config.ts.tpl +14 -0
- package/dist/templates/project/apps/web/layouts/Default.vue.tpl +6 -2
- package/package.json +7 -7
|
@@ -5,6 +5,54 @@
|
|
|
5
5
|
> 대상 패키지: `@gaonjs/async` (파사드 import 는 `gaonjs/async`). 백본 = NATS JetStream ·
|
|
6
6
|
> 실행은 워커 프로세스(`gaon work`).
|
|
7
7
|
|
|
8
|
+
## 비동기 배치의 One Way 판단표 (결정 102)
|
|
9
|
+
|
|
10
|
+
`agents/data.md` §5.3 / `AGENTS.md` §3.1 이 **서버 로직을 어느 층**(모델·서비스·
|
|
11
|
+
컨트롤러)에 둘지 정한다면, 이 표는 그 로직을 **동기(요청 인라인)로 둘지 비동기
|
|
12
|
+
(잡·이벤트·스케줄)로 뺄지**를 정한다 — 같은 "정답이 하나" 원칙의 비동기 축이다.
|
|
13
|
+
|
|
14
|
+
| 상황 | 배치 | 이유 |
|
|
15
|
+
|---|---|---|
|
|
16
|
+
| 응답에 결과가 **필요** | 요청 인라인 | 사용자가 그 값을 기다린다 (조회·검증·생성 결과) |
|
|
17
|
+
| 응답 불필요 · 실패 시 재시도 (메일·알림·웹훅) | **잡** (`domain/jobs/`) | 응답 지연 제거 + 재시도 배터리(백오프·DLQ) |
|
|
18
|
+
| 느리거나 불안정한 **외부 API** 호출 | **잡** | 이벤트 루프·응답 시간 보호 (외부 지연이 응답에 새지 않음) |
|
|
19
|
+
| **CPU 무거운** 작업 (이미지 리사이즈·리포트·대량 연산) | **잡** | 루프 블로킹 = 그 사이 이웃 요청 전부 정지 |
|
|
20
|
+
| 트랜잭션 커밋에 딸린 **파생 효과** | **이벤트 + 아웃박스** (`afterCommit`) | DB 확정과 정합 (롤백 시 발행도 취소 · §4) |
|
|
21
|
+
| **주기** 작업 (정리·집계·랭킹) | **`domain/schedule.ts`** | 요청과 무관한 시간축 · 리더 1인만 실행 |
|
|
22
|
+
|
|
23
|
+
**실무 신호**: 요청 처리 중 **수백 ms 이상** 걸리는 일은 잡 후보다. Node 는 싱글
|
|
24
|
+
스레드라 무거운 작업 하나가 워커 앞에 이웃 요청을 전부 세운다(head-of-line
|
|
25
|
+
blocking). 확신이 안 서면 "응답에 이 결과가 필요한가?" 만 묻는다 — 아니면 잡이다.
|
|
26
|
+
|
|
27
|
+
**"잡으로 뺀다 = work 프로세스로 옮긴다".** 운영 프로세스는 3종(§6): `gaon serve`
|
|
28
|
+
(웹 요청) · `gaon work`(잡·리스너·스케줄·아웃박스 릴레이) · `gaon hub`(실시간).
|
|
29
|
+
인라인 코드는 `serve` 안에서 요청 스레드로 돈다. `.later()`·`afterCommit`·
|
|
30
|
+
`schedule.ts` 로 뺀 일은 **`work` 프로세스**가 별도로 처리하므로 웹 응답이 그
|
|
31
|
+
무게를 지지 않는다. 그래서 "느리다/무겁다/실패해도 재시도" 는 전부 `work` 행(行)이다.
|
|
32
|
+
|
|
33
|
+
각 배치의 정본 예시 (전체 시그니처·옵션은 아래 §1·§4·§5):
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
// 잡 — 응답과 분리해 발행 (메일·알림·이미지 처리 등). 컨트롤러/서비스에서:
|
|
37
|
+
await SendWelcomeMail.later(user.id) // domain/jobs/sendWelcomeMail.ts (§1)
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
// 파생 효과 — 커밋 뒤에만 나가야 하는 발행은 서비스 afterCommit (§4 아웃박스).
|
|
42
|
+
export const RegisterUser = service(async (input: RegisterInput) => {
|
|
43
|
+
const user = await User.create(input)
|
|
44
|
+
afterCommit(() => ResizeAvatar.later(user.id)) // 커밋 성공 후에만 발행
|
|
45
|
+
return user
|
|
46
|
+
})
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
// 주기 작업 — domain/schedule.ts · 대상은 항상 잡 (§5 · 인라인 함수 금지).
|
|
51
|
+
export default schedule((s) => {
|
|
52
|
+
s.cron('0 9 * * 1', SendWeeklyReport) // 매주 월 09:00 · 리더 1인만
|
|
53
|
+
})
|
|
54
|
+
```
|
|
55
|
+
|
|
8
56
|
## 정본 규칙
|
|
9
57
|
|
|
10
58
|
### 1. 잡 (`job()`) (`packages/async/src/jobs.ts:146-185`)
|
|
@@ -209,6 +257,11 @@ async create() {
|
|
|
209
257
|
|
|
210
258
|
- **컨트롤러에서 메일·외부 발송 직접 호출 = 함정** — 컨트롤러는 잡
|
|
211
259
|
발행만. nodemailer·resend·@sendgrid/mail 직접 import 금지.
|
|
260
|
+
- **무거운/느린 작업을 요청 인라인에 두면 함정** (판단표 · 결정 102) —
|
|
261
|
+
이미지 리사이즈(sharp·jimp)·외부 API 호출을 컨트롤러 액션 안에서 동기로
|
|
262
|
+
하면 응답이 그 무게를 진다. `domain/jobs/` 잡으로 빼고 `.later()` 로 발행한다.
|
|
263
|
+
`gaon doctor` 의 `async-offload` 가 이 인라인 패턴을 경고로 잡는다(결정 103 ·
|
|
264
|
+
JSON/API 앱의 정당한 외부 호출은 오탐 방지로 제외).
|
|
212
265
|
- **클래스형 잡·데코레이터(`@Job`·`@Processor`) 금지** — `job()` 함수형만.
|
|
213
266
|
- **잡 파일 위치는 `domain/jobs/`** — 앱 폴더가 아니다 (잡은 도메인
|
|
214
267
|
소속 · 어느 앱에서든 큐잉).
|
|
@@ -225,6 +278,8 @@ async create() {
|
|
|
225
278
|
|
|
226
279
|
| 결정 | 내용 |
|
|
227
280
|
|---|---|
|
|
281
|
+
| 결정 102 | 비동기 배치 One Way 판단표 (동기 인라인 vs 잡/이벤트/스케줄 · 서두 표) |
|
|
282
|
+
| 결정 103 | doctor `async-offload` 검사 (컨트롤러 인라인 메일·이미지·외부 HTTP 경고) |
|
|
228
283
|
| 결정 32 | 잡 발행 위치 자유 (컨트롤러·서비스·리스너 모두 정합) |
|
|
229
284
|
| 결정 42 | 비동기 테스트 헬퍼 `expectJobProcessed` (`agents/testing.md`) |
|
|
230
285
|
| §7 | 비동기 배터리 원문 (백오프 기본값 = M7 벤치마크 확정) |
|
|
@@ -31,6 +31,13 @@ const props = pageProps<'web:posts#index'>()
|
|
|
31
31
|
</template>
|
|
32
32
|
```
|
|
33
33
|
|
|
34
|
+
- **변수로 받아 `props.x` 로 접근 — 구조분해 금지** (결정 99). `pageProps()` 는
|
|
35
|
+
반응형 프록시라, 같은 페이지로의 리다이렉트/리로드로 서버가 새 props 를 주면
|
|
36
|
+
(댓글 작성·삭제 등) 재마운트 없이 화면이 즉시 갱신된다. 하지만
|
|
37
|
+
`const { posts } = pageProps<...>()` 처럼 구조분해하면 그 순간 값을 한 번
|
|
38
|
+
읽어 **반응성이 끊긴다**(Vue `defineProps` 를 구조분해하면 안 되는 것과 같은
|
|
39
|
+
함정). 항상 `const props = pageProps<...>()` 로 받아 `props.posts` 로 쓴다.
|
|
40
|
+
구조분해는 doctor **pageprops-destructure** 가 잡는다(경고).
|
|
34
41
|
- **라우트 키** = `<app>:<controller>#<action>` (앱 폴더명 · 컨트롤러 파일명
|
|
35
42
|
stem · 소문자 복수 · 결정 55). `apps/web/controllers/posts.ts` 의 `index`
|
|
36
43
|
액션 → `'web:posts#index'`.
|
|
@@ -42,6 +49,13 @@ const props = pageProps<'web:posts#index'>()
|
|
|
42
49
|
전부 없음 · CLAUDE.md 규칙 12). 페이지 전환은 `gaonjs/vue` 의 `router`
|
|
43
50
|
(`router.visit(url)`·`router.get/post/delete`), 폼은 `useForm(...)` (결정 64).
|
|
44
51
|
라우트 정의는 서버의 `apps/<앱>/routes.ts` 뿐이다.
|
|
52
|
+
- **앱 내부 이동 = `Link`(선언적) 또는 `router.visit`(프로그램적) — 내부 경로
|
|
53
|
+
일반 `<a>` 금지** (결정 96). 내부 경로로 가는 `<a href="/...">` 는 클릭마다
|
|
54
|
+
전체 문서를 다시 로드해 Inertia SPA 상태(스크롤·폼 입력·채널 구독)가 초기화된다
|
|
55
|
+
(첫 실사용 블로그에서 "메인 경로" 풀 리로드로 관측). 선언적 링크는 `gaonjs/vue`
|
|
56
|
+
의 `Link`(`<Link href="/posts">글 목록</Link>`), 코드에서의 이동은
|
|
57
|
+
`router.visit(url)`. **외부 URL(`https://…`)·`target="_blank"` 만 `<a>`** 를
|
|
58
|
+
유지한다. 내부 경로 일반 앵커는 doctor **internal-anchor** 가 잡는다(결정 96).
|
|
45
59
|
- **`shared/` 밖에서만 사용** — `shared/` 안 `pageProps` 사용은 §4 대칭 표에서
|
|
46
60
|
금지 (라우트를 모른다는 순수 규칙).
|
|
47
61
|
|
|
@@ -175,7 +189,8 @@ import 한다. `gaon doctor` 의 **no-auto-import** 검사가 자동 import
|
|
|
175
189
|
scoped-CSS 미편입 방침은 결정 74 로 뒤집혔다).
|
|
176
190
|
- **auth 링크는 수동(결정 70)** — `gaon g auth` 는 auth 페이지·라우트만 신설하고
|
|
177
191
|
랜딩·레이아웃 nav 를 편집하지 않는다. 헤더에 로그인 링크를 두려면 `Default.vue`
|
|
178
|
-
의 nav 에 `<
|
|
192
|
+
의 nav 에 `<Link href="/session/new">로그인</Link>` 을 직접 추가한다(내부 이동은
|
|
193
|
+
`Link` · 일반 `<a>` 는 풀 리로드 · 결정 96).
|
|
179
194
|
- **auth 페이지 경로 = `pages/Auth/`(PascalCase)** — `gaon g auth` 는 `Auth/Login.vue`
|
|
180
195
|
·`Auth/Signup.vue` 를 내고 컨트롤러는 `this.render('Auth/Login')` 로 부른다.
|
|
181
196
|
소문자 `auth/` 는 doctor page-filename 이 잡는다(결정 32·46).
|
|
@@ -265,6 +280,10 @@ async function runSearch(q: string) {
|
|
|
265
280
|
shared-composable-purity 위반. 데이터는 props/인자로.
|
|
266
281
|
- **Vue 페이지에서 `fetch()` 로 폼 구현 금지** — 세션 앱 폼은
|
|
267
282
|
`gaonjs/vue` 의 `useForm(...).post()` (`agents/web.md` §4 · 결정 64).
|
|
283
|
+
- **내부 경로 일반 `<a href="/...">` 금지** — 클릭마다 전체 문서를 다시
|
|
284
|
+
로드해 SPA 상태가 초기화된다. 앱 내부 이동은 `gaonjs/vue` 의 `Link`
|
|
285
|
+
(`<Link href="/...">`) 또는 `router.visit(...)`, 외부 URL·`target="_blank"`
|
|
286
|
+
만 `<a>` (결정 96 · doctor **internal-anchor**).
|
|
268
287
|
- **`vue-router` import 금지** — Gaon 은 클라이언트 라우터가 없다(Inertia =
|
|
269
288
|
SPA + 서버 라우팅). `import { useRouter } from 'vue-router'` 는 존재하지 않는
|
|
270
289
|
의존을 끌어와 컴파일 실패한다 — 전환은 `gaonjs/vue` 의 `router`, 라우트 정의는
|
|
@@ -300,4 +319,5 @@ async function runSearch(q: string) {
|
|
|
300
319
|
| 결정 74 | Tailwind 스캐폴드 편입(tailwind.config.ts·postcss.config.js·style.css · 결정 69 scoped-CSS 방침 뒤집기) |
|
|
301
320
|
| 결정 75 | shadcn 식 UI 킷(`gaon g ui-kit` · 복사-소유 · Vue 3 신작 · 외부 런타임 의존 0) |
|
|
302
321
|
| 결정 76 | 멀티앱 UI 킷 배선 자동화(`g app`·`g ui-kit --app` 이 앱별 Tailwind 배선 동봉·멱등 보정 · doctor ui-kit-wiring) |
|
|
322
|
+
| 결정 96 | 앱 내부 이동 = `Link`(선언적)/`router.visit`(프로그램적) · 내부 경로 일반 `<a>` 금지(풀 리로드) · `Link` 재수출 · doctor internal-anchor |
|
|
303
323
|
| E-3 §C | 타입드 `api()` 클라이언트 (routes.d.ts 브리지 재사용) |
|
|
@@ -30,6 +30,12 @@
|
|
|
30
30
|
- CSRF: 세션 앱은 상태 변경 메서드(POST/PUT/PATCH/DELETE)에 CSRF
|
|
31
31
|
강제. `api()` 클라이언트는 `<meta name="csrf-token">` 을 자동으로
|
|
32
32
|
읽어 `X-CSRF-Token` 헤더에 붙인다 (`packages/vue/src/api.ts:184`).
|
|
33
|
+
- **CSRF 는 세션 위에 얹힌다 — 세션이 없으면 CSRF 도 없다 (결정 93).**
|
|
34
|
+
세션이 있어야 토큰을 저장·검증할 곳이 생긴다. `gaon new` 기본 web 앱은
|
|
35
|
+
`app.config.ts` 에 세션을 **기본 배선**해 규칙 8(기본 켬)이 실태가 되게
|
|
36
|
+
한다 — 폼(POST)을 추가하는 순간 CSRF 가 이미 켜져 있다. 앱에 비-GET
|
|
37
|
+
라우트가 있는데 `app.config.ts` 에 session 이 없으면 `gaon doctor` 의
|
|
38
|
+
`csrf-wiring` 이 경고한다(JWT/API 앱은 토큰 인증이라 CSRF 대상 제외).
|
|
33
39
|
- JWT 는 API 앱 전용 옵션. 세션 쿠키가 기본 (v0.15 §7 · v0.11 확정).
|
|
34
40
|
|
|
35
41
|
### 3. 시크릿
|
|
@@ -99,3 +105,4 @@ const rows = await Post.query()
|
|
|
99
105
|
| §2.5.1 (v0.15) | 보안 기본값 fail-closed (CORS·rate limit·CSRF 기본 켬) |
|
|
100
106
|
| 결정 24 (E-3 §5) | `this.params` 고정 우선순위 — 파라미터 오염 차단 |
|
|
101
107
|
| §7 (v0.15) | 세션 앱별 분리 · JWT 는 API 앱 전용 |
|
|
108
|
+
| 결정 93 (W2) | 기본 web 앱 세션 기본 배선 = CSRF 기본 켬 실태 · doctor `csrf-wiring` 경고 |
|
|
@@ -100,6 +100,47 @@ export default controller({
|
|
|
100
100
|
- 기본 경로는 여전히 `this.params` 하나(The One Way) — 스캐폴드·문서·
|
|
101
101
|
기본 예시는 `this.params` 만 쓴다 (E-3 §5.3 원문).
|
|
102
102
|
|
|
103
|
+
**폼 모양 두 가지 — 스키마 파생 vs 애드혹 (라우트 파라미터는 둘 다 자동 병합 · 결정 95):**
|
|
104
|
+
|
|
105
|
+
라우트 파라미터·body·query 병합은 폼 모양과 무관하게 항상 일어난다 —
|
|
106
|
+
`this.params(...)` 에 넘기는 폼은 **결과 타입**만 정한다. 그래서 `/posts/:id`
|
|
107
|
+
같은 라우트 파라미터와 폼 필드를 **한 번의 `this.params` 로 함께** 받는다.
|
|
108
|
+
|
|
109
|
+
- **① 스키마 파생 폼 `this.params(Model.form)`** — 정본. 모델 컬럼 타입으로
|
|
110
|
+
런타임 강제 변환·검증(대량 할당 차단)까지 한다. 라우트 파라미터는 **키 이름이
|
|
111
|
+
컬럼과 같으면** 자동으로 그 자리에 들어간다. 그래서 라우트를 컬럼명에 맞춰
|
|
112
|
+
짓는 게 관례다:
|
|
113
|
+
|
|
114
|
+
```ts
|
|
115
|
+
// routes: r.post('/posts/:postId/comments', 'comments#create')
|
|
116
|
+
// 스키마 comments 에 postId·author·body 컬럼이 있으면:
|
|
117
|
+
async create() {
|
|
118
|
+
// :postId 는 라우트에서, author·body 는 body 에서 — 한 번에 검증까지.
|
|
119
|
+
const data = this.params(Comment.form) // { postId, author, body, ... } (검증됨)
|
|
120
|
+
const comment = await Comment.create(data)
|
|
121
|
+
return this.redirect(`/posts/${String(data.postId)}`)
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
- **② 애드혹 폼 `this.params({ _row: {} as { ... } })`** — 전용 모델이 없거나
|
|
126
|
+
받을 필드를 **정확히** 고를 때. 라우트 파라미터·폼 필드를 자유롭게 섞어 타입을
|
|
127
|
+
못박는다. 단 **런타임 스키마 검증은 없다**(타입만 · 컬럼 정의가 없어 coerce
|
|
128
|
+
스킵) — 필요하면 값 검사를 직접 하거나 ①로 간다:
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
// routes: r.post('/posts/:id/comments', 'comments#create')
|
|
132
|
+
async create() {
|
|
133
|
+
// :id(라우트) + author·body(폼) 를 한 폼 모양으로.
|
|
134
|
+
const { id, author, body } = this.params({
|
|
135
|
+
_row: {} as { id: string; author: string; body: string },
|
|
136
|
+
})
|
|
137
|
+
// ...
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
①이 검증까지 주므로 **모델이 있으면 ①을 먼저 고른다**. ②는 로그인 폼처럼
|
|
142
|
+
전용 테이블이 없는 입력의 탈출구다.
|
|
143
|
+
|
|
103
144
|
### 4. 데이터 경로 판단 — 루트 판단표가 정본
|
|
104
145
|
|
|
105
146
|
데이터가 필요할 때는 루트 `AGENTS.md` 의 데이터 경로 4종 판단표를 따른다
|
|
@@ -114,12 +155,13 @@ redirect 로 처리한다 — 전체 페이지 리로드도, 별도 REST 엔드
|
|
|
114
155
|
|
|
115
156
|
```ts
|
|
116
157
|
// 로그인 폼 — 제출은 Inertia SPA 방식, 서버는 redirect 로 답한다.
|
|
117
|
-
|
|
118
|
-
const
|
|
119
|
-
|
|
158
|
+
// pageProps 는 반응형 — 변수로 받아 props.x 로 접근한다(구조분해 금지 · 결정 99).
|
|
159
|
+
const props = pageProps<'web:session#new'>()
|
|
160
|
+
const form = useForm({ email: '', password: '', _csrf: props.csrf })
|
|
161
|
+
// <form @submit.prevent="form.post('/session')"> · 실패 시 {{ props.error }} 가 반응형으로 갱신
|
|
120
162
|
|
|
121
163
|
// HTML <form> 이 못 보내는 메서드(DELETE 등)는 router 로 보낸다.
|
|
122
|
-
router.delete('/session', { headers: { 'x-csrf-token': csrf } })
|
|
164
|
+
router.delete('/session', { headers: { 'x-csrf-token': props.csrf } })
|
|
123
165
|
```
|
|
124
166
|
|
|
125
167
|
`?_method=DELETE` 같은 우회는 **서버가 해석하지 않는다** — POST 로 나가
|
|
@@ -290,4 +332,5 @@ export default controller({
|
|
|
290
332
|
| 결정 32 | 잡 발행 위치 자유 (컨트롤러·서비스·리스너 — `agents/async.md`) |
|
|
291
333
|
| 결정 37 | bigint PK 컨트롤러 `String()` 정규화 (`agents/frontend.md`) |
|
|
292
334
|
| 결정 59 | 인증 배선 = `app.config.ts` 의 `session`+`auth(loadUser)` — 없으면 currentUser 영구 null |
|
|
335
|
+
| 결정 95 (W4) | 폼 모양 2종 — 스키마 파생 `Model.form`(검증) vs 애드혹 `{ _row }`(타입만) · 라우트 파라미터는 둘 다 자동 병합 |
|
|
293
336
|
| E-1 | 파사드 = `gaonjs` · CLI = `gaon` |
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// 앱 설정 — apps/web/app.config.ts. 보안 기본값(세션 + CSRF)을 표준 부팅
|
|
2
|
+
// (gaon dev / gaon serve)에 배선한다. 잘 지은 app.config 는 거의 비어 있어야
|
|
3
|
+
// 하지만(§1.1), 세션은 CSRF 토큰 저장소의 토대라 기본으로 켠다.
|
|
4
|
+
//
|
|
5
|
+
// 규칙 8(§2.5.1): CORS·rate limit·CSRF 는 코어에서 **기본 켬** — 끄는 것은
|
|
6
|
+
// 명시적 설정으로만. 세션이 있어야 상태 변경 요청(POST/PUT/PATCH/DELETE)에
|
|
7
|
+
// CSRF 토큰 검증이 걸린다. 세션 스토어는 루트 gaon.config.ts 의 redis(REDIS_URL)
|
|
8
|
+
// 를 wireGaon 이 주입한다(결정 93).
|
|
9
|
+
import { defineAppConfig } from 'gaonjs/config'
|
|
10
|
+
|
|
11
|
+
export default defineAppConfig({
|
|
12
|
+
// secret 은 32자 이상 — .env 의 SESSION_SECRET 로 주입한다(운영은 반드시 교체).
|
|
13
|
+
session: { secret: process.env.SESSION_SECRET ?? 'dev-only-session-secret-change-me-now!!' },
|
|
14
|
+
})
|
|
@@ -9,6 +9,10 @@
|
|
|
9
9
|
// 다크 헤더 + 라이트 본문(결정 69). 스타일은 Tailwind 유틸(결정 74) — 다크
|
|
10
10
|
// 헤더는 본문 테마와 무관하게 항상 어둡게 두려고 브랜드 색을 명시값으로 박는다.
|
|
11
11
|
// 버전은 스캐폴드 시점 gaonjs 버전이 박힌다.
|
|
12
|
+
//
|
|
13
|
+
// 결정 96: 앱 내부 이동은 `Link`(선언적) — `<a href="/">` 는 전체 문서
|
|
14
|
+
// 리로드라 SPA 가 깨진다. 외부 URL 만 `<a>`(문서·GitHub).
|
|
15
|
+
import { Link } from 'gaonjs/vue'
|
|
12
16
|
|
|
13
17
|
// package.json 의 gaonjs 의존 범위(예: ^0.9.2)에서 캐럿·틸드를 벗겨 표기.
|
|
14
18
|
const version = '{{GAONJS_VERSION}}'.replace(/^[\^~]/, '')
|
|
@@ -19,13 +23,13 @@ const version = '{{GAONJS_VERSION}}'.replace(/^[\^~]/, '')
|
|
|
19
23
|
<header
|
|
20
24
|
class="flex items-center justify-between gap-4 border-b border-[#21262d] bg-[#0d1117] px-6 py-3.5 text-[#e6edf3]"
|
|
21
25
|
>
|
|
22
|
-
<
|
|
26
|
+
<Link href="/" class="inline-flex items-baseline gap-2.5 text-inherit no-underline">
|
|
23
27
|
<span
|
|
24
28
|
class="inline-block rounded-md bg-gradient-to-br from-[#4f8cff] to-[#7c5cff] px-1.5 py-0.5 text-xs font-bold tracking-wide text-white"
|
|
25
29
|
>가온</span>
|
|
26
30
|
<span class="text-[0.95rem] font-bold tracking-[0.08em]">GAONJS</span>
|
|
27
31
|
<span class="text-xs tabular-nums text-[#8b949e]">v{{ version }}</span>
|
|
28
|
-
</
|
|
32
|
+
</Link>
|
|
29
33
|
<nav class="flex gap-[1.1rem] text-sm">
|
|
30
34
|
<a href="https://gaonjs.dev" target="_blank" rel="noreferrer" class="text-[#c9d1d9] no-underline hover:text-white">문서</a>
|
|
31
35
|
<a href="https://github.com/gaonjs" target="_blank" rel="noreferrer" class="text-[#c9d1d9] no-underline hover:text-white">GitHub</a>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gaonjs/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.24.0",
|
|
4
4
|
"description": "Gaon CLI 구현: 제너레이터·스캐폴딩·로드맵 출력 (M1 스텁)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -27,12 +27,12 @@
|
|
|
27
27
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
28
28
|
"typescript": "^5.9.0",
|
|
29
29
|
"vite": "^7.0.0",
|
|
30
|
-
"@gaonjs/async": "0.6.
|
|
31
|
-
"@gaonjs/
|
|
32
|
-
"@gaonjs/
|
|
33
|
-
"@gaonjs/core": "0.2.
|
|
34
|
-
"@gaonjs/mail": "0.1.
|
|
35
|
-
"@gaonjs/
|
|
30
|
+
"@gaonjs/async": "0.6.1",
|
|
31
|
+
"@gaonjs/web": "0.7.2",
|
|
32
|
+
"@gaonjs/config": "0.5.3",
|
|
33
|
+
"@gaonjs/core": "0.2.1",
|
|
34
|
+
"@gaonjs/mail": "0.1.3",
|
|
35
|
+
"@gaonjs/data": "0.9.2"
|
|
36
36
|
},
|
|
37
37
|
"scripts": {
|
|
38
38
|
"build": "node ../../node_modules/typescript/bin/tsc -p tsconfig.json && node -e \"require('fs').cpSync('src/templates','dist/templates',{recursive:true})\""
|