diff options
Diffstat (limited to 'lib/vendor-registration-status')
| -rw-r--r-- | lib/vendor-registration-status/repository.ts | 165 | ||||
| -rw-r--r-- | lib/vendor-registration-status/service.ts | 260 | ||||
| -rw-r--r-- | lib/vendor-registration-status/vendor-registration-status-view.tsx | 470 |
3 files changed, 895 insertions, 0 deletions
diff --git a/lib/vendor-registration-status/repository.ts b/lib/vendor-registration-status/repository.ts new file mode 100644 index 00000000..f9c3d63f --- /dev/null +++ b/lib/vendor-registration-status/repository.ts @@ -0,0 +1,165 @@ +import { db } from "@/db"
+import {
+ vendorBusinessContacts,
+ vendorAdditionalInfo,
+ vendors
+} from "@/db/schema"
+import { eq, and, inArray } from "drizzle-orm"
+
+// 업무담당자 정보 타입
+export interface VendorBusinessContact {
+ id: number
+ vendorId: number
+ contactType: "sales" | "design" | "delivery" | "quality" | "tax_invoice"
+ contactName: string
+ position: string
+ department: string
+ responsibility: string
+ email: string
+ createdAt: Date
+ updatedAt: Date
+}
+
+// 추가정보 타입
+export interface VendorAdditionalInfo {
+ id: number
+ vendorId: number
+ businessType?: string
+ industryType?: string
+ companySize?: string
+ revenue?: string
+ factoryEstablishedDate?: Date
+ preferredContractTerms?: string
+ createdAt: Date
+ updatedAt: Date
+}
+
+// 업무담당자 정보 조회
+export async function getBusinessContactsByVendorId(vendorId: number): Promise<VendorBusinessContact[]> {
+ try {
+ return await db
+ .select()
+ .from(vendorBusinessContacts)
+ .where(eq(vendorBusinessContacts.vendorId, vendorId))
+ .orderBy(vendorBusinessContacts.contactType)
+ } catch (error) {
+ console.error("Error fetching business contacts:", error)
+ throw new Error("업무담당자 정보를 가져오는 중 오류가 발생했습니다.")
+ }
+}
+
+// 업무담당자 정보 저장/업데이트
+export async function upsertBusinessContacts(
+ vendorId: number,
+ contacts: Omit<VendorBusinessContact, "id" | "vendorId" | "createdAt" | "updatedAt">[]
+): Promise<void> {
+ try {
+ // 기존 데이터 삭제
+ await db
+ .delete(vendorBusinessContacts)
+ .where(eq(vendorBusinessContacts.vendorId, vendorId))
+
+ // 새 데이터 삽입
+ if (contacts.length > 0) {
+ await db
+ .insert(vendorBusinessContacts)
+ .values(contacts.map(contact => ({
+ ...contact,
+ vendorId,
+ })))
+ }
+ } catch (error) {
+ console.error("Error upserting business contacts:", error)
+ throw new Error("업무담당자 정보 저장 중 오류가 발생했습니다.")
+ }
+}
+
+// 추가정보 조회
+export async function getAdditionalInfoByVendorId(vendorId: number): Promise<VendorAdditionalInfo | null> {
+ try {
+ const result = await db
+ .select()
+ .from(vendorAdditionalInfo)
+ .where(eq(vendorAdditionalInfo.vendorId, vendorId))
+ .limit(1)
+
+ return result[0] || null
+ } catch (error) {
+ console.error("Error fetching additional info:", error)
+ throw new Error("추가정보를 가져오는 중 오류가 발생했습니다.")
+ }
+}
+
+// 추가정보 저장/업데이트
+export async function upsertAdditionalInfo(
+ vendorId: number,
+ info: Omit<VendorAdditionalInfo, "id" | "vendorId" | "createdAt" | "updatedAt">
+): Promise<void> {
+ try {
+ const existing = await getAdditionalInfoByVendorId(vendorId)
+
+ if (existing) {
+ // 업데이트
+ await db
+ .update(vendorAdditionalInfo)
+ .set({
+ ...info,
+ updatedAt: new Date(),
+ })
+ .where(eq(vendorAdditionalInfo.vendorId, vendorId))
+ } else {
+ // 신규 삽입
+ await db
+ .insert(vendorAdditionalInfo)
+ .values({
+ ...info,
+ vendorId,
+ })
+ }
+ } catch (error) {
+ console.error("Error upserting additional info:", error)
+ throw new Error("추가정보 저장 중 오류가 발생했습니다.")
+ }
+}
+
+// 특정 벤더의 모든 추가정보 조회 (업무담당자 + 추가정보)
+export async function getVendorAllAdditionalData(vendorId: number) {
+ try {
+ const [businessContacts, additionalInfo] = await Promise.all([
+ getBusinessContactsByVendorId(vendorId),
+ getAdditionalInfoByVendorId(vendorId)
+ ])
+
+ return {
+ businessContacts,
+ additionalInfo
+ }
+ } catch (error) {
+ console.error("Error fetching vendor additional data:", error)
+ throw new Error("벤더 추가정보를 가져오는 중 오류가 발생했습니다.")
+ }
+}
+
+// 업무담당자 정보 삭제
+export async function deleteBusinessContactsByVendorId(vendorId: number): Promise<void> {
+ try {
+ await db
+ .delete(vendorBusinessContacts)
+ .where(eq(vendorBusinessContacts.vendorId, vendorId))
+ } catch (error) {
+ console.error("Error deleting business contacts:", error)
+ throw new Error("업무담당자 정보 삭제 중 오류가 발생했습니다.")
+ }
+}
+
+// 추가정보 삭제
+export async function deleteAdditionalInfoByVendorId(vendorId: number): Promise<void> {
+ try {
+ await db
+ .delete(vendorAdditionalInfo)
+ .where(eq(vendorAdditionalInfo.vendorId, vendorId))
+ } catch (error) {
+ console.error("Error deleting additional info:", error)
+ throw new Error("추가정보 삭제 중 오류가 발생했습니다.")
+ }
+}
diff --git a/lib/vendor-registration-status/service.ts b/lib/vendor-registration-status/service.ts new file mode 100644 index 00000000..97503a13 --- /dev/null +++ b/lib/vendor-registration-status/service.ts @@ -0,0 +1,260 @@ +import { revalidateTag, unstable_cache } from "next/cache"
+import {
+ getBusinessContactsByVendorId,
+ upsertBusinessContacts,
+ getAdditionalInfoByVendorId,
+ upsertAdditionalInfo,
+ getVendorAllAdditionalData,
+ deleteBusinessContactsByVendorId,
+ deleteAdditionalInfoByVendorId,
+ type VendorBusinessContact,
+ type VendorAdditionalInfo
+} from "./repository"
+
+// 업무담당자 정보 조회
+export async function fetchBusinessContacts(vendorId: number) {
+ return unstable_cache(
+ async () => {
+ try {
+ const contacts = await getBusinessContactsByVendorId(vendorId)
+ return {
+ success: true,
+ data: contacts,
+ }
+ } catch (error) {
+ console.error("Error in fetchBusinessContacts:", error)
+ return {
+ success: false,
+ error: error instanceof Error ? error.message : "업무담당자 정보를 가져오는 중 오류가 발생했습니다.",
+ }
+ }
+ },
+ [`business-contacts-${vendorId}`],
+ {
+ revalidate: 300, // 5분 캐시
+ tags: ["business-contacts", `vendor-${vendorId}`],
+ }
+ )()
+}
+
+// 업무담당자 정보 저장
+export async function saveBusinessContacts(
+ vendorId: number,
+ contacts: Omit<VendorBusinessContact, "id" | "vendorId" | "createdAt" | "updatedAt">[]
+) {
+ try {
+ await upsertBusinessContacts(vendorId, contacts)
+
+ // 캐시 무효화
+ revalidateTag("business-contacts")
+ revalidateTag(`vendor-${vendorId}`)
+
+ return {
+ success: true,
+ message: "업무담당자 정보가 저장되었습니다.",
+ }
+ } catch (error) {
+ console.error("Error in saveBusinessContacts:", error)
+ return {
+ success: false,
+ error: error instanceof Error ? error.message : "업무담당자 정보 저장 중 오류가 발생했습니다.",
+ }
+ }
+}
+
+// 추가정보 조회
+export async function fetchAdditionalInfo(vendorId: number) {
+ return unstable_cache(
+ async () => {
+ try {
+ const additionalInfo = await getAdditionalInfoByVendorId(vendorId)
+ return {
+ success: true,
+ data: additionalInfo,
+ }
+ } catch (error) {
+ console.error("Error in fetchAdditionalInfo:", error)
+ return {
+ success: false,
+ error: error instanceof Error ? error.message : "추가정보를 가져오는 중 오류가 발생했습니다.",
+ }
+ }
+ },
+ [`additional-info-${vendorId}`],
+ {
+ revalidate: 300, // 5분 캐시
+ tags: ["additional-info", `vendor-${vendorId}`],
+ }
+ )()
+}
+
+// 추가정보 저장
+export async function saveAdditionalInfo(
+ vendorId: number,
+ info: Omit<VendorAdditionalInfo, "id" | "vendorId" | "createdAt" | "updatedAt">
+) {
+ try {
+ await upsertAdditionalInfo(vendorId, info)
+
+ // 캐시 무효화
+ revalidateTag("additional-info")
+ revalidateTag(`vendor-${vendorId}`)
+
+ return {
+ success: true,
+ message: "추가정보가 저장되었습니다.",
+ }
+ } catch (error) {
+ console.error("Error in saveAdditionalInfo:", error)
+ return {
+ success: false,
+ error: error instanceof Error ? error.message : "추가정보 저장 중 오류가 발생했습니다.",
+ }
+ }
+}
+
+// 모든 추가정보 조회 (업무담당자 + 추가정보)
+export async function fetchAllAdditionalData(vendorId: number) {
+ return unstable_cache(
+ async () => {
+ try {
+ const data = await getVendorAllAdditionalData(vendorId)
+ return {
+ success: true,
+ data,
+ }
+ } catch (error) {
+ console.error("Error in fetchAllAdditionalData:", error)
+ return {
+ success: false,
+ error: error instanceof Error ? error.message : "벤더 추가정보를 가져오는 중 오류가 발생했습니다.",
+ }
+ }
+ },
+ [`all-additional-data-${vendorId}`],
+ {
+ revalidate: 300, // 5분 캐시
+ tags: ["business-contacts", "additional-info", `vendor-${vendorId}`],
+ }
+ )()
+}
+
+// 업무담당자 + 추가정보 한 번에 저장
+export async function saveAllAdditionalData(
+ vendorId: number,
+ data: {
+ businessContacts: Omit<VendorBusinessContact, "id" | "vendorId" | "createdAt" | "updatedAt">[]
+ additionalInfo: Omit<VendorAdditionalInfo, "id" | "vendorId" | "createdAt" | "updatedAt">
+ }
+) {
+ try {
+ // 두 작업을 순차적으로 실행
+ await Promise.all([
+ upsertBusinessContacts(vendorId, data.businessContacts),
+ upsertAdditionalInfo(vendorId, data.additionalInfo)
+ ])
+
+ // 캐시 무효화
+ revalidateTag("business-contacts")
+ revalidateTag("additional-info")
+ revalidateTag(`vendor-${vendorId}`)
+
+ return {
+ success: true,
+ message: "모든 추가정보가 저장되었습니다.",
+ }
+ } catch (error) {
+ console.error("Error in saveAllAdditionalData:", error)
+ return {
+ success: false,
+ error: error instanceof Error ? error.message : "추가정보 저장 중 오류가 발생했습니다.",
+ }
+ }
+}
+
+// 업무담당자 정보 삭제
+export async function removeBusinessContacts(vendorId: number) {
+ try {
+ await deleteBusinessContactsByVendorId(vendorId)
+
+ // 캐시 무효화
+ revalidateTag("business-contacts")
+ revalidateTag(`vendor-${vendorId}`)
+
+ return {
+ success: true,
+ message: "업무담당자 정보가 삭제되었습니다.",
+ }
+ } catch (error) {
+ console.error("Error in removeBusinessContacts:", error)
+ return {
+ success: false,
+ error: error instanceof Error ? error.message : "업무담당자 정보 삭제 중 오류가 발생했습니다.",
+ }
+ }
+}
+
+// 추가정보 삭제
+export async function removeAdditionalInfo(vendorId: number) {
+ try {
+ await deleteAdditionalInfoByVendorId(vendorId)
+
+ // 캐시 무효화
+ revalidateTag("additional-info")
+ revalidateTag(`vendor-${vendorId}`)
+
+ return {
+ success: true,
+ message: "추가정보가 삭제되었습니다.",
+ }
+ } catch (error) {
+ console.error("Error in removeAdditionalInfo:", error)
+ return {
+ success: false,
+ error: error instanceof Error ? error.message : "추가정보 삭제 중 오류가 발생했습니다.",
+ }
+ }
+}
+
+// 입력 완성도 체크
+export async function checkAdditionalDataCompletion(vendorId: number) {
+ try {
+ const result = await fetchAllAdditionalData(vendorId)
+
+ if (!result.success || !result.data) {
+ return {
+ success: false,
+ error: "추가정보를 확인할 수 없습니다.",
+ }
+ }
+
+ const { businessContacts, additionalInfo } = result.data
+
+ // 필수 업무담당자 5개 타입이 모두 입력되었는지 체크
+ const requiredContactTypes = ["sales", "design", "delivery", "quality", "tax_invoice"]
+ const existingContactTypes = businessContacts.map(contact => contact.contactType)
+ const missingContactTypes = requiredContactTypes.filter(type => !existingContactTypes.includes(type))
+
+ // 업무담당자 완성도
+ const businessContactsComplete = missingContactTypes.length === 0
+
+ // 추가정보 완성도 (선택사항이므로 존재 여부만 체크)
+ const additionalInfoExists = !!additionalInfo
+
+ return {
+ success: true,
+ data: {
+ businessContactsComplete,
+ missingContactTypes,
+ additionalInfoExists,
+ totalCompletion: businessContactsComplete && additionalInfoExists
+ }
+ }
+ } catch (error) {
+ console.error("Error in checkAdditionalDataCompletion:", error)
+ return {
+ success: false,
+ error: error instanceof Error ? error.message : "완성도 확인 중 오류가 발생했습니다.",
+ }
+ }
+}
diff --git a/lib/vendor-registration-status/vendor-registration-status-view.tsx b/lib/vendor-registration-status/vendor-registration-status-view.tsx new file mode 100644 index 00000000..b3000f73 --- /dev/null +++ b/lib/vendor-registration-status/vendor-registration-status-view.tsx @@ -0,0 +1,470 @@ +"use client"
+
+import { useState, useEffect } from "react"
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
+import { Badge } from "@/components/ui/badge"
+import { Button } from "@/components/ui/button"
+import { Separator } from "@/components/ui/separator"
+import {
+ CheckCircle,
+ XCircle,
+ FileText,
+ Users,
+ Building2,
+ AlertCircle,
+ Eye,
+ Upload
+} from "lucide-react"
+import { DocumentStatusDialog } from "@/components/vendor-regular-registrations/document-status-dialog"
+import { AdditionalInfoDialog } from "@/components/vendor-regular-registrations/additional-info-dialog"
+import { format } from "date-fns"
+import { toast } from "sonner"
+import { fetchVendorRegistrationStatus } from "@/lib/vendor-regular-registrations/service"
+
+// 상태별 정의
+const statusConfig = {
+ audit_pass: {
+ label: "실사통과",
+ color: "bg-blue-100 text-blue-800",
+ description: "품질담당자(QM) 최종 의견에 따라 실사 통과로 결정된 상태"
+ },
+ cp_submitted: {
+ label: "CP등록",
+ color: "bg-green-100 text-green-800",
+ description: "협력업체에서 실사 통과 후 기본계약문서에 대한 답변 제출/서약 완료한 상태"
+ },
+ cp_review: {
+ label: "CP검토",
+ color: "bg-yellow-100 text-yellow-800",
+ description: "협력업체에서 제출한 CP/GTC에 대한 법무검토 의뢰한 상태"
+ },
+ cp_finished: {
+ label: "CP완료",
+ color: "bg-purple-100 text-purple-800",
+ description: "CP 답변에 대한 법무검토 완료되어 정규업체 등록 가능한 상태"
+ },
+ approval_ready: {
+ label: "조건충족",
+ color: "bg-emerald-100 text-emerald-800",
+ description: "정규업체 등록 문서/자료 접수현황에 누락이 없는 상태"
+ },
+ in_review: {
+ label: "정규등록검토",
+ color: "bg-orange-100 text-orange-800",
+ description: "구매담당자 요청에 따라 정규업체 등록 관리자가 정규업체 등록 가능여부 검토"
+ },
+ pending_approval: {
+ label: "장기미등록",
+ color: "bg-red-100 text-red-800",
+ description: "정규업체로 등록 요청되어 3개월 이내 정규업체 등록되지 않은 상태"
+ }
+}
+
+// 필수문서 목록
+const requiredDocuments = [
+ { key: "businessRegistration", label: "사업자등록증" },
+ { key: "creditEvaluation", label: "신용평가서" },
+ { key: "bankCopy", label: "통장사본" },
+ { key: "cpDocument", label: "CP문서" },
+ { key: "gtc", label: "GTC" },
+ { key: "standardSubcontract", label: "표준하도급" },
+ { key: "safetyHealth", label: "안전보건관리" },
+ { key: "ethics", label: "윤리규범준수" },
+ { key: "domesticCredit", label: "내국신용장" },
+ { key: "safetyQualification", label: "안전적격성평가" },
+]
+
+export function VendorRegistrationStatusView() {
+ const [additionalInfoDialogOpen, setAdditionalInfoDialogOpen] = useState(false)
+ const [documentDialogOpen, setDocumentDialogOpen] = useState(false)
+ const [hasSignature, setHasSignature] = useState(false)
+ const [data, setData] = useState<any>(null)
+ const [loading, setLoading] = useState(true)
+
+ // 임시로 vendorId = 1 사용 (실제로는 세션에서 가져와야 함)
+ const vendorId = 1
+
+ // 데이터 로드
+ useEffect(() => {
+ const initialLoad = async () => {
+ try {
+ const result = await fetchVendorRegistrationStatus(vendorId)
+ if (result.success) {
+ setData(result.data)
+ } else {
+ toast.error(result.error)
+ }
+ } catch {
+ toast.error("데이터 로드 중 오류가 발생했습니다.")
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ initialLoad()
+ }, [vendorId])
+
+ if (loading) {
+ return <div className="p-8 text-center">로딩 중...</div>
+ }
+
+ if (!data) {
+ return <div className="p-8 text-center">데이터를 불러올 수 없습니다.</div>
+ }
+
+ const currentStatusConfig = statusConfig[data.registration?.status as keyof typeof statusConfig] || statusConfig.audit_pass
+
+ // 미완성 항목 계산
+ const missingDocuments = requiredDocuments.filter(
+ doc => !data.documentStatus[doc.key as keyof typeof data.documentStatus]
+ )
+
+ // Document Status Dialog에 전달할 registration 데이터 구성
+ const registrationForDialog: any = {
+ id: data.registration?.id || 0,
+ vendorId: data.vendor.id,
+ companyName: data.vendor.companyName,
+ businessNumber: data.vendor.businessNumber,
+ representative: data.vendor.representative || "",
+ potentialCode: data.registration?.potentialCode || "",
+ status: data.registration?.status || "audit_pass",
+ majorItems: "[]", // 빈 JSON 문자열
+ establishmentDate: data.vendor.createdAt || new Date(),
+ registrationRequestDate: data.registration?.registrationRequestDate,
+ assignedDepartment: data.registration?.assignedDepartment,
+ assignedDepartmentCode: data.registration?.assignedDepartmentCode,
+ assignedUser: data.registration?.assignedUser,
+ assignedUserCode: data.registration?.assignedUserCode,
+ remarks: data.registration?.remarks,
+ additionalInfo: data.additionalInfo,
+ documentSubmissions: data.documentStatus, // documentSubmissions를 documentStatus로 설정
+ contractAgreements: [],
+ documentSubmissionsStatus: data.documentStatus,
+ contractAgreementsStatus: {
+ cpDocument: data.documentStatus.cpDocument,
+ gtc: data.documentStatus.gtc,
+ standardSubcontract: data.documentStatus.standardSubcontract,
+ safetyHealth: data.documentStatus.safetyHealth,
+ ethics: data.documentStatus.ethics,
+ domesticCredit: data.documentStatus.domesticCredit,
+ },
+ createdAt: data.registration?.createdAt || new Date(),
+ updatedAt: data.registration?.updatedAt || new Date(),
+ }
+
+ const handleSignatureUpload = () => {
+ // TODO: 서명/직인 업로드 기능 구현
+ setHasSignature(true)
+ toast.success("서명/직인이 등록되었습니다.")
+ }
+
+ const handleAdditionalInfoSave = () => {
+ // 데이터 새로고침
+ loadData()
+ }
+
+ const loadData = async () => {
+ try {
+ const result = await fetchVendorRegistrationStatus(vendorId)
+ if (result.success) {
+ setData(result.data)
+ } else {
+ toast.error(result.error)
+ }
+ } catch {
+ toast.error("데이터 로드 중 오류가 발생했습니다.")
+ }
+ }
+
+ return (
+ <div className="space-y-6">
+ {/* 헤더 섹션 */}
+ <div className="space-y-4">
+ <div className="flex items-center justify-between">
+ <div>
+ <h1 className="text-3xl font-bold">정규업체 등록관리 현황</h1>
+ <p className="text-muted-foreground">
+ {data.registration?.potentialCode || "미등록"} | {data.vendor.companyName}
+ </p>
+ <p className="text-sm text-muted-foreground mt-1">
+ 정규업체 등록 진행현황을 확인하세요.
+ </p>
+ </div>
+ <Badge className={currentStatusConfig.color} variant="secondary">
+ {currentStatusConfig.label}
+ </Badge>
+ </div>
+ </div>
+
+ {/* 회사 서명/직인 등록 */}
+ <Card>
+ <CardHeader>
+ <CardTitle className="flex items-center gap-2">
+ <FileText className="w-5 h-5" />
+ 회사 서명/직인 등록
+ <Badge variant="destructive" className="text-xs">필수</Badge>
+ </CardTitle>
+ </CardHeader>
+ <CardContent>
+ {hasSignature ? (
+ <div className="flex items-center gap-3 p-4 border rounded-lg bg-green-50">
+ <CheckCircle className="w-5 h-5 text-green-600" />
+ <span className="text-green-800">서명/직인이 등록되었습니다.</span>
+ </div>
+ ) : (
+ <Button
+ onClick={handleSignatureUpload}
+ className="w-full h-20 border-2 border-dashed border-muted-foreground/25 bg-muted/25"
+ variant="outline"
+ >
+ <div className="text-center">
+ <Upload className="w-6 h-6 mx-auto mb-2" />
+ <span>서명/직인 등록하기</span>
+ </div>
+ </Button>
+ )}
+ </CardContent>
+ </Card>
+
+ {/* 기본 정보 */}
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
+ <Card>
+ <CardHeader>
+ <CardTitle className="flex items-center gap-2">
+ <Building2 className="w-5 h-5" />
+ 업체 정보
+ </CardTitle>
+ </CardHeader>
+ <CardContent className="space-y-3">
+ <div className="grid grid-cols-2 gap-4">
+ <div>
+ <span className="text-sm font-medium text-gray-600">업체명:</span>
+ <p className="mt-1">{data.vendor.companyName}</p>
+ </div>
+ <div>
+ <span className="text-sm font-medium text-gray-600">사업자번호:</span>
+ <p className="mt-1">{data.vendor.businessNumber}</p>
+ </div>
+ </div>
+ <div className="grid grid-cols-2 gap-4">
+ <div>
+ <span className="text-sm font-medium text-gray-600">업체구분:</span>
+ <p className="mt-1">{data.registration ? "정규업체" : "잠재업체"}</p>
+ </div>
+ <div>
+ <span className="text-sm font-medium text-gray-600">eVCP 가입:</span>
+ <p className="mt-1">{data.vendor.createdAt ? format(new Date(data.vendor.createdAt), "yyyy.MM.dd") : "-"}</p>
+ </div>
+ </div>
+ </CardContent>
+ </Card>
+
+ <Card>
+ <CardHeader>
+ <CardTitle className="flex items-center gap-2">
+ <Users className="w-5 h-5" />
+ 담당자 정보
+ </CardTitle>
+ </CardHeader>
+ <CardContent className="space-y-3">
+ <div className="grid grid-cols-2 gap-4">
+ <div>
+ <span className="text-sm font-medium text-gray-600">SHI 담당자:</span>
+ <p className="mt-1">{data.registration?.assignedDepartment || "-"} {data.registration?.assignedUser || "-"}</p>
+ </div>
+ <div>
+ <span className="text-sm font-medium text-gray-600">진행상태:</span>
+ <Badge className={`mt-1 ${currentStatusConfig.color}`} variant="secondary">
+ {currentStatusConfig.label}
+ </Badge>
+ </div>
+ </div>
+ <div className="grid grid-cols-2 gap-4">
+ <div>
+ <span className="text-sm font-medium text-gray-600">상태변경일:</span>
+ <p className="mt-1">{data.registration?.updatedAt ? format(new Date(data.registration.updatedAt), "yyyy.MM.dd") : "-"}</p>
+ </div>
+ </div>
+ </CardContent>
+ </Card>
+ </div>
+
+ {/* 미완항목 */}
+ {missingDocuments.length > 0 && (
+ <Card className="border-red-200 bg-red-50">
+ <CardHeader>
+ <CardTitle className="flex items-center gap-2 text-red-800">
+ <AlertCircle className="w-5 h-5" />
+ 미완항목
+ </CardTitle>
+ </CardHeader>
+ <CardContent>
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
+ {data.incompleteItemsCount.documents > 0 && (
+ <div className="flex items-center justify-between p-3 bg-white rounded-lg border">
+ <span className="text-sm font-medium">미제출문서</span>
+ <Badge variant="destructive">{data.incompleteItemsCount.documents} 건</Badge>
+ </div>
+ )}
+ {!data.documentStatus.auditResult && (
+ <div className="flex items-center justify-between p-3 bg-white rounded-lg border">
+ <span className="text-sm font-medium">실사결과</span>
+ <Badge variant="destructive">미실시</Badge>
+ </div>
+ )}
+ {data.incompleteItemsCount.additionalInfo > 0 && (
+ <div className="flex items-center justify-between p-3 bg-white rounded-lg border">
+ <span className="text-sm font-medium">추가정보</span>
+ <Badge variant="destructive">미입력</Badge>
+ </div>
+ )}
+ </div>
+ </CardContent>
+ </Card>
+ )}
+
+ {/* 상세 진행현황 */}
+ <Card>
+ <CardHeader>
+ <CardTitle>상세 진행현황</CardTitle>
+ </CardHeader>
+ <CardContent>
+ <div className="space-y-6">
+ {/* 기본 진행상황 */}
+ <div className="grid grid-cols-4 gap-4 text-center">
+ <div className="space-y-2">
+ <div className="text-sm font-medium text-gray-600">PQ 제출</div>
+ <div className="text-lg font-semibold">
+ {data.pqSubmission ? (
+ <div className="flex items-center justify-center gap-2">
+ <CheckCircle className="w-5 h-5 text-green-600" />
+ {format(new Date(data.pqSubmission.createdAt), "yyyy.MM.dd")}
+ </div>
+ ) : (
+ <div className="flex items-center justify-center gap-2">
+ <XCircle className="w-5 h-5 text-red-500" />
+ 미제출
+ </div>
+ )}
+ </div>
+ </div>
+ <div className="space-y-2">
+ <div className="text-sm font-medium text-gray-600">실사 통과</div>
+ <div className="text-lg font-semibold">
+ {data.auditPassed ? (
+ <div className="flex items-center justify-center gap-2">
+ <CheckCircle className="w-5 h-5 text-green-600" />
+ 통과
+ </div>
+ ) : (
+ <div className="flex items-center justify-center gap-2">
+ <XCircle className="w-5 h-5 text-red-500" />
+ 미통과
+ </div>
+ )}
+ </div>
+ </div>
+ <div className="space-y-2">
+ <div className="text-sm font-medium text-gray-600">문서 현황</div>
+ <Button
+ onClick={() => setDocumentDialogOpen(true)}
+ variant="outline"
+ size="sm"
+ className="flex items-center gap-2"
+ >
+ <Eye className="w-4 h-4" />
+ 확인하기
+ </Button>
+ </div>
+ <div className="space-y-2">
+ <div className="text-sm font-medium text-gray-600">추가정보</div>
+ <Button
+ onClick={() => setAdditionalInfoDialogOpen(true)}
+ variant={data.additionalInfo ? "outline" : "default"}
+ size="sm"
+ className="flex items-center gap-2"
+ >
+ <FileText className="w-4 h-4" />
+ {data.additionalInfo ? "수정하기" : "등록하기"}
+ </Button>
+ </div>
+ </div>
+
+ <Separator />
+
+ {/* 필수문서 상태 */}
+ <div>
+ <h4 className="text-sm font-medium text-gray-600 mb-4">필수문서 제출 현황</h4>
+ <div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-3">
+ {requiredDocuments.map((doc) => {
+ const isSubmitted = data.documentStatus[doc.key as keyof typeof data.documentStatus]
+ return (
+ <div
+ key={doc.key}
+ className={`p-3 rounded-lg border text-center ${
+ isSubmitted
+ ? 'bg-green-50 border-green-200'
+ : 'bg-red-50 border-red-200'
+ }`}
+ >
+ <div className="flex items-center justify-center mb-2">
+ {isSubmitted ? (
+ <CheckCircle className="w-5 h-5 text-green-600" />
+ ) : (
+ <XCircle className="w-5 h-5 text-red-500" />
+ )}
+ </div>
+ <div className="text-xs font-medium">{doc.label}</div>
+ {isSubmitted && (
+ <Button
+ variant="ghost"
+ size="sm"
+ className="mt-2 h-6 text-xs"
+ >
+ <Eye className="w-3 h-3 mr-1" />
+ 보기
+ </Button>
+ )}
+ </div>
+ )
+ })}
+ </div>
+ </div>
+ </div>
+ </CardContent>
+ </Card>
+
+ {/* 상태 설명 */}
+ <Card>
+ <CardHeader>
+ <CardTitle>현재 상태 안내</CardTitle>
+ </CardHeader>
+ <CardContent>
+ <div className="flex items-start gap-3">
+ <Badge className={currentStatusConfig.color} variant="secondary">
+ {currentStatusConfig.label}
+ </Badge>
+ <p className="text-sm text-muted-foreground">
+ {currentStatusConfig.description}
+ </p>
+ </div>
+ </CardContent>
+ </Card>
+
+ {/* 문서 현황 Dialog */}
+ <DocumentStatusDialog
+ open={documentDialogOpen}
+ onOpenChange={setDocumentDialogOpen}
+ registration={registrationForDialog}
+ />
+
+ {/* 추가정보 입력 Dialog */}
+ <AdditionalInfoDialog
+ open={additionalInfoDialogOpen}
+ onOpenChange={setAdditionalInfoDialogOpen}
+ vendorId={vendorId}
+ onSave={handleAdditionalInfoSave}
+ />
+ </div>
+ )
+}
|
