summaryrefslogtreecommitdiff
path: root/app/api/pdftron-comments/xfdf/count
diff options
context:
space:
mode:
Diffstat (limited to 'app/api/pdftron-comments/xfdf/count')
-rw-r--r--app/api/pdftron-comments/xfdf/count/route.ts171
1 files changed, 171 insertions, 0 deletions
diff --git a/app/api/pdftron-comments/xfdf/count/route.ts b/app/api/pdftron-comments/xfdf/count/route.ts
new file mode 100644
index 00000000..19127ea9
--- /dev/null
+++ b/app/api/pdftron-comments/xfdf/count/route.ts
@@ -0,0 +1,171 @@
+// app/api/pdftron-comments/xfdf/count/route.ts
+import { NextRequest, NextResponse } from "next/server"
+import { getServerSession } from "next-auth"
+import { authOptions } from "@/app/api/auth/[...nextauth]/route"
+import db from "@/db/db"
+import { rfqLastTbePdftronComments } from "@/db/schema"
+import { inArray } from "drizzle-orm"
+import { parseStringPromise } from "xml2js"
+
+type Counts = { totalCount: number; openCount: number }
+
+function fromCommentSummary(summary: any | null | undefined): Counts | null {
+ if (!summary) return null
+ // commentSummary가 다음 형태를 따른다고 가정:
+ // { totalCount?: number, openCount?: number } 또는 유사 구조
+ const t = Number((summary as any)?.totalCount)
+ const o = Number((summary as any)?.openCount)
+ if (Number.isFinite(t)) {
+ return { totalCount: t, openCount: Number.isFinite(o) ? o : t }
+ }
+ return null
+}
+
+async function fromXfdfString(xfdf: string | null | undefined): Promise<Counts | null> {
+ if (!xfdf) return null
+ try {
+ const xml = await parseStringPromise(xfdf, { explicitArray: true })
+ // XFDF 기본 구조: xfdf.annotations[0].annotation = [...]
+ const ann =
+ xml?.xfdf?.annotations?.[0]?.annotation ??
+ xml?.xfdf?.fdf?.annots?.[0]?.annot ??
+ [] // 방어적
+ const total = Array.isArray(ann) ? ann.length : 0
+
+ // “오픈/클로즈드” 판단 로직은 팀의 규칙에 맞게 조정:
+ // - 상태(StateModel/State) 혹은 CustomData를 쓰는 경우가 많음.
+ // - 기본 폴백: 전부 오픈으로 간주.
+ let open = total
+
+ // 예: <status>Completed</status> 이면 클로즈드로 처리
+ // (실제 저장 스키마에 맞춰 커스터마이즈하세요.)
+ let closed = 0
+ if (Array.isArray(ann)) {
+ for (const a of ann) {
+ const status =
+ a?.status?.[0] ||
+ a?.["it:status"]?.[0] ||
+ a?.state?.[0] ||
+ a?.custom?.[0]?.status?.[0]
+ if (
+ typeof status === "string" &&
+ ["Completed", "Resolved", "Accepted", "Rejected", "Closed"].includes(status)
+ ) {
+ closed += 1
+ }
+ }
+ }
+ open = Math.max(total - closed, 0)
+
+ return { totalCount: total, openCount: open }
+ } catch {
+ return null
+ }
+}
+
+
+type CommentSummary = {
+ total?: number
+ open?: number
+ resolved?: number
+ rejected?: number
+ deferred?: number
+ byAuthor?: Record<string, number>
+ byCategory?: Record<string, number>
+ bySeverity?: Record<string, number>
+}
+
+type Counts = { totalCount: number; openCount: number }
+
+function countsFromSummary(s?: CommentSummary | null): Counts | null {
+ if (!s) return null
+
+ // 1) open이 있으면 그걸 신뢰
+ if (Number.isFinite(s.open) && Number.isFinite(s.total)) {
+ return { totalCount: s.total!, openCount: s.open! }
+ }
+
+ // 2) open이 없으면 상태 기반으로 계산
+ if (Number.isFinite(s.total)) {
+ const resolved = Number(s.resolved ?? 0)
+ const rejected = Number(s.rejected ?? 0)
+ const deferred = Number(s.deferred ?? 0)
+ const open = Math.max(s.total! - resolved - rejected - deferred, 0)
+ return { totalCount: s.total!, openCount: open }
+ }
+
+ // 3) total이 누락된 희귀 케이스 → 분포 합으로 추정
+ const sum = (...recs: (Record<string, number> | undefined)[]) =>
+ recs.reduce((acc, r) => acc + (r ? Object.values(r).reduce((a, b) => a + (b || 0), 0) : 0), 0)
+
+ const guessedTotal = sum(s.byAuthor, s.byCategory, s.bySeverity)
+ if (guessedTotal > 0) {
+ const open = Number(s.open ?? Math.max(guessedTotal - Number(s.resolved ?? 0) - Number(s.rejected ?? 0) - Number(s.deferred ?? 0), 0))
+ return { totalCount: guessedTotal, openCount: open }
+ }
+
+ return null
+}
+
+export async function GET(request: NextRequest) {
+ try {
+ const session = await getServerSession(authOptions)
+ if (!session?.user) {
+ return NextResponse.json({ error: "인증이 필요합니다." }, { status: 401 })
+ }
+
+ const idsParam = request.nextUrl.searchParams.get("ids")
+ if (!idsParam) {
+ return NextResponse.json({ error: "ids is required (comma-separated)" }, { status: 400 })
+ }
+
+ const ids = idsParam
+ .split(",")
+ .map((s) => s.trim())
+ .filter(Boolean)
+ .map((s) => Number(s))
+ .filter((n) => Number.isFinite(n))
+
+ if (ids.length === 0) {
+ return NextResponse.json({ error: "no valid ids" }, { status: 400 })
+ }
+
+ // 한 번에 조회
+ const rows = await db
+ .select()
+ .from(rfqLastTbePdftronComments)
+ .where(inArray(rfqLastTbePdftronComments.documentReviewId, ids))
+
+ const result: Record<
+ number,
+ { totalCount: number; openCount: number; updatedAt: string | null }
+ > = {}
+
+ // 기본값: 코멘트 없음 → 0/0
+ for (const id of ids) {
+ result[id] = { totalCount: 0, openCount: 0, updatedAt: null }
+ }
+
+ // 요약 우선 → XFDF 파싱 폴백
+ await Promise.all(
+ rows.map(async (r: any) => {
+ const id = Number(r.documentReviewId)
+ let counts =
+ countsFromSummary(r.commentSummary as CommentSummary) ||
+ (await fromXfdfString(r.xfdfString)) || // 폴백
+ { totalCount: 0, openCount: 0 }
+
+ result[id] = {
+ totalCount: counts.totalCount,
+ openCount: counts.openCount,
+ updatedAt: r.updatedAt ?? null,
+ }
+ })
+ )
+
+ return NextResponse.json({ data: result })
+ } catch (err) {
+ console.error("xfdf/count GET error:", err)
+ return NextResponse.json({ error: "Failed to fetch counts" }, { status: 500 })
+ }
+}