/* meeting-detail.js — 회의록 상세/작성 */ let meetingId = null; let meetingData = null; let selectedAttendees = []; // [{user_id, name, username}] let projects = []; let users = []; let canEdit = false; let isAdmin = false; let isPublished = false; document.addEventListener('DOMContentLoaded', async () => { const ok = await initAuth(); if (!ok) return; document.querySelector('.fade-in').classList.add('visible'); const role = currentUser?.role || ''; canEdit = ['support_team', 'admin', 'system', 'system admin'].includes(role); isAdmin = ['admin', 'system', 'system admin'].includes(role); // Parse URL const params = new URLSearchParams(location.search); meetingId = params.get('id'); // Load master data try { const [projRes, userRes] = await Promise.all([ api('/projects'), api('/users') ]); projects = projRes.data || []; users = (userRes.data || []).filter(u => u.is_active !== 0); } catch {} // Populate project select in item modal const projSel = document.getElementById('itemProject'); projects.forEach(p => { projSel.innerHTML += ``; }); // Populate responsible user select const respSel = document.getElementById('itemResponsible'); users.forEach(u => { respSel.innerHTML += ``; }); // Attendee search const searchInput = document.getElementById('attendeeSearch'); const resultsDiv = document.getElementById('attendeeResults'); searchInput.addEventListener('input', debounce(() => { const q = searchInput.value.trim().toLowerCase(); if (q.length < 1) { resultsDiv.classList.add('hidden'); return; } const matches = users.filter(u => !selectedAttendees.some(a => a.user_id === u.user_id) && (u.name?.toLowerCase().includes(q) || u.username?.toLowerCase().includes(q)) ).slice(0, 10); if (matches.length === 0) { resultsDiv.classList.add('hidden'); return; } resultsDiv.innerHTML = matches.map(u => `
${escapeHtml(u.name)} (${escapeHtml(u.username)})
` ).join(''); resultsDiv.classList.remove('hidden'); }, 200)); searchInput.addEventListener('blur', () => setTimeout(() => resultsDiv.classList.add('hidden'), 200)); if (meetingId) { await loadMeeting(); } else { // New meeting document.getElementById('meetingDate').value = new Date().toISOString().split('T')[0]; updateUI(); } }); async function loadMeeting() { try { const res = await api(`/meetings/${meetingId}`); meetingData = res.data; isPublished = meetingData.status === 'published'; document.getElementById('pageTitle').textContent = meetingData.title; document.getElementById('meetingDate').value = formatDate(meetingData.meeting_date); document.getElementById('meetingTime').value = meetingData.meeting_time || ''; document.getElementById('meetingTitle').value = meetingData.title; document.getElementById('meetingLocation').value = meetingData.location || ''; document.getElementById('meetingSummary').value = meetingData.summary || ''; // Status badge const badge = document.getElementById('statusBadge'); badge.classList.remove('hidden'); if (isPublished) { badge.className = 'badge badge-green'; badge.textContent = '발행'; } else { badge.className = 'badge badge-gray'; badge.textContent = '초안'; } // Attendees selectedAttendees = (meetingData.attendees || []).map(a => ({ user_id: a.user_id, name: a.name, username: a.username })); renderAttendees(); // Agenda items renderAgendaItems(meetingData.items || []); updateUI(); } catch (err) { showToast('회의록 로드 실패: ' + err.message, 'error'); } } function updateUI() { const editable = canEdit && (!isPublished || isAdmin); // Fields ['meetingDate', 'meetingTime', 'meetingTitle', 'meetingLocation', 'meetingSummary', 'attendeeSearch'].forEach(id => { const el = document.getElementById(id); if (el) { el.disabled = !editable; if (!editable) el.classList.add('bg-gray-100'); } }); // Buttons document.getElementById('btnSave').classList.toggle('hidden', !editable); document.getElementById('btnAddItem').classList.toggle('hidden', !editable); document.getElementById('btnPublish').classList.toggle('hidden', !canEdit || isPublished || !meetingId); document.getElementById('btnUnpublish').classList.toggle('hidden', !isAdmin || !isPublished); document.getElementById('btnDelete').classList.toggle('hidden', !isAdmin || !meetingId); } /* ===== Attendees ===== */ function addAttendee(userId, name, username) { if (selectedAttendees.some(a => a.user_id === userId)) return; selectedAttendees.push({ user_id: userId, name, username }); renderAttendees(); document.getElementById('attendeeSearch').value = ''; document.getElementById('attendeeResults').classList.add('hidden'); } function removeAttendee(userId) { selectedAttendees = selectedAttendees.filter(a => a.user_id !== userId); renderAttendees(); } function renderAttendees() { const container = document.getElementById('attendeeTags'); const editable = canEdit && (!isPublished || isAdmin); container.innerHTML = selectedAttendees.map(a => `${escapeHtml(a.name)}${editable ? ` ×` : ''}` ).join(''); } /* ===== Agenda Items ===== */ function renderAgendaItems(items) { const list = document.getElementById('agendaList'); const empty = document.getElementById('agendaEmpty'); if (items.length === 0) { list.innerHTML = ''; empty.classList.remove('hidden'); return; } empty.classList.add('hidden'); const typeLabels = { schedule_update: '공정현황', issue: '이슈', decision: '결정사항', action_item: '조치사항', other: '기타' }; const typeColors = { schedule_update: 'badge-blue', issue: 'badge-red', decision: 'badge-green', action_item: 'badge-amber', other: 'badge-gray' }; const statusLabels = { open: '미처리', in_progress: '진행중', completed: '완료', cancelled: '취소' }; const statusColors = { open: 'badge-amber', in_progress: 'badge-blue', completed: 'badge-green', cancelled: 'badge-gray' }; const editable = canEdit && (!isPublished || isAdmin); const canUpdateStatus = ['group_leader', 'support_team', 'admin', 'system', 'system admin'].includes(currentUser?.role || ''); list.innerHTML = items.map(item => `
${typeLabels[item.item_type] || item.item_type} ${statusLabels[item.status] || item.status} ${item.project_code ? `${escapeHtml(item.project_code)}` : ''} ${item.milestone_name ? `◆ ${escapeHtml(item.milestone_name)}` : ''}
${canUpdateStatus && item.status !== 'completed' ? `` : ''} ${editable ? ` ` : ''}

