From 4fb273b7fc85352183113f1240fc33f7d6c98328 Mon Sep 17 00:00:00 2001 From: joonhoekim <26rote@gmail.com> Date: Tue, 1 Jul 2025 10:45:37 +0000 Subject: (김준회) 벤더 기본 정보 UI 및 서비스 - NONSAP 에서 데이터 페칭 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../evcp/(evcp)/vendors/[id]/info/basic/page.tsx | 39 ++ .../vendors/[id]/info/basic/vendor-basic-info.tsx | 735 +++++++++++++++++++++ app/[lng]/evcp/(evcp)/vendors/[id]/info/layout.tsx | 4 + db/schema/PLM/plmVendorSchema.ts | 337 ---------- lib/vendors/mdg-actions.ts | 93 +++ lib/vendors/mdg-service.ts | 598 +++++++++++++++++ 6 files changed, 1469 insertions(+), 337 deletions(-) create mode 100644 app/[lng]/evcp/(evcp)/vendors/[id]/info/basic/page.tsx create mode 100644 app/[lng]/evcp/(evcp)/vendors/[id]/info/basic/vendor-basic-info.tsx delete mode 100644 db/schema/PLM/plmVendorSchema.ts create mode 100644 lib/vendors/mdg-actions.ts create mode 100644 lib/vendors/mdg-service.ts diff --git a/app/[lng]/evcp/(evcp)/vendors/[id]/info/basic/page.tsx b/app/[lng]/evcp/(evcp)/vendors/[id]/info/basic/page.tsx new file mode 100644 index 00000000..6b058b37 --- /dev/null +++ b/app/[lng]/evcp/(evcp)/vendors/[id]/info/basic/page.tsx @@ -0,0 +1,39 @@ +import { vendorMdgService } from "@/lib/vendors/mdg-service" +import { VendorBasicInfo } from "./vendor-basic-info" + +interface VendorBasicPageProps { + params: { + lng: string + // 협력업체 ID: 여기서는 Oracle의 벤더 코드(VNDRCD)를 사용 + id: string + } +} + +export default async function VendorBasicPage(props: VendorBasicPageProps) { + const resolvedParams = await props.params + const vendorId = resolvedParams.id + + // Oracle에서 벤더 상세 정보 조회 (ID로 조회) + const vendorDetails = await vendorMdgService.getVendorDetailsByVendorId(vendorId) + + if (!vendorDetails) { + return ( +
+
+

+ 벤더 정보를 찾을 수 없습니다 +

+

+ 벤더 ID: {vendorId} +

+
+
+ ) + } + + return ( +
+ +
+ ) +} \ No newline at end of file diff --git a/app/[lng]/evcp/(evcp)/vendors/[id]/info/basic/vendor-basic-info.tsx b/app/[lng]/evcp/(evcp)/vendors/[id]/info/basic/vendor-basic-info.tsx new file mode 100644 index 00000000..16f75bcb --- /dev/null +++ b/app/[lng]/evcp/(evcp)/vendors/[id]/info/basic/vendor-basic-info.tsx @@ -0,0 +1,735 @@ +"use client" + +import { useState, useTransition, useMemo } from "react" +import { useParams } from "next/navigation" +import { toast } from "sonner" +import { Separator } from "@/components/ui/separator" +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" +import { Badge } from "@/components/ui/badge" +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { Textarea } from "@/components/ui/textarea" +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" +import { AddressDisplay } from "@/components/ui/text-utils" +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog" +import { + Phone, + Mail, + Calendar, + CheckCircle, + XCircle, + AlertCircle, + Edit, + Save, + X, + Building2, + Eye +} from "lucide-react" +import { updateMdgVendorBasicInfo } from "@/lib/vendors/mdg-actions" + +// 구매조직별 정보 타입 +interface PurchasingOrgInfo { + PUR_ORG_CD: string + PUR_ORD_CUR: string | null + SPLY_COND: string | null + DL_COND_1: string | null + DL_COND_2: string | null + GR_BSE_INVC_VR: string | null + ORD_CNFM_REQ_ORDR: string | null + CNFM_CTL_KEY: string | null + PUR_HOLD_ORDR: string | null + DEL_ORDR: string | null + AT_PUR_ORD_ORDR: string | null + SALE_CHRGR_NM: string | null + VNDR_TELNO: string | null + PUR_HOLD_DT: string | null + PUR_HOLD_CAUS: string | null +} + +interface VendorDetails { + VNDRCD: string + VNDRNM_1: string | null + VNDRNM_2: string | null + VNDRNM_ABRV_1: string | null + CO_VLM: string | null + BIZR_NO: string | null + CO_REG_NO: string | null + REPR_NM: string | null + REP_TEL_NO: string | null + REPR_RESNO: string | null + REPRESENTATIVE_EMAIL: string | null + BIZTP: string | null + BIZCON: string | null + NTN_CD: string | null + REG_DT: string | null + ADR_1: string | null + ADR_2: string | null + POSTAL_CODE: string | null + ADDR_DETAIL_1: string | null + PREVIOUS_VENDOR_CODE: string | null + PRTNR_GB: string | null + PURCHASING_ORGS: PurchasingOrgInfo[] + DEL_ORDR: string | null + PUR_HOLD_ORDR: string | null +} + +interface VendorBasicInfoProps { + vendorDetails: VendorDetails +} + +export function VendorBasicInfo({ vendorDetails }: VendorBasicInfoProps) { + const params = useParams() + const vendorId = params.id as string + const [isEditing, setIsEditing] = useState(false) + const [editData, setEditData] = useState(vendorDetails) + const [isPending, startTransition] = useTransition() + const [showConfirmDialog, setShowConfirmDialog] = useState(false) + const [selectedPurchasingOrg, setSelectedPurchasingOrg] = useState(() => { + // 구매조직이 1개면 자동 선택, 여러개면 첫 번째 선택, 없으면 'none' + if (vendorDetails.PURCHASING_ORGS.length === 1) { + return vendorDetails.PURCHASING_ORGS[0].PUR_ORG_CD + } else if (vendorDetails.PURCHASING_ORGS.length > 1) { + return vendorDetails.PURCHASING_ORGS[0].PUR_ORG_CD + } + return 'none' + }) + const [showAllOrgs, setShowAllOrgs] = useState(false) + + // 변경사항 감지 + const changes = useMemo(() => { + const changedFields: Array<{ label: string; before: string; after: string }> = [] + + const fieldLabels: Record = { + VNDRNM_1: "업체명", + VNDRNM_2: "영문명", + VNDRNM_ABRV_1: "업체약어", + BIZR_NO: "사업자번호", + CO_REG_NO: "법인등록번호", + CO_VLM: "기업규모", + REPR_NM: "대표자명", + REP_TEL_NO: "대표자 전화번호", + REPR_RESNO: "대표자 생년월일", + REPRESENTATIVE_EMAIL: "대표자 이메일", + BIZTP: "사업유형", + BIZCON: "산업유형", + NTN_CD: "국가코드", + ADR_1: "주소", + ADR_2: "영문주소", + POSTAL_CODE: "우편번호", + ADDR_DETAIL_1: "상세주소" + } + + Object.keys(fieldLabels).forEach(field => { + const originalValue = vendorDetails[field as keyof VendorDetails] as string || '' + const editedValue = editData[field as keyof VendorDetails] as string || '' + + if (originalValue !== editedValue) { + changedFields.push({ + label: fieldLabels[field], + before: originalValue || '(없음)', + after: editedValue || '(없음)' + }) + } + }) + + return changedFields + }, [vendorDetails, editData]) + + // 선택된 구매조직 정보 + const currentPurchasingOrg = vendorDetails.PURCHASING_ORGS.find( + org => org.PUR_ORG_CD === selectedPurchasingOrg + ) + + // 상태에 따른 뱃지 스타일 결정 + const getStatusBadge = (status: string | null) => { + if (!status || status === 'N') { + return 활성 + } + return 비활성 + } + + const handleEditStart = () => { + setIsEditing(true) + } + + const handleEditCancel = () => { + setIsEditing(false) + setEditData(vendorDetails) + } + + const handleEditSave = () => { + if (isPending) return + + // 변경사항이 없으면 바로 편집 모드 종료 + if (changes.length === 0) { + setIsEditing(false) + toast.info("변경된 내용이 없습니다.") + return + } + + // 변경사항이 있으면 확인 Dialog 표시 + setShowConfirmDialog(true) + } + + const handleConfirmSave = () => { + setShowConfirmDialog(false) + + startTransition(async () => { + try { + const result = await updateMdgVendorBasicInfo({ + vendorId, + updateData: { + VNDRNM_1: editData.VNDRNM_1, + VNDRNM_2: editData.VNDRNM_2, + VNDRNM_ABRV_1: editData.VNDRNM_ABRV_1, + BIZR_NO: editData.BIZR_NO, + CO_REG_NO: editData.CO_REG_NO, + CO_VLM: editData.CO_VLM, + REPR_NM: editData.REPR_NM, + REP_TEL_NO: editData.REP_TEL_NO, + REPR_RESNO: editData.REPR_RESNO, + REPRESENTATIVE_EMAIL: editData.REPRESENTATIVE_EMAIL, + BIZTP: editData.BIZTP, + BIZCON: editData.BIZCON, + NTN_CD: editData.NTN_CD, + ADR_1: editData.ADR_1, + ADR_2: editData.ADR_2, + POSTAL_CODE: editData.POSTAL_CODE, + ADDR_DETAIL_1: editData.ADDR_DETAIL_1, + } + }) + + if (result.success) { + toast.success(result.message || "벤더 정보가 성공적으로 업데이트되었습니다.") + setIsEditing(false) + // 필요한 경우 페이지 리로드 또는 데이터 갱신 + window.location.reload() + } else { + toast.error(result.error || "벤더 정보 업데이트에 실패했습니다.") + } + } catch (error) { + console.error('벤더 정보 업데이트 중 오류:', error) + toast.error("벤더 정보 업데이트 중 오류가 발생했습니다.") + } + }) + } + + const handleInputChange = (field: keyof VendorDetails, value: string) => { + setEditData(prev => ({ + ...prev, + [field]: value + })) + } + + const renderField = ( + label: string, + value: string | null, + field?: keyof VendorDetails, + isTextarea = false, + isMono = false + ) => { + if (isEditing && field) { + return ( +
+ + {isTextarea ? ( +