diff --git a/negodata/front/src/features/quotations/components/QuotationTable.tsx b/negodata/front/src/features/quotations/components/QuotationTable.tsx
index 293a3d9..9ca85f7 100644
--- a/negodata/front/src/features/quotations/components/QuotationTable.tsx
+++ b/negodata/front/src/features/quotations/components/QuotationTable.tsx
@@ -1,7 +1,15 @@
-import type { ReactNode } from 'react';
-import { Clock, Building2, Link2 } from 'lucide-react';
+import { type ReactNode } from 'react';
+import { Clock, Building2, Link2, CornerDownRight } from 'lucide-react';
import { DataTable } from '@/components/ui/data-table';
-import { type Estimate, type Product, quotationStatusLabel, quotationTypeLabel } from '../types';
+import { Typography } from '@/components/ui/typography';
+import { useQuotationChain } from '../hooks/useQuotationChain';
+import {
+ type Estimate,
+ type Product,
+ quotationStatusLabel,
+ quotationTypeLabel,
+ CHAIN_ROUND_STATE_LABEL,
+} from '../types';
import { QuotationType, QuotationStatus } from '@/api/generated/model';
type QuotationTableProps = {
@@ -44,13 +52,15 @@ export function QuotationTable({ data, products, onOpenDetail, onFilterChain, fo
const productName = product?.name ?? est.productName; // 목록에 없으면 서버 조인 상품명으로 폴백
return (
-
+
{est.title}
-
-
+
+
대상 상품: {productName ?? '확인 불가'} (₩{(product?.price ?? 0).toLocaleString()})
-
+
+ {/* 이 견적에서 재생성된 다음 차수(직속 자식)만 행 밑에 표시 — 전체 체인 반복 X */}
+
);
},
@@ -70,17 +80,19 @@ export function QuotationTable({ data, products, onOpenDetail, onFilterChain, fo
className="inline-flex items-center gap-1 hover:text-primary hover:underline cursor-pointer"
>
- {est.number}
+
{est.number}
) : (
- est.number
+
{est.number}
),
},
{
header: '유형',
align: 'center',
cell: (est) => (
-
{quotationTypeLabel(est.type)}
-
+
),
},
{
header: '차수',
align: 'center',
- cellClassName: 'font-bold font-mono text-sm',
- cell: (est) => `${est.round}차`,
+ cell: (est) => (
+
+ {est.round}차
+
+ ),
},
{
header: '견적상태',
align: 'center',
cell: (est) => (
-
{quotationStatusLabel(est.status)}
-
+
),
},
{
@@ -114,22 +131,63 @@ export function QuotationTable({ data, products, onOpenDetail, onFilterChain, fo
cell: (est) => (
- {est.dueDate}
+ {est.dueDate}
),
},
{
header: '생성일',
cellClassName: 'font-mono text-muted-foreground whitespace-nowrap',
- cell: (est) => est.createdDate ?? '-',
+ cell: (est) => (
+
{est.createdDate ?? '-'}
+ ),
},
{
header: '협력사수',
align: 'center',
cellClassName: 'font-mono font-bold text-foreground',
- cell: (est) => `${est.participationCount}개사`,
+ cell: (est) => (
+
{est.participationCount}개사
+ ),
},
]}
/>
);
}
+function RegeneratedChild({
+ number,
+ currentQtId,
+ onOpenRound,
+}: {
+ number?: string | null;
+ currentQtId?: string;
+ onOpenRound: (qtId: string) => void;
+}) {
+ const { rounds } = useQuotationChain(number);
+ const current = rounds.find((r) => r.qt_id === currentQtId);
+ // rounds 는 round 오름차순 → 현재보다 큰 첫 라운드가 직속 자식.
+ const child = current ? rounds.find((r) => r.round > current.round) : undefined;
+ if (!child) return null;
+
+ return (
+
+
+
+ 재생성됨
+
+
+
+ );
+}
diff --git a/negodata/front/src/lib/utils.ts b/negodata/front/src/lib/utils.ts
index bd0c391..e433c61 100644
--- a/negodata/front/src/lib/utils.ts
+++ b/negodata/front/src/lib/utils.ts
@@ -4,3 +4,10 @@ import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
+
+// 문자열에서 금액(원/₩) 표기의 숫자만 '***'로 가린다. 접미사·문장 구조는 유지.
+export function maskPrices(text: string): string {
+ return text
+ .replace(/₩\s?[0-9][0-9,]*/g, "₩***")
+ .replace(/[0-9][0-9,]*\s*(?=원)/g, "***")
+}
diff --git a/negodata/front/src/pages/not-found.tsx b/negodata/front/src/pages/not-found.tsx
new file mode 100644
index 0000000..0595c3d
--- /dev/null
+++ b/negodata/front/src/pages/not-found.tsx
@@ -0,0 +1,42 @@
+import {Link, useNavigate, useRouteError} from 'react-router';
+import {FileQuestion} from 'lucide-react';
+import { Typography } from '@/components/ui/typography';
+
+// 매칭되는 라우트가 없을 때(catch-all '*') 떨어지는 404 페이지.
+// 라우터 errorElement 로도 재사용 가능하도록 useRouteError 는 있으면 참고만 한다(없어도 동작).
+export default function NotFoundPage() {
+ const navigate = useNavigate();
+ const error = useRouteError() as {status?: number} | undefined;
+ const status = error?.status ?? 404;
+
+ return (
+
+
+
+
+
+
{status} · 페이지를 찾을 수 없음
+
+ 요청하신 주소의 페이지가 존재하지 않거나, 이동·삭제되었습니다.
+
+ 주소를 다시 확인해 주세요.
+
+
+
+
+ 상품관리로 돌아가기
+
+
+
+
+ );
+}