/* * 회의록 작성 및 공유 개선 서비스 - 공통 자바스크립트 * 버전: 1.0 * 작성일: 2025-10-20 * 레퍼런스: 스타일 가이드 v1.0 */ // ===== 전역 상태 관리 ===== const AppState = { currentUser: { id: 'user-001', name: '김민준', email: 'minjun.kim@example.com', avatar: 'https://ui-avatars.com/api/?name=김민준&background=00D9B1&color=fff' }, meetings: [], todos: [] }; // ===== 유틸리티 함수 ===== /** * DOM 준비 완료 시 콜백 실행 */ function ready(callback) { if (document.readyState !== 'loading') { callback(); } else { document.addEventListener('DOMContentLoaded', callback); } } /** * 날짜 포맷팅 (YYYY-MM-DD HH:mm) */ function formatDateTime(date) { const d = new Date(date); const year = d.getFullYear(); const month = String(d.getMonth() + 1).padStart(2, '0'); const day = String(d.getDate()).padStart(2, '0'); const hours = String(d.getHours()).padStart(2, '0'); const minutes = String(d.getMinutes()).padStart(2, '0'); return `${year}-${month}-${day} ${hours}:${minutes}`; } /** * 상대 시간 표현 (방금 전, 3분 전, 2시간 전 등) */ function timeAgo(date) { const now = new Date(); const past = new Date(date); const diff = Math.floor((now - past) / 1000); // 초 단위 if (diff < 60) return '방금 전'; if (diff < 3600) return `${Math.floor(diff / 60)}분 전`; if (diff < 86400) return `${Math.floor(diff / 3600)}시간 전`; if (diff < 2592000) return `${Math.floor(diff / 86400)}일 전`; if (diff < 31536000) return `${Math.floor(diff / 2592000)}개월 전`; return `${Math.floor(diff / 31536000)}년 전`; } /** * D-day 계산 */ function getDday(targetDate) { const now = new Date(); now.setHours(0, 0, 0, 0); const target = new Date(targetDate); target.setHours(0, 0, 0, 0); const diff = Math.floor((target - now) / (1000 * 60 * 60 * 24)); if (diff === 0) return '오늘'; if (diff > 0) return `D-${diff}`; return `D+${Math.abs(diff)} (지남)`; } /** * UUID 생성 (간단한 버전) */ function generateUUID() { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { const r = Math.random() * 16 | 0; const v = c === 'x' ? r : (r & 0x3 | 0x8); return v.toString(16); }); } // ===== 모달 관리 ===== const Modal = { /** * 모달 열기 */ open(modalId) { const modal = document.getElementById(modalId); if (!modal) return; modal.style.display = 'flex'; document.body.style.overflow = 'hidden'; // backdrop 클릭 시 모달 닫기 const backdrop = modal.querySelector('.modal-backdrop'); if (backdrop) { backdrop.addEventListener('click', (e) => { if (e.target === backdrop) { this.close(modalId); } }); } // 닫기 버튼 const closeBtn = modal.querySelector('.modal-close'); if (closeBtn) { closeBtn.addEventListener('click', () => this.close(modalId)); } }, /** * 모달 닫기 */ close(modalId) { const modal = document.getElementById(modalId); if (!modal) return; modal.style.display = 'none'; document.body.style.overflow = 'auto'; } }; // ===== 토스트 알림 ===== const Toast = { container: null, /** * 토스트 컨테이너 초기화 */ init() { if (!this.container) { this.container = document.createElement('div'); this.container.className = 'toast-container'; document.body.appendChild(this.container); } }, /** * 토스트 표시 */ show(message, type = 'info', duration = 4000) { this.init(); const toast = document.createElement('div'); toast.className = `toast toast-${type}`; const icons = { success: '✓', error: '✕', warning: '⚠', info: 'ℹ' }; toast.innerHTML = `
${icons[type] || icons.info}
${message}
`; this.container.appendChild(toast); // 자동 제거 setTimeout(() => { if (toast.parentElement) { toast.remove(); } }, duration); }, success(message) { this.show(message, 'success'); }, error(message) { this.show(message, 'error'); }, warning(message) { this.show(message, 'warning'); }, info(message) { this.show(message, 'info'); } }; // ===== 로컬 스토리지 관리 ===== const Storage = { /** * 데이터 저장 */ set(key, value) { try { localStorage.setItem(key, JSON.stringify(value)); return true; } catch (e) { console.error('Storage.set error:', e); return false; } }, /** * 데이터 가져오기 */ get(key, defaultValue = null) { try { const item = localStorage.getItem(key); return item ? JSON.parse(item) : defaultValue; } catch (e) { console.error('Storage.get error:', e); return defaultValue; } }, /** * 데이터 삭제 */ remove(key) { try { localStorage.removeItem(key); return true; } catch (e) { console.error('Storage.remove error:', e); return false; } }, /** * 전체 삭제 */ clear() { try { localStorage.clear(); return true; } catch (e) { console.error('Storage.clear error:', e); return false; } } }; // ===== API 호출 (Mock) ===== const API = { /** * 지연 시뮬레이션 */ delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); }, /** * GET 요청 (Mock) */ async get(endpoint) { await this.delay(500); console.log(`API GET: ${endpoint}`); return { success: true, data: {} }; }, /** * POST 요청 (Mock) */ async post(endpoint, data) { await this.delay(500); console.log(`API POST: ${endpoint}`, data); return { success: true, data: {} }; }, /** * PUT 요청 (Mock) */ async put(endpoint, data) { await this.delay(500); console.log(`API PUT: ${endpoint}`, data); return { success: true, data: {} }; }, /** * DELETE 요청 (Mock) */ async delete(endpoint) { await this.delay(500); console.log(`API DELETE: ${endpoint}`); return { success: true }; } }; // ===== 페이지 네비게이션 ===== function navigateTo(page) { // 실제로는 SPA 라우팅이나 페이지 이동 처리 // 프로토타입에서는 링크 클릭으로 처리 console.log(`Navigate to: ${page}`); window.location.href = page; } // ===== 폼 유효성 검사 ===== const Validator = { /** * 이메일 유효성 검사 */ isEmail(email) { const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; return re.test(email); }, /** * 필수 입력 검사 */ required(value) { return value !== null && value !== undefined && value.trim() !== ''; }, /** * 최소 길이 검사 */ minLength(value, min) { return value && value.length >= min; }, /** * 최대 길이 검사 */ maxLength(value, max) { return value && value.length <= max; }, /** * 폼 필드 에러 표시 */ showError(inputElement, message) { inputElement.classList.add('error'); let errorElement = inputElement.nextElementSibling; if (!errorElement || !errorElement.classList.contains('form-error')) { errorElement = document.createElement('span'); errorElement.className = 'form-error'; inputElement.parentElement.appendChild(errorElement); } errorElement.textContent = message; }, /** * 폼 필드 에러 제거 */ clearError(inputElement) { inputElement.classList.remove('error'); const errorElement = inputElement.nextElementSibling; if (errorElement && errorElement.classList.contains('form-error')) { errorElement.remove(); } } }; // ===== 로딩 상태 관리 ===== const Loading = { /** * 로딩 표시 */ show(target = 'body') { const element = typeof target === 'string' ? document.querySelector(target) : target; if (!element) return; const spinner = document.createElement('div'); spinner.className = 'spinner'; spinner.id = 'global-spinner'; spinner.style.position = 'fixed'; spinner.style.top = '50%'; spinner.style.left = '50%'; spinner.style.transform = 'translate(-50%, -50%)'; spinner.style.zIndex = '9999'; document.body.appendChild(spinner); }, /** * 로딩 숨김 */ hide() { const spinner = document.getElementById('global-spinner'); if (spinner) { spinner.remove(); } } }; // ===== 회의록 관련 유틸리티 ===== const MeetingUtils = { /** * 회의 상태 레이블 */ getStatusLabel(status) { const labels = { 'scheduled': '예정', 'in_progress': '진행 중', 'ended': '종료', 'draft': '작성 중', 'verifying': '검증 중', 'confirmed': '확정됨' }; return labels[status] || status; }, /** * 회의 상태 클래스 */ getStatusClass(status) { const classes = { 'draft': 'status-draft', 'verifying': 'status-verifying', 'confirmed': 'status-confirmed' }; return classes[status] || 'badge-neutral'; }, /** * Todo 우선순위 레이블 */ getPriorityLabel(priority) { const labels = { 'high': '높음', 'medium': '보통', 'low': '낮음' }; return labels[priority] || priority; }, /** * Todo 상태 레이블 */ getTodoStatusLabel(status) { const labels = { 'todo': '시작 전', 'in_progress': '진행 중', 'done': '완료' }; return labels[status] || status; } }; // ===== 예시 데이터 생성 ===== const MockData = { /** * 샘플 회의 데이터 */ generateMeetings() { return [ { id: 'm-001', title: '2025년 1분기 제품 기획 회의', date: '2025-10-25 14:00', location: '본사 2층 대회의실', status: 'scheduled', attendees: ['김민준', '박서연', '이준호', '최유진'], description: '신규 회의록 서비스 기획 논의' }, { id: 'm-002', title: '주간 스크럼 회의', date: '2025-10-21 10:00', location: 'Zoom', status: 'confirmed', attendees: ['김민준', '이준호', '최유진'], description: '지난 주 진행 상황 공유 및 이번 주 계획' }, { id: 'm-003', title: 'AI 기능 개선 회의', date: '2025-10-23 15:00', location: '본사 3층 소회의실', status: 'in_progress', attendees: ['박서연', '이준호'], description: 'LLM 기반 회의록 자동 작성 개선 방안' } ]; }, /** * 샘플 Todo 데이터 */ generateTodos() { return [ { id: 't-001', title: 'API 명세서 작성', assignee: '이준호', dueDate: '2025-10-25', priority: 'high', status: 'in_progress', progress: 60, meetingId: 'm-002' }, { id: 't-002', title: 'UI 프로토타입 디자인', assignee: '최유진', dueDate: '2025-10-23', priority: 'medium', status: 'done', progress: 100, meetingId: 'm-002' }, { id: 't-003', title: '데이터베이스 스키마 설계', assignee: '이준호', dueDate: '2025-10-28', priority: 'high', status: 'todo', progress: 0, meetingId: 'm-001' } ]; } }; // ===== 초기화 ===== ready(() => { console.log('Common.js loaded'); // 로컬 스토리지에서 상태 복원 const savedMeetings = Storage.get('meetings'); const savedTodos = Storage.get('todos'); if (!savedMeetings) { AppState.meetings = MockData.generateMeetings(); Storage.set('meetings', AppState.meetings); } else { AppState.meetings = savedMeetings; } if (!savedTodos) { AppState.todos = MockData.generateTodos(); Storage.set('todos', AppState.todos); } else { AppState.todos = savedTodos; } console.log('AppState initialized:', AppState); }); // ===== Export (전역 네임스페이스) ===== window.MeetingApp = { AppState, Modal, Toast, Storage, API, Validator, Loading, MeetingUtils, MockData, navigateTo, formatDateTime, timeAgo, getDday, generateUUID, ready };