📦 Initialize Geulbeot structure and merge Prompts & test projects

This commit is contained in:
2026-03-05 11:32:29 +09:00
commit 555a954458
687 changed files with 205247 additions and 0 deletions

View File

@@ -0,0 +1 @@
도레미파솔라시도

View File

@@ -0,0 +1,27 @@
시추조사, 지반조사, 지질조사, 보링조사, 현장조사,
시추공, 시추공별, 시추계획, 시추위치, 시추심도, 시추기,
주상도, 지층주상도, 시추주상도, 토질주상도,
표준관입시험, SPT, N값, 관입시험, 현장시험,
지층, 지층정보, 지층구성, 지층분류, 지층경계,
토사, 점토, 실트, 모래, 자갈, 호박돌, 전석,
풍화토, 풍화암, 연암, 보통암, 경암, 극경암,
지하수, 지하수위, 수위측정, 투수시험, 현장투수시험,
토질, 토질시험, 토질역학, 토질조사,
샘플링, 불교란시료, 교란시료, 코어, 코어채취,
지반, 지반정보, 지반특성, 지반강도, 지반침하,
기초, 기초설계, 말뚝기초, 직접기초, 깊은기초,
측량, GPS측량, GNSS, RTK, 토탈스테이션, 레벨측량,
수치지형도, 정사영상, 수치표고모델, DEM, DSM, DTM,
좌표, 좌표계, TM좌표, UTM좌표, 경위도, 지오이드,
드론, UAV, 무인항공기, 항공측량, 드론측량,
DJI, Terra, Pix4D, 포인트클라우드, 정밀도,
성과품, 성과도, 보고서, 조사보고서, 지반조사보고서,
위치도, 평면도, 종단면도, 횡단면도, 단면도,
현장, 현장조사, 현장시험, 현장작업, 현장지시,
IoT센서, 센서, 자동입력, 실시간, 웹기반,
토공, 절토, 성토, 물량산출, 체적계산,
도로, 도로노선, 노선측량, 중심선, 기준점,
신규, 기존, 추가, 수정, 검토, 승인,
주상도, 시추주상도, 토질주상도, 지층주상도,
신규, 신규의, 기존의,
지반조사, 지반조사업무

View File

@@ -0,0 +1,27 @@
시추조사, 지반조사, 지질조사, 보링조사, 현장조사,
시추공, 시추공별, 시추계획, 시추위치, 시추심도, 시추기,
주상도, 지층주상도, 시추주상도, 토질주상도,
표준관입시험, SPT, N값, 관입시험, 현장시험,
지층, 지층정보, 지층구성, 지층분류, 지층경계,
토사, 점토, 실트, 모래, 자갈, 호박돌, 전석,
풍화토, 풍화암, 연암, 보통암, 경암, 극경암,
지하수, 지하수위, 수위측정, 투수시험, 현장투수시험,
토질, 토질시험, 토질역학, 토질조사,
샘플링, 불교란시료, 교란시료, 코어, 코어채취,
지반, 지반정보, 지반특성, 지반강도, 지반침하,
기초, 기초설계, 말뚝기초, 직접기초, 깊은기초,
측량, GPS측량, GNSS, RTK, 토탈스테이션, 레벨측량,
수치지형도, 정사영상, 수치표고모델, DEM, DSM, DTM,
좌표, 좌표계, TM좌표, UTM좌표, 경위도, 지오이드,
드론, UAV, 무인항공기, 항공측량, 드론측량,
DJI, Terra, Pix4D, 포인트클라우드, 정밀도,
성과품, 성과도, 보고서, 조사보고서, 지반조사보고서,
위치도, 평면도, 종단면도, 횡단면도, 단면도,
현장, 현장조사, 현장시험, 현장작업, 현장지시,
IoT센서, 센서, 자동입력, 실시간, 웹기반,
토공, 절토, 성토, 물량산출, 체적계산,
도로, 도로노선, 노선측량, 중심선, 기준점,
신규, 기존, 추가, 수정, 검토, 승인,
주상도, 시추주상도, 토질주상도, 지층주상도,
신규, 신규의, 기존의,
지반조사, 지반조사업무

View File

@@ -0,0 +1,27 @@
시추조사, 지반조사, 지질조사, 보링조사, 현장조사,
시추공, 시추공별, 시추계획, 시추위치, 시추심도, 시추기,
주상도, 지층주상도, 시추주상도, 토질주상도,
표준관입시험, SPT, N값, 관입시험, 현장시험,
지층, 지층정보, 지층구성, 지층분류, 지층경계,
토사, 점토, 실트, 모래, 자갈, 호박돌, 전석,
풍화토, 풍화암, 연암, 보통암, 경암, 극경암,
지하수, 지하수위, 수위측정, 투수시험, 현장투수시험,
토질, 토질시험, 토질역학, 토질조사,
샘플링, 불교란시료, 교란시료, 코어, 코어채취,
지반, 지반정보, 지반특성, 지반강도, 지반침하,
기초, 기초설계, 말뚝기초, 직접기초, 깊은기초,
측량, GPS측량, GNSS, RTK, 토탈스테이션, 레벨측량,
수치지형도, 정사영상, 수치표고모델, DEM, DSM, DTM,
좌표, 좌표계, TM좌표, UTM좌표, 경위도, 지오이드,
드론, UAV, 무인항공기, 항공측량, 드론측량,
DJI, Terra, Pix4D, 포인트클라우드, 정밀도,
성과품, 성과도, 보고서, 조사보고서, 지반조사보고서,
위치도, 평면도, 종단면도, 횡단면도, 단면도,
현장, 현장조사, 현장시험, 현장작업, 현장지시,
IoT센서, 센서, 자동입력, 실시간, 웹기반,
토공, 절토, 성토, 물량산출, 체적계산,
도로, 도로노선, 노선측량, 중심선, 기준점,
신규, 기존, 추가, 수정, 검토, 승인,
주상도, 시추주상도, 토질주상도, 지층주상도,
신규, 신규의, 기존의,
지반조사, 지반조사업무

View File

@@ -0,0 +1,27 @@
시추조사, 지반조사, 지질조사, 보링조사, 현장조사,
시추공, 시추공별, 시추계획, 시추위치, 시추심도, 시추기,
주상도, 지층주상도, 시추주상도, 토질주상도,
표준관입시험, SPT, N값, 관입시험, 현장시험,
지층, 지층정보, 지층구성, 지층분류, 지층경계,
토사, 점토, 실트, 모래, 자갈, 호박돌, 전석,
풍화토, 풍화암, 연암, 보통암, 경암, 극경암,
지하수, 지하수위, 수위측정, 투수시험, 현장투수시험,
토질, 토질시험, 토질역학, 토질조사,
샘플링, 불교란시료, 교란시료, 코어, 코어채취,
지반, 지반정보, 지반특성, 지반강도, 지반침하,
기초, 기초설계, 말뚝기초, 직접기초, 깊은기초,
측량, GPS측량, GNSS, RTK, 토탈스테이션, 레벨측량,
수치지형도, 정사영상, 수치표고모델, DEM, DSM, DTM,
좌표, 좌표계, TM좌표, UTM좌표, 경위도, 지오이드,
드론, UAV, 무인항공기, 항공측량, 드론측량,
DJI, Terra, Pix4D, 포인트클라우드, 정밀도,
성과품, 성과도, 보고서, 조사보고서, 지반조사보고서,
위치도, 평면도, 종단면도, 횡단면도, 단면도,
현장, 현장조사, 현장시험, 현장작업, 현장지시,
IoT센서, 센서, 자동입력, 실시간, 웹기반,
토공, 절토, 성토, 물량산출, 체적계산,
도로, 도로노선, 노선측량, 중심선, 기준점,
신규, 기존, 추가, 수정, 검토, 승인,
주상도, 시추주상도, 토질주상도, 지층주상도,
신규, 신규의, 기존의,
지반조사, 지반조사업무

View File

@@ -0,0 +1,27 @@
시추조사, 지반조사, 지질조사, 보링조사, 현장조사,
시추공, 시추공별, 시추계획, 시추위치, 시추심도, 시추기,
주상도, 지층주상도, 시추주상도, 토질주상도,
표준관입시험, SPT, N값, 관입시험, 현장시험,
지층, 지층정보, 지층구성, 지층분류, 지층경계,
토사, 점토, 실트, 모래, 자갈, 호박돌, 전석,
풍화토, 풍화암, 연암, 보통암, 경암, 극경암,
지하수, 지하수위, 수위측정, 투수시험, 현장투수시험,
토질, 토질시험, 토질역학, 토질조사,
샘플링, 불교란시료, 교란시료, 코어, 코어채취,
지반, 지반정보, 지반특성, 지반강도, 지반침하,
기초, 기초설계, 말뚝기초, 직접기초, 깊은기초,
측량, GPS측량, GNSS, RTK, 토탈스테이션, 레벨측량,
수치지형도, 정사영상, 수치표고모델, DEM, DSM, DTM,
좌표, 좌표계, TM좌표, UTM좌표, 경위도, 지오이드,
드론, UAV, 무인항공기, 항공측량, 드론측량,
DJI, Terra, Pix4D, 포인트클라우드, 정밀도,
성과품, 성과도, 보고서, 조사보고서, 지반조사보고서,
위치도, 평면도, 종단면도, 횡단면도, 단면도,
현장, 현장조사, 현장시험, 현장작업, 현장지시,
IoT센서, 센서, 자동입력, 실시간, 웹기반,
토공, 절토, 성토, 물량산출, 체적계산,
도로, 도로노선, 노선측량, 중심선, 기준점,
신규, 기존, 추가, 수정, 검토, 승인,
주상도, 시추주상도, 토질주상도, 지층주상도,
신규, 신규의, 기존의,
지반조사, 지반조사업무

View File

@@ -0,0 +1,27 @@
시추조사, 지반조사, 지질조사, 보링조사, 현장조사,
시추공, 시추공별, 시추계획, 시추위치, 시추심도, 시추기,
주상도, 지층주상도, 시추주상도, 토질주상도,
표준관입시험, SPT, N값, 관입시험, 현장시험,
지층, 지층정보, 지층구성, 지층분류, 지층경계,
토사, 점토, 실트, 모래, 자갈, 호박돌, 전석,
풍화토, 풍화암, 연암, 보통암, 경암, 극경암,
지하수, 지하수위, 수위측정, 투수시험, 현장투수시험,
토질, 토질시험, 토질역학, 토질조사,
샘플링, 불교란시료, 교란시료, 코어, 코어채취,
지반, 지반정보, 지반특성, 지반강도, 지반침하,
기초, 기초설계, 말뚝기초, 직접기초, 깊은기초,
측량, GPS측량, GNSS, RTK, 토탈스테이션, 레벨측량,
수치지형도, 정사영상, 수치표고모델, DEM, DSM, DTM,
좌표, 좌표계, TM좌표, UTM좌표, 경위도, 지오이드,
드론, UAV, 무인항공기, 항공측량, 드론측량,
DJI, Terra, Pix4D, 포인트클라우드, 정밀도,
성과품, 성과도, 보고서, 조사보고서, 지반조사보고서,
위치도, 평면도, 종단면도, 횡단면도, 단면도,
현장, 현장조사, 현장시험, 현장작업, 현장지시,
IoT센서, 센서, 자동입력, 실시간, 웹기반,
토공, 절토, 성토, 물량산출, 체적계산,
도로, 도로노선, 노선측량, 중심선, 기준점,
신규, 기존, 추가, 수정, 검토, 승인,
주상도, 시추주상도, 토질주상도, 지층주상도,
신규, 신규의, 기존의,
지반조사, 지반조사업무

View File

@@ -0,0 +1,27 @@
시추조사, 지반조사, 지질조사, 보링조사, 현장조사,
시추공, 시추공별, 시추계획, 시추위치, 시추심도, 시추기,
주상도, 지층주상도, 시추주상도, 토질주상도,
표준관입시험, SPT, N값, 관입시험, 현장시험,
지층, 지층정보, 지층구성, 지층분류, 지층경계,
토사, 점토, 실트, 모래, 자갈, 호박돌, 전석,
풍화토, 풍화암, 연암, 보통암, 경암, 극경암,
지하수, 지하수위, 수위측정, 투수시험, 현장투수시험,
토질, 토질시험, 토질역학, 토질조사,
샘플링, 불교란시료, 교란시료, 코어, 코어채취,
지반, 지반정보, 지반특성, 지반강도, 지반침하,
기초, 기초설계, 말뚝기초, 직접기초, 깊은기초,
측량, GPS측량, GNSS, RTK, 토탈스테이션, 레벨측량,
수치지형도, 정사영상, 수치표고모델, DEM, DSM, DTM,
좌표, 좌표계, TM좌표, UTM좌표, 경위도, 지오이드,
드론, UAV, 무인항공기, 항공측량, 드론측량,
DJI, Terra, Pix4D, 포인트클라우드, 정밀도,
성과품, 성과도, 보고서, 조사보고서, 지반조사보고서,
위치도, 평면도, 종단면도, 횡단면도, 단면도,
현장, 현장조사, 현장시험, 현장작업, 현장지시,
IoT센서, 센서, 자동입력, 실시간, 웹기반,
토공, 절토, 성토, 물량산출, 체적계산,
도로, 도로노선, 노선측량, 중심선, 기준점,
신규, 기존, 추가, 수정, 검토, 승인,
주상도, 시추주상도, 토질주상도, 지층주상도,
신규, 신규의, 기존의,
지반조사, 지반조사업무

View File

