summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-09-01 09:09:15 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-09-01 09:09:15 +0000
commit089c70ffbe2303ab5e2611a152ddd3aed0e6e718 (patch)
tree1ce91012dba99495dde5eb8b414b2732197bfec4 /lib
parent69648a25c2ac62bbc3354b3a0e41abc932273b7c (diff)
(최겸) 구매 pq, 기본정보 수정
Diffstat (limited to 'lib')
-rw-r--r--lib/vendor-basic-info/actions.ts193
-rw-r--r--lib/vendor-basic-info/basic-info-client.tsx1650
-rw-r--r--lib/vendor-basic-info/constants.ts1
-rw-r--r--lib/vendor-basic-info/types.ts180
-rw-r--r--lib/vendor-regular-registrations/table/vendor-regular-registrations-table-columns.tsx1
-rw-r--r--lib/vendors/service.ts11
6 files changed, 2031 insertions, 5 deletions
diff --git a/lib/vendor-basic-info/actions.ts b/lib/vendor-basic-info/actions.ts
new file mode 100644
index 00000000..8428deb9
--- /dev/null
+++ b/lib/vendor-basic-info/actions.ts
@@ -0,0 +1,193 @@
+"use server";
+
+import { getVendorBasicInfo } from "@/lib/vendors/service";
+import { VendorFormData } from "./types";
+import { getPQDataByVendorId } from "@/lib/pq/service";
+import db from "@/db/db"
+import { vendorPQSubmissions, vendorPqCriteriaAnswers, pqCriterias, vendorCriteriaAttachments, vendorAdditionalInfo } from "@/db/schema"
+import { vendorBusinessContacts } from "@/db/schema"
+
+import { eq } from "drizzle-orm";
+
+/**
+ * 벤더 기본정보를 가져오는 서버 액션
+ */
+export async function getVendorData(vendorId: string) {
+ try {
+ const id = parseInt(vendorId);
+ if (isNaN(id)) {
+ return null;
+ }
+
+ const vendorData = await getVendorBasicInfo(id);
+ return vendorData;
+ } catch (error) {
+ console.error("Error in getVendorData:", error);
+ return null;
+ }
+}
+
+/**
+ * 벤더의 PQ 데이터를 가져오는 서버 액션
+ */
+export async function getVendorPQData(vendorId: string) {
+ try {
+ const id = parseInt(vendorId);
+ if (isNaN(id)) {
+ return null;
+ }
+
+ const pqData = await getPQDataByVendorId(id);
+ return pqData;
+ } catch (error) {
+ console.error("Error in getVendorPQData:", error);
+ return null;
+ }
+}
+
+/**
+ * 벤더의 PQ 제출 데이터와 답변을 가져오는 서버 액션
+ */
+export async function getVendorPQSubmissionData(vendorId: string) {
+ try {
+ const id = parseInt(vendorId);
+ if (isNaN(id)) {
+ return null;
+ }
+
+ // 벤더의 모든 PQ 제출 데이터 조회
+ const submissions = await db
+ .select()
+ .from(vendorPQSubmissions)
+ .where(eq(vendorPQSubmissions.vendorId, id));
+
+ if (submissions.length === 0) {
+ return null;
+ }
+
+ // 각 제출에 대한 답변 데이터 조회
+ const submissionData = await Promise.all(
+ submissions.map(async (submission) => {
+ const answers = await db
+ .select({
+ id: vendorPqCriteriaAnswers.id,
+ vendorId: vendorPqCriteriaAnswers.vendorId,
+ criteriaId: vendorPqCriteriaAnswers.criteriaId,
+ projectId: vendorPqCriteriaAnswers.projectId,
+ answer: vendorPqCriteriaAnswers.answer,
+ shiComment: vendorPqCriteriaAnswers.shiComment,
+ vendorReply: vendorPqCriteriaAnswers.vendorReply,
+ createdAt: vendorPqCriteriaAnswers.createdAt,
+ updatedAt: vendorPqCriteriaAnswers.updatedAt,
+ criteriaCode: pqCriterias.code,
+ checkPoint: pqCriterias.checkPoint,
+ description: pqCriterias.description,
+ groupName: pqCriterias.groupName,
+ subGroupName: pqCriterias.subGroupName,
+ inputFormat: pqCriterias.inputFormat
+ })
+ .from(vendorPqCriteriaAnswers)
+ .leftJoin(pqCriterias, eq(vendorPqCriteriaAnswers.criteriaId, pqCriterias.id))
+ .where(eq(vendorPqCriteriaAnswers.vendorId, id));
+
+ // 각 답변에 대한 첨부파일 정보 조회
+ const answersWithAttachments = await Promise.all(
+ answers.map(async (answer) => {
+ const attachments = await db
+ .select({
+ id: vendorCriteriaAttachments.id,
+ fileName: vendorCriteriaAttachments.fileName,
+ originalFileName: vendorCriteriaAttachments.originalFileName,
+ filePath: vendorCriteriaAttachments.filePath,
+ fileType: vendorCriteriaAttachments.fileType,
+ fileSize: vendorCriteriaAttachments.fileSize
+ })
+ .from(vendorCriteriaAttachments)
+ .where(eq(vendorCriteriaAttachments.vendorCriteriaAnswerId, answer.id));
+
+ return {
+ ...answer,
+ uploadedFiles: attachments
+ };
+ })
+ );
+
+ return {
+ submission,
+ answers: answersWithAttachments
+ };
+ })
+ );
+
+ return submissionData;
+ } catch (error) {
+ console.error("Error in getVendorPQSubmissionData:", error);
+ return null;
+ }
+}
+
+/**
+ * 벤더의 추가정보를 가져오는 서버 액션
+ */
+export async function getVendorAdditionalInfo(vendorId: string) {
+ try {
+ const id = parseInt(vendorId);
+ if (isNaN(id)) {
+ return null;
+ }
+
+ const additionalInfo = await db
+ .select()
+ .from(vendorAdditionalInfo)
+ .where(eq(vendorAdditionalInfo.vendorId, id));
+
+ return additionalInfo.length > 0 ? additionalInfo[0] : null;
+ } catch (error) {
+ console.error("Error in getVendorAdditionalInfo:", error);
+ return null;
+ }
+}
+
+/**
+ * 벤더의 업무담당자 정보를 가져오는 서버 액션
+ */
+export async function getVendorBusinessContacts(vendorId: string) {
+ try {
+ const id = parseInt(vendorId);
+ if (isNaN(id)) {
+ return [];
+ }
+
+ const rows = await db
+ .select()
+ .from(vendorBusinessContacts)
+ .where(eq(vendorBusinessContacts.vendorId, id));
+
+ return rows;
+ } catch (error) {
+ console.error("Error in getVendorBusinessContacts:", error);
+ return [];
+ }
+}
+
+/**
+ * 벤더 기본정보를 업데이트하는 서버 액션 (향후 구현)
+ */
+export async function updateVendorData(vendorId: string, formData: VendorFormData) {
+ try {
+ // TODO: 실제 업데이트 로직 구현
+ console.log("Updating vendor data:", { vendorId, formData });
+
+ // 임시로 성공 응답 반환
+ return {
+ success: true,
+ message: "(개발중입니다) 벤더 정보가 성공적으로 업데이트되었습니다.",
+ };
+ } catch (error) {
+ console.error("Error in updateVendorData:", error);
+ return {
+ success: false,
+ message: "업데이트 중 오류가 발생했습니다.",
+ };
+ }
+} \ No newline at end of file
diff --git a/lib/vendor-basic-info/basic-info-client.tsx b/lib/vendor-basic-info/basic-info-client.tsx
new file mode 100644
index 00000000..ce8e4dfc
--- /dev/null
+++ b/lib/vendor-basic-info/basic-info-client.tsx
@@ -0,0 +1,1650 @@
+"use client";
+
+import React, { useState, useTransition } from "react";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import { Separator } from "@/components/ui/separator";
+import { Checkbox } from "@/components/ui/checkbox";
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select";
+import { Edit, Save, X } from "lucide-react";
+import { toast } from "sonner";
+import { VendorData, VendorFormData, VendorAttachment } from "./types";
+import { updateVendorData, getVendorPQData, getVendorPQSubmissionData, getVendorAdditionalInfo } from "./actions";
+import { getVendorBusinessContacts } from "./actions";
+import { noDataString } from "./constants";
+import { PQSimpleDialog } from "@/components/vendor-info/pq-simple-dialog";
+import { SiteVisitDetailDialog } from "@/lib/site-visit/site-visit-detail-dialog";
+import { DocumentStatusDialog } from "@/components/vendor-regular-registrations/document-status-dialog";
+import { AdditionalInfoDialog } from "@/components/vendor-regular-registrations/additional-info-dialog";
+import { getSiteVisitRequestsByVendorId } from "@/lib/site-visit/service";
+import { fetchVendorRegistrationStatus } from "@/lib/vendor-regular-registrations/service";
+import { getVendorAttachmentsByType, getVendorPeriodicGrade, getVendorTypeInfo } from "@/lib/vendor-info/service";
+// downloadFile은 동적으로 import
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from "@/components/ui/table";
+
+interface BasicInfoClientProps {
+ initialData: VendorData | null;
+ vendorId: string;
+}
+
+interface DropdownOption {
+ value: string;
+ label: string;
+}
+
+interface InfoItemProps {
+ title: string;
+ value: string | boolean;
+ isEditable?: boolean;
+ editMode?: boolean;
+ onChange?: (value: string | boolean) => void;
+ fieldKey?: string;
+ type?: "text" | "checkbox" | "dropdown" | "file-button" | "readonly";
+ options?: DropdownOption[]; // dropdown용 옵션들
+ onFileButtonClick?: () => void; // 파일 버튼 클릭 핸들러
+ placeholder?: string; // input placeholder
+}
+
+const InfoItem = ({
+ title,
+ value,
+ isEditable = false,
+ editMode = false,
+ onChange,
+ fieldKey,
+ type = "text",
+ options = [],
+ onFileButtonClick,
+ placeholder,
+}: InfoItemProps) => {
+ // 편집 가능 여부 결정 (readonly 타입은 항상 읽기 전용)
+ const canEdit = isEditable && editMode && type !== "readonly";
+
+ // 표시할 값 결정 (빈 값일 때 처리)
+ const displayValue = value || "";
+ const showNoData = !value && !canEdit;
+
+ const renderEditableField = () => {
+ switch (type) {
+ case "checkbox":
+ return (
+ <div className="flex items-center space-x-2">
+ <Checkbox
+ id={fieldKey}
+ checked={value as boolean}
+ onCheckedChange={(checked) => onChange?.(checked)}
+ />
+ <Label
+ htmlFor={fieldKey}
+ className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
+ >
+ {value ? "있음" : "없음"}
+ </Label>
+ </div>
+ );
+
+ case "dropdown":
+ return (
+ <Select
+ value={displayValue as string}
+ onValueChange={(val) => onChange?.(val)}
+ >
+ <SelectTrigger className="h-8">
+ <SelectValue placeholder={placeholder || "선택하세요"} />
+ </SelectTrigger>
+ <SelectContent>
+ {options.map((option) => (
+ <SelectItem key={option.value} value={option.value}>
+ {option.label}
+ </SelectItem>
+ ))}
+ </SelectContent>
+ </Select>
+ );
+
+ case "file-button":
+ return (
+ <Button
+ type="button"
+ variant="outline"
+ className="h-8 text-xs"
+ onClick={onFileButtonClick}
+ >
+ {displayValue || "파일 관리"}
+ </Button>
+ );
+
+ case "text":
+ default:
+ return (
+ <Input
+ id={fieldKey}
+ value={displayValue as string}
+ onChange={(e) => onChange?.(e.target.value)}
+ className="h-8"
+ placeholder={placeholder}
+ />
+ );
+ }
+ };
+
+ const renderReadOnlyField = () => {
+ switch (type) {
+ case "checkbox":
+ return (
+ <div className="flex items-center space-x-2">
+ <Checkbox
+ id={`readonly-${fieldKey}`}
+ checked={value as boolean}
+ disabled={true}
+ />
+ <Label
+ htmlFor={`readonly-${fieldKey}`}
+ className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
+ >
+ {value ? "있음" : "없음"}
+ </Label>
+ </div>
+ );
+
+ case "file-button":
+ return (
+ <Button
+ type="button"
+ variant="ghost"
+ className="h-auto p-0 text-xs text-left justify-start"
+ onClick={onFileButtonClick}
+ disabled={!onFileButtonClick}
+ >
+ {displayValue || noDataString}
+ </Button>
+ );
+
+ case "dropdown":
+ case "text":
+ case "readonly":
+ default:
+ return showNoData ? noDataString : displayValue;
+ }
+ };
+
+ return (
+ <div className="grid grid-cols-2 gap-2 py-1 min-w-0">
+ <div className="text-sm text-muted-foreground break-words">{title}:</div>
+ <div className="text-sm font-medium space-y-1 break-words overflow-hidden min-w-0">
+ {canEdit ? (
+ <div className="space-y-1">
+ <Label htmlFor={fieldKey} className="sr-only">
+ {title}
+ </Label>
+ {renderEditableField()}
+ </div>
+ ) : (
+ <span>{renderReadOnlyField()}</span>
+ )}
+ </div>
+ </div>
+ );
+};
+
+const OrganizationChart = ({
+ data,
+ editMode = false,
+ onChange,
+ pqData,
+}: {
+ data: any;
+ editMode?: boolean;
+ onChange?: (field: string, value: string) => void;
+ pqData: any[];
+}) => {
+ const organizationFields = [
+ { key: "representative", label: "대표", code: "1-5-1" },
+ { key: "sales", label: "영업", code: "1-5-2" },
+ { key: "design", label: "설계", code: "1-5-3" },
+ { key: "procurement", label: "구매", code: "1-5-4" },
+ { key: "production", label: "생산", code: "1-5-5" },
+ { key: "quality", label: "품질", code: "1-5-6" },
+ ];
+
+ return (
+ <div className="space-y-4">
+ <div className="text-sm font-semibold text-center">조직도</div>
+ <div className="grid grid-cols-3 gap-4">
+ {organizationFields.map((field) => (
+ <div key={field.key} className="flex flex-col items-center space-y-2">
+ <div className="text-sm font-medium text-center whitespace-nowrap">
+ {field.label}
+ </div>
+ <div className="text-center">
+ {editMode ? (
+ <Input
+ value={data?.[field.key]?.toString() || ""}
+ onChange={(e) => onChange?.(field.key, e.target.value)}
+ className="h-8 w-16 text-center"
+ placeholder="0"
+ />
+ ) : (
+ <span className="text-sm text-muted-foreground">
+ {pqData && Array.isArray(pqData) && pqData.find((a: any) => a.criteriaCode === field.code)?.answer || data?.[field.key]?.toString() || noDataString}
+ </span>
+ )}
+ </div>
+ </div>
+ ))}
+ </div>
+ </div>
+ );
+};
+
+const InfoSection = ({
+ title,
+ subtitle,
+ column1,
+ column2,
+ column3,
+ additionalContent,
+}: {
+ title: string;
+ subtitle?: string;
+ column1: React.ReactNode;
+ column2: React.ReactNode;
+ column3: React.ReactNode;
+ additionalContent?: React.ReactNode;
+}) => (
+ <div className="border">
+ <div className="flex">
+ <div className="w-32 bg-muted p-4 border-r flex flex-col">
+ <div className="text-sm font-semibold text-center w-full">{title}</div>
+ {subtitle && (
+ <div className="text-sm text-muted-foreground text-center w-full mt-1">
+ {subtitle}
+ </div>
+ )}
+ </div>
+ <div className="flex-1 grid grid-cols-3 min-w-0">
+ <div className="p-4 border-r min-w-0 overflow-hidden">{column1}</div>
+ <div className="p-4 border-r min-w-0 overflow-hidden">{column2}</div>
+ <div className="p-4 min-w-0 overflow-hidden">{column3}</div>
+ </div>
+ </div>
+ {additionalContent && (
+ <div className="flex">
+ <div className="w-32 bg-muted border-r"></div>
+ <div className="flex-1 p-4 border-t">{additionalContent}</div>
+ </div>
+ )}
+ </div>
+);
+
+const WideInfoSection = ({
+ title,
+ subtitle,
+ content,
+ noPadding = false,
+}: {
+ title?: string;
+ subtitle?: string;
+ content: React.ReactNode;
+ noPadding?: boolean;
+}) => (
+ <div className="border">
+ <div className="flex">
+ <div className="w-32 bg-muted p-4 border-r flex flex-col">
+ <div className="text-sm font-semibold text-center w-full">{title}</div>
+ {subtitle && (
+ <div className="text-sm text-muted-foreground text-center w-full mt-1">
+ {subtitle}
+ </div>
+ )}
+ </div>
+ <div className={`flex-1 min-w-0 overflow-x-auto ${noPadding ? '' : 'p-4'}`}>
+ {content}
+ </div>
+ </div>
+ </div>
+);
+
+export default function BasicInfoClient({
+ initialData,
+ vendorId,
+}: BasicInfoClientProps) {
+ const [editMode, setEditMode] = useState(false);
+ const [isPending, startTransition] = useTransition();
+
+ // 다이얼로그 상태
+ const [pqDialogOpen, setPqDialogOpen] = useState(false);
+ const [siteVisitDialogOpen, setSiteVisitDialogOpen] = useState(false);
+ const [contractDialogOpen, setContractDialogOpen] = useState(false);
+ const [additionalInfoDialogOpen, setAdditionalInfoDialogOpen] = useState(false);
+
+ // 각 다이얼로그에 필요한 데이터 상태
+ const [selectedSiteVisitRequest, setSelectedSiteVisitRequest] = useState<any>(null);
+ const [registrationData, setRegistrationData] = useState<any>(null);
+
+ // 첨부파일 및 평가 정보 상태
+ const [attachmentsByType, setAttachmentsByType] = useState<Record<string, any[]>>({});
+ const [periodicGrade, setPeriodicGrade] = useState<string | null>(null);
+ const [vendorTypeInfo, setVendorTypeInfo] = useState<any>(null);
+ const [pqData, setPqData] = useState<any[]>([]);
+ const [pqSubmissionData, setPqSubmissionData] = useState<any[]>([]);
+ const [additionalInfo, setAdditionalInfo] = useState<any>(null);
+ const [businessContacts, setBusinessContacts] = useState<any[]>([]);
+ const [formData, setFormData] = useState<VendorFormData>({
+ vendorName: initialData?.vendorName || "",
+ representativeName: initialData?.representativeName || "",
+ representativeWorkExperience:
+ initialData?.representativeWorkExperience || false,
+ representativeBirth: initialData?.representativeBirth || "",
+ representativePhone: initialData?.representativePhone || "",
+ representativeEmail: initialData?.representativeEmail || "",
+ phone: initialData?.phone || "",
+ fax: initialData?.fax || "",
+ email: initialData?.email || "",
+ address: initialData?.address || "",
+ addressDetail: initialData?.addressDetail || "",
+ postalCode: initialData?.postalCode || "",
+ businessSize: initialData?.businessSize || "",
+ country: initialData?.country || "",
+ website: initialData?.website || "",
+ businessType: initialData?.additionalInfo?.businessType || "",
+ employeeCount: initialData?.additionalInfo?.employeeCount || 0,
+ mainBusiness: initialData?.additionalInfo?.mainBusiness || "",
+ });
+
+ const handleSave = () => {
+ // startTransition(async () => {
+ // try {
+ // const result = await updateVendorData(vendorId, formData);
+ // if (result.success) {
+ // toast.success("[개발중] 저장되지 않습니다. 업데이트는 구현중입니다.");
+ // setEditMode(false);
+ // } else {
+ // toast.error(result.message || "저장에 실패했습니다.");
+ // }
+ // } catch {
+ // toast.error("저장 중 오류가 발생했습니다.");
+ // }
+ // });
+ };
+
+ const handleCancel = () => {
+ setFormData({
+ vendorName: initialData?.vendorName || "",
+ representativeName: initialData?.representativeName || "",
+ representativeWorkExperience:
+ initialData?.representativeWorkExperience || false,
+ representativeBirth: initialData?.representativeBirth || "",
+ representativePhone: initialData?.representativePhone || "",
+ representativeEmail: initialData?.representativeEmail || "",
+ phone: initialData?.phone || "",
+ fax: initialData?.fax || "",
+ email: initialData?.email || "",
+ address: initialData?.address || "",
+ addressDetail: initialData?.addressDetail || "",
+ postalCode: initialData?.postalCode || "",
+ businessSize: initialData?.businessSize || "",
+ country: initialData?.country || "",
+ website: initialData?.website || "",
+ businessType: initialData?.additionalInfo?.businessType || "",
+ employeeCount: initialData?.additionalInfo?.employeeCount || 0,
+ mainBusiness: initialData?.additionalInfo?.mainBusiness || "",
+ });
+ setEditMode(false);
+ };
+
+ const updateField = (
+ field: keyof VendorFormData,
+ value: string | number | boolean
+ ) => {
+ setFormData((prev) => ({ ...prev, [field]: value }));
+ };
+
+ // 기본계약 현황 조회 핸들러
+ const handleContractView = async () => {
+ try {
+ const result = await fetchVendorRegistrationStatus(parseInt(vendorId));
+ if (!result.success || !result.data) {
+ toast.info("기본계약 정보가 없습니다.");
+ return;
+ }
+
+ // DocumentStatusDialog가 기대하는 형태로 데이터 구성
+ const dialogData = {
+ // 기본 정보
+ id: result.data.registration?.id || 0,
+ vendorId: parseInt(vendorId),
+ companyName: result.data.vendor.vendorName,
+ businessNumber: result.data.vendor.taxId,
+ representative: result.data.vendor.representativeName,
+ country: result.data.vendor.country,
+ status: result.data.registration?.status || "정보없음",
+
+ // 문서 제출 현황 - documentSubmissions 속성으로 매핑
+ documentSubmissions: result.data.documentStatus,
+
+ // 문서별 파일 정보 추가
+ documentFiles: result.data.documentFiles || {
+ businessRegistration: [],
+ creditEvaluation: [],
+ bankCopy: [],
+ auditResult: []
+ },
+
+ // 기본계약 정보
+ basicContracts: result.data.basicContracts || [],
+
+ // 안전적격성 평가
+ safetyQualificationContent: result.data.registration?.safetyQualificationContent || null,
+
+ // 추가정보 완료 여부
+ additionalInfo: result.data.additionalInfoCompleted,
+ };
+
+ setRegistrationData(dialogData);
+ setContractDialogOpen(true);
+ } catch (error) {
+ console.error("기본계약 정보 조회 오류:", error);
+ toast.error("기본계약 정보를 불러오는데 실패했습니다.");
+ }
+ };
+
+ // 첨부파일 및 평가 정보 로드
+ const loadVendorData = async () => {
+ try {
+ // 첨부파일 조회
+ const attachmentsResult = await getVendorAttachmentsByType(parseInt(vendorId));
+ if (attachmentsResult.success && attachmentsResult.data) {
+ setAttachmentsByType(attachmentsResult.data);
+ }
+
+ // 정기평가 등급 조회
+ const gradeResult = await getVendorPeriodicGrade(parseInt(vendorId));
+ if (gradeResult.success && gradeResult.data) {
+ setPeriodicGrade(gradeResult.data.finalGrade);
+ }
+
+ // 벤더 타입 정보 조회
+ const typeResult = await getVendorTypeInfo(parseInt(vendorId));
+ if (typeResult.success && typeResult.data) {
+ setVendorTypeInfo(typeResult.data);
+ }
+
+ // PQ 데이터 조회
+ const pqResult = await getVendorPQData(vendorId);
+ if (pqResult) {
+ setPqData(pqResult);
+ }
+
+ // PQ 제출 데이터 조회
+ const pqSubmissionResult = await getVendorPQSubmissionData(vendorId);
+ if (pqSubmissionResult) {
+ setPqSubmissionData(pqSubmissionResult);
+ }
+
+ // 추가정보 조회
+ const additionalInfoResult = await getVendorAdditionalInfo(vendorId);
+ if (additionalInfoResult) {
+ setAdditionalInfo(additionalInfoResult);
+ }
+
+ // 업무담당자 정보 조회
+ const contacts = await getVendorBusinessContacts(vendorId);
+ setBusinessContacts(contacts || []);
+ } catch (error) {
+ console.error("벤더 데이터 로드 오류:", error);
+ }
+ };
+
+ // 컴포넌트 마운트 시 데이터 로드
+ React.useEffect(() => {
+ if (vendorId) {
+ loadVendorData();
+ }
+ }, [vendorId]);
+
+ // 첨부파일 다운로드 핸들러
+ const handleAttachmentDownload = async (filePath: string, fileName: string) => {
+ try {
+ // 동적으로 downloadFile 함수 import
+ const { downloadFile } = await import('@/lib/file-download')
+
+ const result = await downloadFile(filePath, fileName);
+ if (result.success) {
+ toast.success(`${fileName} 파일이 다운로드되었습니다.`);
+ } else {
+ toast.error(result.error || "파일 다운로드에 실패했습니다.");
+ }
+ } catch (error) {
+ console.error("파일 다운로드 오류:", error);
+ toast.error("파일 다운로드에 실패했습니다.");
+ }
+ };
+
+ // PQ 데이터에서 실사 정보 추출
+ const extractFactoryInfo = (pqData: any[]) => {
+ const factoryInfo = {
+ factoryAddress: "",
+ factoryPhone: "",
+ factoryFax: "",
+ factoryPIC: "",
+ factoryPICPosition: "",
+ factoryPICContact: "",
+ factoryPICEmail: "",
+ mainSupplyItems: "",
+ inspectionResult: "",
+ inspectionDate: "",
+ inspectionFiles: [] as any[]
+ };
+
+ if (!pqData || !Array.isArray(pqData)) {
+ return factoryInfo;
+ }
+
+ pqData.forEach(group => {
+ if (group && group.items && Array.isArray(group.items)) {
+ group.items.forEach((item: any) => {
+ const code = item.code;
+ const answer = item.answer;
+ const files = item.uploadedFiles || [];
+
+ // 공장주소 (1-4-1)
+ if (code === "1-4-1") {
+ factoryInfo.factoryAddress = answer || "";
+ }
+ // 공장 전화 (1-4-2)
+ else if (code === "1-4-2") {
+ factoryInfo.factoryPhone = answer || "";
+ }
+ // 공장 팩스 (1-4-3)
+ else if (code === "1-4-3") {
+ factoryInfo.factoryFax = answer || "";
+ }
+ // 공장 대표/담당자 이름 (1-4-4)
+ else if (code === "1-4-4") {
+ factoryInfo.factoryPIC = answer || "";
+ }
+ // 공장 대표/담당자 직책 (1-4-5)
+ else if (code === "1-4-5") {
+ factoryInfo.factoryPICPosition = answer || "";
+ }
+ // 공장 대표/담당자 전화 (1-4-6)
+ else if (code === "1-4-6") {
+ factoryInfo.factoryPICContact = answer || "";
+ }
+ // 공장 대표/담당자 이메일 (1-4-7)
+ else if (code === "1-4-7") {
+ factoryInfo.factoryPICEmail = answer || "";
+ }
+ // 공급품목 (첫 번째 것만 가져오기)
+ else if (code.startsWith("1-5") && !factoryInfo.mainSupplyItems) {
+ try {
+ const supplyItems = JSON.parse(answer || "[]");
+ if (Array.isArray(supplyItems) && supplyItems.length > 0) {
+ factoryInfo.mainSupplyItems = supplyItems[0].name || supplyItems[0] || "";
+ }
+ } catch {
+ factoryInfo.mainSupplyItems = answer || "";
+ }
+ }
+ // 실사 결과
+ else if (code.startsWith("4-") && answer && !factoryInfo.inspectionResult) {
+ factoryInfo.inspectionResult = answer;
+ factoryInfo.inspectionFiles = files;
+ }
+ });
+ }
+ });
+
+ return factoryInfo;
+ };
+
+ // PQ 제출 데이터에서 특정 코드의 답변 가져오기
+ const getPQAnswerByCode = (targetCode: string) => {
+ if (!pqSubmissionData || !Array.isArray(pqSubmissionData)) {
+ return "";
+ }
+
+ for (const submission of pqSubmissionData) {
+ if (submission && submission.answers && Array.isArray(submission.answers)) {
+ const answer = submission.answers.find((a: any) => a.criteriaCode === targetCode);
+ if (answer) {
+ return answer.answer;
+ }
+ }
+ }
+ return "";
+ };
+
+ // PQ 제출 데이터에서 특정 코드의 첨부파일 가져오기
+ const getPQAttachmentsByCode = (targetCode: string) => {
+ const files: any[] = [];
+
+ if (!pqSubmissionData || !Array.isArray(pqSubmissionData)) {
+ return files;
+ }
+
+ for (const submission of pqSubmissionData) {
+ if (submission && submission.answers && Array.isArray(submission.answers)) {
+ const answer = submission.answers.find((a: any) => a.criteriaCode === targetCode);
+ if (answer && answer.uploadedFiles) {
+ files.push(...answer.uploadedFiles);
+ }
+ }
+ }
+
+ return files;
+ };
+
+ // 첨부파일 관리 핸들러 (타입별)
+ const handleAttachmentFileManagement = (attachmentType: string, typeName: string) => {
+ const files = attachmentsByType[attachmentType] || [];
+
+ if (files.length === 0) {
+ toast.info(`${typeName} 파일이 없습니다.`);
+ return;
+ }
+
+ // 파일이 하나인 경우 바로 다운로드
+ if (files.length === 1) {
+ handleAttachmentDownload(files[0].filePath, files[0].fileName);
+ return;
+ }
+
+ // 파일이 여러 개인 경우 순차적으로 모든 파일 다운로드
+ toast.info(`${typeName} 파일 ${files.length}개를 다운로드합니다.`);
+ files.forEach((file, index) => {
+ setTimeout(() => {
+ handleAttachmentDownload(file.filePath, file.fileName);
+ }, index * 500); // 500ms 간격으로 순차 다운로드
+ });
+ };
+
+ if (!initialData) {
+ return (
+ <div className="p-6 bg-background max-w-full">
+ <div className="text-center py-8">
+ <p className="text-muted-foreground">{noDataString}</p>
+ </div>
+ </div>
+ );
+ }
+
+ // attachmentsByType는 상태로 관리되고 있으므로 제거
+
+ return (
+ <div className="p-6 bg-background w-full overflow-x-auto">
+ <div className="mb-6 flex justify-between items-center">
+ <h2 className="text-xl font-bold">협력업체 기본정보</h2>
+ <div className="flex gap-2">
+ {editMode ? (
+ <>
+ <Button
+ onClick={handleSave}
+ disabled={isPending}
+ className="flex items-center gap-1"
+ >
+ <Save className="w-4 h-4" />
+ 저장
+ </Button>
+ <Button
+ variant="outline"
+ onClick={handleCancel}
+ disabled={isPending}
+ className="flex items-center gap-1"
+ >
+ <X className="w-4 h-4" />
+ 취소
+ </Button>
+ </>
+ ) : (
+ <>
+ <Button
+ onClick={() => setEditMode(true)}
+ className="flex items-center gap-1"
+ >
+ <Edit className="w-4 h-4" />
+ 수정
+ </Button>
+ <Button
+ variant="outline"
+ onClick={handleContractView}
+ className="flex items-center gap-1"
+ >
+ 정규업체등록 현황
+ </Button>
+ </>
+ )}
+ </div>
+ </div>
+
+ <div className="space-y-4">
+ {/* 업체정보 */}
+ <InfoSection
+ title="업체정보"
+ column1={
+ <div className="space-y-2">
+ <InfoItem
+ title="업체명"
+ value={formData.vendorName}
+ isEditable={true}
+ editMode={editMode}
+ fieldKey="vendorName"
+ onChange={(value) => updateField("vendorName", value)}
+ />
+ {/* <InfoItem
+ title="설립일"
+ // 현재 필드 없고 linter error 나도 무시. createdAt은 데이터베이스 생성시점이므로 잘못된 필드.
+ value={initialData.establishmentDate}
+ type="readonly"
+ /> */}
+ <InfoItem
+ title="대표전화"
+ value={formData.phone}
+ isEditable={true}
+ editMode={editMode}
+ fieldKey="phone"
+ onChange={(value) => updateField("phone", value)}
+ />
+ {/* <InfoItem
+ title="팩스"
+ value={formData.fax}
+ isEditable={true}
+ editMode={editMode}
+ fieldKey="fax"
+ onChange={(value) => updateField("fax", value)}
+ /> */}
+ <InfoItem
+ title="업체유형"
+ value={formData.businessType}
+ isEditable={true}
+ editMode={editMode}
+ fieldKey="businessType"
+ onChange={(value) => updateField("businessType", value)}
+ />
+ {/* <InfoItem
+ title="소개자료"
+ value={`회사: ${
+ attachmentsByType.COMPANY_INTRO?.length || 0
+ }건 / 제품: ${attachmentsByType.PRODUCT_INTRO?.length || 0}건`}
+ isEditable={true}
+ editMode={editMode}
+ type="file-button"
+ onFileButtonClick={() => handleFileManagement("소개자료")}
+ /> */}
+ <InfoItem
+ title="정기평가 등급"
+ value={periodicGrade || ""}
+ type="readonly"
+ />
+ </div>
+ }
+ column2={
+ <div className="space-y-2">
+ <InfoItem
+ title="사업자번호"
+ value={initialData.taxId}
+ type="readonly"
+ />
+ <InfoItem
+ title="법인등록번호"
+ value={initialData.corporateRegistrationNumber}
+ type="readonly"
+ />
+ <InfoItem
+ title="회사주소"
+ value={initialData.address || ""}
+ type="readonly"
+ />
+ <InfoItem
+ title="상세주소"
+ value={initialData.addressDetail || ""}
+ type="readonly"
+ />
+ <InfoItem
+ title="우편번호"
+ value={initialData.postalCode || ""}
+ type="readonly"
+ />
+ <InfoItem
+ title="E-mail"
+ value={formData.email}
+ isEditable={true}
+ editMode={editMode}
+ fieldKey="email"
+ onChange={(value) => updateField("email", value)}
+ />
+ {/* <InfoItem
+ title="사업유형"
+ value={formData.businessType}
+ isEditable={true}
+ />
+ <InfoItem
+ title="기업규모"
+ value={formData.businessSize}
+ isEditable={true}
+ editMode={editMode}
+ fieldKey="businessSize"
+ type="dropdown"
+ options={[
+ { value: "A", label: "대기업 (A)" },
+ { value: "B", label: "중견기업 (B)" },
+ { value: "C", label: "중소기업 (C)" },
+ { value: "D", label: "소상공인 (D)" },
+ ]}
+ onChange={(value) => updateField("businessSize", value)}
+ placeholder="기업규모를 선택하세요"
+ /> */}
+
+ {/* <InfoItem
+ title="안전적격성평가"
+ value={
+ initialData.evaluationInfo?.safetyQualificationEvaluation ||
+ null
+ }
+ type="readonly"
+ /> */}
+ </div>
+ }
+ column3={
+ <div className="space-y-2">
+ <InfoItem
+ title="업체분류"
+ value={vendorTypeInfo?.vendorTypeName || ""}
+ type="readonly"
+ />
+ {/* <InfoItem
+ title="그룹사"
+ value={initialData.classificationInfo?.groupCompany || null}
+ isEditable={true}
+ /> */}
+ <InfoItem
+ title="국가"
+ value={formData.country}
+ isEditable={true}
+ editMode={editMode}
+ fieldKey="country"
+ onChange={(value) => updateField("country", value)}
+ />
+ <InfoItem
+ title="선호언어"
+ value={
+ initialData.classificationInfo?.preferredLanguage || ""
+ }
+ isEditable={true}
+ />
+ {/* <InfoItem
+ title="산업유형"
+ value={initialData.classificationInfo?.industryType || ""}
+ isEditable={true}
+ /> */}
+
+ {/* <InfoItem
+ title="당사거래비중"
+ value={
+ initialData.evaluationInfo?.companyTransactionRatio || ""
+ }
+ type="readonly"
+ /> */}
+ </div>
+ }
+ />
+
+ {/* 첨부파일 */}
+ <WideInfoSection
+ title="첨부파일"
+ content={
+ <div className="grid grid-cols-2 md:grid-cols-5 gap-4 p-4">
+ {/* 사업자등록증 */}
+ <div className="flex flex-col items-center gap-3">
+ <div className="text-sm font-medium text-center">사업자등록증</div>
+ <div className="text-center">
+ <div className="text-lg font-semibold text-primary">
+ {attachmentsByType.BUSINESS_REGISTRATION?.length || 0}건
+ </div>
+ {attachmentsByType.BUSINESS_REGISTRATION?.length > 0 && (
+ <Button
+ variant="outline"
+ size="sm"
+ className="mt-2"
+ onClick={() => handleAttachmentFileManagement("BUSINESS_REGISTRATION", "사업자등록증")}
+ >
+ 파일 다운로드
+ </Button>
+ )}
+ </div>
+ </div>
+
+ {/* 신용평가보고서 */}
+ <div className="flex flex-col items-center gap-3">
+ <div className="text-sm font-medium text-center">신용평가보고서</div>
+ <div className="text-center">
+ <div className="text-lg font-semibold text-primary">
+ {attachmentsByType.CREDIT_REPORT?.length || 0}건
+ </div>
+ {attachmentsByType.CREDIT_REPORT?.length > 0 && (
+ <Button
+ variant="outline"
+ size="sm"
+ className="mt-2"
+ onClick={() => handleAttachmentFileManagement("CREDIT_REPORT", "신용평가보고서")}
+ >
+ 파일 다운로드
+ </Button>
+ )}
+ </div>
+ </div>
+
+ {/* 통장사본 */}
+ <div className="flex flex-col items-center gap-3">
+ <div className="text-sm font-medium text-center">통장사본</div>
+ <div className="text-center">
+ <div className="text-lg font-semibold text-primary">
+ {attachmentsByType.BANK_ACCOUNT_COPY?.length || 0}건
+ </div>
+ {attachmentsByType.BANK_ACCOUNT_COPY?.length > 0 && (
+ <Button
+ variant="outline"
+ size="sm"
+ className="mt-2"
+ onClick={() => handleAttachmentFileManagement("BANK_ACCOUNT_COPY", "통장사본")}
+ >
+ 파일 다운로드
+ </Button>
+ )}
+ </div>
+ </div>
+
+ {/* ISO 인증서 */}
+ <div className="flex flex-col items-center gap-3">
+ <div className="text-sm font-medium text-center">ISO 인증서</div>
+ <div className="text-center">
+ <div className="text-lg font-semibold text-primary">
+ {attachmentsByType.ISO_CERTIFICATION?.length || 0}건
+ </div>
+ {attachmentsByType.ISO_CERTIFICATION?.length > 0 && (
+ <Button
+ variant="outline"
+ size="sm"
+ className="mt-2"
+ onClick={() => handleAttachmentFileManagement("ISO_CERTIFICATION", "ISO 인증서")}
+ >
+ 파일 다운로드
+ </Button>
+ )}
+ </div>
+ </div>
+
+ {/* 기타 첨부파일 (GENERAL) */}
+ <div className="flex flex-col items-center gap-3">
+ <div className="text-sm font-medium text-center">기타 첨부파일</div>
+ <div className="text-center">
+ <div className="text-lg font-semibold text-primary">
+ {attachmentsByType.GENERAL?.length || 0}건
+ </div>
+ {attachmentsByType.GENERAL?.length > 0 && (
+ <Button
+ variant="outline"
+ size="sm"
+ className="mt-2"
+ onClick={() => handleAttachmentFileManagement("GENERAL", "기타 첨부파일")}
+ >
+ 파일 다운로드
+ </Button>
+ )}
+ </div>
+ </div>
+ </div>
+ }
+ />
+
+
+ {/* 상세정보 */}
+ <InfoSection
+ title="상세정보"
+ column1={
+ <div className="space-y-2">
+ <InfoItem
+ title="대표자명"
+ value={formData.representativeName}
+ isEditable={true}
+ editMode={editMode}
+ fieldKey="representativeName"
+ onChange={(value) => updateField("representativeName", value)}
+ />
+ <InfoItem
+ title="대표자 당사근무경험"
+ value={formData.representativeWorkExperience}
+ isEditable={true}
+ editMode={editMode}
+ fieldKey="representativeWorkExperience"
+ type="checkbox"
+ onChange={(value) =>
+ updateField("representativeWorkExperience", value)
+ }
+ />
+ <InfoItem
+ title="대표자 생년월일"
+ value={formData.representativeBirth || ""}
+ isEditable={true}
+ editMode={editMode}
+ fieldKey="representativeBirth"
+ onChange={(value) => updateField("representativeBirth", value)}
+ />
+ <InfoItem
+ title="임직원수"
+ value={getPQAnswerByCode("1-8-3") || formData.employeeCount.toString()}
+ type="readonly"
+ />
+ </div>
+ }
+ column2={
+ <div className="space-y-2">
+ <InfoItem
+ title="대표자Tel"
+ value={formData.representativePhone}
+ isEditable={true}
+ editMode={editMode}
+ fieldKey="representativePhone"
+ onChange={(value) => updateField("representativePhone", value)}
+ />
+ <InfoItem
+ title="대표자 주소"
+ value={formData.address || ""}
+ isEditable={true}
+ editMode={editMode}
+ fieldKey="address"
+ onChange={(value) => updateField("address", value)}
+ />
+ <InfoItem
+ title="연간 매출"
+ value={getPQAnswerByCode("1-7-1") || getPQAnswerByCode("1-7-2") || initialData.capacityInfo?.annualSales || ""}
+ type="readonly"
+ />
+ </div>
+ }
+ column3={
+ <div className="space-y-2">
+ <InfoItem
+ title="대표자 E-mail"
+ value={formData.representativeEmail}
+ isEditable={true}
+ editMode={editMode}
+ fieldKey="representativeEmail"
+ onChange={(value) => updateField("representativeEmail", value)}
+ />
+ <InfoItem
+ title="생산능력"
+ value={getPQAnswerByCode("1-9-1") || getPQAnswerByCode("1-9-2") || initialData.capacityInfo?.productionCapacity || ""}
+ type="readonly"
+ />
+ </div>
+ }
+ additionalContent={
+ <div className="grid grid-cols-2 gap-8 py-4 min-w-0 overflow-x-auto">
+ <OrganizationChart
+ data={initialData.organization}
+ editMode={editMode}
+ pqData={pqSubmissionData && Array.isArray(pqSubmissionData) ? pqSubmissionData.flatMap(s => s.answers || []) : []}
+ onChange={(field, value) => {
+ // TODO: 조직도 업데이트 로직 구현
+ }}
+ />
+ <div className="flex flex-col items-center gap-3">
+ <div className="text-sm font-semibold text-center">
+ 관련 첨부파일
+ </div>
+ <div className="space-y-2">
+ <Button
+ variant="outline"
+ className="text-xs w-32 flex items-center gap-2"
+ onClick={() => {
+ const files = getPQAttachmentsByCode("1-10");
+ if (files.length > 0) {
+ files.forEach((file, index) => {
+ setTimeout(() => {
+ handleAttachmentDownload(file.filePath, file.fileName);
+ }, index * 500);
+ });
+ } else {
+ toast.info("협력업체정보 파일이 없습니다.");
+ }
+ }}
+ >
+ 협력업체정보 ({getPQAttachmentsByCode("1-10").length}건)
+ </Button>
+ <Button
+ variant="outline"
+ className="text-xs w-32 flex items-center gap-2"
+ onClick={() => {
+ const files = getPQAttachmentsByCode("1-12");
+ if (files.length > 0) {
+ files.forEach((file, index) => {
+ setTimeout(() => {
+ handleAttachmentDownload(file.filePath, file.fileName);
+ }, index * 500);
+ });
+ } else {
+ toast.info("외주화정보 파일이 없습니다.");
+ }
+ }}
+ >
+ 외주화정보 ({getPQAttachmentsByCode("1-12").length}건)
+ </Button>
+ <Button
+ variant="outline"
+ className="text-xs w-32 flex items-center gap-2"
+ onClick={() => {
+ const files = getPQAttachmentsByCode("1-13");
+ if (files.length > 0) {
+ files.forEach((file, index) => {
+ setTimeout(() => {
+ handleAttachmentDownload(file.filePath, file.fileName);
+ }, index * 500);
+ });
+ } else {
+ toast.info("A/S 네트워크 파일이 없습니다.");
+ }
+ }}
+ >
+ A/S 네트워크 ({getPQAttachmentsByCode("1-13").length}건)
+ </Button>
+ </div>
+ </div>
+ </div>
+ }
+ />
+
+ {/* <Separator /> */}
+
+ {/* 매출정보 */}
+ <WideInfoSection
+ title="매출정보"
+ subtitle="(3개년)"
+ noPadding={true}
+ content={
+ <Table>
+ <TableHeader>
+ <TableRow>
+ <TableHead
+ rowSpan={2}
+ className="text-center border-r align-middle"
+ >
+ 기준일
+ </TableHead>
+ <TableHead colSpan={3} className="text-center border-r">
+ 자산 구성
+ </TableHead>
+ <TableHead
+ rowSpan={2}
+ className="text-center border-r align-middle"
+ >
+ 영업이익
+ <br />
+ (백만원)
+ </TableHead>
+ <TableHead
+ rowSpan={2}
+ className="text-center border-r align-middle"
+ >
+ 당기순이익
+ <br />
+ (백만원)
+ </TableHead>
+ <TableHead
+ rowSpan={2}
+ className="text-center border-r align-middle"
+ >
+ 부채비율
+ <br />
+ (%)
+ </TableHead>
+ <TableHead
+ rowSpan={2}
+ className="text-center border-r align-middle"
+ >
+ 차입금의존도
+ <br />
+ (%)
+ </TableHead>
+ <TableHead
+ rowSpan={2}
+ className="text-center border-r align-middle"
+ >
+ 영업이익률
+ <br />
+ (%)
+ </TableHead>
+ <TableHead
+ rowSpan={2}
+ className="text-center border-r align-middle"
+ >
+ 순이익률
+ <br />
+ (%)
+ </TableHead>
+ <TableHead
+ rowSpan={2}
+ className="text-center border-r align-middle"
+ >
+ 매출액증감
+ <br />
+ (%)
+ </TableHead>
+ <TableHead rowSpan={2} className="text-center align-middle">
+ 유동비율
+ <br />
+ (%)
+ </TableHead>
+ </TableRow>
+ <TableRow>
+ <TableHead className="text-center border-r">총자산</TableHead>
+ <TableHead className="text-center border-r">
+ 부채총계
+ </TableHead>
+ <TableHead className="text-center border-r">
+ 자본총계
+ </TableHead>
+ </TableRow>
+ </TableHeader>
+ <TableBody>
+ {["20231231", "20221231", "20211231"].map((dateKey) => {
+ const year = dateKey;
+ const salesData = initialData.salesInfo?.[year];
+ const metricsData = initialData.calculatedMetrics?.[dateKey];
+
+ return (
+ <TableRow key={dateKey}>
+ <TableCell className="text-center font-medium border-r bg-yellow-50">
+ {year}
+ </TableCell>
+ <TableCell className="text-right border-r">
+ {salesData
+ ? (
+ parseInt(salesData.totalDebt.replace(/,/g, "")) +
+ parseInt(salesData.totalEquity.replace(/,/g, ""))
+ ).toLocaleString()
+ : "-"}
+ </TableCell>
+ <TableCell className="text-right border-r">
+ {salesData?.totalDebt || "-"}
+ </TableCell>
+ <TableCell className="text-right border-r">
+ {salesData?.totalEquity || "-"}
+ </TableCell>
+ <TableCell className="text-right border-r">
+ {salesData?.operatingProfit || "-"}
+ </TableCell>
+ <TableCell className="text-right border-r">
+ {salesData?.netIncome || "-"}
+ </TableCell>
+ <TableCell className="text-right border-r">
+ {metricsData?.debtRatio?.toFixed(1) || "-"}
+ </TableCell>
+ <TableCell className="text-right border-r">
+ {metricsData?.borrowingDependency?.toFixed(1) || "-"}
+ </TableCell>
+ <TableCell className="text-right border-r">
+ {metricsData?.operatingMargin?.toFixed(1) || "-"}
+ </TableCell>
+ <TableCell className="text-right border-r">
+ {metricsData?.netMargin?.toFixed(1) || "-"}
+ </TableCell>
+ <TableCell className="text-right border-r">
+ {metricsData?.salesGrowth?.toFixed(1) || "-"}
+ </TableCell>
+ <TableCell className="text-right">
+ {metricsData?.currentRatio?.toFixed(1) || "-"}
+ </TableCell>
+ </TableRow>
+ );
+ })}
+ </TableBody>
+ </Table>
+ }
+ />
+
+ {/* <Separator /> */}
+
+ {/* 실사정보 */}
+ {pqSubmissionData && pqSubmissionData.length > 0 && pqSubmissionData.map((submission, index) => {
+ const factoryInfo = extractFactoryInfo([{
+ groupName: "Factory Info",
+ items: submission.answers.map((answer: any) => ({
+ code: answer.criteriaCode,
+ answer: answer.answer,
+ uploadedFiles: answer.uploadedFiles || []
+ }))
+ }]);
+ const inspectionFiles = getPQAttachmentsByCode("4-1");
+
+ return (
+ <InfoSection
+ key={submission.submission.id}
+ title="실사정보"
+ subtitle={`${submission.submission.type || "일반"} - ${submission.submission.status || "상태없음"}`}
+ column1={
+ <div className="space-y-2">
+ <InfoItem
+ title="공장주소"
+ value={factoryInfo.factoryAddress || ""}
+ type="readonly"
+ />
+ <InfoItem
+ title="공장 전화"
+ value={factoryInfo.factoryPhone || ""}
+ type="readonly"
+ />
+ <InfoItem
+ title="공장 팩스"
+ value={factoryInfo.factoryFax || ""}
+ type="readonly"
+ />
+ </div>
+ }
+ column2={
+ <div className="space-y-2">
+ <InfoItem
+ title="공장 담당자"
+ value={
+ factoryInfo.factoryPIC
+ ? `${factoryInfo.factoryPIC} [${factoryInfo.factoryPICPosition || ""}] [${factoryInfo.factoryPICContact || ""}] [${factoryInfo.factoryPICEmail || ""}]`
+ : ""
+ }
+ type="readonly"
+ />
+ <InfoItem
+ title="실사결과"
+ value={factoryInfo.inspectionResult || ""}
+ type="readonly"
+ />
+ {inspectionFiles.length > 0 && (
+ <Button
+ variant="outline"
+ size="sm"
+ onClick={() => {
+ inspectionFiles.forEach((file, idx) => {
+ setTimeout(() => {
+ handleAttachmentDownload(file.filePath, file.fileName);
+ }, idx * 500);
+ });
+ }}
+ >
+ 실사결과 파일 다운로드 ({inspectionFiles.length}건)
+ </Button>
+ )}
+ </div>
+ }
+ column3={
+ <div className="flex flex-col gap-2">
+ <div className="space-y-2">
+ <InfoItem
+ title="대표공급품목"
+ value={factoryInfo.mainSupplyItems || ""}
+ type="readonly"
+ />
+ </div>
+ </div>
+ }
+ additionalContent={
+ <div className="grid grid-cols-5 gap-4 min-w-0 overflow-x-auto">
+ <div className="text-center min-w-0">
+ <div className="text-sm font-medium mb-2 break-words">
+ 공장소개자료
+ </div>
+ <div className="text-sm text-muted-foreground">
+ {getPQAttachmentsByCode("1-4").length}건
+ </div>
+ {getPQAttachmentsByCode("1-4").length > 0 && (
+ <Button
+ variant="outline"
+ size="sm"
+ className="mt-2"
+ onClick={() => {
+ const files = getPQAttachmentsByCode("1-4");
+ files.forEach((file, idx) => {
+ setTimeout(() => {
+ handleAttachmentDownload(file.filePath, file.fileName);
+ }, idx * 500);
+ });
+ }}
+ >
+ 파일 다운로드
+ </Button>
+ )}
+ </div>
+ <div className="text-center min-w-0">
+ <div className="text-sm font-medium mb-2 break-words">
+ QMS Cert
+ </div>
+ <div className="text-sm text-muted-foreground">
+ {getPQAttachmentsByCode("2-1").length}건
+ </div>
+ {getPQAttachmentsByCode("2-1").length > 0 && (
+ <Button
+ variant="outline"
+ size="sm"
+ className="mt-2"
+ onClick={() => {
+ const files = getPQAttachmentsByCode("2-1");
+ files.forEach((file, idx) => {
+ setTimeout(() => {
+ handleAttachmentDownload(file.filePath, file.fileName);
+ }, idx * 500);
+ });
+ }}
+ >
+ 파일 다운로드
+ </Button>
+ )}
+ </div>
+ <div className="text-center min-w-0">
+ <div className="text-sm font-medium mb-2 break-words">
+ Product Cert
+ </div>
+ <div className="text-sm text-muted-foreground">
+ {getPQAttachmentsByCode("2-2").length}건
+ </div>
+ {getPQAttachmentsByCode("2-2").length > 0 && (
+ <Button
+ variant="outline"
+ size="sm"
+ className="mt-2"
+ onClick={() => {
+ const files = getPQAttachmentsByCode("2-2");
+ files.forEach((file, idx) => {
+ setTimeout(() => {
+ handleAttachmentDownload(file.filePath, file.fileName);
+ }, idx * 500);
+ });
+ }}
+ >
+ 파일 다운로드
+ </Button>
+ )}
+ </div>
+ <div className="text-center min-w-0">
+ <div className="text-sm font-medium mb-2 break-words">
+ Ex. Cert
+ </div>
+ <div className="text-sm text-muted-foreground">
+ {getPQAttachmentsByCode("2-17").length}건
+ </div>
+ {getPQAttachmentsByCode("2-17").length > 0 && (
+ <Button
+ variant="outline"
+ size="sm"
+ className="mt-2"
+ onClick={() => {
+ const files = getPQAttachmentsByCode("2-17");
+ files.forEach((file, idx) => {
+ setTimeout(() => {
+ handleAttachmentDownload(file.filePath, file.fileName);
+ }, idx * 500);
+ });
+ }}
+ >
+ 파일 다운로드
+ </Button>
+ )}
+ </div>
+ <div className="text-center min-w-0">
+ <div className="text-sm font-medium mb-2 break-words">
+ HSE Cert
+ </div>
+ <div className="text-sm text-muted-foreground">
+ {getPQAttachmentsByCode("3-1").length}건
+ </div>
+ {getPQAttachmentsByCode("3-1").length > 0 && (
+ <Button
+ variant="outline"
+ size="sm"
+ className="mt-2"
+ onClick={() => {
+ const files = getPQAttachmentsByCode("3-1");
+ files.forEach((file, idx) => {
+ setTimeout(() => {
+ handleAttachmentDownload(file.filePath, file.fileName);
+ }, idx * 500);
+ });
+ }}
+ >
+ 파일 다운로드
+ </Button>
+ )}
+ </div>
+ </div>
+ }
+ />
+ );
+ })}
+ {/* 추가정보 */}
+ <InfoSection
+ title="추가정보"
+ // subtitle="정규업체 등록 시 입력된 정보"
+ column1={
+ <div className="space-y-2">
+ <InfoItem
+ title="사업유형"
+ value={additionalInfo?.businessType || ""}
+ type="readonly"
+ />
+ <InfoItem
+ title="산업유형"
+ value={additionalInfo?.industryType || ""}
+ type="readonly"
+ />
+ <InfoItem
+ title="회사규모"
+ value={additionalInfo?.companySize || ""}
+ type="readonly"
+ />
+ </div>
+ }
+ column2={
+ <div className="space-y-2">
+ <InfoItem
+ title="매출액"
+ value={additionalInfo?.revenue ? `${additionalInfo.revenue.toLocaleString()}원` : ""}
+ type="readonly"
+ />
+ <InfoItem
+ title="공장설립일"
+ value={additionalInfo?.factoryEstablishedDate ? new Date(additionalInfo.factoryEstablishedDate).toLocaleDateString('ko-KR') : ""}
+ type="readonly"
+ />
+ <InfoItem
+ title="선호계약조건"
+ value={additionalInfo?.preferredContractTerms || ""}
+ type="readonly"
+ />
+ </div>
+ }
+ column3={
+ <div className="space-y-2">
+ {/* 추가 정보가 더 있다면 여기에 배치 */}
+ </div>
+ }
+ />
+ {/* 업무담당자 */}
+ <InfoSection
+ title="업무담당자"
+ column1={
+ <div className="space-y-3">
+ <div className="space-y-1">
+ <div className="text-sm font-medium">영업 담당자</div>
+ <InfoItem title="이름" value={businessContacts.find(c => c.contactType === "sales")?.contactName || ""} type="readonly" />
+ <InfoItem title="직급" value={businessContacts.find(c => c.contactType === "sales")?.position || ""} type="readonly" />
+ <InfoItem title="부서" value={businessContacts.find(c => c.contactType === "sales")?.department || ""} type="readonly" />
+ <InfoItem title="담당업무" value={businessContacts.find(c => c.contactType === "sales")?.responsibility || ""} type="readonly" />
+ <InfoItem title="E-mail" value={businessContacts.find(c => c.contactType === "sales")?.email || ""} type="readonly" />
+ </div>
+ </div>
+ }
+ column2={
+ <div className="space-y-3">
+ <div className="space-y-1">
+ <div className="text-sm font-medium">설계 담당자</div>
+ <InfoItem title="이름" value={businessContacts.find(c => c.contactType === "design")?.contactName || ""} type="readonly" />
+ <InfoItem title="직급" value={businessContacts.find(c => c.contactType === "design")?.position || ""} type="readonly" />
+ <InfoItem title="부서" value={businessContacts.find(c => c.contactType === "design")?.department || ""} type="readonly" />
+ <InfoItem title="담당업무" value={businessContacts.find(c => c.contactType === "design")?.responsibility || ""} type="readonly" />
+ <InfoItem title="E-mail" value={businessContacts.find(c => c.contactType === "design")?.email || ""} type="readonly" />
+ </div>
+ <div className="space-y-1">
+ <div className="text-sm font-medium">납기 담당자</div>
+ <InfoItem title="이름" value={businessContacts.find(c => c.contactType === "delivery")?.contactName || ""} type="readonly" />
+ <InfoItem title="직급" value={businessContacts.find(c => c.contactType === "delivery")?.position || ""} type="readonly" />
+ <InfoItem title="부서" value={businessContacts.find(c => c.contactType === "delivery")?.department || ""} type="readonly" />
+ <InfoItem title="담당업무" value={businessContacts.find(c => c.contactType === "delivery")?.responsibility || ""} type="readonly" />
+ <InfoItem title="E-mail" value={businessContacts.find(c => c.contactType === "delivery")?.email || ""} type="readonly" />
+ </div>
+ </div>
+ }
+ column3={
+ <div className="space-y-3">
+ <div className="space-y-1">
+ <div className="text-sm font-medium">품질 담당자</div>
+ <InfoItem title="이름" value={businessContacts.find(c => c.contactType === "quality")?.contactName || ""} type="readonly" />
+ <InfoItem title="직급" value={businessContacts.find(c => c.contactType === "quality")?.position || ""} type="readonly" />
+ <InfoItem title="부서" value={businessContacts.find(c => c.contactType === "quality")?.department || ""} type="readonly" />
+ <InfoItem title="담당업무" value={businessContacts.find(c => c.contactType === "quality")?.responsibility || ""} type="readonly" />
+ <InfoItem title="E-mail" value={businessContacts.find(c => c.contactType === "quality")?.email || ""} type="readonly" />
+ </div>
+ <div className="space-y-1">
+ <div className="text-sm font-medium">세금계산서 담당자</div>
+ <InfoItem title="이름" value={businessContacts.find(c => c.contactType === "tax_invoice")?.contactName || ""} type="readonly" />
+ <InfoItem title="직급" value={businessContacts.find(c => c.contactType === "tax_invoice")?.position || ""} type="readonly" />
+ <InfoItem title="부서" value={businessContacts.find(c => c.contactType === "tax_invoice")?.department || ""} type="readonly" />
+ <InfoItem title="담당업무" value={businessContacts.find(c => c.contactType === "tax_invoice")?.responsibility || ""} type="readonly" />
+ <InfoItem title="E-mail" value={businessContacts.find(c => c.contactType === "tax_invoice")?.email || ""} type="readonly" />
+ </div>
+ </div>
+ }
+ />
+
+ </div>
+ <DocumentStatusDialog
+ open={contractDialogOpen}
+ onOpenChange={setContractDialogOpen}
+ registration={registrationData}
+ isVendorUser={false}
+ />
+ </div>
+ );
+}
diff --git a/lib/vendor-basic-info/constants.ts b/lib/vendor-basic-info/constants.ts
new file mode 100644
index 00000000..d16f791f
--- /dev/null
+++ b/lib/vendor-basic-info/constants.ts
@@ -0,0 +1 @@
+export const noDataString = "-"; \ No newline at end of file
diff --git a/lib/vendor-basic-info/types.ts b/lib/vendor-basic-info/types.ts
new file mode 100644
index 00000000..ead3a44c
--- /dev/null
+++ b/lib/vendor-basic-info/types.ts
@@ -0,0 +1,180 @@
+export interface VendorContact {
+ id: number;
+ contactName: string;
+ contactPosition: string;
+ contactEmail: string;
+ contactPhone: string;
+ isPrimary: boolean;
+}
+
+export interface VendorAttachment {
+ id: number;
+ fileName: string;
+ filePath: string;
+ attachmentType: string;
+ createdAt: string;
+}
+
+export interface VendorProcessInfo {
+ processCount: number;
+ processPIC: string;
+ processApprovalDate: string;
+ implementationApproval: string;
+}
+
+export interface VendorContractInfo {
+ contractRegistrationNumber: string;
+ contractPeriod: string;
+ lastEquipmentInspection: string;
+}
+
+export interface VendorSalesData {
+ totalSales: string;
+ totalDebt: string;
+ totalEquity: string;
+ operatingProfit: string;
+ netIncome: string;
+}
+
+export interface VendorAdditionalInfo {
+ postalCode: string;
+ detailAddress: string;
+ mainBusiness: string;
+ employeeCount: number;
+ businessType: string;
+}
+
+export interface VendorOrganization {
+ representative: number;
+ sales: number;
+ design: number;
+ procurement: number;
+ production: number;
+ quality: number;
+}
+
+export interface VendorFactoryInfo {
+ factoryAddress: string;
+ factoryEstablishmentDate: string;
+ factoryPIC: string;
+ factoryPICContact: string;
+ factoryPICEmail: string;
+}
+
+export interface VendorInspectionInfo {
+ inspectionResult: string;
+ inspectionDate: string;
+ inspectionReportUrl?: string;
+}
+
+export interface VendorEvaluationInfo {
+ regularEvaluationGrade: string;
+ safetyQualificationEvaluation: string;
+ companyTransactionRatio: string;
+}
+
+export interface VendorClassificationInfo {
+ vendorClassification: string;
+ groupCompany: string;
+ preferredLanguage: string;
+ industryType: string;
+ isoCertification: string;
+}
+
+export interface VendorContractDetails {
+ regularRegistrationStatus: string;
+ preferredContractTerms: string;
+ recentTransactionStatus: string;
+ compliancePledgeDate: string;
+ technicalDataDate: string;
+ confidentialityDate: string;
+ gtcDate: string;
+ standardSubcontractDate: string;
+ safetyHealthDate: string;
+ directMaterialDate: string;
+ domesticLCDate: string;
+ mutualGrowthDate: string;
+ ethicsDate: string;
+}
+
+export interface VendorCapacityInfo {
+ annualSales: string;
+ productionCapacity: string;
+ mainSupplyItems: string;
+}
+
+export interface VendorCalculatedMetrics {
+ debtRatio: number;
+ borrowingDependency: number;
+ operatingMargin: number;
+ netMargin: number;
+ salesGrowth: number;
+ currentRatio: number;
+}
+
+export interface VendorData {
+ id: number;
+ vendorName: string;
+ vendorCode: string;
+ taxId: string;
+ address: string;
+ addressDetail: string;
+ postalCode: string;
+ businessSize: string;
+ country: string;
+ phone: string;
+ fax: string;
+ email: string;
+ website: string;
+ status: string;
+ representativeName: string;
+ representativeBirth: string;
+ representativeEmail: string;
+ representativePhone: string;
+ representativeWorkExperience: boolean;
+ corporateRegistrationNumber: string;
+ creditAgency: string;
+ creditRating: string;
+ cashFlowRating: string;
+ createdAt: string;
+ updatedAt: string;
+ contacts: VendorContact[];
+ attachments: VendorAttachment[];
+ processInfo: VendorProcessInfo;
+ contractInfo: VendorContractInfo;
+ salesInfo: {
+ [year: string]: VendorSalesData;
+ };
+ additionalInfo: VendorAdditionalInfo;
+ organization: VendorOrganization;
+ factoryInfo: VendorFactoryInfo;
+ inspectionInfo: VendorInspectionInfo;
+ evaluationInfo: VendorEvaluationInfo;
+ classificationInfo: VendorClassificationInfo;
+ contractDetails: VendorContractDetails;
+ capacityInfo: VendorCapacityInfo;
+ calculatedMetrics: {
+ [year: string]: VendorCalculatedMetrics;
+ };
+}
+
+export interface VendorFormData {
+ vendorName: string;
+ representativeName: string;
+ representativeWorkExperience: boolean;
+ representativeBirth: string;
+ representativePhone: string;
+ representativeEmail: string;
+ addressDetail: string;
+ postalCode: string;
+ phone: string;
+ fax: string;
+ email: string;
+ address: string;
+ businessSize: string;
+ country: string;
+ website: string;
+ businessType: string;
+ employeeCount: number;
+ mainBusiness: string;
+} \ No newline at end of file
diff --git a/lib/vendor-regular-registrations/table/vendor-regular-registrations-table-columns.tsx b/lib/vendor-regular-registrations/table/vendor-regular-registrations-table-columns.tsx
index 7446716b..8d21df24 100644
--- a/lib/vendor-regular-registrations/table/vendor-regular-registrations-table-columns.tsx
+++ b/lib/vendor-regular-registrations/table/vendor-regular-registrations-table-columns.tsx
@@ -214,6 +214,7 @@ export function getColumns(): ColumnDef<VendorRegularRegistration>[] {
open={documentDialogOpen}
onOpenChange={setDocumentDialogOpen}
registration={registration}
+ isVendorUser={false}
/>
</>
)
diff --git a/lib/vendors/service.ts b/lib/vendors/service.ts
index e3a38891..5d790a6e 100644
--- a/lib/vendors/service.ts
+++ b/lib/vendors/service.ts
@@ -2779,10 +2779,8 @@ export async function requestPQVendors(input: ApproveVendorsInput & {
? `[eVCP] You are invited to submit Non-Inspection PQ ${vendorPQ?.pqNumber || ""}`
: `[eVCP] You are invited to submit PQ ${vendorPQ?.pqNumber || ""}`;
- const baseLoginUrl = `${host}/partners/pq`;
- const loginUrl = input.projectId
- ? `${baseLoginUrl}?projectId=${input.projectId}`
- : baseLoginUrl;
+ const baseUrl = process.env.NEXT_PUBLIC_URL || `http://${host}`;
+ const loginUrl = `${baseUrl}/partners/pq_new`;
// 체크된 계약 항목 배열 생성
const contracts = input.agreements
@@ -2807,6 +2805,8 @@ export async function requestPQVendors(input: ApproveVendorsInput & {
}
}
+ console.log("loginUrl-pq", loginUrl);
+
await sendEmail({
to: vendor.email,
subject,
@@ -2959,8 +2959,9 @@ export async function requestBasicContractInfo({
const headersList = await headers();
const host = headersList.get('host') || 'localhost:3000';
// 로그인 또는 서명 페이지 URL 생성
- const baseUrl = `http://${host}`
+ const baseUrl = process.env.NEXT_PUBLIC_URL || `http://${host}`;
const loginUrl = `${baseUrl}/partners/basic-contract`;
+ console.log("loginUrl-basic-contract", loginUrl);
// 사용자 언어 설정 (기본값은 한국어)
const userLang = "ko";