summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/[lng]/evcp/(evcp)/vendors/[id]/info/basic/page.tsx23
-rw-r--r--app/[lng]/evcp/(evcp)/vendors/[id]/info/basic/text-utils.tsx131
-rw-r--r--app/[lng]/evcp/(evcp)/vendors/[id]/info/basic/vendor-basic-info.tsx126
-rw-r--r--app/[lng]/evcp/(evcp)/vendors/[id]/info/basic/vendor-evcp-info.tsx396
-rw-r--r--db/schema/SOAP/soap.ts4
5 files changed, 614 insertions, 66 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
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 (
<div className="space-y-6">
<div className="text-center py-12">
@@ -32,8 +37,16 @@ export default async function VendorBasicPage(props: VendorBasicPageProps) {
}
return (
- <div className="space-y-6">
- <VendorBasicInfo vendorDetails={vendorDetails} />
+ <div className="space-y-8">
+ {/* eVCP 벤더 정보 */}
+ {evcpVendorDetails && (
+ <VendorEvCpInfo vendorDetails={evcpVendorDetails} />
+ )}
+
+ {/* Oracle 벤더 정보 */}
+ {oracleVendorDetails && (
+ <VendorBasicInfo vendorDetails={oracleVendorDetails} />
+ )}
</div>
)
} \ 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 <span className="text-muted-foreground">-</span>
+
+ if (text.length <= maxLength) {
+ return <span>{text}</span>
+ }
+
+ const truncated = text.slice(0, maxLength) + "..."
+
+ if (!showTooltip) {
+ return <span>{truncated}</span>
+ }
+
+ return (
+ <TooltipProvider>
+ <Tooltip>
+ <TooltipTrigger asChild>
+ <span className="cursor-help border-b border-dotted border-gray-400">
+ {truncated}
+ </span>
+ </TooltipTrigger>
+ <TooltipContent className="max-w-xs">
+ <p className="whitespace-pre-wrap">{text}</p>
+ </TooltipContent>
+ </Tooltip>
+ </TooltipProvider>
+ )
+}
+
+export function ExpandableText({
+ text,
+ maxLength = 100,
+ className = ""
+}: {
+ text: string | null
+ maxLength?: number
+ className?: string
+}) {
+ const [isExpanded, setIsExpanded] = useState(false)
+
+ if (!text) return <span className="text-muted-foreground">-</span>
+
+ if (text.length <= maxLength) {
+ return <span className={className}>{text}</span>
+ }
+
+ return (
+ <Collapsible open={isExpanded} onOpenChange={setIsExpanded}>
+ <div className={className}>
+ <CollapsibleTrigger asChild>
+ <button className="text-left w-full group">
+ <span className="whitespace-pre-wrap">
+ {isExpanded ? text : text.slice(0, maxLength) + "..."}
+ </span>
+ <span className="inline-flex items-center ml-2 text-blue-600 hover:text-blue-800">
+ {isExpanded ? (
+ <>
+ <ChevronUp className="w-3 h-3" />
+ <span className="text-xs ml-1">접기</span>
+ </>
+ ) : (
+ <>
+ <ChevronDown className="w-3 h-3" />
+ <span className="text-xs ml-1">더보기</span>
+ </>
+ )}
+ </span>
+ </button>
+ </CollapsibleTrigger>
+ </div>
+ </Collapsible>
+ )
+}
+
+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 <span className="text-muted-foreground">-</span>
+ }
+
+ return (
+ <div className="space-y-1">
+ {postalCode && (
+ <div className="text-xs text-muted-foreground">
+ 우편번호: {postalCode}
+ </div>
+ )}
+ {address && (
+ <div className="font-medium break-words">
+ {address}
+ </div>
+ )}
+ {addressDetail && (
+ <div className="text-sm text-muted-foreground break-words">
+ {addressDetail}
+ </div>
+ )}
+ {addressEng && (
+ <div className="text-sm text-muted-foreground break-words italic">
+ {addressEng}
+ </div>
+ )}
+ </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
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 (
<>
{/* 헤더 */}
- <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)}
+ <Card className="border-l-4 border-l-orange-500">
+ <CardHeader>
+ <div className="flex items-center justify-between">
+ <div className="min-w-0 flex-1">
+ <CardTitle className="text-2xl font-bold tracking-tight break-words flex items-center gap-2">
+ <Building2 className="w-6 h-6 text-orange-600" />
+ {editData.VNDRNM_1 || '업체명 없음'}
+ <Badge variant="outline" className="text-xs bg-orange-50 text-orange-700">Oracle</Badge>
+ </CardTitle>
+ <CardDescription className="text-base">
+ 벤더 코드: {editData.VNDRCD}
+ </CardDescription>
+ </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>
-
- {/* 액션 버튼들 */}
- <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>
- </>
- )}
+ </CardHeader>
+ <CardContent>
+ <div className="text-sm text-muted-foreground">
+ Oracle 시스템에서 연동된 협력업체 정보입니다. 수정 시 Oracle에 반영됩니다.
</div>
- </div>
- </div>
-
- <Separator />
+ </CardContent>
+ </Card>
<div className="grid gap-6 md:grid-cols-2">
{/* 기본 정보 */}
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<string, string> = {
+ NICE: "NICE평가정보",
+ KIS: "KIS (한국신용평가)",
+ KED: "KED (한국기업데이터)",
+ SCI: "SCI평가정보",
+}
+
+// 사업규모 표시 개선
+const businessSizeMap: Record<string, { label: string; color: string }> = {
+ "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 <Badge variant="default" className="bg-green-100 text-green-800"><CheckCircle className="w-3 h-3 mr-1" />활성</Badge>
+ case 'INACTIVE':
+ return <Badge variant="secondary" className="bg-gray-100 text-gray-600"><XCircle className="w-3 h-3 mr-1" />비활성</Badge>
+ case 'PENDING_REVIEW':
+ return <Badge variant="outline" className="bg-yellow-50 text-yellow-700 border-yellow-300">검토중</Badge>
+ case 'APPROVED':
+ return <Badge variant="default" className="bg-blue-100 text-blue-800">승인됨</Badge>
+ case 'BLACKLISTED':
+ return <Badge variant="destructive">거래금지</Badge>
+ default:
+ return <Badge variant="outline">{status}</Badge>
+ }
+ }
+
+ // 신용등급 색상 결정
+ 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 (
+ <div className="space-y-1">
+ <label className="text-sm font-medium text-muted-foreground flex items-center gap-1">
+ {icon}
+ {label}
+ </label>
+ <div className="text-sm">
+ {value}
+ </div>
+ </div>
+ )
+ }
+
+ return (
+ <div className="space-y-6">
+ {/* 헤더 */}
+ <Card className="border-l-4 border-l-blue-500">
+ <CardHeader>
+ <div className="flex items-center justify-between">
+ <div className="min-w-0 flex-1">
+ <CardTitle className="text-2xl font-bold tracking-tight break-words flex items-center gap-2">
+ <Building2 className="w-6 h-6 text-blue-600" />
+ {vendorDetails.vendorName || '업체명 없음'}
+ <Badge variant="outline" className="text-xs bg-blue-50 text-blue-700 border-blue-300">
+ eVCP
+ </Badge>
+ </CardTitle>
+ <CardDescription className="flex items-center gap-4 mt-2">
+ <span>벤더 코드: {vendorDetails.vendorCode || 'N/A'}</span>
+ <span>사업자번호: {vendorDetails.taxId || 'N/A'}</span>
+ </CardDescription>
+ </div>
+ <div className="flex items-center space-x-2">
+ {getStatusBadge(vendorDetails.status || 'ACTIVE')}
+ </div>
+ </div>
+ </CardHeader>
+ </Card>
+
+ <div className="grid gap-6 md:grid-cols-2">
+ {/* 기본 정보 */}
+ <Card>
+ <CardHeader>
+ <CardTitle className="text-lg flex items-center gap-2">
+ 기본 정보
+ </CardTitle>
+ </CardHeader>
+ <CardContent className="space-y-4">
+ {renderField("업체명", vendorDetails.vendorName)}
+ {renderField("업체 코드", vendorDetails.vendorCode, <Hash className="w-3 h-3" />)}
+ {renderField("사업자등록번호", vendorDetails.taxId, <FileText className="w-3 h-3" />)}
+ {renderField("법인등록번호", vendorDetails.corporateRegistrationNumber, <FileText className="w-3 h-3" />)}
+ {renderField("국가", vendorDetails.country, <MapPin className="w-3 h-3" />)}
+
+ {/* 사업규모 */}
+ {vendorDetails.businessSize && (
+ <div className="space-y-1">
+ <label className="text-sm font-medium text-muted-foreground flex items-center gap-1">
+ <Briefcase className="w-3 h-3" />
+ 사업규모
+ </label>
+ <div className="text-sm">
+ <span className={`font-medium ${businessSizeMap[vendorDetails.businessSize]?.color || 'text-gray-600'}`}>
+ {businessSizeMap[vendorDetails.businessSize]?.label || vendorDetails.businessSize}
+ </span>
+ </div>
+ </div>
+ )}
+
+ {/* 등록일 */}
+ {renderField("등록일",
+ vendorDetails.createdAt ? new Date(vendorDetails.createdAt).toLocaleDateString('ko-KR') : null,
+ <Calendar className="w-3 h-3" />
+ )}
+ </CardContent>
+ </Card>
+
+ {/* 연락처 정보 */}
+ <Card>
+ <CardHeader>
+ <CardTitle className="text-lg flex items-center gap-2">
+ 연락처 정보
+ </CardTitle>
+ </CardHeader>
+ <CardContent className="space-y-4">
+ {renderField("전화번호", vendorDetails.phone, <Phone className="w-3 h-3" />)}
+ {renderField("이메일",
+ vendorDetails.email && (
+ <a
+ href={`mailto:${vendorDetails.email}`}
+ className="text-blue-600 hover:underline break-all"
+ >
+ {vendorDetails.email}
+ </a>
+ ),
+ <Mail className="w-3 h-3" />
+ )}
+
+ <div>
+ <label className="text-sm font-medium text-muted-foreground">웹사이트</label>
+ <p className="text-sm break-words">
+ {vendorDetails.website ? (
+ <div className="flex items-center gap-1">
+ <Globe className="w-3 h-3" />
+ <a
+ href={vendorDetails.website.startsWith('http') ? vendorDetails.website : `https://${vendorDetails.website}`}
+ target="_blank"
+ rel="noopener noreferrer"
+ className="text-blue-600 hover:underline break-all"
+ >
+ {vendorDetails.website}
+ </a>
+ </div>
+ ) : (
+ <span className="text-muted-foreground">정보 없음</span>
+ )}
+ </p>
+ </div>
+
+ {renderField("주소",
+ vendorDetails.address && <AddressDisplay address={vendorDetails.address} />,
+ <MapPin className="w-3 h-3" />
+ )}
+ </CardContent>
+ </Card>
+
+ {/* 대표자 정보 */}
+ {(vendorDetails.representativeName || vendorDetails.representativeEmail || vendorDetails.representativePhone) && (
+ <Card>
+ <CardHeader>
+ <CardTitle className="text-lg flex items-center gap-2">
+ 대표자 정보
+ </CardTitle>
+ </CardHeader>
+ <CardContent className="space-y-4">
+ {renderField("대표자명", vendorDetails.representativeName, <User className="w-3 h-3" />)}
+ {renderField("대표자 이메일",
+ vendorDetails.representativeEmail && (
+ <a
+ href={`mailto:${vendorDetails.representativeEmail}`}
+ className="text-blue-600 hover:underline break-all"
+ >
+ {vendorDetails.representativeEmail}
+ </a>
+ ),
+ <Mail className="w-3 h-3" />
+ )}
+ {renderField("대표자 전화번호", vendorDetails.representativePhone, <Phone className="w-3 h-3" />)}
+ {renderField("대표자 생년월일", vendorDetails.representativeBirth, <Calendar className="w-3 h-3" />)}
+ </CardContent>
+ </Card>
+ )}
+
+ {/* 신용평가 정보 */}
+ {(vendorDetails.creditAgency || vendorDetails.creditRating || vendorDetails.cashFlowRating) && (
+ <Card>
+ <CardHeader>
+ <CardTitle className="text-lg flex items-center gap-2">
+ <Award className="w-5 h-5" />
+ 신용평가 정보
+ </CardTitle>
+ </CardHeader>
+ <CardContent className="space-y-4">
+ {renderField("신용평가기관",
+ vendorDetails.creditAgency && creditAgencyMap[vendorDetails.creditAgency] || vendorDetails.creditAgency,
+ <Shield className="w-3 h-3" />
+ )}
+
+ {vendorDetails.creditRating && (
+ <div className="space-y-1">
+ <label className="text-sm font-medium text-muted-foreground flex items-center gap-1">
+ <Star className="w-3 h-3" />
+ 신용등급
+ </label>
+ <div className="text-sm">
+ <Badge className={`${getCreditRatingColor(vendorDetails.creditRating)} border-0`}>
+ {vendorDetails.creditRating}
+ </Badge>
+ </div>
+ </div>
+ )}
+
+ {vendorDetails.cashFlowRating && (
+ <div className="space-y-1">
+ <label className="text-sm font-medium text-muted-foreground flex items-center gap-1">
+ <TrendingUp className="w-3 h-3" />
+ 현금흐름등급
+ </label>
+ <div className="text-sm">
+ <Badge variant="outline" className="bg-green-50 text-green-700 border-green-300">
+ {vendorDetails.cashFlowRating}
+ </Badge>
+ </div>
+ </div>
+ )}
+ </CardContent>
+ </Card>
+ )}
+
+ {/* 제공 서비스/품목 */}
+ {vendorDetails.items && (
+ <Card className="md:col-span-2">
+ <CardHeader>
+ <CardTitle className="text-lg flex items-center gap-2">
+ <Briefcase className="w-5 h-5" />
+ 제공 서비스/품목
+ </CardTitle>
+ </CardHeader>
+ <CardContent>
+ <div className="text-sm whitespace-pre-wrap bg-gray-50 p-3 rounded-md border">
+ {vendorDetails.items}
+ </div>
+ </CardContent>
+ </Card>
+ )}
+
+ {/* 등록된 연락처 */}
+ {contacts.length > 0 && (
+ <Card className="md:col-span-2">
+ <CardHeader>
+ <CardTitle className="text-lg flex items-center gap-2">
+ <Users className="w-5 h-5" />
+ 등록된 연락처 ({contacts.length}개)
+ </CardTitle>
+ </CardHeader>
+ <CardContent>
+ <div className="grid gap-3 md:grid-cols-2">
+ {contacts.map((contact: any, index: number) => (
+ <div key={contact.id || index} className="p-3 bg-gray-50 rounded-md border">
+ <div className="flex items-center justify-between mb-2">
+ <span className="font-medium">{contact.contactName}</span>
+ {contact.isPrimary && (
+ <Badge variant="default" className="text-xs">주 담당자</Badge>
+ )}
+ </div>
+ {contact.contactPosition && (
+ <p className="text-sm text-muted-foreground mb-1">
+ {contact.contactPosition}
+ </p>
+ )}
+ <div className="space-y-1 text-sm">
+ {contact.contactEmail && (
+ <div className="flex items-center gap-1">
+ <Mail className="w-3 h-3" />
+ <a
+ href={`mailto:${contact.contactEmail}`}
+ className="text-blue-600 hover:underline"
+ >
+ {contact.contactEmail}
+ </a>
+ </div>
+ )}
+ {contact.contactPhone && (
+ <div className="flex items-center gap-1">
+ <Phone className="w-3 h-3" />
+ <span>{contact.contactPhone}</span>
+ </div>
+ )}
+ </div>
+ </div>
+ ))}
+ </div>
+ </CardContent>
+ </Card>
+ )}
+
+ {/* 첨부파일 정보 */}
+ {attachments.length > 0 && (
+ <Card className="md:col-span-2">
+ <CardHeader>
+ <CardTitle className="text-lg flex items-center gap-2">
+ <FileText className="w-5 h-5" />
+ 첨부파일 ({attachments.length}개)
+ </CardTitle>
+ </CardHeader>
+ <CardContent>
+ <div className="space-y-2">
+ {attachments.map((attachment: any, index: number) => (
+ <div key={attachment.id || index} className="flex items-center justify-between p-2 bg-gray-50 rounded border">
+ <div className="flex items-center gap-2">
+ <FileText className="w-4 h-4 text-gray-500" />
+ <span className="text-sm font-medium">{attachment.fileName}</span>
+ </div>
+ <div className="flex items-center gap-2">
+ <Badge variant="outline" className="text-xs">
+ {attachment.attachmentType || 'GENERAL'}
+ </Badge>
+ {attachment.createdAt && (
+ <span className="text-xs text-muted-foreground">
+ {new Date(attachment.createdAt).toLocaleDateString('ko-KR')}
+ </span>
+ )}
+ </div>
+ </div>
+ ))}
+ </div>
+ </CardContent>
+ </Card>
+ )}
+ </div>
+ </div>
+ )
+} \ 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(),