export const runtime = 'nodejs' import { NextResponse } from 'next/server' import { PrismaClient } from '@/app/generated/prisma' import { auth } from '@/auth' function parseIsoDate(value: string | null): Date | null { if (!value) return null const d = new Date(value) return Number.isFinite(d.getTime()) ? d : null } export async function GET(request: Request) { const prisma = new PrismaClient() try { const session = await auth() if (!session?.user?.email) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) } const { searchParams } = new URL(request.url) const startParam = searchParams.get('start') const endParam = searchParams.get('end') const mode = (searchParams.get('mode') || 'items').toLowerCase() const interval = (searchParams.get('interval') || 'day').toLowerCase() as 'day' | 'week' | 'month' const start = parseIsoDate(startParam) const end = parseIsoDate(endParam) if (!start || !end) { return NextResponse.json({ error: 'invalid start/end' }, { status: 400 }) } if (start > end) { return NextResponse.json({ error: 'start after end' }, { status: 400 }) } // 날짜를 UTC 기준 하루 경계로 확장 const startDay = new Date(Date.UTC(start.getUTCFullYear(), start.getUTCMonth(), start.getUTCDate(), 0, 0, 0, 0)) const endDay = new Date(Date.UTC(end.getUTCFullYear(), end.getUTCMonth(), end.getUTCDate(), 23, 59, 59, 999)) // 사용자 소속 핸들 조회 (없으면 사용자 필터 생략) const userHandles = await prisma.handle.findMany({ where: { users: { some: { email: session.user.email } } }, select: { id: true, handle: true }, }) const handleIds = userHandles.map(h => h.id) // 선택된 채널 필터(query): handleId, handleIds(쉼표), handle, handles(쉼표) const handleIdParam = searchParams.get('handleId') const handleIdsParam = searchParams.get('handleIds') const handleParam = searchParams.get('handle') const handlesParam = searchParams.get('handles') let selectedHandleIds: string[] | null = null if (handleIdParam) selectedHandleIds = [handleIdParam] if (!selectedHandleIds && handleIdsParam) selectedHandleIds = handleIdsParam.split(',').map(s => s.trim()).filter(Boolean) if (!selectedHandleIds && (handleParam || handlesParam)) { const targetHandles = (handlesParam ? handlesParam.split(',') : [handleParam!]).map(s => s!.trim()).filter(Boolean) if (targetHandles.length > 0) { const matched = await prisma.handle.findMany({ where: { handle: { in: targetHandles } }, select: { id: true } }) selectedHandleIds = matched.map(m => m.id) } } const where: any = { date: { gte: startDay, lte: endDay }, } if (selectedHandleIds && selectedHandleIds.length > 0) { // 지정된 채널만 where.content = { handleId: { in: selectedHandleIds } } } else if (handleIds.length > 0) { // 사용자 소속 채널만 where.content = { handleId: { in: handleIds } } } // 시계열(기간별) 모드: day/week/month로 버킷팅해 합계 반환 if (mode === 'series') { const contentIdsParam = searchParams.get('contentIds') let contentIdsFilter: string[] | null = null if (contentIdsParam) contentIdsFilter = contentIdsParam.split(',').map(s => s.trim()).filter(Boolean) const seriesRows = await prisma.contentDayView.findMany({ where: { ...where, ...(contentIdsFilter && contentIdsFilter.length > 0 ? { contentId: { in: contentIdsFilter } } : {}), }, select: { date: true, views: true, validViews: true, premiumViews: true, watchTime: true, content: { select: { handle: { select: { costPerView: true } } } }, }, orderBy: { date: 'asc' }, }) // 버킷 키 생성기 const makeKey = (d: Date): string => { const y = d.getUTCFullYear() const m = String(d.getUTCMonth() + 1).padStart(2, '0') const dd = String(d.getUTCDate()).padStart(2, '0') if (interval === 'day') return `${y}-${m}-${dd}` if (interval === 'month') return `${y}-${m}` // week: ISO 주 시작(월요일)로 버킷팅 const date = new Date(Date.UTC(y, d.getUTCMonth(), d.getUTCDate())) const day = date.getUTCDay() || 7 // 1..7, 월=1, 일=7 const monday = new Date(date) monday.setUTCDate(date.getUTCDate() - (day - 1)) const my = String(monday.getUTCMonth() + 1).padStart(2, '0') const md = String(monday.getUTCDate()).padStart(2, '0') return `${monday.getUTCFullYear()}-${my}-${md}` } const bucket = new Map() for (const r of seriesRows) { const key = makeKey(new Date(r.date)) const cpv = Number(r.content?.handle?.costPerView ?? 0) || 0 const cur = bucket.get(key) || { views: 0, validViews: 0, premiumViews: 0, watchTime: 0, expectedRevenue: 0 } cur.views += r.views || 0 cur.validViews += r.validViews || 0 cur.premiumViews += r.premiumViews || 0 cur.watchTime += r.watchTime || 0 const effViews = (r.validViews || 0) + (r.premiumViews || 0) cur.expectedRevenue += Math.max(0, Math.round(effViews * cpv)) bucket.set(key, cur) } // 빈 버킷 채우기 (연속 타임라인) const addZeroIfMissing = (d: Date) => { const k = makeKey(d) if (!bucket.has(k)) bucket.set(k, { views: 0, validViews: 0, premiumViews: 0, watchTime: 0, expectedRevenue: 0 }) } if (interval === 'day') { let d = new Date(Date.UTC(startDay.getUTCFullYear(), startDay.getUTCMonth(), startDay.getUTCDate())) const end = new Date(Date.UTC(endDay.getUTCFullYear(), endDay.getUTCMonth(), endDay.getUTCDate())) while (d.getTime() <= end.getTime()) { addZeroIfMissing(d) d = new Date(d.getTime() + 24*60*60*1000) } } else if (interval === 'week') { const mkMonday = (d0: Date) => { const t = new Date(Date.UTC(d0.getUTCFullYear(), d0.getUTCMonth(), d0.getUTCDate())) const wd = t.getUTCDay() || 7 t.setUTCDate(t.getUTCDate() - (wd - 1)) return t } let d = mkMonday(startDay) const end = new Date(Date.UTC(endDay.getUTCFullYear(), endDay.getUTCMonth(), endDay.getUTCDate())) while (d.getTime() <= end.getTime()) { addZeroIfMissing(d) d = new Date(d.getTime() + 7*24*60*60*1000) } } else if (interval === 'month') { let d = new Date(Date.UTC(startDay.getUTCFullYear(), startDay.getUTCMonth(), 1)) const end = new Date(Date.UTC(endDay.getUTCFullYear(), endDay.getUTCMonth(), 1)) while (d.getTime() <= end.getTime()) { addZeroIfMissing(d) d = new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth() + 1, 1)) } } const items = Array.from(bucket.entries()).sort((a,b) => a[0] < b[0] ? -1 : 1).map(([period, agg]) => ({ period, ...agg })) return NextResponse.json({ success: true, mode: 'series', interval, start: startDay.toISOString(), end: endDay.toISOString(), count: items.length, items }) } const grouped = await prisma.contentDayView.groupBy({ by: ['contentId'], where, _sum: { views: true, validViews: true, premiumViews: true, watchTime: true, }, }) const contentIds = grouped.map(g => g.contentId).filter(Boolean) const contents = contentIds.length > 0 ? await prisma.content.findMany({ where: { id: { in: contentIds } }, select: { id: true, subject: true, pubDate: true, handle: { select: { id: true, handle: true, avatar: true, costPerView: true } }, }, }) : [] const idToContent = new Map(contents.map(c => [c.id, c])) const items = grouped.map(g => { const c = idToContent.get(g.contentId) as any const h = c?.handle as (undefined | { id: string, handle: string, avatar: string | null, costPerView?: number }) const views = g._sum?.views ?? 0 const validViews = g._sum?.validViews ?? 0 const premiumViews = g._sum?.premiumViews ?? 0 const watchTime = g._sum?.watchTime ?? 0 const cpv = typeof h?.costPerView === 'number' ? h!.costPerView! : 0 const expectedRevenue = Math.max(0, Math.round((validViews + premiumViews) * cpv)) return { id: c?.id ?? g.contentId, subject: c?.subject ?? '', pubDate: (c?.pubDate ?? null) as any, views, premiumViews, watchTime, handle: h?.handle ?? '', handleId: h?.id ?? null, icon: h?.avatar ?? '', validViews, expectedRevenue, } }) items.sort((a, b) => { const pa = a.pubDate ? new Date(a.pubDate as any).getTime() : 0 const pb = b.pubDate ? new Date(b.pubDate as any).getTime() : 0 return pb - pa }) return NextResponse.json({ success: true, start: startDay.toISOString(), end: endDay.toISOString(), count: items.length, items }) } catch (e) { console.error('[contents/mycontent] error:', e) return NextResponse.json({ error: 'query failed' }, { status: 500 }) } finally { try { await (prisma as any).$disconnect() } catch {} } }