@mandujs/cli 0.9.42 → 0.9.44
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 +1 -1
- package/README.md +3 -3
- package/package.json +2 -2
- package/src/commands/build.ts +55 -33
- package/src/commands/check.ts +16 -18
- package/src/commands/contract.ts +50 -42
- package/src/commands/dev.ts +95 -42
- package/src/commands/doctor.ts +27 -25
- package/src/commands/guard-arch.ts +12 -3
- package/src/commands/init.ts +53 -52
- package/src/commands/monitor.ts +2 -3
- package/src/commands/openapi.ts +107 -48
- package/src/errors/messages.ts +2 -2
- package/src/main.ts +75 -145
- package/src/util/manifest.ts +52 -0
- package/src/util/port.ts +71 -0
- package/templates/default/AGENTS.md +96 -0
- package/templates/default/app/globals.css +45 -33
- package/templates/default/package.json +15 -12
- package/templates/default/postcss.config.js +0 -6
- package/templates/default/tailwind.config.ts +0 -64
package/src/main.ts
CHANGED
|
@@ -31,30 +31,25 @@ const HELP_TEXT = `
|
|
|
31
31
|
Usage: bunx mandu <command> [options]
|
|
32
32
|
|
|
33
33
|
Commands:
|
|
34
|
-
init
|
|
35
|
-
check
|
|
36
|
-
routes generate
|
|
37
|
-
routes list
|
|
38
|
-
routes watch
|
|
39
|
-
dev
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
guard
|
|
43
|
-
guard
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
watch 실시간 파일 감시 - 경고만 (Brain)
|
|
54
|
-
monitor MCP Activity Monitor 로그 스트림
|
|
55
|
-
|
|
56
|
-
brain setup sLLM 설정 (선택)
|
|
57
|
-
brain status Brain 상태 확인
|
|
34
|
+
init 새 프로젝트 생성 (Tailwind + shadcn/ui 기본 포함)
|
|
35
|
+
check FS Routes + Guard 통합 검사
|
|
36
|
+
routes generate FS Routes 스캔 및 매니페스트 생성
|
|
37
|
+
routes list 현재 라우트 목록 출력
|
|
38
|
+
routes watch 실시간 라우트 감시
|
|
39
|
+
dev 개발 서버 실행 (FS Routes + Guard 기본)
|
|
40
|
+
build 클라이언트 번들 빌드 (Hydration)
|
|
41
|
+
guard 아키텍처 위반 검사 (기본)
|
|
42
|
+
guard arch 아키텍처 위반 검사 (FSD/Clean/Hexagonal)
|
|
43
|
+
guard legacy 레거시 Spec Guard 검사
|
|
44
|
+
spec-upsert Spec 파일 검증 및 lock 갱신 (레거시)
|
|
45
|
+
generate Spec에서 코드 생성 (레거시)
|
|
46
|
+
|
|
47
|
+
doctor Guard 실패 분석 + 패치 제안 (Brain)
|
|
48
|
+
watch 실시간 파일 감시 - 경고만 (Brain)
|
|
49
|
+
monitor MCP Activity Monitor 로그 스트림
|
|
50
|
+
|
|
51
|
+
brain setup sLLM 설정 (선택)
|
|
52
|
+
brain status Brain 상태 확인
|
|
58
53
|
|
|
59
54
|
contract create <routeId> 라우트에 대한 Contract 생성
|
|
60
55
|
contract validate Contract-Slot 일관성 검증
|
|
@@ -64,79 +59,68 @@ Commands:
|
|
|
64
59
|
openapi generate OpenAPI 3.0 스펙 생성
|
|
65
60
|
openapi serve Swagger UI 로컬 서버 실행
|
|
66
61
|
|
|
67
|
-
change begin
|
|
68
|
-
change commit
|
|
69
|
-
change rollback
|
|
70
|
-
change status
|
|
71
|
-
change list
|
|
72
|
-
change prune
|
|
62
|
+
change begin 변경 트랜잭션 시작 (스냅샷 생성)
|
|
63
|
+
change commit 변경 확정
|
|
64
|
+
change rollback 스냅샷으로 복원
|
|
65
|
+
change status 현재 트랜잭션 상태
|
|
66
|
+
change list 변경 이력 조회
|
|
67
|
+
change prune 오래된 스냅샷 정리
|
|
73
68
|
|
|
74
69
|
Options:
|
|
75
|
-
--name <name>
|
|
76
|
-
--css <framework>
|
|
77
|
-
--ui <library>
|
|
78
|
-
--theme
|
|
79
|
-
--minimal
|
|
80
|
-
--file <path>
|
|
81
|
-
--
|
|
82
|
-
--guard
|
|
83
|
-
--
|
|
84
|
-
--
|
|
85
|
-
--
|
|
86
|
-
--
|
|
87
|
-
--
|
|
88
|
-
--
|
|
89
|
-
--
|
|
90
|
-
--
|
|
91
|
-
--
|
|
92
|
-
--
|
|
93
|
-
--
|
|
94
|
-
--
|
|
95
|
-
--
|
|
96
|
-
--watch
|
|
97
|
-
--
|
|
98
|
-
--
|
|
99
|
-
--
|
|
100
|
-
--
|
|
101
|
-
--
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
--format <fmt> doctor 출력 형식: console, json, markdown (기본: console)
|
|
110
|
-
--no-llm doctor에서 LLM 사용 안 함 (템플릿 모드)
|
|
111
|
-
--model <name> brain setup 시 모델 이름 (기본: llama3.2)
|
|
112
|
-
--url <url> brain setup 시 Ollama URL
|
|
113
|
-
--verbose 상세 출력
|
|
114
|
-
--help, -h 도움말 표시
|
|
70
|
+
--name <name> init 시 프로젝트 이름 (기본: my-mandu-app)
|
|
71
|
+
--css <framework> init 시 CSS 프레임워크: tailwind, panda, none (기본: tailwind)
|
|
72
|
+
--ui <library> init 시 UI 라이브러리: shadcn, ark, none (기본: shadcn)
|
|
73
|
+
--theme init 시 다크모드 테마 시스템 추가
|
|
74
|
+
--minimal init 시 CSS/UI 없이 최소 템플릿 생성 (--css none --ui none)
|
|
75
|
+
--file <path> spec-upsert spec 파일/monitor 로그 파일 경로
|
|
76
|
+
--watch build/guard arch 파일 감시 모드
|
|
77
|
+
--output <path> routes/openapi/doctor/contract/guard 출력 경로
|
|
78
|
+
--verbose routes list/watch, contract validate, brain status 상세 출력
|
|
79
|
+
--from <path> contract diff 기준 레지스트리 경로
|
|
80
|
+
--to <path> contract diff 대상 레지스트리 경로
|
|
81
|
+
--json contract diff 결과 JSON 출력
|
|
82
|
+
--title <title> openapi generate title
|
|
83
|
+
--version <ver> openapi generate version
|
|
84
|
+
--summary monitor 요약 출력 (JSON 로그에서만)
|
|
85
|
+
--since <duration> monitor 요약 기간 (예: 5m, 30s, 1h)
|
|
86
|
+
--follow <bool> monitor follow 모드 (기본: true)
|
|
87
|
+
--message <msg> change begin 시 설명 메시지
|
|
88
|
+
--id <id> change rollback 시 특정 변경 ID
|
|
89
|
+
--keep <n> change prune 시 유지할 스냅샷 수 (기본: 5)
|
|
90
|
+
--no-llm doctor에서 LLM 사용 안 함 (템플릿 모드)
|
|
91
|
+
--status watch 상태만 출력
|
|
92
|
+
--debounce <ms> watch debounce (ms)
|
|
93
|
+
--model <name> brain setup 시 모델 이름 (기본: llama3.2)
|
|
94
|
+
--url <url> brain setup 시 Ollama URL
|
|
95
|
+
--skip-check brain setup 시 모델/서버 체크 건너뜀
|
|
96
|
+
--help, -h 도움말 표시
|
|
97
|
+
|
|
98
|
+
Notes:
|
|
99
|
+
- 출력 포맷은 환경에 따라 자동 결정됩니다 (TTY/CI/MANDU_OUTPUT).
|
|
100
|
+
- doctor 출력은 .json이면 JSON, 그 외는 markdown으로 저장됩니다.
|
|
101
|
+
- guard arch 리포트는 .json/.html/.md 확장자를 자동 추론합니다.
|
|
102
|
+
- 포트는 PORT 환경변수 또는 mandu.config의 server.port로 설정합니다.
|
|
103
|
+
- 포트 충돌 시 다음 사용 가능한 포트로 자동 변경됩니다.
|
|
115
104
|
|
|
116
105
|
Examples:
|
|
117
106
|
bunx mandu init --name my-app # Tailwind + shadcn/ui 기본
|
|
118
107
|
bunx mandu init my-app --minimal # CSS/UI 없이 최소 템플릿
|
|
119
|
-
bunx mandu
|
|
120
|
-
bunx mandu
|
|
121
|
-
bunx mandu check
|
|
122
|
-
bunx mandu routes list
|
|
123
|
-
bunx mandu routes generate
|
|
124
|
-
bunx mandu dev --port 3000
|
|
125
|
-
bunx mandu dev --no-guard
|
|
126
|
-
bunx mandu build --minify
|
|
108
|
+
bunx mandu dev
|
|
109
|
+
bunx mandu build --watch
|
|
127
110
|
bunx mandu guard
|
|
128
|
-
bunx mandu guard arch --preset fsd
|
|
129
111
|
bunx mandu guard arch --watch
|
|
130
|
-
bunx mandu guard arch --
|
|
131
|
-
bunx mandu
|
|
132
|
-
bunx mandu
|
|
112
|
+
bunx mandu guard arch --output guard-report.md
|
|
113
|
+
bunx mandu check
|
|
114
|
+
bunx mandu routes list --verbose
|
|
115
|
+
bunx mandu contract create users
|
|
116
|
+
bunx mandu contract validate --verbose
|
|
117
|
+
bunx mandu contract build --output .mandu/contracts.json
|
|
118
|
+
bunx mandu contract diff --json
|
|
119
|
+
bunx mandu openapi generate --output docs/openapi.json
|
|
120
|
+
bunx mandu openapi serve
|
|
133
121
|
bunx mandu monitor --summary --since 5m
|
|
134
|
-
bunx mandu doctor
|
|
122
|
+
bunx mandu doctor --output reports/doctor.json
|
|
135
123
|
bunx mandu brain setup --model codellama
|
|
136
|
-
bunx mandu contract create users
|
|
137
|
-
bunx mandu contract build
|
|
138
|
-
bunx mandu contract diff
|
|
139
|
-
bunx mandu openapi generate --output docs/api.json
|
|
140
124
|
bunx mandu change begin --message "Add new route"
|
|
141
125
|
|
|
142
126
|
FS Routes Workflow (권장):
|
|
@@ -173,31 +157,6 @@ function parseArgs(args: string[]): { command: string; options: Record<string, s
|
|
|
173
157
|
return { command, options };
|
|
174
158
|
}
|
|
175
159
|
|
|
176
|
-
/**
|
|
177
|
-
* 포트 옵션 안전하게 파싱
|
|
178
|
-
* - 숫자가 아니면 undefined 반환 (기본값 사용)
|
|
179
|
-
* - 유효 범위: 1-65535
|
|
180
|
-
*/
|
|
181
|
-
function parsePort(value: string | undefined, optionName = "port"): number | undefined {
|
|
182
|
-
if (!value || value === "true") {
|
|
183
|
-
return undefined; // 기본값 사용
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
const port = Number(value);
|
|
187
|
-
|
|
188
|
-
if (Number.isNaN(port)) {
|
|
189
|
-
console.warn(`⚠️ Invalid --${optionName} value: "${value}" (using default)`);
|
|
190
|
-
return undefined;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
194
|
-
console.warn(`⚠️ Invalid --${optionName} range: ${port} (must be 1-65535, using default)`);
|
|
195
|
-
return undefined;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
return port;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
160
|
async function main(): Promise<void> {
|
|
202
161
|
const args = process.argv.slice(2);
|
|
203
162
|
const { command, options } = parseArgs(args);
|
|
@@ -229,30 +188,15 @@ async function main(): Promise<void> {
|
|
|
229
188
|
break;
|
|
230
189
|
|
|
231
190
|
case "check":
|
|
232
|
-
success = await check(
|
|
233
|
-
preset: options.preset as any,
|
|
234
|
-
format: options.format as any,
|
|
235
|
-
ci: options.ci === "true",
|
|
236
|
-
quiet: options.quiet === "true",
|
|
237
|
-
legacy: options.legacy === "true",
|
|
238
|
-
});
|
|
191
|
+
success = await check();
|
|
239
192
|
break;
|
|
240
193
|
|
|
241
194
|
case "guard": {
|
|
242
195
|
const subCommand = args[1];
|
|
243
196
|
const hasSubCommand = subCommand && !subCommand.startsWith("--");
|
|
244
197
|
const guardArchOptions = {
|
|
245
|
-
preset: options.preset as any,
|
|
246
198
|
watch: options.watch === "true",
|
|
247
|
-
ci: options.ci === "true",
|
|
248
|
-
format: options.format as any,
|
|
249
|
-
quiet: options.quiet === "true",
|
|
250
|
-
srcDir: options["src-dir"],
|
|
251
|
-
listPresets: options["list-presets"] === "true",
|
|
252
199
|
output: options.output,
|
|
253
|
-
reportFormat: (options["report-format"] as any) || "markdown",
|
|
254
|
-
saveStats: options["save-stats"] === "true",
|
|
255
|
-
showTrend: options["show-trend"] === "true",
|
|
256
200
|
};
|
|
257
201
|
switch (subCommand) {
|
|
258
202
|
case "arch":
|
|
@@ -260,9 +204,7 @@ async function main(): Promise<void> {
|
|
|
260
204
|
break;
|
|
261
205
|
case "legacy":
|
|
262
206
|
case "spec":
|
|
263
|
-
success = await guardCheck(
|
|
264
|
-
autoCorrect: options["no-auto-correct"] !== "true",
|
|
265
|
-
});
|
|
207
|
+
success = await guardCheck();
|
|
266
208
|
break;
|
|
267
209
|
default:
|
|
268
210
|
if (hasSubCommand) {
|
|
@@ -281,20 +223,12 @@ async function main(): Promise<void> {
|
|
|
281
223
|
|
|
282
224
|
case "build":
|
|
283
225
|
success = await build({
|
|
284
|
-
minify: options.minify === "true",
|
|
285
|
-
sourcemap: options.sourcemap === "true",
|
|
286
226
|
watch: options.watch === "true",
|
|
287
227
|
});
|
|
288
228
|
break;
|
|
289
229
|
|
|
290
230
|
case "dev":
|
|
291
|
-
await dev(
|
|
292
|
-
port: parsePort(options.port),
|
|
293
|
-
guard: options["no-guard"] === "true" ? false : options.guard !== "false",
|
|
294
|
-
guardPreset: options["guard-preset"] as any,
|
|
295
|
-
guardFormat: options["guard-format"] as any,
|
|
296
|
-
legacy: options.legacy === "true",
|
|
297
|
-
});
|
|
231
|
+
await dev();
|
|
298
232
|
break;
|
|
299
233
|
|
|
300
234
|
case "routes": {
|
|
@@ -384,9 +318,7 @@ async function main(): Promise<void> {
|
|
|
384
318
|
});
|
|
385
319
|
break;
|
|
386
320
|
case "serve":
|
|
387
|
-
success = await openAPIServe(
|
|
388
|
-
port: parsePort(options.port),
|
|
389
|
-
});
|
|
321
|
+
success = await openAPIServe();
|
|
390
322
|
break;
|
|
391
323
|
default:
|
|
392
324
|
printCLIError(CLI_ERROR_CODES.UNKNOWN_SUBCOMMAND, {
|
|
@@ -435,7 +367,6 @@ async function main(): Promise<void> {
|
|
|
435
367
|
|
|
436
368
|
case "doctor":
|
|
437
369
|
success = await doctor({
|
|
438
|
-
format: (options.format as "console" | "json" | "markdown") || "console",
|
|
439
370
|
useLLM: options["no-llm"] !== "true",
|
|
440
371
|
output: options.output,
|
|
441
372
|
});
|
|
@@ -450,7 +381,6 @@ async function main(): Promise<void> {
|
|
|
450
381
|
|
|
451
382
|
case "monitor":
|
|
452
383
|
success = await monitor({
|
|
453
|
-
format: options.format as any,
|
|
454
384
|
summary: options.summary === "true",
|
|
455
385
|
since: options.since,
|
|
456
386
|
follow: options.follow === "false" ? false : true,
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import {
|
|
3
|
+
generateManifest,
|
|
4
|
+
loadManifest,
|
|
5
|
+
type RoutesManifest,
|
|
6
|
+
type FSScannerConfig,
|
|
7
|
+
} from "@mandujs/core";
|
|
8
|
+
import { isDirectory } from "./fs";
|
|
9
|
+
|
|
10
|
+
export type ManifestSource = "fs" | "spec";
|
|
11
|
+
|
|
12
|
+
export interface ResolvedManifest {
|
|
13
|
+
manifest: RoutesManifest;
|
|
14
|
+
source: ManifestSource;
|
|
15
|
+
warnings: string[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function resolveManifest(
|
|
19
|
+
rootDir: string,
|
|
20
|
+
options: { fsRoutes?: FSScannerConfig; outputPath?: string } = {}
|
|
21
|
+
): Promise<ResolvedManifest> {
|
|
22
|
+
const appDir = path.resolve(rootDir, "app");
|
|
23
|
+
const hasApp = await isDirectory(appDir);
|
|
24
|
+
|
|
25
|
+
if (hasApp) {
|
|
26
|
+
const result = await generateManifest(rootDir, {
|
|
27
|
+
scanner: options.fsRoutes,
|
|
28
|
+
outputPath: options.outputPath,
|
|
29
|
+
skipLegacy: true,
|
|
30
|
+
});
|
|
31
|
+
return {
|
|
32
|
+
manifest: result.manifest,
|
|
33
|
+
source: "fs",
|
|
34
|
+
warnings: result.warnings,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const specPath = path.join(rootDir, "spec", "routes.manifest.json");
|
|
39
|
+
if (await Bun.file(specPath).exists()) {
|
|
40
|
+
const result = await loadManifest(specPath);
|
|
41
|
+
if (!result.success) {
|
|
42
|
+
throw new Error(result.errors?.join(", ") || "Failed to load routes manifest");
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
manifest: result.data!,
|
|
46
|
+
source: "spec",
|
|
47
|
+
warnings: [],
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
throw new Error("No routes found. Create app/ routes or spec/routes.manifest.json");
|
|
52
|
+
}
|
package/src/util/port.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { createServer } from "net";
|
|
2
|
+
|
|
3
|
+
const DEFAULT_MAX_ATTEMPTS = 10;
|
|
4
|
+
|
|
5
|
+
function isPortUsable(error: unknown): boolean {
|
|
6
|
+
if (!error || typeof error !== "object") return false;
|
|
7
|
+
const code = (error as { code?: string }).code;
|
|
8
|
+
return code === "EADDRINUSE" || code === "EACCES";
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async function isPortAvailable(port: number, hostname?: string): Promise<boolean> {
|
|
12
|
+
return new Promise((resolve) => {
|
|
13
|
+
const server = createServer();
|
|
14
|
+
|
|
15
|
+
server.once("error", (error) => {
|
|
16
|
+
if (isPortUsable(error)) {
|
|
17
|
+
resolve(false);
|
|
18
|
+
} else {
|
|
19
|
+
resolve(false);
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
server.once("listening", () => {
|
|
24
|
+
server.close(() => resolve(true));
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
server.listen(port, hostname);
|
|
29
|
+
server.unref();
|
|
30
|
+
} catch {
|
|
31
|
+
resolve(false);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function resolveAvailablePort(
|
|
37
|
+
startPort: number,
|
|
38
|
+
options: {
|
|
39
|
+
hostname?: string;
|
|
40
|
+
offsets?: number[];
|
|
41
|
+
maxAttempts?: number;
|
|
42
|
+
} = {}
|
|
43
|
+
): Promise<{ port: number; attempts: number }> {
|
|
44
|
+
const offsets = options.offsets && options.offsets.length > 0 ? options.offsets : [0];
|
|
45
|
+
const maxAttempts = options.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;
|
|
46
|
+
|
|
47
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
48
|
+
const candidate = startPort + attempt;
|
|
49
|
+
if (candidate < 1 || candidate > 65535) {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const targets = offsets
|
|
54
|
+
.map((offset) => candidate + offset)
|
|
55
|
+
.filter((port) => port >= 1 && port <= 65535);
|
|
56
|
+
|
|
57
|
+
if (targets.length !== offsets.length) {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const results = await Promise.all(
|
|
62
|
+
targets.map((port) => isPortAvailable(port, options.hostname))
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
if (results.every(Boolean)) {
|
|
66
|
+
return { port: candidate, attempts: attempt };
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
throw new Error(`No available port found starting at ${startPort}`);
|
|
71
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# AI Agent Instructions for Mandu Project
|
|
2
|
+
|
|
3
|
+
이 프로젝트는 **Mandu Framework**로 구축되었습니다. AI 에이전트가 이 프로젝트를 다룰 때 아래 지침을 따라주세요.
|
|
4
|
+
|
|
5
|
+
## 패키지 매니저: Bun (필수)
|
|
6
|
+
|
|
7
|
+
**⚠️ 중요: 이 프로젝트는 Bun만 사용합니다. npm/yarn/pnpm을 사용하지 마세요.**
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
# ✅ 올바른 명령어
|
|
11
|
+
bun install # 의존성 설치
|
|
12
|
+
bun add <package> # 패키지 추가
|
|
13
|
+
bun remove <package> # 패키지 제거
|
|
14
|
+
bun run dev # 개발 서버 시작
|
|
15
|
+
bun run build # 프로덕션 빌드
|
|
16
|
+
bun test # 테스트 실행
|
|
17
|
+
|
|
18
|
+
# ❌ 사용 금지
|
|
19
|
+
npm install / yarn install / pnpm install
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## 프로젝트 구조
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
├── app/ # FS 기반 라우팅 (페이지, API)
|
|
26
|
+
│ ├── page.tsx # / 라우트
|
|
27
|
+
│ ├── layout.tsx # 루트 레이아웃
|
|
28
|
+
│ ├── globals.css # Tailwind CSS (v4)
|
|
29
|
+
│ └── api/ # API 라우트
|
|
30
|
+
├── src/
|
|
31
|
+
│ ├── client/ # 클라이언트 코드 (FSD 구조)
|
|
32
|
+
│ │ ├── shared/ # 공용 UI, 유틸리티
|
|
33
|
+
│ │ ├── entities/ # 엔티티 컴포넌트
|
|
34
|
+
│ │ ├── features/ # 기능 컴포넌트
|
|
35
|
+
│ │ └── widgets/ # 위젯/Island 컴포넌트
|
|
36
|
+
│ ├── server/ # 서버 코드 (Clean Architecture)
|
|
37
|
+
│ │ ├── domain/ # 도메인 모델
|
|
38
|
+
│ │ ├── application/ # 비즈니스 로직
|
|
39
|
+
│ │ └── infra/ # 인프라/DB
|
|
40
|
+
│ └── shared/ # 클라이언트-서버 공유 코드
|
|
41
|
+
│ ├── contracts/ # API 계약 타입
|
|
42
|
+
│ └── types/ # 공용 타입
|
|
43
|
+
└── mandu.config.ts # Mandu 설정 (선택)
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## 주요 규칙
|
|
47
|
+
|
|
48
|
+
### 1. Island 컴포넌트
|
|
49
|
+
클라이언트 상호작용이 필요한 컴포넌트는 `*.island.tsx`로 명명:
|
|
50
|
+
```tsx
|
|
51
|
+
// src/client/widgets/counter/Counter.island.tsx
|
|
52
|
+
"use client";
|
|
53
|
+
export function CounterIsland() { ... }
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### 2. API 라우트
|
|
57
|
+
`app/api/` 폴더에 `route.ts` 파일로 정의:
|
|
58
|
+
```typescript
|
|
59
|
+
// app/api/users/route.ts
|
|
60
|
+
import { Mandu } from "@mandujs/core";
|
|
61
|
+
export default Mandu.filling()
|
|
62
|
+
.get((ctx) => ctx.ok({ users: [] }))
|
|
63
|
+
.post(async (ctx) => { ... });
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### 3. Tailwind CSS v4
|
|
67
|
+
CSS-first 설정 사용 (`tailwind.config.ts` 없음):
|
|
68
|
+
```css
|
|
69
|
+
/* app/globals.css */
|
|
70
|
+
@import "tailwindcss";
|
|
71
|
+
@theme {
|
|
72
|
+
--color-primary: hsl(222.2 47.4% 11.2%);
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### 4. Import Alias
|
|
77
|
+
`@/` = `src/` 경로:
|
|
78
|
+
```typescript
|
|
79
|
+
import { Button } from "@/client/shared/ui/button";
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## 실행 방법
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
bun install # 최초 설치
|
|
86
|
+
bun run dev # 개발 서버 (http://localhost:4000)
|
|
87
|
+
bun run build # 프로덕션 빌드
|
|
88
|
+
bun run guard # 아키텍처 검증
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## 기술 스택
|
|
92
|
+
|
|
93
|
+
- **Runtime**: Bun 1.x
|
|
94
|
+
- **Framework**: Mandu (React 19 + Bun native)
|
|
95
|
+
- **Styling**: Tailwind CSS v4
|
|
96
|
+
- **Language**: TypeScript 5.x
|
|
@@ -1,37 +1,49 @@
|
|
|
1
|
-
@
|
|
2
|
-
@tailwind components;
|
|
3
|
-
@tailwind utilities;
|
|
1
|
+
@import "tailwindcss";
|
|
4
2
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
3
|
+
/*
|
|
4
|
+
* Tailwind CSS v4 - CSS-first Configuration
|
|
5
|
+
* https://tailwindcss.com/docs/v4
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
@theme {
|
|
9
|
+
/* Colors - shadcn/ui compatible */
|
|
10
|
+
--color-background: hsl(0 0% 100%);
|
|
11
|
+
--color-foreground: hsl(222.2 84% 4.9%);
|
|
12
|
+
--color-card: hsl(0 0% 100%);
|
|
13
|
+
--color-card-foreground: hsl(222.2 84% 4.9%);
|
|
14
|
+
--color-popover: hsl(0 0% 100%);
|
|
15
|
+
--color-popover-foreground: hsl(222.2 84% 4.9%);
|
|
16
|
+
--color-primary: hsl(222.2 47.4% 11.2%);
|
|
17
|
+
--color-primary-foreground: hsl(210 40% 98%);
|
|
18
|
+
--color-secondary: hsl(210 40% 96.1%);
|
|
19
|
+
--color-secondary-foreground: hsl(222.2 47.4% 11.2%);
|
|
20
|
+
--color-muted: hsl(210 40% 96.1%);
|
|
21
|
+
--color-muted-foreground: hsl(215.4 16.3% 46.9%);
|
|
22
|
+
--color-accent: hsl(210 40% 96.1%);
|
|
23
|
+
--color-accent-foreground: hsl(222.2 47.4% 11.2%);
|
|
24
|
+
--color-destructive: hsl(0 84.2% 60.2%);
|
|
25
|
+
--color-destructive-foreground: hsl(210 40% 98%);
|
|
26
|
+
--color-border: hsl(214.3 31.8% 91.4%);
|
|
27
|
+
--color-input: hsl(214.3 31.8% 91.4%);
|
|
28
|
+
--color-ring: hsl(222.2 84% 4.9%);
|
|
29
|
+
|
|
30
|
+
/* Radius */
|
|
31
|
+
--radius-sm: 0.25rem;
|
|
32
|
+
--radius-md: 0.5rem;
|
|
33
|
+
--radius-lg: 0.75rem;
|
|
34
|
+
--radius-xl: 1rem;
|
|
35
|
+
|
|
36
|
+
/* Fonts */
|
|
37
|
+
--font-sans: 'Inter', ui-sans-serif, system-ui, sans-serif;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/* Base styles */
|
|
41
|
+
* {
|
|
42
|
+
border-color: var(--color-border);
|
|
28
43
|
}
|
|
29
44
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
body {
|
|
35
|
-
@apply bg-background text-foreground;
|
|
36
|
-
}
|
|
45
|
+
body {
|
|
46
|
+
background-color: var(--color-background);
|
|
47
|
+
color: var(--color-foreground);
|
|
48
|
+
font-family: var(--font-sans);
|
|
37
49
|
}
|
|
@@ -2,15 +2,19 @@
|
|
|
2
2
|
"name": "{{PROJECT_NAME}}",
|
|
3
3
|
"version": "0.1.0",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"
|
|
6
|
-
|
|
7
|
-
"
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
"
|
|
11
|
-
|
|
5
|
+
"packageManager": "bun@1.2.0",
|
|
6
|
+
"engines": {
|
|
7
|
+
"bun": ">=1.0.0"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"dev": "mandu dev",
|
|
11
|
+
"build": "mandu build",
|
|
12
|
+
"check": "mandu check",
|
|
13
|
+
"guard": "mandu guard",
|
|
14
|
+
"test": "bun test"
|
|
15
|
+
},
|
|
12
16
|
"dependencies": {
|
|
13
|
-
"@mandujs/core": "^0.9.
|
|
17
|
+
"@mandujs/core": "^0.9.42",
|
|
14
18
|
"@radix-ui/react-slot": "^1.1.0",
|
|
15
19
|
"class-variance-authority": "^0.7.0",
|
|
16
20
|
"clsx": "^2.1.1",
|
|
@@ -19,12 +23,11 @@
|
|
|
19
23
|
"tailwind-merge": "^2.5.2"
|
|
20
24
|
},
|
|
21
25
|
"devDependencies": {
|
|
22
|
-
"@mandujs/cli": "^0.9.
|
|
26
|
+
"@mandujs/cli": "^0.9.42",
|
|
27
|
+
"@tailwindcss/cli": "^4.1.0",
|
|
23
28
|
"@types/react": "^19.2.0",
|
|
24
29
|
"@types/react-dom": "^19.2.0",
|
|
25
|
-
"
|
|
26
|
-
"postcss": "^8.4.47",
|
|
27
|
-
"tailwindcss": "^3.4.14",
|
|
30
|
+
"tailwindcss": "^4.1.0",
|
|
28
31
|
"typescript": "^5.0.0"
|
|
29
32
|
}
|
|
30
33
|
}
|