import { NextResponse } from "next/server"; import { prisma } from "@/lib/db"; import { getActiveAccountId } from "@/lib/account-context"; import { authErrorResponse } from "@/lib/auth/api"; import { isAccountInUserScope, requireUserAccountScope } from "@/lib/auth/user-scope"; import { resolveProductContextForPlacement } from "@/lib/services/product-catalog"; import { attachBrandSelectionToTopic, attachProfileToTopic, migrateOrphanProductContexts, } from "@/lib/services/product-profile"; import { deleteTopicWithRelations } from "@/lib/services/topic"; import { hasProductContext } from "@/lib/types/product-context"; export async function GET() { try { const accountId = await getActiveAccountId(); const { where, accountIds } = await requireUserAccountScope(accountId); try { await migrateOrphanProductContexts(accountIds); } catch (migrateErr) { console.error("[topics] migrateOrphanProductContexts failed:", migrateErr); } const topics = await prisma.topic.findMany({ where, include: { productProfile: true, brandProfile: true, scans: { orderBy: { createdAt: "desc" }, take: 1, select: { id: true, createdAt: true, _count: { select: { items: true } }, }, }, _count: { select: { scans: true } }, }, orderBy: { createdAt: "desc" }, }); const enriched = topics.map(({ scans, _count, ...topic }) => ({ ...topic, scanCount: _count.scans, latestScan: scans[0] ? { id: scans[0].id, createdAt: scans[0].createdAt, itemCount: scans[0]._count.items, } : null, })); return NextResponse.json({ topics: enriched }); } catch (error) { const authRes = authErrorResponse(error); if (authRes) return authRes; const message = error instanceof Error ? error.message : "讀取主題失敗"; console.error("[topics] GET failed:", error); return NextResponse.json({ error: message, topics: [] }, { status: 500 }); } } export async function POST(request: Request) { try { const { label, query, brief, productContext, brandProfileId, productProfileId, topicGoal } = (await request.json()) as { label?: string; query?: string; brief?: string; productContext?: string; brandProfileId?: string; productProfileId?: string | null; topicGoal?: string; }; if (!label?.trim() || !query?.trim()) { return NextResponse.json({ error: "label 與 query 為必填" }, { status: 400 }); } const goal = topicGoal === "placement" ? "placement" : "viral"; if (goal === "placement" && !brandProfileId && !productProfileId && !hasProductContext(productContext)) { return NextResponse.json({ error: "置入產品模式請選擇品牌" }, { status: 400 }); } const accountId = await getActiveAccountId(); if (!accountId) { return NextResponse.json({ error: "請先建立並選定經營帳號" }, { status: 400 }); } let resolvedContext = productContext?.trim() || null; let resolvedBrandId: string | null = null; let resolvedProfileId: string | null = productProfileId ?? null; if (goal === "placement" && brandProfileId) { const brand = await prisma.brandProfile.findUnique({ where: { id: brandProfileId } }); if (!brand || brand.accountId !== accountId) { return NextResponse.json({ error: "找不到品牌" }, { status: 400 }); } resolvedBrandId = brand.id; resolvedContext = await resolveProductContextForPlacement({ brandProfileId: resolvedBrandId, productProfileId: resolvedProfileId, fallbackContext: productContext, }); } else if (goal === "placement" && productProfileId) { const profile = await prisma.productProfile.findUnique({ where: { id: productProfileId } }); if (!profile || profile.accountId !== accountId) { return NextResponse.json({ error: "找不到產品" }, { status: 400 }); } resolvedProfileId = profile.id; resolvedBrandId = profile.brandId; resolvedContext = await resolveProductContextForPlacement({ brandProfileId: resolvedBrandId, productProfileId: resolvedProfileId, fallbackContext: productContext, }); } const topic = await prisma.topic.create({ data: { accountId, label: label.trim(), query: query.trim(), brief: brief?.trim() || null, productContext: resolvedContext, brandProfileId: resolvedBrandId, productProfileId: resolvedProfileId, topicGoal: goal, }, include: { productProfile: true, brandProfile: true }, }); return NextResponse.json({ topic }); } catch (error) { const authRes = authErrorResponse(error); if (authRes) return authRes; const message = error instanceof Error ? error.message : "建立主題失敗"; return NextResponse.json({ error: message }, { status: 500 }); } } export async function PATCH(request: Request) { try { const { id, label, query, brief, productContext, brandProfileId, productProfileId, topicGoal, active, selectedTags, researchMap, } = (await request.json()) as { id?: string; label?: string; query?: string; brief?: string | null; productContext?: string | null; brandProfileId?: string | null; productProfileId?: string | null; topicGoal?: string; active?: boolean; selectedTags?: string[]; researchMap?: unknown; }; if (!id) { return NextResponse.json({ error: "缺少 id" }, { status: 400 }); } const { accountIds } = await requireUserAccountScope(await getActiveAccountId()); const existing = await prisma.topic.findUnique({ where: { id } }); if (!existing || !isAccountInUserScope(accountIds, existing.accountId)) { return NextResponse.json({ error: "找不到主題" }, { status: 404 }); } if (brandProfileId !== undefined) { await attachBrandSelectionToTopic(id, brandProfileId, productProfileId ?? null); } else if (productProfileId !== undefined) { await attachProfileToTopic(id, productProfileId); } const topic = await prisma.topic.update({ where: { id }, data: { ...(label !== undefined && { label }), ...(query !== undefined && { query }), ...(brief !== undefined && { brief }), ...(productContext !== undefined && productProfileId === undefined && { productContext }), ...(topicGoal !== undefined && { topicGoal: topicGoal === "placement" ? "placement" : "viral", }), ...(active !== undefined && { active }), ...(selectedTags !== undefined && { selectedTags: JSON.stringify(selectedTags) }), ...(researchMap !== undefined && { researchMap: JSON.stringify(researchMap) }), }, include: { productProfile: true, brandProfile: true }, }); return NextResponse.json({ topic }); } catch (error) { const authRes = authErrorResponse(error); if (authRes) return authRes; const message = error instanceof Error ? error.message : "更新主題失敗"; return NextResponse.json({ error: message }, { status: 500 }); } } export async function DELETE(request: Request) { try { const { searchParams } = new URL(request.url); const id = searchParams.get("id"); if (!id) { return NextResponse.json({ error: "缺少 id" }, { status: 400 }); } const { accountIds } = await requireUserAccountScope(await getActiveAccountId()); const topic = await prisma.topic.findUnique({ where: { id } }); if (!topic || !isAccountInUserScope(accountIds, topic.accountId)) { return NextResponse.json({ error: "找不到主題" }, { status: 404 }); } const result = await deleteTopicWithRelations(id); return NextResponse.json({ success: true, ...result }); } catch (error) { const authRes = authErrorResponse(error); if (authRes) return authRes; const message = error instanceof Error ? error.message : "刪除主題失敗"; return NextResponse.json({ error: message }, { status: 500 }); } }