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,86 +1,86 @@
1
1
  ---
2
2
  name: sonamu-frontend
3
- description: Sonamu 프론트엔드 연동. 자동 생성 Service, TanStack Query hook, useTypeForm/useListParams/useSelection, FileInput, MultiSelect, SonamuProvider (react-components v0.1.8+). Use when calling APIs, building forms, handling file uploads, or managing list/selection states.
3
+ description: Sonamu frontend integration. Auto-generated Service, TanStack Query hooks, useTypeForm/useListParams/useSelection, FileInput, MultiSelect, SonamuProvider (react-components v0.1.8+). Use when calling APIs, building forms, handling file uploads, or managing list/selection states.
4
4
  ---
5
5
 
6
6
  # Frontend Service
7
7
 
8
- ## 빠른 참조
8
+ ## Quick Reference
9
9
 
10
10
  ### Hooks
11
11
 
12
- | Hook | 용도 | 주요 반환값 |
13
- | --------------- | -------------------------- | ------------------------------------------------ |
14
- | `useTypeForm` | 상태 관리 (Zod 기반) | form, setForm, register, submit, addError, reset |
15
- | `useListParams` | URL 동기화 리스트 파라미터 | listParams, setListParams, register |
16
- | `useSelection` | 체크박스 다중 선택 | selectedKeys, toggle, selectAll, deselectAll |
17
- | `useModal` | 모달 상태 관리 | open, modal |
18
- | `useToast` | 토스트 알림 | toast |
19
-
20
- ### 컴포넌트
21
-
22
- | 컴포넌트 | 용도 | 주요 Props |
23
- | ------------- | ------------ | --------------------------------------------------- |
24
- | `Input` | 텍스트 입력 | value, onValueChange |
25
- | `Textarea` | 여러 줄 입력 | value, onValueChange |
26
- | `Checkbox` | 체크박스 | value (boolean), onValueChange, label |
27
- | `Select` | 단일 선택 | items, value, onValueChange, placeholder, clearable |
28
- | `MultiSelect` | 다중 선택 | options, value (array), onValueChange, maxCount |
29
- | `EnumSelect` | Enum 선택 | enum, labels, value, onValueChange |
30
- | `FileInput` | 파일 업로드 | uploadMode, viewMode, multiple, maxFiles |
31
-
32
- ### Service (자동 생성)
33
-
34
- | 메서드 | 용도 | 예시 |
35
- | ----------------- | ---------------- | ----------------------------------- |
36
- | `get{Entity}` | 단일 조회 | `UserService.getUser("A", 123)` |
37
- | `get{Entities}` | 목록 조회 | `UserService.getUsers("P", params)` |
38
- | `save` | 저장 (생성/수정) | `UserService.save([data])` |
39
- | `del` | 삭제 | `UserService.del([1, 2, 3])` |
40
- | `use{Entity}` | 단일 조회 hook | `UserService.useUser("A", id)` |
41
- | `use{Entities}` | 목록 조회 hook | `UserService.useUsers("P", params)` |
42
- | `useSaveMutation` | 저장 mutation | `UserService.useSaveMutation()` |
43
-
44
- ### 유틸리티
45
-
46
- | 함수 | 용도 | 예시 |
47
- | ------------------ | -------------------- | ------------------------------------------------- |
48
- | `dateF` | 날짜 포맷 | `dateF(new Date())` → `"2024-01-15"` |
49
- | `datetimeF` | 날짜시간 포맷 | `datetimeF(new Date())` → `"2024-01-15 10:30:00"` |
50
- | `numF` | 숫자 포맷 | `numF(1234567)` → `"1,234,567"` |
51
- | `hidden` | 조건부 hidden 클래스 | `hidden(true)` → `"hidden"` |
52
- | `arrayableToArray` | 배열 변환 | `arrayableToArray("a")` → `["a"]` |
53
-
54
- ### 설정
55
-
56
- | 항목 | 설명 | 필수 여부 |
57
- | ---------------- | --------------------------------------- | ----------------------------------- |
58
- | `SonamuProvider` | 전역 설정 Provider (uploader, auth, SD) | 필수 (uploader FileInput 사용 ) |
59
- | `uploader` | 파일 업로드 함수 | FileInput 사용 필수 |
60
- | `auth` | 인증 상태 함수 | 옵션 |
61
- | `SD` | 다국어 함수 | 옵션 |
12
+ | Hook | Purpose | Key Return Values |
13
+ | --------------- | ------------------------------------ | -------------------------------------------------------- |
14
+ | `useTypeForm` | Form state management (Zod-based) | form, setForm, register, submit, addError, reset |
15
+ | `useListParams` | URL-synced list parameters | listParams, setListParams, register |
16
+ | `useSelection` | Checkbox multi-selection | selectedKeys, toggle, selectAll, deselectAll |
17
+ | `useModal` | Modal state management | open, modal |
18
+ | `useToast` | Toast notifications | toast |
19
+
20
+ ### Components
21
+
22
+ | Component | Purpose | Key Props |
23
+ | ------------- | -------------------- | --------------------------------------------------- |
24
+ | `Input` | Text input | value, onValueChange |
25
+ | `Textarea` | Multi-line input | value, onValueChange |
26
+ | `Checkbox` | Checkbox | value (boolean), onValueChange, label |
27
+ | `Select` | Single select | items, value, onValueChange, placeholder, clearable |
28
+ | `MultiSelect` | Multi-select | options, value (array), onValueChange, maxCount |
29
+ | `EnumSelect` | Enum select | enum, labels, value, onValueChange |
30
+ | `FileInput` | File upload | uploadMode, viewMode, multiple, maxFiles |
31
+
32
+ ### Service (Auto-generated)
33
+
34
+ | Method | Purpose | Example |
35
+ | ----------------- | ------------------------ | -------------------------------------- |
36
+ | `get{Entity}` | Fetch single record | `UserService.getUser("A", 123)` |
37
+ | `get{Entities}` | Fetch list | `UserService.getUsers("P", params)` |
38
+ | `save` | Save (create/update) | `UserService.save([data])` |
39
+ | `del` | Delete | `UserService.del([1, 2, 3])` |
40
+ | `use{Entity}` | Single fetch hook | `UserService.useUser("A", id)` |
41
+ | `use{Entities}` | List fetch hook | `UserService.useUsers("P", params)` |
42
+ | `useSaveMutation` | Save mutation | `UserService.useSaveMutation()` |
43
+
44
+ ### Utilities
45
+
46
+ | Function | Purpose | Example |
47
+ | ------------------ | ---------------------------- | ---------------------------------------------------- |
48
+ | `dateF` | Date formatting | `dateF(new Date())` → `"2024-01-15"` |
49
+ | `datetimeF` | Datetime formatting | `datetimeF(new Date())` → `"2024-01-15 10:30:00"` |
50
+ | `numF` | Number formatting | `numF(1234567)` → `"1,234,567"` |
51
+ | `hidden` | Conditional hidden class | `hidden(true)` → `"hidden"` |
52
+ | `arrayableToArray` | Convert to array | `arrayableToArray("a")` → `["a"]` |
53
+
54
+ ### Configuration
55
+
56
+ | Item | Description | Required |
57
+ | ---------------- | ------------------------------------------------ | ----------------------------------------- |
58
+ | `SonamuProvider` | Global configuration Provider (uploader, auth, SD) | Required (uploader required for FileInput) |
59
+ | `uploader` | File upload function | Required when using FileInput |
60
+ | `auth` | Authentication state and functions | Optional |
61
+ | `SD` | Internationalization function | Optional |
62
62
 
