aiwf 0.3.21 → 0.3.23

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 (140) hide show
  1. package/package.json +3 -2
  2. package/src/cli/index.js +39 -336
  3. package/src/lib/installer.js +310 -90
  4. package/src/utils/messages.js +24 -0
  5. package/src/DEPENDENCY_MAP.md +0 -90
  6. package/src/cli/cache-cli.js +0 -459
  7. package/src/cli/checkpoint-cli.js +0 -417
  8. package/src/cli/language-cli.js +0 -286
  9. package/src/cli/sprint-cli.js +0 -287
  10. package/src/commands/ai-tool.js +0 -383
  11. package/src/commands/compress.js +0 -60
  12. package/src/commands/create-project.js +0 -160
  13. package/src/commands/evaluate.js +0 -459
  14. package/src/commands/persona.js +0 -309
  15. package/src/commands/sprint-task.js +0 -262
  16. package/src/commands/token.js +0 -312
  17. package/src/lib/ai-persona-manager.js +0 -711
  18. package/src/lib/cache-system.js +0 -332
  19. package/src/lib/context-engine.js +0 -568
  20. package/src/lib/github-integration.js +0 -402
  21. package/src/lib/memory-profiler.js +0 -471
  22. package/src/lib/metrics-collector.js +0 -885
  23. package/src/lib/offline-detector.js +0 -386
  24. package/src/lib/resource-loader-enhanced.js +0 -399
  25. package/src/lib/resource-loader.js +0 -250
  26. package/src/lib/resources/commands/ai-persona.js +0 -602
  27. package/src/lib/resources/commands/compress-context.js +0 -389
  28. package/src/lib/resources/commands/evaluate.js +0 -246
  29. package/src/lib/resources/commands/persona-context-apply.js +0 -562
  30. package/src/lib/resources/commands/token-tracking.js +0 -443
  31. package/src/lib/resources/config/commit-patterns.js +0 -103
  32. package/src/lib/resources/config/language.json +0 -6
  33. package/src/lib/resources/personas/PERSONA_INDEX.md +0 -57
  34. package/src/lib/resources/personas/analyst.json +0 -44
  35. package/src/lib/resources/personas/architect/best_practices.md +0 -136
  36. package/src/lib/resources/personas/architect/knowledge_base.md +0 -377
  37. package/src/lib/resources/personas/architect.json +0 -44
  38. package/src/lib/resources/personas/backend/best_practices.md +0 -766
  39. package/src/lib/resources/personas/backend/knowledge_base.md +0 -1070
  40. package/src/lib/resources/personas/data_analyst/best_practices.md +0 -594
  41. package/src/lib/resources/personas/data_analyst/knowledge_base.md +0 -1057
  42. package/src/lib/resources/personas/developer.json +0 -44
  43. package/src/lib/resources/personas/developer.md +0 -37
  44. package/src/lib/resources/personas/evaluation_criteria.json +0 -137
  45. package/src/lib/resources/personas/frontend/best_practices.md +0 -445
  46. package/src/lib/resources/personas/frontend/knowledge_base.md +0 -729
  47. package/src/lib/resources/personas/persona-index.json +0 -31
  48. package/src/lib/resources/personas/reviewer.json +0 -44
  49. package/src/lib/resources/personas/security/best_practices.md +0 -307
  50. package/src/lib/resources/personas/security/knowledge_base.md +0 -498
  51. package/src/lib/resources/personas/tester.json +0 -44
  52. package/src/lib/resources/templates/README.md +0 -78
  53. package/src/lib/resources/templates/api-server/config.json +0 -64
  54. package/src/lib/resources/templates/api-server/template/.aiwf/config.json +0 -56
  55. package/src/lib/resources/templates/api-server/template/.aiwf/feature-ledger.json +0 -42
  56. package/src/lib/resources/templates/api-server/template/.aiwf/personas/backend-engineer.json +0 -40
  57. package/src/lib/resources/templates/api-server/template/.aiwf/scripts/cli.js +0 -111
  58. package/src/lib/resources/templates/api-server/template/.env.example +0 -29
  59. package/src/lib/resources/templates/api-server/template/.eslintrc.json +0 -23
  60. package/src/lib/resources/templates/api-server/template/README.md +0 -171
  61. package/src/lib/resources/templates/api-server/template/jest.config.js +0 -26
  62. package/src/lib/resources/templates/api-server/template/nodemon.json +0 -9
  63. package/src/lib/resources/templates/api-server/template/package.json +0 -61
  64. package/src/lib/resources/templates/api-server/template/src/app.ts +0 -60
  65. package/src/lib/resources/templates/api-server/template/src/config/swagger.ts +0 -38
  66. package/src/lib/resources/templates/api-server/template/src/controllers/aiwfController.ts +0 -54
  67. package/src/lib/resources/templates/api-server/template/src/controllers/authController.ts +0 -128
  68. package/src/lib/resources/templates/api-server/template/src/controllers/statusController.ts +0 -28
  69. package/src/lib/resources/templates/api-server/template/src/index.ts +0 -28
  70. package/src/lib/resources/templates/api-server/template/src/middleware/aiwfMiddleware.ts +0 -50
  71. package/src/lib/resources/templates/api-server/template/src/middleware/authMiddleware.ts +0 -41
  72. package/src/lib/resources/templates/api-server/template/src/middleware/errorHandler.ts +0 -42
  73. package/src/lib/resources/templates/api-server/template/src/middleware/notFoundHandler.ts +0 -14
  74. package/src/lib/resources/templates/api-server/template/src/middleware/requestLogger.ts +0 -20
  75. package/src/lib/resources/templates/api-server/template/src/routes/aiwf.ts +0 -31
  76. package/src/lib/resources/templates/api-server/template/src/routes/index.ts +0 -13
  77. package/src/lib/resources/templates/api-server/template/src/routes/v1/index.ts +0 -67
  78. package/src/lib/resources/templates/api-server/template/src/utils/logger.ts +0 -37
  79. package/src/lib/resources/templates/api-server/template/tests/app.test.ts +0 -40
  80. package/src/lib/resources/templates/api-server/template/tsconfig.json +0 -43
  81. package/src/lib/resources/templates/npm-library/config.json +0 -61
  82. package/src/lib/resources/templates/npm-library/template/LICENSE +0 -21
  83. package/src/lib/resources/templates/npm-library/template/README.md +0 -201
  84. package/src/lib/resources/templates/npm-library/template/package.json +0 -78
  85. package/src/lib/resources/templates/web-app/config.json +0 -53
  86. package/src/lib/resources/templates/web-app/template/.aiwf/config.json +0 -54
  87. package/src/lib/resources/templates/web-app/template/.aiwf/feature-ledger.json +0 -37
  88. package/src/lib/resources/templates/web-app/template/.aiwf/personas/fullstack-developer.json +0 -40
  89. package/src/lib/resources/templates/web-app/template/.aiwf/scripts/cli.js +0 -110
  90. package/src/lib/resources/templates/web-app/template/.eslintrc.cjs +0 -20
  91. package/src/lib/resources/templates/web-app/template/README.md +0 -151
  92. package/src/lib/resources/templates/web-app/template/index.html +0 -14
  93. package/src/lib/resources/templates/web-app/template/package.json +0 -44
  94. package/src/lib/resources/templates/web-app/template/postcss.config.js +0 -6
  95. package/src/lib/resources/templates/web-app/template/public/vite.svg +0 -1
  96. package/src/lib/resources/templates/web-app/template/src/App.tsx +0 -21
  97. package/src/lib/resources/templates/web-app/template/src/components/Layout.tsx +0 -57
  98. package/src/lib/resources/templates/web-app/template/src/components/aiwf/ContextStatus.tsx +0 -107
  99. package/src/lib/resources/templates/web-app/template/src/components/aiwf/TokenUsage.tsx +0 -71
  100. package/src/lib/resources/templates/web-app/template/src/index.css +0 -60
  101. package/src/lib/resources/templates/web-app/template/src/main.tsx +0 -10
  102. package/src/lib/resources/templates/web-app/template/src/pages/AiwfDashboard.tsx +0 -57
  103. package/src/lib/resources/templates/web-app/template/src/pages/HomePage.tsx +0 -89
  104. package/src/lib/resources/templates/web-app/template/src/pages/NotFound.tsx +0 -25
  105. package/src/lib/resources/templates/web-app/template/src/stores/aiwfStore.ts +0 -126
  106. package/src/lib/resources/templates/web-app/template/src/types/global.d.ts +0 -9
  107. package/src/lib/resources/templates/web-app/template/src/vite-env.d.ts +0 -1
  108. package/src/lib/resources/templates/web-app/template/tailwind.config.js +0 -30
  109. package/src/lib/resources/templates/web-app/template/tsconfig.json +0 -37
  110. package/src/lib/resources/templates/web-app/template/tsconfig.node.json +0 -10
  111. package/src/lib/resources/templates/web-app/template/vite.config.ts +0 -23
  112. package/src/lib/resources/utils/background-monitor.js +0 -223
  113. package/src/lib/resources/utils/compression-metrics.js +0 -1031
  114. package/src/lib/resources/utils/compression-strategies.js +0 -557
  115. package/src/lib/resources/utils/content-normalizer.js +0 -455
  116. package/src/lib/resources/utils/context-compressor.js +0 -583
  117. package/src/lib/resources/utils/context-rule-parser.js +0 -180
  118. package/src/lib/resources/utils/context-token-monitor.js +0 -463
  119. package/src/lib/resources/utils/context-update-manager.js +0 -502
  120. package/src/lib/resources/utils/git-utils.js +0 -220
  121. package/src/lib/resources/utils/importance-classifier.js +0 -645
  122. package/src/lib/resources/utils/information-filter.js +0 -710
  123. package/src/lib/resources/utils/persona-aware-compressor.js +0 -598
  124. package/src/lib/resources/utils/prompt-injector.js +0 -242
  125. package/src/lib/resources/utils/simplified-evaluator.js +0 -119
  126. package/src/lib/resources/utils/text-summarizer.js +0 -492
  127. package/src/lib/resources/utils/token-counter.js +0 -177
  128. package/src/lib/resources/utils/token-monitor.js +0 -484
  129. package/src/lib/resources/utils/token-reporter.js +0 -452
  130. package/src/lib/resources/utils/token-storage.js +0 -433
  131. package/src/lib/resources/utils/token-tracker.js +0 -283
  132. package/src/lib/state/priority-calculator.js +0 -217
  133. package/src/lib/state/state-index.js +0 -155
  134. package/src/lib/state/task-scanner.js +0 -328
  135. package/src/lib/task-analyzer.js +0 -599
  136. package/src/lib/template-cache-system.js +0 -559
  137. package/src/lib/template-downloader.js +0 -467
  138. package/src/lib/template-version-manager.js +0 -491
  139. package/src/lib/token-optimizer.js +0 -517
  140. package/src/utils/engineering-guard.js +0 -399
