summaryrefslogtreecommitdiff
path: root/lib/cover/cover-service.ts
blob: eecc289b9d0ea1d877bb3bc648fcd6e295e6b9e2 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
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 : "알 수 없는 오류"
    }
  }
}