@mandujs/core 0.9.39 → 0.9.40

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/README.ko.md +27 -0
  2. package/README.md +21 -5
  3. package/package.json +1 -1
  4. package/src/config/index.ts +1 -0
  5. package/src/config/mandu.ts +60 -0
  6. package/src/contract/client-safe.test.ts +42 -0
  7. package/src/contract/client-safe.ts +114 -0
  8. package/src/contract/client.ts +12 -11
  9. package/src/contract/handler.ts +10 -11
  10. package/src/contract/index.ts +25 -16
  11. package/src/contract/registry.test.ts +206 -0
  12. package/src/contract/registry.ts +568 -0
  13. package/src/contract/schema.ts +48 -12
  14. package/src/contract/types.ts +58 -35
  15. package/src/contract/validator.ts +32 -17
  16. package/src/filling/context.ts +103 -0
  17. package/src/generator/templates.ts +70 -17
  18. package/src/guard/analyzer.ts +9 -4
  19. package/src/guard/check.ts +66 -30
  20. package/src/guard/contract-guard.ts +9 -9
  21. package/src/guard/file-type.test.ts +24 -0
  22. package/src/guard/presets/index.ts +193 -60
  23. package/src/guard/rules.ts +12 -6
  24. package/src/guard/statistics.ts +6 -0
  25. package/src/guard/suggestions.ts +9 -2
  26. package/src/guard/types.ts +11 -1
  27. package/src/guard/validator.ts +160 -9
  28. package/src/guard/watcher.ts +2 -0
  29. package/src/index.ts +8 -1
  30. package/src/runtime/index.ts +1 -0
  31. package/src/runtime/streaming-ssr.ts +123 -2
  32. package/src/seo/index.ts +214 -0
  33. package/src/seo/integration/ssr.ts +307 -0
  34. package/src/seo/render/basic.ts +427 -0
  35. package/src/seo/render/index.ts +143 -0
  36. package/src/seo/render/jsonld.ts +539 -0
  37. package/src/seo/render/opengraph.ts +191 -0
  38. package/src/seo/render/robots.ts +116 -0
  39. package/src/seo/render/sitemap.ts +137 -0
  40. package/src/seo/render/twitter.ts +126 -0
  41. package/src/seo/resolve/index.ts +353 -0
  42. package/src/seo/resolve/opengraph.ts +143 -0
  43. package/src/seo/resolve/robots.ts +73 -0
  44. package/src/seo/resolve/title.ts +94 -0
  45. package/src/seo/resolve/twitter.ts +73 -0
  46. package/src/seo/resolve/url.ts +97 -0
  47. package/src/seo/routes/index.ts +290 -0
  48. package/src/seo/types.ts +575 -0
  49. package/src/slot/validator.ts +39 -16
@@ -23,94 +23,226 @@ export { atomicPreset, ATOMIC_HIERARCHY } from "./atomic";
23
23
  */
