Todo 및 회의록 관련 요구사항 재정의 (v2.0.5 / v1.4.7)

[유저스토리 v2.0.5]
- UFR-TODO-040 (09-Todo관리): "Todo수정" → "Todo관리" 기능 확장
  - 통계 블록 재정의: 전체(미완료), 마감임박(3일 이내), 지연(기한 경과)
  - 필터링: 전체, 지연, 마감임박, 완료 (각 필터에 개수 표시)
  - 체크박스 확인 모달: 완료/미완료 전환 시 확인
  - 권한: 담당자 본인 OR 회의록 작성자만 편집 가능

- UFR-MEET-047 (10-회의록상세조회): 탭 순서 및 기본 노출 변경
  - 탭 구성: 대시보드 / 회의록
  - 기본 노출: 대시보드 탭 우선 노출

- UFR-MEET-055 (11-회의록수정): 진입 경로 및 권한 제어 명확화
  - 진입 경로: 10-회의록상세조회 → "수정" 버튼 클릭
  - 권한 제어: 검증완료 전(모든 참석자), 검증완료 후(회의 생성자만)
  - 회의 일시/장소: 읽기 전용 표시 명시

[화면설계서 v1.4.7]
- 09-Todo관리: 통계, 필터, 모달 UI/UX 재정의
- 10-회의록상세조회: 탭 순서 변경, 대시보드 탭 기본 활성
- 11-회의록수정: 진입 경로, 권한 제어, UI 구성 명확화

