mirror of
https://github.com/hwanny1128/HGZero.git
synced 2025-12-06 20:46:23 +00:00
- 가파팀 프로토타입 파일 삭제 - 가파팀 유저스토리 삭제 - 실시간 회의록 작성 플로우 설계서 추가 (Mermaid, Markdown) - 백업 및 데이터 디렉토리 추가 - AI 데이터 샘플 생성 도구 추가 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
846 lines
27 KiB
HTML
846 lines
27 KiB
HTML
<!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">
|
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200">
|
|
<style>
|
|
.auto-save-indicator {
|
|
position: fixed;
|
|
top: 70px;
|
|
right: 16px;
|
|
padding: 8px 12px;
|
|
background: var(--color-white);
|
|
border-radius: 20px;
|
|
box-shadow: var(--shadow-sm);
|
|
font-size: 12px;
|
|
color: var(--color-gray-600);
|
|
z-index: var(--z-sticky);
|
|
display: none;
|
|
}
|
|
|
|
.auto-save-indicator.active {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
}
|
|
|
|
/* NEW - UFR-MEET-055: 섹션 잠금 해제 버튼 스타일 */
|
|
.section-lock-area {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
padding: 12px;
|
|
background: var(--color-gray-50);
|
|
border-radius: 8px;
|
|
margin-top: 12px;
|
|
}
|
|
|
|
.btn-unlock {
|
|
padding: 6px 12px;
|
|
font-size: 14px;
|
|
background: var(--color-primary-main);
|
|
color: var(--color-white);
|
|
border: none;
|
|
border-radius: 6px;
|
|
cursor: pointer;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
}
|
|
|
|
.btn-unlock:hover {
|
|
background: var(--color-primary-dark);
|
|
}
|
|
|
|
/* NEW - UFR-COLLAB-020: 충돌 해결 UI 스타일 */
|
|
.conflict-banner {
|
|
position: fixed;
|
|
top: 60px;
|
|
left: 16px;
|
|
right: 16px;
|
|
padding: 12px 16px;
|
|
background: rgba(239, 68, 68, 0.1);
|
|
border: 1px solid #EF4444;
|
|
border-radius: 8px;
|
|
z-index: var(--z-sticky);
|
|
display: none;
|
|
align-items: center;
|
|
gap: 12px;
|
|
}
|
|
|
|
.conflict-banner.active {
|
|
display: flex;
|
|
}
|
|
|
|
.conflict-icon {
|
|
color: #EF4444;
|
|
font-size: 24px;
|
|
}
|
|
|
|
.conflict-content {
|
|
flex: 1;
|
|
}
|
|
|
|
.conflict-title {
|
|
font-weight: 600;
|
|
color: #B91C1C;
|
|
margin-bottom: 4px;
|
|
}
|
|
|
|
.conflict-description {
|
|
font-size: 12px;
|
|
color: #DC2626;
|
|
}
|
|
|
|
.btn-resolve {
|
|
padding: 6px 12px;
|
|
font-size: 14px;
|
|
background: #EF4444;
|
|
color: var(--color-white);
|
|
border: none;
|
|
border-radius: 6px;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.btn-resolve:hover {
|
|
background: #DC2626;
|
|
}
|
|
|
|
/* 충돌 해결 모달 스타일 */
|
|
.conflict-resolution {
|
|
padding: 0;
|
|
}
|
|
|
|
.conflict-header {
|
|
padding: 20px;
|
|
background: rgba(239, 68, 68, 0.1);
|
|
border-bottom: 1px solid var(--color-gray-200);
|
|
}
|
|
|
|
.conflict-body {
|
|
padding: 20px;
|
|
}
|
|
|
|
.conflict-section {
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.conflict-label {
|
|
font-weight: 600;
|
|
font-size: 14px;
|
|
color: var(--color-gray-700);
|
|
margin-bottom: 8px;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
}
|
|
|
|
.conflict-diff {
|
|
padding: 12px;
|
|
background: var(--color-gray-50);
|
|
border-radius: 8px;
|
|
border: 2px solid transparent;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
}
|
|
|
|
.conflict-diff:hover {
|
|
border-color: var(--color-primary-light);
|
|
}
|
|
|
|
.conflict-diff.selected {
|
|
border-color: var(--color-primary-main);
|
|
background: rgba(0, 217, 177, 0.1);
|
|
}
|
|
|
|
.conflict-user {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
font-size: 12px;
|
|
color: var(--color-gray-600);
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.conflict-time {
|
|
font-size: 11px;
|
|
color: var(--color-gray-500);
|
|
}
|
|
|
|
.conflict-content-box {
|
|
padding: 12px;
|
|
background: var(--color-white);
|
|
border-radius: 6px;
|
|
font-size: 14px;
|
|
line-height: 1.6;
|
|
white-space: pre-wrap;
|
|
word-break: break-word;
|
|
}
|
|
|
|
.conflict-actions {
|
|
display: flex;
|
|
gap: 8px;
|
|
padding: 20px;
|
|
border-top: 1px solid var(--color-gray-200);
|
|
}
|
|
|
|
/* 직접 작성 모드 */
|
|
.merge-editor {
|
|
width: 100%;
|
|
min-height: 150px;
|
|
padding: 12px;
|
|
border: 1px solid var(--color-gray-300);
|
|
border-radius: 8px;
|
|
font-family: inherit;
|
|
font-size: 14px;
|
|
resize: vertical;
|
|
}
|
|
|
|
.merge-editor:focus {
|
|
outline: none;
|
|
border-color: var(--color-primary-main);
|
|
}
|
|
|
|
/* 충돌 표시 배지 */
|
|
.conflict-badge {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
padding: 4px 8px;
|
|
background: rgba(239, 68, 68, 0.2);
|
|
color: #B91C1C;
|
|
border-radius: 12px;
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="page">
|
|
<!-- 헤더 -->
|
|
<div class="header">
|
|
<button class="btn-icon" onclick="handleBack()" aria-label="뒤로가기">
|
|
<span class="material-symbols-outlined">arrow_back</span>
|
|
</button>
|
|
<h1 class="header-title">회의록 수정</h1>
|
|
<button class="btn btn-primary btn-sm" onclick="saveMeeting()">저장</button>
|
|
</div>
|
|
|
|
<!-- 자동 저장 인디케이터 -->
|
|
<div class="auto-save-indicator" id="autoSaveIndicator">
|
|
<span class="material-symbols-outlined" style="font-size: 16px;">check_circle</span>
|
|
<span id="autoSaveText">저장됨</span>
|
|
</div>
|
|
|
|
<!-- NEW - 충돌 알림 배너 (UFR-COLLAB-020) -->
|
|
<div class="conflict-banner" id="conflictBanner">
|
|
<span class="material-symbols-outlined conflict-icon">warning</span>
|
|
<div class="conflict-content">
|
|
<div class="conflict-title">동시 수정 충돌 감지</div>
|
|
<div class="conflict-description" id="conflictDescription">
|
|
다른 사용자가 동일한 섹션을 수정했습니다. 충돌을 해결해주세요.
|
|
</div>
|
|
</div>
|
|
<button class="btn-resolve" onclick="showConflictResolution()">
|
|
해결하기
|
|
</button>
|
|
</div>
|
|
|
|
<!-- 메인 컨텐츠 -->
|
|
<div class="content">
|
|
<!-- 회의록 목록 모드 -->
|
|
<div id="listMode">
|
|
<!-- 필터 및 검색 -->
|
|
<div class="d-flex gap-2 mb-4">
|
|
<select id="statusFilter" class="form-select" style="flex: 1;" onchange="renderMeetingList()">
|
|
<option value="all">전체</option>
|
|
<option value="draft">작성중</option>
|
|
<option value="confirmed">확정완료</option>
|
|
</select>
|
|
<select id="sortOrder" class="form-select" style="flex: 1;" onchange="renderMeetingList()">
|
|
<option value="recent">최신순</option>
|
|
<option value="date">회의일시순</option>
|
|
<option value="title">제목순</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<input
|
|
type="text"
|
|
id="searchInput"
|
|
class="form-input"
|
|
placeholder="회의 제목, 참석자, 키워드 검색"
|
|
oninput="renderMeetingList()"
|
|
>
|
|
</div>
|
|
|
|
<!-- 회의록 목록 -->
|
|
<div id="meetingList">
|
|
<!-- JavaScript로 동적 생성 -->
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 수정 모드 -->
|
|
<div id="editMode" style="display: none;">
|
|
<!-- 기본 정보 수정 -->
|
|
<div class="card mb-4">
|
|
<h3 class="text-h5 mb-3">기본 정보</h3>
|
|
<div class="form-group">
|
|
<label for="editTitle" class="form-label required">회의 제목</label>
|
|
<input type="text" id="editTitle" class="form-input" maxlength="100">
|
|
</div>
|
|
<div class="d-flex gap-2">
|
|
<div class="form-group" style="flex: 1;">
|
|
<label for="editDate" class="form-label">날짜</label>
|
|
<input type="date" id="editDate" class="form-input">
|
|
</div>
|
|
<div class="form-group" style="flex: 1;">
|
|
<label for="editStartTime" class="form-label">시작</label>
|
|
<input type="time" id="editStartTime" class="form-input">
|
|
</div>
|
|
<div class="form-group" style="flex: 1;">
|
|
<label for="editEndTime" class="form-label">종료</label>
|
|
<input type="time" id="editEndTime" class="form-input">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 섹션별 수정 -->
|
|
<div id="editSectionList">
|
|
<!-- JavaScript로 동적 생성 -->
|
|
</div>
|
|
|
|
<!-- 하단 액션 -->
|
|
<div class="d-flex gap-2 mt-4">
|
|
<button class="btn btn-secondary" onclick="cancelEdit()">
|
|
취소
|
|
</button>
|
|
<button class="btn btn-primary" style="flex: 1;" onclick="saveMeeting()">
|
|
저장
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script src="common.js"></script>
|
|
<script>
|
|
if (!NavigationHelper.requireAuth()) {}
|
|
|
|
const currentUser = StorageManager.getCurrentUser();
|
|
const meetingId = NavigationHelper.getQueryParam('id');
|
|
let currentMeeting = null;
|
|
let isEditMode = false;
|
|
let autoSaveTimer = null;
|
|
let hasUnsavedChanges = false;
|
|
|
|
// NEW - UFR-COLLAB-020: 충돌 관리 변수
|
|
let conflicts = [];
|
|
let currentConflict = null;
|
|
|
|
// 회의록 목록 렌더링
|
|
function renderMeetingList() {
|
|
const meetings = StorageManager.getMeetings();
|
|
const myMeetings = meetings.filter(m =>
|
|
m.createdBy === currentUser.id || m.attendees.includes(currentUser.name)
|
|
);
|
|
|
|
// 필터링
|
|
const statusFilter = document.getElementById('statusFilter').value;
|
|
let filtered = myMeetings;
|
|
if (statusFilter !== 'all') {
|
|
filtered = myMeetings.filter(m => m.status === statusFilter);
|
|
}
|
|
|
|
// 검색
|
|
const searchQuery = document.getElementById('searchInput').value.toLowerCase();
|
|
if (searchQuery) {
|
|
filtered = filtered.filter(m =>
|
|
m.title.toLowerCase().includes(searchQuery) ||
|
|
m.attendees.some(a => a.toLowerCase().includes(searchQuery))
|
|
);
|
|
}
|
|
|
|
// 정렬
|
|
const sortOrder = document.getElementById('sortOrder').value;
|
|
if (sortOrder === 'recent') {
|
|
filtered.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
|
|
} else if (sortOrder === 'date') {
|
|
filtered.sort((a, b) => new Date(b.date) - new Date(a.date));
|
|
} else if (sortOrder === 'title') {
|
|
filtered.sort((a, b) => a.title.localeCompare(b.title));
|
|
}
|
|
|
|
// 렌더링
|
|
const container = document.getElementById('meetingList');
|
|
|
|
if (filtered.length === 0) {
|
|
container.innerHTML = '<p class="text-body text-gray text-center" style="padding: 48px 0;">회의록이 없습니다</p>';
|
|
return;
|
|
}
|
|
|
|
container.innerHTML = filtered.map(meeting => `
|
|
<div class="meeting-item" onclick="editMeetingById('${meeting.id}')">
|
|
<div style="flex: 1;">
|
|
<h3 class="text-h5">${meeting.title}</h3>
|
|
<p class="text-caption text-gray">${Utils.formatDate(meeting.date)} ${meeting.startTime || ''} · ${meeting.attendees?.length || 0}명</p>
|
|
<p class="text-caption text-gray mt-1">최종 수정: ${Utils.formatTimeAgo(meeting.updatedAt)}</p>
|
|
</div>
|
|
<div class="d-flex flex-column align-end gap-2">
|
|
${meeting.status === 'confirmed' ? '<span class="badge badge-confirmed">확정완료</span>' : '<span class="badge badge-draft">작성중</span>'}
|
|
${meeting.createdBy === currentUser.id ? '' : '<span class="text-caption text-gray">조회 전용</span>'}
|
|
</div>
|
|
</div>
|
|
`).join('');
|
|
}
|
|
|
|
// 회의록 수정 모드로 전환
|
|
function editMeetingById(id) {
|
|
const meeting = StorageManager.getMeetingById(id);
|
|
if (!meeting) {
|
|
UIComponents.showToast('회의록을 찾을 수 없습니다', 'error');
|
|
return;
|
|
}
|
|
|
|
// 권한 체크
|
|
const canEdit = meeting.createdBy === currentUser.id;
|
|
if (!canEdit) {
|
|
UIComponents.showToast('본인이 작성한 회의록만 수정할 수 있습니다', 'warning');
|
|
setTimeout(() => {
|
|
NavigationHelper.navigate('MEETING_DETAIL', { id });
|
|
}, 1500);
|
|
return;
|
|
}
|
|
|
|
currentMeeting = { ...meeting };
|
|
isEditMode = true;
|
|
|
|
// 확정완료 → 작성중으로 변경
|
|
if (currentMeeting.status === 'confirmed') {
|
|
currentMeeting.status = 'draft';
|
|
UIComponents.showToast('확정완료 회의록이 작성중으로 변경되었습니다', 'info');
|
|
}
|
|
|
|
// UI 전환
|
|
document.getElementById('listMode').style.display = 'none';
|
|
document.getElementById('editMode').style.display = 'block';
|
|
|
|
// 기본 정보 설정
|
|
document.getElementById('editTitle').value = currentMeeting.title;
|
|
document.getElementById('editDate').value = currentMeeting.date;
|
|
document.getElementById('editStartTime').value = currentMeeting.startTime || '';
|
|
document.getElementById('editEndTime').value = currentMeeting.endTime || '';
|
|
|
|
// 섹션 렌더링
|
|
renderEditSections();
|
|
|
|
// NEW - 충돌 감지 (UFR-COLLAB-020)
|
|
detectConflicts();
|
|
|
|
// 자동 저장 시작
|
|
startAutoSave();
|
|
}
|
|
|
|
// NEW - UFR-COLLAB-020: 충돌 감지
|
|
function detectConflicts() {
|
|
// 시뮬레이션: 30% 확률로 충돌 발생
|
|
if (Math.random() < 0.3 && currentMeeting.sections.length > 0) {
|
|
const conflictSectionIndex = Math.floor(Math.random() * currentMeeting.sections.length);
|
|
const conflictSection = currentMeeting.sections[conflictSectionIndex];
|
|
|
|
const otherUsers = DUMMY_USERS.filter(u => u.id !== currentUser.id);
|
|
const conflictUser = otherUsers[Math.floor(Math.random() * otherUsers.length)];
|
|
|
|
conflicts.push({
|
|
sectionId: conflictSection.id,
|
|
sectionName: conflictSection.name,
|
|
myVersion: {
|
|
content: conflictSection.content || '(내용 없음)',
|
|
modifiedAt: new Date().toISOString(),
|
|
modifiedBy: currentUser.name
|
|
},
|
|
theirVersion: {
|
|
content: generateRandomConflictContent(conflictSection.content),
|
|
modifiedAt: new Date(Date.now() - 5000).toISOString(),
|
|
modifiedBy: conflictUser.name
|
|
}
|
|
});
|
|
|
|
showConflictBanner();
|
|
}
|
|
}
|
|
|
|
// 충돌 내용 생성 (시뮬레이션)
|
|
function generateRandomConflictContent(originalContent) {
|
|
if (!originalContent) return '다른 사용자가 추가한 내용입니다.';
|
|
|
|
const variations = [
|
|
originalContent + '\n\n추가 논의사항: 예산 검토 필요',
|
|
originalContent.replace('결정', '잠정 결정'),
|
|
'수정된 내용:\n' + originalContent,
|
|
originalContent + '\n\n※ 재논의 필요'
|
|
];
|
|
|
|
return variations[Math.floor(Math.random() * variations.length)];
|
|
}
|
|
|
|
// 충돌 배너 표시
|
|
function showConflictBanner() {
|
|
const banner = document.getElementById('conflictBanner');
|
|
const description = document.getElementById('conflictDescription');
|
|
|
|
if (conflicts.length > 0) {
|
|
description.textContent = `${conflicts.length}개 섹션에서 충돌이 감지되었습니다. 충돌을 해결해주세요.`;
|
|
banner.classList.add('active');
|
|
} else {
|
|
banner.classList.remove('active');
|
|
}
|
|
}
|
|
|
|
// NEW - UFR-COLLAB-020: 충돌 해결 모달 표시
|
|
function showConflictResolution() {
|
|
if (conflicts.length === 0) return;
|
|
|
|
currentConflict = conflicts[0];
|
|
let selectedVersion = 'mine'; // 기본값: 내 버전
|
|
|
|
const modalContent = `
|
|
<div class="conflict-resolution">
|
|
<div class="conflict-header">
|
|
<h3 class="text-h5" style="color: #B91C1C;">
|
|
<span class="material-symbols-outlined" style="vertical-align: middle;">warning</span>
|
|
충돌 해결 필요
|
|
</h3>
|
|
<p class="text-caption text-gray mt-2">
|
|
"${currentConflict.sectionName}" 섹션에서 충돌이 감지되었습니다. 최종 버전을 선택하거나 직접 작성하세요.
|
|
</p>
|
|
</div>
|
|
|
|
<div class="conflict-body">
|
|
<!-- 내 버전 -->
|
|
<div class="conflict-section">
|
|
<div class="conflict-label">
|
|
<span class="material-symbols-outlined" style="color: var(--color-primary-main);">person</span>
|
|
내 수정 내용
|
|
</div>
|
|
<div class="conflict-diff selected" id="myVersion" onclick="selectVersion('mine')">
|
|
<div class="conflict-user">
|
|
<span class="material-symbols-outlined" style="font-size: 14px;">account_circle</span>
|
|
${currentConflict.myVersion.modifiedBy}
|
|
<span class="conflict-time">· ${Utils.formatTimeAgo(currentConflict.myVersion.modifiedAt)}</span>
|
|
</div>
|
|
<div class="conflict-content-box">${currentConflict.myVersion.content}</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 타인 버전 -->
|
|
<div class="conflict-section">
|
|
<div class="conflict-label">
|
|
<span class="material-symbols-outlined" style="color: #F59E0B;">group</span>
|
|
다른 사용자 수정 내용
|
|
</div>
|
|
<div class="conflict-diff" id="theirVersion" onclick="selectVersion('theirs')">
|
|
<div class="conflict-user">
|
|
<span class="material-symbols-outlined" style="font-size: 14px;">account_circle</span>
|
|
${currentConflict.theirVersion.modifiedBy}
|
|
<span class="conflict-time">· ${Utils.formatTimeAgo(currentConflict.theirVersion.modifiedAt)}</span>
|
|
</div>
|
|
<div class="conflict-content-box">${currentConflict.theirVersion.content}</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 직접 작성 -->
|
|
<div class="conflict-section">
|
|
<div class="conflict-label">
|
|
<span class="material-symbols-outlined" style="color: #10B981;">edit</span>
|
|
직접 작성하기
|
|
</div>
|
|
<div class="conflict-diff" id="manualVersion" onclick="selectVersion('manual')">
|
|
<textarea
|
|
class="merge-editor"
|
|
id="manualContent"
|
|
placeholder="양쪽 내용을 참고하여 직접 작성하세요..."
|
|
>${currentConflict.myVersion.content}</textarea>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="conflict-actions">
|
|
<button class="btn btn-secondary" onclick="UIComponents.closeModal()">
|
|
취소
|
|
</button>
|
|
<button class="btn btn-primary" style="flex: 1;" onclick="resolveConflict()">
|
|
이 버전으로 확정
|
|
</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
UIComponents.showModal('충돌 해결', modalContent, null, 'large');
|
|
|
|
// 버전 선택 함수
|
|
window.selectVersion = function(version) {
|
|
selectedVersion = version;
|
|
|
|
document.getElementById('myVersion').classList.remove('selected');
|
|
document.getElementById('theirVersion').classList.remove('selected');
|
|
document.getElementById('manualVersion').classList.remove('selected');
|
|
|
|
if (version === 'mine') {
|
|
document.getElementById('myVersion').classList.add('selected');
|
|
} else if (version === 'theirs') {
|
|
document.getElementById('theirVersion').classList.add('selected');
|
|
} else if (version === 'manual') {
|
|
document.getElementById('manualVersion').classList.add('selected');
|
|
document.getElementById('manualContent').focus();
|
|
}
|
|
};
|
|
|
|
// 충돌 해결 함수
|
|
window.resolveConflict = function() {
|
|
let finalContent = '';
|
|
|
|
if (selectedVersion === 'mine') {
|
|
finalContent = currentConflict.myVersion.content;
|
|
} else if (selectedVersion === 'theirs') {
|
|
finalContent = currentConflict.theirVersion.content;
|
|
} else if (selectedVersion === 'manual') {
|
|
finalContent = document.getElementById('manualContent').value;
|
|
}
|
|
|
|
// 섹션 내용 업데이트
|
|
const section = currentMeeting.sections.find(s => s.id === currentConflict.sectionId);
|
|
if (section) {
|
|
section.content = finalContent;
|
|
|
|
// textarea 업데이트
|
|
const textarea = document.querySelector(`textarea[data-section-id="${currentConflict.sectionId}"]`);
|
|
if (textarea) {
|
|
textarea.value = finalContent;
|
|
}
|
|
}
|
|
|
|
// 충돌 목록에서 제거
|
|
conflicts.shift();
|
|
|
|
UIComponents.closeModal();
|
|
UIComponents.showToast('충돌이 해결되었습니다', 'success');
|
|
|
|
// 남은 충돌 처리
|
|
if (conflicts.length > 0) {
|
|
setTimeout(() => {
|
|
showConflictResolution();
|
|
}, 500);
|
|
} else {
|
|
showConflictBanner();
|
|
markAsChanged();
|
|
}
|
|
};
|
|
}
|
|
|
|
// 섹션 수정 렌더링
|
|
function renderEditSections() {
|
|
const container = document.getElementById('editSectionList');
|
|
|
|
container.innerHTML = currentMeeting.sections.map((section, index) => {
|
|
const hasConflict = conflicts.some(c => c.sectionId === section.id);
|
|
|
|
return `
|
|
<div class="card mb-4">
|
|
<div class="d-flex justify-between align-center mb-3">
|
|
<div class="d-flex align-center gap-2">
|
|
<h3 class="text-h5">${section.name}</h3>
|
|
${hasConflict ? '<span class="conflict-badge"><span class="material-symbols-outlined" style="font-size: 14px;">warning</span> 충돌</span>' : ''}
|
|
</div>
|
|
${section.locked ? '<span class="material-symbols-outlined" style="color: var(--color-gray-600);">lock</span>' : ''}
|
|
</div>
|
|
<textarea
|
|
class="form-textarea"
|
|
rows="5"
|
|
data-section-id="${section.id}"
|
|
onchange="markAsChanged()"
|
|
${section.locked ? 'disabled' : ''}
|
|
>${section.content || ''}</textarea>
|
|
${section.locked ? `
|
|
<!-- NEW - UFR-MEET-055: 섹션 잠금 해제 버튼 -->
|
|
<div class="section-lock-area">
|
|
<span class="material-symbols-outlined" style="color: #F59E0B; font-size: 18px;">lock</span>
|
|
<div style="flex: 1;">
|
|
<p class="text-caption text-gray" style="margin: 0;">
|
|
이 섹션은 잠겨있습니다. 수정하려면 잠금을 해제하세요.
|
|
</p>
|
|
</div>
|
|
<button class="btn-unlock" onclick="unlockSection('${section.id}')">
|
|
<span class="material-symbols-outlined" style="font-size: 16px;">lock_open</span>
|
|
잠금 해제
|
|
</button>
|
|
</div>
|
|
` : ''}
|
|
</div>
|
|
`;
|
|
}).join('');
|
|
}
|
|
|
|
// NEW - UFR-MEET-055: 섹션 잠금 해제
|
|
function unlockSection(sectionId) {
|
|
UIComponents.confirm(
|
|
'이 섹션의 잠금을 해제하시겠습니까? 해제 후에는 내용을 수정할 수 있습니다.',
|
|
() => {
|
|
const section = currentMeeting.sections.find(s => s.id === sectionId);
|
|
if (section) {
|
|
section.locked = false;
|
|
renderEditSections();
|
|
UIComponents.showToast('섹션 잠금이 해제되었습니다', 'success');
|
|
markAsChanged();
|
|
}
|
|
},
|
|
() => {}
|
|
);
|
|
}
|
|
|
|
// 변경사항 표시
|
|
function markAsChanged() {
|
|
hasUnsavedChanges = true;
|
|
}
|
|
|
|
// 자동 저장 시작
|
|
function startAutoSave() {
|
|
if (autoSaveTimer) clearInterval(autoSaveTimer);
|
|
|
|
autoSaveTimer = setInterval(() => {
|
|
if (hasUnsavedChanges) {
|
|
autoSaveMeeting();
|
|
}
|
|
}, 30000); // 30초마다 자동 저장
|
|
}
|
|
|
|
// 자동 저장
|
|
function autoSaveMeeting() {
|
|
const indicator = document.getElementById('autoSaveIndicator');
|
|
document.getElementById('autoSaveText').textContent = '저장 중...';
|
|
indicator.classList.add('active');
|
|
|
|
// 데이터 수집
|
|
collectMeetingData();
|
|
|
|
// 저장
|
|
setTimeout(() => {
|
|
StorageManager.updateMeeting(currentMeeting.id, currentMeeting);
|
|
hasUnsavedChanges = false;
|
|
|
|
document.getElementById('autoSaveText').textContent = '저장됨';
|
|
|
|
setTimeout(() => {
|
|
indicator.classList.remove('active');
|
|
}, 2000);
|
|
}, 500);
|
|
}
|
|
|
|
// 회의록 데이터 수집
|
|
function collectMeetingData() {
|
|
currentMeeting.title = document.getElementById('editTitle').value;
|
|
currentMeeting.date = document.getElementById('editDate').value;
|
|
currentMeeting.startTime = document.getElementById('editStartTime').value;
|
|
currentMeeting.endTime = document.getElementById('editEndTime').value;
|
|
|
|
// 섹션 내용 수집
|
|
currentMeeting.sections.forEach(section => {
|
|
const textarea = document.querySelector(`textarea[data-section-id="${section.id}"]`);
|
|
if (textarea) {
|
|
section.content = textarea.value;
|
|
}
|
|
});
|
|
|
|
currentMeeting.updatedAt = new Date().toISOString();
|
|
}
|
|
|
|
// 회의록 저장
|
|
function saveMeeting() {
|
|
if (!currentMeeting) return;
|
|
|
|
// 충돌 확인
|
|
if (conflicts.length > 0) {
|
|
UIComponents.showToast('먼저 충돌을 해결해주세요', 'warning');
|
|
showConflictResolution();
|
|
return;
|
|
}
|
|
|
|
collectMeetingData();
|
|
|
|
UIComponents.showLoading('저장하는 중...');
|
|
|
|
setTimeout(() => {
|
|
StorageManager.updateMeeting(currentMeeting.id, currentMeeting);
|
|
hasUnsavedChanges = false;
|
|
|
|
UIComponents.hideLoading();
|
|
UIComponents.showToast('회의록이 저장되었습니다', 'success');
|
|
|
|
setTimeout(() => {
|
|
window.location.href = '12-회의록상세조회.html';
|
|
}, 1000);
|
|
}, 800);
|
|
}
|
|
|
|
// 수정 취소
|
|
function cancelEdit() {
|
|
if (hasUnsavedChanges) {
|
|
UIComponents.confirm(
|
|
'저장하지 않은 변경사항이 있습니다. 정말 취소하시겠습니까?',
|
|
() => {
|
|
resetEditMode();
|
|
},
|
|
() => {}
|
|
);
|
|
} else {
|
|
resetEditMode();
|
|
}
|
|
}
|
|
|
|
// 수정 모드 리셋
|
|
function resetEditMode() {
|
|
if (autoSaveTimer) clearInterval(autoSaveTimer);
|
|
|
|
currentMeeting = null;
|
|
isEditMode = false;
|
|
hasUnsavedChanges = false;
|
|
conflicts = [];
|
|
currentConflict = null;
|
|
|
|
document.getElementById('listMode').style.display = 'block';
|
|
document.getElementById('editMode').style.display = 'none';
|
|
document.getElementById('conflictBanner').classList.remove('active');
|
|
|
|
renderMeetingList();
|
|
}
|
|
|
|
// 뒤로가기 처리
|
|
function handleBack() {
|
|
if (isEditMode) {
|
|
cancelEdit();
|
|
} else {
|
|
NavigationHelper.navigate('DASHBOARD');
|
|
}
|
|
}
|
|
|
|
// 페이지 이탈 방지
|
|
window.addEventListener('beforeunload', (e) => {
|
|
if (hasUnsavedChanges) {
|
|
e.preventDefault();
|
|
e.returnValue = '';
|
|
}
|
|
});
|
|
|
|
// 초기화
|
|
if (meetingId) {
|
|
editMeetingById(meetingId);
|
|
} else {
|
|
renderMeetingList();
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|