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
|
// app/api/stage-submissions/bulk-upload/route.ts
import { NextRequest, NextResponse } from "next/server"
import { getServerSession } from "next-auth/next"
import { authOptions } from "@/app/api/auth/[...nextauth]/route"
import db from "@/db/db"
import { stageSubmissions, stageSubmissionAttachments, vendors } from "@/db/schema"
import { eq, and } from "drizzle-orm"
import {
extractRevisionNumber,
normalizeRevisionCode
} from "@/lib/vendor-document-list/plant/upload/util/filie-parser"
import { saveFileStream } from "@/lib/file-stroage"
export async function POST(request: NextRequest) {
const session = await getServerSession(authOptions)
if (!session?.user?.companyId) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
}
const vendorId = session.user.companyId
const userId = session.user.id
const vendor = await db.query.vendors.findFirst({
where: eq(vendors.id, session.user.companyId),
columns: {
vendorName: true,
vendorCode: true,
}
})
try {
const formData = await request.formData()
const files = formData.getAll('files') as File[]
// 메타데이터 파싱
const metadata: any[] = []
let index = 0
while (formData.has(`metadata[${index}]`)) {
const meta = formData.get(`metadata[${index}]`)
if (meta) {
metadata.push(JSON.parse(meta as string))
}
index++
}
if (files.length !== metadata.length) {
return NextResponse.json(
{ error: "Files and metadata count mismatch" },
{ status: 400 }
)
}
// 총 용량 체크 (10GB)
const totalSize = files.reduce((acc, file) => acc + file.size, 0)
const maxSize = 10 * 1024 * 1024 * 1024 // 10GB
if (totalSize > maxSize) {
return NextResponse.json(
{ error: `Total file size exceeds 10GB limit` },
{ status: 400 }
)
}
const uploadResults = []
// 각 파일 처리
for (let i = 0; i < files.length; i++) {
const file = files[i]
const meta = metadata[i]
try {
await db.transaction(async (tx) => {
// 리비전 정보 추출
const revisionNumber = extractRevisionNumber(meta.revision)
const revisionCode = normalizeRevisionCode(meta.revision) // ⭐ 추가
// 1. 해당 스테이지의 최신 submission 찾기
let submission = await tx
.select()
.from(stageSubmissions)
.where(
and(
eq(stageSubmissions.stageId, meta.stageId),
eq(stageSubmissions.documentId, meta.documentId)
)
)
.orderBy(stageSubmissions.revisionNumber)
.limit(1)
if (!submission[0] || submission[0].revisionNumber < revisionNumber) {
// 새 submission 생성
const [newSubmission] = await tx
.insert(stageSubmissions)
.values({
stageId: meta.stageId,
documentId: meta.documentId,
revisionNumber,
revisionCode, // ⭐ 원본 리비전 코드 저장
revisionType: revisionNumber === 0 ? "INITIAL" : "RESUBMISSION",
submissionStatus: "SUBMITTED",
submittedBy: session.user.name || session.user.email || "Unknown",
submittedByEmail: session.user.email,
vendorId,
vendorCode: vendor?.vendorCode || null,
totalFiles: 1,
totalFileSize: file.size,
submissionTitle: `Revision ${revisionCode} Submission`, // ⭐ 코드 사용
syncStatus: "pending",
lastModifiedBy: "EVCP",
})
.returning()
submission = [newSubmission]
} else if (submission[0].revisionNumber === revisionNumber) {
// 같은 리비전 업데이트
await tx
.update(stageSubmissions)
.set({
revisionCode, // ⭐ 코드 업데이트
totalFiles: submission[0].totalFiles + 1,
totalFileSize: submission[0].totalFileSize + file.size,
updatedAt: new Date(),
})
.where(eq(stageSubmissions.id, submission[0].id))
}
// 2. 파일 저장 (대용량 파일은 스트리밍)
const directory = `submissions/${meta.documentId}/${meta.stageId}/${revisionCode}` // ⭐ 디렉토리에 리비전 코드 포함
let saveResult
if (file.size > 100 * 1024 * 1024) { // 100MB 이상은 스트리밍
saveResult = await saveFileStream({
file,
directory,
originalName: meta.originalName,
userId: userId.toString()
})
} else {
const { saveFile } = await import("@/lib/file-stroage")
saveResult = await saveFile({
file,
directory,
originalName: meta.originalName,
userId: userId.toString()
})
}
if (!saveResult.success) {
throw new Error(saveResult.error || "File save failed")
}
// 3. attachment 레코드 생성
await tx.insert(stageSubmissionAttachments).values({
submissionId: submission[0].id,
fileName: saveResult.fileName!,
originalFileName: meta.originalName,
fileType: file.type,
fileExtension: meta.originalName.split('.').pop(),
fileSize: file.size,
storageType: "LOCAL",
storagePath: saveResult.filePath!,
storageUrl: saveResult.publicPath!,
mimeType: file.type,
uploadedBy: session.user.name || session.user.email || "Unknown",
status: "ACTIVE",
syncStatus: "pending",
lastModifiedBy: "EVCP",
})
})
uploadResults.push({
fileName: meta.originalName,
success: true
})
} catch (error) {
console.error(`Failed to upload ${meta.originalName}:`, error)
uploadResults.push({
fileName: meta.originalName,
success: false,
error: error instanceof Error ? error.message : "Upload failed"
})
}
}
const successCount = uploadResults.filter(r => r.success).length
return NextResponse.json({
success: true,
uploaded: successCount,
failed: uploadResults.length - successCount,
results: uploadResults
})
} catch (error) {
console.error("Bulk upload error:", error)
return NextResponse.json(
{ error: "Bulk upload failed" },
{ status: 500 }
)
}
}
|