63
63
  ---
64
64
 
65
65
  # Frontend Service
66
66
 
67
- ## 기본 사용
67
+ ## Basic Usage
68
68
 
69
69
  ```typescript
70
70
  import { UserService } from "@/services/services.generated";
71
71
 
72
- // 단일 조회 (Subset 필수) - get{Entity} 형태
72
+ // Single fetch (Subset required) - get{Entity} form
73
73
  const user = await UserService.getUser("A", 123);
74
74
 
75
- // 목록 조회 - get{Entities} 형태
75
+ // List fetch - get{Entities} form
76
76
  const { rows, total } = await UserService.getUsers("P", { num: 20, page: 1 });
77
77
 
78
- // 저장
78
+ // Save
79
79
  const [userId] = await UserService.save([
80
80
  { email: "new@test.com", username: "newuser" },
81
81
  ]);
82
82
 
83
- // 삭제
83
+ // Delete
84
84
  const count = await UserService.del([1, 2, 3]);
85
85
  ```
86
86
 
@@ -90,7 +90,7 @@ const count = await UserService.del([1, 2, 3]);
90
90
 
91
91
  ```typescript
92
92
  function UserProfile({ userId }: { userId: number }) {
93
- // use{Entity} 형태 (단일), use{Entities} 형태 (목록)
93
+ // use{Entity} form (single), use{Entities} form (list)
94
94
  const { data: user, isLoading, error } = UserService.useUser("A", userId);
95
95
 
96
96
  if (isLoading) return <div>Loading...</div>;
@@ -113,11 +113,11 @@ function EditProfile() {
113
113
  });
114
114
  }
115
115
 
116
- return <button disabled={saveMutation.isPending}>저장</button>;
116
+ return <button disabled={saveMutation.isPending}>Save</button>;
117
117
  }
118
118
  ```
119
119
 
120
- ### 조건부 페칭
120
+ ### Conditional Fetching
121
121
 
122
122
  ```typescript
123
123
  const { data } = UserService.useUser("A", userId!, {
@@ -125,7 +125,7 @@ const { data } = UserService.useUser("A", userId!, {
125
125
  });
126
126
  ```
127
127
 
128
- ### 캐시 무효화
128
+ ### Cache Invalidation
129
129
 
130
130
  ```typescript
131
131
  const queryClient = useQueryClient();
@@ -134,9 +134,9 @@ queryClient.invalidateQueries({ queryKey: ["User", "findById", "A", userId] });
134
134
 
135
135
  ## useTypeForm
136
136
 
137
- Zod 스키마 기반 타입 안전 관리 (react-components v0.1.8+)
137
+ Type-safe form management based on Zod schemas (react-components v0.1.8+)
138
138
 
139
- ### 반환값
139
+ ### Return Values
140
140
 
141
141
  ```typescript