24
24
  export const manduPreset: PresetDefinition = {
25
25
  name: "mandu",
26
- description: "Mandu 권장 아키텍처 - FSD + Clean Architecture 조합",
26
+ description: "Mandu 권장 아키텍처 - client/server 분리 + strict shared",
27
27
 
28
28
  hierarchy: [
29
- // Frontend (FSD)
30
- "app",
31
- "pages",
32
- "widgets",
33
- "features",
34
- "entities",
35
- // Backend (Clean)
36
- "api",
37
- "application",
38
- "domain",
39
- "infra",
40
- // Shared
41
- "core",
42
- "shared",
29
+ // Client (FSD)
30
+ "client/app",
31
+ "client/pages",
32
+ "client/widgets",
33
+ "client/features",
34
+ "client/entities",
35
+ "client/shared",
36
+ // Shared (strict)
37
+ "shared/contracts",
38
+ "shared/types",
39
+ "shared/utils/client",
40
+ "shared/utils/server",
41
+ "shared/schema",
42
+ "shared/env",
43
+ "shared/unsafe",
44
+ // Server (Clean)
45
+ "server/api",
46
+ "server/application",
47
+ "server/domain",
48
+ "server/infra",
49
+ "server/core",
43
50
  ],
44
51
 
45
52
  layers: [
46
- // Frontend layers
53
+ // Client layers
47
54
  {
48
- name: "app",
49
- pattern: "src/app/**",
50
- canImport: ["pages", "widgets", "features", "entities", "shared"],
51
- description: "앱 진입점",
55
+ name: "client/app",
56
+ pattern: "src/client/app/**",
57
+ canImport: [
58
+ "client/pages",
59
+ "client/widgets",
60
+ "client/features",
61
+ "client/entities",
62
+ "client/shared",
63
+ "shared/contracts",
64
+ "shared/types",
65
+ "shared/utils/client",
66
+ ],
67
+ description: "클라이언트 앱 진입점",
52
68
  },
53
69
  {
54
- name: "pages",
55
- pattern: "src/pages/**",
56
- canImport: ["widgets", "features", "entities", "shared"],
57
- description: "페이지 컴포넌트",
70
+ name: "client/pages",
71
+ pattern: "src/client/pages/**",
72
+ canImport: [
73
+ "client/widgets",
74
+ "client/features",
75
+ "client/entities",
76
+ "client/shared",
77
+ "shared/contracts",
78
+ "shared/types",
79
+ "shared/utils/client",
80
+ ],
81
+ description: "클라이언트 페이지",
58
82
  },
59
83
  {
60
- name: "widgets",
61
- pattern: "src/widgets/**",
62
- canImport: ["features", "entities", "shared"],
63
- description: "독립적인 UI 블록",
84
+ name: "client/widgets",
85
+ pattern: "src/client/widgets/**",
86
+ canImport: [
87
+ "client/features",
88
+ "client/entities",
89
+ "client/shared",
90
+ "shared/contracts",
91
+ "shared/types",
92
+ "shared/utils/client",
93
+ ],
94
+ description: "클라이언트 위젯",
64
95
  },
65
96
  {
66
- name: "features",
67
- pattern: "src/features/**",
68
- canImport: ["entities", "shared"],
69
- description: "비즈니스 기능",
97
+ name: "client/features",
98
+ pattern: "src/client/features/**",
99
+ canImport: [
100
+ "client/entities",
101
+ "client/shared",
102
+ "shared/contracts",
103
+ "shared/types",
104
+ "shared/utils/client",
105
+ ],
106
+ description: "클라이언트 기능",
70
107
  },
71
108
  {
72
- name: "entities",
73
- pattern: "src/entities/**",
74
- canImport: ["shared"],
75
- description: "비즈니스 엔티티",
109
+ name: "client/entities",
110
+ pattern: "src/client/entities/**",
111
+ canImport: [
112
+ "client/shared",
113
+ "shared/contracts",
114
+ "shared/types",
115
+ "shared/utils/client",
116
+ ],
117
+ description: "클라이언트 엔티티",
76
118
  },
77
- // Backend layers
78
119
  {
79
- name: "api",
80
- pattern: "src/api/**",
81
- canImport: ["application", "domain", "core", "shared"],
82
- description: "API 라우트, 컨트롤러",
120
+ name: "client/shared",
121
+ pattern: "src/client/shared/**",
122
+ canImport: [
123
+ "shared/contracts",
124
+ "shared/types",
125
+ "shared/utils/client",
126
+ ],
127
+ description: "클라이언트 전용 공유",
83
128
  },
129
+ // Shared layers
84
130
  {
85
- name: "application",
86
- pattern: "src/application/**",
87
- canImport: ["domain", "core", "shared"],
88
- description: "유스케이스, 서비스",
131
+ name: "shared/contracts",
132
+ pattern: "src/shared/contracts/**",
133
+ canImport: ["shared/types", "shared/utils/client"],
134
+ description: "공용 계약 (클라이언트 safe)",
89
135
  },
90
136
  {
91
- name: "domain",
92
- pattern: "src/domain/**",
93
- canImport: ["shared"],
94
- description: "도메인 모델",
137
+ name: "shared/schema",
138
+ pattern: "src/shared/schema/**",
139
+ canImport: ["shared/types", "shared/utils/server"],
140
+ description: "서버 전용 스키마 (JSON/OpenAPI)",
95
141
  },
96
142
  {
97
- name: "infra",
98
- pattern: "src/infra/**",
99
- canImport: ["application", "domain", "core", "shared"],
100
- description: "인프라 구현",
143
+ name: "shared/types",
144
+ pattern: "src/shared/types/**",
145
+ canImport: [],
146
+ description: "공용 타입",
147
+ },
148
+ {
149
+ name: "shared/utils/client",
150
+ pattern: "src/shared/utils/client/**",
151
+ canImport: ["shared/types"],
152
+ description: "클라이언트 safe 유틸",
101
153
  },
102
- // Shared layers
103
154
  {
104
- name: "core",
105
- pattern: "src/core/**",
106
- canImport: ["shared"],
107
- description: "핵심 공통 (auth, config)",
155
+ name: "shared/utils/server",
156
+ pattern: "src/shared/utils/server/**",
157
+ canImport: ["shared/types", "shared/utils/client"],
158
+ description: "서버 전용 유틸",
108
159
  },
109
160
  {
110
- name: "shared",
161
+ name: "shared/env",
162
+ pattern: "src/shared/env/**",
163
+ canImport: ["shared/types", "shared/utils/client", "shared/utils/server"],
164
+ description: "서버 전용 환경/설정",
165
+ },
166
+ {
167
+ name: "shared/unsafe",
111
168
  pattern: "src/shared/**",
112
169
  canImport: [],
113
- description: "공유 유틸리티",
170
+ description: "금지된 shared 경로",
171
+ },
172
+ // Server layers
173
+ {
174
+ name: "server/api",
175
+ pattern: "src/server/api/**",
176
+ canImport: [
177
+ "server/application",
178
+ "server/domain",
179
+ "server/infra",
180
+ "server/core",
181
+ "shared/contracts",
182
+ "shared/schema",
183
+ "shared/types",
184
+ "shared/utils/client",
185
+ "shared/utils/server",
186
+ "shared/env",
187
+ ],
188
+ description: "서버 API 라우트, 컨트롤러",
189
+ },
190
+ {
191
+ name: "server/application",
192
+ pattern: "src/server/application/**",
193
+ canImport: [
194
+ "server/domain",
195
+ "server/core",
196
+ "shared/contracts",
197
+ "shared/schema",
198
+ "shared/types",
199
+ "shared/utils/client",
200
+ "shared/utils/server",
201
+ "shared/env",
202
+ ],
203
+ description: "서버 유스케이스, 서비스",
204
+ },
205
+ {
206
+ name: "server/domain",
207
+ pattern: "src/server/domain/**",
208
+ canImport: [
209
+ "shared/contracts",
210
+ "shared/schema",
211
+ "shared/types",
212
+ "shared/utils/client",
213
+ "shared/utils/server",
214
+ "shared/env",
215
+ ],
216
+ description: "서버 도메인 모델",
217
+ },
218
+ {
219
+ name: "server/infra",
220
+ pattern: "src/server/infra/**",
221
+ canImport: [
222
+ "server/application",
223
+ "server/domain",
224
+ "server/core",
225
+ "shared/contracts",
226
+ "shared/schema",
227
+ "shared/types",
228
+ "shared/utils/client",
229
+ "shared/utils/server",
230
+ "shared/env",
231
+ ],
232
+ description: "서버 인프라 구현",
233
+ },
234
+ {
235
+ name: "server/core",
236
+ pattern: "src/server/core/**",
237
+ canImport: [
238
+ "shared/contracts",
239
+ "shared/schema",
240
+ "shared/types",
241
+ "shared/utils/client",
242
+ "shared/utils/server",
243
+ "shared/env",
244
+ ],
245
+ description: "서버 핵심 공통",
114
246
  },
115
247
  ],
116
248
 
@@ -119,6 +251,7 @@ export const manduPreset: PresetDefinition = {
119
251
  circularDependency: "warn",
120
252
  crossSliceDependency: "warn",
121
253
  deepNesting: "info",
254
+ fileType: "error",
122
255
  },
123
256
  };
