Merge pull request 'V20260210' (#186) from V20260210 into main
Reviewed-on: #186
This commit was merged in pull request #186.
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -35,7 +35,8 @@ Thumbs.db
|
||||
.cursor/
|
||||
|
||||
# Claude Code
|
||||
CLAUDE.md
|
||||
|
||||
.claude/
|
||||
.playwright-mcp/
|
||||
.omc/
|
||||
.mcp.json
|
||||
|
||||
343
CLAUDE.md
Normal file
343
CLAUDE.md
Normal file
@@ -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<String, Object> paramMap) {
|
||||
// ... returns JSP path string
|
||||
}
|
||||
|
||||
// Form popup
|
||||
@RequestMapping(value = "/{module}/{feature}FormPopup.do", method = RequestMethod.GET)
|
||||
public String {feature}FormPopup(HttpServletRequest request, @RequestParam Map<String, Object> paramMap) { ... }
|
||||
|
||||
// Save (AJAX)
|
||||
@ResponseBody
|
||||
@RequestMapping(value = "/{module}/save{Feature}.do", method = RequestMethod.POST)
|
||||
public Map<String, Object> save{Feature}(HttpServletRequest request, @RequestParam Map<String, Object> paramMap) { ... }
|
||||
|
||||
// Delete (AJAX)
|
||||
@ResponseBody
|
||||
@RequestMapping(value = "/{module}/delete{Feature}.do", method = RequestMethod.POST)
|
||||
public Map<String, Object> delete{Feature}(HttpServletRequest request, @RequestParam Map<String, Object> 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<Map<String,Object>> get{Feature}All(HttpServletRequest request, Map<String, Object> 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
|
||||
<mapper namespace="{module}.{feature}">
|
||||
<!-- Count for pagination -->
|
||||
<select id="get{Feature}ListCnt" parameterType="map" resultType="map">
|
||||
SELECT COUNT(*) AS TOTAL_COUNT FROM TABLE_NAME WHERE 1=1
|
||||
<if test='condField != null and condField != ""'>AND FIELD = #{condField}</if>
|
||||
</select>
|
||||
|
||||
<!-- List with pagination -->
|
||||
<select id="get{Feature}List" parameterType="map" resultType="map">
|
||||
SELECT * FROM (
|
||||
SELECT ROW_NUMBER() OVER (ORDER BY CRET_DATE) AS RNUM, ...
|
||||
FROM TABLE_NAME WHERE 1=1
|
||||
<if test='condField != null and condField != ""'>AND FIELD = #{condField}</if>
|
||||
) T WHERE 1=1
|
||||
<if test="PAGE_END != null and PAGE_END != ''">
|
||||
<![CDATA[ AND RNUM <= #{PAGE_END}::integer ]]>
|
||||
</if>
|
||||
<if test="PAGE_START != null and PAGE_START != ''">
|
||||
<![CDATA[ AND RNUM >= #{PAGE_START}::integer ]]>
|
||||
</if>
|
||||
</select>
|
||||
|
||||
<!-- Single record -->
|
||||
<select id="get{Feature}" parameterType="map" resultType="map"> ... </select>
|
||||
|
||||
<!-- Insert/Update -->
|
||||
<insert id="insert{Feature}" parameterType="map"> ... </insert>
|
||||
|
||||
<!-- Delete -->
|
||||
<delete id="delete{Feature}" parameterType="map"> ... </delete>
|
||||
</mapper>
|
||||
```
|
||||
|
||||
**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.
|
||||
@@ -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
|
||||
</select>
|
||||
|
||||
@@ -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
|
||||
<if test="search_category_cd != null and search_category_cd != ''">
|
||||
@@ -5394,5 +5425,60 @@
|
||||
INNER JOIN PROJECT_MGMT PM ON MHD.PROJECT_OBJID = PM.OBJID::VARCHAR
|
||||
WHERE MH.OBJID::VARCHAR = #{historyObjId}
|
||||
</select>
|
||||
|
||||
|
||||
<!-- M-BOM → ERP BOM 동기화용: 모품목-자품목 관계 추출 -->
|
||||
<select id="selectMbomBomRelationsForErp" parameterType="map" resultType="map">
|
||||
SELECT
|
||||
ITEM_PARENT_CD,
|
||||
ITEM_CHILD_CD,
|
||||
JUST_QT,
|
||||
SUPPLY_TYPE,
|
||||
ROW_NUMBER() OVER (PARTITION BY ITEM_PARENT_CD ORDER BY SEQ, CHILD_OBJID) AS BOM_SQ
|
||||
FROM (
|
||||
SELECT
|
||||
CASE
|
||||
WHEN MD.PARENT_OBJID IS NULL OR MD.PARENT_OBJID = ''
|
||||
THEN MH.PART_NO
|
||||
ELSE PARENT_MD.PART_NO
|
||||
END AS ITEM_PARENT_CD,
|
||||
MD.PART_NO AS ITEM_CHILD_CD,
|
||||
COALESCE(MD.QTY, 1) AS JUST_QT,
|
||||
COALESCE(MD.SUPPLY_TYPE, '') AS SUPPLY_TYPE,
|
||||
MD.SEQ,
|
||||
MD.CHILD_OBJID
|
||||
FROM MBOM_DETAIL MD
|
||||
JOIN MBOM_HEADER MH ON MH.OBJID::VARCHAR = MD.MBOM_HEADER_OBJID::VARCHAR
|
||||
LEFT JOIN MBOM_DETAIL PARENT_MD
|
||||
ON MD.PARENT_OBJID = PARENT_MD.CHILD_OBJID
|
||||
AND MD.MBOM_HEADER_OBJID = PARENT_MD.MBOM_HEADER_OBJID
|
||||
AND PARENT_MD.STATUS = 'ACTIVE'
|
||||
WHERE MD.MBOM_HEADER_OBJID = #{mbomHeaderObjid}
|
||||
AND MD.STATUS = 'ACTIVE'
|
||||
) SUB
|
||||
ORDER BY ITEM_PARENT_CD, BOM_SQ
|
||||
</select>
|
||||
|
||||
<!-- M-BOM → ERP BOM 동기화용: DISTINCT 모품목 목록 -->
|
||||
<select id="selectDistinctParentItemsForErp" parameterType="map" resultType="string">
|
||||
SELECT DISTINCT ITEM_PARENT_CD
|
||||
FROM (
|
||||
SELECT
|
||||
CASE
|
||||
WHEN MD.PARENT_OBJID IS NULL OR MD.PARENT_OBJID = ''
|
||||
THEN MH.PART_NO
|
||||
ELSE PARENT_MD.PART_NO
|
||||
END AS ITEM_PARENT_CD
|
||||
FROM MBOM_DETAIL MD
|
||||
JOIN MBOM_HEADER MH ON MH.OBJID::VARCHAR = MD.MBOM_HEADER_OBJID::VARCHAR
|
||||
LEFT JOIN MBOM_DETAIL PARENT_MD
|
||||
ON MD.PARENT_OBJID = PARENT_MD.CHILD_OBJID
|
||||
AND MD.MBOM_HEADER_OBJID = PARENT_MD.MBOM_HEADER_OBJID
|
||||
AND PARENT_MD.STATUS = 'ACTIVE'
|
||||
WHERE MD.MBOM_HEADER_OBJID = #{mbomHeaderObjid}
|
||||
AND MD.STATUS = 'ACTIVE'
|
||||
) SUB
|
||||
WHERE ITEM_PARENT_CD IS NOT NULL AND ITEM_PARENT_CD != ''
|
||||
</select>
|
||||
|
||||
|
||||
</mapper>
|
||||
|
||||
@@ -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}
|
||||
)
|
||||
</insert>
|
||||
|
||||
|
||||
|
||||
<!-- 기존 프로젝트에 CONTRACT_ITEM_OBJID 매핑 업데이트 -->
|
||||
<update id="updateProjectContractItemObjid" parameterType="map">
|
||||
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
|
||||
</update>
|
||||
|
||||
<!-- //계약정보를 받아 프로젝트 TASK 최초저장 -->
|
||||
<insert id="insertProjectTask" parameterType="map">
|
||||
INSERT INTO
|
||||
|
||||
@@ -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}
|
||||
<if test="contract_result != null and contract_result != ''">
|
||||
,CONTRACT_RESULT = #{contract_result}
|
||||
</if>
|
||||
,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}
|
||||
<if test="contract_result != null and contract_result != ''">
|
||||
,CONTRACT_RESULT = #{contract_result}
|
||||
</if>
|
||||
,PROJECT_NAME = #{project_name}
|
||||
,AREA_CD = #{area_cd} -->
|
||||
</update>
|
||||
|
||||
@@ -4991,7 +4995,9 @@ WHERE
|
||||
<update id="updateOrderInfo" parameterType="map">
|
||||
UPDATE CONTRACT_MGMT
|
||||
SET
|
||||
<if test="contract_result != null and contract_result != ''">
|
||||
CONTRACT_RESULT = #{contract_result},
|
||||
</if>
|
||||
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},
|
||||
<if test="contract_result != null and contract_result != ''">
|
||||
CONTRACT_RESULT = #{contract_result},
|
||||
</if>
|
||||
PO_NO = #{po_no},
|
||||
ORDER_DATE = #{order_date},
|
||||
CONTRACT_CURRENCY = #{contract_currency},
|
||||
@@ -5629,6 +5637,67 @@ WHERE
|
||||
STATUS = 'ACTIVE'
|
||||
</insert>
|
||||
|
||||
<!-- 품목 UPSERT (수주 정보 포함 - 통합등록용) -->
|
||||
<insert id="upsertContractItemWithOrder" parameterType="map">
|
||||
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>
|
||||
|
||||
<!-- S/N UPSERT (INSERT or UPDATE) -->
|
||||
<insert id="upsertContractItemSerial" parameterType="map">
|
||||
INSERT INTO CONTRACT_ITEM_SERIAL (
|
||||
|
||||
@@ -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}
|
||||
)
|
||||
</insert>
|
||||
|
||||
@@ -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
|
||||
<if test="incoterms != null and incoterms != ''">
|
||||
, incoterms = #{incoterms}
|
||||
</if>
|
||||
<if test="splitSerialNo != null and splitSerialNo != ''">
|
||||
, split_serial_no = #{splitSerialNo}
|
||||
</if>
|
||||
WHERE log_id = #{logId}::integer
|
||||
</update>
|
||||
|
||||
@@ -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}
|
||||
</update>
|
||||
|
||||
<!-- 프로젝트의 이미 사용된 분할S/N 목록 조회 -->
|
||||
<select id="getUsedSplitSerialNos" parameterType="map" resultType="map">
|
||||
/* salesNcollectMgmt.getUsedSplitSerialNos - 이미 사용된 분할S/N 조회 */
|
||||
SELECT
|
||||
SL.log_id,
|
||||
COALESCE(SL.split_serial_no, '') AS split_serial_no
|
||||
FROM shipment_log SL
|
||||
WHERE SL.target_objid = #{projectNo}
|
||||
<if test="excludeLogId != null and excludeLogId != ''">
|
||||
AND SL.log_id != #{excludeLogId}::integer
|
||||
</if>
|
||||
AND SL.split_serial_no IS NOT NULL
|
||||
AND SL.split_serial_no != ''
|
||||
ORDER BY SL.log_id
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
</style>
|
||||
<script type="text/javascript">
|
||||
// 품목 관리 변수
|
||||
@@ -50,9 +108,103 @@
|
||||
var hasProject = ("${hasProject}" === "Y");
|
||||
var isMachine = ("${isMachine}" === "Y");
|
||||
|
||||
// ===== 컬럼 리사이즈 기능 =====
|
||||
function fn_initColumnResize() {
|
||||
var $table = $('#itemListTable');
|
||||
if($table.length === 0) return;
|
||||
|
||||
// 각 th에 리사이저 핸들 추가
|
||||
$table.find('thead th').each(function() {
|
||||
if($(this).find('.col-resizer').length === 0) {
|
||||
$(this).append('<div class="col-resizer"></div>');
|
||||
}
|
||||
});
|
||||
|
||||
var startX, startWidth, $th, colIndex, $col;
|
||||
|
||||
$(document).on('mousedown', '#itemListTable .col-resizer', function(e) {
|
||||
$th = $(this).parent();
|
||||
colIndex = $th.index();
|
||||
$col = $table.find('colgroup col').eq(colIndex);
|
||||
startX = e.pageX;
|
||||
startWidth = $th.outerWidth();
|
||||
|
||||
$('body').css('cursor', 'col-resize');
|
||||
|
||||
$(document).on('mousemove.colResize', function(e) {
|
||||
var newWidth = startWidth + (e.pageX - startX);
|
||||
if(newWidth >= 40) {
|
||||
$col.css('width', newWidth + 'px');
|
||||
$th.css({'width': newWidth + 'px', 'min-width': newWidth + 'px'});
|
||||
}
|
||||
});
|
||||
|
||||
$(document).on('mouseup.colResize', function() {
|
||||
$('body').css('cursor', '');
|
||||
$(document).off('mousemove.colResize mouseup.colResize');
|
||||
});
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
});
|
||||
}
|
||||
|
||||
// ===== Select2 드래그 복사 지원 =====
|
||||
function fn_initSelect2DragCopy() {
|
||||
var mouseTarget = null;
|
||||
var wasDragged = false;
|
||||
var startX = 0, startY = 0;
|
||||
|
||||
// select2:opening 상시 감시 — mouseTarget이 설정되어 있으면 열림 차단
|
||||
$(document).on('select2:opening', '#itemListTable select', function(e) {
|
||||
if(mouseTarget === this) {
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
|
||||
// capturing phase: Select2 핸들러보다 먼저 mouseTarget 플래그 설정
|
||||
document.addEventListener('mousedown', function(e) {
|
||||
var $sel = $(e.target).closest('#itemListTable .select2-selection');
|
||||
if($sel.length === 0) return;
|
||||
if($(e.target).hasClass('select2-selection__clear')) return;
|
||||
|
||||
var $container = $sel.closest('.select2-container');
|
||||
var $select = $container.prev('select');
|
||||
mouseTarget = $select[0]; // Select2 열림 차단 활성화
|
||||
wasDragged = false;
|
||||
startX = e.pageX;
|
||||
startY = e.pageY;
|
||||
|
||||
var onMove = function(ev) {
|
||||
if(Math.abs(ev.pageX - startX) > 3 || Math.abs(ev.pageY - startY) > 3) {
|
||||
wasDragged = true;
|
||||
}
|
||||
};
|
||||
|
||||
var onUp = function() {
|
||||
document.removeEventListener('mousemove', onMove);
|
||||
document.removeEventListener('mouseup', onUp);
|
||||
if(!wasDragged) {
|
||||
mouseTarget = null;
|
||||
$select.select2('open');
|
||||
}
|
||||
setTimeout(function() { mouseTarget = null; }, 50);
|
||||
};
|
||||
|
||||
document.addEventListener('mousemove', onMove);
|
||||
document.addEventListener('mouseup', onUp);
|
||||
}, true); // capturing phase — Select2보다 먼저 실행
|
||||
}
|
||||
|
||||
$(function() {
|
||||
// 반납사유 옵션을 템플릿에서 읽어옴
|
||||
returnReasonOptions = $('#return_reason_template').html();
|
||||
|
||||
// 컬럼 리사이즈 초기화
|
||||
fn_initColumnResize();
|
||||
|
||||
// Select2 드래그 복사 초기화
|
||||
fn_initSelect2DragCopy();
|
||||
|
||||
// 기존 품목 데이터 로드
|
||||
fn_loadExistingItems();
|
||||
|
||||
@@ -36,6 +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;
|
||||
}
|
||||
|
||||
</style>
|
||||
<script type="text/javascript">
|
||||
// 품목 관리 변수
|
||||
@@ -50,10 +109,104 @@
|
||||
// 프로젝트 존재 여부 체크 변수
|
||||
var hasProject = false;
|
||||
|
||||
// ===== 컬럼 리사이즈 기능 =====
|
||||
function fn_initColumnResize() {
|
||||
var $table = $('#itemListTable');
|
||||
if($table.length === 0) return;
|
||||
|
||||
// 각 th에 리사이저 핸들 추가
|
||||
$table.find('thead th').each(function() {
|
||||
if($(this).find('.col-resizer').length === 0) {
|
||||
$(this).append('<div class="col-resizer"></div>');
|
||||
}
|
||||
});
|
||||
|
||||
var startX, startWidth, $th, colIndex, $col;
|
||||
|
||||
$(document).on('mousedown', '#itemListTable .col-resizer', function(e) {
|
||||
$th = $(this).parent();
|
||||
colIndex = $th.index();
|
||||
$col = $table.find('colgroup col').eq(colIndex);
|
||||
startX = e.pageX;
|
||||
startWidth = $th.outerWidth();
|
||||
|
||||
$('body').css('cursor', 'col-resize');
|
||||
|
||||
$(document).on('mousemove.colResize', function(e) {
|
||||
var newWidth = startWidth + (e.pageX - startX);
|
||||
if(newWidth >= 40) {
|
||||
$col.css('width', newWidth + 'px');
|
||||
$th.css({'width': newWidth + 'px', 'min-width': newWidth + 'px'});
|
||||
}
|
||||
});
|
||||
|
||||
$(document).on('mouseup.colResize', function() {
|
||||
$('body').css('cursor', '');
|
||||
$(document).off('mousemove.colResize mouseup.colResize');
|
||||
});
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
});
|
||||
}
|
||||
|
||||
// ===== Select2 드래그 복사 지원 =====
|
||||
function fn_initSelect2DragCopy() {
|
||||
var mouseTarget = null;
|
||||
var wasDragged = false;
|
||||
var startX = 0, startY = 0;
|
||||
|
||||
// select2:opening 상시 감시 — mouseTarget이 설정되어 있으면 열림 차단
|
||||
$(document).on('select2:opening', '#itemListTable select', function(e) {
|
||||
if(mouseTarget === this) {
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
|
||||
// capturing phase: Select2 핸들러보다 먼저 mouseTarget 플래그 설정
|
||||
document.addEventListener('mousedown', function(e) {
|
||||
var $sel = $(e.target).closest('#itemListTable .select2-selection');
|
||||
if($sel.length === 0) return;
|
||||
if($(e.target).hasClass('select2-selection__clear')) return;
|
||||
|
||||
var $container = $sel.closest('.select2-container');
|
||||
var $select = $container.prev('select');
|
||||
mouseTarget = $select[0]; // Select2 열림 차단 활성화
|
||||
wasDragged = false;
|
||||
startX = e.pageX;
|
||||
startY = e.pageY;
|
||||
|
||||
var onMove = function(ev) {
|
||||
if(Math.abs(ev.pageX - startX) > 3 || Math.abs(ev.pageY - startY) > 3) {
|
||||
wasDragged = true;
|
||||
}
|
||||
};
|
||||
|
||||
var onUp = function() {
|
||||
document.removeEventListener('mousemove', onMove);
|
||||
document.removeEventListener('mouseup', onUp);
|
||||
if(!wasDragged) {
|
||||
mouseTarget = null;
|
||||
$select.select2('open');
|
||||
}
|
||||
setTimeout(function() { mouseTarget = null; }, 50);
|
||||
};
|
||||
|
||||
document.addEventListener('mousemove', onMove);
|
||||
document.addEventListener('mouseup', onUp);
|
||||
}, true); // capturing phase — Select2보다 먼저 실행
|
||||
}
|
||||
|
||||
$(function() {
|
||||
// 반납사유 옵션을 템플릿에서 읽어옴
|
||||
returnReasonOptions = $('#return_reason_template').html();
|
||||
|
||||
|
||||
// 컬럼 리사이즈 초기화
|
||||
fn_initColumnResize();
|
||||
|
||||
// Select2 드래그 복사 초기화
|
||||
fn_initSelect2DragCopy();
|
||||
|
||||
//alert("${info.CATEGORY_CD}")
|
||||
if("${info.CATEGORY_CD}" == '0000170' || "${info.CATEGORY_CD}" == '0000171'){//오버홀, 개조
|
||||
$(".DIRECT").show();
|
||||
|
||||
98
docs/yc/GUX[계획]-팝업폼-그리드-UX-개선.md
Normal file
98
docs/yc/GUX[계획]-팝업폼-그리드-UX-개선.md
Normal file
@@ -0,0 +1,98 @@
|
||||
# GUX 계획서 — 팝업 폼 그리드 UX 개선
|
||||
|
||||
## 개요
|
||||
|
||||
영업관리_견적관리_견적요청등록, 영업관리_주문서관리_수주통합등록 2개 팝업 화면에서:
|
||||
1. 팝업 폼 전체 가로 너비를 확장하여 좌우 여백을 최소화
|
||||
2. 품목 그리드 컬럼 경계선 드래그로 가로 너비 조절 가능하게
|
||||
3. Select2(품번/품명) 선택값 드래그 복사 시 드롭다운 열림 방지
|
||||
|
||||
## 현재 동작
|
||||
|
||||
- `#EntirePopupFormWrap`이 `width: 97.5%`, `margin: 5px auto`로 설정되어 팝업 윈도우 대비 좌우 여백 발생
|
||||
- 품목 그리드 컬럼 너비가 고정(colgroup %)이라 품번/품명 등 내용이 잘려서 표시됨 (예: "000AN01... x")
|
||||
- 컬럼 리사이즈 기능 없음
|
||||
- Select2 선택값을 드래그하면 텍스트 선택 대신 드롭다운이 열림
|
||||
|
||||
## 변경 후 동작
|
||||
|
||||
- 팝업 폼이 좌우 5~10px 여백만 남기고 윈도우를 거의 채움
|
||||
- 기본정보 영역과 품목 그리드 모두 넓어짐
|
||||
- 품목 그리드 `<th>` 헤더 경계를 드래그하여 컬럼 너비 조절 가능
|
||||
- 품번/품명 Select2에서 선택된 값을 드래그하면 텍스트 선택 동작 (드롭다운 열리지 않음)
|
||||
|
||||
## 시각적 예시
|
||||
|
||||
```
|
||||
변경 전:
|
||||
|---여백---|========== 폼 (97.5%) ==========|---여백---|
|
||||
|
||||
변경 후:
|
||||
|-5px-|=============== 폼 (≈99%) ===============|-5px-|
|
||||
```
|
||||
|
||||
## 아키텍처
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[JSP style 태그] -->|CSS 오버라이드| B[#EntirePopupFormWrap 너비 확장]
|
||||
A -->|CSS 추가| C[th resize: horizontal]
|
||||
A -->|JS 추가| D[Select2 mousedown 이벤트 핸들링]
|
||||
B --> E[기본정보 + 품목 그리드 동시 확장]
|
||||
C --> F[컬럼 경계 드래그 리사이즈]
|
||||
D --> G[드래그 시 드롭다운 방지, 클릭 시 정상 동작]
|
||||
```
|
||||
|
||||
## 변경 대상 파일
|
||||
|
||||
| 파일 | 변경 내용 |
|
||||
|------|-----------|
|
||||
| `WebContent/WEB-INF/view/contractMgmt/estimateRegistFormPopup.jsp` | style 태그에 CSS/JS 추가 |
|
||||
| `WebContent/WEB-INF/view/contractMgmt/estimateAndOrderRegistFormPopup.jsp` | 동일 |
|
||||
|
||||
**수정하지 않는 파일:** CSS 파일(basic_new.css, d_basic.css 등), common.js, 백엔드 코드
|
||||
|
||||
## 코드 설계
|
||||
|
||||
### 1. 폼 너비 확장 (CSS)
|
||||
```css
|
||||
section.business_popup_min_width {
|
||||
padding: 0 5px !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
#EntirePopupFormWrap {
|
||||
width: 100% !important;
|
||||
margin: 5px 0 0 0 !important;
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 컬럼 리사이즈 (CSS)
|
||||
```css
|
||||
#itemListTable th {
|
||||
resize: horizontal;
|
||||
overflow: hidden;
|
||||
min-width: 40px;
|
||||
}
|
||||
#itemListTable {
|
||||
table-layout: auto;
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Select2 드래그 복사 (JS)
|
||||
- Select2 `select2:opening` 이벤트에서 마우스 드래그 중이면 `preventDefault`
|
||||
- `mousedown` → `mousemove` 감지로 드래그 판별
|
||||
- 단순 클릭은 기존대로 드롭다운 열림
|
||||
|
||||
## 예상 문제 및 대응
|
||||
|
||||
| 문제 | 원인 | 대응 방안 |
|
||||
|------|------|-----------|
|
||||
| CSS `resize: horizontal`이 `<th>`에서 미동작 | 일부 브라우저에서 테이블 셀 resize 미지원 | JS 기반 컬럼 리사이즈로 전환 |
|
||||
| 폼 확장 시 기본정보 레이아웃 깨짐 | colgroup %가 넓어진 너비에서 비율 유지 | % 기반이므로 자동 조정됨 |
|
||||
| Select2 드래그 감지 오작동 | mousedown→mousemove 임계값 부정확 | 이동 거리 5px 이상을 드래그로 판별 |
|
||||
|
||||
## 설계 원칙
|
||||
|
||||
- JSP 내부 `<style>` 태그로만 수정하여 다른 팝업에 영향 없도록 격리
|
||||
- 외부 라이브러리 추가 없이 순수 CSS + JS로 구현
|
||||
- 기존 품목 추가/수정/삭제/저장 기능에 영향 없음
|
||||
41
docs/yc/GUX[맥락]-팝업폼-그리드-UX-개선.md
Normal file
41
docs/yc/GUX[맥락]-팝업폼-그리드-UX-개선.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# GUX 맥락노트 — 팝업 폼 그리드 UX 개선
|
||||
|
||||
## 왜 이 작업을 하는가
|
||||
|
||||
수주통합등록, 견적요청등록 팝업에서 품목 그리드의 품번/품명 값이 좁은 컬럼 너비로 인해 잘려서 보이고, 사용자가 값을 확인하거나 복사하기 어려운 UX 문제가 있다. 팝업 윈도우 대비 폼 영역이 좁아 좌우 여백이 낭비되고 있어, 이를 활용하면 전체적인 가독성과 사용성이 개선된다.
|
||||
|
||||
## 핵심 결정 사항과 근거
|
||||
|
||||
### 결정 1: JSP 내부 style로만 수정 (CSS 파일 미수정)
|
||||
|
||||
- **결정:** 각 JSP의 `<style>` 태그에 CSS 오버라이드 추가
|
||||
- **근거:** d_basic.css, basic_new.css 등을 수정하면 모든 팝업에 영향. 해당 2개 화면만 변경하기 위해 JSP 로컬 스타일로 격리
|
||||
- **실패한 대안:** CSS 파일 직접 수정 → 다른 팝업 레이아웃 깨질 위험
|
||||
|
||||
### 결정 2: CSS `resize: horizontal` 우선 시도
|
||||
|
||||
- **결정:** `<th>`에 CSS resize 속성으로 컬럼 리사이즈 구현
|
||||
- **근거:** JS 기반보다 구현이 간단하고 브라우저 네이티브 기능. Chrome/Edge에서 동작 확인 필요
|
||||
- **실패 시 대안:** JS 기반 마우스 드래그 리사이즈로 전환
|
||||
|
||||
### 결정 3: Select2 드래그 복사는 이벤트 핸들링 방식 (A안)
|
||||
|
||||
- **결정:** mousedown→mousemove 감지로 드래그 시 `select2:opening` 방지
|
||||
- **근거:** 사용자가 기존 UX(클릭으로 드롭다운 열기)를 유지하면서 드래그 복사도 가능하길 원함
|
||||
- **실패한 대안:** B안(복사 아이콘) — 사용자가 A안 선택
|
||||
|
||||
## 관련 파일 위치
|
||||
|
||||
| 파일 | 역할 |
|
||||
|------|------|
|
||||
| `WebContent/WEB-INF/view/contractMgmt/estimateRegistFormPopup.jsp` | 견적요청등록 팝업 (2385줄) |
|
||||
| `WebContent/WEB-INF/view/contractMgmt/estimateAndOrderRegistFormPopup.jsp` | 수주통합등록 팝업 (1552줄) |
|
||||
| `WebContent/css/basic_new.css` | 현재 로드되는 CSS (init_new.jsp 경유) |
|
||||
| `WebContent/css/d_basic.css` | `#EntirePopupFormWrap` width: 97.5% 정의 |
|
||||
|
||||
## 기술 참고
|
||||
|
||||
- Select2 4.x의 `select2:opening` 이벤트는 `preventDefault()`로 열림 방지 가능
|
||||
- CSS `resize` 속성은 `overflow: hidden/scroll/auto`와 함께 사용해야 동작
|
||||
- `table-layout: auto`로 변경하면 컬럼이 내용에 맞게 자동 확장 가능
|
||||
- 팝업 윈도우 크기: 견적요청등록 1400x560, 수주통합등록 1600x800
|
||||
44
docs/yc/GUX[체크]-팝업폼-그리드-UX-개선.md
Normal file
44
docs/yc/GUX[체크]-팝업폼-그리드-UX-개선.md
Normal file
@@ -0,0 +1,44 @@
|
||||
# GUX 체크리스트 — 팝업 폼 그리드 UX 개선
|
||||
|
||||
## 공정 상태
|
||||
|
||||
진행률: 100%
|
||||
|
||||
## 구현 체크리스트
|
||||
|
||||
### estimateAndOrderRegistFormPopup.jsp (수주통합등록)
|
||||
- [x] 폼 전체 너비 확장 + 중앙 정렬 CSS 추가
|
||||
- [x] 품목 그리드 컬럼 리사이즈 CSS/JS 추가
|
||||
- [x] Select2 드래그 복사 이벤트 핸들링 추가
|
||||
- [x] Select2 x(초기화) 버튼 위치 이동
|
||||
|
||||
### estimateRegistFormPopup.jsp (견적요청등록)
|
||||
- [x] 폼 전체 너비 확장 + 중앙 정렬 CSS 추가
|
||||
- [x] 품목 그리드 컬럼 리사이즈 CSS/JS 추가
|
||||
- [x] Select2 드래그 복사 이벤트 핸들링 추가
|
||||
- [x] Select2 x(초기화) 버튼 위치 이동
|
||||
|
||||
## 검증 체크리스트
|
||||
|
||||
- [x] 수주통합등록: 폼이 팝업 윈도우 중앙 정렬, 좌우 8px 여백
|
||||
- [x] 수주통합등록: 기본정보 영역 레이아웃 정상
|
||||
- [x] 수주통합등록: 품목 그리드 컬럼 헤더 경계 드래그로 리사이즈 가능
|
||||
- [x] 수주통합등록: 품번/품명 Select2 클릭 → 드롭다운 정상 열림
|
||||
- [x] 수주통합등록: 품번/품명 Select2 드래그 → 텍스트 선택 가능 (드롭다운 안 열림)
|
||||
- [x] 수주통합등록: 품목 추가/수정/삭제/저장 정상 동작
|
||||
- [x] 견적요청등록: 위와 동일 항목 모두 정상
|
||||
|
||||
## 정리
|
||||
|
||||
- [x] 불필요한 코드/주석 정리
|
||||
- [x] 커밋 완료
|
||||
|
||||
## 변경 이력
|
||||
|
||||
| 날짜 | 내용 |
|
||||
|------|------|
|
||||
| 2026-03-26 | PCC 문서 최초 생성 |
|
||||
| 2026-03-26 | 구현 완료, Playwright 1차 검증 통과 |
|
||||
| 2026-03-26 | Step 5 디버그: 컬럼 리사이즈 colgroup 연동, Select2 드래그 복사 capturing phase 방식 |
|
||||
| 2026-03-26 | Select2 x버튼 위치 이동, 리사이즈 호버 영역 좌우 대칭, 폼 중앙 정렬 |
|
||||
| 2026-03-26 | 사용자 2차 테스트 전체 통과 — 완료 |
|
||||
Reference in New Issue
Block a user