o2o-negosium-original/negodata/front/src/features/quotations/components/QuotationDetailSheet/DrawerHeaderCards.tsx
Mina Choi eb6612af1c [refactor] negodata: 도메인 코드값 enum 정비 — 백엔드 정본 + 프론트 생성타입 사용
- 백엔드: 도메인 enum CodeEnum 상속(x-enum-varnames) → orval이 이름 있는 enum 생성. 응답 필드 enum 타입 지정(요청은 int 유지). ENUM_LABELS/DOMAIN_ENUMS·/v1/enums 제거
- 프론트: 생성 enum + 라벨맵으로 교체(매직넘버·발명 어휘 제거), 견적·세션 상태 코드화, role/delivery 라벨 프론트 소유
- 동반 정리: unwrap 캐스트·목업(buildSessions·데모)·死코드 제거, mapItem toMinPrice dedupe

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 17:06:26 +09:00

191 lines
8.3 KiB
TypeScript

import type { ReactNode } from 'react';
import { Link } from 'react-router';
import { Package } from 'lucide-react';
import { Card } from '@/components/ui/card';
import { InfoField } from './InfoField';
import { QuotationStatusBadge } from './StatusPill';
import type { QuotationData } from '@/api/generated/model/quotationData';
import {
type Product,
type Partner,
type QuotationSetting,
buildBidSummary,
quotationTypeLabel,
} from '../../types';
const fmtYn = (b: boolean | null | undefined, yes: string, no: string) =>
b == null ? '-' : b ? yes : no;
/** 헤더 정보 카드 컨테이너. ui/Card 의 넉넉한 기본 여백을 촘촘하게 덮어쓴다. */
function SectionCard({ title, children }: { title: string; children: ReactNode }) {
return (
<Card className="p-3 gap-2 rounded shadow-xs border-border/80">
<span className="font-bold text-foreground text-[11px] border-b border-border pb-1 block font-sans">
{title}
</span>
{children}
</Card>
);
}
type DrawerHeaderCardsProps = {
quotation: QuotationData;
partners: Partner[];
quotationSettings: QuotationSetting[];
/** 현재 선택 세션의 상품(없으면 상품 카드는 빈 상태). */
currentProduct: Product | undefined;
};
export function DrawerHeaderCards({
quotation,
partners,
quotationSettings,
currentProduct,
}: DrawerHeaderCardsProps) {
// Quotations DDL 표시값
const q_name = quotation.name || '미지정';
const q_number = quotation.number || 'EST-000000-0000';
const q_round = quotation.round || 1;
const q_end_time = quotation.end_time || '미지정';
const q_manager_name = quotation.manager_name || '홍길동 파트너';
const q_manager_email = quotation.manager_email || 'gildong@negodata.com';
const q_memo = quotation.memo || '안내사항 없음';
const bidSummaryObj = buildBidSummary(quotation, partners);
const selectedSettingObj = quotationSettings.find((qs) => qs.qt_setting_id === quotation.qt_setting_id);
// 상품 상세 패널 행(negowiz 협상대화의 상품 정보 대응). negodata 컬럼명: maker_name→manufacturer, min_order_quantity→moq.
const productSpecRows = currentProduct
? [
{ label: '상품코드', value: currentProduct.code || '-' },
{ label: '단가', value: currentProduct.price != null ? `${Number(currentProduct.price).toLocaleString()}` : '-' },
{ label: '모델명', value: currentProduct.model_name || '-' },
{ label: '규격', value: currentProduct.spec || '-' },
{ label: '제조사', value: currentProduct.manufacturer || '-' },
{ label: '원산지', value: currentProduct.made_in || '-' },
{ label: 'MOQ', value: currentProduct.moq || '-' },
{ label: '리드타임', value: currentProduct.lead_time != null ? `${currentProduct.lead_time}` : '-' },
{ label: 'VAT', value: fmtYn(currentProduct.vat_yn, '포함', '별도') },
{ label: '배송비', value: fmtYn(currentProduct.delivery_fee_yn, '포함', '별도') },
]
: [];
return (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 mt-4 text-xs font-mono">
{/* 좌측 컬럼: 견적정보 + 진행상태 */}
<div className="space-y-4">
{/* Quotations */}
<SectionCard title="견적 정보">
<div className="grid grid-cols-2 gap-x-2 gap-y-1.5 font-mono text-muted-foreground">
<InfoField label="견적명" value={q_name} valueClassName="font-sans" />
<InfoField label="견적번호" value={q_number} />
<InfoField label="유형" value={quotationTypeLabel(quotation.type)} />
<InfoField label="차수" value={`${q_round}`} />
<InfoField label="견적상태" labelClassName="opacity-90 font-bold mb-1">
<QuotationStatusBadge status={quotation.status} />
</InfoField>
<InfoField label="마감시각" value={q_end_time} />
<InfoField label="담당자" value={`${q_manager_name} (${q_manager_email})`} valueClassName="font-sans" />
<InfoField
label="메모"
value={q_memo}
valueClassName="font-sans truncate block"
title={q_memo || ''}
/>
</div>
</SectionCard>
{/* Bid Summary */}
<SectionCard title="견적 진행상태/결과">
<div className="grid grid-cols-2 gap-x-2 gap-y-1.5 font-mono text-muted-foreground">
<InfoField label="식별자" value={bidSummaryObj.bid_summary_id} />
<InfoField label="진행/결과 상태" value={bidSummaryObj.status} />
<InfoField label="반복횟수" value={`${bidSummaryObj.qt_iteration}`} />
<InfoField
label="우선협상자 존재여부"
value={bidSummaryObj.has_preferred ? '존재' : '미존재'}
valueClassName={bidSummaryObj.has_preferred ? 'text-emerald-600 font-bold' : undefined}
/>
<InfoField
label="우선협상자명"
value={bidSummaryObj.preferred_sp_name}
className="col-span-2"
valueClassName="font-sans"
/>
<InfoField label="동가입찰정보" className="col-span-2">
<code className="text-foreground font-semibold bg-muted/60 p-1 rounded text-[10px] block overflow-x-auto whitespace-pre">
{bidSummaryObj.equal_data}
</code>
</InfoField>
</div>
</SectionCard>
</div>
{/* 우측 컬럼: 상품정보 + 세팅 */}
<div className="space-y-4">
{/* 상품 정보 (협상 대상 상품) */}
<SectionCard title="상품 정보">
{currentProduct ? (
<div className="flex gap-4">
<div className="h-24 w-24 shrink-0 rounded-md border border-border bg-background overflow-hidden flex items-center justify-center">
{currentProduct.image_url ? (
<img
src={currentProduct.image_url}
alt={currentProduct.name || '상품'}
className="h-full w-full object-cover"
/>
) : (
<Package size={28} className="text-muted-foreground/50" />
)}
</div>
<div className="flex-1 min-w-0">
<Link
to={`/products?detail=${currentProduct.id}`}
className="text-foreground font-bold font-sans text-[12px] mb-2 truncate block hover:text-primary hover:underline"
title={`${currentProduct.name || ''} — 상품 상세로 이동`}
>
{currentProduct.name || '-'}
</Link>
<div className="grid grid-cols-2 lg:grid-cols-3 gap-x-3 gap-y-1.5 font-mono text-muted-foreground">
{productSpecRows.map((r) => (
<InfoField
key={r.label}
label={r.label}
value={r.value}
valueClassName="font-sans truncate block"
title={r.value}
/>
))}
</div>
</div>
</div>
) : (
<div className="text-muted-foreground py-6 text-center"> .</div>
)}
</SectionCard>
{/* Quotation Settings */}
<SectionCard title="견적 세팅">
{selectedSettingObj ? (
<div className="grid grid-cols-2 gap-x-2 gap-y-1.5 font-mono text-muted-foreground">
<InfoField
label="목표 마진율"
value={selectedSettingObj.target_margin}
valueClassName="font-bold text-emerald-600 dark:text-emerald-400 font-sans"
/>
<InfoField label="앵커링 설정 값" value={selectedSettingObj.anchoring_value} valueClassName="font-sans" />
<InfoField
label="카드 사용 횟수"
value={selectedSettingObj.card_use_count}
className="col-span-2 col-start-1"
/>
</div>
) : (
<div className="text-muted-foreground py-6 text-center"> .</div>
)}
</SectionCard>
</div>
</div>
);
}