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