summaryrefslogtreecommitdiff
path: root/lib/cover
diff options
context:
space:
mode:
authorjoonhoekim <26rote@gmail.com>2025-11-03 18:46:35 +0900
committerjoonhoekim <26rote@gmail.com>2025-11-03 18:46:35 +0900
commit1393acc4b6675fd5eac65c6f1a9e399edfb2d44f (patch)
tree6610385198545277ed51c4616d315aa0800c07bc /lib/cover
parenta9c038e51f1cf508165e9d196ffe332f6ac54d74 (diff)
(김준회) SWP: 커버페이지 생성 API 오류 수정
Diffstat (limited to 'lib/cover')
-rw-r--r--lib/cover/cover-service.ts213
1 files changed, 213 insertions, 0 deletions
diff --git a/lib/cover/cover-service.ts b/lib/cover/cover-service.ts
new file mode 100644
index 00000000..eecc289b
--- /dev/null
+++ b/lib/cover/cover-service.ts
@@ -0,0 +1,213 @@
+// lib/cover/cover-service.ts
+import { PDFNet } from "@pdftron/pdfnet-node"
+import { promises as fs } from "fs"
+import path from "path"
+import { file as tmpFile } from "tmp-promise"
+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 초기화 및 변수 치환 시작")
+
+ // 임시 파일 생성 (결과물 저장용)
+ const { path: tempOutputPath, cleanup } = await tmpFile({
+ postfix: ".docx",
+ })
+
+ try {
+ // 템플릿 로드 및 변수 치환
+ console.log("📄 템플릿 로드 중...")
+ const templateDoc = await PDFNet.Convert.createOfficeTemplateWithPath(
+ templateFilePath
+ )
+
+ console.log("🔄 변수 치환 중...")
+ // JSON 형태로 변수 전달하여 치환
+ const resultDoc = await templateDoc.fillTemplateJson(
+ JSON.stringify(templateData)
+ )
+
+ console.log("💾 결과 파일 저장 중...")
+ // 임시 파일로 저장
+ await resultDoc.save(tempOutputPath, PDFNet.SDFDoc.SaveOptions.e_linearized)
+
+ console.log("✅ 변수 치환 완료")
+
+ // 파일 읽기
+ const buffer = await fs.readFile(tempOutputPath)
+
+ return {
+ success: true,
+ buffer: Buffer.from(buffer),
+ }
+ } finally {
+ // 임시 파일 정리
+ await cleanup()
+ }
+ }, process.env.NEXT_PUBLIC_PDFTRON_SERVER_KEY)
+
+ if (!result.success || !result.buffer) {
+ return {
+ success: false,
+ error: "커버페이지 생성 중 오류가 발생했습니다"
+ }
+ }
+
+ // 7. 파일명 생성
+ const fileName = `${docNumber}_cover.docx`
+
+ console.log(`✅ 커버페이지 생성 완료: ${fileName}`)
+
+ return {
+ success: true,
+ buffer: result.buffer,
+ fileName,
+ }
+
+ } catch (error) {
+ console.error("❌ 커버페이지 생성 오류:", error)
+ return {
+ success: false,
+ error: error instanceof Error ? error.message : "알 수 없는 오류"
+ }
+ }
+}
+