@@ -0,0 +1,27 @@
시추조사, 지반조사, 지질조사, 보링조사, 현장조사,
시추공, 시추공별, 시추계획, 시추위치, 시추심도, 시추기,
주상도, 지층주상도, 시추주상도, 토질주상도,
표준관입시험, SPT, N값, 관입시험, 현장시험,
지층, 지층정보, 지층구성, 지층분류, 지층경계,
토사, 점토, 실트, 모래, 자갈, 호박돌, 전석,
풍화토, 풍화암, 연암, 보통암, 경암, 극경암,
지하수, 지하수위, 수위측정, 투수시험, 현장투수시험,
토질, 토질시험, 토질역학, 토질조사,
샘플링, 불교란시료, 교란시료, 코어, 코어채취,
지반, 지반정보, 지반특성, 지반강도, 지반침하,
기초, 기초설계, 말뚝기초, 직접기초, 깊은기초,
측량, GPS측량, GNSS, RTK, 토탈스테이션, 레벨측량,
수치지형도, 정사영상, 수치표고모델, DEM, DSM, DTM,
좌표, 좌표계, TM좌표, UTM좌표, 경위도, 지오이드,
드론, UAV, 무인항공기, 항공측량, 드론측량,
DJI, Terra, Pix4D, 포인트클라우드, 정밀도,
성과품, 성과도, 보고서, 조사보고서, 지반조사보고서,
위치도, 평면도, 종단면도, 횡단면도, 단면도,
현장, 현장조사, 현장시험, 현장작업, 현장지시,
IoT센서, 센서, 자동입력, 실시간, 웹기반,
토공, 절토, 성토, 물량산출, 체적계산,
도로, 도로노선, 노선측량, 중심선, 기준점,
신규, 기존, 추가, 수정, 검토, 승인,
주상도, 시추주상도, 토질주상도, 지층주상도,
신규, 신규의, 기존의,
지반조사, 지반조사업무

View File

@@ -0,0 +1,27 @@
시추조사, 지반조사, 지질조사, 보링조사, 현장조사,
시추공, 시추공별, 시추계획, 시추위치, 시추심도, 시추기,
주상도, 지층주상도, 시추주상도, 토질주상도,
표준관입시험, SPT, N값, 관입시험, 현장시험,
지층, 지층정보, 지층구성, 지층분류, 지층경계,
토사, 점토, 실트, 모래, 자갈, 호박돌, 전석,
풍화토, 풍화암, 연암, 보통암, 경암, 극경암,
지하수, 지하수위, 수위측정, 투수시험, 현장투수시험,
토질, 토질시험, 토질역학, 토질조사,
샘플링, 불교란시료, 교란시료, 코어, 코어채취,
지반, 지반정보, 지반특성, 지반강도, 지반침하,
기초, 기초설계, 말뚝기초, 직접기초, 깊은기초,
측량, GPS측량, GNSS, RTK, 토탈스테이션, 레벨측량,
수치지형도, 정사영상, 수치표고모델, DEM, DSM, DTM,
좌표, 좌표계, TM좌표, UTM좌표, 경위도, 지오이드,
드론, UAV, 무인항공기, 항공측량, 드론측량,
DJI, Terra, Pix4D, 포인트클라우드, 정밀도,
성과품, 성과도, 보고서, 조사보고서, 지반조사보고서,
위치도, 평면도, 종단면도, 횡단면도, 단면도,
현장, 현장조사, 현장시험, 현장작업, 현장지시,
IoT센서, 센서, 자동입력, 실시간, 웹기반,
토공, 절토, 성토, 물량산출, 체적계산,
도로, 도로노선, 노선측량, 중심선, 기준점,
신규, 기존, 추가, 수정, 검토, 승인,
주상도, 시추주상도, 토질주상도, 지층주상도,
신규, 신규의, 기존의,
지반조사, 지반조사업무

View File

@@ -0,0 +1,27 @@
시추조사, 지반조사, 지질조사, 보링조사, 현장조사,
시추공, 시추공별, 시추계획, 시추위치, 시추심도, 시추기,
주상도, 지층주상도, 시추주상도, 토질주상도,
표준관입시험, SPT, N값, 관입시험, 현장시험,
지층, 지층정보, 지층구성, 지층분류, 지층경계,
토사, 점토, 실트, 모래, 자갈, 호박돌, 전석,
풍화토, 풍화암, 연암, 보통암, 경암, 극경암,
지하수, 지하수위, 수위측정, 투수시험, 현장투수시험,
토질, 토질시험, 토질역학, 토질조사,
샘플링, 불교란시료, 교란시료, 코어, 코어채취,
지반, 지반정보, 지반특성, 지반강도, 지반침하,
기초, 기초설계, 말뚝기초, 직접기초, 깊은기초,
측량, GPS측량, GNSS, RTK, 토탈스테이션, 레벨측량,
수치지형도, 정사영상, 수치표고모델, DEM, DSM, DTM,
좌표, 좌표계, TM좌표, UTM좌표, 경위도, 지오이드,
드론, UAV, 무인항공기, 항공측량, 드론측량,
DJI, Terra, Pix4D, 포인트클라우드, 정밀도,
성과품, 성과도, 보고서, 조사보고서, 지반조사보고서,
위치도, 평면도, 종단면도, 횡단면도, 단면도,
현장, 현장조사, 현장시험, 현장작업, 현장지시,
IoT센서, 센서, 자동입력, 실시간, 웹기반,
토공, 절토, 성토, 물량산출, 체적계산,
도로, 도로노선, 노선측량, 중심선, 기준점,
신규, 기존, 추가, 수정, 검토, 승인,
주상도, 시추주상도, 토질주상도, 지층주상도,
신규, 신규의, 기존의,
지반조사, 지반조사업무

View File

@@ -0,0 +1,27 @@
시추조사, 지반조사, 지질조사, 보링조사, 현장조사,
시추공, 시추공별, 시추계획, 시추위치, 시추심도, 시추기,
주상도, 지층주상도, 시추주상도, 토질주상도,
표준관입시험, SPT, N값, 관입시험, 현장시험,
지층, 지층정보, 지층구성, 지층분류, 지층경계,
토사, 점토, 실트, 모래, 자갈, 호박돌, 전석,
풍화토, 풍화암, 연암, 보통암, 경암, 극경암,
지하수, 지하수위, 수위측정, 투수시험, 현장투수시험,
토질, 토질시험, 토질역학, 토질조사,
샘플링, 불교란시료, 교란시료, 코어, 코어채취,
지반, 지반정보, 지반특성, 지반강도, 지반침하,
기초, 기초설계, 말뚝기초, 직접기초, 깊은기초,
측량, GPS측량, GNSS, RTK, 토탈스테이션, 레벨측량,
수치지형도, 정사영상, 수치표고모델, DEM, DSM, DTM,
좌표, 좌표계, TM좌표, UTM좌표, 경위도, 지오이드,
드론, UAV, 무인항공기, 항공측량, 드론측량,
DJI, Terra, Pix4D, 포인트클라우드, 정밀도,
성과품, 성과도, 보고서, 조사보고서, 지반조사보고서,
위치도, 평면도, 종단면도, 횡단면도, 단면도,
현장, 현장조사, 현장시험, 현장작업, 현장지시,
IoT센서, 센서, 자동입력, 실시간, 웹기반,
토공, 절토, 성토, 물량산출, 체적계산,
도로, 도로노선, 노선측량, 중심선, 기준점,
신규, 기존, 추가, 수정, 검토, 승인,
주상도, 시추주상도, 토질주상도, 지층주상도,
신규, 신규의, 기존의,
지반조사, 지반조사업무

View File

@@ -0,0 +1,27 @@
시추조사, 지반조사, 지질조사, 보링조사, 현장조사,
시추공, 시추공별, 시추계획, 시추위치, 시추심도, 시추기,
주상도, 지층주상도, 시추주상도, 토질주상도,
표준관입시험, SPT, N값, 관입시험, 현장시험,
지층, 지층정보, 지층구성, 지층분류, 지층경계,
토사, 점토, 실트, 모래, 자갈, 호박돌, 전석,
풍화토, 풍화암, 연암, 보통암, 경암, 극경암,
지하수, 지하수위, 수위측정, 투수시험, 현장투수시험,
토질, 토질시험, 토질역학, 토질조사,
샘플링, 불교란시료, 교란시료, 코어, 코어채취,
지반, 지반정보, 지반특성, 지반강도, 지반침하,
기초, 기초설계, 말뚝기초, 직접기초, 깊은기초,
측량, GPS측량, GNSS, RTK, 토탈스테이션, 레벨측량,
수치지형도, 정사영상, 수치표고모델, DEM, DSM, DTM,
좌표, 좌표계, TM좌표, UTM좌표, 경위도, 지오이드,
드론, UAV, 무인항공기, 항공측량, 드론측량,
DJI, Terra, Pix4D, 포인트클라우드, 정밀도,
성과품, 성과도, 보고서, 조사보고서, 지반조사보고서,
위치도, 평면도, 종단면도, 횡단면도, 단면도,
현장, 현장조사, 현장시험, 현장작업, 현장지시,
IoT센서, 센서, 자동입력, 실시간, 웹기반,
토공, 절토, 성토, 물량산출, 체적계산,
도로, 도로노선, 노선측량, 중심선, 기준점,
신규, 기존, 추가, 수정, 검토, 승인,
주상도, 시추주상도, 토질주상도, 지층주상도,
신규, 신규의, 기존의,
지반조사, 지반조사업무

View File

@@ -0,0 +1,27 @@
시추조사, 지반조사, 지질조사, 보링조사, 현장조사,
시추공, 시추공별, 시추계획, 시추위치, 시추심도, 시추기,
주상도, 지층주상도, 시추주상도, 토질주상도,
표준관입시험, SPT, N값, 관입시험, 현장시험,
지층, 지층정보, 지층구성, 지층분류, 지층경계,
토사, 점토, 실트, 모래, 자갈, 호박돌, 전석,
풍화토, 풍화암, 연암, 보통암, 경암, 극경암,
지하수, 지하수위, 수위측정, 투수시험, 현장투수시험,
토질, 토질시험, 토질역학, 토질조사,
샘플링, 불교란시료, 교란시료, 코어, 코어채취,
지반, 지반정보, 지반특성, 지반강도, 지반침하,
기초, 기초설계, 말뚝기초, 직접기초, 깊은기초,
측량, GPS측량, GNSS, RTK, 토탈스테이션, 레벨측량,
수치지형도, 정사영상, 수치표고모델, DEM, DSM, DTM,
좌표, 좌표계, TM좌표, UTM좌표, 경위도, 지오이드,
드론, UAV, 무인항공기, 항공측량, 드론측량,
DJI, Terra, Pix4D, 포인트클라우드, 정밀도,
성과품, 성과도, 보고서, 조사보고서, 지반조사보고서,
위치도, 평면도, 종단면도, 횡단면도, 단면도,
현장, 현장조사, 현장시험, 현장작업, 현장지시,
IoT센서, 센서, 자동입력, 실시간, 웹기반,
토공, 절토, 성토, 물량산출, 체적계산,
도로, 도로노선, 노선측량, 중심선, 기준점,
신규, 기존, 추가, 수정, 검토, 승인,
주상도, 시추주상도, 토질주상도, 지층주상도,
신규, 신규의, 기존의,
지반조사, 지반조사업무

View File

