diff options
| -rw-r--r-- | app/[lng]/evcp/(evcp)/vendors/[id]/info/basic/page.tsx | 39 | ||||
| -rw-r--r-- | app/[lng]/evcp/(evcp)/vendors/[id]/info/basic/vendor-basic-info.tsx | 735 | ||||
| -rw-r--r-- | app/[lng]/evcp/(evcp)/vendors/[id]/info/layout.tsx | 4 | ||||
| -rw-r--r-- | db/schema/PLM/plmVendorSchema.ts | 337 | ||||
| -rw-r--r-- | lib/vendors/mdg-actions.ts | 93 | ||||
| -rw-r--r-- | lib/vendors/mdg-service.ts | 598 |
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() |
