313 lines
14 KiB
JavaScript
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');
|
|
});
|