@mandujs/core 0.12.0 → 0.12.2
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.ko.md +81 -4
- package/README.md +81 -0
- package/package.json +2 -2
- package/src/guard/healing.ts +2 -0
- package/src/guard/index.ts +2 -0
- package/src/guard/negotiation.ts +430 -4
- package/src/guard/presets/cqrs.test.ts +154 -0
- package/src/guard/presets/cqrs.ts +107 -0
- package/src/guard/presets/index.ts +3 -0
- package/src/guard/suggestions.ts +6 -0
- package/src/guard/types.ts +1 -0
package/README.ko.md
CHANGED
|
@@ -25,11 +25,16 @@ bun add @mandujs/core
|
|
|
25
25
|
|
|
26
26
|
```
|
|
27
27
|
@mandujs/core
|
|
28
|
-
├──
|
|
29
|
-
├── generator/ # 코드 생성
|
|
28
|
+
├── router/ # 파일 시스템 기반 라우팅
|
|
30
29
|
├── guard/ # 아키텍처 검사 및 자동 수정
|
|
31
|
-
├── runtime/ #
|
|
32
|
-
|
|
30
|
+
├── runtime/ # 서버, SSR, 스트리밍
|
|
31
|
+
├── filling/ # 핸들러 체인 API
|
|
32
|
+
├── contract/ # 타입 안전 API 계약
|
|
33
|
+
├── content/ # Content Layer - 빌드 타임 콘텐츠 로딩 🆕
|
|
34
|
+
├── bundler/ # 클라이언트 번들링, HMR
|
|
35
|
+
├── client/ # Island 하이드레이션, 클라이언트 라우터
|
|
36
|
+
├── brain/ # Doctor, Watcher, 아키텍처 분석
|
|
37
|
+
└── change/ # 트랜잭션 & 히스토리
|
|
33
38
|
```
|
|
34
39
|
|
|
35
40
|
## Spec 모듈
|
|
@@ -133,6 +138,73 @@ if (!result.passed) {
|
|
|
133
138
|
| `COMPONENT_NOT_FOUND` | 컴포넌트 파일 없음 | ❌ |
|
|
134
139
|
| `SLOT_NOT_FOUND` | slot 파일 없음 | ✅ |
|
|
135
140
|
|
|
141
|
+
## Content Layer 🆕
|
|
142
|
+
|
|
143
|
+
Astro에서 영감받은 빌드 타임 콘텐츠 로딩 시스템.
|
|
144
|
+
|
|
145
|
+
```typescript
|
|
146
|
+
// content.config.ts
|
|
147
|
+
import { defineContentConfig, glob, file, api } from "@mandujs/core/content";
|
|
148
|
+
import { z } from "zod";
|
|
149
|
+
|
|
150
|
+
const postSchema = z.object({
|
|
151
|
+
title: z.string(),
|
|
152
|
+
date: z.coerce.date(),
|
|
153
|
+
tags: z.array(z.string()).default([]),
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
export default defineContentConfig({
|
|
157
|
+
collections: {
|
|
158
|
+
// Markdown 파일 (프론트매터 지원)
|
|
159
|
+
posts: {
|
|
160
|
+
loader: glob({ pattern: "content/posts/**/*.md" }),
|
|
161
|
+
schema: postSchema,
|
|
162
|
+
},
|
|
163
|
+
// 단일 JSON/YAML 파일
|
|
164
|
+
settings: {
|
|
165
|
+
loader: file({ path: "data/settings.json" }),
|
|
166
|
+
},
|
|
167
|
+
// 외부 API
|
|
168
|
+
products: {
|
|
169
|
+
loader: api({
|
|
170
|
+
url: "https://api.example.com/products",
|
|
171
|
+
cacheTTL: 3600,
|
|
172
|
+
}),
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
});
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### 콘텐츠 조회
|
|
179
|
+
|
|
180
|
+
```typescript
|
|
181
|
+
import { getCollection, getEntry } from "@mandujs/core/content";
|
|
182
|
+
|
|
183
|
+
// 전체 컬렉션 조회
|
|
184
|
+
const posts = await getCollection("posts");
|
|
185
|
+
|
|
186
|
+
// 단일 엔트리 조회
|
|
187
|
+
const post = await getEntry("posts", "hello-world");
|
|
188
|
+
console.log(post?.data.title, post?.body);
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
### 내장 로더
|
|
192
|
+
|
|
193
|
+
| 로더 | 설명 | 예시 |
|
|
194
|
+
|------|------|------|
|
|
195
|
+
| `file()` | 단일 파일 (JSON, YAML, TOML) | `file({ path: "data/config.json" })` |
|
|
196
|
+
| `glob()` | 패턴 매칭 (Markdown, JSON) | `glob({ pattern: "content/**/*.md" })` |
|
|
197
|
+
| `api()` | HTTP API (캐싱 지원) | `api({ url: "https://...", cacheTTL: 3600 })` |
|
|
198
|
+
|
|
199
|
+
### 주요 기능
|
|
200
|
+
|
|
201
|
+
- **Digest 기반 캐싱**: 변경된 파일만 재파싱
|
|
202
|
+
- **Zod 검증**: 스키마 기반 타입 안전 콘텐츠
|
|
203
|
+
- **프론트매터 지원**: Markdown YAML 프론트매터
|
|
204
|
+
- **Dev 모드 감시**: 콘텐츠 변경 시 자동 리로드
|
|
205
|
+
|
|
206
|
+
---
|
|
207
|
+
|
|
136
208
|
## Contract 모듈
|
|
137
209
|
|
|
138
210
|
Zod 기반 계약(Contract) 정의 및 타입 안전 클라이언트 생성.
|
|
@@ -209,6 +281,11 @@ import type {
|
|
|
209
281
|
GuardViolation,
|
|
210
282
|
GenerateResult,
|
|
211
283
|
AutoCorrectResult,
|
|
284
|
+
// Content Layer
|
|
285
|
+
DataEntry,
|
|
286
|
+
ContentConfig,
|
|
287
|
+
CollectionConfig,
|
|
288
|
+
Loader,
|
|
212
289
|
} from "@mandujs/core";
|
|
213
290
|
```
|
|
214
291
|
|
package/README.md
CHANGED
|
@@ -81,6 +81,8 @@ const handlers = Mandu.handler(userContract, {
|
|
|
81
81
|
├── runtime/ # Server, SSR, streaming
|
|
82
82
|
├── filling/ # Handler chain API (Mandu.filling())
|
|
83
83
|
├── contract/ # Type-safe API contracts
|
|
84
|
+
├── content/ # Content Layer - build-time content loading 🆕
|
|
85
|
+
│ └── loaders # file(), glob(), api() loaders
|
|
84
86
|
├── bundler/ # Client bundling, HMR
|
|
85
87
|
├── client/ # Island hydration, client router
|
|
86
88
|
├── brain/ # Doctor, Watcher, Architecture analyzer
|
|
@@ -453,6 +455,78 @@ function Navigation() {
|
|
|
453
455
|
|
|
454
456
|
---
|
|
455
457
|
|
|
458
|
+
## Content Layer 🆕
|
|
459
|
+
|
|
460
|
+
Astro-inspired build-time content loading system.
|
|
461
|
+
|
|
462
|
+
```typescript
|
|
463
|
+
// content.config.ts
|
|
464
|
+
import { defineContentConfig, glob, file, api } from "@mandujs/core/content";
|
|
465
|
+
import { z } from "zod";
|
|
466
|
+
|
|
467
|
+
const postSchema = z.object({
|
|
468
|
+
title: z.string(),
|
|
469
|
+
date: z.coerce.date(),
|
|
470
|
+
tags: z.array(z.string()).default([]),
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
export default defineContentConfig({
|
|
474
|
+
collections: {
|
|
475
|
+
// Markdown files with frontmatter
|
|
476
|
+
posts: {
|
|
477
|
+
loader: glob({ pattern: "content/posts/**/*.md" }),
|
|
478
|
+
schema: postSchema,
|
|
479
|
+
},
|
|
480
|
+
// Single JSON/YAML file
|
|
481
|
+
settings: {
|
|
482
|
+
loader: file({ path: "data/settings.json" }),
|
|
483
|
+
},
|
|
484
|
+
// External API
|
|
485
|
+
products: {
|
|
486
|
+
loader: api({
|
|
487
|
+
url: "https://api.example.com/products",
|
|
488
|
+
headers: () => ({ Authorization: `Bearer ${process.env.API_KEY}` }),
|
|
489
|
+
cacheTTL: 3600,
|
|
490
|
+
}),
|
|
491
|
+
},
|
|
492
|
+
},
|
|
493
|
+
});
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
### Querying Content
|
|
497
|
+
|
|
498
|
+
```typescript
|
|
499
|
+
import { getCollection, getEntry } from "@mandujs/core/content";
|
|
500
|
+
|
|
501
|
+
// Get all entries
|
|
502
|
+
const posts = await getCollection("posts");
|
|
503
|
+
posts.forEach(post => {
|
|
504
|
+
console.log(post.id, post.data.title);
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
// Get single entry
|
|
508
|
+
const post = await getEntry("posts", "hello-world");
|
|
509
|
+
console.log(post?.data.title, post?.body);
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
### Built-in Loaders
|
|
513
|
+
|
|
514
|
+
| Loader | Description | Example |
|
|
515
|
+
|--------|-------------|---------|
|
|
516
|
+
| `file()` | Single file (JSON, YAML, TOML) | `file({ path: "data/config.json" })` |
|
|
517
|
+
| `glob()` | Pattern matching (Markdown, JSON) | `glob({ pattern: "content/**/*.md" })` |
|
|
518
|
+
| `api()` | HTTP API with caching | `api({ url: "https://...", cacheTTL: 3600 })` |
|
|
519
|
+
|
|
520
|
+
### Features
|
|
521
|
+
|
|
522
|
+
- **Digest-based caching**: Only re-parse changed files
|
|
523
|
+
- **Zod validation**: Type-safe content with schema validation
|
|
524
|
+
- **Frontmatter support**: YAML frontmatter in Markdown files
|
|
525
|
+
- **Dev mode watching**: Auto-reload on content changes
|
|
526
|
+
- **Incremental updates**: Efficient builds with change detection
|
|
527
|
+
|
|
528
|
+
---
|
|
529
|
+
|
|
456
530
|
## Brain (AI Assistant)
|
|
457
531
|
|
|
458
532
|
Doctor and architecture analyzer.
|
|
@@ -551,6 +625,13 @@ import type {
|
|
|
551
625
|
|
|
552
626
|
// Filling
|
|
553
627
|
ManduContext,
|
|
628
|
+
|
|
629
|
+
// Content Layer
|
|
630
|
+
DataEntry,
|
|
631
|
+
ContentConfig,
|
|
632
|
+
CollectionConfig,
|
|
633
|
+
Loader,
|
|
634
|
+
LoaderContext,
|
|
554
635
|
} from "@mandujs/core";
|
|
555
636
|
```
|
|
556
637
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mandujs/core",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.2",
|
|
4
4
|
"description": "Mandu Framework Core - Spec, Generator, Guard, Runtime",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"directory": "packages/core"
|
|
36
36
|
},
|
|
37
37
|
"author": "konamgil",
|
|
38
|
-
"license": "
|
|
38
|
+
"license": "MPL-2.0",
|
|
39
39
|
"publishConfig": {
|
|
40
40
|
"access": "public"
|
|
41
41
|
},
|
package/src/guard/healing.ts
CHANGED
|
@@ -669,6 +669,8 @@ function getLayerHierarchy(preset?: GuardPreset): string {
|
|
|
669
669
|
return "adapters → ports → domain";
|
|
670
670
|
case "atomic":
|
|
671
671
|
return "pages → templates → organisms → molecules → atoms";
|
|
672
|
+
case "cqrs":
|
|
673
|
+
return "api → commands|queries → dto/events → domain → shared";
|
|
672
674
|
case "mandu":
|
|
673
675
|
return "client(FSD) | shared | server(Clean)";
|
|
674
676
|
default:
|
package/src/guard/index.ts
CHANGED
|
@@ -139,11 +139,13 @@ export {
|
|
|
139
139
|
cleanPreset,
|
|
140
140
|
hexagonalPreset,
|
|
141
141
|
atomicPreset,
|
|
142
|
+
cqrsPreset,
|
|
142
143
|
manduPreset,
|
|
143
144
|
FSD_HIERARCHY,
|
|
144
145
|
CLEAN_HIERARCHY,
|
|
145
146
|
HEXAGONAL_HIERARCHY,
|
|
146
147
|
ATOMIC_HIERARCHY,
|
|
148
|
+
CQRS_HIERARCHY,
|
|
147
149
|
} from "./presets";
|
|
148
150
|
|
|
149
151
|
// ═══════════════════════════════════════════════════════════════════════════
|
package/src/guard/negotiation.ts
CHANGED
|
@@ -118,7 +118,11 @@ export type FileTemplate =
|
|
|
118
118
|
| "util"
|
|
119
119
|
| "type"
|
|
120
120
|
| "test"
|
|
121
|
-
| "slot"
|
|
121
|
+
| "slot"
|
|
122
|
+
| "command"
|
|
123
|
+
| "query"
|
|
124
|
+
| "event"
|
|
125
|
+
| "dto";
|
|
122
126
|
|
|
123
127
|
/**
|
|
124
128
|
* 협상 응답
|
|
@@ -438,6 +442,341 @@ const STRUCTURE_TEMPLATES: Record<FeatureCategory, (featureName: string) => Dire
|
|
|
438
442
|
],
|
|
439
443
|
};
|
|
440
444
|
|
|
445
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
446
|
+
// CQRS Structure Templates
|
|
447
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* CQRS 프리셋 전용 구조 템플릿
|
|
451
|
+
*
|
|
452
|
+
* application 레이어를 commands/queries/dto/events/mappers로 세분화
|
|
453
|
+
*/
|
|
454
|
+
const CQRS_STRUCTURE_TEMPLATES: Record<FeatureCategory, (featureName: string) => DirectoryProposal[]> = {
|
|
455
|
+
auth: (name) => [
|
|
456
|
+
{
|
|
457
|
+
path: `src/domain/${name}`,
|
|
458
|
+
purpose: `${name} 도메인 모델`,
|
|
459
|
+
layer: "domain",
|
|
460
|
+
files: [
|
|
461
|
+
{ name: `${name}.entity.ts`, purpose: "사용자/인증 엔티티", template: "type" },
|
|
462
|
+
{ name: `${name}.service.ts`, purpose: "도메인 서비스 인터페이스", template: "service" },
|
|
463
|
+
{ name: `${name}.repository.ts`, purpose: "Repository 인터페이스", template: "repository" },
|
|
464
|
+
],
|
|
465
|
+
},
|
|
466
|
+
{
|
|
467
|
+
path: `src/application/commands/${name}`,
|
|
468
|
+
purpose: `${name} 쓰기 경로`,
|
|
469
|
+
layer: "application/commands",
|
|
470
|
+
files: [
|
|
471
|
+
{ name: `login.command.ts`, purpose: "로그인 커맨드 핸들러", template: "command" },
|
|
472
|
+
{ name: `logout.command.ts`, purpose: "로그아웃 커맨드 핸들러", template: "command" },
|
|
473
|
+
{ name: `refresh-token.command.ts`, purpose: "토큰 갱신 커맨드 핸들러", template: "command" },
|
|
474
|
+
],
|
|
475
|
+
},
|
|
476
|
+
{
|
|
477
|
+
path: `src/application/queries/${name}`,
|
|
478
|
+
purpose: `${name} 읽기 경로`,
|
|
479
|
+
layer: "application/queries",
|
|
480
|
+
files: [
|
|
481
|
+
{ name: `get-session.query.ts`, purpose: "세션 조회 쿼리 핸들러", template: "query" },
|
|
482
|
+
{ name: `verify-token.query.ts`, purpose: "토큰 검증 쿼리 핸들러", template: "query" },
|
|
483
|
+
],
|
|
484
|
+
},
|
|
485
|
+
{
|
|
486
|
+
path: `src/application/dto/${name}`,
|
|
487
|
+
purpose: `${name} DTO`,
|
|
488
|
+
layer: "application/dto",
|
|
489
|
+
files: [
|
|
490
|
+
{ name: `login.dto.ts`, purpose: "로그인 요청/응답 DTO", template: "dto" },
|
|
491
|
+
{ name: `token.dto.ts`, purpose: "토큰 DTO", template: "dto" },
|
|
492
|
+
],
|
|
493
|
+
},
|
|
494
|
+
{
|
|
495
|
+
path: `src/application/events/${name}`,
|
|
496
|
+
purpose: `${name} 도메인 이벤트`,
|
|
497
|
+
layer: "application/events",
|
|
498
|
+
files: [
|
|
499
|
+
{ name: `user-logged-in.event.ts`, purpose: "로그인 성공 이벤트", template: "event" },
|
|
500
|
+
{ name: `user-logged-out.event.ts`, purpose: "로그아웃 이벤트", template: "event" },
|
|
501
|
+
],
|
|
502
|
+
},
|
|
503
|
+
{
|
|
504
|
+
path: `src/infra/${name}`,
|
|
505
|
+
purpose: `${name} 인프라 어댑터`,
|
|
506
|
+
layer: "infrastructure",
|
|
507
|
+
files: [
|
|
508
|
+
{ name: `token.provider.ts`, purpose: "토큰 생성/검증 구현", template: "service" },
|
|
509
|
+
{ name: `session.repository.ts`, purpose: "세션 저장소 구현", template: "repository" },
|
|
510
|
+
],
|
|
511
|
+
},
|
|
512
|
+
{
|
|
513
|
+
path: `src/api/${name}`,
|
|
514
|
+
purpose: `${name} API 라우트`,
|
|
515
|
+
layer: "api",
|
|
516
|
+
files: [
|
|
517
|
+
{ name: `login/route.ts`, purpose: "로그인 API → LoginCommand 디스패치", template: "route", isSlot: true },
|
|
518
|
+
{ name: `logout/route.ts`, purpose: "로그아웃 API → LogoutCommand 디스패치", template: "route", isSlot: true },
|
|
519
|
+
{ name: `refresh/route.ts`, purpose: "토큰 갱신 API → RefreshTokenCommand 디스패치", template: "route", isSlot: true },
|
|
520
|
+
{ name: `session/route.ts`, purpose: "세션 조회 API → GetSessionQuery 디스패치", template: "route", isSlot: true },
|
|
521
|
+
],
|
|
522
|
+
},
|
|
523
|
+
],
|
|
524
|
+
|
|
525
|
+
crud: (name) => [
|
|
526
|
+
{
|
|
527
|
+
path: `src/domain/${name}`,
|
|
528
|
+
purpose: `${name} 도메인`,
|
|
529
|
+
layer: "domain",
|
|
530
|
+
files: [
|
|
531
|
+
{ name: `${name}.entity.ts`, purpose: "엔티티 정의", template: "type" },
|
|
532
|
+
{ name: `${name}.repository.ts`, purpose: "Repository 인터페이스", template: "repository" },
|
|
533
|
+
],
|
|
534
|
+
},
|
|
535
|
+
{
|
|
536
|
+
path: `src/application/commands/${name}`,
|
|
537
|
+
purpose: `${name} 쓰기 커맨드`,
|
|
538
|
+
layer: "application/commands",
|
|
539
|
+
files: [
|
|
540
|
+
{ name: `create-${name}.command.ts`, purpose: "생성 커맨드 핸들러", template: "command" },
|
|
541
|
+
{ name: `update-${name}.command.ts`, purpose: "수정 커맨드 핸들러", template: "command" },
|
|
542
|
+
{ name: `delete-${name}.command.ts`, purpose: "삭제 커맨드 핸들러", template: "command" },
|
|
543
|
+
],
|
|
544
|
+
},
|
|
545
|
+
{
|
|
546
|
+
path: `src/application/queries/${name}`,
|
|
547
|
+
purpose: `${name} 읽기 쿼리`,
|
|
548
|
+
layer: "application/queries",
|
|
549
|
+
files: [
|
|
550
|
+
{ name: `get-${name}.query.ts`, purpose: "단건 조회 쿼리 핸들러", template: "query" },
|
|
551
|
+
{ name: `list-${name}.query.ts`, purpose: "목록 조회 쿼리 핸들러", template: "query" },
|
|
552
|
+
],
|
|
553
|
+
},
|
|
554
|
+
{
|
|
555
|
+
path: `src/application/dto/${name}`,
|
|
556
|
+
purpose: `${name} DTO`,
|
|
557
|
+
layer: "application/dto",
|
|
558
|
+
files: [
|
|
559
|
+
{ name: `create-${name}.dto.ts`, purpose: "생성 요청 DTO", template: "dto" },
|
|
560
|
+
{ name: `update-${name}.dto.ts`, purpose: "수정 요청 DTO", template: "dto" },
|
|
561
|
+
{ name: `${name}-response.dto.ts`, purpose: "응답 DTO", template: "dto" },
|
|
562
|
+
],
|
|
563
|
+
},
|
|
564
|
+
{
|
|
565
|
+
path: `src/infra/${name}`,
|
|
566
|
+
purpose: `${name} Repository 구현`,
|
|
567
|
+
layer: "infrastructure",
|
|
568
|
+
files: [
|
|
569
|
+
{ name: `${name}.repository-impl.ts`, purpose: "Repository 구현체", template: "repository" },
|
|
570
|
+
],
|
|
571
|
+
},
|
|
572
|
+
{
|
|
573
|
+
path: `src/api/${name}`,
|
|
574
|
+
purpose: `${name} API`,
|
|
575
|
+
layer: "api",
|
|
576
|
+
files: [
|
|
577
|
+
{ name: `route.ts`, purpose: "목록/생성 API (GET→ListQuery, POST→CreateCommand)", template: "route", isSlot: true },
|
|
578
|
+
{ name: `[id]/route.ts`, purpose: "상세/수정/삭제 API (GET→GetQuery, PUT→UpdateCommand, DELETE→DeleteCommand)", template: "route", isSlot: true },
|
|
579
|
+
],
|
|
580
|
+
},
|
|
581
|
+
],
|
|
582
|
+
|
|
583
|
+
api: (name) => [
|
|
584
|
+
{
|
|
585
|
+
path: `src/domain/${name}`,
|
|
586
|
+
purpose: `${name} 도메인`,
|
|
587
|
+
layer: "domain",
|
|
588
|
+
files: [
|
|
589
|
+
{ name: `${name}.service.ts`, purpose: "도메인 서비스", template: "service" },
|
|
590
|
+
{ name: `${name}.types.ts`, purpose: "타입 정의", template: "type" },
|
|
591
|
+
],
|
|
592
|
+
},
|
|
593
|
+
{
|
|
594
|
+
path: `src/application/commands/${name}`,
|
|
595
|
+
purpose: `${name} 커맨드`,
|
|
596
|
+
layer: "application/commands",
|
|
597
|
+
files: [
|
|
598
|
+
{ name: `${name}.command.ts`, purpose: "커맨드 핸들러", template: "command" },
|
|
599
|
+
],
|
|
600
|
+
},
|
|
601
|
+
{
|
|
602
|
+
path: `src/application/queries/${name}`,
|
|
603
|
+
purpose: `${name} 쿼리`,
|
|
604
|
+
layer: "application/queries",
|
|
605
|
+
files: [
|
|
606
|
+
{ name: `${name}.query.ts`, purpose: "쿼리 핸들러", template: "query" },
|
|
607
|
+
],
|
|
608
|
+
},
|
|
609
|
+
{
|
|
610
|
+
path: `src/api/${name}`,
|
|
611
|
+
purpose: `${name} API 엔드포인트`,
|
|
612
|
+
layer: "api",
|
|
613
|
+
files: [
|
|
614
|
+
{ name: `route.ts`, purpose: "API 핸들러 → Command/Query 디스패치", template: "route", isSlot: true },
|
|
615
|
+
],
|
|
616
|
+
},
|
|
617
|
+
],
|
|
618
|
+
|
|
619
|
+
ui: (name) => [
|
|
620
|
+
{
|
|
621
|
+
path: `src/application/queries/${name}`,
|
|
622
|
+
purpose: `${name} 데이터 조회`,
|
|
623
|
+
layer: "application/queries",
|
|
624
|
+
files: [
|
|
625
|
+
{ name: `get-${name}.query.ts`, purpose: "UI용 데이터 조회 쿼리", template: "query" },
|
|
626
|
+
],
|
|
627
|
+
},
|
|
628
|
+
{
|
|
629
|
+
path: `src/application/dto/${name}`,
|
|
630
|
+
purpose: `${name} DTO`,
|
|
631
|
+
layer: "application/dto",
|
|
632
|
+
files: [
|
|
633
|
+
{ name: `${name}-view.dto.ts`, purpose: "뷰 모델 DTO", template: "dto" },
|
|
634
|
+
],
|
|
635
|
+
},
|
|
636
|
+
{
|
|
637
|
+
path: `src/api/${name}`,
|
|
638
|
+
purpose: `${name} API`,
|
|
639
|
+
layer: "api",
|
|
640
|
+
files: [
|
|
641
|
+
{ name: `route.ts`, purpose: "UI 데이터 API", template: "route", isSlot: true },
|
|
642
|
+
],
|
|
643
|
+
},
|
|
644
|
+
],
|
|
645
|
+
|
|
646
|
+
integration: (name) => [
|
|
647
|
+
{
|
|
648
|
+
path: `src/domain/${name}`,
|
|
649
|
+
purpose: `${name} 도메인 포트`,
|
|
650
|
+
layer: "domain",
|
|
651
|
+
files: [
|
|
652
|
+
{ name: `${name}.port.ts`, purpose: "포트 인터페이스", template: "type" },
|
|
653
|
+
],
|
|
654
|
+
},
|
|
655
|
+
{
|
|
656
|
+
path: `src/application/commands/${name}`,
|
|
657
|
+
purpose: `${name} 동기화 커맨드`,
|
|
658
|
+
layer: "application/commands",
|
|
659
|
+
files: [
|
|
660
|
+
{ name: `sync-${name}.command.ts`, purpose: "외부 서비스 동기화 커맨드", template: "command" },
|
|
661
|
+
],
|
|
662
|
+
},
|
|
663
|
+
{
|
|
664
|
+
path: `src/application/events/${name}`,
|
|
665
|
+
purpose: `${name} 연동 이벤트`,
|
|
666
|
+
layer: "application/events",
|
|
667
|
+
files: [
|
|
668
|
+
{ name: `${name}-synced.event.ts`, purpose: "동기화 완료 이벤트", template: "event" },
|
|
669
|
+
],
|
|
670
|
+
},
|
|
671
|
+
{
|
|
672
|
+
path: `src/infra/${name}`,
|
|
673
|
+
purpose: `${name} 외부 서비스 어댑터`,
|
|
674
|
+
layer: "infrastructure",
|
|
675
|
+
files: [
|
|
676
|
+
{ name: `${name}.client.ts`, purpose: "외부 API 클라이언트", template: "service" },
|
|
677
|
+
{ name: `${name}.config.ts`, purpose: "연동 설정", template: "util" },
|
|
678
|
+
],
|
|
679
|
+
},
|
|
680
|
+
{
|
|
681
|
+
path: `src/api/webhooks/${name}`,
|
|
682
|
+
purpose: `${name} 웹훅`,
|
|
683
|
+
layer: "api",
|
|
684
|
+
files: [
|
|
685
|
+
{ name: `route.ts`, purpose: "웹훅 핸들러", template: "route", isSlot: true },
|
|
686
|
+
],
|
|
687
|
+
},
|
|
688
|
+
],
|
|
689
|
+
|
|
690
|
+
data: (name) => [
|
|
691
|
+
{
|
|
692
|
+
path: `src/domain/${name}`,
|
|
693
|
+
purpose: `${name} 데이터 처리 도메인`,
|
|
694
|
+
layer: "domain",
|
|
695
|
+
files: [
|
|
696
|
+
{ name: `${name}.types.ts`, purpose: "타입 정의", template: "type" },
|
|
697
|
+
],
|
|
698
|
+
},
|
|
699
|
+
{
|
|
700
|
+
path: `src/application/commands/${name}`,
|
|
701
|
+
purpose: `${name} 데이터 처리 커맨드`,
|
|
702
|
+
layer: "application/commands",
|
|
703
|
+
files: [
|
|
704
|
+
{ name: `import-${name}.command.ts`, purpose: "데이터 임포트 커맨드", template: "command" },
|
|
705
|
+
{ name: `transform-${name}.command.ts`, purpose: "데이터 변환 커맨드", template: "command" },
|
|
706
|
+
],
|
|
707
|
+
},
|
|
708
|
+
{
|
|
709
|
+
path: `src/application/queries/${name}`,
|
|
710
|
+
purpose: `${name} 데이터 조회`,
|
|
711
|
+
layer: "application/queries",
|
|
712
|
+
files: [
|
|
713
|
+
{ name: `export-${name}.query.ts`, purpose: "데이터 익스포트 쿼리", template: "query" },
|
|
714
|
+
],
|
|
715
|
+
},
|
|
716
|
+
{
|
|
717
|
+
path: `src/application/dto/${name}`,
|
|
718
|
+
purpose: `${name} DTO`,
|
|
719
|
+
layer: "application/dto",
|
|
720
|
+
files: [
|
|
721
|
+
{ name: `${name}-import.dto.ts`, purpose: "임포트 DTO", template: "dto" },
|
|
722
|
+
],
|
|
723
|
+
},
|
|
724
|
+
],
|
|
725
|
+
|
|
726
|
+
util: (name) => [
|
|
727
|
+
{
|
|
728
|
+
path: `src/shared/${name}`,
|
|
729
|
+
purpose: `${name} 유틸리티`,
|
|
730
|
+
layer: "shared",
|
|
731
|
+
files: [
|
|
732
|
+
{ name: `${name}.ts`, purpose: "유틸리티 함수", template: "util" },
|
|
733
|
+
{ name: `${name}.test.ts`, purpose: "테스트", template: "test" },
|
|
734
|
+
{ name: `index.ts`, purpose: "Public API", template: "util" },
|
|
735
|
+
],
|
|
736
|
+
},
|
|
737
|
+
],
|
|
738
|
+
|
|
739
|
+
config: (name) => [
|
|
740
|
+
{
|
|
741
|
+
path: `src/shared/config`,
|
|
742
|
+
purpose: "설정 관리",
|
|
743
|
+
layer: "shared",
|
|
744
|
+
files: [
|
|
745
|
+
{ name: `${name}.config.ts`, purpose: `${name} 설정`, template: "util" },
|
|
746
|
+
{ name: `${name}.schema.ts`, purpose: "설정 스키마 (Zod)", template: "type" },
|
|
747
|
+
],
|
|
748
|
+
},
|
|
749
|
+
],
|
|
750
|
+
|
|
751
|
+
other: (name) => [
|
|
752
|
+
{
|
|
753
|
+
path: `src/domain/${name}`,
|
|
754
|
+
purpose: `${name} 도메인`,
|
|
755
|
+
layer: "domain",
|
|
756
|
+
files: [
|
|
757
|
+
{ name: `${name}.service.ts`, purpose: "도메인 서비스", template: "service" },
|
|
758
|
+
{ name: `${name}.types.ts`, purpose: "타입 정의", template: "type" },
|
|
759
|
+
],
|
|
760
|
+
},
|
|
761
|
+
{
|
|
762
|
+
path: `src/application/commands/${name}`,
|
|
763
|
+
purpose: `${name} 커맨드`,
|
|
764
|
+
layer: "application/commands",
|
|
765
|
+
files: [
|
|
766
|
+
{ name: `${name}.command.ts`, purpose: "커맨드 핸들러", template: "command" },
|
|
767
|
+
],
|
|
768
|
+
},
|
|
769
|
+
{
|
|
770
|
+
path: `src/application/queries/${name}`,
|
|
771
|
+
purpose: `${name} 쿼리`,
|
|
772
|
+
layer: "application/queries",
|
|
773
|
+
files: [
|
|
774
|
+
{ name: `${name}.query.ts`, purpose: "쿼리 핸들러", template: "query" },
|
|
775
|
+
],
|
|
776
|
+
},
|
|
777
|
+
],
|
|
778
|
+
};
|
|
779
|
+
|
|
441
780
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
442
781
|
// File Templates
|
|
443
782
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -557,6 +896,91 @@ describe("${name}", () => {
|
|
|
557
896
|
expect(true).toBe(true);
|
|
558
897
|
});
|
|
559
898
|
});
|
|
899
|
+
`;
|
|
900
|
+
|
|
901
|
+
case "command":
|
|
902
|
+
return `/**
|
|
903
|
+
* ${purpose}
|
|
904
|
+
*
|
|
905
|
+
* Command Handler - 쓰기 경로
|
|
906
|
+
*/
|
|
907
|
+
|
|
908
|
+
export interface ${toPascalCase(name)}Command {
|
|
909
|
+
// TODO: Define command payload
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
export interface ${toPascalCase(name)}Result {
|
|
913
|
+
// TODO: Define command result
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
export class ${toPascalCase(name)}Handler {
|
|
917
|
+
async execute(command: ${toPascalCase(name)}Command): Promise<${toPascalCase(name)}Result> {
|
|
918
|
+
// TODO: Implement command handler
|
|
919
|
+
throw new Error("Not implemented");
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
`;
|
|
923
|
+
|
|
924
|
+
case "query":
|
|
925
|
+
return `/**
|
|
926
|
+
* ${purpose}
|
|
927
|
+
*
|
|
928
|
+
* Query Handler - 읽기 경로
|
|
929
|
+
*/
|
|
930
|
+
|
|
931
|
+
export interface ${toPascalCase(name)}Query {
|
|
932
|
+
// TODO: Define query parameters
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
export interface ${toPascalCase(name)}Result {
|
|
936
|
+
// TODO: Define query result
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
export class ${toPascalCase(name)}Handler {
|
|
940
|
+
async execute(query: ${toPascalCase(name)}Query): Promise<${toPascalCase(name)}Result> {
|
|
941
|
+
// TODO: Implement query handler
|
|
942
|
+
throw new Error("Not implemented");
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
`;
|
|
946
|
+
|
|
947
|
+
case "event":
|
|
948
|
+
return `/**
|
|
949
|
+
* ${purpose}
|
|
950
|
+
*
|
|
951
|
+
* Domain Event
|
|
952
|
+
*/
|
|
953
|
+
|
|
954
|
+
export interface ${toPascalCase(name)}Event {
|
|
955
|
+
readonly type: "${name}";
|
|
956
|
+
readonly occurredAt: Date;
|
|
957
|
+
// TODO: Define event payload
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
export function create${toPascalCase(name)}Event(
|
|
961
|
+
// TODO: Define factory parameters
|
|
962
|
+
): ${toPascalCase(name)}Event {
|
|
963
|
+
return {
|
|
964
|
+
type: "${name}",
|
|
965
|
+
occurredAt: new Date(),
|
|
966
|
+
};
|
|
967
|
+
}
|
|
968
|
+
`;
|
|
969
|
+
|
|
970
|
+
case "dto":
|
|
971
|
+
return `/**
|
|
972
|
+
* ${purpose}
|
|
973
|
+
*
|
|
974
|
+
* Data Transfer Object
|
|
975
|
+
*/
|
|
976
|
+
|
|
977
|
+
export interface ${toPascalCase(name)}Dto {
|
|
978
|
+
// TODO: Define DTO fields
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
export interface ${toPascalCase(name)}ResponseDto {
|
|
982
|
+
// TODO: Define response DTO fields
|
|
983
|
+
}
|
|
560
984
|
`;
|
|
561
985
|
|
|
562
986
|
case "util":
|
|
@@ -659,6 +1083,7 @@ function adjustStructureForPreset(
|
|
|
659
1083
|
"shared": "src/utils",
|
|
660
1084
|
"app/api": "src/api",
|
|
661
1085
|
},
|
|
1086
|
+
cqrs: {}, // CQRS 전용 템플릿이 자체 경로 사용
|
|
662
1087
|
mandu: {}, // 기본값, 매핑 불필요
|
|
663
1088
|
};
|
|
664
1089
|
|
|
@@ -716,11 +1141,12 @@ export async function negotiate(
|
|
|
716
1141
|
|
|
717
1142
|
// 4. 프리셋 정의 로드 및 구조 템플릿 선택
|
|
718
1143
|
const presetDef = getPreset(preset);
|
|
719
|
-
const
|
|
1144
|
+
const templates = preset === "cqrs" ? CQRS_STRUCTURE_TEMPLATES : STRUCTURE_TEMPLATES;
|
|
1145
|
+
const templateFn = templates[category] || templates.other;
|
|
720
1146
|
let structure = templateFn(featureName);
|
|
721
1147
|
|
|
722
|
-
// 5. 프리셋에 따른 구조 조정
|
|
723
|
-
if (presetDef && preset !== "mandu") {
|
|
1148
|
+
// 5. 프리셋에 따른 구조 조정 (cqrs, mandu는 자체 경로 사용)
|
|
1149
|
+
if (presetDef && preset !== "mandu" && preset !== "cqrs") {
|
|
724
1150
|
structure = adjustStructureForPreset(structure, presetDef, preset);
|
|
725
1151
|
}
|
|
726
1152
|
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CQRS Preset Tests
|
|
3
|
+
*
|
|
4
|
+
* CQRS 프리셋의 구조 검증 및 Command/Query 분리 규칙 테스트
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, it, expect, beforeAll, afterAll } from "bun:test";
|
|
8
|
+
import { mkdtemp, rm, mkdir } from "fs/promises";
|
|
9
|
+
import { join } from "path";
|
|
10
|
+
import { tmpdir } from "os";
|
|
11
|
+
import { cqrsPreset, CQRS_HIERARCHY } from "./cqrs";
|
|
12
|
+
import { presets, getPreset } from "./index";
|
|
13
|
+
import { negotiate, generateScaffold } from "../negotiation";
|
|
14
|
+
|
|
15
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
16
|
+
// Preset Structure Tests
|
|
17
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
18
|
+
|
|
19
|
+
describe("CQRS Preset - Structure", () => {
|
|
20
|
+
it("should be registered in presets map", () => {
|
|
21
|
+
expect(presets.cqrs).toBeDefined();
|
|
22
|
+
expect(presets.cqrs).toBe(cqrsPreset);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("should be retrievable via getPreset", () => {
|
|
26
|
+
expect(getPreset("cqrs")).toBe(cqrsPreset);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("should have correct name and description", () => {
|
|
30
|
+
expect(cqrsPreset.name).toBe("cqrs");
|
|
31
|
+
expect(cqrsPreset.description).toContain("CQRS");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("should define all 10 layers", () => {
|
|
35
|
+
expect(cqrsPreset.layers).toHaveLength(10);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("should have hierarchy matching layer names", () => {
|
|
39
|
+
const layerNames = cqrsPreset.layers.map((l) => l.name);
|
|
40
|
+
for (const h of CQRS_HIERARCHY) {
|
|
41
|
+
expect(layerNames).toContain(h);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("should set layerViolation severity to error", () => {
|
|
46
|
+
expect(cqrsPreset.defaultSeverity?.layerViolation).toBe("error");
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
51
|
+
// CQRS Separation Rules - TODO(human)
|
|
52
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
53
|
+
|
|
54
|
+
describe("CQRS Preset - Command/Query Separation", () => {
|
|
55
|
+
// TODO(human): CQRS 분리 규칙 검증 테스트를 작성하세요.
|
|
56
|
+
//
|
|
57
|
+
// 힌트:
|
|
58
|
+
// - cqrsPreset.layers 배열에서 특정 레이어를 찾으려면:
|
|
59
|
+
// const commandsLayer = cqrsPreset.layers.find(l => l.name === "application/commands");
|
|
60
|
+
// - commandsLayer.canImport 배열에 특정 레이어가 포함/미포함인지 검증
|
|
61
|
+
//
|
|
62
|
+
// 작성해야 할 테스트 케이스:
|
|
63
|
+
// 1. commands는 queries를 import할 수 없어야 함
|
|
64
|
+
// 2. queries는 commands와 events를 import할 수 없어야 함
|
|
65
|
+
// 3. commands는 domain, dto, events를 import할 수 있어야 함
|
|
66
|
+
// 4. queries는 domain, dto를 import할 수 있어야 함
|
|
67
|
+
// 5. domain은 shared만 import할 수 있어야 함
|
|
68
|
+
// 6. shared는 아무것도 import할 수 없어야 함
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
72
|
+
// CQRS Scaffolding Tests
|
|
73
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
74
|
+
|
|
75
|
+
let SCAFFOLD_DIR: string;
|
|
76
|
+
|
|
77
|
+
beforeAll(async () => {
|
|
78
|
+
SCAFFOLD_DIR = await mkdtemp(join(tmpdir(), "test-cqrs-scaffold-"));
|
|
79
|
+
await mkdir(join(SCAFFOLD_DIR, "spec", "decisions"), { recursive: true });
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
afterAll(async () => {
|
|
83
|
+
await rm(SCAFFOLD_DIR, { recursive: true, force: true });
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe("CQRS Preset - Scaffold Templates", () => {
|
|
87
|
+
it("should generate commands/queries separation for auth", async () => {
|
|
88
|
+
const result = await negotiate(
|
|
89
|
+
{ intent: "사용자 인증 추가", category: "auth", preset: "cqrs" },
|
|
90
|
+
SCAFFOLD_DIR,
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
expect(result.approved).toBe(true);
|
|
94
|
+
expect(result.preset).toBe("cqrs");
|
|
95
|
+
|
|
96
|
+
const paths = result.structure.map((s) => s.path);
|
|
97
|
+
expect(paths.some((p) => p.includes("application/commands"))).toBe(true);
|
|
98
|
+
expect(paths.some((p) => p.includes("application/queries"))).toBe(true);
|
|
99
|
+
expect(paths.some((p) => p.includes("application/dto"))).toBe(true);
|
|
100
|
+
expect(paths.some((p) => p.includes("application/events"))).toBe(true);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("should generate CRUD with separate commands and queries", async () => {
|
|
104
|
+
const result = await negotiate(
|
|
105
|
+
{ intent: "상품 관리 CRUD", category: "crud", preset: "cqrs" },
|
|
106
|
+
SCAFFOLD_DIR,
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
const commandsDir = result.structure.find((s) => s.path.includes("application/commands"));
|
|
110
|
+
const queriesDir = result.structure.find((s) => s.path.includes("application/queries"));
|
|
111
|
+
|
|
112
|
+
expect(commandsDir).toBeDefined();
|
|
113
|
+
expect(queriesDir).toBeDefined();
|
|
114
|
+
|
|
115
|
+
// commands에 create/update/delete가 있어야 함
|
|
116
|
+
const cmdFiles = commandsDir!.files.map((f) => f.name);
|
|
117
|
+
expect(cmdFiles.some((f) => f.includes("create"))).toBe(true);
|
|
118
|
+
expect(cmdFiles.some((f) => f.includes("update"))).toBe(true);
|
|
119
|
+
expect(cmdFiles.some((f) => f.includes("delete"))).toBe(true);
|
|
120
|
+
|
|
121
|
+
// queries에 get/list가 있어야 함
|
|
122
|
+
const qryFiles = queriesDir!.files.map((f) => f.name);
|
|
123
|
+
expect(qryFiles.some((f) => f.includes("get"))).toBe(true);
|
|
124
|
+
expect(qryFiles.some((f) => f.includes("list"))).toBe(true);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("should create actual scaffold files", async () => {
|
|
128
|
+
const testDir = await mkdtemp(join(SCAFFOLD_DIR, "actual-"));
|
|
129
|
+
await mkdir(join(testDir, "spec", "decisions"), { recursive: true });
|
|
130
|
+
|
|
131
|
+
const plan = await negotiate(
|
|
132
|
+
{ intent: "order feature", category: "crud", preset: "cqrs" },
|
|
133
|
+
testDir,
|
|
134
|
+
);
|
|
135
|
+
const result = await generateScaffold(plan.structure, testDir);
|
|
136
|
+
|
|
137
|
+
expect(result.success).toBe(true);
|
|
138
|
+
expect(result.createdFiles.some((f) => f.includes("command"))).toBe(true);
|
|
139
|
+
expect(result.createdFiles.some((f) => f.includes("query"))).toBe(true);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("should use src/ prefixed paths (no adjustStructureForPreset)", async () => {
|
|
143
|
+
const result = await negotiate(
|
|
144
|
+
{ intent: "payment api", category: "api", preset: "cqrs" },
|
|
145
|
+
SCAFFOLD_DIR,
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
// CQRS 전용 템플릿은 이미 src/ 기준 경로를 사용
|
|
149
|
+
const paths = result.structure.map((s) => s.path);
|
|
150
|
+
for (const p of paths) {
|
|
151
|
+
expect(p.startsWith("src/")).toBe(true);
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
});
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CQRS Preset
|
|
3
|
+
*
|
|
4
|
+
* Command Query Responsibility Segregation 아키텍처
|
|
5
|
+
* 쓰기 경로(Commands)와 읽기 경로(Queries)의 격리를 프레임워크 레벨에서 강제
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { PresetDefinition } from "../types";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* CQRS 레이어 계층 구조
|
|
12
|
+
*
|
|
13
|
+
* 의존성: 외부 → 내부 (domain이 핵심)
|
|
14
|
+
* commands와 queries는 서로 격리됨
|
|
15
|
+
*/
|
|
16
|
+
export const CQRS_HIERARCHY = [
|
|
17
|
+
"api",
|
|
18
|
+
"infra",
|
|
19
|
+
"application/commands",
|
|
20
|
+
"application/queries",
|
|
21
|
+
"application/dto",
|
|
22
|
+
"application/mappers",
|
|
23
|
+
"application/events",
|
|
24
|
+
"domain",
|
|
25
|
+
"core",
|
|
26
|
+
"shared",
|
|
27
|
+
] as const;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* CQRS 프리셋 정의
|
|
31
|
+
*/
|
|
32
|
+
export const cqrsPreset: PresetDefinition = {
|
|
33
|
+
name: "cqrs",
|
|
34
|
+
description: "CQRS - Command/Query 분리 아키텍처",
|
|
35
|
+
|
|
36
|
+
hierarchy: [...CQRS_HIERARCHY],
|
|
37
|
+
|
|
38
|
+
layers: [
|
|
39
|
+
{
|
|
40
|
+
name: "api",
|
|
41
|
+
pattern: "src/**/api/**",
|
|
42
|
+
canImport: ["application/commands", "application/queries", "application/dto", "core", "shared"],
|
|
43
|
+
description: "Controllers, Routes - Command/Query 디스패치",
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
name: "infra",
|
|
47
|
+
pattern: "src/**/infra/**",
|
|
48
|
+
canImport: ["application/commands", "application/queries", "domain", "core", "shared"],
|
|
49
|
+
description: "Repository 구현, 외부 서비스 어댑터",
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
name: "application/commands",
|
|
53
|
+
pattern: "src/**/application/commands/**",
|
|
54
|
+
canImport: ["domain", "application/dto", "application/events", "core", "shared"],
|
|
55
|
+
description: "Command Handlers - 쓰기 경로 (queries 접근 불가)",
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
name: "application/queries",
|
|
59
|
+
pattern: "src/**/application/queries/**",
|
|
60
|
+
canImport: ["domain", "application/dto", "core", "shared"],
|
|
61
|
+
description: "Query Handlers - 읽기 경로 (commands, events 접근 불가)",
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
name: "application/dto",
|
|
65
|
+
pattern: "src/**/application/dto/**",
|
|
66
|
+
canImport: ["domain", "shared"],
|
|
67
|
+
description: "Data Transfer Objects",
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
name: "application/mappers",
|
|
71
|
+
pattern: "src/**/application/mappers/**",
|
|
72
|
+
canImport: ["domain", "application/dto", "shared"],
|
|
73
|
+
description: "Domain ↔ DTO 변환기",
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
name: "application/events",
|
|
77
|
+
pattern: "src/**/application/events/**",
|
|
78
|
+
canImport: ["domain", "shared"],
|
|
79
|
+
description: "Domain Events, Integration Events",
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
name: "domain",
|
|
83
|
+
pattern: "src/**/domain/**",
|
|
84
|
+
canImport: ["shared"],
|
|
85
|
+
description: "Entities, Value Objects, Domain Services, Aggregates",
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
name: "core",
|
|
89
|
+
pattern: "src/core/**",
|
|
90
|
+
canImport: ["shared"],
|
|
91
|
+
description: "공통 핵심 (auth, config, errors, CQRS bus)",
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
name: "shared",
|
|
95
|
+
pattern: "src/shared/**",
|
|
96
|
+
canImport: [],
|
|
97
|
+
description: "공유 유틸리티, 타입, 인터페이스",
|
|
98
|
+
},
|
|
99
|
+
],
|
|
100
|
+
|
|
101
|
+
defaultSeverity: {
|
|
102
|
+
layerViolation: "error",
|
|
103
|
+
circularDependency: "error",
|
|
104
|
+
crossSliceDependency: "warn",
|
|
105
|
+
deepNesting: "info",
|
|
106
|
+
},
|
|
107
|
+
};
|
|
@@ -9,12 +9,14 @@ import { fsdPreset, FSD_HIERARCHY } from "./fsd";
|
|
|
9
9
|
import { cleanPreset, CLEAN_HIERARCHY } from "./clean";
|
|
10
10
|
import { hexagonalPreset, HEXAGONAL_HIERARCHY } from "./hexagonal";
|
|
11
11
|
import { atomicPreset, ATOMIC_HIERARCHY } from "./atomic";
|
|
12
|
+
import { cqrsPreset, CQRS_HIERARCHY } from "./cqrs";
|
|
12
13
|
|
|
13
14
|
// Re-export
|
|
14
15
|
export { fsdPreset, FSD_HIERARCHY } from "./fsd";
|
|
15
16
|
export { cleanPreset, CLEAN_HIERARCHY } from "./clean";
|
|
16
17
|
export { hexagonalPreset, HEXAGONAL_HIERARCHY } from "./hexagonal";
|
|
17
18
|
export { atomicPreset, ATOMIC_HIERARCHY } from "./atomic";
|
|
19
|
+
export { cqrsPreset, CQRS_HIERARCHY } from "./cqrs";
|
|
18
20
|
|
|
19
21
|
/**
|
|
20
22
|
* Mandu 권장 프리셋 (FSD + Clean 조합)
|
|
@@ -263,6 +265,7 @@ export const presets: Record<GuardPreset, PresetDefinition> = {
|
|
|
263
265
|
clean: cleanPreset,
|
|
264
266
|
hexagonal: hexagonalPreset,
|
|
265
267
|
atomic: atomicPreset,
|
|
268
|
+
cqrs: cqrsPreset,
|
|
266
269
|
mandu: manduPreset,
|
|
267
270
|
};
|
|
268
271
|
|
package/src/guard/suggestions.ts
CHANGED
|
@@ -35,6 +35,12 @@ const DOCS: Record<GuardPreset | "default", Record<string, string>> = {
|
|
|
35
35
|
molecules: "https://bradfrost.com/blog/post/atomic-web-design/#molecules",
|
|
36
36
|
organisms: "https://bradfrost.com/blog/post/atomic-web-design/#organisms",
|
|
37
37
|
},
|
|
38
|
+
cqrs: {
|
|
39
|
+
base: "https://learn.microsoft.com/en-us/azure/architecture/patterns/cqrs",
|
|
40
|
+
commands: "https://learn.microsoft.com/en-us/azure/architecture/patterns/cqrs#solution",
|
|
41
|
+
queries: "https://learn.microsoft.com/en-us/azure/architecture/patterns/cqrs#solution",
|
|
42
|
+
layers: "https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html",
|
|
43
|
+
},
|
|
38
44
|
mandu: {
|
|
39
45
|
base: "https://github.com/mandujs/mandu/docs/guard",
|
|
40
46
|
layers: "https://github.com/mandujs/mandu/docs/guard#layers",
|
package/src/guard/types.ts
CHANGED