- UserRole 1=일반 2=최고관리자 3=개발자(내부 운영). 개발자 계정은 회원 목록·총계에서 제외해 고객사에 노출하지 않음 - 계정 생성 시 권한 선택(일반/최고관리자) 추가, 개발자는 앱에서 부여 불가(DB 시드 전용) - 사이드바 '개발자' 그룹 신설 — 회사 설정·디자인 시스템을 개발자에게만 노출 - /dev/design 디자인 시스템 페이지: 색 토큰·타이포·버튼·배지·입력·반경, 목록 화면 구조, 반응형 기준, URL 상태 규칙, 회사 커스터마이징 훅 - 공급사 재협상 요청(#15) 플로우 문서 — sessions.custom 기반(DDL 0)
8.4 KiB
공급사 재협상 요청 (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 과 동일 경로), 요청 공급사 포함 |
요청 가능 조건 (전부 만족)
- 견적이 마감(
CLOSED)이고close_reason이OPEN_*— 낙찰(AWARDED) 건은 불가 - 요청자가 그 견적 마지막 라운드의 세션 보유자
- 같은 세션에 대기(PENDING) 상태 요청이 없음 (중복 방지)
- 해당 견적 체인에 더 뒤 라운드가 아직 없음 (이미 재생성됐으면 요청 의미 없음)
거부(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' 로 거른다.
세션 규모(수십~수백)에서 전용 인덱스 없이 충분하다. 느려지면 다음 순으로 올린다.
custom에 GIN 인덱스- 그래도 부족하면 전용 테이블(
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. 승인 처리 로직
- 요청 상태가
PENDING인지 확인(아니면INVALID_REQUEST_DATA) - 대상 견적이 여전히 마지막 라운드인지 재확인 — 그 사이 담당자가 수동 재생성했을 수 있다
regenerate_quotation(qt_id, company_id, supplier_ids, ...)호출supplier_ids기본값 = 요청 공급사 1곳- 담당자가 화면에서 다른 공급사를 추가로 체크하면 함께 포함(같은 라운드에 묶음)
- 성공 시 요청을
APPROVED로,next_quotation_id·decided_by·decided_at기록 - 생성된 라운드는 기존과 동일하게 초청메일 수동 발송 — 자동 발송하지 않는다(현행 정책 유지)
같은 견적에 대기 요청이 여러 건이면, 하나를 승인할 때 나머지도 함께 처리할지 담당자가 선택한다(기본: 함께 승인하여 한 라운드에 묶음).
6. 화면
공급사 포털
- 협상 목록: 결렬 건 행에
재협상 요청버튼. 이미 요청했으면 상태 배지(심사 중/승인됨/반려됨)로 대체 - 요청 모달: 사유 선택(프리셋: 가격 조건 재검토 / 재고·납기 확보 / 단가 정정 / 직접 입력) + 희망가(선택) + 안내 문구
- 반려된 경우 배지에 담당자 메모를 툴팁으로 노출
관리자
- 신규 메뉴
재협상 요청(사이드바 업무 그룹). 목록 컬럼: 요청일시 · 견적번호/차수 · 상품 · 공급사 · 사유 · 희망가 · 상태 · 심사자 - 상태 탭:
대기/승인/반려— 기본대기 - 행 클릭 → 심사 패널: 원 견적 요약(목표가·최저 투찰가·마감사유), 요청 사유·희망가,
승인/반려(사유 입력) - 승인 시 함께 포함할 공급사 체크박스(기본: 요청자만)
- 대시보드: 대기 건수 카드 추가 — 방치 방지
7. 알림
- 요청 접수 → 견적 담당자에게 알림(기존 notifications 테이블 재사용)
- 승인/반려 → 공급사 포털 목록에서 상태로 확인. 메일은 승인 시 생성되는 초청메일로 대체(중복 발송 방지)
8. 구현 순서
- (DDL 없음) 요청 payload 형태를 양쪽 백엔드에서 공유하는 상수/헬퍼로 정리
- 공급사 포털 API(요청 생성·조회·철회) + 자격 판정
- 관리자 API(목록·승인·반려) + 승인 시
regenerate_quotation연결 - 관리자 화면(메뉴·목록·심사 패널·대시보드 카드)
- 공급사 포털 화면(버튼·모달·상태 배지)
- 알림 연결
9. 미결 (IMK 확인 필요)
- 요청 횟수 제한: 같은 견적 체인에서 공급사가 몇 번까지 요청 가능한가? (제안: 체인당 1회)
- 요청 기한: 마감 후 며칠까지 허용? (제안: 7일)
- 자동 승인 옵션: 담당자 심사 없이 자동 재생성하는 회사 설정이 필요한가? (제안: 초기엔 없음 — 승인 필수)
- 희망가 노출 범위: 담당자에게만인지, 협상 봇의 앵커링에 반영할지 (제안: 담당자 판단 근거로만, 봇 미반영)
- 요청 이력 보존 필요 여부: 반려 후 재요청 이력을 남겨야 하면
sessions.custom대신 전용 테이블이 필요하다 (제안: 초기엔 최신 1건만)