한글뷰어 기능수정

This commit is contained in:
koj729
2026-06-18 08:52:23 +09:00
parent cb0c42fbeb
commit 9268e4e6bc
38 changed files with 2544 additions and 211 deletions

View File

@@ -760,6 +760,7 @@
<th>카테고리</th>
<th>용량 제한</th>
<th>상태</th>
<th style="width: 70px;">과업개요</th>
<th>관리</th>
</tr>
</thead>
@@ -1004,10 +1005,10 @@
<h3 class="card-title">🔎 시스템 활동 로그 조회 (tb_log)</h3>
</div>
<div class="form-row">
<input type="text" class="text-input" id="search-log-user" placeholder="사용자 ID 검색...">
<input type="text" class="text-input" id="search-log-project" placeholder="프로젝트명 검색...">
<input type="text" class="text-input" id="filter-log-action" placeholder="조작 액션 검색...">
<button class="btn btn-secondary" onclick="renderAuditLogs()">활동 로그 필터링</button>
<input type="text" class="text-input" id="search-log-user" placeholder="사용자 ID 검색..." onkeyup="if(event.key === 'Enter') renderAuditLogs()">
<input type="text" class="text-input" id="search-log-project" placeholder="프로젝트명 검색..." onkeyup="if(event.key === 'Enter') renderAuditLogs()">
<input type="text" class="text-input" id="filter-log-action" placeholder="조작 액션 검색..." onkeyup="if(event.key === 'Enter') renderAuditLogs()">
<button class="btn btn-secondary" onclick="renderAuditLogs()">검색</button>
</div>
<div class="table-wrapper">
<table class="admin-table">
@@ -1194,11 +1195,11 @@
<option value="overseas">해외 프로젝트 (overseas)</option>
</select>
</div>
<div class="form-group">
<label for="form-project-storage">스토리지 제한 (GB)</label>
<input type="number" class="text-input" id="form-project-storage" value="10" min="1" max="1000">
</div>
<div class="form-row">
<div class="form-group">
<label for="form-project-storage">스토리지 제한 (GB)</label>
<input type="number" class="text-input" id="form-project-storage" value="10" min="1" max="1000">
</div>
<div class="form-group">
<label for="form-project-active">운영 상태</label>
<select class="select-input" id="form-project-active">
@@ -1206,6 +1207,13 @@
<option value="false">일시잠금 (Inactive)</option>
</select>
</div>
<div class="form-group">
<label for="form-project-overview">과업개요 여부</label>
<select class="select-input" id="form-project-overview">
<option value="true" selected>사용 (True)</option>
<option value="false">미사용 (False)</option>
</select>
</div>
</div>
<div style="display: flex; gap: 10px; justify-content: flex-end; margin-top: 10px;">
<button class="btn btn-secondary" type="button" onclick="closeProjectModal()">취소</button>
@@ -1616,6 +1624,7 @@
<td>${p.category_nm || p.category || '-'}</td>
<td>${p.storage_byte ? (Number(p.storage_byte) / (1024*1024*1024)).toFixed(0) + ' GB' : '0 GB'}</td>
<td><span class="badge ${p.is_active ? 'active' : 'inactive'}">${p.is_active ? '활성' : '비활성'}</span></td>
<td><span class="badge ${p.overview !== false ? 'active' : 'inactive'}">${p.overview !== false ? '사용' : '미사용'}</span></td>
<td>
<div class="action-btns" onclick="event.stopPropagation();">
<button class="btn btn-secondary btn-sm" onclick="openProjectModal('edit', '${p.project_id}')">수정</button>
@@ -1831,6 +1840,7 @@
document.getElementById('form-project-id').removeAttribute('readonly');
document.getElementById('form-project-id').disabled = false;
document.getElementById('project-submit-btn').innerText = '등록 하기';
document.getElementById('form-project-overview').value = 'true';
form.onsubmit = submitCreateProject;
} else {
document.getElementById('project-modal-title').innerText = '📝 프로젝트 상세 정보 수정';
@@ -1848,6 +1858,7 @@
document.getElementById('form-project-category').value = p.category || '';
document.getElementById('form-project-storage').value = p.storage_byte ? (Number(p.storage_byte) / (1024*1024*1024)).toFixed(0) : 10;
document.getElementById('form-project-active').value = p.is_active ? 'true' : 'false';
document.getElementById('form-project-overview').value = p.overview !== false ? 'true' : 'false';
}
} catch (err) {
console.error(err);
@@ -1869,7 +1880,8 @@
short_nm: document.getElementById('form-project-short').value.trim(),
category: document.getElementById('form-project-category').value,
limit_storage: Number(document.getElementById('form-project-storage').value),
is_active: document.getElementById('form-project-active').value === 'true'
is_active: document.getElementById('form-project-active').value === 'true',
overview: document.getElementById('form-project-overview').value === 'true'
};
try {
@@ -1893,7 +1905,8 @@
short_nm: document.getElementById('form-project-short').value.trim(),
category: document.getElementById('form-project-category').value,
limit_storage: Number(document.getElementById('form-project-storage').value),
is_active: document.getElementById('form-project-active').value === 'true'
is_active: document.getElementById('form-project-active').value === 'true',
overview: document.getElementById('form-project-overview').value === 'true'
};
try {
@@ -2147,6 +2160,7 @@
document.getElementById('form-user-id').removeAttribute('readonly');
document.getElementById('form-user-id').disabled = false;
document.getElementById('form-user-pw').required = true;
document.getElementById('form-user-pw').placeholder = '••••••••';
pwRow.style.display = 'flex'; // PW 보이기
document.getElementById('user-submit-btn').innerText = '등록 하기';
form.onsubmit = submitCreateUser;
@@ -2155,9 +2169,10 @@
document.getElementById('form-user-id').setAttribute('readonly', 'true');
document.getElementById('form-user-id').disabled = true;
// 패스워드 입력칸 숨기기 및 필수조건 해제
// 패스워드 필수조건 해제 및 노출 유지 (공백 시 미수정)
document.getElementById('form-user-pw').required = false;
pwRow.style.display = 'none';
document.getElementById('form-user-pw').placeholder = '변경할 비밀번호 입력 (공백 시 유지)';
pwRow.style.display = 'flex';
document.getElementById('user-submit-btn').innerText = '수정 하기';
@@ -2216,6 +2231,7 @@
async function submitEditUser(event, userId) {
event.preventDefault();
const payload = {
user_pw: document.getElementById('form-user-pw').value,
user_nm: document.getElementById('form-user-nm').value.trim(),
company: document.getElementById('form-user-company').value.trim(),
dept: document.getElementById('form-user-dept').value.trim(),

View File

@@ -370,7 +370,7 @@ export async function openNewWindowViewer() {
case 'pdf' :
case 'doc' :
case 'ppt' :
case 'pptx' :
case 'pptx':
case 'dwg' :
case 'dxf' :
case 'grm' :

View File

@@ -94,6 +94,22 @@ async function loadSystemPolicy() {
}
}
export function updateSystemPolicyCache(policy) {
if (policy) {
FOLDER_KEEP_POLICY_ACTIVE = policy.is_active ?? false;
if (FOLDER_KEEP_POLICY_ACTIVE) {
FOLDER_KEEP_FILE_THRESHOLD = Number(policy.limit_file_count) || 3;
FOLDER_KEEP_DAYS_THRESHOLD = Number(policy.limit_days) || 15;
} else {
FOLDER_KEEP_FILE_THRESHOLD = 3;
FOLDER_KEEP_DAYS_THRESHOLD = 15;
}
} else {
isPolicyLoaded = false;
}
}
// 브라우저 뒤로가기, 앞으로가기 이벤트
window.addEventListener('popstate', async (e)=>{
// console.log(e);

View File

@@ -22,6 +22,7 @@ import {
changeHeaderBtnStyle,
changeTreeItemStyle,
changeListItemStyle,
updateSystemPolicyCache,
} from './pageRenderer.js';
import { toggleModal } from './modalManager.js'
import { mgmtFunc_addClickLog } from './managementFunctions.js';
@@ -326,6 +327,27 @@ socket.on('popupNotice', (data)=>{
alert(text);
})
//// 보관 및 삭제 정책 변경 실시간 반영
socket.on('updateSystemPolicy_success', async (policy) => {
// 정책 캐시 갱신
updateSystemPolicyCache(policy);
// 트리 화면 갱신 (D-Day 타이머 갱신을 위해)
let userCurPath = getMyCurPath();
if (userCurPath) {
let pathSplit = userCurPath.split('/');
let extractedPath = extractPathByLength(pathSplit, 1);
let pageRanderingOption = {
scope: 'tree',
resourcePath: extractedPath,
userCurPath: userCurPath,
pushState: false,
debug: '정책 변경 실시간 갱신 - tree'
};
await preparePageRendering(pageRanderingOption);
}
})
//// 강제 로그아웃
socket.on('forcedLogout', () => {
alert('프로젝트 재시작으로 인해 자동으로 로그아웃됩니다.\n다시 로그인 후 사용해주세요.');
@@ -441,15 +463,16 @@ socket.on('addConvetPdfLog_success', async (resultData) => {
socket.on('convertPdf_failed', async (resultData) => {
console.log('-------- convertPdf_failed');
console.log(resultData);
let resourcePath = (resultData.jobData.resourcePath) ? resultData.jobData.resourcePath : resultData.jobProgress.resourcePath;
let dataId = (resultData.jobData.dataId) ? resultData.jobData.dataId : resultData.jobProgress.dataId;
let userInfoString = (resultData.jobData.userInfoString) ? resultData.jobData.userInfoString : resultData.jobProgress.userInfoString;
// 서버의 convertingDataArr에서 변환 실패한 파일 정보 삭제
let resourcePath = (resultData.jobData && resultData.jobData.resourcePath) ? resultData.jobData.resourcePath : (resultData.jobProgress ? resultData.jobProgress.resourcePath : '');
let dataId = (resultData.jobData && resultData.jobData.dataId) ? resultData.jobData.dataId : (resultData.jobProgress ? resultData.jobProgress.dataId : '');
let userInfoString = (resultData.jobData && resultData.jobData.userInfoString) ? resultData.jobData.userInfoString : (resultData.jobProgress ? resultData.jobProgress.userInfoString : '');
let failedReason = resultData.failedReason || '';
let removeConvertingDataParams = {
resourcePath: resourcePath,
dataId: dataId,
userInfoString: userInfoString
userInfoString: userInfoString,
stdout: failedReason
}
let removeConvertingDataResult = await axios.post(`${vars.path_name}/removeConvertingData`, { params: removeConvertingDataParams });
console.log(removeConvertingDataResult);

View File

@@ -573,7 +573,8 @@ export async function renderDocViewer(resourcePath, docId) {
let excelDirectArr = ['xls', 'xlsx', 'xlsm'];
let hwpDirectArr = ['hwp', 'hwpx'];
let wordDirectArr = ['docx'];
let isDirectView = excelDirectArr.includes(ext) || hwpDirectArr.includes(ext) || wordDirectArr.includes(ext);
let pptxDirectArr = [];
let isDirectView = excelDirectArr.includes(ext) || hwpDirectArr.includes(ext) || wordDirectArr.includes(ext) || pptxDirectArr.includes(ext);
let selectedDoc = docVars.allDocData?.find((doc) => doc.doc_id === docId);
let previewKey = selectedDoc?.preview_key;
@@ -617,12 +618,13 @@ export async function renderDocViewer(resourcePath, docId) {
let threeArr = ['glb', 'gltf', 'obj', 'stl', 'fbx', '3dm'];
let allArr = [...pdfArr, ...gsimArr, ...ifcArr, ...imageArr, ...videoArr, ...textArr, ...urlArr, ...zipArr, ...threeArr];
if (allArr.includes(ext)) {
let pdfArrFiltered = pdfArr.filter(e => !excelDirectArr.includes(e) && !hwpDirectArr.includes(e) && !wordDirectArr.includes(e));
let pdfArrFiltered = pdfArr.filter(e => !excelDirectArr.includes(e) && !hwpDirectArr.includes(e) && !wordDirectArr.includes(e) && !pptxDirectArr.includes(e));
if (pdfArrFiltered.includes(ext)) viewerPdf(PresignedUrl);
if (excelDirectArr.includes(ext)) viewerExcel(PresignedUrl);
if (hwpDirectArr.includes(ext)) viewerHwp(PresignedUrl);
if (wordDirectArr.includes(ext)) viewerWord(PresignedUrl);
if (pptxDirectArr.includes(ext)) viewerPptx(PresignedUrl);
if (gsimArr.includes(ext)) viewerGsim(PresignedUrl);
if (ifcArr.includes(ext)) viewerIfc(PresignedUrl);
if (threeArr.includes(ext)) viewer3d(PresignedUrl);
@@ -860,18 +862,54 @@ export async function renderDocViewer(resourcePath, docId) {
const container = document.createElement('div');
container.style.width = '100%';
container.style.height = '100%';
container.style.overflow = 'auto';
container.style.overflowX = 'hidden';
container.style.overflowY = 'auto';
container.style.padding = '20px';
container.style.boxSizing = 'border-box';
container.style.background = '#f5f5f5';
const styleEl = document.createElement('style');
styleEl.textContent = `
.hwp-inner-container {
background: #ffffff;
margin: 0 auto;
max-width: 800px;
box-shadow: 0 4px 10px rgba(0,0,0,0.1);
padding: 30px !important;
box-sizing: border-box !important;
min-height: 100%;
}
.hwp-inner-container > div > div {
max-width: 100% !important;
height: auto !important;
box-sizing: border-box !important;
padding-left: 20px !important;
padding-right: 20px !important;
margin-bottom: 20px !important;
}
.hwp-inner-container table {
max-width: 100% !important;
width: 100% !important;
table-layout: fixed !important;
}
.hwp-inner-container img {
max-width: 100% !important;
height: auto !important;
}
@media (max-width: 600px) {
.hwp-inner-container {
padding: 10px !important;
}
.hwp-inner-container > div > div {
padding-left: 10px !important;
padding-right: 10px !important;
}
}
`;
container.appendChild(styleEl);
const hwpInner = document.createElement('div');
hwpInner.style.background = '#ffffff';
hwpInner.style.margin = '0 auto';
hwpInner.style.maxWidth = '800px';
hwpInner.style.boxShadow = '0 4px 10px rgba(0,0,0,0.1)';
hwpInner.style.padding = '40px';
hwpInner.style.minHeight = '100%';
hwpInner.classList.add('hwp-inner-container');
container.appendChild(hwpInner);
docVars.viewer.appendChild(container);
@@ -896,6 +934,281 @@ export async function renderDocViewer(resourcePath, docId) {
docVars.viewer.dataset.viewerType = 'hwp';
}
function viewerPptx(presignedUrl) {
docVars.viewer.innerHTML = '<div style="display:flex;justify-content:center;align-items:center;height:100%;font-size:1.2rem;color:#666;background:#fff;">PPTX 문서를 불러오는 중...</div>';
initDocFallbackPdfButton(docId, resourcePath, objectKey, previewKey);
fetch(presignedUrl)
.then(res => {
if (!res.ok) throw new Error('PPTX fetch failed');
return res.arrayBuffer();
})
.then(async (arrayBuffer) => {
try {
const zip = await JSZip.loadAsync(arrayBuffer);
// Read presentation.xml to get slide size
const presentationXmlText = await zip.file("ppt/presentation.xml").async("text");
const parser = new DOMParser();
const presDoc = parser.parseFromString(presentationXmlText, "text/xml");
const sldSz = presDoc.getElementsByTagName("p:sldSz")[0];
const cx = sldSz ? (parseInt(sldSz.getAttribute("cx"), 10) || 12192000) : 12192000;
const cy = sldSz ? (parseInt(sldSz.getAttribute("cy"), 10) || 6858000) : 6858000;
const ratio = (cy / cx) * 100;
// Get slide files
const slideFiles = Object.keys(zip.files).filter(name => name.startsWith("ppt/slides/slide") && name.endsWith(".xml"));
slideFiles.sort((a, b) => {
const numA = parseInt(a.replace("ppt/slides/slide", "").replace(".xml", ""), 10);
const numB = parseInt(b.replace("ppt/slides/slide", "").replace(".xml", ""), 10);
return numA - numB;
});
docVars.viewer.innerHTML = '';
const slidesContainer = document.createElement('div');
slidesContainer.style.display = 'flex';
slidesContainer.style.flexDirection = 'column';
slidesContainer.style.gap = '20px';
slidesContainer.style.alignItems = 'center';
slidesContainer.style.background = '#f0f0f0';
slidesContainer.style.padding = '20px';
slidesContainer.style.width = '100%';
slidesContainer.style.height = '100%';
slidesContainer.style.overflow = 'auto';
slidesContainer.style.boxSizing = 'border-box';
docVars.viewer.appendChild(slidesContainer);
for (let i = 0; i < slideFiles.length; i++) {
const slideXmlText = await zip.file(slideFiles[i]).async("text");
const slideDoc = parser.parseFromString(slideXmlText, "text/xml");
const slideCard = document.createElement('div');
slideCard.className = 'pptx-slide-card';
slideCard.style.position = 'relative';
slideCard.style.width = '100%';
slideCard.style.maxWidth = '800px';
slideCard.style.backgroundColor = '#ffffff';
slideCard.style.boxShadow = '0 4px 10px rgba(0,0,0,0.1)';
slideCard.style.height = '0';
slideCard.style.paddingTop = ratio + '%';
slideCard.style.overflow = 'hidden';
slideCard.style.flexShrink = '0';
const slideContent = document.createElement('div');
slideContent.style.position = 'absolute';
slideContent.style.top = '0';
slideContent.style.left = '0';
slideContent.style.width = '100%';
slideContent.style.height = '100%';
slideCard.appendChild(slideContent);
slidesContainer.appendChild(slideCard);
// Parse relationships for this slide
const relMap = {};
try {
const slideName = slideFiles[i].split('/').pop();
const relsFileName = `ppt/slides/_rels/${slideName}.rels`;
const relsFile = zip.file(relsFileName);
if (relsFile) {
const relsXmlText = await relsFile.async("text");
const relsDoc = parser.parseFromString(relsXmlText, "text/xml");
const relationships = relsDoc.getElementsByTagName("Relationship");
for (let r = 0; r < relationships.length; r++) {
const id = relationships[r].getAttribute("Id");
const target = relationships[r].getAttribute("Target");
relMap[id] = target;
}
}
} catch (relErr) {
console.warn("Failed to parse relationships for slide:", slideFiles[i], relErr);
}
const elements = slideDoc.querySelectorAll('p\\:sp, sp, p\\:pic, pic, p\\:graphicFrame, graphicFrame');
for (const elem of elements) {
const xfrm = elem.querySelector('a\\:xfrm, xfrm');
if (!xfrm) continue;
const off = xfrm.querySelector('a\\:off, off');
const ext = xfrm.querySelector('a\\:ext, ext');
if (!off || !ext) continue;
const x = parseInt(off.getAttribute('x'), 10);
const y = parseInt(off.getAttribute('y'), 10);
const w = parseInt(ext.getAttribute('cx'), 10);
const h = parseInt(ext.getAttribute('cy'), 10);
const leftPct = (x / cx) * 100;
const topPct = (y / cy) * 100;
const widthPct = (w / cx) * 100;
const heightPct = (h / cy) * 100;
const itemDiv = document.createElement('div');
itemDiv.style.position = 'absolute';
itemDiv.style.left = leftPct + '%';
itemDiv.style.top = topPct + '%';
itemDiv.style.width = widthPct + '%';
itemDiv.style.height = heightPct + '%';
itemDiv.style.boxSizing = 'border-box';
const nodeName = elem.nodeName.toLowerCase();
if (nodeName.includes('pic')) {
let imgUrl = null;
try {
const blip = elem.querySelector('a\\:blip, blip');
const rId = blip ? (blip.getAttribute('r:embed') || blip.getAttribute('embed')) : null;
if (rId && relMap[rId]) {
const targetPath = relMap[rId].replace('../', 'ppt/');
const imgFile = zip.file(targetPath);
if (imgFile) {
const imgBlob = await imgFile.async("blob");
imgUrl = URL.createObjectURL(imgBlob);
}
}
} catch (imgErr) {
console.warn("Failed to extract slide image:", imgErr);
}
if (imgUrl) {
itemDiv.style.backgroundImage = `url("${imgUrl}")`;
itemDiv.style.backgroundRepeat = 'no-repeat';
itemDiv.style.backgroundPosition = 'center';
itemDiv.style.backgroundSize = 'contain';
} else {
itemDiv.style.border = '1px dashed #cccccc';
itemDiv.style.backgroundColor = '#f9f9f9';
itemDiv.style.display = 'flex';
itemDiv.style.alignItems = 'center';
itemDiv.style.justifyContent = 'center';
const label = document.createElement('span');
label.style.color = '#999999';
label.style.fontSize = '10px';
label.style.fontWeight = 'bold';
label.textContent = '[그림 영역]';
itemDiv.appendChild(label);
}
} else if (nodeName.includes('graphicframe')) {
const tbl = elem.querySelector('a\\:tbl, tbl');
if (tbl) {
const htmlTable = document.createElement('table');
htmlTable.style.width = '100%';
htmlTable.style.height = '100%';
htmlTable.style.borderCollapse = 'collapse';
htmlTable.style.fontSize = 'calc(0.4vw + 5px)';
htmlTable.style.fontFamily = 'sans-serif';
htmlTable.style.backgroundColor = '#ffffff';
htmlTable.style.boxShadow = '0 1px 3px rgba(0,0,0,0.05)';
const rows = tbl.querySelectorAll('a\\:tr, tr');
rows.forEach((row, rIdx) => {
const trEl = document.createElement('tr');
if (rIdx === 0) {
trEl.style.backgroundColor = '#f8f9fa';
trEl.style.fontWeight = '600';
} else if (rIdx % 2 === 0) {
trEl.style.backgroundColor = '#fafafa';
}
const cells = row.querySelectorAll('a\\:tc, tc');
cells.forEach(cell => {
const tdEl = document.createElement('td');
tdEl.style.border = '1px solid #e0e0e0';
tdEl.style.padding = '4px 6px';
tdEl.style.wordBreak = 'break-all';
tdEl.style.verticalAlign = 'middle';
const gridSpan = cell.getAttribute('gridSpan');
if (gridSpan) tdEl.setAttribute('colspan', gridSpan);
const rowSpan = cell.getAttribute('rowSpan');
if (rowSpan) tdEl.setAttribute('rowspan', rowSpan);
const txBody = cell.querySelector('a\\:txBody, txBody');
if (txBody) {
const paragraphs = txBody.querySelectorAll('a\\:p, p');
paragraphs.forEach(p => {
const runs = p.querySelectorAll('a\\:r, r');
let cellText = '';
runs.forEach(r => {
const t = r.querySelector('a\\:t, t');
if (t) cellText += t.textContent;
});
if (cellText.trim()) {
const pEl = document.createElement('p');
pEl.style.margin = '0';
pEl.style.lineHeight = '1.2';
pEl.textContent = cellText;
tdEl.appendChild(pEl);
}
});
}
trEl.appendChild(tdEl);
});
htmlTable.appendChild(trEl);
});
itemDiv.appendChild(htmlTable);
} else {
itemDiv.style.border = '1px dashed #dddddd';
itemDiv.style.backgroundColor = '#fdfdfd';
itemDiv.style.display = 'flex';
itemDiv.style.alignItems = 'center';
itemDiv.style.justifyContent = 'center';
const label = document.createElement('span');
label.style.color = '#aaaaaa';
label.style.fontSize = '10px';
label.style.fontWeight = 'bold';
label.textContent = '[차트 영역]';
itemDiv.appendChild(label);
}
} else {
const txBody = elem.querySelector('p\\:txBody, txBody');
if (txBody) {
itemDiv.style.overflow = 'hidden';
itemDiv.style.wordBreak = 'break-all';
itemDiv.style.fontSize = 'calc(0.5vw + 5px)';
itemDiv.style.fontFamily = 'sans-serif';
itemDiv.style.color = '#333333';
const paragraphs = txBody.querySelectorAll('a\\:p, p');
paragraphs.forEach(p => {
const runs = p.querySelectorAll('a\\:r, r');
let paraText = '';
runs.forEach(r => {
const t = r.querySelector('a\\:t, t');
if (t) paraText += t.textContent;
});
if (paraText.trim()) {
const pEl = document.createElement('p');
pEl.style.margin = '0 0 2px 0';
pEl.style.lineHeight = '1.2';
pEl.textContent = paraText;
itemDiv.appendChild(pEl);
}
});
} else {
itemDiv.style.border = '1px solid #eeeeee';
itemDiv.style.backgroundColor = 'rgba(0,0,0,0.01)';
}
}
slideContent.appendChild(itemDiv);
}
}
} catch (parseErr) {
console.error("PPTX parse error:", parseErr);
docVars.viewer.innerHTML = '<div style="display:flex;justify-content:center;align-items:center;height:100%;color:#d9534f;background:#fff;padding:20px;text-align:center;">PPTX 파싱 중 에러가 발생했습니다. 상단의 "PDF로 보기" 버튼을 이용해 주세요.</div>';
}
})
.catch(err => {
console.error(err);
docVars.viewer.innerHTML = '<div style="display:flex;justify-content:center;align-items:center;height:100%;color:#d9534f;background:#fff;">PPTX 문서를 불러오는데 실패했습니다.</div>';
});
docVars.viewer.dataset.viewerType = 'pptx';
}
async function viewerPdf(PresignedUrl) {
resetViewer();
@@ -1222,7 +1535,11 @@ document.querySelector('.official-doc-main .official-doc-preview .official-doc-p
//Presigned URL
let PresignedUrl;
await syncDocInfo(['official', 'attach', null]);
let objectKey = docVars.allDocData?.find((doc) => doc.doc_id === docId)?.preview_key;
let directViewExtArr = ['xls', 'xlsx', 'xlsm', 'docx', 'hwp', 'hwpx'];
let selectedDoc = docVars.allDocData?.find((doc) => doc.doc_id === docId);
let objectKey = directViewExtArr.includes(ext) ? selectedDoc?.object_key : selectedDoc?.preview_key;
if (objectKey == undefined || objectKey == `` || objectKey == null) {
return;
}
@@ -1250,19 +1567,26 @@ document.querySelector('.official-doc-main .official-doc-preview .official-doc-p
let open_ext = `pdf`;
switch (ext) {
case 'pdf':
case 'hwp':
case 'hwpx':
case 'xls':
case 'xlsm':
case 'ppt':
case 'pptx':
case 'doc':
case 'docx':
case 'dwg':
case 'dxf':
case 'grm':
open_ext = 'pdf';
break;
case 'hwp':
case 'hwpx':
open_ext = ext;
break;
case 'xls':
case 'xlsx':
case 'xlsm':
open_ext = ext;
break;
case 'docx':
open_ext = ext;
break;
case 'gsim':
open_ext = 'gsim';
break;

View File

@@ -44,6 +44,9 @@ if(data && Object.keys(data).length>0 && (data.$type == 'text'|| data.type == 't
case 'docx':
_openDocx(fullPath, data);
break;
case 'pptx':
_openPptx(fullPath, data);
break;
case 'hwp':
case 'hwpx':
_openHwp(fullPath, data);
@@ -812,18 +815,54 @@ function _openHwp(path, data) {
const container = document.createElement('div');
container.style.width = '100%';
container.style.height = '100%';
container.style.overflow = 'auto';
container.style.overflowX = 'hidden';
container.style.overflowY = 'auto';
container.style.padding = '20px';
container.style.boxSizing = 'border-box';
container.style.background = '#f5f5f5';
const styleEl = document.createElement('style');
styleEl.textContent = `
.hwp-inner-container {
background: #ffffff;
margin: 0 auto;
max-width: 800px;
box-shadow: 0 4px 10px rgba(0,0,0,0.1);
padding: 30px !important;
box-sizing: border-box !important;
min-height: 100%;
}
.hwp-inner-container > div > div {
max-width: 100% !important;
height: auto !important;
box-sizing: border-box !important;
padding-left: 20px !important;
padding-right: 20px !important;
margin-bottom: 20px !important;
}
.hwp-inner-container table {
max-width: 100% !important;
width: 100% !important;
table-layout: fixed !important;
}
.hwp-inner-container img {
max-width: 100% !important;
height: auto !important;
}
@media (max-width: 600px) {
.hwp-inner-container {
padding: 10px !important;
}
.hwp-inner-container > div > div {
padding-left: 10px !important;
padding-right: 10px !important;
}
}
`;
container.appendChild(styleEl);
const hwpInner = document.createElement('div');
hwpInner.style.background = '#ffffff';
hwpInner.style.margin = '0 auto';
hwpInner.style.maxWidth = '800px';
hwpInner.style.boxShadow = '0 4px 10px rgba(0,0,0,0.1)';
hwpInner.style.padding = '40px';
hwpInner.style.minHeight = '100%';
hwpInner.classList.add('hwp-inner-container');
container.appendChild(hwpInner);
viewer.appendChild(container);
@@ -844,4 +883,281 @@ function _openHwp(path, data) {
console.error(err);
viewer.innerHTML = '<div style="display:flex;justify-content:center;align-items:center;height:100%;color:#d9534f;background:#fff;">한글 문서를 불러오는데 실패했습니다.</div>';
});
}
function _openPptx(path, data) {
const viewer = document.getElementById('popup_viewer');
viewer.innerHTML = '<div style="display:flex;justify-content:center;align-items:center;height:100%;font-size:1.2rem;color:#666;background:#fff;">PPTX 문서를 불러오는 중...</div>';
if (dataId && path_name) {
initFallbackPdfButton(dataId, path_name, resourcePath);
}
fetch(path)
.then(res => {
if (!res.ok) throw new Error('PPTX fetch failed');
return res.arrayBuffer();
})
.then(async (arrayBuffer) => {
try {
const zip = await JSZip.loadAsync(arrayBuffer);
// Read presentation.xml to get slide size
const presentationXmlText = await zip.file("ppt/presentation.xml").async("text");
const parser = new DOMParser();
const presDoc = parser.parseFromString(presentationXmlText, "text/xml");
const sldSz = presDoc.getElementsByTagName("p:sldSz")[0];
const cx = sldSz ? (parseInt(sldSz.getAttribute("cx"), 10) || 12192000) : 12192000;
const cy = sldSz ? (parseInt(sldSz.getAttribute("cy"), 10) || 6858000) : 6858000;
const ratio = (cy / cx) * 100;
// Get slide files
const slideFiles = Object.keys(zip.files).filter(name => name.startsWith("ppt/slides/slide") && name.endsWith(".xml"));
slideFiles.sort((a, b) => {
const numA = parseInt(a.replace("ppt/slides/slide", "").replace(".xml", ""), 10);
const numB = parseInt(b.replace("ppt/slides/slide", "").replace(".xml", ""), 10);
return numA - numB;
});
viewer.innerHTML = '';
const slidesContainer = document.createElement('div');
slidesContainer.style.display = 'flex';
slidesContainer.style.flexDirection = 'column';
slidesContainer.style.gap = '20px';
slidesContainer.style.alignItems = 'center';
slidesContainer.style.background = '#f0f0f0';
slidesContainer.style.padding = '20px';
slidesContainer.style.width = '100%';
slidesContainer.style.height = '100%';
slidesContainer.style.overflow = 'auto';
slidesContainer.style.boxSizing = 'border-box';
viewer.appendChild(slidesContainer);
for (let i = 0; i < slideFiles.length; i++) {
const slideXmlText = await zip.file(slideFiles[i]).async("text");
const slideDoc = parser.parseFromString(slideXmlText, "text/xml");
const slideCard = document.createElement('div');
slideCard.className = 'pptx-slide-card';
slideCard.style.position = 'relative';
slideCard.style.width = '100%';
slideCard.style.maxWidth = '800px';
slideCard.style.backgroundColor = '#ffffff';
slideCard.style.boxShadow = '0 4px 10px rgba(0,0,0,0.1)';
slideCard.style.height = '0';
slideCard.style.paddingTop = ratio + '%';
slideCard.style.overflow = 'hidden';
slideCard.style.flexShrink = '0';
const slideContent = document.createElement('div');
slideContent.style.position = 'absolute';
slideContent.style.top = '0';
slideContent.style.left = '0';
slideContent.style.width = '100%';
slideContent.style.height = '100%';
slideCard.appendChild(slideContent);
slidesContainer.appendChild(slideCard);
// Parse relationships for this slide
const relMap = {};
try {
const slideName = slideFiles[i].split('/').pop();
const relsFileName = `ppt/slides/_rels/${slideName}.rels`;
const relsFile = zip.file(relsFileName);
if (relsFile) {
const relsXmlText = await relsFile.async("text");
const relsDoc = parser.parseFromString(relsXmlText, "text/xml");
const relationships = relsDoc.getElementsByTagName("Relationship");
for (let r = 0; r < relationships.length; r++) {
const id = relationships[r].getAttribute("Id");
const target = relationships[r].getAttribute("Target");
relMap[id] = target;
}
}
} catch (relErr) {
console.warn("Failed to parse relationships for slide:", slideFiles[i], relErr);
}
const elements = slideDoc.querySelectorAll('p\\:sp, sp, p\\:pic, pic, p\\:graphicFrame, graphicFrame');
for (const elem of elements) {
const xfrm = elem.querySelector('a\\:xfrm, xfrm');
if (!xfrm) continue;
const off = xfrm.querySelector('a\\:off, off');
const ext = xfrm.querySelector('a\\:ext, ext');
if (!off || !ext) continue;
const x = parseInt(off.getAttribute('x'), 10);
const y = parseInt(off.getAttribute('y'), 10);
const w = parseInt(ext.getAttribute('cx'), 10);
const h = parseInt(ext.getAttribute('cy'), 10);
const leftPct = (x / cx) * 100;
const topPct = (y / cy) * 100;
const widthPct = (w / cx) * 100;
const heightPct = (h / cy) * 100;
const itemDiv = document.createElement('div');
itemDiv.style.position = 'absolute';
itemDiv.style.left = leftPct + '%';
itemDiv.style.top = topPct + '%';
itemDiv.style.width = widthPct + '%';
itemDiv.style.height = heightPct + '%';
itemDiv.style.boxSizing = 'border-box';
const nodeName = elem.nodeName.toLowerCase();
if (nodeName.includes('pic')) {
let imgUrl = null;
try {
const blip = elem.querySelector('a\\:blip, blip');
const rId = blip ? (blip.getAttribute('r:embed') || blip.getAttribute('embed')) : null;
if (rId && relMap[rId]) {
const targetPath = relMap[rId].replace('../', 'ppt/');
const imgFile = zip.file(targetPath);
if (imgFile) {
const imgBlob = await imgFile.async("blob");
imgUrl = URL.createObjectURL(imgBlob);
}
}
} catch (imgErr) {
console.warn("Failed to extract slide image:", imgErr);
}
if (imgUrl) {
itemDiv.style.backgroundImage = `url("${imgUrl}")`;
itemDiv.style.backgroundRepeat = 'no-repeat';
itemDiv.style.backgroundPosition = 'center';
itemDiv.style.backgroundSize = 'contain';
} else {
itemDiv.style.border = '1px dashed #cccccc';
itemDiv.style.backgroundColor = '#f9f9f9';
itemDiv.style.display = 'flex';
itemDiv.style.alignItems = 'center';
itemDiv.style.justifyContent = 'center';
const label = document.createElement('span');
label.style.color = '#999999';
label.style.fontSize = '10px';
label.style.fontWeight = 'bold';
label.textContent = '[그림 영역]';
itemDiv.appendChild(label);
}
} else if (nodeName.includes('graphicframe')) {
const tbl = elem.querySelector('a\\:tbl, tbl');
if (tbl) {
const htmlTable = document.createElement('table');
htmlTable.style.width = '100%';
htmlTable.style.height = '100%';
htmlTable.style.borderCollapse = 'collapse';
htmlTable.style.fontSize = 'calc(0.4vw + 5px)';
htmlTable.style.fontFamily = 'sans-serif';
htmlTable.style.backgroundColor = '#ffffff';
htmlTable.style.boxShadow = '0 1px 3px rgba(0,0,0,0.05)';
const rows = tbl.querySelectorAll('a\\:tr, tr');
rows.forEach((row, rIdx) => {
const trEl = document.createElement('tr');
if (rIdx === 0) {
trEl.style.backgroundColor = '#f8f9fa';
trEl.style.fontWeight = '600';
} else if (rIdx % 2 === 0) {
trEl.style.backgroundColor = '#fafafa';
}
const cells = row.querySelectorAll('a\\:tc, tc');
cells.forEach(cell => {
const tdEl = document.createElement('td');
tdEl.style.border = '1px solid #e0e0e0';
tdEl.style.padding = '4px 6px';
tdEl.style.wordBreak = 'break-all';
tdEl.style.verticalAlign = 'middle';
const gridSpan = cell.getAttribute('gridSpan');
if (gridSpan) tdEl.setAttribute('colspan', gridSpan);
const rowSpan = cell.getAttribute('rowSpan');
if (rowSpan) tdEl.setAttribute('rowspan', rowSpan);
const txBody = cell.querySelector('a\\:txBody, txBody');
if (txBody) {
const paragraphs = txBody.querySelectorAll('a\\:p, p');
paragraphs.forEach(p => {
const runs = p.querySelectorAll('a\\:r, r');
let cellText = '';
runs.forEach(r => {
const t = r.querySelector('a\\:t, t');
if (t) cellText += t.textContent;
});
if (cellText.trim()) {
const pEl = document.createElement('p');
pEl.style.margin = '0';
pEl.style.lineHeight = '1.2';
pEl.textContent = cellText;
tdEl.appendChild(pEl);
}
});
}
trEl.appendChild(tdEl);
});
htmlTable.appendChild(trEl);
});
itemDiv.appendChild(htmlTable);
} else {
itemDiv.style.border = '1px dashed #dddddd';
itemDiv.style.backgroundColor = '#fdfdfd';
itemDiv.style.display = 'flex';
itemDiv.style.alignItems = 'center';
itemDiv.style.justifyContent = 'center';
const label = document.createElement('span');
label.style.color = '#aaaaaa';
label.style.fontSize = '10px';
label.style.fontWeight = 'bold';
label.textContent = '[차트 영역]';
itemDiv.appendChild(label);
}
} else {
const txBody = elem.querySelector('p\\:txBody, txBody');
if (txBody) {
itemDiv.style.overflow = 'hidden';
itemDiv.style.wordBreak = 'break-all';
itemDiv.style.fontSize = 'calc(0.5vw + 5px)';
itemDiv.style.fontFamily = 'sans-serif';
itemDiv.style.color = '#333333';
const paragraphs = txBody.querySelectorAll('a\\:p, p');
paragraphs.forEach(p => {
const runs = p.querySelectorAll('a\\:r, r');
let paraText = '';
runs.forEach(r => {
const t = r.querySelector('a\\:t, t');
if (t) paraText += t.textContent;
});
if (paraText.trim()) {
const pEl = document.createElement('p');
pEl.style.margin = '0 0 2px 0';
pEl.style.lineHeight = '1.2';
pEl.textContent = paraText;
itemDiv.appendChild(pEl);
}
});
} else {
itemDiv.style.border = '1px solid #eeeeee';
itemDiv.style.backgroundColor = 'rgba(0,0,0,0.01)';
}
}
slideContent.appendChild(itemDiv);
}
}
} catch (parseErr) {
console.error("PPTX parse error:", parseErr);
viewer.innerHTML = '<div style="display:flex;justify-content:center;align-items:center;height:100%;color:#d9534f;background:#fff;padding:20px;text-align:center;">PPTX 파싱 중 에러가 발생했습니다. 상단의 "PDF로 보기" 버튼을 이용해 주세요.</div>';
}
})
.catch(err => {
console.error(err);
viewer.innerHTML = '<div style="display:flex;justify-content:center;align-items:center;height:100%;color:#d9534f;background:#fff;">PPTX 문서를 불러오는데 실패했습니다.</div>';
});
}