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,28 +1,28 @@
1
1
  ---
2
2
  name: sonamu-testing
3
- description: Sonamu 테스트 작성. bootstrap, test/testAs 함수, Fixture 생성, Naite.get() 검증, expectQuery/expectUB 헬퍼, Mock 패턴. Use when writing or structuring test code for Models and APIs.
3
+ description: Writing Sonamu tests. bootstrap, test/testAs functions, Fixture creation, Naite.get() assertions, expectQuery/expectUB helpers, Mock patterns. Use when writing or structuring test code for Models and APIs.
4
4
  ---
5
5
 
6
- # Sonamu 테스트 시스템
6
+ # Sonamu Test System
7
7
 
8
- Sonamu Vitest 기반 테스트 환경을 제공한다. 테스트는 트랜잭션으로 격리되어 자동 롤백된다.
8
+ Sonamu provides a Vitest-based test environment. Each test is isolated in a transaction and automatically rolled back.
9
9
 
10
- **예시 프로젝트**: `sonamu/examples/miomock` - 실제 테스트 코드 참고
10
+ **Example project**: `sonamu/examples/miomock` - reference for real test code
11
11
 
12
- **WARNING: 엔티티 10 이상 프로젝트는 반드시 배치 전략 사용** (아래 "대규모 프로젝트 전략" 참고)
12
+ **WARNING: Projects with 10 or more entities must use a batch strategy** (see "Large-Scale Project Strategy" below)
13
13
 
14
- **참고 문서**:
14
+ **Reference documents**:
15
15
 
16
- - **Fixture CLI 명령어**: `fixture-cli.md` - fixture gen/fetch/explore 사용법, 3-Tier DB 구조
17
- - **Fixture 생성 팁**: 문서 하단 "Fixture 데이터 생성 " 섹션 또는 `fixture-cli.md` "실전 팁" 섹션
16
+ - **Fixture CLI commands**: `fixture-cli.md` - fixture gen/fetch/explore usage, 3-Tier DB structure
17
+ - **Fixture creation tips**: "Fixture Data Creation Tips" section at the bottom of this document, or the "Practical Tips" section in `fixture-cli.md`
18
18
 
19
19
  ---
20
20
 
21
- ## Quick Start - 테스트 작성 빠른 시작
21
+ ## Quick Start - Getting Started with Tests Quickly
22
22
 
23
- **전제조건**: scaffolding 완료, types.ts nullable 필드 처리 완료
23
+ **Prerequisites**: scaffolding completed, nullable field handling in types.ts completed
24
24
 
25
- ### 1단계: test-helpers.ts 확장
25
+ ### Step 1: Extend test-helpers.ts
26
26
 
27
27
  ```typescript
28
28
  // packages/api/src/application/__tests__/test-helpers.ts
@@ -34,7 +34,7 @@ import UserModel from "../user/user.model";
34
34
  import PostModel from "../post/post.model";
35
35
  import CommentModel from "../comment/comment.model";
36
36
 
37
- // User 헬퍼
37
+ // User helper
38
38
  export async function createTestUser(
39
39
  params?: Partial<UserSaveParams>,
40
40
  ): Promise<number> {
@@ -47,13 +47,13 @@ export async function createTestUser(
47
47
  return id;
48
48
  }
49
49
 
50
- // User with dependencies (의존성 체인)
50
+ // User with dependencies (dependency chain)
51
51
  export async function createTestUserWithDeps() {
52
52
  const userId = await createTestUser();
53
53
  return { userId };
54
54
  }
55
55
 
56
- // Post 헬퍼
56
+ // Post helper
57
57
  export async function createTestPost(
58
58
  authorId: number,
59
59
  params?: Partial<PostSaveParams>,
@@ -75,7 +75,7 @@ export async function createTestPostWithDeps() {
75
75
  return { userId, postId };
76
76
  }
77
77
 
78
- // Comment 헬퍼
78
+ // Comment helper
79
79
  export async function createTestComment(
80
80
  postId: number,
81
81
  authorId: number,
@@ -99,66 +99,66 @@ export async function createTestCommentWithDeps() {
99
99
  }
100
100
  ```
101
101
 
102
- **CRITICAL 패턴**:
102
+ **CRITICAL patterns**:
103
103
 
104
- - `createTestX()`: 기본 생성 헬퍼 (params로 override 가능)
105
- - `createTestXWithDeps()`: 의존성 자동 처리 헬퍼 (모든 필요 데이터 함께 생성)
106
- - FK 필드는 `_id` 접미사 사용 (`author_id`, `post_id`)
107
- - 반환: 주로 ID 반환, WithDeps 객체로 여러 ID 반환
104
+ - `createTestX()`: basic creation helper (overridable via params)
105
+ - `createTestXWithDeps()`: helper that automatically handles dependencies (creates all required data together)
106
+ - FK fields use the `_id` suffix (`author_id`, `post_id`)
107
+ - Returns: primarily returns ID; WithDeps returns an object with multiple IDs
108
108
 
109
- **CRITICAL: 모든 필수 필드 포함 필수!**
109
+ **CRITICAL: All required fields must be included!**
110
110
 
111
- Sonamu `ubUpsert`는 PostgreSQL `ON CONFLICT ... DO UPDATE` 쿼리를 사용합니다.
112
- 업데이트 시에도 **모든 필수 필드(NOT NULL 제약이 있는 필드)**를 포함해야 합니다.
111
+ Sonamu's `ubUpsert` uses PostgreSQL's `ON CONFLICT ... DO UPDATE` query.
112
+ Even for updates, **all required fields (fields with NOT NULL constraints)** must be included.
113
113
 
114
- 필수 필드 누락 시:
114
+ When required fields are missing:
115
115
 
116
116
  ```typescript
117
- // BAD - content 필수 필드 누락
117
+ // BAD - missing required field content
118
118
  const post: PostSaveParams = {
119
119
  author_id: authorId,
120
120
  title: "Test",
121
- // content 누락! → ubUpsert ON CONFLICT UPDATE NULL 설정 시도 → DB 에러
121
+ // content missing! → ubUpsert ON CONFLICT UPDATE attempts to set NULL → DB error
122
122
  };
123
123
  // Error: null value in column "content" violates not-null constraint
124
124
  ```
125
125
 
126
- ### 필수 필드 vs 선택 필드 구분
126
+ ### Distinguishing Required vs Optional Fields
127
127
 
128
- **1. entity.json 확인**
128
+ **1. Check entity.json**
129
129
 
130
130
  ```json
131
131
  // post.entity.json
132
132
  {
133
133
  "props": [
134
- { "name": "id", "type": "integer" }, // 자동 생성 - 제외
135
- { "name": "title", "type": "string", "length": 255 }, // 필수! (nullable 없음)
136
- { "name": "content", "type": "string" }, // 필수! (nullable 없음)
137
- { "name": "category", "type": "string", "nullable": true }, // 선택 (nullable)
138
- { "name": "author_id", "type": "integer" }, // 필수! (FK, nullable 없음)
139
- { "name": "view_count", "type": "integer", "dbDefault": "0" }, // 필수이지만 DB 기본값 있음
140
- { "name": "created_at", "type": "date", "dbDefault": "CURRENT_TIMESTAMP" } // 자동
134
+ { "name": "id", "type": "integer" }, // auto-generated - exclude
135
+ { "name": "title", "type": "string", "length": 255 }, // required! (no nullable)
136
+ { "name": "content", "type": "string" }, // required! (no nullable)
137
+ { "name": "category", "type": "string", "nullable": true }, // optional (nullable)
138
+ { "name": "author_id", "type": "integer" }, // required! (FK, no nullable)
139
+ { "name": "view_count", "type": "integer", "dbDefault": "0" }, // required but has DB default
140
+ { "name": "created_at", "type": "date", "dbDefault": "CURRENT_TIMESTAMP" } // automatic
141
141
  ]
142
142
  }
143
143
  ```
144
144
 
145
- **필수 필드 (Required)**: `nullable: true`가 **없는** 필드
145
+ **Required fields**: Fields **without** `nullable: true`
146
146
 
147
147
  - `title`, `content`, `author_id`
148
- - test-helpers.ts에서 **반드시** 기본값 제공
148
+ - **Must** provide default values in test-helpers.ts
149
149
 
150
- **선택 필드 (Optional)**: `nullable: true`가 **있는** 필드
150
+ **Optional fields**: Fields **with** `nullable: true`
151
151
 
152
152
  - `category`
153
- - test-helpers.ts에서 생략 가능
153
+ - Can be omitted in test-helpers.ts
154
154
 
155
- **제외 필드**:
155
+ **Excluded fields**:
156
156
 
157
- - `id`: 자동 증가 (save 자동 생성)
158
- - `created_at`: dbDefault가 있어 자동 설정
159
- - `view_count`: dbDefault="0"이 있어 자동 설정
157
+ - `id`: auto-increment (auto-generated on save)
158
+ - `created_at`: automatically set by dbDefault
159
+ - `view_count`: automatically set by dbDefault="0"
160
160
 
161
- **2. test-helpers.ts 작성**
161
+ **2. Write test-helpers.ts**
162
162
 
163
163
  ```typescript
164
164
  export async function createTestPost(
@@ -166,32 +166,32 @@ export async function createTestPost(
166
166
  params?: Partial<PostSaveParams>,
167
167
  ): Promise<number> {
168
168
  const post: PostSaveParams = {
169
- // 필수 필드는 반드시 포함 (nullable이 없는 필드)
169
+ // Required fields must be included (fields without nullable)
170
170
  author_id: authorId,
171
- title: "Test Post", // 필수!
172
- content: "Test content", // 필수!
171
+ title: "Test Post", // required!
172
+ content: "Test content", // required!
173
173
 
174
- // 선택 필드는 생략 가능 (nullable: true인 필드)
175
- // category: null, // 생략 가능
174
+ // Optional fields can be omitted (fields with nullable: true)
175
+ // category: null, // can be omitted
176
176
 
177
- // dbDefault 있는 필드도 생략 가능
178
- // view_count: 0, // dbDefault="0"이므로 생략 가능
177
+ // Fields with dbDefault can also be omitted
178
+ // view_count: 0, // can be omitted since dbDefault="0"
179
179
 
180
- ...params, // override 허용
180
+ ...params, // allow override
181
181
  };
182
182
  const saved = await PostModel.save(post);
183
183
  return saved.id;
184
184
  }
185
185
  ```
186
186
 
187
- **규칙 요약**:
187
+ **Rule summary**:
188
188
 
189
- 1. entity.json에서 `nullable: true` 없는 필드 = 필수 필드
190
- 2. 필수 필드는 test-helpers.ts에 **반드시** 기본값 포함
191
- 3. `id`, `created_at`, `dbDefault` 있는 필드는 제외 가능
192
- 4. ubUpsert ON CONFLICT UPDATE 시에도 필수 필드 필요
189
+ 1. Fields without `nullable: true` in entity.json = required fields
190
+ 2. Required fields **must** have default values in test-helpers.ts
191
+ 3. `id`, `created_at`, fields with `dbDefault` can be excluded
192
+ 4. Required fields are also needed for ubUpsert's ON CONFLICT UPDATE
193
193
 
194
- ### 2단계: 테스트 파일 작성
194
+ ### Step 2: Write the test file
195
195
 
196
196
  ```typescript
197
197
  // packages/api/src/application/post/__tests__/post.test.ts
@@ -201,11 +201,11 @@ import { describe, test, expect, vi } from "vitest";
201
201
  import PostModel from "../post.model";
202
202
  import { createTestPostWithDeps } from "../../__tests__/test-helpers";
203
203
 
204
- bootstrap(vi); // CRITICAL: 필수!
204
+ bootstrap(vi); // CRITICAL: required!
205
205
 
206
206
  describe("PostModel", () => {
207
- describe("A. Create (생성)", () => {
208
- test("게시글 생성", async () => {
207
+ describe("A. Create", () => {
208
+ test("create post", async () => {
209
209
  const { userId, postId } = await createTestPostWithDeps();
210
210
 
211
211
  const post = await PostModel.findById(postId, ["A"]);
@@ -214,7 +214,7 @@ describe("PostModel", () => {
214
214
  });
215
215
  });
216
216
 
217
- describe("B. Read (조회)", () => {
217
+ describe("B. Read", () => {
218
218
  test("findById - Subset A", async () => {
219
219
  const { postId } = await createTestPostWithDeps();
220
220
 
@@ -224,7 +224,7 @@ describe("PostModel", () => {
224
224
  expect(post).toHaveProperty("content");
225
225
  });
226
226
 
227
- test("findMany - 목록 조회", async () => {
227
+ test("findMany - list query", async () => {
228
228
  await createTestPostWithDeps();
229
229
  await createTestPostWithDeps();
230
230
 
@@ -233,8 +233,8 @@ describe("PostModel", () => {
233
233
  });
234
234
  });
235
235
 
236
- describe("C. Update (수정)", () => {
237
- test("게시글 수정", async () => {
236
+ describe("C. Update", () => {
237
+ test("update post", async () => {
238
238
  const { postId } = await createTestPostWithDeps();
239
239
 
240
240
  await PostModel.save([
@@ -249,8 +249,8 @@ describe("PostModel", () => {
249
249
  });
250
250
  });
251
251
 
252
- describe("D. Delete (삭제)", () => {
253
- test("게시글 삭제", async () => {
252
+ describe("D. Delete", () => {
253
+ test("delete post", async () => {
254
254
  const { postId } = await createTestPostWithDeps();
255
255
 
256
256
  await PostModel.del(postId);
@@ -260,21 +260,21 @@ describe("PostModel", () => {
260
260
  });
261
261
  });
262
262
 
263
- describe("E. Business Logic (비즈니스 로직)", () => {
264
- test("게시글 발행부터 댓글 추가까지 전체 프로세스", async () => {
265
- // 1. 게시글 작성
263
+ describe("E. Business Logic", () => {
264
+ test("full process from post creation to adding a comment", async () => {
265
+ // 1. create post
266
266
  const { userId, postId } = await createTestPostWithDeps({
267
- title: " ",
268
- content: "내용",
267
+ title: "New Post",
268
+ content: "Content",
269
269
  });
270
270
 
271
- // 2. 다른 사용자가 댓글 작성
271
+ // 2. another user writes a comment
272
272
  const commenterId = await createTestUser();
273
273
  const commentId = await createTestComment(postId, commenterId, {
274
- content: "좋은 글이네요!",
274
+ content: "Great post!",
275
275
  });
276
276
 
277
- // 3. 게시글 조회 (댓글 포함)
277
+ // 3. fetch post (with comments)
278
278
  const post = await PostModel.findById(postId, ["A"]);
279
279
  expect(post.comments).toHaveLength(1);
280
280
  expect(post.comments[0].id).toBe(commentId);
@@ -283,110 +283,110 @@ describe("PostModel", () => {
283
283
  });
284
284
  ```
285
285
 
286
- **패턴 요약**:
286
+ **Pattern summary**:
287
287
 
288
- - `bootstrap(vi)` 호출 필수
289
- - `describe` + `test` 패턴 (순서: A. Create, B. Read, C. Update, D. Delete, E. Business Logic)
290
- - `createTestXWithDeps()` 헬퍼로 의존성 자동 해결
291
- - Business Logic 섹션이 가장 중요! (실제 업무 시나리오 구현)
288
+ - `bootstrap(vi)` call is required
289
+ - `describe` + `test` pattern (order: A. Create, B. Read, C. Update, D. Delete, E. Business Logic)
290
+ - Use `createTestXWithDeps()` helper to automatically resolve dependencies
291
+ - The Business Logic section is the most important! (implements real business scenarios)
292
292
 