[프로토타입]
- 09-Todo관리.html: 통계 블록, 필터 개수, 체크박스 확인 모달 구현
- 10-회의록상세조회.html: 탭 순서 및 active 클래스 변경
- 11-회의록수정.html: 권한 코멘트, 읽기 전용 표시 추가

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
yabo0812
2025-10-23 17:20:19 +09:00
parent 40f02d6d8e
commit bd6f2d5c45
6 changed files with 397 additions and 273 deletions
+161 -113
View File
@@ -88,27 +88,57 @@
margin-bottom: var(--space-md);
}
/* 통계 카드 - 모바일에서 컴팩트하게 */
/* 통계 카드 - 정보 표시용 (인터랙션 없음) */
.stat-box {
min-height: 80px;
padding: var(--space-sm);
background: var(--gray-100); /* 플랫한 배경 */
border: 1px solid var(--gray-200); /* 얇은 경계선 */
border-radius: var(--radius-md); /* 부드러운 모서리 */
box-shadow: none; /* 그림자 제거 */
transition: none; /* 호버 효과 제거 */
}
.stat-box.highlight {
background: var(--primary-light);
border: 2px solid var(--primary);
/* 상태별 컬러 코딩 */
.stat-box.stat-incomplete {
background: linear-gradient(135deg, #E3F2FD 0%, #BBDEFB 100%); /* 차분한 블루 그라데이션 */
border-color: #90CAF9;
}
.stat-box.stat-urgent {
background: linear-gradient(135deg, #FFF3E0 0%, #FFE0B2 100%); /* 주의 오렌지 그라데이션 */
border-color: #FFB74D;
}
.stat-box.stat-overdue {
background: linear-gradient(135deg, #FFEBEE 0%, #FFCDD2 100%); /* 긴급 레드 그라데이션 */
border-color: #EF5350;
}
.stat-number {
font-size: var(--font-h2);
font-size: var(--font-h1); /* 더 크게 강조 */
font-weight: var(--font-weight-bold);
color: var(--primary);
margin-bottom: 2px;
line-height: 1.2;
margin-bottom: 4px;
}
.stat-box.stat-incomplete .stat-number {
color: #1976D2; /* 진한 블루 */
}
.stat-box.stat-urgent .stat-number {
color: #F57C00; /* 진한 오렌지 */
}
.stat-box.stat-overdue .stat-number {
color: #D32F2F; /* 진한 레드 */
}
.stat-text {
font-size: var(--font-caption);
color: var(--gray-700);
color: var(--gray-600); /* 더 연하게 */
font-weight: var(--font-weight-regular); /* 보통 굵기 */
opacity: 0.85;
}
/* 원형 진행 바 - 모바일에서 축소 */
@@ -131,17 +161,17 @@
/* 데스크톱에서 통계 카드 크기 복원 */
@media (min-width: 768px) {
.stat-box {
min-height: 100px;
padding: var(--space-md);
min-height: 110px;
padding: var(--space-lg);
}
.stat-number {
font-size: var(--font-h1);
font-size: 2.5rem; /* 더 큰 숫자 */
margin-bottom: var(--space-xs);
}
.stat-text {
font-size: var(--font-small);
font-size: var(--font-body);
}
.circular-progress {
@@ -227,23 +257,26 @@
border-color: var(--primary);
}
/* Todo 카드 */
/* Todo 카드 - 컴팩트 디자인 */
.todo-card {
position: relative;
background: var(--white);
padding: var(--space-md);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-md);
margin-bottom: var(--space-md);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
margin-bottom: var(--space-sm);
transition: all var(--transition-normal);
border: 1px solid var(--gray-200);
}
.todo-card:hover {
box-shadow: var(--shadow-lg);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.12);
border-color: var(--primary);
}
.todo-card.completed {
opacity: 0.6;
opacity: 0.5;
background: var(--gray-50);
}
.todo-card.completed .todo-title {
@@ -251,20 +284,22 @@
color: var(--gray-500);
}
/* 레이아웃: 체크박스 + 콘텐츠 */
.todo-top {
display: flex;
gap: var(--space-md);
margin-bottom: var(--space-md);
align-items: flex-start;
}
.todo-checkbox-wrapper {
flex-shrink: 0;
padding-top: 2px;
}
.todo-checkbox {
width: 24px;
height: 24px;
border: 2px solid var(--gray-300);
border: 2px solid var(--gray-400);
border-radius: 6px;
cursor: pointer;
transition: all var(--transition-fast);
@@ -277,41 +312,41 @@
.todo-content-wrapper {
flex: 1;
min-width: 0; /* 텍스트 오버플로우 방지 */
}
.todo-title {
font-size: var(--font-body);
font-weight: var(--font-weight-medium);
color: var(--gray-900);
margin-bottom: var(--space-sm);
}
/* 배지 영역 */
.todo-badges {
display: flex;
gap: var(--space-sm);
gap: var(--space-xs);
flex-wrap: wrap;
margin-bottom: var(--space-sm);
margin-bottom: var(--space-xs);
}
/* Todo 제목 */
.todo-title {
font-size: var(--font-body);
font-weight: var(--font-weight-regular);
color: var(--gray-900);
margin-bottom: var(--space-xs);
line-height: 1.5;
}
/* 하단 메타 정보 */
.todo-meta-row {
display: flex;
align-items: center;
gap: var(--space-md);
gap: var(--space-sm);
font-size: var(--font-small);
color: var(--gray-500);
}
.todo-assignee {
display: flex;
align-items: center;
gap: var(--space-sm);
flex-wrap: wrap;
}
.todo-meeting-link {
display: flex;
display: inline-flex;
align-items: center;
gap: var(--space-xs);
color: var(--primary);
gap: 4px;
color: #4CAF50; /* 연한 초록색 */
text-decoration: none;
font-size: var(--font-small);
cursor: pointer;
@@ -319,19 +354,47 @@
}
.todo-meeting-link:hover {
color: var(--primary-dark);
color: #388E3C;
text-decoration: underline;
}
.todo-progress-section {
margin-top: var(--space-md);
padding-top: var(--space-md);
border-top: 1px solid var(--gray-300);
/* 담당자 정보 숨김 (간결한 디자인) */
.todo-assignee {
display: none;
}
.todo-progress-label {
font-size: var(--font-small);
color: var(--gray-700);
margin-bottom: var(--space-sm);
/* 액션 버튼 영역 */
.todo-actions {
margin-top: var(--space-xs);
}
/* 아이콘 버튼 */
.icon-btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
padding: 0;
background: none;
border: none;
border-radius: var(--radius-md);
color: var(--gray-600);
cursor: pointer;
transition: all var(--transition-fast);
}
.icon-btn:hover {
background: var(--gray-100);
color: var(--primary);
}
.icon-btn:active {
background: var(--gray-200);
}
.icon-btn .material-icons {
font-size: 20px;
}
/* 빈 상태 */
@@ -463,44 +526,33 @@
<main class="main-content">
<!-- 통계 개요 -->
<div class="stats-overview">
<div class="stat-box">
<div class="stat-box stat-incomplete">
<div class="stat-number" id="totalTodos">0</div>
<div class="stat-text">전체</div>
<div class="stat-text">미완료</div>
</div>
<div class="stat-box highlight">
<div class="circular-progress">
<svg viewBox="0 0 80 80">
<circle class="bg-circle" cx="40" cy="40" r="36"></circle>
<circle class="progress-circle" cx="40" cy="40" r="36"
stroke-dasharray="226" stroke-dashoffset="226" id="progressCircle"></circle>
</svg>
<div class="circular-progress-text" id="completionRate">0%</div>
</div>
<div class="stat-text">완료율</div>
</div>
<div class="stat-box">
<div class="stat-number" id="inProgressTodos">0</div>
<div class="stat-text">진행 중</div>
</div>
<div class="stat-box">
<div class="stat-number text-error" id="urgentTodos">0</div>
<div class="stat-box stat-urgent">
<div class="stat-number" id="urgentTodos">0</div>
<div class="stat-text">마감 임박</div>
</div>
<div class="stat-box stat-overdue">
<div class="stat-number" id="overdueTodos">0</div>
<div class="stat-text">지연</div>
</div>
</div>
<!-- 필터 탭 -->
<div class="filter-tabs">
<button class="filter-tab active" data-filter="all" onclick="filterTodos('all')">
전체
전체 (<span id="filterAllCount">0</span>)
</button>
<button class="filter-tab" data-filter="in_progress" onclick="filterTodos('in_progress')">
진행 중
</button>
<button class="filter-tab" data-filter="completed" onclick="filterTodos('completed')">
완료
<button class="filter-tab" data-filter="overdue" onclick="filterTodos('overdue')">
지연 (<span id="filterOverdueCount">0</span>)
</button>
<button class="filter-tab" data-filter="urgent" onclick="filterTodos('urgent')">
마감 임박
마감 임박 (<span id="filterUrgentCount">0</span>)
</button>
<button class="filter-tab" data-filter="completed" onclick="filterTodos('completed')">
완료 (<span id="filterCompletedCount">0</span>)
</button>
</div>
@@ -623,28 +675,27 @@
// 통계 업데이트
function updateStats() {
const total = allTodos.length;
const total = allTodos.filter(t => t.status !== 'completed').length; // 미완료 전체
const completed = allTodos.filter(t => t.status === 'completed').length;
const inProgress = allTodos.filter(t => t.status === 'in_progress').length;
const urgent = allTodos.filter(t => {
const dday = calculateDday(t.dueDate);
return dday >= 0 && dday <= 3 && t.status !== 'completed';
}).length;
const overdue = allTodos.filter(t => {
const dday = calculateDday(t.dueDate);
return dday < 0 && t.status !== 'completed';
}).length;
// 카운터 애니메이션
// 상단 통계 블록 카운터 애니메이션
animateCounter('totalTodos', total);
animateCounter('inProgressTodos', inProgress);
animateCounter('urgentTodos', urgent);
animateCounter('overdueTodos', overdue);
// 완료율 원형 진행 바
const completionRate = total > 0 ? Math.round((completed / total) * 100) : 0;
const circumference = 226;
const offset = circumference - (completionRate / 100 * circumference);
setTimeout(() => {
$('#progressCircle').style.strokeDashoffset = offset;
$('#completionRate').textContent = `${completionRate}%`;
}, 100);
// 필터 탭 개수 업데이트
$('#filterAllCount').textContent = allTodos.length;
$('#filterOverdueCount').textContent = overdue;
$('#filterUrgentCount').textContent = urgent;
$('#filterCompletedCount').textContent = completed;
}
// Todo 리스트 렌더링
@@ -655,8 +706,11 @@
// 필터 적용
if (currentFilter === 'completed') {
filteredTodos = allTodos.filter(t => t.status === 'completed');
} else if (currentFilter === 'in_progress') {
filteredTodos = allTodos.filter(t => t.status === 'in_progress');
} else if (currentFilter === 'overdue') {
filteredTodos = allTodos.filter(t => {
const dday = calculateDday(t.dueDate);
return dday < 0 && t.status !== 'completed';
});
} else if (currentFilter === 'urgent') {
filteredTodos = allTodos.filter(t => {
const dday = calculateDday(t.dueDate);
@@ -702,7 +756,6 @@
onchange="toggleTodoComplete('${todo.id}', this.checked)">
</div>
<div class="todo-content-wrapper">
<div class="todo-title">${todo.title}</div>
<div class="todo-badges">
${createBadge(statusInfo.badgeText, statusInfo.badgeType)}
${createBadge(
@@ -711,28 +764,17 @@
todo.priority
)}
</div>
<div class="todo-title">${todo.title}</div>
<div class="todo-meta-row">
<div class="todo-assignee">
${createAvatar(todo.assignee, 'sm')}
<span>${todo.assignee.name}</span>
</div>
<span>•</span>
<a class="todo-meeting-link" onclick="goToMeeting('${todo.meetingId}')">
🔗 ${todo.meetingTitle}
</a>
<span>${formatDate(todo.dueDate)}</span>
</div>
<a class="todo-meeting-link" onclick="goToMeeting('${todo.meetingId}')">
🔗 ${todo.meetingTitle}
</a>
${!isCompleted && todo.progress > 0 ? `
<div class="todo-progress-section">
<div class="todo-progress-label">진행률: ${todo.progress}%</div>
${createProgressBar(todo.progress)}
</div>
` : ''}
${!isCompleted ? `
<div class="todo-actions">
<button class="btn btn-ghost btn-sm" onclick="editTodo('${todo.id}')">
<span class="material-icons" style="font-size: 16px;">edit</span>
편집
<button class="icon-btn" onclick="editTodo('${todo.id}')" title="편집">
<span class="material-icons">edit</span>
</button>
</div>
` : ''}
@@ -760,11 +802,11 @@
// Todo 완료 토글
function toggleTodoComplete(todoId, isChecked) {
if (isChecked) {
if (confirm('이 Todo를 완료 처리하시겠습니까?')) {
// 완료 처리
if (confirm('완료 처리하시겠습니까?')) {
const todo = allTodos.find(t => t.id === todoId);
if (todo) {
todo.status = 'completed';
todo.progress = 100;
showToast('Todo가 완료되었습니다', 'success');
updateStats();
renderTodoList();
@@ -773,11 +815,17 @@
event.target.checked = false;
}
} else {
const todo = allTodos.find(t => t.id === todoId);
if (todo) {
todo.status = 'in_progress';
updateStats();
renderTodoList();
// 미완료로 되돌리기
if (confirm('미완료로 변경하시겠습니까?')) {
const todo = allTodos.find(t => t.id === todoId);
if (todo) {
todo.status = 'incomplete';
showToast('미완료로 변경되었습니다', 'info');
updateStats();
renderTodoList();
}
} else {
event.target.checked = true;
}
}
}
@@ -591,8 +591,8 @@
<!-- 탭 네비게이션 -->
<nav class="tab-nav">
<div class="tabs">
<div class="tab active" data-tab="minutes">회의록</div>
<div class="tab" data-tab="dashboard">대시보드</div>
<div class="tab active" data-tab="dashboard">대시보드</div>
<div class="tab" data-tab="minutes">회의록</div>
</div>
</nav>
@@ -642,7 +642,7 @@
</div>
<!-- 회의록 탭 -->
<div id="minutes-content" class="tab-content active">
<div id="minutes-content" class="tab-content">
<!-- 안건 1 -->
<div class="section">
<div class="section-header">
@@ -826,7 +826,7 @@
</div>
<!-- 대시보드 탭 -->
<div id="dashboard-content" class="tab-content">
<div id="dashboard-content" class="tab-content active">
<!-- 핵심내용 -->
<div class="section dashboard-section">
<div class="section-header">
@@ -249,6 +249,17 @@
</style>
</head>
<body>
<!--
[진입 경로]
10-회의록상세조회.html → 하단 액션 바 "수정" 버튼 클릭
[권한 제어]
- 검증완료 전 (작성중/초안 상태): 모든 참석자가 수정 가능
- 검증완료 후: 회의 생성자만 수정 가능 (참석자는 "수정" 버튼 비활성화)
- 참석자 관리: 회의 생성자만 추가/삭제 가능
- 회의 일시/장소: 읽기 전용 (회의 예약 화면에서만 변경 가능)
-->
<!-- 헤더 -->
<header class="header">
<div class="header-left">
@@ -267,6 +278,7 @@
<main class="main-content">
<!-- 기본 정보 -->
<div class="info-card">
<!-- 회의 제목 (편집 가능) -->
<input
type="text"
class="meeting-title-input"
@@ -274,13 +286,17 @@
placeholder="회의 제목을 입력하세요"
oninput="markAsUnsaved()"
>
<!-- 회의 일시 (읽기 전용 - 회의 예약 화면에서만 변경 가능) -->
<div class="info-row">
<span class="info-icon">📅</span>
<span>2025년 10월 25일 14:00 (90분)</span>
<span class="text-caption text-muted" style="margin-left: 8px;">(읽기 전용)</span>
</div>
<!-- 회의 장소 (읽기 전용 - 회의 예약 화면에서만 변경 가능) -->
<div class="info-row">
<span class="info-icon">📍</span>
<span>본사 2층 대회의실</span>
<span class="text-caption text-muted" style="margin-left: 8px;">(읽기 전용)</span>
</div>
<div class="info-row">
<span class="info-icon"></span>