@@ -0,0 +1,769 @@
# HWP/HWPX ↔ HTML/CSS 도메인 가이드
> **목적**: HWPX에서 문서 유형·스타일·템플릿을 추출하거나, HTML → HWPX → HWP 변환 시
> 하드코딩 없이 이 가이드를 참조하여 정확한 매핑을 수행한다.
> **출처**: 한글과컴퓨터 공식 "글 문서 파일 구조 5.0" (revision 1.3, 2018-11-08)
> **범위**: HWP 5.0 바이너리 스펙의 개념 체계 + HWPX XML 태그 + HTML/CSS 매핑
---
## 0. 문서 형식 관계
```
HWP (바이너리) HWPX (XML) HTML/CSS
───────────────── ───────────────────── ─────────────────
Compound File ZIP Archive 단일 HTML 파일
├─ FileHeader ├─ META-INF/ ├─ <head>
├─ DocInfo │ └─ manifest.xml │ ├─ <meta>
│ (글꼴, 스타일, ├─ Contents/ │ └─ <style>
│ 테두리/배경, │ ├─ header.xml └─ <body>
│ 글자모양 등) │ │ (DocInfo 대응) ├─ 헤더 영역
├─ BodyText/ │ ├─ section0.xml │ ├─ 본문
│ └─ Section0 │ │ (본문 대응) │ └─ 푸터 영역
├─ BinData/ │ └─ section1.xml └─ @page CSS
│ └─ 이미지 등 ├─ BinData/
└─ PrvImage │ └─ 이미지 파일
└─ version.xml
```
**핵심**: HWP 바이너리의 레코드 구조와 HWPX XML의 엘리먼트는 1:1 대응한다.
이 가이드는 두 형식의 공통 개념 체계를 기준으로, CSS 변환까지 연결한다.
---
## 1. 단위 체계
### 1.1 HWPUNIT (글 내부 단위)
HWP는 1/7200 인치를 기본 단위로 사용한다.
| 변환 대상 | 공식 | 예시 |
|-----------|------|------|
| HWPUNIT → mm | `hwpunit / 7200 * 25.4` | 7200 → 25.4mm (= 1인치) |
| HWPUNIT → pt | `hwpunit / 7200 * 72` | 7200 → 72pt |
| HWPUNIT → px (96dpi) | `hwpunit / 7200 * 96` | 7200 → 96px |
| mm → HWPUNIT | `mm / 25.4 * 7200` | 25.4mm → 7200 |
| pt → HWPUNIT | `pt / 72 * 7200` | 10pt → 1000 |
```python
def hwpunit_to_mm(hwpunit): return hwpunit / 7200 * 25.4
def hwpunit_to_pt(hwpunit): return hwpunit / 7200 * 72
def hwpunit_to_px(hwpunit): return hwpunit / 7200 * 96
def mm_to_hwpunit(mm): return mm / 25.4 * 7200
```
### 1.2 글자 크기 (CharShape)
HWP의 글자 크기는 HWPUNIT 단위이지만 100배 스케일이 적용되어 있다.
| HWP 값 | 실제 크기 | CSS |
|--------|----------|-----|
| 1000 | 10pt | `font-size: 10pt` |
| 1200 | 12pt | `font-size: 12pt` |
| 2400 | 24pt | `font-size: 24pt` |
```python
def charsize_to_pt(hwp_size): return hwp_size / 100 # 1000 → 10pt
```
### 1.3 COLORREF (색상)
HWP는 0x00BBGGRR 형식(리틀 엔디안 BGR). CSS는 #RRGGBB.
| HWP COLORREF | 분해 | CSS |
|-------------|------|-----|
| 0x00000000 | R=0, G=0, B=0 | `#000000` (검정) |
| 0x00FF0000 | R=0, G=0, B=255 | `#0000ff` (파랑) |
| 0x0000FF00 | R=0, G=255, B=0 | `#00ff00` (초록) |
| 0x000000FF | R=255, G=0, B=0 | `#ff0000` (빨강) |
| 0x00FFFFFF | R=255, G=255, B=255 | `#ffffff` (흰색) |
```python
def colorref_to_css(colorref):
r = colorref & 0xFF
g = (colorref >> 8) & 0xFF
b = (colorref >> 16) & 0xFF
return f'#{r:02x}{g:02x}{b:02x}'
```
**HWPX XML에서의 색상**: `#RRGGBB` 형식으로 직접 기록됨 (변환 불필요).
---
## 2. 테두리/배경 (BorderFill)
> HWP: `HWPTAG_BORDER_FILL` (DocInfo 레코드)
> HWPX: `<hh:borderFill>` (header.xml 내)
> 용도: 표 셀, 문단, 쪽 테두리/배경에 공통 적용
### 2.1 테두리선 종류
| HWP 값 | 이름 | HWPX type 속성 | CSS border-style |
|--------|------|---------------|-----------------|
| 0 | 실선 | `SOLID` | `solid` |
| 1 | 긴 점선 | `DASH` | `dashed` |
| 2 | 점선 | `DOT` | `dotted` |
| 3 | -.-.-. | `DASH_DOT` | `dashed` (근사) |
| 4 | -..-.. | `DASH_DOT_DOT` | `dashed` (근사) |
| 5 | 긴 Dash | `LONG_DASH` | `dashed` |
| 6 | 큰 동그라미 | `CIRCLE` | `dotted` (근사) |
| 7 | 2중선 | `DOUBLE` | `double` |
| 8 | 가는선+굵은선 | `THIN_THICK` | `double` (근사) |
| 9 | 굵은선+가는선 | `THICK_THIN` | `double` (근사) |
| 10 | 가는+굵은+가는 | `THIN_THICK_THIN` | `double` (근사) |
| 11 | 물결 | `WAVE` | `solid` (근사) |
| 12 | 물결 2중선 | `DOUBLE_WAVE` | `double` (근사) |
| 13 | 두꺼운 3D | `THICK_3D` | `ridge` |
| 14 | 두꺼운 3D(역) | `THICK_3D_REV` | `groove` |
| 15 | 3D 단선 | `3D` | `outset` |
| 16 | 3D 단선(역) | `3D_REV` | `inset` |
| — | 없음 | `NONE` | `none` |
### 2.2 테두리선 굵기
| HWP 값 | 실제 굵기 | HWPX width 속성 | CSS border-width |
|--------|----------|----------------|-----------------|
| 0 | 0.1 mm | `0.1mm` | `0.1mm``0.4px` |
| 1 | 0.12 mm | `0.12mm` | `0.12mm``0.5px` |
| 2 | 0.15 mm | `0.15mm` | `0.15mm``0.6px` |
| 3 | 0.2 mm | `0.2mm` | `0.2mm``0.8px` |
| 4 | 0.25 mm | `0.25mm` | `0.25mm``1px` |
| 5 | 0.3 mm | `0.3mm` | `0.3mm``1.1px` |
| 6 | 0.4 mm | `0.4mm` | `0.4mm``1.5px` |
| 7 | 0.5 mm | `0.5mm` | `0.5mm``1.9px` |
| 8 | 0.6 mm | `0.6mm` | `0.6mm``2.3px` |
| 9 | 0.7 mm | `0.7mm` | `0.7mm``2.6px` |
| 10 | 1.0 mm | `1.0mm` | `1mm``3.8px` |
| 11 | 1.5 mm | `1.5mm` | `1.5mm``5.7px` |
| 12 | 2.0 mm | `2.0mm` | `2mm``7.6px` |
| 13 | 3.0 mm | `3.0mm` | `3mm``11.3px` |
| 14 | 4.0 mm | `4.0mm` | `4mm``15.1px` |
| 15 | 5.0 mm | `5.0mm` | `5mm``18.9px` |
```python
BORDER_WIDTH_MAP = {
0: 0.1, 1: 0.12, 2: 0.15, 3: 0.2, 4: 0.25, 5: 0.3,
6: 0.4, 7: 0.5, 8: 0.6, 9: 0.7, 10: 1.0, 11: 1.5,
12: 2.0, 13: 3.0, 14: 4.0, 15: 5.0
}
def border_width_to_css(hwp_val):
mm = BORDER_WIDTH_MAP.get(hwp_val, 0.12)
return f'{mm}mm' # 또는 mm * 3.7795px
```
### 2.3 테두리 4방향 순서
| HWP 배열 인덱스 | HWPX 속성 | CSS 대응 |
|:---:|:---:|:---:|
| [0] | `<left>` / `<hh:left>` | `border-left` |
| [1] | `<right>` / `<hh:right>` | `border-right` |
| [2] | `<top>` / `<hh:top>` | `border-top` |
| [3] | `<bottom>` / `<hh:bottom>` | `border-bottom` |
### 2.4 채우기 (Fill) 정보
| 채우기 종류 (type 비트) | HWPX 엘리먼트 | CSS 대응 |
|:---:|:---:|:---:|
| 0x00 — 없음 | (없음) | `background: none` |
| 0x01 — 단색 | `<hh:windowBrush>` 또는 `<hh:colorFill>` | `background-color: #...` |
| 0x02 — 이미지 | `<hh:imgBrush>` | `background-image: url(...)` |
| 0x04 — 그러데이션 | `<hh:gradation>` | `background: linear-gradient(...)` |
**단색 채우기 구조** (가장 빈번):
```xml
<!-- HWPX header.xml -->
<hh:borderFill id="4">
<hh:slash .../>
<hh:backSlash .../>
<hh:left type="SOLID" width="0.12mm" color="#000000"/>
<hh:right type="SOLID" width="0.12mm" color="#000000"/>
<hh:top type="SOLID" width="0.12mm" color="#000000"/>
<hh:bottom type="SOLID" width="0.12mm" color="#000000"/>
<hh:diagonal .../>
<hh:fillBrush>
<hh:windowBrush faceColor="#E8F5E9" hatchColor="none" .../>
</hh:fillBrush>
</hh:borderFill>
```
```css
/* CSS 대응 */
.cell-bf4 {
border-left: 0.12mm solid #000000;
border-right: 0.12mm solid #000000;
border-top: 0.12mm solid #000000;
border-bottom: 0.12mm solid #000000;
background-color: #E8F5E9;
}
```
### 2.5 HWPX borderFill → CSS 변환 함수 (의사 코드)
```python
def borderfill_to_css(bf_element):
"""HWPX <hh:borderFill> 엘리먼트 → CSS 딕셔너리"""
css = {}
for side in ['left', 'right', 'top', 'bottom']:
el = bf_element.find(f'hh:{side}')
if el is None:
css[f'border-{side}'] = 'none'
continue
btype = el.get('type', 'NONE')
width = el.get('width', '0.12mm')
color = el.get('color', '#000000')
if btype == 'NONE':
css[f'border-{side}'] = 'none'
else:
css_style = BORDER_TYPE_MAP.get(btype, 'solid')
css[f'border-{side}'] = f'{width} {css_style} {color}'
# 배경
fill = bf_element.find('.//hh:windowBrush')
if fill is not None:
face = fill.get('faceColor', 'none')
if face and face != 'none':
css['background-color'] = face
return css
```
---
## 3. 글꼴 (FaceName)
> HWP: `HWPTAG_FACE_NAME` (DocInfo)
> HWPX: `<hh:fontface>` → `<hh:font>` (header.xml)
> CSS: `font-family`
### 3.1 언어별 글꼴 시스템
HWP는 한글·영문·한자·일어·기타·기호·사용자 총 7개 언어 슬롯에 각각 다른 글꼴을 지정한다.
| 언어 인덱스 | HWPX lang 속성 | 주요 글꼴 예시 |
|:---:|:---:|:---:|
| 0 | `HANGUL` | 맑은 고딕, 나눔고딕 |
| 1 | `LATIN` | Arial, Times New Roman |
| 2 | `HANJA` | (한글 글꼴 공유) |
| 3 | `JAPANESE` | MS Mincho |
| 4 | `OTHER` | — |
| 5 | `SYMBOL` | Symbol, Wingdings |
| 6 | `USER` | — |
**CSS 매핑**: 일반적으로 한글(0)과 영문(1) 글꼴을 `font-family` 스택으로 결합.
```css
/* HWPX: hangul="맑은 고딕" latin="Arial" */
font-family: "맑은 고딕", Arial, sans-serif;
```
### 3.2 글꼴 관련 HWPX 구조
```xml
<!-- header.xml -->
<hh:fontfaces>
<hh:fontface lang="HANGUL">
<hh:font face="맑은 고딕" type="TTF" id="0"/>
</hh:fontface>
<hh:fontface lang="LATIN">
<hh:font face="Arial" type="TTF" id="0"/>
</hh:fontface>
...
</hh:fontfaces>
```
---
## 4. 글자 모양 (CharShape)
> HWP: `HWPTAG_CHAR_SHAPE` (DocInfo, 72바이트)
> HWPX: `<hh:charPr>` (header.xml charProperties 내)
> CSS: font-*, color, text-decoration 등
### 4.1 주요 속성 매핑
| HWP 필드 | HWPX 속성 | CSS 속성 | 비고 |
|----------|----------|---------|------|
| 글꼴 ID [7] | `fontRef` | `font-family` | 언어별 참조 |
| 장평 [7] | `ratio` | `font-stretch` | 50%~200% |
| 자간 [7] | `spacing` | `letter-spacing` | -50%~50%, pt 변환 필요 |
| 기준 크기 | `height` | `font-size` | 값/100 = pt |
| 글자 색 | `color` 속성 | `color` | COLORREF → #RRGGBB |
| 밑줄 색 | `underline color` | `text-decoration-color` | |
| 진하게(bit 1) | `bold="true"` | `font-weight: bold` | |
| 기울임(bit 0) | `italic="true"` | `font-style: italic` | |
| 밑줄(bit 2-3) | `underline type` | `text-decoration: underline` | |
| 취소선(bit 18-20) | `strikeout type` | `text-decoration: line-through` | |
| 위첨자(bit 15) | `supscript` | `vertical-align: super; font-size: 70%` | |
| 아래첨자(bit 16) | `subscript` | `vertical-align: sub; font-size: 70%` | |
### 4.2 HWPX charPr 구조 예시
```xml
<hh:charPr id="1" height="1000" bold="false" italic="false"
underline="NONE" strikeout="NONE" color="#000000">
<hh:fontRef hangul="0" latin="0" hanja="0" japanese="0"
other="0" symbol="0" user="0"/>
<hh:ratio hangul="100" latin="100" .../>
<hh:spacing hangul="0" latin="0" .../>
<hh:relSz hangul="100" latin="100" .../>
<hh:offset hangul="0" latin="0" .../>
</hh:charPr>
```
---
## 5. 문단 모양 (ParaShape)
> HWP: `HWPTAG_PARA_SHAPE` (DocInfo, 54바이트)
> HWPX: `<hh:paraPr>` (header.xml paraProperties 내)
> CSS: text-align, margin, line-height, text-indent 등
### 5.1 정렬 방식
| HWP 값 (bit 2-4) | HWPX 속성값 | CSS text-align |
|:---:|:---:|:---:|
| 0 | `JUSTIFY` | `justify` |
| 1 | `LEFT` | `left` |
| 2 | `RIGHT` | `right` |
| 3 | `CENTER` | `center` |
| 4 | `DISTRIBUTE` | `justify` (근사) |
| 5 | `DISTRIBUTE_SPACE` | `justify` (근사) |
### 5.2 줄 간격 종류
| HWP 값 | HWPX 속성값 | CSS line-height | 비고 |
|:---:|:---:|:---:|:---:|
| 0 | `PERCENT` | `160%` (예) | 글자 크기 기준 % |
| 1 | `FIXED` | `24pt` (예) | 고정 pt |
| 2 | `BETWEEN_LINES` | — | 여백만 지정 |
| 3 | `AT_LEAST` | — | 최소값 |
### 5.3 주요 속성 매핑
| HWP 필드 | HWPX 속성 | CSS 속성 | 단위 |
|----------|----------|---------|------|
| 왼쪽 여백 | `margin left` | `margin-left` / `padding-left` | HWPUNIT → mm |
| 오른쪽 여백 | `margin right` | `margin-right` / `padding-right` | HWPUNIT → mm |
| 들여쓰기 | `indent` | `text-indent` | HWPUNIT → mm |
| 문단 간격 위 | `spacing before` | `margin-top` | HWPUNIT → mm |
| 문단 간격 아래 | `spacing after` | `margin-bottom` | HWPUNIT → mm |
| 줄 간격 | `lineSpacing` | `line-height` | 종류에 따라 다름 |
| BorderFill ID | `borderFillIDRef` | border + background | ID로 참조 |
### 5.4 HWPX paraPr 구조 예시
```xml
<hh:paraPr id="0" align="JUSTIFY">
<hh:margin left="0" right="0" indent="0"/>
<hh:spacing before="0" after="0"
lineSpacingType="PERCENT" lineSpacing="160"/>
<hh:border borderFillIDRef="1"
left="0" right="0" top="0" bottom="0"/>
<hh:autoSpacing eAsianEng="false" eAsianNum="false"/>
</hh:paraPr>
```
---
## 6. 표 (Table) 구조
> HWP: `HWPTAG_TABLE` (본문 레코드)
> HWPX: `<hp:tbl>` (section*.xml 내)
> HTML: `<table>`, `<tr>`, `<td>`/`<th>`
### 6.1 표 속성 매핑
| HWP 필드 | HWPX 속성 | HTML/CSS 대응 | 비고 |
|----------|----------|-------------|------|
| RowCount | `rowCnt` | (행 수) | |
| nCols | `colCnt` | (열 수) | `<colgroup>` 참조 |
| CellSpacing | `cellSpacing` | `border-spacing` | HWPUNIT16 |
| 안쪽 여백 | `cellMargin` left/right/top/bottom | `padding` | |
| BorderFill ID | `borderFillIDRef` | 표 전체 테두리 | |
| 쪽나눔(bit 0-1) | `pageBreak` | `page-break-inside` | 0=avoid, 1=auto |
| 제목줄 반복(bit 2) | `repeatHeader` | `<thead>` 출력 | |
### 6.2 열 너비
```xml
<!-- HWPX -->
<hp:tbl colCnt="3" rowCnt="5" ...>
<hp:colSz>
<hp:widthList>8504 8504 8504</hp:widthList> <!-- HWPUNIT -->
</hp:colSz>
...
</hp:tbl>
```
```html
<!-- HTML 변환 -->
<colgroup>
<col style="width: 33.33%"> <!-- 8504 / 총합 * 100 -->
<col style="width: 33.33%">
<col style="width: 33.33%">
</colgroup>
```
### 6.3 셀 (Cell) 속성
| HWP 필드 | HWPX 속성 | HTML 속성 | 비고 |
|----------|----------|----------|------|
| Column 주소 | `colAddr` | — | 0부터 시작 |
| Row 주소 | `rowAddr` | — | 0부터 시작 |
| 열 병합 개수 | `colSpan` | `colspan` | 1 = 병합 없음 |
| 행 병합 개수 | `rowSpan` | `rowspan` | 1 = 병합 없음 |
| 셀 폭 | `width` | `width` | HWPUNIT |
| 셀 높이 | `height` | `height` | HWPUNIT |
| 셀 여백 [4] | `cellMargin` | `padding` | HWPUNIT16 → mm |
| BorderFill ID | `borderFillIDRef` | `border` + `background` | 셀별 스타일 |
### 6.4 HWPX 셀 구조 예시
```xml
<hp:tc colAddr="0" rowAddr="0" colSpan="2" rowSpan="1"
width="17008" height="2400" borderFillIDRef="4">
<hp:cellMargin left="510" right="510" top="142" bottom="142"/>
<hp:cellAddr colAddr="0" rowAddr="0"/>
<hp:subList ...>
<hp:p ...>
<!-- 셀 내용 -->
</hp:p>
</hp:subList>
</hp:tc>
```
```html
<!-- HTML 변환 -->
<td colspan="2" style="
width: 60mm;
height: 8.5mm;
padding: 0.5mm 1.8mm;
border: 0.12mm solid #000;
background-color: #E8F5E9;
">셀 내용</td>
```
### 6.5 병합 셀 처리 규칙
HWP/HWPX에서 병합된 셀은 **왼쪽 위 셀만 존재**하고, 병합에 포함된 다른 셀은 아예 없다.
HTML에서는 colspan/rowspan으로 표현하고, 병합된 위치의 `<td>`를 생략한다.
```
HWPX: colSpan="2", rowSpan="3" at (col=0, row=0)
→ 이 셀이 col 0~1, row 0~2를 차지
→ col=1/row=0, col=0/row=1, col=1/row=1, col=0/row=2, col=1/row=2 셀은 없음
HTML: <td colspan="2" rowspan="3">...</td>
→ 해당 행/열 위치에서 <td> 생략
```
---
## 7. 용지 설정 (PageDef / SecPr)
> HWP: `HWPTAG_PAGE_DEF` (구역 정의 하위)
> HWPX: `<hp:secPr>` → `<hp:pageDef>` (section*.xml 내)
> CSS: `@page`, `@media print`
### 7.1 용지 크기 사전 정의
| 용지 이름 | 가로 (mm) | 세로 (mm) | HWPUNIT (가로×세로) |
|----------|----------|----------|:---:|
| A4 | 210 | 297 | 59528 × 84188 |
| A3 | 297 | 420 | 84188 × 119055 |
| B5 | 176 | 250 | 49896 × 70866 |
| Letter | 215.9 | 279.4 | 61200 × 79200 |
| Legal | 215.9 | 355.6 | 61200 × 100800 |
### 7.2 여백 매핑
```xml
<!-- HWPX section0.xml -->
<hp:secPr>
<hp:pageDef width="59528" height="84188"
landscape="NARROWLY"> <!-- 좁게 = 세로 -->
<hp:margin left="8504" right="8504"
top="5668" bottom="4252"
header="4252" footer="4252"
gutter="0"/>
</hp:pageDef>
</hp:secPr>
```
```css
/* CSS 변환 */
@page {
size: A4 portrait; /* 210mm × 297mm */
margin-top: 20mm; /* 5668 / 7200 * 25.4 ≈ 20mm */
margin-bottom: 15mm; /* 4252 → 15mm */
margin-left: 30mm; /* 8504 → 30mm */
margin-right: 30mm; /* 8504 → 30mm */
}
/* 머리말/꼬리말 여백은 CSS에서 body padding으로 근사 */
```
### 7.3 용지 방향
| HWP 값 (bit 0) | HWPX 속성값 | CSS |
|:---:|:---:|:---:|
| 0 | `NARROWLY` (좁게) | `portrait` |
| 1 | `WIDELY` (넓게) | `landscape` |
---
## 8. 머리말/꼬리말 (Header/Footer)
> HWP: `HWPTAG_CTRL_HEADER` → 컨트롤 ID `head` / `foot`
> HWPX: `<hp:headerFooter>` (section*.xml 내, 또는 별도 header/footer 영역)
> HTML: 페이지 상단/하단 고정 영역
### 8.1 머리말/꼬리말 적용 범위
| HWP/HWPX 설정 | 의미 |
|:---:|:---:|
| 양쪽 | 모든 쪽에 동일 |
| 짝수쪽만 | 짝수 페이지 |
| 홀수쪽만 | 홀수 페이지 |
### 8.2 HTML 근사 표현
```html
<!-- 머리말 -->
<div class="page-header" style="
position: absolute; top: 0; left: 0; right: 0;
height: 15mm; /* header margin 값 */
padding: 0 30mm; /* 좌우 본문 여백 */
">
<table class="header-table">...</table>
</div>
<!-- 꼬리말 -->
<div class="page-footer" style="
position: absolute; bottom: 0; left: 0; right: 0;
height: 15mm; /* footer margin 값 */
padding: 0 30mm;
">
<span class="footer-text">페이지 번호</span>
</div>
```
---
## 9. 구역 정의 (Section)
> HWP: 구역 정의 컨트롤 (`secd`)
> HWPX: `<hp:secPr>` (section*.xml 최상위)
### 9.1 구역 속성
| 속성 | HWPX | CSS/HTML 대응 | 비고 |
|------|------|-------------|------|
| 머리말 감춤 | `hideHeader` | header 영역 display:none | |
| 꼬리말 감춤 | `hideFooter` | footer 영역 display:none | |
| 텍스트 방향 | `textDirection` | `writing-mode` | 0=가로, 1=세로 |
| 단 정의 | `<hp:colDef>` | CSS `columns` / `column-count` | |
| 쪽 번호 | `pageStartNo` | 쪽 번호 출력 값 | 0=이어서 |
---
## 10. HTML → HWPX → HWP 변환 파이프라인
### 10.1 전체 흐름
```
[HTML (우리 출력)]
↓ (1) HTML 파싱 → CSS 속성 추출
[중간 표현 (JSON)]
↓ (2) 이 가이드의 역방향 매핑
[HWPX (XML ZIP)]
↓ (3) 한컴오피스 변환 도구
[HWP (바이너리)]
```
### 10.2 단계별 매핑 방향
| 단계 | 입력 | 출력 | 참조할 가이드 섹션 |
|:---:|:---:|:---:|:---:|
| HTML → HWPX | CSS border | `<hh:borderFill>` 생성 | §2 역방향 |
| HTML → HWPX | CSS font | `<hh:charPr>` + `<hh:fontface>` | §3, §4 역방향 |
| HTML → HWPX | CSS text-align 등 | `<hh:paraPr>` | §5 역방향 |
| HTML → HWPX | `<table>` | `<hp:tbl>` + `<hp:tc>` | §6 역방향 |
| HTML → HWPX | @page CSS | `<hp:pageDef>` | §7 역방향 |
| HTML → HWPX | header/footer div | `<hp:headerFooter>` | §8 역방향 |
### 10.3 CSS → HWPX 역변환 예시
```python
def css_border_to_hwpx(css_border):
"""'0.12mm solid #000000' → HWPX 속성"""
parts = css_border.split()
width = parts[0] # '0.12mm'
style = parts[1] # 'solid'
color = parts[2] # '#000000'
hwpx_type = CSS_TO_BORDER_TYPE.get(style, 'SOLID')
return {
'type': hwpx_type,
'width': width,
'color': color
}
CSS_TO_BORDER_TYPE = {
'solid': 'SOLID', 'dashed': 'DASH', 'dotted': 'DOT',
'double': 'DOUBLE', 'ridge': 'THICK_3D', 'groove': 'THICK_3D_REV',
'outset': '3D', 'inset': '3D_REV', 'none': 'NONE'
}
```
### 10.4 HWPX ZIP 구조 생성
```
output.hwpx (ZIP)
├── META-INF/
│ └── manifest.xml ← 파일 목록
├── Contents/
│ ├── header.xml ← DocInfo (글꼴, 스타일, borderFill)
│ ├── section0.xml ← 본문 (문단, 표, 머리말/꼬리말)
│ └── content.hpf ← 콘텐츠 메타
├── BinData/ ← 이미지 등
├── Preview/
│ └── PrvImage.png ← 미리보기
└── version.xml ← 버전 정보
```
**header.xml 필수 구조**:
```xml
<?xml version="1.0" encoding="UTF-8"?>
<hh:head xmlns:hh="...">
<hh:beginNum .../>
<hh:refList>
<hh:fontfaces>...</hh:fontfaces> <!-- §3 -->
<hh:borderFills>...</hh:borderFills> <!-- §2 -->
<hh:charProperties>...</hh:charProperties> <!-- §4 -->
<hh:tabProperties>...</hh:tabProperties>
<hh:numberingProperties>...</hh:numberingProperties>
<hh:bulletProperties>...</hh:bulletProperties>
<hh:paraProperties>...</hh:paraProperties> <!-- §5 -->
<hh:styles>...</hh:styles>
</hh:refList>
</hh:head>
```
**section0.xml 필수 구조**:
```xml
<?xml version="1.0" encoding="UTF-8"?>
<hp:sec xmlns:hp="...">
<hp:secPr>
<hp:pageDef .../> <!-- §7 -->
<hp:headerFooter .../> <!-- §8 -->
</hp:secPr>
<hp:p paraPrIDRef="0" styleIDRef="0"> <!-- 문단 -->
<hp:run charPrIDRef="0">
<hp:t>텍스트</hp:t>
</hp:run>
</hp:p>
<hp:p ...>
<hp:ctrl>
<hp:tbl ...>...</hp:tbl> <!-- §6 -->
</hp:ctrl>
</hp:p>
</hp:sec>
```
---
## 11. 시스템 적용 가이드
### 11.1 적용 대상 모듈
| 모듈 | 파일 | 이 가이드 활용 방식 |
|------|------|:---:|
| **doc_template_analyzer.py** | HWPX → HTML 템플릿 추출 | §2,6,7,8 정방향 (HWPX→CSS) |
| **template_manager.py** | 추출된 스타일 저장/로드 | §2 borderFill ID 매핑 |
| **custom_doc_type.py** | HTML 문서 생성 | §2,4,5 CSS 값 참조 |
| **hwpx_converter.py** (예정) | HTML → HWPX 변환 | §2~8 역방향 (CSS→HWPX) |
| **hwp_converter.py** (예정) | HWPX → HWP 변환 | §1 단위 변환 |
### 11.2 하드코딩 제거 전략
**현재 문제 (AS-IS)**:
```python
# doc_template_analyzer.py에 하드코딩됨
border_css = "2px solid var(--primary)"
header_bg = "#E8F5E9"
```
**해결 방향 (TO-BE)**:
```python
# style.json에서 추출된 borderFill 참조
bf = style['border_fills']['3'] # id=3
border_css = f"{bf['top']['width']} {bf['top']['css_style']} {bf['top']['color']}"
# → "0.12mm solid #000000"
header_bf = style['border_fills']['4'] # id=4 (헤더 배경 포함)
header_bg = header_bf.get('background', 'none')
# → "#E8F5E9"
```
### 11.3 이 가이드를 코드에서 참조하는 방식
이 문서(`hwpx_domain_guide.md`)는 다음과 같이 활용한다:
1. **변환 테이블을 JSON으로 추출**`hwpx_mappings.json`
- 테두리선 종류, 굵기, 색상 변환 등의 룩업 테이블
2. **변환 함수 라이브러리**`hwpx_utils.py`
- `hwpunit_to_mm()`, `borderfill_to_css()`, `css_border_to_hwpx()`
3. **AI 프롬프트 컨텍스트** → 문서 유형/구조 분석 시 참조
- "이 HWPX의 borderFill id=3은 실선 0.12mm 검정이므로 표 일반 셀에 해당"
4. **검증 기준** → 변환 결과물 검증 시 정확성 확인
- 추출된 CSS가 원본 HWPX의 스펙과 일치하는지
---
## 부록 A. 빠른 참조 — HWPX XML 태그 ↔ HWP 레코드 대응
| HWP 레코드 (Tag ID) | HWPX XML 엘리먼트 | 위치 |
|---------------------|------------------|------|
| HWPTAG_DOCUMENT_PROPERTIES | `<hh:beginNum>` 등 | header.xml |
| HWPTAG_ID_MAPPINGS | (암묵적) | header.xml |
| HWPTAG_FACE_NAME | `<hh:font>` | header.xml > fontfaces |
| HWPTAG_BORDER_FILL | `<hh:borderFill>` | header.xml > borderFills |
| HWPTAG_CHAR_SHAPE | `<hh:charPr>` | header.xml > charProperties |
| HWPTAG_TAB_DEF | `<hh:tabPr>` | header.xml > tabProperties |
| HWPTAG_NUMBERING | `<hh:numbering>` | header.xml > numberingProperties |
| HWPTAG_BULLET | `<hh:bullet>` | header.xml > bulletProperties |
| HWPTAG_PARA_SHAPE | `<hh:paraPr>` | header.xml > paraProperties |
| HWPTAG_STYLE | `<hh:style>` | header.xml > styles |
| HWPTAG_PARA_HEADER | `<hp:p>` | section*.xml |
| HWPTAG_TABLE | `<hp:tbl>` | section*.xml > p > ctrl |
| (셀 속성) | `<hp:tc>` | section*.xml > tbl > tr > tc |
| HWPTAG_PAGE_DEF | `<hp:pageDef>` | section*.xml > secPr |
| (머리말/꼬리말) | `<hp:headerFooter>` | section*.xml > secPr |
## 부록 B. 빠른 참조 — CSS → HWPX 역변환
| CSS 속성 | HWPX 대응 | 변환 공식 |
|----------|----------|----------|
| `font-family` | `<hh:font face="...">` | 첫 번째 값 → hangul, 두 번째 → latin |
| `font-size: 10pt` | `<hh:charPr height="1000">` | pt × 100 |
| `font-weight: bold` | `bold="true"` | |
| `font-style: italic` | `italic="true"` | |
| `color: #1a365d` | `color="#1a365d"` | 동일 |
| `text-align: center` | `align="CENTER"` | 대문자 |
| `margin-left: 30mm` | `left="8504"` | mm → HWPUNIT |
| `line-height: 160%` | `lineSpacing="160"` + `type="PERCENT"` | |
| `border: 1px solid #000` | `<hh:borderFill>` 내 각 방향 | §2 참조 |
| `background-color: #E8F5E9` | `<hh:windowBrush faceColor="...">` | |
| `padding: 2mm 5mm` | `<hp:cellMargin top="567" left="1417">` | mm → HWPUNIT |
| `width: 210mm` | `width="59528"` | mm → HWPUNIT |
| `@page { size: A4 }` | `<hp:pageDef width="59528" height="84188">` | |
---
*이 가이드는 한글과컴퓨터의 "글 문서 파일 구조 5.0 (revision 1.3)"을 참고하여 작성되었습니다.*

