diff --git a/src/main/java/com/interplug/qcast/biz/estimate/EstimateService.java b/src/main/java/com/interplug/qcast/biz/estimate/EstimateService.java index cebc07d9..210d1684 100644 --- a/src/main/java/com/interplug/qcast/biz/estimate/EstimateService.java +++ b/src/main/java/com/interplug/qcast/biz/estimate/EstimateService.java @@ -26,10 +26,18 @@ import org.apache.poi.ss.usermodel.Cell; import org.apache.poi.ss.usermodel.CellStyle; import org.apache.poi.ss.usermodel.CellType; import org.apache.poi.ss.usermodel.DataFormat; +import org.apache.poi.ss.usermodel.Drawing; +import org.apache.poi.ss.usermodel.PictureData; import org.apache.poi.ss.usermodel.Row; import org.apache.poi.ss.usermodel.Sheet; import org.apache.poi.ss.usermodel.Workbook; import org.apache.poi.ss.usermodel.WorkbookFactory; +import org.apache.poi.xssf.usermodel.XSSFClientAnchor; +import org.apache.poi.xssf.usermodel.XSSFDrawing; +import org.apache.poi.xssf.usermodel.XSSFPicture; +import org.apache.poi.xssf.usermodel.XSSFShape; +import org.apache.poi.xssf.usermodel.XSSFSheet; +import org.apache.poi.util.Units; import org.jsoup.nodes.Document; import org.jsoup.nodes.Element; import org.springframework.beans.factory.annotation.Autowired; @@ -1868,6 +1876,9 @@ public class EstimateService { InputStream in = new ByteArrayInputStream(excelBytes); workbook = WorkbookFactory.create(in); // JXLS POI 엑셀로 재변환 + // 이미지 가로세로 비율 보정 + adjustImageAspectRatio(workbook); + // SchDrawingFlg (1 : 견적서,2 : 발전시뮬레이션, 3 : 도면, 4 : 가대) // ex) 1|2|3|4 if (!StringUtils.isEmpty(estimateRequest.getSchDrawingFlg())) { @@ -3002,12 +3013,145 @@ public class EstimateService { } } + /** + * 엑셀 워크북의 모든 시트에서 이미지의 가로세로 비율을 보정 + * JXLS jx:image 가 셀 영역에 맞춰 이미지를 늘리기 때문에 원본 비율로 재조정 + */ + private void adjustImageAspectRatio(Workbook workbook) { + for (int i = 0; i < workbook.getNumberOfSheets(); i++) { + Sheet sheet = workbook.getSheetAt(i); + if (!(sheet instanceof XSSFSheet)) continue; + + XSSFDrawing drawing = ((XSSFSheet) sheet).getDrawingPatriarch(); + if (drawing == null) continue; + + for (XSSFShape shape : drawing.getShapes()) { + if (!(shape instanceof XSSFPicture)) continue; + + XSSFPicture picture = (XSSFPicture) shape; + XSSFClientAnchor anchor = (XSSFClientAnchor) picture.getAnchor(); + PictureData pictureData = picture.getPictureData(); + if (pictureData == null) continue; + + // 원본 이미지 크기 (EMU 단위) + java.awt.Dimension imgDim = picture.getImageDimension(); + if (imgDim == null || imgDim.width == 0 || imgDim.height == 0) continue; + + double imgRatio = (double) imgDim.width / imgDim.height; + + // 현재 앵커 영역의 실제 크기 계산 (EMU 단위) + long cellWidthEmu = 0; + for (int col = anchor.getCol1(); col < anchor.getCol2(); col++) { + int colWidth = sheet.getColumnWidth(col); // 1/256 of character width + cellWidthEmu += (long) (colWidth / 256.0 * Units.DEFAULT_CHARACTER_WIDTH * Units.EMU_PER_PIXEL); + } + // dx 오프셋 보정 + cellWidthEmu = cellWidthEmu - anchor.getDx1() + anchor.getDx2(); + + long cellHeightEmu = 0; + for (int row = anchor.getRow1(); row < anchor.getRow2(); row++) { + Row r = sheet.getRow(row); + float heightPt = (r != null) ? r.getHeightInPoints() : sheet.getDefaultRowHeightInPoints(); + cellHeightEmu += Units.toEMU(heightPt); + } + // dy 오프셋 보정 + cellHeightEmu = cellHeightEmu - anchor.getDy1() + anchor.getDy2(); + + if (cellWidthEmu <= 0 || cellHeightEmu <= 0) continue; + + double cellRatio = (double) cellWidthEmu / cellHeightEmu; + + if (imgRatio > cellRatio) { + // 이미지가 더 넓음 → 너비에 맞추고 높이를 줄임 + long newHeightEmu = (long) (cellWidthEmu / imgRatio); + long verticalPadding = (cellHeightEmu - newHeightEmu) / 2; + + // dy1 오프셋으로 세로 중앙 정렬 + anchor.setDy1((int) (anchor.getDy1() + verticalPadding)); + // row2/dy2 재계산 + long remainHeight = newHeightEmu; + int newRow2 = anchor.getRow1(); + // dy1 오프셋이 있는 첫 행 처리 + Row firstRow = sheet.getRow(newRow2); + float firstRowPt = (firstRow != null) ? firstRow.getHeightInPoints() : sheet.getDefaultRowHeightInPoints(); + long firstRowAvail = Units.toEMU(firstRowPt) - anchor.getDy1(); + if (remainHeight <= firstRowAvail) { + anchor.setRow2(newRow2); + anchor.setDy2((int) (anchor.getDy1() + remainHeight)); + } else { + remainHeight -= firstRowAvail; + newRow2++; + while (remainHeight > 0) { + Row r = sheet.getRow(newRow2); + float hPt = (r != null) ? r.getHeightInPoints() : sheet.getDefaultRowHeightInPoints(); + long rowEmu = Units.toEMU(hPt); + if (remainHeight <= rowEmu) { + anchor.setRow2(newRow2); + anchor.setDy2((int) remainHeight); + remainHeight = 0; + } else { + remainHeight -= rowEmu; + newRow2++; + } + } + } + } else { + // 이미지가 더 높음 → 높이에 맞추고 너비를 줄임 + long newWidthEmu = (long) (cellHeightEmu * imgRatio); + long horizontalPadding = (cellWidthEmu - newWidthEmu) / 2; + + // dx1 오프셋으로 가로 중앙 정렬 + anchor.setDx1((int) (anchor.getDx1() + horizontalPadding)); + // col2/dx2 재계산 + long remainWidth = newWidthEmu; + int newCol2 = anchor.getCol1(); + // dx1 오프셋이 있는 첫 열 처리 + int firstColW = sheet.getColumnWidth(newCol2); + long firstColEmu = (long) (firstColW / 256.0 * Units.DEFAULT_CHARACTER_WIDTH * Units.EMU_PER_PIXEL); + long firstColAvail = firstColEmu - anchor.getDx1(); + if (remainWidth <= firstColAvail) { + anchor.setCol2(newCol2); + anchor.setDx2((int) (anchor.getDx1() + remainWidth)); + } else { + remainWidth -= firstColAvail; + newCol2++; + while (remainWidth > 0) { + int colW = sheet.getColumnWidth(newCol2); + long colEmu = (long) (colW / 256.0 * Units.DEFAULT_CHARACTER_WIDTH * Units.EMU_PER_PIXEL); + if (remainWidth <= colEmu) { + anchor.setCol2(newCol2); + anchor.setDx2((int) remainWidth); + remainWidth = 0; + } else { + remainWidth -= colEmu; + newCol2++; + } + } + } + } + } + } + } + + @Autowired + private org.springframework.core.env.Environment environment; + private byte[] loadDrawingImage(URL url) { try { URLConnection connection = url.openConnection(); + // local 프로파일일 때만 SSL 우회 + if (environment.matchesProfiles("local") && connection instanceof javax.net.ssl.HttpsURLConnection httpsConn) { + javax.net.ssl.SSLContext sc = javax.net.ssl.SSLContext.getInstance("TLSv1.2"); + sc.init(null, new javax.net.ssl.TrustManager[]{new javax.net.ssl.X509TrustManager() { + public java.security.cert.X509Certificate[] getAcceptedIssuers() { return null; } + public void checkClientTrusted(java.security.cert.X509Certificate[] c, String t) {} + public void checkServerTrusted(java.security.cert.X509Certificate[] c, String t) {} + }}, null); + httpsConn.setSSLSocketFactory(sc.getSocketFactory()); + httpsConn.setHostnameVerifier((h, s) -> true); + } if (connection instanceof HttpURLConnection) { - HttpURLConnection httpConnection = (HttpURLConnection) connection; - if (httpConnection.getResponseCode() != HttpURLConnection.HTTP_OK) { + if (((HttpURLConnection) connection).getResponseCode() != HttpURLConnection.HTTP_OK) { return null; } } diff --git a/src/main/java/com/interplug/qcast/biz/object/ObjectController.java b/src/main/java/com/interplug/qcast/biz/object/ObjectController.java index 4b2e1d68..2a526dc6 100644 --- a/src/main/java/com/interplug/qcast/biz/object/ObjectController.java +++ b/src/main/java/com/interplug/qcast/biz/object/ObjectController.java @@ -77,6 +77,14 @@ public class ObjectController { return objectService.selectParentAgentInfo(saleStoreId, storeLvl); } + @Operation(description = "하위 판매점 목록을 조회한다.") + @GetMapping("/saleStore/{saleStoreId}/childList") + @ResponseStatus(HttpStatus.OK) + public List selectChildSaleStoreList( + @PathVariable String saleStoreId, String storeLvl, String userId) throws Exception { + return objectService.selectChildSaleStoreList(saleStoreId, storeLvl, userId); + } + @Operation(description = "물건정보 목록을 조회한다.") @GetMapping("/list") @ResponseStatus(HttpStatus.OK) diff --git a/src/main/java/com/interplug/qcast/biz/object/ObjectMapper.java b/src/main/java/com/interplug/qcast/biz/object/ObjectMapper.java index 02a6120b..bc031f47 100644 --- a/src/main/java/com/interplug/qcast/biz/object/ObjectMapper.java +++ b/src/main/java/com/interplug/qcast/biz/object/ObjectMapper.java @@ -31,6 +31,9 @@ public interface ObjectMapper { // 상위 판매점 정보 조회 public SaleStoreResponse selectParentAgentInfo(String saleStoreId, String storeLvl); + // 하위 판매점 목록 조회 + public List selectChildSaleStoreList(String saleStoreId, String storeLvl, String userId); + // 물건정보 메인 목록 조회 public List selectObjectMainList(ObjectRequest objectRequest); diff --git a/src/main/java/com/interplug/qcast/biz/object/ObjectService.java b/src/main/java/com/interplug/qcast/biz/object/ObjectService.java index 9cd24951..a83b2c8d 100644 --- a/src/main/java/com/interplug/qcast/biz/object/ObjectService.java +++ b/src/main/java/com/interplug/qcast/biz/object/ObjectService.java @@ -182,6 +182,30 @@ public class ObjectService { return result; } + /** + * 하위 판매점 목록 조회 + * + * @param saleStoreId 판매점 아이디 (부모) + * @param storeLvl 판매점 레벨 + * @return List 하위 판매점 목록 + * @throws Exception + */ + public List selectChildSaleStoreList(String saleStoreId, String storeLvl, String userId) throws Exception { + if (StringUtils.isEmpty(saleStoreId)) { + throw new QcastException(ErrorCode.INVALID_INPUT_VALUE, + message.getMessage("common.message.required.data", "Sale Store ID")); + } + if (StringUtils.isEmpty(storeLvl)) { + throw new QcastException(ErrorCode.INVALID_INPUT_VALUE, + message.getMessage("common.message.required.data", "Store Level")); + } + if (StringUtils.isEmpty(userId)) { + throw new QcastException(ErrorCode.INVALID_INPUT_VALUE, + message.getMessage("common.message.required.data", "User ID")); + } + return objectMapper.selectChildSaleStoreList(saleStoreId, storeLvl, userId); + } + /** * 물건정보 메인 목록 조회 * diff --git a/src/main/resources/mappers/object/objectMapper.xml b/src/main/resources/mappers/object/objectMapper.xml index e32e4fa7..05c67d44 100644 --- a/src/main/resources/mappers/object/objectMapper.xml +++ b/src/main/resources/mappers/object/objectMapper.xml @@ -162,6 +162,54 @@ AND A.DEL_FLG = '0' + +