@mandujs/core 0.19.0 → 0.20.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.
Files changed (100) hide show
  1. package/README.ko.md +0 -14
  2. package/package.json +4 -1
  3. package/src/brain/architecture/analyzer.ts +4 -4
  4. package/src/brain/doctor/analyzer.ts +18 -14
  5. package/src/bundler/build.test.ts +127 -0
  6. package/src/bundler/build.ts +385 -115
  7. package/src/bundler/css.ts +20 -5
  8. package/src/bundler/dev.ts +55 -2
  9. package/src/bundler/prerender.ts +195 -0
  10. package/src/bundler/types.ts +20 -0
  11. package/src/change/snapshot.ts +4 -23
  12. package/src/change/types.ts +2 -3
  13. package/src/client/Form.tsx +105 -0
  14. package/src/client/__tests__/use-sse.test.ts +153 -0
  15. package/src/client/hooks.ts +105 -6
  16. package/src/client/index.ts +35 -6
  17. package/src/client/router.ts +670 -433
  18. package/src/client/rpc.ts +140 -0
  19. package/src/client/runtime.ts +24 -21
  20. package/src/client/use-fetch.ts +239 -0
  21. package/src/client/use-head.ts +197 -0
  22. package/src/client/use-sse.ts +378 -0
  23. package/src/components/Image.tsx +162 -0
  24. package/src/config/mandu.ts +5 -0
  25. package/src/config/validate.ts +34 -0
  26. package/src/content/index.ts +5 -1
  27. package/src/devtools/client/catchers/error-catcher.ts +17 -0
  28. package/src/devtools/client/catchers/network-proxy.ts +390 -367
  29. package/src/devtools/client/components/kitchen-root.tsx +479 -467
  30. package/src/devtools/client/components/mandu-character.tsx +77 -53
  31. package/src/devtools/client/components/panel/diff-viewer.tsx +219 -0
  32. package/src/devtools/client/components/panel/errors-panel.tsx +2 -2
  33. package/src/devtools/client/components/panel/guard-panel.tsx +363 -234
  34. package/src/devtools/client/components/panel/index.ts +45 -32
  35. package/src/devtools/client/components/panel/islands-panel.tsx +30 -14
  36. package/src/devtools/client/components/panel/network-panel.tsx +2 -3
  37. package/src/devtools/client/components/panel/panel-container.tsx +273 -100
  38. package/src/devtools/client/components/panel/preview-panel.tsx +212 -0
  39. package/src/devtools/client/state-manager.ts +535 -478
  40. package/src/devtools/design-tokens.ts +265 -264
  41. package/src/devtools/init.ts +1 -1
  42. package/src/devtools/types.ts +321 -295
  43. package/src/filling/filling.ts +328 -6
  44. package/src/filling/index.ts +5 -1
  45. package/src/filling/session.ts +216 -0
  46. package/src/filling/ws.ts +78 -0
  47. package/src/generator/generate.ts +2 -2
  48. package/src/guard/auto-correct.ts +0 -29
  49. package/src/guard/check.ts +14 -31
  50. package/src/guard/presets/index.ts +296 -294
  51. package/src/guard/rules.ts +15 -19
  52. package/src/guard/validator.ts +834 -834
  53. package/src/index.ts +6 -1
  54. package/src/island/index.ts +373 -304
  55. package/src/kitchen/api/contract-api.ts +225 -0
  56. package/src/kitchen/api/diff-parser.ts +108 -0
  57. package/src/kitchen/api/file-api.ts +273 -0
  58. package/src/kitchen/api/guard-api.ts +83 -0
  59. package/src/kitchen/api/guard-decisions.ts +100 -0
  60. package/src/kitchen/api/routes-api.ts +50 -0
  61. package/src/kitchen/index.ts +21 -0
  62. package/src/kitchen/kitchen-handler.ts +335 -0
  63. package/src/kitchen/kitchen-ui.ts +1732 -0
  64. package/src/kitchen/stream/activity-sse.ts +145 -0
  65. package/src/kitchen/stream/file-tailer.ts +99 -0
  66. package/src/middleware/compress.ts +62 -0
  67. package/src/middleware/cors.ts +47 -0
  68. package/src/middleware/index.ts +10 -0
  69. package/src/middleware/jwt.ts +134 -0
  70. package/src/middleware/logger.ts +58 -0
  71. package/src/middleware/timeout.ts +55 -0
  72. package/src/observability/event-bus.ts +79 -0
  73. package/src/observability/index.ts +8 -0
  74. package/src/observability/logger-adapter.ts +36 -0
  75. package/src/paths.ts +0 -4
  76. package/src/plugins/hooks.ts +64 -0
  77. package/src/plugins/index.ts +3 -0
  78. package/src/plugins/types.ts +5 -0
  79. package/src/report/build.ts +0 -6
  80. package/src/resource/__tests__/backward-compat.test.ts +0 -1
  81. package/src/router/fs-patterns.ts +11 -1
  82. package/src/router/fs-routes.ts +78 -14
  83. package/src/router/fs-scanner.ts +2 -2
  84. package/src/router/fs-types.ts +2 -1
  85. package/src/runtime/adapter-bun.ts +62 -0
  86. package/src/runtime/adapter.ts +47 -0
  87. package/src/runtime/cache.ts +310 -0
  88. package/src/runtime/handler.ts +65 -0
  89. package/src/runtime/image-handler.ts +195 -0
  90. package/src/runtime/index.ts +13 -0
  91. package/src/runtime/middleware.ts +263 -0
  92. package/src/runtime/ppr.ts +74 -0
  93. package/src/runtime/server.ts +706 -71
  94. package/src/runtime/ssr.ts +70 -31
  95. package/src/runtime/streaming-ssr.ts +121 -78
  96. package/src/spec/index.ts +0 -1
  97. package/src/spec/schema.ts +1 -0
  98. package/src/testing/index.ts +189 -0
  99. package/src/watcher/watcher.ts +27 -1
  100. package/src/spec/lock.ts +0 -56
