- {item.itemList || item.itemName || '아이템명 없음'}
+ {item.itemList || '아이템명 없음'}
- {item.itemCode} • {item.description || '설명 없음'}
+ {item.itemCode || '자재그룹코드 없음'}
공종: {item.workType} • 선종: {item.shipTypes}
@@ -749,7 +748,7 @@ export function CreateRfqDialog({ onCreated }: CreateRfqDialogProps) {
variant="secondary"
className="flex items-center gap-1"
>
- {item.itemList || item.itemName || '아이템명 없음'} ({item.itemCode})
+ {item.itemList || '아이템명 없음'} ({item.itemCode})
handleRemoveItem(item.id)}
diff --git a/lib/techsales-rfq/table/rfq-table-column.tsx b/lib/techsales-rfq/table/rfq-table-column.tsx
index dfb85420..2740170b 100644
--- a/lib/techsales-rfq/table/rfq-table-column.tsx
+++ b/lib/techsales-rfq/table/rfq-table-column.tsx
@@ -154,7 +154,7 @@ export function getColumns({
},
enableResizing: true,
minSize: 80,
- size: 120,
+ size: 250,
},
{
accessorKey: "itemName",
@@ -169,7 +169,7 @@ export function getColumns({
excelHeader: "자재명"
},
enableResizing: true,
- size: 180,
+ size: 250,
},
{
accessorKey: "projNm",
diff --git a/lib/techsales-rfq/vendor-response/detail/project-info-tab.tsx b/lib/techsales-rfq/vendor-response/detail/project-info-tab.tsx
index 7ba3320d..e4b1b8c3 100644
--- a/lib/techsales-rfq/vendor-response/detail/project-info-tab.tsx
+++ b/lib/techsales-rfq/vendor-response/detail/project-info-tab.tsx
@@ -48,7 +48,7 @@ interface ProjectInfoTabProps {
item?: {
id: number
itemCode: string | null
- itemName: string | null
+ itemList: string | null
} | null
biddingProject?: {
id: number
@@ -74,6 +74,8 @@ export function ProjectInfoTab({ quotation }: ProjectInfoTabProps) {
const projectSnapshot = rfq?.projectSnapshot
const seriesSnapshot = rfq?.seriesSnapshot
+ console.log("rfq: ", rfq)
+
if (!rfq) {
return (
@@ -112,8 +114,9 @@ export function ProjectInfoTab({ quotation }: ProjectInfoTabProps) {
{rfq.materialCode || "N/A"}
-
품목명
-
{rfq.item?.itemName || "N/A"}
+
자재명
+ {/* TODO : 타입 작업 (시연을 위해 빌드 중단 상태임. 추후 수정) */}
+
{rfq.itemShipbuilding?.itemList || "N/A"}
마감일
diff --git a/lib/techsales-rfq/vendor-response/detail/quotation-tabs.tsx b/lib/techsales-rfq/vendor-response/detail/quotation-tabs.tsx
index a800dd95..97bba2bd 100644
--- a/lib/techsales-rfq/vendor-response/detail/quotation-tabs.tsx
+++ b/lib/techsales-rfq/vendor-response/detail/quotation-tabs.tsx
@@ -56,7 +56,7 @@ interface QuotationData {
item?: {
id: number
itemCode: string | null
- itemName: string | null
+ itemList: string | null
} | null
biddingProject?: {
id: number
diff --git a/lib/techsales-rfq/vendor-response/quotation-editor.tsx b/lib/techsales-rfq/vendor-response/quotation-editor.tsx
index f3fab10d..b30f612c 100644
--- a/lib/techsales-rfq/vendor-response/quotation-editor.tsx
+++ b/lib/techsales-rfq/vendor-response/quotation-editor.tsx
@@ -101,7 +101,7 @@ interface TechSalesVendorQuotation {
item?: {
id: number
itemCode: string | null
- itemName: string | null
+ itemList: string | null
} | null
biddingProject?: {
id: number
@@ -342,8 +342,8 @@ export default function TechSalesQuotationEditor({ quotation }: TechSalesQuotati
{quotation.rfq.materialCode || "N/A"}
-
-
{quotation.rfq.item?.itemName || "N/A"}
+
+
{quotation.rfq.item?.itemList || "N/A"}
diff --git a/lib/techsales-rfq/vendor-response/table/vendor-quotations-table-columns.tsx b/lib/techsales-rfq/vendor-response/table/vendor-quotations-table-columns.tsx
index 109698ea..cf1dac42 100644
--- a/lib/techsales-rfq/vendor-response/table/vendor-quotations-table-columns.tsx
+++ b/lib/techsales-rfq/vendor-response/table/vendor-quotations-table-columns.tsx
@@ -13,24 +13,46 @@ import {
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip"
-import { DataTableColumnHeader } from "@/components/data-table/data-table-column-header"
import {
TechSalesVendorQuotations,
TECH_SALES_QUOTATION_STATUS_CONFIG
} from "@/db/schema"
import { AppRouterInstance } from "next/dist/shared/lib/app-router-context.shared-runtime"
+import { DataTableColumnHeaderSimple } from "@/components/data-table/data-table-column-simple-header"
interface QuotationWithRfqCode extends TechSalesVendorQuotations {
+ // RFQ 관련 정보
rfqCode?: string;
materialCode?: string;
dueDate?: Date;
rfqStatus?: string;
+
+ // 아이템 정보
itemName?: string;
+ itemShipbuildingId?: number;
+
+ // 프로젝트 정보
projNm?: string;
+ pspid?: string;
+ sector?: string;
+
+ // 벤더 정보
+ vendorName?: string;
+ vendorCode?: string;
+
+ // 사용자 정보
+ createdByName?: string | null;
+ updatedByName?: string | null;
+
+ // 견적 코드 및 버전
quotationCode?: string | null;
quotationVersion?: number | null;
+
+ // 추가 상태 정보
rejectionReason?: string | null;
acceptedAt?: Date | null;
+
+ // 첨부파일 개수
attachmentCount?: number;
}
@@ -65,23 +87,23 @@ export function getColumns({ router, openAttachmentsSheet }: GetColumnsProps): C
enableSorting: false,
enableHiding: false,
},
- {
- accessorKey: "id",
- header: ({ column }) => (
-
- ),
- cell: ({ row }) => (
-
- {row.getValue("id")}
-
- ),
- enableSorting: true,
- enableHiding: true,
- },
+ // {
+ // accessorKey: "id",
+ // header: ({ column }) => (
+ //
+ // ),
+ // cell: ({ row }) => (
+ //
+ // {row.getValue("id")}
+ //
+ // ),
+ // enableSorting: true,
+ // enableHiding: true,
+ // },
{
accessorKey: "rfqCode",
header: ({ column }) => (
-
+
),
cell: ({ row }) => {
const rfqCode = row.getValue("rfqCode") as string;
@@ -94,26 +116,58 @@ export function getColumns({ router, openAttachmentsSheet }: GetColumnsProps): C
enableSorting: true,
enableHiding: false,
},
- {
- accessorKey: "materialCode",
- header: ({ column }) => (
-
- ),
- cell: ({ row }) => {
- const materialCode = row.getValue("materialCode") as string;
- return (
-
- {materialCode || "N/A"}
-
- );
- },
- enableSorting: true,
- enableHiding: true,
- },
+ // {
+ // accessorKey: "vendorName",
+ // header: ({ column }) => (
+ //
+ // ),
+ // cell: ({ row }) => {
+ // const vendorName = row.getValue("vendorName") as string;
+ // return (
+ //
+ // {vendorName || "N/A"}
+ //
+ // );
+ // },
+ // enableSorting: true,
+ // enableHiding: false,
+ // },
+ // {
+ // accessorKey: "vendorCode",
+ // header: ({ column }) => (
+ //
+ // ),
+ // cell: ({ row }) => {
+ // const vendorCode = row.getValue("vendorCode") as string;
+ // return (
+ //
+ // {vendorCode || "N/A"}
+ //
+ // );
+ // },
+ // enableSorting: true,
+ // enableHiding: true,
+ // },
+ // {
+ // accessorKey: "materialCode",
+ // header: ({ column }) => (
+ //
+ // ),
+ // cell: ({ row }) => {
+ // const materialCode = row.getValue("materialCode") as string;
+ // return (
+ //
+ // {materialCode || "N/A"}
+ //
+ // );
+ // },
+ // enableSorting: true,
+ // enableHiding: true,
+ // },
{
accessorKey: "itemName",
header: ({ column }) => (
-
+
),
cell: ({ row }) => {
const itemName = row.getValue("itemName") as string;
@@ -134,13 +188,13 @@ export function getColumns({ router, openAttachmentsSheet }: GetColumnsProps): C
);
},
- enableSorting: false,
+ enableSorting: true,
enableHiding: true,
},
{
accessorKey: "projNm",
header: ({ column }) => (
-
+
),
cell: ({ row }) => {
const projNm = row.getValue("projNm") as string;
@@ -161,13 +215,45 @@ export function getColumns({ router, openAttachmentsSheet }: GetColumnsProps): C
);
},
- enableSorting: false,
+ enableSorting: true,
enableHiding: true,
},
+ // {
+ // accessorKey: "quotationCode",
+ // header: ({ column }) => (
+ //
+ // ),
+ // cell: ({ row }) => {
+ // const quotationCode = row.getValue("quotationCode") as string;
+ // return (
+ //
+ // {quotationCode || "미부여"}
+ //
+ // );
+ // },
+ // enableSorting: true,
+ // enableHiding: true,
+ // },
+ // {
+ // accessorKey: "quotationVersion",
+ // header: ({ column }) => (
+ //
+ // ),
+ // cell: ({ row }) => {
+ // const quotationVersion = row.getValue("quotationVersion") as number;
+ // return (
+ //
+ // {quotationVersion || 1}
+ //
+ // );
+ // },
+ // enableSorting: true,
+ // enableHiding: true,
+ // },
{
id: "attachments",
header: ({ column }) => (
-
+
),
cell: ({ row }) => {
const quotation = row.original
@@ -216,7 +302,7 @@ export function getColumns({ router, openAttachmentsSheet }: GetColumnsProps): C
{
accessorKey: "status",
header: ({ column }) => (
-
+
),
cell: ({ row }) => {
const status = row.getValue("status") as string;
@@ -243,7 +329,7 @@ export function getColumns({ router, openAttachmentsSheet }: GetColumnsProps): C
{
accessorKey: "currency",
header: ({ column }) => (
-
+
),
cell: ({ row }) => {
const currency = row.getValue("currency") as string;
@@ -259,7 +345,7 @@ export function getColumns({ router, openAttachmentsSheet }: GetColumnsProps): C
{
accessorKey: "totalPrice",
header: ({ column }) => (
-
+
),
cell: ({ row }) => {
const totalPrice = row.getValue("totalPrice") as string;
@@ -287,7 +373,7 @@ export function getColumns({ router, openAttachmentsSheet }: GetColumnsProps): C
{
accessorKey: "validUntil",
header: ({ column }) => (
-
+
),
cell: ({ row }) => {
const validUntil = row.getValue("validUntil") as Date;
@@ -305,7 +391,7 @@ export function getColumns({ router, openAttachmentsSheet }: GetColumnsProps): C
{
accessorKey: "submittedAt",
header: ({ column }) => (
-
+
),
cell: ({ row }) => {
const submittedAt = row.getValue("submittedAt") as Date;
@@ -320,10 +406,28 @@ export function getColumns({ router, openAttachmentsSheet }: GetColumnsProps): C
enableSorting: true,
enableHiding: true,
},
+ // {
+ // accessorKey: "acceptedAt",
+ // header: ({ column }) => (
+ //
+ // ),
+ // cell: ({ row }) => {
+ // const acceptedAt = row.getValue("acceptedAt") as Date;
+ // return (
+ //
+ //
+ // {acceptedAt ? formatDateTime(acceptedAt) : "미승인"}
+ //
+ //
+ // );
+ // },
+ // enableSorting: true,
+ // enableHiding: true,
+ // },
{
accessorKey: "dueDate",
header: ({ column }) => (
-
+
),
cell: ({ row }) => {
const dueDate = row.getValue("dueDate") as Date;
@@ -340,10 +444,41 @@ export function getColumns({ router, openAttachmentsSheet }: GetColumnsProps): C
enableSorting: true,
enableHiding: true,
},
+ // {
+ // accessorKey: "rejectionReason",
+ // header: ({ column }) => (
+ //
+ // ),
+ // cell: ({ row }) => {
+ // const rejectionReason = row.getValue("rejectionReason") as string;
+ // return (
+ //
+ // {rejectionReason ? (
+ //
+ //
+ //
+ //
+ // {rejectionReason}
+ //
+ //
+ //
+ // {rejectionReason}
+ //
+ //
+ //
+ // ) : (
+ //
N/A
+ // )}
+ //
+ // );
+ // },
+ // enableSorting: false,
+ // enableHiding: true,
+ // },
{
accessorKey: "createdAt",
header: ({ column }) => (
-
+
),
cell: ({ row }) => {
const createdAt = row.getValue("createdAt") as Date;
@@ -361,7 +496,7 @@ export function getColumns({ router, openAttachmentsSheet }: GetColumnsProps): C
{
accessorKey: "updatedAt",
header: ({ column }) => (
-
+
),
cell: ({ row }) => {
const updatedAt = row.getValue("updatedAt") as Date;
@@ -376,6 +511,38 @@ export function getColumns({ router, openAttachmentsSheet }: GetColumnsProps): C
enableSorting: true,
enableHiding: true,
},
+ // {
+ // accessorKey: "createdByName",
+ // header: ({ column }) => (
+ //
+ // ),
+ // cell: ({ row }) => {
+ // const createdByName = row.getValue("createdByName") as string;
+ // return (
+ //
+ // {createdByName || "N/A"}
+ //
+ // );
+ // },
+ // enableSorting: true,
+ // enableHiding: true,
+ // },
+ // {
+ // accessorKey: "updatedByName",
+ // header: ({ column }) => (
+ //
+ // ),
+ // cell: ({ row }) => {
+ // const updatedByName = row.getValue("updatedByName") as string;
+ // return (
+ //
+ // {updatedByName || "N/A"}
+ //
+ // );
+ // },
+ // enableSorting: true,
+ // enableHiding: true,
+ // },
{
id: "actions",
header: "작업",
diff --git a/lib/techsales-rfq/vendor-response/table/vendor-quotations-table.tsx b/lib/techsales-rfq/vendor-response/table/vendor-quotations-table.tsx
index e1b82579..e98d6bdc 100644
--- a/lib/techsales-rfq/vendor-response/table/vendor-quotations-table.tsx
+++ b/lib/techsales-rfq/vendor-response/table/vendor-quotations-table.tsx
@@ -2,6 +2,7 @@
"use client"
import * as React from "react"
+import { useSearchParams } from "next/navigation"
import { type DataTableAdvancedFilterField, type DataTableFilterField } from "@/types/table"
import { useDataTable } from "@/hooks/use-data-table"
import { DataTable } from "@/components/data-table/data-table"
@@ -10,41 +11,192 @@ import { TechSalesVendorQuotations, TECH_SALES_QUOTATION_STATUSES, TECH_SALES_QU
import { useRouter } from "next/navigation"
import { getColumns } from "./vendor-quotations-table-columns"
import { TechSalesRfqAttachmentsSheet, ExistingTechSalesAttachment } from "../../table/tech-sales-rfq-attachments-sheet"
-import { getTechSalesRfqAttachments } from "@/lib/techsales-rfq/service"
+import { getTechSalesRfqAttachments, getVendorQuotations } from "@/lib/techsales-rfq/service"
import { toast } from "sonner"
+import { Skeleton } from "@/components/ui/skeleton"
interface QuotationWithRfqCode extends TechSalesVendorQuotations {
- rfqCode?: string;
- materialCode?: string;
+ rfqCode?: string | null;
+ materialCode?: string | null;
dueDate?: Date;
rfqStatus?: string;
- itemName?: string;
- projNm?: string;
+ itemName?: string | null;
+ projNm?: string | null;
quotationCode?: string | null;
- quotationVersion: number | null;
+ quotationVersion?: number | null;
rejectionReason?: string | null;
acceptedAt?: Date | null;
attachmentCount?: number;
}
interface VendorQuotationsTableProps {
- promises: Promise<[{ data: QuotationWithRfqCode[], pageCount: number, total?: number }]>;
+ vendorId: string;
}
-export function VendorQuotationsTable({ promises }: VendorQuotationsTableProps) {
+// 로딩 스켈레톤 컴포넌트
+function TableLoadingSkeleton() {
+ return (
+
+ {/* 툴바 스켈레톤 */}
+
+
+ {/* 테이블 헤더 스켈레톤 */}
+
+
+
+ {/* 테이블 행 스켈레톤 */}
+ {Array.from({ length: 5 }).map((_, index) => (
+
+ ))}
+
+
+ {/* 페이지네이션 스켈레톤 */}
+
+
+ )
+}
- // TODO: 안정화 이후 삭제
- console.log("렌더링 사이클 점검용 로그: VendorQuotationsTable 렌더링됨");
+// 중앙 로딩 인디케이터 컴포넌트
+function CenterLoadingIndicator() {
+ return (
+
+
+
+
데이터를 불러오는 중...
+
잠시만 기다려주세요.
+
+
+ )
+}
- const [{ data, pageCount }] = React.use(promises);
- const router = useRouter();
+export function VendorQuotationsTable({ vendorId }: VendorQuotationsTableProps) {
+ const searchParams = useSearchParams()
+ const router = useRouter()
// 첨부파일 시트 상태
const [attachmentsOpen, setAttachmentsOpen] = React.useState(false)
const [selectedRfqForAttachments, setSelectedRfqForAttachments] = React.useState<{ id: number; rfqCode: string | null; status: string } | null>(null)
const [attachmentsDefault, setAttachmentsDefault] = React.useState
([])
- // 데이터 안정성을 위한 메모이제이션 - 핵심 속성만 비교
+ // 데이터 로딩 상태
+ const [data, setData] = React.useState([])
+ const [pageCount, setPageCount] = React.useState(0)
+ const [total, setTotal] = React.useState(0)
+ const [isLoading, setIsLoading] = React.useState(true)
+ const [isInitialLoad, setIsInitialLoad] = React.useState(true) // 최초 로딩 구분
+
+ // URL 파라미터에서 설정 읽기
+ const initialSettings = React.useMemo(() => ({
+ page: parseInt(searchParams?.get('page') || '1'),
+ perPage: parseInt(searchParams?.get('perPage') || '10'),
+ sort: searchParams?.get('sort') ? JSON.parse(searchParams.get('sort')!) : [{ id: "updatedAt", desc: true }],
+ filters: searchParams?.get('filters') ? JSON.parse(searchParams.get('filters')!) : [],
+ joinOperator: (searchParams?.get('joinOperator') as "and" | "or") || "and",
+ basicFilters: searchParams?.get('basicFilters') ? JSON.parse(searchParams.get('basicFilters')!) : [],
+ basicJoinOperator: (searchParams?.get('basicJoinOperator') as "and" | "or") || "and",
+ search: searchParams?.get('search') || '',
+ from: searchParams?.get('from') || '',
+ to: searchParams?.get('to') || '',
+ }), [searchParams])
+
+ // 데이터 로드 함수
+ const loadData = React.useCallback(async () => {
+ try {
+ setIsLoading(true)
+
+ console.log('🔍 [VendorQuotationsTable] 데이터 로드 요청:', {
+ vendorId,
+ settings: initialSettings
+ })
+
+ const result = await getVendorQuotations({
+ page: initialSettings.page,
+ perPage: initialSettings.perPage,
+ sort: initialSettings.sort,
+ filters: initialSettings.filters,
+ joinOperator: initialSettings.joinOperator,
+ basicFilters: initialSettings.basicFilters,
+ basicJoinOperator: initialSettings.basicJoinOperator,
+ search: initialSettings.search,
+ from: initialSettings.from,
+ to: initialSettings.to,
+ }, vendorId)
+
+ console.log('🔍 [VendorQuotationsTable] 데이터 로드 결과:', {
+ dataLength: result.data.length,
+ pageCount: result.pageCount,
+ total: result.total
+ })
+
+ setData(result.data as QuotationWithRfqCode[])
+ setPageCount(result.pageCount)
+ setTotal(result.total)
+ } catch (error) {
+ console.error('데이터 로드 오류:', error)
+ toast.error('데이터를 불러오는 중 오류가 발생했습니다.')
+ } finally {
+ setIsLoading(false)
+ setIsInitialLoad(false)
+ }
+ }, [vendorId, initialSettings])
+
+ // URL 파라미터 변경 감지 및 데이터 재로드 (초기 로드 포함)
+ React.useEffect(() => {
+ loadData()
+ }, [
+ searchParams?.get('page'),
+ searchParams?.get('perPage'),
+ searchParams?.get('sort'),
+ searchParams?.get('filters'),
+ searchParams?.get('joinOperator'),
+ searchParams?.get('basicFilters'),
+ searchParams?.get('basicJoinOperator'),
+ searchParams?.get('search'),
+ searchParams?.get('from'),
+ searchParams?.get('to'),
+ // vendorId 변경도 감지
+ vendorId
+ ])
+
+ // 데이터 안정성을 위한 메모이제이션
const stableData = React.useMemo(() => {
return data;
}, [data.length, data.map(item => `${item.id}-${item.status}-${item.updatedAt}`).join(',')]);
@@ -95,13 +247,13 @@ export function VendorQuotationsTable({ promises }: VendorQuotationsTableProps)
}
}, [data])
- // 테이블 컬럼 정의 - router는 안정적이므로 한 번만 생성
+ // 테이블 컬럼 정의
const columns = React.useMemo(() => getColumns({
router,
openAttachmentsSheet,
- }), [router, openAttachmentsSheet]);
+ }), [router, openAttachmentsSheet])
- // 필터 필드 - 중앙화된 상태 상수 사용
+ // 필터 필드
const filterFields = React.useMemo[]>(() => [
{
id: "status",
@@ -121,9 +273,9 @@ export function VendorQuotationsTable({ promises }: VendorQuotationsTableProps)
label: "자재 코드",
placeholder: "자재 코드 검색...",
}
- ], []);
+ ], [])
- // 고급 필터 필드 - 중앙화된 상태 상수 사용
+ // 고급 필터 필드
const advancedFilterFields = React.useMemo[]>(() => [
{
id: "rfqCode",
@@ -154,20 +306,21 @@ export function VendorQuotationsTable({ promises }: VendorQuotationsTableProps)
label: "제출일",
type: "date",
},
- ], []);
+ ], [])
// useDataTable 훅 사용
const { table } = useDataTable({
data: stableData,
columns,
pageCount,
+ rowCount: total,
filterFields,
enablePinning: true,
enableAdvancedFilter: true,
enableColumnResizing: true,
columnResizeMode: 'onChange',
initialState: {
- sorting: [{ id: "updatedAt", desc: true }],
+ sorting: initialSettings.sort,
columnPinning: { right: ["actions"] },
},
getRowId: (originalRow) => String(originalRow.id),
@@ -177,22 +330,48 @@ export function VendorQuotationsTable({ promises }: VendorQuotationsTableProps)
minSize: 50,
maxSize: 500,
},
- });
+ })
+
+ // 최초 로딩 시 전체 스켈레톤 표시
+ if (isInitialLoad && isLoading) {
+ return (
+
+ )
+ }
return (
-
-
+ {/* 로딩 오버레이 (재로딩 시) */}
+ {/* {!isInitialLoad && isLoading && (
+
+
+
+ )} */}
+
+
-
-
+
+ {!isInitialLoad && isLoading && (
+
+ )}
+
+
+
{/* 첨부파일 관리 시트 (읽기 전용) */}
--
cgit v1.2.3