From 5ae1c91ad923def74a6cda01465b8591f063a040 Mon Sep 17 00:00:00 2001 From: Hyojin Ahn Date: Fri, 10 Jul 2026 14:49:01 -0400 Subject: [PATCH] =?UTF-8?q?install=20=EA=B3=BC=20container=20=ED=86=B5?= =?UTF-8?q?=ED=95=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/mis-integration-api.md | 377 ++++++++++++++++++ .../CustomerContainerController.java | 14 + .../CustomerDailyInstallController.java | 37 ++ .../CustomerDailyOrderController.java | 5 + .../CustomerContainerIntegrateRequestDto.java | 24 ++ ...stomerDailyInstallIntegrateRequestDto.java | 25 ++ ...tomerDailyInstallIntegrateResponseDto.java | 19 + .../dto/CustomerDailyInstallRequestDto.java | 12 + .../com/goi/erp/entity/CustomerContainer.java | 6 + .../goi/erp/entity/CustomerDailyInstall.java | 3 + .../CustomerContainerRepository.java | 3 + ...stomerDailyInstallContainerRepository.java | 3 + .../CustomerDailyInstallRepository.java | 8 + .../erp/service/CustomerContainerService.java | 100 ++++- .../CustomerDailyInstallContainerService.java | 51 +++ ...ustomerDailyInstallIntegrationService.java | 72 ++++ .../service/CustomerDailyInstallService.java | 197 ++++++++- 17 files changed, 922 insertions(+), 34 deletions(-) create mode 100644 docs/mis-integration-api.md create mode 100644 src/main/java/com/goi/erp/dto/CustomerContainerIntegrateRequestDto.java create mode 100644 src/main/java/com/goi/erp/dto/CustomerDailyInstallIntegrateRequestDto.java create mode 100644 src/main/java/com/goi/erp/dto/CustomerDailyInstallIntegrateResponseDto.java create mode 100644 src/main/java/com/goi/erp/service/CustomerDailyInstallIntegrationService.java diff --git a/docs/mis-integration-api.md b/docs/mis-integration-api.md new file mode 100644 index 0000000..2e7d198 --- /dev/null +++ b/docs/mis-integration-api.md @@ -0,0 +1,377 @@ +# MIS → CRM 통합 API (crm-rest-api) + +MIS 가 CRM 으로 데이터를 밀어 넣을 때 호출하는 **통합(integrate) 엔드포인트** 명세입니다. +이번 작업으로 추가된 2개 엔드포인트만 다룹니다. + +| # | 대상 | Method | Path | +|---|------|--------|------| +| 1 | Daily Install (+ Container) 통합 | `POST` | `/customer-daily-install/integrate` | +| 2 | Daily Install 삭제 (자연키) | `DELETE` | `/customer-daily-install/customer/{accountNo}/date/{installDate}/jobtype/{workType}` | +| 3 | Customer Container 통합 | `POST` | `/customer-container/integrate` | + +--- + +## 공통 사항 + +### Base URL +``` +http://{host}:8082/crm-rest-api +``` +- 서버 포트 `8082`, context-path `/crm-rest-api` 포함. +- 예: `http://192.168.2.172:8082/crm-rest-api/customer-daily-install/integrate` + +### 인증 (필수) +JWT 를 아래 둘 중 하나로 전달합니다. **쿠키 우선**, 없으면 헤더. +- 쿠키: `AUTH_TOKEN={jwt}` +- 헤더: `Authorization: Bearer {jwt}` + +권한: 두 엔드포인트 모두 **CRM 생성 권한**(`Module=C, Action=C`, 또는 ADMIN) 필요. + +### 요청 형식 +- `Content-Type: application/json` +- 문자 인코딩 UTF-8 + +### 날짜/시간 포맷 (JSON 문자열) +| 타입 | 포맷 | 예 | +|------|------|-----| +| date | `yyyy-MM-dd` | `"2026-07-08"` | +| time | `HH:mm:ss` | `"14:30:00"` | +| datetime | `yyyy-MM-dd'T'HH:mm:ss` | `"2026-07-08T14:30:00"` | +| 문자열 타임스탬프(14자리) | `yyyyMMddHHmmss` | `"20260708143000"` | + +> `createddate` / `updateddate` 처럼 varchar(14) 로 저장되는 필드는 위 14자리 문자열 그대로 전달합니다. + +### 공통 응답 코드 +| 코드 | 의미 | 본문 예 | +|------|------|---------| +| `200 OK` | 성공 | (엔드포인트별 응답 참조) | +| `400 Bad Request` | 업무 오류 (예: accountNo 로 고객 못 찾음) | `{ "error": "Customer not found by accountNo: 12345", "timestamp": "...", "status": 400 }` | +| `401 Unauthorized` | 토큰 만료/무효 | `{ "error": "Session has expired." }` 또는 `{ "error": "Invalid login information." }` | +| `403 Forbidden` | 권한 없음 | `{ "error": "Forbidden", "message": "No permission", "status": 403, "timestamp": "..." }` | +| `500 Internal Server Error` | 서버 오류 | `{ "error": "Internal Server Error", "message": "...", "status": 500 }` | + +### 내부 ID 자동 변환 규칙 (중요) +MIS 는 CRM 내부 PK 를 모르므로, CRM 이 아래 규칙으로 변환합니다. **MIS 는 내부 id 대신 아래 값을 보냅니다.** + +| 논리 대상 | MIS 가 보내는 값 | CRM 변환 방식 | +|-----------|------------------|----------------| +| 고객 | `accountNo` (= MIS `di_accountno` / `cc_accountno`) | `customer.cus_no` 로 조회 → 내부 `cusId` | +| 기사(driver) | `externalDriverId` (= MIS `di_driver_uid`) | HCM `employee_external_map(MIS_DRIVER)` 조회 → 내부 `empId` | +| 생성자 | `externalCreatedBy` (= MIS member id) | HCM `employee_external_map(MIS_MEMBER)` 조회 → `empNo` | + +> `accountNo == cus_no` 전제. 해당 고객이 CRM 에 없으면 `400` (`Customer not found by accountNo`). + +--- + +## 1. Daily Install 통합 + +``` +POST /customer-daily-install/integrate +``` + +`tbl_daily_install` 1건과 그에 딸린 `tbl_daily_install_container` N건을 **한 번의 호출·한 트랜잭션**으로 반영합니다. + +### 동작 +- **install 은 자연키로 upsert**: `(accountNo, workType, installDate)` — MIS `tbl_daily_install` 의 `UNIQUE KEY (di_accountno, di_work_type, di_install_date)` 와 동일. + - 기존 있으면 update, 없으면 insert. +- **containers 는 install 단위로 replace-all**: 컨테이너 행에는 자연키가 없으므로, 해당 install 의 기존 컨테이너를 **전부 삭제 후 재삽입**. + - `containers` 필드 **생략/`null`** → 컨테이너 손대지 않음. + - `containers: []` (빈 배열) → 해당 install 의 컨테이너 **전부 삭제**. + - `containers: [ ... ]` → 통째로 교체. +- 컨테이너의 부모 연결(`cdicDailyInstallId`)은 저장된 install 의 내부 id 로 **서버가 자동 세팅**하므로 요청에 넣을 필요 없음(넣어도 무시). +- 타임스탬프(`cdiCreatedAt` / `cdiUpdatedAt`)는 **서버 auditing 이 자동 기록**. + +### 요청 본문 + +```jsonc +{ + "install": { + // ── 자연키 (필수) ── + "cdiAccountNo": "A00123", // = di_accountno (= cus_no) + "cdiWorkType": "INSTALL", // = di_work_type + "cdiInstallDate": "2026-07-08", // = di_install_date + + // ── 필수(NOT NULL) ── + "cdiOrderSeq": 1, // = di_order_seq + "cdiExternalDriverId": "88", // = di_driver_uid → 내부 empId 로 변환 + + // ── 선택 ── + "cdiInstallTime": "14:30:00", + "cdiCustomerName": "홍길동 식당", + "cdiPhone": "416-555-1234", + "cdiAddress": "123 Main St", + "cdiCity": "Toronto", + "cdiPostalCode": "M1A1A1", + "cdiGeoLat": 43.6532100, + "cdiGeoLon": -79.3831800, + "cdiExternalCustomerDriverId": "88", // = di_customer_driver_uid → 내부 empId (선택) + "cdiOilPickup": 1, + "cdiSludge": 0, + "cdiRequestNote": "...", + "cdiWorkNote": "...", + "cdiInstallNote": "...", + "cdiCsNote": "...", + "cdiStatus": "A", + "cdiExternalCreatedBy": "1002" // = MIS member id → empNo 로 변환 + }, + "containers": [ + { + "cdicAction": "IN", // 필수 + "cdicType": "T20", // 필수 + "cdicCapacity": 20, + "cdicIsUsed": "N", + "cdicHasLock": "N", + "cdicHasWheel": "N", + "cdicHasWoodframe":"N", + "cdicOwnerType": "C" + } + ] +} +``` + +#### `install` 필드 (MIS `tbl_daily_install` 매핑) + +| 필드 | 타입 | 필수 | MIS 컬럼 | 비고 | +|------|------|:---:|----------|------| +| `cdiAccountNo` | string | ✅ | `di_accountno` | 자연키 · 고객 해석(=`cus_no`) | +| `cdiWorkType` | string(10) | ✅ | `di_work_type` | 자연키 | +| `cdiInstallDate` | date | ✅ | `di_install_date` | 자연키 | +| `cdiOrderSeq` | int | ✅ | `di_order_seq` | | +| `cdiExternalDriverId` | string | ✅\* | `di_driver_uid` | 내부 driver empId 로 변환. `cdiDriverId`(내부 Long) 직접 전달도 가능 | +| `cdiInstallTime` | time | | `di_install_time` | | +| `cdiCustomerName` | string(100) | | `di_customer_name` | | +| `cdiPhone` | string(50) | | `di_phone` | | +| `cdiAddress` | string(200) | | `di_address` | | +| `cdiCity` | string(50) | | `di_city` | | +| `cdiPostalCode` | string(20) | | `di_postal` | | +| `cdiGeoLat` | number | | `di_lat` | | +| `cdiGeoLon` | number | | `di_lng` | | +| `cdiExternalCustomerDriverId` | string | | `di_customer_driver_uid` | 내부 empId 로 변환(선택) | +| `cdiWorkType`(위 자연키와 동일) | | | | | +| `cdiOilPickup` | int | | `di_oil_pickup` | | +| `cdiSludge` | int | | `di_sludge` | | +| `cdiRequestNote` | string | | `di_request_note` | | +| `cdiWorkNote` | string | | `di_work_note` | | +| `cdiInstallNote` | string | | `di_install_note` | | +| `cdiCsNote` | string | | `di_cs_note` | | +| `cdiStatus` | string(1) | | `di_status` | 기본 'A' | +| `cdiExternalCreatedBy` | string | | `di_created_by`(member id) | `empNo` 로 변환 | +| `cdiLoginUser` | string | | — | ERP 내부 호출용(생성/수정자 직접 지정). MIS 는 보통 미사용 | + +\* 내부 `cdiDriverId`(Long) 를 직접 주면 그 값을 우선 사용. MIS 는 `cdiExternalDriverId` 로 전달. + +> MIS 의 `di_wait_uid`, `di_lock_date`, `di_container_applied` 는 CRM 에 대응 컬럼이 없어 **저장하지 않습니다.** + +#### `containers[]` 필드 (MIS `tbl_daily_install_container` 매핑) + +| 필드 | 타입 | 필수 | MIS 컬럼 | +|------|------|:---:|----------| +| `cdicAction` | string(10) | ✅ | `dic_action` | +| `cdicType` | string(10) | ✅ | `dic_type` | +| `cdicCapacity` | int | | `dic_capacity` | +| `cdicIsUsed` | string(1) | | `dic_is_used` | +| `cdicHasLock` | string(1) | | `dic_has_lock` | +| `cdicHasWheel` | string(1) | | `dic_has_wheel` | +| `cdicHasWoodframe` | string(1) | | `dic_has_woodframe` | +| `cdicOwnerType` | string(1) | | `dic_owner_type` | + +> `dic_di_uid`(부모 install FK)는 서버가 자동 세팅하므로 보내지 않습니다. + +### 응답 `200 OK` + +```jsonc +{ + "install": { + "cdiUuid": "0f9c...-...", + "cdiOrderSeq": 1, + "cdiDriverId": 57, + "cdiInstallDate": "2026-07-08", + "cdiInstallTime": "14:30:00", + "cdiCustomerId": 1024, + "cdiAccountNo": "A00123", + "cdiCustomerName": "홍길동 식당", + "cdiWorkType": "INSTALL", + "cdiStatus": "A", + "cdiCreatedBy": "E1002", + "cdiCreatedAt": "2026-07-08T14:31:05", + "cdiUpdatedAt": "2026-07-08T14:31:05" + // ... 나머지 필드 포함 + }, + "containers": [ + { + "cdicId": 3037, + "cdicDailyInstallId": 1235, + "cdicAction": "IN", + "cdicType": "T20", + "cdicCapacity": 20, + "cdicIsUsed": "N", + "cdicHasLock": "N", + "cdicHasWheel": "N", + "cdicHasWoodframe": "N", + "cdicOwnerType": "C" + } + ] +} +``` + +--- + +## 2. Daily Install 삭제 (자연키) + +``` +DELETE /customer-daily-install/customer/{accountNo}/date/{installDate}/jobtype/{workType} +``` + +MIS 에서 install 이 삭제/이동되었을 때, 자연키로 CRM 의 install 을 삭제합니다. + +### 경로 변수 +| 변수 | 타입 | MIS 컬럼 | 예 | +|------|------|----------|-----| +| `accountNo` | string | `di_accountno` (= `cus_no`) | `A00123` | +| `installDate` | date (`yyyy-MM-dd`) | `di_install_date` | `2026-07-08` | +| `workType` | string | `di_work_type` | `INSTALL` | + +### 동작 +- 자연키 `(accountNo, workType, installDate)` 로 install 을 찾음. +- **딸린 컨테이너(`tbl_daily_install_container`)를 먼저 전부 삭제**한 뒤 install 을 삭제(고아 방지). +- 대상이 없어도 **멱등**: 그대로 `204` 반환(에러 아님). 재시도/중복 호출 안전. +- 요청 본문 없음. + +### 응답 +| 코드 | 의미 | +|------|------| +| `204 No Content` | 삭제 완료 또는 대상 없음(멱등) | +| `403 Forbidden` | 권한 없음 | + +> 권한: **CRM 삭제 권한**(`Module=C, Action=D`) 필요. (통합 POST 는 생성 권한 `C` 이지만, 이 삭제는 삭제 권한 `D` 를 확인함.) + +### 호출 예시 +```bash +curl -X DELETE \ + "http://192.168.2.172:8082/crm-rest-api/customer-daily-install/customer/A00123/date/2026-07-08/jobtype/INSTALL" \ + -H "Authorization: Bearer $JWT" +``` + +--- + +## 3. Customer Container 통합 + +``` +POST /customer-container/integrate +``` + +특정 고객의 컨테이너 목록을 **통째로 교체(replace-all)** 합니다. +CRM 에서는 고객 컨테이너를 수정하지 않고 **MIS 를 단일 소스**로 보기 때문에 전량 동기화합니다. + +### 동작 +- `accountNo`(= `cc_accountno` = `cus_no`) 로 고객을 찾아 내부 `cusId` 로 변환. +- 그 고객의 **기존 컨테이너 전부 삭제 후 재삽입**. + - `containers` **생략/`null`** → 아무것도 건드리지 않음(현재 목록 반환). + - `containers: []` → 해당 고객 컨테이너 **전부 삭제**. + - `containers: [ ... ]` → 통째로 교체. +- `ccoModifiedAt` 은 서버 auditing 이 자동 기록. + +### 요청 본문 + +```jsonc +{ + "accountNo": "A00123", // = cc_accountno (= cus_no) + "containers": [ + { + "ccoType": "T20", // 필수 + "ccoCapacity": 20, + "ccoOwnerType": "C", + "ccoIsUsed": "Y", + "ccoHasLock": "N", + "ccoHasWheel": "N", + "ccoHasWoodframe":"N", + "ccoStatus": "I", + "ccoCreateddate": "20260708143000", // varchar(14) + "ccoUpdateddate": "20260708143000", // varchar(14) + "ccoInstallNote": "...", + "ccoPickupNote": "...", + "ccoInstallDate": "2026-07-08", + "ccoPickupDate": null, + "ccoInstallAt": "2026-07-08T14:30:00", + "ccoPickupAt": null + } + ] +} +``` + +#### 최상위 필드 +| 필드 | 타입 | 필수 | 비고 | +|------|------|:---:|------| +| `accountNo` | string | ✅ | `cc_accountno` (= `cus_no`). 고객 해석용 | +| `containers` | array | | 아래 표. 교체 규칙은 위 "동작" 참조 | + +#### `containers[]` 필드 (MIS `tbl_customer_container` 매핑) + +| 필드 | 타입 | 필수 | MIS 컬럼 | +|------|------|:---:|----------| +| `ccoType` | string(10) | ✅ | `cc_type` | +| `ccoCapacity` | int | | `cc_capacity` | +| `ccoOwnerType` | string(1) | | `cc_owner_type` | +| `ccoIsUsed` | string(1) | | `cc_is_used` | +| `ccoHasLock` | string(1) | | `cc_has_lock` | +| `ccoHasWheel` | string(1) | | `cc_has_wheel` | +| `ccoHasWoodframe` | string(1) | | `cc_has_woodframe` | +| `ccoStatus` | string(1) | | `cc_status` | +| `ccoCreateddate` | string(14) | | `cc_createddate` | +| `ccoUpdateddate` | string(14) | | `cc_updateddate` | +| `ccoInstallNote` | string(500) | | `cc_install_note` | +| `ccoPickupNote` | string(500) | | `cc_pickup_note` | +| `ccoInstallDate` | date | | `cc_install_date` | +| `ccoPickupDate` | date | | `cc_pickup_date` | +| `ccoInstallAt` | datetime | | `cc_install_at` | +| `ccoPickupAt` | datetime | | `cc_pickup_at` | + +> `cc_uid`(MIS 내부 PK)는 사용하지 않습니다. 고객 단위 전량 교체이므로 컨테이너 개별 식별자가 필요 없습니다. +> `ccoCustomerId` 는 `accountNo` 로 서버가 채우므로 보내지 않습니다(보내도 무시). + +### 응답 `200 OK` +교체 후 그 고객의 전체 컨테이너 목록(배열)을 반환합니다. + +```jsonc +[ + { + "ccoId": 30235, + "ccoCustomerId": 1024, + "ccoType": "T20", + "ccoCapacity": 20, + "ccoOwnerType": "C", + "ccoIsUsed": "Y", + "ccoHasLock": "N", + "ccoHasWheel": "N", + "ccoHasWoodframe": "N", + "ccoStatus": "I", + "ccoCreateddate": "20260708143000", + "ccoUpdateddate": "20260708143000", + "ccoInstallNote": "...", + "ccoPickupNote": "...", + "ccoInstallDate": "2026-07-08", + "ccoPickupDate": null, + "ccoInstallAt": "2026-07-08T14:30:00", + "ccoPickupAt": null, + "ccoModifiedAt": "2026-07-08T14:31:05" + } +] +``` + +--- + +## 호출 예시 (curl) + +```bash +# 1) Daily Install 통합 +curl -X POST "http://192.168.2.172:8082/crm-rest-api/customer-daily-install/integrate" \ + -H "Authorization: Bearer $JWT" \ + -H "Content-Type: application/json" \ + -d @install.json + +# 2) Customer Container 통합 +curl -X POST "http://192.168.2.172:8082/crm-rest-api/customer-container/integrate" \ + -H "Authorization: Bearer $JWT" \ + -H "Content-Type: application/json" \ + -d @container.json +``` diff --git a/src/main/java/com/goi/erp/controller/CustomerContainerController.java b/src/main/java/com/goi/erp/controller/CustomerContainerController.java index 889f0f6..4a4183e 100644 --- a/src/main/java/com/goi/erp/controller/CustomerContainerController.java +++ b/src/main/java/com/goi/erp/controller/CustomerContainerController.java @@ -2,6 +2,7 @@ package com.goi.erp.controller; import com.goi.erp.common.permission.PermissionChecker; import com.goi.erp.common.permission.PermissionSet; +import com.goi.erp.dto.CustomerContainerIntegrateRequestDto; import com.goi.erp.dto.CustomerContainerRequestDto; import com.goi.erp.dto.CustomerContainerResponseDto; import com.goi.erp.service.CustomerContainerService; @@ -54,6 +55,19 @@ public class CustomerContainerController { ); } + // MIS 통합: accountNo(=cus_no) 로 고객의 컨테이너를 통째로 replace-all + @PostMapping("/integrate") + public ResponseEntity> integrate( + @RequestBody CustomerContainerIntegrateRequestDto dto + ) { + + if (!PermissionChecker.canCreateCRM(getPermission())) { + throw new AccessDeniedException("No permission"); + } + + return ResponseEntity.ok(service.integrateByAccountNo(dto)); + } + @GetMapping("/customer/{customerId}") public ResponseEntity> getByCustomerId( @PathVariable Long customerId diff --git a/src/main/java/com/goi/erp/controller/CustomerDailyInstallController.java b/src/main/java/com/goi/erp/controller/CustomerDailyInstallController.java index 6cff7a9..499ea97 100644 --- a/src/main/java/com/goi/erp/controller/CustomerDailyInstallController.java +++ b/src/main/java/com/goi/erp/controller/CustomerDailyInstallController.java @@ -2,8 +2,11 @@ package com.goi.erp.controller; import com.goi.erp.common.permission.PermissionChecker; import com.goi.erp.common.permission.PermissionSet; +import com.goi.erp.dto.CustomerDailyInstallIntegrateRequestDto; +import com.goi.erp.dto.CustomerDailyInstallIntegrateResponseDto; import com.goi.erp.dto.CustomerDailyInstallRequestDto; import com.goi.erp.dto.CustomerDailyInstallResponseDto; +import com.goi.erp.service.CustomerDailyInstallIntegrationService; import com.goi.erp.service.CustomerDailyInstallService; import com.goi.erp.token.PermissionAuthenticationToken; @@ -27,6 +30,7 @@ import java.util.List; public class CustomerDailyInstallController { private final CustomerDailyInstallService service; + private final CustomerDailyInstallIntegrationService integrationService; private PermissionSet getPermission() { @@ -56,6 +60,22 @@ public class CustomerDailyInstallController { ); } + // MIS 통합: install 1건 + container N건을 한 번에 upsert / replace-all + @PostMapping("/integrate") + public ResponseEntity integrate( + @RequestBody CustomerDailyInstallIntegrateRequestDto dto + ) { + + if (!PermissionChecker.canCreateCRM(getPermission())) { + throw new AccessDeniedException("No permission"); + } + + return new ResponseEntity<>( + integrationService.integrate(dto), + HttpStatus.OK + ); + } + @GetMapping("/date/{installDate}") public ResponseEntity> getByInstallDate( @PathVariable @@ -96,4 +116,21 @@ public class CustomerDailyInstallController { return ResponseEntity.noContent().build(); } + + // MIS 통합: 자연키(accountNo, installDate, workType) 로 install + 딸린 container 삭제 (멱등) + @DeleteMapping("/customer/{accountNo}/date/{installDate}/jobtype/{workType}") + public ResponseEntity deleteByBusinessKey( + @PathVariable String accountNo, + @PathVariable @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate installDate, + @PathVariable String workType + ) { + + if (!PermissionChecker.canDeleteCRM(getPermission())) { + throw new AccessDeniedException("No permission"); + } + + integrationService.deleteByBusinessKey(accountNo, workType, installDate); + + return ResponseEntity.noContent().build(); + } } \ No newline at end of file diff --git a/src/main/java/com/goi/erp/controller/CustomerDailyOrderController.java b/src/main/java/com/goi/erp/controller/CustomerDailyOrderController.java index 8651208..56e3db1 100644 --- a/src/main/java/com/goi/erp/controller/CustomerDailyOrderController.java +++ b/src/main/java/com/goi/erp/controller/CustomerDailyOrderController.java @@ -171,6 +171,11 @@ public class CustomerDailyOrderController { @PathVariable @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate orderDate, @PathVariable String jobType) { + PermissionSet permissions = getPermission(); + if (!PermissionChecker.canDeleteCRM(permissions)) { + throw new AccessDeniedException("You do not have permission to delete daily order"); + } + dailyOrderService.removeDailyOrderByBusinessKey(customerNo, orderDate, jobType); return ResponseEntity.noContent().build(); // 204 } diff --git a/src/main/java/com/goi/erp/dto/CustomerContainerIntegrateRequestDto.java b/src/main/java/com/goi/erp/dto/CustomerContainerIntegrateRequestDto.java new file mode 100644 index 0000000..25a0e41 --- /dev/null +++ b/src/main/java/com/goi/erp/dto/CustomerContainerIntegrateRequestDto.java @@ -0,0 +1,24 @@ +package com.goi.erp.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +/** + * MIS → CRM 고객 컨테이너 통합 요청. + * accountNo(=cus_no) 로 고객을 찾고, 그 고객의 컨테이너를 통째로 replace-all 한다. + * (CRM 에서는 고객 컨테이너를 수정하지 않으므로 MIS 를 단일 소스로 보고 전량 동기화한다.) + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class CustomerContainerIntegrateRequestDto { + + private String accountNo; + + private List containers; +} diff --git a/src/main/java/com/goi/erp/dto/CustomerDailyInstallIntegrateRequestDto.java b/src/main/java/com/goi/erp/dto/CustomerDailyInstallIntegrateRequestDto.java new file mode 100644 index 0000000..2b49d44 --- /dev/null +++ b/src/main/java/com/goi/erp/dto/CustomerDailyInstallIntegrateRequestDto.java @@ -0,0 +1,25 @@ +package com.goi.erp.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +/** + * MIS → CRM 통합 요청. + * tbl_daily_install 1건과 그에 딸린 tbl_daily_install_container N건을 한 번에 받는다. + * install 은 자연키(accountNo, workType, installDate) 로 upsert 하고, + * containers 는 저장된 install 의 내부 id 기준으로 replace-all 한다. + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class CustomerDailyInstallIntegrateRequestDto { + + private CustomerDailyInstallRequestDto install; + + private List containers; +} diff --git a/src/main/java/com/goi/erp/dto/CustomerDailyInstallIntegrateResponseDto.java b/src/main/java/com/goi/erp/dto/CustomerDailyInstallIntegrateResponseDto.java new file mode 100644 index 0000000..dcc5e19 --- /dev/null +++ b/src/main/java/com/goi/erp/dto/CustomerDailyInstallIntegrateResponseDto.java @@ -0,0 +1,19 @@ +package com.goi.erp.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class CustomerDailyInstallIntegrateResponseDto { + + private CustomerDailyInstallResponseDto install; + + private List containers; +} diff --git a/src/main/java/com/goi/erp/dto/CustomerDailyInstallRequestDto.java b/src/main/java/com/goi/erp/dto/CustomerDailyInstallRequestDto.java index 65a7e4a..30cbe5f 100644 --- a/src/main/java/com/goi/erp/dto/CustomerDailyInstallRequestDto.java +++ b/src/main/java/com/goi/erp/dto/CustomerDailyInstallRequestDto.java @@ -19,6 +19,9 @@ public class CustomerDailyInstallRequestDto { private Long cdiDriverId; + // MIS 에서 di_driver_uid 로 호출 → employee_external_map(MIS_DRIVER) 참조해 내부 empId 로 변환 + private String cdiExternalDriverId; + private LocalDate cdiInstallDate; private LocalTime cdiInstallTime; @@ -43,6 +46,9 @@ public class CustomerDailyInstallRequestDto { private Long cdiCustomerDriverId; + // MIS 에서 di_customer_driver_uid 로 호출 → MIS_DRIVER 참조해 내부 empId 로 변환 (선택) + private String cdiExternalCustomerDriverId; + private String cdiWorkType; private Integer cdiOilPickup; @@ -60,4 +66,10 @@ public class CustomerDailyInstallRequestDto { private String cdiStatus; private String cdiCreatedBy; + + // MIS 에서 member id 로 호출 → employee_external_map(MIS_MEMBER) 참조해 empNo 로 변환 + private String cdiExternalCreatedBy; + + // ERP 내부 호출 시 로그인 사용자 (createdBy/updatedBy 직접 지정) + private String cdiLoginUser; } \ No newline at end of file diff --git a/src/main/java/com/goi/erp/entity/CustomerContainer.java b/src/main/java/com/goi/erp/entity/CustomerContainer.java index 253276d..d746aa9 100644 --- a/src/main/java/com/goi/erp/entity/CustomerContainer.java +++ b/src/main/java/com/goi/erp/entity/CustomerContainer.java @@ -2,6 +2,7 @@ package com.goi.erp.entity; import jakarta.persistence.Column; import jakarta.persistence.Entity; +import jakarta.persistence.EntityListeners; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; @@ -15,12 +16,16 @@ import lombok.NoArgsConstructor; import java.time.LocalDate; import java.time.LocalDateTime; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + @Entity @Table(schema = "crm", name = "customer_container") @Data @NoArgsConstructor @AllArgsConstructor @Builder +@EntityListeners(AuditingEntityListener.class) public class CustomerContainer { @Id @@ -79,6 +84,7 @@ public class CustomerContainer { @Column(name = "cco_pickup_at") private LocalDateTime ccoPickupAt; + @LastModifiedDate @Column(name = "cco_modified_at") private LocalDateTime ccoModifiedAt; } \ No newline at end of file diff --git a/src/main/java/com/goi/erp/entity/CustomerDailyInstall.java b/src/main/java/com/goi/erp/entity/CustomerDailyInstall.java index 9845730..59dfc5e 100644 --- a/src/main/java/com/goi/erp/entity/CustomerDailyInstall.java +++ b/src/main/java/com/goi/erp/entity/CustomerDailyInstall.java @@ -2,6 +2,7 @@ package com.goi.erp.entity; import jakarta.persistence.Column; import jakarta.persistence.Entity; +import jakarta.persistence.EntityListeners; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; @@ -20,6 +21,7 @@ import java.util.UUID; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; @Entity @Table(schema = "crm", name = "customer_daily_install") @@ -27,6 +29,7 @@ import org.springframework.data.annotation.LastModifiedDate; @NoArgsConstructor @AllArgsConstructor @Builder +@EntityListeners(AuditingEntityListener.class) public class CustomerDailyInstall { @Id diff --git a/src/main/java/com/goi/erp/repository/CustomerContainerRepository.java b/src/main/java/com/goi/erp/repository/CustomerContainerRepository.java index 83b464a..0950c93 100644 --- a/src/main/java/com/goi/erp/repository/CustomerContainerRepository.java +++ b/src/main/java/com/goi/erp/repository/CustomerContainerRepository.java @@ -12,4 +12,7 @@ public interface CustomerContainerRepository extends JpaRepository findByCcoCustomerId(Long ccoCustomerId); List findByCcoCustomerIdAndCcoStatus(Long ccoCustomerId, String ccoStatus); + + // MIS 통합 replace-all: 특정 고객의 컨테이너 전부 삭제 후 재삽입 + void deleteByCcoCustomerId(Long ccoCustomerId); } \ No newline at end of file diff --git a/src/main/java/com/goi/erp/repository/CustomerDailyInstallContainerRepository.java b/src/main/java/com/goi/erp/repository/CustomerDailyInstallContainerRepository.java index 23bdde8..f30091e 100644 --- a/src/main/java/com/goi/erp/repository/CustomerDailyInstallContainerRepository.java +++ b/src/main/java/com/goi/erp/repository/CustomerDailyInstallContainerRepository.java @@ -16,4 +16,7 @@ public interface CustomerDailyInstallContainerRepository Long cdicDailyInstallId, String cdicAction ); + + // MIS 통합 replace-all: 특정 install 의 컨테이너 전부 삭제 후 재삽입 + void deleteByCdicDailyInstallId(Long cdicDailyInstallId); } \ No newline at end of file diff --git a/src/main/java/com/goi/erp/repository/CustomerDailyInstallRepository.java b/src/main/java/com/goi/erp/repository/CustomerDailyInstallRepository.java index 4bcf281..059f0f3 100644 --- a/src/main/java/com/goi/erp/repository/CustomerDailyInstallRepository.java +++ b/src/main/java/com/goi/erp/repository/CustomerDailyInstallRepository.java @@ -6,12 +6,20 @@ import org.springframework.stereotype.Repository; import java.time.LocalDate; import java.util.List; +import java.util.Optional; @Repository public interface CustomerDailyInstallRepository extends JpaRepository { List findByCdiInstallDate(LocalDate cdiInstallDate); + // MIS 통합 자연키: (accountno, work_type, install_date) == tbl_daily_install UNIQUE KEY + Optional findByCdiAccountNoAndCdiWorkTypeAndCdiInstallDate( + String cdiAccountNo, + String cdiWorkType, + LocalDate cdiInstallDate + ); + List findByCdiDriverId(Long cdiDriverId); List findByCdiDriverIdAndCdiInstallDate( diff --git a/src/main/java/com/goi/erp/service/CustomerContainerService.java b/src/main/java/com/goi/erp/service/CustomerContainerService.java index cd1bfb4..c46462e 100644 --- a/src/main/java/com/goi/erp/service/CustomerContainerService.java +++ b/src/main/java/com/goi/erp/service/CustomerContainerService.java @@ -1,9 +1,11 @@ package com.goi.erp.service; +import com.goi.erp.dto.CustomerContainerIntegrateRequestDto; import com.goi.erp.dto.CustomerContainerRequestDto; import com.goi.erp.dto.CustomerContainerResponseDto; import com.goi.erp.entity.CustomerContainer; import com.goi.erp.repository.CustomerContainerRepository; +import com.goi.erp.repository.CustomerRepository; import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; @@ -18,26 +20,11 @@ import java.util.stream.Collectors; public class CustomerContainerService { private final CustomerContainerRepository customerContainerRepository; + private final CustomerRepository customerRepository; public CustomerContainerResponseDto create(CustomerContainerRequestDto dto) { - CustomerContainer entity = CustomerContainer.builder() - .ccoCustomerId(dto.getCcoCustomerId()) - .ccoType(dto.getCcoType()) - .ccoCapacity(dto.getCcoCapacity()) - .ccoOwnerType(dto.getCcoOwnerType()) - .ccoIsUsed(dto.getCcoIsUsed()) - .ccoHasLock(dto.getCcoHasLock()) - .ccoHasWheel(dto.getCcoHasWheel()) - .ccoHasWoodframe(dto.getCcoHasWoodframe()) - .ccoStatus(dto.getCcoStatus()) - .ccoInstallNote(dto.getCcoInstallNote()) - .ccoPickupNote(dto.getCcoPickupNote()) - .ccoInstallDate(dto.getCcoInstallDate()) - .ccoPickupDate(dto.getCcoPickupDate()) - .ccoInstallAt(dto.getCcoInstallAt()) - .ccoPickupAt(dto.getCcoPickupAt()) - .build(); + CustomerContainer entity = buildEntity(dto, dto.getCcoCustomerId()); entity = customerContainerRepository.save(entity); @@ -52,6 +39,43 @@ public class CustomerContainerService { .collect(Collectors.toList()); } + /** + * MIS 통합 replace-all. + * accountNo(=cus_no) 로 고객을 찾아, 그 고객의 기존 컨테이너를 전부 삭제하고 전달된 목록으로 재삽입한다. + * containers == null 이면 컨테이너 정보가 전달되지 않은 것으로 보고 아무것도 건드리지 않는다. + * containers 가 빈 리스트이면 "컨테이너 없음" 으로 보고 전부 삭제한다. + */ + @Transactional + public List integrateByAccountNo( + CustomerContainerIntegrateRequestDto request + ) { + + if (request.getAccountNo() == null) { + throw new RuntimeException("accountNo is required"); + } + + Long customerId = customerRepository.findByCusNo(request.getAccountNo()) + .map(c -> c.getCusId()) + .orElseThrow(() -> new RuntimeException( + "Customer not found by accountNo: " + request.getAccountNo())); + + List dtos = request.getContainers(); + if (dtos == null) { + return getByCustomerId(customerId); + } + + customerContainerRepository.deleteByCcoCustomerId(customerId); + + List entities = dtos.stream() + .map(dto -> buildEntity(dto, customerId)) + .collect(Collectors.toList()); + + return customerContainerRepository.saveAll(entities) + .stream() + .map(this::mapToDto) + .collect(Collectors.toList()); + } + @Transactional public CustomerContainerResponseDto update(Long id, CustomerContainerRequestDto dto) { @@ -71,6 +95,29 @@ public class CustomerContainerService { customerContainerRepository.delete(entity); } + private CustomerContainer buildEntity(CustomerContainerRequestDto dto, Long customerId) { + + return CustomerContainer.builder() + .ccoCustomerId(customerId) + .ccoType(dto.getCcoType()) + .ccoCapacity(dto.getCcoCapacity()) + .ccoOwnerType(dto.getCcoOwnerType()) + .ccoIsUsed(dto.getCcoIsUsed()) + .ccoHasLock(dto.getCcoHasLock()) + .ccoHasWheel(dto.getCcoHasWheel()) + .ccoHasWoodframe(dto.getCcoHasWoodframe()) + .ccoStatus(dto.getCcoStatus()) + .ccoCreateddate(dto.getCcoCreateddate()) + .ccoUpdateddate(dto.getCcoUpdateddate()) + .ccoInstallNote(dto.getCcoInstallNote()) + .ccoPickupNote(dto.getCcoPickupNote()) + .ccoInstallDate(dto.getCcoInstallDate()) + .ccoPickupDate(dto.getCcoPickupDate()) + .ccoInstallAt(dto.getCcoInstallAt()) + .ccoPickupAt(dto.getCcoPickupAt()) + .build(); // ccoModifiedAt 은 @LastModifiedDate 로 자동 세팅 + } + private void updateInternal(CustomerContainer entity, CustomerContainerRequestDto dto) { if (dto.getCcoType() != null) entity.setCcoType(dto.getCcoType()); @@ -81,6 +128,14 @@ public class CustomerContainerService { if (dto.getCcoHasWheel() != null) entity.setCcoHasWheel(dto.getCcoHasWheel()); if (dto.getCcoHasWoodframe() != null) entity.setCcoHasWoodframe(dto.getCcoHasWoodframe()); if (dto.getCcoStatus() != null) entity.setCcoStatus(dto.getCcoStatus()); + if (dto.getCcoInstallNote() != null) entity.setCcoInstallNote(dto.getCcoInstallNote()); + if (dto.getCcoPickupNote() != null) entity.setCcoPickupNote(dto.getCcoPickupNote()); + if (dto.getCcoInstallDate() != null) entity.setCcoInstallDate(dto.getCcoInstallDate()); + if (dto.getCcoPickupDate() != null) entity.setCcoPickupDate(dto.getCcoPickupDate()); + if (dto.getCcoInstallAt() != null) entity.setCcoInstallAt(dto.getCcoInstallAt()); + if (dto.getCcoPickupAt() != null) entity.setCcoPickupAt(dto.getCcoPickupAt()); + if (dto.getCcoUpdateddate() != null) entity.setCcoUpdateddate(dto.getCcoUpdateddate()); + // ccoModifiedAt 은 @LastModifiedDate 로 자동 갱신 } private CustomerContainerResponseDto mapToDto(CustomerContainer entity) { @@ -96,6 +151,15 @@ public class CustomerContainerService { .ccoHasWheel(entity.getCcoHasWheel()) .ccoHasWoodframe(entity.getCcoHasWoodframe()) .ccoStatus(entity.getCcoStatus()) + .ccoCreateddate(entity.getCcoCreateddate()) + .ccoUpdateddate(entity.getCcoUpdateddate()) + .ccoInstallNote(entity.getCcoInstallNote()) + .ccoPickupNote(entity.getCcoPickupNote()) + .ccoInstallDate(entity.getCcoInstallDate()) + .ccoPickupDate(entity.getCcoPickupDate()) + .ccoInstallAt(entity.getCcoInstallAt()) + .ccoPickupAt(entity.getCcoPickupAt()) + .ccoModifiedAt(entity.getCcoModifiedAt()) .build(); } -} \ No newline at end of file +} diff --git a/src/main/java/com/goi/erp/service/CustomerDailyInstallContainerService.java b/src/main/java/com/goi/erp/service/CustomerDailyInstallContainerService.java index df03fd9..39a87ac 100644 --- a/src/main/java/com/goi/erp/service/CustomerDailyInstallContainerService.java +++ b/src/main/java/com/goi/erp/service/CustomerDailyInstallContainerService.java @@ -51,6 +51,45 @@ public class CustomerDailyInstallContainerService { .collect(Collectors.toList()); } + /** + * MIS 통합 replace-all. + * 해당 install 의 기존 컨테이너를 전부 삭제하고, 전달된 컨테이너 목록으로 재삽입한다. + * (컨테이너 행에는 자연키가 없어 개별 upsert 가 불가능하므로 install 단위로 동기화한다.) + * dtos == null 이면 컨테이너 정보가 전달되지 않은 것으로 보고 아무것도 건드리지 않는다. + * dtos 가 빈 리스트이면 "컨테이너 없음" 으로 보고 전부 삭제한다. + */ + @Transactional + public List replaceForInstall( + Long dailyInstallId, + List dtos + ) { + + if (dtos == null) { + return getByDailyInstallId(dailyInstallId); + } + + repository.deleteByCdicDailyInstallId(dailyInstallId); + + List entities = dtos.stream() + .map(dto -> CustomerDailyInstallContainer.builder() + .cdicDailyInstallId(dailyInstallId) // 부모 install 의 내부 id 로 강제 + .cdicAction(dto.getCdicAction()) + .cdicType(dto.getCdicType()) + .cdicCapacity(dto.getCdicCapacity()) + .cdicIsUsed(dto.getCdicIsUsed()) + .cdicHasLock(dto.getCdicHasLock()) + .cdicHasWheel(dto.getCdicHasWheel()) + .cdicHasWoodframe(dto.getCdicHasWoodframe()) + .cdicOwnerType(dto.getCdicOwnerType()) + .build()) + .collect(Collectors.toList()); + + return repository.saveAll(entities) + .stream() + .map(this::mapToDto) + .collect(Collectors.toList()); + } + @Transactional public CustomerDailyInstallContainerResponseDto update( Long id, @@ -73,6 +112,14 @@ public class CustomerDailyInstallContainerService { repository.delete(entity); } + /** + * 특정 install 에 속한 컨테이너 전부 삭제 (install 삭제 시 고아 방지). + */ + @Transactional + public void deleteForInstall(Long dailyInstallId) { + repository.deleteByCdicDailyInstallId(dailyInstallId); + } + private void updateInternal( CustomerDailyInstallContainer entity, CustomerDailyInstallContainerRequestDto dto @@ -95,6 +142,10 @@ public class CustomerDailyInstallContainerService { .cdicType(entity.getCdicType()) .cdicCapacity(entity.getCdicCapacity()) .cdicIsUsed(entity.getCdicIsUsed()) + .cdicHasLock(entity.getCdicHasLock()) + .cdicHasWheel(entity.getCdicHasWheel()) + .cdicHasWoodframe(entity.getCdicHasWoodframe()) + .cdicOwnerType(entity.getCdicOwnerType()) .build(); } } \ No newline at end of file diff --git a/src/main/java/com/goi/erp/service/CustomerDailyInstallIntegrationService.java b/src/main/java/com/goi/erp/service/CustomerDailyInstallIntegrationService.java new file mode 100644 index 0000000..04076d4 --- /dev/null +++ b/src/main/java/com/goi/erp/service/CustomerDailyInstallIntegrationService.java @@ -0,0 +1,72 @@ +package com.goi.erp.service; + +import com.goi.erp.dto.CustomerDailyInstallContainerResponseDto; +import com.goi.erp.dto.CustomerDailyInstallIntegrateRequestDto; +import com.goi.erp.dto.CustomerDailyInstallIntegrateResponseDto; +import com.goi.erp.dto.CustomerDailyInstallResponseDto; +import com.goi.erp.entity.CustomerDailyInstall; + +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; + +import org.springframework.stereotype.Service; + +import java.time.LocalDate; +import java.util.List; + +/** + * MIS 가 호출하는 daily-install 통합 오케스트레이션. + * install upsert(자연키) → 저장된 내부 id 로 container replace-all 을 한 트랜잭션으로 처리한다. + */ +@Service +@RequiredArgsConstructor +public class CustomerDailyInstallIntegrationService { + + private final CustomerDailyInstallService installService; + private final CustomerDailyInstallContainerService containerService; + + @Transactional + public CustomerDailyInstallIntegrateResponseDto integrate( + CustomerDailyInstallIntegrateRequestDto request + ) { + + if (request.getInstall() == null) { + throw new RuntimeException("install payload is required"); + } + + // 1. install upsert (accountNo, workType, installDate) + CustomerDailyInstall saved = installService.upsert(request.getInstall()); + + // 2. 저장된 install 의 내부 id 기준으로 container replace-all + List containers = + containerService.replaceForInstall(saved.getCdiId(), request.getContainers()); + + CustomerDailyInstallResponseDto install = installService.mapToDto(saved); + + return CustomerDailyInstallIntegrateResponseDto.builder() + .install(install) + .containers(containers) + .build(); + } + + /** + * 자연키 (accountNo, workType, installDate) 로 install 을 삭제한다. + * 딸린 컨테이너를 먼저 삭제해 고아를 남기지 않는다. + * 대상이 없으면 멱등 처리(아무것도 하지 않음). + * + * @return 실제로 삭제했으면 true, 대상이 없어 넘어갔으면 false + */ + @Transactional + public boolean deleteByBusinessKey(String accountNo, String workType, LocalDate installDate) { + + CustomerDailyInstall existing = installService.findByBusinessKey(accountNo, workType, installDate); + + if (existing == null) { + return false; + } + + containerService.deleteForInstall(existing.getCdiId()); + installService.deleteEntity(existing); + return true; + } +} diff --git a/src/main/java/com/goi/erp/service/CustomerDailyInstallService.java b/src/main/java/com/goi/erp/service/CustomerDailyInstallService.java index 719d5ea..516c319 100644 --- a/src/main/java/com/goi/erp/service/CustomerDailyInstallService.java +++ b/src/main/java/com/goi/erp/service/CustomerDailyInstallService.java @@ -4,6 +4,7 @@ import com.goi.erp.dto.CustomerDailyInstallRequestDto; import com.goi.erp.dto.CustomerDailyInstallResponseDto; import com.goi.erp.entity.CustomerDailyInstall; import com.goi.erp.repository.CustomerDailyInstallRepository; +import com.goi.erp.repository.CustomerRepository; import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; @@ -12,6 +13,8 @@ import org.springframework.stereotype.Service; import java.time.LocalDate; import java.util.List; +import java.util.Map; +import java.util.UUID; import java.util.stream.Collectors; @Service @@ -19,15 +22,52 @@ import java.util.stream.Collectors; public class CustomerDailyInstallService { private final CustomerDailyInstallRepository repository; + private final CustomerRepository customerRepository; + private final HcmEmployeeClient hcmEmployeeClient; + /** + * CREATE (POST /customer-daily-install) + * DailyOrder 와 동일하게 자연키 기준 upsert 로 동작한다. + */ public CustomerDailyInstallResponseDto create(CustomerDailyInstallRequestDto dto) { + return mapToDto(upsert(dto)); + } + /** + * MIS 통합용 upsert. + * 자연키 (accountNo, workType, installDate) 로 조회 → 있으면 update, 없으면 insert. + * 통합(integrate) 흐름에서 저장된 cdiId 가 필요하므로 엔티티를 반환한다. + */ + @Transactional + public CustomerDailyInstall upsert(CustomerDailyInstallRequestDto dto) { + + Long customerId = resolveCustomerId(dto); + Long driverId = resolveDriverId(dto.getCdiDriverId(), dto.getCdiExternalDriverId()); + Long customerDriverId = resolveDriverId(dto.getCdiCustomerDriverId(), dto.getCdiExternalCustomerDriverId()); + String createdBy = resolveCreatedBy(dto); + + CustomerDailyInstall existing = repository + .findByCdiAccountNoAndCdiWorkTypeAndCdiInstallDate( + dto.getCdiAccountNo(), + dto.getCdiWorkType(), + dto.getCdiInstallDate() + ) + .orElse(null); + + // UPDATE (cdiUpdatedAt 은 @LastModifiedDate 로 자동 갱신) + if (existing != null) { + applyUpdate(existing, dto, customerId, driverId, customerDriverId); + return repository.save(existing); + } + + // CREATE CustomerDailyInstall entity = CustomerDailyInstall.builder() + .cdiUuid(UUID.randomUUID()) .cdiOrderSeq(dto.getCdiOrderSeq()) - .cdiDriverId(dto.getCdiDriverId()) + .cdiDriverId(driverId) .cdiInstallDate(dto.getCdiInstallDate()) .cdiInstallTime(dto.getCdiInstallTime()) - .cdiCustomerId(dto.getCdiCustomerId()) + .cdiCustomerId(customerId) .cdiAccountNo(dto.getCdiAccountNo()) .cdiCustomerName(dto.getCdiCustomerName()) .cdiPhone(dto.getCdiPhone()) @@ -36,13 +76,19 @@ public class CustomerDailyInstallService { .cdiPostalCode(dto.getCdiPostalCode()) .cdiGeoLat(dto.getCdiGeoLat()) .cdiGeoLon(dto.getCdiGeoLon()) + .cdiCustomerDriverId(customerDriverId) .cdiWorkType(dto.getCdiWorkType()) + .cdiOilPickup(dto.getCdiOilPickup()) + .cdiSludge(dto.getCdiSludge()) + .cdiRequestNote(dto.getCdiRequestNote()) + .cdiWorkNote(dto.getCdiWorkNote()) + .cdiInstallNote(dto.getCdiInstallNote()) + .cdiCsNote(dto.getCdiCsNote()) .cdiStatus(dto.getCdiStatus()) - .build(); + .cdiCreatedBy(createdBy) + .build(); // cdiCreatedAt / cdiUpdatedAt 은 auditing 이 자동 세팅 - entity = repository.save(entity); - - return mapToDto(entity); + return repository.save(entity); } public List getByInstallDate(LocalDate date) { @@ -59,7 +105,10 @@ public class CustomerDailyInstallService { CustomerDailyInstall entity = repository.findById(id) .orElseThrow(() -> new RuntimeException("CustomerDailyInstall not found")); - updateInternal(entity, dto); + Long driverId = resolveDriverId(dto.getCdiDriverId(), dto.getCdiExternalDriverId()); + Long customerDriverId = resolveDriverId(dto.getCdiCustomerDriverId(), dto.getCdiExternalCustomerDriverId()); + + applyUpdate(entity, dto, null, driverId, customerDriverId); return mapToDto(entity); } @@ -72,25 +121,141 @@ public class CustomerDailyInstallService { repository.delete(entity); } - private void updateInternal(CustomerDailyInstall entity, CustomerDailyInstallRequestDto dto) { - - if (dto.getCdiDriverId() != null) entity.setCdiDriverId(dto.getCdiDriverId()); - if (dto.getCdiInstallTime() != null) entity.setCdiInstallTime(dto.getCdiInstallTime()); - if (dto.getCdiStatus() != null) entity.setCdiStatus(dto.getCdiStatus()); - if (dto.getCdiWorkNote() != null) entity.setCdiWorkNote(dto.getCdiWorkNote()); + /** + * 자연키 (accountNo, workType, installDate) 로 조회. 없으면 null. + */ + public CustomerDailyInstall findByBusinessKey(String accountNo, String workType, LocalDate installDate) { + return repository + .findByCdiAccountNoAndCdiWorkTypeAndCdiInstallDate(accountNo, workType, installDate) + .orElse(null); } - private CustomerDailyInstallResponseDto mapToDto(CustomerDailyInstall entity) { + public void deleteEntity(CustomerDailyInstall entity) { + repository.delete(entity); + } + + /** + * 공통 업데이트 (null 이 아닌 값만 반영). + * customerId 는 자연키(account_no) 로 조회되므로 upsert 흐름에서만 재설정한다. + */ + private void applyUpdate( + CustomerDailyInstall entity, + CustomerDailyInstallRequestDto dto, + Long customerId, + Long driverId, + Long customerDriverId + ) { + if (dto.getCdiOrderSeq() != null) entity.setCdiOrderSeq(dto.getCdiOrderSeq()); + if (driverId != null) entity.setCdiDriverId(driverId); + if (customerId != null) entity.setCdiCustomerId(customerId); + if (dto.getCdiInstallTime() != null) entity.setCdiInstallTime(dto.getCdiInstallTime()); + if (dto.getCdiCustomerName() != null) entity.setCdiCustomerName(dto.getCdiCustomerName()); + if (dto.getCdiPhone() != null) entity.setCdiPhone(dto.getCdiPhone()); + if (dto.getCdiAddress() != null) entity.setCdiAddress(dto.getCdiAddress()); + if (dto.getCdiCity() != null) entity.setCdiCity(dto.getCdiCity()); + if (dto.getCdiPostalCode() != null) entity.setCdiPostalCode(dto.getCdiPostalCode()); + if (dto.getCdiGeoLat() != null) entity.setCdiGeoLat(dto.getCdiGeoLat()); + if (dto.getCdiGeoLon() != null) entity.setCdiGeoLon(dto.getCdiGeoLon()); + if (customerDriverId != null) entity.setCdiCustomerDriverId(customerDriverId); + if (dto.getCdiWorkType() != null) entity.setCdiWorkType(dto.getCdiWorkType()); + if (dto.getCdiOilPickup() != null) entity.setCdiOilPickup(dto.getCdiOilPickup()); + if (dto.getCdiSludge() != null) entity.setCdiSludge(dto.getCdiSludge()); + if (dto.getCdiRequestNote() != null) entity.setCdiRequestNote(dto.getCdiRequestNote()); + if (dto.getCdiWorkNote() != null) entity.setCdiWorkNote(dto.getCdiWorkNote()); + if (dto.getCdiInstallNote() != null) entity.setCdiInstallNote(dto.getCdiInstallNote()); + if (dto.getCdiCsNote() != null) entity.setCdiCsNote(dto.getCdiCsNote()); + if (dto.getCdiStatus() != null) entity.setCdiStatus(dto.getCdiStatus()); + } + + public CustomerDailyInstallResponseDto mapToDto(CustomerDailyInstall entity) { + + if (entity == null) return null; return CustomerDailyInstallResponseDto.builder() - .cdiUuid(entity.getCdiUuid()) + .cdiUuid(entity.getCdiUuid()) .cdiOrderSeq(entity.getCdiOrderSeq()) .cdiDriverId(entity.getCdiDriverId()) .cdiInstallDate(entity.getCdiInstallDate()) .cdiInstallTime(entity.getCdiInstallTime()) .cdiCustomerId(entity.getCdiCustomerId()) + .cdiAccountNo(entity.getCdiAccountNo()) .cdiCustomerName(entity.getCdiCustomerName()) + .cdiPhone(entity.getCdiPhone()) + .cdiAddress(entity.getCdiAddress()) + .cdiCity(entity.getCdiCity()) + .cdiPostalCode(entity.getCdiPostalCode()) + .cdiGeoLat(entity.getCdiGeoLat()) + .cdiGeoLon(entity.getCdiGeoLon()) + .cdiCustomerDriverId(entity.getCdiCustomerDriverId()) + .cdiWorkType(entity.getCdiWorkType()) + .cdiOilPickup(entity.getCdiOilPickup()) + .cdiSludge(entity.getCdiSludge()) + .cdiRequestNote(entity.getCdiRequestNote()) + .cdiWorkNote(entity.getCdiWorkNote()) + .cdiInstallNote(entity.getCdiInstallNote()) + .cdiCsNote(entity.getCdiCsNote()) .cdiStatus(entity.getCdiStatus()) + .cdiCreatedBy(entity.getCdiCreatedBy()) + .cdiCreatedAt(entity.getCdiCreatedAt()) + .cdiUpdatedAt(entity.getCdiUpdatedAt()) .build(); } -} \ No newline at end of file + + /** + * accountNo(=cus_no) → 내부 customerId 해석. MIS 는 di_customer_uid 를 모르므로 accountNo 로 찾는다. + */ + private Long resolveCustomerId(CustomerDailyInstallRequestDto dto) { + + // 1. ERP 내부 호출: customerId 직접 전달 + if (dto.getCdiCustomerId() != null) { + return dto.getCdiCustomerId(); + } + + // 2. MIS: accountNo == cus_no + if (dto.getCdiAccountNo() != null) { + return customerRepository.findByCusNo(dto.getCdiAccountNo()) + .map(c -> c.getCusId()) + .orElseThrow(() -> new RuntimeException("Customer not found by accountNo: " + dto.getCdiAccountNo())); + } + + return null; + } + + /** + * 내부 driverId 직접값 우선, 없으면 MIS externalId 를 HCM 에서 empId 로 변환. + */ + private Long resolveDriverId(Long directDriverId, String externalDriverId) { + + if (directDriverId != null) { + return directDriverId; + } + + if (externalDriverId != null) { + Map body = hcmEmployeeClient.getEmpFromExternalId("MIS_DRIVER", externalDriverId); + if (body != null && body.get("eexEmpId") instanceof Number num) { + return num.longValue(); + } + } + + return null; + } + + /** + * createdBy 해석: 로그인 사용자 > MIS member(empNo 변환) > 원본 문자열. + */ + private String resolveCreatedBy(CustomerDailyInstallRequestDto dto) { + + if (dto.getCdiLoginUser() != null) { + return dto.getCdiLoginUser(); + } + + if (dto.getCdiExternalCreatedBy() != null) { + Map body = hcmEmployeeClient.getEmpFromExternalId("MIS_MEMBER", dto.getCdiExternalCreatedBy()); + if (body != null && body.get("eexEmpNo") instanceof String empNo) { + return empNo; + } + } + + return dto.getCdiCreatedBy(); + } +}