Files
ef_front/app/api/contents/mycontent/route.ts
2025-09-09 07:31:48 +00:00

223 lines
9.2 KiB
TypeScript

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<string, { views: number, validViews: number, premiumViews: number, watchTime: number, expectedRevenue: number }>()
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
cur.expectedRevenue += Math.max(0, Math.round((r.validViews || 0) * 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 * 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 {}
}
}