View File

@@ -0,0 +1,323 @@
# -*- coding: utf-8 -*-
"""
HWP/HWPX ↔ HTML/CSS 변환 유틸리티
hwpx_domain_guide.md의 매핑 테이블을 코드화.
하드코딩 없이 이 모듈의 함수/상수를 참조하여 정확한 변환을 수행한다.
참조: 한글과컴퓨터 "글 문서 파일 구조 5.0" (revision 1.3, 2018-11-08)
"""
# ================================================================
# §1. 단위 변환
# ================================================================
def hwpunit_to_mm(hwpunit):
"""HWPUNIT → mm (1 HWPUNIT = 1/7200 inch)"""
return hwpunit / 7200 * 25.4
def hwpunit_to_pt(hwpunit):
"""HWPUNIT → pt"""
return hwpunit / 7200 * 72
def hwpunit_to_px(hwpunit, dpi=96):
"""HWPUNIT → px (기본 96dpi)"""
return hwpunit / 7200 * dpi
def mm_to_hwpunit(mm):
"""mm → HWPUNIT"""
return mm / 25.4 * 7200
def pt_to_hwpunit(pt):
"""pt → HWPUNIT"""
return pt / 72 * 7200
def px_to_hwpunit(px, dpi=96):
"""px → HWPUNIT"""
return px / dpi * 7200
def charsize_to_pt(hwp_size):
"""HWP 글자 크기 → pt (100 스케일 제거)
예: 1000 → 10pt, 2400 → 24pt
"""
return hwp_size / 100
def pt_to_charsize(pt):
"""pt → HWP 글자 크기
예: 10pt → 1000, 24pt → 2400
"""
return int(pt * 100)
# ================================================================
# §1.3 색상 변환
# ================================================================
def colorref_to_css(colorref):
"""HWP COLORREF (0x00BBGGRR) → CSS #RRGGBB
HWP는 리틀 엔디안 BGR 순서:
- 0x00FF0000 → B=255,G=0,R=0 → #0000ff (파랑)
- 0x000000FF → B=0,G=0,R=255 → #ff0000 (빨강)
"""
r = colorref & 0xFF
g = (colorref >> 8) & 0xFF
b = (colorref >> 16) & 0xFF
return f'#{r:02x}{g:02x}{b:02x}'
def css_to_colorref(css_color):
"""CSS #RRGGBB → HWP COLORREF (0x00BBGGRR)"""
css_color = css_color.lstrip('#')
if len(css_color) == 3: # 단축형 #rgb → #rrggbb
css_color = ''.join(c * 2 for c in css_color)
r = int(css_color[0:2], 16)
g = int(css_color[2:4], 16)
b = int(css_color[4:6], 16)
return (b << 16) | (g << 8) | r
# ================================================================
# §2. 테두리/배경 (BorderFill) 매핑
# ================================================================
# §2.1 테두리선 종류: HWPX type → CSS border-style
BORDER_TYPE_TO_CSS = {
'NONE': 'none',
'SOLID': 'solid',
'DASH': 'dashed',
'DOT': 'dotted',
'DASH_DOT': 'dashed', # CSS 근사
'DASH_DOT_DOT': 'dashed', # CSS 근사
'LONG_DASH': 'dashed',
'CIRCLE': 'dotted', # CSS 근사 (큰 동그라미 → dot)
'DOUBLE': 'double',
'THIN_THICK': 'double', # CSS 근사
'THICK_THIN': 'double', # CSS 근사
'THIN_THICK_THIN':'double', # CSS 근사
'WAVE': 'solid', # CSS 근사 (물결 → 실선)
'DOUBLE_WAVE': 'double', # CSS 근사
'THICK_3D': 'ridge',
'THICK_3D_REV': 'groove',
'3D': 'outset',
'3D_REV': 'inset',
}
# CSS border-style → HWPX type (역방향)
CSS_TO_BORDER_TYPE = {
'none': 'NONE',
'solid': 'SOLID',
'dashed': 'DASH',
'dotted': 'DOT',
'double': 'DOUBLE',
'ridge': 'THICK_3D',
'groove': 'THICK_3D_REV',
'outset': '3D',
'inset': '3D_REV',
}
# §2.2 HWP 바이너리 테두리 굵기 값 → 실제 mm
BORDER_WIDTH_HWP_TO_MM = {
0: 0.1, 1: 0.12, 2: 0.15, 3: 0.2, 4: 0.25, 5: 0.3,
6: 0.4, 7: 0.5, 8: 0.6, 9: 0.7, 10: 1.0, 11: 1.5,
12: 2.0, 13: 3.0, 14: 4.0, 15: 5.0,
}
# ================================================================
# §5. 정렬 매핑
# ================================================================
# §5.1 HWPX align → CSS text-align
ALIGN_TO_CSS = {
'JUSTIFY': 'justify',
'LEFT': 'left',
'RIGHT': 'right',
'CENTER': 'center',
'DISTRIBUTE': 'justify', # CSS 근사
'DISTRIBUTE_SPACE': 'justify', # CSS 근사
}
# CSS text-align → HWPX align (역방향)
CSS_TO_ALIGN = {
'justify': 'JUSTIFY',
'left': 'LEFT',
'right': 'RIGHT',
'center': 'CENTER',
}
# ================================================================
# §5.2 줄 간격 매핑
# ================================================================
LINE_SPACING_TYPE_TO_CSS = {
'PERCENT': 'percent', # 글자에 따라 (%) → line-height: 160%
'FIXED': 'fixed', # 고정값 → line-height: 24pt
'BETWEEN_LINES': 'between', # 여백만 지정
'AT_LEAST': 'at_least', # 최소
}
# ================================================================
# 종합 변환 함수
# ================================================================
def hwpx_border_to_css(border_attrs):
"""HWPX 테두리 속성 dict → CSS border 문자열
Args:
border_attrs: {'type': 'SOLID', 'width': '0.12mm', 'color': '#000000'}
Returns:
'0.12mm solid #000000' 또는 'none'
"""
btype = border_attrs.get('type', 'NONE')
if btype == 'NONE' or btype is None:
return 'none'
width = border_attrs.get('width', '0.12mm')
color = border_attrs.get('color', '#000000')
css_style = BORDER_TYPE_TO_CSS.get(btype, 'solid')
return f'{width} {css_style} {color}'
def css_border_to_hwpx(css_border):
"""CSS border 문자열 → HWPX 속성 dict
Args:
css_border: '0.12mm solid #000000' 또는 'none'
Returns:
{'type': 'SOLID', 'width': '0.12mm', 'color': '#000000'}
"""
if not css_border or css_border.strip() == 'none':
return {'type': 'NONE', 'width': '0mm', 'color': '#000000'}
parts = css_border.strip().split()
width = parts[0] if len(parts) > 0 else '0.12mm'
style = parts[1] if len(parts) > 1 else 'solid'
color = parts[2] if len(parts) > 2 else '#000000'
return {
'type': CSS_TO_BORDER_TYPE.get(style, 'SOLID'),
'width': width,
'color': color,
}
def hwpx_borderfill_to_css(bf_element_attrs):
"""HWPX <hh:borderFill> 전체 속성 → CSS dict
Args:
bf_element_attrs: {
'left': {'type': 'SOLID', 'width': '0.12mm', 'color': '#000000'},
'right': {'type': 'SOLID', 'width': '0.12mm', 'color': '#000000'},
'top': {'type': 'SOLID', 'width': '0.12mm', 'color': '#000000'},
'bottom': {'type': 'SOLID', 'width': '0.12mm', 'color': '#000000'},
'background': '#E8F5E9' or None,
}
Returns:
{
'border-left': '0.12mm solid #000000',
'border-right': '0.12mm solid #000000',
'border-top': '0.12mm solid #000000',
'border-bottom': '0.12mm solid #000000',
'background-color': '#E8F5E9',
}
"""
css = {}
for side in ['left', 'right', 'top', 'bottom']:
border = bf_element_attrs.get(side, {})
css[f'border-{side}'] = hwpx_border_to_css(border)
bg = bf_element_attrs.get('background')
if bg and bg != 'none':
css['background-color'] = bg
return css
def hwpx_align_to_css(hwpx_align):
"""HWPX 정렬 값 → CSS text-align"""
return ALIGN_TO_CSS.get(hwpx_align, 'left')
def css_align_to_hwpx(css_align):
"""CSS text-align → HWPX 정렬 값"""
return CSS_TO_ALIGN.get(css_align, 'LEFT')
def hwpx_line_spacing_to_css(spacing_type, spacing_value):
"""HWPX 줄 간격 → CSS line-height
Args:
spacing_type: 'PERCENT' | 'FIXED' | 'BETWEEN_LINES' | 'AT_LEAST'
spacing_value: 숫자값
Returns:
CSS line-height 문자열 (예: '160%', '24pt')
"""
if spacing_type == 'PERCENT':
return f'{spacing_value}%'
elif spacing_type == 'FIXED':
pt = hwpunit_to_pt(spacing_value)
return f'{pt:.1f}pt'
else:
return f'{spacing_value}%' # 기본 근사
# ================================================================
# 용지 크기 사전 정의 (§7.1)
# ================================================================
PAPER_SIZES = {
'A4': {'width_mm': 210, 'height_mm': 297, 'width_hu': 59528, 'height_hu': 84188},
'A3': {'width_mm': 297, 'height_mm': 420, 'width_hu': 84188, 'height_hu': 119055},
'B5': {'width_mm': 176, 'height_mm': 250, 'width_hu': 49896, 'height_hu': 70866},
'Letter': {'width_mm': 215.9, 'height_mm': 279.4, 'width_hu': 61200, 'height_hu': 79200},
'Legal': {'width_mm': 215.9, 'height_mm': 355.6, 'width_hu': 61200, 'height_hu': 100800},
}
def detect_paper_size(width_hu, height_hu, tolerance=200):
"""HWPUNIT 용지 크기 → 용지 이름 추정
Args:
width_hu: 가로 크기 (HWPUNIT)
height_hu: 세로 크기 (HWPUNIT)
tolerance: 허용 오차 (HWPUNIT)
Returns:
'A4', 'A3', 'Letter' 등 또는 'custom'
"""
for name, size in PAPER_SIZES.items():
if (abs(width_hu - size['width_hu']) <= tolerance and
abs(height_hu - size['height_hu']) <= tolerance):
return name
# landscape 체크
if (abs(width_hu - size['height_hu']) <= tolerance and
abs(height_hu - size['width_hu']) <= tolerance):
return f'{name}_landscape'
return 'custom'
# ================================================================
# 편의 함수
# ================================================================
def css_style_string(css_dict):
"""CSS dict → CSS style 문자열
예: {'border-left': '1px solid #000', 'padding': '5mm'}
'border-left: 1px solid #000; padding: 5mm;'
"""
return ' '.join(f'{k}: {v};' for k, v in css_dict.items() if v)
def mm_format(hwpunit, decimal=1):
"""HWPUNIT → 'Xmm' 포맷 문자열"""
mm = hwpunit_to_mm(hwpunit)
return f'{mm:.{decimal}f}mm'

