From 9dca78dc72fedf00bd98e22ad4fb040dfbd5a10e Mon Sep 17 00:00:00 2001 From: Mina Choi Date: Wed, 29 Jul 2026 17:28:56 +0900 Subject: [PATCH] =?UTF-8?q?[feat]=20negosium:=20=ED=98=91=EC=83=81?= =?UTF-8?q?=EC=99=84=EB=A3=8C=20=EB=B6=80=EA=B0=80=EC=A0=95=EB=B3=B4=20?= =?UTF-8?q?=EC=88=98=EC=B7=A8=C2=B7=ED=91=9C=EC=8B=9C=C2=B7=EC=9E=A0?= =?UTF-8?q?=EA=B8=88=20+=20VAT=20=ED=91=9C=EA=B8=B0=20=ED=86=B5=EC=9D=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 완료 요약 카드(투찰/협상 결과)에 부가정보·의견을 항상 구조로 표시, 미입력 필드는 '-' - 협상 리스트 부가정보를 완전 읽기전용으로 잠금 — 입력은 완료 챗 마무리에서만 - 어드민 협상상세(현황·챗 탭)에 협력사 의견(custom.opinion) 노출 - 협상 화면 VAT 표기를 회사설정(vat_yn) 기준으로 통일 — hidden이면 라벨 생략 --- backend/services/chat_service.py | 5 ++ .../features/chat/components/ExtraInfoBar.tsx | 6 +- .../features/chat/components/ItemSection.tsx | 4 +- .../chat/components/templates/BidSummary.tsx | 37 +++++++++-- .../chat/components/templates/Summary.tsx | 14 ++++- .../list/components/ExtraInfoPopup.tsx | 61 +++++++++++++------ .../list/components/WorkspaceCards.tsx | 3 +- .../list/components/WorkspaceTable.tsx | 3 +- .../QuotationDetailSheet/ChatTab.tsx | 11 +++- .../SessionsStatusTab.tsx | 11 +++- 10 files changed, 115 insertions(+), 40 deletions(-) diff --git a/backend/services/chat_service.py b/backend/services/chat_service.py index 572ffe2..6f7691c 100644 --- a/backend/services/chat_service.py +++ b/backend/services/chat_service.py @@ -260,6 +260,11 @@ class ChatService: lambda s: self.user_crud.get_company_settings(s, sess.supplier_id), ) res.labels = (settings.get("labels") or {}) if _e == ErrorType.SUCCESS and settings else {} + + # 회사가 VAT(vat_yn)를 관리하지 않으면(hidden_fields) 협상 화면 VAT 표기를 숨긴다(값 null → 프론트 라벨 생략). + _hidden = (settings.get("hidden_fields") or []) if _e == ErrorType.SUCCESS and settings else [] + if "vat_yn" in _hidden: + res.item_vat_yn = None return res async def _ensure_in_progress(self, sess, quote) -> None: diff --git a/frontend/src/features/chat/components/ExtraInfoBar.tsx b/frontend/src/features/chat/components/ExtraInfoBar.tsx index f79b2f8..ba189a3 100644 --- a/frontend/src/features/chat/components/ExtraInfoBar.tsx +++ b/frontend/src/features/chat/components/ExtraInfoBar.tsx @@ -15,6 +15,7 @@ export function ExtraInfoBar({ proceedText }: { proceedText: string }) { const fields: SessionField[] = user?.sessionFields ?? [] const save = useSaveExtraInfoMutation() const existing = useChatInitStore((s) => s.custom) // 기존 입력값(재진입 프리필) + const setInitData = useChatInitStore((s) => s.setInitData) // 의견은 session_fields 와 무관한 공통 필드 — 타결·결렬 모두 항상 받는다. custom.opinion 에 저장. const [opinion, setOpinion] = useState(null) const opinionValue = opinion ?? String(existing?.opinion ?? '') @@ -42,7 +43,10 @@ export function ExtraInfoBar({ proceedText }: { proceedText: string }) { save.mutate( { sessionId, request: { custom } }, { - onSuccess: () => proceed(), + onSuccess: () => { + setInitData({ custom: { ...existing, ...custom } }) // 요약 카드가 방금 입력값을 즉시 반영하도록 store 갱신 + proceed() + }, onError: (error) => toast.error(getApiErrorMessage(error, '부가정보 저장에 실패했습니다.')), }, ) diff --git a/frontend/src/features/chat/components/ItemSection.tsx b/frontend/src/features/chat/components/ItemSection.tsx index 40db790..a2fc245 100644 --- a/frontend/src/features/chat/components/ItemSection.tsx +++ b/frontend/src/features/chat/components/ItemSection.tsx @@ -44,6 +44,7 @@ function ItemInfo() { item_lead_time, item_spec, item_name, + item_vat_yn, labels, } = useChatInitStore() // 회사 설정 라벨(companies.settings.labels)로 필드명 치환. 미설정 시 기본 라벨. @@ -73,7 +74,8 @@ function ItemInfo() { const formatPrice = (price: number) => price.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',') const isNewItem = !item_code && !item_price const priceText = isNewItem ? '신규' : `${formatPrice(item_price)}원` - const formattedPrice = `${priceText}(VAT별도)` // 협상 화면 상품 단가는 VAT별도 표기(IMK #11) + // VAT 표기는 회사 설정 기준(item_vat_yn). vat_yn 미관리(hidden)면 값이 비어 라벨을 생략한다. + const formattedPrice = item_vat_yn ? `${priceText}(${item_vat_yn})` : priceText const renderRow = (title: string, data: string) => { const displayData = data || '-' diff --git a/frontend/src/features/chat/components/templates/BidSummary.tsx b/frontend/src/features/chat/components/templates/BidSummary.tsx index 5d2c9cf..b43f4ef 100644 --- a/frontend/src/features/chat/components/templates/BidSummary.tsx +++ b/frontend/src/features/chat/components/templates/BidSummary.tsx @@ -1,17 +1,40 @@ import { CheckCircle2 } from 'lucide-react' import { numberToKorean } from '@/features/chat/lib/koreanNumber' +import { useMeQuery } from '@/apis' +import type { SessionField } from '@/apis/auth/auth.type' +import { useChatInitStore } from '@/features/chat/stores/useChatInitStore' interface BidSummaryProps { itemName: string itemCode: string bidPrice: number - deliveryType: string - isVAT: boolean +} + +// 부가정보 값 표시 포맷(불리언→예/아니오, 숫자→천단위, 그 외→문자열) +function formatFieldValue(type: SessionField['type'], value: unknown): string { + if (type === 'boolean') return value ? '예' : '아니오' + if (type === 'number') return Number(value).toLocaleString() + return String(value) } // 투찰 결과 요약 카드 (보고서식 final-summary) -export function BidSummary({ itemName, itemCode, bidPrice, deliveryType, isVAT }: BidSummaryProps) { +// 공급사가 마무리에서 입력한 부가정보(회사설정 session_fields + 의견)가 있으면 함께 표시한다(입력완료 시 요약 갱신). +export function BidSummary({ itemName, itemCode, bidPrice }: BidSummaryProps) { + // VAT 표기는 회사 설정 기준(item_vat_yn). vat_yn 미관리(hidden)면 비어 라벨을 생략한다. + const item_vat_yn = useChatInitStore((s) => s.item_vat_yn) + const custom = useChatInitStore((s) => s.custom) + const { data: user } = useMeQuery() + const fields: SessionField[] = user?.sessionFields ?? [] const priceStr = (bidPrice || 0).toLocaleString() + + // 부가정보(회사설정 session_fields) — 완료 요약엔 구조를 항상 노출(미입력 값은 '-') + const extraRows = fields.map((f) => { + const v = custom?.[f.key] + const has = v !== undefined && v !== null && v !== '' + return { key: f.key, label: f.label, value: has ? formatFieldValue(f.type, v) : '-' } + }) + const opinion = custom?.opinion ? String(custom.opinion) : '' + return (
@@ -27,10 +50,14 @@ export function BidSummary({ itemName, itemCode, bidPrice, deliveryType, isVAT } {priceStr}원 ({numberToKorean(bidPrice || 0)}원) - {isVAT ? 'VAT포함' : 'VAT별도'} + {item_vat_yn ? {item_vat_yn} : null}
- + {extraRows.map((r) => ( + + ))} + {/* 의견은 타결·결렬 공통 기본 필드 — 값이 없어도 항상 표시 */} +
) diff --git a/frontend/src/features/chat/components/templates/Summary.tsx b/frontend/src/features/chat/components/templates/Summary.tsx index 433c1e8..2e8b1a7 100644 --- a/frontend/src/features/chat/components/templates/Summary.tsx +++ b/frontend/src/features/chat/components/templates/Summary.tsx @@ -1,11 +1,17 @@ import { CheckCircle2 } from 'lucide-react' import { numberToKorean } from '@/features/chat/lib/koreanNumber' import { formatLeadTime } from '@/features/chat/lib/format' +import { useChatInitStore } from '@/features/chat/stores/useChatInitStore' import type { ChatSummary } from '@/features/chat/types' // 협상 결과 요약 카드 export function Summary({ data }: { data: ChatSummary }) { const priceStr = (data.final_price || 0).toLocaleString() + // VAT 표기는 회사 설정 기준(item_vat_yn). vat_yn 미관리(hidden)면 비어 라벨을 생략한다. + const item_vat_yn = useChatInitStore((s) => s.item_vat_yn) + // 공급사가 마무리에서 남긴 의견(있을 때만 표시) + const custom = useChatInitStore((s) => s.custom) + const opinion = custom?.opinion ? String(custom.opinion) : '' return (
@@ -43,16 +49,18 @@ export function Summary({ data }: { data: ChatSummary }) { {priceStr}원 ({numberToKorean(data.final_price || 0)}원) - {data.item_isVAT ? 'VAT포함' : 'VAT별도'} + {item_vat_yn ? {item_vat_yn} : null}
+ {/* 의견은 타결·결렬 공통 기본 필드 — 값이 없어도 항상 표시 */} + - {/* 계약 / 담당 MD */} + {/* 계약 / 구매담당자 */}

공급 계약 기간: 협상 완료일로부터 1년 ({addOneYear(data.nego_end_date)})까지

- 담당 MD: {data.md_name || '-'} ({data.md_phone_number || '-'}) {data.md_email || '-'} + 구매담당자: {data.md_name || '-'} ({data.md_phone_number || '-'}) {data.md_email || '-'}

diff --git a/frontend/src/features/list/components/ExtraInfoPopup.tsx b/frontend/src/features/list/components/ExtraInfoPopup.tsx index 9e36ee2..10cfad0 100644 --- a/frontend/src/features/list/components/ExtraInfoPopup.tsx +++ b/frontend/src/features/list/components/ExtraInfoPopup.tsx @@ -23,6 +23,8 @@ export function ExtraInfoPopup({ target, onClose, onSubmit }: ExtraInfoPopupProp return init }) const [opinion, setOpinion] = useState(() => String(target.custom?.opinion ?? '')) + // 부가정보는 협상 완료(동의) 시점에 확정 — 리스트에서는 항상 읽기전용(보기). 입력은 완료 챗에서만. + const readOnly = true const set = (key: string, value: unknown) => setValues((v) => ({ ...v, [key]: value })) @@ -44,8 +46,11 @@ export function ExtraInfoPopup({ target, onClose, onSubmit }: ExtraInfoPopupProp
-

협상완료 부가정보

-

{target.qt_number} · {target.item_name}

+

협상완료 부가정보{readOnly ? ' (보기)' : ''}

+

+ {target.qt_number} · {target.item_name} + {readOnly ? ' · 제출 후에는 수정할 수 없습니다.' : ''} +