import { X, Check } from 'lucide-react'; import { Typography } from '@/components/ui/typography'; import { useGetTargetBreakdown } from '@/api/generated/quotation/quotation'; import { useScrollLock } from '@/lib/useScrollLock'; import { useLabels, useVatMode } from '@/features/settings/useCompanySettings'; // 세션 목표가 산정내역 모달. 후보·채택·앵커링가는 백엔드 /target-breakdown 이 산정한 값을 '표시만' 한다. // (프론트 재계산 없음 → 저장된 목표가와 항상 일치. 산정 로직은 백엔드 _candidates 단일 출처.) type TargetPriceModalProps = { onClose: () => void; sessionId: string; qtNumber: string; itemName: string; vatYn?: boolean | null; deliveryFeeYn?: boolean | null; category?: string | null; }; const won = (n?: number | null) => (n != null ? `₩${n.toLocaleString()}` : '-'); // 후보 basis 별 부가 설명(수수료/마진 적용 표시). 백엔드 라벨에 없는 보조 문구만 프론트가 덧붙인다. // 회사 설정 용어(목표 마진)를 반영해 컴포넌트 안에서 만든다. const candidateSub = (marginLabel: string): Record => ({ internet: '인터넷 평균 수수료 적용', purchase: `${marginLabel} 적용`, selling: `${marginLabel} 적용`, purchase_raw: `${marginLabel} 미적용`, selling_raw: `${marginLabel} 미적용`, }); export function TargetPriceModal({ onClose, sessionId, qtNumber, itemName, vatYn, deliveryFeeYn, category, }: TargetPriceModalProps) { useScrollLock(); // 모달은 열릴 때만 마운트(부모 게이트) → 배경 스크롤 잠금 const label = useLabels(); // 회사 설정 용어 // 부가세 전체 통일 회사는 상품 vat_yn 잔존값과 무관하게 'VAT별도' 고정. const vatUnified = useVatMode() === 'unified_excluded'; const CANDIDATE_SUB = candidateSub(`${label('target_margin')}`); const { data: bd, isLoading } = useGetTargetBreakdown(sessionId, { query: { enabled: !!sessionId } }); const candidates = bd?.candidates ?? []; const hiddenPrice = bd?.hidden_price_fields ?? []; // 회사 설정으로 감춰 후보에서 뺀 가격 필드 return (
e.stopPropagation()} > {/* 헤더 */}
목표가
견적번호: {qtNumber} · 상품: {itemName} 배송비: {deliveryFeeYn ? '배송비포함' : '배송비별도'} · 부가세: {!vatUnified && vatYn ? 'VAT포함' : 'VAT별도'} {isLoading || !bd ? ( 산정내역을 불러오는 중… ) : ( <> {/* 목표가 */}
목표가
{won(bd.target_price)}
{/* 선정방식 — 목표가가 어느 후보들 중에서 산정되는지 설명. 매입가·판매가는 견적등록 네고율 적용/미적용에 따라 원가일 수 있어 문구에 반영. */}
목표가 선정방식 1. 구매담당자 제시가 존재 시, 최우선 적용 {/* 설명 문구는 회사 설정(숨김 필드)에 따라 실제 후보와 일치하게 바꾼다. */} 2. 다음 중 가장 작은 값 — {[ !hiddenPrice.includes('internet_lowest_price') && `${label('item.internet_lowest_price')}×(1−수수료)`, !hiddenPrice.includes('purchase_price') && `${label('item.purchase_price')}×(1−${label('target_margin')}) 또는 원가`, !hiddenPrice.includes('selling_price') && `${label('item.selling_price')}×(1−${label('target_margin')}) 또는 원가`, ].filter(Boolean).join(' | ')} {/* 네고 적용/미적용 안내 — 숨김 안 된 매입가·판매가만 언급(신규견적은 인터넷최저가만이라 생략). */} {!bd.is_new && (() => { const negoSources = [ !hiddenPrice.includes('purchase_price') && label('item.purchase_price'), !hiddenPrice.includes('selling_price') && label('item.selling_price'), ].filter(Boolean); return negoSources.length > 0 ? ( * {negoSources.join('·')}는 견적등록의 네고율 적용 여부에 따라 원가(미적용)로도 산정됩니다. ) : null; })()} * 인터넷 평균 수수료: {bd.fee} · {label('target_margin')}: {bd.margin}{bd.is_new ? ' · 신규견적이라 인터넷최저가만 적용' : ''} {bd.is_inherited && ( * 저장된 목표가와 일치하는 현재 후보 없음 — 재생성 상속 또는 산정 후 상품·세팅 변경(아래 후보는 현재값 기준 참고용) )}
{/* 후보 */}
{candidates.map((c) => { const on = c.basis === bd.chosen_basis; const sub = CANDIDATE_SUB[c.basis]; return (
{on ? : null}
{c.label} {sub && ( ({sub}) )}
{won(c.value)}
); })}
{/* 앵커링 (negodata: 설정 anchoring_value 고정율) */}
앵커링 {category && ( 서비스 카테고리: {category} )} 앵커링 값: {bd.anchoring_value} 앵커링가: {won(bd.anchoring_price)} = 목표가×(1−{bd.anchoring_value})
)}
); }