142
142
  const {
@@ -151,29 +151,29 @@ const {
151
151
  } = useTypeForm(Schema, defaultValue);
152
152
  ```
153
153
 
154
- | 반환값 | 타입 | 설명 |
155
- | ------------- | --------------------------------------------------- | -------------------------- |
156
- | `form` | `z.infer<Schema>` | 현재 데이터 |
157
- | `setForm` | `React.Dispatch<SetStateAction<...>>` | 상태 업데이트 함수 |
158
- | `register` | `(field: string) => RegisterReturn` | 필드 등록 함수 |
159
- | `submit` | `(callback) => () => Promise<R>` | 제출 핸들러 생성 |
160
- | `addError` | `(path: string, error: string \| ErrorObj) => void` | 에러 수동 추가 |
161
- | `removeError` | `(path: string) => void` | 특정 필드 에러 제거 |
162
- | `clearError` | `() => void` | 모든 에러 제거 |
163
- | `reset` | `() => void` | 폼을 defaultValue로 초기화 |
154
+ | Return Value | Type | Description |
155
+ | ------------- | --------------------------------------------------- | ---------------------------------------- |
156
+ | `form` | `z.infer<Schema>` | Current form data |
157
+ | `setForm` | `React.Dispatch<SetStateAction<...>>` | Form state update function |
158
+ | `register` | `(field: string) => RegisterReturn` | Field registration function |
159
+ | `submit` | `(callback) => () => Promise<R>` | Submit handler factory |
160
+ | `addError` | `(path: string, error: string \| ErrorObj) => void` | Manually add an error |
161
+ | `removeError` | `(path: string) => void` | Remove error for a specific field |
162
+ | `clearError` | `() => void` | Clear all errors |
163
+ | `reset` | `() => void` | Reset form to defaultValue |
164
164
 
165
- ### register 반환 객체
165
+ ### register Return Object
166
166
 
167
167
  ```typescript
168
168
  register(fieldName) // Returns:
169
169
  {
170
- value: any, // 현재 필드
171
- onValueChange: (value: any) => void, // 변경 핸들러
172
- error?: { content: string } // 에러 객체 (있는 경우)
170
+ value: any, // Current field value
171
+ onValueChange: (value: any) => void, // Value change handler
172
+ error?: { content: string } // Error object (if present)
173
173
  }
174
174
  ```
175
175
 
176
- ### 기본 사용법
176
+ ### Basic Usage
177
177
 
178
178
  ```tsx
179
179
  import { useTypeForm } from "@sonamu-kit/react-components/lib";
@@ -191,7 +191,7 @@ function RegisterForm() {
191
191
  await UserService.save([form]);
192
192
  });
193
193
 
194
- // 방법 1: spread operator (권장)
194
+ // Method 1: spread operator (recommended)
195
195
  const emailProps = register("email");
196
196
 
197
197
  return (
@@ -201,21 +201,21 @@ function RegisterForm() {
201
201
  <span className="error">{emailProps.error.content}</span>
202
202
  )}
203
203
 
204
- {/* 방법 2: 인라인 (짧은 경우) */}
204
+ {/* Method 2: inline (for short cases) */}
205
205
  <Input {...register("username")} />
206
206
  {register("username").error && (
207
207
  <span className="error">{register("username").error.content}</span>
208
208
  )}
209
209
 
210
- <button onClick={handleSubmit}>등록</button>
210
+ <button onClick={handleSubmit}>Register</button>
211
211
  </form>
212
212
  );
213
213
  }
214
214
  ```
215
215
 
216
- ### IMPORTANT: react-components UI 컴포넌트 사용
216
+ ### IMPORTANT: react-components UI Component Usage
217
217
 
218
- react-components 모든 UI 컴포넌트는 `value/onValueChange` 패턴을 따릅니다:
218
+ All UI components in react-components follow the `value/onValueChange` pattern:
219
219
 
220
220
  ```tsx
221
221
  import { Input, Checkbox, Select, Textarea } from "@sonamu-kit/react-components/components";
@@ -229,41 +229,41 @@ import { Input, Checkbox, Select, Textarea } from "@sonamu-kit/react-components/
229
229
  // Checkbox (boolean)
230
230
  <Checkbox {...register("agreed")} />
231
231
 
232
- // Select (items prop 사용)
232
+ // Select (using items prop)
233
233
  <Select
234
234
  {...register("status")}
235
235
  items={[
236
- { value: "active", label: "활성" },
237
- { value: "inactive", label: "비활성" }
236
+ { value: "active", label: "Active" },
237
+ { value: "inactive", label: "Inactive" }
238
238
  ]}
239
- placeholder="상태 선택"
239
+ placeholder="Select status"
240
240
  />
241
241
 
242
- // Select 간단한 형태 (string[] | number[])
242
+ // Select simple form (string[] | number[])
243
243
  <Select
244
244
  {...register("priority")}
245
245
  items={["high", "medium", "low"]}
246
- placeholder="우선순위"
246
+ placeholder="Priority"
247
247
  />
248
248
  ```
249
249
 
250
- **Select 컴포넌트 주요 props:**
250
+ **Select component key props:**
251
251
 
252
- - `items`: 선택 항목 배열 (`V[]` 또는 `{ value: V, label?: ReactNode, disabled?: boolean }[]`)
253
- - `placeholder`: 선택 표시 텍스트
254
- - `clearable`: X 버튼으로 선택 해제 가능 여부
255
- - `renderItem`: 커스텀 렌더링 함수
252
+ - `items`: Array of selectable items (`V[]` or `{ value: V, label?: ReactNode, disabled?: boolean }[]`)
253
+ - `placeholder`: Text shown before selection
254
+ - `clearable`: Whether the X button can deselect
255
+ - `renderItem`: Custom render function
256
256
 
257
257
  ### IMPORTANT: Form Required Field Initial Values
258
258
 
259
- SaveParams에 required 정의된 필드는 form 초기값에 **반드시 포함**:
259
+ Fields defined as required in SaveParams **must be included** in the form initial values:
260
260
 
261
- | 타입 | 초기값 |
261
+ | Type | Initial Value |
262
262
  | ----------------- | ---------------------- |
263
263
  | string (required) | `""` |
264
264
  | number (required) | `0` |
265
265
  | Date (required) | `new Date()` |
266
- | enum (required) | 기본값 (예: `"draft"`) |
266
+ | enum (required) | Default value (e.g. `"draft"`) |
267
267
  | FK (required) | `0` |
268
268
  | nullable | `null` |
269
269
 
@@ -280,9 +280,9 @@ const { form, setForm, register } = useTypeForm(TaskSaveParams, {
280
280
 
281
281
  ### IMPORTANT: Accessing Relation Objects When Loading Data
282
282
 
283
- 스캐폴딩된 form `row.collection?.id` 같은 relation 객체에 접근하면, subset A에 해당 relation이 포함되어 있어야 합니다.
283
+ If a scaffolded form accesses relation objects like `row.collection?.id`, that relation must be included in subset A.
284
284
 
285
- **오류**: `Property 'collection' does not exist on type` → entity.json subset A에 `"collection.id"` 추가
285
+ **Error**: `Property 'collection' does not exist on type` → Add `"collection.id"` to subset A in entity.json
286
286
 
287
287
  ```json