293
- ### 3단계: 테스트 실행
293
+ ### Step 3: Run tests
294
294
 
295
295
  ```bash
296
- # dev 서버가 내려가 있으면 먼저 실행
296
+ # Start dev server if it's down
297
297
  pnpm sonamu dev
298
298
 
299
- # 개발 테스트 (기본)
299
+ # Tests during development (default)
300
300
  pnpm sonamu test
301
301
  pnpm sonamu test user.model
302
302
  ```
303
303
 
304
- **완료!** 상세한 내용은 아래 섹션 참조.
304
+ **Done!** See the sections below for detailed information.
305
305
 
306
306
  ---
307
307
 
308
- ## 테스트 작성 전 체크리스트
308
+ ## Pre-Test Writing Checklist
309
309
 
310
- - [ ] **엔티티 설계 완료 확인** - `pnpm db:migration` `pnpm scaffolding` 오류 없이 완료
311
- - [ ] **테스트 작성 계획 수립** - 업무 프로세스별 엔티티 그룹핑 (→ 아래 "테스트 작성 계획 수립" 참조)
312
- - [ ] **types.ts nullable 처리 (FIRST!)** - 엔티티 생성 직후 nullable 필드 partial + extend 처리 (→ 아래 "엔티티 생성 즉시 해야 작업" 참조)
313
- - [ ] **Seed Data 준비** - FK 제약으로 인한 기본 데이터 필요 (→ database.md "최소 seed data" 참고)
314
- - [ ] **테스트 헬퍼 함수** - 복잡한 엔티티 의존성 처리용 헬퍼 준비
315
- - [ ] **엔티티 10 이상 시** - 배치 전략 수립 (아래 "대규모 프로젝트 전략" 참고)
310
+ - [ ] **Confirm entity design is complete** - `pnpm db:migration` and `pnpm scaffolding` completed without errors
311
+ - [ ] **Plan test writing** - group entities by business process (→ see "Test Writing Plan" below)
312
+ - [ ] **Handle nullable fields in types.ts (FIRST!)** - immediately after entity creation, apply partial + extend handling for nullable fields (→ see "Tasks to Do Immediately After Entity Creation" below)
313
+ - [ ] **Prepare Seed Data** - base data required due to FK constraints (→ see "minimum seed data" in database.md)
314
+ - [ ] **Test helper functions** - prepare helpers for handling complex entity dependencies
315
+ - [ ] **For 10 or more entities** - plan batch strategy (see "Large-Scale Project Strategy" below)
316
316
 
317
- ## 테스트 작성 핵심 원칙
317
+ ## Core Test Writing Principles
318
318
 
319
- ### 1. 실제 구조 확인 우선
319
+ ### 1. Verify Actual Structure First
320
320
 
321
- **CRITICAL: 테스트 계획 전에 반드시 실제 엔티티 구조를 확인하세요.**
321
+ **CRITICAL: Always verify the actual entity structure before planning tests.**
322
322
 
323
- 테스트를 작성하기 전에 다음을 확인해야 합니다:
323
+ Before writing tests, you must verify the following:
324
324
 
325
325
  ```typescript
326
- // STEP 1: entity.json 확인
327
- // - 실제 필드명과 타입
328
- // - nullable 여부
329
- // - enum 목록
330
- // - relation 구조
326
+ // STEP 1: Check entity.json
327
+ // - actual field names and types
328
+ // - nullable status
329
+ // - enum value list
330
+ // - relation structure
331
331
 
332
- // STEP 2: types.ts 확인
333
- // - SaveParams의 partial 설정
334
- // - nullable 필드의 nullish 처리
335
- // - ManyToMany relation의 _ids 배열
332
+ // STEP 2: Check types.ts
333
+ // - partial settings in SaveParams
334
+ // - nullish handling for nullable fields
335
+ // - _ids arrays for ManyToMany relations
336
336
 
337
- // STEP 3: sonamu.generated.ts 확인
338
- // - Enum 타입 정의
339
- // - Subset 타입 구조
340
- // - BaseSchema 구조
337
+ // STEP 3: Check sonamu.generated.ts
338
+ // - Enum type definitions
339
+ // - Subset type structure
340
+ // - BaseSchema structure
341
341
  ```
342
342
 
343
- **잘못된 접근:**
343
+ **Wrong approach:**
344
344
 
345
345
  ```typescript
346
- // BAD - 추측으로 테스트 작성
347
- test("사용자 생성", async () => {
346
+ // BAD - writing tests based on guesses
347
+ test("create user", async () => {
348
348
  const [userId] = await UserModel.save([
349
349
  {
350
350
  name: "Test",
351
- status: "active", // 실제로는 "normal"일 수 있음
352
- role: "user", // 실제로는 "normal"일 수 있음
351
+ status: "active", // may actually be "normal"
352
+ role: "user", // may actually be "normal"
353
353
  },
354
354
  ]);
355
355
  });
356
356
  ```
357
357
 
358
- **올바른 접근:**
358
+ **Correct approach:**
359
359
 
360
360
  ```typescript
361
- // GOOD - entity.json 확인 후 작성
362
- // 1. user.entity.json 확인:
361
+ // GOOD - write after checking entity.json
362
+ // 1. Check user.entity.json:
363
363
  // - role: enum ["admin", "normal", "guest"]
364
364
  // - status: enum ["active", "inactive"] with dbDefault: "active"
365
365
  // - name: string (required)
366
366
  // - email: string (nullable)
367
367
 
368
- // 2. user.types.ts 확인:
369
- // - SaveParams에서 status, email partial 처리됨
368
+ // 2. Check user.types.ts:
369
+ // - status, email are partial in SaveParams
370
370
 
371
- // 3. 테스트 작성
372
- test("사용자 생성", async () => {
371
+ // 3. Write test
372
+ test("create user", async () => {
373
373
  const [userId] = await UserModel.save([
374
374
  {
375
375
  name: "Test",
376
- role: "normal", // entity.json의 정확한 enum 값
377
- // status dbDefault가 있어 생략 가능
378
- // email nullable이므로 생략 가능
376
+ role: "normal", // exact enum value from entity.json
377
+ // status can be omitted since it has dbDefault
378
+ // email can be omitted since it's nullable
379
379
  },
380
380
  ]);
381
381
  });
382
382
  ```
383
383
 
384
- ### 2. Subset 구조 이해
384
+ ### 2. Understanding Subset Structure
385
385
 
386
- **중첩된 관계는 dot notation으로 접근합니다.**
386
+ **Access nested relations using dot notation.**
387
387
 
388
388
  ```typescript
389
- // entity.json에서 Subset 정의 확인
389
+ // Check Subset definition in entity.json
390
390
  {
391
391
  "subsets": {
392
392
  "A": [
@@ -394,36 +394,36 @@ test("사용자 생성", async () => {
394
394
  "title",
395
395
  "evaluation_form.id", // BelongsToOne relation
396
396
  "evaluation_form.title",
397
- "evaluation_form.category.id", // 중첩 relation
397
+ "evaluation_form.category.id", // nested relation
398
398
  "evaluation_form.category.name"
399
399
  ]
400
400
  }
401
401
  }
402
402
 
403
- // 테스트에서 접근
404
- test("평가 항목 조회", async () => {
403
+ // Access in tests
404
+ test("fetch evaluation item", async () => {
405
405
  const { itemId } = await createTestEvaluationItemWithDeps();
406
406
 
407
407
  const item = await EvaluationItemModel.findById("A", itemId);
408
408
 
409
- // CORRECT - dot notation으로 중첩 접근
409
+ // CORRECT - nested access via dot notation
410
410
  expect(item.evaluation_form.id).toBe(formId);
411
- expect(item.evaluation_form.category.name).toBe("역량평가");
411
+ expect(item.evaluation_form.category.name).toBe("Competency Evaluation");
412
412
 
413
- // WRONG - 직접 FK 접근 시도
414
- // expect(item.evaluation_form_id).toBe(formId); // 타입 에러!
413
+ // WRONG - attempting direct FK access
414
+ // expect(item.evaluation_form_id).toBe(formId); // type error!
415
415
  });
416
416
  ```
417
417
 
418
- **중요 규칙:**
418
+ **Important rules:**
419
419
 
420
- - BelongsToOne relation FK는 Subset에서 `relation.id` 형태로 정의됨
421
- - 테스트에서는 `entity.relation.field` 형태로 접근
422
- - 직접 `entity.relation_id` 접근은 불가능 (Subset에 포함되지 않음)
420
+ - FK of BelongsToOne relation is defined as `relation.id` form in Subset
421
+ - Access in tests as `entity.relation.field` form
422
+ - Direct `entity.relation_id` access is not possible (not included in Subset)
423
423
 
424
- ### 3. DECIMAL 타입 처리
424
+ ### 3. Handling DECIMAL Types
425
425
 
426
- **DECIMAL 타입은 PostgreSQL에서 `.00` 접미사를 포함하여 반환됩니다.**
426
+ **DECIMAL types are returned from PostgreSQL with a `.00` suffix.**
427
427
 
428
428
  ```typescript
429
429
  // entity.json
@@ -433,157 +433,157 @@ test("평가 항목 조회", async () => {
433
433
  ]
434
434
  }
435
435
 
436
- // Migration에서 생성됨
436
+ // Generated in migration
437
437
  table.decimal("salary", 10, 2); // DECIMAL(10,2)
438
438
 
439
- // 테스트 작성
440
- test("급여 정보 조회", async () => {
439
+ // Writing tests
440
+ test("fetch salary info", async () => {
441
441
  const [userId] = await UserModel.save([{
442
442
  name: "Test",
443
- salary: 75000, // 입력: 숫자
443
+ salary: 75000, // input: number
444
444
  }]);
445
445
 
446
446
  const user = await UserModel.findById("A", userId);
447
447
 
448
- // WRONG - 정확한 비교는 실패할 수 있음
449
- // expect(user.salary).toBe(75000); // DB에서 "75000.00" 반환 가능
448
+ // WRONG - exact comparison may fail
449
+ // expect(user.salary).toBe(75000); // DB may return "75000.00"
450
450
 
451
- // CORRECT - toMatch()로 패턴 매칭
451
+ // CORRECT - pattern matching with toMatch()
452
452
  expect(String(user.salary)).toMatch(/^75000(\.00)?$/);
453
453
 
454
- // 또는 숫자 변환 비교
454
+ // Or convert to number and compare
455
455
  expect(Number(user.salary)).toBe(75000);
456
456
 
457
- // 또는 범위 체크
457
+ // Or range check
458
458
  expect(user.salary).toBeGreaterThanOrEqual(74999.99);
459
459
  expect(user.salary).toBeLessThanOrEqual(75000.01);
460
460
  });
461
461
  ```
462
462
 
463
- **DECIMAL 타입 비교 패턴:**
463
+ **DECIMAL type comparison patterns:**
464
464
 
465
465
  ```typescript
466
- // 패턴 1: 문자열 패턴 매칭
466
+ // Pattern 1: string pattern matching
467
467
  expect(String(value)).toMatch(/^1234\.56$/);
468
- expect(String(value)).toMatch(/^1234(\.56)?$/); // .56 선택적
468
+ expect(String(value)).toMatch(/^1234(\.56)?$/); // .56 optional
469
469
 
470
- // 패턴 2: 숫자 변환 비교
470
+ // Pattern 2: convert to number and compare
471
471
  expect(Number(value)).toBe(1234.56);
472
472
 
473
- // 패턴 3: 범위 체크 (부동소수점 오차 고려)
474
- expect(value).toBeCloseTo(1234.56, 2); // 소수점 2자리까지
473
+ // Pattern 3: range check (considering floating point errors)
474
+ expect(value).toBeCloseTo(1234.56, 2); // up to 2 decimal places
475
475
 
476
- // 패턴 4: toMatchObject (객체 비교 )
476
+ // Pattern 4: toMatchObject (when comparing objects)
477
477
  expect(result).toMatchObject({
478
- salary: expect.any(Number), // 타입만 체크
478
+ salary: expect.any(Number), // type check only
479
479
  });
480
480
  ```
481
481
 
482
- ## Enum 사용 규칙
482
+ ## Enum Value Usage Rules
483
483
 
484
- **CRITICAL: entity.json에 정의된 enum 값만 사용해야 합니다.**
484
+ **CRITICAL: Only use enum values defined in entity.json.**
485
485
 
486
- ### 규칙
486
+ ### Rules
487
487
 
488
- 1. entity.json에서 enum 필드의 정확한 목록 확인
489
- 2. 가능하면 `sonamu.generated.ts`의 TypeScript enum 타입 사용 (타입 안전)
490
- 3. test-helpers.ts에 기본값으로 유효한 enum 값 설정
491
- 4. 임의의 문자열 사용 금지
488
+ 1. Check the exact value list for enum fields in entity.json
489
+ 2. If possible, use TypeScript enum types from `sonamu.generated.ts` (type-safe)
490
+ 3. Set valid enum values as defaults in test-helpers.ts
491
+ 4. Do not use arbitrary strings
492
492
 
493
493
  ```typescript
494
- // WRONG: 추측으로 작성
495
- role: "user"; // entity.json에는 "normal"로 정의됨
496
- status: "in_progress"; // entity.json에는 "pending"로 정의됨
494
+ // WRONG: written based on guesses
495
+ role: "user"; // entity.json defines it as "normal"
496
+ status: "in_progress"; // entity.json defines it as "pending"
497
497
 
498
- // CORRECT: entity.json 확인 후 작성
499
- role: "normal"; // entity.json의 정확한 값
500
- status: "pending"; // entity.json의 정확한 값
498
+ // CORRECT: written after checking entity.json
499
+ role: "normal"; // exact value from entity.json
500
+ status: "pending"; // exact value from entity.json
501
501
 
502
- // BEST: TypeScript enum 사용
502
+ // BEST: use TypeScript enum
503
503
  import { UserRoleEnum } from "../sonamu.generated";
504
504
  role: UserRoleEnum.normal;
505
505
  ```
506
506
 
507
- **핵심 원칙: entity.json 단일 진실 공급원(Single Source of Truth)입니다.**
507
+ **Core principle: entity.json is the Single Source of Truth.**
508
508
 
509
- ## 테스트 작성 계획 수립
509
+ ## Test Writing Plan
510
510
 
511
- ### 엔티티 설계 프롬프트 기반 계획
511
+ ### Planning Based on Entity Design Prompt
512
512
 
513
- 엔티티 설계 완료 (migration + scaffolding 성공 확인), **엔티티 설계 시점에 명시한 업무 프로세스와 데이터 흐름**에 따라 테스트를 그룹핑한다.
513
+ After entity design is complete (confirming migration + scaffolding succeed), group tests according to **the business processes and data flows specified at the time of entity design**.
514
514
 
515
- **CRITICAL:** 단순 알파벳 순서나 개별 엔티티가 아니라, **업무 흐름 단위**로 테스트를 묶어서 작성한다.
515
+ **CRITICAL:** Group tests by **business flow units**, not by simple alphabetical order or individual entities.
516
516
 
517
- ### 1단계: 엔티티 설계 프롬프트 재확인
517
+ ### Step 1: Re-examine the Entity Design Prompt
518
518
 
519
- 설계 요청 작성한 프롬프트에서 다음 정보를 추출:
519
+ Extract the following from the prompt written at the time of the design request:
520
520
 
