Merge pull request 'dev' (#471) from dev into prd-deploy

Reviewed-on: #471
This commit is contained in:
ysCha 2026-04-16 17:20:24 +09:00
commit 3fc0c6246a
5 changed files with 229 additions and 2 deletions

View File

@ -26,10 +26,18 @@ import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.CellStyle; import org.apache.poi.ss.usermodel.CellStyle;
import org.apache.poi.ss.usermodel.CellType; import org.apache.poi.ss.usermodel.CellType;
import org.apache.poi.ss.usermodel.DataFormat; 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.Row;
import org.apache.poi.ss.usermodel.Sheet; import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook; import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.ss.usermodel.WorkbookFactory; 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.Document;
import org.jsoup.nodes.Element; import org.jsoup.nodes.Element;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
@ -1868,6 +1876,9 @@ public class EstimateService {
InputStream in = new ByteArrayInputStream(excelBytes); InputStream in = new ByteArrayInputStream(excelBytes);
workbook = WorkbookFactory.create(in); // JXLS POI 엑셀로 재변환 workbook = WorkbookFactory.create(in); // JXLS POI 엑셀로 재변환
// 이미지 가로세로 비율 보정
adjustImageAspectRatio(workbook);
// SchDrawingFlg (1 : 견적서,2 : 발전시뮬레이션, 3 : 도면, 4 : 가대) // SchDrawingFlg (1 : 견적서,2 : 발전시뮬레이션, 3 : 도면, 4 : 가대)
// ex) 1|2|3|4 // ex) 1|2|3|4
if (!StringUtils.isEmpty(estimateRequest.getSchDrawingFlg())) { 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) { private byte[] loadDrawingImage(URL url) {
try { try {
URLConnection connection = url.openConnection(); 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) { if (connection instanceof HttpURLConnection) {
HttpURLConnection httpConnection = (HttpURLConnection) connection; if (((HttpURLConnection) connection).getResponseCode() != HttpURLConnection.HTTP_OK) {
if (httpConnection.getResponseCode() != HttpURLConnection.HTTP_OK) {
return null; return null;
} }
} }

View File

@ -77,6 +77,14 @@ public class ObjectController {
return objectService.selectParentAgentInfo(saleStoreId, storeLvl); return objectService.selectParentAgentInfo(saleStoreId, storeLvl);
} }
@Operation(description = "하위 판매점 목록을 조회한다.")
@GetMapping("/saleStore/{saleStoreId}/childList")
@ResponseStatus(HttpStatus.OK)
public List<SaleStoreResponse> selectChildSaleStoreList(
@PathVariable String saleStoreId, String storeLvl, String userId) throws Exception {
return objectService.selectChildSaleStoreList(saleStoreId, storeLvl, userId);
}
@Operation(description = "물건정보 목록을 조회한다.") @Operation(description = "물건정보 목록을 조회한다.")
@GetMapping("/list") @GetMapping("/list")
@ResponseStatus(HttpStatus.OK) @ResponseStatus(HttpStatus.OK)

View File

@ -31,6 +31,9 @@ public interface ObjectMapper {
// 상위 판매점 정보 조회 // 상위 판매점 정보 조회
public SaleStoreResponse selectParentAgentInfo(String saleStoreId, String storeLvl); public SaleStoreResponse selectParentAgentInfo(String saleStoreId, String storeLvl);
// 하위 판매점 목록 조회
public List<SaleStoreResponse> selectChildSaleStoreList(String saleStoreId, String storeLvl, String userId);
// 물건정보 메인 목록 조회 // 물건정보 메인 목록 조회
public List<ObjectResponse> selectObjectMainList(ObjectRequest objectRequest); public List<ObjectResponse> selectObjectMainList(ObjectRequest objectRequest);

View File

@ -182,6 +182,30 @@ public class ObjectService {
return result; return result;
} }
/**
* 하위 판매점 목록 조회
*
* @param saleStoreId 판매점 아이디 (부모)
* @param storeLvl 판매점 레벨
* @return List<SaleStoreResponse> 하위 판매점 목록
* @throws Exception
*/
public List<SaleStoreResponse> 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);
}
/** /**
* 물건정보 메인 목록 조회 * 물건정보 메인 목록 조회
* *

View File

@ -162,6 +162,54 @@
AND A.DEL_FLG = '0' AND A.DEL_FLG = '0'
</select> </select>
<select id="selectChildSaleStoreList" resultType="com.interplug.qcast.biz.object.dto.SaleStoreResponse">
/* sqlid : com.interplug.qcast.biz.object.selectChildSaleStoreList */
/* 계층형 구조에 맞는 하위 SALE_STORE_ID 축출 - 재귀함수 */
WITH SALES_STORE_CTE AS (
SELECT
SALE_STORE_ID
, SALE_STORE_NAME
, SALE_STORE_LEVEL
FROM M_SALES_STORE WITH(NOLOCK)
WHERE APPROVE_FLG = '2'
AND DEL_FLG = '0'
AND SALE_STORE_ID = #{saleStoreId}
UNION ALL
SELECT
A.SALE_STORE_ID
, A.SALE_STORE_NAME
, A.SALE_STORE_LEVEL
FROM M_SALES_STORE A WITH(NOLOCK)
INNER JOIN SALES_STORE_CTE B
ON A.PARENT_SALE_AGENT_ID = B.SALE_STORE_ID
WHERE A.APPROVE_FLG = '2'
AND A.DEL_FLG = '0'
)
SELECT
TT.*
FROM (
SELECT
T.*
, CASE WHEN UFA.DISP_ORDER IS NULL THEN 'B'
ELSE 'A' + CAST(UFA.DISP_ORDER AS NVARCHAR) END AS PRIORITY
FROM (
SELECT
A.SALE_STORE_ID
, A.SALE_STORE_NAME
, A.SALE_STORE_LEVEL
FROM SALES_STORE_CTE A
WHERE A.SALE_STORE_LEVEL <![CDATA[ >= ]]> ${storeLvl}
) T
LEFT OUTER JOIN M_USER_FAV_SALES_STORE UFA WITH (NOLOCK)
ON T.SALE_STORE_ID = UFA.SALE_STORE_ID
AND UFA.USER_ID = #{userId}
AND UFA.DEL_FLG = '0'
) TT
ORDER BY TT.PRIORITY ASC, TT.SALE_STORE_LEVEL ASC, TT.SALE_STORE_ID ASC
</select>
<select id="selectObjectMainList" parameterType="com.interplug.qcast.biz.object.dto.ObjectRequest" resultType="com.interplug.qcast.biz.object.dto.ObjectResponse"> <select id="selectObjectMainList" parameterType="com.interplug.qcast.biz.object.dto.ObjectRequest" resultType="com.interplug.qcast.biz.object.dto.ObjectResponse">
/* sqlid : com.interplug.qcast.biz.object.selectObjectMainList */ /* sqlid : com.interplug.qcast.biz.object.selectObjectMainList */