// lib/cover/cover-service.ts import { PDFNet } from "@pdftron/pdfnet-node" import { promises as fs } from "fs" import path from "path" import db from "@/db/db" import { projectCoverTemplates, projects } from "@/db/schema" import { eq, and } from "drizzle-orm" /** * 프로젝트 정보 조회 */ async function getProjectInfo(projectId: number) { const [project] = await db .select({ id: projects.id, code: projects.code, name: projects.name, }) .from(projects) .where(eq(projects.id, projectId)) .limit(1) return project || null } /** * 활성 커버페이지 템플릿 조회 */ async function getActiveTemplate(projectId: number) { const [template] = await db .select() .from(projectCoverTemplates) .where( and( eq(projectCoverTemplates.projectId, projectId), eq(projectCoverTemplates.isActive, true) ) ) .limit(1) return template || null } /** * 웹 경로를 실제 파일 시스템 경로로 변환 * 환경변수 NAS_PATH를 기준으로 통일 */ function convertWebPathToFilePath(webPath: string): string { // 환경변수 기준 (개발: public, 프로덕션: NAS) const storagePath = process.env.NAS_PATH || path.join(process.cwd(), "public") // /api/files/... 형태인 경우 처리 if (webPath.startsWith("/api/files/")) { const requestedPath = webPath.substring("/api/files/".length) return path.join(storagePath, requestedPath) } // /uploads/... 형태 if (webPath.startsWith("/uploads/")) { return path.join(storagePath, webPath.substring(1)) } // /archive/... 형태 if (webPath.startsWith("/archive/")) { return path.join(storagePath, webPath.substring(1)) } // 이미 절대 경로인 경우 그대로 반환 if (path.isAbsolute(webPath)) { return webPath } // 상대 경로인 경우 return path.join(storagePath, webPath) } /** * 커버페이지 생성 (템플릿 변수 치환) * * @param projectId - 프로젝트 ID * @param docNumber - 문서 번호 * @returns 생성된 DOCX 파일 버퍼 */ export async function generateCoverPage( projectId: number, docNumber: string ): Promise<{ success: boolean buffer?: Buffer fileName?: string error?: string }> { try { console.log(`📄 커버페이지 생성 시작 - ProjectID: ${projectId}, DocNumber: ${docNumber}`) // 1. 프로젝트 정보 조회 const project = await getProjectInfo(projectId) if (!project) { return { success: false, error: "프로젝트를 찾을 수 없습니다" } } console.log(`✅ 프로젝트 정보 조회 완료:`, project) // 2. 활성 템플릿 조회 const template = await getActiveTemplate(projectId) if (!template) { return { success: false, error: "활성 커버페이지 템플릿을 찾을 수 없습니다" } } console.log(`✅ 템플릿 조회 완료:`, template.templateName) // 3. 템플릿 파일 경로 변환 console.log(`🔄 웹 경로: ${template.filePath}`) const templateFilePath = convertWebPathToFilePath(template.filePath) console.log(`📂 실제 파일 경로: ${templateFilePath}`) console.log(`💾 파일 저장소 경로: ${process.env.NAS_PATH || path.join(process.cwd(), "public")}`) // 4. 템플릿 파일 존재 확인 try { await fs.access(templateFilePath) console.log(`✅ 템플릿 파일 존재 확인 완료`) } catch { console.error(`❌ 템플릿 파일을 찾을 수 없음: ${templateFilePath}`) return { success: false, error: `템플릿 파일을 찾을 수 없습니다: ${template.filePath}` } } // 5. 템플릿 변수 데이터 준비 const templateData = { docNumber: docNumber, projectNumber: project.code, projectName: project.name, } console.log(`🔄 템플릿 변수:`, templateData) // 6. PDFTron으로 변수 치환 (서버 사이드) const result = await PDFNet.runWithCleanup(async () => { console.log("🔄 PDFTron 초기화 및 변수 치환 시작") // 템플릿 로드 및 변수 치환 console.log("📄 템플릿 로드 중...") const templateDoc = await PDFNet.Convert.createOfficeTemplateWithPath( templateFilePath ) console.log("🔄 변수 치환 중...") // JSON 형태로 변수 전달하여 치환 const resultDoc = await templateDoc.fillTemplateJson( JSON.stringify(templateData) ) console.log("💾 메모리 버퍼로 저장 중...") // 메모리에서 직접 버퍼 가져오기 (임시 파일 불필요) const buffer = await resultDoc.saveMemoryBuffer( PDFNet.SDFDoc.SaveOptions.e_linearized ) console.log("✅ 변수 치환 및 PDF 생성 완료") return { success: true, buffer: Buffer.from(buffer), } }, process.env.NEXT_PUBLIC_PDFTRON_SERVER_KEY) if (!result.success || !result.buffer) { return { success: false, error: "커버페이지 생성 중 오류가 발생했습니다" } } // 7. 파일명 생성 const fileName = `${docNumber}_cover.pdf` console.log(`✅ 커버페이지 생성 완료: ${fileName}`) return { success: true, buffer: result.buffer, fileName, } } catch (error) { console.error("❌ 커버페이지 생성 오류:", error) return { success: false, error: error instanceof Error ? error.message : "알 수 없는 오류" } } }