@mandujs/core 0.9.46 → 0.11.0
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/README.md +79 -10
- package/package.json +1 -1
- package/src/brain/doctor/config-analyzer.ts +498 -0
- package/src/brain/doctor/index.ts +10 -0
- package/src/change/snapshot.ts +46 -1
- package/src/change/types.ts +13 -0
- package/src/config/index.ts +9 -2
- package/src/config/mcp-ref.ts +348 -0
- package/src/config/mcp-status.ts +348 -0
- package/src/config/metadata.test.ts +308 -0
- package/src/config/metadata.ts +293 -0
- package/src/config/symbols.ts +144 -0
- package/src/config/validate.ts +122 -65
- package/src/config/watcher.ts +311 -0
- package/src/contract/index.ts +26 -25
- package/src/contract/protection.ts +364 -0
- package/src/error/domains.ts +265 -0
- package/src/error/index.ts +25 -13
- package/src/errors/extractor.ts +409 -0
- package/src/errors/index.ts +19 -0
- package/src/filling/context.ts +29 -1
- package/src/filling/deps.ts +238 -0
- package/src/filling/filling.ts +94 -8
- package/src/filling/index.ts +18 -0
- package/src/guard/analyzer.ts +7 -2
- package/src/guard/config-guard.ts +281 -0
- package/src/guard/decision-memory.test.ts +293 -0
- package/src/guard/decision-memory.ts +532 -0
- package/src/guard/healing.test.ts +259 -0
- package/src/guard/healing.ts +874 -0
- package/src/guard/index.ts +119 -0
- package/src/guard/negotiation.test.ts +282 -0
- package/src/guard/negotiation.ts +975 -0
- package/src/guard/semantic-slots.test.ts +379 -0
- package/src/guard/semantic-slots.ts +796 -0
- package/src/index.ts +4 -1
- package/src/lockfile/generate.ts +259 -0
- package/src/lockfile/index.ts +186 -0
- package/src/lockfile/lockfile.test.ts +410 -0
- package/src/lockfile/types.ts +184 -0
- package/src/lockfile/validate.ts +308 -0
- package/src/logging/index.ts +22 -0
- package/src/logging/transports.ts +365 -0
- package/src/plugins/index.ts +38 -0
- package/src/plugins/registry.ts +377 -0
- package/src/plugins/types.ts +363 -0
- package/src/runtime/security.ts +155 -0
- package/src/runtime/server.ts +318 -256
- package/src/runtime/session-key.ts +328 -0
- package/src/utils/differ.test.ts +342 -0
- package/src/utils/differ.ts +482 -0
- package/src/utils/hasher.test.ts +326 -0
- package/src/utils/hasher.ts +319 -0
- package/src/utils/index.ts +29 -0
- package/src/utils/safe-io.ts +188 -0
- package/src/utils/string-safe.ts +298 -0
|
@@ -0,0 +1,975 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Architecture Negotiation - 아키텍처 협상 시스템
|
|
3
|
+
*
|
|
4
|
+
* AI가 기능 구현 전에 프레임워크와 "협상"하여 최적의 구조를 받아옴
|
|
5
|
+
*
|
|
6
|
+
* @module guard/negotiation
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```typescript
|
|
10
|
+
* import { negotiate, generateScaffold } from "@mandujs/core/guard";
|
|
11
|
+
*
|
|
12
|
+
* const plan = await negotiate({
|
|
13
|
+
* intent: "사용자 인증 기능 추가",
|
|
14
|
+
* requirements: ["JWT 기반", "리프레시 토큰"],
|
|
15
|
+
* constraints: ["기존 User 모델 활용"],
|
|
16
|
+
* }, projectRoot);
|
|
17
|
+
*
|
|
18
|
+
* if (plan.approved) {
|
|
19
|
+
* await generateScaffold(plan.structure, projectRoot);
|
|
20
|
+
* }
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { join, dirname } from "path";
|
|
25
|
+
import { mkdir, writeFile, readdir, stat } from "fs/promises";
|
|
26
|
+
import { searchDecisions, type ArchitectureDecision } from "./decision-memory";
|
|
27
|
+
import { getPreset, type GuardPreset, type PresetDefinition } from "./presets";
|
|
28
|
+
|
|
29
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
30
|
+
// Types
|
|
31
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 협상 요청
|
|
35
|
+
*/
|
|
36
|
+
export interface NegotiationRequest {
|
|
37
|
+
/** 구현하려는 기능의 의도 */
|
|
38
|
+
intent: string;
|
|
39
|
+
|
|
40
|
+
/** 요구사항 목록 */
|
|
41
|
+
requirements?: string[];
|
|
42
|
+
|
|
43
|
+
/** 제약 조건 */
|
|
44
|
+
constraints?: string[];
|
|
45
|
+
|
|
46
|
+
/** 사용할 프리셋 (미지정 시 프로젝트 설정 사용) */
|
|
47
|
+
preset?: GuardPreset;
|
|
48
|
+
|
|
49
|
+
/** 기능 카테고리 (자동 감지 시도) */
|
|
50
|
+
category?: FeatureCategory;
|
|
51
|
+
|
|
52
|
+
/** 추가 컨텍스트 */
|
|
53
|
+
context?: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* 기능 카테고리
|
|
58
|
+
*/
|
|
59
|
+
export type FeatureCategory =
|
|
60
|
+
| "auth" // 인증/인가
|
|
61
|
+
| "crud" // CRUD 작업
|
|
62
|
+
| "api" // API 엔드포인트
|
|
63
|
+
| "ui" // UI 컴포넌트
|
|
64
|
+
| "integration" // 외부 서비스 연동
|
|
65
|
+
| "data" // 데이터 처리
|
|
66
|
+
| "util" // 유틸리티
|
|
67
|
+
| "config" // 설정
|
|
68
|
+
| "other"; // 기타
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* 디렉토리 구조 제안
|
|
72
|
+
*/
|
|
73
|
+
export interface DirectoryProposal {
|
|
74
|
+
/** 디렉토리 경로 */
|
|
75
|
+
path: string;
|
|
76
|
+
|
|
77
|
+
/** 목적 설명 */
|
|
78
|
+
purpose: string;
|
|
79
|
+
|
|
80
|
+
/** 생성할 파일들 */
|
|
81
|
+
files: FileProposal[];
|
|
82
|
+
|
|
83
|
+
/** 레이어 (FSD/Clean 등) */
|
|
84
|
+
layer?: string;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* 파일 제안
|
|
89
|
+
*/
|
|
90
|
+
export interface FileProposal {
|
|
91
|
+
/** 파일명 */
|
|
92
|
+
name: string;
|
|
93
|
+
|
|
94
|
+
/** 목적 */
|
|
95
|
+
purpose: string;
|
|
96
|
+
|
|
97
|
+
/** 템플릿 타입 */
|
|
98
|
+
template?: FileTemplate;
|
|
99
|
+
|
|
100
|
+
/** 슬롯 여부 */
|
|
101
|
+
isSlot?: boolean;
|
|
102
|
+
|
|
103
|
+
/** 권장 제약 조건 */
|
|
104
|
+
suggestedConstraints?: string[];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* 파일 템플릿 타입
|
|
109
|
+
*/
|
|
110
|
+
export type FileTemplate =
|
|
111
|
+
| "service"
|
|
112
|
+
| "repository"
|
|
113
|
+
| "usecase"
|
|
114
|
+
| "controller"
|
|
115
|
+
| "route"
|
|
116
|
+
| "component"
|
|
117
|
+
| "hook"
|
|
118
|
+
| "util"
|
|
119
|
+
| "type"
|
|
120
|
+
| "test"
|
|
121
|
+
| "slot";
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* 협상 응답
|
|
125
|
+
*/
|
|
126
|
+
export interface NegotiationResponse {
|
|
127
|
+
/** 승인 여부 */
|
|
128
|
+
approved: boolean;
|
|
129
|
+
|
|
130
|
+
/** 승인 거부 사유 (approved=false일 때) */
|
|
131
|
+
rejectionReason?: string;
|
|
132
|
+
|
|
133
|
+
/** 제안된 구조 */
|
|
134
|
+
structure: DirectoryProposal[];
|
|
135
|
+
|
|
136
|
+
/** 생성할 슬롯 목록 */
|
|
137
|
+
slots: SlotProposal[];
|
|
138
|
+
|
|
139
|
+
/** 경고 사항 */
|
|
140
|
+
warnings: string[];
|
|
141
|
+
|
|
142
|
+
/** 권장 사항 */
|
|
143
|
+
recommendations: string[];
|
|
144
|
+
|
|
145
|
+
/** 관련 기존 결정 */
|
|
146
|
+
relatedDecisions: RelatedDecision[];
|
|
147
|
+
|
|
148
|
+
/** 예상 파일 수 */
|
|
149
|
+
estimatedFiles: number;
|
|
150
|
+
|
|
151
|
+
/** 사용된 프리셋 */
|
|
152
|
+
preset: GuardPreset;
|
|
153
|
+
|
|
154
|
+
/** 다음 단계 안내 */
|
|
155
|
+
nextSteps: string[];
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* 슬롯 제안
|
|
160
|
+
*/
|
|
161
|
+
export interface SlotProposal {
|
|
162
|
+
/** 슬롯 경로 */
|
|
163
|
+
path: string;
|
|
164
|
+
|
|
165
|
+
/** 목적 */
|
|
166
|
+
purpose: string;
|
|
167
|
+
|
|
168
|
+
/** 권장 제약 조건 */
|
|
169
|
+
constraints?: string[];
|
|
170
|
+
|
|
171
|
+
/** 필요한 import */
|
|
172
|
+
suggestedImports?: string[];
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* 관련 결정 요약
|
|
177
|
+
*/
|
|
178
|
+
export interface RelatedDecision {
|
|
179
|
+
/** 결정 ID */
|
|
180
|
+
id: string;
|
|
181
|
+
|
|
182
|
+
/** 제목 */
|
|
183
|
+
title: string;
|
|
184
|
+
|
|
185
|
+
/** 핵심 내용 요약 */
|
|
186
|
+
summary: string;
|
|
187
|
+
|
|
188
|
+
/** 관련성 설명 */
|
|
189
|
+
relevance: string;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Scaffold 생성 결과
|
|
194
|
+
*/
|
|
195
|
+
export interface ScaffoldResult {
|
|
196
|
+
/** 성공 여부 */
|
|
197
|
+
success: boolean;
|
|
198
|
+
|
|
199
|
+
/** 생성된 디렉토리 */
|
|
200
|
+
createdDirs: string[];
|
|
201
|
+
|
|
202
|
+
/** 생성된 파일 */
|
|
203
|
+
createdFiles: string[];
|
|
204
|
+
|
|
205
|
+
/** 건너뛴 파일 (이미 존재) */
|
|
206
|
+
skippedFiles: string[];
|
|
207
|
+
|
|
208
|
+
/** 에러 메시지 */
|
|
209
|
+
errors: string[];
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
213
|
+
// Category Detection
|
|
214
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* 키워드 기반 카테고리 매핑
|
|
218
|
+
*/
|
|
219
|
+
const CATEGORY_KEYWORDS: Record<FeatureCategory, string[]> = {
|
|
220
|
+
auth: ["인증", "로그인", "로그아웃", "회원가입", "비밀번호", "토큰", "jwt", "oauth", "session", "auth", "login", "signup", "password"],
|
|
221
|
+
crud: ["생성", "조회", "수정", "삭제", "목록", "create", "read", "update", "delete", "list", "crud"],
|
|
222
|
+
api: ["api", "엔드포인트", "endpoint", "rest", "graphql", "route"],
|
|
223
|
+
ui: ["컴포넌트", "페이지", "화면", "폼", "버튼", "component", "page", "form", "button", "modal", "ui"],
|
|
224
|
+
integration: ["연동", "통합", "외부", "third-party", "integration", "webhook", "stripe", "payment", "email", "sms"],
|
|
225
|
+
data: ["데이터", "처리", "변환", "마이그레이션", "data", "transform", "migration", "import", "export"],
|
|
226
|
+
util: ["유틸", "헬퍼", "공통", "util", "helper", "common", "shared"],
|
|
227
|
+
config: ["설정", "환경", "config", "env", "setting"],
|
|
228
|
+
other: [],
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* 의도에서 카테고리 자동 감지
|
|
233
|
+
*/
|
|
234
|
+
export function detectCategory(intent: string): FeatureCategory {
|
|
235
|
+
const normalizedIntent = intent.toLowerCase();
|
|
236
|
+
|
|
237
|
+
for (const [category, keywords] of Object.entries(CATEGORY_KEYWORDS)) {
|
|
238
|
+
if (category === "other") continue;
|
|
239
|
+
if (keywords.some((kw) => normalizedIntent.includes(kw))) {
|
|
240
|
+
return category as FeatureCategory;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return "other";
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
248
|
+
// Structure Templates
|
|
249
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* 카테고리별 구조 템플릿 (FSD + Clean 조합)
|
|
253
|
+
*/
|
|
254
|
+
const STRUCTURE_TEMPLATES: Record<FeatureCategory, (featureName: string) => DirectoryProposal[]> = {
|
|
255
|
+
auth: (name) => [
|
|
256
|
+
{
|
|
257
|
+
path: `server/domain/${name}`,
|
|
258
|
+
purpose: `${name} 도메인 로직`,
|
|
259
|
+
layer: "domain",
|
|
260
|
+
files: [
|
|
261
|
+
{ name: `${name}.service.ts`, purpose: "핵심 비즈니스 로직", template: "service" },
|
|
262
|
+
{ name: `${name}.types.ts`, purpose: "타입 정의", template: "type" },
|
|
263
|
+
],
|
|
264
|
+
},
|
|
265
|
+
{
|
|
266
|
+
path: `server/application/${name}`,
|
|
267
|
+
purpose: `${name} 유스케이스`,
|
|
268
|
+
layer: "application",
|
|
269
|
+
files: [
|
|
270
|
+
{ name: `login.usecase.ts`, purpose: "로그인 유스케이스", template: "usecase" },
|
|
271
|
+
{ name: `logout.usecase.ts`, purpose: "로그아웃 유스케이스", template: "usecase" },
|
|
272
|
+
{ name: `refresh.usecase.ts`, purpose: "토큰 갱신 유스케이스", template: "usecase" },
|
|
273
|
+
],
|
|
274
|
+
},
|
|
275
|
+
{
|
|
276
|
+
path: `server/infra/${name}`,
|
|
277
|
+
purpose: `${name} 인프라 어댑터`,
|
|
278
|
+
layer: "infrastructure",
|
|
279
|
+
files: [
|
|
280
|
+
{ name: `token.provider.ts`, purpose: "토큰 생성/검증", template: "service" },
|
|
281
|
+
{ name: `session.repository.ts`, purpose: "세션 저장소", template: "repository" },
|
|
282
|
+
],
|
|
283
|
+
},
|
|
284
|
+
{
|
|
285
|
+
path: `app/api/${name}`,
|
|
286
|
+
purpose: `${name} API 라우트`,
|
|
287
|
+
layer: "api",
|
|
288
|
+
files: [
|
|
289
|
+
{ name: `login/route.ts`, purpose: "로그인 API", template: "route", isSlot: true },
|
|
290
|
+
{ name: `logout/route.ts`, purpose: "로그아웃 API", template: "route", isSlot: true },
|
|
291
|
+
{ name: `refresh/route.ts`, purpose: "토큰 갱신 API", template: "route", isSlot: true },
|
|
292
|
+
],
|
|
293
|
+
},
|
|
294
|
+
],
|
|
295
|
+
|
|
296
|
+
crud: (name) => [
|
|
297
|
+
{
|
|
298
|
+
path: `server/domain/${name}`,
|
|
299
|
+
purpose: `${name} 도메인`,
|
|
300
|
+
layer: "domain",
|
|
301
|
+
files: [
|
|
302
|
+
{ name: `${name}.service.ts`, purpose: "CRUD 비즈니스 로직", template: "service" },
|
|
303
|
+
{ name: `${name}.repository.ts`, purpose: "데이터 접근", template: "repository" },
|
|
304
|
+
{ name: `${name}.types.ts`, purpose: "타입 정의", template: "type" },
|
|
305
|
+
],
|
|
306
|
+
},
|
|
307
|
+
{
|
|
308
|
+
path: `app/api/${name}`,
|
|
309
|
+
purpose: `${name} API`,
|
|
310
|
+
layer: "api",
|
|
311
|
+
files: [
|
|
312
|
+
{ name: `route.ts`, purpose: "목록/생성 API (GET, POST)", template: "route", isSlot: true },
|
|
313
|
+
{ name: `[id]/route.ts`, purpose: "상세/수정/삭제 API (GET, PUT, DELETE)", template: "route", isSlot: true },
|
|
314
|
+
],
|
|
315
|
+
},
|
|
316
|
+
],
|
|
317
|
+
|
|
318
|
+
api: (name) => [
|
|
319
|
+
{
|
|
320
|
+
path: `server/domain/${name}`,
|
|
321
|
+
purpose: `${name} 도메인 로직`,
|
|
322
|
+
layer: "domain",
|
|
323
|
+
files: [
|
|
324
|
+
{ name: `${name}.service.ts`, purpose: "비즈니스 로직", template: "service" },
|
|
325
|
+
{ name: `${name}.types.ts`, purpose: "타입 정의", template: "type" },
|
|
326
|
+
],
|
|
327
|
+
},
|
|
328
|
+
{
|
|
329
|
+
path: `app/api/${name}`,
|
|
330
|
+
purpose: `${name} API 엔드포인트`,
|
|
331
|
+
layer: "api",
|
|
332
|
+
files: [
|
|
333
|
+
{ name: `route.ts`, purpose: "API 핸들러", template: "route", isSlot: true },
|
|
334
|
+
],
|
|
335
|
+
},
|
|
336
|
+
],
|
|
337
|
+
|
|
338
|
+
ui: (name) => [
|
|
339
|
+
{
|
|
340
|
+
path: `client/widgets/${name}`,
|
|
341
|
+
purpose: `${name} 위젯`,
|
|
342
|
+
layer: "widgets",
|
|
343
|
+
files: [
|
|
344
|
+
{ name: `${name}.tsx`, purpose: "메인 컴포넌트", template: "component" },
|
|
345
|
+
{ name: `${name}.styles.ts`, purpose: "스타일", template: "util" },
|
|
346
|
+
{ name: `index.ts`, purpose: "Public API", template: "util" },
|
|
347
|
+
],
|
|
348
|
+
},
|
|
349
|
+
{
|
|
350
|
+
path: `client/features/${name}`,
|
|
351
|
+
purpose: `${name} 기능 로직`,
|
|
352
|
+
layer: "features",
|
|
353
|
+
files: [
|
|
354
|
+
{ name: `model/store.ts`, purpose: "상태 관리", template: "service" },
|
|
355
|
+
{ name: `model/types.ts`, purpose: "타입 정의", template: "type" },
|
|
356
|
+
{ name: `api/${name}.api.ts`, purpose: "API 호출", template: "service" },
|
|
357
|
+
],
|
|
358
|
+
},
|
|
359
|
+
],
|
|
360
|
+
|
|
361
|
+
integration: (name) => [
|
|
362
|
+
{
|
|
363
|
+
path: `server/infra/${name}`,
|
|
364
|
+
purpose: `${name} 외부 서비스 어댑터`,
|
|
365
|
+
layer: "infrastructure",
|
|
366
|
+
files: [
|
|
367
|
+
{ name: `${name}.client.ts`, purpose: "외부 API 클라이언트", template: "service" },
|
|
368
|
+
{ name: `${name}.types.ts`, purpose: "타입 정의", template: "type" },
|
|
369
|
+
{ name: `${name}.config.ts`, purpose: "설정", template: "util" },
|
|
370
|
+
],
|
|
371
|
+
},
|
|
372
|
+
{
|
|
373
|
+
path: `server/domain/${name}`,
|
|
374
|
+
purpose: `${name} 도메인 인터페이스`,
|
|
375
|
+
layer: "domain",
|
|
376
|
+
files: [
|
|
377
|
+
{ name: `${name}.port.ts`, purpose: "포트 인터페이스", template: "type" },
|
|
378
|
+
],
|
|
379
|
+
},
|
|
380
|
+
{
|
|
381
|
+
path: `app/api/webhooks/${name}`,
|
|
382
|
+
purpose: `${name} 웹훅`,
|
|
383
|
+
layer: "api",
|
|
384
|
+
files: [
|
|
385
|
+
{ name: `route.ts`, purpose: "웹훅 핸들러", template: "route", isSlot: true },
|
|
386
|
+
],
|
|
387
|
+
},
|
|
388
|
+
],
|
|
389
|
+
|
|
390
|
+
data: (name) => [
|
|
391
|
+
{
|
|
392
|
+
path: `server/domain/${name}`,
|
|
393
|
+
purpose: `${name} 데이터 처리`,
|
|
394
|
+
layer: "domain",
|
|
395
|
+
files: [
|
|
396
|
+
{ name: `${name}.processor.ts`, purpose: "데이터 처리 로직", template: "service" },
|
|
397
|
+
{ name: `${name}.transformer.ts`, purpose: "데이터 변환", template: "util" },
|
|
398
|
+
{ name: `${name}.types.ts`, purpose: "타입 정의", template: "type" },
|
|
399
|
+
],
|
|
400
|
+
},
|
|
401
|
+
],
|
|
402
|
+
|
|
403
|
+
util: (name) => [
|
|
404
|
+
{
|
|
405
|
+
path: `shared/utils/${name}`,
|
|
406
|
+
purpose: `${name} 유틸리티`,
|
|
407
|
+
layer: "shared",
|
|
408
|
+
files: [
|
|
409
|
+
{ name: `${name}.ts`, purpose: "유틸리티 함수", template: "util" },
|
|
410
|
+
{ name: `${name}.test.ts`, purpose: "테스트", template: "test" },
|
|
411
|
+
{ name: `index.ts`, purpose: "Public API", template: "util" },
|
|
412
|
+
],
|
|
413
|
+
},
|
|
414
|
+
],
|
|
415
|
+
|
|
416
|
+
config: (name) => [
|
|
417
|
+
{
|
|
418
|
+
path: `shared/config`,
|
|
419
|
+
purpose: "설정 관리",
|
|
420
|
+
layer: "shared",
|
|
421
|
+
files: [
|
|
422
|
+
{ name: `${name}.config.ts`, purpose: `${name} 설정`, template: "util" },
|
|
423
|
+
{ name: `${name}.schema.ts`, purpose: "설정 스키마 (Zod)", template: "type" },
|
|
424
|
+
],
|
|
425
|
+
},
|
|
426
|
+
],
|
|
427
|
+
|
|
428
|
+
other: (name) => [
|
|
429
|
+
{
|
|
430
|
+
path: `server/domain/${name}`,
|
|
431
|
+
purpose: `${name} 도메인`,
|
|
432
|
+
layer: "domain",
|
|
433
|
+
files: [
|
|
434
|
+
{ name: `${name}.service.ts`, purpose: "비즈니스 로직", template: "service" },
|
|
435
|
+
{ name: `${name}.types.ts`, purpose: "타입 정의", template: "type" },
|
|
436
|
+
],
|
|
437
|
+
},
|
|
438
|
+
],
|
|
439
|
+
};
|
|
440
|
+
|
|
441
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
442
|
+
// File Templates
|
|
443
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* 파일 템플릿 생성
|
|
447
|
+
*/
|
|
448
|
+
function generateFileContent(template: FileTemplate, name: string, purpose: string): string {
|
|
449
|
+
switch (template) {
|
|
450
|
+
case "service":
|
|
451
|
+
return `/**
|
|
452
|
+
* ${purpose}
|
|
453
|
+
*/
|
|
454
|
+
|
|
455
|
+
export class ${toPascalCase(name)}Service {
|
|
456
|
+
// TODO: Implement service methods
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
export const ${toCamelCase(name)}Service = new ${toPascalCase(name)}Service();
|
|
460
|
+
`;
|
|
461
|
+
|
|
462
|
+
case "repository":
|
|
463
|
+
return `/**
|
|
464
|
+
* ${purpose}
|
|
465
|
+
*/
|
|
466
|
+
|
|
467
|
+
export interface ${toPascalCase(name)}Repository {
|
|
468
|
+
// TODO: Define repository interface
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
export class ${toPascalCase(name)}RepositoryImpl implements ${toPascalCase(name)}Repository {
|
|
472
|
+
// TODO: Implement repository methods
|
|
473
|
+
}
|
|
474
|
+
`;
|
|
475
|
+
|
|
476
|
+
case "usecase":
|
|
477
|
+
return `/**
|
|
478
|
+
* ${purpose}
|
|
479
|
+
*/
|
|
480
|
+
|
|
481
|
+
export interface ${toPascalCase(name)}Input {
|
|
482
|
+
// TODO: Define input
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
export interface ${toPascalCase(name)}Output {
|
|
486
|
+
// TODO: Define output
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
export async function ${toCamelCase(name)}(input: ${toPascalCase(name)}Input): Promise<${toPascalCase(name)}Output> {
|
|
490
|
+
// TODO: Implement usecase
|
|
491
|
+
throw new Error("Not implemented");
|
|
492
|
+
}
|
|
493
|
+
`;
|
|
494
|
+
|
|
495
|
+
case "route":
|
|
496
|
+
case "slot":
|
|
497
|
+
return `/**
|
|
498
|
+
* ${purpose}
|
|
499
|
+
*
|
|
500
|
+
* @slot
|
|
501
|
+
*/
|
|
502
|
+
|
|
503
|
+
import { Mandu } from "@mandujs/core";
|
|
504
|
+
|
|
505
|
+
export default Mandu.filling()
|
|
506
|
+
.purpose("${purpose}")
|
|
507
|
+
.constraints({
|
|
508
|
+
maxLines: 50,
|
|
509
|
+
requiredPatterns: ["input-validation", "error-handling"],
|
|
510
|
+
})
|
|
511
|
+
.get(async (ctx) => {
|
|
512
|
+
// TODO: Implement handler
|
|
513
|
+
return ctx.json({ message: "Not implemented" }, 501);
|
|
514
|
+
});
|
|
515
|
+
`;
|
|
516
|
+
|
|
517
|
+
case "component":
|
|
518
|
+
return `/**
|
|
519
|
+
* ${purpose}
|
|
520
|
+
*/
|
|
521
|
+
|
|
522
|
+
export interface ${toPascalCase(name)}Props {
|
|
523
|
+
// TODO: Define props
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
export function ${toPascalCase(name)}({ ...props }: ${toPascalCase(name)}Props) {
|
|
527
|
+
return (
|
|
528
|
+
<div>
|
|
529
|
+
{/* TODO: Implement component */}
|
|
530
|
+
</div>
|
|
531
|
+
);
|
|
532
|
+
}
|
|
533
|
+
`;
|
|
534
|
+
|
|
535
|
+
case "type":
|
|
536
|
+
return `/**
|
|
537
|
+
* ${purpose}
|
|
538
|
+
*/
|
|
539
|
+
|
|
540
|
+
export interface ${toPascalCase(name)} {
|
|
541
|
+
// TODO: Define type
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
export type ${toPascalCase(name)}Id = string;
|
|
545
|
+
`;
|
|
546
|
+
|
|
547
|
+
case "test":
|
|
548
|
+
return `/**
|
|
549
|
+
* ${purpose}
|
|
550
|
+
*/
|
|
551
|
+
|
|
552
|
+
import { describe, it, expect } from "bun:test";
|
|
553
|
+
|
|
554
|
+
describe("${name}", () => {
|
|
555
|
+
it("should work", () => {
|
|
556
|
+
// TODO: Add tests
|
|
557
|
+
expect(true).toBe(true);
|
|
558
|
+
});
|
|
559
|
+
});
|
|
560
|
+
`;
|
|
561
|
+
|
|
562
|
+
case "util":
|
|
563
|
+
default:
|
|
564
|
+
return `/**
|
|
565
|
+
* ${purpose}
|
|
566
|
+
*/
|
|
567
|
+
|
|
568
|
+
// TODO: Implement utility functions
|
|
569
|
+
`;
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
574
|
+
// Helper Functions
|
|
575
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
576
|
+
|
|
577
|
+
function toPascalCase(str: string): string {
|
|
578
|
+
return str
|
|
579
|
+
.replace(/[-_](.)/g, (_, c) => c.toUpperCase())
|
|
580
|
+
.replace(/^(.)/, (_, c) => c.toUpperCase());
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
function toCamelCase(str: string): string {
|
|
584
|
+
return str
|
|
585
|
+
.replace(/[-_](.)/g, (_, c) => c.toUpperCase())
|
|
586
|
+
.replace(/^(.)/, (_, c) => c.toLowerCase());
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
function extractFeatureName(intent: string): string {
|
|
590
|
+
// 한글/영문에서 핵심 명사 추출 시도
|
|
591
|
+
const patterns = [
|
|
592
|
+
/(?:추가|구현|만들)(?:어|해)[줘요]?\s*[:\-]?\s*(.+)/,
|
|
593
|
+
/(.+?)\s*(?:기능|시스템|모듈)/,
|
|
594
|
+
/(?:add|implement|create)\s+(.+)/i,
|
|
595
|
+
];
|
|
596
|
+
|
|
597
|
+
for (const pattern of patterns) {
|
|
598
|
+
const match = intent.match(pattern);
|
|
599
|
+
if (match) {
|
|
600
|
+
return match[1]
|
|
601
|
+
.trim()
|
|
602
|
+
.toLowerCase()
|
|
603
|
+
.replace(/\s+/g, "-")
|
|
604
|
+
.replace(/[^a-z0-9-]/g, "");
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// 기본값: 첫 단어
|
|
609
|
+
return intent
|
|
610
|
+
.split(/\s+/)[0]
|
|
611
|
+
.toLowerCase()
|
|
612
|
+
.replace(/[^a-z0-9]/g, "") || "feature";
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/**
|
|
616
|
+
* 프리셋에 따라 구조를 조정
|
|
617
|
+
* FSD, Clean, Hexagonal 등 프리셋별 레이어 매핑 적용
|
|
618
|
+
*/
|
|
619
|
+
function adjustStructureForPreset(
|
|
620
|
+
structure: DirectoryProposal[],
|
|
621
|
+
presetDef: PresetDefinition,
|
|
622
|
+
preset: GuardPreset
|
|
623
|
+
): DirectoryProposal[] {
|
|
624
|
+
// 프리셋별 경로 매핑
|
|
625
|
+
const pathMappings: Record<GuardPreset, Record<string, string>> = {
|
|
626
|
+
fsd: {
|
|
627
|
+
"server/domain": "src/entities",
|
|
628
|
+
"server/application": "src/features",
|
|
629
|
+
"server/infra": "src/shared/api",
|
|
630
|
+
"client/widgets": "src/widgets",
|
|
631
|
+
"client/features": "src/features",
|
|
632
|
+
"shared": "src/shared",
|
|
633
|
+
"app/api": "src/app/api",
|
|
634
|
+
},
|
|
635
|
+
clean: {
|
|
636
|
+
"server/domain": "src/domain",
|
|
637
|
+
"server/application": "src/application",
|
|
638
|
+
"server/infra": "src/infrastructure",
|
|
639
|
+
"client/widgets": "src/presentation/components",
|
|
640
|
+
"client/features": "src/presentation/features",
|
|
641
|
+
"shared": "src/shared",
|
|
642
|
+
"app/api": "src/interfaces/http",
|
|
643
|
+
},
|
|
644
|
+
hexagonal: {
|
|
645
|
+
"server/domain": "src/domain",
|
|
646
|
+
"server/application": "src/application",
|
|
647
|
+
"server/infra": "src/adapters",
|
|
648
|
+
"client/widgets": "src/adapters/primary/ui",
|
|
649
|
+
"client/features": "src/adapters/primary/ui",
|
|
650
|
+
"shared": "src/shared",
|
|
651
|
+
"app/api": "src/adapters/primary/api",
|
|
652
|
+
},
|
|
653
|
+
atomic: {
|
|
654
|
+
"server/domain": "src/services",
|
|
655
|
+
"server/application": "src/hooks",
|
|
656
|
+
"server/infra": "src/api",
|
|
657
|
+
"client/widgets": "src/components/organisms",
|
|
658
|
+
"client/features": "src/components/templates",
|
|
659
|
+
"shared": "src/utils",
|
|
660
|
+
"app/api": "src/api",
|
|
661
|
+
},
|
|
662
|
+
mandu: {}, // 기본값, 매핑 불필요
|
|
663
|
+
};
|
|
664
|
+
|
|
665
|
+
const mapping = pathMappings[preset] || {};
|
|
666
|
+
if (Object.keys(mapping).length === 0) {
|
|
667
|
+
return structure;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
return structure.map((dir) => {
|
|
671
|
+
// 경로 매핑 적용
|
|
672
|
+
let newPath = dir.path;
|
|
673
|
+
for (const [from, to] of Object.entries(mapping)) {
|
|
674
|
+
if (dir.path.startsWith(from)) {
|
|
675
|
+
newPath = dir.path.replace(from, to);
|
|
676
|
+
break;
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
return {
|
|
681
|
+
...dir,
|
|
682
|
+
path: newPath,
|
|
683
|
+
};
|
|
684
|
+
});
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
688
|
+
// Core Functions
|
|
689
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
690
|
+
|
|
691
|
+
/**
|
|
692
|
+
* 아키텍처 협상 수행
|
|
693
|
+
*/
|
|
694
|
+
export async function negotiate(
|
|
695
|
+
request: NegotiationRequest,
|
|
696
|
+
rootDir: string
|
|
697
|
+
): Promise<NegotiationResponse> {
|
|
698
|
+
const {
|
|
699
|
+
intent,
|
|
700
|
+
requirements = [],
|
|
701
|
+
constraints = [],
|
|
702
|
+
preset = "mandu",
|
|
703
|
+
context,
|
|
704
|
+
} = request;
|
|
705
|
+
|
|
706
|
+
// 1. 카테고리 감지
|
|
707
|
+
const category = request.category || detectCategory(intent);
|
|
708
|
+
|
|
709
|
+
// 2. 기능 이름 추출
|
|
710
|
+
const featureName = extractFeatureName(intent);
|
|
711
|
+
|
|
712
|
+
// 3. 관련 결정 검색
|
|
713
|
+
const categoryTags = CATEGORY_KEYWORDS[category] || [];
|
|
714
|
+
const searchTags = [...categoryTags.slice(0, 3), featureName];
|
|
715
|
+
const decisionsResult = await searchDecisions(rootDir, searchTags);
|
|
716
|
+
|
|
717
|
+
// 4. 프리셋 정의 로드 및 구조 템플릿 선택
|
|
718
|
+
const presetDef = getPreset(preset);
|
|
719
|
+
const templateFn = STRUCTURE_TEMPLATES[category] || STRUCTURE_TEMPLATES.other;
|
|
720
|
+
let structure = templateFn(featureName);
|
|
721
|
+
|
|
722
|
+
// 5. 프리셋에 따른 구조 조정
|
|
723
|
+
if (presetDef && preset !== "mandu") {
|
|
724
|
+
structure = adjustStructureForPreset(structure, presetDef, preset);
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
// 6. 슬롯 추출
|
|
728
|
+
const slots: SlotProposal[] = structure
|
|
729
|
+
.flatMap((dir) => dir.files)
|
|
730
|
+
.filter((file) => file.isSlot)
|
|
731
|
+
.map((file) => ({
|
|
732
|
+
path: file.name,
|
|
733
|
+
purpose: file.purpose,
|
|
734
|
+
constraints: file.suggestedConstraints || ["input-validation", "error-handling"],
|
|
735
|
+
}));
|
|
736
|
+
|
|
737
|
+
// 7. 경고 및 권장사항 생성
|
|
738
|
+
const warnings: string[] = [];
|
|
739
|
+
const recommendations: string[] = [];
|
|
740
|
+
|
|
741
|
+
// 기존 결정과 충돌 확인
|
|
742
|
+
for (const decision of decisionsResult.decisions) {
|
|
743
|
+
if (decision.status === "deprecated") {
|
|
744
|
+
warnings.push(`⚠️ Related decision ${decision.id} is deprecated: ${decision.title}`);
|
|
745
|
+
}
|
|
746
|
+
if (decision.status === "accepted") {
|
|
747
|
+
recommendations.push(`📋 Follow ${decision.id}: ${decision.decision.slice(0, 100)}...`);
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
// 제약 조건 기반 권장사항
|
|
752
|
+
if (constraints.length > 0) {
|
|
753
|
+
recommendations.push(`Ensure compatibility with: ${constraints.join(", ")}`);
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
// 8. 다음 단계 안내
|
|
757
|
+
const nextSteps = [
|
|
758
|
+
`1. Review the proposed structure below`,
|
|
759
|
+
`2. Run \`mandu_generate_scaffold\` to create files`,
|
|
760
|
+
`3. Implement the TODO sections in each file`,
|
|
761
|
+
`4. Run \`mandu_guard_heal\` to verify architecture compliance`,
|
|
762
|
+
];
|
|
763
|
+
|
|
764
|
+
// 9. 파일 수 계산
|
|
765
|
+
const estimatedFiles = structure.reduce((sum, dir) => sum + dir.files.length, 0);
|
|
766
|
+
|
|
767
|
+
// 10. 관련 결정 포맷
|
|
768
|
+
const relatedDecisions: RelatedDecision[] = decisionsResult.decisions.map((d) => ({
|
|
769
|
+
id: d.id,
|
|
770
|
+
title: d.title,
|
|
771
|
+
summary: d.decision.slice(0, 150),
|
|
772
|
+
relevance: `Related to ${category} implementation`,
|
|
773
|
+
}));
|
|
774
|
+
|
|
775
|
+
return {
|
|
776
|
+
approved: true,
|
|
777
|
+
structure,
|
|
778
|
+
slots,
|
|
779
|
+
warnings,
|
|
780
|
+
recommendations,
|
|
781
|
+
relatedDecisions,
|
|
782
|
+
estimatedFiles,
|
|
783
|
+
preset,
|
|
784
|
+
nextSteps,
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
/**
|
|
789
|
+
* Scaffold 생성 (병렬 처리 최적화)
|
|
790
|
+
*/
|
|
791
|
+
export async function generateScaffold(
|
|
792
|
+
structure: DirectoryProposal[],
|
|
793
|
+
rootDir: string,
|
|
794
|
+
options: { overwrite?: boolean; dryRun?: boolean } = {}
|
|
795
|
+
): Promise<ScaffoldResult> {
|
|
796
|
+
const { overwrite = false, dryRun = false } = options;
|
|
797
|
+
|
|
798
|
+
const createdDirs: string[] = [];
|
|
799
|
+
const createdFiles: string[] = [];
|
|
800
|
+
const skippedFiles: string[] = [];
|
|
801
|
+
const errors: string[] = [];
|
|
802
|
+
|
|
803
|
+
// 1단계: 모든 디렉토리 먼저 생성 (병렬)
|
|
804
|
+
const dirPaths = new Set<string>();
|
|
805
|
+
for (const dir of structure) {
|
|
806
|
+
dirPaths.add(join(rootDir, dir.path));
|
|
807
|
+
// nested file 경로의 부모 디렉토리도 추가
|
|
808
|
+
for (const file of dir.files) {
|
|
809
|
+
dirPaths.add(dirname(join(rootDir, dir.path, file.name)));
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
if (!dryRun) {
|
|
814
|
+
const dirResults = await Promise.allSettled(
|
|
815
|
+
Array.from(dirPaths).map(async (dirPath) => {
|
|
816
|
+
await mkdir(dirPath, { recursive: true });
|
|
817
|
+
return dirPath;
|
|
818
|
+
})
|
|
819
|
+
);
|
|
820
|
+
|
|
821
|
+
for (const result of dirResults) {
|
|
822
|
+
if (result.status === "fulfilled") {
|
|
823
|
+
const relativePath = result.value.replace(rootDir, "").replace(/^[/\\]/, "");
|
|
824
|
+
if (relativePath) createdDirs.push(relativePath);
|
|
825
|
+
} else {
|
|
826
|
+
errors.push(`Failed to create directory: ${result.reason}`);
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
} else {
|
|
830
|
+
structure.forEach((dir) => createdDirs.push(dir.path));
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
// 2단계: 모든 파일 정보 수집 및 병렬 처리
|
|
834
|
+
interface FileTask {
|
|
835
|
+
filePath: string;
|
|
836
|
+
relativePath: string;
|
|
837
|
+
content: string;
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
const fileTasks: FileTask[] = [];
|
|
841
|
+
|
|
842
|
+
for (const dir of structure) {
|
|
843
|
+
const dirPath = join(rootDir, dir.path);
|
|
844
|
+
|
|
845
|
+
for (const file of dir.files) {
|
|
846
|
+
const filePath = join(dirPath, file.name);
|
|
847
|
+
const relativePath = join(dir.path, file.name);
|
|
848
|
+
const content = generateFileContent(
|
|
849
|
+
file.template || "util",
|
|
850
|
+
file.name.replace(/\.\w+$/, ""),
|
|
851
|
+
file.purpose
|
|
852
|
+
);
|
|
853
|
+
|
|
854
|
+
fileTasks.push({ filePath, relativePath, content });
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
// 3단계: 파일 존재 여부 확인 (병렬)
|
|
859
|
+
const existsResults = await Promise.allSettled(
|
|
860
|
+
fileTasks.map(async (task) => {
|
|
861
|
+
try {
|
|
862
|
+
await stat(task.filePath);
|
|
863
|
+
return { ...task, exists: true };
|
|
864
|
+
} catch {
|
|
865
|
+
return { ...task, exists: false };
|
|
866
|
+
}
|
|
867
|
+
})
|
|
868
|
+
);
|
|
869
|
+
|
|
870
|
+
// 4단계: 파일 쓰기 (병렬)
|
|
871
|
+
const writePromises: Promise<void>[] = [];
|
|
872
|
+
|
|
873
|
+
for (const result of existsResults) {
|
|
874
|
+
if (result.status !== "fulfilled") continue;
|
|
875
|
+
const { filePath, relativePath, content, exists } = result.value;
|
|
876
|
+
|
|
877
|
+
if (exists && !overwrite) {
|
|
878
|
+
skippedFiles.push(relativePath);
|
|
879
|
+
continue;
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
if (dryRun) {
|
|
883
|
+
createdFiles.push(relativePath);
|
|
884
|
+
} else {
|
|
885
|
+
writePromises.push(
|
|
886
|
+
writeFile(filePath, content, "utf-8")
|
|
887
|
+
.then(() => {
|
|
888
|
+
createdFiles.push(relativePath);
|
|
889
|
+
})
|
|
890
|
+
.catch((error) => {
|
|
891
|
+
errors.push(`Failed to create file ${relativePath}: ${error}`);
|
|
892
|
+
})
|
|
893
|
+
);
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
// 모든 쓰기 작업 완료 대기
|
|
898
|
+
await Promise.allSettled(writePromises);
|
|
899
|
+
|
|
900
|
+
return {
|
|
901
|
+
success: errors.length === 0,
|
|
902
|
+
createdDirs,
|
|
903
|
+
createdFiles,
|
|
904
|
+
skippedFiles,
|
|
905
|
+
errors,
|
|
906
|
+
};
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
/**
|
|
910
|
+
* 기존 프로젝트 구조 분석
|
|
911
|
+
*/
|
|
912
|
+
export async function analyzeExistingStructure(
|
|
913
|
+
rootDir: string
|
|
914
|
+
): Promise<{
|
|
915
|
+
layers: string[];
|
|
916
|
+
existingFeatures: string[];
|
|
917
|
+
recommendations: string[];
|
|
918
|
+
}> {
|
|
919
|
+
const layers: string[] = [];
|
|
920
|
+
const existingFeatures: string[] = [];
|
|
921
|
+
const recommendations: string[] = [];
|
|
922
|
+
|
|
923
|
+
// 일반적인 레이어 디렉토리 확인
|
|
924
|
+
const commonLayers = [
|
|
925
|
+
"server/domain",
|
|
926
|
+
"server/application",
|
|
927
|
+
"server/infra",
|
|
928
|
+
"client/features",
|
|
929
|
+
"client/widgets",
|
|
930
|
+
"client/shared",
|
|
931
|
+
"shared",
|
|
932
|
+
"app/api",
|
|
933
|
+
];
|
|
934
|
+
|
|
935
|
+
for (const layer of commonLayers) {
|
|
936
|
+
try {
|
|
937
|
+
const layerPath = join(rootDir, layer);
|
|
938
|
+
const stats = await stat(layerPath);
|
|
939
|
+
if (stats.isDirectory()) {
|
|
940
|
+
layers.push(layer);
|
|
941
|
+
|
|
942
|
+
// 하위 디렉토리 (feature) 목록
|
|
943
|
+
const entries = await readdir(layerPath);
|
|
944
|
+
for (const entry of entries) {
|
|
945
|
+
const entryPath = join(layerPath, entry);
|
|
946
|
+
const entryStats = await stat(entryPath);
|
|
947
|
+
if (entryStats.isDirectory()) {
|
|
948
|
+
existingFeatures.push(`${layer}/${entry}`);
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
} catch {
|
|
953
|
+
// 레이어 없음
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
// 권장사항 생성
|
|
958
|
+
if (layers.length === 0) {
|
|
959
|
+
recommendations.push("No standard layers found. Consider using Mandu preset structure.");
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
if (!layers.includes("server/domain")) {
|
|
963
|
+
recommendations.push("Missing server/domain layer for business logic isolation.");
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
if (!layers.includes("shared")) {
|
|
967
|
+
recommendations.push("Consider adding shared/ for cross-cutting utilities.");
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
return {
|
|
971
|
+
layers,
|
|
972
|
+
existingFeatures,
|
|
973
|
+
recommendations,
|
|
974
|
+
};
|
|
975
|
+
}
|