From b67e36df49f067cbd5ba899f9fbcc755f38d4b4f Mon Sep 17 00:00:00 2001 From: dujinkim Date: Thu, 4 Sep 2025 08:31:31 +0000 Subject: (대표님, 최겸, 임수민) 작업사항 커밋 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/forms/vendor-completion-stats.ts | 340 ++++++++++++++++++++++++++++++----- 1 file changed, 296 insertions(+), 44 deletions(-) (limited to 'lib/forms') diff --git a/lib/forms/vendor-completion-stats.ts b/lib/forms/vendor-completion-stats.ts index db2d376d..97efec30 100644 --- a/lib/forms/vendor-completion-stats.ts +++ b/lib/forms/vendor-completion-stats.ts @@ -1,8 +1,8 @@ "use server"; import db from "@/db/db"; -import { - formMetas, +import { + formMetas, formEntries, tags, tagClasses, @@ -66,6 +66,7 @@ export interface VendorAllContractsCompletionSummary { contracts: VendorContractCompletionStats[]; totalContracts: number; totalForms: number; + totalTags: number; totalRequiredFields: number; totalFilledFields: number; totalEmptyFields: number; @@ -82,18 +83,21 @@ export interface VendorAllContractsCompletionSummary { /** * 필드가 벤더에 의해 편집 가능한지 확인 - * SHI 값이 "BOTH" 또는 "IN"인 경우만 벤더가 편집 가능 + * SHI 값이 "BOTH" 또는 "IN"인 경우만 벤더가 편집 가능 (대소문자 무관) */ function isFieldEditableByVendor(column: DataTableColumnJSON): boolean { - return column.shi === "BOTH" || column.shi === "IN"; + const shi = column.shi?.toString().toUpperCase(); + const isEditable = shi === "BOTH" || shi === "IN"; + console.log(`isFieldEditableByVendor - Key: ${column.key}, shi: ${column.shi}, upperShi: ${shi}, isEditable: ${isEditable}`); + return isEditable; } /** * 특정 태그에 대해 편집 가능한 필드 목록을 가져옴 */ async function getEditableFieldsForTag( - tagNo: string, - contractItemId: number, + tagNo: string, + contractItemId: number, projectId: number ): Promise { try { @@ -112,8 +116,11 @@ async function getEditableFieldsForTag( .limit(1); if (tagResult.length === 0) { + console.log(`getEditableFieldsForTag - No tag found for tagNo: ${tagNo}, contractItemId: ${contractItemId}`); return []; } + + console.log(`getEditableFieldsForTag - Found tag for tagNo: ${tagNo}, class: ${tagResult[0].tagClass}`); // 2. tagClasses에서 해당 class와 projectId로 tagClass 찾기 const tagClassResult = await db @@ -128,8 +135,11 @@ async function getEditableFieldsForTag( .limit(1); if (tagClassResult.length === 0) { + console.log(`getEditableFieldsForTag - No tag class found for class: ${tagResult[0].tagClass}, projectId: ${projectId}`); return []; } + + console.log(`getEditableFieldsForTag - Found tag class: ${tagClassResult[0].id} for class: ${tagResult[0].tagClass}`); // 3. tagClassAttributes에서 편집 가능한 필드 목록 조회 const editableAttributes = await db @@ -138,6 +148,8 @@ async function getEditableFieldsForTag( .where(eq(tagClassAttributes.tagClassId, tagClassResult[0].id)) .orderBy(tagClassAttributes.seq); + console.log(`getEditableFieldsForTag - Found ${editableAttributes.length} editable attributes for tag class ${tagClassResult[0].id}:`, editableAttributes.map(attr => attr.attId)); + return editableAttributes.map(attr => attr.attId); } catch (error) { console.error(`Error getting editable fields for tag ${tagNo}:`, error); @@ -191,12 +203,7 @@ export async function calculateVendorFormCompletion( const metaRows = await db .select() .from(formMetas) - .where( - and( - eq(formMetas.formCode, formCode), - eq(formMetas.projectId, projectId) - ) - ) + .where(eq(formMetas.formCode, formCode)) .orderBy(desc(formMetas.updatedAt)) .limit(1); @@ -205,6 +212,8 @@ export async function calculateVendorFormCompletion( console.warn(`No form meta found for formCode: ${formCode} and projectId: ${projectId}`); return null; } + + console.log(`calculateVendorFormCompletion - Found form meta for formCode: ${formCode}, projectId: ${projectId}, columns type: ${typeof meta.columns}, isArray: ${Array.isArray(meta.columns)}`); // 3. Form 실제 데이터 조회 const entryRows = await db @@ -227,10 +236,15 @@ export async function calculateVendorFormCompletion( // 4. 컬럼 정의에서 벤더가 편집 가능한 필드 필터링 const columns = meta.columns as DataTableColumnJSON[]; - const excludeKeys = ['BF_TAG_NO', 'TAG_TYPE_ID', 'PIC_NO', 'status', 'TAG_NO', 'TAG_DESC']; - const editableColumns = columns.filter(col => + const excludeKeys = ['BF_TAG_NO', 'TAG_TYPE_ID', 'PIC_NO', 'status']; + const editableColumns = columns.filter(col => !excludeKeys.includes(col.key) && isFieldEditableByVendor(col) ); + + console.log(`calculateVendorFormCompletion - Total columns: ${columns.length}, Editable columns: ${editableColumns.length}`); + console.log(`calculateVendorFormCompletion - Editable column keys:`, editableColumns.map(col => col.key)); + console.log(`calculateVendorFormCompletion - All column keys:`, columns.map(col => col.key)); + console.log(`calculateVendorFormCompletion - All column shi values:`, columns.map(col => col.shi)); // 5. 각 태그별로 완성도 계산 const detailsByTag: VendorFormCompletionStats['detailsByTag'] = []; @@ -243,13 +257,8 @@ export async function calculateVendorFormCompletion( const tagNo = rowData.TAG_NO as string; if (!tagNo) continue; - // 이 태그에 대해 실제로 편집 가능한 필드 목록 가져오기 - const tagEditableFields = await getEditableFieldsForTag(tagNo, contractItemId, projectId); - - // 컬럼 정의와 태그별 편집 가능 필드를 교집합으로 구해서 실제 편집 가능한 필드 확정 - const actualEditableFields = editableColumns.filter(col => - tagEditableFields.includes(col.key) - ); + // Debug 페이지와 동일하게 직접 editableColumns 사용 (getEditableFieldsForTag 대신) + const actualEditableFields = editableColumns; const requiredFieldsCount = actualEditableFields.length; let filledFieldsCount = 0; @@ -263,8 +272,8 @@ export async function calculateVendorFormCompletion( } const emptyFieldsCount = requiredFieldsCount - filledFieldsCount; - const completionPercentage = requiredFieldsCount > 0 - ? Math.round((filledFieldsCount / requiredFieldsCount) * 100) + const completionPercentage = requiredFieldsCount > 0 + ? Math.round((filledFieldsCount / requiredFieldsCount) * 100) : 100; detailsByTag.push({ @@ -280,8 +289,8 @@ export async function calculateVendorFormCompletion( } const totalEmptyFields = totalRequiredFields - totalFilledFields; - const overallCompletionPercentage = totalRequiredFields > 0 - ? Math.round((totalFilledFields / totalRequiredFields) * 100) + const overallCompletionPercentage = totalRequiredFields > 0 + ? Math.round((totalFilledFields / totalRequiredFields) * 100) : 100; return { @@ -348,13 +357,13 @@ export async function getProjectVendorCompletionSummary( // 3. 각 contract item별로 완성도 계산 const vendorStats: VendorFormCompletionStats[] = []; - + for (const item of contractItemsInfo) { const stats = await calculateVendorFormCompletion( - item.contractItemId, + item.contractItemId, formCode ); - + if (stats) { vendorStats.push(stats); } @@ -363,8 +372,8 @@ export async function getProjectVendorCompletionSummary( // 4. 전체 평균 완성도 계산 const averageCompletionPercentage = vendorStats.length > 0 ? Math.round( - vendorStats.reduce((sum, stat) => sum + stat.completionPercentage, 0) / vendorStats.length - ) + vendorStats.reduce((sum, stat) => sum + stat.completionPercentage, 0) / vendorStats.length + ) : 0; return { @@ -447,8 +456,8 @@ export async function calculateVendorContractCompletion( const totalEmptyFields = totalRequiredFields - totalFilledFields; const averageCompletionPercentage = formStats.length > 0 ? Math.round( - formStats.reduce((sum, stat) => sum + stat.completionPercentage, 0) / formStats.length - ) + formStats.reduce((sum, stat) => sum + stat.completionPercentage, 0) / formStats.length + ) : 0; return { @@ -515,15 +524,23 @@ export async function getVendorAllContractsCompletionSummary( // 3. 각 contract item별로 완성도 계산 const contractStats: VendorContractCompletionStats[] = []; - + for (const item of contractItemsInfo) { + console.log(`getVendorAllContractsCompletionSummary - Processing contract item: ${item.contractItemId} for vendor: ${vendorId}`); const contractCompletion = await calculateVendorContractCompletion( - vendorId, + vendorId, item.contractItemId ); - + if (contractCompletion) { + console.log(`getVendorAllContractsCompletionSummary - Contract completion for item ${item.contractItemId}:`, { + totalRequiredFields: contractCompletion.totalRequiredFields, + totalFilledFields: contractCompletion.totalFilledFields, + totalForms: contractCompletion.totalForms + }); contractStats.push(contractCompletion); + } else { + console.log(`getVendorAllContractsCompletionSummary - No contract completion for item: ${item.contractItemId}`); } } @@ -532,7 +549,10 @@ export async function getVendorAllContractsCompletionSummary( const totalFilledFields = contractStats.reduce((sum, stat) => sum + stat.totalFilledFields, 0); const totalEmptyFields = totalRequiredFields - totalFilledFields; const totalForms = contractStats.reduce((sum, stat) => sum + stat.totalForms, 0); - + const totalTags = contractStats.reduce((sum, stat) => + sum + stat.forms.reduce((formSum, form) => formSum + form.tagCount, 0), 0 + ); + const overallCompletionPercentage = totalRequiredFields > 0 ? Math.round((totalFilledFields / totalRequiredFields) * 100) : 100; @@ -581,6 +601,7 @@ export async function getVendorAllContractsCompletionSummary( contracts: contractStats, totalContracts: contractStats.length, totalForms, + totalTags, totalRequiredFields, totalFilledFields, totalEmptyFields, @@ -614,7 +635,7 @@ export async function getAllProjectsVendorCompletionSummary(): Promise<{ // 2. 각 프로젝트별로 form들의 완성도 조회 const projectSummaries: ProjectVendorCompletionSummary[] = []; - + for (const project of allProjects) { // 해당 프로젝트의 모든 form codes 조회 const formCodes = await db @@ -626,7 +647,7 @@ export async function getAllProjectsVendorCompletionSummary(): Promise<{ // 각 form에 대한 완성도 조회 후 통합 const allVendorStats: VendorFormCompletionStats[] = []; - + for (const { formCode } of formCodes) { const summary = await getProjectVendorCompletionSummary(project.id, formCode); if (summary) { @@ -653,8 +674,8 @@ export async function getAllProjectsVendorCompletionSummary(): Promise<{ // 3. 전체 평균 계산 const overallAverageCompletion = projectSummaries.length > 0 ? Math.round( - projectSummaries.reduce((sum, proj) => sum + proj.averageCompletionPercentage, 0) / projectSummaries.length - ) + projectSummaries.reduce((sum, proj) => sum + proj.averageCompletionPercentage, 0) / projectSummaries.length + ) : 0; return { @@ -673,6 +694,227 @@ export async function getAllProjectsVendorCompletionSummary(): Promise<{ } } +/** + * 특정 벤더의 필드 계산 상세 정보를 디버깅용으로 반환 + */ +export async function debugVendorFieldCalculation(vendorId: number): Promise<{ + vendorId: number; + vendorName: string; + debugInfo: { + contracts: Array<{ + contractId: number; + contractItemId: number; + projectName: string; + forms: Array<{ + formCode: string; + formName: string; + tags: Array<{ + tagNo: string; + editableFields: string[]; + requiredFieldsCount: number; + filledFieldsCount: number; + fieldDetails: Array<{ + fieldKey: string; + fieldValue: unknown; + isEmpty: boolean; + }>; + }>; + totalRequiredFields: number; + totalFilledFields: number; + }>; + totalRequiredFields: number; + totalFilledFields: number; + }>; + grandTotal: { + totalRequiredFields: number; + totalFilledFields: number; + totalEmptyFields: number; + completionPercentage: number; + }; + }; +} | null> { + try { + // 1. 벤더 정보 조회 + const vendorInfo = await db + .select({ + id: vendors.id, + vendorName: vendors.vendorName + }) + .from(vendors) + .where(eq(vendors.id, vendorId)) + .limit(1); + + if (vendorInfo.length === 0) { + console.warn(`No vendor found with ID: ${vendorId}`); + return null; + } + + const vendor = vendorInfo[0]; + + // 2. 해당 벤더의 모든 contract items 조회 + const contractItemsInfo = await db + .select({ + contractId: contracts.id, + contractItemId: contractItems.id, + projectId: projects.id, + projectCode: projects.code, + projectName: projects.name, + itemId: contractItems.itemId, + description: contractItems.description + }) + .from(contractItems) + .innerJoin(contracts, eq(contractItems.contractId, contracts.id)) + .innerJoin(projects, eq(contracts.projectId, projects.id)) + .where(eq(contracts.vendorId, vendorId)); + + const debugContracts = []; + + for (const item of contractItemsInfo) { + // 3. 해당 contract item과 연관된 모든 form codes 조회 + const formCodes = await db + .selectDistinct({ + formCode: formEntries.formCode + }) + .from(formEntries) + .where(eq(formEntries.contractItemId, item.contractItemId)); + + const debugForms = []; + let contractTotalRequired = 0; + let contractTotalFilled = 0; + + for (const { formCode } of formCodes) { + // 4. Form 메타데이터 조회 + const metaRows = await db + .select() + .from(formMetas) + .where(eq(formMetas.formCode, formCode)) + .orderBy(desc(formMetas.updatedAt)) + .limit(1); + + const meta = metaRows[0]; + if (!meta) { + console.log(`No form meta found for formCode: ${formCode}, projectId: ${item.projectId}`); + continue; + } + + console.log(`Found form meta for formCode: ${formCode}, projectId: ${item.projectId}, columns type: ${typeof meta.columns}, isArray: ${Array.isArray(meta.columns)}`); + + // 5. Form 실제 데이터 조회 + const entryRows = await db + .select() + .from(formEntries) + .where( + and( + eq(formEntries.formCode, formCode), + eq(formEntries.contractItemId, item.contractItemId) + ) + ) + .orderBy(desc(formEntries.updatedAt)) + .limit(1); + + const entry = entryRows[0]; + if (!entry || !Array.isArray(entry.data)) continue; + + // 6. 컬럼 정의에서 벤더가 편집 가능한 필드 필터링 + const columns = meta.columns as DataTableColumnJSON[]; + const excludeKeys = ['BF_TAG_NO', 'TAG_TYPE_ID', 'PIC_NO', 'status']; + const editableColumns = columns.filter(col => + !excludeKeys.includes(col.key) && isFieldEditableByVendor(col) + ); + + const debugTags = []; + let formTotalRequired = 0; + let formTotalFilled = 0; + + const formData = entry.data as Array>; + + for (const rowData of formData) { + const tagNo = rowData.TAG_NO as string; + if (!tagNo) continue; + + // 직접 editableColumns 사용 (getEditableFieldsForTag 대신) + const actualEditableFields = editableColumns; + + const requiredFieldsCount = actualEditableFields.length; + let filledFieldsCount = 0; + + const fieldDetails = []; + // 각 편집 가능한 필드의 값 확인 + for (const column of actualEditableFields) { + const value = rowData[column.key]; + const isEmpty = isEmptyValue(value); + if (!isEmpty) { + filledFieldsCount++; + } + fieldDetails.push({ + fieldKey: column.key, + fieldValue: value, + isEmpty + }); + } + + debugTags.push({ + tagNo, + editableFields: actualEditableFields.map(col => col.key), + requiredFieldsCount, + filledFieldsCount, + fieldDetails + }); + + formTotalRequired += requiredFieldsCount; + formTotalFilled += filledFieldsCount; + } + + debugForms.push({ + formCode, + formName: meta.formName, + tags: debugTags, + totalRequiredFields: formTotalRequired, + totalFilledFields: formTotalFilled + }); + + contractTotalRequired += formTotalRequired; + contractTotalFilled += formTotalFilled; + } + + debugContracts.push({ + contractId: item.contractId, + contractItemId: item.contractItemId, + projectName: item.projectName, + forms: debugForms, + totalRequiredFields: contractTotalRequired, + totalFilledFields: contractTotalFilled + }); + } + + // 전체 합계 계산 + const grandTotalRequired = debugContracts.reduce((sum, contract) => sum + contract.totalRequiredFields, 0); + const grandTotalFilled = debugContracts.reduce((sum, contract) => sum + contract.totalFilledFields, 0); + const grandTotalEmpty = grandTotalRequired - grandTotalFilled; + const grandCompletionPercentage = grandTotalRequired > 0 + ? Math.round((grandTotalFilled / grandTotalRequired) * 100) + : 100; + + return { + vendorId: vendor.id, + vendorName: vendor.vendorName, + debugInfo: { + contracts: debugContracts, + grandTotal: { + totalRequiredFields: grandTotalRequired, + totalFilledFields: grandTotalFilled, + totalEmptyFields: grandTotalEmpty, + completionPercentage: grandCompletionPercentage + } + } + }; + + } catch (error) { + console.error(`Error debugging vendor field calculation:`, error); + return null; + } +} + /** * 모든 벤더들의 전체 계약 완성도 요약 (관리자용) */ @@ -704,24 +946,33 @@ export async function getAllVendorsContractsCompletionSummary(): Promise<{ // 2. 각 벤더별로 완성도 계산 const vendorSummaries: VendorAllContractsCompletionSummary[] = []; - + for (const vendor of vendorsWithContracts) { + console.log(`getAllVendorsContractsCompletionSummary - Processing vendor: ${vendor.vendorId} (${vendor.vendorName})`); const summary = await getVendorAllContractsCompletionSummary(vendor.vendorId); if (summary) { + console.log(`getAllVendorsContractsCompletionSummary - Vendor ${vendor.vendorId} summary:`, { + totalRequiredFields: summary.totalRequiredFields, + totalFilledFields: summary.totalFilledFields, + totalTags: summary.totalTags, + totalForms: summary.totalForms + }); vendorSummaries.push(summary); + } else { + console.log(`getAllVendorsContractsCompletionSummary - No summary for vendor: ${vendor.vendorId}`); } } // 3. 전체 평균 계산 const overallAverageCompletion = vendorSummaries.length > 0 ? Math.round( - vendorSummaries.reduce((sum, vendor) => sum + vendor.overallCompletionPercentage, 0) / vendorSummaries.length - ) + vendorSummaries.reduce((sum, vendor) => sum + vendor.overallCompletionPercentage, 0) / vendorSummaries.length + ) : 0; // 4. 상위/하위 성과 벤더 추출 (상위 5개, 하위 5개) const sortedVendors = [...vendorSummaries].sort((a, b) => b.overallCompletionPercentage - a.overallCompletionPercentage); - + const topPerformingVendors = sortedVendors.slice(0, 5).map(vendor => ({ vendorId: vendor.vendorId, vendorName: vendor.vendorName, @@ -753,3 +1004,4 @@ export async function getAllVendorsContractsCompletionSummary(): Promise<{ }; } } + -- cgit v1.2.3