/*
* 회의록 작성 및 공유 개선 서비스 - 공통 자바스크립트
* 버전: 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}
`;
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
};