diff --git a/.claude/workflow-version.json b/.claude/workflow-version.json index 863bec2..003eaf0 100644 --- a/.claude/workflow-version.json +++ b/.claude/workflow-version.json @@ -1,6 +1,6 @@ { "applied_global_version": "1.6.1", - "applied_date": "2026-03-25", + "applied_date": "2026-03-26", "project_type": "react-ts", "gitea_url": "https://gitea.gc-si.dev", "custom_pre_commit": true diff --git a/backend/src/prediction/predictionService.ts b/backend/src/prediction/predictionService.ts index 63a6845..2c3dde0 100644 --- a/backend/src/prediction/predictionService.ts +++ b/backend/src/prediction/predictionService.ts @@ -183,7 +183,7 @@ export async function listAnalyses(input: ListAnalysesInput): Promise${trs}`; } } + if (activeCat === 0 && sec.id === 'oil-tide') { + const wx = oilPayload?.weather; + if (wx) { + const stationLabel = weatherSnapshot + ? `${weatherSnapshot.stationName} 조위관측소` + : '조위관측소'; + const capturedAt = weatherSnapshot + ? new Date(weatherSnapshot.capturedAt).toLocaleString('ko-KR', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }) + : ''; + const rows = [ + ['풍향/풍속', `${wx.windDir} / ${wx.windSpeed}`], + ['파고', wx.waveHeight + (wx.waveMaxHeight ? ` (최대 ${wx.waveMaxHeight})` : '')], + ['파주기', wx.wavePeriod ?? '—'], + ['수온', wx.temp], + ['기압', wx.pressure ?? '—'], + ['시정', wx.visibility ?? '—'], + ['염분', wx.salinity ?? '—'], + ...(wx.currentDir ? [['유향/유속', `${wx.currentDir} / ${wx.currentSpeed ?? '—'}`]] : []), + ]; + const headerHtml = `

${stationLabel}${capturedAt ? ` 수집: ${capturedAt}` : ''}

`; + const trs = rows.map(r => + `${r[0]}${r[1]}` + ).join(''); + content = `${headerHtml}${trs}
`; + } + } // HNS 섹션에 실 데이터 삽입 if (activeCat === 1 && hnsPayload) { diff --git a/frontend/src/tabs/reports/components/hwpxExport.ts b/frontend/src/tabs/reports/components/hwpxExport.ts index e708afd..7ef5814 100644 --- a/frontend/src/tabs/reports/components/hwpxExport.ts +++ b/frontend/src/tabs/reports/components/hwpxExport.ts @@ -498,17 +498,40 @@ function buildEmptyPara(): string { function buildPicParagraph(binDataId: number, widthHwp: number, heightHwp: number): string { const pId = nextId(); const picId = nextId(); + const fileId = `image${binDataId}`; + const halfW = Math.round(widthHwp / 2); + const halfH = Math.round(heightHwp / 2); return ( `` + '' + - `` + - `` + + `` + + '' + + `` + + `` + + '' + + `` + + '' + + '' + + '' + + '' + + '' + + '' + + `` + + `` + + `` + + `` + + '' + + `` + + '' + + `` + + `` + + '' + + `` + '' + '' + - `` + '' + '' + '' @@ -879,8 +902,6 @@ export async function exportAsHWPX( // 이미지 처리 let imageBinIds: { step3?: number; step6?: number; sensitiveMap?: number; sensitivityMap?: number } | undefined; let extraManifestItems = ''; - let binDataListXml = ''; - let binCount = 0; const processImage = (src: string, binId: number, fileId: string) => { // 실제 이미지 포맷 감지 (JPEG vs PNG) @@ -890,13 +911,10 @@ export async function exportAsHWPX( const filePath = `BinData/image${binId}.${ext}`; const base64 = src.replace(/^data:image\/\w+;base64,/, ''); + // HWPX 스펙: 이미지는 ZIP 루트의 BinData/에 저장 (Contents/ 아래 아님) zip.file(filePath, base64, { base64: true }); - extraManifestItems += ``; - // inMemory="NO": 데이터는 ZIP 내 파일로 저장, 요소 내용은 파일 경로 - binDataListXml += - `${filePath}`; - binCount++; + // isEmbeded="1": 한글 HWPX 표준 속성, hc:img binaryItemIDRef가 이 id로 참조 + extraManifestItems += ``; }; if (images?.step3 || images?.step6) { @@ -921,16 +939,8 @@ export async function exportAsHWPX( processImage(images.sensitivityMap, 4, 'image4'); } - // header.xml: binDataList를 hh:refList 뒤에 삽입 (HWPML 스펙 준수) - let headerXml = HEADER_XML; - if (binCount > 0) { - const binDataList = - `` + - binDataListXml + - ''; - // refList 닫힘 태그 직후에 삽입 (binDataList는 head의 직접 자식) - headerXml = HEADER_XML.replace('', '' + binDataList); - } + // header.xml: hh:binDataList 불필요 — hc:img binaryItemIDRef → content.hpf manifest 방식 사용 + const headerXml = HEADER_XML; // Contents zip.file('Contents/content.hpf', buildContentHpf(extraManifestItems));