release: 2026-03-01 (6건 커밋) #69

병합
htlee develop 에서 main 로 6 commits 를 머지했습니다 2026-03-03 08:51:08 +09:00
6개의 변경된 파일729개의 추가작업 그리고 24개의 파일을 삭제
Showing only changes of commit 18e93d17d7 - Show all commits

파일 보기

@ -25,6 +25,7 @@
"@vis.gl/react-maplibre": "^8.1.0",
"axios": "^1.13.5",
"emoji-mart": "^5.6.0",
"jszip": "^3.10.1",
"lucide-react": "^0.564.0",
"maplibre-gl": "^5.19.0",
"react": "^19.2.0",

파일 보기

@ -27,6 +27,7 @@
"@vis.gl/react-maplibre": "^8.1.0",
"axios": "^1.13.5",
"emoji-mart": "^5.6.0",
"jszip": "^3.10.1",
"lucide-react": "^0.564.0",
"maplibre-gl": "^5.19.0",
"react": "^19.2.0",

파일 보기

@ -16,7 +16,8 @@ import {
analysisCatColors,
inferAnalysisCategory,
type ViewState,
} from './reportUtils'
} from './reportUtils';
import type { TemplateType } from './reportTypes';
import TemplateFormEditor from './TemplateFormEditor'
import ReportGenerator from './ReportGenerator'
@ -296,7 +297,7 @@ export function ReportsView() {
</button>
<button
onClick={() => {
const tpl = templateTypes.find(t => t.id === previewReport.reportType)
const tpl = templateTypes.find(t => t.id === previewReport.reportType) as TemplateType | undefined
if (tpl) {
const getVal = (key: string) => {
if (key === 'author') return previewReport.author
@ -308,14 +309,15 @@ export function ReportsView() {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return (previewReport as any)[key] || ''
}
const html = generateReportHTML(tpl.label, { writeTime: previewReport.incident.writeTime, author: previewReport.author, jurisdiction: previewReport.jurisdiction }, tpl.sections, getVal)
exportAsHWP(html, previewReport.title || tpl.label)
const meta = { writeTime: previewReport.incident.writeTime, author: previewReport.author, jurisdiction: previewReport.jurisdiction }
const filename = previewReport.title || tpl.label
exportAsHWP(tpl.label, meta, tpl.sections, getVal, filename)
}
}}
className="w-full flex items-center justify-center gap-1.5 font-korean text-[12px] font-bold cursor-pointer rounded-md py-[11px]"
style={{ border: '1px solid rgba(59,130,246,0.4)', background: 'rgba(59,130,246,0.1)', color: 'var(--blue)' }}
>
<span>📝</span> HWP
<span>📝</span> HWPX
</button>
</div>
</div>

파일 보기

@ -67,15 +67,14 @@ function TemplateFormEditor({ onSave, onBack }: TemplateFormEditorProps) {
}
const doExport = (format: 'pdf' | 'hwp') => {
const html = generateReportHTML(
template.label,
{ writeTime: reportMeta.writeTime, author: reportMeta.author, jurisdiction: reportMeta.jurisdiction },
template.sections,
getVal
)
const meta = { writeTime: reportMeta.writeTime, author: reportMeta.author, jurisdiction: reportMeta.jurisdiction }
const filename = formData['incident.name'] || `${template.label}_${reportMeta.writeTime.replace(/[\s:]/g, '_')}`
if (format === 'pdf') exportAsPDF(html, filename)
else exportAsHWP(html, filename)
if (format === 'pdf') {
const html = generateReportHTML(template.label, meta, template.sections, getVal)
exportAsPDF(html, filename)
} else {
exportAsHWP(template.label, meta, template.sections, getVal, filename)
}
}
return (
@ -185,7 +184,7 @@ function TemplateFormEditor({ onSave, onBack }: TemplateFormEditorProps) {
<div className="flex items-center justify-between px-6 py-3 border-t border-border bg-bg-1">
<div className="flex items-center gap-2">
<button onClick={() => doExport('pdf')} className="px-3 py-2 text-[11px] font-semibold rounded bg-status-red text-white hover:opacity-90 transition-all">PDF</button>
<button onClick={() => doExport('hwp')} className="px-3 py-2 text-[11px] font-semibold rounded bg-[#2563eb] text-white hover:opacity-90 transition-all">HWP</button>
<button onClick={() => doExport('hwp')} className="px-3 py-2 text-[11px] font-semibold rounded bg-[#2563eb] text-white hover:opacity-90 transition-all">HWPX</button>
</div>
<div className="flex items-center gap-3">
<button

파일 보기

@ -0,0 +1,703 @@
import JSZip from 'jszip';
// ─── XML 이스케이프 ──────────────────────────────────────────
function esc(str: string): string {
return str
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
// ─── ID 생성 ─────────────────────────────────────────────────
let _idSeq = 1000000000;
function nextId(): number {
return _idSeq++;
}
// ─── 타입 ────────────────────────────────────────────────────
interface ReportSection {
title: string;
fields: { key: string; label: string }[];
}
interface ReportMeta {
writeTime: string;
author: string;
jurisdiction: string;
}
// ─── 정적 파일 (Skeleton.hwpx 기반) ─────────────────────────
/**
* mimetype: STORE ()
*/
const MIMETYPE = 'application/hwp+zip';
/**
* version.xml: Skeleton.hwpx
*/
const VERSION_XML =
'<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>' +
'<hv:HCFVersion xmlns:hv="http://www.hancom.co.kr/hwpml/2011/version" ' +
'tagetApplication="WORDPROCESSOR" major="5" minor="1" micro="1" buildNumber="0" ' +
'os="1" xmlVersion="1.5" application="Hancom Office Hangul" ' +
'appVersion="13, 0, 0, 1408 WIN32LEWindows_10"/>';
/**
* settings.xml: Skeleton.hwpx
*/
const SETTINGS_XML =
'<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>' +
'<ha:HWPApplicationSetting xmlns:ha="http://www.hancom.co.kr/hwpml/2011/app" ' +
'xmlns:config="urn:oasis:names:tc:opendocument:xmlns:config:1.0">' +
'<ha:CaretPosition listIDRef="0" paraIDRef="0" pos="16"/>' +
'</ha:HWPApplicationSetting>';
/**
* META-INF/container.xml: Skeleton.hwpx
*/
const CONTAINER_XML =
'<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>' +
'<ocf:container xmlns:ocf="urn:oasis:names:tc:opendocument:xmlns:container" ' +
'xmlns:hpf="http://www.hancom.co.kr/schema/2011/hpf">' +
'<ocf:rootfiles>' +
'<ocf:rootfile full-path="Contents/content.hpf" media-type="application/hwpml-package+xml"/>' +
'<ocf:rootfile full-path="Preview/PrvText.txt" media-type="text/plain"/>' +
'<ocf:rootfile full-path="META-INF/container.rdf" media-type="application/rdf+xml"/>' +
'</ocf:rootfiles>' +
'</ocf:container>';
/**
* META-INF/container.rdf: Skeleton.hwpx
*/
const CONTAINER_RDF =
'<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>' +
'<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">' +
'<rdf:Description rdf:about="">' +
'<ns0:hasPart xmlns:ns0="http://www.hancom.co.kr/hwpml/2016/meta/pkg#" rdf:resource="Contents/header.xml"/>' +
'</rdf:Description>' +
'<rdf:Description rdf:about="Contents/header.xml">' +
'<rdf:type rdf:resource="http://www.hancom.co.kr/hwpml/2016/meta/pkg#HeaderFile"/>' +
'</rdf:Description>' +
'<rdf:Description rdf:about="">' +
'<ns0:hasPart xmlns:ns0="http://www.hancom.co.kr/hwpml/2016/meta/pkg#" rdf:resource="Contents/section0.xml"/>' +
'</rdf:Description>' +
'<rdf:Description rdf:about="Contents/section0.xml">' +
'<rdf:type rdf:resource="http://www.hancom.co.kr/hwpml/2016/meta/pkg#SectionFile"/>' +
'</rdf:Description>' +
'<rdf:Description rdf:about="">' +
'<rdf:type rdf:resource="http://www.hancom.co.kr/hwpml/2016/meta/pkg#Document"/>' +
'</rdf:Description>' +
'</rdf:RDF>';
/**
* META-INF/manifest.xml: Skeleton.hwpx
*/
const MANIFEST_XML =
'<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>' +
'<odf:manifest xmlns:odf="urn:oasis:names:tc:opendocument:xmlns:manifest:1.0"/>';
/**
* Contents/content.hpf: Skeleton.hwpx ,
*/
function buildContentHpf(): string {
const now = new Date().toISOString();
return (
'<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>' +
'<opf:package xmlns:ha="http://www.hancom.co.kr/hwpml/2011/app" ' +
'xmlns:hp="http://www.hancom.co.kr/hwpml/2011/paragraph" ' +
'xmlns:hp10="http://www.hancom.co.kr/hwpml/2016/paragraph" ' +
'xmlns:hs="http://www.hancom.co.kr/hwpml/2011/section" ' +
'xmlns:hc="http://www.hancom.co.kr/hwpml/2011/core" ' +
'xmlns:hh="http://www.hancom.co.kr/hwpml/2011/head" ' +
'xmlns:hhs="http://www.hancom.co.kr/hwpml/2011/history" ' +
'xmlns:hm="http://www.hancom.co.kr/hwpml/2011/master-page" ' +
'xmlns:hpf="http://www.hancom.co.kr/schema/2011/hpf" ' +
'xmlns:dc="http://purl.org/dc/elements/1.1/" ' +
'xmlns:opf="http://www.idpf.org/2007/opf/" ' +
'xmlns:ooxmlchart="http://www.hancom.co.kr/hwpml/2016/ooxmlchart" ' +
'xmlns:hwpunitchar="http://www.hancom.co.kr/hwpml/2016/HwpUnitChar" ' +
'xmlns:epub="http://www.idpf.org/2007/ops" ' +
'xmlns:config="urn:oasis:names:tc:opendocument:xmlns:config:1.0" ' +
'version="" unique-identifier="" id="">' +
'<opf:metadata>' +
'<opf:title/>' +
'<opf:language>ko</opf:language>' +
'<opf:meta name="creator" content="text">WING-OPS</opf:meta>' +
'<opf:meta name="subject" content="text"/>' +
'<opf:meta name="description" content="text"/>' +
'<opf:meta name="lastsaveby" content="text">WING-OPS</opf:meta>' +
`<opf:meta name="CreatedDate" content="text">${now}</opf:meta>` +
`<opf:meta name="ModifiedDate" content="text">${now}</opf:meta>` +
'</opf:metadata>' +
'<opf:manifest>' +
'<opf:item id="header" href="Contents/header.xml" media-type="application/xml"/>' +
'<opf:item id="section0" href="Contents/section0.xml" media-type="application/xml"/>' +
'<opf:item id="settings" href="settings.xml" media-type="application/xml"/>' +
'</opf:manifest>' +
'<opf:spine>' +
'<opf:itemref idref="header" linear="yes"/>' +
'<opf:itemref idref="section0" linear="yes"/>' +
'</opf:spine>' +
'</opf:package>'
);
}
/**
* Contents/header.xml: Skeleton.hwpx ( )
* charPr :
* id=0: 10pt, ( )
* id=1: 10pt, ( - )
* id=2: 9pt,
* id=3: 9pt, ()
* id=4: 9pt, ( -5)
* id=5: 16pt, (#2E74B5)
* id=6: 11pt,
*/
const HEADER_XML =
'<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>' +
'<hh:head xmlns:ha="http://www.hancom.co.kr/hwpml/2011/app" ' +
'xmlns:hp="http://www.hancom.co.kr/hwpml/2011/paragraph" ' +
'xmlns:hp10="http://www.hancom.co.kr/hwpml/2016/paragraph" ' +
'xmlns:hs="http://www.hancom.co.kr/hwpml/2011/section" ' +
'xmlns:hc="http://www.hancom.co.kr/hwpml/2011/core" ' +
'xmlns:hh="http://www.hancom.co.kr/hwpml/2011/head" ' +
'xmlns:hhs="http://www.hancom.co.kr/hwpml/2011/history" ' +
'xmlns:hm="http://www.hancom.co.kr/hwpml/2011/master-page" ' +
'xmlns:hpf="http://www.hancom.co.kr/schema/2011/hpf" ' +
'xmlns:dc="http://purl.org/dc/elements/1.1/" ' +
'xmlns:opf="http://www.idpf.org/2007/opf/" ' +
'xmlns:ooxmlchart="http://www.hancom.co.kr/hwpml/2016/ooxmlchart" ' +
'xmlns:hwpunitchar="http://www.hancom.co.kr/hwpml/2016/HwpUnitChar" ' +
'xmlns:epub="http://www.idpf.org/2007/ops" ' +
'xmlns:config="urn:oasis:names:tc:opendocument:xmlns:config:1.0" ' +
'version="1.5" secCnt="1">' +
'<hh:beginNum page="1" footnote="1" endnote="1" pic="1" tbl="1" equation="1"/>' +
'<hh:refList>' +
// ── 폰트 (함초롬돋움/함초롬바탕, 7개 언어 그룹)
'<hh:fontfaces itemCnt="7">' +
'<hh:fontface lang="HANGUL" fontCnt="2">' +
'<hh:font id="0" face="함초롬돋움" type="TTF" isEmbedded="0"><hh:typeInfo familyType="FCAT_GOTHIC" weight="6" proportion="4" contrast="0" strokeVariation="1" armStyle="1" letterform="1" midline="1" xHeight="1"/></hh:font>' +
'<hh:font id="1" face="함초롬바탕" type="TTF" isEmbedded="0"><hh:typeInfo familyType="FCAT_GOTHIC" weight="6" proportion="4" contrast="0" strokeVariation="1" armStyle="1" letterform="1" midline="1" xHeight="1"/></hh:font>' +
'</hh:fontface>' +
'<hh:fontface lang="LATIN" fontCnt="2">' +
'<hh:font id="0" face="함초롬돋움" type="TTF" isEmbedded="0"><hh:typeInfo familyType="FCAT_GOTHIC" weight="6" proportion="4" contrast="0" strokeVariation="1" armStyle="1" letterform="1" midline="1" xHeight="1"/></hh:font>' +
'<hh:font id="1" face="함초롬바탕" type="TTF" isEmbedded="0"><hh:typeInfo familyType="FCAT_GOTHIC" weight="6" proportion="4" contrast="0" strokeVariation="1" armStyle="1" letterform="1" midline="1" xHeight="1"/></hh:font>' +
'</hh:fontface>' +
'<hh:fontface lang="HANJA" fontCnt="2">' +
'<hh:font id="0" face="함초롬돋움" type="TTF" isEmbedded="0"><hh:typeInfo familyType="FCAT_GOTHIC" weight="6" proportion="4" contrast="0" strokeVariation="1" armStyle="1" letterform="1" midline="1" xHeight="1"/></hh:font>' +
'<hh:font id="1" face="함초롬바탕" type="TTF" isEmbedded="0"><hh:typeInfo familyType="FCAT_GOTHIC" weight="6" proportion="4" contrast="0" strokeVariation="1" armStyle="1" letterform="1" midline="1" xHeight="1"/></hh:font>' +
'</hh:fontface>' +
'<hh:fontface lang="JAPANESE" fontCnt="2">' +
'<hh:font id="0" face="함초롬돋움" type="TTF" isEmbedded="0"><hh:typeInfo familyType="FCAT_GOTHIC" weight="6" proportion="4" contrast="0" strokeVariation="1" armStyle="1" letterform="1" midline="1" xHeight="1"/></hh:font>' +
'<hh:font id="1" face="함초롬바탕" type="TTF" isEmbedded="0"><hh:typeInfo familyType="FCAT_GOTHIC" weight="6" proportion="4" contrast="0" strokeVariation="1" armStyle="1" letterform="1" midline="1" xHeight="1"/></hh:font>' +
'</hh:fontface>' +
'<hh:fontface lang="OTHER" fontCnt="2">' +
'<hh:font id="0" face="함초롬돋움" type="TTF" isEmbedded="0"><hh:typeInfo familyType="FCAT_GOTHIC" weight="6" proportion="4" contrast="0" strokeVariation="1" armStyle="1" letterform="1" midline="1" xHeight="1"/></hh:font>' +
'<hh:font id="1" face="함초롬바탕" type="TTF" isEmbedded="0"><hh:typeInfo familyType="FCAT_GOTHIC" weight="6" proportion="4" contrast="0" strokeVariation="1" armStyle="1" letterform="1" midline="1" xHeight="1"/></hh:font>' +
'</hh:fontface>' +
'<hh:fontface lang="SYMBOL" fontCnt="2">' +
'<hh:font id="0" face="함초롬돋움" type="TTF" isEmbedded="0"><hh:typeInfo familyType="FCAT_GOTHIC" weight="6" proportion="4" contrast="0" strokeVariation="1" armStyle="1" letterform="1" midline="1" xHeight="1"/></hh:font>' +
'<hh:font id="1" face="함초롬바탕" type="TTF" isEmbedded="0"><hh:typeInfo familyType="FCAT_GOTHIC" weight="6" proportion="4" contrast="0" strokeVariation="1" armStyle="1" letterform="1" midline="1" xHeight="1"/></hh:font>' +
'</hh:fontface>' +
'<hh:fontface lang="USER" fontCnt="2">' +
'<hh:font id="0" face="함초롬돋움" type="TTF" isEmbedded="0"><hh:typeInfo familyType="FCAT_GOTHIC" weight="6" proportion="4" contrast="0" strokeVariation="1" armStyle="1" letterform="1" midline="1" xHeight="1"/></hh:font>' +
'<hh:font id="1" face="함초롬바탕" type="TTF" isEmbedded="0"><hh:typeInfo familyType="FCAT_GOTHIC" weight="6" proportion="4" contrast="0" strokeVariation="1" armStyle="1" letterform="1" midline="1" xHeight="1"/></hh:font>' +
'</hh:fontface>' +
'</hh:fontfaces>' +
// ── borderFill
'<hh:borderFills itemCnt="2">' +
'<hh:borderFill id="1" threeD="0" shadow="0" centerLine="NONE" breakCellSeparateLine="0">' +
'<hh:slash type="NONE" Crooked="0" isCounter="0"/>' +
'<hh:backSlash type="NONE" Crooked="0" isCounter="0"/>' +
'<hh:leftBorder type="NONE" width="0.1 mm" color="#000000"/>' +
'<hh:rightBorder type="NONE" width="0.1 mm" color="#000000"/>' +
'<hh:topBorder type="NONE" width="0.1 mm" color="#000000"/>' +
'<hh:bottomBorder type="NONE" width="0.1 mm" color="#000000"/>' +
'<hh:diagonal type="SOLID" width="0.1 mm" color="#000000"/>' +
'</hh:borderFill>' +
'<hh:borderFill id="2" threeD="0" shadow="0" centerLine="NONE" breakCellSeparateLine="0">' +
'<hh:slash type="NONE" Crooked="0" isCounter="0"/>' +
'<hh:backSlash type="NONE" Crooked="0" isCounter="0"/>' +
'<hh:leftBorder type="NONE" width="0.1 mm" color="#000000"/>' +
'<hh:rightBorder type="NONE" width="0.1 mm" color="#000000"/>' +
'<hh:topBorder type="NONE" width="0.1 mm" color="#000000"/>' +
'<hh:bottomBorder type="NONE" width="0.1 mm" color="#000000"/>' +
'<hh:diagonal type="SOLID" width="0.1 mm" color="#000000"/>' +
'<hc:fillBrush><hc:winBrush faceColor="none" hatchColor="#999999" alpha="0"/></hc:fillBrush>' +
'</hh:borderFill>' +
'</hh:borderFills>' +
// ── charProperties (Skeleton 7개 그대로)
'<hh:charProperties itemCnt="7">' +
'<hh:charPr id="0" height="1000" textColor="#000000" shadeColor="none" useFontSpace="0" useKerning="0" symMark="NONE" borderFillIDRef="2">' +
'<hh:fontRef hangul="1" latin="1" hanja="1" japanese="1" other="1" symbol="1" user="1"/>' +
'<hh:ratio hangul="100" latin="100" hanja="100" japanese="100" other="100" symbol="100" user="100"/>' +
'<hh:spacing hangul="0" latin="0" hanja="0" japanese="0" other="0" symbol="0" user="0"/>' +
'<hh:relSz hangul="100" latin="100" hanja="100" japanese="100" other="100" symbol="100" user="100"/>' +
'<hh:offset hangul="0" latin="0" hanja="0" japanese="0" other="0" symbol="0" user="0"/>' +
'<hh:underline type="NONE" shape="SOLID" color="#000000"/>' +
'<hh:strikeout shape="NONE" color="#000000"/>' +
'<hh:outline type="NONE"/>' +
'<hh:shadow type="NONE" color="#C0C0C0" offsetX="10" offsetY="10"/>' +
'</hh:charPr>' +
'<hh:charPr id="1" height="1000" textColor="#000000" shadeColor="none" useFontSpace="0" useKerning="0" symMark="NONE" borderFillIDRef="2">' +
'<hh:fontRef hangul="0" latin="0" hanja="0" japanese="0" other="0" symbol="0" user="0"/>' +
'<hh:ratio hangul="100" latin="100" hanja="100" japanese="100" other="100" symbol="100" user="100"/>' +
'<hh:spacing hangul="0" latin="0" hanja="0" japanese="0" other="0" symbol="0" user="0"/>' +
'<hh:relSz hangul="100" latin="100" hanja="100" japanese="100" other="100" symbol="100" user="100"/>' +
'<hh:offset hangul="0" latin="0" hanja="0" japanese="0" other="0" symbol="0" user="0"/>' +
'<hh:underline type="NONE" shape="SOLID" color="#000000"/>' +
'<hh:strikeout shape="NONE" color="#000000"/>' +
'<hh:outline type="NONE"/>' +
'<hh:shadow type="NONE" color="#C0C0C0" offsetX="10" offsetY="10"/>' +
'</hh:charPr>' +
'<hh:charPr id="2" height="900" textColor="#000000" shadeColor="none" useFontSpace="0" useKerning="0" symMark="NONE" borderFillIDRef="2">' +
'<hh:fontRef hangul="0" latin="0" hanja="0" japanese="0" other="0" symbol="0" user="0"/>' +
'<hh:ratio hangul="100" latin="100" hanja="100" japanese="100" other="100" symbol="100" user="100"/>' +
'<hh:spacing hangul="0" latin="0" hanja="0" japanese="0" other="0" symbol="0" user="0"/>' +
'<hh:relSz hangul="100" latin="100" hanja="100" japanese="100" other="100" symbol="100" user="100"/>' +
'<hh:offset hangul="0" latin="0" hanja="0" japanese="0" other="0" symbol="0" user="0"/>' +
'<hh:underline type="NONE" shape="SOLID" color="#000000"/>' +
'<hh:strikeout shape="NONE" color="#000000"/>' +
'<hh:outline type="NONE"/>' +
'<hh:shadow type="NONE" color="#C0C0C0" offsetX="10" offsetY="10"/>' +
'</hh:charPr>' +
'<hh:charPr id="3" height="900" textColor="#000000" shadeColor="none" useFontSpace="0" useKerning="0" symMark="NONE" borderFillIDRef="2">' +
'<hh:fontRef hangul="1" latin="1" hanja="1" japanese="1" other="1" symbol="1" user="1"/>' +
'<hh:ratio hangul="100" latin="100" hanja="100" japanese="100" other="100" symbol="100" user="100"/>' +
'<hh:spacing hangul="0" latin="0" hanja="0" japanese="0" other="0" symbol="0" user="0"/>' +
'<hh:relSz hangul="100" latin="100" hanja="100" japanese="100" other="100" symbol="100" user="100"/>' +
'<hh:offset hangul="0" latin="0" hanja="0" japanese="0" other="0" symbol="0" user="0"/>' +
'<hh:underline type="NONE" shape="SOLID" color="#000000"/>' +
'<hh:strikeout shape="NONE" color="#000000"/>' +
'<hh:outline type="NONE"/>' +
'<hh:shadow type="NONE" color="#C0C0C0" offsetX="10" offsetY="10"/>' +
'</hh:charPr>' +
'<hh:charPr id="4" height="900" textColor="#000000" shadeColor="none" useFontSpace="0" useKerning="0" symMark="NONE" borderFillIDRef="2">' +
'<hh:fontRef hangul="0" latin="0" hanja="0" japanese="0" other="0" symbol="0" user="0"/>' +
'<hh:ratio hangul="100" latin="100" hanja="100" japanese="100" other="100" symbol="100" user="100"/>' +
'<hh:spacing hangul="-5" latin="-5" hanja="-5" japanese="-5" other="-5" symbol="-5" user="-5"/>' +
'<hh:relSz hangul="100" latin="100" hanja="100" japanese="100" other="100" symbol="100" user="100"/>' +
'<hh:offset hangul="0" latin="0" hanja="0" japanese="0" other="0" symbol="0" user="0"/>' +
'<hh:underline type="NONE" shape="SOLID" color="#000000"/>' +
'<hh:strikeout shape="NONE" color="#000000"/>' +
'<hh:outline type="NONE"/>' +
'<hh:shadow type="NONE" color="#C0C0C0" offsetX="10" offsetY="10"/>' +
'</hh:charPr>' +
// charPr id=5: 16pt 파란색 (제목용으로 활용)
'<hh:charPr id="5" height="1600" textColor="#2E74B5" shadeColor="none" useFontSpace="0" useKerning="0" symMark="NONE" borderFillIDRef="2">' +
'<hh:fontRef hangul="0" latin="0" hanja="0" japanese="0" other="0" symbol="0" user="0"/>' +
'<hh:ratio hangul="100" latin="100" hanja="100" japanese="100" other="100" symbol="100" user="100"/>' +
'<hh:spacing hangul="0" latin="0" hanja="0" japanese="0" other="0" symbol="0" user="0"/>' +
'<hh:relSz hangul="100" latin="100" hanja="100" japanese="100" other="100" symbol="100" user="100"/>' +
'<hh:offset hangul="0" latin="0" hanja="0" japanese="0" other="0" symbol="0" user="0"/>' +
'<hh:underline type="NONE" shape="SOLID" color="#000000"/>' +
'<hh:strikeout shape="NONE" color="#000000"/>' +
'<hh:outline type="NONE"/>' +
'<hh:shadow type="NONE" color="#C0C0C0" offsetX="10" offsetY="10"/>' +
'</hh:charPr>' +
// charPr id=6: 11pt 검정
'<hh:charPr id="6" height="1100" textColor="#000000" shadeColor="none" useFontSpace="0" useKerning="0" symMark="NONE" borderFillIDRef="2">' +
'<hh:fontRef hangul="0" latin="0" hanja="0" japanese="0" other="0" symbol="0" user="0"/>' +
'<hh:ratio hangul="100" latin="100" hanja="100" japanese="100" other="100" symbol="100" user="100"/>' +
'<hh:spacing hangul="0" latin="0" hanja="0" japanese="0" other="0" symbol="0" user="0"/>' +
'<hh:relSz hangul="100" latin="100" hanja="100" japanese="100" other="100" symbol="100" user="100"/>' +
'<hh:offset hangul="0" latin="0" hanja="0" japanese="0" other="0" symbol="0" user="0"/>' +
'<hh:underline type="NONE" shape="SOLID" color="#000000"/>' +
'<hh:strikeout shape="NONE" color="#000000"/>' +
'<hh:outline type="NONE"/>' +
'<hh:shadow type="NONE" color="#C0C0C0" offsetX="10" offsetY="10"/>' +
'</hh:charPr>' +
'</hh:charProperties>' +
// ── tabProperties
'<hh:tabProperties itemCnt="3">' +
'<hh:tabPr id="0" autoTabLeft="0" autoTabRight="0"/>' +
'<hh:tabPr id="1" autoTabLeft="1" autoTabRight="0"/>' +
'<hh:tabPr id="2" autoTabLeft="0" autoTabRight="1"/>' +
'</hh:tabProperties>' +
// ── numberings (Skeleton 그대로)
'<hh:numberings itemCnt="1">' +
'<hh:numbering id="1" start="0">' +
'<hh:paraHead start="1" level="1" align="LEFT" useInstWidth="1" autoIndent="1" widthAdjust="0" textOffsetType="PERCENT" textOffset="50" numFormat="DIGIT" charPrIDRef="4294967295" checkable="0">^1.</hh:paraHead>' +
'<hh:paraHead start="1" level="2" align="LEFT" useInstWidth="1" autoIndent="1" widthAdjust="0" textOffsetType="PERCENT" textOffset="50" numFormat="HANGUL_SYLLABLE" charPrIDRef="4294967295" checkable="0">^2.</hh:paraHead>' +
'<hh:paraHead start="1" level="3" align="LEFT" useInstWidth="1" autoIndent="1" widthAdjust="0" textOffsetType="PERCENT" textOffset="50" numFormat="DIGIT" charPrIDRef="4294967295" checkable="0">^3)</hh:paraHead>' +
'<hh:paraHead start="1" level="4" align="LEFT" useInstWidth="1" autoIndent="1" widthAdjust="0" textOffsetType="PERCENT" textOffset="50" numFormat="HANGUL_SYLLABLE" charPrIDRef="4294967295" checkable="0">^4)</hh:paraHead>' +
'<hh:paraHead start="1" level="5" align="LEFT" useInstWidth="1" autoIndent="1" widthAdjust="0" textOffsetType="PERCENT" textOffset="50" numFormat="DIGIT" charPrIDRef="4294967295" checkable="0">(^5)</hh:paraHead>' +
'<hh:paraHead start="1" level="6" align="LEFT" useInstWidth="1" autoIndent="1" widthAdjust="0" textOffsetType="PERCENT" textOffset="50" numFormat="HANGUL_SYLLABLE" charPrIDRef="4294967295" checkable="0">(^6)</hh:paraHead>' +
'<hh:paraHead start="1" level="7" align="LEFT" useInstWidth="1" autoIndent="1" widthAdjust="0" textOffsetType="PERCENT" textOffset="50" numFormat="CIRCLED_DIGIT" charPrIDRef="4294967295" checkable="1">^7</hh:paraHead>' +
'<hh:paraHead start="1" level="8" align="LEFT" useInstWidth="1" autoIndent="1" widthAdjust="0" textOffsetType="PERCENT" textOffset="50" numFormat="CIRCLED_HANGUL_SYLLABLE" charPrIDRef="4294967295" checkable="1">^8</hh:paraHead>' +
'<hh:paraHead start="1" level="9" align="LEFT" useInstWidth="1" autoIndent="1" widthAdjust="0" textOffsetType="PERCENT" textOffset="50" numFormat="HANGUL_JAMO" charPrIDRef="4294967295" checkable="0"/>' +
'<hh:paraHead start="1" level="10" align="LEFT" useInstWidth="1" autoIndent="1" widthAdjust="0" textOffsetType="PERCENT" textOffset="50" numFormat="ROMAN_SMALL" charPrIDRef="4294967295" checkable="1"/>' +
'</hh:numbering>' +
'</hh:numberings>' +
// ── paraProperties (Skeleton id=0만 사용)
'<hh:paraProperties itemCnt="1">' +
'<hh:paraPr id="0" tabPrIDRef="0" condense="0" fontLineHeight="0" snapToGrid="1" suppressLineNumbers="0" checked="0" textDir="LTR">' +
'<hh:align horizontal="JUSTIFY" vertical="BASELINE"/>' +
'<hh:heading type="NONE" idRef="0" level="0"/>' +
'<hh:breakSetting breakLatinWord="KEEP_WORD" breakNonLatinWord="BREAK_WORD" widowOrphan="0" keepWithNext="0" keepLines="0" pageBreakBefore="0" lineWrap="BREAK"/>' +
'<hh:autoSpacing eAsianEng="0" eAsianNum="0"/>' +
'<hp:switch>' +
'<hp:case hp:required-namespace="http://www.hancom.co.kr/hwpml/2016/HwpUnitChar">' +
'<hh:margin><hc:intent value="0" unit="HWPUNIT"/><hc:left value="0" unit="HWPUNIT"/><hc:right value="0" unit="HWPUNIT"/><hc:prev value="0" unit="HWPUNIT"/><hc:next value="0" unit="HWPUNIT"/></hh:margin>' +
'<hh:lineSpacing type="PERCENT" value="160" unit="HWPUNIT"/>' +
'</hp:case>' +
'<hp:default>' +
'<hh:margin><hc:intent value="0" unit="HWPUNIT"/><hc:left value="0" unit="HWPUNIT"/><hc:right value="0" unit="HWPUNIT"/><hc:prev value="0" unit="HWPUNIT"/><hc:next value="0" unit="HWPUNIT"/></hh:margin>' +
'<hh:lineSpacing type="PERCENT" value="160" unit="HWPUNIT"/>' +
'</hp:default>' +
'</hp:switch>' +
'<hh:border borderFillIDRef="2" offsetLeft="0" offsetRight="0" offsetTop="0" offsetBottom="0" connect="0" ignoreMargin="0"/>' +
'</hh:paraPr>' +
'</hh:paraProperties>' +
// ── styles (Skeleton 바탕글 스타일만)
'<hh:styles itemCnt="1">' +
'<hh:style id="0" type="PARA" name="바탕글" engName="Normal" paraPrIDRef="0" charPrIDRef="0" nextStyleIDRef="0" langID="1042" lockForm="0"/>' +
'</hh:styles>' +
'</hh:refList>' +
'<hh:compatibleDocument targetProgram="HWP201X"><hh:layoutCompatibility/></hh:compatibleDocument>' +
'<hh:docOption><hh:linkinfo path="" pageInherit="0" footnoteInherit="0"/></hh:docOption>' +
'<hh:metaTag>{"name":""}</hh:metaTag>' +
'<hh:trackchageConfig flags="56"/>' +
'</hh:head>';
// ─── 공통 네임스페이스 선언 (section0.xml 루트 속성) ─────────
const SEC_NS =
'xmlns:ha="http://www.hancom.co.kr/hwpml/2011/app" ' +
'xmlns:hp="http://www.hancom.co.kr/hwpml/2011/paragraph" ' +
'xmlns:hp10="http://www.hancom.co.kr/hwpml/2016/paragraph" ' +
'xmlns:hs="http://www.hancom.co.kr/hwpml/2011/section" ' +
'xmlns:hc="http://www.hancom.co.kr/hwpml/2011/core" ' +
'xmlns:hh="http://www.hancom.co.kr/hwpml/2011/head" ' +
'xmlns:hhs="http://www.hancom.co.kr/hwpml/2011/history" ' +
'xmlns:hm="http://www.hancom.co.kr/hwpml/2011/master-page" ' +
'xmlns:hpf="http://www.hancom.co.kr/schema/2011/hpf" ' +
'xmlns:dc="http://purl.org/dc/elements/1.1/" ' +
'xmlns:opf="http://www.idpf.org/2007/opf/" ' +
'xmlns:ooxmlchart="http://www.hancom.co.kr/hwpml/2016/ooxmlchart" ' +
'xmlns:hwpunitchar="http://www.hancom.co.kr/hwpml/2016/HwpUnitChar" ' +
'xmlns:epub="http://www.idpf.org/2007/ops" ' +
'xmlns:config="urn:oasis:names:tc:opendocument:xmlns:config:1.0"';
// ─── section0.xml 본문 빌더 ──────────────────────────────────
/**
* 단락: secPr ( ) + colPr
* Skeleton.hwpx의
*/
function buildSecPrParagraph(): string {
const id = nextId();
return (
`<hp:p id="${id}" paraPrIDRef="0" styleIDRef="0" pageBreak="0" columnBreak="0" merged="0">` +
'<hp:run charPrIDRef="0">' +
'<hp:secPr id="" textDirection="HORIZONTAL" spaceColumns="1134" tabStop="8000" ' +
'tabStopVal="4000" tabStopUnit="HWPUNIT" outlineShapeIDRef="1" memoShapeIDRef="0" ' +
'textVerticalWidthHead="0" masterPageCnt="0">' +
'<hp:grid lineGrid="0" charGrid="0" wonggojiFormat="0"/>' +
'<hp:startNum pageStartsOn="BOTH" page="0" pic="0" tbl="0" equation="0"/>' +
'<hp:visibility hideFirstHeader="0" hideFirstFooter="0" hideFirstMasterPage="0" ' +
'border="SHOW_ALL" fill="SHOW_ALL" hideFirstPageNum="0" hideFirstEmptyLine="0" showLineNumber="0"/>' +
'<hp:lineNumberShape restartType="0" countBy="0" distance="0" startNumber="0"/>' +
'<hp:pagePr landscape="WIDELY" width="59528" height="84186" gutterType="LEFT_ONLY">' +
'<hp:margin header="4252" footer="4252" gutter="0" left="8504" right="8504" top="5668" bottom="4252"/>' +
'</hp:pagePr>' +
'<hp:footNotePr>' +
'<hp:autoNumFormat type="DIGIT" userChar="" prefixChar="" suffixChar=")" supscript="0"/>' +
'<hp:noteLine length="-1" type="SOLID" width="0.12 mm" color="#000000"/>' +
'<hp:noteSpacing betweenNotes="283" belowLine="567" aboveLine="850"/>' +
'<hp:numbering type="CONTINUOUS" newNum="1"/>' +
'<hp:placement place="EACH_COLUMN" beneathText="0"/>' +
'</hp:footNotePr>' +
'<hp:endNotePr>' +
'<hp:autoNumFormat type="DIGIT" userChar="" prefixChar="" suffixChar=")" supscript="0"/>' +
'<hp:noteLine length="14692344" type="SOLID" width="0.12 mm" color="#000000"/>' +
'<hp:noteSpacing betweenNotes="0" belowLine="567" aboveLine="850"/>' +
'<hp:numbering type="CONTINUOUS" newNum="1"/>' +
'<hp:placement place="END_OF_DOCUMENT" beneathText="0"/>' +
'</hp:endNotePr>' +
'<hp:pageBorderFill type="BOTH" borderFillIDRef="1" textBorder="PAPER" headerInside="0" footerInside="0" fillArea="PAPER">' +
'<hp:offset left="1417" right="1417" top="1417" bottom="1417"/>' +
'</hp:pageBorderFill>' +
'<hp:pageBorderFill type="EVEN" borderFillIDRef="1" textBorder="PAPER" headerInside="0" footerInside="0" fillArea="PAPER">' +
'<hp:offset left="1417" right="1417" top="1417" bottom="1417"/>' +
'</hp:pageBorderFill>' +
'<hp:pageBorderFill type="ODD" borderFillIDRef="1" textBorder="PAPER" headerInside="0" footerInside="0" fillArea="PAPER">' +
'<hp:offset left="1417" right="1417" top="1417" bottom="1417"/>' +
'</hp:pageBorderFill>' +
'</hp:secPr>' +
'<hp:ctrl>' +
'<hp:colPr id="" type="NEWSPAPER" layout="LEFT" colCount="1" sameSz="1" sameGap="0"/>' +
'</hp:ctrl>' +
'</hp:run>' +
'<hp:run charPrIDRef="0"><hp:t/></hp:run>' +
'<hp:linesegarray>' +
'<hp:lineseg textpos="0" vertpos="0" vertsize="1000" textheight="1000" baseline="850" ' +
'spacing="600" horzpos="0" horzsize="42520" flags="393216"/>' +
'</hp:linesegarray>' +
'</hp:p>'
);
}
/**
* (charPrIDRef=0 10pt)
*/
function buildPara(text: string, charPrId = 0): string {
const id = nextId();
return (
`<hp:p id="${id}" paraPrIDRef="0" styleIDRef="0" pageBreak="0" columnBreak="0" merged="0">` +
`<hp:run charPrIDRef="${charPrId}">` +
`<hp:t>${esc(text)}</hp:t>` +
'</hp:run>' +
'</hp:p>'
);
}
/**
* ()
*/
function buildEmptyPara(): string {
const id = nextId();
return (
`<hp:p id="${id}" paraPrIDRef="0" styleIDRef="0" pageBreak="0" columnBreak="0" merged="0">` +
'<hp:run charPrIDRef="0"><hp:t/></hp:run>' +
'</hp:p>'
);
}
/**
* (subList )
*/
function buildCellPara(text: string): string {
const id = nextId();
return (
`<hp:p id="${id}" paraPrIDRef="0" styleIDRef="0" pageBreak="0" columnBreak="0" merged="0">` +
'<hp:run charPrIDRef="0">' +
`<hp:t>${esc(text)}</hp:t>` +
'</hp:run>' +
'</hp:p>'
);
}
/**
*
* OWPML : <hp:tc> 순서: subList cellAddr cellSpan cellSz cellMargin
*/
function buildCell(
text: string,
colAddr: number,
rowAddr: number,
colSpan: number,
rowSpan: number,
width: number,
isHeader = false,
): string {
return (
`<hp:tc name="" header="${isHeader ? '1' : '0'}" hasMargin="0" protect="0" editable="0" dirty="0" borderFillIDRef="2">` +
`<hp:subList id="" textDirection="HORIZONTAL" lineWrap="BREAK" vertAlign="CENTER" ` +
`linkListIDRef="0" linkListNextIDRef="0" textWidth="0" textHeight="0" hasTextRef="0" hasNumRef="0">` +
buildCellPara(text) +
'</hp:subList>' +
`<hp:cellAddr colAddr="${colAddr}" rowAddr="${rowAddr}"/>` +
`<hp:cellSpan colSpan="${colSpan}" rowSpan="${rowSpan}"/>` +
`<hp:cellSz width="${width}" height="564"/>` +
'<hp:cellMargin left="510" right="510" top="141" bottom="141"/>' +
'</hp:tc>'
);
}
/**
* 2 ( | )
* A4 42520 HWPUNIT :
* : 33% = 14032, : 67% = 28488
*/
const CONTENT_WIDTH = 42520;
const LABEL_WIDTH = Math.round(CONTENT_WIDTH * 0.33);
const VALUE_WIDTH = CONTENT_WIDTH - LABEL_WIDTH;
function buildFieldTable(
fields: { key: string; label: string }[],
getVal: (key: string) => string,
): string {
const rowCnt = fields.length;
if (rowCnt === 0) return '';
let rows = '';
fields.forEach((field, rowIdx) => {
const value = getVal(field.key) || '-';
if (field.label) {
// 2열: 라벨 | 값
rows +=
'<hp:tr>' +
buildCell(field.label, 0, rowIdx, 1, 1, LABEL_WIDTH) +
buildCell(value, 1, rowIdx, 1, 1, VALUE_WIDTH) +
'</hp:tr>';
} else {
// 1열 전체 너비 (textarea 류)
rows +=
'<hp:tr>' +
buildCell(value, 0, rowIdx, 2, 1, CONTENT_WIDTH) +
'</hp:tr>';
}
});
const colCnt = 2;
const pId = nextId();
const tblId = nextId();
// 테이블 높이 추정: 각 행 564 HWPUNIT
const tblHeight = rowCnt * 564;
// 테이블은 <hp:p> > <hp:run> 안에 감싸야 함
// OWPML 스펙: hp:tbl 속성 + 자식(sz, pos, outMargin, inMargin) 후 행
return (
`<hp:p id="${pId}" paraPrIDRef="0" styleIDRef="0" pageBreak="0" columnBreak="0" merged="0">` +
'<hp:run charPrIDRef="0">' +
`<hp:tbl id="${tblId}" zOrder="0" numberingType="TABLE" textWrap="TOP_AND_BOTTOM" ` +
`textFlow="BOTH_SIDES" lock="0" dropcapstyle="None" pageBreak="CELL" ` +
`repeatHeader="0" rowCnt="${rowCnt}" colCnt="${colCnt}" cellSpacing="0" ` +
`borderFillIDRef="2" noAdjust="0">` +
`<hp:sz width="${CONTENT_WIDTH}" height="${tblHeight}" widthRelTo="ABSOLUTE" heightRelTo="ABSOLUTE" protect="0"/>` +
'<hp:pos treatAsChar="1" affectLSpacing="0" flowWithText="1" allowOverlap="0" ' +
'holdAnchorAndSO="0" vertRelTo="PARA" horzRelTo="COLUMN" ' +
'vertAlign="TOP" horzAlign="LEFT" vertOffset="0" horzOffset="0"/>' +
'<hp:outMargin left="0" right="0" top="0" bottom="0"/>' +
'<hp:inMargin left="0" right="0" top="0" bottom="0"/>' +
rows +
'</hp:tbl>' +
'</hp:run>' +
'</hp:p>'
);
}
/**
* section0.xml
*/
function buildSection0Xml(
templateLabel: string,
meta: ReportMeta,
sections: ReportSection[],
getVal: (key: string) => string,
): string {
// ID 시퀀스 초기화 (재사용 시 충돌 방지)
_idSeq = 1000000000;
let body = '';
// 1) 첫 단락: secPr (필수 - 페이지 설정 포함)
body += buildSecPrParagraph();
// 2) 메인 제목 (16pt 파란색 = charPrId 5)
body += buildPara('해양오염방제지원시스템', 5);
// 3) 부제목 (11pt = charPrId 6)
body += buildPara(templateLabel, 6);
// 4) 메타 정보 (기본 10pt = charPrId 0)
const metaLine =
`작성일시: ${meta.writeTime}` +
` | 작성자: ${meta.author || '-'}` +
` | 관할: ${meta.jurisdiction}`;
body += buildPara(metaLine, 0);
// 5) 빈 줄
body += buildEmptyPara();
// 6) 각 섹션
for (const section of sections) {
// 섹션 제목 (11pt = charPrId 6)
body += buildPara(section.title, 6);
// 필드 테이블
if (section.fields.length > 0) {
body += buildFieldTable(section.fields, getVal);
}
// 섹션 후 빈 줄
body += buildEmptyPara();
}
return (
'<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>' +
`<hs:sec ${SEC_NS}>` +
body +
'</hs:sec>'
);
}
/**
* Preview/PrvText.txt ( )
*/
function buildPrvText(
templateLabel: string,
meta: ReportMeta,
sections: ReportSection[],
getVal: (key: string) => string,
): string {
const lines: string[] = [];
lines.push('해양오염방제지원시스템');
lines.push(templateLabel);
lines.push(`작성일시: ${meta.writeTime} | 작성자: ${meta.author || '-'} | 관할: ${meta.jurisdiction}`);
lines.push('');
for (const section of sections) {
lines.push(`[${section.title}]`);
for (const field of section.fields) {
const value = getVal(field.key) || '-';
if (field.label) {
lines.push(` ${field.label}: ${value}`);
} else {
lines.push(` ${value}`);
}
}
lines.push('');
}
return lines.join('\n');
}
// ─── 메인 Export 함수 ────────────────────────────────────────
export async function exportAsHWPX(
templateLabel: string,
meta: ReportMeta,
sections: ReportSection[],
getVal: (key: string) => string,
filename: string,
): Promise<void> {
const zip = new JSZip();
// mimetype: STORE (무압축) + 첫 번째 항목
zip.file('mimetype', MIMETYPE, { compression: 'STORE' });
// 정적 메타 파일
zip.file('version.xml', VERSION_XML);
zip.file('settings.xml', SETTINGS_XML);
zip.file('META-INF/container.xml', CONTAINER_XML);
zip.file('META-INF/container.rdf', CONTAINER_RDF);
zip.file('META-INF/manifest.xml', MANIFEST_XML);
// Contents
zip.file('Contents/content.hpf', buildContentHpf());
zip.file('Contents/header.xml', HEADER_XML);
zip.file('Contents/section0.xml', buildSection0Xml(templateLabel, meta, sections, getVal));
// Preview
zip.file('Preview/PrvText.txt', buildPrvText(templateLabel, meta, sections, getVal));
// ZIP 생성 및 다운로드
const blob = await zip.generateAsync({
type: 'blob',
mimeType: 'application/hwp+zip',
compression: 'DEFLATE',
compressionOptions: { level: 6 },
});
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${filename}.hwpx`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}

파일 보기

@ -41,16 +41,15 @@ export function exportAsPDF(html: string, _filename: string) {
setTimeout(() => URL.revokeObjectURL(url), 30000)
}
export function exportAsHWP(html: string, filename: string) {
const blob = new Blob([html], { type: 'application/msword;charset=utf-8' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `${filename}.doc`
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
URL.revokeObjectURL(url)
export async function exportAsHWP(
templateLabel: string,
meta: { writeTime: string; author: string; jurisdiction: string },
sections: { title: string; fields: { key: string; label: string }[] }[],
getVal: (key: string) => string,
filename: string,
) {
const { exportAsHWPX } = await import('./hwpxExport');
await exportAsHWPX(templateLabel, meta, sections, getVal, filename);
}
export type ViewState =