프로토타입 회의록 대시보드 수정 및 UI/UX 설계 문서 적용

This commit is contained in:
hjmoons 2025-10-21 15:20:36 +09:00
parent 75e7146877
commit a68340735b
2 changed files with 649 additions and 20 deletions

View File

@ -289,6 +289,10 @@
transform: translateY(-2px);
}
.meeting-card.ongoing {
border-left: 4px solid var(--color-error-main);
}
.meeting-header {
display: flex;
align-items: flex-start;
@ -296,6 +300,79 @@
margin-bottom: var(--spacing-3);
}
.meeting-badges {
display: flex;
align-items: center;
gap: var(--spacing-2);
}
.badge-ongoing {
background-color: var(--color-error-main);
color: var(--color-white);
animation: pulse 1.5s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.6; }
}
.role-indicator {
display: inline-flex;
align-items: center;
gap: var(--spacing-1);
font-size: var(--font-size-caption);
color: var(--color-gray-600);
}
.crown-icon {
color: var(--color-warning-main);
}
.meeting-actions {
display: flex;
gap: var(--spacing-2);
margin-top: var(--spacing-3);
}
.btn-join {
flex: 1;
padding: var(--spacing-2) var(--spacing-4);
background-color: var(--color-primary-main);
color: var(--color-white);
border: none;
border-radius: var(--radius-md);
font-weight: var(--font-weight-medium);
cursor: pointer;
transition: all var(--transition-fast);
}
.btn-join:hover {
background-color: var(--color-primary-dark);
transform: translateY(-1px);
}
.btn-edit {
padding: var(--spacing-2) var(--spacing-3);
background-color: var(--color-white);
color: var(--color-primary-main);
border: 1px solid var(--color-primary-main);
border-radius: var(--radius-md);
font-size: var(--font-size-body-small);
cursor: pointer;
transition: all var(--transition-fast);
}
.btn-edit:hover {
background-color: rgba(0, 217, 177, 0.1);
}
.timer-text {
font-size: var(--font-size-caption);
color: var(--color-gray-500);
margin-top: var(--spacing-2);
}
.meeting-title {
font-size: var(--font-size-h4);
font-weight: var(--font-weight-semibold);
@ -364,6 +441,151 @@
.dday.warning { color: var(--color-warning-main); }
.dday.normal { color: var(--color-gray-500); }
/* Quick Actions */
.quick-actions {
display: flex;
gap: var(--spacing-4);
margin-bottom: var(--spacing-8);
}
.quick-action-btn {
flex: 1;
padding: var(--spacing-6);
background: linear-gradient(135deg, var(--color-primary-light) 0%, var(--color-primary-main) 100%);
color: var(--color-white);
border: none;
border-radius: var(--radius-lg);
cursor: pointer;
transition: all var(--transition-base);
text-align: center;
box-shadow: var(--shadow-sm);
}
.quick-action-btn:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-md);
}
.quick-action-btn.secondary {
background: var(--color-white);
color: var(--color-primary-main);
border: 2px solid var(--color-primary-main);
}
.quick-action-btn.secondary:hover {
background-color: rgba(0, 217, 177, 0.05);
}
.quick-action-icon {
font-size: 32px;
margin-bottom: var(--spacing-2);
}
.quick-action-title {
font-size: var(--font-size-h4);
font-weight: var(--font-weight-semibold);
margin-bottom: var(--spacing-1);
}
.quick-action-desc {
font-size: var(--font-size-body-small);
opacity: 0.9;
}
/* FAB Action Modal */
.fab-modal {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
z-index: var(--z-modal-backdrop);
align-items: flex-end;
justify-content: center;
padding: var(--spacing-4);
}
.fab-modal.show {
display: flex;
}
.fab-menu {
background-color: var(--color-white);
border-radius: var(--radius-xl) var(--radius-xl) 0 0;
padding: var(--spacing-6);
width: 100%;
max-width: 600px;
animation: slideUp var(--transition-base);
}
@keyframes slideUp {
from {
transform: translateY(100%);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
.fab-menu-title {
font-size: var(--font-size-h3);
font-weight: var(--font-weight-semibold);
color: var(--color-gray-900);
margin-bottom: var(--spacing-6);
text-align: center;
}
.fab-menu-item {
display: flex;
align-items: center;
gap: var(--spacing-4);
padding: var(--spacing-5);
background-color: var(--color-white);
border: 2px solid var(--color-gray-200);
border-radius: var(--radius-lg);
margin-bottom: var(--spacing-3);
cursor: pointer;
transition: all var(--transition-fast);
}
.fab-menu-item:hover {
border-color: var(--color-primary-main);
background-color: rgba(0, 217, 177, 0.05);
transform: translateX(4px);
}
.fab-menu-icon {
width: 56px;
height: 56px;
background-color: var(--color-primary-light);
border-radius: var(--radius-md);
display: flex;
align-items: center;
justify-content: center;
font-size: 28px;
flex-shrink: 0;
}
.fab-menu-content {
flex: 1;
}
.fab-menu-item-title {
font-size: var(--font-size-h4);
font-weight: var(--font-weight-semibold);
color: var(--color-gray-900);
margin-bottom: var(--spacing-1);
}
.fab-menu-item-desc {
font-size: var(--font-size-body-small);
color: var(--color-gray-600);
}
/* Bottom Navigation (Mobile) */
.bottom-nav {
position: fixed;
@ -479,6 +701,20 @@
<p class="welcome-subtitle" id="welcomeSubtitle">오늘의 일정을 확인하세요</p>
</section>
<!-- Quick Actions -->
<section class="quick-actions hide-mobile">
<button class="quick-action-btn" onclick="navigateTo('04-템플릿선택.html')">
<div class="quick-action-icon">🚀</div>
<div class="quick-action-title">새 회의 시작</div>
<div class="quick-action-desc">템플릿을 선택하여 회의를 바로 시작합니다</div>
</button>
<button class="quick-action-btn secondary" onclick="navigateTo('03-회의예약.html')">
<div class="quick-action-icon">📅</div>
<div class="quick-action-title">회의 예약</div>
<div class="quick-action-desc">향후 진행할 회의를 미리 예약합니다</div>
</button>
</section>
<!-- Stats Grid -->
<section class="stats-grid">
<div class="stat-card">
@ -498,14 +734,25 @@
</div>
</section>
<!-- Recent Meetings -->
<!-- 예정된/진행중 회의 -->
<section class="section">
<div class="section-header">
<h2 class="section-title">최근 회의</h2>
<h2 class="section-title">예정된 회의</h2>
<a href="12-회의록목록조회.html" class="view-all-link">전체 보기 →</a>
</div>
<div class="meeting-grid" id="meetingGrid">
<!-- Meetings will be rendered here -->
<div class="meeting-grid" id="upcomingMeetingGrid">
<!-- Upcoming/Ongoing meetings will be rendered here -->
</div>
</section>
<!-- 내 회의록 -->
<section class="section">
<div class="section-header">
<h2 class="section-title">내 회의록</h2>
<a href="12-회의록목록조회.html" class="view-all-link">전체 보기 →</a>
</div>
<div class="meeting-grid" id="myMeetingGrid">
<!-- My meetings will be rendered here -->
</div>
</section>
@ -519,6 +766,17 @@
<!-- Todos will be rendered here -->
</div>
</section>
<!-- Shared Meetings -->
<section class="section">
<div class="section-header">
<h2 class="section-title">공유받은 회의록</h2>
<a href="12-회의록목록조회.html" class="view-all-link">전체 보기 →</a>
</div>
<div class="meeting-grid" id="sharedMeetingGrid">
<!-- Shared meetings will be rendered here -->
</div>
</section>
</main>
</div>
@ -543,13 +801,198 @@
</nav>
<!-- FAB -->
<button class="fab" id="fabButton" title="새 회의 예약">+</button>
<button class="fab" id="fabButton" title="새 회의 시작">+</button>
<!-- FAB Action Modal -->
<div class="fab-modal" id="fabModal">
<div class="fab-menu">
<h3 class="fab-menu-title">무엇을 하시겠습니까?</h3>
<div class="fab-menu-item" onclick="navigateTo('04-템플릿선택.html')">
<div class="fab-menu-icon">🚀</div>
<div class="fab-menu-content">
<div class="fab-menu-item-title">새 회의 시작</div>
<div class="fab-menu-item-desc">템플릿을 선택하여 회의를 바로 시작합니다</div>
</div>
</div>
<div class="fab-menu-item" onclick="navigateTo('03-회의예약.html')">
<div class="fab-menu-icon">📅</div>
<div class="fab-menu-content">
<div class="fab-menu-item-title">회의 예약</div>
<div class="fab-menu-item-desc">향후 진행할 회의를 미리 예약합니다</div>
</div>
</div>
</div>
</div>
<!-- JavaScript -->
<script src="common.js"></script>
<script>
const { AppState, Storage, Toast, MeetingUtils, formatDateTime, getDday, navigateTo } = window.MeetingApp;
// 샘플 데이터 초기화
function initSampleData() {
// 기존 데이터가 없으면 샘플 데이터 생성
const currentUserId = 'user-001'; // 현재 사용자 ID (예시)
if (!Storage.get('meetings') || Storage.get('meetings').length === 0) {
const sampleMeetings = [
{
id: 'meeting-001',
title: '긴급 버그 픽스 회의',
date: new Date(Date.now() - 1800000).toISOString(), // 30분 전 시작
startTime: new Date(Date.now() - 1800000).toISOString(),
location: '온라인 (Zoom)',
status: 'ongoing',
description: '프로덕션 환경 긴급 버그 대응',
creatorId: 'user-002',
attendees: ['user-001', 'user-002', 'user-003']
},
{
id: 'meeting-002',
title: '주간 스프린트 회의',
date: new Date(Date.now() + 86400000).toISOString(), // 내일
startTime: new Date(Date.now() + 86400000).toISOString(),
location: '온라인 (Zoom)',
status: 'scheduled',
description: '이번 주 스프린트 진행사항 및 다음 주 계획 논의',
creatorId: 'user-001', // 현재 사용자가 생성자
attendees: ['user-001', 'user-002', 'user-003', 'user-004']
},
{
id: 'meeting-003',
title: 'Q4 마케팅 전략 회의',
date: new Date(Date.now() - 172800000).toISOString(), // 2일 전
startTime: new Date(Date.now() - 172800000).toISOString(),
location: '본사 3층 회의실',
status: 'completed',
description: '4분기 마케팅 캠페인 기획 및 예산 검토',
creatorId: 'user-005',
attendees: ['user-001', 'user-005']
},
{
id: 'meeting-004',
title: '디자인 리뷰',
date: new Date(Date.now() + 3600000).toISOString(), // 1시간 후
startTime: new Date(Date.now() + 3600000).toISOString(),
location: '온라인 (Google Meet)',
status: 'scheduled',
description: 'UI/UX 디자인 최종 검토 및 피드백',
creatorId: 'user-003',
attendees: ['user-001', 'user-003', 'user-006']
},
{
id: 'meeting-005',
title: '월간 전체 회의',
date: new Date(Date.now() + 259200000).toISOString(), // 3일 후
startTime: new Date(Date.now() + 259200000).toISOString(),
location: '대강당',
status: 'scheduled',
description: '월간 성과 공유 및 다음 달 목표 설정',
creatorId: 'user-001', // 현재 사용자가 생성자
attendees: ['user-001', 'user-002', 'user-003', 'user-004', 'user-005']
},
{
id: 'meeting-006',
title: '고객 피드백 리뷰',
date: new Date(Date.now() - 345600000).toISOString(), // 4일 전
startTime: new Date(Date.now() - 345600000).toISOString(),
location: '온라인 (Teams)',
status: 'completed',
description: '최근 수집된 고객 VOC 분석 및 개선 방안 도출',
creatorId: 'user-007',
attendees: ['user-001', 'user-007']
}
];
Storage.set('meetings', sampleMeetings);
Storage.set('currentUserId', currentUserId);
}
if (!Storage.get('todos') || Storage.get('todos').length === 0) {
const sampleTodos = [
{
id: 'todo-001',
title: '마케팅 자료 최종 검토 및 승인',
assignee: '김민준',
dueDate: new Date(Date.now() + 86400000).toISOString(), // 내일
priority: 'high',
status: 'in_progress',
meetingId: 'meeting-002'
},
{
id: 'todo-002',
title: '개발 환경 설정 가이드 문서 작성',
assignee: '이준호',
dueDate: new Date(Date.now() + 172800000).toISOString(), // 2일 후
priority: 'medium',
status: 'in_progress',
meetingId: 'meeting-003'
},
{
id: 'todo-003',
title: '고객 설문조사 결과 분석 및 보고서 작성',
assignee: '박서연',
dueDate: new Date(Date.now() + 432000000).toISOString(), // 5일 후
priority: 'high',
status: 'pending',
meetingId: 'meeting-005'
},
{
id: 'todo-004',
title: 'UI/UX 디자인 개선안 3차 리뷰',
assignee: '최유진',
dueDate: new Date(Date.now() - 86400000).toISOString(), // 어제 (지연)
priority: 'high',
status: 'in_progress',
meetingId: 'meeting-003'
},
{
id: 'todo-005',
title: '다음 스프린트 백로그 정리',
assignee: '정도현',
dueDate: new Date(Date.now() + 259200000).toISOString(), // 3일 후
priority: 'medium',
status: 'pending',
meetingId: 'meeting-001'
}
];
Storage.set('todos', sampleTodos);
}
// 공유받은 회의록 샘플 데이터
if (!Storage.get('sharedMeetings') || Storage.get('sharedMeetings').length === 0) {
const sharedMeetings = [
{
id: 'meeting-s001',
title: 'AI 기술 도입 검토 회의',
date: new Date(Date.now() - 259200000).toISOString(), // 3일 전
location: '본사 2층 회의실',
status: 'completed',
description: 'LLM 기반 서비스 개선 방안 논의',
sharedBy: '홍길동'
},
{
id: 'meeting-s002',
title: '보안 정책 업데이트',
date: new Date(Date.now() - 432000000).toISOString(), // 5일 전
location: '온라인 (Zoom)',
status: 'completed',
description: '최신 보안 가이드라인 공유 및 적용 계획',
sharedBy: '송주영'
},
{
id: 'meeting-s003',
title: '팀 빌딩 워크샵',
date: new Date(Date.now() - 604800000).toISOString(), // 7일 전
location: '제주 연수원',
status: 'completed',
description: '팀 협업 강화 및 커뮤니케이션 개선 활동',
sharedBy: '백현정'
}
];
Storage.set('sharedMeetings', sharedMeetings);
}
}
// 인증 체크
MeetingApp.ready(() => {
const authToken = Storage.get('authToken');
@ -569,10 +1012,20 @@
AppState.currentUser = currentUser;
}
// 샘플 데이터 초기화
initSampleData();
// 데이터 로드 및 렌더링
loadDashboardData();
renderMeetings();
renderUpcomingMeetings();
renderMyMeetings();
renderTodos();
renderSharedMeetings();
// 타이머 업데이트 (1분마다)
setInterval(() => {
renderUpcomingMeetings();
}, 60000);
});
// 대시보드 통계 로드
@ -594,27 +1047,182 @@
document.getElementById('todoCompletionRate').textContent = `${completionRate}%`;
}
// 회의 목록 렌더링
function renderMeetings() {
const meetings = Storage.get('meetings', []).slice(0, 3);
const meetingGrid = document.getElementById('meetingGrid');
// 유틸리티 함수: 시간까지 남은 시간 계산
function getTimeUntilMeeting(startTime) {
const now = new Date();
const start = new Date(startTime);
const diff = start - now;
return diff;
}
if (meetings.length === 0) {
meetingGrid.innerHTML = '<p style="color: var(--color-gray-500);">아직 등록된 회의가 없습니다.</p>';
// 유틸리티 함수: 타이머 텍스트 포맷팅
function formatTimerText(milliseconds) {
if (milliseconds < 0) return '시작됨';
const hours = Math.floor(milliseconds / 3600000);
const minutes = Math.floor((milliseconds % 3600000) / 60000);
if (hours > 24) {
const days = Math.floor(hours / 24);
return `D-${days}`;
} else if (hours > 0) {
return `${hours}시간 후`;
} else if (minutes > 10) {
return `${minutes}분 후`;
} else if (minutes > 0) {
return `곧 시작`;
} else {
return '시작 가능';
}
}
// 예정된/진행중 회의 렌더링
function renderUpcomingMeetings() {
const meetings = Storage.get('meetings', []);
const currentUserId = Storage.get('currentUserId', 'user-001');
const upcomingMeetingGrid = document.getElementById('upcomingMeetingGrid');
// 진행중 및 예정된 회의 필터링
const now = new Date();
const upcomingMeetings = meetings.filter(m => {
if (m.status === 'ongoing') return true;
if (m.status === 'scheduled') {
const meetingDate = new Date(m.startTime || m.date);
return meetingDate >= now;
}
return false;
});
// 정렬: 진행중 회의 우선, 그 다음 가까운 회의순
upcomingMeetings.sort((a, b) => {
if (a.status === 'ongoing' && b.status !== 'ongoing') return -1;
if (a.status !== 'ongoing' && b.status === 'ongoing') return 1;
return new Date(a.startTime || a.date) - new Date(b.startTime || b.date);
});
// 최대 3개만 표시
const displayMeetings = upcomingMeetings.slice(0, 3);
if (displayMeetings.length === 0) {
upcomingMeetingGrid.innerHTML = '<p style="color: var(--color-gray-500);">예정된 회의가 없습니다.</p>';
return;
}
meetingGrid.innerHTML = meetings.map(meeting => `
<div class="meeting-card" onclick="navigateTo('05-회의진행.html')">
upcomingMeetingGrid.innerHTML = displayMeetings.map(meeting => {
const isCreator = meeting.creatorId === currentUserId;
const isOngoing = meeting.status === 'ongoing';
const timeUntil = isOngoing ? 0 : getTimeUntilMeeting(meeting.startTime || meeting.date);
const canJoin = isOngoing || (timeUntil > 0 && timeUntil <= 600000); // 10분 이내
const timerText = isOngoing ? '진행중' : formatTimerText(timeUntil);
let actionButtons = '';
if (isOngoing) {
// 진행중 회의: 모든 참석자에게 참여하기 버튼
actionButtons = `
<div class="meeting-actions">
<button class="btn-join" onclick="navigateTo('05-회의진행.html')">참여하기</button>
</div>
`;
} else if (isCreator) {
// 생성자: 수정 버튼
actionButtons = `
<div class="meeting-actions">
<button class="btn-edit" onclick="navigateTo('03-회의예약.html')">수정</button>
</div>
`;
} else if (canJoin) {
// 참석자: 10분 이내면 참여하기 버튼
actionButtons = `
<div class="meeting-actions">
<button class="btn-join" onclick="navigateTo('05-회의진행.html')">참여하기</button>
</div>
`;
} else {
// 참석자: 10분 이상 남음
actionButtons = `
<div class="timer-text">${timerText} 참여 가능</div>
`;
}
const roleIndicator = isCreator && !isOngoing ?
'<span class="role-indicator"><span class="crown-icon">👑</span> 생성자</span>' : '';
return `
<div class="meeting-card ${isOngoing ? 'ongoing' : ''}" onclick="event.stopPropagation(); navigateTo('10-회의록대시보드.html')">
<div class="meeting-header">
<div style="flex: 1;">
<div class="meeting-badges">
${isOngoing ? '<span class="badge badge-ongoing">진행중</span>' : `<span class="badge badge-neutral">${timerText}</span>`}
${roleIndicator}
</div>
<div class="meeting-title">${meeting.title}</div>
<div class="meeting-meta">📅 ${formatDateTime(meeting.date)}</div>
<div class="meeting-meta">📍 ${meeting.location}</div>
<div class="meeting-meta">👥 ${meeting.attendees ? meeting.attendees.length : 0}명</div>
</div>
</div>
<div style="font-size: var(--font-size-body-small); color: var(--color-gray-600); margin-bottom: var(--spacing-2);">
${meeting.description}
</div>
${actionButtons}
</div>
`;
}).join('');
}
// 내 회의록 렌더링
function renderMyMeetings() {
const meetings = Storage.get('meetings', []);
const myMeetingGrid = document.getElementById('myMeetingGrid');
// 완료된 회의만 필터링
const completedMeetings = meetings
.filter(m => m.status === 'completed')
.sort((a, b) => new Date(b.date) - new Date(a.date))
.slice(0, 3);
if (completedMeetings.length === 0) {
myMeetingGrid.innerHTML = '<p style="color: var(--color-gray-500);">작성한 회의록이 없습니다. 첫 회의를 시작해보세요!</p>';
return;
}
myMeetingGrid.innerHTML = completedMeetings.map(meeting => `
<div class="meeting-card" onclick="navigateTo('10-회의록상세조회.html')">
<div class="meeting-header">
<div>
<div class="meeting-title">${meeting.title}</div>
<div class="meeting-meta">📅 ${formatDateTime(meeting.date)}</div>
<div class="meeting-meta">📍 ${meeting.location}</div>
<div class="meeting-meta">👥 ${meeting.attendees ? meeting.attendees.length : 0}명</div>
</div>
<span class="badge ${MeetingUtils.getStatusClass(meeting.status)}">
${MeetingUtils.getStatusLabel(meeting.status)}
</span>
<span class="badge badge-success">확정완료</span>
</div>
<div style="font-size: var(--font-size-body-small); color: var(--color-gray-600);">
${meeting.description}
</div>
</div>
`).join('');
}
// 공유받은 회의록 렌더링
function renderSharedMeetings() {
const sharedMeetings = Storage.get('sharedMeetings', []);
const sharedMeetingGrid = document.getElementById('sharedMeetingGrid');
if (sharedMeetings.length === 0) {
sharedMeetingGrid.innerHTML = '<p style="color: var(--color-gray-500);">공유받은 회의록이 없습니다.</p>';
return;
}
sharedMeetingGrid.innerHTML = sharedMeetings.map(meeting => `
<div class="meeting-card" onclick="navigateTo('10-회의록상세조회.html')">
<div class="meeting-header">
<div>
<div class="meeting-title">${meeting.title}</div>
<div class="meeting-meta">📅 ${formatDateTime(meeting.date)}</div>
<div class="meeting-meta">👤 공유자: ${meeting.sharedBy}</div>
</div>
<span class="badge badge-primary">공유됨</span>
</div>
<div style="font-size: var(--font-size-body-small); color: var(--color-gray-600);">
${meeting.description}
@ -678,9 +1286,30 @@
}, 1000);
});
// FAB 버튼
document.getElementById('fabButton').addEventListener('click', () => {
window.location.href = '03-회의예약.html';
// FAB 버튼 - 모달 토글
const fabButton = document.getElementById('fabButton');
const fabModal = document.getElementById('fabModal');
fabButton.addEventListener('click', (e) => {
e.stopPropagation();
fabModal.classList.add('show');
document.body.style.overflow = 'hidden';
});
// 모달 배경 클릭 시 닫기
fabModal.addEventListener('click', (e) => {
if (e.target === fabModal) {
fabModal.classList.remove('show');
document.body.style.overflow = 'auto';
}
});
// ESC 키로 모달 닫기
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && fabModal.classList.contains('show')) {
fabModal.classList.remove('show');
document.body.style.overflow = 'auto';
}
});
</script>
</body>