summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-11-18 10:33:20 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-11-18 10:33:20 +0000
commit41ad2455ac47d8e2da331d7240ded1354df9a784 (patch)
tree7980d9a6e260f774dd3ff97c541399a32b9c9b4f
parent1e6d30c9f649dcaa0c1d24561af35d7a77fd51b2 (diff)
(최겸) 구매 피드백 반영
-rw-r--r--components/common/selectors/procurement-item/procurement-item-selector-dialog-single.tsx6
-rw-r--r--components/login/login-form.tsx19
-rw-r--r--components/login/partner-auth-form.tsx5
-rw-r--r--components/pq-input/pq-input-tabs.tsx136
-rw-r--r--components/vendor-regular-registrations/document-status-dialog.tsx852
-rw-r--r--components/vendor-regular-registrations/registration-request-dialog.tsx17
-rw-r--r--db/schema/bidding.ts1
-rw-r--r--lib/rfq-last/shared/rfq-items-dialog.tsx4
-rw-r--r--lib/rfq-last/vendor-response/editor/vendor-response-editor.tsx2
-rw-r--r--lib/rfq-last/vendor/rfq-vendor-table.tsx10
-rw-r--r--lib/vendor-regular-registrations/repository.ts614
11 files changed, 880 insertions, 786 deletions
diff --git a/components/common/selectors/procurement-item/procurement-item-selector-dialog-single.tsx b/components/common/selectors/procurement-item/procurement-item-selector-dialog-single.tsx
index dab65780..90d4975b 100644
--- a/components/common/selectors/procurement-item/procurement-item-selector-dialog-single.tsx
+++ b/components/common/selectors/procurement-item/procurement-item-selector-dialog-single.tsx
@@ -70,13 +70,13 @@ export interface ProcurementItemSelectorDialogSingleProps {
* ```
*/
export function ProcurementItemSelectorDialogSingle({
- triggerLabel = "품목 선택",
+ triggerLabel = "1회성 품목 선택",
triggerVariant = "outline",
triggerSize = "default",
selectedProcurementItem = null,
onProcurementItemSelect,
- title = "품목 선택",
- description = "품목을 검색하고 선택해주세요.",
+ title = "1회성 품목 선택",
+ description = "1회성 품목을 검색하고 선택해주세요.",
showConfirmButtons = false,
}: ProcurementItemSelectorDialogSingleProps) {
const [open, setOpen] = useState(false);
diff --git a/components/login/login-form.tsx b/components/login/login-form.tsx
index 5fe6ab51..ee84add2 100644
--- a/components/login/login-form.tsx
+++ b/components/login/login-form.tsx
@@ -950,15 +950,16 @@ export function LoginForm() {
{/* Terms - MFA 화면에서는 숨김 */}
{!showMfaForm && (
- <div className="text-balance text-center text-xs text-muted-foreground [&_a]:underline [&_a]:underline-offset-4 hover:[&_a]:text-primary">
- {t("agreement")}{" "}
- <Link
- href={`/${lng}/privacy`}
- className="underline underline-offset-4 hover:text-primary"
- >
- {t("privacyPolicy")}
- </Link>
- </div>
+ // 1118 구매 파워유저 요구사항에 따라 삭제
+ // <div className="text-balance text-center text-xs text-muted-foreground [&_a]:underline [&_a]:underline-offset-4 hover:[&_a]:text-primary">
+ // {t("agreement")}{" "}
+ // <Link
+ // href={`/${lng}/privacy`}
+ // className="underline underline-offset-4 hover:text-primary"
+ // >
+ // {t("privacyPolicy")}
+ // </Link>
+ // </div>
)}
</div>
</div>
diff --git a/components/login/partner-auth-form.tsx b/components/login/partner-auth-form.tsx
index 22917997..ebd2219c 100644
--- a/components/login/partner-auth-form.tsx
+++ b/components/login/partner-auth-form.tsx
@@ -300,13 +300,14 @@ export function CompanyAuthForm({ className, ...props }: React.HTMLAttributes<HT
</form>
</div>
<p className="px-8 text-center text-sm text-muted-foreground">
- {t("agreement")}{" "}
+ {/* 1118 구매 파워유저 요구사항에 따라 삭제 */}
+ {/* {t("agreement")}{" "}
<Link
href={`/${lng}/privacy`} // 개인정보처리방침만 남김
className="underline underline-offset-4 hover:text-primary"
>
{t("privacyPolicy")}
- </Link>
+ </Link> */}
{/* {t("privacyAgreement")}. */}
</p>
</div>
diff --git a/components/pq-input/pq-input-tabs.tsx b/components/pq-input/pq-input-tabs.tsx
index f0d44d04..df911d5e 100644
--- a/components/pq-input/pq-input-tabs.tsx
+++ b/components/pq-input/pq-input-tabs.tsx
@@ -276,6 +276,55 @@ export function PQInputTabs({
setAllSaved(allItemsSaved)
}, [form.watch()])
+ // ----------------------------------------------------------------------
+ // C-1) Calculate item counts for display
+ // ----------------------------------------------------------------------
+
+ // ----------------------------------------------------------------------
+ // C-2) Tab color mapping for better visual distinction
+ // ----------------------------------------------------------------------
+ const getTabColorClasses = (groupName: string) => {
+ switch (groupName.toLowerCase()) {
+ case 'general':
+ return {
+ tab: 'data-[state=active]:bg-blue-50 data-[state=active]:text-blue-700 data-[state=active]:border-blue-200',
+ badge: 'bg-blue-100 text-blue-800 border-blue-200'
+ }
+ case 'hsg':
+ return {
+ tab: 'data-[state=active]:bg-green-50 data-[state=active]:text-green-700 data-[state=active]:border-green-200',
+ badge: 'bg-green-100 text-green-800 border-green-200'
+ }
+ case 'qms':
+ return {
+ tab: 'data-[state=active]:bg-orange-50 data-[state=active]:text-orange-700 data-[state=active]:border-orange-200',
+ badge: 'bg-orange-100 text-orange-800 border-orange-200'
+ }
+ case 'warranty':
+ return {
+ tab: 'data-[state=active]:bg-red-50 data-[state=active]:text-red-700 data-[state=active]:border-red-200',
+ badge: 'bg-red-100 text-red-800 border-red-200'
+ }
+ default:
+ return {
+ tab: 'data-[state=active]:bg-gray-50 data-[state=active]:text-gray-700 data-[state=active]:border-gray-200',
+ badge: 'bg-gray-100 text-gray-800 border-gray-200'
+ }
+ }
+ }
+ const getItemCounts = () => {
+ const values = form.getValues()
+ const totalItems = values.answers.length
+ const savedItems = values.answers.filter(
+ (answer) => answer.saved && (answer.answer || answer.uploadedFiles.length > 0)
+ ).length
+ const notSavedItems = totalItems - savedItems
+
+ return { totalItems, savedItems, notSavedItems }
+ }
+
+ const { totalItems, savedItems, notSavedItems } = getItemCounts()
+
// Helper to find the array index by criteriaId
const getAnswerIndex = (criteriaId: number): number => {
return form.getValues().answers.findIndex((a) => a.criteriaId === criteriaId)
@@ -677,6 +726,30 @@ export function PQInputTabs({
<Tabs defaultValue={data[0]?.groupName || ""} className="w-full">
{/* Top Controls - Sticky Header */}
<div className="sticky top-0 z-10 bg-background border-b border-border mb-4 pb-4">
+ {/* Item Count Display */}
+ <div className="mb-3 flex items-center gap-6 text-sm">
+ <div className="flex items-center gap-4">
+ <span className="font-medium">총 항목:</span>
+ <Badge variant="outline" className="text-xs">
+ {totalItems}
+ </Badge>
+ </div>
+ <div className="flex items-center gap-2">
+ <CheckCircle2 className="h-4 w-4 text-green-600" />
+ <span className="text-green-600 font-medium">Saved:</span>
+ <Badge variant="outline" className="bg-green-50 text-green-700 border-green-200 text-xs">
+ {savedItems}
+ </Badge>
+ </div>
+ <div className="flex items-center gap-2">
+ <AlertTriangle className="h-4 w-4 text-amber-600" />
+ <span className="text-amber-600 font-medium">Not Saved:</span>
+ <Badge variant="outline" className="bg-amber-50 text-amber-700 border-amber-200 text-xs">
+ {notSavedItems}
+ </Badge>
+ </div>
+ </div>
+
{/* Filter Controls */}
<div className="mb-3 flex items-center gap-4">
<span className="text-sm font-medium">필터:</span>
@@ -702,8 +775,11 @@ export function PQInputTabs({
checked={filterOptions.showSaved}
onCheckedChange={(checked) => {
const newOptions = { ...filterOptions, showSaved: !!checked };
- if (!checked && !filterOptions.showAll && !filterOptions.showNotSaved) {
- // 최소 하나는 체크되어 있어야 함
+ // Save 항목이나 Not Save 항목을 선택하면 전체 항목 자동 해제
+ if (checked) {
+ newOptions.showAll = false;
+ } else if (!filterOptions.showNotSaved && !filterOptions.showAll) {
+ // 최소 하나는 체크되어 있어야 함 - 모두 해제되면 전체 항목 체크
newOptions.showAll = true;
}
setFilterOptions(newOptions);
@@ -717,8 +793,11 @@ export function PQInputTabs({
checked={filterOptions.showNotSaved}
onCheckedChange={(checked) => {
const newOptions = { ...filterOptions, showNotSaved: !!checked };
- if (!checked && !filterOptions.showAll && !filterOptions.showSaved) {
- // 최소 하나는 체크되어 있어야 함
+ // Save 항목이나 Not Save 항목을 선택하면 전체 항목 자동 해제
+ if (checked) {
+ newOptions.showAll = false;
+ } else if (!filterOptions.showSaved && !filterOptions.showAll) {
+ // 최소 하나는 체크되어 있어야 함 - 모두 해제되면 전체 항목 체크
newOptions.showAll = true;
}
setFilterOptions(newOptions);
@@ -731,27 +810,30 @@ export function PQInputTabs({
<div className="flex justify-between items-center">
<TabsList className="grid grid-cols-4">
- {data.map((group) => (
- <TabsTrigger
- key={group.groupName}
- value={group.groupName}
- className="truncate"
- >
- <div className="flex items-center gap-2">
- {/* Mobile: truncated version */}
- <span className="block sm:hidden">
- {group.groupName.length > 5
- ? group.groupName.slice(0, 5) + "..."
- : group.groupName}
- </span>
- {/* Desktop: full text */}
- <span className="hidden sm:block">{group.groupName}</span>
- <span className="inline-flex items-center justify-center h-5 min-w-5 px-1 rounded-full bg-muted text-xs font-medium">
- {group.items.length}
- </span>
- </div>
- </TabsTrigger>
- ))}
+ {data.map((group) => {
+ const colorClasses = getTabColorClasses(group.groupName)
+ return (
+ <TabsTrigger
+ key={group.groupName}
+ value={group.groupName}
+ className={`truncate ${colorClasses.tab}`}
+ >
+ <div className="flex items-center gap-2">
+ {/* Mobile: truncated version */}
+ <span className="block sm:hidden">
+ {group.groupName.length > 5
+ ? group.groupName.slice(0, 5) + "..."
+ : group.groupName}
+ </span>
+ {/* Desktop: full text */}
+ <span className="hidden sm:block">{group.groupName}</span>
+ <span className={`inline-flex items-center justify-center h-5 min-w-5 px-1 rounded-full text-xs font-medium ${colorClasses.badge}`}>
+ {group.items.length}
+ </span>
+ </div>
+ </TabsTrigger>
+ )
+ })}
</TabsList>
<div className="flex gap-2">
@@ -849,13 +931,13 @@ export function PQInputTabs({
{/* Save Status & Button */}
<div className="flex items-center gap-2">
{!isSaved && canSave && (
- <span className="text-amber-600 text-xs flex items-center">
+ <span className="text-amber-600 text-sm font-medium flex items-center">
<AlertTriangle className="h-4 w-4 mr-1" />
Not Saved
</span>
)}
{isSaved && (
- <span className="text-green-600 text-xs flex items-center">
+ <span className="text-green-600 text-sm font-medium flex items-center">
<CheckCircle2 className="h-4 w-4 mr-1" />
Saved
</span>
diff --git a/components/vendor-regular-registrations/document-status-dialog.tsx b/components/vendor-regular-registrations/document-status-dialog.tsx
index 5efee64e..02da19bf 100644
--- a/components/vendor-regular-registrations/document-status-dialog.tsx
+++ b/components/vendor-regular-registrations/document-status-dialog.tsx
@@ -1,426 +1,426 @@
-"use client";
-
-import {
- Dialog,
- DialogContent,
- DialogHeader,
- DialogTitle,
-} from "@/components/ui/dialog";
-import { Badge } from "@/components/ui/badge";
-import { Button } from "@/components/ui/button";
-import { FileText, Download, CheckCircle, XCircle, Clock, RefreshCw } from "lucide-react";
-import { toast } from "sonner";
-
-import type { VendorRegularRegistration } from "@/config/vendorRegularRegistrationsColumnsConfig";
-import {
- documentStatusColumns,
-} from "@/config/vendorRegularRegistrationsColumnsConfig";
-
-interface DocumentStatusDialogProps {
- open: boolean;
- onOpenChange: (open: boolean) => void;
- registration: VendorRegularRegistration | null;
- onRefresh?: () => void;
- isVendorUser?: boolean;
-}
-
-const StatusIcon = ({ status }: { status: string | boolean }) => {
- if (typeof status === "boolean") {
- return status ? (
- <CheckCircle className="w-4 h-4 text-green-600" />
- ) : (
- <XCircle className="w-4 h-4 text-red-500" />
- );
- }
-
- switch (status) {
- case "completed":
- return <CheckCircle className="w-4 h-4 text-green-600" />;
- case "reviewing":
- return <Clock className="w-4 h-4 text-yellow-600" />;
- case "not_submitted":
- default:
- return <XCircle className="w-4 h-4 text-red-500" />;
- }
-};
-
-const StatusBadge = ({ status }: { status: string | boolean }) => {
- if (typeof status === "boolean") {
- return (
- <Badge variant={status ? "default" : "destructive"}>
- {status ? "제출완료" : "미제출"}
- </Badge>
- );
- }
-
- const statusConfig = {
- completed: { label: "완료", variant: "default" as const },
- reviewing: { label: "검토중", variant: "secondary" as const },
- not_submitted: { label: "미제출", variant: "destructive" as const },
- };
-
- const config = statusConfig[status as keyof typeof statusConfig] || statusConfig.not_submitted;
-
- return <Badge variant={config.variant}>{config.label}</Badge>;
-};
-
-export function DocumentStatusDialog({
- open,
- onOpenChange,
- registration,
- onRefresh,
- isVendorUser = false,
-}: DocumentStatusDialogProps) {
- if (!registration) return null;
-
- // 파일 다운로드 핸들러
- const handleFileDownload = async (docKey: string, fileIndex: number = 0) => {
- try {
- console.log(`🔍 파일 다운로드 시도:`, {
- docKey,
- fileIndex,
- allDocumentFiles: registration.documentFiles,
- registrationId: registration.id,
- registrationKeys: Object.keys(registration),
- fullRegistration: registration
- });
- //isvendoruser인 경우는 실사 결과 파일 다운로드 불가능
- if (isVendorUser && docKey === "auditResult") {
- toast.error("실사 결과 파일은 다운로드할 수 없습니다.");
- return;
- }
-
- // documentFiles가 없는 경우 처리
- if (!registration.documentFiles) {
- console.error(`❌ documentFiles가 없음:`, {
- registration,
- hasDocumentFiles: !!registration.documentFiles,
- registrationKeys: Object.keys(registration)
- });
- toast.error("문서 파일 정보를 찾을 수 없습니다. 페이지를 새로고침 해주세요.");
- return;
- }
-
- const files = registration.documentFiles[docKey as keyof typeof registration.documentFiles];
- console.log(`📂 ${docKey} 파일 목록:`, files);
-
- if (!files || files.length === 0) {
- console.warn(`❌ ${docKey}에 파일이 없음:`, { files, length: files?.length });
- toast.error("다운로드할 파일이 없습니다.");
- return;
- }
-
- const file = files[fileIndex];
- console.log(`📄 선택된 파일 (index ${fileIndex}):`, file);
-
- if (!file) {
- console.warn(`❌ 파일 인덱스 ${fileIndex}에 파일이 없음`);
- toast.error("파일을 찾을 수 없습니다.");
- return;
- }
-
- // 파일 객체의 모든 속성 확인
- console.log(`🔍 파일 객체 전체 속성:`, Object.keys(file));
- console.log(`🔍 파일 상세 정보:`, {
- filePath: file.filePath,
- path: file.path,
- originalFileName: file.originalFileName,
- fileName: file.fileName,
- name: file.name,
- fullObject: file
- });
-
- // filePath와 fileName 추출
- const filePath = file.filePath || file.path;
- const fileName = file.originalFileName || file.fileName || file.name;
-
- console.log(`📝 추출된 파일 정보:`, { filePath, fileName });
-
- if (!filePath || !fileName) {
- console.error(`❌ 파일 정보 누락:`, {
- filePath,
- fileName,
- fileObject: file,
- availableKeys: Object.keys(file)
- });
- toast.error("파일 정보가 올바르지 않습니다.");
- return;
- }
-
- console.log(`📥 파일 다운로드 시작:`, { filePath, fileName, docKey });
-
- // downloadFile 함수를 동적으로 import하여 파일 다운로드
- const { downloadFile } = await import('@/lib/file-download');
- const result = await downloadFile(filePath, fileName, {
- showToast: true,
- onError: (error: any) => {
- console.error("파일 다운로드 오류:", error);
- toast.error(`파일 다운로드 실패: ${error}`);
- },
- onSuccess: (fileName: string, fileSize?: number) => {
- console.log(`✅ 파일 다운로드 성공:`, { fileName, fileSize });
- }
- });
-
- if (!result.success) {
- console.error("파일 다운로드 실패:", result.error);
- }
- } catch (error) {
- console.error("파일 다운로드 중 오류 발생:", error);
- toast.error("파일 다운로드 중 오류가 발생했습니다.");
- }
- };
-
- // 기본계약 파일 다운로드 핸들러
- const handleContractDownload = async (contractIndex: number) => {
- try {
- if (!registration.basicContracts || registration.basicContracts.length === 0) {
- toast.error("다운로드할 계약이 없습니다.");
- return;
- }
-
- const contract = registration.basicContracts[contractIndex];
- if (!contract) {
- toast.error("계약을 찾을 수 없습니다.");
- return;
- }
-
- if (contract.status !== "VENDOR_SIGNED" && contract.status !== "COMPLETED") {
- toast.error("완료된 계약서만 다운로드할 수 있습니다.");
- return;
- }
-
- // 서명된 계약서 파일 정보 확인
- const filePath = contract.filePath;
- const fileName = contract.fileName || `${contract.templateName || '기본계약'}_${registration.companyName}.docx`;
-
- if (!filePath) {
- toast.error("계약서 파일을 찾을 수 없습니다.");
- return;
- }
-
- console.log(`📥 기본계약 다운로드 시작:`, {
- filePath,
- fileName,
- templateName: contract.templateName
- });
-
- // downloadFile 함수를 사용하여 서명된 계약서 다운로드
- const { downloadFile } = await import('@/lib/file-download');
- const result = await downloadFile(filePath, fileName, {
- showToast: true,
- onError: (error: any) => {
- console.error("기본계약 다운로드 오류:", error);
- toast.error(`기본계약 다운로드 실패: ${error}`);
- },
- onSuccess: (fileName: string, fileSize?: number) => {
- console.log(`✅ 기본계약 다운로드 성공:`, { fileName, fileSize });
- }
- });
-
- if (!result.success) {
- console.error("기본계약 다운로드 실패:", result.error);
- }
- } catch (error) {
- console.error("기본계약 다운로드 중 오류 발생:", error);
- toast.error("기본계약 다운로드 중 오류가 발생했습니다.");
- }
- };
-
- return (
- <Dialog open={open} onOpenChange={onOpenChange}>
- <DialogContent className="max-w-4xl max-h-[80vh]">
- <DialogHeader className="sticky top-0 z-10 border-b pr-4 pb-4 mb-4">
- <DialogTitle className="flex items-center justify-between">
- <div className="flex items-center gap-2">
- <FileText className="w-5 h-5" />
- 문서/자료 접수 현황 - {registration.companyName}
- </div>
- {onRefresh && (
- <Button
- variant="outline"
- size="sm"
- onClick={onRefresh}
- className="flex items-center gap-2"
- >
- <RefreshCw className="w-4 h-4" />
- 새로고침
- </Button>
- )}
- </DialogTitle>
- </DialogHeader>
-
- <div className="overflow-y-auto max-h-[calc(80vh-120px)] space-y-6">
- {/* 기본 정보 */}
- <div className="grid grid-cols-2 gap-4 p-4 bg-gray-50 rounded-lg">
- <div>
- <span className="text-sm font-medium text-gray-600">업체명:</span>
- <span className="ml-2">{registration.companyName}</span>
- </div>
- <div>
- <span className="text-sm font-medium text-gray-600">사업자번호:</span>
- <span className="ml-2">{registration.businessNumber}</span>
- </div>
- <div>
- <span className="text-sm font-medium text-gray-600">대표자:</span>
- <span className="ml-2">{registration.representative || "-"}</span>
- </div>
- <div>
- <span className="text-sm font-medium text-gray-600">현재상태:</span>
- <Badge className="ml-2">{registration.status}</Badge>
- </div>
- </div>
-
- {/* 문서 제출 현황 */}
- <div>
- <div className="flex items-center justify-between mb-4">
- <h3 className="text-lg font-semibold">문서 제출 현황</h3>
- </div>
- <div className="border rounded-lg">
- <div className="grid grid-cols-4 gap-4 p-4 bg-gray-50 font-medium text-sm">
- <div>문서유형</div>
- <div>상태</div>
- <div>제출일자</div>
- <div>액션</div>
- </div>
- {documentStatusColumns.map((doc) => {
- const isSubmitted = registration.documentSubmissions?.[
- doc.key as keyof typeof registration.documentSubmissions
- ] as boolean || false;
-
- // 내자인 경우 통장사본은 표시하지 않음
- const isForeign = registration.country !== 'KR';
- if (doc.key === 'bankCopy' && !isForeign) {
- return null;
- }
-
- return (
- <div
- key={doc.key}
- className="grid grid-cols-4 gap-4 p-4 border-t items-center"
- >
- <div className="flex items-center gap-2">
- <StatusIcon status={isSubmitted} />
- {doc.label}
- {doc.key === 'bankCopy' && isForeign && (
- <span className="text-xs text-blue-600">(외자 필수)</span>
- )}
- </div>
- <div>
- <StatusBadge status={isSubmitted} />
- </div>
- <div className="text-sm text-gray-600">
- {isSubmitted ? "2024.01.01" : "-"}
- </div>
- <div>
- {isSubmitted && (
- <Button
- size="sm"
- variant="outline"
- onClick={() => handleFileDownload(doc.key)}
- >
- <Download className="w-4 h-4 mr-1" />
- 다운로드
- </Button>
- )}
- </div>
- </div>
- );
- })}
- </div>
- </div>
-
- {/* 계약 동의 현황 */}
- <div>
- <div className="flex items-center justify-between mb-4">
- <h3 className="text-lg font-semibold">계약 동의 현황</h3>
- </div>
- <div className="border rounded-lg">
- <div className="grid grid-cols-4 gap-4 p-4 bg-gray-50 font-medium text-sm">
- <div>계약유형</div>
- <div>상태</div>
- <div>서약일자</div>
- <div>액션</div>
- </div>
- {!registration.basicContracts || registration.basicContracts.length === 0 ? (
- <div className="p-4 border-t text-center text-gray-500">
- 요청된 기본계약이 없습니다.
- </div>
- ) : (
- registration.basicContracts.map((contract, index) => {
- const isCompleted = contract.status === "VENDOR_SIGNED" || contract.status === "COMPLETED";
-
- return (
- <div
- key={`${contract.templateId}-${index}`}
- className="grid grid-cols-4 gap-4 p-4 border-t items-center"
- >
- <div className="flex items-center gap-2">
- <StatusIcon status={isCompleted} />
- {contract.templateName || "템플릿명 없음"}
- </div>
- <div>
- <StatusBadge status={isCompleted} />
- </div>
- <div className="text-sm text-gray-600">
- {isCompleted && contract.createdAt
- ? new Intl.DateTimeFormat('ko-KR').format(new Date(contract.createdAt))
- : "-"
- }
- </div>
- <div>
- {isCompleted && (
- <Button
- size="sm"
- variant="outline"
- onClick={() => handleContractDownload(index)}
- >
- <Download className="w-4 h-4 mr-1" />
- 다운로드
- </Button>
- )}
- </div>
- </div>
- );
- })
- )}
- </div>
- </div>
-
- {/* 안전적격성 평가 */}
- <div>
- <h3 className="text-lg font-semibold mb-4">안전적격성 평가</h3>
- <div className="p-4 border rounded-lg">
- <div className="flex items-center justify-between">
- <div className="flex items-center gap-2">
- <StatusIcon status={!!registration.safetyQualificationContent} />
- <span>안전적격성 평가</span>
- </div>
- <StatusBadge status={!!registration.safetyQualificationContent} />
- </div>
- {registration.safetyQualificationContent && (
- <div className="mt-3 p-3 bg-gray-50 rounded">
- <p className="text-sm">{registration.safetyQualificationContent}</p>
- </div>
- )}
- </div>
- </div>
-
- {/* 추가 정보 */}
- <div>
- <h3 className="text-lg font-semibold mb-4">추가 정보</h3>
- <div className="p-4 border rounded-lg">
- <div className="flex items-center justify-between">
- <div className="flex items-center gap-2">
- <StatusIcon status={registration.additionalInfo} />
- <span>추가 정보 등록</span>
- </div>
- <StatusBadge status={registration.additionalInfo} />
- </div>
- </div>
- </div>
- </div>
- </DialogContent>
- </Dialog>
- );
-}
+"use client";
+
+import {
+ Dialog,
+ DialogContent,
+ DialogHeader,
+ DialogTitle,
+} from "@/components/ui/dialog";
+import { Badge } from "@/components/ui/badge";
+import { Button } from "@/components/ui/button";
+import { FileText, Download, CheckCircle, XCircle, Clock, RefreshCw } from "lucide-react";
+import { toast } from "sonner";
+
+import type { VendorRegularRegistration } from "@/config/vendorRegularRegistrationsColumnsConfig";
+import {
+ documentStatusColumns,
+} from "@/config/vendorRegularRegistrationsColumnsConfig";
+
+interface DocumentStatusDialogProps {
+ open: boolean;
+ onOpenChange: (open: boolean) => void;
+ registration: VendorRegularRegistration | null;
+ onRefresh?: () => void;
+ isVendorUser?: boolean;
+}
+
+const StatusIcon = ({ status }: { status: string | boolean }) => {
+ if (typeof status === "boolean") {
+ return status ? (
+ <CheckCircle className="w-4 h-4 text-green-600" />
+ ) : (
+ <XCircle className="w-4 h-4 text-red-500" />
+ );
+ }
+
+ switch (status) {
+ case "completed":
+ return <CheckCircle className="w-4 h-4 text-green-600" />;
+ case "reviewing":
+ return <Clock className="w-4 h-4 text-yellow-600" />;
+ case "not_submitted":
+ default:
+ return <XCircle className="w-4 h-4 text-red-500" />;
+ }
+};
+
+const StatusBadge = ({ status }: { status: string | boolean }) => {
+ if (typeof status === "boolean") {
+ return (
+ <Badge variant={status ? "default" : "destructive"}>
+ {status ? "제출완료" : "미제출"}
+ </Badge>
+ );
+ }
+
+ const statusConfig = {
+ completed: { label: "완료", variant: "default" as const },
+ reviewing: { label: "검토중", variant: "secondary" as const },
+ not_submitted: { label: "미제출", variant: "destructive" as const },
+ };
+
+ const config = statusConfig[status as keyof typeof statusConfig] || statusConfig.not_submitted;
+
+ return <Badge variant={config.variant}>{config.label}</Badge>;
+};
+
+export function DocumentStatusDialog({
+ open,
+ onOpenChange,
+ registration,
+ onRefresh,
+ isVendorUser = false,
+}: DocumentStatusDialogProps) {
+ if (!registration) return null;
+
+ // 파일 다운로드 핸들러
+ const handleFileDownload = async (docKey: string, fileIndex: number = 0) => {
+ try {
+ console.log(`🔍 파일 다운로드 시도:`, {
+ docKey,
+ fileIndex,
+ allDocumentFiles: registration.documentFiles,
+ registrationId: registration.id,
+ registrationKeys: Object.keys(registration),
+ fullRegistration: registration
+ });
+ //isvendoruser인 경우는 실사 결과 파일 다운로드 불가능
+ if (isVendorUser && docKey === "auditResult") {
+ toast.error("실사 결과 파일은 다운로드할 수 없습니다.");
+ return;
+ }
+
+ // documentFiles가 없는 경우 처리
+ if (!registration.documentFiles) {
+ console.error(`❌ documentFiles가 없음:`, {
+ registration,
+ hasDocumentFiles: !!registration.documentFiles,
+ registrationKeys: Object.keys(registration)
+ });
+ toast.error("문서 파일 정보를 찾을 수 없습니다. 페이지를 새로고침 해주세요.");
+ return;
+ }
+
+ const files = registration.documentFiles[docKey as keyof typeof registration.documentFiles];
+ console.log(`📂 ${docKey} 파일 목록:`, files);
+
+ if (!files || files.length === 0) {
+ console.warn(`❌ ${docKey}에 파일이 없음:`, { files, length: files?.length });
+ toast.error("다운로드할 파일이 없습니다.");
+ return;
+ }
+
+ const file = files[fileIndex];
+ console.log(`📄 선택된 파일 (index ${fileIndex}):`, file);
+
+ if (!file) {
+ console.warn(`❌ 파일 인덱스 ${fileIndex}에 파일이 없음`);
+ toast.error("파일을 찾을 수 없습니다.");
+ return;
+ }
+
+ // 파일 객체의 모든 속성 확인
+ console.log(`🔍 파일 객체 전체 속성:`, Object.keys(file));
+ console.log(`🔍 파일 상세 정보:`, {
+ filePath: file.filePath,
+ path: file.path,
+ originalFileName: file.originalFileName,
+ fileName: file.fileName,
+ name: file.name,
+ fullObject: file
+ });
+
+ // filePath와 fileName 추출
+ const filePath = file.filePath || file.path;
+ const fileName = file.originalFileName || file.fileName || file.name;
+
+ console.log(`📝 추출된 파일 정보:`, { filePath, fileName });
+
+ if (!filePath || !fileName) {
+ console.error(`❌ 파일 정보 누락:`, {
+ filePath,
+ fileName,
+ fileObject: file,
+ availableKeys: Object.keys(file)
+ });
+ toast.error("파일 정보가 올바르지 않습니다.");
+ return;
+ }
+
+ console.log(`📥 파일 다운로드 시작:`, { filePath, fileName, docKey });
+
+ // downloadFile 함수를 동적으로 import하여 파일 다운로드
+ const { downloadFile } = await import('@/lib/file-download');
+ const result = await downloadFile(filePath, fileName, {
+ showToast: true,
+ onError: (error: any) => {
+ console.error("파일 다운로드 오류:", error);
+ toast.error(`파일 다운로드 실패: ${error}`);
+ },
+ onSuccess: (fileName: string, fileSize?: number) => {
+ console.log(`✅ 파일 다운로드 성공:`, { fileName, fileSize });
+ }
+ });
+
+ if (!result.success) {
+ console.error("파일 다운로드 실패:", result.error);
+ }
+ } catch (error) {
+ console.error("파일 다운로드 중 오류 발생:", error);
+ toast.error("파일 다운로드 중 오류가 발생했습니다.");
+ }
+ };
+
+ // 기본계약 파일 다운로드 핸들러
+ const handleContractDownload = async (contractIndex: number) => {
+ try {
+ if (!registration.basicContracts || registration.basicContracts.length === 0) {
+ toast.error("다운로드할 계약이 없습니다.");
+ return;
+ }
+
+ const contract = registration.basicContracts[contractIndex];
+ if (!contract) {
+ toast.error("계약을 찾을 수 없습니다.");
+ return;
+ }
+
+ if (contract.status !== "VENDOR_SIGNED" && contract.status !== "COMPLETED") {
+ toast.error("완료된 계약서만 다운로드할 수 있습니다.");
+ return;
+ }
+
+ // 서명된 계약서 파일 정보 확인
+ const filePath = contract.filePath;
+ const fileName = contract.fileName || `${contract.templateName || '기본계약'}_${registration.companyName}.docx`;
+
+ if (!filePath) {
+ toast.error("계약서 파일을 찾을 수 없습니다.");
+ return;
+ }
+
+ console.log(`📥 기본계약 다운로드 시작:`, {
+ filePath,
+ fileName,
+ templateName: contract.templateName
+ });
+
+ // downloadFile 함수를 사용하여 서명된 계약서 다운로드
+ const { downloadFile } = await import('@/lib/file-download');
+ const result = await downloadFile(filePath, fileName, {
+ showToast: true,
+ onError: (error: any) => {
+ console.error("기본계약 다운로드 오류:", error);
+ toast.error(`기본계약 다운로드 실패: ${error}`);
+ },
+ onSuccess: (fileName: string, fileSize?: number) => {
+ console.log(`✅ 기본계약 다운로드 성공:`, { fileName, fileSize });
+ }
+ });
+
+ if (!result.success) {
+ console.error("기본계약 다운로드 실패:", result.error);
+ }
+ } catch (error) {
+ console.error("기본계약 다운로드 중 오류 발생:", error);
+ toast.error("기본계약 다운로드 중 오류가 발생했습니다.");
+ }
+ };
+
+ return (
+ <Dialog open={open} onOpenChange={onOpenChange}>
+ <DialogContent className="max-w-4xl max-h-[80vh]">
+ <DialogHeader className="sticky top-0 z-10 border-b pr-4 pb-4 mb-4">
+ <DialogTitle className="flex items-center justify-between">
+ <div className="flex items-center gap-2">
+ <FileText className="w-5 h-5" />
+ 문서/자료 접수 현황 - {registration.companyName}
+ </div>
+ {onRefresh && (
+ <Button
+ variant="outline"
+ size="sm"
+ onClick={onRefresh}
+ className="flex items-center gap-2"
+ >
+ <RefreshCw className="w-4 h-4" />
+ 새로고침
+ </Button>
+ )}
+ </DialogTitle>
+ </DialogHeader>
+
+ <div className="overflow-y-auto max-h-[calc(80vh-120px)] space-y-6">
+ {/* 기본 정보 */}
+ <div className="grid grid-cols-2 gap-4 p-4 bg-gray-50 rounded-lg">
+ <div>
+ <span className="text-sm font-medium text-gray-600">업체명:</span>
+ <span className="ml-2">{registration.companyName}</span>
+ </div>
+ <div>
+ <span className="text-sm font-medium text-gray-600">사업자번호:</span>
+ <span className="ml-2">{registration.businessNumber}</span>
+ </div>
+ <div>
+ <span className="text-sm font-medium text-gray-600">대표자:</span>
+ <span className="ml-2">{registration.representative || "-"}</span>
+ </div>
+ <div>
+ <span className="text-sm font-medium text-gray-600">현재상태:</span>
+ <Badge className="ml-2">{registration.status}</Badge>
+ </div>
+ </div>
+
+ {/* 문서 제출 현황 */}
+ <div>
+ <div className="flex items-center justify-between mb-4">
+ <h3 className="text-lg font-semibold">문서 제출 현황</h3>
+ </div>
+ <div className="border rounded-lg">
+ <div className="grid grid-cols-4 gap-4 p-4 bg-gray-50 font-medium text-sm">
+ <div>문서유형</div>
+ <div>상태</div>
+ <div>제출일자</div>
+ <div>액션</div>
+ </div>
+ {documentStatusColumns.map((doc) => {
+ const isSubmitted = registration.documentSubmissions?.[
+ doc.key as keyof typeof registration.documentSubmissions
+ ] as boolean || false;
+
+ // 내자인 경우 통장사본은 표시하지 않음
+ const isForeign = registration.country !== 'KR';
+ if (doc.key === 'bankCopy' && !isForeign) {
+ return null;
+ }
+
+ return (
+ <div
+ key={doc.key}
+ className="grid grid-cols-4 gap-4 p-4 border-t items-center"
+ >
+ <div className="flex items-center gap-2">
+ <StatusIcon status={isSubmitted} />
+ {doc.label}
+ {doc.key === 'bankCopy' && isForeign && (
+ <span className="text-xs text-blue-600">(외자 필수)</span>
+ )}
+ </div>
+ <div>
+ <StatusBadge status={isSubmitted} />
+ </div>
+ <div className="text-sm text-gray-600">
+ {isSubmitted ? "2024.01.01" : "-"}
+ </div>
+ <div>
+ {isSubmitted && (
+ <Button
+ size="sm"
+ variant="outline"
+ onClick={() => handleFileDownload(doc.key)}
+ >
+ <Download className="w-4 h-4 mr-1" />
+ 다운로드
+ </Button>
+ )}
+ </div>
+ </div>
+ );
+ })}
+ </div>
+ </div>
+
+ {/* 계약 동의 현황 */}
+ <div>
+ <div className="flex items-center justify-between mb-4">
+ <h3 className="text-lg font-semibold">계약 동의 현황</h3>
+ </div>
+ <div className="border rounded-lg">
+ <div className="grid grid-cols-4 gap-4 p-4 bg-gray-50 font-medium text-sm">
+ <div>계약유형</div>
+ <div>상태</div>
+ <div>서약일자</div>
+ <div>액션</div>
+ </div>
+ {!registration.basicContracts || registration.basicContracts.length === 0 ? (
+ <div className="p-4 border-t text-center text-gray-500">
+ 요청된 기본계약이 없습니다.
+ </div>
+ ) : (
+ registration.basicContracts.map((contract, index) => {
+ const isCompleted = contract.status === "VENDOR_SIGNED" || contract.status === "COMPLETED";
+
+ return (
+ <div
+ key={`${contract.templateId}-${index}`}
+ className="grid grid-cols-4 gap-4 p-4 border-t items-center"
+ >
+ <div className="flex items-center gap-2">
+ <StatusIcon status={isCompleted} />
+ {contract.templateName || "템플릿명 없음"}
+ </div>
+ <div>
+ <StatusBadge status={isCompleted} />
+ </div>
+ <div className="text-sm text-gray-600">
+ {isCompleted && contract.createdAt
+ ? new Intl.DateTimeFormat('ko-KR').format(new Date(contract.createdAt))
+ : "-"
+ }
+ </div>
+ <div>
+ {isCompleted && (
+ <Button
+ size="sm"
+ variant="outline"
+ onClick={() => handleContractDownload(index)}
+ >
+ <Download className="w-4 h-4 mr-1" />
+ 다운로드
+ </Button>
+ )}
+ </div>
+ </div>
+ );
+ })
+ )}
+ </div>
+ </div>
+
+ {/* 안전적격성 평가 */}
+ <div>
+ <h3 className="text-lg font-semibold mb-4">안전적격성 평가</h3>
+ <div className="p-4 border rounded-lg">
+ <div className="flex items-center justify-between">
+ <div className="flex items-center gap-2">
+ <StatusIcon status={!!registration.safetyQualificationContent} />
+ <span>안전적격성 평가</span>
+ </div>
+ <StatusBadge status={!!registration.safetyQualificationContent} />
+ </div>
+ {registration.safetyQualificationContent && (
+ <div className="mt-3 p-3 bg-gray-50 rounded">
+ <p className="text-sm">{registration.safetyQualificationContent}</p>
+ </div>
+ )}
+ </div>
+ </div>
+
+ {/* 추가 정보 */}
+ <div>
+ <h3 className="text-lg font-semibold mb-4">추가 정보</h3>
+ <div className="p-4 border rounded-lg">
+ <div className="flex items-center justify-between">
+ <div className="flex items-center gap-2">
+ <StatusIcon status={registration.additionalInfo} />
+ <span>추가 정보 등록</span>
+ </div>
+ <StatusBadge status={registration.additionalInfo} />
+ </div>
+ </div>
+ </div>
+ </div>
+ </DialogContent>
+ </Dialog>
+ );
+}
diff --git a/components/vendor-regular-registrations/registration-request-dialog.tsx b/components/vendor-regular-registrations/registration-request-dialog.tsx
index 99599ce5..d3aeb812 100644
--- a/components/vendor-regular-registrations/registration-request-dialog.tsx
+++ b/components/vendor-regular-registrations/registration-request-dialog.tsx
@@ -313,6 +313,16 @@ export function RegistrationRequestDialog({
return;
}
+ // 업무담당자 검증 (최소 하나의 담당자라도 이름과 이메일이 있어야 함)
+ const hasValidBusinessContact = Object.values(formData.businessContacts).some(contact =>
+ contact.name?.trim() && contact.email?.trim()
+ );
+
+ if (!hasValidBusinessContact) {
+ toast.error("업무담당자 정보를 최소 하나 이상 입력해주세요. (담당자명과 이메일 필수)");
+ return;
+ }
+
if (onSubmit) {
await onSubmit(formData);
}
@@ -599,7 +609,8 @@ export function RegistrationRequestDialog({
{/* 업무담당자 */}
<div>
- <h4 className="font-semibold mb-3">업무담당자</h4>
+ <h4 className="font-semibold mb-3">업무담당자 <span className="text-red-500">*</span></h4>
+ <p className="text-sm text-muted-foreground mb-4">최소 하나의 업무담당자 정보를 입력해주세요.</p>
<div className="space-y-4">
{Object.entries(formData.businessContacts).map(([type, contact]) => {
const labels = {
@@ -615,7 +626,7 @@ export function RegistrationRequestDialog({
<h5 className="font-medium text-sm">{labels[type as keyof typeof labels]} 담당자</h5>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<div>
- <Label>담당자명</Label>
+ <Label>담당자명 <span className="text-red-500">*</span></Label>
<Input
value={contact.name}
onChange={(e) => handleBusinessContactChange(type as keyof typeof formData.businessContacts, 'name', e.target.value)}
@@ -646,7 +657,7 @@ export function RegistrationRequestDialog({
/>
</div>
<div>
- <Label>이메일</Label>
+ <Label>이메일 <span className="text-red-500">*</span></Label>
<Input
type="email"
value={contact.email}
diff --git a/db/schema/bidding.ts b/db/schema/bidding.ts
index 2f0dd07f..1d1fe50a 100644
--- a/db/schema/bidding.ts
+++ b/db/schema/bidding.ts
@@ -42,6 +42,7 @@ export const biddingStatusEnum = pgEnum('bidding_status', [
'bidding_opened', // 입찰공고
'bidding_closed', // 입찰마감
'evaluation_of_bidding', // 입찰평가중
+ 'approval_pending', // 결재 진행중
'bidding_disposal', // 유찰
'vendor_selected', // 업체선정
'bid_opening', // 개찰
diff --git a/lib/rfq-last/shared/rfq-items-dialog.tsx b/lib/rfq-last/shared/rfq-items-dialog.tsx
index f3095c98..e4f71e79 100644
--- a/lib/rfq-last/shared/rfq-items-dialog.tsx
+++ b/lib/rfq-last/shared/rfq-items-dialog.tsx
@@ -328,11 +328,11 @@ export function RfqItemsDialog({
<TableCell>
<div className="flex flex-col items-center gap-1">
<span className="text-xs font-mono">#{index + 1}</span>
- {item.majorYn && (
+ {/* {item.majorYn && (
<Badge variant="default" className="text-xs px-1 py-0">
주요
</Badge>
- )}
+ )} */}
</div>
</TableCell>
<TableCell>
diff --git a/lib/rfq-last/vendor-response/editor/vendor-response-editor.tsx b/lib/rfq-last/vendor-response/editor/vendor-response-editor.tsx
index abd2b516..8c70b8dd 100644
--- a/lib/rfq-last/vendor-response/editor/vendor-response-editor.tsx
+++ b/lib/rfq-last/vendor-response/editor/vendor-response-editor.tsx
@@ -322,7 +322,7 @@ export default function VendorResponseEditor({
if (errors.quotationItems) {
toast.error("견적 품목 정보를 확인해주세요. 모든 품목의 단가와 총액을 입력해야 합니다.")
} else {
- toast.error("입력 정보를 확인해주세요.")
+ toast.error("기본계약 또는 상업조건 정보를 확인해주세요.")
}
}
}
diff --git a/lib/rfq-last/vendor/rfq-vendor-table.tsx b/lib/rfq-last/vendor/rfq-vendor-table.tsx
index 2ee2cb73..577ae492 100644
--- a/lib/rfq-last/vendor/rfq-vendor-table.tsx
+++ b/lib/rfq-last/vendor/rfq-vendor-table.tsx
@@ -1539,10 +1539,10 @@ export function RfqVendorTable({
)}
{/* 기본계약 수정 메뉴 추가 */}
- <DropdownMenuItem onClick={() => handleAction("edit-contract", vendor)}>
+ {/* <DropdownMenuItem onClick={() => handleAction("edit-contract", vendor)}>
<FileText className="mr-2 h-4 w-4" />
기본계약 수정
- </DropdownMenuItem>
+ </DropdownMenuItem> */}
{emailSentAt && (
<>
@@ -1824,9 +1824,6 @@ export function RfqVendorTable({
<Plus className="h-4 w-4 mr-2" />
벤더 추가
</Button>
-
- {selectedRows.length > 0 && (
- <>
{/* 정보 일괄 입력 버튼 - 취소되지 않은 벤더만 */}
<Button
variant="outline"
@@ -1837,7 +1834,8 @@ export function RfqVendorTable({
<Settings2 className="h-4 w-4 mr-2" />
협력업체 조건 설정 ({nonCancelledRows.length})
</Button>
-
+ {selectedRows.length > 0 && (
+ <>
{/* RFQ 발송 버튼 - 취소되지 않은 벤더만 */}
<Button
variant="outline"
diff --git a/lib/vendor-regular-registrations/repository.ts b/lib/vendor-regular-registrations/repository.ts
index 314afb6c..3713f628 100644
--- a/lib/vendor-regular-registrations/repository.ts
+++ b/lib/vendor-regular-registrations/repository.ts
@@ -1,307 +1,307 @@
-import db from "@/db/db";
-import {
- vendorRegularRegistrations,
- vendors,
- vendorAttachments,
- vendorInvestigationAttachments,
- basicContract,
- basicContractTemplates,
- vendorPQSubmissions,
- vendorInvestigations,
- vendorBusinessContacts,
- vendorAdditionalInfo,
-} from "@/db/schema";
-import { eq, desc, and, sql, inArray } from "drizzle-orm";
-import type { VendorRegularRegistration } from "@/config/vendorRegularRegistrationsColumnsConfig";
-
-export async function getVendorRegularRegistrations(
-): Promise<VendorRegularRegistration[]> {
- try {
- // DB 레코드 기준으로 정규업체등록 데이터를 가져옴
- const registrations = await db
- .select({
- // 정규업체등록 정보
- id: vendorRegularRegistrations.id,
- vendorId: vendorRegularRegistrations.vendorId,
- status: vendorRegularRegistrations.status,
- potentialCode: vendorRegularRegistrations.potentialCode,
- majorItems: vendorRegularRegistrations.majorItems,
- registrationRequestDate: vendorRegularRegistrations.registrationRequestDate,
- assignedDepartment: vendorRegularRegistrations.assignedDepartment,
- assignedUser: vendorRegularRegistrations.assignedUser,
- remarks: vendorRegularRegistrations.remarks,
- // 새로 추가된 필드들
- safetyQualificationContent: vendorRegularRegistrations.safetyQualificationContent,
- gtcSkipped: vendorRegularRegistrations.gtcSkipped,
- // 벤더 기본 정보
- businessNumber: vendors.taxId,
- companyName: vendors.vendorName,
- establishmentDate: vendors.createdAt,
- representative: vendors.representativeName,
- // 국가 정보 추가
- country: vendors.country,
- })
- .from(vendorRegularRegistrations)
- .innerJoin(vendors, eq(vendorRegularRegistrations.vendorId, vendors.id))
- .orderBy(desc(vendorRegularRegistrations.createdAt));
-
- // 벤더 ID 배열 생성
- const vendorIds = registrations.map(r => r.vendorId);
-
- // 벤더 첨부파일 정보 조회 - 벤더별로 그룹화
- const vendorAttachmentsList = vendorIds.length > 0 ? await db
- .select()
- .from(vendorAttachments)
- .where(inArray(vendorAttachments.vendorId, vendorIds)) : [];
-
- // 실사 첨부파일 정보 조회 - 실사 ID를 통해 벤더 ID 매핑
- const investigationAttachmentsList = vendorIds.length > 0 ? await db
- .select({
- vendorId: vendorInvestigations.vendorId,
- attachmentId: vendorInvestigationAttachments.id,
- fileName: vendorInvestigationAttachments.fileName,
- attachmentType: vendorInvestigationAttachments.attachmentType,
- createdAt: vendorInvestigationAttachments.createdAt,
- })
- .from(vendorInvestigationAttachments)
- .innerJoin(vendorInvestigations, eq(vendorInvestigationAttachments.investigationId, vendorInvestigations.id))
- .where(inArray(vendorInvestigations.vendorId, vendorIds)) : [];
-
- // 기본 계약 정보 조회 (템플릿별로 가장 최신 것만)
- const basicContractsList = vendorIds.length > 0 ? await db
- .select({
- vendorId: basicContract.vendorId,
- templateId: basicContract.templateId,
- status: basicContract.status,
- templateName: basicContractTemplates.templateName,
- createdAt: basicContract.createdAt,
- filePath: basicContract.filePath,
- fileName: basicContract.fileName,
- })
- .from(basicContract)
- .leftJoin(basicContractTemplates, eq(basicContract.templateId, basicContractTemplates.id))
- .where(inArray(basicContract.vendorId, vendorIds))
- .orderBy(desc(basicContract.createdAt)) : [];
-
- // 추가정보 입력 상태 조회 (업무담당자 정보)
- const businessContactsList = vendorIds.length > 0 ? await db
- .select({
- vendorId: vendorBusinessContacts.vendorId,
- contactType: vendorBusinessContacts.contactType,
- })
- .from(vendorBusinessContacts)
- .where(inArray(vendorBusinessContacts.vendorId, vendorIds)) : [];
-
- // 추가정보 테이블 조회
- const additionalInfoList = vendorIds.length > 0 ? await db
- .select({
- vendorId: vendorAdditionalInfo.vendorId,
- })
- .from(vendorAdditionalInfo)
- .where(inArray(vendorAdditionalInfo.vendorId, vendorIds)) : [];
-
- // 각 등록 레코드별로 데이터를 매핑하여 결과 반환
- return registrations.map((registration) => {
- // 벤더별 첨부파일 필터링
- const vendorFiles = vendorAttachmentsList.filter(att => att.vendorId === registration.vendorId);
- const investigationFiles = investigationAttachmentsList.filter(att => att.vendorId === registration.vendorId);
- const allVendorContracts = basicContractsList.filter(contract => contract.vendorId === registration.vendorId);
- const vendorContacts = businessContactsList.filter(contact => contact.vendorId === registration.vendorId);
- const vendorAdditionalInfoData = additionalInfoList.filter(info => info.vendorId === registration.vendorId);
-
- // 기술자료 동의서, 비밀유지계약서 제외 필터링
- const filteredContracts = allVendorContracts.filter(contract => {
- const templateName = contract.templateName?.toLowerCase() || '';
- return !templateName.includes('기술자료') && !templateName.includes('비밀유지');
- });
-
- // 템플릿명 기준으로 가장 최신 계약만 유지 (중복 제거)
- const vendorContracts = filteredContracts.reduce((acc, contract) => {
- const existing = acc.find(c => c.templateName === contract.templateName);
- if (!existing || (contract.createdAt && existing.createdAt && contract.createdAt > existing.createdAt)) {
- // 기존에 같은 템플릿명이 없거나, 더 최신인 경우 추가/교체
- return acc.filter(c => c.templateName !== contract.templateName).concat(contract);
- }
- return acc;
- }, [] as typeof filteredContracts);
-
- // 문서 제출 현황 - 국가별 요구사항 적용
- const isForeign = registration.country !== 'KR';
- const documentSubmissionsStatus = {
- businessRegistration: vendorFiles.some(f => f.attachmentType === "BUSINESS_REGISTRATION"),
- creditEvaluation: vendorFiles.some(f => f.attachmentType === "CREDIT_REPORT"),
- bankCopy: isForeign ? vendorFiles.some(f => f.attachmentType === "BANK_ACCOUNT_COPY") : true, // 내자는 통장사본 불필요
- auditResult: investigationFiles.length > 0, // 실사 첨부파일이 하나라도 있으면 true
- };
-
- // 문서별 파일 정보 (다운로드용)
- const documentFiles = {
- businessRegistration: vendorFiles.filter(f => f.attachmentType === "BUSINESS_REGISTRATION"),
- creditEvaluation: vendorFiles.filter(f => f.attachmentType === "CREDIT_REPORT"),
- bankCopy: vendorFiles.filter(f => f.attachmentType === "BANK_ACCOUNT_COPY"),
- auditResult: investigationFiles,
- };
-
- // 계약 동의 현황 - 실제 기본 계약 데이터 기반으로 단순화
- const contractAgreementsStatus = {
- cp: vendorContracts.some(c => c.status === "COMPLETED") ? "completed" : "not_submitted",
- gtc: registration.gtcSkipped ? "completed" : (vendorContracts.some(c => c.templateName?.includes("GTC") && c.status === "COMPLETED") ? "completed" : "not_submitted"),
- standardSubcontract: vendorContracts.some(c => c.templateName?.includes("표준하도급") && c.status === "COMPLETED") ? "completed" : "not_submitted",
- safetyHealth: vendorContracts.some(c => c.templateName?.includes("안전보건") && c.status === "COMPLETED") ? "completed" : "not_submitted",
- ethics: vendorContracts.some(c => c.templateName?.includes("윤리") && c.status === "COMPLETED") ? "completed" : "not_submitted",
- domesticCredit: vendorContracts.some(c => c.templateName?.includes("내국신용장") && c.status === "COMPLETED") ? "completed" : "not_submitted",
- };
-
- // 추가정보 입력 완료 여부 - 5개 필수 업무담당자 타입 + 추가정보 테이블 모두 입력되었는지 확인
- const requiredContactTypes = ["sales", "design", "delivery", "quality", "tax_invoice"];
- const contactsCompleted = requiredContactTypes.every(type =>
- vendorContacts.some(contact => contact.contactType === type)
- );
- const additionalInfoTableCompleted = vendorAdditionalInfoData.length > 0;
- const additionalInfoCompleted = contactsCompleted && additionalInfoTableCompleted;
-
- // 모든 조건 충족 여부 확인
- const allDocumentsSubmitted = Object.values(documentSubmissionsStatus).every(status => status === true);
- // 진행현황과 dialog에서 VENDOR_SIGNED도 완료로 간주하므로, 조건충족 체크도 동일하게 처리
- const allContractsCompleted = vendorContracts.length > 0 && vendorContracts.every(c => c.status === "COMPLETED" || c.status === "VENDOR_SIGNED");
- const safetyQualificationCompleted = !!registration.safetyQualificationContent;
-
- // 모든 조건이 충족되면 status를 "approval_ready"(조건충족)로 자동 변경
- const shouldUpdateStatus = allDocumentsSubmitted && allContractsCompleted && safetyQualificationCompleted && additionalInfoCompleted;
-
- // 현재 상태가 조건충족이 아닌데 모든 조건이 충족되면 상태 업데이트
- // 단, 결재진행중(pending_approval) 또는 등록요청완료(registration_completed), 등록실패(registration_failed) 상태인 경우 무시
- if (shouldUpdateStatus && registration.status !== "approval_ready" && registration.status !== "pending_approval" && registration.status !== "registration_completed" && registration.status !== "registration_failed") {
- // 비동기 업데이트 (백그라운드에서 실행)
- updateVendorRegularRegistration(registration.id, {
- status: "approval_ready"
- }).catch(error => {
- console.error(`상태 자동 업데이트 실패 (벤더 ID: ${registration.vendorId}):`, error);
- });
- }
-
- return {
- id: registration.id,
- vendorId: registration.vendorId,
- status: registration.status || "audit_pass",
- potentialCode: registration.potentialCode,
- businessNumber: registration.businessNumber || "",
- companyName: registration.companyName || "",
- majorItems: registration.majorItems,
- establishmentDate: registration.establishmentDate?.toISOString() || null,
- representative: registration.representative,
- country: registration.country,
- documentSubmissions: documentSubmissionsStatus,
- documentFiles: documentFiles, // 파일 정보 추가
- contractAgreements: contractAgreementsStatus,
- // 새로 추가된 필드들
- safetyQualificationContent: registration.safetyQualificationContent,
- gtcSkipped: registration.gtcSkipped || false,
- additionalInfo: additionalInfoCompleted,
- // 기본계약 정보
- basicContracts: vendorContracts.map((contract: any) => ({
- templateId: contract.templateId,
- templateName: contract.templateName,
- status: contract.status,
- createdAt: contract.createdAt,
- filePath: contract.filePath,
- fileName: contract.fileName,
- })),
- registrationRequestDate: registration.registrationRequestDate || null,
- assignedDepartment: registration.assignedDepartment,
- assignedUser: registration.assignedUser,
- remarks: registration.remarks,
- };
- });
- } catch (error) {
- console.error("Error fetching vendor regular registrations:", error);
- throw new Error("정규업체 등록 목록을 가져오는 중 오류가 발생했습니다.");
- }
-}
-
-export async function createVendorRegularRegistration(data: {
- vendorId: number;
- status?: string;
- potentialCode?: string;
- majorItems?: string;
- assignedDepartment?: string;
- assignedDepartmentCode?: string;
- assignedUser?: string;
- assignedUserCode?: string;
- remarks?: string;
- safetyQualificationContent?: string;
- gtcSkipped?: boolean;
-}) {
- try {
- const [registration] = await db
- .insert(vendorRegularRegistrations)
- .values({
- vendorId: data.vendorId,
- status: data.status || "under_review",
- potentialCode: data.potentialCode,
- majorItems: data.majorItems,
- assignedDepartment: data.assignedDepartment,
- assignedDepartmentCode: data.assignedDepartmentCode,
- assignedUser: data.assignedUser,
- assignedUserCode: data.assignedUserCode,
- remarks: data.remarks,
- safetyQualificationContent: data.safetyQualificationContent,
- gtcSkipped: data.gtcSkipped || false,
- })
- .returning();
-
- return registration;
- } catch (error) {
- console.error("Error creating vendor regular registration:", error);
- throw new Error("정규업체 등록을 생성하는 중 오류가 발생했습니다.");
- }
-}
-
-export async function updateVendorRegularRegistration(
- id: number,
- data: Partial<{
- status: string;
- potentialCode: string;
- majorItems: string;
- registrationRequestDate: string;
- assignedDepartment: string;
- assignedDepartmentCode: string;
- assignedUser: string;
- assignedUserCode: string;
- remarks: string;
- safetyQualificationContent: string;
- gtcSkipped: boolean;
- }>
-) {
- try {
- const [registration] = await db
- .update(vendorRegularRegistrations)
- .set({
- ...data,
- updatedAt: new Date(),
- })
- .where(eq(vendorRegularRegistrations.id, id))
- .returning();
-
- return registration;
- } catch (error) {
- console.error("Error updating vendor regular registration:", error);
- throw new Error("정규업체 등록을 업데이트하는 중 오류가 발생했습니다.");
- }
-}
-
-export async function getVendorRegularRegistrationById(id: number) {
- try {
- const [registration] = await db
- .select()
- .from(vendorRegularRegistrations)
- .where(eq(vendorRegularRegistrations.id, id));
-
- return registration;
- } catch (error) {
- console.error("Error fetching vendor regular registration by id:", error);
- throw new Error("정규업체 등록 정보를 가져오는 중 오류가 발생했습니다.");
- }
-}
-
-
+import db from "@/db/db";
+import {
+ vendorRegularRegistrations,
+ vendors,
+ vendorAttachments,
+ vendorInvestigationAttachments,
+ basicContract,
+ basicContractTemplates,
+ vendorPQSubmissions,
+ vendorInvestigations,
+ vendorBusinessContacts,
+ vendorAdditionalInfo,
+} from "@/db/schema";
+import { eq, desc, and, sql, inArray } from "drizzle-orm";
+import type { VendorRegularRegistration } from "@/config/vendorRegularRegistrationsColumnsConfig";
+
+export async function getVendorRegularRegistrations(
+): Promise<VendorRegularRegistration[]> {
+ try {
+ // DB 레코드 기준으로 정규업체등록 데이터를 가져옴
+ const registrations = await db
+ .select({
+ // 정규업체등록 정보
+ id: vendorRegularRegistrations.id,
+ vendorId: vendorRegularRegistrations.vendorId,
+ status: vendorRegularRegistrations.status,
+ potentialCode: vendorRegularRegistrations.potentialCode,
+ majorItems: vendorRegularRegistrations.majorItems,
+ registrationRequestDate: vendorRegularRegistrations.registrationRequestDate,
+ assignedDepartment: vendorRegularRegistrations.assignedDepartment,
+ assignedUser: vendorRegularRegistrations.assignedUser,
+ remarks: vendorRegularRegistrations.remarks,
+ // 새로 추가된 필드들
+ safetyQualificationContent: vendorRegularRegistrations.safetyQualificationContent,
+ gtcSkipped: vendorRegularRegistrations.gtcSkipped,
+ // 벤더 기본 정보
+ businessNumber: vendors.taxId,
+ companyName: vendors.vendorName,
+ establishmentDate: vendors.createdAt,
+ representative: vendors.representativeName,
+ // 국가 정보 추가
+ country: vendors.country,
+ })
+ .from(vendorRegularRegistrations)
+ .innerJoin(vendors, eq(vendorRegularRegistrations.vendorId, vendors.id))
+ .orderBy(desc(vendorRegularRegistrations.createdAt));
+
+ // 벤더 ID 배열 생성
+ const vendorIds = registrations.map(r => r.vendorId);
+
+ // 벤더 첨부파일 정보 조회 - 벤더별로 그룹화
+ const vendorAttachmentsList = vendorIds.length > 0 ? await db
+ .select()
+ .from(vendorAttachments)
+ .where(inArray(vendorAttachments.vendorId, vendorIds)) : [];
+
+ // 실사 첨부파일 정보 조회 - 실사 ID를 통해 벤더 ID 매핑
+ const investigationAttachmentsList = vendorIds.length > 0 ? await db
+ .select({
+ vendorId: vendorInvestigations.vendorId,
+ attachmentId: vendorInvestigationAttachments.id,
+ fileName: vendorInvestigationAttachments.fileName,
+ attachmentType: vendorInvestigationAttachments.attachmentType,
+ createdAt: vendorInvestigationAttachments.createdAt,
+ })
+ .from(vendorInvestigationAttachments)
+ .innerJoin(vendorInvestigations, eq(vendorInvestigationAttachments.investigationId, vendorInvestigations.id))
+ .where(inArray(vendorInvestigations.vendorId, vendorIds)) : [];
+
+ // 기본 계약 정보 조회 (템플릿별로 가장 최신 것만)
+ const basicContractsList = vendorIds.length > 0 ? await db
+ .select({
+ vendorId: basicContract.vendorId,
+ templateId: basicContract.templateId,
+ status: basicContract.status,
+ templateName: basicContractTemplates.templateName,
+ createdAt: basicContract.createdAt,
+ filePath: basicContract.filePath,
+ fileName: basicContract.fileName,
+ })
+ .from(basicContract)
+ .leftJoin(basicContractTemplates, eq(basicContract.templateId, basicContractTemplates.id))
+ .where(inArray(basicContract.vendorId, vendorIds))
+ .orderBy(desc(basicContract.createdAt)) : [];
+
+ // 추가정보 입력 상태 조회 (업무담당자 정보)
+ const businessContactsList = vendorIds.length > 0 ? await db
+ .select({
+ vendorId: vendorBusinessContacts.vendorId,
+ contactType: vendorBusinessContacts.contactType,
+ })
+ .from(vendorBusinessContacts)
+ .where(inArray(vendorBusinessContacts.vendorId, vendorIds)) : [];
+
+ // 추가정보 테이블 조회
+ const additionalInfoList = vendorIds.length > 0 ? await db
+ .select({
+ vendorId: vendorAdditionalInfo.vendorId,
+ })
+ .from(vendorAdditionalInfo)
+ .where(inArray(vendorAdditionalInfo.vendorId, vendorIds)) : [];
+
+ // 각 등록 레코드별로 데이터를 매핑하여 결과 반환
+ return registrations.map((registration) => {
+ // 벤더별 첨부파일 필터링
+ const vendorFiles = vendorAttachmentsList.filter(att => att.vendorId === registration.vendorId);
+ const investigationFiles = investigationAttachmentsList.filter(att => att.vendorId === registration.vendorId);
+ const allVendorContracts = basicContractsList.filter(contract => contract.vendorId === registration.vendorId);
+ const vendorContacts = businessContactsList.filter(contact => contact.vendorId === registration.vendorId);
+ const vendorAdditionalInfoData = additionalInfoList.filter(info => info.vendorId === registration.vendorId);
+
+ // 기술자료 동의서, 비밀유지계약서 제외 필터링
+ const filteredContracts = allVendorContracts.filter(contract => {
+ const templateName = contract.templateName?.toLowerCase() || '';
+ return !templateName.includes('기술자료') && !templateName.includes('비밀유지');
+ });
+
+ // 템플릿명 기준으로 가장 최신 계약만 유지 (중복 제거)
+ const vendorContracts = filteredContracts.reduce((acc, contract) => {
+ const existing = acc.find(c => c.templateName === contract.templateName);
+ if (!existing || (contract.createdAt && existing.createdAt && contract.createdAt > existing.createdAt)) {
+ // 기존에 같은 템플릿명이 없거나, 더 최신인 경우 추가/교체
+ return acc.filter(c => c.templateName !== contract.templateName).concat(contract);
+ }
+ return acc;
+ }, [] as typeof filteredContracts);
+
+ // 문서 제출 현황 - 국가별 요구사항 적용
+ const isForeign = registration.country !== 'KR';
+ const documentSubmissionsStatus = {
+ businessRegistration: vendorFiles.some(f => f.attachmentType === "BUSINESS_REGISTRATION"),
+ creditEvaluation: vendorFiles.some(f => f.attachmentType === "CREDIT_REPORT"),
+ bankCopy: isForeign ? vendorFiles.some(f => f.attachmentType === "BANK_ACCOUNT_COPY") : true, // 내자는 통장사본 불필요
+ auditResult: investigationFiles.length > 0, // 실사 첨부파일이 하나라도 있으면 true
+ };
+
+ // 문서별 파일 정보 (다운로드용)
+ const documentFiles = {
+ businessRegistration: vendorFiles.filter(f => f.attachmentType === "BUSINESS_REGISTRATION"),
+ creditEvaluation: vendorFiles.filter(f => f.attachmentType === "CREDIT_REPORT"),
+ bankCopy: vendorFiles.filter(f => f.attachmentType === "BANK_ACCOUNT_COPY"),
+ auditResult: investigationFiles,
+ };
+
+ // 계약 동의 현황 - 실제 기본 계약 데이터 기반으로 단순화
+ const contractAgreementsStatus = {
+ cp: vendorContracts.some(c => c.status === "COMPLETED") ? "completed" : "not_submitted",
+ gtc: registration.gtcSkipped ? "completed" : (vendorContracts.some(c => c.templateName?.includes("GTC") && c.status === "COMPLETED") ? "completed" : "not_submitted"),
+ standardSubcontract: vendorContracts.some(c => c.templateName?.includes("표준하도급") && c.status === "COMPLETED") ? "completed" : "not_submitted",
+ safetyHealth: vendorContracts.some(c => c.templateName?.includes("안전보건") && c.status === "COMPLETED") ? "completed" : "not_submitted",
+ ethics: vendorContracts.some(c => c.templateName?.includes("윤리") && c.status === "COMPLETED") ? "completed" : "not_submitted",
+ domesticCredit: vendorContracts.some(c => c.templateName?.includes("내국신용장") && c.status === "COMPLETED") ? "completed" : "not_submitted",
+ };
+
+ // 추가정보 입력 완료 여부 - 5개 필수 업무담당자 타입 + 추가정보 테이블 모두 입력되었는지 확인
+ const requiredContactTypes = ["sales", "design", "delivery", "quality", "tax_invoice"];
+ const contactsCompleted = requiredContactTypes.every(type =>
+ vendorContacts.some(contact => contact.contactType === type)
+ );
+ const additionalInfoTableCompleted = vendorAdditionalInfoData.length > 0;
+ const additionalInfoCompleted = contactsCompleted && additionalInfoTableCompleted;
+
+ // 모든 조건 충족 여부 확인
+ const allDocumentsSubmitted = Object.values(documentSubmissionsStatus).every(status => status === true);
+ // 진행현황과 dialog에서 VENDOR_SIGNED도 완료로 간주하므로, 조건충족 체크도 동일하게 처리
+ const allContractsCompleted = vendorContracts.length > 0 && vendorContracts.every(c => c.status === "COMPLETED" || c.status === "VENDOR_SIGNED");
+ const safetyQualificationCompleted = !!registration.safetyQualificationContent;
+
+ // 모든 조건이 충족되면 status를 "approval_ready"(조건충족)로 자동 변경
+ const shouldUpdateStatus = allDocumentsSubmitted && allContractsCompleted && safetyQualificationCompleted && additionalInfoCompleted;
+
+ // 현재 상태가 조건충족이 아닌데 모든 조건이 충족되면 상태 업데이트
+ // 단, 결재진행중(pending_approval) 또는 등록요청완료(registration_completed), 등록실패(registration_failed) 상태인 경우 무시
+ if (shouldUpdateStatus && registration.status !== "approval_ready" && registration.status !== "pending_approval" && registration.status !== "registration_completed" && registration.status !== "registration_failed") {
+ // 비동기 업데이트 (백그라운드에서 실행)
+ updateVendorRegularRegistration(registration.id, {
+ status: "approval_ready"
+ }).catch(error => {
+ console.error(`상태 자동 업데이트 실패 (벤더 ID: ${registration.vendorId}):`, error);
+ });
+ }
+
+ return {
+ id: registration.id,
+ vendorId: registration.vendorId,
+ status: registration.status || "audit_pass",
+ potentialCode: registration.potentialCode,
+ businessNumber: registration.businessNumber || "",
+ companyName: registration.companyName || "",
+ majorItems: registration.majorItems,
+ establishmentDate: registration.establishmentDate?.toISOString() || null,
+ representative: registration.representative,
+ country: registration.country,
+ documentSubmissions: documentSubmissionsStatus,
+ documentFiles: documentFiles, // 파일 정보 추가
+ contractAgreements: contractAgreementsStatus,
+ // 새로 추가된 필드들
+ safetyQualificationContent: registration.safetyQualificationContent,
+ gtcSkipped: registration.gtcSkipped || false,
+ additionalInfo: additionalInfoCompleted,
+ // 기본계약 정보
+ basicContracts: vendorContracts.map((contract: any) => ({
+ templateId: contract.templateId,
+ templateName: contract.templateName,
+ status: contract.status,
+ createdAt: contract.createdAt,
+ filePath: contract.filePath,
+ fileName: contract.fileName,
+ })),
+ registrationRequestDate: registration.registrationRequestDate || null,
+ assignedDepartment: registration.assignedDepartment,
+ assignedUser: registration.assignedUser,
+ remarks: registration.remarks,
+ };
+ });
+ } catch (error) {
+ console.error("Error fetching vendor regular registrations:", error);
+ throw new Error("정규업체 등록 목록을 가져오는 중 오류가 발생했습니다.");
+ }
+}
+
+export async function createVendorRegularRegistration(data: {
+ vendorId: number;
+ status?: string;
+ potentialCode?: string;
+ majorItems?: string;
+ assignedDepartment?: string;
+ assignedDepartmentCode?: string;
+ assignedUser?: string;
+ assignedUserCode?: string;
+ remarks?: string;
+ safetyQualificationContent?: string;
+ gtcSkipped?: boolean;
+}) {
+ try {
+ const [registration] = await db
+ .insert(vendorRegularRegistrations)
+ .values({
+ vendorId: data.vendorId,
+ status: data.status || "under_review",
+ potentialCode: data.potentialCode,
+ majorItems: data.majorItems,
+ assignedDepartment: data.assignedDepartment,
+ assignedDepartmentCode: data.assignedDepartmentCode,
+ assignedUser: data.assignedUser,
+ assignedUserCode: data.assignedUserCode,
+ remarks: data.remarks,
+ safetyQualificationContent: data.safetyQualificationContent,
+ gtcSkipped: data.gtcSkipped || false,
+ })
+ .returning();
+
+ return registration;
+ } catch (error) {
+ console.error("Error creating vendor regular registration:", error);
+ throw new Error("정규업체 등록을 생성하는 중 오류가 발생했습니다.");
+ }
+}
+
+export async function updateVendorRegularRegistration(
+ id: number,
+ data: Partial<{
+ status: string;
+ potentialCode: string;
+ majorItems: string;
+ registrationRequestDate: string;
+ assignedDepartment: string;
+ assignedDepartmentCode: string;
+ assignedUser: string;
+ assignedUserCode: string;
+ remarks: string;
+ safetyQualificationContent: string;
+ gtcSkipped: boolean;
+ }>
+) {
+ try {
+ const [registration] = await db
+ .update(vendorRegularRegistrations)
+ .set({
+ ...data,
+ updatedAt: new Date(),
+ })
+ .where(eq(vendorRegularRegistrations.id, id))
+ .returning();
+
+ return registration;
+ } catch (error) {
+ console.error("Error updating vendor regular registration:", error);
+ throw new Error("정규업체 등록을 업데이트하는 중 오류가 발생했습니다.");
+ }
+}
+
+export async function getVendorRegularRegistrationById(id: number) {
+ try {
+ const [registration] = await db
+ .select()
+ .from(vendorRegularRegistrations)
+ .where(eq(vendorRegularRegistrations.id, id));
+
+ return registration;
+ } catch (error) {
+ console.error("Error fetching vendor regular registration by id:", error);
+ throw new Error("정규업체 등록 정보를 가져오는 중 오류가 발생했습니다.");
+ }
+}
+
+