sonamu 0.8.24 → 0.8.26

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 (88) hide show
  1. package/dist/api/__tests__/config.test.js +189 -0
  2. package/dist/api/config.d.ts.map +1 -1
  3. package/dist/api/config.js +7 -2
  4. package/dist/api/sonamu.d.ts.map +1 -1
  5. package/dist/api/sonamu.js +14 -10
  6. package/dist/auth/index.d.ts +1 -0
  7. package/dist/auth/index.d.ts.map +1 -1
  8. package/dist/auth/index.js +2 -1
  9. package/dist/auth/knex-adapter.d.ts +23 -0
  10. package/dist/auth/knex-adapter.d.ts.map +1 -0
  11. package/dist/auth/knex-adapter.js +163 -0
  12. package/dist/auth/plugins/wrappers/admin.d.ts +2 -2
  13. package/dist/bin/__tests__/ts-loader-register.test.js +45 -0
  14. package/dist/bin/cli.js +47 -9
  15. package/dist/bin/ts-loader-register.js +3 -29
  16. package/dist/bin/ts-loader-registration.d.ts +2 -0
  17. package/dist/bin/ts-loader-registration.d.ts.map +1 -0
  18. package/dist/bin/ts-loader-registration.js +42 -0
  19. package/dist/cone/cone-generator.js +3 -3
  20. package/dist/database/puri-subset.test-d.js +9 -1
  21. package/dist/database/puri-subset.types.d.ts +1 -1
  22. package/dist/database/puri-subset.types.d.ts.map +1 -1
  23. package/dist/database/puri-subset.types.js +1 -1
  24. package/dist/testing/fixture-generator.js +5 -5
  25. package/dist/ui/ai-client.js +2 -2
  26. package/dist/ui/api.d.ts.map +1 -1
  27. package/dist/ui/api.js +14 -14
  28. package/dist/ui/cdd-service.d.ts +15 -18
  29. package/dist/ui/cdd-service.d.ts.map +1 -1
  30. package/dist/ui/cdd-service.js +246 -222
  31. package/dist/ui/cdd-types.d.ts +41 -68
  32. package/dist/ui/cdd-types.d.ts.map +1 -1
  33. package/dist/ui/cdd-types.js +2 -2
  34. package/dist/ui-web/assets/index-CKo0Z2Iu.css +1 -0
  35. package/dist/ui-web/assets/{index-CxiydzeC.js → index-DK-2aacv.js} +83 -83
  36. package/dist/ui-web/index.html +2 -2
  37. package/package.json +6 -2
  38. package/src/api/__tests__/config.test.ts +225 -0
  39. package/src/api/config.ts +10 -4
  40. package/src/api/sonamu.ts +16 -13
  41. package/src/auth/index.ts +1 -0
  42. package/src/auth/knex-adapter.ts +208 -0
  43. package/src/bin/__tests__/ts-loader-register.test.ts +62 -0
  44. package/src/bin/cli.ts +52 -9
  45. package/src/bin/ts-loader-register.ts +2 -32
  46. package/src/bin/ts-loader-registration.ts +55 -0
  47. package/src/cone/cone-generator.ts +2 -2
  48. package/src/database/puri-subset.test-d.ts +102 -0
  49. package/src/database/puri-subset.types.ts +1 -1
  50. package/src/skills/commands/sonamu-skills.md +20 -0
  51. package/src/skills/sonamu/SKILL.md +179 -137
  52. package/src/skills/sonamu/ai-agents.md +69 -69
  53. package/src/skills/sonamu/api.md +147 -147
  54. package/src/skills/sonamu/auth-migration.md +220 -220
  55. package/src/skills/sonamu/auth-plugins.md +83 -83
  56. package/src/skills/sonamu/auth.md +106 -106
  57. package/src/skills/sonamu/cdd.md +65 -200
  58. package/src/skills/sonamu/cone.md +138 -138
  59. package/src/skills/sonamu/config.md +191 -191
  60. package/src/skills/sonamu/create-sonamu.md +66 -66
  61. package/src/skills/sonamu/database.md +158 -158
  62. package/src/skills/sonamu/entity-basic.md +292 -293
  63. package/src/skills/sonamu/entity-relations.md +246 -246
  64. package/src/skills/sonamu/entity-validation-checklist.md +124 -124
  65. package/src/skills/sonamu/fixture-cli.md +231 -231
  66. package/src/skills/sonamu/framework-change.md +37 -37
  67. package/src/skills/sonamu/frontend.md +223 -223
  68. package/src/skills/sonamu/i18n.md +82 -82
  69. package/src/skills/sonamu/migration.md +77 -77
  70. package/src/skills/sonamu/model.md +222 -222
  71. package/src/skills/sonamu/naite.md +86 -86
  72. package/src/skills/sonamu/project-init.md +228 -228
  73. package/src/skills/sonamu/puri.md +122 -122
  74. package/src/skills/sonamu/scaffolding.md +154 -154
  75. package/src/skills/sonamu/skill-contribution.md +124 -124
  76. package/src/skills/sonamu/subset.md +46 -46
  77. package/src/skills/sonamu/tasks.md +82 -82
  78. package/src/skills/sonamu/testing-devrunner.md +147 -147
  79. package/src/skills/sonamu/testing.md +673 -673
  80. package/src/skills/sonamu/upsert.md +79 -79
  81. package/src/skills/sonamu/vector.md +67 -67
  82. package/src/testing/fixture-generator.ts +4 -4
  83. package/src/ui/ai-client.ts +1 -1
  84. package/src/ui/api.ts +18 -17
  85. package/src/ui/cdd-service.ts +264 -254
  86. package/src/ui/cdd-types.ts +40 -75
  87. package/dist/ui-web/assets/index-BrQKU3j9.css +0 -1
  88. package/src/skills/sonamu/workflow.md +0 -317
@@ -1,16 +1,16 @@
1
1
  ---
2
2
  name: sonamu-model
