summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-08-11 09:00:38 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-08-11 09:00:38 +0000
commiteb654f88214095f71be142b989e620fd28db3f69 (patch)
treeaaad3074de4a6422a880b35f9e577d489b0a6c91 /lib
parenta383fd2a30f60360ebc0c1b897b3d43cbae178fa (diff)
(최겸) 기술영업 변경사항 반영, PQ/실사 변경사항 반영
Diffstat (limited to 'lib')
-rw-r--r--lib/mail/templates/site-visit-request.hbs6
-rw-r--r--lib/mail/templates/vendor-approved.hbs225
-rw-r--r--lib/pq/pq-review-table-new/request-investigation-dialog.tsx39
-rw-r--r--lib/pq/pq-review-table-new/site-visit-dialog.tsx21
-rw-r--r--lib/pq/pq-review-table-new/vendors-table-columns.tsx69
-rw-r--r--lib/pq/pq-review-table-new/vendors-table-toolbar-actions.tsx59
-rw-r--r--lib/pq/pq-review-table-new/vendors-table.tsx7
-rw-r--r--lib/site-visit/client-site-visit-wrapper.tsx5
-rw-r--r--lib/site-visit/service.ts30
-rw-r--r--lib/site-visit/shi-attendees-dialog.tsx1
-rw-r--r--lib/site-visit/site-visit-detail-dialog.tsx3
-rw-r--r--lib/tech-project-avl/table/accepted-quotations-table-columns.tsx74
-rw-r--r--lib/tech-project-avl/table/accepted-quotations-table.tsx2
-rw-r--r--lib/techsales-rfq/repository.ts3
-rw-r--r--lib/techsales-rfq/service.ts6
-rw-r--r--lib/techsales-rfq/table/detail-table/rfq-detail-table.tsx10
-rw-r--r--lib/vendor-investigation/service.ts9
-rw-r--r--lib/vendor-investigation/table/investigation-table-columns.tsx13
-rw-r--r--lib/vendor-investigation/table/investigation-table.tsx11
-rw-r--r--lib/vendor-investigation/table/update-investigation-sheet.tsx90
-rw-r--r--lib/vendor-investigation/validations.ts1
-rw-r--r--lib/vendors/validations.ts11
22 files changed, 427 insertions, 268 deletions
diff --git a/lib/mail/templates/site-visit-request.hbs b/lib/mail/templates/site-visit-request.hbs
index 6b2c3a2a..12c05326 100644
--- a/lib/mail/templates/site-visit-request.hbs
+++ b/lib/mail/templates/site-visit-request.hbs
@@ -160,10 +160,10 @@
<div class="section">
<div class="section-title">1. 실사방법</div>
<div class="info-item">
- <span class="info-value">{{evaluationType}}</span>
- {{#if evaluationTypeDescription}}
+ <span class="info-value">{{investigationMethod}}</span>
+ {{#if investigationMethodDescription}}
<div style="font-size: 14px; color: #6b7280; margin-top: 5px;">
- (안내: {{evaluationTypeDescription}})
+ (안내: {{investigationMethodDescription}})
</div>
{{/if}}
</div>
diff --git a/lib/mail/templates/vendor-approved.hbs b/lib/mail/templates/vendor-approved.hbs
new file mode 100644
index 00000000..50f3487a
--- /dev/null
+++ b/lib/mail/templates/vendor-approved.hbs
@@ -0,0 +1,225 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>eVCP 메일</title>
+ <style>
+ body {
+ margin: 0 !important;
+ padding: 20px !important;
+ background-color: #f4f4f4;
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
+ }
+ .email-container {
+ max-width: 600px;
+ margin: 0 auto;
+ background-color: #ffffff;
+ padding: 20px;
+ border-radius: 8px;
+ box-shadow: 0 2px 10px rgba(0,0,0,0.1);
+ }
+ .success-badge {
+ display: inline-block;
+ background-color: #10b981;
+ color: white;
+ padding: 4px 12px;
+ border-radius: 16px;
+ font-size: 12px;
+ font-weight: 600;
+ text-transform: uppercase;
+ margin-bottom: 16px;
+ }
+ .cta-button {
+ display: inline-block;
+ width: 250px;
+ padding: 12px 20px;
+ background-color: #163CC4;
+ color: #ffffff !important;
+ text-decoration: none;
+ border-radius: 8px;
+ text-align: center;
+ line-height: 28px;
+ margin: 8px 0;
+ }
+ .secondary-button {
+ display: inline-block;
+ width: 250px;
+ padding: 12px 20px;
+ background-color: #6b7280;
+ color: #ffffff !important;
+ text-decoration: none;
+ border-radius: 8px;
+ text-align: center;
+ line-height: 28px;
+ margin: 8px 0;
+ }
+ .highlight-box {
+ background-color: #f0f9ff;
+ border-left: 4px solid #163CC4;
+ padding: 16px;
+ margin: 16px 0;
+ border-radius: 0 8px 8px 0;
+ }
+ </style>
+</head>
+<body>
+ <div class="email-container">
+ <!-- Header -->
+ <table width="100%" cellpadding="0" cellspacing="0" style="margin-bottom:24px; border-bottom:1px solid #163CC4; padding-bottom:16px;">
+ <tr>
+ <td align="center">
+ <span style="display: block; text-align: left; color: #163CC4; font-weight: bold; font-size: 32px;">eVCP</span>
+ </td>
+ </tr>
+ </table>
+
+ <!-- Success Badge -->
+ <div class="success-badge">
+ {{#if (eq language 'ko')}}승인 완료{{else}}APPROVED{{/if}}
+ </div>
+
+ <!-- Title -->
+ <h1 style="font-size:28px; margin-bottom:16px; color:#111827;">
+ {{#if (eq language 'ko')}}
+ 업체 승인이 완료되었습니다!
+ {{else}}
+ Your Vendor Application Has Been Approved!
+ {{/if}}
+ </h1>
+
+ <!-- Greeting -->
+ <p style="font-size:16px; line-height:32px; margin-bottom:16px;">
+ {{#if (eq language 'ko')}}
+ 안녕하세요, <strong>{{vendorName}}</strong> 담당자님.
+ {{else}}
+ Hello, <strong>{{vendorName}}</strong> representative.
+ {{/if}}
+ </p>
+
+ <!-- Main Content -->
+ <p style="font-size:16px; line-height:32px; margin-bottom:16px;">
+ {{#if (eq language 'ko')}}
+ 축하합니다! 귀하의 업체 등록 신청이 승인되었으며, 계정이 활성화되었습니다.
+ 이제 eVCP 플랫폼의 모든 서비스를 이용하실 수 있습니다.
+ {{else}}
+ Congratulations! Your vendor registration has been approved and your account has been activated.
+ You can now access all services on the eVCP platform.
+ {{/if}}
+ </p>
+
+ <!-- Highlight Box -->
+ <div class="highlight-box">
+ <h3 style="margin-top:0; margin-bottom:12px; color:#163CC4;">
+ {{#if (eq language 'ko')}}다음 단계{{else}}Next Steps{{/if}}
+ </h3>
+ <ol style="margin:0; padding-left:20px;">
+ <li style="margin-bottom:8px;">
+ {{#if (eq language 'ko')}}
+ 아래 버튼을 클릭하여 패스워드를 설정하세요
+ {{else}}
+ Click the button below to set up your password
+ {{/if}}
+ </li>
+ <li style="margin-bottom:8px;">
+ {{#if (eq language 'ko')}}
+ 패스워드 설정 완료 후 로그인하여 서비스를 시작하세요
+ {{else}}
+ After setting up your password, log in to start using our services
+ {{/if}}
+ </li>
+ </ol>
+ </div>
+
+ <!-- Action Buttons -->
+ <div style="text-align: center; margin: 24px 0;">
+ <!-- Password Setup Button (Primary) -->
+ <a href="{{passwordSetupUrl}}" target="_blank" class="cta-button">
+ {{#if (eq language 'ko')}}
+ 패스워드 설정하기
+ {{else}}
+ Set Up Password
+ {{/if}}
+ </a>
+ <br>
+
+ <!-- Login Button (Secondary) -->
+ <a href="{{loginUrl}}" target="_blank" class="secondary-button">
+ {{#if (eq language 'ko')}}
+ 로그인 페이지로 이동
+ {{else}}
+ Go to Login Page
+ {{/if}}
+ </a>
+ </div>
+
+ <!-- Account Info -->
+ <div style="background-color:#f9fafb; padding:16px; border-radius:8px; margin:16px 0;">
+ <h4 style="margin-top:0; margin-bottom:12px; color:#374151;">
+ {{#if (eq language 'ko')}}계정 정보{{else}}Account Information{{/if}}
+ </h4>
+ <p style="margin:4px 0; font-size:14px; color:#6b7280;">
+ <strong>{{#if (eq language 'ko')}}업체명{{else}}Company{{/if}}:</strong> {{vendorName}}
+ </p>
+ <p style="margin:4px 0; font-size:14px; color:#6b7280;">
+ <strong>{{#if (eq language 'ko')}}이메일{{else}}Email{{/if}}:</strong> {{email}}
+ </p>
+ <p style="margin:4px 0; font-size:14px; color:#6b7280;">
+ <strong>{{#if (eq language 'ko')}}계정 상태{{else}}Account Status{{/if}}:</strong>
+ <span style="color:#10b981; font-weight:600;">
+ {{#if (eq language 'ko')}}활성화됨{{else}}Active{{/if}}
+ </span>
+ </p>
+ </div>
+
+ <!-- Important Notice -->
+ <div style="background-color:#fef3c7; border:1px solid #f59e0b; padding:16px; border-radius:8px; margin:16px 0;">
+ <p style="margin:0; font-size:14px; color:#92400e;">
+ <strong>{{#if (eq language 'ko')}}중요 안내{{else}}Important Notice{{/if}}:</strong>
+ {{#if (eq language 'ko')}}
+ 패스워드 설정 링크는 24시간 동안만 유효합니다.
+ 기간 내에 설정을 완료해주세요.
+ {{else}}
+ The password setup link is valid for 24 hours only.
+ Please complete the setup within this time frame.
+ {{/if}}
+ </p>
+ </div>
+
+ <!-- Support Message -->
+ <p style="font-size:16px; line-height:24px; margin-top:24px; color:#6b7280;">
+ {{#if (eq language 'ko')}}
+ 궁금한 사항이 있으시면 언제든지
+ <a href="mailto:{{supportEmail}}" style="color:#163CC4;">{{supportEmail}}</a>로
+ 연락해주세요.
+ {{else}}
+ If you have any questions, please feel free to contact us at
+ <a href="mailto:{{supportEmail}}" style="color:#163CC4;">{{supportEmail}}</a>.
+ {{/if}}
+ </p>
+
+ <!-- Footer -->
+ <table width="100%" cellpadding="0" cellspacing="0" style="margin-top:32px; border-top:1px solid #e5e7eb; padding-top:16px;">
+ <tr>
+ <td align="center">
+ <p style="font-size:14px; color:#6b7280; margin:4px 0;">
+ © {{currentYear}} EVCP.
+ {{#if (eq language 'ko')}}
+ 모든 권리 보유.
+ {{else}}
+ All rights reserved.
+ {{/if}}
+ </p>
+ <p style="font-size:14px; color:#6b7280; margin:4px 0;">
+ {{#if (eq language 'ko')}}
+ 본 이메일은 발신 전용입니다. 회신하지 마세요.
+ {{else}}
+ This is an automated email. Please do not reply.
+ {{/if}}
+ </p>
+ </td>
+ </tr>
+ </table>
+ </div>
+</body>
+</html> \ No newline at end of file
diff --git a/lib/pq/pq-review-table-new/request-investigation-dialog.tsx b/lib/pq/pq-review-table-new/request-investigation-dialog.tsx
index 6cbb885f..b9648e74 100644
--- a/lib/pq/pq-review-table-new/request-investigation-dialog.tsx
+++ b/lib/pq/pq-review-table-new/request-investigation-dialog.tsx
@@ -51,14 +51,6 @@ interface QMUser {
}
const requestInvestigationFormSchema = z.object({
- evaluationType: z.enum([
- "PURCHASE_SELF_EVAL", // 구매자체평가
- "DOCUMENT_EVAL", // 서류평가
- // "PRODUCT_INSPECTION", // 제품검사평가
- // "SITE_VISIT_EVAL" // 방문실사평가
- ], {
- required_error: "평가 유형을 선택해주세요.",
- }),
qmManagerId: z.number({
required_error: "QM 담당자를 선택해주세요.",
}),
@@ -76,7 +68,6 @@ interface RequestInvestigationDialogProps {
isOpen: boolean
onClose: () => void
onSubmit: (data: {
- evaluationType: "PURCHASE_SELF_EVAL" | "DOCUMENT_EVAL" | "PRODUCT_INSPECTION" | "SITE_VISIT_EVAL",
qmManagerId: number,
forecastedAt: Date,
investigationAddress: string,
@@ -86,7 +77,6 @@ interface RequestInvestigationDialogProps {
selectedCount: number
// 선택된 행에서 가져온 초기값
initialData?: {
- evaluationType?: "PURCHASE_SELF_EVAL" | "DOCUMENT_EVAL" | "PRODUCT_INSPECTION" | "SITE_VISIT_EVAL",
qmManagerId?: number,
forecastedAt?: Date,
investigationAddress?: string,
@@ -110,7 +100,6 @@ export function RequestInvestigationDialog({
const form = useForm<RequestInvestigationFormValues>({
resolver: zodResolver(requestInvestigationFormSchema),
defaultValues: {
- evaluationType: initialData?.evaluationType || "PURCHASE_SELF_EVAL",
qmManagerId: initialData?.qmManagerId || undefined,
forecastedAt: initialData?.forecastedAt || undefined,
investigationAddress: initialData?.investigationAddress || "",
@@ -123,7 +112,6 @@ export function RequestInvestigationDialog({
React.useEffect(() => {
if (isOpen) {
form.reset({
- evaluationType: initialData?.evaluationType || "PURCHASE_SELF_EVAL",
qmManagerId: initialData?.qmManagerId || undefined,
forecastedAt: initialData?.forecastedAt || undefined,
investigationAddress: initialData?.investigationAddress || "",
@@ -175,33 +163,6 @@ export function RequestInvestigationDialog({
</DialogHeader>
<Form {...form}>
<form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-4">
- <FormField
- control={form.control}
- name="evaluationType"
- render={({ field }) => (
- <FormItem>
- <FormLabel>평가 유형</FormLabel>
- <Select
- onValueChange={field.onChange}
- defaultValue={field.value}
- disabled={isPending}
- >
- <FormControl>
- <SelectTrigger>
- <SelectValue placeholder="평가 유형을 선택하세요" />
- </SelectTrigger>
- </FormControl>
- <SelectContent>
- <SelectItem value="PURCHASE_SELF_EVAL">구매자체평가</SelectItem>
- <SelectItem value="DOCUMENT_EVAL">서류평가</SelectItem>
- {/* <SelectItem value="PRODUCT_INSPECTION">제품검사평가</SelectItem> */}
- {/* <SelectItem value="SITE_VISIT_EVAL">방문실사평가</SelectItem> */}
- </SelectContent>
- </Select>
- <FormMessage />
- </FormItem>
- )}
- />
<FormField
control={form.control}
diff --git a/lib/pq/pq-review-table-new/site-visit-dialog.tsx b/lib/pq/pq-review-table-new/site-visit-dialog.tsx
index 63390cb1..b6bd3624 100644
--- a/lib/pq/pq-review-table-new/site-visit-dialog.tsx
+++ b/lib/pq/pq-review-table-new/site-visit-dialog.tsx
@@ -131,8 +131,7 @@ interface SiteVisitDialogProps {
onSubmit: (data: SiteVisitRequestFormValues, attachments?: File[]) => Promise<void>
investigation: {
id: number
- evaluationType: "PRODUCT_INSPECTION" | "SITE_VISIT_EVAL"
- investigationMethod?: string
+ investigationMethod?: "PURCHASE_SELF_EVAL" | "DOCUMENT_EVAL" | "PRODUCT_INSPECTION" | "SITE_VISIT_EVAL"
investigationAddress?: string
vendorName: string
vendorCode: string
@@ -275,17 +274,6 @@ export function SiteVisitDialog({
setSelectedFiles(prev => prev.filter((_, i) => i !== index))
}
- const getEvaluationTypeLabel = (type: string) => {
- switch (type) {
- case "PRODUCT_INSPECTION":
- return "제품검사평가"
- case "SITE_VISIT_EVAL":
- return "방문실사평가"
- default:
- return type
- }
- }
-
const getInvestigationMethodLabel = (method: string) => {
switch (method) {
case "PURCHASE_SELF_EVAL":
@@ -338,13 +326,8 @@ export function SiteVisitDialog({
<FormLabel className="text-sm font-medium">실사방법</FormLabel>
<div className="mt-1 p-3 bg-muted rounded-md">
<Badge variant="outline">
- {getEvaluationTypeLabel(investigation.evaluationType)}
+ {getInvestigationMethodLabel(investigation.investigationMethod || "")}
</Badge>
- {investigation.investigationMethod && (
- <div className="mt-2 text-sm text-muted-foreground">
- {getInvestigationMethodLabel(investigation.investigationMethod)}
- </div>
- )}
</div>
</div>
diff --git a/lib/pq/pq-review-table-new/vendors-table-columns.tsx b/lib/pq/pq-review-table-new/vendors-table-columns.tsx
index d99f201e..d3fada0d 100644
--- a/lib/pq/pq-review-table-new/vendors-table-columns.tsx
+++ b/lib/pq/pq-review-table-new/vendors-table-columns.tsx
@@ -65,7 +65,6 @@ export interface PQSubmission {
id: number
investigationStatus: string
requesterName: string | null // 실사 요청자 이름
- evaluationType: "PURCHASE_SELF_EVAL" | "DOCUMENT_EVAL" | "PRODUCT_INSPECTION" | "SITE_VISIT_EVAL" | null
qmManagerId: number | null
qmManagerName: string | null // QM 담당자 이름
qmManagerEmail: string | null // QM 담당자 이메일
@@ -301,37 +300,38 @@ export function getColumns({ setRowAction, router }: GetColumnsProps): ColumnDef
}
}
- const evaluationTypeColumn: ColumnDef<PQSubmission> = {
- accessorKey: "evaluationType",
- header: ({ column }) => (
- <DataTableColumnHeaderSimple column={column} title="평가 유형" />
- ),
- cell: ({ row }) => {
- const investigation = row.original.investigation;
-
- if (!investigation || !investigation.evaluationType) {
- return <span className="text-muted-foreground">-</span>;
- }
-
- switch (investigation.evaluationType) {
- case "PURCHASE_SELF_EVAL":
- return <Badge variant="outline">구매자체평가</Badge>;
- case "DOCUMENT_EVAL":
- return <Badge variant="secondary">서류평가</Badge>;
- case "PRODUCT_INSPECTION":
- return <Badge variant="default">제품검사평가</Badge>;
- case "SITE_VISIT_EVAL":
- return <Badge variant="destructive">방문실사평가</Badge>;
- default:
- return <span>{investigation.evaluationType}</span>;
- }
- },
- filterFn: (row, id, value) => {
- const investigation = row.original.investigation;
- if (!investigation || !investigation.evaluationType) return value.includes("null");
- return value.includes(investigation.evaluationType);
- },
- };
+ // 평가유형 컬럼 (QM실사방법으로 교체됨)
+ // const evaluationTypeColumn: ColumnDef<PQSubmission> = {
+ // accessorKey: "evaluationType",
+ // header: ({ column }) => (
+ // <DataTableColumnHeaderSimple column={column} title="평가 유형" />
+ // ),
+ // cell: ({ row }) => {
+ // const investigation = row.original.investigation;
+
+ // if (!investigation || !investigation.evaluationType) {
+ // return <span className="text-muted-foreground">-</span>;
+ // }
+
+ // switch (investigation.evaluationType) {
+ // case "PURCHASE_SELF_EVAL":
+ // return <Badge variant="outline">구매자체평가</Badge>;
+ // case "DOCUMENT_EVAL":
+ // return <Badge variant="secondary">서류평가</Badge>;
+ // case "PRODUCT_INSPECTION":
+ // return <Badge variant="default">제품검사평가</Badge>;
+ // case "SITE_VISIT_EVAL":
+ // return <Badge variant="destructive">방문실사평가</Badge>;
+ // default:
+ // return <span>{investigation.evaluationType}</span>;
+ // }
+ // },
+ // filterFn: (row, id, value) => {
+ // const investigation = row.original.investigation;
+ // if (!investigation || !investigation.evaluationType) return value.includes("null");
+ // return value.includes(investigation.evaluationType);
+ // },
+ // };
const evaluationResultColumn: ColumnDef<PQSubmission> = {
@@ -387,7 +387,7 @@ export function getColumns({ setRowAction, router }: GetColumnsProps): ColumnDef
cell: ({ row }) => {
const investigation = row.original.investigation;
- if (!investigation || !investigation.evaluationType) {
+ if (!investigation || !investigation.investigationAddress) {
return <span className="text-muted-foreground">-</span>;
}
@@ -675,7 +675,7 @@ export function getColumns({ setRowAction, router }: GetColumnsProps): ColumnDef
e.preventDefault();
// 실사 정보 수정 다이얼로그 열기 로직
setRowAction({
- type: "edit-investigation",
+ type: "update",
row: row.original
});
}}
@@ -772,7 +772,6 @@ const qmManagerColumn: ColumnDef<PQSubmission> = {
submittedAtColumn,
approvalDateColumn,
answerCountColumn,
- evaluationTypeColumn, // 평가 유형 컬럼
investigationMethodColumn,
investigationForecastedAtColumn,
investigationRequestedAtColumn,
diff --git a/lib/pq/pq-review-table-new/vendors-table-toolbar-actions.tsx b/lib/pq/pq-review-table-new/vendors-table-toolbar-actions.tsx
index 48aeb552..f731a922 100644
--- a/lib/pq/pq-review-table-new/vendors-table-toolbar-actions.tsx
+++ b/lib/pq/pq-review-table-new/vendors-table-toolbar-actions.tsx
@@ -23,7 +23,7 @@ interface VendorsTableToolbarActionsProps {
}
interface InvestigationInitialData {
- evaluationType?: "PURCHASE_SELF_EVAL" | "DOCUMENT_EVAL" | "PRODUCT_INSPECTION" | "SITE_VISIT_EVAL";
+ investigationMethod?: "PURCHASE_SELF_EVAL" | "DOCUMENT_EVAL" | "PRODUCT_INSPECTION" | "SITE_VISIT_EVAL";
qmManagerId?: number;
forecastedAt?: Date;
createdAt?: Date;
@@ -91,7 +91,7 @@ const handleOpenRequestDialog = async () => {
})[0].investigation;
if (latestInvestigation) {
- initialData.evaluationType = latestInvestigation.evaluationType || "SITE_VISIT_EVAL";
+ initialData.investigationMethod = latestInvestigation.investigationMethod || undefined;
initialData.qmManagerId = latestInvestigation.qmManagerId || undefined;
initialData.investigationAddress = defaultAddress; // Factory Location 사용
@@ -102,7 +102,7 @@ const handleOpenRequestDialog = async () => {
}
} else {
// 기본값 설정
- initialData.evaluationType = "SITE_VISIT_EVAL";
+ initialData.investigationMethod = undefined;
const futureDate = new Date();
futureDate.setDate(futureDate.getDate() + 14); // 기본값으로 2주 후
initialData.forecastedAt = futureDate;
@@ -110,14 +110,14 @@ const handleOpenRequestDialog = async () => {
}
}
// 실사가 이미 있고 수정하는 경우
- else if (row.investigation) {
- initialData.evaluationType = row.investigation.evaluationType || "SITE_VISIT_EVAL";
- initialData.qmManagerId = row.investigation.qmManagerId !== null ?
- row.investigation.qmManagerId : undefined;
- initialData.forecastedAt = row.investigation.forecastedAt || new Date();
- initialData.investigationAddress = row.investigation.investigationAddress || "";
- initialData.investigationNotes = row.investigation.investigationNotes || "";
- }
+ // else if (row.investigation) {
+ // initialData.investigationMethod = row.investigation.investigationMethod || undefined;
+ // initialData.qmManagerId = row.investigation.qmManagerId !== null ?
+ // row.investigation.qmManagerId : undefined;
+ // initialData.forecastedAt = row.investigation.forecastedAt || new Date();
+ // initialData.investigationAddress = row.investigation.investigationAddress || "";
+ // initialData.investigationNotes = row.investigation.investigationNotes || "";
+ // }
}
} catch (error) {
console.error("초기 데이터 로드 중 오류:", error);
@@ -132,22 +132,26 @@ const handleOpenRequestDialog = async () => {
};
// 실사 의뢰 요청 처리
const handleRequestInvestigation = async (formData: {
- evaluationType: "PURCHASE_SELF_EVAL" | "DOCUMENT_EVAL" | "PRODUCT_INSPECTION" | "SITE_VISIT_EVAL",
qmManagerId: number,
forecastedAt: Date,
investigationAddress: string,
- investigationMethod?: string,
investigationNotes?: string
}) => {
setIsLoading(true)
try {
- // 승인된 PQ 제출만 필터링
+ // 승인된 PQ 제출만 필터링 (미실사 PQ 제외)
const approvedPQs = selectedRows.filter(row =>
- row.original.status === "APPROVED" && !row.original.investigation
+ row.original.status === "APPROVED" &&
+ !row.original.investigation &&
+ row.original.type !== "NON_INSPECTION"
)
if (approvedPQs.length === 0) {
- toast.error("실사를 의뢰할 수 있는 업체가 없습니다. 승인된 PQ 제출만 실사 의뢰가 가능합니다.")
+ if (hasNonInspectionPQ) {
+ toast.error("미실사 PQ는 실사 의뢰할 수 없습니다. 미실사 PQ를 제외하고 선택해주세요.")
+ } else {
+ toast.error("실사를 의뢰할 수 있는 업체가 없습니다. 승인된 PQ 제출만 실사 의뢰가 가능합니다.")
+ }
return
}
@@ -158,13 +162,8 @@ const handleOpenRequestDialog = async () => {
)
if (result.success) {
- const evaluationTypeLabels = {
- "PURCHASE_SELF_EVAL": "구매자체평가",
- "DOCUMENT_EVAL": "서류평가",
- "PRODUCT_INSPECTION": "제품검사평가",
- "SITE_VISIT_EVAL": "방문실사평가"
- };
- toast.success(`${result.count}개 업체에 대한 ${evaluationTypeLabels[formData.evaluationType]}가 의뢰되었습니다.`)
+
+ toast.success(`${result.count}개 업체에 대해 실사가 의뢰되었습니다.`)
window.location.reload()
} else {
toast.error(result.error || "실사 의뢰 처리 중 오류가 발생했습니다.")
@@ -258,9 +257,11 @@ const handleOpenRequestDialog = async () => {
}
}
- // 승인된 업체 수 확인
+ // 승인된 업체 수 확인 (미실사 PQ 제외)
const approvedPQsCount = selectedRows.filter(row =>
- row.original.status === "APPROVED" && !row.original.investigation
+ row.original.status === "APPROVED" &&
+ !row.original.investigation &&
+ row.original.type !== "NON_INSPECTION"
).length
// 계획 상태 실사 수 확인
@@ -276,6 +277,11 @@ const handleOpenRequestDialog = async () => {
row.original.investigation.evaluationResult === "APPROVED"
).length
+ // 미실사 PQ가 선택되었는지 확인
+ const hasNonInspectionPQ = selectedRows.some(row =>
+ row.original.type === "NON_INSPECTION"
+ )
+
// 실사 방법 라벨 변환 함수
const getInvestigationMethodLabel = (method: string): string => {
switch (method) {
@@ -328,8 +334,9 @@ const handleOpenRequestDialog = async () => {
variant="outline"
size="sm"
onClick={handleOpenRequestDialog} // 여기를 수정: 새로운 핸들러 함수 사용
- disabled={isLoading || selectedRows.length === 0}
+ disabled={isLoading || selectedRows.length === 0 || hasNonInspectionPQ}
className="gap-2"
+ title={hasNonInspectionPQ ? "미실사 PQ는 실사 의뢰할 수 없습니다." : undefined}
>
<ClipboardCheck className="size-4" aria-hidden="true" />
<span className="hidden sm:inline">실사 의뢰</span>
diff --git a/lib/pq/pq-review-table-new/vendors-table.tsx b/lib/pq/pq-review-table-new/vendors-table.tsx
index c2712611..191c8bfa 100644
--- a/lib/pq/pq-review-table-new/vendors-table.tsx
+++ b/lib/pq/pq-review-table-new/vendors-table.tsx
@@ -192,7 +192,7 @@ export function PQSubmissionsTable({ promises, className }: PQSubmissionsTablePr
setSelectedSiteVisitRequestId(rowAction.row.siteVisitRequestId || null)
setIsVendorInfoViewDialogOpen(true)
setRowAction(null)
- } else if (rowAction?.type === "edit-investigation") {
+ } else if (rowAction?.type === "update") {
// 실사 정보 수정 다이얼로그 열기
setSelectedInvestigationForEdit(rowAction.row)
setIsEditInvestigationDialogOpen(true)
@@ -432,9 +432,8 @@ export function PQSubmissionsTable({ promises, className }: PQSubmissionsTablePr
onSubmit={handleSiteVisitRequest}
investigation={{
id: selectedInvestigation.investigation?.id || 0,
- evaluationType: selectedInvestigation.investigation?.evaluationType as "PRODUCT_INSPECTION" | "SITE_VISIT_EVAL",
- investigationMethod: selectedInvestigation.investigation?.investigationMethod,
- investigationAddress: selectedInvestigation.investigation?.investigationAddress,
+ investigationMethod: selectedInvestigation.investigation?.investigationMethod || "",
+ investigationAddress: selectedInvestigation.investigation?.investigationAddress || "",
vendorName: selectedInvestigation.vendorName,
vendorCode: selectedInvestigation.vendorCode,
projectName: selectedInvestigation.projectName || undefined,
diff --git a/lib/site-visit/client-site-visit-wrapper.tsx b/lib/site-visit/client-site-visit-wrapper.tsx
index 4f056b3a..ad8da632 100644
--- a/lib/site-visit/client-site-visit-wrapper.tsx
+++ b/lib/site-visit/client-site-visit-wrapper.tsx
@@ -62,7 +62,6 @@ interface SiteVisitRequest {
updatedAt: Date
// 실사 정보
- evaluationType: string | null //구매담당자가 작성한 실사방법
investigationMethod: string | null // QM담당자가 작성한 실사방법
investigationAddress: string | null
investigationNotes: string | null
@@ -202,9 +201,9 @@ export function ClientSiteVisitWrapper({
방문실사 요청 정보를 조회하고 회신할 수 있습니다.
</p>
</div>
- <div className="flex items-center gap-2">
+ {/* <div className="flex items-center gap-2">
<Badge variant="outline">Vendor ID: {vendorId}</Badge>
- </div>
+ </div> */}
</div>
{/* 통계 카드 */}
diff --git a/lib/site-visit/service.ts b/lib/site-visit/service.ts
index 3b9bcb91..b525fabe 100644
--- a/lib/site-visit/service.ts
+++ b/lib/site-visit/service.ts
@@ -155,30 +155,7 @@ export async function createSiteVisitRequestAction(input: {
if (!sender) {
throw new Error('발송자 정보를 찾을 수 없습니다.');
}
-
- // 평가 유형 라벨 및 설명
- const getEvaluationTypeInfo = (type: string) => {
- switch (type) {
- case 'PRODUCT_INSPECTION':
- return {
- label: '제품검사평가',
- description: '제품의 품질, 성능, 안전성 등을 직접 검사하는 평가'
- };
- case 'SITE_VISIT_EVAL':
- return {
- label: '방문실사평가',
- description: '공장 시설, 생산능력, 품질관리체계 등을 현장에서 점검하는 평가'
- };
- default:
- return {
- label: type,
- description: ''
- };
- }
- };
-
- const evaluationTypeInfo = getEvaluationTypeInfo(investigation.evaluationType || '');
-
+
// 마감일 계산 (발송일 + 7일)
const deadlineDate = format(new Date(), 'yyyy.MM.dd');
@@ -198,8 +175,8 @@ export async function createSiteVisitRequestAction(input: {
requesterEmail: sender.email,
// 실사 정보
- evaluationType: evaluationTypeInfo.label,
- evaluationTypeDescription: evaluationTypeInfo.description,
+ investigationMethod: investigation.investigationMethod,
+ investigationMethodDescription: investigation.investigationMethodDescription,
requestedStartDate: format(siteVisitRequest.requestedStartDate!, 'yyyy.MM.dd'),
requestedEndDate: format(siteVisitRequest.requestedEndDate!, 'yyyy.MM.dd'),
inspectionDuration: siteVisitRequest.inspectionDuration,
@@ -368,7 +345,6 @@ export async function getSiteVisitRequestAction(investigationId: number) {
updatedAt: siteVisitRequests.updatedAt,
// 실사 정보
- evaluationType: vendorInvestigations.evaluationType,
investigationMethod: vendorInvestigations.investigationMethod,
investigationAddress: vendorInvestigations.investigationAddress,
investigationNotes: vendorInvestigations.investigationNotes,
diff --git a/lib/site-visit/shi-attendees-dialog.tsx b/lib/site-visit/shi-attendees-dialog.tsx
index b90689f4..80681cb5 100644
--- a/lib/site-visit/shi-attendees-dialog.tsx
+++ b/lib/site-visit/shi-attendees-dialog.tsx
@@ -27,7 +27,6 @@ interface SiteVisitRequest {
updatedAt: Date
// 실사 정보
- evaluationType: string | null //구매담당자가 작성한 실사방법
investigationMethod: string | null // QM담당자가 작성한 실사방법
investigationAddress: string | null
investigationNotes: string | null
diff --git a/lib/site-visit/site-visit-detail-dialog.tsx b/lib/site-visit/site-visit-detail-dialog.tsx
index 714ca3e3..3043f358 100644
--- a/lib/site-visit/site-visit-detail-dialog.tsx
+++ b/lib/site-visit/site-visit-detail-dialog.tsx
@@ -32,8 +32,7 @@ interface SiteVisitRequest {
createdAt: Date
updatedAt: Date
- // 실사 정보
- evaluationType: string | null //구매담당자가 작성한 실사방법
+ // 실사정보
investigationMethod: string | null // QM담당자가 작성한 실사방법
investigationAddress: string | null
investigationNotes: string | null
diff --git a/lib/tech-project-avl/table/accepted-quotations-table-columns.tsx b/lib/tech-project-avl/table/accepted-quotations-table-columns.tsx
index e73c2163..5dd1b58f 100644
--- a/lib/tech-project-avl/table/accepted-quotations-table-columns.tsx
+++ b/lib/tech-project-avl/table/accepted-quotations-table-columns.tsx
@@ -21,6 +21,7 @@ export interface RfqItemInfo {
// Accepted Quotation 타입 정의
export interface AcceptedQuotationItem {
id: number
+ uniqueKey: string // 확장된 데이터의 유니크 키
rfqId: number
vendorId: number
quotationVersion: number | null
@@ -162,6 +163,59 @@ export function getColumns(): ColumnDef<AcceptedQuotationItem>[] {
excelHeader: "프로젝트명",
},
},
+ // 선주명
+ {
+ accessorKey: "kunnrNm",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="선주명" />
+ ),
+ cell: ({ row }) => (
+ <div className="max-w-32 truncate">
+ {row.original.kunnrNm || "-"}
+ </div>
+ ),
+ enableSorting: true,
+ enableHiding: true,
+ meta: {
+ excelHeader: "선주명",
+ },
+ },
+ // 선종코드
+ {
+ accessorKey: "ptype",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="선종코드" />
+ ),
+ cell: ({ row }) => (
+ <div className="font-medium">
+ {row.original.ptype || "-"}
+ </div>
+ ),
+ enableSorting: true,
+ enableHiding: true,
+ meta: {
+ excelHeader: "선종코드",
+ },
+ },
+ // 공종
+ {
+ accessorKey: "workType",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="공종" />
+ ),
+ cell: ({ row }) => {
+ return (
+ <div className="max-w-32 truncate">
+ {row.original.workType || "-"}
+ </div>
+ );
+ },
+ enableSorting: false,
+ enableHiding: true,
+ meta: {
+ excelHeader: "공종",
+ },
+ },
// RFQ 코드
{
accessorKey: "rfqCode",
@@ -215,25 +269,7 @@ export function getColumns(): ColumnDef<AcceptedQuotationItem>[] {
excelHeader: "자재그룹",
},
},
- // 공종
- {
- accessorKey: "workType",
- header: ({ column }) => (
- <DataTableColumnHeaderSimple column={column} title="공종" />
- ),
- cell: ({ row }) => {
- return (
- <div className="max-w-32 truncate">
- {row.original.workType || "-"}
- </div>
- );
- },
- enableSorting: false,
- enableHiding: true,
- meta: {
- excelHeader: "공종",
- },
- },
+
// 선종
{
accessorKey: "shipType",
diff --git a/lib/tech-project-avl/table/accepted-quotations-table.tsx b/lib/tech-project-avl/table/accepted-quotations-table.tsx
index da33d0d5..3b0ffdd9 100644
--- a/lib/tech-project-avl/table/accepted-quotations-table.tsx
+++ b/lib/tech-project-avl/table/accepted-quotations-table.tsx
@@ -100,7 +100,7 @@ export function AcceptedQuotationsTable({
sorting: [{ id: "acceptedAt", desc: true }],
columnPinning: { left: ["select"] },
},
- getRowId: (originalRow) => `${originalRow.id}`,
+ getRowId: (originalRow) => originalRow.uniqueKey,
})
return (
diff --git a/lib/techsales-rfq/repository.ts b/lib/techsales-rfq/repository.ts
index e41982b9..e6138651 100644
--- a/lib/techsales-rfq/repository.ts
+++ b/lib/techsales-rfq/repository.ts
@@ -558,7 +558,8 @@ export async function selectSingleTechSalesVendorQuotationWithJoin(
status: techSalesVendorQuotations.status,
remark: techSalesVendorQuotations.remark,
rejectionReason: techSalesVendorQuotations.rejectionReason,
-
+ vendorFlags: techSalesVendorQuotations.vendorFlags, // 벤더 구분자 정보 추가
+
// 날짜 정보
submittedAt: techSalesVendorQuotations.submittedAt,
acceptedAt: techSalesVendorQuotations.acceptedAt,
diff --git a/lib/techsales-rfq/service.ts b/lib/techsales-rfq/service.ts
index 0d00a4e2..5ec02f63 100644
--- a/lib/techsales-rfq/service.ts
+++ b/lib/techsales-rfq/service.ts
@@ -911,6 +911,7 @@ export async function getTechSalesVendorQuotation(quotationId: number) {
status: quotation.status,
remark: quotation.remark,
rejectionReason: quotation.rejectionReason,
+ vendorFlags: quotation.vendorFlags,
submittedAt: quotation.submittedAt,
acceptedAt: quotation.acceptedAt,
createdAt: quotation.createdAt,
@@ -3132,6 +3133,7 @@ export async function getTechSalesRfqTechVendors(rfqId: number) {
validUntil: techSalesVendorQuotations.validUntil,
submittedAt: techSalesVendorQuotations.submittedAt,
createdAt: techSalesVendorQuotations.createdAt,
+ vendorFlags: techSalesVendorQuotations.vendorFlags, // 벤더 구분자 정보 추가
})
.from(techSalesVendorQuotations)
.innerJoin(techVendors, eq(techSalesVendorQuotations.vendorId, techVendors.id))
@@ -3794,9 +3796,10 @@ export async function getAcceptedTechSalesVendorQuotations(input: {
if (rfqItems.length === 0) {
// 아이템이 없는 경우 각 벤더별로 행 생성
- vendorQuotations.forEach((quotation) => {
+ vendorQuotations.forEach((quotation: any, vendorIndex: number) => {
expandedData.push({
...quotation.vendorQuotation,
+ uniqueKey: `${quotation.vendorQuotation.id}-${quotation.vendorQuotation.rfqId}-no-item-${vendorIndex}`, // 유니크 키 생성
rfqItems: [],
itemIndex: 0,
totalItems: 0,
@@ -3814,6 +3817,7 @@ export async function getAcceptedTechSalesVendorQuotations(input: {
vendorQuotations.forEach((quotation: { vendorQuotation: any; rfqItems: RfqItemInfo[] }, vendorIndex: number) => {
expandedData.push({
...quotation.vendorQuotation,
+ uniqueKey: `${quotation.vendorQuotation.id}-${quotation.vendorQuotation.rfqId}-item-${itemIndex}-vendor-${vendorIndex}`, // 유니크 키 생성
rfqItems: [rfqItem], // 단일 아이템만 포함
itemIndex: itemIndex,
totalItems: rfqItems.length,
diff --git a/lib/techsales-rfq/table/detail-table/rfq-detail-table.tsx b/lib/techsales-rfq/table/detail-table/rfq-detail-table.tsx
index 8bfb8299..249a2c74 100644
--- a/lib/techsales-rfq/table/detail-table/rfq-detail-table.tsx
+++ b/lib/techsales-rfq/table/detail-table/rfq-detail-table.tsx
@@ -643,9 +643,11 @@ export function RfqDetailTables({ selectedRfq, maxHeight }: RfqDetailTablesProps
disabled={
selectedRows.length === 0 ||
isSendingRfq ||
- selectedRows.some(row => row.status !== "Assigned")
+ selectedRows.some(row => row.status !== "Assigned") ||
+ selectedRfq?.status === "Closed"
}
className="gap-2"
+ title={selectedRfq?.status === "Closed" ? "마감된 RFQ는 발송할 수 없습니다." : undefined}
>
{isSendingRfq ? (
<Loader2 className="size-4 animate-spin" aria-hidden="true" />
@@ -676,8 +678,9 @@ export function RfqDetailTables({ selectedRfq, maxHeight }: RfqDetailTablesProps
variant="outline"
size="sm"
onClick={handleAddVendor}
- disabled={isAdddialogLoading}
+ disabled={isAdddialogLoading || selectedRfq?.status === "Closed"}
className="gap-2"
+ title={selectedRfq?.status === "Closed" ? "마감된 RFQ는 벤더를 추가할 수 없습니다." : undefined}
>
{isAdddialogLoading ? (
<Loader2 className="size-4 animate-spin" aria-hidden="true" />
@@ -698,8 +701,9 @@ export function RfqDetailTables({ selectedRfq, maxHeight }: RfqDetailTablesProps
variant="outline"
size="sm"
onClick={handleAddVendor}
- disabled={isAdddialogLoading}
+ disabled={isAdddialogLoading || selectedRfq?.status === "Closed"}
className="mt-4 gap-2"
+ title={selectedRfq?.status === "Closed" ? "마감된 RFQ는 벤더를 추가할 수 없습니다." : undefined}
>
{isAdddialogLoading ? (
<Loader2 className="size-4 animate-spin" aria-hidden="true" />
diff --git a/lib/vendor-investigation/service.ts b/lib/vendor-investigation/service.ts
index 7c486fc9..81eabc37 100644
--- a/lib/vendor-investigation/service.ts
+++ b/lib/vendor-investigation/service.ts
@@ -45,7 +45,6 @@ export async function getVendorsInvestigation(input: GetVendorsInvestigationSche
// 실사 정보
ilike(vendorInvestigationsView.investigationNotes, s),
ilike(vendorInvestigationsView.investigationStatus, s),
- ilike(vendorInvestigationsView.evaluationType, s),
ilike(vendorInvestigationsView.investigationAddress, s),
ilike(vendorInvestigationsView.investigationMethod, s),
@@ -215,8 +214,8 @@ export async function updateVendorInvestigationAction(formData: FormData) {
}
// 선택적 enum 필드
- if (textEntries.evaluationType) {
- processedEntries.evaluationType = textEntries.evaluationType
+ if (textEntries.investigationMethod) {
+ processedEntries.investigationMethod = textEntries.investigationMethod
}
// 선택적 문자열 필드
@@ -264,8 +263,8 @@ export async function updateVendorInvestigationAction(formData: FormData) {
}
// 선택적 필드들은 존재할 때만 추가
- if (parsed.evaluationType !== undefined) {
- updateData.evaluationType = parsed.evaluationType
+ if (parsed.investigationMethod !== undefined) {
+ updateData.investigationMethod = parsed.investigationMethod
}
if (parsed.investigationAddress !== undefined) {
updateData.investigationAddress = parsed.investigationAddress
diff --git a/lib/vendor-investigation/table/investigation-table-columns.tsx b/lib/vendor-investigation/table/investigation-table-columns.tsx
index 88b6644f..3d765179 100644
--- a/lib/vendor-investigation/table/investigation-table-columns.tsx
+++ b/lib/vendor-investigation/table/investigation-table-columns.tsx
@@ -168,17 +168,6 @@ export function getColumns({
)
}
- // Handle evaluation type
- if (column.id === "evaluationType") {
- if (!value) return ""
-
- return (
- <span>
- {formatEnumValue(value as string)}
- </span>
- )
- }
-
// Handle evaluation result
if (column.id === "evaluationResult") {
if (!value) return ""
@@ -287,6 +276,8 @@ function formatStatus(status: string): string {
return "완료됨"
case "CANCELED":
return "취소됨"
+ case "RESULT_SENT":
+ return "실사결과발송"
default:
return status
}
diff --git a/lib/vendor-investigation/table/investigation-table.tsx b/lib/vendor-investigation/table/investigation-table.tsx
index 660a8507..fcd2d0be 100644
--- a/lib/vendor-investigation/table/investigation-table.tsx
+++ b/lib/vendor-investigation/table/investigation-table.tsx
@@ -98,17 +98,6 @@ export function VendorsInvestigationTable({ promises }: VendorsTableProps) {
]
},
{
- id: "evaluationType",
- label: "평가 유형",
- type: "select",
- options: [
- { label: "구매자체평가", value: "PURCHASE_SELF_EVAL" },
- { label: "서류평가", value: "DOCUMENT_EVAL" },
- { label: "제품검사평가", value: "PRODUCT_INSPECTION" },
- { label: "방문실사평가", value: "SITE_VISIT_EVAL" },
- ]
- },
- {
id: "evaluationResult",
label: "평가 결과",
type: "select",
diff --git a/lib/vendor-investigation/table/update-investigation-sheet.tsx b/lib/vendor-investigation/table/update-investigation-sheet.tsx
index c04aad64..14350815 100644
--- a/lib/vendor-investigation/table/update-investigation-sheet.tsx
+++ b/lib/vendor-investigation/table/update-investigation-sheet.tsx
@@ -3,7 +3,7 @@
import * as React from "react"
import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form"
-import { CalendarIcon, Loader, X } from "lucide-react"
+import { CalendarIcon, Loader, X, Download } from "lucide-react"
import { format } from "date-fns"
import { toast } from "sonner"
@@ -67,6 +67,7 @@ import {
import { updateVendorInvestigationAction, getInvestigationAttachments, deleteInvestigationAttachment } from "../service"
import { VendorInvestigationsViewWithContacts } from "@/config/vendorInvestigationsColumnsConfig"
import prettyBytes from "pretty-bytes"
+import { downloadFile } from "@/lib/file-download"
interface UpdateVendorInvestigationSheetProps
extends React.ComponentPropsWithoutRef<typeof Sheet> {
@@ -123,7 +124,6 @@ export function UpdateVendorInvestigationSheet({
defaultValues: {
investigationId: investigation?.investigationId ?? 0,
investigationStatus: investigation?.investigationStatus ?? "PLANNED",
- evaluationType: investigation?.evaluationType ?? undefined,
investigationAddress: investigation?.investigationAddress ?? "",
investigationMethod: investigation?.investigationMethod ?? undefined,
forecastedAt: investigation?.forecastedAt ?? undefined,
@@ -143,7 +143,6 @@ export function UpdateVendorInvestigationSheet({
form.reset({
investigationId: investigation.investigationId,
investigationStatus: investigation.investigationStatus || "PLANNED",
- evaluationType: investigation.evaluationType ?? undefined,
investigationAddress: investigation.investigationAddress ?? "",
investigationMethod: investigation.investigationMethod ?? undefined,
forecastedAt: investigation.forecastedAt ?? undefined,
@@ -203,10 +202,28 @@ export function UpdateVendorInvestigationSheet({
}
}
+ // 첨부파일 다운로드 함수
+ const handleDownloadAttachment = async (attachment: any) => {
+ if (!attachment.filePath || !attachment.fileName) {
+ toast.error("첨부파일 정보가 올바르지 않습니다.")
+ return
+ }
+
+ try {
+ await downloadFile(attachment.filePath, attachment.fileName, {
+ showToast: true,
+ action: 'download'
+ })
+ } catch (error) {
+ console.error("첨부파일 다운로드 오류:", error)
+ toast.error("첨부파일 다운로드 중 오류가 발생했습니다.")
+ }
+ }
+
// 선택된 파일에서 특정 파일 제거
const handleRemoveSelectedFile = (indexToRemove: number) => {
const currentFiles = form.getValues("attachments") || []
- const updatedFiles = currentFiles.filter((_, index) => index !== indexToRemove)
+ const updatedFiles = currentFiles.filter((_: File, index: number) => index !== indexToRemove)
form.setValue("attachments", updatedFiles.length > 0 ? updatedFiles : undefined)
if (updatedFiles.length === 0) {
@@ -250,16 +267,30 @@ export function UpdateVendorInvestigationSheet({
({Math.round(attachment.fileSize / 1024)}KB)
</span>
</div>
- <Button
- type="button"
- variant="ghost"
- size="sm"
- onClick={() => handleDeleteAttachment(attachment.id)}
- className="text-destructive hover:text-destructive"
- disabled={isPending}
- >
- 삭제
- </Button>
+ <div className="flex items-center gap-1">
+ <Button
+ type="button"
+ variant="ghost"
+ size="sm"
+ onClick={() => handleDownloadAttachment(attachment)}
+ className="text-blue-600 hover:text-blue-700"
+ disabled={isPending}
+ title="파일 다운로드"
+ >
+ <Download className="h-4 w-4" />
+ </Button>
+ <Button
+ type="button"
+ variant="ghost"
+ size="sm"
+ onClick={() => handleDeleteAttachment(attachment.id)}
+ className="text-destructive hover:text-destructive"
+ disabled={isPending}
+ title="파일 삭제"
+ >
+ <X className="h-4 w-4" />
+ </Button>
+ </div>
</div>
))
) : (
@@ -417,10 +448,6 @@ export function UpdateVendorInvestigationSheet({
formData.append("investigationId", String(values.investigationId))
formData.append("investigationStatus", values.investigationStatus)
- // 선택적 필드들
- if (values.evaluationType) {
- formData.append("evaluationType", values.evaluationType)
- }
if (values.investigationAddress) {
formData.append("investigationAddress", values.investigationAddress)
@@ -559,33 +586,6 @@ export function UpdateVendorInvestigationSheet({
)}
/>
- {/* 평가 유형 */}
- <FormField
- control={form.control}
- name="evaluationType"
- render={({ field }) => (
- <FormItem>
- <FormLabel>평가 유형</FormLabel>
- <FormControl>
- <Select value={field.value || ""} onValueChange={field.onChange}>
- <SelectTrigger>
- <SelectValue placeholder="평가 유형을 선택하세요" />
- </SelectTrigger>
- <SelectContent>
- <SelectGroup>
- <SelectItem value="PURCHASE_SELF_EVAL">구매자체평가</SelectItem>
- <SelectItem value="DOCUMENT_EVAL">서류평가</SelectItem>
- <SelectItem value="PRODUCT_INSPECTION">제품검사평가</SelectItem>
- <SelectItem value="SITE_VISIT_EVAL">방문실사평가</SelectItem>
- </SelectGroup>
- </SelectContent>
- </Select>
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
-
{/* 실사 주소 */}
<FormField
control={form.control}
diff --git a/lib/vendor-investigation/validations.ts b/lib/vendor-investigation/validations.ts
index e4ec2b52..0e84f13a 100644
--- a/lib/vendor-investigation/validations.ts
+++ b/lib/vendor-investigation/validations.ts
@@ -67,7 +67,6 @@ export const updateVendorInvestigationSchema = z.object({
investigationStatus: z.enum(["PLANNED", "IN_PROGRESS", "COMPLETED", "CANCELED", "RESULT_SENT"], {
required_error: "실사 상태를 선택해주세요.",
}),
- evaluationType: z.enum(["PURCHASE_SELF_EVAL", "DOCUMENT_EVAL", "PRODUCT_INSPECTION", "SITE_VISIT_EVAL"]).optional(),
investigationAddress: z.string().optional(),
investigationMethod: z.enum(["PURCHASE_SELF_EVAL", "DOCUMENT_EVAL", "PRODUCT_INSPECTION", "SITE_VISIT_EVAL"]).optional(),
diff --git a/lib/vendors/validations.ts b/lib/vendors/validations.ts
index 681bac62..ecadd67a 100644
--- a/lib/vendors/validations.ts
+++ b/lib/vendors/validations.ts
@@ -10,7 +10,6 @@ import * as z from "zod"
import { getFiltersStateParser, getSortingStateParser } from "@/lib/parsers"
import { Vendor, VendorContact, VendorItemsView, VendorMaterialsView, vendors, VendorWithType } from "@/db/schema/vendors";
import { rfqs } from "@/db/schema/rfq"
-import { countryDialCodes } from "@/components/signup/join-form";
export const searchParamsCache = createSearchParamsCache({
@@ -255,16 +254,6 @@ export const createVendorSchema = z
})
.superRefine((data, ctx) => {
// Validate main phone number with country code
- if (data.phone && data.country) {
- if (!validatePhoneByCountry(data.phone, data.country)) {
- const countryDialCode = countryDialCodes[data.country] || "+XX";
- ctx.addIssue({
- code: "custom",
- path: ["phone"],
- message: `올바른 전화번호 형식이 아닙니다. ${countryDialCode}로 시작하는 국제 전화번호를 입력해주세요. (예: ${countryDialCode}XXXXXXXXX)`,
- });
- }
- }
// Validate representative phone for Korean companies
if (data.country === "KR") {