From ce23c4e6125feb8370006bb57f1ba42c2c5dc7ce Mon Sep 17 00:00:00 2001 From: gahusb Date: Sat, 2 May 2026 09:05:30 +0900 Subject: [PATCH] =?UTF-8?q?feat(api):=20/api/admin/packs=20=E2=80=94=20adm?= =?UTF-8?q?in=20=ED=8C=8C=EC=9D=BC=20=EB=AA=A9=EB=A1=9D/=ED=8E=B8=EC=A7=91?= =?UTF-8?q?/=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - GET: pack_files 목록 (deleted_at IS NULL) - PATCH: { id, label?, sort_order?, min_tier? } 인라인 편집 - DELETE: web-backend 통한 soft delete Co-Authored-By: Claude Opus 4.7 (1M context) --- app/api/admin/packs/route.ts | 72 ++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 app/api/admin/packs/route.ts diff --git a/app/api/admin/packs/route.ts b/app/api/admin/packs/route.ts new file mode 100644 index 0000000..bbc93ef --- /dev/null +++ b/app/api/admin/packs/route.ts @@ -0,0 +1,72 @@ +import { NextResponse } from 'next/server'; +import { cookies } from 'next/headers'; +import { createAdminClient } from '@/lib/supabase/admin'; +import { verifyAdminTokenNode } from '@/lib/admin-auth'; +import { deletePackFileViaBackend } from '@/lib/web-backend'; +import type { PackTier } from '@/lib/pack-assets'; + +export const runtime = 'nodejs'; + +async function checkAuth() { + const cookieStore = await cookies(); + const token = cookieStore.get('admin_token')?.value; + return token && verifyAdminTokenNode(token); +} + +const VALID_TIERS = new Set(['starter', 'pro', 'master']); + +export async function GET() { + if (!(await checkAuth())) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + const supabase = createAdminClient(); + const { data, error } = await supabase + .from('pack_files') + .select('*') + .is('deleted_at', null) + .order('min_tier') + .order('sort_order'); + if (error) return NextResponse.json({ error: error.message }, { status: 500 }); + return NextResponse.json({ files: data ?? [] }); +} + +export async function PATCH(request: Request) { + if (!(await checkAuth())) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + const { id, label, sort_order, min_tier } = await request.json(); + if (!id) return NextResponse.json({ error: 'id 필요' }, { status: 400 }); + + const updates: Record = {}; + if (typeof label === 'string') updates.label = label; + if (typeof sort_order === 'number') updates.sort_order = sort_order; + if (typeof min_tier === 'string' && VALID_TIERS.has(min_tier as PackTier)) { + updates.min_tier = min_tier; + } + if (Object.keys(updates).length === 0) { + return NextResponse.json({ error: '변경할 필드 없음' }, { status: 400 }); + } + + const supabase = createAdminClient(); + const { error } = await supabase.from('pack_files').update(updates).eq('id', id); + if (error) return NextResponse.json({ error: error.message }, { status: 500 }); + return NextResponse.json({ success: true }); +} + +export async function DELETE(request: Request) { + if (!(await checkAuth())) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + const url = new URL(request.url); + const id = url.searchParams.get('id'); + if (!id) return NextResponse.json({ error: 'id 필요' }, { status: 400 }); + + // web-backend가 soft delete 담당 (DSM 정리도 backend가 향후 추가 예정) + try { + await deletePackFileViaBackend(id); + } catch (e) { + const msg = e instanceof Error ? e.message : 'unknown'; + return NextResponse.json({ error: 'backend delete 실패', detail: msg }, { status: 502 }); + } + return NextResponse.json({ success: true }); +}