release: 2026-03-26 (5건 커밋) #128
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"applied_global_version": "1.6.1",
|
"applied_global_version": "1.6.1",
|
||||||
"applied_date": "2026-03-25",
|
"applied_date": "2026-03-26",
|
||||||
"project_type": "react-ts",
|
"project_type": "react-ts",
|
||||||
"gitea_url": "https://gitea.gc-si.dev",
|
"gitea_url": "https://gitea.gc-si.dev",
|
||||||
"custom_pre_commit": true
|
"custom_pre_commit": true
|
||||||
|
|||||||
@ -183,7 +183,7 @@ export async function listAnalyses(input: ListAnalysesInput): Promise<Prediction
|
|||||||
GROUP BY ACDNT_SN
|
GROUP BY ACDNT_SN
|
||||||
) B ON B.ACDNT_SN = A.ACDNT_SN
|
) B ON B.ACDNT_SN = A.ACDNT_SN
|
||||||
${whereClause}
|
${whereClause}
|
||||||
ORDER BY A.OCCRN_DTM DESC, P.RUN_DTM DESC NULLS LAST
|
ORDER BY P.RUN_DTM DESC NULLS LAST, A.OCCRN_DTM DESC
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const { rows } = await wingPool.query(sql, params);
|
const { rows } = await wingPool.query(sql, params);
|
||||||
|
|||||||
@ -4,6 +4,15 @@
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [2026-03-26]
|
||||||
|
|
||||||
|
### 추가
|
||||||
|
- 보고서: 조위/기상(oil-tide) 섹션에 실데이터 렌더링 추가 (풍향/풍속·파고·수온·유향 등)
|
||||||
|
|
||||||
|
### 수정
|
||||||
|
- 보고서: HWPX 이미지 내보내기 구조를 HWPX 스펙(hc:img + manifest 방식)으로 수정
|
||||||
|
- 확산예측: 분석 목록 정렬 기준 변경 (RUN_DTM DESC 우선)
|
||||||
|
|
||||||
## [2026-03-25.2]
|
## [2026-03-25.2]
|
||||||
|
|
||||||
### 추가
|
### 추가
|
||||||
|
|||||||
@ -206,6 +206,32 @@ function ReportGenerator({ onSave }: ReportGeneratorProps) {
|
|||||||
content = `${simBanner}<table style="width:100%;border-collapse:collapse;font-size:12px;">${trs}</table>`;
|
content = `${simBanner}<table style="width:100%;border-collapse:collapse;font-size:12px;">${trs}</table>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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 = `<p style="font-size:11px;font-weight:bold;color:#0891b2;margin-bottom:8px;">${stationLabel}${capturedAt ? ` <span style="font-size:10px;color:#999;font-weight:normal;">수집: ${capturedAt}</span>` : ''}</p>`;
|
||||||
|
const trs = rows.map(r =>
|
||||||
|
`<tr><td style="padding:6px 8px;border:1px solid #ddd;color:#888;width:100px;">${r[0]}</td><td style="padding:6px 8px;border:1px solid #ddd;font-weight:bold;">${r[1]}</td></tr>`
|
||||||
|
).join('');
|
||||||
|
content = `${headerHtml}<table style="width:100%;border-collapse:collapse;font-size:12px;">${trs}</table>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// HNS 섹션에 실 데이터 삽입
|
// HNS 섹션에 실 데이터 삽입
|
||||||
if (activeCat === 1 && hnsPayload) {
|
if (activeCat === 1 && hnsPayload) {
|
||||||
|
|||||||
@ -498,17 +498,40 @@ function buildEmptyPara(): string {
|
|||||||
function buildPicParagraph(binDataId: number, widthHwp: number, heightHwp: number): string {
|
function buildPicParagraph(binDataId: number, widthHwp: number, heightHwp: number): string {
|
||||||
const pId = nextId();
|
const pId = nextId();
|
||||||
const picId = nextId();
|
const picId = nextId();
|
||||||
|
const fileId = `image${binDataId}`;
|
||||||
|
const halfW = Math.round(widthHwp / 2);
|
||||||
|
const halfH = Math.round(heightHwp / 2);
|
||||||
return (
|
return (
|
||||||
`<hp:p id="${pId}" paraPrIDRef="0" styleIDRef="0" pageBreak="0" columnBreak="0" merged="0">` +
|
`<hp:p id="${pId}" paraPrIDRef="0" styleIDRef="0" pageBreak="0" columnBreak="0" merged="0">` +
|
||||||
'<hp:run charPrIDRef="0">' +
|
'<hp:run charPrIDRef="0">' +
|
||||||
`<hp:pic id="${picId}" zOrder="0" numberingType="FIGURE" textWrap="FLOAT" ` +
|
`<hp:pic id="${picId}" zOrder="0" numberingType="PICTURE" textWrap="TOP_AND_BOTTOM" ` +
|
||||||
`textFlow="BOTH_SIDES" lock="0" dropcapstyle="None" pageBreak="CELL">` +
|
`textFlow="BOTH_SIDES" lock="0" dropcapstyle="None" href="" groupLevel="0" instid="${picId}" reverse="0">` +
|
||||||
`<hp:sz width="${widthHwp}" height="${heightHwp}" widthRelTo="ABSOLUTE" heightRelTo="ABSOLUTE" protect="0"/>` +
|
'<hp:offset x="0" y="0"/>' +
|
||||||
|
`<hp:orgSz width="${widthHwp}" height="${heightHwp}"/>` +
|
||||||
|
`<hp:curSz width="${widthHwp}" height="${heightHwp}"/>` +
|
||||||
|
'<hp:flip horizontal="0" vertical="0"/>' +
|
||||||
|
`<hp:rotationInfo angle="0" centerX="${halfW}" centerY="${halfH}" rotateimage="1"/>` +
|
||||||
|
'<hp:renderingInfo>' +
|
||||||
|
'<hc:transMatrix e1="1" e2="0" e3="0" e4="0" e5="1" e6="0"/>' +
|
||||||
|
'<hc:scaMatrix e1="1" e2="0" e3="0" e4="0" e5="1" e6="0"/>' +
|
||||||
|
'<hc:rotMatrix e1="1" e2="0" e3="0" e4="0" e5="1" e6="0"/>' +
|
||||||
|
'</hp:renderingInfo>' +
|
||||||
|
'<hp:imgRect>' +
|
||||||
|
`<hc:pt0 x="0" y="0"/>` +
|
||||||
|
`<hc:pt1 x="${widthHwp}" y="0"/>` +
|
||||||
|
`<hc:pt2 x="${widthHwp}" y="${heightHwp}"/>` +
|
||||||
|
`<hc:pt3 x="0" y="${heightHwp}"/>` +
|
||||||
|
'</hp:imgRect>' +
|
||||||
|
`<hp:imgClip left="0" right="${widthHwp}" top="0" bottom="${heightHwp}"/>` +
|
||||||
|
'<hp:inMargin left="0" right="0" top="0" bottom="0"/>' +
|
||||||
|
`<hp:imgDim dimwidth="${widthHwp}" dimheight="${heightHwp}"/>` +
|
||||||
|
`<hc:img binaryItemIDRef="${fileId}" bright="0" contrast="0" effect="REAL_PIC" alpha="0"/>` +
|
||||||
|
'<hp:effects/>' +
|
||||||
|
`<hp:sz width="${widthHwp}" widthRelTo="ABSOLUTE" height="${heightHwp}" heightRelTo="ABSOLUTE" protect="0"/>` +
|
||||||
'<hp:pos treatAsChar="1" affectLSpacing="0" flowWithText="1" allowOverlap="0" ' +
|
'<hp:pos treatAsChar="1" affectLSpacing="0" flowWithText="1" allowOverlap="0" ' +
|
||||||
'holdAnchorAndSO="0" vertRelTo="PARA" horzRelTo="COLUMN" ' +
|
'holdAnchorAndSO="0" vertRelTo="PARA" horzRelTo="PARA" ' +
|
||||||
'vertAlign="TOP" horzAlign="LEFT" vertOffset="0" horzOffset="0"/>' +
|
'vertAlign="TOP" horzAlign="LEFT" vertOffset="0" horzOffset="0"/>' +
|
||||||
'<hp:outMargin left="0" right="0" top="0" bottom="0"/>' +
|
'<hp:outMargin left="0" right="0" top="0" bottom="0"/>' +
|
||||||
`<hp:img binDataIDRef="${binDataId}" effect="REAL_PIC" alpha="0"/>` +
|
|
||||||
'</hp:pic>' +
|
'</hp:pic>' +
|
||||||
'</hp:run>' +
|
'</hp:run>' +
|
||||||
'</hp:p>'
|
'</hp:p>'
|
||||||
@ -879,8 +902,6 @@ export async function exportAsHWPX(
|
|||||||
// 이미지 처리
|
// 이미지 처리
|
||||||
let imageBinIds: { step3?: number; step6?: number; sensitiveMap?: number; sensitivityMap?: number } | undefined;
|
let imageBinIds: { step3?: number; step6?: number; sensitiveMap?: number; sensitivityMap?: number } | undefined;
|
||||||
let extraManifestItems = '';
|
let extraManifestItems = '';
|
||||||
let binDataListXml = '';
|
|
||||||
let binCount = 0;
|
|
||||||
|
|
||||||
const processImage = (src: string, binId: number, fileId: string) => {
|
const processImage = (src: string, binId: number, fileId: string) => {
|
||||||
// 실제 이미지 포맷 감지 (JPEG vs PNG)
|
// 실제 이미지 포맷 감지 (JPEG vs PNG)
|
||||||
@ -890,13 +911,10 @@ export async function exportAsHWPX(
|
|||||||
const filePath = `BinData/image${binId}.${ext}`;
|
const filePath = `BinData/image${binId}.${ext}`;
|
||||||
|
|
||||||
const base64 = src.replace(/^data:image\/\w+;base64,/, '');
|
const base64 = src.replace(/^data:image\/\w+;base64,/, '');
|
||||||
|
// HWPX 스펙: 이미지는 ZIP 루트의 BinData/에 저장 (Contents/ 아래 아님)
|
||||||
zip.file(filePath, base64, { base64: true });
|
zip.file(filePath, base64, { base64: true });
|
||||||
extraManifestItems += `<opf:item id="${fileId}" href="${filePath}" media-type="${mediaType}"/>`;
|
// isEmbeded="1": 한글 HWPX 표준 속성, hc:img binaryItemIDRef가 이 id로 참조
|
||||||
// inMemory="NO": 데이터는 ZIP 내 파일로 저장, 요소 내용은 파일 경로
|
extraManifestItems += `<opf:item id="${fileId}" href="${filePath}" media-type="${mediaType}" isEmbeded="1"/>`;
|
||||||
binDataListXml +=
|
|
||||||
`<hh:binData id="${binId}" isSameDocData="0" compress="NO" inMemory="NO" ` +
|
|
||||||
`doNotCompressFile="0" blockDecompress="0" limitWidth="0" limitHeight="0">${filePath}</hh:binData>`;
|
|
||||||
binCount++;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (images?.step3 || images?.step6) {
|
if (images?.step3 || images?.step6) {
|
||||||
@ -921,16 +939,8 @@ export async function exportAsHWPX(
|
|||||||
processImage(images.sensitivityMap, 4, 'image4');
|
processImage(images.sensitivityMap, 4, 'image4');
|
||||||
}
|
}
|
||||||
|
|
||||||
// header.xml: binDataList를 hh:refList 뒤에 삽입 (HWPML 스펙 준수)
|
// header.xml: hh:binDataList 불필요 — hc:img binaryItemIDRef → content.hpf manifest 방식 사용
|
||||||
let headerXml = HEADER_XML;
|
const headerXml = HEADER_XML;
|
||||||
if (binCount > 0) {
|
|
||||||
const binDataList =
|
|
||||||
`<hh:binDataList itemCnt="${binCount}">` +
|
|
||||||
binDataListXml +
|
|
||||||
'</hh:binDataList>';
|
|
||||||
// refList 닫힘 태그 직후에 삽입 (binDataList는 head의 직접 자식)
|
|
||||||
headerXml = HEADER_XML.replace('</hh:refList>', '</hh:refList>' + binDataList);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Contents
|
// Contents
|
||||||
zip.file('Contents/content.hpf', buildContentHpf(extraManifestItems));
|
zip.file('Contents/content.hpf', buildContentHpf(extraManifestItems));
|
||||||
|
|||||||
불러오는 중...
Reference in New Issue
Block a user