'use server' import { revalidateTag, unstable_cache } from "next/cache" import { and, desc, asc, eq, or, ilike, count, max , inArray, isNotNull, notInArray} from "drizzle-orm" import db from "@/db/db" import { GtcClauseTreeView, gtcClauses, gtcDocuments, gtcDocumentsView, gtcVendorDocuments, type GtcDocument, type GtcDocumentWithRelations } from "@/db/schema/gtc" import { projects } from "@/db/schema/projects" import { users } from "@/db/schema/users" import { vendors } from "@/db/schema/vendors" import { filterColumns } from "@/lib/filter-columns" import type { GetGtcDocumentsSchema, CreateGtcDocumentSchema, UpdateGtcDocumentSchema, CreateNewRevisionSchema, CloneGtcDocumentSchema } from "./validations" import { getServerSession } from "next-auth/next" import { authOptions } from "@/app/api/auth/[...nextauth]/route" /** * 프로젝트 존재 여부 확인 */ export async function checkProjectExists(projectId: number): Promise { const result = await db .select({ id: projects.id }) .from(projects) .where(eq(projects.id, projectId)) .limit(1) return result.length > 0 } /** * GTC 문서 개수 조회 */ async function countGtcDocuments(tx: any, where: any) { const result = await tx .select({ count: count() }) .from(gtcDocumentsView) .where(where) return result[0]?.count ?? 0 } /** * GTC 문서 목록 조회 (필터링, 정렬, 페이징 지원) */ export async function getGtcDocuments(input: GetGtcDocumentsSchema) { return unstable_cache( async () => { try { const offset = (input.page - 1) * input.perPage // (1) advancedWhere - 고급 필터 const customColumnMapping = { "project.name": { table: gtcDocumentsView, column: "projectName" }, "createdBy.name": { table: gtcDocumentsView, column: "createdByName" }, "updatedBy.name": { table: gtcDocumentsView, column: "updatedByName" }, } const advancedWhere = filterColumns({ table: gtcDocumentsView, filters: input.filters, joinOperator: input.joinOperator, customColumnMapping, }) // (2) globalWhere - 전역 검색 let globalWhere if (input.search) { const s = `%${input.search}%` globalWhere = or( ilike(gtcDocumentsView.editReason, s), ilike(gtcDocumentsView.projectName, s), ilike(gtcDocumentsView.projectCode, s) ) } // (4) 최종 where 조건 const finalWhere = and( eq(gtcDocumentsView.isActive, true), // 활성 상태인 문서만 조회 advancedWhere, globalWhere, ) // (5) 정렬 const orderBy = input.sort.length > 0 ? input.sort.map((item) => { // 중첩 필드 매핑 처리 let column: any if (item.id === 'project.name' || item.id === 'projectName') { column = gtcDocumentsView.projectName } else if (item.id === 'createdBy.name' || item.id === 'createdByName') { column = gtcDocumentsView.createdByName } else if (item.id === 'updatedBy.name' || item.id === 'updatedByName') { column = gtcDocumentsView.updatedByName } else { column = (gtcDocumentsView as any)[item.id] } return item.desc ? desc(column) : asc(column) }) : [desc(gtcDocumentsView.updatedAt)] // (6) 데이터 조회 const { data, total } = await db.transaction(async (tx) => { const data = await db .select() .from(gtcDocumentsView) .where(finalWhere) .orderBy(...orderBy) .limit(input.perPage) .offset(offset); const total = await countGtcDocuments(tx, finalWhere) return { data, total } }) const pageCount = Math.ceil(total / input.perPage) return { data, pageCount } } catch (err) { console.error("Error fetching GTC documents:", err) return { data: [], pageCount: 0 } } }, [JSON.stringify(input)], { revalidate: 3600, tags: ["gtc-documents"], } )() } // 성공한 ID들을 반환하는 버전 export async function deleteGtcDocuments( ids: number[], updatedById: number ): Promise { if (ids.length === 0) { return []; } const updated = await db .update(gtcDocuments) .set({ isActive: false, updatedById, updatedAt: new Date(), }) .where(inArray(gtcDocuments.id, ids)) .returning({ id: gtcDocuments.id }); return updated.map(doc => doc.id); } /** * 다음 리비전 번호 조회 */ export async function getNextRevision(type: "standard" | "project", projectId?: number): Promise { const where = projectId ? and(eq(gtcDocuments.type, type), eq(gtcDocuments.projectId, projectId)) : and(eq(gtcDocuments.type, type), eq(gtcDocuments.projectId, null)) const result = await db .select({ maxRevision: max(gtcDocuments.revision) }) .from(gtcDocuments) .where(where) return (result[0]?.maxRevision ?? -1) + 1 } /** * GTC 문서 생성 */ export async function createGtcDocument( data: CreateGtcDocumentSchema & { createdById: number } ): Promise { // 리비전 번호가 없는 경우 자동 생성 if (!data.revision && data.revision !== 0) { data.revision = await getNextRevision(data.type, data.projectId || undefined) } const [newDocument] = await db .insert(gtcDocuments) .values({ ...data, updatedById: data.createdById, // 생성시에는 생성자와 수정자가 동일 }) .returning() revalidateTag("gtc-documents") return newDocument } /** * GTC 문서 업데이트 */ export async function updateGtcDocument( id: number, data: UpdateGtcDocumentSchema & { updatedById: number } ): Promise { console.log(data, "data") const [updatedDocument] = await db .update(gtcDocuments) .set({ ...data, updatedAt: new Date(), }) .where(eq(gtcDocuments.id, id)) .returning() revalidateTag("gtc-documents") return updatedDocument || null } /** * 새 리비전 생성 */ export async function createNewRevision( originalId: number, data: CreateNewRevisionSchema & { createdById: number } ): Promise { // 원본 문서 조회 const original = await getGtcDocumentById(originalId) if (!original) { throw new Error("Original document not found") } // 다음 리비전 번호 계산 const nextRevision = await getNextRevision( original.type, original.projectId || undefined ) // 새 리비전 생성 const [newRevision] = await db .insert(gtcDocuments) .values({ type: original.type, projectId: original.projectId, revision: nextRevision, editReason: data.editReason, createdById: data.createdById, updatedById: data.createdById, }) .returning() return newRevision } /** * GTC 문서 삭제 (소프트 삭제) */ export async function deleteGtcDocument( id: number, updatedById: number ): Promise { const [updated] = await db .update(gtcDocuments) .set({ isActive: false, updatedById, updatedAt: new Date(), }) .where(eq(gtcDocuments.id, id)) .returning() return !!updated } // 타입 정의 export type ProjectForFilter = { id: number code: string name: string } export type UserForFilter = { id: number name: string email: string } /** * 프로젝트 목록 조회 (필터용) */ export async function getProjectsForFilter(): Promise { return await db .select({ id: projects.id, code: projects.code, name: projects.name, }) .from(projects) .orderBy(projects.name) } /** * 프로젝트 목록 조회 (선택용) */ export async function getProjectsForSelect(): Promise { return await db .select({ id: projects.id, code: projects.code, name: projects.name, }) .from(projects) .orderBy(projects.name) } /** * 사용자 목록 조회 (필터용) */ export async function getUsersForFilter(): Promise { return await db .select({ id: users.id, name: users.name, email: users.email, }) .from(users) .where(eq(users.isActive, true)) // 활성 사용자만 .orderBy(users.name) } /** * GTC 문서 단일 조회 */ export async function getGtcDocumentById(id: number) { return unstable_cache( async () => { try { const [document] = await db .select() .from(gtcDocumentsView) .where(eq(gtcDocumentsView.id, id)) .limit(1) return document || null } catch (err) { console.error("Error fetching GTC document by ID:", err) return null } }, [`gtc-document-${id}`], { revalidate: 3600, tags: [`gtc-document-${id}`, "gtc-documents"], } )() } // 복제 함수 export async function cloneGtcDocument( data: CloneGtcDocumentSchema & { createdById: number } ): Promise<{ data?: GtcDocument; error?: string }> { try { return await db.transaction(async (tx) => { // 1. 원본 문서 조회 const [sourceDocument] = await tx .select() .from(gtcDocuments) .where(eq(gtcDocuments.id, data.sourceDocumentId)) if (!sourceDocument) { throw new Error("원본 문서를 찾을 수 없습니다.") } // 2. 새로운 리비전 번호 계산 const nextRevision = await getNextRevision(data.type, data.projectId || undefined) // 3. 새 문서 생성 const [newDocument] = await tx .insert(gtcDocuments) .values({ type: data.type, projectId: data.projectId, title: data.title || sourceDocument.title, revision: nextRevision, fileName: sourceDocument.fileName, // 파일 정보도 복사 filePath: sourceDocument.filePath, fileSize: sourceDocument.fileSize, createdById: data.createdById, updatedById: data.createdById, editReason: data.editReason || `${sourceDocument.title || 'GTC 문서'} v${sourceDocument.revision}에서 복제`, isActive: true, }) .returning() // 4. 원본 문서의 모든 clauses 조회 const sourceClauses = await tx .select() .from(gtcClauses) .where(eq(gtcClauses.documentId, data.sourceDocumentId)) .orderBy(gtcClauses.sortOrder) // 5. clauses 복제 (ID 매핑을 위한 Map 생성) if (sourceClauses.length > 0) { const clauseIdMapping = new Map() // 첫 번째 pass: 모든 clauses를 복사하고 ID 매핑 생성 for (const sourceClause of sourceClauses) { const [newClause] = await tx .insert(gtcClauses) .values({ documentId: newDocument.id, parentId: null, // 첫 번째 pass에서는 null로 설정 itemNumber: sourceClause.itemNumber, category: sourceClause.category, subtitle: sourceClause.subtitle, content: sourceClause.content, sortOrder: sourceClause.sortOrder, depth: sourceClause.depth, fullPath: sourceClause.fullPath, images: sourceClause.images, isActive: sourceClause.isActive, createdById: data.createdById, updatedById: data.createdById, editReason: data.editReason || "문서 복제", }) .returning() clauseIdMapping.set(sourceClause.id, newClause.id) } // 두 번째 pass: parentId 관계 설정 for (const sourceClause of sourceClauses) { if (sourceClause.parentId) { const newParentId = clauseIdMapping.get(sourceClause.parentId) const newClauseId = clauseIdMapping.get(sourceClause.id) if (newParentId && newClauseId) { await tx .update(gtcClauses) .set({ parentId: newParentId }) .where(eq(gtcClauses.id, newClauseId)) } } } } revalidateTag("gtc-documents") revalidateTag(`gtc-clauses-${newDocument.id}`) return { data: newDocument } }) } catch (error) { console.error("Error cloning GTC document:", error) return { error: error instanceof Error ? error.message : "문서 복제 중 오류가 발생했습니다." } } } // 새 함수: GTC 문서가 없는 프로젝트만 조회 export async function getAvailableProjectsForGtc(): Promise { // 이미 GTC 문서가 있는 프로젝트 ID들 조회 const projectsWithGtc = await db .selectDistinct({ projectId: gtcDocuments.projectId }) .from(gtcDocuments) .where(isNotNull(gtcDocuments.projectId)) console.log(projectsWithGtc,"projectsWithGtc") const usedProjectIds = projectsWithGtc .map(row => row.projectId) .filter((id): id is number => id !== null) // GTC 문서가 없는 프로젝트들만 반환 if (usedProjectIds.length === 0) { // 사용된 프로젝트가 없으면 모든 프로젝트 반환 return await getProjectsForSelect() } return await db .select({ id: projects.id, code: projects.code, name: projects.name, }) .from(projects) .where(notInArray(projects.id, usedProjectIds)) .orderBy(projects.name) } // 복제시 사용할 함수: 특정 프로젝트는 제외하고 조회 export async function getAvailableProjectsForGtcExcluding(excludeProjectId?: number): Promise { // 이미 GTC 문서가 있는 프로젝트 ID들 조회 const projectsWithGtc = await db .selectDistinct({ projectId: gtcDocuments.projectId }) .from(gtcDocuments) .where(isNotNull(gtcDocuments.projectId)) let usedProjectIds = projectsWithGtc .map(row => row.projectId) .filter((id): id is number => id !== null) // 제외할 프로젝트 ID가 있다면 사용된 ID 목록에서 제거 (복제시 원본 프로젝트는 선택 가능) if (excludeProjectId) { usedProjectIds = usedProjectIds.filter(id => id !== excludeProjectId) } // GTC 문서가 없는 프로젝트들만 반환 if (usedProjectIds.length === 0) { return await getProjectsForSelect() } return await db .select({ id: projects.id, code: projects.code, name: projects.name, }) .from(projects) .where(notInArray(projects.id, usedProjectIds)) .orderBy(projects.name) } export async function hasStandardGtcDocument(): Promise { const result = await db .select({ id: gtcDocuments.id }) .from(gtcDocuments) .where(eq(gtcDocuments.type, "standard")) .limit(1) return result.length > 0 } export async function getAllGtcClausesForExport(documentId: number): Promise { try { // 실제 데이터베이스 쿼리 로직을 여기에 구현 // 예시: 문서 ID에 해당하는 모든 조항을 트리 뷰 형태로 가져오기 const clauses = await db .select() .from(gtcClauses) .where(eq(gtcClauses.documentId, documentId)) // 여기에 필요한 JOIN, ORDER BY 등을 추가 .orderBy(gtcClauses.sortOrder) // GtcClauseTreeView 형태로 변환하여 반환 return clauses.map((clause) => ({ ...clause, // 필요한 추가 필드들을 여기에 매핑 })) as GtcClauseTreeView[] } catch (error) { console.error("Failed to fetch GTC clauses for export:", error) throw new Error("Failed to fetch GTC clauses for export") } } interface ImportGtcClauseData { itemNumber: string subtitle: string content?: string category?: string sortOrder?: number parentId?: number | null depth?: number fullPath?: string images?: any[] isActive?: boolean editReason?: string } interface ImportResult { success: boolean importedCount: number errors: string[] duplicates: string[] } /** * Excel에서 가져온 GTC 조항들을 데이터베이스에 저장 */ export async function importGtcClausesFromExcel( documentId: number, data: Partial[], ): Promise { const result: ImportResult = { success: false, importedCount: 0, errors: [], duplicates: [] } const session = await getServerSession(authOptions) if (!session?.user?.id) { throw new Error("인증이 필요합니다.") } const userId = Number(session.user.id) try { // 데이터 검증 및 변환 const validData: ImportGtcClauseData[] = [] for (let i = 0; i < data.length; i++) { const item = data[i] const rowNumber = i + 1 // 필수 필드 검증 if (!item.itemNumber || typeof item.itemNumber !== 'string' || item.itemNumber.trim() === '') { result.errors.push(`${rowNumber}행: 채번은 필수 항목입니다.`) continue } if (!item.subtitle || typeof item.subtitle !== 'string' || item.subtitle.trim() === '') { result.errors.push(`${rowNumber}행: 소제목은 필수 항목입니다.`) continue } // 중복 채번 체크 (같은 문서 내에서, 같은 부모 하에서) const existingClause = await db .select({ id: gtcClauses.id, itemNumber: gtcClauses.itemNumber }) .from(gtcClauses) .where( and( eq(gtcClauses.documentId, documentId), eq(gtcClauses.parentId, item.parentId || null), eq(gtcClauses.itemNumber, item.itemNumber.trim()) ) ) .limit(1) if (existingClause.length > 0) { result.duplicates.push(`${rowNumber}행: 채번 "${item.itemNumber}"는 이미 존재합니다.`) continue } // 상위 조항 ID 검증 (제공된 경우) if (item.parentId && typeof item.parentId === 'number') { const parentExists = await db .select({ id: gtcClauses.id }) .from(gtcClauses) .where( and( eq(gtcClauses.documentId, documentId), eq(gtcClauses.id, item.parentId) ) ) .limit(1) if (parentExists.length === 0) { result.errors.push(`${rowNumber}행: 상위 조항 ID ${item.parentId}를 찾을 수 없습니다.`) continue } } // sortOrder를 decimal로 변환 let sortOrder = 0 if (item.sortOrder !== undefined) { if (typeof item.sortOrder === 'number') { sortOrder = item.sortOrder } else if (typeof item.sortOrder === 'string') { const parsed = parseFloat(item.sortOrder) if (!isNaN(parsed)) { sortOrder = parsed } } } else { // 기본값: (현재 인덱스 + 1) * 10 sortOrder = (validData.length + 1) * 10 } // depth 계산 (parentId가 있으면 부모의 depth + 1, 아니면 0) let depth = 0 if (item.parentId) { const parentClause = await db .select({ depth: gtcClauses.depth }) .from(gtcClauses) .where(eq(gtcClauses.id, item.parentId)) .limit(1) if (parentClause.length > 0) { depth = (parentClause[0].depth || 0) + 1 } } // 유효한 데이터 추가 validData.push({ itemNumber: item.itemNumber.trim(), subtitle: item.subtitle.trim(), content: item.content?.toString().trim() || null, category: item.category?.toString().trim() || null, sortOrder: sortOrder, parentId: item.parentId || null, isActive: typeof item.isActive === 'boolean' ? item.isActive : true, editReason: item.editReason?.toString().trim() || null, }) } // 오류가 있거나 중복이 있으면 가져오기 중단 if (result.errors.length > 0 || result.duplicates.length > 0) { return result } // 트랜잭션으로 데이터 저장 await db.transaction(async (tx) => { for (const clauseData of validData) { try { // depth 재계산 (저장 시점에서) let finalDepth = 0 if (clauseData.parentId) { const parentClause = await tx .select({ depth: gtcClauses.depth }) .from(gtcClauses) .where(eq(gtcClauses.id, clauseData.parentId)) .limit(1) if (parentClause.length > 0) { finalDepth = (parentClause[0].depth || 0) + 1 } } await tx.insert(gtcClauses).values({ documentId, parentId: clauseData.parentId, itemNumber: clauseData.itemNumber, category: clauseData.category, subtitle: clauseData.subtitle, content: clauseData.content, sortOrder:clauseData.sortOrder? clauseData.sortOrder.toString() :"0", // decimal로 저장 depth: finalDepth, fullPath: null, // 추후 별도 로직에서 생성 images: null, // Excel 가져오기에서는 이미지 제외 isActive: clauseData.isActive, createdById: userId, updatedById: userId, editReason: clauseData.editReason, createdAt: new Date(), updatedAt: new Date(), }) result.importedCount++ } catch (insertError) { result.errors.push(`"${clauseData.subtitle}" 저장 중 오류: ${insertError instanceof Error ? insertError.message : '알 수 없는 오류'}`) } } }) result.success = result.importedCount > 0 && result.errors.length === 0 return result } catch (error) { result.errors.push(`가져오기 처리 중 오류가 발생했습니다: ${error instanceof Error ? error.message : '알 수 없는 오류'}`) return result } } /** * Excel 가져오기 전 데이터 유효성 검사 */ export async function validateGtcClausesImport( documentId: number, data: Partial[] ): Promise<{ valid: boolean errors: string[] warnings: string[] summary: { totalRows: number validRows: number duplicateCount: number errorCount: number } }> { const errors: string[] = [] const warnings: string[] = [] let validRows = 0 let duplicateCount = 0 try { // 기존 채번들 가져오기 (중복 체크용) const existingItems = await db .select({ itemNumber: gtcClauses.itemNumber, parentId: gtcClauses.parentId }) .from(gtcClauses) .where(eq(gtcClauses.documentId, documentId)) // 채번-부모ID 조합으로 중복 체크 세트 생성 const existingItemSet = new Set( existingItems.map(item => `${item.parentId || 'null'}:${item.itemNumber.toLowerCase()}`) ) // 같은 파일 내에서의 중복 체크를 위한 세트 const currentFileItems = new Set() for (let i = 0; i < data.length; i++) { const item = data[i] const rowNumber = i + 1 let hasError = false // 필수 필드 검증 if (!item.itemNumber || typeof item.itemNumber !== 'string' || item.itemNumber.trim() === '') { errors.push(`${rowNumber}행: 채번은 필수 항목입니다.`) hasError = true } if (!item.subtitle || typeof item.subtitle !== 'string' || item.subtitle.trim() === '') { errors.push(`${rowNumber}행: 소제목은 필수 항목입니다.`) hasError = true } if (item.itemNumber && item.itemNumber.trim() !== '') { const itemKey = `${item.parentId || 'null'}:${item.itemNumber.toLowerCase()}` // DB에 이미 존재하는지 체크 if (existingItemSet.has(itemKey)) { warnings.push(`${rowNumber}행: 채번 "${item.itemNumber}"는 이미 존재합니다.`) duplicateCount++ } // 현재 파일 내에서 중복인지 체크 if (currentFileItems.has(itemKey)) { errors.push(`${rowNumber}행: 채번 "${item.itemNumber}"가 파일 내에서 중복됩니다.`) hasError = true } else { currentFileItems.add(itemKey) } } // 숫자 필드 검증 if (item.sortOrder !== undefined) { const sortOrderNum = typeof item.sortOrder === 'string' ? parseFloat(item.sortOrder) : item.sortOrder if (typeof sortOrderNum !== 'number' || isNaN(sortOrderNum) || sortOrderNum < 0) { warnings.push(`${rowNumber}행: 순서는 0 이상의 숫자여야 합니다.`) } } if (!hasError) { validRows++ } } return { valid: errors.length === 0, errors, warnings, summary: { totalRows: data.length, validRows, duplicateCount, errorCount: errors.length } } } catch (error) { errors.push(`유효성 검사 중 오류가 발생했습니다: ${error instanceof Error ? error.message : '알 수 없는 오류'}`) return { valid: false, errors, warnings, summary: { totalRows: data.length, validRows: 0, duplicateCount: 0, errorCount: errors.length } } } } /** * GTC 문서를 벤더별로 생성 */ export async function createGtcVendorDocuments({ baseDocumentId, vendorIds, createdById, documentTitle = "General GTC" }: { baseDocumentId: number vendorIds: number[] createdById: number documentTitle?: string }) { try { console.log(`🔍 [GTC] 표준 GTC 문서 생성 시작: baseDocumentId=${baseDocumentId}, title=${documentTitle}`) const results = [] for (let i = 0; i < vendorIds.length; i++) { const vendorId = vendorIds[i] try { console.log(`📄 [GTC] 벤더 ${i + 1}/${vendorIds.length} 표준 GTC 문서 생성: vendorId=${vendorId}`) const result = await db.insert(gtcVendorDocuments).values({ baseDocumentId, vendorId, name: documentTitle, reviewStatus: "draft", isActive: true, createdById, updatedById: createdById, }).returning() console.log(`✅ [GTC] 표준 GTC 문서 생성 성공: vendorId=${vendorId}, insertedId=${result[0].id}`) results.push(result[0]) } catch (vendorError) { console.error(`❌ [GTC] 표준 GTC 문서 생성 실패: vendorId=${vendorId}`, vendorError) continue } } console.log(`🎉 [GTC] 표준 GTC 문서 생성 완료: ${results.length}/${vendorIds.length}개 성공`) return { success: true, data: results, count: results.length } } catch (error) { console.error("❌ [GTC] 표준 GTC 벤더 문서 생성 오류:", error) return { success: false, error: error instanceof Error ? error.message : "알 수 없는 오류" } } } /** * 프로젝트별 GTC 문서를 벤더별로 생성 */ export async function createProjectGtcVendorDocuments({ projectCode, vendorIds, createdById, documentTitle }: { projectCode: string vendorIds: number[] createdById: number documentTitle?: string }) { try { console.log(`🔍 [GTC] 프로젝트별 GTC 문서 생성 시작: ${projectCode}`) // 1. 프로젝트 코드로 GTC 문서 찾기 console.log(`🔍 [GTC] GTC 문서 검색 쿼리 실행: projectCode=${projectCode}`) // 1. 프로젝트 코드로 프로젝트 id 조회 const projectRow = await db .select({ id: projects.id }) .from(projects) .where(eq(projects.code, projectCode)) .limit(1); if (!projectRow || projectRow.length === 0) { console.log(`❌ [GTC] 프로젝트 코드에 해당하는 프로젝트를 찾을 수 없음: ${projectCode}`); return { success: false, error: `프로젝트 코드 "${projectCode}"에 해당하는 프로젝트를 찾을 수 없습니다.` } } const projectId = projectRow[0].id; console.log(projectId,"projectId") // 2. 해당 프로젝트 id로 GTC 문서 조회 const gtcDocument = await db .select({ id: gtcDocuments.id, title: gtcDocuments.title }) .from(gtcDocuments) .where( and( eq(gtcDocuments.type, "project"), eq(gtcDocuments.isActive, true), eq(gtcDocuments.projectId, projectId) ) ) .orderBy(desc(gtcDocuments.revision)) .limit(1); console.log(`🔍 [GTC] GTC 문서 검색 결과:`, gtcDocument) if (!gtcDocument || gtcDocument.length === 0) { console.log(`❌ [GTC] 프로젝트 GTC 문서를 찾을 수 없음: ${projectCode}`) return { success: false, error: `프로젝트 코드 "${projectCode}"에 해당하는 GTC 문서를 찾을 수 없습니다.` } } const baseDocumentId = gtcDocument[0].id const finalDocumentTitle = documentTitle || `${projectCode} GTC` console.log(`✅ [GTC] GTC 문서 찾음: id=${baseDocumentId}, title=${finalDocumentTitle}`) // 2. 각 벤더별로 GTC 벤더 문서 생성 console.log(`📝 [GTC] 벤더별 GTC 문서 생성 시작: ${vendorIds.length}개 벤더`) const results = [] for (let i = 0; i < vendorIds.length; i++) { const vendorId = vendorIds[i] try { console.log(`📄 [GTC] 벤더 ${i + 1}/${vendorIds.length} GTC 문서 생성: vendorId=${vendorId}`) const result = await db.insert(gtcVendorDocuments).values({ baseDocumentId, vendorId, name: finalDocumentTitle, reviewStatus: "draft", isActive: true, createdById, updatedById: createdById, }).returning() console.log(`✅ [GTC] 벤더 GTC 문서 생성 성공: vendorId=${vendorId}, insertedId=${result[0].id}`) results.push(result[0]) } catch (vendorError) { console.error(`❌ [GTC] 벤더 GTC 문서 생성 실패: vendorId=${vendorId}`, vendorError) // 개별 벤더 실패는 전체 실패로 처리하지 않고 계속 진행 continue } } console.log(`🎉 [GTC] 프로젝트 GTC 문서 생성 완료: ${results.length}/${vendorIds.length}개 성공`) return { success: true, data: results, count: results.length } } catch (error) { console.error("❌ [GTC] 프로젝트 GTC 벤더 문서 생성 오류:", error) return { success: false, error: error instanceof Error ? error.message : "알 수 없는 오류" } } } /** * 표준 GTC 문서 ID 가져오기 (최신 리비전) */ export async function getStandardGtcDocumentId(): Promise<{ id: number; title: string } | null> { try { console.log(`🔍 [GTC-UTIL] 표준 GTC 문서 조회 시작`) const result = await db .select({ id: gtcDocuments.id, title: gtcDocuments.title, revision: gtcDocuments.revision }) .from(gtcDocuments) .where( and( eq(gtcDocuments.type, "standard"), eq(gtcDocuments.isActive, true) ) ) .orderBy(desc(gtcDocuments.revision)) .limit(1) console.log(`🔍 [GTC-UTIL] 표준 GTC 문서 조회 결과:`, result) if (result.length > 0) { const gtcDoc = { id: result[0].id, title: result[0].title || "General GTC" } console.log(`✅ [GTC-UTIL] 표준 GTC 문서 찾음:`, gtcDoc) return gtcDoc } else { console.log(`⚠️ [GTC-UTIL] 표준 GTC 문서를 찾을 수 없음`) return null } } catch (error) { console.error("❌ [GTC-UTIL] 표준 GTC 문서 ID 조회 오류:", error) return null } } /** * 프로젝트 코드로 GTC 문서 ID 가져오기 (최신 리비전) */ export async function getProjectGtcDocumentId(projectCode: string): Promise<{ id: number; title: string } | null> { try { console.log(`🔍 [GTC-UTIL] 프로젝트 GTC 문서 조회 시작: ${projectCode}`) const result = await db .select({ id: gtcDocuments.id, title: gtcDocuments.title, revision: gtcDocuments.revision, projectCode: projects.code }) .from(gtcDocuments) .leftJoin(projects, eq(gtcDocuments.projectId, projects.id)) .where( and( eq(gtcDocuments.type, "project"), eq(gtcDocuments.isActive, true), eq(projects.code, projectCode) ) ) .orderBy(desc(gtcDocuments.revision)) .limit(1) console.log(`🔍 [GTC-UTIL] 프로젝트 GTC 문서 조회 결과:`, result) if (result.length > 0) { const gtcDoc = { id: result[0].id, title: result[0].title || `${projectCode} GTC` } console.log(`✅ [GTC-UTIL] 프로젝트 GTC 문서 찾음:`, gtcDoc) return gtcDoc } else { console.log(`⚠️ [GTC-UTIL] 프로젝트 GTC 문서를 찾을 수 없음: ${projectCode}`) return null } } catch (error) { console.error("❌ [GTC-UTIL] 프로젝트 GTC 문서 ID 조회 오류:", error) return null } }