Compare commits
4 Commits
75a786f681
...
661d78a225
| Author | SHA1 | Date |
|---|---|---|
|
|
661d78a225 | |
|
|
114c58737f | |
|
|
087a274f47 | |
|
|
d964b65f0a |
|
|
@ -125,22 +125,36 @@ function table(columns, rows, options = {}) {
|
||||||
const value = options.render ? options.render(column, row[column.id], row) : row[column.id];
|
const value = options.render ? options.render(column, row[column.id], row) : row[column.id];
|
||||||
return `<td>${formatValue(value)}</td>`;
|
return `<td>${formatValue(value)}</td>`;
|
||||||
})
|
})
|
||||||
.join("")}</tr>`
|
.join("")}${options.actions ? `<td class="actions-cell">${options.actions(row) || ""}</td>` : ""}</tr>`
|
||||||
)
|
)
|
||||||
.join("")
|
.join("")
|
||||||
: `<tr><td colspan="${columns.length}">데이터가 없습니다.</td></tr>`;
|
: `<tr><td colspan="${columns.length + (options.actions ? 1 : 0)}">데이터가 없습니다.</td></tr>`;
|
||||||
|
|
||||||
return `<div class="table-wrap"><table><thead><tr>${columns
|
return `<div class="table-wrap"><table><thead><tr>${columns
|
||||||
.map((column) => `<th>${column.text}</th>`)
|
.map((column) => `<th>${column.text}</th>`)
|
||||||
.join("")}</tr></thead><tbody>${body}</tbody></table></div>`;
|
.join("")}${options.actions ? "<th>작업</th>" : ""}</tr></thead><tbody>${body}</tbody></table></div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function pill(value) {
|
function pill(value) {
|
||||||
return `<span class="pill ${value}">${value}</span>`;
|
return `<span class="pill ${value}">${value}</span>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hasRole(requiredRole) {
|
||||||
|
const roleOrder = {
|
||||||
|
PUBLIC: 0,
|
||||||
|
VIEWER: 10,
|
||||||
|
OPERATOR: 20,
|
||||||
|
ADMIN: 30
|
||||||
|
};
|
||||||
|
return (roleOrder[state.session?.roleCode] || 0) >= (roleOrder[requiredRole] || 0);
|
||||||
|
}
|
||||||
|
|
||||||
function isAdmin() {
|
function isAdmin() {
|
||||||
return state.session?.roleCode === "ADMIN";
|
return hasRole("ADMIN");
|
||||||
|
}
|
||||||
|
|
||||||
|
function isOperator() {
|
||||||
|
return hasRole("OPERATOR");
|
||||||
}
|
}
|
||||||
|
|
||||||
function cloneRows(rows) {
|
function cloneRows(rows) {
|
||||||
|
|
@ -418,11 +432,11 @@ function renderUploads() {
|
||||||
<div class="row"><label>회계기간</label><input id="upload-period" value="2026-03" /></div>
|
<div class="row"><label>회계기간</label><input id="upload-period" value="2026-03" /></div>
|
||||||
<div class="row"><label>파일 선택</label><input id="upload-file" type="file" /></div>
|
<div class="row"><label>파일 선택</label><input id="upload-file" type="file" /></div>
|
||||||
<div class="row actions">
|
<div class="row actions">
|
||||||
<button data-action="upload">파일 업로드</button>
|
<button data-action="upload" ${isOperator() ? "" : "disabled"}>파일 업로드</button>
|
||||||
<button class="secondary" data-action="reload-uploads">내역 새로고침</button>
|
<button class="secondary" data-action="reload-uploads">내역 새로고침</button>
|
||||||
<a class="ghost" href="/sample-data/trial-balance-invalid.xlsx" download>오류 샘플</a>
|
<button class="ghost" data-action="download-sample" data-sample-code="trial-balance-invalid">오류 샘플</button>
|
||||||
<a class="ghost" href="/sample-data/trial-balance-valid.xlsx" download>정상 TB</a>
|
<button class="ghost" data-action="download-sample" data-sample-code="trial-balance-valid">정상 TB</button>
|
||||||
<a class="ghost" href="/sample-data/forecast-valid.xlsx" download>정상 Forecast</a>
|
<button class="ghost" data-action="download-sample" data-sample-code="forecast-valid">정상 Forecast</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel">
|
<div class="panel">
|
||||||
|
|
@ -445,6 +459,9 @@ function renderUploads() {
|
||||||
return pill(value);
|
return pill(value);
|
||||||
}
|
}
|
||||||
return value;
|
return value;
|
||||||
|
},
|
||||||
|
actions(row) {
|
||||||
|
return `<button class="danger small" data-action="delete-upload" data-batch-id="${row.id}" ${isOperator() ? "" : "disabled"}>삭제</button>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
|
|
@ -587,14 +604,6 @@ function shellContent(content) {
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
<main class="main">
|
<main class="main">
|
||||||
<div class="topbar">
|
|
||||||
<div></div>
|
|
||||||
<div class="footer-links">
|
|
||||||
<a href="/sample-data/trial-balance-invalid.xlsx" download>오류 샘플</a>
|
|
||||||
<a href="/sample-data/trial-balance-valid.xlsx" download>정상 TB</a>
|
|
||||||
<a href="/sample-data/forecast-valid.xlsx" download>정상 Forecast</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
${content}
|
${content}
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -791,6 +800,9 @@ function bindEvents() {
|
||||||
const uploadButton = document.querySelector("[data-action='upload']");
|
const uploadButton = document.querySelector("[data-action='upload']");
|
||||||
if (uploadButton) {
|
if (uploadButton) {
|
||||||
uploadButton.addEventListener("click", async () => {
|
uploadButton.addEventListener("click", async () => {
|
||||||
|
if (!isOperator()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const templateCode = document.getElementById("upload-template").value;
|
const templateCode = document.getElementById("upload-template").value;
|
||||||
const fiscalPeriod = document.getElementById("upload-period").value;
|
const fiscalPeriod = document.getElementById("upload-period").value;
|
||||||
const file = document.getElementById("upload-file").files[0];
|
const file = document.getElementById("upload-file").files[0];
|
||||||
|
|
@ -816,6 +828,30 @@ function bindEvents() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
document.querySelectorAll("[data-action='download-sample']").forEach((button) => {
|
||||||
|
button.addEventListener("click", () => {
|
||||||
|
const fiscalPeriod = document.getElementById("upload-period")?.value?.trim() || "2026-03";
|
||||||
|
const sampleCode = button.dataset.sampleCode;
|
||||||
|
window.location.href = `/api/uploads/samples/${sampleCode}/download?fiscalPeriod=${encodeURIComponent(fiscalPeriod)}`;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelectorAll("[data-action='delete-upload']").forEach((button) => {
|
||||||
|
button.addEventListener("click", async () => {
|
||||||
|
if (!isOperator()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const batchId = button.dataset.batchId;
|
||||||
|
const confirmed = window.confirm(`업로드 이력 ${batchId}번을 삭제하시겠습니까? 관련 오류내역과 업로드 행도 함께 삭제됩니다.`);
|
||||||
|
if (!confirmed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await api(`/api/uploads/${batchId}`, { method: "DELETE" });
|
||||||
|
await loadUploads();
|
||||||
|
render();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
const runButton = document.querySelector("[data-action='request-run']");
|
const runButton = document.querySelector("[data-action='request-run']");
|
||||||
if (runButton) {
|
if (runButton) {
|
||||||
runButton.addEventListener("click", async () => {
|
runButton.addEventListener("click", async () => {
|
||||||
|
|
|
||||||
|
|
@ -270,13 +270,17 @@ th {
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.actions-cell {
|
||||||
|
white-space: nowrap;
|
||||||
|
width: 88px;
|
||||||
|
}
|
||||||
|
|
||||||
.master-table input {
|
.master-table input {
|
||||||
min-width: 110px;
|
min-width: 110px;
|
||||||
padding: 10px 12px;
|
padding: 10px 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.master-table td.actions-cell {
|
.master-table td.actions-cell {
|
||||||
white-space: nowrap;
|
|
||||||
width: 88px;
|
width: 88px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,7 @@ this.actRunConsolidation = function(obj, e)
|
||||||
<Grid id="grdRuns" taborder="1" left="36" top="176" width="1368" height="360" binddataset="dsRuns">
|
<Grid id="grdRuns" taborder="1" left="36" top="176" width="1368" height="360" binddataset="dsRuns">
|
||||||
<Formats><Format id="default"><Columns><Column size="195"/><Column size="195"/><Column size="195"/><Column size="195"/><Column size="195"/><Column size="195"/><Column size="195"/></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="상태"/><Cell col="3" text="요청자"/><Cell col="4" text="요청시각"/><Cell col="5" text="완료시각"/><Cell col="6" text="요약"/></Band><Band id="body"><Cell col="0" text="bind:id"/><Cell col="1" text="bind:fiscalPeriod"/><Cell col="2" text="bind:statusCode"/><Cell col="3" text="bind:requestedBy"/><Cell col="4" text="bind:requestedAt"/><Cell col="5" text="bind:finishedAt"/><Cell col="6" text="bind:summaryMessage"/></Band></Format></Formats>
|
<Formats><Format id="default"><Columns><Column size="195"/><Column size="195"/><Column size="195"/><Column size="195"/><Column size="195"/><Column size="195"/><Column size="195"/></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="상태"/><Cell col="3" text="요청자"/><Cell col="4" text="요청시각"/><Cell col="5" text="완료시각"/><Cell col="6" text="요약"/></Band><Band id="body"><Cell col="0" text="bind:id"/><Cell col="1" text="bind:fiscalPeriod"/><Cell col="2" text="bind:statusCode"/><Cell col="3" text="bind:requestedBy"/><Cell col="4" text="bind:requestedAt"/><Cell col="5" text="bind:finishedAt"/><Cell col="6" text="bind:summaryMessage"/></Band></Format></Formats>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
<Static id="staMessage0" taborder="900" left="36" top="824" width="1368" height="20" text="업로드가 ACCEPTED 상태인 파일만 집계 대상에 포함됩니다."/>
|
||||||
<Layouts>
|
<Layouts>
|
||||||
<Layout width="1440" height="900" screenid="Desktop_screen"/>
|
<Layout width="1440" height="900" screenid="Desktop_screen"/>
|
||||||
</Layouts>
|
</Layouts>
|
||||||
|
|
@ -39,10 +39,11 @@ this.actLogin = function(obj, e)
|
||||||
]]></Script>
|
]]></Script>
|
||||||
<Static id="staTitle" taborder="0" left="88" top="72" width="460" height="64" text="Hanwha Nexacro Demo"/>
|
<Static id="staTitle" taborder="0" left="88" top="72" width="460" height="64" text="Hanwha Nexacro Demo"/>
|
||||||
<Static id="staSubtitle" taborder="1" left="88" top="148" width="460" height="28" text="업로드/검증 중심 재무 통합 데모"/>
|
<Static id="staSubtitle" taborder="1" left="88" top="148" width="460" height="28" text="업로드/검증 중심 재무 통합 데모"/>
|
||||||
<Edit id="edtUsername" taborder="2" left="88" top="248" width="320" height="44" displaynulltext="사용자 ID" value="bind:username"/>
|
<Edit id="edtUsername" taborder="2" left="88" top="248" width="320" height="44" displaynulltext="사용자 ID"/>
|
||||||
<Edit id="edtPassword" taborder="3" left="88" top="308" width="320" height="44" displaynulltext="비밀번호" value="bind:password" password="true"/>
|
<Edit id="edtPassword" taborder="3" left="88" top="308" width="320" height="44" displaynulltext="비밀번호" password="true"/>
|
||||||
<Button id="btnLogin" taborder="4" left="88" top="376" width="320" height="48" text="로그인"/>
|
<Button id="btnLogin" taborder="4" left="88" top="376" width="320" height="48" text="로그인"/>
|
||||||
|
|
||||||
|
<Static id="staMessage0" taborder="900" left="36" top="824" width="1368" height="20" text="기본 계정 admin/operator/viewer, 비밀번호 demo1234"/>
|
||||||
<Layouts>
|
<Layouts>
|
||||||
<Layout width="1440" height="900" screenid="Desktop_screen"/>
|
<Layout width="1440" height="900" screenid="Desktop_screen"/>
|
||||||
</Layouts>
|
</Layouts>
|
||||||
|
|
@ -75,6 +75,7 @@ this.txLoadReference = function(obj, e)
|
||||||
<Grid id="grdOwnership" taborder="1" left="708" top="410" width="696" height="260" binddataset="dsOwnership">
|
<Grid id="grdOwnership" taborder="1" left="708" top="410" width="696" height="260" binddataset="dsOwnership">
|
||||||
<Formats><Format id="default"><Columns><Column size="232"/><Column size="232"/><Column size="232"/></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="지분율"/></Band><Band id="body"><Cell col="0" text="bind:parentEntityCode"/><Cell col="1" text="bind:childEntityCode"/><Cell col="2" text="bind:ownershipRatio"/></Band></Format></Formats>
|
<Formats><Format id="default"><Columns><Column size="232"/><Column size="232"/><Column size="232"/></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="지분율"/></Band><Band id="body"><Cell col="0" text="bind:parentEntityCode"/><Cell col="1" text="bind:childEntityCode"/><Cell col="2" text="bind:ownershipRatio"/></Band></Format></Formats>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
<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>
|
||||||
|
|
@ -57,6 +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를 내려받고 최근 로그를 확인합니다."/>
|
||||||
<Layouts>
|
<Layouts>
|
||||||
<Layout width="1440" height="900" screenid="Desktop_screen"/>
|
<Layout width="1440" height="900" screenid="Desktop_screen"/>
|
||||||
</Layouts>
|
</Layouts>
|
||||||
|
|
@ -56,7 +56,7 @@ this.actUpload = function(obj, e)
|
||||||
]]></Script>
|
]]></Script>
|
||||||
<Combo id="cboTemplate" taborder="0" left="36" top="108" width="220" height="38" codecolumn="code" datacolumn="label"/>
|
<Combo id="cboTemplate" taborder="0" left="36" top="108" width="220" height="38" codecolumn="code" datacolumn="label"/>
|
||||||
<Edit id="edtFiscalPeriod" taborder="1" left="274" top="108" width="140" height="38" displaynulltext="회계기간 (YYYY-MM)"/>
|
<Edit id="edtFiscalPeriod" taborder="1" left="274" top="108" width="140" height="38" displaynulltext="회계기간 (YYYY-MM)"/>
|
||||||
<FileUpload id="fileUpload" taborder="2" left="432" top="108" width="480" height="38"/>
|
<Edit id="fileUpload" taborder="2" left="432" top="108" width="480" height="38" displaynulltext="업로드 파일" readonly="true"/>
|
||||||
<Button id="btnUpload" taborder="3" left="930" top="108" width="140" height="38" text="파일 업로드"/>
|
<Button id="btnUpload" taborder="3" left="930" top="108" width="140" height="38" text="파일 업로드"/>
|
||||||
<Button id="btnReloadUploads" taborder="4" left="1086" top="108" width="140" height="38" text="내역 새로고침"/>
|
<Button id="btnReloadUploads" taborder="4" left="1086" top="108" width="140" height="38" text="내역 새로고침"/>
|
||||||
<Static id="sta_grdUploadBatches" taborder="0" left="36" top="148" width="1368" height="24" text="업로드 이력"/>
|
<Static id="sta_grdUploadBatches" taborder="0" left="36" top="148" width="1368" height="24" text="업로드 이력"/>
|
||||||
|
|
@ -67,6 +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 샘플을 재업로드합니다."/>
|
||||||
<Layouts>
|
<Layouts>
|
||||||
<Layout width="1440" height="900" screenid="Desktop_screen"/>
|
<Layout width="1440" height="900" screenid="Desktop_screen"/>
|
||||||
</Layouts>
|
</Layouts>
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<FDL version="2.1">
|
<FDL version="2.1">
|
||||||
<Form id="Form_Work" width="1200" height="844" titletext="Form_Work">
|
<Form id="Form_Work" width="1200" height="844" titletext="Form_Work">
|
||||||
<Div id="divWork" taborder="0" left="0" top="0" width="1200" height="844" url="Forms::frmLogin.xfdl"/>
|
<Div id="divWork" taborder="0" left="0" top="0" width="1200" height="844" url="Base::frmLogin.xfdl"/>
|
||||||
<Layouts>
|
<Layouts>
|
||||||
<Layout width="1200" height="844" screenid="Desktop_screen"/>
|
<Layout width="1200" height="844" screenid="Desktop_screen"/>
|
||||||
</Layouts>
|
</Layouts>
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<Project version="2.1" nexacrosdk="Latest Version" sdkversion="">
|
<Project version="2.1" nexacrosdk="Latest Version" sdkversion="">
|
||||||
<EnvironmentDefinition url="/environment.xml"/>
|
<EnvironmentDefinition url="environment.xml"/>
|
||||||
<TypeDefinition url="/typedefinition.xml"/>
|
<TypeDefinition url="typedefinition.xml"/>
|
||||||
<AppVariables url="/appvariables.xml"/>
|
<AppVariables url="appvariables.xml"/>
|
||||||
<AppInfos>
|
<AppInfos>
|
||||||
<AppInfo url="/Application_Desktop.xadl"/>
|
<AppInfo url="Application_Desktop.xadl"/>
|
||||||
</AppInfos>
|
</AppInfos>
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
this.gfnApiBase = function()
|
|
||||||
{
|
|
||||||
return application.g_apiBase || "/api";
|
|
||||||
};
|
|
||||||
|
|
||||||
this.gfnBuildTransactionUrl = function(path)
|
|
||||||
{
|
|
||||||
return this.gfnApiBase() + path;
|
|
||||||
};
|
|
||||||
|
|
||||||
this.gfnShowMessage = function(message)
|
|
||||||
{
|
|
||||||
trace(message);
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
@ -41,8 +41,6 @@
|
||||||
<Component type="JavaScript" id="DateField" classname="nexacro.DateField"/>
|
<Component type="JavaScript" id="DateField" classname="nexacro.DateField"/>
|
||||||
<Component type="JavaScript" id="DateRangePicker" classname="nexacro.DateRangePicker"/>
|
<Component type="JavaScript" id="DateRangePicker" classname="nexacro.DateRangePicker"/>
|
||||||
<Component type="JavaScript" id="PopupDateRangePicker" classname="nexacro.PopupDateRangePicker"/>
|
<Component type="JavaScript" id="PopupDateRangePicker" classname="nexacro.PopupDateRangePicker"/>
|
||||||
<Component type="JavaScript" id="View" classname="nexacro.View"/>
|
|
||||||
<Component type="JavaScript" id="FileUpload" classname="nexacro.FileUpload"/>
|
|
||||||
</Components>
|
</Components>
|
||||||
<Services>
|
<Services>
|
||||||
<Service prefixid="theme" type="resource" url="./_resource_/_theme_/" version="0" cachelevel="session" include_subdir="true"/>
|
<Service prefixid="theme" type="resource" url="./_resource_/_theme_/" version="0" cachelevel="session" include_subdir="true"/>
|
||||||
|
|
@ -52,8 +50,8 @@
|
||||||
<Service prefixid="font" type="resource" url="./_resource_/_font_/" version="0" cachelevel="session" include_subdir="false"/>
|
<Service prefixid="font" type="resource" url="./_resource_/_font_/" version="0" cachelevel="session" include_subdir="false"/>
|
||||||
<Service prefixid="stringrc" type="resource" url="./_resource_/_stringrc_/" version="0" cachelevel="session" include_subdir="false"/>
|
<Service prefixid="stringrc" type="resource" url="./_resource_/_stringrc_/" version="0" cachelevel="session" include_subdir="false"/>
|
||||||
<Service prefixid="extlib" type="resource" url="./_extlib_/" version="0" cachelevel="session" include_subdir="true"/>
|
<Service prefixid="extlib" type="resource" url="./_extlib_/" version="0" cachelevel="session" include_subdir="true"/>
|
||||||
|
<Service prefixid="Base" type="form" url="./Base/" version="0" cachelevel="session" include_subdir="false" communicationversion="0"/>
|
||||||
<Service prefixid="FrameBase" type="form" url="./FrameBase/" version="0" cachelevel="session" include_subdir="false" communicationversion="0"/>
|
<Service prefixid="FrameBase" type="form" url="./FrameBase/" version="0" cachelevel="session" include_subdir="false" communicationversion="0"/>
|
||||||
<Service prefixid="Forms" type="form" url="./forms/" version="0" cachelevel="session" include_subdir="false" communicationversion="0"/>
|
|
||||||
</Services>
|
</Services>
|
||||||
<Protocols/>
|
<Protocols/>
|
||||||
<DeviceAdaptors/>
|
<DeviceAdaptors/>
|
||||||
|
|
|
||||||
|
|
@ -8,15 +8,36 @@ 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.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.Paragraph;
|
||||||
|
import com.lowagie.text.Phrase;
|
||||||
|
import com.lowagie.text.Rectangle;
|
||||||
|
import com.lowagie.text.pdf.PdfPCell;
|
||||||
|
import com.lowagie.text.pdf.PdfPTable;
|
||||||
import com.lowagie.text.pdf.PdfWriter;
|
import com.lowagie.text.pdf.PdfWriter;
|
||||||
|
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.text.DecimalFormat;
|
||||||
|
import java.time.ZoneId;
|
||||||
|
import java.time.ZonedDateTime;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import org.apache.poi.ss.usermodel.BorderStyle;
|
||||||
|
import org.apache.poi.ss.usermodel.CellStyle;
|
||||||
|
import org.apache.poi.ss.usermodel.FillPatternType;
|
||||||
|
import org.apache.poi.ss.usermodel.FontUnderline;
|
||||||
|
import org.apache.poi.ss.usermodel.HorizontalAlignment;
|
||||||
import org.apache.poi.ss.usermodel.Row;
|
import org.apache.poi.ss.usermodel.Row;
|
||||||
import org.apache.poi.ss.usermodel.Sheet;
|
import org.apache.poi.ss.usermodel.Sheet;
|
||||||
|
import org.apache.poi.ss.usermodel.VerticalAlignment;
|
||||||
|
import org.apache.poi.ss.util.CellRangeAddress;
|
||||||
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
|
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
|
||||||
import org.springframework.core.io.ByteArrayResource;
|
import org.springframework.core.io.ByteArrayResource;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
|
|
@ -27,6 +48,10 @@ import org.springframework.stereotype.Service;
|
||||||
@Service
|
@Service
|
||||||
public class ReportService {
|
public class ReportService {
|
||||||
|
|
||||||
|
private static final DateTimeFormatter REPORT_TIME_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
|
||||||
|
private static final DecimalFormat AMOUNT_FORMAT = new DecimalFormat("#,##0.00");
|
||||||
|
private static final Map<String, String> LABELS = buildLabels();
|
||||||
|
|
||||||
private final ReportMapper reportMapper;
|
private final ReportMapper reportMapper;
|
||||||
private final ConsolidationMapper consolidationMapper;
|
private final ConsolidationMapper consolidationMapper;
|
||||||
private final ObjectStorageService objectStorageService;
|
private final ObjectStorageService objectStorageService;
|
||||||
|
|
@ -53,7 +78,7 @@ public class ReportService {
|
||||||
|
|
||||||
public void generateReports(Long runId, String fiscalPeriod, ConsolidationPayload payload) {
|
public void generateReports(Long runId, String fiscalPeriod, ConsolidationPayload payload) {
|
||||||
try {
|
try {
|
||||||
byte[] excelBytes = buildExcel(payload);
|
byte[] excelBytes = buildExcel(fiscalPeriod, payload);
|
||||||
byte[] pdfBytes = buildPdf(fiscalPeriod, payload);
|
byte[] pdfBytes = buildPdf(fiscalPeriod, payload);
|
||||||
|
|
||||||
ReportArtifactCommand excelArtifact = new ReportArtifactCommand();
|
ReportArtifactCommand excelArtifact = new ReportArtifactCommand();
|
||||||
|
|
@ -93,19 +118,13 @@ public class ReportService {
|
||||||
.body(new ByteArrayResource(storedObject.getBytes()));
|
.body(new ByteArrayResource(storedObject.getBytes()));
|
||||||
}
|
}
|
||||||
|
|
||||||
private byte[] buildExcel(ConsolidationPayload payload) throws IOException {
|
private byte[] buildExcel(String fiscalPeriod, ConsolidationPayload payload) throws IOException {
|
||||||
try (XSSFWorkbook workbook = new XSSFWorkbook()) {
|
try (XSSFWorkbook workbook = new XSSFWorkbook()) {
|
||||||
Sheet summarySheet = workbook.createSheet("Summary");
|
Map<String, CellStyle> styles = createExcelStyles(workbook);
|
||||||
int rowIndex = 0;
|
buildSummarySheet(workbook, fiscalPeriod, payload, styles);
|
||||||
for (Map.Entry<String, BigDecimal> metric : payload.getMetrics().entrySet()) {
|
buildDetailSheet(workbook, "Contributions", fiscalPeriod, payload.getContributionRows(), styles);
|
||||||
Row row = summarySheet.createRow(rowIndex++);
|
buildDetailSheet(workbook, "Eliminations", fiscalPeriod, payload.getEliminationRows(), styles);
|
||||||
row.createCell(0).setCellValue(metric.getKey());
|
buildDetailSheet(workbook, "Forecast", fiscalPeriod, payload.getForecastRows(), styles);
|
||||||
row.createCell(1).setCellValue(metric.getValue().doubleValue());
|
|
||||||
}
|
|
||||||
|
|
||||||
writeRows(workbook.createSheet("Contributions"), payload.getContributionRows());
|
|
||||||
writeRows(workbook.createSheet("Eliminations"), payload.getEliminationRows());
|
|
||||||
writeRows(workbook.createSheet("Forecast"), payload.getForecastRows());
|
|
||||||
|
|
||||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||||
workbook.write(outputStream);
|
workbook.write(outputStream);
|
||||||
|
|
@ -115,39 +134,352 @@ public class ReportService {
|
||||||
|
|
||||||
private byte[] buildPdf(String fiscalPeriod, ConsolidationPayload payload) throws IOException {
|
private byte[] buildPdf(String fiscalPeriod, ConsolidationPayload payload) throws IOException {
|
||||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||||
Document document = new Document();
|
Document document = new Document(PageSize.A4.rotate(), 36, 36, 36, 36);
|
||||||
PdfWriter.getInstance(document, outputStream);
|
PdfWriter.getInstance(document, outputStream);
|
||||||
document.open();
|
document.open();
|
||||||
document.add(new Paragraph("Hanwha Consolidation Demo"));
|
addPdfTitle(document, "Hanwha Consolidation Summary", 18, new Color(24, 54, 93));
|
||||||
document.add(new Paragraph("Fiscal period: " + fiscalPeriod));
|
addPdfTitle(document, "Fiscal period " + fiscalPeriod + " | Generated " + generatedAt(), 10, new Color(90, 96, 106));
|
||||||
document.add(new Paragraph(" "));
|
document.add(new Paragraph(" "));
|
||||||
for (Map.Entry<String, BigDecimal> metric : payload.getMetrics().entrySet()) {
|
addSummaryTable(document, payload.getMetrics());
|
||||||
document.add(new Paragraph(metric.getKey() + ": " + metric.getValue()));
|
addDetailTable(document, "Contributions", payload.getContributionRows(), 8);
|
||||||
}
|
addDetailTable(document, "Eliminations", payload.getEliminationRows(), 8);
|
||||||
|
addDetailTable(document, "Forecast", payload.getForecastRows(), 8);
|
||||||
document.close();
|
document.close();
|
||||||
return outputStream.toByteArray();
|
return outputStream.toByteArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void writeRows(Sheet sheet, List<Map<String, Object>> rows) {
|
private void buildSummarySheet(
|
||||||
|
XSSFWorkbook workbook,
|
||||||
|
String fiscalPeriod,
|
||||||
|
ConsolidationPayload payload,
|
||||||
|
Map<String, CellStyle> styles
|
||||||
|
) {
|
||||||
|
Sheet sheet = workbook.createSheet("Summary");
|
||||||
|
sheet.setDisplayGridlines(false);
|
||||||
|
|
||||||
|
Row titleRow = sheet.createRow(0);
|
||||||
|
titleRow.setHeightInPoints(28);
|
||||||
|
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, "Fiscal period " + fiscalPeriod + " | Generated " + generatedAt(), styles.get("subtitle"));
|
||||||
|
sheet.addMergedRegion(new CellRangeAddress(1, 1, 0, 3));
|
||||||
|
|
||||||
|
Row sectionRow = sheet.createRow(3);
|
||||||
|
createCell(sectionRow, 0, "Performance Snapshot", styles.get("section"));
|
||||||
|
|
||||||
|
Row headerRow = sheet.createRow(4);
|
||||||
|
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()) {
|
||||||
|
Row row = sheet.createRow(rowIndex++);
|
||||||
|
createCell(row, 0, label(metric.getKey()), styles.get("text"));
|
||||||
|
createNumericCell(row, 1, metric.getValue(), styleForKey(styles, metric.getKey()));
|
||||||
|
}
|
||||||
|
|
||||||
|
sheet.createFreezePane(0, 5);
|
||||||
|
sheet.setColumnWidth(0, 26 * 256);
|
||||||
|
sheet.setColumnWidth(1, 18 * 256);
|
||||||
|
sheet.setColumnWidth(2, 18 * 256);
|
||||||
|
sheet.setColumnWidth(3, 18 * 256);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void buildDetailSheet(
|
||||||
|
XSSFWorkbook workbook,
|
||||||
|
String sheetName,
|
||||||
|
String fiscalPeriod,
|
||||||
|
List<Map<String, Object>> rows,
|
||||||
|
Map<String, CellStyle> styles
|
||||||
|
) {
|
||||||
|
Sheet sheet = workbook.createSheet(sheetName);
|
||||||
|
sheet.setDisplayGridlines(false);
|
||||||
|
int columnCount = rows.isEmpty() ? 1 : rows.get(0).size();
|
||||||
|
|
||||||
|
Row titleRow = sheet.createRow(0);
|
||||||
|
titleRow.setHeightInPoints(24);
|
||||||
|
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, "Fiscal period " + fiscalPeriod + " | Rows " + rows.size(), styles.get("subtitle"));
|
||||||
|
sheet.addMergedRegion(new CellRangeAddress(1, 1, 0, Math.max(0, columnCount - 1)));
|
||||||
|
|
||||||
if (rows.isEmpty()) {
|
if (rows.isEmpty()) {
|
||||||
Row row = sheet.createRow(0);
|
Row emptyRow = sheet.createRow(3);
|
||||||
row.createCell(0).setCellValue("No data");
|
createCell(emptyRow, 0, "No data available", styles.get("empty"));
|
||||||
|
sheet.setColumnWidth(0, 24 * 256);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Row header = sheet.createRow(0);
|
List<String> keys = List.copyOf(rows.get(0).keySet());
|
||||||
int cellIndex = 0;
|
Row headerRow = sheet.createRow(3);
|
||||||
for (String key : rows.get(0).keySet()) {
|
for (int index = 0; index < keys.size(); index++) {
|
||||||
header.createCell(cellIndex++).setCellValue(key);
|
createCell(headerRow, index, label(keys.get(index)), styles.get("header"));
|
||||||
}
|
}
|
||||||
|
|
||||||
int rowIndex = 1;
|
int rowIndex = 4;
|
||||||
for (Map<String, Object> item : rows) {
|
for (Map<String, Object> item : rows) {
|
||||||
Row row = sheet.createRow(rowIndex++);
|
Row row = sheet.createRow(rowIndex++);
|
||||||
int valueIndex = 0;
|
for (int cellIndex = 0; cellIndex < keys.size(); cellIndex++) {
|
||||||
for (Object value : item.values()) {
|
String key = keys.get(cellIndex);
|
||||||
row.createCell(valueIndex++).setCellValue(value == null ? "" : String.valueOf(value));
|
writeValueCell(row, cellIndex, key, item.get(key), styles);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sheet.createFreezePane(0, 4);
|
||||||
|
sheet.setAutoFilter(new CellRangeAddress(3, rowIndex - 1, 0, keys.size() - 1));
|
||||||
|
for (int index = 0; index < keys.size(); index++) {
|
||||||
|
sheet.autoSizeColumn(index);
|
||||||
|
int currentWidth = sheet.getColumnWidth(index);
|
||||||
|
sheet.setColumnWidth(index, Math.min(currentWidth + 1024, 28 * 256));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, CellStyle> createExcelStyles(XSSFWorkbook workbook) {
|
||||||
|
Map<String, CellStyle> styles = new LinkedHashMap<>();
|
||||||
|
short amountFormat = workbook.createDataFormat().getFormat("#,##0.00");
|
||||||
|
short percentFormat = workbook.createDataFormat().getFormat("0.00%");
|
||||||
|
|
||||||
|
var titleFont = workbook.createFont();
|
||||||
|
titleFont.setBold(true);
|
||||||
|
titleFont.setColor(org.apache.poi.ss.usermodel.IndexedColors.WHITE.getIndex());
|
||||||
|
titleFont.setFontHeightInPoints((short) 14);
|
||||||
|
|
||||||
|
var subtitleFont = workbook.createFont();
|
||||||
|
subtitleFont.setColor(org.apache.poi.ss.usermodel.IndexedColors.GREY_80_PERCENT.getIndex());
|
||||||
|
|
||||||
|
var sectionFont = workbook.createFont();
|
||||||
|
sectionFont.setBold(true);
|
||||||
|
sectionFont.setColor(org.apache.poi.ss.usermodel.IndexedColors.DARK_BLUE.getIndex());
|
||||||
|
|
||||||
|
var headerFont = workbook.createFont();
|
||||||
|
headerFont.setBold(true);
|
||||||
|
headerFont.setColor(org.apache.poi.ss.usermodel.IndexedColors.WHITE.getIndex());
|
||||||
|
|
||||||
|
var emptyFont = workbook.createFont();
|
||||||
|
emptyFont.setItalic(true);
|
||||||
|
emptyFont.setUnderline(FontUnderline.SINGLE);
|
||||||
|
emptyFont.setColor(org.apache.poi.ss.usermodel.IndexedColors.GREY_50_PERCENT.getIndex());
|
||||||
|
|
||||||
|
CellStyle title = workbook.createCellStyle();
|
||||||
|
title.setFont(titleFont);
|
||||||
|
title.setFillForegroundColor(org.apache.poi.ss.usermodel.IndexedColors.DARK_BLUE.getIndex());
|
||||||
|
title.setFillPattern(FillPatternType.SOLID_FOREGROUND);
|
||||||
|
title.setAlignment(HorizontalAlignment.LEFT);
|
||||||
|
title.setVerticalAlignment(VerticalAlignment.CENTER);
|
||||||
|
title.setBorderBottom(BorderStyle.THIN);
|
||||||
|
styles.put("title", title);
|
||||||
|
|
||||||
|
CellStyle subtitle = workbook.createCellStyle();
|
||||||
|
subtitle.setFont(subtitleFont);
|
||||||
|
subtitle.setAlignment(HorizontalAlignment.LEFT);
|
||||||
|
styles.put("subtitle", subtitle);
|
||||||
|
|
||||||
|
CellStyle section = workbook.createCellStyle();
|
||||||
|
section.setFont(sectionFont);
|
||||||
|
section.setAlignment(HorizontalAlignment.LEFT);
|
||||||
|
styles.put("section", section);
|
||||||
|
|
||||||
|
CellStyle header = workbook.createCellStyle();
|
||||||
|
header.setFont(headerFont);
|
||||||
|
header.setFillForegroundColor(org.apache.poi.ss.usermodel.IndexedColors.ORANGE.getIndex());
|
||||||
|
header.setFillPattern(FillPatternType.SOLID_FOREGROUND);
|
||||||
|
header.setAlignment(HorizontalAlignment.CENTER);
|
||||||
|
header.setVerticalAlignment(VerticalAlignment.CENTER);
|
||||||
|
applyBorder(header);
|
||||||
|
styles.put("header", header);
|
||||||
|
|
||||||
|
CellStyle text = workbook.createCellStyle();
|
||||||
|
text.setAlignment(HorizontalAlignment.LEFT);
|
||||||
|
text.setVerticalAlignment(VerticalAlignment.CENTER);
|
||||||
|
applyBorder(text);
|
||||||
|
styles.put("text", text);
|
||||||
|
|
||||||
|
CellStyle number = workbook.createCellStyle();
|
||||||
|
number.cloneStyleFrom(text);
|
||||||
|
number.setAlignment(HorizontalAlignment.RIGHT);
|
||||||
|
number.setDataFormat(amountFormat);
|
||||||
|
styles.put("number", number);
|
||||||
|
|
||||||
|
CellStyle ratio = workbook.createCellStyle();
|
||||||
|
ratio.cloneStyleFrom(number);
|
||||||
|
ratio.setDataFormat(percentFormat);
|
||||||
|
styles.put("ratio", ratio);
|
||||||
|
|
||||||
|
CellStyle empty = workbook.createCellStyle();
|
||||||
|
empty.setFont(emptyFont);
|
||||||
|
styles.put("empty", empty);
|
||||||
|
|
||||||
|
return styles;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyBorder(CellStyle style) {
|
||||||
|
style.setBorderTop(BorderStyle.THIN);
|
||||||
|
style.setBorderRight(BorderStyle.THIN);
|
||||||
|
style.setBorderBottom(BorderStyle.THIN);
|
||||||
|
style.setBorderLeft(BorderStyle.THIN);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void writeValueCell(Row row, int cellIndex, String key, Object value, Map<String, CellStyle> styles) {
|
||||||
|
if (value instanceof BigDecimal decimal) {
|
||||||
|
createNumericCell(row, cellIndex, decimal, styleForKey(styles, key));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (value instanceof Number number) {
|
||||||
|
createNumericCell(row, cellIndex, BigDecimal.valueOf(number.doubleValue()), styleForKey(styles, key));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
createCell(row, cellIndex, value == null ? "" : String.valueOf(value), styles.get("text"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createCell(Row row, int index, String value, CellStyle style) {
|
||||||
|
var cell = row.createCell(index);
|
||||||
|
cell.setCellValue(value);
|
||||||
|
cell.setCellStyle(style);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createNumericCell(Row row, int index, BigDecimal value, CellStyle style) {
|
||||||
|
var cell = row.createCell(index);
|
||||||
|
cell.setCellValue(value.doubleValue());
|
||||||
|
cell.setCellStyle(style);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = FontFactory.getFont(FontFactory.HELVETICA_BOLD, size, color);
|
||||||
|
Paragraph paragraph = new Paragraph(text, font);
|
||||||
|
paragraph.setAlignment(Element.ALIGN_LEFT);
|
||||||
|
paragraph.setSpacingAfter(4f);
|
||||||
|
document.add(paragraph);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addSummaryTable(Document document, Map<String, BigDecimal> metrics) {
|
||||||
|
PdfPTable table = new PdfPTable(new float[] {3f, 1.3f});
|
||||||
|
table.setWidthPercentage(100f);
|
||||||
|
table.setSpacingBefore(6f);
|
||||||
|
table.setSpacingAfter(12f);
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
document.add(table);
|
||||||
|
}
|
||||||
|
|
||||||
|
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));
|
||||||
|
Paragraph section = new Paragraph(title, sectionFont);
|
||||||
|
section.setSpacingBefore(8f);
|
||||||
|
section.setSpacingAfter(6f);
|
||||||
|
document.add(section);
|
||||||
|
|
||||||
|
if (rows.isEmpty()) {
|
||||||
|
document.add(new Paragraph("No data available", FontFactory.getFont(FontFactory.HELVETICA_OBLIQUE, 10, new Color(120, 120, 120))));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> keys = List.copyOf(rows.get(0).keySet());
|
||||||
|
PdfPTable table = new PdfPTable(keys.size());
|
||||||
|
table.setWidthPercentage(100f);
|
||||||
|
table.setSpacingAfter(8f);
|
||||||
|
for (String key : keys) {
|
||||||
|
table.addCell(pdfHeaderCell(label(key)));
|
||||||
|
}
|
||||||
|
|
||||||
|
int visibleRows = Math.min(maxRows, rows.size());
|
||||||
|
for (int rowIndex = 0; rowIndex < visibleRows; rowIndex++) {
|
||||||
|
Map<String, Object> row = rows.get(rowIndex);
|
||||||
|
for (String key : keys) {
|
||||||
|
int alignment = isNumericValue(row.get(key)) ? Element.ALIGN_RIGHT : Element.ALIGN_LEFT;
|
||||||
|
table.addCell(pdfBodyCell(format(row.get(key), key), alignment));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
document.add(table);
|
||||||
|
|
||||||
|
if (rows.size() > maxRows) {
|
||||||
|
document.add(new Paragraph(
|
||||||
|
"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, 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));
|
||||||
|
cell.setBorderColor(new Color(220, 220, 220));
|
||||||
|
cell.setPadding(6f);
|
||||||
|
return cell;
|
||||||
|
}
|
||||||
|
|
||||||
|
private PdfPCell pdfBodyCell(String text, int alignment) {
|
||||||
|
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));
|
||||||
|
cell.setPadding(5f);
|
||||||
|
return cell;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isNumericValue(Object value) {
|
||||||
|
return value instanceof Number || value instanceof BigDecimal;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String format(Object value, String key) {
|
||||||
|
if (value == null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
if (value instanceof BigDecimal decimal) {
|
||||||
|
return format(decimal, key);
|
||||||
|
}
|
||||||
|
if (value instanceof Number number) {
|
||||||
|
return format(BigDecimal.valueOf(number.doubleValue()), key);
|
||||||
|
}
|
||||||
|
return String.valueOf(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String format(BigDecimal value, String key) {
|
||||||
|
if (key.toLowerCase().contains("ratio")) {
|
||||||
|
return AMOUNT_FORMAT.format(value.multiply(BigDecimal.valueOf(100))) + "%";
|
||||||
|
}
|
||||||
|
return AMOUNT_FORMAT.format(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String label(String key) {
|
||||||
|
return LABELS.getOrDefault(key, key);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String generatedAt() {
|
||||||
|
return ZonedDateTime.now(ZoneId.of("Asia/Seoul")).format(REPORT_TIME_FORMAT);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Map<String, String> buildLabels() {
|
||||||
|
Map<String, String> labels = new LinkedHashMap<>();
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import org.springframework.core.io.ByteArrayResource;
|
||||||
import org.springframework.http.HttpHeaders;
|
import org.springframework.http.HttpHeaders;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.PathVariable;
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
|
@ -46,6 +47,12 @@ public class UploadController {
|
||||||
return uploadService.loadIssues(batchId);
|
return uploadService.loadIssues(batchId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/{batchId}")
|
||||||
|
public Map<String, Object> delete(@PathVariable Long batchId, HttpSession session) {
|
||||||
|
authService.requireRole(session, "OPERATOR");
|
||||||
|
return uploadService.deleteUploadBatch(batchId);
|
||||||
|
}
|
||||||
|
|
||||||
@GetMapping("/templates/{templateCode}/download")
|
@GetMapping("/templates/{templateCode}/download")
|
||||||
public ResponseEntity<ByteArrayResource> templateDownload(@PathVariable String templateCode, HttpSession session) {
|
public ResponseEntity<ByteArrayResource> templateDownload(@PathVariable String templateCode, HttpSession session) {
|
||||||
authService.requireRole(session, "VIEWER");
|
authService.requireRole(session, "VIEWER");
|
||||||
|
|
@ -55,5 +62,18 @@ public class UploadController {
|
||||||
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + templateCode + "-template.xlsx\"")
|
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + templateCode + "-template.xlsx\"")
|
||||||
.body(resource);
|
.body(resource);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
@GetMapping("/samples/{sampleCode}/download")
|
||||||
|
public ResponseEntity<ByteArrayResource> sampleDownload(
|
||||||
|
@PathVariable String sampleCode,
|
||||||
|
@RequestParam String fiscalPeriod,
|
||||||
|
HttpSession session
|
||||||
|
) {
|
||||||
|
authService.requireRole(session, "VIEWER");
|
||||||
|
ByteArrayResource resource = uploadService.downloadSample(sampleCode, fiscalPeriod);
|
||||||
|
return ResponseEntity.ok()
|
||||||
|
.contentType(MediaType.parseMediaType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"))
|
||||||
|
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + sampleCode + "-" + fiscalPeriod + ".xlsx\"")
|
||||||
|
.body(resource);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package com.hanwha.nexacrodemo.upload;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import org.apache.ibatis.annotations.Delete;
|
||||||
import org.apache.ibatis.annotations.Insert;
|
import org.apache.ibatis.annotations.Insert;
|
||||||
import org.apache.ibatis.annotations.Mapper;
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
import org.apache.ibatis.annotations.Options;
|
import org.apache.ibatis.annotations.Options;
|
||||||
|
|
@ -154,5 +155,22 @@ public interface UploadMapper {
|
||||||
order by ur.id
|
order by ur.id
|
||||||
""")
|
""")
|
||||||
List<Map<String, Object>> listAcceptedRows(@Param("templateCode") String templateCode, @Param("fiscalPeriod") String fiscalPeriod);
|
List<Map<String, Object>> listAcceptedRows(@Param("templateCode") String templateCode, @Param("fiscalPeriod") String fiscalPeriod);
|
||||||
}
|
|
||||||
|
|
||||||
|
@Delete("""
|
||||||
|
delete from validation_issue
|
||||||
|
where batch_id = #{batchId}
|
||||||
|
""")
|
||||||
|
int deleteValidationIssuesByBatchId(Long batchId);
|
||||||
|
|
||||||
|
@Delete("""
|
||||||
|
delete from upload_row
|
||||||
|
where batch_id = #{batchId}
|
||||||
|
""")
|
||||||
|
int deleteUploadRowsByBatchId(Long batchId);
|
||||||
|
|
||||||
|
@Delete("""
|
||||||
|
delete from upload_batch
|
||||||
|
where id = #{batchId}
|
||||||
|
""")
|
||||||
|
int deleteUploadBatchById(Long batchId);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ import org.apache.poi.ss.usermodel.WorkbookFactory;
|
||||||
import org.springframework.core.io.ByteArrayResource;
|
import org.springframework.core.io.ByteArrayResource;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
|
|
@ -133,10 +134,35 @@ public class UploadService {
|
||||||
return MapKeyUtils.camelizeList(uploadMapper.listValidationIssuesByBatchId(batchId));
|
return MapKeyUtils.camelizeList(uploadMapper.listValidationIssuesByBatchId(batchId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public Map<String, Object> deleteUploadBatch(Long batchId) {
|
||||||
|
Map<String, Object> batch = uploadMapper.findBatchById(batchId);
|
||||||
|
if (batch == null) {
|
||||||
|
throw new ApiException(HttpStatus.NOT_FOUND, "삭제할 업로드 이력을 찾을 수 없습니다.");
|
||||||
|
}
|
||||||
|
|
||||||
|
uploadMapper.deleteValidationIssuesByBatchId(batchId);
|
||||||
|
uploadMapper.deleteUploadRowsByBatchId(batchId);
|
||||||
|
int deleted = uploadMapper.deleteUploadBatchById(batchId);
|
||||||
|
if (deleted != 1) {
|
||||||
|
throw new ApiException(HttpStatus.INTERNAL_SERVER_ERROR, "업로드 이력 삭제에 실패했습니다.");
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, Object> payload = new LinkedHashMap<>();
|
||||||
|
payload.put("ok", true);
|
||||||
|
payload.put("batchId", batchId);
|
||||||
|
payload.put("message", "업로드 이력이 삭제되었습니다.");
|
||||||
|
return payload;
|
||||||
|
}
|
||||||
|
|
||||||
public ByteArrayResource downloadTemplate(String templateCode) {
|
public ByteArrayResource downloadTemplate(String templateCode) {
|
||||||
return new ByteArrayResource(workbookTemplateService.createTemplate(templateCode));
|
return new ByteArrayResource(workbookTemplateService.createTemplate(templateCode));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ByteArrayResource downloadSample(String sampleCode, String fiscalPeriod) {
|
||||||
|
return new ByteArrayResource(workbookTemplateService.createSample(sampleCode, fiscalPeriod));
|
||||||
|
}
|
||||||
|
|
||||||
private void parseDataRows(
|
private void parseDataRows(
|
||||||
String templateCode,
|
String templateCode,
|
||||||
String fiscalPeriod,
|
String fiscalPeriod,
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
import org.apache.poi.ss.usermodel.Row;
|
import org.apache.poi.ss.usermodel.Row;
|
||||||
import org.apache.poi.xssf.usermodel.XSSFSheet;
|
import org.apache.poi.xssf.usermodel.XSSFSheet;
|
||||||
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
|
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
|
||||||
|
|
@ -14,6 +15,8 @@ import org.springframework.stereotype.Service;
|
||||||
@Service
|
@Service
|
||||||
public class WorkbookTemplateService {
|
public class WorkbookTemplateService {
|
||||||
|
|
||||||
|
private static final Pattern PERIOD_PATTERN = Pattern.compile("^\\d{4}-\\d{2}$");
|
||||||
|
|
||||||
private static final Map<String, List<String>> HEADERS = Map.of(
|
private static final Map<String, List<String>> HEADERS = Map.of(
|
||||||
"trial-balance", List.of("fiscalPeriod", "entityCode", "accountCode", "partnerEntityCode", "currencyCode", "debitAmount", "creditAmount"),
|
"trial-balance", List.of("fiscalPeriod", "entityCode", "accountCode", "partnerEntityCode", "currencyCode", "debitAmount", "creditAmount"),
|
||||||
"forecast", List.of("fiscalPeriod", "entityCode", "accountCode", "currencyCode", "scenarioCode", "amountValue")
|
"forecast", List.of("fiscalPeriod", "entityCode", "accountCode", "currencyCode", "scenarioCode", "amountValue")
|
||||||
|
|
@ -28,6 +31,34 @@ public class WorkbookTemplateService {
|
||||||
}
|
}
|
||||||
|
|
||||||
public byte[] createTemplate(String templateCode) {
|
public byte[] createTemplate(String templateCode) {
|
||||||
|
return createWorkbook(templateCode, List.of());
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] createSample(String sampleCode, String fiscalPeriod) {
|
||||||
|
if (!PERIOD_PATTERN.matcher(fiscalPeriod).matches()) {
|
||||||
|
throw new ApiException(HttpStatus.BAD_REQUEST, "회계기간 형식은 YYYY-MM 이어야 합니다.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return switch (sampleCode) {
|
||||||
|
case "trial-balance-invalid" -> createWorkbook("trial-balance", List.of(
|
||||||
|
List.of(fiscalPeriod, "US1", "AR_INT", "", "USD", "100", "0"),
|
||||||
|
List.of(fiscalPeriod, "HQ", "REV_EXT", "", "KRW", "0", "70")
|
||||||
|
));
|
||||||
|
case "trial-balance-valid" -> createWorkbook("trial-balance", List.of(
|
||||||
|
List.of(fiscalPeriod, "US1", "AR_INT", "HQ", "USD", "100", "0"),
|
||||||
|
List.of(fiscalPeriod, "HQ", "AP_INT", "US1", "KRW", "0", "100"),
|
||||||
|
List.of(fiscalPeriod, "HQ", "REV_EXT", "", "KRW", "0", "200000"),
|
||||||
|
List.of(fiscalPeriod, "HQ", "EXP_OPEX", "", "KRW", "200000", "0")
|
||||||
|
));
|
||||||
|
case "forecast-valid" -> createWorkbook("forecast", List.of(
|
||||||
|
List.of(fiscalPeriod, "US1", "REV_EXT", "USD", "PLAN", "500"),
|
||||||
|
List.of(fiscalPeriod, "SG1", "EXP_OPEX", "SGD", "FORECAST", "100")
|
||||||
|
));
|
||||||
|
default -> throw new ApiException(HttpStatus.BAD_REQUEST, "지원하지 않는 샘플입니다: " + sampleCode);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] createWorkbook(String templateCode, List<List<String>> rows) {
|
||||||
try (XSSFWorkbook workbook = new XSSFWorkbook()) {
|
try (XSSFWorkbook workbook = new XSSFWorkbook()) {
|
||||||
XSSFSheet metaSheet = workbook.createSheet("META");
|
XSSFSheet metaSheet = workbook.createSheet("META");
|
||||||
Row metaHeader = metaSheet.createRow(0);
|
Row metaHeader = metaSheet.createRow(0);
|
||||||
|
|
@ -46,6 +77,13 @@ public class WorkbookTemplateService {
|
||||||
for (int index = 0; index < headers.size(); index++) {
|
for (int index = 0; index < headers.size(); index++) {
|
||||||
header.createCell(index).setCellValue(headers.get(index));
|
header.createCell(index).setCellValue(headers.get(index));
|
||||||
}
|
}
|
||||||
|
int rowIndex = 1;
|
||||||
|
for (List<String> rowValues : rows) {
|
||||||
|
Row row = dataSheet.createRow(rowIndex++);
|
||||||
|
for (int cellIndex = 0; cellIndex < rowValues.size(); cellIndex++) {
|
||||||
|
row.createCell(cellIndex).setCellValue(rowValues.get(cellIndex));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||||
workbook.write(outputStream);
|
workbook.write(outputStream);
|
||||||
|
|
@ -55,4 +93,3 @@ public class WorkbookTemplateService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,12 @@ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilder
|
||||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import com.hanwha.nexacrodemo.upload.TestWorkbookFactory;
|
import com.hanwha.nexacrodemo.upload.TestWorkbookFactory;
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import org.apache.poi.ss.usermodel.WorkbookFactory;
|
||||||
|
import org.junit.jupiter.api.Assertions;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
|
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
|
||||||
|
|
@ -29,6 +34,9 @@ class ConsolidationIntegrationTest {
|
||||||
@Autowired
|
@Autowired
|
||||||
private ConsolidationService consolidationService;
|
private ConsolidationService consolidationService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ObjectMapper objectMapper;
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void validUploadsCanBeConsolidatedAndReported() throws Exception {
|
void validUploadsCanBeConsolidatedAndReported() throws Exception {
|
||||||
MockHttpSession session = login("operator", "demo1234");
|
MockHttpSession session = login("operator", "demo1234");
|
||||||
|
|
@ -55,10 +63,33 @@ class ConsolidationIntegrationTest {
|
||||||
.andExpect(status().isOk())
|
.andExpect(status().isOk())
|
||||||
.andExpect(jsonPath("$.datasets.runs[0].statusCode").value("SUCCESS"));
|
.andExpect(jsonPath("$.datasets.runs[0].statusCode").value("SUCCESS"));
|
||||||
|
|
||||||
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.artifacts.length()").value(2))
|
||||||
.andExpect(jsonPath("$.datasets.jobLogs[0].logLevel").exists());
|
.andExpect(jsonPath("$.datasets.jobLogs[0].logLevel").exists())
|
||||||
|
.andReturn();
|
||||||
|
|
||||||
|
JsonNode artifacts = objectMapper.readTree(overviewResult.getResponse().getContentAsByteArray())
|
||||||
|
.path("datasets")
|
||||||
|
.path("artifacts");
|
||||||
|
long excelArtifactId = -1L;
|
||||||
|
for (JsonNode artifact : artifacts) {
|
||||||
|
if ("EXCEL".equals(artifact.path("artifactType").asText())) {
|
||||||
|
excelArtifactId = artifact.path("id").asLong();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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("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"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void upload(MockHttpSession session, String templateCode, String fileName, byte[] content) throws Exception {
|
private void upload(MockHttpSession session, String templateCode, String fileName, byte[] content) throws Exception {
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,15 @@
|
||||||
package com.hanwha.nexacrodemo.upload;
|
package com.hanwha.nexacrodemo.upload;
|
||||||
|
|
||||||
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
|
||||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart;
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart;
|
||||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.apache.poi.ss.usermodel.WorkbookFactory;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
|
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
|
||||||
import org.springframework.boot.test.context.SpringBootTest;
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
|
|
@ -52,6 +55,81 @@ class UploadValidationIntegrationTest {
|
||||||
.andExpect(jsonPath("$.datasets.validationIssues[0].issueCode").exists());
|
.andExpect(jsonPath("$.datasets.validationIssues[0].issueCode").exists());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void operatorCanDeleteUploadBatchAndRelatedIssuesDisappearFromOverview() throws Exception {
|
||||||
|
MockHttpSession session = login("operator", "demo1234");
|
||||||
|
MockMultipartFile file = new MockMultipartFile(
|
||||||
|
"file",
|
||||||
|
"trial-balance-invalid.xlsx",
|
||||||
|
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||||
|
TestWorkbookFactory.trialBalanceInvalid()
|
||||||
|
);
|
||||||
|
|
||||||
|
MvcResult uploadResult = mockMvc.perform(multipart("/api/uploads")
|
||||||
|
.file(file)
|
||||||
|
.param("templateCode", "trial-balance")
|
||||||
|
.param("fiscalPeriod", "2026-03")
|
||||||
|
.session(session))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andReturn();
|
||||||
|
|
||||||
|
String uploadPayload = uploadResult.getResponse().getContentAsString();
|
||||||
|
String batchId = uploadPayload.replaceAll(".*\"id\":(\\d+).*", "$1");
|
||||||
|
|
||||||
|
mockMvc.perform(delete("/api/uploads/{batchId}", batchId).session(session))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.ok").value(true))
|
||||||
|
.andExpect(jsonPath("$.batchId").value(Integer.parseInt(batchId)));
|
||||||
|
|
||||||
|
mockMvc.perform(get("/api/tx/uploads/overview").session(session))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.datasets.uploadBatches[?(@.id == " + batchId + ")]").isEmpty())
|
||||||
|
.andExpect(jsonPath("$.datasets.validationIssues[?(@.batchId == " + batchId + ")]").isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void viewerCannotDeleteUploadBatch() throws Exception {
|
||||||
|
MockHttpSession operatorSession = login("operator", "demo1234");
|
||||||
|
MockHttpSession viewerSession = login("viewer", "demo1234");
|
||||||
|
MockMultipartFile file = new MockMultipartFile(
|
||||||
|
"file",
|
||||||
|
"trial-balance-invalid.xlsx",
|
||||||
|
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||||
|
TestWorkbookFactory.trialBalanceInvalid()
|
||||||
|
);
|
||||||
|
|
||||||
|
MvcResult uploadResult = mockMvc.perform(multipart("/api/uploads")
|
||||||
|
.file(file)
|
||||||
|
.param("templateCode", "trial-balance")
|
||||||
|
.param("fiscalPeriod", "2026-03")
|
||||||
|
.session(operatorSession))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andReturn();
|
||||||
|
|
||||||
|
String uploadPayload = uploadResult.getResponse().getContentAsString();
|
||||||
|
String batchId = uploadPayload.replaceAll(".*\"id\":(\\d+).*", "$1");
|
||||||
|
|
||||||
|
mockMvc.perform(delete("/api/uploads/{batchId}", batchId).session(viewerSession))
|
||||||
|
.andExpect(status().isForbidden());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void sampleDownloadUsesRequestedFiscalPeriod() throws Exception {
|
||||||
|
MockHttpSession session = login("viewer", "demo1234");
|
||||||
|
|
||||||
|
MvcResult result = mockMvc.perform(get("/api/uploads/samples/trial-balance-valid/download")
|
||||||
|
.param("fiscalPeriod", "2026-04")
|
||||||
|
.session(session))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andReturn();
|
||||||
|
|
||||||
|
try (var workbook = WorkbookFactory.create(new ByteArrayInputStream(result.getResponse().getContentAsByteArray()))) {
|
||||||
|
var dataSheet = workbook.getSheet("DATA");
|
||||||
|
var firstDataRow = dataSheet.getRow(1);
|
||||||
|
org.junit.jupiter.api.Assertions.assertEquals("2026-04", firstDataRow.getCell(0).getStringCellValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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)
|
||||||
|
|
|
||||||
|
|
@ -57,22 +57,36 @@ function table(columns, rows, options = {}) {
|
||||||
const value = options.render ? options.render(column, row[column.id], row) : row[column.id];
|
const value = options.render ? options.render(column, row[column.id], row) : row[column.id];
|
||||||
return `<td>${formatValue(value)}</td>`;
|
return `<td>${formatValue(value)}</td>`;
|
||||||
})
|
})
|
||||||
.join("")}</tr>`
|
.join("")}${options.actions ? `<td class="actions-cell">${options.actions(row) || ""}</td>` : ""}</tr>`
|
||||||
)
|
)
|
||||||
.join("")
|
.join("")
|
||||||
: `<tr><td colspan="${columns.length}">데이터가 없습니다.</td></tr>`;
|
: `<tr><td colspan="${columns.length + (options.actions ? 1 : 0)}">데이터가 없습니다.</td></tr>`;
|
||||||
|
|
||||||
return `<div class="table-wrap"><table><thead><tr>${columns
|
return `<div class="table-wrap"><table><thead><tr>${columns
|
||||||
.map((column) => `<th>${column.text}</th>`)
|
.map((column) => `<th>${column.text}</th>`)
|
||||||
.join("")}</tr></thead><tbody>${body}</tbody></table></div>`;
|
.join("")}${options.actions ? "<th>작업</th>" : ""}</tr></thead><tbody>${body}</tbody></table></div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function pill(value) {
|
function pill(value) {
|
||||||
return `<span class="pill ${value}">${value}</span>`;
|
return `<span class="pill ${value}">${value}</span>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hasRole(requiredRole) {
|
||||||
|
const roleOrder = {
|
||||||
|
PUBLIC: 0,
|
||||||
|
VIEWER: 10,
|
||||||
|
OPERATOR: 20,
|
||||||
|
ADMIN: 30
|
||||||
|
};
|
||||||
|
return (roleOrder[state.session?.roleCode] || 0) >= (roleOrder[requiredRole] || 0);
|
||||||
|
}
|
||||||
|
|
||||||
function isAdmin() {
|
function isAdmin() {
|
||||||
return state.session?.roleCode === "ADMIN";
|
return hasRole("ADMIN");
|
||||||
|
}
|
||||||
|
|
||||||
|
function isOperator() {
|
||||||
|
return hasRole("OPERATOR");
|
||||||
}
|
}
|
||||||
|
|
||||||
function cloneRows(rows) {
|
function cloneRows(rows) {
|
||||||
|
|
@ -350,11 +364,11 @@ function renderUploads() {
|
||||||
<div class="row"><label>회계기간</label><input id="upload-period" value="2026-03" /></div>
|
<div class="row"><label>회계기간</label><input id="upload-period" value="2026-03" /></div>
|
||||||
<div class="row"><label>파일 선택</label><input id="upload-file" type="file" /></div>
|
<div class="row"><label>파일 선택</label><input id="upload-file" type="file" /></div>
|
||||||
<div class="row actions">
|
<div class="row actions">
|
||||||
<button data-action="upload">파일 업로드</button>
|
<button data-action="upload" ${isOperator() ? "" : "disabled"}>파일 업로드</button>
|
||||||
<button class="secondary" data-action="reload-uploads">내역 새로고침</button>
|
<button class="secondary" data-action="reload-uploads">내역 새로고침</button>
|
||||||
<a class="ghost" href="/sample-data/trial-balance-invalid.xlsx" download>오류 샘플</a>
|
<button class="ghost" data-action="download-sample" data-sample-code="trial-balance-invalid">오류 샘플</button>
|
||||||
<a class="ghost" href="/sample-data/trial-balance-valid.xlsx" download>정상 TB</a>
|
<button class="ghost" data-action="download-sample" data-sample-code="trial-balance-valid">정상 TB</button>
|
||||||
<a class="ghost" href="/sample-data/forecast-valid.xlsx" download>정상 Forecast</a>
|
<button class="ghost" data-action="download-sample" data-sample-code="forecast-valid">정상 Forecast</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel">
|
<div class="panel">
|
||||||
|
|
@ -377,6 +391,9 @@ function renderUploads() {
|
||||||
return pill(value);
|
return pill(value);
|
||||||
}
|
}
|
||||||
return value;
|
return value;
|
||||||
|
},
|
||||||
|
actions(row) {
|
||||||
|
return `<button class="danger small" data-action="delete-upload" data-batch-id="${row.id}" ${isOperator() ? "" : "disabled"}>삭제</button>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
|
|
@ -519,14 +536,6 @@ function shellContent(content) {
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
<main class="main">
|
<main class="main">
|
||||||
<div class="topbar">
|
|
||||||
<div></div>
|
|
||||||
<div class="footer-links">
|
|
||||||
<a href="/sample-data/trial-balance-invalid.xlsx" download>오류 샘플</a>
|
|
||||||
<a href="/sample-data/trial-balance-valid.xlsx" download>정상 TB</a>
|
|
||||||
<a href="/sample-data/forecast-valid.xlsx" download>정상 Forecast</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
${content}
|
${content}
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -723,6 +732,9 @@ function bindEvents() {
|
||||||
const uploadButton = document.querySelector("[data-action='upload']");
|
const uploadButton = document.querySelector("[data-action='upload']");
|
||||||
if (uploadButton) {
|
if (uploadButton) {
|
||||||
uploadButton.addEventListener("click", async () => {
|
uploadButton.addEventListener("click", async () => {
|
||||||
|
if (!isOperator()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const templateCode = document.getElementById("upload-template").value;
|
const templateCode = document.getElementById("upload-template").value;
|
||||||
const fiscalPeriod = document.getElementById("upload-period").value;
|
const fiscalPeriod = document.getElementById("upload-period").value;
|
||||||
const file = document.getElementById("upload-file").files[0];
|
const file = document.getElementById("upload-file").files[0];
|
||||||
|
|
@ -748,6 +760,30 @@ function bindEvents() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
document.querySelectorAll("[data-action='download-sample']").forEach((button) => {
|
||||||
|
button.addEventListener("click", () => {
|
||||||
|
const fiscalPeriod = document.getElementById("upload-period")?.value?.trim() || "2026-03";
|
||||||
|
const sampleCode = button.dataset.sampleCode;
|
||||||
|
window.location.href = `/api/uploads/samples/${sampleCode}/download?fiscalPeriod=${encodeURIComponent(fiscalPeriod)}`;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelectorAll("[data-action='delete-upload']").forEach((button) => {
|
||||||
|
button.addEventListener("click", async () => {
|
||||||
|
if (!isOperator()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const batchId = button.dataset.batchId;
|
||||||
|
const confirmed = window.confirm(`업로드 이력 ${batchId}번을 삭제하시겠습니까? 관련 오류내역과 업로드 행도 함께 삭제됩니다.`);
|
||||||
|
if (!confirmed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await api(`/api/uploads/${batchId}`, { method: "DELETE" });
|
||||||
|
await loadUploads();
|
||||||
|
render();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
const runButton = document.querySelector("[data-action='request-run']");
|
const runButton = document.querySelector("[data-action='request-run']");
|
||||||
if (runButton) {
|
if (runButton) {
|
||||||
runButton.addEventListener("click", async () => {
|
runButton.addEventListener("click", async () => {
|
||||||
|
|
|
||||||
|
|
@ -270,13 +270,17 @@ th {
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.actions-cell {
|
||||||
|
white-space: nowrap;
|
||||||
|
width: 88px;
|
||||||
|
}
|
||||||
|
|
||||||
.master-table input {
|
.master-table input {
|
||||||
min-width: 110px;
|
min-width: 110px;
|
||||||
padding: 10px 12px;
|
padding: 10px 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.master-table td.actions-cell {
|
.master-table td.actions-cell {
|
||||||
white-space: nowrap;
|
|
||||||
width: 88px;
|
width: 88px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<Project version="2.1" nexacrosdk="Latest Version" sdkversion="">
|
<Project version="2.1" nexacrosdk="Latest Version" sdkversion="">
|
||||||
<EnvironmentDefinition url="/environment.xml"/>
|
<EnvironmentDefinition url="environment.xml"/>
|
||||||
<TypeDefinition url="/typedefinition.xml"/>
|
<TypeDefinition url="typedefinition.xml"/>
|
||||||
<AppVariables url="/appvariables.xml"/>
|
<AppVariables url="appvariables.xml"/>
|
||||||
<AppInfos>
|
<AppInfos>
|
||||||
<AppInfo url="/Application_Desktop.xadl"/>
|
<AppInfo url="Application_Desktop.xadl"/>
|
||||||
</AppInfos>
|
</AppInfos>
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
||||||
|
|
@ -41,8 +41,6 @@
|
||||||
<Component type="JavaScript" id="DateField" classname="nexacro.DateField"/>
|
<Component type="JavaScript" id="DateField" classname="nexacro.DateField"/>
|
||||||
<Component type="JavaScript" id="DateRangePicker" classname="nexacro.DateRangePicker"/>
|
<Component type="JavaScript" id="DateRangePicker" classname="nexacro.DateRangePicker"/>
|
||||||
<Component type="JavaScript" id="PopupDateRangePicker" classname="nexacro.PopupDateRangePicker"/>
|
<Component type="JavaScript" id="PopupDateRangePicker" classname="nexacro.PopupDateRangePicker"/>
|
||||||
<Component type="JavaScript" id="View" classname="nexacro.View"/>
|
|
||||||
<Component type="JavaScript" id="FileUpload" classname="nexacro.FileUpload"/>
|
|
||||||
</Components>
|
</Components>
|
||||||
<Services>
|
<Services>
|
||||||
<Service prefixid="theme" type="resource" url="./_resource_/_theme_/" version="0" cachelevel="session" include_subdir="true"/>
|
<Service prefixid="theme" type="resource" url="./_resource_/_theme_/" version="0" cachelevel="session" include_subdir="true"/>
|
||||||
|
|
@ -52,8 +50,8 @@
|
||||||
<Service prefixid="font" type="resource" url="./_resource_/_font_/" version="0" cachelevel="session" include_subdir="false"/>
|
<Service prefixid="font" type="resource" url="./_resource_/_font_/" version="0" cachelevel="session" include_subdir="false"/>
|
||||||
<Service prefixid="stringrc" type="resource" url="./_resource_/_stringrc_/" version="0" cachelevel="session" include_subdir="false"/>
|
<Service prefixid="stringrc" type="resource" url="./_resource_/_stringrc_/" version="0" cachelevel="session" include_subdir="false"/>
|
||||||
<Service prefixid="extlib" type="resource" url="./_extlib_/" version="0" cachelevel="session" include_subdir="true"/>
|
<Service prefixid="extlib" type="resource" url="./_extlib_/" version="0" cachelevel="session" include_subdir="true"/>
|
||||||
|
<Service prefixid="Base" type="form" url="./Base/" version="0" cachelevel="session" include_subdir="false" communicationversion="0"/>
|
||||||
<Service prefixid="FrameBase" type="form" url="./FrameBase/" version="0" cachelevel="session" include_subdir="false" communicationversion="0"/>
|
<Service prefixid="FrameBase" type="form" url="./FrameBase/" version="0" cachelevel="session" include_subdir="false" communicationversion="0"/>
|
||||||
<Service prefixid="Forms" type="form" url="./forms/" version="0" cachelevel="session" include_subdir="false" communicationversion="0"/>
|
|
||||||
</Services>
|
</Services>
|
||||||
<Protocols/>
|
<Protocols/>
|
||||||
<DeviceAdaptors/>
|
<DeviceAdaptors/>
|
||||||
|
|
|
||||||
|
|
@ -10,9 +10,8 @@ const previewDir = path.join(repoRoot, "client", "nexacro-deploy");
|
||||||
const UTF8_BOM = "\uFEFF";
|
const UTF8_BOM = "\uFEFF";
|
||||||
const FRAME_TOP_HEIGHT = 56;
|
const FRAME_TOP_HEIGHT = 56;
|
||||||
const FRAME_LEFT_WIDTH = 240;
|
const FRAME_LEFT_WIDTH = 240;
|
||||||
const FORMS_DIR = "forms";
|
const BASE_DIR = "Base";
|
||||||
const FRAMEBASE_DIR = "FrameBase";
|
const FRAMEBASE_DIR = "FrameBase";
|
||||||
const LIB_DIR = "lib";
|
|
||||||
|
|
||||||
function readYaml(filePath) {
|
function readYaml(filePath) {
|
||||||
return yaml.load(fs.readFileSync(filePath, "utf8"));
|
return yaml.load(fs.readFileSync(filePath, "utf8"));
|
||||||
|
|
@ -62,7 +61,7 @@ function cleanGeneratedOutput(baseOutputDir, projectName) {
|
||||||
".DS_Store"
|
".DS_Store"
|
||||||
].forEach((file) => removePathIfExists(path.join(baseOutputDir, file)));
|
].forEach((file) => removePathIfExists(path.join(baseOutputDir, file)));
|
||||||
|
|
||||||
["forms", FORMS_DIR, "frame", FRAMEBASE_DIR, "lib", LIB_DIR].forEach((dir) =>
|
["forms", BASE_DIR, "frame", FRAMEBASE_DIR, "lib"].forEach((dir) =>
|
||||||
removePathIfExists(path.join(baseOutputDir, dir))
|
removePathIfExists(path.join(baseOutputDir, dir))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -104,6 +103,7 @@ function toXfdlDataset(dataset) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function componentTag(component, index = 0) {
|
function componentTag(component, index = 0) {
|
||||||
|
const componentType = component.type === "FileUpload" ? "Edit" : component.type;
|
||||||
const attrs = [
|
const attrs = [
|
||||||
`id="${component.id}"`,
|
`id="${component.id}"`,
|
||||||
`taborder="${index}"`,
|
`taborder="${index}"`,
|
||||||
|
|
@ -116,22 +116,21 @@ function componentTag(component, index = 0) {
|
||||||
if (component.text) {
|
if (component.text) {
|
||||||
attrs.push(`text="${escapeXml(component.text)}"`);
|
attrs.push(`text="${escapeXml(component.text)}"`);
|
||||||
}
|
}
|
||||||
if (component.prompt && ["Edit", "MaskEdit", "TextArea"].includes(component.type)) {
|
if (component.prompt && ["Edit", "MaskEdit", "TextArea"].includes(componentType)) {
|
||||||
attrs.push(`displaynulltext="${escapeXml(component.prompt)}"`);
|
attrs.push(`displaynulltext="${escapeXml(component.prompt)}"`);
|
||||||
}
|
}
|
||||||
if (component.bind && ["Edit", "MaskEdit", "TextArea"].includes(component.type)) {
|
if (componentType === "Edit" && component.id.toLowerCase().includes("password")) {
|
||||||
const [, columnId] = component.bind.split(".");
|
|
||||||
attrs.push(`value="bind:${columnId}"`);
|
|
||||||
}
|
|
||||||
if (component.type === "Edit" && component.id.toLowerCase().includes("password")) {
|
|
||||||
attrs.push('password="true"');
|
attrs.push('password="true"');
|
||||||
}
|
}
|
||||||
if (component.type === "Combo") {
|
if (componentType === "Combo") {
|
||||||
attrs.push('codecolumn="code"');
|
attrs.push('codecolumn="code"');
|
||||||
attrs.push('datacolumn="label"');
|
attrs.push('datacolumn="label"');
|
||||||
}
|
}
|
||||||
|
if (component.type === "FileUpload") {
|
||||||
|
attrs.push('readonly="true"');
|
||||||
|
}
|
||||||
|
|
||||||
return ` <${component.type} ${attrs.join(" ")}/>`;
|
return ` <${componentType} ${attrs.join(" ")}/>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function gridFormats(grid) {
|
function gridFormats(grid) {
|
||||||
|
|
@ -185,11 +184,21 @@ function buildFormScript(form) {
|
||||||
.trim();
|
.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderMessageStatics(form, layout) {
|
||||||
|
return (form.messages || [])
|
||||||
|
.map(
|
||||||
|
(message, index) =>
|
||||||
|
` <Static id="staMessage${index}" taborder="${900 + index}" left="36" top="${Math.max(layout.height - 76 + index * 22, 36)}" width="${Math.max(layout.width - 72, 300)}" height="20" text="${escapeXml(message.text)}"/>`
|
||||||
|
)
|
||||||
|
.join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
function renderXfdl(form, appSpec) {
|
function renderXfdl(form, appSpec) {
|
||||||
const datasets = (form.datasets || []).map(toXfdlDataset).join("\n");
|
const datasets = (form.datasets || []).map(toXfdlDataset).join("\n");
|
||||||
const components = (form.components || []).map((component, index) => componentTag(component, index)).join("\n");
|
const components = (form.components || []).map((component, index) => componentTag(component, index)).join("\n");
|
||||||
const grids = (form.grids || []).map(gridTag).join("\n");
|
const grids = (form.grids || []).map(gridTag).join("\n");
|
||||||
const layout = form.layout || appSpec.layout;
|
const layout = form.layout || appSpec.layout;
|
||||||
|
const messages = renderMessageStatics(form, layout);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'<?xml version="1.0" encoding="utf-8"?>',
|
'<?xml version="1.0" encoding="utf-8"?>',
|
||||||
|
|
@ -205,6 +214,7 @@ function renderXfdl(form, appSpec) {
|
||||||
" ]]></Script>",
|
" ]]></Script>",
|
||||||
components,
|
components,
|
||||||
grids,
|
grids,
|
||||||
|
messages,
|
||||||
" <Layouts>",
|
" <Layouts>",
|
||||||
` <Layout width="${layout.width}" height="${layout.height}" screenid="Desktop_screen"/>`,
|
` <Layout width="${layout.width}" height="${layout.height}" screenid="Desktop_screen"/>`,
|
||||||
" </Layouts>",
|
" </Layouts>",
|
||||||
|
|
@ -261,7 +271,7 @@ function renderWorkFrame(appSpec, defaultFormId) {
|
||||||
'<?xml version="1.0" encoding="utf-8"?>',
|
'<?xml version="1.0" encoding="utf-8"?>',
|
||||||
'<FDL version="2.1">',
|
'<FDL version="2.1">',
|
||||||
` <Form id="Form_Work" width="${workWidth}" height="${workHeight}" titletext="Form_Work">`,
|
` <Form id="Form_Work" width="${workWidth}" height="${workHeight}" titletext="Form_Work">`,
|
||||||
` <Div id="divWork" taborder="0" left="0" top="0" width="${workWidth}" height="${workHeight}" url="Forms::${defaultFormId}.xfdl"/>`,
|
` <Div id="divWork" taborder="0" left="0" top="0" width="${workWidth}" height="${workHeight}" url="Base::${defaultFormId}.xfdl"/>`,
|
||||||
" <Layouts>",
|
" <Layouts>",
|
||||||
` <Layout width="${workWidth}" height="${workHeight}" screenid="Desktop_screen"/>`,
|
` <Layout width="${workWidth}" height="${workHeight}" screenid="Desktop_screen"/>`,
|
||||||
" </Layouts>",
|
" </Layouts>",
|
||||||
|
|
@ -274,9 +284,9 @@ function renderWorkFrame(appSpec, defaultFormId) {
|
||||||
function generateProjectFiles(appSpec, forms, baseOutputDir = outputDir) {
|
function generateProjectFiles(appSpec, forms, baseOutputDir = outputDir) {
|
||||||
cleanGeneratedOutput(baseOutputDir, appSpec.projectName);
|
cleanGeneratedOutput(baseOutputDir, appSpec.projectName);
|
||||||
ensureDir(baseOutputDir);
|
ensureDir(baseOutputDir);
|
||||||
ensureDir(path.join(baseOutputDir, FORMS_DIR));
|
ensureDir(path.join(baseOutputDir, BASE_DIR));
|
||||||
ensureDir(path.join(baseOutputDir, FRAMEBASE_DIR));
|
ensureDir(path.join(baseOutputDir, FRAMEBASE_DIR));
|
||||||
ensureDir(path.join(baseOutputDir, LIB_DIR));
|
ensureDir(path.join(baseOutputDir, "_extlib_"));
|
||||||
|
|
||||||
writeGeneratedFile(
|
writeGeneratedFile(
|
||||||
path.join(baseOutputDir, `${appSpec.projectName}.xprj`),
|
path.join(baseOutputDir, `${appSpec.projectName}.xprj`),
|
||||||
|
|
@ -302,9 +312,6 @@ function generateProjectFiles(appSpec, forms, baseOutputDir = outputDir) {
|
||||||
);
|
);
|
||||||
writeGeneratedFile(path.join(baseOutputDir, "typedefinition.xml"), readTemplate("typedefinition.xml.tpl"));
|
writeGeneratedFile(path.join(baseOutputDir, "typedefinition.xml"), readTemplate("typedefinition.xml.tpl"));
|
||||||
writeGeneratedFile(path.join(baseOutputDir, "appvariables.xml"), readTemplate("appvariables.xml.tpl"), { bom: false });
|
writeGeneratedFile(path.join(baseOutputDir, "appvariables.xml"), readTemplate("appvariables.xml.tpl"), { bom: false });
|
||||||
writeGeneratedFile(path.join(baseOutputDir, LIB_DIR, "common.xjs"), readTemplate(path.join("common", "common.xjs.tpl")), {
|
|
||||||
bom: false
|
|
||||||
});
|
|
||||||
writeGeneratedFile(path.join(baseOutputDir, FRAMEBASE_DIR, "Form_Top.xfdl"), renderTopFrame(appSpec));
|
writeGeneratedFile(path.join(baseOutputDir, FRAMEBASE_DIR, "Form_Top.xfdl"), renderTopFrame(appSpec));
|
||||||
writeGeneratedFile(path.join(baseOutputDir, FRAMEBASE_DIR, "Form_Left.xfdl"), renderLeftFrame(forms));
|
writeGeneratedFile(path.join(baseOutputDir, FRAMEBASE_DIR, "Form_Left.xfdl"), renderLeftFrame(forms));
|
||||||
const defaultForm = forms.find((form) => form.route === "login") || forms[0];
|
const defaultForm = forms.find((form) => form.route === "login") || forms[0];
|
||||||
|
|
@ -314,7 +321,7 @@ function generateProjectFiles(appSpec, forms, baseOutputDir = outputDir) {
|
||||||
);
|
);
|
||||||
|
|
||||||
forms.forEach((form) => {
|
forms.forEach((form) => {
|
||||||
writeGeneratedFile(path.join(baseOutputDir, FORMS_DIR, `${form.formId}.xfdl`), renderXfdl(form, appSpec));
|
writeGeneratedFile(path.join(baseOutputDir, BASE_DIR, `${form.formId}.xfdl`), renderXfdl(form, appSpec));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ test("project generator writes required Nexacro files", () => {
|
||||||
assert.ok(fs.existsSync(path.join(tempDir, "environment.xml")));
|
assert.ok(fs.existsSync(path.join(tempDir, "environment.xml")));
|
||||||
assert.ok(fs.existsSync(path.join(tempDir, "typedefinition.xml")));
|
assert.ok(fs.existsSync(path.join(tempDir, "typedefinition.xml")));
|
||||||
assert.ok(fs.existsSync(path.join(tempDir, "appvariables.xml")));
|
assert.ok(fs.existsSync(path.join(tempDir, "appvariables.xml")));
|
||||||
assert.ok(fs.existsSync(path.join(tempDir, "forms", "frmLogin.xfdl")));
|
assert.ok(fs.existsSync(path.join(tempDir, "Base", "frmLogin.xfdl")));
|
||||||
assert.ok(fs.existsSync(path.join(tempDir, "FrameBase", "Form_Work.xfdl")));
|
assert.ok(fs.existsSync(path.join(tempDir, "FrameBase", "Form_Work.xfdl")));
|
||||||
assert.ok(fs.readFileSync(path.join(tempDir, "HanwhaNexacroDemo.xprj"), "utf8").includes("Application_Desktop.xadl"));
|
assert.ok(fs.readFileSync(path.join(tempDir, "HanwhaNexacroDemo.xprj"), "utf8").includes("Application_Desktop.xadl"));
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue