middle 2
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -41,3 +41,6 @@ yarn-error.log*
|
||||
next-env.d.ts
|
||||
|
||||
/app/generated/prisma
|
||||
|
||||
|
||||
downloads
|
||||
@@ -1,2 +1,3 @@
|
||||
export const runtime = 'nodejs'
|
||||
import { handlers } from "@/auth" // Referring to the auth.ts we just created
|
||||
export const { GET, POST } = handlers
|
||||
60
app/api/channel/delete/route.ts
Normal file
60
app/api/channel/delete/route.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
export const runtime = 'nodejs'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { auth } from '@/auth'
|
||||
import { PrismaClient } from '@/app/generated/prisma'
|
||||
|
||||
// 연결 해제: 세션 사용자 ↔ 주어진 핸들의 매핑만 제거 (핸들 삭제 X)
|
||||
export async function GET(request: Request) {
|
||||
const session = await auth()
|
||||
if (!session) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const { searchParams } = new URL(request.url)
|
||||
const handle = searchParams.get('handle')
|
||||
if (!handle) {
|
||||
return NextResponse.json({ error: '핸들이 필요합니다' }, { status: 400 })
|
||||
}
|
||||
|
||||
const prisma = new PrismaClient()
|
||||
try {
|
||||
const email = session.user?.email ?? ''
|
||||
const user = await prisma.user.findFirst({ where: { email }, select: { id: true } })
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: '사용자를 찾을 수 없습니다.' }, { status: 404 })
|
||||
}
|
||||
|
||||
const handleRow = await prisma.handle.findUnique({ where: { handle } })
|
||||
if (!handleRow) {
|
||||
return NextResponse.json({ error: '핸들을 찾을 수 없습니다.' }, { status: 404 })
|
||||
}
|
||||
|
||||
// 이미 연결되어 있는지 확인
|
||||
const linked = await prisma.handle.findFirst({
|
||||
where: { id: handleRow.id, users: { some: { id: user.id } } },
|
||||
select: { id: true },
|
||||
})
|
||||
|
||||
if (!linked) {
|
||||
return NextResponse.json({ success: true, message: '이미 연결되지 않은 채널입니다.' }, { status: 200 })
|
||||
}
|
||||
|
||||
// 연결 해제
|
||||
await prisma.user.update({
|
||||
where: { id: user.id },
|
||||
data: { handles: { disconnect: { id: handleRow.id } } },
|
||||
})
|
||||
|
||||
return NextResponse.json({ success: true, message: '채널 연결을 해제했습니다.' }, { status: 200 })
|
||||
} finally {
|
||||
// prisma 종료
|
||||
try { await (prisma as any).$disconnect() } catch {}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('channel/delete 오류:', e)
|
||||
return NextResponse.json({ error: '요청 처리 실패' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
61
app/api/channel/list/route.ts
Normal file
61
app/api/channel/list/route.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
export const runtime = 'nodejs'
|
||||
import { NextResponse } from 'next/server';
|
||||
import { auth } from '@/auth';
|
||||
import { PrismaClient } from '@/app/generated/prisma';
|
||||
|
||||
export async function GET() {
|
||||
const session = await auth();
|
||||
if (!session) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
const email = session.user?.email as string | undefined;
|
||||
if (!email) {
|
||||
return NextResponse.json({ error: '세션 이메일을 찾을 수 없습니다' }, { status: 400 });
|
||||
}
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
try {
|
||||
// Admin: return all handles
|
||||
if (email === 'wsx204@naver.com') {
|
||||
const all = await prisma.handle.findMany({
|
||||
orderBy: { handle: 'asc' },
|
||||
select: { id: true, handle: true, avatar: true }
|
||||
});
|
||||
const items = all.map(h => ({
|
||||
id: h.id,
|
||||
handle: h.handle,
|
||||
createtime: new Date().toISOString(),
|
||||
is_approved: false,
|
||||
icon: h.avatar,
|
||||
}));
|
||||
return NextResponse.json({ items });
|
||||
}
|
||||
|
||||
// Non-admin: handles linked to session user
|
||||
const user = await prisma.user.findFirst({ where: { email }, select: { id: true } });
|
||||
if (!user) {
|
||||
return NextResponse.json({ items: [] });
|
||||
}
|
||||
const linked = await prisma.handle.findMany({
|
||||
where: { users: { some: { id: user.id } } },
|
||||
orderBy: { handle: 'asc' },
|
||||
select: { id: true, handle: true, avatar: true }
|
||||
});
|
||||
const items = linked.map(h => ({
|
||||
id: h.id,
|
||||
handle: h.handle,
|
||||
createtime: new Date().toISOString(),
|
||||
is_approved: false,
|
||||
icon: h.avatar,
|
||||
}));
|
||||
return NextResponse.json({ items });
|
||||
} catch (e) {
|
||||
console.error('list_channel 오류:', e);
|
||||
return NextResponse.json({ error: '조회 실패' }, { status: 500 });
|
||||
} finally {
|
||||
await prisma.$disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
33
app/api/channel/mycode/route.ts
Normal file
33
app/api/channel/mycode/route.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
export const runtime = 'nodejs'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { auth } from '@/auth'
|
||||
import { PrismaClient } from '@/app/generated/prisma'
|
||||
|
||||
export async function GET() {
|
||||
console.log('mycode 요청')
|
||||
const session = await auth()
|
||||
if (!session) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const email = session.user?.email as string | undefined
|
||||
if (!email) {
|
||||
return NextResponse.json({ error: '세션 이메일을 찾을 수 없습니다' }, { status: 400 })
|
||||
}
|
||||
|
||||
const prisma = new PrismaClient()
|
||||
try {
|
||||
const user = await prisma.user.findFirst({
|
||||
where: { email },
|
||||
select: { RegisgerCode: true },
|
||||
})
|
||||
return NextResponse.json({ registerCode: user?.RegisgerCode ?? null })
|
||||
} catch (e) {
|
||||
console.error('register_code 조회 오류:', e)
|
||||
return NextResponse.json({ error: '조회 실패' }, { status: 500 })
|
||||
} finally {
|
||||
await prisma.$disconnect()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
112
app/api/channel/register/route.ts
Normal file
112
app/api/channel/register/route.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
export const runtime = 'nodejs'
|
||||
import { NextResponse } from 'next/server';
|
||||
import { auth } from '@/auth';
|
||||
import { PrismaClient } from '@/app/generated/prisma';
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const session = await auth();
|
||||
if (!session) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
try {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const handle = searchParams.get('handle');
|
||||
// 안전하게 DB에서 최신 registerCode 조회 (세션 의존 제거)
|
||||
const prisma = new PrismaClient();
|
||||
const me = await prisma.user.findFirst({ where: { email: session.user?.email ?? '' }, select: { RegisgerCode: true } });
|
||||
const registerCode = me?.RegisgerCode ?? undefined;
|
||||
console.log(`registerCode: ${registerCode} and handle: ${handle}`);
|
||||
// 이미 등록된 채널인지 사전 확인 (현재 로그인 유저 기준)
|
||||
const userRowEarly = await prisma.user.findFirst({ where: { email: session.user?.email ?? '' }, select: { id: true } });
|
||||
if (!userRowEarly) {
|
||||
await prisma.$disconnect();
|
||||
return NextResponse.json({ error: '사용자를 찾을 수 없습니다.' }, { status: 404 });
|
||||
}
|
||||
const alreadyLinkedEarly = await prisma.handle.findFirst({
|
||||
where: { handle: handle ?? '', users: { some: { id: userRowEarly.id } } },
|
||||
select: { id: true },
|
||||
});
|
||||
if (alreadyLinkedEarly) {
|
||||
await prisma.$disconnect();
|
||||
return NextResponse.json({ success: true, message: '이미 등록된 채널입니다.' }, { status: 409 });
|
||||
}
|
||||
if (!handle) {
|
||||
return NextResponse.json({ error: '핸들이 필요합니다' }, { status: 400 });
|
||||
}
|
||||
|
||||
if (!registerCode) {
|
||||
return NextResponse.json({ error: 'can not find register code' }, { status: 400 });
|
||||
}
|
||||
|
||||
console.log('registerCode:', registerCode);
|
||||
|
||||
const upstream = await fetch('http://localhost:9556/isCodeMatch', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ handle: handle, code: registerCode })
|
||||
});
|
||||
|
||||
let data: any = null;
|
||||
const text = await upstream.text();
|
||||
try {
|
||||
data = text ? JSON.parse(text) : null;
|
||||
} catch {
|
||||
data = { message: text };
|
||||
}
|
||||
|
||||
console.log('data:', data);
|
||||
if (data.success) {
|
||||
if (data.foundtext) {
|
||||
if (data.foundtext == registerCode) {
|
||||
const avatarUrl = (data as any)?.avatar as string | undefined;
|
||||
// map user <-> handle (N:M)
|
||||
const email = session.user?.email ?? '';
|
||||
const userRow = await prisma.user.findFirst({ where: { email }, select: { id: true } });
|
||||
if (!userRow) {
|
||||
return NextResponse.json({ error: '사용자를 찾을 수 없습니다.' }, { status: 404 });
|
||||
}
|
||||
|
||||
// ensure handle exists
|
||||
const handleRow = await prisma.handle.upsert({
|
||||
where: { handle: handle },
|
||||
update: { ...(avatarUrl ? { avatar: avatarUrl } : {}) },
|
||||
create: { handle: handle, avatar: avatarUrl ?? '' },
|
||||
});
|
||||
|
||||
// avoid duplicate link
|
||||
const alreadyLinked = await prisma.handle.findFirst({
|
||||
where: { id: handleRow.id, users: { some: { id: userRow.id } } },
|
||||
select: { id: true },
|
||||
});
|
||||
if (!alreadyLinked) {
|
||||
await prisma.user.update({
|
||||
where: { id: userRow.id },
|
||||
data: { handles: { connect: { id: handleRow.id } } },
|
||||
});
|
||||
}
|
||||
|
||||
await prisma.$disconnect();
|
||||
return NextResponse.json({ success: true, message: '채널 매핑 완료' }, { status: 200 });
|
||||
}
|
||||
else{
|
||||
return NextResponse.json({ error: '코드가 일치하지 않습니다.' }, { status: 400 });
|
||||
}
|
||||
} else {
|
||||
return NextResponse.json({ error: '요청 실패' }, { status: 400 });
|
||||
}
|
||||
} else {
|
||||
return NextResponse.json({ error: '요청 실패' }, { status: 400 });
|
||||
}
|
||||
|
||||
|
||||
return NextResponse.json(data ?? {}, { status: upstream.status });
|
||||
} catch (error) {
|
||||
console.error('register_channel 프록시 오류:', error);
|
||||
return NextResponse.json({ error: '업스트림 요청 실패' }, { status: 502 });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { auth } from '@/auth';
|
||||
import { PrismaClient, Prisma } from '@/app/generated/prisma';
|
||||
import { randomBytes } from 'crypto';
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const session = await auth();
|
||||
if (!session) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
try {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const handle = searchParams.get('handle');
|
||||
|
||||
if (!handle) {
|
||||
return NextResponse.json({ error: '핸들이 필요합니다' }, { status: 400 });
|
||||
}
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
// 안전한 난수 기반 32자 코드 (충돌 확률 극히 낮음)
|
||||
const randomcode = randomBytes(16).toString('hex');
|
||||
|
||||
// 중복 여부 확인 (email + handle 기준)
|
||||
const exists = await prisma.registerChannel.findFirst({
|
||||
where: {
|
||||
email: session.user?.email as string,
|
||||
handle: handle,
|
||||
},
|
||||
select: { id: true, randomcode: true },
|
||||
});
|
||||
if (exists) {
|
||||
return NextResponse.json(
|
||||
{ error: '이미 등록된 요청입니다', randomcode: exists.randomcode },
|
||||
{ status: 409 }
|
||||
);
|
||||
}
|
||||
|
||||
// DB에 저장
|
||||
const created = await prisma.registerChannel.create({
|
||||
data: {
|
||||
email: session.user?.email as string,
|
||||
handle: handle,
|
||||
randomcode: randomcode
|
||||
}
|
||||
});
|
||||
|
||||
return NextResponse.json({ message: '성공', code: handle, randomcode: created.randomcode, id: created.id }, { status: 200 });
|
||||
} catch (error: unknown) {
|
||||
console.error('에러 발생:', error);
|
||||
|
||||
// Prisma 에러 코드별 분기
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
// 고유 제약조건 위반 등
|
||||
if (error.code === 'P2002') {
|
||||
return NextResponse.json({ error: '중복된 값으로 저장할 수 없습니다' }, { status: 409 });
|
||||
}
|
||||
return NextResponse.json({ error: '요청이 올바르지 않습니다', code: error.code }, { status: 400 });
|
||||
}
|
||||
if (error instanceof Prisma.PrismaClientValidationError) {
|
||||
return NextResponse.json({ error: '유효하지 않은 데이터입니다' }, { status: 400 });
|
||||
}
|
||||
|
||||
return NextResponse.json({ error: '서버 에러가 발생했습니다' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
// import { PrismaClient } from '../../generated/prisma/client';
|
||||
|
||||
// const prisma = new PrismaClient();
|
||||
|
||||
// export async function GET() {
|
||||
// const publishers = await prisma.content.findMany({
|
||||
// distinct: ['publisher'],
|
||||
// select: {
|
||||
// publisher: true,
|
||||
// },
|
||||
// });
|
||||
// const publisherList = publishers.map(p => p.publisher);
|
||||
// return Response.json(publisherList);
|
||||
// }
|
||||
@@ -1,4 +1,4 @@
|
||||
import { PrismaClient } from '../../generated/prisma/client';
|
||||
import { PrismaClient } from '@/app/generated/prisma';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
72
app/api/contents/handleupdate/route.ts
Normal file
72
app/api/contents/handleupdate/route.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
export const runtime = 'nodejs'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { PrismaClient } from '@/app/generated/prisma'
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const prisma = new PrismaClient()
|
||||
try {
|
||||
const { searchParams } = new URL(request.url)
|
||||
const limitRaw = searchParams.get('limit')
|
||||
let limit = Number(limitRaw ?? '10')
|
||||
if (!Number.isFinite(limit) || limit <= 0) limit = 100
|
||||
if (limit > 1000) limit = 1000
|
||||
|
||||
const rows = await prisma.content.findMany({
|
||||
where: { handleId: null },
|
||||
select: { id: true },
|
||||
take: limit,
|
||||
orderBy: { pubDate: 'desc' },
|
||||
})
|
||||
const ids = rows.map(r => r.id)
|
||||
console.log(`[contents/handleupdate] fetched ${ids.length}/${limit} ids without handle`)
|
||||
// Call local parsingServer with body { ids }
|
||||
try {
|
||||
const upstream = await fetch('http://localhost:9556/gethandle', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ ids }),
|
||||
})
|
||||
const text = await upstream.text()
|
||||
let forwarded: any = null
|
||||
try { forwarded = text ? JSON.parse(text) : null } catch { forwarded = { message: text } }
|
||||
// Map handles back to Content rows
|
||||
let mapped = 0
|
||||
const failures: Array<{ id: string, reason: string }> = []
|
||||
if (forwarded?.success && Array.isArray(forwarded?.items)) {
|
||||
for (const item of forwarded.items as Array<{ id?: string, handle?: string, avatar?: string }>) {
|
||||
const cid = String(item?.id ?? '')
|
||||
let h = String(item?.handle ?? '').trim()
|
||||
const avatar = typeof item?.avatar === 'string' ? item.avatar : undefined
|
||||
if (!cid || !h) { if (cid) failures.push({ id: cid, reason: 'missing_handle' }); continue }
|
||||
try {
|
||||
const handleRow = await prisma.handle.upsert({
|
||||
where: { handle: h },
|
||||
update: { ...(avatar ? { avatar } : {}) },
|
||||
create: { handle: h, ...(avatar ? { avatar } : {}) },
|
||||
select: { id: true }
|
||||
})
|
||||
await prisma.content.update({
|
||||
where: { id: cid },
|
||||
data: { handle: { connect: { id: handleRow.id } } },
|
||||
})
|
||||
mapped += 1
|
||||
} catch (e) {
|
||||
failures.push({ id: cid, reason: 'db_update_failed' })
|
||||
console.error('[contents/handleupdate] map failed for', cid, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
return NextResponse.json({ success: true, count: ids.length, ids, mapped, failures, upstream: forwarded }, { status: upstream.status })
|
||||
} catch (e) {
|
||||
console.error('[contents/handleupdate] upstream call error:', e)
|
||||
return NextResponse.json({ success: true, count: ids.length, ids, upstream: { error: 'upstream failed' } })
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[contents/handleupdate] query error:', e)
|
||||
return NextResponse.json({ error: 'query failed' }, { status: 500 })
|
||||
} finally {
|
||||
try { await (prisma as any).$disconnect() } catch {}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
152
app/api/contents/update/route.ts
Normal file
152
app/api/contents/update/route.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
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 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();
|
||||
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['시청 시간(단위: 시간)']);
|
||||
|
||||
// contentId + date 복합 유니크로 upsert
|
||||
await prisma.contentDayView.upsert({
|
||||
where: { contentId_date: { contentId: upsertedContent.id, date: new Date(isoDate) } },
|
||||
update: { views, validViews, premiumViews, watchTime },
|
||||
create: {
|
||||
contentId: upsertedContent.id,
|
||||
date: new Date(isoDate),
|
||||
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 })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { PrismaClient } from '@/app/generated/prisma';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const row = await prisma.costPerView.findUnique({ where: { id: 1 } });
|
||||
return NextResponse.json({ value: row?.costPerView ?? null });
|
||||
} catch (e) {
|
||||
console.error('GET /api/cost_per_view 오류:', e);
|
||||
return NextResponse.json({ error: '조회 실패' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const valueRaw = body?.value;
|
||||
const value = typeof valueRaw === 'string' ? parseFloat(valueRaw) : valueRaw;
|
||||
if (typeof value !== 'number' || Number.isNaN(value)) {
|
||||
return NextResponse.json({ error: '유효한 숫자 값을 제공하세요' }, { status: 400 });
|
||||
}
|
||||
|
||||
const saved = await prisma.costPerView.upsert({
|
||||
where: { id: 1 },
|
||||
update: { costPerView: value },
|
||||
create: { id: 1, costPerView: value },
|
||||
});
|
||||
|
||||
return NextResponse.json({ ok: true, value: saved.costPerView });
|
||||
} catch (e) {
|
||||
console.error('POST /api/cost_per_view 오류:', e);
|
||||
return NextResponse.json({ error: '저장 실패' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { auth } from '@/auth';
|
||||
import { PrismaClient } from '@/app/generated/prisma';
|
||||
|
||||
export async function GET() {
|
||||
const session = await auth();
|
||||
if (!session) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
const email = session.user?.email as string | undefined;
|
||||
if (!email) {
|
||||
return NextResponse.json({ error: '세션 이메일을 찾을 수 없습니다' }, { status: 400 });
|
||||
}
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
try {
|
||||
if (email === 'wsx204@naver.com') {
|
||||
const allHandles = await prisma.userHandle.findMany({
|
||||
orderBy: { createtime: 'desc' },
|
||||
select: { id: true, email: true, handle: true, isApproved: true, createtime: true, icon: true }
|
||||
});
|
||||
return NextResponse.json({ items: allHandles });
|
||||
}
|
||||
else{
|
||||
const handles = await prisma.userHandle.findMany({
|
||||
where: { email },
|
||||
orderBy: { createtime: 'desc' },
|
||||
select: { id: true, email: true, handle: true, isApproved: true, createtime: true, icon: true }
|
||||
});
|
||||
return NextResponse.json({ items: handles });
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('list_channel 오류:', e);
|
||||
return NextResponse.json({ error: '조회 실패' }, { status: 500 });
|
||||
} finally {
|
||||
await prisma.$disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { auth } from '@/auth';
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const session = await auth();
|
||||
if (!session) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
try {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const handle = searchParams.get('handle');
|
||||
const email = session.user?.email as string | undefined;
|
||||
|
||||
if (!handle) {
|
||||
return NextResponse.json({ error: '핸들이 필요합니다' }, { status: 400 });
|
||||
}
|
||||
|
||||
if (!email) {
|
||||
return NextResponse.json({ error: '세션 이메일을 찾을 수 없습니다' }, { status: 400 });
|
||||
}
|
||||
|
||||
const upstream = await fetch('http://localhost:10001/register_channel', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ email, handle })
|
||||
});
|
||||
|
||||
let data: any = null;
|
||||
const text = await upstream.text();
|
||||
try {
|
||||
data = text ? JSON.parse(text) : null;
|
||||
} catch {
|
||||
data = { message: text };
|
||||
}
|
||||
|
||||
return NextResponse.json(data ?? {}, { status: upstream.status });
|
||||
} catch (error) {
|
||||
console.error('register_channel 프록시 오류:', error);
|
||||
return NextResponse.json({ error: '업스트림 요청 실패' }, { status: 502 });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,10 +2,13 @@
|
||||
import CalenderSelector from "@/app/components/CalenderSelector";
|
||||
import Modal from "@/app/components/Modal";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useSession } from "next-auth/react";
|
||||
|
||||
|
||||
export default function Page() {
|
||||
const { data: session } = useSession();
|
||||
const [isreqmodal, setIsreqmodal] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [chanellist, setChanellist] = useState<string[]>([]);
|
||||
const [contentlist, setContentlist] = useState<{
|
||||
id: string;
|
||||
@@ -37,58 +40,47 @@ export default function Page() {
|
||||
return number.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
||||
};
|
||||
|
||||
const get_code = async () => {
|
||||
if (handleInputRef.current && handleInputRef.current.value) {
|
||||
const inputValue = handleInputRef.current.value;
|
||||
if (confirm(`핸들 "${inputValue}" 의 등록 코드를 발급하시겠습니까?`)) {
|
||||
try {
|
||||
const response = await fetch(`/api/channel_code?handle=${inputValue}`);
|
||||
if (response.status === 409) {
|
||||
alert("이미 등록된 요청입니다. 기존 코드를 사용합니다.");
|
||||
const data = await response.json();
|
||||
setRegisterCode(data.randomcode);
|
||||
return;
|
||||
}
|
||||
if (!response.ok) {
|
||||
throw new Error('서버 응답이 올바르지 않습니다.');
|
||||
}
|
||||
const data = await response.json();
|
||||
setRegisterCode(data.randomcode);
|
||||
} catch (error) {
|
||||
console.error('채널 코드 요청 중 오류 발생:', error);
|
||||
alert('채널 코드를 가져오는데 실패했습니다. 다시 시도해주세요.');
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
} else{ alert("채널 핸들을 입력해주세요"); }
|
||||
}
|
||||
|
||||
const register_channel = async () => {
|
||||
if (handleInputRef.current && handleInputRef.current.value) {
|
||||
const inputValue = handleInputRef.current.value;
|
||||
try {
|
||||
const response = await fetch(`/api/register_channel?handle=${encodeURIComponent(inputValue)}`);
|
||||
const data = await response.json();
|
||||
if (!response.ok) {
|
||||
alert(data?.error ?? '등록 요청 실패');
|
||||
return;
|
||||
}
|
||||
alert('등록 요청을 전송했습니다.');
|
||||
setIsreqmodal(false);
|
||||
} catch (error) {
|
||||
console.error('등록 요청 중 오류:', error);
|
||||
alert('등록 요청 실패');
|
||||
if (isLoading) return;
|
||||
if (!(handleInputRef.current && handleInputRef.current.value)) {
|
||||
alert("채널 핸들을 입력해주세요");
|
||||
return;
|
||||
}
|
||||
const inputValue = handleInputRef.current.value;
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const response = await fetch(`/api/channel/register?handle=${encodeURIComponent(inputValue)}`);
|
||||
const data = await response.json();
|
||||
if (!response.ok) {
|
||||
alert(data?.error ?? data?.message ?? '등록 요청 실패');
|
||||
return;
|
||||
}
|
||||
} else { alert("채널 핸들을 입력해주세요"); }
|
||||
alert('등록 요청을 전송했습니다.');
|
||||
fetchListChannel();
|
||||
setIsreqmodal(false);
|
||||
} catch (error) {
|
||||
console.error('등록 요청 중 오류:', error);
|
||||
alert('등록 요청 실패');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
const fetchRegisterCode = async () => {
|
||||
try {
|
||||
const resp = await fetch('/api/channel/mycode', { cache: 'no-store' });
|
||||
const data = await resp.json();
|
||||
setRegisterCode(data.registerCode);
|
||||
} catch (e) {
|
||||
console.error('register_code 요청 에러:', e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
const fetchListChannel = async () => {
|
||||
try {
|
||||
const resp = await fetch('/api/list_channel', { cache: 'no-store' });
|
||||
const resp = await fetch('/api/channel/list', { cache: 'no-store' });
|
||||
const data = await resp.json();
|
||||
setChannelList(data.items);
|
||||
console.log('list_channel:', data);
|
||||
@@ -97,9 +89,33 @@ export default function Page() {
|
||||
}
|
||||
}
|
||||
|
||||
const delete_channel = async (handle: string) => {
|
||||
if (isLoading) return;
|
||||
if (!handle) return;
|
||||
const ok = confirm(`정말 "${handle}" 채널 연결을 해제하시겠습니까?`);
|
||||
if (!ok) return;
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const resp = await fetch(`/api/channel/delete?handle=${encodeURIComponent(handle)}`);
|
||||
const data = await resp.json();
|
||||
if (!resp.ok) {
|
||||
alert(data?.error ?? '삭제 실패');
|
||||
return;
|
||||
}
|
||||
alert(data?.message ?? '채널 연결을 해제했습니다.');
|
||||
await fetchListChannel();
|
||||
} catch (e) {
|
||||
console.error('delete_channel 요청 에러:', e);
|
||||
alert('삭제 요청 실패');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
// 세션 이메일 기준 채널 목록 조회 로그
|
||||
fetchListChannel();
|
||||
fetchRegisterCode();
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -112,7 +128,7 @@ export default function Page() {
|
||||
<div className="flex flex-row h-[36px]">
|
||||
<div
|
||||
className="w-[104px] h-[36px] rounded-lg flex items-center justify-center bg-[#F94B37] border-[#D73B29] text-white cursor-pointer hover:bg-[#D73B29]"
|
||||
onClick={() => {setIsreqmodal(true); setRegisterCode("")}}
|
||||
onClick={() => {setIsreqmodal(true); }}
|
||||
>
|
||||
채널 등록
|
||||
</div>
|
||||
@@ -129,6 +145,7 @@ export default function Page() {
|
||||
<col className="w-[100px]"/>
|
||||
<col className="w-[100px]"/>
|
||||
<col className="w-[110px]"/>
|
||||
<col className="w-[90px]"/>
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr className="sticky top-0 bg-white h-[49px] ">
|
||||
@@ -137,12 +154,13 @@ export default function Page() {
|
||||
<th className="border-b-1 right-border border-[#e6e9ef] ">승인여부</th>
|
||||
<th className="border-b-1 right-border border-[#e6e9ef] ">영상수</th>
|
||||
<th className="border-b-1 right-border border-[#e6e9ef] ">조회수</th>
|
||||
<th className="border-b-1 border-[#e6e9ef] ">예상수익</th>
|
||||
<th className="border-b-1 right-border border-[#e6e9ef] ">예상수익</th>
|
||||
<th className="border-b-1 border-[#e6e9ef] ">삭제</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{
|
||||
channelList.map((channel)=>{
|
||||
channelList && channelList.map((channel)=>{
|
||||
return (
|
||||
<tr key={channel.id} className="h-[54px] border-1 border-[#e6e9ef] rounded-lg font-semibold">
|
||||
<td className="right-border rounded-l-lg border-l-1 border-t-1 border-b-1 border-[#e6e9ef] pl-2 h-[54px] " >
|
||||
@@ -159,7 +177,15 @@ export default function Page() {
|
||||
<td className="right-border border-b-1 border-t-1 border-[#e6e9ef] text-center whitespace-nowrap overflow-hidden">{channel.is_approved? "승인" : "미승인"}</td>
|
||||
<td className="right-border border-b-1 border-t-1 border-[#e6e9ef] text-center"> - </td>
|
||||
<td className="right-border border-b-1 border-t-1 border-[#e6e9ef] text-center"> - </td>
|
||||
<td className="border-b-1 border-t-1 border-[#e6e9ef] border-r-1 rounded-r-lg text-center"> - </td>
|
||||
<td className="right-border border-b-1 border-t-1 border-[#e6e9ef] text-center"> - </td>
|
||||
<td className="border-b-1 border-t-1 border-[#e6e9ef] border-r-1 rounded-r-lg text-center">
|
||||
<button
|
||||
className={`px-3 py-1 rounded-md ${isLoading ? 'bg-gray-300 cursor-not-allowed' : 'bg-red-500 hover:bg-red-600'} text-white`}
|
||||
onClick={() => { if (!isLoading) delete_channel(channel.handle); }}
|
||||
>
|
||||
삭제
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
})}
|
||||
@@ -179,58 +205,62 @@ export default function Page() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="flex flex-col sh-[287px]">
|
||||
<div className="flex flex-col sh-[287px] gap-3">
|
||||
<div className="flex flex-col justify-between">
|
||||
<div className="text-lg font-semibold text-black">핸들</div>
|
||||
<div className="flex flex-row justify-between items-center">
|
||||
<div className="w-[400px] h-[56px] border-[#d5d5d5] border-1 bg-white text-black font-normal rounded-lg flex items-center justify-center text-md p-1 px-2">
|
||||
<input type="text" className="w-full h-full border-none outline-none" ref={handleInputRef}/>
|
||||
</div>
|
||||
<div className="ml-2 w-[140px] h-[50px] border-[#D73B29] bg-[#F94B37] text-white font-bold rounded-lg flex items-center justify-center text-xl cursor-pointer hover:bg-[#D73B29] transition-colors" onClick={() => get_code()}>
|
||||
코드 발급
|
||||
<div className=" h-[56px] flex-1 border-[#d5d5d5] border-1 bg-white text-black font-normal rounded-lg flex items-center justify-center text-md p-1 px-2">
|
||||
<input type="text" className="w-full h-full border-none outline-none" ref={handleInputRef} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="flex flex-col justify-between mt-5">
|
||||
<div className="flex flex-col justify-between ">
|
||||
<div className="text-lg font-semibold text-black">채널 등록 코드</div>
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
|
||||
|
||||
|
||||
<div className="w-full h-[56px] border-[#d5d5d5] border-1 bg-white text-black font-normal rounded-lg flex items-center justify-start text-xl p-5">
|
||||
{registerCode ? registerCode : "코드를 발급해주세요"}
|
||||
{registerCode ? registerCode : "코드 로드 에러"}
|
||||
</div>
|
||||
{registerCode && (
|
||||
<div
|
||||
<div
|
||||
className="w-[45px] h-[56px] border-[#d5d5d5] border-1 bg-white rounded-lg flex items-center justify-center cursor-pointer"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(registerCode);
|
||||
}}
|
||||
>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M20 9H11C9.89543 9 9 9.89543 9 11V20C9 21.1046 9.89543 22 11 22H20C21.1046 22 22 21.1046 22 20V11C22 9.89543 21.1046 9 20 9Z" stroke="#666666" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
<path d="M5 15H4C3.46957 15 2.96086 14.7893 2.58579 14.4142C2.21071 14.0391 2 13.5304 2 13V4C2 3.46957 2.21071 2.96086 2.58579 2.58579C2.96086 2.21071 3.46957 2 4 2H13C13.5304 2 14.0391 2.21071 14.4142 2.58579C14.7893 2.96086 15 3.46957 15 4V5" stroke="#666666" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
<path d="M20 9H11C9.89543 9 9 9.89543 9 11V20C9 21.1046 9.89543 22 11 22H20C21.1046 22 22 21.1046 22 20V11C22 9.89543 21.1046 9 20 9Z" stroke="#666666" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||
<path d="M5 15H4C3.46957 15 2.96086 14.7893 2.58579 14.4142C2.21071 14.0391 2 13.5304 2 13V4C2 3.46957 2.21071 2.96086 2.58579 2.58579C2.96086 2.21071 3.46957 2 4 2H13C13.5304 2 14.0391 2.21071 14.4142 2.58579C14.7893 2.96086 15 3.46957 15 4V5" stroke="#666666" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<div className="mt-3 w-[100%-50px] h-[56px] border-[#D73B29] bg-[#F94B37] text-white font-bold rounded-lg flex items-center justify-center text-xl cursor-pointer hover:bg-[#D73B29] transition-colors" onClick={() => register_channel()}>
|
||||
</div>
|
||||
|
||||
<div className =" flex flex-row justify-between items-center gap-3">
|
||||
<div className="text-sm text-gray-500">
|
||||
* 채널에 코드를 설정한 후 채널 등록 버튼을 눌러주세요.
|
||||
</div>
|
||||
|
||||
<div className={`mt-3 h-[56px] flex-1 border-[#D73B29] bg-[#F94B37] text-white font-bold rounded-lg flex items-center justify-center text-xl transition-colors ${isLoading ? 'opacity-60 cursor-not-allowed' : 'cursor-pointer hover:bg-[#D73B29]'}`} onClick={() => { if (!isLoading) register_channel(); }}>
|
||||
채널 등록
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</Modal>
|
||||
|
||||
{isLoading && (
|
||||
<div className="fixed inset-0 z-[1000] bg-black/40 backdrop-blur-sm flex items-center justify-center">
|
||||
<div className="w-12 h-12 rounded-full border-4 border-white/30 border-t-white animate-spin" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
26
auth.ts
26
auth.ts
@@ -2,10 +2,14 @@
|
||||
import NextAuth from "next-auth";
|
||||
import Google from "next-auth/providers/google";
|
||||
import { PrismaClient } from "@/app/generated/prisma";
|
||||
import { randomBytes } from "crypto";
|
||||
|
||||
function generateRegisterCode(): string {
|
||||
return randomBytes(16).toString("hex");
|
||||
async function generateRegisterCode(): Promise<string> {
|
||||
// Library-free random (Edge 호환). 16바이트 hex
|
||||
let out = "";
|
||||
for (let i = 0; i < 16; i++) {
|
||||
const byte = Math.floor(Math.random() * 256);
|
||||
out += byte.toString(16).padStart(2, "0");
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
export const { handlers, auth, signIn, signOut } = NextAuth({
|
||||
@@ -16,18 +20,10 @@ export const { handlers, auth, signIn, signOut } = NextAuth({
|
||||
}),
|
||||
|
||||
],
|
||||
// callbacks 제거: 세션에 registerCode를 포함하지 않음
|
||||
|
||||
events: {
|
||||
async signIn({ user, account, profile }) {
|
||||
|
||||
console.log("[NextAuth] signIn user:", {
|
||||
id: (user as any)?.id,
|
||||
name: user?.name,
|
||||
email: user?.email,
|
||||
image: (user as any)?.image,
|
||||
provider: account?.provider,
|
||||
});
|
||||
|
||||
// Upsert User (by email)
|
||||
try {
|
||||
const prisma = new PrismaClient();
|
||||
@@ -41,7 +37,7 @@ export const { handlers, auth, signIn, signOut } = NextAuth({
|
||||
if (existing) {
|
||||
const registerCodeData = existing.RegisgerCode
|
||||
? {}
|
||||
: { RegisgerCode: generateRegisterCode() };
|
||||
: { RegisgerCode: await generateRegisterCode() };
|
||||
await prisma.user.update({
|
||||
where: { id: existing.id },
|
||||
data: {
|
||||
@@ -54,7 +50,7 @@ export const { handlers, auth, signIn, signOut } = NextAuth({
|
||||
data: {
|
||||
email,
|
||||
icon: (user as any)?.image ?? "",
|
||||
RegisgerCode: generateRegisterCode(),
|
||||
RegisgerCode: await generateRegisterCode(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
33601
datas/allview.csv
33601
datas/allview.csv
File diff suppressed because it is too large
Load Diff
@@ -1,22 +0,0 @@
|
||||
Id,Handle
|
||||
rM3ncw0Ptfg,@everfactory-vv
|
||||
crWGJmWxlnw,@everfactory-vv
|
||||
mcLo-NsO-NM,@everfactory-vv
|
||||
1OHjEOzTmv4,@everfactory-vv
|
||||
Zjqg68TZdQ4,@everfactory-vv
|
||||
rmLlvq99dTU,@everfactory-vv
|
||||
UVVZhU5uJIQ,@everfactory-vv
|
||||
Thhh8TYjmqQ,@everfactory-vv
|
||||
sMtkeSDDxB8,@everfactory-vv
|
||||
K1Bm4kk3rQk,@everfactory-vv
|
||||
kbjc0uQ7qbg,@everfactory-vv
|
||||
mQcZCZwqvxQ,@everfactory-vv
|
||||
7PtmMeek6L8,@everfactory-vv
|
||||
2xVTPCwDVgU,@everfactory-vv
|
||||
YEPMCEt1ecc,@everfactory-vv
|
||||
PmkxScRvHe8,@everfactory-vv
|
||||
nhQ9A6XEYDs,@everfactory-vv
|
||||
5o7uOU2DMyU,@everfactory-vv
|
||||
ENu1Z1E_NoY,@everfactory-vv
|
||||
fAAaXBn9O-U,@everfactory-vv
|
||||
zvAVfjijiCk,@everfactory-vv
|
||||
|
@@ -1,68 +0,0 @@
|
||||
Id,Handle
|
||||
nuahXCZWXPo,@한컵레시피
|
||||
lwc_pIdJY8I,@한컵레시피
|
||||
loJ_rxbqrZU,@한컵레시피
|
||||
NnkNOeMfM5c,@한컵레시피
|
||||
wNdv9WcFmQ0,@한컵레시피
|
||||
CWXePp-Xrm4,@한컵레시피
|
||||
RXTrXVRTfv0,@한컵레시피
|
||||
sM0i4naEPLM,@한컵레시피
|
||||
ZG_6QcaH91U,@한컵레시피
|
||||
-dxXNlgUOXM,@한컵레시피
|
||||
6531T9LmSkE,@한컵레시피
|
||||
6KmmHx-Oy6I,@한컵레시피
|
||||
37VUCIY9fLU,@한컵레시피
|
||||
xkYFC--fHN0,@한컵레시피
|
||||
HfcKOEiRZ-o,@한컵레시피
|
||||
B6DoyRp5mmw,@한컵레시피
|
||||
M7gkSmNfxno,@한컵레시피
|
||||
0tRjttSqmso,@한컵레시피
|
||||
UmR8_sqm7s8,@한컵레시피
|
||||
xWCnExggxSQ,@한컵레시피
|
||||
cMiHkXLA_Uo,@한컵레시피
|
||||
goFwS_M7gCU,@한컵레시피
|
||||
ZkopiYZ4axA,@한컵레시피
|
||||
6zVcsa75DBM,@한컵레시피
|
||||
mgTf6dtRWhU,@한컵레시피
|
||||
j21gZFMCbro,@한컵레시피
|
||||
pKzVoZMUjrc,@한컵레시피
|
||||
0yaQSG6H35A,@한컵레시피
|
||||
N7Af6IBj6N8,@한컵레시피
|
||||
2oEHULzBpe4,@한컵레시피
|
||||
ksJf608acRw,@한컵레시피
|
||||
LDIUV4rsowU,@한컵레시피
|
||||
J_ej2T9U3yk,@한컵레시피
|
||||
ZwKoCzm_Y9E,@한컵레시피
|
||||
USUsaLn1QKw,@한컵레시피
|
||||
GRe70V7-vZo,@한컵레시피
|
||||
9FZtZhY3wv4,@한컵레시피
|
||||
ws2CWeClmGE,@한컵레시피
|
||||
2QcNVPEg1Z8,@한컵레시피
|
||||
wGPOtOKO9G4,@한컵레시피
|
||||
MEgFyAwyruw,@한컵레시피
|
||||
FGod3w0byx0,@한컵레시피
|
||||
uv2To4Tpy40,@한컵레시피
|
||||
eFXkacofqtM,@한컵레시피
|
||||
ITeGIOsFSws,@한컵레시피
|
||||
tTivGZggRDk,@한컵레시피
|
||||
rM3ncw0Ptfg,@everfactory-vv
|
||||
crWGJmWxlnw,@everfactory-vv
|
||||
mcLo-NsO-NM,@everfactory-vv
|
||||
1OHjEOzTmv4,@everfactory-vv
|
||||
Zjqg68TZdQ4,@everfactory-vv
|
||||
rmLlvq99dTU,@everfactory-vv
|
||||
UVVZhU5uJIQ,@everfactory-vv
|
||||
Thhh8TYjmqQ,@everfactory-vv
|
||||
sMtkeSDDxB8,@everfactory-vv
|
||||
K1Bm4kk3rQk,@everfactory-vv
|
||||
kbjc0uQ7qbg,@everfactory-vv
|
||||
mQcZCZwqvxQ,@everfactory-vv
|
||||
7PtmMeek6L8,@everfactory-vv
|
||||
2xVTPCwDVgU,@everfactory-vv
|
||||
YEPMCEt1ecc,@everfactory-vv
|
||||
PmkxScRvHe8,@everfactory-vv
|
||||
nhQ9A6XEYDs,@everfactory-vv
|
||||
5o7uOU2DMyU,@everfactory-vv
|
||||
ENu1Z1E_NoY,@everfactory-vv
|
||||
fAAaXBn9O-U,@everfactory-vv
|
||||
zvAVfjijiCk,@everfactory-vv
|
||||
|
@@ -1,68 +0,0 @@
|
||||
Id,subject,pubDate,views,validViews,premiumViews,watchTime
|
||||
nuahXCZWXPo,컵으로 오레오쿠키케익 만들기 - 한끼_Cup,"Jun 10, 2025",32709,15705,3301,101.6509
|
||||
lwc_pIdJY8I,컵하나로 콘치즈버터 만들기 - 한끼_Cup,"May 13, 2025",37409,15258,6989,88.2174
|
||||
loJ_rxbqrZU,컵으로 엑설런트 크림 브륄레 만들기 - 한끼_Cup,"Jun 1, 2025",28762,14661,8808,89.384
|
||||
NnkNOeMfM5c,컵하나로 소떡소떡 만들기 - 한끼_Cup,"May 15, 2025",28159,13879,2501,72.1229
|
||||
wNdv9WcFmQ0,컵으로 떠먹는 컵피자 만들기 - 한끼_Cup,"May 18, 2025",25318,12658,6877,97.5433
|
||||
CWXePp-Xrm4,컵으로 전남친 토스트 만들기 - 한끼_Cup,"May 17, 2025",24187,11821,6116,65.9049
|
||||
RXTrXVRTfv0,컵으로 오레오쫀득쿠키 만들기 - 한끼_Cup,"Jun 14, 2025",20564,11464,5332,78.9794
|
||||
sM0i4naEPLM,컵으로 연유초콜릿 만들기 - 한끼_Cup,"May 27, 2025",23864,10652,6664,60.2018
|
||||
ZG_6QcaH91U,"단호박 에그슬럿 만들기 -한끼_Cup 밤호박,단호박","Jul 31, 2025",24272,10497,2126,143.3451
|
||||
-dxXNlgUOXM,컵으로 로투스 치즈케익 만들기 - 한끼_Cup,"Jun 12, 2025",13613,6394,3355,41.5752
|
||||
6531T9LmSkE,컵으로 고구마치즈브리또 만들기 - 한끼_Cup,"May 29, 2025",12189,5698,3441,37.9903
|
||||
6KmmHx-Oy6I,컵으로 또띠아 피자 브리또 만들기 - 한끼_Cup,"Jun 5, 2025",10749,5605,4515,32.2854
|
||||
37VUCIY9fLU,더블치즈햄토스트 만들기,"Aug 19, 2025",11421,4947,3486,40.2133
|
||||
xkYFC--fHN0,컵으로 치즈제육 민들기 - 한끼_Cup,"May 25, 2025",10109,4723,3743,31.8623
|
||||
HfcKOEiRZ-o,정숭제햄치즈롤 만들기,"Aug 21, 2025",6045,3909,1350,22.48
|
||||
B6DoyRp5mmw,깐풍만두 만들기 - 한끼_Cup,"Aug 2, 2025",7825,3888,1956,39.6999
|
||||
M7gkSmNfxno,구운두부 강정 만들기 - 한끼_Cup,"Jul 24, 2025",6064,2654,772,33.9777
|
||||
0tRjttSqmso,소세지 또띠아 만들기 -한끼_Cup,"Jul 29, 2025",5443,2574,1580,18.0177
|
||||
UmR8_sqm7s8,딸기모찌 민들기 - 한끼Cup #여름딸기,"Aug 5, 2025",4470,2476,674,20.4254
|
||||
xWCnExggxSQ,만두 그라탕 만들기 - 한끼_Cup #물만두,"Aug 14, 2025",4760,2365,1256,29.0473
|
||||
cMiHkXLA_Uo,컵으로 오징어볶음 만들기 - 한끼_Cup,"May 31, 2025",4853,2287,1602,17.6316
|
||||
goFwS_M7gCU,컵으로 초코제티브라우니 만들기 - 한끼_Cup,"Jun 17, 2025",5432,2125,1131,11.9966
|
||||
ZkopiYZ4axA,컵으로 다이제 에그타르트 만들기 - 한끼_Cup,"Jul 3, 2025",4987,2103,1750,16.5978
|
||||
6zVcsa75DBM,컵으로 마시멜로우 초코타르트 만들기 - 한끼_Cup,"Jun 21, 2025",3964,2094,731,15.9976
|
||||
mgTf6dtRWhU,컵으로 초코칩 쿠키 만들기 - 한끼_Cup,"Jun 19, 2025",4617,1972,1444,11.9979
|
||||
j21gZFMCbro,컵으로 닭갈비 만들기 - 한끼_Cup,"Jun 3, 2025",4096,1648,1651,11.8163
|
||||
pKzVoZMUjrc,컵으로 팽이부추베이컨말이 만들기 - 한끼_Cup,"May 21, 2025",4127,1592,990,12.1174
|
||||
0yaQSG6H35A,새우 멘보샤 만들기 -한끼_Cup #새우요리 #멘보샤,"Aug 12, 2025",3664,1555,1014,15.8445
|
||||
N7Af6IBj6N8,베이컨에그토스트 만들기 -한끼_Cup,"Aug 5, 2025",3585,1522,914,15.7509
|
||||
2oEHULzBpe4,마늘쫑무침 만들기 - 한끼_Cup,"Jul 22, 2025",3114,1410,276,19.9879
|
||||
ksJf608acRw,컵으로 컵 티라미슈 만들기 - 한끼_Cup,"May 24, 2025",2920,1203,729,7.4371
|
||||
LDIUV4rsowU,치킨마요 덮밥 만들기-한끼_Cup,"Jul 26, 2025",2362,755,629,5.9985
|
||||
J_ej2T9U3yk,컵으로 치즈오븐 스파게티 만들기 - 한끼_Cup,"Jul 10, 2025",2211,687,668,4.8089
|
||||
ZwKoCzm_Y9E,컵으로 파스타 치즈떡볶이 만들기 - 한끼_Cup,"Jun 24, 2025",1476,623,358,4.6206
|
||||
USUsaLn1QKw,더덕구이 만들기 Grilled deodeok -한끼_Cup #더덕구이,"Aug 16, 2025",1867,593,622,3.7365
|
||||
GRe70V7-vZo,베이컨치즈토스트 민들기 - 한끼_Cup,"Aug 5, 2025",1426,586,493,3.4593
|
||||
9FZtZhY3wv4,컵으로 다이어트 초코바나나케익 만들기 - 한끼_Cup,"Jun 7, 2025",2052,572,595,3.4588
|
||||
ws2CWeClmGE,허니콤보치킨 만들기 - 한끼_Cup,"Jun 28, 2025",1721,567,487,3.6471
|
||||
2QcNVPEg1Z8,에그마요 베이컨브레드 만들기 - 한끼_Cup,"Jul 8, 2025",1900,566,477,4.2135
|
||||
wGPOtOKO9G4,"식빵치즈핫도그 만들기 - 한끼_Cup #식빵핫도그,#핫도그만들기","Aug 9, 2025",1778,562,481,4.8828
|
||||
MEgFyAwyruw,서울 고속도로에 미확인 괴생명체 출몰,"Aug 20, 2025",1331,538,256,2.7627
|
||||
FGod3w0byx0,컵으로 마파두부 만들기 - 한끼_Cup,"May 20, 2025",1913,475,538,2.8631
|
||||
uv2To4Tpy40,양송이 게살 치즈구이 만들기 - 한끼_Cup,"Jul 5, 2025",1371,370,404,2.3947
|
||||
eFXkacofqtM,컵으로 밤티라미슈 만들기 - 한끼_Cup,"Jun 26, 2025",971,360,269,2.6383
|
||||
ITeGIOsFSws,"불닭팽이버섯치즈쌈 만들기 - 한끼_Cup #팽이버섯,#불닭볶음면","Aug 7, 2025",440,228,81,1.9858
|
||||
tTivGZggRDk,통마늘 닭똥집 파채구이 만들기 - 한끼_Cup,"Jul 1, 2025",470,111,141,0.9671
|
||||
rM3ncw0Ptfg,ZzuB,"Mar 10, 2025",46,46,16,0.2592
|
||||
crWGJmWxlnw,Twinkle water,"Mar 10, 2025",26,26,4,0.1799
|
||||
mcLo-NsO-NM,merry hill,"Apr 28, 2025",21,21,6,0.2222
|
||||
1OHjEOzTmv4,ZzuB,"Mar 10, 2025",20,20,15,0.0852
|
||||
Zjqg68TZdQ4,Raccoon dance,"Mar 10, 2025",19,19,12,0.0768
|
||||
rmLlvq99dTU,2025년 8월 22일,"Aug 22, 2025",68,19,17,0.079
|
||||
UVVZhU5uJIQ,Twinkle water,"Mar 10, 2025",16,16,7,0.1588
|
||||
Thhh8TYjmqQ,Raccoon dance,"Mar 10, 2025",14,14,6,0.0637
|
||||
sMtkeSDDxB8,Smile king,"Jun 22, 2025",12,12,2,0.0717
|
||||
K1Bm4kk3rQk,merry hill,"Apr 29, 2025",11,11,3,0.0336
|
||||
kbjc0uQ7qbg,Lilac love,"Apr 29, 2025",9,9,1,0.0508
|
||||
mQcZCZwqvxQ,select romance,"Apr 29, 2025",9,9,1,0.0413
|
||||
7PtmMeek6L8,The Flight Thief,"Apr 28, 2025",8,8,5,0.1007
|
||||
2xVTPCwDVgU,select romance,"Apr 28, 2025",7,7,4,0.1054
|
||||
YEPMCEt1ecc,Lilac love,"Apr 28, 2025",7,7,5,0.133
|
||||
PmkxScRvHe8,Light Waltz,"Apr 28, 2025",6,6,5,0.1103
|
||||
nhQ9A6XEYDs,light waltz,"Apr 29, 2025",6,6,1,0.0275
|
||||
5o7uOU2DMyU,The Flight Thief,"Apr 29, 2025",5,5,2,0.0181
|
||||
ENu1Z1E_NoY,Sunny afternoon,"Jun 27, 2025",4,4,1,0.0107
|
||||
fAAaXBn9O-U,Sunny afternoon,"Jun 27, 2025",3,3,1,0.0275
|
||||
zvAVfjijiCk,Smile king,"Jun 22, 2025",3,3,0,0.0247
|
||||
|
74
datas/to0831.csv
Normal file
74
datas/to0831.csv
Normal file
@@ -0,0 +1,74 @@
|
||||
콘텐츠,동영상 제목,동영상 게시 시간,길이,조회수,시청 시간(단위: 시간),노출수,노출 클릭률 (%)
|
||||
lwc_pIdJY8I,컵하나로 콘치즈버터 만들기 - 한끼_Cup,"May 13, 2025",18,37435,88.2594,4201,7.76
|
||||
nuahXCZWXPo,컵으로 오레오쿠키케익 만들기 - 한끼_Cup,"Jun 10, 2025",25,32733,101.7176,2706,4.25
|
||||
loJ_rxbqrZU,컵으로 엑설런트 크림 브륄레 만들기 - 한끼_Cup,"Jun 1, 2025",23,28871,89.9535,12390,4.67
|
||||
NnkNOeMfM5c,컵하나로 소떡소떡 만들기 - 한끼_Cup,"May 15, 2025",17,28169,72.1403,3526,4.74
|
||||
wNdv9WcFmQ0,컵으로 떠먹는 컵피자 만들기 - 한끼_Cup,"May 18, 2025",31,25373,97.8292,4033,5.53
|
||||
ZG_6QcaH91U,"단호박 에그슬럿 만들기 -한끼_Cup 밤호박,단호박","Jul 31, 2025",41,24281,143.3622,1744,5.33
|
||||
CWXePp-Xrm4,컵으로 전남친 토스트 만들기 - 한끼_Cup,"May 17, 2025",18,24197,65.9174,2653,3.43
|
||||
sM0i4naEPLM,컵으로 연유초콜릿 만들기 - 한끼_Cup,"May 27, 2025",19,23870,60.2184,2149,2.51
|
||||
RXTrXVRTfv0,컵으로 오레오쫀득쿠키 만들기 - 한끼_Cup,"Jun 14, 2025",26,20571,79.0032,3341,3.83
|
||||
37VUCIY9fLU,더블치즈햄토스트 만들기,"Aug 19, 2025",32,14747,50.556,715,5.73
|
||||
niZYAgpFcYc,메론킥으로 컵케익 만들게,"Aug 28, 2025",23,14239,46.0611,580,8.1
|
||||
HfcKOEiRZ-o,정숭제햄치즈롤 만들기,"Aug 21, 2025",31,13873,56.3047,590,5.93
|
||||
-dxXNlgUOXM,컵으로 로투스 치즈케익 만들기 - 한끼_Cup,"Jun 12, 2025",29,13621,41.6008,2664,2.06
|
||||
jyzX8UBWgJE,감자로 치즈고르케 만들게,"Aug 30, 2025",24,13224,45.9606,259,10.81
|
||||
6531T9LmSkE,컵으로 고구마치즈브리또 만들기 - 한끼_Cup,"May 29, 2025",20,12208,38.0439,3304,2.72
|
||||
6KmmHx-Oy6I,컵으로 또띠아 피자 브리또 만들기 - 한끼_Cup,"Jun 5, 2025",21,10772,32.354,2712,3.69
|
||||
xkYFC--fHN0,컵으로 치즈제육 민들기 - 한끼_Cup,"May 25, 2025",27,10115,31.8811,1425,3.51
|
||||
B6DoyRp5mmw,깐풍만두 만들기 - 한끼_Cup,"Aug 2, 2025",44,7845,39.7734,2004,2.2
|
||||
M7gkSmNfxno,구운두부 강정 만들기 - 한끼_Cup,"Jul 24, 2025",41,6073,34.1498,624,1.44
|
||||
0tRjttSqmso,소세지 또띠아 만들기 -한끼_Cup,"Jul 29, 2025",37,5458,18.0715,735,3.95
|
||||
goFwS_M7gCU,컵으로 초코제티브라우니 만들기 - 한끼_Cup,"Jun 17, 2025",16,5440,12.0064,1145,3.14
|
||||
ZkopiYZ4axA,컵으로 다이제 에그타르트 만들기 - 한끼_Cup,"Jul 3, 2025",45,5029,16.8321,2042,5.88
|
||||
cMiHkXLA_Uo,컵으로 오징어볶음 만들기 - 한끼_Cup,"May 31, 2025",32,4858,17.6495,1059,3.4
|
||||
xWCnExggxSQ,만두 그라탕 만들기 - 한끼_Cup #물만두,"Aug 14, 2025",48,4855,29.277,654,3.52
|
||||
mgTf6dtRWhU,컵으로 초코칩 쿠키 만들기 - 한끼_Cup,"Jun 19, 2025",25,4628,12.0034,1550,1.68
|
||||
UmR8_sqm7s8,딸기모찌 민들기 - 한끼Cup #여름딸기,"Aug 5, 2025",48,4485,20.4986,713,2.52
|
||||
pKzVoZMUjrc,컵으로 팽이부추베이컨말이 만들기 - 한끼_Cup,"May 21, 2025",22,4142,12.1568,2605,3.26
|
||||
j21gZFMCbro,컵으로 닭갈비 만들기 - 한끼_Cup,"Jun 3, 2025",29,4099,11.8247,1381,2.82
|
||||
6zVcsa75DBM,컵으로 마시멜로우 초코타르트 만들기 - 한끼_Cup,"Jun 21, 2025",42,3967,16.0031,949,2.63
|
||||
0yaQSG6H35A,새우 멘보샤 만들기 -한끼_Cup #새우요리 #멘보샤,"Aug 12, 2025",55,3733,16.0053,860,2.56
|
||||
N7Af6IBj6N8,베이컨에그토스트 만들기 -한끼_Cup,"Aug 5, 2025",41,3605,15.8132,591,3.38
|
||||
2oEHULzBpe4,마늘쫑무침 만들기 - 한끼_Cup,"Jul 22, 2025",42,3122,20.0238,654,4.59
|
||||
ksJf608acRw,컵으로 컵 티라미슈 만들기 - 한끼_Cup,"May 24, 2025",22,2924,7.442,1321,3.63
|
||||
LDIUV4rsowU,치킨마요 덮밥 만들기-한끼_Cup,"Jul 26, 2025",51,2450,6.2148,1361,3.09
|
||||
J_ej2T9U3yk,컵으로 치즈오븐 스파게티 만들기 - 한끼_Cup,"Jul 10, 2025",41,2228,4.8985,981,3.06
|
||||
9FZtZhY3wv4,컵으로 다이어트 초코바나나케익 만들기 - 한끼_Cup,"Jun 7, 2025",29,2058,3.4794,3364,2.2
|
||||
USUsaLn1QKw,더덕구이 만들기 Grilled deodeok -한끼_Cup #더덕구이,"Aug 16, 2025",46,1920,3.9045,550,4.91
|
||||
FGod3w0byx0,컵으로 마파두부 만들기 - 한끼_Cup,"May 20, 2025",20,1919,2.8747,2618,2.41
|
||||
2QcNVPEg1Z8,에그마요 베이컨브레드 만들기 - 한끼_Cup,"Jul 8, 2025",41,1903,4.2355,586,1.37
|
||||
wGPOtOKO9G4,"식빵치즈핫도그 만들기 - 한끼_Cup #식빵핫도그,#핫도그만들기","Aug 9, 2025",49,1797,4.9541,1068,1.59
|
||||
JA4mmsYRsRg,바나나킥 컵케익 만들기,"Aug 23, 2025",36,1723,3.6647,457,7
|
||||
ws2CWeClmGE,허니콤보치킨 만들기 - 한끼_Cup,"Jun 28, 2025",46,1722,3.6661,712,2.67
|
||||
-XiYk7gu-pU,또띠아시카고피자 만들기,"Aug 26, 2025",33,1709,2.8246,290,9.31
|
||||
ZwKoCzm_Y9E,컵으로 파스타 치즈떡볶이 만들기 - 한끼_Cup,"Jun 24, 2025",43,1479,4.6369,817,3.92
|
||||
GRe70V7-vZo,베이컨치즈토스트 민들기 - 한끼_Cup,"Aug 5, 2025",38,1440,3.4979,547,3.11
|
||||
uv2To4Tpy40,양송이 게살 치즈구이 만들기 - 한끼_Cup,"Jul 5, 2025",40,1372,2.4023,630,2.86
|
||||
MEgFyAwyruw,서울 고속도로에 미확인 괴생명체 출몰,"Aug 20, 2025",16,1335,2.7705,43,2.33
|
||||
eFXkacofqtM,컵으로 밤티라미슈 만들기 - 한끼_Cup,"Jun 26, 2025",41,973,2.6399,579,2.42
|
||||
tTivGZggRDk,통마늘 닭똥집 파채구이 만들기 - 한끼_Cup,"Jul 1, 2025",48,472,0.9817,490,2.86
|
||||
ITeGIOsFSws,"불닭팽이버섯치즈쌈 만들기 - 한끼_Cup #팽이버섯,#불닭볶음면","Aug 7, 2025",52,459,2.0423,1619,2.53
|
||||
rmLlvq99dTU,똑똑한망치,"Aug 22, 2025",12,274,0.2549,8,25
|
||||
m57Bt6_8MAM,Step to Success,"Aug 31, 2025",85,16,0.0696,2763,0.51
|
||||
rM3ncw0Ptfg,ZzuB,"Mar 10, 2025",80,12,0.0175,48,6.25
|
||||
sMtkeSDDxB8,Smile king,"Jun 22, 2025",103,12,0.0717,734,1.23
|
||||
K1Bm4kk3rQk,merry hill,"Apr 29, 2025",151,9,0.029,27,25.93
|
||||
Thhh8TYjmqQ,Raccoon dance,"Mar 10, 2025",89,7,0.0297,27,22.22
|
||||
mQcZCZwqvxQ,select romance,"Apr 29, 2025",99,7,0.0237,50,12
|
||||
1OHjEOzTmv4,ZzuB,"Mar 10, 2025",80,5,0.0343,24,12.5
|
||||
ENu1Z1E_NoY,Sunny afternoon,"Jun 27, 2025",83,5,0.0163,73,4.11
|
||||
Zjqg68TZdQ4,Raccoon dance,"Mar 10, 2025",88,5,0.0291,12,0
|
||||
crWGJmWxlnw,Twinkle water,"Mar 10, 2025",83,5,0.0073,57,7.02
|
||||
kbjc0uQ7qbg,Lilac love,"Apr 29, 2025",95,5,0.0401,120,2.5
|
||||
nhQ9A6XEYDs,light waltz,"Apr 29, 2025",84,5,0.0151,48,8.33
|
||||
2xVTPCwDVgU,select romance,"Apr 28, 2025",102,4,0.0749,14,7.14
|
||||
7PtmMeek6L8,The Flight Thief,"Apr 28, 2025",79,4,0.0655,17,0
|
||||
UVVZhU5uJIQ,Twinkle water,"Mar 10, 2025",83,4,0.0479,31,0
|
||||
YEPMCEt1ecc,Lilac love,"Apr 28, 2025",95,4,0.104,105,0
|
||||
fAAaXBn9O-U,Sunny afternoon,"Jun 27, 2025",82,4,0.0503,46,4.35
|
||||
zvAVfjijiCk,Smile king,"Jun 22, 2025",103,4,0.0307,63,3.17
|
||||
5o7uOU2DMyU,The Flight Thief,"Apr 29, 2025",79,3,0.011,30,10
|
||||
PmkxScRvHe8,Light Waltz,"Apr 28, 2025",80,3,0.0615,18,0
|
||||
mcLo-NsO-NM,merry hill,"Apr 28, 2025",151,3,0.0759,20,0
|
||||
1dQ4E_m0pgE,Step to Success,"Aug 31, 2025",85,0,0,0,
|
||||
|
File diff suppressed because it is too large
Load Diff
193
package-lock.json
generated
193
package-lock.json
generated
@@ -12,13 +12,17 @@
|
||||
"@milkdown/kit": "^7.15.3",
|
||||
"@milkdown/react": "^7.15.3",
|
||||
"@prisma/client": "^6.13.0",
|
||||
"adm-zip": "^0.5.16",
|
||||
"chart.js": "^4.5.0",
|
||||
"csv-parse": "^6.1.0",
|
||||
"next": "15.4.6",
|
||||
"next-auth": "^5.0.0-beta.29",
|
||||
"react": "19.1.0",
|
||||
"react-chartjs-2": "^5.3.0",
|
||||
"react-dom": "19.1.0",
|
||||
"react-icons": "^5.5.0"
|
||||
"react-icons": "^5.5.0",
|
||||
"require": "^2.4.20",
|
||||
"user-agents": "^1.1.645"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4",
|
||||
@@ -26,6 +30,8 @@
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"papaparse": "^5.5.3",
|
||||
"playwright": "^1.55.0",
|
||||
"playwright-extra": "^4.3.6",
|
||||
"prisma": "^6.13.0",
|
||||
"tailwindcss": "^4",
|
||||
"typescript": "^5"
|
||||
@@ -2384,6 +2390,29 @@
|
||||
"integrity": "sha512-cZy8Dq+uuIXbxCZpuLd2GJdeSO/lIzIspC2WtkqIpje5QyFbvLaI5wZtdUjLHjGZrlVX6GilejatWwVYYRc8tA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/adm-zip": {
|
||||
"version": "0.5.16",
|
||||
"resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.16.tgz",
|
||||
"integrity": "sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/amdefine": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz",
|
||||
"integrity": "sha512-S2Hw0TtNkMJhIabBwIojKL9YHO5T0n5eNqWJ7Lrlel/zDbftQpxpapi8tZs3X1HWa+u+QeydGmzzNU0m09+Rcg==",
|
||||
"license": "BSD-3-Clause OR MIT",
|
||||
"engines": {
|
||||
"node": ">=0.4.2"
|
||||
}
|
||||
},
|
||||
"node_modules/async": {
|
||||
"version": "0.2.10",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-0.2.10.tgz",
|
||||
"integrity": "sha512-eAkdoKxU6/LkKDBzLpT+t6Ff5EtfSF4wx1WfJiPEEV7WNLnDaRXk0oVysiEPm262roaachGexwUv94WhSgN5TQ=="
|
||||
},
|
||||
"node_modules/bail": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz",
|
||||
@@ -2624,6 +2653,12 @@
|
||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/csv-parse": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/csv-parse/-/csv-parse-6.1.0.tgz",
|
||||
"integrity": "sha512-CEE+jwpgLn+MmtCpVcPtiCZpVtB6Z2OKPTr34pycYYoL7sxdOkXDdQ4lRiw6ioC0q6BLqhc6cKweCVvral8yhw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
|
||||
@@ -2836,6 +2871,21 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/giget": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/giget/-/giget-2.0.0.tgz",
|
||||
@@ -3193,6 +3243,12 @@
|
||||
"integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.clonedeep": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
|
||||
"integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/longest-streak": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz",
|
||||
@@ -4262,6 +4318,15 @@
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/optimist": {
|
||||
"version": "0.3.7",
|
||||
"resolved": "https://registry.npmjs.org/optimist/-/optimist-0.3.7.tgz",
|
||||
"integrity": "sha512-TCx0dXQzVtSCg2OgY/bO9hjM9cV4XYx09TVK+s3+FhkjT6LovsLe+pPMzpWf+6yXK/hUizs2gUoTw3jHM0VaTQ==",
|
||||
"license": "MIT/X11",
|
||||
"dependencies": {
|
||||
"wordwrap": "~0.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/orderedmap": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/orderedmap/-/orderedmap-2.1.1.tgz",
|
||||
@@ -4325,6 +4390,63 @@
|
||||
"pathe": "^2.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright": {
|
||||
"version": "1.55.0",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.55.0.tgz",
|
||||
"integrity": "sha512-sdCWStblvV1YU909Xqx0DhOjPZE4/5lJsIS84IfN9dAZfcl/CIZ5O8l3o0j7hPMjDvqoTF8ZUcc+i/GL5erstA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.55.0"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright-core": {
|
||||
"version": "1.55.0",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.55.0.tgz",
|
||||
"integrity": "sha512-GvZs4vU3U5ro2nZpeiwyb0zuFaqb9sUiAJuyrWpcGouD8y9/HLgGbNRjIph7zU9D3hnPaisMl9zG9CgFi/biIg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"playwright-core": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright-extra": {
|
||||
"version": "4.3.6",
|
||||
"resolved": "https://registry.npmjs.org/playwright-extra/-/playwright-extra-4.3.6.tgz",
|
||||
"integrity": "sha512-q2rVtcE8V8K3vPVF1zny4pvwZveHLH8KBuVU2MoE3Jw4OKVoBWsHI9CH9zPydovHHOCDxjGN2Vg+2m644q3ijA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "^4.3.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"playwright": "*",
|
||||
"playwright-core": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"playwright": {
|
||||
"optional": true
|
||||
},
|
||||
"playwright-core": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.6",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
|
||||
@@ -4791,6 +4913,22 @@
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/require": {
|
||||
"version": "2.4.20",
|
||||
"resolved": "https://registry.npmjs.org/require/-/require-2.4.20.tgz",
|
||||
"integrity": "sha512-7eop5rvh38qhQQQOoUyf68meVIcxT2yFySNywTbxoEECgkX4KDqqDRaEszfvFnuB3fuZVjDdJZ1TI/Esr16RRA==",
|
||||
"dependencies": {
|
||||
"std": "0.1.40",
|
||||
"uglify-js": "2.3.0"
|
||||
},
|
||||
"bin": {
|
||||
"require": "bin/require-command.js"
|
||||
},
|
||||
"engines": {
|
||||
"browsers": "*",
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/rope-sequence": {
|
||||
"version": "1.3.4",
|
||||
"resolved": "https://registry.npmjs.org/rope-sequence/-/rope-sequence-1.3.4.tgz",
|
||||
@@ -4869,6 +5007,17 @@
|
||||
"is-arrayish": "^0.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map": {
|
||||
"version": "0.1.43",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.1.43.tgz",
|
||||
"integrity": "sha512-VtCvB9SIQhk3aF6h+N85EaqIaBFIAfZ9Cu+NJHHVvc8BbEcnvDcFw6sqQ2dQrT6SlOrZq3tIvyD9+EGq/lJryQ==",
|
||||
"dependencies": {
|
||||
"amdefine": ">=0.0.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map-js": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||
@@ -4914,6 +5063,14 @@
|
||||
"devOptional": true,
|
||||
"license": "CC0-1.0"
|
||||
},
|
||||
"node_modules/std": {
|
||||
"version": "0.1.40",
|
||||
"resolved": "https://registry.npmjs.org/std/-/std-0.1.40.tgz",
|
||||
"integrity": "sha512-wUf57hkDGCoVShrhPA8Q7lAg2Qosk+FaMlECmAsr1A4/rL2NRXFHQGBcgMUFKVkPEemJFW9gzjCQisRty14ohg==",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/style-mod": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.2.tgz",
|
||||
@@ -5028,6 +5185,22 @@
|
||||
"node": ">=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/uglify-js": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.3.0.tgz",
|
||||
"integrity": "sha512-AQvbxRKdaQeYADywQaao0k8Tj+7NGEVTne6xwgX1yQpv/G8b0CKdIw70HkCptwfvNGDsVe+0Bng3U9hfWbxxfg==",
|
||||
"dependencies": {
|
||||
"async": "~0.2.6",
|
||||
"optimist": "~0.3.5",
|
||||
"source-map": "~0.1.7"
|
||||
},
|
||||
"bin": {
|
||||
"uglifyjs": "bin/uglifyjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||
@@ -5136,6 +5309,15 @@
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/user-agents": {
|
||||
"version": "1.1.656",
|
||||
"resolved": "https://registry.npmjs.org/user-agents/-/user-agents-1.1.656.tgz",
|
||||
"integrity": "sha512-i+uWRpqcQ2JeiSlZA6NTS3NGcRPdKkI4N9I6uDleA/N/Fyuk8+wQBjF2WvcP8QY7wLH2PtU6uJ8GX3Jaw3q6kA==",
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"lodash.clonedeep": "^4.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/validate-npm-package-license": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",
|
||||
@@ -5202,6 +5384,15 @@
|
||||
"integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/wordwrap": {
|
||||
"version": "0.0.3",
|
||||
"resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz",
|
||||
"integrity": "sha512-1tMA907+V4QmxV7dbRvb4/8MaRALK6q9Abid3ndMYnbyo8piisCmeONVqVSXqQA3KaP4SLt5b7ud6E2sqP8TFw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/yallist": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz",
|
||||
|
||||
14
package.json
14
package.json
@@ -6,7 +6,8 @@
|
||||
"dev": "next dev --turbopack -p 9551",
|
||||
"build": "next build",
|
||||
"start": "next start -p 9551",
|
||||
"lint": "next lint"
|
||||
"lint": "next lint",
|
||||
"server": "node parsingServer/server.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@milkdown/crepe": "^7.15.3",
|
||||
@@ -19,7 +20,12 @@
|
||||
"react": "19.1.0",
|
||||
"react-chartjs-2": "^5.3.0",
|
||||
"react-dom": "19.1.0",
|
||||
"react-icons": "^5.5.0"
|
||||
"react-icons": "^5.5.0",
|
||||
"adm-zip": "^0.5.16",
|
||||
"csv-parse": "^6.1.0",
|
||||
"require": "^2.4.20",
|
||||
"user-agents": "^1.1.645"
|
||||
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4",
|
||||
@@ -29,6 +35,8 @@
|
||||
"papaparse": "^5.5.3",
|
||||
"prisma": "^6.13.0",
|
||||
"tailwindcss": "^4",
|
||||
"typescript": "^5"
|
||||
"typescript": "^5",
|
||||
"playwright": "^1.55.0",
|
||||
"playwright-extra": "^4.3.6"
|
||||
}
|
||||
}
|
||||
|
||||
540
parsingServer/everlogin.json
Normal file
540
parsingServer/everlogin.json
Normal file
@@ -0,0 +1,540 @@
|
||||
{
|
||||
"cookies": [
|
||||
{
|
||||
"name": "YSC",
|
||||
"value": "SoLyLVQqWh0",
|
||||
"domain": ".youtube.com",
|
||||
"path": "/",
|
||||
"expires": -1,
|
||||
"httpOnly": true,
|
||||
"secure": true,
|
||||
"sameSite": "None",
|
||||
"partitionKey": "https://youtube.com",
|
||||
"_crHasCrossSiteAncestor": false
|
||||
},
|
||||
{
|
||||
"name": "VISITOR_INFO1_LIVE",
|
||||
"value": "Yt7NAsrM7WY",
|
||||
"domain": ".youtube.com",
|
||||
"path": "/",
|
||||
"expires": 1772013745.097248,
|
||||
"httpOnly": true,
|
||||
"secure": true,
|
||||
"sameSite": "None",
|
||||
"partitionKey": "https://youtube.com",
|
||||
"_crHasCrossSiteAncestor": false
|
||||
},
|
||||
{
|
||||
"name": "VISITOR_PRIVACY_METADATA",
|
||||
"value": "CgJLUhIEGgAgaA%3D%3D",
|
||||
"domain": ".youtube.com",
|
||||
"path": "/",
|
||||
"expires": 1772013745.097427,
|
||||
"httpOnly": true,
|
||||
"secure": true,
|
||||
"sameSite": "None",
|
||||
"partitionKey": "https://youtube.com",
|
||||
"_crHasCrossSiteAncestor": false
|
||||
},
|
||||
{
|
||||
"name": "OTZ",
|
||||
"value": "8235962_20_20__20_",
|
||||
"domain": "accounts.google.com",
|
||||
"path": "/",
|
||||
"expires": 1759053690,
|
||||
"httpOnly": false,
|
||||
"secure": true,
|
||||
"sameSite": "Lax"
|
||||
},
|
||||
{
|
||||
"name": "NID",
|
||||
"value": "525=c77p5rMCKpZpK-lRczvsP0O7mWcn11XqG2IkZSDDOIiHampZ8XN3LHMwAExrudakTRMb9qK_MzK7oXva0coNq5KS4HyyvaPreH88Yj9U9S2RhaOcpibBnXBk1qqjy7dzYwATFQgWr5tymBLf6aEKTqC3Hc1bsDamyi-1LuvzJ4eC31k9QDc6fxs4MW683ceBCIVFbydkpgCy5KMrCTVZly0alLrNttvy5UsQ9nS5HlxO7KIoX_LMHLxYlWSZ7LBURqJ-E6ojIIAznFDAiOrj55YCRpTupGSnCneIbIVe5mOq8CLZiLdFjxGiQFXsf0McSpspkoqg7VvqIY4NAseyVLY3pK5KrXQXdsQLTLNqrGfs_8CULFfnpNzLGZPTqw0GRkMP8FTOX6OjUKg5BTDuVhJSvTTqq8a-DiK30pAaJkEwck_IfBKNrCiJlTOD47FS84jEtGfHqzPPq_v9JnincjyyjFgYCyExnAgKX3zntocn7egn3VlQcYENWrp63znbl86mD9MD5H31pLpgd7-GPh8F38sMN0gVx41Jt5mwSyUGuS54MV9azj2-zLOaz5xUX92r4cU5AcUBjeytYCDOuLVlbQMJJOQ_VMD9pb2Hanegm916ivNQihX5gNyur3UjC99Ttb-Sgi87g-hbaQ",
|
||||
"domain": ".google.com",
|
||||
"path": "/",
|
||||
"expires": 1772272890.148944,
|
||||
"httpOnly": true,
|
||||
"secure": true,
|
||||
"sameSite": "None"
|
||||
},
|
||||
{
|
||||
"name": "SID",
|
||||
"value": "g.a0000wiGXo9BShFNNJ5CEkScDHAw9yQ9q3mAu2PO_EhHK1FgAgph3zzza1N12w1DaKWvsf15gwACgYKAYISARASFQHGX2MiH6Sk3xSSnRee_PJnGYLj8hoVAUF8yKppY931rQU6jPDD9hhHk42d0076",
|
||||
"domain": ".google.com",
|
||||
"path": "/",
|
||||
"expires": 1791021734.149031,
|
||||
"httpOnly": false,
|
||||
"secure": false,
|
||||
"sameSite": "Lax"
|
||||
},
|
||||
{
|
||||
"name": "__Secure-1PSID",
|
||||
"value": "g.a0000wiGXo9BShFNNJ5CEkScDHAw9yQ9q3mAu2PO_EhHK1FgAgphER3yjvgRYRTknphnKewNLgACgYKAVcSARASFQHGX2MiMZdMKXIePglT17fwJmLVihoVAUF8yKqhvN2HJDp_L7-HNkQLrDmf0076",
|
||||
"domain": ".google.com",
|
||||
"path": "/",
|
||||
"expires": 1791021734.149087,
|
||||
"httpOnly": true,
|
||||
"secure": true,
|
||||
"sameSite": "Lax"
|
||||
},
|
||||
{
|
||||
"name": "__Secure-3PSID",
|
||||
"value": "g.a0000wiGXo9BShFNNJ5CEkScDHAw9yQ9q3mAu2PO_EhHK1FgAgphMlymBwnbbRlMkbORx4z8MQACgYKAbUSARASFQHGX2MiQKVesqsgHy4eHxL-OR7BgRoVAUF8yKpOgg6OQHgzZV4Qpa7hC0hg0076",
|
||||
"domain": ".google.com",
|
||||
"path": "/",
|
||||
"expires": 1791021734.149128,
|
||||
"httpOnly": true,
|
||||
"secure": true,
|
||||
"sameSite": "None"
|
||||
},
|
||||
{
|
||||
"name": "HSID",
|
||||
"value": "Ahb8mLa4WobvWmAAj",
|
||||
"domain": ".google.com",
|
||||
"path": "/",
|
||||
"expires": 1791021734.149327,
|
||||
"httpOnly": true,
|
||||
"secure": false,
|
||||
"sameSite": "Lax"
|
||||
},
|
||||
{
|
||||
"name": "SSID",
|
||||
"value": "AdVbZATUhp5ogskXE",
|
||||
"domain": ".google.com",
|
||||
"path": "/",
|
||||
"expires": 1791021734.149366,
|
||||
"httpOnly": true,
|
||||
"secure": true,
|
||||
"sameSite": "Lax"
|
||||
},
|
||||
{
|
||||
"name": "APISID",
|
||||
"value": "zGwlwp6_0opojmt1/AXZ6m5MWQ7TWQDjqa",
|
||||
"domain": ".google.com",
|
||||
"path": "/",
|
||||
"expires": 1791021734.149387,
|
||||
"httpOnly": false,
|
||||
"secure": false,
|
||||
"sameSite": "Lax"
|
||||
},
|
||||
{
|
||||
"name": "SAPISID",
|
||||
"value": "TM8JfVIqtJ14hk-M/Aqu5S_2gUbGNCXygB",
|
||||
"domain": ".google.com",
|
||||
"path": "/",
|
||||
"expires": 1791021734.149405,
|
||||
"httpOnly": false,
|
||||
"secure": true,
|
||||
"sameSite": "Lax"
|
||||
},
|
||||
{
|
||||
"name": "__Secure-1PAPISID",
|
||||
"value": "TM8JfVIqtJ14hk-M/Aqu5S_2gUbGNCXygB",
|
||||
"domain": ".google.com",
|
||||
"path": "/",
|
||||
"expires": 1791021734.149441,
|
||||
"httpOnly": false,
|
||||
"secure": true,
|
||||
"sameSite": "Lax"
|
||||
},
|
||||
{
|
||||
"name": "__Secure-3PAPISID",
|
||||
"value": "TM8JfVIqtJ14hk-M/Aqu5S_2gUbGNCXygB",
|
||||
"domain": ".google.com",
|
||||
"path": "/",
|
||||
"expires": 1791021734.149458,
|
||||
"httpOnly": false,
|
||||
"secure": true,
|
||||
"sameSite": "None"
|
||||
},
|
||||
{
|
||||
"name": "__Host-GAPS",
|
||||
"value": "1:odOgTzZ6-qPKacvF074yQhojUvqmVOywkdkDIfWMtyJvQYqTZuw0h8jnloQU2kfkScvpu5dADJ6UVdv3_QUgnPc3q5hwPw:xkXGw0cbxA6_iCIJ",
|
||||
"domain": "accounts.google.com",
|
||||
"path": "/",
|
||||
"expires": 1791021734.149493,
|
||||
"httpOnly": true,
|
||||
"secure": true,
|
||||
"sameSite": "Lax"
|
||||
},
|
||||
{
|
||||
"name": "SMSV",
|
||||
"value": "ADHTe-DPKMPWGpahBcEyEsoTraLK2FbFfAkgzwRKznhFiDN8PVbFATUPBoz59OSSDbqw72F5Yn4QsdaCIv9DAZ_O1T0Rhhc4VFNVfn2O69ETDlxZSxmJm24",
|
||||
"domain": "accounts.google.com",
|
||||
"path": "/",
|
||||
"expires": 1791021734.149515,
|
||||
"httpOnly": true,
|
||||
"secure": true,
|
||||
"sameSite": "Lax"
|
||||
},
|
||||
{
|
||||
"name": "ACCOUNT_CHOOSER",
|
||||
"value": "AFx_qI4ud8eCAi5R97SRwILApjOheS11zBfXk3iK4AWOigBrJzgzHHCF3X_EMQ115KVsreYGlpiEgECUt-XZs0CF6xG3pgsawxd0bzojhy1Pbi5tuavIb7wHo02eRm131NIPfezlnRlB",
|
||||
"domain": "accounts.google.com",
|
||||
"path": "/",
|
||||
"expires": 1791021734.302116,
|
||||
"httpOnly": true,
|
||||
"secure": true,
|
||||
"sameSite": "Lax"
|
||||
},
|
||||
{
|
||||
"name": "OTZ",
|
||||
"value": "8235962_20_20__20_",
|
||||
"domain": "gds.google.com",
|
||||
"path": "/",
|
||||
"expires": 1759053736,
|
||||
"httpOnly": false,
|
||||
"secure": true,
|
||||
"sameSite": "Lax"
|
||||
},
|
||||
{
|
||||
"name": "LSID",
|
||||
"value": "s.KR|s.youtube:g.a0000wiGXuTfUhrszcquojp2tBXms542sLQTIcdKY-rYgcUxecTOmPa4dmAzwoDOJF03bgxuGAACgYKAX4SARASFQHGX2MiKOcK7a7qBUyMlD1XyPs7cxoVAUF8yKraLin7jkbQ-DOjgzYdw7tQ0076",
|
||||
"domain": "accounts.google.com",
|
||||
"path": "/",
|
||||
"expires": 1791021743.828325,
|
||||
"httpOnly": true,
|
||||
"secure": true,
|
||||
"sameSite": "Lax"
|
||||
},
|
||||
{
|
||||
"name": "__Host-1PLSID",
|
||||
"value": "s.KR|s.youtube:g.a0000wiGXuTfUhrszcquojp2tBXms542sLQTIcdKY-rYgcUxecTOc8zH3DjKFfMuaC5LR606zgACgYKATASARASFQHGX2MiVY1gGoXeV7jH2gxkX-kUSRoVAUF8yKqX42oKMfAbaqKmAlYwNAfd0076",
|
||||
"domain": "accounts.google.com",
|
||||
"path": "/",
|
||||
"expires": 1791021743.82838,
|
||||
"httpOnly": true,
|
||||
"secure": true,
|
||||
"sameSite": "Lax"
|
||||
},
|
||||
{
|
||||
"name": "__Host-3PLSID",
|
||||
"value": "s.KR|s.youtube:g.a0000wiGXuTfUhrszcquojp2tBXms542sLQTIcdKY-rYgcUxecTO0x4fjQBFldXa9NpwIvH7kwACgYKAbISARASFQHGX2MiNv5RdiMkdcybpyud09WaGBoVAUF8yKr0Xa9asRn8U-Fu6WQaAI5D0076",
|
||||
"domain": "accounts.google.com",
|
||||
"path": "/",
|
||||
"expires": 1791021743.828402,
|
||||
"httpOnly": true,
|
||||
"secure": true,
|
||||
"sameSite": "None"
|
||||
},
|
||||
{
|
||||
"name": "SIDCC",
|
||||
"value": "AKEyXzV8WJuPVhVBsy222hWpVJT1YsbcnBi038CpeWuwUG19Q2rOyZtpVIq6gyTBNTh1vpqR",
|
||||
"domain": ".google.com",
|
||||
"path": "/",
|
||||
"expires": 1787997743.82842,
|
||||
"httpOnly": false,
|
||||
"secure": false,
|
||||
"sameSite": "Lax"
|
||||
},
|
||||
{
|
||||
"name": "__Secure-1PSIDCC",
|
||||
"value": "AKEyXzU2aittHXySbaezuVYaTLjMMLXT5f0AKsfwtBClZYYWiLcoHogtU-T0CafxMf-bixIg",
|
||||
"domain": ".google.com",
|
||||
"path": "/",
|
||||
"expires": 1787997743.828438,
|
||||
"httpOnly": true,
|
||||
"secure": true,
|
||||
"sameSite": "Lax"
|
||||
},
|
||||
{
|
||||
"name": "__Secure-1PSIDTS",
|
||||
"value": "sidts-CjUB5H03P189T55R_SQ5eHSgXNRz2JaEeCOAVW-mFQyk9QTzmQunjJFaLXyIDJmLUkLqyoTPYhAA",
|
||||
"domain": ".youtube.com",
|
||||
"path": "/",
|
||||
"expires": 1787997744.349839,
|
||||
"httpOnly": true,
|
||||
"secure": true,
|
||||
"sameSite": "Lax"
|
||||
},
|
||||
{
|
||||
"name": "__Secure-3PSIDTS",
|
||||
"value": "sidts-CjUB5H03P189T55R_SQ5eHSgXNRz2JaEeCOAVW-mFQyk9QTzmQunjJFaLXyIDJmLUkLqyoTPYhAA",
|
||||
"domain": ".youtube.com",
|
||||
"path": "/",
|
||||
"expires": 1787997744.349979,
|
||||
"httpOnly": true,
|
||||
"secure": true,
|
||||
"sameSite": "None"
|
||||
},
|
||||
{
|
||||
"name": "HSID",
|
||||
"value": "AV0jI4ZTivCSfZQBO",
|
||||
"domain": ".youtube.com",
|
||||
"path": "/",
|
||||
"expires": 1791021744.35004,
|
||||
"httpOnly": true,
|
||||
"secure": false,
|
||||
"sameSite": "Lax"
|
||||
},
|
||||
{
|
||||
"name": "SSID",
|
||||
"value": "A1sqOvbPmy-hlvo_g",
|
||||
"domain": ".youtube.com",
|
||||
"path": "/",
|
||||
"expires": 1791021744.350108,
|
||||
"httpOnly": true,
|
||||
"secure": true,
|
||||
"sameSite": "Lax"
|
||||
},
|
||||
{
|
||||
"name": "APISID",
|
||||
"value": "zGwlwp6_0opojmt1/AXZ6m5MWQ7TWQDjqa",
|
||||
"domain": ".youtube.com",
|
||||
"path": "/",
|
||||
"expires": 1791021744.350202,
|
||||
"httpOnly": false,
|
||||
"secure": false,
|
||||
"sameSite": "Lax"
|
||||
},
|
||||
{
|
||||
"name": "SAPISID",
|
||||
"value": "TM8JfVIqtJ14hk-M/Aqu5S_2gUbGNCXygB",
|
||||
"domain": ".youtube.com",
|
||||
"path": "/",
|
||||
"expires": 1791021744.350299,
|
||||
"httpOnly": false,
|
||||
"secure": true,
|
||||
"sameSite": "Lax"
|
||||
},
|
||||
{
|
||||
"name": "__Secure-1PAPISID",
|
||||
"value": "TM8JfVIqtJ14hk-M/Aqu5S_2gUbGNCXygB",
|
||||
"domain": ".youtube.com",
|
||||
"path": "/",
|
||||
"expires": 1791021744.350339,
|
||||
"httpOnly": false,
|
||||
"secure": true,
|
||||
"sameSite": "Lax"
|
||||
},
|
||||
{
|
||||
"name": "__Secure-3PAPISID",
|
||||
"value": "TM8JfVIqtJ14hk-M/Aqu5S_2gUbGNCXygB",
|
||||
"domain": ".youtube.com",
|
||||
"path": "/",
|
||||
"expires": 1791021744.350369,
|
||||
"httpOnly": false,
|
||||
"secure": true,
|
||||
"sameSite": "None"
|
||||
},
|
||||
{
|
||||
"name": "SID",
|
||||
"value": "g.a0000wiGXo9BShFNNJ5CEkScDHAw9yQ9q3mAu2PO_EhHK1FgAgph3zzza1N12w1DaKWvsf15gwACgYKAYISARASFQHGX2MiH6Sk3xSSnRee_PJnGYLj8hoVAUF8yKppY931rQU6jPDD9hhHk42d0076",
|
||||
"domain": ".youtube.com",
|
||||
"path": "/",
|
||||
"expires": 1791021744.350399,
|
||||
"httpOnly": false,
|
||||
"secure": false,
|
||||
"sameSite": "Lax"
|
||||
},
|
||||
{
|
||||
"name": "__Secure-1PSID",
|
||||
"value": "g.a0000wiGXo9BShFNNJ5CEkScDHAw9yQ9q3mAu2PO_EhHK1FgAgphER3yjvgRYRTknphnKewNLgACgYKAVcSARASFQHGX2MiMZdMKXIePglT17fwJmLVihoVAUF8yKqhvN2HJDp_L7-HNkQLrDmf0076",
|
||||
"domain": ".youtube.com",
|
||||
"path": "/",
|
||||
"expires": 1791021744.35043,
|
||||
"httpOnly": true,
|
||||
"secure": true,
|
||||
"sameSite": "Lax"
|
||||
},
|
||||
{
|
||||
"name": "__Secure-3PSID",
|
||||
"value": "g.a0000wiGXo9BShFNNJ5CEkScDHAw9yQ9q3mAu2PO_EhHK1FgAgphMlymBwnbbRlMkbORx4z8MQACgYKAbUSARASFQHGX2MiQKVesqsgHy4eHxL-OR7BgRoVAUF8yKpOgg6OQHgzZV4Qpa7hC0hg0076",
|
||||
"domain": ".youtube.com",
|
||||
"path": "/",
|
||||
"expires": 1791021744.350459,
|
||||
"httpOnly": true,
|
||||
"secure": true,
|
||||
"sameSite": "None"
|
||||
},
|
||||
{
|
||||
"name": "HSID",
|
||||
"value": "AV0jI4ZTivCSfZQBO",
|
||||
"domain": ".google.co.kr",
|
||||
"path": "/",
|
||||
"expires": 1791021744.707993,
|
||||
"httpOnly": true,
|
||||
"secure": false,
|
||||
"sameSite": "Lax"
|
||||
},
|
||||
{
|
||||
"name": "SSID",
|
||||
"value": "A1sqOvbPmy-hlvo_g",
|
||||
"domain": ".google.co.kr",
|
||||
"path": "/",
|
||||
"expires": 1791021744.708058,
|
||||
"httpOnly": true,
|
||||
"secure": true,
|
||||
"sameSite": "Lax"
|
||||
},
|
||||
{
|
||||
"name": "APISID",
|
||||
"value": "zGwlwp6_0opojmt1/AXZ6m5MWQ7TWQDjqa",
|
||||
"domain": ".google.co.kr",
|
||||
"path": "/",
|
||||
"expires": 1791021744.708087,
|
||||
"httpOnly": false,
|
||||
"secure": false,
|
||||
"sameSite": "Lax"
|
||||
},
|
||||
{
|
||||
"name": "SAPISID",
|
||||
"value": "TM8JfVIqtJ14hk-M/Aqu5S_2gUbGNCXygB",
|
||||
"domain": ".google.co.kr",
|
||||
"path": "/",
|
||||
"expires": 1791021744.708111,
|
||||
"httpOnly": false,
|
||||
"secure": true,
|
||||
"sameSite": "Lax"
|
||||
},
|
||||
{
|
||||
"name": "__Secure-1PAPISID",
|
||||
"value": "TM8JfVIqtJ14hk-M/Aqu5S_2gUbGNCXygB",
|
||||
"domain": ".google.co.kr",
|
||||
"path": "/",
|
||||
"expires": 1791021744.708164,
|
||||
"httpOnly": false,
|
||||
"secure": true,
|
||||
"sameSite": "Lax"
|
||||
},
|
||||
{
|
||||
"name": "__Secure-3PAPISID",
|
||||
"value": "TM8JfVIqtJ14hk-M/Aqu5S_2gUbGNCXygB",
|
||||
"domain": ".google.co.kr",
|
||||
"path": "/",
|
||||
"expires": 1791021744.708196,
|
||||
"httpOnly": false,
|
||||
"secure": true,
|
||||
"sameSite": "None"
|
||||
},
|
||||
{
|
||||
"name": "NID",
|
||||
"value": "525=mBsTIbxkGLvBaj6sIC47UgucSlUTNNupiGs3Z9UtaLOASpJEHluP5PKy3mNgF3HWl4ceeou3MZDkraKgx94R7I8my84pzm80c7BjGgIONz4A6OJYPh2w60eLfc4RR_NmfAhTidm_9GpUEUFFjcGdz_dqgkFdXaOluLSM8lwCWFUeEEA410r8qvHkopIHfoaUgQmpDIdIcU4h",
|
||||
"domain": ".google.co.kr",
|
||||
"path": "/",
|
||||
"expires": 1772272944.708222,
|
||||
"httpOnly": true,
|
||||
"secure": true,
|
||||
"sameSite": "None"
|
||||
},
|
||||
{
|
||||
"name": "SID",
|
||||
"value": "g.a0000wiGXo9BShFNNJ5CEkScDHAw9yQ9q3mAu2PO_EhHK1FgAgph3zzza1N12w1DaKWvsf15gwACgYKAYISARASFQHGX2MiH6Sk3xSSnRee_PJnGYLj8hoVAUF8yKppY931rQU6jPDD9hhHk42d0076",
|
||||
"domain": ".google.co.kr",
|
||||
"path": "/",
|
||||
"expires": 1791021744.70825,
|
||||
"httpOnly": false,
|
||||
"secure": false,
|
||||
"sameSite": "Lax"
|
||||
},
|
||||
{
|
||||
"name": "__Secure-1PSID",
|
||||
"value": "g.a0000wiGXo9BShFNNJ5CEkScDHAw9yQ9q3mAu2PO_EhHK1FgAgphER3yjvgRYRTknphnKewNLgACgYKAVcSARASFQHGX2MiMZdMKXIePglT17fwJmLVihoVAUF8yKqhvN2HJDp_L7-HNkQLrDmf0076",
|
||||
"domain": ".google.co.kr",
|
||||
"path": "/",
|
||||
"expires": 1791021744.708304,
|
||||
"httpOnly": true,
|
||||
"secure": true,
|
||||
"sameSite": "Lax"
|
||||
},
|
||||
{
|
||||
"name": "__Secure-3PSID",
|
||||
"value": "g.a0000wiGXo9BShFNNJ5CEkScDHAw9yQ9q3mAu2PO_EhHK1FgAgphMlymBwnbbRlMkbORx4z8MQACgYKAbUSARASFQHGX2MiQKVesqsgHy4eHxL-OR7BgRoVAUF8yKpOgg6OQHgzZV4Qpa7hC0hg0076",
|
||||
"domain": ".google.co.kr",
|
||||
"path": "/",
|
||||
"expires": 1791021744.70835,
|
||||
"httpOnly": true,
|
||||
"secure": true,
|
||||
"sameSite": "None"
|
||||
},
|
||||
{
|
||||
"name": "LOGIN_INFO",
|
||||
"value": "AFmmF2swRQIgOIxwpXy4aN9YMHVGzb74vububtlMfIa6OA-6x8x2JigCIQDU7tk_zyWY3rMMYvyrSEqq974-ew4aywy8rIYqHvgfsw:QUQ3MjNmd0xHaDBRNlRsUXRNTVhpRlpQTHZrTVVvNVZBOTR5RWRJbFM1dGFoRGVLWlZSbjJRUElFZnl6WGpLeFk4QzR5X0tkVXZEc0stLVVUc3NhYUk5TkxzeENuSjRQSjNLSU5iMDV5WjdjS0d2b0pqdmlZWHdoMjE2Ql9FTjl2RDJrR1FoSktQako5c2RYRVFMRk9kRlpSZ2RnVi14dC1n",
|
||||
"domain": ".youtube.com",
|
||||
"path": "/",
|
||||
"expires": 1791021744.906615,
|
||||
"httpOnly": true,
|
||||
"secure": true,
|
||||
"sameSite": "None"
|
||||
},
|
||||
{
|
||||
"name": "__Secure-ROLLOUT_TOKEN",
|
||||
"value": "CLKs_P69oYiQ0gEQk8ya2OGvjwMY8cjy8uGvjwM%3D",
|
||||
"domain": ".youtube.com",
|
||||
"path": "/",
|
||||
"expires": 1772013744.906735,
|
||||
"httpOnly": true,
|
||||
"secure": true,
|
||||
"sameSite": "None",
|
||||
"partitionKey": "https://youtube.com",
|
||||
"_crHasCrossSiteAncestor": false
|
||||
},
|
||||
{
|
||||
"name": "SIDCC",
|
||||
"value": "AKEyXzXZQRWpKLd8k9cS3rTWJP4Cdq7My0oBF5A6xLNSWr7iOQm6AqV_zpQqzJu1aBjCrwdE",
|
||||
"domain": ".youtube.com",
|
||||
"path": "/",
|
||||
"expires": 1787997749.909119,
|
||||
"httpOnly": false,
|
||||
"secure": false,
|
||||
"sameSite": "Lax"
|
||||
},
|
||||
{
|
||||
"name": "__Secure-1PSIDCC",
|
||||
"value": "AKEyXzXLRIVplx4l-12H6l7ENo-rUbihR3EJ_U4Wj-Yhy7QB_fWV9-MwLNl3xU1NPLqODO4y",
|
||||
"domain": ".youtube.com",
|
||||
"path": "/",
|
||||
"expires": 1787997749.909153,
|
||||
"httpOnly": true,
|
||||
"secure": true,
|
||||
"sameSite": "Lax"
|
||||
},
|
||||
{
|
||||
"name": "__Secure-3PSIDCC",
|
||||
"value": "AKEyXzWxSgoUitaYjKL8BGQnhJFS2O_iu4PF6VqhsFcqMAMGZ03pihnjyjF79GcK7m_L8SvmeA",
|
||||
"domain": ".youtube.com",
|
||||
"path": "/",
|
||||
"expires": 1787997749.909184,
|
||||
"httpOnly": true,
|
||||
"secure": true,
|
||||
"sameSite": "None"
|
||||
},
|
||||
{
|
||||
"name": "__Secure-3PSIDCC",
|
||||
"value": "AKEyXzVokd6aELk0J5t6MVN1-9qBJNOwikt-UAi2eHsRRjxHkLdGlbRkh_PgD5BrdTEC4Ong",
|
||||
"domain": ".google.com",
|
||||
"path": "/",
|
||||
"expires": 1787997776.889637,
|
||||
"httpOnly": true,
|
||||
"secure": true,
|
||||
"sameSite": "None"
|
||||
}
|
||||
],
|
||||
"origins": [
|
||||
{
|
||||
"origin": "https://studio.youtube.com",
|
||||
"localStorage": [
|
||||
{
|
||||
"name": "yt.innertube::nextId",
|
||||
"value": "{\"data\":3,\"expiration\":1756548150931,\"creation\":1756461750931}"
|
||||
},
|
||||
{
|
||||
"name": "yt.innertube::requests",
|
||||
"value": "{\"data\":{},\"expiration\":1756548158426,\"creation\":1756461758426}"
|
||||
},
|
||||
{
|
||||
"name": "ytidb::LAST_RESULT_ENTRY_KEY",
|
||||
"value": "{\"data\":{\"hasSucceededOnce\":true},\"expiration\":1759053746089,\"creation\":1756461746089}"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"origin": "https://accounts.youtube.com",
|
||||
"localStorage": [
|
||||
{
|
||||
"name": "nextRotationAttemptTs",
|
||||
"value": "1756462347315"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
4
parsingServer/package.json
Normal file
4
parsingServer/package.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"type": "module"
|
||||
}
|
||||
|
||||
503
parsingServer/server.js
Normal file
503
parsingServer/server.js
Normal file
@@ -0,0 +1,503 @@
|
||||
import http from 'http';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { parse } from 'csv-parse/sync';
|
||||
import playwright from 'playwright-extra';
|
||||
const { chromium } = playwright;
|
||||
import AdmZip from 'adm-zip';
|
||||
|
||||
// 전역 헤드리스 설정(환경변수 HEADLESS=true/false로 제어 가능) - 기본값: true
|
||||
const HEADLESS = process.env.HEADLESS ? process.env.HEADLESS !== 'false' : true;
|
||||
// ESM에서 __dirname 대체
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
// 최신 다운로드된 unzipped 폴더 경로 찾기
|
||||
function findLatestUnzippedDir(baseDir) {
|
||||
if (!fs.existsSync(baseDir)) return null;
|
||||
const entries = fs.readdirSync(baseDir)
|
||||
.map(name => ({ name, full: path.join(baseDir, name) }))
|
||||
.filter(e => fs.statSync(e.full).isDirectory() && /unzipped$/i.test(e.name))
|
||||
.sort((a, b) => fs.statSync(b.full).mtimeMs - fs.statSync(a.full).mtimeMs);
|
||||
return entries[0]?.full || null;
|
||||
}
|
||||
|
||||
// CSV 파일 선택(기본: '표 데이터.csv')
|
||||
function chooseCsvFile(unzipDir, preferredPattern = /표 데이터\.csv$/) {
|
||||
const files = fs.readdirSync(unzipDir).filter(f => f.toLowerCase().endsWith('.csv'));
|
||||
if (files.length === 0) return null;
|
||||
const picked = preferredPattern ? (files.find(f => preferredPattern.test(f)) || files[0]) : files[0];
|
||||
return path.join(unzipDir, picked);
|
||||
}
|
||||
|
||||
// CSV → JSON 배열 파싱
|
||||
function parseCsvToJson(csvPath) {
|
||||
const csvContent = fs.readFileSync(csvPath, 'utf-8');
|
||||
return parse(csvContent, {
|
||||
columns: true,
|
||||
skip_empty_lines: true,
|
||||
bom: true,
|
||||
relax_column_count: true,
|
||||
trim: true,
|
||||
});
|
||||
}
|
||||
|
||||
// 공통 JSON 응답 유틸
|
||||
function sendJson(res, status, obj, extraHeaders = {}) {
|
||||
res.writeHead(status, { 'Content-Type': 'application/json; charset=utf-8', 'Access-Control-Allow-Origin': '*', ...extraHeaders });
|
||||
res.end(JSON.stringify(obj));
|
||||
}
|
||||
|
||||
// 요청 본문(JSON) 파서
|
||||
function parseRequestBody(req) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let body = '';
|
||||
req.on('data', (chunk) => { body += chunk; });
|
||||
req.on('end', () => {
|
||||
try {
|
||||
resolve(body ? JSON.parse(body) : {});
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
req.on('error', reject);
|
||||
});
|
||||
}
|
||||
|
||||
// ====== 아래부터는 새로 내보내기를 수행하기 위한 자동화 유틸 ======
|
||||
function ensureDirExists(dirPath) {
|
||||
if (!fs.existsSync(dirPath)) {
|
||||
fs.mkdirSync(dirPath, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
const loadSession = async (filePath) => {
|
||||
const jsonData = fs.readFileSync(filePath, 'utf-8');
|
||||
return JSON.parse(jsonData);
|
||||
};
|
||||
|
||||
async function ensureChecked(dlg, page, label) {
|
||||
const cb = dlg.getByRole('checkbox', { name: label, exact: true });
|
||||
await cb.scrollIntoViewIfNeeded();
|
||||
let state = await cb.getAttribute('aria-checked');
|
||||
if (state !== 'true') {
|
||||
await cb.click({ force: true });
|
||||
for (let i = 0; i < 5; i++) {
|
||||
await page.waitForTimeout(100);
|
||||
state = await cb.getAttribute('aria-checked');
|
||||
if (state === 'true') break;
|
||||
}
|
||||
if (state !== 'true') throw new Error(`체크 실패: ${label}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function createBrowser() {
|
||||
const optionsBrowser = {
|
||||
headless: HEADLESS,
|
||||
args: [
|
||||
'--disable-blink-features=AutomationControlled',
|
||||
'--no-sandbox',
|
||||
'--disable-web-security',
|
||||
'--disable-infobars',
|
||||
'--disable-extensions',
|
||||
'--start-maximized',
|
||||
'--window-size=1280,720',
|
||||
],
|
||||
};
|
||||
return chromium.launch(optionsBrowser);
|
||||
}
|
||||
|
||||
async function createContext(browser, sessionState) {
|
||||
const optionsContext = {
|
||||
userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36',
|
||||
locale: 'ko-KR',
|
||||
viewport: { width: 1280, height: 720 },
|
||||
deviceScaleFactor: 1,
|
||||
acceptDownloads: true,
|
||||
storageState: sessionState,
|
||||
extraHTTPHeaders: {
|
||||
'sec-ch-ua': '"Not;A=Brand";v="99", "Google Chrome";v="139", "Chromium";v="139"',
|
||||
'sec-ch-ua-arch': '"arm"',
|
||||
'sec-ch-ua-bitness': '"64"',
|
||||
'sec-ch-ua-form-factors': '"Desktop"',
|
||||
'sec-ch-ua-full-version': '"139.0.7258.154"',
|
||||
'sec-ch-ua-full-version-list': '"Not;A=Brand";v="99.0.0.0", "Google Chrome";v="139.0.7258.154", "Chromium";v="139.0.7258.154"',
|
||||
'sec-ch-ua-mobile': '?0',
|
||||
'sec-ch-ua-model': '""',
|
||||
'sec-ch-ua-platform': '"macOS"',
|
||||
'sec-ch-ua-platform-version': '"15.6.1"',
|
||||
'sec-ch-ua-wow64': '?0',
|
||||
}
|
||||
};
|
||||
return browser.newContext(optionsContext);
|
||||
}
|
||||
|
||||
async function openAnalyticsAdvanced(page) {
|
||||
await page.goto('https://studio.youtube.com/');
|
||||
await page.locator('ytcp-navigation-drawer').getByRole('button', { name: '분석', exact: true }).click();
|
||||
await page.getByRole('link', { name: '고급 모드', exact: true }).click();
|
||||
}
|
||||
|
||||
function formatKoreanDateFromYMD(ymd) {
|
||||
// 입력: '20250901' → 출력: '2025. 9. 1.'
|
||||
if (!/^\d{8}$/.test(ymd)) return null;
|
||||
const yyyy = ymd.slice(0, 4);
|
||||
const mm = String(parseInt(ymd.slice(4, 6), 10));
|
||||
const dd = String(parseInt(ymd.slice(6, 8), 10));
|
||||
return `${yyyy}. ${mm}. ${dd}.`;
|
||||
}
|
||||
|
||||
// 입력칸의 기존 값을 지우고 새 값 입력
|
||||
async function clearAndType(inputLocator, page, value) {
|
||||
await inputLocator.click();
|
||||
await page.keyboard.press(process.platform === 'darwin' ? 'Meta+A' : 'Control+A');
|
||||
await page.keyboard.press('Backspace');
|
||||
await inputLocator.type(value);
|
||||
}
|
||||
|
||||
async function configureDateRangeSingleDay(page, ymdTarget) {
|
||||
await page.locator('yta-time-picker #picker-trigger ytcp-dropdown-trigger[role="button"]').click();
|
||||
await page.locator('tp-yt-paper-item[test-id="week"]').click();
|
||||
await page.locator('yta-time-picker #picker-trigger ytcp-dropdown-trigger[role="button"]').click();
|
||||
await page.locator('tp-yt-paper-item[test-id="fixed"]').click();
|
||||
|
||||
const caldlg = page.locator('tp-yt-paper-dialog:has(ytcp-date-period-picker)');
|
||||
await caldlg.waitFor({ state: 'visible' });
|
||||
const endInput = caldlg.locator('#end-date input');
|
||||
await endInput.waitFor({ state: 'visible' });
|
||||
|
||||
|
||||
|
||||
|
||||
let startVal;
|
||||
let endVal;
|
||||
if (ymdTarget === 'latest') {
|
||||
// 가장 최신 날짜(endInput 값)를 시작일에 복사하여 최신 하루 데이터 요청
|
||||
endVal = await endInput.inputValue();
|
||||
const startInput = caldlg.locator('#start-date input');
|
||||
await clearAndType(startInput, page, endVal);
|
||||
startVal = endVal;
|
||||
} else if (ymdTarget) {
|
||||
const formatted = formatKoreanDateFromYMD(ymdTarget);
|
||||
if (!formatted) throw new Error(`잘못된 날짜 형식입니다. (예: 20250901)`);
|
||||
// 시작/종료 모두 동일 날짜로 설정
|
||||
const startInput = caldlg.locator('#start-date input');
|
||||
await clearAndType(startInput, page, formatted);
|
||||
await clearAndType(endInput, page, formatted);
|
||||
startVal = formatted;
|
||||
endVal = formatted;
|
||||
} else {
|
||||
// 지정 날짜가 없으면 기존 종료일 값을 읽어 시작일에 복사
|
||||
endVal = await endInput.inputValue();
|
||||
const startInput = caldlg.locator('#start-date input');
|
||||
await clearAndType(startInput, page, endVal);
|
||||
startVal = endVal;
|
||||
}
|
||||
await caldlg.locator('#apply-button[aria-disabled="false"] button').click();
|
||||
return { startDate: startVal, endDate: endVal };
|
||||
}
|
||||
|
||||
async function configureMetrics(page) {
|
||||
await page.locator('yta-explore-column-picker-dropdown[title="측정항목"] ytcp-dropdown-trigger').click();
|
||||
const dlg = page.getByRole('dialog', { name: '측정항목' });
|
||||
await page.locator('h2.picker-text', { hasText: 'Premium' }).click();
|
||||
await dlg.getByRole('button', { name: '전체 선택 해제' }).click();
|
||||
await ensureChecked(dlg, page, '조회수');
|
||||
await ensureChecked(dlg, page, '유효 조회수');
|
||||
await ensureChecked(dlg, page, '시청 시간(단위: 시간)');
|
||||
await ensureChecked(dlg, page, 'YouTube Premium 조회수');
|
||||
await dlg.getByRole('button', { name: '적용' }).click();
|
||||
}
|
||||
|
||||
async function exportCsvAndExtract(page, downloadDir) {
|
||||
ensureDirExists(downloadDir);
|
||||
const [download] = await Promise.all([
|
||||
page.waitForEvent('download'),
|
||||
(async () => {
|
||||
await page.locator('ytcp-icon-button#export-button, ytcp-icon-button[aria-label="현재 화면 내보내기"]').click();
|
||||
await page.locator('tp-yt-paper-item[test-id="CSV"]').click();
|
||||
})()
|
||||
]);
|
||||
const suggested = download.suggestedFilename();
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||
const zipPath = path.join(downloadDir, `${timestamp}-${suggested || 'export.zip'}`);
|
||||
await download.saveAs(zipPath);
|
||||
const unzipDir = path.join(downloadDir, `${timestamp}-unzipped`);
|
||||
ensureDirExists(unzipDir);
|
||||
const zip = new AdmZip(zipPath);
|
||||
zip.extractAllTo(unzipDir, true);
|
||||
return { zipPath, unzipDir };
|
||||
}
|
||||
|
||||
async function runFreshExport(ymdTarget) {
|
||||
let browser;
|
||||
try {
|
||||
browser = await createBrowser();
|
||||
const sessionData = await loadSession(path.join(__dirname, 'everlogin.json'));
|
||||
const context = await createContext(browser, sessionData);
|
||||
const page = await context.newPage();
|
||||
await openAnalyticsAdvanced(page);
|
||||
const { startDate, endDate } = await configureDateRangeSingleDay(page, ymdTarget);
|
||||
await configureMetrics(page);
|
||||
const downloadDir = path.resolve(process.cwd(), 'downloads');
|
||||
const { unzipDir } = await exportCsvAndExtract(page, downloadDir);
|
||||
const csvPath = chooseCsvFile(unzipDir, /표 데이터\.csv$/);
|
||||
if (!csvPath) throw new Error('CSV 파일을 찾지 못했습니다.');
|
||||
const data = parseCsvToJson(csvPath);
|
||||
return { file: path.basename(csvPath), count: data.length, data, startDate, endDate };
|
||||
} finally {
|
||||
try { await browser?.close(); } catch {}
|
||||
}
|
||||
}
|
||||
|
||||
let isRunning = false;
|
||||
const server = http.createServer((req, res) => {
|
||||
// CORS preflight 처리
|
||||
if (req.method === 'OPTIONS') {
|
||||
res.writeHead(204, {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'GET,POST,OPTIONS',
|
||||
'Access-Control-Allow-Headers': 'Content-Type',
|
||||
});
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.url?.startsWith('/data')) {
|
||||
const urlObj = new URL(req.url, 'http://localhost');
|
||||
const ymd = urlObj.searchParams.get('date') || undefined;
|
||||
if (isRunning) {
|
||||
sendJson(res, 429, { error: '이미 요청 처리 중입니다. 잠시 후 다시 시도하세요.' });
|
||||
return;
|
||||
}
|
||||
isRunning = true;
|
||||
(async () => {
|
||||
try {
|
||||
const result = await runFreshExport(ymd);
|
||||
const payload = {
|
||||
...result,
|
||||
date: ymd === 'latest' ? result.endDate : (ymd || result.endDate),
|
||||
};
|
||||
sendJson(res, 200, payload);
|
||||
} catch (err) {
|
||||
console.error('[data] export error:', err);
|
||||
sendJson(res, 500, { error: String(err) });
|
||||
} finally {
|
||||
isRunning = false;
|
||||
}
|
||||
})();
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.url?.startsWith('/isCodeMatch')) {
|
||||
if (req.method !== 'POST') {
|
||||
sendJson(res, 405, { error: 'Method Not Allowed' }, { 'Allow': 'POST, OPTIONS' });
|
||||
return;
|
||||
}
|
||||
(async () => {
|
||||
try {
|
||||
const body = await parseRequestBody(req);
|
||||
if (!body || typeof body.handle !== 'string') {
|
||||
sendJson(res, 400, { error: 'Missing handle' });
|
||||
return;
|
||||
}
|
||||
if (!body || typeof body.code !== 'string') {
|
||||
sendJson(res, 400, { error: 'Missing code' });
|
||||
return;
|
||||
}
|
||||
|
||||
const handle = body.handle;
|
||||
const registerCode = body.code;
|
||||
console.log('[isCodeMatch] handle:', handle, 'code:', registerCode);
|
||||
// const registerCode = body.code; // 필요시 사용
|
||||
|
||||
// Playwright로 채널 페이지 접속 및 정보 추출
|
||||
let browser;
|
||||
try {
|
||||
browser = await chromium.launch({ headless: HEADLESS, args: ['--no-sandbox'] });
|
||||
const context = await browser.newContext();
|
||||
const page = await context.newPage();
|
||||
await page.goto(`https://www.youtube.com/${handle}`, { waitUntil: 'domcontentloaded', timeout: 20000 });
|
||||
|
||||
const header = page.locator('#page-header');
|
||||
const descSpan = header.locator('yt-description-preview-view-model .truncated-text-wiz__truncated-text-content span.yt-core-attributed-string').first();
|
||||
|
||||
let foundtext = null;
|
||||
try {
|
||||
await descSpan.waitFor({ state: 'visible', timeout: 3000 });
|
||||
foundtext = await descSpan.innerText();
|
||||
} catch {
|
||||
const descBtn = header.getByRole('button', { name: /^설명:/ });
|
||||
try {
|
||||
await descBtn.waitFor({ state: 'attached', timeout: 5000 });
|
||||
const aria = await descBtn.getAttribute('aria-label');
|
||||
if (aria) {
|
||||
const parts = aria.split('설명:');
|
||||
if (parts.length > 1) {
|
||||
const rest = parts[1];
|
||||
foundtext = rest.split('...', 1)[0].trim();
|
||||
} else {
|
||||
foundtext = aria;
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
// 아바타 이미지 src 추출
|
||||
let avatar = null;
|
||||
try {
|
||||
const img = header.locator('img[src^="https://yt3.googleusercontent.com/"]').first();
|
||||
await img.waitFor({ state: 'attached', timeout: 5000 });
|
||||
avatar = await img.getAttribute('src');
|
||||
} catch {}
|
||||
|
||||
await context.close();
|
||||
await browser.close();
|
||||
sendJson(res, 200, { success: true, foundtext, avatar });
|
||||
} catch (e) {
|
||||
try { await browser?.close(); } catch {}
|
||||
console.error('[isCodeMatch] playwright error:', e);
|
||||
sendJson(res, 400, { success: false, message: `${handle} connection failed` });
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[isCodeMatch] handler error:', e);
|
||||
sendJson(res, 400, { success: false, message: `unknown error ${String(e)}` });
|
||||
}
|
||||
})();
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.url?.startsWith('/gethandle')) {
|
||||
if (req.method !== 'POST') {
|
||||
sendJson(res, 405, { error: 'Method Not Allowed' }, { 'Allow': 'POST, OPTIONS' });
|
||||
return;
|
||||
}
|
||||
(async () => {
|
||||
try {
|
||||
const body = await parseRequestBody(req);
|
||||
const ids = Array.isArray(body?.ids) ? body.ids : [];
|
||||
console.log('[gethandle] received ids:', ids);
|
||||
|
||||
let browser;
|
||||
let context;
|
||||
let page;
|
||||
const results = [];
|
||||
try {
|
||||
browser = await chromium.launch({ headless: HEADLESS, args: ['--no-sandbox'] });
|
||||
context = await browser.newContext();
|
||||
page = await context.newPage();
|
||||
|
||||
for (const id of ids) {
|
||||
const url = `https://www.youtube.com/shorts/${id}`;
|
||||
let handleText = null;
|
||||
let avatar = null;
|
||||
try {
|
||||
console.log(`[gethandle] (${id}) goto shorts URL:`, url);
|
||||
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 20000 });
|
||||
|
||||
// 우선 채널 핸들 앵커( '/@' 로 시작) 탐색
|
||||
const handleAnchor = page.locator('ytd-reel-player-overlay-renderer a[href^="/@"]').first();
|
||||
try {
|
||||
console.log(`[gethandle] (${id}) try overlay handle selector: ytd-reel-player-overlay-renderer a[href^="/@"]`);
|
||||
await handleAnchor.waitFor({ state: 'visible', timeout: 5000 });
|
||||
handleText = (await handleAnchor.innerText())?.trim() || null;
|
||||
console.log(`[gethandle] (${id}) overlay handle found:`, handleText);
|
||||
} catch {
|
||||
// 대안: 텍스트가 '@'로 시작하는 앵커
|
||||
const alt = page.locator('a:has-text("@")').first();
|
||||
try {
|
||||
console.log(`[gethandle] (${id}) overlay handle not found, try alt anchor with '@' text`);
|
||||
await alt.waitFor({ state: 'attached', timeout: 4000 });
|
||||
const t = (await alt.innerText())?.trim();
|
||||
handleText = t && t.startsWith('@') ? t : handleText;
|
||||
if (handleText) console.log(`[gethandle] (${id}) alt handle found:`, handleText);
|
||||
} catch {}
|
||||
}
|
||||
|
||||
// 추가 대안: 메타패널 내부의 링크들 중 '@' 포함
|
||||
if (!handleText) {
|
||||
console.log(`[gethandle] (${id}) try metapanel handle extraction`);
|
||||
const metaLink = page.locator('#metapanel a').first();
|
||||
try {
|
||||
await metaLink.waitFor({ state: 'attached', timeout: 3000 });
|
||||
const t = (await metaLink.innerText())?.trim();
|
||||
handleText = t && t.startsWith('@') ? t : handleText;
|
||||
if (handleText) console.log(`[gethandle] (${id}) metapanel handle found:`, handleText);
|
||||
} catch {}
|
||||
}
|
||||
|
||||
// 최종 대안: 채널 이름 영역의 채널 링크로 이동 후 채널 페이지에서 @handle/아바타 추출
|
||||
if (!handleText) {
|
||||
try {
|
||||
console.log(`[gethandle] (${id}) overlay handle not found, try channel link route`);
|
||||
const channelNameAnchor = page.locator('ytd-channel-name a[href^="/channel/"]').first();
|
||||
await channelNameAnchor.waitFor({ state: 'attached', timeout: 4000 });
|
||||
const href = await channelNameAnchor.getAttribute('href');
|
||||
if (href) {
|
||||
const channelUrl = `https://www.youtube.com${href}`;
|
||||
console.log(`[gethandle] (${id}) navigate to channel URL:`, channelUrl);
|
||||
await page.goto(channelUrl, { waitUntil: 'domcontentloaded', timeout: 20000 });
|
||||
// 채널 핸들(@...)
|
||||
try {
|
||||
console.log(`[gethandle] (${id}) try channel page handle selector: a[href^="/@"]`);
|
||||
const chHandle = page.locator('a[href^="/@"]').first();
|
||||
await chHandle.waitFor({ state: 'visible', timeout: 5000 });
|
||||
const t = (await chHandle.innerText())?.trim();
|
||||
if (t && t.startsWith('@')) handleText = t;
|
||||
if (handleText) console.log(`[gethandle] (${id}) channel page handle found:`, handleText);
|
||||
} catch {}
|
||||
// 채널 아바타
|
||||
try {
|
||||
console.log(`[gethandle] (${id}) try channel page avatar selector: img[src*="yt3"]`);
|
||||
const chAvatar = page.locator('img[src*="yt3"], img[src*="yt3.ggpht.com"]').first();
|
||||
await chAvatar.waitFor({ state: 'attached', timeout: 4000 });
|
||||
const src = await chAvatar.getAttribute('src');
|
||||
if (src) avatar = src;
|
||||
if (avatar) console.log(`[gethandle] (${id}) channel page avatar found:`, avatar);
|
||||
} catch {}
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
// 아바타 이미지 추출(overlay 내 yt3 도메인 이미지)
|
||||
try {
|
||||
if (!avatar) console.log(`[gethandle] (${id}) try overlay avatar selector: ytd-reel-player-overlay-renderer img[src*="yt3"]`);
|
||||
const avatarImg = page.locator('ytd-reel-player-overlay-renderer img[src*="yt3"]').first();
|
||||
await avatarImg.waitFor({ state: 'attached', timeout: 4000 });
|
||||
avatar = await avatarImg.getAttribute('src');
|
||||
if (avatar) console.log(`[gethandle] (${id}) overlay avatar found:`, avatar);
|
||||
} catch {}
|
||||
|
||||
results.push({ id, url, handle: handleText, avatar });
|
||||
} catch (e) {
|
||||
console.error('[gethandle] navigate/parsing error for id', id, e);
|
||||
results.push({ id, url, error: 'navigation_failed' });
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
try { await page?.close(); } catch {}
|
||||
try { await context?.close(); } catch {}
|
||||
try { await browser?.close(); } catch {}
|
||||
}
|
||||
|
||||
sendJson(res, 200, { success: true, count: results.length, items: results });
|
||||
} catch (e) {
|
||||
console.error('[gethandle] handler error:', e);
|
||||
sendJson(res, 400, { success: false, message: `invalid body ${String(e)}` });
|
||||
}
|
||||
})();
|
||||
return;
|
||||
}
|
||||
|
||||
// 기본 루트
|
||||
res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' });
|
||||
res.end('OK. GET /data 로 CSV JSON을 가져오세요.');
|
||||
});
|
||||
|
||||
const PORT = process.env.PORT ? Number(process.env.PORT) : 9556;
|
||||
server.listen(PORT, () => {
|
||||
console.log(`HTTP server listening on http://localhost:${PORT}`);
|
||||
});
|
||||
|
||||
|
||||
2
prisma/migrations/20250908195249_new03/migration.sql
Normal file
2
prisma/migrations/20250908195249_new03/migration.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE `Handle` ADD COLUMN `avatar` VARCHAR(191) NOT NULL DEFAULT '';
|
||||
@@ -41,7 +41,8 @@ model RegisterChannel {
|
||||
model Handle {
|
||||
id String @id @default(uuid())
|
||||
handle String @unique
|
||||
constPerView Float @default(1)
|
||||
avatar String @default("")
|
||||
costPerView Float @default(1)
|
||||
// Relations
|
||||
contents Content[]
|
||||
// Many-to-many: a handle can belong to many users
|
||||
@@ -58,13 +59,13 @@ model Content{
|
||||
// premiumViews Int
|
||||
// watchTime Int
|
||||
// Relation: many contents belong to one handle
|
||||
handleId String
|
||||
handle Handle @relation(fields: [handleId], references: [id])
|
||||
handleId String?
|
||||
handle Handle? @relation(fields: [handleId], references: [id])
|
||||
// Back relation: one content has many day views
|
||||
dayViews contentDayView[]
|
||||
dayViews ContentDayView[]
|
||||
}
|
||||
|
||||
model contentDayView{
|
||||
model ContentDayView{
|
||||
id String @id @default(uuid())
|
||||
contentId String
|
||||
date DateTime
|
||||
|
||||
19
types/next-auth.d.ts
vendored
Normal file
19
types/next-auth.d.ts
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
import { DefaultSession } from "next-auth";
|
||||
|
||||
declare module "next-auth" {
|
||||
interface Session {
|
||||
user: {
|
||||
registerCode?: string | null;
|
||||
registerCodeDate?: string | null;
|
||||
} & DefaultSession["user"];
|
||||
}
|
||||
}
|
||||
|
||||
declare module "next-auth/jwt" {
|
||||
interface JWT {
|
||||
registerCode?: string | null;
|
||||
registerCodeDate?: string | null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user