This commit is contained in:
2025-09-09 00:15:08 +00:00
parent 0cce0322a1
commit fd34eb370f
30 changed files with 1953 additions and 35881 deletions

3
.gitignore vendored
View File

@@ -41,3 +41,6 @@ yarn-error.log*
next-env.d.ts
/app/generated/prisma
downloads

View File

@@ -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

View 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 })
}
}

View 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();
}
}

View 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()
}
}

View 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 });
}
}

View File

@@ -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 });
}
}

View File

@@ -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);
// }

View File

@@ -1,4 +1,4 @@
import { PrismaClient } from '../../generated/prisma/client';
import { PrismaClient } from '@/app/generated/prisma';
const prisma = new PrismaClient();

View 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 {}
}
}

View 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 })
}
}

View File

@@ -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 });
}
}

View File

@@ -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();
}
}

View File

@@ -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 });
}
}

View File

@@ -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
View File

@@ -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(),
},
});
}

File diff suppressed because it is too large Load Diff

View File

@@ -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 Id Handle
2 rM3ncw0Ptfg @everfactory-vv
3 crWGJmWxlnw @everfactory-vv
4 mcLo-NsO-NM @everfactory-vv
5 1OHjEOzTmv4 @everfactory-vv
6 Zjqg68TZdQ4 @everfactory-vv
7 rmLlvq99dTU @everfactory-vv
8 UVVZhU5uJIQ @everfactory-vv
9 Thhh8TYjmqQ @everfactory-vv
10 sMtkeSDDxB8 @everfactory-vv
11 K1Bm4kk3rQk @everfactory-vv
12 kbjc0uQ7qbg @everfactory-vv
13 mQcZCZwqvxQ @everfactory-vv
14 7PtmMeek6L8 @everfactory-vv
15 2xVTPCwDVgU @everfactory-vv
16 YEPMCEt1ecc @everfactory-vv
17 PmkxScRvHe8 @everfactory-vv
18 nhQ9A6XEYDs @everfactory-vv
19 5o7uOU2DMyU @everfactory-vv
20 ENu1Z1E_NoY @everfactory-vv
21 fAAaXBn9O-U @everfactory-vv
22 zvAVfjijiCk @everfactory-vv

View File

@@ -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 Id Handle
2 nuahXCZWXPo @한컵레시피
3 lwc_pIdJY8I @한컵레시피
4 loJ_rxbqrZU @한컵레시피
5 NnkNOeMfM5c @한컵레시피
6 wNdv9WcFmQ0 @한컵레시피
7 CWXePp-Xrm4 @한컵레시피
8 RXTrXVRTfv0 @한컵레시피
9 sM0i4naEPLM @한컵레시피
10 ZG_6QcaH91U @한컵레시피
11 -dxXNlgUOXM @한컵레시피
12 6531T9LmSkE @한컵레시피
13 6KmmHx-Oy6I @한컵레시피
14 37VUCIY9fLU @한컵레시피
15 xkYFC--fHN0 @한컵레시피
16 HfcKOEiRZ-o @한컵레시피
17 B6DoyRp5mmw @한컵레시피
18 M7gkSmNfxno @한컵레시피
19 0tRjttSqmso @한컵레시피
20 UmR8_sqm7s8 @한컵레시피
21 xWCnExggxSQ @한컵레시피
22 cMiHkXLA_Uo @한컵레시피
23 goFwS_M7gCU @한컵레시피
24 ZkopiYZ4axA @한컵레시피
25 6zVcsa75DBM @한컵레시피
26 mgTf6dtRWhU @한컵레시피
27 j21gZFMCbro @한컵레시피
28 pKzVoZMUjrc @한컵레시피
29 0yaQSG6H35A @한컵레시피
30 N7Af6IBj6N8 @한컵레시피
31 2oEHULzBpe4 @한컵레시피
32 ksJf608acRw @한컵레시피
33 LDIUV4rsowU @한컵레시피
34 J_ej2T9U3yk @한컵레시피
35 ZwKoCzm_Y9E @한컵레시피
36 USUsaLn1QKw @한컵레시피
37 GRe70V7-vZo @한컵레시피
38 9FZtZhY3wv4 @한컵레시피
39 ws2CWeClmGE @한컵레시피
40 2QcNVPEg1Z8 @한컵레시피
41 wGPOtOKO9G4 @한컵레시피
42 MEgFyAwyruw @한컵레시피
43 FGod3w0byx0 @한컵레시피
44 uv2To4Tpy40 @한컵레시피
45 eFXkacofqtM @한컵레시피
46 ITeGIOsFSws @한컵레시피
47 tTivGZggRDk @한컵레시피
48 rM3ncw0Ptfg @everfactory-vv
49 crWGJmWxlnw @everfactory-vv
50 mcLo-NsO-NM @everfactory-vv
51 1OHjEOzTmv4 @everfactory-vv
52 Zjqg68TZdQ4 @everfactory-vv
53 rmLlvq99dTU @everfactory-vv
54 UVVZhU5uJIQ @everfactory-vv
55 Thhh8TYjmqQ @everfactory-vv
56 sMtkeSDDxB8 @everfactory-vv
57 K1Bm4kk3rQk @everfactory-vv
58 kbjc0uQ7qbg @everfactory-vv
59 mQcZCZwqvxQ @everfactory-vv
60 7PtmMeek6L8 @everfactory-vv
61 2xVTPCwDVgU @everfactory-vv
62 YEPMCEt1ecc @everfactory-vv
63 PmkxScRvHe8 @everfactory-vv
64 nhQ9A6XEYDs @everfactory-vv
65 5o7uOU2DMyU @everfactory-vv
66 ENu1Z1E_NoY @everfactory-vv
67 fAAaXBn9O-U @everfactory-vv
68 zvAVfjijiCk @everfactory-vv

