mirror of
https://github.com/hwanny1128/HGZero.git
synced 2025-12-06 12:36:23 +00:00
- 가파팀 프로토타입 파일 삭제 - 가파팀 유저스토리 삭제 - 실시간 회의록 작성 플로우 설계서 추가 (Mermaid, Markdown) - 백업 및 데이터 디렉토리 추가 - AI 데이터 샘플 생성 도구 추가 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
557 lines
12 KiB
JavaScript
557 lines
12 KiB
JavaScript
/*
|
||
* 회의록 작성 및 공유 개선 서비스 - 공통 자바스크립트
|
||
* 버전: 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 = `
|
||
<div class="toast-icon">${icons[type] || icons.info}</div>
|
||
<div class="toast-content">
|
||
<div class="toast-message">${message}</div>
|
||
</div>
|
||
<button class="toast-close" onclick="this.parentElement.remove()">×</button>
|
||
`;
|
||
|
||
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
|
||
};
|