@@ -1,244 +1,373 @@
1
- /**
2
- * Mandu Kitchen DevTools - Guard Panel
3
- * @version 1.0.3
4
- */
5
-
6
- import React, { useMemo } from 'react';
7
- import type { DevToolsGuardViolation } from '../../../types';
8
- import { colors, typography, spacing, borderRadius, animation } from '../../../design-tokens';
9
-
10
- // ============================================================================
11
- // Styles
12
- // ============================================================================
13
-
14
- const styles = {
15
- container: {
16
- padding: spacing.md,
17
- height: '100%',
18
- display: 'flex',
19
- flexDirection: 'column' as const,
20
- gap: spacing.md,
21
- },
22
- header: {
23
- display: 'flex',
24
- alignItems: 'center',
25
- justifyContent: 'space-between',
26
- },
27
- clearButton: {
28
- padding: `${spacing.xs} ${spacing.sm}`,
29
- borderRadius: borderRadius.sm,
30
- backgroundColor: colors.background.light,
31
- border: 'none',
32
- color: colors.text.secondary,
33
- fontSize: typography.fontSize.xs,
34
- cursor: 'pointer',
35
- transition: `all ${animation.duration.fast}`,
36
- },
37
- list: {
38
- flex: 1,
39
- overflow: 'auto',
40
- display: 'flex',
41
- flexDirection: 'column' as const,
42
- gap: spacing.sm,
43
- },
44
- emptyState: {
45
- flex: 1,
46
- display: 'flex',
47
- flexDirection: 'column' as const,
48
- alignItems: 'center',
49
- justifyContent: 'center',
50
- gap: spacing.md,
51
- padding: spacing.xl,
52
- color: colors.text.muted,
53
- fontSize: typography.fontSize.sm,
54
- textAlign: 'center' as const,
55
- },
56
- violationItem: {
57
- padding: spacing.md,
58
- borderRadius: borderRadius.md,
59
- backgroundColor: colors.background.medium,
60
- borderLeft: '3px solid',
61
- transition: `all ${animation.duration.fast}`,
62
- },
63
- violationHeader: {
64
- display: 'flex',
65
- alignItems: 'flex-start',
66
- justifyContent: 'space-between',
67
- gap: spacing.sm,
68
- marginBottom: spacing.xs,
69
- },
70
- ruleName: {
71
- display: 'flex',
72
- alignItems: 'center',
73
- gap: spacing.xs,
74
- },
75
- badge: {
76
- padding: `2px ${spacing.xs}`,
77
- borderRadius: borderRadius.sm,
78
- fontSize: typography.fontSize.xs,
79
- fontWeight: typography.fontWeight.medium,
80
- },
81
- message: {
82
- fontSize: typography.fontSize.sm,
83
- color: colors.text.primary,
84
- lineHeight: typography.lineHeight.normal,
85
- marginBottom: spacing.sm,
86
- },
87
- location: {
88
- display: 'flex',
89
- alignItems: 'center',
90
- gap: spacing.sm,
91
- fontSize: typography.fontSize.xs,
92
- fontFamily: typography.fontFamily.mono,
93
- color: colors.text.muted,
94
- },
95
- arrow: {
96
- color: colors.text.muted,
97
- },
98
- suggestion: {
99
- marginTop: spacing.sm,
100
- padding: spacing.sm,
101
- borderRadius: borderRadius.sm,
102
- backgroundColor: `${colors.semantic.info}10`,
103
- borderLeft: `2px solid ${colors.semantic.info}`,
104
- fontSize: typography.fontSize.xs,
105
- color: colors.text.secondary,
106
- lineHeight: typography.lineHeight.normal,
107
- },
108
- suggestionLabel: {
109
- fontWeight: typography.fontWeight.semibold,
110
- color: colors.semantic.info,
111
- marginBottom: '4px',
112
- },
113
- };
114
-
115
- const severityStyles: Record<string, { border: string; bg: string; color: string }> = {
116
- error: {
117
- border: colors.semantic.error,
118
- bg: `${colors.semantic.error}20`,
119
- color: colors.semantic.error,
120
- },
121
- warning: {
122
- border: colors.semantic.warning,
123
- bg: `${colors.semantic.warning}20`,
124
- color: colors.semantic.warning,
125
- },
126
- };
127
-
128
- // ============================================================================
129
- // Props
130
- // ============================================================================
131
-
132
- export interface GuardPanelProps {
133
- violations: DevToolsGuardViolation[];
134
- onClear: () => void;
135
- }
136
-
137
- // ============================================================================
138
- // Component
139
- // ============================================================================
140
-
141
- export function GuardPanel({ violations, onClear }: GuardPanelProps): React.ReactElement {
142
- // Group by rule
143
- const groupedByRule = useMemo(() => {
144
- const groups = new Map<string, DevToolsGuardViolation[]>();
145
- for (const v of violations) {
146
- const existing = groups.get(v.ruleId) ?? [];
147
- groups.set(v.ruleId, [...existing, v]);
148
- }
149
- return groups;
150
- }, [violations]);
151
-
152
- if (violations.length === 0) {
153
- return (
154
- <div style={styles.container}>
1
+ /**
2
+ * Mandu Kitchen DevTools - Guard Panel
3
+ * @version 1.0.3
4
+ */
5
+
6
+ import React, { useMemo, useState, useCallback } from 'react';
7
+ import type { DevToolsGuardViolation } from '../../../types';
8
+ import { colors, typography, spacing, borderRadius, animation } from '../../../design-tokens';
9
+
10
+ // ============================================================================
11
+ // Styles
12
+ // ============================================================================
13
+
14
+ const styles = {
15
+ container: {
16
+ padding: spacing.md,
17
+ height: '100%',
18
+ display: 'flex',
19
+ flexDirection: 'column' as const,
20
+ gap: spacing.md,
21
+ },
22
+ header: {
23
+ display: 'flex',
24
+ alignItems: 'center',
25
+ justifyContent: 'space-between',
26
+ },
27
+ clearButton: {
28
+ padding: `${spacing.xs} ${spacing.sm}`,
29
+ borderRadius: borderRadius.sm,
30
+ backgroundColor: colors.background.light,
31
+ border: 'none',
32
+ color: colors.text.secondary,
33
+ fontSize: typography.fontSize.xs,
34
+ cursor: 'pointer',
35
+ transition: `all ${animation.duration.fast}`,
36
+ },
37
+ list: {
38
+ flex: 1,
39
+ overflow: 'auto',
40
+ display: 'flex',
41
+ flexDirection: 'column' as const,
42
+ gap: spacing.sm,
43
+ },
44
+ emptyState: {
45
+ flex: 1,
46
+ display: 'flex',
47
+ flexDirection: 'column' as const,
48
+ alignItems: 'center',
49
+ justifyContent: 'center',
50
+ gap: spacing.md,
51
+ padding: spacing.xl,
52
+ color: colors.text.muted,
53
+ fontSize: typography.fontSize.sm,
54
+ textAlign: 'center' as const,
55
+ },
56
+ violationItem: {
57
+ padding: spacing.md,
58
+ borderRadius: borderRadius.md,
59
+ backgroundColor: colors.background.medium,
60
+ borderLeft: '3px solid',
61
+ transition: `all ${animation.duration.fast}`,
62
+ },
63
+ violationHeader: {
64
+ display: 'flex',
65
+ alignItems: 'flex-start',
66
+ justifyContent: 'space-between',
67
+ gap: spacing.sm,
68
+ marginBottom: spacing.xs,
69
+ },
70
+ ruleName: {
71
+ display: 'flex',
72
+ alignItems: 'center',
73
+ gap: spacing.xs,
74
+ },
75
+ badge: {
76
+ padding: `2px ${spacing.xs}`,
77
+ borderRadius: borderRadius.sm,
78
+ fontSize: typography.fontSize.xs,
79
+ fontWeight: typography.fontWeight.medium,
80
+ },
81
+ message: {
82
+ fontSize: typography.fontSize.sm,
83
+ color: colors.text.primary,
84
+ lineHeight: typography.lineHeight.normal,
85
+ marginBottom: spacing.sm,
86
+ },
87
+ location: {
88
+ display: 'flex',
89
+ alignItems: 'center',
90
+ gap: spacing.sm,
91
+ fontSize: typography.fontSize.xs,
92
+ fontFamily: typography.fontFamily.mono,
93
+ color: colors.text.muted,
94
+ },
95
+ arrow: {
96
+ color: colors.text.muted,
97
+ },
98
+ suggestion: {
99
+ marginTop: spacing.sm,
100
+ padding: spacing.sm,
101
+ borderRadius: borderRadius.sm,
102
+ backgroundColor: `${colors.semantic.info}10`,
103
+ borderLeft: `2px solid ${colors.semantic.info}`,
104
+ fontSize: typography.fontSize.xs,
105
+ color: colors.text.secondary,
106
+ lineHeight: typography.lineHeight.normal,
107
+ },
108
+ suggestionLabel: {
109
+ fontWeight: typography.fontWeight.semibold,
110
+ color: colors.semantic.info,
111
+ marginBottom: '4px',
112
+ },
113
+ };
114
+
115
+ const severityStyles: Record<string, { border: string; bg: string; color: string }> = {
116
+ error: {
117
+ border: colors.semantic.error,
118
+ bg: `${colors.semantic.error}20`,
119
+ color: colors.semantic.error,
120
+ },
121
+ warning: {
122
+ border: colors.semantic.warning,
123
+ bg: `${colors.semantic.warning}20`,
124
+ color: colors.semantic.warning,
125
+ },
126
+ };
127
+
128
+ // ============================================================================
129
+ // Props
130
+ // ============================================================================
131
+
132
+ export interface GuardDecisionState {
133
+ violationKey: string;
134
+ action: 'approve' | 'reject';
135
+ }
136
+
137
+ export interface GuardPanelProps {
138
+ violations: DevToolsGuardViolation[];
139
+ onClear: () => void;
140
+ }
141
+
142
+ // ============================================================================
143
+ // Component
144
+ // ============================================================================
145
+
146
+ export function GuardPanel({ violations, onClear }: GuardPanelProps): React.ReactElement {
147
+ const [scanning, setScanning] = useState(false);
148
+ const [scanResult, setScanResult] = useState<{ total: number; errors: number; warnings: number } | null>(null);
149
+ const [decisions, setDecisions] = useState<Map<string, 'approve' | 'reject'>>(new Map());
150
+ const [showApproved, setShowApproved] = useState(false);
151
+
152
+ const handleDecision = useCallback(async (ruleId: string, filePath: string, action: 'approve' | 'reject') => {
153
+ try {
154
+ const endpoint = action === 'approve' ? '/__kitchen/api/guard/approve' : '/__kitchen/api/guard/reject';
155
+ const res = await fetch(endpoint, {
156
+ method: 'POST',
157
+ headers: { 'Content-Type': 'application/json' },
158
+ body: JSON.stringify({ ruleId, filePath }),
159
+ });
160
+ if (res.ok) {
161
+ setDecisions(prev => {
162
+ const next = new Map(prev);
163
+ next.set(`${ruleId}::${filePath}`, action);
164
+ return next;
165
+ });
166
+ }
167
+ } catch {
168
+ // Kitchen API not available
169
+ }
170
+ }, []);
171
+
172
+ const handleScan = useCallback(async () => {
173
+ setScanning(true);
174
+ setScanResult(null);
175
+ try {
176
+ const res = await fetch('/__kitchen/api/guard/scan', { method: 'POST' });
177
+ if (res.ok) {
178
+ const data = await res.json();
179
+ if (data.report) {
180
+ setScanResult({
181
+ total: data.report.totalViolations,
182
+ errors: data.report.bySeverity?.error ?? 0,
183
+ warnings: data.report.bySeverity?.warn ?? data.report.bySeverity?.warning ?? 0,
184
+ });
185
+ }
186
+ }
187
+ } catch {
188
+ // Kitchen API not available (non-dev or older version)
189
+ } finally {
190
+ setScanning(false);
191
+ }
192
+ }, []);
193
+
194
+ // Group by rule
195
+ const groupedByRule = useMemo(() => {
196
+ const groups = new Map<string, DevToolsGuardViolation[]>();
197
+ for (const v of violations) {
198
+ const existing = groups.get(v.ruleId) ?? [];
199
+ groups.set(v.ruleId, [...existing, v]);
200
+ }
201
+ return groups;
202
+ }, [violations]);
203
+
204
+ if (violations.length === 0) {
205
+ return (
206
+ <div style={styles.container}>
155
207
  <div style={styles.emptyState}>
156
- 🛡️
157
208
  <p>
158
- 아키텍처 위반이 없어요!<br />
159
- 코드가 규칙을 따르고 있어요.
209
+ 현재 아키텍처 위반이 없습니다.<br />
210
+ 스캔 결과가 생기면 여기에 표시됩니다.
160
211
  </p>
161
- </div>
162
- </div>
163
- );
164
- }
165
-
166
- return (
167
- <div style={styles.container}>
168
- {/* Header */}
169
- <div style={styles.header}>
170
- <span style={{ fontSize: typography.fontSize.sm, color: colors.text.secondary }}>
171
- {violations.length}개의 위반 ({groupedByRule.size}개 규칙)
172
- </span>
173
- <button style={styles.clearButton} onClick={onClear}>
174
- 모두 지우기
175
- </button>
176
- </div>
177
-
178
- {/* Violation List */}
179
- <div style={styles.list}>
180
- {violations.map((violation) => {
181
- const severity = severityStyles[violation.severity] ?? severityStyles.warning;
182
-
183
- return (
184
- <div
185
- key={violation.id}
186
- style={{
187
- ...styles.violationItem,
188
- borderLeftColor: severity.border,
189
- }}
190
- >
191
- <div style={styles.violationHeader}>
192
- <div style={styles.ruleName}>
193
- <span
194
- style={{
195
- ...styles.badge,
196
- backgroundColor: severity.bg,
197
- color: severity.color,
198
- }}
212
+ <button
213
+ style={{
214
+ ...styles.clearButton,
215
+ padding: `${spacing.sm} ${spacing.md}`,
216
+ marginTop: spacing.sm,
217
+ opacity: scanning ? 0.5 : 1,
218
+ }}
219
+ onClick={handleScan}
220
+ disabled={scanning}
221
+ >
222
+ {scanning ? '스캔 중...' : '전체 스캔'}
223
+ </button>
224
+ {scanResult && (
225
+ <p style={{ marginTop: spacing.sm, fontSize: typography.fontSize.xs }}>
226
+ 스캔 결과: {scanResult.total === 0
227
+ ? '위반 없음'
228
+ : `${scanResult.total}개 위반 (에러 ${scanResult.errors}, 경고 ${scanResult.warnings})`
229
+ }
230
+ </p>
231
+ )}
232
+ </div>
233
+ </div>
234
+ );
235
+ }
236
+
237
+ return (
238
+ <div style={styles.container}>
239
+ {/* Header */}
240
+ <div style={styles.header}>
241
+ <span style={{ fontSize: typography.fontSize.sm, color: colors.text.secondary }}>
242
+ {violations.length}개의 위반 ({groupedByRule.size}개 규칙)
243
+ </span>
244
+ <div style={{ display: 'flex', gap: spacing.xs }}>
245
+ <button
246
+ style={styles.clearButton}
247
+ onClick={() => setShowApproved(!showApproved)}
248
+ >
249
+ {showApproved ? '승인 숨기기' : '승인 보기'}
250
+ </button>
251
+ <button
252
+ style={{ ...styles.clearButton, opacity: scanning ? 0.5 : 1 }}
253
+ onClick={handleScan}
254
+ disabled={scanning}
255
+ >
256
+ {scanning ? '스캔 중...' : '스캔'}
257
+ </button>
258
+ <button style={styles.clearButton} onClick={onClear}>
259
+ 모두 지우기
260
+ </button>
261
+ </div>
262
+ </div>
263
+
264
+ {/* Violation List */}
265
+ <div style={styles.list}>
266
+ {violations.map((violation) => {
267
+ const severity = severityStyles[violation.severity] ?? severityStyles.warning;
268
+ const decisionKey = `${violation.ruleId}::${violation.source.file}`;
269
+ const decision = decisions.get(decisionKey);
270
+ const isApproved = decision === 'approve';
271
+
272
+ if (isApproved && !showApproved) return null;
273
+
274
+ return (
275
+ <div
276
+ key={violation.id}
277
+ style={{
278
+ ...styles.violationItem,
279
+ borderLeftColor: severity.border,
280
+ opacity: isApproved ? 0.5 : 1,
281
+ }}
282
+ >
283
+ <div style={styles.violationHeader}>
284
+ <div style={styles.ruleName}>
285
+ <span
286
+ style={{
287
+ ...styles.badge,
288
+ backgroundColor: severity.bg,
289
+ color: severity.color,
290
+ }}
291
+ >
292
+ {violation.severity}
293
+ </span>
294
+ <span
295
+ style={{
296
+ ...styles.badge,
297
+ backgroundColor: colors.background.light,
298
+ color: colors.text.secondary,
299
+ }}
300
+ >
301
+ {violation.ruleName}
302
+ </span>
303
+ {isApproved && (
304
+ <span
305
+ style={{
306
+ ...styles.badge,
307
+ backgroundColor: `${colors.semantic.success}20`,
308
+ color: colors.semantic.success,
309
+ }}
310
+ >
311
+ Approved
312
+ </span>
313
+ )}
314
+ </div>
315
+ <div style={{ display: 'flex', gap: '4px' }}>
316
+ <button
317
+ style={{
318
+ ...styles.clearButton,
319
+ fontSize: typography.fontSize.xs,
320
+ padding: '2px 6px',
321
+ backgroundColor: isApproved ? `${colors.semantic.success}20` : colors.background.light,
322
+ color: isApproved ? colors.semantic.success : colors.text.secondary,
323
+ }}
324
+ onClick={() => handleDecision(violation.ruleId, violation.source.file, 'approve')}
199
325
  >
200
- {violation.severity}
201
- </span>
202
- <span
203
- style={{
204
- ...styles.badge,
205
- backgroundColor: colors.background.light,
206
- color: colors.text.secondary,
207
- }}
326
+ 허용
327
+ </button>
328
+ <button
329
+ style={{
330
+ ...styles.clearButton,
331
+ fontSize: typography.fontSize.xs,
332
+ padding: '2px 6px',
333
+ backgroundColor: decision === 'reject' ? `${colors.semantic.error}20` : colors.background.light,
334
+ color: decision === 'reject' ? colors.semantic.error : colors.text.secondary,
335
+ }}
336
+ onClick={() => handleDecision(violation.ruleId, violation.source.file, 'reject')}
208
337
  >
209
- {violation.ruleName}
210
- </span>
211
- </div>
212
- </div>
213
-
214
- <p style={styles.message}>{violation.message}</p>
215
-
216
- <div style={styles.location}>
217
- <span>
218
- {violation.source.file}
219
- {violation.source.line && `:${violation.source.line}`}
220
- </span>
221
- {violation.target && (
222
- <>
223
- <span style={styles.arrow}>→</span>
224
- <span>
225
- {violation.target.file}
226
- {violation.target.line && `:${violation.target.line}`}
227
- </span>
228
- </>
229
- )}
230
- </div>
231
-
338
+ 차단
339
+ </button>
340
+ </div>
341
+ </div>
342
+
343
+ <p style={styles.message}>{violation.message}</p>
344
+
345
+ <div style={styles.location}>
346
+ <span>
347
+ {violation.source.file}
348
+ {violation.source.line && `:${violation.source.line}`}
349
+ </span>
350
+ {violation.target && (
351
+ <>
352
+ <span style={styles.arrow}>→</span>
353
+ <span>
354
+ {violation.target.file}
355
+ {violation.target.line && `:${violation.target.line}`}
356
+ </span>
357
+ </>
358
+ )}
359
+ </div>
360
+
232
361
  {violation.suggestion && (
233
362
  <div style={styles.suggestion}>
234
- <div style={styles.suggestionLabel}>💡 제안</div>
363
+ <div style={styles.suggestionLabel}>제안</div>
235
364
  {violation.suggestion}
236
365
  </div>
237
366
  )}
238
- </div>
239
- );
240
- })}
241
- </div>
242
- </div>
243
- );
244
- }
367
+ </div>
368
+ );
369
+ })}
370
+ </div>
371
+ </div>
372
+ );
373
+ }