diff --git a/.gitignore b/.gitignore index 431f5cf..2a8c154 100644 --- a/.gitignore +++ b/.gitignore @@ -35,7 +35,8 @@ Thumbs.db .cursor/ # Claude Code -CLAUDE.md + .claude/ .playwright-mcp/ .omc/ +.mcp.json diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..75c64a7 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,343 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +WACE-PLM (웨이스 PLM) — a Java-based enterprise Resource Planning System (RPS) for managing business processes, resources, projects, and documents. Korean-language UI and documentation throughout. + +## Tech Stack + +- **Backend:** Java 7, Spring Framework 3.2.4, MyBatis 3.2.3 +- **Frontend:** JSP (1,100+ views), jQuery 2.1.4, Select2, SweetAlert2, jqGrid 4.7.1, Tabulator/TUI-Grid, Highcharts, dhtmlxGantt, SmartEditor 2 (HuskyEZCreator) +- **Database:** PostgreSQL (JNDI datasource `java:/comp/env/plm`) +- **Server:** Apache Tomcat 7.0.94 +- **Build:** Eclipse IDE-based (no Maven/Gradle), Docker for dev/prod +- **CSS:** Custom `basic.css`, Bootstrap, jQuery UI, Select2 CSS, Tabulator CSS + +## Build & Run Commands + +```bash +# Development environment (Docker) +docker-compose -f docker-compose.dev.yml up --build -d +docker-compose -f docker-compose.dev.yml down + +# Compile only +./compile_only.sh + +# Full rebuild + restart +./rebuild-and-restart.sh + +# Production +./start-prod-full.sh +``` + +Dev port: 9090 (maps to 8080 in container). JVM: Xms=512m, Xmx=1024m. + +## Architecture + +### MVC Pattern (Spring + MyBatis) + +All source code lives under `src/com/pms/`. The request flow is: + +``` +HTTP Request (*.do) → Controller (@RequestMapping) → Service (@Service) → SqlSession → Mapper XML → PostgreSQL +``` + +- View resolution: prefix `/WEB-INF/view`, suffix `.jsp` +- JSON responses: `@ResponseBody` with Jackson converter +- Component scan: base package `com.pms` +- Session timeout: 1440 minutes (24 hours) + +### Module Structure (`src/com/pms/`) + +| Package | Domain | Scale | +|---------|--------|-------| +| `salesmgmt/` | Sales management (contracts, orders, estimates, delivery, dealers, goods, inspections) | 25+ controllers, 20+ mappers | +| `ions/itemmgmt/` | Item/BOM management, purchasing, material warehousing | 9 controllers, 8 mappers | +| `ions/productioninventory/` | Production & inventory | controllers + mappers | +| `controller/`, `service/`, `mapper/` | Core modules (admin, login, specs, distribution, project, quality, dashboard) | root-level | +| `api/` | External API integrations (ERP sync, Amaranth approval/user) | 11 client classes | +| `common/` | Shared utilities, beans, base services | CommonUtils, Constants, SessionManager | + +### View Directory Structure (`WebContent/WEB-INF/view/`) + +Major directories: `admin/` (40+ sub-modules), `salesmgmt/` (20+ sub-modules), `ions/` (itemmgmt, productioninventory), `project/` (gate, partMaster, wbs), `board/`, `bom/`, `contractMgmt/`, `costMgmt/`, `dashboard/`, `documentMng/`, `fundMgmt/`, `inventoryMng/`, `orderMgmt/`, `part/`, `quality/`, `productionplanning/`, `purchaseOrder/`, and more. + +### Key Configuration Files + +| File | Purpose | +|------|---------| +| `WebContent/WEB-INF/web.xml` | Deployment descriptor (UTF-8 filter, `*.do` mapping, 1440min session, error pages) | +| `WebContent/WEB-INF/dispatcher-servlet.xml` | Spring MVC config (component scan, view resolver, Jackson JSON, scheduler pool=10) | +| `src/com/pms/mapper/mybatisConf.xml` | MyBatis config with 52 registered mapper XML files | +| `WebContent/WEB-INF/log4j.xml` | Logging config (DEBUG level, daily rolling) | +| `tomcat-conf/context.xml` | JNDI DataSource (max 200 connections, 50 idle, 10s wait) | +| `.env.development` | Dev DB: `jdbc:postgresql://211.115.91.141:11133/waceplm` | +| `.env.production` | Prod DB: Docker internal `wace-plm-db:5432/waceplm` | + +### Database + +PostgreSQL with MyBatis XML mappers (52 registered). Automated backup via `db/backup.py` (2x daily, 7-day retention). Database dump: `db/dbexport.pgsql`. + +**Table naming:** UPPERCASE_SNAKE_CASE (primary: `USER_INFO`, `PART_MGMT`, `ORDER_MGMT`). Some legacy tables use ERP-style naming (`SWSB110A_TBL`). +**Column naming:** UPPERCASE_SNAKE_CASE (`USER_ID`, `PART_NO`, `DEL_YN`). +**Boolean columns:** `DEL_YN` with Y/N values. + +--- + +## Coding Conventions + +### Controller Pattern + +```java +@Controller +public class {Feature}Controller { + @Autowired + private {Feature}Service service; + + // List page + @RequestMapping(value = "/{module}/{feature}List.do", method = RequestMethod.GET) + public String get{Feature}All(HttpServletRequest request, @RequestParam Map paramMap) { + // ... returns JSP path string + } + + // Form popup + @RequestMapping(value = "/{module}/{feature}FormPopup.do", method = RequestMethod.GET) + public String {feature}FormPopup(HttpServletRequest request, @RequestParam Map paramMap) { ... } + + // Save (AJAX) + @ResponseBody + @RequestMapping(value = "/{module}/save{Feature}.do", method = RequestMethod.POST) + public Map save{Feature}(HttpServletRequest request, @RequestParam Map paramMap) { ... } + + // Delete (AJAX) + @ResponseBody + @RequestMapping(value = "/{module}/delete{Feature}.do", method = RequestMethod.POST) + public Map delete{Feature}(HttpServletRequest request, @RequestParam Map paramMap) { ... } +} +``` + +**Naming:** `{Feature}Controller.java` — CamelCase. Methods: `get{Feature}All`, `save{Feature}`, `delete{Feature}`. + +### Service Pattern + +```java +@Service +public class {Feature}Service { + @Autowired + CommonService commonService; + + public List> get{Feature}All(HttpServletRequest request, Map paramMap) { + SqlSession sqlSession = null; + try { + sqlSession = SqlMapConfig.getInstance().getSqlSession(); + + // Pagination + String countPerPage = CommonUtils.checkNull(request.getParameter("countPerPage"), Constants.ADMIN_COUNT_PER_PAGE+""); + paramMap.put("COUNT_PER_PAGE", Integer.parseInt(countPerPage)); + Map pageMap = (HashMap) sqlSession.selectOne("namespace.get{Feature}ListCnt", paramMap); + pageMap = (HashMap) CommonUtils.setPagingInfo(request, pageMap, countPerPage); + paramMap.put("PAGE_END", CommonUtils.checkNull(pageMap.get("PAGE_END"))); + paramMap.put("PAGE_START", CommonUtils.checkNull(pageMap.get("PAGE_START"))); + + resultList = (ArrayList) sqlSession.selectList("namespace.get{Feature}List", paramMap); + } catch(Exception e) { + e.printStackTrace(); + } finally { + sqlSession.close(); + } + return CommonUtils.toUpperCaseMapKey(resultList); + } +} +``` + +**Key points:** +- SqlSession obtained via `SqlMapConfig.getInstance().getSqlSession()` — must close in finally block. +- Result maps converted to uppercase keys via `CommonUtils.toUpperCaseMapKey()`. +- Pagination uses `PAGE_START`/`PAGE_END` parameters with count query + list query. + +### Mapper XML Pattern + +```xml + + + + + + + + + + + + ... + + + ... + +``` + +**Naming conventions:** +- File: `{feature}.xml` (camelCase) +- Namespace: `{module}.{feature}` or just `{feature}` for root-level +- SQL IDs: `get{Feature}List`, `get{Feature}ListCnt`, `get{Feature}`, `insert{Feature}`, `delete{Feature}` +- Parameter type casting: `#{param}::numeric`, `#{param}::integer` for PostgreSQL +- All `parameterType="map"`, `resultType="map"` (no typed beans for queries) + +### JSP Pattern + +**Every JSP starts with:** +```jsp +<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> +<%@ page import="com.pms.common.utils.*"%> +<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt"%> +<%@ page import="java.util.*" %> +<%@include file="/init.jsp" %> +``` + +**`init.jsp`** (common include) provides: +- Session check and PersonBean extraction (`connectUserId`, `connectUserDeptCode`, `connectUserName`, etc.) +- All CSS/JS includes (jQuery, Select2, SweetAlert2, Tabulator, common.js, datepicker) +- Loading overlay markup +- Page authorization AJAX call + +**List page structure:** +``` +Search form (#plmSearchZon) → Button group (.plm_btn_wrap) → Data table (.plm_table) → Pagination (.pdm_page) +``` + +**Form popup structure:** +``` +Hidden fields → Form table (#adminPopupForm) → Button group (저장/닫기) +``` + +**No Tiles framework** — layout is managed through `init.jsp` include and consistent HTML structure. + +### JavaScript/AJAX Patterns + +**Standard AJAX call:** +```javascript +$.ajax({ + url: "/module/action.do", + type: "POST", + data: {"param1": value1, "param2": value2}, + dataType: "json", + success: function(data) { + if (data == "SUCCESS") { + Swal.fire("완료되었습니다."); + fn_search(); + } + }, + error: function(jqxhr, status, error) { + Swal.fire(jqxhr.statusText + ", " + status + ", " + error); + } +}); +``` + +**Popup pattern:** +```javascript +window.open("", "formPopup", "width=1150, height=676"); +document.form1.action = "/module/formPopup.do"; +document.form1.target = "formPopup"; +document.form1.submit(); + +// From popup back to parent: +opener.fn_search(); +self.close(0); +``` + +**Key functions in `common.js` (WebContent/js/common.js, 3500+ lines):** + +| Category | Functions | +|----------|-----------| +| Validation | `fnc_valitate(formName)`, `fnc_validate(formName)`, `fnc_checkDataType(dataType, value)` | +| Null check | `fnc_isEmpty(val)`, `fnc_isNotEmpty(val)`, `fnc_checkNull(val)`, `fnc_checkNullDefaultValue(val, default)` | +| Date | `fnc_datepick()` (auto-init on ID containing "Date"), `fnc_monthpick()`, `fnc_getDate(delimiter)`, `fnc_dateCheck(from, to)` | +| Pagination | `fnc_goPrev(page)`, `fnc_goNext(page)`, `fnc_goPage(page)`, `fnc_goStart()`, `fnc_goEnd(maxPage)` | +| Code lookup | `fnc_getCodeList(codeId)`, `fnc_getCodeJsonStr(codeId)` | +| File ops | `fnc_deleteFile(fileObjId, callback)`, `fnc_downloadFile(fileObjId)` | +| Grid | `fnc_createGridAnchorTag(cell, params, onRendered)` | +| Form | `serializeObject()` (jQuery extension), custom `Map` class | + +**Common AJAX endpoints:** `/common/getCodeList.do`, `/common/getSupplyCodeList.do`, `/common/getClientMngList.do`, `/common/getProjectNoList.do`, `/common/getDeptList.do`, `/common/searchUserList.do`, `/common/getCarTypeList.do`, `/common/getOEMList.do`, `/common/getPartMngList.do`, `/common/getProductMgmtList.do` + +--- + +## Key Utility Classes + +### CommonUtils (`src/com/pms/common/utils/CommonUtils.java`) + +| Method | Purpose | +|--------|---------| +| `checkNull(Object obj)` | Returns empty string if null | +| `isBlank(Object)` / `isNotBlank(Object)` | Null/empty check | +| `isEmpty(Object)` / `isNotEmpty(Object)` | Null/empty check | +| `setPagingInfo(request, pageMap, countPerPage)` | Calculate pagination | +| `toUpperCaseMapKey(List/Map)` | Convert map keys to uppercase | +| `createObjId()` | Generate unique object ID | +| Date format constants | `DATE_FORMAT1="yyyy-MM-dd"`, `DATE_FORMAT2="yyyyMMdd"`, etc. | + +### Constants (`src/com/pms/common/utils/Constants.java`) + +| Constant | Value | Purpose | +|----------|-------|---------| +| `PERSON_BEAN` | `"PERSON_BEAN"` | Session key for logged-in user | +| `COUNT_PER_PAGE` | `25` | Default pagination size | +| `ADMIN_COUNT_PER_PAGE` | `25` | Admin pagination size | +| `FILE_STORAGE` | `"/data_storage"` | File storage root | +| `AJAX_RESPONSOR` | `"/common/ajaxResponsor"` | AJAX response view | +| `SYSTEM_CHARSET` | `"UTF-8"` | System encoding | +| `SUPER_ADMIN` | `"plm_admin"` | Super admin user ID | + +### SessionManager (`src/com/pms/common/utils/SessionManager.java`) + +- `setSessionManage(request, paramMap)` — Creates PersonBean from login query, stores in session +- `hasSession(session)` — Checks if PERSON_BEAN exists +- `destroy(session)` — Removes session attributes and invalidates + +### SqlMapConfig (`src/com/pms/common/utils/SqlMapConfig.java`) + +- Singleton pattern: `SqlMapConfig.getInstance().getSqlSession()` +- Reads `mybatisConf.xml` for DataSource and mapper configuration + +--- + +## Adding New Features + +To add a new screen/feature, follow the existing pattern: + +1. **Controller** — Create `{Feature}Controller.java` with `@Controller`, `@RequestMapping` for `*.do` URLs +2. **Service** — Create `{Feature}Service.java` with `@Service`, use `SqlMapConfig.getInstance().getSqlSession()` +3. **Mapper XML** — Create `{feature}.xml` with namespace matching, register in `mybatisConf.xml` +4. **JSP View** — Create in `WebContent/WEB-INF/view/{module}/`, start with `<%@include file="/init.jsp" %>` +5. **Wire** — `@Autowired` service into controller + +**Pagination:** Use count query (`get{Feature}ListCnt`) + list query (`get{Feature}List`) with `PAGE_START`/`PAGE_END` pattern. +**Session:** Get user via `request.getSession().getAttribute(Constants.PERSON_BEAN)` → `PersonBean`. +**Grids:** Use jqGrid or Tabulator depending on module. Sales modules increasingly use Tabulator. +**Alerts:** Use `Swal.fire()` (SweetAlert2) for user notifications. +**Popups:** Use `window.open()` with form target submission. + +## External Integrations + +11 API client classes in `src/com/pms/api/` handle ERP synchronization (parts, sales slips, warehouses), Amaranth system (approvals, users), and master data sync (accounts, customers, departments, employees). + +## No Automated Tests + +There is no test framework configured. Testing is manual via browser. diff --git a/WebContent/WEB-INF/classes/com/pms/mapper/productionplanning.xml b/WebContent/WEB-INF/classes/com/pms/mapper/productionplanning.xml index b800a75..108d047 100644 --- a/WebContent/WEB-INF/classes/com/pms/mapper/productionplanning.xml +++ b/WebContent/WEB-INF/classes/com/pms/mapper/productionplanning.xml @@ -3031,10 +3031,12 @@ FROM PROJECT_MGMT PM LEFT JOIN CONTRACT_MGMT CM ON PM.CONTRACT_OBJID = CM.OBJID - LEFT OUTER JOIN CONTRACT_ITEM CI ON PM.CONTRACT_OBJID = CI.CONTRACT_OBJID - AND PM.PART_OBJID = CI.PART_OBJID - -- CONTRACT_ITEM과 LEFT JOIN하여 품목별로 펼쳐서 보이기 - -- LEFT JOIN CONTRACT_ITEM CI ON CM.OBJID::VARCHAR = CI.CONTRACT_OBJID + LEFT OUTER JOIN CONTRACT_ITEM CI ON ( + CASE + WHEN PM.CONTRACT_ITEM_OBJID IS NOT NULL THEN CI.OBJID::VARCHAR = PM.CONTRACT_ITEM_OBJID + ELSE CI.CONTRACT_OBJID = PM.CONTRACT_OBJID AND CI.PART_OBJID = PM.PART_OBJID + END + ) AND CI.STATUS = 'ACTIVE' WHERE 1=1 AND PM.PROJECT_NO IS NOT NULL @@ -4597,8 +4599,12 @@ FROM PROJECT_MGMT PM LEFT JOIN CONTRACT_MGMT CM ON PM.CONTRACT_OBJID = CM.OBJID - LEFT OUTER JOIN CONTRACT_ITEM CI ON PM.CONTRACT_OBJID = CI.CONTRACT_OBJID - AND PM.PART_OBJID = CI.PART_OBJID + LEFT OUTER JOIN CONTRACT_ITEM CI ON ( + CASE + WHEN PM.CONTRACT_ITEM_OBJID IS NOT NULL THEN CI.OBJID::VARCHAR = PM.CONTRACT_ITEM_OBJID + ELSE CI.CONTRACT_OBJID = PM.CONTRACT_OBJID AND CI.PART_OBJID = PM.PART_OBJID + END + ) AND CI.STATUS = 'ACTIVE' LEFT OUTER JOIN PRODUCTION_PLAN PP ON PP.PROJECT_OBJID = PM.OBJID AND PP.STATUS = 'active' @@ -4731,8 +4737,13 @@ COALESCE(CI.PART_NAME, PM.PART_NAME, '') AS PART_NAME FROM PROJECT_MGMT PM LEFT JOIN CONTRACT_MGMT CM ON PM.CONTRACT_OBJID = CM.OBJID - LEFT OUTER JOIN CONTRACT_ITEM CI ON PM.CONTRACT_OBJID = CI.CONTRACT_OBJID - AND PM.PART_OBJID = CI.PART_OBJID AND CI.STATUS = 'ACTIVE' + LEFT OUTER JOIN CONTRACT_ITEM CI ON ( + CASE + WHEN PM.CONTRACT_ITEM_OBJID IS NOT NULL THEN CI.OBJID::VARCHAR = PM.CONTRACT_ITEM_OBJID + ELSE CI.CONTRACT_OBJID = PM.CONTRACT_OBJID AND CI.PART_OBJID = PM.PART_OBJID + END + ) + AND CI.STATUS = 'ACTIVE' WHERE PM.OBJID::VARCHAR = #{projectObjid}::VARCHAR @@ -4935,7 +4946,13 @@ FROM PROJECT_MGMT PM LEFT JOIN CONTRACT_MGMT CM ON PM.CONTRACT_OBJID = CM.OBJID - LEFT OUTER JOIN CONTRACT_ITEM CI ON PM.CONTRACT_OBJID = CI.CONTRACT_OBJID + LEFT OUTER JOIN CONTRACT_ITEM CI ON ( + CASE + WHEN PM.CONTRACT_ITEM_OBJID IS NOT NULL THEN CI.OBJID::VARCHAR = PM.CONTRACT_ITEM_OBJID + ELSE CI.CONTRACT_OBJID = PM.CONTRACT_OBJID AND CI.PART_OBJID = PM.PART_OBJID + END + ) + AND CI.STATUS = 'ACTIVE' AND PM.PART_OBJID = CI.PART_OBJID AND CI.STATUS = 'ACTIVE' LEFT OUTER JOIN PRODUCTION_PLAN PP ON PP.PROJECT_OBJID = PM.OBJID @@ -5008,8 +5025,12 @@ ), '') AS SERIAL_NO FROM PROJECT_MGMT PM LEFT JOIN CONTRACT_MGMT CM ON PM.CONTRACT_OBJID = CM.OBJID - LEFT JOIN CONTRACT_ITEM CI ON CI.CONTRACT_OBJID = PM.CONTRACT_OBJID - AND CI.PART_OBJID = PM.PART_OBJID + LEFT JOIN CONTRACT_ITEM CI ON ( + CASE + WHEN PM.CONTRACT_ITEM_OBJID IS NOT NULL THEN CI.OBJID::VARCHAR = PM.CONTRACT_ITEM_OBJID + ELSE CI.CONTRACT_OBJID = PM.CONTRACT_OBJID AND CI.PART_OBJID = PM.PART_OBJID + END + ) AND CI.STATUS = 'ACTIVE' WHERE PM.OBJID::VARCHAR = #{projectObjid} @@ -5051,8 +5072,12 @@ COALESCE( (SELECT STRING_AGG(CIS.SERIAL_NO, ', ' ORDER BY CIS.SERIAL_NO) FROM PROJECT_MGMT PM - JOIN CONTRACT_ITEM CI ON CI.CONTRACT_OBJID = PM.CONTRACT_OBJID - AND CI.PART_OBJID = PM.PART_OBJID AND CI.STATUS = 'ACTIVE' + JOIN CONTRACT_ITEM CI ON ( + CASE + WHEN PM.CONTRACT_ITEM_OBJID IS NOT NULL THEN CI.OBJID::VARCHAR = PM.CONTRACT_ITEM_OBJID + ELSE CI.CONTRACT_OBJID = PM.CONTRACT_OBJID AND CI.PART_OBJID = PM.PART_OBJID + END + ) AND CI.STATUS = 'ACTIVE' JOIN CONTRACT_ITEM_SERIAL CIS ON CIS.ITEM_OBJID = CI.OBJID AND UPPER(CIS.STATUS) = 'ACTIVE' AND CIS.SERIAL_NO IS NOT NULL WHERE PM.OBJID::VARCHAR = PP.PROJECT_OBJID), @@ -5346,7 +5371,13 @@ FROM MBOM_HISTORY MH INNER JOIN MBOM_HEADER MHD ON MH.MBOM_HEADER_OBJID = MHD.OBJID INNER JOIN PROJECT_MGMT PM ON MHD.PROJECT_OBJID = PM.OBJID::VARCHAR - LEFT OUTER JOIN CONTRACT_ITEM CI ON PM.CONTRACT_OBJID = CI.CONTRACT_OBJID + LEFT OUTER JOIN CONTRACT_ITEM CI ON ( + CASE + WHEN PM.CONTRACT_ITEM_OBJID IS NOT NULL THEN CI.OBJID::VARCHAR = PM.CONTRACT_ITEM_OBJID + ELSE CI.CONTRACT_OBJID = PM.CONTRACT_OBJID AND CI.PART_OBJID = PM.PART_OBJID + END + ) + AND CI.STATUS = 'ACTIVE' LEFT JOIN CONTRACT_MGMT CM ON PM.CONTRACT_OBJID = CM.OBJID WHERE 1=1 @@ -5394,5 +5425,60 @@ INNER JOIN PROJECT_MGMT PM ON MHD.PROJECT_OBJID = PM.OBJID::VARCHAR WHERE MH.OBJID::VARCHAR = #{historyObjId} - + + + + + + + + diff --git a/WebContent/WEB-INF/classes/com/pms/mapper/project.xml b/WebContent/WEB-INF/classes/com/pms/mapper/project.xml index 535257b..bac22cc 100644 --- a/WebContent/WEB-INF/classes/com/pms/mapper/project.xml +++ b/WebContent/WEB-INF/classes/com/pms/mapper/project.xml @@ -3933,12 +3933,13 @@ AND S.SERIAL_NO IS NOT NULL) AS SERIAL_NO -- 요청납기: CONTRACT_ITEM 우선, 없으면 PROJECT_MGMT.DUE_DATE, 없으면 CONTRACT_MGMT.due_date ,COALESCE( - (SELECT CI.DUE_DATE - FROM CONTRACT_ITEM CI - WHERE CI.CONTRACT_OBJID = T.CONTRACT_OBJID - AND CI.PART_OBJID = T.PART_OBJID - AND CI.STATUS = 'ACTIVE'), - T.DUE_DATE, + (SELECT CI.DUE_DATE + FROM CONTRACT_ITEM CI + WHERE CI.CONTRACT_OBJID = T.CONTRACT_OBJID + AND CI.PART_OBJID = T.PART_OBJID + AND CI.STATUS = 'ACTIVE' + ORDER BY CI.OBJID DESC LIMIT 1), + T.DUE_DATE, (SELECT CM.due_date FROM CONTRACT_MGMT CM WHERE CM.OBJID = T.CONTRACT_OBJID) ) AS REQ_DEL_DATE -- 영업관리_주문서관리_수주등록 @@ -3951,9 +3952,10 @@ ,PRODUCTION_TEAM_3 -- 출하일: sales_registration 테이블에서 가져오기 (영업관리_판매관리와 동일) ,COALESCE( - (SELECT TO_CHAR(SR.shipping_date, 'YYYY-MM-DD') - FROM sales_registration SR - WHERE SR.project_no = T.PROJECT_NO), + (SELECT TO_CHAR(SR.shipping_date, 'YYYY-MM-DD') + FROM sales_registration SR + WHERE SR.project_no = T.PROJECT_NO + ORDER BY SR.sale_no DESC LIMIT 1), '' ) AS SHIPMENT_DATE ,(((SELECT SUM(COALESCE(DESIGN_RATE,'0')::INTEGER) / COUNT(1) FROM PMS_WBS_TASK AS O WHERE O.CONTRACT_OBJID = T.OBJID) @@ -7468,6 +7470,7 @@ SELECT ,PART_NO ,PART_NAME ,QUANTITY + ,CONTRACT_ITEM_OBJID ) ( @@ -7609,12 +7612,21 @@ SELECT ,#{part_no} ,#{part_name} ,#{quantity} + ,#{contract_item_objid} FROM CONTRACT_MGMT WHERE OBJID=#{objId} ) - - + + + + UPDATE PROJECT_MGMT + SET CONTRACT_ITEM_OBJID = #{contract_item_objid} + WHERE CONTRACT_OBJID = #{contract_objid} + AND PART_OBJID = #{part_objid} + AND CONTRACT_ITEM_OBJID IS NULL + + INSERT INTO diff --git a/WebContent/WEB-INF/classes/com/pms/salesmgmt/mapper/contractMgmt.xml b/WebContent/WEB-INF/classes/com/pms/salesmgmt/mapper/contractMgmt.xml index 7153949..d680e10 100644 --- a/WebContent/WEB-INF/classes/com/pms/salesmgmt/mapper/contractMgmt.xml +++ b/WebContent/WEB-INF/classes/com/pms/salesmgmt/mapper/contractMgmt.xml @@ -1560,10 +1560,12 @@ ,CONTRACT_DEL_DATE = #{contract_del_date} ,CONTRACT_COMPANY = #{contract_company} ,CONTRACT_DATE = #{contract_date} - ,PO_NO = #{po_no} - ,MANUFACTURE_PLANT = #{manufacture_plant} - ,CONTRACT_RESULT = #{contract_result} - ,PROJECT_NAME = #{project_name} + ,PO_NO = #{po_no} + ,MANUFACTURE_PLANT = #{manufacture_plant} + + ,CONTRACT_RESULT = #{contract_result} + + ,PROJECT_NAME = #{project_name} ,SPEC_USER_ID = #{spec_user_id} ,SPEC_PLAN_DATE = #{spec_plan_date} ,SPEC_COMP_DATE = #{spec_comp_date} @@ -1730,10 +1732,12 @@ ,CONTRACT_DEL_DATE = #{contract_del_date} ,CONTRACT_COMPANY = #{contract_company} ,CONTRACT_DATE = #{contract_date} - ,PO_NO = #{po_no} - ,MANUFACTURE_PLANT = #{manufacture_plant} - ,CONTRACT_RESULT = #{contract_result} - ,PROJECT_NAME = #{project_name} + ,PO_NO = #{po_no} + ,MANUFACTURE_PLANT = #{manufacture_plant} + + ,CONTRACT_RESULT = #{contract_result} + + ,PROJECT_NAME = #{project_name} ,AREA_CD = #{area_cd} --> @@ -4991,7 +4995,9 @@ WHERE UPDATE CONTRACT_MGMT SET + CONTRACT_RESULT = #{contract_result}, + PO_NO = #{po_no}, ORDER_DATE = #{order_date}, CONTRACT_CURRENCY = #{contract_currency}, @@ -5083,7 +5089,9 @@ WHERE PAID_TYPE = #{paid_type}, RECEIPT_DATE = #{receipt_date}, REQ_DEL_DATE = #{req_del_date}, + CONTRACT_RESULT = #{contract_result}, + PO_NO = #{po_no}, ORDER_DATE = #{order_date}, CONTRACT_CURRENCY = #{contract_currency}, @@ -5629,6 +5637,67 @@ WHERE STATUS = 'ACTIVE' + + + INSERT INTO CONTRACT_ITEM ( + OBJID, + CONTRACT_OBJID, + SEQ, + PART_OBJID, + PART_NO, + PART_NAME, + QUANTITY, + DUE_DATE, + CUSTOMER_REQUEST, + RETURN_REASON, + REGDATE, + WRITER, + STATUS, + ORDER_QUANTITY, + ORDER_UNIT_PRICE, + ORDER_SUPPLY_PRICE, + ORDER_VAT, + ORDER_TOTAL_AMOUNT + ) VALUES ( + #{objId}, + #{contractObjId}, + #{seq}, + #{partObjId}, + #{partNo}, + #{partName}, + CASE WHEN #{quantity} = '' OR #{quantity} IS NULL THEN NULL ELSE #{quantity}::INTEGER END, + #{dueDate}, + #{customerRequest}, + #{returnReason}, + NOW(), + #{writer}, + 'ACTIVE', + #{orderQuantity}, + #{orderUnitPrice}, + #{orderSupplyPrice}, + #{orderVat}, + #{orderTotalAmount} + ) + ON CONFLICT (OBJID) DO UPDATE + SET + SEQ = #{seq}, + PART_OBJID = #{partObjId}, + PART_NO = #{partNo}, + PART_NAME = #{partName}, + QUANTITY = CASE WHEN #{quantity} = '' OR #{quantity} IS NULL THEN NULL ELSE #{quantity}::INTEGER END, + DUE_DATE = #{dueDate}, + CUSTOMER_REQUEST = #{customerRequest}, + RETURN_REASON = #{returnReason}, + CHGDATE = NOW(), + CHG_USER_ID = #{writer}, + STATUS = 'ACTIVE', + ORDER_QUANTITY = #{orderQuantity}, + ORDER_UNIT_PRICE = #{orderUnitPrice}, + ORDER_SUPPLY_PRICE = #{orderSupplyPrice}, + ORDER_VAT = #{orderVat}, + ORDER_TOTAL_AMOUNT = #{orderTotalAmount} + + INSERT INTO CONTRACT_ITEM_SERIAL ( 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 b66e833..e9b12f2 100644 --- a/WebContent/WEB-INF/classes/com/pms/salesmgmt/mapper/salesNcollectMgmt.xml +++ b/WebContent/WEB-INF/classes/com/pms/salesmgmt/mapper/salesNcollectMgmt.xml @@ -844,14 +844,9 @@ FROM CONTRACT_MGMT CM WHERE CM.OBJID = T.CONTRACT_OBJID) AS PAYMENT_TYPE, T.PART_NO AS PRODUCT_NO, T.PART_NAME AS PRODUCT_NAME, - -- S/N: CONTRACT_ITEM_SERIAL(마스터) 우선, 없으면 판매등록 텍스트 fallback (그리드 요약용) + -- S/N: CONTRACT_ITEM_SERIAL(마스터) 우선, 없으면 판매등록 텍스트 fallback (콤마 구분 전체 목록) COALESCE( - (SELECT - CASE - WHEN COUNT(*) = 0 THEN NULL - WHEN COUNT(*) = 1 THEN MIN(CIS.SERIAL_NO) - ELSE MIN(CIS.SERIAL_NO) || ' 외 ' || (COUNT(*) - 1)::TEXT || '건' - END + (SELECT STRING_AGG(CIS.SERIAL_NO, ',' ORDER BY CIS.SEQ) FROM CONTRACT_ITEM CI LEFT JOIN CONTRACT_ITEM_SERIAL CIS ON CI.OBJID = CIS.ITEM_OBJID AND UPPER(CIS.STATUS) = 'ACTIVE' WHERE CI.CONTRACT_OBJID = T.CONTRACT_OBJID @@ -860,23 +855,34 @@ AND CIS.SERIAL_NO IS NOT NULL), SR.serial_no ) AS SERIAL_NO, + -- 분할S/N: shipment_log에서 해당 프로젝트의 모든 분할S/N 집계 + COALESCE( + (SELECT STRING_AGG(SL_SN.split_serial_no, ',' ORDER BY SL_SN.log_id) + FROM shipment_log SL_SN + WHERE SL_SN.target_objid = T.PROJECT_NO + AND SL_SN.split_serial_no IS NOT NULL + AND SL_SN.split_serial_no != ''), + '' + ) AS SPLIT_SERIAL_NO, COALESCE(NULLIF(REPLACE(T.QUANTITY, ',', ''), '')::numeric, 0) AS ORDER_QUANTITY, -- 요청납기: CONTRACT_ITEM 우선, 없으면 PROJECT_MGMT, 없으면 CONTRACT_MGMT COALESCE( - (SELECT CI.DUE_DATE - FROM CONTRACT_ITEM CI - WHERE CI.CONTRACT_OBJID = T.CONTRACT_OBJID - AND CI.PART_OBJID = T.PART_OBJID - AND CI.STATUS = 'ACTIVE'), - T.DUE_DATE, + (SELECT CI.DUE_DATE + FROM CONTRACT_ITEM CI + WHERE CI.CONTRACT_OBJID = T.CONTRACT_OBJID + AND CI.PART_OBJID = T.PART_OBJID + AND CI.STATUS = 'ACTIVE' + ORDER BY CI.OBJID DESC LIMIT 1), + T.DUE_DATE, (SELECT CM.due_date FROM CONTRACT_MGMT CM WHERE CM.OBJID = T.CONTRACT_OBJID) ) AS REQUEST_DATE, -- 고객요청사항: CONTRACT_ITEM에서만 가져옴 (견적관리와 완전히 동일) - (SELECT CI.CUSTOMER_REQUEST - FROM CONTRACT_ITEM CI - WHERE CI.CONTRACT_OBJID = T.CONTRACT_OBJID - AND CI.PART_OBJID = T.PART_OBJID - AND CI.STATUS = 'ACTIVE') AS CUSTOMER_REQUEST, + (SELECT CI.CUSTOMER_REQUEST + FROM CONTRACT_ITEM CI + WHERE CI.CONTRACT_OBJID = T.CONTRACT_OBJID + AND CI.PART_OBJID = T.PART_OBJID + AND CI.STATUS = 'ACTIVE' + ORDER BY CI.OBJID DESC LIMIT 1) AS CUSTOMER_REQUEST, CODE_NAME(T.CONTRACT_RESULT) AS ORDER_STATUS, (SELECT CM.PO_NO FROM CONTRACT_MGMT CM WHERE CM.OBJID = T.CONTRACT_OBJID) AS PO_NO, COALESCE(T.CONTRACT_DATE, (SELECT CM.order_date FROM CONTRACT_MGMT CM WHERE CM.OBJID = T.CONTRACT_OBJID)) AS ORDER_DATE, @@ -1572,20 +1578,22 @@ ORDER BY T.REGDATE DESC, T.PROJECT_NO DESC COALESCE(NULLIF(REPLACE(T.QUANTITY, ',', ''), '')::numeric, 0) AS ORDER_QUANTITY, -- 요청납기: CONTRACT_ITEM 우선, 없으면 PROJECT_MGMT, 없으면 CONTRACT_MGMT COALESCE( - (SELECT CI.DUE_DATE - FROM CONTRACT_ITEM CI - WHERE CI.CONTRACT_OBJID = T.CONTRACT_OBJID - AND CI.PART_OBJID = T.PART_OBJID - AND CI.STATUS = 'ACTIVE'), - T.DUE_DATE, + (SELECT CI.DUE_DATE + FROM CONTRACT_ITEM CI + WHERE CI.CONTRACT_OBJID = T.CONTRACT_OBJID + AND CI.PART_OBJID = T.PART_OBJID + AND CI.STATUS = 'ACTIVE' + ORDER BY CI.OBJID DESC LIMIT 1), + T.DUE_DATE, (SELECT CM.due_date FROM CONTRACT_MGMT CM WHERE CM.OBJID = T.CONTRACT_OBJID) ) AS REQUEST_DATE, -- 고객요청사항: CONTRACT_ITEM에서만 가져옴 (견적관리와 완전히 동일) - (SELECT CI.CUSTOMER_REQUEST - FROM CONTRACT_ITEM CI - WHERE CI.CONTRACT_OBJID = T.CONTRACT_OBJID - AND CI.PART_OBJID = T.PART_OBJID - AND CI.STATUS = 'ACTIVE') AS CUSTOMER_REQUEST, + (SELECT CI.CUSTOMER_REQUEST + FROM CONTRACT_ITEM CI + WHERE CI.CONTRACT_OBJID = T.CONTRACT_OBJID + AND CI.PART_OBJID = T.PART_OBJID + AND CI.STATUS = 'ACTIVE' + ORDER BY CI.OBJID DESC LIMIT 1) AS CUSTOMER_REQUEST, CODE_NAME(T.CONTRACT_RESULT) AS ORDER_STATUS, T.PO_NO, COALESCE(T.CONTRACT_DATE, (SELECT CM.order_date FROM CONTRACT_MGMT CM WHERE CM.OBJID = T.CONTRACT_OBJID)) AS ORDER_DATE, @@ -1898,7 +1906,7 @@ ORDER BY T.REGDATE DESC, T.PROJECT_NO DESC remaining_quantity, shipping_status, shipping_date, shipping_method, sales_unit_price, sales_supply_price, sales_vat, sales_total_amount, sales_currency, sales_exchange_rate, manager_user_id, incoterms, - serial_no, parent_sale_no, reg_user_id + serial_no, parent_sale_no, reg_user_id, split_serial_no ) VALUES ( #{targetObjid}, 'SPLIT_SHIPMENT', '분할 출하', #{salesQuantity}::integer, #{originalQuantity}::integer, #{remainingQuantity}::integer, @@ -1912,7 +1920,7 @@ ORDER BY T.REGDATE DESC, T.PROJECT_NO DESC #{shippingMethod}, #{salesUnitPrice}::numeric, #{salesSupplyPrice}::numeric, #{salesVat}::numeric, #{salesTotalAmount}::numeric, #{salesCurrency}, #{salesExchangeRate}::numeric, #{managerUserId}, #{incoterms}, #{serialNo}, - #{parentSaleNo}::integer, #{cretEmpNo} + #{parentSaleNo}::integer, #{cretEmpNo}, #{splitSerialNo} ) @@ -1990,6 +1998,7 @@ ORDER BY T.REGDATE DESC, T.PROJECT_NO DESC WHERE PM.PROJECT_NO = SL.target_objid AND CI.STATUS = 'ACTIVE' ), '-') AS serial_no, SL.target_objid AS project_no, + COALESCE(SL.split_serial_no, '-') AS split_serial_no, TO_CHAR(SL.reg_date, 'YYYY-MM-DD HH24:MI:SS') AS reg_date FROM shipment_log SL WHERE SL.target_objid = #{projectNo} @@ -2021,6 +2030,7 @@ ORDER BY T.REGDATE DESC, T.PROJECT_NO DESC COALESCE(SL.sales_exchange_rate, 0) AS SALES_EXCHANGE_RATE, COALESCE(SL.manager_user_id, '') AS MANAGER, COALESCE(SL.incoterms, '') AS INCOTERMS, + COALESCE(SL.split_serial_no, '') AS SPLIT_SERIAL_NO, COALESCE(SL.original_quantity, 0) AS ORDER_QUANTITY, COALESCE(SL.remaining_quantity, 0) AS REMAINING_QUANTITY FROM shipment_log SL @@ -2066,6 +2076,9 @@ ORDER BY T.REGDATE DESC, T.PROJECT_NO DESC , incoterms = #{incoterms} + + , split_serial_no = #{splitSerialNo} + WHERE log_id = #{logId}::integer @@ -2402,6 +2415,7 @@ ORDER BY T.REGDATE DESC, T.PROJECT_NO DESC WHERE CI.CONTRACT_OBJID = T.CONTRACT_OBJID AND CI.PART_OBJID = T.PART_OBJID AND CI.STATUS = 'ACTIVE' ), '') AS SERIAL_NO, + COALESCE(SL.split_serial_no, '') AS SPLIT_SERIAL_NO, COALESCE(NULLIF(REPLACE(T.QUANTITY, ',', ''), '')::numeric, 0) AS ORDER_QUANTITY, (SELECT CM.PO_NO FROM CONTRACT_MGMT CM WHERE CM.OBJID = T.CONTRACT_OBJID) AS PO_NO, COALESCE(T.CONTRACT_DATE, (SELECT CM.order_date FROM CONTRACT_MGMT CM WHERE CM.OBJID = T.CONTRACT_OBJID)) AS ORDER_DATE, @@ -2738,6 +2752,22 @@ ORDER BY T.REGDATE DESC, T.PROJECT_NO DESC WHERE PROJECT_NO = #{projectNo} + + + diff --git a/WebContent/WEB-INF/view/contractMgmt/estimateAndOrderRegistFormPopup.jsp b/WebContent/WEB-INF/view/contractMgmt/estimateAndOrderRegistFormPopup.jsp index c927c8d..9e0b650 100644 --- a/WebContent/WEB-INF/view/contractMgmt/estimateAndOrderRegistFormPopup.jsp +++ b/WebContent/WEB-INF/view/contractMgmt/estimateAndOrderRegistFormPopup.jsp @@ -36,7 +36,65 @@ #serial_no:-ms-input-placeholder { color: #999 !important; } - + + /* ===== 팝업 폼 전체 너비 확장 ===== */ + section.business_popup_min_width { + padding: 0 !important; + margin: 0 auto !important; + width: calc(100% - 16px) !important; + } + #EntirePopupFormWrap { + width: 100% !important; + margin: 5px 0 0 0 !important; + } + + /* ===== 품목 그리드 컬럼 리사이즈 ===== */ + #EntirePopupFormWrap > table { + width: 100% !important; + } + #itemListTable { + table-layout: fixed; + width: 100%; + } + #itemListTable th { + position: relative; + user-select: none; + } + #itemListTable th .col-resizer { + position: absolute; + right: -4.5px; + top: 0; + width: 7px; + height: 100%; + cursor: col-resize; + z-index: 10; + } + #itemListTable th .col-resizer:hover { + background: rgba(0, 120, 215, 0.3); + } + + /* ===== Select2 드래그 복사 지원 ===== */ + .select2-container .select2-selection__rendered { + user-select: text !important; + -webkit-user-select: text !important; + cursor: text; + padding-right: 36px !important; + } + /* x(초기화) 버튼을 드롭다운 화살표 왼쪽에 배치 */ + #itemListTable .select2-selection__clear { + position: absolute !important; + right: 20px; + top: 50%; + transform: translateY(-50%); + font-size: 14px; + color: #999; + cursor: pointer; + user-select: none; + } + #itemListTable .select2-selection--single { + position: relative; + } +