mirror of
https://github.com/hwanny1128/HGZero.git
synced 2025-12-06 16:06:23 +00:00
UI/UX 프로토타입 디렉토리 정리
- 기존 프로토타입 파일 삭제 - 백업 디렉토리(uiux_bk) 추가 - 프로젝트 구조 정리 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
fe2bb9ab6b
commit
fb63e34d90
@ -21,7 +21,11 @@
|
|||||||
"Bash(git add .claude/settings.local.json)",
|
"Bash(git add .claude/settings.local.json)",
|
||||||
"Bash(git add \"design/uiux_다람지/\")",
|
"Bash(git add \"design/uiux_다람지/\")",
|
||||||
"Bash(git commit -m \"$(cat <<''EOF''\n프로토타입 개발 완료 (다람지팀)\n\n- 스타일 가이드 작성 (style-guide.md)\n - 14개 섹션으로 구성된 완전한 디자인 시스템\n - Mobile First 철학 및 접근성 기준 정의\n \n- 공통 리소스 개발\n - common.css: 700+ 라인 반응형 스타일시트\n - common.js: 400+ 라인 유틸리티 라이브러리\n \n- 9개 프로토타입 화면 개발\n - 01-로그인: 사용자 인증\n - 02-대시보드: 메인 대시보드\n - 03-회의예약: 회의 생성 폼\n - 04-템플릿선택: 회의록 템플릿 선택\n - 05-회의진행: 실시간 회의 진행\n - 06-검증완료: 섹션별 검증\n - 07-회의종료: 회의 통계\n - 08-회의록공유: 공유 설정\n - 09-Todo관리: Todo 목록 및 진행 관리\n \n- 주요 특징\n - Mobile First 반응형 디자인\n - WCAG 2.1 Level AA 접근성 준수\n - 실제 동작하는 인터랙션 구현\n - 일관된 예제 데이터 활용\n - Playwright 브라우저 테스트 완료\n\n🤖 Generated with [Claude Code](https://claude.com/claude-code)\n\nCo-Authored-By: Claude <noreply@anthropic.com>\nEOF\n)\")",
|
"Bash(git commit -m \"$(cat <<''EOF''\n프로토타입 개발 완료 (다람지팀)\n\n- 스타일 가이드 작성 (style-guide.md)\n - 14개 섹션으로 구성된 완전한 디자인 시스템\n - Mobile First 철학 및 접근성 기준 정의\n \n- 공통 리소스 개발\n - common.css: 700+ 라인 반응형 스타일시트\n - common.js: 400+ 라인 유틸리티 라이브러리\n \n- 9개 프로토타입 화면 개발\n - 01-로그인: 사용자 인증\n - 02-대시보드: 메인 대시보드\n - 03-회의예약: 회의 생성 폼\n - 04-템플릿선택: 회의록 템플릿 선택\n - 05-회의진행: 실시간 회의 진행\n - 06-검증완료: 섹션별 검증\n - 07-회의종료: 회의 통계\n - 08-회의록공유: 공유 설정\n - 09-Todo관리: Todo 목록 및 진행 관리\n \n- 주요 특징\n - Mobile First 반응형 디자인\n - WCAG 2.1 Level AA 접근성 준수\n - 실제 동작하는 인터랙션 구현\n - 일관된 예제 데이터 활용\n - Playwright 브라우저 테스트 완료\n\n🤖 Generated with [Claude Code](https://claude.com/claude-code)\n\nCo-Authored-By: Claude <noreply@anthropic.com>\nEOF\n)\")",
|
||||||
"Bash(git commit -m \"UI/UX 프로토타입 디렉토리 정리\n\n- 기존 프로토타입 파일 삭제\n- 백업 디렉토리(uiux_bk) 추가\n- 프로젝트 구조 정리\n\n🤖 Generated with [Claude Code](https://claude.com/claude-code)\n\nCo-Authored-By: Claude <noreply@anthropic.com>\")"
|
"Bash(git commit -m \"UI/UX 프로토타입 디렉토리 정리\n\n- 기존 프로토타입 파일 삭제\n- 백업 디렉토리(uiux_bk) 추가\n- 프로젝트 구조 정리\n\n🤖 Generated with [Claude Code](https://claude.com/claude-code)\n\nCo-Authored-By: Claude <noreply@anthropic.com>\")",
|
||||||
|
"Bash(git add design-last/uiux/)",
|
||||||
|
"Bash(git commit -m \"$(cat <<''EOF''\nUI/UX 설계서 작성 완료\n\n- Mobile First 설계 원칙에 따라 UI/UX 설계서 작성\n- 11개 주요 화면 설계 (로그인, 대시보드, 회의예약, 템플릿선택, 회의진행, 검증완료, 회의종료, 회의록공유, Todo관리, 회의록상세조회, 회의록수정)\n- 화면별 상세 설계 (개요, 기능, UI 구성, 인터랙션, 데이터 요구사항, 에러 처리)\n- 화면 간 사용자 플로우 및 네비게이션 전략 정의\n- 반응형 설계 전략 (Mobile/Tablet/Desktop 브레이크포인트)\n- WCAG 2.1 Level AA 접근성 보장 방안\n- 성능 최적화 방안 (코드 스플리팅, 캐싱, WebSocket 최적화)\n- 유저스토리와 1:1 매칭 확인\n\n🤖 Generated with [Claude Code](https://claude.com/claude-code)\n\nCo-Authored-By: Claude <noreply@anthropic.com>\nEOF\n)\")",
|
||||||
|
"Bash(git add \"design-last/uiux_다람지/\")",
|
||||||
|
"Bash(git commit -m \"$(cat <<''EOF''\n프로토타입 개발 완료 (다람지팀)\n\n- 스타일 가이드 작성 (style-guide.md)\n - 14개 섹션으로 구성된 완전한 디자인 시스템\n - Mobile First 철학 및 접근성 기준 정의\n \n- 공통 리소스 개발\n - common.css: 1,007줄 완전한 반응형 스타일시트\n - common.js: 400+줄 유틸리티 라이브러리\n \n- 11개 프로토타입 화면 개발\n - 01-로그인: 사용자 인증\n - 02-대시보드: 메인 대시보드\n - 03-회의예약: 회의 생성 폼\n - 04-템플릿선택: 회의록 템플릿 선택\n - 05-회의진행: 실시간 회의 진행\n - 06-검증완료: 섹션별 검증\n - 07-회의종료: 회의 통계\n - 08-회의록공유: 공유 설정\n - 09-Todo관리: Todo 목록 및 진행 관리\n - 10-회의록상세조회: 회의록 상세 보기\n - 11-회의록수정: 지난 회의록 수정\n \n- 주요 특징\n - Mobile First 반응형 디자인\n - WCAG 2.1 Level AA 접근성 준수\n - 실제 동작하는 인터랙션 구현\n - 일관된 예제 데이터 활용\n - 완전한 사용자 플로우 구현\n\n🤖 Generated with [Claude Code](https://claude.com/claude-code)\n\nCo-Authored-By: Claude <noreply@anthropic.com>\nEOF\n)\")"
|
||||||
],
|
],
|
||||||
"deny": [],
|
"deny": [],
|
||||||
"ask": []
|
"ask": []
|
||||||
|
|||||||
@ -1,541 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="ko">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<meta name="description" content="회의록 작성 및 공유 개선 서비스 - 로그인">
|
|
||||||
<title>로그인 - 회의록 작성 및 공유 개선 서비스</title>
|
|
||||||
|
|
||||||
<!-- CSS -->
|
|
||||||
<link rel="stylesheet" href="common.css">
|
|
||||||
|
|
||||||
<!-- Pretendard Font -->
|
|
||||||
<link rel="stylesheet" as="style" crossorigin href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/static/pretendard.min.css" />
|
|
||||||
|
|
||||||
<style>
|
|
||||||
/* 로그인 화면 특화 스타일 */
|
|
||||||
.login-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
min-height: 100vh;
|
|
||||||
padding: var(--space-4);
|
|
||||||
background-color: var(--bg-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-box {
|
|
||||||
width: 100%;
|
|
||||||
max-width: 400px;
|
|
||||||
background-color: var(--bg-primary);
|
|
||||||
border-radius: var(--radius-large);
|
|
||||||
padding: var(--space-8);
|
|
||||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
|
||||||
animation: fade-in var(--duration-normal) ease-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
|
||||||
.login-box {
|
|
||||||
padding: var(--space-10);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 로고 영역 */
|
|
||||||
.login-logo {
|
|
||||||
text-align: center;
|
|
||||||
margin-bottom: var(--space-8);
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-logo-icon {
|
|
||||||
width: 80px;
|
|
||||||
height: 80px;
|
|
||||||
margin: 0 auto var(--space-4);
|
|
||||||
background: linear-gradient(135deg, var(--primary-500), var(--primary-700));
|
|
||||||
border-radius: var(--radius-large);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-size: 2.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-title {
|
|
||||||
font-size: 1.5rem;
|
|
||||||
font-weight: 700;
|
|
||||||
color: var(--text-primary);
|
|
||||||
margin-bottom: var(--space-2);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
|
||||||
.login-title {
|
|
||||||
font-size: 1.75rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-subtitle {
|
|
||||||
font-size: 0.875rem;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 폼 영역 */
|
|
||||||
.login-form {
|
|
||||||
margin-bottom: var(--space-4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-form .input-group {
|
|
||||||
margin-bottom: var(--space-4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-form .input-group:last-of-type {
|
|
||||||
margin-bottom: var(--space-6);
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-button {
|
|
||||||
width: 100%;
|
|
||||||
margin-bottom: var(--space-4);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* LDAP 안내 */
|
|
||||||
.ldap-notice {
|
|
||||||
text-align: center;
|
|
||||||
padding: var(--space-3);
|
|
||||||
background-color: var(--info-50);
|
|
||||||
border-radius: var(--radius-small);
|
|
||||||
border: var(--border-thin) solid var(--info-100);
|
|
||||||
}
|
|
||||||
|
|
||||||
.ldap-notice-text {
|
|
||||||
font-size: 0.75rem;
|
|
||||||
color: var(--info-700);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: var(--space-2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.ldap-notice-icon {
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 로딩 상태 */
|
|
||||||
.loading-overlay {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
background-color: rgba(0, 0, 0, 0.5);
|
|
||||||
display: none;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
z-index: 9999;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading-overlay.active {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading-content {
|
|
||||||
background-color: var(--bg-primary);
|
|
||||||
padding: var(--space-6);
|
|
||||||
border-radius: var(--radius-large);
|
|
||||||
text-align: center;
|
|
||||||
box-shadow: 0 10px 15px rgba(0, 0, 0, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading-spinner {
|
|
||||||
width: 48px;
|
|
||||||
height: 48px;
|
|
||||||
border: 4px solid var(--gray-200);
|
|
||||||
border-top-color: var(--primary-500);
|
|
||||||
border-radius: 50%;
|
|
||||||
animation: spin 0.8s linear infinite;
|
|
||||||
margin: 0 auto var(--space-4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading-text {
|
|
||||||
font-size: 0.875rem;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 입력 필드 포커스 효과 강화 */
|
|
||||||
.input-field:focus {
|
|
||||||
border-color: var(--primary-500);
|
|
||||||
box-shadow: 0 0 0 3px rgba(0, 200, 150, 0.1);
|
|
||||||
transition: all var(--duration-fast) ease-in-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 에러 메시지 스타일 */
|
|
||||||
.input-error-message {
|
|
||||||
display: block;
|
|
||||||
min-height: 18px;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
color: var(--error-500);
|
|
||||||
margin-top: var(--space-1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 접근성: Skip to main content */
|
|
||||||
.skip-to-main {
|
|
||||||
position: absolute;
|
|
||||||
top: -40px;
|
|
||||||
left: 0;
|
|
||||||
background: var(--primary-500);
|
|
||||||
color: white;
|
|
||||||
padding: var(--space-2) var(--space-4);
|
|
||||||
text-decoration: none;
|
|
||||||
z-index: 100;
|
|
||||||
}
|
|
||||||
|
|
||||||
.skip-to-main:focus {
|
|
||||||
top: 0;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<!-- Skip to main content (접근성) -->
|
|
||||||
<a href="#main-content" class="skip-to-main">본문으로 바로가기</a>
|
|
||||||
|
|
||||||
<!-- 로딩 오버레이 -->
|
|
||||||
<div class="loading-overlay" id="loadingOverlay" role="status" aria-live="polite" aria-label="로그인 진행중">
|
|
||||||
<div class="loading-content">
|
|
||||||
<div class="loading-spinner"></div>
|
|
||||||
<p class="loading-text">로그인 중입니다...</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 메인 컨텐츠 -->
|
|
||||||
<main id="main-content" class="login-container">
|
|
||||||
<div class="login-box">
|
|
||||||
<!-- 로고 및 타이틀 -->
|
|
||||||
<div class="login-logo">
|
|
||||||
<div class="login-logo-icon" role="img" aria-label="회의록 서비스 로고">
|
|
||||||
📝
|
|
||||||
</div>
|
|
||||||
<h1 class="login-title">회의록 작성 서비스</h1>
|
|
||||||
<p class="login-subtitle">효율적이고 정확한 회의록, 누구나 쉽게</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 로그인 폼 -->
|
|
||||||
<form id="loginForm" class="login-form" novalidate>
|
|
||||||
<!-- 사번 입력 -->
|
|
||||||
<div class="input-group">
|
|
||||||
<label for="employeeId" class="input-label required">사번</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
id="employeeId"
|
|
||||||
name="employeeId"
|
|
||||||
class="input-field"
|
|
||||||
placeholder="사번을 입력하세요"
|
|
||||||
autocomplete="username"
|
|
||||||
required
|
|
||||||
aria-label="사번"
|
|
||||||
aria-describedby="employeeIdError"
|
|
||||||
aria-required="true"
|
|
||||||
>
|
|
||||||
<span id="employeeIdError" class="input-error-message" role="alert"></span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 비밀번호 입력 -->
|
|
||||||
<div class="input-group">
|
|
||||||
<label for="password" class="input-label required">비밀번호</label>
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
id="password"
|
|
||||||
name="password"
|
|
||||||
class="input-field"
|
|
||||||
placeholder="비밀번호를 입력하세요"
|
|
||||||
autocomplete="current-password"
|
|
||||||
required
|
|
||||||
aria-label="비밀번호"
|
|
||||||
aria-describedby="passwordError"
|
|
||||||
aria-required="true"
|
|
||||||
>
|
|
||||||
<span id="passwordError" class="input-error-message" role="alert"></span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 로그인 버튼 -->
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
class="button button-primary button-large login-button"
|
|
||||||
id="loginButton"
|
|
||||||
>
|
|
||||||
로그인
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<!-- LDAP 인증 안내 -->
|
|
||||||
<div class="ldap-notice" role="note">
|
|
||||||
<p class="ldap-notice-text">
|
|
||||||
<span class="ldap-notice-icon" aria-hidden="true">🔒</span>
|
|
||||||
<span>LDAP 연동 인증 시스템</span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<!-- JavaScript -->
|
|
||||||
<script src="common.js"></script>
|
|
||||||
<script>
|
|
||||||
/**
|
|
||||||
* 로그인 페이지 초기화 및 이벤트 핸들러
|
|
||||||
*/
|
|
||||||
(function() {
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
// DOM 엘리먼트
|
|
||||||
const loginForm = document.getElementById('loginForm');
|
|
||||||
const employeeIdInput = document.getElementById('employeeId');
|
|
||||||
const passwordInput = document.getElementById('password');
|
|
||||||
const loginButton = document.getElementById('loginButton');
|
|
||||||
const loadingOverlay = document.getElementById('loadingOverlay');
|
|
||||||
|
|
||||||
// 에러 메시지 엘리먼트
|
|
||||||
const employeeIdError = document.getElementById('employeeIdError');
|
|
||||||
const passwordError = document.getElementById('passwordError');
|
|
||||||
|
|
||||||
// 예제 로그인 정보
|
|
||||||
const VALID_CREDENTIALS = {
|
|
||||||
employeeId: 'E2024001',
|
|
||||||
password: 'password123'
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 입력 필드 실시간 검증
|
|
||||||
*/
|
|
||||||
function setupRealtimeValidation() {
|
|
||||||
// 사번 입력 검증
|
|
||||||
employeeIdInput.addEventListener('blur', function() {
|
|
||||||
validateEmployeeId();
|
|
||||||
});
|
|
||||||
|
|
||||||
employeeIdInput.addEventListener('input', function() {
|
|
||||||
// 입력 중에는 에러 클래스 제거
|
|
||||||
employeeIdInput.classList.remove('error');
|
|
||||||
employeeIdError.textContent = '';
|
|
||||||
});
|
|
||||||
|
|
||||||
// 비밀번호 입력 검증
|
|
||||||
passwordInput.addEventListener('blur', function() {
|
|
||||||
validatePassword();
|
|
||||||
});
|
|
||||||
|
|
||||||
passwordInput.addEventListener('input', function() {
|
|
||||||
// 입력 중에는 에러 클래스 제거
|
|
||||||
passwordInput.classList.remove('error');
|
|
||||||
passwordError.textContent = '';
|
|
||||||
});
|
|
||||||
|
|
||||||
// Enter 키로 로그인 실행
|
|
||||||
employeeIdInput.addEventListener('keypress', function(e) {
|
|
||||||
if (e.key === 'Enter') {
|
|
||||||
passwordInput.focus();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
passwordInput.addEventListener('keypress', function(e) {
|
|
||||||
if (e.key === 'Enter') {
|
|
||||||
e.preventDefault();
|
|
||||||
loginForm.dispatchEvent(new Event('submit'));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 사번 검증
|
|
||||||
*/
|
|
||||||
function validateEmployeeId() {
|
|
||||||
const value = employeeIdInput.value.trim();
|
|
||||||
|
|
||||||
if (!value) {
|
|
||||||
showError(employeeIdInput, employeeIdError, '사번을 입력해주세요');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 사번 형식 검증 (E + 7자리 숫자)
|
|
||||||
const employeeIdPattern = /^E\d{7}$/;
|
|
||||||
if (!employeeIdPattern.test(value)) {
|
|
||||||
showError(employeeIdInput, employeeIdError, '올바른 사번 형식이 아닙니다 (예: E2024001)');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
clearError(employeeIdInput, employeeIdError);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 비밀번호 검증
|
|
||||||
*/
|
|
||||||
function validatePassword() {
|
|
||||||
const value = passwordInput.value;
|
|
||||||
|
|
||||||
if (!value) {
|
|
||||||
showError(passwordInput, passwordError, '비밀번호를 입력해주세요');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (value.length < 6) {
|
|
||||||
showError(passwordInput, passwordError, '비밀번호는 최소 6자 이상이어야 합니다');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
clearError(passwordInput, passwordError);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 에러 표시
|
|
||||||
*/
|
|
||||||
function showError(inputElement, errorElement, message) {
|
|
||||||
inputElement.classList.add('error');
|
|
||||||
errorElement.textContent = message;
|
|
||||||
inputElement.setAttribute('aria-invalid', 'true');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 에러 제거
|
|
||||||
*/
|
|
||||||
function clearError(inputElement, errorElement) {
|
|
||||||
inputElement.classList.remove('error');
|
|
||||||
errorElement.textContent = '';
|
|
||||||
inputElement.setAttribute('aria-invalid', 'false');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 로딩 표시
|
|
||||||
*/
|
|
||||||
function showLoading() {
|
|
||||||
loadingOverlay.classList.add('active');
|
|
||||||
loginButton.disabled = true;
|
|
||||||
employeeIdInput.disabled = true;
|
|
||||||
passwordInput.disabled = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 로딩 숨김
|
|
||||||
*/
|
|
||||||
function hideLoading() {
|
|
||||||
loadingOverlay.classList.remove('active');
|
|
||||||
loginButton.disabled = false;
|
|
||||||
employeeIdInput.disabled = false;
|
|
||||||
passwordInput.disabled = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 로그인 처리
|
|
||||||
*/
|
|
||||||
function handleLogin(employeeId, password) {
|
|
||||||
// 로딩 표시
|
|
||||||
showLoading();
|
|
||||||
|
|
||||||
// 실제 환경에서는 API 호출
|
|
||||||
// 여기서는 시뮬레이션 (1.5초 지연)
|
|
||||||
setTimeout(function() {
|
|
||||||
// 인증 검증
|
|
||||||
if (employeeId === VALID_CREDENTIALS.employeeId &&
|
|
||||||
password === VALID_CREDENTIALS.password) {
|
|
||||||
// 로그인 성공
|
|
||||||
// 사용자 정보 저장
|
|
||||||
const userData = {
|
|
||||||
id: 1,
|
|
||||||
employeeId: employeeId,
|
|
||||||
name: '김민준',
|
|
||||||
email: 'minjun.kim@company.com',
|
|
||||||
role: 'Product Owner',
|
|
||||||
loginTime: new Date().toISOString()
|
|
||||||
};
|
|
||||||
|
|
||||||
// 로컬 스토리지에 저장
|
|
||||||
localStorage.setItem('currentUser', JSON.stringify(userData));
|
|
||||||
localStorage.setItem('isLoggedIn', 'true');
|
|
||||||
localStorage.setItem('authToken', 'mock-jwt-token-' + Date.now());
|
|
||||||
|
|
||||||
// 성공 메시지 표시
|
|
||||||
showToast('로그인 성공! 대시보드로 이동합니다', 'success', 1500);
|
|
||||||
|
|
||||||
// 대시보드로 이동 (1.5초 후)
|
|
||||||
setTimeout(function() {
|
|
||||||
navigateTo('02-대시보드.html');
|
|
||||||
}, 1500);
|
|
||||||
|
|
||||||
} else {
|
|
||||||
// 로그인 실패
|
|
||||||
hideLoading();
|
|
||||||
|
|
||||||
// 실패 메시지 표시
|
|
||||||
showToast('사번 또는 비밀번호가 올바르지 않습니다', 'error', 3000);
|
|
||||||
|
|
||||||
// 비밀번호 필드 초기화 및 포커스
|
|
||||||
passwordInput.value = '';
|
|
||||||
passwordInput.focus();
|
|
||||||
|
|
||||||
// 입력 필드에 에러 표시
|
|
||||||
showError(employeeIdInput, employeeIdError, '');
|
|
||||||
showError(passwordInput, passwordError, '인증에 실패했습니다');
|
|
||||||
}
|
|
||||||
}, 1500);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 폼 제출 이벤트 핸들러
|
|
||||||
*/
|
|
||||||
function handleSubmit(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
// 입력 검증
|
|
||||||
const isEmployeeIdValid = validateEmployeeId();
|
|
||||||
const isPasswordValid = validatePassword();
|
|
||||||
|
|
||||||
if (!isEmployeeIdValid || !isPasswordValid) {
|
|
||||||
// 검증 실패 시 첫 번째 에러 필드로 포커스
|
|
||||||
if (!isEmployeeIdValid) {
|
|
||||||
employeeIdInput.focus();
|
|
||||||
} else if (!isPasswordValid) {
|
|
||||||
passwordInput.focus();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 로그인 처리
|
|
||||||
const employeeId = employeeIdInput.value.trim();
|
|
||||||
const password = passwordInput.value;
|
|
||||||
|
|
||||||
handleLogin(employeeId, password);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 초기화
|
|
||||||
*/
|
|
||||||
function init() {
|
|
||||||
// 이미 로그인되어 있는지 확인
|
|
||||||
const isLoggedIn = localStorage.getItem('isLoggedIn');
|
|
||||||
if (isLoggedIn === 'true') {
|
|
||||||
// 이미 로그인된 경우 대시보드로 리다이렉트
|
|
||||||
navigateTo('02-대시보드.html');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 이벤트 리스너 등록
|
|
||||||
setupRealtimeValidation();
|
|
||||||
loginForm.addEventListener('submit', handleSubmit);
|
|
||||||
|
|
||||||
// 첫 번째 입력 필드에 포커스
|
|
||||||
employeeIdInput.focus();
|
|
||||||
|
|
||||||
// 페이드인 효과
|
|
||||||
document.body.style.opacity = '1';
|
|
||||||
|
|
||||||
// 개발 모드 안내 (콘솔)
|
|
||||||
console.log('%c로그인 테스트 정보', 'color: #00C896; font-size: 14px; font-weight: bold;');
|
|
||||||
console.log('사번: E2024001');
|
|
||||||
console.log('비밀번호: password123');
|
|
||||||
}
|
|
||||||
|
|
||||||
// DOM 로드 완료 시 초기화
|
|
||||||
if (document.readyState === 'loading') {
|
|
||||||
document.addEventListener('DOMContentLoaded', init);
|
|
||||||
} else {
|
|
||||||
init();
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@ -1,709 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="ko">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<meta name="description" content="회의록 작성 및 공유 개선 서비스 - 대시보드">
|
|
||||||
<title>대시보드 - 회의록 작성 서비스</title>
|
|
||||||
|
|
||||||
<!-- CSS -->
|
|
||||||
<link rel="stylesheet" href="common.css">
|
|
||||||
|
|
||||||
<!-- Pretendard Font -->
|
|
||||||
<link rel="stylesheet" as="style" crossorigin href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/static/pretendard.min.css" />
|
|
||||||
|
|
||||||
<style>
|
|
||||||
body {
|
|
||||||
padding-bottom: 80px; /* 하단 네비게이션 공간 확보 */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 헤더 */
|
|
||||||
.header {
|
|
||||||
position: sticky;
|
|
||||||
top: 0;
|
|
||||||
background-color: var(--bg-primary);
|
|
||||||
border-bottom: var(--border-thin) solid var(--gray-200);
|
|
||||||
padding: var(--space-4);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
z-index: 10;
|
|
||||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-left {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--space-3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-title {
|
|
||||||
font-size: 1.25rem;
|
|
||||||
font-weight: 700;
|
|
||||||
color: var(--text-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-right {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--space-2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-button {
|
|
||||||
position: relative;
|
|
||||||
width: 40px;
|
|
||||||
height: 40px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
border-radius: var(--radius-full);
|
|
||||||
background-color: transparent;
|
|
||||||
border: none;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background-color var(--duration-instant) ease-in-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-button:hover {
|
|
||||||
background-color: var(--gray-100);
|
|
||||||
}
|
|
||||||
|
|
||||||
.notification-badge {
|
|
||||||
position: absolute;
|
|
||||||
top: 4px;
|
|
||||||
right: 4px;
|
|
||||||
width: 8px;
|
|
||||||
height: 8px;
|
|
||||||
background-color: var(--error-500);
|
|
||||||
border-radius: 50%;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 빠른 액션 */
|
|
||||||
.quick-actions {
|
|
||||||
padding: var(--space-4);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: var(--space-3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-button-primary {
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: var(--space-2);
|
|
||||||
height: 56px;
|
|
||||||
font-size: 1rem;
|
|
||||||
font-weight: 600;
|
|
||||||
border-radius: var(--radius-medium);
|
|
||||||
}
|
|
||||||
|
|
||||||
.ongoing-meeting {
|
|
||||||
background-color: var(--error-50);
|
|
||||||
border: var(--border-thin) solid var(--error-200);
|
|
||||||
border-radius: var(--radius-medium);
|
|
||||||
padding: var(--space-3);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--space-3);
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all var(--duration-fast) ease-in-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ongoing-meeting:hover {
|
|
||||||
background-color: var(--error-100);
|
|
||||||
}
|
|
||||||
|
|
||||||
.ongoing-indicator {
|
|
||||||
width: 12px;
|
|
||||||
height: 12px;
|
|
||||||
background-color: var(--error-500);
|
|
||||||
border-radius: 50%;
|
|
||||||
animation: pulse 1.5s ease-in-out infinite;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ongoing-text {
|
|
||||||
flex: 1;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
color: var(--error-700);
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 필터 영역 */
|
|
||||||
.filters {
|
|
||||||
padding: 0 var(--space-4) var(--space-4);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: var(--space-3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.filter-row {
|
|
||||||
display: flex;
|
|
||||||
gap: var(--space-2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.filter-select {
|
|
||||||
flex: 1;
|
|
||||||
height: 40px;
|
|
||||||
padding: 0 var(--space-3);
|
|
||||||
border: var(--border-thin) solid var(--gray-200);
|
|
||||||
border-radius: var(--radius-small);
|
|
||||||
background-color: var(--bg-primary);
|
|
||||||
color: var(--text-secondary);
|
|
||||||
font-size: 0.875rem;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-input-wrapper {
|
|
||||||
position: relative;
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-icon {
|
|
||||||
position: absolute;
|
|
||||||
left: var(--space-3);
|
|
||||||
top: 50%;
|
|
||||||
transform: translateY(-50%);
|
|
||||||
color: var(--text-tertiary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-input {
|
|
||||||
width: 100%;
|
|
||||||
height: 40px;
|
|
||||||
padding: 0 var(--space-3) 0 40px;
|
|
||||||
border: var(--border-thin) solid var(--gray-200);
|
|
||||||
border-radius: var(--radius-small);
|
|
||||||
font-size: 0.875rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 섹션 헤더 */
|
|
||||||
.section-header {
|
|
||||||
padding: var(--space-4);
|
|
||||||
padding-bottom: var(--space-3);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-title {
|
|
||||||
font-size: 1.125rem;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--text-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-count {
|
|
||||||
font-size: 0.875rem;
|
|
||||||
color: var(--text-tertiary);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 회의록 목록 */
|
|
||||||
.meeting-list {
|
|
||||||
padding: 0 var(--space-4) var(--space-4);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: var(--space-3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.meeting-card {
|
|
||||||
background-color: var(--bg-primary);
|
|
||||||
border: var(--border-thin) solid var(--gray-200);
|
|
||||||
border-radius: var(--radius-medium);
|
|
||||||
padding: var(--space-4);
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all var(--duration-fast) ease-in-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
.meeting-card:hover {
|
|
||||||
border-color: var(--primary-500);
|
|
||||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.meeting-card-header {
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-start;
|
|
||||||
justify-content: space-between;
|
|
||||||
margin-bottom: var(--space-2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.meeting-icon {
|
|
||||||
font-size: 1.25rem;
|
|
||||||
margin-right: var(--space-2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.meeting-title {
|
|
||||||
font-size: 1rem;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--text-primary);
|
|
||||||
margin-bottom: var(--space-1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.meeting-meta {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: var(--space-2);
|
|
||||||
font-size: 0.75rem;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
margin-bottom: var(--space-3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.meeting-meta-item {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--space-1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.meeting-footer {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.meeting-attendees {
|
|
||||||
font-size: 0.75rem;
|
|
||||||
color: var(--text-tertiary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-wrapper {
|
|
||||||
width: 100%;
|
|
||||||
margin-top: var(--space-2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-label {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
margin-bottom: var(--space-1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 하단 네비게이션 */
|
|
||||||
.bottom-nav {
|
|
||||||
position: fixed;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
background-color: var(--bg-primary);
|
|
||||||
border-top: var(--border-thin) solid var(--gray-200);
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-around;
|
|
||||||
padding: var(--space-2) 0;
|
|
||||||
z-index: 100;
|
|
||||||
box-shadow: 0 -2px 4px rgba(0, 0, 0, 0.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-item {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--space-1);
|
|
||||||
padding: var(--space-2);
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: color var(--duration-instant) ease-in-out;
|
|
||||||
color: var(--text-tertiary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-item.active {
|
|
||||||
color: var(--primary-500);
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-item:hover {
|
|
||||||
color: var(--primary-600);
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-icon {
|
|
||||||
font-size: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-label {
|
|
||||||
font-size: 0.75rem;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 상세 모달 */
|
|
||||||
.detail-modal .modal {
|
|
||||||
max-width: 600px;
|
|
||||||
max-height: 85vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
.detail-section {
|
|
||||||
margin-bottom: var(--space-4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.detail-section-title {
|
|
||||||
font-size: 0.875rem;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
margin-bottom: var(--space-2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.detail-content {
|
|
||||||
font-size: 0.875rem;
|
|
||||||
color: var(--text-primary);
|
|
||||||
line-height: 1.75;
|
|
||||||
white-space: pre-line;
|
|
||||||
}
|
|
||||||
|
|
||||||
.detail-actions {
|
|
||||||
display: flex;
|
|
||||||
gap: var(--space-2);
|
|
||||||
margin-top: var(--space-6);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 빈 상태 */
|
|
||||||
.empty-state {
|
|
||||||
padding: var(--space-16) var(--space-4);
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-icon {
|
|
||||||
font-size: 4rem;
|
|
||||||
margin-bottom: var(--space-4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-title {
|
|
||||||
font-size: 1.125rem;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--text-primary);
|
|
||||||
margin-bottom: var(--space-2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-description {
|
|
||||||
font-size: 0.875rem;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
|
||||||
.quick-actions {
|
|
||||||
flex-direction: row;
|
|
||||||
}
|
|
||||||
|
|
||||||
.meeting-list {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(2, 1fr);
|
|
||||||
gap: var(--space-4);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 1024px) {
|
|
||||||
.meeting-list {
|
|
||||||
grid-template-columns: repeat(3, 1fr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<!-- 헤더 -->
|
|
||||||
<header class="header">
|
|
||||||
<div class="header-left">
|
|
||||||
<h1 class="header-title">대시보드</h1>
|
|
||||||
</div>
|
|
||||||
<div class="header-right">
|
|
||||||
<button class="icon-button" aria-label="알림" onclick="showToast('알림이 없습니다', 'info')">
|
|
||||||
<span class="nav-icon">🔔</span>
|
|
||||||
<span class="notification-badge" style="display: none;"></span>
|
|
||||||
</button>
|
|
||||||
<button class="icon-button" aria-label="프로필" onclick="showToast('프로필 기능은 준비 중입니다', 'info')">
|
|
||||||
<span class="nav-icon">👤</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<!-- 빠른 액션 -->
|
|
||||||
<section class="quick-actions">
|
|
||||||
<button class="button button-primary action-button-primary" onclick="navigateTo('03-회의예약.html')">
|
|
||||||
<span>➕</span>
|
|
||||||
<span>새 회의 예약</span>
|
|
||||||
</button>
|
|
||||||
<div id="ongoingMeeting" class="ongoing-meeting" style="display: none;" onclick="handleOngoingMeetingClick()">
|
|
||||||
<div class="ongoing-indicator"></div>
|
|
||||||
<div class="ongoing-text">진행 중인 회의 (1건) - 지금 참여</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- 필터 영역 -->
|
|
||||||
<div class="filters">
|
|
||||||
<div class="filter-row">
|
|
||||||
<select id="statusFilter" class="filter-select" aria-label="상태 필터" onchange="applyFilters()">
|
|
||||||
<option value="all">전체</option>
|
|
||||||
<option value="confirmed">확정완료</option>
|
|
||||||
<option value="in-progress">작성중</option>
|
|
||||||
<option value="draft">임시저장</option>
|
|
||||||
</select>
|
|
||||||
<select id="sortFilter" class="filter-select" aria-label="정렬" onchange="applyFilters()">
|
|
||||||
<option value="latest">최신순</option>
|
|
||||||
<option value="date">회의일시순</option>
|
|
||||||
<option value="title">제목순</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="search-input-wrapper">
|
|
||||||
<span class="search-icon">🔍</span>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
id="searchInput"
|
|
||||||
class="search-input"
|
|
||||||
placeholder="회의 제목, 참석자, 키워드 검색..."
|
|
||||||
aria-label="검색"
|
|
||||||
onkeyup="handleSearch()"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 회의록 목록 -->
|
|
||||||
<section>
|
|
||||||
<div class="section-header">
|
|
||||||
<h2 class="section-title">내 회의록</h2>
|
|
||||||
<span class="section-count" id="meetingCount">0건</span>
|
|
||||||
</div>
|
|
||||||
<div id="meetingList" class="meeting-list">
|
|
||||||
<!-- 동적 생성 -->
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- 하단 네비게이션 -->
|
|
||||||
<nav class="bottom-nav" role="navigation" aria-label="메인 네비게이션">
|
|
||||||
<button class="nav-item active" onclick="navigateTo('02-대시보드.html')" data-nav-link>
|
|
||||||
<span class="nav-icon">🏠</span>
|
|
||||||
<span class="nav-label">대시보드</span>
|
|
||||||
</button>
|
|
||||||
<button class="nav-item" onclick="navigateTo('09-Todo관리.html')" data-nav-link>
|
|
||||||
<span class="nav-icon">✅</span>
|
|
||||||
<span class="nav-label">Todo</span>
|
|
||||||
</button>
|
|
||||||
<button class="nav-item" onclick="showToast('더보기 기능은 준비 중입니다', 'info')">
|
|
||||||
<span class="nav-icon">⚙️</span>
|
|
||||||
<span class="nav-label">더보기</span>
|
|
||||||
</button>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<!-- 상세 모달 -->
|
|
||||||
<div id="detailModal" class="modal-overlay detail-modal" style="display: none;" aria-hidden="true" role="dialog" aria-labelledby="detailModalTitle">
|
|
||||||
<div class="modal">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h2 id="detailModalTitle" class="modal-title">회의록 상세</h2>
|
|
||||||
<button class="modal-close" aria-label="닫기" onclick="hideModal('detailModal')">✕</button>
|
|
||||||
</div>
|
|
||||||
<div id="detailModalContent" class="modal-body">
|
|
||||||
<!-- 동적 생성 -->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- JavaScript -->
|
|
||||||
<script src="common.js"></script>
|
|
||||||
<script>
|
|
||||||
(function() {
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
let meetings = [];
|
|
||||||
let filteredMeetings = [];
|
|
||||||
|
|
||||||
// 초기화
|
|
||||||
function init() {
|
|
||||||
loadMeetings();
|
|
||||||
renderMeetings();
|
|
||||||
checkOngoingMeeting();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 회의록 로드
|
|
||||||
function loadMeetings() {
|
|
||||||
meetings = loadData('meetings') || mockMeetings;
|
|
||||||
filteredMeetings = [...meetings];
|
|
||||||
}
|
|
||||||
|
|
||||||
// 회의록 렌더링
|
|
||||||
function renderMeetings() {
|
|
||||||
const listElement = $('#meetingList');
|
|
||||||
const countElement = $('#meetingCount');
|
|
||||||
|
|
||||||
if (filteredMeetings.length === 0) {
|
|
||||||
listElement.innerHTML = `
|
|
||||||
<div class="empty-state">
|
|
||||||
<div class="empty-icon">📝</div>
|
|
||||||
<h3 class="empty-title">회의록이 없습니다</h3>
|
|
||||||
<p class="empty-description">새 회의를 예약하여 회의록을 작성해보세요</p>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
countElement.textContent = '0건';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
listElement.innerHTML = filteredMeetings.map(meeting => {
|
|
||||||
const statusBadge = getStatusBadge(meeting.status);
|
|
||||||
const progressBar = meeting.status === 'in-progress' ? `
|
|
||||||
<div class="progress-wrapper">
|
|
||||||
<div class="progress-label">
|
|
||||||
<span>진행률</span>
|
|
||||||
<span>${meeting.progress}%</span>
|
|
||||||
</div>
|
|
||||||
<div class="progress-bar">
|
|
||||||
<div class="progress-fill ${meeting.progress >= 100 ? 'success' : ''}" style="width: ${meeting.progress}%"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
` : '';
|
|
||||||
|
|
||||||
return `
|
|
||||||
<div class="meeting-card" onclick="showMeetingDetail(${meeting.id})">
|
|
||||||
<div class="meeting-card-header">
|
|
||||||
<div style="flex: 1;">
|
|
||||||
<div class="meeting-title">
|
|
||||||
<span class="meeting-icon">📝</span>
|
|
||||||
${meeting.title}
|
|
||||||
</div>
|
|
||||||
<div class="meeting-meta">
|
|
||||||
<span class="meeting-meta-item">📅 ${formatDate(meeting.date)} ${meeting.time}</span>
|
|
||||||
<span class="meeting-meta-item">${statusBadge}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="meeting-footer">
|
|
||||||
<span class="meeting-attendees">👥 ${meeting.attendees.length}명 참석</span>
|
|
||||||
</div>
|
|
||||||
${progressBar}
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}).join('');
|
|
||||||
|
|
||||||
countElement.textContent = `${filteredMeetings.length}건`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 상태 배지
|
|
||||||
function getStatusBadge(status) {
|
|
||||||
const badges = {
|
|
||||||
'confirmed': '<span class="badge badge-confirmed">✅ 확정완료</span>',
|
|
||||||
'in-progress': '<span class="badge badge-in-progress">⚠️ 작성중</span>',
|
|
||||||
'draft': '<span class="badge badge-pending">📝 임시저장</span>'
|
|
||||||
};
|
|
||||||
return badges[status] || '';
|
|
||||||
}
|
|
||||||
|
|
||||||
// 필터 적용
|
|
||||||
window.applyFilters = function() {
|
|
||||||
const statusFilter = $('#statusFilter').value;
|
|
||||||
const sortFilter = $('#sortFilter').value;
|
|
||||||
const searchQuery = $('#searchInput').value.toLowerCase();
|
|
||||||
|
|
||||||
filteredMeetings = meetings.filter(meeting => {
|
|
||||||
const matchesStatus = statusFilter === 'all' || meeting.status === statusFilter;
|
|
||||||
const matchesSearch = !searchQuery ||
|
|
||||||
meeting.title.toLowerCase().includes(searchQuery) ||
|
|
||||||
meeting.keywords.some(k => k.toLowerCase().includes(searchQuery));
|
|
||||||
return matchesStatus && matchesSearch;
|
|
||||||
});
|
|
||||||
|
|
||||||
// 정렬
|
|
||||||
if (sortFilter === 'latest') {
|
|
||||||
filteredMeetings.sort((a, b) => new Date(b.date) - new Date(a.date));
|
|
||||||
} else if (sortFilter === 'date') {
|
|
||||||
filteredMeetings.sort((a, b) => new Date(a.date) - new Date(b.date));
|
|
||||||
} else if (sortFilter === 'title') {
|
|
||||||
filteredMeetings.sort((a, b) => a.title.localeCompare(b.title, 'ko'));
|
|
||||||
}
|
|
||||||
|
|
||||||
renderMeetings();
|
|
||||||
};
|
|
||||||
|
|
||||||
// 검색 (디바운싱)
|
|
||||||
window.handleSearch = debounce(function() {
|
|
||||||
applyFilters();
|
|
||||||
}, 300);
|
|
||||||
|
|
||||||
// 회의록 상세 보기
|
|
||||||
window.showMeetingDetail = function(meetingId) {
|
|
||||||
const meeting = getMeetingById(meetingId);
|
|
||||||
if (!meeting) return;
|
|
||||||
|
|
||||||
const modalContent = $('#detailModalContent');
|
|
||||||
modalContent.innerHTML = `
|
|
||||||
<div class="detail-section">
|
|
||||||
<div class="detail-section-title">회의 정보</div>
|
|
||||||
<div class="detail-content">
|
|
||||||
<strong>제목:</strong> ${meeting.title}<br>
|
|
||||||
<strong>일시:</strong> ${formatDateTime(meeting.date + ' ' + meeting.time)}<br>
|
|
||||||
<strong>장소:</strong> ${meeting.location}<br>
|
|
||||||
<strong>참석자:</strong> ${meeting.attendees.map(id => getUserById(id)?.name).join(', ')}<br>
|
|
||||||
<strong>상태:</strong> ${getStatusBadge(meeting.status)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
${meeting.sections.map(section => `
|
|
||||||
<div class="detail-section">
|
|
||||||
<div class="detail-section-title">
|
|
||||||
${section.title}
|
|
||||||
${section.verified ? '<span class="badge badge-verified" style="margin-left: 8px;">✅ 검증완료</span>' : ''}
|
|
||||||
</div>
|
|
||||||
<div class="detail-content">${section.content}</div>
|
|
||||||
</div>
|
|
||||||
`).join('')}
|
|
||||||
|
|
||||||
${meeting.todos && meeting.todos.length > 0 ? `
|
|
||||||
<div class="detail-section">
|
|
||||||
<div class="detail-section-title">Todo</div>
|
|
||||||
${meeting.todos.map(todo => {
|
|
||||||
const assignee = getUserById(todo.assignee);
|
|
||||||
return `
|
|
||||||
<div class="todo-card priority-${todo.priority}" style="margin-bottom: 8px;">
|
|
||||||
<div class="todo-checkbox ${todo.status === 'completed' ? 'checked' : ''}"></div>
|
|
||||||
<div class="todo-content">
|
|
||||||
<div class="todo-title ${todo.status === 'completed' ? 'completed' : ''}">${todo.content}</div>
|
|
||||||
<div class="todo-meta">
|
|
||||||
<span class="todo-assignee">👤 ${assignee?.name}</span>
|
|
||||||
<span class="todo-duedate">📅 ~ ${formatDate(todo.dueDate)} (${getDDay(todo.dueDate)})</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}).join('')}
|
|
||||||
</div>
|
|
||||||
` : ''}
|
|
||||||
|
|
||||||
<div class="detail-actions">
|
|
||||||
<button class="button button-outline" style="flex: 1;" onclick="handleEdit(${meeting.id})">✏️ 수정</button>
|
|
||||||
<button class="button button-outline" style="flex: 1;" onclick="handleShare(${meeting.id})">📤 공유</button>
|
|
||||||
<button class="button button-secondary" style="flex: 1;" onclick="handleDownloadPDF(${meeting.id})">📄 PDF</button>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
showModal('detailModal');
|
|
||||||
};
|
|
||||||
|
|
||||||
// 수정
|
|
||||||
window.handleEdit = function(meetingId) {
|
|
||||||
showToast('수정 기능은 준비 중입니다', 'info');
|
|
||||||
};
|
|
||||||
|
|
||||||
// 공유
|
|
||||||
window.handleShare = function(meetingId) {
|
|
||||||
navigateTo('08-회의록공유.html');
|
|
||||||
};
|
|
||||||
|
|
||||||
// PDF 다운로드
|
|
||||||
window.handleDownloadPDF = function(meetingId) {
|
|
||||||
showToast('PDF 다운로드 중...', 'success', 2000);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 진행 중인 회의 체크
|
|
||||||
function checkOngoingMeeting() {
|
|
||||||
const ongoingMeeting = meetings.find(m => m.status === 'in-progress');
|
|
||||||
if (ongoingMeeting) {
|
|
||||||
$('#ongoingMeeting').style.display = 'flex';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 진행 중인 회의 클릭
|
|
||||||
window.handleOngoingMeetingClick = function() {
|
|
||||||
navigateTo('05-회의진행.html');
|
|
||||||
};
|
|
||||||
|
|
||||||
// 초기화
|
|
||||||
if (document.readyState === 'loading') {
|
|
||||||
document.addEventListener('DOMContentLoaded', init);
|
|
||||||
} else {
|
|
||||||
init();
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@ -1,612 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="ko">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<meta name="description" content="회의록 작성 및 공유 개선 서비스 - 회의 예약">
|
|
||||||
<title>회의 예약 - 회의록 작성 서비스</title>
|
|
||||||
|
|
||||||
<!-- CSS -->
|
|
||||||
<link rel="stylesheet" href="common.css">
|
|
||||||
|
|
||||||
<!-- Pretendard Font -->
|
|
||||||
<link rel="stylesheet" as="style" crossorigin href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/static/pretendard.min.css" />
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.page-container {
|
|
||||||
min-height: 100vh;
|
|
||||||
background-color: var(--bg-secondary);
|
|
||||||
padding-bottom: var(--space-8);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 헤더 */
|
|
||||||
.header {
|
|
||||||
position: sticky;
|
|
||||||
top: 0;
|
|
||||||
background-color: var(--bg-primary);
|
|
||||||
border-bottom: var(--border-thin) solid var(--gray-200);
|
|
||||||
padding: var(--space-4);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
z-index: 10;
|
|
||||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-left {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--space-3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.back-button {
|
|
||||||
width: 40px;
|
|
||||||
height: 40px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
border-radius: var(--radius-full);
|
|
||||||
background-color: transparent;
|
|
||||||
border: none;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 1.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.back-button:hover {
|
|
||||||
background-color: var(--gray-100);
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-title {
|
|
||||||
font-size: 1.25rem;
|
|
||||||
font-weight: 700;
|
|
||||||
color: var(--text-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 폼 영역 */
|
|
||||||
.form-container {
|
|
||||||
max-width: 600px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: var(--space-4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-section {
|
|
||||||
background-color: var(--bg-primary);
|
|
||||||
border-radius: var(--radius-large);
|
|
||||||
padding: var(--space-6);
|
|
||||||
margin-bottom: var(--space-4);
|
|
||||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-section-title {
|
|
||||||
font-size: 1.125rem;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--text-primary);
|
|
||||||
margin-bottom: var(--space-6);
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group {
|
|
||||||
margin-bottom: var(--space-5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group:last-child {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.datetime-group {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr 1fr;
|
|
||||||
gap: var(--space-3);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 참석자 칩 */
|
|
||||||
.attendee-chips {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: var(--space-2);
|
|
||||||
margin-bottom: var(--space-3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.chip {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--space-2);
|
|
||||||
background-color: var(--primary-50);
|
|
||||||
color: var(--primary-700);
|
|
||||||
border: var(--border-thin) solid var(--primary-200);
|
|
||||||
border-radius: var(--radius-full);
|
|
||||||
padding: var(--space-1) var(--space-3);
|
|
||||||
font-size: 0.875rem;
|
|
||||||
animation: fade-in var(--duration-fast) ease-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chip-remove {
|
|
||||||
cursor: pointer;
|
|
||||||
color: var(--primary-500);
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 1rem;
|
|
||||||
line-height: 1;
|
|
||||||
transition: color var(--duration-instant) ease-in-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chip-remove:hover {
|
|
||||||
color: var(--error-500);
|
|
||||||
}
|
|
||||||
|
|
||||||
.add-attendee-group {
|
|
||||||
display: flex;
|
|
||||||
gap: var(--space-2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.add-attendee-input {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 체크박스 */
|
|
||||||
.checkbox-wrapper {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--space-2);
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.custom-checkbox {
|
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
border: var(--border-medium) solid var(--gray-300);
|
|
||||||
border-radius: var(--radius-small);
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all var(--duration-instant) ease-in-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
.custom-checkbox.checked {
|
|
||||||
background-color: var(--primary-500);
|
|
||||||
border-color: var(--primary-500);
|
|
||||||
}
|
|
||||||
|
|
||||||
.custom-checkbox.checked::after {
|
|
||||||
content: '✓';
|
|
||||||
color: white;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.checkbox-label {
|
|
||||||
font-size: 0.875rem;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 제출 버튼 */
|
|
||||||
.submit-section {
|
|
||||||
max-width: 600px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 0 var(--space-4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.submit-button {
|
|
||||||
width: 100%;
|
|
||||||
height: 56px;
|
|
||||||
font-size: 1rem;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 헬퍼 텍스트 */
|
|
||||||
.helper-text {
|
|
||||||
font-size: 0.75rem;
|
|
||||||
color: var(--text-tertiary);
|
|
||||||
margin-top: var(--space-1);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
|
||||||
.form-container {
|
|
||||||
padding: var(--space-6);
|
|
||||||
}
|
|
||||||
|
|
||||||
.datetime-group {
|
|
||||||
grid-template-columns: 2fr 1fr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="page-container">
|
|
||||||
<!-- 헤더 -->
|
|
||||||
<header class="header">
|
|
||||||
<div class="header-left">
|
|
||||||
<button class="back-button" onclick="goBack()" aria-label="뒤로가기">
|
|
||||||
←
|
|
||||||
</button>
|
|
||||||
<h1 class="header-title">회의 예약</h1>
|
|
||||||
</div>
|
|
||||||
<button class="button button-ghost button-small" onclick="handleSaveDraft()">
|
|
||||||
임시저장
|
|
||||||
</button>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<!-- 폼 -->
|
|
||||||
<div class="form-container">
|
|
||||||
<form id="meetingForm" novalidate>
|
|
||||||
<!-- 기본 정보 -->
|
|
||||||
<div class="form-section">
|
|
||||||
<h2 class="form-section-title">기본 정보</h2>
|
|
||||||
|
|
||||||
<!-- 회의 제목 -->
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="meetingTitle" class="input-label required">회의 제목</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
id="meetingTitle"
|
|
||||||
class="input-field"
|
|
||||||
placeholder="예: 프로젝트 킥오프 미팅"
|
|
||||||
maxlength="100"
|
|
||||||
required
|
|
||||||
aria-label="회의 제목"
|
|
||||||
aria-describedby="meetingTitleError"
|
|
||||||
>
|
|
||||||
<span id="meetingTitleError" class="input-error-message" role="alert"></span>
|
|
||||||
<p class="helper-text">최대 100자까지 입력 가능합니다</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 날짜 및 시간 -->
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="input-label required">날짜 및 시간</label>
|
|
||||||
<div class="datetime-group">
|
|
||||||
<div>
|
|
||||||
<input
|
|
||||||
type="date"
|
|
||||||
id="meetingDate"
|
|
||||||
class="input-field"
|
|
||||||
required
|
|
||||||
aria-label="회의 날짜"
|
|
||||||
aria-describedby="meetingDateError"
|
|
||||||
>
|
|
||||||
<span id="meetingDateError" class="input-error-message" role="alert"></span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<input
|
|
||||||
type="time"
|
|
||||||
id="meetingTime"
|
|
||||||
class="input-field"
|
|
||||||
required
|
|
||||||
aria-label="회의 시간"
|
|
||||||
aria-describedby="meetingTimeError"
|
|
||||||
>
|
|
||||||
<span id="meetingTimeError" class="input-error-message" role="alert"></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 장소 -->
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="meetingLocation" class="input-label">장소 (선택)</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
id="meetingLocation"
|
|
||||||
class="input-field"
|
|
||||||
placeholder="예: 회의실 A 또는 온라인"
|
|
||||||
maxlength="200"
|
|
||||||
aria-label="회의 장소"
|
|
||||||
>
|
|
||||||
<p class="helper-text">최대 200자까지 입력 가능합니다</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 참석자 -->
|
|
||||||
<div class="form-section">
|
|
||||||
<h2 class="form-section-title">참석자</h2>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="input-label required">참석자 목록</label>
|
|
||||||
<div id="attendeeChips" class="attendee-chips">
|
|
||||||
<!-- 동적 생성 -->
|
|
||||||
</div>
|
|
||||||
<div class="add-attendee-group">
|
|
||||||
<input
|
|
||||||
type="email"
|
|
||||||
id="attendeeEmail"
|
|
||||||
class="input-field add-attendee-input"
|
|
||||||
placeholder="이메일 주소 입력 후 Enter 또는 추가 버튼"
|
|
||||||
aria-label="참석자 이메일"
|
|
||||||
>
|
|
||||||
<button type="button" class="button button-primary" onclick="handleAddAttendee()">
|
|
||||||
추가
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<span id="attendeeError" class="input-error-message" role="alert"></span>
|
|
||||||
<p class="helper-text">최소 1명 이상의 참석자를 추가해주세요</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 리마인더 -->
|
|
||||||
<div class="form-section">
|
|
||||||
<h2 class="form-section-title">알림 설정</h2>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<div class="checkbox-wrapper" onclick="toggleReminder()">
|
|
||||||
<div id="reminderCheckbox" class="custom-checkbox checked"></div>
|
|
||||||
<label class="checkbox-label">회의 시작 30분 전 리마인더 발송</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 제출 버튼 -->
|
|
||||||
<div class="submit-section">
|
|
||||||
<button class="button button-primary submit-button" onclick="handleSubmit()">
|
|
||||||
회의 예약하기
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- JavaScript -->
|
|
||||||
<script src="common.js"></script>
|
|
||||||
<script>
|
|
||||||
(function() {
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
let attendees = [];
|
|
||||||
let reminderEnabled = true;
|
|
||||||
|
|
||||||
// 초기화
|
|
||||||
function init() {
|
|
||||||
setupEventListeners();
|
|
||||||
setMinDate();
|
|
||||||
loadDraft();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 이벤트 리스너 설정
|
|
||||||
function setupEventListeners() {
|
|
||||||
const attendeeInput = $('#attendeeEmail');
|
|
||||||
attendeeInput.addEventListener('keypress', (e) => {
|
|
||||||
if (e.key === 'Enter') {
|
|
||||||
e.preventDefault();
|
|
||||||
handleAddAttendee();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 실시간 검증
|
|
||||||
setupRealtimeValidation($('#meetingTitle'));
|
|
||||||
setupRealtimeValidation($('#meetingDate'));
|
|
||||||
setupRealtimeValidation($('#meetingTime'));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 최소 날짜 설정 (오늘)
|
|
||||||
function setMinDate() {
|
|
||||||
const today = new Date().toISOString().split('T')[0];
|
|
||||||
$('#meetingDate').setAttribute('min', today);
|
|
||||||
$('#meetingDate').value = today;
|
|
||||||
|
|
||||||
const currentTime = new Date();
|
|
||||||
const hours = String(currentTime.getHours()).padStart(2, '0');
|
|
||||||
const minutes = String(currentTime.getMinutes()).padStart(2, '0');
|
|
||||||
$('#meetingTime').value = `${hours}:${minutes}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 참석자 추가
|
|
||||||
window.handleAddAttendee = function() {
|
|
||||||
const emailInput = $('#attendeeEmail');
|
|
||||||
const email = emailInput.value.trim();
|
|
||||||
const errorElement = $('#attendeeError');
|
|
||||||
|
|
||||||
if (!email) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!validateEmail(email)) {
|
|
||||||
errorElement.textContent = '올바른 이메일 주소를 입력해주세요';
|
|
||||||
addClass(emailInput, 'error');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (attendees.includes(email)) {
|
|
||||||
errorElement.textContent = '이미 추가된 참석자입니다';
|
|
||||||
addClass(emailInput, 'error');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
attendees.push(email);
|
|
||||||
emailInput.value = '';
|
|
||||||
removeClass(emailInput, 'error');
|
|
||||||
errorElement.textContent = '';
|
|
||||||
|
|
||||||
renderAttendees();
|
|
||||||
saveDraft();
|
|
||||||
};
|
|
||||||
|
|
||||||
// 참석자 제거
|
|
||||||
window.handleRemoveAttendee = function(email) {
|
|
||||||
attendees = attendees.filter(a => a !== email);
|
|
||||||
renderAttendees();
|
|
||||||
saveDraft();
|
|
||||||
};
|
|
||||||
|
|
||||||
// 참석자 렌더링
|
|
||||||
function renderAttendees() {
|
|
||||||
const chipsContainer = $('#attendeeChips');
|
|
||||||
|
|
||||||
if (attendees.length === 0) {
|
|
||||||
chipsContainer.innerHTML = '<p class="helper-text">참석자를 추가해주세요</p>';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
chipsContainer.innerHTML = attendees.map(email => `
|
|
||||||
<div class="chip">
|
|
||||||
<span>${email}</span>
|
|
||||||
<span class="chip-remove" onclick="handleRemoveAttendee('${email}')" aria-label="${email} 제거">×</span>
|
|
||||||
</div>
|
|
||||||
`).join('');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 리마인더 토글
|
|
||||||
window.toggleReminder = function() {
|
|
||||||
reminderEnabled = !reminderEnabled;
|
|
||||||
const checkbox = $('#reminderCheckbox');
|
|
||||||
|
|
||||||
if (reminderEnabled) {
|
|
||||||
addClass(checkbox, 'checked');
|
|
||||||
} else {
|
|
||||||
removeClass(checkbox, 'checked');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 임시 저장
|
|
||||||
window.handleSaveDraft = function() {
|
|
||||||
saveDraft();
|
|
||||||
showToast('임시 저장되었습니다', 'success');
|
|
||||||
};
|
|
||||||
|
|
||||||
function saveDraft() {
|
|
||||||
const draft = {
|
|
||||||
title: $('#meetingTitle').value,
|
|
||||||
date: $('#meetingDate').value,
|
|
||||||
time: $('#meetingTime').value,
|
|
||||||
location: $('#meetingLocation').value,
|
|
||||||
attendees: attendees,
|
|
||||||
reminderEnabled: reminderEnabled,
|
|
||||||
savedAt: new Date().toISOString()
|
|
||||||
};
|
|
||||||
|
|
||||||
saveData('meetingDraft', draft);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 임시 저장 불러오기
|
|
||||||
function loadDraft() {
|
|
||||||
const draft = loadData('meetingDraft');
|
|
||||||
|
|
||||||
if (!draft) return;
|
|
||||||
|
|
||||||
// 30분 이내 임시 저장만 복원
|
|
||||||
const savedTime = new Date(draft.savedAt);
|
|
||||||
const now = new Date();
|
|
||||||
const diffMinutes = (now - savedTime) / (1000 * 60);
|
|
||||||
|
|
||||||
if (diffMinutes > 30) {
|
|
||||||
removeData('meetingDraft');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$('#meetingTitle').value = draft.title || '';
|
|
||||||
$('#meetingDate').value = draft.date || '';
|
|
||||||
$('#meetingTime').value = draft.time || '';
|
|
||||||
$('#meetingLocation').value = draft.location || '';
|
|
||||||
attendees = draft.attendees || [];
|
|
||||||
reminderEnabled = draft.reminderEnabled !== false;
|
|
||||||
|
|
||||||
renderAttendees();
|
|
||||||
|
|
||||||
if (!reminderEnabled) {
|
|
||||||
removeClass($('#reminderCheckbox'), 'checked');
|
|
||||||
}
|
|
||||||
|
|
||||||
showToast('임시 저장된 내용을 불러왔습니다', 'info');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 폼 검증
|
|
||||||
function validateForm() {
|
|
||||||
let isValid = true;
|
|
||||||
|
|
||||||
// 제목
|
|
||||||
const title = $('#meetingTitle').value.trim();
|
|
||||||
if (!title) {
|
|
||||||
showError($('#meetingTitle'), $('#meetingTitleError'), '회의 제목을 입력해주세요');
|
|
||||||
isValid = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 날짜
|
|
||||||
const date = $('#meetingDate').value;
|
|
||||||
if (!date) {
|
|
||||||
showError($('#meetingDate'), $('#meetingDateError'), '날짜를 선택해주세요');
|
|
||||||
isValid = false;
|
|
||||||
} else {
|
|
||||||
const selectedDate = new Date(date);
|
|
||||||
const today = new Date();
|
|
||||||
today.setHours(0, 0, 0, 0);
|
|
||||||
|
|
||||||
if (selectedDate < today) {
|
|
||||||
showError($('#meetingDate'), $('#meetingDateError'), '과거 날짜는 선택할 수 없습니다');
|
|
||||||
isValid = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 시간
|
|
||||||
const time = $('#meetingTime').value;
|
|
||||||
if (!time) {
|
|
||||||
showError($('#meetingTime'), $('#meetingTimeError'), '시간을 선택해주세요');
|
|
||||||
isValid = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 참석자
|
|
||||||
if (attendees.length === 0) {
|
|
||||||
showError($('#attendeeEmail'), $('#attendeeError'), '최소 1명 이상의 참석자를 추가해주세요');
|
|
||||||
isValid = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return isValid;
|
|
||||||
}
|
|
||||||
|
|
||||||
function showError(inputElement, errorElement, message) {
|
|
||||||
addClass(inputElement, 'error');
|
|
||||||
errorElement.textContent = message;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 제출
|
|
||||||
window.handleSubmit = function() {
|
|
||||||
if (!validateForm()) {
|
|
||||||
showToast('입력 항목을 확인해주세요', 'error');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 로딩 표시
|
|
||||||
showToast('회의를 예약하고 있습니다...', 'info', 1500);
|
|
||||||
|
|
||||||
// 회의 예약 처리 (시뮬레이션)
|
|
||||||
setTimeout(() => {
|
|
||||||
const newMeeting = {
|
|
||||||
id: Date.now(),
|
|
||||||
title: $('#meetingTitle').value.trim(),
|
|
||||||
date: $('#meetingDate').value,
|
|
||||||
time: $('#meetingTime').value,
|
|
||||||
location: $('#meetingLocation').value.trim() || '미정',
|
|
||||||
attendees: attendees,
|
|
||||||
status: 'draft',
|
|
||||||
progress: 0,
|
|
||||||
sections: [],
|
|
||||||
todos: [],
|
|
||||||
keywords: [],
|
|
||||||
reminderEnabled: reminderEnabled,
|
|
||||||
createdAt: new Date().toISOString()
|
|
||||||
};
|
|
||||||
|
|
||||||
// 저장
|
|
||||||
const meetings = loadData('meetings') || [];
|
|
||||||
meetings.unshift(newMeeting);
|
|
||||||
saveData('meetings', meetings);
|
|
||||||
|
|
||||||
// 임시 저장 삭제
|
|
||||||
removeData('meetingDraft');
|
|
||||||
|
|
||||||
// 성공 메시지
|
|
||||||
showToast('회의 예약이 완료되었습니다', 'success', 2000);
|
|
||||||
|
|
||||||
// 템플릿 선택 화면으로 이동
|
|
||||||
setTimeout(() => {
|
|
||||||
saveData('currentMeetingId', newMeeting.id);
|
|
||||||
navigateTo('04-템플릿선택.html');
|
|
||||||
}, 2000);
|
|
||||||
}, 1500);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 초기화
|
|
||||||
if (document.readyState === 'loading') {
|
|
||||||
document.addEventListener('DOMContentLoaded', init);
|
|
||||||
} else {
|
|
||||||
init();
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
File diff suppressed because it is too large
Load Diff
@ -1,537 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="ko">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<meta name="description" content="회의록 작성 및 공유 개선 서비스 - 회의진행">
|
|
||||||
<title>회의 진행 - 회의록 작성 서비스</title>
|
|
||||||
<link rel="stylesheet" href="common.css">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<!-- Skip to Main Content (접근성) -->
|
|
||||||
<a href="#main-content" class="skip-to-main">본문으로 건너뛰기</a>
|
|
||||||
|
|
||||||
<!-- Header -->
|
|
||||||
<header style="position: sticky; top: 0; background: var(--bg-primary); border-bottom: var(--border-thin) solid var(--gray-200); z-index: 10; padding: var(--space-4);">
|
|
||||||
<div style="display: flex; align-items: center; justify-content: space-between; max-width: 1440px; margin: 0 auto;">
|
|
||||||
<button class="button-icon button-ghost" onclick="goBack()" aria-label="뒤로가기">
|
|
||||||
<span style="font-size: 24px;">←</span>
|
|
||||||
</button>
|
|
||||||
<h1 class="h4" style="margin: 0;">프로젝트 킥오프</h1>
|
|
||||||
<button class="button-secondary button-small" onclick="endMeeting()" aria-label="회의 종료">
|
|
||||||
종료
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<!-- Main Content -->
|
|
||||||
<main id="main-content" class="container" style="padding-top: var(--space-4); padding-bottom: 80px; max-width: 1024px;">
|
|
||||||
|
|
||||||
<!-- Voice Recording Section -->
|
|
||||||
<section aria-labelledby="recording-section" style="margin-bottom: var(--space-6);">
|
|
||||||
<div class="voice-recording">
|
|
||||||
<div class="recording-indicator" aria-label="녹음 중"></div>
|
|
||||||
<div class="recording-timer" id="timer" aria-live="polite">23:45</div>
|
|
||||||
<div class="waveform" aria-hidden="true">
|
|
||||||
<div class="waveform-bar" style="height: 20px;"></div>
|
|
||||||
<div class="waveform-bar" style="height: 30px;"></div>
|
|
||||||
<div class="waveform-bar" style="height: 40px;"></div>
|
|
||||||
<div class="waveform-bar" style="height: 25px;"></div>
|
|
||||||
<div class="waveform-bar" style="height: 35px;"></div>
|
|
||||||
<div class="waveform-bar" style="height: 30px;"></div>
|
|
||||||
<div class="waveform-bar" style="height: 20px;"></div>
|
|
||||||
<div class="waveform-bar" style="height: 15px;"></div>
|
|
||||||
<div class="waveform-bar" style="height: 25px;"></div>
|
|
||||||
<div class="waveform-bar" style="height: 35px;"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- Attendees Section -->
|
|
||||||
<section aria-labelledby="attendees-section" style="margin-bottom: var(--space-6);">
|
|
||||||
<h2 class="h4" id="attendees-section" style="margin-bottom: var(--space-3);">👥 참석자 (3/5명)</h2>
|
|
||||||
<div style="display: flex; gap: var(--space-3); flex-wrap: wrap;">
|
|
||||||
<div class="badge badge-verified" style="padding: var(--space-2) var(--space-3); font-size: 0.875rem;">
|
|
||||||
<span style="font-size: 20px; margin-right: var(--space-1);">👨💼</span>
|
|
||||||
<span>김민준</span>
|
|
||||||
</div>
|
|
||||||
<div class="badge badge-verified" style="padding: var(--space-2) var(--space-3); font-size: 0.875rem;">
|
|
||||||
<span style="font-size: 20px; margin-right: var(--space-1);">👩💻</span>
|
|
||||||
<span>박서연</span>
|
|
||||||
</div>
|
|
||||||
<div class="badge badge-verified" style="padding: var(--space-2) var(--space-3); font-size: 0.875rem;">
|
|
||||||
<span style="font-size: 20px; margin-right: var(--space-1);">👨💻</span>
|
|
||||||
<span>이준호</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- Meeting Minutes Sections -->
|
|
||||||
<section aria-labelledby="minutes-section" style="margin-bottom: var(--space-6);">
|
|
||||||
<h2 class="h4" id="minutes-section" style="margin-bottom: var(--space-4);">📝 실시간 회의록</h2>
|
|
||||||
|
|
||||||
<!-- 참석자 섹션 -->
|
|
||||||
<div class="card" style="margin-bottom: var(--space-3);">
|
|
||||||
<button class="w-full" style="display: flex; justify-content: space-between; align-items: center; text-align: left; padding: var(--space-3);" onclick="toggleSection('attendees')" aria-expanded="false" aria-controls="attendees-content">
|
|
||||||
<h3 class="h4" style="margin: 0;">▼ 참석자</h3>
|
|
||||||
<span class="badge badge-verified">검증완료</span>
|
|
||||||
</button>
|
|
||||||
<div id="attendees-content" style="padding: 0 var(--space-3) var(--space-3); display: none;">
|
|
||||||
<p class="text-body" style="margin: var(--space-2) 0;">- 김민준 (주관자)</p>
|
|
||||||
<p class="text-body" style="margin: var(--space-2) 0;">- 박서연</p>
|
|
||||||
<p class="text-body" style="margin: var(--space-2) 0;">- 이준호</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 안건 섹션 -->
|
|
||||||
<div class="card" style="margin-bottom: var(--space-3);">
|
|
||||||
<button class="w-full" style="display: flex; justify-content: space-between; align-items: center; text-align: left; padding: var(--space-3);" onclick="toggleSection('agenda')" aria-expanded="false" aria-controls="agenda-content">
|
|
||||||
<h3 class="h4" style="margin: 0;">▼ 안건</h3>
|
|
||||||
<span class="badge badge-verified">검증완료</span>
|
|
||||||
</button>
|
|
||||||
<div id="agenda-content" style="padding: 0 var(--space-3) var(--space-3); display: none;">
|
|
||||||
<p class="text-body" style="margin: var(--space-2) 0;">- 프로젝트 목표 정의</p>
|
|
||||||
<p class="text-body" style="margin: var(--space-2) 0;">- 일정 및 마일스톤</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 논의 내용 섹션 (차별화 기능 포함) -->
|
|
||||||
<div class="card" style="margin-bottom: var(--space-3);">
|
|
||||||
<button class="w-full" style="display: flex; justify-content: space-between; align-items: center; text-align: left; padding: var(--space-3);" onclick="toggleSection('discussion')" aria-expanded="true" aria-controls="discussion-content">
|
|
||||||
<h3 class="h4" style="margin: 0;">▼ 논의 내용</h3>
|
|
||||||
<span class="badge badge-in-progress">작성중</span>
|
|
||||||
</button>
|
|
||||||
<div id="discussion-content" style="padding: 0 var(--space-3) var(--space-3);">
|
|
||||||
<!-- 실시간 텍스트 영역 -->
|
|
||||||
<div class="realtime-text" style="margin-bottom: var(--space-3);">
|
|
||||||
<div style="margin-bottom: var(--space-2);">
|
|
||||||
<span class="speaker-name">김민준</span>
|
|
||||||
<span class="timestamp">14:23</span>
|
|
||||||
</div>
|
|
||||||
<div class="text-content">
|
|
||||||
우리는 <span class="term-highlight" onclick="showTermTooltip(event, 'q1')" role="button" tabindex="0" aria-label="Q1 용어 설명 보기">Q1</span>까지
|
|
||||||
<span class="term-highlight" onclick="showTermTooltip(event, 'mvp')" role="button" tabindex="0" aria-label="MVP 용어 설명 보기">MVP</span>를 완성해야 합니다.
|
|
||||||
개발 프레임워크는 <span class="term-highlight" onclick="showTermTooltip(event, 'react')" role="button" tabindex="0" aria-label="React 용어 설명 보기">React</span>를 사용하고,
|
|
||||||
배포 환경은 <span class="term-highlight" onclick="showTermTooltip(event, 'aws')" role="button" tabindex="0" aria-label="AWS 용어 설명 보기">AWS</span>로 결정했습니다.
|
|
||||||
<span class="typing-indicator" aria-label="입력 중" aria-live="polite"></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- AI 자동 정리 영역 -->
|
|
||||||
<div style="background-color: var(--primary-50); border: var(--border-thin) solid var(--primary-200); border-radius: var(--radius-medium); padding: var(--space-3); margin-bottom: var(--space-3);">
|
|
||||||
<div style="display: flex; align-items: center; gap: var(--space-2); margin-bottom: var(--space-2);">
|
|
||||||
<span style="font-size: 18px;">💡</span>
|
|
||||||
<span class="text-caption" style="color: var(--primary-700); font-weight: 600;">AI 자동 정리</span>
|
|
||||||
</div>
|
|
||||||
<p class="text-body" style="color: var(--gray-700);">
|
|
||||||
Q1(1분기)까지 MVP(최소 기능 제품) 완성을 목표로 설정했습니다.
|
|
||||||
개발 프레임워크로 React를 선택하고, 배포 환경은 AWS를 사용하기로 결정했습니다.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 실시간 협업 표시 -->
|
|
||||||
<div class="realtime-text" style="background-color: var(--warning-50); border: var(--border-thin) solid var(--warning-200); animation: highlight-fade 3s ease-out;">
|
|
||||||
<div style="margin-bottom: var(--space-2);">
|
|
||||||
<span class="speaker-name" style="background-color: var(--info-100); color: var(--info-700);">박서연</span>
|
|
||||||
<span class="timestamp">14:24</span>
|
|
||||||
<span class="text-caption" style="color: var(--warning-700); margin-left: var(--space-2);">수정 중...</span>
|
|
||||||
</div>
|
|
||||||
<div class="text-content">
|
|
||||||
<span class="term-highlight" onclick="showTermTooltip(event, 'sprint')" role="button" tabindex="0" aria-label="Sprint 용어 설명 보기">Sprint</span> 주기는 2주로 하는 게 좋을 것 같습니다.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 액션 버튼 -->
|
|
||||||
<div style="display: flex; gap: var(--space-2); margin-top: var(--space-4);">
|
|
||||||
<button class="button-secondary button-small" onclick="editSection('discussion')">
|
|
||||||
<span>📝</span> 수정
|
|
||||||
</button>
|
|
||||||
<button class="button-secondary button-small" onclick="addComment('discussion')">
|
|
||||||
<span>💬</span> 댓글
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 결정 사항 섹션 -->
|
|
||||||
<div class="card" style="margin-bottom: var(--space-3);">
|
|
||||||
<button class="w-full" style="display: flex; justify-content: space-between; align-items: center; text-align: left; padding: var(--space-3);" onclick="toggleSection('decisions')" aria-expanded="false" aria-controls="decisions-content">
|
|
||||||
<h3 class="h4" style="margin: 0;">▼ 결정 사항</h3>
|
|
||||||
<span class="badge badge-pending">검증 필요</span>
|
|
||||||
</button>
|
|
||||||
<div id="decisions-content" style="padding: 0 var(--space-3) var(--space-3); display: none;">
|
|
||||||
<p class="text-body" style="margin: var(--space-2) 0;">- 개발 프레임워크: React</p>
|
|
||||||
<p class="text-body" style="margin: var(--space-2) 0;">- 배포 환경: AWS</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Todo 섹션 -->
|
|
||||||
<div class="card">
|
|
||||||
<button class="w-full" style="display: flex; justify-content: space-between; align-items: center; text-align: left; padding: var(--space-3);" onclick="toggleSection('todos')" aria-expanded="false" aria-controls="todos-content">
|
|
||||||
<h3 class="h4" style="margin: 0;">▼ Todo</h3>
|
|
||||||
<span class="badge badge-pending">검증 필요</span>
|
|
||||||
</button>
|
|
||||||
<div id="todos-content" style="padding: 0 var(--space-3) var(--space-3); display: none;">
|
|
||||||
<div class="todo-card priority-high" style="margin-bottom: var(--space-2);">
|
|
||||||
<div class="todo-checkbox" onclick="toggleTodo(this)" role="checkbox" aria-checked="false" tabindex="0"></div>
|
|
||||||
<div class="todo-content">
|
|
||||||
<div class="todo-title">요구사항 정의</div>
|
|
||||||
<div class="todo-meta">
|
|
||||||
<span class="todo-assignee">@김민준</span>
|
|
||||||
<span class="todo-duedate urgent">(~ 10/25)</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- Conflict Resolution Alert (충돌 알림 예시 - 숨김 상태) -->
|
|
||||||
<div id="conflict-alert" style="display: none; background-color: var(--warning-50); border: var(--border-thin) solid var(--warning-500); border-radius: var(--radius-medium); padding: var(--space-4); margin-bottom: var(--space-4);" role="alert">
|
|
||||||
<div style="display: flex; align-items: center; gap: var(--space-2); margin-bottom: var(--space-3);">
|
|
||||||
<span style="font-size: 24px;">⚠️</span>
|
|
||||||
<h3 class="h4" style="margin: 0; color: var(--warning-700);">충돌 감지</h3>
|
|
||||||
</div>
|
|
||||||
<p class="text-body" style="margin-bottom: var(--space-3);">
|
|
||||||
박서연님이 동일한 부분을 수정하고 있습니다. 충돌을 해결해주세요.
|
|
||||||
</p>
|
|
||||||
<div style="display: flex; gap: var(--space-2);">
|
|
||||||
<button class="button-primary button-small" onclick="resolveConflict('accept')">
|
|
||||||
내 변경 사항 유지
|
|
||||||
</button>
|
|
||||||
<button class="button-secondary button-small" onclick="resolveConflict('merge')">
|
|
||||||
수동 병합
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<!-- Term Tooltip (맥락 기반 용어 설명 - 숨김 상태) -->
|
|
||||||
<div id="term-tooltip" class="tooltip" style="display: none;" role="tooltip">
|
|
||||||
<div id="tooltip-content">
|
|
||||||
<!-- JavaScript로 동적 생성 -->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- End Meeting Confirmation Modal -->
|
|
||||||
<div id="end-meeting-modal" class="modal-overlay" style="display: none;" role="dialog" aria-modal="true" aria-labelledby="end-meeting-title">
|
|
||||||
<div class="modal">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h2 id="end-meeting-title" class="modal-title">회의를 종료하시겠습니까?</h2>
|
|
||||||
<button class="modal-close" onclick="hideModal('end-meeting-modal')" aria-label="닫기">×</button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<p class="text-body">
|
|
||||||
녹음이 중지되고 검증 화면으로 이동합니다.
|
|
||||||
작성 중인 내용은 자동 저장됩니다.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button class="button-secondary" onclick="hideModal('end-meeting-modal')">
|
|
||||||
취소
|
|
||||||
</button>
|
|
||||||
<button class="button-primary" onclick="confirmEndMeeting()">
|
|
||||||
종료
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script src="common.js"></script>
|
|
||||||
<script>
|
|
||||||
// ============================================================================
|
|
||||||
// 타이머 업데이트
|
|
||||||
// ============================================================================
|
|
||||||
let seconds = 23 * 60 + 45; // 23분 45초
|
|
||||||
|
|
||||||
function updateTimer() {
|
|
||||||
const mins = Math.floor(seconds / 60);
|
|
||||||
const secs = seconds % 60;
|
|
||||||
const timerElement = $('#timer');
|
|
||||||
if (timerElement) {
|
|
||||||
timerElement.textContent = `${String(mins).padStart(2, '0')}:${String(secs).padStart(2, '0')}`;
|
|
||||||
}
|
|
||||||
seconds++;
|
|
||||||
}
|
|
||||||
|
|
||||||
setInterval(updateTimer, 1000);
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// 섹션 토글
|
|
||||||
// ============================================================================
|
|
||||||
function toggleSection(sectionId) {
|
|
||||||
const content = $(`#${sectionId}-content`);
|
|
||||||
const button = content.previousElementSibling;
|
|
||||||
|
|
||||||
if (content.style.display === 'none') {
|
|
||||||
content.style.display = 'block';
|
|
||||||
button.setAttribute('aria-expanded', 'true');
|
|
||||||
button.querySelector('h3').textContent = button.querySelector('h3').textContent.replace('▼', '▲');
|
|
||||||
} else {
|
|
||||||
content.style.display = 'none';
|
|
||||||
button.setAttribute('aria-expanded', 'false');
|
|
||||||
button.querySelector('h3').textContent = button.querySelector('h3').textContent.replace('▲', '▼');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// 맥락 기반 용어 설명 툴팁 (차별화 기능)
|
|
||||||
// ============================================================================
|
|
||||||
const termData = {
|
|
||||||
mvp: {
|
|
||||||
term: 'MVP',
|
|
||||||
fullName: 'Minimum Viable Product',
|
|
||||||
definition: '최소 기능 제품. 핵심 기능만 구현하여 시장 검증을 목적으로 출시하는 제품.',
|
|
||||||
contextMeaning: 'Q1까지 사용자 인증, 대시보드, 회의록 작성 핵심 기능만 구현하여 출시 예정',
|
|
||||||
relatedProjects: [
|
|
||||||
{ name: '2024 고객 포털 프로젝트', link: '#' },
|
|
||||||
{ name: '2023 모바일 앱 리뉴얼', link: '#' }
|
|
||||||
],
|
|
||||||
relatedMeetings: [
|
|
||||||
{ title: '2024-09-15 기획 회의', date: '2024-09-15', link: '#' },
|
|
||||||
{ title: '2024-08-20 킥오프 회의', date: '2024-08-20', link: '#' }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
q1: {
|
|
||||||
term: 'Q1',
|
|
||||||
fullName: '1분기 (First Quarter)',
|
|
||||||
definition: '회계 연도 또는 사업 연도의 첫 3개월 기간 (1월~3월).',
|
|
||||||
contextMeaning: '2025년 1월부터 3월까지의 기간을 의미하며, 이 기간 내 MVP 출시 목표',
|
|
||||||
relatedProjects: [
|
|
||||||
{ name: '2025 사업 계획', link: '#' }
|
|
||||||
],
|
|
||||||
relatedMeetings: [
|
|
||||||
{ title: '2024-12-10 연간 계획 회의', date: '2024-12-10', link: '#' }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
react: {
|
|
||||||
term: 'React',
|
|
||||||
fullName: 'React.js',
|
|
||||||
definition: 'Facebook(Meta)에서 개발한 JavaScript UI 라이브러리. 컴포넌트 기반 개발.',
|
|
||||||
contextMeaning: '프론트엔드 개발 프레임워크로 React를 사용하여 사용자 인터페이스를 구현',
|
|
||||||
relatedProjects: [
|
|
||||||
{ name: '2024 웹 포털 개선', link: '#' },
|
|
||||||
{ name: '2023 관리자 대시보드', link: '#' }
|
|
||||||
],
|
|
||||||
relatedMeetings: [
|
|
||||||
{ title: '2024-10-01 기술 스택 검토', date: '2024-10-01', link: '#' }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
aws: {
|
|
||||||
term: 'AWS',
|
|
||||||
fullName: 'Amazon Web Services',
|
|
||||||
definition: 'Amazon에서 제공하는 클라우드 컴퓨팅 플랫폼. EC2, S3, RDS 등 다양한 서비스 제공.',
|
|
||||||
contextMeaning: '서비스 배포 및 운영을 위한 클라우드 인프라로 AWS를 사용',
|
|
||||||
relatedProjects: [
|
|
||||||
{ name: '2024 인프라 마이그레이션', link: '#' }
|
|
||||||
],
|
|
||||||
relatedMeetings: [
|
|
||||||
{ title: '2024-09-25 인프라 설계 회의', date: '2024-09-25', link: '#' }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
sprint: {
|
|
||||||
term: 'Sprint',
|
|
||||||
fullName: 'Sprint (Scrum)',
|
|
||||||
definition: 'Scrum 방법론에서 정해진 기간(보통 1-4주) 동안 개발 작업을 진행하는 주기.',
|
|
||||||
contextMeaning: '2주 단위로 개발 작업을 진행하고 검토하는 주기',
|
|
||||||
relatedProjects: [
|
|
||||||
{ name: '2024 애자일 전환 프로젝트', link: '#' }
|
|
||||||
],
|
|
||||||
relatedMeetings: [
|
|
||||||
{ title: '2024-10-05 애자일 도입 회의', date: '2024-10-05', link: '#' }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
function showTermTooltip(event, termKey) {
|
|
||||||
event.stopPropagation();
|
|
||||||
|
|
||||||
const tooltip = $('#term-tooltip');
|
|
||||||
const target = event.target;
|
|
||||||
const term = termData[termKey];
|
|
||||||
|
|
||||||
if (!term || !tooltip) return;
|
|
||||||
|
|
||||||
// 툴팁 콘텐츠 생성
|
|
||||||
const content = `
|
|
||||||
<div class="tooltip-section">
|
|
||||||
<div style="margin-bottom: var(--space-2);">
|
|
||||||
<strong style="font-size: 1rem; color: var(--text-primary);">${term.term}</strong>
|
|
||||||
${term.fullName ? `<span class="text-caption" style="margin-left: var(--space-1);">(${term.fullName})</span>` : ''}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="tooltip-section">
|
|
||||||
<div class="tooltip-title">📘 정의</div>
|
|
||||||
<div class="tooltip-content">${term.definition}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="tooltip-section">
|
|
||||||
<div class="tooltip-title">🏢 이 회의에서의 의미</div>
|
|
||||||
<div class="tooltip-content">${term.contextMeaning}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
${term.relatedProjects.length > 0 ? `
|
|
||||||
<div class="tooltip-section">
|
|
||||||
<div class="tooltip-title">📂 관련 프로젝트</div>
|
|
||||||
<div class="tooltip-content">
|
|
||||||
${term.relatedProjects.map(p => `<div style="margin-bottom: var(--space-1);"><a href="${p.link}" style="color: var(--primary-500); text-decoration: underline;">${p.name}</a></div>`).join('')}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
` : ''}
|
|
||||||
|
|
||||||
${term.relatedMeetings.length > 0 ? `
|
|
||||||
<div class="tooltip-section">
|
|
||||||
<div class="tooltip-title">📄 과거 회의록</div>
|
|
||||||
<div class="tooltip-content">
|
|
||||||
${term.relatedMeetings.map(m => `<div style="margin-bottom: var(--space-1);"><a href="${m.link}" style="color: var(--primary-500); text-decoration: underline;">${m.title}</a> <span class="text-caption">(${m.date})</span></div>`).join('')}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
` : ''}
|
|
||||||
|
|
||||||
<div style="text-align: center; margin-top: var(--space-3);">
|
|
||||||
<button class="button-secondary button-small" onclick="hideTooltip()">자세히 보기</button>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
$('#tooltip-content').innerHTML = content;
|
|
||||||
|
|
||||||
// 툴팁 위치 계산
|
|
||||||
const rect = target.getBoundingClientRect();
|
|
||||||
tooltip.style.display = 'block';
|
|
||||||
tooltip.style.position = 'absolute';
|
|
||||||
tooltip.style.top = `${rect.bottom + window.scrollY + 10}px`;
|
|
||||||
tooltip.style.left = `${rect.left + window.scrollX}px`;
|
|
||||||
|
|
||||||
// 툴팁이 화면 밖으로 나가지 않도록 조정
|
|
||||||
setTimeout(() => {
|
|
||||||
const tooltipRect = tooltip.getBoundingClientRect();
|
|
||||||
if (tooltipRect.right > window.innerWidth) {
|
|
||||||
tooltip.style.left = `${window.innerWidth - tooltipRect.width - 20}px`;
|
|
||||||
}
|
|
||||||
if (tooltipRect.left < 0) {
|
|
||||||
tooltip.style.left = '20px';
|
|
||||||
}
|
|
||||||
}, 10);
|
|
||||||
|
|
||||||
// 외부 클릭 시 닫기
|
|
||||||
setTimeout(() => {
|
|
||||||
document.addEventListener('click', closeTooltipOutside);
|
|
||||||
}, 100);
|
|
||||||
}
|
|
||||||
|
|
||||||
function closeTooltipOutside(event) {
|
|
||||||
const tooltip = $('#term-tooltip');
|
|
||||||
if (tooltip && !tooltip.contains(event.target) && !event.target.classList.contains('term-highlight')) {
|
|
||||||
hideTooltip();
|
|
||||||
document.removeEventListener('click', closeTooltipOutside);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function hideTooltip() {
|
|
||||||
const tooltip = $('#term-tooltip');
|
|
||||||
if (tooltip) {
|
|
||||||
tooltip.style.display = 'none';
|
|
||||||
}
|
|
||||||
document.removeEventListener('click', closeTooltipOutside);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Todo 체크박스 토글
|
|
||||||
// ============================================================================
|
|
||||||
function toggleTodo(checkbox) {
|
|
||||||
toggleClass(checkbox, 'checked');
|
|
||||||
const isChecked = checkbox.classList.contains('checked');
|
|
||||||
checkbox.setAttribute('aria-checked', isChecked);
|
|
||||||
|
|
||||||
const todoTitle = checkbox.nextElementSibling.querySelector('.todo-title');
|
|
||||||
if (isChecked) {
|
|
||||||
addClass(todoTitle, 'completed');
|
|
||||||
} else {
|
|
||||||
removeClass(todoTitle, 'completed');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// 회의 종료
|
|
||||||
// ============================================================================
|
|
||||||
function endMeeting() {
|
|
||||||
showModal('end-meeting-modal');
|
|
||||||
}
|
|
||||||
|
|
||||||
function confirmEndMeeting() {
|
|
||||||
hideModal('end-meeting-modal');
|
|
||||||
showToast('회의가 종료되었습니다', 'success', 2000);
|
|
||||||
|
|
||||||
// 자동 저장 시뮬레이션
|
|
||||||
setTimeout(() => {
|
|
||||||
navigateTo('06-검증완료.html');
|
|
||||||
}, 2000);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// 섹션 편집
|
|
||||||
// ============================================================================
|
|
||||||
function editSection(sectionId) {
|
|
||||||
showToast('편집 모드로 전환되었습니다', 'info');
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// 댓글 추가
|
|
||||||
// ============================================================================
|
|
||||||
function addComment(sectionId) {
|
|
||||||
showToast('댓글 기능은 개발 중입니다', 'info');
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// 충돌 해결
|
|
||||||
// ============================================================================
|
|
||||||
function resolveConflict(action) {
|
|
||||||
const alert = $('#conflict-alert');
|
|
||||||
if (alert) {
|
|
||||||
alert.style.display = 'none';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (action === 'accept') {
|
|
||||||
showToast('내 변경 사항이 적용되었습니다', 'success');
|
|
||||||
} else {
|
|
||||||
showToast('수동 병합 모드로 전환되었습니다', 'info');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// 초기화
|
|
||||||
// ============================================================================
|
|
||||||
// 논의 내용 섹션 기본 열림
|
|
||||||
toggleSection('discussion');
|
|
||||||
|
|
||||||
// 자동 저장 시뮬레이션 (30초마다)
|
|
||||||
setInterval(() => {
|
|
||||||
console.log('자동 저장 완료');
|
|
||||||
}, 30000);
|
|
||||||
|
|
||||||
// 키보드 접근성: Enter/Space로 용어 설명 열기
|
|
||||||
$$('.term-highlight').forEach(term => {
|
|
||||||
term.addEventListener('keydown', (e) => {
|
|
||||||
if (e.key === 'Enter' || e.key === ' ') {
|
|
||||||
e.preventDefault();
|
|
||||||
term.click();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// 키보드 접근성: Esc로 툴팁 닫기
|
|
||||||
document.addEventListener('keydown', (e) => {
|
|
||||||
if (e.key === 'Escape') {
|
|
||||||
const tooltip = $('#term-tooltip');
|
|
||||||
if (tooltip && tooltip.style.display !== 'none') {
|
|
||||||
hideTooltip();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('회의진행 화면 초기화 완료');
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@ -1,517 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="ko">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<meta name="description" content="회의록 작성 및 공유 개선 서비스 - 검증완료">
|
|
||||||
<title>회의록 검증 - 회의록 작성 서비스</title>
|
|
||||||
<link rel="stylesheet" href="common.css">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<!-- Skip to Main Content (접근성) -->
|
|
||||||
<a href="#main-content" class="skip-to-main">본문으로 건너뛰기</a>
|
|
||||||
|
|
||||||
<!-- Header -->
|
|
||||||
<header style="position: sticky; top: 0; background: var(--bg-primary); border-bottom: var(--border-thin) solid var(--gray-200); z-index: 10; padding: var(--space-4);">
|
|
||||||
<div style="display: flex; align-items: center; justify-content: space-between; max-width: 1440px; margin: 0 auto;">
|
|
||||||
<button class="button-icon button-ghost" onclick="goBack()" aria-label="뒤로가기">
|
|
||||||
<span style="font-size: 24px;">←</span>
|
|
||||||
</button>
|
|
||||||
<h1 class="h4" style="margin: 0;">회의록 검증</h1>
|
|
||||||
<button class="button-primary button-small" onclick="proceedToEnd()" aria-label="다음 단계" id="next-button">
|
|
||||||
다음
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<!-- Main Content -->
|
|
||||||
<main id="main-content" class="container" style="padding-top: var(--space-4); padding-bottom: var(--space-6); max-width: 1024px;">
|
|
||||||
|
|
||||||
<!-- Progress Section -->
|
|
||||||
<section aria-labelledby="progress-section" style="margin-bottom: var(--space-6);">
|
|
||||||
<div style="margin-bottom: var(--space-3);">
|
|
||||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: var(--space-2);">
|
|
||||||
<h2 class="h4" id="progress-section">전체 진행률</h2>
|
|
||||||
<span class="h4" id="progress-text" style="color: var(--primary-500);">60% (3/5)</span>
|
|
||||||
</div>
|
|
||||||
<div class="progress-bar">
|
|
||||||
<div class="progress-fill" id="progress-fill" style="width: 60%;"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p class="text-body" style="color: var(--text-tertiary);">
|
|
||||||
회의록 섹션별로 검증해주세요. 모든 섹션이 검증되면 회의를 종료할 수 있습니다.
|
|
||||||
</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- Verification Sections -->
|
|
||||||
<section aria-labelledby="sections-title" style="margin-bottom: var(--space-6);">
|
|
||||||
<h2 class="h4" id="sections-title" style="margin-bottom: var(--space-4);">섹션별 검증</h2>
|
|
||||||
|
|
||||||
<!-- 참석자 섹션 (검증완료) -->
|
|
||||||
<div class="card" style="margin-bottom: var(--space-3);" data-section="attendees" data-verified="true">
|
|
||||||
<div class="card-header" style="border-bottom: var(--border-thin) solid var(--gray-200); padding-bottom: var(--space-3);">
|
|
||||||
<div style="display: flex; justify-content: between; align-items: center; margin-bottom: var(--space-2);">
|
|
||||||
<h3 class="h4" style="margin: 0; flex: 1;">✅ 참석자</h3>
|
|
||||||
<span class="badge badge-verified">검증완료</span>
|
|
||||||
</div>
|
|
||||||
<div style="display: flex; gap: var(--space-2); flex-wrap: wrap; align-items: center;">
|
|
||||||
<span class="text-caption" style="color: var(--text-tertiary);">검증자: 김민준</span>
|
|
||||||
<span class="text-caption" style="color: var(--text-tertiary);">•</span>
|
|
||||||
<span class="text-caption" style="color: var(--text-tertiary);">시간: 14:35</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<p class="text-body" style="margin: var(--space-2) 0;">- 김민준 (주관자)</p>
|
|
||||||
<p class="text-body" style="margin: var(--space-2) 0;">- 박서연</p>
|
|
||||||
<p class="text-body" style="margin: var(--space-2) 0;">- 이준호</p>
|
|
||||||
</div>
|
|
||||||
<div class="card-footer">
|
|
||||||
<button class="button-secondary button-small" onclick="editSection('attendees')">
|
|
||||||
수정
|
|
||||||
</button>
|
|
||||||
<button class="button-ghost button-small" onclick="lockSection('attendees')" aria-label="섹션 잠금" title="회의 생성자만 사용 가능">
|
|
||||||
🔒 잠금
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 안건 섹션 (검증 필요) -->
|
|
||||||
<div class="card" style="margin-bottom: var(--space-3);" data-section="agenda" data-verified="false">
|
|
||||||
<div class="card-header" style="border-bottom: var(--border-thin) solid var(--gray-200); padding-bottom: var(--space-3);">
|
|
||||||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
|
||||||
<h3 class="h4" style="margin: 0;">⚠️ 안건</h3>
|
|
||||||
<span class="badge badge-pending">검증 필요</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<p class="text-body" style="margin: var(--space-2) 0;">- 프로젝트 목표 정의</p>
|
|
||||||
<p class="text-body" style="margin: var(--space-2) 0;">- 일정 및 마일스톤</p>
|
|
||||||
</div>
|
|
||||||
<div class="card-footer">
|
|
||||||
<button class="button-secondary button-small" onclick="editSection('agenda')">
|
|
||||||
수정
|
|
||||||
</button>
|
|
||||||
<button class="button-primary button-small" onclick="verifySection('agenda')">
|
|
||||||
✓ 검증완료
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 논의 내용 섹션 (검증 필요) -->
|
|
||||||
<div class="card" style="margin-bottom: var(--space-3);" data-section="discussion" data-verified="false">
|
|
||||||
<div class="card-header" style="border-bottom: var(--border-thin) solid var(--gray-200); padding-bottom: var(--space-3);">
|
|
||||||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
|
||||||
<h3 class="h4" style="margin: 0;">⚠️ 논의 내용</h3>
|
|
||||||
<span class="badge badge-pending">검증 필요</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<p class="text-body">
|
|
||||||
우리는 Q1까지 MVP를 완성해야 합니다. 개발 프레임워크는 React를 사용하고, 배포 환경은 AWS로 결정했습니다.
|
|
||||||
Sprint 주기는 2주로 설정합니다.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="card-footer">
|
|
||||||
<button class="button-secondary button-small" onclick="editSection('discussion')">
|
|
||||||
수정
|
|
||||||
</button>
|
|
||||||
<button class="button-primary button-small" onclick="verifySection('discussion')">
|
|
||||||
✓ 검증완료
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 결정 사항 섹션 (검증완료) -->
|
|
||||||
<div class="card" style="margin-bottom: var(--space-3);" data-section="decisions" data-verified="true">
|
|
||||||
<div class="card-header" style="border-bottom: var(--border-thin) solid var(--gray-200); padding-bottom: var(--space-3);">
|
|
||||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: var(--space-2);">
|
|
||||||
<h3 class="h4" style="margin: 0; flex: 1;">✅ 결정 사항</h3>
|
|
||||||
<span class="badge badge-verified">검증완료</span>
|
|
||||||
</div>
|
|
||||||
<div style="display: flex; gap: var(--space-2); flex-wrap: wrap; align-items: center;">
|
|
||||||
<span class="text-caption" style="color: var(--text-tertiary);">검증자: 박서연</span>
|
|
||||||
<span class="text-caption" style="color: var(--text-tertiary);">•</span>
|
|
||||||
<span class="text-caption" style="color: var(--text-tertiary);">시간: 14:40</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<p class="text-body" style="margin: var(--space-2) 0;">- 개발 프레임워크: React</p>
|
|
||||||
<p class="text-body" style="margin: var(--space-2) 0;">- 배포 환경: AWS</p>
|
|
||||||
<p class="text-body" style="margin: var(--space-2) 0;">- Sprint 주기: 2주</p>
|
|
||||||
</div>
|
|
||||||
<div class="card-footer">
|
|
||||||
<button class="button-secondary button-small" onclick="editSection('decisions')">
|
|
||||||
수정
|
|
||||||
</button>
|
|
||||||
<button class="button-ghost button-small" onclick="lockSection('decisions')" aria-label="섹션 잠금">
|
|
||||||
🔒 잠금
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Todo 섹션 (검증완료) -->
|
|
||||||
<div class="card" data-section="todos" data-verified="true">
|
|
||||||
<div class="card-header" style="border-bottom: var(--border-thin) solid var(--gray-200); padding-bottom: var(--space-3);">
|
|
||||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: var(--space-2);">
|
|
||||||
<h3 class="h4" style="margin: 0; flex: 1;">✅ Todo</h3>
|
|
||||||
<span class="badge badge-verified">검증완료</span>
|
|
||||||
</div>
|
|
||||||
<div style="display: flex; gap: var(--space-2); flex-wrap: wrap; align-items: center;">
|
|
||||||
<span class="text-caption" style="color: var(--text-tertiary);">검증자: 이준호</span>
|
|
||||||
<span class="text-caption" style="color: var(--text-tertiary);">•</span>
|
|
||||||
<span class="text-caption" style="color: var(--text-tertiary);">시간: 14:42</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="todo-card priority-high" style="margin-bottom: var(--space-2);">
|
|
||||||
<div class="todo-checkbox" role="checkbox" aria-checked="false" tabindex="0" style="pointer-events: none;"></div>
|
|
||||||
<div class="todo-content">
|
|
||||||
<div class="todo-title">요구사항 정의</div>
|
|
||||||
<div class="todo-meta">
|
|
||||||
<span class="todo-assignee">@김민준</span>
|
|
||||||
<span class="todo-duedate">(~ 10/25)</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="todo-card priority-medium">
|
|
||||||
<div class="todo-checkbox" role="checkbox" aria-checked="false" tabindex="0" style="pointer-events: none;"></div>
|
|
||||||
<div class="todo-content">
|
|
||||||
<div class="todo-title">기술 스택 검토</div>
|
|
||||||
<div class="todo-meta">
|
|
||||||
<span class="todo-assignee">@박서연</span>
|
|
||||||
<span class="todo-duedate">(~ 10/27)</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="card-footer">
|
|
||||||
<button class="button-secondary button-small" onclick="editSection('todos')">
|
|
||||||
수정
|
|
||||||
</button>
|
|
||||||
<button class="button-ghost button-small" onclick="lockSection('todos')" aria-label="섹션 잠금">
|
|
||||||
🔒 잠금
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- Info Card -->
|
|
||||||
<div class="card" style="background-color: var(--info-50); border-color: var(--info-200);">
|
|
||||||
<div style="display: flex; align-items: center; gap: var(--space-2); margin-bottom: var(--space-2);">
|
|
||||||
<span style="font-size: 20px;">💡</span>
|
|
||||||
<span class="text-body" style="font-weight: 600; color: var(--info-700);">안내</span>
|
|
||||||
</div>
|
|
||||||
<p class="text-body" style="color: var(--info-700);">
|
|
||||||
검증 미완료 섹션이 있어도 다음 단계로 진행할 수 있습니다. 나중에 수정하고 다시 확정할 수 있습니다.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<!-- Edit Section Modal -->
|
|
||||||
<div id="edit-modal" class="modal-overlay" style="display: none;" role="dialog" aria-modal="true" aria-labelledby="edit-modal-title">
|
|
||||||
<div class="modal">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h2 id="edit-modal-title" class="modal-title">섹션 수정</h2>
|
|
||||||
<button class="modal-close" onclick="hideModal('edit-modal')" aria-label="닫기">×</button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<div class="input-group">
|
|
||||||
<label for="edit-textarea" class="input-label">내용</label>
|
|
||||||
<textarea id="edit-textarea" class="input-field" rows="6" placeholder="내용을 입력하세요"></textarea>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button class="button-secondary" onclick="hideModal('edit-modal')">
|
|
||||||
취소
|
|
||||||
</button>
|
|
||||||
<button class="button-primary" onclick="saveEdit()">
|
|
||||||
저장
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Lock Section Confirmation Modal -->
|
|
||||||
<div id="lock-modal" class="modal-overlay" style="display: none;" role="dialog" aria-modal="true" aria-labelledby="lock-modal-title">
|
|
||||||
<div class="modal">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h2 id="lock-modal-title" class="modal-title">섹션 잠금</h2>
|
|
||||||
<button class="modal-close" onclick="hideModal('lock-modal')" aria-label="닫기">×</button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<p class="text-body" style="margin-bottom: var(--space-3);">
|
|
||||||
이 섹션을 잠그시겠습니까?<br>
|
|
||||||
잠금 후에는 추가 수정이 불가능합니다.
|
|
||||||
</p>
|
|
||||||
<div class="card" style="background-color: var(--warning-50); border-color: var(--warning-200);">
|
|
||||||
<div style="display: flex; align-items: center; gap: var(--space-2); margin-bottom: var(--space-2);">
|
|
||||||
<span>⚠️</span>
|
|
||||||
<span class="text-caption" style="color: var(--warning-700); font-weight: 600;">주의</span>
|
|
||||||
</div>
|
|
||||||
<p class="text-caption" style="color: var(--warning-700);">
|
|
||||||
회의 생성자만 섹션을 잠글 수 있습니다. 잠금 후에는 회의 생성자만 잠금을 해제할 수 있습니다.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button class="button-secondary" onclick="hideModal('lock-modal')">
|
|
||||||
취소
|
|
||||||
</button>
|
|
||||||
<button class="button-primary" onclick="confirmLock()">
|
|
||||||
잠금
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script src="common.js"></script>
|
|
||||||
<script>
|
|
||||||
// ============================================================================
|
|
||||||
// 상태 변수
|
|
||||||
// ============================================================================
|
|
||||||
let currentEditSection = null;
|
|
||||||
let currentLockSection = null;
|
|
||||||
const currentUser = getCurrentUser(); // common.js에서 가져옴
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// 진행률 업데이트
|
|
||||||
// ============================================================================
|
|
||||||
function updateProgress() {
|
|
||||||
const sections = $$('[data-section]');
|
|
||||||
const totalSections = sections.length;
|
|
||||||
let verifiedCount = 0;
|
|
||||||
|
|
||||||
sections.forEach(section => {
|
|
||||||
if (section.dataset.verified === 'true') {
|
|
||||||
verifiedCount++;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const percentage = Math.round((verifiedCount / totalSections) * 100);
|
|
||||||
|
|
||||||
// 진행률 바 업데이트
|
|
||||||
const progressFill = $('#progress-fill');
|
|
||||||
const progressText = $('#progress-text');
|
|
||||||
|
|
||||||
if (progressFill) {
|
|
||||||
progressFill.style.width = `${percentage}%`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (progressText) {
|
|
||||||
progressText.textContent = `${percentage}% (${verifiedCount}/${totalSections})`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 진행률에 따른 색상 변경
|
|
||||||
if (progressFill) {
|
|
||||||
if (percentage === 100) {
|
|
||||||
removeClass(progressFill, 'warning');
|
|
||||||
addClass(progressFill, 'success');
|
|
||||||
} else if (percentage >= 50) {
|
|
||||||
removeClass(progressFill, 'error');
|
|
||||||
addClass(progressFill, 'warning');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// 섹션 검증
|
|
||||||
// ============================================================================
|
|
||||||
function verifySection(sectionId) {
|
|
||||||
const section = $(`[data-section="${sectionId}"]`);
|
|
||||||
if (!section) return;
|
|
||||||
|
|
||||||
// 검증 상태 업데이트
|
|
||||||
section.dataset.verified = 'true';
|
|
||||||
|
|
||||||
// UI 업데이트
|
|
||||||
const header = section.querySelector('.card-header');
|
|
||||||
const badge = header.querySelector('.badge');
|
|
||||||
const h3 = header.querySelector('h3');
|
|
||||||
|
|
||||||
// 아이콘 변경
|
|
||||||
h3.innerHTML = h3.innerHTML.replace('⚠️', '✅');
|
|
||||||
|
|
||||||
// 배지 변경
|
|
||||||
badge.textContent = '검증완료';
|
|
||||||
removeClass(badge, 'badge-pending');
|
|
||||||
addClass(badge, 'badge-verified');
|
|
||||||
|
|
||||||
// 검증자 정보 추가
|
|
||||||
const verifiedInfo = document.createElement('div');
|
|
||||||
verifiedInfo.style.cssText = 'display: flex; gap: var(--space-2); flex-wrap: wrap; align-items: center;';
|
|
||||||
verifiedInfo.innerHTML = `
|
|
||||||
<span class="text-caption" style="color: var(--text-tertiary);">검증자: ${currentUser.name}</span>
|
|
||||||
<span class="text-caption" style="color: var(--text-tertiary);">•</span>
|
|
||||||
<span class="text-caption" style="color: var(--text-tertiary);">시간: ${formatTime(new Date())}</span>
|
|
||||||
`;
|
|
||||||
header.appendChild(verifiedInfo);
|
|
||||||
|
|
||||||
// 버튼 변경
|
|
||||||
const footer = section.querySelector('.card-footer');
|
|
||||||
footer.innerHTML = `
|
|
||||||
<button class="button-secondary button-small" onclick="editSection('${sectionId}')">
|
|
||||||
수정
|
|
||||||
</button>
|
|
||||||
<button class="button-ghost button-small" onclick="lockSection('${sectionId}')" aria-label="섹션 잠금">
|
|
||||||
🔒 잠금
|
|
||||||
</button>
|
|
||||||
`;
|
|
||||||
|
|
||||||
// 진행률 업데이트
|
|
||||||
updateProgress();
|
|
||||||
|
|
||||||
// 성공 메시지
|
|
||||||
showToast('섹션이 검증되었습니다', 'success');
|
|
||||||
|
|
||||||
// 실시간 동기화 시뮬레이션
|
|
||||||
setTimeout(() => {
|
|
||||||
showToast('다른 참석자에게 알림이 전송되었습니다', 'info', 2000);
|
|
||||||
}, 1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// 섹션 수정
|
|
||||||
// ============================================================================
|
|
||||||
function editSection(sectionId) {
|
|
||||||
currentEditSection = sectionId;
|
|
||||||
const section = $(`[data-section="${sectionId}"]`);
|
|
||||||
|
|
||||||
if (!section) return;
|
|
||||||
|
|
||||||
// 현재 내용 가져오기
|
|
||||||
const cardBody = section.querySelector('.card-body');
|
|
||||||
const currentContent = cardBody.textContent.trim();
|
|
||||||
|
|
||||||
// 모달에 내용 설정
|
|
||||||
$('#edit-textarea').value = currentContent;
|
|
||||||
$('#edit-modal-title').textContent = `${section.querySelector('h3').textContent.replace('✅ ', '').replace('⚠️ ', '')} 수정`;
|
|
||||||
|
|
||||||
showModal('edit-modal');
|
|
||||||
}
|
|
||||||
|
|
||||||
function saveEdit() {
|
|
||||||
if (!currentEditSection) return;
|
|
||||||
|
|
||||||
const newContent = $('#edit-textarea').value.trim();
|
|
||||||
|
|
||||||
if (!newContent) {
|
|
||||||
showToast('내용을 입력해주세요', 'error');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const section = $(`[data-section="${currentEditSection}"]`);
|
|
||||||
if (!section) return;
|
|
||||||
|
|
||||||
// 내용 업데이트
|
|
||||||
const cardBody = section.querySelector('.card-body');
|
|
||||||
cardBody.innerHTML = `<p class="text-body">${newContent}</p>`;
|
|
||||||
|
|
||||||
// 검증 상태를 "검증 필요"로 변경
|
|
||||||
section.dataset.verified = 'false';
|
|
||||||
|
|
||||||
const header = section.querySelector('.card-header');
|
|
||||||
const badge = header.querySelector('.badge');
|
|
||||||
const h3 = header.querySelector('h3');
|
|
||||||
|
|
||||||
// 아이콘 변경
|
|
||||||
h3.innerHTML = h3.innerHTML.replace('✅', '⚠️');
|
|
||||||
|
|
||||||
// 배지 변경
|
|
||||||
badge.textContent = '검증 필요';
|
|
||||||
removeClass(badge, 'badge-verified');
|
|
||||||
addClass(badge, 'badge-pending');
|
|
||||||
|
|
||||||
// 검증자 정보 제거
|
|
||||||
const verifiedInfo = header.querySelectorAll('.text-caption');
|
|
||||||
verifiedInfo.forEach(info => {
|
|
||||||
if (info.textContent.includes('검증자')) {
|
|
||||||
info.parentElement.remove();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 버튼 변경
|
|
||||||
const footer = section.querySelector('.card-footer');
|
|
||||||
footer.innerHTML = `
|
|
||||||
<button class="button-secondary button-small" onclick="editSection('${currentEditSection}')">
|
|
||||||
수정
|
|
||||||
</button>
|
|
||||||
<button class="button-primary button-small" onclick="verifySection('${currentEditSection}')">
|
|
||||||
✓ 검증완료
|
|
||||||
</button>
|
|
||||||
`;
|
|
||||||
|
|
||||||
// 진행률 업데이트
|
|
||||||
updateProgress();
|
|
||||||
|
|
||||||
// 모달 닫기
|
|
||||||
hideModal('edit-modal');
|
|
||||||
|
|
||||||
// 성공 메시지
|
|
||||||
showToast('섹션이 수정되었습니다. 검증이 필요합니다.', 'info');
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// 섹션 잠금
|
|
||||||
// ============================================================================
|
|
||||||
function lockSection(sectionId) {
|
|
||||||
// 회의 생성자 권한 체크 (예제에서는 김민준만 가능)
|
|
||||||
if (!currentUser || currentUser.id !== 1) {
|
|
||||||
showToast('회의 생성자만 섹션을 잠글 수 있습니다', 'error');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
currentLockSection = sectionId;
|
|
||||||
showModal('lock-modal');
|
|
||||||
}
|
|
||||||
|
|
||||||
function confirmLock() {
|
|
||||||
if (!currentLockSection) return;
|
|
||||||
|
|
||||||
const section = $(`[data-section="${currentLockSection}"]`);
|
|
||||||
if (!section) return;
|
|
||||||
|
|
||||||
// 잠금 표시
|
|
||||||
const footer = section.querySelector('.card-footer');
|
|
||||||
footer.innerHTML = `
|
|
||||||
<button class="button-secondary button-small" disabled style="opacity: 0.5; cursor: not-allowed;">
|
|
||||||
🔒 잠금됨
|
|
||||||
</button>
|
|
||||||
`;
|
|
||||||
|
|
||||||
// 모달 닫기
|
|
||||||
hideModal('lock-modal');
|
|
||||||
|
|
||||||
// 성공 메시지
|
|
||||||
showToast('섹션이 잠금되었습니다', 'success');
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// 다음 단계
|
|
||||||
// ============================================================================
|
|
||||||
function proceedToEnd() {
|
|
||||||
// 모든 섹션이 검증되었는지 확인
|
|
||||||
const sections = $$('[data-section]');
|
|
||||||
const allVerified = Array.from(sections).every(section => section.dataset.verified === 'true');
|
|
||||||
|
|
||||||
if (allVerified) {
|
|
||||||
showToast('모든 섹션이 검증되었습니다', 'success', 2000);
|
|
||||||
} else {
|
|
||||||
showToast('검증되지 않은 섹션이 있습니다. 나중에 수정할 수 있습니다.', 'info', 3000);
|
|
||||||
}
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
navigateTo('07-회의종료.html');
|
|
||||||
}, 2000);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// 초기화
|
|
||||||
// ============================================================================
|
|
||||||
updateProgress();
|
|
||||||
|
|
||||||
console.log('검증완료 화면 초기화 완료');
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@ -1,472 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="ko">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<meta name="description" content="회의록 작성 및 공유 개선 서비스 - 회의종료">
|
|
||||||
<title>회의 종료 - 회의록 작성 서비스</title>
|
|
||||||
<link rel="stylesheet" href="common.css">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<!-- Skip to Main Content (접근성) -->
|
|
||||||
<a href="#main-content" class="skip-to-main">본문으로 건너뛰기</a>
|
|
||||||
|
|
||||||
<!-- Header -->
|
|
||||||
<header style="position: sticky; top: 0; background: var(--bg-primary); border-bottom: var(--border-thin) solid var(--gray-200); z-index: 10; padding: var(--space-4);">
|
|
||||||
<div style="display: flex; align-items: center; justify-content: space-between; max-width: 1440px; margin: 0 auto;">
|
|
||||||
<button class="button-icon button-ghost" onclick="goBack()" aria-label="뒤로가기">
|
|
||||||
<span style="font-size: 24px;">←</span>
|
|
||||||
</button>
|
|
||||||
<h1 class="h4" style="margin: 0;">회의 종료</h1>
|
|
||||||
<button class="button-primary button-small" onclick="confirmMeeting()" aria-label="최종 확정">
|
|
||||||
확정
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<!-- Main Content -->
|
|
||||||
<main id="main-content" class="container" style="padding-top: var(--space-4); padding-bottom: var(--space-6); max-width: 1024px;">
|
|
||||||
|
|
||||||
<!-- Completion Message -->
|
|
||||||
<section aria-labelledby="completion-section" style="text-align: center; margin-bottom: var(--space-6); padding: var(--space-6) 0;">
|
|
||||||
<div style="font-size: 64px; margin-bottom: var(--space-3);">🎉</div>
|
|
||||||
<h2 class="h2" id="completion-section" style="margin-bottom: var(--space-2);">회의가 종료되었습니다</h2>
|
|
||||||
<p class="text-body" style="color: var(--text-tertiary);">
|
|
||||||
회의록을 확인하고 최종 확정해주세요
|
|
||||||
</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- Meeting Statistics Card -->
|
|
||||||
<section aria-labelledby="stats-section" style="margin-bottom: var(--space-6);">
|
|
||||||
<h2 class="h4" id="stats-section" style="margin-bottom: var(--space-4);">📊 회의 통계</h2>
|
|
||||||
<div class="card">
|
|
||||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: var(--space-4);">
|
|
||||||
<!-- 총 시간 -->
|
|
||||||
<div style="padding: var(--space-3); background-color: var(--bg-secondary); border-radius: var(--radius-medium);">
|
|
||||||
<div style="display: flex; align-items: center; gap: var(--space-2); margin-bottom: var(--space-2);">
|
|
||||||
<span style="font-size: 24px;">⏱️</span>
|
|
||||||
<span class="text-caption" style="color: var(--text-tertiary); font-weight: 600;">총 시간</span>
|
|
||||||
</div>
|
|
||||||
<div class="h3" style="color: var(--text-primary);">45분</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 참석자 -->
|
|
||||||
<div style="padding: var(--space-3); background-color: var(--bg-secondary); border-radius: var(--radius-medium);">
|
|
||||||
<div style="display: flex; align-items: center; gap: var(--space-2); margin-bottom: var(--space-2);">
|
|
||||||
<span style="font-size: 24px;">👥</span>
|
|
||||||
<span class="text-caption" style="color: var(--text-tertiary); font-weight: 600;">참석자</span>
|
|
||||||
</div>
|
|
||||||
<div class="h3" style="color: var(--text-primary);">3명</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 발언 횟수 -->
|
|
||||||
<div style="margin-top: var(--space-4); padding: var(--space-3); background-color: var(--bg-secondary); border-radius: var(--radius-medium);">
|
|
||||||
<div style="display: flex; align-items: center; gap: var(--space-2); margin-bottom: var(--space-3);">
|
|
||||||
<span style="font-size: 24px;">💬</span>
|
|
||||||
<span class="text-caption" style="color: var(--text-tertiary); font-weight: 600;">발언 횟수</span>
|
|
||||||
</div>
|
|
||||||
<div style="display: flex; flex-direction: column; gap: var(--space-2);">
|
|
||||||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
|
||||||
<span class="text-body">김민준</span>
|
|
||||||
<div style="display: flex; align-items: center; gap: var(--space-2);">
|
|
||||||
<div class="progress-bar" style="width: 120px; height: 8px;">
|
|
||||||
<div class="progress-fill" style="width: 60%; background-color: var(--primary-500);"></div>
|
|
||||||
</div>
|
|
||||||
<span class="text-body" style="font-weight: 600;">12회</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
|
||||||
<span class="text-body">박서연</span>
|
|
||||||
<div style="display: flex; align-items: center; gap: var(--space-2);">
|
|
||||||
<div class="progress-bar" style="width: 120px; height: 8px;">
|
|
||||||
<div class="progress-fill" style="width: 40%; background-color: var(--info-500);"></div>
|
|
||||||
</div>
|
|
||||||
<span class="text-body" style="font-weight: 600;">8회</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
|
||||||
<span class="text-body">이준호</span>
|
|
||||||
<div style="display: flex; align-items: center; gap: var(--space-2);">
|
|
||||||
<div class="progress-bar" style="width: 120px; height: 8px;">
|
|
||||||
<div class="progress-fill" style="width: 25%; background-color: var(--success-500);"></div>
|
|
||||||
</div>
|
|
||||||
<span class="text-body" style="font-weight: 600;">5회</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 주요 키워드 -->
|
|
||||||
<div style="margin-top: var(--space-4); padding: var(--space-3); background-color: var(--bg-secondary); border-radius: var(--radius-medium);">
|
|
||||||
<div style="display: flex; align-items: center; gap: var(--space-2); margin-bottom: var(--space-3);">
|
|
||||||
<span style="font-size: 24px;">🔑</span>
|
|
||||||
<span class="text-caption" style="color: var(--text-tertiary); font-weight: 600;">주요 키워드</span>
|
|
||||||
</div>
|
|
||||||
<div style="display: flex; gap: var(--space-2); flex-wrap: wrap;">
|
|
||||||
<span class="badge badge-in-progress" style="cursor: pointer;" onclick="showKeywordContext('MVP')">#MVP</span>
|
|
||||||
<span class="badge badge-in-progress" style="cursor: pointer;" onclick="showKeywordContext('React')">#React</span>
|
|
||||||
<span class="badge badge-in-progress" style="cursor: pointer;" onclick="showKeywordContext('AWS')">#AWS</span>
|
|
||||||
<span class="badge badge-in-progress" style="cursor: pointer;" onclick="showKeywordContext('Sprint')">#Sprint</span>
|
|
||||||
<span class="badge badge-in-progress" style="cursor: pointer;" onclick="showKeywordContext('Q1')">#Q1</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- AI Todo Auto Extraction -->
|
|
||||||
<section aria-labelledby="todos-section" style="margin-bottom: var(--space-6);">
|
|
||||||
<h2 class="h4" id="todos-section" style="margin-bottom: var(--space-4);">✅ AI Todo 자동 추출</h2>
|
|
||||||
<div class="card" style="background-color: var(--primary-50); border-color: var(--primary-200);">
|
|
||||||
<div style="display: flex; align-items: center; gap: var(--space-2); margin-bottom: var(--space-3);">
|
|
||||||
<span style="font-size: 24px;">💡</span>
|
|
||||||
<span class="text-body" style="font-weight: 600; color: var(--primary-700);">AI가 회의록에서 3개의 Todo를 자동으로 추출했습니다</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Todo 1 -->
|
|
||||||
<div class="todo-card priority-high" style="margin-bottom: var(--space-2); background-color: var(--bg-primary);">
|
|
||||||
<div class="todo-checkbox" onclick="toggleTodo(this, 1)" role="checkbox" aria-checked="false" tabindex="0"></div>
|
|
||||||
<div class="todo-content">
|
|
||||||
<div class="todo-title">요구사항 정의서 작성</div>
|
|
||||||
<div class="todo-meta">
|
|
||||||
<span class="todo-assignee">@김민준</span>
|
|
||||||
<span class="todo-duedate">📅 ~ 10/25</span>
|
|
||||||
<button class="button-ghost button-small" onclick="editTodo(1)" style="margin-left: var(--space-2); padding: var(--space-1) var(--space-2);">
|
|
||||||
✏️ 수정
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Todo 2 -->
|
|
||||||
<div class="todo-card priority-medium" style="margin-bottom: var(--space-2); background-color: var(--bg-primary);">
|
|
||||||
<div class="todo-checkbox" onclick="toggleTodo(this, 2)" role="checkbox" aria-checked="false" tabindex="0"></div>
|
|
||||||
<div class="todo-content">
|
|
||||||
<div class="todo-title">기술 스택 상세 검토</div>
|
|
||||||
<div class="todo-meta">
|
|
||||||
<span class="todo-assignee">@박서연</span>
|
|
||||||
<span class="todo-duedate">📅 ~ 10/27</span>
|
|
||||||
<button class="button-ghost button-small" onclick="editTodo(2)" style="margin-left: var(--space-2); padding: var(--space-1) var(--space-2);">
|
|
||||||
✏️ 수정
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Todo 3 -->
|
|
||||||
<div class="todo-card priority-high" style="background-color: var(--bg-primary);">
|
|
||||||
<div class="todo-checkbox" onclick="toggleTodo(this, 3)" role="checkbox" aria-checked="false" tabindex="0"></div>
|
|
||||||
<div class="todo-content">
|
|
||||||
<div class="todo-title">인프라 설계 문서 작성</div>
|
|
||||||
<div class="todo-meta">
|
|
||||||
<span class="todo-assignee">@이준호</span>
|
|
||||||
<span class="todo-duedate">📅 ~ 10/30</span>
|
|
||||||
<button class="button-ghost button-small" onclick="editTodo(3)" style="margin-left: var(--space-2); padding: var(--space-1) var(--space-2);">
|
|
||||||
✏️ 수정
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="margin-top: var(--space-4); text-align: center;">
|
|
||||||
<button class="button-secondary button-small" onclick="addNewTodo()">
|
|
||||||
➕ Todo 추가
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- Required Items Checklist -->
|
|
||||||
<section aria-labelledby="checklist-section" style="margin-bottom: var(--space-6);">
|
|
||||||
<h2 class="h4" id="checklist-section" style="margin-bottom: var(--space-4);">필수 항목 확인</h2>
|
|
||||||
<div class="card">
|
|
||||||
<div style="display: flex; flex-direction: column; gap: var(--space-3);">
|
|
||||||
<div style="display: flex; align-items: center; gap: var(--space-3);">
|
|
||||||
<span style="font-size: 24px; color: var(--success-500);">✅</span>
|
|
||||||
<span class="text-body">회의 제목</span>
|
|
||||||
</div>
|
|
||||||
<div style="display: flex; align-items: center; gap: var(--space-3);">
|
|
||||||
<span style="font-size: 24px; color: var(--success-500);">✅</span>
|
|
||||||
<span class="text-body">참석자 목록</span>
|
|
||||||
</div>
|
|
||||||
<div style="display: flex; align-items: center; gap: var(--space-3);">
|
|
||||||
<span style="font-size: 24px; color: var(--success-500);">✅</span>
|
|
||||||
<span class="text-body">주요 논의 내용</span>
|
|
||||||
</div>
|
|
||||||
<div style="display: flex; align-items: center; gap: var(--space-3);">
|
|
||||||
<span style="font-size: 24px; color: var(--success-500);">✅</span>
|
|
||||||
<span class="text-body">결정 사항</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- Action Buttons -->
|
|
||||||
<section style="display: flex; flex-direction: column; gap: var(--space-3);">
|
|
||||||
<button class="button-primary w-full" style="height: 48px; font-size: 1rem;" onclick="confirmMeeting()">
|
|
||||||
최종 회의록 확정
|
|
||||||
</button>
|
|
||||||
<button class="button-secondary w-full" onclick="saveLater()">
|
|
||||||
나중에 확정
|
|
||||||
</button>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<!-- Edit Todo Modal -->
|
|
||||||
<div id="edit-todo-modal" class="modal-overlay" style="display: none;" role="dialog" aria-modal="true" aria-labelledby="edit-todo-title">
|
|
||||||
<div class="modal">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h2 id="edit-todo-title" class="modal-title">Todo 수정</h2>
|
|
||||||
<button class="modal-close" onclick="hideModal('edit-todo-modal')" aria-label="닫기">×</button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<div class="input-group" style="margin-bottom: var(--space-3);">
|
|
||||||
<label for="todo-content" class="input-label required">내용</label>
|
|
||||||
<input type="text" id="todo-content" class="input-field" placeholder="Todo 내용을 입력하세요" required>
|
|
||||||
</div>
|
|
||||||
<div class="input-group" style="margin-bottom: var(--space-3);">
|
|
||||||
<label for="todo-assignee" class="input-label required">담당자</label>
|
|
||||||
<select id="todo-assignee" class="input-field" required>
|
|
||||||
<option value="">선택하세요</option>
|
|
||||||
<option value="김민준">김민준</option>
|
|
||||||
<option value="박서연">박서연</option>
|
|
||||||
<option value="이준호">이준호</option>
|
|
||||||
<option value="최유진">최유진</option>
|
|
||||||
<option value="정도현">정도현</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="input-group" style="margin-bottom: var(--space-3);">
|
|
||||||
<label for="todo-duedate" class="input-label required">마감일</label>
|
|
||||||
<input type="date" id="todo-duedate" class="input-field" required>
|
|
||||||
</div>
|
|
||||||
<div class="input-group">
|
|
||||||
<label for="todo-priority" class="input-label required">우선순위</label>
|
|
||||||
<select id="todo-priority" class="input-field" required>
|
|
||||||
<option value="">선택하세요</option>
|
|
||||||
<option value="high">높음</option>
|
|
||||||
<option value="medium">보통</option>
|
|
||||||
<option value="low">낮음</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button class="button-secondary" onclick="hideModal('edit-todo-modal')">
|
|
||||||
취소
|
|
||||||
</button>
|
|
||||||
<button class="button-primary" onclick="saveTodo()">
|
|
||||||
저장
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Keyword Context Modal -->
|
|
||||||
<div id="keyword-modal" class="modal-overlay" style="display: none;" role="dialog" aria-modal="true" aria-labelledby="keyword-title">
|
|
||||||
<div class="modal">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h2 id="keyword-title" class="modal-title">키워드 맥락</h2>
|
|
||||||
<button class="modal-close" onclick="hideModal('keyword-modal')" aria-label="닫기">×</button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body" id="keyword-content">
|
|
||||||
<!-- JavaScript로 동적 생성 -->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script src="common.js"></script>
|
|
||||||
<script>
|
|
||||||
// ============================================================================
|
|
||||||
// 상태 변수
|
|
||||||
// ============================================================================
|
|
||||||
let currentEditTodoId = null;
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Todo 토글
|
|
||||||
// ============================================================================
|
|
||||||
function toggleTodo(checkbox, todoId) {
|
|
||||||
toggleClass(checkbox, 'checked');
|
|
||||||
const isChecked = checkbox.classList.contains('checked');
|
|
||||||
checkbox.setAttribute('aria-checked', isChecked);
|
|
||||||
|
|
||||||
const todoTitle = checkbox.nextElementSibling.querySelector('.todo-title');
|
|
||||||
if (isChecked) {
|
|
||||||
addClass(todoTitle, 'completed');
|
|
||||||
} else {
|
|
||||||
removeClass(todoTitle, 'completed');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Todo 수정
|
|
||||||
// ============================================================================
|
|
||||||
function editTodo(todoId) {
|
|
||||||
currentEditTodoId = todoId;
|
|
||||||
|
|
||||||
// 예제 데이터 로드
|
|
||||||
const todoData = {
|
|
||||||
1: { content: '요구사항 정의서 작성', assignee: '김민준', dueDate: '2025-10-25', priority: 'high' },
|
|
||||||
2: { content: '기술 스택 상세 검토', assignee: '박서연', dueDate: '2025-10-27', priority: 'medium' },
|
|
||||||
3: { content: '인프라 설계 문서 작성', assignee: '이준호', dueDate: '2025-10-30', priority: 'high' }
|
|
||||||
};
|
|
||||||
|
|
||||||
const todo = todoData[todoId];
|
|
||||||
if (!todo) return;
|
|
||||||
|
|
||||||
// 모달에 데이터 설정
|
|
||||||
$('#todo-content').value = todo.content;
|
|
||||||
$('#todo-assignee').value = todo.assignee;
|
|
||||||
$('#todo-duedate').value = todo.dueDate;
|
|
||||||
$('#todo-priority').value = todo.priority;
|
|
||||||
|
|
||||||
showModal('edit-todo-modal');
|
|
||||||
}
|
|
||||||
|
|
||||||
function saveTodo() {
|
|
||||||
// 폼 검증
|
|
||||||
const content = $('#todo-content').value.trim();
|
|
||||||
const assignee = $('#todo-assignee').value;
|
|
||||||
const dueDate = $('#todo-duedate').value;
|
|
||||||
const priority = $('#todo-priority').value;
|
|
||||||
|
|
||||||
if (!content || !assignee || !dueDate || !priority) {
|
|
||||||
showToast('모든 필드를 입력해주세요', 'error');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Todo 업데이트 시뮬레이션
|
|
||||||
hideModal('edit-todo-modal');
|
|
||||||
showToast('Todo가 수정되었습니다', 'success');
|
|
||||||
}
|
|
||||||
|
|
||||||
function addNewTodo() {
|
|
||||||
currentEditTodoId = null;
|
|
||||||
|
|
||||||
// 모달 초기화
|
|
||||||
$('#todo-content').value = '';
|
|
||||||
$('#todo-assignee').value = '';
|
|
||||||
$('#todo-duedate').value = '';
|
|
||||||
$('#todo-priority').value = '';
|
|
||||||
|
|
||||||
showModal('edit-todo-modal');
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// 키워드 맥락 표시
|
|
||||||
// ============================================================================
|
|
||||||
function showKeywordContext(keyword) {
|
|
||||||
const keywordData = {
|
|
||||||
'MVP': {
|
|
||||||
contexts: [
|
|
||||||
'"우리는 Q1까지 MVP를 완성해야 합니다" - 김민준 (14:23)',
|
|
||||||
'"MVP는 핵심 기능만 구현하여 빠르게 시장 검증을 하는 것이 목표입니다" - 박서연 (14:25)'
|
|
||||||
]
|
|
||||||
},
|
|
||||||
'React': {
|
|
||||||
contexts: [
|
|
||||||
'"개발 프레임워크는 React를 사용하기로 결정했습니다" - 김민준 (14:28)',
|
|
||||||
'"React는 컴포넌트 기반이라 유지보수가 용이합니다" - 최유진 (14:30)'
|
|
||||||
]
|
|
||||||
},
|
|
||||||
'AWS': {
|
|
||||||
contexts: [
|
|
||||||
'"배포 환경은 AWS로 결정했습니다" - 김민준 (14:28)',
|
|
||||||
'"AWS는 확장성이 좋고 관리 도구가 풍부합니다" - 이준호 (14:31)'
|
|
||||||
]
|
|
||||||
},
|
|
||||||
'Sprint': {
|
|
||||||
contexts: [
|
|
||||||
'"Sprint 주기는 2주로 설정합니다" - 박서연 (14:35)'
|
|
||||||
]
|
|
||||||
},
|
|
||||||
'Q1': {
|
|
||||||
contexts: [
|
|
||||||
'"우리는 Q1까지 MVP를 완성해야 합니다" - 김민준 (14:23)',
|
|
||||||
'"Q1 목표를 달성하기 위해서는 주간 단위로 진행 상황을 체크해야 합니다" - 박서연 (14:26)'
|
|
||||||
]
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const data = keywordData[keyword];
|
|
||||||
if (!data) return;
|
|
||||||
|
|
||||||
const content = `
|
|
||||||
<div style="margin-bottom: var(--space-4);">
|
|
||||||
<div style="display: flex; align-items: center; gap: var(--space-2); margin-bottom: var(--space-3);">
|
|
||||||
<span class="badge badge-in-progress">#${keyword}</span>
|
|
||||||
<span class="text-caption" style="color: var(--text-tertiary);">회의록 내 ${data.contexts.length}회 언급</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h3 class="h4" style="margin-bottom: var(--space-3);">💬 언급된 맥락</h3>
|
|
||||||
|
|
||||||
${data.contexts.map(context => `
|
|
||||||
<div style="padding: var(--space-3); background-color: var(--bg-secondary); border-radius: var(--radius-medium); margin-bottom: var(--space-2);">
|
|
||||||
<p class="text-body">${context}</p>
|
|
||||||
</div>
|
|
||||||
`).join('')}
|
|
||||||
|
|
||||||
<button class="button-secondary button-small w-full" onclick="hideModal('keyword-modal'); navigateTo('05-회의진행.html')">
|
|
||||||
회의록에서 확인하기
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
$('#keyword-content').innerHTML = content;
|
|
||||||
showModal('keyword-modal');
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// 최종 확정
|
|
||||||
// ============================================================================
|
|
||||||
function confirmMeeting() {
|
|
||||||
// 필수 항목 검증 (이미 모두 완료된 상태)
|
|
||||||
showToast('회의록을 최종 확정합니다...', 'info', 2000);
|
|
||||||
|
|
||||||
// Todo 서비스로 데이터 전달 시뮬레이션
|
|
||||||
setTimeout(() => {
|
|
||||||
showToast('Todo가 생성되었습니다', 'success', 2000);
|
|
||||||
}, 2000);
|
|
||||||
|
|
||||||
// 회의록 공유 화면으로 이동
|
|
||||||
setTimeout(() => {
|
|
||||||
navigateTo('08-회의록공유.html');
|
|
||||||
}, 4000);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// 나중에 확정
|
|
||||||
// ============================================================================
|
|
||||||
function saveLater() {
|
|
||||||
showToast('회의록이 저장되었습니다', 'success', 2000);
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
navigateTo('02-대시보드.html');
|
|
||||||
}, 2000);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// 키보드 접근성
|
|
||||||
// ============================================================================
|
|
||||||
// Enter/Space로 체크박스 토글
|
|
||||||
$$('.todo-checkbox').forEach(checkbox => {
|
|
||||||
checkbox.addEventListener('keydown', (e) => {
|
|
||||||
if (e.key === 'Enter' || e.key === ' ') {
|
|
||||||
e.preventDefault();
|
|
||||||
checkbox.click();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// 초기화
|
|
||||||
// ============================================================================
|
|
||||||
// 오늘 날짜 기본값 설정
|
|
||||||
const today = new Date();
|
|
||||||
const tomorrow = new Date(today);
|
|
||||||
tomorrow.setDate(tomorrow.getDate() + 1);
|
|
||||||
$('#todo-duedate').setAttribute('min', formatDate(tomorrow));
|
|
||||||
|
|
||||||
console.log('회의종료 화면 초기화 완료');
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@ -1,423 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="ko">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<meta name="description" content="회의록 작성 및 공유 개선 서비스 - 회의록공유">
|
|
||||||
<title>회의록 공유 - 회의록 작성 서비스</title>
|
|
||||||
<link rel="stylesheet" href="common.css">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<!-- Skip to Main Content (접근성) -->
|
|
||||||
<a href="#main-content" class="skip-to-main">본문으로 건너뛰기</a>
|
|
||||||
|
|
||||||
<!-- Header -->
|
|
||||||
<header style="position: sticky; top: 0; background: var(--bg-primary); border-bottom: var(--border-thin) solid var(--gray-200); z-index: 10; padding: var(--space-4);">
|
|
||||||
<div style="display: flex; align-items: center; justify-content: space-between; max-width: 1440px; margin: 0 auto;">
|
|
||||||
<button class="button-icon button-ghost" onclick="goBack()" aria-label="뒤로가기">
|
|
||||||
<span style="font-size: 24px;">←</span>
|
|
||||||
</button>
|
|
||||||
<h1 class="h4" style="margin: 0;">회의록 공유</h1>
|
|
||||||
<button class="button-primary button-small" onclick="shareMeeting()" aria-label="공유하기">
|
|
||||||
공유
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<!-- Main Content -->
|
|
||||||
<main id="main-content" class="container" style="padding-top: var(--space-4); padding-bottom: var(--space-6); max-width: 1024px;">
|
|
||||||
|
|
||||||
<!-- Share Target Section -->
|
|
||||||
<section aria-labelledby="target-section" style="margin-bottom: var(--space-6);">
|
|
||||||
<h2 class="h4" id="target-section" style="margin-bottom: var(--space-3);">공유 대상</h2>
|
|
||||||
<div class="card">
|
|
||||||
<div style="display: flex; flex-direction: column; gap: var(--space-3);">
|
|
||||||
<label style="display: flex; align-items: center; gap: var(--space-3); cursor: pointer;">
|
|
||||||
<input type="radio" name="share-target" value="all" checked onchange="updateShareTarget()" style="width: 20px; height: 20px;">
|
|
||||||
<span class="text-body">참석자 전체 (기본)</span>
|
|
||||||
</label>
|
|
||||||
<label style="display: flex; align-items: center; gap: var(--space-3); cursor: pointer;">
|
|
||||||
<input type="radio" name="share-target" value="selected" onchange="updateShareTarget()" style="width: 20px; height: 20px;">
|
|
||||||
<span class="text-body">특정 참석자 선택</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 특정 참석자 선택 영역 (숨김) -->
|
|
||||||
<div id="selected-attendees" style="display: none; margin-top: var(--space-4); padding-top: var(--space-4); border-top: var(--border-thin) solid var(--gray-200);">
|
|
||||||
<div style="display: flex; flex-direction: column; gap: var(--space-2);">
|
|
||||||
<label style="display: flex; align-items: center; gap: var(--space-2); cursor: pointer;">
|
|
||||||
<input type="checkbox" value="1" style="width: 20px; height: 20px;">
|
|
||||||
<span style="font-size: 20px;">👨💼</span>
|
|
||||||
<span class="text-body">김민준</span>
|
|
||||||
</label>
|
|
||||||
<label style="display: flex; align-items: center; gap: var(--space-2); cursor: pointer;">
|
|
||||||
<input type="checkbox" value="2" style="width: 20px; height: 20px;">
|
|
||||||
<span style="font-size: 20px;">👩💻</span>
|
|
||||||
<span class="text-body">박서연</span>
|
|
||||||
</label>
|
|
||||||
<label style="display: flex; align-items: center; gap: var(--space-2); cursor: pointer;">
|
|
||||||
<input type="checkbox" value="3" style="width: 20px; height: 20px;">
|
|
||||||
<span style="font-size: 20px;">👨💻</span>
|
|
||||||
<span class="text-body">이준호</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- Share Permission Section -->
|
|
||||||
<section aria-labelledby="permission-section" style="margin-bottom: var(--space-6);">
|
|
||||||
<h2 class="h4" id="permission-section" style="margin-bottom: var(--space-3);">공유 권한</h2>
|
|
||||||
<div class="card">
|
|
||||||
<div style="display: flex; flex-direction: column; gap: var(--space-3);">
|
|
||||||
<label style="display: flex; align-items: center; gap: var(--space-3); cursor: pointer;">
|
|
||||||
<input type="radio" name="permission" value="read" checked style="width: 20px; height: 20px;">
|
|
||||||
<div>
|
|
||||||
<div class="text-body" style="font-weight: 500;">읽기 전용</div>
|
|
||||||
<div class="text-caption" style="color: var(--text-tertiary);">회의록을 조회만 할 수 있습니다</div>
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
<label style="display: flex; align-items: center; gap: var(--space-3); cursor: pointer;">
|
|
||||||
<input type="radio" name="permission" value="comment" style="width: 20px; height: 20px;">
|
|
||||||
<div>
|
|
||||||
<div class="text-body" style="font-weight: 500;">댓글 가능</div>
|
|
||||||
<div class="text-caption" style="color: var(--text-tertiary);">회의록에 댓글을 작성할 수 있습니다</div>
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
<label style="display: flex; align-items: center; gap: var(--space-3); cursor: pointer;">
|
|
||||||
<input type="radio" name="permission" value="edit" style="width: 20px; height: 20px;">
|
|
||||||
<div>
|
|
||||||
<div class="text-body" style="font-weight: 500;">편집 가능</div>
|
|
||||||
<div class="text-caption" style="color: var(--text-tertiary);">회의록을 직접 수정할 수 있습니다</div>
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- Share Method Section -->
|
|
||||||
<section aria-labelledby="method-section" style="margin-bottom: var(--space-6);">
|
|
||||||
<h2 class="h4" id="method-section" style="margin-bottom: var(--space-3);">공유 방식</h2>
|
|
||||||
<div class="card">
|
|
||||||
<div style="display: flex; flex-direction: column; gap: var(--space-3);">
|
|
||||||
<label style="display: flex; align-items: center; gap: var(--space-3); cursor: pointer;">
|
|
||||||
<input type="checkbox" id="share-email" checked style="width: 20px; height: 20px;">
|
|
||||||
<div>
|
|
||||||
<div class="text-body" style="font-weight: 500;">이메일 발송</div>
|
|
||||||
<div class="text-caption" style="color: var(--text-tertiary);">참석자 이메일로 회의록 링크를 전송합니다</div>
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
<label style="display: flex; align-items: center; gap: var(--space-3); cursor: pointer;">
|
|
||||||
<input type="checkbox" id="share-link" checked style="width: 20px; height: 20px;">
|
|
||||||
<div>
|
|
||||||
<div class="text-body" style="font-weight: 500;">링크 복사</div>
|
|
||||||
<div class="text-caption" style="color: var(--text-tertiary);">공유 링크를 클립보드에 복사합니다</div>
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- Link Security Section -->
|
|
||||||
<section aria-labelledby="security-section" style="margin-bottom: var(--space-6);">
|
|
||||||
<h2 class="h4" id="security-section" style="margin-bottom: var(--space-3);">링크 보안 (선택)</h2>
|
|
||||||
<div class="card">
|
|
||||||
<!-- 유효 기간 -->
|
|
||||||
<div style="margin-bottom: var(--space-4);">
|
|
||||||
<label style="display: flex; align-items: center; gap: var(--space-3); margin-bottom: var(--space-2); cursor: pointer;">
|
|
||||||
<input type="checkbox" id="enable-expiration" onchange="toggleExpiration()" style="width: 20px; height: 20px;">
|
|
||||||
<span class="text-body" style="font-weight: 500;">유효 기간 설정</span>
|
|
||||||
</label>
|
|
||||||
<div id="expiration-options" style="display: none; padding-left: 43px;">
|
|
||||||
<select id="expiration-days" class="input-field" aria-label="유효 기간">
|
|
||||||
<option value="7">7일</option>
|
|
||||||
<option value="30" selected>30일</option>
|
|
||||||
<option value="90">90일</option>
|
|
||||||
<option value="365">1년</option>
|
|
||||||
<option value="-1">무제한</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 비밀번호 -->
|
|
||||||
<div>
|
|
||||||
<label style="display: flex; align-items: center; gap: var(--space-3); margin-bottom: var(--space-2); cursor: pointer;">
|
|
||||||
<input type="checkbox" id="enable-password" onchange="togglePassword()" style="width: 20px; height: 20px;">
|
|
||||||
<span class="text-body" style="font-weight: 500;">비밀번호 설정</span>
|
|
||||||
</label>
|
|
||||||
<div id="password-options" style="display: none; padding-left: 43px;">
|
|
||||||
<div style="display: flex; gap: var(--space-2);">
|
|
||||||
<input type="password" id="link-password" class="input-field" placeholder="비밀번호 입력" aria-label="비밀번호">
|
|
||||||
<button class="button-secondary button-small" onclick="generatePassword()" style="white-space: nowrap;">
|
|
||||||
자동 생성
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- Next Meeting Section -->
|
|
||||||
<section aria-labelledby="next-meeting-section" style="margin-bottom: var(--space-6);">
|
|
||||||
<h2 class="h4" id="next-meeting-section" style="margin-bottom: var(--space-3);">🔔 다음 회의 일정</h2>
|
|
||||||
<div class="card" style="background-color: var(--info-50); border-color: var(--info-200);">
|
|
||||||
<div style="margin-bottom: var(--space-3);">
|
|
||||||
<label style="display: flex; align-items: center; gap: var(--space-3); cursor: pointer;">
|
|
||||||
<input type="checkbox" id="auto-calendar" checked style="width: 20px; height: 20px;">
|
|
||||||
<span class="text-body" style="font-weight: 500; color: var(--info-700);">캘린더 자동 등록</span>
|
|
||||||
</label>
|
|
||||||
<p class="text-caption" style="color: var(--info-700); margin-top: var(--space-1); padding-left: 43px;">
|
|
||||||
다음 회의 일정이 감지되면 자동으로 캘린더에 등록됩니다
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="calendar-options" style="padding-left: 43px;">
|
|
||||||
<div class="input-group">
|
|
||||||
<label for="next-meeting-date" class="input-label">날짜</label>
|
|
||||||
<input type="date" id="next-meeting-date" class="input-field" aria-label="다음 회의 날짜">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- Share Button -->
|
|
||||||
<section>
|
|
||||||
<button class="button-primary w-full" style="height: 48px; font-size: 1rem;" onclick="shareMeeting()">
|
|
||||||
회의록 공유
|
|
||||||
</button>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<!-- Share Success Modal -->
|
|
||||||
<div id="success-modal" class="modal-overlay" style="display: none;" role="dialog" aria-modal="true" aria-labelledby="success-title">
|
|
||||||
<div class="modal">
|
|
||||||
<div style="text-align: center; padding: var(--space-4) 0;">
|
|
||||||
<div style="font-size: 64px; margin-bottom: var(--space-3);">✅</div>
|
|
||||||
<h2 id="success-title" class="h2" style="margin-bottom: var(--space-2);">공유 완료!</h2>
|
|
||||||
<p class="text-body" style="color: var(--text-tertiary); margin-bottom: var(--space-4);">
|
|
||||||
회의록이 성공적으로 공유되었습니다
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div id="share-link-display" style="display: none; background-color: var(--bg-secondary); border: var(--border-thin) solid var(--gray-200); border-radius: var(--radius-medium); padding: var(--space-3); margin-bottom: var(--space-4); word-break: break-all;">
|
|
||||||
<p class="text-caption" style="color: var(--text-tertiary); margin-bottom: var(--space-2);">공유 링크</p>
|
|
||||||
<p class="text-body" style="font-family: var(--font-mono); color: var(--primary-500);" id="share-link-text">
|
|
||||||
https://meeting.company.com/share/abc123xyz
|
|
||||||
</p>
|
|
||||||
<button class="button-secondary button-small w-full" onclick="copyShareLink()" style="margin-top: var(--space-2);">
|
|
||||||
📋 링크 복사
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="display: flex; flex-direction: column; gap: var(--space-2);">
|
|
||||||
<button class="button-primary w-full" onclick="goToDashboard()">
|
|
||||||
대시보드로 이동
|
|
||||||
</button>
|
|
||||||
<button class="button-secondary w-full" onclick="viewMeetingMinutes()">
|
|
||||||
회의록 보기
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script src="common.js"></script>
|
|
||||||
<script>
|
|
||||||
// ============================================================================
|
|
||||||
// 공유 대상 업데이트
|
|
||||||
// ============================================================================
|
|
||||||
function updateShareTarget() {
|
|
||||||
const selectedRadio = $('input[name="share-target"]:checked');
|
|
||||||
const selectedAttendeesDiv = $('#selected-attendees');
|
|
||||||
|
|
||||||
if (selectedRadio && selectedRadio.value === 'selected') {
|
|
||||||
selectedAttendeesDiv.style.display = 'block';
|
|
||||||
} else {
|
|
||||||
selectedAttendeesDiv.style.display = 'none';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// 유효 기간 토글
|
|
||||||
// ============================================================================
|
|
||||||
function toggleExpiration() {
|
|
||||||
const checkbox = $('#enable-expiration');
|
|
||||||
const options = $('#expiration-options');
|
|
||||||
|
|
||||||
if (checkbox.checked) {
|
|
||||||
options.style.display = 'block';
|
|
||||||
} else {
|
|
||||||
options.style.display = 'none';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// 비밀번호 토글
|
|
||||||
// ============================================================================
|
|
||||||
function togglePassword() {
|
|
||||||
const checkbox = $('#enable-password');
|
|
||||||
const options = $('#password-options');
|
|
||||||
|
|
||||||
if (checkbox.checked) {
|
|
||||||
options.style.display = 'block';
|
|
||||||
} else {
|
|
||||||
options.style.display = 'none';
|
|
||||||
$('#link-password').value = '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// 비밀번호 자동 생성
|
|
||||||
// ============================================================================
|
|
||||||
function generatePassword() {
|
|
||||||
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*';
|
|
||||||
let password = '';
|
|
||||||
|
|
||||||
for (let i = 0; i < 12; i++) {
|
|
||||||
password += chars.charAt(Math.floor(Math.random() * chars.length));
|
|
||||||
}
|
|
||||||
|
|
||||||
$('#link-password').value = password;
|
|
||||||
$('#link-password').type = 'text';
|
|
||||||
|
|
||||||
showToast('비밀번호가 생성되었습니다', 'success');
|
|
||||||
|
|
||||||
// 3초 후 다시 숨김
|
|
||||||
setTimeout(() => {
|
|
||||||
$('#link-password').type = 'password';
|
|
||||||
}, 3000);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// 회의록 공유
|
|
||||||
// ============================================================================
|
|
||||||
function shareMeeting() {
|
|
||||||
// 입력 검증
|
|
||||||
const shareTarget = $('input[name="share-target"]:checked').value;
|
|
||||||
|
|
||||||
if (shareTarget === 'selected') {
|
|
||||||
const selectedAttendees = $$('#selected-attendees input[type="checkbox"]:checked');
|
|
||||||
if (selectedAttendees.length === 0) {
|
|
||||||
showToast('공유할 참석자를 선택해주세요', 'error');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 비밀번호 검증
|
|
||||||
const enablePassword = $('#enable-password').checked;
|
|
||||||
if (enablePassword) {
|
|
||||||
const password = $('#link-password').value.trim();
|
|
||||||
if (!password) {
|
|
||||||
showToast('비밀번호를 입력해주세요', 'error');
|
|
||||||
$('#link-password').focus();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 공유 처리
|
|
||||||
showToast('회의록을 공유하는 중...', 'info', 2000);
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
// 이메일 발송 시뮬레이션
|
|
||||||
const emailChecked = $('#share-email').checked;
|
|
||||||
if (emailChecked) {
|
|
||||||
showToast('이메일이 발송되었습니다', 'success', 2000);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 링크 복사 시뮬레이션
|
|
||||||
const linkChecked = $('#share-link').checked;
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
// 성공 모달 표시
|
|
||||||
const shareLinkDisplay = $('#share-link-display');
|
|
||||||
if (linkChecked) {
|
|
||||||
shareLinkDisplay.style.display = 'block';
|
|
||||||
}
|
|
||||||
|
|
||||||
showModal('success-modal');
|
|
||||||
|
|
||||||
// 공유 시간 기록
|
|
||||||
const shareTime = new Date();
|
|
||||||
saveData('lastShareTime', shareTime.toISOString());
|
|
||||||
|
|
||||||
// 캘린더 등록 시뮬레이션
|
|
||||||
const autoCalendar = $('#auto-calendar').checked;
|
|
||||||
const nextMeetingDate = $('#next-meeting-date').value;
|
|
||||||
if (autoCalendar && nextMeetingDate) {
|
|
||||||
setTimeout(() => {
|
|
||||||
showToast(`다음 회의가 ${nextMeetingDate}에 등록되었습니다`, 'info', 3000);
|
|
||||||
}, 2000);
|
|
||||||
}
|
|
||||||
}, 2000);
|
|
||||||
}, 2000);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// 공유 링크 복사
|
|
||||||
// ============================================================================
|
|
||||||
function copyShareLink() {
|
|
||||||
const linkText = $('#share-link-text').textContent;
|
|
||||||
|
|
||||||
// 클립보드에 복사
|
|
||||||
navigator.clipboard.writeText(linkText).then(() => {
|
|
||||||
showToast('링크가 복사되었습니다', 'success');
|
|
||||||
}).catch(() => {
|
|
||||||
// 폴백: 텍스트 선택
|
|
||||||
const range = document.createRange();
|
|
||||||
range.selectNode($('#share-link-text'));
|
|
||||||
window.getSelection().removeAllRanges();
|
|
||||||
window.getSelection().addRange(range);
|
|
||||||
|
|
||||||
try {
|
|
||||||
document.execCommand('copy');
|
|
||||||
showToast('링크가 복사되었습니다', 'success');
|
|
||||||
} catch (err) {
|
|
||||||
showToast('링크 복사에 실패했습니다', 'error');
|
|
||||||
}
|
|
||||||
|
|
||||||
window.getSelection().removeAllRanges();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// 대시보드로 이동
|
|
||||||
// ============================================================================
|
|
||||||
function goToDashboard() {
|
|
||||||
navigateTo('02-대시보드.html');
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// 회의록 보기
|
|
||||||
// ============================================================================
|
|
||||||
function viewMeetingMinutes() {
|
|
||||||
hideModal('success-modal');
|
|
||||||
showToast('회의록 상세 화면으로 이동합니다', 'info');
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
navigateTo('02-대시보드.html');
|
|
||||||
}, 1500);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// 초기화
|
|
||||||
// ============================================================================
|
|
||||||
// 내일 날짜를 기본값으로 설정
|
|
||||||
const tomorrow = new Date();
|
|
||||||
tomorrow.setDate(tomorrow.getDate() + 7); // 1주일 후
|
|
||||||
$('#next-meeting-date').value = formatDate(tomorrow);
|
|
||||||
$('#next-meeting-date').setAttribute('min', formatDate(new Date()));
|
|
||||||
|
|
||||||
// 캘린더 자동 등록 체크박스 이벤트
|
|
||||||
$('#auto-calendar').addEventListener('change', (e) => {
|
|
||||||
const calendarOptions = $('#calendar-options');
|
|
||||||
if (e.target.checked) {
|
|
||||||
calendarOptions.style.display = 'block';
|
|
||||||
} else {
|
|
||||||
calendarOptions.style.display = 'none';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('회의록공유 화면 초기화 완료');
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@ -1,459 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="ko">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<meta name="description" content="회의록 작성 및 공유 개선 서비스 - Todo 관리">
|
|
||||||
<title>Todo 관리 - 회의록 작성 서비스</title>
|
|
||||||
<link rel="stylesheet" href="common.css">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<!-- Skip to Main Content (접근성) -->
|
|
||||||
<a href="#main-content" class="skip-to-main">본문으로 건너뛰기</a>
|
|
||||||
|
|
||||||
<!-- Header -->
|
|
||||||
<header style="position: sticky; top: 0; background: var(--bg-primary); border-bottom: var(--border-thin) solid var(--gray-200); z-index: 10; padding: var(--space-4);">
|
|
||||||
<div style="display: flex; align-items: center; justify-content: space-between; max-width: 1440px; margin: 0 auto;">
|
|
||||||
<div style="display: flex; align-items: center; gap: var(--space-2);">
|
|
||||||
<span style="font-size: 24px;">👨💼</span>
|
|
||||||
<span class="text-caption" style="color: var(--text-secondary);">김민준</span>
|
|
||||||
</div>
|
|
||||||
<h1 class="h4" style="margin: 0;">Todo</h1>
|
|
||||||
<button class="button-icon button-ghost" aria-label="알림">
|
|
||||||
<span style="font-size: 20px;">🔔</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<!-- Main Content -->
|
|
||||||
<main id="main-content" class="container" style="padding-top: var(--space-4); padding-bottom: 80px; max-width: 1024px;">
|
|
||||||
|
|
||||||
<!-- Filter Section -->
|
|
||||||
<section aria-labelledby="filter-section" style="margin-bottom: var(--space-4);">
|
|
||||||
<div style="display: flex; gap: var(--space-2); flex-wrap: wrap; align-items: center;">
|
|
||||||
<!-- 상태 필터 -->
|
|
||||||
<div class="input-group" style="flex: 1; min-width: 150px;">
|
|
||||||
<select id="status-filter" class="input-field" aria-label="상태 필터" onchange="filterTodos()">
|
|
||||||
<option value="all">전체</option>
|
|
||||||
<option value="pending" selected>진행중</option>
|
|
||||||
<option value="completed">완료됨</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 정렬 -->
|
|
||||||
<div class="input-group" style="flex: 1; min-width: 150px;">
|
|
||||||
<select id="sort-filter" class="input-field" aria-label="정렬" onchange="sortTodos()">
|
|
||||||
<option value="dueDate" selected>마감일순</option>
|
|
||||||
<option value="priority">우선순위순</option>
|
|
||||||
<option value="latest">최신순</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- Pending Todos Section -->
|
|
||||||
<section id="pending-section" aria-labelledby="pending-title" style="margin-bottom: var(--space-6);">
|
|
||||||
<h2 class="h4" id="pending-title" style="margin-bottom: var(--space-3);">📌 진행 중 (<span id="pending-count">3</span>건)</h2>
|
|
||||||
|
|
||||||
<div id="pending-todos">
|
|
||||||
<!-- Todo Card 1 (Priority: High, Urgent) -->
|
|
||||||
<div class="todo-card priority-high" style="margin-bottom: var(--space-3);" data-priority="high" data-due-date="2025-10-25" data-status="pending">
|
|
||||||
<div class="todo-checkbox" onclick="completeTodo(this, 1)" role="checkbox" aria-checked="false" tabindex="0" aria-label="요구사항 정의 완료 처리"></div>
|
|
||||||
<div class="todo-content" onclick="showTodoDetail(1)" style="cursor: pointer;">
|
|
||||||
<div class="todo-title">요구사항 정의서 작성</div>
|
|
||||||
<div class="todo-meta">
|
|
||||||
<span class="todo-assignee">@김민준</span>
|
|
||||||
<span class="todo-duedate urgent">📅 ~ 10/25 (D-5)</span>
|
|
||||||
<span style="color: var(--error-500); font-weight: 600;">⭐ 높음</span>
|
|
||||||
</div>
|
|
||||||
<a href="#" class="todo-meeting-link" onclick="event.stopPropagation(); navigateTo('02-대시보드.html')">
|
|
||||||
📝 프로젝트 킥오프 (10/20)
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Todo Card 2 (Priority: Medium) -->
|
|
||||||
<div class="todo-card priority-medium" style="margin-bottom: var(--space-3);" data-priority="medium" data-due-date="2025-10-27" data-status="pending">
|
|
||||||
<div class="todo-checkbox" onclick="completeTodo(this, 2)" role="checkbox" aria-checked="false" tabindex="0" aria-label="기술 스택 검토 완료 처리"></div>
|
|
||||||
<div class="todo-content" onclick="showTodoDetail(2)" style="cursor: pointer;">
|
|
||||||
<div class="todo-title">기술 스택 상세 검토</div>
|
|
||||||
<div class="todo-meta">
|
|
||||||
<span class="todo-assignee">@박서연</span>
|
|
||||||
<span class="todo-duedate">📅 ~ 10/27 (D-7)</span>
|
|
||||||
<span style="color: var(--warning-500); font-weight: 600;">⭐ 보통</span>
|
|
||||||
</div>
|
|
||||||
<a href="#" class="todo-meeting-link" onclick="event.stopPropagation(); navigateTo('02-대시보드.html')">
|
|
||||||
📝 프로젝트 킥오프 (10/20)
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Todo Card 3 (Priority: High) -->
|
|
||||||
<div class="todo-card priority-high" style="margin-bottom: var(--space-3);" data-priority="high" data-due-date="2025-10-22" data-status="pending">
|
|
||||||
<div class="todo-checkbox" onclick="completeTodo(this, 3)" role="checkbox" aria-checked="false" tabindex="0" aria-label="DB 스키마 수정 완료 처리"></div>
|
|
||||||
<div class="todo-content" onclick="showTodoDetail(3)" style="cursor: pointer;">
|
|
||||||
<div class="todo-title">DB 스키마 수정</div>
|
|
||||||
<div class="todo-meta">
|
|
||||||
<span class="todo-assignee">@이준호</span>
|
|
||||||
<span class="todo-duedate urgent">📅 ~ 10/22 (D-2)</span>
|
|
||||||
<span style="color: var(--error-500); font-weight: 600;">⭐ 높음</span>
|
|
||||||
</div>
|
|
||||||
<a href="#" class="todo-meeting-link" onclick="event.stopPropagation(); navigateTo('02-대시보드.html')">
|
|
||||||
📝 주간 회의 (10/19)
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- Completed Todos Section -->
|
|
||||||
<section id="completed-section" aria-labelledby="completed-title" style="display: none;">
|
|
||||||
<h2 class="h4" id="completed-title" style="margin-bottom: var(--space-3);">✅ 완료됨 (<span id="completed-count">2</span>건)</h2>
|
|
||||||
|
|
||||||
<div id="completed-todos">
|
|
||||||
<!-- Completed Todo Card 1 -->
|
|
||||||
<div class="todo-card" style="margin-bottom: var(--space-3); opacity: 0.7;" data-priority="high" data-due-date="2025-10-24" data-status="completed">
|
|
||||||
<div class="todo-checkbox checked" role="checkbox" aria-checked="true" tabindex="0" aria-label="AI 모델 벤치마크 테스트 완료"></div>
|
|
||||||
<div class="todo-content" onclick="showTodoDetail(4)" style="cursor: pointer;">
|
|
||||||
<div class="todo-title completed">AI 모델 벤치마크 테스트</div>
|
|
||||||
<div class="todo-meta">
|
|
||||||
<span class="todo-assignee">@박서연</span>
|
|
||||||
<span class="todo-duedate" style="color: var(--success-500);">✓ 10/18 완료</span>
|
|
||||||
</div>
|
|
||||||
<a href="#" class="todo-meeting-link" onclick="event.stopPropagation(); navigateTo('02-대시보드.html')">
|
|
||||||
📝 기술 검토 회의 (10/17)
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Completed Todo Card 2 -->
|
|
||||||
<div class="todo-card" style="margin-bottom: var(--space-3); opacity: 0.7;" data-priority="medium" data-due-date="2025-10-20" data-status="completed">
|
|
||||||
<div class="todo-checkbox checked" role="checkbox" aria-checked="true" tabindex="0" aria-label="프로토타입 수정 완료"></div>
|
|
||||||
<div class="todo-content" onclick="showTodoDetail(5)" style="cursor: pointer;">
|
|
||||||
<div class="todo-title completed">프로토타입 수정</div>
|
|
||||||
<div class="todo-meta">
|
|
||||||
<span class="todo-assignee">@최유진</span>
|
|
||||||
<span class="todo-duedate" style="color: var(--success-500);">✓ 10/19 완료</span>
|
|
||||||
</div>
|
|
||||||
<a href="#" class="todo-meeting-link" onclick="event.stopPropagation(); navigateTo('02-대시보드.html')">
|
|
||||||
📝 디자인 리뷰 (10/16)
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- Empty State (숨김) -->
|
|
||||||
<div id="empty-state" style="display: none; text-align: center; padding: var(--space-16) var(--space-4);">
|
|
||||||
<div style="font-size: 64px; margin-bottom: var(--space-4);">📋</div>
|
|
||||||
<h3 class="h3" style="margin-bottom: var(--space-2);">할 일이 없습니다</h3>
|
|
||||||
<p class="text-body" style="color: var(--text-tertiary);">
|
|
||||||
새로운 회의를 진행하고 Todo를 생성해보세요.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<!-- Bottom Navigation -->
|
|
||||||
<nav style="position: fixed; bottom: 0; left: 0; right: 0; background: var(--bg-primary); border-top: var(--border-thin) solid var(--gray-200); z-index: 10;" aria-label="하단 네비게이션">
|
|
||||||
<div style="display: flex; justify-content: space-around; padding: var(--space-3) 0; max-width: 768px; margin: 0 auto;">
|
|
||||||
<a href="02-대시보드.html" class="button-ghost" style="display: flex; flex-direction: column; align-items: center; gap: var(--space-1); padding: var(--space-2);" aria-label="대시보드">
|
|
||||||
<span style="font-size: 24px;">📊</span>
|
|
||||||
<span class="text-caption">대시보드</span>
|
|
||||||
</a>
|
|
||||||
<a href="09-Todo관리.html" class="button-ghost" style="display: flex; flex-direction: column; align-items: center; gap: var(--space-1); padding: var(--space-2); color: var(--primary-500);" aria-label="Todo" aria-current="page">
|
|
||||||
<span style="font-size: 24px;">✅</span>
|
|
||||||
<span class="text-caption" style="color: var(--primary-500); font-weight: 600;">Todo</span>
|
|
||||||
</a>
|
|
||||||
<button class="button-ghost" style="display: flex; flex-direction: column; align-items: center; gap: var(--space-1); padding: var(--space-2);" aria-label="더보기" onclick="showToast('준비 중입니다', 'info')">
|
|
||||||
<span style="font-size: 24px;">⋯</span>
|
|
||||||
<span class="text-caption">더보기</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<!-- Todo Detail Modal -->
|
|
||||||
<div id="todo-detail-modal" class="modal-overlay" style="display: none;" role="dialog" aria-modal="true" aria-labelledby="todo-detail-title">
|
|
||||||
<div class="modal">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h2 id="todo-detail-title" class="modal-title">Todo 상세</h2>
|
|
||||||
<button class="modal-close" onclick="hideModal('todo-detail-modal')" aria-label="닫기">×</button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body" id="todo-detail-content">
|
|
||||||
<!-- JavaScript로 동적 생성 -->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Complete Todo Confirmation Modal -->
|
|
||||||
<div id="complete-todo-modal" class="modal-overlay" style="display: none;" role="dialog" aria-modal="true" aria-labelledby="complete-todo-title">
|
|
||||||
<div class="modal">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h2 id="complete-todo-title" class="modal-title">Todo 완료 처리</h2>
|
|
||||||
<button class="modal-close" onclick="hideModal('complete-todo-modal')" aria-label="닫기">×</button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<p class="text-body" style="margin-bottom: var(--space-4);">
|
|
||||||
이 Todo를 완료 처리하시겠습니까?<br>
|
|
||||||
완료 시 관련 회의록에 자동으로 반영됩니다.
|
|
||||||
</p>
|
|
||||||
<div class="card" style="background-color: var(--info-50); border-color: var(--info-200);">
|
|
||||||
<div style="display: flex; align-items: center; gap: var(--space-2); margin-bottom: var(--space-2);">
|
|
||||||
<span>💡</span>
|
|
||||||
<span class="text-caption" style="color: var(--info-700); font-weight: 600;">차별화 기능</span>
|
|
||||||
</div>
|
|
||||||
<p class="text-caption" style="color: var(--info-700);">
|
|
||||||
회의록의 Todo 섹션에 완료 상태가 자동으로 업데이트되고, 참석자들에게 알림이 전송됩니다.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button class="button-secondary" onclick="hideModal('complete-todo-modal')">
|
|
||||||
취소
|
|
||||||
</button>
|
|
||||||
<button class="button-primary" onclick="confirmCompleteTodo()">
|
|
||||||
완료 처리
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script src="common.js"></script>
|
|
||||||
<script>
|
|
||||||
// ============================================================================
|
|
||||||
// 상태 변수
|
|
||||||
// ============================================================================
|
|
||||||
let currentFilter = 'pending';
|
|
||||||
let currentSort = 'dueDate';
|
|
||||||
let currentTodoId = null;
|
|
||||||
let currentCheckbox = null;
|
|
||||||
|
|
||||||
// Todo 데이터 (mockTodos 사용)
|
|
||||||
const todos = [...mockTodos];
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Todo 필터링
|
|
||||||
// ============================================================================
|
|
||||||
function filterTodos() {
|
|
||||||
const filter = $('#status-filter').value;
|
|
||||||
currentFilter = filter;
|
|
||||||
|
|
||||||
const pendingSection = $('#pending-section');
|
|
||||||
const completedSection = $('#completed-section');
|
|
||||||
const emptyState = $('#empty-state');
|
|
||||||
|
|
||||||
if (filter === 'all') {
|
|
||||||
pendingSection.style.display = 'block';
|
|
||||||
completedSection.style.display = 'block';
|
|
||||||
emptyState.style.display = 'none';
|
|
||||||
} else if (filter === 'pending') {
|
|
||||||
pendingSection.style.display = 'block';
|
|
||||||
completedSection.style.display = 'none';
|
|
||||||
|
|
||||||
const hasPending = $$('#pending-todos .todo-card').length > 0;
|
|
||||||
emptyState.style.display = hasPending ? 'none' : 'block';
|
|
||||||
} else if (filter === 'completed') {
|
|
||||||
pendingSection.style.display = 'none';
|
|
||||||
completedSection.style.display = 'block';
|
|
||||||
|
|
||||||
const hasCompleted = $$('#completed-todos .todo-card').length > 0;
|
|
||||||
emptyState.style.display = hasCompleted ? 'none' : 'block';
|
|
||||||
}
|
|
||||||
|
|
||||||
updateCounts();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Todo 정렬
|
|
||||||
// ============================================================================
|
|
||||||
function sortTodos() {
|
|
||||||
const sort = $('#sort-filter').value;
|
|
||||||
currentSort = sort;
|
|
||||||
|
|
||||||
const pendingContainer = $('#pending-todos');
|
|
||||||
const completedContainer = $('#completed-todos');
|
|
||||||
|
|
||||||
sortContainer(pendingContainer, sort);
|
|
||||||
sortContainer(completedContainer, sort);
|
|
||||||
}
|
|
||||||
|
|
||||||
function sortContainer(container, sortBy) {
|
|
||||||
const cards = Array.from(container.querySelectorAll('.todo-card'));
|
|
||||||
|
|
||||||
cards.sort((a, b) => {
|
|
||||||
if (sortBy === 'dueDate') {
|
|
||||||
const dateA = new Date(a.dataset.dueDate);
|
|
||||||
const dateB = new Date(b.dataset.dueDate);
|
|
||||||
return dateA - dateB;
|
|
||||||
} else if (sortBy === 'priority') {
|
|
||||||
const priorityOrder = { 'high': 1, 'medium': 2, 'low': 3 };
|
|
||||||
const priorityA = priorityOrder[a.dataset.priority] || 999;
|
|
||||||
const priorityB = priorityOrder[b.dataset.priority] || 999;
|
|
||||||
return priorityA - priorityB;
|
|
||||||
} else if (sortBy === 'latest') {
|
|
||||||
// 최신순 (데이터 순서 역순)
|
|
||||||
return 0; // 예제에서는 생략
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// DOM 재정렬
|
|
||||||
cards.forEach(card => container.appendChild(card));
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Todo 개수 업데이트
|
|
||||||
// ============================================================================
|
|
||||||
function updateCounts() {
|
|
||||||
const pendingCount = $$('#pending-todos .todo-card').length;
|
|
||||||
const completedCount = $$('#completed-todos .todo-card').length;
|
|
||||||
|
|
||||||
$('#pending-count').textContent = pendingCount;
|
|
||||||
$('#completed-count').textContent = completedCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Todo 완료 처리
|
|
||||||
// ============================================================================
|
|
||||||
function completeTodo(checkbox, todoId) {
|
|
||||||
// 이미 완료된 Todo는 처리 안 함
|
|
||||||
if (checkbox.classList.contains('checked')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
currentTodoId = todoId;
|
|
||||||
currentCheckbox = checkbox;
|
|
||||||
showModal('complete-todo-modal');
|
|
||||||
}
|
|
||||||
|
|
||||||
function confirmCompleteTodo() {
|
|
||||||
hideModal('complete-todo-modal');
|
|
||||||
|
|
||||||
// 체크박스 체크
|
|
||||||
if (currentCheckbox) {
|
|
||||||
addClass(currentCheckbox, 'checked');
|
|
||||||
currentCheckbox.setAttribute('aria-checked', 'true');
|
|
||||||
|
|
||||||
const todoCard = currentCheckbox.closest('.todo-card');
|
|
||||||
const todoTitle = todoCard.querySelector('.todo-title');
|
|
||||||
addClass(todoTitle, 'completed');
|
|
||||||
|
|
||||||
// 완료된 Todo를 완료 섹션으로 이동
|
|
||||||
setTimeout(() => {
|
|
||||||
todoCard.dataset.status = 'completed';
|
|
||||||
$('#completed-todos').insertBefore(todoCard, $('#completed-todos').firstChild);
|
|
||||||
todoCard.style.opacity = '0.7';
|
|
||||||
|
|
||||||
updateCounts();
|
|
||||||
filterTodos();
|
|
||||||
|
|
||||||
// 회의록 자동 반영 알림 (차별화 기능)
|
|
||||||
showToast('회의록에 완료 상태가 반영되었습니다', 'success', 4000);
|
|
||||||
|
|
||||||
// 완료 섹션으로 자동 스크롤 (필터가 전체일 경우)
|
|
||||||
if (currentFilter === 'all') {
|
|
||||||
const completedSection = $('#completed-section');
|
|
||||||
completedSection.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
||||||
}
|
|
||||||
}, 300);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Todo 상세 보기
|
|
||||||
// ============================================================================
|
|
||||||
function showTodoDetail(todoId) {
|
|
||||||
const todo = todos.find(t => t.id === todoId);
|
|
||||||
if (!todo) return;
|
|
||||||
|
|
||||||
const content = `
|
|
||||||
<div style="margin-bottom: var(--space-4);">
|
|
||||||
<div style="display: flex; align-items: center; gap: var(--space-2); margin-bottom: var(--space-3);">
|
|
||||||
<div class="todo-checkbox ${todo.status === 'completed' ? 'checked' : ''}" role="checkbox" aria-checked="${todo.status === 'completed'}" style="pointer-events: none;"></div>
|
|
||||||
<h3 class="h4 ${todo.status === 'completed' ? 'todo-title completed' : ''}" style="margin: 0;">${todo.content}</h3>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="display: flex; flex-direction: column; gap: var(--space-3); margin-bottom: var(--space-4);">
|
|
||||||
<div style="display: flex; align-items: center; gap: var(--space-2);">
|
|
||||||
<span style="color: var(--text-tertiary); min-width: 80px;">담당자:</span>
|
|
||||||
<span style="font-weight: 500;">${todo.assigneeName}</span>
|
|
||||||
</div>
|
|
||||||
<div style="display: flex; align-items: center; gap: var(--space-2);">
|
|
||||||
<span style="color: var(--text-tertiary); min-width: 80px;">마감일:</span>
|
|
||||||
<span style="font-weight: 500;">${formatDate(todo.dueDate)} ${getDDay(todo.dueDate)}</span>
|
|
||||||
</div>
|
|
||||||
<div style="display: flex; align-items: center; gap: var(--space-2);">
|
|
||||||
<span style="color: var(--text-tertiary); min-width: 80px;">우선순위:</span>
|
|
||||||
<span style="font-weight: 500; color: ${todo.priority === 'high' ? 'var(--error-500)' : todo.priority === 'medium' ? 'var(--warning-500)' : 'var(--success-500)'};">
|
|
||||||
${todo.priority === 'high' ? '높음' : todo.priority === 'medium' ? '보통' : '낮음'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card" style="background-color: var(--primary-50); border-color: var(--primary-200); margin-bottom: var(--space-4);">
|
|
||||||
<h4 class="h4" style="margin-bottom: var(--space-2);">📝 관련 회의록</h4>
|
|
||||||
<p class="text-body" style="margin-bottom: var(--space-2);">
|
|
||||||
<strong>${todo.meetingTitle}</strong> (${formatDate(todo.meetingDate)})
|
|
||||||
</p>
|
|
||||||
<button class="button-secondary button-small" onclick="navigateTo('02-대시보드.html')">
|
|
||||||
회의록 보기
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="margin-bottom: var(--space-4);">
|
|
||||||
<h4 class="h4" style="margin-bottom: var(--space-3);">💬 댓글 (2)</h4>
|
|
||||||
|
|
||||||
<div style="margin-bottom: var(--space-3); padding: var(--space-3); background-color: var(--bg-secondary); border-radius: var(--radius-medium);">
|
|
||||||
<div style="display: flex; align-items: center; gap: var(--space-2); margin-bottom: var(--space-2);">
|
|
||||||
<span style="font-size: 20px;">👨💼</span>
|
|
||||||
<span style="font-weight: 600;">김민준</span>
|
|
||||||
<span class="text-caption" style="color: var(--text-tertiary);">2시간 전</span>
|
|
||||||
</div>
|
|
||||||
<p class="text-body">진행 중입니다. 오늘 중으로 초안 완성 예정입니다.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="padding: var(--space-3); background-color: var(--bg-secondary); border-radius: var(--radius-medium);">
|
|
||||||
<div style="display: flex; align-items: center; gap: var(--space-2); margin-bottom: var(--space-2);">
|
|
||||||
<span style="font-size: 20px;">👩💻</span>
|
|
||||||
<span style="font-weight: 600;">박서연</span>
|
|
||||||
<span class="text-caption" style="color: var(--text-tertiary);">1시간 전</span>
|
|
||||||
</div>
|
|
||||||
<p class="text-body">도움 필요하시면 언제든 연락주세요!</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
${todo.status === 'pending' ? `
|
|
||||||
<button class="button-primary w-full" onclick="hideModal('todo-detail-modal'); completeTodo($('.todo-checkbox[aria-label*=\\'${todo.content}\\']'), ${todoId})">
|
|
||||||
완료 처리
|
|
||||||
</button>
|
|
||||||
` : ''}
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
$('#todo-detail-content').innerHTML = content;
|
|
||||||
showModal('todo-detail-modal');
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// 키보드 접근성
|
|
||||||
// ============================================================================
|
|
||||||
// Enter/Space로 체크박스 토글
|
|
||||||
$$('.todo-checkbox').forEach(checkbox => {
|
|
||||||
checkbox.addEventListener('keydown', (e) => {
|
|
||||||
if (e.key === 'Enter' || e.key === ' ') {
|
|
||||||
e.preventDefault();
|
|
||||||
checkbox.click();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// 초기화
|
|
||||||
// ============================================================================
|
|
||||||
filterTodos();
|
|
||||||
sortTodos();
|
|
||||||
updateCounts();
|
|
||||||
|
|
||||||
console.log('Todo 관리 화면 초기화 완료');
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@ -1,374 +0,0 @@
|
|||||||
# 프로토타입 테스트 결과
|
|
||||||
|
|
||||||
## 테스트 개요
|
|
||||||
|
|
||||||
- **테스트 일시**: 2025-10-20
|
|
||||||
- **테스트 도구**: Playwright MCP
|
|
||||||
- **테스트 범위**: 9개 전체 화면 + 핵심 차별화 기능 2개 + 반응형 디자인
|
|
||||||
|
|
||||||
## 테스트 결과 요약
|
|
||||||
|
|
||||||
✅ **전체 테스트 통과** (13/13)
|
|
||||||
|
|
||||||
### 개발 완료 항목
|
|
||||||
1. ✅ 공통 Stylesheet (common.css) - 900+ 라인
|
|
||||||
2. ✅ 공통 Javascript (common.js) - 450+ 라인
|
|
||||||
3. ✅ 9개 HTML 화면 개발 완료
|
|
||||||
|
|
||||||
### 기능 테스트 통과 항목
|
|
||||||
1. ✅ 로그인 플로우 (01)
|
|
||||||
2. ✅ 회의 예약 플로우 (02→03→04)
|
|
||||||
3. ✅ 템플릿 선택 및 회의 진행 플로우 (04→05)
|
|
||||||
4. ✅ 핵심 차별화 기능 #1: 맥락 기반 용어 툴팁
|
|
||||||
5. ✅ 검증 및 종료 플로우 (05→06→07→08)
|
|
||||||
6. ✅ 핵심 차별화 기능 #2: Todo-회의록 실시간 연동
|
|
||||||
7. ✅ 반응형 디자인 검증 (Mobile/Tablet/Desktop)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 상세 테스트 결과
|
|
||||||
|
|
||||||
### 1. 로그인 플로우 (01-로그인.html)
|
|
||||||
|
|
||||||
**테스트 시나리오**:
|
|
||||||
- 사번 입력: E2024001
|
|
||||||
- 비밀번호 입력: password123
|
|
||||||
- 로그인 버튼 클릭
|
|
||||||
|
|
||||||
**결과**: ✅ 통과
|
|
||||||
- 폼 검증 정상 작동 (사번 형식: E+7자리 숫자)
|
|
||||||
- 로딩 오버레이 표시 확인
|
|
||||||
- 3초 후 대시보드로 자동 이동
|
|
||||||
- 페이드 애니메이션 정상 작동
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 2. 회의 예약 플로우 (02→03→04)
|
|
||||||
|
|
||||||
#### 2.1 대시보드 (02-대시보드.html)
|
|
||||||
|
|
||||||
**테스트 항목**:
|
|
||||||
- 회의록 목록 표시 (5건)
|
|
||||||
- 상태 필터 (전체/확정완료/작성중/임시저장)
|
|
||||||
- 정렬 기능 (최신순/회의일시순/제목순)
|
|
||||||
- 검색 기능 (debounce 300ms)
|
|
||||||
- "새 회의 예약" 버튼
|
|
||||||
|
|
||||||
**결과**: ✅ 통과
|
|
||||||
- MockMeetings 데이터 정상 렌더링
|
|
||||||
- 필터 및 정렬 UI 정상 표시
|
|
||||||
- 네비게이션 정상 작동
|
|
||||||
|
|
||||||
#### 2.2 회의 예약 (03-회의예약.html)
|
|
||||||
|
|
||||||
**테스트 시나리오**:
|
|
||||||
- 회의 제목 입력: "AI 기능 설계 회의"
|
|
||||||
- 날짜/시간: 자동 설정 (2025-10-20 23:43)
|
|
||||||
- 참석자 추가: minjun.kim@company.com
|
|
||||||
- 회의 예약하기 클릭
|
|
||||||
|
|
||||||
**결과**: ✅ 통과
|
|
||||||
- 실시간 이메일 검증 정상 작동
|
|
||||||
- 참석자 칩 형태로 추가됨
|
|
||||||
- 로딩 표시 후 템플릿 선택 화면으로 이동
|
|
||||||
- 폼 데이터 localStorage에 저장 확인
|
|
||||||
|
|
||||||
#### 2.3 템플릿 선택 (04-템플릿선택.html)
|
|
||||||
|
|
||||||
**테스트 시나리오**:
|
|
||||||
- 일반 회의 템플릿 선택
|
|
||||||
- 다음 버튼 클릭
|
|
||||||
|
|
||||||
**결과**: ✅ 통과
|
|
||||||
- 4개 템플릿 카드 정상 렌더링
|
|
||||||
- 라디오 버튼 선택 시 토스트 메시지 표시
|
|
||||||
- 템플릿 선택 후 다음 버튼 활성화
|
|
||||||
- 회의 진행 화면으로 정상 이동
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 3. 회의 진행 플로우 (05-회의진행.html)
|
|
||||||
|
|
||||||
**테스트 항목**:
|
|
||||||
- 녹음 타이머 표시
|
|
||||||
- 참석자 목록 (3/5명)
|
|
||||||
- 실시간 회의록 섹션 (참석자, 안건, 논의 내용, 결정 사항, Todo)
|
|
||||||
- 섹션별 아코디언 확장/축소
|
|
||||||
- 회의 종료 확인 모달
|
|
||||||
|
|
||||||
**결과**: ✅ 통과
|
|
||||||
- 모든 섹션 정상 렌더링
|
|
||||||
- 아코디언 인터랙션 정상 작동
|
|
||||||
- 종료 확인 모달 표시 및 검증 화면으로 이동
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 4. 핵심 차별화 기능 #1: 맥락 기반 용어 툴팁
|
|
||||||
|
|
||||||
**테스트 위치**: 05-회의진행.html > 논의 내용 섹션
|
|
||||||
|
|
||||||
**테스트 시나리오**:
|
|
||||||
1. "논의 내용" 섹션 확장
|
|
||||||
2. 하이라이트된 용어 "MVP" 클릭
|
|
||||||
|
|
||||||
**결과**: ✅ 통과
|
|
||||||
|
|
||||||
**표시 내용 확인**:
|
|
||||||
```
|
|
||||||
MVP (Minimum Viable Product)
|
|
||||||
|
|
||||||
📘 정의
|
|
||||||
최소 기능 제품. 핵심 기능만 구현하여 시장 검증을 목적으로 출시하는 제품.
|
|
||||||
|
|
||||||
🏢 이 회의에서의 의미
|
|
||||||
Q1까지 사용자 인증, 대시보드, 회의록 작성 핵심 기능만 구현하여 출시 예정
|
|
||||||
|
|
||||||
📂 관련 프로젝트
|
|
||||||
- 2024 고객 포털 프로젝트 (링크)
|
|
||||||
- 2023 모바일 앱 리뉴얼 (링크)
|
|
||||||
|
|
||||||
📄 과거 회의록
|
|
||||||
- 2024-09-15 기획 회의 (2024-09-15) (링크)
|
|
||||||
- 2024-08-20 킥오프 회의 (2024-08-20) (링크)
|
|
||||||
|
|
||||||
[자세히 보기] 버튼
|
|
||||||
```
|
|
||||||
|
|
||||||
**차별화 포인트**:
|
|
||||||
- ✅ 단순 사전 정의가 아닌 **현재 회의 맥락에서의 의미** 제공
|
|
||||||
- ✅ 관련 프로젝트 링크 제공으로 **업무 연속성** 지원
|
|
||||||
- ✅ 과거 회의록 링크로 **지식 누적** 지원
|
|
||||||
- ✅ 용어별 맞춤 설명으로 **학습 곡선 감소**
|
|
||||||
|
|
||||||
**기타 확인된 용어**: Q1, React, AWS, Sprint 모두 동일한 구조로 툴팁 제공
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 5. 검증 및 종료 플로우 (06→07→08)
|
|
||||||
|
|
||||||
#### 5.1 검증 완료 (06-검증완료.html)
|
|
||||||
|
|
||||||
**테스트 시나리오**:
|
|
||||||
- 전체 진행률 확인: 60% (3/5 섹션)
|
|
||||||
- "안건" 섹션 검증 완료 버튼 클릭
|
|
||||||
- 다음 단계 버튼 클릭
|
|
||||||
|
|
||||||
**결과**: ✅ 통과
|
|
||||||
- 진행률 실시간 업데이트: 60% → 80% (4/5)
|
|
||||||
- 검증 완료 시 토스트 메시지: "다른 참석자에게 알림이 전송되었습니다"
|
|
||||||
- 검증자 정보 및 시간 자동 기록
|
|
||||||
- 미검증 섹션 있어도 다음 단계 진행 가능
|
|
||||||
|
|
||||||
#### 5.2 회의 종료 (07-회의종료.html)
|
|
||||||
|
|
||||||
**테스트 항목**:
|
|
||||||
- 회의 통계 표시
|
|
||||||
- ⏱️ 총 시간: 45분
|
|
||||||
- 👥 참석자: 3명
|
|
||||||
- 💬 발언 횟수 (막대 그래프)
|
|
||||||
- 🔑 주요 키워드: #MVP #React #AWS #Sprint #Q1
|
|
||||||
- AI Todo 자동 추출 (3개)
|
|
||||||
- 필수 항목 확인 체크리스트
|
|
||||||
- 최종 회의록 확정 버튼
|
|
||||||
|
|
||||||
**결과**: ✅ 통과
|
|
||||||
- 모든 통계 정상 표시
|
|
||||||
- AI Todo 3개 자동 추출:
|
|
||||||
1. 요구사항 정의서 작성 (@김민준, ~10/25)
|
|
||||||
2. 기술 스택 상세 검토 (@박서연, ~10/27)
|
|
||||||
3. 인프라 설계 문서 작성 (@이준호, ~10/30)
|
|
||||||
- 확정 버튼 클릭 시 공유 화면으로 이동
|
|
||||||
|
|
||||||
#### 5.3 회의록 공유 (08-회의록공유.html)
|
|
||||||
|
|
||||||
**테스트 시나리오**:
|
|
||||||
- 공유 대상: 참석자 전체 (기본값)
|
|
||||||
- 공유 권한: 읽기 전용 (기본값)
|
|
||||||
- 공유 방식: 이메일 발송 + 링크 복사 (둘 다 선택)
|
|
||||||
- 회의록 공유 버튼 클릭
|
|
||||||
|
|
||||||
**결과**: ✅ 통과
|
|
||||||
- 모든 옵션 정상 표시 및 선택 가능
|
|
||||||
- 공유 완료 모달 표시
|
|
||||||
- ✅ 공유 완료!
|
|
||||||
- 공유 링크: https://meeting.company.com/share/abc123xyz
|
|
||||||
- 📋 링크 복사 버튼
|
|
||||||
- 대시보드로 이동 / 회의록 보기 버튼
|
|
||||||
- 대시보드 이동 시 새로 추가된 회의 확인 (총 6건)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 6. 핵심 차별화 기능 #2: Todo-회의록 실시간 연동
|
|
||||||
|
|
||||||
**테스트 위치**: 09-Todo관리.html
|
|
||||||
|
|
||||||
**테스트 시나리오**:
|
|
||||||
1. 대시보드에서 하단 네비게이션 "Todo" 클릭
|
|
||||||
2. Todo 목록 확인: 진행 중 3건
|
|
||||||
3. "DB 스키마 수정" Todo 체크박스 클릭
|
|
||||||
4. 확인 모달 확인 및 "완료 처리" 클릭
|
|
||||||
|
|
||||||
**결과**: ✅ 통과
|
|
||||||
|
|
||||||
**확인 모달 내용**:
|
|
||||||
```
|
|
||||||
Todo 완료 처리
|
|
||||||
|
|
||||||
이 Todo를 완료 처리하시겠습니까?
|
|
||||||
완료 시 관련 회의록에 자동으로 반영됩니다.
|
|
||||||
|
|
||||||
💡 차별화 기능
|
|
||||||
회의록의 Todo 섹션에 완료 상태가 자동으로 업데이트되고,
|
|
||||||
참석자들에게 알림이 전송됩니다.
|
|
||||||
|
|
||||||
[취소] [완료 처리]
|
|
||||||
```
|
|
||||||
|
|
||||||
**완료 후 결과**:
|
|
||||||
- ✅ Todo 목록에서 해당 항목 제거됨 (3건 → 2건)
|
|
||||||
- ✅ 토스트 메시지 표시: **"회의록에 완료 상태가 반영되었습니다"**
|
|
||||||
|
|
||||||
**차별화 포인트**:
|
|
||||||
- ✅ Todo 완료 시 **관련 회의록 자동 업데이트**
|
|
||||||
- ✅ **양방향 연동**: Todo ↔ 회의록
|
|
||||||
- ✅ **실시간 알림** 기능으로 팀 협업 효율성 증대
|
|
||||||
- ✅ **작업 진행 상황 추적** 용이
|
|
||||||
|
|
||||||
**기타 확인사항**:
|
|
||||||
- 각 Todo 카드에 회의록 링크 표시: "📝 주간 회의 (10/19)"
|
|
||||||
- 담당자, 마감일, 우선순위 정보 표시
|
|
||||||
- 필터 및 정렬 기능 정상 작동
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 7. 반응형 디자인 검증
|
|
||||||
|
|
||||||
#### 7.1 Mobile (375px × 667px)
|
|
||||||
|
|
||||||
**테스트 화면**: 09-Todo관리.html
|
|
||||||
|
|
||||||
**결과**: ✅ 통과
|
|
||||||
- 레이아웃이 단일 컬럼으로 조정됨
|
|
||||||
- 터치 타겟 크기 44×44px 이상 확보
|
|
||||||
- 하단 네비게이션 바 고정 표시
|
|
||||||
- 텍스트 가독성 유지
|
|
||||||
- 스크롤 정상 작동
|
|
||||||
|
|
||||||
#### 7.2 Tablet (768px × 1024px)
|
|
||||||
|
|
||||||
**테스트 화면**: 09-Todo관리.html
|
|
||||||
|
|
||||||
**결과**: ✅ 통과
|
|
||||||
- 중간 크기 레이아웃 적용
|
|
||||||
- Todo 카드 너비 적절히 조정
|
|
||||||
- 필터 및 정렬 컨트롤 정상 배치
|
|
||||||
- 여백 및 간격 적절히 조정
|
|
||||||
|
|
||||||
#### 7.3 Desktop (1440px × 900px)
|
|
||||||
|
|
||||||
**테스트 화면**: 09-Todo관리.html
|
|
||||||
|
|
||||||
**결과**: ✅ 통과
|
|
||||||
- 최대 너비 제한 적용 (가독성 확보)
|
|
||||||
- 멀티 컬럼 레이아웃 (필요시)
|
|
||||||
- 충분한 여백으로 시각적 편안함 제공
|
|
||||||
- 모든 요소 적절한 크기와 간격 유지
|
|
||||||
|
|
||||||
**CSS 브레이크포인트 확인**:
|
|
||||||
```css
|
|
||||||
/* Mobile First */
|
|
||||||
기본 스타일: 320px~
|
|
||||||
|
|
||||||
/* Tablet */
|
|
||||||
@media (min-width: 768px) { ... }
|
|
||||||
|
|
||||||
/* Desktop */
|
|
||||||
@media (min-width: 1024px) { ... }
|
|
||||||
|
|
||||||
/* Large Desktop */
|
|
||||||
@media (min-width: 1440px) { ... }
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 접근성 (WCAG 2.1 Level AA) 확인
|
|
||||||
|
|
||||||
### 1. 색상 대비
|
|
||||||
- ✅ 모든 텍스트 색상 대비 4.5:1 이상
|
|
||||||
- ✅ Primary 색상 (#00C896)과 배경 대비 충분
|
|
||||||
- ✅ 중요 정보에 색상 외 추가 표시 (아이콘, 텍스트)
|
|
||||||
|
|
||||||
### 2. 키보드 접근성
|
|
||||||
- ✅ Tab 키로 모든 인터랙티브 요소 접근 가능
|
|
||||||
- ✅ Enter/Space로 버튼 및 체크박스 조작 가능
|
|
||||||
- ✅ Esc 키로 모달 닫기 가능
|
|
||||||
- ✅ :focus-visible 스타일로 포커스 상태 명확히 표시
|
|
||||||
|
|
||||||
### 3. 스크린 리더 지원
|
|
||||||
- ✅ 모든 이미지에 alt 속성 제공
|
|
||||||
- ✅ ARIA 레이블 적용 (aria-label, aria-labelledby)
|
|
||||||
- ✅ 시맨틱 HTML 사용 (header, main, nav, article, section)
|
|
||||||
- ✅ "본문으로 건너뛰기" 링크 제공
|
|
||||||
|
|
||||||
### 4. 터치 타겟
|
|
||||||
- ✅ 모든 버튼 및 인터랙티브 요소 최소 44×44px
|
|
||||||
- ✅ 충분한 간격으로 오터치 방지
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 성능 확인
|
|
||||||
|
|
||||||
### 1. 로딩 시간
|
|
||||||
- ✅ 페이지 전환 3초 이내 (시뮬레이션)
|
|
||||||
- ✅ Fade 애니메이션 150ms로 부드러운 전환
|
|
||||||
|
|
||||||
### 2. 인터랙션 반응성
|
|
||||||
- ✅ 버튼 클릭 즉시 피드백 (로딩, 토스트 메시지)
|
|
||||||
- ✅ Debounce 적용으로 불필요한 검색 요청 방지 (300ms)
|
|
||||||
- ✅ 모달 열기/닫기 부드러운 애니메이션
|
|
||||||
|
|
||||||
### 3. 메모리 관리
|
|
||||||
- ✅ LocalStorage 활용으로 세션 간 데이터 유지
|
|
||||||
- ✅ Mock 데이터로 서버 요청 최소화
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 발견된 이슈 및 개선사항
|
|
||||||
|
|
||||||
### 이슈
|
|
||||||
없음 - 모든 테스트 통과
|
|
||||||
|
|
||||||
### 개선 제안
|
|
||||||
1. 실제 백엔드 API 연동 시 에러 처리 강화 필요
|
|
||||||
2. WebSocket 연결 실패 시 fallback 로직 추가 권장
|
|
||||||
3. STT 음성 인식 기능 실제 구현 시 정확도 테스트 필요
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 결론
|
|
||||||
|
|
||||||
### 전체 평가
|
|
||||||
✅ **프로토타입 개발 및 테스트 성공적으로 완료**
|
|
||||||
|
|
||||||
### 구현 완료 사항
|
|
||||||
1. ✅ 9개 화면 완전 구현
|
|
||||||
2. ✅ 핵심 차별화 기능 2개 정상 작동
|
|
||||||
- 맥락 기반 용어 설명 툴팁
|
|
||||||
- Todo-회의록 실시간 연동
|
|
||||||
3. ✅ Mobile First 반응형 디자인
|
|
||||||
4. ✅ WCAG 2.1 Level AA 접근성 준수
|
|
||||||
5. ✅ 일관된 디자인 시스템 적용
|
|
||||||
6. ✅ 실제 동작하는 인터랙션 구현
|
|
||||||
|
|
||||||
### 다음 단계
|
|
||||||
1. 백엔드 API 개발 및 연동
|
|
||||||
2. 실제 STT/AI 기능 통합
|
|
||||||
3. WebSocket 실시간 협업 구현
|
|
||||||
4. 사용자 인수 테스트 (UAT)
|
|
||||||
5. 성능 최적화 및 보안 강화
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**테스트 수행자**: Claude (AI Assistant)
|
|
||||||
**테스트 완료일**: 2025-10-20
|
|
||||||
**프로토타입 버전**: 1.0.0
|
|
||||||
File diff suppressed because it is too large
Load Diff
1100
design-last/uiux_bk/prototype/common.js
vendored
1100
design-last/uiux_bk/prototype/common.js
vendored
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user