refactor: AI 음악 메인 개편 — 로또/프롬프트/자동화 삭제, 음악/블로그 팩 신규
- 삭제: services/{lotto,prompt,automation,ai-kit,stock,tools} + api/{lotto,tools}
- 노출 제거: /freelance, /services/website (noindex + robots/sitemap 제외, 외부 지원서 링크 유지)
- 신규: /services/music (3-tier 39k/99k/149k, 4단계 프로세스)
- 신규: /services/blog (블로그 자동화 팩 29k 1회성)
- 신규: PurchaseAgreementModal (전자상거래법 17조 동의 + 계좌이체)
- 개편: 홈 대시보드 (음악 Hero + 사주/블로그팩/일반문의 서브카드)
- 사이드바 재구성, sitemap/robots/JSON-LD 갱신
- 환불정책 신규 상품 반영 + 법적 근거 명시
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,86 +0,0 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { createClient } from '@/lib/supabase/server';
|
||||
|
||||
const LOTTO_PRODUCT_IDS = ['lotto_gold', 'lotto_platinum', 'lotto_diamond', 'lotto_annual'];
|
||||
|
||||
function nasHeaders() {
|
||||
const h: Record<string, string> = {};
|
||||
if (process.env.NAS_LOTTO_API_KEY) h['Authorization'] = `Bearer ${process.env.NAS_LOTTO_API_KEY}`;
|
||||
return h;
|
||||
}
|
||||
|
||||
function nasBase() {
|
||||
const base = process.env.NAS_LOTTO_API_URL;
|
||||
if (!base) throw new Error('NAS_URL_NOT_CONFIGURED');
|
||||
return base;
|
||||
}
|
||||
|
||||
export async function nasGet(path: string, timeoutMs = 25000): Promise<unknown> {
|
||||
const res = await fetch(`${nasBase()}${path}`, {
|
||||
headers: nasHeaders(), signal: AbortSignal.timeout(timeoutMs),
|
||||
});
|
||||
if (!res.ok) throw new Error(`NAS_${res.status}`);
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function nasPost(path: string, body: unknown): Promise<unknown> {
|
||||
const res = await fetch(`${nasBase()}${path}`, {
|
||||
method: 'POST',
|
||||
headers: { ...nasHeaders(), 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body),
|
||||
signal: AbortSignal.timeout(25000),
|
||||
});
|
||||
if (!res.ok) throw new Error(`NAS_${res.status}`);
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function nasPut(path: string, body: unknown): Promise<unknown> {
|
||||
const res = await fetch(`${nasBase()}${path}`, {
|
||||
method: 'PUT',
|
||||
headers: { ...nasHeaders(), 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body),
|
||||
signal: AbortSignal.timeout(25000),
|
||||
});
|
||||
if (!res.ok) throw new Error(`NAS_${res.status}`);
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function nasDelete(path: string): Promise<unknown> {
|
||||
const res = await fetch(`${nasBase()}${path}`, {
|
||||
method: 'DELETE', headers: nasHeaders(), signal: AbortSignal.timeout(25000),
|
||||
});
|
||||
if (!res.ok) throw new Error(`NAS_${res.status}`);
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export interface AuthResult { userId: string; plan: string; }
|
||||
|
||||
export async function requireSubscription(): Promise<AuthResult | NextResponse> {
|
||||
const supabase = await createClient();
|
||||
const { data: { user }, error } = await supabase.auth.getUser();
|
||||
if (error || !user) return NextResponse.json({ error: 'UNAUTHORIZED' }, { status: 401 });
|
||||
|
||||
const { data: sub } = await supabase
|
||||
.from('subscriptions').select('product_id')
|
||||
.eq('user_id', user.id).eq('status', 'active')
|
||||
.in('product_id', LOTTO_PRODUCT_IDS).maybeSingle();
|
||||
if (sub) return { userId: user.id, plan: sub.product_id };
|
||||
|
||||
const ago31 = new Date(Date.now() - 31 * 24 * 60 * 60 * 1000).toISOString();
|
||||
const { data: order } = await supabase
|
||||
.from('orders').select('product_id')
|
||||
.eq('user_id', user.id).eq('status', 'paid')
|
||||
.in('product_id', LOTTO_PRODUCT_IDS)
|
||||
.gte('created_at', ago31)
|
||||
.order('created_at', { ascending: false }).limit(1).maybeSingle();
|
||||
if (order) return { userId: user.id, plan: order.product_id };
|
||||
|
||||
return NextResponse.json({ error: 'NOT_SUBSCRIBED' }, { status: 403 });
|
||||
}
|
||||
|
||||
export function handleNasError(err: unknown): NextResponse {
|
||||
const e = err as { name?: string };
|
||||
if (e?.name === 'TimeoutError') return NextResponse.json({ error: 'NAS_TIMEOUT' }, { status: 504 });
|
||||
console.error('[NAS]', err);
|
||||
return NextResponse.json({ error: 'INTERNAL_ERROR' }, { status: 500 });
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { nasGet, requireSubscription, handleNasError } from '../../_nas';
|
||||
|
||||
export const maxDuration = 60;
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const auth = await requireSubscription();
|
||||
if (auth instanceof NextResponse) return auth;
|
||||
const data = await nasGet('/api/lotto/analysis/personal');
|
||||
return NextResponse.json(data);
|
||||
} catch (err) { return handleNasError(err); }
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { createClient } from '@/lib/supabase/server';
|
||||
|
||||
const LOTTO_PRODUCT_IDS = ['lotto_gold', 'lotto_platinum', 'lotto_diamond'];
|
||||
|
||||
async function nasGet(path: string): Promise<unknown> {
|
||||
const base = process.env.NAS_LOTTO_API_URL;
|
||||
if (!base) throw new Error('NAS_URL_NOT_CONFIGURED');
|
||||
|
||||
const headers: Record<string, string> = {};
|
||||
if (process.env.NAS_LOTTO_API_KEY) {
|
||||
headers['Authorization'] = `Bearer ${process.env.NAS_LOTTO_API_KEY}`;
|
||||
}
|
||||
|
||||
const res = await fetch(`${base}${path}`, {
|
||||
method: 'GET',
|
||||
headers,
|
||||
signal: AbortSignal.timeout(10000),
|
||||
});
|
||||
|
||||
if (!res.ok) throw new Error(`NAS_${res.status}`);
|
||||
return res.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/lotto/dashboard
|
||||
* 페이지 초기 로드용: latest + analysis + simulation 이력 병렬 조회
|
||||
*
|
||||
* Response:
|
||||
* {
|
||||
* plan: string,
|
||||
* latest: { drawNo, date, numbers, bonus, metrics },
|
||||
* analysis: { total_draws, mean_sum, std_sum, number_stats[] },
|
||||
* simulation: { runs[] }
|
||||
* }
|
||||
*/
|
||||
export async function GET() {
|
||||
try {
|
||||
// 1. 인증
|
||||
const supabase = await createClient();
|
||||
const { data: { user }, error: authError } = await supabase.auth.getUser();
|
||||
if (authError || !user) {
|
||||
return NextResponse.json({ error: 'UNAUTHORIZED' }, { status: 401 });
|
||||
}
|
||||
|
||||
// 2. 구독 확인
|
||||
const { data: orders } = await supabase
|
||||
.from('orders')
|
||||
.select('id, product_id, status, created_at')
|
||||
.eq('user_id', user.id)
|
||||
.eq('status', 'paid')
|
||||
.in('product_id', LOTTO_PRODUCT_IDS)
|
||||
.order('created_at', { ascending: false })
|
||||
.limit(1);
|
||||
|
||||
if (!orders || orders.length === 0) {
|
||||
return NextResponse.json({ error: 'NOT_SUBSCRIBED' }, { status: 403 });
|
||||
}
|
||||
|
||||
const order = orders[0];
|
||||
const diffDays =
|
||||
(Date.now() - new Date(order.created_at).getTime()) / (1000 * 60 * 60 * 24);
|
||||
const maxDays = order.product_id === 'lotto_annual' ? 366 : 31;
|
||||
|
||||
if (diffDays > maxDays) {
|
||||
return NextResponse.json({ error: 'NOT_SUBSCRIBED' }, { status: 403 });
|
||||
}
|
||||
|
||||
// 3. NAS 병렬 조회
|
||||
const [latest, analysis, simulation] = await Promise.allSettled([
|
||||
nasGet('/api/lotto/latest'),
|
||||
nasGet('/api/lotto/analysis'),
|
||||
nasGet('/api/lotto/simulation'),
|
||||
]);
|
||||
|
||||
return NextResponse.json({
|
||||
ok: true,
|
||||
plan: order.product_id,
|
||||
latest: latest.status === 'fulfilled' ? latest.value : null,
|
||||
analysis: analysis.status === 'fulfilled' ? analysis.value : null,
|
||||
simulation: simulation.status === 'fulfilled' ? simulation.value : null,
|
||||
});
|
||||
} catch (err: unknown) {
|
||||
const e = err as { name?: string; message?: string };
|
||||
if (e?.name === 'TimeoutError') {
|
||||
return NextResponse.json({ error: 'NAS_TIMEOUT' }, { status: 504 });
|
||||
}
|
||||
console.error('Lotto dashboard error:', err);
|
||||
return NextResponse.json({ error: 'INTERNAL_ERROR' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
|
||||
/**
|
||||
* GET /api/lotto/debug
|
||||
* NAS 엔드포인트 접근 가능 여부 일괄 진단
|
||||
* 배포 후 브라우저에서 직접 호출: https://www.jaengseung-made.com/api/lotto/debug
|
||||
*/
|
||||
export async function GET() {
|
||||
const base = process.env.NAS_LOTTO_API_URL;
|
||||
if (!base) return NextResponse.json({ error: 'NAS_URL_NOT_CONFIGURED' }, { status: 500 });
|
||||
|
||||
const paths = [
|
||||
// 현재 코드에서 호출 중인 경로들
|
||||
'/api/lotto/recommend',
|
||||
'/api/lotto/recommend/batch',
|
||||
'/api/lotto/latest',
|
||||
'/api/lotto/analysis',
|
||||
'/api/lotto/simulation',
|
||||
'/api/lotto/stats',
|
||||
'/api/lotto/stats/performance',
|
||||
'/api/lotto/report/latest',
|
||||
'/api/lotto/report/history',
|
||||
'/api/lotto/purchase',
|
||||
'/api/lotto/purchase/stats',
|
||||
'/api/lotto/analysis/personal',
|
||||
'/api/history',
|
||||
// NAS API 스펙 문서
|
||||
'/openapi.json',
|
||||
'/docs',
|
||||
];
|
||||
|
||||
const results = await Promise.all(
|
||||
paths.map(async (path) => {
|
||||
const start = Date.now();
|
||||
try {
|
||||
const res = await fetch(`${base}${path}`, {
|
||||
signal: AbortSignal.timeout(6000),
|
||||
});
|
||||
const ms = Date.now() - start;
|
||||
let body: unknown = null;
|
||||
try { body = await res.json(); } catch { /* ignore */ }
|
||||
return { path, status: res.status, ok: res.ok, ms, body };
|
||||
} catch (err) {
|
||||
const e = err as { name?: string; message?: string; code?: string };
|
||||
return {
|
||||
path,
|
||||
status: null,
|
||||
ok: false,
|
||||
ms: Date.now() - start,
|
||||
error: e.code ?? e.name ?? e.message,
|
||||
};
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
const ok = results.filter(r => r.ok);
|
||||
const fail = results.filter(r => !r.ok);
|
||||
|
||||
return NextResponse.json({
|
||||
base,
|
||||
summary: { total: results.length, ok: ok.length, fail: fail.length },
|
||||
ok: ok.map(r => ({ path: r.path, status: r.status, ms: r.ms })),
|
||||
fail: fail.map(r => ({ path: r.path, status: r.status, error: (r as { error?: string }).error, ms: r.ms })),
|
||||
full: results,
|
||||
});
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { createClient } from '@/lib/supabase/server';
|
||||
|
||||
/**
|
||||
* POST /api/lotto/history
|
||||
* 생성된 로또 번호 조합을 히스토리에 저장
|
||||
* Body: { numbers: number[], source: 'nas' | 'client', plan_id: string }
|
||||
*
|
||||
* GET /api/lotto/history
|
||||
* 내 로또 번호 히스토리 조회
|
||||
* Query: limit (기본 50)
|
||||
*/
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
const supabase = await createClient();
|
||||
const { data: { user }, error: authError } = await supabase.auth.getUser();
|
||||
if (authError || !user) {
|
||||
return NextResponse.json({ error: 'UNAUTHORIZED' }, { status: 401 });
|
||||
}
|
||||
|
||||
let body: { numbers?: number[]; source?: string; plan_id?: string };
|
||||
try {
|
||||
body = await req.json();
|
||||
} catch {
|
||||
return NextResponse.json({ error: 'INVALID_JSON' }, { status: 400 });
|
||||
}
|
||||
|
||||
const { numbers, source = 'client', plan_id } = body;
|
||||
if (!Array.isArray(numbers) || numbers.length !== 6 || !plan_id) {
|
||||
return NextResponse.json({ error: 'INVALID_BODY' }, { status: 400 });
|
||||
}
|
||||
|
||||
const { error } = await supabase.from('lotto_history').insert({
|
||||
user_id: user.id,
|
||||
numbers,
|
||||
source,
|
||||
plan_id,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
console.error('lotto_history insert error:', error);
|
||||
return NextResponse.json({ error: 'DB_ERROR' }, { status: 500 });
|
||||
}
|
||||
|
||||
return NextResponse.json({ ok: true });
|
||||
}
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
const supabase = await createClient();
|
||||
const { data: { user }, error: authError } = await supabase.auth.getUser();
|
||||
if (authError || !user) {
|
||||
return NextResponse.json({ error: 'UNAUTHORIZED' }, { status: 401 });
|
||||
}
|
||||
|
||||
const limit = Math.min(Number(req.nextUrl.searchParams.get('limit') ?? '50'), 200);
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from('lotto_history')
|
||||
.select('id, numbers, source, plan_id, created_at')
|
||||
.eq('user_id', user.id)
|
||||
.order('created_at', { ascending: false })
|
||||
.limit(limit);
|
||||
|
||||
if (error) {
|
||||
return NextResponse.json({ error: 'DB_ERROR' }, { status: 500 });
|
||||
}
|
||||
|
||||
return NextResponse.json({ ok: true, history: data ?? [] });
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
|
||||
/**
|
||||
* GET /api/lotto/preview
|
||||
* 인증 없이 NAS /api/lotto/recommend 단일 호출 (맛보기용 무료 추천)
|
||||
* NAS 미연결 시 → { error: 'NAS_UNAVAILABLE' } 503 반환
|
||||
* 클라이언트에서 이 경우 자체 Monte Carlo 폴백 처리
|
||||
*/
|
||||
export async function GET() {
|
||||
const base = process.env.NAS_LOTTO_API_URL;
|
||||
|
||||
if (!base) {
|
||||
return NextResponse.json({ error: 'NAS_UNAVAILABLE' }, { status: 503 });
|
||||
}
|
||||
|
||||
try {
|
||||
const headers: Record<string, string> = {};
|
||||
if (process.env.NAS_LOTTO_API_KEY) {
|
||||
headers['Authorization'] = `Bearer ${process.env.NAS_LOTTO_API_KEY}`;
|
||||
}
|
||||
|
||||
const res = await fetch(`${base}/api/lotto/recommend`, {
|
||||
method: 'GET',
|
||||
headers,
|
||||
signal: AbortSignal.timeout(8000),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
console.warn(`[lotto/preview] NAS returned ${res.status}`);
|
||||
return NextResponse.json({ error: 'NAS_UNAVAILABLE' }, { status: 503 });
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
return NextResponse.json({
|
||||
ok: true,
|
||||
source: 'nas',
|
||||
numbers: data.numbers ?? [],
|
||||
metrics: data.metrics ?? null,
|
||||
});
|
||||
} catch (err: unknown) {
|
||||
// ECONNREFUSED, 타임아웃 등 — 클라이언트 폴백 신호
|
||||
const e = err as { name?: string; code?: string; message?: string };
|
||||
console.warn('[lotto/preview] NAS unreachable:', e?.code ?? e?.message ?? e?.name);
|
||||
return NextResponse.json({ error: 'NAS_UNAVAILABLE' }, { status: 503 });
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { nasPut, nasDelete, requireSubscription, handleNasError } from '../../_nas';
|
||||
|
||||
export const maxDuration = 60;
|
||||
|
||||
export async function PUT(request: Request, { params }: { params: Promise<{ id: string }> }) {
|
||||
try {
|
||||
const auth = await requireSubscription();
|
||||
if (auth instanceof NextResponse) return auth;
|
||||
const { id } = await params;
|
||||
const body = await request.json();
|
||||
const data = await nasPut(`/api/lotto/purchase/${id}`, body);
|
||||
return NextResponse.json(data);
|
||||
} catch (err) { return handleNasError(err); }
|
||||
}
|
||||
|
||||
export async function DELETE(_req: Request, { params }: { params: Promise<{ id: string }> }) {
|
||||
try {
|
||||
const auth = await requireSubscription();
|
||||
if (auth instanceof NextResponse) return auth;
|
||||
const { id } = await params;
|
||||
const data = await nasDelete(`/api/lotto/purchase/${id}`);
|
||||
return NextResponse.json(data);
|
||||
} catch (err) { return handleNasError(err); }
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { nasGet, nasPost, requireSubscription, handleNasError } from '../_nas';
|
||||
|
||||
export const maxDuration = 60;
|
||||
|
||||
export async function GET(request: Request) {
|
||||
try {
|
||||
const auth = await requireSubscription();
|
||||
if (auth instanceof NextResponse) return auth;
|
||||
const { searchParams } = new URL(request.url);
|
||||
const params = new URLSearchParams();
|
||||
if (searchParams.get('draw_no')) params.set('draw_no', searchParams.get('draw_no')!);
|
||||
if (searchParams.get('days')) params.set('days', searchParams.get('days')!);
|
||||
const qs = params.toString() ? `?${params}` : '';
|
||||
const data = await nasGet(`/api/lotto/purchase${qs}`);
|
||||
return NextResponse.json(data);
|
||||
} catch (err) { return handleNasError(err); }
|
||||
}
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const auth = await requireSubscription();
|
||||
if (auth instanceof NextResponse) return auth;
|
||||
const body = await request.json();
|
||||
const data = await nasPost('/api/lotto/purchase', body);
|
||||
return NextResponse.json(data, { status: 201 });
|
||||
} catch (err) { return handleNasError(err); }
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { nasGet, requireSubscription, handleNasError } from '../../_nas';
|
||||
|
||||
export const maxDuration = 60;
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const auth = await requireSubscription();
|
||||
if (auth instanceof NextResponse) return auth;
|
||||
const data = await nasGet('/api/lotto/purchase/stats');
|
||||
return NextResponse.json(data);
|
||||
} catch (err) { return handleNasError(err); }
|
||||
}
|
||||
@@ -1,122 +0,0 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { createClient } from '@/lib/supabase/server';
|
||||
|
||||
const LOTTO_PRODUCT_IDS = ['lotto_gold', 'lotto_platinum', 'lotto_diamond'];
|
||||
|
||||
/** 구독 유효 여부 확인 */
|
||||
async function checkSubscription(supabase: Awaited<ReturnType<typeof createClient>>, userId: string) {
|
||||
const { data: orders } = await supabase
|
||||
.from('orders')
|
||||
.select('id, product_id, status, created_at')
|
||||
.eq('user_id', userId)
|
||||
.eq('status', 'paid')
|
||||
.in('product_id', LOTTO_PRODUCT_IDS)
|
||||
.order('created_at', { ascending: false })
|
||||
.limit(1);
|
||||
|
||||
if (!orders || orders.length === 0) return null;
|
||||
|
||||
const order = orders[0];
|
||||
const diffDays =
|
||||
(Date.now() - new Date(order.created_at).getTime()) / (1000 * 60 * 60 * 24);
|
||||
const maxDays = order.product_id === 'lotto_annual' ? 366 : 31;
|
||||
|
||||
return diffDays <= maxDays ? order : null;
|
||||
}
|
||||
|
||||
/** NAS API 호출 헬퍼 */
|
||||
async function nasGet(path: string): Promise<Response> {
|
||||
const base = process.env.NAS_LOTTO_API_URL;
|
||||
if (!base) throw new Error('NAS_URL_NOT_CONFIGURED');
|
||||
|
||||
const headers: Record<string, string> = {};
|
||||
if (process.env.NAS_LOTTO_API_KEY) {
|
||||
headers['Authorization'] = `Bearer ${process.env.NAS_LOTTO_API_KEY}`;
|
||||
}
|
||||
|
||||
return fetch(`${base}${path}`, {
|
||||
method: 'GET',
|
||||
headers,
|
||||
signal: AbortSignal.timeout(15000),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/lotto/recommend
|
||||
* Query params:
|
||||
* mode = "single" (기본) | "batch" | "best"
|
||||
*
|
||||
* single → NAS GET /api/lotto/recommend
|
||||
* batch → NAS GET /api/lotto/recommend/batch (5개 조합)
|
||||
* best → NAS GET /api/lotto/best (Monte Carlo 상위 20쌍)
|
||||
*/
|
||||
export async function GET(req: NextRequest) {
|
||||
try {
|
||||
// 1. 세션 확인
|
||||
const supabase = await createClient();
|
||||
const { data: { user }, error: authError } = await supabase.auth.getUser();
|
||||
|
||||
if (authError || !user) {
|
||||
return NextResponse.json({ error: 'UNAUTHORIZED' }, { status: 401 });
|
||||
}
|
||||
|
||||
// 2. 구독 확인
|
||||
const order = await checkSubscription(supabase, user.id);
|
||||
if (!order) {
|
||||
return NextResponse.json({ error: 'NOT_SUBSCRIBED' }, { status: 403 });
|
||||
}
|
||||
|
||||
const mode = req.nextUrl.searchParams.get('mode') ?? 'single';
|
||||
|
||||
// 3. NAS API 호출
|
||||
const nasPath =
|
||||
mode === 'batch'
|
||||
? '/api/lotto/recommend/batch'
|
||||
: mode === 'best'
|
||||
? '/api/lotto/best'
|
||||
: '/api/lotto/recommend';
|
||||
|
||||
let nasRes: Response;
|
||||
try {
|
||||
nasRes = await nasGet(nasPath);
|
||||
} catch (fetchErr: unknown) {
|
||||
const e = fetchErr as { name?: string };
|
||||
console.warn('NAS unreachable:', fetchErr);
|
||||
if (e?.name === 'TimeoutError') {
|
||||
return NextResponse.json(
|
||||
{ error: 'NAS_UNAVAILABLE', plan: order.product_id, mode },
|
||||
{ status: 503 }
|
||||
);
|
||||
}
|
||||
return NextResponse.json(
|
||||
{ error: 'NAS_UNAVAILABLE', plan: order.product_id, mode },
|
||||
{ status: 503 }
|
||||
);
|
||||
}
|
||||
|
||||
if (!nasRes.ok) {
|
||||
const errText = await nasRes.text();
|
||||
console.error('NAS API error:', nasRes.status, errText);
|
||||
return NextResponse.json(
|
||||
{ error: 'NAS_UNAVAILABLE', plan: order.product_id, mode },
|
||||
{ status: 503 }
|
||||
);
|
||||
}
|
||||
|
||||
const nasData = await nasRes.json();
|
||||
|
||||
return NextResponse.json({
|
||||
ok: true,
|
||||
plan: order.product_id,
|
||||
mode,
|
||||
...nasData,
|
||||
});
|
||||
} catch (err: unknown) {
|
||||
const e = err as { name?: string; message?: string };
|
||||
if (e?.message === 'NAS_URL_NOT_CONFIGURED') {
|
||||
return NextResponse.json({ error: 'NAS_URL_NOT_CONFIGURED' }, { status: 500 });
|
||||
}
|
||||
console.error('Lotto recommend error:', err);
|
||||
return NextResponse.json({ error: 'INTERNAL_ERROR' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { nasGet, requireSubscription, handleNasError } from '../../_nas';
|
||||
|
||||
export const maxDuration = 60;
|
||||
|
||||
export async function GET(request: Request) {
|
||||
try {
|
||||
const auth = await requireSubscription();
|
||||
if (auth instanceof NextResponse) return auth;
|
||||
const { searchParams } = new URL(request.url);
|
||||
const limit = searchParams.get('limit') ?? '10';
|
||||
const data = await nasGet(`/api/lotto/report/history?limit=${limit}`);
|
||||
return NextResponse.json(data);
|
||||
} catch (err) { return handleNasError(err); }
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { nasGet, requireSubscription, handleNasError } from '../../_nas';
|
||||
|
||||
export const maxDuration = 60;
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const auth = await requireSubscription();
|
||||
if (auth instanceof NextResponse) return auth;
|
||||
const data = await nasGet('/api/lotto/report/latest');
|
||||
return NextResponse.json(data);
|
||||
} catch (err) { return handleNasError(err); }
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { nasGet, handleNasError } from '../../_nas';
|
||||
|
||||
// 공개 집계 데이터 — 인증 불필요, Vercel CDN에서 10분 캐시
|
||||
export const maxDuration = 60;
|
||||
export const revalidate = 600; // 10분
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const data = await nasGet('/api/lotto/stats/performance');
|
||||
const res = NextResponse.json(data);
|
||||
res.headers.set('Cache-Control', 's-maxage=600, stale-while-revalidate=60');
|
||||
return res;
|
||||
} catch (err) { return handleNasError(err); }
|
||||
}
|
||||
@@ -1,167 +0,0 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { crawlAll } from '@/lib/ebay-tools/crawler';
|
||||
import { analyzeWithAI } from '@/lib/ebay-tools/ai-analyzer';
|
||||
import { calculatePricing } from '@/lib/ebay-tools/pricing';
|
||||
import type { SearchResult, PriceSource } from '@/lib/ebay-tools/types';
|
||||
|
||||
export const maxDuration = 60; // Vercel Pro timeout
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { partNumber, partName } = body;
|
||||
|
||||
if (!partNumber || typeof partNumber !== 'string' || partNumber.trim().length === 0) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: '품번을 입력해주세요.' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const trimmedPart = partNumber.trim();
|
||||
|
||||
if (trimmedPart.length > 50) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: '품번은 50자 이내로 입력해주세요.' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
if (!/^[a-zA-Z0-9\s\-_.\/]+$/.test(trimmedPart)) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: '품번에 허용되지 않는 문자가 포함되어 있습니다.' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const trimmedName = partName?.trim() || undefined;
|
||||
|
||||
// 1. 크롤링 (RockAuto + eBay)
|
||||
const crawlResults = await crawlAll(trimmedPart);
|
||||
|
||||
// 2. AI 분석 (Claude API)
|
||||
let aiResult;
|
||||
const hasApiKey = !!process.env.ANTHROPIC_API_KEY;
|
||||
|
||||
if (hasApiKey) {
|
||||
try {
|
||||
aiResult = await analyzeWithAI(trimmedPart, trimmedName, crawlResults);
|
||||
} catch (aiError) {
|
||||
console.error('[EbayParts] AI analysis failed, using fallback:', aiError);
|
||||
}
|
||||
}
|
||||
|
||||
// AI 실패 또는 API 키 없으면 크롤링 데이터에서 기본 추출
|
||||
if (!aiResult) {
|
||||
aiResult = buildFallbackResult(trimmedPart, trimmedName, crawlResults);
|
||||
}
|
||||
|
||||
// 3. 가격 비교 + 환율/관세 계산
|
||||
const priceSources: PriceSource[] = extractPrices(crawlResults);
|
||||
const pricing = await calculatePricing(priceSources, aiResult.basicInfo.partName);
|
||||
|
||||
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
||||
|
||||
const result: SearchResult = {
|
||||
success: true,
|
||||
data: {
|
||||
basicInfo: aiResult.basicInfo,
|
||||
listing: aiResult.listing,
|
||||
fitment: aiResult.fitment,
|
||||
pricing,
|
||||
rawData: Object.fromEntries(
|
||||
crawlResults.map(r => [r.source, { success: r.success, data: r.data, error: r.error }])
|
||||
),
|
||||
meta: {
|
||||
searchedAt: new Date().toISOString(),
|
||||
sourcesChecked: crawlResults.map(r => r.source),
|
||||
processingTime: `${elapsed}s`,
|
||||
aiModel: hasApiKey ? 'claude-sonnet-4-20250514' : 'fallback (no API key)',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return NextResponse.json(result, { status: 200 });
|
||||
} catch (error) {
|
||||
console.error('[EbayParts] Search error:', error);
|
||||
return NextResponse.json(
|
||||
{ success: false, error: '검색 처리 중 오류가 발생했습니다.' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 크롤링 결과에서 가격 추출
|
||||
function extractPrices(crawlResults: Awaited<ReturnType<typeof crawlAll>>): PriceSource[] {
|
||||
const prices: PriceSource[] = [];
|
||||
|
||||
for (const result of crawlResults) {
|
||||
if (!result.success) continue;
|
||||
|
||||
if (result.source === 'RockAuto') {
|
||||
const parts = (result.data.parts as Array<{ price?: string; name?: string }>) || [];
|
||||
for (const part of parts) {
|
||||
if (part.price) {
|
||||
const numericPrice = parseFloat(part.price.replace(/[^0-9.]/g, ''));
|
||||
if (!isNaN(numericPrice) && numericPrice > 0) {
|
||||
prices.push({
|
||||
site: 'RockAuto',
|
||||
price: numericPrice,
|
||||
currency: 'USD',
|
||||
url: String(result.data.searchUrl || ''),
|
||||
});
|
||||
break; // 첫 번째 가격만
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (result.source === 'eBay') {
|
||||
const listings = (result.data.listings as Array<{ price?: string; url?: string }>) || [];
|
||||
for (const listing of listings.slice(0, 2)) {
|
||||
if (listing.price) {
|
||||
const numericPrice = parseFloat(listing.price.replace(/[^0-9.]/g, ''));
|
||||
if (!isNaN(numericPrice) && numericPrice > 0) {
|
||||
prices.push({
|
||||
site: 'eBay (참고)',
|
||||
price: numericPrice,
|
||||
currency: 'USD',
|
||||
url: listing.url || '',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return prices;
|
||||
}
|
||||
|
||||
// AI 없이 기본 결과 생성
|
||||
function buildFallbackResult(
|
||||
partNumber: string,
|
||||
partName: string | undefined,
|
||||
crawlResults: Awaited<ReturnType<typeof crawlAll>>
|
||||
) {
|
||||
const name = partName || partNumber;
|
||||
|
||||
return {
|
||||
basicInfo: {
|
||||
partNumber,
|
||||
partName: name,
|
||||
brand: '',
|
||||
oemNumbers: [partNumber],
|
||||
category: 'eBay Motors > Parts & Accessories > Car & Truck Parts',
|
||||
},
|
||||
listing: {
|
||||
title: `${name} ${partNumber} Auto Part`,
|
||||
category: '',
|
||||
itemSpecifics: {
|
||||
'Manufacturer Part Number': partNumber,
|
||||
},
|
||||
},
|
||||
fitment: [],
|
||||
};
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { generateBlogPost } from '@/lib/blog-tools/generator';
|
||||
import type { BlogStyle, BlogTone, BlogLength } from '@/lib/blog-tools/types';
|
||||
|
||||
export const maxDuration = 60;
|
||||
|
||||
const VALID_STYLES: BlogStyle[] = ['informational', 'review', 'howto', 'listicle', 'comparison', 'story'];
|
||||
const VALID_TONES: BlogTone[] = ['professional', 'friendly', 'casual', 'formal'];
|
||||
const VALID_LENGTHS: BlogLength[] = ['short', 'medium', 'long'];
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { topic, keywords, style, tone, length, imageGuide, sections } = body;
|
||||
|
||||
// 유효성 검증
|
||||
if (!topic || typeof topic !== 'string' || topic.trim().length === 0) {
|
||||
return NextResponse.json({ success: false, error: '주제를 입력해주세요.' }, { status: 400 });
|
||||
}
|
||||
|
||||
if (topic.trim().length > 100) {
|
||||
return NextResponse.json({ success: false, error: '주제는 100자 이내로 입력해주세요.' }, { status: 400 });
|
||||
}
|
||||
|
||||
if (!Array.isArray(keywords) || keywords.length === 0) {
|
||||
return NextResponse.json({ success: false, error: '키워드를 최소 1개 입력해주세요.' }, { status: 400 });
|
||||
}
|
||||
|
||||
if (keywords.length > 10) {
|
||||
return NextResponse.json({ success: false, error: '키워드는 최대 10개까지 가능합니다.' }, { status: 400 });
|
||||
}
|
||||
|
||||
if (!VALID_STYLES.includes(style)) {
|
||||
return NextResponse.json({ success: false, error: '유효하지 않은 글 형식입니다.' }, { status: 400 });
|
||||
}
|
||||
|
||||
if (!VALID_TONES.includes(tone)) {
|
||||
return NextResponse.json({ success: false, error: '유효하지 않은 톤입니다.' }, { status: 400 });
|
||||
}
|
||||
|
||||
if (!VALID_LENGTHS.includes(length)) {
|
||||
return NextResponse.json({ success: false, error: '유효하지 않은 분량입니다.' }, { status: 400 });
|
||||
}
|
||||
|
||||
const sectionCount = Math.min(Math.max(Number(sections) || 4, 3), 8);
|
||||
|
||||
const result = await generateBlogPost({
|
||||
topic: topic.trim(),
|
||||
keywords: keywords.map((k: string) => k.trim()).filter(Boolean),
|
||||
style,
|
||||
tone,
|
||||
length,
|
||||
imageGuide: Boolean(imageGuide),
|
||||
sections: sectionCount,
|
||||
});
|
||||
|
||||
return NextResponse.json(result, { status: 200 });
|
||||
} catch (error) {
|
||||
console.error('[NaverBlog] Generate error:', error);
|
||||
return NextResponse.json(
|
||||
{ success: false, error: '블로그 글 생성 중 오류가 발생했습니다.' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user