summaryrefslogtreecommitdiff
path: root/app/api/pdftron-comments/xfdf/count/route.ts
blob: 19127ea9e66d6a5a914f699562bdbeb8eb6826d8 (plain)
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
// 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 })
    }
}