diff options
| author | joonhoekim <26rote@gmail.com> | 2025-09-22 18:59:13 +0900 |
|---|---|---|
| committer | joonhoekim <26rote@gmail.com> | 2025-09-22 18:59:13 +0900 |
| commit | ba35e67845f935c8ce0151c9ef1fefa0b0510faf (patch) | |
| tree | d05eb27fab2acc54a839b2590c89e860d58fb747 /lib | |
| parent | e4bd037d158513e45373ad9e1ef13f71af12162a (diff) | |
(김준회) AVL 피드백 반영 (이진용 프로 건)
Diffstat (limited to 'lib')
| -rw-r--r-- | lib/avl/service.ts | 117 | ||||
| -rw-r--r-- | lib/avl/table/avl-registration-area.tsx | 9 | ||||
| -rw-r--r-- | lib/avl/table/avl-table-columns.tsx | 4 | ||||
| -rw-r--r-- | lib/avl/table/avl-vendor-add-and-modify-dialog.tsx | 386 | ||||
| -rw-r--r-- | lib/avl/table/project-avl-add-dialog.tsx | 779 | ||||
| -rw-r--r-- | lib/avl/table/project-avl-table.tsx | 65 | ||||
| -rw-r--r-- | lib/avl/table/standard-avl-add-dialog.tsx | 960 | ||||
| -rw-r--r-- | lib/avl/table/standard-avl-table.tsx | 146 |
8 files changed, 478 insertions, 1988 deletions
diff --git a/lib/avl/service.ts b/lib/avl/service.ts index 3d188f85..0340f52c 100644 --- a/lib/avl/service.ts +++ b/lib/avl/service.ts @@ -552,7 +552,7 @@ export async function createAvlList(data: CreateAvlListInput): Promise<AvlListIt debugSuccess('DB INSERT 완료', { table: 'avl_list', result: result[0], - savedSnapshotLength: result[0].vendorInfoSnapshot?.length + savedSnapshotLength: Array.isArray((result[0] as any).vendorInfoSnapshot) ? (result[0] as any).vendorInfoSnapshot.length : 0 }); const createdItem = result[0]; @@ -800,6 +800,27 @@ export async function createAvlVendorInfo(data: AvlVendorInfoInput): Promise<Avl return transformedData; } catch (err) { debugError('AVL Vendor Info 생성 실패', { error: err, inputData: data }); + + // unique 제약조건 위반 에러 처리 + if (err instanceof Error) { + if (err.message.includes('unique_standard_avl_material_vendor')) { + console.error("Unique constraint violation (standard AVL):", err.message); + // 사용자에게는 친화적인 메시지를 보여주기 위해 null 반환 + // 실제 애플리케이션에서는 이 에러를 catch해서 적절한 메시지 표시 + return null; + } + + if (err.message.includes('unique_project_avl_material_vendor')) { + console.error("Unique constraint violation (project AVL):", err.message); + return null; + } + + if (err.message.includes('duplicate key value violates unique constraint')) { + console.error("Unique constraint violation:", err.message); + return null; + } + } + console.error("Error in createAvlVendorInfo:", err); return null; } @@ -1559,6 +1580,24 @@ export const copyToProjectAvl = async ( } catch (error) { debugError('선종별표준AVL → 프로젝트AVL 복사 실패', { error, selectedIds, targetProjectCode }); + + // unique 제약조건 위반 에러 처리 + if (error instanceof Error) { + if (error.message.includes('unique_project_avl_material_vendor')) { + return { + success: false, + message: "선택한 항목 중 이미 존재하는 [자재그룹코드 + 협력업체명] 조합이 있어 복사할 수 없습니다. 중복되지 않는 항목만 선택해주세요." + }; + } + + if (error.message.includes('duplicate key value violates unique constraint')) { + return { + success: false, + message: "중복 데이터로 인해 복사할 수 없습니다. 이미 존재하는 데이터와 중복되지 않는 항목만 선택해주세요." + }; + } + } + return { success: false, message: "프로젝트 AVL로 복사 중 오류가 발생했습니다." @@ -1646,6 +1685,24 @@ export const copyToStandardAvl = async ( } catch (error) { debugError('프로젝트AVL → 선종별표준AVL 복사 실패', { error, selectedIds, targetStandardInfo }); + + // unique 제약조건 위반 에러 처리 + if (error instanceof Error) { + if (error.message.includes('unique_standard_avl_material_vendor')) { + return { + success: false, + message: "선택한 항목 중 해당 표준AVL에 이미 존재하는 [자재그룹코드 + 협력업체명] 조합이 있어 복사할 수 없습니다. 중복되지 않는 항목만 선택해주세요." + }; + } + + if (error.message.includes('duplicate key value violates unique constraint')) { + return { + success: false, + message: "중복 데이터로 인해 복사할 수 없습니다. 이미 존재하는 데이터와 중복되지 않는 항목만 선택해주세요." + }; + } + } + return { success: false, message: "선종별표준AVL로 복사 중 오류가 발생했습니다." @@ -1812,6 +1869,17 @@ export const copyToVendorPool = async ( } catch (error) { debugError('프로젝트AVL → 벤더풀 복사 실패', { error, selectedIds }); + + // unique 제약조건 위반 에러 처리 (벤더풀에는 다른 unique 제약조건이 있을 수 있음) + if (error instanceof Error) { + if (error.message.includes('duplicate key value violates unique constraint')) { + return { + success: false, + message: "중복 데이터로 인해 벤더풀로 복사할 수 없습니다. 이미 존재하는 데이터와 중복되지 않는 항목만 선택해주세요." + }; + } + } + return { success: false, message: "벤더풀로 복사 중 오류가 발생했습니다." @@ -1931,6 +1999,24 @@ export const copyFromVendorPoolToProjectAvl = async ( } catch (error) { debugError('벤더풀 → 프로젝트AVL 복사 실패', { error, selectedIds, targetProjectCode }); + + // unique 제약조건 위반 에러 처리 + if (error instanceof Error) { + if (error.message.includes('unique_project_avl_material_vendor')) { + return { + success: false, + message: "선택한 항목 중 해당 프로젝트에 이미 존재하는 [자재그룹코드 + 협력업체명] 조합이 있어 복사할 수 없습니다. 중복되지 않는 항목만 선택해주세요." + }; + } + + if (error.message.includes('duplicate key value violates unique constraint')) { + return { + success: false, + message: "중복 데이터로 인해 복사할 수 없습니다. 이미 존재하는 데이터와 중복되지 않는 항목만 선택해주세요." + }; + } + } + return { success: false, message: "프로젝트 AVL로 복사 중 오류가 발생했습니다." @@ -2055,6 +2141,24 @@ export const copyFromVendorPoolToStandardAvl = async ( } catch (error) { debugError('벤더풀 → 선종별표준AVL 복사 실패', { error, selectedIds, targetStandardInfo }); + + // unique 제약조건 위반 에러 처리 + if (error instanceof Error) { + if (error.message.includes('unique_standard_avl_material_vendor')) { + return { + success: false, + message: "선택한 항목 중 해당 표준AVL에 이미 존재하는 [자재그룹코드 + 협력업체명] 조합이 있어 복사할 수 없습니다. 중복되지 않는 항목만 선택해주세요." + }; + } + + if (error.message.includes('duplicate key value violates unique constraint')) { + return { + success: false, + message: "중복 데이터로 인해 복사할 수 없습니다. 이미 존재하는 데이터와 중복되지 않는 항목만 선택해주세요." + }; + } + } + return { success: false, message: "선종별표준AVL로 복사 중 오류가 발생했습니다." @@ -2213,6 +2317,17 @@ export const copyFromStandardAvlToVendorPool = async ( } catch (error) { debugError('선종별표준AVL → 벤더풀 복사 실패', { error, selectedIds }); + + // unique 제약조건 위반 에러 처리 (벤더풀에는 다른 unique 제약조건이 있을 수 있음) + if (error instanceof Error) { + if (error.message.includes('duplicate key value violates unique constraint')) { + return { + success: false, + message: "중복 데이터로 인해 벤더풀로 복사할 수 없습니다. 이미 존재하는 데이터와 중복되지 않는 항목만 선택해주세요." + }; + } + } + return { success: false, message: "벤더풀로 복사 중 오류가 발생했습니다." diff --git a/lib/avl/table/avl-registration-area.tsx b/lib/avl/table/avl-registration-area.tsx index 52912a2c..ba1c76d4 100644 --- a/lib/avl/table/avl-registration-area.tsx +++ b/lib/avl/table/avl-registration-area.tsx @@ -1,7 +1,6 @@ "use client" import * as React from "react" -import { Card } from "@/components/ui/card" import { Button } from "@/components/ui/button" import { ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight } from "lucide-react" import { useAtom } from "jotai" @@ -430,9 +429,9 @@ export function AvlRegistrationArea({ disabled = false }: AvlRegistrationAreaPro }, [selectedTable, selectedRowCount, getSelectedIds, session]) return ( - <Card className={`h-full min-w-full overflow-visible ${disabled ? 'opacity-50 pointer-events-none' : ''}`}> + <div className={`min-w-full overflow-hidden rounded-md bg-background ${disabled ? 'opacity-50 pointer-events-none' : ''}`}> {/* 고정 헤더 영역 */} - <div className="sticky top-0 z-10 p-4 border-b"> + <div className="sticky top-0 z-10 p-4 border-b bg-background"> <div className="flex items-center justify-between"> <h3 className="text-lg font-semibold">AVL 등록 {disabled ? "(비활성화)" : ""}</h3> <div className="flex gap-2"> @@ -444,7 +443,7 @@ export function AvlRegistrationArea({ disabled = false }: AvlRegistrationAreaPro </div> {/* 스크롤되는 콘텐츠 영역 */} - <div className="overflow-x-auto overflow-y-hidden"> + <div className="overflow-x-auto mb-8"> <div className="grid grid-cols-[2.2fr_2fr_2.5fr] gap-0 min-w-[1200px] w-fit"> {/* 프로젝트 AVL 테이블 - 9개 컬럼 */} <div className="p-4 border-r relative"> @@ -563,6 +562,6 @@ export function AvlRegistrationArea({ disabled = false }: AvlRegistrationAreaPro </div> </div> </div> - </Card> + </div> ) } diff --git a/lib/avl/table/avl-table-columns.tsx b/lib/avl/table/avl-table-columns.tsx index 72c59aa9..d95a29b0 100644 --- a/lib/avl/table/avl-table-columns.tsx +++ b/lib/avl/table/avl-table-columns.tsx @@ -226,7 +226,7 @@ export function getColumns({ selectedRows = [], onRowSelect }: GetColumnsProps): { accessorKey: "createdAt", header: ({ column }) => ( - <DataTableColumnHeaderSimple column={column} title="등재일" /> + <DataTableColumnHeaderSimple column={column} title="등록일" /> ), cell: ({ getValue }) => { const date = getValue() as string @@ -237,7 +237,7 @@ export function getColumns({ selectedRows = [], onRowSelect }: GetColumnsProps): { accessorKey: "createdBy", header: ({ column }) => ( - <DataTableColumnHeaderSimple column={column} title="등재자" /> + <DataTableColumnHeaderSimple column={column} title="등록자" /> ), cell: ({ getValue }) => { const date = getValue() as string diff --git a/lib/avl/table/avl-vendor-add-and-modify-dialog.tsx b/lib/avl/table/avl-vendor-add-and-modify-dialog.tsx index 174982e4..4f0eb404 100644 --- a/lib/avl/table/avl-vendor-add-and-modify-dialog.tsx +++ b/lib/avl/table/avl-vendor-add-and-modify-dialog.tsx @@ -8,7 +8,6 @@ import { DialogFooter, DialogHeader, DialogTitle, - DialogTrigger, } from "@/components/ui/dialog" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" @@ -24,6 +23,14 @@ import { } from "@/components/ui/select" import { toast } from "sonner" import type { AvlVendorInfoInput, AvlDetailItem } from "../types" +import { EngineeringDisciplineSelector, type DisciplineCode } 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 { VendorSelectorDialogSingle } from "@/components/common/vendor" +import type { VendorSearchItem } from "@/components/common/vendor" +import { PlaceOfShippingSelector } from "@/components/common/selectors/place-of-shipping" +import { VendorTierSelector } from "@/components/common/selectors/vendor-tier" +import { DatePicker } from "@/components/ui/date-picker" interface AvlVendorAddAndModifyDialogProps { open: boolean @@ -58,6 +65,16 @@ export function AvlVendorAddAndModifyDialog({ initialHtDivision, initialProjectCode }: AvlVendorAddAndModifyDialogProps) { + // 설계공종 선택 상태 + const [selectedDiscipline, setSelectedDiscipline] = React.useState<DisciplineCode | undefined>(undefined) + // 자재그룹 선택 상태 + const [selectedMaterialGroup, setSelectedMaterialGroup] = React.useState<MaterialSearchItem | null>(null) + // 벤더 선택 상태 + const [selectedVendor, setSelectedVendor] = React.useState<VendorSearchItem | null>(null) + // 날짜 상태 (Date 객체로 관리) + const [quoteReceivedDate, setQuoteReceivedDate] = React.useState<Date | undefined>(undefined) + const [recentQuoteDate, setRecentQuoteDate] = React.useState<Date | undefined>(undefined) + const [recentOrderDate, setRecentOrderDate] = React.useState<Date | undefined>(undefined) const [formData, setFormData] = React.useState<Omit<AvlVendorInfoInput, 'avlListId'>>({ // 공통 기본 설정 isTemplate: isTemplate, @@ -137,9 +154,50 @@ export function AvlVendorAddAndModifyDialog({ remarks: "" }) - // 수정 모드일 때 폼 데이터 초기화 + // 수정 모드일 때 설계공종 선택 상태 및 폼 데이터 초기화 React.useEffect(() => { if (editingItem) { + // 설계공종 선택 상태 초기화 + if (editingItem.disciplineCode && editingItem.disciplineName) { + setSelectedDiscipline({ + CD: editingItem.disciplineCode, + USR_DF_CHAR_18: editingItem.disciplineName + }) + } else { + setSelectedDiscipline(undefined) + } + + // 자재그룹 선택 상태 초기화 + if (editingItem.materialGroupCode && editingItem.materialGroupName) { + setSelectedMaterialGroup({ + materialGroupCode: editingItem.materialGroupCode, + materialGroupDescription: editingItem.materialGroupName, + displayText: `${editingItem.materialGroupCode} - ${editingItem.materialGroupName}` + }) + } else { + setSelectedMaterialGroup(null) + } + + // 벤더 선택 상태 초기화 (기존 데이터가 있으면 가상의 벤더 객체 생성) + if (editingItem.vendorCode || editingItem.vendorName) { + setSelectedVendor({ + id: -1, // 임시 ID (실제 벤더 ID는 알 수 없음) + vendorName: editingItem.vendorName || "", + vendorCode: editingItem.vendorCode || null, + status: "UNKNOWN", // 상태 정보 없음 + displayText: editingItem.vendorCode + ? `${editingItem.vendorName} (${editingItem.vendorCode})` + : editingItem.vendorName || "" + }) + } else { + setSelectedVendor(null) + } + + // 날짜 상태 초기화 + setQuoteReceivedDate(editingItem.quoteReceivedDate ? new Date(editingItem.quoteReceivedDate) : undefined) + setRecentQuoteDate(editingItem.recentQuoteDate ? new Date(editingItem.recentQuoteDate) : undefined) + setRecentOrderDate(editingItem.recentOrderDate ? new Date(editingItem.recentOrderDate) : undefined) + setFormData({ // 공통 기본 설정 isTemplate: editingItem.isTemplate ?? isTemplate, @@ -224,6 +282,17 @@ export function AvlVendorAddAndModifyDialog({ // 다이얼로그가 열릴 때 초기값 재설정 (수정 모드가 아닐 때만) React.useEffect(() => { if (open && !editingItem) { + // 설계공종 선택 상태 초기화 + setSelectedDiscipline(undefined) + // 자재그룹 선택 상태 초기화 + setSelectedMaterialGroup(null) + // 벤더 선택 상태 초기화 + setSelectedVendor(null) + // 날짜 상태 초기화 + setQuoteReceivedDate(undefined) + setRecentQuoteDate(undefined) + setRecentOrderDate(undefined) + setFormData(prev => ({ ...prev, isTemplate: isTemplate, @@ -236,6 +305,40 @@ export function AvlVendorAddAndModifyDialog({ } }, [open, editingItem, isTemplate, initialProjectCode, initialConstructionSector, initialShipType, initialAvlKind, initialHtDivision]) + // 설계공종 선택 핸들러 + const handleDisciplineSelect = React.useCallback((discipline: DisciplineCode) => { + setSelectedDiscipline(discipline) + setFormData(prev => ({ + ...prev, + disciplineCode: discipline.CD, + disciplineName: discipline.USR_DF_CHAR_18 + })) + }, []) + + // 자재그룹 선택 핸들러 + const handleMaterialGroupSelect = React.useCallback((materialGroup: MaterialSearchItem | null) => { + setSelectedMaterialGroup(materialGroup) + setFormData(prev => ({ + ...prev, + materialGroupCode: materialGroup?.materialGroupCode || "", + materialGroupName: materialGroup?.materialGroupDescription || "" + })) + }, []) + + // 벤더 선택 핸들러 (선택기에서 벤더를 선택했을 때 Input 필드에 자동 입력) + const handleVendorSelect = React.useCallback((vendor: VendorSearchItem | null) => { + setSelectedVendor(vendor) + if (vendor) { + setFormData(prev => ({ + ...prev, + vendorCode: vendor.vendorCode || "", + vendorName: vendor.vendorName || "", + // AVL 등재업체명도 기본적으로 벤더명으로 설정 (사용자가 수정 가능) + avlVendorName: vendor.vendorName || "" + })) + } + }, []) + const handleSubmit = async () => { // 공통 필수 필드 검증 if (!formData.disciplineName || !formData.materialNameCustomerSide) { @@ -259,15 +362,34 @@ export function AvlVendorAddAndModifyDialog({ } try { + // 날짜 필드들을 문자열로 변환 + const formatDate = (date: Date | undefined): string => { + if (!date) return "" + return date.toISOString().split('T')[0] // YYYY-MM-DD 형식 + } + + const submissionData = { + ...formData, + quoteReceivedDate: formatDate(quoteReceivedDate), + recentQuoteDate: formatDate(recentQuoteDate), + recentOrderDate: formatDate(recentOrderDate), + } + if (editingItem && onUpdateItem) { // 수정 모드 - await onUpdateItem(editingItem.id, formData) + await onUpdateItem(editingItem.id, submissionData) } else { // 추가 모드 - await onAddItem(formData) + await onAddItem(submissionData) } - // 폼 초기화 + // 폼 및 선택 상태 초기화 + setSelectedDiscipline(undefined) + setSelectedMaterialGroup(null) + setSelectedVendor(null) + setQuoteReceivedDate(undefined) + setRecentQuoteDate(undefined) + setRecentOrderDate(undefined) setFormData({ isTemplate: isTemplate, projectCode: initialProjectCode || "", @@ -313,12 +435,18 @@ export function AvlVendorAddAndModifyDialog({ } as Omit<AvlVendorInfoInput, 'avlListId'>) onOpenChange(false) - } catch (error) { + } catch { // 에러 처리는 호출하는 쪽에서 담당 } } const handleCancel = () => { + setSelectedDiscipline(undefined) + setSelectedMaterialGroup(null) + setSelectedVendor(null) + setQuoteReceivedDate(undefined) + setRecentQuoteDate(undefined) + setRecentOrderDate(undefined) setFormData({ isTemplate: isTemplate, projectCode: initialProjectCode || "", @@ -365,31 +493,6 @@ export function AvlVendorAddAndModifyDialog({ onOpenChange(false) } - // 선종 옵션들 (공사부문에 따라 다름) - const getShipTypeOptions = (constructionSector: string) => { - if (constructionSector === "조선") { - return [ - { value: "A-max", label: "A-max" }, - { value: "S-max", label: "S-max" }, - { value: "VLCC", label: "VLCC" }, - { value: "LNGC", label: "LNGC" }, - { value: "CONT", label: "CONT" }, - ] - } else if (constructionSector === "해양") { - return [ - { value: "FPSO", label: "FPSO" }, - { value: "FLNG", label: "FLNG" }, - { value: "FPU", label: "FPU" }, - { value: "Platform", label: "Platform" }, - { value: "WTIV", label: "WTIV" }, - { value: "GOM", label: "GOM" }, - ] - } else { - return [] - } - } - - const shipTypeOptions = getShipTypeOptions(formData.constructionSector) return ( <Dialog open={open} onOpenChange={onOpenChange}> @@ -419,6 +522,8 @@ export function AvlVendorAddAndModifyDialog({ value={formData.projectCode} onChange={(e) => setFormData(prev => ({ ...prev, projectCode: e.target.value }))} placeholder="프로젝트 코드를 입력하세요" + readOnly + className="bg-muted" /> </div> </div> @@ -430,82 +535,39 @@ export function AvlVendorAddAndModifyDialog({ <div className="grid grid-cols-2 gap-4"> <div className="space-y-2"> <Label htmlFor="constructionSector">공사부문 *</Label> - <Select + <Input value={formData.constructionSector} - onValueChange={(value) => { - setFormData(prev => ({ - ...prev, - constructionSector: value, - shipType: "" // 공사부문 변경 시 선종 초기화 - })) - }} - > - <SelectTrigger> - <SelectValue placeholder="공사부문을 선택하세요" /> - </SelectTrigger> - <SelectContent> - <SelectItem value="조선">조선</SelectItem> - <SelectItem value="해양">해양</SelectItem> - </SelectContent> - </Select> + readOnly + className="bg-muted" + placeholder="공사부문이 설정되지 않았습니다" + /> </div> <div className="space-y-2"> <Label htmlFor="shipType">선종 *</Label> - <Select + <Input value={formData.shipType} - onValueChange={(value) => - setFormData(prev => ({ ...prev, shipType: value })) - } - disabled={!formData.constructionSector} - > - <SelectTrigger> - <SelectValue placeholder="선종을 선택하세요" /> - </SelectTrigger> - <SelectContent> - {shipTypeOptions.map((option) => ( - <SelectItem key={option.value} value={option.value}> - {option.label} - </SelectItem> - ))} - </SelectContent> - </Select> + readOnly + className="bg-muted" + placeholder="선종이 설정되지 않았습니다" + /> </div> <div className="space-y-2"> <Label htmlFor="avlKind">AVL종류 *</Label> - <Select + <Input value={formData.avlKind} - onValueChange={(value) => - setFormData(prev => ({ ...prev, avlKind: value })) - } - > - <SelectTrigger> - <SelectValue placeholder="AVL종류를 선택하세요" /> - </SelectTrigger> - <SelectContent> - <SelectItem value="Nearshore">Nearshore</SelectItem> - <SelectItem value="Offshore">Offshore</SelectItem> - <SelectItem value="IOC">IOC</SelectItem> - <SelectItem value="NOC">NOC</SelectItem> - </SelectContent> - </Select> + readOnly + className="bg-muted" + placeholder="AVL종류가 설정되지 않았습니다" + /> </div> <div className="space-y-2"> <Label htmlFor="htDivision">H/T 구분 *</Label> - <Select + <Input value={formData.htDivision} - onValueChange={(value) => - setFormData(prev => ({ ...prev, htDivision: value })) - } - > - <SelectTrigger> - <SelectValue placeholder="H/T 구분을 선택하세요" /> - </SelectTrigger> - <SelectContent> - <SelectItem value="공통">공통</SelectItem> - <SelectItem value="H">Hull (H)</SelectItem> - <SelectItem value="T">Topside (T)</SelectItem> - </SelectContent> - </Select> + readOnly + className="bg-muted" + placeholder="H/T 구분이 설정되지 않았습니다" + /> </div> </div> </div> @@ -533,23 +595,19 @@ export function AvlVendorAddAndModifyDialog({ </SelectContent> </Select> </div> - <div className="space-y-2"> - <Label htmlFor="disciplineCode">설계공종코드</Label> - <Input - id="disciplineCode" - value={formData.disciplineCode} - onChange={(e) => setFormData(prev => ({ ...prev, disciplineCode: e.target.value }))} - placeholder="설계공종코드를 입력하세요" - /> - </div> <div className="space-y-2 col-span-2"> - <Label htmlFor="disciplineName">설계공종명 *</Label> - <Input - id="disciplineName" - value={formData.disciplineName} - onChange={(e) => setFormData(prev => ({ ...prev, disciplineName: e.target.value }))} - placeholder="설계공종명을 입력하세요" - /> + <Label htmlFor="discipline">설계공종 *</Label> + <EngineeringDisciplineSelector + selectedDiscipline={selectedDiscipline} + onDisciplineSelect={handleDisciplineSelect} + placeholder="설계공종을 선택하세요" + className="h-9" + /> + <div className="text-xs text-muted-foreground"> + {selectedDiscipline && ( + <span>선택됨: [{selectedDiscipline.CD}] {selectedDiscipline.USR_DF_CHAR_18}</span> + )} + </div> </div> <div className="space-y-2 col-span-2"> <Label htmlFor="materialNameCustomerSide">고객사 AVL 자재명 *</Label> @@ -591,31 +649,51 @@ export function AvlVendorAddAndModifyDialog({ {/* 자재그룹 정보 */} <div className="space-y-4"> <h4 className="text-sm font-semibold text-muted-foreground border-b pb-2">자재그룹 정보</h4> - <div className="grid grid-cols-2 gap-4"> - <div className="space-y-2"> - <Label htmlFor="materialGroupCode">자재그룹 코드</Label> - <Input - id="materialGroupCode" - value={formData.materialGroupCode} - onChange={(e) => setFormData(prev => ({ ...prev, materialGroupCode: e.target.value }))} - placeholder="자재그룹 코드를 입력하세요" - /> - </div> + <div className="space-y-4"> <div className="space-y-2"> - <Label htmlFor="materialGroupName">자재그룹 명</Label> - <Input - id="materialGroupName" - value={formData.materialGroupName} - onChange={(e) => setFormData(prev => ({ ...prev, materialGroupName: e.target.value }))} - placeholder="자재그룹 명을 입력하세요" - /> + {/* <Label htmlFor="materialGroup">자재그룹 선택</Label> */} + <MaterialGroupSelectorDialogSingle + triggerLabel="자재그룹 선택" + selectedMaterial={selectedMaterialGroup} + onMaterialSelect={handleMaterialGroupSelect} + placeholder="자재그룹을 검색하세요..." + title="자재그룹 선택" + description="AVL에 등록할 자재그룹을 선택해주세요." + triggerVariant="outline" + /> + <div className="text-xs text-muted-foreground"> + {selectedMaterialGroup && ( + <> + <span>자재그룹코드: {selectedMaterialGroup.materialGroupCode}</span> + <br /> + <span>자재그룹명: {selectedMaterialGroup.materialGroupDescription}</span> + </> + )} + </div> </div> </div> </div> {/* 협력업체 정보 */} <div className="space-y-4"> - <h4 className="text-sm font-semibold text-muted-foreground border-b pb-2">협력업체 정보</h4> + <div className="flex items-center justify-between border-b pb-2"> + <h4 className="text-sm font-semibold text-muted-foreground">협력업체 정보</h4> + <VendorSelectorDialogSingle + triggerLabel="협력업체 로드" + selectedVendor={selectedVendor} + onVendorSelect={handleVendorSelect} + title="협력업체 선택" + description="등록된 협력업체 목록에서 선택하여 정보를 자동으로 입력할 수 있습니다." + triggerVariant="outline" + statusFilter="ACTIVE" + /> + </div> + {selectedVendor && ( + <div className="text-xs text-muted-foreground bg-blue-50 p-2 rounded"> + <span>선택된 협력업체: [{selectedVendor.vendorCode || '-'}] {selectedVendor.vendorName}</span> + <span className="ml-2 text-blue-600">({selectedVendor.status})</span> + </div> + )} <div className="grid grid-cols-2 gap-4"> <div className="space-y-2"> <Label htmlFor="vendorCode">협력업체 코드</Label> @@ -646,11 +724,11 @@ export function AvlVendorAddAndModifyDialog({ </div> <div className="space-y-2"> <Label htmlFor="tier">등급 (Tier)</Label> - <Input - id="tier" + <VendorTierSelector value={formData.tier} - onChange={(e) => setFormData(prev => ({ ...prev, tier: e.target.value }))} - placeholder="등급을 입력하세요" + onValueChange={(value) => setFormData(prev => ({ ...prev, tier: value }))} + placeholder="등급을 선택하세요" + className="h-9" /> </div> </div> @@ -698,11 +776,11 @@ export function AvlVendorAddAndModifyDialog({ </div> <div className="space-y-2"> <Label htmlFor="manufacturingLocation">제작/선적지 (국가)</Label> - <Input - id="manufacturingLocation" + <PlaceOfShippingSelector value={formData.manufacturingLocation} - onChange={(e) => setFormData(prev => ({ ...prev, manufacturingLocation: e.target.value }))} - placeholder="제작/선적지를 입력하세요" + onValueChange={(value) => setFormData(prev => ({ ...prev, manufacturingLocation: value }))} + placeholder="제작/선적지를 선택하세요" + className="h-9" /> </div> </div> @@ -862,12 +940,12 @@ export function AvlVendorAddAndModifyDialog({ /> </div> <div className="space-y-2"> - <Label htmlFor="quoteReceivedDate">견적접수일 (YYYY-MM-DD)</Label> - <Input - id="quoteReceivedDate" - value={formData.quoteReceivedDate} - onChange={(e) => setFormData(prev => ({ ...prev, quoteReceivedDate: e.target.value }))} - placeholder="YYYY-MM-DD" + <Label htmlFor="quoteReceivedDate">견적접수일</Label> + <DatePicker + date={quoteReceivedDate} + onSelect={setQuoteReceivedDate} + placeholder="견적접수일 선택" + className="h-9" /> </div> </div> @@ -887,12 +965,12 @@ export function AvlVendorAddAndModifyDialog({ /> </div> <div className="space-y-2"> - <Label htmlFor="recentQuoteDate">최근견적일 (YYYY-MM-DD)</Label> - <Input - id="recentQuoteDate" - value={formData.recentQuoteDate} - onChange={(e) => setFormData(prev => ({ ...prev, recentQuoteDate: e.target.value }))} - placeholder="YYYY-MM-DD" + <Label htmlFor="recentQuoteDate">최근견적일</Label> + <DatePicker + date={recentQuoteDate} + onSelect={setRecentQuoteDate} + placeholder="최근견적일 선택" + className="h-9" /> </div> <div className="space-y-2"> @@ -905,12 +983,12 @@ export function AvlVendorAddAndModifyDialog({ /> </div> <div className="space-y-2"> - <Label htmlFor="recentOrderDate">최근발주일 (YYYY-MM-DD)</Label> - <Input - id="recentOrderDate" - value={formData.recentOrderDate} - onChange={(e) => setFormData(prev => ({ ...prev, recentOrderDate: e.target.value }))} - placeholder="YYYY-MM-DD" + <Label htmlFor="recentOrderDate">최근발주일</Label> + <DatePicker + date={recentOrderDate} + onSelect={setRecentOrderDate} + placeholder="최근발주일 선택" + className="h-9" /> </div> </div> diff --git a/lib/avl/table/project-avl-add-dialog.tsx b/lib/avl/table/project-avl-add-dialog.tsx deleted file mode 100644 index 509e4258..00000000 --- a/lib/avl/table/project-avl-add-dialog.tsx +++ /dev/null @@ -1,779 +0,0 @@ -"use client" - -import * as React from "react" -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, - DialogTrigger, -} from "@/components/ui/dialog" -import { Button } from "@/components/ui/button" -import { Input } from "@/components/ui/input" -import { Label } from "@/components/ui/label" -import { Checkbox } from "@/components/ui/checkbox" -import { Textarea } from "@/components/ui/textarea" -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select" -import { toast } from "sonner" -import type { AvlVendorInfoInput, AvlDetailItem } from "../types" - -interface ProjectAvlAddDialogProps { - open: boolean - onOpenChange: (open: boolean) => void - onAddItem: (item: Omit<AvlVendorInfoInput, 'avlListId'>) => Promise<void> - editingItem?: AvlDetailItem // 수정할 항목 (없으면 추가 모드) - onUpdateItem?: (id: number, item: Omit<AvlVendorInfoInput, 'avlListId'>) => Promise<void> // 수정 핸들러 -} - -export function ProjectAvlAddDialog({ open, onOpenChange, onAddItem, editingItem, onUpdateItem }: ProjectAvlAddDialogProps) { - const [formData, setFormData] = React.useState<Omit<AvlVendorInfoInput, 'avlListId'>>({ - // 설계 정보 - equipBulkDivision: "EQUIP", - disciplineCode: "", - disciplineName: "", - - // 자재 정보 - materialNameCustomerSide: "", - - // 패키지 정보 - packageCode: "", - packageName: "", - - // 자재그룹 정보 - materialGroupCode: "", - materialGroupName: "", - - // 협력업체 정보 - vendorName: "", - vendorCode: "", - - // AVL 정보 - avlVendorName: "", - tier: "", - - // 제안방향 - ownerSuggestion: false, - shiSuggestion: false, - - // 위치 정보 - headquarterLocation: "", - manufacturingLocation: "", - - // FA 정보 - faTarget: false, - faStatus: "", - - // Agent 정보 - isAgent: false, - - // 계약 서명주체 - contractSignerName: "", - contractSignerCode: "", - - // SHI Qualification - shiAvl: false, - shiBlacklist: false, - shiBcc: false, - - // 기술영업 견적결과 - salesQuoteNumber: "", - quoteCode: "", - salesVendorInfo: "", - salesCountry: "", - totalAmount: "", - quoteReceivedDate: "", - - // 업체 실적 현황 - recentQuoteDate: "", - recentQuoteNumber: "", - recentOrderDate: "", - recentOrderNumber: "", - - // 기타 - remarks: "" - }) - - // 수정 모드일 때 폼 데이터 초기화 - React.useEffect(() => { - if (editingItem) { - setFormData({ - // 설계 정보 - equipBulkDivision: editingItem.equipBulkDivision === "EQUIP" ? "EQUIP" : "BULK", - disciplineCode: editingItem.disciplineCode || "", - disciplineName: editingItem.disciplineName || "", - - // 자재 정보 - materialNameCustomerSide: editingItem.materialNameCustomerSide || "", - - // 패키지 정보 - packageCode: editingItem.packageCode || "", - packageName: editingItem.packageName || "", - - // 자재그룹 정보 - materialGroupCode: editingItem.materialGroupCode || "", - materialGroupName: editingItem.materialGroupName || "", - - // 협력업체 정보 - vendorName: editingItem.vendorName || "", - vendorCode: editingItem.vendorCode || "", - - // AVL 정보 - avlVendorName: editingItem.avlVendorName || "", - tier: editingItem.tier || "", - - // 제안방향 - ownerSuggestion: editingItem.ownerSuggestion || false, - shiSuggestion: editingItem.shiSuggestion || false, - - // 위치 정보 - headquarterLocation: editingItem.headquarterLocation || "", - manufacturingLocation: editingItem.manufacturingLocation || "", - - // FA 정보 - faTarget: editingItem.faTarget || false, - faStatus: editingItem.faStatus || "", - - // Agent 정보 - isAgent: editingItem.isAgent || false, - - // 계약 서명주체 - contractSignerName: editingItem.contractSignerName || "", - contractSignerCode: editingItem.contractSignerCode || "", - - // SHI Qualification - shiAvl: editingItem.shiAvl || false, - shiBlacklist: editingItem.shiBlacklist || false, - shiBcc: editingItem.shiBcc || false, - - // 기술영업 견적결과 - salesQuoteNumber: editingItem.salesQuoteNumber || "", - quoteCode: editingItem.quoteCode || "", - salesVendorInfo: editingItem.salesVendorInfo || "", - salesCountry: editingItem.salesCountry || "", - totalAmount: editingItem.totalAmount || "", - quoteReceivedDate: editingItem.quoteReceivedDate || "", - - // 업체 실적 현황 - recentQuoteDate: editingItem.recentQuoteDate || "", - recentQuoteNumber: editingItem.recentQuoteNumber || "", - recentOrderDate: editingItem.recentOrderDate || "", - recentOrderNumber: editingItem.recentOrderNumber || "", - - // 기타 - remarks: editingItem.remarks || "" - }) - } - }, [editingItem]) - - const handleSubmit = async () => { - // 필수 필드 검증 - if (!formData.disciplineName || !formData.materialNameCustomerSide) { - toast.error("설계공종과 고객사 AVL 자재명은 필수 입력 항목입니다.") - return - } - - try { - if (editingItem && onUpdateItem) { - // 수정 모드 - await onUpdateItem(editingItem.id, formData) - } else { - // 추가 모드 - await onAddItem(formData) - } - - // 폼 초기화 (onAddItem에서 성공적으로 처리된 경우에만) - setFormData({ - // 설계 정보 - equipBulkDivision: "EQUIP", - disciplineCode: "", - disciplineName: "", - - // 자재 정보 - materialNameCustomerSide: "", - - // 패키지 정보 - packageCode: "", - packageName: "", - - // 자재그룹 정보 - materialGroupCode: "", - materialGroupName: "", - - // 협력업체 정보 - vendorName: "", - vendorCode: "", - - // AVL 정보 - avlVendorName: "", - tier: "", - - // 제안방향 - ownerSuggestion: false, - shiSuggestion: false, - - // 위치 정보 - headquarterLocation: "", - manufacturingLocation: "", - - // FA 정보 - faTarget: false, - faStatus: "", - - // Agent 정보 - isAgent: false, - - // 계약 서명주체 - contractSignerName: "", - contractSignerCode: "", - - // SHI Qualification - shiAvl: false, - shiBlacklist: false, - shiBcc: false, - - // 기술영업 견적결과 - salesQuoteNumber: "", - quoteCode: "", - salesVendorInfo: "", - salesCountry: "", - totalAmount: "", - quoteReceivedDate: "", - - // 업체 실적 현황 - recentQuoteDate: "", - recentQuoteNumber: "", - recentOrderDate: "", - recentOrderNumber: "", - - // 기타 - remarks: "" - } as Omit<AvlVendorInfoInput, 'avlListId'>) - - onOpenChange(false) - } catch (error) { - // 에러 처리는 onAddItem에서 담당하므로 여기서는 아무것도 하지 않음 - } - } - - const handleCancel = () => { - setFormData({ - // 설계 정보 - equipBulkDivision: "EQUIP", - disciplineCode: "", - disciplineName: "", - - // 자재 정보 - materialNameCustomerSide: "", - - // 패키지 정보 - packageCode: "", - packageName: "", - - // 자재그룹 정보 - materialGroupCode: "", - materialGroupName: "", - - // 협력업체 정보 - vendorName: "", - vendorCode: "", - - // AVL 정보 - avlVendorName: "", - tier: "", - - // 제안방향 - ownerSuggestion: false, - shiSuggestion: false, - - // 위치 정보 - headquarterLocation: "", - manufacturingLocation: "", - - // FA 정보 - faTarget: false, - faStatus: "", - - // Agent 정보 - isAgent: false, - - // 계약 서명주체 - contractSignerName: "", - contractSignerCode: "", - - // SHI Qualification - shiAvl: false, - shiBlacklist: false, - shiBcc: false, - - // 기술영업 견적결과 - salesQuoteNumber: "", - quoteCode: "", - salesVendorInfo: "", - salesCountry: "", - totalAmount: "", - quoteReceivedDate: "", - - // 업체 실적 현황 - recentQuoteDate: "", - recentQuoteNumber: "", - recentOrderDate: "", - recentOrderNumber: "", - - // 기타 - remarks: "" - } as Omit<AvlVendorInfoInput, 'avlListId'>) - onOpenChange(false) - } - - return ( - <Dialog open={open} onOpenChange={onOpenChange}> - <DialogContent className="sm:max-w-[800px] max-h-[90vh] overflow-y-auto"> - <DialogHeader> - <DialogTitle>{editingItem ? "프로젝트 AVL 항목 수정" : "프로젝트 AVL 항목 추가"}</DialogTitle> - <DialogDescription> - {editingItem - ? "AVL 항목을 수정합니다. 필수 항목을 입력해주세요." - : "새로운 AVL 항목을 추가합니다. 필수 항목을 입력해주세요." - } * 표시된 항목은 필수 입력사항입니다. - </DialogDescription> - </DialogHeader> - <div className="space-y-6 py-4"> - {/* 기본 정보 */} - <div className="space-y-4"> - <h4 className="text-sm font-semibold text-muted-foreground border-b pb-2">기본 정보</h4> - <div className="grid grid-cols-2 gap-4"> - <div className="space-y-2"> - <Label htmlFor="equipBulkDivision">EQUIP/BULK 구분</Label> - <Select - value={formData.equipBulkDivision} - onValueChange={(value: "EQUIP" | "BULK") => - setFormData(prev => ({ ...prev, equipBulkDivision: value })) - } - > - <SelectTrigger> - <SelectValue /> - </SelectTrigger> - <SelectContent> - <SelectItem value="EQUIP">EQUIP</SelectItem> - <SelectItem value="BULK">BULK</SelectItem> - </SelectContent> - </Select> - </div> - <div className="space-y-2"> - <Label htmlFor="disciplineCode">설계공종코드</Label> - <Input - id="disciplineCode" - value={formData.disciplineCode} - onChange={(e) => setFormData(prev => ({ ...prev, disciplineCode: e.target.value }))} - placeholder="설계공종코드를 입력하세요" - /> - </div> - <div className="space-y-2 col-span-2"> - <Label htmlFor="disciplineName">설계공종명 *</Label> - <Input - id="disciplineName" - value={formData.disciplineName} - onChange={(e) => setFormData(prev => ({ ...prev, disciplineName: e.target.value }))} - placeholder="설계공종명을 입력하세요" - /> - </div> - <div className="space-y-2 col-span-2"> - <Label htmlFor="materialNameCustomerSide">고객사 AVL 자재명 *</Label> - <Input - id="materialNameCustomerSide" - value={formData.materialNameCustomerSide} - onChange={(e) => setFormData(prev => ({ ...prev, materialNameCustomerSide: e.target.value }))} - placeholder="고객사 AVL 자재명을 입력하세요" - /> - </div> - </div> - </div> - - {/* 패키지 정보 */} - <div className="space-y-4"> - <h4 className="text-sm font-semibold text-muted-foreground border-b pb-2">패키지 정보</h4> - <div className="grid grid-cols-2 gap-4"> - <div className="space-y-2"> - <Label htmlFor="packageCode">패키지 코드</Label> - <Input - id="packageCode" - value={formData.packageCode} - onChange={(e) => setFormData(prev => ({ ...prev, packageCode: e.target.value }))} - placeholder="패키지 코드를 입력하세요" - /> - </div> - <div className="space-y-2"> - <Label htmlFor="packageName">패키지 명</Label> - <Input - id="packageName" - value={formData.packageName} - onChange={(e) => setFormData(prev => ({ ...prev, packageName: e.target.value }))} - placeholder="패키지 명을 입력하세요" - /> - </div> - </div> - </div> - - {/* 자재그룹 정보 */} - <div className="space-y-4"> - <h4 className="text-sm font-semibold text-muted-foreground border-b pb-2">자재그룹 정보</h4> - <div className="grid grid-cols-2 gap-4"> - <div className="space-y-2"> - <Label htmlFor="materialGroupCode">자재그룹 코드</Label> - <Input - id="materialGroupCode" - value={formData.materialGroupCode} - onChange={(e) => setFormData(prev => ({ ...prev, materialGroupCode: e.target.value }))} - placeholder="자재그룹 코드를 입력하세요" - /> - </div> - <div className="space-y-2"> - <Label htmlFor="materialGroupName">자재그룹 명</Label> - <Input - id="materialGroupName" - value={formData.materialGroupName} - onChange={(e) => setFormData(prev => ({ ...prev, materialGroupName: e.target.value }))} - placeholder="자재그룹 명을 입력하세요" - /> - </div> - </div> - </div> - - {/* 협력업체 정보 */} - <div className="space-y-4"> - <h4 className="text-sm font-semibold text-muted-foreground border-b pb-2">협력업체 정보</h4> - <div className="grid grid-cols-2 gap-4"> - <div className="space-y-2"> - <Label htmlFor="vendorCode">협력업체 코드</Label> - <Input - id="vendorCode" - value={formData.vendorCode} - onChange={(e) => setFormData(prev => ({ ...prev, vendorCode: e.target.value }))} - placeholder="협력업체 코드를 입력하세요" - /> - </div> - <div className="space-y-2"> - <Label htmlFor="vendorName">협력업체 명</Label> - <Input - id="vendorName" - value={formData.vendorName} - onChange={(e) => setFormData(prev => ({ ...prev, vendorName: e.target.value }))} - placeholder="협력업체 명을 입력하세요" - /> - </div> - <div className="space-y-2"> - <Label htmlFor="avlVendorName">AVL 등재업체명</Label> - <Input - id="avlVendorName" - value={formData.avlVendorName} - onChange={(e) => setFormData(prev => ({ ...prev, avlVendorName: e.target.value }))} - placeholder="AVL 등재업체명을 입력하세요" - /> - </div> - <div className="space-y-2"> - <Label htmlFor="tier">등급 (Tier)</Label> - <Input - id="tier" - value={formData.tier} - onChange={(e) => setFormData(prev => ({ ...prev, tier: e.target.value }))} - placeholder="등급을 입력하세요" - /> - </div> - </div> - </div> - - {/* 제안방향 */} - <div className="space-y-4"> - <h4 className="text-sm font-semibold text-muted-foreground border-b pb-2">제안방향</h4> - <div className="flex gap-6"> - <div className="flex items-center space-x-2"> - <Checkbox - id="ownerSuggestion" - checked={formData.ownerSuggestion} - onCheckedChange={(checked) => - setFormData(prev => ({ ...prev, ownerSuggestion: !!checked })) - } - /> - <Label htmlFor="ownerSuggestion">선주제안</Label> - </div> - <div className="flex items-center space-x-2"> - <Checkbox - id="shiSuggestion" - checked={formData.shiSuggestion} - onCheckedChange={(checked) => - setFormData(prev => ({ ...prev, shiSuggestion: !!checked })) - } - /> - <Label htmlFor="shiSuggestion">SHI 제안</Label> - </div> - </div> - </div> - - {/* 위치 정보 */} - <div className="space-y-4"> - <h4 className="text-sm font-semibold text-muted-foreground border-b pb-2">위치 정보</h4> - <div className="grid grid-cols-2 gap-4"> - <div className="space-y-2"> - <Label htmlFor="headquarterLocation">본사 위치 (국가)</Label> - <Input - id="headquarterLocation" - value={formData.headquarterLocation} - onChange={(e) => setFormData(prev => ({ ...prev, headquarterLocation: e.target.value }))} - placeholder="본사 위치를 입력하세요" - /> - </div> - <div className="space-y-2"> - <Label htmlFor="manufacturingLocation">제작/선적지 (국가)</Label> - <Input - id="manufacturingLocation" - value={formData.manufacturingLocation} - onChange={(e) => setFormData(prev => ({ ...prev, manufacturingLocation: e.target.value }))} - placeholder="제작/선적지를 입력하세요" - /> - </div> - </div> - </div> - - {/* FA 정보 */} - <div className="space-y-4"> - <h4 className="text-sm font-semibold text-muted-foreground border-b pb-2">FA 정보</h4> - <div className="grid grid-cols-2 gap-4"> - <div className="flex items-center space-x-2"> - <Checkbox - id="faTarget" - checked={formData.faTarget} - onCheckedChange={(checked) => - setFormData(prev => ({ ...prev, faTarget: !!checked })) - } - /> - <Label htmlFor="faTarget">FA 대상</Label> - </div> - <div className="space-y-2"> - <Label htmlFor="faStatus">FA 현황</Label> - <Input - id="faStatus" - value={formData.faStatus} - onChange={(e) => setFormData(prev => ({ ...prev, faStatus: e.target.value }))} - placeholder="FA 현황을 입력하세요" - /> - </div> - </div> - </div> - - {/* Agent 정보 */} - <div className="space-y-4"> - <h4 className="text-sm font-semibold text-muted-foreground border-b pb-2">Agent 정보</h4> - <div className="flex items-center space-x-2"> - <Checkbox - id="isAgent" - checked={formData.isAgent} - onCheckedChange={(checked) => - setFormData(prev => ({ ...prev, isAgent: !!checked })) - } - /> - <Label htmlFor="isAgent">Agent 여부</Label> - </div> - </div> - - {/* 계약 서명주체 */} - <div className="space-y-4"> - <h4 className="text-sm font-semibold text-muted-foreground border-b pb-2">계약 서명주체</h4> - <div className="grid grid-cols-2 gap-4"> - <div className="space-y-2"> - <Label htmlFor="contractSignerCode">계약서명주체 코드</Label> - <Input - id="contractSignerCode" - value={formData.contractSignerCode} - onChange={(e) => setFormData(prev => ({ ...prev, contractSignerCode: e.target.value }))} - placeholder="계약서명주체 코드를 입력하세요" - /> - </div> - <div className="space-y-2"> - <Label htmlFor="contractSignerName">계약서명주체 명</Label> - <Input - id="contractSignerName" - value={formData.contractSignerName} - onChange={(e) => setFormData(prev => ({ ...prev, contractSignerName: e.target.value }))} - placeholder="계약서명주체 명을 입력하세요" - /> - </div> - </div> - </div> - - {/* SHI Qualification */} - <div className="space-y-4"> - <h4 className="text-sm font-semibold text-muted-foreground border-b pb-2">SHI Qualification</h4> - <div className="flex gap-6"> - <div className="flex items-center space-x-2"> - <Checkbox - id="shiAvl" - checked={formData.shiAvl} - onCheckedChange={(checked) => - setFormData(prev => ({ ...prev, shiAvl: !!checked })) - } - /> - <Label htmlFor="shiAvl">AVL</Label> - </div> - <div className="flex items-center space-x-2"> - <Checkbox - id="shiBlacklist" - checked={formData.shiBlacklist} - onCheckedChange={(checked) => - setFormData(prev => ({ ...prev, shiBlacklist: !!checked })) - } - /> - <Label htmlFor="shiBlacklist">Blacklist</Label> - </div> - <div className="flex items-center space-x-2"> - <Checkbox - id="shiBcc" - checked={formData.shiBcc} - onCheckedChange={(checked) => - setFormData(prev => ({ ...prev, shiBcc: !!checked })) - } - /> - <Label htmlFor="shiBcc">BCC</Label> - </div> - </div> - </div> - - {/* 기술영업 견적결과 */} - <div className="space-y-4"> - <h4 className="text-sm font-semibold text-muted-foreground border-b pb-2">기술영업 견적결과</h4> - <div className="grid grid-cols-2 gap-4"> - <div className="space-y-2"> - <Label htmlFor="salesQuoteNumber">기술영업 견적번호</Label> - <Input - id="salesQuoteNumber" - value={formData.salesQuoteNumber} - onChange={(e) => setFormData(prev => ({ ...prev, salesQuoteNumber: e.target.value }))} - placeholder="기술영업 견적번호를 입력하세요" - /> - </div> - <div className="space-y-2"> - <Label htmlFor="quoteCode">견적서 Code</Label> - <Input - id="quoteCode" - value={formData.quoteCode} - onChange={(e) => setFormData(prev => ({ ...prev, quoteCode: e.target.value }))} - placeholder="견적서 Code를 입력하세요" - /> - </div> - <div className="space-y-2"> - <Label htmlFor="salesVendorInfo">견적 협력업체 명</Label> - <Input - id="salesVendorInfo" - value={formData.salesVendorInfo} - onChange={(e) => setFormData(prev => ({ ...prev, salesVendorInfo: e.target.value }))} - placeholder="견적 협력업체 명을 입력하세요" - /> - </div> - <div className="space-y-2"> - <Label htmlFor="salesCountry">국가</Label> - <Input - id="salesCountry" - value={formData.salesCountry} - onChange={(e) => setFormData(prev => ({ ...prev, salesCountry: e.target.value }))} - placeholder="국가를 입력하세요" - /> - </div> - <div className="space-y-2"> - <Label htmlFor="totalAmount">총 금액</Label> - <Input - id="totalAmount" - type="number" - value={formData.totalAmount} - onChange={(e) => setFormData(prev => ({ ...prev, totalAmount: e.target.value }))} - placeholder="총 금액을 입력하세요" - /> - </div> - <div className="space-y-2"> - <Label htmlFor="quoteReceivedDate">견적접수일 (YYYY-MM-DD)</Label> - <Input - id="quoteReceivedDate" - value={formData.quoteReceivedDate} - onChange={(e) => setFormData(prev => ({ ...prev, quoteReceivedDate: e.target.value }))} - placeholder="YYYY-MM-DD" - /> - </div> - </div> - </div> - - {/* 업체 실적 현황 */} - <div className="space-y-4"> - <h4 className="text-sm font-semibold text-muted-foreground border-b pb-2">업체 실적 현황</h4> - <div className="grid grid-cols-2 gap-4"> - <div className="space-y-2"> - <Label htmlFor="recentQuoteNumber">최근견적번호</Label> - <Input - id="recentQuoteNumber" - value={formData.recentQuoteNumber} - onChange={(e) => setFormData(prev => ({ ...prev, recentQuoteNumber: e.target.value }))} - placeholder="최근견적번호를 입력하세요" - /> - </div> - <div className="space-y-2"> - <Label htmlFor="recentQuoteDate">최근견적일 (YYYY-MM-DD)</Label> - <Input - id="recentQuoteDate" - value={formData.recentQuoteDate} - onChange={(e) => setFormData(prev => ({ ...prev, recentQuoteDate: e.target.value }))} - placeholder="YYYY-MM-DD" - /> - </div> - <div className="space-y-2"> - <Label htmlFor="recentOrderNumber">최근발주번호</Label> - <Input - id="recentOrderNumber" - value={formData.recentOrderNumber} - onChange={(e) => setFormData(prev => ({ ...prev, recentOrderNumber: e.target.value }))} - placeholder="최근발주번호를 입력하세요" - /> - </div> - <div className="space-y-2"> - <Label htmlFor="recentOrderDate">최근발주일 (YYYY-MM-DD)</Label> - <Input - id="recentOrderDate" - value={formData.recentOrderDate} - onChange={(e) => setFormData(prev => ({ ...prev, recentOrderDate: e.target.value }))} - placeholder="YYYY-MM-DD" - /> - </div> - </div> - </div> - - {/* 기타 */} - <div className="space-y-4"> - <h4 className="text-sm font-semibold text-muted-foreground border-b pb-2">기타</h4> - <div className="space-y-2"> - <Label htmlFor="remarks">비고</Label> - <Textarea - id="remarks" - value={formData.remarks} - onChange={(e) => setFormData(prev => ({ ...prev, remarks: e.target.value }))} - placeholder="비고를 입력하세요" - rows={3} - /> - </div> - </div> - </div> - <DialogFooter> - <Button type="button" variant="outline" onClick={handleCancel}> - 취소 - </Button> - <Button type="button" onClick={handleSubmit}> - {editingItem ? "수정" : "추가"} - </Button> - </DialogFooter> - </DialogContent> - </Dialog> - ) -} diff --git a/lib/avl/table/project-avl-table.tsx b/lib/avl/table/project-avl-table.tsx index fc8f0f5e..7a0fda2e 100644 --- a/lib/avl/table/project-avl-table.tsx +++ b/lib/avl/table/project-avl-table.tsx @@ -4,6 +4,14 @@ import * as React from "react" import { forwardRef, useImperativeHandle, useLayoutEffect, useMemo } from "react" import { DataTable } from "@/components/data-table/data-table" import { Button } from "@/components/ui/button" +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog" import { AvlVendorAddAndModifyDialog } from "./avl-vendor-add-and-modify-dialog" import { getProjectAvlVendorInfo, createAvlVendorInfo, updateAvlVendorInfo, deleteAvlVendorInfo, finalizeProjectAvl } from "../service" import { useReactTable, getCoreRowModel, getPaginationRowModel, getSortedRowModel, getFilteredRowModel } from "@tanstack/react-table" @@ -384,6 +392,9 @@ export const ProjectAvlTable = forwardRef<ProjectAvlTableRef, ProjectAvlTablePro } }, [table, loadData]) + // 최종 확정 다이얼로그 상태 + const [isConfirmDialogOpen, setIsConfirmDialogOpen] = React.useState(false) + // 최종 확정 핸들러 const handleFinalizeAvl = React.useCallback(async () => { // 1. 필수 조건 검증 @@ -402,27 +413,20 @@ export const ProjectAvlTable = forwardRef<ProjectAvlTableRef, ProjectAvlTablePro return } - // 2. 사용자 확인 - const confirmed = window.confirm( - `현재 프로젝트(${localProjectCode})의 AVL을 최종 확정하시겠습니까?\n\n` + - `- 프로젝트명: ${projectInfo.projectName}\n` + - `- 벤더 정보: ${data.length}개\n` + - `- 공사부문: ${projectInfo.constructionSector}\n` + - `- 선종: ${projectInfo.shipType}\n` + - `- H/T 구분: ${projectInfo.htDivision}\n\n` + - `확정 후에는 수정이 어려울 수 있습니다.` - ) - - if (!confirmed) return + // 2. 확인 다이얼로그 열기 + setIsConfirmDialogOpen(true) + }, [localProjectCode, projectInfo, data.length]) + // 실제 최종 확정 실행 함수 + const executeFinalizeAvl = React.useCallback(async () => { try { - // 3. 현재 데이터의 모든 ID 수집 + // 3. 현재 데이터의 모든 ID 수집 (전체 레코드 기준) const avlVendorInfoIds = data.map(item => item.id) // 4. 최종 확정 실행 const result = await finalizeProjectAvl( localProjectCode, - projectInfo, + projectInfo!, avlVendorInfoIds, sessionData?.user?.name || "" ) @@ -441,6 +445,8 @@ export const ProjectAvlTable = forwardRef<ProjectAvlTableRef, ProjectAvlTablePro } catch (error) { console.error("AVL 최종 확정 실패:", error) toast.error("AVL 최종 확정 중 오류가 발생했습니다.") + } finally { + setIsConfirmDialogOpen(false) } }, [localProjectCode, projectInfo, data, table, loadData, sessionData?.user?.name]) @@ -595,6 +601,37 @@ export const ProjectAvlTable = forwardRef<ProjectAvlTableRef, ProjectAvlTablePro isTemplate={false} // 프로젝트 AVL 모드 initialProjectCode={localProjectCode} /> + + {/* 최종 확정 확인 다이얼로그 */} + <Dialog open={isConfirmDialogOpen} onOpenChange={setIsConfirmDialogOpen}> + <DialogContent> + <DialogHeader> + <DialogTitle>프로젝트 AVL 최종 확정</DialogTitle> + <DialogDescription> + 현재 프로젝트의 AVL을 최종 확정하시겠습니까? + </DialogDescription> + </DialogHeader> + <div className="space-y-2 text-sm"> + <div>• 프로젝트 코드: {localProjectCode}</div> + <div>• 프로젝트명: {projectInfo?.projectName || ""}</div> + <div>• 공사부문: {projectInfo?.constructionSector || ""}</div> + <div>• 선종: {projectInfo?.shipType || ""}</div> + <div>• H/T 구분: {projectInfo?.htDivision || ""}</div> + <div>• 벤더 정보: {data.length}개 (전체 레코드)</div> + {/* <div className="text-amber-600 font-medium mt-4"> + ⚠️ 확정 후 내용 수정을 필요로 하는 경우 동일 건을 다시 최종확정해 revision 처리로 수정해야 합니다. + </div> */} + </div> + <DialogFooter> + <Button variant="outline" onClick={() => setIsConfirmDialogOpen(false)}> + 취소 + </Button> + <Button onClick={executeFinalizeAvl}> + 최종 확정 + </Button> + </DialogFooter> + </DialogContent> + </Dialog> </div> ) }) diff --git a/lib/avl/table/standard-avl-add-dialog.tsx b/lib/avl/table/standard-avl-add-dialog.tsx deleted file mode 100644 index 9e8b016c..00000000 --- a/lib/avl/table/standard-avl-add-dialog.tsx +++ /dev/null @@ -1,960 +0,0 @@ -"use client" - -import * as React from "react" -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, - DialogTrigger, -} from "@/components/ui/dialog" -import { Button } from "@/components/ui/button" -import { Input } from "@/components/ui/input" -import { Label } from "@/components/ui/label" -import { Checkbox } from "@/components/ui/checkbox" -import { Textarea } from "@/components/ui/textarea" -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select" -import { toast } from "sonner" -import type { AvlVendorInfoInput, AvlDetailItem } from "../types" - -interface StandardAvlAddDialogProps { - open: boolean - onOpenChange: (open: boolean) => void - onAddItem: (item: Omit<AvlVendorInfoInput, 'avlListId'>) => Promise<void> - editingItem?: AvlDetailItem // 수정할 항목 (없으면 추가 모드) - onUpdateItem?: (id: number, item: Omit<AvlVendorInfoInput, 'avlListId'>) => Promise<void> // 수정 핸들러 - // 검색 조건에서 선택한 값들을 초기값으로 사용 - initialConstructionSector?: string - initialShipType?: string - initialAvlKind?: string - initialHtDivision?: string -} - -export function StandardAvlAddDialog({ - open, - onOpenChange, - onAddItem, - editingItem, - onUpdateItem, - initialConstructionSector, - initialShipType, - initialAvlKind, - initialHtDivision -}: StandardAvlAddDialogProps) { - const [formData, setFormData] = React.useState<Omit<AvlVendorInfoInput, 'avlListId'>>({ - // 표준 AVL용 기본 설정 - isTemplate: true, - - // 표준 AVL 필수 필드들 (검색 조건에서 선택한 값들로 초기화) - constructionSector: initialConstructionSector || "", - shipType: initialShipType || "", - avlKind: initialAvlKind || "", - htDivision: initialHtDivision || "", - - // 설계 정보 - equipBulkDivision: "EQUIP", - disciplineCode: "", - disciplineName: "", - - // 자재 정보 - materialNameCustomerSide: "", - - // 패키지 정보 - packageCode: "", - packageName: "", - - // 자재그룹 정보 - materialGroupCode: "", - materialGroupName: "", - - // 협력업체 정보 - vendorName: "", - vendorCode: "", - - // AVL 정보 - avlVendorName: "", - tier: "", - - // 제안방향 - ownerSuggestion: false, - shiSuggestion: false, - - // 위치 정보 - headquarterLocation: "", - manufacturingLocation: "", - - // FA 정보 - faTarget: false, - faStatus: "", - - // Agent 정보 - isAgent: false, - - // 계약 서명주체 - contractSignerName: "", - contractSignerCode: "", - - // SHI Qualification - shiAvl: false, - shiBlacklist: false, - shiBcc: false, - - // 기술영업 견적결과 - salesQuoteNumber: "", - quoteCode: "", - salesVendorInfo: "", - salesCountry: "", - totalAmount: "", - quoteReceivedDate: "", - - // 업체 실적 현황 - recentQuoteDate: "", - recentQuoteNumber: "", - recentOrderDate: "", - recentOrderNumber: "", - - // 기타 - remarks: "" - }) - - // 수정 모드일 때 폼 데이터 초기화 - React.useEffect(() => { - if (editingItem) { - setFormData({ - // 표준 AVL용 기본 설정 - isTemplate: true, - - // 표준 AVL 필수 필드들 (기존 값 우선, 없으면 검색 조건 값 사용) - constructionSector: editingItem.constructionSector || initialConstructionSector || "", - shipType: editingItem.shipType || initialShipType || "", - avlKind: editingItem.avlKind || initialAvlKind || "", - htDivision: editingItem.htDivision || initialHtDivision || "", - - // 설계 정보 - equipBulkDivision: editingItem.equipBulkDivision === "EQUIP" ? "EQUIP" : "BULK", - disciplineCode: editingItem.disciplineCode || "", - disciplineName: editingItem.disciplineName || "", - - // 자재 정보 - materialNameCustomerSide: editingItem.materialNameCustomerSide || "", - - // 패키지 정보 - packageCode: editingItem.packageCode || "", - packageName: editingItem.packageName || "", - - // 자재그룹 정보 - materialGroupCode: editingItem.materialGroupCode || "", - materialGroupName: editingItem.materialGroupName || "", - - // 협력업체 정보 - vendorName: editingItem.vendorName || "", - vendorCode: editingItem.vendorCode || "", - - // AVL 정보 - avlVendorName: editingItem.avlVendorName || "", - tier: editingItem.tier || "", - - // 제안방향 - ownerSuggestion: editingItem.ownerSuggestion || false, - shiSuggestion: editingItem.shiSuggestion || false, - - // 위치 정보 - headquarterLocation: editingItem.headquarterLocation || "", - manufacturingLocation: editingItem.manufacturingLocation || "", - - // FA 정보 - faTarget: editingItem.faTarget || false, - faStatus: editingItem.faStatus || "", - - // Agent 정보 - isAgent: editingItem.isAgent || false, - - // 계약 서명주체 - contractSignerName: editingItem.contractSignerName || "", - contractSignerCode: editingItem.contractSignerCode || "", - - // SHI Qualification - shiAvl: editingItem.shiAvl || false, - shiBlacklist: editingItem.shiBlacklist || false, - shiBcc: editingItem.shiBcc || false, - - // 기술영업 견적결과 - salesQuoteNumber: editingItem.salesQuoteNumber || "", - quoteCode: editingItem.quoteCode || "", - salesVendorInfo: editingItem.salesVendorInfo || "", - salesCountry: editingItem.salesCountry || "", - totalAmount: editingItem.totalAmount || "", - quoteReceivedDate: editingItem.quoteReceivedDate || "", - - // 업체 실적 현황 - recentQuoteDate: editingItem.recentQuoteDate || "", - recentQuoteNumber: editingItem.recentQuoteNumber || "", - recentOrderDate: editingItem.recentOrderDate || "", - recentOrderNumber: editingItem.recentOrderNumber || "", - - // 기타 - remarks: editingItem.remarks || "" - }) - } - }, [editingItem, initialConstructionSector, initialShipType, initialAvlKind, initialHtDivision]) - - // 다이얼로그가 열릴 때 초기값 재설정 (수정 모드가 아닐 때만) - React.useEffect(() => { - if (open && !editingItem) { - setFormData(prev => ({ - ...prev, - constructionSector: initialConstructionSector || "", - shipType: initialShipType || "", - avlKind: initialAvlKind || "", - htDivision: initialHtDivision || "", - })) - } - }, [open, editingItem, initialConstructionSector, initialShipType, initialAvlKind, initialHtDivision]) - - const handleSubmit = async () => { - // 필수 필드 검증 (표준 AVL용) - if (!formData.constructionSector || !formData.shipType || !formData.avlKind || !formData.htDivision) { - toast.error("공사부문, 선종, AVL종류, H/T 구분은 필수 입력 항목입니다.") - return - } - - if (!formData.disciplineName || !formData.materialNameCustomerSide) { - toast.error("설계공종과 고객사 AVL 자재명은 필수 입력 항목입니다.") - return - } - - try { - if (editingItem && onUpdateItem) { - // 수정 모드 - await onUpdateItem(editingItem.id, formData) - } else { - // 추가 모드 - await onAddItem(formData) - } - - // 폼 초기화 (onAddItem에서 성공적으로 처리된 경우에만) - setFormData({ - // 표준 AVL용 기본 설정 - isTemplate: true, - - // 표준 AVL 필수 필드들 - constructionSector: "", - shipType: "", - avlKind: "", - htDivision: "", - - // 설계 정보 - equipBulkDivision: "EQUIP", - disciplineCode: "", - disciplineName: "", - - // 자재 정보 - materialNameCustomerSide: "", - - // 패키지 정보 - packageCode: "", - packageName: "", - - // 자재그룹 정보 - materialGroupCode: "", - materialGroupName: "", - - // 협력업체 정보 - vendorName: "", - vendorCode: "", - - // AVL 정보 - avlVendorName: "", - tier: "", - - // 제안방향 - ownerSuggestion: false, - shiSuggestion: false, - - // 위치 정보 - headquarterLocation: "", - manufacturingLocation: "", - - // FA 정보 - faTarget: false, - faStatus: "", - - // Agent 정보 - isAgent: false, - - // 계약 서명주체 - contractSignerName: "", - contractSignerCode: "", - - // SHI Qualification - shiAvl: false, - shiBlacklist: false, - shiBcc: false, - - // 기술영업 견적결과 - salesQuoteNumber: "", - quoteCode: "", - salesVendorInfo: "", - salesCountry: "", - totalAmount: "", - quoteReceivedDate: "", - - // 업체 실적 현황 - recentQuoteDate: "", - recentQuoteNumber: "", - recentOrderDate: "", - recentOrderNumber: "", - - // 기타 - remarks: "" - } as Omit<AvlVendorInfoInput, 'avlListId'>) - - onOpenChange(false) - } catch (error) { - // 에러 처리는 onAddItem에서 담당하므로 여기서는 아무것도 하지 않음 - } - } - - const handleCancel = () => { - setFormData({ - // 표준 AVL용 기본 설정 - isTemplate: true, - - // 표준 AVL 필수 필드들 - constructionSector: "", - shipType: "", - avlKind: "", - htDivision: "", - - // 설계 정보 - equipBulkDivision: "EQUIP", - disciplineCode: "", - disciplineName: "", - - // 자재 정보 - materialNameCustomerSide: "", - - // 패키지 정보 - packageCode: "", - packageName: "", - - // 자재그룹 정보 - materialGroupCode: "", - materialGroupName: "", - - // 협력업체 정보 - vendorName: "", - vendorCode: "", - - // AVL 정보 - avlVendorName: "", - tier: "", - - // 제안방향 - ownerSuggestion: false, - shiSuggestion: false, - - // 위치 정보 - headquarterLocation: "", - manufacturingLocation: "", - - // FA 정보 - faTarget: false, - faStatus: "", - - // Agent 정보 - isAgent: false, - - // 계약 서명주체 - contractSignerName: "", - contractSignerCode: "", - - // SHI Qualification - shiAvl: false, - shiBlacklist: false, - shiBcc: false, - - // 기술영업 견적결과 - salesQuoteNumber: "", - quoteCode: "", - salesVendorInfo: "", - salesCountry: "", - totalAmount: "", - quoteReceivedDate: "", - - // 업체 실적 현황 - recentQuoteDate: "", - recentQuoteNumber: "", - recentOrderDate: "", - recentOrderNumber: "", - - // 기타 - remarks: "" - } as Omit<AvlVendorInfoInput, 'avlListId'>) - onOpenChange(false) - } - - // 선종 옵션들 (공사부문에 따라 다름) - const getShipTypeOptions = (constructionSector: string) => { - if (constructionSector === "조선") { - return [ - { value: "A-max", label: "A-max" }, - { value: "S-max", label: "S-max" }, - { value: "VLCC", label: "VLCC" }, - { value: "LNGC", label: "LNGC" }, - { value: "CONT", label: "CONT" }, - ] - } else if (constructionSector === "해양") { - return [ - { value: "FPSO", label: "FPSO" }, - { value: "FLNG", label: "FLNG" }, - { value: "FPU", label: "FPU" }, - { value: "Platform", label: "Platform" }, - { value: "WTIV", label: "WTIV" }, - { value: "GOM", label: "GOM" }, - ] - } else { - return [] - } - } - - const shipTypeOptions = getShipTypeOptions(formData.constructionSector) - - return ( - <Dialog open={open} onOpenChange={onOpenChange}> - <DialogContent className="sm:max-w-[800px] max-h-[90vh] overflow-y-auto"> - <DialogHeader> - <DialogTitle>{editingItem ? "표준 AVL 항목 수정" : "표준 AVL 항목 추가"}</DialogTitle> - <DialogDescription> - {editingItem - ? "표준 AVL 항목을 수정합니다. 필수 항목을 입력해주세요." - : "새로운 표준 AVL 항목을 추가합니다. 필수 항목을 입력해주세요." - } * 표시된 항목은 필수 입력사항입니다. - </DialogDescription> - </DialogHeader> - <div className="space-y-6 py-4"> - {/* 표준 AVL 기본 정보 */} - <div className="space-y-4"> - <h4 className="text-sm font-semibold text-muted-foreground border-b pb-2">표준 AVL 기본 정보 *</h4> - <div className="grid grid-cols-2 gap-4"> - <div className="space-y-2"> - <Label htmlFor="constructionSector">공사부문 *</Label> - <Select - value={formData.constructionSector} - onValueChange={(value) => { - setFormData(prev => ({ - ...prev, - constructionSector: value, - shipType: "" // 공사부문 변경 시 선종 초기화 - })) - }} - > - <SelectTrigger> - <SelectValue placeholder="공사부문을 선택하세요" /> - </SelectTrigger> - <SelectContent> - <SelectItem value="조선">조선</SelectItem> - <SelectItem value="해양">해양</SelectItem> - </SelectContent> - </Select> - </div> - <div className="space-y-2"> - <Label htmlFor="shipType">선종 *</Label> - <Select - value={formData.shipType} - onValueChange={(value) => - setFormData(prev => ({ ...prev, shipType: value })) - } - disabled={!formData.constructionSector} - > - <SelectTrigger> - <SelectValue placeholder="선종을 선택하세요" /> - </SelectTrigger> - <SelectContent> - {shipTypeOptions.map((option) => ( - <SelectItem key={option.value} value={option.value}> - {option.label} - </SelectItem> - ))} - </SelectContent> - </Select> - </div> - <div className="space-y-2"> - <Label htmlFor="avlKind">AVL종류 *</Label> - <Select - value={formData.avlKind} - onValueChange={(value) => - setFormData(prev => ({ ...prev, avlKind: value })) - } - > - <SelectTrigger> - <SelectValue placeholder="AVL종류를 선택하세요" /> - </SelectTrigger> - <SelectContent> - <SelectItem value="Nearshore">Nearshore</SelectItem> - <SelectItem value="Offshore">Offshore</SelectItem> - <SelectItem value="IOC">IOC</SelectItem> - <SelectItem value="NOC">NOC</SelectItem> - </SelectContent> - </Select> - </div> - <div className="space-y-2"> - <Label htmlFor="htDivision">H/T 구분 *</Label> - <Select - value={formData.htDivision} - onValueChange={(value) => - setFormData(prev => ({ ...prev, htDivision: value })) - } - > - <SelectTrigger> - <SelectValue placeholder="H/T 구분을 선택하세요" /> - </SelectTrigger> - <SelectContent> - <SelectItem value="공통">공통</SelectItem> - <SelectItem value="H">Hull (H)</SelectItem> - <SelectItem value="T">Topside (T)</SelectItem> - </SelectContent> - </Select> - </div> - </div> - </div> - - {/* 기본 정보 */} - <div className="space-y-4"> - <h4 className="text-sm font-semibold text-muted-foreground border-b pb-2">기본 정보</h4> - <div className="grid grid-cols-2 gap-4"> - <div className="space-y-2"> - <Label htmlFor="equipBulkDivision">EQUIP/BULK 구분</Label> - <Select - value={formData.equipBulkDivision} - onValueChange={(value: "EQUIP" | "BULK") => - setFormData(prev => ({ ...prev, equipBulkDivision: value })) - } - > - <SelectTrigger> - <SelectValue /> - </SelectTrigger> - <SelectContent> - <SelectItem value="EQUIP">EQUIP</SelectItem> - <SelectItem value="BULK">BULK</SelectItem> - </SelectContent> - </Select> - </div> - <div className="space-y-2"> - <Label htmlFor="disciplineCode">설계공종코드</Label> - <Input - id="disciplineCode" - value={formData.disciplineCode} - onChange={(e) => setFormData(prev => ({ ...prev, disciplineCode: e.target.value }))} - placeholder="설계공종코드를 입력하세요" - /> - </div> - <div className="space-y-2 col-span-2"> - <Label htmlFor="disciplineName">설계공종명 *</Label> - <Input - id="disciplineName" - value={formData.disciplineName} - onChange={(e) => setFormData(prev => ({ ...prev, disciplineName: e.target.value }))} - placeholder="설계공종명을 입력하세요" - /> - </div> - <div className="space-y-2 col-span-2"> - <Label htmlFor="materialNameCustomerSide">고객사 AVL 자재명 *</Label> - <Input - id="materialNameCustomerSide" - value={formData.materialNameCustomerSide} - onChange={(e) => setFormData(prev => ({ ...prev, materialNameCustomerSide: e.target.value }))} - placeholder="고객사 AVL 자재명을 입력하세요" - /> - </div> - </div> - </div> - - {/* 패키지 정보 */} - <div className="space-y-4"> - <h4 className="text-sm font-semibold text-muted-foreground border-b pb-2">패키지 정보</h4> - <div className="grid grid-cols-2 gap-4"> - <div className="space-y-2"> - <Label htmlFor="packageCode">패키지 코드</Label> - <Input - id="packageCode" - value={formData.packageCode} - onChange={(e) => setFormData(prev => ({ ...prev, packageCode: e.target.value }))} - placeholder="패키지 코드를 입력하세요" - /> - </div> - <div className="space-y-2"> - <Label htmlFor="packageName">패키지 명</Label> - <Input - id="packageName" - value={formData.packageName} - onChange={(e) => setFormData(prev => ({ ...prev, packageName: e.target.value }))} - placeholder="패키지 명을 입력하세요" - /> - </div> - </div> - </div> - - {/* 자재그룹 정보 */} - <div className="space-y-4"> - <h4 className="text-sm font-semibold text-muted-foreground border-b pb-2">자재그룹 정보</h4> - <div className="grid grid-cols-2 gap-4"> - <div className="space-y-2"> - <Label htmlFor="materialGroupCode">자재그룹 코드</Label> - <Input - id="materialGroupCode" - value={formData.materialGroupCode} - onChange={(e) => setFormData(prev => ({ ...prev, materialGroupCode: e.target.value }))} - placeholder="자재그룹 코드를 입력하세요" - /> - </div> - <div className="space-y-2"> - <Label htmlFor="materialGroupName">자재그룹 명</Label> - <Input - id="materialGroupName" - value={formData.materialGroupName} - onChange={(e) => setFormData(prev => ({ ...prev, materialGroupName: e.target.value }))} - placeholder="자재그룹 명을 입력하세요" - /> - </div> - </div> - </div> - - {/* 협력업체 정보 */} - <div className="space-y-4"> - <h4 className="text-sm font-semibold text-muted-foreground border-b pb-2">협력업체 정보</h4> - <div className="grid grid-cols-2 gap-4"> - <div className="space-y-2"> - <Label htmlFor="vendorCode">협력업체 코드</Label> - <Input - id="vendorCode" - value={formData.vendorCode} - onChange={(e) => setFormData(prev => ({ ...prev, vendorCode: e.target.value }))} - placeholder="협력업체 코드를 입력하세요" - /> - </div> - <div className="space-y-2"> - <Label htmlFor="vendorName">협력업체 명</Label> - <Input - id="vendorName" - value={formData.vendorName} - onChange={(e) => setFormData(prev => ({ ...prev, vendorName: e.target.value }))} - placeholder="협력업체 명을 입력하세요" - /> - </div> - <div className="space-y-2"> - <Label htmlFor="avlVendorName">AVL 등재업체명</Label> - <Input - id="avlVendorName" - value={formData.avlVendorName} - onChange={(e) => setFormData(prev => ({ ...prev, avlVendorName: e.target.value }))} - placeholder="AVL 등재업체명을 입력하세요" - /> - </div> - <div className="space-y-2"> - <Label htmlFor="tier">등급 (Tier)</Label> - <Input - id="tier" - value={formData.tier} - onChange={(e) => setFormData(prev => ({ ...prev, tier: e.target.value }))} - placeholder="등급을 입력하세요" - /> - </div> - </div> - </div> - - {/* 제안방향 */} - <div className="space-y-4"> - <h4 className="text-sm font-semibold text-muted-foreground border-b pb-2">제안방향</h4> - <div className="flex gap-6"> - <div className="flex items-center space-x-2"> - <Checkbox - id="ownerSuggestion" - checked={formData.ownerSuggestion} - onCheckedChange={(checked) => - setFormData(prev => ({ ...prev, ownerSuggestion: !!checked })) - } - /> - <Label htmlFor="ownerSuggestion">선주제안</Label> - </div> - <div className="flex items-center space-x-2"> - <Checkbox - id="shiSuggestion" - checked={formData.shiSuggestion} - onCheckedChange={(checked) => - setFormData(prev => ({ ...prev, shiSuggestion: !!checked })) - } - /> - <Label htmlFor="shiSuggestion">SHI 제안</Label> - </div> - </div> - </div> - - {/* 위치 정보 */} - <div className="space-y-4"> - <h4 className="text-sm font-semibold text-muted-foreground border-b pb-2">위치 정보</h4> - <div className="grid grid-cols-2 gap-4"> - <div className="space-y-2"> - <Label htmlFor="headquarterLocation">본사 위치 (국가)</Label> - <Input - id="headquarterLocation" - value={formData.headquarterLocation} - onChange={(e) => setFormData(prev => ({ ...prev, headquarterLocation: e.target.value }))} - placeholder="본사 위치를 입력하세요" - /> - </div> - <div className="space-y-2"> - <Label htmlFor="manufacturingLocation">제작/선적지 (국가)</Label> - <Input - id="manufacturingLocation" - value={formData.manufacturingLocation} - onChange={(e) => setFormData(prev => ({ ...prev, manufacturingLocation: e.target.value }))} - placeholder="제작/선적지를 입력하세요" - /> - </div> - </div> - </div> - - {/* FA 정보 */} - <div className="space-y-4"> - <h4 className="text-sm font-semibold text-muted-foreground border-b pb-2">FA 정보</h4> - <div className="grid grid-cols-2 gap-4"> - <div className="flex items-center space-x-2"> - <Checkbox - id="faTarget" - checked={formData.faTarget} - onCheckedChange={(checked) => - setFormData(prev => ({ ...prev, faTarget: !!checked })) - } - /> - <Label htmlFor="faTarget">FA 대상</Label> - </div> - <div className="space-y-2"> - <Label htmlFor="faStatus">FA 현황</Label> - <Input - id="faStatus" - value={formData.faStatus} - onChange={(e) => setFormData(prev => ({ ...prev, faStatus: e.target.value }))} - placeholder="FA 현황을 입력하세요" - /> - </div> - </div> - </div> - - {/* Agent 정보 */} - <div className="space-y-4"> - <h4 className="text-sm font-semibold text-muted-foreground border-b pb-2">Agent 정보</h4> - <div className="flex items-center space-x-2"> - <Checkbox - id="isAgent" - checked={formData.isAgent} - onCheckedChange={(checked) => - setFormData(prev => ({ ...prev, isAgent: !!checked })) - } - /> - <Label htmlFor="isAgent">Agent 여부</Label> - </div> - </div> - - {/* 계약 서명주체 */} - <div className="space-y-4"> - <h4 className="text-sm font-semibold text-muted-foreground border-b pb-2">계약 서명주체</h4> - <div className="grid grid-cols-2 gap-4"> - <div className="space-y-2"> - <Label htmlFor="contractSignerCode">계약서명주체 코드</Label> - <Input - id="contractSignerCode" - value={formData.contractSignerCode} - onChange={(e) => setFormData(prev => ({ ...prev, contractSignerCode: e.target.value }))} - placeholder="계약서명주체 코드를 입력하세요" - /> - </div> - <div className="space-y-2"> - <Label htmlFor="contractSignerName">계약서명주체 명</Label> - <Input - id="contractSignerName" - value={formData.contractSignerName} - onChange={(e) => setFormData(prev => ({ ...prev, contractSignerName: e.target.value }))} - placeholder="계약서명주체 명을 입력하세요" - /> - </div> - </div> - </div> - - {/* SHI Qualification */} - <div className="space-y-4"> - <h4 className="text-sm font-semibold text-muted-foreground border-b pb-2">SHI Qualification</h4> - <div className="flex gap-6"> - <div className="flex items-center space-x-2"> - <Checkbox - id="shiAvl" - checked={formData.shiAvl} - onCheckedChange={(checked) => - setFormData(prev => ({ ...prev, shiAvl: !!checked })) - } - /> - <Label htmlFor="shiAvl">AVL</Label> - </div> - <div className="flex items-center space-x-2"> - <Checkbox - id="shiBlacklist" - checked={formData.shiBlacklist} - onCheckedChange={(checked) => - setFormData(prev => ({ ...prev, shiBlacklist: !!checked })) - } - /> - <Label htmlFor="shiBlacklist">Blacklist</Label> - </div> - <div className="flex items-center space-x-2"> - <Checkbox - id="shiBcc" - checked={formData.shiBcc} - onCheckedChange={(checked) => - setFormData(prev => ({ ...prev, shiBcc: !!checked })) - } - /> - <Label htmlFor="shiBcc">BCC</Label> - </div> - </div> - </div> - - {/* 기술영업 견적결과 */} - <div className="space-y-4"> - <h4 className="text-sm font-semibold text-muted-foreground border-b pb-2">기술영업 견적결과</h4> - <div className="grid grid-cols-2 gap-4"> - <div className="space-y-2"> - <Label htmlFor="salesQuoteNumber">기술영업 견적번호</Label> - <Input - id="salesQuoteNumber" - value={formData.salesQuoteNumber} - onChange={(e) => setFormData(prev => ({ ...prev, salesQuoteNumber: e.target.value }))} - placeholder="기술영업 견적번호를 입력하세요" - /> - </div> - <div className="space-y-2"> - <Label htmlFor="quoteCode">견적서 Code</Label> - <Input - id="quoteCode" - value={formData.quoteCode} - onChange={(e) => setFormData(prev => ({ ...prev, quoteCode: e.target.value }))} - placeholder="견적서 Code를 입력하세요" - /> - </div> - <div className="space-y-2"> - <Label htmlFor="salesVendorInfo">견적 협력업체 명</Label> - <Input - id="salesVendorInfo" - value={formData.salesVendorInfo} - onChange={(e) => setFormData(prev => ({ ...prev, salesVendorInfo: e.target.value }))} - placeholder="견적 협력업체 명을 입력하세요" - /> - </div> - <div className="space-y-2"> - <Label htmlFor="salesCountry">국가</Label> - <Input - id="salesCountry" - value={formData.salesCountry} - onChange={(e) => setFormData(prev => ({ ...prev, salesCountry: e.target.value }))} - placeholder="국가를 입력하세요" - /> - </div> - <div className="space-y-2"> - <Label htmlFor="totalAmount">총 금액</Label> - <Input - id="totalAmount" - type="number" - value={formData.totalAmount} - onChange={(e) => setFormData(prev => ({ ...prev, totalAmount: e.target.value }))} - placeholder="총 금액을 입력하세요" - /> - </div> - <div className="space-y-2"> - <Label htmlFor="quoteReceivedDate">견적접수일 (YYYY-MM-DD)</Label> - <Input - id="quoteReceivedDate" - value={formData.quoteReceivedDate} - onChange={(e) => setFormData(prev => ({ ...prev, quoteReceivedDate: e.target.value }))} - placeholder="YYYY-MM-DD" - /> - </div> - </div> - </div> - - {/* 업체 실적 현황 */} - <div className="space-y-4"> - <h4 className="text-sm font-semibold text-muted-foreground border-b pb-2">업체 실적 현황</h4> - <div className="grid grid-cols-2 gap-4"> - <div className="space-y-2"> - <Label htmlFor="recentQuoteNumber">최근견적번호</Label> - <Input - id="recentQuoteNumber" - value={formData.recentQuoteNumber} - onChange={(e) => setFormData(prev => ({ ...prev, recentQuoteNumber: e.target.value }))} - placeholder="최근견적번호를 입력하세요" - /> - </div> - <div className="space-y-2"> - <Label htmlFor="recentQuoteDate">최근견적일 (YYYY-MM-DD)</Label> - <Input - id="recentQuoteDate" - value={formData.recentQuoteDate} - onChange={(e) => setFormData(prev => ({ ...prev, recentQuoteDate: e.target.value }))} - placeholder="YYYY-MM-DD" - /> - </div> - <div className="space-y-2"> - <Label htmlFor="recentOrderNumber">최근발주번호</Label> - <Input - id="recentOrderNumber" - value={formData.recentOrderNumber} - onChange={(e) => setFormData(prev => ({ ...prev, recentOrderNumber: e.target.value }))} - placeholder="최근발주번호를 입력하세요" - /> - </div> - <div className="space-y-2"> - <Label htmlFor="recentOrderDate">최근발주일 (YYYY-MM-DD)</Label> - <Input - id="recentOrderDate" - value={formData.recentOrderDate} - onChange={(e) => setFormData(prev => ({ ...prev, recentOrderDate: e.target.value }))} - placeholder="YYYY-MM-DD" - /> - </div> - </div> - </div> - - {/* 기타 */} - <div className="space-y-4"> - <h4 className="text-sm font-semibold text-muted-foreground border-b pb-2">기타</h4> - <div className="space-y-2"> - <Label htmlFor="remarks">비고</Label> - <Textarea - id="remarks" - value={formData.remarks} - onChange={(e) => setFormData(prev => ({ ...prev, remarks: e.target.value }))} - placeholder="비고를 입력하세요" - rows={3} - /> - </div> - </div> - </div> - <DialogFooter> - <Button type="button" variant="outline" onClick={handleCancel}> - 취소 - </Button> - <Button type="button" onClick={handleSubmit}> - {editingItem ? "수정" : "추가"} - </Button> - </DialogFooter> - </DialogContent> - </Dialog> - ) -} diff --git a/lib/avl/table/standard-avl-table.tsx b/lib/avl/table/standard-avl-table.tsx index cc39540b..bacb5812 100644 --- a/lib/avl/table/standard-avl-table.tsx +++ b/lib/avl/table/standard-avl-table.tsx @@ -15,6 +15,14 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select" +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog" import { Search } from "lucide-react" import { toast } from "sonner" import { standardAvlColumns } from "./standard-avl-table-columns" @@ -22,13 +30,9 @@ import { AvlVendorAddAndModifyDialog } from "./avl-vendor-add-and-modify-dialog" import { createAvlVendorInfo, updateAvlVendorInfo, deleteAvlVendorInfo, finalizeStandardAvl } from "../service" import { AvlVendorInfoInput } from "../types" import { useSession } from "next-auth/react" +import { ShipTypeSelector, ShipTypeItem } from "@/components/common/ship-type" /** - * 조선인 경우, 선종: - * A-max, S-max, VLCC, LNGC, CONT - * 해양인 경우, 선종: - * FPSO, FLNG, FPU, Platform, WTIV, GOM - * * AVL종류: * Nearshore, Offshore, IOC, NOC */ @@ -39,31 +43,6 @@ const constructionSectorOptions = [ { value: "해양", label: "해양" }, ] -// 공사부문에 따른 선종 옵션들 -const getShipTypeOptions = (constructionSector: string) => { - if (constructionSector === "조선") { - return [ - { value: "A-max", label: "A-max" }, - { value: "S-max", label: "S-max" }, - { value: "VLCC", label: "VLCC" }, - { value: "LNGC", label: "LNGC" }, - { value: "CONT", label: "CONT" }, - ] - } else if (constructionSector === "해양") { - return [ - { value: "FPSO", label: "FPSO" }, - { value: "FLNG", label: "FLNG" }, - { value: "FPU", label: "FPU" }, - { value: "Platform", label: "Platform" }, - { value: "WTIV", label: "WTIV" }, - { value: "GOM", label: "GOM" }, - ] - } else { - // 공사부문이 선택되지 않은 경우 빈 배열 - return [] - } -} - const avlKindOptions = [ { value: "Nearshore", label: "Nearshore" }, { value: "Offshore", label: "Offshore" }, @@ -123,7 +102,9 @@ export const StandardAvlTable = forwardRef<StandardAvlTableRef, StandardAvlTable // 검색 상태 const [searchConstructionSector, setSearchConstructionSector] = React.useState(initialConstructionSector || "") - const [searchShipType, setSearchShipType] = React.useState(initialShipType || "") + const [selectedShipType, setSelectedShipType] = React.useState<ShipTypeItem | undefined>( + initialShipType ? { CD: initialShipType, CDNM: initialShipType, displayText: initialShipType } : undefined + ) const [searchAvlKind, setSearchAvlKind] = React.useState(initialAvlKind || "") const [searchHtDivision, setSearchHtDivision] = React.useState(initialHtDivision || "") @@ -173,34 +154,29 @@ export const StandardAvlTable = forwardRef<StandardAvlTableRef, StandardAvlTable const handleConstructionSectorChange = React.useCallback((value: string) => { setSearchConstructionSector(value) // 공사부문이 변경되면 선종을 빈 값으로 초기화 - setSearchShipType("") + setSelectedShipType(undefined) }, []) // 검색 상태 변경 시 부모 컴포넌트에 전달 React.useEffect(() => { onSearchConditionsChange?.({ constructionSector: searchConstructionSector, - shipType: searchShipType, + shipType: selectedShipType?.CD || "", avlKind: searchAvlKind, htDivision: searchHtDivision }) - }, [searchConstructionSector, searchShipType, searchAvlKind, searchHtDivision, onSearchConditionsChange]) + }, [searchConstructionSector, selectedShipType, searchAvlKind, searchHtDivision, onSearchConditionsChange]) - // 현재 공사부문에 따른 선종 옵션들 - const currentShipTypeOptions = React.useMemo(() => - getShipTypeOptions(searchConstructionSector), - [searchConstructionSector] - ) // 모든 검색 조건이 선택되었는지 확인 const isAllSearchConditionsSelected = React.useMemo(() => { return ( searchConstructionSector.trim() !== "" && - searchShipType.trim() !== "" && + selectedShipType?.CD?.trim() !== "" && searchAvlKind.trim() !== "" && searchHtDivision.trim() !== "" ) - }, [searchConstructionSector, searchShipType, searchAvlKind, searchHtDivision]) + }, [searchConstructionSector, selectedShipType, searchAvlKind, searchHtDivision]) // 데이터 로드 함수 const loadData = React.useCallback(async (searchParams: Partial<GetStandardAvlSchema> = {}) => { @@ -213,7 +189,7 @@ export const StandardAvlTable = forwardRef<StandardAvlTableRef, StandardAvlTable sort: searchParams.sort ?? [{ id: "no", desc: false }], flags: searchParams.flags ?? [], constructionSector: searchConstructionSector, - shipType: searchShipType, + shipType: selectedShipType?.CD || "", avlKind: searchAvlKind, htDivision: searchHtDivision as "공통" | "H" | "T" | "", equipBulkDivision: searchParams.equipBulkDivision || "", @@ -249,7 +225,7 @@ export const StandardAvlTable = forwardRef<StandardAvlTableRef, StandardAvlTable } finally { setLoading(false) } - }, [searchConstructionSector, searchShipType, searchAvlKind, searchHtDivision]) + }, [searchConstructionSector, selectedShipType, searchAvlKind, searchHtDivision]) // reloadTrigger가 변경될 때마다 데이터 리로드 React.useEffect(() => { @@ -262,7 +238,7 @@ export const StandardAvlTable = forwardRef<StandardAvlTableRef, StandardAvlTable // 검색 초기화 핸들러 const handleResetSearch = React.useCallback(() => { setSearchConstructionSector("") - setSearchShipType("") + setSelectedShipType(undefined) setSearchAvlKind("") setSearchHtDivision("") // 초기화 시 빈 데이터로 설정 @@ -365,6 +341,9 @@ export const StandardAvlTable = forwardRef<StandardAvlTableRef, StandardAvlTable } }, [table, loadData]) + // 최종 확정 다이얼로그 상태 + const [isConfirmDialogOpen, setIsConfirmDialogOpen] = React.useState(false) + // 최종 확정 핸들러 (표준 AVL) const handleFinalizeStandardAvl = React.useCallback(async () => { // 1. 필수 조건 검증 @@ -378,27 +357,20 @@ export const StandardAvlTable = forwardRef<StandardAvlTableRef, StandardAvlTable return } - // 2. 사용자 확인 - const confirmed = window.confirm( - `현재 표준 AVL을 최종 확정하시겠습니까?\n\n` + - `- 공사부문: ${searchConstructionSector}\n` + - `- 선종: ${searchShipType}\n` + - `- AVL종류: ${searchAvlKind}\n` + - `- H/T 구분: ${searchHtDivision}\n` + - `- 벤더 정보: ${data.length}개\n\n` + - `확정 후에는 수정이 어려울 수 있습니다.` - ) - - if (!confirmed) return + // 2. 확인 다이얼로그 열기 + setIsConfirmDialogOpen(true) + }, [isAllSearchConditionsSelected, data.length]) + // 실제 최종 확정 실행 함수 + const executeFinalizeStandardAvl = React.useCallback(async () => { try { - // 3. 현재 데이터의 모든 ID 수집 + // 3. 현재 데이터의 모든 ID 수집 (전체 레코드 기준) const avlVendorInfoIds = data.map(item => item.id) // 4. 최종 확정 실행 const standardAvlInfo = { constructionSector: searchConstructionSector, - shipType: searchShipType, + shipType: selectedShipType?.CD || "", avlKind: searchAvlKind, htDivision: searchHtDivision } @@ -423,8 +395,10 @@ export const StandardAvlTable = forwardRef<StandardAvlTableRef, StandardAvlTable } catch (error) { console.error("표준 AVL 최종 확정 실패:", error) toast.error("표준 AVL 최종 확정 중 오류가 발생했습니다.") + } finally { + setIsConfirmDialogOpen(false) } - }, [searchConstructionSector, searchShipType, searchAvlKind, searchHtDivision, isAllSearchConditionsSelected, data, table, loadData, sessionData?.user?.name]) + }, [searchConstructionSector, selectedShipType, searchAvlKind, searchHtDivision, data, table, loadData, sessionData?.user?.name]) // 초기 데이터 로드 (검색 조건이 모두 입력되었을 때만) React.useEffect(() => { @@ -437,7 +411,8 @@ export const StandardAvlTable = forwardRef<StandardAvlTableRef, StandardAvlTable setData([]) setPageCount(0) } - }, [isAllSearchConditionsSelected, pagination.pageSize, searchConstructionSector, searchShipType, searchAvlKind, searchHtDivision]) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isAllSearchConditionsSelected, pagination.pageSize, searchConstructionSector, selectedShipType, searchAvlKind, searchHtDivision]) @@ -547,18 +522,13 @@ export const StandardAvlTable = forwardRef<StandardAvlTableRef, StandardAvlTable {/* 선종 */} <div className="space-y-2"> <label className="text-sm font-medium">선종</label> - <Select value={searchShipType} onValueChange={setSearchShipType}> - <SelectTrigger> - <SelectValue /> - </SelectTrigger> - <SelectContent> - {currentShipTypeOptions.map((option) => ( - <SelectItem key={option.value} value={option.value}> - {option.label} - </SelectItem> - ))} - </SelectContent> - </Select> + <ShipTypeSelector + selectedShipType={selectedShipType} + onShipTypeSelect={setSelectedShipType} + placeholder="선종을 선택하세요" + disabled={!searchConstructionSector} + className="h-10" + /> </div> {/* AVL종류 */} @@ -640,10 +610,40 @@ export const StandardAvlTable = forwardRef<StandardAvlTableRef, StandardAvlTable isTemplate={true} // 표준 AVL 모드 // 검색 조건에서 선택한 값들을 초기값으로 전달 initialConstructionSector={searchConstructionSector} - initialShipType={searchShipType} + initialShipType={selectedShipType?.CD || ""} initialAvlKind={searchAvlKind} initialHtDivision={searchHtDivision} /> + + {/* 최종 확정 확인 다이얼로그 */} + <Dialog open={isConfirmDialogOpen} onOpenChange={setIsConfirmDialogOpen}> + <DialogContent> + <DialogHeader> + <DialogTitle>표준 AVL 최종 확정</DialogTitle> + <DialogDescription> + 현재 표준 AVL을 최종 확정하시겠습니까? + </DialogDescription> + </DialogHeader> + <div className="space-y-2 text-sm"> + <div>• 공사부문: {searchConstructionSector}</div> + <div>• 선종: {selectedShipType?.CD || ""}</div> + <div>• AVL종류: {searchAvlKind}</div> + <div>• H/T 구분: {searchHtDivision}</div> + <div>• 벤더 정보: {data.length}개 (전체 레코드)</div> + {/* <div className="text-amber-600 font-medium mt-4"> + ⚠️ 확정 후 내용 수정을 필요로 하는 경우 동일 건을 다시 최종확정해 revision 처리로 수정해야 합니다. + </div> */} + </div> + <DialogFooter> + <Button variant="outline" onClick={() => setIsConfirmDialogOpen(false)}> + 취소 + </Button> + <Button onClick={executeFinalizeStandardAvl}> + 최종 확정 + </Button> + </DialogFooter> + </DialogContent> + </Dialog> </div> ) }) |
