엑셀 데이터를 불러오는 중...
';
+ initMainFallbackPdfButton(dataId, resourcePath, objectKey, previewKey);
fetch(presignedUrl)
.then(res => {
@@ -5616,18 +5617,33 @@ export async function renderViewer(resourcePath, dataId, shouldAddClickLog = tru
const container = document.createElement('div');
container.style.width = '100%';
container.style.height = '100%';
- container.style.overflow = 'auto';
+ container.style.overflowX = 'auto';
+ 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;
+ width: max-content;
+ min-width: 800px;
+ box-shadow: 0 4px 10px rgba(0,0,0,0.1);
+ padding: 0 !important;
+ box-sizing: border-box !important;
+ min-height: 100%;
+ }
+ .hwp-inner-container img {
+ max-width: 100% !important;
+ height: auto !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);
vars.viewer.appendChild(container);
diff --git a/views/main/jsm/officialDoc/docPageRenderer.js b/views/main/jsm/officialDoc/docPageRenderer.js
index 7b144b8..e5dd942 100644
--- a/views/main/jsm/officialDoc/docPageRenderer.js
+++ b/views/main/jsm/officialDoc/docPageRenderer.js
@@ -733,6 +733,7 @@ export async function renderDocViewer(resourcePath, docId) {
function viewerExcel(presignedUrl) {
docVars.viewer.innerHTML = '엑셀 데이터를 불러오는 중...
';
+ initDocFallbackPdfButton(docId, resourcePath, objectKey, previewKey);
fetch(presignedUrl)
.then(res => {
@@ -862,7 +863,7 @@ export async function renderDocViewer(resourcePath, docId) {
const container = document.createElement('div');
container.style.width = '100%';
container.style.height = '100%';
- container.style.overflowX = 'hidden';
+ container.style.overflowX = 'auto';
container.style.overflowY = 'auto';
container.style.padding = '20px';
container.style.boxSizing = 'border-box';
@@ -873,38 +874,17 @@ export async function renderDocViewer(resourcePath, docId) {
.hwp-inner-container {
background: #ffffff;
margin: 0 auto;
- max-width: 800px;
+ width: max-content;
+ min-width: 800px;
box-shadow: 0 4px 10px rgba(0,0,0,0.1);
- padding: 30px !important;
+ padding: 0 !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);
diff --git a/views/main/jsm/popup.js b/views/main/jsm/popup.js
index cf5a35d..f2cf2a3 100644
--- a/views/main/jsm/popup.js
+++ b/views/main/jsm/popup.js
@@ -674,6 +674,10 @@ function _openExcel(path, data) {
const viewer = document.getElementById('popup_viewer');
viewer.innerHTML = '엑셀 데이터를 불러오는 중...
';
+ if (dataId && path_name) {
+ initFallbackPdfButton(dataId, path_name, resourcePath);
+ }
+
fetch(path)
.then(res => {
if (!res.ok) throw new Error(`HTTP ${res.status} ${res.statusText}`);
@@ -815,7 +819,7 @@ function _openHwp(path, data) {
const container = document.createElement('div');
container.style.width = '100%';
container.style.height = '100%';
- container.style.overflowX = 'hidden';
+ container.style.overflowX = 'auto';
container.style.overflowY = 'auto';
container.style.padding = '20px';
container.style.boxSizing = 'border-box';
@@ -826,38 +830,17 @@ function _openHwp(path, data) {
.hwp-inner-container {
background: #ffffff;
margin: 0 auto;
- max-width: 800px;
+ width: max-content;
+ min-width: 800px;
box-shadow: 0 4px 10px rgba(0,0,0,0.1);
- padding: 30px !important;
+ padding: 0 !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);
diff --git a/views/main/main.html b/views/main/main.html
index 17dcbfc..1ff8109 100644
--- a/views/main/main.html
+++ b/views/main/main.html
@@ -3483,6 +3483,9 @@
` 태그 렌더링 | HTML5 표준 비디오 코덱 플레이어 사용 |
+| **텍스트** | `txt, log, md` | `` 태그 렌더링 | 인코딩 감지 후 플레인 텍스트 렌더링 (마크다운은 스타일링 적용) |
+| **3D 모델** | `glb, gltf, obj, stl, fbx, 3dm` | `Three.js` 3D 뷰어 연동 | WebGL 기반의 3D 객체 직접 렌더링 및 회전/스케일 제어 지원 |
+| **공간 정보** | `gsim` | GSIM 전용 뷰어 웹앱 연동 | 별도 마운트된 GSIM 엔진 프레임 내부 렌더링 |
+| **BIM 모델** | `ifc` | IFC 뷰어 전용 iframe 연동 | `web-ifc-three` 기반의 BIM 모델링 구조 브라우저 직접 렌더링 |
+| **기타** | `zip` | 폴더/파일 트리 렌더링 | 압축을 풀지 않고 내부 디렉토리 아키텍처를 JSON으로 트리 뷰잉 |
+| **링크** | `url` | iframe 또는 새 창 이동 | 등록된 외부 URL 리다이렉트 처리 |
+| **웹문서** | `html` | iframe 렌더링 | HTML 샌드박스 프레임 내부 렌더링 |
+
+---
+
+## 2. 미리보기 미지원 확장자 및 기술 사유 (Unsupported Formats)
+아래 확장자들은 **미리보기가 불가능하며 오직 파일 다운로드만 지원**됩니다. 그 구체적인 기술 사유는 다음과 같습니다.
+
+| 파일 분류 | 대상 확장자 (예시) | 미리보기 미지원 기술 사유 |
+| :--- | :--- | :--- |
+| **실행 파일** | `exe, msi, bat, sh, cmd, com` | * **보안 위험**: 웹 브라우저 내에서 OS 실행 파일을 구동하는 것은 크로스 사이트 스크립팅(XSS) 및 악성코드 실행 방지를 위해 보안상 원천 차단됩니다. * **플랫폼 독립성**: 브라우저 샌드박스 환경에서는 로컬 머신의 커널에 직접 액세스하여 실행 프로그램을 실행할 수 없습니다. |
+| **압축 파일** | `rar, 7z, tar, gz, alz, egg` | * **압축 알고리즘 독점성**: `.zip`을 제외한 `.rar`, `.7z` 등은 압축 및 해제 알고리즘이 브라우저 JavaScript 단에서 처리하기에 라이브러리가 매우 무겁거나, 유료 라이선스 제약(독점 포맷)이 있습니다. * **성능 저하**: 클라이언트 브라우저 메모리 상에서 기가바이트 단위의 대용량 압축 파일을 직접 해제하여 트리 구조를 빌드하는 것은 브라우저 탭 다운을 유발합니다. |
+| **데이터베이스** | `db, sqlite, mdb, sql, accdb` | * **연결 세션성**: DB 파일은 전용 DBMS(엔진)가 백그라운드에서 구동되어 인덱스 쿼리를 수행해야 구조 열람이 가능합니다. 브라우저에서 바이너리 파일 그 자체를 텍스트나 그림처럼 단순 뷰잉하는 것은 논리적으로 불가능합니다. |
+| **오디오 파일** | `mp3, wav, ogg, flac, m4a` | * **기획적 요구 부재**: 시스템이 문서 관리 및 3D 모델 협업 중심이므로 오디오 플레이어 탑재의 우선순위가 배제되어 있으며, 필요 시 브라우저 내장 플레이어로 다운로드 대체가 가능합니다. |
+| **소스 코드** | `java, py, cpp, cs, go, ts, js, css` | * **텍스트로 뷰잉 가능 여부**: `.txt`와 동일하게 텍스트 에디터 방식으로 보여줄 수 있으나, 현재 시스템은 개발자용 IDE 환경이 아닌 일반 비즈니스 문서용이므로 코딩 확장자들은 다운로드 전용으로 제한됩니다. |
+| **대용량 원시 그래픽** | `psd, ai, eps, indd, tiff` | * **독점 그래픽 포맷**: Adobe사 등의 전용 디자인 툴에서 쓰이는 포맷으로, 웹 표준 이미지 태그(` `)가 인식하지 못하며, 클라이언트 단의 JS 파싱 라이브러리가 존재하지 않거나 극도로 불안정합니다. |
+| **폰트 파일** | `ttf, otf, woff, eot` | * **시스템 자원**: 폰트 데이터는 글꼴 메타데이터의 모음이므로 독립적인 시각적 형태를 브라우저 캔버스 상에 프리뷰할 논리적 근거가 부재합니다. |
diff --git a/문서뷰어_구현_및_한계대응_분석보고서.pdf b/문서뷰어_구현_및_한계대응_분석보고서.pdf
new file mode 100644
index 0000000..b784c43
Binary files /dev/null and b/문서뷰어_구현_및_한계대응_분석보고서.pdf differ
diff --git a/엑셀파일미리보기도형한계.md b/엑셀파일미리보기도형한계.md
new file mode 100644
index 0000000..336314d
--- /dev/null
+++ b/엑셀파일미리보기도형한계.md
@@ -0,0 +1,67 @@
+# 엑셀 파일 미리보기 시 도형(Shapes) 렌더링 한계 분석 및 대응 방안
+
+본 문서는 웹 브라우저 환경에서 엑셀(`.xlsx`) 파일을 직접 파싱하여 미리보기 화면을 구성할 때, **도형(Shapes, 화살표, 선, 프로세스 차트 등)이 렌더링되지 않는 기술적 원인**과 **한글(HWP) 파일 파싱 방식과의 구조적 차이점**, 그리고 이를 보완하기 위한 **최선의 대응 방안**을 정리한 기술 분석서입니다.
+
+---
+
+## 1. 개요 및 현상
+* **정상 작동 영역**: 셀 데이터(텍스트, 숫자, 날짜), 셀 스타일(배경색, 폰트 크기, 테두리), 시트 수식, 셀 병합 상태, 그리고 일부 부유형 이미지(그림 파일)는 브라우저 미리보기 화면에서 정상적으로 표현됩니다.
+* **비정상 작동 영역**: 사각형, 원형, 화살표, 말풍선, 블록 화살표, 다이어그램, 연결선 등 **엑셀 본문 그리드 위에 독립적으로 배치된 드로잉 도형 객체(Shapes/Drawings)**들은 화면에 아예 표시되지 않거나 누락되는 현상이 발생합니다.
+
+---
+
+## 2. 기술적 원인 분석 (오픈소스 라이브러리의 한계)
+본 시스템은 웹 브라우저 단에서 서버 리소스를 쓰지 않고 빠르게 엑셀을 렌더링하기 위해 다음 두 가지 라이브러리를 결합하여 사용하고 있습니다.
+1. **LuckyExcel**: 엑셀(`.xlsx` binary/zip) 파일을 자바스크립트 객체(JSON) 데이터 모델로 변환해주는 파서.
+2. **Luckysheet**: 변환된 JSON 모델을 바탕으로 웹 브라우저 화면에 엑셀 시트 그리드를 그려주고 셀 동작을 처리해주는 렌더러.
+
+이 두 오픈소스 라이브러리는 태생적으로 **"셀(Cell) 중심의 그리드 표현"**에 특화되어 개발되었기 때문에 다음과 같은 한계를 가집니다.
+
+### ① LuckyExcel (파서) 측면의 한계 (XML 스펙 무시)
+* 엑셀 파일(`.xlsx`)은 OpenXML 포맷을 따르며, 시트 내에 배치된 도형 정보는 `xl/drawings/drawing1.xml`과 같은 파일 내에 **DrawingML** 규격으로 기록됩니다.
+* LuckyExcel은 이 XML 파일을 파싱할 때 이미지 객체를 정의하는 `` 태그만 선별적으로 추출하여 Luckysheet의 이미지 모델로 넘겨줍니다.
+* 반면, 일반 도형을 정의하는 **``(Shape)**나 연결선을 정의하는 **``(Connection Shape)** 등 기타 드로잉 엘리먼트들은 **코드 레벨에서 파싱을 전혀 지원하지 않고 무시**합니다.
+
+### ② Luckysheet (렌더러) 측면의 한계 (드로잉 모델 부재)
+* Luckysheet의 내부 데이터 스키마에는 셀 서식, 수식, 그리고 부유형 이미지를 관리하는 속성(`images`)은 설계되어 있으나, 다각형이나 선, 벡터 그래픽을 추상화하여 저장하고 실시간으로 그려주는 **도형 전용 데이터 규격 및 컴포넌트(Drawing Engine)가 존재하지 않습니다.**
+* 이미지는 웹 브라우저가 기본적으로 지원하는 ` ` 태그를 절대 좌표 기반으로 얹어주면 비교적 단순하게 렌더링할 수 있지만, 벡터 도형은 브라우저 상에서 가변적으로 그리기가 매우 까다로워 처음부터 설계 범위에서 배제되었습니다.
+* **추가 이슈**: 현재 Luckysheet 프로젝트는 공식적으로 개발이 중단(Unmaintained)되었으며, 후속 아키텍처 프로젝트인 Univer로 이전이 권장되고 있어 향후에도 도형 렌더링 기능이 보완될 가능성이 없습니다.
+
+---
+
+## 3. 한글(HWP) 파일 파싱 방식과의 차이점
+"한글 파일은 도형이 나오는데 엑셀은 왜 구현이 불가능한가?"에 대한 구조적 비교는 다음과 같습니다.
+
+| 비교 항목 | 한글(HWP) 미리보기 파싱 | 엑셀(Excel) 미리보기 파싱 |
+| :--- | :--- | :--- |
+| **사용 라이브러리** | `hwp.js` (자체 커스텀 버전) | `LuckyExcel` + `Luckysheet` (외부 CDN 주입) |
+| **도형 데이터 모델** | HWP 스펙에 정의된 Line, Rectangle, Ellipse 객체 모델을 내부에 명확히 파싱하여 보유함 | 데이터 모델(JSON) 상에 도형에 대한 정보(좌표, 타입, 스타일 등)를 담을 필드 자체가 없음 |
+| **렌더링 방식** | 추출된 도형 노드를 브라우저 화면상의 **HTML5 SVG(Scalable Vector Graphics)**로 직접 변환하여 문서 플로우 내에 동적으로 삽입 및 드로잉 처리함 | 시트 좌표계에 독립적인 드로잉 레이어나 SVG 변환 규칙이 존재하지 않으며, 오직 Canvas 표 위에 텍스트/셀 테두리만 출력함 |
+| **개선 가능 범위** | 라이브러리 소스 코드가 프로젝트 내(`libs/hwp.js`)에 패키징되어 있어 좌표 보정이나 텍스트 누락 방어 등의 수정이 유연하게 가능함 | 외부 라이브러리이며, 도형 표현을 위해선 파서와 렌더링 캔버스 엔진을 바닥부터 다시 코딩해야 하므로 프로젝트 규모의 프레임워크 재설계가 필요함 |
+
+---
+
+## 4. 직접 구현 시의 기술적 난제 (CAD/웹 오피스 수준의 공수)
+서버 도움 없이 클라이언트 브라우저 단에서 엑셀 도형을 직접 그리려면 다음과 같은 고난도의 기술적 해결책을 직접 만들어야 합니다.
+
+1. **DrawingML Parser 개발**:
+ `JSZip`으로 엑셀 파일 내의 `xl/drawings/drawingX.xml`을 압축 해제하고 DOMParser로 열어 각 도형의 크기, 회전각, 선 스타일, 그라데이션, 정렬, 텍스트 상자 등을 파싱하는 코드를 개발해야 합니다.
+2. **동적 픽셀 좌표(Dynamic Cell-to-Pixel) 매핑 엔진**:
+ * 엑셀에서 도형 위치는 절대 픽셀이 아니라, 특정 셀을 기준으로 한 위치인 **"Two Cell Anchor"** 구조로 관리됩니다.
+ * *예: "C열 4번째 행의 왼쪽으로부터 10px, 위로부터 5px 떨어진 곳에서 시작하여 E열 8번째 행까지..."*
+ * 따라서 엑셀 시트 내의 열 너비(Column Width)와 행 높이(Row Height)의 동적 변화를 실시간으로 가산하여 정확한 웹 픽셀(X, Y, Width, Height)로 치환해야 합니다.
+3. **스크롤, 확대/축소 및 열 너비 조절 동기화**:
+ * 사용자가 그리드를 마우스로 조작(스크롤, 컬럼 너비 리사이즈, 시트 확대/축소)할 때마다 SVG 도형 객체의 크기와 좌표도 실시간 재계산 및 Repositioning 처리를 해야 하며, 싱크가 1ms라도 어긋나면 도형이 표 위에 둥둥 떠다니는 왜곡 현상이 발생합니다.
+
+---
+
+## 5. 프로젝트 내 조치 내용 및 우회 해결 방법 (Best Practice)
+웹 브라우저의 프론트엔드 파싱만으로는 엑셀 본래의 완벽한 폰트, 다이어그램, 차트 및 수많은 도형 레이아웃을 100% 보존하여 출력할 수 없습니다.
+
+이 때문에 업계 표준(Google Docs, Slack, Confluence 등)에 맞춰 **서버 측 고성능 오피스 변환 엔진**을 이용한 **PDF 뷰어 폴백(Fallback) 방식을 조합하여 최선의 UX를 제공**하도록 처리했습니다.
+
+### 적용된 우회 해결책
+* **"PDF로 보기" 버튼 상시 노출 처리 (2026-06-19 패치)**:
+ * 기존에는 엑셀 파싱 에러(포맷 불일치 등)가 났을 경우에만 "PDF로 보기" 버튼이 활성화되어, 정상적으로 시트가 열린 상태에서 도형이 깨져 보이는 문서는 원본을 열람할 수 없었습니다.
+ * 엑셀 뷰어가 실행되는 즉시 화면 상단 우측에 **"PDF로 보기"** 버튼이 항상 노출되도록 3개 뷰어 모듈(`pageRenderer.js`, `docPageRenderer.js`, `popup.js`)을 개선하였습니다.
+ * 사용자는 도형 및 완벽한 레이아웃 보존이 필요한 경우 **"PDF로 보기"** 버튼을 눌러 서버가 변환한 정밀한 PDF 뷰어 화면으로 즉시 스위칭하여 원하는 내용을 고화질로 확인할 수 있습니다.
diff --git a/한글파일미리보기구현.md b/한글파일미리보기구현.md
index 1043daa..277af21 100644
--- a/한글파일미리보기구현.md
+++ b/한글파일미리보기구현.md
@@ -1,4 +1,4 @@
-# 한글 파일(HWP) 클라이언트 사이드 미리보기 구현 및 기술 명세서
+# 한글 파일(HWP) 클라이언트 사이드 미리보기 구현 및 기술 명세서 (고도화 버전)
본 문서는 서버 자원 소모 없이 웹 브라우저(클라이언트) 단에서 `.hwp` 파일을 직접 파싱하고 안정적으로 이미지를 비롯한 도형 요소까지 화면에 렌더링하도록 커스텀 반영한 작업 내용, 구현 로직, 아키텍처 및 코드 명세를 다룹니다.
@@ -6,7 +6,7 @@
## 1. 미리보기 아키텍처 및 데이터 흐름
-클라이언트 브라우저가 HWP 바이너리를 가져와 렌더링하기까지의 흐름도입니다.
+클라이언트 브라우저가 HWP 바이너리를 가져와 렌더링하기까지의 전체적인 흐름입니다.
```mermaid
graph TD
@@ -14,49 +14,90 @@ graph TD
B --> C["hwp.js Viewer 초기화"]
C --> D["OLE Compound 파일 구조 해석"]
D --> E["BinData 가상 폴더 내 이미지 파싱"]
- E --> F{"Magic Number 분석 (Raw 이미지 여부)"}
- F -->|"PNG/JPEG/GIF/WMF 헤더 일치"| G["Decompress 건너뛰기 (Raw 복사)"]
- F -->|"헤더 불일치 (압축 상태)"| H["pako.inflate (윈도우 비트 -15) 실행"]
+ E --> F{"Magic Number 분석\nRaw 이미지 여부 판별"}
+ F -->|"PNG/JPEG/GIF/WMF 헤더 일치"| G["Decompress 건너뛰기\nRaw 복사"]
+ F -->|"헤더 불일치 - 압축 상태"| H["pako.inflate 실행\nwindowBits: -15"]
H -->|"실패 시"| I["표준 inflate / raw / 원본 복사 폴백"]
G --> J["바이너리 데이터 Uint8Array 래핑"]
I --> J
- J --> K["Blob 객체 및 URL.createObjectURL 주소 생성"]
- K --> L{"컨트롤 타입 (control.type) 판별"}
- L -->|"그림 (Picture)"| M["shapeGroup div에 backgroundImage 매핑"]
- L -->|"도형 (Rectangle/Ellipse/Line/Polygon/Arc/Curve)"| N["도형 테두리 선, 둥글기, 연한 배경색 스타일 및 텍스트 렌더링"]
- M --> O["최종 웹 뷰어 화면 렌더링"]
+ J --> K["Blob 객체 및 URL.createObjectURL 생성"]
+ K --> L{"control.type 판별"}
+ L -->|"그림 Picture"| M{"확장자 판별"}
+ M -->|"일반 이미지 PNG/JPG"| M1["shapeGroup div에\nbackgroundImage 매핑"]
+ M -->|"벡터 이미지 WMF"| M2["wmf.js Canvas 렌더링 후\nPNG 변환 바인딩"]
+ L -->|"도형 Rectangle/Ellipse/Line 등"| N["shapeGroup 내 SVG 배경 레이어 삽입\nrect, ellipse, line 요소 매핑"]
+ M1 --> O["최종 웹 뷰어 화면 렌더링"]
+ M2 --> O
N --> O
```
+
---
## 2. 해결한 핵심 이슈 및 작업 내용
### 2.1 OLE 이미지 압축 해제(Decompression) 안정화 및 우회 로직
-* **현상:** 구형 HWP 파서 라이브러리(`hwp.js`)는 `pako.inflate`를 수행할 때 윈도우 비트 옵션(`{ windowBits: -15 }`)을 무조건적으로 주입해 압축을 해제했습니다. 그러나 한글 문서가 압축 없이 저장되었거나 일부 이미지가 무압축 Raw 바이너리(PNG, JPEG 등)로 OLE 스트림에 기록된 경우, 해제 오류로 인해 뷰어가 크래시되거나 이미지가 유실(0바이트)되는 문제가 있었습니다.
-* **조치:**
+* **현상**: 기존 HWP 파서 라이브러리(`hwp.js`)는 `pako.inflate`를 수행할 때 윈도우 비트 옵션(`{ windowBits: -15 }`)을 무조건적으로 주입해 압축을 해제했습니다. 그러나 한글 문서가 압축 없이 저장되었거나 일부 이미지가 무압축 Raw 바이너리(PNG, JPEG 등)로 OLE 스트림에 기록된 경우, 해제 오류로 인해 뷰어가 크래시되거나 이미지가 유실(0바이트)되는 문제가 있었습니다.
+* **조치**:
1. 압축 해제 전 바이너리의 **첫 4바이트(Magic Number) 시그니처**를 대조하여 웹 표준 파일 유형(PNG, JPEG, GIF, WMF) 검사를 선행합니다.
- 2. 이미 시그니처를 충족하는 Raw 이미지의 경우 압축 해제를 우회하도록 최적화하여 렌더링 성능과 안정성을 향상했습니다.
+ 2. 이미 시그니처를 충족하는 Raw 이미지의 경우 압축 해제를 우회하도록 최적화하여 렌더링 성능 및 안정성을 향상했습니다.
3. 압축이 필요한 경우 3단계 예외 처리 폴백(`windowBits: -15` -> `표준 inflate` -> `inflateRaw` -> `원본 바이너리`)을 구축하여 어떠한 조건에서도 파싱이 멈추지 않도록 조치했습니다.
### 2.2 MIME 타입 오류 수정 및 바이너리 안전성 보장
-* **현상:** 기존 뷰어 소스 코드에 `type: "images/".concat(extension)`이라는 치명적인 오타가 있었습니다. 브라우저는 `images/png`와 같은 비표준 MIME 타입을 이해하지 못해 이미지 Blob URL을 백그라운드로 로드하려 할 때 엑스박스나 투명 빈 공간으로 처리(이미지 사라짐 현상)했습니다. 또한, 브라우저 환경에 따라 OLE 파서가 원본을 일반 Array 형태로 리턴할 때 문자열로 깨지는 위험이 존재했습니다.
-* **조치:**
+* **현상**: 기존 뷰어 소스 코드에 `type: "images/".concat(extension)`이라는 치명적인 오타가 있었습니다. 브라우저는 `images/png`와 같은 비표준 MIME 타입을 이해하지 못해 이미지 Blob URL을 백그라운드로 로드하려 할 때 엑스박스나 투명 빈 공간으로 처리(이미지 사라짐 현상)했습니다. 또한, 브라우저 환경에 따라 OLE 파서가 원본을 일반 Array 형태로 리턴할 때 문자열로 깨지는 위험이 존재했습니다.
+* **조치**:
1. MIME 타입을 표준 규격에 맞게 `"image/".concat(extension)`으로 전면 변경하고, `jpg` 확장자는 브라우저 표준 명칭인 `image/jpeg`로 정확히 매핑했습니다.
2. Blob 생성 시 바이트의 깨짐을 방지하기 위해 생성자 주입 전 데이터 타입을 검증하고 `Uint8Array` 인스턴스로 안전하게 래핑하도록 보장했습니다.
-### 2.3 한글 문서 자체 생성 도형(Shape Object) 렌더링 기능 추가
-* **현상:** 본문 내에 삽입된 이미지는 로드가 완료되었으나, 문서 편집기 내부에서 자체 제작한 **직사각형(Rectangle), 타원(Ellipse), 선(Line), 다각형(Polygon) 등의 벡터 도형**은 파서가 태그 분류를 누락하여 완전히 투명하게 렌더링되었습니다. 이로 인해 테두리와 사각형 배경 없이 글자만 겹쳐서 표시되는 문제가 있었습니다.
-* **조치:**
+### 2.3 한글 문서 자체 생성 도형(Shape Object) 렌더링의 SVG 배경 레이어 적용 및 위치 정합성 교정
+* **현상**: 본문 내에 삽입된 이미지는 로드가 완료되었으나, 문서 편집기 내부에서 자체 제작한 **직사각형(Rectangle), 타원(Ellipse), 선(Line), 다각형(Polygon) 등의 벡터 도형**은 파서가 태그 분류를 누락하여 완전히 투명하게 렌더링되거나, 상대 배치 마진 누적으로 인해 본문 뒤로 엉뚱하게 밀려나서 페이지 바깥의 회색 프레임에 잘못 렌더링되는 문제가 있었습니다.
+* **조치**:
1. HWP 파서의 핵심 순회 구조(`visit` 스위치-케이스 문) 내에 누락된 도형 컴포넌트 태그 ID들을 등록하여 파싱 단계에서 도형 종류(`control.type`)를 올바르게 정의하도록 했습니다.
- 2. 렌더링 엔진(`drawShape`) 단에서 도형 종류별 CSS 대응(직사각형 외곽선 그리기, 원형 둥글기 `borderRadius: 50%` 처리, 선 굵기 및 정렬, 투명 배경색 지정 등)을 적용하여 시각적으로 구현하고 내부 텍스트와 레이어링이 맞물리도록 개선했습니다.
+ 2. 렌더링 엔진(`drawShape`) 단에서 단순 HTML/CSS 스타일로 형태만 구현하던 단독 태그 방식 대신, 글씨 배치가 되는 부모 컨테이너 `div` 내부에 **절대 배치된 `` 배경 레이어를 생성하여 ``, ``, `` 요소를 주입**해 벡터 윤곽선을 정밀하게 구현했습니다.
+ 3. 도형의 세로 기준 설정(`vertRelTo`) 속성을 해석하여 분기 처리했습니다:
+ - `vertRelTo === 2` (문단 기준): 문단 컨테이너 `div`에 `position: relative` 스타일을 부여하고 도형을 `position: absolute; top: [verticalOffset]; left: [horizontalOffset];`로 정밀 정렬하였습니다.
+ - `vertRelTo === 0 또는 1` (종이/쪽 기준): 도형을 부모 쪽 컨테이너(`container.parentNode`)의 절대 레이어에 직접 덧그려 배치했습니다.
+ - `drawParagraph` 시작 지점에서 문단 `div`를 부모(쪽)에 선제적으로 덧붙여 도형들의 절대 배치 위치 기준(`parentNode`)이 유효하도록 설계했습니다.
### 2.4 단락 및 텍스트 줄 간격(Line Spacing) 조절을 통한 겹침 결함 조치
-* **현상:** 뷰어 화면의 텍스트 줄 간격이 브라우저 기본값(normal)을 사용하여 좁게 나타날 뿐만 아니라, 프로젝트 전반의 글로벌 CSS 규격에 의해 자식 요소인 `div` 태그의 `line-height` 속성이 덮어씌워져 줄 영역이 위아래로 심하게 겹쳐 보이는 시각적 오류가 발생했습니다.
-* **조치:**
+* **현상**: 뷰어 화면의 텍스트 줄 간격이 브라우저 기본값(normal)을 사용하여 좁게 나타날 뿐만 아니라, 프로젝트 전반의 글로벌 CSS 규격에 의해 자식 요소인 `div` 태그의 `line-height` 속성이 덮어씌워져 줄 영역이 위아래로 심하게 겹쳐 보이는 시각적 오류가 발생했습니다.
+* **조치**:
1. `drawParagraph`를 통해 생성되는 단락 컨테이너뿐만 아니라, 실제 텍스트가 바인딩되는 개별 문자열 `div` 스팬 요소(`drawText` 내의 `span` 객체)에도 **`line-height: 1.65` 인라인 스타일을 강제 적용**했습니다.
2. 인라인 스타일을 직접 지정함으로써 글로벌 스타일시트(CSS) 셀렉터에 의한 오버라이드를 완전 차단하고, 텍스트 줄 간 영역이 서로 침범 및 겹침 현상 없이 한글 표준 규격(160%대)으로 선명하게 공간 배치되도록 수정했습니다.
+### 2.5 한글 기본 폰트의 웹 폰트(@font-face) 매핑
+* **현상**: 모바일이나 macOS 등 한글 기본 폰트(바탕체, 돋움체, 굴림체, 궁서체)가 설치되어 있지 않은 클라이언트에서 볼 때 줄바꿈과 표 넓이가 어긋나는 문제가 발생했습니다.
+* **조치**:
+ 1. `views/main/css/font.css`에 구글 Noto Serif KR 및 Noto Sans KR 웹 폰트를 임포트했습니다.
+ 2. 각 한글 기본 폰트 이름에 대응하여 로컬 폰트를 우선 선언하고 없으면 Google Web Fonts에서 가져오도록 `@font-face`를 매핑했습니다.
+
+### 2.6 WMF 벡터 이미지의 클라이언트 캔버스 디코딩 연동
+* **현상**: 구형 한글 문서에 많이 쓰이는 WMF 포맷은 브라우저 표준 이미지 디코더로 해독할 수 없어 엑스박스로 출력되었습니다.
+* **조치**:
+ 1. SheetJS의 `wmf.js` 라이브러리를 로컬 `libs/wmf.js`에 다운로드하고 `main.html`에 로드하였습니다.
+ 2. `drawShape` 실행 시 이미지 확장자가 `wmf`인 경우 `WMF.draw_canvas` API로 메모리 내에 Canvas를 생성해 렌더링한 후, PNG Data URL(`canvas.toDataURL('image/png')`)로 추출해 `backgroundImage`에 바인딩하도록 수정했습니다.
+
+### 2.7 표 셀(td) padding 지정 및 세로 정렬(vertical-align) 완벽 일치
+* **현상**: 표 내부의 텍스트가 셀 경계선에 너무 붙어 가독성이 저하되거나, 원본 문서에 지정된 세로 정렬 속성(위, 중간, 아래)이 반영되지 않는 문제가 있었습니다.
+* **조치**:
+ 1. `libs/hwp.js` 내 `visitCellListHeader` 파서에서 셀 버퍼 데이터로부터 **세로 정렬 바이트(vertical-align: 0-상단, 1-중앙, 2-하단)**를 추가로 해독하여 전달하도록 수정했습니다.
+ 2. `drawColumn`에서 각 셀의 HWP `padding` 속성을 CSS로 적용하며, 누락되거나 0인 경우에는 웹 표준 기본 패딩(`2px 5px`)이 설정되도록 삼항 조건식을 추가했습니다.
+ 3. 해독된 정렬 바이트를 CSS `vertical-align` 속성과 매핑시켜 인라인 스타일에 강제 반영했습니다.
+
+### 2.8 뷰어 레이아웃 너비 확장 (횡스크롤바 제거)
+* **현상**: 우측 미리보기 영역 너비가 좁아 A4 Portrait 규격(가로 약 794px + 여백) 문서를 미리볼 때 불필요한 하단 가로 스크롤바가 매번 발생했습니다.
+* **조치**:
+ 1. `views/main/css/style_archive.css`의 우측 미리보기 컨테이너(`.archive-main-right`) 가로 너비를 기존 `41rem`에서 **`55rem` (880px)**으로 확장하여 스크롤바 없는 쾌적한 뷰를 구현했습니다.
+ 2. 이에 맞춰 우측 영역과 인접한 토글 스위치 및 버튼 메뉴들의 우측 절대 좌표 위치(`right: 41.5rem` -> **`right: 55.5rem`**)를 상향 조정했습니다.
+
+### 2.9 HWP 팝업 미리보기 기능 정상화 및 레이아웃 개선
+* **현상**: 한글 미리보기 전용 팝업창(`/popup/` 경로)에서 WMF 이미지/도형 리소스가 누락되거나, default CJK 폰트 매핑이 존재하지 않아 줄바꿈 및 벡터 도형이 비정상적으로 크고 잘못된 곳에 그려지며 횡스크롤바가 다발하는 문제가 지속되었습니다. 또한 페이지 너비를 강제로 줄일 시 A4 고유 가로폭과 절대 좌표 계산이 무너져 표나 이미지가 찌그러지고 겹치는 왜곡이 발생했습니다.
+* **조치**:
+ 1. `views/main/popup.html` 파일 내부에서 `hwp.js`가 구동되기 전 웹 폰트 매핑 스타일시트(`/main/css/font.css`)와 WMF 벡터 캔버스 렌더러 스크립트(`/libs/wmf.js`)를 반드시 추가 로드하도록 수정했습니다.
+ 2. `popup.js`, `pageRenderer.js`, `docPageRenderer.js` 각 한글 미리보기 영역의 동적 CSS 주입(`styleEl.textContent`) 블록 내에 `.hwp-inner-container`의 최대 너비를 **`950px`**로 확장하였습니다.
+ 3. 페이지 요소(`div[data-page-number]`)에 일시적으로 인젝션했던 `max-width: 100%` 및 `box-sizing: border-box` 설정을 완전 소거하여, 한글 고유의 A4 정밀 레이아웃 비율이 파괴되거나 표가 깨지는 문제를 방지하고 원래의 정상적인 형태로 복구시켰습니다.
+ 4. `.hwpjs-viewer > div` 영역에 `overflow-x: hidden !important` 스타일만을 입혀 불필요한 횡스크롤바의 노출을 제어하고, `libs/hwp.js` 내에서 `vertRelTo === 0` (종이) 및 `1` (쪽) 절대 배치 도형 렌더링 시 부모 페이지의 padding(마진) 값만큼을 좌표에서 빼주어 도형들이 밀려나지 않고 표 바로 밑의 지정된 본문 영역 안에 안착하도록 보정했습니다.
+
---
## 3. 구현 코드 및 로직 명세 (libs/hwp.js)
@@ -91,15 +132,13 @@ value: function visitBinData(record) {
// GIF: 47 49 46 38
else if (payload[0] === 0x47 && payload[1] === 0x49 && payload[2] === 0x46 && payload[3] === 0x38) isRawImage = true;
// WMF: D7 CD C6 9A
- else if (payload[0] === 0xD7 && payload[1] === 0xCD && payload[2] === 0xC6 && payload[3] === 0x9A) isRawImage = true;
+ else if (payload[0] === 0x7D && payload[1] === 0xCD && payload[2] === 0xC6 && payload[3] === 0x9A) isRawImage = true;
}
var decompressed;
if (isRawImage) {
- // 압축 해제 없이 raw 바이너리 복사
decompressed = payload;
} else {
- // 2단계 다중 압축 해제 시도 (zlib 윈도우 비트 -15 -> 표준 -> raw 순서)
try {
decompressed = pako_1.inflate(payload, { windowBits: -15 });
} catch (e1) {
@@ -109,7 +148,7 @@ value: function visitBinData(record) {
try {
decompressed = pako_1.inflateRaw(payload);
} catch (e3) {
- decompressed = payload; // 최종 폴백
+ decompressed = payload;
}
}
}
@@ -121,82 +160,106 @@ value: function visitBinData(record) {
}
```
-### 3.2 도형 태그 식별을 위한 파서 해석 추가 (`visit`)
+### 3.2 셀 속성 세로 정렬 감지 추가 (`visitCellListHeader`)
-`switch-case` 블록 내부에 한글 파일 자체 도형 컴포넌트 레코드 식별자를 등록하여 각 도형 오브젝트의 타입 분류를 해석합니다.
+표 내부 셀(Cell)의 크기 및 패딩에 더하여 세로 정렬(상단/중앙/하단) 옵션의 바이트 코드를 감지하는 구조입니다.
```javascript
-switch (record.tagID) {
- // ... 기존 공통 케이스 ...
- case SectionTagID.HWPTAG_SHAPE_COMPONENT_PICTURE:
- {
- this.visitPicture(record, control);
- break;
- }
- // 추가 반영된 개별 도형 파싱 케이스
- case SectionTagID.HWPTAG_SHAPE_COMPONENT_RECTANGLE:
- {
- if (isShape(control)) {
- control.type = CommonCtrlID.Rectangle;
- }
- break;
- }
- case SectionTagID.HWPTAG_SHAPE_COMPONENT_ELLIPSE:
- {
- if (isShape(control)) {
- control.type = CommonCtrlID.Ellipse;
- }
- break;
- }
- case SectionTagID.HWPTAG_SHAPE_COMPONENT_LINE:
- {
- if (isShape(control)) {
- control.type = CommonCtrlID.Line;
- }
- break;
- }
- case SectionTagID.HWPTAG_SHAPE_COMPONENT_ARC:
- {
- if (isShape(control)) {
- control.type = CommonCtrlID.Arc;
- }
- break;
- }
- case SectionTagID.HWPTAG_SHAPE_COMPONENT_POLYGON:
- {
- if (isShape(control)) {
- control.type = CommonCtrlID.Polygon;
- }
- break;
- }
- case SectionTagID.HWPTAG_SHAPE_COMPONENT_CURVE:
- {
- if (isShape(control)) {
- control.type = CommonCtrlID.Curve;
- }
- break;
- }
+key: "visitCellListHeader",
+value: function visitCellListHeader(reader) {
+ var option = {
+ column: reader.readUInt16(),
+ row: reader.readUInt16(),
+ colSpan: reader.readUInt16(),
+ rowSpan: reader.readUInt16(),
+ width: reader.readUInt32(),
+ height: reader.readUInt32(),
+ padding: [reader.readUInt16(), reader.readUInt16(), reader.readUInt16(), reader.readUInt16()]
+ };
+
+ if (!reader.isEOF()) {
+ option.borderFillID = reader.readUInt16() - 1;
+ }
+
+ if (!reader.isEOF()) {
+ reader.readUInt8(); // text flow 건너뛰기
+ }
+
+ if (!reader.isEOF()) {
+ option.verticalAlign = reader.readUInt8(); // 0: Top, 1: Middle, 2: Bottom
+ }
+
+ return option;
}
```
-### 3.3 이미지 및 도형 요소 스타일 렌더링 (`drawShape`)
+### 3.3 표 셀 스타일 및 세로 정렬 일치 (`drawColumn`)
-타입 정보가 명시된 컨트롤을 바탕으로 그림 및 각 도형 컴포넌트를 브라우저에 알맞게 그리는 로직입니다.
+각 td 셀 요소에 여백 수치를 변환하여 할당하고 세로 정렬 스타일을 적용하는 코드입니다.
+
+```javascript
+key: "drawColumn",
+value: function drawColumn(container, paragraphList) {
+ var _this = this;
+
+ var column = document.createElement('td');
+ var attrib = paragraphList.attribute;
+
+ column.style.width = "".concat(attrib.width / 100, "pt");
+ column.style.height = "".concat(attrib.height / 100, "pt");
+ column.colSpan = attrib.colSpan;
+ column.rowSpan = attrib.rowSpan;
+
+ // 여백(padding) 스타일 매핑 & 강제 주입
+ var pad = attrib.padding || [0, 0, 0, 0];
+ var ptLeft = pad[0] > 0 ? (pad[0] / 100) + "pt" : "5px";
+ var ptRight = pad[1] > 0 ? (pad[1] / 100) + "pt" : "5px";
+ var ptTop = pad[2] > 0 ? (pad[2] / 100) + "pt" : "2px";
+ var ptBottom = pad[3] > 0 ? (pad[3] / 100) + "pt" : "2px";
+ column.style.padding = ptTop + " " + ptRight + " " + ptBottom + " " + ptLeft;
+
+ // 세로 정렬(vertical-align) 매핑 (0: top, 1: middle, 2: bottom)
+ var valigns = ['top', 'middle', 'bottom'];
+ column.style.verticalAlign = valigns[attrib.verticalAlign] || 'middle';
+
+ this.drawBorderFill(column, attrib.borderFillID);
+ paragraphList.items.forEach(function (paragraph) {
+ _this.drawParagraph(column, paragraph);
+ });
+ container.appendChild(column);
+}
+```
+
+### 3.4 이미지 WMF 캔버스 디코딩 및 도형 SVG 렌더링 & 절대 배치 (`drawShape`)
+
+타입 정보가 명시된 컨트롤을 바탕으로 그림(WMF 대응 포함) 및 각 도형 컴포넌트를 브라우저에 배경 레이어로 알맞게 그리는 로직입니다.
```javascript
key: "drawShape",
value: function drawShape(container, control) {
var _this3 = this;
+
var shapeGroup = document.createElement('div');
shapeGroup.style.width = "".concat(control.width / 100, "pt");
shapeGroup.style.height = "".concat(control.height / 100, "pt");
- // 위치 스타일 세팅 (절대 좌표 및 여백)
- if (control.attribute.vertRelTo === 0) {
+ var targetContainer = container;
+ if (control.attribute.vertRelTo === 2) {
+ // 문단(Paragraph) 기준 절대 배치
+ container.style.position = 'relative';
shapeGroup.style.position = 'absolute';
shapeGroup.style.top = "".concat(control.verticalOffset / 100, "pt");
shapeGroup.style.left = "".concat(control.horizontalOffset / 100, "pt");
+ } else if (control.attribute.vertRelTo === 0 || control.attribute.vertRelTo === 1) {
+ // 종이(Paper) / 쪽(Page) 기준 절대 배치
+ shapeGroup.style.position = 'absolute';
+ shapeGroup.style.top = "".concat(control.verticalOffset / 100, "pt");
+ shapeGroup.style.left = "".concat(control.horizontalOffset / 100, "pt");
+ if (container.parentNode) {
+ targetContainer = container.parentNode;
+ }
} else {
+ // 문자 단위 등 기타 여백 배치
shapeGroup.style.marginTop = "".concat(control.verticalOffset / 100, "pt");
shapeGroup.style.marginLeft = "".concat(control.horizontalOffset / 100, "pt");
}
@@ -206,7 +269,6 @@ value: function drawShape(container, control) {
shapeGroup.style.display = 'inline-block';
if (isPicture(control)) {
- // [1] 이미지(그림) 렌더링 분기
var image = this.hwpDocument.info.binData[control.info.binID];
if (!image || !image.payload || image.payload.length === 0) {
shapeGroup.style.border = '1px dashed #aaaaaa';
@@ -224,134 +286,110 @@ value: function drawShape(container, control) {
shapeGroup.appendChild(placeholder);
} else {
var uint8Arr = image.payload instanceof Uint8Array ? image.payload : new Uint8Array(image.payload);
- var blob = new Blob([uint8Arr], {
- type: image.extension === 'jpg' ? 'image/jpeg' : "image/".concat(image.extension)
- });
- var imageURL = window.URL.createObjectURL(blob);
-
- shapeGroup.style.backgroundImage = "url(\"".concat(imageURL, "\")");
- shapeGroup.style.backgroundRepeat = 'no-repeat';
- shapeGroup.style.backgroundPosition = 'center';
- shapeGroup.style.backgroundSize = 'contain';
+
+ // WMF 디코딩 처리
+ if (image.extension.toLowerCase() === 'wmf' && typeof WMF !== 'undefined') {
+ try {
+ var size = WMF.image_size(uint8Arr);
+ var canvas = document.createElement('canvas');
+ canvas.width = size[0] || (control.width / 10);
+ canvas.height = size[1] || (control.height / 10);
+ WMF.draw_canvas(uint8Arr, canvas);
+ var imageURL = canvas.toDataURL('image/png');
+
+ shapeGroup.style.backgroundImage = "url(\"".concat(imageURL, "\")");
+ shapeGroup.style.backgroundRepeat = 'no-repeat';
+ shapeGroup.style.backgroundPosition = 'center';
+ shapeGroup.style.backgroundSize = 'contain';
+ } catch (wmfErr) {
+ console.error("[HWP.js Debug] WMF rendering error:", wmfErr);
+ shapeGroup.style.border = '1px solid red';
+ }
+ } else {
+ var blob = new Blob([uint8Arr], {
+ type: image.extension === 'jpg' ? 'image/jpeg' : "image/".concat(image.extension)
+ });
+
+ var imageURL = window.URL.createObjectURL(blob);
+ shapeGroup.style.backgroundImage = "url(\"".concat(imageURL, "\")");
+ shapeGroup.style.backgroundRepeat = 'no-repeat';
+ shapeGroup.style.backgroundPosition = 'center';
+ shapeGroup.style.backgroundSize = 'contain';
+ }
}
} else {
- // [2] 자체 제작 도형 렌더링 분기
shapeGroup.style.boxSizing = 'border-box';
+ shapeGroup.style.position = 'relative';
+
+ var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
+ svg.setAttribute('width', '100%');
+ svg.setAttribute('height', '100%');
+ svg.style.position = 'absolute';
+ svg.style.top = '0';
+ svg.style.left = '0';
+ svg.style.pointerEvents = 'none';
+ svg.style.zIndex = '-1';
+
if (control.type === CommonCtrlID.Rectangle) {
- // 직사각형 외곽 테두리 및 옅은 배경
- shapeGroup.style.border = '1.5px solid #333333';
- shapeGroup.style.backgroundColor = 'rgba(0, 0, 0, 0.02)';
+ var rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
+ rect.setAttribute('x', '1.5');
+ rect.setAttribute('y', '1.5');
+ rect.setAttribute('width', 'calc(100% - 3px)');
+ rect.setAttribute('height', 'calc(100% - 3px)');
+ rect.setAttribute('fill', 'rgba(0, 0, 0, 0.02)');
+ rect.setAttribute('stroke', '#333333');
+ rect.setAttribute('stroke-width', '1.5');
+ svg.appendChild(rect);
} else if (control.type === CommonCtrlID.Ellipse) {
- // 타원형 및 둥근 모서리 처리
- shapeGroup.style.border = '1.5px solid #333333';
- shapeGroup.style.borderRadius = '50%';
- shapeGroup.style.backgroundColor = 'rgba(0, 0, 0, 0.02)';
+ var ellipse = document.createElementNS('http://www.w3.org/2000/svg', 'ellipse');
+ ellipse.setAttribute('cx', '50%');
+ ellipse.setAttribute('cy', '50%');
+ ellipse.setAttribute('rx', 'calc(50% - 1.5px)');
+ ellipse.setAttribute('ry', 'calc(50% - 1.5px)');
+ ellipse.setAttribute('fill', 'rgba(0, 0, 0, 0.02)');
+ ellipse.setAttribute('stroke', '#333333');
+ ellipse.setAttribute('stroke-width', '1.5');
+ svg.appendChild(ellipse);
} else if (control.type === CommonCtrlID.Line) {
- // 선 객체의 세로/가로 두께 정렬
- var w = control.width / 100;
- var h = control.height / 100;
- if (h < 5) {
- shapeGroup.style.borderTop = '1.5px solid #333333';
- } else if (w < 5) {
- shapeGroup.style.borderLeft = '1.5px solid #333333';
- } else {
- shapeGroup.style.border = '1px solid #333333';
- }
+ var line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
+ line.setAttribute('x1', '0');
+ line.setAttribute('y1', '0');
+ line.setAttribute('x2', '100%');
+ line.setAttribute('y2', '100%');
+ line.setAttribute('stroke', '#333333');
+ line.setAttribute('stroke-width', '1.5');
+ svg.appendChild(line);
} else if (control.type === CommonCtrlID.Arc || control.type === CommonCtrlID.Polygon || control.type === CommonCtrlID.Curve) {
- // 다각형, 호, 자유곡선의 흐린 테두리 가이드
- shapeGroup.style.border = '1px dashed #555555';
- shapeGroup.style.backgroundColor = 'rgba(0, 0, 0, 0.01)';
+ var placeholder = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
+ placeholder.setAttribute('x', '1');
+ placeholder.setAttribute('y', '1');
+ placeholder.setAttribute('width', 'calc(100% - 2px)');
+ placeholder.setAttribute('height', 'calc(100% - 2px)');
+ placeholder.setAttribute('fill', 'rgba(0, 0, 0, 0.01)');
+ placeholder.setAttribute('stroke', '#555555');
+ placeholder.setAttribute('stroke-width', '1');
+ placeholder.setAttribute('stroke-dasharray', '4');
+ svg.appendChild(placeholder);
} else {
- shapeGroup.style.border = '1px solid #cccccc';
- shapeGroup.style.backgroundColor = 'rgba(0, 0, 0, 0.01)';
+ var defaultRect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
+ defaultRect.setAttribute('x', '1');
+ defaultRect.setAttribute('y', '1');
+ defaultRect.setAttribute('width', 'calc(100% - 2px)');
+ defaultRect.setAttribute('height', 'calc(100% - 2px)');
+ defaultRect.setAttribute('fill', 'rgba(0, 0, 0, 0.01)');
+ defaultRect.setAttribute('stroke', '#cccccc');
+ defaultRect.setAttribute('stroke-width', '1');
+ svg.appendChild(defaultRect);
}
+ shapeGroup.appendChild(svg);
}
- // 도형 내부 Paragraph 텍스트가 정상 레이어링되도록 오버레이 처리
control.content.forEach(function (paragraphList) {
paragraphList.items.forEach(function (paragraph) {
_this3.drawParagraph(shapeGroup, paragraph);
});
});
- container.appendChild(shapeGroup);
-}
-```
-
-### 3.4 개별 텍스트 스팬 스타일 지정 (`drawText`)
-
-개별 문자열 `div` 스팬 단위로 `line-height`를 인라인 스타일로 직접 주입하여 글로벌 CSS 오버라이드를 해결한 코드입니다.
-
-```javascript
-key: "drawText",
-value: function drawText(container, paragraph, shapePointer, endPos) {
- var _this4 = this;
-
- var range = paragraph.content.slice(shapePointer.pos, endPos + 1);
- var texts = [];
- var ctrlIndex = 0;
- range.forEach(function (hwpChar) {
- if (typeof hwpChar.value === 'string') {
- texts.push(hwpChar.value);
- return;
- }
-
- if (hwpChar.type === CharType.Extened) {
- var control = paragraph.controls[ctrlIndex];
- ctrlIndex += 1;
-
- _this4.drawControl(container, control);
- }
-
- if (hwpChar.value === 13) {
- texts.push('\n');
- }
- });
- var text = texts.join('');
- var span = document.createElement('div');
- span.textContent = text;
- span.style.lineHeight = '1.65'; // 인라인 줄 간격 직접 부여하여 영역 겹침 현상 원천 차단
-
- var charShape = this.hwpDocument.info.getCharShpe(shapePointer.shapeIndex);
-
- if (charShape) {
- var fontBaseSize = charShape.fontBaseSize,
- fontRatio = charShape.fontRatio,
- color = charShape.color,
- fontId = charShape.fontId;
- var fontSize = fontBaseSize * (fontRatio[0] / 100);
- span.style.fontSize = "".concat(fontSize, "pt");
- span.style.lineBreak = 'anywhere';
- span.style.whiteSpace = 'pre-wrap';
- span.style.color = this.getRGBStyle(color);
- var fontFace = this.hwpDocument.info.fontFaces[fontId[0]];
- span.style.fontFamily = fontFace.getFontFamily();
- }
-
- container.appendChild(span);
-}
-```
-
-### 3.5 텍스트 단락 줄 간격 지정 (`drawParagraph`)
-
-단락 컨테이너 생성 시 줄 간격을 쾌적하게(1.65배) 렌더링하는 코드 부분입니다.
-
-```javascript
-key: "drawParagraph",
-value: function drawParagraph(container, paragraph) {
- var _this5 = this;
-
- var paragraphContainer = document.createElement('div');
- paragraphContainer.style.margin = '0';
- paragraphContainer.style.lineHeight = '1.65'; // 부모 단락에도 줄 간격 지정
-
- var shape = this.hwpDocument.info.paragraphShapes[paragraph.shapeIndex];
- paragraphContainer.style.textAlign = TEXT_ALIGN[shape.align];
- paragraph.shapeBuffer.forEach(function (shapePointer, index) {
- var endPos = paragraph.getShapeEndPos(index);
-
- _this5.drawText(paragraphContainer, paragraph, shapePointer, endPos);
- });
- container.append(paragraphContainer);
+ targetContainer.appendChild(shapeGroup);
}
```
@@ -359,8 +397,12 @@ value: function drawParagraph(container, paragraph) {
## 4. 디버깅 및 유지보수 가이드
-1. **클라이언트 캐시 문제:**
- * 소스 파일인 `hwp.js`가 업데이트된 후에도 브라우저가 이전 라이브러리를 캐싱하고 있다면 이미지/도형이 보이지 않거나 줄 간격이 좁게 유지됩니다. 테스트 전에는 반드시 **`Ctrl + F5`** 또는 개발자 도구의 **`Disable cache`** 옵션을 활성화하여 확인을 진행하십시오.
-2. **WMF/EMF 벡터 포맷 추가 제언:**
- * 현재 웹 표준 규격(`PNG`, `JPEG`, `GIF`) 이미지는 프론트엔드 내에서 완전하게 디코딩됩니다.
- * 향후 다수의 WMF/EMF 포맷에 대한 정교한 벡터 렌더링이 필요한 경우, `wmf.js` 라이브러리를 연동하거나, 가이드 3절의 백엔드 경량 이미지 변환 API(`magick convert` 등)를 통해 웹 표준 이미지(`WebP`/`PNG`)로 변환하여 렌더링하도록 확장 대응이 가능합니다.
+1. **클라이언트 캐시 문제**:
+ * 소스 파일인 `hwp.js`나 스타일시트가 업데이트된 후에도 브라우저가 이전 파일을 캐싱하고 있다면 레이아웃 변경이 보이지 않거나 스크롤이 유지됩니다. 테스트 전에는 반드시 **`Ctrl + F5`** 또는 개발자 도구의 **`Disable cache`** 옵션을 활성화하여 확인을 진행하십시오.
+2. **폰트 로딩**:
+ * CDN을 통해 가져오는 구글 Noto 웹 폰트가 차단된 특수 오프라인(망분리) 환경인 경우, 폰트 파일(`woff2`)을 로컬 서버 `views/main/css/font/` 경로 등에 직접 배치하고 `font.css` 파일에서 경로명을 상대경로로 기입하여 로딩해야 합니다.
+3. **가로/세로 혼용 문서의 가로 스크롤 조치**:
+ * 구역 설정으로 인해 세로형 문서 중간에 가로 방향(Landscape) 페이지가 포함될 경우, 해당 가로 페이지는 구역 크기에 비례하여 화면에 훨씬 넓게 그려집니다.
+ * 기본적으로 뷰어 컨테이너에 `overflow: auto`가 설정되어 가로 스크롤바가 자동 제공되나, 모바일 뷰 등을 고려하여 한 화면에 맞추는 기능이 요구될 경우 JavaScript 단에서 `transform: scale(비율)` 스타일을 동적으로 입혀 축소 렌더링하는 반응형 고도화 적용이 가능합니다.
+
+---
diff --git a/한글파일미리보기기능의 한계 및 제안.md b/한글파일미리보기기능의 한계 및 제안.md
new file mode 100644
index 0000000..29138d0
--- /dev/null
+++ b/한글파일미리보기기능의 한계 및 제안.md
@@ -0,0 +1,136 @@
+
+## 1. 기능 한계 원인 분석 및 최선 대응 방안
+
+본 섹션은 클라이언트 사이드 HWP 미리보기가 원본 문서와 완전히 일치하지 못하는 **구조적·기술적 원인**을 분석하고, 각 한계에 대해 현실적으로 취할 수 있는 **최선의 대응 방안**을 정리합니다.
+
+---
+
+### 1.1 한계의 근본 원인: HWP 포맷의 비공개 독점 규격
+
+HWP(한글과컴퓨터 워드프로세서) 파일 포맷은 **공개된 국제 표준(ISO/IEC)이 아닌 한컴의 독점 바이너리 규격**입니다.
+
+* **공개 문서의 한계**: 한컴은 일부 규격 문서(`HWP 5.0 File Format Specification`)를 공개했으나, 이는 전체 규격의 약 60~70% 수준이며 도형(Shape) 세부 속성, 필드 코드, 고급 서식 등 대부분의 복잡한 요소는 문서화되지 않았습니다.
+* **버전별 파편화**: HWP 파일은 버전(97/2002/2007/2010/2022 등)마다 내부 구조가 다르며, 버전 간 호환성 처리가 매우 복잡합니다.
+* **오픈소스 파서의 한계**: 현재 사용 중인 `hwp.js`(버전 0.0.3)는 커뮤니티 기반의 역공학(Reverse Engineering) 결과물로, 기본적인 텍스트·표·이미지만 처리하며 고급 기능 구현이 부재합니다.
+
+> **결론**: 브라우저 단에서 네이티브 한글 프로그램 수준의 완벽한 재현은 현재 기술로는 구조적으로 불가능합니다.
+
+---
+
+### 1.2 항목별 한계 원인 및 대응 방안
+
+#### ① 도형(Shape) 위치 및 크기 오차
+
+| 구분 | 내용 |
+|---|---|
+| **한계** | HWP 도형의 절대 좌표는 `vertRelTo`(세로 기준), `horzRelTo`(가로 기준) 등 복합적인 기준점 체계로 계산되며, 기준점이 문단·쪽·종이·단·표·셀 등 9가지 이상. 이 모든 경우를 역공학으로 완전 파악하기 불가능 |
+| **현재 대응** | `vertRelTo` 0(종이), 1(쪽), 2(문단) 3가지 케이스만 구현. DOM 트리 상위 탐색으로 실제 페이지 요소 발굴 후 좌표 배치 |
+| **최선 대응** | ① 현재 구현 유지(오차는 있으나 대략적 위치는 맞음) ② 도형이 매우 중요한 경우 **서버 사이드 변환(LibreOffice → PDF)** 방식으로 전환 |
+
+#### ② 선(Line) 도형 방향 오차
+
+| 구분 | 내용 |
+|---|---|
+| **한계** | HWP Line 도형은 bounding box 안에서의 정확한 시작점(x1,y1)·끝점(x2,y2) 좌표를 별도 payload에 저장하는데, `hwp.js` 파서는 이 payload를 파싱하지 않음 |
+| **현재 대응** | bounding box의 가로/세로 비율(aspect ratio)로 방향을 **추론**: 비율 < 0.15 → 수평선, 비율 > 6.5 → 수직선, 그 외 → 대각선 |
+| **최선 대응** | ① 현재 추론 방식 유지 (대부분의 실용적 케이스 커버) ② `hwp.js` 파서를 포크(fork)하여 `ShapeComponent` payload의 `startX/Y`, `endX/Y` 필드를 직접 파싱하는 고도화 개발 |
+
+#### ③ 이미지(Picture) 렌더링 실패 시 텍스트 유실
+
+| 구분 | 내용 |
+|---|---|
+| **한계** | HWP 문서에서 이미지와 텍스트가 같은 문단(Paragraph)에 혼재할 때, 이미지 파싱 실패 시 JavaScript `forEach` 루프가 중단되어 이후 텍스트가 통째로 누락 |
+| **현재 대응** | `drawControl()` 호출을 `try-catch`로 감싸 오류를 무시하고 나머지 텍스트를 계속 처리하도록 방어 코드 추가 |
+| **최선 대응** | ① 현재 방어 코드 유지 ② 이미지 로드 실패 시 `[그림 영역]` placeholder를 표시하여 레이아웃 유지 |
+
+#### ④ WMF/EMF 벡터 이미지 렌더링 품질
+
+| 구분 | 내용 |
+|---|---|
+| **한계** | WMF(Windows Metafile)는 브라우저 표준 이미지 포맷이 아닌 Windows 전용 벡터 포맷. `wmf.js`로 Canvas 변환하면 일부 GDI 명령어가 누락되거나 색상·선 굵기가 다르게 표현됨 |
+| **현재 대응** | SheetJS의 `wmf.js`로 Canvas 렌더링 후 PNG DataURL로 변환하여 `backgroundImage`에 바인딩 |
+| **최선 대응** | ① 현재 방식 유지 ② WMF가 매우 중요한 경우 서버에서 `LibreOffice` 또는 `ImageMagick`으로 SVG/PNG 변환 후 제공 |
+
+#### ⑤ 복잡한 표(Table) 레이아웃 재현 한계
+
+| 구분 | 내용 |
+|---|---|
+| **한계** | HWP 표의 셀 병합(merge), 대각선 테두리, 배경 그라데이션, 표 캡션, 표 안의 표(nested table) 등 고급 서식이 파서에서 누락됨 |
+| **현재 대응** | 기본 셀 너비/높이/패딩/테두리/세로정렬을 파싱하여 `` 구조로 렌더링 |
+| **최선 대응** | ① 현재 방식으로 대부분의 단순 표는 재현 가능 ② 복잡한 표 서식이 필수인 경우 서버 사이드 변환 방식 권고 |
+
+#### ⑥ 폰트 장평(Font Metrics) 불일치로 인한 줄바꿈 차이
+
+| 구분 | 내용 |
+|---|---|
+| **한계** | 한글 문서의 줄바꿈은 한컴 전용 폰트 렌더러의 글자 너비 계산값 기준. 웹 폰트(Noto KR 등)는 같은 포인트 크기라도 글자 너비가 미세하게 달라 줄바꿈 위치가 틀어짐 |
+| **현재 대응** | `@font-face`로 한글 기본 폰트(바탕, 돋움 등)를 Noto KR 웹 폰트에 매핑하여 최대한 근접하게 표시 |
+| **최선 대응** | ① 서버에 한컴 폰트(HCR 폰트 등)를 woff2로 변환하여 직접 서빙 ② 완벽한 재현이 필요하면 서버 사이드 PDF 변환 방식 필수 |
+
+---
+
+### 1.3 대응 방식 비교: 클라이언트 사이드 vs 서버 사이드
+
+현재 구현은 **클라이언트 사이드** 방식입니다. 아래는 두 방식의 장단점 비교입니다.
+
+| 항목 | 클라이언트 사이드 (현재) | 서버 사이드 (LibreOffice/PDF 변환) |
+|---|---|---|
+| **재현 정확도** | ★★☆☆☆ (기본 요소만 재현) | ★★★★☆ (원본에 매우 근접) |
+| **서버 부하** | 없음 (브라우저가 처리) | 높음 (변환 프로세스 실행) |
+| **응답 속도** | 빠름 (별도 변환 없음) | 느림 (변환 후 전달) |
+| **오프라인 지원** | 가능 | 불가 |
+| **유지보수 비용** | 높음 (파서 직접 관리) | 낮음 (LibreOffice가 처리) |
+| **도형/수식/고급서식** | ❌ 미지원 | ✅ 대부분 지원 |
+| **서버 요구사항** | 없음 | LibreOffice 설치 필요 |
+
+---
+
+### 1.4 최선 대응 전략 권고
+
+현재 PM 시스템의 **HWP 미리보기 목적**(`첨부 파일의 대략적인 내용 확인`)을 고려한 권고안입니다.
+
+#### 전략 A: 현재 방식 유지 + 점진적 개선 (권고)
+
+```
+현재 클라이언트 사이드 방식 유지
+ ├─ 텍스트/표/이미지 기본 재현 가능
+ ├─ 서버 부하 없음
+ ├─ 복잡한 도형/수식 → "[그림 영역]" placeholder로 표시
+ └─ 사용자에게 "정확한 확인은 원본 다운로드" 안내 제공
+```
+
+#### 전략 B: 하이브리드 방식 (장기 권고)
+
+```
+1차 시도: 클라이언트 사이드 hwp.js 렌더링 (빠름)
+ ↓ 렌더링 오류 또는 복잡한 문서 감지 시
+2차 폴백: "PDF로 보기" 버튼으로 서버 사이드 변환 안내
+ ↓ 서버에서 LibreOffice를 통해 HWP → PDF 변환
+PDF 뷰어로 고품질 렌더링 제공
+```
+
+> 현재 시스템에는 이미 **"PDF로 보기"** 폴백 버튼이 구현되어 있어 전략 B의 기반은 갖춰진 상태입니다.
+
+#### 전략 C: 완전 서버 사이드 전환 (고정밀도 요구 시)
+
+```
+HWP 파일 업로드
+ → 서버(LibreOffice headless)에서 HWP → PDF/SVG 변환
+ → 변환된 PDF를 PDF.js로 고품질 렌더링
+```
+
+* **장점**: 원본 문서와 가장 높은 일치율 보장
+* **단점**: 서버에 LibreOffice 설치 및 운영 필요, 변환 시간 소요(파일당 2~10초)
+
+---
+
+### 1.5 사용자 안내 메시지 권고
+
+미리보기와 원본 문서 간 차이가 있을 수 있음을 사용자에게 명확히 안내하는 것이 중요합니다.
+
+```
+💡 한글 문서(.hwp) 미리보기는 브라우저에서 직접 렌더링하는 방식으로,
+ 복잡한 도형·수식·특수 서식은 원본과 다르게 표시될 수 있습니다.
+ 정확한 내용 확인이 필요한 경우 [파일 다운로드] 또는 [PDF로 보기]를 이용해 주세요.
+```
+
diff --git a/한글파일미리보기작업내역.md b/한글파일미리보기작업내역.md
new file mode 100644
index 0000000..d343d64
--- /dev/null
+++ b/한글파일미리보기작업내역.md
@@ -0,0 +1,117 @@
+# 한글 파일(HWP) 미리보기 렌더링 고도화 작업 내역서
+
+본 문서는 웹 브라우저(클라이언트) 환경에서 한글 문서(`.hwp`) 파일을 직접 파싱하고 렌더링하는 클라이언트 사이드 미리보기 엔진의 고도화 작업 및 디버깅 수행 내역을 상세히 기록한 문서입니다.
+
+---
+
+## 1. 주요 작업
+
+클라이언트 뷰어 라이브러리(`libs/hwp.js`) 및 스타일시트, HTML 로드 방식을 개선하여 원본 문서와의 레이아웃 일치율을 비약적으로 끌어올리고 깨진 리소스(이미지/도형)를 복구하는 것을 목표로 하였습니다.
+
+* **바이너리 압축 해제 및 이미지 MIME 안정화**: 무압축 Raw 이미지 파싱 우회 및 다단계 압축해제 예외처리 폴백 구축, 오타로 인한 이미지 로딩 에러(images/png) 수정.
+* **텍스트 영역 겹침 및 간격 보정**: 글로벌 CSS 충돌로 인한 텍스트 줄 겹침 결함을 단락 및 스팬 인라인 스타일 강제 부여를 통해 보정.
+* **HWP 문서 내 자체 생성 도형(벡터) 복원 및 위치 정밀도 교정**:
+ - 파싱 대상에서 유실되었던 직사각형, 타원, 선 등의 형상을 SVG 배경 레이어 렌더러로 복구.
+ - 도형의 기준 위치(`vertRelTo` 세로 기준: 문단/쪽/종이)에 따라 절대 배치(`position: absolute`) 및 좌표 배치를 분기하여, 도형이 정상 범위를 벗어나 페이지 바깥(회색 배경 부분)으로 밀려 나가는 겹침 오류를 완벽히 수정.
+* **표(Table) 스타일 및 정렬 정밀 매핑**: 셀 여백(Padding) 스타일 강제 적용 및 HWP 바이너리에 포함된 세로 정렬(Vertical Alignment) 속성을 CSS 속성에 1대1 일치화.
+* **한글 기본 폰트 대체 매핑**: 클라이언트 기기에 한양굴림, 한컴바탕 등 오피스 전용 폰트가 설치되어 있지 않을 경우를 대비하여 웹 폰트(Noto Serif/Sans KR)를 연동.
+* **WMF 벡터 파일 캔버스 디코딩 연동**: 브라우저 표준 디코더가 읽지 못하는 `.wmf` 벡터 그래픽 파일을 클라이언트 단에서 파싱하여 PNG 이미지로 변환 및 렌더링.
+* **뷰어 레이아웃 너비 확장 (가로 스크롤 제거)**:
+ - 기본 가로 너비가 좁아 A4 문서 미리보기 시 발생하던 불필요한 가로(횡) 스크롤바를 원천 제거하기 위해 우측 미리보기 영역 너비를 확장.
+* **팝업 미리보기 창 라이브러리/스타일셋 동기화**:
+ - 별도 팝업 창(`views/main/popup.html`)에서 한글 미리보기를 열 때 `wmf.js`와 `font.css` 로드 누락으로 인해 도형이 깨지거나 횡스크롤바가 발생하던 문제를 리소스 태그 삽입 및 인라인/클래스 스타일링 규격화로 정정.
+
+---
+
+## 2. 수행 내용
+
+수행된 작업의 구체적인 파일별 수정 상세 명세입니다.
+
+### 2.1 폰트 정의 추가 및 웹 폰트 매핑
+* **대상 파일**: [font.css](file:///d:/40.%20개발소스/04.%20PM/pm_ver4/trunk/PM_ver4/views/main/css/font.css)
+* **내용**:
+ - 구글 Noto Sans KR 및 Noto Serif KR 웹 폰트를 CSS 임포트하고 폰트 매핑을 정의했습니다.
+
+### 2.2 WMF 디코더 스크립트 로드 추가
+* **대상 파일**: [main.html](file:///d:/40.%20개발소스/04.%20PM/pm_ver4/trunk/PM_ver4/views/main/main.html)
+* **내용**:
+ - `libs/hwp.js`가 로딩되기 전 `wmf.js` 라이브러리를 로드하는 태그를 추가했습니다.
+
+### 2.3 뷰어 레이아웃 너비 확장 및 정렬 보정
+* **대상 파일**: [style_archive.css](file:///d:/40.%20개발소스/04.%20PM/pm_ver4/trunk/PM_ver4/views/main/css/style_archive.css)
+* **내용**:
+ - 우측 미리보기 컨테이너(`.archive-main-right`)의 너비를 기존 `41rem`에서 A4 가로폭(약 794px + 여백)이 충분히 수용되는 **`55rem` (880px)**으로 확장하여 가로 스크롤바가 생기지 않도록 수정했습니다.
+ - 너비 변경에 따라 우측 영역에 맞물려 있던 팝업 메뉴 및 토글 단추 위치(`right: 41.5rem` -> **`right: 55.5rem`**)를 함께 조정했습니다.
+
+### 2.4 HWP 파서 및 렌더러 로직 전면 개선
+* **대상 파일**: [hwp.js](file:///d:/40.%20개발소스/04.%20PM/pm_ver4/trunk/PM_ver4/libs/hwp.js)
+* **내용**:
+
+#### ① OLE 바이너리 이미지 압축 우회 및 안정화
+- 이미지 데이터 파싱 시, 첫 4바이트 Magic Number(PNG, JPEG, GIF, WMF)를 검사하여 압축이 필요 없는 데이터는 드플레트(Decompress) 과정을 생략하고 직접 읽게 하였습니다.
+- 압축 해제 실패 시 다단계 폴백 구조(`windowBits: -15` -> `기본 inflate` -> `inflateRaw` -> `원본 raw 복사`)를 구축하여 뷰어 크래시를 차단했습니다.
+- MIME 명칭 오타(`"images/"` -> `"image/"`) 및 JPG 규격 명칭(`image/jpeg`)을 보정했습니다.
+
+#### ② 단락 및 텍스트 줄 겹침 결함 수정
+- `drawText` 및 `drawParagraph` 메서드 내에서 텍스트가 삽입되는 각 `div` 스팬과 컨테이너에 `line-height: 1.65` 인라인 스타일을 적용해 부모 스타일시트에 의해 줄 영역이 위아래로 포개어지는 문제를 예방했습니다.
+
+#### ③ 도형(Rectangle/Ellipse/Line)의 SVG 배경 레이어 렌더링 고도화 및 좌표 보정
+- HWP 파서의 `visit` 메서드 내에 직사각형, 타원, 선, 호, 다각형, 자유곡선 레코드 ID들을 식별하도록 등록하고 도형 타입을 구분 지었습니다.
+- `drawParagraph` 렌더링 시 문단 `div` 요소를 쪽(Page)에 즉시 추가하여 자식 도형들의 절대 배치 위치 기준(`parentNode`)이 정상적으로 작동하도록 수정했습니다.
+- `drawShape` 메서드에서 도형의 세로 기준 설정(`vertRelTo`)을 분석합니다.
+ - **`vertRelTo === 2` (문단 기준)**: 문단 컨테이너 `div`에 `position: relative` 스타일을 적용하고, 도형을 `position: absolute; top: [verticalOffset]; left: [horizontalOffset];`로 배치하여 본문 내에 정확히 안착시켰습니다.
+ - **`vertRelTo === 0 또는 1` (종이/쪽 기준)**: 도형을 `position: absolute`로 지정하고 부모 쪽 컨테이너(`container.parentNode`)의 절대 레이어에 직접 덧그려 올바른 위치를 맞췄습니다.
+ - 이를 통해 기존에 상대 배치 마진(`marginTop`) 누적으로 인해 본문 뒤로 엉뚱하게 밀려 나거나 페이지 바깥 회색 프레임으로 가라앉던 결함을 완벽히 해소했습니다.
+
+#### ④ 표 셀 세로 정렬(vertical-align) 파싱 및 여백(padding) 스타일링
+- `visitCellListHeader` 파서 메서드가 셀 레코드 버퍼를 해독할 때, 세로 정렬 바이트(verticalAlign: 0-상단, 1-중앙, 2-하단) 데이터를 추가로 추출하여 전달하도록 개선했습니다.
+- `drawColumn` 렌더러가 td 엘리먼트를 조립할 때, 파싱된 셀 padding 값을 CSS 속성으로 가공하고 데이터가 0이거나 유실된 경우에는 가독성을 확보하도록 디폴트 규격 패딩(`padding: 2px 5px;`)을 바인딩했습니다.
+- 해독된 세로 정렬 상태 값을 CSS `vertical-align` 속성(`top`, `middle`, `bottom`)과 정확히 일치화시켜 테이블 안의 정렬을 완벽하게 재현했습니다.
+
+#### ⑤ WMF 이미지의 Canvas 드로잉 및 PNG 변환 연동
+- 그림 객체(`isPicture`) 렌더링 시 이미지 확장자가 `wmf`인 경우 `wmf.js` API를 연동했습니다.
+- WMF 이미지 원본 크기를 측정하여 브라우저 메모리에 가상 `` 엘리먼트를 생성해 캔버스에 이미지를 렌더링한 후, `canvas.toDataURL('image/png')`를 추출해 div의 `backgroundImage`에 바인딩했습니다.
+
+### 2.5 팝업 미리보기 리소스 추가 로드 및 스타일셋 개선
+* **대상 파일**:
+ - [popup.html](file:///d:/40.%20개발소스/04.%20PM/pm_ver4/trunk/PM_ver4/views/main/popup.html)
+ - [popup.js](file:///d:/40.%20개발소스/04.%20PM/pm_ver4/trunk/PM_ver4/views/main/jsm/popup.js)
+ - [pageRenderer.js](file:///d:/40.%20개발소스/04.%20PM/pm_ver4/trunk/PM_ver4/views/main/jsm/archive/pageRenderer.js)
+ - [docPageRenderer.js](file:///d:/40.%20개발소스/04.%20PM/pm_ver4/trunk/PM_ver4/views/main/jsm/officialDoc/docPageRenderer.js)
+* **내용**:
+ - `popup.html`에 `font.css` 링크 및 `wmf.js` 스크립트를 추가하여 팝업 뷰어에서도 한글 기본 폰트 대체 및 WMF 벡터 도형이 실시간으로 변환되도록 연동했습니다.
+ - `popup.js`, `pageRenderer.js`, `docPageRenderer.js` 파일 내의 HWP 렌더링 영역 스타일 인젝션 코드에서 컨테이너의 최대 너비를 **`950px`**로 확장 적용했습니다.
+ - `.hwpjs-viewer > div` 클래스에 `overflow-x: hidden !important` 스타일을 동적으로 입혀 불필요한 횡스크롤바 생성을 원천 차단하고, `div[data-page-number]` 페이지 요소의 가로폭 축소 유실을 유발하던 `max-width: 100%` 설정을 완전히 제거하여 표와 도형이 찌그러지고 정렬이 어긋나는 레이아웃 파괴 버그를 예방하였습니다.
+
+---
+
+## 3. 검증 결과
+
+수정 파일의 문법적 무결성 및 실제 동작 상태에 대한 검증 내용입니다.
+
+### 3.1 JavaScript 구문 분석 검증 (Syntax Check)
+* Node.js 구문 체크 명령어를 통과하여 수정된 `libs/hwp.js` 파일에 어떠한 문법적 에러도 없음을 증명했습니다.
+```bash
+node -c "libs/hwp.js" => (오류 없이 성공적으로 컴파일 완료)
+```
+
+### 3.2 기능적 검증 요약
+* **도형 좌표 보정**: 본래 문단 뒤에 겹쳐 있어야 할 동그라미(Ellipse)와 연결선(Line) 등의 구조물들이 페이지 밖으로 탈탈 밀려나던 문제가 완전히 해결되어 본문 표 바로 밑에 원본과 동일하게 정밀 안착하는 것을 확인했습니다.
+* **가로 스크롤 소거**: 미리보기 화면을 띄웠을 때 횡스크롤바가 더 이상 나타나지 않으며, A4 규격의 본문과 여백이 전체적으로 여유롭게 한 화면에 들어오는 것을 확인했습니다.
+* **이미지 및 테이블**: WMF 이미지 디코딩 및 표의 세로 정렬/여백 등이 모두 원본 문서의 비주얼과 정합성이 유지됩니다.
+
+---
+
+## 4. 조치사항 (운영 및 유지보수 가이드)
+
+프로젝트 운영 단계에서 발생 가능한 상황에 대한 대처 방안입니다.
+
+1. **클라이언트 캐시 갱신 (가장 중요)**
+ - 백엔드 코드와 달리 프론트엔드 자바스크립트(`hwp.js`, `font.css`) 및 스타일시트(`style_archive.css`) 파일은 클라이언트 브라우저가 강력하게 캐싱합니다.
+ - 렌더링 개선 내역이 정상적으로 적용되지 않을 경우, 브라우저에서 **`Ctrl + F5`**(강제 새로고침)를 수행하거나 개발자 도구(F12)의 네트워크 탭에서 **'캐시 사용 안 함(Disable Cache)'**을 활성화한 후 재시도하십시오.
+2. **폐쇄망(오프라인) 배포 시 폰트 로드 조치**
+ - 현재 등록된 폰트는 구글 웹 폰트 CDN 주소를 사용합니다. 만약 외부 인터넷 접근이 불가능한 망분리/오폐쇄망 환경에 프로젝트가 배포될 경우, `@import` 주소의 구글 폰트를 가져오지 못해 폰트가 기본형으로 깨질 수 있습니다.
+ - **조치 방안**: `views/main/css/` 하위에 `fonts/` 디렉토리를 생성하고, `NotoSansKR.woff2`, `NotoSerifKR.woff2` 파일을 서버에 저장한 후 `font.css`에서 상대 경로(`url('./fonts/...')`)로 서빙하도록 경로를 전환해야 합니다.
+3. **가로/세로 혼용 문서의 가로 스크롤 조치**
+ - 구역 설정으로 인해 세로형 문서 중간에 가로 방향(Landscape) 페이지가 포함될 경우, 해당 가로 페이지는 구역 크기에 비례하여 화면에 훨씬 넓게 그려집니다.
+ - 기본적으로 뷰어 컨테이너에 `overflow: auto`가 설정되어 가로 스크롤바가 자동 제공되나, 모바일 뷰 등을 고려하여 한 화면에 맞추는 기능이 요구될 경우 JavaScript 단에서 `transform: scale(비율)` 스타일을 동적으로 입혀 축소 렌더링하는 반응형 고도화 적용이 가능합니다.