@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
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// @gaonjs/cli · doctor · pageProps() 구조분해 검출 (결정 99 · 경고)
|
|
2
|
+
//
|
|
3
|
+
// `pageProps()` 는 반응형 프록시다 — 같은 페이지로의 리다이렉트/리로드로 서버가
|
|
4
|
+
// 새 props 를 주면(댓글 작성·삭제 등) 재마운트 없이 화면이 즉시 갱신된다. 하지만
|
|
5
|
+
// `const { posts } = pageProps<...>()` 처럼 **구조분해**하면 그 순간 값을 한 번
|
|
6
|
+
// 읽어 반응성이 끊긴다(Vue defineProps 를 구조분해하면 안 되는 것과 같은 함정 ·
|
|
7
|
+
// 첫 실사용 블로그에서 preserveState:false 강제 리로드 우회로 나타난 근본 원인).
|
|
8
|
+
// 이 검사가 그 구조분해를 경고로 잡는다. 판정은 소스 텍스트 기반(주석 제외).
|
|
9
|
+
//
|
|
10
|
+
// 오탐 방지: `const props = pageProps<...>()`(변수 바인딩)는 정상이므로 건드리지
|
|
11
|
+
// 않는다 — 여는 중괄호 `{` 로 시작하는 구조분해 바인딩만 잡는다.
|
|
12
|
+
import { readdir, readFile } from 'node:fs/promises';
|
|
13
|
+
import { join, relative } from 'node:path';
|
|
14
|
+
// 주석을 공백으로 치환하되 줄바꿈은 보존한다(라인 번호 유지) — "구조분해하지
|
|
15
|
+
// 말라"고 설명하는 주석이 오탐을 내지 않도록.
|
|
16
|
+
function stripCommentsKeepLines(source) {
|
|
17
|
+
const blank = (m) => m.replace(/[^\n]/g, ' ');
|
|
18
|
+
return source
|
|
19
|
+
.replace(/\/\*[\s\S]*?\*\//g, blank)
|
|
20
|
+
.replace(/<!--[\s\S]*?-->/g, blank)
|
|
21
|
+
.replace(/(^|[^:])\/\/[^\n]*/g, (_m, p1) => p1 + ' '.repeat(_m.length - p1.length));
|
|
22
|
+
}
|
|
23
|
+
// `const|let|var { ... } = pageProps` — 구조분해 바인딩의 초기화가 pageProps 호출.
|
|
24
|
+
// 제네릭 인자(`<'web:posts#index'>`)·공백·중첩 중괄호는 허용하되, `{` 로 시작하는
|
|
25
|
+
// 구조분해만 잡는다(변수 바인딩 `const props =` 은 제외).
|
|
26
|
+
const DESTRUCTURE_RE = /\b(?:const|let|var)\s*\{[^}]*\}\s*=\s*pageProps\b/g;
|
|
27
|
+
/** 소스에 pageProps() 구조분해가 있는지(단위 테스트 진입점 · 주석은 제외). */
|
|
28
|
+
export function usesPagePropsDestructure(source) {
|
|
29
|
+
DESTRUCTURE_RE.lastIndex = 0;
|
|
30
|
+
return DESTRUCTURE_RE.test(stripCommentsKeepLines(source));
|
|
31
|
+
}
|
|
32
|
+
/** apps/ 의 .vue 를 훑어 pageProps() 구조분해를 경고로 낸다. */
|
|
33
|
+
export async function checkPagePropsDestructure(cwd) {
|
|
34
|
+
const appsDir = join(cwd, 'apps');
|
|
35
|
+
const issues = [];
|
|
36
|
+
for (const abs of await walkVue(appsDir)) {
|
|
37
|
+
const source = await readFile(abs, 'utf8').catch(() => '');
|
|
38
|
+
const stripped = stripCommentsKeepLines(source);
|
|
39
|
+
const rel = relative(cwd, abs);
|
|
40
|
+
for (const m of stripped.matchAll(DESTRUCTURE_RE)) {
|
|
41
|
+
const line = stripped.slice(0, m.index ?? 0).split('\n').length;
|
|
42
|
+
issues.push({
|
|
43
|
+
rule: 'pageprops-destructure',
|
|
44
|
+
level: 'warning',
|
|
45
|
+
file: rel,
|
|
46
|
+
line,
|
|
47
|
+
message: `pageProps() 구조분해 발견: ${rel}:${line} 이 \`const { … } = pageProps(…)\` 로 ` +
|
|
48
|
+
`props 를 구조분해합니다. pageProps() 는 반응형 프록시라, 구조분해하면 그 순간 값을 ` +
|
|
49
|
+
`한 번 읽어 반응성이 끊깁니다 — 같은 페이지로의 리다이렉트/리로드(댓글 작성·삭제 등) ` +
|
|
50
|
+
`후 새 데이터가 화면에 반영되지 않습니다(결정 99).\n` +
|
|
51
|
+
`→ 변수로 받아 속성으로 접근하세요: \`const props = pageProps<'…'>()\` 후 ` +
|
|
52
|
+
`\`props.posts\`. (Vue defineProps 를 구조분해하면 안 되는 것과 같은 이유입니다.)`,
|
|
53
|
+
detail: { file: rel, line },
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return { rule: 'pageprops-destructure', issues };
|
|
58
|
+
}
|
|
59
|
+
/** apps/ 하위 .vue 절대경로. */
|
|
60
|
+
async function walkVue(dir) {
|
|
61
|
+
const out = [];
|
|
62
|
+
const walk = async (d) => {
|
|
63
|
+
let entries;
|
|
64
|
+
try {
|
|
65
|
+
entries = await readdir(d, { withFileTypes: true });
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
for (const e of entries) {
|
|
71
|
+
const abs = join(d, e.name);
|
|
72
|
+
if (e.isDirectory()) {
|
|
73
|
+
if (e.name === 'node_modules' || e.name === '.gaon')
|
|
74
|
+
continue;
|
|
75
|
+
await walk(abs);
|
|
76
|
+
}
|
|
77
|
+
else if (e.isFile() && e.name.endsWith('.vue')) {
|
|
78
|
+
out.push(abs);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
await walk(dir);
|
|
83
|
+
return out.sort();
|
|
84
|
+
}
|
package/dist/doctor/types.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type DoctorRule = 'response-mixing' | 'n-plus-one' | 'dependency-direction' | 'connections' | 'migration-diff' | 'shared-composable-purity' | 'no-auto-import' | 'schema-filename' | 'agents-doc-index' | 'column-casing' | 'model-filename' | 'page-filename' | 'auth-wiring' | 'ui-kit-wiring' | 'route-registration' | 'static-collision' | 'method-override';
|
|
1
|
+
export type DoctorRule = 'response-mixing' | 'n-plus-one' | 'dependency-direction' | 'connections' | 'migration-diff' | 'shared-composable-purity' | 'no-auto-import' | 'schema-filename' | 'agents-doc-index' | 'column-casing' | 'model-filename' | 'page-filename' | 'auth-wiring' | 'ui-kit-wiring' | 'route-registration' | 'static-collision' | 'method-override' | 'csrf-wiring' | 'internal-anchor' | 'pageprops-destructure' | 'async-offload';
|
|
2
2
|
export type DoctorLevel = 'passed' | 'warning' | 'error';
|
|
3
3
|
export interface DoctorCheck {
|
|
4
4
|
readonly rule: DoctorRule;
|
package/dist/doctor/types.js
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
// @gaonjs/cli · doctor 공용 타입 (M9-E · M9-E-Fix · M9-E 확장 · E-5)
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
// · agents-doc-index · column-casing · model-filename · page-filename · auth-wiring
|
|
6
|
-
// · ui-kit-wiring)가 모두 이 DoctorCheck 를 낸다. 상위(runDoctorCommand)는 level 로 passed/
|
|
3
|
+
// 21 검사(전체 목록은 AGENTS §2.2 · doctor.ts `ALL_RULES`)가 모두 이 DoctorCheck
|
|
4
|
+
// 를 낸다. 상위(runDoctorCommand)는 level 로 passed/
|
|
7
5
|
// warnings/errors 로 갈라 담는다. 자동화(CI)는 JSON 을 파싱해
|
|
8
6
|
// errors.length > 0 이면 fail 로 판단한다.
|
|
9
7
|
//
|
package/dist/doctor.d.ts
CHANGED
|
@@ -15,6 +15,10 @@ export { isPascalCase, checkModelFilename } from './doctor/model-filename.js';
|
|
|
15
15
|
export { checkPageFilename } from './doctor/page-filename.js';
|
|
16
16
|
export { usesAuthSurface, hasAuthWiring, hasSessionWiring, checkAuthWiring, } from './doctor/auth-wiring.js';
|
|
17
17
|
export { importsUiKit, checkUiKitWiring } from './doctor/ui-kit-wiring.js';
|
|
18
|
+
export { hasStateChangingRoutes, checkCsrfWiring } from './doctor/csrf-wiring.js';
|
|
19
|
+
export { internalAnchorHref, usesInternalAnchor, checkInternalAnchor, } from './doctor/internal-anchor.js';
|
|
20
|
+
export { usesPagePropsDestructure, checkPagePropsDestructure, } from './doctor/pageprops-destructure.js';
|
|
21
|
+
export { isApiApp, importsMailSdk, importsImageLib, callsExternalHttp, pageActions, checkAsyncOffload, } from './doctor/async-offload.js';
|
|
18
22
|
export { renderHuman, renderJson } from './doctor/reporter.js';
|
|
19
23
|
export { checkTypeScriptApi, detectProject, fatalNoProject, fatalTsApiMissing, } from './doctor/setup.js';
|
|
20
24
|
export interface DoctorCommandOptions {
|
package/dist/doctor.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @gaonjs/cli · `gaon doctor` — 정적 검사 (M9-E · CLI DX 완성 · E-5 확장)
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* 21 검사를 조립한다:
|
|
5
5
|
* 1) response-mixing (errata E-3 §C · 라이브)
|
|
6
6
|
* 2) n-plus-one (errata E-4 (e))
|
|
7
7
|
* 3) dependency-direction (CLAUDE.md §5 · 4 규칙)
|
|
@@ -19,6 +19,10 @@
|
|
|
19
19
|
* 15) route-registration (결정 79 · 고아 컨트롤러 = routes.ts 미참조 경고)
|
|
20
20
|
* 16) static-collision (결정 85 · 정적 파일이 라우트/에셋에 가려짐 경고)
|
|
21
21
|
* 17) method-override (결정 89 · _method HTTP 스푸핑 hack 경고)
|
|
22
|
+
* 18) csrf-wiring (결정 93 · 비-GET 라우트 + session 미배선 = CSRF 무방비 경고)
|
|
23
|
+
* 19) internal-anchor (결정 96 · 앱 내부 이동 일반 <a> = 풀 리로드 경고)
|
|
24
|
+
* 20) pageprops-destructure (결정 99 · pageProps() 구조분해 = 반응성 끊김 경고)
|
|
25
|
+
* 21) async-offload (결정 102·103 · 컨트롤러 인라인 메일·이미지·외부 HTTP = 응답 지연 경고)
|
|
22
26
|
*
|
|
23
27
|
* 각 검사는 순수 함수(cwd → RuleReport). 상위 runDoctorCommand 가 조립해
|
|
24
28
|
* DoctorResult 로 낸다. --json 은 자동화(CI)를 위해 반드시 파싱 가능한
|
|
@@ -48,6 +52,10 @@ import { checkUiKitWiring } from './doctor/ui-kit-wiring.js';
|
|
|
48
52
|
import { checkRouteRegistration } from './doctor/route-registration.js';
|
|
49
53
|
import { checkStaticCollision } from './doctor/static-collision.js';
|
|
50
54
|
import { checkMethodOverride } from './doctor/method-override.js';
|
|
55
|
+
import { checkCsrfWiring } from './doctor/csrf-wiring.js';
|
|
56
|
+
import { checkInternalAnchor } from './doctor/internal-anchor.js';
|
|
57
|
+
import { checkPagePropsDestructure } from './doctor/pageprops-destructure.js';
|
|
58
|
+
import { checkAsyncOffload } from './doctor/async-offload.js';
|
|
51
59
|
import { renderHuman, renderJson } from './doctor/reporter.js';
|
|
52
60
|
import { checkTypeScriptApi, detectProject, fatalNoProject, fatalTsApiMissing, } from './doctor/setup.js';
|
|
53
61
|
import { makeResult, } from './doctor/types.js';
|
|
@@ -66,10 +74,14 @@ export { isPascalCase, checkModelFilename } from './doctor/model-filename.js';
|
|
|
66
74
|
export { checkPageFilename } from './doctor/page-filename.js';
|
|
67
75
|
export { usesAuthSurface, hasAuthWiring, hasSessionWiring, checkAuthWiring, } from './doctor/auth-wiring.js';
|
|
68
76
|
export { importsUiKit, checkUiKitWiring } from './doctor/ui-kit-wiring.js';
|
|
77
|
+
export { hasStateChangingRoutes, checkCsrfWiring } from './doctor/csrf-wiring.js';
|
|
78
|
+
export { internalAnchorHref, usesInternalAnchor, checkInternalAnchor, } from './doctor/internal-anchor.js';
|
|
79
|
+
export { usesPagePropsDestructure, checkPagePropsDestructure, } from './doctor/pageprops-destructure.js';
|
|
80
|
+
export { isApiApp, importsMailSdk, importsImageLib, callsExternalHttp, pageActions, checkAsyncOffload, } from './doctor/async-offload.js';
|
|
69
81
|
export { renderHuman, renderJson } from './doctor/reporter.js';
|
|
70
82
|
export { checkTypeScriptApi, detectProject, fatalNoProject, fatalTsApiMissing, } from './doctor/setup.js';
|
|
71
83
|
/**
|
|
72
|
-
* 실행할 검사 이름. 지정 없음(undefined) =
|
|
84
|
+
* 실행할 검사 이름. 지정 없음(undefined) = 21개 모두.
|
|
73
85
|
*/
|
|
74
86
|
const ALL_RULES = [
|
|
75
87
|
'response-mixing',
|
|
@@ -89,6 +101,10 @@ const ALL_RULES = [
|
|
|
89
101
|
'route-registration',
|
|
90
102
|
'static-collision',
|
|
91
103
|
'method-override',
|
|
104
|
+
'csrf-wiring',
|
|
105
|
+
'internal-anchor',
|
|
106
|
+
'pageprops-destructure',
|
|
107
|
+
'async-offload',
|
|
92
108
|
];
|
|
93
109
|
const CHECKERS = {
|
|
94
110
|
'response-mixing': checkResponseMixing,
|
|
@@ -108,6 +124,10 @@ const CHECKERS = {
|
|
|
108
124
|
'route-registration': checkRouteRegistration,
|
|
109
125
|
'static-collision': checkStaticCollision,
|
|
110
126
|
'method-override': checkMethodOverride,
|
|
127
|
+
'csrf-wiring': checkCsrfWiring,
|
|
128
|
+
'internal-anchor': checkInternalAnchor,
|
|
129
|
+
'pageprops-destructure': checkPagePropsDestructure,
|
|
130
|
+
'async-offload': checkAsyncOffload,
|
|
111
131
|
};
|
|
112
132
|
/**
|
|
113
133
|
* 규칙을 순서대로 실행해 RuleReport[] 를 낸다. 규칙 하나가 크래시해도 나머지는
|
package/dist/generate.d.ts
CHANGED
|
@@ -21,6 +21,14 @@ export declare function authScaffoldFiles(opts?: AuthScaffoldOptions): ScaffoldF
|
|
|
21
21
|
* `routes((r) => {` 콜백 여는 지점 뒤에 두 줄을 삽입한다.
|
|
22
22
|
*/
|
|
23
23
|
export declare function patchRoutes(existing: string): string | null;
|
|
24
|
+
/**
|
|
25
|
+
* 기존 app.config.ts 에 auth 배선을 끼워 넣는다 (결정 93 · gaon new 기본 web 앱은
|
|
26
|
+
* 세션만 배선돼 있어 g auth 는 auth 만 추가하면 완성된다). 반환:
|
|
27
|
+
* · auth 가 이미 있으면 null (그대로 둔다),
|
|
28
|
+
* · defineAppConfig 객체 리터럴을 못 찾으면 null (안전 · 상위에서 경고 폴백).
|
|
29
|
+
* 성공 시 loadUser import 도 함께 보장한다.
|
|
30
|
+
*/
|
|
31
|
+
export declare function patchAppConfigAddAuth(existing: string): string | null;
|
|
24
32
|
/** 스캐폴드를 디스크에 쓴다. 기존 파일은 덮어쓰지 않고 skip 한다. routes.ts 는 패치. */
|
|
25
33
|
export declare function writeAuthScaffold(cwd: string, opts?: AuthScaffoldOptions): ScaffoldResult;
|
|
26
34
|
export interface GenerateAuthOptions {
|
package/dist/generate.js
CHANGED
|
@@ -69,6 +69,38 @@ export function patchRoutes(existing) {
|
|
|
69
69
|
"\n r.resource('registration') // 회원가입 (gaon g auth)";
|
|
70
70
|
return existing.slice(0, insertAt) + inject + existing.slice(insertAt);
|
|
71
71
|
}
|
|
72
|
+
/**
|
|
73
|
+
* 기존 app.config.ts 에 auth 배선을 끼워 넣는다 (결정 93 · gaon new 기본 web 앱은
|
|
74
|
+
* 세션만 배선돼 있어 g auth 는 auth 만 추가하면 완성된다). 반환:
|
|
75
|
+
* · auth 가 이미 있으면 null (그대로 둔다),
|
|
76
|
+
* · defineAppConfig 객체 리터럴을 못 찾으면 null (안전 · 상위에서 경고 폴백).
|
|
77
|
+
* 성공 시 loadUser import 도 함께 보장한다.
|
|
78
|
+
*/
|
|
79
|
+
export function patchAppConfigAddAuth(existing) {
|
|
80
|
+
if (/\bauth\s*:/.test(existing))
|
|
81
|
+
return null;
|
|
82
|
+
const m = existing.match(/defineAppConfig\(\s*\{/);
|
|
83
|
+
if (!m || m.index === undefined)
|
|
84
|
+
return null;
|
|
85
|
+
const insertAt = m.index + m[0].length;
|
|
86
|
+
let out = existing.slice(0, insertAt) +
|
|
87
|
+
"\n // 결정 59: 세션 userId → 사용자 로드 배선(g auth). 이게 있어야 로그인 후" +
|
|
88
|
+
"\n // this.currentUser / this.requireAuth() 가 실제 사용자를 받는다." +
|
|
89
|
+
"\n auth: { loadUser, loginRedirect: '/session/new' }," +
|
|
90
|
+
existing.slice(insertAt);
|
|
91
|
+
// loadUser import 보장 — gaonjs/config import 바로 뒤에 넣는다.
|
|
92
|
+
if (!/from\s+['"]\.\/auth\.js['"]/.test(out)) {
|
|
93
|
+
const im = out.match(/import\s+\{[^}]*\}\s+from\s+['"]gaonjs\/config['"][^\n]*\n/);
|
|
94
|
+
if (im && im.index !== undefined) {
|
|
95
|
+
const at = im.index + im[0].length;
|
|
96
|
+
out = out.slice(0, at) + "import { loadUser } from './auth.js'\n" + out.slice(at);
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
out = "import { loadUser } from './auth.js'\n" + out;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return out;
|
|
103
|
+
}
|
|
72
104
|
/** 스캐폴드를 디스크에 쓴다. 기존 파일은 덮어쓰지 않고 skip 한다. routes.ts 는 패치. */
|
|
73
105
|
export function writeAuthScaffold(cwd, opts = {}) {
|
|
74
106
|
const root = resolve(cwd);
|
|
@@ -87,19 +119,31 @@ export function writeAuthScaffold(cwd, opts = {}) {
|
|
|
87
119
|
for (const file of authScaffoldFiles(opts)) {
|
|
88
120
|
const abs = join(root, file.path);
|
|
89
121
|
if (existsSync(abs)) {
|
|
90
|
-
|
|
91
|
-
//
|
|
92
|
-
//
|
|
122
|
+
// 결정 93: gaon new 기본 web 앱은 app.config.ts 에 세션만 배선돼 있다.
|
|
123
|
+
// g auth 는 이를 skip 하지 않고 **auth 배선을 패치로 추가**해 완성한다 —
|
|
124
|
+
// 결정 59: auth 배선이 없으면 로그인 후에도 currentUser 가 영구 null 이다.
|
|
93
125
|
if (file.path === `apps/${app}/app.config.ts`) {
|
|
94
126
|
const existing = readFileSync(abs, 'utf8');
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
127
|
+
const patchedConfig = patchAppConfigAddAuth(existing);
|
|
128
|
+
if (patchedConfig) {
|
|
129
|
+
writeFileSync(abs, patchedConfig, 'utf8');
|
|
130
|
+
patched.push(file.path);
|
|
131
|
+
}
|
|
132
|
+
else if (!/\bauth\s*:/.test(existing)) {
|
|
133
|
+
// 패치 불가(비관례 config) — skip + 수리 안내로 폴백.
|
|
134
|
+
skipped.push(file.path);
|
|
135
|
+
warnings.push(`apps/${app}/app.config.ts 가 이미 있어 인증 배선을 자동 추가하지 못했습니다.\n` +
|
|
136
|
+
`→ apps/${app}/app.config.ts 의 defineAppConfig({...}) 에 다음을 추가하세요:\n` +
|
|
99
137
|
` auth: { loadUser, loginRedirect: '/session/new' } // import { loadUser } from './auth.js'\n` +
|
|
100
138
|
`→ 이 배선이 없으면 로그인해도 this.currentUser/requireAuth() 가 사용자를 받지 못합니다.`);
|
|
101
139
|
}
|
|
140
|
+
else {
|
|
141
|
+
// 이미 auth 배선됨 — 그대로 둔다.
|
|
142
|
+
skipped.push(file.path);
|
|
143
|
+
}
|
|
144
|
+
continue;
|
|
102
145
|
}
|
|
146
|
+
skipped.push(file.path);
|
|
103
147
|
continue;
|
|
104
148
|
}
|
|
105
149
|
mkdirSync(dirname(abs), { recursive: true });
|
package/dist/index.d.ts
CHANGED
|
@@ -23,7 +23,7 @@ export { loadDomain, type LoadedDomain } from "./domain.js";
|
|
|
23
23
|
export interface RoadmapReport {
|
|
24
24
|
readonly name: "gaon";
|
|
25
25
|
readonly version: string;
|
|
26
|
-
readonly stage: "
|
|
26
|
+
readonly stage: "stable";
|
|
27
27
|
readonly homepage: string;
|
|
28
28
|
readonly docs: string;
|
|
29
29
|
readonly milestones: readonly {
|
|
@@ -34,7 +34,11 @@ export interface RoadmapReport {
|
|
|
34
34
|
}
|
|
35
35
|
/** `--json` 출력용 구조화 리포트. */
|
|
36
36
|
export declare function roadmapReport(version?: string): RoadmapReport;
|
|
37
|
-
/**
|
|
37
|
+
/**
|
|
38
|
+
* 인자 없이 실행했을 때의 사람용 배너 — 정식 배포(v1.0) 자기 소개 + 사용법 안내.
|
|
39
|
+
* 결정 94(W3): M1 스텁 시절 "개발 초기·런타임 없음" 오정보를 제거한다 — AI 가
|
|
40
|
+
* 사용 불가로 오판하거나 소형 모델이 멈추는 것을 막는다.
|
|
41
|
+
*/
|
|
38
42
|
export declare function renderRoadmap(version?: string): string;
|
|
39
43
|
/** runCli 옵션. version 은 파사드가 주입하는 표시 버전(설치 패키지 버전). */
|
|
40
44
|
export interface RunOptions {
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @gaonjs/cli — Gaon CLI 구현.
|
|
3
3
|
*
|
|
4
|
-
* 인자 없이 실행하면
|
|
5
|
-
*
|
|
4
|
+
* 인자 없이 실행하면 정식 배포(v1.0) 배너와 사용법을 출력한다. `gaon dev`
|
|
5
|
+
* 가 통합 개발 오케스트레이션을 담당한다 — Docker Compose
|
|
6
6
|
* 자동 기동 + .gaon 타입 브리지 재생성 + serve 자식 프로세스 + tsc/vue-tsc
|
|
7
7
|
* --watch + 소스 변경 시 서버 재시작(commands/dev.ts). 모든 명령은 `--json`
|
|
8
8
|
* 출력을 함께 제공한다 (CLAUDE.md §4).
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* 표시 버전은 호출자(파사드)가 주입한다 — 사용자가 설치한 패키지
|
|
11
11
|
* (`gaonjs`) 버전을 그대로 보여주기 위함. 미주입 시 core 버전을 쓴다.
|
|
12
12
|
*/
|
|
13
|
-
import { MILESTONES, VERSION, HOMEPAGE } from "@gaonjs/core";
|
|
13
|
+
import { MILESTONES, VERSION, HOMEPAGE, loadDotEnv } from "@gaonjs/core";
|
|
14
14
|
import { runDevCommand } from "./commands/dev.js";
|
|
15
15
|
import { runCheckCommand } from "./commands/check.js";
|
|
16
16
|
import { runNewCommand } from "./commands/new.js";
|
|
@@ -54,41 +54,38 @@ export function roadmapReport(version = VERSION) {
|
|
|
54
54
|
return {
|
|
55
55
|
name: "gaon",
|
|
56
56
|
version,
|
|
57
|
-
stage: "
|
|
57
|
+
stage: "stable",
|
|
58
58
|
homepage: HOMEPAGE,
|
|
59
59
|
docs: HOMEPAGE,
|
|
60
60
|
milestones: MILESTONES.map((m) => ({ id: m.id, title: m.title, status: m.status })),
|
|
61
61
|
};
|
|
62
62
|
}
|
|
63
|
-
/**
|
|
63
|
+
/**
|
|
64
|
+
* 인자 없이 실행했을 때의 사람용 배너 — 정식 배포(v1.0) 자기 소개 + 사용법 안내.
|
|
65
|
+
* 결정 94(W3): M1 스텁 시절 "개발 초기·런타임 없음" 오정보를 제거한다 — AI 가
|
|
66
|
+
* 사용 불가로 오판하거나 소형 모델이 멈추는 것을 막는다.
|
|
67
|
+
*/
|
|
64
68
|
export function renderRoadmap(version = VERSION) {
|
|
65
69
|
const lines = [];
|
|
66
70
|
lines.push("");
|
|
67
71
|
lines.push(` Gaon (가온) — AI가 개발을 가장 잘하는 Node.js 풀스택 웹 프레임웍`);
|
|
68
|
-
lines.push(` v${version} ·
|
|
72
|
+
lines.push(` v${version} · 정식 배포 · ${HOMEPAGE}`);
|
|
69
73
|
lines.push("");
|
|
70
|
-
lines.push("
|
|
71
|
-
lines.push("
|
|
74
|
+
lines.push(" 데이터·인증·실시간·비동기·메일/스토리지·CLI 배터리가 모두 동작합니다.");
|
|
75
|
+
lines.push(" 새 프로젝트를 만들고 개발 스택을 통합 기동하려면:");
|
|
72
76
|
lines.push("");
|
|
73
|
-
lines.push("
|
|
74
|
-
lines.push("
|
|
75
|
-
|
|
76
|
-
const mark = m.status === "in-progress" ? "▶" : "·";
|
|
77
|
-
const tag = m.status === "in-progress" ? " (진행 중)" : "";
|
|
78
|
-
lines.push(` ${mark} ${m.id} ${m.title}${tag}`);
|
|
79
|
-
}
|
|
77
|
+
lines.push(" gaon new <name> 새 프로젝트 스캐폴드");
|
|
78
|
+
lines.push(" gaon dev 개발 스택 통합 기동 (Docker · .gaon · serve · watch)");
|
|
79
|
+
lines.push(" gaon --help 전체 명령 목록");
|
|
80
80
|
lines.push("");
|
|
81
|
-
lines.push(
|
|
82
|
-
lines.push(" ──────────");
|
|
83
|
-
lines.push(` 문서 / 진행 상황: ${HOMEPAGE}`);
|
|
84
|
-
lines.push(` JSON 출력: gaon --json`);
|
|
81
|
+
lines.push(` 문서: ${HOMEPAGE} · JSON 출력: gaon --json`);
|
|
85
82
|
lines.push("");
|
|
86
83
|
return lines.join("\n");
|
|
87
84
|
}
|
|
88
85
|
function renderHelp(version = VERSION) {
|
|
89
86
|
return [
|
|
90
87
|
"",
|
|
91
|
-
" gaon — Gaon 프레임웍 CLI (v" + version + "
|
|
88
|
+
" gaon — Gaon 프레임웍 CLI (v" + version + " · 정식 배포)",
|
|
92
89
|
"",
|
|
93
90
|
" 사용법:",
|
|
94
91
|
" gaon 로드맵과 개발 상태를 출력",
|
|
@@ -104,7 +101,7 @@ function renderHelp(version = VERSION) {
|
|
|
104
101
|
" gaon check typecheck · vue-tsc · build 통합 검사 (--only <step> · --include-doctor)",
|
|
105
102
|
" gaon console 프로젝트 컨텍스트 REPL (--no-config)",
|
|
106
103
|
" gaon test 테스트 러너 (--scope unit|integration|all · -- vitest 인자)",
|
|
107
|
-
" gaon doctor 정적 검사 (
|
|
104
|
+
" gaon doctor 정적 검사 (21 검사 · 응답 혼용·N+1·의존·커넥션·마이그·컴포저블 순수·자동 import·파일명/컬럼 관례·인증 배선·UI 킷 배선·라우트 등록·정적 충돌·_method·CSRF 배선·내부 앵커·pageProps 구조분해·비동기 오프로드)",
|
|
108
105
|
" gaon doctor --json 자동화용 JSON 출력",
|
|
109
106
|
" gaon doctor --check=n-plus-one,connections 선택 검사만 실행",
|
|
110
107
|
" gaon doctor --fix 기계 정정 가능한 위반 계획(dry-run · v0.16 §7.5.3)",
|
|
@@ -149,6 +146,8 @@ export function parseDoctorChecks(argv) {
|
|
|
149
146
|
"migration-diff",
|
|
150
147
|
"shared-composable-purity",
|
|
151
148
|
"no-auto-import",
|
|
149
|
+
"csrf-wiring",
|
|
150
|
+
"async-offload",
|
|
152
151
|
];
|
|
153
152
|
const isKnown = (s) => known.includes(s);
|
|
154
153
|
const out = [];
|
|
@@ -165,6 +164,13 @@ export function parseDoctorChecks(argv) {
|
|
|
165
164
|
/** CLI 진입점. argv 는 실행 인자(process.argv.slice(2))를 받는다. */
|
|
166
165
|
export function runCli(argv, opts = {}) {
|
|
167
166
|
const version = opts.version ?? VERSION;
|
|
167
|
+
// 결정 92(W1): `.env` 를 **진입점에서 한 번** 로드해 모든 명령이 동일하게
|
|
168
|
+
// 받는다. 이전엔 serve·dev·console 만 개별 호출해 db·work·hub·jobs·doctor·
|
|
169
|
+
// check 가 `.env` 없이 실행됐고(특히 work·hub 는 조용히 안 뜨는 부류),
|
|
170
|
+
// 실사용에서 `set -a; . ./.env` 수동 우회가 필요했다. 명령별 나열은 새
|
|
171
|
+
// 명령이 추가될 때마다 또 빠지므로 여기서 공통화한다. loadDotEnv 는 이미
|
|
172
|
+
// 설정된 env 를 덮지 않고 파일이 없으면 조용히 지나가 멱등하다(재호출 안전).
|
|
173
|
+
loadDotEnv();
|
|
168
174
|
// `gaon dev` — 통합 개발 오케스트레이션(M9-C · v0.15 §13.5). Docker Compose
|
|
169
175
|
// 자동 기동 + .gaon 재생성 + serve 자식 + tsc/vue-tsc watch + 서버 재시작 워처.
|
|
170
176
|
// SIGINT/SIGTERM 시 순서대로 정리(serve → tsc → 워처 → Docker[--stop-docker 시]).
|
package/dist/serve.js
CHANGED
|
@@ -149,6 +149,13 @@ export async function runServeCommand(opts = {}) {
|
|
|
149
149
|
await wired.app.listen({ host, port });
|
|
150
150
|
const displayHost = host === '0.0.0.0' ? 'localhost' : host;
|
|
151
151
|
emit({ kind: 'listening', host, port, url: `http://${displayHost}:${port}` });
|
|
152
|
+
// 결정 98: serve 는 빌드된 번들을 서빙하며 코드 변경을 감시하지 않는다 —
|
|
153
|
+
// 수정이 조용히 반영 안 되는 혼란(첫 실사용 관측)을 막으려 한 줄 안내한다.
|
|
154
|
+
// dev 자식(gaon dev · opts.dev)은 이미 감시하므로 그때는 안내하지 않고,
|
|
155
|
+
// production 은 운영 로그 소음을 막으려 출력하지 않는다. json 은 파싱 안전상 제외.
|
|
156
|
+
if (!json && !opts.dev && process.env.NODE_ENV !== 'production') {
|
|
157
|
+
process.stdout.write(' ℹ serve 는 빌드된 번들을 서빙하며 코드 변경을 감시하지 않습니다 → 개발 중이면 `gaon dev` 를 쓰세요.\n');
|
|
158
|
+
}
|
|
152
159
|
await new Promise((resolvePromise) => {
|
|
153
160
|
const stop = () => {
|
|
154
161
|
signals.off('SIGINT', stop);
|
|
@@ -9,14 +9,15 @@ import CardFooter from '../components/ui/CardFooter.vue'
|
|
|
9
9
|
import Button from '../components/ui/Button.vue'
|
|
10
10
|
|
|
11
11
|
// dashboard#show 의 render props — user 는 직렬화되며 passwordDigest 는 없다(§4.2).
|
|
12
|
-
|
|
12
|
+
// pageProps 는 반응형 — 변수로 받아 props.x 로 접근한다(구조분해 금지 · 결정 99).
|
|
13
|
+
const props = pageProps<'{{APP_NAME}}:dashboard#show'>()
|
|
13
14
|
|
|
14
15
|
// 로그아웃 = DELETE /session (r.resource('session') 의 destroy).
|
|
15
16
|
// HTML <form> 은 DELETE 를 보낼 수 없으므로 Inertia 라우터로 실제 메서드를
|
|
16
17
|
// 보낸다(결정 64) — `?_method=DELETE` 우회는 서버가 해석하지 않아 POST /session
|
|
17
18
|
// (= 로그인 create) 으로 잘못 라우팅됐다.
|
|
18
19
|
function logout(): void {
|
|
19
|
-
router.delete('/session', { headers: { 'x-csrf-token': csrf } })
|
|
20
|
+
router.delete('/session', { headers: { 'x-csrf-token': props.csrf } })
|
|
20
21
|
}
|
|
21
22
|
</script>
|
|
22
23
|
|
|
@@ -24,8 +25,8 @@ function logout(): void {
|
|
|
24
25
|
<div class="mx-auto max-w-2xl px-4 py-10">
|
|
25
26
|
<Card>
|
|
26
27
|
<CardHeader>
|
|
27
|
-
<CardTitle>환영합니다, {{ user.name }}님</CardTitle>
|
|
28
|
-
<CardDescription>{{ user.email }}</CardDescription>
|
|
28
|
+
<CardTitle>환영합니다, {{ props.user.name }}님</CardTitle>
|
|
29
|
+
<CardDescription>{{ props.user.email }}</CardDescription>
|
|
29
30
|
</CardHeader>
|
|
30
31
|
<CardContent>
|
|
31
32
|
<p class="text-sm text-muted-foreground">보호된 페이지입니다 — this.requireAuth() 로 지킵니다.</p>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { pageProps, useForm } from 'gaonjs/vue'
|
|
2
|
+
import { pageProps, useForm, Link } from 'gaonjs/vue'
|
|
3
3
|
import Card from '../../components/ui/Card.vue'
|
|
4
4
|
import CardHeader from '../../components/ui/CardHeader.vue'
|
|
5
5
|
import CardTitle from '../../components/ui/CardTitle.vue'
|
|
@@ -12,12 +12,14 @@ import Button from '../../components/ui/Button.vue'
|
|
|
12
12
|
import Alert from '../../components/ui/Alert.vue'
|
|
13
13
|
import AlertDescription from '../../components/ui/AlertDescription.vue'
|
|
14
14
|
|
|
15
|
-
// 컨트롤러 session#new 의 render props 타입이 그대로 흐른다(§6.2).
|
|
16
|
-
|
|
15
|
+
// 컨트롤러 session#new 의 render props 타입이 그대로 흐른다(§6.2). pageProps 는
|
|
16
|
+
// 반응형이라 변수로 받아 props.x 로 접근한다 — 구조분해 금지(결정 99). 로그인
|
|
17
|
+
// 실패로 서버가 같은 페이지를 다시 render 하면 props.error 가 즉시 갱신된다.
|
|
18
|
+
const props = pageProps<'{{APP_NAME}}:session#new'>()
|
|
17
19
|
|
|
18
20
|
// 세션 앱 폼 = Inertia SPA 제출(결정 64) — fetch() 로 만들지 않는다.
|
|
19
21
|
// 서버는 redirect(Inertia 응답)로 답하고, 실패 시 같은 페이지를 다시 render 한다.
|
|
20
|
-
const form = useForm({ email: '', password: '', _csrf: csrf })
|
|
22
|
+
const form = useForm({ email: '', password: '', _csrf: props.csrf })
|
|
21
23
|
</script>
|
|
22
24
|
|
|
23
25
|
<template>
|
|
@@ -28,8 +30,8 @@ const form = useForm({ email: '', password: '', _csrf: csrf })
|
|
|
28
30
|
<CardDescription>계정으로 로그인하세요.</CardDescription>
|
|
29
31
|
</CardHeader>
|
|
30
32
|
<CardContent>
|
|
31
|
-
<Alert v-if="error" variant="destructive" class="mb-4">
|
|
32
|
-
<AlertDescription>{{ error }}</AlertDescription>
|
|
33
|
+
<Alert v-if="props.error" variant="destructive" class="mb-4">
|
|
34
|
+
<AlertDescription>{{ props.error }}</AlertDescription>
|
|
33
35
|
</Alert>
|
|
34
36
|
<Form @submit="form.post('/session')">
|
|
35
37
|
<FormField label="이메일" :error="form.errors.email">
|
|
@@ -42,7 +44,7 @@ const form = useForm({ email: '', password: '', _csrf: csrf })
|
|
|
42
44
|
</Form>
|
|
43
45
|
<p class="mt-4 text-center text-sm text-muted-foreground">
|
|
44
46
|
계정이 없으신가요?
|
|
45
|
-
<
|
|
47
|
+
<Link href="/registration/new" class="font-medium text-primary underline-offset-4 hover:underline">회원가입</Link>
|
|
46
48
|
</p>
|
|
47
49
|
</CardContent>
|
|
48
50
|
</Card>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { pageProps, useForm } from 'gaonjs/vue'
|
|
2
|
+
import { pageProps, useForm, Link } from 'gaonjs/vue'
|
|
3
3
|
import Card from '../../components/ui/Card.vue'
|
|
4
4
|
import CardHeader from '../../components/ui/CardHeader.vue'
|
|
5
5
|
import CardTitle from '../../components/ui/CardTitle.vue'
|
|
@@ -12,10 +12,11 @@ import Button from '../../components/ui/Button.vue'
|
|
|
12
12
|
import Alert from '../../components/ui/Alert.vue'
|
|
13
13
|
import AlertDescription from '../../components/ui/AlertDescription.vue'
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
// pageProps 는 반응형 — 변수로 받아 props.x 로 접근한다(구조분해 금지 · 결정 99).
|
|
16
|
+
const props = pageProps<'{{APP_NAME}}:registration#new'>()
|
|
16
17
|
|
|
17
18
|
// 세션 앱 폼 = Inertia SPA 제출(결정 64) — fetch() 로 만들지 않는다.
|
|
18
|
-
const form = useForm({ name: '', email: '', password: '', _csrf: csrf })
|
|
19
|
+
const form = useForm({ name: '', email: '', password: '', _csrf: props.csrf })
|
|
19
20
|
</script>
|
|
20
21
|
|
|
21
22
|
<template>
|
|
@@ -26,8 +27,8 @@ const form = useForm({ name: '', email: '', password: '', _csrf: csrf })
|
|
|
26
27
|
<CardDescription>새 계정을 만드세요.</CardDescription>
|
|
27
28
|
</CardHeader>
|
|
28
29
|
<CardContent>
|
|
29
|
-
<Alert v-if="error" variant="destructive" class="mb-4">
|
|
30
|
-
<AlertDescription>{{ error }}</AlertDescription>
|
|
30
|
+
<Alert v-if="props.error" variant="destructive" class="mb-4">
|
|
31
|
+
<AlertDescription>{{ props.error }}</AlertDescription>
|
|
31
32
|
</Alert>
|
|
32
33
|
<Form @submit="form.post('/registration')">
|
|
33
34
|
<FormField label="이름" :error="form.errors.name">
|
|
@@ -43,7 +44,7 @@ const form = useForm({ name: '', email: '', password: '', _csrf: csrf })
|
|
|
43
44
|
</Form>
|
|
44
45
|
<p class="mt-4 text-center text-sm text-muted-foreground">
|
|
45
46
|
이미 계정이 있으신가요?
|
|
46
|
-
<
|
|
47
|
+
<Link href="/session/new" class="font-medium text-primary underline-offset-4 hover:underline">로그인</Link>
|
|
47
48
|
</p>
|
|
48
49
|
</CardContent>
|
|
49
50
|
</Card>
|
|
@@ -104,7 +104,7 @@ Gaon 의 제1 설계 목표는 **"AI 가 개발을 가장 잘하는 프레임웍
|
|
|
104
104
|
컬럼명 · 스키마 파일 ↔ 테이블 ↔ `tables.d.ts` 키 변환 규칙)은
|
|
105
105
|
`agents/data.md` "DB 네이밍" 표가 정본이다 — 먼저 읽는다.
|
|
106
106
|
|
|
107
|
-
### 2.2 `gaon doctor` 검사
|
|
107
|
+
### 2.2 `gaon doctor` 검사 21종
|
|
108
108
|
|
|
109
109
|
1. `response-mixing` — 한 액션 안 render/JSON/redirect 혼용 (E-3)
|
|
110
110
|
2. `n-plus-one` — include 미사용 · loop 안 관계 호출 (E-4)
|
|
@@ -123,6 +123,10 @@ Gaon 의 제1 설계 목표는 **"AI 가 개발을 가장 잘하는 프레임웍
|
|
|
123
123
|
15. `route-registration` — 고아 컨트롤러(파일은 있는데 `routes.ts` 미참조 · 도달 불가) (결정 79 · 경고)
|
|
124
124
|
16. `static-collision` — 정적 파일(`apps/<앱>/static/`)이 라우트/에셋에 가려져 도달 불가 (결정 85 · 경고)
|
|
125
125
|
17. `method-override` — `_method` HTTP 메서드 스푸핑 hack(Gaon 미지원 · router.delete 를 쓰라) (결정 89 · 경고)
|
|
126
|
+
18. `csrf-wiring` — 비-GET 라우트(POST/PUT/PATCH/DELETE)가 있는데 `app.config.ts` 에 session 미배선 = CSRF 무방비 (결정 93 · 경고)
|
|
127
|
+
19. `internal-anchor` — 앱 내부 경로 일반 `<a href="/...">`(풀 리로드로 SPA 파손 · `Link`/`router.visit` 를 쓰라 · 외부 URL·`target="_blank"` 는 제외) (결정 96 · 경고)
|
|
128
|
+
20. `pageprops-destructure` — `const { x } = pageProps(…)` 구조분해(반응성 끊김 · 리다이렉트/리로드 후 갱신 안 됨 · `const props = pageProps(…)` 후 `props.x` 로 접근하라) (결정 99 · 경고)
|
|
129
|
+
21. `async-offload` — 컨트롤러 액션 인라인의 무거운/외부 작업(메일 SDK·이미지 처리 sharp/jimp·외부 HTTP)이 응답을 지연 (`domain/jobs/` 잡 + `.later()` 로 빼라 · JSON/API 앱 외부 호출·빠른 내부 호출은 오탐 방지로 제외) (결정 102·103 · 경고)
|
|
126
130
|
|
|
127
131
|
## 3. 로직 배치 One Way 판단표
|
|
128
132
|
|
|
@@ -156,6 +160,26 @@ Gaon 의 제1 설계 목표는 **"AI 가 개발을 가장 잘하는 프레임웍
|
|
|
156
160
|
위치를 강제하지 않는다 (`agents/async.md` §2). DB 커밋 정합이 필요하면
|
|
157
161
|
서비스 `afterCommit()` 또는 아웃박스.
|
|
158
162
|
|
|
163
|
+
### 3.5 비동기 배치 (동기 인라인 vs 잡/이벤트/스케줄 · 결정 102)
|
|
164
|
+
|
|
165
|
+
§3.1~§3.4 가 "로직을 **어느 층**에 둘까"라면, 이 표는 "그 로직을 **동기로 둘까
|
|
166
|
+
비동기로 뺄까**"의 One Way 다. 상세·예시는 `agents/async.md` 서두 판단표.
|
|
167
|
+
|
|
168
|
+
| 상황 | 배치 | 이유 |
|
|
169
|
+
|---|---|---|
|
|
170
|
+
| 응답에 결과가 **필요** | 요청 인라인 | 사용자가 그 값을 기다린다 |
|
|
171
|
+
| 응답 불필요 · 실패 시 재시도 (메일·알림·웹훅) | **잡** (`domain/jobs/`) | 응답 지연 제거 + 재시도(백오프·DLQ) |
|
|
172
|
+
| 느리거나 불안정한 **외부 API** | **잡** | 이벤트 루프·응답 시간 보호 |
|
|
173
|
+
| **CPU 무거운** 작업 (이미지 리사이즈·리포트·대량 연산) | **잡** | 루프 블로킹 = 이웃 요청 전부 정지 |
|
|
174
|
+
| 트랜잭션 커밋 **파생 효과** | **이벤트 + 아웃박스** (`afterCommit`) | DB 확정과 정합 (롤백 시 발행 취소) |
|
|
175
|
+
| **주기** 작업 (정리·집계·랭킹) | **`domain/schedule.ts`** | 요청과 무관한 시간축 · 리더 1인만 |
|
|
176
|
+
|
|
177
|
+
**실무 신호**: 요청 중 수백 ms 이상 걸리는 일은 잡 후보. Node 는 싱글 스레드라
|
|
178
|
+
무거운 작업 하나가 이웃 요청을 세운다. **"잡으로 뺀다 = `work` 프로세스로
|
|
179
|
+
옮긴다"** — 인라인은 `serve`(웹)가, 잡·`afterCommit`·`schedule` 은 `work` 가
|
|
180
|
+
처리하므로 웹 응답이 그 무게를 지지 않는다. `gaon doctor` 의 `async-offload`
|
|
181
|
+
가 컨트롤러 인라인 메일·이미지 처리·외부 HTTP 를 경고로 잡는다(결정 103).
|
|
182
|
+
|
|
159
183
|
## 4. 검증 루프
|
|
160
184
|
|
|
161
185
|
작업마다 실행한다:
|
|
@@ -163,7 +187,7 @@ Gaon 의 제1 설계 목표는 **"AI 가 개발을 가장 잘하는 프레임웍
|
|
|
163
187
|
```bash
|
|
164
188
|
gaon check # .gaon 재생성 → typecheck + vue-tsc + build (+doctor)
|
|
165
189
|
gaon test # vitest — DB·NATS 는 실 인프라 (agents/testing.md)
|
|
166
|
-
gaon doctor # 정적 검사
|
|
190
|
+
gaon doctor # 정적 검사 21종 (§2.2)
|
|
167
191
|
```
|
|
168
192
|
|
|
169
193
|
### 4.1 CLI 명령 (전 명령 `--json` 지원)
|
|
@@ -171,8 +195,8 @@ gaon doctor # 정적 검사 17종 (§2.2)
|
|
|
171
195
|
| 명령 | 역할 |
|
|
172
196
|
|---|---|
|
|
173
197
|
| `gaon new <name>` | 프로젝트 스캐폴드 |
|
|
174
|
-
| `gaon dev` | 통합 개발 오케스트레이션 (Docker·`.gaon`
|
|
175
|
-
| `gaon serve` / `work` / `hub` | 운영 프로세스 3종 (웹 · 워커 · 실시간 허브) |
|
|
198
|
+
| `gaon dev` | 통합 개발 오케스트레이션 (Docker·`.gaon` 재생성·**코드 변경 감시·재시작**) |
|
|
199
|
+
| `gaon serve` / `work` / `hub` | 운영 프로세스 3종 (웹 · 워커 · 실시간 허브) — **감시 없음** |
|
|
176
200
|
| `gaon g <type> <name>` | 스캐폴드: `auth`·`controller`·`model`·`page`·`job` |
|
|
177
201
|
| `gaon db <sub>` | `diff`·`migrate`(`down`)·`status`·`reset`·`seed` (`agents/data.md` §10) |
|
|
178
202
|
| `gaon check` / `test` / `doctor` | 검증 루프 |
|
|
@@ -184,6 +208,11 @@ gaon doctor # 정적 검사 17종 (§2.2)
|
|
|
184
208
|
으로 더듬는 대신 프레임웍에게 직접 묻는다. `read_agent_doc` 은 §0
|
|
185
209
|
카테고리 문서를 조회한다 (결정 40).
|
|
186
210
|
|
|
211
|
+
**개발 중엔 `gaon dev`, 배포 실행은 `gaon serve`** (결정 98) — `gaon serve` 는
|
|
212
|
+
빌드된 번들을 서빙하며 **코드 변경을 감시하지 않는다**(수정해도 반영 안 됨).
|
|
213
|
+
개발 중이면 `gaon dev`(감시·재시작·`.gaon` 재생성 통합)를 쓴다. `serve` 는
|
|
214
|
+
비-production 부팅 시 이 안내를 한 줄 출력한다.
|
|
215
|
+
|
|
187
216
|
## 5. npm 배포본 (2026-07-23 실측 · `npm view <pkg> version`)
|
|
188
217
|
|
|
189
218
|
| 패키지 | 버전 | 역할 |
|