View File

@@ -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
1 Id subject pubDate views validViews premiumViews watchTime
2 nuahXCZWXPo 컵으로 오레오쿠키케익 만들기 - 한끼_Cup Jun 10, 2025 32709 15705 3301 101.6509
3 lwc_pIdJY8I 컵하나로 콘치즈버터 만들기 - 한끼_Cup May 13, 2025 37409 15258 6989 88.2174
4 loJ_rxbqrZU 컵으로 엑설런트 크림 브륄레 만들기 - 한끼_Cup Jun 1, 2025 28762 14661 8808 89.384
5 NnkNOeMfM5c 컵하나로 소떡소떡 만들기 - 한끼_Cup May 15, 2025 28159 13879 2501 72.1229
6 wNdv9WcFmQ0 컵으로 떠먹는 컵피자 만들기 - 한끼_Cup May 18, 2025 25318 12658 6877 97.5433
7 CWXePp-Xrm4 컵으로 전남친 토스트 만들기 - 한끼_Cup May 17, 2025 24187 11821 6116 65.9049
8 RXTrXVRTfv0 컵으로 오레오쫀득쿠키 만들기 - 한끼_Cup Jun 14, 2025 20564 11464 5332 78.9794
9 sM0i4naEPLM 컵으로 연유초콜릿 만들기 - 한끼_Cup May 27, 2025 23864 10652 6664 60.2018
10 ZG_6QcaH91U 단호박 에그슬럿 만들기 -한끼_Cup 밤호박,단호박 Jul 31, 2025 24272 10497 2126 143.3451
11 -dxXNlgUOXM 컵으로 로투스 치즈케익 만들기 - 한끼_Cup Jun 12, 2025 13613 6394 3355 41.5752
12 6531T9LmSkE 컵으로 고구마치즈브리또 만들기 - 한끼_Cup May 29, 2025 12189 5698 3441 37.9903
13 6KmmHx-Oy6I 컵으로 또띠아 피자 브리또 만들기 - 한끼_Cup Jun 5, 2025 10749 5605 4515 32.2854
14 37VUCIY9fLU 더블치즈햄토스트 만들기 Aug 19, 2025 11421 4947 3486 40.2133
15 xkYFC--fHN0 컵으로 치즈제육 민들기 - 한끼_Cup May 25, 2025 10109 4723 3743 31.8623
16 HfcKOEiRZ-o 정숭제햄치즈롤 만들기 Aug 21, 2025 6045 3909 1350 22.48
17 B6DoyRp5mmw 깐풍만두 만들기 - 한끼_Cup Aug 2, 2025 7825 3888 1956 39.6999
18 M7gkSmNfxno 구운두부 강정 만들기 - 한끼_Cup Jul 24, 2025 6064 2654 772 33.9777
19 0tRjttSqmso 소세지 또띠아 만들기 -한끼_Cup Jul 29, 2025 5443 2574 1580 18.0177
20 UmR8_sqm7s8 딸기모찌 민들기 - 한끼Cup #여름딸기 Aug 5, 2025 4470 2476 674 20.4254
21 xWCnExggxSQ 만두 그라탕 만들기 - 한끼_Cup #물만두 Aug 14, 2025 4760 2365 1256 29.0473
22 cMiHkXLA_Uo 컵으로 오징어볶음 만들기 - 한끼_Cup May 31, 2025 4853 2287 1602 17.6316
23 goFwS_M7gCU 컵으로 초코제티브라우니 만들기 - 한끼_Cup Jun 17, 2025 5432 2125 1131 11.9966
24 ZkopiYZ4axA 컵으로 다이제 에그타르트 만들기 - 한끼_Cup Jul 3, 2025 4987 2103 1750 16.5978
25 6zVcsa75DBM 컵으로 마시멜로우 초코타르트 만들기 - 한끼_Cup Jun 21, 2025 3964 2094 731 15.9976
26 mgTf6dtRWhU 컵으로 초코칩 쿠키 만들기 - 한끼_Cup Jun 19, 2025 4617 1972 1444 11.9979
27 j21gZFMCbro 컵으로 닭갈비 만들기 - 한끼_Cup Jun 3, 2025 4096 1648 1651 11.8163
28 pKzVoZMUjrc 컵으로 팽이부추베이컨말이 만들기 - 한끼_Cup May 21, 2025 4127 1592 990 12.1174
29 0yaQSG6H35A 새우 멘보샤 만들기 -한끼_Cup #새우요리 #멘보샤 Aug 12, 2025 3664 1555 1014 15.8445
30 N7Af6IBj6N8 베이컨에그토스트 만들기 -한끼_Cup Aug 5, 2025 3585 1522 914 15.7509
31 2oEHULzBpe4 마늘쫑무침 만들기 - 한끼_Cup Jul 22, 2025 3114 1410 276 19.9879
32 ksJf608acRw 컵으로 컵 티라미슈 만들기 - 한끼_Cup May 24, 2025 2920 1203 729 7.4371
33 LDIUV4rsowU 치킨마요 덮밥 만들기-한끼_Cup Jul 26, 2025 2362 755 629 5.9985
34 J_ej2T9U3yk 컵으로 치즈오븐 스파게티 만들기 - 한끼_Cup Jul 10, 2025 2211 687 668 4.8089
35 ZwKoCzm_Y9E 컵으로 파스타 치즈떡볶이 만들기 - 한끼_Cup Jun 24, 2025 1476 623 358 4.6206
36 USUsaLn1QKw 더덕구이 만들기 Grilled deodeok -한끼_Cup #더덕구이 Aug 16, 2025 1867 593 622 3.7365
37 GRe70V7-vZo 베이컨치즈토스트 민들기 - 한끼_Cup Aug 5, 2025 1426 586 493 3.4593
38 9FZtZhY3wv4 컵으로 다이어트 초코바나나케익 만들기 - 한끼_Cup Jun 7, 2025 2052 572 595 3.4588
39 ws2CWeClmGE 허니콤보치킨 만들기 - 한끼_Cup Jun 28, 2025 1721 567 487 3.6471
40 2QcNVPEg1Z8 에그마요 베이컨브레드 만들기 - 한끼_Cup Jul 8, 2025 1900 566 477 4.2135
41 wGPOtOKO9G4 식빵치즈핫도그 만들기 - 한끼_Cup #식빵핫도그,#핫도그만들기 Aug 9, 2025 1778 562 481 4.8828
42 MEgFyAwyruw 서울 고속도로에 미확인 괴생명체 출몰 Aug 20, 2025 1331 538 256 2.7627
43 FGod3w0byx0 컵으로 마파두부 만들기 - 한끼_Cup May 20, 2025 1913 475 538 2.8631
44 uv2To4Tpy40 양송이 게살 치즈구이 만들기 - 한끼_Cup Jul 5, 2025 1371 370 404 2.3947
45 eFXkacofqtM 컵으로 밤티라미슈 만들기 - 한끼_Cup Jun 26, 2025 971 360 269 2.6383
46 ITeGIOsFSws 불닭팽이버섯치즈쌈 만들기 - 한끼_Cup #팽이버섯,#불닭볶음면 Aug 7, 2025 440 228 81 1.9858
47 tTivGZggRDk 통마늘 닭똥집 파채구이 만들기 - 한끼_Cup Jul 1, 2025 470 111 141 0.9671
48 rM3ncw0Ptfg ZzuB Mar 10, 2025 46 46 16 0.2592
49 crWGJmWxlnw Twinkle water Mar 10, 2025 26 26 4 0.1799
50 mcLo-NsO-NM merry hill Apr 28, 2025 21 21 6 0.2222
51 1OHjEOzTmv4 ZzuB Mar 10, 2025 20 20 15 0.0852
52 Zjqg68TZdQ4 Raccoon dance Mar 10, 2025 19 19 12 0.0768
53 rmLlvq99dTU 2025년 8월 22일 Aug 22, 2025 68 19 17 0.079
54 UVVZhU5uJIQ Twinkle water Mar 10, 2025 16 16 7 0.1588
55 Thhh8TYjmqQ Raccoon dance Mar 10, 2025 14 14 6 0.0637
56 sMtkeSDDxB8 Smile king Jun 22, 2025 12 12 2 0.0717
57 K1Bm4kk3rQk merry hill Apr 29, 2025 11 11 3 0.0336
58 kbjc0uQ7qbg Lilac love Apr 29, 2025 9 9 1 0.0508
59 mQcZCZwqvxQ select romance Apr 29, 2025 9 9 1 0.0413
60 7PtmMeek6L8 The Flight Thief Apr 28, 2025 8 8 5 0.1007
61 2xVTPCwDVgU select romance Apr 28, 2025 7 7 4 0.1054
62 YEPMCEt1ecc Lilac love Apr 28, 2025 7 7 5 0.133
63 PmkxScRvHe8 Light Waltz Apr 28, 2025 6 6 5 0.1103
64 nhQ9A6XEYDs light waltz Apr 29, 2025 6 6 1 0.0275
65 5o7uOU2DMyU The Flight Thief Apr 29, 2025 5 5 2 0.0181
66 ENu1Z1E_NoY Sunny afternoon Jun 27, 2025 4 4 1 0.0107
67 fAAaXBn9O-U Sunny afternoon Jun 27, 2025 3 3 1 0.0275
68 zvAVfjijiCk Smile king Jun 22, 2025 3 3 0 0.0247

