Files
test-mcp/js/mail.js

313 lines
14 KiB
JavaScript

/**
* Project Master Overseas Mail Management JS
* 기능: 첨부파일 로드, AI 분석, 메일 목록 렌더링, 미리보기, 주소록 관리
*/
let currentFiles = [];
let editingIndex = -1;
const HIERARCHY = {
"행정": { "계약": ["계약관리", "기성관리", "업무지시서", "인원관리"], "업무관리": ["업무일지(2025)", "업무일지(2025년 이전)", "발주처 정기보고", "본사업무보고", "공사감독일지", "양식서류"] },
"설계성과품": { "시방서": ["공사시방서"], "설계도면": ["공통", "토공", "비탈면안전공", "배수공", "교량공", "포장공"], "수량산출서": ["토공", "배수공"], "내역서": ["단가산출서"], "보고서": ["실시설계보고서", "지반조사보고서"], "측량계산부": ["측량계산부"], "설계단계 수행협의": ["회의·협의"] },
"시공검측": { "토공": ["검측 (깨기)", "검측 (노체)"], "배수공": ["검측 (V형측구)", "검측 (종배수관)"], "구조물공": ["검측 (평목교)"], "포장공": ["검측 (기층)"] },
"설계변경": { "실정보고": ["토공", "배수공", "안전관리"], "기술지원 검토": ["토공", "구조물&부대공"] }
};
const MAIL_SAMPLES = {
inbound: [
{ person: "라오스 농림부", email: "pany.s@lao.gov.la", time: "2026-03-05", title: "ITTC 교육센터 착공식 일정 협의", summary: "착공식 관련하여 정부 측 인사의 일정을 반영한 최종 공문을 송부합니다.", active: true },
{ person: "현대건설 (김철수 소장)", email: "cs.kim@hdec.co.kr", time: "2026-03-04", title: "[긴급] 어천-공주(4차) 하도급 변경계약 통보", summary: "철거공사 물량 변동에 따른 계약 금액 조정 건입니다. 검토 후 승인 부탁드립니다.", active: false }
],
outbound: [
{ person: "공사관리부 (본사)", email: "hq_pm@projectmaster.com", time: "2026-03-04", title: "어천-공주 2월 월간 공정보고서 제출", summary: "2월 한 달간의 주요 공정 및 예산 집행 현황 보고서입니다.", active: false }
],
drafts: [], deleted: []
};
let currentMailTab = 'inbound';
let filteredMails = [];
// --- 첨부파일 데이터 로드 및 렌더링 ---
async function loadAttachments() {
try {
const res = await fetch(API.ATTACHMENTS);
currentFiles = await res.json();
renderFiles();
} catch (e) { console.error("Failed to load attachments:", e); }
}
function renderFiles() {
const isAiActive = document.getElementById('aiToggle').checked;
const container = document.getElementById('attachmentList');
if (!container) return;
container.innerHTML = '';
currentFiles.forEach((file, index) => {
const item = document.createElement('div');
item.className = 'attachment-item-wrap';
item.style.marginBottom = "8px";
let pathText = "경로를 선택해주세요";
let modeClass = "manual-mode";
if (file.analysis) {
const prefix = file.analysis.isManual ? "선택 경로: " : "추천: ";
pathText = `${prefix}${file.analysis.suggested_path}`;
modeClass = file.analysis.isManual ? "manual-mode" : "smart-mode";
} else if (isAiActive) {
pathText = "AI 분석 대기 중...";
modeClass = "smart-mode";
}
item.innerHTML = `
<div class="attachment-item" onclick="showPreview(${index}, event)" style="position:relative;">
<span class="file-icon" style="pointer-events:none;">📄</span>
<div class="file-details" style="pointer-events:none;">
<div class="file-name" title="${file.name}">${file.name}</div>
<div class="file-size">${file.size}</div>
</div>
<div class="btn-group" onclick="event.stopPropagation()" style="position:relative; z-index:2;">
<span id="recommend-${index}" class="ai-recommend path-display ${modeClass}" onclick="openPathModal(${index}, event)">${pathText}</span>
${isAiActive ? `<button class="btn-upload btn-ai" onclick="startAnalysis(${index}, event)">AI 분석</button>` : ''}
<button class="btn-upload btn-normal" onclick="confirmUpload(${index}, event)">파일업로드</button>
</div>
</div>
<div id="log-area-${index}" class="file-log-area"><div id="log-content-${index}"></div></div>
`;
container.appendChild(item);
});
}
// --- AI 분석 실행 ---
async function startAnalysis(index, event) {
if (event) event.stopPropagation();
const file = currentFiles[index];
if (!file) return;
// UI 상태 업데이트: 분석 중 표시
const logArea = document.getElementById(`log-area-${index}`);
const logContent = document.getElementById(`log-content-${index}`);
if (logArea) logArea.classList.add('active');
if (logContent) {
logContent.innerHTML = `<div class="ai-status-msg">
<span class="ai-loading-spinner"></span>
AI가 문서를 정밀 분석 중입니다...
</div>`;
}
try {
const res = await fetch(`${API.ANALYZE_FILE}?filename=${encodeURIComponent(file.name)}`);
const result = await res.json();
if (result.error) {
if (logContent) logContent.innerHTML = `<div style="color:var(--error-color);">오류: ${result.error}</div>`;
return;
}
// 분석 결과 저장 및 UI 갱신
currentFiles[index].analysis = result.final_result;
currentFiles[index].analysis.isManual = false;
if (logContent) {
logContent.innerHTML = `
<div class="ai-analysis-result">
<div style="font-weight:700; color:var(--primary-lv-6); margin-bottom:4px;">✨ AI 분석 완료</div>
<div style="font-size:11px; color:var(--text-sub); line-height:1.4;">${result.final_result.reason}</div>
</div>
`;
}
renderFiles();
} catch (e) {
console.error("AI Analysis failed:", e);
if (logContent) logContent.innerHTML = `<div style="color:var(--error-color);">분석 실패: 네트워크 오류가 발생했습니다.</div>`;
}
}
// --- 미리보기 제어 ---
function showPreview(index, event) {
if (event && (event.target.closest('.btn-group') || event.target.closest('.path-display'))) return;
const file = currentFiles[index];
if (!file) return;
const previewArea = document.getElementById('mailPreviewArea');
const toggleIcon = document.getElementById('previewToggleIcon');
const fullViewBtn = document.getElementById('fullViewBtn');
const previewContainer = document.getElementById('previewContainer');
if (previewArea) {
previewArea.classList.add('active');
if (toggleIcon) toggleIcon.innerText = '▶';
}
const fileUrl = Utils.getSafeFileUrl(file.name);
if (fullViewBtn) {
fullViewBtn.style.display = 'block';
fullViewBtn.onclick = () => window.open(fileUrl, 'PMFullView', 'width=1000,height=800');
}
if (file.name.toLowerCase().endsWith('.pdf')) {
previewContainer.innerHTML = `<iframe src="${fileUrl}#page=1" style="width:100%; height:100%; border:none;"></iframe>`;
} else {
previewContainer.innerHTML = `<div class="flex-column flex-center" style="height:100%; padding:20px; text-align:center;"><img src="/sample.png" style="max-width:80%; max-height:60%; margin-bottom:20px;"><div style="font-weight:700; color:var(--primary-color);">${file.name}</div></div>`;
}
document.querySelectorAll('.attachment-item').forEach(item => item.classList.remove('active'));
if (event?.currentTarget) event.currentTarget.classList.add('active');
}
function togglePreviewAuto() {
const area = document.getElementById('mailPreviewArea');
const icon = document.getElementById('previewToggleIcon');
if (area) {
const isActive = area.classList.toggle('active');
if (icon) icon.innerText = isActive ? '▶' : '◀';
}
}
// --- 메일 리스트 제어 ---
function renderMailList(tabType, mailsToShow = null) {
currentMailTab = tabType;
const container = document.querySelector('.mail-items-container');
if (!container) return;
const mails = mailsToShow || MAIL_SAMPLES[tabType] || [];
filteredMails = mails;
updateBulkActionBar();
container.innerHTML = mails.map((mail, idx) => `
<div class="mail-item ${mail.active ? 'active' : ''}" onclick="selectMailItem(this, ${idx})">
<input type="checkbox" class="mail-item-checkbox" onclick="event.stopPropagation()" onchange="updateBulkActionBar()">
<div class="mail-item-content">
<div class="flex-between" style="margin-bottom:6px;">
<span style="font-weight:700; font-size:14px; color:${mail.active ? 'var(--primary-color)' : 'var(--text-main)'};">${mail.person}</span>
<div class="mail-item-info"><span class="mail-date">${mail.time}</span></div>
</div>
<div style="font-weight:600; font-size:12px; margin-bottom:4px; color:#2d3748;">${mail.title}</div>
<div class="text-truncate" style="-webkit-line-clamp:2; display:-webkit-box; -webkit-box-orient:vertical; white-space:normal; font-size:11px; color:var(--text-sub);">${mail.summary}</div>
</div>
</div>
`).join('');
const activeIdx = mails.findIndex(m => m.active);
if (activeIdx !== -1) updateMailContent(mails[activeIdx]);
}
function selectMailItem(el, index) {
document.querySelectorAll('.mail-item').forEach(item => item.classList.remove('active'));
el.classList.add('active');
const mail = filteredMails[index];
if (mail) updateMailContent(mail);
}
function updateMailContent(mail) {
const title = document.querySelector('.mail-content-header h2');
if (title) title.innerText = mail.title;
document.querySelector('.mail-body').innerHTML = mail.summary.replace(/\n/g, '<br>') + "<br><br>본 내용은 샘플 데이터입니다.";
}
function switchMailTab(el, tabType) {
document.querySelectorAll('.mail-tab').forEach(tab => tab.classList.remove('active'));
el.classList.add('active');
renderMailList(tabType);
}
// --- 경로 선택 모달 ---
function openPathModal(index, event) {
if (event) event.stopPropagation();
editingIndex = index;
const tabSelect = document.getElementById('tabSelect');
if (tabSelect) {
tabSelect.innerHTML = Object.keys(HIERARCHY).map(tab => `<option value="${tab}">${tab}</option>`).join('');
updateCategories();
ModalManager.open('pathModal');
}
}
function updateCategories() {
const tab = document.getElementById('tabSelect').value;
document.getElementById('categorySelect').innerHTML = Object.keys(HIERARCHY[tab]).map(cat => `<option value="${cat}">${cat}</option>`).join('');
updateSubs();
}
function updateSubs() {
const tab = document.getElementById('tabSelect').value;
const cat = document.getElementById('categorySelect').value;
document.getElementById('subSelect').innerHTML = HIERARCHY[tab][cat].map(sub => `<option value="${sub}">${sub}</option>`).join('');
}
function applyPathSelection() {
const path = `${document.getElementById('tabSelect').value} > ${document.getElementById('categorySelect').value} > ${document.getElementById('subSelect').value}`;
if (!currentFiles[editingIndex].analysis) currentFiles[editingIndex].analysis = {};
currentFiles[editingIndex].analysis.suggested_path = path;
currentFiles[editingIndex].analysis.isManual = true;
renderFiles();
ModalManager.close('pathModal');
}
// --- 주소록 관리 ---
let addressBookData = [
{ name: "이태훈", dept: "PM Overseas / 선임연구원", email: "th.lee@projectmaster.com", phone: "010-1234-5678" },
{ name: "Pany S.", dept: "라오스 농림부 / 국장", email: "pany.s@lao.gov.la", phone: "+856-20-1234-5678" }
];
let contactEditingIndex = -1;
function openAddressBook() { renderAddressBook(); ModalManager.open('addressBookModal'); }
function closeAddressBook() { ModalManager.close('addressBookModal'); }
function renderAddressBook() {
const body = document.getElementById('addressBookBody');
if (!body) return;
body.innerHTML = addressBookData.map((c, idx) => `
<tr>
<td><strong>${c.name}</strong></td><td>${c.dept}</td><td>${c.email}</td><td>${c.phone}</td>
<td style="text-align:right;">
<button class="_button-xsmall" onclick="editContact(${idx})">수정</button>
<button class="_button-xsmall" style="color:var(--error-color); border-color:#feb2b2; background:#fff5f5;" onclick="deleteContact(${idx})">삭제</button>
</td>
</tr>`).join('');
}
function toggleAddContactForm() {
const form = document.getElementById('addContactForm');
if (form.style.display === 'none') form.style.display = 'block';
else { form.style.display = 'none'; contactEditingIndex = -1; }
}
function editContact(index) {
const c = addressBookData[index];
contactEditingIndex = index;
document.getElementById('newContactName').value = c.name;
document.getElementById('newContactDept').value = c.dept;
document.getElementById('newContactEmail').value = c.email;
document.getElementById('newContactPhone').value = c.phone;
document.getElementById('addContactForm').style.display = 'block';
}
function deleteContact(index) {
if (confirm(`'${addressBookData[index].name}'님을 삭제하시겠습니까?`)) { addressBookData.splice(index, 1); renderAddressBook(); }
}
function addContact() {
const name = document.getElementById('newContactName').value;
if (!name) return alert("이름을 입력해주세요.");
const data = { name, dept: document.getElementById('newContactDept').value, email: document.getElementById('newContactEmail').value, phone: document.getElementById('newContactPhone').value };
if (contactEditingIndex > -1) addressBookData[contactEditingIndex] = data;
else addressBookData.push(data);
renderAddressBook(); toggleAddContactForm();
}
// --- 공통 액션 ---
function updateBulkActionBar() {
const count = document.querySelectorAll('.mail-item-checkbox:checked').length;
const bar = document.getElementById('mailBulkActions');
if (count > 0) { bar.classList.add('active'); document.getElementById('selectedCount').innerText = `${count}개 선택됨`; }
else bar.classList.remove('active');
}
// 초기화
document.addEventListener('DOMContentLoaded', () => {
loadAttachments();
renderMailList('inbound');
});