o2o-negosium-original/negodata/docs/renegotiation-flow.md
Mina Choi cfee6e89d0 [feat] negodata: 권한 3단계(개발자 신설)·개발자 메뉴 그룹·디자인 시스템 페이지
- UserRole 1=일반 2=최고관리자 3=개발자(내부 운영). 개발자 계정은 회원 목록·총계에서 제외해 고객사에 노출하지 않음
- 계정 생성 시 권한 선택(일반/최고관리자) 추가, 개발자는 앱에서 부여 불가(DB 시드 전용)
- 사이드바 '개발자' 그룹 신설 — 회사 설정·디자인 시스템을 개발자에게만 노출
- /dev/design 디자인 시스템 페이지: 색 토큰·타이포·버튼·배지·입력·반경, 목록 화면 구조, 반응형 기준, URL 상태 규칙, 회사 커스터마이징 훅
- 공급사 재협상 요청(#15) 플로우 문서 — sessions.custom 기반(DDL 0)
2026-07-23 15:41:36 +09:00

8.4 KiB
Raw Permalink Blame History

공급사 재협상 요청 (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 키로 얹는다. 같은 컬럼의 협상완료 부가정보와 키가 갈리므로 서로 덮어쓰지 않는다.

{
  "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": "<user_id>",   // 심사 후 채움
    "decided_at": "2026-07-23T11:00:00Z",
    "memo": "목표가 대비 격차가 커 반려",
    "next_quotation_id": "<qt_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건만)