3
- description: Sonamu Model 클래스 작성. BaseModelClass 상속, CRUD 메서드 패턴, 비즈니스 로직, executeSubsetQuery 옵션. Use when implementing Model classes with business logic.
3
+ description: Writing Sonamu Model classes. BaseModelClass inheritance, CRUD method patterns, business logic, executeSubsetQuery options. Use when implementing Model classes with business logic.
4
4
  ---
5
5
 
6
- # Model 클래스
6
+ # Model Class
7
7
 
8
- **실제 동작 코드 참고:**
9
- - `sonamu/examples/miomock/api/src/application/project/project.model.ts` - ManyToMany save 구현
10
- - `sonamu/examples/miomock/api/src/application/employee/employee.model.ts` - 기본 CRUD 패턴
11
- - `sonamu/examples/miomock/api/src/application/project/project.model.test.ts` - 테스트 예시
8
+ **Reference working code:**
9
+ - `sonamu/examples/miomock/api/src/application/project/project.model.ts` - ManyToMany save implementation
10
+ - `sonamu/examples/miomock/api/src/application/employee/employee.model.ts` - basic CRUD pattern
11
+ - `sonamu/examples/miomock/api/src/application/project/project.model.test.ts` - test examples
12
12
 
13
- ## 기본 구조
13
+ ## Basic Structure
14
14
 
15
15
  ```typescript
16
16
  import { api, BaseModelClass, ListResult, NotFoundException } from "sonamu";
@@ -32,18 +32,18 @@ class UserModelClass extends BaseModelClass<
32
32
  export const UserModel = new UserModelClass();
33
33
  ```
34
34
 
35
- ## CRUD 패턴
35
+ ## CRUD Pattern
36
36
 
37
- Sonamu Model 다음 기본 메서드를 제공한다:
37
+ Sonamu Model provides the following basic methods:
38
38
 
39
- | 메서드 | 용도 | 비고 |
39
+ | Method | Purpose | Notes |
40
40
  |--------|------|------|
41
- | `findById` | 단건 조회 | |
42
- | `findMany` | 목록 조회 | |
43
- | `save` | 생성/수정 | upsert 동작 |
44
- | `del` | 삭제 | `delete` 아님 주의 |
41
+ | `findById` | Retrieve single record | |
42
+ | `findMany` | Retrieve list | |
43
+ | `save` | Create/update | upsert behavior |
44
+ | `del` | Delete | Note: not `delete` |
45
45
 
46
- **JavaScript 예약어 회피:** `delete`는 JS 예약어이므로 `del`로 명명. TypeScript에서는 컴파일 오류 없이 `delete`를 메서드명으로 사용할 있지만, 런타임에서 문제가 발생할 있어 Sonamu `del`을 사용한다.
46
+ **Avoiding JavaScript reserved words:** `delete` is a JS reserved word, so it is named `del`. While TypeScript allows `delete` as a method name without a compile error, it can cause runtime issues, so Sonamu uses `del`.
47
47
 
48
48
  ### findById
49
49
 
@@ -101,55 +101,55 @@ async del(ids: number[]): Promise<number> {
101
101
  }
102
102
  ```
103
103
 
104
- ## BaseModel 메서드
104
+ ## BaseModel Methods
105
105
 
106
- | 메서드 | 설명 |
106
+ | Method | Description |
107
107
  |--------|------|
108
- | `getPuri("r")` | 읽기 쿼리 빌더 |
109
- | `getPuri("w")` | 쓰기 쿼리 빌더 |
110
- | `getSubsetQueries(subset)` | Subset 쿼리 빌더 (`{ qb, onSubset }` 반환) |
111
- | `executeSubsetQuery(options)` | Subset 쿼리 실행 |
112
- | `createEnhancers(enhancers)` | Enhancer 객체 생성 헬퍼 (타입 추론) |
108
+ | `getPuri("r")` | Read query builder |
109
+ | `getPuri("w")` | Write query builder |
110
+ | `getSubsetQueries(subset)` | Subset query builder (returns `{ qb, onSubset }`) |
111
+ | `executeSubsetQuery(options)` | Execute subset query |
112
+ | `createEnhancers(enhancers)` | Enhancer object creation helper (type inference) |
113
113
 
114
114
  ## getSubsetQueries
115
115
 
116
116
  ```typescript
117
117
  const { qb, onSubset } = this.getSubsetQueries(subset);
118
118
 
119
- // qb: 조건 추가용 쿼리 빌더
119
+ // qb: query builder for adding conditions
120
120
  qb.where("users.status", "active");
121
121
 
122
- // onSubset: 특정 서브셋 전용 타입이 필요할
123
- const typedQb = onSubset("A"); // 서브셋 A 타입으로 추론
122
+ // onSubset: when you need the type for a specific subset
123
+ const typedQb = onSubset("A"); // infers as subset A's type
124
124
  ```
125
125
 
126
- ## executeSubsetQuery 옵션
126
+ ## executeSubsetQuery Options
127
127
 
128
128
  ```typescript
129
129
  return this.executeSubsetQuery({
130
- subset, // 서브셋
131
- qb, // 쿼리 빌더
132
- params, // ListParams (num, page, queryMode, sonamuFilter )
133
- debug: true, // 쿼리 로그 출력 (기본값: false)
134
- optimizeCountQuery: true, // COUNT 쿼리 최적화 - 불필요한 LEFT JOIN 제거 (기본값: false)
135
- enhancers, // Enhancer 함수 객체 (옵션)
130
+ subset, // subset key
131
+ qb, // query builder
132
+ params, // ListParams (num, page, queryMode, sonamuFilter, etc.)
133
+ debug: true, // print query log (default: false)
134
+ optimizeCountQuery: true, // COUNT query optimization - removes unnecessary LEFT JOINs (default: false)
135
+ enhancers, // Enhancer function object (optional)
136
136
  });
