# 영업관리 G4/G11 — 견적 결재상신 (Amaranth 직행) 검증 > 작성: 2026-05-11 / 작성자: hjjeong > 목적: wace 영업관리 견적 결재 흐름(외부 Amaranth SSO 직행)을 vexplor_rps 견적관리에 1:1 이식. G11 수주 결재상신과 동일 패턴, `target_type='CONTRACT_ESTIMATE'` + `target_objid=estimate_template.objid` 차이. ## 원본 출처 - 프론트: `wace_plm/WebContent/WEB-INF/view/contractMgmt/estimateList_new.jsp:154~270` (btnApproval 가드 + checkApprovalRequired 사전판정) - 프론트: 같은 파일 `:868~916` (`fn_showApprovalConfirmSimple` + `fn_openAmaranthApproval` → SSO URL 호출 → window.open) - 백엔드: `wace_plm/src/com/pms/service/ApprovalService.java:1782~1909` (`getAmaranthSsoUrl` — CONTRACT_ESTIMATE 분기는 1853~1854) - 매퍼: `wace_plm/src/com/pms/salesmgmt/mapper/contractMgmt.xml:513~522, 656~659` (APPR_STATUS 라벨 + LEFT JOIN AMARANTH_APPROVAL CONTRACT_ESTIMATE) ## G11(수주) 대비 차이점 | 항목 | 수주 (G11) | 견적 (이번 작업) | |---|---|---| | target_type | `CONTRACT_ORDER` | `CONTRACT_ESTIMATE` | | target_objid | `contract_mgmt.objid` (헤더 1개) | `estimate_template.objid` (최신 차수) | | formId | `1161` | `1162` | | 가드 | `has_order_data === 0` | `est_objid` 없음(견적서 미작성) | | 사전판정 | 없음 | wace는 `checkApprovalRequired` 분기 — **G4 영역**이라 이번 작업에서 제외 | 견적은 차수마다 별도 결재. 같은 영업번호의 차수1 결재 후 차수2 만들면 차수2는 다시 신규 amaranth_approval 매핑. ## 이식 위치 - 백엔드 서비스: `backend-node/src/services/salesEstimateService.ts:startEstimateApproval` - 백엔드 컨트롤러: `backend-node/src/controllers/salesEstimateController.ts:startApproval` - 라우트: `POST /api/sales/estimate/:id/amaranth-approval` - 프론트 API: `frontend/lib/api/salesEstimate.ts:startApproval` - 프론트 UI: `frontend/app/(main)/COMPANY_16/sales/estimate/page.tsx:handleAmaranthApproval` + "결재상신" 버튼 - 견적 list SQL 보강: 같은 파일 `salesEstimateService.getList` — `LEFT JOIN amaranth_approval AMR ON ET.objid::VARCHAR=AMR.target_objid AND AMR.target_type='CONTRACT_ESTIMATE'` + APPR_STATUS/AMARANTH_STATUS CASE 분기 - 재사용: `backend-node/src/services/amaranthApprovalClient.ts:getSsoUrl` (chpark, G11에서 도입) ## 결재 정책 (wace 1:1) - target_type: `CONTRACT_ESTIMATE` - formId: `1162` (운영 amaranth 견적 양식 ID — 수주 1161과 별도) - compSeq: `1000` - mod: `W` - empSeq 출처: `user_info.emp_seq` - approKey 분기 (G11 동일): - 신규: `UB_` + Date.now().toString(36).toUpperCase() - 기존 reject/delete/create: 새 approKey + amaranth_approval UPDATE (재상신) - 기존 inProcess/complete: 기존 approKey 재사용 (프론트에서 차단되지만 백엔드 방어) ## 환경변수 (운영 배포 시 주입) | 변수 | 기본값 | 비고 | |---|---|---| | `AMARANTH_OUT_PROCESS_CODE_CONTRACT_ESTIMATE` | (없음) | 견적 결재 전용 코드. 미설정 시 `AMARANTH_OUT_PROCESS_CODE` fallback | | `AMARANTH_FORM_ID_CONTRACT_ESTIMATE` | `1162` | wace 운영값 | | `AMARANTH_COMP_SEQ` | `1000` | wace 운영값 (수주와 공유) | 3개 docker-compose(`deploy/onpremise`, `docker/deploy`, `docker/prod/docker-compose.backend.prod.yml`) 모두 `${VAR:-default}` 형식으로 매핑됨 — 호스트 .env에 아무것도 안 넣어도 wace 운영값 그대로 동작. ## 가드 (프론트 + 백엔드 동시) | 조건 | 메시지 | 처리 | |---|---|---| | 행 미선택 | "결재상신할 행을 선택해주십시오." | 프론트 toast | | `est_objid` 없음 | "견적서를 먼저 작성해주세요." | 프론트 toast + 백엔드 400 | | `amaranth_status === 'inProcess'` | "결재 진행중인 건은 상신할 수 없습니다." | 프론트 toast | | `amaranth_status === 'complete'` | "결재 완료된 건은 상신할 수 없습니다." | 프론트 toast | | `amaranth_status === 'notRequired'` 또는 `approval_required === 'N'` | "결재불필요로 처리된 건입니다." | 프론트 toast | | 사용자 emp_seq 미설정 | "empSeq 정보가 없습니다." | 백엔드 400 | | 원본 contract_mgmt 부재 | "견적을 찾을 수 없습니다." | 백엔드 404 | | SSO API resultCode != 0 | "결재 연동 오류: ..." | 백엔드 502 | ## 검증 SQL (BEGIN/ROLLBACK) ```sql BEGIN; -- 가짜 amaranth_approval 매핑 INSERT (CONTRACT_ESTIMATE, target_objid = est_objid) INSERT INTO amaranth_approval (objid, target_objid, target_type, appro_key, out_process_code, form_id, status, emp_seq, comp_seq, dept_seq, writer, sso_url, regdate) VALUES (9999999998, '-452406811', 'CONTRACT_ESTIMATE', 'UB_TEST_EST', 'RPSPLM_00001', '1162', 'create', '999', '1000', '', 'test', 'http://test', NOW()); -- 4단계 상태 라벨 확인 (UPDATE를 반복) SELECT CASE WHEN AMR.status = 'complete' THEN '결재완료' WHEN AMR.status = 'inProcess' THEN '결재중' WHEN AMR.status = 'reject' THEN '반려' WHEN AMR.status = 'create' THEN '작성중' WHEN AMR.status = 'notRequired' THEN '결재불필요' WHEN COALESCE(T.APPROVAL_REQUIRED, 'N') = 'N' THEN '결재불필요' ELSE '' END AS appr_status, AMR.status, T.contract_no FROM contract_mgmt T LEFT JOIN LATERAL ( SELECT objid FROM estimate_template WHERE contract_objid = T.objid ORDER BY regdate DESC LIMIT 1 ) ET ON true LEFT JOIN amaranth_approval AMR ON AMR.target_objid = ET.objid::VARCHAR AND AMR.target_type = 'CONTRACT_ESTIMATE' WHERE T.objid = '-912974684'; UPDATE amaranth_approval SET status='inProcess' WHERE objid=9999999998; -- 위 SELECT 재실행 → '결재중' UPDATE amaranth_approval SET status='complete' WHERE objid=9999999998; -- → '결재완료' UPDATE amaranth_approval SET status='reject' WHERE objid=9999999998; -- → '반려' DELETE FROM amaranth_approval WHERE objid=9999999998; -- → '결재불필요' (approval_required='N' fallback) ROLLBACK; ``` ### 검증 결과 (2026-05-11) 샘플 contract: `26C-0712` (contract_objid=`-912974684`, est_objid=`-452406811`, approval_required='N') | AMR.status | appr_status (한글) | |---|---| | `create` | 작성중 | | `inProcess` | 결재중 | | `complete` | 결재완료 | | `reject` | 반려 | | (AMR row 삭제, approval_required='N') | 결재불필요 | - AMR row가 있으면 status 우선, 없으면 approval_required fallback — wace 1:1. - ROLLBACK 후 운영 데이터 영향 없음. - 견적 list SQL의 LEFT JOIN AMR 동작 확인 — 차수가 늘어나도 최신 차수만 매핑. ## API 호출 ```bash curl -X POST 'http://localhost:8080/api/sales/estimate//amaranth-approval' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{"approvalTitle":"견적서 결재 - 26C-0712"}' # 성공: {"success":true,"data":{"fullUrl":"https://...","approKey":"UB_...","status":"create","estObjid":"-452406811"}} # 실패: {"success":false,"message":"견적서를 먼저 작성해주세요."} ``` ## UI 동작 1. 견적관리 그리드에서 행 1개 선택 2. "결재상신" 버튼 클릭 (메일발송 옆, sky-600) 3. 가드 통과 → 확인 다이얼로그: "결재상신 하시겠습니까?" 4. 확인 → API 호출 → `window.open(fullUrl, "amaranthApproval", "width=1200,height=900,...")` 5. 외부 Amaranth 결재 페이지에서 사용자가 양식 작성 + 상신 6. 목록 새로고침 → "결재상태" 컬럼이 '작성중' → '결재중' → '결재완료' 순으로 변화 ## 미구현 (백로그) - **첨부파일 원챔버 업로드** — wace `uploadEstimateFilesToOneChamber` (영업관리 첨부 흐름 별도 작업 후 연동) - **사전판정 (`checkApprovalRequired`)** — G4 영역. wace는 결재상신 클릭 → 재오더/신규수주/가격인하 룰 판정 → 재오더면 "결재불필요" 자동 처리, 신규수주/가격인하면 사유 안내 후 결재상신. 이번 작업은 G11 동일 흐름(단순 SSO)만. - **결재 콜백** — amaranth가 우리 시스템에 결재 결과를 통보하는 webhook (운영에서는 폴링 또는 amaranth_approval 수동 갱신) ## 운영 트러블슈팅 (G11과 공유) dev에서 wace 계정으로 결재상신 클릭 시 amaranth가 다음 메시지로 거부 가능: ``` 인증 토큰 발급 실패: API Proxy 호출 시 유효한 레디스 값이 존재하지 않습니다. ``` 원인은 'Amaranth - 결재' 커넥션 accessToken이 amaranth 측 Redis 캐시에 등록 안 된 상태(7개 amaranth 커넥션 중 결재 토큰만 별도). G11 작업 시 동일 현상 확인됨. 대응 = chpark 또는 RPS ERP 담당자에게 결재 토큰 Redis 등록 요청 (자세한 진단: `07-amaranth-approval-verify.md` 트러블슈팅 섹션). **코드 변경 없음** — 운영 협조로 해결되는 영역.