PageSourceSearch

https://ax-education-platform.vercel.app/js/utils.js

js ax-education-platform.vercel.app collected 2026-10-03 06:09:14 UTC 4,785 bytes, 126 lines download raw bytes

1// AX교육플랫폼 공통 유틸리티
2
3const Utils = {
4  // 토스트 알림
5  showToast(message, type = 'info', duration = 3000) {
6    let container = document.getElementById('toast-container');
7    if (!container) {
8      container = document.createElement('div');
9      container.id = 'toast-container';
10      container.style.cssText = 'position:fixed;top:80px;right:24px;z-index:9999;display:flex;flex-direction:column;gap:8px;';
11      document.body.appendChild(container);
12    }
13    const toast = document.createElement('div');
14    const icons = { success: '✅', error: '❌', info: 'ℹ️', warning: '⚠️' };
15    const colors = { success: '#10B981', error: '#EF4444', info: '#3B82F6', warning: '#F59E0B' };
16    toast.style.cssText = `
17      padding:12px 20px;border-radius:8px;background:white;
18      box-shadow:0 4px 16px rgba(0,0,0,0.15);border-left:4px solid ${colors[type]};
19      font-family:'Noto Sans KR',sans-serif;font-size:14px;
20      animation:slideInToast 0.3s ease;max-width:300px;
21    `;
22    toast.textContent = `${icons[type]} ${message}`;
23    container.appendChild(toast);
24    setTimeout(() => { toast.style.opacity='0'; toast.style.transition='opacity 0.3s'; setTimeout(() => toast.remove(), 300); }, duration);
25  },
26
27  // 로딩 표시
28  showLoading(container, text = '로딩 중...') {
29    const el = typeof container === 'string' ? document.querySelector(container) : container;
30    if (el) el.innerHTML = `<div style="text-align:center;padding:48px;color:#6B7280;"><div style="width:32px;height:32px;border:3px solid #E5E7EB;border-top-color:#FF6B35;border-radius:50%;animation:spin 0.8s linear infinite;margin:0 auto 16px;"></div><p>${text}</p></div>`;
31  },
32
33  hideLoading(container) {
34    const el = typeof container === 'string' ? document.querySelector(container) : container;
35    if (el) el.innerHTML = '';
36  },
37
38  // 날짜 포맷
39  formatDate(date, format = 'YYYY-MM-DD') {
40    const d = new Date(date);
41    const pad = n => String(n).padStart(2, '0');
42    return format
43      .replace('YYYY', d.getFullYear())
44      .replace('MM', pad(d.getMonth()+1))
45      .replace('DD', pad(d.getDate()))
46      .replace('HH', pad(d.getHours()))
47      .replace('mm', pad(d.getMinutes()));
48  },
49
50  // 상대 시간
51  timeAgo(date) {
52    const diff = (Date.now() - new Date(date)) / 1000;
53    if (diff < 60) return '방금 전';
54    if (diff < 3600) return `${Math.floor(diff/60)}분 전`;
55    if (diff < 86400) return `${Math.floor(diff/3600)}시간 전`;
56    return `${Math.floor(diff/86400)}일 전`;
57  },
58
59  // 숫자 포맷
60  formatNumber(num) {
61    if (num >= 1000) return (num/1000).toFixed(1) + 'k';
62    return String(num);
63  },
64
65  // 텍스트 자르기
66  truncate(text, maxLen = 100) {
67    return text.length > maxLen ? text.slice(0, maxLen) + '...' : text;
68  },
69
70  // 디바운스
71  debounce(fn, wait = 300) {
72    let t;
73    return (...args) => { clearTimeout(t); t = setTimeout(() => fn(...args), wait); };
74  },
75
76  // URL 파라미터
77  getParam(name) {
78    return new URLSearchParams(window.location.search).get(name);
79  },
80
81  // 로컬스토리지 래퍼
82  storage: {
83    get(key) { try { return JSON.parse(localStorage.getItem('ax_' + key)); } catch { return null; } },
84    set(key, val) { try { localStorage.setItem('ax_' + key, JSON.stringify(val)); } catch {} },
85    remove(key) { localStorage.removeItem('ax_' + key); }
86  },
87
88  // 이니셜 아바타
89  getInitials(name) {
90    return name ? name.split(' ').map(n => n[0]).join('').toUpperCase().slice(0, 2) : '?';
91  },
92
93  // 애니메이션 카운터
94  animateCounter(el, target, duration = 2000) {
95    const start = 0;
96    const step = (timestamp) => {
97      const progress = Math.min((timestamp - startTime) / duration, 1);
98      el.textContent = Math.floor(progress * target).toLocaleString();
99      if (progress < 1) requestAnimationFrame(step);
100    };
101    let startTime;
102    requestAnimationFrame(ts => { startTime = ts; step(ts); });
103  },
104
105  // 스크롤 탑
106  scrollToTop() { window.scrollTo({ top: 0, behavior: 'smooth' }); },
107
108  // 모달 제어
109  openModal(id) {
110    const m = document.getElementById(id);
111    if (m) { m.classList.add('active'); document.body.style.overflow = 'hidden'; }
112  },
113  closeModal(id) {
114    const m = document.getElementById(id);
115    if (m) { m.classList.remove('active'); document.body.style.overflow = ''; }
116  }
117};
118
119// 전역 CSS 애니메이션
120const style = document.createElement('style');
121style.textContent = `
122  @keyframes slideInToast { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } }
123  @keyframes spin { to { transform: rotate(360deg); } }
124  @keyframes fadeIn { from { opacity: 0; transform: translateY(16px); } to { opacity: 1; transform: translateY(0); } }
125`;
126document.head.appendChild(style);

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.