137
137
  ```
138
138
 
139
- > **CRITICAL: `executeSubsetQuery()` 반환 객체를 직접 mutate하지 않는다.**
139
+ > **CRITICAL: Do not directly mutate the object returned by `executeSubsetQuery()`.**
140
140
  >
141
- > `result.rows = result.rows.map(...)` 또는 `(result as any).rows = ...` 방식으로 rows를 교체하면
142
- > `total` 카운트가 깨져 pagination 오동작한다.
141
+ > Replacing rows via `result.rows = result.rows.map(...)` or `(result as any).rows = ...`
142
+ > will break the `total` count and cause pagination to malfunction.
143
143
  >
144
- > 추가 계산이 필요한 virtual 필드는 `enhancers` 패턴으로 처리한다:
144
+ > Use the `enhancers` pattern for virtual fields that require additional computation:
145
145
  >
146
146
  > ```typescript
147
- > // WRONG — pagination 파괴
147
+ > // WRONG — breaks pagination
148
148
  > const result = await this.executeSubsetQuery({ subset, qb, params });
149
149
  > (result as any).rows = result.rows.map((row) => ({ ...row, extra: "value" }));
150
150
  > return result as any;
151
151
  >
152
- > // CORRECT — enhancers 패턴
152
+ > // CORRECT — enhancers pattern
153
153
  > const enhancers = this.createEnhancers({
154
154
  > A: (row) => ({ ...row, extra: "value" }),
155
155
  > });
