summaryrefslogtreecommitdiff
path: root/app/api/pdftron-comments/xfdf/route.ts
diff options
context:
space:
mode:
Diffstat (limited to 'app/api/pdftron-comments/xfdf/route.ts')
-rw-r--r--app/api/pdftron-comments/xfdf/route.ts362
1 files changed, 362 insertions, 0 deletions
diff --git a/app/api/pdftron-comments/xfdf/route.ts b/app/api/pdftron-comments/xfdf/route.ts
new file mode 100644
index 00000000..f2cd7b81
--- /dev/null
+++ b/app/api/pdftron-comments/xfdf/route.ts
@@ -0,0 +1,362 @@
+// app/api/pdftron-comments/xfdf/route.ts
+
+import { NextRequest, NextResponse } from "next/server"
+import db from "@/db/db"
+import { rfqLastTbePdftronComments } from "@/db/schema"
+import { eq, and, desc } from "drizzle-orm"
+import { getServerSession } from "next-auth"
+import { authOptions } from "@/app/api/auth/[...nextauth]/route"
+import { parseStringPromise } from "xml2js"
+import { revalidateTag } from "next/cache"
+
+// GET - XFDF 조회
+export async function GET(request: NextRequest) {
+ try {
+ const session = await getServerSession(authOptions)
+ if (!session?.user) {
+ return NextResponse.json({ error: "인증이 필요합니다." }, { status: 401 })
+ }
+
+ const searchParams = request.nextUrl.searchParams
+ const documentReviewId = searchParams.get('documentReviewId')
+
+ if (!documentReviewId) {
+ return NextResponse.json({ error: "documentReviewId is required" }, { status: 400 })
+ }
+
+ // 해당 문서의 코멘트 조회
+ const [comment] = await db
+ .select()
+ .from(rfqLastTbePdftronComments)
+ .where(
+ eq(rfqLastTbePdftronComments.documentReviewId, parseInt(documentReviewId))
+ )
+ .limit(1)
+
+ if (!comment) {
+ return NextResponse.json({ xfdfString: null })
+ }
+
+ // 권한 체크
+ const userId = typeof session.user.id === 'string' ? parseInt(session.user.id) : session.user.id
+ const isAdmin = (session.user as any).roles?.includes('admin') || false
+ const canEdit = comment.createdBy === userId || isAdmin
+
+ return NextResponse.json({
+ xfdfString: comment.xfdfString,
+ annotationData: comment.annotationData,
+ commentSummary: comment.commentSummary,
+ canEdit: canEdit,
+ createdBy: comment.createdBy,
+ createdByType: comment.createdByType,
+ lastModifiedBy: comment.lastModifiedBy,
+ updatedAt: comment.updatedAt
+ })
+ } catch (error) {
+ console.error("Failed to fetch XFDF:", error)
+ return NextResponse.json({ error: "Failed to fetch XFDF" }, { status: 500 })
+ }
+}
+
+// POST - XFDF 저장 (upsert 방식)
+export async function POST(request: NextRequest) {
+ try {
+ const session = await getServerSession(authOptions)
+ if (!session?.user) {
+ return NextResponse.json({ error: "인증이 필요합니다." }, { status: 401 })
+ }
+
+ const body = await request.json()
+ const {
+ documentReviewId,
+ sessionId,
+ pdftronDocumentId,
+ xfdfString,
+ commentSummary,
+ createdByType
+ } = body
+
+ // 필수 필드 검증
+ if (!documentReviewId || !pdftronDocumentId || !xfdfString) {
+ return NextResponse.json({
+ error: "Missing required fields"
+ }, { status: 400 })
+ }
+
+ const userId = typeof session.user.id === 'string' ? parseInt(session.user.id) : session.user.id
+ const isAdmin = (session.user as any).roles?.includes('admin') || false
+
+ // XFDF 파싱하여 annotation 데이터 추출
+ const annotationData = await parseXFDF(xfdfString)
+
+ // 트랜잭션으로 처리
+ const result = await db.transaction(async (tx) => {
+ // 기존 코멘트 확인
+ const [existing] = await tx
+ .select()
+ .from(rfqLastTbePdftronComments)
+ .where(
+ and(
+ eq(rfqLastTbePdftronComments.documentReviewId, parseInt(documentReviewId)),
+ eq(rfqLastTbePdftronComments.pdftronDocumentId, pdftronDocumentId)
+ )
+ )
+ .limit(1)
+
+ if (existing) {
+ // 권한 체크 - 다른 사용자의 annotation 수정 방지
+ if (!isAdmin) {
+ const currentAnnotations = existing.annotationData?.annotations || []
+ const newAnnotations = annotationData.annotations || []
+
+ // 다른 사용자가 만든 annotation이 수정/삭제되었는지 체크
+ for (const oldAnn of currentAnnotations) {
+ // 다른 사용자가 만든 annotation
+ if (oldAnn.customData?.createdBy && oldAnn.customData.createdBy !== userId) {
+ const newAnn = newAnnotations.find((n: any) => n.id === oldAnn.id)
+
+ // 삭제되었거나 수정되었으면 에러
+ if (!newAnn || JSON.stringify(newAnn) !== JSON.stringify(oldAnn)) {
+ throw new Error("You can only modify your own annotations")
+ }
+ }
+ }
+ }
+
+ // 기존 레코드 업데이트
+ const [updated] = await tx
+ .update(rfqLastTbePdftronComments)
+ .set({
+ xfdfString,
+ annotationData,
+ commentSummary,
+ lastModifiedBy: userId,
+ updatedAt: new Date()
+ })
+ .where(eq(rfqLastTbePdftronComments.id, existing.id))
+ .returning()
+
+ return updated
+ } else {
+ // 새 레코드 삽입
+ const [inserted] = await tx
+ .insert(rfqLastTbePdftronComments)
+ .values({
+ documentReviewId: parseInt(documentReviewId),
+ pdftronDocumentId,
+ xfdfString,
+ annotationData,
+ commentSummary,
+ createdBy: userId,
+ createdByType: createdByType || 'buyer',
+ lastModifiedBy: userId,
+ createdAt: new Date(),
+ updatedAt: new Date()
+ })
+ .returning()
+
+ return inserted
+ }
+ })
+
+ revalidateTag(`tbe-session-${sessionId}`)
+
+
+ return NextResponse.json(result)
+ } catch (error: any) {
+ console.error("Failed to save XFDF:", error)
+
+ if (error.message === "You can only modify your own annotations") {
+ return NextResponse.json({
+ error: "You can only modify your own annotations"
+ }, { status: 403 })
+ }
+
+ return NextResponse.json({ error: "Failed to save XFDF" }, { status: 500 })
+ }
+}
+
+// DELETE - XFDF 삭제
+export async function DELETE(request: NextRequest) {
+ try {
+ const session = await getServerSession(authOptions)
+ if (!session?.user) {
+ return NextResponse.json({ error: "인증이 필요합니다." }, { status: 401 })
+ }
+
+ const searchParams = request.nextUrl.searchParams
+ const documentReviewId = searchParams.get('documentReviewId')
+ const tbeSessionId = searchParams.get('sessionId')
+
+ if (!documentReviewId) {
+ return NextResponse.json({
+ error: "Missing required parameters"
+ }, { status: 400 })
+ }
+
+ const userId = typeof session.user.id === 'string' ? parseInt(session.user.id) : session.user.id
+ const isAdmin = (session.user as any).roles?.includes('admin') || false
+
+ // 권한 체크
+ const [existing] = await db
+ .select()
+ .from(rfqLastTbePdftronComments)
+ .where(
+ eq(rfqLastTbePdftronComments.documentReviewId, parseInt(documentReviewId))
+ )
+ .limit(1)
+
+ if (!existing) {
+ return NextResponse.json({ error: "Comment not found" }, { status: 404 })
+ }
+
+ if (existing.createdBy !== userId && !isAdmin) {
+ return NextResponse.json({ error: "Unauthorized" }, { status: 403 })
+ }
+
+ // 삭제
+ await db
+ .delete(rfqLastTbePdftronComments)
+ .where(eq(rfqLastTbePdftronComments.id, existing.id))
+
+ revalidateTag(`tbe-session-${tbeSessionId}`)
+
+
+ return NextResponse.json({ success: true })
+ } catch (error) {
+ console.error("Failed to delete XFDF:", error)
+ return NextResponse.json({ error: "Failed to delete XFDF" }, { status: 500 })
+ }
+}
+
+// XFDF 파싱 함수 - xml2js 사용
+async function parseXFDF(xfdfString: string): Promise<any> {
+ try {
+ // xml2js로 파싱
+ const result = await parseStringPromise(xfdfString, {
+ explicitArray: false,
+ ignoreAttrs: false,
+ mergeAttrs: false,
+ explicitRoot: false,
+ tagNameProcessors: [(name) => name.toLowerCase()]
+ })
+
+ const annotations: any[] = []
+
+ // annots 노드 확인
+ const annots = result?.annots
+ if (!annots) {
+ return { annotations: [] }
+ }
+
+ // 모든 annotation 타입 처리
+ const annotTypes = [
+ 'highlight', 'text', 'freetext', 'ink', 'square',
+ 'circle', 'line', 'polygon', 'polyline', 'stamp',
+ 'caret', 'fileattachment', 'sound', 'strikeout',
+ 'underline', 'squiggly', 'redact'
+ ]
+
+ for (const type of annotTypes) {
+ const items = annots[type]
+ if (!items) continue
+
+ // 배열이 아니면 배열로 변환
+ const itemArray = Array.isArray(items) ? items : [items]
+
+ for (const item of itemArray) {
+ const annotation: any = {
+ id: item.$?.name || '',
+ type: type,
+ page: parseInt(item.$?.page || '1'),
+ author: item.$?.title || '',
+ subject: item.$?.subject || '',
+ createdDate: item.$?.creationdate || '',
+ modifiedDate: item.$?.date || '',
+ }
+
+ // contents 가져오기
+ if (item.contents) {
+ annotation.contents = typeof item.contents === 'string'
+ ? item.contents
+ : item.contents._ || ''
+ }
+
+ // color 가져오기
+ if (item.$?.color) {
+ annotation.color = item.$.color
+ }
+
+ // opacity 가져오기
+ if (item.$?.opacity) {
+ annotation.opacity = parseFloat(item.$.opacity)
+ }
+
+ // custom data 가져오기
+ if (item.customdata) {
+ annotation.customData = {}
+ const properties = item.customdata.property
+ if (properties) {
+ const propArray = Array.isArray(properties) ? properties : [properties]
+ for (const prop of propArray) {
+ const name = prop.$?.name
+ const value = prop._ || prop
+ if (name && value) {
+ // 숫자 타입 변환
+ if (name === 'createdBy' || name === 'resolvedBy') {
+ annotation.customData[name] = parseInt(value)
+ } else {
+ annotation.customData[name] = value
+ }
+ }
+ }
+ }
+ }
+
+ // replies 가져오기
+ if (item.reply) {
+ annotation.replies = []
+ const replies = Array.isArray(item.reply) ? item.reply : [item.reply]
+ for (const reply of replies) {
+ annotation.replies.push({
+ author: reply.$?.title || '',
+ contents: typeof reply.contents === 'string'
+ ? reply.contents
+ : reply.contents?._ || '',
+ createdDate: reply.$?.creationdate || ''
+ })
+ }
+ }
+
+ // coords 가져오기 (rect, vertices 등)
+ if (item.$?.rect) {
+ annotation.coords = item.$.rect.split(',').map(Number)
+ } else if (item.$?.vertices) {
+ annotation.coords = item.$.vertices.split(';').join(',').split(',').map(Number)
+ } else if (item.$?.coords) {
+ annotation.coords = item.$.coords.split(',').map(Number)
+ }
+
+ // appearance 정보
+ if (item.appearance) {
+ annotation.appearance = item.appearance
+ }
+
+ // popup 정보
+ if (item.popup) {
+ annotation.popup = {
+ open: item.popup.$?.open === 'true',
+ rect: item.popup.$?.rect
+ }
+ }
+
+ annotations.push(annotation)
+ }
+ }
+
+ return { annotations }
+ } catch (error) {
+ console.error("Failed to parse XFDF:", error)
+ return { annotations: [] }
+ }
+} \ No newline at end of file