@wooojin/forgen 0.4.7 → 0.4.9

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 (159) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/CHANGELOG.md +40 -0
  3. package/assets/dev-guide/be/README.md +226 -0
  4. package/assets/dev-guide/be/adapters/build-agents-md.sh +63 -0
  5. package/assets/dev-guide/be/principles/common.md +433 -0
  6. package/assets/dev-guide/be/principles/go.md +469 -0
  7. package/assets/dev-guide/be/principles/node.md +388 -0
  8. package/assets/dev-guide/be/skills/go/be-build/SKILL.md +262 -0
  9. package/assets/dev-guide/be/skills/go/be-perf/SKILL.md +308 -0
  10. package/assets/dev-guide/be/skills/go/be-review/SKILL.md +119 -0
  11. package/assets/dev-guide/be/skills/go/be-security/SKILL.md +362 -0
  12. package/assets/dev-guide/be/skills/node/be-build/SKILL.md +239 -0
  13. package/assets/dev-guide/be/skills/node/be-perf/SKILL.md +272 -0
  14. package/assets/dev-guide/be/skills/node/be-review/SKILL.md +118 -0
  15. package/assets/dev-guide/be/skills/node/be-security/SKILL.md +355 -0
  16. package/assets/dev-guide/be/sources/12factor/INDEX.md +53 -0
  17. package/assets/dev-guide/be/sources/api-design/INDEX.md +56 -0
  18. package/assets/dev-guide/be/sources/ddia/INDEX.md +55 -0
  19. package/assets/dev-guide/be/sources/go-runtime/INDEX.md +62 -0
  20. package/assets/dev-guide/be/sources/node-runtime/INDEX.md +60 -0
  21. package/assets/dev-guide/be/sources/otel/INDEX.md +53 -0
  22. package/assets/dev-guide/be/sources/owasp-api/INDEX.md +52 -0
  23. package/assets/dev-guide/be/sources/postgres/INDEX.md +55 -0
  24. package/assets/dev-guide/be/sources/sre-book/INDEX.md +48 -0
  25. package/assets/dev-guide/fe/README.md +197 -0
  26. package/assets/dev-guide/fe/adapters/build-agents-md.sh +63 -0
  27. package/assets/dev-guide/fe/adapters/refresh.sh +68 -0
  28. package/assets/dev-guide/fe/principles/common.md +160 -0
  29. package/assets/dev-guide/fe/principles/react.md +183 -0
  30. package/assets/dev-guide/fe/principles/vue.md +196 -0
  31. package/assets/dev-guide/fe/skills/react/fe-build/SKILL.md +139 -0
  32. package/assets/dev-guide/fe/skills/react/fe-perf/SKILL.md +179 -0
  33. package/assets/dev-guide/fe/skills/react/fe-review/SKILL.md +141 -0
  34. package/assets/dev-guide/fe/skills/vue/fe-build/SKILL.md +148 -0
  35. package/assets/dev-guide/fe/skills/vue/fe-perf/SKILL.md +163 -0
  36. package/assets/dev-guide/fe/skills/vue/fe-review/SKILL.md +136 -0
  37. package/assets/dev-guide/fe/sources/a11y-dx/INDEX.md +41 -0
  38. package/assets/dev-guide/fe/sources/a11y-dx/chrome-devtools-memory.md +150 -0
  39. package/assets/dev-guide/fe/sources/a11y-dx/chrome-devtools-performance.md +99 -0
  40. package/assets/dev-guide/fe/sources/a11y-dx/lighthouse-audits.md +146 -0
  41. package/assets/dev-guide/fe/sources/a11y-dx/react-devtools-profiler.md +128 -0
  42. package/assets/dev-guide/fe/sources/a11y-dx/wcag22-new-criteria.md +174 -0
  43. package/assets/dev-guide/fe/sources/perf/01-core-web-vitals.md +58 -0
  44. package/assets/dev-guide/fe/sources/perf/02-inp.md +83 -0
  45. package/assets/dev-guide/fe/sources/perf/03-lcp-cls.md +130 -0
  46. package/assets/dev-guide/fe/sources/perf/04-speculation-rules.md +148 -0
  47. package/assets/dev-guide/fe/sources/perf/05-view-transitions.md +153 -0
  48. package/assets/dev-guide/fe/sources/perf/06-nextjs-caching.md +188 -0
  49. package/assets/dev-guide/fe/sources/perf/07-server-components.md +181 -0
  50. package/assets/dev-guide/fe/sources/perf/08-ppr.md +133 -0
  51. package/assets/dev-guide/fe/sources/perf/09-nextjs-image.md +200 -0
  52. package/assets/dev-guide/fe/sources/perf/10-optimize-lcp.md +201 -0
  53. package/assets/dev-guide/fe/sources/perf/INDEX.md +88 -0
  54. package/assets/dev-guide/fe/sources/react/INDEX.md +41 -0
  55. package/assets/dev-guide/fe/sources/react/keeping-components-pure.md +135 -0
  56. package/assets/dev-guide/fe/sources/react/no-effect-patterns.md +183 -0
  57. package/assets/dev-guide/fe/sources/react/react-compiler.md +182 -0
  58. package/assets/dev-guide/fe/sources/react/server-components.md +194 -0
  59. package/assets/dev-guide/fe/sources/react/server-functions.md +192 -0
  60. package/assets/dev-guide/fe/sources/react/suspense.md +218 -0
  61. package/assets/dev-guide/fe/sources/react/use-action-state.md +123 -0
  62. package/assets/dev-guide/fe/sources/react/use-form-status.md +158 -0
  63. package/assets/dev-guide/fe/sources/react/use-hook.md +153 -0
  64. package/assets/dev-guide/fe/sources/react/use-optimistic.md +194 -0
  65. package/assets/dev-guide/fe/sources/toss-ff/INDEX.md +58 -0
  66. package/assets/dev-guide/fe/sources/toss-ff/cohesion-code-directory.md +79 -0
  67. package/assets/dev-guide/fe/sources/toss-ff/cohesion-form-fields.md +110 -0
  68. package/assets/dev-guide/fe/sources/toss-ff/cohesion-magic-number.md +47 -0
  69. package/assets/dev-guide/fe/sources/toss-ff/coupling-item-edit-modal.md +124 -0
  70. package/assets/dev-guide/fe/sources/toss-ff/coupling-use-bottom-sheet.md +57 -0
  71. package/assets/dev-guide/fe/sources/toss-ff/coupling-use-page-state.md +71 -0
  72. package/assets/dev-guide/fe/sources/toss-ff/overview-4-principles.md +77 -0
  73. package/assets/dev-guide/fe/sources/toss-ff/predictability-hidden-logic.md +59 -0
  74. package/assets/dev-guide/fe/sources/toss-ff/predictability-http.md +77 -0
  75. package/assets/dev-guide/fe/sources/toss-ff/predictability-use-user.md +110 -0
  76. package/assets/dev-guide/fe/sources/toss-ff/readability-comparison-order.md +52 -0
  77. package/assets/dev-guide/fe/sources/toss-ff/readability-condition-name.md +64 -0
  78. package/assets/dev-guide/fe/sources/toss-ff/readability-login-start-page.md +183 -0
  79. package/assets/dev-guide/fe/sources/toss-ff/readability-magic-number.md +53 -0
  80. package/assets/dev-guide/fe/sources/toss-ff/readability-submit-button.md +73 -0
  81. package/assets/dev-guide/fe/sources/toss-ff/readability-ternary-operator.md +38 -0
  82. package/assets/dev-guide/fe/sources/toss-ff/readability-use-page-state.md +77 -0
  83. package/assets/dev-guide/fe/sources/toss-ff/readability-user-policy.md +98 -0
  84. package/assets/dev-guide/fe/sources/vue/INDEX.md +17 -0
  85. package/assets/dev-guide/fe/sources/vue/composition-api.md +251 -0
  86. package/assets/dev-guide/fe/sources/vue/nuxt-data-fetching.md +232 -0
  87. package/assets/dev-guide/fe/sources/vue/pinia-state-management.md +134 -0
  88. package/assets/dev-guide/fe/sources/vue/reactivity-pitfalls.md +261 -0
  89. package/assets/dev-guide/fe/sources/vue/style-guide-priority-a.md +117 -0
  90. package/assets/dev-guide/fe/sources/vue/style-guide-priority-b.md +231 -0
  91. package/assets/dev-guide/fe/sources/vue/style-guide-priority-c.md +86 -0
  92. package/assets/dev-guide/fe/sources/vue/style-guide-priority-d.md +72 -0
  93. package/dist/checks/self-score-deflation.js +6 -4
  94. package/dist/cli.js +47 -2
  95. package/dist/core/auto-compound-runner.js +6 -2
  96. package/dist/core/dashboard-cli.d.ts +12 -0
  97. package/dist/core/dashboard-cli.js +226 -0
  98. package/dist/core/dashboard.js +2 -2
  99. package/dist/core/dev-guide-injector.d.ts +26 -0
  100. package/dist/core/dev-guide-injector.js +137 -0
  101. package/dist/core/doctor.d.ts +10 -0
  102. package/dist/core/doctor.js +49 -8
  103. package/dist/core/harness.js +8 -2
  104. package/dist/core/init.js +53 -0
  105. package/dist/core/inspect-cli.js +4 -4
  106. package/dist/core/lifecycle-classifier.d.ts +23 -0
  107. package/dist/core/lifecycle-classifier.js +104 -0
  108. package/dist/core/migrate-evidence-host.js +1 -1
  109. package/dist/core/notify.js +7 -0
  110. package/dist/core/observability-backfill.d.ts +31 -0
  111. package/dist/core/observability-backfill.js +178 -0
  112. package/dist/core/observability-store.d.ts +58 -0
  113. package/dist/core/observability-store.js +195 -0
  114. package/dist/core/paths.d.ts +16 -2
  115. package/dist/core/paths.js +16 -2
  116. package/dist/core/session-store.d.ts +12 -1
  117. package/dist/core/session-store.js +77 -1
  118. package/dist/core/spawn.d.ts +17 -0
  119. package/dist/core/spawn.js +191 -8
  120. package/dist/core/statusline-cli.js +34 -1
  121. package/dist/core/v1-bootstrap.d.ts +7 -0
  122. package/dist/core/v1-bootstrap.js +28 -6
  123. package/dist/engine/compound-extractor.js +40 -1
  124. package/dist/engine/compound-loop.js +6 -0
  125. package/dist/engine/compound-retire.d.ts +20 -0
  126. package/dist/engine/compound-retire.js +85 -0
  127. package/dist/engine/learn-cli.js +2 -2
  128. package/dist/engine/lifecycle/bypass-detector.js +3 -2
  129. package/dist/engine/lifecycle/meta-reclassifier.js +1 -1
  130. package/dist/engine/lifecycle/signals.js +2 -2
  131. package/dist/engine/lifecycle/trigger-t1-correction.js +1 -1
  132. package/dist/engine/solution-candidate.js +1 -1
  133. package/dist/engine/solution-outcomes.js +1 -1
  134. package/dist/engine/solution-quarantine.js +1 -1
  135. package/dist/engine/solution-weakness.js +8 -2
  136. package/dist/forge/cli.js +1 -1
  137. package/dist/hooks/context-guard.js +25 -1
  138. package/dist/hooks/keyword-detector.js +1 -1
  139. package/dist/hooks/post-tool-use.js +48 -0
  140. package/dist/hooks/secret-filter.js +2 -2
  141. package/dist/hooks/shared/hook-response.js +1 -1
  142. package/dist/hooks/shared/hook-timing.js +3 -3
  143. package/dist/hooks/solution-injector.js +94 -1
  144. package/dist/hooks/stop-guard.js +3 -3
  145. package/dist/host/install-claude.d.ts +6 -2
  146. package/dist/host/install-claude.js +74 -2
  147. package/dist/host/install-codex.d.ts +4 -0
  148. package/dist/host/install-codex.js +72 -1
  149. package/dist/host/install-orchestrator.js +1 -0
  150. package/dist/mcp/tools.js +1 -1
  151. package/dist/preset/facet-catalog.js +2 -2
  152. package/dist/renderer/rule-renderer.js +7 -7
  153. package/dist/store/compound-usage-store.js +1 -1
  154. package/dist/store/implicit-feedback-store.js +2 -2
  155. package/dist/store/profile-store.d.ts +11 -0
  156. package/dist/store/profile-store.js +23 -0
  157. package/package.json +6 -6
  158. package/plugin.json +1 -1
  159. package/scripts/postinstall.js +134 -0
