From 3e031a1c80af459eef6f4c179f2d87ebf28547ba Mon Sep 17 00:00:00 2001 From: gahusb Date: Thu, 2 Jul 2026 15:19:09 +0900 Subject: [PATCH] =?UTF-8?q?feat(phase1):=20ad=5Fchannels=20=ED=85=8C?= =?UTF-8?q?=EC=9D=B4=EB=B8=94=20+=20admin=20CRUD=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Migration: ad_channels table (uuid, name, url, status, memo) - Routes: GET/POST /api/admin/ad-channels (list/create) - Routes: PATCH/DELETE /api/admin/ad-channels/[id] (update/delete) - Auth: admin_token verification via verifyAdminTokenNode - RLS: service_role only, no additional policies Co-Authored-By: Claude Opus 4.8 (1M context) --- app/api/admin/ad-channels/[id]/route.ts | 47 +++++++++++++++++ app/api/admin/ad-channels/route.ts | 50 +++++++++++++++++++ .../2026-07-02-phase1-ad-channels.sql | 12 +++++ 3 files changed, 109 insertions(+) create mode 100644 app/api/admin/ad-channels/[id]/route.ts create mode 100644 app/api/admin/ad-channels/route.ts create mode 100644 supabase/migrations/2026-07-02-phase1-ad-channels.sql diff --git a/app/api/admin/ad-channels/[id]/route.ts b/app/api/admin/ad-channels/[id]/route.ts new file mode 100644 index 0000000..a0a747f --- /dev/null +++ b/app/api/admin/ad-channels/[id]/route.ts @@ -0,0 +1,47 @@ +import { NextResponse } from 'next/server'; +import { cookies } from 'next/headers'; +import { createAdminClient } from '@/lib/supabase/admin'; +import { verifyAdminTokenNode } from '@/lib/admin-auth'; + +export const runtime = 'nodejs'; + +async function checkAuth() { + const cookieStore = await cookies(); + const token = cookieStore.get('admin_token')?.value; + return token && verifyAdminTokenNode(token); +} + +export async function PATCH(request: Request, { params }: { params: Promise<{ id: string }> }) { + if (!(await checkAuth())) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + + const { id } = await params; + const body = await request.json(); + + const patch: Record = { updated_at: new Date().toISOString() }; + + if (typeof body.name === 'string' && body.name.trim()) patch.name = body.name.trim(); + if ('url' in body) patch.url = body.url?.trim() || null; + if ('memo' in body) patch.memo = body.memo?.trim() || null; + if (body.status === 'active' || body.status === 'paused') patch.status = body.status; + + const supabase = createAdminClient(); + const { error } = await supabase.from('ad_channels').update(patch).eq('id', id); + + if (error) return NextResponse.json({ error: error.message }, { status: 500 }); + return NextResponse.json({ success: true }); +} + +export async function DELETE(_request: Request, { params }: { params: Promise<{ id: string }> }) { + if (!(await checkAuth())) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + + const { id } = await params; + const supabase = createAdminClient(); + const { error } = await supabase.from('ad_channels').delete().eq('id', id); + + if (error) return NextResponse.json({ error: error.message }, { status: 500 }); + return NextResponse.json({ success: true }); +} diff --git a/app/api/admin/ad-channels/route.ts b/app/api/admin/ad-channels/route.ts new file mode 100644 index 0000000..fdd2914 --- /dev/null +++ b/app/api/admin/ad-channels/route.ts @@ -0,0 +1,50 @@ +import { NextResponse } from 'next/server'; +import { cookies } from 'next/headers'; +import { createAdminClient } from '@/lib/supabase/admin'; +import { verifyAdminTokenNode } from '@/lib/admin-auth'; + +export const runtime = 'nodejs'; + +async function checkAuth() { + const cookieStore = await cookies(); + const token = cookieStore.get('admin_token')?.value; + return token && verifyAdminTokenNode(token); +} + +export async function GET() { + if (!(await checkAuth())) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + + const supabase = createAdminClient(); + const { data, error } = await supabase + .from('ad_channels') + .select('*') + .order('created_at', { ascending: false }); + + if (error) return NextResponse.json({ error: error.message }, { status: 500 }); + return NextResponse.json({ channels: data ?? [] }); +} + +export async function POST(request: Request) { + if (!(await checkAuth())) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + + const body = await request.json(); + const name = (body.name as string | undefined)?.trim(); + + if (!name) { + return NextResponse.json({ error: '채널명을 입력해주세요.' }, { status: 400 }); + } + + const supabase = createAdminClient(); + const { data, error } = await supabase + .from('ad_channels') + .insert({ name, url: body.url?.trim() || null, memo: body.memo?.trim() || null }) + .select() + .single(); + + if (error) return NextResponse.json({ error: error.message }, { status: 500 }); + return NextResponse.json({ channel: data }); +} diff --git a/supabase/migrations/2026-07-02-phase1-ad-channels.sql b/supabase/migrations/2026-07-02-phase1-ad-channels.sql new file mode 100644 index 0000000..d9889f6 --- /dev/null +++ b/supabase/migrations/2026-07-02-phase1-ad-channels.sql @@ -0,0 +1,12 @@ +CREATE TABLE IF NOT EXISTS ad_channels ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + name text NOT NULL, + url text, + status text NOT NULL DEFAULT 'active' CHECK (status IN ('active','paused')), + memo text, + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now() +); + +ALTER TABLE ad_channels ENABLE ROW LEVEL SECURITY; +-- service_role(관리자 API)만 접근 — 별도 policy 없음(기본 거부)