From bf2db28586569499e44b58999f2e0f33ed4cdeb5 Mon Sep 17 00:00:00 2001 From: joonhoekim <26rote@gmail.com> Date: Wed, 24 Sep 2025 17:36:08 +0900 Subject: (김준회) 구매 요청사항 반영 - vendor-pool 및 avl detail (이진용 프로) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/avl/avl-itb-rfq-service.ts | 289 -------------------------------- lib/avl/service.ts | 10 +- lib/avl/table/avl-detail-table.tsx | 70 +------- lib/avl/table/avl-registration-area.tsx | 10 -- lib/avl/table/avl-table-columns.tsx | 16 +- lib/avl/table/avl-table.tsx | 26 ++- lib/avl/table/project-avl-table.tsx | 33 +++- lib/avl/table/standard-avl-table.tsx | 11 +- lib/avl/table/vendor-pool-table.tsx | 3 +- 9 files changed, 58 insertions(+), 410 deletions(-) delete mode 100644 lib/avl/avl-itb-rfq-service.ts (limited to 'lib/avl') 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 { - 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 { - 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 -): Promise { - 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 { 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({ {/* 상단 버튼 영역 */}
- { - // 표준AVL로는 견적요청하지 않으며, 프로젝트 AVL로만 견적요청처리 - avlType === '프로젝트AVL' && businessType && ['조선', '해양'].includes(businessType) && - - } - {/* 단순 이동 버튼 */} -
{/* 데이터 테이블 */} 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 }) =>
{getValue() as number}
, enableResizing: true, - size: 60, + size: 100, }, // AVL 분류 컬럼 { @@ -148,7 +146,7 @@ export function getColumns({ selectedRows = [], onRowSelect }: GetColumnsProps): { accessorKey: "htDivision", header: ({ column }) => ( - + ), cell: ({ getValue }) => { const value = getValue() as string @@ -205,7 +203,7 @@ export function getColumns({ selectedRows = [], onRowSelect }: GetColumnsProps): ), enableResizing: true, - size: 120, + size: 100, }, // 협력업체 컬럼 { @@ -214,7 +212,7 @@ export function getColumns({ selectedRows = [], onRowSelect }: GetColumnsProps): ), enableResizing: true, - size: 150, + size: 100, }, // Tier 컬럼 { @@ -280,7 +278,7 @@ export function getColumns({ selectedRows = [], onRowSelect }: GetColumnsProps): return
{date}
}, enableResizing: true, - size: 100, + size: 120, }, // 최종변경자 컬럼 { @@ -293,7 +291,7 @@ export function getColumns({ selectedRows = [], onRowSelect }: GetColumnsProps): return
{value}
}, 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) => { 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, - - 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 { setSearchConstructionSector(value) - // 공사부문이 변경되면 선종을 빈 값으로 초기화 - setSelectedShipType(undefined) }, []) // 검색 상태 변경 시 부모 컴포넌트에 전달 @@ -506,7 +504,7 @@ export const StandardAvlTable = forwardRef - + @@ -552,7 +549,7 @@ export const StandardAvlTable = forwardRef