@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
@@ -8,24 +8,34 @@ import type { ManduState } from '../../types';
8
8
  import { MANDU_CHARACTERS } from '../../types';
9
9
  import { colors, typography, animation, testIds } from '../../design-tokens';
10
10
 
11
- // ============================================================================
12
- // Styles
13
- // ============================================================================
14
-
15
11
  const styles = {
16
12
  container: {
17
13
  display: 'flex',
18
14
  alignItems: 'center',
19
15
  gap: '12px',
20
16
  padding: '12px 16px',
21
- borderRadius: '12px',
22
- backgroundColor: colors.background.medium,
17
+ borderRadius: '16px',
18
+ background: `linear-gradient(180deg, ${colors.background.medium}, ${colors.background.dark})`,
19
+ border: `1px solid ${colors.background.light}`,
20
+ boxShadow: '0 14px 32px rgba(7, 7, 12, 0.28)',
23
21
  transition: `all ${animation.duration.normal} ${animation.easing.easeOut}`,
24
22
  },
25
- emoji: {
26
- fontSize: '32px',
23
+ mark: {
24
+ display: 'flex',
25
+ alignItems: 'center',
26
+ justifyContent: 'center',
27
+ minWidth: '44px',
28
+ height: '44px',
29
+ padding: '0 12px',
30
+ borderRadius: '9999px',
31
+ fontSize: '11px',
27
32
  lineHeight: 1,
33
+ letterSpacing: '0.12em',
34
+ fontWeight: 700,
28
35
  userSelect: 'none' as const,
36
+ fontFamily: typography.fontFamily.mono,
37
+ textTransform: 'uppercase' as const,
38
+ border: `1px solid ${colors.background.light}`,
29
39
  },
30
40
  content: {
31
41
  display: 'flex',
@@ -35,7 +45,7 @@ const styles = {
35
45
  message: {
36
46
  fontSize: '14px',
37
47
  color: colors.text.primary,
38
- fontWeight: 500,
48
+ fontWeight: 600,
39
49
  },
40
50
  status: {
41
51
  fontSize: '12px',
@@ -51,10 +61,6 @@ const stateColors: Record<ManduState, string> = {
51
61
  hmr: colors.brand.accent,
52
62
  };
53
63
 
54
- // ============================================================================
55
- // Animation Keyframes (inline)
56
- // ============================================================================
57
-
58
64
  const bounceAnimation = `
59
65
  @keyframes mk-bounce {
60
66
  0%, 100% { transform: translateY(0); }
@@ -80,15 +86,11 @@ const shakeAnimation = `
80
86
  const sparkleAnimation = `
81
87
  @keyframes mk-sparkle {
82
88
  0% { transform: scale(1); }
83
- 50% { transform: scale(1.1); }
89
+ 50% { transform: scale(1.08); }
84
90
  100% { transform: scale(1); }
85
91
  }
86
92
  `;
87
93
 
88
- // ============================================================================
89
- // Props
90
- // ============================================================================
91
-
92
94
  export interface ManduCharacterProps {
93
95
  state: ManduState;
94
96
  errorCount?: number;
@@ -97,10 +99,6 @@ export interface ManduCharacterProps {
97
99
  onClick?: () => void;
98
100
  }
99
101
 
100
- // ============================================================================
101
- // Component
102
- // ============================================================================
103
-
104
102
  export function ManduCharacter({
105
103
  state,
106
104
  errorCount = 0,
@@ -126,25 +124,34 @@ export function ManduCharacter({
126
124
  }
127
125
  };
128
126
 
129
- const containerStyle = {
127
+ const containerStyle: React.CSSProperties = {
130
128
  ...styles.container,
131
129
  borderLeft: `4px solid ${stateColor}`,
132
130
  cursor: onClick ? 'pointer' : 'default',
133
131
  ...(compact && {
134
132
  padding: '8px 12px',
135
133
  gap: '8px',
134
+ borderRadius: '14px',
136
135
  }),
137
136
  };
138
137
 
139
- const emojiStyle = {
140
- ...styles.emoji,
138
+ const markStyle: React.CSSProperties = {
139
+ ...styles.mark,
141
140
  animation: getAnimation(),
142
- ...(compact && { fontSize: '24px' }),
141
+ color: stateColor,
142
+ backgroundColor: `${stateColor}18`,
143
+ borderColor: `${stateColor}45`,
144
+ boxShadow: `inset 0 1px 0 rgba(255, 255, 255, 0.03), 0 0 0 1px ${stateColor}10`,
145
+ ...(compact && {
146
+ minWidth: '36px',
147
+ height: '36px',
148
+ padding: '0 10px',
149
+ fontSize: '10px',
150
+ }),
143
151
  };
144
152
 
145
153
  return (
146
154
  <>
147
- {/* Inject keyframes */}
148
155
  <style>
149
156
  {bounceAnimation}
150
157
  {pulseAnimation}
@@ -166,8 +173,8 @@ export function ManduCharacter({
166
173
  }
167
174
  }}
168
175
  >
169
- <span style={emojiStyle} aria-hidden="true">
170
- {character.emoji}
176
+ <span style={markStyle} aria-hidden="true">
177
+ {character.mark}
171
178
  </span>
172
179
 
173
180
  {!compact && (
@@ -175,7 +182,7 @@ export function ManduCharacter({
175
182
  <span style={styles.message}>{character.message}</span>
176
183
  {errorCount > 0 && (
177
184
  <span style={styles.status}>
178
- {errorCount}개의 {state === 'error' ? '에러' : '경고'}가 있어요
185
+ {errorCount}개의 {state === 'error' ? '에러' : '경고'}가 있습니다
179
186
  </span>
180
187
  )}
181
188
  </div>
@@ -185,10 +192,6 @@ export function ManduCharacter({
185
192
  );
186
193
  }
187
194
 
188
- // ============================================================================
189
- // Badge Component (for mini display)
190
- // ============================================================================
191
-
192
195
  export interface ManduBadgeProps {
193
196
  state: ManduState;
194
197
  count?: number;
@@ -210,28 +213,28 @@ export function ManduBadge({
210
213
  display: 'flex',
211
214
  alignItems: 'center',
212
215
  justifyContent: 'center',
213
- width: '52px',
214
- height: '52px',
215
- padding: 0,
216
- borderRadius: '9999px',
217
- backgroundColor: isPressed
218
- ? colors.background.light
216
+ width: '72px',
217
+ height: '56px',
218
+ padding: '0 14px',
219
+ borderRadius: '18px',
220
+ background: isPressed
221
+ ? `linear-gradient(180deg, ${colors.background.light}, ${colors.background.medium})`
219
222
  : isHovered
220
- ? colors.background.medium
221
- : colors.background.dark,
222
- border: `2px solid ${isPressed ? stateColor : isHovered ? colors.brand.accent : stateColor + '70'}`,
223
+ ? `linear-gradient(180deg, ${colors.background.medium}, ${colors.background.dark})`
224
+ : `linear-gradient(180deg, ${colors.background.dark}, ${colors.background.medium})`,
225
+ border: `1px solid ${isPressed ? stateColor : isHovered ? colors.brand.accent : `${stateColor}80`}`,
223
226
  cursor: 'pointer',
224
227
  transition: `all 200ms ${animation.easing.spring}`,
225
228
  boxShadow: isPressed
226
- ? `0 2px 8px rgba(8, 6, 18, 0.4), inset 0 2px 4px rgba(0, 0, 0, 0.15)`
229
+ ? '0 8px 18px rgba(8, 6, 18, 0.36), inset 0 2px 4px rgba(0, 0, 0, 0.12)'
227
230
  : isHovered
228
- ? `0 8px 32px rgba(8, 6, 18, 0.45), 0 0 20px ${stateColor}55, 0 0 0 3px ${stateColor}20`
229
- : `0 4px 16px rgba(8, 6, 18, 0.35), 0 0 10px ${stateColor}30`,
231
+ ? `0 18px 36px rgba(8, 6, 18, 0.42), 0 0 0 4px ${stateColor}18`
232
+ : `0 10px 22px rgba(8, 6, 18, 0.3), 0 0 0 1px ${stateColor}14`,
230
233
  userSelect: 'none',
231
234
  transform: isPressed
232
235
  ? 'scale(0.92) translateY(1px)'
233
236
  : isHovered
234
- ? 'scale(1.1) translateY(-2px)'
237
+ ? 'scale(1.04) translateY(-2px)'
235
238
  : 'scale(1) translateY(0px)',
236
239
  outline: 'none',
237
240
  lineHeight: 1,
@@ -247,17 +250,29 @@ export function ManduBadge({
247
250
  };
248
251
 
249
252
  const textStyle: React.CSSProperties = {
253
+ display: 'flex',
254
+ flexDirection: 'column',
255
+ alignItems: 'flex-start',
256
+ gap: '3px',
250
257
  fontFamily: typography.fontFamily.sans,
251
258
  color: colors.brand.accent,
252
- fontSize: '14px',
259
+ fontSize: '13px',
253
260
  fontWeight: typography.fontWeight.bold,
254
261
  lineHeight: 1,
255
- letterSpacing: '0.5px',
262
+ letterSpacing: '0.08em',
256
263
  transition: `transform 200ms ${animation.easing.spring}`,
257
- transform: isHovered ? 'rotate(-8deg)' : 'rotate(0deg)',
264
+ transform: isHovered ? 'translateY(-1px)' : 'translateY(0px)',
258
265
  userSelect: 'none',
259
266
  };
260
267
 
268
+ const subtextStyle: React.CSSProperties = {
269
+ fontSize: '9px',
270
+ fontWeight: typography.fontWeight.medium,
271
+ letterSpacing: '0.16em',
272
+ color: colors.text.secondary,
273
+ textTransform: 'uppercase',
274
+ };
275
+
261
276
  const countBubbleStyle: React.CSSProperties = {
262
277
  position: 'absolute',
263
278
  top: '-4px',
@@ -286,14 +301,23 @@ export function ManduBadge({
286
301
  style={badgeStyle}
287
302
  onClick={onClick}
288
303
  onMouseEnter={() => setIsHovered(true)}
289
- onMouseLeave={() => { setIsHovered(false); setIsPressed(false); }}
304
+ onMouseLeave={() => {
305
+ setIsHovered(false);
306
+ setIsPressed(false);
307
+ }}
290
308
  onMouseDown={() => setIsPressed(true)}
291
309
  onMouseUp={() => setIsPressed(false)}
292
310
  onFocus={() => setIsHovered(true)}
293
- onBlur={() => { setIsHovered(false); setIsPressed(false); }}
311
+ onBlur={() => {
312
+ setIsHovered(false);
313
+ setIsPressed(false);
314
+ }}
294
315
  aria-label={`Mandu Kitchen: ${character.message}${count > 0 ? `, ${count} issues` : ''}`}
295
316
  >
296
- <span aria-hidden="true" style={textStyle}>MK</span>
317
+ <span aria-hidden="true" style={textStyle}>
318
+ <span>MK</span>
319
+ <span style={subtextStyle}>Dev</span>
320
+ </span>
297
321
  {count > 0 && (
298
322
  <span style={countBubbleStyle}>
299
323
  {count > 99 ? '99+' : count}
@@ -0,0 +1,219 @@
1
+ /**
2
+ * Mandu Kitchen DevTools - Diff Viewer
3
+ * @version 2.0.0
4
+ */
5
+
6
+ import React from 'react';
7
+ import { colors, typography, spacing, borderRadius, animation } from '../../../design-tokens';
8
+
9
+ // ============================================================================
10
+ // Types
11
+ // ============================================================================
12
+
13
+ export interface DiffLine {
14
+ type: 'add' | 'remove' | 'context';
15
+ content: string;
16
+ oldLine?: number;
17
+ newLine?: number;
18
+ }
19
+
20
+ export interface DiffHunk {
21
+ header: string;
22
+ lines: DiffLine[];
23
+ }
24
+
25
+ export interface FileDiff {
26
+ filePath: string;
27
+ hunks: DiffHunk[];
28
+ additions: number;
29
+ deletions: number;
30
+ isNew: boolean;
31
+ }
32
+
33
+ export interface DiffViewerProps {
34
+ diff: FileDiff;
35
+ onClose: () => void;
36
+ }
37
+
38
+ // ============================================================================
39
+ // Styles
40
+ // ============================================================================
41
+
42
+ const styles = {
43
+ container: {
44
+ display: 'flex',
45
+ flexDirection: 'column' as const,
46
+ height: '100%',
47
+ overflow: 'hidden',
48
+ },
49
+ header: {
50
+ display: 'flex',
51
+ alignItems: 'center',
52
+ justifyContent: 'space-between',
53
+ padding: `${spacing.sm} ${spacing.md}`,
54
+ borderBottom: `1px solid ${colors.background.light}`,
55
+ backgroundColor: colors.background.medium,
56
+ flexShrink: 0,
57
+ },
58
+ headerLeft: {
59
+ display: 'flex',
60
+ alignItems: 'center',
61
+ gap: spacing.sm,
62
+ overflow: 'hidden',
63
+ },
64
+ fileName: {
65
+ fontSize: typography.fontSize.sm,
66
+ fontFamily: typography.fontFamily.mono,
67
+ color: colors.text.primary,
68
+ overflow: 'hidden',
69
+ textOverflow: 'ellipsis',
70
+ whiteSpace: 'nowrap' as const,
71
+ },
72
+ stats: {
73
+ display: 'flex',
74
+ gap: spacing.sm,
75
+ fontSize: typography.fontSize.xs,
76
+ flexShrink: 0,
77
+ },
78
+ addStat: {
79
+ color: colors.semantic.success,
80
+ fontWeight: typography.fontWeight.medium,
81
+ },
82
+ removeStat: {
83
+ color: colors.semantic.error,
84
+ fontWeight: typography.fontWeight.medium,
85
+ },
86
+ closeBtn: {
87
+ background: 'transparent',
88
+ border: 'none',
89
+ color: colors.text.secondary,
90
+ fontSize: typography.fontSize.md,
91
+ cursor: 'pointer',
92
+ padding: spacing.xs,
93
+ borderRadius: borderRadius.sm,
94
+ transition: `color ${animation.duration.fast}`,
95
+ flexShrink: 0,
96
+ },
97
+ body: {
98
+ flex: 1,
99
+ overflow: 'auto',
100
+ fontFamily: typography.fontFamily.mono,
101
+ fontSize: typography.fontSize.xs,
102
+ lineHeight: typography.lineHeight.normal,
103
+ },
104
+ hunkHeader: {
105
+ padding: `${spacing.xs} ${spacing.md}`,
106
+ backgroundColor: `${colors.semantic.info}10`,
107
+ color: colors.semantic.info,
108
+ fontSize: typography.fontSize.xs,
109
+ borderBottom: `1px solid ${colors.background.light}`,
110
+ },
111
+ line: {
112
+ display: 'flex',
113
+ minHeight: '20px',
114
+ borderBottom: `1px solid ${colors.background.dark}`,
115
+ },
116
+ lineNumber: {
117
+ width: '40px',
118
+ padding: `0 ${spacing.xs}`,
119
+ textAlign: 'right' as const,
120
+ color: colors.text.muted,
121
+ userSelect: 'none' as const,
122
+ flexShrink: 0,
123
+ fontSize: typography.fontSize.xs,
124
+ lineHeight: '20px',
125
+ },
126
+ lineContent: {
127
+ flex: 1,
128
+ padding: `0 ${spacing.sm}`,
129
+ whiteSpace: 'pre' as const,
130
+ overflow: 'hidden',
131
+ textOverflow: 'ellipsis',
132
+ lineHeight: '20px',
133
+ },
134
+ emptyState: {
135
+ padding: spacing.xl,
136
+ textAlign: 'center' as const,
137
+ color: colors.text.muted,
138
+ fontSize: typography.fontSize.sm,
139
+ },
140
+ };
141
+
142
+ const lineStyles: Record<string, React.CSSProperties> = {
143
+ add: {
144
+ backgroundColor: `${colors.semantic.success}15`,
145
+ color: colors.text.primary,
146
+ },
147
+ remove: {
148
+ backgroundColor: `${colors.semantic.error}15`,
149
+ color: colors.text.primary,
150
+ },
151
+ context: {
152
+ backgroundColor: 'transparent',
153
+ color: colors.text.secondary,
154
+ },
155
+ };
156
+
157
+ const linePrefix: Record<string, string> = {
158
+ add: '+',
159
+ remove: '-',
160
+ context: ' ',
161
+ };
162
+
163
+ // ============================================================================
164
+ // Component
165
+ // ============================================================================
166
+
167
+ export function DiffViewer({ diff, onClose }: DiffViewerProps): React.ReactElement {
168
+ if (diff.hunks.length === 0) {
169
+ return (
170
+ <div style={styles.container}>
171
+ <div style={styles.header}>
172
+ <div style={styles.headerLeft}>
173
+ <span style={styles.fileName}>{diff.filePath}</span>
174
+ </div>
175
+ <button style={styles.closeBtn} onClick={onClose} aria-label="닫기">×</button>
176
+ </div>
177
+ <div style={styles.emptyState}>변경 사항 없음</div>
178
+ </div>
179
+ );
180
+ }
181
+
182
+ return (
183
+ <div style={styles.container}>
184
+ <div style={styles.header}>
185
+ <div style={styles.headerLeft}>
186
+ <span style={styles.fileName}>
187
+ {diff.isNew ? '(new) ' : ''}{diff.filePath}
188
+ </span>
189
+ <div style={styles.stats}>
190
+ <span style={styles.addStat}>+{diff.additions}</span>
191
+ <span style={styles.removeStat}>-{diff.deletions}</span>
192
+ </div>
193
+ </div>
194
+ <button style={styles.closeBtn} onClick={onClose} aria-label="닫기">×</button>
195
+ </div>
196
+
197
+ <div style={styles.body}>
198
+ {diff.hunks.map((hunk, hi) => (
199
+ <div key={hi}>
200
+ <div style={styles.hunkHeader}>{hunk.header}</div>
201
+ {hunk.lines.map((line, li) => (
202
+ <div key={li} style={{ ...styles.line, ...lineStyles[line.type] }}>
203
+ <span style={styles.lineNumber}>
204
+ {line.type !== 'add' ? (line.oldLine ?? '') : ''}
205
+ </span>
206
+ <span style={styles.lineNumber}>
207
+ {line.type !== 'remove' ? (line.newLine ?? '') : ''}
208
+ </span>
209
+ <span style={styles.lineContent}>
210
+ {linePrefix[line.type]}{line.content}
211
+ </span>
212
+ </div>
213
+ ))}
214
+ </div>
215
+ ))}
216
+ </div>
217
+ </div>
218
+ );
219
+ }
@@ -176,8 +176,8 @@ export function ErrorsPanel({
176
176
  <div style={styles.emptyState}>
177
177
  <ManduCharacter state="normal" compact />
178
178
  <p style={styles.emptyMessage}>
179
- 에러가 없어요!<br />
180
- 만두가 익고 있어요 🥟
179
+ 활성 오류가 없습니다.<br />
180
+ 이슈가 생기면 여기에 표시됩니다.
181
181
  </p>
182
182
  </div>
183
183
  </div>