@@ -0,0 +1,388 @@
1
+ ---
2
+ title: Node.js + TypeScript 원칙
3
+ version: 2026-05-18
4
+ sources:
5
+ - sources/node-runtime/
6
+ ---
7
+
8
+ # Node.js + TypeScript 원칙
9
+
10
+ > [공통 원칙](./common.md)을 먼저 따르고, 아래는 Node.js/TypeScript 특화.
11
+
12
+ ## N0. 의사결정 우선순위
13
+
14
+ 1. **비동기는 async/await 일관성** — callback 혼용 금지
15
+ 2. **process 안정성** — unhandledRejection / uncaughtException 반드시 처리
16
+ 3. **Event Loop 보호** — CPU heavy는 worker_threads로 격리
17
+ 4. **입력 경계 강화** — Zod/io-ts 런타임 검증
18
+ 5. **TypeScript strict** — 타입 시스템을 안전망으로 최대 활용
19
+
20
+ ---
21
+
22
+ ## N1. async/await 일관성
23
+
24
+ **callback과 async/await를 혼용하지 마라. async/await로 통일한다.**
25
+
26
+ ### N1.1 기본 규칙
27
+
28
+ ```typescript
29
+ // WRONG: Promise + callback 혼용
30
+ function fetchUser(id: string, callback: (err: Error | null, user?: User) => void) {
31
+ db.query('SELECT * FROM users WHERE id = ?', [id])
32
+ .then(result => callback(null, result))
33
+ .catch(err => callback(err));
34
+ }
35
+
36
+ // RIGHT: async/await 통일
37
+ async function fetchUser(id: string): Promise<User> {
38
+ const result = await db.query('SELECT * FROM users WHERE id = ?', [id]);
39
+ return result;
40
+ }
41
+ ```
42
+
43
+ ### N1.2 기존 callback API 래핑
44
+
45
+ Node.js 레거시 API (`fs.readFile` 등)는 `util.promisify` 또는 `fs/promises` 사용:
46
+
47
+ ```typescript
48
+ import { readFile } from 'node:fs/promises'; // 모던 방식
49
+ // 또는
50
+ import { promisify } from 'node:util';
51
+ import { readFile } from 'node:fs';
52
+ const readFileAsync = promisify(readFile); // 레거시 래핑
53
+ ```
54
+
55
+ ### N1.3 병렬 처리
56
+
57
+ 독립적인 async 작업은 `Promise.all` / `Promise.allSettled`:
58
+
59
+ ```typescript
60
+ // WRONG: 순차 await (불필요한 직렬화)
61
+ const user = await fetchUser(userId);
62
+ const orders = await fetchOrders(userId);
63
+
64
+ // RIGHT: 병렬 실행
65
+ const [user, orders] = await Promise.all([fetchUser(userId), fetchOrders(userId)]);
66
+
67
+ // 일부 실패 허용 시
68
+ const results = await Promise.allSettled([fetchUser(userId), fetchOrders(userId)]);
69
+ results.forEach(result => {
70
+ if (result.status === 'rejected') logger.warn('fetch failed', result.reason);
71
+ });
72
+ ```
73
+
74
+ ---
75
+
76
+ ## N2. Top-Level Error Handler
77
+
78
+ **process 수준 에러를 반드시 처리한다. 처리하지 않으면 서비스가 조용히 죽는다.**
79
+
80
+ ```typescript
81
+ // app.ts — 진입점에서 최우선 등록
82
+ process.on('unhandledRejection', (reason, promise) => {
83
+ logger.error({ reason, promise }, 'Unhandled Promise Rejection');
84
+ // graceful exit: 진행 중인 요청 처리 후 종료
85
+ gracefulShutdown(1);
86
+ });
87
+
88
+ process.on('uncaughtException', (err, origin) => {
89
+ logger.fatal({ err, origin }, 'Uncaught Exception — process will exit');
90
+ // uncaughtException 후 프로세스 상태 보장 불가 → 즉시 종료
91
+ process.exit(1);
92
+ });
93
+
94
+ // Graceful shutdown: SIGTERM (Docker/K8s 종료 신호)
95
+ process.on('SIGTERM', () => {
96
+ logger.info('SIGTERM received, starting graceful shutdown');
97
+ gracefulShutdown(0);
98
+ });
99
+
100
+ async function gracefulShutdown(exitCode: number) {
101
+ // 1. 새 요청 거부 (load balancer 헬스체크 실패 대기)
102
+ server.close();
103
+ // 2. 진행 중 요청 완료 대기 (max 30s)
104
+ await new Promise(resolve => setTimeout(resolve, 30_000));
105
+ // 3. DB 연결 종료
106
+ await db.destroy();
107
+ process.exit(exitCode);
108
+ }
109
+ ```
110
+
111
+ ### N2.1 Express/Fastify 에러 핸들러
112
+
113
+ ```typescript
114
+ // Express 에러 핸들러 (4인자 필수)
115
+ app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
116
+ const statusCode = err instanceof AppError ? err.statusCode : 500;
117
+ logger.error({ err, requestId: req.id, path: req.path }, 'Request error');
118
+ res.status(statusCode).json({
119
+ error: {
120
+ code: err instanceof AppError ? err.code : 'INTERNAL_ERROR',
121
+ message: statusCode < 500 ? err.message : '서버 오류가 발생했습니다',
122
+ requestId: req.id,
123
+ },
124
+ });
125
+ });
126
+
127
+ // Fastify는 setErrorHandler 사용
128
+ fastify.setErrorHandler((err, request, reply) => {
129
+ request.log.error({ err }, 'Request error');
130
+ reply.status(err.statusCode ?? 500).send({ error: { ... } });
131
+ });
132
+ ```
133
+
134
+ ---
135
+
136
+ ## N3. Event Loop 보호
137
+
138
+ **Node.js는 단일 스레드. CPU heavy 작업이 Event Loop를 막으면 모든 요청이 멈춘다.**
139
+
140
+ ### N3.1 Event Loop 차단 탐지
141
+
142
+ ```typescript
143
+ // 차단 탐지: toobusy-js 또는 clinic.js
144
+ import toobusy from 'toobusy-js';
145
+ app.use((req, res, next) => {
146
+ if (toobusy()) {
147
+ res.status(503).json({ error: { code: 'SERVICE_UNAVAILABLE', message: '일시적 과부하' } });
148
+ return;
149
+ }
150
+ next();
151
+ });
152
+ ```
153
+
154
+ ### N3.2 CPU Heavy 작업 격리
155
+
156
+ ```typescript
157
+ // worker_threads로 CPU 집약 작업 격리
158
+ import { Worker, workerData, parentPort } from 'node:worker_threads';
159
+ import { resolve } from 'node:path';
160
+
161
+ function runHeavyTask(input: unknown): Promise<unknown> {
162
+ return new Promise((resolve, reject) => {
163
+ const worker = new Worker(resolve(__dirname, 'workers/heavy-task.js'), {
164
+ workerData: input,
165
+ });
166
+ worker.on('message', resolve);
167
+ worker.on('error', reject);
168
+ worker.on('exit', code => {
169
+ if (code !== 0) reject(new Error(`Worker exited with code ${code}`));
170
+ });
171
+ });
172
+ }
173
+ ```
174
+
175
+ 차단 기준: 10ms 이상 동기 CPU 연산 → worker_threads 고려.
176
+ - 이미지 리사이징 (sharp는 내부 libuv 스레드풀 사용 → 괜찮음)
177
+ - 암호화 (crypto 모듈 → 내부 C++ 바인딩, 대부분 OK)
178
+ - JSON 파싱 대용량 (>1MB) → 차단 위험
179
+ - bcrypt 해싱 → 동기 방식 금지, 비동기 사용
180
+
181
+ ### N3.3 setImmediate / process.nextTick 사용 기준
182
+
183
+ ```typescript
184
+ // process.nextTick: 현재 operation 완료 즉시 (I/O 앞)
185
+ // 주의: 무한 루프 가능 → 재귀 호출 금지
186
+ process.nextTick(() => emitter.emit('ready'));
187
+
188
+ // setImmediate: 현재 I/O 이벤트 사이클 이후
189
+ // 긴 작업을 여러 tick으로 분산할 때
190
+ function processLargeArray(arr: unknown[], callback: () => void, index = 0) {
191
+ if (index >= arr.length) return callback();
192
+ processSingleItem(arr[index]);
193
+ setImmediate(() => processLargeArray(arr, callback, index + 1));
194
+ }
195
+ ```
196
+
197
+ ---
198
+
199
+ ## N4. Stream Backpressure 존중
200
+
201
+ **readable.pipe()는 자동으로 backpressure를 처리한다. 수동 구현 시 반드시 확인.**
202
+
203
+ ```typescript
204
+ // WRONG: backpressure 무시
205
+ readable.on('data', chunk => {
206
+ writable.write(chunk); // writable buffer가 가득 차도 계속 push
207
+ });
208
+
209
+ // RIGHT: pipe 사용 (자동 backpressure)
210
+ readable.pipe(writable);
211
+
212
+ // 또는 stream.pipeline (에러 처리 포함)
213
+ import { pipeline } from 'node:stream/promises';
214
+ await pipeline(
215
+ fs.createReadStream('large-file.csv'),
216
+ new TransformStream(), // 변환 단계
217
+ fs.createWriteStream('output.csv'),
218
+ );
219
+ ```
220
+
221
+ ### N4.1 고수량 스트림 패턴
222
+
223
+ ```typescript
224
+ // Readable stream async iterator (Node.js 10+)
225
+ async function processCSV(filePath: string): Promise<void> {
226
+ const readable = fs.createReadStream(filePath).pipe(csvParser());
227
+ for await (const row of readable) {
228
+ await processRow(row); // 처리 완료 후 다음 청크 요청 (backpressure 자연스럽게 적용)
229
+ }
230
+ }
231
+ ```
232
+
233
+ ---
234
+
235
+ ## N5. 입력 경계 검증 (Zod)
236
+
237
+ **런타임 검증 없는 TypeScript 타입 단언(`as User`)은 안전하지 않다.**
238
+
239
+ ```typescript
240
+ import { z } from 'zod';
241
+
242
+ // 스키마 정의 — single source of truth
243
+ const CreateOrderSchema = z.object({
244
+ userId: z.string().uuid(),
245
+ items: z.array(
246
+ z.object({
247
+ productId: z.string().uuid(),
248
+ quantity: z.number().int().positive(),
249
+ })
250
+ ).min(1),
251
+ couponCode: z.string().optional(), // 옵셔널 명시 — 검증 로직에서 required 취급 금지
252
+ });
253
+
254
+ type CreateOrderInput = z.infer<typeof CreateOrderSchema>;
255
+
256
+ // 핸들러
257
+ async function createOrder(req: Request, res: Response) {
258
+ const parsed = CreateOrderSchema.safeParse(req.body);
259
+ if (!parsed.success) {
260
+ return res.status(400).json({
261
+ error: {
262
+ code: 'VALIDATION_ERROR',
263
+ message: '입력값이 올바르지 않습니다',
264
+ details: parsed.error.flatten().fieldErrors,
265
+ },
266
+ });
267
+ }
268
+ const input: CreateOrderInput = parsed.data;
269
+ // 이 시점부터 input은 타입 안전
270
+ await orderService.create(input);
271
+ }
272
+ ```
273
+
274
+ - **옵셔널 필드는 스키마와 비즈니스 로직에서 동일하게 처리**: `.optional()` 이면 값이 없어도 통과.
275
+ - 응답 직렬화도 검증: `z.output` 또는 `.transform()` 으로 응답 필드 선택.
276
+
277
+ ---
278
+
279
+ ## N6. TypeScript Strict 설정
280
+
281
+ ```json
282
+ // tsconfig.json — 최소 설정
283
+ {
284
+ "compilerOptions": {
285
+ "strict": true, // null 체크, implicit any, 등 통합
286
+ "noUncheckedIndexedAccess": true, // arr[0] 타입이 T | undefined (배열 경계 안전)
287
+ "exactOptionalPropertyTypes": true, // optional: 값이 undefined일 때 assign 금지
288
+ "noImplicitReturns": true, // 모든 분기에서 return 강제
289
+ "noFallthroughCasesInSwitch": true,
290
+ "forceConsistentCasingInFileNames": true,
291
+ "moduleResolution": "bundler", // ESM + bundler 환경
292
+ "target": "ES2022",
293
+ "lib": ["ES2022"]
294
+ }
295
+ }
296
+ ```
297
+
298
+ ### N6.1 타입 단언 규칙
299
+
300
+ ```typescript
301
+ // WRONG: 타입 단언으로 런타임 오류 숨김
302
+ const user = cache.get('user') as User; // undefined일 수 있음
303
+
304
+ // RIGHT: 명시적 가드
305
+ const raw = cache.get('user');
306
+ if (!raw) throw new Error('Cache miss: user');
307
+ const user = UserSchema.parse(raw); // 런타임 검증
308
+ ```
309
+
310
+ - `as T` 단언은 외부 데이터, 레거시 코드와의 경계에서만 허용. 내부 코드에서는 타입 추론 활용.
311
+ - `!` non-null assertion은 null/undefined가 불가능한 이유를 주석으로 설명.
312
+
313
+ ---
314
+
315
+ ## N7. 의존성 트리 관리
316
+
317
+ 근거: `sources/node-runtime/`
318
+
319
+ ### N7.1 의존성 최소화 원칙
320
+
321
+ - 새 패키지 도입 전 체크리스트:
322
+ 1. 표준 라이브러리(`node:fs`, `node:crypto`)로 해결 가능한가?
323
+ 2. 기존 의존성으로 해결 가능한가?
324
+ 3. 코드 10줄 미만이면 직접 구현하는 것이 낫지 않은가?
325
+ - 번들 크기 영향: `bundlephobia.com` 또는 `npm ls --depth=0` 확인.
326
+
327
+ ### N7.2 보안 감사
328
+
329
+ ```bash
330
+ # 의존성 취약점 감사 (CI에 포함)
331
+ npm audit --audit-level=high
332
+
333
+ # 의존성 업데이트 (weekly)
334
+ npx npm-check-updates -u && npm install
335
+
336
+ # supply chain 감사
337
+ npx audit-ci --high
338
+ ```
339
+
340
+ ### N7.3 Lock 파일 정책
341
+
342
+ - `package-lock.json` 또는 `pnpm-lock.yaml` 반드시 커밋.
343
+ - CI에서 `npm ci` (lockfile 기반 설치). `npm install` 금지.
344
+ - 의존성 업데이트는 별도 PR — 기능 PR에 섞지 않는다.
345
+
346
+ ---
347
+
348
+ ## N8. 구조화 로그 (Pino)
349
+
350
+ ```typescript
351
+ import pino from 'pino';
352
+
353
+ const logger = pino({
354
+ level: process.env.LOG_LEVEL ?? 'info',
355
+ // 프로덕션: JSON 출력 (stdout)
356
+ // 개발: pino-pretty로 가독성 향상
357
+ transport: process.env.NODE_ENV === 'development'
358
+ ? { target: 'pino-pretty', options: { colorize: true } }
359
+ : undefined,
360
+ // 민감 정보 자동 리댁션
361
+ redact: ['req.headers.authorization', '*.password', '*.cardNumber'],
362
+ base: {
363
+ service: process.env.SERVICE_NAME ?? 'unknown',
364
+ env: process.env.NODE_ENV ?? 'development',
365
+ },
366
+ });
367
+
368
+ // Request logger 미들웨어 (Fastify는 내장, Express는 pino-http)
369
+ import pinoHttp from 'pino-http';
370
+ app.use(pinoHttp({ logger }));
371
+ ```
372
+
373
+ ---
374
+
375
+ ## N9. Node.js 안티패턴 카탈로그
376
+
377
+ | 안티패턴 | 픽스 |
378
+ |----------|------|
379
+ | `process.on('unhandledRejection')` 미등록 | 앱 진입점에서 즉시 등록 |
380
+ | 동기 I/O (`fs.readFileSync`) in 핸들러 | `fs/promises` 비동기 버전 |
381
+ | `require()` 루프 내 동적 로딩 | 모듈 수준 캐싱 (require는 캐시됨, import() 동적 주의) |
382
+ | callback + async 혼용 | async/await 통일 |
383
+ | 런타임 검증 없는 `as T` 단언 | Zod safeParse |
384
+ | `npm install` in CI | `npm ci` (lockfile 기반) |
385
+ | 로그에 `req.body` 통째로 기록 | redact 설정 |
386
+ | `bcrypt.hashSync()` | `bcrypt.hash()` (비동기) |
387
+ | `JSON.parse(hugeString)` 동기 | 청크 분할 또는 worker_threads |
388
+ | `tsconfig.strict: false` | strict 활성화 |
@@ -0,0 +1,262 @@
1
+ ---
2
+ name: be-build-go
3
+ description: Go 요구사항을 받아 합의된 사내 원칙대로 구현. 명세→API contract→구현→테스트 매핑을 강제하고, error-as-value·context 전파·goroutine 안전성을 적용한다.
4
+ ---
5
+
6
+ # be-build (Go)
7
+
8
+ > **호출 시점**: "이 API 명세대로 Go로 구현해줘", "이 서비스 Go로 만들어줘".
9
+ > **선행 로딩**: `principles/common.md` + `principles/go.md` 필수.
10
+
11
+ ## 0. 절대 금지
12
+
13
+ 1. 명세 읽기 전에 코드 쓰지 마라.
14
+ 2. `panic` 을 일반 에러 처리에 사용 금지.
15
+ 3. `context.Context` 없는 DB/HTTP 호출 금지.
16
+ 4. goroutine 종료 조건 없이 시작 금지.
17
+ 5. 에러 무시 (`_ = err`) 금지 — 이유가 있으면 주석 필수.
18
+ 6. 인터페이스를 구현 패키지에서 정의 금지 (consumer 측 정의).
19
+
20
+ ## 1. 워크플로우
21
+
22
+ ### Step 1 — 요구사항 → 체크리스트 변환
23
+
24
+ 명세 받자마자, 다른 어떤 작업도 하기 전에:
25
+
26
+ ```markdown
27
+ ## 요구사항 체크리스트
28
+ - [ ] R-01: <명세 원문 직접 인용>
29
+ - [ ] R-02: ...
30
+ ```
31
+
32
+ optional/required 구분 명시. optional은 "없어도 통과" 케이스 매핑.
33
+
34
+ ### Step 2 — API Contract 정의
35
+
36
+ ```go
37
+ // types/order.go — 계약 먼저
38
+ type CreateOrderRequest struct {
39
+ UserID string `json:"userId" validate:"required,uuid"`
40
+ Items []OrderItem `json:"items" validate:"required,min=1,dive"`
41
+ CouponCode *string `json:"couponCode,omitempty"` // optional — pointer로 absent 구분
42
+ }
43
+
44
+ type CreateOrderResponse struct {
45
+ OrderID string `json:"orderId"`
46
+ Status string `json:"status"`
47
+ CreatedAt time.Time `json:"createdAt"`
48
+ }
49
+
50
+ type OrderItem struct {
51
+ ProductID string `json:"productId" validate:"required,uuid"`
52
+ Quantity int `json:"quantity" validate:"required,min=1"`
53
+ }
54
+ ```
55
+
56
+ ### Step 3 — 체크리스트 → 테스트 매핑표
57
+
58
+ ```markdown
59
+ ## 매핑표
60
+ | 요구사항 | 함수 | 테스트 파일:케이스 |
61
+ |----------|------|---------------------|
62
+ | R-01 | OrderHandler.Create | order_test.go:TestCreate_Success |
63
+ | R-02 | OrderService.Validate | order_test.go:TestCreate_NoCoupon |
64
+ ```
65
+
66
+ ### Step 4 — 패키지 구조 결정
67
+
68
+ ```
69
+ internal/
70
+ ├─ handler/ HTTP 핸들러 (입력 검증, 라우팅)
71
+ ├─ service/ 비즈니스 로직 (인터페이스 정의)
72
+ ├─ repository/ DB 접근 (구현)
73
+ └─ domain/ 엔티티, 도메인 에러
74
+ ```
75
+
76
+ 결정 기록: "service 레이어가 트랜잭션 경계. repository는 순수 쿼리."
77
+
78
+ ### Step 5 — TDD (Red → Green → Refactor)
79
+
80
+ ```bash
81
+ go test ./... -run TestCreate_Success # 실패 확인
82
+ # 구현
83
+ go test ./... -run TestCreate_Success # 통과 확인
84
+ go test -race ./... # 레이스 컨디션 확인
85
+ ```
86
+
87
+ ### Step 6 — 셀프 체크리스트
88
+
89
+ ```markdown
90
+ - [ ] 모든 함수 첫 인자가 context.Context (I/O 함수)
91
+ - [ ] goroutine 시작 시 종료 조건 존재
92
+ - [ ] 에러 래핑 — fmt.Errorf("funcName: %w", err)
93
+ - [ ] 에러 무시 없음
94
+ - [ ] golangci-lint 통과
95
+ - [ ] go test -race 통과
96
+ - [ ] optional 필드 pointer 또는 omitempty 처리
97
+ ```
98
+
99
+ ### Step 7 — 완료 선언
100
+
101
+ 매핑표 모든 행 ✅ + 테스트 green + race 없음 + lint 통과.
102
+
103
+ ## 2. 구현 디폴트
104
+
105
+ ### 2.1 HTTP 핸들러 (net/http + chi)
106
+
107
+ ```go
108
+ // handler/order.go
109
+ func (h *OrderHandler) Create(w http.ResponseWriter, r *http.Request) {
110
+ var req CreateOrderRequest
111
+ if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
112
+ respondError(w, http.StatusBadRequest, "INVALID_JSON", err.Error(), r)
113
+ return
114
+ }
115
+ if err := h.validate.Struct(req); err != nil {
116
+ respondError(w, http.StatusBadRequest, "VALIDATION_ERROR", err.Error(), r)
117
+ return
118
+ }
119
+
120
+ order, err := h.svc.CreateOrder(r.Context(), req)
121
+ if err != nil {
122
+ switch {
123
+ case errors.Is(err, domain.ErrUserNotFound):
124
+ respondError(w, http.StatusNotFound, "USER_NOT_FOUND", err.Error(), r)
125
+ case errors.Is(err, domain.ErrInsufficientStock):
126
+ respondError(w, http.StatusUnprocessableEntity, "INSUFFICIENT_STOCK", err.Error(), r)
127
+ default:
128
+ slog.ErrorContext(r.Context(), "create order failed", "err", err, "requestId", requestID(r))
129
+ respondError(w, http.StatusInternalServerError, "INTERNAL_ERROR", "서버 오류가 발생했습니다", r)
130
+ }
131
+ return
132
+ }
133
+ respondJSON(w, http.StatusCreated, order)
134
+ }
135
+
136
+ func respondError(w http.ResponseWriter, status int, code, message string, r *http.Request) {
137
+ respondJSON(w, status, map[string]interface{}{
138
+ "error": map[string]interface{}{
139
+ "code": code,
140
+ "message": message,
141
+ "requestId": requestID(r),
142
+ },
143
+ })
144
+ }
145
+ ```
146
+
147
+ ### 2.2 서비스 레이어 (인터페이스 + 트랜잭션)
148
+
149
+ ```go
150
+ // service/order.go
151
+
152
+ // consumer 측 인터페이스 정의
153
+ type orderRepository interface {
154
+ Create(ctx context.Context, order *domain.Order) error
155
+ DecrementStock(ctx context.Context, productID string, qty int) error
156
+ }
157
+
158
+ type OrderService struct {
159
+ repo orderRepository
160
+ db *sql.DB // 트랜잭션용
161
+ }
162
+
163
+ func (s *OrderService) CreateOrder(ctx context.Context, req CreateOrderRequest) (*domain.Order, error) {
164
+ // TX: order 생성 + stock 차감 원자적 처리
165
+ tx, err := s.db.BeginTx(ctx, nil)
166
+ if err != nil {
167
+ return nil, fmt.Errorf("CreateOrder begin tx: %w", err)
168
+ }
169
+ defer tx.Rollback() // 성공 시 Commit 후 Rollback은 no-op
170
+
171
+ order := &domain.Order{
172
+ ID: uuid.New().String(),
173
+ UserID: req.UserID,
174
+ Status: "pending",
175
+ }
176
+ if err := s.repo.Create(ctx, order); err != nil {
177
+ return nil, fmt.Errorf("CreateOrder create: %w", err)
178
+ }
179
+ for _, item := range req.Items {
180
+ if err := s.repo.DecrementStock(ctx, item.ProductID, item.Quantity); err != nil {
181
+ return nil, fmt.Errorf("CreateOrder decrement stock %s: %w", item.ProductID, err)
182
+ }
183
+ }
184
+
185
+ if err := tx.Commit(); err != nil {
186
+ return nil, fmt.Errorf("CreateOrder commit: %w", err)
187
+ }
188
+ return order, nil
189
+ }
190
+ ```
191
+
192
+ ### 2.3 에러 모델
193
+
194
+ ```go
195
+ // domain/errors.go
196
+ var (
197
+ ErrUserNotFound = errors.New("user not found")
198
+ ErrInsufficientStock = errors.New("insufficient stock")
199
+ ErrOrderNotFound = errors.New("order not found")
200
+ )
201
+
202
+ // 상세 정보가 필요한 구조체 에러
203
+ type ValidationError struct {
204
+ Fields map[string]string
205
+ }
206
+ func (e *ValidationError) Error() string {
207
+ return fmt.Sprintf("validation failed: %v", e.Fields)
208
+ }
209
+ ```
210
+
211
+ ### 2.4 관찰가능성
212
+
213
+ ```go
214
+ import (
215
+ "go.opentelemetry.io/otel"
216
+ "go.opentelemetry.io/otel/attribute"
217
+ "go.opentelemetry.io/otel/codes"
218
+ "log/slog"
219
+ )
220
+
221
+ var tracer = otel.Tracer("order-service")
222
+
223
+ func (s *OrderService) CreateOrder(ctx context.Context, req CreateOrderRequest) (*domain.Order, error) {
224
+ ctx, span := tracer.Start(ctx, "OrderService.CreateOrder")
225
+ defer span.End()
226
+
227
+ span.SetAttributes(
228
+ attribute.String("order.userId", req.UserID),
229
+ attribute.Int("order.itemCount", len(req.Items)),
230
+ )
231
+
232
+ order, err := s.createInternal(ctx, req)
233
+ if err != nil {
234
+ span.RecordError(err)
235
+ span.SetStatus(codes.Error, err.Error())
236
+ return nil, err
237
+ }
238
+ slog.InfoContext(ctx, "order created",
239
+ slog.String("orderId", order.ID),
240
+ slog.String("userId", order.UserID),
241
+ )
242
+ return order, nil
243
+ }
244
+ ```
245
+
246
+ ## 3. 출력 형식
247
+
248
+ ```
249
+ ## 완료 보고
250
+ - 체크리스트: N/N ✅
251
+ - 매핑표: 모든 행 테스트 green
252
+ - go test -race: PASS
253
+ - golangci-lint: 0 issues
254
+ - 변경 파일: <목록>
255
+ - 의사결정: <패키지 구조 1-2줄>
256
+ ```
257
+
258
+ ## 4. 관련 문서
259
+
260
+ - 원칙: [`principles/common.md`](../../../principles/common.md), [`principles/go.md`](../../../principles/go.md)
261
+ - 리뷰: [`skills/go/be-review/SKILL.md`](../be-review/SKILL.md)
262
+ - 성능: [`skills/go/be-perf/SKILL.md`](../be-perf/SKILL.md)