mirror of
https://github.com/hwanny1128/HGZero.git
synced 2025-12-06 07:56:24 +00:00
솔루션 후보 문서 추가 및 Claude 설정 업데이트
- 회의록 작성 개선을 위한 솔루션 후보 5개 작성 - 각 솔루션별 구현 방법 및 검증 방안 포함 - Claude Code 로컬 설정 파일 업데이트 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
8265eb5f8e
commit
c72c4dc0e1
307
design/uiux/prototype/01-로그인.html
Normal file
307
design/uiux/prototype/01-로그인.html
Normal file
@ -0,0 +1,307 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>로그인 - 회의록 작성 서비스</title>
|
||||
<link rel="stylesheet" href="common.css">
|
||||
<style>
|
||||
body {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(135deg, #4DFFDB 0%, #00D9B1 100%);
|
||||
}
|
||||
|
||||
.login-container {
|
||||
background-color: white;
|
||||
border-radius: var(--radius-xl);
|
||||
padding: var(--spacing-12);
|
||||
max-width: 400px;
|
||||
width: 90%;
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.login-header {
|
||||
text-align: center;
|
||||
margin-bottom: var(--spacing-8);
|
||||
}
|
||||
|
||||
.logo-large {
|
||||
font-size: 3rem;
|
||||
color: var(--primary-main);
|
||||
margin-bottom: var(--spacing-4);
|
||||
}
|
||||
|
||||
.login-title {
|
||||
font: var(--font-h2);
|
||||
color: var(--gray-900);
|
||||
margin-bottom: var(--spacing-2);
|
||||
}
|
||||
|
||||
.login-subtitle {
|
||||
font: var(--font-body);
|
||||
color: var(--gray-500);
|
||||
}
|
||||
|
||||
.login-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-4);
|
||||
}
|
||||
|
||||
.password-toggle {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.password-toggle-btn {
|
||||
position: absolute;
|
||||
right: var(--spacing-3);
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
color: var(--gray-500);
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.login-options {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.checkbox-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-2);
|
||||
cursor: pointer;
|
||||
color: var(--gray-600);
|
||||
}
|
||||
|
||||
.forgot-password {
|
||||
color: var(--primary-main);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.forgot-password:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.divider {
|
||||
text-align: center;
|
||||
margin: var(--spacing-6) 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.divider::before,
|
||||
.divider::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
width: 45%;
|
||||
height: 1px;
|
||||
background-color: var(--gray-200);
|
||||
}
|
||||
|
||||
.divider::before { left: 0; }
|
||||
.divider::after { right: 0; }
|
||||
|
||||
.divider-text {
|
||||
background-color: white;
|
||||
padding: 0 var(--spacing-3);
|
||||
color: var(--gray-500);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.social-login {
|
||||
display: flex;
|
||||
gap: var(--spacing-3);
|
||||
}
|
||||
|
||||
.social-btn {
|
||||
flex: 1;
|
||||
padding: var(--spacing-3);
|
||||
border: 1px solid var(--gray-300);
|
||||
border-radius: var(--radius-md);
|
||||
background-color: white;
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-fast);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--spacing-2);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.social-btn:hover {
|
||||
background-color: var(--gray-50);
|
||||
border-color: var(--gray-400);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="login-container">
|
||||
<div class="login-header">
|
||||
<div class="logo-large">📝</div>
|
||||
<h1 class="login-title">회의록 작성 서비스</h1>
|
||||
<p class="login-subtitle">AI와 함께하는 스마트한 회의록 관리</p>
|
||||
</div>
|
||||
|
||||
<form class="login-form" id="loginForm">
|
||||
<div class="input-group">
|
||||
<label for="email" class="input-label">이메일</label>
|
||||
<input
|
||||
type="email"
|
||||
id="email"
|
||||
class="input"
|
||||
placeholder="example@company.com"
|
||||
required
|
||||
>
|
||||
<span class="input-error hidden" id="emailError"></span>
|
||||
</div>
|
||||
|
||||
<div class="input-group">
|
||||
<label for="password" class="input-label">비밀번호</label>
|
||||
<div class="password-toggle">
|
||||
<input
|
||||
type="password"
|
||||
id="password"
|
||||
class="input"
|
||||
placeholder="비밀번호를 입력하세요"
|
||||
required
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="password-toggle-btn"
|
||||
id="togglePassword"
|
||||
aria-label="비밀번호 표시/숨김"
|
||||
>
|
||||
👁️
|
||||
</button>
|
||||
</div>
|
||||
<span class="input-error hidden" id="passwordError"></span>
|
||||
</div>
|
||||
|
||||
<div class="login-options">
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" id="rememberMe">
|
||||
<span>로그인 상태 유지</span>
|
||||
</label>
|
||||
<a href="#" class="forgot-password">비밀번호 찾기</a>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary btn-lg">
|
||||
로그인
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div class="divider">
|
||||
<span class="divider-text">또는</span>
|
||||
</div>
|
||||
|
||||
<div class="social-login">
|
||||
<button class="social-btn" id="googleLogin">
|
||||
<span>🔵</span>
|
||||
<span>Google</span>
|
||||
</button>
|
||||
<button class="social-btn" id="microsoftLogin">
|
||||
<span>🟦</span>
|
||||
<span>Microsoft</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="common.js"></script>
|
||||
<script>
|
||||
// 비밀번호 표시/숨김 토글
|
||||
document.getElementById('togglePassword').addEventListener('click', function() {
|
||||
const passwordInput = document.getElementById('password');
|
||||
const type = passwordInput.type === 'password' ? 'text' : 'password';
|
||||
passwordInput.type = type;
|
||||
this.textContent = type === 'password' ? '👁️' : '👁️🗨️';
|
||||
});
|
||||
|
||||
// 로그인 폼 제출
|
||||
document.getElementById('loginForm').addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const email = document.getElementById('email').value;
|
||||
const password = document.getElementById('password').value;
|
||||
const rememberMe = document.getElementById('rememberMe').checked;
|
||||
|
||||
// 간단한 검증
|
||||
const emailError = document.getElementById('emailError');
|
||||
const passwordError = document.getElementById('passwordError');
|
||||
|
||||
emailError.classList.add('hidden');
|
||||
passwordError.classList.add('hidden');
|
||||
document.getElementById('email').classList.remove('error');
|
||||
document.getElementById('password').classList.remove('error');
|
||||
|
||||
let hasError = false;
|
||||
|
||||
// 이메일 검증
|
||||
if (!email.includes('@')) {
|
||||
emailError.textContent = '올바른 이메일 주소를 입력하세요.';
|
||||
emailError.classList.remove('hidden');
|
||||
document.getElementById('email').classList.add('error');
|
||||
hasError = true;
|
||||
}
|
||||
|
||||
// 비밀번호 검증
|
||||
if (password.length < 8) {
|
||||
passwordError.textContent = '비밀번호는 최소 8자 이상이어야 합니다.';
|
||||
passwordError.classList.remove('hidden');
|
||||
document.getElementById('password').classList.add('error');
|
||||
hasError = true;
|
||||
}
|
||||
|
||||
if (hasError) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 로그인 처리 (시뮬레이션)
|
||||
const users = getFromStorage('users') || [];
|
||||
const user = users.find(u => u.email === email);
|
||||
|
||||
if (!user) {
|
||||
showToast('이메일 또는 비밀번호가 일치하지 않습니다.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// 로그인 성공
|
||||
saveToStorage('currentUser', user);
|
||||
|
||||
if (rememberMe) {
|
||||
localStorage.setItem('rememberMe', 'true');
|
||||
}
|
||||
|
||||
showToast('로그인 성공!', 'success');
|
||||
|
||||
// 0.5초 후 대시보드로 이동
|
||||
setTimeout(() => {
|
||||
navigateTo('02-대시보드.html');
|
||||
}, 500);
|
||||
});
|
||||
|
||||
// 소셜 로그인 (시뮬레이션)
|
||||
document.getElementById('googleLogin').addEventListener('click', function() {
|
||||
showToast('Google 로그인은 프로토타입에서 지원하지 않습니다.', 'info');
|
||||
});
|
||||
|
||||
document.getElementById('microsoftLogin').addEventListener('click', function() {
|
||||
showToast('Microsoft 로그인은 프로토타입에서 지원하지 않습니다.', 'info');
|
||||
});
|
||||
|
||||
// 비밀번호 찾기
|
||||
document.querySelector('.forgot-password').addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
showToast('비밀번호 재설정 이메일이 발송되었습니다.', 'success');
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
539
design/uiux/prototype/02-대시보드.html
Normal file
539
design/uiux/prototype/02-대시보드.html
Normal file
@ -0,0 +1,539 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>대시보드 - 회의록 작성 서비스</title>
|
||||
<link rel="stylesheet" href="common.css">
|
||||
<style>
|
||||
.dashboard-layout {
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
width: 250px;
|
||||
background-color: white;
|
||||
border-right: 1px solid var(--gray-200);
|
||||
padding: var(--spacing-6);
|
||||
position: fixed;
|
||||
height: 100vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.sidebar-logo {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: var(--primary-main);
|
||||
margin-bottom: var(--spacing-8);
|
||||
text-decoration: none;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.sidebar-menu {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.sidebar-menu-item {
|
||||
margin-bottom: var(--spacing-2);
|
||||
}
|
||||
|
||||
.sidebar-menu-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-3);
|
||||
padding: var(--spacing-3);
|
||||
border-radius: var(--radius-md);
|
||||
text-decoration: none;
|
||||
color: var(--gray-700);
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.sidebar-menu-link:hover {
|
||||
background-color: var(--gray-100);
|
||||
}
|
||||
|
||||
.sidebar-menu-link.active {
|
||||
background-color: var(--primary-light);
|
||||
color: var(--primary-dark);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.main-layout {
|
||||
margin-left: 250px;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.top-header {
|
||||
background-color: white;
|
||||
border-bottom: 1px solid var(--gray-200);
|
||||
padding: var(--spacing-4);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.header-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--spacing-4);
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
flex: 1;
|
||||
max-width: 600px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
width: 100%;
|
||||
padding: var(--spacing-2) var(--spacing-4) var(--spacing-2) var(--spacing-10);
|
||||
border: 1px solid var(--gray-300);
|
||||
border-radius: var(--radius-md);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
position: absolute;
|
||||
left: var(--spacing-3);
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: var(--gray-400);
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-3);
|
||||
}
|
||||
|
||||
.notification-btn {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.notification-badge {
|
||||
position: absolute;
|
||||
top: -4px;
|
||||
right: -4px;
|
||||
background-color: var(--error-main);
|
||||
color: white;
|
||||
font-size: 0.75rem;
|
||||
padding: 2px 6px;
|
||||
border-radius: var(--radius-full);
|
||||
}
|
||||
|
||||
.profile-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-2);
|
||||
padding: var(--spacing-2);
|
||||
border-radius: var(--radius-md);
|
||||
cursor: pointer;
|
||||
transition: background-color var(--transition-fast);
|
||||
}
|
||||
|
||||
.profile-btn:hover {
|
||||
background-color: var(--gray-100);
|
||||
}
|
||||
|
||||
.profile-avatar {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: var(--radius-full);
|
||||
background-color: var(--primary-main);
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.dashboard-content {
|
||||
padding: var(--spacing-8);
|
||||
background-color: var(--gray-50);
|
||||
}
|
||||
|
||||
.dashboard-header {
|
||||
margin-bottom: var(--spacing-6);
|
||||
}
|
||||
|
||||
.dashboard-title {
|
||||
font: var(--font-h1);
|
||||
margin-bottom: var(--spacing-2);
|
||||
}
|
||||
|
||||
.dashboard-subtitle {
|
||||
color: var(--gray-500);
|
||||
}
|
||||
|
||||
.widget-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: var(--spacing-6);
|
||||
}
|
||||
|
||||
.widget {
|
||||
background-color: white;
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--spacing-6);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.widget-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: var(--spacing-4);
|
||||
}
|
||||
|
||||
.widget-title {
|
||||
font: var(--font-h4);
|
||||
}
|
||||
|
||||
.widget-link {
|
||||
color: var(--primary-main);
|
||||
font-size: 0.875rem;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.widget-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.meeting-item {
|
||||
padding: var(--spacing-3);
|
||||
border-bottom: 1px solid var(--gray-100);
|
||||
cursor: pointer;
|
||||
transition: background-color var(--transition-fast);
|
||||
}
|
||||
|
||||
.meeting-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.meeting-item:hover {
|
||||
background-color: var(--gray-50);
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
|
||||
.meeting-title {
|
||||
font-weight: 500;
|
||||
margin-bottom: var(--spacing-1);
|
||||
}
|
||||
|
||||
.meeting-meta {
|
||||
font-size: 0.875rem;
|
||||
color: var(--gray-500);
|
||||
display: flex;
|
||||
gap: var(--spacing-3);
|
||||
}
|
||||
|
||||
.todo-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: var(--spacing-3);
|
||||
padding: var(--spacing-3);
|
||||
border-bottom: 1px solid var(--gray-100);
|
||||
}
|
||||
|
||||
.todo-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.todo-checkbox {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.todo-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.todo-text {
|
||||
font-size: 0.875rem;
|
||||
margin-bottom: var(--spacing-1);
|
||||
}
|
||||
|
||||
.todo-due {
|
||||
font-size: 0.75rem;
|
||||
color: var(--gray-500);
|
||||
}
|
||||
|
||||
.todo-due.overdue {
|
||||
color: var(--error-main);
|
||||
}
|
||||
|
||||
.stat-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: var(--spacing-4);
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
text-align: center;
|
||||
padding: var(--spacing-4);
|
||||
background-color: var(--gray-50);
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
color: var(--primary-main);
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 0.875rem;
|
||||
color: var(--gray-600);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="dashboard-layout">
|
||||
<!-- Sidebar -->
|
||||
<aside class="sidebar">
|
||||
<a href="02-대시보드.html" class="sidebar-logo">📝 회의록</a>
|
||||
<nav>
|
||||
<ul class="sidebar-menu">
|
||||
<li class="sidebar-menu-item">
|
||||
<a href="02-대시보드.html" class="sidebar-menu-link active">
|
||||
<span>🏠</span>
|
||||
<span>대시보드</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="sidebar-menu-item">
|
||||
<a href="#" class="sidebar-menu-link">
|
||||
<span>📄</span>
|
||||
<span>내 회의록</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="sidebar-menu-item">
|
||||
<a href="03-회의예약.html" class="sidebar-menu-link">
|
||||
<span>📅</span>
|
||||
<span>예정된 회의</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="sidebar-menu-item">
|
||||
<a href="09-Todo관리.html" class="sidebar-menu-link">
|
||||
<span>✅</span>
|
||||
<span>Todo 목록</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="sidebar-menu-item">
|
||||
<a href="04-템플릿선택.html" class="sidebar-menu-link">
|
||||
<span>📋</span>
|
||||
<span>템플릿</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="sidebar-menu-item">
|
||||
<a href="#" class="sidebar-menu-link">
|
||||
<span>⚙️</span>
|
||||
<span>설정</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="main-layout">
|
||||
<!-- Header -->
|
||||
<header class="top-header">
|
||||
<div class="header-content">
|
||||
<div class="search-bar">
|
||||
<span class="search-icon">🔍</span>
|
||||
<input type="text" class="search-input" placeholder="회의록 검색...">
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<button class="btn-icon notification-btn">
|
||||
<span>🔔</span>
|
||||
<span class="notification-badge">3</span>
|
||||
</button>
|
||||
<div class="profile-btn" id="profileBtn">
|
||||
<div class="profile-avatar" id="profileAvatar">👤</div>
|
||||
<span id="profileName">사용자</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Dashboard Content -->
|
||||
<main class="dashboard-content">
|
||||
<div class="dashboard-header">
|
||||
<h1 class="dashboard-title" id="welcomeMessage">안녕하세요!</h1>
|
||||
<p class="dashboard-subtitle">오늘도 생산적인 하루 되세요</p>
|
||||
</div>
|
||||
|
||||
<div class="widget-grid">
|
||||
<!-- 예정된 회의 -->
|
||||
<div class="widget">
|
||||
<div class="widget-header">
|
||||
<h3 class="widget-title">예정된 회의</h3>
|
||||
<a href="03-회의예약.html" class="widget-link">모두 보기</a>
|
||||
</div>
|
||||
<div id="scheduledMeetings"></div>
|
||||
</div>
|
||||
|
||||
<!-- 최근 회의록 -->
|
||||
<div class="widget">
|
||||
<div class="widget-header">
|
||||
<h3 class="widget-title">최근 회의록</h3>
|
||||
<a href="#" class="widget-link">모두 보기</a>
|
||||
</div>
|
||||
<div id="recentMeetings"></div>
|
||||
</div>
|
||||
|
||||
<!-- 대기 중인 Todo -->
|
||||
<div class="widget">
|
||||
<div class="widget-header">
|
||||
<h3 class="widget-title">내가 할 일</h3>
|
||||
<a href="09-Todo관리.html" class="widget-link">모두 보기</a>
|
||||
</div>
|
||||
<div id="pendingTodos"></div>
|
||||
</div>
|
||||
|
||||
<!-- 통계 -->
|
||||
<div class="widget">
|
||||
<div class="widget-header">
|
||||
<h3 class="widget-title">이번 주 통계</h3>
|
||||
</div>
|
||||
<div class="stat-grid">
|
||||
<div class="stat-card">
|
||||
<div class="stat-number" id="weeklyMeetings">0</div>
|
||||
<div class="stat-label">회의</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-number" id="completedTodos">0</div>
|
||||
<div class="stat-label">완료 Todo</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- FAB -->
|
||||
<button class="fab" id="fabBtn">+</button>
|
||||
</div>
|
||||
|
||||
<script src="common.js"></script>
|
||||
<script>
|
||||
// 사용자 정보 표시
|
||||
const currentUser = getCurrentUser();
|
||||
if (currentUser) {
|
||||
document.getElementById('welcomeMessage').textContent = `안녕하세요, ${currentUser.name}님!`;
|
||||
document.getElementById('profileName').textContent = currentUser.name;
|
||||
document.getElementById('profileAvatar').textContent = currentUser.avatar;
|
||||
}
|
||||
|
||||
// 예정된 회의 표시
|
||||
const scheduledMeetings = getScheduledMeetings();
|
||||
const scheduledContainer = document.getElementById('scheduledMeetings');
|
||||
if (scheduledMeetings.length > 0) {
|
||||
scheduledContainer.innerHTML = scheduledMeetings.slice(0, 3).map(meeting => `
|
||||
<div class="meeting-item" onclick="navigateTo('05-회의진행.html?id=${meeting.id}')">
|
||||
<div class="meeting-title">${meeting.title}</div>
|
||||
<div class="meeting-meta">
|
||||
<span>📅 ${meeting.date}</span>
|
||||
<span>🕐 ${meeting.time}</span>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
} else {
|
||||
scheduledContainer.innerHTML = '<p style="color: var(--gray-500); font-size: 0.875rem;">예정된 회의가 없습니다.</p>';
|
||||
}
|
||||
|
||||
// 최근 회의록 표시
|
||||
const recentMeetings = getRecentMeetings(3);
|
||||
const recentContainer = document.getElementById('recentMeetings');
|
||||
if (recentMeetings.length > 0) {
|
||||
recentContainer.innerHTML = recentMeetings.map(meeting => {
|
||||
const statusClass = meeting.status === 'completed' ? 'success' : meeting.status === 'in_progress' ? 'warning' : 'neutral';
|
||||
const statusText = meeting.status === 'completed' ? '확정됨' : meeting.status === 'in_progress' ? '작성 중' : '예정됨';
|
||||
return `
|
||||
<div class="meeting-item" onclick="navigateTo('05-회의진행.html?id=${meeting.id}')">
|
||||
<div class="meeting-title">${meeting.title}</div>
|
||||
<div class="meeting-meta">
|
||||
<span>📅 ${meeting.date}</span>
|
||||
<span class="badge badge-${statusClass}">${statusText}</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
} else {
|
||||
recentContainer.innerHTML = '<p style="color: var(--gray-500); font-size: 0.875rem;">회의록이 없습니다.</p>';
|
||||
}
|
||||
|
||||
// 대기 중인 Todo 표시
|
||||
if (currentUser) {
|
||||
const pendingTodos = getPendingTodos(currentUser.id).slice(0, 3);
|
||||
const todosContainer = document.getElementById('pendingTodos');
|
||||
if (pendingTodos.length > 0) {
|
||||
todosContainer.innerHTML = pendingTodos.map(todo => {
|
||||
const dueClass = getDdayText(todo.dueDate).includes('지남') ? 'overdue' : '';
|
||||
return `
|
||||
<div class="todo-item">
|
||||
<input type="checkbox" class="todo-checkbox" data-id="${todo.id}">
|
||||
<div class="todo-content">
|
||||
<div class="todo-text">${todo.content}</div>
|
||||
<div class="todo-due ${dueClass}">${getDdayText(todo.dueDate)}</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
// Todo 체크박스 이벤트
|
||||
document.querySelectorAll('.todo-checkbox').forEach(checkbox => {
|
||||
checkbox.addEventListener('change', function() {
|
||||
const todoId = this.getAttribute('data-id');
|
||||
const todo = getTodoById(todoId);
|
||||
if (todo && this.checked) {
|
||||
todo.status = 'completed';
|
||||
todo.progress = 100;
|
||||
saveTodo(todo);
|
||||
showToast('Todo가 완료되었습니다.', 'success');
|
||||
setTimeout(() => location.reload(), 1000);
|
||||
}
|
||||
});
|
||||
});
|
||||
} else {
|
||||
todosContainer.innerHTML = '<p style="color: var(--gray-500); font-size: 0.875rem;">할 일이 없습니다.</p>';
|
||||
}
|
||||
}
|
||||
|
||||
// 통계 표시
|
||||
const allMeetings = getAllMeetings();
|
||||
document.getElementById('weeklyMeetings').textContent = allMeetings.length;
|
||||
const completedTodos = getTodosByStatus('completed');
|
||||
document.getElementById('completedTodos').textContent = completedTodos.length;
|
||||
|
||||
// FAB 클릭
|
||||
document.getElementById('fabBtn').addEventListener('click', function() {
|
||||
const menu = [
|
||||
{ text: '새 회의 예약', url: '03-회의예약.html' },
|
||||
{ text: '즉시 회의 시작', url: '04-템플릿선택.html' },
|
||||
{ text: '새 Todo 추가', url: '09-Todo관리.html' }
|
||||
];
|
||||
// 간단한 메뉴 표시
|
||||
const choice = confirm('새 회의를 예약하시겠습니까?\n확인: 예약\n취소: 즉시 시작');
|
||||
if (choice) {
|
||||
navigateTo('03-회의예약.html');
|
||||
} else {
|
||||
navigateTo('04-템플릿선택.html');
|
||||
}
|
||||
});
|
||||
|
||||
// 프로필 버튼 클릭
|
||||
document.getElementById('profileBtn').addEventListener('click', function() {
|
||||
const choice = confirm('로그아웃하시겠습니까?');
|
||||
if (choice) {
|
||||
localStorage.removeItem('currentUser');
|
||||
showToast('로그아웃되었습니다.', 'success');
|
||||
setTimeout(() => navigateTo('01-로그인.html'), 500);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
699
design/uiux/prototype/03-회의예약.html
Normal file
699
design/uiux/prototype/03-회의예약.html
Normal file
@ -0,0 +1,699 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>회의 예약 - 회의록 작성 및 공유 서비스</title>
|
||||
<link rel="stylesheet" href="common.css">
|
||||
<style>
|
||||
.reservation-container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: var(--spacing-8);
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: var(--spacing-4);
|
||||
margin-bottom: var(--spacing-8);
|
||||
}
|
||||
|
||||
.progress-step {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-2);
|
||||
}
|
||||
|
||||
.progress-circle {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
background-color: var(--gray-300);
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.progress-circle.active {
|
||||
background-color: var(--primary-main);
|
||||
}
|
||||
|
||||
.progress-circle.completed {
|
||||
background-color: var(--success-main);
|
||||
}
|
||||
|
||||
.progress-label {
|
||||
font-weight: 500;
|
||||
color: var(--gray-500);
|
||||
}
|
||||
|
||||
.progress-label.active {
|
||||
color: var(--gray-900);
|
||||
}
|
||||
|
||||
.progress-arrow {
|
||||
color: var(--gray-300);
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.reservation-layout {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 400px;
|
||||
gap: var(--spacing-8);
|
||||
}
|
||||
|
||||
.form-section {
|
||||
background: white;
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--spacing-8);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.preview-section {
|
||||
position: sticky;
|
||||
top: var(--spacing-8);
|
||||
height: fit-content;
|
||||
}
|
||||
|
||||
.preview-card {
|
||||
background: white;
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--spacing-6);
|
||||
box-shadow: var(--shadow-sm);
|
||||
border: 2px solid var(--primary-main);
|
||||
}
|
||||
|
||||
.preview-title {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
color: var(--gray-900);
|
||||
margin-bottom: var(--spacing-4);
|
||||
}
|
||||
|
||||
.preview-item {
|
||||
margin-bottom: var(--spacing-4);
|
||||
padding-bottom: var(--spacing-4);
|
||||
border-bottom: 1px solid var(--gray-200);
|
||||
}
|
||||
|
||||
.preview-item:last-child {
|
||||
border-bottom: none;
|
||||
margin-bottom: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.preview-label {
|
||||
font-size: 0.75rem;
|
||||
color: var(--gray-500);
|
||||
margin-bottom: var(--spacing-1);
|
||||
}
|
||||
|
||||
.preview-value {
|
||||
font-size: 1rem;
|
||||
color: var(--gray-900);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.datetime-group {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: var(--spacing-4);
|
||||
}
|
||||
|
||||
.participant-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--spacing-2);
|
||||
margin-top: var(--spacing-2);
|
||||
}
|
||||
|
||||
.participant-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-2);
|
||||
padding: var(--spacing-2) var(--spacing-3);
|
||||
background-color: var(--gray-100);
|
||||
border-radius: var(--radius-md);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.participant-tag .remove {
|
||||
cursor: pointer;
|
||||
color: var(--gray-500);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.participant-tag .remove:hover {
|
||||
color: var(--error-main);
|
||||
}
|
||||
|
||||
.checkbox-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-2);
|
||||
}
|
||||
|
||||
.checkbox-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-2);
|
||||
}
|
||||
|
||||
.checkbox-item input[type="checkbox"] {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-top: var(--spacing-8);
|
||||
padding-top: var(--spacing-6);
|
||||
border-top: 1px solid var(--gray-200);
|
||||
}
|
||||
|
||||
@media (max-width: 1023px) {
|
||||
.reservation-layout {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.preview-section {
|
||||
position: static;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="reservation-container">
|
||||
<!-- 진행 단계 표시 -->
|
||||
<div class="progress-bar">
|
||||
<div class="progress-step">
|
||||
<div class="progress-circle completed">✓</div>
|
||||
<span class="progress-label">회의 예약</span>
|
||||
</div>
|
||||
<span class="progress-arrow">→</span>
|
||||
<div class="progress-step">
|
||||
<div class="progress-circle active">2</div>
|
||||
<span class="progress-label active">템플릿 선택</span>
|
||||
</div>
|
||||
<span class="progress-arrow">→</span>
|
||||
<div class="progress-step">
|
||||
<div class="progress-circle">3</div>
|
||||
<span class="progress-label">회의 진행</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="reservation-layout">
|
||||
<!-- 입력 폼 -->
|
||||
<div class="form-section">
|
||||
<h1>회의 예약</h1>
|
||||
<p style="color: var(--gray-500); margin-bottom: var(--spacing-8);">
|
||||
회의 정보를 입력하고 참석자를 초대하세요
|
||||
</p>
|
||||
|
||||
<form id="reservationForm">
|
||||
<!-- 회의 제목 -->
|
||||
<div class="input-group">
|
||||
<label class="input-label" for="meetingTitle">
|
||||
회의 제목 <span style="color: var(--error-main);">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="meetingTitle"
|
||||
class="input"
|
||||
placeholder="예: 2025년 1분기 전략 회의"
|
||||
maxlength="100"
|
||||
required>
|
||||
<span class="input-error hidden" id="titleError"></span>
|
||||
</div>
|
||||
|
||||
<!-- 날짜 및 시간 -->
|
||||
<div class="datetime-group">
|
||||
<div class="input-group">
|
||||
<label class="input-label" for="meetingDate">
|
||||
날짜 <span style="color: var(--error-main);">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="date"
|
||||
id="meetingDate"
|
||||
class="input"
|
||||
required>
|
||||
<span class="input-error hidden" id="dateError"></span>
|
||||
</div>
|
||||
|
||||
<div class="input-group">
|
||||
<label class="input-label" for="startTime">
|
||||
시작 시간 <span style="color: var(--error-main);">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="time"
|
||||
id="startTime"
|
||||
class="input"
|
||||
required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="datetime-group">
|
||||
<div class="input-group">
|
||||
<label class="input-label" for="endTime">
|
||||
종료 시간 <span style="color: var(--error-main);">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="time"
|
||||
id="endTime"
|
||||
class="input"
|
||||
required>
|
||||
<span class="input-error hidden" id="timeError"></span>
|
||||
</div>
|
||||
|
||||
<div class="input-group">
|
||||
<label class="input-label"> </label>
|
||||
<div style="padding: var(--spacing-3) 0; color: var(--gray-500); font-size: 0.875rem;">
|
||||
예상 소요: <span id="duration">-</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 장소 -->
|
||||
<div class="input-group">
|
||||
<label class="input-label" for="location">장소</label>
|
||||
<input
|
||||
type="text"
|
||||
id="location"
|
||||
class="input"
|
||||
placeholder="예: 3층 회의실"
|
||||
maxlength="200"
|
||||
:disabled="isOnline">
|
||||
|
||||
<div class="checkbox-item mt-2">
|
||||
<input type="checkbox" id="isOnline">
|
||||
<label for="isOnline">온라인 회의</label>
|
||||
</div>
|
||||
|
||||
<input
|
||||
type="url"
|
||||
id="onlineLink"
|
||||
class="input mt-2 hidden"
|
||||
placeholder="Zoom, Teams 링크를 입력하세요">
|
||||
</div>
|
||||
|
||||
<!-- 참석자 -->
|
||||
<div class="input-group">
|
||||
<label class="input-label" for="participantEmail">
|
||||
참석자 <span style="color: var(--error-main);">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
id="participantEmail"
|
||||
class="input"
|
||||
placeholder="참석자 이메일을 입력하세요">
|
||||
<button type="button" class="btn btn-secondary btn-sm mt-2" id="addParticipant">
|
||||
+ 참석자 추가
|
||||
</button>
|
||||
<div class="participant-tags" id="participantTags"></div>
|
||||
<span class="input-error hidden" id="participantError"></span>
|
||||
</div>
|
||||
|
||||
<!-- 회의 설명 -->
|
||||
<div class="input-group">
|
||||
<label class="input-label" for="description">회의 설명</label>
|
||||
<textarea
|
||||
id="description"
|
||||
class="textarea"
|
||||
placeholder="회의 목적 및 안건을 입력하세요"
|
||||
maxlength="1000"></textarea>
|
||||
</div>
|
||||
|
||||
<!-- 알림 설정 -->
|
||||
<div class="input-group">
|
||||
<label class="input-label">알림 설정</label>
|
||||
<div class="checkbox-group">
|
||||
<div class="checkbox-item">
|
||||
<input type="checkbox" id="notify30" checked>
|
||||
<label for="notify30">회의 30분 전 알림</label>
|
||||
</div>
|
||||
<div class="checkbox-item">
|
||||
<input type="checkbox" id="notify60">
|
||||
<label for="notify60">회의 1시간 전 알림</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 반복 설정 -->
|
||||
<div class="input-group">
|
||||
<label class="input-label" for="repeat">반복 설정</label>
|
||||
<select id="repeat" class="select">
|
||||
<option value="none">반복 안 함</option>
|
||||
<option value="daily">매일</option>
|
||||
<option value="weekly">매주</option>
|
||||
<option value="monthly">매월</option>
|
||||
</select>
|
||||
|
||||
<div class="input-group mt-4 hidden" id="repeatEndGroup">
|
||||
<label class="input-label" for="repeatEnd">반복 종료일</label>
|
||||
<input type="date" id="repeatEnd" class="input">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 캘린더 연동 -->
|
||||
<div class="input-group">
|
||||
<label class="input-label">캘린더 연동</label>
|
||||
<div class="checkbox-group">
|
||||
<div class="checkbox-item">
|
||||
<input type="checkbox" id="googleCalendar">
|
||||
<label for="googleCalendar">Google Calendar에 추가</label>
|
||||
</div>
|
||||
<div class="checkbox-item">
|
||||
<input type="checkbox" id="outlook">
|
||||
<label for="outlook">Outlook에 추가</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 액션 버튼 -->
|
||||
<div class="action-buttons">
|
||||
<button type="button" class="btn btn-text" onclick="navigateTo('02-대시보드.html')">
|
||||
취소
|
||||
</button>
|
||||
<div style="display: flex; gap: var(--spacing-3);">
|
||||
<button type="button" class="btn btn-secondary" id="saveDraft">
|
||||
저장 후 나중에 계속
|
||||
</button>
|
||||
<button type="submit" class="btn btn-primary">
|
||||
다음: 템플릿 선택 →
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- 미리보기 -->
|
||||
<div class="preview-section">
|
||||
<div class="preview-card">
|
||||
<div class="preview-title">회의 미리보기</div>
|
||||
|
||||
<div class="preview-item">
|
||||
<div class="preview-label">회의 제목</div>
|
||||
<div class="preview-value" id="previewTitle">-</div>
|
||||
</div>
|
||||
|
||||
<div class="preview-item">
|
||||
<div class="preview-label">날짜 및 시간</div>
|
||||
<div class="preview-value" id="previewDateTime">-</div>
|
||||
</div>
|
||||
|
||||
<div class="preview-item">
|
||||
<div class="preview-label">장소</div>
|
||||
<div class="preview-value" id="previewLocation">-</div>
|
||||
</div>
|
||||
|
||||
<div class="preview-item">
|
||||
<div class="preview-label">참석자</div>
|
||||
<div class="preview-value" id="previewParticipants">-</div>
|
||||
</div>
|
||||
|
||||
<div class="preview-item">
|
||||
<div class="preview-label">회의 설명</div>
|
||||
<div class="preview-value" id="previewDescription" style="white-space: pre-wrap;">-</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="common.js"></script>
|
||||
<script>
|
||||
// 참석자 목록
|
||||
let participants = [];
|
||||
|
||||
// 오늘 날짜를 최소값으로 설정
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
document.getElementById('meetingDate').min = today;
|
||||
document.getElementById('meetingDate').value = today;
|
||||
document.getElementById('repeatEnd').min = today;
|
||||
|
||||
// 시작 시간 기본값 설정
|
||||
const now = new Date();
|
||||
const currentHour = now.getHours();
|
||||
const nextHour = currentHour + 1;
|
||||
document.getElementById('startTime').value = `${String(nextHour).padStart(2, '0')}:00`;
|
||||
document.getElementById('endTime').value = `${String(nextHour + 1).padStart(2, '0')}:00`;
|
||||
|
||||
// 실시간 미리보기 업데이트
|
||||
function updatePreview() {
|
||||
const title = document.getElementById('meetingTitle').value || '-';
|
||||
const date = document.getElementById('meetingDate').value;
|
||||
const startTime = document.getElementById('startTime').value;
|
||||
const endTime = document.getElementById('endTime').value;
|
||||
const isOnline = document.getElementById('isOnline').checked;
|
||||
const location = isOnline ?
|
||||
(document.getElementById('onlineLink').value || '온라인 회의') :
|
||||
(document.getElementById('location').value || '-');
|
||||
const description = document.getElementById('description').value || '-';
|
||||
|
||||
document.getElementById('previewTitle').textContent = title;
|
||||
|
||||
if (date && startTime && endTime) {
|
||||
const dateObj = new Date(date);
|
||||
const dateStr = `${dateObj.getFullYear()}년 ${dateObj.getMonth() + 1}월 ${dateObj.getDate()}일`;
|
||||
document.getElementById('previewDateTime').textContent = `${dateStr} ${startTime} ~ ${endTime}`;
|
||||
} else {
|
||||
document.getElementById('previewDateTime').textContent = '-';
|
||||
}
|
||||
|
||||
document.getElementById('previewLocation').textContent = location;
|
||||
document.getElementById('previewParticipants').textContent =
|
||||
participants.length > 0 ? participants.join(', ') : '-';
|
||||
document.getElementById('previewDescription').textContent = description;
|
||||
}
|
||||
|
||||
// 소요 시간 계산
|
||||
function calculateDuration() {
|
||||
const startTime = document.getElementById('startTime').value;
|
||||
const endTime = document.getElementById('endTime').value;
|
||||
|
||||
if (startTime && endTime) {
|
||||
const [startHour, startMin] = startTime.split(':').map(Number);
|
||||
const [endHour, endMin] = endTime.split(':').map(Number);
|
||||
|
||||
const startMinutes = startHour * 60 + startMin;
|
||||
const endMinutes = endHour * 60 + endMin;
|
||||
const diff = endMinutes - startMinutes;
|
||||
|
||||
if (diff > 0) {
|
||||
document.getElementById('duration').textContent = formatDuration(diff);
|
||||
document.getElementById('timeError').classList.add('hidden');
|
||||
document.getElementById('endTime').classList.remove('error');
|
||||
return true;
|
||||
} else {
|
||||
document.getElementById('timeError').textContent = '종료 시간은 시작 시간보다 늦어야 합니다.';
|
||||
document.getElementById('timeError').classList.remove('hidden');
|
||||
document.getElementById('endTime').classList.add('error');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// 온라인 회의 체크박스
|
||||
document.getElementById('isOnline').addEventListener('change', (e) => {
|
||||
const isOnline = e.target.checked;
|
||||
const locationInput = document.getElementById('location');
|
||||
const onlineLinkInput = document.getElementById('onlineLink');
|
||||
|
||||
if (isOnline) {
|
||||
locationInput.disabled = true;
|
||||
locationInput.value = '';
|
||||
onlineLinkInput.classList.remove('hidden');
|
||||
} else {
|
||||
locationInput.disabled = false;
|
||||
onlineLinkInput.classList.add('hidden');
|
||||
onlineLinkInput.value = '';
|
||||
}
|
||||
updatePreview();
|
||||
});
|
||||
|
||||
// 참석자 추가
|
||||
document.getElementById('addParticipant').addEventListener('click', () => {
|
||||
const emailInput = document.getElementById('participantEmail');
|
||||
const email = emailInput.value.trim();
|
||||
const errorSpan = document.getElementById('participantError');
|
||||
|
||||
if (!email) {
|
||||
errorSpan.textContent = '이메일을 입력하세요.';
|
||||
errorSpan.classList.remove('hidden');
|
||||
return;
|
||||
}
|
||||
|
||||
// 간단한 이메일 검증
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (!emailRegex.test(email)) {
|
||||
errorSpan.textContent = '올바른 이메일 주소를 입력하세요.';
|
||||
errorSpan.classList.remove('hidden');
|
||||
emailInput.classList.add('error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (participants.includes(email)) {
|
||||
errorSpan.textContent = '이미 추가된 참석자입니다.';
|
||||
errorSpan.classList.remove('hidden');
|
||||
return;
|
||||
}
|
||||
|
||||
participants.push(email);
|
||||
emailInput.value = '';
|
||||
emailInput.classList.remove('error');
|
||||
errorSpan.classList.add('hidden');
|
||||
renderParticipants();
|
||||
updatePreview();
|
||||
});
|
||||
|
||||
// 참석자 렌더링
|
||||
function renderParticipants() {
|
||||
const container = document.getElementById('participantTags');
|
||||
container.innerHTML = participants.map((email, index) => `
|
||||
<div class="participant-tag">
|
||||
<span>👤 ${email}</span>
|
||||
<span class="remove" onclick="removeParticipant(${index})">×</span>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
// 참석자 제거
|
||||
function removeParticipant(index) {
|
||||
participants.splice(index, 1);
|
||||
renderParticipants();
|
||||
updatePreview();
|
||||
}
|
||||
|
||||
// 반복 설정
|
||||
document.getElementById('repeat').addEventListener('change', (e) => {
|
||||
const repeatEndGroup = document.getElementById('repeatEndGroup');
|
||||
if (e.target.value !== 'none') {
|
||||
repeatEndGroup.classList.remove('hidden');
|
||||
} else {
|
||||
repeatEndGroup.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
|
||||
// 입력 필드 이벤트 리스너
|
||||
document.querySelectorAll('input, textarea, select').forEach(el => {
|
||||
el.addEventListener('input', updatePreview);
|
||||
el.addEventListener('change', updatePreview);
|
||||
});
|
||||
|
||||
document.getElementById('startTime').addEventListener('change', calculateDuration);
|
||||
document.getElementById('endTime').addEventListener('change', calculateDuration);
|
||||
|
||||
// 폼 제출
|
||||
document.getElementById('reservationForm').addEventListener('submit', (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
// 유효성 검사
|
||||
const title = document.getElementById('meetingTitle').value.trim();
|
||||
const date = document.getElementById('meetingDate').value;
|
||||
|
||||
if (!title) {
|
||||
document.getElementById('titleError').textContent = '회의 제목을 입력하세요.';
|
||||
document.getElementById('titleError').classList.remove('hidden');
|
||||
document.getElementById('meetingTitle').classList.add('error');
|
||||
document.getElementById('meetingTitle').focus();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!date) {
|
||||
document.getElementById('dateError').textContent = '날짜를 선택하세요.';
|
||||
document.getElementById('dateError').classList.remove('hidden');
|
||||
document.getElementById('meetingDate').classList.add('error');
|
||||
return;
|
||||
}
|
||||
|
||||
// 과거 날짜 검사
|
||||
const selectedDate = new Date(date);
|
||||
const todayDate = new Date(today);
|
||||
if (selectedDate < todayDate) {
|
||||
document.getElementById('dateError').textContent = '과거 날짜는 선택할 수 없습니다.';
|
||||
document.getElementById('dateError').classList.remove('hidden');
|
||||
document.getElementById('meetingDate').classList.add('error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!calculateDuration()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (participants.length === 0) {
|
||||
document.getElementById('participantError').textContent = '최소 1명의 참석자를 추가하세요.';
|
||||
document.getElementById('participantError').classList.remove('hidden');
|
||||
document.getElementById('participantEmail').focus();
|
||||
return;
|
||||
}
|
||||
|
||||
// 회의 데이터 저장
|
||||
const meetingData = {
|
||||
id: generateId('meeting'),
|
||||
title: title,
|
||||
date: date,
|
||||
time: document.getElementById('startTime').value,
|
||||
endTime: document.getElementById('endTime').value,
|
||||
location: document.getElementById('isOnline').checked ?
|
||||
document.getElementById('onlineLink').value || '온라인 회의' :
|
||||
document.getElementById('location').value,
|
||||
participants: participants,
|
||||
description: document.getElementById('description').value,
|
||||
notifications: {
|
||||
notify30: document.getElementById('notify30').checked,
|
||||
notify60: document.getElementById('notify60').checked
|
||||
},
|
||||
repeat: document.getElementById('repeat').value,
|
||||
repeatEnd: document.getElementById('repeatEnd').value,
|
||||
calendar: {
|
||||
google: document.getElementById('googleCalendar').checked,
|
||||
outlook: document.getElementById('outlook').checked
|
||||
},
|
||||
status: 'scheduled',
|
||||
createdAt: new Date().toISOString()
|
||||
};
|
||||
|
||||
// LocalStorage에 임시 저장
|
||||
sessionStorage.setItem('newMeeting', JSON.stringify(meetingData));
|
||||
|
||||
showToast('회의 정보가 저장되었습니다.', 'success');
|
||||
|
||||
// 템플릿 선택 화면으로 이동
|
||||
setTimeout(() => {
|
||||
navigateTo('04-템플릿선택.html');
|
||||
}, 500);
|
||||
});
|
||||
|
||||
// 임시 저장
|
||||
document.getElementById('saveDraft').addEventListener('click', () => {
|
||||
const meetingData = {
|
||||
title: document.getElementById('meetingTitle').value,
|
||||
date: document.getElementById('meetingDate').value,
|
||||
time: document.getElementById('startTime').value,
|
||||
endTime: document.getElementById('endTime').value,
|
||||
location: document.getElementById('location').value,
|
||||
participants: participants,
|
||||
description: document.getElementById('description').value
|
||||
};
|
||||
|
||||
sessionStorage.setItem('draftMeeting', JSON.stringify(meetingData));
|
||||
showToast('회의 정보가 임시 저장되었습니다.', 'success');
|
||||
|
||||
setTimeout(() => {
|
||||
navigateTo('02-대시보드.html');
|
||||
}, 500);
|
||||
});
|
||||
|
||||
// 초기 업데이트
|
||||
calculateDuration();
|
||||
updatePreview();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
537
design/uiux/prototype/04-템플릿선택.html
Normal file
537
design/uiux/prototype/04-템플릿선택.html
Normal file
@ -0,0 +1,537 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>템플릿 선택 - 회의록 작성 및 공유 서비스</title>
|
||||
<link rel="stylesheet" href="common.css">
|
||||
<style>
|
||||
.template-container {
|
||||
max-width: 1536px;
|
||||
margin: 0 auto;
|
||||
padding: var(--spacing-8);
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: var(--spacing-4);
|
||||
margin-bottom: var(--spacing-8);
|
||||
}
|
||||
|
||||
.progress-step {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-2);
|
||||
}
|
||||
|
||||
.progress-circle {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
background-color: var(--gray-300);
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.progress-circle.active {
|
||||
background-color: var(--primary-main);
|
||||
}
|
||||
|
||||
.progress-circle.completed {
|
||||
background-color: var(--success-main);
|
||||
}
|
||||
|
||||
.progress-label {
|
||||
font-weight: 500;
|
||||
color: var(--gray-500);
|
||||
}
|
||||
|
||||
.progress-label.active {
|
||||
color: var(--gray-900);
|
||||
}
|
||||
|
||||
.progress-arrow {
|
||||
color: var(--gray-300);
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.template-layout {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 400px;
|
||||
gap: var(--spacing-8);
|
||||
}
|
||||
|
||||
.template-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: var(--spacing-6);
|
||||
}
|
||||
|
||||
.template-card {
|
||||
background: white;
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--spacing-6);
|
||||
box-shadow: var(--shadow-sm);
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-base);
|
||||
border: 2px solid transparent;
|
||||
}
|
||||
|
||||
.template-card:hover {
|
||||
box-shadow: var(--shadow-md);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.template-card.selected {
|
||||
border-color: var(--primary-main);
|
||||
background-color: rgba(0, 217, 177, 0.05);
|
||||
}
|
||||
|
||||
.template-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-3);
|
||||
margin-bottom: var(--spacing-3);
|
||||
}
|
||||
|
||||
.template-icon {
|
||||
font-size: 32px;
|
||||
}
|
||||
|
||||
.template-title {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
color: var(--gray-900);
|
||||
}
|
||||
|
||||
.template-description {
|
||||
color: var(--gray-600);
|
||||
font-size: 0.875rem;
|
||||
margin-bottom: var(--spacing-4);
|
||||
}
|
||||
|
||||
.template-usage {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-2);
|
||||
color: var(--gray-500);
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.template-sections {
|
||||
margin-top: var(--spacing-4);
|
||||
padding-top: var(--spacing-4);
|
||||
border-top: 1px solid var(--gray-200);
|
||||
}
|
||||
|
||||
.section-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-2);
|
||||
padding: var(--spacing-2) 0;
|
||||
font-size: 0.875rem;
|
||||
color: var(--gray-600);
|
||||
}
|
||||
|
||||
.section-required {
|
||||
color: var(--error-main);
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.preview-panel {
|
||||
position: sticky;
|
||||
top: var(--spacing-8);
|
||||
height: fit-content;
|
||||
background: white;
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--spacing-6);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.preview-title {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
color: var(--gray-900);
|
||||
margin-bottom: var(--spacing-4);
|
||||
}
|
||||
|
||||
.preview-empty {
|
||||
text-align: center;
|
||||
padding: var(--spacing-8);
|
||||
color: var(--gray-400);
|
||||
}
|
||||
|
||||
.section-list {
|
||||
margin-top: var(--spacing-4);
|
||||
}
|
||||
|
||||
.section-drag-item {
|
||||
background: var(--gray-50);
|
||||
padding: var(--spacing-3);
|
||||
margin-bottom: var(--spacing-2);
|
||||
border-radius: var(--radius-md);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-2);
|
||||
cursor: move;
|
||||
}
|
||||
|
||||
.drag-handle {
|
||||
color: var(--gray-400);
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
.section-drag-item:active .drag-handle {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.section-name {
|
||||
flex: 1;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.section-actions {
|
||||
display: flex;
|
||||
gap: var(--spacing-2);
|
||||
}
|
||||
|
||||
.icon-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
color: var(--gray-500);
|
||||
padding: var(--spacing-1);
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.icon-btn:hover {
|
||||
color: var(--primary-main);
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-top: var(--spacing-8);
|
||||
padding-top: var(--spacing-6);
|
||||
border-top: 1px solid var(--gray-200);
|
||||
}
|
||||
|
||||
@media (max-width: 1023px) {
|
||||
.template-layout {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.template-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.preview-panel {
|
||||
position: static;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="template-container">
|
||||
<!-- 진행 단계 표시 -->
|
||||
<div class="progress-bar">
|
||||
<div class="progress-step">
|
||||
<div class="progress-circle completed">✓</div>
|
||||
<span class="progress-label">회의 예약</span>
|
||||
</div>
|
||||
<span class="progress-arrow">→</span>
|
||||
<div class="progress-step">
|
||||
<div class="progress-circle active">2</div>
|
||||
<span class="progress-label active">템플릿 선택</span>
|
||||
</div>
|
||||
<span class="progress-arrow">→</span>
|
||||
<div class="progress-step">
|
||||
<div class="progress-circle">3</div>
|
||||
<span class="progress-label">회의 진행</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h1>회의 템플릿 선택</h1>
|
||||
<p style="color: var(--gray-500); margin-bottom: var(--spacing-8);">
|
||||
회의 유형에 맞는 템플릿을 선택하거나 커스터마이징하세요
|
||||
</p>
|
||||
|
||||
<div class="template-layout">
|
||||
<!-- 템플릿 그리드 -->
|
||||
<div>
|
||||
<div class="template-grid" id="templateGrid"></div>
|
||||
</div>
|
||||
|
||||
<!-- 미리보기 및 커스터마이징 -->
|
||||
<div class="preview-panel">
|
||||
<div class="preview-title">템플릿 미리보기</div>
|
||||
|
||||
<div id="previewContent" class="preview-empty">
|
||||
템플릿을 선택하면<br>여기에 미리보기가 표시됩니다
|
||||
</div>
|
||||
|
||||
<div id="customizePanel" class="hidden">
|
||||
<div style="margin-top: var(--spacing-6); margin-bottom: var(--spacing-4);">
|
||||
<strong>섹션 구성</strong>
|
||||
<p style="font-size: 0.875rem; color: var(--gray-500); margin-top: var(--spacing-1);">
|
||||
드래그하여 순서를 변경하거나 섹션을 추가/삭제할 수 있습니다
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="section-list" id="sectionList"></div>
|
||||
|
||||
<button type="button" class="btn btn-secondary btn-sm" style="width: 100%; margin-top: var(--spacing-4);" id="addSection">
|
||||
+ 섹션 추가
|
||||
</button>
|
||||
|
||||
<button type="button" class="btn btn-text btn-sm" style="width: 100%; margin-top: var(--spacing-2);" id="saveTemplate">
|
||||
✓ 나만의 템플릿으로 저장
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 액션 버튼 -->
|
||||
<div class="action-buttons">
|
||||
<button type="button" class="btn btn-text" onclick="navigateTo('03-회의예약.html')">
|
||||
← 뒤로
|
||||
</button>
|
||||
<div style="display: flex; gap: var(--spacing-3);">
|
||||
<button type="button" class="btn btn-secondary" id="startBlank">
|
||||
템플릿 없이 시작
|
||||
</button>
|
||||
<button type="button" class="btn btn-primary" id="selectTemplate" disabled>
|
||||
선택 완료 →
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="common.js"></script>
|
||||
<script>
|
||||
let selectedTemplate = null;
|
||||
let customSections = [];
|
||||
let draggedItem = null;
|
||||
|
||||
// 템플릿 아이콘 매핑
|
||||
const templateIcons = {
|
||||
general: '📋',
|
||||
scrum: '🔄',
|
||||
kickoff: '🚀',
|
||||
weekly: '📅'
|
||||
};
|
||||
|
||||
// 템플릿 렌더링
|
||||
function renderTemplates() {
|
||||
const templates = getAllTemplates();
|
||||
const grid = document.getElementById('templateGrid');
|
||||
|
||||
grid.innerHTML = templates.map(template => `
|
||||
<div class="template-card" data-id="${template.id}" onclick="selectTemplate('${template.id}')">
|
||||
<div class="template-header">
|
||||
<span class="template-icon">${templateIcons[template.id] || '📄'}</span>
|
||||
<div class="template-title">${template.name}</div>
|
||||
</div>
|
||||
<div class="template-description">${template.description}</div>
|
||||
<div class="template-usage">
|
||||
<span>👥</span>
|
||||
<span>자주 사용됨</span>
|
||||
</div>
|
||||
<div class="template-sections">
|
||||
${template.sections.map(section => `
|
||||
<div class="section-item">
|
||||
<span>•</span>
|
||||
<span>${section.name}</span>
|
||||
${section.required ? '<span class="section-required">*</span>' : ''}
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
// 템플릿 선택
|
||||
function selectTemplate(templateId) {
|
||||
selectedTemplate = templateId;
|
||||
const template = getTemplate(templateId);
|
||||
customSections = JSON.parse(JSON.stringify(template.sections));
|
||||
|
||||
// 선택 표시
|
||||
document.querySelectorAll('.template-card').forEach(card => {
|
||||
card.classList.remove('selected');
|
||||
});
|
||||
document.querySelector(`[data-id="${templateId}"]`).classList.add('selected');
|
||||
|
||||
// 미리보기 업데이트
|
||||
updatePreview();
|
||||
|
||||
// 선택 완료 버튼 활성화
|
||||
document.getElementById('selectTemplate').disabled = false;
|
||||
}
|
||||
|
||||
// 미리보기 업데이트
|
||||
function updatePreview() {
|
||||
const previewContent = document.getElementById('previewContent');
|
||||
const customizePanel = document.getElementById('customizePanel');
|
||||
|
||||
if (selectedTemplate) {
|
||||
previewContent.innerHTML = '';
|
||||
customizePanel.classList.remove('hidden');
|
||||
renderSections();
|
||||
} else {
|
||||
previewContent.className = 'preview-empty';
|
||||
previewContent.innerHTML = '템플릿을 선택하면<br>여기에 미리보기가 표시됩니다';
|
||||
customizePanel.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
// 섹션 렌더링
|
||||
function renderSections() {
|
||||
const sectionList = document.getElementById('sectionList');
|
||||
|
||||
sectionList.innerHTML = customSections.map((section, index) => `
|
||||
<div class="section-drag-item" draggable="true" data-index="${index}">
|
||||
<span class="drag-handle">⋮⋮</span>
|
||||
<span class="section-name">
|
||||
${section.name}
|
||||
${section.required ? '<span class="section-required">*</span>' : ''}
|
||||
</span>
|
||||
<div class="section-actions">
|
||||
<button class="icon-btn" onclick="editSection(${index})" title="수정">✏️</button>
|
||||
${!section.required ? `<button class="icon-btn" onclick="deleteSection(${index})" title="삭제">🗑️</button>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
// 드래그 이벤트 추가
|
||||
document.querySelectorAll('.section-drag-item').forEach(item => {
|
||||
item.addEventListener('dragstart', handleDragStart);
|
||||
item.addEventListener('dragover', handleDragOver);
|
||||
item.addEventListener('drop', handleDrop);
|
||||
item.addEventListener('dragend', handleDragEnd);
|
||||
});
|
||||
}
|
||||
|
||||
// 드래그 앤 드롭 핸들러
|
||||
function handleDragStart(e) {
|
||||
draggedItem = parseInt(e.target.dataset.index);
|
||||
e.target.style.opacity = '0.5';
|
||||
}
|
||||
|
||||
function handleDragOver(e) {
|
||||
e.preventDefault();
|
||||
e.dataTransfer.dropEffect = 'move';
|
||||
}
|
||||
|
||||
function handleDrop(e) {
|
||||
e.preventDefault();
|
||||
const targetIndex = parseInt(e.target.closest('.section-drag-item').dataset.index);
|
||||
|
||||
if (draggedItem !== targetIndex) {
|
||||
const draggedSection = customSections[draggedItem];
|
||||
customSections.splice(draggedItem, 1);
|
||||
customSections.splice(targetIndex, 0, draggedSection);
|
||||
renderSections();
|
||||
}
|
||||
}
|
||||
|
||||
function handleDragEnd(e) {
|
||||
e.target.style.opacity = '1';
|
||||
draggedItem = null;
|
||||
}
|
||||
|
||||
// 섹션 추가
|
||||
document.getElementById('addSection').addEventListener('click', () => {
|
||||
const sectionName = prompt('섹션 이름을 입력하세요:');
|
||||
if (sectionName && sectionName.trim()) {
|
||||
customSections.push({
|
||||
id: generateId('section'),
|
||||
name: sectionName.trim(),
|
||||
required: false
|
||||
});
|
||||
renderSections();
|
||||
showToast('섹션이 추가되었습니다.', 'success');
|
||||
}
|
||||
});
|
||||
|
||||
// 섹션 수정
|
||||
function editSection(index) {
|
||||
const section = customSections[index];
|
||||
const newName = prompt('섹션 이름 수정:', section.name);
|
||||
if (newName && newName.trim()) {
|
||||
customSections[index].name = newName.trim();
|
||||
renderSections();
|
||||
showToast('섹션이 수정되었습니다.', 'success');
|
||||
}
|
||||
}
|
||||
|
||||
// 섹션 삭제
|
||||
function deleteSection(index) {
|
||||
if (confirm('이 섹션을 삭제하시겠습니까?')) {
|
||||
customSections.splice(index, 1);
|
||||
renderSections();
|
||||
showToast('섹션이 삭제되었습니다.', 'info');
|
||||
}
|
||||
}
|
||||
|
||||
// 나만의 템플릿 저장
|
||||
document.getElementById('saveTemplate').addEventListener('click', () => {
|
||||
const templateName = prompt('템플릿 이름을 입력하세요:');
|
||||
if (templateName && templateName.trim()) {
|
||||
const customTemplate = {
|
||||
id: generateId('template'),
|
||||
name: templateName.trim(),
|
||||
description: '나만의 템플릿',
|
||||
sections: customSections,
|
||||
createdBy: getCurrentUser().id,
|
||||
createdAt: new Date().toISOString()
|
||||
};
|
||||
|
||||
// 여기서는 시뮬레이션만 (실제로는 서버에 저장)
|
||||
showToast('템플릿이 저장되었습니다.', 'success');
|
||||
}
|
||||
});
|
||||
|
||||
// 템플릿 없이 시작
|
||||
document.getElementById('startBlank').addEventListener('click', () => {
|
||||
if (confirm('빈 회의록으로 시작하시겠습니까?')) {
|
||||
sessionStorage.setItem('selectedTemplate', JSON.stringify({
|
||||
id: 'blank',
|
||||
name: '빈 회의록',
|
||||
sections: []
|
||||
}));
|
||||
navigateTo('05-회의진행.html');
|
||||
}
|
||||
});
|
||||
|
||||
// 선택 완료
|
||||
document.getElementById('selectTemplate').addEventListener('click', () => {
|
||||
if (!selectedTemplate) {
|
||||
showToast('템플릿을 선택하세요.', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
const template = getTemplate(selectedTemplate);
|
||||
const finalTemplate = {
|
||||
...template,
|
||||
sections: customSections
|
||||
};
|
||||
|
||||
sessionStorage.setItem('selectedTemplate', JSON.stringify(finalTemplate));
|
||||
showToast('템플릿이 선택되었습니다.', 'success');
|
||||
|
||||
setTimeout(() => {
|
||||
navigateTo('05-회의진행.html');
|
||||
}, 500);
|
||||
});
|
||||
|
||||
// 초기화
|
||||
renderTemplates();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
858
design/uiux/prototype/05-회의진행.html
Normal file
858
design/uiux/prototype/05-회의진행.html
Normal file
@ -0,0 +1,858 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>회의 진행 - 회의록 작성 및 공유 서비스</title>
|
||||
<link rel="stylesheet" href="common.css">
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.meeting-editor {
|
||||
display: grid;
|
||||
grid-template-columns: 250px 1fr 350px;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
/* 좌측 패널 */
|
||||
.left-panel {
|
||||
background: white;
|
||||
border-right: 1px solid var(--gray-200);
|
||||
padding: var(--spacing-6);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.meeting-info {
|
||||
margin-bottom: var(--spacing-6);
|
||||
padding-bottom: var(--spacing-6);
|
||||
border-bottom: 1px solid var(--gray-200);
|
||||
}
|
||||
|
||||
.meeting-title-small {
|
||||
font-weight: 600;
|
||||
color: var(--gray-900);
|
||||
margin-bottom: var(--spacing-2);
|
||||
}
|
||||
|
||||
.meeting-time {
|
||||
font-size: 0.875rem;
|
||||
color: var(--gray-500);
|
||||
}
|
||||
|
||||
.timer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-2);
|
||||
padding: var(--spacing-3);
|
||||
background: var(--gray-50);
|
||||
border-radius: var(--radius-md);
|
||||
margin-top: var(--spacing-3);
|
||||
}
|
||||
|
||||
.timer-icon {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.timer-text {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
font-variant-numeric: tabular-nums;
|
||||
color: var(--primary-main);
|
||||
}
|
||||
|
||||
.recording-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-2);
|
||||
margin-top: var(--spacing-2);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.recording-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background-color: var(--error-main);
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.5; }
|
||||
}
|
||||
|
||||
.participants-list {
|
||||
margin-bottom: var(--spacing-6);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: var(--gray-700);
|
||||
margin-bottom: var(--spacing-3);
|
||||
}
|
||||
|
||||
.participant-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-2);
|
||||
padding: var(--spacing-2) 0;
|
||||
}
|
||||
|
||||
.participant-avatar {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
background: var(--gray-200);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.participant-avatar.online {
|
||||
border: 2px solid var(--success-main);
|
||||
}
|
||||
|
||||
.participant-name {
|
||||
flex: 1;
|
||||
font-size: 0.875rem;
|
||||
color: var(--gray-700);
|
||||
}
|
||||
|
||||
.participant-status {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background-color: var(--success-main);
|
||||
}
|
||||
|
||||
/* 중앙 에디터 */
|
||||
.editor-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: white;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.editor-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-1);
|
||||
padding: var(--spacing-3) var(--spacing-4);
|
||||
border-bottom: 1px solid var(--gray-200);
|
||||
background: var(--gray-50);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.toolbar-group {
|
||||
display: flex;
|
||||
gap: var(--spacing-1);
|
||||
padding-right: var(--spacing-3);
|
||||
border-right: 1px solid var(--gray-300);
|
||||
}
|
||||
|
||||
.toolbar-group:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.toolbar-btn {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
border-radius: var(--radius-md);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--gray-600);
|
||||
font-size: 16px;
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.toolbar-btn:hover {
|
||||
background: white;
|
||||
color: var(--primary-main);
|
||||
}
|
||||
|
||||
.toolbar-btn.active {
|
||||
background: var(--primary-main);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.editor-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: var(--spacing-8) var(--spacing-6);
|
||||
}
|
||||
|
||||
.section-block {
|
||||
margin-bottom: var(--spacing-8);
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: var(--spacing-4);
|
||||
padding-bottom: var(--spacing-3);
|
||||
border-bottom: 2px solid var(--gray-200);
|
||||
}
|
||||
|
||||
.section-header h2 {
|
||||
font-size: 1.5rem;
|
||||
color: var(--gray-900);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.section-verified {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-2);
|
||||
font-size: 0.875rem;
|
||||
color: var(--gray-600);
|
||||
}
|
||||
|
||||
.section-verified input[type="checkbox"] {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.section-verified input[type="checkbox"]:checked {
|
||||
accent-color: var(--success-main);
|
||||
}
|
||||
|
||||
.section-body {
|
||||
min-height: 150px;
|
||||
padding: var(--spacing-4);
|
||||
border: 1px solid var(--gray-200);
|
||||
border-radius: var(--radius-md);
|
||||
font-size: 1rem;
|
||||
line-height: 1.75;
|
||||
outline: none;
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.section-body:focus {
|
||||
border-color: var(--primary-main);
|
||||
box-shadow: 0 0 0 3px rgba(0, 217, 177, 0.1);
|
||||
}
|
||||
|
||||
.section-body.verified {
|
||||
background-color: rgba(16, 185, 129, 0.05);
|
||||
border-color: var(--success-main);
|
||||
}
|
||||
|
||||
.ai-suggestion {
|
||||
background: var(--gray-50);
|
||||
border: 1px dashed var(--primary-main);
|
||||
border-radius: var(--radius-md);
|
||||
padding: var(--spacing-4);
|
||||
margin-top: var(--spacing-4);
|
||||
}
|
||||
|
||||
.ai-suggestion-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-2);
|
||||
margin-bottom: var(--spacing-3);
|
||||
font-weight: 600;
|
||||
color: var(--primary-main);
|
||||
}
|
||||
|
||||
.ai-suggestion-content {
|
||||
color: var(--gray-700);
|
||||
margin-bottom: var(--spacing-3);
|
||||
}
|
||||
|
||||
.ai-suggestion-actions {
|
||||
display: flex;
|
||||
gap: var(--spacing-2);
|
||||
}
|
||||
|
||||
.term-highlight {
|
||||
color: var(--primary-main);
|
||||
border-bottom: 2px dotted var(--primary-main);
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.term-highlight:hover {
|
||||
background-color: rgba(0, 217, 177, 0.1);
|
||||
}
|
||||
|
||||
/* 우측 패널 */
|
||||
.right-panel {
|
||||
background: white;
|
||||
border-left: 1px solid var(--gray-200);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.panel-tabs {
|
||||
display: flex;
|
||||
border-bottom: 1px solid var(--gray-200);
|
||||
}
|
||||
|
||||
.panel-tab {
|
||||
flex: 1;
|
||||
padding: var(--spacing-3);
|
||||
border: none;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
color: var(--gray-600);
|
||||
transition: all var(--transition-fast);
|
||||
border-bottom: 2px solid transparent;
|
||||
}
|
||||
|
||||
.panel-tab:hover {
|
||||
background: var(--gray-50);
|
||||
}
|
||||
|
||||
.panel-tab.active {
|
||||
color: var(--primary-main);
|
||||
border-bottom-color: var(--primary-main);
|
||||
}
|
||||
|
||||
.panel-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: var(--spacing-4);
|
||||
}
|
||||
|
||||
.ai-suggestion-card {
|
||||
background: var(--gray-50);
|
||||
border-radius: var(--radius-md);
|
||||
padding: var(--spacing-4);
|
||||
margin-bottom: var(--spacing-3);
|
||||
}
|
||||
|
||||
.ai-suggestion-title {
|
||||
font-weight: 600;
|
||||
color: var(--gray-900);
|
||||
margin-bottom: var(--spacing-2);
|
||||
}
|
||||
|
||||
.ai-suggestion-text {
|
||||
color: var(--gray-700);
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.6;
|
||||
margin-bottom: var(--spacing-3);
|
||||
}
|
||||
|
||||
.ai-actions {
|
||||
display: flex;
|
||||
gap: var(--spacing-2);
|
||||
}
|
||||
|
||||
.term-card {
|
||||
background: var(--gray-50);
|
||||
border-radius: var(--radius-md);
|
||||
padding: var(--spacing-4);
|
||||
margin-bottom: var(--spacing-3);
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.term-card:hover {
|
||||
background: var(--gray-100);
|
||||
}
|
||||
|
||||
.term-name {
|
||||
font-weight: 600;
|
||||
color: var(--primary-main);
|
||||
margin-bottom: var(--spacing-2);
|
||||
}
|
||||
|
||||
.term-definition {
|
||||
color: var(--gray-700);
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.term-source {
|
||||
color: var(--gray-500);
|
||||
font-size: 0.75rem;
|
||||
margin-top: var(--spacing-2);
|
||||
}
|
||||
|
||||
/* 하단 바 */
|
||||
.bottom-bar {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 250px;
|
||||
right: 350px;
|
||||
background: white;
|
||||
border-top: 1px solid var(--gray-200);
|
||||
padding: var(--spacing-4) var(--spacing-6);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-2);
|
||||
color: var(--gray-600);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background-color: var(--success-main);
|
||||
}
|
||||
|
||||
.voice-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-2);
|
||||
color: var(--gray-600);
|
||||
}
|
||||
|
||||
.voice-wave {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.voice-bar {
|
||||
width: 3px;
|
||||
height: 16px;
|
||||
background: var(--primary-main);
|
||||
border-radius: 2px;
|
||||
animation: voiceWave 1s infinite;
|
||||
}
|
||||
|
||||
.voice-bar:nth-child(2) { animation-delay: 0.1s; }
|
||||
.voice-bar:nth-child(3) { animation-delay: 0.2s; }
|
||||
.voice-bar:nth-child(4) { animation-delay: 0.3s; }
|
||||
|
||||
@keyframes voiceWave {
|
||||
0%, 100% { height: 8px; }
|
||||
50% { height: 16px; }
|
||||
}
|
||||
|
||||
.bottom-actions {
|
||||
display: flex;
|
||||
gap: var(--spacing-3);
|
||||
}
|
||||
|
||||
@media (max-width: 1023px) {
|
||||
.meeting-editor {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.left-panel,
|
||||
.right-panel {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.bottom-bar {
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="meeting-editor">
|
||||
<!-- 좌측 패널: 회의 정보 & 참석자 -->
|
||||
<div class="left-panel">
|
||||
<div class="meeting-info">
|
||||
<div class="meeting-title-small" id="meetingTitle">2025년 1분기 전략 회의</div>
|
||||
<div class="meeting-time" id="meetingTime">2025년 10월 20일 14:00</div>
|
||||
|
||||
<div class="timer">
|
||||
<span class="timer-icon">⏱️</span>
|
||||
<span class="timer-text" id="timer">00:00:00</span>
|
||||
</div>
|
||||
|
||||
<div class="recording-status">
|
||||
<span class="recording-dot"></span>
|
||||
<span>녹음 중</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="participants-list">
|
||||
<div class="section-title">참석자 (3명)</div>
|
||||
<div id="participantsList"></div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="section-title">타임라인</div>
|
||||
<div id="timeline" style="font-size: 0.75rem; color: var(--gray-500);"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 중앙 패널: 에디터 -->
|
||||
<div class="editor-panel">
|
||||
<!-- 툴바 -->
|
||||
<div class="editor-toolbar">
|
||||
<div class="toolbar-group">
|
||||
<button class="toolbar-btn" title="굵게" onclick="execCommand('bold')"><strong>B</strong></button>
|
||||
<button class="toolbar-btn" title="기울임" onclick="execCommand('italic')"><em>I</em></button>
|
||||
<button class="toolbar-btn" title="밑줄" onclick="execCommand('underline')"><u>U</u></button>
|
||||
<button class="toolbar-btn" title="취소선" onclick="execCommand('strikeThrough')"><s>S</s></button>
|
||||
</div>
|
||||
<div class="toolbar-group">
|
||||
<button class="toolbar-btn" title="번호 목록" onclick="execCommand('insertOrderedList')">1.</button>
|
||||
<button class="toolbar-btn" title="글머리 기호" onclick="execCommand('insertUnorderedList')">•</button>
|
||||
</div>
|
||||
<div class="toolbar-group">
|
||||
<button class="toolbar-btn" title="링크" onclick="insertLink()">🔗</button>
|
||||
<button class="toolbar-btn" title="실행 취소" onclick="document.execCommand('undo')">↶</button>
|
||||
<button class="toolbar-btn" title="다시 실행" onclick="document.execCommand('redo')">↷</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 에디터 콘텐츠 -->
|
||||
<div class="editor-content" id="editorContent"></div>
|
||||
|
||||
<!-- 하단 바 -->
|
||||
<div class="bottom-bar">
|
||||
<div class="status-indicator">
|
||||
<span class="status-dot"></span>
|
||||
<span id="saveStatus">저장됨</span>
|
||||
</div>
|
||||
|
||||
<div class="voice-status">
|
||||
<div class="voice-wave">
|
||||
<div class="voice-bar"></div>
|
||||
<div class="voice-bar"></div>
|
||||
<div class="voice-bar"></div>
|
||||
<div class="voice-bar"></div>
|
||||
</div>
|
||||
<span>음성 인식 중...</span>
|
||||
</div>
|
||||
|
||||
<div class="bottom-actions">
|
||||
<button class="btn btn-secondary btn-sm" id="pauseRecording">
|
||||
⏸️ 일시정지
|
||||
</button>
|
||||
<button class="btn btn-primary" id="endMeeting">
|
||||
회의 종료
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 우측 패널: AI 제안 & 용어 설명 -->
|
||||
<div class="right-panel">
|
||||
<div class="panel-tabs">
|
||||
<button class="panel-tab active" data-tab="ai" onclick="switchTab('ai')">AI 제안</button>
|
||||
<button class="panel-tab" data-tab="terms" onclick="switchTab('terms')">용어 설명</button>
|
||||
<button class="panel-tab" data-tab="comments" onclick="switchTab('comments')">댓글</button>
|
||||
</div>
|
||||
|
||||
<div class="panel-content">
|
||||
<!-- AI 제안 탭 -->
|
||||
<div id="aiTab" class="tab-pane">
|
||||
<p style="color: var(--gray-500); font-size: 0.875rem; margin-bottom: var(--spacing-4);">
|
||||
AI가 자동으로 회의록을 작성하고 제안합니다
|
||||
</p>
|
||||
<div id="aiSuggestions"></div>
|
||||
</div>
|
||||
|
||||
<!-- 용어 설명 탭 -->
|
||||
<div id="termsTab" class="tab-pane hidden">
|
||||
<p style="color: var(--gray-500); font-size: 0.875rem; margin-bottom: var(--spacing-4);">
|
||||
감지된 전문용어와 설명입니다
|
||||
</p>
|
||||
<div id="termsList"></div>
|
||||
</div>
|
||||
|
||||
<!-- 댓글 탭 -->
|
||||
<div id="commentsTab" class="tab-pane hidden">
|
||||
<p style="color: var(--gray-500); font-size: 0.875rem; margin-bottom: var(--spacing-4);">
|
||||
참석자의 댓글과 제안사항입니다
|
||||
</p>
|
||||
<div style="text-align: center; padding: var(--spacing-8); color: var(--gray-400);">
|
||||
아직 댓글이 없습니다
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="common.js"></script>
|
||||
<script>
|
||||
let elapsedSeconds = 0;
|
||||
let timerInterval;
|
||||
let saveTimeout;
|
||||
let currentSuggestionIndex = 0;
|
||||
|
||||
// 회의 정보 로드
|
||||
function loadMeetingInfo() {
|
||||
const meetingData = JSON.parse(sessionStorage.getItem('newMeeting') || '{}');
|
||||
const template = JSON.parse(sessionStorage.getItem('selectedTemplate') || '{}');
|
||||
|
||||
if (meetingData.title) {
|
||||
document.getElementById('meetingTitle').textContent = meetingData.title;
|
||||
}
|
||||
|
||||
if (meetingData.date && meetingData.time) {
|
||||
const dateObj = new Date(meetingData.date);
|
||||
const dateStr = `${dateObj.getFullYear()}년 ${dateObj.getMonth() + 1}월 ${dateObj.getDate()}일`;
|
||||
document.getElementById('meetingTime').textContent = `${dateStr} ${meetingData.time}`;
|
||||
}
|
||||
|
||||
// 참석자 렌더링
|
||||
if (meetingData.participants) {
|
||||
const users = getFromStorage(STORAGE_KEYS.USERS) || [];
|
||||
const participantsList = document.getElementById('participantsList');
|
||||
participantsList.innerHTML = meetingData.participants.map(email => {
|
||||
const user = users.find(u => u.email === email) || { name: email, avatar: '👤' };
|
||||
return `
|
||||
<div class="participant-item">
|
||||
<div class="participant-avatar online">${user.avatar}</div>
|
||||
<div class="participant-name">${user.name}</div>
|
||||
<div class="participant-status"></div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// 템플릿 섹션 렌더링
|
||||
if (template.sections) {
|
||||
const editorContent = document.getElementById('editorContent');
|
||||
editorContent.innerHTML = template.sections.map(section => `
|
||||
<div class="section-block" data-section="${section.id}">
|
||||
<div class="section-header">
|
||||
<h2>${section.name}${section.required ? '<span style="color: var(--error-main);">*</span>' : ''}</h2>
|
||||
<div class="section-verified">
|
||||
<input type="checkbox" id="verify-${section.id}">
|
||||
<label for="verify-${section.id}">검증 완료</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section-body" contenteditable="true" data-section-id="${section.id}">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
// 섹션 검증 이벤트
|
||||
document.querySelectorAll('[id^="verify-"]').forEach(checkbox => {
|
||||
checkbox.addEventListener('change', (e) => {
|
||||
const sectionId = e.target.id.replace('verify-', '');
|
||||
const sectionBody = document.querySelector(`[data-section-id="${sectionId}"]`);
|
||||
if (e.target.checked) {
|
||||
sectionBody.classList.add('verified');
|
||||
showToast('섹션이 검증되었습니다.', 'success');
|
||||
} else {
|
||||
sectionBody.classList.remove('verified');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 자동 저장
|
||||
document.querySelectorAll('.section-body').forEach(section => {
|
||||
section.addEventListener('input', () => {
|
||||
clearTimeout(saveTimeout);
|
||||
document.getElementById('saveStatus').textContent = '저장 중...';
|
||||
|
||||
saveTimeout = setTimeout(() => {
|
||||
document.getElementById('saveStatus').textContent = '저장됨';
|
||||
// 실제로는 서버에 저장
|
||||
}, 500);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 타이머 시작
|
||||
function startTimer() {
|
||||
timerInterval = setInterval(() => {
|
||||
elapsedSeconds++;
|
||||
const hours = Math.floor(elapsedSeconds / 3600);
|
||||
const minutes = Math.floor((elapsedSeconds % 3600) / 60);
|
||||
const seconds = elapsedSeconds % 60;
|
||||
|
||||
document.getElementById('timer').textContent =
|
||||
`${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
|
||||
|
||||
// 5분마다 타임라인 추가
|
||||
if (elapsedSeconds % 300 === 0) {
|
||||
addTimelineEvent(`${hours}시간 ${minutes}분 경과`);
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
// 타임라인 이벤트 추가
|
||||
function addTimelineEvent(text) {
|
||||
const timeline = document.getElementById('timeline');
|
||||
const time = new Date().toLocaleTimeString('ko-KR', { hour: '2-digit', minute: '2-digit' });
|
||||
const event = document.createElement('div');
|
||||
event.style.marginBottom = 'var(--spacing-2)';
|
||||
event.innerHTML = `<strong>${time}</strong> ${text}`;
|
||||
timeline.appendChild(event);
|
||||
}
|
||||
|
||||
// AI 제안 시뮬레이션
|
||||
function generateAISuggestions() {
|
||||
const suggestions = [
|
||||
{
|
||||
title: '논의 내용 요약',
|
||||
text: '1분기 매출 목표를 20% 상향 조정하고, 마케팅 예산을 15% 증액하기로 논의했습니다.'
|
||||
},
|
||||
{
|
||||
title: '결정 사항',
|
||||
text: '신규 프로젝트 팀을 구성하고, 김민준님이 PM을 맡기로 결정했습니다.'
|
||||
},
|
||||
{
|
||||
title: 'Todo 추출',
|
||||
text: '• 마케팅 예산안 작성 (박서연, 10/25)\n• 경쟁사 분석 보고서 (이준호, 10/22)'
|
||||
}
|
||||
];
|
||||
|
||||
const container = document.getElementById('aiSuggestions');
|
||||
|
||||
function showNextSuggestion() {
|
||||
if (currentSuggestionIndex < suggestions.length) {
|
||||
const suggestion = suggestions[currentSuggestionIndex];
|
||||
const card = document.createElement('div');
|
||||
card.className = 'ai-suggestion-card';
|
||||
card.innerHTML = `
|
||||
<div class="ai-suggestion-title">✨ ${suggestion.title}</div>
|
||||
<div class="ai-suggestion-text">${suggestion.text}</div>
|
||||
<div class="ai-actions">
|
||||
<button class="btn btn-primary btn-sm" onclick="applySuggestion(this, '${suggestion.title}')">적용</button>
|
||||
<button class="btn btn-text btn-sm" onclick="dismissSuggestion(this)">무시</button>
|
||||
</div>
|
||||
`;
|
||||
container.appendChild(card);
|
||||
currentSuggestionIndex++;
|
||||
|
||||
// 5초 후 다음 제안
|
||||
setTimeout(showNextSuggestion, 5000);
|
||||
}
|
||||
}
|
||||
|
||||
// 3초 후 첫 제안 시작
|
||||
setTimeout(showNextSuggestion, 3000);
|
||||
}
|
||||
|
||||
// 제안 적용
|
||||
function applySuggestion(button, title) {
|
||||
const card = button.closest('.ai-suggestion-card');
|
||||
const text = card.querySelector('.ai-suggestion-text').textContent;
|
||||
|
||||
// 첫 번째 섹션에 텍스트 추가 (시뮬레이션)
|
||||
const sections = document.querySelectorAll('.section-body');
|
||||
if (sections.length > 0) {
|
||||
const firstSection = sections[0];
|
||||
if (!firstSection.textContent.trim()) {
|
||||
firstSection.textContent = text;
|
||||
} else {
|
||||
firstSection.innerHTML += `<br><br>${text}`;
|
||||
}
|
||||
}
|
||||
|
||||
card.remove();
|
||||
showToast('AI 제안이 적용되었습니다.', 'success');
|
||||
}
|
||||
|
||||
// 제안 무시
|
||||
function dismissSuggestion(button) {
|
||||
button.closest('.ai-suggestion-card').remove();
|
||||
}
|
||||
|
||||
// 용어 설명 렌더링
|
||||
function renderTerms() {
|
||||
const terms = [
|
||||
{ name: 'ROI', definition: '투자 대비 수익률 (Return On Investment)', source: '과거 회의록: 2024년 4분기 전략 회의' },
|
||||
{ name: 'KPI', definition: '핵심 성과 지표 (Key Performance Indicator)', source: '사내 용어집' },
|
||||
{ name: 'MQL', definition: '마케팅 적격 리드 (Marketing Qualified Lead)', source: '마케팅 팀 문서' }
|
||||
];
|
||||
|
||||
const termsList = document.getElementById('termsList');
|
||||
termsList.innerHTML = terms.map(term => `
|
||||
<div class="term-card">
|
||||
<div class="term-name">${term.name}</div>
|
||||
<div class="term-definition">${term.definition}</div>
|
||||
<div class="term-source">출처: ${term.source}</div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
// 탭 전환
|
||||
function switchTab(tabName) {
|
||||
document.querySelectorAll('.panel-tab').forEach(tab => {
|
||||
tab.classList.remove('active');
|
||||
});
|
||||
document.querySelector(`[data-tab="${tabName}"]`).classList.add('active');
|
||||
|
||||
document.querySelectorAll('.tab-pane').forEach(pane => {
|
||||
pane.classList.add('hidden');
|
||||
});
|
||||
document.getElementById(`${tabName}Tab`).classList.remove('hidden');
|
||||
}
|
||||
|
||||
// 툴바 명령
|
||||
function execCommand(command) {
|
||||
document.execCommand(command, false, null);
|
||||
}
|
||||
|
||||
function insertLink() {
|
||||
const url = prompt('링크 URL을 입력하세요:');
|
||||
if (url) {
|
||||
document.execCommand('createLink', false, url);
|
||||
}
|
||||
}
|
||||
|
||||
// 녹음 일시정지
|
||||
document.getElementById('pauseRecording').addEventListener('click', function() {
|
||||
if (this.textContent.includes('일시정지')) {
|
||||
clearInterval(timerInterval);
|
||||
this.innerHTML = '▶️ 계속';
|
||||
document.querySelector('.recording-dot').style.display = 'none';
|
||||
document.querySelector('.voice-status').style.display = 'none';
|
||||
showToast('녹음이 일시정지되었습니다.', 'info');
|
||||
} else {
|
||||
startTimer();
|
||||
this.innerHTML = '⏸️ 일시정지';
|
||||
document.querySelector('.recording-dot').style.display = 'block';
|
||||
document.querySelector('.voice-status').style.display = 'flex';
|
||||
showToast('녹음이 재개되었습니다.', 'success');
|
||||
}
|
||||
});
|
||||
|
||||
// 회의 종료
|
||||
document.getElementById('endMeeting').addEventListener('click', () => {
|
||||
if (confirm('회의를 종료하시겠습니까?')) {
|
||||
clearInterval(timerInterval);
|
||||
|
||||
// 회의록 데이터 저장
|
||||
const sections = {};
|
||||
document.querySelectorAll('.section-body').forEach(section => {
|
||||
const id = section.dataset.sectionId;
|
||||
sections[id] = section.innerHTML;
|
||||
});
|
||||
|
||||
sessionStorage.setItem('meetingContent', JSON.stringify(sections));
|
||||
sessionStorage.setItem('meetingDuration', elapsedSeconds);
|
||||
|
||||
navigateTo('06-검증완료.html');
|
||||
}
|
||||
});
|
||||
|
||||
// 초기화
|
||||
loadMeetingInfo();
|
||||
startTimer();
|
||||
generateAISuggestions();
|
||||
renderTerms();
|
||||
addTimelineEvent('회의 시작');
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
499
design/uiux/prototype/06-검증완료.html
Normal file
499
design/uiux/prototype/06-검증완료.html
Normal file
@ -0,0 +1,499 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>검증 완료 - 회의록 작성 및 공유 서비스</title>
|
||||
<link rel="stylesheet" href="common.css">
|
||||
<style>
|
||||
.verification-container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: var(--spacing-8);
|
||||
}
|
||||
|
||||
.verification-header {
|
||||
text-align: center;
|
||||
margin-bottom: var(--spacing-8);
|
||||
}
|
||||
|
||||
.progress-circle-large {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
margin: 0 auto var(--spacing-4);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.progress-ring {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
.progress-ring-circle {
|
||||
transition: stroke-dashoffset 0.5s;
|
||||
stroke: var(--primary-main);
|
||||
stroke-width: 8;
|
||||
fill: transparent;
|
||||
}
|
||||
|
||||
.progress-ring-bg {
|
||||
stroke: var(--gray-200);
|
||||
stroke-width: 8;
|
||||
fill: transparent;
|
||||
}
|
||||
|
||||
.progress-text {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: var(--primary-main);
|
||||
}
|
||||
|
||||
.verification-layout {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 400px;
|
||||
gap: var(--spacing-8);
|
||||
}
|
||||
|
||||
.section-list-panel {
|
||||
background: white;
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--spacing-6);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.verification-item {
|
||||
padding: var(--spacing-4);
|
||||
margin-bottom: var(--spacing-3);
|
||||
border-radius: var(--radius-md);
|
||||
border: 1px solid var(--gray-200);
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.verification-item:hover {
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.verification-item.verified {
|
||||
background-color: rgba(16, 185, 129, 0.05);
|
||||
border-color: var(--success-main);
|
||||
}
|
||||
|
||||
.verification-item.pending {
|
||||
background-color: rgba(245, 158, 11, 0.05);
|
||||
border-color: var(--warning-main);
|
||||
}
|
||||
|
||||
.verification-header-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: var(--spacing-2);
|
||||
}
|
||||
|
||||
.section-name-verify {
|
||||
font-weight: 600;
|
||||
color: var(--gray-900);
|
||||
}
|
||||
|
||||
.verification-status {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-2);
|
||||
padding: var(--spacing-1) var(--spacing-3);
|
||||
border-radius: var(--radius-full);
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.verification-status.verified {
|
||||
background-color: var(--success-light);
|
||||
color: var(--success-dark);
|
||||
}
|
||||
|
||||
.verification-status.pending {
|
||||
background-color: var(--warning-light);
|
||||
color: var(--warning-dark);
|
||||
}
|
||||
|
||||
.verifier-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-2);
|
||||
margin-top: var(--spacing-2);
|
||||
font-size: 0.875rem;
|
||||
color: var(--gray-600);
|
||||
}
|
||||
|
||||
.verifier-avatar {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
background: var(--gray-200);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.verify-btn {
|
||||
margin-top: var(--spacing-3);
|
||||
}
|
||||
|
||||
.lock-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-2);
|
||||
margin-top: var(--spacing-2);
|
||||
font-size: 0.875rem;
|
||||
color: var(--gray-600);
|
||||
}
|
||||
|
||||
.stats-panel {
|
||||
background: white;
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--spacing-6);
|
||||
box-shadow: var(--shadow-sm);
|
||||
position: sticky;
|
||||
top: var(--spacing-8);
|
||||
}
|
||||
|
||||
.stats-item {
|
||||
padding: var(--spacing-4);
|
||||
background: var(--gray-50);
|
||||
border-radius: var(--radius-md);
|
||||
margin-bottom: var(--spacing-3);
|
||||
}
|
||||
|
||||
.stats-label {
|
||||
font-size: 0.875rem;
|
||||
color: var(--gray-500);
|
||||
margin-bottom: var(--spacing-1);
|
||||
}
|
||||
|
||||
.stats-value {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: var(--gray-900);
|
||||
}
|
||||
|
||||
.participant-progress {
|
||||
margin-top: var(--spacing-6);
|
||||
}
|
||||
|
||||
.participant-progress-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-3);
|
||||
margin-bottom: var(--spacing-3);
|
||||
}
|
||||
|
||||
.participant-avatar-small {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
background: var(--gray-200);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.progress-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.progress-name {
|
||||
font-weight: 500;
|
||||
font-size: 0.875rem;
|
||||
color: var(--gray-900);
|
||||
margin-bottom: var(--spacing-1);
|
||||
}
|
||||
|
||||
.progress-bar-small {
|
||||
height: 6px;
|
||||
background: var(--gray-200);
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: var(--primary-main);
|
||||
transition: width 0.3s;
|
||||
}
|
||||
|
||||
.progress-percentage {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: var(--primary-main);
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-top: var(--spacing-8);
|
||||
padding-top: var(--spacing-6);
|
||||
border-top: 1px solid var(--gray-200);
|
||||
}
|
||||
|
||||
@media (max-width: 1023px) {
|
||||
.verification-layout {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.stats-panel {
|
||||
position: static;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="verification-container">
|
||||
<div class="verification-header">
|
||||
<div class="progress-circle-large">
|
||||
<svg class="progress-ring" width="120" height="120">
|
||||
<circle class="progress-ring-bg" cx="60" cy="60" r="52"></circle>
|
||||
<circle class="progress-ring-circle" cx="60" cy="60" r="52"
|
||||
stroke-dasharray="326.73" stroke-dashoffset="0" id="progressCircle"></circle>
|
||||
</svg>
|
||||
<div class="progress-text" id="progressText">0%</div>
|
||||
</div>
|
||||
<h1>섹션 검증</h1>
|
||||
<p style="color: var(--gray-500);">
|
||||
각 섹션을 확인하고 검증을 완료하세요
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="verification-layout">
|
||||
<!-- 섹션 리스트 -->
|
||||
<div class="section-list-panel">
|
||||
<h3 style="margin-bottom: var(--spacing-4);">검증 항목</h3>
|
||||
<div id="verificationList"></div>
|
||||
</div>
|
||||
|
||||
<!-- 통계 패널 -->
|
||||
<div class="stats-panel">
|
||||
<h3 style="margin-bottom: var(--spacing-4);">검증 현황</h3>
|
||||
|
||||
<div class="stats-item">
|
||||
<div class="stats-label">전체 진행률</div>
|
||||
<div class="stats-value" id="totalProgress">0%</div>
|
||||
</div>
|
||||
|
||||
<div class="stats-item">
|
||||
<div class="stats-label">검증 완료</div>
|
||||
<div class="stats-value">
|
||||
<span id="verifiedCount">0</span> / <span id="totalCount">0</span> 섹션
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stats-item">
|
||||
<div class="stats-label">잠금 섹션</div>
|
||||
<div class="stats-value" id="lockedCount">0</div>
|
||||
</div>
|
||||
|
||||
<div class="participant-progress">
|
||||
<div class="stats-label" style="margin-bottom: var(--spacing-3);">참석자별 진행률</div>
|
||||
<div id="participantProgress"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 액션 버튼 -->
|
||||
<div class="action-buttons">
|
||||
<button type="button" class="btn btn-text" onclick="navigateTo('05-회의진행.html')">
|
||||
← 회의록으로 돌아가기
|
||||
</button>
|
||||
<button type="button" class="btn btn-primary" id="completeVerification" disabled>
|
||||
검증 완료 및 회의 종료 →
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="common.js"></script>
|
||||
<script>
|
||||
let sections = [];
|
||||
let verifiedSections = new Set();
|
||||
|
||||
// 섹션 데이터 로드
|
||||
function loadSections() {
|
||||
const template = JSON.parse(sessionStorage.getItem('selectedTemplate') || '{}');
|
||||
|
||||
if (template.sections) {
|
||||
sections = template.sections.map(section => ({
|
||||
...section,
|
||||
verified: false,
|
||||
locked: false,
|
||||
verifier: null,
|
||||
verifiedAt: null
|
||||
}));
|
||||
|
||||
renderVerificationList();
|
||||
renderParticipantProgress();
|
||||
updateStats();
|
||||
}
|
||||
}
|
||||
|
||||
// 검증 리스트 렌더링
|
||||
function renderVerificationList() {
|
||||
const list = document.getElementById('verificationList');
|
||||
const currentUser = getCurrentUser();
|
||||
|
||||
list.innerHTML = sections.map((section, index) => `
|
||||
<div class="verification-item ${section.verified ? 'verified' : 'pending'}" id="section-${index}">
|
||||
<div class="verification-header-item">
|
||||
<div class="section-name-verify">
|
||||
${section.name}
|
||||
${section.required ? '<span style="color: var(--error-main);">*</span>' : ''}
|
||||
</div>
|
||||
<div class="verification-status ${section.verified ? 'verified' : 'pending'}">
|
||||
${section.verified ? '✓ 검증 완료' : '⏳ 검증 대기'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${section.verified && section.verifier ? `
|
||||
<div class="verifier-info">
|
||||
<div class="verifier-avatar">${getUserById(section.verifier)?.avatar || '👤'}</div>
|
||||
<span>검증자: ${getUserName(section.verifier)}</span>
|
||||
<span style="color: var(--gray-400);">•</span>
|
||||
<span>${new Date(section.verifiedAt).toLocaleTimeString('ko-KR', { hour: '2-digit', minute: '2-digit' })}</span>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
${!section.verified ? `
|
||||
<button class="btn btn-primary btn-sm verify-btn" onclick="verifySectionItem(${index})">
|
||||
검증 완료 표시
|
||||
</button>
|
||||
` : `
|
||||
<div class="lock-toggle">
|
||||
<input type="checkbox" id="lock-${index}" ${section.locked ? 'checked' : ''} onchange="toggleLock(${index})">
|
||||
<label for="lock-${index}">섹션 잠금 (편집 불가)</label>
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
// 섹션 검증
|
||||
function verifySectionItem(index) {
|
||||
const currentUser = getCurrentUser();
|
||||
sections[index].verified = true;
|
||||
sections[index].verifier = currentUser.id;
|
||||
sections[index].verifiedAt = new Date().toISOString();
|
||||
|
||||
verifiedSections.add(index);
|
||||
|
||||
renderVerificationList();
|
||||
updateStats();
|
||||
|
||||
showToast(`"${sections[index].name}" 섹션이 검증되었습니다.`, 'success');
|
||||
|
||||
// 모두 검증되면 버튼 활성화
|
||||
checkAllVerified();
|
||||
}
|
||||
|
||||
// 섹션 잠금 토글
|
||||
function toggleLock(index) {
|
||||
const checkbox = document.getElementById(`lock-${index}`);
|
||||
sections[index].locked = checkbox.checked;
|
||||
|
||||
updateStats();
|
||||
|
||||
if (checkbox.checked) {
|
||||
showToast('섹션이 잠겼습니다. 더 이상 편집할 수 없습니다.', 'info');
|
||||
} else {
|
||||
showToast('섹션 잠금이 해제되었습니다.', 'info');
|
||||
}
|
||||
}
|
||||
|
||||
// 통계 업데이트
|
||||
function updateStats() {
|
||||
const verifiedCount = sections.filter(s => s.verified).length;
|
||||
const totalCount = sections.length;
|
||||
const lockedCount = sections.filter(s => s.locked).length;
|
||||
const progress = totalCount > 0 ? Math.round((verifiedCount / totalCount) * 100) : 0;
|
||||
|
||||
document.getElementById('verifiedCount').textContent = verifiedCount;
|
||||
document.getElementById('totalCount').textContent = totalCount;
|
||||
document.getElementById('lockedCount').textContent = lockedCount;
|
||||
document.getElementById('totalProgress').textContent = `${progress}%`;
|
||||
document.getElementById('progressText').textContent = `${progress}%`;
|
||||
|
||||
// 진행률 원 업데이트
|
||||
const circle = document.getElementById('progressCircle');
|
||||
const circumference = 2 * Math.PI * 52;
|
||||
const offset = circumference - (progress / 100) * circumference;
|
||||
circle.style.strokeDashoffset = offset;
|
||||
}
|
||||
|
||||
// 참석자별 진행률
|
||||
function renderParticipantProgress() {
|
||||
const users = getFromStorage(STORAGE_KEYS.USERS) || [];
|
||||
const meetingData = JSON.parse(sessionStorage.getItem('newMeeting') || '{}');
|
||||
|
||||
if (meetingData.participants) {
|
||||
const progressContainer = document.getElementById('participantProgress');
|
||||
|
||||
progressContainer.innerHTML = meetingData.participants.map(email => {
|
||||
const user = users.find(u => u.email === email) || { name: email, avatar: '👤', id: 'unknown' };
|
||||
const userVerifications = sections.filter(s => s.verifier === user.id).length;
|
||||
const userProgress = sections.length > 0 ? Math.round((userVerifications / sections.length) * 100) : 0;
|
||||
|
||||
return `
|
||||
<div class="participant-progress-item">
|
||||
<div class="participant-avatar-small">${user.avatar}</div>
|
||||
<div class="progress-info">
|
||||
<div class="progress-name">${user.name}</div>
|
||||
<div class="progress-bar-small">
|
||||
<div class="progress-fill" style="width: ${userProgress}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="progress-percentage">${userProgress}%</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
}
|
||||
|
||||
// 모든 섹션 검증 확인
|
||||
function checkAllVerified() {
|
||||
const requiredSections = sections.filter(s => s.required);
|
||||
const allRequiredVerified = requiredSections.every(s => s.verified);
|
||||
|
||||
const completeBtn = document.getElementById('completeVerification');
|
||||
completeBtn.disabled = !allRequiredVerified;
|
||||
|
||||
if (allRequiredVerified) {
|
||||
showToast('모든 필수 섹션이 검증되었습니다!', 'success');
|
||||
}
|
||||
}
|
||||
|
||||
// 검증 완료 및 회의 종료
|
||||
document.getElementById('completeVerification').addEventListener('click', () => {
|
||||
const allVerified = sections.every(s => !s.required || s.verified);
|
||||
|
||||
if (!allVerified) {
|
||||
showToast('모든 필수 섹션을 검증해주세요.', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
if (confirm('검증을 완료하고 회의를 종료하시겠습니까?')) {
|
||||
// 검증 데이터 저장
|
||||
sessionStorage.setItem('verificationData', JSON.stringify(sections));
|
||||
|
||||
showToast('검증이 완료되었습니다.', 'success');
|
||||
|
||||
setTimeout(() => {
|
||||
navigateTo('07-회의종료.html');
|
||||
}, 1000);
|
||||
}
|
||||
});
|
||||
|
||||
// 초기화
|
||||
loadSections();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
573
design/uiux/prototype/07-회의종료.html
Normal file
573
design/uiux/prototype/07-회의종료.html
Normal file
@ -0,0 +1,573 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>회의 종료 - 회의록 작성 및 공유 서비스</title>
|
||||
<link rel="stylesheet" href="common.css">
|
||||
<style>
|
||||
.completion-container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: var(--spacing-8);
|
||||
}
|
||||
|
||||
.completion-header {
|
||||
text-align: center;
|
||||
margin-bottom: var(--spacing-8);
|
||||
}
|
||||
|
||||
.success-icon {
|
||||
font-size: 80px;
|
||||
margin-bottom: var(--spacing-4);
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: var(--spacing-6);
|
||||
margin-bottom: var(--spacing-8);
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: white;
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--spacing-6);
|
||||
box-shadow: var(--shadow-sm);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
font-size: 32px;
|
||||
margin-bottom: var(--spacing-3);
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
color: var(--gray-900);
|
||||
margin-bottom: var(--spacing-2);
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 0.875rem;
|
||||
color: var(--gray-500);
|
||||
}
|
||||
|
||||
.content-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: var(--spacing-6);
|
||||
margin-bottom: var(--spacing-8);
|
||||
}
|
||||
|
||||
.content-panel {
|
||||
background: white;
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--spacing-6);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.panel-title {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
color: var(--gray-900);
|
||||
margin-bottom: var(--spacing-4);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-2);
|
||||
}
|
||||
|
||||
.checklist-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-3);
|
||||
padding: var(--spacing-3);
|
||||
margin-bottom: var(--spacing-2);
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--gray-50);
|
||||
}
|
||||
|
||||
.checklist-icon {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.checklist-text {
|
||||
flex: 1;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.checklist-action {
|
||||
font-size: 0.875rem;
|
||||
color: var(--primary-main);
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.todo-item {
|
||||
padding: var(--spacing-4);
|
||||
background: var(--gray-50);
|
||||
border-radius: var(--radius-md);
|
||||
margin-bottom: var(--spacing-3);
|
||||
border-left: 4px solid var(--primary-main);
|
||||
}
|
||||
|
||||
.todo-content {
|
||||
font-weight: 500;
|
||||
color: var(--gray-900);
|
||||
margin-bottom: var(--spacing-2);
|
||||
}
|
||||
|
||||
.todo-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-4);
|
||||
font-size: 0.875rem;
|
||||
color: var(--gray-600);
|
||||
}
|
||||
|
||||
.todo-meta-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-1);
|
||||
}
|
||||
|
||||
.speaker-stats {
|
||||
margin-top: var(--spacing-4);
|
||||
}
|
||||
|
||||
.speaker-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-3);
|
||||
padding: var(--spacing-3);
|
||||
margin-bottom: var(--spacing-2);
|
||||
}
|
||||
|
||||
.speaker-avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
background: var(--gray-200);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.speaker-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.speaker-name {
|
||||
font-weight: 500;
|
||||
color: var(--gray-900);
|
||||
margin-bottom: var(--spacing-1);
|
||||
}
|
||||
|
||||
.speaker-bar {
|
||||
height: 6px;
|
||||
background: var(--gray-200);
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.speaker-fill {
|
||||
height: 100%;
|
||||
background: var(--primary-main);
|
||||
transition: width 0.3s;
|
||||
}
|
||||
|
||||
.speaker-count {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: var(--gray-600);
|
||||
}
|
||||
|
||||
.keyword-cloud {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--spacing-2);
|
||||
margin-top: var(--spacing-4);
|
||||
}
|
||||
|
||||
.keyword-tag {
|
||||
padding: var(--spacing-2) var(--spacing-4);
|
||||
background: var(--primary-light);
|
||||
color: var(--primary-dark);
|
||||
border-radius: var(--radius-full);
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.next-meeting {
|
||||
background: linear-gradient(135deg, var(--primary-main), var(--secondary-main));
|
||||
color: white;
|
||||
padding: var(--spacing-6);
|
||||
border-radius: var(--radius-lg);
|
||||
margin-top: var(--spacing-4);
|
||||
}
|
||||
|
||||
.next-meeting-title {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: var(--spacing-2);
|
||||
}
|
||||
|
||||
.next-meeting-date {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: var(--spacing-3);
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-top: var(--spacing-8);
|
||||
padding-top: var(--spacing-6);
|
||||
border-top: 1px solid var(--gray-200);
|
||||
}
|
||||
|
||||
@media (max-width: 1023px) {
|
||||
.stats-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.content-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="completion-container">
|
||||
<div class="completion-header">
|
||||
<div class="success-icon">🎉</div>
|
||||
<h1>회의가 종료되었습니다</h1>
|
||||
<p style="color: var(--gray-500); font-size: 1.125rem;">
|
||||
회의 통계와 Todo를 확인하고 최종 확정하세요
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 통계 카드 -->
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon">⏱️</div>
|
||||
<div class="stat-value" id="meetingDuration">-</div>
|
||||
<div class="stat-label">회의 총 시간</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon">👥</div>
|
||||
<div class="stat-value" id="participantCount">-</div>
|
||||
<div class="stat-label">참석자 수</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon">💬</div>
|
||||
<div class="stat-value" id="speechCount">-</div>
|
||||
<div class="stat-label">총 발언 횟수</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon">✅</div>
|
||||
<div class="stat-value" id="todoCount">-</div>
|
||||
<div class="stat-label">생성된 Todo</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 콘텐츠 그리드 -->
|
||||
<div class="content-grid">
|
||||
<!-- 필수 항목 검사 -->
|
||||
<div class="content-panel">
|
||||
<div class="panel-title">
|
||||
<span>📋</span>
|
||||
<span>필수 항목 검사</span>
|
||||
</div>
|
||||
<div id="checklistItems"></div>
|
||||
</div>
|
||||
|
||||
<!-- AI Todo 추출 -->
|
||||
<div class="content-panel">
|
||||
<div class="panel-title">
|
||||
<span>✨</span>
|
||||
<span>AI Todo 추출 결과</span>
|
||||
</div>
|
||||
<div id="todoList"></div>
|
||||
<button class="btn btn-secondary btn-sm" style="width: 100%; margin-top: var(--spacing-3);" onclick="openModal('editTodoModal')">
|
||||
Todo 수정
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 발언 통계 -->
|
||||
<div class="content-panel">
|
||||
<div class="panel-title">
|
||||
<span>📊</span>
|
||||
<span>화자별 발언 통계</span>
|
||||
</div>
|
||||
<div class="speaker-stats" id="speakerStats"></div>
|
||||
</div>
|
||||
|
||||
<!-- 주요 키워드 -->
|
||||
<div class="content-panel">
|
||||
<div class="panel-title">
|
||||
<span>🔑</span>
|
||||
<span>주요 키워드</span>
|
||||
</div>
|
||||
<div class="keyword-cloud" id="keywordCloud"></div>
|
||||
|
||||
<!-- 다음 회의 일정 감지 -->
|
||||
<div class="next-meeting">
|
||||
<div class="next-meeting-title">🗓️ AI가 감지한 다음 회의</div>
|
||||
<div class="next-meeting-date">2025년 10월 27일 14:00</div>
|
||||
<button class="btn" style="background: white; color: var(--primary-main);" onclick="addToCalendar()">
|
||||
📅 캘린더에 추가
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 액션 버튼 -->
|
||||
<div class="action-buttons">
|
||||
<button type="button" class="btn btn-text" onclick="navigateTo('05-회의진행.html')">
|
||||
← 회의록으로 돌아가기
|
||||
</button>
|
||||
<div style="display: flex; gap: var(--spacing-3);">
|
||||
<button type="button" class="btn btn-secondary" id="saveDraft">
|
||||
나중에 확정
|
||||
</button>
|
||||
<button type="button" class="btn btn-primary" id="finalizeMinutes">
|
||||
최종 확정 →
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Todo 수정 모달 -->
|
||||
<div class="modal-backdrop" id="editTodoModal">
|
||||
<div class="modal">
|
||||
<div class="modal-header">
|
||||
<h3 class="modal-title">Todo 수정</h3>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p style="color: var(--gray-600); margin-bottom: var(--spacing-4);">
|
||||
AI가 추출한 Todo를 수정하거나 새로운 Todo를 추가할 수 있습니다.
|
||||
</p>
|
||||
<div id="editableTodoList"></div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-text" onclick="closeModal('editTodoModal')">취소</button>
|
||||
<button class="btn btn-primary" onclick="saveTodos()">저장</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="common.js"></script>
|
||||
<script>
|
||||
let extractedTodos = [];
|
||||
|
||||
// 회의 통계 로드
|
||||
function loadMeetingStats() {
|
||||
const duration = parseInt(sessionStorage.getItem('meetingDuration') || '0');
|
||||
const meetingData = JSON.parse(sessionStorage.getItem('newMeeting') || '{}');
|
||||
const template = JSON.parse(sessionStorage.getItem('selectedTemplate') || '{}');
|
||||
|
||||
// 회의 시간
|
||||
document.getElementById('meetingDuration').textContent = formatDuration(duration / 60);
|
||||
|
||||
// 참석자 수
|
||||
const participantCount = meetingData.participants?.length || 0;
|
||||
document.getElementById('participantCount').textContent = `${participantCount}명`;
|
||||
|
||||
// 발언 횟수 (시뮬레이션)
|
||||
const totalSpeech = 45 + Math.floor(Math.random() * 20);
|
||||
document.getElementById('speechCount').textContent = `${totalSpeech}회`;
|
||||
|
||||
// Todo 개수
|
||||
extractedTodos = generateTodos();
|
||||
document.getElementById('todoCount').textContent = `${extractedTodos.length}개`;
|
||||
}
|
||||
|
||||
// 필수 항목 체크리스트
|
||||
function renderChecklist() {
|
||||
const template = JSON.parse(sessionStorage.getItem('selectedTemplate') || '{}');
|
||||
const content = JSON.parse(sessionStorage.getItem('meetingContent') || '{}');
|
||||
|
||||
const requiredSections = template.sections?.filter(s => s.required) || [];
|
||||
const checklist = requiredSections.map(section => {
|
||||
const hasContent = content[section.id] && content[section.id].trim().length > 0;
|
||||
return {
|
||||
name: section.name,
|
||||
completed: hasContent,
|
||||
sectionId: section.id
|
||||
};
|
||||
});
|
||||
|
||||
const container = document.getElementById('checklistItems');
|
||||
container.innerHTML = checklist.map(item => `
|
||||
<div class="checklist-item">
|
||||
<div class="checklist-icon">${item.completed ? '✅' : '❌'}</div>
|
||||
<div class="checklist-text">${item.name}</div>
|
||||
${!item.completed ? `<span class="checklist-action" onclick="goToSection('${item.sectionId}')">작성하기</span>` : ''}
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
// 모두 완료되었는지 확인
|
||||
const allCompleted = checklist.every(item => item.completed);
|
||||
document.getElementById('finalizeMinutes').disabled = !allCompleted;
|
||||
|
||||
return allCompleted;
|
||||
}
|
||||
|
||||
// Todo 생성 (AI 시뮬레이션)
|
||||
function generateTodos() {
|
||||
return [
|
||||
{
|
||||
id: generateId('todo'),
|
||||
content: '1분기 마케팅 예산안 작성',
|
||||
assignee: 'user2',
|
||||
dueDate: '2025-10-25',
|
||||
priority: 'high',
|
||||
source: '결정 사항 섹션'
|
||||
},
|
||||
{
|
||||
id: generateId('todo'),
|
||||
content: '경쟁사 분석 보고서 작성',
|
||||
assignee: 'user3',
|
||||
dueDate: '2025-10-22',
|
||||
priority: 'high',
|
||||
source: '논의 내용 섹션'
|
||||
},
|
||||
{
|
||||
id: generateId('todo'),
|
||||
content: '신규 프로젝트 팀 구성안 제출',
|
||||
assignee: 'user1',
|
||||
dueDate: '2025-10-23',
|
||||
priority: 'normal',
|
||||
source: '결정 사항 섹션'
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
// Todo 렌더링
|
||||
function renderTodos() {
|
||||
const container = document.getElementById('todoList');
|
||||
container.innerHTML = extractedTodos.map(todo => `
|
||||
<div class="todo-item">
|
||||
<div class="todo-content">${todo.content}</div>
|
||||
<div class="todo-meta">
|
||||
<div class="todo-meta-item">
|
||||
<span>👤</span>
|
||||
<span>${getUserName(todo.assignee)}</span>
|
||||
</div>
|
||||
<div class="todo-meta-item">
|
||||
<span>📅</span>
|
||||
<span>${getDdayText(todo.dueDate)}</span>
|
||||
</div>
|
||||
<div class="todo-meta-item">
|
||||
<span style="display: inline-block; width: 8px; height: 8px; border-radius: 50%; background: ${todo.priority === 'high' ? 'var(--error-main)' : 'var(--gray-400)'};"></span>
|
||||
<span>${todo.priority === 'high' ? '높음' : '보통'}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
// 화자별 통계
|
||||
function renderSpeakerStats() {
|
||||
const meetingData = JSON.parse(sessionStorage.getItem('newMeeting') || '{}');
|
||||
const speakers = (meetingData.participants || []).map(email => {
|
||||
const user = getUserById(email) || { name: email, avatar: '👤' };
|
||||
const count = 10 + Math.floor(Math.random() * 20);
|
||||
return { ...user, count };
|
||||
});
|
||||
|
||||
const maxCount = Math.max(...speakers.map(s => s.count));
|
||||
const container = document.getElementById('speakerStats');
|
||||
|
||||
container.innerHTML = speakers.map(speaker => `
|
||||
<div class="speaker-item">
|
||||
<div class="speaker-avatar">${speaker.avatar}</div>
|
||||
<div class="speaker-info">
|
||||
<div class="speaker-name">${speaker.name}</div>
|
||||
<div class="speaker-bar">
|
||||
<div class="speaker-fill" style="width: ${(speaker.count / maxCount) * 100}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="speaker-count">${speaker.count}회</div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
// 키워드 렌더링
|
||||
function renderKeywords() {
|
||||
const keywords = ['1분기 목표', '마케팅', '예산', 'ROI', '경쟁사 분석', '프로젝트', '전략', '실적'];
|
||||
const container = document.getElementById('keywordCloud');
|
||||
|
||||
container.innerHTML = keywords.map(keyword => `
|
||||
<div class="keyword-tag">${keyword}</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
// 섹션으로 이동
|
||||
function goToSection(sectionId) {
|
||||
sessionStorage.setItem('focusSection', sectionId);
|
||||
navigateTo('05-회의진행.html');
|
||||
}
|
||||
|
||||
// Todo 저장
|
||||
function saveTodos() {
|
||||
showToast('Todo가 저장되었습니다.', 'success');
|
||||
closeModal('editTodoModal');
|
||||
}
|
||||
|
||||
// 캘린더에 추가
|
||||
function addToCalendar() {
|
||||
showToast('다음 회의 일정이 캘린더에 추가되었습니다.', 'success');
|
||||
}
|
||||
|
||||
// 나중에 확정
|
||||
document.getElementById('saveDraft').addEventListener('click', () => {
|
||||
if (confirm('나중에 확정하시겠습니까?')) {
|
||||
showToast('임시 저장되었습니다.', 'info');
|
||||
setTimeout(() => {
|
||||
navigateTo('02-대시보드.html');
|
||||
}, 1000);
|
||||
}
|
||||
});
|
||||
|
||||
// 최종 확정
|
||||
document.getElementById('finalizeMinutes').addEventListener('click', () => {
|
||||
const allCompleted = renderChecklist();
|
||||
|
||||
if (!allCompleted) {
|
||||
showToast('모든 필수 항목을 작성해주세요.', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
if (confirm('회의록을 최종 확정하시겠습니까?\n확정 후에는 수정이 제한됩니다.')) {
|
||||
// Todo 저장
|
||||
extractedTodos.forEach(todo => {
|
||||
todo.meetingId = 'meeting_final';
|
||||
todo.status = 'pending';
|
||||
todo.progress = 0;
|
||||
saveTodo(todo);
|
||||
});
|
||||
|
||||
showToast('회의록이 최종 확정되었습니다.', 'success');
|
||||
|
||||
setTimeout(() => {
|
||||
navigateTo('08-회의록공유.html');
|
||||
}, 1000);
|
||||
}
|
||||
});
|
||||
|
||||
// 초기화
|
||||
loadMeetingStats();
|
||||
renderChecklist();
|
||||
renderTodos();
|
||||
renderSpeakerStats();
|
||||
renderKeywords();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
610
design/uiux/prototype/08-회의록공유.html
Normal file
610
design/uiux/prototype/08-회의록공유.html
Normal file
@ -0,0 +1,610 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>회의록 공유 - 회의록 작성 및 공유 서비스</title>
|
||||
<link rel="stylesheet" href="common.css">
|
||||
<style>
|
||||
.share-container {
|
||||
max-width: 1000px;
|
||||
margin: 0 auto;
|
||||
padding: var(--spacing-8);
|
||||
}
|
||||
|
||||
.share-header {
|
||||
text-align: center;
|
||||
margin-bottom: var(--spacing-8);
|
||||
}
|
||||
|
||||
.share-icon {
|
||||
font-size: 64px;
|
||||
margin-bottom: var(--spacing-4);
|
||||
}
|
||||
|
||||
.share-form {
|
||||
background: white;
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--spacing-8);
|
||||
box-shadow: var(--shadow-sm);
|
||||
margin-bottom: var(--spacing-6);
|
||||
}
|
||||
|
||||
.form-section {
|
||||
margin-bottom: var(--spacing-6);
|
||||
padding-bottom: var(--spacing-6);
|
||||
border-bottom: 1px solid var(--gray-200);
|
||||
}
|
||||
|
||||
.form-section:last-child {
|
||||
border-bottom: none;
|
||||
margin-bottom: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
color: var(--gray-900);
|
||||
margin-bottom: var(--spacing-4);
|
||||
}
|
||||
|
||||
.radio-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-3);
|
||||
}
|
||||
|
||||
.radio-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-2);
|
||||
padding: var(--spacing-3);
|
||||
border: 1px solid var(--gray-200);
|
||||
border-radius: var(--radius-md);
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.radio-item:hover {
|
||||
background: var(--gray-50);
|
||||
}
|
||||
|
||||
.radio-item input[type="radio"] {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.radio-item.selected {
|
||||
border-color: var(--primary-main);
|
||||
background: rgba(0, 217, 177, 0.05);
|
||||
}
|
||||
|
||||
.recipient-selector {
|
||||
margin-top: var(--spacing-3);
|
||||
display: none;
|
||||
}
|
||||
|
||||
.recipient-selector.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.recipient-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--spacing-2);
|
||||
margin-top: var(--spacing-3);
|
||||
}
|
||||
|
||||
.recipient-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-2);
|
||||
padding: var(--spacing-2) var(--spacing-3);
|
||||
background: var(--primary-light);
|
||||
color: var(--primary-dark);
|
||||
border-radius: var(--radius-full);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.link-display {
|
||||
display: flex;
|
||||
gap: var(--spacing-3);
|
||||
margin-top: var(--spacing-3);
|
||||
}
|
||||
|
||||
.link-input {
|
||||
flex: 1;
|
||||
background: var(--gray-50);
|
||||
border: 1px solid var(--gray-300);
|
||||
padding: var(--spacing-3) var(--spacing-4);
|
||||
border-radius: var(--radius-md);
|
||||
font-family: monospace;
|
||||
font-size: 0.875rem;
|
||||
color: var(--gray-700);
|
||||
}
|
||||
|
||||
.advanced-settings {
|
||||
margin-top: var(--spacing-4);
|
||||
padding: var(--spacing-4);
|
||||
background: var(--gray-50);
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
|
||||
.advanced-settings-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.advanced-settings-content {
|
||||
margin-top: var(--spacing-4);
|
||||
display: none;
|
||||
}
|
||||
|
||||
.advanced-settings-content.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.share-history {
|
||||
background: white;
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--spacing-6);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.history-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-4);
|
||||
padding: var(--spacing-4);
|
||||
margin-bottom: var(--spacing-3);
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--gray-50);
|
||||
}
|
||||
|
||||
.history-time {
|
||||
font-size: 0.875rem;
|
||||
color: var(--gray-500);
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.history-method {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-1);
|
||||
padding: var(--spacing-1) var(--spacing-2);
|
||||
background: var(--primary-light);
|
||||
color: var(--primary-dark);
|
||||
border-radius: var(--radius-md);
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.history-recipients {
|
||||
flex: 1;
|
||||
font-size: 0.875rem;
|
||||
color: var(--gray-700);
|
||||
}
|
||||
|
||||
.history-status {
|
||||
font-size: 0.75rem;
|
||||
padding: var(--spacing-1) var(--spacing-2);
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
|
||||
.history-status.sent {
|
||||
background: var(--success-light);
|
||||
color: var(--success-dark);
|
||||
}
|
||||
|
||||
.history-status.read {
|
||||
background: var(--info-light);
|
||||
color: var(--info-dark);
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-top: var(--spacing-8);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="share-container">
|
||||
<div class="share-header">
|
||||
<div class="share-icon">📤</div>
|
||||
<h1>회의록 공유</h1>
|
||||
<p style="color: var(--gray-500); font-size: 1.125rem;">
|
||||
참석자에게 회의록을 공유하고 배포하세요
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form id="shareForm" class="share-form">
|
||||
<!-- 공유 대상 선택 -->
|
||||
<div class="form-section">
|
||||
<div class="section-header">공유 대상</div>
|
||||
<div class="radio-group">
|
||||
<label class="radio-item selected">
|
||||
<input type="radio" name="shareTarget" value="all" checked onchange="updateRecipientSelector()">
|
||||
<div>
|
||||
<div style="font-weight: 500;">참석자 전체</div>
|
||||
<div style="font-size: 0.875rem; color: var(--gray-500);">회의에 참석한 모든 사람에게 공유합니다</div>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<label class="radio-item">
|
||||
<input type="radio" name="shareTarget" value="specific" onchange="updateRecipientSelector()">
|
||||
<div>
|
||||
<div style="font-weight: 500;">특정 참석자 선택</div>
|
||||
<div style="font-size: 0.875rem; color: var(--gray-500);">선택한 참석자에게만 공유합니다</div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="recipient-selector" id="recipientSelector">
|
||||
<div style="margin-top: var(--spacing-4); margin-bottom: var(--spacing-2); font-weight: 500;">참석자 선택</div>
|
||||
<div id="participantCheckboxes"></div>
|
||||
<div class="recipient-list" id="selectedRecipients"></div>
|
||||
</div>
|
||||
|
||||
<div class="input-group" style="margin-top: var(--spacing-4);">
|
||||
<label class="input-label">외부 공유 (선택)</label>
|
||||
<input type="email" id="externalEmail" class="input" placeholder="외부 이메일 주소">
|
||||
<button type="button" class="btn btn-secondary btn-sm mt-2" onclick="addExternalEmail()">
|
||||
+ 외부 이메일 추가
|
||||
</button>
|
||||
<div class="recipient-list" id="externalRecipients"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 공유 권한 설정 -->
|
||||
<div class="form-section">
|
||||
<div class="section-header">공유 권한</div>
|
||||
<div class="input-group">
|
||||
<select id="permission" class="select">
|
||||
<option value="read_only" selected>읽기 전용 (기본)</option>
|
||||
<option value="comment">댓글 가능</option>
|
||||
<option value="edit">편집 가능</option>
|
||||
</select>
|
||||
<small style="display: block; margin-top: var(--spacing-2); color: var(--gray-500);">
|
||||
권한에 따라 수신자가 회의록을 보거나 수정할 수 있는 범위가 결정됩니다
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 공유 방식 선택 -->
|
||||
<div class="form-section">
|
||||
<div class="section-header">공유 방식</div>
|
||||
<div class="checkbox-group">
|
||||
<div class="checkbox-item">
|
||||
<input type="checkbox" id="shareEmail" checked>
|
||||
<label for="shareEmail">
|
||||
<strong>이메일로 전송</strong>
|
||||
<div style="font-size: 0.875rem; color: var(--gray-500); margin-top: 2px;">수신자 이메일로 회의록 링크를 발송합니다</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="checkbox-item">
|
||||
<input type="checkbox" id="shareSlack">
|
||||
<label for="shareSlack">
|
||||
<strong>슬랙으로 전송</strong>
|
||||
<div style="font-size: 0.875rem; color: var(--gray-500); margin-top: 2px;">연동된 슬랙 채널에 알림을 발송합니다</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 공유 링크 설정 -->
|
||||
<div class="form-section">
|
||||
<div class="section-header">공유 링크</div>
|
||||
<div class="link-display">
|
||||
<input type="text" class="link-input" id="shareLink" value="https://meeting.example.com/share/abc123xyz" readonly>
|
||||
<button type="button" class="btn btn-secondary" onclick="copyLink()">
|
||||
📋 복사
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 고급 설정 -->
|
||||
<div class="advanced-settings">
|
||||
<div class="advanced-settings-header" onclick="toggleAdvancedSettings()">
|
||||
<span style="font-weight: 500;">고급 설정</span>
|
||||
<span id="advancedToggle">▼</span>
|
||||
</div>
|
||||
<div class="advanced-settings-content" id="advancedContent">
|
||||
<div class="input-group">
|
||||
<label class="input-label">링크 유효 기간</label>
|
||||
<select id="linkExpiration" class="select">
|
||||
<option value="none">무제한</option>
|
||||
<option value="7">7일</option>
|
||||
<option value="30" selected>30일</option>
|
||||
<option value="custom">직접 입력</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="input-group mt-4">
|
||||
<label class="input-label">비밀번호 설정 (선택)</label>
|
||||
<input type="password" id="linkPassword" class="input" placeholder="링크 접근 시 필요한 비밀번호">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- 공유 이력 -->
|
||||
<div class="share-history">
|
||||
<div class="section-header">공유 이력</div>
|
||||
<div id="shareHistoryList"></div>
|
||||
</div>
|
||||
|
||||
<!-- 액션 버튼 -->
|
||||
<div class="action-buttons">
|
||||
<button type="button" class="btn btn-text" onclick="navigateTo('07-회의종료.html')">
|
||||
← 뒤로
|
||||
</button>
|
||||
<button type="button" class="btn btn-primary" onclick="shareMinutes()">
|
||||
공유하기 →
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="common.js"></script>
|
||||
<script>
|
||||
let selectedParticipants = [];
|
||||
let externalEmails = [];
|
||||
|
||||
// 참석자 체크박스 렌더링
|
||||
function renderParticipantCheckboxes() {
|
||||
const meetingData = JSON.parse(sessionStorage.getItem('newMeeting') || '{}');
|
||||
const users = getFromStorage(STORAGE_KEYS.USERS) || [];
|
||||
|
||||
if (meetingData.participants) {
|
||||
const container = document.getElementById('participantCheckboxes');
|
||||
container.innerHTML = meetingData.participants.map(email => {
|
||||
const user = users.find(u => u.email === email) || { name: email, avatar: '👤', id: email };
|
||||
return `
|
||||
<div class="checkbox-item">
|
||||
<input type="checkbox" id="participant-${user.id}" value="${email}" onchange="updateSelectedParticipants()">
|
||||
<label for="participant-${user.id}" style="display: flex; align-items: center; gap: var(--spacing-2);">
|
||||
<span>${user.avatar}</span>
|
||||
<span>${user.name}</span>
|
||||
<span style="color: var(--gray-400);">(${email})</span>
|
||||
</label>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
}
|
||||
|
||||
// 라디오 버튼 스타일 업데이트
|
||||
document.querySelectorAll('input[name="shareTarget"]').forEach(radio => {
|
||||
radio.addEventListener('change', () => {
|
||||
document.querySelectorAll('.radio-item').forEach(item => {
|
||||
item.classList.remove('selected');
|
||||
});
|
||||
radio.closest('.radio-item').classList.add('selected');
|
||||
});
|
||||
});
|
||||
|
||||
// 수신자 선택기 표시/숨김
|
||||
function updateRecipientSelector() {
|
||||
const target = document.querySelector('input[name="shareTarget"]:checked').value;
|
||||
const selector = document.getElementById('recipientSelector');
|
||||
|
||||
if (target === 'specific') {
|
||||
selector.classList.add('active');
|
||||
} else {
|
||||
selector.classList.remove('active');
|
||||
// 모든 체크박스 해제
|
||||
document.querySelectorAll('#participantCheckboxes input[type="checkbox"]').forEach(cb => {
|
||||
cb.checked = false;
|
||||
});
|
||||
selectedParticipants = [];
|
||||
updateSelectedDisplay();
|
||||
}
|
||||
}
|
||||
|
||||
// 선택된 참석자 업데이트
|
||||
function updateSelectedParticipants() {
|
||||
selectedParticipants = Array.from(
|
||||
document.querySelectorAll('#participantCheckboxes input[type="checkbox"]:checked')
|
||||
).map(cb => cb.value);
|
||||
|
||||
updateSelectedDisplay();
|
||||
}
|
||||
|
||||
// 선택된 수신자 표시
|
||||
function updateSelectedDisplay() {
|
||||
const container = document.getElementById('selectedRecipients');
|
||||
|
||||
if (selectedParticipants.length === 0) {
|
||||
container.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = selectedParticipants.map(email => `
|
||||
<div class="recipient-chip">
|
||||
<span>${email}</span>
|
||||
<span style="cursor: pointer;" onclick="removeParticipant('${email}')">×</span>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
// 참석자 제거
|
||||
function removeParticipant(email) {
|
||||
const checkbox = document.querySelector(`#participantCheckboxes input[value="${email}"]`);
|
||||
if (checkbox) {
|
||||
checkbox.checked = false;
|
||||
}
|
||||
updateSelectedParticipants();
|
||||
}
|
||||
|
||||
// 외부 이메일 추가
|
||||
function addExternalEmail() {
|
||||
const input = document.getElementById('externalEmail');
|
||||
const email = input.value.trim();
|
||||
|
||||
if (!email) {
|
||||
showToast('이메일을 입력하세요.', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (!emailRegex.test(email)) {
|
||||
showToast('올바른 이메일 주소를 입력하세요.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (externalEmails.includes(email)) {
|
||||
showToast('이미 추가된 이메일입니다.', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
externalEmails.push(email);
|
||||
input.value = '';
|
||||
renderExternalEmails();
|
||||
}
|
||||
|
||||
// 외부 이메일 렌더링
|
||||
function renderExternalEmails() {
|
||||
const container = document.getElementById('externalRecipients');
|
||||
|
||||
container.innerHTML = externalEmails.map(email => `
|
||||
<div class="recipient-chip">
|
||||
<span>🌐 ${email}</span>
|
||||
<span style="cursor: pointer;" onclick="removeExternalEmail('${email}')">×</span>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
// 외부 이메일 제거
|
||||
function removeExternalEmail(email) {
|
||||
externalEmails = externalEmails.filter(e => e !== email);
|
||||
renderExternalEmails();
|
||||
}
|
||||
|
||||
// 링크 복사
|
||||
function copyLink() {
|
||||
const linkInput = document.getElementById('shareLink');
|
||||
linkInput.select();
|
||||
document.execCommand('copy');
|
||||
showToast('링크가 클립보드에 복사되었습니다.', 'success');
|
||||
}
|
||||
|
||||
// 고급 설정 토글
|
||||
function toggleAdvancedSettings() {
|
||||
const content = document.getElementById('advancedContent');
|
||||
const toggle = document.getElementById('advancedToggle');
|
||||
|
||||
if (content.classList.contains('active')) {
|
||||
content.classList.remove('active');
|
||||
toggle.textContent = '▼';
|
||||
} else {
|
||||
content.classList.add('active');
|
||||
toggle.textContent = '▲';
|
||||
}
|
||||
}
|
||||
|
||||
// 공유 이력 렌더링
|
||||
function renderShareHistory() {
|
||||
const history = [
|
||||
{
|
||||
time: '2025-10-18 15:30',
|
||||
method: '이메일',
|
||||
recipients: '김민준, 박서연, 이준호',
|
||||
status: 'read'
|
||||
},
|
||||
{
|
||||
time: '2025-10-18 15:30',
|
||||
method: '슬랙',
|
||||
recipients: '#전략팀',
|
||||
status: 'sent'
|
||||
}
|
||||
];
|
||||
|
||||
const container = document.getElementById('shareHistoryList');
|
||||
|
||||
if (history.length === 0) {
|
||||
container.innerHTML = '<div style="text-align: center; padding: var(--spacing-8); color: var(--gray-400);">아직 공유 이력이 없습니다</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = history.map(item => `
|
||||
<div class="history-item">
|
||||
<div class="history-time">${item.time}</div>
|
||||
<div class="history-method">${item.method}</div>
|
||||
<div class="history-recipients">${item.recipients}</div>
|
||||
<div class="history-status ${item.status}">
|
||||
${item.status === 'read' ? '읽음' : '발송 완료'}
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
// 회의록 공유
|
||||
function shareMinutes() {
|
||||
const shareTarget = document.querySelector('input[name="shareTarget"]:checked').value;
|
||||
const shareEmail = document.getElementById('shareEmail').checked;
|
||||
const shareSlack = document.getElementById('shareSlack').checked;
|
||||
|
||||
// 공유 대상 검증
|
||||
if (shareTarget === 'specific' && selectedParticipants.length === 0 && externalEmails.length === 0) {
|
||||
showToast('공유할 대상을 선택하세요.', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
// 공유 방식 검증
|
||||
if (!shareEmail && !shareSlack) {
|
||||
showToast('최소 하나의 공유 방식을 선택하세요.', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
// 공유 데이터 구성
|
||||
const shareData = {
|
||||
target: shareTarget,
|
||||
recipients: shareTarget === 'all' ? 'all' : selectedParticipants,
|
||||
externalRecipients: externalEmails,
|
||||
permission: document.getElementById('permission').value,
|
||||
methods: {
|
||||
email: shareEmail,
|
||||
slack: shareSlack
|
||||
},
|
||||
link: {
|
||||
url: document.getElementById('shareLink').value,
|
||||
expiration: document.getElementById('linkExpiration').value,
|
||||
password: document.getElementById('linkPassword').value
|
||||
},
|
||||
sharedAt: new Date().toISOString()
|
||||
};
|
||||
|
||||
// 공유 실행 (시뮬레이션)
|
||||
showToast('회의록을 공유하고 있습니다...', 'info');
|
||||
|
||||
setTimeout(() => {
|
||||
const recipientCount = shareTarget === 'all' ?
|
||||
(JSON.parse(sessionStorage.getItem('newMeeting') || '{}').participants?.length || 0) :
|
||||
(selectedParticipants.length + externalEmails.length);
|
||||
|
||||
if (shareEmail) {
|
||||
showToast(`${recipientCount}명에게 이메일이 발송되었습니다.`, 'success');
|
||||
}
|
||||
|
||||
if (shareSlack) {
|
||||
showToast('슬랙으로 알림이 발송되었습니다.', 'success');
|
||||
}
|
||||
|
||||
// Todo 관리 화면으로 이동
|
||||
setTimeout(() => {
|
||||
navigateTo('09-Todo관리.html');
|
||||
}, 1500);
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
// 초기화
|
||||
renderParticipantCheckboxes();
|
||||
renderShareHistory();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
749
design/uiux/prototype/09-Todo관리.html
Normal file
749
design/uiux/prototype/09-Todo관리.html
Normal file
@ -0,0 +1,749 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Todo 관리 - 회의록 작성 및 공유 서비스</title>
|
||||
<link rel="stylesheet" href="common.css">
|
||||
<style>
|
||||
.todo-container {
|
||||
max-width: 1800px;
|
||||
margin: 0 auto;
|
||||
padding: var(--spacing-8);
|
||||
}
|
||||
|
||||
.todo-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: var(--spacing-6);
|
||||
}
|
||||
|
||||
.view-toggle {
|
||||
display: flex;
|
||||
gap: var(--spacing-2);
|
||||
}
|
||||
|
||||
.view-btn {
|
||||
padding: var(--spacing-2) var(--spacing-4);
|
||||
border: 1px solid var(--gray-300);
|
||||
background: white;
|
||||
border-radius: var(--radius-md);
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.view-btn.active {
|
||||
background: var(--primary-main);
|
||||
color: white;
|
||||
border-color: var(--primary-main);
|
||||
}
|
||||
|
||||
.filter-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-4);
|
||||
padding: var(--spacing-4);
|
||||
background: white;
|
||||
border-radius: var(--radius-lg);
|
||||
margin-bottom: var(--spacing-6);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.filter-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-2);
|
||||
}
|
||||
|
||||
.filter-label {
|
||||
font-size: 0.875rem;
|
||||
color: var(--gray-600);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.filter-select {
|
||||
padding: var(--spacing-2) var(--spacing-3);
|
||||
border: 1px solid var(--gray-300);
|
||||
border-radius: var(--radius-md);
|
||||
font-size: 0.875rem;
|
||||
background: white;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* 칸반 보드 뷰 */
|
||||
.kanban-board {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: var(--spacing-6);
|
||||
}
|
||||
|
||||
.kanban-column {
|
||||
background: white;
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--spacing-4);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.column-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: var(--spacing-4);
|
||||
padding-bottom: var(--spacing-3);
|
||||
border-bottom: 2px solid var(--gray-200);
|
||||
}
|
||||
|
||||
.column-title {
|
||||
font-weight: 600;
|
||||
font-size: 1.125rem;
|
||||
color: var(--gray-900);
|
||||
}
|
||||
|
||||
.column-count {
|
||||
background: var(--gray-200);
|
||||
color: var(--gray-700);
|
||||
padding: var(--spacing-1) var(--spacing-2);
|
||||
border-radius: var(--radius-full);
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.todo-card {
|
||||
background: white;
|
||||
border: 1px solid var(--gray-200);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--spacing-4);
|
||||
margin-bottom: var(--spacing-3);
|
||||
cursor: move;
|
||||
transition: all var(--transition-fast);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.todo-card:hover {
|
||||
box-shadow: var(--shadow-md);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.todo-card.dragging {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.todo-card.high-priority {
|
||||
border-left: 4px solid var(--error-main);
|
||||
}
|
||||
|
||||
.todo-content-card {
|
||||
font-weight: 500;
|
||||
color: var(--gray-900);
|
||||
margin-bottom: var(--spacing-3);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.todo-meta-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-3);
|
||||
margin-bottom: var(--spacing-2);
|
||||
font-size: 0.875rem;
|
||||
color: var(--gray-600);
|
||||
}
|
||||
|
||||
.todo-assignee {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-1);
|
||||
}
|
||||
|
||||
.assignee-avatar {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
background: var(--gray-200);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.todo-due-date {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-1);
|
||||
}
|
||||
|
||||
.due-date.overdue {
|
||||
color: var(--error-main);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.due-date.today {
|
||||
color: var(--warning-main);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.progress-container {
|
||||
margin-top: var(--spacing-3);
|
||||
padding-top: var(--spacing-3);
|
||||
border-top: 1px solid var(--gray-200);
|
||||
}
|
||||
|
||||
.progress-label-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: var(--spacing-2);
|
||||
font-size: 0.75rem;
|
||||
color: var(--gray-500);
|
||||
}
|
||||
|
||||
.progress-bar-todo {
|
||||
height: 6px;
|
||||
background: var(--gray-200);
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.progress-fill-todo {
|
||||
height: 100%;
|
||||
background: var(--primary-main);
|
||||
transition: width 0.3s;
|
||||
}
|
||||
|
||||
.todo-actions {
|
||||
display: flex;
|
||||
gap: var(--spacing-2);
|
||||
margin-top: var(--spacing-3);
|
||||
}
|
||||
|
||||
.icon-btn-small {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: var(--radius-md);
|
||||
border: 1px solid var(--gray-300);
|
||||
background: white;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all var(--transition-fast);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.icon-btn-small:hover {
|
||||
background: var(--gray-50);
|
||||
border-color: var(--primary-main);
|
||||
}
|
||||
|
||||
.meeting-link {
|
||||
font-size: 0.75rem;
|
||||
color: var(--primary-main);
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* 리스트 뷰 */
|
||||
.list-view {
|
||||
background: white;
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--spacing-6);
|
||||
box-shadow: var(--shadow-sm);
|
||||
display: none;
|
||||
}
|
||||
|
||||
.list-view.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.todo-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.todo-table th {
|
||||
text-align: left;
|
||||
padding: var(--spacing-3) var(--spacing-4);
|
||||
background: var(--gray-50);
|
||||
font-weight: 600;
|
||||
color: var(--gray-700);
|
||||
font-size: 0.875rem;
|
||||
border-bottom: 2px solid var(--gray-200);
|
||||
}
|
||||
|
||||
.todo-table td {
|
||||
padding: var(--spacing-3) var(--spacing-4);
|
||||
border-bottom: 1px solid var(--gray-200);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.todo-table tr:hover {
|
||||
background: var(--gray-50);
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
display: inline-block;
|
||||
padding: var(--spacing-1) var(--spacing-2);
|
||||
border-radius: var(--radius-md);
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.status-badge.pending {
|
||||
background: var(--gray-200);
|
||||
color: var(--gray-700);
|
||||
}
|
||||
|
||||
.status-badge.in_progress {
|
||||
background: var(--info-light);
|
||||
color: var(--info-dark);
|
||||
}
|
||||
|
||||
.status-badge.completed {
|
||||
background: var(--success-light);
|
||||
color: var(--success-dark);
|
||||
}
|
||||
|
||||
.priority-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
margin-right: var(--spacing-1);
|
||||
}
|
||||
|
||||
.priority-dot.high {
|
||||
background: var(--error-main);
|
||||
}
|
||||
|
||||
.priority-dot.normal {
|
||||
background: var(--gray-400);
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.kanban-board {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="todo-container">
|
||||
<!-- 헤더 -->
|
||||
<div class="todo-header">
|
||||
<div>
|
||||
<h1>Todo 관리</h1>
|
||||
<p style="color: var(--gray-500);">회의에서 생성된 Todo를 추적하고 관리하세요</p>
|
||||
</div>
|
||||
|
||||
<div class="view-toggle">
|
||||
<button class="view-btn active" id="kanbanViewBtn" onclick="switchView('kanban')">
|
||||
📋 칸반 보드
|
||||
</button>
|
||||
<button class="view-btn" id="listViewBtn" onclick="switchView('list')">
|
||||
📄 리스트 뷰
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 필터 바 -->
|
||||
<div class="filter-bar">
|
||||
<div class="filter-group">
|
||||
<span class="filter-label">담당자:</span>
|
||||
<select class="filter-select" id="filterAssignee" onchange="applyFilters()">
|
||||
<option value="all">전체</option>
|
||||
<option value="me">나</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="filter-group">
|
||||
<span class="filter-label">상태:</span>
|
||||
<select class="filter-select" id="filterStatus" onchange="applyFilters()">
|
||||
<option value="all">전체</option>
|
||||
<option value="pending">시작 전</option>
|
||||
<option value="in_progress">진행 중</option>
|
||||
<option value="completed">완료</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="filter-group">
|
||||
<span class="filter-label">우선순위:</span>
|
||||
<select class="filter-select" id="filterPriority" onchange="applyFilters()">
|
||||
<option value="all">전체</option>
|
||||
<option value="high">높음</option>
|
||||
<option value="normal">보통</option>
|
||||
<option value="low">낮음</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="filter-group">
|
||||
<span class="filter-label">정렬:</span>
|
||||
<select class="filter-select" id="sortBy" onchange="applyFilters()">
|
||||
<option value="dueDate">마감일순</option>
|
||||
<option value="priority">우선순위순</option>
|
||||
<option value="created">생성일순</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 칸반 보드 뷰 -->
|
||||
<div class="kanban-board" id="kanbanView">
|
||||
<div class="kanban-column" data-status="pending">
|
||||
<div class="column-header">
|
||||
<div class="column-title">시작 전</div>
|
||||
<div class="column-count" id="pendingCount">0</div>
|
||||
</div>
|
||||
<div id="pendingColumn" class="column-content"></div>
|
||||
</div>
|
||||
|
||||
<div class="kanban-column" data-status="in_progress">
|
||||
<div class="column-header">
|
||||
<div class="column-title">진행 중</div>
|
||||
<div class="column-count" id="inProgressCount">0</div>
|
||||
</div>
|
||||
<div id="inProgressColumn" class="column-content"></div>
|
||||
</div>
|
||||
|
||||
<div class="kanban-column" data-status="completed">
|
||||
<div class="column-header">
|
||||
<div class="column-title">완료</div>
|
||||
<div class="column-count" id="completedCount">0</div>
|
||||
</div>
|
||||
<div id="completedColumn" class="column-content"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 리스트 뷰 -->
|
||||
<div class="list-view" id="listView">
|
||||
<table class="todo-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 40px;">
|
||||
<input type="checkbox" id="selectAll" onchange="toggleSelectAll()">
|
||||
</th>
|
||||
<th style="width: 100px;">상태</th>
|
||||
<th>내용</th>
|
||||
<th style="width: 120px;">담당자</th>
|
||||
<th style="width: 100px;">마감일</th>
|
||||
<th style="width: 80px;">우선순위</th>
|
||||
<th style="width: 100px;">진행률</th>
|
||||
<th style="width: 80px;">액션</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="todoTableBody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="fab" onclick="navigateTo('02-대시보드.html')" title="대시보드로 이동">
|
||||
🏠
|
||||
</button>
|
||||
|
||||
<script src="common.js"></script>
|
||||
<script>
|
||||
let todos = [];
|
||||
let filteredTodos = [];
|
||||
let draggedTodo = null;
|
||||
|
||||
// Todo 데이터 로드
|
||||
function loadTodos() {
|
||||
todos = getAllTodos();
|
||||
filteredTodos = todos;
|
||||
renderKanban();
|
||||
renderList();
|
||||
}
|
||||
|
||||
// 칸반 보드 렌더링
|
||||
function renderKanban() {
|
||||
const columns = {
|
||||
pending: document.getElementById('pendingColumn'),
|
||||
in_progress: document.getElementById('inProgressColumn'),
|
||||
completed: document.getElementById('completedColumn')
|
||||
};
|
||||
|
||||
// 초기화
|
||||
Object.values(columns).forEach(col => col.innerHTML = '');
|
||||
|
||||
// Todo 분류 및 렌더링
|
||||
const grouped = {
|
||||
pending: filteredTodos.filter(t => t.status === 'pending'),
|
||||
in_progress: filteredTodos.filter(t => t.status === 'in_progress'),
|
||||
completed: filteredTodos.filter(t => t.status === 'completed')
|
||||
};
|
||||
|
||||
Object.entries(grouped).forEach(([status, todoList]) => {
|
||||
columns[status].innerHTML = todoList.map(todo => renderTodoCard(todo)).join('');
|
||||
});
|
||||
|
||||
// 카운트 업데이트
|
||||
document.getElementById('pendingCount').textContent = grouped.pending.length;
|
||||
document.getElementById('inProgressCount').textContent = grouped.in_progress.length;
|
||||
document.getElementById('completedCount').textContent = grouped.completed.length;
|
||||
|
||||
// 드래그 이벤트 추가
|
||||
addDragEvents();
|
||||
}
|
||||
|
||||
// Todo 카드 렌더링
|
||||
function renderTodoCard(todo) {
|
||||
const user = getUserById(todo.assignee);
|
||||
const ddayText = getDdayText(todo.dueDate);
|
||||
const isOverdue = ddayText.includes('지남');
|
||||
const isToday = ddayText === '오늘';
|
||||
|
||||
return `
|
||||
<div class="todo-card ${todo.priority === 'high' ? 'high-priority' : ''}"
|
||||
draggable="true"
|
||||
data-id="${todo.id}">
|
||||
<div class="todo-content-card">${todo.content}</div>
|
||||
|
||||
<div class="todo-meta-row">
|
||||
<div class="todo-assignee">
|
||||
<div class="assignee-avatar">${user?.avatar || '👤'}</div>
|
||||
<span>${user?.name || '알 수 없음'}</span>
|
||||
</div>
|
||||
<div class="todo-due-date">
|
||||
<span>📅</span>
|
||||
<span class="due-date ${isOverdue ? 'overdue' : ''} ${isToday ? 'today' : ''}">
|
||||
${ddayText}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${todo.status !== 'completed' ? `
|
||||
<div class="progress-container">
|
||||
<div class="progress-label-row">
|
||||
<span>진행률</span>
|
||||
<span>${todo.progress}%</span>
|
||||
</div>
|
||||
<div class="progress-bar-todo">
|
||||
<div class="progress-fill-todo" style="width: ${todo.progress}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
<div class="todo-actions">
|
||||
<button class="icon-btn-small" title="상세" onclick="viewTodoDetail('${todo.id}')">👁️</button>
|
||||
<button class="icon-btn-small" title="수정" onclick="editTodo('${todo.id}')">✏️</button>
|
||||
${todo.meetingId ? `<span class="meeting-link" onclick="goToMeeting('${todo.meetingId}')">📋 회의록</span>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// 드래그 이벤트 추가
|
||||
function addDragEvents() {
|
||||
const cards = document.querySelectorAll('.todo-card');
|
||||
const columns = document.querySelectorAll('.column-content');
|
||||
|
||||
cards.forEach(card => {
|
||||
card.addEventListener('dragstart', handleDragStart);
|
||||
card.addEventListener('dragend', handleDragEnd);
|
||||
});
|
||||
|
||||
columns.forEach(column => {
|
||||
column.addEventListener('dragover', handleDragOver);
|
||||
column.addEventListener('drop', handleDrop);
|
||||
});
|
||||
}
|
||||
|
||||
function handleDragStart(e) {
|
||||
draggedTodo = e.target.dataset.id;
|
||||
e.target.classList.add('dragging');
|
||||
}
|
||||
|
||||
function handleDragEnd(e) {
|
||||
e.target.classList.remove('dragging');
|
||||
}
|
||||
|
||||
function handleDragOver(e) {
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
function handleDrop(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const newStatus = e.target.closest('.kanban-column').dataset.status;
|
||||
const todo = todos.find(t => t.id === draggedTodo);
|
||||
|
||||
if (todo && todo.status !== newStatus) {
|
||||
todo.status = newStatus;
|
||||
|
||||
if (newStatus === 'completed') {
|
||||
todo.progress = 100;
|
||||
} else if (newStatus === 'in_progress' && todo.progress === 0) {
|
||||
todo.progress = 10;
|
||||
}
|
||||
|
||||
saveTodo(todo);
|
||||
renderKanban();
|
||||
|
||||
showToast(`Todo 상태가 "${newStatus === 'pending' ? '시작 전' : newStatus === 'in_progress' ? '진행 중' : '완료'}"로 변경되었습니다.`, 'success');
|
||||
}
|
||||
|
||||
draggedTodo = null;
|
||||
}
|
||||
|
||||
// 리스트 뷰 렌더링
|
||||
function renderList() {
|
||||
const tbody = document.getElementById('todoTableBody');
|
||||
|
||||
if (filteredTodos.length === 0) {
|
||||
tbody.innerHTML = '<tr><td colspan="8" style="text-align: center; padding: var(--spacing-8); color: var(--gray-400);">Todo가 없습니다</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
tbody.innerHTML = filteredTodos.map(todo => {
|
||||
const user = getUserById(todo.assignee);
|
||||
const ddayText = getDdayText(todo.dueDate);
|
||||
|
||||
return `
|
||||
<tr>
|
||||
<td><input type="checkbox" class="todo-checkbox" data-id="${todo.id}"></td>
|
||||
<td>
|
||||
<select class="status-badge ${todo.status}" onchange="changeStatus('${todo.id}', this.value)">
|
||||
<option value="pending" ${todo.status === 'pending' ? 'selected' : ''}>시작 전</option>
|
||||
<option value="in_progress" ${todo.status === 'in_progress' ? 'selected' : ''}>진행 중</option>
|
||||
<option value="completed" ${todo.status === 'completed' ? 'selected' : ''}>완료</option>
|
||||
</select>
|
||||
</td>
|
||||
<td>${todo.content}</td>
|
||||
<td>
|
||||
<div style="display: flex; align-items: center; gap: var(--spacing-2);">
|
||||
<span>${user?.avatar || '👤'}</span>
|
||||
<span>${user?.name || '알 수 없음'}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>${ddayText}</td>
|
||||
<td>
|
||||
<span class="priority-dot ${todo.priority}"></span>
|
||||
${todo.priority === 'high' ? '높음' : todo.priority === 'low' ? '낮음' : '보통'}
|
||||
</td>
|
||||
<td>
|
||||
<div style="display: flex; align-items: center; gap: var(--spacing-2);">
|
||||
<div style="flex: 1; height: 6px; background: var(--gray-200); border-radius: 3px; overflow: hidden;">
|
||||
<div style="height: 100%; background: var(--primary-main); width: ${todo.progress}%;"></div>
|
||||
</div>
|
||||
<span style="font-size: 0.75rem;">${todo.progress}%</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<button class="icon-btn-small" title="수정" onclick="editTodo('${todo.id}')">✏️</button>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// 뷰 전환
|
||||
function switchView(view) {
|
||||
if (view === 'kanban') {
|
||||
document.getElementById('kanbanView').style.display = 'grid';
|
||||
document.getElementById('listView').classList.remove('active');
|
||||
document.getElementById('kanbanViewBtn').classList.add('active');
|
||||
document.getElementById('listViewBtn').classList.remove('active');
|
||||
} else {
|
||||
document.getElementById('kanbanView').style.display = 'none';
|
||||
document.getElementById('listView').classList.add('active');
|
||||
document.getElementById('kanbanViewBtn').classList.remove('active');
|
||||
document.getElementById('listViewBtn').classList.add('active');
|
||||
}
|
||||
}
|
||||
|
||||
// 필터 적용
|
||||
function applyFilters() {
|
||||
const assigneeFilter = document.getElementById('filterAssignee').value;
|
||||
const statusFilter = document.getElementById('filterStatus').value;
|
||||
const priorityFilter = document.getElementById('filterPriority').value;
|
||||
const sortBy = document.getElementById('sortBy').value;
|
||||
|
||||
const currentUser = getCurrentUser();
|
||||
|
||||
filteredTodos = todos.filter(todo => {
|
||||
if (assigneeFilter === 'me' && todo.assignee !== currentUser.id) return false;
|
||||
if (statusFilter !== 'all' && todo.status !== statusFilter) return false;
|
||||
if (priorityFilter !== 'all' && todo.priority !== priorityFilter) return false;
|
||||
return true;
|
||||
});
|
||||
|
||||
// 정렬
|
||||
filteredTodos.sort((a, b) => {
|
||||
if (sortBy === 'dueDate') {
|
||||
return new Date(a.dueDate) - new Date(b.dueDate);
|
||||
} else if (sortBy === 'priority') {
|
||||
const priorityOrder = { high: 0, normal: 1, low: 2 };
|
||||
return priorityOrder[a.priority] - priorityOrder[b.priority];
|
||||
} else {
|
||||
return new Date(b.createdAt || 0) - new Date(a.createdAt || 0);
|
||||
}
|
||||
});
|
||||
|
||||
renderKanban();
|
||||
renderList();
|
||||
}
|
||||
|
||||
// 상태 변경
|
||||
function changeStatus(todoId, newStatus) {
|
||||
const todo = todos.find(t => t.id === todoId);
|
||||
if (todo) {
|
||||
todo.status = newStatus;
|
||||
|
||||
if (newStatus === 'completed') {
|
||||
todo.progress = 100;
|
||||
}
|
||||
|
||||
saveTodo(todo);
|
||||
applyFilters();
|
||||
showToast('상태가 변경되었습니다.', 'success');
|
||||
}
|
||||
}
|
||||
|
||||
// Todo 수정
|
||||
function editTodo(todoId) {
|
||||
const todo = todos.find(t => t.id === todoId);
|
||||
if (todo) {
|
||||
const newProgress = prompt(`진행률을 입력하세요 (0-100):`, todo.progress);
|
||||
if (newProgress !== null) {
|
||||
const progress = parseInt(newProgress);
|
||||
if (!isNaN(progress) && progress >= 0 && progress <= 100) {
|
||||
todo.progress = progress;
|
||||
|
||||
if (progress === 100) {
|
||||
todo.status = 'completed';
|
||||
} else if (progress > 0 && todo.status === 'pending') {
|
||||
todo.status = 'in_progress';
|
||||
}
|
||||
|
||||
saveTodo(todo);
|
||||
applyFilters();
|
||||
showToast('진행률이 업데이트되었습니다.', 'success');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Todo 상세 보기
|
||||
function viewTodoDetail(todoId) {
|
||||
const todo = todos.find(t => t.id === todoId);
|
||||
if (todo) {
|
||||
alert(`Todo 상세\n\n내용: ${todo.content}\n담당자: ${getUserName(todo.assignee)}\n마감일: ${todo.dueDate}\n우선순위: ${todo.priority}\n진행률: ${todo.progress}%\n상태: ${todo.status}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 회의록으로 이동
|
||||
function goToMeeting(meetingId) {
|
||||
alert('회의록 화면으로 이동합니다.');
|
||||
}
|
||||
|
||||
// 전체 선택
|
||||
function toggleSelectAll() {
|
||||
const selectAll = document.getElementById('selectAll').checked;
|
||||
document.querySelectorAll('.todo-checkbox').forEach(cb => {
|
||||
cb.checked = selectAll;
|
||||
});
|
||||
}
|
||||
|
||||
// 초기화
|
||||
loadTodos();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
596
design/uiux/prototype/common.css
Normal file
596
design/uiux/prototype/common.css
Normal file
@ -0,0 +1,596 @@
|
||||
/* ============================================
|
||||
회의록 작성 및 공유 개선 서비스 - 공통 스타일
|
||||
============================================ */
|
||||
|
||||
/* === CSS Reset === */
|
||||
*, *::before, *::after {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
html {
|
||||
font-size: 16px;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Pretendard', 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Apple SD Gothic Neo', sans-serif;
|
||||
line-height: 1.5;
|
||||
color: #4B5563;
|
||||
background-color: #F9FAFB;
|
||||
}
|
||||
|
||||
/* === CSS Variables === */
|
||||
:root {
|
||||
/* Primary Colors */
|
||||
--primary-light: #4DFFDB;
|
||||
--primary-main: #00D9B1;
|
||||
--primary-dark: #00A88A;
|
||||
|
||||
/* Secondary Colors */
|
||||
--secondary-light: #A5B4FC;
|
||||
--secondary-main: #6366F1;
|
||||
--secondary-dark: #4F46E5;
|
||||
|
||||
/* Semantic Colors */
|
||||
--success-light: #6EE7B7;
|
||||
--success-main: #10B981;
|
||||
--success-dark: #059669;
|
||||
|
||||
--warning-light: #FCD34D;
|
||||
--warning-main: #F59E0B;
|
||||
--warning-dark: #D97706;
|
||||
|
||||
--error-light: #FCA5A5;
|
||||
--error-main: #EF4444;
|
||||
--error-dark: #DC2626;
|
||||
|
||||
--info-light: #93C5FD;
|
||||
--info-main: #3B82F6;
|
||||
--info-dark: #2563EB;
|
||||
|
||||
/* Neutral Colors */
|
||||
--gray-50: #F9FAFB;
|
||||
--gray-100: #F3F4F6;
|
||||
--gray-200: #E5E7EB;
|
||||
--gray-300: #D1D5DB;
|
||||
--gray-400: #9CA3AF;
|
||||
--gray-500: #6B7280;
|
||||
--gray-600: #4B5563;
|
||||
--gray-700: #374151;
|
||||
--gray-800: #1F2937;
|
||||
--gray-900: #111827;
|
||||
|
||||
/* Typography */
|
||||
--font-display: 700 3rem/1.25 'Pretendard';
|
||||
--font-h1: 700 2.25rem/1.25 'Pretendard';
|
||||
--font-h2: 600 1.875rem/1.25 'Pretendard';
|
||||
--font-h3: 600 1.5rem/1.5 'Pretendard';
|
||||
--font-h4: 600 1.25rem/1.5 'Pretendard';
|
||||
--font-body-large: 400 1.125rem/1.5 'Pretendard';
|
||||
--font-body: 400 1rem/1.5 'Pretendard';
|
||||
--font-body-small: 400 0.875rem/1.5 'Pretendard';
|
||||
--font-caption: 400 0.75rem/1.5 'Pretendard';
|
||||
|
||||
/* Spacing (8px 기반) */
|
||||
--spacing-1: 4px;
|
||||
--spacing-2: 8px;
|
||||
--spacing-3: 12px;
|
||||
--spacing-4: 16px;
|
||||
--spacing-5: 20px;
|
||||
--spacing-6: 24px;
|
||||
--spacing-8: 32px;
|
||||
--spacing-10: 40px;
|
||||
--spacing-12: 48px;
|
||||
--spacing-16: 64px;
|
||||
|
||||
/* Border Radius */
|
||||
--radius-sm: 4px;
|
||||
--radius-md: 8px;
|
||||
--radius-lg: 12px;
|
||||
--radius-xl: 16px;
|
||||
--radius-full: 9999px;
|
||||
|
||||
/* Shadows */
|
||||
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
--shadow-lg: 0 20px 25px rgba(0, 0, 0, 0.15);
|
||||
|
||||
/* Transitions */
|
||||
--transition-fast: 150ms ease-out;
|
||||
--transition-base: 200ms ease-out;
|
||||
--transition-slow: 300ms ease-out;
|
||||
}
|
||||
|
||||
/* === Typography === */
|
||||
h1 { font: var(--font-h1); color: var(--gray-900); }
|
||||
h2 { font: var(--font-h2); color: var(--gray-900); }
|
||||
h3 { font: var(--font-h3); color: var(--gray-900); }
|
||||
h4 { font: var(--font-h4); color: var(--gray-900); }
|
||||
p { font: var(--font-body); margin-bottom: var(--spacing-4); }
|
||||
small { font: var(--font-caption); color: var(--gray-500); }
|
||||
|
||||
/* === Buttons === */
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: var(--spacing-3) var(--spacing-6);
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
border: none;
|
||||
border-radius: var(--radius-md);
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-fast);
|
||||
text-decoration: none;
|
||||
gap: var(--spacing-2);
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: var(--primary-main);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background-color: var(--primary-light);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.btn-primary:active {
|
||||
background-color: var(--primary-dark);
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.btn-primary:disabled {
|
||||
background-color: var(--gray-300);
|
||||
color: var(--gray-500);
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background-color: transparent;
|
||||
color: var(--primary-main);
|
||||
border: 1px solid var(--primary-main);
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background-color: rgba(0, 217, 177, 0.1);
|
||||
}
|
||||
|
||||
.btn-secondary:active {
|
||||
background-color: rgba(0, 217, 177, 0.2);
|
||||
}
|
||||
|
||||
.btn-text {
|
||||
background-color: transparent;
|
||||
color: var(--gray-700);
|
||||
}
|
||||
|
||||
.btn-text:hover {
|
||||
background-color: var(--gray-100);
|
||||
}
|
||||
|
||||
.btn-text:active {
|
||||
background-color: var(--gray-200);
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
padding: 0;
|
||||
border-radius: var(--radius-full);
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.btn-icon:hover {
|
||||
background-color: var(--gray-100);
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
padding: var(--spacing-2) var(--spacing-4);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.btn-lg {
|
||||
padding: var(--spacing-4) var(--spacing-8);
|
||||
font-size: 1.125rem;
|
||||
}
|
||||
|
||||
/* FAB (Floating Action Button) */
|
||||
.fab {
|
||||
position: fixed;
|
||||
bottom: var(--spacing-4);
|
||||
right: var(--spacing-4);
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
background-color: var(--primary-main);
|
||||
color: white;
|
||||
border-radius: var(--radius-full);
|
||||
border: none;
|
||||
box-shadow: var(--shadow-md);
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-base);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 24px;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.fab:hover {
|
||||
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.2);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.fab:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
/* === Input Fields === */
|
||||
.input,
|
||||
.textarea,
|
||||
.select {
|
||||
width: 100%;
|
||||
padding: var(--spacing-3) var(--spacing-4);
|
||||
font-size: 1rem;
|
||||
font-family: inherit;
|
||||
border: 1px solid var(--gray-300);
|
||||
border-radius: var(--radius-md);
|
||||
background-color: white;
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.input:focus,
|
||||
.textarea:focus,
|
||||
.select:focus {
|
||||
outline: 4px solid rgba(0, 217, 177, 0.2);
|
||||
border-color: var(--primary-main);
|
||||
border-width: 2px;
|
||||
}
|
||||
|
||||
.input.error,
|
||||
.textarea.error {
|
||||
border-color: var(--error-main);
|
||||
}
|
||||
|
||||
.input:disabled,
|
||||
.textarea:disabled,
|
||||
.select:disabled {
|
||||
background-color: var(--gray-100);
|
||||
color: var(--gray-400);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.textarea {
|
||||
min-height: 120px;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
margin-bottom: var(--spacing-4);
|
||||
}
|
||||
|
||||
.input-label {
|
||||
display: block;
|
||||
margin-bottom: var(--spacing-2);
|
||||
font-weight: 500;
|
||||
color: var(--gray-700);
|
||||
}
|
||||
|
||||
.input-error {
|
||||
display: block;
|
||||
margin-top: var(--spacing-1);
|
||||
font-size: 0.875rem;
|
||||
color: var(--error-main);
|
||||
}
|
||||
|
||||
/* === Cards === */
|
||||
.card {
|
||||
background-color: white;
|
||||
border: 1px solid var(--gray-200);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--spacing-6);
|
||||
box-shadow: var(--shadow-sm);
|
||||
transition: all var(--transition-base);
|
||||
}
|
||||
|
||||
.card.interactive {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.card.interactive:hover {
|
||||
box-shadow: var(--shadow-md);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.card.interactive:active {
|
||||
box-shadow: var(--shadow-sm);
|
||||
transform: scale(0.99);
|
||||
}
|
||||
|
||||
/* === Badges === */
|
||||
.badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: var(--spacing-1) var(--spacing-3);
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
border-radius: var(--radius-full);
|
||||
}
|
||||
|
||||
.badge-primary {
|
||||
background-color: var(--primary-light);
|
||||
color: var(--primary-dark);
|
||||
}
|
||||
|
||||
.badge-success {
|
||||
background-color: var(--success-light);
|
||||
color: var(--success-dark);
|
||||
}
|
||||
|
||||
.badge-warning {
|
||||
background-color: var(--warning-light);
|
||||
color: var(--warning-dark);
|
||||
}
|
||||
|
||||
.badge-error {
|
||||
background-color: var(--error-light);
|
||||
color: var(--error-dark);
|
||||
}
|
||||
|
||||
.badge-neutral {
|
||||
background-color: var(--gray-200);
|
||||
color: var(--gray-700);
|
||||
}
|
||||
|
||||
/* === Modal === */
|
||||
.modal-backdrop {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
display: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.modal-backdrop.active {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.modal {
|
||||
background-color: white;
|
||||
border-radius: var(--radius-xl);
|
||||
padding: var(--spacing-8);
|
||||
max-width: 600px;
|
||||
width: 90%;
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
margin-bottom: var(--spacing-6);
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
font: var(--font-h3);
|
||||
color: var(--gray-900);
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
margin-bottom: var(--spacing-8);
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: var(--spacing-3);
|
||||
}
|
||||
|
||||
/* === Toast === */
|
||||
.toast-container {
|
||||
position: fixed;
|
||||
top: var(--spacing-4);
|
||||
right: var(--spacing-4);
|
||||
z-index: 10000;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-3);
|
||||
}
|
||||
|
||||
.toast {
|
||||
background-color: white;
|
||||
border-radius: var(--radius-md);
|
||||
padding: var(--spacing-4) var(--spacing-5);
|
||||
max-width: 400px;
|
||||
box-shadow: var(--shadow-md);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-3);
|
||||
border-left: 4px solid;
|
||||
animation: slideIn 200ms ease-out;
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
transform: translateX(400px);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.toast-success {
|
||||
border-left-color: var(--success-main);
|
||||
}
|
||||
|
||||
.toast-error {
|
||||
border-left-color: var(--error-main);
|
||||
}
|
||||
|
||||
.toast-warning {
|
||||
border-left-color: var(--warning-main);
|
||||
}
|
||||
|
||||
.toast-info {
|
||||
border-left-color: var(--info-main);
|
||||
}
|
||||
|
||||
/* === Layout === */
|
||||
.container {
|
||||
max-width: 1536px;
|
||||
margin: 0 auto;
|
||||
padding: 0 var(--spacing-6);
|
||||
}
|
||||
|
||||
.header {
|
||||
background-color: white;
|
||||
border-bottom: 1px solid var(--gray-200);
|
||||
padding: var(--spacing-4) 0;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.header-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.logo {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: var(--primary-main);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 250px;
|
||||
height: 100vh;
|
||||
background-color: white;
|
||||
border-right: 1px solid var(--gray-200);
|
||||
padding: var(--spacing-6);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
margin-left: 250px;
|
||||
padding: var(--spacing-8);
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* === Grid System === */
|
||||
.grid {
|
||||
display: grid;
|
||||
gap: var(--spacing-4);
|
||||
}
|
||||
|
||||
.grid-2 {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.grid-3 {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
|
||||
.grid-4 {
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
}
|
||||
|
||||
/* === Utility Classes === */
|
||||
.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.text-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.mt-2 { margin-top: var(--spacing-2); }
|
||||
.mt-4 { margin-top: var(--spacing-4); }
|
||||
.mt-6 { margin-top: var(--spacing-6); }
|
||||
.mt-8 { margin-top: var(--spacing-8); }
|
||||
|
||||
.mb-2 { margin-bottom: var(--spacing-2); }
|
||||
.mb-4 { margin-bottom: var(--spacing-4); }
|
||||
.mb-6 { margin-bottom: var(--spacing-6); }
|
||||
.mb-8 { margin-bottom: var(--spacing-8); }
|
||||
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.flex-col {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.items-center {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.justify-between {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.gap-2 { gap: var(--spacing-2); }
|
||||
.gap-4 { gap: var(--spacing-4); }
|
||||
.gap-6 { gap: var(--spacing-6); }
|
||||
|
||||
/* === Responsive === */
|
||||
@media (max-width: 1023px) {
|
||||
.sidebar {
|
||||
transform: translateX(-100%);
|
||||
transition: transform 300ms ease-out;
|
||||
}
|
||||
|
||||
.sidebar.active {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.main-content {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.grid-3 {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.grid-4 {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.container {
|
||||
padding: 0 var(--spacing-4);
|
||||
}
|
||||
|
||||
.grid-2,
|
||||
.grid-3,
|
||||
.grid-4 {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.modal {
|
||||
width: 95%;
|
||||
padding: var(--spacing-6);
|
||||
}
|
||||
}
|
||||
408
design/uiux/prototype/common.js
vendored
Normal file
408
design/uiux/prototype/common.js
vendored
Normal file
@ -0,0 +1,408 @@
|
||||
/* ============================================
|
||||
회의록 작성 및 공유 개선 서비스 - 공통 Javascript
|
||||
============================================ */
|
||||
|
||||
// === LocalStorage 키 상수 ===
|
||||
const STORAGE_KEYS = {
|
||||
CURRENT_USER: 'currentUser',
|
||||
USERS: 'users',
|
||||
MEETINGS: 'meetings',
|
||||
TODOS: 'todos',
|
||||
INITIALIZED: 'initialized'
|
||||
};
|
||||
|
||||
// === 예제 데이터 초기화 ===
|
||||
function initializeData() {
|
||||
if (localStorage.getItem(STORAGE_KEYS.INITIALIZED)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 사용자 데이터
|
||||
const users = [
|
||||
{ id: 'user1', name: '김민준', email: 'minjun@example.com', avatar: '👤' },
|
||||
{ id: 'user2', name: '박서연', email: 'seoyeon@example.com', avatar: '👩' },
|
||||
{ id: 'user3', name: '이준호', email: 'junho@example.com', avatar: '👨' },
|
||||
{ id: 'user4', name: '최유진', email: 'yujin@example.com', avatar: '👧' },
|
||||
{ id: 'user5', name: '정도현', email: 'dohyun@example.com', avatar: '🧑' }
|
||||
];
|
||||
|
||||
// 회의 데이터
|
||||
const meetings = [
|
||||
{
|
||||
id: 'meeting1',
|
||||
title: '2025년 1분기 전략 회의',
|
||||
date: '2025-10-20',
|
||||
time: '14:00',
|
||||
endTime: '15:30',
|
||||
location: '3층 회의실',
|
||||
participants: ['user1', 'user2', 'user3'],
|
||||
template: 'general',
|
||||
status: 'scheduled', // scheduled, in_progress, completed
|
||||
content: {
|
||||
sections: {
|
||||
participants: '김민준, 박서연, 이준호',
|
||||
agenda: '1분기 목표 설정 및 전략 수립',
|
||||
discussion: '',
|
||||
decisions: '',
|
||||
todos: ''
|
||||
}
|
||||
},
|
||||
createdAt: new Date().toISOString()
|
||||
},
|
||||
{
|
||||
id: 'meeting2',
|
||||
title: '주간 스크럼 회의',
|
||||
date: '2025-10-18',
|
||||
time: '10:00',
|
||||
endTime: '10:30',
|
||||
location: '온라인',
|
||||
participants: ['user1', 'user2', 'user3', 'user4'],
|
||||
template: 'scrum',
|
||||
status: 'completed',
|
||||
content: {
|
||||
sections: {
|
||||
participants: '김민준, 박서연, 이준호, 최유진',
|
||||
yesterday: '- API 개발 완료\n- 테스트 코드 작성',
|
||||
today: '- 프론트엔드 개발 시작\n- 디자인 리뷰',
|
||||
blockers: '없음'
|
||||
}
|
||||
},
|
||||
createdAt: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000).toISOString()
|
||||
}
|
||||
];
|
||||
|
||||
// Todo 데이터
|
||||
const todos = [
|
||||
{
|
||||
id: 'todo1',
|
||||
meetingId: 'meeting1',
|
||||
content: '1분기 마케팅 예산안 작성',
|
||||
assignee: 'user2',
|
||||
dueDate: '2025-10-25',
|
||||
priority: 'high', // high, normal, low
|
||||
status: 'pending', // pending, in_progress, completed
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'todo2',
|
||||
meetingId: 'meeting1',
|
||||
content: '경쟁사 분석 보고서 작성',
|
||||
assignee: 'user3',
|
||||
dueDate: '2025-10-22',
|
||||
priority: 'high',
|
||||
status: 'in_progress',
|
||||
progress: 60
|
||||
},
|
||||
{
|
||||
id: 'todo3',
|
||||
meetingId: 'meeting2',
|
||||
content: '테스트 커버리지 80% 달성',
|
||||
assignee: 'user3',
|
||||
dueDate: '2025-10-20',
|
||||
priority: 'normal',
|
||||
status: 'completed',
|
||||
progress: 100
|
||||
}
|
||||
];
|
||||
|
||||
// 현재 로그인 사용자
|
||||
const currentUser = users[0];
|
||||
|
||||
// LocalStorage에 저장
|
||||
localStorage.setItem(STORAGE_KEYS.USERS, JSON.stringify(users));
|
||||
localStorage.setItem(STORAGE_KEYS.MEETINGS, JSON.stringify(meetings));
|
||||
localStorage.setItem(STORAGE_KEYS.TODOS, JSON.stringify(todos));
|
||||
localStorage.setItem(STORAGE_KEYS.CURRENT_USER, JSON.stringify(currentUser));
|
||||
localStorage.setItem(STORAGE_KEYS.INITIALIZED, 'true');
|
||||
}
|
||||
|
||||
// === LocalStorage 헬퍼 함수 ===
|
||||
function getFromStorage(key) {
|
||||
const data = localStorage.getItem(key);
|
||||
return data ? JSON.parse(data) : null;
|
||||
}
|
||||
|
||||
function saveToStorage(key, data) {
|
||||
localStorage.setItem(key, JSON.stringify(data));
|
||||
}
|
||||
|
||||
// === 날짜/시간 유틸리티 ===
|
||||
function formatDate(dateString) {
|
||||
const date = new Date(dateString);
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
return `${year}-${month}-${day}`;
|
||||
}
|
||||
|
||||
function formatTime(timeString) {
|
||||
return timeString; // HH:MM 형식 그대로 반환
|
||||
}
|
||||
|
||||
function formatDateTime(dateString, timeString) {
|
||||
return `${formatDate(dateString)} ${formatTime(timeString)}`;
|
||||
}
|
||||
|
||||
function getDdayText(dueDateString) {
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
|
||||
const dueDate = new Date(dueDateString);
|
||||
dueDate.setHours(0, 0, 0, 0);
|
||||
|
||||
const diff = Math.ceil((dueDate - today) / (1000 * 60 * 60 * 24));
|
||||
|
||||
if (diff < 0) return `D${diff} (지남)`;
|
||||
if (diff === 0) return '오늘';
|
||||
return `D-${diff}`;
|
||||
}
|
||||
|
||||
function formatDuration(minutes) {
|
||||
const hours = Math.floor(minutes / 60);
|
||||
const mins = minutes % 60;
|
||||
if (hours > 0 && mins > 0) return `${hours}시간 ${mins}분`;
|
||||
if (hours > 0) return `${hours}시간`;
|
||||
return `${mins}분`;
|
||||
}
|
||||
|
||||
// === 사용자 관련 함수 ===
|
||||
function getCurrentUser() {
|
||||
return getFromStorage(STORAGE_KEYS.CURRENT_USER);
|
||||
}
|
||||
|
||||
function getUserById(userId) {
|
||||
const users = getFromStorage(STORAGE_KEYS.USERS) || [];
|
||||
return users.find(u => u.id === userId);
|
||||
}
|
||||
|
||||
function getUserName(userId) {
|
||||
const user = getUserById(userId);
|
||||
return user ? user.name : '알 수 없음';
|
||||
}
|
||||
|
||||
// === 회의 관련 함수 ===
|
||||
function getAllMeetings() {
|
||||
return getFromStorage(STORAGE_KEYS.MEETINGS) || [];
|
||||
}
|
||||
|
||||
function getMeetingById(meetingId) {
|
||||
const meetings = getAllMeetings();
|
||||
return meetings.find(m => m.id === meetingId);
|
||||
}
|
||||
|
||||
function saveMeeting(meeting) {
|
||||
const meetings = getAllMeetings();
|
||||
const index = meetings.findIndex(m => m.id === meeting.id);
|
||||
if (index >= 0) {
|
||||
meetings[index] = meeting;
|
||||
} else {
|
||||
meetings.push(meeting);
|
||||
}
|
||||
saveToStorage(STORAGE_KEYS.MEETINGS, meetings);
|
||||
}
|
||||
|
||||
function getScheduledMeetings() {
|
||||
const meetings = getAllMeetings();
|
||||
return meetings.filter(m => m.status === 'scheduled');
|
||||
}
|
||||
|
||||
function getRecentMeetings(limit = 6) {
|
||||
const meetings = getAllMeetings();
|
||||
return meetings
|
||||
.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt))
|
||||
.slice(0, limit);
|
||||
}
|
||||
|
||||
// === Todo 관련 함수 ===
|
||||
function getAllTodos() {
|
||||
return getFromStorage(STORAGE_KEYS.TODOS) || [];
|
||||
}
|
||||
|
||||
function getTodoById(todoId) {
|
||||
const todos = getAllTodos();
|
||||
return todos.find(t => t.id === todoId);
|
||||
}
|
||||
|
||||
function saveTodo(todo) {
|
||||
const todos = getAllTodos();
|
||||
const index = todos.findIndex(t => t.id === todo.id);
|
||||
if (index >= 0) {
|
||||
todos[index] = todo;
|
||||
} else {
|
||||
todos.push(todo);
|
||||
}
|
||||
saveToStorage(STORAGE_KEYS.TODOS, todos);
|
||||
}
|
||||
|
||||
function getPendingTodos(userId) {
|
||||
const todos = getAllTodos();
|
||||
return todos.filter(t => t.assignee === userId && t.status !== 'completed');
|
||||
}
|
||||
|
||||
function getTodosByStatus(status) {
|
||||
const todos = getAllTodos();
|
||||
return todos.filter(t => t.status === status);
|
||||
}
|
||||
|
||||
// === Toast 알림 ===
|
||||
function showToast(message, type = 'info') {
|
||||
// Toast 컨테이너 생성 (없으면)
|
||||
let container = document.querySelector('.toast-container');
|
||||
if (!container) {
|
||||
container = document.createElement('div');
|
||||
container.className = 'toast-container';
|
||||
document.body.appendChild(container);
|
||||
}
|
||||
|
||||
// Toast 생성
|
||||
const toast = document.createElement('div');
|
||||
toast.className = `toast toast-${type}`;
|
||||
|
||||
const icons = {
|
||||
success: '✓',
|
||||
error: '✗',
|
||||
warning: '⚠',
|
||||
info: 'ℹ'
|
||||
};
|
||||
|
||||
toast.innerHTML = `
|
||||
<span style="font-size: 20px;">${icons[type]}</span>
|
||||
<span>${message}</span>
|
||||
`;
|
||||
|
||||
container.appendChild(toast);
|
||||
|
||||
// 4초 후 자동 제거
|
||||
setTimeout(() => {
|
||||
toast.style.animation = 'slideOut 150ms ease-in';
|
||||
setTimeout(() => toast.remove(), 150);
|
||||
}, 4000);
|
||||
}
|
||||
|
||||
// slideOut 애니메이션 추가
|
||||
if (!document.querySelector('style[data-toast-style]')) {
|
||||
const style = document.createElement('style');
|
||||
style.setAttribute('data-toast-style', 'true');
|
||||
style.textContent = `
|
||||
@keyframes slideOut {
|
||||
to {
|
||||
transform: translateX(400px);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
// === 모달 제어 ===
|
||||
function openModal(modalId) {
|
||||
const modal = document.getElementById(modalId);
|
||||
if (modal) {
|
||||
modal.classList.add('active');
|
||||
}
|
||||
}
|
||||
|
||||
function closeModal(modalId) {
|
||||
const modal = document.getElementById(modalId);
|
||||
if (modal) {
|
||||
modal.classList.remove('active');
|
||||
}
|
||||
}
|
||||
|
||||
// 모달 백드롭 클릭 시 닫기
|
||||
document.addEventListener('click', (e) => {
|
||||
if (e.target.classList.contains('modal-backdrop')) {
|
||||
closeModal(e.target.id);
|
||||
}
|
||||
});
|
||||
|
||||
// === 화면 전환 ===
|
||||
function navigateTo(page) {
|
||||
window.location.href = page;
|
||||
}
|
||||
|
||||
function goBack() {
|
||||
window.history.back();
|
||||
}
|
||||
|
||||
// === 유틸리티 함수 ===
|
||||
function generateId(prefix = 'id') {
|
||||
return `${prefix}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
}
|
||||
|
||||
function debounce(func, wait) {
|
||||
let timeout;
|
||||
return function executedFunction(...args) {
|
||||
const later = () => {
|
||||
clearTimeout(timeout);
|
||||
func(...args);
|
||||
};
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(later, wait);
|
||||
};
|
||||
}
|
||||
|
||||
// === 템플릿 데이터 ===
|
||||
const TEMPLATES = {
|
||||
general: {
|
||||
id: 'general',
|
||||
name: '일반 회의',
|
||||
description: '기본 회의 구조',
|
||||
sections: [
|
||||
{ id: 'participants', name: '참석자', required: true },
|
||||
{ id: 'agenda', name: '회의 목적', required: false },
|
||||
{ id: 'discussion', name: '논의 내용', required: true },
|
||||
{ id: 'decisions', name: '결정 사항', required: true },
|
||||
{ id: 'todos', name: 'Todo', required: false }
|
||||
]
|
||||
},
|
||||
scrum: {
|
||||
id: 'scrum',
|
||||
name: '스크럼 회의',
|
||||
description: '데일리 스탠드업',
|
||||
sections: [
|
||||
{ id: 'participants', name: '참석자', required: true },
|
||||
{ id: 'yesterday', name: '어제 한 일', required: true },
|
||||
{ id: 'today', name: '오늘 할 일', required: true },
|
||||
{ id: 'blockers', name: '이슈/블로커', required: false }
|
||||
]
|
||||
},
|
||||
kickoff: {
|
||||
id: 'kickoff',
|
||||
name: '프로젝트 킥오프',
|
||||
description: '프로젝트 시작 회의',
|
||||
sections: [
|
||||
{ id: 'participants', name: '참석자', required: true },
|
||||
{ id: 'overview', name: '프로젝트 개요', required: true },
|
||||
{ id: 'goals', name: '목표', required: true },
|
||||
{ id: 'schedule', name: '일정', required: true },
|
||||
{ id: 'roles', name: '역할 분담', required: true },
|
||||
{ id: 'risks', name: '리스크', required: false }
|
||||
]
|
||||
},
|
||||
weekly: {
|
||||
id: 'weekly',
|
||||
name: '주간 회의',
|
||||
description: '주간 진행 상황 리뷰',
|
||||
sections: [
|
||||
{ id: 'participants', name: '참석자', required: true },
|
||||
{ id: 'achievements', name: '주간 실적', required: true },
|
||||
{ id: 'issues', name: '주요 이슈', required: false },
|
||||
{ id: 'nextWeek', name: '다음 주 계획', required: true },
|
||||
{ id: 'support', name: '지원 필요 사항', required: false }
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
function getTemplate(templateId) {
|
||||
return TEMPLATES[templateId];
|
||||
}
|
||||
|
||||
function getAllTemplates() {
|
||||
return Object.values(TEMPLATES);
|
||||
}
|
||||
|
||||
// === 페이지 로드 시 데이터 초기화 ===
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
initializeData();
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user