175 lines
6.2 KiB
TypeScript
175 lines
6.2 KiB
TypeScript
export const runtime = 'nodejs'
|
|
import { NextResponse } from 'next/server'
|
|
import { PrismaClient } from '@/app/generated/prisma'
|
|
import { auth } from '@/auth'
|
|
import fs from 'fs/promises'
|
|
import path from 'path'
|
|
import { parse } from 'csv-parse/sync'
|
|
|
|
function parseKoreanDateToISO(dateStr: string): string | null {
|
|
// 입력 예: '2025. 9. 1.' → ISO YYYY-MM-DDT00:00:00.000Z (로컬 기준 단순화)
|
|
const m = dateStr?.match(/^(\d{4})\.\s*(\d{1,2})\.\s*(\d{1,2})\.?$/);
|
|
if (!m) return null;
|
|
const yyyy = Number(m[1]);
|
|
const mm = Number(m[2]);
|
|
const dd = Number(m[3]);
|
|
// UTC 자정으로 맞춤
|
|
const d = new Date(Date.UTC(yyyy, mm - 1, dd, 0, 0, 0));
|
|
return d.toISOString();
|
|
}
|
|
|
|
function toInt(v: any): number {
|
|
if (v == null) return 0;
|
|
if (typeof v === 'number') return Math.floor(v);
|
|
const s = String(v).replace(/,/g, '').trim();
|
|
const n = Number(s);
|
|
return Number.isFinite(n) ? Math.floor(n) : 0;
|
|
}
|
|
|
|
export async function GET(request: Request) {
|
|
try {
|
|
const urlObj = new URL(request.url)
|
|
const hostHeader = (request.headers.get('host') || '').toLowerCase()
|
|
const isLocal = hostHeader.includes('localhost') || hostHeader.includes('127.0.0.1') || urlObj.hostname === 'localhost' || urlObj.hostname === '127.0.0.1'
|
|
|
|
if (!isLocal) {
|
|
// 세션 사용자 확인 (핸들 매핑용)
|
|
const session = await auth();
|
|
if (!session?.user?.email) {
|
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
|
}
|
|
}
|
|
|
|
const { searchParams } = new URL(request.url)
|
|
const dateParam = searchParams.get('date') || 'latest'
|
|
|
|
let rows: any[] = []
|
|
let dateStr: string | undefined = undefined
|
|
|
|
if (dateParam === 'files') {
|
|
// Read from local CSV and use fixed date 250831
|
|
const filePath = path.join(process.cwd(), 'datas', 'to0831.csv')
|
|
const csv = await fs.readFile(filePath, 'utf-8')
|
|
rows = parse(csv, {
|
|
columns: true,
|
|
skip_empty_lines: true,
|
|
bom: true,
|
|
relax_column_count: true,
|
|
trim: true,
|
|
}) as any[]
|
|
dateStr = '250831'
|
|
} else {
|
|
const upstream = await fetch(`http://localhost:9556/data?date=${encodeURIComponent(dateParam)}`, {
|
|
method: 'GET',
|
|
})
|
|
const text = await upstream.text()
|
|
let data: any = null
|
|
try {
|
|
data = text ? JSON.parse(text) : null
|
|
} catch {
|
|
data = { message: text }
|
|
}
|
|
rows = Array.isArray(data?.data) ? data.data : []
|
|
dateStr = data?.date
|
|
if (!dateStr) {
|
|
return NextResponse.json({ error: 'invalid upstream payload' }, { status: 502 })
|
|
}
|
|
}
|
|
|
|
// 유효성 체크
|
|
if (!dateStr || rows.length === 0) {
|
|
return NextResponse.json({ error: 'invalid upstream payload' }, { status: 502 })
|
|
}
|
|
if (rows.length <= 1) {
|
|
return NextResponse.json({ success: true, message: 'no rows to import', date: dateStr })
|
|
}
|
|
|
|
// Convert date string: supports 'YYYY. M. D.' or 'YYMMDD'
|
|
const parseYYMMDD = (s: string): string | null => {
|
|
const m = s.match(/^(\d{2})(\d{2})(\d{2})$/)
|
|
if (!m) return null
|
|
const yyyy = 2000 + Number(m[1])
|
|
const mm = Number(m[2])
|
|
const dd = Number(m[3])
|
|
return new Date(Date.UTC(yyyy, mm - 1, dd, 0, 0, 0)).toISOString()
|
|
}
|
|
|
|
const isoDate = parseKoreanDateToISO(dateStr) || parseYYMMDD(dateStr) || new Date().toISOString();
|
|
const prisma = new PrismaClient();
|
|
// 날짜를 하루 단위(UTC 자정)로 고정해 동일 날짜는 같은 값으로 취급
|
|
const dayStart = new Date(isoDate);
|
|
dayStart.setUTCHours(0, 0, 0, 0);
|
|
const nextDay = new Date(dayStart.getTime() + 24 * 60 * 60 * 1000);
|
|
try {
|
|
// handle 연결은 비워둠 (요청에 따라 핸들 매핑 생략)
|
|
|
|
let upserted = 0;
|
|
// 2번째 행부터 반영
|
|
for (let i = 1; i < rows.length; i++) {
|
|
const row = rows[i] || {};
|
|
const contentId: string | undefined = row['콘텐츠'] ?? row['contentId'] ?? row['콘텐츠 ID'];
|
|
const subject: string = row['동영상 제목'] ?? row['제목'] ?? row['title'] ?? row['동영상'] ?? `content-${i}`;
|
|
const publishAtRaw: string | undefined = row['동영상 게시 시간'] ?? row['게시 시간'] ?? row['publishedAt'];
|
|
const publishAtDate = publishAtRaw ? new Date(publishAtRaw) : new Date(isoDate);
|
|
|
|
// Content upsert: id=콘텐츠, subject=동영상 제목, pubDate=동영상 게시 시간
|
|
const upsertedContent = await prisma.content.upsert({
|
|
where: { id: String(contentId ?? `content-${i}`) },
|
|
update: {
|
|
subject,
|
|
pubDate: publishAtDate,
|
|
},
|
|
create: {
|
|
id: String(contentId ?? `content-${i}`),
|
|
subject,
|
|
pubDate: publishAtDate,
|
|
// handle 연결 없음
|
|
},
|
|
select: { id: true },
|
|
});
|
|
|
|
const views = toInt(row['조회수']);
|
|
const validViews = toInt(row['유효 조회수']);
|
|
const premiumViews = toInt(row['YouTube Premium 조회수']);
|
|
const watchTime = toInt(row['시청 시간(단위: 시간)']);
|
|
|
|
// 같은 날짜(하루 단위)에 이미 존재하면 update, 없으면 create
|
|
const existing = await prisma.contentDayView.findFirst({
|
|
where: {
|
|
contentId: upsertedContent.id,
|
|
date: { gte: dayStart, lt: nextDay },
|
|
},
|
|
select: { id: true },
|
|
});
|
|
if (existing) {
|
|
await prisma.contentDayView.update({
|
|
where: { id: existing.id },
|
|
data: { views, validViews, premiumViews, watchTime },
|
|
});
|
|
} else {
|
|
await prisma.contentDayView.create({
|
|
data: {
|
|
contentId: upsertedContent.id,
|
|
date: dayStart,
|
|
views,
|
|
validViews,
|
|
premiumViews,
|
|
watchTime,
|
|
},
|
|
});
|
|
}
|
|
upserted += 1;
|
|
}
|
|
|
|
return NextResponse.json({ success: true, date: dateStr, upserted, data: rows })
|
|
} finally {
|
|
try { await (prisma as any).$disconnect() } catch {}
|
|
}
|
|
} catch (e) {
|
|
console.error('contents/update upstream error:', e)
|
|
return NextResponse.json({ error: '업스트림 요청 실패' }, { status: 502 })
|
|
}
|
|
}
|
|
|
|
|