521
- - 업무 프로세스 흐름
522
- - 엔티티 관계 (relation)
523
- - 데이터 생성 순서
524
- - 주요 사용 시나리오
521
+ - Business process flow
522
+ - Relationships between entities (relations)
523
+ - Data creation order
524
+ - Key usage scenarios
525
525
 
526
- ### 2단계: 업무 프로세스별 그룹핑
526
+ ### Step 2: Group by Business Process
527
527
 
528
- 단순 우선순위가 아닌, **업무 흐름 단위**로 엔티티를 묶는다.
528
+ Group entities by **business flow units**, not simple priority.
529
529
 
530
- **고객 상담 시스템 예시:**
530
+ **Customer consultation system example:**
531
531
 
532
532
  ```
533
- 그룹 1: 기반 인프라
534
- Organization (유관기관)
535
- └─ User (사용자)
536
- └─ LoginHistory (로그인 이력)
533
+ Group 1: Core Infrastructure
534
+ Organization (related agency)
535
+ └─ User
536
+ └─ LoginHistory
537
537
 
538
- 업무 흐름: 기관 등록사용자 생성로그인
539
- 테스트 순서: Organization → User → LoginHistory
538
+ Business flow: register agencycreate userlogin
539
+ Test order: Organization → User → LoginHistory
540
540
 
541
- 그룹 2: 피해유형 관리
542
- DamageType (피해유형, self-referencing)
543
- └─ CounterMeasure (대응방안)
541
+ Group 2: Damage Type Management
542
+ DamageType (self-referencing)
543
+ └─ CounterMeasure
544
544
 
545
- 업무 흐름: 피해유형 계층 구성 유형별 대응방안 작성
546
- 테스트 순서: DamageType → CounterMeasure
545
+ Business flow: build damage type hierarchy write countermeasures for each type
546
+ Test order: DamageType → CounterMeasure
547
547
 
548
- 그룹 3: 상담 프로세스 (핵심 업무)
549
- User (신청인) + User (상담사) + DamageType
550
- └─ Consultation (상담)
551
- ├─ ConsultationChannelLog (채널 로그)
552
- └─ ConsultationHistory (상담 이력)
548
+ Group 3: Consultation Process (core business)
549
+ User (applicant) + User (counselor) + DamageType
550
+ └─ Consultation
551
+ ├─ ConsultationChannelLog
552
+ └─ ConsultationHistory
553
553
 
554
- 업무 흐름:
555
- 1. 신청인이 상담 접수
556
- 2. 상담사 배정
557
- 3. 피해유형 분류
558
- 4. 채널별 소통 (온라인/전화/SMS/카카오)
559
- 5. 상태 변경 이력 기록
554
+ Business flow:
555
+ 1. Applicant submits consultation request
556
+ 2. Assign counselor
557
+ 3. Classify damage type
558
+ 4. Communication by channel (online/phone/SMS/KakaoTalk)
559
+ 5. Record status change history
560
560
 
561
- 테스트 순서: Consultation → ConsultationChannelLog → ConsultationHistory
561
+ Test order: Consultation → ConsultationChannelLog → ConsultationHistory
562
562
 
563
- 그룹 4: 콘텐츠 관리 (독립적)
564
- FAQ (자주묻는질문)
565
- Banner (배너)
566
- Material (자료실)
567
- Notice (공지사항)
563
+ Group 4: Content Management (independent)
564
+ FAQ
565
+ Banner
566
+ Material
567
+ Notice
568
568
 
569
- 업무 흐름: 각각 독립적으로 CRUD
570
- 테스트 순서: 순서 무관 (병렬 작성 가능)
569
+ Business flow: independent CRUD for each
570
+ Test order: any order (can be written in parallel)
571
571
  ```
572
572
 
573
- ### 3단계: 그룹별 작업 순서
573
+ ### Step 3: Work Order per Group
574
574
 
575
- **각 그룹마다:**
575
+ **For each group:**
576
576
 
577
- 1. **types.ts 수정** - 그룹 모든 엔티티의 nullable 필드를 번에 처리
578
- 2. **test-helpers.ts 확장** - 그룹 엔티티들의 헬퍼 함수를 함께 작성
579
- 3. **테스트 파일 작성** - 그룹 내에서는 의존성 순서대로 작성
580
- 4. **Business Logic 테스트** - 실제 업무 시나리오 구현 (핵심!)
581
- 5. **테스트 통과 확인** - 다음 그룹으로 진행
577
+ 1. **Modify types.ts** - handle nullable fields for all entities in the group at once
578
+ 2. **Extend test-helpers.ts** - write helper functions for entities in the group together
579
+ 3. **Write test files** - write in dependency order within the group
580
+ 4. **Business Logic tests** - implement real business scenarios (the key!)
581
+ 5. **Verify tests pass** - proceed to next group
582
582
 
583
- **test-helpers.ts 예시 (의존성 체인 고려):**
583
+ **test-helpers.ts example (considering dependency chains):**
584
584
 
585
585
  ```typescript
586
- // 의존성 체인을 고려한 헬퍼 작성
586
+ // Write helpers considering dependency chains
587
587
  export async function createTestUserWithDeps() {
588
588
  const organizationId = await createTestOrganization();
589
589
  const userId = await createTestUser(organizationId);
@@ -607,99 +607,99 @@ export async function createTestConsultationWithDeps() {
607
607
  }
608
608
  ```
609
609
 
610
- ### 4단계: Business Logic 테스트 (핵심!)
610
+ ### Step 4: Business Logic Tests (the key!)
611
611
 
612
- **IMPORTANT:** E. Business Logic 섹션이 가장 중요하다.
612
+ **IMPORTANT:** The E. Business Logic section is the most important.
613
613
 
614
- 섹션에서:
614
+ In this section:
615
615
 
616
- - 엔티티 설계 프롬프트에 명시된 **실제 업무 시나리오** 구현
617
- - 엔티티 **상호작용** 테스트
618
- - **데이터 흐름** 검증
616
+ - Implement **real business scenarios** specified in the entity design prompt
617
+ - Test **interactions** between entities
618
+ - Validate **data flows**
619
619
 
620
- 이것이 단순 CRUD 테스트와의 차별점이며, **설계 의도를 검증하는 핵심**이다.
620
+ This is what differentiates it from simple CRUD tests, and it's **the core that validates design intent**.
621
621
 
622
- **Business Logic 테스트 예시 (상담 프로세스):**
622
+ **Business Logic test example (consultation process):**
623
623
 
624
624
  ```typescript
625
625
  describe("E. Business Logic", () => {
626
- test("상담 접수부터 완료까지 전체 프로세스", async () => {
627
- // 1. 상담 접수 + 의존성 생성
626
+ test("full process from consultation submission to completion", async () => {
627
+ // 1. submit consultation + create dependencies
628
628
  const { consultationId, counselorId } =
629
629
  await createTestConsultationWithDeps();
630
- // 2. 채널 로그 기록 (온라인 접수, 전화 상담)
630
+ // 2. record channel logs (online submission, phone consultation)
631
631
  await createTestConsultationChannelLog(consultationId, {
632
632
  channel: "online",
633
633
  });
634
634
  await createTestConsultationChannelLog(consultationId, {
635
635
  channel: "phone",
636
636
  });
637
- // 3. 상태 이력 기록
637
+ // 3. record status history
638
638
  await createTestConsultationHistory(consultationId, counselorId, {
639
639
  status: "consulting",
640
640
  });
641
- // 4. 상담 완료
641
+ // 4. complete consultation
642
642
  await ConsultationModel.save([{ id: consultationId, status: "completed" }]);
643
- // 5. 검증: 상태, 채널 로그 2건, 이력 확인
643
+ // 5. verify: status, 2 channel logs, history
644
644
  const c = await ConsultationModel.findById("A", consultationId);
645
645
  expect(c.status).toBe("completed");
646
646
  });
647
647
  });
648
648
  ```
649
649
 
650
- ### 주의사항
650
+ ### Notes
651
651
 
652
652
  **DO:**
653
653
 
654
- - 엔티티 설계 프롬프트를 항상 참고
655
- - 업무 프로세스 흐름대로 그룹핑
656
- - 의존성 순서를 고려한 테스트 순서
657
- - 실제 사용 시나리오 기반 Business Logic 테스트
658
- - test-helpers에 의존성 체인 명확히 구현
654
+ - Always reference the entity design prompt
655
+ - Group by business process flow
656
+ - Test order that considers dependency order
657
+ - Business Logic tests based on real usage scenarios
658
+ - Clearly implement dependency chains in test-helpers
659
659
 
660
660
  **DON'T:**
661
661
 
662
- - 단순히 알파벳 순서로 테스트 작성
663
- - 엔티티를 개별적으로만 테스트 (통합 관점 누락)
664
- - 업무 흐름과 무관한 우선순위 설정
665
- - 엔티티 설계 의도를 무시한 테스트
662
+ - Write tests in simple alphabetical order
663
+ - Only test entities individually (missing integration perspective)
664
+ - Set priorities unrelated to business flow
665
+ - Write tests that ignore the intent of the entity design
666
666
 
667
- ### 그룹별 체크리스트
667
+ ### Checklist per Group
668
668
 
669
- 프로세스 그룹별로 테스트 작성 완료 시:
669
+ When test writing for a process group is complete:
670
670
 
671
- - [ ] 그룹 모든 엔티티의 types.ts nullable 필드 처리 완료
672
- - [ ] 그룹 의존성 체인을 반영한 test-helpers 작성
673
- - [ ] 그룹 엔티티의 모듈 테스트 파일 작성
674
- - [ ] **핵심 업무 시나리오가 Business Logic 테스트에 포함됨**
675
- - [ ] 모든 테스트 통과 확인 (`pnpm sonamu test`)
676
- - [ ] 다음 그룹으로 진행
671
+ - [ ] Nullable field handling in types.ts completed for all entities in the group
672
+ - [ ] test-helpers written reflecting dependency chains within the group
673
+ - [ ] Module test file written for each entity in the group
674
+ - [ ] **Key business scenarios included in Business Logic tests**
675
+ - [ ] All tests pass confirmed (`pnpm sonamu test`)
676
+ - [ ] Proceed to next group
677
677
 
678
- ## 엔티티 생성 즉시 해야 작업
678
+ ## Tasks to Do Immediately After Entity Creation
679
679
 
680
- ### types.ts nullable 필드 처리 (필수)
680
+ ### Handling nullable Fields in types.ts (Required)
681
681
 
682
- 엔티티를 생성하고 `sonamu generate`로 types.ts 생성되면, **테스트 작성 전** 즉시 nullable 필드를 처리하세요.
682
+ After creating an entity and generating types.ts with `sonamu generate`, immediately handle nullable fields **before writing tests**.
683
683
 
684
- #### 작업 순서
684
+ #### Work Order
685
685
 
686
- 1. `sonamu generate` 실행
687
- 2. 생성된 `*.types.ts` 파일 확인
688
- 3. nullable 필드를 partial + extend + nullish 처리
689
- 4. 테스트 작성 시작
686
+ 1. Run `sonamu generate`
687
+ 2. Check the generated `*.types.ts` file
688
+ 3. Apply partial + extend + nullish handling for nullable fields
689
+ 4. Start writing tests
690
690
 
691
- #### 처리 대상 필드
691
+ #### Fields to Process
692
692
 
693
- - `nullable: true`인 모든 필드
694
- - `dbDefault`가 있는 필드 (`.optional().default(value)`)
695
- - FK 관계 필드 nullable인 것
693
+ - All fields with `nullable: true`
694
+ - Fields with `dbDefault` (`.optional().default(value)`)
695
+ - FK relation fields that are nullable
696
696
 
697
- #### 실전 예시
697
+ #### Practical Example
698
698
 
699
- **STEP 1: sonamu generate 실행 생성된 파일**
699
+ **STEP 1: File generated after running sonamu generate**
700
700
 
701
701
  ```typescript
702
- // faq.types.ts (자동 생성)
702
+ // faq.types.ts (auto-generated)
703
703
  import type { z } from "zod"; // WRONG: type import
704
704
  import { FAQBaseListParams, FAQBaseSchema } from "../sonamu.generated";
705
705
 
@@ -714,11 +714,11 @@ export const FAQSaveParams = FAQBaseSchema.partial({
714
714
  export type FAQSaveParams = z.infer<typeof FAQSaveParams>;
715
715
  ```
716
716
 
717
- **STEP 2: 즉시 수정 (nullable 필드 + Zod import 처리)**
717
+ **STEP 2: Immediate fix (nullable fields + Zod import handling)**
718
718
 
719
719
  ```typescript
720
- // faq.types.ts (수정 완료)
721
- import { z } from "zod"; // CORRECT: 일반 import로 수정
720
+ // faq.types.ts (fix complete)
721
+ import { z } from "zod"; // CORRECT: change to regular import
722
722
  import { FAQBaseListParams, FAQBaseSchema } from "../sonamu.generated";
723
723
 
724
724
  export const FAQListParams = FAQBaseListParams;
@@ -728,11 +728,11 @@ export const FAQSaveParams = FAQBaseSchema.partial({
728
728
  id: true,
729
729
  created_at: true,
730
730
  updated_at: true,
731
- // nullable 필드 추가
731
+ // add nullable fields
732
732
  category: true,
733
733
  order_num: true,
734
734
  }).extend({
735
- // nullable 필드를 nullish로 재정의
735
+ // redefine nullable fields as nullish
736
736
  category: z.string().nullish(), // string | null | undefined
737
737
  order_num: z.number().nullish(), // number | null | undefined
738
738
  updated_at: z.date().nullish(), // date | null | undefined
@@ -741,335 +741,335 @@ export const FAQSaveParams = FAQBaseSchema.partial({
741
741
  export type FAQSaveParams = z.infer<typeof FAQSaveParams>;
742
742
  ```
743
743
 
744
- #### 이렇게 해야 하나?
744
+ #### Why Is This Necessary?
745
745
 
746
- **문제:** Zod `nullable()`은 `T | null`이지만 여전히 required입니다.
746
+ **Problem:** Zod's `nullable()` gives `T | null` but it's still required.
747
747
 
748
748
  ```typescript
749
749
  // entity.json
750
750
  { "name": "category", "type": "string", "nullable": true }
751
751
 
752
- // 생성된 BaseSchema
752
+ // Generated BaseSchema
753
753
  z.object({
754
754
  category: z.string().nullable(), // string | null (required!)
755
755
  })
756
756
 
757
- // partial 적용
757
+ // applying partial only
758
758
  .partial({ category: true }) // category?: string | null
759
759
 
760
- // WRONG: undefined string | null에 할당 불가
760
+ // WRONG: undefined cannot be assigned to string | null
761
761
  const [id] = await FAQModel.save([{
762
- question: "질문",
763
- answer: "답변",
764
- // category 생략 타입 에러!
762
+ question: "Question",
763
+ answer: "Answer",
764
+ // omitting category causes type error!
765
765
  }]);
766
766
  ```
767
767
 
768
- **해결:** `partial()` + `extend()` + `nullish()` 조합
768
+ **Solution:** Combination of `partial()` + `extend()` + `nullish()`
769
769
 
770
770
  ```typescript
771
- // CORRECT: 올바른 처리
771
+ // CORRECT: proper handling
772
772
  FAQBaseSchema.partial({ category: true }).extend({
773
773
  category: z.string().nullish(),
774
774
  }); // string | null | undefined
775
775
 
776
- // 테스트에서 자유롭게 생략 가능
776
+ // Can freely omit in tests
777
777
  const [id] = await FAQModel.save([
778
778
  {
779
- question: "질문",
780
- answer: "답변",
781
- // category 생략 가능!
779
+ question: "Question",
780
+ answer: "Answer",
781
+ // category can be omitted!
782
782
  },
783
783
  ]);
784
784
  ```
