From 2c02afd48a4d9276a4f5c132e088540a578d0972 Mon Sep 17 00:00:00 2001
From: dujinkim
Date: Tue, 30 Sep 2025 10:08:53 +0000
Subject: (대표님) 폼리스트, spreadjs 관련 변경사항, 벤더문서 뷰 쿼리 수정,
이메일 템플릿 추가 등
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../table/document-class-option-add-dialog.tsx | 161 +++++++-
lib/evaluation-criteria/stat.ts | 413 +++++++++++++++++++++
lib/forms/stat.ts | 209 ++++++++++-
lib/mail/templates/pq.hbs | 3 -
lib/mail/templates/vendor-rejected.hbs | 195 ++++++++++
lib/projects/service.ts | 23 ++
lib/rfq-last/attachment/vendor-response-table.tsx | 1 -
lib/rfq-last/service.ts | 87 ++++-
lib/rfq-last/validations.ts | 2 +-
lib/rfq-last/vendor/rfq-vendor-table.tsx | 8 +-
lib/sedp/get-form-tags.ts | 147 +++++++-
lib/tbe-last/service.ts | 2 +-
.../ship/send-to-shi-button.tsx | 70 ++--
lib/vendors/table/request-pq-dialog.tsx | 13 +
14 files changed, 1265 insertions(+), 69 deletions(-)
create mode 100644 lib/evaluation-criteria/stat.ts
create mode 100644 lib/mail/templates/vendor-rejected.hbs
(limited to 'lib')
diff --git a/lib/docu-list-rule/document-class/table/document-class-option-add-dialog.tsx b/lib/docu-list-rule/document-class/table/document-class-option-add-dialog.tsx
index 93681c09..31337675 100644
--- a/lib/docu-list-rule/document-class/table/document-class-option-add-dialog.tsx
+++ b/lib/docu-list-rule/document-class/table/document-class-option-add-dialog.tsx
@@ -5,7 +5,7 @@ import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form"
import { toast } from "sonner"
import * as z from "zod"
-import { Plus } from "lucide-react"
+import { Plus, Check, ChevronsUpDown } from "lucide-react"
import { Button } from "@/components/ui/button"
import {
@@ -25,12 +25,62 @@ import {
FormLabel,
FormMessage,
} from "@/components/ui/form"
-import { Input } from "@/components/ui/input"
+import {
+ Command,
+ CommandEmpty,
+ CommandGroup,
+ CommandInput,
+ CommandItem,
+} from "@/components/ui/command"
+import {
+ Popover,
+ PopoverContent,
+ PopoverTrigger,
+} from "@/components/ui/popover"
+import { cn } from "@/lib/utils"
+import { useParams } from "next/navigation"
import { createDocumentClassOptionItem } from "@/lib/docu-list-rule/document-class/service"
+import { getProjectCode } from "@/lib/projects/service"
+
+// API 응답 타입
+interface ScheduleSetting {
+ COL_NM: string
+ DC_OBX_USE_YN: string
+ PROJ_COL_NM: string
+ PROJ_COL_NM_EN: string
+ SCD_VIEW_MGNT: string
+ USE_YN1: string
+ USE_YN2: string
+}
+
+// 프로젝트 일정 설정을 가져오는 함수
+async function getProjectKindScheduleSetting(projectCode: string): Promise {
+ try {
+ const response = await fetch(
+ `http://60.100.99.217/DDP/Services/VNDRService.svc/GetProjectKindScheduleSetting?PROJ_NO=${projectCode}`,
+ {
+ method: 'GET',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ }
+ )
+
+ if (!response.ok) {
+ throw new Error('Failed to fetch schedule settings')
+ }
+
+ const data = await response.json()
+ return data.GetProjectKindScheduleSettingResult || []
+ } catch (error) {
+ console.error('Error fetching schedule settings:', error)
+ return []
+ }
+}
const createOptionSchema = z.object({
- optionCode: z.string().min(1, "코드는 필수입니다."),
+ optionCode: z.string().min(1, "옵션을 선택해주세요."),
})
type CreateOptionSchema = z.infer
@@ -42,7 +92,12 @@ interface DocumentClassOptionAddDialogProps {
export function DocumentClassOptionAddDialog({ documentClassId, onSuccess }: DocumentClassOptionAddDialogProps) {
const [open, setOpen] = React.useState(false)
+ const [comboboxOpen, setComboboxOpen] = React.useState(false)
const [isPending, startTransition] = React.useTransition()
+ const [scheduleSettings, setScheduleSettings] = React.useState([])
+ const [isLoading, setIsLoading] = React.useState(false)
+ const params = useParams()
+ const projectId = Number(params?.projectId)
const form = useForm({
resolver: zodResolver(createOptionSchema),
@@ -51,6 +106,35 @@ export function DocumentClassOptionAddDialog({ documentClassId, onSuccess }: Doc
},
})
+ // Dialog가 열릴 때 데이터 로드
+ React.useEffect(() => {
+ if (open && projectId) {
+ loadScheduleSettings()
+ }
+ }, [open, projectId])
+
+ const loadScheduleSettings = async () => {
+ setIsLoading(true)
+ try {
+ // 먼저 projectId로 프로젝트 코드 가져오기
+ const projectCode = await getProjectCode(projectId)
+
+ if (!projectCode) {
+ toast.error("프로젝트 코드를 찾을 수 없습니다.")
+ return
+ }
+
+ // 프로젝트 코드로 일정 설정 가져오기
+ const settings = await getProjectKindScheduleSetting(projectCode)
+ setScheduleSettings(settings)
+ } catch (error) {
+ console.error("Error loading schedule settings:", error)
+ toast.error("옵션 목록을 불러오는 중 오류가 발생했습니다.")
+ } finally {
+ setIsLoading(false)
+ }
+ }
+
const handleSubmit = (data: CreateOptionSchema) => {
startTransition(async () => {
try {
@@ -79,6 +163,10 @@ export function DocumentClassOptionAddDialog({ documentClassId, onSuccess }: Doc
form.reset()
}
+ const selectedOption = scheduleSettings.find(
+ (setting) => setting.COL_NM === form.watch("optionCode")
+ )
+
return (
)
-}
\ No newline at end of file
+}
\ No newline at end of file
diff --git a/lib/evaluation-criteria/stat.ts b/lib/evaluation-criteria/stat.ts
new file mode 100644
index 00000000..c39c8627
--- /dev/null
+++ b/lib/evaluation-criteria/stat.ts
@@ -0,0 +1,413 @@
+"use server"
+
+import db from "@/db/db"
+import { vendors, contracts, contractItems, forms, formEntries, formMetas, tags, tagClasses, tagClassAttributes, projects } from "@/db/schema"
+import { eq, and, inArray } from "drizzle-orm"
+import { getEditableFieldsByTag } from "./services"
+import { getServerSession } from "next-auth/next"
+import { authOptions } from "@/app/api/auth/[...nextauth]/route"
+
+interface VendorFormStatus {
+ vendorId: number
+ vendorName: string
+ formCount: number // 벤더가 가진 form 개수
+ tagCount: number // 벤더가 가진 tag 개수
+ totalFields: number // 입력해야 하는 총 필드 개수
+ completedFields: number // 입력 완료된 필드 개수
+ completionRate: number // 완료율 (%)
+}
+
+export interface FormStatusByVendor {
+ tagCount: number;
+ totalFields: number;
+ completedFields: number;
+ completionRate: number;
+ upcomingCount: number; // 7일 이내 임박한 개수
+ overdueCount: number; // 지연된 개수
+}
+
+export async function getProjectsWithContracts() {
+ try {
+ const projectList = await db
+ .selectDistinct({
+ id: projects.id,
+ projectCode: projects.code,
+ projectName: projects.name,
+ })
+ .from(projects)
+ .innerJoin(contracts, eq(contracts.projectId, projects.id))
+ .orderBy(projects.code)
+
+ return projectList
+ } catch (error) {
+ console.error('Error getting projects with contracts:', error)
+ throw new Error('계약이 있는 프로젝트 조회 중 오류가 발생했습니다.')
+ }
+}
+
+
+
+export async function getVendorFormStatus(projectId?: number): Promise {
+ try {
+ // 1. 벤더 조회 쿼리 수정
+ const vendorList = projectId
+ ? await db
+ .selectDistinct({
+ vendorId: vendors.id,
+ vendorName: vendors.vendorName,
+ })
+ .from(vendors)
+ .innerJoin(contracts, eq(contracts.vendorId, vendors.id))
+ .where(eq(contracts.projectId, projectId))
+ : await db
+ .selectDistinct({
+ vendorId: vendors.id,
+ vendorName: vendors.vendorName,
+ })
+ .from(vendors)
+ .innerJoin(contracts, eq(contracts.vendorId, vendors.id))
+
+
+ const vendorStatusList: VendorFormStatus[] = []
+
+ for (const vendor of vendorList) {
+ let vendorFormCount = 0
+ let vendorTagCount = 0
+ let vendorTotalFields = 0
+ let vendorCompletedFields = 0
+ const uniqueTags = new Set()
+
+ // 2. 계약 조회 시 projectId 필터 추가
+ const vendorContracts = projectId
+ ? await db
+ .select({
+ id: contracts.id,
+ projectId: contracts.projectId
+ })
+ .from(contracts)
+ .where(
+ and(
+ eq(contracts.vendorId, vendor.vendorId),
+ eq(contracts.projectId, projectId)
+ )
+ )
+ : await db
+ .select({
+ id: contracts.id,
+ projectId: contracts.projectId
+ })
+ .from(contracts)
+ .where(eq(contracts.vendorId, vendor.vendorId))
+
+
+ for (const contract of vendorContracts) {
+ // 3. 계약별 contractItems 조회
+ const contractItemsList = await db
+ .select({
+ id: contractItems.id
+ })
+ .from(contractItems)
+ .where(eq(contractItems.contractId, contract.id))
+
+ for (const contractItem of contractItemsList) {
+ // 4. contractItem별 forms 조회
+ const formsList = await db
+ .select({
+ id: forms.id,
+ formCode: forms.formCode,
+ contractItemId: forms.contractItemId
+ })
+ .from(forms)
+ .where(eq(forms.contractItemId, contractItem.id))
+
+ vendorFormCount += formsList.length
+
+ // 5. formEntries 조회
+ const entriesList = await db
+ .select({
+ id: formEntries.id,
+ formCode: formEntries.formCode,
+ data: formEntries.data
+ })
+ .from(formEntries)
+ .where(eq(formEntries.contractItemId, contractItem.id))
+
+ // 6. TAG별 편집 가능 필드 조회
+ const editableFieldsByTag = await getEditableFieldsByTag(contractItem.id, contract.projectId)
+
+ for (const entry of entriesList) {
+ // formMetas에서 해당 formCode의 columns 조회
+ const metaResult = await db
+ .select({
+ columns: formMetas.columns
+ })
+ .from(formMetas)
+ .where(
+ and(
+ eq(formMetas.formCode, entry.formCode),
+ eq(formMetas.projectId, contract.projectId)
+ )
+ )
+ .limit(1)
+
+ if (metaResult.length === 0) continue
+
+ const metaColumns = metaResult[0].columns as any[]
+
+ // shi가 'IN' 또는 'BOTH'인 필드 찾기
+ const inputRequiredFields = metaColumns
+ .filter(col => col.shi === 'IN' || col.shi === 'BOTH')
+ .map(col => col.key)
+
+ // entry.data 분석 (배열로 가정)
+ const dataArray = Array.isArray(entry.data) ? entry.data : []
+
+ for (const dataItem of dataArray) {
+ if (typeof dataItem !== 'object' || !dataItem) continue
+
+ const tagNo = dataItem.TAG_NO
+ if (tagNo) {
+ uniqueTags.add(tagNo)
+
+ // TAG별 편집 가능 필드 가져오기
+ const tagEditableFields = editableFieldsByTag.get(tagNo) || []
+
+ // 최종 입력 필요 필드 = shi 기반 필드 + TAG 기반 편집 가능 필드
+ const allRequiredFields = inputRequiredFields.filter(field =>
+ tagEditableFields.includes(field)
+ )
+ // 각 필드별 입력 상태 체크
+ for (const fieldKey of allRequiredFields) {
+ vendorTotalFields++
+
+ const fieldValue = dataItem[fieldKey]
+ // 값이 있고, 빈 문자열이 아니고, null이 아니면 입력 완료
+ if (fieldValue !== undefined && fieldValue !== null && fieldValue !== '') {
+ vendorCompletedFields++
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // 완료율 계산
+ const completionRate = vendorTotalFields > 0
+ ? Math.round((vendorCompletedFields / vendorTotalFields) * 100 * 10) / 10
+ : 0
+
+ vendorStatusList.push({
+ vendorId: vendor.vendorId,
+ vendorName: vendor.vendorName || '이름 없음',
+ formCount: vendorFormCount,
+ tagCount: uniqueTags.size,
+ totalFields: vendorTotalFields,
+ completedFields: vendorCompletedFields,
+ completionRate
+ })
+ }
+
+ return vendorStatusList
+
+ } catch (error) {
+ console.error('Error getting vendor form status:', error)
+ throw new Error('벤더별 Form 입력 현황 조회 중 오류가 발생했습니다.')
+ }
+}
+
+
+
+export async function getFormStatusByVendor(projectId: number, formCode: string): Promise {
+ try {
+ const session = await getServerSession(authOptions)
+ if (!session?.user?.id) {
+ throw new Error("인증이 필요합니다.")
+ }
+
+ const vendorStatusList: FormStatusByVendor[] = []
+ const vendorId = Number(session.user.companyId)
+
+ const vendorContracts = await db
+ .select({
+ id: contracts.id,
+ projectId: contracts.projectId
+ })
+ .from(contracts)
+ .where(
+ and(
+ eq(contracts.vendorId, vendorId),
+ eq(contracts.projectId, projectId)
+ )
+ )
+
+ const contractIds = vendorContracts.map(v => v.id)
+
+ const contractItemsList = await db
+ .select({
+ id: contractItems.id
+ })
+ .from(contractItems)
+ .where(inArray(contractItems.contractId, contractIds))
+
+ const contractItemIds = contractItemsList.map(v => v.id)
+
+ let vendorFormCount = 0
+ let vendorTagCount = 0
+ let vendorTotalFields = 0
+ let vendorCompletedFields = 0
+ let vendorUpcomingCount = 0 // 7일 이내 임박한 개수
+ let vendorOverdueCount = 0 // 지연된 개수
+ const uniqueTags = new Set()
+ const processedTags = new Set() // 중복 처리 방지용
+
+ // 현재 날짜와 7일 후 날짜 계산
+ const today = new Date()
+ today.setHours(0, 0, 0, 0) // 시간 부분 제거
+ const sevenDaysLater = new Date(today)
+ sevenDaysLater.setDate(sevenDaysLater.getDate() + 7)
+
+ // 4. contractItem별 forms 조회
+ const formsList = await db
+ .select({
+ id: forms.id,
+ formCode: forms.formCode,
+ contractItemId: forms.contractItemId
+ })
+ .from(forms)
+ .where(
+ and(
+ inArray(forms.contractItemId, contractItemIds),
+ eq(forms.formCode, formCode)
+ )
+ )
+
+ vendorFormCount += formsList.length
+
+ // 5. formEntries 조회
+ const entriesList = await db
+ .select({
+ id: formEntries.id,
+ formCode: formEntries.formCode,
+ data: formEntries.data
+ })
+ .from(formEntries)
+ .where(
+ and(
+ inArray(formEntries.contractItemId, contractItemIds),
+ eq(formEntries.formCode, formCode)
+ )
+ )
+
+ // 6. TAG별 편집 가능 필드 조회
+ const editableFieldsByTag = new Map()
+
+ for (const contractItemId of contractItemIds) {
+ const tagFields = await getEditableFieldsByTag(contractItemId, projectId)
+
+ tagFields.forEach((fields, tagNo) => {
+ if (!editableFieldsByTag.has(tagNo)) {
+ editableFieldsByTag.set(tagNo, fields)
+ } else {
+ const existingFields = editableFieldsByTag.get(tagNo) || []
+ const mergedFields = [...new Set([...existingFields, ...fields])]
+ editableFieldsByTag.set(tagNo, mergedFields)
+ }
+ })
+ }
+
+ for (const entry of entriesList) {
+ const metaResult = await db
+ .select({
+ columns: formMetas.columns
+ })
+ .from(formMetas)
+ .where(
+ and(
+ eq(formMetas.formCode, entry.formCode),
+ eq(formMetas.projectId, projectId)
+ )
+ )
+ .limit(1)
+
+ if (metaResult.length === 0) continue
+
+ const metaColumns = metaResult[0].columns as any[]
+
+ const inputRequiredFields = metaColumns
+ .filter(col => col.shi === 'IN' || col.shi === 'BOTH')
+ .map(col => col.key)
+
+ const dataArray = Array.isArray(entry.data) ? entry.data : []
+
+ for (const dataItem of dataArray) {
+ if (typeof dataItem !== 'object' || !dataItem) continue
+
+ const tagNo = dataItem.TAG_NO
+ if (tagNo) {
+ uniqueTags.add(tagNo)
+
+ // TAG별 편집 가능 필드 가져오기
+ const tagEditableFields = editableFieldsByTag.get(tagNo) || []
+
+ const allRequiredFields = inputRequiredFields.filter(field =>
+ tagEditableFields.includes(field)
+ )
+
+ // 해당 TAG의 필드 완료 상태 체크
+ let tagHasIncompleteFields = false
+
+ for (const fieldKey of allRequiredFields) {
+ vendorTotalFields++
+
+ const fieldValue = dataItem[fieldKey]
+ if (fieldValue !== undefined && fieldValue !== null && fieldValue !== '') {
+ vendorCompletedFields++
+ } else {
+ tagHasIncompleteFields = true
+ }
+ }
+
+ // 미완료 TAG에 대해서만 날짜 체크 (TAG당 한 번만 처리)
+ if (!processedTags.has(tagNo) && tagHasIncompleteFields) {
+ processedTags.add(tagNo)
+
+ const targetDate = dataItem.target_date
+ if (targetDate) {
+ const target = new Date(targetDate)
+ target.setHours(0, 0, 0, 0) // 시간 부분 제거
+
+ if (target < today) {
+ // 미완료이면서 지연된 경우 (오늘보다 이전)
+ vendorOverdueCount++
+ } else if (target >= today && target <= sevenDaysLater) {
+ // 미완료이면서 7일 이내 임박한 경우
+ vendorUpcomingCount++
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // 완료율 계산
+ const completionRate = vendorTotalFields > 0
+ ? Math.round((vendorCompletedFields / vendorTotalFields) * 100 * 10) / 10
+ : 0
+
+ vendorStatusList.push({
+ tagCount: uniqueTags.size,
+ totalFields: vendorTotalFields,
+ completedFields: vendorCompletedFields,
+ completionRate,
+ upcomingCount: vendorUpcomingCount,
+ overdueCount: vendorOverdueCount
+ })
+
+ return vendorStatusList
+
+ } catch (error) {
+ console.error('Error getting vendor form status:', error)
+ throw new Error('벤더별 Form 입력 현황 조회 중 오류가 발생했습니다.')
+ }
+}
\ No newline at end of file
diff --git a/lib/forms/stat.ts b/lib/forms/stat.ts
index 80193c48..054f2462 100644
--- a/lib/forms/stat.ts
+++ b/lib/forms/stat.ts
@@ -2,8 +2,10 @@
import db from "@/db/db"
import { vendors, contracts, contractItems, forms, formEntries, formMetas, tags, tagClasses, tagClassAttributes, projects } from "@/db/schema"
-import { eq, and } from "drizzle-orm"
+import { eq, and, inArray } from "drizzle-orm"
import { getEditableFieldsByTag } from "./services"
+import { getServerSession } from "next-auth/next"
+import { authOptions } from "@/app/api/auth/[...nextauth]/route"
interface VendorFormStatus {
vendorId: number
@@ -15,6 +17,15 @@ interface VendorFormStatus {
completionRate: number // 완료율 (%)
}
+export interface FormStatusByVendor {
+ tagCount: number;
+ totalFields: number;
+ completedFields: number;
+ completionRate: number;
+ upcomingCount: number; // 7일 이내 임박한 개수
+ overdueCount: number; // 지연된 개수
+}
+
export async function getProjectsWithContracts() {
try {
const projectList = await db
@@ -204,3 +215,199 @@ export async function getVendorFormStatus(projectId?: number): Promise {
+ try {
+ const session = await getServerSession(authOptions)
+ if (!session?.user?.id) {
+ throw new Error("인증이 필요합니다.")
+ }
+
+ const vendorStatusList: FormStatusByVendor[] = []
+ const vendorId = Number(session.user.companyId)
+
+ const vendorContracts = await db
+ .select({
+ id: contracts.id,
+ projectId: contracts.projectId
+ })
+ .from(contracts)
+ .where(
+ and(
+ eq(contracts.vendorId, vendorId),
+ eq(contracts.projectId, projectId)
+ )
+ )
+
+ const contractIds = vendorContracts.map(v => v.id)
+
+ const contractItemsList = await db
+ .select({
+ id: contractItems.id
+ })
+ .from(contractItems)
+ .where(inArray(contractItems.contractId, contractIds))
+
+ const contractItemIds = contractItemsList.map(v => v.id)
+
+ let vendorFormCount = 0
+ let vendorTagCount = 0
+ let vendorTotalFields = 0
+ let vendorCompletedFields = 0
+ let vendorUpcomingCount = 0 // 7일 이내 임박한 개수
+ let vendorOverdueCount = 0 // 지연된 개수
+ const uniqueTags = new Set()
+ const processedTags = new Set() // 중복 처리 방지용
+
+ // 현재 날짜와 7일 후 날짜 계산
+ const today = new Date()
+ today.setHours(0, 0, 0, 0) // 시간 부분 제거
+ const sevenDaysLater = new Date(today)
+ sevenDaysLater.setDate(sevenDaysLater.getDate() + 7)
+
+ // 4. contractItem별 forms 조회
+ const formsList = await db
+ .select({
+ id: forms.id,
+ formCode: forms.formCode,
+ contractItemId: forms.contractItemId
+ })
+ .from(forms)
+ .where(
+ and(
+ inArray(forms.contractItemId, contractItemIds),
+ eq(forms.formCode, formCode)
+ )
+ )
+
+ vendorFormCount += formsList.length
+
+ // 5. formEntries 조회
+ const entriesList = await db
+ .select({
+ id: formEntries.id,
+ formCode: formEntries.formCode,
+ data: formEntries.data
+ })
+ .from(formEntries)
+ .where(
+ and(
+ inArray(formEntries.contractItemId, contractItemIds),
+ eq(formEntries.formCode, formCode)
+ )
+ )
+
+ // 6. TAG별 편집 가능 필드 조회
+ const editableFieldsByTag = new Map()
+
+ for (const contractItemId of contractItemIds) {
+ const tagFields = await getEditableFieldsByTag(contractItemId, projectId)
+
+ tagFields.forEach((fields, tagNo) => {
+ if (!editableFieldsByTag.has(tagNo)) {
+ editableFieldsByTag.set(tagNo, fields)
+ } else {
+ const existingFields = editableFieldsByTag.get(tagNo) || []
+ const mergedFields = [...new Set([...existingFields, ...fields])]
+ editableFieldsByTag.set(tagNo, mergedFields)
+ }
+ })
+ }
+
+ for (const entry of entriesList) {
+ const metaResult = await db
+ .select({
+ columns: formMetas.columns
+ })
+ .from(formMetas)
+ .where(
+ and(
+ eq(formMetas.formCode, entry.formCode),
+ eq(formMetas.projectId, projectId)
+ )
+ )
+ .limit(1)
+
+ if (metaResult.length === 0) continue
+
+ const metaColumns = metaResult[0].columns as any[]
+
+ const inputRequiredFields = metaColumns
+ .filter(col => col.shi === 'IN' || col.shi === 'BOTH')
+ .map(col => col.key)
+
+ const dataArray = Array.isArray(entry.data) ? entry.data : []
+
+ for (const dataItem of dataArray) {
+ if (typeof dataItem !== 'object' || !dataItem) continue
+
+ const tagNo = dataItem.TAG_NO
+ if (tagNo) {
+ uniqueTags.add(tagNo)
+
+ // TAG별 편집 가능 필드 가져오기
+ const tagEditableFields = editableFieldsByTag.get(tagNo) || []
+
+ const allRequiredFields = inputRequiredFields.filter(field =>
+ tagEditableFields.includes(field)
+ )
+
+ // 해당 TAG의 필드 완료 상태 체크
+ let tagHasIncompleteFields = false
+
+ for (const fieldKey of allRequiredFields) {
+ vendorTotalFields++
+
+ const fieldValue = dataItem[fieldKey]
+ if (fieldValue !== undefined && fieldValue !== null && fieldValue !== '') {
+ vendorCompletedFields++
+ } else {
+ tagHasIncompleteFields = true
+ }
+ }
+
+ // 미완료 TAG에 대해서만 날짜 체크 (TAG당 한 번만 처리)
+ if (!processedTags.has(tagNo) && tagHasIncompleteFields) {
+ processedTags.add(tagNo)
+
+ const targetDate = dataItem.DUE_DATE
+ if (targetDate) {
+ const target = new Date(targetDate)
+ target.setHours(0, 0, 0, 0) // 시간 부분 제거
+
+ if (target < today) {
+ // 미완료이면서 지연된 경우 (오늘보다 이전)
+ vendorOverdueCount++
+ } else if (target >= today && target <= sevenDaysLater) {
+ // 미완료이면서 7일 이내 임박한 경우
+ vendorUpcomingCount++
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // 완료율 계산
+ const completionRate = vendorTotalFields > 0
+ ? Math.round((vendorCompletedFields / vendorTotalFields) * 100 * 10) / 10
+ : 0
+
+ vendorStatusList.push({
+ tagCount: uniqueTags.size,
+ totalFields: vendorTotalFields,
+ completedFields: vendorCompletedFields,
+ completionRate,
+ upcomingCount: vendorUpcomingCount,
+ overdueCount: vendorOverdueCount
+ })
+
+ return vendorStatusList
+
+ } catch (error) {
+ console.error('Error getting vendor form status:', error)
+ throw new Error('벤더별 Form 입력 현황 조회 중 오류가 발생했습니다.')
+ }
+}
\ No newline at end of file
diff --git a/lib/mail/templates/pq.hbs b/lib/mail/templates/pq.hbs
index 02523696..abdff056 100644
--- a/lib/mail/templates/pq.hbs
+++ b/lib/mail/templates/pq.hbs
@@ -61,9 +61,6 @@
아래의 해당 링크를 통해 당사 eVCP시스템에 접속하시어 요청드린 PQ 항목 및 자료에 대한 제출 요청드립니다.
-
- 별도의 견적을 제출하시어 당사에서 적극 검토할 수 있도록 협조 바랍니다.
-
귀사의 제출 자료 및 정보는 아래의 제출 마감일 이전에 당사로 제출 되어야 하며,
diff --git a/lib/mail/templates/vendor-rejected.hbs b/lib/mail/templates/vendor-rejected.hbs
new file mode 100644
index 00000000..22a1d6f3
--- /dev/null
+++ b/lib/mail/templates/vendor-rejected.hbs
@@ -0,0 +1,195 @@
+
+
+
+
+
+ eVCP 메일
+
+
+
+
+
+
+
+
+ eVCP
+
+
+
+
+
+
+ {{#if (eq language 'ko')}}등록 거절{{else}}REGISTRATION REJECTED{{/if}}
+
+
+
+
+ {{#if (eq language 'ko')}}
+ 업체 등록 신청이 거절되었습니다
+ {{else}}
+ Your Vendor Registration Has Been Rejected
+ {{/if}}
+
+ {{#if (eq language 'ko')}}
+ 귀하의 업체 등록 신청이 검토 결과 거절되었습니다.
+ 등록 기준에 부합하지 않거나 추가 정보 확인이 필요하여 거절 처리되었습니다.
+ {{else}}
+ After review, your vendor registration application has been rejected.
+ The application did not meet our registration criteria or required additional verification.
+ {{/if}}
+
+
+
+
+
+ {{#if (eq language 'ko')}}거절 사유 및 향후 절차{{else}}Rejection Reason & Next Steps{{/if}}
+
+
+
+ {{#if (eq language 'ko')}}
+ 등록 기준에 부합하지 않거나 제출 정보가 불충분합니다
+ {{else}}
+ The application did not meet registration criteria or submitted information was insufficient
+ {{/if}}
+
+
+ {{#if (eq language 'ko')}}
+ 추가 정보 확인 및 보완이 필요한 경우 재신청 가능합니다
+ {{else}}
+ You may reapply if you can provide additional information and meet requirements
+ {{/if}}
+
+
+ {{#if (eq language 'ko')}}
+ 재신청을 원하시면 모든 필요 서류를 준비하여 다시 신청해주세요
+ {{else}}
+ If you wish to reapply, please prepare all required documents and submit again
+ {{/if}}
+
+ {{#if (eq language 'ko')}}신청 정보{{else}}Application Information{{/if}}
+
+
+ {{#if (eq language 'ko')}}업체명{{else}}Company{{/if}}: {{vendorName}}
+
+
+ {{#if (eq language 'ko')}}이메일{{else}}Email{{/if}}: {{email}}
+
+
+ {{#if (eq language 'ko')}}신청 상태{{else}}Application Status{{/if}}:
+
+ {{#if (eq language 'ko')}}거절됨{{else}}Rejected{{/if}}
+
+
+
+
+
+
+ {{#if (eq language 'ko')}}
+ 등록 기준에 대한 자세한 내용은 eVCP 플랫폼을 방문하시거나
+ {{supportEmail}}로
+ 문의해 주세요.
+ {{else}}
+ For more information about registration criteria, please visit the eVCP platform or
+ contact us at {{supportEmail}}.
+ {{/if}}
+