@@ -158,45 +158,45 @@ return this.executeSubsetQuery({
158
158
 
159
159
  ### queryMode
160
160
 
161
- params에 queryMode 전달하여 반환값 제어:
161
+ Pass queryMode in params to control the return value:
162
162
 
163
163
  ```typescript
164
- // 리스트만 (COUNT 쿼리 스킵) - 성능 최적화
164
+ // List only (skip COUNT query) - performance optimization
165
165
  const { rows } = await this.findMany(subset, { ...params, queryMode: "list" });
166
166
 
167
- // 카운트만 (리스트 스킵)
167
+ // Count only (skip list)
168
168
  const { total } = await this.findMany(subset, { ...params, queryMode: "count" });
169
169
 
170
- // (기본값)
170
+ // Both (default)
171
171
  const { rows, total } = await this.findMany(subset, { ...params, queryMode: "both" });
172
172
  ```
173
173
 
174
174
  ### sonamuFilter (FilterQuery)
175
175
 
176
- params.sonamuFilter로 필터 조건 자동 적용:
176
+ Automatically apply filter conditions via params.sonamuFilter:
177
177
 
178
- **전제조건:** entity.json에서 해당 prop에 `"toFilter": true`가 설정되어 있어야 함. 설정되지 않은 필드는 필터링 대상에서 제외된다.
178
+ **Prerequisite:** The corresponding prop in entity.json must have `"toFilter": true` set. Fields without this setting are excluded from filtering.
179
179
 
180
180
  ```typescript
181
- // 클라이언트에서 전달된 필터
181
+ // Filter passed from the client
182
182
  const params = {
183
183
  num: 10,
184
184
  page: 1,
185
185
  sonamuFilter: {
186
- status: "active", // eq (기본)
186
+ status: "active", // eq (default)
187
187
  age: { gte: 18 }, // >=
188
188
  role: { in: ["admin", "user"] },
189
189
  email: { contains: "@test" }, // LIKE %...%
190
190
  }
191
191
  };
192
192
 
193
- // Model에서 자동 적용됨
193
+ // Automatically applied in the Model
194
194
  return this.executeSubsetQuery({ subset, qb, params });
195
195
  ```
196
196
 
197
- **타입별 허용 연산자:**
197
+ **Allowed operators by type:**
198
198
 
199
- | 타입 | 연산자 |
199
+ | Type | Operators |
200
200
  |------|--------|
201
201
  | `string` | eq, ne, contains, startsWith, endsWith, in, notIn, isNull, isNotNull |
202
202
  | `integer` | eq, ne, gt, gte, lt, lte, in, notIn, between, isNull, isNotNull |
@@ -206,11 +206,11 @@ return this.executeSubsetQuery({ subset, qb, params });
206
206
  | `enum` | eq, ne, in, notIn, isNull, isNotNull |
207
207
  | `json` | isNull, isNotNull |
208
208
 
209
- **연산자 예시:**
209
+ **Operator examples:**
210
210
 
211
- | 연산자 | SQL | 예시 |
211
+ | Operator | SQL | Example |
212
212
  |--------|-----|------|
213
- | `eq` (기본) | `=` | `{ status: "active" }` |
213
+ | `eq` (default) | `=` | `{ status: "active" }` |
214
214
  | `ne` | `!=` | `{ status: { ne: "deleted" } }` |
215
215
  | `gt`, `gte` | `>`, `>=` | `{ age: { gte: 18 } }` |
216
216
  | `lt`, `lte` | `<`, `<=` | `{ price: { lte: 1000 } }` |
@@ -219,32 +219,32 @@ return this.executeSubsetQuery({ subset, qb, params });
219
219
  | `startsWith` | `LIKE ...%` | `{ code: { startsWith: "A" } }` |
220
220
  | `endsWith` | `LIKE %...` | `{ ext: { endsWith: ".pdf" } }` |
221
221
  | `isNull`, `isNotNull` | `IS NULL` | `{ deleted_at: { isNull: true } }` |
222
- | `before`, `after` | `<`, `>` (날짜) | `{ created_at: { after: "2024-01-01" } }` |
222
+ | `before`, `after` | `<`, `>` (date) | `{ created_at: { after: "2024-01-01" } }` |
223
223
  | `between` | `BETWEEN` | `{ price: { between: [100, 500] } }` |
224
224
 
225
- **타입 정의 (`ApplySonamuFilter`):**
225
+ **Type definition (`ApplySonamuFilter`):**
226
226
 
227
227
  ```typescript
228
228
  import type { ApplySonamuFilter } from "sonamu";
229
229
 
230
- // ListParams에서 sonamuFilter 타입 정의
230
+ // Define sonamuFilter type in ListParams
231
231
  type ProjectListParams = {
232
232
  num: number;
233
233
  page: number;
234
234
  sonamuFilter?: ApplySonamuFilter<
235
- ProjectSubsetA, // 엔티티 타입
236
- "id" | "created_at", // 제외할 필드 (TOmitKeys)
237
- "budget" // numeric으로 취급할 필드 (TNumericKeys)
235
+ ProjectSubsetA, // entity type
236
+ "id" | "created_at", // fields to exclude (TOmitKeys)
237
+ "budget" // fields to treat as numeric (TNumericKeys)
238
238
  >;
239
239
  };
240
240
  ```
241
241
 
242
242
  ## Enhancers
243
243
 
244
- virtual 필드 계산 쿼리 후 가공:
244
+ Post-query processing for virtual field computation and similar needs:
245
245
 
246
246
  ```typescript
247
- // Enhancer 정의
247
+ // Define enhancer
248
248
  const enhancers = this.createEnhancers({
249
249
  A: async (row) => ({
250
250
  ...row,
@@ -256,11 +256,11 @@ const enhancers = this.createEnhancers({
256
256
  }),
257
257
  });
258
258
 
259
- // executeSubsetQuery에서 사용
259
+ // Use in executeSubsetQuery
260
260
  return this.executeSubsetQuery({ subset, qb, params, enhancers });
261
261
  ```
262
262
 
263
- ## Types 파일
263
+ ## Types File
264
264
 
265
265
  ```typescript
266
266
  // user.types.ts
@@ -270,7 +270,7 @@ import { UserOrderBy, UserSearchField, UserBaseSchema, UserBaseListParams } from
270
270
  export const UserListParams = UserBaseListParams;
271
271
  export type UserListParams = z.infer<typeof UserListParams>;
272
272
 
273
- // 기본 패턴: BaseSchema에서 partial 처리
273
+ // Basic pattern: partial from BaseSchema
274
274
  export const UserSaveParams = UserBaseSchema.partial({
275
275
  id: true,
276
276
  created_at: true,
@@ -278,9 +278,9 @@ export const UserSaveParams = UserBaseSchema.partial({
278
278
  export type UserSaveParams = z.infer<typeof UserSaveParams>;
279
279
  ```
280
280
 
281
- ### SaveParams 패턴
281
+ ### SaveParams Patterns
282
282
 
283
- **기본 패턴 (relation 없음):**
283
+ **Basic pattern (no relations):**
284
284
  ```typescript
285
285
  import { UserBaseSchema, UserBaseListParams } from "../sonamu.generated";
286
286
 
@@ -294,9 +294,9 @@ export const UserSaveParams = UserBaseSchema.partial({
294
294
  export type UserSaveParams = z.infer<typeof UserSaveParams>;
295
295
  ```
296
296
 
297
- **ManyToMany relation 있는 경우:**
297
+ **If a ManyToMany relation exists:**
298
298
  ```typescript
299
- // ManyToMany 관계: {relation_name}_ids 배열 추가
299
+ // ManyToMany relation: add {relation_name}_ids array
300
300
  export const ProjectSaveParams = ProjectBaseSchema.partial({
301
301
  id: true,
302
302
  created_at: true,
@@ -306,49 +306,49 @@ export const ProjectSaveParams = ProjectBaseSchema.partial({
306
306
  tag_ids: z.array(z.number().int().positive()),
307
307
  })
308
308
  .omit({
309
- // virtual 필드, 시스템 생성 필드 등은 omit
309
+ // omit virtual fields, system-generated fields, etc.
310
310
  virtual_test: true,
311
311
  });
312
312
  export type ProjectSaveParams = z.infer<typeof ProjectSaveParams>;
313
313
  ```
314
314
 
315
- **BelongsToOne relation의 nullable 필드 처리:**
315
+ **Handling nullable fields in BelongsToOne relations:**
316
316
  ```typescript
317
- // nullable relation은 자동으로 optional이므로 추가 partial 불필요
317
+ // Nullable relations are automatically optional, so no extra partial is needed
318
318
  export const ResponseSaveParams = ResponseBaseSchema.partial({
319
319
  id: true,
320
320
  created_at: true,
321
- updated_at: true, // timestamp 필드도 partial 처리
321
+ updated_at: true, // also make timestamp fields partial
322
322
  });
323
323
  export type ResponseSaveParams = z.infer<typeof ResponseSaveParams>;
324
324
  ```
325
325
 
326
- **실제 동작 코드 참고:**
327
- - `sonamu/examples/miomock/api/src/application/project/project.types.ts` - ManyToMany SaveParams 예시
328
- - `sonamu/examples/miomock/api/src/application/employee/employee.types.ts` - BelongsToOne SaveParams 예시
326
+ **Reference working code:**
327
+ - `sonamu/examples/miomock/api/src/application/project/project.types.ts` - ManyToMany SaveParams example
328
+ - `sonamu/examples/miomock/api/src/application/employee/employee.types.ts` - BelongsToOne SaveParams example
329
329
 
330
- ### Model에서 Relation 처리
330
+ ### Handling Relations in the Model
331
331
 
332
- **Update relation 객체 제거:**
332
+ **Removing relation objects on update:**
333
333
  ```typescript
334
- // Test에서 Update 사용하는 패턴
334
+ // Pattern used in tests for updates
335
335
  const original = await UserModel.findById("A", userId);
336
336
 
337
- // Relation 객체 제거하고 FK 추출
337
+ // Remove relation object and extract FK only
338
338
  const { institution, ...userData } = original;
339
339
 
340
340
  await UserModel.save([
341
341
  {
342
342
  ...userData,
343
- institution_id: institution?.id ?? null, // FK 명시적 추가
344
- name: "수정된이름",
343
+ institution_id: institution?.id ?? null, // explicitly add FK
344
+ name: "Updated Name",
345
345
  },
346
346
  ]);
347
347
  ```
348
348
 
349
- **ManyToMany save 시:**
349
+ **ManyToMany save:**
350
350
  ```typescript
351
- // ManyToMany _ids 배열로 전달
351
+ // ManyToMany is passed as an _ids array
352
352
  await ProjectModel.save([
353
353
  {
354
354
  id: projectId,
@@ -359,11 +359,11 @@ await ProjectModel.save([
359
359
  ]);
360
360
  ```
361
361
 
362
- **실제 동작 코드 참고:**
363
- - `sonamu/examples/miomock/api/src/application/project/project.model.ts` - ManyToMany save 구현
364
- - `sonamu/examples/miomock/api/src/application/project/project.model.test.ts` - Save 테스트 예시
362
+ **Reference working code:**
363
+ - `sonamu/examples/miomock/api/src/application/project/project.model.ts` - ManyToMany save implementation
364
+ - `sonamu/examples/miomock/api/src/application/project/project.model.test.ts` - Save test example
365
365
 
366
- ## 트랜잭션
366
+ ## Transactions
367
367
 
368
368
  ```typescript
369
369
  await this.getPuri("w").transaction(async (trx) => {
@@ -372,57 +372,57 @@ await this.getPuri("w").transaction(async (trx) => {
372
372
  });
373
373
  ```
374
374
 
375
- ## 검증 패턴
375
+ ## Validation Patterns
376
376
 
377
- ### 단계별 검증
377
+ ### Step-by-step Validation
378
378
 
379
- 비즈니스 규칙을 단계별로 검증하는 패턴:
379
+ A pattern for validating business rules step by step:
380
380
 
381
381
  ```typescript
382
382
  async enroll(courseId: number, userId: number): Promise<Enrollment> {
383
- // 1단계: 중복 체크
383
+ // Step 1: Duplicate check
384
384
  const existing = await this.findOne("A", {
385
385
  course_id: courseId,
386
386
  user_id: userId,
387
387
  });
388
388
 
389
389
  if (existing) {
390
- throw new Error("이미 등록된 강좌입니다");
390
+ throw new Error("Already enrolled in this course");
391
391
  }
392
392
 
393
- // 2단계: 정원 확인
393
+ // Step 2: Capacity check
394
394
  const course = await CourseModel.findById("A", courseId);
395
395
  const { total } = await this.findMany({ course_id: courseId });
396
396
 
397
397
  if (total >= course.max_students) {
398
- throw new Error("정원이 가득 찼습니다");
398
+ throw new Error("The course is full");
399
399
  }
400
400
 
401
- // 3단계: 실행
401
+ // Step 3: Execute
402
402
  const [id] = await this.save([{ course_id: courseId, user_id: userId }]);
403
403
  return this.findById("A", id);
404
404
  }
405
405
  ```
406
406
 
407
- ### 조건부 검증
407
+ ### Conditional Validation
408
408
 
409
- 조건에 따라 다른 검증 수행:
409
+ Perform different validations depending on conditions:
410
410
 
411
411
  ```typescript
412
412
  async save(spa: TaskSaveParams[]): Promise<number[]> {
413
413
  for (const sp of spa) {
414
- // 상태가 완료일 때만 완료일 필수
414
+ // completion date is required only when status is completed
415
415
  if (sp.status === "completed" && !sp.completed_at) {
416
- throw new Error("완료 상태는 완료일이 필요합니다");
416
+ throw new Error("A completion date is required for completed status");
417
417
  }
418
418
 
419
- // 예산이 있을 때만 금액 범위 체크
419
+ // Check amount range only when budget is present
420
420
  if (sp.budget !== null && sp.budget < 0) {
421
- throw new Error("예산은 0 이상이어야 합니다");
421
+ throw new Error("Budget must be 0 or greater");
422
422
  }
423
423
  }
424
424
 
425
- // 검증 통과 저장
425
+ // Save after validation passes
426
426
  const wdb = this.getPuri("w");
427
427
  spa.forEach((sp) => wdb.ubRegister("tasks", sp));
428
428
 
@@ -432,28 +432,28 @@ async save(spa: TaskSaveParams[]): Promise<number[]> {
432
432
  }
433
433
  ```
434
434
 
435
- ### 관련 데이터 검증
435
+ ### Validating Against Related Data
436
436
 
437
- 다른 테이블과의 관계를 검증:
437
+ Validate relationships with other tables:
438
438
 
439
439
  ```typescript
440
440
  async save(spa: ResponseSaveParams[]): Promise<number[]> {
441
441
  for (const sp of spa) {
442
- // 설문이 아직 열려있는지 확인
442
+ // Check if the survey is still open
443
443
  const collection = await CollectionModel.findById("A", sp.collection_id);
444
444
 
445
445
  if (collection.status === "closed") {
446
- throw new Error("이미 종료된 설문입니다");
446
+ throw new Error("This survey has already ended");
447
447
  }
448
448
 
449
- // 응답 기간 확인
449
+ // Check response period
450
450
  const now = new Date();
451
451
  if (now < collection.begin_date || now > collection.end_date) {
452
- throw new Error("응답 가능 기간이 아닙니다");
452
+ throw new Error("This is not within the response period");
453
453
  }
454
454
  }
455
455
 
456
- // 검증 통과 저장
456
+ // Save after validation passes
457
457
  const wdb = this.getPuri("w");
458
458
  spa.forEach((sp) => wdb.ubRegister("responses", sp));
459
459
 
@@ -463,32 +463,32 @@ async save(spa: ResponseSaveParams[]): Promise<number[]> {
463
463
  }
464
464
  ```
465
465
 
466
- **핵심 포인트:**
467
- - 검증 실패 명확한 에러 메시지
468
- - 검증을 모두 통과한 후에만 저장
469
- - 비즈니스 규칙을 코드로 강제
466
+ **Key points:**
467
+ - Clear error messages when validation fails
468
+ - Only save after all validations pass
469
+ - Enforce business rules through code
470
470
 
471
471
  ---
472
472
 
473
473
  ## IMPORTANT: Verify orderBy After Scaffolding
474
474
 
475
- ### 문제
475
+ ### Problem
476
476
 
477
- Sonamu UI에서 스캐폴딩 실행 model 파일이 **재생성**되면서 기본값(`id-desc`) 남고 커스텀 orderBy 케이스가 사라집니다.
477
+ When scaffolding is run from Sonamu UI, the model file is **regenerated**, leaving only the default value (`id-desc`) and losing any custom orderBy cases.
478
478
 
479
479
  ```
480
- 오류: Argument of type 'xxx-asc' is not assignable to parameter of type 'never'
480
+ Error: Argument of type 'xxx-asc' is not assignable to parameter of type 'never'
481
481
  ```
482
482
 
483
- ### 해결
483
+ ### Fix
484
484
 
485
- 스캐폴딩 model 파일에서 entity.json의 **모든 orderBy enum 케이스**를 exhaustive() 처리해야 합니다.
485
+ After scaffolding, you must exhaustively handle **all orderBy enum cases** from entity.json in the model file.
486
486
 
487
487
  ```typescript
488
- // entity.json orderBy enum
489
- { "TaskOrderBy": { "id-desc": "ID최신순", "created_at-desc": "등록일순", "title-asc": "제목순" } }
488
+ // entity.json orderBy enum
489
+ { "TaskOrderBy": { "id-desc": "ID Latest", "created_at-desc": "By Date", "title-asc": "By Title" } }
490
490
 
491
- // model - 스캐폴딩 반드시 확인/추가
491
+ // model - must verify/add after scaffolding
492
492
  if (params.orderBy) {
493
493
  if (params.orderBy === "id-desc") {
494
494
  qb.orderBy("tasks.id", "desc");
@@ -497,26 +497,26 @@ if (params.orderBy) {
497
497
  } else if (params.orderBy === "title-asc") {
498
498
  qb.orderBy("tasks.title", "asc");
499
499
  } else {
500
- exhaustive(params.orderBy); // 누락 컴파일 오류
500
+ exhaustive(params.orderBy); // compile error if any case is missing
501
501
  }
502
502
  }
503
503
  ```
504
504
 
505
- ### 체크리스트
505
+ ### Checklist
506
506
 
507
- - 스캐폴딩 model orderBy 케이스 확인
508
- - entity.json의 orderBy enum 일치하는지 확인
509
- - search 케이스, enhancers 다른 커스텀 로직도 확인
507
+ - Verify orderBy cases in model after scaffolding
508
+ - Confirm they match the orderBy enum in entity.json
509
+ - Also check other custom logic such as search cases and enhancers
510
510
 
511
511
  ---
512
512
 
513
- ## 코드 품질과 일관성
513
+ ## Code Quality and Consistency
514
514
 
515
- ### DRY 원칙: this.modelName 사용
515
+ ### DRY principle: use this.modelName
516
516
 
517
- 에러 메시지에서 모델명을 하드코딩하지 않고 `this.modelName`을 사용합니다.
517
+ Use `this.modelName` instead of hardcoding the model name in error messages.
518
518
 
519
- **BAD: 모델명 하드코딩**
519
+ **BAD: hardcoded model name**
520
520
  ```typescript
521
521
  // department.model.ts
522
522
  if (!rows[0]) {
@@ -529,206 +529,206 @@ if (!rows[0]) {
529
529
  }
530
530
  ```
531
531
 
532
- **GOOD: this.modelName 활용**
532
+ **GOOD: use this.modelName**
533
533
  ```typescript
534
- // 모든 Model 공통
534
+ // Common to all Models
535
535
  if (!rows[0]) {
536
536
  throw new NotFoundException(SD("notFound")(this.modelName, id));
537
537
  }
538
538
  ```
539
539
 
540
- **장점:**
541
- - 복붙 실수 방지: 다른 모델 코드 복사 모델명 수정 불필요
542
- - 일관성: 모든 모델이 동일한 패턴 사용
543
- - 유지보수: constructor의 modelName 변경하면 모든 에러 메시지 자동 반영
540
+ **Benefits:**
541
+ - Prevents copy-paste mistakes: no need to update the model name when copying from another model
542
+ - Consistency: all models use the same pattern
543
+ - Maintainability: changing modelName in the constructor automatically reflects in all error messages
544
544
 
545
- ### 일관된 i18n 사용
545
+ ### Consistent i18n Key Usage
546
546
 
547
- 프로젝트 전체에서 동일한 목적의 i18n 키를 일관되게 사용합니다.
547
+ Use the same i18n keys consistently for the same purpose across the entire project.
548
548
 
549
- **BAD: 중복된 i18n 키**
549
+ **BAD: duplicate i18n keys**
550
550
  ```typescript
551
- // 여러 모델에서 서로 다른 키 사용
551
+ // Different keys used across models
552
552
  throw new NotFoundException(SD("error.entityNotFound")(this.modelName, id));
553
553
  throw new NotFoundException(SD("error.notFound")(this.modelName, id));
554
554
  throw new NotFoundException(SD("notFound")(this.modelName, id));
555
555
 
556
- // 검색 필드 오류
556
+ // Search field error
557
557
  throw new BadRequestException(SD("error.unknownSearchField")(params.search));
558
558
  throw new BadRequestException(SD("error.invalidSearchField")(params.search));
559
559
  ```
560
560
 
561
- **GOOD: 표준 i18n 키 사용**
561
+ **GOOD: use standard i18n keys**
562
562
  ```typescript
563
- // Entity 조회 실패 - 짧고 명확
563
+ // Entity lookup failure - short and clear
564
564
  throw new NotFoundException(SD("notFound")(this.modelName, id));
565
565
 
566
- // 검색 필드 오류 - search 네임스페이스
566
+ // Search field error - search namespace
567
567
  throw new BadRequestException(SD("search.invalidField")(params.search));
568
568
  ```
569
569
 
570
- **권장 i18n 패턴:**
571
- | 상황 | i18n | 사용처 |
570
+ **Recommended i18n key patterns:**
571
+ | Situation | i18n key | Used in |
572
572
  |------|---------|--------|
573
- | Entity 조회 실패 | `notFound` | findById |
574
- | 잘못된 검색 필드 | `search.invalidField` | findMany search |
575
- | 필수 필드 누락 | `validation.required` | save 검증 |
576
- | 권한 없음 | `error.forbidden` | guards 실패 |
577
- | 로그인 필요 | `error.loginRequired` | Context.user null |
573
+ | Entity lookup failure | `notFound` | findById |
574
+ | Invalid search field | `search.invalidField` | findMany search |
575
+ | Missing required field | `validation.required` | save validation |
576
+ | Unauthorized | `error.forbidden` | guards failure |
577
+ | Login required | `error.loginRequired` | Context.user null |
578
578
 
579
- ### 벌크 리팩토링 전략
579
+ ### Bulk Refactoring Strategy
580
580
 
581
- 여러 모델 파일을 일관되게 수정할 sed 활용한 자동화:
581
+ When consistently modifying multiple model files, use sed for automation:
582
582
 
583
- **1단계: 패턴 확인**
583
+ **Step 1: Confirm pattern**
584
584
  ```bash
585
- # 수정 대상 파일 찾기
585
+ # Find files to modify
586
586
  grep -r 'SD("error.entityNotFound")' packages/api/src/application/*/
587
587
  ```
588
588
 
589
- **2단계: 변경 검증 (dry-run)**
589
+ **Step 2: Validate changes (dry-run)**
590
590
  ```bash
591
- # 변경될 내용 미리 확인
591
+ # Preview changes before applying
592
592
  sed -n 's/SD("error.entityNotFound")(\(.*\), id)/SD("notFound")(this.modelName, id)/p' file.ts
593
593
  ```
594
594
 
595
- **3단계: 일괄 적용**
595
+ **Step 3: Apply in bulk**
596
596
  ```bash
597
- # 모든 model 파일 수정
597
+ # Modify all model files
598
598
  find packages/api/src/application -name "*.model.ts" -exec sed -i '' \
599
599
  's/SD("error.entityNotFound")(\(.*\), id)/SD("notFound")(this.modelName, id)/g' {} \;
600
600
  ```
601
601
 
602
- **4단계: 빌드로 검증**
602
+ **Step 4: Validate with build**
603
603
  ```bash
604
- # TypeScript 타입 체크
604
+ # TypeScript type check
605
605
  pnpm typecheck
606
606
 
607
- # 전체 빌드
607
+ # Full build
608
608
  pnpm build
609
609
  ```
610
610
 
611
- **주의사항:**
612
- - 반드시 git commit 실행 (롤백 가능하도록)
613
- - dry-run으로 변경 내용 먼저 확인
614
- - 빌드로 타입 오류 체크
615
- - 테스트 실행으로 동작 검증
611
+ **Cautions:**
612
+ - Always run after a git commit (to allow rollback)
613
+ - Confirm changes with dry-run first
614
+ - Check for type errors with build
615
+ - Verify behavior with tests
616
616
 
617
- ### 타입 체크 패턴
617
+ ### Type Check Patterns
618
618
 
619
619
  **satisfies vs as const:**
620
620
 
621
621
  ```typescript
622
- // BAD: 타입 단언으로 타입 체크 우회
622
+ // BAD: bypasses type checking with type assertion
623
623
  const params = {
624
624
  num: 24,
625
625
  page: 1,
626
626
  search: "id" as const,
627
- orderBy: "wrong-value" as const, // 오류 감지 안 됨
627
+ orderBy: "wrong-value" as const, // error not detected
628
628
  ...rawParams,
629
629
  } as RoleListParams;
630
630
 
631
- // GOOD: satisfies로 컴파일 타임 검증
631
+ // GOOD: compile-time validation with satisfies
632
632
  const params = {
633
633
  num: 24,
634
634
  page: 1,
635
635
  search: "id" as const,
636
- orderBy: "wrong-value" as const, // 컴파일 오류 발생!
636
+ orderBy: "wrong-value" as const, // compile error!
637
637
  ...rawParams,
638
638
  } satisfies RoleListParams;
639
639
  ```
640
640
 
641
- **적용 권장 위치:**
642
- - findMany의 params 기본값
643
- - 복잡한 객체 리터럴 (타입 체크가 중요한 경우)
641
+ **Recommended usage locations:**
642
+ - Default values for params in findMany
643
+ - Complex object literals (where type checking is important)
644
644
 
645
- ### IMPORTANT: ListParams / findMany / SearchField 동기화
645
+ ### IMPORTANT: ListParams / findMany / SearchField Synchronization
646
646
 
647
- 다음 가지는 항상 일관성을 유지해야 한다. 하나라도 어긋나면 선언만 있고 동작하지 않거나, 런타임 오류가 발생한다.
647
+ The following three must always remain consistent. If any one is out of sync, the feature either exists as a declaration only with no behavior, or a runtime error will occur.
648
648
 
649
- 1. `entity.json`의 `SearchField` enum values
650
- 2. `types.ts`의 `ListParams` 필드 정의
651
- 3. `model.ts`의 `findMany` 필터/검색 처리 코드
649
+ 1. `SearchField` enum values in `entity.json`
650
+ 2. `ListParams` field definitions in `types.ts`
651
+ 3. Filter/search handling code in `findMany` in `model.ts`
652
652
 
653
- **체크리스트:**
654
- - [ ] SearchField에 선언된 모든 값이 findMany에 구현되어 있는가?
655
- - [ ] 주석 처리된 필터 분기가 있다면 제거하거나 구현하거나 하나
656
- - [ ] 요구사항의 "~별 필터", "~로 검색" 기능이 ListParams에 반영되어 있는가?
653
+ **Checklist:**
654
+ - [ ] Are all values declared in SearchField implemented in findMany?
655
+ - [ ] If any filter branch is commented out, either remove it or implement it
656
+ - [ ] Are "filter by ~", "search by ~" features from requirements reflected in ListParams?
657
657
 
658
- **특히 승인 워크플로우가 있는 엔티티는 status 필터를 반드시 추가한다.**
659
- (단계별 건수 클릭해당 목록만 필터링 조회 패턴이 공통으로 요구됨)
658
+ **In particular, entities with an approval workflow must always add a status filter.**
659
+ (Clicking count by stage filter to show only that list is a commonly required pattern)
660
660
 
661
661
  ```typescript
662
- // types.ts - 승인 워크플로우 엔티티 예시
662
+ // types.ts - approval workflow entity example
663
663
  export const AchievementListParams = AchievementBaseListParams.extend({
664
664
  status: z.nativeEnum(AchievementStatus).optional(),
665
665
  achievement_type: z.nativeEnum(AchievementType).optional(),
666
666
  submitter_id: z.string().optional(),
667
667
  });
668
668
 
669
- // model.ts - 대응하는 필터 구현
669
+ // model.ts - corresponding filter implementation
670
670
  if (params.status) qb.where("achievements.status", params.status);
671
671
  if (params.achievement_type) qb.where("achievements.achievement_type", params.achievement_type);
672
672
  if (params.submitter_id) qb.where("achievements.submitter_id", params.submitter_id);
673
673
  ```
674
674
 
675
- **DO NOT - 선언/구현 불일치:**
675
+ **DO NOT - declaration/implementation mismatch:**
676
676
  ```typescript
677
- // entity.json에 SearchField "title" 선언
678
- // model.ts에서 "id" 케이스만 처리하고 "title" 주석 처리
677
+ // SearchField "title" declared in entity.json
678
+ // model.ts only handles "id" case, "title" is commented out
679
679
  if (params.search === "id") {
680
680
  // ...
681
681
  } /* else if (params.search === "title") {
682
- // TODO: 미구현
682
+ // TODO: not implemented
683
683
  } */
684
684
  ```
685
685
 
686
- ### 코드 리뷰 체크리스트
686
+ ### Code Review Checklist
687
687
 
688
- 새로운 Model 작성 시:
689
- - [ ] `this.modelName` 사용 (하드코딩 금지)
690
- - [ ] 표준 i18n 사용 (`notFound`, `search.invalidField`)
691
- - [ ] satisfies 키워드 활용 (타입 안전성)
692
- - [ ] debug 옵션 불필요하게 명시하지 않음
693
- - [ ] orderBy 모든 케이스 exhaustive 처리
694
- - [ ] ManyToMany relation 있으면 _ids 배열 SaveParams에 추가
695
- - [ ] `@upload` 메서드에 `@api`가 붙어 있지 않은가? (`@upload`는 단독 사용, 함께 쓰면 빌드 에러)
696
- - [ ] SearchField enum findMany 구현이 일치하는가?
697
- - [ ] 승인 워크플로우 엔티티에 status/type 필터가 ListParams와 findMany 양쪽에 모두 있는가?
688
+ When writing a new Model:
689
+ - [ ] Use `this.modelName` (no hardcoding)
690
+ - [ ] Use standard i18n keys (`notFound`, `search.invalidField`)
691
+ - [ ] Use the `satisfies` keyword (type safety)
692
+ - [ ] Do not unnecessarily specify the debug option
693
+ - [ ] Exhaustively handle all orderBy cases
694
+ - [ ] If a ManyToMany relation exists, add _ids array to SaveParams
695
+ - [ ] Does the `@upload` method have `@api` on it? (`@upload` is used standalone; using both together causes a build error)
696
+ - [ ] Do the SearchField enum and findMany implementation match?
697
+ - [ ] For entities with approval workflows, are status/type filters present in both ListParams and findMany?
698
698
 
699
- 20개 Model 일괄 수정 시:
700
- - [ ] miomock 같은 레퍼런스 코드와 패턴 비교
701
- - [ ] 불일치하는 패턴 우선순위 정리
702
- - [ ] sed 등으로 자동화 스크립트 작성
703
- - [ ] 변경 git commit
704
- - [ ] dry-run으로 변경 내용 검증
705
- - [ ] pnpm typecheck로 타입 오류 확인
706
- - [ ] pnpm test로 동작 검증
707
- - [ ] `any` 타입 사용 여부 (사용 금지)
699
+ When bulk-modifying 20+ Models:
700
+ - [ ] Compare patterns with reference code like miomock
701
+ - [ ] Prioritize inconsistent patterns
702
+ - [ ] Write an automation script using sed or similar
703
+ - [ ] Commit to git before making changes
704
+ - [ ] Validate changes with dry-run
705
+ - [ ] Check for type errors with pnpm typecheck
706
+ - [ ] Verify behavior with pnpm test
707
+ - [ ] Check for `any` type usage (prohibited)
708
708
 
709
- ### any 타입 금지
709
+ ### Prohibition on any type
710
710
 
711
- `any` 타입은 TypeScript 타입 안전성을 무력화하므로 **절대 사용하지 않는다.**
711
+ The `any` type neutralizes TypeScript's type safety and must **never be used**.
712
712
 
713
- **BAD: any 사용**
713
+ **BAD: using any**
714
714
  ```typescript
715
715
  const { category_ids, ...data } = sp as any;
716
716
  function process(input: any) { ... }
717
717
  ```
718
718
 
719
- **GOOD: 정확한 타입 또는 unknown 사용**
719
+ **GOOD: use precise types or unknown**
720
720
  ```typescript
721
- // 정확한 타입으로 구조 분해
721
+ // Destructure with a precise type
722
722
  const { category_ids, ...data } = sp as QuestionCollectionSaveParams;
723
723
 
724
- // 타입을 없을 때는 unknown (any 대신)
724
+ // Use unknown when the type is not known (instead of any)
725
725
  function process(input: unknown) {
726
726
  if (typeof input === "string") { ... }
727
727
  }
728
728
  ```
729
729
 
730
- **규칙:**
731
- - `any`는 사용 금지
732
- - 타입을 모를 때는 `unknown` 사용 타입 가드로 좁힌다
733
- - 구조 분해 타입 단언이 필요하면 정확한 타입명을 명시한다 (`as ConcreteType`)
734
- - `eslint-disable @typescript-eslint/no-explicit-any` 같은 억제 주석도 사용 금지
730
+ **Rules:**
731
+ - `any` is prohibited
732
+ - When the type is unknown, use `unknown` and narrow with a type guard
733
+ - When a type assertion is needed during destructuring, specify the exact type name (`as ConcreteType`)
734
+ - Suppression comments like `eslint-disable @typescript-eslint/no-explicit-any` are also prohibited