// files.js — file preview + attachment handling extracted from helpers.js.
// Sheet preview (PDF embed, XLSX viewer, Luckysheet modal) + file upload/attachment
// staging (handleImageUpload, handleFileUpload, stageAttachment, renderStagedAttachments,
// pdfPageToText, ...). stagedAttachments stays global (sendChat reads it).
function togglePdfEmbed(url, containerId, btn) {
var container = document.getElementById(containerId);
if (!container) return;
var iframe = container.querySelector('iframe');
if (container.style.display === 'none') {
container.style.display = 'block';
if (iframe) iframe.src = url; // src=""이면 현재페이지로 해석 → 항상 명시 설정
btn.innerHTML = '▲ 닫기';
} else {
container.style.display = 'none';
btn.innerHTML = '◀ 미리보기';
}
}
var _luckyCurrentSaveUrl = null;
function openXlsxNewWindow(url, title) {
var q = '/html/xlsx-viewer.html?file=' + encodeURIComponent(url)
+ '&save=' + encodeURIComponent(url)
+ '&title=' + encodeURIComponent(title || '스프레드시트');
window.open(q, '_blank', 'width=1200,height=800,menubar=no,toolbar=no,location=no,status=no,scrollbars=no,resizable=yes');
}
function toggleXlsxEmbedBtn(btn) {
var url = encodeURI(btn.getAttribute('data-xlsx-url'));
var card = btn.closest('.xlsx-card');
var nameEl = card ? card.querySelector('span[style*="font-weight"]') : null;
var title = nameEl ? nameEl.textContent : url.split('/').pop();
var saveUrl = card ? card.querySelector('[data-xlsx-save-url]')?.getAttribute('data-xlsx-save-url') : null;
openLuckyModal(url, title, saveUrl);
}
function openLuckyModal(url, title, saveUrl) {
var modal = document.getElementById('lucky-modal');
var titleEl = document.getElementById('lucky-modal-title');
var container = document.getElementById('lucky-container');
if (!modal || !container) return;
// Reset box to centered state on each open
var box = document.getElementById('lucky-modal-box');
if (box) {
box.style.position = 'relative';
box.style.left = ''; box.style.top = '';
box.style.width = '96vw'; box.style.height = '92vh';
box._isAbsolute = false; box.dataset.minimized = '0';
}
modal.style.display = 'flex';
modal.style.alignItems = 'center';
modal.style.justifyContent = 'center';
titleEl.textContent = title || '스프레드시트';
_luckyCurrentSaveUrl = saveUrl || null;
container.innerHTML = '
Luckysheet 로딩 중… (최초 1회 ~3MB)
';
initLuckyModalInteraction();
fetch(url, { headers: authHeaders() })
.then(function(r) { if (!r.ok) throw new Error('HTTP ' + r.status); return r.arrayBuffer(); })
.then(function(buf) {
loadLuckysheet(function() {
if (typeof LuckyExcel === 'undefined' || typeof luckysheet === 'undefined') {
container.innerHTML = 'Luckysheet 로드 실패 — 네트워크 연결을 확인하세요.
'; return;
}
var file = new File([buf], 'sheet.xlsx', { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
LuckyExcel.transformExcelToLucky(file, function(exportInfo) {
container.innerHTML = '';
try { luckysheet.destroy(); } catch(e) {}
luckysheet.create({
container: 'lucky-container',
data: exportInfo.sheets,
title: '',
lang: 'en',
showinfobar: false,
showstatisticBar: false,
sheetBottomConfig: true,
allowUpdate: true,
enableAddRow: true,
enableAddCol: true,
showToolbar: true,
plugins: [],
defaultFontSize: 11,
defaultFontName: 'Nanum Gothic',
});
}, function(err) {
container.innerHTML = 'Excel 변환 오류: ' + escHtml(String(err?.message || err)) + '
';
});
});
})
.catch(function(e) {
container.innerHTML = '불러오기 실패: ' + escHtml(e.message) + '
';
});
}
function minimizeLuckyModal() {
var box = document.getElementById('lucky-modal-box');
var modal = document.getElementById('lucky-modal');
var btn = event.target;
if (!box || !modal) return;
var minimized = box.dataset.minimized === '1';
if (minimized) {
box.style.height = box._savedHeight || '92vh';
box.dataset.minimized = '0';
btn.textContent = '–';
btn.title = '최소화';
if (!box._isAbsolute) { modal.style.alignItems = 'center'; modal.style.justifyContent = 'center'; }
} else {
box._savedHeight = box.style.height || '92vh';
box.dataset.minimized = '1';
btn.innerHTML = '◻';
btn.title = '복원';
if (box._isAbsolute) {
box.style.top = (window.innerHeight - 52) + 'px';
box.style.height = 'auto';
} else {
box.style.height = 'auto';
modal.style.alignItems = 'flex-end';
modal.style.justifyContent = 'center';
modal.style.paddingBottom = '0';
}
}
}
function closeLuckyModal() {
var modal = document.getElementById('lucky-modal');
var box = document.getElementById('lucky-modal-box');
if (modal) { modal.style.display = 'none'; modal.style.alignItems = 'center'; modal.style.justifyContent = 'center'; }
if (box) {
box.style.position = 'relative';
box.style.left = ''; box.style.top = '';
box.style.width = '96vw'; box.style.height = '92vh';
box._isAbsolute = false; box._savedHeight = null;
box.dataset.minimized = '0';
}
try { luckysheet.destroy(); } catch(e) {}
_luckyCurrentSaveUrl = null;
}
// Close on backdrop click
document.getElementById('lucky-modal')?.addEventListener('click', function(e) {
if (e.target === this) closeLuckyModal();
});
function initLuckyModalInteraction() {
var box = document.getElementById('lucky-modal-box');
var modal = document.getElementById('lucky-modal');
if (!box || box._interactInit) return;
box._interactInit = true;
// Inject 8 resize handles
[
{ dir:'n', css:'top:0;left:10px;right:10px;height:6px;cursor:n-resize' },
{ dir:'s', css:'bottom:0;left:10px;right:10px;height:6px;cursor:s-resize' },
{ dir:'e', css:'right:0;top:10px;bottom:10px;width:6px;cursor:e-resize' },
{ dir:'w', css:'left:0;top:10px;bottom:10px;width:6px;cursor:w-resize' },
{ dir:'ne', css:'top:0;right:0;width:12px;height:12px;cursor:ne-resize' },
{ dir:'nw', css:'top:0;left:0;width:12px;height:12px;cursor:nw-resize' },
{ dir:'se', css:'bottom:0;right:0;width:12px;height:12px;cursor:se-resize' },
{ dir:'sw', css:'bottom:0;left:0;width:12px;height:12px;cursor:sw-resize' },
].forEach(function(d) {
var h = document.createElement('div');
h.className = 'lucky-rh';
h.dataset.dir = d.dir;
h.style.cssText = 'position:absolute;z-index:200;' + d.css;
box.appendChild(h);
});
function toAbsolute() {
if (box._isAbsolute) return;
var rect = box.getBoundingClientRect();
modal.style.display = 'block';
box.style.position = 'absolute';
box.style.left = rect.left + 'px';
box.style.top = rect.top + 'px';
box.style.width = rect.width + 'px';
box.style.height = rect.height + 'px';
box._isAbsolute = true;
}
// Title bar drag
var header = box.firstElementChild;
if (header) {
header.style.cursor = 'grab';
header.addEventListener('mousedown', function(e) {
if (e.target.tagName === 'BUTTON') return;
toAbsolute();
var rect = box.getBoundingClientRect();
var ox = e.clientX - rect.left, oy = e.clientY - rect.top;
header.style.cursor = 'grabbing';
function onMove(e) {
box.style.left = (e.clientX - ox) + 'px';
box.style.top = Math.max(0, e.clientY - oy) + 'px';
}
function onUp() {
header.style.cursor = 'grab';
document.removeEventListener('mousemove', onMove);
document.removeEventListener('mouseup', onUp);
}
document.addEventListener('mousemove', onMove);
document.addEventListener('mouseup', onUp);
e.preventDefault();
});
}
// Resize handle drag
box.addEventListener('mousedown', function(e) {
var handle = e.target.closest ? e.target.closest('.lucky-rh') : null;
if (!handle) return;
toAbsolute();
var dir = handle.dataset.dir;
var rect = box.getBoundingClientRect();
var sx = e.clientX, sy = e.clientY;
var sl = rect.left, st = rect.top, sw = rect.width, sh = rect.height;
document.body.style.userSelect = 'none';
document.body.style.cursor = handle.style.cursor;
function onMove(e) {
var dx = e.clientX - sx, dy = e.clientY - sy;
var nl = sl, nt = st, nw = sw, nh = sh;
if (dir.includes('e')) nw = Math.max(380, sw + dx);
if (dir.includes('s')) nh = Math.max(180, sh + dy);
if (dir.includes('w')) { nw = Math.max(380, sw - dx); nl = sl + sw - nw; }
if (dir.includes('n')) { nh = Math.max(180, sh - dy); nt = st + sh - nh; }
box.style.left = nl + 'px'; box.style.top = nt + 'px';
box.style.width = nw + 'px'; box.style.height = nh + 'px';
}
function onUp() {
document.body.style.userSelect = '';
document.body.style.cursor = '';
document.removeEventListener('mousemove', onMove);
document.removeEventListener('mouseup', onUp);
}
document.addEventListener('mousemove', onMove);
document.addEventListener('mouseup', onUp);
e.preventDefault();
e.stopPropagation();
});
}
function saveLuckyModal() {
var url = _luckyCurrentSaveUrl;
if (!url) { alert('저장할 파일 경로가 없습니다.'); return; }
var btn = document.getElementById('lucky-modal-save');
var relPath = url.replace(/^\/api\/files\//, '');
loadSheetJS(function() {
try {
// Luckysheet → SheetJS aoa conversion
var sheets = luckysheet.getAllSheets();
var wb = XLSX.utils.book_new();
sheets.forEach(function(sheet) {
var celldata = sheet.celldata || [];
if (!celldata.length) { XLSX.utils.book_append_sheet(wb, XLSX.utils.aoa_to_sheet([[]]), sheet.name || 'Sheet'); return; }
var maxR = 0, maxC = 0;
celldata.forEach(function(c) { maxR = Math.max(maxR, c.r); maxC = Math.max(maxC, c.c); });
var aoa = [];
for (var r = 0; r <= maxR; r++) aoa.push(new Array(maxC + 1).fill(''));
celldata.forEach(function(c) {
if (c.v == null) return;
var val = (typeof c.v === 'object') ? (c.v.m !== undefined ? c.v.m : (c.v.v !== undefined ? c.v.v : '')) : c.v;
aoa[c.r][c.c] = val;
});
XLSX.utils.book_append_sheet(wb, XLSX.utils.aoa_to_sheet(aoa), sheet.name || 'Sheet');
});
var xlsxBuf = XLSX.write(wb, { bookType: 'xlsx', type: 'array' });
btn.disabled = true; btn.textContent = '저장 중…';
fetch('/api/files/' + relPath, {
method: 'PUT', headers: authHeaders(),
body: new Blob([xlsxBuf], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }),
})
.then(function(r) { return r.json(); })
.then(function(res) {
btn.disabled = false; btn.innerHTML = '💾 저장';
if (res.success) { btn.textContent = '✓ 저장됨'; setTimeout(function() { btn.innerHTML = '💾 저장'; }, 2000); }
else alert('저장 실패: ' + (res.error || '알 수 없는 오류'));
})
.catch(function(e) { btn.disabled = false; btn.innerHTML = '💾 저장'; alert('저장 오류: ' + e.message); });
} catch(e) { alert('Excel 변환 오류: ' + e.message); }
});
}
function toggleStagedExcelPreview(id) {
var el = document.getElementById('excel-staged-' + id);
if (!el) return;
el.style.display = el.style.display === 'none' ? 'block' : 'none';
}
function toggleTxtEmbedBtn(btn) {
var card = btn.closest('.txt-card');
if (!card) return;
var container = card.querySelector('.txt-preview-body');
if (!container) return;
var url = encodeURI(btn.getAttribute('data-txt-url'));
if (container.style.display === 'none') {
container.style.display = 'block';
btn.innerHTML = '▲ 접기';
if (!container.dataset.loaded) {
container.textContent = '불러오는 중…';
fetch(url, { headers: authHeaders() })
.then(function(r) {
if (!r.ok) throw new Error('HTTP ' + r.status);
return r.text();
})
.then(function(t) {
container.textContent = t;
container.dataset.loaded = '1';
})
.catch(function(e) {
container.textContent = '텍스트를 불러올 수 없습니다. (' + e.message + ')';
});
}
} else {
container.style.display = 'none';
btn.innerHTML = '◀ 미리보기';
}
}
function togglePptxPreview(fileUrl, containerId, btn) {
var container = document.getElementById(containerId);
if (!container) return;
var isVisible = container.style.display !== 'none';
if (isVisible) {
container.style.display = 'none';
btn.innerHTML = '◀ Preview';
return;
}
// Show loading state
container.style.display = 'flex';
container.innerHTML = 'Generating preview...';
btn.innerHTML = '▲ Preview';
btn.classList.add('loading');
// Extract relative path from various URL formats:
// /api/files/Folder/file.pptx → Folder/file.pptx
// https://host/api/files/Folder/file.pptx → Folder/file.pptx
// Folder/file.pptx → Folder/file.pptx
var filePath = fileUrl;
// Handle sandboxed environments where origin may be sandbox:/
if (fileUrl.charAt(0) === '/') {
filePath = fileUrl; // Already an absolute path, use as-is
} else {
try { filePath = new URL(fileUrl, window.location.origin).pathname; } catch(e) {}
}
filePath = filePath.replace(/^\/api\/files\/?/, '');
// If the link itself is a preview URL, extract the path query parameter
var previewMatch = filePath.match(/^\/api\/pptx\/preview\?path=(.+)$/);
if (previewMatch) {
filePath = previewMatch[1];
}
// Safely decode - handle already-decoded or invalid encodings
try { filePath = decodeURIComponent(filePath); } catch(e) {}
fetch('/api/pptx/preview?path=' + encodeURIComponent(filePath), {
headers: authHeaders()
})
.then(function(r) { return r.json(); })
.then(function(data) {
btn.classList.remove('loading');
if (!data.success || !data.images || !data.images.length) {
container.innerHTML = 'Preview failed: ' + escHtml(data.error || 'No slides found') + '';
return;
}
// img 태그는 Bearer 헤더를 보낼 수 없어서 auth 없이 요청 → user context 미설정 → 파일 못 찾음.
// fetch()로 auth 포함해 가져온 뒤 Blob URL로 교체.
Promise.all(data.images.map(function(imgUrl) {
return fetch(imgUrl, { headers: authHeaders() })
.then(function(r) { return r.ok ? r.blob() : Promise.reject(new Error('HTTP ' + r.status)); })
.then(function(blob) { return URL.createObjectURL(blob); })
.catch(function() { return imgUrl; });
})).then(function(srcs) {
container.innerHTML = srcs.map(function(src, i) {
return '
';
}).join('');
});
})
.catch(function(err) {
btn.classList.remove('loading');
container.innerHTML = 'Preview error: ' + escHtml(err.message) + '';
});
}
function handleImageUpload(input) {
var file = input.files && input.files[0];
if (!file) return;
input.value = '';
var formData = new FormData();
formData.append('image', file);
fetch('/api/upload/image', {
method: 'POST',
headers: authHeaders(),
body: formData
})
.then(function(r) { return r.json(); })
.then(function(data) {
if (data.success && data.path) {
var chatInput = document.getElementById('chat-input');
var imgRef = '';
chatInput.value = chatInput.value ? chatInput.value + '\n' + imgRef : imgRef;
chatInput.focus();
} else {
alert(data.error || 'Upload failed');
}
})
.catch(function(e) { alert('Upload error: ' + e.message); });
}
// ── 파일 첨부 (이미지·PDF·Excel·txt·csv 등) ─────────────────────────────────
function handleFileUpload(input) {
var file = input.files && input.files[0];
if (!file) return;
input.value = '';
var name = file.name.toLowerCase();
// MIDI → 악보 변환
if (/\.(mid|midi)$/.test(name) || file.type === 'audio/midi' || file.type === 'audio/x-midi') {
uploadMidiAndRender(file);
return;
}
// 이미지
if (file.type.startsWith('image/')) {
var imgForm = new FormData();
imgForm.append('image', file);
fetch('/api/upload/image', { method: 'POST', headers: authHeaders(), body: imgForm })
.then(function(r) { return r.json(); })
.then(function(data) {
if (data.success && data.path) {
insertIntoChatInput('');
} else { alert(data.error || '이미지 업로드 실패'); }
})
.catch(function(e) { alert('업로드 오류: ' + e.message); });
// txt / md
} else if (/\.(txt|md)$/.test(name)) {
var r1 = new FileReader();
r1.onload = function(e) {
var ext = name.split('.').pop();
stageAttachment(file.name, null, '**[파일: ' + file.name + ']**\n```' + ext + '\n' + e.target.result + '\n```', 'text');
};
r1.readAsText(file, 'UTF-8');
// CSV
} else if (/\.csv$/.test(name)) {
var r2 = new FileReader();
r2.onload = function(e) {
stageAttachment(file.name, null, '**[파일: ' + file.name + ']**\n```csv\n' + e.target.result + '\n```', 'csv');
};
r2.readAsText(file, 'UTF-8');
// Excel
} else if (/\.(xlsx|xls)$/.test(name)) {
var xlsxFile = file;
var xlsxForm = new FormData();
xlsxForm.append('file', xlsxFile);
var xlsxUploadPromise = fetch('/api/upload/file', { method: 'POST', headers: authHeaders(), body: xlsxForm })
.then(function(r) { return r.json(); }).catch(function() { return {}; });
loadSheetJS(function() {
var r3 = new FileReader();
r3.onload = function(e) {
try {
var wb = XLSX.read(e.target.result, { type: 'array' });
var markdownContent = '**[파일: ' + xlsxFile.name + ']**\n\n';
var previewHtml = '';
wb.SheetNames.forEach(function(sn) {
var rows = XLSX.utils.sheet_to_json(wb.Sheets[sn], { header: 1 });
markdownContent += '### ' + sn + '\n';
rows.forEach(function(row) { markdownContent += '| ' + row.map(function(c) { return String(c ?? ''); }).join(' | ') + ' |\n'; });
markdownContent += '\n';
previewHtml += '' + escHtml(sn) + '
';
previewHtml += '';
rows.slice(0, 30).forEach(function(row, ri) {
previewHtml += '';
row.forEach(function(cell) {
var tag = ri === 0 ? 'th' : 'td';
previewHtml += '<' + tag + ' style="border:1px solid var(--line);padding:3px 7px;text-align:left;white-space:nowrap">' + escHtml(String(cell ?? '')) + '' + tag + '>';
});
previewHtml += '
';
});
if (rows.length > 30) previewHtml += '| … ' + (rows.length - 30) + '행 더 |
';
previewHtml += '
';
});
xlsxUploadPromise.then(function(d) {
var fileUrl = (d.success && d.path) ? '/api/files/' + d.path : null;
stageAttachment(xlsxFile.name, fileUrl, markdownContent, 'excel', { excelPreviewHtml: previewHtml });
});
} catch(err) { alert('Excel 파싱 오류: ' + err.message); }
};
r3.readAsArrayBuffer(xlsxFile);
});
// PDF — 업로드 + PDF.js 텍스트 추출 + txt 자동 저장
} else if (/\.pdf$/.test(name)) {
var pdfFile = file;
var pdfForm = new FormData();
pdfForm.append('file', pdfFile);
var uploadPromise = fetch('/api/upload/file', { method: 'POST', headers: authHeaders(), body: pdfForm })
.then(function(r) { return r.json(); });
var extractPromise = new Promise(function(resolve) {
loadPdfJs(function() {
var fr = new FileReader();
fr.onload = function(e) {
pdfjsLib.getDocument({ data: new Uint8Array(e.target.result) }).promise
.then(function(pdf) {
var pages = [];
for (var i = 1; i <= pdf.numPages; i++) pages.push(pdf.getPage(i));
Promise.all(pages).then(function(ps) {
return Promise.all(ps.map(function(p, pi) {
return p.getTextContent().then(function(tc) {
return pdfPageToText(tc.items, pi + 1, pdf.numPages);
});
}));
}).then(function(texts) {
resolve({ text: texts.join('\n\n'), pages: pdf.numPages });
}).catch(function() { resolve({ text: '', pages: 0 }); });
}).catch(function() { resolve({ text: '', pages: 0 }); });
};
fr.readAsArrayBuffer(pdfFile);
});
});
Promise.all([uploadPromise, extractPromise]).then(function(res) {
var upData = res[0], extracted = res[1];
var fileUrl = (upData.success && upData.path) ? '/api/files/' + upData.path : null;
var fullText = (extracted.text || '').trim();
if (fullText) {
var txtName = pdfFile.name.replace(/\.pdf$/i, '.txt');
var txtBlob = new Blob([extracted.text], { type: 'text/plain; charset=utf-8' });
var txtForm = new FormData();
txtForm.append('file', new File([txtBlob], txtName, { type: 'text/plain' }));
fetch('/api/upload/file', { method: 'POST', headers: authHeaders(), body: txtForm })
.then(function(r) { return r.json(); })
.then(function(d) {
var txtUrl = (d.success && d.path) ? '/api/files/' + d.path : null;
if (txtUrl) addProcessEntry('info', '📝 텍스트 저장: ' + txtName);
var content = '[' + pdfFile.name + '](' + (fileUrl || '') + ')';
if (txtUrl) content += '\n[' + txtName + '](' + txtUrl + ')';
content += '\n\n이 파일로 무엇을 도와드릴까요? 사용자가 원하는 작업을 직접 알려주기 전까지 어떤 변환이나 생성 작업도 시작하지 마세요.';
stageAttachment(pdfFile.name, fileUrl, content, 'pdf', {
txtPreview: fullText.slice(0, 500),
txtUrl: txtUrl,
txtName: txtName,
pages: extracted.pages
});
}).catch(function() {
var content = '[' + pdfFile.name + '](' + (fileUrl || '') + ')\n\n이 파일로 무엇을 도와드릴까요?';
stageAttachment(pdfFile.name, fileUrl, content, 'pdf', {
txtPreview: fullText.slice(0, 500),
pages: extracted.pages
});
});
} else {
var content = '[' + pdfFile.name + '](' + (fileUrl || '') + ')\n*(텍스트 추출 불가 — 스캔 이미지이거나 보안 잠금 PDF)*';
stageAttachment(pdfFile.name, fileUrl, content, 'pdf');
}
}).catch(function(e) { alert('PDF 처리 오류: ' + e.message); });
// 기타 (docx/pptx 등)
} else {
var otherForm = new FormData();
otherForm.append('file', file);
fetch('/api/upload/file', { method: 'POST', headers: authHeaders(), body: otherForm })
.then(function(r) { return r.json(); })
.then(function(data) {
if (data.success && data.path) {
insertIntoChatInput('[' + file.name + '](/api/files/' + data.path + ')');
} else { alert(data.error || '파일 업로드 실패'); }
})
.catch(function(e) { alert('업로드 오류: ' + e.message); });
}
}
function insertIntoChatInput(text) {
var el = document.getElementById('chat-input');
el.value = el.value ? el.value + '\n' + text : text;
el.focus();
el.dispatchEvent(new Event('input'));
}
// ── 첨부 파일 스테이징 ──────────────────────────────────────────────────────
var stagedAttachments = [];
function stageAttachment(name, url, content, type, extra) {
var id = 'att-' + Date.now() + '-' + Math.random().toString(36).substr(2, 5);
var att = { id: id, name: name, url: url, content: content, type: type };
if (extra) Object.assign(att, extra);
stagedAttachments.push(att);
renderStagedAttachments();
}
function removeStagedAttachment(id) {
stagedAttachments = stagedAttachments.filter(function(a) { return a.id !== id; });
renderStagedAttachments();
}
function clearStagedAttachments() {
stagedAttachments = [];
renderStagedAttachments();
}
function renderStagedAttachments() {
var container = document.getElementById('staged-attachments');
if (!container) return;
if (!stagedAttachments.length) { container.style.display = 'none'; container.innerHTML = ''; return; }
container.style.display = 'flex';
container.innerHTML = stagedAttachments.map(function(a) {
var icon = a.type === 'pdf' ? '📄' : a.type === 'excel' ? '📊' : a.type === 'csv' ? '📋' : '📝';
var btnStyle = 'background:none;border:1px solid var(--brand);cursor:pointer;font-size:11px;color:var(--brand);padding:2px 6px;border-radius:4px';
// PDF 카드
if (a.type === 'pdf') {
var txtPreview = a.txtPreview ? escHtml(a.txtPreview) + (a.txtPreview.length >= 500 ? '...' : '') : '';
var pagesInfo = a.pages ? ' (' + a.pages + '페이지)' : '';
return ''
+ '
'
+ '
📄'
+ '
' + escHtml(a.name) + ''
+ (txtPreview ? '
' : '')
+ (a.url ? '
' : '')
+ (a.txtUrl ? '
📝⬇' : '')
+ '
'
+ '
'
+ (pagesInfo ? '
텍스트 추출 완료' + pagesInfo + '
' : '')
+ (txtPreview ? '
' + txtPreview + '
' : '')
+ '
'
+ '
';
}
// TXT 자동저장 카드 (미리보기 + 다운로드)
if (a.type === 'txt-auto') {
var preview = escHtml((a.content || '').slice(0, 300)) + ((a.content || '').length > 300 ? '...' : '');
return ''
+ '
'
+ '
📝'
+ '
' + escHtml(a.name) + ''
+ '
'
+ (a.url ? '
⬇ 저장' : '')
+ '
'
+ '
'
+ '
' + preview + '
'
+ '
';
}
// excel 카드
if (a.type === 'excel') {
return ''
+ '
'
+ '
📊'
+ '
' + escHtml(a.name) + ''
+ (a.excelPreviewHtml ? '
' : '')
+ (a.url ? '
⬇ 저장' : '')
+ '
'
+ '
'
+ (a.excelPreviewHtml ? '
' + a.excelPreviewHtml + '
' : '')
+ '
';
}
// 기타 카드 (text, csv)
return ''
+ '
'
+ '' + icon + ''
+ '' + escHtml(a.name) + ''
+ ''
+ '
'
+ '
';
}).join('');
}
function toggleStagedPdfPreview(id) {
var el = document.getElementById('pdf-preview-' + id);
if (!el) return;
var att = stagedAttachments.find(function(a) { return a.id === id; });
if (el.style.display === 'none') {
el.style.display = 'block';
var iframe = el.querySelector('iframe');
if (iframe && att && att.url) iframe.src = att.url; // 항상 명시 설정
} else {
el.style.display = 'none';
}
}
function toggleStagedTxtPreview(id) {
var el = document.getElementById('txt-preview-' + id);
if (!el) return;
el.style.display = el.style.display === 'none' ? 'block' : 'none';
}
function toggleStagedTxtPdfPreview(id) {
var el = document.getElementById('txt-pdf-preview-' + id);
if (!el) return;
el.style.display = el.style.display === 'none' ? 'block' : 'none';
}
// PDF 카드에 txt 저장 버튼 동적 삽입 (txt 업로드 완료 후 호출)
function addTxtButtonToPdfCard(pdfName, txtName, txtUrl) {
// stagedAttachments에서 해당 PDF 찾기
var att = stagedAttachments.find(function(a) { return a.name === pdfName && a.type === 'pdf'; });
if (!att) return;
// txt 정보를 attachment에 저장 (전송 시 displayMessage에 포함용)
att.txtName = txtName;
att.txtUrl = txtUrl;
// 카드 DOM에 txt 버튼 삽입
var card = document.getElementById('card-' + att.id);
if (!card) return;
var btnRow = card.querySelector('div');
if (!btnRow) return;
var btnStyle = 'background:none;border:1px solid var(--brand);cursor:pointer;font-size:11px;color:var(--brand);padding:2px 6px;border-radius:4px;text-decoration:none;display:inline-block';
var txtBtn = document.createElement('a');
txtBtn.href = txtUrl;
txtBtn.download = txtName;
txtBtn.title = txtName + ' 다운로드';
txtBtn.style.cssText = btnStyle;
txtBtn.innerHTML = '📝 txt ⬇';
// ✕ 버튼 바로 앞에 삽입
var closeBtn = btnRow.lastElementChild;
btnRow.insertBefore(txtBtn, closeBtn);
}
// ── PDF 페이지 텍스트 구조화 ────────────────────────────────────────────────
function pdfPageToText(items, pageNum, totalPages) {
// filter out empty items
var valid = items.filter(function(it) { return it.str && it.str.trim() !== ''; });
if (!valid.length) return '';
// sort top→bottom (y desc), left→right (x asc)
valid.sort(function(a, b) {
var ay = a.transform[5], by = b.transform[5];
if (Math.abs(ay - by) > 2) return by - ay;
return a.transform[4] - b.transform[4];
});
// group items into lines by y-coordinate proximity
var LINE_Y_THRESH = 3;
var lines = [];
var cur = null;
valid.forEach(function(item) {
var y = item.transform[5];
var h = item.height || 10;
if (!cur || Math.abs(y - cur.y) > LINE_Y_THRESH) {
cur = { y: y, h: h, items: [item] };
lines.push(cur);
} else {
cur.items.push(item);
if (h > cur.h) cur.h = h;
}
});
// build output: join items per line; insert blank line on large y-gaps (paragraph break)
var out = [];
if (totalPages > 1) out.push('── ' + pageNum + '페이지 ──');
for (var i = 0; i < lines.length; i++) {
var lineText = lines[i].items.map(function(it) { return it.str; }).join('').replace(/\s+/g, ' ').trim();
if (!lineText) continue;
// paragraph break: gap > 1.4× current line height
if (i > 0) {
var gap = lines[i - 1].y - lines[i].y;
if (gap > lines[i - 1].h * 1.4) out.push('');
}
// fix hyphenation: merge "단어-\n다음줄" → "단어다음줄"
if (out.length && out[out.length - 1].endsWith('-')) {
out[out.length - 1] = out[out.length - 1].slice(0, -1) + lineText;
} else {
out.push(lineText);
}
}
return out.join('\n');
}