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

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