diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-09-15 14:41:01 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-09-15 14:41:01 +0000 |
| commit | 4ee8b24cfadf47452807fa2af801385ed60ab47c (patch) | |
| tree | e1d1fb029f0cf5519c517494bf9a545505c35700 /app/api/pdftron-comments/xfdf/route.ts | |
| parent | 265859d691a01cdcaaf9154f93c38765bc34df06 (diff) | |
(대표님) 작업사항 - rfqLast, tbeLast, pdfTron, userAuth
Diffstat (limited to 'app/api/pdftron-comments/xfdf/route.ts')
| -rw-r--r-- | app/api/pdftron-comments/xfdf/route.ts | 362 |
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 |
