jun-claude-code 0.6.2 → 0.6.4

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.
package/dist/update.js ADDED
@@ -0,0 +1,433 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.computeUpdateStatus = computeUpdateStatus;
40
+ exports.updateClaudeFiles = updateClaudeFiles;
41
+ const fs = __importStar(require("fs"));
42
+ const path = __importStar(require("path"));
43
+ const chalk_1 = __importDefault(require("chalk"));
44
+ const copy_1 = require("./copy");
45
+ const metadata_1 = require("./metadata");
46
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
47
+ const { MultiSelect } = require('enquirer');
48
+ /**
49
+ * Compute 3-way update status for a file.
50
+ * Compares base (from metadata), current (on disk), and new (from template).
51
+ */
52
+ function computeUpdateStatus(file, sourceDir, destDir, metadata) {
53
+ const sourcePath = path.join(sourceDir, file);
54
+ const destPath = path.join(destDir, file);
55
+ const newHash = (0, copy_1.getFileHash)(sourcePath);
56
+ const baseHash = metadata?.files[file]?.hash ?? null;
57
+ const currentExists = fs.existsSync(destPath);
58
+ const currentHash = currentExists ? (0, copy_1.getFileHash)(destPath) : null;
59
+ // No base metadata for this file
60
+ if (!baseHash) {
61
+ if (!currentExists)
62
+ return 'new-file';
63
+ if (currentHash === newHash)
64
+ return 'unchanged';
65
+ return 'user-modified';
66
+ }
67
+ // Base exists but user deleted the file
68
+ if (!currentExists)
69
+ return 'new-file';
70
+ // All three hashes available
71
+ if (baseHash === currentHash && currentHash === newHash)
72
+ return 'unchanged';
73
+ if (baseHash === currentHash)
74
+ return 'update-available';
75
+ if (baseHash === newHash)
76
+ return 'user-modified';
77
+ return 'conflict';
78
+ }
79
+ /**
80
+ * Fetch latest version from npm registry
81
+ */
82
+ async function fetchLatestVersion(packageName) {
83
+ try {
84
+ const response = await fetch(`https://registry.npmjs.org/${packageName}/latest`);
85
+ if (!response.ok)
86
+ return null;
87
+ const data = (await response.json());
88
+ return data.version ?? null;
89
+ }
90
+ catch {
91
+ return null;
92
+ }
93
+ }
94
+ /**
95
+ * Format update status for MultiSelect hint
96
+ */
97
+ function updateStatusLabel(status) {
98
+ switch (status) {
99
+ case 'update-available': return chalk_1.default.green('update');
100
+ case 'new-file': return chalk_1.default.green('new');
101
+ case 'user-modified': return chalk_1.default.yellow('customized');
102
+ case 'conflict': return chalk_1.default.red('conflict');
103
+ case 'unchanged': return chalk_1.default.gray('unchanged');
104
+ case 'removed-upstream': return chalk_1.default.gray('removed upstream');
105
+ }
106
+ }
107
+ /**
108
+ * Format update status bracket for log output
109
+ */
110
+ function updateStatusBracket(status) {
111
+ switch (status) {
112
+ case 'update-available': return chalk_1.default.green('[update]');
113
+ case 'new-file': return chalk_1.default.green('[new]');
114
+ case 'user-modified': return chalk_1.default.yellow('[skip]');
115
+ case 'conflict': return chalk_1.default.red('[conflict]');
116
+ case 'unchanged': return chalk_1.default.gray('[unchanged]');
117
+ case 'removed-upstream': return chalk_1.default.gray('[removed]');
118
+ }
119
+ }
120
+ /**
121
+ * Get aggregate update status for a skill directory
122
+ */
123
+ function getSkillUpdateStatus(skillName, allStatuses) {
124
+ const skillFiles = allStatuses.filter(f => f.file.startsWith(`skills/${skillName}/`));
125
+ if (skillFiles.length === 0)
126
+ return 'unchanged';
127
+ if (skillFiles.some(f => f.status === 'conflict'))
128
+ return 'conflict';
129
+ if (skillFiles.some(f => f.status === 'update-available'))
130
+ return 'update-available';
131
+ if (skillFiles.some(f => f.status === 'new-file'))
132
+ return 'new-file';
133
+ if (skillFiles.some(f => f.status === 'user-modified'))
134
+ return 'user-modified';
135
+ return 'unchanged';
136
+ }
137
+ /**
138
+ * MultiSelect prompt for update items
139
+ */
140
+ async function selectUpdateItems(category, items) {
141
+ if (items.length === 0)
142
+ return [];
143
+ const choices = items.map(item => ({
144
+ name: item.file,
145
+ message: item.file.startsWith('agents/')
146
+ ? path.basename(item.file, '.md')
147
+ : item.file,
148
+ hint: updateStatusLabel(item.status) +
149
+ ((item.status === 'user-modified' || item.status === 'conflict')
150
+ ? ' \u2014 will overwrite your changes' : ''),
151
+ enabled: item.status === 'update-available' || item.status === 'new-file',
152
+ }));
153
+ const prompt = new MultiSelect({
154
+ name: category,
155
+ message: `Select ${category} to update`,
156
+ choices,
157
+ hint: '(\u2191\u2193 navigate, <space> toggle, <a> select all, <enter> confirm)',
158
+ });
159
+ try {
160
+ return await prompt.run();
161
+ }
162
+ catch {
163
+ console.log(chalk_1.default.yellow('\nUpdate cancelled.'));
164
+ process.exit(0);
165
+ }
166
+ }
167
+ /**
168
+ * MultiSelect prompt for skill sub-files across multiple skills
169
+ */
170
+ async function selectSkillUpdateFiles(skills) {
171
+ const choices = [];
172
+ for (const { skillName, files } of skills) {
173
+ const actionable = files.filter(f => f.status !== 'unchanged');
174
+ if (actionable.length === 0)
175
+ continue;
176
+ choices.push({ role: 'separator', message: chalk_1.default.cyan(`\u2500\u2500 ${skillName} \u2500\u2500`) });
177
+ for (const item of actionable) {
178
+ choices.push({
179
+ name: item.file,
180
+ message: ` ${path.basename(item.file)}`,
181
+ hint: updateStatusLabel(item.status) +
182
+ ((item.status === 'user-modified' || item.status === 'conflict')
183
+ ? ' \u2014 will overwrite' : ''),
184
+ enabled: item.status === 'update-available' || item.status === 'new-file',
185
+ });
186
+ }
187
+ }
188
+ if (choices.filter(c => !c.role).length === 0)
189
+ return [];
190
+ const prompt = new MultiSelect({
191
+ name: 'skill-files',
192
+ message: 'Select skill files to update',
193
+ choices,
194
+ hint: '(\u2191\u2193 navigate, <space> toggle, <a> select all, <enter> confirm)',
195
+ });
196
+ try {
197
+ return await prompt.run();
198
+ }
199
+ catch {
200
+ console.log(chalk_1.default.yellow('\nUpdate cancelled.'));
201
+ process.exit(0);
202
+ }
203
+ }
204
+ /**
205
+ * Update installed templates while preserving user customizations
206
+ */
207
+ async function updateClaudeFiles(options = {}) {
208
+ const { dryRun = false, force = false, project = false } = options;
209
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
210
+ const currentVersion = require('../package.json').version;
211
+ const sourceDir = (0, copy_1.getSourceGlobalDir)();
212
+ const destDir = project ? path.join(process.cwd(), '.claude') : (0, copy_1.getDestClaudeDir)();
213
+ const targetLabel = project ? 'project' : 'global';
214
+ // Step 1: Version info
215
+ const metadata = (0, metadata_1.loadMetadata)(destDir);
216
+ const latestVersion = await fetchLatestVersion('jun-claude-code');
217
+ console.log(chalk_1.default.cyan('Version Info:'));
218
+ console.log(` Package version: ${chalk_1.default.bold(currentVersion)}`);
219
+ if (metadata?.version) {
220
+ console.log(` Installed version: ${chalk_1.default.bold(metadata.version)}`);
221
+ }
222
+ else {
223
+ console.log(` Installed version: ${chalk_1.default.gray('(no metadata found)')}`);
224
+ }
225
+ if (latestVersion && latestVersion !== currentVersion) {
226
+ console.log(` Latest on npm: ${chalk_1.default.bold(latestVersion)}`);
227
+ console.log(` ${chalk_1.default.yellow('Tip:')} npm update -g jun-claude-code`);
228
+ }
229
+ console.log();
230
+ // Check source exists
231
+ if (!fs.existsSync(sourceDir)) {
232
+ console.error(chalk_1.default.red('Error:'), 'Source templates/global directory not found');
233
+ process.exit(1);
234
+ }
235
+ // Step 2: Collect file lists
236
+ const allFiles = (0, copy_1.getAllFiles)(sourceDir);
237
+ const files = allFiles.filter(file => {
238
+ if (copy_1.EXCLUDE_ALWAYS.includes(file))
239
+ return false;
240
+ if (project && copy_1.EXCLUDE_FROM_PROJECT.includes(file))
241
+ return false;
242
+ return true;
243
+ });
244
+ if (files.length === 0) {
245
+ console.log(chalk_1.default.yellow('No template files found.'));
246
+ return;
247
+ }
248
+ // Step 3: Compute 3-way status for each file
249
+ const fileStatuses = files.map(file => ({
250
+ file,
251
+ status: computeUpdateStatus(file, sourceDir, destDir, metadata),
252
+ }));
253
+ // Check for removed-upstream files (in metadata but not in new template)
254
+ if (metadata) {
255
+ for (const file of Object.keys(metadata.files)) {
256
+ if (!files.includes(file) && !copy_1.EXCLUDE_ALWAYS.includes(file)) {
257
+ fileStatuses.push({ file, status: 'removed-upstream' });
258
+ }
259
+ }
260
+ }
261
+ // Group by status
262
+ const updateAvailable = fileStatuses.filter(f => f.status === 'update-available');
263
+ const newFiles = fileStatuses.filter(f => f.status === 'new-file');
264
+ const userModified = fileStatuses.filter(f => f.status === 'user-modified');
265
+ const conflicts = fileStatuses.filter(f => f.status === 'conflict');
266
+ const unchanged = fileStatuses.filter(f => f.status === 'unchanged');
267
+ const removedUpstream = fileStatuses.filter(f => f.status === 'removed-upstream');
268
+ // Step 4: Show summary
269
+ const installedVer = metadata?.version ?? '(unknown)';
270
+ console.log(chalk_1.default.cyan(`Update Summary (${installedVer} \u2192 ${currentVersion}):`));
271
+ console.log(chalk_1.default.blue('Destination:'), `${destDir} ${chalk_1.default.gray(`(${targetLabel})`)}`);
272
+ console.log();
273
+ if (updateAvailable.length > 0) {
274
+ console.log(chalk_1.default.green(` Safe to update (${updateAvailable.length}):`));
275
+ for (const f of updateAvailable) {
276
+ console.log(` ${updateStatusBracket(f.status)} ${f.file}`);
277
+ }
278
+ }
279
+ if (newFiles.length > 0) {
280
+ console.log(chalk_1.default.green(` New files (${newFiles.length}):`));
281
+ for (const f of newFiles) {
282
+ console.log(` ${updateStatusBracket(f.status)} ${f.file}`);
283
+ }
284
+ }
285
+ if (userModified.length > 0) {
286
+ console.log(chalk_1.default.yellow(` User-modified \u2014 preserved (${userModified.length}):`));
287
+ for (const f of userModified) {
288
+ console.log(` ${updateStatusBracket(f.status)} ${f.file} ${chalk_1.default.gray('(customized)')}`);
289
+ }
290
+ }
291
+ if (conflicts.length > 0) {
292
+ console.log(chalk_1.default.red(` Conflicts (${conflicts.length}):`));
293
+ for (const f of conflicts) {
294
+ console.log(` ${updateStatusBracket(f.status)} ${f.file}`);
295
+ }
296
+ }
297
+ if (removedUpstream.length > 0) {
298
+ console.log(chalk_1.default.gray(` Removed upstream (${removedUpstream.length}):`));
299
+ for (const f of removedUpstream) {
300
+ console.log(` ${updateStatusBracket(f.status)} ${f.file}`);
301
+ }
302
+ }
303
+ if (unchanged.length > 0) {
304
+ console.log(chalk_1.default.gray(` Unchanged (${unchanged.length}): ...`));
305
+ }
306
+ console.log();
307
+ // Check if there are actionable files
308
+ const actionableStatuses = ['update-available', 'new-file', 'user-modified', 'conflict'];
309
+ const actionableFiles = fileStatuses.filter(f => actionableStatuses.includes(f.status));
310
+ if (actionableFiles.length === 0) {
311
+ console.log(chalk_1.default.green('Everything is up to date!'));
312
+ return;
313
+ }
314
+ // Dry run: stop here
315
+ if (dryRun) {
316
+ console.log(chalk_1.default.yellow('No files were changed (dry run mode)'));
317
+ return;
318
+ }
319
+ // Step 5: Determine files to copy
320
+ let filesToCopy;
321
+ if (force) {
322
+ filesToCopy = files;
323
+ }
324
+ else {
325
+ filesToCopy = [];
326
+ // Legacy install warning
327
+ if (!metadata) {
328
+ console.log(chalk_1.default.yellow('No installation metadata found. All existing files will be treated as user-modified.'));
329
+ console.log(chalk_1.default.yellow('Use --force to overwrite all files.'));
330
+ console.log();
331
+ }
332
+ // Others: auto-include update-available and new-file
333
+ const categorized = (0, copy_1.categorizeFiles)(actionableFiles.map(f => f.file));
334
+ for (const file of categorized.others) {
335
+ const info = actionableFiles.find(f => f.file === file);
336
+ if (info && (info.status === 'update-available' || info.status === 'new-file')) {
337
+ filesToCopy.push(file);
338
+ }
339
+ }
340
+ // Agents: MultiSelect
341
+ const agentInfos = actionableFiles.filter(f => f.file.startsWith('agents/'));
342
+ if (agentInfos.length > 0) {
343
+ const selected = await selectUpdateItems('Agents', agentInfos);
344
+ filesToCopy.push(...selected);
345
+ }
346
+ // Skills: 2-step MultiSelect
347
+ const allSkillStatuses = fileStatuses.filter(f => f.file.startsWith('skills/'));
348
+ const skillDirs = new Set();
349
+ for (const f of allSkillStatuses) {
350
+ const parts = f.file.split('/');
351
+ if (parts.length >= 2 && parts[1])
352
+ skillDirs.add(parts[1]);
353
+ }
354
+ // Step 1: Select skill directories
355
+ const skillDirItems = [];
356
+ for (const skillName of Array.from(skillDirs).sort()) {
357
+ const status = getSkillUpdateStatus(skillName, allSkillStatuses);
358
+ if (status !== 'unchanged') {
359
+ skillDirItems.push({ file: skillName, status });
360
+ }
361
+ }
362
+ if (skillDirItems.length > 0) {
363
+ const selectedSkillDirs = await selectUpdateItems('Skills', skillDirItems);
364
+ // Step 2: For selected skills, show sub-file selection
365
+ const singleFileSkills = [];
366
+ const multiFileSkills = [];
367
+ for (const skillName of selectedSkillDirs) {
368
+ const skillFiles = allSkillStatuses.filter(f => f.file.startsWith(`skills/${skillName}/`));
369
+ const actionableSkillFiles = skillFiles.filter(f => f.status !== 'unchanged');
370
+ if (actionableSkillFiles.length <= 1) {
371
+ // Auto-include single actionable file
372
+ singleFileSkills.push(...actionableSkillFiles.map(f => f.file));
373
+ }
374
+ else {
375
+ multiFileSkills.push({ skillName, files: skillFiles });
376
+ }
377
+ }
378
+ filesToCopy.push(...singleFileSkills);
379
+ if (multiFileSkills.length > 0) {
380
+ const selectedSubFiles = await selectSkillUpdateFiles(multiFileSkills);
381
+ filesToCopy.push(...selectedSubFiles);
382
+ }
383
+ }
384
+ }
385
+ // Step 6: Copy files
386
+ let copiedCount = 0;
387
+ for (const file of filesToCopy) {
388
+ const sourcePath = path.join(sourceDir, file);
389
+ const destPath = path.join(destDir, file);
390
+ const exists = fs.existsSync(destPath);
391
+ (0, copy_1.copyFile)(sourcePath, destPath);
392
+ const label = exists ? chalk_1.default.yellow('[overwritten]') : chalk_1.default.green('[created]');
393
+ console.log(` ${label} ${file}`);
394
+ copiedCount++;
395
+ }
396
+ // Merge settings.json
397
+ (0, copy_1.mergeSettingsJson)(sourceDir, destDir, { project });
398
+ // Update metadata
399
+ const updatedMeta = (0, metadata_1.mergeMetadata)(metadata, filesToCopy, sourceDir, currentVersion);
400
+ // Refresh hashes for unchanged files
401
+ for (const fi of unchanged) {
402
+ const sourcePath = path.join(sourceDir, fi.file);
403
+ if (fs.existsSync(sourcePath)) {
404
+ updatedMeta.files[fi.file] = { hash: (0, copy_1.getFileHash)(sourcePath) };
405
+ }
406
+ }
407
+ (0, metadata_1.saveMetadata)(destDir, updatedMeta);
408
+ // Step 7: Summary
409
+ console.log();
410
+ const newCount = filesToCopy.filter(f => {
411
+ const info = fileStatuses.find(i => i.file === f);
412
+ return info?.status === 'new-file';
413
+ }).length;
414
+ const updateCount = copiedCount - newCount;
415
+ const preservedCount = userModified.length + conflicts.length -
416
+ filesToCopy.filter(f => {
417
+ const info = fileStatuses.find(i => i.file === f);
418
+ return info && (info.status === 'user-modified' || info.status === 'conflict');
419
+ }).length;
420
+ const parts = [];
421
+ if (updateCount > 0)
422
+ parts.push(`updated ${updateCount} files`);
423
+ if (newCount > 0)
424
+ parts.push(`added ${newCount} new files`);
425
+ if (preservedCount > 0)
426
+ parts.push(`preserved ${preservedCount} customized files`);
427
+ if (parts.length > 0) {
428
+ console.log(chalk_1.default.green(`Done! ${parts.join(', ')}.`));
429
+ }
430
+ else {
431
+ console.log(chalk_1.default.green('Done! No files were updated.'));
432
+ }
433
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jun-claude-code",
3
- "version": "0.6.2",
3
+ "version": "0.6.4",
4
4
  "description": "Claude Code configuration template - copy .claude settings to your project",
5
5
  "main": "dist/index.js",
6
6
  "bin": "dist/cli.js",
@@ -146,13 +146,15 @@ Plan 파일의 Context 섹션에 위 내용을 명시하여 작업 목적이 희
146
146
 
147
147
  ### Step 1.5.5: Plan 문서 생성 -- 필수
148
148
 
149
+ > **`Planning` Skill (`skills/Planning/SKILL.md`)의 템플릿을 따라 작성한다.**
150
+
149
151
  계획이 확정되면 프로젝트의 `.claude/plan/` 폴더에 3가지 문서를 생성하세요:
150
152
 
151
- - [ ] `.claude/plan/plan.md` -- 전체 계획 (목적, 설계, 수정 파일 목록, TaskList 요약)
153
+ - [ ] `.claude/plan/plan.md` -- 전체 계획 (목적, DB/API/FE 설계, 설계 결정과 차선책, TaskList 요약)
152
154
  - [ ] `.claude/plan/context.md` -- 맥락 (사용자 요청 원문, 비즈니스/기술적 배경, 탐색한 코드, 결정 사항)
153
155
  - [ ] `.claude/plan/checklist.md` -- 실행 체크리스트 (Phase별 체크리스트, Task별 세부 작업)
154
156
 
155
- 각 문서에는 frontmatter(name, description, created)를 포함하세요.
157
+ 각 문서에는 frontmatter(name, description, created, status)를 포함하세요.
156
158
  이 문서들은 구현 중 맥락 유실 방지와 진행 추적에 활용됩니다.
157
159
 
158
160
  ### Step 1.6: 사용자 Confirm -- 필수
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: plan-verifier
3
3
  description: Plan 수립 후 구현 전에 Plan 품질을 검증하는 Agent. 목적 정합성, 완전성, 논리적 일관성, 실현 가능성, 스코프 초과 여부를 코드베이스 탐색을 통해 능동적으로 검증.
4
- skills: [Reporting]
4
+ skills: [Planning, Reporting]
5
5
  keywords: [Plan검증, 목적정합성, 완전성, 논리적일관성, 실현가능성, 스코프검증, PlanReview]
6
6
  model: opus
7
7
  color: cyan
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: task-planner
3
3
  description: 복잡한 작업 시작 전 계획 수립 시 호출. 요구사항 명확화 질문, TaskList 생성, 파일별 수정 계획 작성, 의존성 순서 정의.
4
- skills: [Reporting]
4
+ skills: [Planning, Reporting]
5
5
  keywords: [TaskList, 계획수립, 요구사항명확화, 작업분해, 질문, 수정계획, 파일목록, 의존성]
6
6
  model: opus
7
7
  color: green
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: Backend
3
- description: NestJS/TypeORM 백엔드 개발 시 사용. 레이어 객체 변환, find vs queryBuilder 선택 기준, BDD 테스트 작성 규칙 제공.
4
- keywords: [Backend, 백엔드, 레이어, DTO, Entity, Service, Controller, TypeORM, find, queryBuilder, test, BDD, 테스트, Jest]
3
+ description: NestJS 백엔드 개발 시 사용. 레이어 객체 변환 규칙, BDD 테스트 작성 규칙 제공.
4
+ keywords: [Backend, 백엔드, 레이어, DTO, Entity, Service, Controller, test, BDD, 테스트, Jest]
5
5
  user-invocable: false
6
6
  ---
7
7
 
@@ -90,78 +90,6 @@ async findOne(@Param('id') id: number) {
90
90
 
91
91
  ---
92
92
 
93
- <rules>
94
-
95
- ## TypeORM 사용 규칙
96
-
97
- > **find 메서드를 기본으로 사용하고, QueryBuilder는 필요한 경우에만 사용한다.**
98
-
99
- ### find 메서드 우선 사용
100
-
101
- ```typescript
102
- // ✅ 기본 조회
103
- const user = await this.userRepository.findOneBy({ id });
104
- const users = await this.userRepository.find({
105
- where: { status: 'active' },
106
- relations: ['orders'],
107
- order: { createdAt: 'DESC' },
108
- take: 10,
109
- });
110
-
111
- // ✅ 조건 조합
112
- const users = await this.userRepository.find({
113
- where: [
114
- { status: 'active', role: 'admin' },
115
- { status: 'active', role: 'manager' },
116
- ],
117
- });
118
- ```
119
-
120
- ### QueryBuilder 허용 케이스
121
-
122
- **다음 경우에만 QueryBuilder 사용:**
123
-
124
- | 케이스 | 예시 |
125
- |--------|------|
126
- | **groupBy** | 집계 쿼리 |
127
- | **getRawMany/getRawOne** | 원시 데이터 필요 |
128
- | **복잡한 서브쿼리** | 중첩 쿼리 |
129
- | **복잡한 JOIN 조건** | ON 절 커스텀 |
130
-
131
- </rules>
132
-
133
- <examples>
134
- <example type="good">
135
- ```typescript
136
- // ✅ QueryBuilder 허용: groupBy + getRawMany
137
- const stats = await this.orderRepository
138
- .createQueryBuilder('order')
139
- .select('order.status', 'status')
140
- .addSelect('COUNT(*)', 'count')
141
- .addSelect('SUM(order.amount)', 'total')
142
- .groupBy('order.status')
143
- .getRawMany();
144
- ```
145
- </example>
146
- <example type="bad">
147
- ```typescript
148
- // ❌ 불필요한 QueryBuilder 사용
149
- const user = await this.userRepository
150
- .createQueryBuilder('user')
151
- .where('user.id = :id', { id })
152
- .getOne();
153
- ```
154
- </example>
155
- <example type="good">
156
- ```typescript
157
- // ✅ find로 대체
158
- const user = await this.userRepository.findOneBy({ id });
159
- ```
160
- </example>
161
- </examples>
162
-
163
- ---
164
-
165
93
  <checklist>
166
94
 
167
95
  ## 체크리스트
@@ -170,8 +98,6 @@ const user = await this.userRepository.findOneBy({ id });
170
98
  - [ ] Service에서 Entity가 필요한 시점에 변환하는가?
171
99
  - [ ] Service의 return은 Entity 또는 일반 객체인가?
172
100
  - [ ] Controller에서 Response DTO/Schema로 변환하는가?
173
- - [ ] TypeORM find 메서드를 우선 사용하는가?
174
- - [ ] QueryBuilder는 groupBy, getRawMany 등 필요한 경우에만 사용하는가?
175
101
 
176
102
  </checklist>
177
103
 
@@ -181,6 +107,7 @@ const user = await this.userRepository.findOneBy({ id });
181
107
 
182
108
  | 주제 | 위치 | 설명 |
183
109
  |-----|------|------|
110
+ | TypeORM 사용 규칙 | `TypeORM/SKILL.md` | find vs queryBuilder 선택 기준 |
184
111
  | BDD 테스트 | `bdd-testing.md` | NestJS + Jest BDD 스타일 테스트 작성 규칙 |
185
112
 
186
113
  </reference>