74
datas/to0831.csv Normal file
View 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,
1 콘텐츠 동영상 제목 동영상 게시 시간 길이 조회수 시청 시간(단위: 시간) 노출수 노출 클릭률 (%)
2 lwc_pIdJY8I 컵하나로 콘치즈버터 만들기 - 한끼_Cup May 13, 2025 18 37435 88.2594 4201 7.76
3 nuahXCZWXPo 컵으로 오레오쿠키케익 만들기 - 한끼_Cup Jun 10, 2025 25 32733 101.7176 2706 4.25
4 loJ_rxbqrZU 컵으로 엑설런트 크림 브륄레 만들기 - 한끼_Cup Jun 1, 2025 23 28871 89.9535 12390 4.67
5 NnkNOeMfM5c 컵하나로 소떡소떡 만들기 - 한끼_Cup May 15, 2025 17 28169 72.1403 3526 4.74
6 wNdv9WcFmQ0 컵으로 떠먹는 컵피자 만들기 - 한끼_Cup May 18, 2025 31 25373 97.8292 4033 5.53
7 ZG_6QcaH91U 단호박 에그슬럿 만들기 -한끼_Cup 밤호박,단호박 Jul 31, 2025 41 24281 143.3622 1744 5.33
8 CWXePp-Xrm4 컵으로 전남친 토스트 만들기 - 한끼_Cup May 17, 2025 18 24197 65.9174 2653 3.43
9 sM0i4naEPLM 컵으로 연유초콜릿 만들기 - 한끼_Cup May 27, 2025 19 23870 60.2184 2149 2.51
10 RXTrXVRTfv0 컵으로 오레오쫀득쿠키 만들기 - 한끼_Cup Jun 14, 2025 26 20571 79.0032 3341 3.83
11 37VUCIY9fLU 더블치즈햄토스트 만들기 Aug 19, 2025 32 14747 50.556 715 5.73
12 niZYAgpFcYc 메론킥으로 컵케익 만들게 Aug 28, 2025 23 14239 46.0611 580 8.1
13 HfcKOEiRZ-o 정숭제햄치즈롤 만들기 Aug 21, 2025 31 13873 56.3047 590 5.93
14 -dxXNlgUOXM 컵으로 로투스 치즈케익 만들기 - 한끼_Cup Jun 12, 2025 29 13621 41.6008 2664 2.06
15 jyzX8UBWgJE 감자로 치즈고르케 만들게 Aug 30, 2025 24 13224 45.9606 259 10.81
16 6531T9LmSkE 컵으로 고구마치즈브리또 만들기 - 한끼_Cup May 29, 2025 20 12208 38.0439 3304 2.72
17 6KmmHx-Oy6I 컵으로 또띠아 피자 브리또 만들기 - 한끼_Cup Jun 5, 2025 21 10772 32.354 2712 3.69
18 xkYFC--fHN0 컵으로 치즈제육 민들기 - 한끼_Cup May 25, 2025 27 10115 31.8811 1425 3.51
19 B6DoyRp5mmw 깐풍만두 만들기 - 한끼_Cup Aug 2, 2025 44 7845 39.7734 2004 2.2
20 M7gkSmNfxno 구운두부 강정 만들기 - 한끼_Cup Jul 24, 2025 41 6073 34.1498 624 1.44
21 0tRjttSqmso 소세지 또띠아 만들기 -한끼_Cup Jul 29, 2025 37 5458 18.0715 735 3.95
22 goFwS_M7gCU 컵으로 초코제티브라우니 만들기 - 한끼_Cup Jun 17, 2025 16 5440 12.0064 1145 3.14
23 ZkopiYZ4axA 컵으로 다이제 에그타르트 만들기 - 한끼_Cup Jul 3, 2025 45 5029 16.8321 2042 5.88
24 cMiHkXLA_Uo 컵으로 오징어볶음 만들기 - 한끼_Cup May 31, 2025 32 4858 17.6495 1059 3.4
25 xWCnExggxSQ 만두 그라탕 만들기 - 한끼_Cup #물만두 Aug 14, 2025 48 4855 29.277 654 3.52
26 mgTf6dtRWhU 컵으로 초코칩 쿠키 만들기 - 한끼_Cup Jun 19, 2025 25 4628 12.0034 1550 1.68
27 UmR8_sqm7s8 딸기모찌 민들기 - 한끼Cup #여름딸기 Aug 5, 2025 48 4485 20.4986 713 2.52
28 pKzVoZMUjrc 컵으로 팽이부추베이컨말이 만들기 - 한끼_Cup May 21, 2025 22 4142 12.1568 2605 3.26
29 j21gZFMCbro 컵으로 닭갈비 만들기 - 한끼_Cup Jun 3, 2025 29 4099 11.8247 1381 2.82
30 6zVcsa75DBM 컵으로 마시멜로우 초코타르트 만들기 - 한끼_Cup Jun 21, 2025 42 3967 16.0031 949 2.63
31 0yaQSG6H35A 새우 멘보샤 만들기 -한끼_Cup #새우요리 #멘보샤 Aug 12, 2025 55 3733 16.0053 860 2.56
32 N7Af6IBj6N8 베이컨에그토스트 만들기 -한끼_Cup Aug 5, 2025 41 3605 15.8132 591 3.38
33 2oEHULzBpe4 마늘쫑무침 만들기 - 한끼_Cup Jul 22, 2025 42 3122 20.0238 654 4.59
34 ksJf608acRw 컵으로 컵 티라미슈 만들기 - 한끼_Cup May 24, 2025 22 2924 7.442 1321 3.63
35 LDIUV4rsowU 치킨마요 덮밥 만들기-한끼_Cup Jul 26, 2025 51 2450 6.2148 1361 3.09
36 J_ej2T9U3yk 컵으로 치즈오븐 스파게티 만들기 - 한끼_Cup Jul 10, 2025 41 2228 4.8985 981 3.06
37 9FZtZhY3wv4 컵으로 다이어트 초코바나나케익 만들기 - 한끼_Cup Jun 7, 2025 29 2058 3.4794 3364 2.2
38 USUsaLn1QKw 더덕구이 만들기 Grilled deodeok -한끼_Cup #더덕구이 Aug 16, 2025 46 1920 3.9045 550 4.91
39 FGod3w0byx0 컵으로 마파두부 만들기 - 한끼_Cup May 20, 2025 20 1919 2.8747 2618 2.41
40 2QcNVPEg1Z8 에그마요 베이컨브레드 만들기 - 한끼_Cup Jul 8, 2025 41 1903 4.2355 586 1.37
41 wGPOtOKO9G4 식빵치즈핫도그 만들기 - 한끼_Cup #식빵핫도그,#핫도그만들기 Aug 9, 2025 49 1797 4.9541 1068 1.59
42 JA4mmsYRsRg 바나나킥 컵케익 만들기 Aug 23, 2025 36 1723 3.6647 457 7
43 ws2CWeClmGE 허니콤보치킨 만들기 - 한끼_Cup Jun 28, 2025 46 1722 3.6661 712 2.67
44 -XiYk7gu-pU 또띠아시카고피자 만들기 Aug 26, 2025 33 1709 2.8246 290 9.31
45 ZwKoCzm_Y9E 컵으로 파스타 치즈떡볶이 만들기 - 한끼_Cup Jun 24, 2025 43 1479 4.6369 817 3.92
46 GRe70V7-vZo 베이컨치즈토스트 민들기 - 한끼_Cup Aug 5, 2025 38 1440 3.4979 547 3.11
47 uv2To4Tpy40 양송이 게살 치즈구이 만들기 - 한끼_Cup Jul 5, 2025 40 1372 2.4023 630 2.86
48 MEgFyAwyruw 서울 고속도로에 미확인 괴생명체 출몰 Aug 20, 2025 16 1335 2.7705 43 2.33
49 eFXkacofqtM 컵으로 밤티라미슈 만들기 - 한끼_Cup Jun 26, 2025 41 973 2.6399 579 2.42
50 tTivGZggRDk 통마늘 닭똥집 파채구이 만들기 - 한끼_Cup Jul 1, 2025 48 472 0.9817 490 2.86
51 ITeGIOsFSws 불닭팽이버섯치즈쌈 만들기 - 한끼_Cup #팽이버섯,#불닭볶음면 Aug 7, 2025 52 459 2.0423 1619 2.53
52 rmLlvq99dTU 똑똑한망치 Aug 22, 2025 12 274 0.2549 8 25
53 m57Bt6_8MAM Step to Success Aug 31, 2025 85 16 0.0696 2763 0.51
54 rM3ncw0Ptfg ZzuB Mar 10, 2025 80 12 0.0175 48 6.25
55 sMtkeSDDxB8 Smile king Jun 22, 2025 103 12 0.0717 734 1.23
56 K1Bm4kk3rQk merry hill Apr 29, 2025 151 9 0.029 27 25.93
57 Thhh8TYjmqQ Raccoon dance Mar 10, 2025 89 7 0.0297 27 22.22
58 mQcZCZwqvxQ select romance Apr 29, 2025 99 7 0.0237 50 12
59 1OHjEOzTmv4 ZzuB Mar 10, 2025 80 5 0.0343 24 12.5
60 ENu1Z1E_NoY Sunny afternoon Jun 27, 2025 83 5 0.0163 73 4.11
61 Zjqg68TZdQ4 Raccoon dance Mar 10, 2025 88 5 0.0291 12 0
62 crWGJmWxlnw Twinkle water Mar 10, 2025 83 5 0.0073 57 7.02
63 kbjc0uQ7qbg Lilac love Apr 29, 2025 95 5 0.0401 120 2.5
64 nhQ9A6XEYDs light waltz Apr 29, 2025 84 5 0.0151 48 8.33
65 2xVTPCwDVgU select romance Apr 28, 2025 102 4 0.0749 14 7.14
66 7PtmMeek6L8 The Flight Thief Apr 28, 2025 79 4 0.0655 17 0
67 UVVZhU5uJIQ Twinkle water Mar 10, 2025 83 4 0.0479 31 0
68 YEPMCEt1ecc Lilac love Apr 28, 2025 95 4 0.104 105 0
69 fAAaXBn9O-U Sunny afternoon Jun 27, 2025 82 4 0.0503 46 4.35
70 zvAVfjijiCk Smile king Jun 22, 2025 103 4 0.0307 63 3.17
71 5o7uOU2DMyU The Flight Thief Apr 29, 2025 79 3 0.011 30 10
72 PmkxScRvHe8 Light Waltz Apr 28, 2025 80 3 0.0615 18 0
73 mcLo-NsO-NM merry hill Apr 28, 2025 151 3 0.0759 20 0
74 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
View File

@@ -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",

View File

@@ -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"
}
}

View 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"
}
]
}
]
}

View File

@@ -0,0 +1,4 @@
{
"type": "module"
}

503
parsingServer/server.js Normal file
View 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}`);
});

View File

@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE `Handle` ADD COLUMN `avatar` VARCHAR(191) NOT NULL DEFAULT '';

View File

@@ -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
View 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;
}
}