View File

@@ -0,0 +1,22 @@
이 문서 묶음은 건설/토목 분야의 측량과 디지털 전환(DX)에 초점을 둔 자료로, 드론(UAV) 사진측량, GIS, 지형·지반(terrain/geotech) 정보 모델의 구축·활용을 다룬다.
시공 단계별 측량 절차와 성과품, 기준점 체계, 드론 운용 학습자료, 내부 솔루션(GAIA, GIS Solutions, Terrain Information Model, Map v2.0, GSIM 등)과 워크플로가 포함된다.
핵심 키워드: 측량, 드론/UAV, 사진측량, GIS, 디지털 트윈, 지형지반 모델, GAIA, GSIM, 수치지도 2.0, 배수·유역 분석, 성과품·절차.
너는 건설/토목 측량·드론·GIS·지형지반 모델링 분야의 기술 교육콘텐츠 기획자이자, 자사 솔루션 홍보를 위한 기술 전문가이다.
나는 이 문서 묶음을 근거로 측량에 대한 기본 개념부터 동향, 건설사업의 디지털 전환(DX)을 위한 측량 변화와 절차, 드론 운용·사진측량 워크플로, GIS/지형지반 모델의 구축·활용과 함께 사례를 정확히 이해하고 적용하고 싶다.
우리는 이 자료를 기반으로 CEL(기술발신력 강화) 콘텐츠를 제작하고자 한다.
목표는 다음과 같다:
1) 분야에 대한 개념과 관련 기초 개념, 용어에 대한 정의를 명확하게 설명한다.
2) 기존 방식에 대한 한계와 분야별 디지털 전환(DX)에 대한 변화, 요구사항 등에 대하여 명확히 설명한다.
3) 기존 시장 솔루션들의 방향성과 함께 한계를 최대한 객관적으로 제시한다
4) 이러한 문제를 해결하는 방향과 솔루션을 제시하고, 이를 만족하는 자사 솔루션(GAIA, GSIM, Map v2.0 등)의 기술적 강점과 차별점을 자연스럽게 부각한다
5) 적용된 사례를 이미지와 함게 이해하기 쉽게 정리한다
과업수행 절차와 성과품 기준, 데이터 품질·정합성, 시스템 구성(예: GAIA, GSIM, Map v2.0 등)과 관련된 문제를 분석하여 정리한다.
너의 답변은 문서 기반 사실에 한정하고 추측을 금지하며, 가능한 경우 문서명/페이지/그림 참조 등 근거를 명시하라.
자료의 공백이나 모호함이 있으면 필요한 가정을 분리해 표시하거나 추가 질문으로 명확히 하라; 외부 일반지식은 참고로만 제시하고 출처 구분을 유지하라.
콘텐츠는 내부 직원 교육 및 외부 고객/파트너 대상 기술 세미나에 활용될 예정이므로, 전문성과 신뢰성을 유지하되 이해하기 쉬운 스토리 흐름으로 구성하라.
이후 청킹, 요약, 용어정의, RAG 검색·인용, 비교표 작성, 분석·보고서 작성, 체크리스트·절차서 도출 등 다양한 작업을 네가 주도적으로 구조화해 지원하라.

