Compare commits
3 Commits
661d78a225
...
8224cd02f5
| Author | SHA1 | Date |
|---|---|---|
|
|
8224cd02f5 | |
|
|
eb7df53eff | |
|
|
a01aa46f3e |
|
|
@ -47,7 +47,7 @@ window.HANWHA_FORMS = [
|
||||||
{
|
{
|
||||||
"code": "REPORT_HINT",
|
"code": "REPORT_HINT",
|
||||||
"level": "INFO",
|
"level": "INFO",
|
||||||
"text": "batch가 생성한 Excel/PDF를 내려받고 최근 로그를 확인합니다."
|
"text": "배치가 생성한 Excel/PDF를 내려받고 최근 로그를 확인합니다."
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
@ -60,7 +60,7 @@ window.HANWHA_FORMS = [
|
||||||
{
|
{
|
||||||
"code": "UPLOAD_HINT",
|
"code": "UPLOAD_HINT",
|
||||||
"level": "INFO",
|
"level": "INFO",
|
||||||
"text": "invalid 샘플로 오류 시나리오를 확인한 뒤 valid 샘플을 재업로드합니다."
|
"text": "오류 샘플로 오류 시나리오를 확인한 뒤 정상 샘플을 재업로드합니다."
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -436,7 +436,7 @@ function renderUploads() {
|
||||||
<button class="secondary" data-action="reload-uploads">내역 새로고침</button>
|
<button class="secondary" data-action="reload-uploads">내역 새로고침</button>
|
||||||
<button class="ghost" data-action="download-sample" data-sample-code="trial-balance-invalid">오류 샘플</button>
|
<button class="ghost" data-action="download-sample" data-sample-code="trial-balance-invalid">오류 샘플</button>
|
||||||
<button class="ghost" data-action="download-sample" data-sample-code="trial-balance-valid">정상 TB</button>
|
<button class="ghost" data-action="download-sample" data-sample-code="trial-balance-valid">정상 TB</button>
|
||||||
<button class="ghost" data-action="download-sample" data-sample-code="forecast-valid">정상 Forecast</button>
|
<button class="ghost" data-action="download-sample" data-sample-code="forecast-valid">정상 전망</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel">
|
<div class="panel">
|
||||||
|
|
@ -592,7 +592,7 @@ function shellContent(content) {
|
||||||
<aside class="sidebar">
|
<aside class="sidebar">
|
||||||
<div class="brand">
|
<div class="brand">
|
||||||
<h1>Hanwha Nexacro Demo</h1>
|
<h1>Hanwha Nexacro Demo</h1>
|
||||||
<p>Spec driven preview generated from Nexacro DSL</p>
|
<p>Nexacro DSL에서 생성한 미리보기 화면</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="nav-list">${renderNav()}</div>
|
<div class="nav-list">${renderNav()}</div>
|
||||||
<div class="panel" style="margin-top: 18px;">
|
<div class="panel" style="margin-top: 18px;">
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,7 @@ window.HANWHA_FORMS = [
|
||||||
{
|
{
|
||||||
"code": "REPORT_HINT",
|
"code": "REPORT_HINT",
|
||||||
"level": "INFO",
|
"level": "INFO",
|
||||||
"text": "batch가 생성한 Excel/PDF를 내려받고 최근 로그를 확인합니다."
|
"text": "배치가 생성한 Excel/PDF를 내려받고 최근 로그를 확인합니다."
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
@ -60,7 +60,7 @@ window.HANWHA_FORMS = [
|
||||||
{
|
{
|
||||||
"code": "UPLOAD_HINT",
|
"code": "UPLOAD_HINT",
|
||||||
"level": "INFO",
|
"level": "INFO",
|
||||||
"text": "invalid 샘플로 오류 시나리오를 확인한 뒤 valid 샘플을 재업로드합니다."
|
"text": "오류 샘플로 오류 시나리오를 확인한 뒤 정상 샘플을 재업로드합니다."
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,7 @@ this.txLoadReports = function(obj, e)
|
||||||
<Grid id="grdJobLogs" taborder="1" left="36" top="420" width="1368" height="320" binddataset="dsJobLogs">
|
<Grid id="grdJobLogs" taborder="1" left="36" top="420" width="1368" height="320" binddataset="dsJobLogs">
|
||||||
<Formats><Format id="default"><Columns><Column size="228"/><Column size="228"/><Column size="228"/><Column size="228"/><Column size="228"/><Column size="228"/></Columns><Rows><Row size="32" band="head"/><Row size="28"/></Rows><Band id="head"><Cell col="0" text="로그ID"/><Cell col="1" text="작업유형"/><Cell col="2" text="참조ID"/><Cell col="3" text="레벨"/><Cell col="4" text="메시지"/><Cell col="5" text="생성시각"/></Band><Band id="body"><Cell col="0" text="bind:id"/><Cell col="1" text="bind:jobType"/><Cell col="2" text="bind:referenceId"/><Cell col="3" text="bind:logLevel"/><Cell col="4" text="bind:logMessage"/><Cell col="5" text="bind:createdAt"/></Band></Format></Formats>
|
<Formats><Format id="default"><Columns><Column size="228"/><Column size="228"/><Column size="228"/><Column size="228"/><Column size="228"/><Column size="228"/></Columns><Rows><Row size="32" band="head"/><Row size="28"/></Rows><Band id="head"><Cell col="0" text="로그ID"/><Cell col="1" text="작업유형"/><Cell col="2" text="참조ID"/><Cell col="3" text="레벨"/><Cell col="4" text="메시지"/><Cell col="5" text="생성시각"/></Band><Band id="body"><Cell col="0" text="bind:id"/><Cell col="1" text="bind:jobType"/><Cell col="2" text="bind:referenceId"/><Cell col="3" text="bind:logLevel"/><Cell col="4" text="bind:logMessage"/><Cell col="5" text="bind:createdAt"/></Band></Format></Formats>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Static id="staMessage0" taborder="900" left="36" top="824" width="1368" height="20" text="batch가 생성한 Excel/PDF를 내려받고 최근 로그를 확인합니다."/>
|
<Static id="staMessage0" taborder="900" left="36" top="824" width="1368" height="20" text="배치가 생성한 Excel/PDF를 내려받고 최근 로그를 확인합니다."/>
|
||||||
<Layouts>
|
<Layouts>
|
||||||
<Layout width="1440" height="900" screenid="Desktop_screen"/>
|
<Layout width="1440" height="900" screenid="Desktop_screen"/>
|
||||||
</Layouts>
|
</Layouts>
|
||||||
|
|
|
||||||
|
|
@ -67,7 +67,7 @@ this.actUpload = function(obj, e)
|
||||||
<Grid id="grdIssues" taborder="1" left="36" top="486" width="1368" height="300" binddataset="dsIssues">
|
<Grid id="grdIssues" taborder="1" left="36" top="486" width="1368" height="300" binddataset="dsIssues">
|
||||||
<Formats><Format id="default"><Columns><Column size="342"/><Column size="342"/><Column size="342"/><Column size="342"/></Columns><Rows><Row size="32" band="head"/><Row size="28"/></Rows><Band id="head"><Cell col="0" text="행번호"/><Cell col="1" text="오류코드"/><Cell col="2" text="오류메시지"/><Cell col="3" text="등급"/></Band><Band id="body"><Cell col="0" text="bind:rowNumber"/><Cell col="1" text="bind:issueCode"/><Cell col="2" text="bind:issueMessage"/><Cell col="3" text="bind:severityCode"/></Band></Format></Formats>
|
<Formats><Format id="default"><Columns><Column size="342"/><Column size="342"/><Column size="342"/><Column size="342"/></Columns><Rows><Row size="32" band="head"/><Row size="28"/></Rows><Band id="head"><Cell col="0" text="행번호"/><Cell col="1" text="오류코드"/><Cell col="2" text="오류메시지"/><Cell col="3" text="등급"/></Band><Band id="body"><Cell col="0" text="bind:rowNumber"/><Cell col="1" text="bind:issueCode"/><Cell col="2" text="bind:issueMessage"/><Cell col="3" text="bind:severityCode"/></Band></Format></Formats>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Static id="staMessage0" taborder="900" left="36" top="824" width="1368" height="20" text="invalid 샘플로 오류 시나리오를 확인한 뒤 valid 샘플을 재업로드합니다."/>
|
<Static id="staMessage0" taborder="900" left="36" top="824" width="1368" height="20" text="오류 샘플로 오류 시나리오를 확인한 뒤 정상 샘플을 재업로드합니다."/>
|
||||||
<Layouts>
|
<Layouts>
|
||||||
<Layout width="1440" height="900" screenid="Desktop_screen"/>
|
<Layout width="1440" height="900" screenid="Desktop_screen"/>
|
||||||
</Layouts>
|
</Layouts>
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,11 @@ RUN gradle --no-daemon bootJar
|
||||||
FROM eclipse-temurin:21-jre
|
FROM eclipse-temurin:21-jre
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends fonts-nanum \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
COPY --from=builder /workspace/build/libs/*.jar /app/app.jar
|
COPY --from=builder /workspace/build/libs/*.jar /app/app.jar
|
||||||
|
|
||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
ENTRYPOINT ["java", "-jar", "/app/app.jar"]
|
ENTRYPOINT ["java", "-jar", "/app/app.jar"]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -101,7 +101,7 @@ public class ConsolidationService {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
reportService.generateReports(runId, fiscalPeriod, payload);
|
reportService.generateReports(runId, fiscalPeriod, payload);
|
||||||
String summary = "rows=" + payload.getContributionRows().size() + ", eliminations=" + payload.getEliminationRows().size() + ", forecast=" + payload.getForecastRows().size();
|
String summary = "기여내역=" + payload.getContributionRows().size() + "건, 제거내역=" + payload.getEliminationRows().size() + "건, 전망=" + payload.getForecastRows().size() + "건";
|
||||||
consolidationMapper.markRunSuccess(runId, summary);
|
consolidationMapper.markRunSuccess(runId, summary);
|
||||||
consolidationMapper.insertJobLog("CONSOLIDATION", runId, "SUCCESS", "집계와 리포트 생성을 완료했습니다.");
|
consolidationMapper.insertJobLog("CONSOLIDATION", runId, "SUCCESS", "집계와 리포트 생성을 완료했습니다.");
|
||||||
} catch (Exception exception) {
|
} catch (Exception exception) {
|
||||||
|
|
@ -155,7 +155,7 @@ public class ConsolidationService {
|
||||||
eliminationRow.put("partnerEntityCode", partnerEntityCode);
|
eliminationRow.put("partnerEntityCode", partnerEntityCode);
|
||||||
eliminationRow.put("accountCode", accountCode);
|
eliminationRow.put("accountCode", accountCode);
|
||||||
eliminationRow.put("eliminationAmount", weightedAmount.negate().setScale(2, RoundingMode.HALF_UP));
|
eliminationRow.put("eliminationAmount", weightedAmount.negate().setScale(2, RoundingMode.HALF_UP));
|
||||||
eliminationRow.put("note", "Intercompany elimination");
|
eliminationRow.put("note", "내부거래 제거");
|
||||||
payload.getEliminationRows().add(eliminationRow);
|
payload.getEliminationRows().add(eliminationRow);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,13 +8,14 @@ import com.hanwha.nexacrodemo.consolidation.ConsolidationPayload;
|
||||||
import com.hanwha.nexacrodemo.minio.ObjectStorageService;
|
import com.hanwha.nexacrodemo.minio.ObjectStorageService;
|
||||||
import com.hanwha.nexacrodemo.minio.StoredObject;
|
import com.hanwha.nexacrodemo.minio.StoredObject;
|
||||||
import com.lowagie.text.Document;
|
import com.lowagie.text.Document;
|
||||||
|
import com.lowagie.text.DocumentException;
|
||||||
import com.lowagie.text.Element;
|
import com.lowagie.text.Element;
|
||||||
import com.lowagie.text.Font;
|
import com.lowagie.text.Font;
|
||||||
import com.lowagie.text.FontFactory;
|
|
||||||
import com.lowagie.text.PageSize;
|
import com.lowagie.text.PageSize;
|
||||||
import com.lowagie.text.Paragraph;
|
import com.lowagie.text.Paragraph;
|
||||||
import com.lowagie.text.Phrase;
|
import com.lowagie.text.Phrase;
|
||||||
import com.lowagie.text.Rectangle;
|
import com.lowagie.text.Rectangle;
|
||||||
|
import com.lowagie.text.pdf.BaseFont;
|
||||||
import com.lowagie.text.pdf.PdfPCell;
|
import com.lowagie.text.pdf.PdfPCell;
|
||||||
import com.lowagie.text.pdf.PdfPTable;
|
import com.lowagie.text.pdf.PdfPTable;
|
||||||
import com.lowagie.text.pdf.PdfWriter;
|
import com.lowagie.text.pdf.PdfWriter;
|
||||||
|
|
@ -22,10 +23,13 @@ import java.awt.Color;
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
import java.text.DecimalFormat;
|
import java.text.DecimalFormat;
|
||||||
import java.time.ZoneId;
|
import java.time.ZoneId;
|
||||||
import java.time.ZonedDateTime;
|
import java.time.ZonedDateTime;
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
@ -43,6 +47,7 @@ import org.springframework.core.io.ByteArrayResource;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
|
|
@ -55,15 +60,19 @@ public class ReportService {
|
||||||
private final ReportMapper reportMapper;
|
private final ReportMapper reportMapper;
|
||||||
private final ConsolidationMapper consolidationMapper;
|
private final ConsolidationMapper consolidationMapper;
|
||||||
private final ObjectStorageService objectStorageService;
|
private final ObjectStorageService objectStorageService;
|
||||||
|
private final String pdfFontPath;
|
||||||
|
private volatile BaseFont pdfBaseFont;
|
||||||
|
|
||||||
public ReportService(
|
public ReportService(
|
||||||
ReportMapper reportMapper,
|
ReportMapper reportMapper,
|
||||||
ConsolidationMapper consolidationMapper,
|
ConsolidationMapper consolidationMapper,
|
||||||
ObjectStorageService objectStorageService
|
ObjectStorageService objectStorageService,
|
||||||
|
@Value("${app.report.pdf-font-path:}") String pdfFontPath
|
||||||
) {
|
) {
|
||||||
this.reportMapper = reportMapper;
|
this.reportMapper = reportMapper;
|
||||||
this.consolidationMapper = consolidationMapper;
|
this.consolidationMapper = consolidationMapper;
|
||||||
this.objectStorageService = objectStorageService;
|
this.objectStorageService = objectStorageService;
|
||||||
|
this.pdfFontPath = pdfFontPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
public TxResponse loadRunOverview() {
|
public TxResponse loadRunOverview() {
|
||||||
|
|
@ -122,9 +131,9 @@ public class ReportService {
|
||||||
try (XSSFWorkbook workbook = new XSSFWorkbook()) {
|
try (XSSFWorkbook workbook = new XSSFWorkbook()) {
|
||||||
Map<String, CellStyle> styles = createExcelStyles(workbook);
|
Map<String, CellStyle> styles = createExcelStyles(workbook);
|
||||||
buildSummarySheet(workbook, fiscalPeriod, payload, styles);
|
buildSummarySheet(workbook, fiscalPeriod, payload, styles);
|
||||||
buildDetailSheet(workbook, "Contributions", fiscalPeriod, payload.getContributionRows(), styles);
|
buildDetailSheet(workbook, "기여내역", fiscalPeriod, payload.getContributionRows(), styles);
|
||||||
buildDetailSheet(workbook, "Eliminations", fiscalPeriod, payload.getEliminationRows(), styles);
|
buildDetailSheet(workbook, "제거내역", fiscalPeriod, payload.getEliminationRows(), styles);
|
||||||
buildDetailSheet(workbook, "Forecast", fiscalPeriod, payload.getForecastRows(), styles);
|
buildDetailSheet(workbook, "전망", fiscalPeriod, payload.getForecastRows(), styles);
|
||||||
|
|
||||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||||
workbook.write(outputStream);
|
workbook.write(outputStream);
|
||||||
|
|
@ -137,13 +146,13 @@ public class ReportService {
|
||||||
Document document = new Document(PageSize.A4.rotate(), 36, 36, 36, 36);
|
Document document = new Document(PageSize.A4.rotate(), 36, 36, 36, 36);
|
||||||
PdfWriter.getInstance(document, outputStream);
|
PdfWriter.getInstance(document, outputStream);
|
||||||
document.open();
|
document.open();
|
||||||
addPdfTitle(document, "Hanwha Consolidation Summary", 18, new Color(24, 54, 93));
|
addPdfTitle(document, "한화 연결 집계 보고서", 18, new Color(24, 54, 93));
|
||||||
addPdfTitle(document, "Fiscal period " + fiscalPeriod + " | Generated " + generatedAt(), 10, new Color(90, 96, 106));
|
addPdfTitle(document, "회계기간 " + fiscalPeriod + " | 생성시각 " + generatedAt(), 10, new Color(90, 96, 106));
|
||||||
document.add(new Paragraph(" "));
|
document.add(new Paragraph(" "));
|
||||||
addSummaryTable(document, payload.getMetrics());
|
addSummaryTable(document, payload.getMetrics());
|
||||||
addDetailTable(document, "Contributions", payload.getContributionRows(), 8);
|
addDetailTable(document, "기여내역", payload.getContributionRows(), 8);
|
||||||
addDetailTable(document, "Eliminations", payload.getEliminationRows(), 8);
|
addDetailTable(document, "제거내역", payload.getEliminationRows(), 8);
|
||||||
addDetailTable(document, "Forecast", payload.getForecastRows(), 8);
|
addDetailTable(document, "전망", payload.getForecastRows(), 8);
|
||||||
document.close();
|
document.close();
|
||||||
return outputStream.toByteArray();
|
return outputStream.toByteArray();
|
||||||
}
|
}
|
||||||
|
|
@ -154,24 +163,24 @@ public class ReportService {
|
||||||
ConsolidationPayload payload,
|
ConsolidationPayload payload,
|
||||||
Map<String, CellStyle> styles
|
Map<String, CellStyle> styles
|
||||||
) {
|
) {
|
||||||
Sheet sheet = workbook.createSheet("Summary");
|
Sheet sheet = workbook.createSheet("요약");
|
||||||
sheet.setDisplayGridlines(false);
|
sheet.setDisplayGridlines(false);
|
||||||
|
|
||||||
Row titleRow = sheet.createRow(0);
|
Row titleRow = sheet.createRow(0);
|
||||||
titleRow.setHeightInPoints(28);
|
titleRow.setHeightInPoints(28);
|
||||||
createCell(titleRow, 0, "Hanwha Consolidation Summary", styles.get("title"));
|
createCell(titleRow, 0, "한화 연결 집계 요약", styles.get("title"));
|
||||||
sheet.addMergedRegion(new CellRangeAddress(0, 0, 0, 3));
|
sheet.addMergedRegion(new CellRangeAddress(0, 0, 0, 3));
|
||||||
|
|
||||||
Row subtitleRow = sheet.createRow(1);
|
Row subtitleRow = sheet.createRow(1);
|
||||||
createCell(subtitleRow, 0, "Fiscal period " + fiscalPeriod + " | Generated " + generatedAt(), styles.get("subtitle"));
|
createCell(subtitleRow, 0, "회계기간 " + fiscalPeriod + " | 생성시각 " + generatedAt(), styles.get("subtitle"));
|
||||||
sheet.addMergedRegion(new CellRangeAddress(1, 1, 0, 3));
|
sheet.addMergedRegion(new CellRangeAddress(1, 1, 0, 3));
|
||||||
|
|
||||||
Row sectionRow = sheet.createRow(3);
|
Row sectionRow = sheet.createRow(3);
|
||||||
createCell(sectionRow, 0, "Performance Snapshot", styles.get("section"));
|
createCell(sectionRow, 0, "주요 지표", styles.get("section"));
|
||||||
|
|
||||||
Row headerRow = sheet.createRow(4);
|
Row headerRow = sheet.createRow(4);
|
||||||
createCell(headerRow, 0, "Metric", styles.get("header"));
|
createCell(headerRow, 0, "항목", styles.get("header"));
|
||||||
createCell(headerRow, 1, "Value", styles.get("header"));
|
createCell(headerRow, 1, "값", styles.get("header"));
|
||||||
|
|
||||||
int rowIndex = 5;
|
int rowIndex = 5;
|
||||||
for (Map.Entry<String, BigDecimal> metric : payload.getMetrics().entrySet()) {
|
for (Map.Entry<String, BigDecimal> metric : payload.getMetrics().entrySet()) {
|
||||||
|
|
@ -200,16 +209,16 @@ public class ReportService {
|
||||||
|
|
||||||
Row titleRow = sheet.createRow(0);
|
Row titleRow = sheet.createRow(0);
|
||||||
titleRow.setHeightInPoints(24);
|
titleRow.setHeightInPoints(24);
|
||||||
createCell(titleRow, 0, sheetName + " Detail", styles.get("title"));
|
createCell(titleRow, 0, sheetName, styles.get("title"));
|
||||||
sheet.addMergedRegion(new CellRangeAddress(0, 0, 0, Math.max(0, columnCount - 1)));
|
mergeIfNeeded(sheet, 0, columnCount);
|
||||||
|
|
||||||
Row subtitleRow = sheet.createRow(1);
|
Row subtitleRow = sheet.createRow(1);
|
||||||
createCell(subtitleRow, 0, "Fiscal period " + fiscalPeriod + " | Rows " + rows.size(), styles.get("subtitle"));
|
createCell(subtitleRow, 0, "회계기간 " + fiscalPeriod + " | 건수 " + rows.size(), styles.get("subtitle"));
|
||||||
sheet.addMergedRegion(new CellRangeAddress(1, 1, 0, Math.max(0, columnCount - 1)));
|
mergeIfNeeded(sheet, 1, columnCount);
|
||||||
|
|
||||||
if (rows.isEmpty()) {
|
if (rows.isEmpty()) {
|
||||||
Row emptyRow = sheet.createRow(3);
|
Row emptyRow = sheet.createRow(3);
|
||||||
createCell(emptyRow, 0, "No data available", styles.get("empty"));
|
createCell(emptyRow, 0, "데이터가 없습니다.", styles.get("empty"));
|
||||||
sheet.setColumnWidth(0, 24 * 256);
|
sheet.setColumnWidth(0, 24 * 256);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -347,12 +356,18 @@ public class ReportService {
|
||||||
cell.setCellStyle(style);
|
cell.setCellStyle(style);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void mergeIfNeeded(Sheet sheet, int rowIndex, int columnCount) {
|
||||||
|
if (columnCount > 1) {
|
||||||
|
sheet.addMergedRegion(new CellRangeAddress(rowIndex, rowIndex, 0, columnCount - 1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private CellStyle styleForKey(Map<String, CellStyle> styles, String key) {
|
private CellStyle styleForKey(Map<String, CellStyle> styles, String key) {
|
||||||
return key.toLowerCase().contains("ratio") ? styles.get("ratio") : styles.get("number");
|
return key.toLowerCase().contains("ratio") ? styles.get("ratio") : styles.get("number");
|
||||||
}
|
}
|
||||||
|
|
||||||
private void addPdfTitle(Document document, String text, int size, Color color) {
|
private void addPdfTitle(Document document, String text, int size, Color color) {
|
||||||
Font font = FontFactory.getFont(FontFactory.HELVETICA_BOLD, size, color);
|
Font font = pdfFont(size, Font.BOLD, color);
|
||||||
Paragraph paragraph = new Paragraph(text, font);
|
Paragraph paragraph = new Paragraph(text, font);
|
||||||
paragraph.setAlignment(Element.ALIGN_LEFT);
|
paragraph.setAlignment(Element.ALIGN_LEFT);
|
||||||
paragraph.setSpacingAfter(4f);
|
paragraph.setSpacingAfter(4f);
|
||||||
|
|
@ -364,8 +379,8 @@ public class ReportService {
|
||||||
table.setWidthPercentage(100f);
|
table.setWidthPercentage(100f);
|
||||||
table.setSpacingBefore(6f);
|
table.setSpacingBefore(6f);
|
||||||
table.setSpacingAfter(12f);
|
table.setSpacingAfter(12f);
|
||||||
table.addCell(pdfHeaderCell("Metric"));
|
table.addCell(pdfHeaderCell("항목"));
|
||||||
table.addCell(pdfHeaderCell("Value"));
|
table.addCell(pdfHeaderCell("값"));
|
||||||
for (Map.Entry<String, BigDecimal> metric : metrics.entrySet()) {
|
for (Map.Entry<String, BigDecimal> metric : metrics.entrySet()) {
|
||||||
table.addCell(pdfBodyCell(label(metric.getKey()), Element.ALIGN_LEFT));
|
table.addCell(pdfBodyCell(label(metric.getKey()), Element.ALIGN_LEFT));
|
||||||
table.addCell(pdfBodyCell(format(metric.getValue(), metric.getKey()), Element.ALIGN_RIGHT));
|
table.addCell(pdfBodyCell(format(metric.getValue(), metric.getKey()), Element.ALIGN_RIGHT));
|
||||||
|
|
@ -374,14 +389,14 @@ public class ReportService {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void addDetailTable(Document document, String title, List<Map<String, Object>> rows, int maxRows) {
|
private void addDetailTable(Document document, String title, List<Map<String, Object>> rows, int maxRows) {
|
||||||
Font sectionFont = FontFactory.getFont(FontFactory.HELVETICA_BOLD, 12, new Color(24, 54, 93));
|
Font sectionFont = pdfFont(12, Font.BOLD, new Color(24, 54, 93));
|
||||||
Paragraph section = new Paragraph(title, sectionFont);
|
Paragraph section = new Paragraph(title, sectionFont);
|
||||||
section.setSpacingBefore(8f);
|
section.setSpacingBefore(8f);
|
||||||
section.setSpacingAfter(6f);
|
section.setSpacingAfter(6f);
|
||||||
document.add(section);
|
document.add(section);
|
||||||
|
|
||||||
if (rows.isEmpty()) {
|
if (rows.isEmpty()) {
|
||||||
document.add(new Paragraph("No data available", FontFactory.getFont(FontFactory.HELVETICA_OBLIQUE, 10, new Color(120, 120, 120))));
|
document.add(new Paragraph("데이터가 없습니다.", pdfFont(10, Font.ITALIC, new Color(120, 120, 120))));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -405,14 +420,14 @@ public class ReportService {
|
||||||
|
|
||||||
if (rows.size() > maxRows) {
|
if (rows.size() > maxRows) {
|
||||||
document.add(new Paragraph(
|
document.add(new Paragraph(
|
||||||
"Showing first " + maxRows + " of " + rows.size() + " rows.",
|
"전체 " + rows.size() + "건 중 상위 " + maxRows + "건만 표시합니다.",
|
||||||
FontFactory.getFont(FontFactory.HELVETICA_OBLIQUE, 9, new Color(120, 120, 120))
|
pdfFont(9, Font.ITALIC, new Color(120, 120, 120))
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private PdfPCell pdfHeaderCell(String text) {
|
private PdfPCell pdfHeaderCell(String text) {
|
||||||
PdfPCell cell = new PdfPCell(new Phrase(text, FontFactory.getFont(FontFactory.HELVETICA_BOLD, 9, Color.WHITE)));
|
PdfPCell cell = new PdfPCell(new Phrase(text, pdfFont(9, Font.BOLD, Color.WHITE)));
|
||||||
cell.setHorizontalAlignment(Element.ALIGN_CENTER);
|
cell.setHorizontalAlignment(Element.ALIGN_CENTER);
|
||||||
cell.setVerticalAlignment(Element.ALIGN_MIDDLE);
|
cell.setVerticalAlignment(Element.ALIGN_MIDDLE);
|
||||||
cell.setBackgroundColor(new Color(243, 127, 32));
|
cell.setBackgroundColor(new Color(243, 127, 32));
|
||||||
|
|
@ -422,7 +437,7 @@ public class ReportService {
|
||||||
}
|
}
|
||||||
|
|
||||||
private PdfPCell pdfBodyCell(String text, int alignment) {
|
private PdfPCell pdfBodyCell(String text, int alignment) {
|
||||||
PdfPCell cell = new PdfPCell(new Phrase(text, FontFactory.getFont(FontFactory.HELVETICA, 9, Color.DARK_GRAY)));
|
PdfPCell cell = new PdfPCell(new Phrase(text, pdfFont(9, Font.NORMAL, Color.DARK_GRAY)));
|
||||||
cell.setHorizontalAlignment(alignment);
|
cell.setHorizontalAlignment(alignment);
|
||||||
cell.setVerticalAlignment(Element.ALIGN_MIDDLE);
|
cell.setVerticalAlignment(Element.ALIGN_MIDDLE);
|
||||||
cell.setBorderColor(new Color(226, 229, 233));
|
cell.setBorderColor(new Color(226, 229, 233));
|
||||||
|
|
@ -430,6 +445,53 @@ public class ReportService {
|
||||||
return cell;
|
return cell;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Font pdfFont(float size, int style, Color color) {
|
||||||
|
BaseFont baseFont = resolvePdfBaseFont();
|
||||||
|
if (baseFont == null) {
|
||||||
|
return new Font(Font.HELVETICA, size, style, color);
|
||||||
|
}
|
||||||
|
return new Font(baseFont, size, style, color);
|
||||||
|
}
|
||||||
|
|
||||||
|
private BaseFont resolvePdfBaseFont() {
|
||||||
|
if (pdfBaseFont != null) {
|
||||||
|
return pdfBaseFont;
|
||||||
|
}
|
||||||
|
synchronized (this) {
|
||||||
|
if (pdfBaseFont != null) {
|
||||||
|
return pdfBaseFont;
|
||||||
|
}
|
||||||
|
for (String candidate : pdfFontCandidates()) {
|
||||||
|
try {
|
||||||
|
pdfBaseFont = BaseFont.createFont(candidate, BaseFont.IDENTITY_H, BaseFont.EMBEDDED);
|
||||||
|
return pdfBaseFont;
|
||||||
|
} catch (DocumentException | IOException ignored) {
|
||||||
|
// Fall through to the next candidate. The Docker image installs NanumGothic,
|
||||||
|
// but local test environments may not have a Korean font available.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> pdfFontCandidates() {
|
||||||
|
List<String> candidates = new ArrayList<>();
|
||||||
|
if (pdfFontPath != null && !pdfFontPath.isBlank()) {
|
||||||
|
candidates.add(pdfFontPath.trim());
|
||||||
|
}
|
||||||
|
addIfExists(candidates, "/usr/share/fonts/truetype/nanum/NanumGothic.ttf");
|
||||||
|
addIfExists(candidates, "/usr/share/fonts/truetype/nanum/NanumBarunGothic.ttf");
|
||||||
|
addIfExists(candidates, "/workspace/server/api/src/main/resources/fonts/NanumGothic.ttf");
|
||||||
|
addIfExists(candidates, "/app/resources/fonts/NanumGothic.ttf");
|
||||||
|
return candidates;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addIfExists(List<String> candidates, String path) {
|
||||||
|
if (Files.exists(Path.of(path))) {
|
||||||
|
candidates.add(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private boolean isNumericValue(Object value) {
|
private boolean isNumericValue(Object value) {
|
||||||
return value instanceof Number || value instanceof BigDecimal;
|
return value instanceof Number || value instanceof BigDecimal;
|
||||||
}
|
}
|
||||||
|
|
@ -464,22 +526,22 @@ public class ReportService {
|
||||||
|
|
||||||
private static Map<String, String> buildLabels() {
|
private static Map<String, String> buildLabels() {
|
||||||
Map<String, String> labels = new LinkedHashMap<>();
|
Map<String, String> labels = new LinkedHashMap<>();
|
||||||
labels.put("acceptedTrialRows", "Accepted Trial Rows");
|
labels.put("acceptedTrialRows", "승인된 TB 건수");
|
||||||
labels.put("acceptedForecastRows", "Accepted Forecast Rows");
|
labels.put("acceptedForecastRows", "승인된 전망 건수");
|
||||||
labels.put("grossContributionKrw", "Gross Contribution (KRW)");
|
labels.put("grossContributionKrw", "총 기여금액 (KRW)");
|
||||||
labels.put("eliminationKrw", "Elimination (KRW)");
|
labels.put("eliminationKrw", "제거금액 (KRW)");
|
||||||
labels.put("forecastKrw", "Forecast (KRW)");
|
labels.put("forecastKrw", "전망금액 (KRW)");
|
||||||
labels.put("netContributionKrw", "Net Contribution (KRW)");
|
labels.put("netContributionKrw", "순 기여금액 (KRW)");
|
||||||
labels.put("entityCode", "Entity Code");
|
labels.put("entityCode", "법인코드");
|
||||||
labels.put("accountCode", "Account Code");
|
labels.put("accountCode", "계정코드");
|
||||||
labels.put("partnerEntityCode", "Partner Entity");
|
labels.put("partnerEntityCode", "상대법인코드");
|
||||||
labels.put("translatedAmount", "Translated Amount");
|
labels.put("translatedAmount", "환산금액");
|
||||||
labels.put("ownershipRatio", "Ownership Ratio");
|
labels.put("ownershipRatio", "지분율");
|
||||||
labels.put("finalAmount", "Final Amount");
|
labels.put("finalAmount", "최종반영금액");
|
||||||
labels.put("internalTrade", "Internal Trade");
|
labels.put("internalTrade", "내부거래 여부");
|
||||||
labels.put("eliminationAmount", "Elimination Amount");
|
labels.put("eliminationAmount", "제거금액");
|
||||||
labels.put("note", "Note");
|
labels.put("note", "비고");
|
||||||
labels.put("scenarioCode", "Scenario");
|
labels.put("scenarioCode", "시나리오");
|
||||||
return labels;
|
return labels;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,8 @@ app:
|
||||||
enabled: false
|
enabled: false
|
||||||
poll-interval-ms: ${BATCH_POLL_INTERVAL_MS:5000}
|
poll-interval-ms: ${BATCH_POLL_INTERVAL_MS:5000}
|
||||||
worker-name: ${BATCH_WORKER_NAME:batch-1}
|
worker-name: ${BATCH_WORKER_NAME:batch-1}
|
||||||
|
report:
|
||||||
|
pdf-font-path: ${PDF_FONT_PATH:}
|
||||||
storage:
|
storage:
|
||||||
provider: ${STORAGE_PROVIDER:minio}
|
provider: ${STORAGE_PROVIDER:minio}
|
||||||
bucket: ${MINIO_BUCKET:reports}
|
bucket: ${MINIO_BUCKET:reports}
|
||||||
|
|
@ -57,4 +59,3 @@ logging:
|
||||||
level:
|
level:
|
||||||
root: INFO
|
root: INFO
|
||||||
com.hanwha.nexacrodemo: INFO
|
com.hanwha.nexacrodemo: INFO
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import com.fasterxml.jackson.databind.JsonNode;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import com.hanwha.nexacrodemo.upload.TestWorkbookFactory;
|
import com.hanwha.nexacrodemo.upload.TestWorkbookFactory;
|
||||||
import java.io.ByteArrayInputStream;
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
import org.apache.poi.ss.usermodel.WorkbookFactory;
|
import org.apache.poi.ss.usermodel.WorkbookFactory;
|
||||||
import org.junit.jupiter.api.Assertions;
|
import org.junit.jupiter.api.Assertions;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
@ -20,12 +21,14 @@ import org.springframework.http.MediaType;
|
||||||
import org.springframework.mock.web.MockHttpSession;
|
import org.springframework.mock.web.MockHttpSession;
|
||||||
import org.springframework.mock.web.MockMultipartFile;
|
import org.springframework.mock.web.MockMultipartFile;
|
||||||
import org.springframework.test.context.ActiveProfiles;
|
import org.springframework.test.context.ActiveProfiles;
|
||||||
|
import org.springframework.test.annotation.DirtiesContext;
|
||||||
import org.springframework.test.web.servlet.MockMvc;
|
import org.springframework.test.web.servlet.MockMvc;
|
||||||
import org.springframework.test.web.servlet.MvcResult;
|
import org.springframework.test.web.servlet.MvcResult;
|
||||||
|
|
||||||
@SpringBootTest
|
@SpringBootTest
|
||||||
@AutoConfigureMockMvc
|
@AutoConfigureMockMvc
|
||||||
@ActiveProfiles("test")
|
@ActiveProfiles("test")
|
||||||
|
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD)
|
||||||
class ConsolidationIntegrationTest {
|
class ConsolidationIntegrationTest {
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
|
|
@ -39,24 +42,27 @@ class ConsolidationIntegrationTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void validUploadsCanBeConsolidatedAndReported() throws Exception {
|
void validUploadsCanBeConsolidatedAndReported() throws Exception {
|
||||||
|
String fiscalPeriod = "2026-04";
|
||||||
MockHttpSession session = login("operator", "demo1234");
|
MockHttpSession session = login("operator", "demo1234");
|
||||||
|
|
||||||
upload(session, "trial-balance", "tb-valid.xlsx", TestWorkbookFactory.trialBalanceValid());
|
upload(session, "trial-balance", fiscalPeriod, "tb-valid.xlsx", withFiscalPeriod(TestWorkbookFactory.trialBalanceValid(), fiscalPeriod));
|
||||||
upload(session, "forecast", "forecast-valid.xlsx", TestWorkbookFactory.forecastValid());
|
upload(session, "forecast", fiscalPeriod, "forecast-valid.xlsx", withFiscalPeriod(TestWorkbookFactory.forecastValid(), fiscalPeriod));
|
||||||
|
|
||||||
MvcResult runResult = mockMvc.perform(post("/api/consolidations/runs")
|
MvcResult runResult = mockMvc.perform(post("/api/consolidations/runs")
|
||||||
.contentType(MediaType.APPLICATION_JSON)
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
.session(session)
|
.session(session)
|
||||||
.content("""
|
.content("""
|
||||||
{
|
{
|
||||||
"fiscalPeriod": "2026-03",
|
"fiscalPeriod": "%s",
|
||||||
"reportCurrency": "KRW"
|
"reportCurrency": "KRW"
|
||||||
}
|
}
|
||||||
"""))
|
""".formatted(fiscalPeriod)))
|
||||||
.andExpect(status().isOk())
|
.andExpect(status().isOk())
|
||||||
.andExpect(jsonPath("$.statusCode").value("REQUESTED"))
|
.andExpect(jsonPath("$.statusCode").value("REQUESTED"))
|
||||||
.andReturn();
|
.andReturn();
|
||||||
|
|
||||||
|
long runId = objectMapper.readTree(runResult.getResponse().getContentAsByteArray()).path("id").asLong();
|
||||||
|
|
||||||
consolidationService.processPendingRuns("test-worker");
|
consolidationService.processPendingRuns("test-worker");
|
||||||
|
|
||||||
mockMvc.perform(get("/api/tx/consolidations/overview").session(session))
|
mockMvc.perform(get("/api/tx/consolidations/overview").session(session))
|
||||||
|
|
@ -65,20 +71,24 @@ class ConsolidationIntegrationTest {
|
||||||
|
|
||||||
MvcResult overviewResult = mockMvc.perform(get("/api/tx/reports/overview").session(session))
|
MvcResult overviewResult = mockMvc.perform(get("/api/tx/reports/overview").session(session))
|
||||||
.andExpect(status().isOk())
|
.andExpect(status().isOk())
|
||||||
.andExpect(jsonPath("$.datasets.artifacts.length()").value(2))
|
|
||||||
.andExpect(jsonPath("$.datasets.jobLogs[0].logLevel").exists())
|
.andExpect(jsonPath("$.datasets.jobLogs[0].logLevel").exists())
|
||||||
.andReturn();
|
.andReturn();
|
||||||
|
|
||||||
JsonNode artifacts = objectMapper.readTree(overviewResult.getResponse().getContentAsByteArray())
|
JsonNode artifacts = objectMapper.readTree(overviewResult.getResponse().getContentAsByteArray())
|
||||||
.path("datasets")
|
.path("datasets")
|
||||||
.path("artifacts");
|
.path("artifacts");
|
||||||
|
int artifactCountForRun = 0;
|
||||||
long excelArtifactId = -1L;
|
long excelArtifactId = -1L;
|
||||||
for (JsonNode artifact : artifacts) {
|
for (JsonNode artifact : artifacts) {
|
||||||
|
if (artifact.path("runId").asLong() != runId) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
artifactCountForRun++;
|
||||||
if ("EXCEL".equals(artifact.path("artifactType").asText())) {
|
if ("EXCEL".equals(artifact.path("artifactType").asText())) {
|
||||||
excelArtifactId = artifact.path("id").asLong();
|
excelArtifactId = artifact.path("id").asLong();
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Assertions.assertEquals(2, artifactCountForRun);
|
||||||
Assertions.assertTrue(excelArtifactId > 0, "EXCEL artifact should exist");
|
Assertions.assertTrue(excelArtifactId > 0, "EXCEL artifact should exist");
|
||||||
|
|
||||||
MvcResult excelDownload = mockMvc.perform(get("/api/reports/{artifactId}/download", excelArtifactId).session(session))
|
MvcResult excelDownload = mockMvc.perform(get("/api/reports/{artifactId}/download", excelArtifactId).session(session))
|
||||||
|
|
@ -86,13 +96,67 @@ class ConsolidationIntegrationTest {
|
||||||
.andReturn();
|
.andReturn();
|
||||||
|
|
||||||
try (var workbook = WorkbookFactory.create(new ByteArrayInputStream(excelDownload.getResponse().getContentAsByteArray()))) {
|
try (var workbook = WorkbookFactory.create(new ByteArrayInputStream(excelDownload.getResponse().getContentAsByteArray()))) {
|
||||||
Assertions.assertEquals("Hanwha Consolidation Summary", workbook.getSheet("Summary").getRow(0).getCell(0).getStringCellValue());
|
Assertions.assertEquals("한화 연결 집계 요약", workbook.getSheet("요약").getRow(0).getCell(0).getStringCellValue());
|
||||||
Assertions.assertEquals("Entity Code", workbook.getSheet("Contributions").getRow(3).getCell(0).getStringCellValue());
|
Assertions.assertEquals("법인코드", workbook.getSheet("기여내역").getRow(3).getCell(0).getStringCellValue());
|
||||||
Assertions.assertNotNull(workbook.getSheet("Forecast"));
|
Assertions.assertNotNull(workbook.getSheet("전망"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void upload(MockHttpSession session, String templateCode, String fileName, byte[] content) throws Exception {
|
@Test
|
||||||
|
void consolidationStillSucceedsWhenForecastSectionIsEmpty() throws Exception {
|
||||||
|
String fiscalPeriod = "2026-03";
|
||||||
|
MockHttpSession session = login("operator", "demo1234");
|
||||||
|
|
||||||
|
upload(session, "trial-balance", fiscalPeriod, "tb-valid.xlsx", TestWorkbookFactory.trialBalanceValid());
|
||||||
|
|
||||||
|
MvcResult runResult = mockMvc.perform(post("/api/consolidations/runs")
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.session(session)
|
||||||
|
.content("""
|
||||||
|
{
|
||||||
|
"fiscalPeriod": "%s",
|
||||||
|
"reportCurrency": "KRW"
|
||||||
|
}
|
||||||
|
""".formatted(fiscalPeriod)))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.statusCode").value("REQUESTED"))
|
||||||
|
.andReturn();
|
||||||
|
|
||||||
|
long runId = objectMapper.readTree(runResult.getResponse().getContentAsByteArray()).path("id").asLong();
|
||||||
|
|
||||||
|
consolidationService.processPendingRuns("test-worker");
|
||||||
|
|
||||||
|
MvcResult overviewResult = mockMvc.perform(get("/api/tx/reports/overview").session(session))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andReturn();
|
||||||
|
|
||||||
|
JsonNode artifacts = objectMapper.readTree(overviewResult.getResponse().getContentAsByteArray())
|
||||||
|
.path("datasets")
|
||||||
|
.path("artifacts");
|
||||||
|
int artifactCountForRun = 0;
|
||||||
|
long excelArtifactId = -1L;
|
||||||
|
for (JsonNode artifact : artifacts) {
|
||||||
|
if (artifact.path("runId").asLong() != runId) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
artifactCountForRun++;
|
||||||
|
if ("EXCEL".equals(artifact.path("artifactType").asText())) {
|
||||||
|
excelArtifactId = artifact.path("id").asLong();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Assertions.assertEquals(2, artifactCountForRun);
|
||||||
|
Assertions.assertTrue(excelArtifactId > 0, "EXCEL artifact should exist");
|
||||||
|
|
||||||
|
MvcResult excelDownload = mockMvc.perform(get("/api/reports/{artifactId}/download", excelArtifactId).session(session))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andReturn();
|
||||||
|
|
||||||
|
try (var workbook = WorkbookFactory.create(new ByteArrayInputStream(excelDownload.getResponse().getContentAsByteArray()))) {
|
||||||
|
Assertions.assertEquals("데이터가 없습니다.", workbook.getSheet("전망").getRow(3).getCell(0).getStringCellValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void upload(MockHttpSession session, String templateCode, String fiscalPeriod, String fileName, byte[] content) throws Exception {
|
||||||
MockMultipartFile file = new MockMultipartFile(
|
MockMultipartFile file = new MockMultipartFile(
|
||||||
"file",
|
"file",
|
||||||
fileName,
|
fileName,
|
||||||
|
|
@ -103,12 +167,27 @@ class ConsolidationIntegrationTest {
|
||||||
mockMvc.perform(multipart("/api/uploads")
|
mockMvc.perform(multipart("/api/uploads")
|
||||||
.file(file)
|
.file(file)
|
||||||
.param("templateCode", templateCode)
|
.param("templateCode", templateCode)
|
||||||
.param("fiscalPeriod", "2026-03")
|
.param("fiscalPeriod", fiscalPeriod)
|
||||||
.session(session))
|
.session(session))
|
||||||
.andExpect(status().isOk())
|
.andExpect(status().isOk())
|
||||||
.andExpect(jsonPath("$.statusCode").value("ACCEPTED"));
|
.andExpect(jsonPath("$.statusCode").value("ACCEPTED"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private byte[] withFiscalPeriod(byte[] content, String fiscalPeriod) throws Exception {
|
||||||
|
try (var workbook = WorkbookFactory.create(new ByteArrayInputStream(content))) {
|
||||||
|
var dataSheet = workbook.getSheet("DATA");
|
||||||
|
for (int rowIndex = 1; rowIndex <= dataSheet.getLastRowNum(); rowIndex++) {
|
||||||
|
var row = dataSheet.getRow(rowIndex);
|
||||||
|
if (row != null && row.getCell(0) != null) {
|
||||||
|
row.getCell(0).setCellValue(fiscalPeriod);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||||
|
workbook.write(outputStream);
|
||||||
|
return outputStream.toByteArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private MockHttpSession login(String username, String password) throws Exception {
|
private MockHttpSession login(String username, String password) throws Exception {
|
||||||
MvcResult result = mockMvc.perform(post("/api/auth/login")
|
MvcResult result = mockMvc.perform(post("/api/auth/login")
|
||||||
.contentType(MediaType.APPLICATION_JSON)
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
|
|
||||||
|
|
@ -59,5 +59,4 @@ transactions:
|
||||||
messages:
|
messages:
|
||||||
- code: REPORT_HINT
|
- code: REPORT_HINT
|
||||||
level: INFO
|
level: INFO
|
||||||
text: batch가 생성한 Excel/PDF를 내려받고 최근 로그를 확인합니다.
|
text: 배치가 생성한 Excel/PDF를 내려받고 최근 로그를 확인합니다.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -103,5 +103,4 @@ actions:
|
||||||
messages:
|
messages:
|
||||||
- code: UPLOAD_HINT
|
- code: UPLOAD_HINT
|
||||||
level: INFO
|
level: INFO
|
||||||
text: invalid 샘플로 오류 시나리오를 확인한 뒤 valid 샘플을 재업로드합니다.
|
text: 오류 샘플로 오류 시나리오를 확인한 뒤 정상 샘플을 재업로드합니다.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -368,7 +368,7 @@ function renderUploads() {
|
||||||
<button class="secondary" data-action="reload-uploads">내역 새로고침</button>
|
<button class="secondary" data-action="reload-uploads">내역 새로고침</button>
|
||||||
<button class="ghost" data-action="download-sample" data-sample-code="trial-balance-invalid">오류 샘플</button>
|
<button class="ghost" data-action="download-sample" data-sample-code="trial-balance-invalid">오류 샘플</button>
|
||||||
<button class="ghost" data-action="download-sample" data-sample-code="trial-balance-valid">정상 TB</button>
|
<button class="ghost" data-action="download-sample" data-sample-code="trial-balance-valid">정상 TB</button>
|
||||||
<button class="ghost" data-action="download-sample" data-sample-code="forecast-valid">정상 Forecast</button>
|
<button class="ghost" data-action="download-sample" data-sample-code="forecast-valid">정상 전망</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel">
|
<div class="panel">
|
||||||
|
|
@ -524,7 +524,7 @@ function shellContent(content) {
|
||||||
<aside class="sidebar">
|
<aside class="sidebar">
|
||||||
<div class="brand">
|
<div class="brand">
|
||||||
<h1>{{appTitle}}</h1>
|
<h1>{{appTitle}}</h1>
|
||||||
<p>Spec driven preview generated from Nexacro DSL</p>
|
<p>Nexacro DSL에서 생성한 미리보기 화면</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="nav-list">${renderNav()}</div>
|
<div class="nav-list">${renderNav()}</div>
|
||||||
<div class="panel" style="margin-top: 18px;">
|
<div class="panel" style="margin-top: 18px;">
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue