middle 2
This commit is contained in:
@@ -1,2 +1,3 @@
|
||||
export const runtime = 'nodejs'
|
||||
import { handlers } from "@/auth" // Referring to the auth.ts we just created
|
||||
export const { GET, POST } = handlers
|
||||
60
app/api/channel/delete/route.ts
Normal file
60
app/api/channel/delete/route.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
export const runtime = 'nodejs'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { auth } from '@/auth'
|
||||
import { PrismaClient } from '@/app/generated/prisma'
|
||||
|
||||
// 연결 해제: 세션 사용자 ↔ 주어진 핸들의 매핑만 제거 (핸들 삭제 X)
|
||||
export async function GET(request: Request) {
|
||||
const session = await auth()
|
||||
if (!session) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const { searchParams } = new URL(request.url)
|
||||
const handle = searchParams.get('handle')
|
||||
if (!handle) {
|
||||
return NextResponse.json({ error: '핸들이 필요합니다' }, { status: 400 })
|
||||
}
|
||||
|
||||
const prisma = new PrismaClient()
|
||||
try {
|
||||
const email = session.user?.email ?? ''
|
||||
const user = await prisma.user.findFirst({ where: { email }, select: { id: true } })
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: '사용자를 찾을 수 없습니다.' }, { status: 404 })
|
||||
}
|
||||
|
||||
const handleRow = await prisma.handle.findUnique({ where: { handle } })
|
||||
if (!handleRow) {
|
||||
return NextResponse.json({ error: '핸들을 찾을 수 없습니다.' }, { status: 404 })
|
||||
}
|
||||
|
||||
// 이미 연결되어 있는지 확인
|
||||
const linked = await prisma.handle.findFirst({
|
||||
where: { id: handleRow.id, users: { some: { id: user.id } } },
|
||||
select: { id: true },
|
||||
})
|
||||
|
||||
if (!linked) {
|
||||
return NextResponse.json({ success: true, message: '이미 연결되지 않은 채널입니다.' }, { status: 200 })
|
||||
}
|
||||
|
||||
// 연결 해제
|
||||
await prisma.user.update({
|
||||
where: { id: user.id },
|
||||
data: { handles: { disconnect: { id: handleRow.id } } },
|
||||
})
|
||||
|
||||
return NextResponse.json({ success: true, message: '채널 연결을 해제했습니다.' }, { status: 200 })
|
||||
} finally {
|
||||
// prisma 종료
|
||||
try { await (prisma as any).$disconnect() } catch {}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('channel/delete 오류:', e)
|
||||
return NextResponse.json({ error: '요청 처리 실패' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
61
app/api/channel/list/route.ts
Normal file
61
app/api/channel/list/route.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
export const runtime = 'nodejs'
|
||||
import { NextResponse } from 'next/server';
|
||||
import { auth } from '@/auth';
|
||||
import { PrismaClient } from '@/app/generated/prisma';
|
||||
|
||||
export async function GET() {
|
||||
const session = await auth();
|
||||
if (!session) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
const email = session.user?.email as string | undefined;
|
||||
if (!email) {
|
||||
return NextResponse.json({ error: '세션 이메일을 찾을 수 없습니다' }, { status: 400 });
|
||||
}
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
try {
|
||||
// Admin: return all handles
|
||||
if (email === 'wsx204@naver.com') {
|
||||
const all = await prisma.handle.findMany({
|
||||
orderBy: { handle: 'asc' },
|
||||
select: { id: true, handle: true, avatar: true }
|
||||
});
|
||||
const items = all.map(h => ({
|
||||
id: h.id,
|
||||
handle: h.handle,
|
||||
createtime: new Date().toISOString(),
|
||||
is_approved: false,
|
||||
icon: h.avatar,
|
||||
}));
|
||||
return NextResponse.json({ items });
|
||||
}
|
||||
|
||||
// Non-admin: handles linked to session user
|
||||
const user = await prisma.user.findFirst({ where: { email }, select: { id: true } });
|
||||
if (!user) {
|
||||
return NextResponse.json({ items: [] });
|
||||
}
|
||||
const linked = await prisma.handle.findMany({
|
||||
where: { users: { some: { id: user.id } } },
|
||||
orderBy: { handle: 'asc' },
|
||||
select: { id: true, handle: true, avatar: true }
|
||||
});
|
||||
const items = linked.map(h => ({
|
||||
id: h.id,
|
||||
handle: h.handle,
|
||||
createtime: new Date().toISOString(),
|
||||
is_approved: false,
|
||||
icon: h.avatar,
|
||||
}));
|
||||
return NextResponse.json({ items });
|
||||
} catch (e) {
|
||||
console.error('list_channel 오류:', e);
|
||||
return NextResponse.json({ error: '조회 실패' }, { status: 500 });
|
||||
} finally {
|
||||
await prisma.$disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
33
app/api/channel/mycode/route.ts
Normal file
33
app/api/channel/mycode/route.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
export const runtime = 'nodejs'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { auth } from '@/auth'
|
||||
import { PrismaClient } from '@/app/generated/prisma'
|
||||
|
||||
export async function GET() {
|
||||
console.log('mycode 요청')
|
||||
const session = await auth()
|
||||
if (!session) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const email = session.user?.email as string | undefined
|
||||
if (!email) {
|
||||
return NextResponse.json({ error: '세션 이메일을 찾을 수 없습니다' }, { status: 400 })
|
||||
}
|
||||
|
||||
const prisma = new PrismaClient()
|
||||
try {
|
||||
const user = await prisma.user.findFirst({
|
||||
where: { email },
|
||||
select: { RegisgerCode: true },
|
||||
})
|
||||
return NextResponse.json({ registerCode: user?.RegisgerCode ?? null })
|
||||
} catch (e) {
|
||||
console.error('register_code 조회 오류:', e)
|
||||
return NextResponse.json({ error: '조회 실패' }, { status: 500 })
|
||||
} finally {
|
||||
await prisma.$disconnect()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
112
app/api/channel/register/route.ts
Normal file
112
app/api/channel/register/route.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
export const runtime = 'nodejs'
|
||||
import { NextResponse } from 'next/server';
|
||||
import { auth } from '@/auth';
|
||||
import { PrismaClient } from '@/app/generated/prisma';
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const session = await auth();
|
||||
if (!session) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
try {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const handle = searchParams.get('handle');
|
||||
// 안전하게 DB에서 최신 registerCode 조회 (세션 의존 제거)
|
||||
const prisma = new PrismaClient();
|
||||
const me = await prisma.user.findFirst({ where: { email: session.user?.email ?? '' }, select: { RegisgerCode: true } });
|
||||
const registerCode = me?.RegisgerCode ?? undefined;
|
||||
console.log(`registerCode: ${registerCode} and handle: ${handle}`);
|
||||
// 이미 등록된 채널인지 사전 확인 (현재 로그인 유저 기준)
|
||||
const userRowEarly = await prisma.user.findFirst({ where: { email: session.user?.email ?? '' }, select: { id: true } });
|
||||
if (!userRowEarly) {
|
||||
await prisma.$disconnect();
|
||||
return NextResponse.json({ error: '사용자를 찾을 수 없습니다.' }, { status: 404 });
|
||||
}
|
||||
const alreadyLinkedEarly = await prisma.handle.findFirst({
|
||||
where: { handle: handle ?? '', users: { some: { id: userRowEarly.id } } },
|
||||
select: { id: true },
|
||||
});
|
||||
if (alreadyLinkedEarly) {
|
||||
await prisma.$disconnect();
|
||||
return NextResponse.json({ success: true, message: '이미 등록된 채널입니다.' }, { status: 409 });
|
||||
}
|
||||
if (!handle) {
|
||||
return NextResponse.json({ error: '핸들이 필요합니다' }, { status: 400 });
|
||||
}
|
||||
|
||||
if (!registerCode) {
|
||||
return NextResponse.json({ error: 'can not find register code' }, { status: 400 });
|
||||
}
|
||||
|
||||
console.log('registerCode:', registerCode);
|
||||
|
||||
const upstream = await fetch('http://localhost:9556/isCodeMatch', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ handle: handle, code: registerCode })
|
||||
});
|
||||
|
||||
let data: any = null;
|
||||
const text = await upstream.text();
|
||||
try {
|
||||
data = text ? JSON.parse(text) : null;
|
||||
} catch {
|
||||
data = { message: text };
|
||||
}
|
||||
|
||||
console.log('data:', data);
|
||||
if (data.success) {
|
||||
if (data.foundtext) {
|
||||
if (data.foundtext == registerCode) {
|
||||
const avatarUrl = (data as any)?.avatar as string | undefined;
|
||||
// map user <-> handle (N:M)
|
||||
const email = session.user?.email ?? '';
|
||||
const userRow = await prisma.user.findFirst({ where: { email }, select: { id: true } });
|
||||
if (!userRow) {
|
||||
return NextResponse.json({ error: '사용자를 찾을 수 없습니다.' }, { status: 404 });
|
||||
}
|
||||
|
||||
// ensure handle exists
|
||||
const handleRow = await prisma.handle.upsert({
|
||||
where: { handle: handle },
|
||||
update: { ...(avatarUrl ? { avatar: avatarUrl } : {}) },
|
||||
create: { handle: handle, avatar: avatarUrl ?? '' },
|
||||
});
|
||||
|
||||
// avoid duplicate link
|
||||
const alreadyLinked = await prisma.handle.findFirst({
|
||||
where: { id: handleRow.id, users: { some: { id: userRow.id } } },
|
||||
select: { id: true },
|
||||
});
|
||||
if (!alreadyLinked) {
|
||||
await prisma.user.update({
|
||||
where: { id: userRow.id },
|
||||
data: { handles: { connect: { id: handleRow.id } } },
|
||||
});
|
||||
}
|
||||
|
||||
await prisma.$disconnect();
|
||||
return NextResponse.json({ success: true, message: '채널 매핑 완료' }, { status: 200 });
|
||||
}
|
||||
else{
|
||||
return NextResponse.json({ error: '코드가 일치하지 않습니다.' }, { status: 400 });
|
||||
}
|
||||
} else {
|
||||
return NextResponse.json({ error: '요청 실패' }, { status: 400 });
|
||||
}
|
||||
} else {
|
||||
return NextResponse.json({ error: '요청 실패' }, { status: 400 });
|
||||
}
|
||||
|
||||
|
||||
return NextResponse.json(data ?? {}, { status: upstream.status });
|
||||
} catch (error) {
|
||||
console.error('register_channel 프록시 오류:', error);
|
||||
return NextResponse.json({ error: '업스트림 요청 실패' }, { status: 502 });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { auth } from '@/auth';
|
||||
import { PrismaClient, Prisma } from '@/app/generated/prisma';
|
||||
import { randomBytes } from 'crypto';
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const session = await auth();
|
||||
if (!session) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
try {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const handle = searchParams.get('handle');
|
||||
|
||||
if (!handle) {
|
||||
return NextResponse.json({ error: '핸들이 필요합니다' }, { status: 400 });
|
||||
}
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
// 안전한 난수 기반 32자 코드 (충돌 확률 극히 낮음)
|
||||
const randomcode = randomBytes(16).toString('hex');
|
||||
|
||||
// 중복 여부 확인 (email + handle 기준)
|
||||
const exists = await prisma.registerChannel.findFirst({
|
||||
where: {
|
||||
email: session.user?.email as string,
|
||||
handle: handle,
|
||||
},
|
||||
select: { id: true, randomcode: true },
|
||||
});
|
||||
if (exists) {
|
||||
return NextResponse.json(
|
||||
{ error: '이미 등록된 요청입니다', randomcode: exists.randomcode },
|
||||
{ status: 409 }
|
||||
);
|
||||
}
|
||||
|
||||
// DB에 저장
|
||||
const created = await prisma.registerChannel.create({
|
||||
data: {
|
||||
email: session.user?.email as string,
|
||||
handle: handle,
|
||||
randomcode: randomcode
|
||||
}
|
||||
});
|
||||
|
||||
return NextResponse.json({ message: '성공', code: handle, randomcode: created.randomcode, id: created.id }, { status: 200 });
|
||||
} catch (error: unknown) {
|
||||
console.error('에러 발생:', error);
|
||||
|
||||
// Prisma 에러 코드별 분기
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
// 고유 제약조건 위반 등
|
||||
if (error.code === 'P2002') {
|
||||
return NextResponse.json({ error: '중복된 값으로 저장할 수 없습니다' }, { status: 409 });
|
||||
}
|
||||
return NextResponse.json({ error: '요청이 올바르지 않습니다', code: error.code }, { status: 400 });
|
||||
}
|
||||
if (error instanceof Prisma.PrismaClientValidationError) {
|
||||
return NextResponse.json({ error: '유효하지 않은 데이터입니다' }, { status: 400 });
|
||||
}
|
||||
|
||||
return NextResponse.json({ error: '서버 에러가 발생했습니다' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
// import { PrismaClient } from '../../generated/prisma/client';
|
||||
|
||||
// const prisma = new PrismaClient();
|
||||
|
||||
// export async function GET() {
|
||||
// const publishers = await prisma.content.findMany({
|
||||
// distinct: ['publisher'],
|
||||
// select: {
|
||||
// publisher: true,
|
||||
// },
|
||||
// });
|
||||
// const publisherList = publishers.map(p => p.publisher);
|
||||
// return Response.json(publisherList);
|
||||
// }
|
||||
@@ -1,4 +1,4 @@
|
||||
import { PrismaClient } from '../../generated/prisma/client';
|
||||
import { PrismaClient } from '@/app/generated/prisma';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
72
app/api/contents/handleupdate/route.ts
Normal file
72
app/api/contents/handleupdate/route.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
export const runtime = 'nodejs'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { PrismaClient } from '@/app/generated/prisma'
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const prisma = new PrismaClient()
|
||||
try {
|
||||
const { searchParams } = new URL(request.url)
|
||||
const limitRaw = searchParams.get('limit')
|
||||
let limit = Number(limitRaw ?? '10')
|
||||
if (!Number.isFinite(limit) || limit <= 0) limit = 100
|
||||
if (limit > 1000) limit = 1000
|
||||
|
||||
const rows = await prisma.content.findMany({
|
||||
where: { handleId: null },
|
||||
select: { id: true },
|
||||
take: limit,
|
||||
orderBy: { pubDate: 'desc' },
|
||||
})
|
||||
const ids = rows.map(r => r.id)
|
||||
console.log(`[contents/handleupdate] fetched ${ids.length}/${limit} ids without handle`)
|
||||
// Call local parsingServer with body { ids }
|
||||
try {
|
||||
const upstream = await fetch('http://localhost:9556/gethandle', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ ids }),
|
||||
})
|
||||
const text = await upstream.text()
|
||||
let forwarded: any = null
|
||||
try { forwarded = text ? JSON.parse(text) : null } catch { forwarded = { message: text } }
|
||||
// Map handles back to Content rows
|
||||
let mapped = 0
|
||||
const failures: Array<{ id: string, reason: string }> = []
|
||||
if (forwarded?.success && Array.isArray(forwarded?.items)) {
|
||||
for (const item of forwarded.items as Array<{ id?: string, handle?: string, avatar?: string }>) {
|
||||
const cid = String(item?.id ?? '')
|
||||
let h = String(item?.handle ?? '').trim()
|
||||
const avatar = typeof item?.avatar === 'string' ? item.avatar : undefined
|
||||
if (!cid || !h) { if (cid) failures.push({ id: cid, reason: 'missing_handle' }); continue }
|
||||
try {
|
||||
const handleRow = await prisma.handle.upsert({
|
||||
where: { handle: h },
|
||||
update: { ...(avatar ? { avatar } : {}) },
|
||||
create: { handle: h, ...(avatar ? { avatar } : {}) },
|
||||
select: { id: true }
|
||||
})
|
||||
await prisma.content.update({
|
||||
where: { id: cid },
|
||||
data: { handle: { connect: { id: handleRow.id } } },
|
||||
})
|
||||
mapped += 1
|
||||
} catch (e) {
|
||||
failures.push({ id: cid, reason: 'db_update_failed' })
|
||||
console.error('[contents/handleupdate] map failed for', cid, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
return NextResponse.json({ success: true, count: ids.length, ids, mapped, failures, upstream: forwarded }, { status: upstream.status })
|
||||
} catch (e) {
|
||||
console.error('[contents/handleupdate] upstream call error:', e)
|
||||
return NextResponse.json({ success: true, count: ids.length, ids, upstream: { error: 'upstream failed' } })
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[contents/handleupdate] query error:', e)
|
||||
return NextResponse.json({ error: 'query failed' }, { status: 500 })
|
||||
} finally {
|
||||
try { await (prisma as any).$disconnect() } catch {}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
152
app/api/contents/update/route.ts
Normal file
152
app/api/contents/update/route.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
export const runtime = 'nodejs'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { PrismaClient } from '@/app/generated/prisma'
|
||||
import { auth } from '@/auth'
|
||||
import fs from 'fs/promises'
|
||||
import path from 'path'
|
||||
import { parse } from 'csv-parse/sync'
|
||||
|
||||
function parseKoreanDateToISO(dateStr: string): string | null {
|
||||
// 입력 예: '2025. 9. 1.' → ISO YYYY-MM-DDT00:00:00.000Z (로컬 기준 단순화)
|
||||
const m = dateStr?.match(/^(\d{4})\.\s*(\d{1,2})\.\s*(\d{1,2})\.?$/);
|
||||
if (!m) return null;
|
||||
const yyyy = Number(m[1]);
|
||||
const mm = Number(m[2]);
|
||||
const dd = Number(m[3]);
|
||||
// UTC 자정으로 맞춤
|
||||
const d = new Date(Date.UTC(yyyy, mm - 1, dd, 0, 0, 0));
|
||||
return d.toISOString();
|
||||
}
|
||||
|
||||
function toInt(v: any): number {
|
||||
if (v == null) return 0;
|
||||
if (typeof v === 'number') return Math.floor(v);
|
||||
const s = String(v).replace(/,/g, '').trim();
|
||||
const n = Number(s);
|
||||
return Number.isFinite(n) ? Math.floor(n) : 0;
|
||||
}
|
||||
|
||||
export async function GET(request: Request) {
|
||||
try {
|
||||
// 세션 사용자 확인 (핸들 매핑용)
|
||||
const session = await auth();
|
||||
if (!session?.user?.email) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const { searchParams } = new URL(request.url)
|
||||
const dateParam = searchParams.get('date') || 'latest'
|
||||
|
||||
let rows: any[] = []
|
||||
let dateStr: string | undefined = undefined
|
||||
|
||||
if (dateParam === 'files') {
|
||||
// Read from local CSV and use fixed date 250831
|
||||
const filePath = path.join(process.cwd(), 'datas', 'to0831.csv')
|
||||
const csv = await fs.readFile(filePath, 'utf-8')
|
||||
rows = parse(csv, {
|
||||
columns: true,
|
||||
skip_empty_lines: true,
|
||||
bom: true,
|
||||
relax_column_count: true,
|
||||
trim: true,
|
||||
}) as any[]
|
||||
dateStr = '250831'
|
||||
} else {
|
||||
const upstream = await fetch(`http://localhost:9556/data?date=${encodeURIComponent(dateParam)}`, {
|
||||
method: 'GET',
|
||||
})
|
||||
const text = await upstream.text()
|
||||
let data: any = null
|
||||
try {
|
||||
data = text ? JSON.parse(text) : null
|
||||
} catch {
|
||||
data = { message: text }
|
||||
}
|
||||
rows = Array.isArray(data?.data) ? data.data : []
|
||||
dateStr = data?.date
|
||||
if (!dateStr) {
|
||||
return NextResponse.json({ error: 'invalid upstream payload' }, { status: 502 })
|
||||
}
|
||||
}
|
||||
|
||||
// 유효성 체크
|
||||
if (!dateStr || rows.length === 0) {
|
||||
return NextResponse.json({ error: 'invalid upstream payload' }, { status: 502 })
|
||||
}
|
||||
if (rows.length <= 1) {
|
||||
return NextResponse.json({ success: true, message: 'no rows to import', date: dateStr })
|
||||
}
|
||||
|
||||
// Convert date string: supports 'YYYY. M. D.' or 'YYMMDD'
|
||||
const parseYYMMDD = (s: string): string | null => {
|
||||
const m = s.match(/^(\d{2})(\d{2})(\d{2})$/)
|
||||
if (!m) return null
|
||||
const yyyy = 2000 + Number(m[1])
|
||||
const mm = Number(m[2])
|
||||
const dd = Number(m[3])
|
||||
return new Date(Date.UTC(yyyy, mm - 1, dd, 0, 0, 0)).toISOString()
|
||||
}
|
||||
|
||||
const isoDate = parseKoreanDateToISO(dateStr) || parseYYMMDD(dateStr) || new Date().toISOString();
|
||||
const prisma = new PrismaClient();
|
||||
try {
|
||||
// handle 연결은 비워둠 (요청에 따라 핸들 매핑 생략)
|
||||
|
||||
let upserted = 0;
|
||||
// 2번째 행부터 반영
|
||||
for (let i = 1; i < rows.length; i++) {
|
||||
const row = rows[i] || {};
|
||||
const contentId: string | undefined = row['콘텐츠'] ?? row['contentId'] ?? row['콘텐츠 ID'];
|
||||
const subject: string = row['동영상 제목'] ?? row['제목'] ?? row['title'] ?? row['동영상'] ?? `content-${i}`;
|
||||
const publishAtRaw: string | undefined = row['동영상 게시 시간'] ?? row['게시 시간'] ?? row['publishedAt'];
|
||||
const publishAtDate = publishAtRaw ? new Date(publishAtRaw) : new Date(isoDate);
|
||||
|
||||
// Content upsert: id=콘텐츠, subject=동영상 제목, pubDate=동영상 게시 시간
|
||||
const upsertedContent = await prisma.content.upsert({
|
||||
where: { id: String(contentId ?? `content-${i}`) },
|
||||
update: {
|
||||
subject,
|
||||
pubDate: publishAtDate,
|
||||
},
|
||||
create: {
|
||||
id: String(contentId ?? `content-${i}`),
|
||||
subject,
|
||||
pubDate: publishAtDate,
|
||||
// handle 연결 없음
|
||||
},
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
const views = toInt(row['조회수']);
|
||||
const validViews = toInt(row['유효 조회수']);
|
||||
const premiumViews = toInt(row['YouTube Premium 조회수']);
|
||||
const watchTime = toInt(row['시청 시간(단위: 시간)']);
|
||||
|
||||
// contentId + date 복합 유니크로 upsert
|
||||
await prisma.contentDayView.upsert({
|
||||
where: { contentId_date: { contentId: upsertedContent.id, date: new Date(isoDate) } },
|
||||
update: { views, validViews, premiumViews, watchTime },
|
||||
create: {
|
||||
contentId: upsertedContent.id,
|
||||
date: new Date(isoDate),
|
||||
views,
|
||||
validViews,
|
||||
premiumViews,
|
||||
watchTime,
|
||||
},
|
||||
});
|
||||
upserted += 1;
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: true, date: dateStr, upserted, data: rows })
|
||||
} finally {
|
||||
try { await (prisma as any).$disconnect() } catch {}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('contents/update upstream error:', e)
|
||||
return NextResponse.json({ error: '업스트림 요청 실패' }, { status: 502 })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { PrismaClient } from '@/app/generated/prisma';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const row = await prisma.costPerView.findUnique({ where: { id: 1 } });
|
||||
return NextResponse.json({ value: row?.costPerView ?? null });
|
||||
} catch (e) {
|
||||
console.error('GET /api/cost_per_view 오류:', e);
|
||||
return NextResponse.json({ error: '조회 실패' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const valueRaw = body?.value;
|
||||
const value = typeof valueRaw === 'string' ? parseFloat(valueRaw) : valueRaw;
|
||||
if (typeof value !== 'number' || Number.isNaN(value)) {
|
||||
return NextResponse.json({ error: '유효한 숫자 값을 제공하세요' }, { status: 400 });
|
||||
}
|
||||
|
||||
const saved = await prisma.costPerView.upsert({
|
||||
where: { id: 1 },
|
||||
update: { costPerView: value },
|
||||
create: { id: 1, costPerView: value },
|
||||
});
|
||||
|
||||
return NextResponse.json({ ok: true, value: saved.costPerView });
|
||||
} catch (e) {
|
||||
console.error('POST /api/cost_per_view 오류:', e);
|
||||
return NextResponse.json({ error: '저장 실패' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { auth } from '@/auth';
|
||||
import { PrismaClient } from '@/app/generated/prisma';
|
||||
|
||||
export async function GET() {
|
||||
const session = await auth();
|
||||
if (!session) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
const email = session.user?.email as string | undefined;
|
||||
if (!email) {
|
||||
return NextResponse.json({ error: '세션 이메일을 찾을 수 없습니다' }, { status: 400 });
|
||||
}
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
try {
|
||||
if (email === 'wsx204@naver.com') {
|
||||
const allHandles = await prisma.userHandle.findMany({
|
||||
orderBy: { createtime: 'desc' },
|
||||
select: { id: true, email: true, handle: true, isApproved: true, createtime: true, icon: true }
|
||||
});
|
||||
return NextResponse.json({ items: allHandles });
|
||||
}
|
||||
else{
|
||||
const handles = await prisma.userHandle.findMany({
|
||||
where: { email },
|
||||
orderBy: { createtime: 'desc' },
|
||||
select: { id: true, email: true, handle: true, isApproved: true, createtime: true, icon: true }
|
||||
});
|
||||
return NextResponse.json({ items: handles });
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('list_channel 오류:', e);
|
||||
return NextResponse.json({ error: '조회 실패' }, { status: 500 });
|
||||
} finally {
|
||||
await prisma.$disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { auth } from '@/auth';
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const session = await auth();
|
||||
if (!session) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
try {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const handle = searchParams.get('handle');
|
||||
const email = session.user?.email as string | undefined;
|
||||
|
||||
if (!handle) {
|
||||
return NextResponse.json({ error: '핸들이 필요합니다' }, { status: 400 });
|
||||
}
|
||||
|
||||
if (!email) {
|
||||
return NextResponse.json({ error: '세션 이메일을 찾을 수 없습니다' }, { status: 400 });
|
||||
}
|
||||
|
||||
const upstream = await fetch('http://localhost:10001/register_channel', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ email, handle })
|
||||
});
|
||||
|
||||
let data: any = null;
|
||||
const text = await upstream.text();
|
||||
try {
|
||||
data = text ? JSON.parse(text) : null;
|
||||
} catch {
|
||||
data = { message: text };
|
||||
}
|
||||
|
||||
return NextResponse.json(data ?? {}, { status: upstream.status });
|
||||
} catch (error) {
|
||||
console.error('register_channel 프록시 오류:', error);
|
||||
return NextResponse.json({ error: '업스트림 요청 실패' }, { status: 502 });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,10 +2,13 @@
|
||||
import CalenderSelector from "@/app/components/CalenderSelector";
|
||||
import Modal from "@/app/components/Modal";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useSession } from "next-auth/react";
|
||||
|
||||
|
||||
export default function Page() {
|
||||
const { data: session } = useSession();
|
||||
const [isreqmodal, setIsreqmodal] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [chanellist, setChanellist] = useState<string[]>([]);
|
||||
const [contentlist, setContentlist] = useState<{
|
||||
id: string;
|
||||
@@ -37,58 +40,47 @@ export default function Page() {
|
||||
return number.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
||||
};
|
||||
|
||||
const get_code = async () => {
|
||||
if (handleInputRef.current && handleInputRef.current.value) {
|
||||
const inputValue = handleInputRef.current.value;
|
||||
if (confirm(`핸들 "${inputValue}" 의 등록 코드를 발급하시겠습니까?`)) {
|
||||
try {
|
||||
const response = await fetch(`/api/channel_code?handle=${inputValue}`);
|
||||
if (response.status === 409) {
|
||||
alert("이미 등록된 요청입니다. 기존 코드를 사용합니다.");
|
||||
const data = await response.json();
|
||||
setRegisterCode(data.randomcode);
|
||||
return;
|
||||
}
|
||||
if (!response.ok) {
|
||||
throw new Error('서버 응답이 올바르지 않습니다.');
|
||||
}
|
||||
const data = await response.json();
|
||||
setRegisterCode(data.randomcode);
|
||||
} catch (error) {
|
||||
console.error('채널 코드 요청 중 오류 발생:', error);
|
||||
alert('채널 코드를 가져오는데 실패했습니다. 다시 시도해주세요.');
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
} else{ alert("채널 핸들을 입력해주세요"); }
|
||||
}
|
||||
|
||||
const register_channel = async () => {
|
||||
if (handleInputRef.current && handleInputRef.current.value) {
|
||||
const inputValue = handleInputRef.current.value;
|
||||
try {
|
||||
const response = await fetch(`/api/register_channel?handle=${encodeURIComponent(inputValue)}`);
|
||||
const data = await response.json();
|
||||
if (!response.ok) {
|
||||
alert(data?.error ?? '등록 요청 실패');
|
||||
return;
|
||||
}
|
||||
alert('등록 요청을 전송했습니다.');
|
||||
setIsreqmodal(false);
|
||||
} catch (error) {
|
||||
console.error('등록 요청 중 오류:', error);
|
||||
alert('등록 요청 실패');
|
||||
if (isLoading) return;
|
||||
if (!(handleInputRef.current && handleInputRef.current.value)) {
|
||||
alert("채널 핸들을 입력해주세요");
|
||||
return;
|
||||
}
|
||||
const inputValue = handleInputRef.current.value;
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const response = await fetch(`/api/channel/register?handle=${encodeURIComponent(inputValue)}`);
|
||||
const data = await response.json();
|
||||
if (!response.ok) {
|
||||
alert(data?.error ?? data?.message ?? '등록 요청 실패');
|
||||
return;
|
||||
}
|
||||
} else { alert("채널 핸들을 입력해주세요"); }
|
||||
alert('등록 요청을 전송했습니다.');
|
||||
fetchListChannel();
|
||||
setIsreqmodal(false);
|
||||
} catch (error) {
|
||||
console.error('등록 요청 중 오류:', error);
|
||||
alert('등록 요청 실패');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
const fetchRegisterCode = async () => {
|
||||
try {
|
||||
const resp = await fetch('/api/channel/mycode', { cache: 'no-store' });
|
||||
const data = await resp.json();
|
||||
setRegisterCode(data.registerCode);
|
||||
} catch (e) {
|
||||
console.error('register_code 요청 에러:', e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
const fetchListChannel = async () => {
|
||||
try {
|
||||
const resp = await fetch('/api/list_channel', { cache: 'no-store' });
|
||||
const resp = await fetch('/api/channel/list', { cache: 'no-store' });
|
||||
const data = await resp.json();
|
||||
setChannelList(data.items);
|
||||
console.log('list_channel:', data);
|
||||
@@ -97,9 +89,33 @@ export default function Page() {
|
||||
}
|
||||
}
|
||||
|
||||
const delete_channel = async (handle: string) => {
|
||||
if (isLoading) return;
|
||||
if (!handle) return;
|
||||
const ok = confirm(`정말 "${handle}" 채널 연결을 해제하시겠습니까?`);
|
||||
if (!ok) return;
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const resp = await fetch(`/api/channel/delete?handle=${encodeURIComponent(handle)}`);
|
||||
const data = await resp.json();
|
||||
if (!resp.ok) {
|
||||
alert(data?.error ?? '삭제 실패');
|
||||
return;
|
||||
}
|
||||
alert(data?.message ?? '채널 연결을 해제했습니다.');
|
||||
await fetchListChannel();
|
||||
} catch (e) {
|
||||
console.error('delete_channel 요청 에러:', e);
|
||||
alert('삭제 요청 실패');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
// 세션 이메일 기준 채널 목록 조회 로그
|
||||
fetchListChannel();
|
||||
fetchRegisterCode();
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -112,7 +128,7 @@ export default function Page() {
|
||||
<div className="flex flex-row h-[36px]">
|
||||
<div
|
||||
className="w-[104px] h-[36px] rounded-lg flex items-center justify-center bg-[#F94B37] border-[#D73B29] text-white cursor-pointer hover:bg-[#D73B29]"
|
||||
onClick={() => {setIsreqmodal(true); setRegisterCode("")}}
|
||||
onClick={() => {setIsreqmodal(true); }}
|
||||
>
|
||||
채널 등록
|
||||
</div>
|
||||
@@ -129,6 +145,7 @@ export default function Page() {
|
||||
<col className="w-[100px]"/>
|
||||
<col className="w-[100px]"/>
|
||||
<col className="w-[110px]"/>
|
||||
<col className="w-[90px]"/>
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr className="sticky top-0 bg-white h-[49px] ">
|
||||
@@ -137,12 +154,13 @@ export default function Page() {
|
||||
<th className="border-b-1 right-border border-[#e6e9ef] ">승인여부</th>
|
||||
<th className="border-b-1 right-border border-[#e6e9ef] ">영상수</th>
|
||||
<th className="border-b-1 right-border border-[#e6e9ef] ">조회수</th>
|
||||
<th className="border-b-1 border-[#e6e9ef] ">예상수익</th>
|
||||
<th className="border-b-1 right-border border-[#e6e9ef] ">예상수익</th>
|
||||
<th className="border-b-1 border-[#e6e9ef] ">삭제</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{
|
||||
channelList.map((channel)=>{
|
||||
channelList && channelList.map((channel)=>{
|
||||
return (
|
||||
<tr key={channel.id} className="h-[54px] border-1 border-[#e6e9ef] rounded-lg font-semibold">
|
||||
<td className="right-border rounded-l-lg border-l-1 border-t-1 border-b-1 border-[#e6e9ef] pl-2 h-[54px] " >
|
||||
@@ -159,7 +177,15 @@ export default function Page() {
|
||||
<td className="right-border border-b-1 border-t-1 border-[#e6e9ef] text-center whitespace-nowrap overflow-hidden">{channel.is_approved? "승인" : "미승인"}</td>
|
||||
<td className="right-border border-b-1 border-t-1 border-[#e6e9ef] text-center"> - </td>
|
||||
<td className="right-border border-b-1 border-t-1 border-[#e6e9ef] text-center"> - </td>
|
||||
<td className="border-b-1 border-t-1 border-[#e6e9ef] border-r-1 rounded-r-lg text-center"> - </td>
|
||||
<td className="right-border border-b-1 border-t-1 border-[#e6e9ef] text-center"> - </td>
|
||||
<td className="border-b-1 border-t-1 border-[#e6e9ef] border-r-1 rounded-r-lg text-center">
|
||||
<button
|
||||
className={`px-3 py-1 rounded-md ${isLoading ? 'bg-gray-300 cursor-not-allowed' : 'bg-red-500 hover:bg-red-600'} text-white`}
|
||||
onClick={() => { if (!isLoading) delete_channel(channel.handle); }}
|
||||
>
|
||||
삭제
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
})}
|
||||
@@ -179,58 +205,62 @@ export default function Page() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="flex flex-col sh-[287px]">
|
||||
<div className="flex flex-col sh-[287px] gap-3">
|
||||
<div className="flex flex-col justify-between">
|
||||
<div className="text-lg font-semibold text-black">핸들</div>
|
||||
<div className="flex flex-row justify-between items-center">
|
||||
<div className="w-[400px] h-[56px] border-[#d5d5d5] border-1 bg-white text-black font-normal rounded-lg flex items-center justify-center text-md p-1 px-2">
|
||||
<input type="text" className="w-full h-full border-none outline-none" ref={handleInputRef}/>
|
||||
</div>
|
||||
<div className="ml-2 w-[140px] h-[50px] border-[#D73B29] bg-[#F94B37] text-white font-bold rounded-lg flex items-center justify-center text-xl cursor-pointer hover:bg-[#D73B29] transition-colors" onClick={() => get_code()}>
|
||||
코드 발급
|
||||
<div className=" h-[56px] flex-1 border-[#d5d5d5] border-1 bg-white text-black font-normal rounded-lg flex items-center justify-center text-md p-1 px-2">
|
||||
<input type="text" className="w-full h-full border-none outline-none" ref={handleInputRef} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="flex flex-col justify-between mt-5">
|
||||
<div className="flex flex-col justify-between ">
|
||||
<div className="text-lg font-semibold text-black">채널 등록 코드</div>
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
|
||||
|
||||
|
||||
<div className="w-full h-[56px] border-[#d5d5d5] border-1 bg-white text-black font-normal rounded-lg flex items-center justify-start text-xl p-5">
|
||||
{registerCode ? registerCode : "코드를 발급해주세요"}
|
||||
{registerCode ? registerCode : "코드 로드 에러"}
|
||||
</div>
|
||||
{registerCode && (
|
||||
<div
|
||||
<div
|
||||
className="w-[45px] h-[56px] border-[#d5d5d5] border-1 bg-white rounded-lg flex items-center justify-center cursor-pointer"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(registerCode);
|
||||
}}
|
||||
>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M20 9H11C9.89543 9 9 9.89543 9 11V20C9 21.1046 9.89543 22 11 22H20C21.1046 22 22 21.1046 22 20V11C22 9.89543 21.1046 9 20 9Z" stroke="#666666" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
<path d="M5 15H4C3.46957 15 2.96086 14.7893 2.58579 14.4142C2.21071 14.0391 2 13.5304 2 13V4C2 3.46957 2.21071 2.96086 2.58579 2.58579C2.96086 2.21071 3.46957 2 4 2H13C13.5304 2 14.0391 2.21071 14.4142 2.58579C14.7893 2.96086 15 3.46957 15 4V5" stroke="#666666" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
<path d="M20 9H11C9.89543 9 9 9.89543 9 11V20C9 21.1046 9.89543 22 11 22H20C21.1046 22 22 21.1046 22 20V11C22 9.89543 21.1046 9 20 9Z" stroke="#666666" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||
<path d="M5 15H4C3.46957 15 2.96086 14.7893 2.58579 14.4142C2.21071 14.0391 2 13.5304 2 13V4C2 3.46957 2.21071 2.96086 2.58579 2.58579C2.96086 2.21071 3.46957 2 4 2H13C13.5304 2 14.0391 2.21071 14.4142 2.58579C14.7893 2.96086 15 3.46957 15 4V5" stroke="#666666" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<div className="mt-3 w-[100%-50px] h-[56px] border-[#D73B29] bg-[#F94B37] text-white font-bold rounded-lg flex items-center justify-center text-xl cursor-pointer hover:bg-[#D73B29] transition-colors" onClick={() => register_channel()}>
|
||||
</div>
|
||||
|
||||
<div className =" flex flex-row justify-between items-center gap-3">
|
||||
<div className="text-sm text-gray-500">
|
||||
* 채널에 코드를 설정한 후 채널 등록 버튼을 눌러주세요.
|
||||
</div>
|
||||
|
||||
<div className={`mt-3 h-[56px] flex-1 border-[#D73B29] bg-[#F94B37] text-white font-bold rounded-lg flex items-center justify-center text-xl transition-colors ${isLoading ? 'opacity-60 cursor-not-allowed' : 'cursor-pointer hover:bg-[#D73B29]'}`} onClick={() => { if (!isLoading) register_channel(); }}>
|
||||
채널 등록
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</Modal>
|
||||
|
||||
{isLoading && (
|
||||
<div className="fixed inset-0 z-[1000] bg-black/40 backdrop-blur-sm flex items-center justify-center">
|
||||
<div className="w-12 h-12 rounded-full border-4 border-white/30 border-t-white animate-spin" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user