결재시 첨부파일 업로드 반영영

This commit is contained in:
2026-02-24 15:10:20 +09:00
parent 1f7d35ef01
commit 5c4504182f
2 changed files with 490 additions and 4 deletions

View File

@@ -1817,11 +1817,22 @@ public class ApprovalService {
System.out.println("outProcessCode: " + outProcessCode + ", formId: " + formId);
System.out.println("approKey: " + approKey);
// API 호출 (loginId 파라미터 추가)
// API 클라이언트 생성
com.pms.api.AmaranthApprovalApiClient apiClient = new com.pms.api.AmaranthApprovalApiClient();
// 첨부파일 처리: ATTACH_FILE_INFO에서 품의서 파일 조회 → 원챔버 업로드
String fileListEncoded = null;
try {
fileListEncoded = uploadProposalFilesToOneChamber(apiClient, empSeq, targetObjId);
} catch(Exception fileEx) {
System.err.println("[첨부파일] 원챔버 업로드 중 오류 (결재는 계속 진행): " + fileEx.getMessage());
fileEx.printStackTrace();
}
// SSO URL 생성 API 호출
String apiResponse = apiClient.getSsoUrl(
AMARANTH_BASE_URL, empSeq, outProcessCode, formId,
approKey, approvalTitle, "W", compSeq, deptSeq, loginId
approKey, approvalTitle, "W", compSeq, deptSeq, loginId, fileListEncoded
);
System.out.println("SSO API 응답: " + apiResponse);
@@ -1867,6 +1878,132 @@ public class ApprovalService {
}
}
/**
* 품의서 첨부파일을 원챔버에 업로드하고 fileList (URL 인코딩) 반환
* @return URL 인코딩된 fileList JSON 문자열 (파일 없으면 null)
*/
private String uploadProposalFilesToOneChamber(
com.pms.api.AmaranthApprovalApiClient apiClient,
String empSeq, String targetObjId) throws Exception {
if(targetObjId == null || targetObjId.isEmpty()) return null;
SqlSession sqlSession = SqlMapConfig.getInstance().getSqlSession(true);
try {
// 1. 인증 토큰 발급 (파일 업로드는 사용자 인증 필요)
Map<String, String> authResult = apiClient.getAuthToken(AMARANTH_BASE_URL, empSeq);
if(!"true".equals(authResult.get("success"))){
throw new Exception("파일 업로드용 인증 토큰 발급 실패: " + authResult.get("resultMsg"));
}
String authToken = authResult.get("authToken");
String userHashKey = authResult.get("hashKey");
StringBuilder listItems = new StringBuilder();
int uploadCount = 0;
// 2. 품의서 본문을 JSP와 동일한 레이아웃의 HTML 파일로 생성하여 업로드
try {
Map<String, Object> proposalParam = new HashMap<String, Object>();
proposalParam.put("PROPOSAL_OBJID", targetObjId);
Map<String, Object> proposalInfo = sqlSession.selectOne("salesMng.getProposalInfo", proposalParam);
if(proposalInfo != null){
proposalInfo = CommonUtils.toUpperCaseMapKey(proposalInfo);
String proposalNo = CommonUtils.checkNull(proposalInfo.get("PROPOSAL_NO"));
// 품목 리스트 조회
Map<String, Object> partParam = new HashMap<String, Object>();
partParam.put("PROPOSAL_OBJID", targetObjId);
List<Map> partList = sqlSession.selectList("salesMng.getProposalPartList", partParam);
String fullHtml = buildProposalFormFileHtml(proposalInfo, partList);
String tempFileName = "구매품의서_" + proposalNo + ".html";
java.io.File tempFile = java.io.File.createTempFile("proposal_", ".html");
java.io.OutputStreamWriter writer = new java.io.OutputStreamWriter(
new java.io.FileOutputStream(tempFile), "UTF-8");
writer.write(fullHtml);
writer.close();
System.out.println("[첨부파일] 품의서 HTML 생성: " + tempFileName + " (" + tempFile.length() + " bytes)");
String uploadResponse = apiClient.uploadFileToOneChamber(
AMARANTH_BASE_URL, authToken, userHashKey, empSeq, tempFile, tempFileName
);
String listItem = apiClient.extractListItemFromUploadResponse(uploadResponse);
if(listItem != null){
listItems.append(listItem);
uploadCount++;
System.out.println("[첨부파일] 품의서 HTML 업로드 성공");
}
tempFile.delete();
}
} catch(Exception htmlEx){
System.err.println("[첨부파일] 품의서 HTML 생성/업로드 오류: " + htmlEx.getMessage());
}
// 3. ATTACH_FILE_INFO에서 기존 첨부파일도 업로드
Map<String, Object> fileParam = new HashMap<String, Object>();
fileParam.put("targetObjId", targetObjId);
List<Map<String, Object>> fileList = sqlSession.selectList("common.getFileList", fileParam);
if(fileList != null && !fileList.isEmpty()){
System.out.println("[첨부파일] 품의서(" + targetObjId + ") 기존 첨부파일 " + fileList.size() + "건 발견");
for(Map<String, Object> fileInfo : fileList){
String savedFileName = CommonUtils.checkNull(fileInfo.get("saved_file_name"));
String realFileName = CommonUtils.checkNull(fileInfo.get("real_file_name"));
String filePath = CommonUtils.checkNull(fileInfo.get("file_path"));
String fileExt = CommonUtils.checkNull(fileInfo.get("file_ext"));
if(savedFileName.isEmpty()) continue;
java.io.File physicalFile = new java.io.File(filePath, savedFileName);
if(!physicalFile.exists()){
System.err.println("[첨부파일] 물리 파일 없음: " + physicalFile.getAbsolutePath());
continue;
}
String originalName = realFileName;
if(!originalName.contains(".") && !fileExt.isEmpty()){
originalName = realFileName + "." + fileExt;
}
System.out.println("[첨부파일] 업로드 시작: " + originalName + " (" + physicalFile.length() + " bytes)");
String uploadResponse = apiClient.uploadFileToOneChamber(
AMARANTH_BASE_URL, authToken, userHashKey, empSeq, physicalFile, originalName
);
String listItem = apiClient.extractListItemFromUploadResponse(uploadResponse);
if(listItem != null){
if(uploadCount > 0) listItems.append(",");
listItems.append(listItem);
uploadCount++;
System.out.println("[첨부파일] 업로드 성공: " + originalName);
} else {
System.err.println("[첨부파일] 업로드 응답에서 list 항목 추출 실패: " + originalName);
}
}
}
if(uploadCount == 0) return null;
// 4. JSON 배열 구성 → URL 인코딩
String fileListJson = "[" + listItems.toString() + "]";
String fileListEncoded = java.net.URLEncoder.encode(fileListJson, "UTF-8");
System.out.println("[첨부파일] 총 " + uploadCount + "건 업로드 완료");
return fileListEncoded;
} finally {
if(sqlSession != null) sqlSession.close();
}
}
/**
* Amaranth10 전자결재 콜백 처리
* 결재 이벤트(상신/진행/종결/반려/삭제 등) 발생 시 Amaranth10에서 호출
@@ -2172,6 +2309,185 @@ public class ApprovalService {
return html.toString();
}
/**
* 품의서 JSP(proposalFormPopUp.jsp)와 동일한 레이아웃의 HTML 파일 생성
* 결재 첨부파일용 (입력 필드 없이 값만 표시)
*/
private String buildProposalFormFileHtml(Map proposalInfo, List<Map> partList){
String proposalNo = CommonUtils.checkNull(proposalInfo.get("PROPOSAL_NO"));
String regdate = CommonUtils.checkNull(proposalInfo.get("REGDATE_TITLE"));
String writerName = CommonUtils.checkNull(proposalInfo.get("WRITER_NAME"));
String recipientRef = CommonUtils.checkNull(proposalInfo.get("RECIPIENT_REF"));
String executor = CommonUtils.checkNull(proposalInfo.get("EXECUTOR"));
String executionDate = CommonUtils.checkNull(proposalInfo.get("EXECUTION_DATE_TITLE"));
String title = CommonUtils.checkNull(proposalInfo.get("TITLE"));
String remark = CommonUtils.checkNull(proposalInfo.get("REMARK"));
// 부서명/기안자명 분리 (WRITER_NAME = "경영지원팀 관리자" 형식)
String deptName = "-";
String writerOnly = "-";
if(!writerName.isEmpty()){
if(writerName.contains(" ")){
deptName = writerName.substring(0, writerName.indexOf(" "));
writerOnly = writerName.substring(writerName.lastIndexOf(" ") + 1);
} else {
deptName = writerName;
writerOnly = writerName;
}
}
// 합계 계산
long totalAmount = 0;
if(partList != null){
for(Map part : partList){
try {
Object tp = part.get("TOTAL_PRICE");
if(tp != null) totalAmount += Long.parseLong(tp.toString());
} catch(Exception ignore){}
}
}
StringBuilder h = new StringBuilder();
h.append("<!DOCTYPE html><html><head><meta charset='UTF-8'/>");
h.append("<title>구매품의서 - ").append(escapeHtml(proposalNo)).append("</title>");
h.append("<style>");
h.append("body{font-family:'Malgun Gothic','맑은 고딕',sans-serif;font-size:12px;margin:0;padding:10px;background:#fff;}");
h.append(".proposal-container{width:100%;max-width:800px;margin:0 auto;}");
h.append(".proposal-title{text-align:center;font-size:28px;font-weight:bold;letter-spacing:20px;padding:15px 0;border-bottom:2px solid #000;}");
h.append(".header-section{border-bottom:1px solid #000;border-left:2px solid #000;border-right:2px solid #000;}");
h.append(".sub-info-section{border-bottom:1px solid #000;border-left:2px solid #000;border-right:2px solid #000;}");
h.append(".info-table{width:100%;border-collapse:collapse;}");
h.append(".info-table td{border:1px solid #999;padding:2px 4px;height:26px;vertical-align:middle;}");
h.append(".info-table .label{background-color:#f5f5f5;font-weight:bold;width:90px;min-width:90px;text-align:center;}");
h.append(".revision-info{text-align:right;font-size:11px;color:#ff0000;padding:5px 10px;border-bottom:1px solid #000;}");
h.append(".middle-section{border-bottom:1px solid #000;border-left:2px solid #000;border-right:2px solid #000;}");
h.append(".middle-table{width:100%;border-collapse:collapse;}");
h.append(".middle-table td,.middle-table th{border:1px solid #999;padding:5px 8px;text-align:center;vertical-align:middle;}");
h.append(".middle-table .header-cell{background-color:#ebf1de;font-weight:bold;}");
h.append(".middle-table .total-header{background-color:#dce6f1;}");
h.append(".middle-table .total-value{background-color:#ffffcc;font-weight:bold;text-align:right;font-size:14px;}");
h.append(".item-section{border-bottom:1px solid #000;border-left:2px solid #000;border-right:2px solid #000;}");
h.append(".item-table{width:100%;border-collapse:collapse;}");
h.append(".item-table th,.item-table td{border:1px solid #999;padding:1px 2px;text-align:center;vertical-align:middle;}");
h.append(".item-table th{background-color:#f5f5f5;font-weight:bold;height:30px;}");
h.append(".item-table td{height:35px;}");
h.append(".text-left{text-align:left;padding-left:4px;}");
h.append(".text-right{text-align:right;padding-right:4px;}");
h.append(".reference-table{width:100%;border-collapse:collapse;}");
h.append(".reference-table td{border:1px solid #999;padding:8px;}");
h.append(".reference-table .label{background-color:#f5f5f5;font-weight:bold;width:80px;text-align:center;}");
h.append("</style></head><body>");
h.append("<div class='proposal-container'>");
// 제목
h.append("<div class='proposal-title'>구 매 품 의 서</div>");
// 상단 기본정보
h.append("<div class='header-section'>");
h.append("<table class='info-table'>");
h.append("<tr><td class='label'>품 의 번 호</td><td colspan='3'>").append(escapeHtml(proposalNo)).append("</td></tr>");
h.append("<tr><td class='label'>작 성 일 자</td><td colspan='3'>").append(escapeHtml(regdate)).append("</td></tr>");
h.append("<tr><td class='label'>기 안 부 서</td><td colspan='3'>").append(escapeHtml(deptName)).append("</td></tr>");
h.append("<tr><td class='label'>기 안 자</td><td colspan='3'>").append(escapeHtml(writerOnly)).append("</td></tr>");
h.append("</table></div>");
// 하단 기본정보 (수신및참조, 시행자, 시행일자, 제목)
h.append("<div class='sub-info-section'>");
h.append("<table class='info-table'>");
h.append("<tr><td class='label'>수신및참조</td><td colspan='3'>").append(escapeHtml(recipientRef)).append("</td></tr>");
h.append("<tr><td class='label'>시 행 자</td><td colspan='3'>").append(escapeHtml(executor)).append("</td></tr>");
h.append("<tr><td class='label'>시행일자</td><td colspan='3'>").append(escapeHtml(executionDate)).append("</td></tr>");
h.append("<tr><td class='label'>제&nbsp;&nbsp;&nbsp;&nbsp;목</td><td colspan='3'>").append(escapeHtml(title)).append("</td></tr>");
h.append("</table></div>");
// 개정 정보
h.append("<div class='revision-info'>[구매품의서 개정 : 22.05.17]</div>");
// 중간 정보 섹션
h.append("<div class='middle-section'><table class='middle-table'>");
h.append("<tr>");
h.append("<th rowspan='2' class='header-cell' style='width:50px;'>구<br/>분</th>");
h.append("<th class='header-cell'>부 서</th><th class='header-cell'>소속팀</th>");
h.append("<th class='header-cell'>날 짜</th><th>").append(escapeHtml(regdate)).append("</th>");
h.append("<th class='total-header'>총 합 계</th>");
h.append("</tr>");
h.append("<tr style='height:50px;'>");
h.append("<td>").append(escapeHtml(deptName)).append("</td>");
h.append("<td>").append(escapeHtml(deptName)).append("</td>");
h.append("<td class='header-cell'>기 안 자</td>");
h.append("<td>").append(escapeHtml(writerOnly)).append("</td>");
h.append("<td class='total-value'>").append(formatNumber(totalAmount)).append("</td>");
h.append("</tr></table></div>");
// 품목 테이블
h.append("<div class='item-section'><table class='item-table'><thead><tr>");
h.append("<th style='width:40px;'>No.</th>");
h.append("<th style='width:150px;'>목 적</th>");
h.append("<th style='width:250px;'>품명 / 규격</th>");
h.append("<th style='width:100px;'>납 기 일</th>");
h.append("<th style='width:100px;'>업 체 명</th>");
h.append("<th style='width:60px;'>수량</th>");
h.append("<th style='width:50px;'>단위</th>");
h.append("<th style='width:80px;'>단가</th>");
h.append("<th style='width:100px;'>합 계</th>");
h.append("</tr></thead><tbody>");
if(partList != null && !partList.isEmpty()){
int idx = 1;
for(Map part : partList){
String partName = CommonUtils.checkNull(part.get("PART_NAME"));
String spec = CommonUtils.checkNull(part.get("SPEC"));
String partRemark = CommonUtils.checkNull(part.get("REMARK"));
String deliveryDate = CommonUtils.checkNull(part.get("DELIVERY_REQUEST_DATE_TITLE"));
String vendorName = CommonUtils.checkNull(part.get("VENDOR_NAME"));
String qty = CommonUtils.checkNull(part.get("QTY"));
String unit = CommonUtils.checkNull(part.get("UNIT_NAME"));
String unitPrice = CommonUtils.checkNull(part.get("UNIT_PRICE"));
String totalPrice = CommonUtils.checkNull(part.get("TOTAL_PRICE"));
h.append("<tr>");
h.append("<td>").append(idx++).append("</td>");
h.append("<td class='text-left'>").append(escapeHtml(partRemark)).append("</td>");
h.append("<td class='text-left'>").append(escapeHtml(partName));
if(!spec.isEmpty()) h.append("<br/>(").append(escapeHtml(spec)).append(")");
h.append("</td>");
h.append("<td>").append(escapeHtml(deliveryDate)).append("</td>");
h.append("<td class='text-left'>").append(escapeHtml(vendorName)).append("</td>");
h.append("<td class='text-right'>").append(formatNumber(qty)).append("</td>");
h.append("<td>").append(escapeHtml(unit)).append("</td>");
h.append("<td class='text-right'>").append(formatNumber(unitPrice)).append("</td>");
h.append("<td class='text-right'>").append(formatNumber(totalPrice)).append("</td>");
h.append("</tr>");
}
} else {
h.append("<tr><td colspan='9' style='height:100px;'>등록된 품목이 없습니다.</td></tr>");
}
h.append("</tbody></table></div>");
// 참조문서
h.append("<table class='reference-table'>");
h.append("<tr style='height:50px;'><td class='label' style='width:150px;'>참 조 문 서</td>");
h.append("<td>선택된 문서가 없습니다.</td></tr></table>");
h.append("</div></body></html>");
return h.toString();
}
/**
* 숫자 천단위 콤마 포맷
*/
private String formatNumber(Object value){
if(value == null) return "0";
try {
long num = Long.parseLong(value.toString().replaceAll("[^0-9\\-]", ""));
return String.format("%,d", num);
} catch(Exception e){
return value.toString();
}
}
/**
* HTML 이스케이프 (XSS 방지)
*/