124
257
 
@@ -64,12 +64,18 @@ export const GUARD_RULES: Record<string, GuardRule> = {
64
64
  description: "Slot 핸들러에 ctx.ok(), ctx.json() 등의 응답 패턴이 없습니다",
65
65
  severity: "error",
66
66
  },
67
- SLOT_MISSING_FILLING_PATTERN: {
68
- id: "SLOT_MISSING_FILLING_PATTERN",
69
- name: "Slot Missing Filling Pattern",
70
- description: "Slot 파일에 Mandu.filling() 패턴이 없습니다",
71
- severity: "error",
72
- },
67
+ SLOT_MISSING_FILLING_PATTERN: {
68
+ id: "SLOT_MISSING_FILLING_PATTERN",
69
+ name: "Slot Missing Filling Pattern",
70
+ description: "Slot 파일에 Mandu.filling() 패턴이 없습니다",
71
+ severity: "error",
72
+ },
73
+ SLOT_ZOD_DIRECT_IMPORT: {
74
+ id: "SLOT_ZOD_DIRECT_IMPORT",
75
+ name: "Zod Direct Import in Slot",
76
+ description: "Slot 파일에서 zod를 직접 import 했습니다",
77
+ severity: "error",
78
+ },
73
79
  // Contract-related rules
74
80
  CONTRACT_MISSING: {
75
81
  id: "CONTRACT_MISSING",
@@ -568,5 +568,11 @@ function getTypeTitle(type: ViolationType): string {
568
568
  return "Cross-Slice Dependencies";
569
569
  case "deep-nesting":
570
570
  return "Deep Nesting";
571
+ case "file-type":
572
+ return "File Type Violations";
573
+ case "invalid-shared-segment":
574
+ return "Shared Segment Violations";
575
+ default:
576
+ return "Violations";
571
577
  }
572
578
  }
@@ -172,7 +172,7 @@ function generateCircularDependencySuggestions(context: SuggestionContext): stri
172
172
  */
173
173
  function generateCrossSliceSuggestions(context: SuggestionContext): string[] {
174
174
  const { fromLayer, importPath, slice } = context;
175
- const toSlice = extractSliceFromPath(importPath);
175
+ const toSlice = extractSliceFromPath(importPath, fromLayer);
176
176
  const suggestions: string[] = [];
177
177
 
178
178
  suggestions.push(
@@ -339,7 +339,14 @@ function extractModuleName(importPath: string): string {
339
339
  /**
340
340
  * 경로에서 슬라이스 추출
341
341
  */
342
- function extractSliceFromPath(importPath: string): string {
342
+ function extractSliceFromPath(importPath: string, fromLayer?: string): string {
343
343
  const parts = importPath.replace(/^[@~]\//, "").split("/");
344
+ if (fromLayer) {
345
+ const layerParts = fromLayer.split("/");
346
+ const matchesLayer = parts.slice(0, layerParts.length).join("/") === fromLayer;
347
+ if (matchesLayer && parts.length > layerParts.length) {
348
+ return parts[layerParts.length];
349
+ }
350
+ }
344
351
  return parts[1] ?? "unknown";
345
352
  }
@@ -37,6 +37,10 @@ export interface SeverityConfig {
37
37
  deepNesting?: Severity;
38
38
  /** 같은 레이어 내 슬라이스 간 의존 */
39
39
  crossSliceDependency?: Severity;
40
+ /** 파일 타입 위반 (.js/.jsx 금지 등) */
41
+ fileType?: Severity;
42
+ /** shared 하위 세그먼트 위반 */
43
+ invalidSharedSegment?: Severity;
40
44
  }
41
45
 
42
46
  /**
@@ -49,6 +53,8 @@ export interface FSRoutesGuardConfig {
49
53
  pageCanImport?: string[];
50
54
  /** layout.tsx가 import 가능한 레이어 */
51
55
  layoutCanImport?: string[];
56
+ /** route.ts가 import 가능한 레이어 */
57
+ routeCanImport?: string[];
52
58
  }
53
59
 
54
60
  /**
@@ -183,7 +189,9 @@ export type ViolationType =
183
189
  | "layer-violation" // 레이어 의존 위반
184
190
  | "circular-dependency" // 순환 의존
185
191
  | "cross-slice" // 같은 레이어 내 슬라이스 간 의존
186
- | "deep-nesting"; // 깊은 중첩 import
192
+ | "deep-nesting" // 깊은 중첩 import
193
+ | "file-type" // 파일 타입 위반
194
+ | "invalid-shared-segment"; // shared 세그먼트 위반
187
195
 
188
196
  /**
189
197
  * 아키텍처 위반
@@ -322,6 +330,8 @@ export const DEFAULT_GUARD_CONFIG: Required<Omit<GuardConfig, "preset" | "layers
322
330
  circularDependency: "warn",
323
331
  deepNesting: "info",
324
332
  crossSliceDependency: "warn",
333
+ fileType: "error",
334
+ invalidSharedSegment: "error",
325
335
  },
326
336
  realtimeOutput: "console",
327
337
  cache: true,
@@ -72,6 +72,8 @@ export function createViolation(
72
72
  "circular-dependency": "circularDependency",
73
73
  "cross-slice": "crossSliceDependency",
74
74
  "deep-nesting": "deepNesting",
75
+ "file-type": "fileType",
76
+ "invalid-shared-segment": "invalidSharedSegment",
75
77
  };
76
78
 
77
79
  const severity: Severity = severityConfig[severityMap[type]] ?? "error";
@@ -81,6 +83,8 @@ export function createViolation(
81
83
  "circular-dependency": "Circular Dependency",
82
84
  "cross-slice": "Cross-Slice Dependency",
83
85
  "deep-nesting": "Deep Nesting",
86
+ "file-type": "TypeScript Only",
87
+ "invalid-shared-segment": "Shared Segment",
84
88
  };
85
89
 
86
90
  const ruleDescriptions: Record<ViolationType, string> = {
@@ -88,6 +92,8 @@ export function createViolation(
88
92
  "circular-dependency": `순환 의존성이 감지되었습니다: ${fromLayer} ⇄ ${toLayer}`,
89
93
  "cross-slice": `같은 레이어 내 다른 슬라이스 간 직접 import가 감지되었습니다`,
90
94
  "deep-nesting": `깊은 경로 import가 감지되었습니다. Public API를 통해 import하세요`,
95
+ "file-type": `JS/JSX 파일은 금지됩니다. .ts/.tsx로 변환하세요`,
96
+ "invalid-shared-segment": `shared 하위 세그먼트 규칙을 위반했습니다`,
91
97
  };
92
98
 
93
99
  // 스마트 제안 생성
@@ -118,6 +124,98 @@ export function createViolation(
118
124
  };
119
125
  }
120
126
 
127
+ function createFileTypeViolation(
128
+ analysis: FileAnalysis,
129
+ severityConfig: SeverityConfig
130
+ ): Violation {
131
+ const severity: Severity = severityConfig.fileType ?? "error";
132
+ const normalizedPath = analysis.filePath.replace(/\\/g, "/");
133
+ const extension = normalizedPath.slice(normalizedPath.lastIndexOf("."));
134
+
135
+ return {
136
+ type: "file-type",
137
+ filePath: analysis.filePath,
138
+ line: 1,
139
+ column: 1,
140
+ importStatement: normalizedPath,
141
+ importPath: normalizedPath,
142
+ fromLayer: "typescript",
143
+ toLayer: extension,
144
+ ruleName: "TypeScript Only",
145
+ ruleDescription: `JS/JSX 파일은 금지됩니다 (${extension}). .ts/.tsx로 변환하세요`,
146
+ severity,
147
+ allowedLayers: [],
148
+ suggestions: [
149
+ "파일 확장자를 .ts 또는 .tsx로 변경하세요",
150
+ "필요한 타입을 추가하고 TypeScript로 변환하세요",
151
+ ],
152
+ };
153
+ }
154
+
155
+ function createInvalidSharedSegmentViolation(
156
+ analysis: FileAnalysis,
157
+ severityConfig: SeverityConfig
158
+ ): Violation {
159
+ const severity: Severity = severityConfig.invalidSharedSegment ?? "error";
160
+ const normalizedPath = analysis.filePath.replace(/\\/g, "/");
161
+ const marker = "src/shared/";
162
+ let segment = "(unknown)";
163
+
164
+ const index = normalizedPath.indexOf(marker);
165
+ if (index !== -1) {
166
+ const rest = normalizedPath.slice(index + marker.length);
167
+ segment = rest.split("/")[0] || "(root)";
168
+ }
169
+
170
+ return {
171
+ type: "invalid-shared-segment",
172
+ filePath: analysis.filePath,
173
+ line: 1,
174
+ column: 1,
175
+ importStatement: normalizedPath,
176
+ importPath: normalizedPath,
177
+ fromLayer: "shared",
178
+ toLayer: "shared/unsafe",
179
+ ruleName: "Shared Segment",
180
+ ruleDescription: `src/shared/${segment}는 허용되지 않습니다`,
181
+ severity,
182
+ allowedLayers: [],
183
+ suggestions: [
184
+ "허용 경로: src/shared/contracts|schema|types|utils/client|utils/server|env",
185
+ "파일을 허용된 shared 하위 폴더로 이동하세요",
186
+ ],
187
+ };
188
+ }
189
+
190
+ function createSharedEnvImportViolation(
191
+ analysis: FileAnalysis,
192
+ importInfo: ImportInfo,
193
+ fromLayer: string,
194
+ allowedLayers: string[],
195
+ severityConfig: SeverityConfig
196
+ ): Violation {
197
+ const severity: Severity = severityConfig.layerViolation ?? "error";
198
+
199
+ return {
200
+ type: "layer-violation",
201
+ filePath: analysis.filePath,
202
+ line: importInfo.line,
203
+ column: importInfo.column,
204
+ importStatement: importInfo.statement,
205
+ importPath: importInfo.path,
206
+ fromLayer,
207
+ toLayer: "shared/env",
208
+ ruleName: "Shared Env (Server-only)",
209
+ ruleDescription: "shared/env는 서버 전용입니다. 클라이언트 레이어에서 import할 수 없습니다",
210
+ severity,
211
+ allowedLayers,
212
+ suggestions: [
213
+ "환경변수 접근은 src/server 또는 app/api/route.ts에서 처리하세요",
214
+ "필요한 값은 서버에서 주입하거나 응답으로 전달하세요",
215
+ ],
216
+ };
217
+ }
218
+
121
219
  // ═══════════════════════════════════════════════════════════════════════════
122
220
  // File Validation
123
221
  // ═══════════════════════════════════════════════════════════════════════════
@@ -132,6 +230,15 @@ export function validateFileAnalysis(
132
230
  ): Violation[] {
133
231
  const violations: Violation[] = [];
134
232
  const severityConfig = config.severity ?? {};
233
+ const normalizedPath = analysis.filePath.toLowerCase();
234
+
235
+ if (normalizedPath.endsWith(".js") || normalizedPath.endsWith(".jsx")) {
236
+ violations.push(createFileTypeViolation(analysis, severityConfig));
237
+ }
238
+
239
+ if (analysis.layer === "shared/unsafe") {
240
+ violations.push(createInvalidSharedSegmentViolation(analysis, severityConfig));
241
+ }
135
242
 
136
243
  // FS Routes 규칙 검사 (app/ 내부)
137
244
  violations.push(...validateFsRoutesImports(analysis, layers, config));
@@ -167,6 +274,19 @@ export function validateFileAnalysis(
167
274
  continue; // 레이어를 알 수 없으면 무시
168
275
  }
169
276
 
277
+ if (toLayer === "shared/env" && fromLayer.startsWith("client/")) {
278
+ violations.push(
279
+ createSharedEnvImportViolation(
280
+ analysis,
281
+ importInfo,
282
+ fromLayer,
283
+ fromLayerDef?.canImport ?? [],
284
+ severityConfig
285
+ )
286
+ );
287
+ continue;
288
+ }
289
+
170
290
  // 같은 레이어 내 같은 슬라이스는 허용
171
291
  if (fromLayer === toLayer) {
172
292
  // Cross-slice 체크 (같은 레이어 내 다른 슬라이스)
@@ -257,8 +377,10 @@ function extractSliceFromImport(
257
377
  if (!layerRelative) return undefined;
258
378
 
259
379
  const parts = layerRelative.split("/");
260
- if (parts[0] === layer && parts.length > 1) {
261
- return parts[1];
380
+ const layerParts = layer.split("/");
381
+ const matchesLayer = parts.slice(0, layerParts.length).join("/") === layer;
382
+ if (matchesLayer && parts.length > layerParts.length) {
383
+ return parts[layerParts.length];
262
384
  }
263
385
 
264
386
  return undefined;
@@ -281,7 +403,11 @@ function validateFsRoutesImports(
281
403
  const violations: Violation[] = [];
282
404
  const severity = config.severity?.layerViolation ?? "error";
283
405
  const allowedLayers =
284
- fileType === "page" ? fsRoutesConfig.pageCanImport : fsRoutesConfig.layoutCanImport;
406
+ fileType === "page"
407
+ ? fsRoutesConfig.pageCanImport
408
+ : fileType === "layout"
409
+ ? fsRoutesConfig.layoutCanImport
410
+ : fsRoutesConfig.routeCanImport;
285
411
 
286
412
  for (const importInfo of analysis.imports) {
287
413
  const isFsRoutesImport = isFsRoutesImportPath(importInfo.path);
@@ -323,7 +449,32 @@ function validateFsRoutesImports(
323
449
  analysis.rootDir
324
450
  );
325
451
 
452
+ if (toLayer === "shared/env" && fileType !== "route") {
453
+ violations.push(
454
+ createSharedEnvImportViolation(
455
+ analysis,
456
+ importInfo,
457
+ fileType,
458
+ allowedLayers,
459
+ config.severity ?? {}
460
+ )
461
+ );
462
+ continue;
463
+ }
464
+
326
465
  if (toLayer && !allowedLayers.includes(toLayer)) {
466
+ const fileLabel = fileType === "route" ? "route.ts" : `${fileType}.tsx`;
467
+ const suggestions =
468
+ fileType === "route"
469
+ ? [
470
+ `허용 레이어: ${allowedLayers.join(", ")}`,
471
+ "서버 로직은 src/server 또는 src/shared로 이동하세요",
472
+ ]
473
+ : [
474
+ `허용 레이어: ${allowedLayers.join(", ")}`,
475
+ "클라이언트 로직은 src/client 또는 src/shared로 이동하세요",
476
+ ];
477
+
327
478
  violations.push({
328
479
  type: "layer-violation",
329
480
  filePath: analysis.filePath,
@@ -334,13 +485,10 @@ function validateFsRoutesImports(
334
485
  fromLayer: fileType,
335
486
  toLayer,
336
487
  ruleName: "FS Routes Import Rule",
337
- ruleDescription: `${fileType}.tsx는 지정된 레이어만 import 가능합니다`,
488
+ ruleDescription: `${fileLabel}는 지정된 레이어만 import 가능합니다`,
338
489
  severity,
339
490
  allowedLayers,
340
- suggestions: [
341
- `허용 레이어: ${allowedLayers.join(", ")}`,
342
- "필요한 모듈은 widgets/features/shared로 옮겨 사용하세요",
343
- ],
491
+ suggestions,
344
492
  });
345
493
  }
346
494
  }
@@ -349,7 +497,7 @@ function validateFsRoutesImports(
349
497
  return violations;
350
498
  }
351
499
 
352
- function getFsRouteFileType(analysis: FileAnalysis): "page" | "layout" | null {
500
+ function getFsRouteFileType(analysis: FileAnalysis): "page" | "layout" | "route" | null {
353
501
  const normalizedPath = normalizePathValue(
354
502
  analysis.rootDir
355
503
  ? relative(
@@ -372,6 +520,9 @@ function getFsRouteFileType(analysis: FileAnalysis): "page" | "layout" | null {
372
520
  if (FILE_PATTERNS.layout.test(fileName)) {
373
521
  return "layout";
374
522
  }
523
+ if (FILE_PATTERNS.route.test(fileName)) {
524
+ return "route";
525
+ }
375
526
 
376
527
  return null;
377
528
  }
@@ -345,6 +345,8 @@ function countByType(violations: Violation[]): Record<ViolationType, number> {
345
345
  "circular-dependency": 0,
346
346
  "cross-slice": 0,
347
347
  "deep-nesting": 0,
348
+ "file-type": 0,
349
+ "invalid-shared-segment": 0,
348
350
  };
349
351
 
350
352
  for (const v of violations) {