# 공급사 재협상 요청 (IMK #15) > 요구 원문: "협상결렬 건에 한하여 해당 공급사측 재협상 기능 추가 및 재협상 현황 확인·승인 화면 추가 요청" 기존 재생성은 **담당자가 먼저** 마감 견적에서 공급사를 골라 다음 라운드를 만드는 방향이다(`regenerate_quotation`). #15는 방향이 반대다 — **공급사가 먼저 요청하고 담당자가 승인**하면 그 결과로 다음 라운드가 생성된다. 따라서 재생성 로직 자체는 재사용하고, 그 앞에 "요청 → 심사" 단계를 붙인다. ## 1. 용어와 범위 | 용어 | 정의 | |---|---| | 결렬 건 | 낙찰되지 못한 채 마감된 건. `quotations.status=CLOSED` 且 `close_reason ∈ {OPEN_PRICE(5), OPEN_EQUAL(6), OPEN_NOSHOW(7), OPEN_REJECT(8)}` | | 요청 자격 | 그 견적의 **마지막 라운드**에 세션이 있는 공급사 본인 | | 승인 결과 | 원 견적의 다음 라운드 생성(`regenerate_quotation` 과 동일 경로), 요청 공급사 포함 | ### 요청 가능 조건 (전부 만족) 1. 견적이 마감(`CLOSED`)이고 `close_reason` 이 `OPEN_*` — **낙찰(AWARDED) 건은 불가** 2. 요청자가 그 견적 **마지막 라운드**의 세션 보유자 3. 같은 세션에 **대기(PENDING) 상태 요청이 없음** (중복 방지) 4. 해당 견적 체인에 **더 뒤 라운드가 아직 없음** (이미 재생성됐으면 요청 의미 없음) 거부(`REJECTED`)·미참여(`NOT_PARTICIPATED`) 세션도 요청은 허용한다 — 단종·품절로 거부했다가 조건이 풀리는 경우가 실제로 있다. ## 2. 상태 흐름 ``` [공급사] 결렬 건 확인 → 재협상 요청(사유·희망가) → PENDING │ [담당자] 요청 현황 화면에서 심사 │ ├─ 승인 → APPROVED → 다음 라운드 생성 → 초청메일 └─ 반려 → REJECTED (사유 기록, 공급사에 노출) ``` 요청 상태(`sessions.custom.renegotiation.status`) | 코드 | 상태 | 설명 | |---|---|---| | 1 | PENDING | 접수, 담당자 심사 대기 | | 2 | APPROVED | 승인 — 다음 라운드 생성 완료 | | 3 | REJECTED | 반려 — 사유 기록 | | 4 | CANCELED | 공급사가 스스로 철회(PENDING 일 때만) | ## 3. 저장 위치 — `sessions.custom` (DDL 0) **신규 테이블을 만들지 않는다.** 재협상 요청은 세션당 1건이라 기존 `negotiation.sessions.custom` JSONB 에 `renegotiation` 키로 얹는다. 같은 컬럼의 협상완료 부가정보와 키가 갈리므로 서로 덮어쓰지 않는다. ```json { "std_lead_time": 30, // 기존: 협상완료 부가정보(session_fields 정의대로) "renegotiation": { "status": 1, // 1=PENDING 2=APPROVED 3=REJECTED 4=CANCELED "reason": "가격 조건 재검토", "desired_price": 15000000, // 선택 "requested_at": "2026-07-23T10:00:00Z", "decided_by": "", // 심사 후 채움 "decided_at": "2026-07-23T11:00:00Z", "memo": "목표가 대비 격차가 커 반려", "next_quotation_id": "" } } ``` 조회는 `sessions` 를 견적·공급사와 조인하면서 `custom -> 'renegotiation' ->> 'status'` 로 거른다. 세션 규모(수십~수백)에서 전용 인덱스 없이 충분하다. 느려지면 다음 순으로 올린다. 1. `custom` 에 GIN 인덱스 2. 그래도 부족하면 전용 테이블(`renegotiation_requests`)로 승격 — `custom` 값을 그대로 옮기면 되므로 되돌리기 부담이 작다 ### 이 방식의 제약(수용 범위) - **요청 이력은 최신 1건만 남는다.** 반려 후 재요청하면 이전 기록을 덮어쓴다. 이력이 필요해지는 시점 = 테이블 승격 시점. - 세션당 대기 요청 1건 제약은 DB 유니크가 아니라 **API 검증**으로 건다(기존 값이 PENDING 이면 거부). - 쓰기는 반드시 `execute_lambda_run` 으로 — `execute_lambda` 는 커밋하지 않아 값이 조용히 사라진다. ## 4. API ### 공급사 포털 (backend) | 메서드 | 경로 | 설명 | |---|---|---| | `GET` | `/v1/negotiation/renegotiation/eligible` | 요청 가능한 결렬 건 목록(본인 공급사) | | `POST` | `/v1/negotiation/session/{session_id}/renegotiation` | 요청 생성 `{reason, desired_price?}` | | `GET` | `/v1/negotiation/renegotiation` | 내 요청 목록(상태·심사결과 포함) | | `DELETE` | `/v1/negotiation/session/{session_id}/renegotiation` | 철회(PENDING 일 때만) | ### 관리자 (negodata backend) | 메서드 | 경로 | 설명 | |---|---|---| | `GET` | `/v1/renegotiation/list` | 요청 현황(상태·견적·공급사 필터, 페이지네이션) | | `POST` | `/v1/renegotiation/{session_id}/approve` | 승인 `{supplier_ids?}` — 미지정 시 요청자만 | | `POST` | `/v1/renegotiation/{session_id}/reject` | 반려 `{memo}` | 식별자는 별도 request_id 가 아니라 **`session_id`** 다 — 세션당 요청 1건이므로 그것으로 충분하다. 권한: 승인·반려는 **해당 견적 소유자 ∪ 최고관리자**(기존 `is_owner_or_admin` 그대로). ## 5. 승인 처리 로직 1. 요청 상태가 `PENDING` 인지 확인(아니면 `INVALID_REQUEST_DATA`) 2. 대상 견적이 여전히 마지막 라운드인지 재확인 — 그 사이 담당자가 수동 재생성했을 수 있다 3. `regenerate_quotation(qt_id, company_id, supplier_ids, ...)` 호출 - `supplier_ids` 기본값 = 요청 공급사 1곳 - 담당자가 화면에서 다른 공급사를 추가로 체크하면 함께 포함(같은 라운드에 묶음) 4. 성공 시 요청을 `APPROVED` 로, `next_quotation_id`·`decided_by`·`decided_at` 기록 5. 생성된 라운드는 **기존과 동일하게 초청메일 수동 발송** — 자동 발송하지 않는다(현행 정책 유지) 같은 견적에 대기 요청이 여러 건이면, 하나를 승인할 때 나머지도 함께 처리할지 담당자가 선택한다(기본: 함께 승인하여 한 라운드에 묶음). ## 6. 화면 ### 공급사 포털 - **협상 목록**: 결렬 건 행에 `재협상 요청` 버튼. 이미 요청했으면 상태 배지(`심사 중` / `승인됨` / `반려됨`)로 대체 - **요청 모달**: 사유 선택(프리셋: 가격 조건 재검토 / 재고·납기 확보 / 단가 정정 / 직접 입력) + 희망가(선택) + 안내 문구 - 반려된 경우 배지에 담당자 메모를 툴팁으로 노출 ### 관리자 - **신규 메뉴 `재협상 요청`**(사이드바 업무 그룹). 목록 컬럼: 요청일시 · 견적번호/차수 · 상품 · 공급사 · 사유 · 희망가 · 상태 · 심사자 - 상태 탭: `대기` / `승인` / `반려` — 기본 `대기` - 행 클릭 → 심사 패널: 원 견적 요약(목표가·최저 투찰가·마감사유), 요청 사유·희망가, `승인` / `반려(사유 입력)` - 승인 시 함께 포함할 공급사 체크박스(기본: 요청자만) - **대시보드**: 대기 건수 카드 추가 — 방치 방지 ## 7. 알림 - 요청 접수 → 견적 담당자에게 알림(기존 notifications 테이블 재사용) - 승인/반려 → 공급사 포털 목록에서 상태로 확인. 메일은 승인 시 생성되는 **초청메일로 대체**(중복 발송 방지) ## 8. 구현 순서 1. (DDL 없음) 요청 payload 형태를 양쪽 백엔드에서 공유하는 상수/헬퍼로 정리 2. 공급사 포털 API(요청 생성·조회·철회) + 자격 판정 3. 관리자 API(목록·승인·반려) + 승인 시 `regenerate_quotation` 연결 4. 관리자 화면(메뉴·목록·심사 패널·대시보드 카드) 5. 공급사 포털 화면(버튼·모달·상태 배지) 6. 알림 연결 ## 9. 미결 (IMK 확인 필요) - **요청 횟수 제한**: 같은 견적 체인에서 공급사가 몇 번까지 요청 가능한가? (제안: 체인당 1회) - **요청 기한**: 마감 후 며칠까지 허용? (제안: 7일) - **자동 승인 옵션**: 담당자 심사 없이 자동 재생성하는 회사 설정이 필요한가? (제안: 초기엔 없음 — 승인 필수) - **희망가 노출 범위**: 담당자에게만인지, 협상 봇의 앵커링에 반영할지 (제안: 담당자 판단 근거로만, 봇 미반영) - **요청 이력 보존 필요 여부**: 반려 후 재요청 이력을 남겨야 하면 `sessions.custom` 대신 전용 테이블이 필요하다 (제안: 초기엔 최신 1건만)