summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/[lng]/evcp/(evcp)/vendors/[id]/info/basic/page.tsx39
-rw-r--r--app/[lng]/evcp/(evcp)/vendors/[id]/info/basic/vendor-basic-info.tsx735
-rw-r--r--app/[lng]/evcp/(evcp)/vendors/[id]/info/layout.tsx4
-rw-r--r--db/schema/PLM/plmVendorSchema.ts337
-rw-r--r--lib/vendors/mdg-actions.ts93
-rw-r--r--lib/vendors/mdg-service.ts598
6 files changed, 1469 insertions, 337 deletions
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 (
+ <div className="space-y-6">
+ <div className="text-center py-12">
+ <h3 className="text-lg font-medium text-gray-900 mb-2">
+ 벤더 정보를 찾을 수 없습니다
+ </h3>
+ <p className="text-gray-500">
+ 벤더 ID: {vendorId}
+ </p>
+ </div>
+ </div>
+ )
+ }
+
+ return (
+ <div className="space-y-6">
+ <VendorBasicInfo vendorDetails={vendorDetails} />
+ </div>
+ )
+} \ 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<string>(() => {
+ // 구매조직이 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<string, string> = {
+ 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 <Badge variant="default" className="bg-green-100 text-green-800"><CheckCircle className="w-3 h-3 mr-1" />활성</Badge>
+ }
+ return <Badge variant="destructive"><XCircle className="w-3 h-3 mr-1" />비활성</Badge>
+ }
+
+ 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 (
+ <div>
+ <label className="text-sm font-medium text-muted-foreground">{label}</label>
+ {isTextarea ? (
+ <Textarea
+ value={editData[field] as string || ''}
+ onChange={(e) => handleInputChange(field, e.target.value)}
+ className="mt-1"
+ />
+ ) : (
+ <Input
+ value={editData[field] as string || ''}
+ onChange={(e) => handleInputChange(field, e.target.value)}
+ className={`mt-1 ${isMono ? 'font-mono' : ''}`}
+ />
+ )}
+ </div>
+ )
+ }
+
+ return (
+ <div>
+ <label className="text-sm font-medium text-muted-foreground">{label}</label>
+ <p className={`text-sm ${isMono ? 'font-mono' : ''} break-words ${isTextarea ? 'whitespace-pre-wrap' : ''}`}>
+ {value || '-'}
+ </p>
+ </div>
+ )
+ }
+
+ // 구매조직별 정보 필드 렌더링
+ const renderPurchasingOrgField = (
+ label: string,
+ value: string | null | undefined,
+ isBadge = false,
+ badgeType?: 'status' | 'confirm' | 'hold'
+ ) => {
+ if (isBadge) {
+ let badgeContent
+ switch (badgeType) {
+ case 'status':
+ badgeContent = value === 'X' ? (
+ <Badge variant="outline" className="text-xs bg-green-50 text-green-700">활성</Badge>
+ ) : (
+ <Badge variant="secondary" className="text-xs">비활성</Badge>
+ )
+ break
+ case 'confirm':
+ badgeContent = value === 'X' ? (
+ <Badge variant="outline" className="text-xs bg-blue-50 text-blue-700">요청</Badge>
+ ) : (
+ <Badge variant="secondary" className="text-xs">미요청</Badge>
+ )
+ break
+ case 'hold':
+ badgeContent = value ? (
+ <Badge variant="destructive" className="text-xs">
+ <AlertCircle className="w-3 h-3 mr-1" />정지
+ </Badge>
+ ) : (
+ <Badge variant="outline" className="text-xs bg-green-50 text-green-700">
+ <CheckCircle className="w-3 h-3 mr-1" />정상
+ </Badge>
+ )
+ break
+ default:
+ badgeContent = <Badge variant="outline">{value || '-'}</Badge>
+ }
+
+ return (
+ <div>
+ <label className="text-sm font-medium text-muted-foreground">{label}</label>
+ <p className="text-sm">{badgeContent}</p>
+ </div>
+ )
+ }
+
+ return (
+ <div>
+ <label className="text-sm font-medium text-muted-foreground">{label}</label>
+ <p className="text-sm break-words">{value || '-'}</p>
+ </div>
+ )
+ }
+
+ // 구매조직 정보 카드 컴포넌트
+ const PurchasingOrgCard = ({ org }: { org: PurchasingOrgInfo }) => (
+ <Card key={org.PUR_ORG_CD} className="border-l-4 border-l-blue-500">
+ <CardHeader className="pb-3">
+ <CardTitle className="text-lg flex items-center gap-2">
+ 구매조직: {org.PUR_ORG_CD}
+ </CardTitle>
+ </CardHeader>
+ <CardContent>
+ <div className="grid grid-cols-3 gap-4">
+ {renderPurchasingOrgField("오더통화", org.PUR_ORD_CUR)}
+ <div>
+ <label className="text-sm font-medium text-muted-foreground">내외자구분</label>
+ <p className="text-sm">
+ {editData.PRTNR_GB ? (
+ <Badge variant="outline" className="text-xs">
+ {editData.PRTNR_GB === '1' ? '사내' : editData.PRTNR_GB === '2' ? '사외' : editData.PRTNR_GB}
+ </Badge>
+ ) : '-'}
+ </p>
+ </div>
+ {renderPurchasingOrgField("인도조건", org.DL_COND_1)}
+ {renderPurchasingOrgField("GR송장검증", org.GR_BSE_INVC_VR, true, 'status')}
+ {renderPurchasingOrgField("P/O 확인요청", org.ORD_CNFM_REQ_ORDR, true, 'confirm')}
+ {renderPurchasingOrgField("확정제어", org.CNFM_CTL_KEY)}
+ {renderPurchasingOrgField("지급조건", org.SPLY_COND)}
+ {renderPurchasingOrgField("거래정지", org.PUR_HOLD_ORDR, true, 'hold')}
+ {renderPurchasingOrgField("삭제상태", org.DEL_ORDR)}
+ {renderPurchasingOrgField("영업담당자", org.SALE_CHRGR_NM)}
+ {renderPurchasingOrgField("전화번호", org.VNDR_TELNO)}
+ {renderPurchasingOrgField("보류일자", org.PUR_HOLD_DT)}
+ </div>
+ {org.PUR_HOLD_CAUS && (
+ <div className="mt-4">
+ {renderPurchasingOrgField("보류사유", org.PUR_HOLD_CAUS)}
+ </div>
+ )}
+ </CardContent>
+ </Card>
+ )
+
+ return (
+ <>
+ {/* 헤더 */}
+ <div className="flex items-center justify-between">
+ <div className="min-w-0 flex-1">
+ <h3 className="text-2xl font-bold tracking-tight break-words">
+ {editData.VNDRNM_1 || '업체명 없음'}
+ </h3>
+ <p className="text-muted-foreground">
+ 벤더 코드: {editData.VNDRCD}
+ </p>
+ </div>
+ <div className="flex items-center space-x-4">
+ {/* 상태 배지 */}
+ <div className="flex items-center space-x-2">
+ {getStatusBadge(editData.DEL_ORDR)}
+ </div>
+
+ {/* 액션 버튼들 */}
+ <div className="flex items-center space-x-2">
+ {isEditing ? (
+ <>
+ <Button onClick={handleEditSave} size="sm" disabled={showConfirmDialog}>
+ <Save className="w-4 h-4 mr-2" />
+ 저장
+ </Button>
+ <Button onClick={handleEditCancel} variant="outline" size="sm" disabled={showConfirmDialog || isPending}>
+ <X className="w-4 h-4 mr-2" />
+ 취소
+ </Button>
+ </>
+ ) : (
+ <>
+ <Button onClick={handleEditStart} variant="outline" size="sm">
+ <Edit className="w-4 h-4 mr-2" />
+ 수정
+ </Button>
+ </>
+ )}
+ </div>
+ </div>
+ </div>
+
+ <Separator />
+
+ <div className="grid gap-6 md:grid-cols-2">
+ {/* 기본 정보 */}
+ <Card>
+ <CardHeader>
+ <CardTitle className="flex items-center gap-2">
+ 기본 정보
+ </CardTitle>
+ <CardDescription>
+ 업체의 기본적인 정보입니다.
+ </CardDescription>
+ </CardHeader>
+ <CardContent className="space-y-4">
+ <div className="grid grid-cols-1 gap-4">
+ {renderField("업체명", editData.VNDRNM_1, "VNDRNM_1")}
+ {renderField("영문명", editData.VNDRNM_2, "VNDRNM_2")}
+ <div className="grid grid-cols-2 gap-4">
+ {renderField("업체약어", editData.VNDRNM_ABRV_1, "VNDRNM_ABRV_1")}
+ {renderField("기업규모", editData.CO_VLM, "CO_VLM")}
+ </div>
+ <div className="grid grid-cols-2 gap-4">
+ {renderField("사업자번호", editData.BIZR_NO, "BIZR_NO", false, true)}
+ {renderField("법인등록번호", editData.CO_REG_NO, "CO_REG_NO", false, true)}
+ </div>
+ </div>
+ </CardContent>
+ </Card>
+
+ {/* 대표자 정보 */}
+ <Card>
+ <CardHeader>
+ <CardTitle className="flex items-center gap-2">
+ 대표자 정보
+ </CardTitle>
+ </CardHeader>
+ <CardContent className="space-y-4">
+ <div className="grid grid-cols-1 gap-4">
+ {renderField("대표자명", editData.REPR_NM, "REPR_NM")}
+
+ <div>
+ <label className="text-sm font-medium text-muted-foreground">대표자 전화번호</label>
+ {isEditing ? (
+ <Input
+ value={editData.REP_TEL_NO || ''}
+ onChange={(e) => handleInputChange('REP_TEL_NO', e.target.value)}
+ className="mt-1 font-mono"
+ />
+ ) : (
+ <p className="text-sm flex items-center gap-1">
+ {editData.REP_TEL_NO ? (
+ <>
+ <Phone className="w-3 h-3" />
+ <span className="font-mono">{editData.REP_TEL_NO}</span>
+ </>
+ ) : '-'}
+ </p>
+ )}
+ </div>
+
+ <div>
+ <label className="text-sm font-medium text-muted-foreground">대표자 생년월일</label>
+ {isEditing ? (
+ <Input
+ value={editData.REPR_RESNO || ''}
+ onChange={(e) => handleInputChange('REPR_RESNO', e.target.value)}
+ className="mt-1 font-mono"
+ />
+ ) : (
+ <p className="text-sm flex items-center gap-1">
+ {editData.REPR_RESNO ? (
+ <>
+ <Calendar className="w-3 h-3" />
+ <span className="font-mono">{editData.REPR_RESNO}</span>
+ </>
+ ) : '-'}
+ </p>
+ )}
+ </div>
+
+ <div>
+ <label className="text-sm font-medium text-muted-foreground">대표자 이메일</label>
+ {isEditing ? (
+ <Input
+ type="email"
+ value={editData.REPRESENTATIVE_EMAIL || ''}
+ onChange={(e) => handleInputChange('REPRESENTATIVE_EMAIL', e.target.value)}
+ className="mt-1"
+ />
+ ) : (
+ <p className="text-sm flex items-center gap-1">
+ {editData.REPRESENTATIVE_EMAIL ? (
+ <>
+ <Mail className="w-3 h-3 flex-shrink-0" />
+ <span className="break-all">{editData.REPRESENTATIVE_EMAIL}</span>
+ </>
+ ) : '-'}
+ </p>
+ )}
+ </div>
+ </div>
+ </CardContent>
+ </Card>
+
+ {/* 사업 정보 */}
+ <Card>
+ <CardHeader>
+ <CardTitle className="flex items-center gap-2">
+ 사업 정보
+ </CardTitle>
+ </CardHeader>
+ <CardContent className="space-y-4">
+ <div className="space-y-4">
+ {renderField("사업유형", editData.BIZTP, "BIZTP", true)}
+ {renderField("산업유형", editData.BIZCON, "BIZCON", true)}
+ <div className="grid grid-cols-2 gap-4">
+ {renderField("국가코드", editData.NTN_CD, "NTN_CD")}
+ {renderField("등록일자", editData.REG_DT, "REG_DT", false, true)}
+ </div>
+ </div>
+ </CardContent>
+ </Card>
+
+ {/* 주소 정보 */}
+ <Card>
+ <CardHeader>
+ <CardTitle className="flex items-center gap-2">
+ 주소 정보
+ </CardTitle>
+ </CardHeader>
+ <CardContent className="space-y-4">
+ <div className="space-y-4">
+ {isEditing ? (
+ <div className="space-y-4">
+ {renderField("주소", editData.ADR_1, "ADR_1")}
+ {renderField("영문주소", editData.ADR_2, "ADR_2")}
+ {renderField("우편번호", editData.POSTAL_CODE, "POSTAL_CODE")}
+ {renderField("상세주소", editData.ADDR_DETAIL_1, "ADDR_DETAIL_1")}
+ </div>
+ ) : (
+ <div>
+ <label className="text-sm font-medium text-muted-foreground mb-2 block">주소</label>
+ <AddressDisplay
+ address={editData.ADR_1}
+ addressEng={editData.ADR_2}
+ postalCode={editData.POSTAL_CODE}
+ addressDetail={editData.ADDR_DETAIL_1}
+ />
+ </div>
+ )}
+ </div>
+ </CardContent>
+ </Card>
+
+ {/* 구매조직 정보 */}
+ <Card className="md:col-span-2">
+ <CardHeader>
+ <CardTitle className="flex items-center gap-2">
+ 구매조직 정보
+ {vendorDetails.PURCHASING_ORGS.length > 0 && (
+ <Badge variant="secondary" className="ml-2">
+ {vendorDetails.PURCHASING_ORGS.length}개 조직
+ </Badge>
+ )}
+ </CardTitle>
+ <CardDescription>
+ 구매조직에 따른 상세 정보입니다.
+ </CardDescription>
+ </CardHeader>
+ <CardContent className="space-y-4">
+ {vendorDetails.PURCHASING_ORGS.length === 0 ? (
+ <div className="text-center py-8 text-muted-foreground">
+ <Building2 className="w-12 h-12 mx-auto mb-4 opacity-50" />
+ <p>구매조직 정보가 없습니다.</p>
+ </div>
+ ) : vendorDetails.PURCHASING_ORGS.length === 1 ? (
+ // 구매조직이 1개인 경우
+ <div className="grid grid-cols-3 gap-4">
+ {renderPurchasingOrgField("구매조직", currentPurchasingOrg?.PUR_ORG_CD)}
+ {renderPurchasingOrgField("오더통화", currentPurchasingOrg?.PUR_ORD_CUR)}
+ <div>
+ <label className="text-sm font-medium text-muted-foreground">내외자구분</label>
+ <p className="text-sm">
+ {editData.PRTNR_GB ? (
+ <Badge variant="outline" className="text-xs">
+ {editData.PRTNR_GB === '1' ? '사내' : editData.PRTNR_GB === '2' ? '사외' : editData.PRTNR_GB}
+ </Badge>
+ ) : '-'}
+ </p>
+ </div>
+ {renderPurchasingOrgField("인도조건", currentPurchasingOrg?.DL_COND_1)}
+ {renderPurchasingOrgField("GR송장검증", currentPurchasingOrg?.GR_BSE_INVC_VR, true, 'status')}
+ {renderPurchasingOrgField("P/O 확인요청", currentPurchasingOrg?.ORD_CNFM_REQ_ORDR, true, 'confirm')}
+ {renderPurchasingOrgField("확정제어", currentPurchasingOrg?.CNFM_CTL_KEY)}
+ {renderPurchasingOrgField("지급조건", currentPurchasingOrg?.SPLY_COND)}
+ {renderPurchasingOrgField("거래정지", currentPurchasingOrg?.PUR_HOLD_ORDR, true, 'hold')}
+ {renderPurchasingOrgField("이전업체코드", editData.PREVIOUS_VENDOR_CODE)}
+ </div>
+ ) : (
+ // 구매조직이 여러개인 경우
+ <div className="space-y-4">
+ <div className="flex items-center gap-4">
+ <div className="flex-1">
+ <label className="text-sm font-medium text-muted-foreground">구매조직 선택</label>
+ <Select value={selectedPurchasingOrg} onValueChange={setSelectedPurchasingOrg}>
+ <SelectTrigger className="mt-1">
+ <SelectValue placeholder="구매조직을 선택하세요" />
+ </SelectTrigger>
+ <SelectContent>
+ {vendorDetails.PURCHASING_ORGS.map((org) => (
+ <SelectItem key={org.PUR_ORG_CD} value={org.PUR_ORG_CD}>
+ {org.PUR_ORG_CD} - {org.SALE_CHRGR_NM || '담당자 미지정'}
+ </SelectItem>
+ ))}
+ </SelectContent>
+ </Select>
+ </div>
+ <div className="pt-6">
+ <Button
+ variant={showAllOrgs ? "default" : "outline"}
+ onClick={() => setShowAllOrgs(!showAllOrgs)}
+ size="sm"
+ >
+ <Eye className="w-4 h-4 mr-2" />
+ {showAllOrgs ? '선택 보기' : '전체 보기'}
+ </Button>
+ </div>
+ </div>
+
+ {showAllOrgs ? (
+ // 전체 구매조직 정보 표시
+ <div className="space-y-4">
+ {vendorDetails.PURCHASING_ORGS.map((org) => (
+ <PurchasingOrgCard key={org.PUR_ORG_CD} org={org} />
+ ))}
+ </div>
+ ) : (
+ // 선택된 구매조직 정보만 표시
+ currentPurchasingOrg && (
+ <div className="grid grid-cols-3 gap-4">
+ {renderPurchasingOrgField("구매조직", currentPurchasingOrg.PUR_ORG_CD)}
+ {renderPurchasingOrgField("오더통화", currentPurchasingOrg.PUR_ORD_CUR)}
+ <div>
+ <label className="text-sm font-medium text-muted-foreground">내외자구분</label>
+ <p className="text-sm">
+ {editData.PRTNR_GB ? (
+ <Badge variant="outline" className="text-xs">
+ {editData.PRTNR_GB === '1' ? '사내' : editData.PRTNR_GB === '2' ? '사외' : editData.PRTNR_GB}
+ </Badge>
+ ) : '-'}
+ </p>
+ </div>
+ {renderPurchasingOrgField("인도조건", currentPurchasingOrg.DL_COND_1)}
+ {renderPurchasingOrgField("GR송장검증", currentPurchasingOrg.GR_BSE_INVC_VR, true, 'status')}
+ {renderPurchasingOrgField("P/O 확인요청", currentPurchasingOrg.ORD_CNFM_REQ_ORDR, true, 'confirm')}
+ {renderPurchasingOrgField("확정제어", currentPurchasingOrg.CNFM_CTL_KEY)}
+ {renderPurchasingOrgField("지급조건", currentPurchasingOrg.SPLY_COND)}
+ {renderPurchasingOrgField("거래정지", currentPurchasingOrg.PUR_HOLD_ORDR, true, 'hold')}
+ {renderPurchasingOrgField("이전업체코드", editData.PREVIOUS_VENDOR_CODE)}
+ </div>
+ )
+ )}
+ </div>
+ )}
+ </CardContent>
+ </Card>
+ </div>
+
+ {/* 변경사항 확인 Dialog */}
+ <Dialog open={showConfirmDialog} onOpenChange={setShowConfirmDialog}>
+ <DialogContent className="sm:max-w-[600px]">
+ <DialogHeader>
+ <DialogTitle className="flex items-center gap-2">
+ <AlertCircle className="w-5 h-5 text-amber-500" />
+ 변경사항 확인
+ </DialogTitle>
+ <DialogDescription>
+ 다음 정보가 변경됩니다. 저장하시겠습니까?
+ </DialogDescription>
+ </DialogHeader>
+
+ <div className="max-h-[400px] overflow-y-auto">
+ <div className="space-y-4">
+ {changes.map((change, index) => (
+ <div key={index} className="border rounded-lg p-4 space-y-2">
+ <div className="font-medium text-sm text-muted-foreground">
+ {change.label}
+ </div>
+ <div className="grid grid-cols-1 gap-2">
+ <div className="flex items-start gap-2">
+ <span className="text-xs bg-red-100 text-red-700 px-2 py-1 rounded font-mono">이전</span>
+ <span className="text-sm break-words flex-1 line-through text-muted-foreground">
+ {change.before}
+ </span>
+ </div>
+ <div className="flex items-start gap-2">
+ <span className="text-xs bg-green-100 text-green-700 px-2 py-1 rounded font-mono">변경</span>
+ <span className="text-sm break-words flex-1 font-medium">
+ {change.after}
+ </span>
+ </div>
+ </div>
+ </div>
+ ))}
+ </div>
+ </div>
+
+ <DialogFooter>
+ <Button
+ variant="outline"
+ onClick={() => setShowConfirmDialog(false)}
+ disabled={isPending}
+ >
+ 취소
+ </Button>
+ <Button
+ onClick={handleConfirmSave}
+ disabled={isPending}
+ className="bg-blue-600 hover:bg-blue-700"
+ >
+ {isPending ? "저장 중..." : `${changes.length}개 항목 저장`}
+ </Button>
+ </DialogFooter>
+ </DialogContent>
+ </Dialog>
+ </>
+ )
+} \ No newline at end of file
diff --git a/app/[lng]/evcp/(evcp)/vendors/[id]/info/layout.tsx b/app/[lng]/evcp/(evcp)/vendors/[id]/info/layout.tsx
index 7e2cd4f6..7826a7c0 100644
--- a/app/[lng]/evcp/(evcp)/vendors/[id]/info/layout.tsx
+++ b/app/[lng]/evcp/(evcp)/vendors/[id]/info/layout.tsx
@@ -35,6 +35,10 @@ export default async function SettingsLayout({
href: `/${lng}/evcp/vendors/${id}/info`,
},
{
+ title: "기본정보",
+ href: `/${lng}/evcp/vendors/${id}/info/basic`,
+ },
+ {
title: "공급품목(패키지)",
href: `/${lng}/evcp/vendors/${id}/info/items`,
},
diff --git a/db/schema/PLM/plmVendorSchema.ts b/db/schema/PLM/plmVendorSchema.ts
deleted file mode 100644
index 7338521b..00000000
--- a/db/schema/PLM/plmVendorSchema.ts
+++ /dev/null
@@ -1,337 +0,0 @@
-// db/schema/vendors.ts
-import { pgTable, serial, varchar, text, timestamp, boolean, integer ,pgView} from "drizzle-orm/pg-core";
-import { sql, eq, relations } from "drizzle-orm";
-
-// ------------------------------------------------------------------------------------------------
-
-// ------- [시작] MDG 인터페이스 목적 테이블 추가 (이미 인터페이스한 레거시 DB에서 직접 가져옴) -------------
-
-export const vendorMdgGenerals = pgTable("vendor_mdg_generals", {
- id: serial("id").primaryKey(),
- vendorCode: varchar("vendor_code", { length: 100 }),
- accountGroup: varchar("account_group", { length: 4 }), // ACNT_GRP - 계정그룹
- accountGroupType: varchar("account_group_type", { length: 2 }), // ACNT_GRP_TP - 계정그룹종류
- customerCode: varchar("customer_code", { length: 10 }), // CSTM_CD - 고객코드
- postingHoldIndicator: varchar("posting_hold_indicator", { length: 1 }), // PST_HOLD_ORDR - 전기보류지시자
- purchaseHoldIndicator: varchar("purchase_hold_indicator", { length: 1 }), // PUR_HOLD_ORDR - 구매보류지시자
- holdReason: varchar("hold_reason", { length: 200 }), // HOLD_CAUS - 보류사유
- deleteIndicator: varchar("delete_indicator", { length: 1 }), // DEL_ORDR - 삭제지시자
- companyId: varchar("company_id", { length: 6 }), // CO_ID - 법인ID
- businessType: varchar("business_type", { length: 90 }), // BIZTP - 사업유형
- industryType: varchar("industry_type", { length: 90 }), // BIZCON - 산업유형
- registrationDate: varchar("registration_date", { length: 8 }), // REG_DT - 등록일자
- registrationTime: varchar("registration_time", { length: 6 }), // REG_DTM - 등록시간
- registrarId: varchar("registrar_id", { length: 13 }), // REGR_ID - 등록자
- approvalDate: varchar("approval_date", { length: 8 }), // AGR_DT - 승인일자
- approvalTime: varchar("approval_time", { length: 6 }), // AGR_TM - 승인시간
- approverId: varchar("approver_id", { length: 13 }), // AGR_R_ID - 승인자ID
- changeDate: varchar("change_date", { length: 8 }), // CHG_DT - 변경일자
- changeTime: varchar("change_time", { length: 6 }), // CHG_TM - 변경시간
- changerId: varchar("changer_id", { length: 13 }), // CHGR_ID - 변경자ID
- nationCode: varchar("nation_code", { length: 3 }), // NTN_CD - 국가코드
- representativeTelNumber: varchar("representative_tel_number", { length: 30 }), // REP_TEL_NO - 대표전화번호
- representativeFaxNumber: varchar("representative_fax_number", { length: 31 }), // REP_FAX_NO - 대표FAX번호
- businessRegistrationNumber: varchar("business_registration_number", { length: 10 }), // BIZR_NO - 사업자번호
- corporateRegistrationNumberOracle: varchar("corporate_registration_number_oracle", { length: 18 }), // CO_REG_NO - 법인등록번호
- taxCode4: varchar("tax_code_4", { length: 54 }), // TX_CD_4 - 세금번호4
- companyEstablishmentDate: varchar("company_establishment_date", { length: 8 }), // CO_INST_DT - 설립일자
- vendorType: varchar("vendor_type", { length: 2 }), // VNDR_TP - 구매처유형
- globalTopCode: varchar("global_top_code", { length: 11 }), // GBL_TOP_CD - GLOBALTOP코드
- globalTopName: varchar("global_top_name", { length: 120 }), // GBL_TOP_NM - GLOBALTOP명
- domesticTopCode: varchar("domestic_top_code", { length: 11 }), // DMST_TOP_CD - 국내TOP코드
- domesticTopName: varchar("domestic_top_name", { length: 120 }), // DMST_TOP_NM - 국내TOP명
- businessUnitCode: varchar("business_unit_code", { length: 11 }), // BIZ_UOM_CD - 사업단위코드
- businessUnitName: varchar("business_unit_name", { length: 120 }), // BIZ_UOM_NM - 사업단위명
- dunsNumber: varchar("duns_number", { length: 11 }), // DNS_NO - DUNS번호
- interfaceDate: varchar("interface_date", { length: 8 }), // IF_DT - 인터페이스일자
- interfaceTime: varchar("interface_time", { length: 6 }), // IF_TM - 인터페이스시간
- interfaceStatus: varchar("interface_status", { length: 1 }), // IF_STAT - 인터페이스상태
- interfaceMessage: varchar("interface_message", { length: 100 }), // IF_MSG - 인터페이스메시지
- title: varchar("title", { length: 45 }), // TTL - 타이틀
- vatRegistrationNumber: varchar("vat_registration_number", { length: 20 }), // VAT_REG_NO - 부가세등록번호
- giroVendorIndicator: varchar("giro_vendor_indicator", { length: 1 }), // GIRO_VNDR_ORDR - 지로VENDOR지시자
- vendorName1: varchar("vendor_name_1", { length: 120 }), // VNDRNM_1 - Vendor명1
- vendorName2: varchar("vendor_name_2", { length: 120 }), // VNDRNM_2 - VENDOR명2
- vendorName3: varchar("vendor_name_3", { length: 120 }), // VNDRNM_3 - VENDOR명3
- vendorName4: varchar("vendor_name_4", { length: 120 }), // VNDRNM_4 - VENDOR명4
- vendorNameAbbreviation1: varchar("vendor_name_abbreviation_1", { length: 60 }), // VNDRNM_ABRV_1 - VENDOR명약어1
- vendorNameAbbreviation2: varchar("vendor_name_abbreviation_2", { length: 60 }), // VNDRNM_ABRV_2 - VENDOR명약어2
- potentialVendorCode: varchar("potential_vendor_code", { length: 10 }), // PTNT_VNDRCD - 잠재VENDOR코드
- address1: varchar("address_1", { length: 120 }), // ADR_1 - 주소1
- address2: varchar("address_2", { length: 512 }), // ADR_2 - 주소2
- qualityManagerName: varchar("quality_manager_name", { length: 60 }), // QLT_CHRGR_NM - 품질담당자명
- qualityManagerTelNumber: varchar("quality_manager_tel_number", { length: 30 }), // QLT_CHRGR_TELNO - 품질담당자전화번호
- qualityManagerEmail: varchar("quality_manager_email", { length: 241 }), // QLT_CHRGR_EMAIL - 품질담당자이메일
- subWorkplaceSequence: varchar("sub_workplace_sequence", { length: 16 }), // SB_WKA_SEQ - SUB작업장순서
- overlapCauseCode: varchar("overlap_cause_code", { length: 2 }), // OVLAP_CAUS_CD - 중복사유코드
- documentType: varchar("document_type", { length: 3 }), // DOC_TP - 문서유형
- documentNumber: varchar("document_number", { length: 25 }), // DOC_NO - 문서번호
- partialDocument: varchar("partial_document", { length: 3 }), // PTN_DOC - 부분문서
- documentVersion: varchar("document_version", { length: 2 }), // DOC_VER - 문서버전
- inboundFlag: varchar("inbound_flag", { length: 1 }), // INB_FLAG - 인바운드플래그
- deleteHoldIndicator: varchar("delete_hold_indicator", { length: 1 }), // DEL_HOLD_ORDR - 삭제보류지시자
- purchaseHoldDate: varchar("purchase_hold_date", { length: 8 }), // PUR_HOLD_DT - 구매보류일자
- postBox: varchar("post_box", { length: 30 }), // POBX - 사서함
- internationalLocationCheckNumber: integer("international_location_check_number"), // INTL_LCTN_CHK_NUM - 국제LOCATION점검숫자
- withholdingTaxGenderKey: varchar("withholding_tax_gender_key", { length: 1 }), // SRCETX_RP_SEX_KEY - 원천세의무자성별키
- vendorContractManager1: varchar("vendor_contract_manager_1", { length: 105 }), // VNDR_CNRT_CHRGR_1 - VENDOR계약담당자1
- vendorContractManager2: varchar("vendor_contract_manager_2", { length: 105 }), // VNDR_CNRT_CHRGR_2 - VENDOR계약담당자2
- representativeResidentNumber: varchar("representative_resident_number", { length: 13 }), // REPR_RESNO - 대표생년월일
- companyVolume: varchar("company_volume", { length: 1 }), // CO_VLM - 기업규모
- })
-
- // 벤더 업무그룹 테이블 (CMCTB_VENDOR_GRP 대응)
- export const vendorBusinessGroups = pgTable("vendor_business_groups", {
- id: serial("id").primaryKey(), // postgres 인공키
- vendorCode: varchar("vendor_code", { length: 10 }).notNull().references(() => vendorMdgGenerals.vendorCode), // VNDRCD - 벤더코드 (키) 이고, SAP에서는 VARCHAR10 이다.
- businessGroupCode: varchar("business_group_code", { length: 3 }).notNull(), // BIZ_GRP_CD - 업무그룹코드
- createdDate: varchar("created_date", { length: 8 }), // CRTE_DT - 생성일자
- createdTime: varchar("created_time", { length: 6 }), // CRTE_TM - 생성시간
- creatorId: varchar("creator_id", { length: 13 }), // CRTER_ID - 생성자ID
- createdAt: timestamp("created_at").defaultNow().notNull(),
- updatedAt: timestamp("updated_at").defaultNow().notNull(),
- });
-
- // 사내협력사 벤더 테이블 (CMCTB_VENDOR_INCO 대응)
- export const vendorInternalPartners = pgTable("vendor_internal_partners", {
- id: serial("id").primaryKey(),
- vendorCode: varchar("vendor_code", { length: 10 }).notNull().references(() => vendorMdgGenerals.vendorCode), // VNDRCD - 벤더코드 (키) 이고, SAP에서는 VARCHAR10 이다.
- vendorName: varchar("vendor_name", { length: 120 }), // VNDRNM - VENDOR코명
- representativeName: varchar("representative_name", { length: 30 }), // REPR_NM - 대표자명
- partnerType: varchar("partner_type", { length: 1 }), // PRTNR_GB - 협력사구분
- internalPartnerCode: varchar("internal_partner_code", { length: 3 }), // INCO_PRTNR_CD - 사내협력사코드
- internalPartnerWorkplace1: varchar("internal_partner_workplace_1", { length: 1 }), // INCO_PRTNR_WKA_1 - 사내협력사작업장1
- internalPartnerWorkplace2: varchar("internal_partner_workplace_2", { length: 1 }), // INCO_PRTNR_WKA_2 - 사내협력사작업장2
- internalPartnerWorkplace3: varchar("internal_partner_workplace_3", { length: 1 }), // INCO_PRTNR_WKA_3 - 사내협력사작업장3
- jobTypeCode: varchar("job_type_code", { length: 2 }), // JBTYPE_CD - 직종코드
- jobTypeCode2: varchar("job_type_code_2", { length: 2 }), // JBTYPE_CD_2 - 직종코드2
- individualCorporateType: varchar("individual_corporate_type", { length: 2 }), // INDV_CO_GB - 개인법인구분
- internalFoundationYn: varchar("internal_foundation_yn", { length: 1 }), // INCO_FOND_YN - 사내창립유무
- dockNumber: varchar("dock_number", { length: 25 }), // DOCK_NO - 도크번호
- companyInputDate: varchar("company_input_date", { length: 8 }), // OCMP_INP_DT - 당사투입일자
- internalWithdrawalDate: varchar("internal_withdrawal_date", { length: 8 }), // INCO_DUSE_DT - 사내철수일자
- industrialInsurancePremiumRate: integer("industrial_insurance_premium_rate"), // INDST_INS_PMRAT - 산재보험요율
- contractPerformanceGuarantee: integer("contract_performance_guarantee"), // CNRT_PFRM_GRAMT - 계약이행보증금
- wageRate: integer("wage_rate"), // WGE_RAT - 임금율
- correspondingDepartmentCode1: varchar("corresponding_department_code_1", { length: 30 }), // CRSPD_DEPTCD_1 - 해당부서코드1
- correspondingDepartmentCode2: varchar("corresponding_department_code_2", { length: 30 }), // CRSPD_DEPTCD_2 - 해당부서코드2
- correspondingTeamBelonging: varchar("corresponding_team_belonging", { length: 100 }), // CRSPD_TEAM_BLNG - 해당팀소속
- internalPartnerItem1: varchar("internal_partner_item_1", { length: 120 }), // INCO_PRTNR_ITM_1 - 사내협력사종목1
- internalPartnerItem2: varchar("internal_partner_item_2", { length: 120 }), // INCO_PRTNR_ITM_2 - 사내협력사종목2
- officeLocation: varchar("office_location", { length: 240 }), // OFC_LOC - 사무실위치
- representativeCompanyCareer: varchar("representative_company_career", { length: 300 }), // REP_OCMP_CARR - 대표당사경력
- internalWithdrawalReason: varchar("internal_withdrawal_reason", { length: 600 }), // INCO_DUSE_CAUS - 사내철수사유
- telephoneNumber: varchar("telephone_number", { length: 30 }), // TEL_NO - 전화번호
- address1: varchar("address_1", { length: 200 }), // ADR1 - 주소
- address2: varchar("address_2", { length: 200 }), // ADR2 - 상세주소
- oldVendorCode: varchar("old_vendor_code", { length: 10 }), // OLD_VNDRCD - 이전 VENDOR코드
- treeNumber: varchar("tree_number", { length: 1 }), // TREE_NUM - 하위 VENDOR 갯수
- createdDate: varchar("created_date", { length: 8 }), // CRTE_DT - 생성일자
- createdTime: varchar("created_time", { length: 6 }), // CRTE_TM - 생성시간
- createdUserId: varchar("created_user_id", { length: 13 }), // CRTE_USR_ID - 생성사용자ID
- changeDate: varchar("change_date", { length: 8 }), // CHG_DT - 수정일자
- changeTime: varchar("change_time", { length: 6 }), // CHG_TM - 수정시간
- changeUserId: varchar("change_user_id", { length: 13 }), // CHG_USR_ID - 수정사용자ID
- upperJobType: varchar("upper_job_type", { length: 2 }), // UPR_JBTYPE - 직종단가
- supplierBusinessPlaceCode: varchar("supplier_business_place_code", { length: 4 }), // ZBYBP - 공급받는자 종사업장 식별코드
- remark: varchar("remark", { length: 4000 }), // RMK - 비고
- withdrawalPlanYn: varchar("withdrawal_plan_yn", { length: 1 }), // WDL_PLN_YN - 철수예정유무
- wageDelayOccurrence: varchar("wage_delay_occurrence", { length: 8 }), // WGE_DELY_DVL - 임금체불발생
- escrowYn: varchar("escrow_yn", { length: 1 }), // ESCROW_YN - 에스크로가입유무
- createdAt: timestamp("created_at").defaultNow().notNull(),
- updatedAt: timestamp("updated_at").defaultNow().notNull(),
- });
-
- // 벤더 구매조직 테이블 (CMCTB_VENDOR_PORG 대응)
- export const vendorPurchaseOrganizations = pgTable("vendor_purchase_organizations", {
- id: serial("id").primaryKey(),
- vendorCode: varchar("vendor_code", { length: 10 }).notNull().references(() => vendorMdgGenerals.vendorCode), // VNDRCD - 벤더코드 (키) 이고, SAP에서는 VARCHAR10 이다.
- purchaseOrgCode: varchar("purchase_org_code", { length: 4 }).notNull(), // PUR_ORG_CD - 구매조직
- purchaseOrderCurrency: varchar("purchase_order_currency", { length: 5 }), // PUR_ORD_CUR - 구매오더통화
- paymentTerms: varchar("payment_terms", { length: 4 }), // SPLY_COND - 지급조건
- deliveryTerms1: varchar("delivery_terms_1", { length: 3 }), // DL_COND_1 - 인도조건1
- deliveryTerms2: varchar("delivery_terms_2", { length: 90 }), // DL_COND_2 - 인도조건2
- calculationSchemaGroup: varchar("calculation_schema_group", { length: 2 }), // CALC_SHM_GRP - 계산스키마그룹
- grBasedInvoiceVerification: varchar("gr_based_invoice_verification", { length: 1 }), // GR_BSE_INVC_VR - GR기준송장검증
- automaticPurchaseOrderIndicator: varchar("automatic_purchase_order_indicator", { length: 1 }), // AT_PUR_ORD_ORDR - 자동구매오더지시자
- purchaseHoldIndicator: varchar("purchase_hold_indicator", { length: 1 }), // PUR_HOLD_ORDR - 구매보류지시자
- deleteIndicator: varchar("delete_indicator", { length: 1 }), // DEL_ORDR - 삭제지시자
- interfaceDate: varchar("interface_date", { length: 8 }), // IF_DT - 인터페이스일자
- interfaceTime: varchar("interface_time", { length: 6 }), // IF_TM - 인터페이스시간
- interfaceStatus: varchar("interface_status", { length: 1 }), // IF_STAT - 인터페이스상태
- interfaceMessage: varchar("interface_message", { length: 100 }), // IF_MSG - 인터페이스메시지
- orderConfirmationRequestIndicator: varchar("order_confirmation_request_indicator", { length: 1 }), // ORD_CNFM_REQ_ORDR - 오더확인요청지시자
- salesManagerName: varchar("sales_manager_name", { length: 120 }), // SALE_CHRGR_NM - 영업담당자명
- vendorTelephoneNumber: varchar("vendor_telephone_number", { length: 30 }), // VNDR_TELNO - VENDOR전화번호
- confirmationControlKey: varchar("confirmation_control_key", { length: 4 }), // CNFM_CTL_KEY - 확정제어키
- purchaseHoldDate: varchar("purchase_hold_date", { length: 8 }), // PUR_HOLD_DT - 구매보류일자
- purchaseHoldReason: varchar("purchase_hold_reason", { length: 120 }), // PUR_HOLD_CAUS - 구매보류사유
- createdAt: timestamp("created_at").defaultNow().notNull(),
- updatedAt: timestamp("updated_at").defaultNow().notNull(),
- });
-
- // 벤더 대표자 이메일 테이블 (CMCTB_VENDOR_REPREMAIL 대응)
- export const vendorRepresentativeEmails = pgTable("vendor_representative_emails", {
- id: serial("id").primaryKey(),
- vendorCode: varchar("vendor_code", { length: 10 }).notNull().references(() => vendorMdgGenerals.vendorCode), // VNDRCD - 벤더코드 (키) 이고, SAP에서는 VARCHAR10 이다.
- addressNumber: varchar("address_number", { length: 10 }), // ADR_NO - 주소번호
- representativeSequence: varchar("representative_sequence", { length: 3 }).notNull(), // REPR_SER - 대표자순번
- validStartDate: varchar("valid_start_date", { length: 8 }).notNull(), // VLD_ST_DT - 유효시작일자
- emailAddress: varchar("email_address", { length: 241 }), // EMAIL_ADR - 이메일주소
- interfaceDate: varchar("interface_date", { length: 8 }), // IF_DT - 인터페이스일자
- interfaceTime: varchar("interface_time", { length: 6 }), // IF_TM - 인터페이스시간
- interfaceStatus: varchar("interface_status", { length: 1 }), // IF_STAT - 인터페이스상태
- interfaceMessage: varchar("interface_message", { length: 100 }), // IF_MSG - 인터페이스메시지
- createdAt: timestamp("created_at").defaultNow().notNull(),
- updatedAt: timestamp("updated_at").defaultNow().notNull(),
- });
-
- // 벤더 대표자 팩스 테이블 (CMCTB_VENDOR_REPRFAX 대응)
- export const vendorRepresentativeFaxes = pgTable("vendor_representative_faxes", {
- id: serial("id").primaryKey(),
- vendorCode: varchar("vendor_code", { length: 10 }).notNull().references(() => vendorMdgGenerals.vendorCode), // VNDRCD - 벤더코드 (키) 이고, SAP에서는 VARCHAR10 이다.
- addressNumber: varchar("address_number", { length: 10 }), // ADR_NO - 주소번호
- representativeSequence: varchar("representative_sequence", { length: 3 }).notNull(), // REPR_SER - 대표자순번
- validStartDate: varchar("valid_start_date", { length: 8 }).notNull(), // VLD_ST_DT - 유효시작일자
- nationCode: varchar("nation_code", { length: 3 }), // NTN_CD - 국가코드
- faxNumber: varchar("fax_number", { length: 30 }), // FAXNO - 팩스번호
- faxExtensionNumber: varchar("fax_extension_number", { length: 10 }), // FAX_ETS_NO - 팩스내선번호
- interfaceDate: varchar("interface_date", { length: 8 }), // IF_DT - 인터페이스일자
- interfaceTime: varchar("interface_time", { length: 6 }), // IF_TM - 인터페이스시간
- interfaceStatus: varchar("interface_status", { length: 1 }), // IF_STAT - 인터페이스상태
- interfaceMessage: varchar("interface_message", { length: 100 }), // IF_MSG - 인터페이스메시지
- createdAt: timestamp("created_at").defaultNow().notNull(),
- updatedAt: timestamp("updated_at").defaultNow().notNull(),
- });
-
- // 벤더 대표자 전화번호 테이블 (CMCTB_VENDOR_REPRTEL 대응)
- export const vendorRepresentativeTelephones = pgTable("vendor_representative_telephones", {
- id: serial("id").primaryKey(),
- vendorCode: varchar("vendor_code", { length: 10 }).notNull().references(() => vendorMdgGenerals.vendorCode), // VNDRCD - 벤더코드 (키) 이고, SAP에서는 VARCHAR10 이다.
- addressNumber: varchar("address_number", { length: 10 }), // ADR_NO - 주소번호
- representativeSequence: varchar("representative_sequence", { length: 3 }).notNull(), // REPR_SER - 대표자순번
- validStartDate: varchar("valid_start_date", { length: 8 }).notNull(), // VLD_ST_DT - 유효시작일자
- nationCode: varchar("nation_code", { length: 3 }), // NTN_CD - 국가코드
- telephoneNumber: varchar("telephone_number", { length: 30 }), // TELNO - 전화번호
- extensionNumber: varchar("extension_number", { length: 10 }), // ETX_NO - 내선번호
- mobileIndicator: varchar("mobile_indicator", { length: 1 }), // HP_ORDR - 핸드폰지시자
- interfaceDate: varchar("interface_date", { length: 8 }), // IF_DT - 인터페이스일자
- interfaceTime: varchar("interface_time", { length: 6 }), // IF_TM - 인터페이스시간
- interfaceStatus: varchar("interface_status", { length: 1 }), // IF_STAT - 인터페이스상태
- interfaceMessage: varchar("interface_message", { length: 100 }), // IF_MSG - 인터페이스메시지
- createdAt: timestamp("created_at").defaultNow().notNull(),
- updatedAt: timestamp("updated_at").defaultNow().notNull(),
- });
-
- // 벤더 대표자 URL 테이블 (CMCTB_VENDOR_REPRURL 대응)
- export const vendorRepresentativeUrls = pgTable("vendor_representative_urls", {
- id: serial("id").primaryKey(),
- vendorCode: varchar("vendor_code", { length: 10 }).notNull().references(() => vendorMdgGenerals.vendorCode), // VNDRCD - 벤더코드 (키) 이고, SAP에서는 VARCHAR10 이다.
- addressNumber: varchar("address_number", { length: 10 }), // ADR_NO - 주소번호
- representativeSequence: varchar("representative_sequence", { length: 3 }).notNull(), // REPR_SER - 대표자순번
- validStartDate: varchar("valid_start_date", { length: 8 }).notNull(), // VLD_ST_DT - 유효시작일자
- url: varchar("url", { length: 2048 }), // URL - URL
- interfaceDate: varchar("interface_date", { length: 8 }), // IF_DT - 인터페이스일자
- interfaceTime: varchar("interface_time", { length: 6 }), // IF_TM - 인터페이스시간
- interfaceStatus: varchar("interface_status", { length: 1 }), // IF_STAT - 인터페이스상태
- interfaceMessage: varchar("interface_message", { length: 100 }), // IF_MSG - 인터페이스메시지
- createdAt: timestamp("created_at").defaultNow().notNull(),
- updatedAt: timestamp("updated_at").defaultNow().notNull(),
- });
-
- // 벤더 세금번호 테이블 (CMCTB_VENDOR_TAXNUM 대응)
- export const vendorTaxNumbers = pgTable("vendor_tax_numbers", {
- id: serial("id").primaryKey(),
- vendorCode: varchar("vendor_code", { length: 10 }).notNull().references(() => vendorMdgGenerals.vendorCode), // VNDRCD - 벤더코드 (키) 이고, SAP에서는 VARCHAR10 이다.
- taxNumberCategory: varchar("tax_number_category", { length: 4 }).notNull(), // TX_NO_CTG - 세금번호범주
- businessPartnerTaxNumber: varchar("business_partner_tax_number", { length: 20 }), // BIZ_PTNR_TX_NO - 사업파트너세금번호
- interfaceDate: varchar("interface_date", { length: 8 }), // IF_DT - 인터페이스일자
- interfaceTime: varchar("interface_time", { length: 6 }), // IF_TM - 인터페이스시간
- interfaceStatus: varchar("interface_status", { length: 1 }), // IF_STAT - 인터페이스상태
- interfaceMessage: varchar("interface_message", { length: 100 }), // IF_MSG - 인터페이스메시지
- createdAt: timestamp("created_at").defaultNow().notNull(),
- updatedAt: timestamp("updated_at").defaultNow().notNull(),
- });
-
- // 벤더 파트너역할 테이블 (CMCTB_VENDOR_VFPN 대응)
- export const vendorPartnerFunctions = pgTable("vendor_partner_functions", {
- id: serial("id").primaryKey(),
- vendorCode: varchar("vendor_code", { length: 10 }).notNull().references(() => vendorMdgGenerals.vendorCode), // VNDRCD - 벤더코드 (키) 이고, SAP에서는 VARCHAR10 이다.
- purchaseOrgCode: varchar("purchase_org_code", { length: 4 }).notNull(), // PUR_ORG_CD - 구매조직
- vendorSubNumber: varchar("vendor_sub_number", { length: 6 }).notNull(), // VNDR_SUB_NO - VENDOR서브번호
- plantCode: varchar("plant_code", { length: 4 }).notNull(), // PLNT_CD - 플랜트코드
- partnerFunction: varchar("partner_function", { length: 2 }).notNull(), // PTNR_SKL - 파트너기능
- partnerCounter: varchar("partner_counter", { length: 3 }).notNull(), // PTNR_CNT - 파트너카운터
- otherReferenceVendorCode: varchar("other_reference_vendor_code", { length: 10 }), // ETC_REF_VNDRCD - 기타참조VENDOR코드
- defaultPartnerIndicator: varchar("default_partner_indicator", { length: 1 }), // BSE_PTNR_ORDR - 기본파트너지시자
- interfaceDate: varchar("interface_date", { length: 8 }), // IF_DT - 인터페이스일자
- interfaceTime: varchar("interface_time", { length: 6 }), // IF_TM - 인터페이스시간
- interfaceStatus: varchar("interface_status", { length: 1 }), // IF_STAT - 인터페이스상태
- interfaceMessage: varchar("interface_message", { length: 100 }), // IF_MSG - 인터페이스메시지
- createdAt: timestamp("created_at").defaultNow().notNull(),
- updatedAt: timestamp("updated_at").defaultNow().notNull(),
- });
-
- // 벤더 원천세 테이블 (CMCTB_VENDOR_WHTHX 대응)
- export const vendorWithholdingTax = pgTable("vendor_withholding_tax", {
- id: serial("id").primaryKey(),
- vendorCode: varchar("vendor_code", { length: 10 }).notNull().references(() => vendorMdgGenerals.vendorCode), // VNDRCD - 벤더코드 (키) 이고, SAP에서는 VARCHAR10 이다.
- companyCode: varchar("company_code", { length: 4 }).notNull(), // CO_CD - 회사코드
- withholdingTaxType: varchar("withholding_tax_type", { length: 2 }).notNull(), // SRCE_TX_TP - 원천세유형
- withholdingTaxRelatedIndicator: varchar("withholding_tax_related_indicator", { length: 1 }), // SRCE_TX_REL_ORDR - 원천세관련지시자
- recipientType: varchar("recipient_type", { length: 2 }), // RECIP_TP - 수취인유형
- withholdingTaxIdentificationNumber: varchar("withholding_tax_identification_number", { length: 16 }), // SRCE_TX_IDENT_NO - 원천세식별번호
- withholdingTaxCode: varchar("withholding_tax_code", { length: 2 }), // SRCE_TX_NO - 원천세코드
- exemptionCertificateNumber: varchar("exemption_certificate_number", { length: 15 }), // DCHAG_CERT_NO - 면제증명서번호
- exemptionRate: integer("exemption_rate"), // DCHAG_RAT - 면제율
- exemptionStartDate: varchar("exemption_start_date", { length: 8 }), // DCHAG_ST_DT - 면제시작일자
- exemptionEndDate: varchar("exemption_end_date", { length: 8 }), // DCHAG_ED_DT - 면제종료일
- exemptionReason: varchar("exemption_reason", { length: 200 }), // DCHAG_CAUS - 면제사유
- interfaceDate: varchar("interface_date", { length: 8 }), // IF_DT - 인터페이스일자
- interfaceTime: varchar("interface_time", { length: 6 }), // IF_TM - 인터페이스시간
- interfaceStatus: varchar("interface_status", { length: 1 }), // IF_STAT - 인터페이스상태
- interfaceMessage: varchar("interface_message", { length: 100 }), // IF_MSG - 인터페이스메시지
- createdAt: timestamp("created_at").defaultNow().notNull(),
- updatedAt: timestamp("updated_at").defaultNow().notNull(),
- });
-
- // ------- [끝] MDG 인터페이스 목적 테이블 추가 -------------
-
- // ------- [시작] MDG 대응을 위한 새로운 테이블 타입 정의 -------------
- export type VendorBusinessGroup = typeof vendorBusinessGroups.$inferSelect
- export type VendorInternalPartner = typeof vendorInternalPartners.$inferSelect
- export type VendorPurchaseOrganization = typeof vendorPurchaseOrganizations.$inferSelect
- export type VendorRepresentativeEmail = typeof vendorRepresentativeEmails.$inferSelect
- export type VendorRepresentativeFax = typeof vendorRepresentativeFaxes.$inferSelect
- export type VendorRepresentativeTelephone = typeof vendorRepresentativeTelephones.$inferSelect
- export type VendorRepresentativeUrl = typeof vendorRepresentativeUrls.$inferSelect
- export type VendorTaxNumber = typeof vendorTaxNumbers.$inferSelect
- export type VendorPartnerFunction = typeof vendorPartnerFunctions.$inferSelect
- export type VendorWithholdingTax = typeof vendorWithholdingTax.$inferSelect
- // ------- [끝] MDG 대응을 위한 새로운 테이블 타입 정의 -------------
-
- // vendors 통합 뷰 - vendorCode 하나로 모든 마이그레이션 정보 확인
- export const vendorComprehensiveView = pgView("vendor_comprehensive_view").as((qb) => {
- return qb
- .select({
- // TODO: 셀렉할 컬럼들 선택
- mdgBusinessRegistrationNumber: vendorMdgGenerals.businessRegistrationNumber,
-
-
- })
- .from(vendorMdgGenerals)
- .leftJoin(vendorMdgGenerals, eq(vendorMdgGenerals.vendorCode, vendorMdgGenerals.vendorCode))
- .leftJoin(vendorBusinessGroups, eq(vendorMdgGenerals.vendorCode, vendorBusinessGroups.vendorCode))
- .leftJoin(vendorInternalPartners, eq(vendorMdgGenerals.vendorCode, vendorInternalPartners.vendorCode))
- .leftJoin(vendorPurchaseOrganizations, eq(vendorMdgGenerals.vendorCode, vendorPurchaseOrganizations.vendorCode))
- .leftJoin(vendorRepresentativeEmails, eq(vendorMdgGenerals.vendorCode, vendorRepresentativeEmails.vendorCode))
- .leftJoin(vendorRepresentativeFaxes, eq(vendorMdgGenerals.vendorCode, vendorRepresentativeFaxes.vendorCode))
- .leftJoin(vendorRepresentativeTelephones, eq(vendorMdgGenerals.vendorCode, vendorRepresentativeTelephones.vendorCode))
- .leftJoin(vendorRepresentativeUrls, eq(vendorMdgGenerals.vendorCode, vendorRepresentativeUrls.vendorCode))
- .leftJoin(vendorTaxNumbers, eq(vendorMdgGenerals.vendorCode, vendorTaxNumbers.vendorCode))
- .leftJoin(vendorPartnerFunctions, eq(vendorMdgGenerals.vendorCode, vendorPartnerFunctions.vendorCode))
- .leftJoin(vendorWithholdingTax, eq(vendorMdgGenerals.vendorCode, vendorWithholdingTax.vendorCode))
- })
-
- export type VendorComprehensiveView = typeof vendorComprehensiveView.$inferSelect
- \ No newline at end of file
diff --git a/lib/vendors/mdg-actions.ts b/lib/vendors/mdg-actions.ts
new file mode 100644
index 00000000..ac57aec4
--- /dev/null
+++ b/lib/vendors/mdg-actions.ts
@@ -0,0 +1,93 @@
+"use server"
+
+/**
+ * MDG 마이그레이션 진행되지 않은 상태라 PLM DB를 싱크해 사용했으므로, 추후 수정 필요
+ * PLM 쪽으로는 업데이트 불가능하므로, 최초 1회 마이그레이션한 데이터만 사용할 것임
+ * node-cron 으로 PLM 데이터 동기화할 필요도 없다는 얘기
+ */
+
+import { revalidateTag } from "next/cache"
+import { vendorMdgService, type VendorUpdateData } from "./mdg-service"
+import { z } from "zod"
+
+// 벤더 업데이트 데이터 스키마
+const vendorUpdateSchema = z.object({
+ vendorId: z.string().min(1, "Vendor ID is required"),
+ updateData: z.object({
+ VNDRNM_1: z.string().optional(),
+ VNDRNM_2: z.string().optional(),
+ VNDRNM_ABRV_1: z.string().optional(),
+ BIZR_NO: z.string().optional(),
+ CO_REG_NO: z.string().optional(),
+ CO_VLM: z.string().optional(),
+ REPR_NM: z.string().optional(),
+ REP_TEL_NO: z.string().optional(),
+ REPR_RESNO: z.string().optional(),
+ REPRESENTATIVE_EMAIL: z.string().optional(),
+ BIZTP: z.string().optional(),
+ BIZCON: z.string().optional(),
+ NTN_CD: z.string().optional(),
+ ADR_1: z.string().optional(),
+ ADR_2: z.string().optional(),
+ POSTAL_CODE: z.string().optional(),
+ ADDR_DETAIL_1: z.string().optional(),
+ })
+})
+
+export type VendorUpdateInput = z.infer<typeof vendorUpdateSchema>
+
+/**
+ * MDG 벤더 기본 정보 업데이트 서버 액션
+ */
+export async function updateMdgVendorBasicInfo(input: VendorUpdateInput) {
+ try {
+ // 입력 데이터 검증
+ const validatedData = vendorUpdateSchema.parse(input)
+
+ // 벤더 ID로 벤더 코드 조회
+ const vendorCode = await vendorMdgService.getVendorCodeByVendorId(validatedData.vendorId)
+
+ if (!vendorCode) {
+ return {
+ success: false,
+ error: "벤더를 찾을 수 없습니다."
+ }
+ }
+
+ // MDG 서비스를 통해 벤더 정보 업데이트
+ const success = await vendorMdgService.updateVendorBasicInfo(
+ vendorCode,
+ validatedData.updateData
+ )
+
+ if (success) {
+ // 캐시 무효화
+ revalidateTag(`vendor-details-${validatedData.vendorId}`)
+ revalidateTag("vendors")
+
+ return {
+ success: true,
+ message: "벤더 정보가 성공적으로 업데이트되었습니다."
+ }
+ } else {
+ return {
+ success: false,
+ error: "벤더 정보 업데이트에 실패했습니다."
+ }
+ }
+ } catch (error) {
+ console.error("MDG 벤더 정보 업데이트 중 오류:", error)
+
+ if (error instanceof z.ZodError) {
+ return {
+ success: false,
+ error: `입력 데이터 오류: ${error.errors[0].message}`
+ }
+ }
+
+ return {
+ success: false,
+ error: error instanceof Error ? error.message : "알 수 없는 오류가 발생했습니다."
+ }
+ }
+} \ No newline at end of file
diff --git a/lib/vendors/mdg-service.ts b/lib/vendors/mdg-service.ts
new file mode 100644
index 00000000..27372a1e
--- /dev/null
+++ b/lib/vendors/mdg-service.ts
@@ -0,0 +1,598 @@
+/**
+ * MDG 마이그레이션 진행되지 않은 상태라 PLM DB를 싱크해 사용했으므로, 추후 수정 필요
+ * PLM 쪽으로는 업데이트 불가능하므로, 최초 1회 마이그레이션한 데이터만 사용할 것임
+ * node-cron 으로 PLM 데이터 동기화할 필요도 없다는 얘기
+ */
+
+import { eq } from 'drizzle-orm'
+import {
+ cmctbVendorGeneral,
+ cmctbVendorAddr,
+ cmctbVendorCompny,
+ cmctbVendorPorg,
+ cmctbVendorRepremail,
+ cmctbVendorInco,
+ type CmctbVendorGeneral,
+ type CmctbVendorAddr
+} from '@/db/schema/NONSAP/nonsap'
+import { vendors } from '@/db/schema/vendors'
+import db from '@/db/db'
+import { debugLog, debugError, debugWarn, debugSuccess } from '@/lib/debug-utils'
+
+// 구매조직별 정보 타입 정의
+export interface PurchasingOrgInfo {
+ PUR_ORG_CD: string // 구매조직 코드
+ PUR_ORD_CUR: string | null // 오더통화
+ SPLY_COND: string | null // 지급조건
+ DL_COND_1: string | null // 인도조건1
+ DL_COND_2: string | null // 인도조건2
+ GR_BSE_INVC_VR: string | null // GR기준송장검증
+ ORD_CNFM_REQ_ORDR: string | null // P/O 확인요청
+ 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 // VENDOR전화번호
+ PUR_HOLD_DT: string | null // 구매보류일자
+ PUR_HOLD_CAUS: string | null // 구매보류사유
+}
+
+// 벤더 상세 정보 타입 정의
+export interface VendorDetails {
+ // 기본 정보
+ VNDRCD: string
+ VNDRNM_1: string | null
+ VNDRNM_2: string | null
+ VNDRNM_ABRV_1: string | null
+ BIZR_NO: string | null
+ CO_REG_NO: string | null
+ CO_VLM: 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
+}
+
+// 벤더 수정 데이터 타입
+export interface VendorUpdateData {
+ // 기본 정보
+ VNDRNM_1?: string
+ VNDRNM_2?: string
+ VNDRNM_ABRV_1?: string
+ BIZR_NO?: string
+ CO_REG_NO?: string
+ CO_VLM?: string
+
+ // 대표자 정보
+ REPR_NM?: string
+ REP_TEL_NO?: string
+ REPR_RESNO?: string
+ REPRESENTATIVE_EMAIL?: string
+
+ // 사업 정보
+ BIZTP?: string
+ BIZCON?: string
+ NTN_CD?: string
+
+ // 주소 정보
+ ADR_1?: string
+ ADR_2?: string
+ POSTAL_CODE?: string
+ ADDR_DETAIL_1?: string
+}
+
+// 벤더 목록 아이템 타입
+export interface VendorListItem {
+ VNDRCD: string
+ VNDRNM_1: string | null
+ VNDRNM_2: string | null
+ BIZR_NO: string | null
+ REG_DT: string | null
+ DEL_ORDR: string | null
+ PUR_HOLD_ORDR: string | null
+}
+
+export class VendorMdgService {
+ /**
+ * 벤더 ID로 벤더 코드 조회
+ * @param vendorId 벤더 ID
+ * @returns 벤더 코드 (VNDRCD)
+ */
+ async getVendorCodeByVendorId(vendorId: string): Promise<string | null> {
+ debugLog(`벤더 코드 조회 시작: ID ${vendorId}`)
+
+ try {
+ const vendor = await db
+ .select({ vendorCode: vendors.vendorCode })
+ .from(vendors)
+ .where(eq(vendors.id, parseInt(vendorId)))
+ .limit(1)
+
+ debugLog(`vendors 테이블 조회 결과:`, {
+ found: vendor.length > 0,
+ vendorCode: vendor[0]?.vendorCode || null
+ })
+
+ if (vendor.length === 0) {
+ debugWarn(`벤더 ID ${vendorId}에 해당하는 벤더를 찾을 수 없습니다.`)
+ return null
+ }
+
+ const vendorCode = vendor[0].vendorCode
+ if (!vendorCode) {
+ debugWarn(`벤더 ID ${vendorId}의 vendor_code가 null입니다.`)
+ return null
+ }
+
+ debugSuccess(`벤더 코드 조회 성공: ID ${vendorId} -> 코드 ${vendorCode}`)
+ return vendorCode
+ } catch (error) {
+ debugError('벤더 코드 조회 중 오류 발생:', error)
+ return null
+ }
+ }
+
+ /**
+ * 벤더 ID로 벤더 상세 정보 조회
+ * @param vendorId 벤더 ID
+ * @returns 벤더 상세 정보 (데이터가 없어도 기본 구조 반환)
+ */
+ async getVendorDetailsByVendorId(vendorId: string): Promise<VendorDetails | null> {
+ debugLog(`벤더 ID로 상세 정보 조회 시작: ${vendorId}`)
+
+ // 1. 벤더 코드 조회
+ const vendorCode = await this.getVendorCodeByVendorId(vendorId)
+
+ if (!vendorCode) {
+ debugWarn(`벤더 ID ${vendorId}에 대한 벤더 코드를 찾을 수 없습니다.`)
+ return null
+ }
+
+ // 2. 벤더 코드로 상세 정보 조회
+ debugLog(`벤더 코드 ${vendorCode}로 상세 정보 조회 시작`)
+ return await this.getVendorDetails(vendorCode)
+ }
+
+ /**
+ * 벤더 코드로 벤더 상세 정보 조회
+ * @param vendorCode 벤더 코드
+ * @returns 벤더 상세 정보 (데이터가 없어도 기본 구조 반환)
+ */
+ async getVendorDetails(vendorCode: string): Promise<VendorDetails> {
+ debugLog(`벤더 정보 조회 시작: ${vendorCode}`)
+
+ try {
+ // 메인 쿼리: 벤더 일반 정보
+ debugLog(`CMCTB_VENDOR_GENERAL 테이블에서 ${vendorCode} 조회 중...`)
+ const vendorGeneral = await db
+ .select()
+ .from(cmctbVendorGeneral)
+ .where(eq(cmctbVendorGeneral.VNDRCD, vendorCode))
+ .limit(1)
+
+ debugLog(`CMCTB_VENDOR_GENERAL 조회 결과:`, {
+ found: vendorGeneral.length > 0,
+ count: vendorGeneral.length,
+ data: vendorGeneral.length > 0 ? vendorGeneral[0] : null
+ })
+
+ const vendor = vendorGeneral[0] || null
+
+ // 주소 정보 조회
+ debugLog(`CMCTB_VENDOR_ADDR 테이블에서 ${vendorCode} 조회 중...`)
+ const vendorAddr = await db
+ .select()
+ .from(cmctbVendorAddr)
+ .where(eq(cmctbVendorAddr.VNDRCD, vendorCode))
+ .limit(1)
+
+ debugLog(`CMCTB_VENDOR_ADDR 조회 결과:`, {
+ found: vendorAddr.length > 0,
+ count: vendorAddr.length,
+ data: vendorAddr.length > 0 ? vendorAddr[0] : null
+ })
+
+ // 회사 정보 조회 (첫 번째 회사 코드)
+ debugLog(`CMCTB_VENDOR_COMPNY 테이블에서 ${vendorCode} 조회 중...`)
+ const vendorCompany = await db
+ .select()
+ .from(cmctbVendorCompny)
+ .where(eq(cmctbVendorCompny.VNDRCD, vendorCode))
+ .limit(1)
+
+ debugLog(`CMCTB_VENDOR_COMPNY 조회 결과:`, {
+ found: vendorCompany.length > 0,
+ count: vendorCompany.length,
+ data: vendorCompany.length > 0 ? vendorCompany[0] : null
+ })
+
+ // 모든 구매조직 정보 조회
+ debugLog(`CMCTB_VENDOR_PORG 테이블에서 ${vendorCode}의 모든 구매조직 조회 중...`)
+ const vendorPorgs = await db
+ .select()
+ .from(cmctbVendorPorg)
+ .where(eq(cmctbVendorPorg.VNDRCD, vendorCode))
+
+ debugLog(`CMCTB_VENDOR_PORG 조회 결과:`, {
+ found: vendorPorgs.length > 0,
+ count: vendorPorgs.length,
+ data: vendorPorgs
+ })
+
+ // 사내협력사 정보 조회 (내외자구분)
+ debugLog(`CMCTB_VENDOR_INCO 테이블에서 ${vendorCode} 조회 중...`)
+ const vendorInco = await db
+ .select()
+ .from(cmctbVendorInco)
+ .where(eq(cmctbVendorInco.VNDRCD, vendorCode))
+ .limit(1)
+
+ debugLog(`CMCTB_VENDOR_INCO 조회 결과:`, {
+ found: vendorInco.length > 0,
+ count: vendorInco.length,
+ data: vendorInco.length > 0 ? vendorInco[0] : null
+ })
+
+ // 대표자 이메일 조회
+ debugLog(`CMCTB_VENDOR_REPREMAIL 테이블에서 ${vendorCode} 조회 중...`)
+ const vendorEmail = await db
+ .select()
+ .from(cmctbVendorRepremail)
+ .where(eq(cmctbVendorRepremail.VNDRCD, vendorCode))
+ .limit(1)
+
+ debugLog(`CMCTB_VENDOR_REPREMAIL 조회 결과:`, {
+ found: vendorEmail.length > 0,
+ count: vendorEmail.length,
+ data: vendorEmail.length > 0 ? vendorEmail[0] : null
+ })
+
+ const addr = vendorAddr[0] || null
+ const company = vendorCompany[0] || null
+ const inco = vendorInco[0] || null
+ const email = vendorEmail[0] || null
+
+ // 구매조직 정보 배열 구성
+ const purchasingOrgs: PurchasingOrgInfo[] = vendorPorgs.map(porg => ({
+ PUR_ORG_CD: porg.PUR_ORG_CD,
+ PUR_ORD_CUR: porg.PUR_ORD_CUR,
+ SPLY_COND: porg.SPLY_COND,
+ DL_COND_1: porg.DL_COND_1,
+ DL_COND_2: porg.DL_COND_2,
+ GR_BSE_INVC_VR: porg.GR_BSE_INVC_VR,
+ ORD_CNFM_REQ_ORDR: porg.ORD_CNFM_REQ_ORDR,
+ CNFM_CTL_KEY: porg.CNFM_CTL_KEY,
+ PUR_HOLD_ORDR: porg.PUR_HOLD_ORDR,
+ DEL_ORDR: porg.DEL_ORDR,
+ AT_PUR_ORD_ORDR: porg.AT_PUR_ORD_ORDR,
+ SALE_CHRGR_NM: porg.SALE_CHRGR_NM,
+ VNDR_TELNO: porg.VNDR_TELNO,
+ PUR_HOLD_DT: porg.PUR_HOLD_DT,
+ PUR_HOLD_CAUS: porg.PUR_HOLD_CAUS
+ }))
+
+ // 데이터 존재 여부 확인
+ const hasAnyData = vendor || addr || company || purchasingOrgs.length > 0 || inco || email
+ if (!hasAnyData) {
+ debugWarn(`벤더 ${vendorCode}에 대한 데이터가 전혀 없습니다. 기본 구조만 반환합니다.`)
+ } else {
+ debugSuccess(`벤더 ${vendorCode} 데이터 조회 완료`, {
+ general: !!vendor,
+ addr: !!addr,
+ company: !!company,
+ purchasingOrgs: purchasingOrgs.length,
+ inco: !!inco,
+ email: !!email
+ })
+ }
+
+ // 벤더 상세 정보 구성 (데이터가 없어도 기본 구조 제공)
+ const vendorDetails: VendorDetails = {
+ // 기본 정보 (General 테이블)
+ VNDRCD: vendorCode, // 항상 요청된 벤더 코드 반환
+ VNDRNM_1: vendor?.VNDRNM_1 || null,
+ VNDRNM_2: vendor?.VNDRNM_2 || null,
+ VNDRNM_ABRV_1: vendor?.VNDRNM_ABRV_1 || null,
+ BIZR_NO: vendor?.BIZR_NO || null,
+ CO_REG_NO: vendor?.CO_REG_NO || null,
+ CO_VLM: vendor?.CO_VLM || null,
+
+ // 대표자 정보
+ REPR_NM: vendor?.REPR_NM || null,
+ REP_TEL_NO: vendor?.REP_TEL_NO || null,
+ REPR_RESNO: vendor?.REPR_RESNO || null,
+ REPRESENTATIVE_EMAIL: email?.EMAIL_ADR || null,
+
+ // 사업 정보
+ BIZTP: vendor?.BIZTP || null,
+ BIZCON: vendor?.BIZCON || null,
+ NTN_CD: vendor?.NTN_CD || null,
+ REG_DT: vendor?.REG_DT || null,
+
+ // 주소 정보 (Address 테이블 우선, 없으면 General 테이블)
+ ADR_1: addr?.ADR_1 || vendor?.ADR_1 || null,
+ ADR_2: addr?.ADR_2 || vendor?.ADR_2 || null,
+ POSTAL_CODE: addr?.CITY_ZIP_NO || null,
+ ADDR_DETAIL_1: addr?.ETC_ADR_1 || null,
+
+ // 이전업체코드
+ PREVIOUS_VENDOR_CODE: company?.BF_VNDRCD || null,
+
+ // 내외자구분 (사내협력사 정보)
+ PRTNR_GB: inco?.PRTNR_GB || null,
+
+ // 구매조직별 정보 배열
+ PURCHASING_ORGS: purchasingOrgs,
+
+ // 상태 정보 (기본값 제공)
+ DEL_ORDR: vendor?.DEL_ORDR || 'N', // 기본값: 활성
+ PUR_HOLD_ORDR: vendor?.PUR_HOLD_ORDR || null
+ }
+
+ debugLog(`최종 벤더 정보 구성 완료:`, vendorDetails)
+
+ return vendorDetails
+ } catch (error) {
+ debugError('벤더 정보 조회 중 오류 발생:', error)
+
+ // 오류가 발생해도 기본 구조는 제공
+ debugWarn(`오류로 인해 ${vendorCode}의 기본 구조만 반환합니다.`)
+ return {
+ VNDRCD: vendorCode,
+ VNDRNM_1: null,
+ VNDRNM_2: null,
+ VNDRNM_ABRV_1: null,
+ BIZR_NO: null,
+ CO_REG_NO: null,
+ CO_VLM: null,
+ REPR_NM: null,
+ REP_TEL_NO: null,
+ REPR_RESNO: null,
+ REPRESENTATIVE_EMAIL: null,
+ BIZTP: null,
+ BIZCON: null,
+ NTN_CD: null,
+ REG_DT: null,
+ ADR_1: null,
+ ADR_2: null,
+ POSTAL_CODE: null,
+ ADDR_DETAIL_1: null,
+ PREVIOUS_VENDOR_CODE: null,
+ PRTNR_GB: null,
+ PURCHASING_ORGS: [],
+ DEL_ORDR: 'N', // 기본값: 활성
+ PUR_HOLD_ORDR: null
+ }
+ }
+ }
+
+ /**
+ * 벤더 기본 정보 수정
+ * @param vendorCode 벤더 코드
+ * @param updateData 수정할 데이터
+ * @returns 성공 여부
+ */
+ async updateVendorBasicInfo(
+ vendorCode: string,
+ updateData: VendorUpdateData
+ ): Promise<boolean> {
+ try {
+ // 트랜잭션으로 여러 테이블 업데이트
+ await db.transaction(async (tx) => {
+ // 1. General 테이블 업데이트
+ const generalUpdateData: Partial<CmctbVendorGeneral> = {}
+ if (updateData.VNDRNM_1 !== undefined) generalUpdateData.VNDRNM_1 = updateData.VNDRNM_1
+ if (updateData.VNDRNM_2 !== undefined) generalUpdateData.VNDRNM_2 = updateData.VNDRNM_2
+ if (updateData.VNDRNM_ABRV_1 !== undefined) generalUpdateData.VNDRNM_ABRV_1 = updateData.VNDRNM_ABRV_1
+ if (updateData.BIZR_NO !== undefined) generalUpdateData.BIZR_NO = updateData.BIZR_NO
+ if (updateData.CO_REG_NO !== undefined) generalUpdateData.CO_REG_NO = updateData.CO_REG_NO
+ if (updateData.CO_VLM !== undefined) generalUpdateData.CO_VLM = updateData.CO_VLM
+ if (updateData.REPR_NM !== undefined) generalUpdateData.REPR_NM = updateData.REPR_NM
+ if (updateData.REP_TEL_NO !== undefined) generalUpdateData.REP_TEL_NO = updateData.REP_TEL_NO
+ if (updateData.REPR_RESNO !== undefined) generalUpdateData.REPR_RESNO = updateData.REPR_RESNO
+ if (updateData.BIZTP !== undefined) generalUpdateData.BIZTP = updateData.BIZTP
+ if (updateData.BIZCON !== undefined) generalUpdateData.BIZCON = updateData.BIZCON
+ if (updateData.NTN_CD !== undefined) generalUpdateData.NTN_CD = updateData.NTN_CD
+ if (updateData.ADR_1 !== undefined) generalUpdateData.ADR_1 = updateData.ADR_1
+ if (updateData.ADR_2 !== undefined) generalUpdateData.ADR_2 = updateData.ADR_2
+
+ // 현재 시간 설정
+ generalUpdateData.CHG_DT = new Date().toISOString().slice(0, 10).replace(/-/g, '')
+ generalUpdateData.CHG_TM = new Date().toTimeString().slice(0, 8).replace(/:/g, '')
+
+ if (Object.keys(generalUpdateData).length > 2) { // CHG_DT, CHG_TM 외에 다른 필드가 있는 경우만 업데이트
+ await tx
+ .update(cmctbVendorGeneral)
+ .set(generalUpdateData)
+ .where(eq(cmctbVendorGeneral.VNDRCD, vendorCode))
+ }
+
+ // 2. Address 테이블 업데이트 (있는 경우)
+ if (updateData.ADR_1 || updateData.ADR_2 || updateData.POSTAL_CODE || updateData.ADDR_DETAIL_1) {
+ const addrUpdateData: Partial<CmctbVendorAddr> = {}
+ if (updateData.ADR_1 !== undefined) addrUpdateData.ADR_1 = updateData.ADR_1
+ if (updateData.ADR_2 !== undefined) addrUpdateData.ADR_2 = updateData.ADR_2
+ if (updateData.POSTAL_CODE !== undefined) addrUpdateData.CITY_ZIP_NO = updateData.POSTAL_CODE
+ if (updateData.ADDR_DETAIL_1 !== undefined) addrUpdateData.ETC_ADR_1 = updateData.ADDR_DETAIL_1
+
+ // 주소 레코드가 있는지 확인
+ const existingAddr = await tx
+ .select()
+ .from(cmctbVendorAddr)
+ .where(eq(cmctbVendorAddr.VNDRCD, vendorCode))
+ .limit(1)
+
+ if (existingAddr.length > 0) {
+ // 기존 주소 업데이트
+ await tx
+ .update(cmctbVendorAddr)
+ .set(addrUpdateData)
+ .where(eq(cmctbVendorAddr.VNDRCD, vendorCode))
+ } else {
+ // 새 주소 레코드 생성
+ await tx
+ .insert(cmctbVendorAddr)
+ .values({
+ VNDRCD: vendorCode,
+ ADR_NO: '0001',
+ INTL_ADR_VER_ID: '1',
+ ...addrUpdateData
+ })
+ }
+ }
+
+ // 3. 대표자 이메일 업데이트 (있는 경우)
+ if (updateData.REPRESENTATIVE_EMAIL !== undefined) {
+ // 기존 이메일 레코드가 있는지 확인
+ const existingEmail = await tx
+ .select()
+ .from(cmctbVendorRepremail)
+ .where(eq(cmctbVendorRepremail.VNDRCD, vendorCode))
+ .limit(1)
+
+ const currentDate = new Date().toISOString().slice(0, 10).replace(/-/g, '')
+
+ if (existingEmail.length > 0) {
+ // 기존 이메일 업데이트
+ await tx
+ .update(cmctbVendorRepremail)
+ .set({
+ EMAIL_ADR: updateData.REPRESENTATIVE_EMAIL,
+ IF_DT: currentDate,
+ IF_TM: new Date().toTimeString().slice(0, 8).replace(/:/g, ''),
+ IF_STAT: '1'
+ })
+ .where(eq(cmctbVendorRepremail.VNDRCD, vendorCode))
+ } else if (updateData.REPRESENTATIVE_EMAIL) {
+ // 새 이메일 레코드 생성 (빈 문자열이 아닌 경우만)
+ await tx
+ .insert(cmctbVendorRepremail)
+ .values({
+ VNDRCD: vendorCode,
+ ADR_NO: '0001',
+ REPR_SER: '001',
+ VLD_ST_DT: currentDate,
+ EMAIL_ADR: updateData.REPRESENTATIVE_EMAIL,
+ IF_DT: currentDate,
+ IF_TM: new Date().toTimeString().slice(0, 8).replace(/:/g, ''),
+ IF_STAT: '1'
+ })
+ }
+ }
+ })
+
+ return true
+ } catch (error) {
+ console.error('벤더 정보 수정 중 오류 발생:', error)
+ throw new Error('벤더 정보를 수정할 수 없습니다.')
+ }
+ }
+
+ /**
+ * 벤더 목록 조회 (페이징)
+ * @param page 페이지 번호 (1부터 시작)
+ * @param limit 페이지당 개수
+ * @param searchTerm 검색어 (업체명 검색)
+ * @returns 벤더 목록과 총 개수
+ */
+ async getVendorList(
+ page: number = 1,
+ limit: number = 20,
+ searchTerm?: string
+ ): Promise<{ vendors: VendorListItem[], total: number }> {
+ try {
+ const offset = (page - 1) * limit
+
+ // 기본 조건: 활성 벤더만 조회
+ const whereCondition = eq(cmctbVendorGeneral.DEL_ORDR, 'N')
+
+ // 검색어가 있는 경우 업체명으로 필터링 (추후 구현 시 사용)
+ void searchTerm // 현재는 미사용
+
+ // 총 개수 조회
+ const totalResult = await db
+ .select({ count: cmctbVendorGeneral.VNDRCD })
+ .from(cmctbVendorGeneral)
+ .where(whereCondition)
+
+ // 벤더 목록 조회
+ const vendors = await db
+ .select({
+ VNDRCD: cmctbVendorGeneral.VNDRCD,
+ VNDRNM_1: cmctbVendorGeneral.VNDRNM_1,
+ VNDRNM_2: cmctbVendorGeneral.VNDRNM_2,
+ BIZR_NO: cmctbVendorGeneral.BIZR_NO,
+ REG_DT: cmctbVendorGeneral.REG_DT,
+ DEL_ORDR: cmctbVendorGeneral.DEL_ORDR,
+ PUR_HOLD_ORDR: cmctbVendorGeneral.PUR_HOLD_ORDR
+ })
+ .from(cmctbVendorGeneral)
+ .where(whereCondition)
+ .limit(limit)
+ .offset(offset)
+
+ return {
+ vendors,
+ total: totalResult.length
+ }
+ } catch (error) {
+ console.error('벤더 목록 조회 중 오류 발생:', error)
+ throw new Error('벤더 목록을 조회할 수 없습니다.')
+ }
+ }
+
+ /**
+ * 벤더 상태 변경 (활성/비활성)
+ * @param vendorCode 벤더 코드
+ * @param isActive 활성 상태 여부
+ * @returns 성공 여부
+ */
+ async updateVendorStatus(vendorCode: string, isActive: boolean): Promise<boolean> {
+ try {
+ await db
+ .update(cmctbVendorGeneral)
+ .set({
+ DEL_ORDR: isActive ? 'N' : 'Y',
+ CHG_DT: new Date().toISOString().slice(0, 10).replace(/-/g, ''),
+ CHG_TM: new Date().toTimeString().slice(0, 8).replace(/:/g, '')
+ })
+ .where(eq(cmctbVendorGeneral.VNDRCD, vendorCode))
+
+ return true
+ } catch (error) {
+ console.error('벤더 상태 변경 중 오류 발생:', error)
+ throw new Error('벤더 상태를 변경할 수 없습니다.')
+ }
+ }
+}
+
+// 싱글톤 인스턴스 생성
+export const vendorMdgService = new VendorMdgService()