mirror of
https://github.com/hwanny1128/HGZero.git
synced 2025-12-06 14:56:23 +00:00
파일 재정렬 및 프로토타입 구조 변경
This commit is contained in:
parent
301eed5a38
commit
d3c8b57116
307
design-v1/uiux/prototype/01-로그인.html
Normal file
307
design-v1/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-v1/uiux/prototype/02-대시보드.html
Normal file
539
design-v1/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-v1/uiux/prototype/03-회의예약.html
Normal file
699
design-v1/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-v1/uiux/prototype/04-템플릿선택.html
Normal file
537
design-v1/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-v1/uiux/prototype/05-회의진행.html
Normal file
858
design-v1/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-v1/uiux/prototype/06-검증완료.html
Normal file
499
design-v1/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-v1/uiux/prototype/07-회의종료.html
Normal file
573
design-v1/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>
|
||||||
596
design-v1/uiux/prototype/common.css
Normal file
596
design-v1/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-v1/uiux/prototype/common.js
vendored
Normal file
408
design-v1/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();
|
||||||
|
});
|
||||||
674
design-v1/userstory.md
Normal file
674
design-v1/userstory.md
Normal file
@ -0,0 +1,674 @@
|
|||||||
|
# 회의록 작성 및 공유 개선 서비스 - 유저스토리
|
||||||
|
|
||||||
|
- [회의록 작성 및 공유 개선 서비스 - 유저스토리](#회의록-작성-및-공유-개선-서비스---유저스토리)
|
||||||
|
- [마이크로서비스 구성](#마이크로서비스-구성)
|
||||||
|
- [유저스토리](#유저스토리)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 마이크로서비스 구성
|
||||||
|
1. **User** - 사용자 인증 및 권한 관리
|
||||||
|
2. **Meeting** - 회의 관리, 회의록 생성 및 관리, 회의록 공유
|
||||||
|
3. **STT** - 음성 녹음 관리, 음성-텍스트 변환, 화자 식별
|
||||||
|
4. **AI** - LLM 기반 회의록 자동 작성, Todo 자동 추출
|
||||||
|
5. **RAG** - 전문용어 감지 및 설명 제공, 문서 검색
|
||||||
|
6. **Collaboration** - 실시간 동기화, 버전 관리, 충돌 해결
|
||||||
|
7. **Todo** - Todo 할당 및 관리, 진행 상황 추적
|
||||||
|
8. **Notification** - 알림 발송 및 리마인더 관리
|
||||||
|
9. **Calendar** - 일정 생성 및 외부 캘린더 연동
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 유저스토리
|
||||||
|
```
|
||||||
|
1. User 서비스
|
||||||
|
1) 사용자 인증 및 관리
|
||||||
|
AFR-USER-010: [사용자관리] 시스템 관리자로서 | 나는, 서비스 보안을 위해 | 사용자 인증 및 권한 관리 기능을 원한다.
|
||||||
|
- 시나리오: 사용자 인증 및 권한 관리
|
||||||
|
사용자가 로그인을 시도한 상황에서 | 아이디와 비밀번호를 입력하면 | 인증이 완료되고 권한에 따라 서비스에 접근할 수 있다.
|
||||||
|
- [ ] 사용자 인증 (아이디, 비밀번호)
|
||||||
|
- [ ] JWT 토큰 기반 인증
|
||||||
|
- [ ] 사용자 권한 관리 (관리자, 일반 사용자)
|
||||||
|
- [ ] 세션 관리
|
||||||
|
- M/8
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
2. Meeting 서비스
|
||||||
|
1) 회의 준비 및 관리
|
||||||
|
UFR-MEET-010: [회의예약] 회의록 작성자로서 | 나는, 회의를 효율적으로 준비하기 위해 | 회의를 예약하고 참석자를 초대하고 싶다.
|
||||||
|
- 시나리오: 회의 예약 및 참석자 초대
|
||||||
|
회의 예약 화면에 접근한 상황에서 | 회의 제목, 날짜/시간, 장소, 참석자 목록을 입력하고 예약 버튼을 클릭하면 | 회의가 예약되고 참석자에게 초대 이메일이 자동 발송된다.
|
||||||
|
|
||||||
|
[입력 요구사항]
|
||||||
|
- 회의 제목: 최대 100자 (필수)
|
||||||
|
- 날짜/시간: 날짜 및 시간 선택 (필수)
|
||||||
|
- 장소: 최대 200자 (선택)
|
||||||
|
- 참석자 목록: 이메일 주소 입력 (최소 1명 필수)
|
||||||
|
|
||||||
|
[처리 결과]
|
||||||
|
- 회의가 예약됨 (회의 ID 생성)
|
||||||
|
- 일정이 캘린더에 자동 등록됨
|
||||||
|
- 참석자에게 초대 이메일 발송됨
|
||||||
|
- 회의 시작 30분 전 리마인더 자동 발송
|
||||||
|
|
||||||
|
- M/13
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
UFR-MEET-020: [템플릿선택] 회의록 작성자로서 | 나는, 회의록을 효율적으로 작성하기 위해 | 회의 유형에 맞는 템플릿을 선택하고 싶다.
|
||||||
|
- 시나리오: 회의록 템플릿 선택
|
||||||
|
회의 시작 전 템플릿 선택 화면에 접근한 상황에서 | 제공되는 템플릿 중 하나를 선택하고 커스터마이징하면 | 회의록 도구가 준비된다.
|
||||||
|
|
||||||
|
[템플릿 유형]
|
||||||
|
- 일반 회의: 기본 구조 (참석자, 안건, 논의 내용, 결정 사항, Todo)
|
||||||
|
- 스크럼 회의: 어제 한 일, 오늘 할 일, 이슈
|
||||||
|
- 프로젝트 킥오프: 프로젝트 개요, 목표, 일정, 역할, 리스크
|
||||||
|
- 주간 회의: 주간 실적, 주요 이슈, 다음 주 계획
|
||||||
|
|
||||||
|
[커스터마이징 옵션]
|
||||||
|
- 섹션 추가/삭제
|
||||||
|
- 섹션 순서 변경
|
||||||
|
- 기본 항목 설정
|
||||||
|
|
||||||
|
[처리 결과]
|
||||||
|
- 선택된 템플릿으로 회의록 도구가 준비됨
|
||||||
|
- 템플릿 ID와 설정 정보가 저장됨
|
||||||
|
|
||||||
|
- S/5
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
UFR-MEET-030: [회의시작] 회의록 작성자로서 | 나는, 회의를 시작하고 회의록을 작성하기 위해 | 회의를 시작하고 음성 녹음을 준비하고 싶다.
|
||||||
|
- 시나리오: 회의 시작
|
||||||
|
예약된 회의 시간에 회의 시작 버튼을 클릭한 상황에서 | 회의 ID를 확인하고 시작하면 | 회의 세션이 생성되고 음성 녹음이 준비된다.
|
||||||
|
|
||||||
|
[회의 시작 조건]
|
||||||
|
- 예약된 회의가 존재함
|
||||||
|
- 회의 시작 시간이 도래함
|
||||||
|
- 회의록 작성자가 시작 권한을 가짐
|
||||||
|
|
||||||
|
[처리 결과]
|
||||||
|
- 회의 세션이 생성됨 (세션 ID)
|
||||||
|
- 음성 녹음 준비 완료
|
||||||
|
- 참석자 목록 표시
|
||||||
|
- 회의 시작 시간 기록
|
||||||
|
- 실시간 회의록 작성 화면 활성화
|
||||||
|
|
||||||
|
- M/8
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
2) 회의 종료 및 완료
|
||||||
|
UFR-MEET-040: [회의종료] 회의록 작성자로서 | 나는, 회의를 종료하고 회의록을 정리하기 위해 | 회의를 종료하고 통계를 확인하고 싶다.
|
||||||
|
- 시나리오: 회의 종료
|
||||||
|
회의가 진행 중인 상황에서 | 회의 종료 버튼을 클릭하면 | 음성 녹음이 중지되고 회의 통계가 생성된다.
|
||||||
|
|
||||||
|
[회의 종료 처리]
|
||||||
|
- 음성 녹음 즉시 중지
|
||||||
|
- 회의 종료 시간 기록
|
||||||
|
- 회의 통계 자동 생성
|
||||||
|
- 회의 총 시간
|
||||||
|
- 참석자 수
|
||||||
|
- 발언 횟수 (화자별)
|
||||||
|
- 주요 키워드
|
||||||
|
|
||||||
|
[처리 결과]
|
||||||
|
- 회의가 종료됨
|
||||||
|
- 회의 통계 표시
|
||||||
|
- 최종 회의록 확정 단계로 이동
|
||||||
|
|
||||||
|
- M/8
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
UFR-MEET-050: [최종확정] 회의록 작성자로서 | 나는, 회의록을 완성하기 위해 | 최종 회의록을 확정하고 버전을 생성하고 싶다.
|
||||||
|
- 시나리오: 최종 회의록 확정
|
||||||
|
회의가 종료된 상황에서 | 회의록 내용을 최종 검토하고 확정 버튼을 클릭하면 | 필수 항목이 검사되고 최종 버전이 생성된다.
|
||||||
|
|
||||||
|
[필수 항목 검사]
|
||||||
|
- 회의 제목 입력 여부
|
||||||
|
- 참석자 목록 작성 여부
|
||||||
|
- 주요 논의 내용 작성 여부
|
||||||
|
- 결정 사항 작성 여부
|
||||||
|
|
||||||
|
[처리 결과]
|
||||||
|
- 최종 회의록 확정됨 (확정 버전 번호)
|
||||||
|
- 확정 시간 기록
|
||||||
|
- AI가 자동으로 Todo 항목 추출 (UFR-AI-020 연동)
|
||||||
|
- 회의록 공유 가능 상태로 전환
|
||||||
|
|
||||||
|
[필수 항목 미작성 시]
|
||||||
|
- 누락된 항목 안내 메시지 표시
|
||||||
|
- 해당 섹션으로 자동 이동
|
||||||
|
|
||||||
|
- M/13
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
3) 회의록 공유
|
||||||
|
UFR-MEET-060: [회의록공유] 회의록 작성자로서 | 나는, 회의 내용을 참석자들과 공유하기 위해 | 최종 회의록을 공유하고 싶다.
|
||||||
|
- 시나리오: 회의록 공유
|
||||||
|
최종 회의록이 확정된 상황에서 | 공유 버튼을 클릭하고 공유 대상과 권한을 설정하면 | 공유 링크가 생성되고 참석자 전원에게 알림이 발송된다.
|
||||||
|
|
||||||
|
[공유 설정]
|
||||||
|
- 공유 대상: 참석자 전체 (기본) / 특정 참석자 선택
|
||||||
|
- 공유 권한: 읽기 전용 / 댓글 가능 / 편집 가능
|
||||||
|
- 공유 방식: 이메일 / 슬랙 / 링크 복사
|
||||||
|
|
||||||
|
[처리 결과]
|
||||||
|
- 공유 링크 생성 (고유 URL)
|
||||||
|
- 참석자에게 이메일/슬랙 알림 발송
|
||||||
|
- 공유 시간 기록
|
||||||
|
- 다음 회의 일정이 언급된 경우 캘린더에 자동 등록 (UFR-CAL-010 연동)
|
||||||
|
|
||||||
|
[공유 링크 보안]
|
||||||
|
- 링크 유효 기간 설정 (선택)
|
||||||
|
- 비밀번호 설정 (선택)
|
||||||
|
|
||||||
|
- M/13
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
3. STT 서비스
|
||||||
|
1) 음성 인식 및 변환
|
||||||
|
UFR-STT-010: [음성녹음인식] 회의 참석자로서 | 나는, 발언 내용이 자동으로 기록되기 위해 | 음성이 실시간으로 녹음되고 인식되기를 원한다.
|
||||||
|
- 시나리오: 음성 녹음 및 발언 인식
|
||||||
|
회의가 시작된 상황에서 | 참석자가 발언을 시작하면 | 음성이 자동으로 녹음되고 화자가 식별되며 발언이 인식된다.
|
||||||
|
|
||||||
|
[음성 녹음 처리]
|
||||||
|
- 오디오 스트림 실시간 캡처
|
||||||
|
- 회의 ID와 연결
|
||||||
|
- 음성 데이터 저장 (클라우드 스토리지)
|
||||||
|
|
||||||
|
[발언 인식 처리]
|
||||||
|
- AI 음성인식 엔진 연동 (Whisper, Google STT 등)
|
||||||
|
- 화자 자동 식별
|
||||||
|
- 참석자 목록 매칭
|
||||||
|
- 음성 특징 분석
|
||||||
|
- 타임스탬프 기록
|
||||||
|
- 발언 구간 구분
|
||||||
|
|
||||||
|
[처리 결과]
|
||||||
|
- 음성 녹음이 시작됨 (녹음 ID)
|
||||||
|
- 발언이 인식됨 (발언 ID, 화자, 타임스탬프)
|
||||||
|
- 실시간으로 텍스트 변환 요청 (UFR-STT-020 연동)
|
||||||
|
|
||||||
|
[성능 요구사항]
|
||||||
|
- 발언 인식 지연 시간: 1초 이내
|
||||||
|
- 화자 식별 정확도: 90% 이상
|
||||||
|
|
||||||
|
- M/21
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
UFR-STT-020: [텍스트변환] 회의록 시스템으로서 | 나는, 인식된 발언을 회의록에 기록하기 위해 | 음성을 텍스트로 변환하고 싶다.
|
||||||
|
- 시나리오: 음성-텍스트 변환
|
||||||
|
발언이 인식된 상황에서 | AI 음성인식 엔진에 텍스트 변환을 요청하면 | 음성이 텍스트로 변환되고 정확도와 함께 반환된다.
|
||||||
|
|
||||||
|
[텍스트 변환 처리]
|
||||||
|
- 인식된 발언 데이터 전달
|
||||||
|
- 언어 설정 (한국어, 영어 등)
|
||||||
|
- AI 음성인식 엔진 처리
|
||||||
|
- 문장 부호 자동 추가
|
||||||
|
- 숫자/날짜 형식 정규화
|
||||||
|
|
||||||
|
[처리 결과]
|
||||||
|
- 텍스트가 변환됨 (텍스트 ID)
|
||||||
|
- 변환된 내용 (원문 텍스트)
|
||||||
|
- 정확도 점수 (0-100%)
|
||||||
|
- AI 회의록 자동 작성 요청 (UFR-AI-010 연동)
|
||||||
|
|
||||||
|
[정확도 낮은 경우]
|
||||||
|
- 정확도 60% 미만 시 경고 표시
|
||||||
|
- 수동 수정 인터페이스 제공
|
||||||
|
|
||||||
|
- M/13
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
4. AI 서비스
|
||||||
|
1) AI 회의록 작성
|
||||||
|
UFR-AI-010: [회의록자동작성] 회의록 작성자로서 | 나는, 회의록 작성 부담을 줄이기 위해 | AI가 발언 내용을 자동으로 정리하여 회의록을 작성하기를 원한다.
|
||||||
|
- 시나리오: AI 회의록 자동 작성
|
||||||
|
텍스트가 변환된 상황에서 | LLM에 회의록 자동 작성을 요청하면 | 회의 맥락을 이해하고 구조화된 회의록 초안이 생성된다.
|
||||||
|
|
||||||
|
[AI 처리 과정]
|
||||||
|
- 변환된 텍스트와 회의 맥락(제목, 참석자, 이전 내용) 분석
|
||||||
|
- 회의 내용 이해
|
||||||
|
- 주제별 분류
|
||||||
|
- 발언자별 의견 정리
|
||||||
|
- 중요 키워드 추출
|
||||||
|
- 문장 다듬기
|
||||||
|
- 구어체 → 문어체 변환
|
||||||
|
- 불필요한 표현 제거
|
||||||
|
- 문법 교정
|
||||||
|
- 구조화
|
||||||
|
- 회의록 템플릿에 맞춰 정리
|
||||||
|
- 주제, 발언자, 내용 구조화
|
||||||
|
- 요약문 생성
|
||||||
|
|
||||||
|
[처리 결과]
|
||||||
|
- 회의록 초안이 생성됨 (회의록 버전)
|
||||||
|
- 생성 시간 기록
|
||||||
|
- 구조화된 내용
|
||||||
|
- 논의 주제
|
||||||
|
- 발언자별 의견
|
||||||
|
- 결정 사항
|
||||||
|
- 보류 사항
|
||||||
|
- 참석자에게 실시간 동기화 (UFR-COLLAB-010 연동)
|
||||||
|
|
||||||
|
[Policy/Rule]
|
||||||
|
- 텍스트 변환되면 자동으로 회의록 구조에 맞춰 정리
|
||||||
|
- 실시간 업데이트 (3-5초 간격)
|
||||||
|
|
||||||
|
- M/34
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
2) Todo 자동 추출
|
||||||
|
UFR-AI-020: [Todo자동추출] 회의록 작성자로서 | 나는, 회의 후 실행 사항을 명확히 하기 위해 | AI가 회의록에서 Todo 항목을 자동으로 추출하고 담당자를 식별하기를 원한다.
|
||||||
|
- 시나리오: AI Todo 자동 추출
|
||||||
|
회의가 종료된 상황에서 | 최종 회의록을 분석하여 Todo 자동 추출을 요청하면 | 액션 아이템이 식별되고 담당자가 자동으로 지정된다.
|
||||||
|
|
||||||
|
[AI 분석 과정]
|
||||||
|
- 회의록 전체 내용 분석
|
||||||
|
- 액션 아이템 식별
|
||||||
|
- "~하기로 함", "~까지 완료", "~담당" 등 키워드 탐지
|
||||||
|
- 명령형 문장 분석
|
||||||
|
- 마감일 언급 추출
|
||||||
|
- 담당자 자동 식별
|
||||||
|
- 발언 내용 기반 ("제가 하겠습니다", "~님이 담당")
|
||||||
|
- 직책/역할 기반 매칭
|
||||||
|
- 과거 회의록 패턴 학습
|
||||||
|
|
||||||
|
[처리 결과]
|
||||||
|
- Todo가 자동 추출됨
|
||||||
|
- 추출된 항목 수
|
||||||
|
- 각 Todo별 정보
|
||||||
|
- Todo 내용
|
||||||
|
- 담당자 (자동 식별)
|
||||||
|
- 마감일 (언급된 경우)
|
||||||
|
- 우선순위 (언급된 경우)
|
||||||
|
- 관련 회의록 섹션 링크
|
||||||
|
- Todo 서비스에 자동 전달 (UFR-TODO-010 연동)
|
||||||
|
|
||||||
|
[담당자 식별 실패 시]
|
||||||
|
- 미지정 상태로 Todo 생성
|
||||||
|
- 회의 주최자에게 수동 할당 요청 알림
|
||||||
|
|
||||||
|
- M/21
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
5. RAG 서비스
|
||||||
|
1) 전문용어 지원
|
||||||
|
UFR-RAG-010: [전문용어감지] 회의록 작성자로서 | 나는, 업무 지식이 없어도 회의록을 정확히 작성하기 위해 | 전문용어가 자동으로 감지되고 설명을 제공받고 싶다.
|
||||||
|
- 시나리오: 전문용어 자동 감지
|
||||||
|
회의록이 작성되는 상황에서 | 시스템이 회의록 텍스트를 분석하면 | 전문용어가 자동으로 감지되고 하이라이트 표시된다.
|
||||||
|
|
||||||
|
[전문용어 감지 처리]
|
||||||
|
- 회의록 텍스트 실시간 분석
|
||||||
|
- 용어 사전과 비교
|
||||||
|
- 조직별 전문용어 DB
|
||||||
|
- 산업별 표준 용어 DB
|
||||||
|
- 신뢰도 계산 (0-100%)
|
||||||
|
- 감지된 용어 위치 기록
|
||||||
|
|
||||||
|
[처리 결과]
|
||||||
|
- 전문용어가 감지됨
|
||||||
|
- 감지된 용어 정보
|
||||||
|
- 용어명
|
||||||
|
- 감지 위치 (줄 번호, 문단)
|
||||||
|
- 신뢰도 점수
|
||||||
|
- 용어 하이라이트 표시
|
||||||
|
- RAG 검색 자동 실행 (UFR-RAG-020 연동)
|
||||||
|
|
||||||
|
[Policy/Rule]
|
||||||
|
- 신뢰도 70% 이상만 자동 감지
|
||||||
|
- 중복 용어는 첫 번째만 하이라이트
|
||||||
|
|
||||||
|
- S/13
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
UFR-RAG-020: [용어설명제공] 회의록 작성자로서 | 나는, 전문용어를 이해하기 위해 | 용어에 대한 설명을 자동으로 제공받고 싶다.
|
||||||
|
- 시나리오: 용어 설명 자동 제공
|
||||||
|
전문용어가 감지된 상황에서 | RAG 시스템이 용어 설명을 검색하면 | 과거 회의록 및 사내 문서에서 관련 설명이 생성되어 제공된다.
|
||||||
|
|
||||||
|
[RAG 검색 수행]
|
||||||
|
- 벡터 유사도 검색
|
||||||
|
- 과거 회의록 검색
|
||||||
|
- 사내 문서 저장소 검색 (위키, 매뉴얼, 보고서)
|
||||||
|
- 관련 문서 추출 (관련도 점수순)
|
||||||
|
- 최대 5개 문서 선택
|
||||||
|
|
||||||
|
[LLM 설명 생성]
|
||||||
|
- 검색된 문서 내용 분석
|
||||||
|
- 용어 정의 추출
|
||||||
|
- 회의 맥락에 맞는 설명 생성
|
||||||
|
- 간단한 정의 (1-2문장)
|
||||||
|
- 상세 설명
|
||||||
|
- 사용 예시
|
||||||
|
- 참조 출처
|
||||||
|
|
||||||
|
[처리 결과]
|
||||||
|
- 용어 설명이 생성됨 (설명 ID)
|
||||||
|
- 설명 내용
|
||||||
|
- 간단한 정의
|
||||||
|
- 상세 설명
|
||||||
|
- 참조 문서 링크
|
||||||
|
- 툴팁 또는 사이드 패널로 표시
|
||||||
|
- 설명 제공 시간 기록
|
||||||
|
|
||||||
|
[설명을 찾지 못한 경우]
|
||||||
|
- "설명을 찾을 수 없습니다" 메시지 표시
|
||||||
|
- 전문가(회의 참석자)에게 설명 요청 버튼 제공
|
||||||
|
- 수동 입력된 설명은 용어 사전에 자동 저장
|
||||||
|
|
||||||
|
- S/21
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
6. Collaboration 서비스
|
||||||
|
1) 실시간 협업
|
||||||
|
UFR-COLLAB-010: [회의록수정동기화] 회의 참석자로서 | 나는, 회의록을 함께 검증하기 위해 | 회의록을 수정하고 실시간으로 다른 참석자와 동기화하고 싶다.
|
||||||
|
- 시나리오: 회의록 실시간 수정 및 동기화
|
||||||
|
회의록이 작성된 상황에서 | 참석자가 회의록 내용을 수정하면 | 수정 사항이 버전 관리되고 웹소켓을 통해 모든 참석자에게 즉시 동기화된다.
|
||||||
|
|
||||||
|
[회의록 수정 처리]
|
||||||
|
- 수정 내용 검증
|
||||||
|
- 수정 권한 확인
|
||||||
|
- 수정 범위 제한 (잠긴 섹션은 수정 불가)
|
||||||
|
- 수정 이력 저장
|
||||||
|
- 수정자
|
||||||
|
- 수정 시간
|
||||||
|
- 수정 전/후 내용
|
||||||
|
- 수정 위치
|
||||||
|
- 버전 관리
|
||||||
|
- 새 버전 번호 생성
|
||||||
|
- 이전 버전 보관
|
||||||
|
|
||||||
|
[실시간 동기화]
|
||||||
|
- 웹소켓을 통해 수정 델타 전송
|
||||||
|
- 전체 내용이 아닌 변경 부분만 전송 (효율성)
|
||||||
|
- 모든 참석자 화면에 실시간 반영
|
||||||
|
- 수정자 표시 (아바타, 이름)
|
||||||
|
- 수정 영역 하이라이트 (3초간)
|
||||||
|
|
||||||
|
[처리 결과]
|
||||||
|
- 참석자가 회의록을 수정함 (수정 ID)
|
||||||
|
- 수정 사항이 동기화됨
|
||||||
|
- 동기화 시간
|
||||||
|
- 영향받은 참석자 목록
|
||||||
|
|
||||||
|
[Policy/Rule]
|
||||||
|
- 회의록 수정 시 웹소켓을 통해 모든 참석자에게 즉시 동기화
|
||||||
|
|
||||||
|
- M/34
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
UFR-COLLAB-020: [충돌해결] 회의 참석자로서 | 나는, 동시 수정 상황에서도 내용을 잃지 않기 위해 | 충돌을 감지하고 해결하고 싶다.
|
||||||
|
- 시나리오: 동시 수정 충돌 해결
|
||||||
|
두 명의 참석자가 동일한 위치를 동시에 수정한 상황에서 | 시스템이 충돌을 감지하면 | 충돌 알림이 표시되고 해결 방법을 선택할 수 있다.
|
||||||
|
|
||||||
|
[충돌 감지]
|
||||||
|
- 동일 위치 동시 수정 탐지
|
||||||
|
- 라인 단위 비교
|
||||||
|
- 버전 기반 충돌 확인
|
||||||
|
- 충돌 정보 생성
|
||||||
|
- 충돌 위치
|
||||||
|
- 관련 수정자 2명
|
||||||
|
- 각자의 수정 내용
|
||||||
|
|
||||||
|
[충돌 해결 방식]
|
||||||
|
- Last Write Wins (기본)
|
||||||
|
- 가장 최근 수정이 우선
|
||||||
|
- 이전 수정은 버전 이력에 보관
|
||||||
|
- 수동 병합 (선택)
|
||||||
|
- 충돌 내용 비교 UI 표시
|
||||||
|
- 사용자가 최종 내용 선택
|
||||||
|
- A 선택 / B 선택 / 직접 작성
|
||||||
|
|
||||||
|
[처리 결과]
|
||||||
|
- 충돌이 감지됨 (충돌 ID)
|
||||||
|
- 충돌 위치
|
||||||
|
- 관련 수정자
|
||||||
|
- 충돌이 해결됨
|
||||||
|
- 해결 방법 (Last Write Wins / 수동 병합)
|
||||||
|
- 최종 내용
|
||||||
|
- 해결된 내용 실시간 동기화
|
||||||
|
|
||||||
|
[Policy/Rule]
|
||||||
|
- 동시 수정 발생 시 최종 수정이 우선 (Last Write Wins) 또는 충돌 알림
|
||||||
|
|
||||||
|
- M/21
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
UFR-COLLAB-030: [검증완료] 회의 참석자로서 | 나는, 회의록의 정확성을 보장하기 위해 | 주요 섹션을 검증하고 완료 표시를 하고 싶다.
|
||||||
|
- 시나리오: 회의록 검증 완료
|
||||||
|
회의록 내용을 확인한 상황에서 | 참석자가 검증 완료 버튼을 클릭하면 | 검증 상태가 업데이트되고 다른 참석자에게 동기화된다.
|
||||||
|
|
||||||
|
[검증 처리]
|
||||||
|
- 검증자 정보 기록
|
||||||
|
- 검증 시간 기록
|
||||||
|
- 검증 대상 섹션 기록
|
||||||
|
- 검증 상태 업데이트
|
||||||
|
- 미검증 → 검증 중 → 검증 완료
|
||||||
|
|
||||||
|
[섹션 잠금 기능]
|
||||||
|
- 주요 섹션 검증 완료 시 잠금 가능 (선택)
|
||||||
|
- 잠긴 섹션은 추가 수정 불가
|
||||||
|
- 잠금 해제는 검증자 또는 회의 주최자만 가능
|
||||||
|
|
||||||
|
[처리 결과]
|
||||||
|
- 검증이 완료됨
|
||||||
|
- 검증자 정보
|
||||||
|
- 검증 상태 (검증 완료)
|
||||||
|
- 완료 시간
|
||||||
|
- 검증 완료 상태 실시간 동기화
|
||||||
|
- 검증 배지 표시 (체크 아이콘)
|
||||||
|
|
||||||
|
[Policy/Rule]
|
||||||
|
- 주요 섹션 검증 완료 시 해당 섹션 잠금 가능
|
||||||
|
|
||||||
|
- M/8
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
7. Todo 서비스
|
||||||
|
1) Todo 관리
|
||||||
|
UFR-TODO-010: [Todo할당] Todo 시스템으로서 | 나는, AI가 추출한 Todo를 담당자에게 전달하기 위해 | Todo를 할당하고 알림을 발송하고 싶다.
|
||||||
|
- 시나리오: Todo 자동 할당
|
||||||
|
AI가 Todo를 추출한 상황에서 | 시스템이 Todo를 등록하고 담당자를 지정하면 | Todo가 할당되고 담당자에게 즉시 알림이 발송되며 캘린더에 마감일이 등록된다.
|
||||||
|
|
||||||
|
[Todo 등록]
|
||||||
|
- Todo 정보 저장
|
||||||
|
- Todo ID 생성
|
||||||
|
- Todo 내용
|
||||||
|
- 담당자 (AI 자동 식별 또는 수동 지정)
|
||||||
|
- 마감일 (언급된 경우 자동 설정, 없으면 수동 설정)
|
||||||
|
- 우선순위 (높음/보통/낮음)
|
||||||
|
- 관련 회의록 링크
|
||||||
|
|
||||||
|
[알림 발송]
|
||||||
|
- 담당자에게 즉시 알림
|
||||||
|
- 이메일
|
||||||
|
- 슬랙 (연동된 경우)
|
||||||
|
- 알림 내용
|
||||||
|
- Todo 내용
|
||||||
|
- 마감일
|
||||||
|
- 회의록 링크
|
||||||
|
|
||||||
|
[캘린더 연동]
|
||||||
|
- 마감일이 있는 경우 캘린더에 자동 등록
|
||||||
|
- 마감일 3일 전 리마인더 일정 생성
|
||||||
|
|
||||||
|
[처리 결과]
|
||||||
|
- Todo가 할당됨 (Todo ID)
|
||||||
|
- 담당자 정보
|
||||||
|
- 마감일
|
||||||
|
- 할당 시간
|
||||||
|
- 담당자에게 알림이 발송됨
|
||||||
|
- 캘린더 등록 완료
|
||||||
|
|
||||||
|
[Policy/Rule]
|
||||||
|
- Todo 할당 시 담당자에게 즉시 알림 발송
|
||||||
|
|
||||||
|
- M/13
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
UFR-TODO-020: [Todo진행상황업데이트] Todo 담당자로서 | 나는, Todo 진행 상황을 공유하기 위해 | 진행률을 업데이트하고 상태를 변경하고 싶다.
|
||||||
|
- 시나리오: Todo 진행 상황 업데이트
|
||||||
|
할당된 Todo가 있는 상황에서 | 담당자가 진행률과 상태를 입력하면 | 진행 상황이 저장되고 회의 주최자에게 알림이 발송된다.
|
||||||
|
|
||||||
|
[진행 상황 입력]
|
||||||
|
- 진행률: 0-100% (슬라이더 또는 직접 입력)
|
||||||
|
- 상태: 시작 전 / 진행 중 / 완료
|
||||||
|
- 메모: 진행 상황 설명 (선택)
|
||||||
|
|
||||||
|
[진행 상황 저장]
|
||||||
|
- 업데이트 시간 기록
|
||||||
|
- 진행률 히스토리 저장
|
||||||
|
- 상태 변경 이력 저장
|
||||||
|
|
||||||
|
[알림 발송]
|
||||||
|
- 회의 주최자에게 진행 상황 알림
|
||||||
|
- 진행률이 50%, 100%에 도달하면 자동 알림
|
||||||
|
|
||||||
|
[처리 결과]
|
||||||
|
- Todo 진행 상황이 업데이트됨
|
||||||
|
- 업데이트 시간
|
||||||
|
- 진행률 (%)
|
||||||
|
- 상태 (시작 전/진행 중/완료)
|
||||||
|
- 회의록에 진행 상황 반영
|
||||||
|
|
||||||
|
- M/5
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
UFR-TODO-030: [Todo완료처리] Todo 담당자로서 | 나는, 완료된 Todo를 처리하기 위해 | Todo를 완료하고 회의록에 자동 반영하고 싶다.
|
||||||
|
- 시나리오: Todo 완료 처리
|
||||||
|
Todo 작업이 완료된 상황에서 | 담당자가 완료 버튼을 클릭하면 | Todo가 완료 상태로 변경되고 회의록에 자동 반영되며 회의 주최자에게 알림이 발송된다.
|
||||||
|
|
||||||
|
[완료 처리]
|
||||||
|
- 완료 시간 자동 기록
|
||||||
|
- 완료자 정보 저장
|
||||||
|
- 완료 상태로 변경
|
||||||
|
- 완료 여부 확인 다이얼로그 표시
|
||||||
|
|
||||||
|
[회의록 반영]
|
||||||
|
- 관련 회의록의 Todo 섹션 업데이트
|
||||||
|
- 완료 표시 (체크 아이콘)
|
||||||
|
- 완료 시간 기록
|
||||||
|
|
||||||
|
[알림 발송]
|
||||||
|
- 회의 주최자에게 완료 알림
|
||||||
|
- 모든 Todo 완료 시 전체 완료 알림
|
||||||
|
|
||||||
|
[처리 결과]
|
||||||
|
- Todo가 완료됨
|
||||||
|
- 완료 시간
|
||||||
|
- 완료자 정보
|
||||||
|
- 회의록에 완료 상태가 반영됨
|
||||||
|
- 반영 시간
|
||||||
|
- 회의록 버전 업데이트
|
||||||
|
|
||||||
|
[Policy/Rule]
|
||||||
|
- Todo 완료 시 회의록에 완료 상태 자동 반영
|
||||||
|
- 모든 Todo 완료 시 회의 주최자에게 완료 알림
|
||||||
|
|
||||||
|
- M/8
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
8. Notification 서비스
|
||||||
|
1) 알림 관리
|
||||||
|
UFR-NOTI-010: [알림리마인더] 회의 참석자로서 | 나는, 중요한 일정을 놓치지 않기 위해 | 회의 및 Todo 관련 알림과 리마인더를 받고 싶다.
|
||||||
|
- 시나리오 1: 회의 알림
|
||||||
|
회의가 예약된 상황에서 | 회의 시작 30분 전이 되면 | 참석자에게 리마인더가 자동 발송된다.
|
||||||
|
|
||||||
|
[회의 알림 유형]
|
||||||
|
- 회의 초대: 회의 예약 시
|
||||||
|
- 회의 시작 리마인더: 30분 전
|
||||||
|
- 회의록 공유: 회의 종료 후
|
||||||
|
|
||||||
|
- 시나리오 2: Todo 알림
|
||||||
|
Todo가 할당된 상황에서 | 마감일 3일 전이 되면 | 담당자에게 리마인더가 자동 발송된다.
|
||||||
|
|
||||||
|
[Todo 알림 유형]
|
||||||
|
- Todo 할당: 할당 즉시
|
||||||
|
- 마감일 3일 전 리마인더
|
||||||
|
- 마감일 당일 리마인더
|
||||||
|
- 마감일 경과 긴급 알림 (미완료 시)
|
||||||
|
- Todo 완료: 완료 시
|
||||||
|
|
||||||
|
[알림 채널]
|
||||||
|
- 이메일 (기본)
|
||||||
|
- 슬랙 (연동 시)
|
||||||
|
- 인앱 알림
|
||||||
|
|
||||||
|
[알림 설정]
|
||||||
|
- 알림 채널 선택
|
||||||
|
- 알림 시간 설정
|
||||||
|
- 알림 끄기/켜기
|
||||||
|
|
||||||
|
[처리 결과]
|
||||||
|
- 알림이 발송됨 (알림 ID)
|
||||||
|
- 알림 대상 (이메일 주소, 슬랙 ID)
|
||||||
|
- 알림 내용
|
||||||
|
- 발송 시간
|
||||||
|
- 발송 채널
|
||||||
|
- 발송 상태 (성공/실패)
|
||||||
|
|
||||||
|
[Policy/Rule]
|
||||||
|
- 회의 시작 30분 전 리마인더 자동 발송
|
||||||
|
- 마감일 3일 전 자동 리마인더 발송
|
||||||
|
- 마감일 당일 미완료 시 긴급 알림 발송
|
||||||
|
|
||||||
|
- M/13
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
9. Calendar 서비스
|
||||||
|
1) 일정 관리
|
||||||
|
UFR-CAL-010: [일정연동] 회의록 작성자로서 | 나는, 일정을 통합 관리하기 위해 | 회의 및 다음 회의 일정을 외부 캘린더에 자동으로 연동하고 싶다.
|
||||||
|
- 시나리오 1: 회의 일정 자동 등록
|
||||||
|
회의가 예약된 상황에서 | 시스템이 일정 동기화를 요청하면 | 회의 일정이 Google Calendar, Outlook 등 외부 캘린더에 자동으로 등록된다.
|
||||||
|
|
||||||
|
[일정 등록 정보]
|
||||||
|
- 회의 제목
|
||||||
|
- 날짜 및 시간
|
||||||
|
- 장소
|
||||||
|
- 참석자 목록
|
||||||
|
- 회의록 링크 (메모)
|
||||||
|
|
||||||
|
- 시나리오 2: 다음 회의 일정 연동
|
||||||
|
회의록에서 다음 회의 일정이 언급된 상황에서 | 시스템이 자동으로 감지하면 | 다음 회의 일정이 캘린더에 자동으로 생성된다.
|
||||||
|
|
||||||
|
[자동 감지 키워드]
|
||||||
|
- "다음 회의: ~"
|
||||||
|
- "~에 다시 모이기로 함"
|
||||||
|
- "후속 회의 일정: ~"
|
||||||
|
|
||||||
|
[처리 결과]
|
||||||
|
- 일정이 캘린더에 연동됨 (일정 ID)
|
||||||
|
- 연동 상태 (성공/실패)
|
||||||
|
- 캘린더 종류 (Google Calendar, Outlook)
|
||||||
|
- 연동 시간
|
||||||
|
|
||||||
|
[지원 캘린더]
|
||||||
|
- Google Calendar
|
||||||
|
- Microsoft Outlook
|
||||||
|
- Apple Calendar
|
||||||
|
|
||||||
|
[Policy/Rule]
|
||||||
|
- 다음 회의 일정이 언급되면 자동으로 캘린더에 등록
|
||||||
|
|
||||||
|
- S/13
|
||||||
|
|
||||||
|
---
|
||||||
|
```
|
||||||
1184
design/style-guide.md
Normal file
1184
design/style-guide.md
Normal file
File diff suppressed because it is too large
Load Diff
@ -3,24 +3,26 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>로그인 - 회의록 작성 서비스</title>
|
<title>로그인 - 회의록 작성 및 공유 서비스</title>
|
||||||
<link rel="stylesheet" href="common.css">
|
<link rel="stylesheet" href="common.css">
|
||||||
<style>
|
<style>
|
||||||
|
/* 페이지 전용 스타일 */
|
||||||
body {
|
body {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
background: linear-gradient(135deg, #4DFFDB 0%, #00D9B1 100%);
|
background: linear-gradient(135deg, #00D9B1 0%, #6366F1 100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-container {
|
.login-card {
|
||||||
background-color: white;
|
background-color: var(--color-white);
|
||||||
border-radius: var(--radius-xl);
|
border-radius: var(--radius-xl);
|
||||||
padding: var(--spacing-12);
|
padding: var(--spacing-10);
|
||||||
max-width: 400px;
|
|
||||||
width: 90%;
|
|
||||||
box-shadow: var(--shadow-lg);
|
box-shadow: var(--shadow-lg);
|
||||||
|
width: 100%;
|
||||||
|
max-width: 480px;
|
||||||
|
margin: var(--spacing-4);
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-header {
|
.login-header {
|
||||||
@ -28,279 +30,321 @@
|
|||||||
margin-bottom: var(--spacing-8);
|
margin-bottom: var(--spacing-8);
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo-large {
|
.login-logo {
|
||||||
font-size: 3rem;
|
width: 64px;
|
||||||
color: var(--primary-main);
|
height: 64px;
|
||||||
margin-bottom: var(--spacing-4);
|
margin: 0 auto var(--spacing-4);
|
||||||
|
background-color: var(--color-primary-main);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 32px;
|
||||||
|
color: var(--color-white);
|
||||||
|
font-weight: var(--font-weight-bold);
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-title {
|
.login-title {
|
||||||
font: var(--font-h2);
|
font-size: var(--font-size-h2);
|
||||||
color: var(--gray-900);
|
font-weight: var(--font-weight-bold);
|
||||||
|
color: var(--color-gray-900);
|
||||||
margin-bottom: var(--spacing-2);
|
margin-bottom: var(--spacing-2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-subtitle {
|
.login-subtitle {
|
||||||
font: var(--font-body);
|
font-size: var(--font-size-body);
|
||||||
color: var(--gray-500);
|
color: var(--color-gray-500);
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-form {
|
#loginForm {
|
||||||
|
margin-bottom: var(--spacing-6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: var(--spacing-5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-footer {
|
||||||
display: flex;
|
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;
|
align-items: center;
|
||||||
font-size: 0.875rem;
|
justify-content: space-between;
|
||||||
|
margin-bottom: var(--spacing-5);
|
||||||
}
|
}
|
||||||
|
|
||||||
.checkbox-label {
|
.checkbox-wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--spacing-2);
|
gap: var(--spacing-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-wrapper input[type="checkbox"] {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
cursor: pointer;
|
||||||
|
accent-color: var(--color-primary-main);
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-wrapper label {
|
||||||
|
font-size: var(--font-size-body-small);
|
||||||
|
color: var(--color-gray-600);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: var(--gray-600);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.forgot-password {
|
.forgot-password {
|
||||||
color: var(--primary-main);
|
font-size: var(--font-size-body-small);
|
||||||
|
color: var(--color-primary-main);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
transition: color var(--transition-fast);
|
||||||
}
|
}
|
||||||
|
|
||||||
.forgot-password:hover {
|
.forgot-password:hover {
|
||||||
text-decoration: underline;
|
color: var(--color-primary-dark);
|
||||||
}
|
}
|
||||||
|
|
||||||
.divider {
|
.login-footer {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin: var(--spacing-6) 0;
|
padding-top: var(--spacing-6);
|
||||||
position: relative;
|
border-top: 1px solid var(--color-gray-200);
|
||||||
}
|
}
|
||||||
|
|
||||||
.divider::before,
|
.login-footer-text {
|
||||||
.divider::after {
|
font-size: var(--font-size-body-small);
|
||||||
content: '';
|
color: var(--color-gray-500);
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
width: 45%;
|
|
||||||
height: 1px;
|
|
||||||
background-color: var(--gray-200);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.divider::before { left: 0; }
|
.login-footer a {
|
||||||
.divider::after { right: 0; }
|
color: var(--color-primary-main);
|
||||||
|
font-weight: var(--font-weight-medium);
|
||||||
.divider-text {
|
text-decoration: none;
|
||||||
background-color: white;
|
transition: color var(--transition-fast);
|
||||||
padding: 0 var(--spacing-3);
|
|
||||||
color: var(--gray-500);
|
|
||||||
font-size: 0.875rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.social-login {
|
.login-footer a:hover {
|
||||||
display: flex;
|
color: var(--color-primary-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 예시 크리덴셜 표시 */
|
||||||
|
.credential-hint {
|
||||||
|
background-color: var(--color-gray-50);
|
||||||
|
border: 1px dashed var(--color-gray-300);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
padding: var(--spacing-3);
|
||||||
|
margin-bottom: var(--spacing-5);
|
||||||
|
font-size: var(--font-size-body-small);
|
||||||
|
color: var(--color-gray-600);
|
||||||
|
}
|
||||||
|
|
||||||
|
.credential-hint-title {
|
||||||
|
font-weight: var(--font-weight-medium);
|
||||||
|
color: var(--color-gray-700);
|
||||||
|
margin-bottom: var(--spacing-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.credential-hint code {
|
||||||
|
background-color: var(--color-gray-200);
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
font-family: 'Consolas', monospace;
|
||||||
|
font-size: var(--font-size-caption);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 반응형 */
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
.login-card {
|
||||||
|
padding: var(--spacing-6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-title {
|
||||||
|
font-size: var(--font-size-h3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-footer {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
gap: var(--spacing-3);
|
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>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="login-container">
|
<div class="login-card">
|
||||||
|
<!-- 헤더 -->
|
||||||
<div class="login-header">
|
<div class="login-header">
|
||||||
<div class="logo-large">📝</div>
|
<div class="login-logo">M</div>
|
||||||
<h1 class="login-title">회의록 작성 서비스</h1>
|
<h1 class="login-title">회의록 서비스</h1>
|
||||||
<p class="login-subtitle">AI와 함께하는 스마트한 회의록 관리</p>
|
<p class="login-subtitle">스마트한 협업의 시작</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form class="login-form" id="loginForm">
|
<!-- 예시 크리덴셜 (프로토타입용) -->
|
||||||
<div class="input-group">
|
<div class="credential-hint">
|
||||||
<label for="email" class="input-label">이메일</label>
|
<div class="credential-hint-title">📝 테스트 계정</div>
|
||||||
|
<div>이메일: <code>test@example.com</code></div>
|
||||||
|
<div>비밀번호: <code>password123</code></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 로그인 폼 -->
|
||||||
|
<form id="loginForm">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="email" class="form-label">이메일</label>
|
||||||
<input
|
<input
|
||||||
type="email"
|
type="email"
|
||||||
id="email"
|
id="email"
|
||||||
class="input"
|
class="form-input"
|
||||||
placeholder="example@company.com"
|
placeholder="example@company.com"
|
||||||
required
|
required
|
||||||
|
autocomplete="email"
|
||||||
>
|
>
|
||||||
<span class="input-error hidden" id="emailError"></span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="input-group">
|
<div class="form-group">
|
||||||
<label for="password" class="input-label">비밀번호</label>
|
<label for="password" class="form-label">비밀번호</label>
|
||||||
<div class="password-toggle">
|
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
id="password"
|
id="password"
|
||||||
class="input"
|
class="form-input"
|
||||||
placeholder="비밀번호를 입력하세요"
|
placeholder="비밀번호를 입력하세요"
|
||||||
required
|
required
|
||||||
|
autocomplete="current-password"
|
||||||
>
|
>
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="password-toggle-btn"
|
|
||||||
id="togglePassword"
|
|
||||||
aria-label="비밀번호 표시/숨김"
|
|
||||||
>
|
|
||||||
👁️
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<span class="input-error hidden" id="passwordError"></span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="login-options">
|
<div class="form-footer">
|
||||||
<label class="checkbox-label">
|
<div class="checkbox-wrapper">
|
||||||
<input type="checkbox" id="rememberMe">
|
<input type="checkbox" id="rememberMe">
|
||||||
<span>로그인 상태 유지</span>
|
<label for="rememberMe">로그인 상태 유지</label>
|
||||||
</label>
|
</div>
|
||||||
<a href="#" class="forgot-password">비밀번호 찾기</a>
|
<a href="#" class="forgot-password">비밀번호 찾기</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button type="submit" class="btn btn-primary btn-lg">
|
<button type="submit" class="btn btn-primary" style="width: 100%;">
|
||||||
로그인
|
로그인
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div class="divider">
|
<!-- 푸터 -->
|
||||||
<span class="divider-text">또는</span>
|
<div class="login-footer">
|
||||||
</div>
|
<p class="login-footer-text">
|
||||||
|
아직 계정이 없으신가요? <a href="#">회원가입</a>
|
||||||
<div class="social-login">
|
</p>
|
||||||
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- JavaScript -->
|
||||||
<script src="common.js"></script>
|
<script src="common.js"></script>
|
||||||
<script>
|
<script>
|
||||||
// 비밀번호 표시/숨김 토글
|
// 로그인 폼 처리
|
||||||
document.getElementById('togglePassword').addEventListener('click', function() {
|
const loginForm = document.getElementById('loginForm');
|
||||||
|
const emailInput = document.getElementById('email');
|
||||||
const passwordInput = document.getElementById('password');
|
const passwordInput = document.getElementById('password');
|
||||||
const type = passwordInput.type === 'password' ? 'text' : 'password';
|
const rememberMeCheckbox = document.getElementById('rememberMe');
|
||||||
passwordInput.type = type;
|
|
||||||
this.textContent = type === 'password' ? '👁️' : '👁️🗨️';
|
// 페이지 로드 시 저장된 이메일 불러오기
|
||||||
|
MeetingApp.ready(() => {
|
||||||
|
const savedEmail = MeetingApp.Storage.get('savedEmail');
|
||||||
|
if (savedEmail) {
|
||||||
|
emailInput.value = savedEmail;
|
||||||
|
rememberMeCheckbox.checked = true;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 로그인 폼 제출
|
// 폼 제출 핸들러
|
||||||
document.getElementById('loginForm').addEventListener('submit', function(e) {
|
loginForm.addEventListener('submit', async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
const email = document.getElementById('email').value;
|
// 에러 초기화
|
||||||
const password = document.getElementById('password').value;
|
MeetingApp.Validator.clearError(emailInput);
|
||||||
const rememberMe = document.getElementById('rememberMe').checked;
|
MeetingApp.Validator.clearError(passwordInput);
|
||||||
|
|
||||||
// 간단한 검증
|
const email = emailInput.value.trim();
|
||||||
const emailError = document.getElementById('emailError');
|
const password = passwordInput.value.trim();
|
||||||
const passwordError = document.getElementById('passwordError');
|
|
||||||
|
|
||||||
emailError.classList.add('hidden');
|
// 유효성 검사
|
||||||
passwordError.classList.add('hidden');
|
let isValid = true;
|
||||||
document.getElementById('email').classList.remove('error');
|
|
||||||
document.getElementById('password').classList.remove('error');
|
|
||||||
|
|
||||||
let hasError = false;
|
if (!MeetingApp.Validator.required(email)) {
|
||||||
|
MeetingApp.Validator.showError(emailInput, '이메일을 입력해주세요.');
|
||||||
// 이메일 검증
|
isValid = false;
|
||||||
if (!email.includes('@')) {
|
} else if (!MeetingApp.Validator.isEmail(email)) {
|
||||||
emailError.textContent = '올바른 이메일 주소를 입력하세요.';
|
MeetingApp.Validator.showError(emailInput, '올바른 이메일 형식이 아닙니다.');
|
||||||
emailError.classList.remove('hidden');
|
isValid = false;
|
||||||
document.getElementById('email').classList.add('error');
|
|
||||||
hasError = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 비밀번호 검증
|
if (!MeetingApp.Validator.required(password)) {
|
||||||
if (password.length < 8) {
|
MeetingApp.Validator.showError(passwordInput, '비밀번호를 입력해주세요.');
|
||||||
passwordError.textContent = '비밀번호는 최소 8자 이상이어야 합니다.';
|
isValid = false;
|
||||||
passwordError.classList.remove('hidden');
|
} else if (!MeetingApp.Validator.minLength(password, 6)) {
|
||||||
document.getElementById('password').classList.add('error');
|
MeetingApp.Validator.showError(passwordInput, '비밀번호는 최소 6자 이상이어야 합니다.');
|
||||||
hasError = true;
|
isValid = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasError) {
|
if (!isValid) return;
|
||||||
return;
|
|
||||||
|
// 로딩 표시
|
||||||
|
const submitButton = loginForm.querySelector('button[type="submit"]');
|
||||||
|
const originalText = submitButton.textContent;
|
||||||
|
submitButton.disabled = true;
|
||||||
|
submitButton.innerHTML = '<div class="spinner spinner-sm" style="border-color: white; border-top-color: transparent;"></div>';
|
||||||
|
|
||||||
|
try {
|
||||||
|
// API 호출 시뮬레이션
|
||||||
|
await MeetingApp.API.post('/api/auth/login', { email, password });
|
||||||
|
|
||||||
|
// 로그인 성공 시뮬레이션 (테스트 계정 체크)
|
||||||
|
if (email === 'test@example.com' && password === 'password123') {
|
||||||
|
// 사용자 정보 저장
|
||||||
|
MeetingApp.Storage.set('currentUser', {
|
||||||
|
id: 'user-001',
|
||||||
|
name: '김민준',
|
||||||
|
email: email,
|
||||||
|
avatar: 'https://ui-avatars.com/api/?name=김민준&background=00D9B1&color=fff',
|
||||||
|
role: 'user'
|
||||||
|
});
|
||||||
|
|
||||||
|
// 로그인 상태 유지 체크
|
||||||
|
if (rememberMeCheckbox.checked) {
|
||||||
|
MeetingApp.Storage.set('savedEmail', email);
|
||||||
|
MeetingApp.Storage.set('rememberMe', true);
|
||||||
|
} else {
|
||||||
|
MeetingApp.Storage.remove('savedEmail');
|
||||||
|
MeetingApp.Storage.remove('rememberMe');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 로그인 처리 (시뮬레이션)
|
// JWT 토큰 시뮬레이션
|
||||||
const users = getFromStorage('users') || [];
|
MeetingApp.Storage.set('authToken', 'mock-jwt-token-' + Date.now());
|
||||||
const user = users.find(u => u.email === email);
|
|
||||||
|
|
||||||
if (!user) {
|
// 성공 토스트
|
||||||
showToast('이메일 또는 비밀번호가 일치하지 않습니다.', 'error');
|
MeetingApp.Toast.success('로그인에 성공했습니다!');
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 로그인 성공
|
// 대시보드로 이동
|
||||||
saveToStorage('currentUser', user);
|
|
||||||
|
|
||||||
if (rememberMe) {
|
|
||||||
localStorage.setItem('rememberMe', 'true');
|
|
||||||
}
|
|
||||||
|
|
||||||
showToast('로그인 성공!', 'success');
|
|
||||||
|
|
||||||
// 0.5초 후 대시보드로 이동
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
navigateTo('02-대시보드.html');
|
window.location.href = '02-대시보드.html';
|
||||||
}, 500);
|
}, 1000);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// 로그인 실패
|
||||||
|
MeetingApp.Toast.error('이메일 또는 비밀번호가 올바르지 않습니다.');
|
||||||
|
submitButton.disabled = false;
|
||||||
|
submitButton.textContent = originalText;
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Login error:', error);
|
||||||
|
MeetingApp.Toast.error('로그인 중 오류가 발생했습니다. 다시 시도해주세요.');
|
||||||
|
submitButton.disabled = false;
|
||||||
|
submitButton.textContent = originalText;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 소셜 로그인 (시뮬레이션)
|
// 비밀번호 찾기 (프로토타입용)
|
||||||
document.getElementById('googleLogin').addEventListener('click', function() {
|
document.querySelector('.forgot-password').addEventListener('click', (e) => {
|
||||||
showToast('Google 로그인은 프로토타입에서 지원하지 않습니다.', 'info');
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById('microsoftLogin').addEventListener('click', function() {
|
|
||||||
showToast('Microsoft 로그인은 프로토타입에서 지원하지 않습니다.', 'info');
|
|
||||||
});
|
|
||||||
|
|
||||||
// 비밀번호 찾기
|
|
||||||
document.querySelector('.forgot-password').addEventListener('click', function(e) {
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
showToast('비밀번호 재설정 이메일이 발송되었습니다.', 'success');
|
MeetingApp.Toast.info('비밀번호 찾기 기능은 준비 중입니다.');
|
||||||
|
});
|
||||||
|
|
||||||
|
// 회원가입 (프로토타입용)
|
||||||
|
document.querySelector('.login-footer a').addEventListener('click', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
MeetingApp.Toast.info('회원가입 기능은 준비 중입니다.');
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -3,697 +3,131 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>회의 예약 - 회의록 작성 및 공유 서비스</title>
|
<title>회의 예약 - 회의록 서비스</title>
|
||||||
<link rel="stylesheet" href="common.css">
|
<link rel="stylesheet" href="common.css">
|
||||||
<style>
|
<style>
|
||||||
.reservation-container {
|
body { background-color: var(--color-gray-50); }
|
||||||
max-width: 1200px;
|
.page-container {
|
||||||
|
max-width: 800px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: var(--spacing-8);
|
padding: var(--spacing-8) var(--spacing-4);
|
||||||
}
|
}
|
||||||
|
.page-header {
|
||||||
.progress-bar {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--spacing-4);
|
|
||||||
margin-bottom: var(--spacing-8);
|
margin-bottom: var(--spacing-8);
|
||||||
}
|
}
|
||||||
|
.page-title {
|
||||||
.progress-step {
|
font-size: var(--font-size-h1);
|
||||||
display: flex;
|
color: var(--color-gray-900);
|
||||||
align-items: center;
|
margin-bottom: var(--spacing-2);
|
||||||
gap: var(--spacing-2);
|
|
||||||
}
|
}
|
||||||
|
.page-subtitle {
|
||||||
.progress-circle {
|
font-size: var(--font-size-body);
|
||||||
width: 32px;
|
color: var(--color-gray-500);
|
||||||
height: 32px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background-color: var(--gray-300);
|
|
||||||
color: white;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
}
|
||||||
|
.form-container {
|
||||||
.progress-circle.active {
|
background-color: var(--color-white);
|
||||||
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);
|
border-radius: var(--radius-lg);
|
||||||
padding: var(--spacing-8);
|
padding: var(--spacing-8);
|
||||||
box-shadow: var(--shadow-sm);
|
box-shadow: var(--shadow-sm);
|
||||||
}
|
}
|
||||||
|
.button-group {
|
||||||
.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;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
gap: var(--spacing-3);
|
||||||
gap: var(--spacing-2);
|
margin-top: var(--spacing-6);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
.page-title { font-size: var(--font-size-h2); }
|
||||||
|
.form-container { padding: var(--spacing-5); }
|
||||||
|
.button-group { flex-direction: column; }
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="reservation-container">
|
<div class="page-container">
|
||||||
<!-- 진행 단계 표시 -->
|
<div class="page-header">
|
||||||
<div class="progress-bar">
|
<h1 class="page-title">회의 예약</h1>
|
||||||
<div class="progress-step">
|
<p class="page-subtitle">새로운 회의를 예약하고 참석자를 초대하세요</p>
|
||||||
<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>
|
||||||
|
|
||||||
<div class="reservation-layout">
|
<div class="form-container">
|
||||||
<!-- 입력 폼 -->
|
<form id="meetingForm">
|
||||||
<div class="form-section">
|
<div class="form-group">
|
||||||
<h1>회의 예약</h1>
|
<label for="title" class="form-label">회의 제목 *</label>
|
||||||
<p style="color: var(--gray-500); margin-bottom: var(--spacing-8);">
|
<input type="text" id="title" class="form-input" placeholder="예: 2025년 1분기 기획 회의" required maxlength="100">
|
||||||
회의 정보를 입력하고 참석자를 초대하세요
|
|
||||||
</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>
|
||||||
|
|
||||||
<!-- 날짜 및 시간 -->
|
<div class="form-group">
|
||||||
<div class="datetime-group">
|
<label for="date" class="form-label">날짜 *</label>
|
||||||
<div class="input-group">
|
<input type="date" id="date" class="form-input" required>
|
||||||
<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>
|
||||||
|
|
||||||
<div class="input-group">
|
<div class="form-group">
|
||||||
<label class="input-label" for="startTime">
|
<label for="time" class="form-label">시간 *</label>
|
||||||
시작 시간 <span style="color: var(--error-main);">*</span>
|
<input type="time" id="time" class="form-input" required>
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="time"
|
|
||||||
id="startTime"
|
|
||||||
class="input"
|
|
||||||
required>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="datetime-group">
|
<div class="form-group">
|
||||||
<div class="input-group">
|
<label for="location" class="form-label">장소</label>
|
||||||
<label class="input-label" for="endTime">
|
<input type="text" id="location" class="form-input" placeholder="예: 본사 2층 대회의실" maxlength="200">
|
||||||
종료 시간 <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>
|
||||||
|
|
||||||
<div class="input-group">
|
<div class="form-group">
|
||||||
<label class="input-label"> </label>
|
<label for="attendees" class="form-label">참석자 (이메일, 쉼표로 구분) *</label>
|
||||||
<div style="padding: var(--spacing-3) 0; color: var(--gray-500); font-size: 0.875rem;">
|
<input type="text" id="attendees" class="form-input" placeholder="예: user1@example.com, user2@example.com" required>
|
||||||
예상 소요: <span id="duration">-</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 장소 -->
|
<div class="form-group">
|
||||||
<div class="input-group">
|
<label for="description" class="form-label">회의 설명</label>
|
||||||
<label class="input-label" for="location">장소</label>
|
<textarea id="description" class="form-textarea" placeholder="회의 목적과 안건을 간략히 작성하세요"></textarea>
|
||||||
<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>
|
</div>
|
||||||
|
|
||||||
<input
|
<div class="button-group">
|
||||||
type="url"
|
<button type="submit" class="btn btn-primary" style="flex: 1;">회의 예약하기</button>
|
||||||
id="onlineLink"
|
<button type="button" class="btn btn-secondary" onclick="history.back()">취소</button>
|
||||||
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>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
<script src="common.js"></script>
|
<script src="common.js"></script>
|
||||||
<script>
|
<script>
|
||||||
// 참석자 목록
|
const form = document.getElementById('meetingForm');
|
||||||
let participants = [];
|
|
||||||
|
|
||||||
// 오늘 날짜를 최소값으로 설정
|
// 최소 날짜를 오늘로 설정
|
||||||
const today = new Date().toISOString().split('T')[0];
|
document.getElementById('date').min = new Date().toISOString().split('T')[0];
|
||||||
document.getElementById('meetingDate').min = today;
|
|
||||||
document.getElementById('meetingDate').value = today;
|
|
||||||
document.getElementById('repeatEnd').min = today;
|
|
||||||
|
|
||||||
// 시작 시간 기본값 설정
|
form.addEventListener('submit', async (e) => {
|
||||||
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();
|
e.preventDefault();
|
||||||
|
|
||||||
// 유효성 검사
|
const title = document.getElementById('title').value.trim();
|
||||||
const title = document.getElementById('meetingTitle').value.trim();
|
const date = document.getElementById('date').value;
|
||||||
const date = document.getElementById('meetingDate').value;
|
const time = document.getElementById('time').value;
|
||||||
|
const location = document.getElementById('location').value.trim();
|
||||||
|
const attendees = document.getElementById('attendees').value.trim();
|
||||||
|
const description = document.getElementById('description').value.trim();
|
||||||
|
|
||||||
if (!title) {
|
// 새 회의 생성
|
||||||
document.getElementById('titleError').textContent = '회의 제목을 입력하세요.';
|
const newMeeting = {
|
||||||
document.getElementById('titleError').classList.remove('hidden');
|
id: 'm-' + Date.now(),
|
||||||
document.getElementById('meetingTitle').classList.add('error');
|
title,
|
||||||
document.getElementById('meetingTitle').focus();
|
date: `${date} ${time}`,
|
||||||
return;
|
location: location || '미정',
|
||||||
}
|
|
||||||
|
|
||||||
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',
|
status: 'scheduled',
|
||||||
createdAt: new Date().toISOString()
|
attendees: attendees.split(',').map(email => email.trim()),
|
||||||
|
description: description || ''
|
||||||
};
|
};
|
||||||
|
|
||||||
// LocalStorage에 임시 저장
|
// 저장
|
||||||
sessionStorage.setItem('newMeeting', JSON.stringify(meetingData));
|
const meetings = MeetingApp.Storage.get('meetings', []);
|
||||||
|
meetings.unshift(newMeeting);
|
||||||
|
MeetingApp.Storage.set('meetings', meetings);
|
||||||
|
|
||||||
showToast('회의 정보가 저장되었습니다.', 'success');
|
MeetingApp.Toast.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(() => {
|
setTimeout(() => {
|
||||||
navigateTo('02-대시보드.html');
|
window.location.href = '04-템플릿선택.html?meetingId=' + newMeeting.id;
|
||||||
}, 500);
|
}, 1000);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 초기 업데이트
|
|
||||||
calculateDuration();
|
|
||||||
updatePreview();
|
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -3,535 +3,233 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>템플릿 선택 - 회의록 작성 및 공유 서비스</title>
|
<title>템플릿 선택 - 회의록 서비스</title>
|
||||||
<link rel="stylesheet" href="common.css">
|
<link rel="stylesheet" href="common.css">
|
||||||
<style>
|
<style>
|
||||||
.template-container {
|
body { background-color: var(--color-gray-50); }
|
||||||
max-width: 1536px;
|
.page-container {
|
||||||
|
max-width: 1024px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: var(--spacing-8);
|
padding: var(--spacing-8) var(--spacing-4);
|
||||||
}
|
}
|
||||||
|
.page-header {
|
||||||
.progress-bar {
|
margin-bottom: var(--spacing-8);
|
||||||
display: flex;
|
text-align: center;
|
||||||
justify-content: center;
|
}
|
||||||
align-items: center;
|
.page-title {
|
||||||
gap: var(--spacing-4);
|
font-size: var(--font-size-h1);
|
||||||
|
color: var(--color-gray-900);
|
||||||
|
margin-bottom: var(--spacing-2);
|
||||||
|
}
|
||||||
|
.page-subtitle {
|
||||||
|
font-size: var(--font-size-body);
|
||||||
|
color: var(--color-gray-500);
|
||||||
|
}
|
||||||
|
.template-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||||
|
gap: var(--spacing-6);
|
||||||
margin-bottom: var(--spacing-8);
|
margin-bottom: var(--spacing-8);
|
||||||
}
|
}
|
||||||
|
.template-card {
|
||||||
.progress-step {
|
background: var(--color-white);
|
||||||
display: flex;
|
border: 2px solid var(--color-gray-200);
|
||||||
align-items: center;
|
border-radius: var(--radius-lg);
|
||||||
gap: var(--spacing-2);
|
padding: var(--spacing-6);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all var(--transition-base);
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
.template-card:hover {
|
||||||
.progress-circle {
|
border-color: var(--color-primary-main);
|
||||||
width: 32px;
|
box-shadow: var(--shadow-md);
|
||||||
height: 32px;
|
transform: translateY(-4px);
|
||||||
border-radius: 50%;
|
}
|
||||||
background-color: var(--gray-300);
|
.template-card.selected {
|
||||||
color: white;
|
border-color: var(--color-primary-main);
|
||||||
|
border-width: 3px;
|
||||||
|
background-color: rgba(0, 217, 177, 0.05);
|
||||||
|
}
|
||||||
|
.template-card.selected::after {
|
||||||
|
content: '✓';
|
||||||
|
position: absolute;
|
||||||
|
top: var(--spacing-3);
|
||||||
|
right: var(--spacing-3);
|
||||||
|
background-color: var(--color-primary-main);
|
||||||
|
color: var(--color-white);
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
border-radius: var(--radius-full);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
font-weight: 600;
|
font-weight: var(--font-weight-bold);
|
||||||
}
|
}
|
||||||
|
|
||||||
.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 {
|
.template-icon {
|
||||||
font-size: 32px;
|
font-size: 48px;
|
||||||
}
|
|
||||||
|
|
||||||
.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);
|
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;
|
text-align: center;
|
||||||
padding: var(--spacing-8);
|
|
||||||
color: var(--gray-400);
|
|
||||||
}
|
}
|
||||||
|
.template-title {
|
||||||
.section-list {
|
font-size: var(--font-size-h4);
|
||||||
margin-top: var(--spacing-4);
|
font-weight: var(--font-weight-semibold);
|
||||||
}
|
color: var(--color-gray-900);
|
||||||
|
|
||||||
.section-drag-item {
|
|
||||||
background: var(--gray-50);
|
|
||||||
padding: var(--spacing-3);
|
|
||||||
margin-bottom: var(--spacing-2);
|
margin-bottom: var(--spacing-2);
|
||||||
border-radius: var(--radius-md);
|
}
|
||||||
|
.template-description {
|
||||||
|
font-size: var(--font-size-body-small);
|
||||||
|
color: var(--color-gray-600);
|
||||||
|
line-height: var(--line-height-relaxed);
|
||||||
|
margin-bottom: var(--spacing-4);
|
||||||
|
}
|
||||||
|
.template-sections {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
flex-wrap: wrap;
|
||||||
gap: var(--spacing-2);
|
gap: var(--spacing-1);
|
||||||
cursor: move;
|
|
||||||
}
|
}
|
||||||
|
.section-tag {
|
||||||
.drag-handle {
|
font-size: var(--font-size-caption);
|
||||||
color: var(--gray-400);
|
padding: var(--spacing-1) var(--spacing-2);
|
||||||
cursor: grab;
|
background-color: var(--color-gray-100);
|
||||||
|
color: var(--color-gray-600);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
.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 {
|
.action-buttons {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
gap: var(--spacing-3);
|
||||||
margin-top: var(--spacing-8);
|
justify-content: center;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
.page-title { font-size: var(--font-size-h2); }
|
||||||
|
.template-grid { grid-template-columns: 1fr; }
|
||||||
|
.action-buttons { flex-direction: column; }
|
||||||
|
.action-buttons .btn { width: 100%; }
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="template-container">
|
<div class="page-container">
|
||||||
<!-- 진행 단계 표시 -->
|
<div class="page-header">
|
||||||
<div class="progress-bar">
|
<h1 class="page-title">회의록 템플릿 선택</h1>
|
||||||
<div class="progress-step">
|
<p class="page-subtitle">회의 유형에 맞는 템플릿을 선택하여 효율적으로 회의록을 작성하세요</p>
|
||||||
<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>
|
||||||
|
|
||||||
<h1>회의 템플릿 선택</h1>
|
<div class="template-grid">
|
||||||
<p style="color: var(--gray-500); margin-bottom: var(--spacing-8);">
|
<!-- 일반 회의 템플릿 -->
|
||||||
회의 유형에 맞는 템플릿을 선택하거나 커스터마이징하세요
|
<div class="template-card" data-template="general">
|
||||||
|
<div class="template-icon">📋</div>
|
||||||
|
<h3 class="template-title">일반 회의</h3>
|
||||||
|
<p class="template-description">
|
||||||
|
가장 기본적인 회의록 형식입니다. 모든 유형의 회의에 적합합니다.
|
||||||
</p>
|
</p>
|
||||||
|
<div class="template-sections">
|
||||||
<div class="template-layout">
|
<span class="section-tag">참석자</span>
|
||||||
<!-- 템플릿 그리드 -->
|
<span class="section-tag">안건</span>
|
||||||
<div>
|
<span class="section-tag">논의 내용</span>
|
||||||
<div class="template-grid" id="templateGrid"></div>
|
<span class="section-tag">결정 사항</span>
|
||||||
|
<span class="section-tag">Todo</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 미리보기 및 커스터마이징 -->
|
<!-- 스크럼 회의 템플릿 -->
|
||||||
<div class="preview-panel">
|
<div class="template-card" data-template="scrum">
|
||||||
<div class="preview-title">템플릿 미리보기</div>
|
<div class="template-icon">🏃</div>
|
||||||
|
<h3 class="template-title">스크럼 회의</h3>
|
||||||
<div id="previewContent" class="preview-empty">
|
<p class="template-description">
|
||||||
템플릿을 선택하면<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>
|
</p>
|
||||||
|
<div class="template-sections">
|
||||||
|
<span class="section-tag">어제 한 일</span>
|
||||||
|
<span class="section-tag">오늘 할 일</span>
|
||||||
|
<span class="section-tag">이슈/블로커</span>
|
||||||
|
<span class="section-tag">다음 스프린트</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="section-list" id="sectionList"></div>
|
<!-- 프로젝트 킥오프 템플릿 -->
|
||||||
|
<div class="template-card" data-template="kickoff">
|
||||||
|
<div class="template-icon">🚀</div>
|
||||||
|
<h3 class="template-title">프로젝트 킥오프</h3>
|
||||||
|
<p class="template-description">
|
||||||
|
새 프로젝트 시작 시 필요한 모든 정보를 담는 템플릿입니다.
|
||||||
|
</p>
|
||||||
|
<div class="template-sections">
|
||||||
|
<span class="section-tag">프로젝트 개요</span>
|
||||||
|
<span class="section-tag">목표</span>
|
||||||
|
<span class="section-tag">일정</span>
|
||||||
|
<span class="section-tag">역할 분담</span>
|
||||||
|
<span class="section-tag">리스크</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<button type="button" class="btn btn-secondary btn-sm" style="width: 100%; margin-top: var(--spacing-4);" id="addSection">
|
<!-- 주간 회의 템플릿 -->
|
||||||
+ 섹션 추가
|
<div class="template-card" data-template="weekly">
|
||||||
</button>
|
<div class="template-icon">📅</div>
|
||||||
|
<h3 class="template-title">주간 회의</h3>
|
||||||
<button type="button" class="btn btn-text btn-sm" style="width: 100%; margin-top: var(--spacing-2);" id="saveTemplate">
|
<p class="template-description">
|
||||||
✓ 나만의 템플릿으로 저장
|
매주 반복되는 정기 회의에 적합한 템플릿입니다.
|
||||||
</button>
|
</p>
|
||||||
|
<div class="template-sections">
|
||||||
|
<span class="section-tag">주간 실적</span>
|
||||||
|
<span class="section-tag">주요 이슈</span>
|
||||||
|
<span class="section-tag">다음 주 계획</span>
|
||||||
|
<span class="section-tag">공지사항</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 액션 버튼 -->
|
|
||||||
<div class="action-buttons">
|
<div class="action-buttons">
|
||||||
<button type="button" class="btn btn-text" onclick="navigateTo('03-회의예약.html')">
|
<button type="button" class="btn btn-secondary" onclick="history.back()">이전으로</button>
|
||||||
← 뒤로
|
<button type="button" class="btn btn-primary" id="startMeetingBtn" disabled>
|
||||||
|
회의 시작하기
|
||||||
</button>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="common.js"></script>
|
<script src="common.js"></script>
|
||||||
<script>
|
<script>
|
||||||
let selectedTemplate = null;
|
let selectedTemplate = null;
|
||||||
let customSections = [];
|
const startBtn = document.getElementById('startMeetingBtn');
|
||||||
let draggedItem = null;
|
const templateCards = document.querySelectorAll('.template-card');
|
||||||
|
|
||||||
// 템플릿 아이콘 매핑
|
templateCards.forEach(card => {
|
||||||
const templateIcons = {
|
card.addEventListener('click', () => {
|
||||||
general: '📋',
|
// 기존 선택 해제
|
||||||
scrum: '🔄',
|
templateCards.forEach(c => c.classList.remove('selected'));
|
||||||
kickoff: '🚀',
|
|
||||||
weekly: '📅'
|
|
||||||
};
|
|
||||||
|
|
||||||
// 템플릿 렌더링
|
// 새로운 선택
|
||||||
function renderTemplates() {
|
card.classList.add('selected');
|
||||||
const templates = getAllTemplates();
|
selectedTemplate = card.getAttribute('data-template');
|
||||||
const grid = document.getElementById('templateGrid');
|
startBtn.disabled = false;
|
||||||
|
|
||||||
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');
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// 섹션 수정
|
startBtn.addEventListener('click', () => {
|
||||||
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) {
|
if (!selectedTemplate) {
|
||||||
showToast('템플릿을 선택하세요.', 'warning');
|
MeetingApp.Toast.warning('템플릿을 선택해주세요');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const template = getTemplate(selectedTemplate);
|
// URL에서 meetingId 가져오기
|
||||||
const finalTemplate = {
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
...template,
|
const meetingId = urlParams.get('meetingId');
|
||||||
sections: customSections
|
|
||||||
};
|
|
||||||
|
|
||||||
sessionStorage.setItem('selectedTemplate', JSON.stringify(finalTemplate));
|
// 선택한 템플릿 저장
|
||||||
showToast('템플릿이 선택되었습니다.', 'success');
|
MeetingApp.Storage.set('selectedTemplate', {
|
||||||
|
meetingId: meetingId,
|
||||||
|
template: selectedTemplate,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
|
||||||
|
MeetingApp.Toast.success('템플릿이 선택되었습니다');
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
navigateTo('05-회의진행.html');
|
window.location.href = '05-회의진행.html?meetingId=' + meetingId;
|
||||||
}, 500);
|
}, 500);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 초기화
|
// 페이지 로드 시 일반 회의 템플릿 기본 선택 (선택적)
|
||||||
renderTemplates();
|
// templateCards[0].click();
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -3,497 +3,181 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>검증 완료 - 회의록 작성 및 공유 서비스</title>
|
<title>검증 완료 - 회의록 서비스</title>
|
||||||
<link rel="stylesheet" href="common.css">
|
<link rel="stylesheet" href="common.css">
|
||||||
<style>
|
<style>
|
||||||
.verification-container {
|
body { background-color: var(--color-gray-50); }
|
||||||
max-width: 1200px;
|
.page-container {
|
||||||
|
max-width: 800px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: var(--spacing-8);
|
padding: var(--spacing-8) var(--spacing-4);
|
||||||
}
|
}
|
||||||
|
.completion-icon {
|
||||||
.verification-header {
|
text-align: center;
|
||||||
|
font-size: 80px;
|
||||||
|
margin-bottom: var(--spacing-6);
|
||||||
|
}
|
||||||
|
.page-title {
|
||||||
|
font-size: var(--font-size-h1);
|
||||||
|
color: var(--color-gray-900);
|
||||||
|
margin-bottom: var(--spacing-3);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.page-subtitle {
|
||||||
|
font-size: var(--font-size-body);
|
||||||
|
color: var(--color-gray-500);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin-bottom: var(--spacing-8);
|
margin-bottom: var(--spacing-8);
|
||||||
}
|
}
|
||||||
|
.stats-grid {
|
||||||
.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;
|
display: grid;
|
||||||
grid-template-columns: 1fr 400px;
|
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
|
||||||
gap: var(--spacing-8);
|
gap: var(--spacing-4);
|
||||||
|
margin-bottom: var(--spacing-8);
|
||||||
}
|
}
|
||||||
|
.stat-card {
|
||||||
.section-list-panel {
|
background: var(--color-white);
|
||||||
background: white;
|
border: 1px solid var(--color-gray-200);
|
||||||
border-radius: var(--radius-lg);
|
border-radius: var(--radius-lg);
|
||||||
padding: var(--spacing-6);
|
padding: var(--spacing-5);
|
||||||
box-shadow: var(--shadow-sm);
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
.stat-value {
|
||||||
.verification-item {
|
font-size: var(--font-size-h2);
|
||||||
padding: var(--spacing-4);
|
font-weight: var(--font-weight-bold);
|
||||||
margin-bottom: var(--spacing-3);
|
color: var(--color-primary-main);
|
||||||
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);
|
margin-bottom: var(--spacing-2);
|
||||||
}
|
}
|
||||||
|
.stat-label {
|
||||||
.section-name-verify {
|
font-size: var(--font-size-body-small);
|
||||||
font-weight: 600;
|
color: var(--color-gray-600);
|
||||||
color: var(--gray-900);
|
|
||||||
}
|
}
|
||||||
|
.summary-card {
|
||||||
.verification-status {
|
background: var(--color-white);
|
||||||
display: inline-flex;
|
border: 1px solid var(--color-gray-200);
|
||||||
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);
|
border-radius: var(--radius-lg);
|
||||||
padding: var(--spacing-6);
|
padding: var(--spacing-6);
|
||||||
box-shadow: var(--shadow-sm);
|
margin-bottom: var(--spacing-6);
|
||||||
position: sticky;
|
|
||||||
top: var(--spacing-8);
|
|
||||||
}
|
}
|
||||||
|
.summary-title {
|
||||||
.stats-item {
|
font-size: var(--font-size-h4);
|
||||||
padding: var(--spacing-4);
|
font-weight: var(--font-weight-semibold);
|
||||||
background: var(--gray-50);
|
color: var(--color-gray-900);
|
||||||
|
margin-bottom: var(--spacing-4);
|
||||||
|
}
|
||||||
|
.keyword-list {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: var(--spacing-2);
|
||||||
|
}
|
||||||
|
.keyword-tag {
|
||||||
|
padding: var(--spacing-2) var(--spacing-3);
|
||||||
|
background-color: var(--color-primary-light);
|
||||||
|
color: var(--color-primary-dark);
|
||||||
border-radius: var(--radius-md);
|
border-radius: var(--radius-md);
|
||||||
margin-bottom: var(--spacing-3);
|
font-size: var(--font-size-body-small);
|
||||||
|
font-weight: var(--font-weight-medium);
|
||||||
}
|
}
|
||||||
|
|
||||||
.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 {
|
.action-buttons {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
gap: var(--spacing-3);
|
||||||
margin-top: var(--spacing-8);
|
justify-content: center;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
.completion-icon { font-size: 60px; }
|
||||||
|
.page-title { font-size: var(--font-size-h2); }
|
||||||
|
.stats-grid { grid-template-columns: repeat(2, 1fr); }
|
||||||
|
.action-buttons { flex-direction: column; }
|
||||||
|
.action-buttons .btn { width: 100%; }
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="verification-container">
|
<div class="page-container">
|
||||||
<div class="verification-header">
|
<div class="completion-icon">✅</div>
|
||||||
<div class="progress-circle-large">
|
<h1 class="page-title">AI 검증이 완료되었습니다</h1>
|
||||||
<svg class="progress-ring" width="120" height="120">
|
<p class="page-subtitle">회의 내용이 분석되었습니다. 통계를 확인하고 회의를 종료하세요</p>
|
||||||
<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="stats-grid">
|
||||||
<div class="section-list-panel">
|
<div class="stat-card">
|
||||||
<h3 style="margin-bottom: var(--spacing-4);">검증 항목</h3>
|
<div class="stat-value">45분</div>
|
||||||
<div id="verificationList"></div>
|
<div class="stat-label">회의 시간</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
<!-- 통계 패널 -->
|
<div class="stat-value">3명</div>
|
||||||
<div class="stats-panel">
|
<div class="stat-label">참석자</div>
|
||||||
<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>
|
||||||
|
<div class="stat-card">
|
||||||
<div class="stats-item">
|
<div class="stat-value">12회</div>
|
||||||
<div class="stats-label">검증 완료</div>
|
<div class="stat-label">발언 횟수</div>
|
||||||
<div class="stats-value">
|
</div>
|
||||||
<span id="verifiedCount">0</span> / <span id="totalCount">0</span> 섹션
|
<div class="stat-card">
|
||||||
|
<div class="stat-value">5개</div>
|
||||||
|
<div class="stat-label">Todo 생성</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="stats-item">
|
<!-- 주요 키워드 -->
|
||||||
<div class="stats-label">잠금 섹션</div>
|
<div class="summary-card">
|
||||||
<div class="stats-value" id="lockedCount">0</div>
|
<h2 class="summary-title">주요 키워드</h2>
|
||||||
|
<div class="keyword-list">
|
||||||
|
<span class="keyword-tag">신규 기능</span>
|
||||||
|
<span class="keyword-tag">개발 일정</span>
|
||||||
|
<span class="keyword-tag">API 설계</span>
|
||||||
|
<span class="keyword-tag">예산</span>
|
||||||
|
<span class="keyword-tag">테스트</span>
|
||||||
|
<span class="keyword-tag">배포</span>
|
||||||
|
<span class="keyword-tag">마케팅</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="participant-progress">
|
<!-- 발언 분포 -->
|
||||||
<div class="stats-label" style="margin-bottom: var(--spacing-3);">참석자별 진행률</div>
|
<div class="summary-card">
|
||||||
<div id="participantProgress"></div>
|
<h2 class="summary-title">발언 분포</h2>
|
||||||
|
<div style="margin-bottom: var(--spacing-3);">
|
||||||
|
<div style="display: flex; justify-content: space-between; margin-bottom: var(--spacing-1);">
|
||||||
|
<span style="font-size: var(--font-size-body-small); color: var(--color-gray-600);">김민준</span>
|
||||||
|
<span style="font-size: var(--font-size-body-small); color: var(--color-gray-600);">5회 (42%)</span>
|
||||||
|
</div>
|
||||||
|
<div style="height: 8px; background-color: var(--color-gray-200); border-radius: var(--radius-sm); overflow: hidden;">
|
||||||
|
<div style="width: 42%; height: 100%; background-color: var(--color-primary-main);"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="margin-bottom: var(--spacing-3);">
|
||||||
|
<div style="display: flex; justify-content: space-between; margin-bottom: var(--spacing-1);">
|
||||||
|
<span style="font-size: var(--font-size-body-small); color: var(--color-gray-600);">박서연</span>
|
||||||
|
<span style="font-size: var(--font-size-body-small); color: var(--color-gray-600);">4회 (33%)</span>
|
||||||
|
</div>
|
||||||
|
<div style="height: 8px; background-color: var(--color-gray-200); border-radius: var(--radius-sm); overflow: hidden;">
|
||||||
|
<div style="width: 33%; height: 100%; background-color: var(--color-secondary-main);"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div style="display: flex; justify-content: space-between; margin-bottom: var(--spacing-1);">
|
||||||
|
<span style="font-size: var(--font-size-body-small); color: var(--color-gray-600);">이준호</span>
|
||||||
|
<span style="font-size: var(--font-size-body-small); color: var(--color-gray-600);">3회 (25%)</span>
|
||||||
|
</div>
|
||||||
|
<div style="height: 8px; background-color: var(--color-gray-200); border-radius: var(--radius-sm); overflow: hidden;">
|
||||||
|
<div style="width: 25%; height: 100%; background-color: var(--color-info-main);"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 액션 버튼 -->
|
<!-- 액션 버튼 -->
|
||||||
<div class="action-buttons">
|
<div class="action-buttons">
|
||||||
<button type="button" class="btn btn-text" onclick="navigateTo('05-회의진행.html')">
|
<button class="btn btn-secondary" onclick="history.back()">회의로 돌아가기</button>
|
||||||
← 회의록으로 돌아가기
|
<button class="btn btn-primary" onclick="window.location.href='07-회의종료.html'">
|
||||||
</button>
|
회의 종료하기
|
||||||
<button type="button" class="btn btn-primary" id="completeVerification" disabled>
|
|
||||||
검증 완료 및 회의 종료 →
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="common.js"></script>
|
<script src="common.js"></script>
|
||||||
<script>
|
<script>
|
||||||
let sections = [];
|
MeetingApp.ready(() => {
|
||||||
let verifiedSections = new Set();
|
console.log('검증 완료 페이지 로드됨');
|
||||||
|
|
||||||
// 섹션 데이터 로드
|
|
||||||
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>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -3,571 +3,110 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>회의 종료 - 회의록 작성 및 공유 서비스</title>
|
<title>회의 종료 - 회의록 서비스</title>
|
||||||
<link rel="stylesheet" href="common.css">
|
<link rel="stylesheet" href="common.css">
|
||||||
<style>
|
<style>
|
||||||
.completion-container {
|
body { background-color: var(--color-gray-50); }
|
||||||
max-width: 1400px;
|
.page-container {
|
||||||
|
max-width: 600px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: var(--spacing-8);
|
padding: var(--spacing-8) var(--spacing-4);
|
||||||
}
|
|
||||||
|
|
||||||
.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;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
.completion-icon {
|
||||||
.stat-icon {
|
font-size: 100px;
|
||||||
font-size: 32px;
|
margin-bottom: var(--spacing-6);
|
||||||
|
}
|
||||||
|
.page-title {
|
||||||
|
font-size: var(--font-size-h1);
|
||||||
|
color: var(--color-gray-900);
|
||||||
margin-bottom: var(--spacing-3);
|
margin-bottom: var(--spacing-3);
|
||||||
}
|
}
|
||||||
|
.page-subtitle {
|
||||||
.stat-value {
|
font-size: var(--font-size-body);
|
||||||
font-size: 2rem;
|
color: var(--color-gray-500);
|
||||||
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);
|
margin-bottom: var(--spacing-8);
|
||||||
}
|
}
|
||||||
|
.info-card {
|
||||||
.content-panel {
|
background: var(--color-white);
|
||||||
background: white;
|
border: 1px solid var(--color-gray-200);
|
||||||
border-radius: var(--radius-lg);
|
border-radius: var(--radius-lg);
|
||||||
padding: var(--spacing-6);
|
padding: var(--spacing-6);
|
||||||
box-shadow: var(--shadow-sm);
|
margin-bottom: var(--spacing-6);
|
||||||
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
.info-item {
|
||||||
.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;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
margin-top: var(--spacing-8);
|
padding: var(--spacing-3) 0;
|
||||||
padding-top: var(--spacing-6);
|
border-bottom: 1px solid var(--color-gray-100);
|
||||||
border-top: 1px solid var(--gray-200);
|
|
||||||
}
|
}
|
||||||
|
.info-item:last-child {
|
||||||
@media (max-width: 1023px) {
|
border-bottom: none;
|
||||||
.stats-grid {
|
|
||||||
grid-template-columns: repeat(2, 1fr);
|
|
||||||
}
|
}
|
||||||
|
.info-label {
|
||||||
.content-grid {
|
font-weight: var(--font-weight-medium);
|
||||||
grid-template-columns: 1fr;
|
color: var(--color-gray-700);
|
||||||
}
|
}
|
||||||
|
.info-value {
|
||||||
|
color: var(--color-gray-900);
|
||||||
|
font-weight: var(--font-weight-semibold);
|
||||||
|
}
|
||||||
|
.action-buttons {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing-3);
|
||||||
|
}
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
.completion-icon { font-size: 80px; }
|
||||||
|
.page-title { font-size: var(--font-size-h2); }
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="completion-container">
|
<div class="page-container">
|
||||||
<div class="completion-header">
|
<div class="completion-icon">🏁</div>
|
||||||
<div class="success-icon">🎉</div>
|
<h1 class="page-title">회의가 종료되었습니다</h1>
|
||||||
<h1>회의가 종료되었습니다</h1>
|
<p class="page-subtitle">회의록이 자동으로 저장되었습니다</p>
|
||||||
<p style="color: var(--gray-500); font-size: 1.125rem;">
|
|
||||||
회의 통계와 Todo를 확인하고 최종 확정하세요
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 통계 카드 -->
|
<!-- 회의 정보 -->
|
||||||
<div class="stats-grid">
|
<div class="info-card">
|
||||||
<div class="stat-card">
|
<div class="info-item">
|
||||||
<div class="stat-icon">⏱️</div>
|
<span class="info-label">회의 제목</span>
|
||||||
<div class="stat-value" id="meetingDuration">-</div>
|
<span class="info-value">2025년 1분기 제품 기획 회의</span>
|
||||||
<div class="stat-label">회의 총 시간</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
<div class="stat-card">
|
<span class="info-label">회의 시간</span>
|
||||||
<div class="stat-icon">👥</div>
|
<span class="info-value">45분</span>
|
||||||
<div class="stat-value" id="participantCount">-</div>
|
|
||||||
<div class="stat-label">참석자 수</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
<div class="stat-card">
|
<span class="info-label">참석자</span>
|
||||||
<div class="stat-icon">💬</div>
|
<span class="info-value">3명</span>
|
||||||
<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 class="info-item">
|
||||||
|
<span class="info-label">생성된 Todo</span>
|
||||||
|
<span class="info-value">5개</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 액션 버튼 -->
|
<!-- 액션 버튼 -->
|
||||||
<div class="action-buttons">
|
<div class="action-buttons">
|
||||||
<button type="button" class="btn btn-text" onclick="navigateTo('05-회의진행.html')">
|
<button class="btn btn-primary" onclick="window.location.href='08-최종확정.html'">
|
||||||
← 회의록으로 돌아가기
|
회의록 확정하기
|
||||||
</button>
|
</button>
|
||||||
<div style="display: flex; gap: var(--spacing-3);">
|
<button class="btn btn-secondary" onclick="window.location.href='02-대시보드.html'">
|
||||||
<button type="button" class="btn btn-secondary" id="saveDraft">
|
대시보드로 이동
|
||||||
나중에 확정
|
|
||||||
</button>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="common.js"></script>
|
<script src="common.js"></script>
|
||||||
<script>
|
<script>
|
||||||
let extractedTodos = [];
|
MeetingApp.ready(() => {
|
||||||
|
console.log('회의 종료 페이지 로드됨');
|
||||||
// 회의 통계 로드
|
// 회의 종료 알림
|
||||||
function loadMeetingStats() {
|
MeetingApp.Toast.success('회의가 성공적으로 종료되었습니다');
|
||||||
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>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
303
design/uiux/prototype/08-최종확정.html
Normal file
303
design/uiux/prototype/08-최종확정.html
Normal file
@ -0,0 +1,303 @@
|
|||||||
|
<!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 { background-color: var(--color-gray-50); }
|
||||||
|
.page-container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: var(--spacing-8) var(--spacing-4);
|
||||||
|
}
|
||||||
|
.page-header {
|
||||||
|
margin-bottom: var(--spacing-8);
|
||||||
|
}
|
||||||
|
.page-title {
|
||||||
|
font-size: var(--font-size-h1);
|
||||||
|
color: var(--color-gray-900);
|
||||||
|
margin-bottom: var(--spacing-2);
|
||||||
|
}
|
||||||
|
.page-subtitle {
|
||||||
|
font-size: var(--font-size-body);
|
||||||
|
color: var(--color-gray-500);
|
||||||
|
}
|
||||||
|
.content-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 2fr 1fr;
|
||||||
|
gap: var(--spacing-6);
|
||||||
|
margin-bottom: var(--spacing-8);
|
||||||
|
}
|
||||||
|
.preview-panel {
|
||||||
|
background: var(--color-white);
|
||||||
|
border: 1px solid var(--color-gray-200);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
padding: var(--spacing-6);
|
||||||
|
}
|
||||||
|
.preview-title {
|
||||||
|
font-size: var(--font-size-h3);
|
||||||
|
font-weight: var(--font-weight-semibold);
|
||||||
|
color: var(--color-gray-900);
|
||||||
|
margin-bottom: var(--spacing-4);
|
||||||
|
}
|
||||||
|
.meeting-content {
|
||||||
|
font-size: var(--font-size-body);
|
||||||
|
line-height: var(--line-height-relaxed);
|
||||||
|
color: var(--color-gray-700);
|
||||||
|
}
|
||||||
|
.meeting-content h2 {
|
||||||
|
font-size: var(--font-size-h4);
|
||||||
|
margin-top: var(--spacing-6);
|
||||||
|
margin-bottom: var(--spacing-3);
|
||||||
|
color: var(--color-gray-900);
|
||||||
|
}
|
||||||
|
.meeting-content ul {
|
||||||
|
margin-left: var(--spacing-5);
|
||||||
|
margin-bottom: var(--spacing-4);
|
||||||
|
}
|
||||||
|
.checklist-panel {
|
||||||
|
background: var(--color-white);
|
||||||
|
border: 1px solid var(--color-gray-200);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
padding: var(--spacing-6);
|
||||||
|
height: fit-content;
|
||||||
|
}
|
||||||
|
.checklist-title {
|
||||||
|
font-size: var(--font-size-h4);
|
||||||
|
font-weight: var(--font-weight-semibold);
|
||||||
|
color: var(--color-gray-900);
|
||||||
|
margin-bottom: var(--spacing-4);
|
||||||
|
}
|
||||||
|
.checklist-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: var(--spacing-3);
|
||||||
|
padding: var(--spacing-3);
|
||||||
|
margin-bottom: var(--spacing-2);
|
||||||
|
background: var(--color-gray-50);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color var(--transition-fast);
|
||||||
|
}
|
||||||
|
.checklist-item:hover {
|
||||||
|
background: var(--color-gray-100);
|
||||||
|
}
|
||||||
|
.checklist-item.checked {
|
||||||
|
background: rgba(0, 217, 177, 0.1);
|
||||||
|
}
|
||||||
|
.checklist-checkbox {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border: 2px solid var(--color-gray-300);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.checklist-item.checked .checklist-checkbox {
|
||||||
|
background-color: var(--color-success-main);
|
||||||
|
border-color: var(--color-success-main);
|
||||||
|
color: var(--color-white);
|
||||||
|
}
|
||||||
|
.checklist-text {
|
||||||
|
flex: 1;
|
||||||
|
font-size: var(--font-size-body-small);
|
||||||
|
color: var(--color-gray-700);
|
||||||
|
}
|
||||||
|
.action-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spacing-3);
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.warning-message {
|
||||||
|
background-color: var(--color-warning-light);
|
||||||
|
border-left: 4px solid var(--color-warning-main);
|
||||||
|
padding: var(--spacing-4);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
margin-bottom: var(--spacing-4);
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.warning-message.show {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
@media (max-width: 1023px) {
|
||||||
|
.content-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
.page-title { font-size: var(--font-size-h2); }
|
||||||
|
.action-buttons { flex-direction: column; }
|
||||||
|
.action-buttons .btn { width: 100%; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="page-container">
|
||||||
|
<div class="page-header">
|
||||||
|
<h1 class="page-title">회의록 최종 확정</h1>
|
||||||
|
<p class="page-subtitle">필수 항목을 확인하고 회의록을 최종 확정하세요</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="warningMessage" class="warning-message">
|
||||||
|
⚠️ 아래 필수 항목을 모두 확인해주세요.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="content-grid">
|
||||||
|
<!-- 회의록 미리보기 -->
|
||||||
|
<div class="preview-panel">
|
||||||
|
<h2 class="preview-title">2025년 1분기 제품 기획 회의</h2>
|
||||||
|
<div class="meeting-content">
|
||||||
|
<p><strong>날짜:</strong> 2025-10-25 14:00<br>
|
||||||
|
<strong>장소:</strong> 본사 2층 대회의실<br>
|
||||||
|
<strong>참석자:</strong> 김민준, 박서연, 이준호</p>
|
||||||
|
|
||||||
|
<h2>안건</h2>
|
||||||
|
<ul>
|
||||||
|
<li>신규 기능 개발 일정 논의</li>
|
||||||
|
<li>예산 편성 검토</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2>논의 내용</h2>
|
||||||
|
<p>신규 회의록 서비스의 핵심 기능에 대해 논의했습니다. AI 기반 자동 작성 기능과 실시간 협업 기능을 우선적으로 개발하기로 결정했습니다.</p>
|
||||||
|
|
||||||
|
<p>개발 일정은 3월 말 완료를 목표로 하며, 주요 마일스톤은 다음과 같습니다:</p>
|
||||||
|
<ul>
|
||||||
|
<li>3월 10일: 기본 UI 완성</li>
|
||||||
|
<li>3월 20일: AI 기능 통합</li>
|
||||||
|
<li>3월 30일: 베타 테스트 시작</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2>결정 사항</h2>
|
||||||
|
<ul>
|
||||||
|
<li>신규 기능 개발은 3월 말 완료 목표</li>
|
||||||
|
<li>이준호님이 API 설계 담당</li>
|
||||||
|
<li>예산은 5천만원으로 확정</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2>Todo</h2>
|
||||||
|
<ul>
|
||||||
|
<li>API 명세서 작성 (담당: 이준호, 마감: 3월 25일)</li>
|
||||||
|
<li>UI 프로토타입 완성 (담당: 최유진, 마감: 3월 15일)</li>
|
||||||
|
<li>예산 편성안 검토 (담당: 박서연, 마감: 3월 20일)</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 확인 체크리스트 -->
|
||||||
|
<div class="checklist-panel">
|
||||||
|
<h3 class="checklist-title">필수 항목 확인</h3>
|
||||||
|
|
||||||
|
<div class="checklist-item" data-required="true">
|
||||||
|
<div class="checklist-checkbox"></div>
|
||||||
|
<div class="checklist-text">
|
||||||
|
<strong>회의 제목</strong><br>
|
||||||
|
회의 제목이 명확하게 작성되었습니다
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="checklist-item" data-required="true">
|
||||||
|
<div class="checklist-checkbox"></div>
|
||||||
|
<div class="checklist-text">
|
||||||
|
<strong>참석자 목록</strong><br>
|
||||||
|
모든 참석자가 기록되었습니다
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="checklist-item" data-required="true">
|
||||||
|
<div class="checklist-checkbox"></div>
|
||||||
|
<div class="checklist-text">
|
||||||
|
<strong>주요 논의 내용</strong><br>
|
||||||
|
핵심 논의 내용이 포함되었습니다
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="checklist-item" data-required="true">
|
||||||
|
<div class="checklist-checkbox"></div>
|
||||||
|
<div class="checklist-text">
|
||||||
|
<strong>결정 사항</strong><br>
|
||||||
|
회의 중 결정된 사항이 명시되었습니다
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="checklist-item">
|
||||||
|
<div class="checklist-checkbox"></div>
|
||||||
|
<div class="checklist-text">
|
||||||
|
<strong>Todo 생성</strong><br>
|
||||||
|
실행 항목이 Todo로 생성되었습니다
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="checklist-item">
|
||||||
|
<div class="checklist-checkbox"></div>
|
||||||
|
<div class="checklist-text">
|
||||||
|
<strong>전문용어 설명</strong><br>
|
||||||
|
필요한 용어에 설명이 추가되었습니다
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 액션 버튼 -->
|
||||||
|
<div class="action-buttons">
|
||||||
|
<button class="btn btn-secondary" onclick="history.back()">이전으로</button>
|
||||||
|
<button class="btn btn-primary" id="confirmBtn" disabled>회의록 확정하기</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="common.js"></script>
|
||||||
|
<script>
|
||||||
|
const checklistItems = document.querySelectorAll('.checklist-item');
|
||||||
|
const confirmBtn = document.getElementById('confirmBtn');
|
||||||
|
const warningMessage = document.getElementById('warningMessage');
|
||||||
|
|
||||||
|
// 체크리스트 항목 클릭
|
||||||
|
checklistItems.forEach(item => {
|
||||||
|
item.addEventListener('click', () => {
|
||||||
|
item.classList.toggle('checked');
|
||||||
|
const checkbox = item.querySelector('.checklist-checkbox');
|
||||||
|
if (item.classList.contains('checked')) {
|
||||||
|
checkbox.textContent = '✓';
|
||||||
|
} else {
|
||||||
|
checkbox.textContent = '';
|
||||||
|
}
|
||||||
|
checkCompletion();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 완료 여부 확인
|
||||||
|
function checkCompletion() {
|
||||||
|
const requiredItems = document.querySelectorAll('.checklist-item[data-required="true"]');
|
||||||
|
const checkedRequired = document.querySelectorAll('.checklist-item[data-required="true"].checked');
|
||||||
|
|
||||||
|
if (requiredItems.length === checkedRequired.length) {
|
||||||
|
confirmBtn.disabled = false;
|
||||||
|
warningMessage.classList.remove('show');
|
||||||
|
} else {
|
||||||
|
confirmBtn.disabled = true;
|
||||||
|
warningMessage.classList.add('show');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 확정 버튼 클릭
|
||||||
|
confirmBtn.addEventListener('click', () => {
|
||||||
|
MeetingApp.Loading.show();
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
MeetingApp.Loading.hide();
|
||||||
|
MeetingApp.Toast.success('회의록이 확정되었습니다!');
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
window.location.href = '09-회의록공유.html';
|
||||||
|
}, 1000);
|
||||||
|
}, 1500);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 초기 확인
|
||||||
|
checkCompletion();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
File diff suppressed because it is too large
Load Diff
872
design/uiux/prototype/common.js
vendored
872
design/uiux/prototype/common.js
vendored
@ -1,408 +1,556 @@
|
|||||||
/* ============================================
|
/*
|
||||||
회의록 작성 및 공유 개선 서비스 - 공통 Javascript
|
* 회의록 작성 및 공유 개선 서비스 - 공통 자바스크립트
|
||||||
============================================ */
|
* 버전: 1.0
|
||||||
|
* 작성일: 2025-10-20
|
||||||
|
* 레퍼런스: 스타일 가이드 v1.0
|
||||||
|
*/
|
||||||
|
|
||||||
// === LocalStorage 키 상수 ===
|
// ===== 전역 상태 관리 =====
|
||||||
const STORAGE_KEYS = {
|
const AppState = {
|
||||||
CURRENT_USER: 'currentUser',
|
currentUser: {
|
||||||
USERS: 'users',
|
id: 'user-001',
|
||||||
MEETINGS: 'meetings',
|
name: '김민준',
|
||||||
TODOS: 'todos',
|
email: 'minjun.kim@example.com',
|
||||||
INITIALIZED: 'initialized'
|
avatar: 'https://ui-avatars.com/api/?name=김민준&background=00D9B1&color=fff'
|
||||||
|
},
|
||||||
|
meetings: [],
|
||||||
|
todos: []
|
||||||
};
|
};
|
||||||
|
|
||||||
// === 예제 데이터 초기화 ===
|
// ===== 유틸리티 함수 =====
|
||||||
function initializeData() {
|
|
||||||
if (localStorage.getItem(STORAGE_KEYS.INITIALIZED)) {
|
/**
|
||||||
return;
|
* DOM 준비 완료 시 콜백 실행
|
||||||
|
*/
|
||||||
|
function ready(callback) {
|
||||||
|
if (document.readyState !== 'loading') {
|
||||||
|
callback();
|
||||||
|
} else {
|
||||||
|
document.addEventListener('DOMContentLoaded', callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 사용자 데이터
|
|
||||||
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) {
|
* 날짜 포맷팅 (YYYY-MM-DD HH:mm)
|
||||||
const data = localStorage.getItem(key);
|
*/
|
||||||
return data ? JSON.parse(data) : null;
|
function formatDateTime(date) {
|
||||||
|
const d = new Date(date);
|
||||||
|
const year = d.getFullYear();
|
||||||
|
const month = String(d.getMonth() + 1).padStart(2, '0');
|
||||||
|
const day = String(d.getDate()).padStart(2, '0');
|
||||||
|
const hours = String(d.getHours()).padStart(2, '0');
|
||||||
|
const minutes = String(d.getMinutes()).padStart(2, '0');
|
||||||
|
return `${year}-${month}-${day} ${hours}:${minutes}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveToStorage(key, data) {
|
/**
|
||||||
localStorage.setItem(key, JSON.stringify(data));
|
* 상대 시간 표현 (방금 전, 3분 전, 2시간 전 등)
|
||||||
|
*/
|
||||||
|
function timeAgo(date) {
|
||||||
|
const now = new Date();
|
||||||
|
const past = new Date(date);
|
||||||
|
const diff = Math.floor((now - past) / 1000); // 초 단위
|
||||||
|
|
||||||
|
if (diff < 60) return '방금 전';
|
||||||
|
if (diff < 3600) return `${Math.floor(diff / 60)}분 전`;
|
||||||
|
if (diff < 86400) return `${Math.floor(diff / 3600)}시간 전`;
|
||||||
|
if (diff < 2592000) return `${Math.floor(diff / 86400)}일 전`;
|
||||||
|
if (diff < 31536000) return `${Math.floor(diff / 2592000)}개월 전`;
|
||||||
|
return `${Math.floor(diff / 31536000)}년 전`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// === 날짜/시간 유틸리티 ===
|
/**
|
||||||
function formatDate(dateString) {
|
* D-day 계산
|
||||||
const date = new Date(dateString);
|
*/
|
||||||
const year = date.getFullYear();
|
function getDday(targetDate) {
|
||||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
const now = new Date();
|
||||||
const day = String(date.getDate()).padStart(2, '0');
|
now.setHours(0, 0, 0, 0);
|
||||||
return `${year}-${month}-${day}`;
|
const target = new Date(targetDate);
|
||||||
}
|
target.setHours(0, 0, 0, 0);
|
||||||
|
const diff = Math.floor((target - now) / (1000 * 60 * 60 * 24));
|
||||||
|
|
||||||
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 '오늘';
|
if (diff === 0) return '오늘';
|
||||||
return `D-${diff}`;
|
if (diff > 0) return `D-${diff}`;
|
||||||
|
return `D+${Math.abs(diff)} (지남)`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatDuration(minutes) {
|
/**
|
||||||
const hours = Math.floor(minutes / 60);
|
* UUID 생성 (간단한 버전)
|
||||||
const mins = minutes % 60;
|
*/
|
||||||
if (hours > 0 && mins > 0) return `${hours}시간 ${mins}분`;
|
function generateUUID() {
|
||||||
if (hours > 0) return `${hours}시간`;
|
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
||||||
return `${mins}분`;
|
const r = Math.random() * 16 | 0;
|
||||||
|
const v = c === 'x' ? r : (r & 0x3 | 0x8);
|
||||||
|
return v.toString(16);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// === 사용자 관련 함수 ===
|
// ===== 모달 관리 =====
|
||||||
function getCurrentUser() {
|
const Modal = {
|
||||||
return getFromStorage(STORAGE_KEYS.CURRENT_USER);
|
/**
|
||||||
}
|
* 모달 열기
|
||||||
|
*/
|
||||||
|
open(modalId) {
|
||||||
|
const modal = document.getElementById(modalId);
|
||||||
|
if (!modal) return;
|
||||||
|
|
||||||
function getUserById(userId) {
|
modal.style.display = 'flex';
|
||||||
const users = getFromStorage(STORAGE_KEYS.USERS) || [];
|
document.body.style.overflow = 'hidden';
|
||||||
return users.find(u => u.id === userId);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getUserName(userId) {
|
// backdrop 클릭 시 모달 닫기
|
||||||
const user = getUserById(userId);
|
const backdrop = modal.querySelector('.modal-backdrop');
|
||||||
return user ? user.name : '알 수 없음';
|
if (backdrop) {
|
||||||
}
|
backdrop.addEventListener('click', (e) => {
|
||||||
|
if (e.target === backdrop) {
|
||||||
// === 회의 관련 함수 ===
|
this.close(modalId);
|
||||||
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 closeBtn = modal.querySelector('.modal-close');
|
||||||
|
if (closeBtn) {
|
||||||
|
closeBtn.addEventListener('click', () => this.close(modalId));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 모달 닫기
|
||||||
|
*/
|
||||||
|
close(modalId) {
|
||||||
|
const modal = document.getElementById(modalId);
|
||||||
|
if (!modal) return;
|
||||||
|
|
||||||
|
modal.style.display = 'none';
|
||||||
|
document.body.style.overflow = 'auto';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ===== 토스트 알림 =====
|
||||||
|
const Toast = {
|
||||||
|
container: null,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 토스트 컨테이너 초기화
|
||||||
|
*/
|
||||||
|
init() {
|
||||||
|
if (!this.container) {
|
||||||
|
this.container = document.createElement('div');
|
||||||
|
this.container.className = 'toast-container';
|
||||||
|
document.body.appendChild(this.container);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 토스트 표시
|
||||||
|
*/
|
||||||
|
show(message, type = 'info', duration = 4000) {
|
||||||
|
this.init();
|
||||||
|
|
||||||
const toast = document.createElement('div');
|
const toast = document.createElement('div');
|
||||||
toast.className = `toast toast-${type}`;
|
toast.className = `toast toast-${type}`;
|
||||||
|
|
||||||
const icons = {
|
const icons = {
|
||||||
success: '✓',
|
success: '✓',
|
||||||
error: '✗',
|
error: '✕',
|
||||||
warning: '⚠',
|
warning: '⚠',
|
||||||
info: 'ℹ'
|
info: 'ℹ'
|
||||||
};
|
};
|
||||||
|
|
||||||
toast.innerHTML = `
|
toast.innerHTML = `
|
||||||
<span style="font-size: 20px;">${icons[type]}</span>
|
<div class="toast-icon">${icons[type] || icons.info}</div>
|
||||||
<span>${message}</span>
|
<div class="toast-content">
|
||||||
|
<div class="toast-message">${message}</div>
|
||||||
|
</div>
|
||||||
|
<button class="toast-close" onclick="this.parentElement.remove()">×</button>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
container.appendChild(toast);
|
this.container.appendChild(toast);
|
||||||
|
|
||||||
// 4초 후 자동 제거
|
// 자동 제거
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
toast.style.animation = 'slideOut 150ms ease-in';
|
if (toast.parentElement) {
|
||||||
setTimeout(() => toast.remove(), 150);
|
toast.remove();
|
||||||
}, 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;
|
|
||||||
}
|
}
|
||||||
}
|
}, duration);
|
||||||
`;
|
|
||||||
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',
|
success(message) { this.show(message, 'success'); },
|
||||||
name: '스크럼 회의',
|
error(message) { this.show(message, 'error'); },
|
||||||
description: '데일리 스탠드업',
|
warning(message) { this.show(message, 'warning'); },
|
||||||
sections: [
|
info(message) { this.show(message, 'info'); }
|
||||||
{ id: 'participants', name: '참석자', required: true },
|
};
|
||||||
{ id: 'yesterday', name: '어제 한 일', required: true },
|
|
||||||
{ id: 'today', name: '오늘 할 일', required: true },
|
// ===== 로컬 스토리지 관리 =====
|
||||||
{ id: 'blockers', name: '이슈/블로커', required: false }
|
const Storage = {
|
||||||
]
|
/**
|
||||||
|
* 데이터 저장
|
||||||
|
*/
|
||||||
|
set(key, value) {
|
||||||
|
try {
|
||||||
|
localStorage.setItem(key, JSON.stringify(value));
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Storage.set error:', e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
kickoff: {
|
|
||||||
id: 'kickoff',
|
/**
|
||||||
name: '프로젝트 킥오프',
|
* 데이터 가져오기
|
||||||
description: '프로젝트 시작 회의',
|
*/
|
||||||
sections: [
|
get(key, defaultValue = null) {
|
||||||
{ id: 'participants', name: '참석자', required: true },
|
try {
|
||||||
{ id: 'overview', name: '프로젝트 개요', required: true },
|
const item = localStorage.getItem(key);
|
||||||
{ id: 'goals', name: '목표', required: true },
|
return item ? JSON.parse(item) : defaultValue;
|
||||||
{ id: 'schedule', name: '일정', required: true },
|
} catch (e) {
|
||||||
{ id: 'roles', name: '역할 분담', required: true },
|
console.error('Storage.get error:', e);
|
||||||
{ id: 'risks', name: '리스크', required: false }
|
return defaultValue;
|
||||||
]
|
}
|
||||||
},
|
},
|
||||||
weekly: {
|
|
||||||
id: 'weekly',
|
/**
|
||||||
name: '주간 회의',
|
* 데이터 삭제
|
||||||
description: '주간 진행 상황 리뷰',
|
*/
|
||||||
sections: [
|
remove(key) {
|
||||||
{ id: 'participants', name: '참석자', required: true },
|
try {
|
||||||
{ id: 'achievements', name: '주간 실적', required: true },
|
localStorage.removeItem(key);
|
||||||
{ id: 'issues', name: '주요 이슈', required: false },
|
return true;
|
||||||
{ id: 'nextWeek', name: '다음 주 계획', required: true },
|
} catch (e) {
|
||||||
{ id: 'support', name: '지원 필요 사항', required: false }
|
console.error('Storage.remove error:', e);
|
||||||
]
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 전체 삭제
|
||||||
|
*/
|
||||||
|
clear() {
|
||||||
|
try {
|
||||||
|
localStorage.clear();
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Storage.clear error:', e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
function getTemplate(templateId) {
|
// ===== API 호출 (Mock) =====
|
||||||
return TEMPLATES[templateId];
|
const API = {
|
||||||
|
/**
|
||||||
|
* 지연 시뮬레이션
|
||||||
|
*/
|
||||||
|
delay(ms) {
|
||||||
|
return new Promise(resolve => setTimeout(resolve, ms));
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET 요청 (Mock)
|
||||||
|
*/
|
||||||
|
async get(endpoint) {
|
||||||
|
await this.delay(500);
|
||||||
|
console.log(`API GET: ${endpoint}`);
|
||||||
|
return { success: true, data: {} };
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST 요청 (Mock)
|
||||||
|
*/
|
||||||
|
async post(endpoint, data) {
|
||||||
|
await this.delay(500);
|
||||||
|
console.log(`API POST: ${endpoint}`, data);
|
||||||
|
return { success: true, data: {} };
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PUT 요청 (Mock)
|
||||||
|
*/
|
||||||
|
async put(endpoint, data) {
|
||||||
|
await this.delay(500);
|
||||||
|
console.log(`API PUT: ${endpoint}`, data);
|
||||||
|
return { success: true, data: {} };
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DELETE 요청 (Mock)
|
||||||
|
*/
|
||||||
|
async delete(endpoint) {
|
||||||
|
await this.delay(500);
|
||||||
|
console.log(`API DELETE: ${endpoint}`);
|
||||||
|
return { success: true };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ===== 페이지 네비게이션 =====
|
||||||
|
function navigateTo(page) {
|
||||||
|
// 실제로는 SPA 라우팅이나 페이지 이동 처리
|
||||||
|
// 프로토타입에서는 링크 클릭으로 처리
|
||||||
|
console.log(`Navigate to: ${page}`);
|
||||||
|
window.location.href = page;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getAllTemplates() {
|
// ===== 폼 유효성 검사 =====
|
||||||
return Object.values(TEMPLATES);
|
const Validator = {
|
||||||
}
|
/**
|
||||||
|
* 이메일 유효성 검사
|
||||||
|
*/
|
||||||
|
isEmail(email) {
|
||||||
|
const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||||
|
return re.test(email);
|
||||||
|
},
|
||||||
|
|
||||||
// === 페이지 로드 시 데이터 초기화 ===
|
/**
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
* 필수 입력 검사
|
||||||
initializeData();
|
*/
|
||||||
|
required(value) {
|
||||||
|
return value !== null && value !== undefined && value.trim() !== '';
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 최소 길이 검사
|
||||||
|
*/
|
||||||
|
minLength(value, min) {
|
||||||
|
return value && value.length >= min;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 최대 길이 검사
|
||||||
|
*/
|
||||||
|
maxLength(value, max) {
|
||||||
|
return value && value.length <= max;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 폼 필드 에러 표시
|
||||||
|
*/
|
||||||
|
showError(inputElement, message) {
|
||||||
|
inputElement.classList.add('error');
|
||||||
|
|
||||||
|
let errorElement = inputElement.nextElementSibling;
|
||||||
|
if (!errorElement || !errorElement.classList.contains('form-error')) {
|
||||||
|
errorElement = document.createElement('span');
|
||||||
|
errorElement.className = 'form-error';
|
||||||
|
inputElement.parentElement.appendChild(errorElement);
|
||||||
|
}
|
||||||
|
errorElement.textContent = message;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 폼 필드 에러 제거
|
||||||
|
*/
|
||||||
|
clearError(inputElement) {
|
||||||
|
inputElement.classList.remove('error');
|
||||||
|
|
||||||
|
const errorElement = inputElement.nextElementSibling;
|
||||||
|
if (errorElement && errorElement.classList.contains('form-error')) {
|
||||||
|
errorElement.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ===== 로딩 상태 관리 =====
|
||||||
|
const Loading = {
|
||||||
|
/**
|
||||||
|
* 로딩 표시
|
||||||
|
*/
|
||||||
|
show(target = 'body') {
|
||||||
|
const element = typeof target === 'string' ? document.querySelector(target) : target;
|
||||||
|
if (!element) return;
|
||||||
|
|
||||||
|
const spinner = document.createElement('div');
|
||||||
|
spinner.className = 'spinner';
|
||||||
|
spinner.id = 'global-spinner';
|
||||||
|
spinner.style.position = 'fixed';
|
||||||
|
spinner.style.top = '50%';
|
||||||
|
spinner.style.left = '50%';
|
||||||
|
spinner.style.transform = 'translate(-50%, -50%)';
|
||||||
|
spinner.style.zIndex = '9999';
|
||||||
|
|
||||||
|
document.body.appendChild(spinner);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 로딩 숨김
|
||||||
|
*/
|
||||||
|
hide() {
|
||||||
|
const spinner = document.getElementById('global-spinner');
|
||||||
|
if (spinner) {
|
||||||
|
spinner.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ===== 회의록 관련 유틸리티 =====
|
||||||
|
const MeetingUtils = {
|
||||||
|
/**
|
||||||
|
* 회의 상태 레이블
|
||||||
|
*/
|
||||||
|
getStatusLabel(status) {
|
||||||
|
const labels = {
|
||||||
|
'scheduled': '예정',
|
||||||
|
'in_progress': '진행 중',
|
||||||
|
'ended': '종료',
|
||||||
|
'draft': '작성 중',
|
||||||
|
'verifying': '검증 중',
|
||||||
|
'confirmed': '확정됨'
|
||||||
|
};
|
||||||
|
return labels[status] || status;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 회의 상태 클래스
|
||||||
|
*/
|
||||||
|
getStatusClass(status) {
|
||||||
|
const classes = {
|
||||||
|
'draft': 'status-draft',
|
||||||
|
'verifying': 'status-verifying',
|
||||||
|
'confirmed': 'status-confirmed'
|
||||||
|
};
|
||||||
|
return classes[status] || 'badge-neutral';
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Todo 우선순위 레이블
|
||||||
|
*/
|
||||||
|
getPriorityLabel(priority) {
|
||||||
|
const labels = {
|
||||||
|
'high': '높음',
|
||||||
|
'medium': '보통',
|
||||||
|
'low': '낮음'
|
||||||
|
};
|
||||||
|
return labels[priority] || priority;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Todo 상태 레이블
|
||||||
|
*/
|
||||||
|
getTodoStatusLabel(status) {
|
||||||
|
const labels = {
|
||||||
|
'todo': '시작 전',
|
||||||
|
'in_progress': '진행 중',
|
||||||
|
'done': '완료'
|
||||||
|
};
|
||||||
|
return labels[status] || status;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ===== 예시 데이터 생성 =====
|
||||||
|
const MockData = {
|
||||||
|
/**
|
||||||
|
* 샘플 회의 데이터
|
||||||
|
*/
|
||||||
|
generateMeetings() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: 'm-001',
|
||||||
|
title: '2025년 1분기 제품 기획 회의',
|
||||||
|
date: '2025-10-25 14:00',
|
||||||
|
location: '본사 2층 대회의실',
|
||||||
|
status: 'scheduled',
|
||||||
|
attendees: ['김민준', '박서연', '이준호', '최유진'],
|
||||||
|
description: '신규 회의록 서비스 기획 논의'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'm-002',
|
||||||
|
title: '주간 스크럼 회의',
|
||||||
|
date: '2025-10-21 10:00',
|
||||||
|
location: 'Zoom',
|
||||||
|
status: 'confirmed',
|
||||||
|
attendees: ['김민준', '이준호', '최유진'],
|
||||||
|
description: '지난 주 진행 상황 공유 및 이번 주 계획'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'm-003',
|
||||||
|
title: 'AI 기능 개선 회의',
|
||||||
|
date: '2025-10-23 15:00',
|
||||||
|
location: '본사 3층 소회의실',
|
||||||
|
status: 'in_progress',
|
||||||
|
attendees: ['박서연', '이준호'],
|
||||||
|
description: 'LLM 기반 회의록 자동 작성 개선 방안'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 샘플 Todo 데이터
|
||||||
|
*/
|
||||||
|
generateTodos() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: 't-001',
|
||||||
|
title: 'API 명세서 작성',
|
||||||
|
assignee: '이준호',
|
||||||
|
dueDate: '2025-10-25',
|
||||||
|
priority: 'high',
|
||||||
|
status: 'in_progress',
|
||||||
|
progress: 60,
|
||||||
|
meetingId: 'm-002'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 't-002',
|
||||||
|
title: 'UI 프로토타입 디자인',
|
||||||
|
assignee: '최유진',
|
||||||
|
dueDate: '2025-10-23',
|
||||||
|
priority: 'medium',
|
||||||
|
status: 'done',
|
||||||
|
progress: 100,
|
||||||
|
meetingId: 'm-002'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 't-003',
|
||||||
|
title: '데이터베이스 스키마 설계',
|
||||||
|
assignee: '이준호',
|
||||||
|
dueDate: '2025-10-28',
|
||||||
|
priority: 'high',
|
||||||
|
status: 'todo',
|
||||||
|
progress: 0,
|
||||||
|
meetingId: 'm-001'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ===== 초기화 =====
|
||||||
|
ready(() => {
|
||||||
|
console.log('Common.js loaded');
|
||||||
|
|
||||||
|
// 로컬 스토리지에서 상태 복원
|
||||||
|
const savedMeetings = Storage.get('meetings');
|
||||||
|
const savedTodos = Storage.get('todos');
|
||||||
|
|
||||||
|
if (!savedMeetings) {
|
||||||
|
AppState.meetings = MockData.generateMeetings();
|
||||||
|
Storage.set('meetings', AppState.meetings);
|
||||||
|
} else {
|
||||||
|
AppState.meetings = savedMeetings;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!savedTodos) {
|
||||||
|
AppState.todos = MockData.generateTodos();
|
||||||
|
Storage.set('todos', AppState.todos);
|
||||||
|
} else {
|
||||||
|
AppState.todos = savedTodos;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('AppState initialized:', AppState);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ===== Export (전역 네임스페이스) =====
|
||||||
|
window.MeetingApp = {
|
||||||
|
AppState,
|
||||||
|
Modal,
|
||||||
|
Toast,
|
||||||
|
Storage,
|
||||||
|
API,
|
||||||
|
Validator,
|
||||||
|
Loading,
|
||||||
|
MeetingUtils,
|
||||||
|
MockData,
|
||||||
|
navigateTo,
|
||||||
|
formatDateTime,
|
||||||
|
timeAgo,
|
||||||
|
getDday,
|
||||||
|
generateUUID,
|
||||||
|
ready
|
||||||
|
};
|
||||||
|
|||||||
@ -1,25 +1,46 @@
|
|||||||
# 회의록 작성 및 공유 개선 서비스 - 유저스토리
|
# 회의록 작성 및 공유 개선 서비스 - 유저스토리 (v2.0)
|
||||||
|
|
||||||
- [회의록 작성 및 공유 개선 서비스 - 유저스토리](#회의록-작성-및-공유-개선-서비스---유저스토리)
|
- [회의록 작성 및 공유 개선 서비스 - 유저스토리 (v2.0)](#회의록-작성-및-공유-개선-서비스---유저스토리-v20)
|
||||||
|
- [차별화 전략](#차별화-전략)
|
||||||
- [마이크로서비스 구성](#마이크로서비스-구성)
|
- [마이크로서비스 구성](#마이크로서비스-구성)
|
||||||
- [유저스토리](#유저스토리)
|
- [유저스토리](#유저스토리)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 차별화 전략
|
||||||
|
|
||||||
|
본 서비스는 다음과 같은 차별화 포인트를 통해 경쟁 우위를 확보합니다:
|
||||||
|
|
||||||
|
### 1. 기본 기능 (Hygiene Factors)
|
||||||
|
- **STT(Speech To Text)**: 음성을 텍스트로 변환하는 기본 기능
|
||||||
|
- 시장의 대부분 서비스가 제공하는 기능으로 차별화 포인트가 아님
|
||||||
|
- 필수 기능이지만 경쟁 우위를 가져다주지 않음
|
||||||
|
|
||||||
|
### 2. 핵심 차별화 포인트 (Differentiators)
|
||||||
|
- **맥락 기반 용어 설명**: 단순 용어 설명을 넘어, 관련 회의록과 업무이력을 바탕으로 실용적인 정보 제공
|
||||||
|
- **강화된 Todo 연결**: Action item이 담당자의 Todo와 실시간으로 연결되고, 진행 상황이 회의록에 자동 반영
|
||||||
|
- **프롬프팅 기반 회의록 개선**: AI를 활용한 다양한 형식의 회의록 생성 (1Page 요약, 핵심 요약 등)
|
||||||
|
- **지능형 회의 진행 지원**: 회의 패턴 분석을 통한 안건 추천, 효율성 분석 및 개선 제안
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 마이크로서비스 구성
|
## 마이크로서비스 구성
|
||||||
|
|
||||||
1. **User** - 사용자 인증 및 권한 관리
|
1. **User** - 사용자 인증 및 권한 관리
|
||||||
2. **Meeting** - 회의 관리, 회의록 생성 및 관리, 회의록 공유
|
2. **Meeting** - 회의 관리, 회의록 생성 및 관리, 회의록 공유
|
||||||
3. **STT** - 음성 녹음 관리, 음성-텍스트 변환, 화자 식별
|
3. **STT** - 음성 녹음 관리, 음성-텍스트 변환, 화자 식별 (기본 기능)
|
||||||
4. **AI** - LLM 기반 회의록 자동 작성, Todo 자동 추출
|
4. **AI** - LLM 기반 회의록 자동 작성, Todo 자동 추출, 프롬프팅 기반 회의록 개선
|
||||||
5. **RAG** - 전문용어 감지 및 설명 제공, 문서 검색
|
5. **RAG** - 맥락 기반 용어 설명, 관련 문서 검색 및 연결, 업무 이력 통합
|
||||||
6. **Collaboration** - 실시간 동기화, 버전 관리, 충돌 해결
|
6. **Collaboration** - 실시간 동기화, 버전 관리, 충돌 해결
|
||||||
7. **Todo** - Todo 할당 및 관리, 진행 상황 추적
|
7. **Todo** - Todo 할당 및 관리, 진행 상황 추적, 회의록 실시간 연동
|
||||||
8. **Notification** - 알림 발송 및 리마인더 관리
|
8. **Notification** - 알림 발송 및 리마인더 관리
|
||||||
9. **Calendar** - 일정 생성 및 외부 캘린더 연동
|
9. **Calendar** - 일정 생성 및 외부 캘린더 연동
|
||||||
|
10. **Analytics** - 회의 효율성 분석, 패턴 분석, 개선 제안 (신규)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 유저스토리
|
## 유저스토리
|
||||||
|
|
||||||
```
|
```
|
||||||
1. User 서비스
|
1. User 서비스
|
||||||
1) 사용자 인증 및 관리
|
1) 사용자 인증 및 관리
|
||||||
@ -170,7 +191,7 @@ UFR-MEET-060: [회의록공유] 회의록 작성자로서 | 나는, 회의 내
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
3. STT 서비스
|
3. STT 서비스 (기본 기능)
|
||||||
1) 음성 인식 및 변환
|
1) 음성 인식 및 변환
|
||||||
UFR-STT-010: [음성녹음인식] 회의 참석자로서 | 나는, 발언 내용이 자동으로 기록되기 위해 | 음성이 실시간으로 녹음되고 인식되기를 원한다.
|
UFR-STT-010: [음성녹음인식] 회의 참석자로서 | 나는, 발언 내용이 자동으로 기록되기 위해 | 음성이 실시간으로 녹음되고 인식되기를 원한다.
|
||||||
- 시나리오: 음성 녹음 및 발언 인식
|
- 시나리오: 음성 녹음 및 발언 인식
|
||||||
@ -198,6 +219,10 @@ UFR-STT-010: [음성녹음인식] 회의 참석자로서 | 나는, 발언 내용
|
|||||||
- 발언 인식 지연 시간: 1초 이내
|
- 발언 인식 지연 시간: 1초 이내
|
||||||
- 화자 식별 정확도: 90% 이상
|
- 화자 식별 정확도: 90% 이상
|
||||||
|
|
||||||
|
[비고]
|
||||||
|
- STT는 기본 기능으로 경쟁사 대부분이 제공하는 기능임
|
||||||
|
- 차별화 포인트가 아닌 필수 기능
|
||||||
|
|
||||||
- M/21
|
- M/21
|
||||||
|
|
||||||
---
|
---
|
||||||
@ -223,11 +248,14 @@ UFR-STT-020: [텍스트변환] 회의록 시스템으로서 | 나는, 인식된
|
|||||||
- 정확도 60% 미만 시 경고 표시
|
- 정확도 60% 미만 시 경고 표시
|
||||||
- 수동 수정 인터페이스 제공
|
- 수동 수정 인터페이스 제공
|
||||||
|
|
||||||
|
[비고]
|
||||||
|
- STT는 기본 기능으로 차별화 포인트가 아님
|
||||||
|
|
||||||
- M/13
|
- M/13
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
4. AI 서비스
|
4. AI 서비스 (차별화 포인트)
|
||||||
1) AI 회의록 작성
|
1) AI 회의록 작성
|
||||||
UFR-AI-010: [회의록자동작성] 회의록 작성자로서 | 나는, 회의록 작성 부담을 줄이기 위해 | AI가 발언 내용을 자동으로 정리하여 회의록을 작성하기를 원한다.
|
UFR-AI-010: [회의록자동작성] 회의록 작성자로서 | 나는, 회의록 작성 부담을 줄이기 위해 | AI가 발언 내용을 자동으로 정리하여 회의록을 작성하기를 원한다.
|
||||||
- 시나리오: AI 회의록 자동 작성
|
- 시나리오: AI 회의록 자동 작성
|
||||||
@ -301,11 +329,89 @@ UFR-AI-020: [Todo자동추출] 회의록 작성자로서 | 나는, 회의 후
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
5. RAG 서비스
|
3) 프롬프팅 기반 회의록 개선 (신규, 차별화 포인트)
|
||||||
1) 전문용어 지원
|
UFR-AI-030: [회의록개선] 회의록 작성자로서 | 나는, 회의록을 다양한 형식으로 변환하기 위해 | 프롬프팅을 통해 회의록을 개선하고 재구성하고 싶다.
|
||||||
UFR-RAG-010: [전문용어감지] 회의록 작성자로서 | 나는, 업무 지식이 없어도 회의록을 정확히 작성하기 위해 | 전문용어가 자동으로 감지되고 설명을 제공받고 싶다.
|
- 시나리오: 프롬프팅 기반 회의록 개선
|
||||||
- 시나리오: 전문용어 자동 감지
|
회의록이 작성된 상황에서 | "1Page 요약", "핵심 요약", "상세 보고서" 등의 프롬프트를 입력하면 | AI가 해당 형식에 맞춰 회의록을 재구성하여 제공한다.
|
||||||
회의록이 작성되는 상황에서 | 시스템이 회의록 텍스트를 분석하면 | 전문용어가 자동으로 감지되고 하이라이트 표시된다.
|
|
||||||
|
[지원 프롬프트 유형]
|
||||||
|
- "1Page 요약": A4 1장 분량의 요약본 생성
|
||||||
|
- "핵심 요약": 3-5개 핵심 포인트만 추출
|
||||||
|
- "상세 보고서": 시간순 상세 기록 with 타임스탬프
|
||||||
|
- "의사결정 중심": 결정 사항과 근거만 정리
|
||||||
|
- "액션 아이템 중심": Todo와 담당자만 강조
|
||||||
|
- "경영진 보고용": 임원진에게 보고할 형식으로 재구성
|
||||||
|
- "커스텀 프롬프트": 사용자 정의 형식
|
||||||
|
|
||||||
|
[AI 처리 과정]
|
||||||
|
- 원본 회의록 분석
|
||||||
|
- 프롬프트 의도 파악
|
||||||
|
- 내용 재구성
|
||||||
|
- 중요도 기반 필터링
|
||||||
|
- 형식에 맞춘 재배치
|
||||||
|
- 불필요한 내용 제거
|
||||||
|
- 스타일 조정
|
||||||
|
- 문체 변환 (격식체, 구어체 등)
|
||||||
|
- 길이 조정 (압축 또는 확장)
|
||||||
|
|
||||||
|
[처리 결과]
|
||||||
|
- 개선된 회의록이 생성됨 (새 버전)
|
||||||
|
- 원본 회의록 링크 유지
|
||||||
|
- 생성 시간 및 프롬프트 기록
|
||||||
|
- 다운로드 가능 (PDF, DOCX, MD)
|
||||||
|
|
||||||
|
[Policy/Rule]
|
||||||
|
- 원본 회의록은 항상 보존
|
||||||
|
- 여러 버전 동시 생성 가능
|
||||||
|
- 버전 간 비교 기능 제공
|
||||||
|
|
||||||
|
- M/21
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
4) 관련 회의록 자동 연결 (신규, 차별화 포인트)
|
||||||
|
UFR-AI-040: [관련회의록연결] 회의록 작성자로서 | 나는, 이전 회의 내용을 쉽게 참조하기 위해 | AI가 관련 있는 과거 회의록을 자동으로 찾아 연결해주기를 원한다.
|
||||||
|
- 시나리오: 관련 회의록 자동 연결
|
||||||
|
회의록이 작성되는 상황에서 | AI가 회의 주제와 내용을 분석하면 | 유사한 주제의 과거 회의록을 찾아 자동으로 연결한다.
|
||||||
|
|
||||||
|
[AI 분석 과정]
|
||||||
|
- 현재 회의록 주제 및 키워드 추출
|
||||||
|
- 벡터 유사도 검색
|
||||||
|
- 과거 회의록 DB에서 검색
|
||||||
|
- 주제 유사도 계산
|
||||||
|
- 관련도 점수 계산 (0-100%)
|
||||||
|
- 상위 5개 회의록 선정
|
||||||
|
|
||||||
|
[연결 기준]
|
||||||
|
- 주제 유사도 70% 이상
|
||||||
|
- 동일 참석자가 50% 이상
|
||||||
|
- 키워드 3개 이상 일치
|
||||||
|
- 시간적 연관성 (후속 회의, 분기별 회의 등)
|
||||||
|
|
||||||
|
[처리 결과]
|
||||||
|
- 관련 회의록 목록 생성
|
||||||
|
- 각 회의록별 정보
|
||||||
|
- 제목
|
||||||
|
- 날짜
|
||||||
|
- 참석자
|
||||||
|
- 관련도 점수
|
||||||
|
- 연관 키워드
|
||||||
|
- 회의록 상단에 "관련 회의록" 섹션 자동 추가
|
||||||
|
- 클릭 시 해당 회의록으로 이동
|
||||||
|
|
||||||
|
[Policy/Rule]
|
||||||
|
- 관련도 70% 이상만 자동 연결
|
||||||
|
- 최대 5개까지 표시
|
||||||
|
|
||||||
|
- S/13
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
5. RAG 서비스 (차별화 포인트)
|
||||||
|
1) 맥락 기반 용어 설명 (강화)
|
||||||
|
UFR-RAG-010: [전문용어감지] 회의록 작성자로서 | 나는, 업무 지식이 없어도 회의록을 정확히 작성하기 위해 | 전문용어가 자동으로 감지되고 맥락에 맞는 설명을 제공받고 싶다.
|
||||||
|
- 시나리오: 맥락 기반 전문용어 자동 감지
|
||||||
|
회의록이 작성되는 상황에서 | 시스템이 회의록 텍스트를 분석하면 | 전문용어가 자동으로 감지되고 맥락에 맞는 설명이 준비된다.
|
||||||
|
|
||||||
[전문용어 감지 처리]
|
[전문용어 감지 처리]
|
||||||
- 회의록 텍스트 실시간 분석
|
- 회의록 텍스트 실시간 분석
|
||||||
@ -322,54 +428,115 @@ UFR-RAG-010: [전문용어감지] 회의록 작성자로서 | 나는, 업무 지
|
|||||||
- 감지 위치 (줄 번호, 문단)
|
- 감지 위치 (줄 번호, 문단)
|
||||||
- 신뢰도 점수
|
- 신뢰도 점수
|
||||||
- 용어 하이라이트 표시
|
- 용어 하이라이트 표시
|
||||||
- RAG 검색 자동 실행 (UFR-RAG-020 연동)
|
- 맥락 기반 설명 자동 생성 (UFR-RAG-020 연동)
|
||||||
|
|
||||||
[Policy/Rule]
|
[Policy/Rule]
|
||||||
- 신뢰도 70% 이상만 자동 감지
|
- 신뢰도 70% 이상만 자동 감지
|
||||||
- 중복 용어는 첫 번째만 하이라이트
|
- 중복 용어는 첫 번째만 하이라이트
|
||||||
|
|
||||||
|
[비고]
|
||||||
|
- 단순 용어 설명이 아닌 맥락 기반 실용적 정보 제공이 차별화 포인트
|
||||||
|
|
||||||
- S/13
|
- S/13
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
UFR-RAG-020: [용어설명제공] 회의록 작성자로서 | 나는, 전문용어를 이해하기 위해 | 용어에 대한 설명을 자동으로 제공받고 싶다.
|
UFR-RAG-020: [맥락기반용어설명] 회의록 작성자로서 | 나는, 전문용어를 맥락에 맞게 이해하기 위해 | 관련 회의록과 업무 이력을 바탕으로 실용적인 설명을 제공받고 싶다.
|
||||||
- 시나리오: 용어 설명 자동 제공
|
- 시나리오: 맥락 기반 용어 설명 자동 제공
|
||||||
전문용어가 감지된 상황에서 | RAG 시스템이 용어 설명을 검색하면 | 과거 회의록 및 사내 문서에서 관련 설명이 생성되어 제공된다.
|
전문용어가 감지된 상황에서 | RAG 시스템이 관련 문서를 검색하면 | 과거 회의록 및 업무 이력에서 맥락에 맞는 실용적인 설명이 생성되어 제공된다.
|
||||||
|
|
||||||
[RAG 검색 수행]
|
[RAG 검색 수행]
|
||||||
- 벡터 유사도 검색
|
- 벡터 유사도 검색
|
||||||
- 과거 회의록 검색
|
- 과거 회의록 검색 (동일 용어 사용 사례)
|
||||||
- 사내 문서 저장소 검색 (위키, 매뉴얼, 보고서)
|
- 사내 문서 저장소 검색 (위키, 매뉴얼, 보고서)
|
||||||
|
- 업무 이력 검색 (프로젝트 문서, 이메일 등)
|
||||||
- 관련 문서 추출 (관련도 점수순)
|
- 관련 문서 추출 (관련도 점수순)
|
||||||
- 최대 5개 문서 선택
|
- 최대 5개 문서 선택
|
||||||
|
|
||||||
[LLM 설명 생성]
|
[맥락 기반 설명 생성]
|
||||||
- 검색된 문서 내용 분석
|
- 검색된 문서 내용 분석
|
||||||
- 용어 정의 추출
|
- 용어 정의 추출
|
||||||
- 회의 맥락에 맞는 설명 생성
|
- 실제 사용 사례 추출
|
||||||
|
- 현재 회의 맥락에 맞는 설명 생성
|
||||||
- 간단한 정의 (1-2문장)
|
- 간단한 정의 (1-2문장)
|
||||||
- 상세 설명
|
- 이 회의에서의 의미 (맥락 기반)
|
||||||
- 사용 예시
|
- 관련 프로젝트/이슈 연결
|
||||||
- 참조 출처
|
- 과거 논의 요약 (언제, 누가, 어떻게 사용했는지)
|
||||||
|
- 참조 출처 링크
|
||||||
|
|
||||||
[처리 결과]
|
[처리 결과]
|
||||||
- 용어 설명이 생성됨 (설명 ID)
|
- 맥락 기반 용어 설명이 생성됨 (설명 ID)
|
||||||
- 설명 내용
|
- 설명 내용
|
||||||
- 간단한 정의
|
- 간단한 정의
|
||||||
- 상세 설명
|
- 맥락 기반 상세 설명
|
||||||
- 참조 문서 링크
|
- 실제 사용 사례
|
||||||
|
- 관련 프로젝트/이슈
|
||||||
|
- 과거 회의록 링크 (최대 3개)
|
||||||
|
- 사내 문서 링크
|
||||||
- 툴팁 또는 사이드 패널로 표시
|
- 툴팁 또는 사이드 패널로 표시
|
||||||
- 설명 제공 시간 기록
|
- 설명 제공 시간 기록
|
||||||
|
|
||||||
[설명을 찾지 못한 경우]
|
[설명을 찾지 못한 경우]
|
||||||
- "설명을 찾을 수 없습니다" 메시지 표시
|
- "관련 정보를 찾을 수 없습니다" 메시지 표시
|
||||||
- 전문가(회의 참석자)에게 설명 요청 버튼 제공
|
- 전문가(회의 참석자)에게 설명 요청 버튼 제공
|
||||||
- 수동 입력된 설명은 용어 사전에 자동 저장
|
- 수동 입력된 설명은 용어 사전에 자동 저장
|
||||||
|
|
||||||
|
[비고]
|
||||||
|
- **차별화 포인트**: 단순 용어 설명이 아닌, 조직 내 실제 사용 맥락과 이력을 제공
|
||||||
|
- 업무 지식이 없어도 실질적인 도움을 받을 수 있음
|
||||||
|
|
||||||
- S/21
|
- S/21
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
2) 관련 문서 자동 연결 (신규, 차별화 포인트)
|
||||||
|
UFR-RAG-030: [관련문서연결] 회의록 작성자로서 | 나는, 회의 내용을 더 잘 이해하기 위해 | 관련된 사내 문서와 업무 이력이 자동으로 연결되기를 원한다.
|
||||||
|
- 시나리오: 관련 문서 자동 연결
|
||||||
|
회의록이 작성되는 상황에서 | RAG 시스템이 주제와 키워드를 분석하면 | 관련된 사내 문서와 업무 이력이 자동으로 검색되어 연결된다.
|
||||||
|
|
||||||
|
[문서 검색 범위]
|
||||||
|
- 과거 회의록
|
||||||
|
- 프로젝트 문서
|
||||||
|
- 위키 페이지
|
||||||
|
- 이메일 스레드
|
||||||
|
- 보고서 및 기획서
|
||||||
|
- 이슈 트래커 (Jira, Asana 등)
|
||||||
|
|
||||||
|
[RAG 검색 수행]
|
||||||
|
- 회의 주제 및 키워드 추출
|
||||||
|
- 벡터 유사도 검색
|
||||||
|
- 관련도 점수 계산 (0-100%)
|
||||||
|
- 문서 타입별 상위 3개 선정
|
||||||
|
|
||||||
|
[연결 기준]
|
||||||
|
- 주제 유사도 70% 이상
|
||||||
|
- 키워드 3개 이상 일치
|
||||||
|
- 최근 3개월 이내 문서 우선
|
||||||
|
- 동일 프로젝트/팀 문서 가중치
|
||||||
|
|
||||||
|
[처리 결과]
|
||||||
|
- 관련 문서 목록 생성
|
||||||
|
- 각 문서별 정보
|
||||||
|
- 제목
|
||||||
|
- 문서 타입
|
||||||
|
- 작성자
|
||||||
|
- 작성일
|
||||||
|
- 관련도 점수
|
||||||
|
- 핵심 내용 요약 (2-3줄)
|
||||||
|
- 회의록 하단에 "관련 문서" 섹션 자동 추가
|
||||||
|
- 클릭 시 해당 문서로 이동 또는 미리보기
|
||||||
|
|
||||||
|
[Policy/Rule]
|
||||||
|
- 관련도 70% 이상만 자동 연결
|
||||||
|
- 문서 타입별 최대 3개까지 표시
|
||||||
|
|
||||||
|
[비고]
|
||||||
|
- **차별화 포인트**: 회의록만 보는 것이 아니라 관련 업무 이력 전체를 통합 제공
|
||||||
|
|
||||||
|
- S/13
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
6. Collaboration 서비스
|
6. Collaboration 서비스
|
||||||
1) 실시간 협업
|
1) 실시간 협업
|
||||||
UFR-COLLAB-010: [회의록수정동기화] 회의 참석자로서 | 나는, 회의록을 함께 검증하기 위해 | 회의록을 수정하고 실시간으로 다른 참석자와 동기화하고 싶다.
|
UFR-COLLAB-010: [회의록수정동기화] 회의 참석자로서 | 나는, 회의록을 함께 검증하기 위해 | 회의록을 수정하고 실시간으로 다른 참석자와 동기화하고 싶다.
|
||||||
@ -478,11 +645,11 @@ UFR-COLLAB-030: [검증완료] 회의 참석자로서 | 나는, 회의록의 정
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
7. Todo 서비스
|
7. Todo 서비스 (차별화 포인트)
|
||||||
1) Todo 관리
|
1) 실시간 Todo 연결 (강화)
|
||||||
UFR-TODO-010: [Todo할당] Todo 시스템으로서 | 나는, AI가 추출한 Todo를 담당자에게 전달하기 위해 | Todo를 할당하고 알림을 발송하고 싶다.
|
UFR-TODO-010: [Todo할당] Todo 시스템으로서 | 나는, AI가 추출한 Todo를 담당자에게 전달하기 위해 | Todo를 실시간으로 할당하고 회의록과 연결하고 싶다.
|
||||||
- 시나리오: Todo 자동 할당
|
- 시나리오: Todo 실시간 할당 및 회의록 연결
|
||||||
AI가 Todo를 추출한 상황에서 | 시스템이 Todo를 등록하고 담당자를 지정하면 | Todo가 할당되고 담당자에게 즉시 알림이 발송되며 캘린더에 마감일이 등록된다.
|
AI가 Todo를 추출한 상황에서 | 시스템이 Todo를 등록하고 담당자를 지정하면 | Todo가 실시간으로 할당되고 회의록의 해당 위치와 연결되며 담당자에게 즉시 알림이 발송된다.
|
||||||
|
|
||||||
[Todo 등록]
|
[Todo 등록]
|
||||||
- Todo 정보 저장
|
- Todo 정보 저장
|
||||||
@ -491,7 +658,13 @@ UFR-TODO-010: [Todo할당] Todo 시스템으로서 | 나는, AI가 추출한 Tod
|
|||||||
- 담당자 (AI 자동 식별 또는 수동 지정)
|
- 담당자 (AI 자동 식별 또는 수동 지정)
|
||||||
- 마감일 (언급된 경우 자동 설정, 없으면 수동 설정)
|
- 마감일 (언급된 경우 자동 설정, 없으면 수동 설정)
|
||||||
- 우선순위 (높음/보통/낮음)
|
- 우선순위 (높음/보통/낮음)
|
||||||
- 관련 회의록 링크
|
- 관련 회의록 링크 (섹션 위치 포함)
|
||||||
|
- 원문 발언 링크 (타임스탬프 포함)
|
||||||
|
|
||||||
|
[회의록 실시간 연결]
|
||||||
|
- 회의록 해당 섹션에 Todo 뱃지 표시
|
||||||
|
- Todo 클릭 시 Todo 상세 정보 표시
|
||||||
|
- 양방향 연결 (Todo → 회의록, 회의록 → Todo)
|
||||||
|
|
||||||
[알림 발송]
|
[알림 발송]
|
||||||
- 담당자에게 즉시 알림
|
- 담당자에게 즉시 알림
|
||||||
@ -500,7 +673,8 @@ UFR-TODO-010: [Todo할당] Todo 시스템으로서 | 나는, AI가 추출한 Tod
|
|||||||
- 알림 내용
|
- 알림 내용
|
||||||
- Todo 내용
|
- Todo 내용
|
||||||
- 마감일
|
- 마감일
|
||||||
- 회의록 링크
|
- 회의록 링크 (해당 섹션으로 바로 이동)
|
||||||
|
- 원문 발언 링크
|
||||||
|
|
||||||
[캘린더 연동]
|
[캘린더 연동]
|
||||||
- 마감일이 있는 경우 캘린더에 자동 등록
|
- 마감일이 있는 경우 캘린더에 자동 등록
|
||||||
@ -511,19 +685,24 @@ UFR-TODO-010: [Todo할당] Todo 시스템으로서 | 나는, AI가 추출한 Tod
|
|||||||
- 담당자 정보
|
- 담당자 정보
|
||||||
- 마감일
|
- 마감일
|
||||||
- 할당 시간
|
- 할당 시간
|
||||||
|
- 회의록 연결 정보 (섹션 ID, 타임스탬프)
|
||||||
- 담당자에게 알림이 발송됨
|
- 담당자에게 알림이 발송됨
|
||||||
- 캘린더 등록 완료
|
- 캘린더 등록 완료
|
||||||
|
|
||||||
[Policy/Rule]
|
[Policy/Rule]
|
||||||
- Todo 할당 시 담당자에게 즉시 알림 발송
|
- Todo 할당 시 담당자에게 즉시 알림 발송
|
||||||
|
- 회의록과 실시간 양방향 연결
|
||||||
|
|
||||||
|
[비고]
|
||||||
|
- **차별화 포인트**: Todo와 회의록의 강력한 연결, 원문 맥락 추적 가능
|
||||||
|
|
||||||
- M/13
|
- M/13
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
UFR-TODO-020: [Todo진행상황업데이트] Todo 담당자로서 | 나는, Todo 진행 상황을 공유하기 위해 | 진행률을 업데이트하고 상태를 변경하고 싶다.
|
UFR-TODO-020: [Todo진행상황업데이트] Todo 담당자로서 | 나는, Todo 진행 상황을 공유하고 회의록에 반영하기 위해 | 진행률을 업데이트하고 상태를 변경하고 싶다.
|
||||||
- 시나리오: Todo 진행 상황 업데이트
|
- 시나리오: Todo 진행 상황 업데이트 및 회의록 자동 반영
|
||||||
할당된 Todo가 있는 상황에서 | 담당자가 진행률과 상태를 입력하면 | 진행 상황이 저장되고 회의 주최자에게 알림이 발송된다.
|
할당된 Todo가 있는 상황에서 | 담당자가 진행률과 상태를 입력하면 | 진행 상황이 저장되고 연결된 회의록에 실시간으로 반영되며 회의 주최자에게 알림이 발송된다.
|
||||||
|
|
||||||
[진행 상황 입력]
|
[진행 상황 입력]
|
||||||
- 진행률: 0-100% (슬라이더 또는 직접 입력)
|
- 진행률: 0-100% (슬라이더 또는 직접 입력)
|
||||||
@ -535,6 +714,13 @@ UFR-TODO-020: [Todo진행상황업데이트] Todo 담당자로서 | 나는, Todo
|
|||||||
- 진행률 히스토리 저장
|
- 진행률 히스토리 저장
|
||||||
- 상태 변경 이력 저장
|
- 상태 변경 이력 저장
|
||||||
|
|
||||||
|
[회의록 실시간 반영]
|
||||||
|
- 연결된 회의록의 Todo 섹션 자동 업데이트
|
||||||
|
- 진행률 표시 (프로그레스 바)
|
||||||
|
- 상태 배지 업데이트 (시작 전/진행 중/완료)
|
||||||
|
- 마지막 업데이트 시간 표시
|
||||||
|
- 담당자 메모 표시 (있는 경우)
|
||||||
|
|
||||||
[알림 발송]
|
[알림 발송]
|
||||||
- 회의 주최자에게 진행 상황 알림
|
- 회의 주최자에게 진행 상황 알림
|
||||||
- 진행률이 50%, 100%에 도달하면 자동 알림
|
- 진행률이 50%, 100%에 도달하면 자동 알림
|
||||||
@ -544,15 +730,23 @@ UFR-TODO-020: [Todo진행상황업데이트] Todo 담당자로서 | 나는, Todo
|
|||||||
- 업데이트 시간
|
- 업데이트 시간
|
||||||
- 진행률 (%)
|
- 진행률 (%)
|
||||||
- 상태 (시작 전/진행 중/완료)
|
- 상태 (시작 전/진행 중/완료)
|
||||||
- 회의록에 진행 상황 반영
|
- 회의록에 진행 상황이 실시간 반영됨
|
||||||
|
- 반영 시간 기록
|
||||||
|
|
||||||
|
[Policy/Rule]
|
||||||
|
- Todo 진행 상황 업데이트 시 회의록에 즉시 반영
|
||||||
|
- 진행률 50%, 100% 도달 시 자동 알림
|
||||||
|
|
||||||
|
[비고]
|
||||||
|
- **차별화 포인트**: Todo 진행 상황이 회의록에 실시간 반영되어 추적 용이
|
||||||
|
|
||||||
- M/5
|
- M/5
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
UFR-TODO-030: [Todo완료처리] Todo 담당자로서 | 나는, 완료된 Todo를 처리하기 위해 | Todo를 완료하고 회의록에 자동 반영하고 싶다.
|
UFR-TODO-030: [Todo완료처리] Todo 담당자로서 | 나는, 완료된 Todo를 처리하고 회의록에 반영하기 위해 | Todo를 완료하고 회의록에 자동 반영하고 싶다.
|
||||||
- 시나리오: Todo 완료 처리
|
- 시나리오: Todo 완료 처리 및 회의록 자동 반영
|
||||||
Todo 작업이 완료된 상황에서 | 담당자가 완료 버튼을 클릭하면 | Todo가 완료 상태로 변경되고 회의록에 자동 반영되며 회의 주최자에게 알림이 발송된다.
|
Todo 작업이 완료된 상황에서 | 담당자가 완료 버튼을 클릭하면 | Todo가 완료 상태로 변경되고 연결된 회의록에 완료 상태가 실시간으로 반영되며 회의 주최자에게 알림이 발송된다.
|
||||||
|
|
||||||
[완료 처리]
|
[완료 처리]
|
||||||
- 완료 시간 자동 기록
|
- 완료 시간 자동 기록
|
||||||
@ -560,10 +754,11 @@ UFR-TODO-030: [Todo완료처리] Todo 담당자로서 | 나는, 완료된 Todo
|
|||||||
- 완료 상태로 변경
|
- 완료 상태로 변경
|
||||||
- 완료 여부 확인 다이얼로그 표시
|
- 완료 여부 확인 다이얼로그 표시
|
||||||
|
|
||||||
[회의록 반영]
|
[회의록 실시간 반영]
|
||||||
- 관련 회의록의 Todo 섹션 업데이트
|
- 관련 회의록의 Todo 섹션 자동 업데이트
|
||||||
- 완료 표시 (체크 아이콘)
|
- 완료 표시 (체크 아이콘)
|
||||||
- 완료 시간 기록
|
- 완료 시간 기록
|
||||||
|
- 완료자 정보 표시
|
||||||
|
|
||||||
[알림 발송]
|
[알림 발송]
|
||||||
- 회의 주최자에게 완료 알림
|
- 회의 주최자에게 완료 알림
|
||||||
@ -578,13 +773,57 @@ UFR-TODO-030: [Todo완료처리] Todo 담당자로서 | 나는, 완료된 Todo
|
|||||||
- 회의록 버전 업데이트
|
- 회의록 버전 업데이트
|
||||||
|
|
||||||
[Policy/Rule]
|
[Policy/Rule]
|
||||||
- Todo 완료 시 회의록에 완료 상태 자동 반영
|
- Todo 완료 시 회의록에 완료 상태 즉시 반영
|
||||||
- 모든 Todo 완료 시 회의 주최자에게 완료 알림
|
- 모든 Todo 완료 시 회의 주최자에게 완료 알림
|
||||||
|
|
||||||
|
[비고]
|
||||||
|
- **차별화 포인트**: Todo 완료가 회의록에 실시간 반영되어 회의 결과 추적 용이
|
||||||
|
|
||||||
- M/8
|
- M/8
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
2) 회의 중 실시간 Todo 생성 (신규, 차별화 포인트)
|
||||||
|
UFR-TODO-040: [실시간Todo생성] 회의 참석자로서 | 나는, 회의 중 논의된 액션 아이템을 즉시 기록하기 위해 | 회의 진행 중 실시간으로 Todo를 생성하고 회의록과 연결하고 싶다.
|
||||||
|
- 시나리오: 회의 중 실시간 Todo 생성
|
||||||
|
회의가 진행 중인 상황에서 | 참석자가 "Todo 추가" 버튼을 클릭하고 내용을 입력하면 | Todo가 즉시 생성되고 현재 회의록 위치와 연결되며 타임스탬프가 기록된다.
|
||||||
|
|
||||||
|
[실시간 Todo 생성]
|
||||||
|
- Todo 내용 입력 (필수)
|
||||||
|
- 담당자 선택 (필수)
|
||||||
|
- 마감일 설정 (선택)
|
||||||
|
- 우선순위 설정 (선택)
|
||||||
|
- 현재 회의 시간 자동 기록 (타임스탬프)
|
||||||
|
|
||||||
|
[회의록 자동 연결]
|
||||||
|
- 현재 작성 중인 회의록 섹션과 자동 연결
|
||||||
|
- Todo 생성 시점의 타임스탬프 저장
|
||||||
|
- 회의록에 Todo 뱃지 자동 추가
|
||||||
|
- 음성 녹음 링크 연결 (해당 시간대)
|
||||||
|
|
||||||
|
[실시간 동기화]
|
||||||
|
- 모든 참석자 화면에 즉시 표시
|
||||||
|
- Todo 추가 알림 (인앱)
|
||||||
|
- 담당자에게 즉시 알림 발송
|
||||||
|
|
||||||
|
[처리 결과]
|
||||||
|
- Todo가 생성됨 (Todo ID)
|
||||||
|
- Todo 내용, 담당자, 마감일
|
||||||
|
- 회의록 연결 정보 (섹션 ID, 타임스탬프)
|
||||||
|
- 생성 시간 및 생성자
|
||||||
|
- 모든 참석자에게 동기화됨
|
||||||
|
|
||||||
|
[Policy/Rule]
|
||||||
|
- 회의 중 생성된 Todo는 회의록과 자동 연결
|
||||||
|
- 담당자에게 즉시 알림 발송
|
||||||
|
|
||||||
|
[비고]
|
||||||
|
- **차별화 포인트**: 회의 중 실시간 Todo 생성으로 액션 아이템 누락 방지
|
||||||
|
|
||||||
|
- S/8
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
8. Notification 서비스
|
8. Notification 서비스
|
||||||
1) 알림 관리
|
1) 알림 관리
|
||||||
UFR-NOTI-010: [알림리마인더] 회의 참석자로서 | 나는, 중요한 일정을 놓치지 않기 위해 | 회의 및 Todo 관련 알림과 리마인더를 받고 싶다.
|
UFR-NOTI-010: [알림리마인더] 회의 참석자로서 | 나는, 중요한 일정을 놓치지 않기 위해 | 회의 및 Todo 관련 알림과 리마인더를 받고 싶다.
|
||||||
@ -671,4 +910,98 @@ UFR-CAL-010: [일정연동] 회의록 작성자로서 | 나는, 일정을 통합
|
|||||||
- S/13
|
- S/13
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
10. Analytics 서비스 (신규, 차별화 포인트)
|
||||||
|
1) 회의 효율성 분석
|
||||||
|
UFR-ANAL-010: [회의효율성분석] 회의 주최자로서 | 나는, 회의를 개선하기 위해 | 회의 효율성을 분석하고 개선 제안을 받고 싶다.
|
||||||
|
- 시나리오: 회의 효율성 분석 및 개선 제안
|
||||||
|
회의가 종료된 상황에서 | Analytics 시스템이 회의 데이터를 분석하면 | 회의 효율성 점수와 구체적인 개선 제안이 제공된다.
|
||||||
|
|
||||||
|
[분석 지표]
|
||||||
|
- 회의 시간 준수율 (예정 시간 대비 실제 시간)
|
||||||
|
- 참석자 참여도 (발언 분포, 침묵 시간)
|
||||||
|
- 안건 소화율 (계획된 안건 대비 논의된 안건)
|
||||||
|
- 의사결정 효율성 (결정 사항 수 / 회의 시간)
|
||||||
|
- Todo 생성률 (액션 아이템 명확성)
|
||||||
|
|
||||||
|
[AI 분석 과정]
|
||||||
|
- 회의 통계 데이터 수집
|
||||||
|
- 과거 유사 회의와 비교
|
||||||
|
- 업계 벤치마크 대조
|
||||||
|
- 비효율 패턴 감지
|
||||||
|
- 너무 긴 회의 (2시간 이상)
|
||||||
|
- 참여도 불균형 (1명이 50% 이상 발언)
|
||||||
|
- 안건 없이 진행
|
||||||
|
- 결정 사항 없음
|
||||||
|
- Todo 미생성
|
||||||
|
|
||||||
|
[개선 제안 생성]
|
||||||
|
- 구체적인 개선 사항 제시
|
||||||
|
- "회의 시간을 30분 단축 권장"
|
||||||
|
- "참석자 A의 발언 시간이 과도합니다. 타임박스 적용 권장"
|
||||||
|
- "안건을 사전에 공유하여 준비도를 높이세요"
|
||||||
|
- "결정 사항이 없습니다. 회의 목적을 재검토하세요"
|
||||||
|
|
||||||
|
[처리 결과]
|
||||||
|
- 회의 효율성 점수 (0-100점)
|
||||||
|
- 각 지표별 점수 및 벤치마크 비교
|
||||||
|
- 개선 제안 리스트 (우선순위순)
|
||||||
|
- 다음 회의 시 적용할 액션 아이템
|
||||||
|
|
||||||
|
[Policy/Rule]
|
||||||
|
- 모든 회의 종료 시 자동 분석
|
||||||
|
- 효율성 점수 70점 미만 시 개선 알림
|
||||||
|
|
||||||
|
[비고]
|
||||||
|
- **차별화 포인트**: 회의 효율성을 정량적으로 측정하고 실질적 개선 제안 제공
|
||||||
|
|
||||||
|
- M/21
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
2) 회의 패턴 분석 및 안건 추천 (신규, 차별화 포인트)
|
||||||
|
UFR-ANAL-020: [회의패턴분석] 회의 주최자로서 | 나는, 더 나은 회의를 준비하기 위해 | 과거 회의 패턴을 분석하고 안건을 추천받고 싶다.
|
||||||
|
- 시나리오: 회의 패턴 분석 및 안건 추천
|
||||||
|
새로운 회의를 예약하는 상황에서 | Analytics 시스템이 과거 유사 회의를 분석하면 | 회의 패턴 인사이트와 안건 추천이 제공된다.
|
||||||
|
|
||||||
|
[패턴 분석]
|
||||||
|
- 회의 유형 분류 (주간 회의, 프로젝트 회의, 의사결정 회의 등)
|
||||||
|
- 주기성 분석 (주간, 격주, 월간)
|
||||||
|
- 참석자 패턴 (핵심 멤버, 선택 멤버)
|
||||||
|
- 주요 논의 주제 추출
|
||||||
|
- 평균 회의 시간 및 최적 시간대
|
||||||
|
|
||||||
|
[안건 추천]
|
||||||
|
- 과거 회의록 분석
|
||||||
|
- 미해결 이슈 추출
|
||||||
|
- 후속 논의 필요 사항 식별
|
||||||
|
- 주기적 확인 사항 (KPI, 진행 상황)
|
||||||
|
- 관련 프로젝트/업무 이력 검토
|
||||||
|
- 추천 안건 생성
|
||||||
|
- 안건 제목
|
||||||
|
- 논의 배경 (과거 회의록 링크)
|
||||||
|
- 예상 소요 시간
|
||||||
|
|
||||||
|
[최적 회의 구성 제안]
|
||||||
|
- 추천 참석자 (과거 패턴 기반)
|
||||||
|
- 추천 회의 시간 (참석자 캘린더 분석)
|
||||||
|
- 추천 회의 길이 (안건 수 기반)
|
||||||
|
|
||||||
|
[처리 결과]
|
||||||
|
- 회의 패턴 인사이트
|
||||||
|
- 추천 안건 리스트 (최대 5개)
|
||||||
|
- 최적 회의 구성 제안
|
||||||
|
- 과거 유사 회의 링크
|
||||||
|
|
||||||
|
[Policy/Rule]
|
||||||
|
- 회의 예약 시 자동으로 패턴 분석 및 추천 제공
|
||||||
|
- 사용자가 수락/거부 가능
|
||||||
|
|
||||||
|
[비고]
|
||||||
|
- **차별화 포인트**: 과거 회의 데이터를 활용한 지능형 회의 준비 지원
|
||||||
|
|
||||||
|
- M/13
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user