@gaonjs/cli 0.4.0 → 0.10.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.
Files changed (120) hide show
  1. package/dist/commands/check.d.ts +50 -0
  2. package/dist/commands/check.js +286 -0
  3. package/dist/commands/console.d.ts +46 -0
  4. package/dist/commands/console.js +129 -0
  5. package/dist/commands/db.d.ts +3 -1
  6. package/dist/commands/db.js +8 -2
  7. package/dist/commands/g.d.ts +1 -1
  8. package/dist/commands/g.js +27 -3
  9. package/dist/commands/mcp.d.ts +15 -0
  10. package/dist/commands/mcp.js +78 -0
  11. package/dist/commands/new.d.ts +45 -0
  12. package/dist/commands/new.js +274 -0
  13. package/dist/commands/test.d.ts +11 -0
  14. package/dist/commands/test.js +119 -0
  15. package/dist/db/diff.js +5 -0
  16. package/dist/db/journal.d.ts +34 -0
  17. package/dist/db/journal.js +71 -0
  18. package/dist/db/migrate.d.ts +6 -1
  19. package/dist/db/migrate.js +120 -102
  20. package/dist/db/replay.d.ts +49 -0
  21. package/dist/db/replay.js +148 -0
  22. package/dist/db/status.d.ts +12 -0
  23. package/dist/db/status.js +61 -0
  24. package/dist/dev/index.d.ts +2 -0
  25. package/dist/dev/index.js +2 -0
  26. package/dist/dev/vite.d.ts +67 -0
  27. package/dist/dev/vite.js +126 -0
  28. package/dist/dev.d.ts +18 -0
  29. package/dist/dev.js +15 -0
  30. package/dist/doctor/agents-doc-index.d.ts +4 -0
  31. package/dist/doctor/agents-doc-index.js +80 -0
  32. package/dist/doctor/fixers/dependency-direction.d.ts +9 -0
  33. package/dist/doctor/fixers/dependency-direction.js +98 -0
  34. package/dist/doctor/fixers/index.d.ts +15 -0
  35. package/dist/doctor/fixers/index.js +66 -0
  36. package/dist/doctor/fixers/schema-filename.d.ts +14 -0
  37. package/dist/doctor/fixers/schema-filename.js +104 -0
  38. package/dist/doctor/fixers/types.d.ts +59 -0
  39. package/dist/doctor/fixers/types.js +15 -0
  40. package/dist/doctor/no-auto-import.d.ts +10 -0
  41. package/dist/doctor/no-auto-import.js +158 -0
  42. package/dist/doctor/schema-filename.d.ts +6 -0
  43. package/dist/doctor/schema-filename.js +81 -0
  44. package/dist/doctor/shared-composable-purity.d.ts +8 -0
  45. package/dist/doctor/shared-composable-purity.js +164 -0
  46. package/dist/doctor/types.d.ts +1 -1
  47. package/dist/doctor/types.js +6 -5
  48. package/dist/doctor.d.ts +51 -0
  49. package/dist/doctor.js +191 -7
  50. package/dist/generate.js +2 -2
  51. package/dist/hub.d.ts +1 -1
  52. package/dist/index.d.ts +6 -2
  53. package/dist/index.js +154 -16
  54. package/dist/mcp/index.d.ts +7 -0
  55. package/dist/mcp/index.js +7 -0
  56. package/dist/mcp/server.d.ts +50 -0
  57. package/dist/mcp/server.js +102 -0
  58. package/dist/mcp/tools.d.ts +109 -0
  59. package/dist/mcp/tools.js +485 -0
  60. package/dist/scaffold/app.d.ts +5 -0
  61. package/dist/scaffold/app.js +172 -0
  62. package/dist/scaffold/controller.js +2 -2
  63. package/dist/scaffold/index.d.ts +2 -1
  64. package/dist/scaffold/index.js +2 -1
  65. package/dist/scaffold/job.d.ts +5 -0
  66. package/dist/scaffold/job.js +35 -0
  67. package/dist/scaffold/model.js +8 -8
  68. package/dist/templates/auth/auth.wiring.ts.tpl +1 -1
  69. package/dist/templates/auth/registration.controller.ts.tpl +1 -1
  70. package/dist/templates/auth/session.controller.ts.tpl +1 -1
  71. package/dist/templates/auth/user.model.ts.tpl +1 -1
  72. package/dist/templates/index.d.ts +23 -0
  73. package/dist/templates/index.js +66 -0
  74. package/dist/templates/index.ts +85 -0
  75. package/dist/templates/project/.env.example.tpl +18 -0
  76. package/dist/templates/project/.gitignore.tpl +24 -0
  77. package/dist/templates/project/.npmrc.tpl +4 -0
  78. package/dist/templates/project/AGENTS.md.tpl +210 -0
  79. package/dist/templates/project/CLAUDE.md.tpl +119 -0
  80. package/dist/templates/project/agents/async.md.tpl +218 -0
  81. package/dist/templates/project/agents/data.md.tpl +532 -0
  82. package/dist/templates/project/agents/frontend.md.tpl +201 -0
  83. package/dist/templates/project/agents/realtime.md.tpl +157 -0
  84. package/dist/templates/project/agents/security.md.tpl +92 -0
  85. package/dist/templates/project/agents/testing.md.tpl +101 -0
  86. package/dist/templates/project/agents/web.md.tpl +177 -0
  87. package/dist/templates/project/apps/web/channels/.gitkeep.tpl +1 -0
  88. package/dist/templates/project/apps/web/components/.gitkeep.tpl +1 -0
  89. package/dist/templates/project/apps/web/composables/useApiPing.ts.tpl +25 -0
  90. package/dist/templates/project/apps/web/controllers/home.ts.tpl +19 -0
  91. package/dist/templates/project/apps/web/index.html.tpl +18 -0
  92. package/dist/templates/project/apps/web/layouts/Default.vue.tpl +43 -0
  93. package/dist/templates/project/apps/web/main.ts.tpl +24 -0
  94. package/dist/templates/project/apps/web/pages/Home/Index.vue.tpl +36 -0
  95. package/dist/templates/project/apps/web/routes.ts.tpl +8 -0
  96. package/dist/templates/project/docker-compose.yaml.tpl +73 -0
  97. package/dist/templates/project/domain/events/.gitkeep.tpl +1 -0
  98. package/dist/templates/project/domain/jobs/.gitkeep.tpl +1 -0
  99. package/dist/templates/project/domain/listeners/.gitkeep.tpl +1 -0
  100. package/dist/templates/project/domain/mails/.gitkeep.tpl +1 -0
  101. package/dist/templates/project/domain/models/.gitkeep.tpl +1 -0
  102. package/dist/templates/project/domain/schema/.gitkeep.tpl +1 -0
  103. package/dist/templates/project/domain/services/.gitkeep.tpl +1 -0
  104. package/dist/templates/project/gaon.config.ts.tpl +27 -0
  105. package/dist/templates/project/package.json.tpl +30 -0
  106. package/dist/templates/project/pnpm-workspace.yaml.tpl +11 -0
  107. package/dist/templates/project/shared/components/.gitkeep.tpl +1 -0
  108. package/dist/templates/project/shared/composables/useDebounce.ts.tpl +21 -0
  109. package/dist/templates/project/tsconfig.json.tpl +25 -0
  110. package/dist/templates/project/vite.config.ts.tpl +23 -0
  111. package/dist/tsResolve.js +1 -1
  112. package/dist/work.d.ts +2 -2
  113. package/dist/work.js +3 -1
  114. package/package.json +13 -11
  115. package/dist/__fixtures__/db-minimal/domain/schema/widgets.d.ts +0 -12
  116. package/dist/__fixtures__/db-minimal/domain/schema/widgets.js +0 -7
  117. package/dist/__fixtures__/db-minimal/gaon.config.d.ts +0 -2
  118. package/dist/__fixtures__/db-minimal/gaon.config.js +0 -11
  119. package/dist/check.d.ts +0 -29
  120. package/dist/check.js +0 -92
