summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorjoonhoekim <26rote@gmail.com>2025-09-24 17:36:08 +0900
committerjoonhoekim <26rote@gmail.com>2025-09-24 17:36:08 +0900
commitbf2db28586569499e44b58999f2e0f33ed4cdeb5 (patch)
tree9ef9305829fdec30ec7a442f2ba0547a62dba7a9
parent1bda7f20f113737f4af32495e7ff24f6022dc283 (diff)
(김준회) 구매 요청사항 반영 - vendor-pool 및 avl detail (이진용 프로)
-rw-r--r--app/[lng]/evcp/(evcp)/vendor-pool/page.tsx123
-rw-r--r--components/common/project/project-service.ts10
-rw-r--r--components/common/selectors/place-of-shipping/place-of-shipping-selector.tsx250
-rw-r--r--components/common/vendor/vendor-selector-dialog-single.tsx1
-rw-r--r--components/common/vendor/vendor-service.ts4
-rw-r--r--lib/avl/avl-itb-rfq-service.ts289
-rw-r--r--lib/avl/service.ts10
-rw-r--r--lib/avl/table/avl-detail-table.tsx70
-rw-r--r--lib/avl/table/avl-registration-area.tsx10
-rw-r--r--lib/avl/table/avl-table-columns.tsx16
-rw-r--r--lib/avl/table/avl-table.tsx26
-rw-r--r--lib/avl/table/project-avl-table.tsx33
-rw-r--r--lib/avl/table/standard-avl-table.tsx11
-rw-r--r--lib/avl/table/vendor-pool-table.tsx3
-rw-r--r--lib/vendor-pool/excel-utils.ts310
-rw-r--r--lib/vendor-pool/service.ts187
-rw-r--r--lib/vendor-pool/table/vendor-pool-excel-import-button.tsx258
-rw-r--r--lib/vendor-pool/table/vendor-pool-table-columns.tsx2956
-rw-r--r--lib/vendor-pool/table/vendor-pool-table.tsx224
19 files changed, 2779 insertions, 2012 deletions
diff --git a/app/[lng]/evcp/(evcp)/vendor-pool/page.tsx b/app/[lng]/evcp/(evcp)/vendor-pool/page.tsx
index 6708e674..7426e069 100644
--- a/app/[lng]/evcp/(evcp)/vendor-pool/page.tsx
+++ b/app/[lng]/evcp/(evcp)/vendor-pool/page.tsx
@@ -1,3 +1,5 @@
+"use client"
+
import * as React from "react"
import { type SearchParams } from "@/types/table"
@@ -9,24 +11,13 @@ import { getVendorPools } from "@/lib/vendor-pool/service"
import { vendorPoolSearchParamsCache } from "@/lib/vendor-pool/validations"
import { VendorPoolTable } from "@/lib/vendor-pool/table/vendor-pool-table"
import { InformationButton } from "@/components/information/information-button"
+import { useSearchParams } from "next/navigation"
interface VendorPoolPageProps {
searchParams: Promise<SearchParams>
}
-export default async function VendorPoolPage(props: VendorPoolPageProps) {
- const searchParams = await props.searchParams
- const search = vendorPoolSearchParamsCache.parse(searchParams)
-
- const validFilters = getValidFilters(search.filters)
-
- const promises = Promise.all([
- getVendorPools({
- ...search,
- filters: validFilters,
- }),
- ])
-
+export default function VendorPoolPage({ searchParams }: VendorPoolPageProps) {
return (
<Shell className="gap-2">
<div className="flex items-center justify-between space-y-2">
@@ -60,20 +51,118 @@ export default async function VendorPoolPage(props: VendorPoolPageProps) {
/>
}
>
- <VendorPoolTableWrapper promises={promises} />
+ <VendorPoolTableWrapperClient searchParamsPromise={searchParams} />
</React.Suspense>
</Shell>
)
}
-// 실제 데이터를 받아서 VendorPoolTable에 전달하는 컴포넌트
-function VendorPoolTableWrapper({ promises }: { promises: Promise<any> }) {
- const [{ data, pageCount }] = React.use(promises)
+// 클라이언트 컴포넌트: 필터 변경을 감시하여 데이터 재조회
+function VendorPoolTableWrapperClient({ searchParamsPromise }: { searchParamsPromise: Promise<SearchParams> }) {
+ const searchParams = useSearchParams()
+ const [initialData, setInitialData] = React.useState<{ data: any[], pageCount: number } | null>(null)
+ const [data, setData] = React.useState<any[]>([])
+ const [pageCount, setPageCount] = React.useState(0)
+ const [isLoading, setIsLoading] = React.useState(false)
+
+ // 초기 데이터 로딩
+ React.useEffect(() => {
+ const loadInitialData = async () => {
+ try {
+ const searchParamsData = await searchParamsPromise
+ const search = vendorPoolSearchParamsCache.parse(searchParamsData)
+ const validFilters = getValidFilters(search.filters)
+
+ const result = await getVendorPools({
+ ...search,
+ filters: validFilters,
+ })
+
+ setInitialData(result)
+ setData(result.data)
+ setPageCount(result.pageCount)
+ } catch (error) {
+ console.error('Failed to load initial data:', error)
+ }
+ }
+ loadInitialData()
+ }, [searchParamsPromise])
+
+ // 필터 상태 변경 감시 및 데이터 재조회
+ React.useEffect(() => {
+ if (!initialData) return // 초기 데이터가 로드되기 전까지는 실행하지 않음
+
+ const refreshData = async () => {
+ setIsLoading(true)
+ try {
+ const currentParams = Object.fromEntries(searchParams.entries())
+ const search = vendorPoolSearchParamsCache.parse(currentParams)
+ const validFilters = getValidFilters(search.filters)
+
+ const result = await getVendorPools({
+ ...search,
+ filters: validFilters,
+ })
+
+ setData(result.data)
+ setPageCount(result.pageCount)
+ } catch (error) {
+ console.error('Failed to refresh vendor pool data:', error)
+ } finally {
+ setIsLoading(false)
+ }
+ }
+
+ // 필터 파라미터가 변경될 때마다 데이터 재조회
+ const currentFilters = searchParams.get('filters')
+ const currentJoinOperator = searchParams.get('joinOperator')
+ const currentSearch = searchParams.get('search')
+ const currentPage = searchParams.get('page')
+ const currentPerPage = searchParams.get('perPage')
+ const currentSort = searchParams.get('sort')
+
+ // 필터 관련 파라미터가 변경되면 재조회
+ if (currentFilters !== '[]' || currentJoinOperator || currentSearch || currentPage || currentPerPage || currentSort) {
+ refreshData()
+ } else {
+ // 필터가 초기 상태면 초기 데이터로 복원
+ setData(initialData.data)
+ setPageCount(initialData.pageCount)
+ }
+ }, [searchParams, initialData])
+
+ const handleRefresh = React.useCallback(async () => {
+ if (!initialData) return
+
+ setIsLoading(true)
+ try {
+ const currentParams = Object.fromEntries(searchParams.entries())
+ const search = vendorPoolSearchParamsCache.parse(currentParams)
+ const validFilters = getValidFilters(search.filters)
+
+ const result = await getVendorPools({
+ ...search,
+ filters: validFilters,
+ })
+
+ setData(result.data)
+ setPageCount(result.pageCount)
+ } catch (error) {
+ console.error('Failed to refresh vendor pool data:', error)
+ } finally {
+ setIsLoading(false)
+ }
+ }, [searchParams, initialData])
+
+ if (!initialData) {
+ return null // 초기 데이터 로딩 중
+ }
return (
<VendorPoolTable
data={data}
pageCount={pageCount}
+ onRefresh={handleRefresh}
/>
)
}
diff --git a/components/common/project/project-service.ts b/components/common/project/project-service.ts
index 6c103c6f..510d7527 100644
--- a/components/common/project/project-service.ts
+++ b/components/common/project/project-service.ts
@@ -28,6 +28,10 @@ export interface ProjectInfo {
shipType?: string | null
projectMsrm?: string | null
projectHtDivision?: string | null
+ type?: string | null // projects 테이블의 type 필드
+ sector?: string | null // biddingProjects 테이블의 sector 필드
+ typeMdg?: string | null // projects 테이블의 TYPE_MDG 필드
+ pjtType?: string | null // biddingProjects 테이블의 pjtType 필드
source: 'projects' | 'biddingProjects'
}
@@ -145,13 +149,15 @@ export async function getProjectInfoByCode(projectCode: string, searchFrom: 'bot
if (searchFrom === 'both' || searchFrom === 'projects') {
try {
const projectInfo = await db.select().from(projects).where(eq(projects.code, projectCode)).limit(1)
-
+
if (projectInfo && projectInfo.length > 0) {
return {
projectCode: projectInfo[0].code,
projectName: projectInfo[0].name,
shipType: projectInfo[0].SKND || undefined,
projectHtDivision: projectInfo[0].type || undefined,
+ type: projectInfo[0].type || undefined,
+ typeMdg: projectInfo[0].TYPE_MDG || undefined,
source: 'projects'
}
}
@@ -180,6 +186,8 @@ export async function getProjectInfoByCode(projectCode: string, searchFrom: 'bot
projectName: projectInfo[0].projNm,
projectMsrm: projectInfo[0].ptypeNm,
projectHtDivision,
+ sector: projectInfo[0].sector || undefined,
+ pjtType: projectInfo[0].pjtType || undefined,
source: 'biddingProjects'
}
}
diff --git a/components/common/selectors/place-of-shipping/place-of-shipping-selector.tsx b/components/common/selectors/place-of-shipping/place-of-shipping-selector.tsx
index 63532365..0a9916cd 100644
--- a/components/common/selectors/place-of-shipping/place-of-shipping-selector.tsx
+++ b/components/common/selectors/place-of-shipping/place-of-shipping-selector.tsx
@@ -13,8 +13,27 @@ import { Select, SelectItem, SelectContent } from "@/components/ui/select"
import { SelectTrigger } from "@/components/ui/select"
import { SelectValue } from "@/components/ui/select"
import { Input } from "@/components/ui/input"
-import { useState, useEffect, useMemo } from "react"
+import { useState, useEffect, useMemo, useCallback } from "react"
import { getPlaceOfShippingForSelection } from "./place-of-shipping-service"
+import { Button } from "@/components/ui/button"
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from "@/components/ui/dialog"
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from "@/components/ui/table"
+import { Search, Check } from "lucide-react"
interface PlaceOfShippingData {
code: string
@@ -96,4 +115,233 @@ export function PlaceOfShippingSelector({
</Select>
</div>
)
+}
+
+/**
+ * 선적지/하역지 단일 선택 Dialog 컴포넌트
+ *
+ * @description
+ * - PlaceOfShippingSelector를 Dialog로 래핑한 단일 선택 컴포넌트
+ * - 버튼 클릭 시 Dialog가 열리고, 장소를 선택하면 Dialog가 닫히며 결과를 반환
+ *
+ * @PlaceOfShippingData_Structure
+ * 선택된 장소 객체의 형태:
+ * ```typescript
+ * interface PlaceOfShippingData {
+ * code: string; // 장소코드
+ * description: string; // 장소명
+ * }
+ * ```
+ *
+ * @state
+ * - open: Dialog 열림/닷힘 상태
+ * - selectedPlace: 현재 선택된 장소 (단일)
+ * - tempSelectedPlace: Dialog 내에서 임시로 선택된 장소 (확인 버튼 클릭 전까지)
+ *
+ * @callback
+ * - onPlaceSelect: 장소 선택 완료 시 호출되는 콜백
+ * - 매개변수: PlaceOfShippingData | null
+ * - 선택된 장소 정보 또는 null (선택 해제 시)
+ *
+ * @usage
+ * ```tsx
+ * <PlaceOfShippingSelectorDialogSingle
+ * triggerLabel="장소 선택"
+ * selectedPlace={selectedPlace}
+ * onPlaceSelect={(place) => {
+ * setSelectedPlace(place);
+ * console.log('선택된 장소:', place);
+ * }}
+ * placeholder="장소를 검색하세요..."
+ * />
+ * ```
+ */
+
+interface PlaceOfShippingSelectorDialogSingleProps {
+ /** Dialog를 여는 트리거 버튼 텍스트 */
+ triggerLabel?: string
+ /** 현재 선택된 장소 */
+ selectedPlace?: PlaceOfShippingData | null
+ /** 장소 선택 완료 시 호출되는 콜백 */
+ onPlaceSelect?: (place: PlaceOfShippingData | null) => void
+ /** 검색 입력창 placeholder */
+ placeholder?: string
+ /** Dialog 제목 */
+ title?: string
+ /** Dialog 설명 */
+ description?: string
+ /** 트리거 버튼 비활성화 여부 */
+ disabled?: boolean
+ /** 트리거 버튼 variant */
+ triggerVariant?: "default" | "destructive" | "outline" | "secondary" | "ghost" | "link"
+}
+
+export function PlaceOfShippingSelectorDialogSingle({
+ triggerLabel = "장소 선택",
+ selectedPlace = null,
+ onPlaceSelect,
+ placeholder = "장소를 검색하세요...",
+ title = "장소 선택",
+ description = "원하는 장소를 검색하고 선택해주세요.",
+ disabled = false,
+ triggerVariant = "outline",
+}: PlaceOfShippingSelectorDialogSingleProps) {
+ // Dialog 열림/닫힘 상태
+ const [open, setOpen] = useState(false)
+
+ // Dialog 내에서 임시로 선택된 장소 (확인 버튼 클릭 전까지)
+ const [tempSelectedPlace, setTempSelectedPlace] = useState<PlaceOfShippingData | null>(null)
+
+ // 장소 데이터
+ const [placeOfShippingData, setPlaceOfShippingData] = useState<PlaceOfShippingData[]>([])
+ const [isLoading, setIsLoading] = useState(true)
+ const [searchTerm, setSearchTerm] = useState("")
+
+ const filteredData = useMemo(() => {
+ if (!searchTerm) return placeOfShippingData
+ return placeOfShippingData.filter(item =>
+ item.code.toLowerCase().includes(searchTerm.toLowerCase()) ||
+ item.description.toLowerCase().includes(searchTerm.toLowerCase())
+ )
+ }, [placeOfShippingData, searchTerm])
+
+ // Dialog 열림 시 현재 선택된 장소로 임시 선택 초기화
+ const handleOpenChange = useCallback((newOpen: boolean) => {
+ setOpen(newOpen)
+ if (newOpen) {
+ setTempSelectedPlace(selectedPlace || null)
+ }
+ }, [selectedPlace])
+
+ // 장소 선택 처리 (Dialog 내에서)
+ const handlePlaceChange = useCallback((place: PlaceOfShippingData) => {
+ setTempSelectedPlace(place)
+ }, [])
+
+ // 확인 버튼 클릭 시 선택 완료
+ const handleConfirm = useCallback(() => {
+ onPlaceSelect?.(tempSelectedPlace)
+ setOpen(false)
+ }, [tempSelectedPlace, onPlaceSelect])
+
+ // 취소 버튼 클릭 시
+ const handleCancel = useCallback(() => {
+ setTempSelectedPlace(selectedPlace || null)
+ setOpen(false)
+ }, [selectedPlace])
+
+ // 선택 해제
+ const handleClear = useCallback(() => {
+ setTempSelectedPlace(null)
+ }, [])
+
+ useEffect(() => {
+ const loadData = async () => {
+ try {
+ const data = await getPlaceOfShippingForSelection()
+ setPlaceOfShippingData(data)
+ } catch (error) {
+ console.error('선적지/하역지 데이터 로드 실패:', error)
+ setPlaceOfShippingData([])
+ } finally {
+ setIsLoading(false)
+ }
+ }
+
+ loadData()
+ }, [])
+
+ return (
+ <Dialog open={open} onOpenChange={handleOpenChange}>
+ <DialogTrigger asChild>
+ <Button variant={triggerVariant} disabled={disabled}>
+ {selectedPlace ? (
+ <span className="truncate">
+ {selectedPlace.code} - {selectedPlace.description}
+ </span>
+ ) : (
+ triggerLabel
+ )}
+ </Button>
+ </DialogTrigger>
+
+ <DialogContent className="max-w-4xl max-h-[80vh]">
+ <DialogHeader>
+ <DialogTitle>{title}</DialogTitle>
+ <DialogDescription>{description}</DialogDescription>
+ </DialogHeader>
+
+ <div className="space-y-4">
+ <div className="flex items-center space-x-2">
+ <Search className="h-4 w-4" />
+ <Input
+ placeholder={placeholder}
+ value={searchTerm}
+ onChange={(e) => setSearchTerm(e.target.value)}
+ className="flex-1"
+ />
+ </div>
+
+ {isLoading ? (
+ <div className="flex justify-center py-8">
+ <div className="text-sm text-muted-foreground">장소 데이터를 불러오는 중...</div>
+ </div>
+ ) : (
+ <div className="border rounded-md max-h-96 overflow-auto">
+ <Table>
+ <TableHeader>
+ <TableRow>
+ <TableHead className="w-12"></TableHead>
+ <TableHead>장소코드</TableHead>
+ <TableHead>장소명</TableHead>
+ </TableRow>
+ </TableHeader>
+ <TableBody>
+ {filteredData.length === 0 ? (
+ <TableRow>
+ <TableCell colSpan={3} className="text-center py-8 text-muted-foreground">
+ {searchTerm ? "검색 결과가 없습니다" : "데이터가 없습니다"}
+ </TableCell>
+ </TableRow>
+ ) : (
+ filteredData.map((item) => (
+ <TableRow
+ key={item.code}
+ className={`cursor-pointer hover:bg-muted/50 ${
+ tempSelectedPlace?.code === item.code ? "bg-muted" : ""
+ }`}
+ onClick={() => handlePlaceChange(item)}
+ >
+ <TableCell>
+ {tempSelectedPlace?.code === item.code && (
+ <Check className="h-4 w-4 text-primary" />
+ )}
+ </TableCell>
+ <TableCell className="font-mono">{item.code}</TableCell>
+ <TableCell>{item.description}</TableCell>
+ </TableRow>
+ ))
+ )}
+ </TableBody>
+ </Table>
+ </div>
+ )}
+ </div>
+
+ <DialogFooter className="gap-2">
+ <Button variant="outline" onClick={handleCancel}>
+ 취소
+ </Button>
+ {tempSelectedPlace && (
+ <Button variant="ghost" onClick={handleClear}>
+ 선택 해제
+ </Button>
+ )}
+ <Button onClick={handleConfirm}>
+ 확인
+ </Button>
+ </DialogFooter>
+ </DialogContent>
+ </Dialog>
+ )
} \ No newline at end of file
diff --git a/components/common/vendor/vendor-selector-dialog-single.tsx b/components/common/vendor/vendor-selector-dialog-single.tsx
index da9a9a74..7bb4b14c 100644
--- a/components/common/vendor/vendor-selector-dialog-single.tsx
+++ b/components/common/vendor/vendor-selector-dialog-single.tsx
@@ -28,6 +28,7 @@ import { VendorSearchItem } from "./vendor-service"
* id: number; // 벤더 ID
* vendorName: string; // 벤더명
* vendorCode: string | null; // 벤더코드 (없을 수 있음)
+ * taxId: string | null; // 사업자번호
* status: string; // 벤더 상태 (ACTIVE, PENDING_REVIEW 등)
* displayText: string; // 표시용 텍스트 (vendorName + vendorCode)
* }
diff --git a/components/common/vendor/vendor-service.ts b/components/common/vendor/vendor-service.ts
index 83a63cae..1c59843c 100644
--- a/components/common/vendor/vendor-service.ts
+++ b/components/common/vendor/vendor-service.ts
@@ -9,6 +9,7 @@ export interface VendorSearchItem {
id: number
vendorName: string
vendorCode: string | null
+ taxId: string | null // 사업자번호
status: string
displayText: string // vendorName + vendorCode로 구성된 표시용 텍스트
}
@@ -100,6 +101,7 @@ export async function searchVendorsForSelector(
id: vendors.id,
vendorName: vendors.vendorName,
vendorCode: vendors.vendorCode,
+ taxId: vendors.taxId,
status: vendors.status,
})
.from(vendors)
@@ -169,6 +171,7 @@ export async function getAllVendors(): Promise<{
id: vendors.id,
vendorName: vendors.vendorName,
vendorCode: vendors.vendorCode,
+ taxId: vendors.taxId,
status: vendors.status,
})
.from(vendors)
@@ -209,6 +212,7 @@ export async function getVendorById(vendorId: number): Promise<VendorSearchItem
id: vendors.id,
vendorName: vendors.vendorName,
vendorCode: vendors.vendorCode,
+ taxId: vendors.taxId,
status: vendors.status,
})
.from(vendors)
diff --git a/lib/avl/avl-itb-rfq-service.ts b/lib/avl/avl-itb-rfq-service.ts
deleted file mode 100644
index f7662c2e..00000000
--- a/lib/avl/avl-itb-rfq-service.ts
+++ /dev/null
@@ -1,289 +0,0 @@
-// AVL 기반 RFQ/ITB 생성 서비스
-'use server'
-
-import { getServerSession } from 'next-auth/next'
-import { authOptions } from '@/app/api/auth/[...nextauth]/route'
-import db from '@/db/db'
-import { users, rfqsLast, rfqPrItems } from '@/db/schema'
-import { eq, desc, sql, and } from 'drizzle-orm'
-import type { AvlDetailItem } from './types'
-
-// RFQ/ITB 코드 생성 헬퍼 함수
-async function generateAvlRfqItbCode(userCode: string, type: 'RFQ' | 'ITB'): Promise<string> {
- try {
- // 동일한 userCode를 가진 마지막 RFQ/ITB 번호 조회
- const lastRfq = await db
- .select({ rfqCode: rfqsLast.rfqCode })
- .from(rfqsLast)
- .where(
- and(
- eq(rfqsLast.picCode, userCode),
- type === 'RFQ'
- ? sql`${rfqsLast.prNumber} IS NOT NULL AND ${rfqsLast.prNumber} != ''`
- : sql`${rfqsLast.projectCompany} IS NOT NULL AND ${rfqsLast.projectCompany} != ''`
- )
- )
- .orderBy(desc(rfqsLast.createdAt))
- .limit(1);
-
- let nextNumber = 1;
-
- if (lastRfq.length > 0 && lastRfq[0].rfqCode) {
- // 마지막 코드에서 숫자 부분 추출 (ex: "RFQ001001" -> "001")
- const codeMatch = lastRfq[0].rfqCode.match(/([A-Z]{3})(\d{3})(\d{3})/);
- if (codeMatch) {
- const currentNumber = parseInt(codeMatch[3]);
- nextNumber = currentNumber + 1;
- }
- }
-
- // 코드 형식: RFQ/ITB + userCode(3자리) + 일련번호(3자리)
- const prefix = type === 'RFQ' ? 'RFQ' : 'ITB';
- const paddedNumber = nextNumber.toString().padStart(3, '0');
-
- return `${prefix}${userCode}${paddedNumber}`;
- } catch (error) {
- console.error('RFQ/ITB 코드 생성 오류:', error);
- // 오류 발생 시 기본 코드 생성
- const prefix = type === 'RFQ' ? 'RFQ' : 'ITB';
- return `${prefix}${userCode}001`;
- }
-}
-
-// AVL 기반 RFQ/ITB 생성을 위한 입력 타입
-export interface CreateAvlRfqItbInput {
- // AVL 정보
- avlItems: AvlDetailItem[]
- businessType: '조선' | '해양' // 조선: RFQ, 해양: ITB
-
- // RFQ/ITB 공통 정보
- rfqTitle: string
- dueDate: Date
- remark?: string
-
- // 담당자 정보
- picUserId: number
-
- // 추가 정보 (ITB용)
- projectCompany?: string
- projectFlag?: string
- projectSite?: string
- smCode?: string
-
- // PR 정보 (RFQ용)
- prNumber?: string
- prIssueDate?: Date
- series?: string
-}
-
-// RFQ/ITB 생성 결과 타입
-export interface CreateAvlRfqItbResult {
- success: boolean
- message?: string
- data?: {
- id: number
- rfqCode: string
- type: 'RFQ' | 'ITB'
- }
- error?: string
-}
-
-/**
- * AVL 기반 RFQ/ITB 생성 서비스
- * - 조선 사업: RFQ 생성
- * - 해양 사업: ITB 생성
- * - rfqLast 테이블에 직접 데이터 삽입
- */
-export async function createAvlRfqItbAction(input: CreateAvlRfqItbInput): Promise<CreateAvlRfqItbResult> {
- try {
- // 세션 확인
- const session = await getServerSession(authOptions)
- if (!session?.user?.id) {
- return {
- success: false,
- error: '로그인이 필요합니다.'
- }
- }
-
- // 입력 검증
- if (!input.avlItems || input.avlItems.length === 0) {
- return {
- success: false,
- error: '견적 요청할 AVL 아이템이 없습니다.'
- }
- }
-
- if (!input.businessType || !['조선', '해양'].includes(input.businessType)) {
- return {
- success: false,
- error: '올바른 사업 유형을 선택해주세요.'
- }
- }
-
- // 담당자 정보 확인
- const picUser = await db
- .select({
- id: users.id,
- name: users.name,
- userCode: users.userCode
- })
- .from(users)
- .where(eq(users.id, input.picUserId))
- .limit(1)
-
- if (!picUser || picUser.length === 0) {
- return {
- success: false,
- error: '담당자를 찾을 수 없습니다.'
- }
- }
-
- const userCode = picUser[0].userCode;
- if (!userCode || userCode.length !== 3) {
- return {
- success: false,
- error: '담당자의 userCode가 올바르지 않습니다 (3자리 필요)'
- }
- }
-
- // 사업 유형에 따른 RFQ/ITB 구분 및 데이터 준비
- const rfqType = input.businessType === '조선' ? 'RFQ' : 'ITB'
- const rfqTypeLabel = rfqType
-
- // RFQ/ITB 코드 생성
- const rfqCode = await generateAvlRfqItbCode(userCode, rfqType)
-
- // 대표 아이템 정보 추출 (첫 번째 아이템)
- const representativeItem = input.avlItems[0]
-
- // 트랜잭션으로 RFQ/ITB 생성
- const result = await db.transaction(async (tx) => {
- // 1. rfqsLast 테이블에 기본 정보 삽입
- const [newRfq] = await tx
- .insert(rfqsLast)
- .values({
- rfqCode,
- status: "RFQ 생성",
- dueDate: input.dueDate,
-
- // 대표 아이템 정보
- itemCode: representativeItem.materialGroupCode || `AVL-${representativeItem.id}`,
- itemName: representativeItem.materialNameCustomerSide || representativeItem.materialGroupName || 'AVL 아이템',
-
- // 담당자 정보
- pic: input.picUserId,
- picCode: userCode,
- picName: picUser[0].name || '',
-
- // 기타 정보
- remark: input.remark || null,
- createdBy: Number(session.user.id),
- updatedBy: Number(session.user.id),
- createdAt: new Date(),
- updatedAt: new Date(),
-
- // 사업 유형별 추가 필드
- ...(input.businessType === '조선' && {
- // RFQ 필드
- prNumber: input.prNumber || rfqCode, // PR 번호가 없으면 RFQ 코드 사용
- prIssueDate: input.prIssueDate || new Date(),
- series: input.series || null
- }),
-
- ...(input.businessType === '해양' && {
- // ITB 필드
- projectCompany: input.projectCompany || 'AVL 기반 프로젝트',
- projectFlag: input.projectFlag || null,
- projectSite: input.projectSite || null,
- smCode: input.smCode || null
- })
- })
- .returning()
-
- // 2. rfqPrItems 테이블에 AVL 아이템들 삽입
- const prItemsData = input.avlItems.map((item, index) => ({
- rfqsLastId: newRfq.id,
- rfqItem: `${index + 1}`.padStart(3, '0'), // 001, 002, ...
- prItem: `${index + 1}`.padStart(3, '0'),
- prNo: rfqCode,
-
- materialCode: item.materialGroupCode || `AVL-${item.id}`,
- materialDescription: item.materialNameCustomerSide || item.materialGroupName || `AVL 아이템 ${index + 1}`,
- materialCategory: item.materialGroupCode || null,
-
- quantity: 1, // AVL에서는 수량 정보가 없으므로 1로 설정
- uom: 'EA', // 기본 단위
-
- majorYn: index === 0, // 첫 번째 아이템을 주요 아이템으로 설정
-
- deliveryDate: input.dueDate, // 납기일은 RFQ 마감일과 동일하게 설정
- }))
-
- await tx.insert(rfqPrItems).values(prItemsData)
-
- return newRfq
- })
-
- // 성공 결과 반환
- return {
- success: true,
- message: `${rfqTypeLabel}가 성공적으로 생성되었습니다.`,
- data: {
- id: result.id,
- rfqCode: result.rfqCode!,
- type: rfqTypeLabel as 'RFQ' | 'ITB'
- }
- }
-
- } catch (error) {
- console.error('AVL RFQ/ITB 생성 오류:', error)
-
- if (error instanceof Error) {
- return {
- success: false,
- error: error.message
- }
- }
-
- return {
- success: false,
- error: '알 수 없는 오류가 발생했습니다.'
- }
- }
-}
-
-/**
- * AVL 데이터에서 RFQ/ITB 생성을 위한 기본값 설정 헬퍼 함수
- */
-export async function prepareAvlRfqItbInput(
- selectedItems: AvlDetailItem[],
- businessType: '조선' | '해양',
- defaultValues?: Partial<CreateAvlRfqItbInput>
-): Promise<CreateAvlRfqItbInput> {
- const now = new Date()
- const dueDate = new Date(now.getTime() + (30 * 24 * 60 * 60 * 1000)) // 30일 후
-
- // 선택된 아이템들의 대표 정보를 추출하여 제목 생성
- const representativeItem = selectedItems[0]
- const itemCount = selectedItems.length
- const titleSuffix = itemCount > 1 ? ` 외 ${itemCount - 1}건` : ''
- const defaultTitle = `${representativeItem?.materialNameCustomerSide || 'AVL 자재'}${titleSuffix}`
-
- return {
- avlItems: selectedItems,
- businessType,
- rfqTitle: defaultValues?.rfqTitle || `${businessType} - ${defaultTitle}`,
- dueDate: defaultValues?.dueDate || dueDate,
- remark: defaultValues?.remark || `AVL 기반 ${businessType} 견적 요청`,
- picUserId: defaultValues?.picUserId || 0, // 호출 측에서 설정 필요
- // ITB용 필드들
- projectCompany: defaultValues?.projectCompany,
- projectFlag: defaultValues?.projectFlag,
- projectSite: defaultValues?.projectSite,
- smCode: defaultValues?.smCode,
- // RFQ용 필드들
- prNumber: defaultValues?.prNumber,
- prIssueDate: defaultValues?.prIssueDate,
- series: defaultValues?.series
- }
-}
diff --git a/lib/avl/service.ts b/lib/avl/service.ts
index 0340f52c..1f781486 100644
--- a/lib/avl/service.ts
+++ b/lib/avl/service.ts
@@ -438,14 +438,8 @@ export async function handleAvlAction(
): Promise<ActionResult> {
try {
switch (action) {
- case "new-registration":
- return { success: true, message: "신규 AVL 등록 모드" };
-
- case "standard-registration":
- return { success: true, message: "표준 AVL 등재 모드" };
-
- case "project-registration":
- return { success: true, message: "프로젝트 AVL 등재 모드" };
+ case "avl-registration":
+ return { success: true, message: "AVL 등록 패널을 활성화했습니다." };
case "bulk-import":
if (!data?.file) {
diff --git a/lib/avl/table/avl-detail-table.tsx b/lib/avl/table/avl-detail-table.tsx
index 4408340a..22c503ff 100644
--- a/lib/avl/table/avl-detail-table.tsx
+++ b/lib/avl/table/avl-detail-table.tsx
@@ -6,12 +6,10 @@ import { useDataTable } from "@/hooks/use-data-table"
import { DataTable } from "@/components/data-table/data-table"
import { Button } from "@/components/ui/button"
import { toast } from "sonner"
-import { createAvlRfqItbAction, prepareAvlRfqItbInput } from "../avl-itb-rfq-service"
import { columns } from "./columns-detail"
import type { AvlDetailItem } from "../types"
import { BackButton } from "@/components/ui/back-button"
-import { useSession } from "next-auth/react"
interface AvlDetailTableProps {
data: AvlDetailItem[]
@@ -33,63 +31,11 @@ export function AvlDetailTable({
projectInfo,
businessType,
}: AvlDetailTableProps) {
- // 견적요청 처리 상태 관리
- const [isProcessingQuote, setIsProcessingQuote] = React.useState(false)
- const { data: session } = useSession()
-
- // 견적요청 처리 함수
- const handleQuoteRequest = React.useCallback(async () => {
- if (!businessType || !['조선', '해양'].includes(businessType)) {
- toast.error("공사구분이 올바르지 않습니다. 견적요청 처리 불가.")
- return
- }
- if (data.length === 0) {
- toast.error("견적요청할 AVL 데이터가 없습니다.")
- return
- }
-
- setIsProcessingQuote(true)
-
- try {
- // 현재 사용자 세션에서 ID 가져오기
- const currentUserId = session?.user?.id ? Number(session.user.id) : undefined
-
- // 견적요청 입력 데이터 준비 (전체 데이터를 사용)
- const quoteInput = await prepareAvlRfqItbInput(
- data, // 전체 데이터를 사용
- businessType as '조선' | '해양',
- {
- picUserId: currentUserId,
- rfqTitle: `${businessType} AVL 견적요청 - ${data[0]?.materialNameCustomerSide || 'AVL 아이템'}${data.length > 1 ? ` 외 ${data.length - 1}건` : ''}`
- }
- )
-
- // 견적요청 실행
- const result = await createAvlRfqItbAction(quoteInput)
-
- if (result.success) {
- toast.success(`${result.data?.type}가 성공적으로 생성되었습니다. (코드: ${result.data?.rfqCode})`)
- } else {
- toast.error(result.error || "견적요청 처리 중 오류가 발생했습니다.")
- }
-
- } catch (error) {
- console.error('견적요청 처리 오류:', error)
- toast.error("견적요청 처리 중 오류가 발생했습니다.")
- } finally {
- setIsProcessingQuote(false)
- }
- }, [businessType, data, session?.user?.id])
// 액션 핸들러
const handleAction = React.useCallback(async (action: string) => {
switch (action) {
-
- case 'quote-request':
- await handleQuoteRequest()
- break
-
case 'vendor-pool':
window.open('/evcp/vendor-pool', '_blank')
break
@@ -107,7 +53,7 @@ export function AvlDetailTable({
default:
toast.error(`알 수 없는 액션: ${action}`)
}
- }, [handleQuoteRequest])
+ }, [])
// 테이블 메타 설정
@@ -153,24 +99,10 @@ export function AvlDetailTable({
{/* 상단 버튼 영역 */}
<div className="flex items-center gap-2 ml-auto justify-end">
- {
- // 표준AVL로는 견적요청하지 않으며, 프로젝트 AVL로만 견적요청처리
- avlType === '프로젝트AVL' && businessType && ['조선', '해양'].includes(businessType) &&
- <Button
- variant="outline"
- size="sm"
- onClick={() => handleAction('quote-request')}
- disabled={data.length === 0 || isProcessingQuote}
- >
- {isProcessingQuote ? '처리중...' : `${businessType} 견적요청 (${businessType === '조선' ? 'RFQ' : 'ITB'})`}
- </Button>
- }
-
{/* 단순 이동 버튼 */}
<Button variant="outline" size="sm" onClick={() => handleAction('vendor-pool')}>
Vendor Pool
</Button>
-
</div>
{/* 데이터 테이블 */}
diff --git a/lib/avl/table/avl-registration-area.tsx b/lib/avl/table/avl-registration-area.tsx
index ba1c76d4..6c7eba9d 100644
--- a/lib/avl/table/avl-registration-area.tsx
+++ b/lib/avl/table/avl-registration-area.tsx
@@ -15,16 +15,6 @@ import { toast } from "sonner"
// 선택된 테이블 타입
type SelectedTable = 'project' | 'standard' | 'vendor' | null
-// TODO: 나머지 테이블들도 ref 지원 추가 시 인터페이스 추가 필요
-// interface StandardAvlTableRef {
-// getSelectedIds?: () => number[]
-// }
-//
-// interface VendorPoolTableRef {
-// getSelectedIds?: () => number[]
-// }
-
-
// 선택 상태 액션 타입
type SelectionAction =
| { type: 'SELECT_PROJECT'; count: number }
diff --git a/lib/avl/table/avl-table-columns.tsx b/lib/avl/table/avl-table-columns.tsx
index 6ec2c3db..06005d3d 100644
--- a/lib/avl/table/avl-table-columns.tsx
+++ b/lib/avl/table/avl-table-columns.tsx
@@ -45,9 +45,7 @@ export function getColumns({ selectedRows = [], onRowSelect }: GetColumnsProps):
enableSorting: false,
enableHiding: false,
enableResizing: false,
- size: 10,
- minSize: 10,
- maxSize: 10,
+ size: 50,
},
// No 컬럼
{
@@ -57,7 +55,7 @@ export function getColumns({ selectedRows = [], onRowSelect }: GetColumnsProps):
),
cell: ({ getValue }) => <div className="text-center">{getValue() as number}</div>,
enableResizing: true,
- size: 60,
+ size: 100,
},
// AVL 분류 컬럼
{
@@ -148,7 +146,7 @@ export function getColumns({ selectedRows = [], onRowSelect }: GetColumnsProps):
{
accessorKey: "htDivision",
header: ({ column }) => (
- <DataTableColumnHeaderSimple column={column} title="H/T 구분" />
+ <DataTableColumnHeaderSimple column={column} title="H/T" />
),
cell: ({ getValue }) => {
const value = getValue() as string
@@ -205,7 +203,7 @@ export function getColumns({ selectedRows = [], onRowSelect }: GetColumnsProps):
<DataTableColumnHeaderSimple column={column} title="자재그룹" />
),
enableResizing: true,
- size: 120,
+ size: 100,
},
// 협력업체 컬럼
{
@@ -214,7 +212,7 @@ export function getColumns({ selectedRows = [], onRowSelect }: GetColumnsProps):
<DataTableColumnHeaderSimple column={column} title="협력업체" />
),
enableResizing: true,
- size: 150,
+ size: 100,
},
// Tier 컬럼
{
@@ -280,7 +278,7 @@ export function getColumns({ selectedRows = [], onRowSelect }: GetColumnsProps):
return <div className="text-center text-sm">{date}</div>
},
enableResizing: true,
- size: 100,
+ size: 120,
},
// 최종변경자 컬럼
{
@@ -293,7 +291,7 @@ export function getColumns({ selectedRows = [], onRowSelect }: GetColumnsProps):
return <div className="text-center text-sm">{value}</div>
},
enableResizing: true,
- size: 100,
+ size: 120,
},
// 액션 컬럼
{
diff --git a/lib/avl/table/avl-table.tsx b/lib/avl/table/avl-table.tsx
index 61db658d..9b6ac90b 100644
--- a/lib/avl/table/avl-table.tsx
+++ b/lib/avl/table/avl-table.tsx
@@ -100,9 +100,9 @@ export function AvlTable({ data, pageCount, isLoading, onRegistrationModeChange,
const handleAction = React.useCallback(async (action: string, data?: Partial<AvlListItem>) => {
try {
switch (action) {
- case 'standard-registration':
- // 표준 AVL 등록
- const result = await handleAvlActionAction('standard-registration')
+ case 'avl-registration':
+ // AVL 등록 (통합된 기능)
+ const result = await handleAvlActionAction('avl-registration')
if (result.success) {
toast.success(result.message)
onRegistrationModeChange?.('standard') // 등록 모드 변경 콜백 호출
@@ -112,9 +112,10 @@ export function AvlTable({ data, pageCount, isLoading, onRegistrationModeChange,
break
case 'view-detail':
- // 상세 조회 (페이지 이동)
+ // 상세 조회 (페이지 이동) - 원래 방식으로 복원
if (data?.id && !String(data.id).startsWith('temp-')) {
- window.location.href = `/evcp/avl/${data.id}`
+ console.log('AVL 상세보기 이동:', data.id) // 디버깅용
+ window.location.href = `/ko/evcp/avl/${data.id}`
}
break
@@ -177,6 +178,9 @@ export function AvlTable({ data, pageCount, isLoading, onRegistrationModeChange,
columnSizing: {},
},
getRowId: (row) => String(row.id),
+ meta: {
+ onAction: handleAction,
+ },
})
return (
@@ -191,17 +195,9 @@ export function AvlTable({ data, pageCount, isLoading, onRegistrationModeChange,
<Button
variant="outline"
size="sm"
- onClick={() => handleAction('standard-registration')}
- >
- 표준AVL등록
- </Button>
-
- <Button
- variant="outline"
- size="sm"
- onClick={() => handleAction('project-registration')}
+ onClick={() => handleAction('avl-registration')}
>
- 프로젝트AVL등록
+ AVL 등록
</Button>
</div>
diff --git a/lib/avl/table/project-avl-table.tsx b/lib/avl/table/project-avl-table.tsx
index 9584c6f9..ad72b221 100644
--- a/lib/avl/table/project-avl-table.tsx
+++ b/lib/avl/table/project-avl-table.tsx
@@ -177,12 +177,40 @@ export const ProjectAvlTable = forwardRef<ProjectAvlTableRef, ProjectAvlTablePro
const projectData = await getProjectInfoByCode(projectCode.trim(), 'both')
if (projectData) {
+ // constructionSector 동적 결정
+ let constructionSector = "해양" // 기본값
+ if (projectData.source === 'projects') {
+ // projects 테이블의 경우: type='ship'이면 '조선', 그 외는 '해양'
+ constructionSector = projectData.type === 'ship' ? "조선" : "해양"
+ } else if (projectData.source === 'biddingProjects') {
+ // biddingProjects 테이블의 경우: sector가 'S'이면 '조선', 'M'이면 '해양'
+ constructionSector = projectData.sector === 'S' ? "조선" : "해양"
+ }
+
+ // htDivision 동적 결정
+ let htDivision = "" // 기본값
+ if (constructionSector === "조선") {
+ // constructionSector가 '조선'인 경우는 항상 H
+ htDivision = "H"
+ } else if (projectData.source === 'projects') {
+ // projects에서는 TYPE_MDG 컬럼이 Top이면 T, Hull이면 H
+ htDivision = projectData.typeMdg === 'Top' ? "T" : "H"
+ } else if (projectData.source === 'biddingProjects') {
+ if (projectData.sector === 'S') {
+ // biddingProjects에서 sector가 S이면 HtDivision은 항상 H
+ htDivision = "H"
+ } else if (projectData.sector === 'M') {
+ // biddingProjects에서 sector가 M인 경우: pjtType이 TOP이면 'T', HULL이면 'H'
+ htDivision = projectData.pjtType === 'TOP' ? "T" : "H"
+ }
+ }
+
// 프로젝트 정보 설정
setProjectInfo({
projectName: projectData.projectName || "",
- constructionSector: "조선", // 기본값으로 조선 설정 (필요시 로직 변경)
+ constructionSector: constructionSector,
shipType: projectData.shipType || projectData.projectMsrm || "",
- htDivision: projectData.projectHtDivision || ""
+ htDivision: htDivision
})
// 검색 상태 설정
@@ -307,6 +335,7 @@ export const ProjectAvlTable = forwardRef<ProjectAvlTableRef, ProjectAvlTablePro
getFilteredRowModel: getFilteredRowModel(),
manualPagination: true,
pageCount,
+ columnResizeMode: "onChange",
state: {
pagination,
},
diff --git a/lib/avl/table/standard-avl-table.tsx b/lib/avl/table/standard-avl-table.tsx
index bacb5812..06fa6931 100644
--- a/lib/avl/table/standard-avl-table.tsx
+++ b/lib/avl/table/standard-avl-table.tsx
@@ -123,6 +123,7 @@ export const StandardAvlTable = forwardRef<StandardAvlTableRef, StandardAvlTable
getFilteredRowModel: getFilteredRowModel(),
manualPagination: true,
pageCount,
+ columnResizeMode: "onChange",
state: {
pagination,
},
@@ -150,11 +151,8 @@ export const StandardAvlTable = forwardRef<StandardAvlTableRef, StandardAvlTable
},
})
- // 공사부문 변경 시 선종 초기화
const handleConstructionSectorChange = React.useCallback((value: string) => {
setSearchConstructionSector(value)
- // 공사부문이 변경되면 선종을 빈 값으로 초기화
- setSelectedShipType(undefined)
}, [])
// 검색 상태 변경 시 부모 컴포넌트에 전달
@@ -506,7 +504,7 @@ export const StandardAvlTable = forwardRef<StandardAvlTableRef, StandardAvlTable
<div className="space-y-2">
<label className="text-sm font-medium">공사부문</label>
<Select value={searchConstructionSector} onValueChange={handleConstructionSectorChange}>
- <SelectTrigger>
+ <SelectTrigger className="bg-background">
<SelectValue />
</SelectTrigger>
<SelectContent>
@@ -526,7 +524,6 @@ export const StandardAvlTable = forwardRef<StandardAvlTableRef, StandardAvlTable
selectedShipType={selectedShipType}
onShipTypeSelect={setSelectedShipType}
placeholder="선종을 선택하세요"
- disabled={!searchConstructionSector}
className="h-10"
/>
</div>
@@ -535,7 +532,7 @@ export const StandardAvlTable = forwardRef<StandardAvlTableRef, StandardAvlTable
<div className="space-y-2">
<label className="text-sm font-medium">AVL종류</label>
<Select value={searchAvlKind} onValueChange={setSearchAvlKind}>
- <SelectTrigger>
+ <SelectTrigger className="bg-background">
<SelectValue />
</SelectTrigger>
<SelectContent>
@@ -552,7 +549,7 @@ export const StandardAvlTable = forwardRef<StandardAvlTableRef, StandardAvlTable
<div className="space-y-2">
<label className="text-sm font-medium">H/T 구분</label>
<Select value={searchHtDivision} onValueChange={setSearchHtDivision}>
- <SelectTrigger>
+ <SelectTrigger className="bg-background">
<SelectValue />
</SelectTrigger>
<SelectContent>
diff --git a/lib/avl/table/vendor-pool-table.tsx b/lib/avl/table/vendor-pool-table.tsx
index 7ad9eb56..3ac67928 100644
--- a/lib/avl/table/vendor-pool-table.tsx
+++ b/lib/avl/table/vendor-pool-table.tsx
@@ -174,6 +174,7 @@ export const VendorPoolTable = forwardRef<VendorPoolTableRef, VendorPoolTablePro
getFilteredRowModel: getFilteredRowModel(),
manualPagination: !showAll, // 전체보기 시에는 수동 페이징 비활성화
pageCount: showAll ? 1 : pageCount, // 전체보기 시 1페이지, 일반 모드에서는 API에서 받은 pageCount 사용
+ columnResizeMode: "onChange",
state: {
pagination: showAll ? { pageIndex: 0, pageSize: data.length } : pagination,
},
@@ -262,7 +263,7 @@ export const VendorPoolTable = forwardRef<VendorPoolTableRef, VendorPoolTablePro
placeholder="설계공종, 업체명, 자재그룹 등으로 검색..."
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
- className="flex-1"
+ className="flex-1 bg-background"
onKeyPress={(e) => {
if (e.key === 'Enter') {
handleSearch()
diff --git a/lib/vendor-pool/excel-utils.ts b/lib/vendor-pool/excel-utils.ts
new file mode 100644
index 00000000..8dd8743c
--- /dev/null
+++ b/lib/vendor-pool/excel-utils.ts
@@ -0,0 +1,310 @@
+"use client"
+
+import ExcelJS from 'exceljs'
+import { saveAs } from 'file-saver'
+import type { VendorPoolItem } from './table/vendor-pool-table-columns'
+
+// Excel 컬럼 정의
+export interface ExcelColumnConfig {
+ accessorKey: string
+ header: string
+ width?: number
+ type?: 'text' | 'number' | 'boolean' | 'date'
+}
+
+// vendor-pool Excel 컬럼 매핑
+export const vendorPoolExcelColumns: ExcelColumnConfig[] = [
+ { accessorKey: 'constructionSector', header: '조선/해양', width: 15 },
+ { accessorKey: 'htDivision', header: 'H/T구분', width: 15 },
+ { accessorKey: 'designCategoryCode', header: '설계기능코드', width: 20 },
+ { accessorKey: 'designCategory', header: '설계기능(공종)', width: 25 },
+ { accessorKey: 'equipBulkDivision', header: 'Equip/Bulk', width: 15 },
+ { accessorKey: 'packageCode', header: '패키지코드', width: 20 },
+ { accessorKey: 'packageName', header: '패키지명', width: 25 },
+ { accessorKey: 'materialGroupCode', header: '자재그룹코드', width: 20 },
+ { accessorKey: 'materialGroupName', header: '자재그룹명', width: 30 },
+ { accessorKey: 'smCode', header: 'SM Code', width: 15 },
+ { accessorKey: 'similarMaterialNamePurchase', header: '유사자재명(구매)', width: 25 },
+ { accessorKey: 'similarMaterialNameOther', header: '유사자재명(기타)', width: 25 },
+ { accessorKey: 'vendorCode', header: '협력업체코드', width: 20 },
+ { accessorKey: 'vendorName', header: '협력업체명', width: 25 },
+ { accessorKey: 'taxId', header: '사업자번호', width: 20 },
+ { accessorKey: 'faTarget', header: 'FA대상', width: 15, type: 'boolean' },
+ { accessorKey: 'faStatus', header: 'FA현황', width: 15 },
+ { accessorKey: 'tier', header: '등급', width: 15 },
+ { accessorKey: 'isAgent', header: 'Agent여부', width: 15, type: 'boolean' },
+ { accessorKey: 'contractSignerCode', header: '계약서명주체코드', width: 20 },
+ { accessorKey: 'contractSignerName', header: '계약서명주체명', width: 25 },
+ { accessorKey: 'headquarterLocation', header: '본사위치', width: 20 },
+ { accessorKey: 'manufacturingLocation', header: '제작/선적지', width: 20 },
+ { accessorKey: 'avlVendorName', header: 'AVL등재업체명', width: 25 },
+ { accessorKey: 'similarVendorName', header: '유사업체명', width: 25 },
+ { accessorKey: 'hasAvl', header: 'AVL보유', width: 15, type: 'boolean' },
+ { accessorKey: 'isBlacklist', header: '블랙리스트', width: 15, type: 'boolean' },
+ { accessorKey: 'isBcc', header: 'BCC', width: 15, type: 'boolean' },
+ { accessorKey: 'purchaseOpinion', header: '구매의견', width: 30 },
+ // 선종
+ { accessorKey: 'shipTypeCommon', header: '선종공통', width: 15, type: 'boolean' },
+ { accessorKey: 'shipTypeAmax', header: 'A-MAX', width: 15, type: 'boolean' },
+ { accessorKey: 'shipTypeSmax', header: 'S-MAX', width: 15, type: 'boolean' },
+ { accessorKey: 'shipTypeVlcc', header: 'VLCC', width: 15, type: 'boolean' },
+ { accessorKey: 'shipTypeLngc', header: 'LNGC', width: 15, type: 'boolean' },
+ { accessorKey: 'shipTypeCont', header: '컨테이너선', width: 15, type: 'boolean' },
+ // 해양플랜트
+ { accessorKey: 'offshoreTypeCommon', header: '해양플랜트공통', width: 20, type: 'boolean' },
+ { accessorKey: 'offshoreTypeFpso', header: 'FPSO', width: 15, type: 'boolean' },
+ { accessorKey: 'offshoreTypeFlng', header: 'FLNG', width: 15, type: 'boolean' },
+ { accessorKey: 'offshoreTypeFpu', header: 'FPU', width: 15, type: 'boolean' },
+ { accessorKey: 'offshoreTypePlatform', header: '플랫폼', width: 15, type: 'boolean' },
+ { accessorKey: 'offshoreTypeWtiv', header: 'WTIV', width: 15, type: 'boolean' },
+ { accessorKey: 'offshoreTypeGom', header: 'GOM', width: 15, type: 'boolean' },
+ // 담당자 정보
+ { accessorKey: 'picName', header: '담당자명', width: 20 },
+ { accessorKey: 'picEmail', header: '담당자이메일', width: 30 },
+ { accessorKey: 'picPhone', header: '담당자연락처', width: 20 },
+ // 대행사 정보
+ { accessorKey: 'agentName', header: '대행사명', width: 20 },
+ { accessorKey: 'agentEmail', header: '대행사이메일', width: 30 },
+ { accessorKey: 'agentPhone', header: '대행사연락처', width: 20 },
+ // 최근 거래 정보
+ { accessorKey: 'recentQuoteDate', header: '최근견적일', width: 20 },
+ { accessorKey: 'recentQuoteNumber', header: '최근견적번호', width: 25 },
+ { accessorKey: 'recentOrderDate', header: '최근발주일', width: 20 },
+ { accessorKey: 'recentOrderNumber', header: '최근발주번호', width: 25 },
+]
+
+// 값 변환 헬퍼 함수
+const formatCellValue = (value: unknown, type?: string): string => {
+ if (value === null || value === undefined) return ''
+
+ switch (type) {
+ case 'boolean':
+ return value ? 'TRUE' : 'FALSE'
+ case 'date':
+ if (value instanceof Date) {
+ return value.toISOString().split('T')[0]
+ }
+ return String(value)
+ default:
+ return String(value)
+ }
+}
+
+// Excel 템플릿 생성
+export async function createVendorPoolTemplate(filename?: string) {
+ const workbook = new ExcelJS.Workbook()
+ const worksheet = workbook.addWorksheet('VendorPool Template')
+
+ // 1. 안내 텍스트 추가 (첫 번째 행)
+ const instructionText = '벤더풀 데이터 입력 템플릿 - 입력 방법은 "입력 가이드" 시트를 참조하세요'
+ worksheet.getCell(1, 1).value = instructionText
+ worksheet.getCell(1, 1).font = { bold: true, color: { argb: 'FF0066CC' } }
+ worksheet.getCell(1, 1).fill = {
+ type: 'pattern',
+ pattern: 'solid',
+ fgColor: { argb: 'FFFFCC00' }
+ }
+ // 첫 번째 행을 여러 컬럼에 걸쳐 병합
+ worksheet.mergeCells(1, 1, 1, Math.min(vendorPoolExcelColumns.length, 10))
+
+ // 2. 헤더 행 추가 (두 번째 행)
+ const headerRow = worksheet.addRow(vendorPoolExcelColumns.map(col => col.header))
+ headerRow.font = { bold: true }
+ headerRow.fill = {
+ type: 'pattern',
+ pattern: 'solid',
+ fgColor: { argb: 'FFE0E0E0' }
+ }
+
+ // 3. 컬럼 너비 설정
+ vendorPoolExcelColumns.forEach((col, index) => {
+ const column = worksheet.getColumn(index + 1)
+ column.width = col.width || 15
+
+ // 헤더 셀 스타일 설정 (두 번째 행)
+ const headerCell = worksheet.getCell(2, index + 1)
+ headerCell.border = {
+ top: { style: 'thin' },
+ left: { style: 'thin' },
+ bottom: { style: 'thin' },
+ right: { style: 'thin' }
+ }
+ headerCell.alignment = { vertical: 'middle', horizontal: 'center' }
+ })
+
+ // 3. 데이터 입력을 위한 빈 행 한 개 추가 (사용자가 바로 입력할 수 있도록)
+ worksheet.addRow(vendorPoolExcelColumns.map(() => ''))
+
+ // 5. 가이드 워크시트 추가
+ const guideWorksheet = workbook.addWorksheet('입력 가이드')
+
+ // 가이드 제목
+ guideWorksheet.getCell(1, 1).value = '벤더풀 데이터 입력 가이드'
+ guideWorksheet.getCell(1, 1).font = { bold: true, size: 16, color: { argb: 'FF0066CC' } }
+
+ // 가이드 내용
+ const guideContent = [
+ '',
+ '■ 필수 입력 필드 (빨간색 * 표시)',
+ ' - 조선/해양: "조선" 또는 "해양"',
+ ' - H/T구분: "H", "T", 또는 "공통"',
+ ' - 설계기능코드: 2자리 이하 영문코드 (예: EL, ME)',
+ ' - 설계기능(공종): 설계기능 한글명 (예: 전장, 기관)',
+ ' - Equip/Bulk: "E" 또는 "B" (1자리)',
+ ' - 협력업체명: 업체 한글명',
+ ' - 자재그룹코드/명: 해당 자재그룹 정보',
+ ' - 등급: "Tier 1", "Tier 2", "Tier 3" 등',
+ ' - 계약서명주체명: 계약 주체 업체명',
+ ' - 본사위치/제작선적지: 국가명',
+ ' - AVL등재업체명: AVL에 등재된 업체명',
+ '',
+ '■ Boolean (참/거짓) 필드 입력법',
+ ' - TRUE, true, 1, Y, y, O, o → 참',
+ ' - FALSE, false, 0, N, n → 거짓',
+ ' - 빈 값은 기본적으로 거짓(false)으로 처리',
+ '',
+ '■ 주의사항',
+ ' - 첫 번째 행의 안내 텍스트는 삭제하지 마세요',
+ ' - 헤더 행(2번째 행)은 수정하지 마세요',
+ ' - 데이터는 3번째 행부터 입력하세요',
+ '',
+ '■ 문제 해결',
+ ' - 필드 길이 초과 오류: 해당 필드의 글자 수를 확인하세요',
+ ' - 필수 필드 누락: 빨간색 * 표시 필드를 모두 입력했는지 확인하세요',
+ ' - Boolean 값 오류: TRUE/FALSE 형태로 입력했는지 확인하세요'
+ ]
+
+ guideContent.forEach((content, index) => {
+ const cell = guideWorksheet.getCell(index + 2, 1)
+ cell.value = content
+ if (content.startsWith('■')) {
+ cell.font = { bold: true, color: { argb: 'FF333333' } }
+ } else if (content.startsWith(' -')) {
+ cell.font = { color: { argb: 'FF666666' } }
+ }
+ })
+
+ // 가이드 워크시트 컬럼 너비 설정
+ guideWorksheet.getColumn(1).width = 80
+
+ // 파일 저장
+ const buffer = await workbook.xlsx.writeBuffer()
+ const blob = new Blob([buffer], {
+ type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
+ })
+
+ const defaultFilename = `vendor-pool-template-${new Date().toISOString().split('T')[0]}.xlsx`
+ saveAs(blob, filename || defaultFilename)
+}
+
+// Excel 데이터 내보내기
+export async function exportVendorPoolToExcel(
+ data: VendorPoolItem[],
+ filename?: string,
+ includeIds: boolean = true
+) {
+ const workbook = new ExcelJS.Workbook()
+ const worksheet = workbook.addWorksheet('VendorPool Data')
+
+ // ID 제외할 컬럼들 필터링
+ const columnsToExport = includeIds
+ ? vendorPoolExcelColumns
+ : vendorPoolExcelColumns.filter(col => col.accessorKey !== 'id')
+
+ // 헤더 행 추가
+ const headerRow = worksheet.addRow(columnsToExport.map(col => col.header))
+ headerRow.font = { bold: true }
+ headerRow.fill = {
+ type: 'pattern',
+ pattern: 'solid',
+ fgColor: { argb: 'FFE0E0E0' }
+ }
+
+ // 데이터 행 추가
+ data.forEach(item => {
+ const rowData = columnsToExport.map(col => {
+ const value = item[col.accessorKey as keyof VendorPoolItem]
+ return formatCellValue(value, col.type)
+ })
+ worksheet.addRow(rowData)
+ })
+
+ // 컬럼 너비 및 스타일 설정
+ columnsToExport.forEach((col, index) => {
+ const column = worksheet.getColumn(index + 1)
+ column.width = col.width || 15
+
+ // 헤더 셀 스타일 설정
+ const headerCell = worksheet.getCell(1, index + 1)
+ headerCell.border = {
+ top: { style: 'thin' },
+ left: { style: 'thin' },
+ bottom: { style: 'thin' },
+ right: { style: 'thin' }
+ }
+ headerCell.alignment = { vertical: 'middle', horizontal: 'center' }
+ })
+
+ // 데이터 셀 테두리 추가
+ for (let row = 2; row <= worksheet.rowCount; row++) {
+ for (let col = 1; col <= columnsToExport.length; col++) {
+ const cell = worksheet.getCell(row, col)
+ cell.border = {
+ top: { style: 'thin' },
+ left: { style: 'thin' },
+ bottom: { style: 'thin' },
+ right: { style: 'thin' }
+ }
+ }
+ }
+
+ // 파일 저장
+ const buffer = await workbook.xlsx.writeBuffer()
+ const blob = new Blob([buffer], {
+ type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
+ })
+
+ const defaultFilename = `vendor-pool-${new Date().toISOString().split('T')[0]}.xlsx`
+ saveAs(blob, filename || defaultFilename)
+}
+
+// Excel 컬럼 헤더로 accessorKey 찾기
+export function getAccessorKeyByHeader(header: string): string | undefined {
+ const column = vendorPoolExcelColumns.find(col => col.header === header)
+ return column?.accessorKey
+}
+
+// accessorKey로 Excel 헤더 찾기
+export function getHeaderByAccessorKey(accessorKey: string): string | undefined {
+ const column = vendorPoolExcelColumns.find(col => col.accessorKey === accessorKey)
+ return column?.header
+}
+
+// Boolean 값 파싱
+export function parseBoolean(value: string): boolean {
+ const lowerValue = value.toLowerCase().trim()
+ return lowerValue === 'true' || lowerValue === '1' || lowerValue === 'yes' ||
+ lowerValue === 'o' || lowerValue === 'y' || lowerValue === '참'
+}
+
+// Excel 셀 값을 문자열로 변환
+export function getCellValueAsString(cell: ExcelJS.Cell): string {
+ if (!cell || cell.value === undefined || cell.value === null) return '';
+
+ if (typeof cell.value === 'string') return cell.value.trim();
+ if (typeof cell.value === 'number') return cell.value.toString();
+
+ // Handle rich text
+ if (typeof cell.value === 'object' && cell.value && 'richText' in cell.value) {
+ const richTextValue = cell.value as { richText: Array<{ text: string }> };
+ return richTextValue.richText.map((rt) => rt.text).join('');
+ }
+
+ // Handle dates
+ if (cell.value instanceof Date) {
+ return cell.value.toISOString().split('T')[0];
+ }
+
+ // Fallback
+ return String(cell.value);
+}
diff --git a/lib/vendor-pool/service.ts b/lib/vendor-pool/service.ts
index 1933c199..3d83e3aa 100644
--- a/lib/vendor-pool/service.ts
+++ b/lib/vendor-pool/service.ts
@@ -27,8 +27,10 @@ const _getVendorPools = async (input: GetVendorPoolSchema) => {
whereConditions.push(
or(
ilike(vendorPool.constructionSector, searchTerm),
+ ilike(vendorPool.designCategoryCode, searchTerm),
ilike(vendorPool.designCategory, searchTerm),
ilike(vendorPool.vendorName, searchTerm),
+ ilike(vendorPool.materialGroupCode, searchTerm),
ilike(vendorPool.materialGroupName, searchTerm),
ilike(vendorPool.packageName, searchTerm),
ilike(vendorPool.avlVendorName, searchTerm),
@@ -37,7 +39,190 @@ const _getVendorPools = async (input: GetVendorPoolSchema) => {
);
}
- // 필터 조건 추가
+ // Advanced filters 처리 (DataTableFilterList에서 생성된 필터들)
+ if (input.filters && input.filters.length > 0) {
+ const filterConditions: any[] = [];
+
+ for (const filter of input.filters) {
+ let condition: any = null;
+
+ switch (filter.id) {
+ case 'constructionSector':
+ if (filter.operator === 'eq') {
+ condition = eq(vendorPool.constructionSector, filter.value as string);
+ } else if (filter.operator === 'iLike') {
+ condition = ilike(vendorPool.constructionSector, `%${filter.value}%`);
+ }
+ break;
+ case 'htDivision':
+ if (filter.operator === 'eq') {
+ condition = eq(vendorPool.htDivision, filter.value as string);
+ } else if (filter.operator === 'iLike') {
+ condition = ilike(vendorPool.htDivision, `%${filter.value}%`);
+ }
+ break;
+ case 'designCategoryCode':
+ if (filter.operator === 'iLike') {
+ condition = ilike(vendorPool.designCategoryCode, `%${filter.value}%`);
+ } else if (filter.operator === 'eq') {
+ condition = eq(vendorPool.designCategoryCode, filter.value as string);
+ }
+ break;
+ case 'designCategory':
+ if (filter.operator === 'iLike') {
+ condition = ilike(vendorPool.designCategory, `%${filter.value}%`);
+ } else if (filter.operator === 'eq') {
+ condition = eq(vendorPool.designCategory, filter.value as string);
+ }
+ break;
+ case 'packageCode':
+ if (filter.operator === 'iLike') {
+ condition = ilike(vendorPool.packageCode, `%${filter.value}%`);
+ } else if (filter.operator === 'eq') {
+ condition = eq(vendorPool.packageCode, filter.value as string);
+ }
+ break;
+ case 'packageName':
+ if (filter.operator === 'iLike') {
+ condition = ilike(vendorPool.packageName, `%${filter.value}%`);
+ } else if (filter.operator === 'eq') {
+ condition = eq(vendorPool.packageName, filter.value as string);
+ }
+ break;
+ case 'materialGroupCode':
+ if (filter.operator === 'iLike') {
+ condition = ilike(vendorPool.materialGroupCode, `%${filter.value}%`);
+ } else if (filter.operator === 'eq') {
+ condition = eq(vendorPool.materialGroupCode, filter.value as string);
+ }
+ break;
+ case 'materialGroupName':
+ if (filter.operator === 'iLike') {
+ condition = ilike(vendorPool.materialGroupName, `%${filter.value}%`);
+ } else if (filter.operator === 'eq') {
+ condition = eq(vendorPool.materialGroupName, filter.value as string);
+ }
+ break;
+ case 'vendorCode':
+ if (filter.operator === 'iLike') {
+ condition = ilike(vendorPool.vendorCode, `%${filter.value}%`);
+ } else if (filter.operator === 'eq') {
+ condition = eq(vendorPool.vendorCode, filter.value as string);
+ }
+ break;
+ case 'vendorName':
+ if (filter.operator === 'iLike') {
+ condition = ilike(vendorPool.vendorName, `%${filter.value}%`);
+ } else if (filter.operator === 'eq') {
+ condition = eq(vendorPool.vendorName, filter.value as string);
+ }
+ break;
+ case 'taxId':
+ if (filter.operator === 'iLike') {
+ condition = ilike(vendorPool.taxId, `%${filter.value}%`);
+ } else if (filter.operator === 'eq') {
+ condition = eq(vendorPool.taxId, filter.value as string);
+ }
+ break;
+ case 'faStatus':
+ if (filter.operator === 'iLike') {
+ condition = ilike(vendorPool.faStatus, `%${filter.value}%`);
+ } else if (filter.operator === 'eq') {
+ condition = eq(vendorPool.faStatus, filter.value as string);
+ }
+ break;
+ case 'tier':
+ if (filter.operator === 'iLike') {
+ condition = ilike(vendorPool.tier, `%${filter.value}%`);
+ } else if (filter.operator === 'eq') {
+ condition = eq(vendorPool.tier, filter.value as string);
+ }
+ break;
+ case 'headquarterLocation':
+ if (filter.operator === 'iLike') {
+ condition = ilike(vendorPool.headquarterLocation, `%${filter.value}%`);
+ } else if (filter.operator === 'eq') {
+ condition = eq(vendorPool.headquarterLocation, filter.value as string);
+ }
+ break;
+ case 'manufacturingLocation':
+ if (filter.operator === 'iLike') {
+ condition = ilike(vendorPool.manufacturingLocation, `%${filter.value}%`);
+ } else if (filter.operator === 'eq') {
+ condition = eq(vendorPool.manufacturingLocation, filter.value as string);
+ }
+ break;
+ case 'avlVendorName':
+ if (filter.operator === 'iLike') {
+ condition = ilike(vendorPool.avlVendorName, `%${filter.value}%`);
+ } else if (filter.operator === 'eq') {
+ condition = eq(vendorPool.avlVendorName, filter.value as string);
+ }
+ break;
+ case 'registrant':
+ if (filter.operator === 'iLike') {
+ condition = ilike(vendorPool.registrant, `%${filter.value}%`);
+ } else if (filter.operator === 'eq') {
+ condition = eq(vendorPool.registrant, filter.value as string);
+ }
+ break;
+ case 'lastModifier':
+ if (filter.operator === 'iLike') {
+ condition = ilike(vendorPool.lastModifier, `%${filter.value}%`);
+ } else if (filter.operator === 'eq') {
+ condition = eq(vendorPool.lastModifier, filter.value as string);
+ }
+ break;
+ case 'hasAvl':
+ if (filter.operator === 'eq') {
+ condition = eq(vendorPool.hasAvl, filter.value === 'true');
+ }
+ break;
+ case 'isAgent':
+ if (filter.operator === 'eq') {
+ condition = eq(vendorPool.isAgent, filter.value === 'true');
+ }
+ break;
+ case 'isBlacklist':
+ if (filter.operator === 'eq') {
+ condition = eq(vendorPool.isBlacklist, filter.value === 'true');
+ }
+ break;
+ case 'isBcc':
+ if (filter.operator === 'eq') {
+ condition = eq(vendorPool.isBcc, filter.value === 'true');
+ }
+ break;
+ case 'registrationDate':
+ case 'lastModifiedDate':
+ // 날짜 필터 처리 (단순화된 버전)
+ if (filter.operator === 'eq' && filter.value) {
+ const dateValue = new Date(filter.value as string);
+ if (filter.id === 'registrationDate') {
+ condition = eq(vendorPool.registrationDate, dateValue);
+ } else {
+ condition = eq(vendorPool.lastModifiedDate, dateValue);
+ }
+ }
+ break;
+ }
+
+ if (condition) {
+ filterConditions.push(condition);
+ }
+ }
+
+ // 필터 조건들을 AND 또는 OR로 결합
+ if (filterConditions.length > 0) {
+ if (input.joinOperator === 'or') {
+ whereConditions.push(or(...filterConditions));
+ } else {
+ whereConditions.push(and(...filterConditions));
+ }
+ }
+ }
+
+ // 기존 필터 조건 추가 (하위 호환성 유지)
if (input.constructionSector) {
whereConditions.push(eq(vendorPool.constructionSector, input.constructionSector));
}
diff --git a/lib/vendor-pool/table/vendor-pool-excel-import-button.tsx b/lib/vendor-pool/table/vendor-pool-excel-import-button.tsx
new file mode 100644
index 00000000..704d4aff
--- /dev/null
+++ b/lib/vendor-pool/table/vendor-pool-excel-import-button.tsx
@@ -0,0 +1,258 @@
+/**
+ * 특정 컬럼들 복합키로 묶어 UPDATE 처리해야 함.
+ */
+
+"use client"
+
+import React, { useRef } from 'react'
+import ExcelJS from 'exceljs'
+import { toast } from 'sonner'
+import { Button } from '@/components/ui/button'
+import { Upload, Loader } from 'lucide-react'
+import { createVendorPool } from '../service'
+import { Input } from '@/components/ui/input'
+import { useSession } from "next-auth/react"
+import {
+ getCellValueAsString,
+ parseBoolean,
+ getAccessorKeyByHeader,
+ vendorPoolExcelColumns
+} from '../excel-utils'
+
+interface ImportExcelProps {
+ onSuccess?: () => void
+}
+
+export function ImportVendorPoolButton({ onSuccess }: ImportExcelProps) {
+ const fileInputRef = useRef<HTMLInputElement>(null)
+ const [isImporting, setIsImporting] = React.useState(false)
+ const { data: session, status } = useSession()
+
+ // 헬퍼 함수들은 excel-utils에서 import
+
+ const handleImport = async (event: React.ChangeEvent<HTMLInputElement>) => {
+ const file = event.target.files?.[0]
+ if (!file) return
+
+ setIsImporting(true)
+
+ try {
+ // Read the Excel file using ExcelJS
+ const data = await file.arrayBuffer()
+ const workbook = new ExcelJS.Workbook()
+ await workbook.xlsx.load(data)
+
+ // Get the first worksheet
+ const worksheet = workbook.getWorksheet(1)
+ if (!worksheet) {
+ toast.error("No worksheet found in the spreadsheet")
+ return
+ }
+
+ // Check if there's an instruction row (템플릿 안내 텍스트가 있는지 확인)
+ const firstRowText = getCellValueAsString(worksheet.getRow(1).getCell(1));
+ const hasInstructionRow = firstRowText.includes('벤더풀 데이터 입력 템플릿') ||
+ firstRowText.includes('입력 가이드 시트') ||
+ firstRowText.includes('입력 가이드') ||
+ (worksheet.getRow(1).getCell(1).value !== null &&
+ worksheet.getRow(1).getCell(2).value === null);
+
+ // Get header row index (row 2 if there's an instruction row, otherwise row 1)
+ const headerRowIndex = hasInstructionRow ? 2 : 1;
+
+ // Get column headers and their indices
+ const headerRow = worksheet.getRow(headerRowIndex);
+ const columnIndices: Record<string, number> = {};
+
+ headerRow.eachCell((cell, colNumber) => {
+ const header = getCellValueAsString(cell);
+ // Excel 헤더를 통해 accessorKey 찾기
+ const accessorKey = getAccessorKeyByHeader(header);
+ if (accessorKey) {
+ columnIndices[accessorKey] = colNumber;
+ }
+ });
+
+ // Process data rows
+ const rows: any[] = [];
+ const startRow = headerRowIndex + 1;
+
+ for (let i = startRow; i <= worksheet.rowCount; i++) {
+ const row = worksheet.getRow(i);
+
+ // Skip empty rows
+ if (row.cellCount === 0) continue;
+
+ // Check if this is likely an empty template row (빈 템플릿 행 건너뛰기)
+ let hasAnyData = false;
+ for (let col = 1; col <= row.cellCount; col++) {
+ if (getCellValueAsString(row.getCell(col)).trim()) {
+ hasAnyData = true;
+ break;
+ }
+ }
+ if (!hasAnyData) continue;
+
+ const rowData: Record<string, any> = {};
+ let hasData = false;
+
+ // Map the data using accessorKey indices
+ Object.entries(columnIndices).forEach(([accessorKey, colIndex]) => {
+ const value = getCellValueAsString(row.getCell(colIndex));
+ if (value) {
+ rowData[accessorKey] = value;
+ hasData = true;
+ }
+ });
+
+ if (hasData) {
+ rows.push(rowData);
+ }
+ }
+
+ if (rows.length === 0) {
+ toast.error("No data found in the spreadsheet")
+ setIsImporting(false)
+ return
+ }
+
+ // Process each row
+ let successCount = 0;
+ let errorCount = 0;
+
+ // Create promises for all vendor pool creation operations
+ const promises = rows.map(async (row) => {
+ try {
+ // Excel 컬럼 설정을 기반으로 데이터 매핑
+ const vendorPoolData: any = {};
+
+ vendorPoolExcelColumns.forEach(column => {
+ const { accessorKey, type } = column;
+ const value = row[accessorKey] || '';
+
+ if (type === 'boolean') {
+ vendorPoolData[accessorKey] = parseBoolean(String(value));
+ } else if (value === '') {
+ // 빈 문자열은 null로 설정 (스키마에 맞게)
+ vendorPoolData[accessorKey] = null;
+ } else {
+ vendorPoolData[accessorKey] = String(value);
+ }
+ });
+
+ // 현재 사용자 정보 추가
+ vendorPoolData.registrant = session?.user?.name || 'system';
+ vendorPoolData.lastModifier = session?.user?.name || 'system';
+
+ // Validate required fields
+ if (!vendorPoolData.constructionSector || !vendorPoolData.htDivision ||
+ !vendorPoolData.designCategory || !vendorPoolData.vendorName ||
+ !vendorPoolData.designCategoryCode || !vendorPoolData.equipBulkDivision) {
+ console.error("Missing required fields", vendorPoolData);
+ errorCount++;
+ return null;
+ }
+
+ // Validate field lengths and formats
+ const validationErrors: string[] = [];
+
+ if (vendorPoolData.designCategoryCode && vendorPoolData.designCategoryCode.length > 2) {
+ validationErrors.push(`설계기능코드는 2자리 이하여야 합니다: ${vendorPoolData.designCategoryCode}`);
+ }
+
+ if (vendorPoolData.equipBulkDivision && vendorPoolData.equipBulkDivision.length > 1) {
+ validationErrors.push(`Equip/Bulk 구분은 1자리여야 합니다: ${vendorPoolData.equipBulkDivision}`);
+ }
+
+ if (vendorPoolData.constructionSector && !['조선', '해양'].includes(vendorPoolData.constructionSector)) {
+ validationErrors.push(`공사부문은 '조선' 또는 '해양'이어야 합니다: ${vendorPoolData.constructionSector}`);
+ }
+
+ if (vendorPoolData.htDivision && !['H', 'T', '공통'].includes(vendorPoolData.htDivision)) {
+ validationErrors.push(`H/T구분은 'H', 'T' 또는 '공통'이어야 합니다: ${vendorPoolData.htDivision}`);
+ }
+
+ if (validationErrors.length > 0) {
+ console.error("Validation errors:", validationErrors, vendorPoolData);
+ errorCount++;
+ return null;
+ }
+
+ if (!session || !session.user || !session.user.id) {
+ toast.error("인증 오류. 로그인 정보를 찾을 수 없습니다.")
+ return
+ }
+
+ // Create the vendor pool entry
+ const result = await createVendorPool(vendorPoolData as any)
+
+ if (!result) {
+ console.error(`Failed to import row - createVendorPool returned null:`, vendorPoolData);
+ errorCount++;
+ return null;
+ }
+
+ successCount++;
+ return result;
+ } catch (error) {
+ console.error("Error processing row:", error, row);
+ errorCount++;
+ return null;
+ }
+ });
+
+ // Wait for all operations to complete
+ await Promise.all(promises);
+
+ // Show results
+ if (successCount > 0) {
+ toast.success(`${successCount}개 항목이 성공적으로 가져와졌습니다.`);
+ if (errorCount > 0) {
+ toast.warning(`${errorCount}개 항목 가져오기에 실패했습니다. 콘솔에서 자세한 오류를 확인하세요.`);
+ }
+ // Call the success callback to refresh data
+ onSuccess?.();
+ } else if (errorCount > 0) {
+ toast.error(`모든 ${errorCount}개 항목 가져오기에 실패했습니다. 데이터 형식을 확인하세요.`);
+ }
+
+ } catch (error) {
+ console.error("Import error:", error);
+ toast.error("Error importing data. Please check file format.");
+ } finally {
+ setIsImporting(false);
+ // Reset the file input
+ if (fileInputRef.current) {
+ fileInputRef.current.value = '';
+ }
+ }
+ }
+
+ return (
+ <>
+ <Input
+ type="file"
+ ref={fileInputRef}
+ onChange={handleImport}
+ accept=".xlsx,.xls"
+ className="hidden"
+ />
+ <Button
+ variant="outline"
+ size="sm"
+ onClick={() => fileInputRef.current?.click()}
+ disabled={isImporting}
+ className="gap-2"
+ >
+ {isImporting ? (
+ <Loader className="size-4 animate-spin" aria-hidden="true" />
+ ) : (
+ <Upload className="size-4" aria-hidden="true" />
+ )}
+ <span className="hidden sm:inline">
+ {isImporting ? "Importing..." : "Import"}
+ </span>
+ </Button>
+ </>
+ )
+}
diff --git a/lib/vendor-pool/table/vendor-pool-table-columns.tsx b/lib/vendor-pool/table/vendor-pool-table-columns.tsx
index 8f09e684..1f0c455e 100644
--- a/lib/vendor-pool/table/vendor-pool-table-columns.tsx
+++ b/lib/vendor-pool/table/vendor-pool-table-columns.tsx
@@ -1,7 +1,7 @@
import { Checkbox } from "@/components/ui/checkbox"
import { Button } from "@/components/ui/button"
-import { MoreHorizontal, Eye, Edit, Trash2 } from "lucide-react"
-import { type ColumnDef, TableMeta } from "@tanstack/react-table"
+import { Trash2 } from "lucide-react"
+import { type ColumnDef } from "@tanstack/react-table"
import { DataTableColumnHeaderSimple } from "@/components/data-table/data-table-column-simple-header"
import { EditableCell } from "@/components/data-table/editable-cell"
@@ -11,1566 +11,1530 @@ const getIsModified = (table: any, rowId: string, fieldName: string) => {
return String(rowId) in pendingChanges && fieldName in pendingChanges[String(rowId)]
}
-// 테이블 메타 타입 확장
-declare module "@tanstack/react-table" {
- interface TableMeta<TData> {
- onCellUpdate?: (id: string, field: keyof TData, newValue: any) => Promise<void>
- onCellCancel?: (id: string, field: keyof TData) => void
- onAction?: (action: string, data?: any) => void
- onSaveEmptyRow?: (tempId: string) => Promise<void>
- onCancelEmptyRow?: (tempId: string) => void
- isEmptyRow?: (id: string) => boolean
- onTaxIdChange?: (id: string, taxId: string) => Promise<void>
- onMaterialGroupCodeChange?: (id: string, materialGroupCode: string) => Promise<void>
- }
+// vendor-pool 테이블 메타 타입 (복사본)
+interface VendorPoolTableMeta {
+ onCellUpdate?: (id: string | number, field: string, newValue: any) => Promise<void>
+ onCellCancel?: (id: string | number, field: string) => void
+ onAction?: (action: string, data?: any) => void
+ onSaveEmptyRow?: (tempId: string) => Promise<void>
+ onCancelEmptyRow?: (tempId: string) => void
+ isEmptyRow?: (id: string) => boolean
+ onTaxIdChange?: (id: string, taxId: string) => Promise<void>
}
-// Vendor Pool 데이터 타입
-export type VendorPoolItem = {
- id: string
- no: number
- selected: boolean
- constructionSector: string // 공사부문: 조선 또는 해양
- htDivision: string // H/T구분: H 또는 T
- designCategoryCode: string // 설계기능(공종) 코드: 2자리 영문대문자
- designCategory: string // 설계기능(공종): 전장 등
- equipBulkDivision: string // Equip/Bulk 구분: E 또는 B
- // 패키지 정보 (스키마: packageCode, packageName)
- packageCode: string
- packageName: string
- // 자재그룹 (스키마: materialGroupCode, materialGroupName)
- materialGroupCode: string
- materialGroupName: string
- smCode: string // SM Code
- similarMaterialNamePurchase: string // 유사자재명 (구매)
- similarMaterialNameOther: string // 유사자재명 (구매 외)
- // 협력업체 정보 (스키마: vendorCode, vendorName)
- vendorCode: string
- vendorName: string
- taxId: string // 사업자번호(Tax ID)
- faTarget: boolean // FA대상
- faStatus: string // FA현황
- faRemark: string // FA상세(Remark)
- tier: string // 등급(Tier)
- isAgent: boolean // Agent 여부
- // 계약서명주체 (스키마: contractSignerCode, contractSignerName)
- contractSignerCode: string
- contractSignerName: string
- headquarterLocation: string // 본사 위치(국가)
- manufacturingLocation: string // 제작/선적지(국가)
- avlVendorName: string // AVL 등재업체명
- similarVendorName: string // 유사업체명(기술영업)
- hasAvl: boolean // AVL: 존재여부
- isBlacklist: boolean // Blacklist
- isBcc: boolean // BCC
- purchaseOpinion: string // 구매의견
- // AVL 적용 선종(조선)
- shipTypeCommon: boolean // 공통
- shipTypeAmax: boolean // A-max
- shipTypeSmax: boolean // S-max
- shipTypeVlcc: boolean // VLCC
- shipTypeLngc: boolean // LNGC
- shipTypeCont: boolean // CONT
- // AVL 적용 선종(해양)
- offshoreTypeCommon: boolean // 공통
- offshoreTypeFpso: boolean // FPSO
- offshoreTypeFlng: boolean // FLNG
- offshoreTypeFpu: boolean // FPU
- offshoreTypePlatform: boolean // Platform
- offshoreTypeWtiv: boolean // WTIV
- offshoreTypeGom: boolean // GOM
- // eVCP 미등록 정보
- picName: string // PIC(담당자)
- picEmail: string // PIC(E-mail)
- picPhone: string // PIC(Phone)
- agentName: string // Agent(담당자)
- agentEmail: string // Agent(E-mail)
- agentPhone: string // Agent(Phone)
- // 업체 실적 현황
- recentQuoteDate: string // 최근견적일
- recentQuoteNumber: string // 최근견적번호
- recentOrderDate: string // 최근발주일
- recentOrderNumber: string // 최근발주번호
- // 업데이트 히스토리
- registrationDate: string // 등재일
- registrant: string // 등재자
- lastModifiedDate: string // 최종변경일
- lastModifier: string // 최종변경자
+// Vendor Pool 데이터 타입 - 스키마 기반 + 테이블용 추가 필드
+import type { VendorPool } from "@/db/schema/avl/vendor-pool"
+import { DisciplineCode, EngineeringDisciplineSelector } from "@/components/common/discipline"
+import { MaterialGroupSelectorDialogSingle } from "@/components/common/material/material-group-selector-dialog-single"
+import type { MaterialSearchItem } from "@/lib/material/material-group-service"
+import { VendorTierSelector } from "@/components/common/selectors/vendor-tier/vendor-tier-selector"
+import { VendorSelectorDialogSingle } from "@/components/common/vendor/vendor-selector-dialog-single"
+import type { VendorSearchItem } from "@/components/common/vendor/vendor-service"
+import { PlaceOfShippingSelectorDialogSingle } from "@/components/common/selectors/place-of-shipping/place-of-shipping-selector"
+
+export type VendorPoolItem = Omit<VendorPool, 'registrationDate' | 'lastModifiedDate'> & {
+ id: string | number // temp-로 시작하는 경우 string, 실제 데이터는 number
+ no: number // 테이블 표시용 순번
+ selected: boolean // 테이블 선택 상태
+ registrationDate: string // 표시용 string으로 변환
+ lastModifiedDate: string // 표시용 string으로 변환
}
// 테이블 컬럼 정의
export const columns: ColumnDef<VendorPoolItem>[] = [
- {
- id: "select",
- header: ({ table }) => (
- <Checkbox
- checked={
- table.getIsAllPageRowsSelected() ||
- (table.getIsSomePageRowsSelected() && "indeterminate")
- }
- onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
- aria-label="Select all"
- />
- ),
- cell: ({ row }) => (
- <Checkbox
- checked={row.getIsSelected()}
- onCheckedChange={(value) => row.toggleSelected(!!value)}
- aria-label="Select row"
- />
- ),
- enableSorting: false,
- enableHiding: false,
- size: 40,
- },
- {
- accessorKey: "id",
- header: ({ column }) => (
- <DataTableColumnHeaderSimple column={column} title="No." />
- ),
- cell: ({ row }) => {
- const id = String(row.original.id)
-
- // 빈 행의 경우 No. 표시하지 않음
- if (id.startsWith('temp-')) {
- return <div className="text-sm text-muted-foreground italic">신규</div>
- }
+ {
+ id: "select",
+ header: ({ table }) => (
+ <Checkbox
+ checked={
+ table.getIsAllPageRowsSelected() ||
+ (table.getIsSomePageRowsSelected() && "indeterminate")
+ }
+ onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
+ aria-label="Select all"
+ />
+ ),
+ cell: ({ row }) => (
+ <Checkbox
+ checked={row.getIsSelected()}
+ onCheckedChange={(value) => row.toggleSelected(!!value)}
+ aria-label="Select row"
+ />
+ ),
+ enableSorting: false,
+ enableHiding: false,
+ size: 40,
+ },
+ {
+ accessorKey: "id",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="ID" />
+ ),
+ cell: ({ row }) => {
+ const id = String(row.original.id)
+
+ // 빈 행의 경우 신규 표시
+ if (id.startsWith('temp-')) {
+ return <div className="text-sm text-muted-foreground italic">신규</div>
+ }
- // vendor_pool 테이블의 실제 id 표시
- return <div className="text-sm font-mono">{id}</div>
- },
- size: 60,
- },
- {
- accessorKey: "constructionSector",
- header: ({ column }) => (
- <DataTableColumnHeaderSimple column={column} title={<span className="text-red-600 font-medium">공사부문 *</span>} />
- ),
- cell: ({ row, table }) => {
- const value = row.getValue("constructionSector")
- const isEmptyRow = String(row.original.id).startsWith('temp-')
-
- const onSave = async (newValue: any) => {
- if (table.options.meta?.onCellUpdate) {
- await table.options.meta.onCellUpdate(row.original.id, "constructionSector", newValue)
- }
- }
+ // 실제 ID 표시
+ return <div className="text-sm font-mono">{id}</div>
+ },
+ size: 60,
+ },
+ {
+ accessorKey: "constructionSector",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title={<span className="text-red-600 font-medium">공사부문 *</span>} />
+ ),
+ cell: ({ row, table }) => {
+ const value = row.getValue("constructionSector")
+ const isEmptyRow = String(row.original.id).startsWith('temp-')
+
+ const onSave = async (newValue: any) => {
+ const meta = table.options.meta as VendorPoolTableMeta
+ if (meta?.onCellUpdate) {
+ await meta.onCellUpdate(row.original.id, "constructionSector", newValue)
+ }
+ }
- // 수정 여부 확인
- const isModified = getIsModified(table, row.original.id, "constructionSector")
-
- return (
- <EditableCell
- value={value}
- type="select"
- onSave={onSave}
- options={[
- { label: "조선", value: "조선" },
- { label: "해양", value: "해양" }
- ]}
- placeholder="공사부문 선택"
- autoSave={true}
- disabled={false}
- initialEditMode={isEmptyRow}
- isModified={isModified}
- />
- )
- },
- size: 100,
- },
- {
- accessorKey: "htDivision",
- header: ({ column }) => (
- <DataTableColumnHeaderSimple column={column} title={<span className="text-red-600 font-medium">H/T구분 *</span>} />
- ),
- cell: ({ row, table }) => {
- const value = row.getValue("htDivision")
- const isEmptyRow = String(row.original.id).startsWith('temp-')
- const onSave = async (newValue: any) => {
- if (table.options.meta?.onCellUpdate) {
- await table.options.meta.onCellUpdate(row.original.id, "htDivision", newValue)
- }
- }
+ // 수정 여부 확인
+ const isModified = getIsModified(table, row.original.id, "constructionSector")
- // 수정 여부 확인
- const isModified = getIsModified(table, row.original.id, "htDivision")
-
- return (
- <EditableCell
- value={value}
- type="select"
- onSave={onSave}
- options={[
- { label: "H", value: "H" },
- { label: "T", value: "T" },
- { label: "공통", value: "공통" },
- ]}
- placeholder="H/T 선택"
- autoSave={false}
- initialEditMode={isEmptyRow}
- isModified={isModified}
- />
- )
- },
- size: 80,
- },
- {
- accessorKey: "designCategoryCode",
- header: ({ column }) => (
- <DataTableColumnHeaderSimple column={column} title="설계기능코드" />
- ),
- cell: ({ row, table }) => {
- const value = row.getValue("designCategoryCode")
- const onSave = async (newValue: any) => {
- if (table.options.meta?.onCellUpdate) {
- await table.options.meta.onCellUpdate(row.original.id, "designCategoryCode", newValue)
- }
- }
+ return (
+ <EditableCell
+ value={value}
+ type="select"
+ onSave={onSave}
+ options={[
+ { label: "조선", value: "조선" },
+ { label: "해양", value: "해양" }
+ ]}
+ placeholder="공사부문 선택"
+ autoSave={true}
+ disabled={false}
+ initialEditMode={isEmptyRow}
+ isModified={isModified}
+ />
+ )
+ },
+ size: 100,
+ },
+ {
+ accessorKey: "htDivision",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title={<span className="text-red-600 font-medium">H/T *</span>} />
+ ),
+ cell: ({ row, table }) => {
+ const value = row.getValue("htDivision")
+ const isEmptyRow = String(row.original.id).startsWith('temp-')
+ const onSave = async (newValue: any) => {
+ if (table.options.meta?.onCellUpdate) {
+ await table.options.meta.onCellUpdate(row.original.id, "htDivision", newValue)
+ }
+ }
- // 수정 여부 확인
- const isModified = getIsModified(table, row.original.id, "designCategoryCode")
-
- return (
- <EditableCell
- value={value}
- type="text"
- onSave={onSave}
- placeholder="설계기능코드 입력"
- maxLength={10}
- autoSave={false}
- isModified={isModified}
- />
- )
- },
- size: 120,
- },
- {
- accessorKey: "designCategory",
- header: ({ column }) => (
- <DataTableColumnHeaderSimple column={column} title={<span className="text-red-600 font-medium">설계기능(공종) *</span>} />
- ),
- cell: ({ row, table }) => {
- const value = row.getValue("designCategory")
- const isEmptyRow = String(row.original.id).startsWith('temp-')
- const onSave = async (newValue: any) => {
- if (table.options.meta?.onCellUpdate) {
- await table.options.meta.onCellUpdate(row.original.id, "designCategory", newValue)
- }
- }
+ // 수정 여부 확인
+ const isModified = getIsModified(table, row.original.id, "htDivision")
- // 수정 여부 확인
- const isModified = getIsModified(table, row.original.id, "designCategory")
-
- return (
- <EditableCell
- value={value}
- type="text"
- onSave={onSave}
- placeholder="설계기능(공종) 입력"
- maxLength={50}
- autoSave={false}
- initialEditMode={isEmptyRow}
- isModified={isModified}
- />
- )
- },
- size: 120,
- },
- {
- accessorKey: "equipBulkDivision",
- header: ({ column }) => (
- <DataTableColumnHeaderSimple column={column} title="Equip/Bulk 구분" />
- ),
- cell: ({ row, table }) => {
- const value = row.getValue("equipBulkDivision")
- const onSave = async (newValue: any) => {
- if (table.options.meta?.onCellUpdate) {
- await table.options.meta.onCellUpdate(row.original.id, "equipBulkDivision", newValue)
- }
- }
+ return (
+ <EditableCell
+ value={value}
+ type="select"
+ onSave={onSave}
+ options={[
+ { label: "H", value: "H" },
+ { label: "T", value: "T" },
+ { label: "공통", value: "공통" },
+ ]}
+ placeholder="H/T 선택"
+ autoSave={false}
+ initialEditMode={isEmptyRow}
+ isModified={isModified}
+ />
+ )
+ },
+ size: 80,
+ },
+ {
+ accessorKey: "designCategory",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title={<span className="text-red-600 font-medium">설계기능(공종) *</span>} />
+ ),
+ cell: ({ row, table }) => {
+ const designCategoryCode = row.original.designCategoryCode as string
+ const designCategory = row.original.designCategory as string
+
+ // 현재 선택된 discipline 구성
+ const selectedDiscipline: DisciplineCode | undefined = designCategoryCode && designCategory ? {
+ CD: designCategoryCode,
+ USR_DF_CHAR_18: designCategory
+ } : undefined
+
+ const onDisciplineSelect = async (discipline: DisciplineCode) => {
+ console.log('선택된 설계공종:', discipline)
+ console.log('행 ID:', row.original.id)
+
+ // 설계기능코드와 설계기능(공종) 필드 모두 업데이트
+ if (table.options.meta?.onCellUpdate) {
+ await table.options.meta.onCellUpdate(row.original.id, "designCategoryCode", discipline.CD)
+ await table.options.meta.onCellUpdate(row.original.id, "designCategory", discipline.USR_DF_CHAR_18)
+ } else {
+ console.error('onCellUpdate가 정의되지 않음')
+ }
+ }
- return (
- <EditableCell
- value={value}
- type="select"
- onSave={onSave}
- options={[
- { label: "E (Equip)", value: "E" },
- { label: "B (Bulk)", value: "B" },
- { label: "S (강재)", value: "S" }
- ]}
- placeholder="구분 선택"
- autoSave={false}
- />
- )
- },
- size: 120,
- },
- {
- accessorKey: "packageCode",
- header: ({ column }) => (
- <DataTableColumnHeaderSimple column={column} title="패키지 코드" />
- ),
- cell: ({ row, table }) => {
- const value = row.getValue("packageCode")
- const onSave = async (newValue: any) => {
- if (table.options.meta?.onCellUpdate) {
- await table.options.meta.onCellUpdate(row.original.id, "packageCode", newValue)
- }
- }
+ return (
+ <EngineeringDisciplineSelector
+ selectedDiscipline={selectedDiscipline}
+ onDisciplineSelect={onDisciplineSelect}
+ disabled={false}
+ placeholder="설계공종을 선택하세요"
+ />
+ )
+ },
+ size: 260,
+ },
+ {
+ accessorKey: "equipBulkDivision",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="Equip/Bulk" />
+ ),
+ cell: ({ row, table }) => {
+ const value = row.getValue("equipBulkDivision")
+ const onSave = async (newValue: any) => {
+ if (table.options.meta?.onCellUpdate) {
+ await table.options.meta.onCellUpdate(row.original.id, "equipBulkDivision", newValue)
+ }
+ }
- // 수정 여부 확인
- const isModified = getIsModified(table, row.original.id, "packageCode")
-
- return (
- <EditableCell
- value={value}
- type="text"
- onSave={onSave}
- placeholder="패키지 코드 입력"
- maxLength={50}
- autoSave={false}
- isModified={isModified}
- />
- )
- },
- size: 120,
- },
- {
- accessorKey: "packageName",
- header: ({ column }) => (
- <DataTableColumnHeaderSimple column={column} title="패키지 명" />
- ),
- cell: ({ row, table }) => {
- const value = row.getValue("packageName")
- const isEmptyRow = String(row.original.id).startsWith('temp-')
- const onSave = async (newValue: any) => {
- if (table.options.meta?.onCellUpdate) {
- await table.options.meta.onCellUpdate(row.original.id, "packageName", newValue)
- }
- }
+ return (
+ <EditableCell
+ value={value}
+ type="select"
+ onSave={onSave}
+ options={[
+ { label: "E (Equip)", value: "E" },
+ { label: "B (Bulk)", value: "B" },
+ { label: "S (강재)", value: "S" }
+ ]}
+ placeholder="구분 선택"
+ autoSave={false}
+ />
+ )
+ },
+ size: 120,
+ },
+ {
+ accessorKey: "packageCode",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="패키지 코드" />
+ ),
+ cell: ({ row, table }) => {
+ const value = row.getValue("packageCode")
+ const onSave = async (newValue: any) => {
+ if (table.options.meta?.onCellUpdate) {
+ await table.options.meta.onCellUpdate(row.original.id, "packageCode", newValue)
+ }
+ }
- // 수정 여부 확인
- const isModified = getIsModified(table, row.original.id, "packageName")
-
- return (
- <EditableCell
- value={value}
- type="text"
- onSave={onSave}
- placeholder="패키지 명 입력"
- maxLength={100}
- autoSave={false}
- initialEditMode={isEmptyRow}
- isModified={isModified}
- />
- )
- },
- size: 120,
- },
- {
- accessorKey: "materialGroupCode",
- header: ({ column }) => (
- <DataTableColumnHeaderSimple column={column} title={<span className="text-red-600 font-medium">자재그룹 코드 *</span>} />
- ),
- cell: ({ row, table }) => {
- const value = row.getValue("materialGroupCode")
- const onSave = async (newValue: any) => {
- if (table.options.meta?.onCellUpdate) {
- await table.options.meta.onCellUpdate(row.original.id, "materialGroupCode", newValue)
- }
- }
+ // 수정 여부 확인
+ const isModified = getIsModified(table, row.original.id, "packageCode")
- // 수정 여부 확인
- const isModified = getIsModified(table, row.original.id, "materialGroupCode")
+ return (
+ <EditableCell
+ value={value}
+ type="text"
+ onSave={onSave}
+ placeholder="패키지 코드 입력"
+ maxLength={50}
+ autoSave={false}
+ isModified={isModified}
+ />
+ )
+ },
+ size: 120,
+ },
+ {
+ accessorKey: "packageName",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="패키지 명" />
+ ),
+ cell: ({ row, table }) => {
+ const value = row.getValue("packageName")
+ const isEmptyRow = String(row.original.id).startsWith('temp-')
+ const onSave = async (newValue: any) => {
+ if (table.options.meta?.onCellUpdate) {
+ await table.options.meta.onCellUpdate(row.original.id, "packageName", newValue)
+ }
+ }
- const onChange = async (newValue: any) => {
- if (table.options.meta?.onMaterialGroupCodeChange) {
- await table.options.meta.onMaterialGroupCodeChange(row.original.id, newValue)
- }
- }
+ // 수정 여부 확인
+ const isModified = getIsModified(table, row.original.id, "packageName")
- return (
- <EditableCell
- value={value}
- type="text"
- onSave={onSave}
- onChange={onChange}
- placeholder="자재그룹 코드 입력"
- maxLength={50}
- autoSave={false}
- isModified={isModified}
- />
- )
- },
- size: 120,
- },
- {
- accessorKey: "materialGroupName",
- header: ({ column }) => (
- <DataTableColumnHeaderSimple column={column} title={<span className="text-red-600 font-medium">자재그룹 명 *</span>} />
- ),
- cell: ({ row, table }) => {
- const value = row.getValue("materialGroupName")
- const isEmptyRow = String(row.original.id).startsWith('temp-')
- const onSave = async (newValue: any) => {
- if (table.options.meta?.onCellUpdate) {
- await table.options.meta.onCellUpdate(row.original.id, "materialGroupName", newValue)
- }
+ return (
+ <EditableCell
+ value={value}
+ type="text"
+ onSave={onSave}
+ placeholder="패키지 명 입력"
+ maxLength={100}
+ autoSave={false}
+ initialEditMode={isEmptyRow}
+ isModified={isModified}
+ />
+ )
+ },
+ size: 200,
+ },
+ {
+ accessorKey: "materialGroupName",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title={<span className="text-red-600 font-medium">자재그룹 *</span>} />
+ ),
+ cell: ({ row, table }) => {
+ const materialGroupCode = row.original.materialGroupCode as string
+ const materialGroupName = row.original.materialGroupName as string
+
+ // 현재 선택된 material 구성
+ const selectedMaterial: MaterialSearchItem | null = materialGroupCode && materialGroupName ? {
+ materialGroupCode,
+ materialGroupDescription: materialGroupName,
+ displayText: `${materialGroupCode} - ${materialGroupName}`
+ } : null
+
+ const onMaterialSelect = async (material: MaterialSearchItem | null) => {
+ console.log('선택된 자재그룹:', material)
+
+ if (material) {
+ // 자재그룹코드와 자재그룹명 필드 모두 업데이트
+ if (table.options.meta?.onCellUpdate) {
+ await table.options.meta.onCellUpdate(row.original.id, "materialGroupCode", material.materialGroupCode)
+ await table.options.meta.onCellUpdate(row.original.id, "materialGroupName", material.materialGroupDescription)
}
-
- // 수정 여부 확인
- const isModified = getIsModified(table, row.original.id, "materialGroupName")
-
- return (
- <EditableCell
- value={value}
- type="text"
- onSave={onSave}
- placeholder="자재그룹 명 입력"
- maxLength={100}
- autoSave={false}
- initialEditMode={isEmptyRow}
- isModified={isModified}
- />
- )
- },
- size: 120,
- },
- {
- accessorKey: "smCode",
- header: ({ column }) => (
- <DataTableColumnHeaderSimple column={column} title="SM Code" />
- ),
- cell: ({ row, table }) => {
- const value = row.getValue("smCode")
- const onSave = async (newValue: any) => {
- if (table.options.meta?.onCellUpdate) {
- await table.options.meta.onCellUpdate(row.original.id, "smCode", newValue)
- }
+ } else {
+ // 선택 해제 시 빈 값으로 설정
+ if (table.options.meta?.onCellUpdate) {
+ await table.options.meta.onCellUpdate(row.original.id, "materialGroupCode", "")
+ await table.options.meta.onCellUpdate(row.original.id, "materialGroupName", "")
}
+ }
+ }
- return (
- <EditableCell
- value={value}
- type="text"
- onSave={onSave}
- placeholder="SM Code 입력"
- maxLength={50}
- />
- )
- },
- size: 100,
- },
- {
- accessorKey: "similarMaterialNamePurchase",
- header: ({ column }) => (
- <DataTableColumnHeaderSimple column={column} title="유사자재명(구매)" />
- ),
- cell: ({ row, table }) => {
- const value = row.getValue("similarMaterialNamePurchase")
- const onSave = async (newValue: any) => {
- if (table.options.meta?.onCellUpdate) {
- await table.options.meta.onCellUpdate(row.original.id, "similarMaterialNamePurchase", newValue)
- }
- }
+ return (
+ <MaterialGroupSelectorDialogSingle
+ selectedMaterial={selectedMaterial}
+ onMaterialSelect={onMaterialSelect}
+ disabled={false}
+ triggerLabel="자재그룹 선택"
+ placeholder="자재그룹을 검색하세요..."
+ title="자재그룹 선택"
+ description="필요한 자재그룹을 검색하고 선택해주세요."
+ />
+ )
+ },
+ size: 400,
+ },
+ {
+ accessorKey: "smCode",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="SM Code" />
+ ),
+ cell: ({ row, table }) => {
+ const value = row.getValue("smCode")
+ const onSave = async (newValue: any) => {
+ if (table.options.meta?.onCellUpdate) {
+ await table.options.meta.onCellUpdate(row.original.id, "smCode", newValue)
+ }
+ }
- return (
- <EditableCell
- value={value}
- type="text"
- onSave={onSave}
- placeholder="유사자재명(구매) 입력"
- maxLength={100}
- autoSave={false}
- />
- )
- },
- size: 140,
- },
- {
- accessorKey: "similarMaterialNameOther",
- header: ({ column }) => (
- <DataTableColumnHeaderSimple column={column} title="유사자재명(구매외)" />
- ),
- cell: ({ row, table }) => {
- const value = row.getValue("similarMaterialNameOther")
- const onSave = async (newValue: any) => {
- if (table.options.meta?.onCellUpdate) {
- await table.options.meta.onCellUpdate(row.original.id, "similarMaterialNameOther", newValue)
- }
- }
+ return (
+ <EditableCell
+ value={value}
+ type="text"
+ onSave={onSave}
+ placeholder="SM Code 입력"
+ maxLength={50}
+ />
+ )
+ },
+ size: 200,
+ },
+ {
+ accessorKey: "similarMaterialNamePurchase",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="자재명 (검색 키워드)" />
+ // 이전에는 컬럼명이 '유사자재명(구매외)' 였음.
+ ),
+ cell: ({ row, table }) => {
+ const value = row.getValue("similarMaterialNamePurchase")
+ const onSave = async (newValue: any) => {
+ if (table.options.meta?.onCellUpdate) {
+ await table.options.meta.onCellUpdate(row.original.id, "similarMaterialNamePurchase", newValue)
+ }
+ }
- return (
- <EditableCell
- value={value}
- type="text"
- onSave={onSave}
- placeholder="유사자재명(구매외) 입력"
- maxLength={100}
- autoSave={false}
- />
- )
- },
- size: 140,
- },
- {
- accessorKey: "vendorCode",
- header: ({ column }) => (
- <DataTableColumnHeaderSimple column={column} title="협력업체 코드" />
- ),
- cell: ({ row, table }) => {
- const value = row.getValue("vendorCode")
- const onSave = async (newValue: any) => {
- if (table.options.meta?.onCellUpdate) {
- await table.options.meta.onCellUpdate(row.original.id, "vendorCode", newValue)
- }
- }
+ return (
+ <EditableCell
+ value={value}
+ type="text"
+ onSave={onSave}
+ placeholder="유사자재명(구매) 입력"
+ maxLength={100}
+ autoSave={false}
+ />
+ )
+ },
+ size: 250,
+ },
+ {
+ accessorKey: "similarMaterialNameOther",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="유사자재명(구매외)" />
+ ),
+ cell: ({ row, table }) => {
+ const value = row.getValue("similarMaterialNameOther")
+ const onSave = async (newValue: any) => {
+ if (table.options.meta?.onCellUpdate) {
+ await table.options.meta.onCellUpdate(row.original.id, "similarMaterialNameOther", newValue)
+ }
+ }
- // 수정 여부 확인
- const isModified = getIsModified(table, row.original.id, "vendorCode")
-
- return (
- <EditableCell
- value={value}
- type="text"
- onSave={onSave}
- placeholder="협력업체 코드 입력"
- maxLength={50}
- autoSave={false}
- isModified={isModified}
- />
- )
- },
- size: 130,
- },
- {
- accessorKey: "vendorName",
- header: ({ column }) => (
- <DataTableColumnHeaderSimple column={column} title={<span className="text-red-600 font-medium">협력업체 명 *</span>} />
- ),
- cell: ({ row, table }) => {
- const value = row.getValue("vendorName")
- const isEmptyRow = String(row.original.id).startsWith('temp-')
- const onSave = async (newValue: any) => {
- if (table.options.meta?.onCellUpdate) {
- await table.options.meta.onCellUpdate(row.original.id, "vendorName", newValue)
- }
+ return (
+ <EditableCell
+ value={value}
+ type="text"
+ onSave={onSave}
+ placeholder="유사자재명(구매외) 입력"
+ maxLength={100}
+ autoSave={false}
+ />
+ )
+ },
+ size: 140,
+ },
+ {
+ accessorKey: "vendorSelector",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="협력업체 선택" />
+ ),
+ cell: ({ row, table }) => {
+ const vendorCode = row.original.vendorCode as string
+ const vendorName = row.original.vendorName as string
+
+ // 현재 선택된 vendor 구성
+ const selectedVendor: VendorSearchItem | null = vendorCode && vendorName ? {
+ id: 0, // 실제로는 vendorId가 있어야 하지만 여기서는 임시로 0 사용
+ vendorName,
+ vendorCode: vendorCode || null,
+ status: "ACTIVE", // 임시 값
+ displayText: vendorName + (vendorCode ? ` (${vendorCode})` : "")
+ } : null
+
+ const onVendorSelect = async (vendor: VendorSearchItem | null) => {
+ console.log('선택된 협력업체:', vendor)
+
+ if (vendor) {
+ // 협력업체코드, 협력업체명, 사업자번호 필드 모두 업데이트
+ if (table.options.meta?.onCellUpdate) {
+ await table.options.meta.onCellUpdate(row.original.id, "vendorCode", vendor.vendorCode || "")
+ await table.options.meta.onCellUpdate(row.original.id, "vendorName", vendor.vendorName)
+ await table.options.meta.onCellUpdate(row.original.id, "taxId", vendor.taxId || "")
}
-
- // 수정 여부 확인
- const isModified = getIsModified(table, row.original.id, "vendorName")
-
- return (
- <EditableCell
- value={value}
- type="text"
- onSave={onSave}
- placeholder="협력업체 명 입력"
- maxLength={100}
- autoSave={false}
- initialEditMode={isEmptyRow}
- isModified={isModified}
- />
- )
- },
- size: 130,
- },
- {
- accessorKey: "taxId",
- header: ({ column }) => (
- <DataTableColumnHeaderSimple column={column} title={<span className="text-red-600 font-medium">사업자번호 *</span>} />
- ),
- cell: ({ row, table }) => {
- const value = row.getValue("taxId")
- const onSave = async (newValue: any) => {
- if (table.options.meta?.onCellUpdate) {
- await table.options.meta.onCellUpdate(row.original.id, "taxId", newValue)
- }
+ } else {
+ // 선택 해제 시 빈 값으로 설정
+ if (table.options.meta?.onCellUpdate) {
+ await table.options.meta.onCellUpdate(row.original.id, "vendorCode", "")
+ await table.options.meta.onCellUpdate(row.original.id, "vendorName", "")
+ await table.options.meta.onCellUpdate(row.original.id, "taxId", "")
}
+ }
+ }
- const onChange = async (newValue: any) => {
- if (table.options.meta?.onTaxIdChange) {
- await table.options.meta.onTaxIdChange(row.original.id, newValue)
- }
- }
+ return (
+ <VendorSelectorDialogSingle
+ selectedVendor={selectedVendor}
+ onVendorSelect={onVendorSelect}
+ disabled={false}
+ triggerLabel="협력업체 선택"
+ placeholder="협력업체를 검색하세요..."
+ title="협력업체 선택"
+ description="협력업체를 검색하고 선택해주세요."
+ statusFilter="ACTIVE"
+ />
+ )
+ },
+ size: 150,
+ },
+ {
+ accessorKey: "vendorCode",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="협력업체 코드" />
+ ),
+ cell: ({ row, table }) => {
+ const value = row.getValue("vendorCode")
+ const onSave = async (newValue: any) => {
+ if (table.options.meta?.onCellUpdate) {
+ await table.options.meta.onCellUpdate(row.original.id, "vendorCode", newValue)
+ }
+ }
- return (
- <EditableCell
- value={value}
- type="text"
- onSave={onSave}
- onChange={onChange}
- placeholder="사업자번호 입력"
- maxLength={50}
- />
- )
- },
- size: 120,
- },
- {
- accessorKey: "faTarget",
- header: "FA대상",
- cell: ({ row, table }) => {
- const value = row.getValue("faTarget") as boolean
- const onSave = async (newValue: any) => {
- if (table.options.meta?.onCellUpdate) {
- await table.options.meta.onCellUpdate(row.original.id, "faTarget", newValue)
- }
- }
+ // 수정 여부 확인
+ const isModified = getIsModified(table, row.original.id, "vendorCode")
- // 수정 여부 확인
- const isModified = getIsModified(table, row.original.id, "faTarget")
-
- return (
- <EditableCell
- value={value}
- type="checkbox"
- onSave={onSave}
- autoSave={false}
- isModified={isModified}
- />
- )
- },
- enableSorting: false,
- size: 80,
- },
- {
- accessorKey: "faStatus",
- header: ({ column }) => (
- <DataTableColumnHeaderSimple column={column} title="FA현황" />
- ),
- cell: ({ row, table }) => {
- const value = row.getValue("faStatus")
- const onSave = async (newValue: any) => {
- if (table.options.meta?.onCellUpdate) {
- await table.options.meta.onCellUpdate(row.original.id, "faStatus", newValue)
- }
- }
+ return (
+ <EditableCell
+ value={value}
+ type="text"
+ onSave={onSave}
+ placeholder="협력업체 코드 입력"
+ maxLength={50}
+ autoSave={false}
+ isModified={isModified}
+ />
+ )
+ },
+ size: 130,
+ },
+ {
+ accessorKey: "vendorName",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title={<span className="text-red-600 font-medium">협력업체 명 *</span>} />
+ ),
+ cell: ({ row, table }) => {
+ const value = row.getValue("vendorName")
+ const isEmptyRow = String(row.original.id).startsWith('temp-')
+ const onSave = async (newValue: any) => {
+ if (table.options.meta?.onCellUpdate) {
+ await table.options.meta.onCellUpdate(row.original.id, "vendorName", newValue)
+ }
+ }
- return (
- <EditableCell
- value={value}
- type="text"
- onSave={onSave}
- placeholder="FA현황 입력"
- maxLength={50}
- />
- )
- },
- size: 100,
- },
- {
- accessorKey: "faRemark",
- header: ({ column }) => (
- <DataTableColumnHeaderSimple column={column} title="FA상세" />
- ),
- cell: ({ row, table }) => {
- const value = row.getValue("faRemark")
- const onSave = async (newValue: any) => {
- if (table.options.meta?.onCellUpdate) {
- await table.options.meta.onCellUpdate(row.original.id, "faRemark", newValue)
- }
- }
+ // 수정 여부 확인
+ const isModified = getIsModified(table, row.original.id, "vendorName")
- return (
- <EditableCell
- value={value}
- type="textarea"
- onSave={onSave}
- placeholder="FA상세 입력"
- maxLength={500}
- autoSave={false}
- />
- )
- },
- size: 120,
- },
- {
- accessorKey: "tier",
- header: ({ column }) => (
- <DataTableColumnHeaderSimple column={column} title={<span className="text-red-600 font-medium">등급 *</span>} />
- ),
- cell: ({ row, table }) => {
- const value = row.getValue("tier")
- const onSave = async (newValue: any) => {
- if (table.options.meta?.onCellUpdate) {
- await table.options.meta.onCellUpdate(row.original.id, "tier", newValue)
- }
- }
+ return (
+ <EditableCell
+ value={value}
+ type="text"
+ onSave={onSave}
+ placeholder="협력업체 명 입력"
+ maxLength={100}
+ autoSave={false}
+ initialEditMode={isEmptyRow}
+ isModified={isModified}
+ />
+ )
+ },
+ size: 130,
+ },
+ {
+ accessorKey: "taxId",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title={<span className="text-red-600 font-medium">사업자번호 *</span>} />
+ ),
+ cell: ({ row, table }) => {
+ const value = row.getValue("taxId")
+ const onSave = async (newValue: any) => {
+ if (table.options.meta?.onCellUpdate) {
+ await table.options.meta.onCellUpdate(row.original.id, "taxId", newValue)
+ }
+ }
- return (
- <EditableCell
- value={value}
- type="select"
- onSave={onSave}
- options={[
- { label: "Tier 1", value: "Tier 1" },
- { label: "Tier 2", value: "Tier 2" },
- { label: "Tier 3", value: "Tier 3" },
- { label: "Tier 4", value: "Tier 4" },
- ]}
- placeholder="등급 선택"
- />
- )
- },
- size: 80,
- },
- {
- accessorKey: "isAgent",
- header: "Agent 여부",
- cell: ({ row, table }) => {
- const value = row.getValue("isAgent") as boolean
- const onSave = async (newValue: any) => {
- if (table.options.meta?.onCellUpdate) {
- await table.options.meta.onCellUpdate(row.original.id, "isAgent", newValue)
- }
- }
+ const onChange = async (newValue: any) => {
+ if (table.options.meta?.onTaxIdChange) {
+ await table.options.meta.onTaxIdChange(row.original.id, newValue)
+ }
+ }
- return (
- <EditableCell
- value={value}
- type="checkbox"
- onSave={onSave}
- autoSave={false}
- />
- )
- },
- enableSorting: false,
- size: 100,
- },
- {
- accessorKey: "contractSignerCode",
- header: ({ column }) => (
- <DataTableColumnHeaderSimple column={column} title="계약서명주체 코드" />
- ),
- cell: ({ row, table }) => {
- const value = row.getValue("contractSignerCode")
- const onSave = async (newValue: any) => {
- if (table.options.meta?.onCellUpdate) {
- await table.options.meta.onCellUpdate(row.original.id, "contractSignerCode", newValue)
- }
- }
+ return (
+ <EditableCell
+ value={value}
+ type="text"
+ onSave={onSave}
+ onChange={onChange}
+ placeholder="사업자번호 입력"
+ maxLength={50}
+ />
+ )
+ },
+ size: 120,
+ },
+ {
+ accessorKey: "faTarget",
+ header: "FA대상",
+ cell: ({ row, table }) => {
+ const value = row.getValue("faTarget") as boolean
+ const onSave = async (newValue: any) => {
+ if (table.options.meta?.onCellUpdate) {
+ await table.options.meta.onCellUpdate(row.original.id, "faTarget", newValue)
+ }
+ }
- // 수정 여부 확인
- const isModified = getIsModified(table, row.original.id, "contractSignerCode")
-
- return (
- <EditableCell
- value={value}
- type="text"
- onSave={onSave}
- placeholder="계약서명주체 코드 입력"
- maxLength={50}
- autoSave={false}
- isModified={isModified}
- />
- )
- },
- size: 120,
- },
- {
- accessorKey: "contractSignerName",
- header: ({ column }) => (
- <DataTableColumnHeaderSimple column={column} title={<span className="text-red-600 font-medium">계약서명주체 명 *</span>} />
- ),
- cell: ({ row, table }) => {
- const value = row.getValue("contractSignerName")
- const onSave = async (newValue: any) => {
- if (table.options.meta?.onCellUpdate) {
- await table.options.meta.onCellUpdate(row.original.id, "contractSignerName", newValue)
- }
- }
+ // 수정 여부 확인
+ const isModified = getIsModified(table, row.original.id, "faTarget")
- // 수정 여부 확인
- const isModified = getIsModified(table, row.original.id, "contractSignerName")
-
- return (
- <EditableCell
- value={value}
- type="text"
- onSave={onSave}
- placeholder="계약서명주체 명 입력"
- maxLength={100}
- autoSave={false}
- isModified={isModified}
- />
- )
- },
- size: 120,
- },
- {
- accessorKey: "headquarterLocation",
- header: ({ column }) => (
- <DataTableColumnHeaderSimple column={column} title={<span className="text-red-600 font-medium">본사 위치 *</span>} />
- ),
- cell: ({ row, table }) => {
- const value = row.getValue("headquarterLocation")
- const onSave = async (newValue: any) => {
- if (table.options.meta?.onCellUpdate) {
- await table.options.meta.onCellUpdate(row.original.id, "headquarterLocation", newValue)
- }
- }
+ return (
+ <EditableCell
+ value={value}
+ type="checkbox"
+ onSave={onSave}
+ autoSave={false}
+ isModified={isModified}
+ />
+ )
+ },
+ enableSorting: false,
+ size: 80,
+ },
+ {
+ accessorKey: "faStatus",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="FA현황" />
+ ),
+ cell: ({ row }) => {
+ const value = row.original.faStatus as string
- return (
- <EditableCell
- value={value}
- type="text"
- onSave={onSave}
- placeholder="본사 위치 입력"
- maxLength={50}
- />
- )
- },
- size: 100,
- },
- {
- accessorKey: "manufacturingLocation",
- header: ({ column }) => (
- <DataTableColumnHeaderSimple column={column} title={<span className="text-red-600 font-medium">제작/선적지 *</span>} />
- ),
- cell: ({ row, table }) => {
- const value = row.getValue("manufacturingLocation")
- const onSave = async (newValue: any) => {
- if (table.options.meta?.onCellUpdate) {
- await table.options.meta.onCellUpdate(row.original.id, "manufacturingLocation", newValue)
- }
- }
+ // 'O'인 경우에만 'O'를 표시, 그 외에는 빈 셀
+ const displayValue = value === "O" ? "O" : ""
- return (
- <EditableCell
- value={value}
- type="text"
- onSave={onSave}
- placeholder="제작/선적지 입력"
- maxLength={50}
- />
- )
- },
- size: 110,
- },
- {
- accessorKey: "avlVendorName",
- header: ({ column }) => (
- <DataTableColumnHeaderSimple column={column} title={<span className="text-red-600 font-medium">AVL 등재업체명 *</span>} />
- ),
- cell: ({ row, table }) => {
- const value = row.getValue("avlVendorName")
- const onSave = async (newValue: any) => {
- if (table.options.meta?.onCellUpdate) {
- await table.options.meta.onCellUpdate(row.original.id, "avlVendorName", newValue)
- }
- }
+ return (
+ <div className="px-2 py-1 text-sm text-center">
+ {displayValue}
+ </div>
+ )
+ },
+ size: 120,
+ },
+ // {
+ // accessorKey: "faRemark",
+ // header: ({ column }) => (
+ // <DataTableColumnHeaderSimple column={column} title="FA상세" />
+ // ),
+ // cell: ({ row, table }) => {
+ // const value = row.getValue("faRemark")
+ // const onSave = async (newValue: any) => {
+ // if (table.options.meta?.onCellUpdate) {
+ // await table.options.meta.onCellUpdate(row.original.id, "faRemark", newValue)
+ // }
+ // }
+
+ // return (
+ // <EditableCell
+ // value={value}
+ // type="textarea"
+ // onSave={onSave}
+ // placeholder="FA상세 입력"
+ // maxLength={500}
+ // autoSave={false}
+ // />
+ // )
+ // },
+ // size: 120,
+ // },
+ {
+ accessorKey: "tier",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title={<span className="text-red-600 font-medium">등급 *</span>} />
+ ),
+ cell: ({ row, table }) => {
+ const value = row.original.tier as string
- return (
- <EditableCell
- value={value}
- type="text"
- onSave={onSave}
- placeholder="AVL 등재업체명 입력"
- maxLength={100}
- />
- )
- },
- size: 140,
- },
- {
- accessorKey: "similarVendorName",
- header: ({ column }) => (
- <DataTableColumnHeaderSimple column={column} title="유사업체명" />
- ),
- cell: ({ row, table }) => {
- const value = row.getValue("similarVendorName")
- const onSave = async (newValue: any) => {
- if (table.options.meta?.onCellUpdate) {
- await table.options.meta.onCellUpdate(row.original.id, "similarVendorName", newValue)
- }
- }
+ const onValueChange = async (newValue: string) => {
+ if (table.options.meta?.onCellUpdate) {
+ await table.options.meta.onCellUpdate(row.original.id, "tier", newValue)
+ }
+ }
- return (
- <EditableCell
- value={value}
- type="text"
- onSave={onSave}
- placeholder="유사업체명 입력"
- maxLength={100}
- />
- )
- },
- size: 130,
- },
- {
- accessorKey: "hasAvl",
- header: "AVL",
- cell: ({ row, table }) => {
- const value = row.getValue("hasAvl") as boolean
- const onSave = async (newValue: any) => {
- if (table.options.meta?.onCellUpdate) {
- await table.options.meta.onCellUpdate(row.original.id, "hasAvl", newValue)
- }
- }
+ return (
+ <VendorTierSelector
+ value={value}
+ onValueChange={onValueChange}
+ disabled={false}
+ placeholder="등급 선택"
+ />
+ )
+ },
+ size: 200,
+ },
+ {
+ accessorKey: "isAgent",
+ header: "Agent 여부",
+ cell: ({ row, table }) => {
+ const value = row.getValue("isAgent") as boolean
+ const onSave = async (newValue: any) => {
+ if (table.options.meta?.onCellUpdate) {
+ await table.options.meta.onCellUpdate(row.original.id, "isAgent", newValue)
+ }
+ }
- return (
- <EditableCell
- value={value}
- type="checkbox"
- onSave={onSave}
- autoSave={false}
- />
- )
- },
- enableSorting: false,
- size: 60,
- },
- {
- accessorKey: "isBlacklist",
- header: "Blacklist",
- cell: ({ row, table }) => {
- const value = row.getValue("isBlacklist") as boolean
- const onSave = async (newValue: any) => {
- if (table.options.meta?.onCellUpdate) {
- await table.options.meta.onCellUpdate(row.original.id, "isBlacklist", newValue)
- }
- }
+ return (
+ <EditableCell
+ value={value}
+ type="checkbox"
+ onSave={onSave}
+ autoSave={false}
+ />
+ )
+ },
+ enableSorting: false,
+ size: 100,
+ },
+ {
+ accessorKey: "contractSignerCode",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="계약서명주체 코드" />
+ ),
+ cell: ({ row, table }) => {
+ const value = row.getValue("contractSignerCode")
+ const onSave = async (newValue: any) => {
+ if (table.options.meta?.onCellUpdate) {
+ await table.options.meta.onCellUpdate(row.original.id, "contractSignerCode", newValue)
+ }
+ }
- return (
- <EditableCell
- value={value}
- type="checkbox"
- onSave={onSave}
- autoSave={false}
- />
- )
- },
- enableSorting: false,
- size: 100,
- },
- {
- accessorKey: "isBcc",
- header: "BCC",
- cell: ({ row, table }) => {
- const value = row.getValue("isBcc") as boolean
- const onSave = async (newValue: any) => {
- if (table.options.meta?.onCellUpdate) {
- await table.options.meta.onCellUpdate(row.original.id, "isBcc", newValue)
- }
- }
+ // 수정 여부 확인
+ const isModified = getIsModified(table, row.original.id, "contractSignerCode")
- return (
- <EditableCell
- value={value}
- type="checkbox"
- onSave={onSave}
- autoSave={false}
- />
- )
- },
- enableSorting: false,
- size: 80,
- },
- {
- accessorKey: "purchaseOpinion",
- header: ({ column }) => (
- <DataTableColumnHeaderSimple column={column} title="구매의견" />
- ),
- cell: ({ row, table }) => {
- const value = row.getValue("purchaseOpinion")
- const onSave = async (newValue: any) => {
- if (table.options.meta?.onCellUpdate) {
- await table.options.meta.onCellUpdate(row.original.id, "purchaseOpinion", newValue)
- }
+ return (
+ <EditableCell
+ value={value}
+ type="text"
+ onSave={onSave}
+ placeholder="계약서명주체 코드 입력"
+ maxLength={50}
+ autoSave={false}
+ isModified={isModified}
+ />
+ )
+ },
+ size: 120,
+ },
+ {
+ accessorKey: "contractSignerSelector",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="계약서명주체 선택" />
+ ),
+ cell: ({ row, table }) => {
+ const contractSignerCode = row.original.contractSignerCode as string
+ const contractSignerName = row.original.contractSignerName as string
+
+ // 현재 선택된 contract signer 구성
+ const selectedVendor: VendorSearchItem | null = contractSignerCode && contractSignerName ? {
+ id: 0, // 실제로는 vendorId가 있어야 하지만 여기서는 임시로 0 사용
+ vendorName: contractSignerName,
+ vendorCode: contractSignerCode || null,
+ status: "ACTIVE", // 임시 값
+ displayText: contractSignerName + (contractSignerCode ? ` (${contractSignerCode})` : "")
+ } : null
+
+ const onVendorSelect = async (vendor: VendorSearchItem | null) => {
+ console.log('선택된 계약서명주체:', vendor)
+
+ if (vendor) {
+ // 계약서명주체코드와 계약서명주체명 필드 업데이트
+ // 사업자번호는 협력업체 선택 시에만 업데이트됨 (taxId 필드가 하나만 존재)
+ if (table.options.meta?.onCellUpdate) {
+ await table.options.meta.onCellUpdate(row.original.id, "contractSignerCode", vendor.vendorCode || "")
+ await table.options.meta.onCellUpdate(row.original.id, "contractSignerName", vendor.vendorName)
}
-
- return (
- <EditableCell
- value={value}
- type="textarea"
- onSave={onSave}
- placeholder="구매의견 입력"
- maxLength={500}
- />
- )
- },
- size: 120,
- },
- {
- accessorKey: "shipTypeCommon",
- header: "공통",
- cell: ({ row, table }) => {
- const value = row.getValue("shipTypeCommon") as boolean
- const onSave = async (newValue: any) => {
- if (table.options.meta?.onCellUpdate) {
- await table.options.meta.onCellUpdate(row.original.id, "shipTypeCommon", newValue)
- }
+ } else {
+ // 선택 해제 시 빈 값으로 설정
+ if (table.options.meta?.onCellUpdate) {
+ await table.options.meta.onCellUpdate(row.original.id, "contractSignerCode", "")
+ await table.options.meta.onCellUpdate(row.original.id, "contractSignerName", "")
}
+ }
+ }
- return (
- <EditableCell
- value={value}
- type="checkbox"
- onSave={onSave}
- autoSave={false}
- />
- )
- },
- enableSorting: false,
- size: 80,
- },
- {
- accessorKey: "shipTypeAmax",
- header: "A-max",
- cell: ({ row, table }) => {
- const value = row.getValue("shipTypeAmax") as boolean
- const onSave = async (newValue: any) => {
- if (table.options.meta?.onCellUpdate) {
- await table.options.meta.onCellUpdate(row.original.id, "shipTypeAmax", newValue)
- }
- }
+ return (
+ <VendorSelectorDialogSingle
+ selectedVendor={selectedVendor}
+ onVendorSelect={onVendorSelect}
+ disabled={false}
+ triggerLabel="계약서명주체 선택"
+ placeholder="계약서명주체를 검색하세요..."
+ title="계약서명주체 선택"
+ description="계약서명주체를 검색하고 선택해주세요."
+ statusFilter="ACTIVE"
+ />
+ )
+ },
+ size: 150,
+ },
+ {
+ accessorKey: "contractSignerName",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title={<span className="text-red-600 font-medium">계약서명주체 명 *</span>} />
+ ),
+ cell: ({ row, table }) => {
+ const value = row.getValue("contractSignerName")
+ const onSave = async (newValue: any) => {
+ if (table.options.meta?.onCellUpdate) {
+ await table.options.meta.onCellUpdate(row.original.id, "contractSignerName", newValue)
+ }
+ }
- return (
- <EditableCell
- value={value}
- type="checkbox"
- onSave={onSave}
- autoSave={false}
- />
- )
- },
- enableSorting: false,
- size: 80,
- },
- {
- accessorKey: "shipTypeSmax",
- header: "S-max",
- cell: ({ row, table }) => {
- const value = row.getValue("shipTypeSmax") as boolean
- const onSave = async (newValue: any) => {
- if (table.options.meta?.onCellUpdate) {
- await table.options.meta.onCellUpdate(row.original.id, "shipTypeSmax", newValue)
- }
- }
+ // 수정 여부 확인
+ const isModified = getIsModified(table, row.original.id, "contractSignerName")
- return (
- <EditableCell
- value={value}
- type="checkbox"
- onSave={onSave}
- autoSave={false}
- />
- )
- },
- enableSorting: false,
- size: 80,
- },
- {
- accessorKey: "shipTypeVlcc",
- header: "VLCC",
- cell: ({ row, table }) => {
- const value = row.getValue("shipTypeVlcc") as boolean
- const onSave = async (newValue: any) => {
- if (table.options.meta?.onCellUpdate) {
- await table.options.meta.onCellUpdate(row.original.id, "shipTypeVlcc", newValue)
- }
- }
+ return (
+ <EditableCell
+ value={value}
+ type="text"
+ onSave={onSave}
+ placeholder="계약서명주체 명 입력"
+ maxLength={100}
+ autoSave={false}
+ isModified={isModified}
+ />
+ )
+ },
+ size: 120,
+ },
+ {
+ accessorKey: "headquarterLocation",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title={<span className="text-red-600 font-medium">본사 위치 *</span>} />
+ ),
+ cell: ({ row, table }) => {
+ const value = row.getValue("headquarterLocation")
+ const onSave = async (newValue: any) => {
+ if (table.options.meta?.onCellUpdate) {
+ await table.options.meta.onCellUpdate(row.original.id, "headquarterLocation", newValue)
+ }
+ }
- return (
- <EditableCell
- value={value}
- type="checkbox"
- onSave={onSave}
- autoSave={false}
- />
- )
- },
- enableSorting: false,
- size: 80,
- },
- {
- accessorKey: "shipTypeLngc",
- header: "LNGC",
- cell: ({ row, table }) => {
- const value = row.getValue("shipTypeLngc") as boolean
- const onSave = async (newValue: any) => {
- if (table.options.meta?.onCellUpdate) {
- await table.options.meta.onCellUpdate(row.original.id, "shipTypeLngc", newValue)
- }
- }
+ return (
+ <EditableCell
+ value={value}
+ type="text"
+ onSave={onSave}
+ placeholder="본사 위치 입력"
+ maxLength={50}
+ />
+ )
+ },
+ size: 100,
+ },
+ {
+ accessorKey: "manufacturingLocation",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title={<span className="text-red-600 font-medium">제작/선적지 *</span>} />
+ ),
+ cell: ({ row, table }) => {
+ const manufacturingLocation = row.original.manufacturingLocation as string
- return (
- <EditableCell
- value={value}
- type="checkbox"
- onSave={onSave}
- autoSave={false}
- />
- )
- },
- size: 80,
- },
- {
- accessorKey: "shipTypeCont",
- header: "CONT",
- cell: ({ row, table }) => {
- const value = row.getValue("shipTypeCont") as boolean
- const onSave = async (newValue: any) => {
- if (table.options.meta?.onCellUpdate) {
- await table.options.meta.onCellUpdate(row.original.id, "shipTypeCont", newValue)
- }
- }
+ // 현재 선택된 장소 구성 (description은 알 수 없으므로 null로 설정)
+ const selectedPlace = null // 선택된 장소 표시를 위해 null로 설정
- return (
- <EditableCell
- value={value}
- type="checkbox"
- onSave={onSave}
- autoSave={false}
- />
- )
- },
- size: 80,
- },
- {
- accessorKey: "offshoreTypeCommon",
- header: "공통",
- cell: ({ row, table }) => {
- const value = row.getValue("offshoreTypeCommon") as boolean
- const onSave = async (newValue: any) => {
- if (table.options.meta?.onCellUpdate) {
- await table.options.meta.onCellUpdate(row.original.id, "offshoreTypeCommon", newValue)
- }
- }
+ const onPlaceSelect = async (place: { code: string; description: string } | null) => {
+ console.log('선택된 제작/선적지:', place)
- return (
- <EditableCell
- value={value}
- type="checkbox"
- onSave={onSave}
- autoSave={false}
- />
- )
- },
- size: 80,
- },
- {
- accessorKey: "offshoreTypeFpso",
- header: "FPSO",
- cell: ({ row, table }) => {
- const value = row.getValue("offshoreTypeFpso") as boolean
- const onSave = async (newValue: any) => {
- if (table.options.meta?.onCellUpdate) {
- await table.options.meta.onCellUpdate(row.original.id, "offshoreTypeFpso", newValue)
- }
- }
+ if (table.options.meta?.onCellUpdate) {
+ await table.options.meta.onCellUpdate(row.original.id, "manufacturingLocation", place?.code || "")
+ }
+ }
- return (
- <EditableCell
- value={value}
- type="checkbox"
- onSave={onSave}
- autoSave={false}
- />
- )
- },
- size: 80,
- },
- {
- accessorKey: "offshoreTypeFlng",
- header: "FLNG",
- cell: ({ row, table }) => {
- const value = row.getValue("offshoreTypeFlng") as boolean
- const onSave = async (newValue: any) => {
- if (table.options.meta?.onCellUpdate) {
- await table.options.meta.onCellUpdate(row.original.id, "offshoreTypeFlng", newValue)
- }
- }
+ return (
+ <PlaceOfShippingSelectorDialogSingle
+ selectedPlace={selectedPlace}
+ onPlaceSelect={onPlaceSelect}
+ disabled={false}
+ triggerLabel={manufacturingLocation || "제작/선적지 선택"}
+ placeholder="제작/선적지를 검색하세요..."
+ title="제작/선적지 선택"
+ description="제작/선적지를 검색하고 선택해주세요."
+ />
+ )
+ },
+ size: 200,
+ },
+ {
+ accessorKey: "avlVendorName",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title={<span className="text-red-600 font-medium">AVL 등재업체명 *</span>} />
+ ),
+ cell: ({ row, table }) => {
+ const value = row.getValue("avlVendorName")
+ const onSave = async (newValue: any) => {
+ if (table.options.meta?.onCellUpdate) {
+ await table.options.meta.onCellUpdate(row.original.id, "avlVendorName", newValue)
+ }
+ }
- return (
- <EditableCell
- value={value}
- type="checkbox"
- onSave={onSave}
- autoSave={false}
- />
- )
- },
- size: 80,
- },
- {
- accessorKey: "offshoreTypeFpu",
- header: "FPU",
- cell: ({ row, table }) => {
- const value = row.getValue("offshoreTypeFpu") as boolean
- const onSave = async (newValue: any) => {
- if (table.options.meta?.onCellUpdate) {
- await table.options.meta.onCellUpdate(row.original.id, "offshoreTypeFpu", newValue)
- }
- }
+ return (
+ <EditableCell
+ value={value}
+ type="text"
+ onSave={onSave}
+ placeholder="AVL 등재업체명 입력"
+ maxLength={100}
+ />
+ )
+ },
+ size: 140,
+ },
+ {
+ accessorKey: "similarVendorName",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="업체명 (검색 키워드)" />
+ // 이전에는 컬럼명이 '유사업체명' 였음.
+ ),
+ cell: ({ row, table }) => {
+ const value = row.getValue("similarVendorName")
+ const onSave = async (newValue: any) => {
+ if (table.options.meta?.onCellUpdate) {
+ await table.options.meta.onCellUpdate(row.original.id, "similarVendorName", newValue)
+ }
+ }
- return (
- <EditableCell
- value={value}
- type="checkbox"
- onSave={onSave}
- autoSave={false}
- />
- )
- },
- size: 80,
- },
- {
- accessorKey: "offshoreTypePlatform",
- header: "Platform",
- cell: ({ row, table }) => {
- const value = row.getValue("offshoreTypePlatform") as boolean
- const onSave = async (newValue: any) => {
- if (table.options.meta?.onCellUpdate) {
- await table.options.meta.onCellUpdate(row.original.id, "offshoreTypePlatform", newValue)
- }
- }
+ return (
+ <EditableCell
+ value={value}
+ type="text"
+ onSave={onSave}
+ placeholder="유사업체명 입력"
+ maxLength={100}
+ />
+ )
+ },
+ size: 130,
+ },
+ {
+ accessorKey: "hasAvl",
+ header: "AVL",
+ cell: ({ row, table }) => {
+ const value = row.getValue("hasAvl") as boolean
+ const onSave = async (newValue: any) => {
+ if (table.options.meta?.onCellUpdate) {
+ await table.options.meta.onCellUpdate(row.original.id, "hasAvl", newValue)
+ }
+ }
- return (
- <EditableCell
- value={value}
- type="checkbox"
- onSave={onSave}
- autoSave={false}
- />
- )
- },
- size: 100,
- },
- {
- accessorKey: "offshoreTypeWtiv",
- header: "WTIV",
- cell: ({ row, table }) => {
- const value = row.getValue("offshoreTypeWtiv") as boolean
- const onSave = async (newValue: any) => {
- if (table.options.meta?.onCellUpdate) {
- await table.options.meta.onCellUpdate(row.original.id, "offshoreTypeWtiv", newValue)
- }
- }
+ return (
+ <EditableCell
+ value={value}
+ type="checkbox"
+ onSave={onSave}
+ autoSave={false}
+ />
+ )
+ },
+ enableSorting: false,
+ size: 60,
+ },
+ {
+ accessorKey: "isBlacklist",
+ header: "Blacklist",
+ cell: ({ row, table }) => {
+ const value = row.getValue("isBlacklist") as boolean
+ const onSave = async (newValue: any) => {
+ if (table.options.meta?.onCellUpdate) {
+ await table.options.meta.onCellUpdate(row.original.id, "isBlacklist", newValue)
+ }
+ }
- return (
- <EditableCell
- value={value}
- type="checkbox"
- onSave={onSave}
- autoSave={false}
- />
- )
- },
- size: 80,
- },
- {
- accessorKey: "offshoreTypeGom",
- header: "GOM",
- cell: ({ row, table }) => {
- const value = row.getValue("offshoreTypeGom") as boolean
- const onSave = async (newValue: any) => {
- if (table.options.meta?.onCellUpdate) {
- await table.options.meta.onCellUpdate(row.original.id, "offshoreTypeGom", newValue)
- }
- }
+ return (
+ <EditableCell
+ value={value}
+ type="checkbox"
+ onSave={onSave}
+ autoSave={false}
+ />
+ )
+ },
+ enableSorting: false,
+ size: 60,
+ },
+ {
+ accessorKey: "isBcc",
+ header: "BCC",
+ cell: ({ row, table }) => {
+ const value = row.getValue("isBcc") as boolean
+ const onSave = async (newValue: any) => {
+ if (table.options.meta?.onCellUpdate) {
+ await table.options.meta.onCellUpdate(row.original.id, "isBcc", newValue)
+ }
+ }
- return (
- <EditableCell
- value={value}
- type="checkbox"
- onSave={onSave}
- autoSave={false}
- />
- )
- },
- size: 80,
- },
- {
- accessorKey: "picName",
- header: ({ column }) => (
- <DataTableColumnHeaderSimple column={column} title="PIC(담당자)" />
- ),
- cell: ({ row, table }) => {
- const value = row.getValue("picName")
- const onSave = async (newValue: any) => {
- if (table.options.meta?.onCellUpdate) {
- await table.options.meta.onCellUpdate(row.original.id, "picName", newValue)
- }
- }
+ return (
+ <EditableCell
+ value={value}
+ type="checkbox"
+ onSave={onSave}
+ autoSave={false}
+ />
+ )
+ },
+ enableSorting: false,
+ size: 60,
+ },
+ {
+ accessorKey: "purchaseOpinion",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="구매의견" />
+ ),
+ cell: ({ row, table }) => {
+ const value = row.getValue("purchaseOpinion")
+ const onSave = async (newValue: any) => {
+ if (table.options.meta?.onCellUpdate) {
+ await table.options.meta.onCellUpdate(row.original.id, "purchaseOpinion", newValue)
+ }
+ }
- return (
- <EditableCell
- value={value}
- type="text"
- onSave={onSave}
- placeholder="PIC 담당자명 입력"
- maxLength={50}
- />
- )
- },
- size: 120,
- },
- {
- accessorKey: "picEmail",
- header: ({ column }) => (
- <DataTableColumnHeaderSimple column={column} title="PIC(E-mail)" />
- ),
- cell: ({ row, table }) => {
- const value = row.getValue("picEmail")
- const onSave = async (newValue: any) => {
- if (table.options.meta?.onCellUpdate) {
- await table.options.meta.onCellUpdate(row.original.id, "picEmail", newValue)
- }
- }
- return (
- <EditableCell
- value={value}
- type="text"
- onSave={onSave}
- placeholder="PIC 이메일 입력"
- maxLength={100}
- />
- )
- },
- size: 140,
- },
- {
- accessorKey: "picPhone",
- header: ({ column }) => (
- <DataTableColumnHeaderSimple column={column} title="PIC(Phone)" />
- ),
- cell: ({ row, table }) => {
- const value = row.getValue("picPhone")
- const onSave = async (newValue: any) => {
- if (table.options.meta?.onCellUpdate) {
- await table.options.meta.onCellUpdate(row.original.id, "picPhone", newValue)
- }
- }
+ return (
+ <EditableCell
+ value={value}
+ type="textarea"
+ onSave={onSave}
+ placeholder="구매의견 입력"
+ maxLength={500}
+ />
+ )
+ },
+ size: 300,
+ },
+ {
+ accessorKey: "shipTypeCommon",
+ header: "공통",
+ cell: ({ row, table }) => {
+ const value = row.getValue("shipTypeCommon") as boolean
+ const onSave = async (newValue: any) => {
+ if (table.options.meta?.onCellUpdate) {
+ await table.options.meta.onCellUpdate(row.original.id, "shipTypeCommon", newValue)
+ }
+ }
- return (
- <EditableCell
- value={value}
- type="text"
- onSave={onSave}
- placeholder="PIC 전화번호 입력"
- maxLength={20}
- />
- )
- },
- size: 120,
- },
- {
- accessorKey: "agentName",
- header: ({ column }) => (
- <DataTableColumnHeaderSimple column={column} title="Agent(담당자)" />
- ),
- cell: ({ row, table }) => {
- const value = row.getValue("agentName")
- const onSave = async (newValue: any) => {
- if (table.options.meta?.onCellUpdate) {
- await table.options.meta.onCellUpdate(row.original.id, "agentName", newValue)
- }
- }
+ return (
+ <EditableCell
+ value={value}
+ type="checkbox"
+ onSave={onSave}
+ autoSave={false}
+ />
+ )
+ },
+ enableSorting: false,
+ size: 80,
+ },
+ {
+ accessorKey: "shipTypeAmax",
+ header: "A-max",
+ cell: ({ row, table }) => {
+ const value = row.getValue("shipTypeAmax") as boolean
+ const onSave = async (newValue: any) => {
+ if (table.options.meta?.onCellUpdate) {
+ await table.options.meta.onCellUpdate(row.original.id, "shipTypeAmax", newValue)
+ }
+ }
- return (
- <EditableCell
- value={value}
- type="text"
- onSave={onSave}
- placeholder="Agent 담당자명 입력"
- maxLength={50}
- />
- )
- },
- size: 120,
- },
- {
- accessorKey: "agentEmail",
- header: ({ column }) => (
- <DataTableColumnHeaderSimple column={column} title="Agent(E-mail)" />
- ),
- cell: ({ row, table }) => {
- const value = row.getValue("agentEmail")
- const onSave = async (newValue: any) => {
- if (table.options.meta?.onCellUpdate) {
- await table.options.meta.onCellUpdate(row.original.id, "agentEmail", newValue)
- }
- }
+ return (
+ <EditableCell
+ value={value}
+ type="checkbox"
+ onSave={onSave}
+ autoSave={false}
+ />
+ )
+ },
+ enableSorting: false,
+ size: 60,
+ },
+ {
+ accessorKey: "shipTypeSmax",
+ header: "S-max",
+ cell: ({ row, table }) => {
+ const value = row.getValue("shipTypeSmax") as boolean
+ const onSave = async (newValue: any) => {
+ if (table.options.meta?.onCellUpdate) {
+ await table.options.meta.onCellUpdate(row.original.id, "shipTypeSmax", newValue)
+ }
+ }
- return (
- <EditableCell
- value={value}
- type="text"
- onSave={onSave}
- placeholder="Agent 이메일 입력"
- maxLength={100}
- />
- )
- },
- size: 140,
- },
- {
- accessorKey: "agentPhone",
- header: ({ column }) => (
- <DataTableColumnHeaderSimple column={column} title="Agent(Phone)" />
- ),
- cell: ({ row, table }) => {
- const value = row.getValue("agentPhone")
- const onSave = async (newValue: any) => {
- if (table.options.meta?.onCellUpdate) {
- await table.options.meta.onCellUpdate(row.original.id, "agentPhone", newValue)
- }
- }
+ return (
+ <EditableCell
+ value={value}
+ type="checkbox"
+ onSave={onSave}
+ autoSave={false}
+ />
+ )
+ },
+ enableSorting: false,
+ size: 60,
+ },
+ {
+ accessorKey: "shipTypeVlcc",
+ header: "VLCC",
+ cell: ({ row, table }) => {
+ const value = row.getValue("shipTypeVlcc") as boolean
+ const onSave = async (newValue: any) => {
+ if (table.options.meta?.onCellUpdate) {
+ await table.options.meta.onCellUpdate(row.original.id, "shipTypeVlcc", newValue)
+ }
+ }
- return (
- <EditableCell
- value={value}
- type="text"
- onSave={onSave}
- placeholder="Agent 전화번호 입력"
- maxLength={20}
- />
- )
- },
- size: 120,
- },
- {
- accessorKey: "recentQuoteDate",
- header: ({ column }) => (
- <DataTableColumnHeaderSimple column={column} title="최근견적일" />
- ),
- cell: ({ row, table }) => {
- const value = row.getValue("recentQuoteDate")
- const onSave = async (newValue: any) => {
- if (table.options.meta?.onCellUpdate) {
- await table.options.meta.onCellUpdate(row.original.id, "recentQuoteDate", newValue)
- }
- }
+ return (
+ <EditableCell
+ value={value}
+ type="checkbox"
+ onSave={onSave}
+ autoSave={false}
+ />
+ )
+ },
+ enableSorting: false,
+ size: 60,
+ },
+ {
+ accessorKey: "shipTypeLngc",
+ header: "LNGC",
+ cell: ({ row, table }) => {
+ const value = row.getValue("shipTypeLngc") as boolean
+ const onSave = async (newValue: any) => {
+ if (table.options.meta?.onCellUpdate) {
+ await table.options.meta.onCellUpdate(row.original.id, "shipTypeLngc", newValue)
+ }
+ }
- return (
- <EditableCell
- value={value}
- type="text"
- onSave={onSave}
- placeholder="최근견적일 입력 (YYYY-MM-DD)"
- maxLength={20}
- autoSave={false}
- />
- )
- },
- size: 120,
- },
- {
- accessorKey: "recentQuoteNumber",
- header: ({ column }) => (
- <DataTableColumnHeaderSimple column={column} title="최근견적번호" />
- ),
- cell: ({ row, table }) => {
- const value = row.getValue("recentQuoteNumber")
- const onSave = async (newValue: any) => {
- if (table.options.meta?.onCellUpdate) {
- await table.options.meta.onCellUpdate(row.original.id, "recentQuoteNumber", newValue)
- }
- }
+ return (
+ <EditableCell
+ value={value}
+ type="checkbox"
+ onSave={onSave}
+ autoSave={false}
+ />
+ )
+ },
+ size: 60,
+ },
+ {
+ accessorKey: "shipTypeCont",
+ header: "CONT",
+ cell: ({ row, table }) => {
+ const value = row.getValue("shipTypeCont") as boolean
+ const onSave = async (newValue: any) => {
+ if (table.options.meta?.onCellUpdate) {
+ await table.options.meta.onCellUpdate(row.original.id, "shipTypeCont", newValue)
+ }
+ }
- return (
- <EditableCell
- value={value}
- type="text"
- onSave={onSave}
- placeholder="최근견적번호 입력"
- maxLength={50}
- />
- )
- },
- size: 130,
- },
- {
- accessorKey: "recentOrderDate",
- header: ({ column }) => (
- <DataTableColumnHeaderSimple column={column} title="최근발주일" />
- ),
- cell: ({ row, table }) => {
- const value = row.getValue("recentOrderDate")
- const onSave = async (newValue: any) => {
- if (table.options.meta?.onCellUpdate) {
- await table.options.meta.onCellUpdate(row.original.id, "recentOrderDate", newValue)
- }
- }
+ return (
+ <EditableCell
+ value={value}
+ type="checkbox"
+ onSave={onSave}
+ autoSave={false}
+ />
+ )
+ },
+ size: 60,
+ },
+ {
+ accessorKey: "offshoreTypeCommon",
+ header: "공통",
+ cell: ({ row, table }) => {
+ const value = row.getValue("offshoreTypeCommon") as boolean
+ const onSave = async (newValue: any) => {
+ if (table.options.meta?.onCellUpdate) {
+ await table.options.meta.onCellUpdate(row.original.id, "offshoreTypeCommon", newValue)
+ }
+ }
- return (
- <EditableCell
- value={value}
- type="text"
- onSave={onSave}
- placeholder="최근발주일 입력 (YYYY-MM-DD)"
- maxLength={20}
- autoSave={false}
- />
- )
- },
- size: 120,
- },
- {
- accessorKey: "recentOrderNumber",
- header: ({ column }) => (
- <DataTableColumnHeaderSimple column={column} title="최근발주번호" />
- ),
- cell: ({ row, table }) => {
- const value = row.getValue("recentOrderNumber")
- const onSave = async (newValue: any) => {
- if (table.options.meta?.onCellUpdate) {
- await table.options.meta.onCellUpdate(row.original.id, "recentOrderNumber", newValue)
- }
- }
+ return (
+ <EditableCell
+ value={value}
+ type="checkbox"
+ onSave={onSave}
+ autoSave={false}
+ />
+ )
+ },
+ size: 60,
+ },
+ {
+ accessorKey: "offshoreTypeFpso",
+ header: "FPSO",
+ cell: ({ row, table }) => {
+ const value = row.getValue("offshoreTypeFpso") as boolean
+ const onSave = async (newValue: any) => {
+ if (table.options.meta?.onCellUpdate) {
+ await table.options.meta.onCellUpdate(row.original.id, "offshoreTypeFpso", newValue)
+ }
+ }
+
+ return (
+ <EditableCell
+ value={value}
+ type="checkbox"
+ onSave={onSave}
+ autoSave={false}
+ />
+ )
+ },
+ size: 60,
+ },
+ {
+ accessorKey: "offshoreTypeFlng",
+ header: "FLNG",
+ cell: ({ row, table }) => {
+ const value = row.getValue("offshoreTypeFlng") as boolean
+ const onSave = async (newValue: any) => {
+ if (table.options.meta?.onCellUpdate) {
+ await table.options.meta.onCellUpdate(row.original.id, "offshoreTypeFlng", newValue)
+ }
+ }
+
+ return (
+ <EditableCell
+ value={value}
+ type="checkbox"
+ onSave={onSave}
+ autoSave={false}
+ />
+ )
+ },
+ size: 60,
+ },
+ {
+ accessorKey: "offshoreTypeFpu",
+ header: "FPU",
+ cell: ({ row, table }) => {
+ const value = row.getValue("offshoreTypeFpu") as boolean
+ const onSave = async (newValue: any) => {
+ if (table.options.meta?.onCellUpdate) {
+ await table.options.meta.onCellUpdate(row.original.id, "offshoreTypeFpu", newValue)
+ }
+ }
+
+ return (
+ <EditableCell
+ value={value}
+ type="checkbox"
+ onSave={onSave}
+ autoSave={false}
+ />
+ )
+ },
+ size: 60,
+ },
+ {
+ accessorKey: "offshoreTypePlatform",
+ header: "Platform",
+ cell: ({ row, table }) => {
+ const value = row.getValue("offshoreTypePlatform") as boolean
+ const onSave = async (newValue: any) => {
+ if (table.options.meta?.onCellUpdate) {
+ await table.options.meta.onCellUpdate(row.original.id, "offshoreTypePlatform", newValue)
+ }
+ }
+
+ return (
+ <EditableCell
+ value={value}
+ type="checkbox"
+ onSave={onSave}
+ autoSave={false}
+ />
+ )
+ },
+ size: 60,
+ },
+ {
+ accessorKey: "offshoreTypeWtiv",
+ header: "WTIV",
+ cell: ({ row, table }) => {
+ const value = row.getValue("offshoreTypeWtiv") as boolean
+ const onSave = async (newValue: any) => {
+ if (table.options.meta?.onCellUpdate) {
+ await table.options.meta.onCellUpdate(row.original.id, "offshoreTypeWtiv", newValue)
+ }
+ }
+
+ return (
+ <EditableCell
+ value={value}
+ type="checkbox"
+ onSave={onSave}
+ autoSave={false}
+ />
+ )
+ },
+ size: 60,
+ },
+ {
+ accessorKey: "offshoreTypeGom",
+ header: "GOM",
+ cell: ({ row, table }) => {
+ const value = row.getValue("offshoreTypeGom") as boolean
+ const onSave = async (newValue: any) => {
+ if (table.options.meta?.onCellUpdate) {
+ await table.options.meta.onCellUpdate(row.original.id, "offshoreTypeGom", newValue)
+ }
+ }
+
+ return (
+ <EditableCell
+ value={value}
+ type="checkbox"
+ onSave={onSave}
+ autoSave={false}
+ />
+ )
+ },
+ size: 60,
+ },
+ {
+ accessorKey: "picName",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="협력업체담당자" />
+ // 이전에는 컬럼명이 PIC(담당자) 였음.
+ ),
+ cell: ({ row, table }) => {
+ const value = row.getValue("picName")
+ const onSave = async (newValue: any) => {
+ if (table.options.meta?.onCellUpdate) {
+ await table.options.meta.onCellUpdate(row.original.id, "picName", newValue)
+ }
+ }
+
+ return (
+ <EditableCell
+ value={value}
+ type="text"
+ onSave={onSave}
+ placeholder="입력가능"
+ maxLength={50}
+ />
+ )
+ },
+ size: 120,
+ },
+ {
+ accessorKey: "picEmail",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="협력업체담당자(E-mail)" />
+ // 이전에는 컬럼명이 PIC(E-mail) 였음.
+ ),
+ cell: ({ row, table }) => {
+ const value = row.getValue("picEmail")
+ const onSave = async (newValue: any) => {
+ if (table.options.meta?.onCellUpdate) {
+ await table.options.meta.onCellUpdate(row.original.id, "picEmail", newValue)
+ }
+ }
+ return (
+ <EditableCell
+ value={value}
+ type="text"
+ onSave={onSave}
+ placeholder="입력가능"
+ maxLength={100}
+ />
+ )
+ },
+ size: 140,
+ },
+ {
+ accessorKey: "picPhone",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="협력업체담당자(Phone)" />
+ // 이전에는 컬럼명이 PIC(Phone) 였음.
+ ),
+ cell: ({ row, table }) => {
+ const value = row.getValue("picPhone")
+ const onSave = async (newValue: any) => {
+ if (table.options.meta?.onCellUpdate) {
+ await table.options.meta.onCellUpdate(row.original.id, "picPhone", newValue)
+ }
+ }
+
+ return (
+ <EditableCell
+ value={value}
+ type="text"
+ onSave={onSave}
+ placeholder="입력가능"
+ maxLength={20}
+ />
+ )
+ },
+ size: 120,
+ },
+ {
+ accessorKey: "agentName",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="Agent(담당자)" />
+ ),
+ cell: ({ row, table }) => {
+ const value = row.getValue("agentName")
+ const onSave = async (newValue: any) => {
+ if (table.options.meta?.onCellUpdate) {
+ await table.options.meta.onCellUpdate(row.original.id, "agentName", newValue)
+ }
+ }
+
+ return (
+ <EditableCell
+ value={value}
+ type="text"
+ onSave={onSave}
+ placeholder="입력가능"
+ maxLength={50}
+ />
+ )
+ },
+ size: 120,
+ },
+ {
+ accessorKey: "agentEmail",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="Agent(E-mail)" />
+ ),
+ cell: ({ row, table }) => {
+ const value = row.getValue("agentEmail")
+ const onSave = async (newValue: any) => {
+ if (table.options.meta?.onCellUpdate) {
+ await table.options.meta.onCellUpdate(row.original.id, "agentEmail", newValue)
+ }
+ }
- return (
- <EditableCell
- value={value}
- type="text"
- onSave={onSave}
- placeholder="최근발주번호 입력"
- maxLength={50}
- />
- )
- },
- size: 130,
- },
- {
- accessorKey: "registrationDate",
- header: ({ column }) => (
- <DataTableColumnHeaderSimple column={column} title="등재일" />
- ),
- size: 120,
- },
- {
- accessorKey: "registrant",
- header: ({ column }) => (
- <DataTableColumnHeaderSimple column={column} title="등재자" />
- ),
- cell: ({ row }) => {
- const value = row.getValue("registrant") as string
- return <div className="text-sm">{value || ""}</div>
- },
- size: 100,
- },
- {
- accessorKey: "lastModifiedDate",
- header: ({ column }) => (
- <DataTableColumnHeaderSimple column={column} title="최종변경일" />
- ),
- size: 120,
- },
- {
- accessorKey: "lastModifier",
- header: ({ column }) => (
- <DataTableColumnHeaderSimple column={column} title="최종변경자" />
- ),
- cell: ({ row }) => {
- const value = row.getValue("lastModifier") as string
- return <div className="text-sm">{value || ""}</div>
- },
- size: 120,
- },
+ return (
+ <EditableCell
+ value={value}
+ type="text"
+ onSave={onSave}
+ placeholder="입력가능"
+ maxLength={100}
+ />
+ )
+ },
+ size: 140,
+ },
+ {
+ accessorKey: "agentPhone",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="Agent(Phone)" />
+ ),
+ cell: ({ row, table }) => {
+ const value = row.getValue("agentPhone")
+ const onSave = async (newValue: any) => {
+ if (table.options.meta?.onCellUpdate) {
+ await table.options.meta.onCellUpdate(row.original.id, "agentPhone", newValue)
+ }
+ }
+
+ return (
+ <EditableCell
+ value={value}
+ type="text"
+ onSave={onSave}
+ placeholder="입력가능"
+ maxLength={20}
+ />
+ )
+ },
+ size: 120,
+ },
+ {
+ accessorKey: "recentQuoteDate",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="최근견적일" />
+ ),
+ cell: ({ row }) => {
+ const value = row.getValue("recentQuoteDate") as string
+ return (
+ <div className="px-2 py-1 text-sm">
+ {value || "-"}
+ </div>
+ )
+ },
+ size: 120,
+ },
+ {
+ accessorKey: "recentQuoteNumber",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="최근견적번호" />
+ ),
+ cell: ({ row }) => {
+ const value = row.getValue("recentQuoteNumber") as string
+ return (
+ <div className="px-2 py-1 text-sm">
+ {value || "-"}
+ </div>
+ )
+ },
+ size: 130,
+ },
+ {
+ accessorKey: "recentOrderDate",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="최근발주일" />
+ ),
+ cell: ({ row }) => {
+ const value = row.getValue("recentOrderDate") as string
+ return (
+ <div className="px-2 py-1 text-sm">
+ {value || "-"}
+ </div>
+ )
+ },
+ size: 120,
+ },
+ {
+ accessorKey: "recentOrderNumber",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="최근발주번호" />
+ ),
+ cell: ({ row }) => {
+ const value = row.getValue("recentOrderNumber") as string
+ return (
+ <div className="px-2 py-1 text-sm">
+ {value || "-"}
+ </div>
+ )
+ },
+ size: 130,
+ },
+ {
+ accessorKey: "registrationDate",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="등록일" />
+ ),
+ size: 120,
+ },
+ {
+ accessorKey: "registrant",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="등록자" />
+ ),
+ cell: ({ row }) => {
+ const value = row.getValue("registrant") as string
+ return <div className="text-sm">{value || ""}</div>
+ },
+ size: 100,
+ },
+ {
+ accessorKey: "lastModifiedDate",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="최종변경일" />
+ ),
+ size: 120,
+ },
+ {
+ accessorKey: "lastModifier",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="최종변경자" />
+ ),
+ cell: ({ row }) => {
+ const value = row.getValue("lastModifier") as string
+ return <div className="text-sm">{value || ""}</div>
+ },
+ size: 120,
+ },
// 액션 그룹
{
id: "actions",
diff --git a/lib/vendor-pool/table/vendor-pool-table.tsx b/lib/vendor-pool/table/vendor-pool-table.tsx
index 43dd64c1..46a0588d 100644
--- a/lib/vendor-pool/table/vendor-pool-table.tsx
+++ b/lib/vendor-pool/table/vendor-pool-table.tsx
@@ -16,19 +16,20 @@ import { BulkImportDialog } from "./bulk-import-dialog"
import { columns, type VendorPoolItem } from "./vendor-pool-table-columns"
import { createVendorPool, updateVendorPool, deleteVendorPool } from "../service"
-import type { VendorPool } from "../types"
-
-// 테이블 메타 타입 확장
-declare module "@tanstack/react-table" {
- interface TableMeta<TData> {
- onCellUpdate?: (id: string, field: keyof TData, newValue: any) => Promise<void>
- onCellCancel?: (id: string, field: keyof TData) => void
- onAction?: (action: string, data?: any) => void
- onSaveEmptyRow?: (tempId: string) => Promise<void>
- onCancelEmptyRow?: (tempId: string) => void
- isEmptyRow?: (id: string) => boolean
- getPendingChanges?: () => Record<string, Partial<VendorPoolItem>>
- }
+import type { VendorPool } from "@/db/schema/avl/vendor-pool"
+import { Download, FileSpreadsheet, Upload } from "lucide-react"
+import { ImportVendorPoolButton } from "./vendor-pool-excel-import-button"
+import { exportVendorPoolToExcel, createVendorPoolTemplate } from "../excel-utils"
+
+// vendor-pool 테이블 메타 타입
+interface VendorPoolTableMeta {
+ onCellUpdate?: (id: string | number, field: string, newValue: any) => Promise<void>
+ onCellCancel?: (id: string | number, field: string) => void
+ onAction?: (action: string, data?: any) => void
+ onSaveEmptyRow?: (tempId: string) => Promise<void>
+ onCancelEmptyRow?: (tempId: string) => void
+ isEmptyRow?: (id: string) => boolean
+ getPendingChanges?: () => Record<string, Partial<VendorPoolItem>>
}
interface VendorPoolTableProps {
@@ -37,6 +38,67 @@ interface VendorPoolTableProps {
onRefresh?: () => void // 데이터 새로고침 콜백
}
+// 빈 행 기본값 객체
+const createEmptyVendorPoolBase = (): Omit<VendorPool, 'id'> & { id?: string | number } => ({
+ constructionSector: "",
+ htDivision: "",
+ designCategoryCode: "",
+ designCategory: "",
+ equipBulkDivision: "",
+ packageCode: null,
+ packageName: null,
+ materialGroupCode: null,
+ materialGroupName: null,
+ smCode: null,
+ similarMaterialNamePurchase: null,
+ similarMaterialNameOther: null,
+ vendorCode: null,
+ vendorName: "",
+ taxId: null,
+ faTarget: false,
+ faStatus: null,
+ faRemark: null,
+ tier: null,
+ isAgent: false,
+ contractSignerCode: null,
+ contractSignerName: "",
+ headquarterLocation: "",
+ manufacturingLocation: "",
+ avlVendorName: null,
+ similarVendorName: null,
+ hasAvl: false,
+ isBlacklist: false,
+ isBcc: false,
+ purchaseOpinion: null,
+ shipTypeCommon: false,
+ shipTypeAmax: false,
+ shipTypeSmax: false,
+ shipTypeVlcc: false,
+ shipTypeLngc: false,
+ shipTypeCont: false,
+ offshoreTypeCommon: false,
+ offshoreTypeFpso: false,
+ offshoreTypeFlng: false,
+ offshoreTypeFpu: false,
+ offshoreTypePlatform: false,
+ offshoreTypeWtiv: false,
+ offshoreTypeGom: false,
+ picName: null,
+ picEmail: null,
+ picPhone: null,
+ agentName: null,
+ agentEmail: null,
+ agentPhone: null,
+ recentQuoteDate: null,
+ recentQuoteNumber: null,
+ recentOrderDate: null,
+ recentOrderNumber: null,
+ registrationDate: null,
+ registrant: null,
+ lastModifiedDate: null,
+ lastModifier: null,
+})
+
export function VendorPoolTable({ data, pageCount, onRefresh }: VendorPoolTableProps) {
const { data: session } = useSession()
@@ -54,7 +116,7 @@ export function VendorPoolTable({ data, pageCount, onRefresh }: VendorPoolTableP
// 인라인 편집 핸들러 (일괄 저장용)
- const handleCellUpdate = React.useCallback(async (id: string, field: keyof VendorPoolItem, newValue: any) => {
+ const handleCellUpdate = React.useCallback(async (id: string | number, field: string, newValue: any) => {
const isEmptyRow = String(id).startsWith('temp-')
if (isEmptyRow) {
@@ -81,7 +143,7 @@ export function VendorPoolTable({ data, pageCount, onRefresh }: VendorPoolTableP
// 편집 취소 핸들러
- const handleCellCancel = React.useCallback((id: string, field: keyof VendorPoolItem) => {
+ const handleCellCancel = React.useCallback((id: string | number, field: string) => {
const isEmptyRow = String(id).startsWith('temp-')
if (isEmptyRow) {
@@ -142,13 +204,13 @@ export function VendorPoolTable({ data, pageCount, onRefresh }: VendorPoolTableP
for (const [id, changes] of Object.entries(pendingChanges)) {
try {
// changes에서 id 필드 제거 (서버에서 자동 생성)
- const { id: _, ...updateData } = changes as any
+ const { id: _, no: __, selected: ___, ...updateData } = changes
// 최종변경자를 현재 세션 사용자 정보로 설정
- const updateDataWithModifier = {
+ const updateDataWithModifier: any = {
...updateData,
- lastModifier: session?.user?.name || ""
+ lastModifier: session?.user?.name || null
}
- const result = await updateVendorPool(Number(id), updateDataWithModifier as Partial<VendorPool>)
+ const result = await updateVendorPool(Number(id), updateDataWithModifier)
if (result) {
successCount++
} else {
@@ -190,68 +252,18 @@ export function VendorPoolTable({ data, pageCount, onRefresh }: VendorPoolTableP
if (isCreating) return // 이미 생성 중이면 중복 생성 방지
const tempId = `temp-${Date.now()}`
+ const userName = session?.user?.name || null
+
const emptyRow: VendorPoolItem = {
- id: tempId,
+ ...createEmptyVendorPoolBase(),
+ id: tempId, // string 타입으로 설정
no: 0, // 나중에 계산
selected: false,
- constructionSector: "",
- htDivision: "",
- designCategoryCode: "",
- designCategory: "",
- equipBulkDivision: "",
- packageCode: "",
- packageName: "",
- materialGroupCode: "",
- materialGroupName: "",
- smCode: "",
- similarMaterialNamePurchase: "",
- similarMaterialNameOther: "",
- vendorCode: "",
- vendorName: "",
- taxId: "",
- faTarget: false,
- faStatus: "",
- faRemark: "",
- tier: "",
- isAgent: false,
- contractSignerCode: "",
- contractSignerName: "",
- headquarterLocation: "",
- manufacturingLocation: "",
- avlVendorName: "",
- similarVendorName: "",
- hasAvl: false,
- isBlacklist: false,
- isBcc: false,
- purchaseOpinion: "",
- shipTypeCommon: false,
- shipTypeAmax: false,
- shipTypeSmax: false,
- shipTypeVlcc: false,
- shipTypeLngc: false,
- shipTypeCont: false,
- offshoreTypeCommon: false,
- offshoreTypeFpso: false,
- offshoreTypeFlng: false,
- offshoreTypeFpu: false,
- offshoreTypePlatform: false,
- offshoreTypeWtiv: false,
- offshoreTypeGom: false,
- picName: "",
- picEmail: "",
- picPhone: "",
- agentName: "",
- agentEmail: "",
- agentPhone: "",
- recentQuoteDate: "",
- recentQuoteNumber: "",
- recentOrderDate: "",
- recentOrderNumber: "",
- registrationDate: "",
- registrant: session?.user?.name || "",
+ registrationDate: "", // 빈 행에서는 string으로 표시
+ registrant: userName,
lastModifiedDate: "",
- lastModifier: session?.user?.name || "",
- }
+ lastModifier: userName,
+ } as unknown as VendorPoolItem
setEmptyRows(prev => ({ ...prev, [tempId]: emptyRow }))
setIsCreating(true)
@@ -312,10 +324,10 @@ export function VendorPoolTable({ data, pageCount, onRefresh }: VendorPoolTableP
try {
setIsSaving(true)
- // id 필드 제거 (서버에서 자동 생성)
- const { id: _, no: __, selected: ___, ...createData } = finalData
+ // id, no, selected 필드 제거 및 타입 변환
+ const { id: _, no: __, selected: ___, registrationDate: ____, lastModifiedDate: _____, ...createData } = finalData
- const result = await createVendorPool(createData as Omit<VendorPool, 'id' | 'registrationDate' | 'lastModifiedDate'>)
+ const result = await createVendorPool(createData as any)
if (result) {
toast.success("새 항목이 추가되었습니다.")
@@ -591,8 +603,34 @@ export function VendorPoolTable({ data, pageCount, onRefresh }: VendorPoolTableP
toast.info('저장 기능은 개발 중입니다.')
break
- case 'excel-import':
- toast.info('Excel Import 기능은 개발 중입니다.')
+
+ case 'excel-export':
+ try {
+ // 현재 테이블 데이터를 Excel로 내보내기 (ID 포함)
+ const currentData = table.getFilteredRowModel().rows.map(row => row.original)
+ await exportVendorPoolToExcel(
+ currentData,
+ `vendor-pool-${new Date().toISOString().split('T')[0]}.xlsx`,
+ true // ID 포함
+ )
+ toast.success('Excel 파일이 다운로드되었습니다.')
+ } catch (error) {
+ console.error('Excel export 실패:', error)
+ toast.error('Excel 내보내기에 실패했습니다.')
+ }
+ break
+
+ case 'excel-template':
+ try {
+ // 템플릿 파일 다운로드 (데이터 없음, ID 컬럼 제외)
+ await createVendorPoolTemplate(
+ `vendor-pool-template-${new Date().toISOString().split('T')[0]}.xlsx`
+ )
+ toast.success('Excel 템플릿이 다운로드되었습니다.')
+ } catch (error) {
+ console.error('Excel template export 실패:', error)
+ toast.error('Excel 템플릿 다운로드에 실패했습니다.')
+ }
break
case 'delete':
@@ -634,7 +672,7 @@ export function VendorPoolTable({ data, pageCount, onRefresh }: VendorPoolTableP
// 제공된 값들만 적용 (빈 값이나 undefined는 건너뜀)
Object.entries(bulkData).forEach(([field, value]) => {
if (value !== undefined && value !== null && value !== '') {
- handleCellUpdate(rowId, field as keyof VendorPoolItem, value)
+ handleCellUpdate(rowId, field as keyof VendorPool, value)
}
})
}
@@ -648,7 +686,7 @@ export function VendorPoolTable({ data, pageCount, onRefresh }: VendorPoolTableP
}, [table, handleCellUpdate])
// 테이블 메타에 핸들러 설정
- table.options.meta = {
+ const tableMeta: VendorPoolTableMeta = {
onAction: handleAction,
onCellUpdate: handleCellUpdate,
onCellCancel: handleCellCancel,
@@ -657,6 +695,8 @@ export function VendorPoolTable({ data, pageCount, onRefresh }: VendorPoolTableP
isEmptyRow: (id: string) => String(id).startsWith('temp-'),
getPendingChanges: () => pendingChanges
}
+
+ table.options.meta = tableMeta as any
// 툴바 액션 핸들러들
@@ -699,12 +739,24 @@ export function VendorPoolTable({ data, pageCount, onRefresh }: VendorPoolTableP
일괄입력
</Button>
+ <ImportVendorPoolButton onSuccess={onRefresh} />
+
+ <Button
+ onClick={() => handleToolbarAction('excel-export')}
+ variant="outline"
+ size="sm"
+ >
+ <Download className="mr-2 h-4 w-4" />
+ Excel Export
+ </Button>
+
<Button
- onClick={() => handleToolbarAction('excel-import')}
+ onClick={() => handleToolbarAction('excel-template')}
variant="outline"
size="sm"
>
- Excel Import
+ <FileSpreadsheet className="mr-2 h-4 w-4" />
+ Template
</Button>
<Button