288
288
  // question.entity.json > subsets > A
@@ -296,25 +296,25 @@ const { form, setForm, register } = useTypeForm(TaskSaveParams, {
296
296
  ]
297
297
  ```
298
298
 
299
- **대안**: FK 이미 row에 있으면 relation 접근 없이 `...row`만으로 충분 (subset 수정 불필요)
299
+ **Alternative**: If the FK is already on `row`, using `...row` alone is sufficient without accessing the relation (no subset modification needed)
300
300
 
301
301
  ### IMPORTANT: SD() Translation Key for FK Fields
302
302
 
303
- 스캐폴딩된 form은 `SD("entity.Task.institution_id")`를 사용하지만, `sd.generated.ts`에는 `_id` 없는 키만 생성됩니다.
303
+ Scaffolded forms use `SD("entity.Task.institution_id")`, but `sd.generated.ts` only generates keys without `_id`.
304
304
 
305
- **해결**: `ko.ts`에 `_id` 수동 추가
305
+ **Fix**: Manually add the `_id` key to `ko.ts`
306
306
 
307
307
  ```typescript
308
308
  // packages/api/src/i18n/ko.ts
309
- "entity.Task.institution_id": "소속기관",
310
- "entity.Question.collection_id": "소속 모음집",
309
+ "entity.Task.institution_id": "Institution",
310
+ "entity.Question.collection_id": "Collection",
311
311
  ```
312
312
 
313
- `ko.ts`는 api → web으로 복사되므로 번만 추가하면 됨.
313
+ Since `ko.ts` is copied from api → web, you only need to add it once.
314
314
 
315
315
  ## useListParams
316
316
 
317
- URL 쿼리 파라미터와 동기화되는 리스트 파라미터 관리 (페이지네이션, 필터링)
317
+ List parameter management synchronized with URL query parameters (pagination, filtering)
318
318
 
319
319
  ```typescript
320
320
  import { useListParams } from "@sonamu-kit/react-components/lib";
@@ -337,34 +337,34 @@ function UserListPage() {
337
337
 
338
338
  return (
339
339
  <div>
340
- {/* 검색 (변경 page=1 리셋) */}
341
- <Input {...register("search")} placeholder="검색" />
340
+ {/* Search (resets page=1 on change) */}
341
+ <Input {...register("search")} placeholder="Search" />
342
342
 
343
- {/* 필터 (변경 page=1 리셋) */}
343
+ {/* Filter (resets page=1 on change) */}
344
344
  <Select {...register("status")} items={["active", "inactive"]} />
345
345
 
346
- {/* 페이지네이션 (page 변경) */}
346
+ {/* Pagination (changes page only) */}
347
347
  <button onClick={() => setListParams({ ...listParams, page: listParams.page - 1 })}>
348
- 이전
348
+ Previous
349
349
  </button>
350
350
  <span>Page {listParams.page}</span>
351
351
  <button onClick={() => setListParams({ ...listParams, page: listParams.page + 1 })}>
352
- 다음
352
+ Next
353
353
  </button>
354
354
  </div>
355
355
  );
356
356
  }
357
357
  ```
358
358
 
359
- **핵심:**
359
+ **Key points:**
360
360
 
361
- - URL과 자동 동기화 (`?page=2&status=active`)
362
- - `register`는 page 필드 변경 자동으로 page 1로 리셋
363
- - Zod 스키마로 타입 안전성 보장
361
+ - Automatically syncs with URL (`?page=2&status=active`)
362
+ - `register` automatically resets page to 1 when any field other than page changes
363
+ - Type safety guaranteed by Zod schema
364
364
 
365
365
  ## useSelection
366
366
 
367
- 체크박스 다중 선택 관리 (Shift 범위 선택 지원)
367
+ Checkbox multi-selection management (supports Shift-click range selection)
368
368
 
369
369
  ```typescript
370
370
  import { useSelection } from "@sonamu-kit/react-components/lib";
@@ -393,10 +393,10 @@ function UserListPage() {
393
393
  <Checkbox
394
394
  value={isAllSelected}
395
395
  onValueChange={isAllSelected ? deselectAll : selectAll}
396
- label="전체 선택"
396
+ label="Select all"
397
397
  />
398
398
  <button onClick={handleDelete} disabled={selectedKeys.length === 0}>
399
- 선택 삭제 ({selectedKeys.length})
399
+ Delete selected ({selectedKeys.length})
400
400
  </button>
401
401
 
402
402
  {data?.rows.map((user, index) => (
@@ -413,19 +413,19 @@ function UserListPage() {
413
413
  }
414
414
  ```
415
415
 
416
- **핵심:**
416
+ **Key points:**
417
417
 
418
- - Shift + 클릭으로 범위 선택
419
- - `selectedKeys`: 현재 선택된 배열
420
- - `isAllSelected`: 전체 선택 여부
418
+ - Range selection with Shift + click
419
+ - `selectedKeys`: Array of currently selected keys
420
+ - `isAllSelected`: Whether all items are selected
421
421
 
422
422
  ## IdAsyncSelect
423
423
 
424
- Entity의 레코드를 비동기로 검색하여 선택하는 컴포넌트입니다. Entity Primary Key 타입에 따라 제네릭 타입을 명시해야 합니다.
424
+ Component for asynchronously searching and selecting Entity records. The generic type must be specified according to the Entity's Primary Key type.
425
425
 
426
- ### 기본 사용법
426
+ ### Basic Usage
427
427
 
428
- IdAsyncSelect 일반적으로 Entity 래퍼 컴포넌트로 사용합니다:
428
+ IdAsyncSelect is typically used as a per-Entity wrapper component:
429
429
 
430
430
  ```typescript
431
431
  import { IdAsyncSelect } from "@sonamu-kit/react-components/components";
@@ -454,7 +454,7 @@ export function UserIdAsyncSelect<T extends UserSubsetKey>({
454
454
  baseListParams,
455
455
  displayField = "name",
456
456
  valueField = "id",
457
- placeholder = "사용자",
457
+ placeholder = "User",
458
458
  clearable,
459
459
  disabled,
460
460
  className,
@@ -479,25 +479,25 @@ export function UserIdAsyncSelect<T extends UserSubsetKey>({
479
479
  }
480
480
  ```
481
481
 
482
- **주요 Props:**
482
+ **Key Props:**
483
483
 
484
- - `config`: 자동 생성된 AsyncIdConfig (EntityAsyncIdConfig 형태)
485
- - `subset`: 조회할 Subset
486
- - `baseListParams`: 목록 조회 필터 파라미터
487
- - `displayField`: 화면에 표시할 필드명 (기본값: Entity에 따라 다름)
488
- - `valueField`: value로 사용할 필드명 (기본값: "id")
489
- - `multiple`: 다중 선택 여부
490
- - `value`: 현재 선택된 (PK 타입에 따라 number 또는 string)
491
- - `onValueChange`: 변경 핸들러
484
+ - `config`: Auto-generated AsyncIdConfig (EntityAsyncIdConfig form)
485
+ - `subset`: Subset key to query
486
+ - `baseListParams`: List filter parameters
487
+ - `displayField`: Field name to display (default varies by Entity)
488
+ - `valueField`: Field name used as value (default: "id")
489
+ - `multiple`: Whether multi-selection is enabled
490
+ - `value`: Currently selected value (number or string depending on PK type)
491
+ - `onValueChange`: Value change handler
492
492
 
493
- ### Cascade Dropdown 패턴 (계층 선택)
493
+ ### Cascade Dropdown Pattern (Hierarchical Selection)
494
494
 
495
- 부서 과소 연구실처럼 상위 선택에 따라 하위 목록이 변해야 하는 경우, `baseListParams`를 동적으로 전달하면 된다.
495
+ When lower-level lists should change based on higher-level selection (e.g. Department Division → Lab), pass `baseListParams` dynamically.
496
496
 
497
- **핵심 동작**: `baseListParams` prop 변경되면 `IdAsyncSelect` 내부 React Query가 파라미터로 자동 재조회한다. (v0.2.5+에서 수정된 버그 - 이전 버전은 초기값만 사용하고 변경을 반영하지 않았음)
497
+ **Key behavior**: When the `baseListParams` prop changes, the React Query inside `IdAsyncSelect` automatically re-fetches with the new parameters. (Bug fixed in v0.2.5+ previous versions only used the initial value and did not reflect changes)
498
498
 
499
499
  ```tsx
500
- // 예시: 부서 과소연구실 3단계 cascade
500
+ // Example: 3-level cascade DepartmentDivision Lab
501
501
  function UserForm() {
502
502
  const { form, register, setForm } = useTypeForm(UserSaveParams, {
503
503
  dept_id: null,
@@ -507,29 +507,29 @@ function UserForm() {
507
507
 
508
508
  return (
509
509
  <form>
510
- {/* 1단계: 부서 선택 (전체 목록 → preload 또는 기본 IdAsyncSelect) */}
510
+ {/* Level 1: Department selection (full list → preload or default IdAsyncSelect) */}
511
511
  <DepartmentIdAsyncSelect
512
512
  subset="A"
513
513
  {...register("dept_id")}
514
514
  onValueChange={(v) => {
515
- // 부서 변경 하위 초기화
515
+ // Reset lower values when department changes
516
516
  setForm((prev) => ({ ...prev, dept_id: v ?? null, division_id: null, lab_id: null }));
517
517
  }}
518
518
  />
519
519
 
520
- {/* 2단계: 과소 선택 (선택된 부서의 과소만 조회) */}
520
+ {/* Level 2: Division selection (only divisions in selected department) */}
521
521
  <DivisionIdAsyncSelect
522
522
  subset="A"
523
523
  baseListParams={form.dept_id ? { department_id: form.dept_id } : undefined}
524
524
  disabled={!form.dept_id}
525
525
  {...register("division_id")}
526
526
  onValueChange={(v) => {
527
- // 과소 변경 연구실 초기화
527
+ // Reset lab when division changes
528
528
  setForm((prev) => ({ ...prev, division_id: v ?? null, lab_id: null }));
529
529
  }}
530
530
  />
531
531
 
532
- {/* 3단계: 연구실 선택 (선택된 과소의 연구실만 조회) */}
532
+ {/* Level 3: Lab selection (only labs in selected division) */}
533
533
  <LabIdAsyncSelect
534
534
  subset="A"
535
535
  baseListParams={form.division_id ? { division_id: form.division_id } : undefined}
@@ -541,41 +541,41 @@ function UserForm() {
541
541
  }
542
542
  ```
543
543
 
544
- **주의사항**:
545
- - 상위가 변경될 하위 값을 명시적으로 `null`로 초기화해야 한다. IdAsyncSelect 자동으로 초기화하지 않는다.
546
- - `disabled` prop으로 상위가 선택되지 않은 경우 하위를 비활성화하는 것이 UX에 좋다.
547
- - `baseListParams`가 `undefined`이면 IdAsyncSelect enabled=false 상태로 조회하지 않는다.
544
+ **Notes**:
545
+ - You must explicitly reset lower values to `null` when a higher-level value changes. IdAsyncSelect does not reset automatically.
546
+ - Using the `disabled` prop to disable lower levels when the parent is not selected improves UX.
547
+ - If `baseListParams` is `undefined`, IdAsyncSelect stays in enabled=false state and does not fetch.
548
548
 
549
- **Spec에 명시할 항목** (cascade가 있는 경우 spec.json acceptanceCriteria에 추가 권장):
549
+ **Items to specify in Spec** (recommended to add to acceptanceCriteria in spec.json when cascade is present):
550
550
  ```json
551
551
  "acceptanceCriteria": [
552
- "부서 선택 해당 부서의 과소만 드롭다운으로 조회된다",
553
- "과소 선택 해당 과소의 연구실만 드롭다운으로 조회된다",
554
- "부서 변경 하위 과소/연구실 선택이 초기화된다"
552
+ "When a department is selected, only divisions belonging to that department appear in the dropdown",
553
+ "When a division is selected, only labs belonging to that division appear in the dropdown",
554
+ "When the department changes, the lower division/lab selections are reset"
555
555
  ]
556
556
  ```
557
557
 
558
558
  ### IMPORTANT: String Primary Key Support
559
559
 
560
- 대부분 Entity는 Number PK (`IdAsyncSelect<number>`)이지만, better-auth 관련 Entity는 String PK를 사용합니다.
560
+ Most Entities use Number PK (`IdAsyncSelect<number>`), but better-auth related Entities use String PK.
561
561
 
562
- **String PK Entity**: User, Account, Session, Verification
562
+ **String PK Entities**: User, Account, Session, Verification
563
563
 
564
- **변경 포인트** (scaffolding 수동 수정 필요):
564
+ **Points to change** (manual modification required after scaffolding):
565
565
 
566
566
  ```typescript
567
- // Number PK (기본)
567
+ // Number PK (default)
568
568
  value?: number | number[] | null;
569
569
  onValueChange?: (value: number | number[] | undefined) => void;
570
570
  <IdAsyncSelect<number> config={PostAsyncIdConfig} ... />
571
571
 
572
- // String PK (User, Account ) — 아래 3 모두 string으로 변경
572
+ // String PK (User, Account, etc.) — change all 3 places to string
573
573
  value?: string | string[] | null;
574
574
  onValueChange?: (value: string | string[] | undefined) => void;
575
575
  <IdAsyncSelect<string> config={AccountAsyncIdConfig} ... />
576
576
  ```
577
577
 
578
- ### 폼에서 사용
578
+ ### Usage in Forms
579
579
 
580
580
  ```tsx
581
581
  function PostForm() {
@@ -600,7 +600,7 @@ function PostForm() {
600
600
 
601
601
  ## FileInput
602
602
 
603
- 파일 업로드 컴포넌트 (이미지/일반 파일, eager/lazy 모드)
603
+ File upload component (image/general files, eager/lazy modes)
604
604
 
605
605
  ```typescript
606
606
  import { FileInput } from "@sonamu-kit/react-components/components";
@@ -614,30 +614,30 @@ function ProfileForm() {
614
614
 
615
615
  return (
616
616
  <form>
617
- {/* 단일 이미지 - eager 업로드 */}
617
+ {/* Single image - eager upload */}
618
618
  <FileInput
619
619
  {...register("avatar")}
620
620
  uploadMode="eager"
621
621
  viewMode="image"
622
- placeholder="프로필 이미지"
622
+ placeholder="Profile image"
623
623
  accept="image/*"
624
624
  previewSize="md"
625
625
  />
626
626
 
627
- {/* 다중 파일 - lazy 업로드 */}
627
+ {/* Multiple files - lazy upload */}
628
628
  <FileInput
629
629
  {...register("documents")}
630
630
  uploadMode="lazy"
631
631
  viewMode="file"
632
632
  multiple
633
633
  maxFiles={5}
634
- placeholder="문서 첨부"
634
+ placeholder="Attach documents"
635
635
  />
636
636
 
637
637
  <button onClick={submit(async (form) => {
638
- // lazy 모드: submit 자동 업로드
638
+ // lazy mode: auto-uploads on submit
639
639
  await ProfileService.save([form]);
640
- })}>저장</button>
640
+ })}>Save</button>
641
641
  </form>
642
642
  );
643
643
  }
@@ -645,18 +645,18 @@ function ProfileForm() {
645
645
 
646
646
  **Props:**
647
647
 
648
- - `uploadMode`: `"eager"` (즉시 업로드) | `"lazy"` (submit 업로드)
649
- - `viewMode`: `"image"` (이미지 프리뷰) | `"file"` (파일명)
650
- - `multiple`: 다중 파일 선택 여부
651
- - `maxFiles`: 최대 파일 개수
648
+ - `uploadMode`: `"eager"` (upload immediately) | `"lazy"` (upload on submit)
649
+ - `viewMode`: `"image"` (image preview) | `"file"` (filename)
650
+ - `multiple`: Whether multiple files can be selected
651
+ - `maxFiles`: Maximum number of files
652
652
  - `previewSize`: `"sm" | "md" | "lg" | "xl"`
653
- - `clearable`: X 버튼으로 제거 가능
653
+ - `clearable`: Whether the X button can remove the file
654
654
 
655
- **IMPORTANT**: SonamuProvider에 uploader 함수 필수 설정 (아래 참조)
655
+ **IMPORTANT**: uploader function must be configured in SonamuProvider (see below)
656
656
 
657
- ## Select (다중 선택 모드)
657
+ ## Select (Multi-select Mode)
658
658
 
659
- `Select` 컴포넌트에 `multiple: true`를 설정하면 다중 선택 모드로 동작합니다.
659
+ Setting `multiple: true` on the `Select` component enables multi-select mode.
660
660
 
661
661
  ```typescript
662
662
  import { Select } from "@sonamu-kit/react-components/components";
@@ -678,31 +678,31 @@ function TagForm() {
678
678
  {...register("tag_ids")}
679
679
  items={items}
680
680
  multiple
681
- placeholder="태그 선택"
681
+ placeholder="Select tags"
682
682
  />
683
683
  );
684
684
  }
685
685
  ```
686
686
 
687
- **다중 선택 전용 Props:**
687
+ **Multi-select specific Props:**
688
688
 
689
- - `multiple`: `true` (다중 선택 활성화)
690
- - `maxCount`: 표시할 최대 배지 개수
691
- - `hideSelectAll`: 전체 선택 버튼 숨기기
692
- - `searchable`: 검색 입력 활성화
689
+ - `multiple`: `true` (enables multi-select)
690
+ - `maxCount`: Maximum number of badges to display
691
+ - `hideSelectAll`: Hide the select all button
692
+ - `searchable`: Enable search input
693
693
 
694
- **공통 Props:**
694
+ **Common Props:**
695
695
 
696
- - `items`: `SelectItemDef[]` (값만 또는 `{ value, label, disabled }` 형태)
697
- - `placeholder`: 선택 표시 텍스트
698
- - `clearable`: X 버튼으로 전체 해제
699
- - `disabled`: 비활성화
700
- - `renderItem`: 커스텀 렌더링 함수
701
- - `async`: `true` 설정 `onSearch` 콜백으로 비동기 검색 지원
696
+ - `items`: `SelectItemDef[]` (values only or `{ value, label, disabled }` form)
697
+ - `placeholder`: Text shown before selection
698
+ - `clearable`: X button to deselect all
699
+ - `disabled`: Disable the component
700
+ - `renderItem`: Custom render function
701
+ - `async`: When set to `true`, supports async search via `onSearch` callback
702
702
 
703
703
  ## EnumSelect
704
704
 
705
- Zod enum 연동된 Select (라벨 매핑)
705
+ Select integrated with Zod enum (label mapping)
706
706
 
707
707
  ```typescript
708
708
  import { EnumSelect } from "@sonamu-kit/react-components/components";
@@ -711,9 +711,9 @@ import { z } from "zod";
711
711
  const StatusEnum = z.enum(["draft", "published", "archived"]);
712
712
 
713
713
  const statusLabels = {
714
- draft: "초안",
715
- published: "발행됨",
716
- archived: "보관됨",
714
+ draft: "Draft",
715
+ published: "Published",
716
+ archived: "Archived",
717
717
  } as const;
718
718
 
719
719
  function PostForm() {
@@ -726,30 +726,30 @@ function PostForm() {
726
726
  {...register("status")}
727
727
  enum={StatusEnum}
728
728
  labels={statusLabels}
729
- placeholder="상태 선택"
729
+ placeholder="Select status"
730
730
  clearable
731
731
  />
732
732
  );
733
733
  }
734
734
  ```
735
735
 
736
- **핵심:**
736
+ **Key points:**
737
737
 
738
- - Zod enum 타입 안전성
739
- - labels 객체로 표시명 매핑
740
- - enum.options 자동으로 items로 변환
738
+ - Zod enum type safety
739
+ - Display name mapping via labels object
740
+ - Automatically converts enum.options to items
741
741
 
742
742
  ## SonamuProvider
743
743
 
744
- react-components 전체에서 사용하는 전역 설정
744
+ Global configuration used across react-components
745
745
 
746
746
  ```typescript
747
- // App.tsx 또는 루트 컴포넌트
747
+ // App.tsx or root component
748
748
  import { SonamuProvider } from "@sonamu-kit/react-components/contexts";
749
749
  import type { SonamuFile } from "@sonamu-kit/react-components/contexts";
750
750
 
751
751
  function App() {
752
- // 파일 업로더 함수 (FileInput, useTypeForm에서 사용)
752
+ // File uploader function (used by FileInput, useTypeForm)
753
753
  const uploader = async (files: File[]): Promise<SonamuFile[]> => {
754
754
  const formData = new FormData();
755
755
  files.forEach(file => formData.append("files", file));
@@ -762,7 +762,7 @@ function App() {
762
762
  return response.json();
763
763
  };
764
764
 
765
- // 인증 상태 (옵션)
765
+ // Authentication state (optional)
766
766
  const auth = {
767
767
  user: currentUser,
768
768
  loading: isLoading,
@@ -771,7 +771,7 @@ function App() {
771
771
  refetch: async () => { /* ... */ },
772
772
  };
773
773
 
774
- // 다국어 함수 (옵션)
774
+ // Internationalization function (optional)
775
775
  const SD = (key: string) => dictionary[key] ?? key;
776
776
 
777
777
  return (
@@ -782,13 +782,13 @@ function App() {
782
782
  }
783
783
  ```
784
784
 
785
- **필수 Props:**
785
+ **Required Props:**
786
786
 
787
- - `uploader`: `(files: File[]) => Promise<SonamuFile[]>` - FileInput에서 사용
788
- - `auth`: 인증 상태 함수 (옵션)
789
- - `SD`: 다국어 함수 (옵션)
787
+ - `uploader`: `(files: File[]) => Promise<SonamuFile[]>` - Used by FileInput
788
+ - `auth`: Authentication state and functions (optional)
789
+ - `SD`: Internationalization function (optional)
790
790
 
791
- ## 유틸리티 함수
791
+ ## Utility Functions
792
792
 
793
793
  ```typescript
794
794
  import {
@@ -800,27 +800,27 @@ import {
800
800
  sqlDateToDateString,
801
801
  } from "@sonamu-kit/react-components/lib";
802
802
 
803
- // 날짜 포매팅
803
+ // Date formatting
804
804
  dateF(new Date()); // "2024-01-15"
805
805
  dateF("2024-01-15T10:30:00"); // "2024-01-15"
806
806
  datetimeF(new Date()); // "2024-01-15 10:30:00"
807
807
 
808
- // 숫자 포매팅
808
+ // Number formatting
809
809
  numF(1234567); // "1,234,567"
810
810
 
811
- // 조건부 hidden 클래스
811
+ // Conditional hidden class
812
812
  <div className={hidden(isHidden)}>...</div>
813
813
 
814
814
  // SQL date → date string
815
815
  sqlDateToDateString("2024-01-15T10:30:00.000Z"); // "2024-01-15"
816
816
 
817
- // 배열 변환
817
+ // Convert to array
818
818
  arrayableToArray("single"); // ["single"]
819
819
  arrayableToArray(["a", "b"]); // ["a", "b"]
820
820
  arrayableToArray(undefined); // []
821
821
  ```
822
822
 
823
- ## 에러 처리
823
+ ## Error Handling
824
824
 
825
825
  ```typescript
826
826
  import { isSonamuError } from "@/lib/sonamu.shared";
@@ -857,9 +857,9 @@ registerSSR({
857
857
  });
858
858
  ```
859
859
 
860
- ## 프로젝트 초기 설정
860
+ ## Initial Project Setup
861
861
 
862
- **→ `create-sonamu.md` "프로젝트명 변경" 섹션 참조** (index.html, \_\_root.tsx, index.tsx, Sidebar.tsx 4개 파일 변경)
862
+ **→ See the "Project Name Change" section in `create-sonamu.md`** (change 4 files: index.html, \_\_root.tsx, index.tsx, Sidebar.tsx)
863
863
 
864
864
  ## Rules
865
865
 
@@ -869,9 +869,9 @@ registerSSR({
869
869
 
870
870
  ---
871
871
 
872
- ## 전체 컴포넌트 구현 예시
872
+ ## Full Component Implementation Examples
873
873
 
874
- ### 목록 페이지
874
+ ### List Page
875
875
 
876
876
  ```typescript
877
877
  function ConsultationListPage() {
@@ -880,17 +880,17 @@ function ConsultationListPage() {
880
880
 
881
881
  return (
882
882
  <div>
883
- {/* useSelection으로 선택 관리 */}
883
+ {/* Manage selection with useSelection */}
884
884
  {data?.rows.map((row) => (
885
885
  <div key={row.id}>{row.title} - {row.status}</div>
886
886
  ))}
887
- {/* 페이지네이션: params.page 조작 */}
887
+ {/* Pagination: manipulate params.page */}
888
888
  </div>
889
889
  );
890
890
  }
891
891
  ```
892
892
 
893
- ### 편집 페이지
893
+ ### Edit Page
894
894
 
895
895
  ```typescript
896
896
  function ConsultationFormPage() {
@@ -899,7 +899,7 @@ function ConsultationFormPage() {
899
899
  title: "", content: "", status: "pending", user_id: 0,
900
900
  });
901
901
 
902
- // 수정 모드: 데이터 로드
902
+ // Edit mode: load data
903
903
  useEffect(() => {
904
904
  if (id) ConsultationService.getConsultation("A", Number(id)).then((row) => setForm((prev) => ({ ...prev, ...row })));
905
905
  }, [id]);
@@ -914,18 +914,18 @@ function ConsultationFormPage() {
914
914
  <form>
915
915
  <Input {...register("title")} />
916
916
  <Textarea {...register("content")} />
917
- <Select {...register("status")} items={[{value:"pending",label:"대기중"},{value:"completed",label:"완료"}]} />
918
- <button onClick={handleSubmit} disabled={saveMutation.isPending}>저장</button>
917
+ <Select {...register("status")} items={[{value:"pending",label:"Pending"},{value:"completed",label:"Completed"}]} />
918
+ <button onClick={handleSubmit} disabled={saveMutation.isPending}>Save</button>
919
919
  </form>
920
920
  );
921
921
  }
922
922
  ```
923
923
 
924
- ### 캐시 무효화
924
+ ### Cache Invalidation
925
925
 
926
926
  ```typescript
927
927
  const queryClient = useQueryClient();
928
- await ConsultationService.changeStatus(id, newStatus, "상태 변경");
928
+ await ConsultationService.changeStatus(id, newStatus, "Status change");
929
929
  queryClient.invalidateQueries({
930
930
  queryKey: ["Consultation", "findById", "A", id],
931
931
  });