785
785
 
786
- #### 적용 기준
786
+ #### Application Criteria
787
787
 
788
- | 필드 타입 | 처리 방법 |
788
+ | Field type | Handling |
789
789
  | -------------------------------- | ------------------------------- |
790
- | `id`, `created_at`, `updated_at` | 항상 partial (자동 생성) |
791
- | `dbDefault`가 있는 필드 | `.optional().default(value)` |
792
- | `nullable: true`인 필드 | partial + extend + `.nullish()` |
793
- | 필수 필드 | partial 제외 |
790
+ | `id`, `created_at`, `updated_at` | Always partial (auto-generated) |
791
+ | Fields with `dbDefault` | `.optional().default(value)` |
792
+ | Fields with `nullable: true` | partial + extend + `.nullish()` |
793
+ | Required fields | Excluded from partial |
794
794
 
795
- #### 체크리스트
795
+ #### Checklist
796
796
 
797
- - [ ] `import type { z }`를 `import { z }`로 수정
798
- - [ ] nullable 필드를 partial에 추가
799
- - [ ] extend로 nullish 재정의
800
- - [ ] dbDefault 필드는 `.optional().default()` 사용
801
- - [ ] 필수 필드는 partial 제외 확인
797
+ - [ ] Change `import type { z }` to `import { z }`
798
+ - [ ] Add nullable fields to partial
799
+ - [ ] Redefine as nullish via extend
800
+ - [ ] Use `.optional().default()` for dbDefault fields
801
+ - [ ] Confirm required fields are excluded from partial
802
802
 
803
- **상세 타입 안전성 가이드:** 아래 "TypeScript 타입 안전성" "타입 안전성 주의사항" 섹션 참조
803
+ **Detailed type safety guide:** See "TypeScript Type Safety" and "Type Safety Notes" sections below
804
804
 
805
- ## TypeScript 타입 안전성
805
+ ## TypeScript Type Safety
806
806
 
807
- ### 배열 인덱싱 옵셔널 체이닝 필수
807
+ ### Optional Chaining Required When Indexing Arrays
808
808
 
809
- 배열에서 인덱스로 요소에 접근한 프로퍼티에 접근할 때는 반드시 옵셔널 체이닝(`?.`)을 사용해야 합니다.
809
+ When accessing a property after indexing into an array, you must use optional chaining (`?.`).
810
810
 
811
- **이유:**
811
+ **Reason:**
812
812
 
813
- - 배열 인덱싱(`array[0]`, `array[1]` ) 항상 `undefined`를 반환할 수 있음
814
- - TypeScript `array[0]`의 타입을 `T | undefined`로 추론
815
- - 옵셔널 체이닝 없이 프로퍼티 접근 컴파일 에러 발생
813
+ - Array indexing (`array[0]`, `array[1]`, etc.) can always return `undefined`
814
+ - TypeScript infers the type of `array[0]` as `T | undefined`
815
+ - Accessing a property without optional chaining causes a compile error
816
816
 
817
- **잘못된 예:**
817
+ **Wrong:**
818
818
 
819
819
  ```typescript
820
- // 타입 에러: Object is possibly 'undefined'
821
- expect(list.rows[0].title).toBe("테스트");
822
- expect(searchResults.rows[0].name).toContain("검색어");
820
+ // Type error: Object is possibly 'undefined'
821
+ expect(list.rows[0].title).toBe("test");
822
+ expect(searchResults.rows[0].name).toContain("keyword");
823
823
  ```
824
824
 
825
- **올바른 예:**
825
+ **Correct:**
826
826
 
827
827
  ```typescript
828
- // 옵셔널 체이닝 사용
829
- expect(list.rows[0]?.title).toBe("테스트");
830
- expect(searchResults.rows[0]?.name).toContain("검색어");
828
+ // Use optional chaining
829
+ expect(list.rows[0]?.title).toBe("test");
830
+ expect(searchResults.rows[0]?.name).toContain("keyword");
831
831
 
832
- // 또는 먼저 존재 확인 접근
832
+ // Or verify existence first, then access
833
833
  expect(list.rows.length).toBeGreaterThanOrEqual(1);
834
- expect(list.rows[0].title).toBe("테스트"); // 이제 안전
834
+ expect(list.rows[0].title).toBe("test"); // now safe
835
835
  ```
836
836
 
837
- ### 권장 패턴
837
+ ### Recommended Patterns
838
838
 
839
- 테스트 코드에서 배열 요소 접근 시:
839
+ When accessing array elements in test code:
840
840
 
841
- **패턴 1: 옵셔널 체이닝 사용**
841
+ **Pattern 1: Use optional chaining**
842
842
 
843
843
  ```typescript
844
844
  const result = await Model.findMany("A", { num: 10, page: 1 });
845
845
  expect(result.rows[0]?.field).toBe(expectedValue);
846
846
  ```
847
847
 
848
- **패턴 2: 길이 검증 접근**
848
+ **Pattern 2: Verify length, then access**
849
849
 
850
850
  ```typescript
851
851
  const result = await Model.findMany("A", { num: 10, page: 1 });
852
852
  expect(result.rows.length).toBeGreaterThanOrEqual(1);
853
- expect(result.rows[0].field).toBe(expectedValue); // 타입 안전
853
+ expect(result.rows[0].field).toBe(expectedValue); // type-safe
854
854
  ```
855
855
 
856
- **패턴 3: find() 사용 옵셔널 체이닝 필수**
856
+ **Pattern 3: Optional chaining required when using find()**
857
857
 
858
858
  ```typescript
859
859
  const list = await Model.findMany("A", { num: 10, page: 1 });
860
860
  const item = list.rows.find((r) => r.id === targetId);
861
- expect(item?.field).toBe(expectedValue); // find() undefined 반환 가능
861
+ expect(item?.field).toBe(expectedValue); // find() can return undefined
862
862
  ```
863
863
 
864
- ### 일반 규칙
864
+ ### General Rules
865
865
 
866
- - 배열 인덱싱 프로퍼티 접근: `array[0]?.property`
867
- - `find()`, `filter()[0]` 결과: 항상 `?.` 사용
868
- - 객체 중첩 접근: `obj.nested?.deep?.property`
869
- - Non-null assertion(`!`) 확신이 있을 때만 사용
866
+ - Property access after array indexing: `array[0]?.property`
867
+ - Results of `find()`, `filter()[0]`, etc.: always use `?.`
868
+ - Nested object access: `obj.nested?.deep?.property`
869
+ - Non-null assertion (`!`) only when certain
870
870
 
871
- ## Model 기본 메서드 (테스트 대상)
871
+ ## Model Basic Methods (Test Targets)
872
872
 
873
- Sonamu Model 다음 메서드를 기본 제공한다. 테스트는 메서드들을 대상으로 작성한다:
873
+ Sonamu Model provides the following methods by default. Tests are written targeting these methods:
874
874
 
875
- | 메서드 | 용도 | 반환 |
875
+ | Method | Purpose | Returns |
876
876
  | -------------------------- | ------------------ | ----------------------------- |
877
- | `findById(subset, id)` | 단건 조회 | `Promise<Subset>` |
878
- | `findMany(subset, params)` | 목록 조회 | `Promise<ListResult<Subset>>` |
879
- | `save(rows)` | 생성/수정 (upsert) | `Promise<number[]>` (ids) |
880
- | `del(ids)` | 삭제 | `Promise<number>` (삭제 건수) |
877
+ | `findById(subset, id)` | Fetch single record | `Promise<Subset>` |
878
+ | `findMany(subset, params)` | Fetch list | `Promise<ListResult<Subset>>` |
879
+ | `save(rows)` | Create/update (upsert) | `Promise<number[]>` (ids) |
880
+ | `del(ids)` | Delete | `Promise<number>` (delete count) |
881
881
 
882
- **주의:** `delete`가 아니라 `del`이다. JavaScript 예약어 회피를 위함.
882
+ **Note:** It's `del`, not `delete`. This avoids JavaScript reserved words.
883
883
 
884
- ## 대규모 프로젝트 전략 (10 이상 엔티티)
884
+ ## Large-Scale Project Strategy (10 or more entities)
885
885
 
886
- **CRITICAL: 엔티티가 10개 이상인 프로젝트는 번에 작업하지 마세요.**
886
+ **CRITICAL: Do not work on all entities at once if a project has 10 or more entities.**
887
887
 
888
- ### 문제점
888
+ ### Problems
889
889
 
890
- - 55 엔티티를 한번에 작업하면 컨텍스트 혼란 발생
891
- - 잘못된 파일 수정, 필수 내용 삭제 심각한 실수 위험
892
- - 관계 추적 불가능, 테스트 작성 방향 상실
890
+ - Working on 55 entities at once causes context confusion
891
+ - Serious risk of errors such as modifying the wrong file or deleting required content
892
+ - Cannot track relationships, lose direction while writing tests
893
893
 
894
- ### 해결책: 배치 단위 작업
894
+ ### Solution: Batch Work Units
895
895
 
896
- **규칙: 연관된 엔티티끼리 묶어 5-10개씩 배치로 진행**
896
+ **Rule: Group related entities together and work in batches of 510**
897
897
 
898
898
  ```
899
- 1 배치: User, Institution, Role 관련 (5)
900
- 테스트 완료커밋
899
+ Batch 1: User, Institution, Role related (5 entities)
900
+ Tests completeCommit
901
901
 
902
- 2 배치: Survey, Question, Response 관련 (7)
903
- 테스트 완료커밋
902
+ Batch 2: Survey, Question, Response related (7 entities)
903
+ Tests completeCommit
904
904
 
905
- 3 배치: Report, Statistics 관련 (6)
906
- 테스트 완료커밋
905
+ Batch 3: Report, Statistics related (6 entities)
906
+ Tests completeCommit
907
907
  ```
908
908
 
909
- ### 배치 그룹화 기준
909
+ ### Batch Grouping Criteria
910
910
 
911
- **도메인별 그룹화 (권장):**
911
+ **Grouping by domain (recommended):**
912
912
 
913
913
  ```
914
- 인증/권한: User, Role, Permission, Session
915
- 설문: Survey, Question, Choice, Response
916
- 보고서: Report, Chart, Export
917
- 관리: Institution, Department, Settings
914
+ Auth/Permissions: User, Role, Permission, Session
915
+ Surveys: Survey, Question, Choice, Response
916
+ Reports: Report, Chart, Export
917
+ Administration: Institution, Department, Settings
918
918
  ```
919
919
 
920
- **의존성별 그룹화:**
920
+ **Grouping by dependencies:**
921
921
 
922
922
  ```
923
- 1차: 독립 엔티티 (User, Institution )
924
- 2차: 1차에 의존하는 엔티티 (Survey → Institution)
925
- 3차: 2차에 의존하는 엔티티 (Question → Survey)
923
+ 1st: Independent entities (User, Institution, etc.)
924
+ 2nd: Entities depending on 1st (Survey → Institution)
925
+ 3rd: Entities depending on 2nd (Question → Survey)
926
926
  ```
927
927
 
928
- ### 배치 작업 프로세스
928
+ ### Batch Work Process
929
929
 
930
- **각 배치마다:**
930
+ **For each batch:**
931
931
 
932
- 1. 해당 배치 엔티티 목록 명시
933
- 2. 테스트 헬퍼 작성 (createTest...)
934
- 3. 모든 엔티티 테스트 작성 완료
935
- 4. 전체 테스트 실행 확인
936
- 5. **Git commit 다음 배치 진행**
932
+ 1. List entities in the batch explicitly
933
+ 2. Write test helpers (createTest...)
934
+ 3. Complete tests for all entities
935
+ 4. Confirm all tests pass
936
+ 5. **Git commit, then proceed to next batch**
937
937
 
938
- **배치 사이 확인사항:**
938
+ **Between-batch checklist:**
939
939
 
940
- - [ ] 현재 배치 테스트 모두 통과
941
- - [ ] 기존 배치 테스트 여전히 통과 (회귀 방지)
942
- - [ ] 커밋 완료 (롤백 지점 확보)
940
+ - [ ] All tests in current batch pass
941
+ - [ ] Previous batch tests still pass (prevent regression)
942
+ - [ ] Commit complete (establish rollback point)
943
943
 
944
- ### 작업 시작 선언
944
+ ### Declare Before Starting Work
945
945
 
946
- **IMPORTANT: 배치 시작 명시적으로 선언하세요**
946
+ **IMPORTANT: Declare explicitly before starting each batch**
947
947
 
948
948
  ```
949
- "1차 배치 시작: User, Institution, Role 엔티티 (5)
950
- - User: user.model.test.ts 작성
951
- - Institution: institution.model.test.ts 작성
952
- - Role: role.model.test.ts 작성
953
- 수정할 파일만 작업, 다른 파일 건드리지 않음
954
- 진행할까요?"
949
+ "Starting batch 1: User, Institution, Role entities (5)
950
+ - User: write user.model.test.ts
951
+ - Institution: write institution.model.test.ts
952
+ - Role: write role.model.test.ts
953
+ Only work on files to be modified, do not touch other files
954
+ Shall we proceed?"
955
955
  ```
956
956
 
957
- ### 위험 신호 감지
957
+ ### Warning Signs
958
958
 
959
- 다음 상황이 발생하면 **즉시 작업 중단**:
959
+ **Stop work immediately** if any of the following occur:
960
960
 
961
- - 배치 범위 밖의 엔티티를 수정하려고
962
- - 같은 질문을 반복함
963
- - 엔티티 관계를 혼동함
964
- - 이미 완료한 파일을 다시 수정하려고
961
+ - Attempting to modify entities outside the batch scope
962
+ - Asking the same question repeatedly
963
+ - Confusing entity relationships
964
+ - Trying to re-modify files already completed
965
965
 
966
- ## 테스트 실행
966
+ ## Running Tests
967
967
 
968
- **원칙: 개발 중에는 `pnpm sonamu test`를 사용한다.** dev 서버는 항상 실행 중이라고 가정한다. 만약 dev 서버가 내려가 있다면 `pnpm sonamu dev`로 먼저 띄운 테스트한다. `pnpm test`는 CI 환경에서만 사용한다.
968
+ **Principle: Use `pnpm sonamu test` during development.** Assume the dev server is always running. If the dev server is down, start it first with `pnpm sonamu dev`, then run tests. Use `pnpm test` only in CI environments.
969
969
 
970
970
  ```bash
971
- # dev 서버 확인 (내려가 있으면 먼저 실행)
971
+ # Check dev server (start if it's down)
972
972
  pnpm sonamu dev
973
973
 
974
- # 개발 테스트 (기본)
974
+ # Tests during development (default)
975
975
  pnpm sonamu test
976
976
  pnpm sonamu test user.model
977
977
  pnpm sonamu test user.model -p "findMany"
978
978
 
979
- # CI 환경에서만
979
+ # CI environments only
980
980
  pnpm test
981
981
  ```
982
982
 
983
- ### DevRunner — `sonamu test` (기본 테스트 실행 방식)
983
+ ### DevRunner — `sonamu test` (Default Test Execution Method)
984
984
 
