From b5c174429548a53e5c86a13bdbfc61516e5ee345 Mon Sep 17 00:00:00 2001 From: dujinkim Date: Tue, 9 Dec 2025 03:04:05 +0000 Subject: (최겸) 구매 구매자서명 내 삼성중공업 정보 입력 추가 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../(evcp)/(master-data)/buyer-signature/page.tsx | 3 +- config/vendorInvestigationsColumnsConfig.ts | 1 + db/schema/basicContractDocumnet.ts | 2 + db/schema/pq.ts | 1 + .../vendor/partners-bidding-list-columns.tsx | 9 +- lib/general-contracts/service.ts | 23 +- lib/general-contracts/utils.ts | 10 +- lib/pq/pq-criteria/add-pq-dialog.tsx | 968 ++++++++++----------- lib/pq/pq-criteria/update-pq-sheet.tsx | 952 ++++++++++---------- .../request-investigation-dialog.tsx | 48 +- .../pq-review-table-new/vendors-table-columns.tsx | 1 + .../vendors-table-toolbar-actions.tsx | 3 + lib/pq/service.ts | 1 + lib/shi-signature/buyer-signature.ts | 15 + lib/shi-signature/signature-list.tsx | 10 +- lib/shi-signature/upload-form.tsx | 40 +- .../table/investigation-progress-sheet.tsx | 47 +- 17 files changed, 1159 insertions(+), 975 deletions(-) diff --git a/app/[lng]/evcp/(evcp)/(master-data)/buyer-signature/page.tsx b/app/[lng]/evcp/(evcp)/(master-data)/buyer-signature/page.tsx index dfbd605b..5826b8d8 100644 --- a/app/[lng]/evcp/(evcp)/(master-data)/buyer-signature/page.tsx +++ b/app/[lng]/evcp/(evcp)/(master-data)/buyer-signature/page.tsx @@ -8,6 +8,7 @@ export default async function BuyerSignaturePage(props: { params: Promise<{ lng: const { t } = await useTranslation(lng, 'menu') const signatures = await getAllSignatures(); + const activeSignature = signatures.find((signature) => signature.isActive); return (
@@ -19,7 +20,7 @@ export default async function BuyerSignaturePage(props: { params: Promise<{ lng:

- + diff --git a/config/vendorInvestigationsColumnsConfig.ts b/config/vendorInvestigationsColumnsConfig.ts index 7d74d8e0..0029b5a5 100644 --- a/config/vendorInvestigationsColumnsConfig.ts +++ b/config/vendorInvestigationsColumnsConfig.ts @@ -5,6 +5,7 @@ export type VendorInvestigationsViewRaw = { // Investigation fields investigationId: number vendorId: number + vendorCountry: string | null pqSubmissionId: number | null requesterId: number | null qmManagerId: number | null diff --git a/db/schema/basicContractDocumnet.ts b/db/schema/basicContractDocumnet.ts index 5edc9836..7c8d4bc5 100644 --- a/db/schema/basicContractDocumnet.ts +++ b/db/schema/basicContractDocumnet.ts @@ -284,6 +284,8 @@ export type BasicContractTemplateStatsView = typeof basicContractTemplateStatsVi export const buyerSignatures = pgTable('buyer_signatures', { id: integer("id").primaryKey().generatedAlwaysAsIdentity(), name: varchar('name', { length: 255 }).notNull().default('삼성중공업'), + shiAddress: text('shi_address'), + shiCeoName: varchar('shi_ceo_name', { length: 255 }), imageUrl: text('image_url').notNull(), dataUrl: text('data_url'), // Base64 데이터 mimeType: varchar('mime_type', { length: 100 }), diff --git a/db/schema/pq.ts b/db/schema/pq.ts index 68c58613..e273f656 100644 --- a/db/schema/pq.ts +++ b/db/schema/pq.ts @@ -433,6 +433,7 @@ export const vendorInvestigationsView = pgView( // Essential vendor fields vendorName: vendors.vendorName, vendorCode: vendors.vendorCode, + vendorCountry: vendors.country, // PQ 정보 pqItems: vendorPQSubmissions.pqItems, diff --git a/lib/bidding/vendor/partners-bidding-list-columns.tsx b/lib/bidding/vendor/partners-bidding-list-columns.tsx index 09c3caad..c577ab89 100644 --- a/lib/bidding/vendor/partners-bidding-list-columns.tsx +++ b/lib/bidding/vendor/partners-bidding-list-columns.tsx @@ -174,7 +174,7 @@ export function getPartnersBiddingListColumns({ setRowAction }: PartnersBiddingL const startDate = row.original.submissionStartDate ? new Date(row.original.submissionStartDate).toISOString().slice(0, 16) : null const endDate = row.original.submissionEndDate ? new Date(row.original.submissionEndDate).toISOString().slice(0, 16) : null console.log(startDate, endDate, "startDate, endDate") - if (startDate && now < startDate) { + if (startDate && now < new Date (startDate)) { toast.warning('입찰기간 전 접근 제한', { description: `입찰기간이 아직 시작되지 않았습니다`, duration: 5000, @@ -249,9 +249,10 @@ export function getPartnersBiddingListColumns({ setRowAction }: PartnersBiddingL // 입찰기간 체크 (현 시간 기준으로 입찰기간 시작 전이면 접근 불가) const now = new Date() - const startDate = row.original.submissionStartDate ? new Date(row.original.submissionStartDate) : null - - if (startDate && now < startDate) { + const startDate = row.original.submissionStartDate ? new Date(row.original.submissionStartDate).toISOString().slice(0, 16) : null + const endDate = row.original.submissionEndDate ? new Date(row.original.submissionEndDate).toISOString().slice(0, 16) : null + console.log(startDate, endDate, "startDate, endDate") + if (startDate && now < new Date (startDate)) { toast.warning('입찰기간 전 접근 제한', { description: `입찰기간이 아직 시작되지 않았습니다`, duration: 5000, diff --git a/lib/general-contracts/service.ts b/lib/general-contracts/service.ts index b803d2d4..d0fa7b7a 100644 --- a/lib/general-contracts/service.ts +++ b/lib/general-contracts/service.ts @@ -7,7 +7,7 @@ import path from 'path' import { promises as fs } from 'fs' import { generalContracts, generalContractItems, generalContractAttachments } from '@/db/schema/generalContract' import { contracts, contractItems, contractEnvelopes, contractSigners } from '@/db/schema/contract' -import { basicContract, basicContractTemplates } from '@/db/schema/basicContractDocumnet' +import { basicContract, basicContractTemplates, buyerSignatures } from '@/db/schema/basicContractDocumnet' import { generalContractTemplates } from '@/db/schema' import { vendors } from '@/db/schema/vendors' import { users, roles, userRoles } from '@/db/schema/users' @@ -21,6 +21,9 @@ import { v4 as uuidv4 } from 'uuid' import { GetGeneralContractsSchema } from './validation' import { sendEmail } from '../mail/sendEmail' +const DEFAULT_SHI_ADDRESS = '경기도 성남시 분당구 판교로 227번길 23' +const DEFAULT_SHI_CEO_NAME = '최성안' + export async function getGeneralContracts(input: GetGeneralContractsSchema) { try { const offset = (input.page - 1) * input.perPage @@ -800,6 +803,16 @@ export async function getBasicInfo(contractId: number) { } const contract = result[0] + const [activeBuyerSignature] = await db + .select() + .from(buyerSignatures) + .where(eq(buyerSignatures.isActive, true)) + .limit(1) + + const shiAddress = activeBuyerSignature?.shiAddress || DEFAULT_SHI_ADDRESS + const shiCeoName = activeBuyerSignature?.shiCeoName || DEFAULT_SHI_CEO_NAME + const buyerSignatureName = activeBuyerSignature?.name || '삼성중공업' + return { success: true, enabled: true, // basic-info는 항상 활성화 @@ -844,7 +857,13 @@ export async function getBasicInfo(contractId: number) { interlockingSystem: contract.interlockingSystem, mandatoryDocuments: contract.mandatoryDocuments, contractTerminationConditions: contract.contractTerminationConditions, - externalYardEntry: contract.externalYardEntry || 'N' + externalYardEntry: contract.externalYardEntry || 'N', + + // SHI 서명 정보 + shiAddress, + shiCeoName, + buyerSignatureName, + buyerSignatureId: activeBuyerSignature?.id || null, } } } catch (error) { diff --git a/lib/general-contracts/utils.ts b/lib/general-contracts/utils.ts index ec15a3a1..5bbb5980 100644 --- a/lib/general-contracts/utils.ts +++ b/lib/general-contracts/utils.ts @@ -1,5 +1,8 @@ import { format } from "date-fns" +const DEFAULT_SHI_ADDRESS = "경기도 성남시 분당구 판교로 227번길 23" +const DEFAULT_SHI_CEO_NAME = "최성안" + /** * ContractSummary 인터페이스 (UI 컴포넌트와 맞춤) */ @@ -20,6 +23,9 @@ export function mapContractDataToTemplateVariables(contractSummary: ContractSumm const { basicInfo, items, storageInfo } = contractSummary const firstItem = items && items.length > 0 ? items[0] : {} + const shiAddress = basicInfo.shiAddress || DEFAULT_SHI_ADDRESS + const shiCeoName = basicInfo.shiCeoName || DEFAULT_SHI_CEO_NAME + // 날짜 포맷팅 헬퍼 (YYYY-MM-DD) const formatDate = (date: any) => { if (!date) return '' @@ -206,8 +212,8 @@ export function mapContractDataToTemplateVariables(contractSummary: ContractSumm // ---------------------------------- // 당사(SHI) 정보 (고정값/설정값) // ---------------------------------- - shiAddress: "경기도 성남시 분당구 판교로 227번길 23", // {{SHI_Address}}, {{위탁자 주소}} - shiCeoName: "최성안", // {{SHI_CEO_Name}}, {{대표이사}} + shiAddress: shiAddress, // {{SHI_Address}}, {{위탁자 주소}} + shiCeoName: shiCeoName, // {{SHI_CEO_Name}}, {{대표이사}} // ---------------------------------- // 품목 정보 diff --git a/lib/pq/pq-criteria/add-pq-dialog.tsx b/lib/pq/pq-criteria/add-pq-dialog.tsx index 1752f503..144e5ce4 100644 --- a/lib/pq/pq-criteria/add-pq-dialog.tsx +++ b/lib/pq/pq-criteria/add-pq-dialog.tsx @@ -1,485 +1,485 @@ -"use client" - -import * as React from "react" -import { useForm } from "react-hook-form" -import { zodResolver } from "@hookform/resolvers/zod" -import { z } from "zod" -import { Plus } from "lucide-react" -import { useRouter } from "next/navigation" - -import { - Dialog, - DialogTrigger, - DialogContent, - DialogHeader, - DialogTitle, - DialogDescription, - DialogFooter -} from "@/components/ui/dialog" -import { Button } from "@/components/ui/button" -import { Input } from "@/components/ui/input" -import { Textarea } from "@/components/ui/textarea" -import { - Form, - FormControl, - FormDescription, - FormField, - FormItem, - FormLabel, - FormMessage, -} from "@/components/ui/form" -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select" - -import { useToast } from "@/hooks/use-toast" -import { createPqCriteria } from "../service" -import { uploadPqCriteriaFileAction } from "@/lib/pq/service" -import { Dropzone, DropzoneInput, DropzoneZone, DropzoneUploadIcon, DropzoneTitle, DropzoneDescription } from "@/components/ui/dropzone" -import { FileList, FileListHeader, FileListInfo, FileListItem, FileListName, FileListDescription, FileListAction } from "@/components/ui/file-list" -import { X, Loader2 } from "lucide-react" - -// PQ 생성을 위한 Zod 스키마 정의 -const createPqSchema = z.object({ - code: z.string().min(1, "Code is required"), - checkPoint: z.string().min(1, "Check point is required"), - groupName: z.string().min(1, "Group is required"), - subGroupName: z.string().optional(), - description: z.string().optional(), - remarks: z.string().optional(), - inputFormat: z.string().default("TEXT"), - type: z.string().optional(), -}); - -type CreatePqFormType = z.infer; - -// 그룹 이름 옵션 -export const groupOptions = [ - "GENERAL", - "QMS", - "Warranty", - "HSE+", - "기타", -]; - -// 입력 형식 옵션 -const inputFormatOptions = [ - { value: "TEXT", label: "텍스트" }, - { value: "FILE", label: "파일" }, - { value: "EMAIL", label: "이메일" }, - { value: "PHONE", label: "전화번호" }, - { value: "FAX", label: "팩스번호" }, - { value: "NUMBER", label: "숫자" }, - { value: "NUMBER_WITH_UNIT", label: "숫자+단위" }, - { value: "TEXT_FILE", label: "텍스트 + 파일" }, -]; - -const typeOptions = [ - { value: "내자", label: "내자" }, - { value: "외자", label: "외자" }, - { value: "내외자", label: "내외자" }, -]; - -interface AddPqDialogProps { - pqListId: number; -} - -export function AddPqDialog({ pqListId }: AddPqDialogProps) { - const [open, setOpen] = React.useState(false) - const [isSubmitting, setIsSubmitting] = React.useState(false) - const [isUploading, setIsUploading] = React.useState(false) - const [uploadedFiles, setUploadedFiles] = React.useState< - { fileName: string; url: string; size?: number; originalFileName?: string }[] - >([]) - const router = useRouter() - const { toast } = useToast() - - // react-hook-form 설정 - const form = useForm({ - resolver: zodResolver(createPqSchema), - defaultValues: { - code: "", - checkPoint: "", - groupName: groupOptions[0], - subGroupName: "", - description: "", - remarks: "", - inputFormat: "TEXT", - type: "내외자", - }, - }) - const formState = form.formState - - async function onSubmit(data: CreatePqFormType) { - try { - setIsSubmitting(true) - - // 서버 액션 호출 - const result = await createPqCriteria(pqListId, { - ...data, - attachments: uploadedFiles, - }) - - if (!result.success) { - toast({ - title: "오류", - description: result.message || "PQ 항목 생성에 실패했습니다", - variant: "destructive", - }) - return - } - - // 성공 시 처리 - toast({ - title: "성공", - description: result.message || "PQ 항목이 성공적으로 생성되었습니다", - }) - - // 모달 닫고 폼 리셋 - form.reset() - setUploadedFiles([]) - setOpen(false) - - // 페이지 새로고침 - router.refresh() - - } catch (error) { - console.error('Error creating PQ criteria:', error) - toast({ - title: "오류", - description: "예상치 못한 오류가 발생했습니다", - variant: "destructive", - }) - } finally { - setIsSubmitting(false) - } - } - - function handleDialogOpenChange(nextOpen: boolean) { - if (!nextOpen) { - form.reset() - setUploadedFiles([]) - } - setOpen(nextOpen) - } - - const handleUpload = async (files: File[]) => { - try { - setIsUploading(true) - for (const file of files) { - const uploaded = await uploadPqCriteriaFileAction(file) - setUploadedFiles((prev) => [...prev, uploaded]) - } - toast({ - title: "업로드 완료", - description: "첨부파일이 업로드되었습니다.", - }) - } catch (error) { - console.error(error) - toast({ - title: "업로드 실패", - description: "첨부파일 업로드 중 오류가 발생했습니다.", - variant: "destructive", - }) - } finally { - setIsUploading(false) - } - } - - return ( - - - - - - - - PQ 항목 생성 - - 새 PQ 항목을 추가합니다. - - - -
- -
- {/* Group Name 필드 */} - ( - - 대분류 * - - - - )} - /> - - {/* Sub Group Name 필드 */} - ( - - 소분류 - - - - - 세부 분류를 위한 서브 그룹명을 입력하세요 (선택사항) - - - - )} - /> - {/* Code 필드 */} - ( - - 일련번호 * - - - - - PQ 항목의 고유 코드를 입력하세요 - - - - )} - /> - {/* Check Point 필드 */} - ( - - PQ 항목 * - - - - - - )} - /> - - {/* Type 필드 */} - ( - - 내/외자 구분 - - 미선택 시 기본값은 내외자입니다. - - - )} - /> - - {/* Input Format 필드 */} - ( - - 협력업체 입력사항 * - - - - )} - /> - - {/* 첨부 파일 업로드 */} -
-
- 첨부 파일 - {isUploading && ( -
- 업로드 중... -
- )} -
- handleUpload(files)} - onDropRejected={() => - toast({ - title: "업로드 실패", - description: "파일 크기/형식을 확인하세요.", - variant: "destructive", - }) - } - disabled={isUploading} - > - {() => ( - - - - - -
- -
- 파일을 드래그하거나 클릭하여 업로드 - PDF, 이미지, 문서 (최대 600MB) -
-
-
- 기준 문서 첨부가 필요한 경우 업로드하세요. -
- )} -
- - {uploadedFiles.length > 0 && ( -
-

첨부된 파일 ({uploadedFiles.length})

- - {uploadedFiles.map((file, idx) => ( - - - - {file.originalFileName || file.fileName} - {file.size && ( - {`${file.size} bytes`} - )} - - - setUploadedFiles((prev) => prev.filter((_, i) => i !== idx)) - } - > - - Remove - - - - ))} - -
- )} -
- - {/* Description 필드 */} - ( - - 설명 - -