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
|
// 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 : "알 수 없는 오류"
}
}
}
|