View File

@@ -0,0 +1,20 @@
건설·토목 측량 DX 실무지침: 드론/UAV·GIS·지형/지반 모델 기반 전주기 표준과 품질관리
1. DX 개요와 기본 개념·기준점 체계
1.1 측량 DX 프레임과 기초 용어
1.1.1 측량 DX 발전 단계
- Digitization→Digitalization→DX 정의·사례 | #DX진화 #정책기조 | [인사이트형] | 03 p.6267 근거 문구 수집, 단계-산출물 매트릭스 표 작성
- UAV/3D Mesh/DSM/LiDAR 전환 | #UAV #3D모델 | [기술형] | 03 p.6268에서 제품유형·데이터모델 비교표와 예시 이미지
1.1.2 핵심 용어·원리 정리
- GNSS(RTK/VRS/Static)·TS·LiDAR | #측량센싱 | [기술형] | 03 p.6465,68 용어정의·정확도·용도 표 구성
- GSD/DSM/DEM/DTM/TIN·맵핑 vs 모델 | #데이터모델 | [비교형] | 03 p.68 정의/산출물/활용 비교표와 주석
1.1.3 수치지형도 2.0 vs 정밀도로지도(HD Map)
- 형식·정확도·객체 차이 | #수치지도2.0 #HDMap | [비교형] | 수치지도2.0(SHP 구성) vs HD Map(±0.25m) 비교표(파일·속성·정확도)
- SOC 활용 한계·보완 | #활용성 #한계 | [인사이트형] | 정밀도로지도 외측 결손·역설계 필요 사례 정리(매뉴얼 2023.07)
1.2 기준점 체계와 국가 수직망 정정
1.2.1 기준점 현황·재구축 필요성
- 설계기준점 상태 통계 | #기준점점검 | [인사이트형] | 1·2·4공구 정상/망실 수량표·지도 핀맵 작성
- 수직망 정정(Z 39~63mm) 영향 | #수직망정정 | [기술형] | 고시 2023-3084 변화량 표·적용 체크리스트(01/05/08 인용)
1.2.2 행정·규정·품질 기준
- 공공측량 준용규정·검사기준 | #준용규정 | [절차형] | 서산–명천 문서 내 준용규정 항목 추출, 준수 체크리스트 표
- 성과품 품질·미수령 항목 | #품질관리 | [인사이트형] | 01/05/08 미수령 목록 대조표(원본 Pile·정사영상·망조정 등)

View File

