1/**
2 * ์น ์ ๊ทผ์ฑ ์์ JavaScript
3 * - ์์ฝ๋์ธ
4 * - ํญ ์ปดํฌ๋ํธ
5 * - ๋ชจ๋ฌ
6 * - ์ปค์คํ
๋ฆฌ์คํธ๋ฐ์ค
7 * - ํฌ์ปค์ค ๊ด๋ฆฌ
8 */
9
10// ============================================
11// ์์ฝ๋์ธ
12// ============================================
13function initAccordion() {
14 const button = document.getElementById('accordion-btn');
15 const content = document.getElementById('accordion-content');
16
17 if (!button || !content) return;
18
19 button.addEventListener('click', () => {
20 const isExpanded = button.getAttribute('aria-expanded') === 'true';
21
22 button.setAttribute('aria-expanded', !isExpanded);
23 content.hidden = isExpanded;
24 });
25}
26
27// ============================================
28// ํญ ์ปดํฌ๋ํธ
29// ============================================
30function initTabs() {
31 const tablist = document.querySelector('[role="tablist"]');
32 if (!tablist) return;
33
34 const tabs = tablist.querySelectorAll('[role="tab"]');
35 const panels = document.querySelectorAll('[role="tabpanel"]');
36
37 // ํญ ํด๋ฆญ ์ด๋ฒคํธ
38 tabs.forEach(tab => {
39 tab.addEventListener('click', () => activateTab(tab, tabs, panels));
40 });
41
42 // ํค๋ณด๋ ๋ค๋น๊ฒ์ด์
43 tablist.addEventListener('keydown', (e) => {
44 const currentTab = document.activeElement;
45 const currentIndex = Array.from(tabs).indexOf(currentTab);
46
47 let newIndex;
48
49 switch (e.key) {
50 case 'ArrowLeft':
51 newIndex = currentIndex - 1;
52 if (newIndex < 0) newIndex = tabs.length - 1;
53 break;
54 case 'ArrowRight':
55 newIndex = currentIndex + 1;
56 if (newIndex >= tabs.length) newIndex = 0;
57 break;
58 case 'Home':
59 newIndex = 0;
60 break;
61 case 'End':
62 newIndex = tabs.length - 1;
63 break;
64 default:
65 return;
66 }
67
68 e.preventDefault();
69 tabs[newIndex].focus();
70 activateTab(tabs[newIndex], tabs, panels);
71 });
72}
73
74function activateTab(selectedTab, tabs, panels) {
75 // ๋ชจ๋ ํญ ๋นํ์ฑํ
76 tabs.forEach(tab => {
77 tab.setAttribute('aria-selected', 'false');
78 tab.setAttribute('tabindex', '-1');
79 });
80
81 // ๋ชจ๋ ํจ๋ ์จ๊ธฐ๊ธฐ
82 panels.forEach(panel => {
83 panel.hidden = true;
84 });
85
86 // ์ ํ๋ ํญ ํ์ฑํ
87 selectedTab.setAttribute('aria-selected', 'true');
88 selectedTab.setAttribute('tabindex', '0');
89
90 // ํด๋น ํจ๋ ํ์
91 const panelId = selectedTab.getAttribute('aria-controls');
92 const panel = document.getElementById(panelId);
93 if (panel) {
94 panel.hidden = false;
95 }
96}
97
98// ============================================
99// Live Region ๋ฐ๋ชจ
100// ============================================
101function initLiveRegion() {
102 const button = document.getElementById('update-live-btn');
103 const liveRegion = document.getElementById('live-region');
104
105 if (!button || !liveRegion) return;
106
107 const messages = [
108 '์๋ก์ด ์๋ฆผ์ด ๋์ฐฉํ์ต๋๋ค.',
109 '์์
์ด ์๋ฃ๋์์ต๋๋ค.',
110 '3๊ฐ์ ์ ๋ฉ์์ง๊ฐ ์์ต๋๋ค.',
111 'ํ์ผ์ด ์ฑ๊ณต์ ์ผ๋ก ์
๋ก๋๋์์ต๋๋ค.',
112 '์ค์ ์ด ์ ์ฅ๋์์ต๋๋ค.'
113 ];
114
115 let messageIndex = 0;
116
117 button.addEventListener('click', () => {
118 liveRegion.textContent = messages[messageIndex];
119 messageIndex = (messageIndex + 1) % messages.length;
120 });
121}
122
123// ============================================
124// ๋ชจ๋ฌ
125// ============================================
126let previousFocusElement = null;
127
128function initModal() {
129 const openBtn = document.getElementById('open-modal-btn');
130 const modal = document.getElementById('modal');
131 const overlay = document.getElementById('modal-overlay');
132 const closeBtn = document.getElementById('modal-close');
133 const confirmBtn = document.getElementById('modal-confirm');
134
135 if (!openBtn || !modal) return;
136
137 // ์ด๊ธฐ
138 openBtn.addEventListener('click', () => openModal(modal, overlay));
139
140 // ๋ซ๊ธฐ ๋ฒํผ
141 closeBtn?.addEventListener('click', () => closeModal(modal, overlay));
142 confirmBtn?.addEventListener('click', () => {
143 alert('ํ์ธ๋์์ต๋๋ค!');
144 closeModal(modal, overlay);
145 });
146
147 // ์ค๋ฒ๋ ์ด ํด๋ฆญ
148 overlay?.addEventListener('click', () => closeModal(modal, overlay));
149
150 // ESC ํค
151 document.addEventListener('keydown', (e) => {
152 if (e.key === 'Escape' && !modal.hidden) {
153 closeModal(modal, overlay);
154 }
155 });
156
157 // ํฌ์ปค์ค ํธ๋ฉ
158 modal.addEventListener('keydown', (e) => {
159 if (e.key !== 'Tab') return;
160
161 const focusableElements = modal.querySelectorAll(
162 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
163 );
164 const firstElement = focusableElements[0];
165 const lastElement = focusableElements[focusableElements.length - 1];
166
167 if (e.shiftKey && document.activeElement === firstElement) {
168 e.preventDefault();
169 lastElement.focus();
170 } else if (!e.shiftKey && document.activeElement === lastElement) {
171 e.preventDefault();
172 firstElement.focus();
173 }
174 });
175}
176
177function openModal(modal, overlay) {
178 // ํ์ฌ ํฌ์ปค์ค ์ ์ฅ
179 previousFocusElement = document.activeElement;
180
181 // ๋ชจ๋ฌ ํ์
182 modal.hidden = false;
183 overlay.hidden = false;
184
185 // ๋ฐฐ๊ฒฝ ์คํฌ๋กค ๋ฐฉ์ง
186 document.body.style.overflow = 'hidden';
187
188 // ์ฒซ ๋ฒ์งธ ํฌ์ปค์ค ๊ฐ๋ฅํ ์์์ ํฌ์ปค์ค
189 const firstFocusable = modal.querySelector('button, [href], input');
190 if (firstFocusable) {
191 firstFocusable.focus();
192 }
193}
194
195function closeModal(modal, overlay) {
196 modal.hidden = true;
197 overlay.hidden = true;
198
199 // ๋ฐฐ๊ฒฝ ์คํฌ๋กค ๋ณต์
200 document.body.style.overflow = '';
201
202 // ์ด์ ํฌ์ปค์ค ๋ณต์
203 if (previousFocusElement) {
204 previousFocusElement.focus();
205 }
206}
207
208// ============================================
209// ์ปค์คํ
๋ฆฌ์คํธ๋ฐ์ค
210// ============================================
211function initListbox() {
212 const listbox = document.getElementById('custom-listbox');
213 const output = document.getElementById('listbox-output');
214
215 if (!listbox) return;
216
217 const options = listbox.querySelectorAll('[role="option"]');
218 let currentIndex = 0;
219
220 // ์ด๊ธฐ ์ ํ ์ํ ์ค์
221 updateSelection(options, currentIndex);
222
223 listbox.addEventListener('keydown', (e) => {
224 switch (e.key) {
225 case 'ArrowDown':
226 e.preventDefault();
227 currentIndex = Math.min(currentIndex + 1, options.length - 1);
228 updateSelection(options, currentIndex);
229 break;
230 case 'ArrowUp':
231 e.preventDefault();
232 currentIndex = Math.max(currentIndex - 1, 0);
233 updateSelection(options, currentIndex);
234 break;
235 case 'Home':
236 e.preventDefault();
237 currentIndex = 0;
238 updateSelection(options, currentIndex);
239 break;
240 case 'End':
241 e.preventDefault();
242 currentIndex = options.length - 1;
243 updateSelection(options, currentIndex);
244 break;
245 case 'Enter':
246 case ' ':
247 e.preventDefault();
248 selectOption(options[currentIndex], output);
249 break;
250 }
251 });
252
253 // ํด๋ฆญ์ผ๋ก ์ ํ
254 options.forEach((option, index) => {
255 option.addEventListener('click', () => {
256 currentIndex = index;
257 updateSelection(options, currentIndex);
258 selectOption(option, output);
259 });
260 });
261}
262
263function updateSelection(options, index) {
264 options.forEach((option, i) => {
265 option.setAttribute('aria-selected', i === index);
266 });
267
268 // listbox์ aria-activedescendant ์
๋ฐ์ดํธ
269 const listbox = options[0]?.parentElement;
270 if (listbox) {
271 listbox.setAttribute('aria-activedescendant', options[index].id);
272 }
273}
274
275function selectOption(option, output) {
276 const text = option.textContent.trim();
277 const fruitName = text.replace(/^.+\s/, ''); // ์ด๋ชจ์ง ์ ๊ฑฐ
278 output.textContent = `์ ํ๋ ํญ๋ชฉ: ${fruitName}`;
279}
280
281// ============================================
282// ํผ ์ ํจ์ฑ ๊ฒ์ฌ
283// ============================================
284function initFormValidation() {
285 const form = document.getElementById('accessible-form');
286 if (!form) return;
287
288 const emailInput = document.getElementById('email');
289 const emailError = document.getElementById('email-error');
290
291 // ์ค์๊ฐ ์ด๋ฉ์ผ ์ ํจ์ฑ ๊ฒ์ฌ
292 emailInput?.addEventListener('blur', () => {
293 validateEmail(emailInput, emailError);
294 });
295
296 emailInput?.addEventListener('input', () => {
297 if (emailInput.getAttribute('aria-invalid') === 'true') {
298 validateEmail(emailInput, emailError);
299 }
300 });
301
302 // ํผ ์ ์ถ
303 form.addEventListener('submit', (e) => {
304 e.preventDefault();
305
306 let isValid = true;
307
308 // ์ด๋ฉ์ผ ๊ฒ์ฆ
309 if (!validateEmail(emailInput, emailError)) {
310 isValid = false;
311 }
312
313 // ํ์ ํ๋ ๊ฒ์ฆ
314 const requiredFields = form.querySelectorAll('[required]');
315 requiredFields.forEach(field => {
316 if (!field.value.trim()) {
317 isValid = false;
318 field.setAttribute('aria-invalid', 'true');
319 } else {
320 field.setAttribute('aria-invalid', 'false');
321 }
322 });
323
324 if (isValid) {
325 alert('ํผ์ด ์ฑ๊ณต์ ์ผ๋ก ์ ์ถ๋์์ต๋๋ค!');
326 form.reset();
327 } else {
328 // ์ฒซ ๋ฒ์งธ ์ค๋ฅ ํ๋๋ก ํฌ์ปค์ค
329 const firstError = form.querySelector('[aria-invalid="true"]');
330 if (firstError) {
331 firstError.focus();
332 }
333 }
334 });
335}
336
337function validateEmail(input, errorElement) {
338 if (!input || !errorElement) return true;
339
340 const email = input.value.trim();
341 const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
342
343 if (!email) {
344 showError(input, errorElement, '์ด๋ฉ์ผ์ ์
๋ ฅํด์ฃผ์ธ์.');
345 return false;
346 }
347
348 if (!emailRegex.test(email)) {
349 showError(input, errorElement, '์ ํจํ ์ด๋ฉ์ผ ํ์์ด ์๋๋๋ค.');
350 return false;
351 }
352
353 hideError(input, errorElement);
354 return true;
355}
356
357function showError(input, errorElement, message) {
358 input.setAttribute('aria-invalid', 'true');
359 errorElement.textContent = message;
360 errorElement.hidden = false;
361}
362
363function hideError(input, errorElement) {
364 input.setAttribute('aria-invalid', 'false');
365 errorElement.textContent = '';
366 errorElement.hidden = true;
367}
368
369// ============================================
370// ํฌ์ปค์ค ํธ๋ฉ ๋ฐ๋ชจ
371// ============================================
372function initFocusTrap() {
373 const trapArea = document.getElementById('focus-trap-demo');
374 if (!trapArea) return;
375
376 const focusableElements = trapArea.querySelectorAll(
377 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
378 );
379
380 if (focusableElements.length === 0) return;
381
382 const firstElement = focusableElements[0];
383 const lastElement = focusableElements[focusableElements.length - 1];
384
385 trapArea.addEventListener('keydown', (e) => {
386 if (e.key !== 'Tab') return;
387
388 if (e.shiftKey && document.activeElement === firstElement) {
389 e.preventDefault();
390 lastElement.focus();
391 } else if (!e.shiftKey && document.activeElement === lastElement) {
392 e.preventDefault();
393 firstElement.focus();
394 }
395 });
396}
397
398// ============================================
399// ์ด๊ธฐํ
400// ============================================
401document.addEventListener('DOMContentLoaded', () => {
402 initAccordion();
403 initTabs();
404 initLiveRegion();
405 initModal();
406 initListbox();
407 initFormValidation();
408 initFocusTrap();
409
410 console.log('์น ์ ๊ทผ์ฑ ์์ ๊ฐ ๋ก๋๋์์ต๋๋ค.');
411 console.log('์คํฌ๋ฆฐ ๋ฆฌ๋๋ ํค๋ณด๋๋ก ํ
์คํธํด ๋ณด์ธ์!');
412});