app.js

Download
javascript 413 lines 12.1 KB
  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});