@@ -1,729 +0,0 @@
1
- # Frontend 페르소나 Knowledge Base
2
-
3
- ## 모던 프레임워크 가이드
4
-
5
- ### React 18+ 최신 기능
6
-
7
- #### Concurrent Features
8
- ```jsx
9
- // Suspense for Data Fetching
10
- const ProfilePage = () => {
11
- return (
12
- <Suspense fallback={<ProfileSkeleton />}>
13
- <ProfileDetails />
14
- <Suspense fallback={<PostsSkeleton />}>
15
- <ProfilePosts />
16
- </Suspense>
17
- </Suspense>
18
- );
19
- };
20
-
21
- // useTransition for Non-urgent Updates
22
- const SearchResults = () => {
23
- const [query, setQuery] = useState('');
24
- const [results, setResults] = useState([]);
25
- const [isPending, startTransition] = useTransition();
26
-
27
- const handleSearch = (e) => {
28
- setQuery(e.target.value);
29
-
30
- startTransition(() => {
31
- // 검색 결과 업데이트는 낮은 우선순위
32
- setResults(searchDatabase(e.target.value));
33
- });
34
- };
35
-
36
- return (
37
- <>
38
- <input value={query} onChange={handleSearch} />
39
- {isPending && <Spinner />}
40
- <SearchResultsList results={results} />
41
- </>
42
- );
43
- };
44
-
45
- // useDeferredValue for Debouncing
46
- const DebouncedResults = ({ query }) => {
47
- const deferredQuery = useDeferredValue(query);
48
-
49
- const results = useMemo(
50
- () => expensiveSearch(deferredQuery),
51
- [deferredQuery]
52
- );
53
-
54
- return <ResultsList results={results} />;
55
- };
56
- ```
57
-
58
- #### Server Components
59
- ```jsx
60
- // app/page.server.jsx (Server Component)
61
- async function BlogPost({ id }) {
62
- // 서버에서 직접 데이터 페칭
63
- const post = await db.posts.findUnique({ where: { id } });
64
-
65
- return (
66
- <article>
67
- <h1>{post.title}</h1>
68
- <div>{post.content}</div>
69
- <ClientInteractions postId={id} />
70
- </article>
71
- );
72
- }
73
-
74
- // app/ClientInteractions.client.jsx (Client Component)
75
- 'use client';
76
-
77
- function ClientInteractions({ postId }) {
78
- const [likes, setLikes] = useState(0);
79
-
80
- return (
81
- <button onClick={() => setLikes(l => l + 1)}>
82
- 좋아요 {likes}
83
- </button>
84
- );
85
- }
86
- ```
87
-
88
- ### Vue 3 Composition API
89
-
90
- #### Composables 패턴
91
- ```javascript
92
- // useCounter.js
93
- import { ref, computed } from 'vue';
94
-
95
- export function useCounter(initialValue = 0) {
96
- const count = ref(initialValue);
97
- const double = computed(() => count.value * 2);
98
-
99
- function increment() {
100
- count.value++;
101
- }
102
-
103
- function decrement() {
104
- count.value--;
105
- }
106
-
107
- return {
108
- count,
109
- double,
110
- increment,
111
- decrement
112
- };
113
- }
114
-
115
- // Component.vue
116
- <template>
117
- <div>
118
- <p>Count: {{ count }}</p>
119
- <p>Double: {{ double }}</p>
120
- <button @click="increment">+</button>
121
- <button @click="decrement">-</button>
122
- </div>
123
- </template>
124
-
125
- <script setup>
126
- import { useCounter } from './useCounter';
127
-
128
- const { count, double, increment, decrement } = useCounter(10);
129
- </script>
130
- ```
131
-
132
- #### Reactive State Management
133
- ```javascript
134
- // store/userStore.js
135
- import { reactive, readonly } from 'vue';
136
-
137
- const state = reactive({
138
- user: null,
139
- isLoading: false,
140
- error: null
141
- });
142
-
143
- const actions = {
144
- async fetchUser(userId) {
145
- state.isLoading = true;
146
- state.error = null;
147
-
148
- try {
149
- const response = await api.getUser(userId);
150
- state.user = response.data;
151
- } catch (error) {
152
- state.error = error.message;
153
- } finally {
154
- state.isLoading = false;
155
- }
156
- },
157
-
158
- logout() {
159
- state.user = null;
160
- }
161
- };
162
-
163
- export function useUserStore() {
164
- return {
165
- state: readonly(state),
166
- ...actions
167
- };
168
- }
169
- ```
170
-
171
- ## 상태 관리 패턴
172
-
173
- ### Redux Toolkit
174
- ```javascript
175
- // features/cart/cartSlice.js
176
- import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
177
-
178
- // Async thunk
179
- export const fetchCart = createAsyncThunk(
180
- 'cart/fetch',
181
- async (userId) => {
182
- const response = await api.getCart(userId);
183
- return response.data;
184
- }
185
- );
186
-
187
- const cartSlice = createSlice({
188
- name: 'cart',
189
- initialState: {
190
- items: [],
191
- status: 'idle',
192
- error: null
193
- },
194
- reducers: {
195
- addItem: (state, action) => {
196
- const existingItem = state.items.find(
197
- item => item.id === action.payload.id
198
- );
199
-
200
- if (existingItem) {
201
- existingItem.quantity += 1;
202
- } else {
203
- state.items.push({ ...action.payload, quantity: 1 });
204
- }
205
- },
206
- removeItem: (state, action) => {
207
- state.items = state.items.filter(
208
- item => item.id !== action.payload
209
- );
210
- },
211
- updateQuantity: (state, action) => {
212
- const item = state.items.find(
213
- item => item.id === action.payload.id
214
- );
215
- if (item) {
216
- item.quantity = action.payload.quantity;
217
- }
218
- }
219
- },
220
- extraReducers: (builder) => {
221
- builder
222
- .addCase(fetchCart.pending, (state) => {
223
- state.status = 'loading';
224
- })
225
- .addCase(fetchCart.fulfilled, (state, action) => {
226
- state.status = 'succeeded';
227
- state.items = action.payload;
228
- })
229
- .addCase(fetchCart.rejected, (state, action) => {
230
- state.status = 'failed';
231
- state.error = action.error.message;
232
- });
233
- }
234
- });
235
-
236
- export const { addItem, removeItem, updateQuantity } = cartSlice.actions;
237
- export default cartSlice.reducer;
238
- ```
239
-
240
- ### Zustand
241
- ```javascript
242
- // stores/useAppStore.js
243
- import { create } from 'zustand';
244
- import { devtools, persist } from 'zustand/middleware';
245
-
246
- const useAppStore = create(
247
- devtools(
248
- persist(
249
- (set, get) => ({
250
- // State
251
- user: null,
252
- theme: 'light',
253
- notifications: [],
254
-
255
- // Actions
256
- setUser: (user) => set({ user }),
257
-
258
- toggleTheme: () => set((state) => ({
259
- theme: state.theme === 'light' ? 'dark' : 'light'
260
- })),
261
-
262
- addNotification: (notification) => set((state) => ({
263
- notifications: [...state.notifications, {
264
- id: Date.now(),
265
- ...notification
266
- }]
267
- })),
268
-
269
- removeNotification: (id) => set((state) => ({
270
- notifications: state.notifications.filter(n => n.id !== id)
271
- })),
272
-
273
- // Computed/Derived state
274
- get hasUnreadNotifications() {
275
- return get().notifications.some(n => !n.read);
276
- }
277
- }),
278
- {
279
- name: 'app-storage',
280
- partialize: (state) => ({ user: state.user, theme: state.theme })
281
- }
282
- )
283
- )
284
- );
285
- ```
286
-
287
- ## CSS 아키텍처
288
-
289
- ### CSS Custom Properties (CSS Variables)
290
- ```css
291
- /* Design Tokens */
292
- :root {
293
- /* Colors */
294
- --color-primary-50: #eff6ff;
295
- --color-primary-100: #dbeafe;
296
- --color-primary-500: #3b82f6;
297
- --color-primary-900: #1e3a8a;
298
-
299
- /* Spacing */
300
- --space-xs: 0.25rem;
301
- --space-sm: 0.5rem;
302
- --space-md: 1rem;
303
- --space-lg: 1.5rem;
304
- --space-xl: 2rem;
305
-
306
- /* Typography */
307
- --font-sans: system-ui, -apple-system, sans-serif;
308
- --font-mono: 'Fira Code', monospace;
309
-
310
- /* Shadows */
311
- --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
312
- --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1);
313
-
314
- /* Animations */
315
- --transition-fast: 150ms ease;
316
- --transition-base: 250ms ease;
317
- }
318
-
319
- /* Dark Mode */
320
- [data-theme="dark"] {
321
- --color-primary-50: #1e3a8a;
322
- --color-primary-500: #60a5fa;
323
- }
324
-
325
- /* Component using tokens */
326
- .button {
327
- background: var(--color-primary-500);
328
- padding: var(--space-sm) var(--space-md);
329
- box-shadow: var(--shadow-sm);
330
- transition: all var(--transition-base);
331
- }
332
- ```
333
-
334
- ### CSS Grid Layout Systems
335
- ```css
336
- /* Responsive Grid System */
337
- .grid {
338
- display: grid;
339
- gap: var(--space-md);
340
- grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
341
- }
342
-
343
- /* Advanced Grid Layout */
344
- .dashboard {
345
- display: grid;
346
- grid-template-areas:
347
- "header header header"
348
- "sidebar main aside"
349
- "footer footer footer";
350
- grid-template-columns: 200px 1fr 300px;
351
- grid-template-rows: auto 1fr auto;
352
- min-height: 100vh;
353
- }
354
-
355
- .header { grid-area: header; }
356
- .sidebar { grid-area: sidebar; }
357
- .main { grid-area: main; }
358
- .aside { grid-area: aside; }
359
- .footer { grid-area: footer; }
360
-
361
- /* Responsive adjustment */
362
- @media (max-width: 768px) {
363
- .dashboard {
364
- grid-template-areas:
365
- "header"
366
- "main"
367
- "sidebar"
368
- "aside"
369
- "footer";
370
- grid-template-columns: 1fr;
371
- }
372
- }
373
- ```
374
-
375
- ## 성능 최적화 기법
376
-
377
- ### 이미지 최적화
378
- ```jsx
379
- // Next.js Image Component
380
- import Image from 'next/image';
381
-
382
- const OptimizedImage = () => (
383
- <Image
384
- src="/hero.jpg"
385
- alt="Hero image"
386
- width={1200}
387
- height={600}
388
- priority // LCP 이미지에 사용
389
- placeholder="blur"
390
- blurDataURL={shimmer}
391
- sizes="(max-width: 768px) 100vw,
392
- (max-width: 1200px) 50vw,
393
- 33vw"
394
- />
395
- );
396
-
397
- // Progressive Image Loading
398
- const ProgressiveImage = ({ src, placeholder, alt }) => {
399
- const [imgSrc, setImgSrc] = useState(placeholder);
400
- const [isLoaded, setIsLoaded] = useState(false);
401
-
402
- useEffect(() => {
403
- const img = new Image();
404
- img.src = src;
405
- img.onload = () => {
406
- setImgSrc(src);
407
- setIsLoaded(true);
408
- };
409
- }, [src]);
410
-
411
- return (
412
- <div className={`image-wrapper ${isLoaded ? 'loaded' : ''}`}>
413
- <img src={imgSrc} alt={alt} />
414
- </div>
415
- );
416
- };
417
- ```
418
-
419
- ### 웹 워커 활용
420
- ```javascript
421
- // worker.js
422
- self.addEventListener('message', (e) => {
423
- const { type, data } = e.data;
424
-
425
- switch(type) {
426
- case 'PROCESS_DATA':
427
- const result = expensiveDataProcessing(data);
428
- self.postMessage({ type: 'DATA_PROCESSED', result });
429
- break;
430
-
431
- case 'GENERATE_REPORT':
432
- const report = generateComplexReport(data);
433
- self.postMessage({ type: 'REPORT_READY', report });
434
- break;
435
- }
436
- });
437
-
438
- // Main thread
439
- const worker = new Worker('/worker.js');
440
-
441
- worker.postMessage({
442
- type: 'PROCESS_DATA',
443
- data: largeDataset
444
- });
445
-
446
- worker.addEventListener('message', (e) => {
447
- if (e.data.type === 'DATA_PROCESSED') {
448
- updateUI(e.data.result);
449
- }
450
- });
451
- ```
452
-
453
- ## 접근성 구현
454
-
455
- ### ARIA 패턴
456
- ```jsx
457
- // Accessible Modal
458
- const Modal = ({ isOpen, onClose, title, children }) => {
459
- const modalRef = useRef();
460
- const previousActiveElement = useRef();
461
-
462
- useEffect(() => {
463
- if (isOpen) {
464
- previousActiveElement.current = document.activeElement;
465
- modalRef.current?.focus();
466
- } else {
467
- previousActiveElement.current?.focus();
468
- }
469
- }, [isOpen]);
470
-
471
- useEffect(() => {
472
- const handleEscape = (e) => {
473
- if (e.key === 'Escape') onClose();
474
- };
475
-
476
- if (isOpen) {
477
- document.addEventListener('keydown', handleEscape);
478
- document.body.style.overflow = 'hidden';
479
- }
480
-
481
- return () => {
482
- document.removeEventListener('keydown', handleEscape);
483
- document.body.style.overflow = '';
484
- };
485
- }, [isOpen, onClose]);
486
-
487
- if (!isOpen) return null;
488
-
489
- return createPortal(
490
- <div
491
- className="modal-overlay"
492
- onClick={onClose}
493
- aria-hidden="true"
494
- >
495
- <div
496
- ref={modalRef}
497
- className="modal"
498
- role="dialog"
499
- aria-modal="true"
500
- aria-labelledby="modal-title"
501
- onClick={e => e.stopPropagation()}
502
- tabIndex={-1}
503
- >
504
- <h2 id="modal-title">{title}</h2>
505
- <button
506
- className="modal-close"
507
- onClick={onClose}
508
- aria-label="닫기"
509
- >
510
- ×
511
- </button>
512
- {children}
513
- </div>
514
- </div>,
515
- document.body
516
- );
517
- };
518
- ```
519
-
520
- ### 폼 접근성
521
- ```jsx
522
- // Accessible Form with Validation
523
- const AccessibleForm = () => {
524
- const [errors, setErrors] = useState({});
525
- const [touched, setTouched] = useState({});
526
-
527
- return (
528
- <form noValidate onSubmit={handleSubmit}>
529
- <div className="form-group">
530
- <label htmlFor="email">
531
- 이메일 <span aria-label="필수">*</span>
532
- </label>
533
- <input
534
- id="email"
535
- type="email"
536
- aria-required="true"
537
- aria-invalid={!!errors.email}
538
- aria-describedby={errors.email ? "email-error" : undefined}
539
- onBlur={() => handleBlur('email')}
540
- />
541
- {errors.email && (
542
- <span id="email-error" role="alert" className="error">
543
- {errors.email}
544
- </span>
545
- )}
546
- </div>
547
-
548
- <fieldset>
549
- <legend>알림 설정</legend>
550
- <div className="radio-group" role="radiogroup">
551
- <input
552
- type="radio"
553
- id="notify-all"
554
- name="notifications"
555
- value="all"
556
- />
557
- <label htmlFor="notify-all">모든 알림 받기</label>
558
-
559
- <input
560
- type="radio"
561
- id="notify-important"
562
- name="notifications"
563
- value="important"
564
- />
565
- <label htmlFor="notify-important">중요 알림만</label>
566
-
567
- <input
568
- type="radio"
569
- id="notify-none"
570
- name="notifications"
571
- value="none"
572
- />
573
- <label htmlFor="notify-none">알림 받지 않기</label>
574
- </div>
575
- </fieldset>
576
-
577
- <button type="submit">제출</button>
578
- </form>
579
- );
580
- };
581
- ```
582
-
583
- ## 데이터 시각화
584
-
585
- ### Chart.js 고급 활용
586
- ```javascript
587
- // Responsive & Interactive Chart
588
- const InteractiveChart = ({ data }) => {
589
- const chartRef = useRef();
590
- const [chartInstance, setChartInstance] = useState(null);
591
-
592
- useEffect(() => {
593
- const ctx = chartRef.current.getContext('2d');
594
-
595
- const chart = new Chart(ctx, {
596
- type: 'line',
597
- data: {
598
- labels: data.labels,
599
- datasets: [{
600
- label: '매출',
601
- data: data.values,
602
- borderColor: 'rgb(75, 192, 192)',
603
- tension: 0.1,
604
- pointRadius: 4,
605
- pointHoverRadius: 6
606
- }]
607
- },
608
- options: {
609
- responsive: true,
610
- maintainAspectRatio: false,
611
- interaction: {
612
- mode: 'index',
613
- intersect: false
614
- },
615
- plugins: {
616
- tooltip: {
617
- callbacks: {
618
- label: (context) => {
619
- return `${context.dataset.label}: ${
620
- new Intl.NumberFormat('ko-KR', {
621
- style: 'currency',
622
- currency: 'KRW'
623
- }).format(context.parsed.y)
624
- }`;
625
- }
626
- }
627
- },
628
- zoom: {
629
- zoom: {
630
- wheel: { enabled: true },
631
- pinch: { enabled: true },
632
- mode: 'x'
633
- },
634
- pan: {
635
- enabled: true,
636
- mode: 'x'
637
- }
638
- }
639
- }
640
- }
641
- });
642
-
643
- setChartInstance(chart);
644
-
645
- return () => chart.destroy();
646
- }, [data]);
647
-
648
- return (
649
- <div className="chart-container">
650
- <canvas ref={chartRef} />
651
- </div>
652
- );
653
- };
654
- ```
655
-
656
- ### D3.js 커스텀 시각화
657
- ```javascript
658
- // Interactive Force-Directed Graph
659
- const ForceGraph = ({ nodes, links }) => {
660
- const svgRef = useRef();
661
-
662
- useEffect(() => {
663
- const width = 800;
664
- const height = 600;
665
-
666
- const svg = d3.select(svgRef.current)
667
- .attr('width', width)
668
- .attr('height', height);
669
-
670
- // Clear previous render
671
- svg.selectAll('*').remove();
672
-
673
- const simulation = d3.forceSimulation(nodes)
674
- .force('link', d3.forceLink(links).id(d => d.id))
675
- .force('charge', d3.forceManyBody().strength(-300))
676
- .force('center', d3.forceCenter(width / 2, height / 2));
677
-
678
- const link = svg.append('g')
679
- .selectAll('line')
680
- .data(links)
681
- .enter().append('line')
682
- .attr('stroke', '#999')
683
- .attr('stroke-opacity', 0.6);
684
-
685
- const node = svg.append('g')
686
- .selectAll('circle')
687
- .data(nodes)
688
- .enter().append('circle')
689
- .attr('r', d => d.size || 5)
690
- .attr('fill', d => d.color || '#69b3a2')
691
- .call(drag(simulation));
692
-
693
- node.append('title')
694
- .text(d => d.label);
695
-
696
- simulation.on('tick', () => {
697
- link
698
- .attr('x1', d => d.source.x)
699
- .attr('y1', d => d.source.y)
700
- .attr('x2', d => d.target.x)
701
- .attr('y2', d => d.target.y);
702
-
703
- node
704
- .attr('cx', d => d.x)
705
- .attr('cy', d => d.y);
706
- });
707
-
708
- function drag(simulation) {
709
- return d3.drag()
710
- .on('start', (event, d) => {
711
- if (!event.active) simulation.alphaTarget(0.3).restart();
712
- d.fx = d.x;
713
- d.fy = d.y;
714
- })
715
- .on('drag', (event, d) => {
716
- d.fx = event.x;
717
- d.fy = event.y;
718
- })
719
- .on('end', (event, d) => {
720
- if (!event.active) simulation.alphaTarget(0);
721
- d.fx = null;
722
- d.fy = null;
723
- });
724
- }
725
- }, [nodes, links]);
726
-
727
- return <svg ref={svgRef} />;
728
- };
729
- ```