Files
PM_test/한글파일미리보기구현.md
2026-06-19 17:58:47 +09:00

409 lines
24 KiB
Markdown

# 한글 파일(HWP) 클라이언트 사이드 미리보기 구현 및 기술 명세서 (고도화 버전)
본 문서는 서버 자원 소모 없이 웹 브라우저(클라이언트) 단에서 `.hwp` 파일을 직접 파싱하고 안정적으로 이미지를 비롯한 도형 요소까지 화면에 렌더링하도록 커스텀 반영한 작업 내용, 구현 로직, 아키텍처 및 코드 명세를 다룹니다.
---
## 1. 미리보기 아키텍처 및 데이터 흐름
클라이언트 브라우저가 HWP 바이너리를 가져와 렌더링하기까지의 전체적인 흐름입니다.
```mermaid
graph TD
A["HWP 파일 다운로드 (Blob)"] --> B["FileReader로 Binary String 로드"]
B --> C["hwp.js Viewer 초기화"]
C --> D["OLE Compound 파일 구조 해석"]
D --> E["BinData 가상 폴더 내 이미지 파싱"]
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{"확장자 판별"}
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바이트)되는 문제가 있었습니다.
* **조치**:
1. 압축 해제 전 바이너리의 **첫 4바이트(Magic Number) 시그니처**를 대조하여 웹 표준 파일 유형(PNG, JPEG, GIF, WMF) 검사를 선행합니다.
2. 이미 시그니처를 충족하는 Raw 이미지의 경우 압축 해제를 우회하도록 최적화하여 렌더링 성능 및 안정성을 향상했습니다.
3. 압축이 필요한 경우 3단계 예외 처리 폴백(`windowBits: -15` -> `표준 inflate` -> `inflateRaw` -> `원본 바이너리`)을 구축하여 어떠한 조건에서도 파싱이 멈추지 않도록 조치했습니다.
### 2.2 MIME 타입 오류 수정 및 바이너리 안전성 보장
* **현상**: 기존 뷰어 소스 코드에 `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) 렌더링의 SVG 배경 레이어 적용 및 위치 정합성 교정
* **현상**: 본문 내에 삽입된 이미지는 로드가 완료되었으나, 문서 편집기 내부에서 자체 제작한 **직사각형(Rectangle), 타원(Ellipse), 선(Line), 다각형(Polygon) 등의 벡터 도형**은 파서가 태그 분류를 누락하여 완전히 투명하게 렌더링되거나, 상대 배치 마진 누적으로 인해 본문 뒤로 엉뚱하게 밀려나서 페이지 바깥의 회색 프레임에 잘못 렌더링되는 문제가 있었습니다.
* **조치**:
1. HWP 파서의 핵심 순회 구조(`visit` 스위치-케이스 문) 내에 누락된 도형 컴포넌트 태그 ID들을 등록하여 파싱 단계에서 도형 종류(`control.type`)를 올바르게 정의하도록 했습니다.
2. 렌더링 엔진(`drawShape`) 단에서 단순 HTML/CSS 스타일로 형태만 구현하던 단독 태그 방식 대신, 글씨 배치가 되는 부모 컨테이너 `div` 내부에 **절대 배치된 `<svg>` 배경 레이어를 생성하여 `<rect>`, `<ellipse>`, `<line>` 요소를 주입**해 벡터 윤곽선을 정밀하게 구현했습니다.
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` 속성이 덮어씌워져 줄 영역이 위아래로 심하게 겹쳐 보이는 시각적 오류가 발생했습니다.
* **조치**:
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)
### 3.1 이미지 원본 파싱 및 Decompress 처리 (`visitBinData`)
가상 OLE 디렉토리 내부에서 이미지를 파싱할 때 헤더를 분석하여 압축 우회 및 안정적으로 원본 바이트 배열을 추출하는 로직입니다.
```javascript
key: "visitBinData",
value: function visitBinData(record) {
var reader = new ByteReader(record.payload);
reader.readUInt16();
var id = reader.readUInt16();
var extension = reader.readString();
try {
var path = "Root Entry/BinData/BIN".concat("".concat(id.toString(16).toUpperCase()).padStart(4, '0'), ".").concat(extension);
var entry = cfb.find(this.container, path);
if (!entry || !entry.content) {
this.result.binData.push(new BinData(extension, new Uint8Array(0)));
return;
}
var payload = entry.content;
// 1. 첫 4바이트 매직 넘버 검사로 Raw 이미지 선별
var isRawImage = false;
if (payload.length >= 4) {
// PNG: 89 50 4E 47
if (payload[0] === 0x89 && payload[1] === 0x50 && payload[2] === 0x4E && payload[3] === 0x47) isRawImage = true;
// JPEG: FF D8 FF
else if (payload[0] === 0xFF && payload[1] === 0xD8 && payload[2] === 0xFF) isRawImage = true;
// 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] === 0x7D && payload[1] === 0xCD && payload[2] === 0xC6 && payload[3] === 0x9A) isRawImage = true;
}
var decompressed;
if (isRawImage) {
decompressed = payload;
} else {
try {
decompressed = pako_1.inflate(payload, { windowBits: -15 });
} catch (e1) {
try {
decompressed = pako_1.inflate(payload);
} catch (e2) {
try {
decompressed = pako_1.inflateRaw(payload);
} catch (e3) {
decompressed = payload;
}
}
}
}
this.result.binData.push(new BinData(extension, decompressed));
} catch (err) {
this.result.binData.push(new BinData(extension, new Uint8Array(0)));
}
}
```
### 3.2 셀 속성 세로 정렬 감지 추가 (`visitCellListHeader`)
표 내부 셀(Cell)의 크기 및 패딩에 더하여 세로 정렬(상단/중앙/하단) 옵션의 바이트 코드를 감지하는 구조입니다.
```javascript
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 표 셀 스타일 및 세로 정렬 일치 (`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");
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");
}
shapeGroup.style.zIndex = "".concat(control.zIndex);
shapeGroup.style.verticalAlign = 'middle';
shapeGroup.style.display = 'inline-block';
if (isPicture(control)) {
var image = this.hwpDocument.info.binData[control.info.binID];
if (!image || !image.payload || image.payload.length === 0) {
shapeGroup.style.border = '1px dashed #aaaaaa';
shapeGroup.style.backgroundColor = '#f8f8f8';
var placeholder = document.createElement('div');
placeholder.style.display = 'flex';
placeholder.style.alignItems = 'center';
placeholder.style.justifyContent = 'center';
placeholder.style.width = '100%';
placeholder.style.height = '100%';
placeholder.style.color = '#888888';
placeholder.style.fontSize = '12px';
placeholder.style.fontWeight = 'bold';
placeholder.textContent = '[그림 영역]';
shapeGroup.appendChild(placeholder);
} else {
var uint8Arr = image.payload instanceof Uint8Array ? image.payload : new Uint8Array(image.payload);
// 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 {
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) {
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) {
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 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) {
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 {
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);
}
control.content.forEach(function (paragraphList) {
paragraphList.items.forEach(function (paragraph) {
_this3.drawParagraph(shapeGroup, paragraph);
});
});
targetContainer.appendChild(shapeGroup);
}
```
---
## 4. 디버깅 및 유지보수 가이드
1. **클라이언트 캐시 문제**:
* 소스 파일인 `hwp.js`나 스타일시트가 업데이트된 후에도 브라우저가 이전 파일을 캐싱하고 있다면 레이아웃 변경이 보이지 않거나 스크롤이 유지됩니다. 테스트 전에는 반드시 **`Ctrl + F5`** 또는 개발자 도구의 **`Disable cache`** 옵션을 활성화하여 확인을 진행하십시오.
2. **폰트 로딩**:
* CDN을 통해 가져오는 구글 Noto 웹 폰트가 차단된 특수 오프라인(망분리) 환경인 경우, 폰트 파일(`woff2`)을 로컬 서버 `views/main/css/font/` 경로 등에 직접 배치하고 `font.css` 파일에서 경로명을 상대경로로 기입하여 로딩해야 합니다.
3. **가로/세로 혼용 문서의 가로 스크롤 조치**:
* 구역 설정으로 인해 세로형 문서 중간에 가로 방향(Landscape) 페이지가 포함될 경우, 해당 가로 페이지는 구역 크기에 비례하여 화면에 훨씬 넓게 그려집니다.
* 기본적으로 뷰어 컨테이너에 `overflow: auto`가 설정되어 가로 스크롤바가 자동 제공되나, 모바일 뷰 등을 고려하여 한 화면에 맞추는 기능이 요구될 경우 JavaScript 단에서 `transform: scale(비율)` 스타일을 동적으로 입혀 축소 렌더링하는 반응형 고도화 적용이 가능합니다.
---