diff --git a/.gitignore b/.gitignore index f1d22bf..cd16182 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,5 @@ Thumbs.db # Docker volumes .docker/ +# Cursor files +.cursor/ diff --git a/WebContent/WEB-INF/classes/com/pms/salesmgmt/mapper/salesNcollectMgmt.xml b/WebContent/WEB-INF/classes/com/pms/salesmgmt/mapper/salesNcollectMgmt.xml index 61b6849..c08aafe 100644 --- a/WebContent/WEB-INF/classes/com/pms/salesmgmt/mapper/salesNcollectMgmt.xml +++ b/WebContent/WEB-INF/classes/com/pms/salesmgmt/mapper/salesNcollectMgmt.xml @@ -812,188 +812,332 @@ ,editEmpNo = #{cretEmpNo} /* 수정자 */ - + - - + /* salesNcollectMgmt.getSalesMgmtGridListCount - VIEW 기반 */ SELECT - COUNT(1) - FROM SWSC110A_TBL B - LEFT JOIN SWSD010A_TBL A ON A.orderNo = B.orderNo - INNER JOIN SWSB110A_TBL C ON B.goodsCd = C.goodsCd - INNER JOIN SWSB210A_TBL D ON B.custCd = D.custCd + CEIL(COUNT(1)::float / #{COUNT_PER_PAGE}::float)::numeric::integer AS MAX_PAGE_SIZE, + COUNT(1)::integer AS TOTAL_CNT + FROM v_sales_mgmt_with_splits WHERE 1 = 1 - AND B.SaleType = #{orderType} + AND CATEGORY_CD = #{orderType} - AND C.c_class = #{productType} + AND PRODUCT_TYPE = #{productType} - AND B.nationGB = #{nation} + AND AREA_CD = #{nation} - AND D.custNm LIKE CONCAT('%', #{customer}, '%') + AND CUSTOMER LIKE CONCAT('%', #{customer}, '%') - AND B.freeyn = #{paymentType} + AND PAID_TYPE = #{paymentType} - AND C.GoodsCd LIKE CONCAT('%', #{productNo}, '%') + AND PRODUCT_NO LIKE CONCAT('%', #{productNo}, '%') - AND C.GoodsNm LIKE CONCAT('%', #{productName}, '%') + AND PRODUCT_NAME LIKE CONCAT('%', #{productName}, '%') - AND A.serialno LIKE CONCAT('%', #{serialNo}, '%') + AND SERIAL_NO LIKE CONCAT('%', #{serialNo}, '%') - AND B.endsale = #{orderStatus} + AND STATUS_CD = #{orderStatus} - AND B.OrderNo LIKE CONCAT('%', #{poNo}, '%') + AND PO_NO LIKE CONCAT('%', #{poNo}, '%') - AND B.custreq =]]> #{requestDateFrom} + AND REQUEST_DATE =]]> #{requestDateFrom} - AND B.custreq #{requestDateTo} + AND REQUEST_DATE #{requestDateTo} - AND B.OrderDate =]]> #{orderDateFrom} + AND ORDER_DATE =]]> #{orderDateFrom} - AND B.OrderDate #{orderDateTo} + AND ORDER_DATE #{orderDateTo} - AND B.shippingstatus = #{shippingStatus} + AND SALES_STATUS = #{shippingStatus} - AND B.shippingdate =]]> #{shippingDateFrom} + AND SHIPPING_DATE =]]> #{shippingDateFrom} - AND B.shippingdate #{shippingDateTo} + AND SHIPPING_DATE #{shippingDateTo} - AND B.outGb = #{shippingMethod} + AND SHIPPING_METHOD = #{shippingMethod} - AND B.bEmpNo = #{manager} + AND PM_USER_ID = #{manager} - AND B.incoterms = #{incoterms} + AND INCOTERMS = #{incoterms} + + + + + + + + + /* salesNcollectMgmt.insertSplitShipmentLog */ + INSERT INTO shipment_log ( + target_objid, + log_type, + log_message, + split_quantity, + original_quantity, + shipping_status, + sales_unit_price, + sales_supply_price, + sales_vat, + sales_total_amount, + sales_currency, + sales_exchange_rate, + serial_no, + shipping_date, + shipping_method, + manager_user_id, + incoterms, + remark, + is_split_record, + reg_date, + reg_user_id + ) VALUES ( + #{objid}, + #{logType}, + #{logMessage}, + #{splitQuantity}::integer, + #{originalQuantity}::integer, + 'PENDING', + #{salesUnitPrice}::numeric, + #{salesSupplyPrice}::numeric, + #{salesVat}::numeric, + #{salesTotalAmount}::numeric, + #{salesCurrency}, + #{salesExchangeRate}::numeric, + #{serialNo}, + + + TO_DATE(#{shippingDate}, 'YYYY-MM-DD'), + + + NULL, + + + #{shippingMethod}, + #{managerUserId}, + #{incoterms}, + #{remark}, + true, + NOW(), + #{userId} + ) + + diff --git a/WebContent/WEB-INF/view/salesmgmt/salesMgmt/revenueMgmtList.jsp b/WebContent/WEB-INF/view/salesmgmt/salesMgmt/revenueMgmtList.jsp index 313ddd4..ccfcb8f 100644 --- a/WebContent/WEB-INF/view/salesmgmt/salesMgmt/revenueMgmtList.jsp +++ b/WebContent/WEB-INF/view/salesmgmt/salesMgmt/revenueMgmtList.jsp @@ -14,6 +14,18 @@ <%=Constants.SYSTEM_NAME%> @@ -91,6 +255,9 @@ function fn_excel() {
+ + +
@@ -115,21 +282,27 @@ function fn_excel() { - - - - - - - - - - - + + + + + + + + + + + + + @@ -181,6 +354,23 @@ function fn_excel() {
+ + +
+ + + + + + +
+ Total 공급가액: 0 원 + + Total 부가세: 0 원 + + Total 총액: 0 원 +
+
<%@include file= "/WEB-INF/view/common/common_gridArea.jsp" %>
diff --git a/WebContent/WEB-INF/view/salesmgmt/salesMgmt/salesMgmtList.jsp b/WebContent/WEB-INF/view/salesmgmt/salesMgmt/salesMgmtList.jsp index 28943d5..7d5ddac 100644 --- a/WebContent/WEB-INF/view/salesmgmt/salesMgmt/salesMgmtList.jsp +++ b/WebContent/WEB-INF/view/salesmgmt/salesMgmt/salesMgmtList.jsp @@ -11,7 +11,7 @@ -판매 관리 +<%=Constants.SYSTEM_NAME%> +

- 판매 관리 + 영업관리_판매관리

- - + +
@@ -115,11 +260,13 @@ function fn_search(){ - - - - - + + + - - + + + + + + + + + + + + - - - - ~ - - ~ - - - - ~ - - - - - - @@ -166,7 +303,26 @@ function fn_search(){ - + + + + + + + + + + ~ + + + + ~ + + ~
diff --git a/WebContent/WEB-INF/view/salesmgmt/salesMgmt/salesRegForm.jsp b/WebContent/WEB-INF/view/salesmgmt/salesMgmt/salesRegForm.jsp index 56fae4f..eb2111a 100644 --- a/WebContent/WEB-INF/view/salesmgmt/salesMgmt/salesRegForm.jsp +++ b/WebContent/WEB-INF/view/salesmgmt/salesMgmt/salesRegForm.jsp @@ -22,14 +22,37 @@ // 저장 버튼 $("#btnSave").click(function() { - // 저장 로직은 추후 구현 - alert("저장 기능은 구현 예정입니다."); + fn_save(); }); }); + + function fn_save() { + if(confirm("저장하시겠습니까?")) { + $.ajax({ + url: "/salesMgmt/saveSales.do", + type: "POST", + data: $("#form1").serialize(), + dataType: "json", + success: function(data) { + alert(data.msg); + if (data.result) { + opener.fn_search(); // 부모창 그리드 새로고침 + window.close(); // 팝업 닫기 + } + }, + error: function(jqxhr, status, error) { + console.error("Error:", error); + alert("저장에 실패했습니다."); + } + }); + } + } + +

diff --git a/WebContent/WEB-INF/view/salesmgmt/salesMgmt/splitShipmentForm.jsp b/WebContent/WEB-INF/view/salesmgmt/salesMgmt/splitShipmentForm.jsp new file mode 100644 index 0000000..0e0cffb --- /dev/null +++ b/WebContent/WEB-INF/view/salesmgmt/salesMgmt/splitShipmentForm.jsp @@ -0,0 +1,174 @@ +<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> +<%@ include file="/init.jsp" %> + + + + +분할출하 등록 + + + + + + + + + + +
+
+

+ 분할출하 등록 +

+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ + + +
+ + + +
+ + + +
+ +
+
+ +
+
+ + +
+
+
+ + + + diff --git a/WebContent/WEB_INF/view/salesmgmt/salesMgmt/salesMgmtList.jsp b/WebContent/WEB_INF/view/salesmgmt/salesMgmt/salesMgmtList.jsp new file mode 100644 index 0000000..e69de29 diff --git a/database/add_order_columns.sql b/database/add_order_columns.sql deleted file mode 100644 index f6f8af3..0000000 --- a/database/add_order_columns.sql +++ /dev/null @@ -1,34 +0,0 @@ --- CONTRACT_MGMT 테이블에 수주 관련 컬럼 추가 --- 기존 컬럼 활용: --- - 수주상태: contract_result (기존 컬럼 사용) --- - 환종: contract_currency (기존 컬럼 사용) --- - 환율: exchange_rate (기존 컬럼 사용) --- - 발주번호: po_no (기존 컬럼 사용) - --- 수주 관련 신규 컬럼들 -ALTER TABLE public.contract_mgmt ADD COLUMN IF NOT EXISTS order_date VARCHAR(50); -ALTER TABLE public.contract_mgmt ADD COLUMN IF NOT EXISTS order_unit_price NUMERIC(15,2); -ALTER TABLE public.contract_mgmt ADD COLUMN IF NOT EXISTS order_supply_price NUMERIC(15,2); -ALTER TABLE public.contract_mgmt ADD COLUMN IF NOT EXISTS order_vat NUMERIC(15,2); -ALTER TABLE public.contract_mgmt ADD COLUMN IF NOT EXISTS order_total_amount NUMERIC(15,2); - --- 컬럼 코멘트 추가 -COMMENT ON COLUMN public.contract_mgmt.contract_result IS '영업진행상태 / 수주상태'; -COMMENT ON COLUMN public.contract_mgmt.po_no IS '발주번호'; -COMMENT ON COLUMN public.contract_mgmt.order_date IS '발주일'; -COMMENT ON COLUMN public.contract_mgmt.order_unit_price IS '수주 단가'; -COMMENT ON COLUMN public.contract_mgmt.order_supply_price IS '수주 공급가액'; -COMMENT ON COLUMN public.contract_mgmt.order_vat IS '수주 부가세'; -COMMENT ON COLUMN public.contract_mgmt.order_total_amount IS '수주 총액'; -COMMENT ON COLUMN public.contract_mgmt.contract_currency IS '견적/수주 환종'; -COMMENT ON COLUMN public.contract_mgmt.exchange_rate IS '견적/수주 환율'; - --- 인덱스 추가 (선택사항) -CREATE INDEX IF NOT EXISTS idx_contract_result ON public.contract_mgmt(contract_result); -CREATE INDEX IF NOT EXISTS idx_contract_order_date ON public.contract_mgmt(order_date); - --- 조회 예시 --- SELECT objid, contract_no, contract_result, po_no, order_date, order_total_amount, contract_currency, exchange_rate --- FROM public.contract_mgmt --- WHERE contract_result IS NOT NULL; - diff --git a/database/estimate_template_tables.sql b/database/estimate_template_tables.sql deleted file mode 100644 index 50c88e5..0000000 --- a/database/estimate_template_tables.sql +++ /dev/null @@ -1,88 +0,0 @@ --- 견적서 템플릿 테이블 생성 SQL - --- 1. 견적서 템플릿 메인 테이블 -CREATE TABLE IF NOT EXISTS ESTIMATE_TEMPLATE ( - OBJID VARCHAR(50) PRIMARY KEY, - CONTRACT_OBJID VARCHAR(50) NOT NULL, -- CONTRACT_MGMT 테이블의 OBJID 참조 - TEMPLATE_TYPE VARCHAR(10) NOT NULL, -- '1': 일반 견적서, '2': 장비 견적서 - - -- 일반 견적서 필드 (Template 1) - EXECUTOR VARCHAR(200), -- 시행일자 - RECIPIENT VARCHAR(200), -- 수신처 - ESTIMATE_NO VARCHAR(100), -- 견적번호 - CONTACT_PERSON VARCHAR(100), -- 수신인 - GREETING_TEXT TEXT, -- 인사말 - - -- 장비 견적서 필드 (Template 2) - MODEL_NAME VARCHAR(200), -- 설비 Model 품명 - MODEL_CODE VARCHAR(100), -- 설비 Model 코드 - EXECUTOR_DATE VARCHAR(50), -- 시행일자 - NOTES_CONTENT TEXT, -- 비고 내용 (전체) - VALIDITY_PERIOD VARCHAR(50), -- 견적 유효기간 - - -- 비고 필드 (공통) - NOTE1 VARCHAR(500), - NOTE2 VARCHAR(500), - NOTE3 VARCHAR(500), - NOTE4 VARCHAR(500), - - -- 카테고리 정보 (장비 견적서용, JSON 형태로 저장) - CATEGORIES_JSON TEXT, - - -- 시스템 필드 - WRITER VARCHAR(50), - REGDATE TIMESTAMP DEFAULT NOW(), - CHG_USER_ID VARCHAR(50), - CHGDATE TIMESTAMP DEFAULT NOW(), - - CONSTRAINT fk_estimate_contract FOREIGN KEY (CONTRACT_OBJID) - REFERENCES CONTRACT_MGMT(OBJID) ON DELETE CASCADE -); - --- 인덱스 생성 -CREATE INDEX idx_estimate_contract ON ESTIMATE_TEMPLATE(CONTRACT_OBJID); -CREATE INDEX idx_estimate_type ON ESTIMATE_TEMPLATE(TEMPLATE_TYPE); - --- 코멘트 추가 -COMMENT ON TABLE ESTIMATE_TEMPLATE IS '견적서 템플릿 정보'; -COMMENT ON COLUMN ESTIMATE_TEMPLATE.CONTRACT_OBJID IS '견적요청 OBJID (CONTRACT_MGMT 참조)'; -COMMENT ON COLUMN ESTIMATE_TEMPLATE.TEMPLATE_TYPE IS '템플릿 타입 (1:일반견적서, 2:장비견적서)'; - - --- 2. 견적서 템플릿 품목 테이블 -CREATE TABLE IF NOT EXISTS ESTIMATE_TEMPLATE_ITEM ( - OBJID SERIAL PRIMARY KEY, - TEMPLATE_OBJID VARCHAR(50) NOT NULL, -- ESTIMATE_TEMPLATE 테이블의 OBJID 참조 - SEQ INTEGER NOT NULL, -- 순번 - CATEGORY VARCHAR(100), -- 카테고리 (장비 견적서용) - DESCRIPTION VARCHAR(500), -- 품명 - SPECIFICATION TEXT, -- 규격/사양 - QUANTITY VARCHAR(50), -- 수량 - UNIT VARCHAR(50), -- 단위 - UNIT_PRICE NUMERIC(15,2), -- 단가 - AMOUNT NUMERIC(15,2), -- 금액 - NOTE VARCHAR(500), -- 비고 - REMARK VARCHAR(500), -- 특이사항 - - CONSTRAINT fk_item_template FOREIGN KEY (TEMPLATE_OBJID) - REFERENCES ESTIMATE_TEMPLATE(OBJID) ON DELETE CASCADE -); - --- 인덱스 생성 -CREATE INDEX idx_item_template ON ESTIMATE_TEMPLATE_ITEM(TEMPLATE_OBJID); -CREATE INDEX idx_item_seq ON ESTIMATE_TEMPLATE_ITEM(TEMPLATE_OBJID, SEQ); - --- 코멘트 추가 -COMMENT ON TABLE ESTIMATE_TEMPLATE_ITEM IS '견적서 템플릿 품목 정보'; -COMMENT ON COLUMN ESTIMATE_TEMPLATE_ITEM.TEMPLATE_OBJID IS '견적서 템플릿 OBJID'; -COMMENT ON COLUMN ESTIMATE_TEMPLATE_ITEM.SEQ IS '품목 순번'; -COMMENT ON COLUMN ESTIMATE_TEMPLATE_ITEM.CATEGORY IS '카테고리 (장비견적서: CNC Machine, UTILITY 등)'; - - --- 3. CONTRACT_MGMT 테이블에 CATEGORIES_JSON 컬럼 추가 (이미 없는 경우) --- ALTER TABLE ESTIMATE_TEMPLATE ADD COLUMN IF NOT EXISTS CATEGORIES_JSON TEXT; - --- 샘플 데이터 조회 쿼리 --- SELECT * FROM ESTIMATE_TEMPLATE WHERE CONTRACT_OBJID = 123; --- SELECT * FROM ESTIMATE_TEMPLATE_ITEM WHERE TEMPLATE_OBJID = 456 ORDER BY SEQ; - diff --git a/plan.md b/plan.md deleted file mode 100644 index 6754a95..0000000 --- a/plan.md +++ /dev/null @@ -1,33 +0,0 @@ -# 프로젝트: 판매 관리 기능 개발 - -## 개요 -판매 관리 페이지의 UI를 개선하고, 판매 등록 및 조회 기능을 TDD(테스트 주도 개발) 원칙에 따라 개발합니다. - -## 핵심 기능 -1. 판매 관리 페이지 UI 구현 -2. 판매 등록 팝업 기능 -3. 판매 목록 조회 및 필터링 -4. 데이터베이스 연동 및 CRUD 기능 구현 - -## 테스트 계획 -### 1단계: UI 및 기본 기능 -- [x] 판매 관리 페이지 기본 UI 생성 -- [x] 필터 및 테이블 컬럼 구성 -- [x] 컨트롤러 URL 매핑 및 이름 재정의 -- [x] '판매등록' 버튼 추가 -- [ ] **테스트 1**: '판매등록' 버튼 클릭 시 팝업창이 정상적으로 뜨는가? -- [ ] **테스트 2**: '출고방법' 드롭다운에 DB 데이터가 정상적으로 표시되는가? -- [ ] **테스트 3**: '인도조건' 드롭다운에 DB 데이터가 정상적으로 표시되는가? - -### 2단계: 데이터 처리 -- [ ] **테스트 4**: 판매 등록 팝업에서 '저장' 버튼 클릭 시 데이터가 DB에 저장되는가? -- [ ] **테스트 5**: 판매 목록 조회 시 DB에서 데이터를 가져와 그리드에 표시하는가? -- [ ] **테스트 6**: 검색 조건으로 필터링했을 때 결과가 정확히 조회되는가? - -## 에러 처리 계획 -- 필수 입력값 누락 시 유효성 검사 및 알림 -- 데이터 저장 실패 시 에러 메시지 처리 - -## 진행 상태 -- 완료된 테스트는 [x]로 표시합니다. -- 현재 진행 중인 테스트는 **[진행중]** 으로 표시합니다. diff --git a/src/com/pms/salesmgmt/controller/SalesNcollectMgmtController.java b/src/com/pms/salesmgmt/controller/SalesNcollectMgmtController.java index c5909f5..2cafcb6 100644 --- a/src/com/pms/salesmgmt/controller/SalesNcollectMgmtController.java +++ b/src/com/pms/salesmgmt/controller/SalesNcollectMgmtController.java @@ -58,7 +58,7 @@ public class SalesNcollectMgmtController { /** 매출수금 Service */ @Autowired - private SalesNcollectMgmtService salseNcollectMgmtService; + private SalesNcollectMgmtService salesNcollectMgmtService; /** 영업관리 공통 Service */ @Autowired @@ -200,16 +200,33 @@ public class SalesNcollectMgmtController { Map resultMap = new HashMap(); try { - List> list = salseNcollectMgmtService.getSalesMgmtGridList(request, paramMap); - int totalCount = salseNcollectMgmtService.getSalesMgmtGridListCount(paramMap); + System.out.println("===== revenueGridList.do 파라미터 ====="); + System.out.println("paramMap: " + paramMap); + + List> list = salesNcollectMgmtService.getSalesMgmtGridList(request, paramMap); + int totalCount = salesNcollectMgmtService.getSalesMgmtGridListCount(paramMap); + Map totals = salesNcollectMgmtService.getSalesMgmtTotals(paramMap); + + System.out.println("list size: " + list.size()); + System.out.println("totalCount: " + totalCount); + System.out.println("totals: " + totals); resultMap.put("RESULTLIST", list); resultMap.put("last_page", (int) Math.ceil((double)totalCount / Integer.parseInt(CommonUtils.checkNull(request.getParameter("size"), "10")))); + resultMap.put("TOTALS", totals); // Total 공급가액, 부가세, 총액 } catch(Exception e) { e.printStackTrace(); // Tabulator는 에러 발생 시 빈 배열을 기대합니다. resultMap.put("RESULTLIST", new java.util.ArrayList<>()); resultMap.put("last_page", 0); + + // Total 초기화 + Map emptyTotals = new HashMap(); + + emptyTotals.put("TOTAL_SUPPLY_PRICE", 0); + emptyTotals.put("TOTAL_VAT", 0); + emptyTotals.put("TOTAL_AMOUNT", 0); + resultMap.put("TOTALS", emptyTotals); } return resultMap; @@ -232,6 +249,12 @@ public class SalesNcollectMgmtController { codeMap.put("managerList", salesMgmtCommonService.bizMakeOptionList("", "", "salesMgmtCommon.getSalesmanList")); + // 기존 판매 정보 조회 + if(paramMap.get("orderNo") != null && !paramMap.get("orderNo").equals("")) { + Map saleInfo = salesNcollectMgmtService.getSaleInfo(paramMap); + request.setAttribute("saleInfo", saleInfo); + } + request.setAttribute("codeMap", codeMap); } catch (Exception e) { e.printStackTrace(); @@ -239,6 +262,87 @@ public class SalesNcollectMgmtController { return "/salesmgmt/salesMgmt/salesRegForm"; } + @RequestMapping(value = "/salesMgmt/salesMgmtGridList.do", method = RequestMethod.POST) + @ResponseBody + public Map getSalesMgmtGridList(HttpServletRequest request, @RequestParam Map paramMap) { + Map resultMap = new HashMap(); + + try { + List> list = salesNcollectMgmtService.getSalesMgmtGridList(request, paramMap); + int totalCount = salesNcollectMgmtService.getSalesMgmtGridListCount(paramMap); + + resultMap.put("RESULTLIST", list); + resultMap.put("last_page", (int) Math.ceil((double)totalCount / Integer.parseInt(CommonUtils.checkNull(request.getParameter("size"), "10")))); + } catch(Exception e) { + e.printStackTrace(); + // Tabulator는 에러 발생 시 빈 배열을 기대합니다. + resultMap.put("RESULTLIST", new java.util.ArrayList<>()); + resultMap.put("last_page", 0); + } + + return resultMap; + } + + @RequestMapping(value = "/salesMgmt/saveSales.do", method = RequestMethod.POST) + @ResponseBody + public Map saveSales(HttpServletRequest request, @RequestParam Map paramMap) { + Map resultMap = new HashMap(); + try { + resultMap = salesNcollectMgmtService.saveSaleRegistration(request, paramMap); + } catch (Exception e) { + resultMap.put("result", false); + resultMap.put("msg", "저장 중 오류가 발생했습니다."); + e.printStackTrace(); + } + return resultMap; + } + + /** + *
+	 * 분할출하 팝업 폼
+	 * 
+ * @param request + * @param paramMap + * @return String + */ + @RequestMapping(value = "/salesMgmt/splitShipmentForm.do", method = RequestMethod.GET) + public String showSplitShipmentForm(HttpServletRequest request, @RequestParam Map paramMap) { + try { + Map codeMap = new HashMap(); + + // 담당자 + codeMap.put("managerList", + salesMgmtCommonService.bizMakeOptionList("", "", "salesMgmtCommon.getSalesmanList")); + + request.setAttribute("codeMap", codeMap); + } catch (Exception e) { + e.printStackTrace(); + } + return "/salesmgmt/salesMgmt/splitShipmentForm"; + } + + /** + *
+	 * 분할출하 저장 처리
+	 * 
+ * @param request + * @param paramMap - 분할출하 정보 + * @return Map + */ + @RequestMapping(value = "/salesMgmt/saveSplitShipment.do", method = RequestMethod.POST) + @ResponseBody + public Map saveSplitShipment(HttpServletRequest request, @RequestParam Map paramMap) { + Map resultMap = new HashMap(); + try { + resultMap = salesNcollectMgmtService.splitShipment(request, paramMap); + } catch (Exception e) { + resultMap.put("result", false); + resultMap.put("msg", "분할출하 처리 중 오류가 발생했습니다."); + e.printStackTrace(); + } + return resultMap; + } + /** *
 	 * 계약관리 목록 조회
@@ -299,7 +403,7 @@ public class SalesNcollectMgmtController {
 	public String sales(HttpServletRequest request
 					  , @RequestParam Map paramMap) {
 		try {
-			List> list = salseNcollectMgmtService.getSalseAllByOrderNo(request, paramMap);
+			List> list = salesNcollectMgmtService.getSalseAllByOrderNo(request, paramMap);
 			
 			request.setAttribute("LIST", list);
 		} catch(Exception e) {
@@ -335,7 +439,7 @@ public class SalesNcollectMgmtController {
 		try {
 			if(StringUtils.isNotBlank(saleNo)) {
 				// 매출관리 조회
-				info = salseNcollectMgmtService.getSalesMgmt(paramMap);
+				info = salesNcollectMgmtService.getSalesMgmt(paramMap);
 			} else {
 				info = new HashMap();
 			}
@@ -388,7 +492,7 @@ public class SalesNcollectMgmtController {
 		Map resultMap = new HashMap();
 		
 		try {
-			resultMap.put("RESULT", salseNcollectMgmtService.saveSalesMgmt(request, paramMap));		
+			resultMap.put("RESULT", salesNcollectMgmtService.saveSalesMgmt(request, paramMap));		
 		} catch(Exception e) {
 			e.printStackTrace();
 		}
@@ -409,7 +513,7 @@ public class SalesNcollectMgmtController {
 		Map resultMap = new HashMap();
 		
 		try {
-			resultMap.put("RESULT", salseNcollectMgmtService.deleteSalesMgmt(request, paramMap));
+			resultMap.put("RESULT", salesNcollectMgmtService.deleteSalesMgmt(request, paramMap));
 			
 			
 		
@@ -450,7 +554,7 @@ public class SalesNcollectMgmtController {
 						   , @RequestParam Map paramMap) { 
 
 		try {
-				List> list = salseNcollectMgmtService.getCollectAllByOrderNo(request, paramMap);
+				List> list = salesNcollectMgmtService.getCollectAllByOrderNo(request, paramMap);
 				
 				request.setAttribute("LIST", list);
 			} catch(Exception e) {
@@ -491,7 +595,7 @@ public class SalesNcollectMgmtController {
 			System.out.println("paramMap##################### " + paramMap);
 			if(StringUtils.isNotBlank(saleNo)) {
 				// 수금관리 조회
-				info = salseNcollectMgmtService.getCollectMgmt(paramMap);
+				info = salesNcollectMgmtService.getCollectMgmt(paramMap);
 			} else {
 				info = new HashMap();
 			}
@@ -540,7 +644,7 @@ public class SalesNcollectMgmtController {
 			*/
 			
 			//하단 그리드를 위한 수금 목록 조회
-			List> list = salseNcollectMgmtService.getCollectAllByOrderNo(request, paramMap);			
+			List> list = salesNcollectMgmtService.getCollectAllByOrderNo(request, paramMap);			
 			request.setAttribute("LIST", list);
 			
 			request.setAttribute("info", info);
@@ -577,7 +681,7 @@ public class SalesNcollectMgmtController {
 		Map resultMap = new HashMap();
 		System.out.println("ctr::paramMap##################### " + paramMap);		
 		try {
-			resultMap.put("RESULT", salseNcollectMgmtService.saveCollectMgmt(request, paramMap));		
+			resultMap.put("RESULT", salesNcollectMgmtService.saveCollectMgmt(request, paramMap));		
 		
 		} catch(Exception e) {
 			e.printStackTrace();			
@@ -599,7 +703,7 @@ public class SalesNcollectMgmtController {
 		Map resultMap = new HashMap();
 		
 		try {
-			resultMap.put("RESULT", salseNcollectMgmtService.deleteCollectMgmt(request, paramMap));
+			resultMap.put("RESULT", salesNcollectMgmtService.deleteCollectMgmt(request, paramMap));
 		} catch(Exception e) {
 			e.printStackTrace();
 		}
diff --git a/src/com/pms/salesmgmt/mapper/salesNcollectMgmt.xml b/src/com/pms/salesmgmt/mapper/salesNcollectMgmt.xml
index 61b6849..c08aafe 100644
--- a/src/com/pms/salesmgmt/mapper/salesNcollectMgmt.xml
+++ b/src/com/pms/salesmgmt/mapper/salesNcollectMgmt.xml
@@ -812,188 +812,332 @@
 			,editEmpNo = #{cretEmpNo}			/* 수정자 */
 	
 	
-	
+	
 	
 	
-	
-	
+		/* salesNcollectMgmt.getSalesMgmtGridListCount - VIEW 기반 */
 		SELECT 
-			COUNT(1)
-		FROM SWSC110A_TBL B
-		LEFT JOIN SWSD010A_TBL A ON A.orderNo = B.orderNo
-		INNER JOIN SWSB110A_TBL C ON B.goodsCd = C.goodsCd
-		INNER JOIN SWSB210A_TBL D ON B.custCd = D.custCd
+			CEIL(COUNT(1)::float / #{COUNT_PER_PAGE}::float)::numeric::integer AS MAX_PAGE_SIZE,
+			COUNT(1)::integer AS TOTAL_CNT
+		FROM v_sales_mgmt_with_splits
 		WHERE 1 = 1
 		
-			AND B.SaleType = #{orderType}
+			AND CATEGORY_CD = #{orderType}
 		
 		
-			AND C.c_class = #{productType}
+			AND PRODUCT_TYPE = #{productType}
 		
 		
-			AND B.nationGB = #{nation}
+			AND AREA_CD = #{nation}
 		
 		
-			AND D.custNm LIKE CONCAT('%', #{customer}, '%')
+			AND CUSTOMER LIKE CONCAT('%', #{customer}, '%')
 		
 		
-			AND B.freeyn = #{paymentType}
+			AND PAID_TYPE = #{paymentType}
 		
 		
-			AND C.GoodsCd LIKE CONCAT('%', #{productNo}, '%')
+			AND PRODUCT_NO LIKE CONCAT('%', #{productNo}, '%')
 		
 		
-			AND C.GoodsNm LIKE CONCAT('%', #{productName}, '%')
+			AND PRODUCT_NAME LIKE CONCAT('%', #{productName}, '%')
 		
 		
-			AND A.serialno LIKE CONCAT('%', #{serialNo}, '%')
+			AND SERIAL_NO LIKE CONCAT('%', #{serialNo}, '%')
 		
 		
-			AND B.endsale = #{orderStatus}
+			AND STATUS_CD = #{orderStatus}
 		
 		
-			AND B.OrderNo LIKE CONCAT('%', #{poNo}, '%')
+			AND PO_NO LIKE CONCAT('%', #{poNo}, '%')
 		
 		
-			AND B.custreq =]]> #{requestDateFrom}
+			AND REQUEST_DATE =]]> #{requestDateFrom}
 		
 		
-			AND B.custreq  #{requestDateTo}
+			AND REQUEST_DATE  #{requestDateTo}
 		
 		
-			AND B.OrderDate =]]> #{orderDateFrom}
+			AND ORDER_DATE =]]> #{orderDateFrom}
 		
 		
-			AND B.OrderDate  #{orderDateTo}
+			AND ORDER_DATE  #{orderDateTo}
 		
 		
-			AND B.shippingstatus = #{shippingStatus}
+			AND SALES_STATUS = #{shippingStatus}
 		
 		
-			AND B.shippingdate =]]> #{shippingDateFrom}
+			AND SHIPPING_DATE =]]> #{shippingDateFrom}
 		
 		
-			AND B.shippingdate  #{shippingDateTo}
+			AND SHIPPING_DATE  #{shippingDateTo}
 		
 		
-			AND B.outGb = #{shippingMethod}
+			AND SHIPPING_METHOD = #{shippingMethod}
 		
 		
-			AND B.bEmpNo = #{manager}
+			AND PM_USER_ID = #{manager}
 		
 		
-			AND B.incoterms = #{incoterms}
+			AND INCOTERMS = #{incoterms}
 		
 	
 	
+	
+	
+
+	
+	
+
+	
+	
+		/* salesNcollectMgmt.insertSplitShipmentLog */
+		INSERT INTO shipment_log (
+			target_objid,
+			log_type,
+			log_message,
+			split_quantity,
+			original_quantity,
+			shipping_status,
+			sales_unit_price,
+			sales_supply_price,
+			sales_vat,
+			sales_total_amount,
+			sales_currency,
+			sales_exchange_rate,
+			serial_no,
+			shipping_date,
+			shipping_method,
+			manager_user_id,
+			incoterms,
+			remark,
+			is_split_record,
+			reg_date,
+			reg_user_id
+		) VALUES (
+			#{objid},
+			#{logType},
+			#{logMessage},
+			#{splitQuantity}::integer,
+			#{originalQuantity}::integer,
+			'PENDING',
+			#{salesUnitPrice}::numeric,
+			#{salesSupplyPrice}::numeric,
+			#{salesVat}::numeric,
+			#{salesTotalAmount}::numeric,
+			#{salesCurrency},
+			#{salesExchangeRate}::numeric,
+			#{serialNo},
+			
+				
+					TO_DATE(#{shippingDate}, 'YYYY-MM-DD'),
+				
+				
+					NULL,
+				
+			
+			#{shippingMethod},
+			#{managerUserId},
+			#{incoterms},
+			#{remark},
+			true,
+			NOW(),
+			#{userId}
+		)
+	
+	
 
 
diff --git a/src/com/pms/salesmgmt/service/SalesNcollectMgmtService.java b/src/com/pms/salesmgmt/service/SalesNcollectMgmtService.java
index b2b71ae..00d2d12 100644
--- a/src/com/pms/salesmgmt/service/SalesNcollectMgmtService.java
+++ b/src/com/pms/salesmgmt/service/SalesNcollectMgmtService.java
@@ -147,14 +147,18 @@ public class SalesNcollectMgmtService {
 		try {
 			sqlSession = SqlMapConfig.getInstance().getSqlSession();
 			
-			// Tabulator는 page, size 파라미터를 사용
-			String page = CommonUtils.checkNull(request.getParameter("page"), "1");
-			String size = CommonUtils.checkNull(request.getParameter("size"), Constants.ADMIN_COUNT_PER_PAGE+"");
-			int pageNum = Integer.parseInt(page);
-			int sizeNum = Integer.parseInt(size);
+			// 페이징 HTML 생성을 위해 DB에서 총 카운트 조회
+			String countPerPage = CommonUtils.checkNull(request.getParameter("size"), Constants.ADMIN_COUNT_PER_PAGE+"");
+			paramMap.put("COUNT_PER_PAGE", Integer.parseInt(countPerPage));
+			Map pageMap = (HashMap) sqlSession.selectOne("salesNcollectMgmt.getSalesMgmtGridListCount", paramMap);
+			pageMap = (HashMap) CommonUtils.setPagingInfo(request, pageMap);
+			paramMap.put("PAGE_HTML", CommonUtils.checkNull(pageMap.get("PAGE_HTML")));
 			
+			// 실제 쿼리에 사용할 LIMIT, OFFSET 값 계산 (정수 타입으로)
+			String page = CommonUtils.checkNull(request.getParameter("page"), "1");
+			int pageNum = Integer.parseInt(page);
+			int sizeNum = Integer.parseInt(countPerPage);
 			paramMap.put("PAGE_START", (pageNum - 1) * sizeNum);
-			paramMap.put("COUNT_PER_PAGE", sizeNum);
 			
 			resultList = (ArrayList) sqlSession.selectList("salesNcollectMgmt.getSalesMgmtGridList", paramMap);	
 		} catch(Exception e) {
@@ -181,7 +185,10 @@ public class SalesNcollectMgmtService {
 		
 		try {
 			sqlSession = SqlMapConfig.getInstance().getSqlSession();
-			totalCount = sqlSession.selectOne("salesNcollectMgmt.getSalesMgmtGridListCount", paramMap);
+			Map resultMap = sqlSession.selectOne("salesNcollectMgmt.getSalesMgmtGridListCount", paramMap);
+			if(resultMap != null && resultMap.get("TOTAL_CNT") != null) {
+				totalCount = Integer.parseInt(resultMap.get("TOTAL_CNT").toString());
+			}
 		} catch(Exception e) {
 			e.printStackTrace();
 		} finally {
@@ -193,6 +200,190 @@ public class SalesNcollectMgmtService {
 		return totalCount;
 	}
 	
+	/**
+	 * 매출관리 합계 조회 (Total 공급가액, 부가세, 총액)
+	 * @param paramMap - 검색 조건
+	 * @return Map - TOTAL_SUPPLY_PRICE, TOTAL_VAT, TOTAL_AMOUNT
+	 */
+	public Map getSalesMgmtTotals(Map paramMap) {
+		SqlSession sqlSession = null;
+		Map totalsMap = new HashMap();
+		
+		try {
+			sqlSession = SqlMapConfig.getInstance().getSqlSession();
+			totalsMap = sqlSession.selectOne("salesNcollectMgmt.getSalesMgmtTotals", paramMap);
+			
+			if(totalsMap == null) {
+				totalsMap = new HashMap();
+				totalsMap.put("TOTAL_SUPPLY_PRICE", 0);
+				totalsMap.put("TOTAL_VAT", 0);
+				totalsMap.put("TOTAL_AMOUNT", 0);
+			}
+		} catch(Exception e) {
+			e.printStackTrace();
+			totalsMap.put("TOTAL_SUPPLY_PRICE", 0);
+			totalsMap.put("TOTAL_VAT", 0);
+			totalsMap.put("TOTAL_AMOUNT", 0);
+		} finally {
+			if(sqlSession != null) {
+				sqlSession.close();
+			}
+		}
+		
+		return totalsMap;
+	}
+	
+	/**
+	 * 
+	 * 판매 정보 조회
+	 * 
+ * @param paramMap + * @return Map + */ + public Map getSaleInfo(Map paramMap) { + Map resultMap = new HashMap(); + SqlSession sqlSession = null; + + try { + sqlSession = SqlMapConfig.getInstance().getSqlSession(); + resultMap = sqlSession.selectOne("salesNcollectMgmt.getSaleInfo", paramMap); + } catch(Exception e) { + e.printStackTrace(); + } finally { + if(sqlSession != null) { + sqlSession.close(); + } + } + + return CommonUtils.toUpperCaseMapKey(resultMap); + } + + public Map saveSaleRegistration(HttpServletRequest request, Map paramMap) { + Map resultMap = new HashMap(); + SqlSession sqlSession = null; + + try { + sqlSession = SqlMapConfig.getInstance().getSqlSession(false); + + PersonBean person = (PersonBean) request.getSession().getAttribute(Constants.PERSON_BEAN); + paramMap.put("cretEmpNo", person.getUserId()); + + // SWSC110A_TBL 업데이트 + sqlSession.update("salesNcollectMgmt.updateSaleRegistration_swsc110a", paramMap); + + // SWSD010A_TBL 업데이트 (insert or update) + sqlSession.update("salesNcollectMgmt.upsertSaleRegistration_swsd010a", paramMap); + + sqlSession.commit(); + resultMap.put("result", true); + resultMap.put("msg", "저장되었습니다."); + } catch(Exception e) { + sqlSession.rollback(); + resultMap.put("result", false); + resultMap.put("msg", "저장 중 오류가 발생했습니다."); + e.printStackTrace(); + } finally { + if(sqlSession != null) { + sqlSession.close(); + } + } + + return resultMap; + } + + /** + *
+	 * 분할출하 처리 (로그 기반)
+	 * - 원본 데이터는 그대로 유지
+	 * - 분할출하 로그만 shipment_log에 저장
+	 * - 화면에서는 VIEW를 통해 원본 + 로그를 합쳐서 표시
+	 * 
+ * @param request + * @param paramMap - 분할출하 정보 + * @return Map + */ + public Map splitShipment(HttpServletRequest request, Map paramMap) { + Map resultMap = new HashMap(); + SqlSession sqlSession = null; + + try { + sqlSession = SqlMapConfig.getInstance().getSqlSession(false); + + PersonBean person = (PersonBean) request.getSession().getAttribute(Constants.PERSON_BEAN); + String userId = person.getUserId(); + + // 원본 데이터 조회 + Map originalData = sqlSession.selectOne("salesNcollectMgmt.getContractByObjid", paramMap); + + if(originalData == null) { + resultMap.put("result", false); + resultMap.put("msg", "원본 데이터를 찾을 수 없습니다."); + return resultMap; + } + + // 분할 수량 + int splitQuantity = Integer.parseInt(paramMap.get("splitQuantity").toString()); + int originalQuantity = Integer.parseInt(paramMap.get("originalQuantity").toString()); + + // 원본 데이터의 단가 정보 가져오기 + BigDecimal unitPrice = new BigDecimal(originalData.get("order_unit_price").toString()); + BigDecimal exchangeRate = new BigDecimal(originalData.get("exchange_rate").toString()); + + // 분할 금액 계산 + BigDecimal splitSupplyPrice = unitPrice.multiply(new BigDecimal(splitQuantity)); + BigDecimal splitVat = splitSupplyPrice.multiply(new BigDecimal("0.1")); + BigDecimal splitTotalAmount = splitSupplyPrice.add(splitVat); + + // 분할출하 로그 저장 + paramMap.put("logType", "SPLIT_SHIPMENT"); + paramMap.put("logMessage", "분할출하 - 원본수량: " + originalQuantity + ", 분할수량: " + splitQuantity); + paramMap.put("userId", userId); + paramMap.put("splitQuantity", splitQuantity); + paramMap.put("originalQuantity", originalQuantity); + paramMap.put("salesUnitPrice", unitPrice); + paramMap.put("salesSupplyPrice", splitSupplyPrice); + paramMap.put("salesVat", splitVat); + paramMap.put("salesTotalAmount", splitTotalAmount); + paramMap.put("salesCurrency", originalData.get("contract_currency")); + paramMap.put("salesExchangeRate", exchangeRate); + + // 팝업에서 입력한 데이터 우선, 없으면 원본 데이터 사용 + paramMap.put("shippingMethod", + StringUtils.isNotBlank((String)paramMap.get("shippingMethod")) ? + paramMap.get("shippingMethod") : originalData.get("shipping_method")); + paramMap.put("incoterms", + StringUtils.isNotBlank((String)paramMap.get("incoterms")) ? + paramMap.get("incoterms") : originalData.get("incoterms")); + paramMap.put("managerUserId", + StringUtils.isNotBlank((String)paramMap.get("manager")) ? + paramMap.get("manager") : originalData.get("pm_user_id")); + + // 팝업에서 입력한 추가 정보 + // serialNo, shippingDate, remark는 팝업에서만 입력 가능 + + sqlSession.insert("salesNcollectMgmt.insertSplitShipmentLog", paramMap); + + sqlSession.commit(); + + resultMap.put("result", true); + resultMap.put("msg", "분할출하가 완료되었습니다."); + + } catch(Exception e) { + if(sqlSession != null) { + sqlSession.rollback(); + } + resultMap.put("result", false); + resultMap.put("msg", "분할출하 처리 중 오류가 발생했습니다: " + e.getMessage()); + e.printStackTrace(); + } finally { + if(sqlSession != null) { + sqlSession.close(); + } + } + + return resultMap; + } + /** *
 	 * 매출관리 등록