- 로또 번호 추천 구독자 전용 페이지 (/services/lotto/recommend) - NAS 몬테카를로 API 연동 + 클라이언트 사이드 폴백 - 무료 미리보기 1개 + 구독자용 프리미엄 번호 추천 - 구독 플랜 변경: 골드(900원)/플래티넘(2,900원)/다이아(9,900원) - 텔레그램 봇 연동: 연결/해제, 웹훅, /start 명령 처리 - 마이페이지 텔레그램 연결 UI + 가이드 모달 - 관리자 페이지 (/admin): 대시보드, 회원, 서비스, 문의 관리 - Supabase 마이그레이션: profiles 텔레그램 컬럼, 신규 상품 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
138 lines
4.9 KiB
TypeScript
138 lines
4.9 KiB
TypeScript
'use client';
|
|
|
|
import { useEffect, useState } from 'react';
|
|
|
|
interface Service {
|
|
id: string;
|
|
name: string;
|
|
description: string;
|
|
is_active: boolean;
|
|
order_index: number;
|
|
}
|
|
|
|
const SERVICE_ICONS: Record<string, string> = {
|
|
saju: '🔮',
|
|
lotto: '🎰',
|
|
stock: '📈',
|
|
automation: '🤖',
|
|
prompt: '💡',
|
|
freelance: '🛠',
|
|
};
|
|
|
|
export default function AdminServicesPage() {
|
|
const [services, setServices] = useState<Service[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [toggling, setToggling] = useState<string | null>(null);
|
|
|
|
useEffect(() => {
|
|
fetch('/api/admin/services')
|
|
.then((r) => r.json())
|
|
.then((d) => setServices(d.services ?? []))
|
|
.catch(console.error)
|
|
.finally(() => setLoading(false));
|
|
}, []);
|
|
|
|
async function toggleService(id: string, current: boolean) {
|
|
setToggling(id);
|
|
try {
|
|
const res = await fetch('/api/admin/services', {
|
|
method: 'PATCH',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ id, is_active: !current }),
|
|
});
|
|
if (res.ok) {
|
|
setServices((prev) =>
|
|
prev.map((s) => (s.id === id ? { ...s, is_active: !current } : s))
|
|
);
|
|
}
|
|
} catch (e) {
|
|
console.error(e);
|
|
} finally {
|
|
setToggling(null);
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="p-6 max-w-3xl mx-auto">
|
|
<div className="mb-6">
|
|
<h1 className="text-white text-2xl font-bold">서비스 설정</h1>
|
|
<p className="text-slate-400 text-sm mt-0.5">각 서비스의 노출 여부를 관리합니다</p>
|
|
</div>
|
|
|
|
{loading ? (
|
|
<div className="flex items-center justify-center h-48">
|
|
<div className="animate-spin w-8 h-8 border-2 border-red-500 border-t-transparent rounded-full" />
|
|
</div>
|
|
) : (
|
|
<div className="space-y-3">
|
|
{services.map((service) => (
|
|
<div
|
|
key={service.id}
|
|
className={`bg-slate-900 rounded-2xl p-5 border transition-all ${
|
|
service.is_active ? 'border-slate-700/50' : 'border-slate-800 opacity-60'
|
|
}`}
|
|
>
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-4">
|
|
<span className="text-2xl">{SERVICE_ICONS[service.id] ?? '📦'}</span>
|
|
<div>
|
|
<h3 className="text-white font-semibold">{service.name}</h3>
|
|
<p className="text-slate-400 text-sm">{service.description}</p>
|
|
</div>
|
|
</div>
|
|
{/* 토글 스위치 */}
|
|
<button
|
|
onClick={() => toggleService(service.id, service.is_active)}
|
|
disabled={toggling === service.id}
|
|
aria-label={`${service.name} ${service.is_active ? '비활성화' : '활성화'}`}
|
|
className={`relative w-12 h-6 rounded-full transition-colors duration-200 focus:outline-none ${
|
|
service.is_active ? 'bg-green-500' : 'bg-slate-600'
|
|
} ${toggling === service.id ? 'opacity-50' : ''}`}
|
|
>
|
|
<span
|
|
className={`absolute top-0.5 left-0.5 w-5 h-5 bg-white rounded-full shadow transition-transform duration-200 ${
|
|
service.is_active ? 'translate-x-6' : 'translate-x-0'
|
|
}`}
|
|
/>
|
|
</button>
|
|
</div>
|
|
|
|
{/* 상태 배지 */}
|
|
<div className="mt-3 flex items-center gap-2">
|
|
<span
|
|
className={`inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-medium ${
|
|
service.is_active
|
|
? 'bg-green-900/40 text-green-400'
|
|
: 'bg-slate-700 text-slate-500'
|
|
}`}
|
|
>
|
|
<span className={`w-1.5 h-1.5 rounded-full ${service.is_active ? 'bg-green-400' : 'bg-slate-500'}`} />
|
|
{service.is_active ? '활성' : '비활성'}
|
|
</span>
|
|
{!service.is_active && (
|
|
<span className="text-slate-500 text-xs">사이트에서 숨겨집니다</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
<div className="mt-6 bg-slate-800/50 rounded-xl p-4 border border-slate-700/30">
|
|
<p className="text-slate-400 text-xs">
|
|
💡 서비스 on/off는 Supabase의 <code className="text-slate-300">service_settings</code> 테이블에 저장됩니다.
|
|
아직 테이블이 없으면 아래 SQL을 실행하세요.
|
|
</p>
|
|
<pre className="text-slate-500 text-xs mt-2 bg-slate-900 rounded p-3 overflow-x-auto">{`CREATE TABLE service_settings (
|
|
id text PRIMARY KEY,
|
|
name text,
|
|
description text,
|
|
is_active boolean DEFAULT true,
|
|
order_index integer DEFAULT 0,
|
|
updated_at timestamptz DEFAULT now()
|
|
);`}</pre>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|