mirror of
https://github.com/hwanny1128/HGZero.git
synced 2026-06-13 08:19:10 +00:00
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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user