From 51e496b704f819a6c705fce0553d36be973ce1dd Mon Sep 17 00:00:00 2001 From: joonhoekim <26rote@gmail.com> Date: Wed, 2 Jul 2025 04:48:14 +0000 Subject: (김준회) 협력사관리 - 구매 벤더 기본정보 FIX MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../evcp/(evcp)/vendors/[id]/info/basic/page.tsx | 23 +- .../(evcp)/vendors/[id]/info/basic/text-utils.tsx | 131 +++++++ .../vendors/[id]/info/basic/vendor-basic-info.tsx | 126 ++++--- .../vendors/[id]/info/basic/vendor-evcp-info.tsx | 396 +++++++++++++++++++++ db/schema/SOAP/soap.ts | 4 +- 5 files changed, 614 insertions(+), 66 deletions(-) create mode 100644 app/[lng]/evcp/(evcp)/vendors/[id]/info/basic/text-utils.tsx create mode 100644 app/[lng]/evcp/(evcp)/vendors/[id]/info/basic/vendor-evcp-info.tsx diff --git a/app/[lng]/evcp/(evcp)/vendors/[id]/info/basic/page.tsx b/app/[lng]/evcp/(evcp)/vendors/[id]/info/basic/page.tsx index 6b058b37..d8f04095 100644 --- a/app/[lng]/evcp/(evcp)/vendors/[id]/info/basic/page.tsx +++ b/app/[lng]/evcp/(evcp)/vendors/[id]/info/basic/page.tsx @@ -1,10 +1,12 @@ import { vendorMdgService } from "@/lib/vendors/mdg-service" +import { getVendorDetailById } from "@/lib/vendors/service" import { VendorBasicInfo } from "./vendor-basic-info" +import { VendorEvCpInfo } from "./vendor-evcp-info" interface VendorBasicPageProps { params: { lng: string - // 협력업체 ID: 여기서는 Oracle의 벤더 코드(VNDRCD)를 사용 + // 협력업체 ID: 여기서는 eVCP의 벤더 ID를 사용 id: string } } @@ -13,10 +15,13 @@ export default async function VendorBasicPage(props: VendorBasicPageProps) { const resolvedParams = await props.params const vendorId = resolvedParams.id + // eVCP 벤더 정보 조회 + const evcpVendorDetails = await getVendorDetailById(parseInt(vendorId)) + // Oracle에서 벤더 상세 정보 조회 (ID로 조회) - const vendorDetails = await vendorMdgService.getVendorDetailsByVendorId(vendorId) + const oracleVendorDetails = await vendorMdgService.getVendorDetailsByVendorId(vendorId) - if (!vendorDetails) { + if (!evcpVendorDetails && !oracleVendorDetails) { return (
@@ -32,8 +37,16 @@ export default async function VendorBasicPage(props: VendorBasicPageProps) { } return ( -
- +
+ {/* eVCP 벤더 정보 */} + {evcpVendorDetails && ( + + )} + + {/* Oracle 벤더 정보 */} + {oracleVendorDetails && ( + + )}
) } \ No newline at end of file diff --git a/app/[lng]/evcp/(evcp)/vendors/[id]/info/basic/text-utils.tsx b/app/[lng]/evcp/(evcp)/vendors/[id]/info/basic/text-utils.tsx new file mode 100644 index 00000000..a3507dd0 --- /dev/null +++ b/app/[lng]/evcp/(evcp)/vendors/[id]/info/basic/text-utils.tsx @@ -0,0 +1,131 @@ +"use client" + +import { useState } from "react" +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip" +import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible" +import { ChevronDown, ChevronUp } from "lucide-react" + +export function TruncatedText({ + text, + maxLength = 50, + showTooltip = true +}: { + text: string | null + maxLength?: number + showTooltip?: boolean +}) { + if (!text) return - + + if (text.length <= maxLength) { + return {text} + } + + const truncated = text.slice(0, maxLength) + "..." + + if (!showTooltip) { + return {truncated} + } + + return ( + + + + + {truncated} + + + +

{text}

+
+
+
+ ) +} + +export function ExpandableText({ + text, + maxLength = 100, + className = "" +}: { + text: string | null + maxLength?: number + className?: string +}) { + const [isExpanded, setIsExpanded] = useState(false) + + if (!text) return - + + if (text.length <= maxLength) { + return {text} + } + + return ( + +
+ + + +
+
+ ) +} + +export function AddressDisplay({ + address, + addressEng, + postalCode, + addressDetail +}: { + address: string | null + addressEng: string | null + postalCode: string | null + addressDetail: string | null +}) { + const hasAnyAddress = address || addressEng || postalCode || addressDetail + + if (!hasAnyAddress) { + return - + } + + return ( +
+ {postalCode && ( +
+ 우편번호: {postalCode} +
+ )} + {address && ( +
+ {address} +
+ )} + {addressDetail && ( +
+ {addressDetail} +
+ )} + {addressEng && ( +
+ {addressEng} +
+ )} +
+ ) +} \ 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 index 16f75bcb..e9cbd8be 100644 --- 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 @@ -3,14 +3,13 @@ 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 { AddressDisplay } from "./text-utils" import { Dialog, DialogContent, @@ -86,7 +85,7 @@ interface VendorBasicInfoProps { export function VendorBasicInfo({ vendorDetails }: VendorBasicInfoProps) { const params = useParams() - const vendorId = params.id as string + const vendorId = params?.id as string const [isEditing, setIsEditing] = useState(false) const [editData, setEditData] = useState(vendorDetails) const [isPending, startTransition] = useTransition() @@ -186,23 +185,23 @@ export function VendorBasicInfo({ vendorDetails }: VendorBasicInfoProps) { 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, + VNDRNM_1: editData.VNDRNM_1 || undefined, + VNDRNM_2: editData.VNDRNM_2 || undefined, + VNDRNM_ABRV_1: editData.VNDRNM_ABRV_1 || undefined, + BIZR_NO: editData.BIZR_NO || undefined, + CO_REG_NO: editData.CO_REG_NO || undefined, + CO_VLM: editData.CO_VLM || undefined, + REPR_NM: editData.REPR_NM || undefined, + REP_TEL_NO: editData.REP_TEL_NO || undefined, + REPR_RESNO: editData.REPR_RESNO || undefined, + REPRESENTATIVE_EMAIL: editData.REPRESENTATIVE_EMAIL || undefined, + BIZTP: editData.BIZTP || undefined, + BIZCON: editData.BIZCON || undefined, + NTN_CD: editData.NTN_CD || undefined, + ADR_1: editData.ADR_1 || undefined, + ADR_2: editData.ADR_2 || undefined, + POSTAL_CODE: editData.POSTAL_CODE || undefined, + ADDR_DETAIL_1: editData.ADDR_DETAIL_1 || undefined, } }) @@ -365,47 +364,56 @@ export function VendorBasicInfo({ vendorDetails }: VendorBasicInfoProps) { return ( <> {/* 헤더 */} -
-
-

- {editData.VNDRNM_1 || '업체명 없음'} -

-

- 벤더 코드: {editData.VNDRCD} -

-
-
- {/* 상태 배지 */} -
- {getStatusBadge(editData.DEL_ORDR)} + + +
+
+ + + {editData.VNDRNM_1 || '업체명 없음'} + Oracle + + + 벤더 코드: {editData.VNDRCD} + +
+
+ {/* 상태 배지 */} +
+ {getStatusBadge(editData.DEL_ORDR)} +
+ + {/* 액션 버튼들 */} +
+ {isEditing ? ( + <> + + + + ) : ( + <> + + + )} +
+
- - {/* 액션 버튼들 */} -
- {isEditing ? ( - <> - - - - ) : ( - <> - - - )} + + +
+ Oracle 시스템에서 연동된 협력업체 정보입니다. 수정 시 Oracle에 반영됩니다.
-
-
- - + +
{/* 기본 정보 */} diff --git a/app/[lng]/evcp/(evcp)/vendors/[id]/info/basic/vendor-evcp-info.tsx b/app/[lng]/evcp/(evcp)/vendors/[id]/info/basic/vendor-evcp-info.tsx new file mode 100644 index 00000000..4da3162a --- /dev/null +++ b/app/[lng]/evcp/(evcp)/vendors/[id]/info/basic/vendor-evcp-info.tsx @@ -0,0 +1,396 @@ +"use client" + +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" +import { Badge } from "@/components/ui/badge" +import { AddressDisplay } from "./text-utils" +import { + Phone, + Mail, + Calendar, + CheckCircle, + XCircle, + Building2, + Globe, + User, + FileText, + TrendingUp, + Hash, + MapPin, + Users, + Award, + Briefcase, + Shield, + Star, + DollarSign +} from "lucide-react" +import { VendorDetailView } from "@/db/schema/vendors" + +interface VendorEvCpInfoProps { + vendorDetails: VendorDetailView +} + +// 신용평가기관 표시 매핑 +const creditAgencyMap: Record = { + NICE: "NICE평가정보", + KIS: "KIS (한국신용평가)", + KED: "KED (한국기업데이터)", + SCI: "SCI평가정보", +} + +// 사업규모 표시 개선 +const businessSizeMap: Record = { + "LARGE": { label: "대기업", color: "text-purple-600" }, + "MEDIUM": { label: "중견기업", color: "text-blue-600" }, + "SMALL": { label: "중소기업", color: "text-green-600" }, + "STARTUP": { label: "스타트업", color: "text-orange-600" }, +} + +export function VendorEvCpInfo({ vendorDetails }: VendorEvCpInfoProps) { + // 연락처 정보 파싱 + const contacts = Array.isArray(vendorDetails.contacts) + ? vendorDetails.contacts + : typeof vendorDetails.contacts === 'string' + ? JSON.parse(vendorDetails.contacts || '[]') + : [] + + // 첨부파일 정보 파싱 + const attachments = Array.isArray(vendorDetails.attachments) + ? vendorDetails.attachments + : typeof vendorDetails.attachments === 'string' + ? JSON.parse(vendorDetails.attachments || '[]') + : [] + + // 상태에 따른 뱃지 스타일 결정 + const getStatusBadge = (status: string) => { + switch (status) { + case 'ACTIVE': + return 활성 + case 'INACTIVE': + return 비활성 + case 'PENDING_REVIEW': + return 검토중 + case 'APPROVED': + return 승인됨 + case 'BLACKLISTED': + return 거래금지 + default: + return {status} + } + } + + // 신용등급 색상 결정 + const getCreditRatingColor = (rating: string) => { + if (rating?.includes('AAA')) return "text-green-600 bg-green-50" + if (rating?.includes('AA')) return "text-blue-600 bg-blue-50" + if (rating?.includes('A')) return "text-indigo-600 bg-indigo-50" + if (rating?.includes('BBB')) return "text-yellow-600 bg-yellow-50" + if (rating?.includes('BB') || rating?.includes('B')) return "text-orange-600 bg-orange-50" + return "text-gray-600 bg-gray-50" + } + + // 필드 렌더링 헬퍼 + const renderField = (label: string, value: React.ReactNode, icon?: React.ReactNode) => { + if (!value) return null + return ( +
+ +
+ {value} +
+
+ ) + } + + return ( +
+ {/* 헤더 */} + + +
+
+ + + {vendorDetails.vendorName || '업체명 없음'} + + eVCP + + + + 벤더 코드: {vendorDetails.vendorCode || 'N/A'} + 사업자번호: {vendorDetails.taxId || 'N/A'} + +
+
+ {getStatusBadge(vendorDetails.status || 'ACTIVE')} +
+
+
+
+ +
+ {/* 기본 정보 */} + + + + 기본 정보 + + + + {renderField("업체명", vendorDetails.vendorName)} + {renderField("업체 코드", vendorDetails.vendorCode, )} + {renderField("사업자등록번호", vendorDetails.taxId, )} + {renderField("법인등록번호", vendorDetails.corporateRegistrationNumber, )} + {renderField("국가", vendorDetails.country, )} + + {/* 사업규모 */} + {vendorDetails.businessSize && ( +
+ +
+ + {businessSizeMap[vendorDetails.businessSize]?.label || vendorDetails.businessSize} + +
+
+ )} + + {/* 등록일 */} + {renderField("등록일", + vendorDetails.createdAt ? new Date(vendorDetails.createdAt).toLocaleDateString('ko-KR') : null, + + )} +
+
+ + {/* 연락처 정보 */} + + + + 연락처 정보 + + + + {renderField("전화번호", vendorDetails.phone, )} + {renderField("이메일", + vendorDetails.email && ( + + {vendorDetails.email} + + ), + + )} + +
+ +

+ {vendorDetails.website ? ( +

+ ) : ( + 정보 없음 + )} +

+
+ + {renderField("주소", + vendorDetails.address && , + + )} +
+
+ + {/* 대표자 정보 */} + {(vendorDetails.representativeName || vendorDetails.representativeEmail || vendorDetails.representativePhone) && ( + + + + 대표자 정보 + + + + {renderField("대표자명", vendorDetails.representativeName, )} + {renderField("대표자 이메일", + vendorDetails.representativeEmail && ( + + {vendorDetails.representativeEmail} + + ), + + )} + {renderField("대표자 전화번호", vendorDetails.representativePhone, )} + {renderField("대표자 생년월일", vendorDetails.representativeBirth, )} + + + )} + + {/* 신용평가 정보 */} + {(vendorDetails.creditAgency || vendorDetails.creditRating || vendorDetails.cashFlowRating) && ( + + + + + 신용평가 정보 + + + + {renderField("신용평가기관", + vendorDetails.creditAgency && creditAgencyMap[vendorDetails.creditAgency] || vendorDetails.creditAgency, + + )} + + {vendorDetails.creditRating && ( +
+ +
+ + {vendorDetails.creditRating} + +
+
+ )} + + {vendorDetails.cashFlowRating && ( +
+ +
+ + {vendorDetails.cashFlowRating} + +
+
+ )} +
+
+ )} + + {/* 제공 서비스/품목 */} + {vendorDetails.items && ( + + + + + 제공 서비스/품목 + + + +
+ {vendorDetails.items} +
+
+
+ )} + + {/* 등록된 연락처 */} + {contacts.length > 0 && ( + + + + + 등록된 연락처 ({contacts.length}개) + + + +
+ {contacts.map((contact: any, index: number) => ( +
+
+ {contact.contactName} + {contact.isPrimary && ( + 주 담당자 + )} +
+ {contact.contactPosition && ( +

+ {contact.contactPosition} +

+ )} +
+ {contact.contactEmail && ( + + )} + {contact.contactPhone && ( +
+ + {contact.contactPhone} +
+ )} +
+
+ ))} +
+
+
+ )} + + {/* 첨부파일 정보 */} + {attachments.length > 0 && ( + + + + + 첨부파일 ({attachments.length}개) + + + +
+ {attachments.map((attachment: any, index: number) => ( +
+
+ + {attachment.fileName} +
+
+ + {attachment.attachmentType || 'GENERAL'} + + {attachment.createdAt && ( + + {new Date(attachment.createdAt).toLocaleDateString('ko-KR')} + + )} +
+
+ ))} +
+
+
+ )} +
+
+ ) +} \ No newline at end of file diff --git a/db/schema/SOAP/soap.ts b/db/schema/SOAP/soap.ts index 7a16b50a..c3e7a0b5 100644 --- a/db/schema/SOAP/soap.ts +++ b/db/schema/SOAP/soap.ts @@ -8,8 +8,8 @@ export const soapLogs = soapSchema.table("soap_logs", { direction: varchar({ length: 20 }).notNull(), system: varchar({ length: 50 }).notNull(), interface: varchar({ length: 100 }).notNull(), - startedAt: timestamp().notNull(), - endedAt: timestamp(), + startedAt: timestamp({ withTimezone: true }).notNull(), + endedAt: timestamp({ withTimezone: true }), isSuccess: boolean().default(false).notNull(), requestData: text(), responseData: text(), -- cgit v1.2.3