mirror of
https://github.com/hwanny1128/HGZero.git
synced 2026-06-13 08:19:10 +00:00
main 브랜치 최신 변경사항 병합
- 유저스토리 v2.3.0 업데이트 반영 - UI/UX 프로토타입 개선사항 반영 - Meeting 서비스 기능 추가 및 개선 - Notification 서비스 개선 - User 서비스 LDAP 인증 추가 - 공통 모듈 에러 코드 추가 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -13,6 +13,32 @@
|
||||
padding-bottom: 80px;
|
||||
}
|
||||
|
||||
/* 템플릿 그리드 */
|
||||
.template-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: var(--space-md);
|
||||
}
|
||||
|
||||
/* 템플릿 카드 높이 균등 */
|
||||
.template-list .card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.template-list .card-body {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
|
||||
/* 모바일: 작은 간격 */
|
||||
@media (max-width: 767px) {
|
||||
.template-list {
|
||||
gap: var(--space-sm);
|
||||
}
|
||||
}
|
||||
|
||||
/* 데스크톱: 메인 콘텐츠 조정 */
|
||||
@media (min-width: 768px) {
|
||||
.main-content {
|
||||
@@ -41,18 +67,16 @@
|
||||
<p class="text-muted">회의 유형에 맞는 템플릿을 선택하세요</p>
|
||||
</div>
|
||||
|
||||
<!-- Template Cards -->
|
||||
<div class="template-list" style="display: flex; flex-direction: column; gap: var(--space-md);">
|
||||
<!-- Template Cards (2x2 grid) -->
|
||||
<div class="template-list">
|
||||
<!-- 일반 회의 템플릿 -->
|
||||
<div class="card" style="cursor: pointer;" onclick="selectTemplate('general')">
|
||||
<div class="card-header">
|
||||
<div style="display: flex; align-items: center; gap: var(--space-md);">
|
||||
<div style="font-size: 32px;">📋</div>
|
||||
<div>
|
||||
<h3 class="card-title">일반 회의</h3>
|
||||
<p class="text-muted text-small">기본 회의록 형식</p>
|
||||
</div>
|
||||
<div class="card-header" style="display: block !important;">
|
||||
<div style="margin-bottom: 4px;">
|
||||
<span style="font-size: 20px;">📋</span>
|
||||
<span style="font-size: 14px; font-weight: 600; margin-left: 4px;">일반 회의</span>
|
||||
</div>
|
||||
<div style="font-size: 11px; color: var(--text-muted);">기본 회의록 형식</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="text-small text-muted">
|
||||
@@ -63,21 +87,18 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<button class="btn btn-secondary btn-sm" onclick="previewTemplate(event, 'general')">미리보기</button>
|
||||
<button class="btn btn-primary btn-sm" onclick="selectTemplate('general')">선택</button>
|
||||
<button class="btn btn-primary btn-sm" onclick="selectTemplate('general')" style="width: 100%;">선택</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 스크럼 회의 템플릿 -->
|
||||
<div class="card" style="cursor: pointer;" onclick="selectTemplate('scrum')">
|
||||
<div class="card-header">
|
||||
<div style="display: flex; align-items: center; gap: var(--space-md);">
|
||||
<div style="font-size: 32px;">🏃</div>
|
||||
<div>
|
||||
<h3 class="card-title">스크럼 회의</h3>
|
||||
<p class="text-muted text-small">데일리 스탠드업 형식</p>
|
||||
</div>
|
||||
<div class="card-header" style="display: block !important;">
|
||||
<div style="margin-bottom: 4px;">
|
||||
<span style="font-size: 20px;">🏃</span>
|
||||
<span style="font-size: 14px; font-weight: 600; margin-left: 4px;">스크럼 회의</span>
|
||||
</div>
|
||||
<div style="font-size: 11px; color: var(--text-muted);">데일리 스탠드업 형식</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="text-small text-muted">
|
||||
@@ -87,21 +108,18 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<button class="btn btn-secondary btn-sm" onclick="previewTemplate(event, 'scrum')">미리보기</button>
|
||||
<button class="btn btn-primary btn-sm" onclick="selectTemplate('scrum')">선택</button>
|
||||
<button class="btn btn-primary btn-sm" onclick="selectTemplate('scrum')" style="width: 100%;">선택</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 킥오프 회의 템플릿 -->
|
||||
<div class="card" style="cursor: pointer;" onclick="selectTemplate('kickoff')">
|
||||
<div class="card-header">
|
||||
<div style="display: flex; align-items: center; gap: var(--space-md);">
|
||||
<div style="font-size: 32px;">🚀</div>
|
||||
<div>
|
||||
<h3 class="card-title">킥오프 회의</h3>
|
||||
<p class="text-muted text-small">프로젝트 시작 회의</p>
|
||||
</div>
|
||||
<div class="card-header" style="display: block !important;">
|
||||
<div style="margin-bottom: 4px;">
|
||||
<span style="font-size: 20px;">🚀</span>
|
||||
<span style="font-size: 14px; font-weight: 600; margin-left: 4px;">킥오프 회의</span>
|
||||
</div>
|
||||
<div style="font-size: 11px; color: var(--text-muted);">프로젝트 시작 회의</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="text-small text-muted">
|
||||
@@ -112,21 +130,18 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<button class="btn btn-secondary btn-sm" onclick="previewTemplate(event, 'kickoff')">미리보기</button>
|
||||
<button class="btn btn-primary btn-sm" onclick="selectTemplate('kickoff')">선택</button>
|
||||
<button class="btn btn-primary btn-sm" onclick="selectTemplate('kickoff')" style="width: 100%;">선택</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 주간 회의 템플릿 -->
|
||||
<div class="card" style="cursor: pointer;" onclick="selectTemplate('weekly')">
|
||||
<div class="card-header">
|
||||
<div style="display: flex; align-items: center; gap: var(--space-md);">
|
||||
<div style="font-size: 32px;">📅</div>
|
||||
<div>
|
||||
<h3 class="card-title">주간 회의</h3>
|
||||
<p class="text-muted text-small">주간 리뷰 및 계획</p>
|
||||
</div>
|
||||
<div class="card-header" style="display: block !important;">
|
||||
<div style="margin-bottom: 4px;">
|
||||
<span style="font-size: 20px;">📅</span>
|
||||
<span style="font-size: 14px; font-weight: 600; margin-left: 4px;">주간 회의</span>
|
||||
</div>
|
||||
<div style="font-size: 11px; color: var(--text-muted);">주간 리뷰 및 계획</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="text-small text-muted">
|
||||
@@ -137,72 +152,12 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<button class="btn btn-secondary btn-sm" onclick="previewTemplate(event, 'weekly')">미리보기</button>
|
||||
<button class="btn btn-primary btn-sm" onclick="selectTemplate('weekly')">선택</button>
|
||||
<button class="btn btn-primary btn-sm" onclick="selectTemplate('weekly')" style="width: 100%;">선택</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Template Preview Modal -->
|
||||
<div id="previewModal" class="modal-overlay">
|
||||
<div class="modal">
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title" id="previewTitle">템플릿 미리보기</h2>
|
||||
<button class="modal-close" onclick="closeModal('previewModal')">✕</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div id="previewContent" style="max-height: 400px; overflow-y: auto;">
|
||||
<!-- 섹션 리스트가 여기에 표시됨 -->
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-ghost" onclick="closeModal('previewModal')">닫기</button>
|
||||
<button class="btn btn-primary" onclick="customizeTemplate()">커스터마이징</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Template Customization Modal -->
|
||||
<div id="customizeModal" class="modal-overlay">
|
||||
<div class="modal">
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title">템플릿 커스터마이징</h2>
|
||||
<button class="modal-close" onclick="closeModal('customizeModal')">✕</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p class="text-small text-muted mb-md">섹션을 드래그하여 순서를 변경하거나 삭제할 수 있습니다</p>
|
||||
<div id="sectionList" style="display: flex; flex-direction: column; gap: var(--space-sm);">
|
||||
<!-- 섹션 목록이 여기에 표시됨 -->
|
||||
</div>
|
||||
<button class="btn btn-ghost mt-md" onclick="addSection()" style="width: 100%;">+ 섹션 추가</button>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-ghost" onclick="closeModal('customizeModal')">취소</button>
|
||||
<button class="btn btn-primary" onclick="startMeeting()">이 템플릿으로 시작</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add Section Modal -->
|
||||
<div id="addSectionModal" class="modal-overlay">
|
||||
<div class="modal">
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title">섹션 추가</h2>
|
||||
<button class="modal-close" onclick="closeModal('addSectionModal')">✕</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<label class="form-label">섹션 이름</label>
|
||||
<input type="text" class="form-control" id="newSectionName" placeholder="예: 기술 검토">
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-ghost" onclick="closeModal('addSectionModal')">취소</button>
|
||||
<button class="btn btn-primary" onclick="confirmAddSection()">추가</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="common.js"></script>
|
||||
<script>
|
||||
@@ -230,35 +185,13 @@
|
||||
}
|
||||
};
|
||||
|
||||
let selectedTemplate = null;
|
||||
let customSections = [];
|
||||
|
||||
// 템플릿 미리보기
|
||||
function previewTemplate(event, templateId) {
|
||||
event.stopPropagation();
|
||||
const template = templates[templateId];
|
||||
|
||||
$('#previewTitle').textContent = template.name + ' 미리보기';
|
||||
|
||||
const content = template.sections.map((section, index) => `
|
||||
<div class="list-item">
|
||||
<span class="text-muted text-small">${index + 1}.</span>
|
||||
<span class="list-item-title">${section}</span>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
$('#previewContent').innerHTML = content;
|
||||
openModal('previewModal');
|
||||
}
|
||||
|
||||
// 템플릿 선택
|
||||
function selectTemplate(templateId) {
|
||||
selectedTemplate = templateId;
|
||||
customSections = [...templates[templateId].sections];
|
||||
const templateSections = [...templates[templateId].sections];
|
||||
|
||||
// 회의 진행 화면으로 이동
|
||||
saveToStorage('selectedTemplate', templateId);
|
||||
saveToStorage('templateSections', customSections);
|
||||
saveToStorage('templateSections', templateSections);
|
||||
navigateTo('05-회의진행.html');
|
||||
}
|
||||
|
||||
@@ -276,114 +209,6 @@
|
||||
$('#skip-btn')?.addEventListener('click', () => {
|
||||
skipTemplate();
|
||||
});
|
||||
|
||||
// 커스터마이징 모달 열기
|
||||
function customizeTemplate() {
|
||||
closeModal('previewModal');
|
||||
renderSectionList();
|
||||
openModal('customizeModal');
|
||||
}
|
||||
|
||||
// 섹션 리스트 렌더링
|
||||
function renderSectionList() {
|
||||
const sectionList = $('#sectionList');
|
||||
sectionList.innerHTML = customSections.map((section, index) => `
|
||||
<div class="list-item" draggable="true" data-index="${index}">
|
||||
<span class="text-muted">☰</span>
|
||||
<span class="list-item-title" style="flex: 1;">${section}</span>
|
||||
<button class="btn btn-ghost btn-sm" onclick="removeSection(${index})">
|
||||
<span style="color: var(--error);">✕</span>
|
||||
</button>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
// 드래그 이벤트 설정
|
||||
setupDragAndDrop();
|
||||
}
|
||||
|
||||
// 드래그 앤 드롭 설정
|
||||
function setupDragAndDrop() {
|
||||
const items = $$('#sectionList .list-item');
|
||||
let draggedItem = null;
|
||||
|
||||
items.forEach(item => {
|
||||
item.addEventListener('dragstart', function() {
|
||||
draggedItem = this;
|
||||
this.style.opacity = '0.5';
|
||||
});
|
||||
|
||||
item.addEventListener('dragend', function() {
|
||||
this.style.opacity = '1';
|
||||
});
|
||||
|
||||
item.addEventListener('dragover', function(e) {
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
item.addEventListener('drop', function(e) {
|
||||
e.preventDefault();
|
||||
if (draggedItem !== this) {
|
||||
const draggedIndex = parseInt(draggedItem.dataset.index);
|
||||
const targetIndex = parseInt(this.dataset.index);
|
||||
|
||||
const temp = customSections[draggedIndex];
|
||||
customSections.splice(draggedIndex, 1);
|
||||
customSections.splice(targetIndex, 0, temp);
|
||||
|
||||
renderSectionList();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 섹션 삭제
|
||||
function removeSection(index) {
|
||||
if (customSections.length <= 1) {
|
||||
showToast('최소 1개의 섹션이 필요합니다', 'error');
|
||||
return;
|
||||
}
|
||||
customSections.splice(index, 1);
|
||||
renderSectionList();
|
||||
}
|
||||
|
||||
// 섹션 추가 모달 열기
|
||||
function addSection() {
|
||||
openModal('addSectionModal');
|
||||
$('#newSectionName').value = '';
|
||||
$('#newSectionName').focus();
|
||||
}
|
||||
|
||||
// 섹션 추가 확인
|
||||
function confirmAddSection() {
|
||||
const name = $('#newSectionName').value.trim();
|
||||
if (!name) {
|
||||
showToast('섹션 이름을 입력해주세요', 'error');
|
||||
return;
|
||||
}
|
||||
customSections.push(name);
|
||||
renderSectionList();
|
||||
closeModal('addSectionModal');
|
||||
showToast('섹션이 추가되었습니다', 'success');
|
||||
}
|
||||
|
||||
// 회의 시작
|
||||
function startMeeting() {
|
||||
if (customSections.length === 0) {
|
||||
showToast('최소 1개의 섹션이 필요합니다', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
saveToStorage('selectedTemplate', selectedTemplate);
|
||||
saveToStorage('templateSections', customSections);
|
||||
navigateTo('05-회의진행.html');
|
||||
}
|
||||
|
||||
// Enter 키로 섹션 추가
|
||||
$('#addSectionModal')?.addEventListener('keypress', function(e) {
|
||||
if (e.key === 'Enter') {
|
||||
confirmAddSection();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+299
-151
@@ -94,6 +94,11 @@
|
||||
50% { opacity: 0.3; }
|
||||
}
|
||||
|
||||
.recording-indicator.paused .recording-dot,
|
||||
.recording-indicator.paused .waveform-bar {
|
||||
animation-play-state: paused;
|
||||
}
|
||||
|
||||
.recording-time {
|
||||
font-size: var(--font-small);
|
||||
font-weight: var(--font-weight-medium);
|
||||
@@ -129,18 +134,23 @@
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
background: var(--gray-100);
|
||||
padding: 0 var(--space-md) 10px;
|
||||
padding: var(--space-md);
|
||||
padding-bottom: 88px; /* 하단 버튼 영역 확보 */
|
||||
max-width: none !important; /* common.css의 max-width: 900px 오버라이드 */
|
||||
margin: 0 !important; /* common.css의 auto margin 제거 */
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.main-content {
|
||||
padding: 0 var(--space-lg) 88px;
|
||||
padding: var(--space-lg);
|
||||
padding-bottom: 88px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.main-content {
|
||||
padding: 0 var(--space-xl) 88px;
|
||||
padding: var(--space-xl);
|
||||
padding-bottom: 88px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,6 +161,8 @@
|
||||
padding: var(--space-md);
|
||||
margin-bottom: var(--space-md);
|
||||
box-shadow: var(--shadow-md);
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.meeting-info-grid {
|
||||
@@ -192,6 +204,11 @@
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow-md);
|
||||
margin-bottom: var(--space-md);
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
min-width: 0;
|
||||
box-sizing: border-box;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.tabs-header {
|
||||
@@ -248,22 +265,19 @@
|
||||
.tab-content {
|
||||
display: none;
|
||||
padding: var(--space-md);
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.tab-content.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.tab-content {
|
||||
padding: var(--space-lg);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.tab-content {
|
||||
padding: var(--space-xl);
|
||||
}
|
||||
/* 모든 탭 콘텐츠 내부 요소 텍스트 줄바꿈 강제 */
|
||||
.tab-content * {
|
||||
max-width: 100%;
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
/* 참석자 탭 */
|
||||
@@ -437,6 +451,26 @@
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* 용어 검색 폼 */
|
||||
.term-search-form {
|
||||
display: flex;
|
||||
gap: var(--space-xs);
|
||||
margin-bottom: var(--space-sm);
|
||||
}
|
||||
|
||||
.term-search-input {
|
||||
flex: 1;
|
||||
font-size: var(--font-small);
|
||||
padding: var(--space-sm);
|
||||
}
|
||||
|
||||
.term-search-btn {
|
||||
padding: var(--space-sm) var(--space-md);
|
||||
font-size: var(--font-small);
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* 용어 사전 카드 */
|
||||
.term-item {
|
||||
background: #FAFAFA;
|
||||
@@ -564,6 +598,11 @@
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.rec-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.end-meeting-btn {
|
||||
flex: 1;
|
||||
font-size: var(--font-body);
|
||||
@@ -580,7 +619,7 @@
|
||||
</div>
|
||||
|
||||
<div class="recording-status-bar">
|
||||
<div class="recording-indicator">
|
||||
<div class="recording-indicator" id="recordingIndicator">
|
||||
<span class="recording-dot"></span>
|
||||
<span class="recording-time" id="recordingTime">00:15:51</span>
|
||||
<div class="waveform">
|
||||
@@ -708,13 +747,11 @@
|
||||
</div>
|
||||
|
||||
<h4 class="ai-suggestion-list-title">
|
||||
💬 AI가 실시간으로 분석한 제안사항
|
||||
💬 AI가 실시간으로 분석한 주요 내용
|
||||
</h4>
|
||||
|
||||
<div id="aiSuggestionList">
|
||||
<!-- AI 제안사항이 실시간으로 추가됩니다 -->
|
||||
|
||||
<!-- 백업용 정적 샘플 데이터 (SSE 연결 실패 시 표시)
|
||||
<!-- AI 제안 1 -->
|
||||
<div class="ai-suggestion-card" id="suggestion-1">
|
||||
<div class="ai-suggestion-header">
|
||||
<span class="ai-suggestion-time">00:05:23</span>
|
||||
@@ -727,6 +764,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- AI 제안 2 -->
|
||||
<div class="ai-suggestion-card" id="suggestion-2">
|
||||
<div class="ai-suggestion-header">
|
||||
<span class="ai-suggestion-time">00:08:45</span>
|
||||
@@ -739,6 +777,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- AI 제안 3 -->
|
||||
<div class="ai-suggestion-card" id="suggestion-3">
|
||||
<div class="ai-suggestion-header">
|
||||
<span class="ai-suggestion-time">00:12:18</span>
|
||||
@@ -750,7 +789,6 @@
|
||||
마케팅 예산 배분에 대해 SNS 광고 60%, 인플루언서 마케팅 40%로 의견이 나왔으나 추가 검토 필요
|
||||
</div>
|
||||
</div>
|
||||
-->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -758,8 +796,21 @@
|
||||
<div class="tab-content" id="tab-terms">
|
||||
<h3 class="text-small font-bold mb-md">용어 사전</h3>
|
||||
|
||||
<!-- 용어 검색 폼 -->
|
||||
<div class="term-search-form">
|
||||
<input
|
||||
type="text"
|
||||
class="form-control term-search-input"
|
||||
id="termSearchInput"
|
||||
placeholder="용어 검색..."
|
||||
>
|
||||
<button class="btn btn-primary btn-sm term-search-btn" onclick="searchTerm()">
|
||||
검색
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="termsList">
|
||||
<div class="term-item" onclick="showTermDetail('MVP')">
|
||||
<div class="term-item highlight" onclick="showTermDetail('MVP')">
|
||||
<div class="term-name">
|
||||
MVP
|
||||
<span class="term-badge">기획</span>
|
||||
@@ -770,7 +821,7 @@
|
||||
<div class="term-context">신제품 기획 회의에서 언급</div>
|
||||
</div>
|
||||
|
||||
<div class="term-item" onclick="showTermDetail('B2C')">
|
||||
<div class="term-item highlight" onclick="showTermDetail('B2C')">
|
||||
<div class="term-name">
|
||||
B2C
|
||||
<span class="term-badge">비즈니스</span>
|
||||
@@ -781,7 +832,7 @@
|
||||
<div class="term-context">타겟 고객 분석 시 사용</div>
|
||||
</div>
|
||||
|
||||
<div class="term-item" onclick="showTermDetail('PMF')">
|
||||
<div class="term-item highlight" onclick="showTermDetail('PMF')">
|
||||
<div class="term-name">
|
||||
PMF
|
||||
<span class="term-badge">전략</span>
|
||||
@@ -792,7 +843,7 @@
|
||||
<div class="term-context">제품 전략 논의 중 언급</div>
|
||||
</div>
|
||||
|
||||
<div class="term-item" onclick="showTermDetail('CAC')">
|
||||
<div class="term-item highlight" onclick="showTermDetail('CAC')">
|
||||
<div class="term-name">
|
||||
CAC
|
||||
<span class="term-badge">마케팅</span>
|
||||
@@ -802,6 +853,102 @@
|
||||
</div>
|
||||
<div class="term-context">마케팅 예산 논의에서 사용</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="termList">
|
||||
<div class="term-item" onclick="showTermDetail('Mobile First')">
|
||||
<div class="term-name">
|
||||
Mobile First
|
||||
<span class="term-badge">설계 방법론</span>
|
||||
<span class="term-mention-icon">💬</span>
|
||||
</div>
|
||||
<div class="term-definition">
|
||||
모바일 환경을 우선적으로 고려하여 디자인하고, 이후 더 큰 화면으로 확장하는 설계 방법론입니다.
|
||||
</div>
|
||||
<div class="term-context">회의에서 언급됨 (14:23)</div>
|
||||
</div>
|
||||
|
||||
<div class="term-item" onclick="showTermDetail('AI')">
|
||||
<div class="term-name">
|
||||
AI
|
||||
<span class="term-badge">기술</span>
|
||||
<span class="term-mention-icon">💬</span>
|
||||
</div>
|
||||
<div class="term-definition">
|
||||
Artificial Intelligence의 약자로, 인공지능을 의미합니다. 이 프로젝트에서는 회의록 자동 작성에 활용됩니다.
|
||||
</div>
|
||||
<div class="term-context">회의에서 5회 언급됨</div>
|
||||
</div>
|
||||
|
||||
<div class="term-item" onclick="showTermDetail('API')">
|
||||
<div class="term-name">
|
||||
API
|
||||
<span class="term-badge">기술</span>
|
||||
<span class="term-mention-icon">💬</span>
|
||||
</div>
|
||||
<div class="term-definition">
|
||||
Application Programming Interface의 약자로, 소프트웨어 간 상호작용을 위한 인터페이스입니다.
|
||||
</div>
|
||||
<div class="term-context">회의에서 3회 언급됨</div>
|
||||
</div>
|
||||
|
||||
<div class="term-item" onclick="showTermDetail('API Gateway')">
|
||||
<div class="term-name">
|
||||
API Gateway
|
||||
<span class="term-badge">아키텍처</span>
|
||||
<span class="term-mention-icon">💬</span>
|
||||
</div>
|
||||
<div class="term-definition">
|
||||
클라이언트와 백엔드 마이크로서비스 사이의 단일 진입점 역할을 하는 서버. 요청 라우팅, 인증, 속도 제한, 로드 밸런싱 등을 처리합니다.
|
||||
</div>
|
||||
<div class="term-context">API 설계 리뷰 회의 (2024-09-28)에서 AWS API Gateway 채택 결정</div>
|
||||
</div>
|
||||
|
||||
<div class="term-item" onclick="showTermDetail('마이크로서비스')">
|
||||
<div class="term-name">
|
||||
마이크로서비스
|
||||
<span class="term-badge">아키텍처</span>
|
||||
<span class="term-mention-icon">💬</span>
|
||||
</div>
|
||||
<div class="term-definition">
|
||||
애플리케이션을 작고 독립적인 서비스들로 분리하여 개발하고 배포하는 아키텍처 패턴입니다.
|
||||
</div>
|
||||
<div class="term-context">회의에서 언급됨</div>
|
||||
</div>
|
||||
|
||||
<div class="term-item " onclick="showTermDetail('MVP')">
|
||||
<div class="term-name">
|
||||
MVP
|
||||
<span class="term-badge">방법론</span>
|
||||
<span class="term-mention-icon">💬</span>
|
||||
</div>
|
||||
<div class="term-definition">
|
||||
Minimum Viable Product의 약자. 최소한의 기능만 갖춘 제품으로, 시장 반응을 빠르게 확인하기 위해 개발합니다.
|
||||
</div>
|
||||
<div class="term-context">개발 일정 논의에서 언급</div>
|
||||
</div>
|
||||
|
||||
<div class="term-item" onclick="showTermDetail('RESTful API')">
|
||||
<div class="term-name">
|
||||
RESTful API
|
||||
<span class="term-badge">기술</span>
|
||||
</div>
|
||||
<div class="term-definition">
|
||||
REST(Representational State Transfer) 아키텍처 스타일을 따르는 웹 서비스 API 설계 방식입니다.
|
||||
</div>
|
||||
<div class="term-context">API 설계 리뷰 회의 참조</div>
|
||||
</div>
|
||||
|
||||
<div class="term-item" onclick="showTermDetail('JWT')">
|
||||
<div class="term-name">
|
||||
JWT
|
||||
<span class="term-badge">보안</span>
|
||||
</div>
|
||||
<div class="term-definition">
|
||||
JSON Web Token의 약자. 사용자 인증 정보를 안전하게 전송하기 위한 토큰 기반 인증 방식입니다.
|
||||
</div>
|
||||
<div class="term-context">API Gateway 보안 정책에서 채택</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -853,11 +1000,12 @@
|
||||
|
||||
<!-- 하단 고정 버튼 영역 -->
|
||||
<div class="bottom-action-bar">
|
||||
<button class="btn btn-ghost pause-btn" onclick="pauseRecording()" id="pauseBtn">
|
||||
<svg class="pause-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<button class="btn btn-ghost pause-btn" onclick="toggleRecording()" id="pauseBtn">
|
||||
<svg class="pause-icon" id="pauseIcon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<rect x="6" y="4" width="4" height="16" rx="1"></rect>
|
||||
<rect x="14" y="4" width="4" height="16" rx="1"></rect>
|
||||
</svg>
|
||||
<img class="rec-icon" id="recIcon" src="img/rec.png" alt="녹음 재개" style="display: none;">
|
||||
</button>
|
||||
<button class="btn btn-error end-meeting-btn" onclick="endMeeting()">
|
||||
회의 종료
|
||||
@@ -980,9 +1128,104 @@
|
||||
// });
|
||||
}
|
||||
|
||||
// 녹음 일시정지
|
||||
function pauseRecording() {
|
||||
alert('녹음이 일시정지되었습니다');
|
||||
// 용어 검색
|
||||
function searchTerm() {
|
||||
const searchInput = document.getElementById('termSearchInput');
|
||||
const searchText = searchInput.value.trim().toLowerCase();
|
||||
|
||||
if (!searchText) {
|
||||
alert('검색할 용어를 입력해주세요');
|
||||
return;
|
||||
}
|
||||
|
||||
const termItems = document.querySelectorAll('.term-item');
|
||||
let foundCount = 0;
|
||||
|
||||
termItems.forEach(item => {
|
||||
const termName = item.querySelector('.term-name').textContent.toLowerCase();
|
||||
const termDefinition = item.querySelector('.term-definition').textContent.toLowerCase();
|
||||
|
||||
// 용어명 또는 정의에 검색어가 포함되어 있는지 확인
|
||||
if (termName.includes(searchText) || termDefinition.includes(searchText)) {
|
||||
item.style.display = '';
|
||||
item.classList.add('highlight');
|
||||
foundCount++;
|
||||
} else {
|
||||
item.style.display = 'none';
|
||||
item.classList.remove('highlight');
|
||||
}
|
||||
});
|
||||
|
||||
if (foundCount === 0) {
|
||||
alert('검색 결과가 없습니다');
|
||||
// 모든 항목 다시 표시
|
||||
termItems.forEach(item => {
|
||||
item.style.display = '';
|
||||
item.classList.remove('highlight');
|
||||
});
|
||||
searchInput.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
// Enter 키로 검색 실행
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const searchInput = document.getElementById('termSearchInput');
|
||||
if (searchInput) {
|
||||
searchInput.addEventListener('keypress', function(event) {
|
||||
if (event.key === 'Enter') {
|
||||
searchTerm();
|
||||
}
|
||||
});
|
||||
|
||||
// 입력값이 비워지면 전체 표시
|
||||
searchInput.addEventListener('input', function() {
|
||||
if (this.value.trim() === '') {
|
||||
const termItems = document.querySelectorAll('.term-item');
|
||||
termItems.forEach(item => {
|
||||
item.style.display = '';
|
||||
item.classList.remove('highlight');
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 녹음 상태 관리
|
||||
let isRecording = true;
|
||||
let timerInterval = null;
|
||||
|
||||
// 녹음 일시정지/재개 토글
|
||||
function toggleRecording() {
|
||||
const pauseIcon = document.getElementById('pauseIcon');
|
||||
const recIcon = document.getElementById('recIcon');
|
||||
const recordingIndicator = document.getElementById('recordingIndicator');
|
||||
|
||||
if (isRecording) {
|
||||
// 일시정지
|
||||
isRecording = false;
|
||||
pauseIcon.style.display = 'none';
|
||||
recIcon.style.display = 'block';
|
||||
recordingIndicator.classList.add('paused');
|
||||
|
||||
// 타이머 정지
|
||||
if (timerInterval) {
|
||||
clearInterval(timerInterval);
|
||||
timerInterval = null;
|
||||
}
|
||||
|
||||
alert('녹음이 일시정지되었습니다');
|
||||
} else {
|
||||
// 재개
|
||||
isRecording = true;
|
||||
pauseIcon.style.display = 'block';
|
||||
recIcon.style.display = 'none';
|
||||
recordingIndicator.classList.remove('paused');
|
||||
|
||||
// 타이머 재시작
|
||||
startTimer();
|
||||
|
||||
alert('녹음이 재개되었습니다');
|
||||
}
|
||||
}
|
||||
|
||||
// 회의 종료
|
||||
@@ -991,19 +1234,27 @@
|
||||
alert('회의가 종료되었습니다. AI가 회의록을 생성 중입니다...');
|
||||
|
||||
setTimeout(() => {
|
||||
window.location.href = '10-회의록상세조회.html';
|
||||
window.location.href = '07-회의종료.html';
|
||||
}, 2000);
|
||||
}
|
||||
}
|
||||
|
||||
// 타이머 업데이트
|
||||
function updateTimer() {
|
||||
// 타이머 시작 함수
|
||||
function startTimer() {
|
||||
const timerElement = document.getElementById('recordingTime');
|
||||
let seconds = 51;
|
||||
let minutes = 15;
|
||||
let hours = 0;
|
||||
|
||||
setInterval(() => {
|
||||
// 이미 타이머가 실행 중이면 중복 실행 방지
|
||||
if (timerInterval) {
|
||||
return;
|
||||
}
|
||||
|
||||
timerInterval = setInterval(() => {
|
||||
// 현재 시간을 파싱
|
||||
const currentTime = timerElement.textContent.split(':');
|
||||
let hours = parseInt(currentTime[0]);
|
||||
let minutes = parseInt(currentTime[1]);
|
||||
let seconds = parseInt(currentTime[2]);
|
||||
|
||||
seconds++;
|
||||
if (seconds === 60) {
|
||||
seconds = 0;
|
||||
@@ -1023,131 +1274,28 @@
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
// SSE로 실시간 AI 제안사항 수신
|
||||
let eventSource = null;
|
||||
const meetingId = '550e8400-e29b-41d4-a716-446655440000'; // 테스트용 회의 ID
|
||||
|
||||
function connectAiSuggestionStream() {
|
||||
// EventSource를 사용하여 SSE 연결
|
||||
const apiUrl = `http://localhost:8083/api/suggestions/meetings/${meetingId}/stream`;
|
||||
|
||||
console.log('[DEBUG] SSE 연결 시작:', apiUrl);
|
||||
|
||||
eventSource = new EventSource(apiUrl);
|
||||
|
||||
// 연결 성공
|
||||
eventSource.onopen = function(event) {
|
||||
console.log('[SUCCESS] SSE 연결 성공!', event);
|
||||
};
|
||||
|
||||
// 모든 이벤트 수신 (디버깅용)
|
||||
eventSource.onmessage = function(event) {
|
||||
console.log('[DEBUG] 일반 메시지 수신:', event.data);
|
||||
};
|
||||
|
||||
// ai-suggestion 이벤트 수신
|
||||
eventSource.addEventListener('ai-suggestion', function(event) {
|
||||
console.log('[SUCCESS] AI 제안사항 수신:', event.data);
|
||||
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
const suggestions = data.suggestions;
|
||||
|
||||
console.log('[DEBUG] 파싱된 데이터:', data);
|
||||
console.log('[DEBUG] 제안사항 개수:', suggestions ? suggestions.length : 0);
|
||||
|
||||
if (suggestions && suggestions.length > 0) {
|
||||
suggestions.forEach(suggestion => {
|
||||
console.log('[DEBUG] 제안사항 추가 중:', suggestion);
|
||||
addAiSuggestionToUI(suggestion);
|
||||
});
|
||||
} else {
|
||||
console.warn('[WARNING] 제안사항이 비어있음');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[ERROR] AI 제안사항 파싱 오류:', error);
|
||||
console.error('[ERROR] 원본 데이터:', event.data);
|
||||
}
|
||||
});
|
||||
|
||||
// 에러 처리
|
||||
eventSource.onerror = function(error) {
|
||||
console.error('[ERROR] SSE 연결 오류:', error);
|
||||
console.error('[ERROR] ReadyState:', eventSource.readyState);
|
||||
|
||||
// ReadyState: 0=CONNECTING, 1=OPEN, 2=CLOSED
|
||||
if (eventSource.readyState === EventSource.CLOSED) {
|
||||
console.error('[ERROR] SSE 연결이 닫혔습니다');
|
||||
} else if (eventSource.readyState === EventSource.CONNECTING) {
|
||||
console.warn('[WARNING] SSE 재연결 시도 중...');
|
||||
}
|
||||
|
||||
// 에러 발생 시 닫기
|
||||
eventSource.close();
|
||||
};
|
||||
|
||||
console.log('[INFO] AI 제안사항 SSE 스트림 연결 요청 완료');
|
||||
// 타이머 초기화 함수
|
||||
function updateTimer() {
|
||||
startTimer();
|
||||
}
|
||||
|
||||
// AI 제안사항을 UI에 추가
|
||||
function addAiSuggestionToUI(suggestion) {
|
||||
const listContainer = document.getElementById('aiSuggestionList');
|
||||
// 관련 회의록 열기
|
||||
function openRelatedDoc(docId) {
|
||||
// 새 탭으로 회의록 상세조회 화면 열기
|
||||
window.open('10-회의록상세조회.html', '_blank');
|
||||
|
||||
// 고유 ID 생성 (이미 추가된 제안인지 확인용)
|
||||
const cardId = `suggestion-${suggestion.id}`;
|
||||
|
||||
// 이미 존재하는 제안이면 추가하지 않음
|
||||
if (document.getElementById(cardId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// AI 제안 카드 HTML 생성
|
||||
const cardHtml = `
|
||||
<div class="ai-suggestion-card" id="${cardId}">
|
||||
<div class="ai-suggestion-header">
|
||||
<span class="ai-suggestion-time">${suggestion.timestamp}</span>
|
||||
<button class="ai-suggestion-add-btn"
|
||||
onclick="addToMemo('${escapeHtml(suggestion.content)}', document.getElementById('${cardId}'))"
|
||||
title="메모에 추가">
|
||||
➕
|
||||
</button>
|
||||
</div>
|
||||
<div class="ai-suggestion-text">
|
||||
${escapeHtml(suggestion.content)}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// 리스트에 추가
|
||||
listContainer.insertAdjacentHTML('beforeend', cardHtml);
|
||||
|
||||
console.log('AI 제안사항 추가됨:', suggestion.content);
|
||||
// 기본 동작(링크 이동) 방지
|
||||
return false;
|
||||
}
|
||||
|
||||
// HTML 이스케이프 (XSS 방지)
|
||||
function escapeHtml(text) {
|
||||
const map = {
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
"'": '''
|
||||
};
|
||||
return text.replace(/[&<>"']/g, m => map[m]);
|
||||
// 용어 상세 정보 보기
|
||||
function showTermDetail(termName) {
|
||||
alert(`"${termName}" 용어에 대한 상세 정보를 표시합니다.`);
|
||||
}
|
||||
|
||||
// 페이지 로드 시 타이머 시작 및 SSE 연결
|
||||
// 페이지 로드 시 타이머 시작
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
updateTimer();
|
||||
connectAiSuggestionStream();
|
||||
});
|
||||
|
||||
// 페이지 종료 시 SSE 연결 해제
|
||||
window.addEventListener('beforeunload', function() {
|
||||
if (eventSource) {
|
||||
eventSource.close();
|
||||
console.log('SSE 연결 종료');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
@@ -1,527 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>검증 완료 - 회의록 서비스</title>
|
||||
<link rel="stylesheet" href="common.css">
|
||||
<style>
|
||||
/* 페이지별 커스텀 스타일만 유지 */
|
||||
/* 공통 스타일(헤더, 메인콘텐츠, 액션바)은 common.css 사용 */
|
||||
|
||||
.progress-container {
|
||||
margin-bottom: var(--space-lg);
|
||||
}
|
||||
|
||||
.progress-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: var(--space-sm);
|
||||
}
|
||||
|
||||
.progress-percentage {
|
||||
font-size: var(--font-h2);
|
||||
font-weight: var(--font-weight-bold);
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.verification-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-md);
|
||||
padding: var(--space-md);
|
||||
background: var(--white);
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow-md);
|
||||
margin-bottom: var(--space-md);
|
||||
transition: all var(--transition-normal);
|
||||
}
|
||||
|
||||
.verification-card:hover {
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.verification-card.verified {
|
||||
border-left: 4px solid var(--success);
|
||||
}
|
||||
|
||||
.verification-card.unverified {
|
||||
border-left: 4px solid var(--gray-300);
|
||||
}
|
||||
|
||||
.verification-card.locked {
|
||||
background: var(--gray-100);
|
||||
}
|
||||
|
||||
.verify-icon {
|
||||
font-size: 32px;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.verify-icon.verified {
|
||||
color: var(--success);
|
||||
}
|
||||
|
||||
.verify-icon.unverified {
|
||||
color: var(--gray-300);
|
||||
}
|
||||
|
||||
.lock-icon {
|
||||
font-size: 20px;
|
||||
color: var(--gray-500);
|
||||
}
|
||||
|
||||
/* 편집 모드 스타일 */
|
||||
.edit-field {
|
||||
width: 100%;
|
||||
padding: var(--space-sm);
|
||||
border: 1px solid var(--gray-300);
|
||||
border-radius: var(--radius-md);
|
||||
font-size: var(--font-small);
|
||||
line-height: 1.6;
|
||||
margin-bottom: var(--space-sm);
|
||||
transition: border-color var(--transition-normal);
|
||||
}
|
||||
|
||||
.edit-field:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
.edit-label {
|
||||
display: block;
|
||||
font-weight: var(--font-weight-medium);
|
||||
margin-bottom: var(--space-xs);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="page">
|
||||
<!-- Header -->
|
||||
<header class="header">
|
||||
<div class="header-left">
|
||||
<button class="back-btn" onclick="history.back()">←</button>
|
||||
<h1 class="header-title">검증 완료</h1>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="main-content has-action-bar">
|
||||
<!-- Progress Bar -->
|
||||
<div class="progress-container">
|
||||
<div class="progress-header">
|
||||
<h2 class="text-small font-bold">전체 진행률</h2>
|
||||
<span class="progress-percentage" id="progressText">50%</span>
|
||||
</div>
|
||||
<div class="progress" style="height: 12px;">
|
||||
<div class="progress-bar progress-bar-success" id="progressBar" style="width: 50%;"></div>
|
||||
</div>
|
||||
<p class="text-small text-muted mt-sm">4개 섹션 중 2개 검증 완료</p>
|
||||
</div>
|
||||
|
||||
<!-- Meeting Info -->
|
||||
<div class="card mb-lg">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">2025년 1분기 제품 기획 회의</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="text-small text-muted">
|
||||
<div style="display: flex; gap: var(--space-md); margin-bottom: var(--space-xs);">
|
||||
<span>📅 2025-10-25 14:00</span>
|
||||
<span>⏱️ 90분</span>
|
||||
</div>
|
||||
<div>👥 김민준, 박서연, 이준호, 최유진</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Section List -->
|
||||
<div>
|
||||
<h2 class="text-small font-bold mb-md">섹션별 검증 상태</h2>
|
||||
|
||||
<!-- 섹션 1 - 검증 완료 -->
|
||||
<div class="verification-card verified">
|
||||
<div class="verify-icon verified">✓</div>
|
||||
<div style="flex: 1;">
|
||||
<h3 class="text-small font-bold" style="margin: 0 0 4px 0;">회의 개요</h3>
|
||||
<div style="display: flex; align-items: center; gap: var(--space-sm);">
|
||||
<div class="avatar-group">
|
||||
<div class="avatar avatar-green avatar-sm">김</div>
|
||||
<div class="avatar avatar-blue avatar-sm">박</div>
|
||||
</div>
|
||||
<span class="text-caption text-muted">2명 검증 완료</span>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-secondary btn-sm" onclick="viewSection(0)">보기</button>
|
||||
</div>
|
||||
|
||||
<!-- 섹션 2 - 검증 완료 + 잠금 -->
|
||||
<div class="verification-card verified locked">
|
||||
<div class="verify-icon verified">✓</div>
|
||||
<div style="flex: 1;">
|
||||
<h3 class="text-small font-bold" style="margin: 0 0 4px 0;">
|
||||
논의 사항
|
||||
<span class="lock-icon">🔒</span>
|
||||
</h3>
|
||||
<div style="display: flex; align-items: center; gap: var(--space-sm);">
|
||||
<div class="avatar-group">
|
||||
<div class="avatar avatar-green avatar-sm">김</div>
|
||||
<div class="avatar avatar-blue avatar-sm">박</div>
|
||||
<div class="avatar avatar-yellow avatar-sm">이</div>
|
||||
</div>
|
||||
<span class="text-caption text-muted">3명 검증 완료 · 잠금됨</span>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-secondary btn-sm" onclick="unlockSection(1)">잠금해제</button>
|
||||
</div>
|
||||
|
||||
<!-- 섹션 3 - 미검증 -->
|
||||
<div class="verification-card unverified">
|
||||
<div class="verify-icon unverified">○</div>
|
||||
<div style="flex: 1;">
|
||||
<h3 class="text-small font-bold" style="margin: 0 0 4px 0;">결정 사항</h3>
|
||||
<div style="display: flex; align-items: center; gap: var(--space-sm);">
|
||||
<div class="avatar-group">
|
||||
<div class="avatar avatar-blue avatar-sm">박</div>
|
||||
</div>
|
||||
<span class="text-caption text-muted">1명 검증 완료</span>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-primary btn-sm" onclick="verifySection(2)">검증하기</button>
|
||||
</div>
|
||||
|
||||
<!-- 섹션 4 - 미검증 -->
|
||||
<div class="verification-card unverified">
|
||||
<div class="verify-icon unverified">○</div>
|
||||
<div style="flex: 1;">
|
||||
<h3 class="text-small font-bold" style="margin: 0 0 4px 0;">액션 아이템</h3>
|
||||
<div style="display: flex; align-items: center; gap: var(--space-sm);">
|
||||
<span class="text-caption text-muted">아직 검증되지 않음</span>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-primary btn-sm" onclick="verifySection(3)">검증하기</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</main>
|
||||
|
||||
<!-- 하단 액션 바 -->
|
||||
<div class="action-bar">
|
||||
<button class="btn btn-secondary" onclick="saveLater()">나중에 하기</button>
|
||||
<button class="btn btn-primary" id="completeBtn" disabled onclick="completeAllVerification()">
|
||||
모두 검증 완료
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Section View Modal -->
|
||||
<div id="sectionModal" class="modal-overlay">
|
||||
<div class="modal" style="max-height: 80vh; overflow-y: auto;">
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title" id="sectionTitle">회의 개요</h2>
|
||||
<button class="modal-close" onclick="closeModal('sectionModal')">✕</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div id="sectionContent">
|
||||
<!-- 섹션 내용이 여기에 표시됨 -->
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-secondary btn-sm" onclick="closeModal('sectionModal')">닫기</button>
|
||||
<button class="btn btn-primary btn-sm" id="editBtn" onclick="toggleEditMode()">편집</button>
|
||||
<button class="btn btn-primary btn-sm" id="saveBtn" style="display: none;" onclick="saveSection()">저장</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Verification Confirm Modal -->
|
||||
<div id="verifyModal" class="modal-overlay">
|
||||
<div class="modal">
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title">섹션 검증</h2>
|
||||
<button class="modal-close" onclick="closeModal('verifyModal')">✕</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p class="text-small mb-md" id="verifyMessage">이 섹션의 내용을 검증하시겠습니까?</p>
|
||||
<div class="card" style="background: var(--primary-light);">
|
||||
<p class="text-small font-medium">검증 후에는 다른 참석자들도 이 섹션이 확인되었음을 알 수 있습니다.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-secondary btn-sm" onclick="closeModal('verifyModal')">취소</button>
|
||||
<button class="btn btn-primary btn-sm" onclick="confirmVerification()">검증 완료</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Unlock Confirm Modal -->
|
||||
<div id="unlockModal" class="modal-overlay">
|
||||
<div class="modal">
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title">섹션 잠금 해제</h2>
|
||||
<button class="modal-close" onclick="closeModal('unlockModal')">✕</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p class="text-small mb-md">이 섹션의 잠금을 해제하시겠습니까?</p>
|
||||
<div class="card" style="background: transparent; color: var(--error); border: 1px solid var(--error);">
|
||||
<p class="text-small font-medium">⚠️ 잠금 해제 시 다른 참석자들이 내용을 수정할 수 있습니다.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-sm" style="background: transparent; color: var(--error); border: 1px solid var(--error);" onclick="closeModal('unlockModal')">취소</button>
|
||||
<button class="btn btn-error btn-sm" onclick="confirmUnlock()">잠금 해제</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="common.js"></script>
|
||||
<script>
|
||||
// 섹션 검증 상태
|
||||
const sectionVerifications = [
|
||||
{ name: '회의 개요', verified: true, locked: false, verifiers: ['김민준', '박서연'] },
|
||||
{ name: '논의 사항', verified: true, locked: true, verifiers: ['김민준', '박서연', '이준호'] },
|
||||
{ name: '결정 사항', verified: false, locked: false, verifiers: ['박서연'] },
|
||||
{ name: '액션 아이템', verified: false, locked: false, verifiers: [] }
|
||||
];
|
||||
|
||||
let currentSectionIndex = -1;
|
||||
let isEditMode = false;
|
||||
let originalContent = '';
|
||||
|
||||
// 섹션 데이터 (편집 가능한 필드)
|
||||
const sectionData = [
|
||||
{
|
||||
purpose: '2025년 1분기 신제품 개발 방향 수립',
|
||||
attendees: '김민준(PM), 박서연(AI), 이준호(Backend), 최유진(Frontend)',
|
||||
datetime: '2025년 10월 25일 14:00 - 15:30',
|
||||
location: '본사 2층 대회의실'
|
||||
},
|
||||
{
|
||||
topic1: 'AI 모델 정확도',
|
||||
detail1: '현재 STT 정확도: 92%, 목표 정확도: 95% 이상',
|
||||
topic2: '사용자 인터페이스',
|
||||
detail2: 'Mobile First 디자인 채택, 실시간 협업 기능 필수'
|
||||
}
|
||||
];
|
||||
|
||||
// 진행률 업데이트
|
||||
function updateProgress() {
|
||||
const totalSections = sectionVerifications.length;
|
||||
const verifiedCount = sectionVerifications.filter(s => s.verified).length;
|
||||
const percentage = Math.round((verifiedCount / totalSections) * 100);
|
||||
|
||||
$('#progressBar').style.width = percentage + '%';
|
||||
$('#progressText').textContent = percentage + '%';
|
||||
|
||||
const completeBtn = $('#completeBtn');
|
||||
if (percentage === 100) {
|
||||
completeBtn.disabled = false;
|
||||
completeBtn.style.opacity = '1';
|
||||
} else {
|
||||
completeBtn.disabled = true;
|
||||
completeBtn.style.opacity = '0.5';
|
||||
}
|
||||
}
|
||||
|
||||
// 섹션 보기
|
||||
function viewSection(index) {
|
||||
currentSectionIndex = index;
|
||||
isEditMode = false;
|
||||
const section = sectionVerifications[index];
|
||||
|
||||
$('#sectionTitle').textContent = section.name;
|
||||
|
||||
// 편집 모드 초기화
|
||||
$('#editBtn').style.display = 'inline-block';
|
||||
$('#saveBtn').style.display = 'none';
|
||||
|
||||
// 회의 개요만 편집 가능
|
||||
if (index === 0) {
|
||||
renderSectionContent(index, false);
|
||||
} else {
|
||||
// 다른 섹션은 읽기 전용
|
||||
const sampleContent = `
|
||||
<p><strong>1. AI 모델 정확도</strong></p>
|
||||
<p>- 현재 STT 정확도: 92%</p>
|
||||
<p>- 목표 정확도: 95% 이상</p>
|
||||
<br>
|
||||
<p><strong>2. 사용자 인터페이스</strong></p>
|
||||
<p>- Mobile First 디자인 채택</p>
|
||||
<p>- 실시간 협업 기능 필수</p>
|
||||
`;
|
||||
$('#sectionContent').innerHTML = sampleContent;
|
||||
}
|
||||
|
||||
openModal('sectionModal');
|
||||
}
|
||||
|
||||
// 섹션 내용 렌더링
|
||||
function renderSectionContent(index, editMode) {
|
||||
const data = sectionData[index];
|
||||
const contentDiv = $('#sectionContent');
|
||||
|
||||
if (editMode && index === 0) {
|
||||
// 회의 개요 편집 모드
|
||||
contentDiv.innerHTML = `
|
||||
<div>
|
||||
<label class="edit-label">회의 목적</label>
|
||||
<input type="text" class="edit-field" id="edit_purpose" value="${data.purpose}">
|
||||
</div>
|
||||
<div>
|
||||
<label class="edit-label">참석자</label>
|
||||
<input type="text" class="edit-field" id="edit_attendees" value="${data.attendees}">
|
||||
</div>
|
||||
<div>
|
||||
<label class="edit-label">일시</label>
|
||||
<input type="text" class="edit-field" id="edit_datetime" value="${data.datetime}">
|
||||
</div>
|
||||
<div>
|
||||
<label class="edit-label">장소</label>
|
||||
<input type="text" class="edit-field" id="edit_location" value="${data.location}">
|
||||
</div>
|
||||
`;
|
||||
} else if (index === 0) {
|
||||
// 회의 개요 보기 모드
|
||||
contentDiv.innerHTML = `
|
||||
<p><strong>회의 목적:</strong> ${data.purpose}</p>
|
||||
<p><strong>참석자:</strong> ${data.attendees}</p>
|
||||
<p><strong>일시:</strong> ${data.datetime}</p>
|
||||
<p><strong>장소:</strong> ${data.location}</p>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
// 섹션 검증
|
||||
function verifySection(index) {
|
||||
currentSectionIndex = index;
|
||||
const section = sectionVerifications[index];
|
||||
|
||||
$('#verifyMessage').textContent = `"${section.name}" 섹션의 내용을 검증하시겠습니까?`;
|
||||
openModal('verifyModal');
|
||||
}
|
||||
|
||||
// 검증 확인
|
||||
function confirmVerification() {
|
||||
const section = sectionVerifications[currentSectionIndex];
|
||||
section.verified = true;
|
||||
|
||||
if (!section.verifiers.includes('김민준')) {
|
||||
section.verifiers.push('김민준');
|
||||
}
|
||||
|
||||
closeModal('verifyModal');
|
||||
showToast(`"${section.name}" 섹션이 검증되었습니다`, 'success');
|
||||
|
||||
// 화면 새로고침 시뮬레이션
|
||||
setTimeout(() => {
|
||||
location.reload();
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
// 섹션 잠금 해제
|
||||
function unlockSection(index) {
|
||||
currentSectionIndex = index;
|
||||
openModal('unlockModal');
|
||||
}
|
||||
|
||||
// 잠금 해제 확인
|
||||
function confirmUnlock() {
|
||||
const section = sectionVerifications[currentSectionIndex];
|
||||
section.locked = false;
|
||||
|
||||
closeModal('unlockModal');
|
||||
showToast(`"${section.name}" 섹션의 잠금이 해제되었습니다`, 'success');
|
||||
|
||||
// 화면 새로고침 시뮬레이션
|
||||
setTimeout(() => {
|
||||
location.reload();
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
// 편집 모드 토글
|
||||
function toggleEditMode() {
|
||||
if (currentSectionIndex !== 0) {
|
||||
// 회의 개요가 아닌 경우
|
||||
closeModal('sectionModal');
|
||||
showToast('이 섹션은 회의록수정 화면에서 수정할 수 있습니다', 'info');
|
||||
return;
|
||||
}
|
||||
|
||||
isEditMode = !isEditMode;
|
||||
|
||||
if (isEditMode) {
|
||||
// 편집 모드로 전환
|
||||
renderSectionContent(currentSectionIndex, true);
|
||||
$('#editBtn').style.display = 'none';
|
||||
$('#saveBtn').style.display = 'inline-block';
|
||||
} else {
|
||||
// 보기 모드로 전환
|
||||
renderSectionContent(currentSectionIndex, false);
|
||||
$('#editBtn').style.display = 'inline-block';
|
||||
$('#saveBtn').style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// 섹션 저장
|
||||
function saveSection() {
|
||||
if (currentSectionIndex !== 0) return;
|
||||
|
||||
// 편집된 값 가져오기
|
||||
const updatedData = {
|
||||
purpose: $('#edit_purpose').value,
|
||||
attendees: $('#edit_attendees').value,
|
||||
datetime: $('#edit_datetime').value,
|
||||
location: $('#edit_location').value
|
||||
};
|
||||
|
||||
// 데이터 업데이트
|
||||
sectionData[currentSectionIndex] = updatedData;
|
||||
|
||||
// 보기 모드로 전환
|
||||
isEditMode = false;
|
||||
renderSectionContent(currentSectionIndex, false);
|
||||
$('#editBtn').style.display = 'inline-block';
|
||||
$('#saveBtn').style.display = 'none';
|
||||
|
||||
// 성공 메시지
|
||||
showToast('회의 개요가 저장되었습니다', 'success');
|
||||
}
|
||||
|
||||
// 모두 검증 완료
|
||||
function completeAllVerification() {
|
||||
if (confirm('모든 섹션 검증을 완료하고 회의록을 확정하시겠습니까?')) {
|
||||
showToast('회의록이 최종 확정되었습니다', 'success');
|
||||
|
||||
// 회의 종료 화면 또는 대시보드로 이동
|
||||
setTimeout(() => {
|
||||
alert('회의록이 확정되었습니다.\n참석자들에게 알림이 전송되었습니다.');
|
||||
// navigateTo('01-대시보드.html');
|
||||
}, 1500);
|
||||
}
|
||||
}
|
||||
|
||||
// 나중에 하기
|
||||
function saveLater() {
|
||||
if (confirm('검증을 나중에 완료하시겠습니까?\n회의록은 임시 저장됩니다.')) {
|
||||
// 회의록 상태를 '작성중'으로 저장
|
||||
// 실제로는 Meeting Service API 호출하여 임시 저장
|
||||
showToast('회의록이 임시 저장되었습니다', 'info');
|
||||
|
||||
// 대시보드로 이동
|
||||
setTimeout(() => {
|
||||
navigateTo('02-대시보드.html');
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
// 초기 진행률 업데이트
|
||||
updateProgress();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
+178
-230
@@ -25,92 +25,85 @@
|
||||
color: var(--gray-700);
|
||||
}
|
||||
|
||||
/* 통계 카드 그리드 */
|
||||
/* 통계 카드 그리드 - 10-회의록상세조회와 동일한 디자인 */
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: var(--space-md);
|
||||
margin-bottom: var(--space-lg);
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: var(--white);
|
||||
padding: var(--space-md);
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow-md);
|
||||
/* 모바일에서 gap 축소 */
|
||||
@media (max-width: 600px) {
|
||||
.stats-grid {
|
||||
gap: var(--space-xs);
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
padding: var(--space-sm);
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: var(--font-base);
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: var(--font-xs);
|
||||
}
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
text-align: center;
|
||||
padding: var(--space-lg);
|
||||
background: var(--gray-100);
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: var(--font-h1);
|
||||
font-weight: var(--font-weight-bold);
|
||||
color: var(--primary);
|
||||
margin-bottom: var(--space-xs);
|
||||
margin-bottom: var(--space-sm);
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: var(--font-small);
|
||||
color: var(--gray-500);
|
||||
}
|
||||
|
||||
/* 키워드 클라우드 */
|
||||
.keyword-cloud {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--space-sm);
|
||||
padding: var(--space-md);
|
||||
}
|
||||
|
||||
.keyword-tag {
|
||||
display: inline-block;
|
||||
padding: 6px 12px;
|
||||
background: var(--primary-light);
|
||||
color: var(--primary-dark);
|
||||
border-radius: 16px;
|
||||
font-size: var(--font-small);
|
||||
font-size: var(--font-body);
|
||||
color: var(--gray-600);
|
||||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
|
||||
/* 발언 통계 바 차트 */
|
||||
.speaker-stats {
|
||||
padding: var(--space-md) 0;
|
||||
/* 키워드 섹션 - 10-회의록상세조회와 동일한 디자인 */
|
||||
.keywords-section {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.speaker-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-md);
|
||||
.keywords-title {
|
||||
font-size: var(--font-h4);
|
||||
font-weight: var(--font-weight-bold);
|
||||
color: var(--gray-900);
|
||||
margin-bottom: var(--space-md);
|
||||
}
|
||||
|
||||
.speaker-info {
|
||||
.keyword-tags {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--space-sm);
|
||||
min-width: 120px;
|
||||
margin: var(--space-md) 0;
|
||||
}
|
||||
|
||||
.speaker-bar-container {
|
||||
flex: 1;
|
||||
height: 32px;
|
||||
background: var(--gray-100);
|
||||
.keyword-tag {
|
||||
padding: 6px 12px;
|
||||
background: var(--primary-light);
|
||||
color: var(--primary);
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
font-size: var(--font-small);
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.speaker-bar {
|
||||
height: 100%;
|
||||
.keyword-tag:hover {
|
||||
background: var(--primary);
|
||||
border-radius: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
padding-right: var(--space-sm);
|
||||
color: var(--white);
|
||||
font-size: var(--font-caption);
|
||||
font-weight: var(--font-weight-bold);
|
||||
transition: width 1s ease;
|
||||
}
|
||||
|
||||
/* 안건 카드 */
|
||||
@@ -182,99 +175,69 @@
|
||||
}
|
||||
|
||||
.agenda-section {
|
||||
margin-bottom: var(--space-md);
|
||||
margin-bottom: var(--space-lg);
|
||||
padding-bottom: var(--space-md);
|
||||
border-bottom: 1px solid var(--gray-200);
|
||||
}
|
||||
|
||||
.agenda-section:last-child {
|
||||
border-bottom: none;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.agenda-section-title {
|
||||
font-size: var(--font-small);
|
||||
font-weight: var(--font-weight-bold);
|
||||
color: var(--gray-500);
|
||||
margin-bottom: var(--space-xs);
|
||||
text-transform: uppercase;
|
||||
color: var(--gray-900);
|
||||
margin-bottom: var(--space-sm);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-xs);
|
||||
}
|
||||
|
||||
.agenda-section-title::before {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
width: 4px;
|
||||
height: 16px;
|
||||
background: var(--primary);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.agenda-section-content {
|
||||
font-size: var(--font-body);
|
||||
color: var(--gray-700);
|
||||
line-height: 1.6;
|
||||
padding-left: var(--space-md);
|
||||
}
|
||||
|
||||
/* Todo 리스트 (읽기 전용) */
|
||||
.todo-list-item {
|
||||
padding: var(--space-md);
|
||||
background: var(--gray-50);
|
||||
border-radius: var(--radius-md);
|
||||
margin-bottom: var(--space-sm);
|
||||
opacity: 0.8;
|
||||
.agenda-section-content ul {
|
||||
margin: 0;
|
||||
padding-left: var(--space-lg);
|
||||
}
|
||||
|
||||
.todo-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: var(--space-sm);
|
||||
.agenda-section-content li {
|
||||
margin-bottom: var(--space-xs);
|
||||
}
|
||||
|
||||
.todo-checkbox {
|
||||
margin-right: var(--space-sm);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.todo-content {
|
||||
flex: 1;
|
||||
font-weight: var(--font-weight-medium);
|
||||
color: var(--gray-700);
|
||||
}
|
||||
|
||||
.todo-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-md);
|
||||
font-size: var(--font-small);
|
||||
color: var(--gray-500);
|
||||
}
|
||||
|
||||
/* 하단 액션 바 - 3개 버튼 배치 */
|
||||
.action-bar {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: var(--white);
|
||||
padding: var(--space-md);
|
||||
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.1);
|
||||
display: flex;
|
||||
gap: var(--space-sm);
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.action-bar .btn {
|
||||
/* 하단 버튼 비율 조정 (1:2:1) */
|
||||
.action-bar .btn:nth-child(1) {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.action-bar .btn-primary {
|
||||
flex: 2; /* 바로 최종 확정 버튼 강조 */
|
||||
.action-bar .btn:nth-child(2) {
|
||||
flex: 2;
|
||||
}
|
||||
|
||||
.action-bar .btn:nth-child(3) {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* 데스크톱 반응형 */
|
||||
@media (min-width: 768px) {
|
||||
.stats-grid {
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
}
|
||||
|
||||
.action-bar {
|
||||
position: static;
|
||||
box-shadow: none;
|
||||
justify-content: center;
|
||||
gap: var(--space-md);
|
||||
}
|
||||
|
||||
.action-bar .btn {
|
||||
flex: 0 1 200px;
|
||||
}
|
||||
|
||||
.action-bar .btn-primary {
|
||||
flex: 0 1 250px;
|
||||
}
|
||||
}
|
||||
|
||||
.readonly-notice {
|
||||
@@ -303,44 +266,42 @@
|
||||
🔒 이 화면은 <strong>확인 전용</strong>입니다. 내용을 수정하려면 "회의록 수정" 버튼을 클릭하세요.
|
||||
</div>
|
||||
|
||||
<!-- 통계 카드 그리드 -->
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<div class="stat-value" id="durationValue">0</div>
|
||||
<div class="stat-label">회의 시간 (분)</div>
|
||||
<!-- 회의 통계 및 키워드 카드 -->
|
||||
<div class="card mb-lg">
|
||||
<!-- 통계 카드 그리드 -->
|
||||
<div class="stats-grid">
|
||||
<div class="stat-item">
|
||||
<div class="stat-value" id="participantsValue">4명</div>
|
||||
<div class="stat-label">참석자</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-value" id="durationValue">90분</div>
|
||||
<div class="stat-label">회의 시간</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-value" id="agendasValue">3개</div>
|
||||
<div class="stat-label">주요 안건</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-value" id="todosValue">5개</div>
|
||||
<div class="stat-label">Todo 생성</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-value" id="participantsValue">0</div>
|
||||
<div class="stat-label">참석자</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-value" id="agendasValue">0</div>
|
||||
<div class="stat-label">안건</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-value" id="todosValue">0</div>
|
||||
<div class="stat-label">Todo</div>
|
||||
|
||||
<!-- 주요 키워드 -->
|
||||
<div class="keywords-section">
|
||||
<h3 class="keywords-title">주요 키워드</h3>
|
||||
<div class="keyword-tags">
|
||||
<span class="keyword-tag">#신제품기획</span>
|
||||
<span class="keyword-tag">#예산편성</span>
|
||||
<span class="keyword-tag">#일정조율</span>
|
||||
<span class="keyword-tag">#시장조사</span>
|
||||
<span class="keyword-tag">#UI/UX</span>
|
||||
<span class="keyword-tag">#개발스펙</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 주요 키워드 -->
|
||||
<div class="card mb-md">
|
||||
<h3 class="card-title">주요 키워드</h3>
|
||||
<div class="keyword-cloud">
|
||||
<span class="keyword-tag">신제품 기획</span>
|
||||
<span class="keyword-tag">예산 편성</span>
|
||||
<span class="keyword-tag">일정 조율</span>
|
||||
<span class="keyword-tag">시장 조사</span>
|
||||
<span class="keyword-tag">UI/UX</span>
|
||||
<span class="keyword-tag">개발 스펙</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 발언 통계 -->
|
||||
<div class="card mb-md">
|
||||
<h3 class="card-title">발언 통계</h3>
|
||||
<div class="speaker-stats" id="speakerStats"></div>
|
||||
</div>
|
||||
|
||||
<!-- 안건별 AI 요약 -->
|
||||
<div class="card mb-md">
|
||||
@@ -352,7 +313,7 @@
|
||||
|
||||
<!-- 하단 액션 바 (3가지 선택 옵션) -->
|
||||
<div class="action-bar">
|
||||
<button class="btn btn-ghost" onclick="navigateTo('02-대시보드.html')">
|
||||
<button class="btn btn-neutral" onclick="navigateTo('02-대시보드.html')">
|
||||
대시보드
|
||||
</button>
|
||||
<button class="btn btn-secondary" onclick="navigateTo('11-회의록수정.html')">
|
||||
@@ -459,9 +420,6 @@
|
||||
const totalTodos = SAMPLE_AGENDAS.reduce((sum, agenda) => sum + (agenda.todos?.length || 0), 0);
|
||||
animateCounter('todosValue', totalTodos);
|
||||
|
||||
// 발언 통계 렌더링
|
||||
renderSpeakerStats();
|
||||
|
||||
// 안건 리스트 렌더링
|
||||
renderAgendaList();
|
||||
}
|
||||
@@ -482,42 +440,6 @@
|
||||
}, 30);
|
||||
}
|
||||
|
||||
// 발언 통계 렌더링
|
||||
function renderSpeakerStats() {
|
||||
const stats = [
|
||||
{ user: SAMPLE_MEETINGS[0].participants[0], count: 15, duration: 35 },
|
||||
{ user: SAMPLE_MEETINGS[0].participants[1], count: 12, duration: 28 },
|
||||
{ user: SAMPLE_MEETINGS[0].participants[2], count: 10, duration: 20 },
|
||||
{ user: SAMPLE_MEETINGS[0].participants[3], count: 8, duration: 17 }
|
||||
];
|
||||
|
||||
const maxDuration = Math.max(...stats.map(s => s.duration));
|
||||
const container = $('#speakerStats');
|
||||
|
||||
stats.forEach(stat => {
|
||||
const percentage = (stat.duration / maxDuration) * 100;
|
||||
const item = createElement('div', { className: 'speaker-item' }, `
|
||||
<div class="speaker-info">
|
||||
${createAvatar(stat.user, 'sm')}
|
||||
<span class="text-small">${stat.user.name}</span>
|
||||
</div>
|
||||
<div class="speaker-bar-container">
|
||||
<div class="speaker-bar" style="width: 0%;" data-width="${percentage}">
|
||||
${stat.duration}분
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
container.appendChild(item);
|
||||
});
|
||||
|
||||
// 애니메이션 시작
|
||||
setTimeout(() => {
|
||||
$$('.speaker-bar').forEach(bar => {
|
||||
bar.style.width = bar.dataset.width + '%';
|
||||
});
|
||||
}, 100);
|
||||
}
|
||||
|
||||
// 안건 리스트 렌더링
|
||||
function renderAgendaList() {
|
||||
const container = $('#agendaList');
|
||||
@@ -554,59 +476,85 @@
|
||||
`));
|
||||
}
|
||||
|
||||
// 발언자별 의견
|
||||
if (agenda.details.opinions && agenda.details.opinions.length > 0) {
|
||||
const opinionsHtml = agenda.details.opinions.map(op =>
|
||||
`<li><strong>${op.speaker}:</strong> ${op.opinion}</li>`
|
||||
).join('');
|
||||
content.appendChild(createElement('div', { className: 'agenda-section' }, `
|
||||
<div class="agenda-section-title">발언자별 의견</div>
|
||||
<div class="agenda-section-content"><ul>${opinionsHtml}</ul></div>
|
||||
`));
|
||||
}
|
||||
|
||||
// 결정 사항
|
||||
if (agenda.details.decisions && agenda.details.decisions.length > 0) {
|
||||
const decisionsHtml = agenda.details.decisions.map(d => `<li>✓ ${d}</li>`).join('');
|
||||
content.appendChild(createElement('div', { className: 'agenda-section' }, `
|
||||
const decisionsSection = createElement('div', { className: 'agenda-section' }, `
|
||||
<div class="agenda-section-title">결정 사항</div>
|
||||
<div class="agenda-section-content"><ul>${decisionsHtml}</ul></div>
|
||||
`));
|
||||
`);
|
||||
|
||||
const decisionsList = createElement('ul', {
|
||||
className: 'agenda-section-content',
|
||||
style: 'list-style: none; padding: 0; margin: 0; padding-left: var(--space-md);'
|
||||
});
|
||||
|
||||
agenda.details.decisions.forEach(decision => {
|
||||
const decisionItem = createElement('li', {
|
||||
style: 'display: flex; align-items: flex-start; gap: var(--space-sm); padding: var(--space-xs) 0; font-size: var(--font-body); color: var(--gray-700);'
|
||||
}, `
|
||||
<span style="color: var(--gray-500); margin-top: 2px;">•</span>
|
||||
<span style="flex: 1;">${decision}</span>
|
||||
`);
|
||||
decisionsList.appendChild(decisionItem);
|
||||
});
|
||||
|
||||
decisionsSection.appendChild(decisionsList);
|
||||
content.appendChild(decisionsSection);
|
||||
}
|
||||
|
||||
// 보류 사항
|
||||
if (agenda.details.pending && agenda.details.pending.length > 0) {
|
||||
const pendingHtml = agenda.details.pending.map(p => `<li>⏸ ${p}</li>`).join('');
|
||||
content.appendChild(createElement('div', { className: 'agenda-section' }, `
|
||||
const pendingSection = createElement('div', { className: 'agenda-section' }, `
|
||||
<div class="agenda-section-title">보류 사항</div>
|
||||
<div class="agenda-section-content"><ul>${pendingHtml}</ul></div>
|
||||
`));
|
||||
`);
|
||||
|
||||
const pendingList = createElement('ul', {
|
||||
className: 'agenda-section-content',
|
||||
style: 'list-style: none; padding: 0; margin: 0; padding-left: var(--space-md);'
|
||||
});
|
||||
|
||||
agenda.details.pending.forEach(pending => {
|
||||
const pendingItem = createElement('li', {
|
||||
style: 'display: flex; align-items: flex-start; gap: var(--space-sm); padding: var(--space-xs) 0; font-size: var(--font-body); color: var(--gray-700);'
|
||||
}, `
|
||||
<span style="color: var(--gray-500); margin-top: 2px;">•</span>
|
||||
<span style="flex: 1;">${pending}</span>
|
||||
`);
|
||||
pendingList.appendChild(pendingItem);
|
||||
});
|
||||
|
||||
pendingSection.appendChild(pendingList);
|
||||
content.appendChild(pendingSection);
|
||||
}
|
||||
|
||||
// Todo 목록
|
||||
// Todo 목록 - 제목만 간단히 표시
|
||||
if (agenda.todos && agenda.todos.length > 0) {
|
||||
const todosSection = createElement('div', { className: 'agenda-section' }, `
|
||||
<div class="agenda-section-title">Todo 자동 추출 결과</div>
|
||||
`);
|
||||
|
||||
agenda.todos.forEach(todo => {
|
||||
const todoItem = createElement('div', { className: 'todo-list-item' }, `
|
||||
<div class="todo-header">
|
||||
<input type="checkbox" class="todo-checkbox" disabled>
|
||||
<div class="todo-content">${todo.title}</div>
|
||||
${createBadge(todo.priority === 'high' ? '높음' : todo.priority === 'medium' ? '보통' : '낮음',
|
||||
`priority-${todo.priority}`)}
|
||||
</div>
|
||||
<div class="todo-meta">
|
||||
${createAvatar(todo.assignee, 'sm')}
|
||||
<span>${todo.assignee.name}</span>
|
||||
<span>•</span>
|
||||
<span>${formatDate(todo.dueDate)}</span>
|
||||
</div>
|
||||
`);
|
||||
todosSection.appendChild(todoItem);
|
||||
const todoList = createElement('ul', {
|
||||
className: 'agenda-section-content',
|
||||
style: 'list-style: none; padding: 0; margin: 0; padding-left: var(--space-md);'
|
||||
});
|
||||
|
||||
agenda.todos.forEach(todo => {
|
||||
const todoItem = createElement('li', {
|
||||
style: 'display: flex; align-items: flex-start; gap: var(--space-sm); padding: var(--space-xs) 0; font-size: var(--font-body); color: var(--gray-700);'
|
||||
}, `
|
||||
<span style="color: var(--gray-500); margin-top: 2px;">•</span>
|
||||
<span style="flex: 1;">${todo.title}</span>
|
||||
`);
|
||||
todoList.appendChild(todoItem);
|
||||
});
|
||||
|
||||
todosSection.appendChild(todoList);
|
||||
|
||||
// 안내 문구 추가
|
||||
const notice = createElement('p', {
|
||||
style: 'font-size: var(--font-small); color: var(--gray-500); margin-top: var(--space-md); padding-top: var(--space-sm); border-top: 1px solid var(--gray-200);'
|
||||
}, '💡 담당자 및 마감일은 회의록 수정 화면에서 지정할 수 있습니다.');
|
||||
todosSection.appendChild(notice);
|
||||
|
||||
content.appendChild(todosSection);
|
||||
}
|
||||
|
||||
|
||||
@@ -165,6 +165,23 @@
|
||||
.participant {
|
||||
width: calc(50% - var(--space-md) / 2);
|
||||
}
|
||||
|
||||
/* 통계 그리드: 모바일에서도 4열 유지, gap만 축소 */
|
||||
.stats-grid {
|
||||
gap: var(--space-xs);
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
padding: var(--space-sm);
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: var(--font-base);
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: var(--font-xs);
|
||||
}
|
||||
}
|
||||
|
||||
/* 회의록 섹션 */
|
||||
@@ -333,9 +350,6 @@
|
||||
}
|
||||
|
||||
/* 대시보드 탭 콘텐츠 */
|
||||
.dashboard-section {
|
||||
margin-bottom: var(--space-lg);
|
||||
}
|
||||
|
||||
.key-points {
|
||||
list-style: none;
|
||||
@@ -393,7 +407,7 @@
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: var(--space-md);
|
||||
margin-top: var(--space-md);
|
||||
}
|
||||
@@ -724,7 +738,11 @@
|
||||
<!-- 기본 정보 카드 -->
|
||||
<div class="info-card">
|
||||
<div class="meeting-basic-info">
|
||||
<h2>2025년 1분기 제품 기획 회의</h2>
|
||||
<div id="meeting-title-container" style="display: flex; align-items: center; gap: var(--space-sm); margin-bottom: var(--space-sm);">
|
||||
<span class="badge badge-complete">확정완료</span>
|
||||
<!-- 생성자일 경우 👑 아이콘이 JavaScript로 추가됨 -->
|
||||
<h2 style="margin: 0;">2025년 1분기 제품 기획 회의</h2>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="info-icon">📅</span>
|
||||
<span>2025년 10월 25일 14:00 (90분)</span>
|
||||
@@ -733,10 +751,6 @@
|
||||
<span class="info-icon">📍</span>
|
||||
<span>본사 2층 대회의실</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="info-icon">✅</span>
|
||||
<span class="badge badge-complete">확정완료</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="participants-list">
|
||||
@@ -951,10 +965,8 @@
|
||||
<!-- 대시보드 탭 (기본 노출 탭 - 유저스토리 UFR-MEET-047 요구사항) -->
|
||||
<div id="dashboard-content" class="tab-content active">
|
||||
<!-- 핵심내용 -->
|
||||
<div class="section dashboard-section">
|
||||
<div class="section-header">
|
||||
<h3 class="section-title">💡 핵심내용</h3>
|
||||
</div>
|
||||
<div class="card mb-lg">
|
||||
<h3 class="card-title">💡 핵심내용</h3>
|
||||
|
||||
<ol class="key-points">
|
||||
<li class="key-point">
|
||||
@@ -1004,10 +1016,8 @@
|
||||
</div>
|
||||
|
||||
<!-- 결정사항 -->
|
||||
<div class="section dashboard-section">
|
||||
<div class="section-header">
|
||||
<h3 class="section-title">✅ 결정사항</h3>
|
||||
</div>
|
||||
<div class="card mb-lg">
|
||||
<h3 class="card-title">✅ 결정사항</h3>
|
||||
|
||||
<div class="decision-card">
|
||||
<div class="decision-content">베타 버전 출시일: 2025년 12월 1일</div>
|
||||
@@ -1033,10 +1043,8 @@
|
||||
</div>
|
||||
|
||||
<!-- Todo 진행상황 -->
|
||||
<div class="section dashboard-section">
|
||||
<div class="section-header">
|
||||
<h3 class="section-title">📋 Todo 진행상황</h3>
|
||||
</div>
|
||||
<div class="card mb-lg">
|
||||
<h3 class="card-title">📋 Todo 진행상황</h3>
|
||||
|
||||
<!-- 전체 진행률 -->
|
||||
<div style="margin-bottom: var(--space-lg);">
|
||||
@@ -1182,10 +1190,8 @@
|
||||
</div>
|
||||
|
||||
<!-- 관련회의록 -->
|
||||
<div class="section dashboard-section">
|
||||
<div class="section-header">
|
||||
<h3 class="section-title">📚 관련회의록 (3건)</h3>
|
||||
</div>
|
||||
<div class="card mb-lg">
|
||||
<h3 class="card-title">📚 관련회의록 (3건)</h3>
|
||||
|
||||
<div class="reference-item" onclick="window.open('10-회의록상세조회.html', '_blank')">
|
||||
<div class="reference-header">
|
||||
@@ -1729,6 +1735,20 @@
|
||||
* 페이지 초기화
|
||||
*/
|
||||
function initPage() {
|
||||
// 회의 생성자 확인 후 👑 표시
|
||||
const currentUser = '김민준'; // 현재 로그인 사용자
|
||||
const isCreator = checkIfUserIsCreator(CURRENT_MEETING_ID, currentUser);
|
||||
|
||||
if (isCreator) {
|
||||
const titleContainer = document.getElementById('meeting-title-container');
|
||||
const badge = titleContainer.querySelector('.badge');
|
||||
const crownIcon = document.createElement('span');
|
||||
crownIcon.textContent = '👑';
|
||||
crownIcon.style.fontSize = '24px';
|
||||
// badge 다음에 👑 삽입
|
||||
badge.insertAdjacentElement('afterend', crownIcon);
|
||||
}
|
||||
|
||||
updateTodoProgress();
|
||||
updateFilterCounts();
|
||||
}
|
||||
|
||||
@@ -604,7 +604,7 @@
|
||||
const statusBadge = minute.status === 'complete' ?
|
||||
'<span class="badge badge-complete">확정완료</span>' :
|
||||
'<span class="badge badge-draft">작성중</span>';
|
||||
const crownEmoji = isCreator ? '<span style="font-size: 16px; flex-shrink: 0;" title="생성자">👑</span>' : '';
|
||||
const creatorBadge = isCreator ? '<span class="creator-badge" title="생성자">👑</span>' : '';
|
||||
|
||||
// 검증완료율 실시간 계산 (작성중 상태일 때만 표시)
|
||||
const completionRate = minute.status === 'draft'
|
||||
@@ -615,7 +615,7 @@
|
||||
<div class="meeting-item" data-status="${minute.status}" data-type="${participationType}" data-date="${minute.date}" onclick="navigateTo('10-회의록상세조회.html')">
|
||||
<div class="meeting-header">
|
||||
${statusBadge}
|
||||
${crownEmoji}
|
||||
${creatorBadge}
|
||||
<h3 class="meeting-title">${minute.title}</h3>
|
||||
</div>
|
||||
<div class="meeting-meta">
|
||||
|
||||
@@ -179,6 +179,7 @@ a:hover {
|
||||
font-size: var(--font-h3);
|
||||
font-weight: var(--font-weight-bold);
|
||||
color: var(--gray-900);
|
||||
margin-bottom: var(--space-md);
|
||||
}
|
||||
|
||||
.card-body {
|
||||
@@ -258,6 +259,28 @@ a:hover {
|
||||
background: var(--gray-100);
|
||||
}
|
||||
|
||||
/* Outline Button (회색 테두리) */
|
||||
.btn-outline {
|
||||
background: var(--white);
|
||||
color: var(--gray-700);
|
||||
border: 1px solid var(--gray-500);
|
||||
}
|
||||
|
||||
.btn-outline:hover:not(:disabled) {
|
||||
background: var(--gray-50);
|
||||
border-color: var(--gray-600);
|
||||
}
|
||||
|
||||
/* Neutral Button (진한 회색 배경) */
|
||||
.btn-neutral {
|
||||
background: #424242;
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
.btn-neutral:hover:not(:disabled) {
|
||||
background: var(--gray-900);
|
||||
}
|
||||
|
||||
/* Error Button */
|
||||
.btn-error {
|
||||
background: var(--error);
|
||||
@@ -372,6 +395,21 @@ a:hover {
|
||||
color: var(--gray-700);
|
||||
}
|
||||
|
||||
/* Creator Badge (생성자 표시) */
|
||||
.creator-badge {
|
||||
font-size: 16px;
|
||||
flex-shrink: 0;
|
||||
margin-left: 4px;
|
||||
cursor: default;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.creator-badge[title] {
|
||||
cursor: help;
|
||||
}
|
||||
|
||||
/* Priority Badges */
|
||||
.badge-high {
|
||||
background: #FFEBEE;
|
||||
@@ -1238,9 +1276,9 @@ input[type="date"]::-webkit-calendar-picker-indicator {
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
22. 섹션 카드 컴포넌트
|
||||
22. 안건 카드 컴포넌트
|
||||
======================================== */
|
||||
.section {
|
||||
.agenda {
|
||||
background: var(--white);
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow-md);
|
||||
@@ -1248,14 +1286,14 @@ input[type="date"]::-webkit-calendar-picker-indicator {
|
||||
margin-bottom: var(--space-lg);
|
||||
}
|
||||
|
||||
.section-header {
|
||||
.agenda-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: var(--space-md);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
.agenda-title {
|
||||
font-size: var(--font-h3);
|
||||
font-weight: var(--font-weight-bold);
|
||||
color: var(--gray-900);
|
||||
@@ -1264,7 +1302,7 @@ input[type="date"]::-webkit-calendar-picker-indicator {
|
||||
gap: var(--space-sm);
|
||||
}
|
||||
|
||||
.section-content {
|
||||
.agenda-content {
|
||||
color: var(--gray-700);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user