@@ -0,0 +1,119 @@
1
+ # {{PROJECT_NAME}} — AI 작업 지침
2
+
3
+ 이 파일은 이 프로젝트에서 AI(Claude 등)가 코드를 만질 때 지켜야 할
4
+ 관례를 담는다. Gaon 프레임웍이 이 관례를 스캐폴드(`gaon new`)로 자동
5
+ 심어 두는 이유는 하나다 — 첫 시도부터 프로젝트 관례에 맞게 코드를 짜
6
+ 반복 수정을 없애기 위함(§1.1 AI 첫 시도 성공률).
7
+
8
+ ## 0. 먼저 읽을 것
9
+
10
+ Gaon 프레임웍 문서: https://gaonjs.dev
11
+
12
+ - 설계 정본(v0.15) · errata E-1(파사드 = `gaonjs`) · E-3(JSON 액션 +
13
+ `api()`) · E-4(컬럼 확장) · E-5(컴포저블·레이아웃)
14
+ - 앱 내 One Way(§1) — 선택지가 있는 것을 만들지 않는다. 하나로 정한다.
15
+
16
+ ## 1. 절대 규칙 (12개 · 프레임웍 정본에서 유래)
17
+
18
+ 1. **TypeScript 전용.** JS 파일 추가 금지. 데코레이터 금지 — 함수·객체
19
+ 스타일(`model()`·`controller()`·`job()`) 만 쓴다.
20
+ 2. **`.gaon/` 자동 생성 파일 편집 금지.** `routes.d.ts`·`tables.d.ts`
21
+ 는 `gaon check` / `gaon dev` 가 재생성한다.
22
+ 3. **의존 방향 4규칙**(doctor 강제): 앱→domain 허용 · domain→앱 금지 ·
23
+ 앱→앱 금지 · 앱→shared 허용(shared 는 앱 import 금지, domain 은
24
+ 타입 import 만).
25
+ 4. **PM2·pnpm dev-server 금지.** 클러스터는 `node:cluster` 내장이 유일.
26
+ 컨테이너 기본 워커 1.
27
+ 5. **프론트엔드는 프로젝트당 하나.** v1 은 Vue 단일. 앱 간 Vue/React
28
+ 혼용 금지.
29
+ 6. **보안 기본값(CORS·rate limit·CSRF)은 켠 채로 둔다.** 끄는 것은
30
+ 명시적 설정으로만.
31
+ 7. **멀티 DB 커넥션 규칙**(§4.5): 스키마는 `{ db: '키' }` 로 바인딩,
32
+ 생략 = main. 커넥션을 가로지르는 belongsTo 금지, 서비스 트랜잭션은
33
+ 단일 커넥션에서만 원자적. MongoDB 는 v1 구현 금지.
34
+ 8. **테스트는 Docker 실 인프라 필수 · DB·NATS 목업 절대 금지**(§9).
35
+ SQLite 인메모리·NATS 목업으로 테스트 돌리는 코드 만들지 말 것.
36
+ 9. **실시간은 v1 포함**(§7): 웹서버 ↔ 허브는 TCP 지속 연결 · NATS 는
37
+ broadcast 전용(errata E-2). 운영 프로세스는 serve·work·hub 3종.
38
+ 10. **인증·폼은 Inertia SPA**(§6 · SSR 아님). 로그인/회원가입은
39
+ `this.render('auth/Login')` + `Inertia.post()` → 서버 redirect.
40
+ REST + `fetch()` 는 API 앱(JWT) 전용.
41
+ 11. **컴포저블·레이아웃**(errata E-5): 컴포저블은 컴포넌트와 대칭
42
+ (`apps/<앱>/composables/` + `shared/composables/`, `use` 접두사).
43
+ shared 컴포저블은 인자로만 받는 순수 로직(api·pageProps 금지).
44
+ 레이아웃은 `apps/<앱>/layouts/Default.vue` 존재 시 자동 적용.
45
+ **자동 import 금지** — 모든 import 는 명시적으로.
46
+ 12. **JSON 액션 + 타입드 `api()`**(errata E-3): 페이지와 무관한 데이터
47
+ 요청은 컨트롤러의 JSON 액션(반환값 = 응답) · Vue 는 `api()`
48
+ 클라이언트로 호출. `this.params()` 출처 우선순위 = 라우트 > body >
49
+ query(고정). 출처 명시는 `this.body()`·`this.query()`.
50
+
51
+ ## 2. 프로젝트 구조
52
+
53
+ ```
54
+ {{PROJECT_NAME}}/
55
+ ├─ apps/ 앱마다 폴더 = URL 프리픽스
56
+ │ └─ web/ web 앱 (프리픽스 '/')
57
+ │ ├─ app.config.ts (선택) 앱 오버라이드
58
+ │ ├─ routes.ts 라우트 정의
59
+ │ ├─ controllers/ 컨트롤러 (Rails 관례)
60
+ │ ├─ pages/ Vue 페이지 (Inertia SPA)
61
+ │ ├─ components/ 앱 전용 컴포넌트
62
+ │ ├─ composables/ 앱 전용 컴포저블 (E-5)
63
+ │ ├─ layouts/ 앱별 레이아웃 (E-5 · Default.vue 자동)
64
+ │ └─ channels/ (선택) 실시간 채널 (M6)
65
+ ├─ domain/ 비즈니스 로직 · 앱 간 공유
66
+ │ ├─ schema/ 테이블 스키마 (@gaonjs/data)
67
+ │ ├─ models/ 모델 (조회·연관·훅)
68
+ │ ├─ services/ 트랜잭션 · 규칙
69
+ │ ├─ jobs/ 비동기 잡 (@gaonjs/async)
70
+ │ ├─ events/ 이벤트 정의
71
+ │ ├─ listeners/ 이벤트 리스너
72
+ │ ├─ mails/ 메일 (@gaonjs/mail)
73
+ │ └─ schedule.ts (선택) 크론 스케줄
74
+ ├─ shared/ 앱 간 공용 (props로만)
75
+ │ ├─ components/ shared 컴포넌트 (순수 UI)
76
+ │ └─ composables/ shared 컴포저블 (인자로만 · E-5)
77
+ ├─ gaon.config.ts 루트 설정 (DB · Redis · NATS · ...)
78
+ ├─ docker-compose.yaml 개발 인프라 (gaon dev 자동 기동)
79
+ ├─ .env.example env 템플릿 (cp .env.example .env)
80
+ └─ package.json 개발자는 gaonjs 하나만 설치
81
+ ```
82
+
83
+ ## 3. 개발 검증 루프 (작업마다 실행)
84
+
85
+ ```bash
86
+ gaon check # .gaon 재생성 후 타입 검사 (CI 정합)
87
+ gaon doctor # 정적 검사 5종 (응답·N+1·의존·커넥션·마이그)
88
+ npm test # Vitest · DB 테스트는 실 Docker 필수 (§9)
89
+ ```
90
+
91
+ ## 4. 작업 관례
92
+
93
+ - **추론 금지 · 사실 검증.** 확인 안 된 것은 실행·측정으로 검증하거나
94
+ 사용자에게 묻는다.
95
+ - **주석은 개발자가 단 것처럼.** AI가 단 티가 나는 주석(`// AI 판단…`
96
+ `// TODO(AI)` 등) 금지. 기술적 이유(왜 이 코드가 이런지, 특수 케이스
97
+ 근거) 만 담는다.
98
+ - **파일 작게 · 역할은 하나로.** 영리한 코드보다 읽히는 코드.
99
+ - **에러 메시지**(§7.5.3): "어느 파일에 무엇을 추가/수정하고 어떤
100
+ 명령을 실행하라" 까지 쓴다.
101
+
102
+ ## 5. 자주 쓰는 명령
103
+
104
+ ```bash
105
+ gaon dev # 개발 (Docker · 타입 브리지 · watch)
106
+ gaon serve # 서버만 (운영 프로세스 1/3)
107
+ gaon work # 워커 (잡·리스너·아웃박스)
108
+ gaon hub # 실시간 허브 (프레즌스 · 리더 선출 HA)
109
+ gaon g controller <name> # 컨트롤러 스캐폴드
110
+ gaon g model <Name> # 스키마 + 모델 스캐폴드 (E-4)
111
+ gaon g page <Path>/<Name> # Vue 페이지 (Inertia SPA · pageProps)
112
+ gaon g job <Name> # 비동기 잡
113
+ gaon g auth # 인증 스캐폴드 (세션 + JWT 옵션)
114
+ gaon db diff # 스키마 ↔ DB 차이 (적용 X)
115
+ gaon db migrate # 실제 적용 + _gaon_migrations 이력
116
+ gaon db seed # domain/seed.ts 실행
117
+ ```
118
+
119
+ 문서 · 진행 상황: https://gaonjs.dev
@@ -0,0 +1,218 @@
1
+ # agents/async.md — 비동기 (잡 · 이벤트 · 리스너 · 아웃박스 · 스케줄러)
2
+
3
+ > 골격: **정본 규칙 → 정본 예시 → 알려진 함정 → 관련 결정 번호** (결정 40 · 2층 구조).
4
+ > 루트 `AGENTS.md` 는 코어 요약만 담는다 — 시그니처·표·예시의 정본은 이 파일이다.
5
+ > 대상 패키지: `@gaonjs/async` (파사드 import 는 `gaonjs/async`). 백본 = NATS JetStream ·
6
+ > 실행은 워커 프로세스(`gaon work`).
7
+
8
+ ## 정본 규칙
9
+
10
+ ### 1. 잡 (`job()`) (`packages/async/src/jobs.ts:146-185`)
11
+
12
+ 잡은 도메인 소속이다 — `domain/jobs/*.ts` 에 파일을 놓으면 등록이고,
13
+ 어느 앱에서 큐잉하든 같은 워커(`gaon work`)가 처리한다. 모델·서비스와
14
+ 같은 함수/객체 스타일(데코레이터 금지).
15
+
16
+ ```ts
17
+ // domain/jobs/sendWelcomeMail.ts — 파일명 camelCase (루트 §네이밍)
18
+ import { job } from 'gaonjs/async'
19
+
20
+ export const SendWelcomeMail = job(async (userId: bigint) => {
21
+ // 실 발송 로직 (예: gaonjs/mail 사용)
22
+ }, { retries: 3 })
23
+ ```
24
+
25
+ ```ts
26
+ await SendWelcomeMail.later(user.id) // 즉시 큐잉(인자 타입 그대로 추론)
27
+ await SendWelcomeMail.in('10m', user.id) // 지연 실행
28
+ await SendWelcomeMail.at(someDate, user.id) // 특정 시각 실행
29
+ ```
30
+
31
+ - **시그니처** — `job(handler, options?)`. 첫 인자는 평범한 async
32
+ 함수(`(...args) => Promise<void> | void`) — `defineJob` 이나
33
+ `{ perform }` 객체 형태가 아니다.
34
+ - **이름** — `options.name` 으로 명시하거나, 생략하면 `domain/jobs/`
35
+ 파일 로더가 **파일명**으로 채운다(`assignName`). 이름을 얻기 전까지
36
+ `.later()` 등을 호출하면 에러 — 파일로 두거나 `name` 을 직접 준다.
37
+ - **옵션** — `queue`(기본 `'default'`) · `retries`(기본 3) ·
38
+ `curve`(백오프 곡선 ms) · `jitter` · `concurrency`.
39
+ - **실패** — 재시도를 소진하면 DLQ 로 간다. `gaon jobs list --failed` ·
40
+ `gaon jobs retry <id>` 로 조회·재적재한다.
41
+ - 기본 백오프 곡선은 `[1s, 5s, 30s, 5m, 1h]` + 지터 0.2, 재시도 3
42
+ (총 4시도) — §7 · 벤치마크로 확정된 값.
43
+
44
+ ### 2. 잡 발행 위치 (결정 32 · 2026-07-24 종결)
45
+
46
+ 잡(`.later(...)`)은 **컨트롤러 · 서비스 · 리스너 어디서든 발행
47
+ 가능**하다. The One Way 로 발행 위치를 강제하지 않는다 — 사용 맥락에
48
+ 따라 선택한다 (v0.16 §12 결정 32).
49
+
50
+ - **컨트롤러** — 요청 응답 시 즉시 발행 (예: 회원가입 응답 후 환영
51
+ 메일 잡):
52
+
53
+ ```ts
54
+ async create() {
55
+ const user = await User.create(this.params(User.registerForm))
56
+ await SendWelcomeMail.later(user.id) // 컨트롤러에서 발행
57
+ return this.redirect('/dashboard')
58
+ }
59
+ ```
60
+
61
+ - **서비스** — 여러 컨트롤러 재사용 · 트랜잭션 · 복잡 로직:
62
+
63
+ ```ts
64
+ // domain/services/registerUser.ts — service() 상세는 agents/data.md §9
65
+ import { service } from 'gaonjs/service'
66
+
67
+ export const RegisterUser = service(async (input: RegisterInput) => {
68
+ const user = await User.create(input)
69
+ await SendWelcomeMail.later(user.id) // 서비스에서 발행
70
+ return user
71
+ })
72
+ // 컨트롤러에서: const user = await RegisterUser.call(input)
73
+ ```
74
+
75
+ - **리스너** — 이벤트 반응 (예: OrderPlaced 이벤트 → EmailReceipt 잡).
76
+
77
+ **어디서 발행하든 정합이다.** 컨트롤러 발행 = 즉시성 · 서비스 발행 =
78
+ 재사용성 · 리스너 발행 = 이벤트 기반. DB 커밋과 정합이 필요하면
79
+ 서비스 `afterCommit()`(`agents/data.md` §9) 또는 아웃박스(§4)를 쓴다.
80
+
81
+ ### 3. 이벤트와 리스너
82
+
83
+ 이벤트는 페이로드 shape 에서 타입이 **구조적으로** 추론된다. 리스너는
84
+ `domain/listeners/*.ts` 의 default export 이고, durable 컨슈머 id 는
85
+ **파일명**에서 채워진다(재시작해도 같은 컨슈머).
86
+
87
+ ```ts
88
+ // domain/events/orderPlaced.ts
89
+ import { event } from 'gaonjs/async'
90
+ import { t } from 'gaonjs/data'
91
+
92
+ export const OrderPlaced = event('order.placed', {
93
+ orderId: t.bigint(),
94
+ })
95
+ ```
96
+
97
+ ```ts
98
+ // domain/listeners/notifyAdmin.ts
99
+ import { on } from 'gaonjs/async'
100
+ import { OrderPlaced } from '../events/orderPlaced.js'
101
+
102
+ export default on(OrderPlaced, ({ orderId }) => {
103
+ // orderId 는 bigint 로 추론됨
104
+ })
105
+ ```
106
+
107
+ 이벤트 발행:
108
+
109
+ ```ts
110
+ await OrderPlaced.emit({ orderId: 1n })
111
+ ```
112
+
113
+ 여러 리스너가 같은 이벤트를 durable 컨슈머로 구독하며, 각자 재시도된다.
114
+
115
+ ### 4. 아웃박스 (트랜잭션 정합)
116
+
117
+ 이벤트를 DB 트랜잭션과 **원자적으로** 발행하려면 아웃박스를 쓴다.
118
+ `runInTransaction` 안에서 발행한 이벤트는 같은 트랜잭션의 아웃박스
119
+ 테이블에 스테이징되고, 트랜잭션이 커밋돼야 릴레이가 실제로 NATS 에
120
+ 발행한다. 트랜잭션이 롤백되면 이벤트도 사라진다.
121
+
122
+ ```ts
123
+ import { runInTransaction } from 'gaonjs/async'
124
+
125
+ await runInTransaction(async () => {
126
+ await Order.create({ /* … */ })
127
+ await OrderPlaced.emit({ orderId }) // 커밋돼야 실제 발행됨
128
+ })
129
+ ```
130
+
131
+ - 트랜잭션 안의 `emit` 은 `AsyncLocalStorage` 로 투명하게 감지돼
132
+ 아웃박스에 스테이징된다(별도 API 호출 불필요).
133
+ - 릴레이(`gaon work` 내장)가 `SKIP LOCKED` 로 아웃박스를 폴링해 발행
134
+ 한다 (기본 폴 1000ms · 배치 100).
135
+ - at-least-once — 발행 후 표시하므로 중복 가능성이 있고, dedup(msgID)이
136
+ 흡수한다.
137
+ - 아웃박스 테이블(`_gaon_outbox`)은 코어 내장이며 워커 기동 시 보장된다.
138
+
139
+ ### 5. 스케줄러
140
+
141
+ `domain/schedule.ts` 에서 반복 작업을 선언한다. 실행 대상은 **항상 잡**
142
+ 이다(인라인 함수 금지). 여러 워커가 떠 있어도 **리더로 선출된 하나**만
143
+ 발행하므로 중복 실행이 없다.
144
+
145
+ ```ts
146
+ // domain/schedule.ts
147
+ import { schedule } from 'gaonjs/async'
148
+ import { CleanupExpiredSessions } from './jobs/cleanupExpiredSessions.js'
149
+ import { SendDailyDigest } from './jobs/sendDailyDigest.js'
150
+ import { SendWeeklyReport } from './jobs/sendWeeklyReport.js'
151
+
152
+ export default schedule((s) => {
153
+ s.every('10m', CleanupExpiredSessions) // 주기 실행
154
+ s.daily.at('04:00', SendDailyDigest) // 매일 특정 시각
155
+ s.cron('0 9 * * 1', SendWeeklyReport) // 크론 표현식 (5필드)
156
+ })
157
+ ```
158
+
159
+ | 빌더 | 설명 |
160
+ | --- | --- |
161
+ | `s.every(interval, Job)` | `'10m'`·`'700ms'` 등 주기 또는 ms |
162
+ | `s.daily.at('HH:MM', Job)` | 매일 지정 시각 |
163
+ | `s.cron('분 시 일 월 요일', Job)` | 5필드 크론 표현식 |
164
+
165
+ ### 6. 워커 프로세스 (`gaon work`)
166
+
167
+ 잡·리스너·스케줄러·아웃박스 릴레이를 한 프로세스로 조립한다. 운영
168
+ 프로세스 3종(serve·work·hub) 중 하나. SIGTERM/SIGINT 에 graceful
169
+ drain — 스케줄러 리더를 반납하고 진행 중인 잡을 완료한 뒤 종료한다.
170
+
171
+ ## 정본 예시
172
+
173
+ 회원 가입 → 환영 메일 비동기 발송 세로 조각 (§7 원문 예시):
174
+
175
+ ```ts
176
+ // domain/jobs/sendWelcomeMail.ts
177
+ import { job } from 'gaonjs/async'
178
+
179
+ export const SendWelcomeMail = job(async (userId: bigint) => {
180
+ // 실 발송 (stub 도 OK — 파일 관례가 관건)
181
+ }, { retries: 3 })
182
+ ```
183
+
184
+ ```ts
185
+ // apps/web/controllers/registration.ts — 컨트롤러는 잡 발행만 (직접 발송 금지)
186
+ async create() {
187
+ const user = await RegisterUser.call(this.params(RegisterUser.form))
188
+ await SendWelcomeMail.later(user.id)
189
+ return this.redirect('/dashboard')
190
+ }
191
+ ```
192
+
193
+ 발행·처리를 검증하는 실 NATS 테스트는 `agents/testing.md` (결정 42 ·
194
+ `expectJobProcessed`) 를 따른다.
195
+
196
+ ## 알려진 함정
197
+
198
+ - **컨트롤러에서 메일·외부 발송 직접 호출 = 함정** — 컨트롤러는 잡
199
+ 발행만. nodemailer·resend·@sendgrid/mail 직접 import 금지.
200
+ - **클래스형 잡·데코레이터(`@Job`·`@Processor`) 금지** — `job()` 함수형만.
201
+ - **잡 파일 위치는 `domain/jobs/`** — 앱 폴더가 아니다 (잡은 도메인
202
+ 소속 · 어느 앱에서든 큐잉).
203
+ - **`export const <Pascal> = job(...)`** — export 없이 정의만 하면
204
+ 컨트롤러가 import 해 `.later()` 를 부를 수 없다.
205
+ - **스케줄 대상은 항상 잡** — `s.every('10m', async () => ...)` 인라인
206
+ 함수 금지.
207
+ - **커밋 전 발행 주의** — 트랜잭션 안에서 DB 확정 후에만 나가야 하는
208
+ 발행은 `afterCommit()` 또는 아웃박스로.
209
+ - **테스트에서 NATS 목업 금지** (§9) — 실 JetStream 에 접속한다
210
+ (`agents/testing.md`).
211
+
212
+ ## 관련 결정 번호
213
+
214
+ | 결정 | 내용 |
215
+ |---|---|
216
+ | 결정 32 | 잡 발행 위치 자유 (컨트롤러·서비스·리스너 모두 정합) |
217
+ | 결정 42 | 비동기 테스트 헬퍼 `expectJobProcessed` (`agents/testing.md`) |
218
+ | §7 | 비동기 배터리 원문 (백오프 기본값 = M7 벤치마크 확정) |