985
- `sonamu test`는 `sonamu dev` 프로세스 내부에 상주하는 Vitest Node API 인스턴스를 통해 테스트를 실행한다. 매번 Vitest 새로 기동하는 대신 이미 초기화된 인스턴스를 재사용하므로 실행 속도가 3.2x 빠르고, HMR 연동되어 소스 변경 즉시 최신 코드로 테스트된다.
985
+ `sonamu test` runs tests through a Vitest Node API instance that resides inside the `sonamu dev` process. Instead of starting Vitest fresh each time, it reuses an already-initialized instance, making execution 3.2x faster, and it integrates with HMR so tests always run against the latest code immediately after source changes.
986
986
 
987
- #### 사전 준비
987
+ #### Prerequisites
988
988
 
989
- **1. sonamu.config.ts에서 devRunner 활성화:**
989
+ **1. Enable devRunner in sonamu.config.ts:**
990
990
 
991
991
  ```typescript
992
992
  export default defineConfig({
993
993
  test: {
994
994
  devRunner: {
995
995
  enabled: true,
996
- // routePrefix: "/__test__", // optional, 기본값
997
- // vitestConfigPath: undefined, // optional, 기본값: vitest.config.ts (api-root 상대경로)
996
+ // routePrefix: "/__test__", // optional, default value
997
+ // vitestConfigPath: undefined, // optional, default: vitest.config.ts (relative to api-root)
998
998
  },
999
999
  },
1000
1000
  });
1001
1001
  ```
1002
1002
 
1003
- 설정 타입 (`SonamuDevRunnerConfig`):
1003
+ Configuration type (`SonamuDevRunnerConfig`):
1004
1004
 
1005
- - `enabled: boolean` — DevRunner 활성화 여부 (기본: false)
1006
- - `routePrefix?: string` — 테스트 엔드포인트 경로 접두사 (기본: `/__test__`)
1007
- - `vitestConfigPath?: string` — vitest.config.ts 경로 (api-root 상대경로)
1005
+ - `enabled: boolean` — Whether to enable DevRunner (default: false)
1006
+ - `routePrefix?: string` — Test endpoint path prefix (default: `/__test__`)
1007
+ - `vitestConfigPath?: string` — vitest.config.ts path (relative to api-root)
1008
1008
 
1009
- **2. dev 서버 실행:**
1009
+ **2. Start the dev server:**
1010
1010
 
1011
1011
  ```bash
1012
- sonamu dev # 또는 pnpm dev
1012
+ sonamu dev # or pnpm dev
1013
1013
  ```
1014
1014
 
1015
- dev 서버 기동 `isLocal() && devRunner.enabled` 조건에서 `DevVitestManager`가 자동 초기화되고, Fastify에 테스트 엔드포인트가 등록된다.
1015
+ When the dev server starts, `DevVitestManager` is automatically initialized under the `isLocal() && devRunner.enabled` condition, and test endpoints are registered with Fastify.
1016
1016
 
1017
- #### CLI 사용법
1017
+ #### CLI Usage
1018
1018
 
1019
1019
  ```bash
1020
- # 전체 테스트
1020
+ # Run all tests
1021
1021
  sonamu test
1022
1022
 
1023
- # 파일 지정 (파일명 일부로 매칭 — globTestSpecifications 사용)
1023
+ # Specify file (matched by partial filename uses globTestSpecifications)
1024
1024
  sonamu test user.model
1025
1025
 
1026
- # 여러 파일
1026
+ # Multiple files
1027
1027
  sonamu test user.model order.model
1028
1028
 
1029
- # 특정 테스트 케이스만 (테스트명 패턴)
1029
+ # Run specific test cases only (test name pattern)
1030
1030
  sonamu test user.model --pattern "findMany"
1031
1031
  sonamu test user.model -p "findMany"
1032
1032
 
1033
- # Naite trace 출력
1033
+ # Print Naite traces
1034
1034
  sonamu test user.model --traces
1035
1035
  sonamu test user.model -t
1036
1036
 
1037
- # 파일 + 패턴 + trace 조합
1037
+ # Combine file + pattern + trace
1038
1038
  sonamu test user.model -p "findMany" -t
1039
1039
  ```
1040
1040
 
1041
- 인자 처리 규칙:
1041
+ Argument processing rules:
1042
1042
 
1043
- - `--pattern` / `-p`: 테스트명 문자열 필터 (`setGlobalTestNamePattern` → 실행 후 `resetGlobalTestNamePattern`)
1044
- - `--traces` / `-t`: boolean 플래그, Naite trace 출력 활성화
1045
- - `-`로 시작하지 않는 인자: 파일 목록으로 처리
1046
- - 다중 파일 전달 허용
1047
- - 서버 응답의 `ok: false`는 exit code 1로 반영
1043
+ - `--pattern` / `-p`: test name string filter (`setGlobalTestNamePattern` → `resetGlobalTestNamePattern` after execution)
1044
+ - `--traces` / `-t`: boolean flag, enables Naite trace output
1045
+ - Arguments not starting with `-`: treated as file list
1046
+ - Multiple files allowed
1047
+ - `ok: false` in server response is reflected as exit code 1
1048
1048
 
1049
- **→ Naite trace, HMR 연동, HTTP API, 내부 아키텍처, 성능 비교, 트러블슈팅 상세: `testing-devrunner.md`**
1049
+ **→ Naite traces, HMR integration, HTTP API, internal architecture, performance comparison, troubleshooting details: `testing-devrunner.md`**
1050
1050
 
1051
- ## sonamu.config.ts 테스트 설정 설정 파일
1051
+ ## sonamu.config.ts Test Configuration and Config Files
1052
1052
 
1053
- **→ 설정 타입 정의, DevRunner/병렬 설정, 활성화 조건, 병렬 DB 흐름, vitest.config.ts/global.ts 상세: `testing-devrunner.md`**
1053
+ **→ Configuration type definitions, DevRunner/parallel settings, activation conditions, parallel DB flow, vitest.config.ts/global.ts details: `testing-devrunner.md`**
1054
1054
 
1055
- 핵심 설정만 요약:
1055
+ Key settings summary only:
1056
1056
 
1057
1057
  ```typescript
1058
1058
  // sonamu.config.ts
1059
1059
  export default defineConfig({
1060
1060
  test: {
1061
- devRunner: { enabled: true }, // pnpm sonamu test 사용 시 필수
1062
- // parallel: true, // 선택: Worker별 DB 분리
1063
- // maxWorkers: 4, // 선택: 병렬 Worker
1061
+ devRunner: { enabled: true }, // required to use pnpm sonamu test
1062
+ // parallel: true, // optional: separate DB per worker
1063
+ // maxWorkers: 4, // optional: number of parallel workers
1064
1064
  },
1065
1065
  });
1066
1066
  ```
1067
1067
 
1068
- ## 테스트 기본 패턴
1068
+ ## Test Basic Patterns
1069
1069
 
1070
1070
  ### bootstrap
1071
1071
 
1072
- 모든 테스트 파일에서 `bootstrap(vi)` 호출 필수:
1072
+ `bootstrap(vi)` call required in all test files:
1073
1073
 
1074
1074
  ```typescript
1075
1075
  import { bootstrap, test } from "sonamu/test";
@@ -1078,33 +1078,33 @@ import { describe, expect, vi } from "vitest";
1078
1078
  bootstrap(vi);
1079
1079
 
1080
1080
  describe("MyTest", () => {
1081
- test("테스트 케이스", async () => {
1081
+ test("test case", async () => {
1082
1082
  // ...
1083
1083
  });
1084
1084
  });
1085
1085
  ```
1086
1086
 
1087
- **bootstrap 옵션:**
1087
+ **bootstrap options:**
1088
1088
 
1089
1089
  ```typescript
1090
- // 기본값: forTesting: true (빠름, Syncer/Task 생략)
1090
+ // Default: forTesting: true (fast, skips Syncer/Task)
1091
1091
  bootstrap(vi);
1092
1092
 
1093
- // forTesting: false - 전체 초기화 (Syncer, Task, EntityManager 등 모두 로드)
1094
- // migrator, syncer, template 등의 테스트에서 사용
1093
+ // forTesting: false - full initialization (loads Syncer, Task, EntityManager, etc.)
1094
+ // Used in tests for migrator, syncer, template, etc.
1095
1095
  bootstrap(vi, { forTesting: false });
1096
1096
  ```
1097
1097
 
1098
1098
  ### test vs testAs
1099
1099
 
1100
1100
  ```typescript
1101
- // 비인증 테스트 - Context.user null
1102
- test("비인증 테스트", async () => {
1101
+ // Unauthenticated test - Context.user is null
1102
+ test("unauthenticated test", async () => {
1103
1103
  const me = await UserModel.me();
1104
1104
  expect(me).toBeNull();
1105
1105
  });
1106
1106
 
1107
- // 인증 테스트 - Context.user 설정됨
1107
+ // Authenticated test - Context.user is set
1108
1108
  import type { UserSubsetSS } from "../sonamu.generated";
1109
1109
 
1110
1110
  const adminUser: UserSubsetSS = {
@@ -1115,7 +1115,7 @@ const adminUser: UserSubsetSS = {
1115
1115
  role: "admin",
1116
1116
  };
1117
1117
 
1118
- testAs(adminUser, "관리자 권한 테스트", async () => {
1118
+ testAs(adminUser, "admin permission test", async () => {
1119
1119
  const me = await UserModel.me();
1120
1120
  expect(me?.role).toBe("admin");
1121
1121
  });
@@ -1127,7 +1127,7 @@ testAs(adminUser, "관리자 권한 테스트", async () => {
1127
1127
  test.each([
1128
1128
  { input: "user@example.com", expected: true },
1129
1129
  { input: "invalid-email", expected: false },
1130
- ])("이메일 검증: $input → $expected", async ({ input, expected }) => {
1130
+ ])("email validation: $input → $expected", async ({ input, expected }) => {
1131
1131
  expect(validateEmail(input)).toBe(expected);
1132
1132
  });
1133
1133
  ```
@@ -1148,12 +1148,12 @@ export const loadFixtures = createFixtureLoader({
1148
1148
  });
1149
1149
  ```
1150
1150
 
1151
- ### 테스트에서 사용
1151
+ ### Using in tests
1152
1152
 
1153
1153
  ```typescript
1154
1154
  import { loadFixtures } from "../../testing/fixture";
1155
1155
 