${escapeHtml(item.content)}

${item.decision ? `

결정: ${escapeHtml(item.decision)}

` : ''} ${item.action_required ? `

조치: ${escapeHtml(item.action_required)}

` : ''}
${item.responsible_name ? `${escapeHtml(item.responsible_name)}` : ''} ${item.due_date ? `${formatDate(item.due_date)}` : ''} ${item.milestone_name ? `공정표 보기` : ''}
`).join(''); } /* ===== Save Meeting ===== */ async function saveMeeting() { const title = document.getElementById('meetingTitle').value.trim(); const meetingDate = document.getElementById('meetingDate').value; if (!title || !meetingDate) { showToast('날짜와 제목은 필수입니다.', 'error'); return; } const data = { meeting_date: meetingDate, meeting_time: document.getElementById('meetingTime').value || null, title, location: document.getElementById('meetingLocation').value || null, summary: document.getElementById('meetingSummary').value || null, attendees: selectedAttendees.map(a => a.user_id) }; try { if (meetingId) { await api(`/meetings/${meetingId}`, { method: 'PUT', body: JSON.stringify(data) }); showToast('회의록이 저장되었습니다.'); } else { const res = await api('/meetings', { method: 'POST', body: JSON.stringify(data) }); meetingId = res.data.meeting_id; history.replaceState(null, '', `?id=${meetingId}`); showToast('회의록이 생성되었습니다.'); } await loadMeeting(); } catch (err) { showToast(err.message, 'error'); } } async function publishMeeting() { if (!confirm('회의록을 발행하시겠습니까? 발행 후 일반 사용자는 수정할 수 없습니다.')) return; try { await api(`/meetings/${meetingId}/publish`, { method: 'PUT' }); showToast('회의록이 발행되었습니다.'); await loadMeeting(); } catch (err) { showToast(err.message, 'error'); } } async function unpublishMeeting() { if (!confirm('발행을 취소하시겠습니까?')) return; try { await api(`/meetings/${meetingId}/unpublish`, { method: 'PUT' }); showToast('발행이 취소되었습니다.'); await loadMeeting(); } catch (err) { showToast(err.message, 'error'); } } async function deleteMeeting() { if (!confirm('회의록을 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.')) return; try { await api(`/meetings/${meetingId}`, { method: 'DELETE' }); showToast('회의록이 삭제되었습니다.'); location.href = '/pages/work/meetings.html'; } catch (err) { showToast(err.message, 'error'); } } /* ===== Agenda Item Modal ===== */ function openItemModal(editItemId) { const modal = document.getElementById('itemModal'); const isEdit = !!editItemId; document.getElementById('itemModalTitle').textContent = isEdit ? '안건 수정' : '안건 추가'; if (isEdit && meetingData) { const item = meetingData.items.find(i => i.item_id === editItemId); if (!item) return; document.getElementById('itemId').value = editItemId; document.getElementById('itemType').value = item.item_type; document.getElementById('itemProject').value = item.project_id || ''; loadItemMilestones(item.milestone_id); document.getElementById('itemContent').value = item.content; document.getElementById('itemDecision').value = item.decision || ''; document.getElementById('itemAction').value = item.action_required || ''; document.getElementById('itemResponsible').value = item.responsible_user_id || ''; document.getElementById('itemDueDate').value = item.due_date ? formatDate(item.due_date) : ''; document.getElementById('itemStatus').value = item.status; } else { document.getElementById('itemId').value = ''; document.getElementById('itemType').value = 'schedule_update'; document.getElementById('itemProject').value = ''; document.getElementById('itemMilestone').innerHTML = ''; document.getElementById('itemContent').value = ''; document.getElementById('itemDecision').value = ''; document.getElementById('itemAction').value = ''; document.getElementById('itemResponsible').value = ''; document.getElementById('itemDueDate').value = ''; document.getElementById('itemStatus').value = 'open'; } modal.classList.remove('hidden'); } function closeItemModal() { document.getElementById('itemModal').classList.add('hidden'); } async function loadItemMilestones(selectedId) { const projectId = document.getElementById('itemProject').value; const sel = document.getElementById('itemMilestone'); sel.innerHTML = ''; if (!projectId) return; try { const res = await api(`/schedule/milestones?project_id=${projectId}`); (res.data || []).forEach(m => { const opt = document.createElement('option'); opt.value = m.milestone_id; opt.textContent = `${m.milestone_name} (${formatDate(m.milestone_date)})`; if (selectedId && m.milestone_id === selectedId) opt.selected = true; sel.appendChild(opt); }); } catch {} } async function saveItem() { const content = document.getElementById('itemContent').value.trim(); if (!content) { showToast('안건 내용을 입력해주세요.', 'error'); return; } if (!meetingId) { showToast('회의록을 먼저 저장해주세요.', 'error'); return; } const itemId = document.getElementById('itemId').value; const data = { item_type: document.getElementById('itemType').value, project_id: document.getElementById('itemProject').value || null, milestone_id: document.getElementById('itemMilestone').value || null, content, decision: document.getElementById('itemDecision').value || null, action_required: document.getElementById('itemAction').value || null, responsible_user_id: document.getElementById('itemResponsible').value || null, due_date: document.getElementById('itemDueDate').value || null, status: document.getElementById('itemStatus').value }; try { if (itemId) { await api(`/meetings/${meetingId}/items/${itemId}`, { method: 'PUT', body: JSON.stringify(data) }); showToast('안건이 수정되었습니다.'); } else { await api(`/meetings/${meetingId}/items`, { method: 'POST', body: JSON.stringify(data) }); showToast('안건이 추가되었습니다.'); } closeItemModal(); await loadMeeting(); } catch (err) { showToast(err.message, 'error'); } } async function deleteItem(itemId) { if (!confirm('안건을 삭제하시겠습니까?')) return; try { await api(`/meetings/${meetingId}/items/${itemId}`, { method: 'DELETE' }); showToast('안건이 삭제되었습니다.'); await loadMeeting(); } catch (err) { showToast(err.message, 'error'); } } async function updateItemStatus(itemId, status) { if (!status) return; try { await api(`/meetings/items/${itemId}/status`, { method: 'PUT', body: JSON.stringify({ status }) }); showToast('상태가 업데이트되었습니다.'); await loadMeeting(); } catch (err) { showToast(err.message, 'error'); } }