diff --git a/src/com/pms/api/AmaranthApprovalApiClient.java b/src/com/pms/api/AmaranthApprovalApiClient.java index 9c8d40c..3f5b681 100644 --- a/src/com/pms/api/AmaranthApprovalApiClient.java +++ b/src/com/pms/api/AmaranthApprovalApiClient.java @@ -834,6 +834,18 @@ public class AmaranthApprovalApiClient { String formId, String approKey, String subjectStr, String mod, String compSeq, String deptSeq, String loginId) throws Exception { + return getSsoUrl(baseUrl, empSeq, outProcessCode, formId, approKey, + subjectStr, mod, compSeq, deptSeq, loginId, null); + } + + /** + * Amaranth10 전자결재 SSO URL 생성 (첨부파일 포함) + * @param fileList URL 인코딩된 파일 리스트 JSON (null이면 첨부파일 없음) + */ + public String getSsoUrl(String baseUrl, String empSeq, String outProcessCode, + String formId, String approKey, String subjectStr, + String mod, String compSeq, String deptSeq, + String loginId, String fileList) throws Exception { System.out.println("=== Amaranth SSO URL 생성 시작 ==="); System.out.println("empSeq: " + empSeq + ", outProcessCode: " + outProcessCode + ", formId: " + formId); @@ -887,7 +899,7 @@ public class AmaranthApprovalApiClient { // 요청 본문 구성 String requestBody = buildSsoRequestBody(empSeqEnc, outProcessCode, formId, - approKey, subjectStr, mod, compSeq, deptSeq, loginId); + approKey, subjectStr, mod, compSeq, deptSeq, loginId, fileList); System.out.println("[2단계] SSO API 호출 - URL: " + fullUrl); System.out.println("[2단계] Request Body: " + requestBody); @@ -943,7 +955,7 @@ public class AmaranthApprovalApiClient { String formId, String approKey, String subjectStr, String mod, String compSeq, String deptSeq, - String loginId) { + String loginId, String fileList) { StringBuilder json = new StringBuilder(); json.append("{"); json.append("\"header\":{},"); @@ -986,6 +998,11 @@ public class AmaranthApprovalApiClient { json.append(",\"subjectStr\":\"").append(escapeJson(subjectStr)).append("\""); } + // 첨부파일 (URL 인코딩된 JSON 문자열) + if (fileList != null && !fileList.isEmpty()) { + json.append(",\"fileList\":\"").append(escapeJson(fileList)).append("\""); + } + // 본문 인코딩 (UTF-8) json.append(",\"contentsEnc\":\"U\""); @@ -995,6 +1012,159 @@ public class AmaranthApprovalApiClient { return json.toString(); } + /** + * 원챔버 첨부파일 업로드 (전자결재용) + * API: /apiproxy/authUser/api99u04A12 + * moduleGbn="EAP" (전자결재 영리) + * 단일 파일만 업로드 가능 → 다중 파일은 반복 호출 + * + * @return API 응답 JSON 전체 + */ + public String uploadFileToOneChamber(String baseUrl, String authToken, String userHashKey, + String empSeq, java.io.File file, String originalFileName) throws Exception { + System.setProperty("https.protocols", "TLSv1.2"); + setupSslTrustAll(); + + String urlPath = "/apiproxy/authUser/api99u04A12"; + String cleanBaseUrl = baseUrl.endsWith("/") ? baseUrl.substring(0, baseUrl.length() - 1) : baseUrl; + String fullUrl = cleanBaseUrl + urlPath; + + String boundary = "----Boundary" + System.currentTimeMillis(); + String CRLF = "\r\n"; + + URL url = new URL(fullUrl); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setConnectTimeout(60000); + connection.setReadTimeout(60000); + connection.setInstanceFollowRedirects(false); + + try { + connection.setRequestMethod("POST"); + connection.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + boundary); + connection.setRequestProperty("Accept", "application/json"); + connection.setRequestProperty("callerName", CALLER_NAME); + connection.setRequestProperty("Authorization", "Bearer " + authToken); + + String transactionId = generateTransactionId(); + connection.setRequestProperty("transaction-id", transactionId); + + String timestamp = String.valueOf(System.currentTimeMillis() / 1000); + connection.setRequestProperty("timestamp", timestamp); + connection.setRequestProperty("groupSeq", GROUP_SEQ); + + String wehagoSign = generateWehagoSign(authToken, transactionId, timestamp, urlPath, userHashKey); + connection.setRequestProperty("wehago-sign", wehagoSign); + + connection.setDoOutput(true); + connection.setDoInput(true); + + java.io.DataOutputStream dos = new java.io.DataOutputStream(connection.getOutputStream()); + + // callerName + dos.writeBytes("--" + boundary + CRLF); + dos.writeBytes("Content-Disposition: form-data; name=\"callerName\"" + CRLF + CRLF); + dos.writeBytes(CALLER_NAME + CRLF); + + // groupSeq + dos.writeBytes("--" + boundary + CRLF); + dos.writeBytes("Content-Disposition: form-data; name=\"groupSeq\"" + CRLF + CRLF); + dos.writeBytes(GROUP_SEQ + CRLF); + + // empSeq + dos.writeBytes("--" + boundary + CRLF); + dos.writeBytes("Content-Disposition: form-data; name=\"empSeq\"" + CRLF + CRLF); + dos.writeBytes(empSeq + CRLF); + + // moduleGbn = EAP (전자결재 영리) + dos.writeBytes("--" + boundary + CRLF); + dos.writeBytes("Content-Disposition: form-data; name=\"moduleGbn\"" + CRLF + CRLF); + dos.writeBytes("EAP" + CRLF); + + // file[] - 파일명을 UTF-8 바이트로 전송 + dos.writeBytes("--" + boundary + CRLF); + byte[] contentDisp = ("Content-Disposition: form-data; name=\"file[]\"; filename=\"" + + originalFileName + "\"" + CRLF).getBytes(StandardCharsets.UTF_8); + dos.write(contentDisp); + dos.writeBytes("Content-Type: application/octet-stream" + CRLF + CRLF); + + java.io.FileInputStream fis = new java.io.FileInputStream(file); + byte[] buffer = new byte[4096]; + int bytesRead; + while ((bytesRead = fis.read(buffer)) != -1) { + dos.write(buffer, 0, bytesRead); + } + fis.close(); + + dos.writeBytes(CRLF); + dos.writeBytes("--" + boundary + "--" + CRLF); + dos.flush(); + dos.close(); + + int responseCode = connection.getResponseCode(); + BufferedReader reader = null; + StringBuilder response = new StringBuilder(); + + try { + if (responseCode >= 200 && responseCode < 300) { + reader = new BufferedReader(new InputStreamReader( + connection.getInputStream(), StandardCharsets.UTF_8)); + } else { + java.io.InputStream errorStream = connection.getErrorStream(); + if (errorStream != null) { + reader = new BufferedReader(new InputStreamReader( + errorStream, StandardCharsets.UTF_8)); + } else { + throw new Exception("원챔버 파일 업로드 실패: HTTP " + responseCode); + } + } + String line; + while ((line = reader.readLine()) != null) { + response.append(line); + } + } finally { + if (reader != null) reader.close(); + } + + System.out.println("[원챔버 업로드] 파일: " + originalFileName + ", Response Code: " + responseCode); + System.out.println("[원챔버 업로드] Response: " + response.toString()); + + return response.toString(); + + } finally { + connection.disconnect(); + } + } + + /** + * 원챔버 업로드 응답에서 list 배열의 첫 번째 항목 JSON 추출 + */ + public String extractListItemFromUploadResponse(String jsonResponse) { + int listIdx = jsonResponse.indexOf("\"list\":["); + if (listIdx == -1) return null; + + int arrStart = jsonResponse.indexOf("[", listIdx); + int objStart = jsonResponse.indexOf("{", arrStart); + if (objStart == -1) return null; + + // 중첩 중괄호를 고려하여 닫는 중괄호 찾기 + int depth = 0; + int objEnd = -1; + for (int i = objStart; i < jsonResponse.length(); i++) { + char c = jsonResponse.charAt(i); + if (c == '{') depth++; + else if (c == '}') { + depth--; + if (depth == 0) { + objEnd = i; + break; + } + } + } + + if (objEnd == -1) return null; + return jsonResponse.substring(objStart, objEnd + 1); + } + /** * SSL 인증서 검증 우회 설정 (개발 환경용) */ diff --git a/src/com/pms/service/ApprovalService.java b/src/com/pms/service/ApprovalService.java index 5706032..5ec78f7 100644 --- a/src/com/pms/service/ApprovalService.java +++ b/src/com/pms/service/ApprovalService.java @@ -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 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 proposalParam = new HashMap(); + proposalParam.put("PROPOSAL_OBJID", targetObjId); + Map proposalInfo = sqlSession.selectOne("salesMng.getProposalInfo", proposalParam); + + if(proposalInfo != null){ + proposalInfo = CommonUtils.toUpperCaseMapKey(proposalInfo); + String proposalNo = CommonUtils.checkNull(proposalInfo.get("PROPOSAL_NO")); + + // 품목 리스트 조회 + Map partParam = new HashMap(); + partParam.put("PROPOSAL_OBJID", targetObjId); + List 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 fileParam = new HashMap(); + fileParam.put("targetObjId", targetObjId); + List> fileList = sqlSession.selectList("common.getFileList", fileParam); + + if(fileList != null && !fileList.isEmpty()){ + System.out.println("[첨부파일] 품의서(" + targetObjId + ") 기존 첨부파일 " + fileList.size() + "건 발견"); + + for(Map 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 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(""); + h.append("구매품의서 - ").append(escapeHtml(proposalNo)).append(""); + h.append(""); + + h.append("
"); + + // 제목 + h.append("
구 매 품 의 서
"); + + // 상단 기본정보 + h.append("
"); + h.append(""); + h.append(""); + h.append(""); + h.append(""); + h.append(""); + h.append("
품 의 번 호").append(escapeHtml(proposalNo)).append("
작 성 일 자").append(escapeHtml(regdate)).append("
기 안 부 서").append(escapeHtml(deptName)).append("
기 안 자").append(escapeHtml(writerOnly)).append("
"); + + // 하단 기본정보 (수신및참조, 시행자, 시행일자, 제목) + h.append("
"); + h.append(""); + h.append(""); + h.append(""); + h.append(""); + h.append(""); + h.append("
수신및참조").append(escapeHtml(recipientRef)).append("
시 행 자").append(escapeHtml(executor)).append("
시행일자").append(escapeHtml(executionDate)).append("
제    목").append(escapeHtml(title)).append("
"); + + // 개정 정보 + h.append("
[구매품의서 개정 : 22.05.17]
"); + + // 중간 정보 섹션 + h.append("
"); + h.append(""); + h.append(""); + h.append(""); + h.append(""); + h.append(""); + h.append(""); + h.append(""); + h.append(""); + h.append(""); + h.append(""); + h.append(""); + h.append(""); + h.append("

부 서소속팀날 짜").append(escapeHtml(regdate)).append("총 합 계
").append(escapeHtml(deptName)).append("").append(escapeHtml(deptName)).append("기 안 자").append(escapeHtml(writerOnly)).append("").append(formatNumber(totalAmount)).append("
"); + + // 품목 테이블 + h.append("
"); + h.append(""); + h.append(""); + h.append(""); + h.append(""); + h.append(""); + h.append(""); + h.append(""); + h.append(""); + h.append(""); + h.append(""); + + 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(""); + h.append(""); + h.append(""); + h.append(""); + h.append(""); + h.append(""); + h.append(""); + h.append(""); + h.append(""); + h.append(""); + h.append(""); + } + } else { + h.append(""); + } + + h.append("
No.목 적품명 / 규격납 기 일업 체 명수량단위단가합 계
").append(idx++).append("").append(escapeHtml(partRemark)).append("").append(escapeHtml(partName)); + if(!spec.isEmpty()) h.append("
(").append(escapeHtml(spec)).append(")"); + h.append("
").append(escapeHtml(deliveryDate)).append("").append(escapeHtml(vendorName)).append("").append(formatNumber(qty)).append("").append(escapeHtml(unit)).append("").append(formatNumber(unitPrice)).append("").append(formatNumber(totalPrice)).append("
등록된 품목이 없습니다.
"); + + // 참조문서 + h.append(""); + h.append(""); + h.append("
참 조 문 서선택된 문서가 없습니다.
"); + + h.append("
"); + 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 방지) */