1156
- test("회사 정보 수정", async () => {
1156
+ test("update company info", async () => {
1157
1157
  const f0 = await loadFixtures(["company01"]);
1158
1158
 
1159
1159
  await CompanyModel.save([
@@ -1168,31 +1168,31 @@ test("회사 정보 수정", async () => {
1168
1168
  });
1169
1169
  ```
1170
1170
 
1171
- ## Naite (테스트 추적 시스템)
1171
+ ## Naite (Test Tracing System)
1172
1172
 
1173
- **→ 상세 가이드 ( 목록, 체이닝 필터, wildcard, del, 내부 구조): `naite.md`**
1173
+ **→ Detailed guide (key list, chaining filters, wildcard, del, internal structure): `naite.md`**
1174
1174
 
1175
- Naite 소스 코드에서 `Naite.t("key", value)`로 값을 기록하고, 테스트에서 `Naite.get("key")`로 검증하는 추적 시스템이다.
1175
+ Naite is a tracing system that records values with `Naite.t("key", value)` in source code and validates them with `Naite.get("key")` in tests.
1176
1176
 
1177
- ### 테스트에서 자주 쓰는 패턴
1177
+ ### Commonly Used Patterns in Tests
1178
1178
 
1179
1179
  ```typescript
1180
1180
  import { Naite } from "sonamu";
1181
1181
 
1182
- // 쿼리 검증
1182
+ // Query validation
1183
1183
  expect(Naite.get("esq-query").first()).not.contain("limit");
1184
1184
 
1185
- // UpsertBuilder 동작 검증
1185
+ // UpsertBuilder behavior validation
1186
1186
  const trace = Naite.get("puri:ub-upserted").first();
1187
1187
  expect(trace).toMatchObject({ tableName: "users", rowCount: 3 });
1188
1188
 
1189
- // 조회 메서드: .first(), .last(), .at(n), .result() (전체 배열)
1190
- // 필터: .fromFile("user.model.ts"), .fromFunction("findById"), .where("data.tableName", "=", "users")
1189
+ // Fetch methods: .first(), .last(), .at(n), .result() (full array)
1190
+ // Filters: .fromFile("user.model.ts"), .fromFunction("findById"), .where("data.tableName", "=", "users")
1191
1191
  ```
1192
1192
 
1193
- ## 테스트 헬퍼: expectQuery
1193
+ ## Test Helper: expectQuery
1194
1194
 
1195
- SQL 쿼리의 특정 부분만 검증하는 헬퍼 (miomock 참고):
1195
+ Helper for validating specific parts of SQL queries (see miomock for reference):
1196
1196
 
1197
1197
  ```typescript
1198
1198
  // api/src/testing/expect-query.ts
@@ -1219,12 +1219,12 @@ export function expectQuery(query: string, part?: QueryPart) {
1219
1219
  }
1220
1220
  ```
1221
1221
 
1222
- ### 사용 예시
1222
+ ### Usage Examples
1223
1223
 
1224
1224
  ```typescript
1225
1225
  import { expectQuery } from "../testing/expect-query";
1226
1226
 
1227
- test("select 쿼리 검증", async () => {
1227
+ test("validate select query", async () => {
1228
1228
  const db = UserModel.getPuri("r");
1229
1229
  await db.table("users").select({ id: "users.id" });
1230
1230
  const query = Naite.get("puri:executed-query").first();
@@ -1236,7 +1236,7 @@ test("select 쿼리 검증", async () => {
1236
1236
  );
1237
1237
  });
1238
1238
 
1239
- test("where 조건 검증", async () => {
1239
+ test("validate where condition", async () => {
1240
1240
  const db = UserModel.getPuri("r");
1241
1241
  await db.table("users").where("users.id", 1);
1242
1242
  const query = Naite.get("puri:executed-query").first();
@@ -1244,7 +1244,7 @@ test("where 조건 검증", async () => {
1244
1244
  expectQuery(query, "where").toMatchInlineSnapshot(`""users"."id" = 1"`);
1245
1245
  });
1246
1246
 
1247
- test("join 검증", async () => {
1247
+ test("validate join", async () => {
1248
1248
  const db = UserModel.getPuri("r");
1249
1249
  await db
1250
1250
  .table("employees")
@@ -1257,9 +1257,9 @@ test("join 검증", async () => {
1257
1257
  });
1258
1258
  ```
1259
1259
 
1260
- ## 테스트 헬퍼: expectUB
1260
+ ## Test Helper: expectUB
1261
1261
 
1262
- UpsertBuilder 상태 검증 헬퍼 (miomock 참고):
1262
+ UpsertBuilder state validation helper (see miomock for reference):
1263
1263
 
1264
1264
  ```typescript
1265
1265
  // api/src/testing/expect-ub.ts
@@ -1282,23 +1282,23 @@ export function expectUB<P extends UBPart>(
1282
1282
  tableName?: string,
1283
1283
  index?: number,
1284
1284
  ) {
1285
- // ... 구현
1285
+ // ... implementation
1286
1286
  }
1287
1287
  ```
1288
1288
 
1289
- ### 사용 예시
1289
+ ### Usage Examples
1290
1290
 
1291
1291
  ```typescript
1292
1292
  import { expectUB } from "../testing/expect-ub";
1293
1293
 
1294
- test("UpsertBuilder 상태 검증", async () => {
1294
+ test("validate UpsertBuilder state", async () => {
1295
1295
  const ub = new UpsertBuilder();
1296
1296
 
1297
- // 초기 상태
1297
+ // initial state
1298
1298
  expectUB(ub, "hasTable", "users").toBe(false);
1299
1299
  expectUB(ub, "tables").toEqual([]);
1300
1300
 
1301
- // register
1301
+ // after register
1302
1302
  ub.register("users", {
1303
1303
  email: "test@test.com",
1304
1304
  username: "test",
@@ -1313,13 +1313,13 @@ test("UpsertBuilder 상태 검증", async () => {
1313
1313
  username: "test",
1314
1314
  });
1315
1315
 
1316
- // upsert 초기화 확인
1316
+ // confirm reset after upsert
1317
1317
  await ub.upsert(wdb, "users");
1318
1318
  expectUB(ub, "rowCount", "users").toBe(0);
1319
1319
  });
1320
1320
  ```
1321
1321
 
1322
- ## Mock 패턴
1322
+ ## Mock Patterns
1323
1323
 
1324
1324
  ### setup-mocks.ts
1325
1325
 
@@ -1333,7 +1333,7 @@ vi.mock("fs/promises", async (importOriginal) => {
1333
1333
  return {
1334
1334
  ...actual,
1335
1335
  access: vi.fn((path, mode) => {
1336
- // 가상 파일 시스템 체크
1336
+ // virtual file system check
1337
1337
  const vfs = Naite.get("mock:fs/promises:virtualFileSystem").result();
1338
1338
  if (vfs.some((v) => v === path)) {
1339
1339
  return Promise.resolve();
@@ -1358,7 +1358,7 @@ vi.mock("fs/promises", async (importOriginal) => {
1358
1358
  import { Entity, EntityManager, type EntityJson } from "sonamu";
1359
1359
  import { vi } from "vitest";
1360
1360
 
1361
- // EntityManager.get 모킹
1361
+ // Mocking EntityManager.get
1362
1362
  export function mockEntityManagerGet(
1363
1363
  targetEntityId: string,
1364
1364
  overrideCallback: (original: EntityJson) => EntityJson,
@@ -1374,12 +1374,12 @@ export function mockEntityManagerGet(
1374
1374
  }
1375
1375
  ```
1376
1376
 
1377
- ## CRUD 테스트 패턴
1377
+ ## CRUD Test Patterns
1378
1378
 
1379
1379
  ### Create & Read
1380
1380
 
1381
1381
  ```typescript
1382
- test("Create - 유저 생성", async () => {
1382
+ test("Create - create new user", async () => {
1383
1383
  const [userId] = await UserModel.save([
1384
1384
  {
1385
1385
  email: "newuser@test.com",
@@ -1399,7 +1399,7 @@ test("Create - 새 유저 생성", async () => {
1399
1399
  ### Update
1400
1400
 
1401
1401
  ```typescript
1402
- test("Update - 유저 수정", async () => {
1402
+ test("Update - update user", async () => {
1403
1403
  const f0 = await loadFixtures(["user01"]);
1404
1404
 
1405
1405
  await UserModel.save([
@@ -1414,161 +1414,161 @@ test("Update - 유저 수정", async () => {
1414
1414
  });
1415
1415
  ```
1416
1416
 
1417
- ### 에러 테스트
1417
+ ### Error Tests
1418
1418
 
1419
1419
  ```typescript
1420
- test("존재하지 않는 유저 조회 시 에러", async () => {
1420
+ test("error when fetching non-existent user", async () => {
1421
1421
  await expect(UserModel.findById("A", 99999)).rejects.toThrow("not found");
1422
1422
  });
1423
1423
 
1424
- test("해결되지 않은 참조 에러", async () => {
1424
+ test("unresolved reference error", async () => {
1425
1425
  const ub = new UpsertBuilder();
1426
1426
  const companyRef = ub.register("companies", { name: "Test" });
1427
1427
  ub.register("departments", { company_id: companyRef, name: "Dept" });
1428
1428
 
1429
- // 잘못된 순서로 upsert 시도
1429
+ // attempt upsert in wrong order
1430
1430
  await expect(ub.upsert(wdb, "departments")).rejects.toThrow(
1431
- /해결되지 않은 참조/,
1431
+ /unresolved reference/,
1432
1432
  );
1433
1433
  });
1434
1434
  ```
1435
1435
 
1436
- ## 테스트 구조화 패턴
1436
+ ## Test Structuring Patterns
1437
1437
 
1438
1438
  ```typescript
1439
1439
  describe("UpsertBuilder", () => {
1440
- describe("A. 기본 등록 (register)", () => {
1441
- test("register() 호출 UBRef 반환", async () => {
1440
+ describe("A. Basic registration (register)", () => {
1441
+ test("register() returns UBRef", async () => {
1442
1442
  /* ... */
1443
1443
  });
1444
- test("여러 register() rows 누적", async () => {
1444
+ test("multiple register() calls accumulate rows", async () => {
1445
1445
  /* ... */
1446
1446
  });
1447
1447
  });
1448
1448
 
1449
- describe("B. 테이블 관리", () => {
1450
- test("getTable()/hasTable() 기본 동작", async () => {
1449
+ describe("B. Table management", () => {
1450
+ test("basic behavior of getTable()/hasTable()", async () => {
1451
1451
  /* ... */
1452
1452
  });
1453
1453
  });
1454
1454
 
1455
- describe("C. Upsert 실행", () => {
1456
- test("upsert() - row 삽입", async () => {
1455
+ describe("C. Upsert execution", () => {
1456
+ test("upsert() - insert new row", async () => {
1457
1457
  /* ... */
1458
1458
  });
1459
- test("upsert() - 기존 row 업데이트", async () => {
1459
+ test("upsert() - update existing row", async () => {
1460
1460
  /* ... */
1461
1461
  });
1462
- test("insertOnly() - 삽입만 수행", async () => {
1462
+ test("insertOnly() - insert only", async () => {
1463
1463
  /* ... */
1464
1464
  });
1465
1465
  });
1466
1466
 
1467
- describe("D. 에러 처리", () => {
1468
- test("존재하지 않는 테이블에 upsert 배열", async () => {
1467
+ describe("D. Error handling", () => {
1468
+ test("upsert on non-existent tableempty array", async () => {
1469
1469
  /* ... */
1470
1470
  });
1471
- test("해결되지 않은 참조 에러", async () => {
1471
+ test("unresolved referenceerror", async () => {
1472
1472
  /* ... */
1473
1473
  });
1474
1474
  });
1475
1475
  });
1476
1476
  ```
1477
1477
 
1478
- ## 파일 구조
1478
+ ## File Structure
1479
1479
 
1480
1480
  ```
1481
1481
  api/src/testing/
1482
- ├── fixture.ts # createFixtureLoader 정의
1482
+ ├── fixture.ts # createFixtureLoader definition
1483
1483
  ├── global.ts # globalSetup (dotenv, setup export)
1484
- ├── setup-mocks.ts # 전역 Mock 설정
1485
- ├── test-helpers.ts # 테스트 유틸 함수
1486
- ├── expect-query.ts # SQL 쿼리 검증 헬퍼
1487
- └── expect-ub.ts # UpsertBuilder 검증 헬퍼
1484
+ ├── setup-mocks.ts # global Mock configuration
1485
+ ├── test-helpers.ts # test utility functions
1486
+ ├── expect-query.ts # SQL query validation helper
1487
+ └── expect-ub.ts # UpsertBuilder validation helper
1488
1488
  ```
1489
1489
 
1490
1490
  ## Rules
1491
1491
 
1492
- - 모든 테스트 파일에서 `bootstrap(vi)` 호출 필수
1493
- - 테스트는 자동으로 롤백됨 (테스트 격리)
1494
- - 비인증 테스트는 `test`, 인증 테스트는 `testAs` 사용
1495
- - Fixture는 `createFixtureLoader`로 정의하고 `loadFixtures`로 로드
1496
- - Naite 쿼리/UpsertBuilder 동작 추적 검증
1497
- - `toMatchInlineSnapshot()` 활용하여 스냅샷 테스트 권장
1498
- - Mock은 `setup-mocks.ts`에서 전역 설정하거나 테스트 내에서 `vi.spyOn` 사용
1492
+ - `bootstrap(vi)` call required in all test files
1493
+ - Each test is automatically rolled back (test isolation)
1494
+ - Use `test` for unauthenticated tests, `testAs` for authenticated tests
1495
+ - Define fixtures with `createFixtureLoader` and load with `loadFixtures`
1496
+ - Use Naite to track and validate query/UpsertBuilder behavior
1497
+ - Recommend snapshot tests using `toMatchInlineSnapshot()`
1498
+ - Configure Mocks globally in `setup-mocks.ts` or use `vi.spyOn` within tests
1499
1499
 
1500
- ## 타입 안전성 주의사항
1500
+ ## Type Safety Notes
1501
1501
 
1502
- ### Zod import 방식
1502
+ ### Zod Import Method
1503
1503
 
1504
- **CRITICAL: 테스트 파일에서 Zod를 import할 때는 반드시 일반 import를 사용해야 합니다.**
1504
+ **CRITICAL: Always use regular imports when importing Zod in test files.**
1505
1505
 
1506
1506
  ```typescript
1507
- // CORRECT - 테스트 파일에서
1507
+ // CORRECT - in test files
1508
1508
  import { z } from "zod";
1509
1509
  import { describe, expect, vi } from "vitest";
1510
1510
 
1511
- // WRONG - type import 사용 런타임 에러 발생
1512
- import type { z } from "zod"; // 테스트 실행 에러!
1511
+ // WRONG - runtime error when using type import
1512
+ import type { z } from "zod"; // error when test runs!
1513
1513
  ```
1514
1514
 
1515
- **이유:** 테스트에서 `z.infer<>`나 Zod 스키마를 직접 사용하기 때문에 런타임에 Zod 객체가 필요합니다.
1515
+ **Reason:** Because `z.infer<>` and Zod schemas are used directly in tests, the Zod object is needed at runtime.
1516
1516
 
1517
- **적용 위치:**
1517
+ **Where this applies:**
1518
1518
 
1519
- - `*.model.test.ts` - 모든 테스트 파일
1520
- - `test-helpers.ts` - Zod 스키마를 사용하는 헬퍼 파일
1519
+ - `*.model.test.ts` - all test files
1520
+ - `test-helpers.ts` - helper files that use Zod schemas
1521
1521
 
1522
- ### SaveParams의 partial 설정 확인
1522
+ ### Checking partial Settings in SaveParams
1523
1523
 
1524
- `Model.save()` 테스트 `*.types.ts`의 `SaveParams` partial 설정을 확인해야 함:
1524
+ When testing `Model.save()`, you must check the `SaveParams` partial settings in `*.types.ts`:
1525
1525
 
1526
1526
  ```typescript
1527
1527
  // user.types.ts
1528
- import { z } from "zod"; // types 파일에서도 일반 import
1528
+ import { z } from "zod"; // regular import in types files too
1529
1529
  import { UserBaseSchema } from "../sonamu.generated";
1530
1530
 
1531
1531
  export const UserSaveParams = UserBaseSchema.partial({
1532
- id: true, // 자동 생성
1533
- created_at: true, // 자동 생성
1534
- updated_at: true, // 자동 생성
1532
+ id: true, // auto-generated
1533
+ created_at: true, // auto-generated
1534
+ updated_at: true, // auto-generated
1535
1535
  });
1536
1536
  export type UserSaveParams = z.infer<typeof UserSaveParams>;
1537
1537
  ```
1538
1538
 
1539
- ### Nullable 필드 처리 패턴
1539
+ ### Nullable Field Handling Pattern
1540
1540
 
1541
- **→ "엔티티 생성 즉시 해야 작업" 섹션 참조** (partial + extend + nullish 패턴)
1541
+ **→ See "Tasks to Do Immediately After Entity Creation" section above** (partial + extend + nullish pattern)
1542
1542
 
1543
- ### Nullish Coalescing 사용
1543
+ ### Use Nullish Coalescing
1544
1544
 
1545
- 변수가 `T | undefined` 타입일 있는 경우 nullish coalescing 필수:
1545
+ Nullish coalescing is required when a variable can be of type `T | undefined`:
1546
1546
 
1547
1547
  ```typescript
1548
- // WRONG: userId number | undefined일 수 있음
1548
+ // WRONG: userId may be number | undefined
1549
1549
  const user = await UserModel.findById("A", userId);
1550
1550
 
1551
- // CORRECT: nullish coalescing으로 undefined 방어
1551
+ // CORRECT: guard against undefined with nullish coalescing
1552
1552
  const user = await UserModel.findById("A", userId ?? 0);
1553
1553
  ```
1554
1554
 
1555
- 특히 이전 단계에서 생성한 ID를 사용할 주의:
1555
+ Especially be careful when using IDs created in a previous step:
1556
1556
 
1557
1557
  ```typescript
1558
1558
  const [userId] = await UserModel.save([{ ... }]);
1559
1559
 
1560
- // WRONG: userId number | undefined
1560
+ // WRONG: userId is number | undefined
1561
1561
  const user = await UserModel.findById("A", userId);
1562
1562
 
1563
1563
  // CORRECT:
1564
1564
  const user = await UserModel.findById("A", userId ?? 0);
1565
1565
  ```
1566
1566
 
1567
- ### SaveParams import 위치
1567
+ ### SaveParams Import Location
1568
1568
 
1569
- SaveParams 타입은 sonamu.generated가 아닌 엔티티의 types.ts에서 export됩니다.
1569
+ SaveParams types are exported from each entity's types.ts, not from sonamu.generated.
1570
1570
 
1571
- **잘못된 예:**
1571
+ **Wrong:**
1572
1572
 
1573
1573
  ```typescript
1574
1574
  // test-helpers.ts
@@ -1578,7 +1578,7 @@ import type {
1578
1578
  } from "../application/sonamu.generated"; // WRONG
1579
1579
  ```
1580
1580
 
1581
- **올바른 예:**
1581
+ **Correct:**
1582
1582
 
1583
1583
  ```typescript
1584
1584
  // test-helpers.ts
@@ -1586,48 +1586,48 @@ import type { UserSaveParams } from "../application/user/user.types";
1586
1586
  import type { TaskSaveParams } from "../application/task/task.types";
1587
1587
  ```
1588
1588
 
1589
- **이유:**
1589
+ **Reason:**
1590
1590
 
1591
- - sonamu.generated에는 BaseSchema BaseListParams만 export됨
1592
- - SaveParams 엔티티의 types.ts에서 BaseSchema.partial() 정의됨
1591
+ - sonamu.generated only exports BaseSchema and BaseListParams
1592
+ - SaveParams is defined with BaseSchema.partial() in each entity's types.ts
1593
1593
 
1594
- ## 실전 주의사항 (Common Pitfalls)
1594
+ ## Practical Notes (Common Pitfalls)
1595
1595
 
1596
- ### 1. Fixture 데이터 준비 필수
1596
+ ### 1. Fixture Data Preparation Required
1597
1597
 
1598
- **문제:** Foreign key constraint로 인해 기본 데이터 없으면 테스트 실패
1598
+ **Problem:** Tests fail without base data due to foreign key constraints
1599
1599
 
1600
- **해결:**
1600
+ **Solution:**
1601
1601
 
1602
1602
  ```sql
1603
1603
  -- database/scripts/seed-initial-data.sql
1604
- INSERT INTO institutions (id, name, code) VALUES (1, '본원', 'HQ');
1605
- INSERT INTO departments (id, name, institution_id) VALUES (1, '연구부', 1);
1606
- INSERT INTO roles (id, code, name) VALUES (1, 'ADMIN', '관리자');
1604
+ INSERT INTO institutions (id, name, code) VALUES (1, 'HQ', 'HQ');
1605
+ INSERT INTO departments (id, name, institution_id) VALUES (1, 'Research', 1);
1606
+ INSERT INTO roles (id, code, name) VALUES (1, 'ADMIN', 'Administrator');
1607
1607
  ```
1608
1608
 
1609
1609
  ```bash
1610
- # 1. seed 데이터를 test DB에 적용
1610
+ # 1. apply seed data to test DB
1611
1611
  PGPASSWORD=1234 psql -h 0.0.0.0 -U postgres -d project_test -f database/scripts/seed-initial-data.sql
1612
1612
 
1613
- # 2. dump 생성
1613
+ # 2. create dump
1614
1614
  pnpm dump
1615
1615
 
1616
- # 3. fixture DB에 적용
1616
+ # 3. apply to fixture DB
1617
1617
  pnpm seed
1618
1618
 
1619
- # 4. sonamu fixture sync (선택사항)
1619
+ # 4. sonamu fixture sync (optional)
1620
1620
  pnpm sonamu fixture sync
1621
1621
  ```
1622
1622
 
1623
- ### 2. SaveParams 타입 설계 (Partial)
1623
+ ### 2. SaveParams Type Design (Partial)
1624
1624
 
1625
- **문제 1:** Update 일부 필드만 변경하면 타입 에러 발생
1625
+ **Problem 1:** Type error occurs when changing only some fields on update
1626
1626
 
1627
- **문제 2:** 테스트 헬퍼에서 override를 Partial로 받을 타입 에러 발생
1627
+ **Problem 2:** Type error occurs when receiving overrides as Partial in test helpers
1628
1628
 
1629
1629
  ```typescript
1630
- // WRONG - nullable 필드가 partial 미설정
1630
+ // WRONG - nullable fields not set to partial
1631
1631
  export const QuestionSaveParams = QuestionBaseSchema.partial({
1632
1632
  id: true,
1633
1633
  created_at: true,
@@ -1640,22 +1640,22 @@ export async function createTestQuestion(
1640
1640
  ) {
1641
1641
  const [id] = await QuestionModel.save([
1642
1642
  {
1643
- content: "테스트질문",
1643
+ content: "test question",
1644
1644
  parent_id: null,
1645
1645
  answer_group_id: null,
1646
- ...override, // 타입 에러: undefined null로 할당 불가
1646
+ ...override, // type error: undefined cannot be assigned to null
1647
1647
  },
1648
1648
  ]);
1649
1649
  return id;
1650
1650
  }
1651
1651
  ```
1652
1652
 
1653
- **해결:** nullable/dbDefault 필드를 partial로 설정
1653
+ **Solution:** Set nullable/dbDefault fields to partial
1654
1654
 
1655
1655
  ```typescript
1656
1656
  // api/src/application/user/user.types.ts
1657
1657
  export const UserSaveParams = UserBaseSchema.partial({
1658
- id: true, // update 필요
1658
+ id: true, // needed for update
1659
1659
  created_at: true, // dbDefault
1660
1660
  password: true, // nullable
1661
1661
  email: true, // nullable
@@ -1669,27 +1669,27 @@ export const UserSaveParams = UserBaseSchema.partial({
1669
1669
  });
1670
1670
  ```
1671
1671
 
1672
- **적용 기준:**
1672
+ **Application criteria:**
1673
1673
 
1674
- - id, created_at, updated_at: 항상 partial (자동 생성)
1675
- - dbDefault 있는 필드: partial 처리
1676
- - nullable: true FK 필드: partial 처리
1677
- - nullable: true 일반 필드 (description): partial 처리
1674
+ - id, created_at, updated_at: always partial (auto-generated)
1675
+ - Fields with dbDefault: set to partial
1676
+ - FK fields with nullable: true: set to partial
1677
+ - Regular fields with nullable: true (e.g. description): set to partial
1678
1678
 
1679
- **핵심:** 필수 필드(employee_no, login_id, name, institution_id) partial 제외하여 타입 안정성 유지
1679
+ **Key:** Required fields (employee_no, login_id, name, institution_id) are excluded from partial to maintain type safety
1680
1680
 
1681
- ### 3. Update Relation 필드 제외 패턴
1681
+ ### 3. Excluding Relation Fields on Update
1682
1682
 
1683
- **문제:** Subset에는 relation 객체가 포함되지만, SaveParams에는 FK 있어서 에러 발생
1683
+ **Problem:** Subset includes relation objects, but SaveParams only has FK, causing errors
1684
1684
 
1685
1685
  ```typescript
1686
1686
  // WRONG
1687
1687
  const user = await UserModel.findById("A", userId);
1688
1688
  await UserModel.save([{ ...user, status: "inactive" }]);
1689
- // → "column 'department' does not exist" 에러
1689
+ // → "column 'department' does not exist" error
1690
1690
  ```
1691
1691
 
1692
- **해결:** Relation 필드 제외 + FK 명시적 추가
1692
+ **Solution:** Exclude relation fields + explicitly add FK
1693
1693
 
1694
1694
  ```typescript
1695
1695
  // CORRECT
@@ -1698,81 +1698,81 @@ const { institution, department, ...userData } = user;
1698
1698
  await UserModel.save([
1699
1699
  {
1700
1700
  ...userData,
1701
- institution_id: user.institution.id, // FK 명시적 추가
1701
+ institution_id: user.institution.id, // explicitly add FK
1702
1702
  department_id: user.department?.id ?? null,
1703
1703
  status: "inactive",
1704
1704
  },
1705
1705
  ]);
1706
1706
  ```
1707
1707
 
1708
- **이유:** `UserSubsetA`는 `institution`, `department` 객체를 포함하지만, `institution_id`, `department_id` FK는 포함하지 않음
1708
+ **Reason:** `UserSubsetA` includes `institution`, `department` objects, but does not include `institution_id`, `department_id` FKs
1709
1709
 
1710
- ### 4. ubUpsert Upsert 동작
1710
+ ### 4. ubUpsert is an Upsert Operation
1711
1711
 
1712
- **문제:** Unique constraint 위반 테스트가 실패함
1712
+ **Problem:** Unique constraint violation tests fail
1713
1713
 
1714
1714
  ```typescript
1715
- // 실패하는 테스트
1716
- test("사번은 고유해야 ", async () => {
1715
+ // failing test
1716
+ test("employee number must be unique", async () => {
1717
1717
  await UserModel.save([{ employee_no: "001", ... }]);
1718
1718
 
1719
- // 중복된 사번으로 생성 시도
1719
+ // attempt to create with duplicate employee number
1720
1720
  await expect(
1721
1721
  UserModel.save([{ employee_no: "001", ... }])
1722
- ).rejects.toThrow(); // 에러 던지고 UPDATE
1722
+ ).rejects.toThrow(); // does not throw error, performs UPDATE instead
1723
1723
  });
1724
1724
  ```
1725
1725
 
1726
- **원인:** Sonamu `save()`는 `ubUpsert` 사용 → conflict 에러 대신 UPDATE
1726
+ **Cause:** Sonamu's `save()` uses `ubUpsert` → on conflict, performs UPDATE instead of throwing error
1727
1727
 
1728
- **해결:** 이런 테스트는 skip 처리
1728
+ **Solution:** Skip such tests
1729
1729
 
1730
1730
  ```typescript
1731
- test.skip("사번은 고유해야 (ubUpsert upsert 동작하므로 skip)", async () => {
1731
+ test.skip("employee number must be unique (skipped because ubUpsert performs upsert)", async () => {
1732
1732
  // ...
1733
1733
  });
1734
1734
  ```
1735
1735
 
1736
- ### 5. testAs 사용법
1736
+ ### 5. testAs Usage
1737
1737
 
1738
- **문제:** test 안에서 testAs 호출하면 에러 발생
1738
+ **Problem:** Calling testAs inside test causes an error
1739
1739
 
1740
1740
  ```typescript
1741
1741
  // WRONG
1742
- test("권한 테스트", async () => {
1743
- await testAs(adminUser, "설명", async () => { ... });
1744
- // → "Calling the test function inside another test function is not allowed" 에러
1742
+ test("permission test", async () => {
1743
+ await testAs(adminUser, "description", async () => { ... });
1744
+ // → "Calling the test function inside another test function is not allowed" error
1745
1745
  });
1746
1746
 
1747
- // CORRECT - test를 대체
1748
- testAs(adminUser, "권한 테스트", async () => {
1747
+ // CORRECT - use as a replacement for test
1748
+ testAs(adminUser, "permission test", async () => {
1749
1749
  const result = await UserModel.del([userId]);
1750
1750
  expect(result).toBe(1);
1751
1751
  });
1752
1752
  ```
1753
1753
 
1754
- ### 6. Naite로 Model 쿼리 검증
1754
+ ### 6. Validating Model Queries with Naite
1755
1755
 
1756
- **Model에 Naite 기록 추가:**
1756
+ **Add Naite recording to Model:**
1757
1757
 
1758
1758
  ```typescript
1759
1759
  // user.model.ts
1760
1760
  import { Naite } from "sonamu";
1761
1761
 
1762
1762
  async findMany(...) {
1763
- // ... qb 구성 ...
1763
+ // ... build qb ...
1764
1764
 
1765
- // 테스트를 위한 쿼리 기록
1765
+ // record query for testing
1766
1766
  Naite.t("esq-query", qb.toQuery());
1767
1767
 
1768
1768
  return this.executeSubsetQuery({ ... });
1769
1769
  }
1770
1770
  ```
1771
1771
 
1772
- **Test에서 검증:**
1772
+ **Validate in test:**
1773
1773
 
1774
1774
  ```typescript
1775
- test("num: 0일 limit 없어야 ", async () => {
1775
+ test("should not have limit when num: 0", async () => {
1776
1776
  await UserModel.findMany("A", { num: 0, page: 1 });
1777
1777
 
1778
1778
  expect(Naite.get("esq-query").first()).not.contain("limit");
@@ -1780,81 +1780,81 @@ test("num: 0일 때 limit 없어야 함", async () => {
1780
1780
  });
1781
1781
  ```
1782
1782
 
1783
- ### 7. 에러 메시지는 다국어 고려
1783
+ ### 7. Consider Multilingual Error Messages
1784
1784
 
1785
1785
  ```typescript
1786
- // WRONG: 영어 메시지만 검증
1786
+ // WRONG: only validates English message
1787
1787
  await expect(UserModel.findById("A", 99999)).rejects.toThrow("not found");
1788
1788
 
1789
- // CORRECT: 한글 메시지 부분 매칭
1790
- await expect(UserModel.findById("A", 99999)).rejects.toThrow("존재하지 않는");
1789
+ // CORRECT: partial match on actual error message
1790
+ await expect(UserModel.findById("A", 99999)).rejects.toThrow("does not exist");
1791
1791
  ```
1792
1792
 
1793
- ### 8. pnpm Workspace Vitest 인스턴스 충돌
1793
+ ### 8. pnpm Workspace and Vitest Instance Conflicts
1794
1794
 
1795
- **문제:** "Vitest failed to access its internal state" 에러
1795
+ **Problem:** "Vitest failed to access its internal state" error
1796
1796
 
1797
- **원인:** sonamu `link:`로 연결되어 있으면, sonamu와 프로젝트의 vitest 다른 peer dependency 조합으로 별도 경로에 설치됨
1797
+ **Cause:** When sonamu is connected via `link:`, sonamu and the project's vitest are installed at separate paths with different peer dependency combinations
1798
1798
 
1799
- **임시 해결 (테스트용):**
1799
+ **Temporary fix (for testing):**
1800
1800
 
1801
1801
  ```json
1802
1802
  // packages/api/package.json
1803
1803
  {
1804
1804
  "dependencies": {
1805
- "sonamu": "0.8.0" // link 대신 버전 명시
1805
+ "sonamu": "0.8.0" // specify version instead of link
1806
1806
  }
1807
1807
  }
1808
1808
  ```
1809
1809
 
1810
- **근본 해결:** sonamu 개발자에게 문의 (프레임워크 내부 이슈)
1810
+ **Fundamental fix:** Contact sonamu developers (framework internal issue)
1811
1811
 
1812
1812
  ### 9. assert() for Truthy Checks
1813
1813
 
1814
1814
  ```typescript
1815
1815
  import assert from "assert";
1816
1816
 
1817
- test("사용자 생성", async () => {
1817
+ test("create user", async () => {
1818
1818
  const [userId] = await UserModel.save([{ ... }]);
1819
1819
 
1820
- // truthy 체크
1820
+ // truthy check
1821
1821
  assert(userId);
1822
1822
 
1823
- // 이후 userId number로 확실히 타입 추론됨
1823
+ // userId is now safely inferred as number
1824
1824
  const user = await UserModel.findById("A", userId);
1825
1825
  });
1826
1826
  ```
1827
1827
 
1828
- ### 10. 테스트 데이터는 직접 생성
1828
+ ### 10. Create Test Data Directly
1829
1829
 
1830
- **miomock 컨벤션:** Fixture 최소화, 데이터는 테스트 내에서 직접 생성
1830
+ **miomock convention:** Minimize fixtures, create data directly within tests
1831
1831
 
1832
1832
  ```typescript
1833
- // 권장 패턴
1834
- test("사용자 생성", async () => {
1833
+ // recommended pattern
1834
+ test("create user", async () => {
1835
1835
  const [userId] = await UserModel.save([
1836
1836
  {
1837
1837
  employee_no: "2026001",
1838
1838
  login_id: "testuser",
1839
- name: "테스트유저",
1839
+ name: "Test User",
1840
1840
  institution_id: 1,
1841
- // ... 필요한 필드들
1841
+ // ... required fields
1842
1842
  },
1843
1843
  ]);
1844
1844
 
1845
1845
  const user = await UserModel.findById("A", userId);
1846
- expect(user.name).toBe("테스트유저");
1846
+ expect(user.name).toBe("Test User");
1847
1847
  });
1848
1848
 
1849
- // Fixture는 공통 데이터에만 사용
1850
- const f = await loadFixtures(["institution01"]); // 기관 같은 공통 데이터만
1849
+ // Fixtures only for shared data
1850
+ const f = await loadFixtures(["institution01"]); // only for shared data like institutions
1851
1851
  ```
1852
1852
 
1853
- ## 복잡한 엔티티 테스트 전략
1853
+ ## Complex Entity Test Strategy
1854
1854
 
1855
- 엔티티 의존성이 복잡한 경우 (Institution → Department → User → Task → TaskParticipant) 테스트 헬퍼 함수를 활용한다.
1855
+ When dependencies between entities are complex (Institution → Department → User → Task → TaskParticipant), use test helper functions.
1856
1856
 
1857
- ### 테스트 헬퍼 함수 정의
1857
+ ### Defining Test Helper Functions
1858
1858
 
1859
1859
  ```typescript
1860
1860
  // api/src/testing/test-helpers.ts
@@ -1864,7 +1864,7 @@ import { DepartmentModel } from "../application/department/department.model";
1864
1864
  import { UserModel } from "../application/user/user.model";
1865
1865
  import { TaskModel } from "../application/task/task.model";
1866
1866
 
1867
- // 헬퍼는 최소 필수 필드만 요구하고, 나머지는 기본값 제공
1867
+ // each helper requires only the minimum required fields and provides defaults for the rest
1868
1868
  let counter = 0;
1869
1869
  function uniqueId(prefix: string) {
1870
1870
  return `${prefix}_${Date.now()}_${++counter}`;
@@ -1875,7 +1875,7 @@ export async function createTestInstitution(
1875
1875
  ) {
1876
1876
  const [id] = await InstitutionModel.save([
1877
1877
  {
1878
- name: "테스트기관",
1878
+ name: "Test Institution",
1879
1879
  code: uniqueId("INST"),
1880
1880
  ...override,
1881
1881
  },
@@ -1890,7 +1890,7 @@ export async function createTestDepartment(
1890
1890
  ) {
1891
1891
  const [id] = await DepartmentModel.save([
1892
1892
  {
1893
- name: "테스트부서",
1893
+ name: "Test Department",
1894
1894
  code: uniqueId("DEPT"),
1895
1895
  dept_type: "division",
1896
1896
  institution_id: institutionId,
@@ -1911,7 +1911,7 @@ export async function createTestUser(
1911
1911
  {
1912
1912
  employee_no: uniqueId("EMP"),
1913
1913
  login_id: uniqueId("login"),
1914
- name: "테스트사용자",
1914
+ name: "Test User",
1915
1915
  institution_id: institutionId,
1916
1916
  ...override,
1917
1917
  },
@@ -1927,7 +1927,7 @@ export async function createTestTask(
1927
1927
  const [id] = await TaskModel.save([
1928
1928
  {
1929
1929
  task_no: uniqueId("TASK"),
1930
- title: "테스트과제",
1930
+ title: "Test Task",
1931
1931
  year: new Date().getFullYear(),
1932
1932
  begin_date: new Date(),
1933
1933
  end_date: new Date(Date.now() + 365 * 24 * 60 * 60 * 1000),
@@ -1939,7 +1939,7 @@ export async function createTestTask(
1939
1939
  return id;
1940
1940
  }
1941
1941
 
1942
- // 의존성 체인을 번에 생성
1942
+ // create the entire dependency chain at once
1943
1943
  export async function createTestTaskWithDeps(
1944
1944
  taskOverride?: Partial<TaskSaveParams>,
1945
1945
  ) {
@@ -1958,33 +1958,33 @@ export async function createTestUserWithDeps(
1958
1958
  }
1959
1959
  ```
1960
1960
 
1961
- ### 테스트에서 사용
1961
+ ### Using in Tests
1962
1962
 
1963
1963
  ```typescript
1964
1964
  import { createTestTaskWithDeps, createTestUser } from "../../testing/test-helpers";
1965
1965
 
1966
1966
  describe("TaskModel", () => {
1967
- // GOOD: 헬퍼 함수로 간결하게
1968
- test("Create - 최소 필수 필드로 생성", async () => {
1967
+ // GOOD: concise with helper functions
1968
+ test("Create - create with minimum required fields", async () => {
1969
1969
  const { taskId } = await createTestTaskWithDeps();
1970
1970
 
1971
1971
  const task = await TaskModel.findById("D", taskId);
1972
1972
  expect(task.id).toBe(taskId);
1973
1973
  });
1974
1974
 
1975
- // GOOD: 특정 필드 커스터마이즈
1976
- test("Create - 특정 상태로 생성", async () => {
1975
+ // GOOD: customize specific fields
1976
+ test("Create - create with specific status", async () => {
1977
1977
  const { taskId } = await createTestTaskWithDeps({
1978
1978
  status: "approved",
1979
- title: "승인된 과제",
1979
+ title: "Approved Task",
1980
1980
  });
1981
1981
 
1982
1982
  const task = await TaskModel.findById("D", taskId);
1983
1983
  expect(task.status).toBe("approved");
1984
1984
  });
1985
1985
 
1986
- // BAD: 테스트마다 의존성 직접 생성 (반복적)
1987
- test("Create - 직접 생성 (권장하지 않음)", async () => {
1986
+ // BAD: creating dependencies directly in every test (repetitive)
1987
+ test("Create - direct creation (not recommended)", async () => {
1988
1988
  const [institutionId] = await InstitutionModel.save([{ name: "...", code: "..." }]);
1989
1989
  assert(institutionId);
1990
1990
  const [userId] = await UserModel.save([{ ... }]);
@@ -1996,14 +1996,14 @@ describe("TaskModel", () => {
1996
1996
  });
1997
1997
  ```
1998
1998
 
1999
- ### Subset → SaveParams 변환 헬퍼
1999
+ ### Subset → SaveParams Conversion Helper
2000
2000
 
2001
- findById 결과를 수정 다시 save할 relation을 FK로 변환해야 한다:
2001
+ When modifying findById results and saving again, relations must be converted to FKs:
2002
2002
 
2003
2003
  ```typescript
2004
2004
  // api/src/testing/test-helpers.ts
2005
2005
 
2006
- // Task Subset A → SaveParams 변환
2006
+ // Task Subset A → SaveParams conversion
2007
2007
  export function taskToSaveParams(task: TaskSubsetA): TaskSaveParams {
2008
2008
  const {
2009
2009
  program,
@@ -2024,7 +2024,7 @@ export function taskToSaveParams(task: TaskSubsetA): TaskSaveParams {
2024
2024
  };
2025
2025
  }
2026
2026
 
2027
- // 범용 헬퍼 (주의: relation 필드명이 다른 경우 직접 작성 필요)
2027
+ // generic helper (note: write directly if relation field names differ)
2028
2028
  export function relationToFk<T extends Record<string, any>>(
2029
2029
  data: T,
2030
2030
  relationFields: string[],
@@ -2044,7 +2044,7 @@ export function relationToFk<T extends Record<string, any>>(
2044
2044
  }
2045
2045
  ```
2046
2046
 
2047
- ### Update 테스트 간소화
2047
+ ### Simplifying Update Tests
2048
2048
 
2049
2049
  ```typescript
2050
2050
  import {
@@ -2052,30 +2052,30 @@ import {
2052
2052
  taskToSaveParams,
2053
2053
  } from "../../testing/test-helpers";
2054
2054
 
2055
- test("Update - 과제 정보 수정", async () => {
2055
+ test("Update - update task info", async () => {
2056
2056
  const { taskId } = await createTestTaskWithDeps();
2057
2057
 
2058
2058
  const task = await TaskModel.findById("A", taskId);
2059
2059
  await TaskModel.save([
2060
2060
  {
2061
2061
  ...taskToSaveParams(task),
2062
- title: "수정된 제목",
2062
+ title: "Updated Title",
2063
2063
  },
2064
2064
  ]);
2065
2065
 
2066
2066
  const updated = await TaskModel.findById("A", taskId);
2067
- expect(updated.title).toBe("수정된 제목");
2067
+ expect(updated.title).toBe("Updated Title");
2068
2068
  });
2069
2069
  ```
2070
2070
 
2071
- ### 주의사항
2071
+ ### Notes
2072
2072
 
2073
- **beforeAll/beforeEach 사용 금지:**
2073
+ **Do not use beforeAll/beforeEach:**
2074
2074
 
2075
- sonamu 테스트 환경에서 beforeAll/beforeEach 데이터를 생성하면 sonamu 내부 코드를 바라보게 있다. 대신 테스트 내에서 헬퍼 함수를 호출한다.
2075
+ In sonamu's test environment, creating data with beforeAll/beforeEach may end up referencing sonamu internal code. Instead, call helper functions within each test.
2076
2076
 
2077
2077
  ```typescript
2078
- // WRONG: beforeAll 사용
2078
+ // WRONG: using beforeAll
2079
2079
  describe("TaskModel", () => {
2080
2080
  let taskId: number;
2081
2081
  beforeAll(async () => {
@@ -2084,49 +2084,49 @@ describe("TaskModel", () => {
2084
2084
  });
2085
2085
 
2086
2086
  test("...", async () => {
2087
- // taskId 사용 - 문제 발생 가능
2087
+ // using taskId - may cause problems
2088
2088
  });
2089
2089
  });
2090
2090
 
2091
- // CORRECT: 테스트에서 생성
2091
+ // CORRECT: create in each test
2092
2092
  describe("TaskModel", () => {
2093
2093
  test("...", async () => {
2094
2094
  const { taskId } = await createTestTaskWithDeps();
2095
- // taskId 사용
2095
+ // use taskId
2096
2096
  });
2097
2097
  });
2098
2098
  ```
2099
2099
 
2100
2100
  ---
2101
2101
 
2102
- ## 흔한 실수와 해결 방법
2102
+ ## Common Mistakes and Solutions
2103
2103
 
2104
- ### ubUpsert unique constraint 에러를 던지지 않습니다
2104
+ ### ubUpsert Does Not Throw Unique Constraint Errors
2105
2105
 
2106
- **→ "실전 주의사항 #4. ubUpsert Upsert 동작" 참조**
2106
+ **→ See "Practical Notes #4. ubUpsert is an Upsert Operation" above**
2107
2107
 
2108
- ### Transaction isolation과 테스트 격리
2108
+ ### Transaction Isolation and Test Isolation
2109
2109
 
2110
- 테스트는 독립된 트랜잭션에서 실행되므로 데이터가 격리됩니다. 같은 테스트 내에서도 생성한 데이터가 쿼리에 즉시 보이지 않을 있습니다.
2110
+ Each test runs in an independent transaction so data is isolated. Even within the same test, data you created may not be immediately visible in queries.
2111
2111
 
2112
2112
  ```typescript
2113
- // BAD: 정확한 개수를 기대하면 실패할 수 있음
2114
- test("역할명 검색", async () => {
2115
- await createTestRole({ name: "관리자A" });
2116
- await createTestRole({ name: "관리자B" });
2113
+ // BAD: expecting exact count may fail
2114
+ test("search by role name", async () => {
2115
+ await createTestRole({ name: "AdminA" });
2116
+ await createTestRole({ name: "AdminB" });
2117
2117
 
2118
2118
  const { rows } = await RoleModel.findMany("A", {
2119
- keyword: "관리자",
2119
+ keyword: "Admin",
2120
2120
  });
2121
2121
 
2122
- // Transaction isolation으로 인해 2개가 보이지 않을 있음
2122
+ // may not see 2 due to transaction isolation
2123
2123
  expect(rows.length).toBe(2);
2124
2124
  });
2125
2125
 
2126
- // GOOD: 고유 식별자와 유연한 assertion 사용
2127
- test("역할명 검색", async () => {
2128
- // 고유한 식별자로 충돌 방지
2129
- const testName = `검색테스트_${Date.now()}`;
2126
+ // GOOD: use unique identifier and flexible assertion
2127
+ test("search by role name", async () => {
2128
+ // unique identifier to prevent conflicts
2129
+ const testName = `SearchTest_${Date.now()}`;
2130
2130
  await createTestRole({ name: `${testName}A` });
2131
2131
  await createTestRole({ name: `${testName}B` });
2132
2132
 
@@ -2134,28 +2134,28 @@ test("역할명 검색", async () => {
2134
2134
  keyword: testName,
2135
2135
  });
2136
2136
 
2137
- // 최소 1개 이상 확인
2137
+ // verify at least 1
2138
2138
  expect(rows.length).toBeGreaterThanOrEqual(1);
2139
- // 내용 검증
2139
+ // content validation
2140
2140
  expect(rows.some((r) => r.name.includes(testName))).toBe(true);
2141
2141
  });
2142
2142
  ```
2143
2143
 
2144
- **패턴:**
2144
+ **Patterns:**
2145
2145
 
2146
- - 고유 식별자 사용: `Date.now()`, `uuid()` 등으로 충돌 방지
2147
- - 유연한 assertion: `toBeGreaterThanOrEqual(1)` 대신 `toBe(2)` 사용
2148
- - 내용 검증: 개수보다 실제 데이터가 맞는지 확인
2146
+ - Use unique identifiers: `Date.now()`, `uuid()`, etc. to prevent conflicts
2147
+ - Flexible assertions: use `toBeGreaterThanOrEqual(1)` instead of `toBe(2)`
2148
+ - Content validation: verify actual data matches rather than count
2149
2149
 
2150
- ### 정렬 테스트의 조건부 검증
2150
+ ### Conditional Validation for Sorting Tests
2151
2151
 
2152
- 정렬 테스트에서 모든 데이터가 조회되지 않을 있으므로 조건부 검증을 사용합니다:
2152
+ Since not all data may be returned in sorting tests, use conditional validation:
2153
2153
 
2154
2154
  ```typescript
2155
- // BAD: 항상 항목이 조회된다고 가정
2156
- test("정렬 - ID 최신순", async () => {
2157
- const id1 = await createTestRole({ name: "역할1" });
2158
- const id2 = await createTestRole({ name: "역할2" });
2155
+ // BAD: assumes two items are always returned
2156
+ test("sort - newest ID first", async () => {
2157
+ const id1 = await createTestRole({ name: "Role1" });
2158
+ const id2 = await createTestRole({ name: "Role2" });
2159
2159
 
2160
2160
  const { rows } = await RoleModel.findMany("A", {
2161
2161
  orderBy: "id-desc",
@@ -2164,14 +2164,14 @@ test("정렬 - ID 최신순", async () => {
2164
2164
  const id2Index = rows.findIndex((r) => r.id === id2);
2165
2165
  const id1Index = rows.findIndex((r) => r.id === id1);
2166
2166
 
2167
- // 하나라도 없으면 실패
2167
+ // fails if either is missing
2168
2168
  expect(id2Index).toBeLessThan(id1Index);
2169
2169
  });
2170
2170
 
2171
- // GOOD: 조건부 검증
2172
- test("정렬 - ID 최신순", async () => {
2173
- const id1 = await createTestRole({ name: "역할1" });
2174
- const id2 = await createTestRole({ name: "역할2" });
2171
+ // GOOD: conditional validation
2172
+ test("sort - newest ID first", async () => {
2173
+ const id1 = await createTestRole({ name: "Role1" });
2174
+ const id2 = await createTestRole({ name: "Role2" });
2175
2175
 
2176
2176
  const { rows } = await RoleModel.findMany("A", {
2177
2177
  orderBy: "id-desc",
@@ -2180,7 +2180,7 @@ test("정렬 - ID 최신순", async () => {
2180
2180
  const testRoles = rows.filter((r) => [id1, id2].includes(r.id));
2181
2181
  expect(testRoles.length).toBeGreaterThanOrEqual(1);
2182
2182
 
2183
- // 역할이 모두 조회된 경우에만 순서 검증
2183
+ // only validate order when both roles are returned
2184
2184
  if (testRoles.length === 2) {
2185
2185
  const id2Index = rows.findIndex((r) => r.id === id2);
2186
2186
  const id1Index = rows.findIndex((r) => r.id === id1);
@@ -2189,10 +2189,10 @@ test("정렬 - ID 최신순", async () => {
2189
2189
  });
2190
2190
  ```
2191
2191
 
2192
- **핵심:** Transaction isolation으로 인한 불확실성을 받아들이고, 검증 가능한 경우에만 assertion을 수행합니다.
2192
+ **Key:** Accept the uncertainty caused by transaction isolation, and only assert when validation is possible.
2193
2193
 
2194
2194
  ---
2195
2195
 
2196
- ## Fixture 데이터 생성
2196
+ ## Fixture Data Creation Tips
2197
2197
 
2198
- **→ 상세 가이드 (unique constraint 처리, 한국어 데이터, gen vs fetch 선택, DB 시퀀스 리셋, FixtureGenerator 커스터마이징): `fixture-cli.md` "실전 " 섹션**
2198
+ **→ Detailed guide (unique constraint handling, gen vs fetch selection, DB sequence reset, FixtureGenerator customization): `fixture-cli.md` "Practical Tips" section**