@@ -0,0 +1,848 @@
🏛️ A4 보고서 퍼블리싱 마스터 가이드 (v82.0 Intelligent Flow)
당신은 **'지능형 퍼블리싱 아키텍트'**입니다. 원본의 **[스타일 독소]**를 제거하고, A4 규격에 맞춰 콘텐츠를 재조립하되, 단순 나열이 아닌 **[최적화된 배치]**를 수행하십시오.
텍스트는 **[복사기]**처럼 있는 그대로 보존하고, 레이아웃은 **[강박증]** 수준으로 맞추십시오.
🚨 0. 최우선 절대 원칙 (Data Integrity)
복사기 모드: 원본 텍스트를 절대 요약, 생략(...), 수정하지 마십시오. 무조건 전부 출력하십시오.
데이터 무결성: 표의 수치, 본문의 문장은 토씨 하나 바꾸지 않고 보존합니다.
🚨 1. 핵심 렌더링 원칙 (The 6 Commandments)
Deep Sanitization (심층 세탁): 모든 class, style을 삭제하되, 차트/그림 내부의 제목 텍스트는 캡션과 중복되므로 제거하십시오.
H1 Only Break: 오직 대목차(H1) 태그에서만 무조건 페이지를 나눕니다.
Orphan Control (고아 방지): 중목차(H2), 소목차(H3)가 페이지 하단에 홀로 남을 경우, 통째로 다음 페이지로 넘기십시오.
Smart Fit (지능형 맞춤): 표나 그림이 페이지를 넘어가는데 그 양이 적다면(15% 이내), 최대 85%까지 축소하여 현재 페이지에 넣으십시오.
Gap Filling (공백 채우기): 그림이 다음 장으로 넘어가 현재 페이지 하단에 큰 공백이 생긴다면, 뒤에 있는 텍스트 문단을 당겨와 그 빈공간을 채우십시오.
Visual Standard:
여백: 상하좌우 20mm를 시각적으로 고정하십시오.
캡션: 모든 그림/표의 제목은 하단 중앙 정렬하십시오.
🛠️ 제작 가이드 (Technical Specs)
아래 코드는 렌더링 엔진입니다. 이 구조를 기반으로 사용자 데이터를 raw-container에 주입하여 출력하십시오.
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>A4 Report v83.0 Template</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@300;400;500;700;900&display=swap');
:root {
--primary: #006400;
--accent: #228B22;
--light-green: #E8F5E9;
--bg: #525659;
}
body { margin: 0; background: var(--bg); font-family: 'Noto Sans KR', sans-serif; }
/* [A4 용지 규격] */
.sheet {
width: 210mm; height: 297mm;
background: white; margin: 20px auto;
position: relative; overflow: hidden; box-sizing: border-box;
box-shadow: 0 0 15px rgba(0,0,0,0.1);
}
@media print {
.sheet { margin: 0; break-after: page; box-shadow: none; }
body { background: white; }
}
/* [헤더/푸터: 여백 20mm 영역 내 배치] */
.page-header {
position: absolute; top: 10mm; left: 20mm; right: 20mm;
font-size: 9pt; color: #000000; font-weight: bold;
text-align: right; border-bottom: none !important; padding-bottom: 5px;
}
.page-footer {
position: absolute; bottom: 10mm; left: 20mm; right: 20mm;
display: flex; justify-content: space-between; align-items: flex-end;
font-size: 9pt; color: #555; border-top: 1px solid #eee; padding-top: 5px;
}
/* [본문 영역: 상하좌우 20mm 고정] */
.body-content {
position: absolute;
top: 20mm; left: 20mm; right: 20mm;
bottom: auto; /* 높이는 JS가 제어 */
}
/* [타이포그래피] */
h1, h2, h3 {
white-space: nowrap; overflow: hidden; word-break: keep-all; color: var(--primary);
margin: 0; padding: 0;
}
h1 {
font-size: 20pt; /* H2와 동일하게 변경 (기존 24pt -> 18pt) */
font-weight: 900;
color: var(--primary);
border-bottom: 2px solid var(--primary);
margin-bottom: 20px;
margin-top: 0;
}
h2 {
font-size: 18pt;
border-left: 5px solid var(--accent);
padding-left: 10px;
margin-top: 30px;
margin-bottom: 10px;
color: #03581dff;
}
h3 { font-size: 14pt; margin-top: 20px; margin-bottom: 5px; color: var(--accent); font-weight: 700; }
p, li { font-size: 12pt !important; line-height: 1.6 !important; text-align: justify; word-break: keep-all; margin-bottom: 5px; }
/* [목차 스타일 수정: lvl-1 강조 및 간격 추가] */
.toc-item { line-height: 1.8; list-style: none; border-bottom: 1px dotted #eee; }
.toc-lvl-1 {
color: #006400; /* 녹색 */
font-weight: 900; /* 볼드 */
font-size: 13.5pt; /* lvl-2(10.5pt)보다 3pt 크게 */
margin-top: 15px; /* 위쪽 간격 */
margin-bottom: 5px; /* 아래쪽 3pt 정도의 간격 */
border-bottom: 2px solid #ccc;
}
.toc-lvl-2 { font-size: 10.5pt; color: #333; margin-left: 20px; font-weight: normal; }
.toc-lvl-3 { font-size: 10.5pt; color: #666; margin-left: 40px; }
/* [표/이미지 스타일] */
table {
width: 100%;
border-collapse: collapse;
margin: 15px 0;
font-size: 9.5pt;
table-layout: auto;
border-top: 2px solid var(--primary);
}
th, td {
border: 1px solid #ddd;
padding: 6px 5px;
text-align: center;
vertical-align: middle;
/* ▼▼▼ [핵심 수정] 단어 단위 줄바꿈 적용 ▼▼▼ */
word-break: keep-all; /* 한글 단어 중간 끊김 방지 (필수) */
word-wrap: break-word; /* 아주 긴 영단어는 줄바꿈 허용 (안전장치) */
}
th {
background: var(--light-green);
color: var(--primary);
font-weight: 900;
white-space: nowrap; /* 제목 셀은 무조건 한 줄 유지 */
letter-spacing: -0.05em;
font-size: 9pt;
}
/* [캡션 및 그림 스타일] */
figure { display: block; margin: 20px auto; text-align: center; width: 100%; }
img, svg { max-width: 95% !important; height: auto !important; display: block; margin: 0 auto; border: 1px solid #eee; }
figcaption {
display: block; text-align: center; margin-top: 10px;
font-size: 9.5pt; color: #666; font-weight: 600;
}
.atomic-block { break-inside: avoid; page-break-inside: avoid; }
#raw-container { display: none; }
/* [하이라이트 박스 표준] */
.highlight-box {
background-color: rgb(226, 236, 226);
border: 1px solid #2a2c2aff;
padding: 5px; margin: 1.5px 1.5px 2px 0px; border-radius: 3px;
/* 여기 있는 font-size는 li 태그 때문에 무시됩니다. 아래 코드로 제어하세요. */
color: #333;
}
.highlight-box li,
.highlight-box p {
font-size: 11pt !important; /* 글자 크기 (원하는 대로 수정: 예 9pt, 10pt) */
line-height: 1.2; /* 줄 간격 (숫자가 클수록 넓어짐: 예 1.4, 1.6) */
letter-spacing: -0.6px; /* 자간 (음수면 좁아짐: 예 -0.5px) */
margin-bottom: 3px; /* 항목 간 간격 */
color: #1a1919ff; /* 글자 색상 */
}
.highlight-box h3, .highlight-box strong, .highlight-box b {
font-size: 12pt !important; color: rgba(2, 37, 2, 1) !important;
font-weight: bold; margin: 0; display: block; margin-bottom: 5px;
}
/* 수정 4 목차 스타일 : 대제목 녹색+크게, 그룹 단위 묶음 */
.toc-group {
margin-bottom: 12px; /* 기존 간격 유지 */
break-inside: avoid;
page-break-inside: avoid;
}
/* [수정] 점(Bullet) 제거를 위한 핵심 코드 */
.toc-lvl-1, .toc-lvl-2, .toc-lvl-3 {
list-style: none !important;
}
.toc-item {
line-height: 1.8;
list-style: none; /* 안전장치 */
border-bottom: 1px dotted #f3e1e1ff; /* 기존 점선 스타일 유지 */
}
.toc-lvl-1 {
color: #006400; /* 기존 녹색 유지 */
font-weight: 900;
font-size: 13.5pt; /* 기존 폰트 크기 유지 */
margin-top: 15px; /* 기존 상단 여백 유지 */
margin-bottom: 5px; /* 기존 하단 여백 유지 */
border-bottom: 2px solid #ccc;
}
.toc-lvl-2 {
font-size: 10.5pt;
color: #333;
margin-left: 20px; /* 기존 들여쓰기 유지 */
font-weight: normal;
}
.toc-lvl-3 {
font-size: 10.5pt;
color: #666;
margin-left: 40px; /* 기존 들여쓰기 유지 */
}
/* [대목차 내부 스타일 보존] */
.toc-lvl-1 .toc-number,
.toc-lvl-1 .toc-text {
font-weight: 900;
font-size: 1.2em;
color: #006400;
}
.toc-lvl-1 .toc-number {
float: left;
margin-right: 14px; /* 기존 간격 유지 */
}
.toc-lvl-1 .toc-text {
display: block;
overflow: hidden;
}
/* [소목차 내부 스타일 보존] */
.toc-lvl-2 .toc-number, .toc-lvl-3 .toc-number {
font-weight: bold;
color: #2c5282;
margin-right: 11px; /* 기존 간격 유지 */
}
.toc-lvl-2 .toc-text, .toc-lvl-3 .toc-text {
color: #4a5568;
font-size: 1em;
}
/* [요약 페이지 전용 스타일 미세 조정] */
.squeeze {
line-height: 1.35 !important;
letter-spacing: -0.5px !important;
margin-bottom: 2px !important;
}
.squeeze-title {
margin-bottom: 5px !important;
padding-bottom: 2px !important;
}
/* 요약 페이지 안의 모든 P 태그에 대해 자간/행간을 좁힘 */
#box-summary p,
#box-summary li {
font-size: 10pt !important; /* 본문보다 0.5pt~1pt 정도 작게 */
line-height: 1.45 !important; /* 줄 간격을 조금 더 촘촘하게 (기존 1.6) */
letter-spacing: -0.04em !important; /* 자간을 미세하게 좁힘 */
margin-bottom: 3px !important; /* 문단 간 격을 줄임 */
text-align: justify; /* 양쪽 정렬 유지 */
}
/* 요약 페이지 제목 아래 간격도 조금 줄임 */
#box-summary h1 {
margin-bottom: 10px !important;
padding-bottom: 5px !important;
}
</style>
</head>
<body>
<div id="raw-container">
<div id="box-cover"></div>
<div id="box-toc"></div>
<div id="box-summary"></div>
<div id="box-content"></div>
</div>
<template id="page-template">
<div class="sheet">
<div class="page-header"></div>
<div class="body-content"></div>
<div class="page-footer">
<span class="rpt-title"></span>
<span class="pg-num"></span>
</div>
</div>
</template>
<script>
window.addEventListener("load", async () => {
await document.fonts.ready; // 웹폰트 로딩 대기 (필수)
// [Config] 297mm - 20mm(상) - 20mm(하) = 257mm ≈ 970px
const CONFIG = { maxHeight: 970 };
const rawContainer = document.getElementById('raw-container');
if (rawContainer) {
rawContainer.innerHTML = rawContainer.innerHTML.replace(
/(<rect[^>]*?)\s+y="[^"]*"\s+([^>]*?y="[^"]*")/gi,
"$1 $2"
);
}
const raw = {
cover: document.getElementById('box-cover'),
toc: document.getElementById('box-toc'),
cover: document.getElementById('box-cover'),
toc: document.getElementById('box-toc'),
summary: document.getElementById('box-summary'),
content: document.getElementById('box-content')
};
let globalPage = 1;
let reportTitle = raw.cover.querySelector('h1')?.innerText || "Report";
function cleanH1Text(text) {
if (!text) return "";
const parts = text.split("-");
return parts[0].trim(); // 첫 번째 부분만 남기고 나머지는 버림
}
// [0] Sanitizer & Pre-processing (Integrity Preserved Version)
function detox(node) {
if (node.nodeType !== 1) return;
// [Safety Check 1] SVG 내부는 절대 건드리지 않음 (차트 깨짐 방지)
if (node.closest('svg')) return;
// [Logic A] 클래스 속성 확인 및 변수 할당
let cls = "";
if (node.hasAttribute('class')) {
cls = node.getAttribute('class');
}
// [Logic B] 하이라이트 박스 감지 및 변환 (조건 정밀화)
// 조건: 1. bg-, border-, box 중 하나라도 포함되어야 함
// 2. 단, title-box(제목박스), toc-(목차), cover-(표지)는 절대 아니어야 함
if ( (cls.includes('bg-') || cls.includes('border-') || cls.includes('box')) &&
!cls.includes('title-box') &&
!cls.includes('toc-') &&
!cls.includes('cover-') &&
!cls.includes('highlight-box') ) { // 이미 변환된 놈도 건드리지 않음
// 1. 표준 클래스로 강제 교체
node.setAttribute('class', 'highlight-box atomic-block');
// 2. 박스 내부 제목 스타일 초기화 (기존 스타일과의 충돌 방지)
const internalHeads = node.querySelectorAll('h3, h4, strong, b');
internalHeads.forEach(head => {
head.removeAttribute('style');
head.removeAttribute('class');
});
// 3. 인라인 스타일 삭제 (Tailwind inline style 등 제거)
node.removeAttribute('style');
// [중요] 여기서 return하면 안됨! 아래 공통 로직(표 테두리 등)도 타야 함.
// 대신, class는 이미 세팅했으므로 class 삭제 로직만 건너뛰게 플래그 변경
cls = 'highlight-box atomic-block';
}
// [Logic C] 일반 요소 세탁 (화이트리스트 유지)
// 목차, 표지, 제목박스, 그리고 방금 변환된 하이라이트 박스는 살려둠
if (node.hasAttribute('class')) {
// 위에서 cls 변수가 갱신되었을 수 있으므로 다시 확인하지 않고 기존 조건 활용
if (!cls.includes('toc-') &&
!cls.includes('cover-') &&
!cls.includes('highlight-') &&
!cls.includes('title-box') &&
!cls.includes('atomic-block')) {
node.removeAttribute('class');
}
}
// [Logic D] 공통 정리 (인라인 스타일 삭제)
// 단, 이미 변환된 박스는 위에서 지웠으니 중복 실행되어도 상관없음
node.removeAttribute('style');
// [Logic E] 표 테두리 강제 적용
if (node.tagName === 'TABLE') node.border = "1";
// [Logic F] 캡션 중복 텍스트 숨김 처리
if (node.tagName === 'FIGURE') {
const internalTitles = node.querySelectorAll('h3, h4, .chart-title');
internalTitles.forEach(t => t.style.display = 'none');
}
}
function getFlatNodes(element) {
// [1] 목차(TOC) 처리 로직 (제목 생성 + 완벽한 그룹화)
if(element.id === 'box-toc') {
// 1. 스타일 초기화
element.querySelectorAll('*').forEach(el => detox(el));
// 2. 레벨 분석 (위의 formatTOC 실행)
formatTOC(element);
const tocNodes = [];
// [수정] 원본에 H1이 없으면 '목차' 타이틀 강제 생성
let title = element.querySelector('h1');
if (!title) {
title = document.createElement('h1');
title.innerText = "목차";
// 디자인 통일성을 위해 스타일 적용은 CSS에 맡김
}
tocNodes.push(title.cloneNode(true));
// 3. 그룹화 로직 (Flattened List -> Grouped Divs)
// 중첩이 엉망인 원본 무시하고, 모든 li를 긁어모음
const allLis = element.querySelectorAll('li');
let currentGroup = null;
allLis.forEach(li => {
const isLevel1 = li.classList.contains('toc-lvl-1');
// 대목차(Level 1)가 나오면 새로운 그룹 시작
if (isLevel1) {
// 이전 그룹이 있으면 저장
if (currentGroup) tocNodes.push(currentGroup);
// 새 그룹 박스 생성
currentGroup = document.createElement('div');
currentGroup.className = 'toc-group atomic-block';
// 내부 UL 생성 (들여쓰기 구조용)
const ulWrapper = document.createElement('ul');
ulWrapper.style.margin = "0";
ulWrapper.style.padding = "0";
currentGroup.appendChild(ulWrapper);
}
// 안전장치: 첫 시작이 소목차라 그룹이 없으면 하나 만듦
if (!currentGroup) {
currentGroup = document.createElement('div');
currentGroup.className = 'toc-group atomic-block';
const ulWrapper = document.createElement('ul');
ulWrapper.style.margin = "0";
ulWrapper.style.padding = "0";
currentGroup.appendChild(ulWrapper);
}
// 현재 그룹의 ul에 li 추가
currentGroup.querySelector('ul').appendChild(li.cloneNode(true));
});
// 마지막 그룹 저장
if (currentGroup) tocNodes.push(currentGroup);
return tocNodes;
}
// [2] 본문(Body) 처리 로직 (기존 박스 보존 로직 유지)
let nodes = [];
Array.from(element.children).forEach(child => {
detox(child);
if (child.classList.contains('highlight-box')) {
child.querySelectorAll('h3, h4, strong, b').forEach(head => {
head.removeAttribute('style');
head.removeAttribute('class');
});
nodes.push(child.cloneNode(true));
}
else if(['DIV','SECTION','ARTICLE','MAIN'].includes(child.tagName)) {
nodes = nodes.concat(getFlatNodes(child));
}
else if (['UL','OL'].includes(child.tagName)) {
Array.from(child.children).forEach((li, idx) => {
detox(li);
const w = document.createElement(child.tagName);
w.style.margin="0"; w.style.paddingLeft="20px";
if(child.tagName==='OL') w.start=idx+1;
const cloneLi = li.cloneNode(true);
cloneLi.querySelectorAll('*').forEach(el => detox(el));
w.appendChild(cloneLi);
nodes.push(w);
});
} else {
const clone = child.cloneNode(true);
detox(clone);
clone.querySelectorAll('*').forEach(el => detox(el));
nodes.push(clone);
}
});
return nodes;
}
// [Final Optimized Engine] Place -> Squeeze -> Check -> Split
// 목적: 배치 즉시 자간을 줄여 2글자 고아를 방지하고, 공간을 확보하여 페이지 밀림을 막음
function renderFlow(sectionType, sourceNodes) {
if (!sourceNodes.length) return;
let currentHeaderTitle = sectionType === 'toc' ? "목차" : (sectionType === 'summary' ? "요약" : reportTitle);
let page = createPage(sectionType, currentHeaderTitle);
let body = page.querySelector('.body-content');
// 원본 노드 보존을 위해 큐에 담기
let queue = [...sourceNodes];
while (queue.length > 0) {
let node = queue.shift();
let clone = node.cloneNode(true);
// [태그 판별]
let isH1 = clone.tagName === 'H1';
let isHeading = ['H2', 'H3'].includes(clone.tagName);
// LI도 텍스트로 취급하여 분할 대상에 포함
let isText = ['P', 'LI'].includes(clone.tagName) && !clone.classList.contains('atomic-block');
let isAtomic = ['TABLE', 'FIGURE', 'IMG', 'SVG'].includes(clone.tagName) ||
clone.querySelector('table, img, svg') ||
clone.classList.contains('atomic-block');
// [전처리] H1 텍스트 정제 ("-" 뒤 제거)
if (isH1 && clone.innerText.includes('-')) {
clone.innerText = clone.innerText.split('-')[0].trim();
}
// [Rule 1] H1 처리 (무조건 새 페이지)
if (isH1 && (sectionType === 'body' || sectionType === 'summary')) {
currentHeaderTitle = clone.innerText;
if (body.children.length > 0) {
page = createPage(sectionType, currentHeaderTitle);
body = page.querySelector('.body-content');
} else {
page.querySelector('.page-header').innerText = currentHeaderTitle;
}
}
// [Rule 2] Orphan Control (제목이 페이지 끝에 걸리는 것 방지)
if (isHeading) {
const spaceLeft = CONFIG.maxHeight - body.scrollHeight;
if (spaceLeft < 90) {
page = createPage(sectionType, currentHeaderTitle);
body = page.querySelector('.body-content');
}
}
// ▼▼▼ [Step 1: 일단 배치 (Place)] ▼▼▼
body.appendChild(clone);
// ▼▼▼ [Step 2: 자간 최적화 (Squeeze Logic)] ▼▼▼
// 배치 직후, 자간을 줄여서 줄바꿈을 없앨 수 있는지 확인
// 대상: 10글자 이상인 텍스트 노드
if (isText && clone.innerText.length > 10) {
const originalHeight = clone.offsetHeight;
// 1. 강력하게 줄여봄
clone.style.letterSpacing = "-1.0px";
// 2. 높이가 줄어들었는가? (줄바꿈이 사라짐 = Orphan 해결)
if (clone.offsetHeight < originalHeight) {
// 성공! 너무 빽빽하지 않게 -0.8px로 안착
clone.style.letterSpacing = "-0.8px";
} else {
// 실패! 줄여도 줄이 안 바뀌면 가독성을 위해 원상복구
clone.style.letterSpacing = "";
}
}
// ▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲
// [Rule 3] 넘침 감지 (Overflow Check)
if (body.scrollHeight > CONFIG.maxHeight) {
// 3-1. 텍스트 분할 (Split) - LI 태그 포함
if (isText) {
body.removeChild(clone); // 일단 제거
let textContent = node.innerText;
let tempP = node.cloneNode(false); // 태그 속성 유지 (li면 li, p면 p)
tempP.innerText = "";
// 위에서 결정된 최적 자간 스타일 유지
if (clone.style.letterSpacing) tempP.style.letterSpacing = clone.style.letterSpacing;
body.appendChild(tempP);
const words = textContent.split(' ');
let currentText = "";
for (let i = 0; i < words.length; i++) {
let word = words[i];
let prevText = currentText;
currentText += (currentText ? " " : "") + word;
tempP.innerText = currentText;
// 단어 하나 추가했더니 넘쳤는가?
if (body.scrollHeight > CONFIG.maxHeight) {
// 직전 상태(안 넘치는 상태)로 복구
tempP.innerText = prevText;
// [디자인 보정] 잘린 문단의 마지막 줄 양쪽 정렬
tempP.style.textAlign = "justify";
tempP.style.textAlignLast = "justify";
// 남은 단어들을 다시 합쳐서 대기열 맨 앞으로
let remainingText = words.slice(i).join(' ');
let remainingNode = node.cloneNode(false);
remainingNode.innerText = remainingText;
queue.unshift(remainingNode);
// 새 페이지 생성
page = createPage(sectionType, currentHeaderTitle);
body = page.querySelector('.body-content');
// [중요] 새 페이지 갔으면 압축 플래그/스타일 초기화
// 새 페이지에서는 다시 넉넉하게 시작해야 함
body.style.lineHeight = "";
body.style.letterSpacing = "";
break; // for문 탈출
}
}
}
// 3-2. 표, 그림, 박스인 경우 -> 통째로 다음 장으로 이동
else {
body.removeChild(clone); // 일단 뺌
// [Gap Filling] 빈 공간 채우기
let spaceLeft = CONFIG.maxHeight - body.scrollHeight;
// 공간이 50px 이상 있고, 앞에 글자가 이미 있을 때만 채우기 시도
if (body.children.length > 0 && spaceLeft > 50 && queue.length > 0) {
while(queue.length > 0) {
let candidate = queue[0];
if (['H1','H2','H3'].includes(candidate.tagName) ||
candidate.classList.contains('atomic-block') ||
candidate.querySelector('img, table')) break;
let filler = candidate.cloneNode(true);
// 가져올 때도 최적화(Squeeze) 시도
if(['P','LI'].includes(filler.tagName) && filler.innerText.length > 10) {
const hBefore = filler.offsetHeight; // (가상)
filler.style.letterSpacing = "-1.0px";
// 실제 DOM에 붙여봐야 높이를 알 수 있으므로 일단 적용
}
body.appendChild(filler);
if (body.scrollHeight <= CONFIG.maxHeight) {
// 들어갔으면 확정하고 대기열 제거
// 최적화 스타일 유지 (-1.0px -> -0.8px 조정 등은 생략해도 무방하나 디테일 원하면 추가 가능)
if(filler.style.letterSpacing === "-1.0px") filler.style.letterSpacing = "-0.8px";
queue.shift();
} else {
body.removeChild(filler);
break;
}
}
}
// 2. 이미지 배치 (수정된 핵심 로직)
// [버그 수정] 현재 페이지가 비어있지 않을 때만 새 페이지 생성!
if (body.children.length > 0) {
page = createPage(sectionType, currentHeaderTitle);
body = page.querySelector('.body-content');
}
// 이미지를 붙임
body.appendChild(clone);
// [Smart Fit] 넘치면 축소 (기존 유지)
if (isAtomic && body.scrollHeight > CONFIG.maxHeight) {
const currentH = clone.offsetHeight;
const overflow = body.scrollHeight - CONFIG.maxHeight;
body.removeChild(clone);
if (overflow > 0 && overflow < (currentH * 0.15)) {
clone.style.transform = "scale(0.85)";
clone.style.transformOrigin = "top center";
clone.style.marginBottom = `-${currentH * 0.15}px`;
body.appendChild(clone);
} else {
body.appendChild(clone); // 너무 크면 그냥 둠
}
}
}
}
}
}
function createPage(type, headerTitle) {
const tpl = document.getElementById('page-template');
const clone = tpl.content.cloneNode(true);
const sheet = clone.querySelector('.sheet');
if (type === 'cover') {
sheet.innerHTML = "";
const title = raw.cover.querySelector('h1')?.innerText || "Report";
const sub = raw.cover.querySelector('h2')?.innerText || "";
const pTags = raw.cover.querySelectorAll('p');
const infos = pTags.length > 0 ? Array.from(pTags).map(p => p.innerText).join(" / ") : "";
// [표지 스타일] 테두리 제거 및 중앙 정렬
sheet.innerHTML = `
<div style="position:absolute; top:20mm; right:20mm; text-align:right; font-size:11pt; color:#666;">${infos}</div>
<div style="display:flex; flex-direction:column; justify-content:center; align-items:center; height:100%; text-align:center; width:100%;">
<div style="width:85%;">
<div style="font-size:32pt; font-weight:900; color:var(--primary); line-height:1.2; margin-bottom:30px; word-break:keep-all;">${title}</div>
<div style="font-size:20pt; font-weight:300; color:#444; word-break:keep-all;">${sub}</div>
</div>
</div>`;
} else {
// ... (나머지 페이지 생성 로직 기존 유지) ...
clone.querySelector('.page-header').innerText = headerTitle;
clone.querySelector('.rpt-title').innerText = reportTitle;
if (type !== 'toc') clone.querySelector('.pg-num').innerText = `- ${globalPage++} -`;
else clone.querySelector('.pg-num').innerText = "";
}
document.body.appendChild(sheet);
return sheet;
}
createPage('cover');
if(raw.toc) renderFlow('toc', getFlatNodes(raw.toc));
// [요약 페이지 지능형 맞춤 로직 (Smart Squeeze)]
const summaryNodes = getFlatNodes(raw.summary);
// 1. 가상 공간에 미리 렌더링하여 높이 측정
const tempBox = document.createElement('div');
tempBox.style.width = "210mm";
tempBox.style.position = "absolute";
tempBox.style.visibility = "hidden";
tempBox.id = 'box-summary'; // CSS 적용
document.body.appendChild(tempBox);
// 노드 복제하여 주입
summaryNodes.forEach(node => tempBox.appendChild(node.cloneNode(true)));
// 2. 높이 분석 (Smart Runt Control)
const totalHeight = tempBox.scrollHeight;
const pageHeight = CONFIG.maxHeight; // 1페이지 가용 높이 (약 970px)
const lastPart = totalHeight % pageHeight;
// [조건] 넘친 양이 100px 미만일 때 압축
if (totalHeight > pageHeight && lastPart > 0 && lastPart < 180) {
summaryNodes.forEach(node => {
if(node.nodeType === 1) {
node.classList.add('squeeze');
if(node.tagName === 'H1') node.classList.add('squeeze-title');
// [추가] P, LI 태그에 더 강력한 인라인 스타일 강제 주입 (폰트 축소 포함)
if(node.tagName === 'P' || node.tagName === 'LI') {
node.style.fontSize = "9.5pt";
node.style.lineHeight = "1.4";
node.style.letterSpacing = "-0.8px";
}
}
});
}
// 뒷정리
document.body.removeChild(tempBox);
// 3. 렌더링 실행
renderFlow('summary', summaryNodes);
// ▼▼▼ [기존 유지] 본문 렌더링 및 마무리 작업 ▼▼▼
renderFlow('body', getFlatNodes(raw.content));
// 긴 제목 자동 축소 (기존 기능 유지)
document.querySelectorAll('.sheet h1, .sheet h2').forEach(el => {
let fs = 100;
while(el.scrollWidth > el.clientWidth && fs > 50) { el.style.fontSize = (--fs)+"%"; }
});
// ▼▼▼▼▼ [수정된 핵심 로직: 통합 자간 조정] ▼▼▼▼▼
// 변경점 1: 'li' 태그 포함
// 변경점 2: 표, 그림 내부 텍스트 제외
// 변경점 3: 글자수 제한 완화 (10자 이상이면 검사)
const allTextNodes = document.querySelectorAll('.sheet .body-content p, .sheet .body-content li');
allTextNodes.forEach(el => {
// 1. [제외 대상] 표(table), 그림(figure), 차트 내부는 건드리지 않음
if (el.closest('table') || el.closest('figure') || el.closest('.chart')) return;
// 2. [최소 길이] 10자 미만은 무시
if (el.innerText.trim().length < 10) return;
// 3. [테스트]
const originH = el.offsetHeight;
const originSpacing = el.style.letterSpacing;
el.style.fontSize = "12pt";
// 강력하게 당겨봄
el.style.letterSpacing = "-1.4px";
const newH = el.offsetHeight;
// 성공(높이 줄어듦) 시
if (newH < originH) {
el.style.letterSpacing = "-1.0px"; // 적당히 안착
}
else {
el.style.letterSpacing = originSpacing; // 원상복구
}
});
// ▲▲▲▲▲ [수정 끝] ▲▲▲▲▲
// 제목 자동 축소 (중복 실행 방지를 위해 제거해도 되지만, 안전하게 둠)
document.querySelectorAll('.sheet h1, .sheet h2').forEach(el => {
let fs = 100;
while(el.scrollWidth > el.clientWidth && fs > 50) { el.style.fontSize = (--fs)+"%"; }
});
const pages = document.querySelectorAll('.sheet'); // .page 대신 .sheet로 수정하여 정확도 높임
if (pages.length >= 2) {
const lastSheet = pages[pages.length - 1];
const prevSheet = pages[pages.length - 2];
// 커버나 목차가 아닐때만 진행
if(lastSheet.querySelector('.rpt-title')) {
const lastBody = lastSheet.querySelector('.body-content');
const prevBody = prevSheet.querySelector('.body-content');
// 마지막 페이지 내용이 3줄(약 150px) 이하인가?
if (lastBody.scrollHeight < 150 && lastBody.innerText.trim().length > 0) {
prevBody.style.lineHeight = "1.3"; // 앞 페이지 압축
prevBody.style.paddingBottom = "0px";
const contentToMove = Array.from(lastBody.children);
contentToMove.forEach(child => prevBody.appendChild(child.cloneNode(true)));
if (prevBody.scrollHeight <= CONFIG.maxHeight + 5) {
lastSheet.remove(); // 성공 시 마지막 장 삭제
} else {
// 실패 시 원상 복구
for(let i=0; i<contentToMove.length; i++) prevBody.lastElementChild.remove();
prevBody.style.lineHeight = "";
}
}
}
}
// 원본 데이터 삭제
const rawContainer = document.getElementById('raw-container');
if(rawContainer) rawContainer.remove();
});
</script>
</body>
</html>
⚠️ [최종 경고 - 출력 직전 필수 확인]
1. 원본의 모든 텍스트가 100% 포함되었는가?
2. "..." 또는 요약된 문장이 없는가?
3. 생략된 문단이 단 하나도 없는가?
위 3가지 중 하나라도 위반 시, 출력을 중단하고 처음부터 다시 작성하십시오.
원본 텍스트 글자 수와 출력 텍스트 글자 수가 동일해야 합니다.