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:
2026-04-15 00:56:34 +09:00
parent 441bf00b95
commit 5cc224a743
57 changed files with 1175 additions and 7911 deletions

View File

@@ -11,7 +11,10 @@
"Bash(git push:*)", "Bash(git push:*)",
"WebFetch(domain:jaengseung-made.com)", "WebFetch(domain:jaengseung-made.com)",
"Bash(npx vercel:*)", "Bash(npx vercel:*)",
"Bash(1:*)" "Bash(1:*)",
"Bash(npx next:*)",
"Bash(grep -E \"^d|\\\\.tsx$|\\\\.ts$\")",
"Bash(grep -E \"^d|\\\\.ts$\")"
] ]
} }
} }

2
.gitignore vendored
View File

@@ -39,3 +39,5 @@ yarn-error.log*
# typescript # typescript
*.tsbuildinfo *.tsbuildinfo
next-env.d.ts next-env.d.ts
.vercel

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 ?? [] });
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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: [],
};
}

View File

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

View File

@@ -4,15 +4,15 @@ import { useState } from 'react';
import { usePathname } from 'next/navigation'; import { usePathname } from 'next/navigation';
import Sidebar from './Sidebar'; import Sidebar from './Sidebar';
const AUTH_PATHS = ['/login', '/signup', '/admin']; const STANDALONE_PATHS = ['/login', '/signup', '/admin'];
export default function DashboardShell({ children }: { children: React.ReactNode }) { export default function DashboardShell({ children }: { children: React.ReactNode }) {
const [sidebarOpen, setSidebarOpen] = useState(false); const [sidebarOpen, setSidebarOpen] = useState(false);
const pathname = usePathname(); const pathname = usePathname();
const isAuthPage = AUTH_PATHS.some((p) => pathname.startsWith(p)); const isStandalone = STANDALONE_PATHS.some((p) => pathname.startsWith(p));
if (isAuthPage) { if (isStandalone) {
return <>{children}</>; return <>{children}</>;
} }

View File

@@ -0,0 +1,185 @@
'use client';
import { useState, useEffect } from 'react';
import Link from 'next/link';
interface Props {
isOpen: boolean;
onClose: () => void;
productName: string;
price: string;
bankInfo?: {
bank: string;
account: string;
holder: string;
};
}
const DEFAULT_BANK = {
bank: '토스뱅크',
account: '1000-0000-0000',
holder: '박재오',
};
export default function PurchaseAgreementModal({
isOpen,
onClose,
productName,
price,
bankInfo = DEFAULT_BANK,
}: Props) {
const [agreed, setAgreed] = useState(false);
const [email, setEmail] = useState('');
const [sent, setSent] = useState(false);
const [loading, setLoading] = useState(false);
useEffect(() => {
if (!isOpen) {
setAgreed(false);
setEmail('');
setSent(false);
}
}, [isOpen]);
if (!isOpen) return null;
const handleSubmit = async () => {
if (!agreed || !email) return;
setLoading(true);
try {
await fetch('/api/contact', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
service: `구매 신청: ${productName}`,
name: email.split('@')[0],
email,
phone: '',
message: `상품: ${productName} (${price})\n입금 대기 중. 입금 확인 후 이메일로 상품 전달 예정.`,
}),
});
setSent(true);
} catch (e) {
alert('신청 전송 실패. 다시 시도해주세요.');
} finally {
setLoading(false);
}
};
return (
<div
className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/70 backdrop-blur-sm"
onClick={onClose}
>
<div
className="bg-white rounded-2xl w-full max-w-lg max-h-[90vh] overflow-y-auto"
onClick={(e) => e.stopPropagation()}
>
<div className="bg-gradient-to-br from-slate-900 to-slate-800 px-6 py-5 text-white">
<h3 className="font-extrabold text-lg">{productName}</h3>
<p className="text-slate-300 text-sm mt-0.5">{price}</p>
</div>
{sent ? (
<div className="p-8 text-center">
<div className="text-5xl mb-4"></div>
<h4 className="text-lg font-extrabold text-slate-900 mb-2"> </h4>
<p className="text-sm text-slate-600 leading-relaxed">
<strong>24 </strong> .
</p>
<div className="mt-5 bg-slate-50 border border-slate-200 rounded-xl p-4 text-left">
<p className="text-xs text-slate-500 mb-1"> </p>
<p className="font-mono text-sm text-slate-900">
{bankInfo.bank} {bankInfo.account}
</p>
<p className="text-xs text-slate-600 mt-1"> {bankInfo.holder}</p>
</div>
<button
onClick={onClose}
className="mt-6 w-full bg-slate-900 text-white py-3 rounded-xl font-bold text-sm hover:bg-slate-800 transition"
>
</button>
</div>
) : (
<div className="p-6 space-y-5">
<div>
<label className="block text-xs font-bold text-slate-700 mb-2">
( )
</label>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="your@email.com"
className="w-full px-4 py-3 border border-slate-300 rounded-xl text-sm focus:outline-none focus:border-violet-500"
/>
</div>
<div className="bg-amber-50 border border-amber-200 rounded-xl p-4 text-xs text-slate-700 leading-relaxed">
<p className="font-bold text-amber-900 mb-2">📌 </p>
<ul className="space-y-1.5 list-disc pl-4">
<li>
<strong> </strong>, ( )
17 2 5 () <strong></strong>.
</li>
<li>
<strong> · </strong> .
</li>
<li>
· <strong> </strong>.
</li>
<li>
{' '}
<Link href="/legal/refund" className="underline text-amber-900 font-bold" target="_blank">
</Link>{' '}
.
</li>
</ul>
</div>
<label className="flex items-start gap-3 cursor-pointer">
<input
type="checkbox"
checked={agreed}
onChange={(e) => setAgreed(e.target.checked)}
className="mt-0.5 w-4 h-4 accent-violet-600"
/>
<span className="text-sm text-slate-700 leading-relaxed">
, {' '}
<strong className="text-slate-900"></strong>. ()
</span>
</label>
<div className="bg-slate-50 border border-slate-200 rounded-xl p-4 text-xs">
<p className="font-bold text-slate-900 mb-1">💳 방법: 계좌이체</p>
<p className="font-mono text-slate-700">
{bankInfo.bank} {bankInfo.account} ({bankInfo.holder})
</p>
<p className="text-slate-500 mt-2">
24 .
</p>
</div>
<div className="flex gap-2">
<button
onClick={onClose}
className="flex-1 py-3 border border-slate-300 rounded-xl text-sm font-bold text-slate-700 hover:bg-slate-50"
>
</button>
<button
onClick={handleSubmit}
disabled={!agreed || !email || loading}
className="flex-[2] py-3 bg-violet-600 hover:bg-violet-500 disabled:bg-slate-300 disabled:cursor-not-allowed text-white rounded-xl text-sm font-bold transition"
>
{loading ? '전송 중...' : '구매 신청'}
</button>
</div>
</div>
)}
</div>
</div>
);
}

View File

@@ -24,32 +24,21 @@ const navGroups: NavGroup[] = [
title: 'AI 상품', title: 'AI 상품',
items: [ items: [
{ {
href: '/services/prompt', href: '/services/music',
label: '프롬프트 스토어', label: 'AI 음악 마스터',
badge: 'HOT', badge: 'NEW',
icon: ( icon: (
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M9 19V6l12-3v13M9 19a3 3 0 11-6 0 3 3 0 016 0zm12-3a3 3 0 11-6 0 3 3 0 016 0z" />
</svg> </svg>
), ),
}, },
{ {
href: '/services/automation', href: '/services/blog',
label: '업무 자동화', label: '블로그 자동화',
icon: ( icon: (
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5M18.5 2.5a2.121 2.121 0 013 3L12 15l-4 1 1-4 9.5-9.5z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
),
},
{
href: '/services/ai-kit',
label: 'AI 자동화 키트',
badge: '구독',
icon: (
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg> </svg>
), ),
}, },
@@ -68,39 +57,6 @@ const navGroups: NavGroup[] = [
</svg> </svg>
), ),
}, },
{
href: '/tools',
label: '도구 쇼케이스',
badge: 'DEMO',
icon: (
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M19.428 15.428a2 2 0 00-1.022-.547l-2.387-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 10.172V5L8 4z" />
</svg>
),
},
],
},
{
title: '외주 의뢰',
items: [
{
href: '/freelance',
label: '외주 개발 문의',
icon: (
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M21 13.255A23.931 23.931 0 0112 15c-3.183 0-6.22-.62-9-1.745M16 6V4a2 2 0 00-2-2h-4a2 2 0 00-2 2v2m4 6h.01M5 20h14a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
</svg>
),
},
{
href: '/services/website',
label: '홈페이지 제작',
icon: (
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
</svg>
),
},
], ],
}, },
]; ];

View File

@@ -19,6 +19,7 @@ export const metadata: Metadata = {
'47건 납품 완료. 계약서 먼저, 납기 패널티, 소스코드 100% 인도. 연락 두절 없는 개발자.', '47건 납품 완료. 계약서 먼저, 납기 패널티, 소스코드 100% 인도. 연락 두절 없는 개발자.',
url: 'https://jaengseung-made.com/freelance', url: 'https://jaengseung-made.com/freelance',
}, },
robots: { index: false, follow: false },
}; };
export default function FreelanceLayout({ children }: { children: React.ReactNode }) { export default function FreelanceLayout({ children }: { children: React.ReactNode }) {

View File

@@ -72,7 +72,7 @@ const jsonLd = {
'@id': 'https://jaengseung-made.com/#business', '@id': 'https://jaengseung-made.com/#business',
name: '쟁승메이드', name: '쟁승메이드',
url: 'https://jaengseung-made.com', url: 'https://jaengseung-made.com',
description: 'AI 프롬프트 패키지, 업무 자동화, AI 사주 분석. 7년차 현직 개발자가 직접 만들고 운영하는 AI 도구 스토어.', description: 'AI 음악 작곡·뮤비 구조 설계 팩, 블로그 자동화, AI 사주 분석. 7년차 현직 개발자가 직접 만들고 운영하는 AI 크리에이티브 스토어.',
email: 'bgg8988@gmail.com', email: 'bgg8988@gmail.com',
telephone: '010-3907-1392', telephone: '010-3907-1392',
priceRange: '₩', priceRange: '₩',
@@ -81,11 +81,9 @@ const jsonLd = {
'@type': 'OfferCatalog', '@type': 'OfferCatalog',
name: '쟁승메이드 AI 도구 · 서비스', name: '쟁승메이드 AI 도구 · 서비스',
itemListElement: [ itemListElement: [
{ '@type': 'Offer', itemOffered: { '@type': 'Product', name: 'AI 프롬프트 패키지', url: 'https://jaengseung-made.com/services/prompt', description: 'ChatGPT·Claude 업무 최적화 프롬프트. 자소서, 마케팅, 이메일, 보고서 등.' } }, { '@type': 'Offer', itemOffered: { '@type': 'Product', name: 'AI 음악 마스터 구조 팩', url: 'https://jaengseung-made.com/services/music', description: 'Suno 프롬프트 + MV 워크플로우 + 저작권 가이드 + 템플릿 PDF + 샘플 프로젝트. 4단계 AI 음악 제작 공정.' } },
{ '@type': 'Offer', itemOffered: { '@type': 'Service', name: '업무 자동화 개발', url: 'https://jaengseung-made.com/services/automation' } }, { '@type': 'Offer', itemOffered: { '@type': 'Product', name: '블로그 자동화 솔루션 팩', url: 'https://jaengseung-made.com/services/blog', description: '쿠팡파트너스·애드포스트 수익화 프롬프트 조합법 + 구조 템플릿 PDF + 샘플.' } },
{ '@type': 'Offer', itemOffered: { '@type': 'Product', name: 'AI 자동화 키트', url: 'https://jaengseung-made.com/services/ai-kit', description: '업무일지·이메일·SNS 자동화 도구 6종 월 구독.' } },
{ '@type': 'Offer', itemOffered: { '@type': 'Service', name: 'AI 사주 분석', url: 'https://jaengseung-made.com/saju', description: '생년월일 기반 AI 사주팔자 분석. 무료 체험 가능.' } }, { '@type': 'Offer', itemOffered: { '@type': 'Service', name: 'AI 사주 분석', url: 'https://jaengseung-made.com/saju', description: '생년월일 기반 AI 사주팔자 분석. 무료 체험 가능.' } },
{ '@type': 'Offer', itemOffered: { '@type': 'Service', name: '맞춤 외주 개발', url: 'https://jaengseung-made.com/freelance' } },
], ],
}, },
}, },

View File

@@ -18,7 +18,11 @@ export default function RefundPage() {
<section> <section>
<h2 className="text-lg font-bold text-slate-800 mt-8 mb-3">1. ( )</h2> <h2 className="text-lg font-bold text-slate-800 mt-8 mb-3">1. ( )</h2>
<p className="font-medium text-slate-700">대상: 프롬프트 , AI , </p> <p className="font-medium text-slate-700">대상: AI , , AI </p>
<p className="text-xs text-slate-500 mt-2">
17 2 5 , .
· , .
</p>
<table className="w-full text-sm border border-slate-200 mt-3"> <table className="w-full text-sm border border-slate-200 mt-3">
<thead> <thead>
<tr className="bg-slate-50"> <tr className="bg-slate-50">

View File

@@ -545,7 +545,7 @@ export default function MyPage() {
icon="📦" icon="📦"
title="활성 구독이 없습니다" title="활성 구독이 없습니다"
desc="구독 중인 서비스가 없습니다" desc="구독 중인 서비스가 없습니다"
linkHref="/services/prompt" linkHref="/services/music"
linkLabel="서비스 둘러보기" linkLabel="서비스 둘러보기"
/> />
) : ( ) : (
@@ -645,7 +645,7 @@ export default function MyPage() {
{/* 서비스 이동 */} {/* 서비스 이동 */}
<div className="text-center py-2"> <div className="text-center py-2">
<a href="/services/prompt" className="text-sm text-slate-400 hover:text-slate-600 transition"> <a href="/services/music" className="text-sm text-slate-400 hover:text-slate-600 transition">
</a> </a>
</div> </div>

View File

@@ -6,75 +6,16 @@ import ContactModal from './components/ContactModal';
import { trackCTAClick } from '../lib/gtag'; import { trackCTAClick } from '../lib/gtag';
/* ═══════════════════════════════════════════════════ /* ═══════════════════════════════════════════════════
쟁승메이드 홈페이지 — v3 (수익 구조 개편) 쟁승메이드 홈 — v4 (AI Music 중심 개편)
설계 원칙: 1. Hero: AI 음악 팩 (메인 매출)
1. AI 상품 → 무료 도구 → 외주 순서로 노출 2. Sub: 사주 · 블로그팩 · 일반 문의
2. 매출 전환 동선 최우선 3. About: 신뢰 지표
3. 증거 기반 신뢰 확보
═══════════════════════════════════════════════════ */ ═══════════════════════════════════════════════════ */
/* ── AI 상품 (매출 핵심) ─────────────────────────── */
const AI_PRODUCTS = [
{
href: '/services/prompt',
tag: 'BEST',
tagColor: 'bg-rose-500/15 text-rose-400 border-rose-500/20',
title: '프롬프트 패키지',
desc: 'ChatGPT·Claude 업무 최적화 프롬프트. 자소서, 마케팅, 이메일, 보고서 등 즉시 다운로드.',
price: '9,900원~',
action: '스토어 보기',
},
{
href: '/services/automation',
tag: 'HOT',
tagColor: 'bg-amber-500/15 text-amber-400 border-amber-500/20',
title: '업무 자동화 개발',
desc: '엑셀 처리, 이메일·보고서 자동화, 데이터 수집 스크립트. 반복 업무를 없앱니다.',
price: '5만원~',
action: '자세히 보기',
},
{
href: '/services/ai-kit',
tag: '구독',
tagColor: 'bg-violet-500/15 text-violet-400 border-violet-500/20',
title: 'AI 자동화 키트',
desc: '업무일지·이메일·SNS 자동화 도구 6종. 설치 없이 월 구독으로 바로 사용.',
price: '19,900원/월',
action: '구독 시작',
},
];
/* ── 무료 도구 (트래픽 엔진) ─────────────────────── */
const FREE_TOOLS = [
{
href: '/saju',
icon: (
<svg className="w-6 h-6 text-violet-500" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth={1.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M9.813 15.904L9 18.75l-.813-2.846a4.5 4.5 0 00-3.09-3.09L2.25 12l2.846-.813a4.5 4.5 0 003.09-3.09L9 5.25l.813 2.846a4.5 4.5 0 003.09 3.09L15.75 12l-2.846.813a4.5 4.5 0 00-3.09 3.09zM18.259 8.715L18 9.75l-.259-1.035a3.375 3.375 0 00-2.455-2.456L14.25 6l1.036-.259a3.375 3.375 0 002.455-2.456L18 2.25l.259 1.035a3.375 3.375 0 002.455 2.456L21.75 6l-1.036.259a3.375 3.375 0 00-2.455 2.456z" />
</svg>
),
title: 'AI 사주 분석',
desc: '생년월일 입력하면 AI가 성격·직업·관계·운세를 즉시 분석',
badge: '무료',
},
{
href: '/tools',
icon: (
<svg className="w-6 h-6 text-amber-500" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth={1.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M9.75 3.104v5.714a2.25 2.25 0 01-.659 1.591L5 14.5M9.75 3.104c-.251.023-.501.05-.75.082m.75-.082a24.301 24.301 0 014.5 0m0 0v5.714c0 .597.237 1.17.659 1.591L19.8 15.3M14.25 3.104c.251.023.501.05.75.082M19.8 15.3l-1.57.393A9.065 9.065 0 0112 15a9.065 9.065 0 00-6.23.693L5 14.5m14.8.8l1.402 1.402c1.232 1.232.65 3.318-1.067 3.611A48.309 48.309 0 0112 21c-2.773 0-5.491-.235-8.135-.687-1.718-.293-2.3-2.379-1.067-3.61L5 14.5" />
</svg>
),
title: '도구 쇼케이스',
desc: '네이버 블로그 자동화, eBay 리스팅 등 AI 도구 데모',
badge: 'DEMO',
},
];
/* ── 운영 중 서비스 (신뢰 지표) ──────────────────── */
const LIVE_SERVICES = [ const LIVE_SERVICES = [
{ name: '쟁승메이드', label: '이 사이트' }, { name: 'AI Music Pack', label: '메인 상품' },
{ name: 'AI 사주 분석', label: '서비스' }, { name: 'AI 사주 분석', label: '도구' },
{ name: 'AI 자동화 키트', label: '월 구독' }, { name: '블로그 자동화 ', label: '디지털 상품' },
]; ];
function useScrollReveal() { function useScrollReveal() {
@@ -108,99 +49,114 @@ export default function Home() {
<ContactModal <ContactModal
isOpen={modalOpen} isOpen={modalOpen}
onClose={() => setModalOpen(false)} onClose={() => setModalOpen(false)}
service="외주 개발 문의" service="일반 문의"
checklist={[ checklist={[
'개발하고 싶은 서비스를 간략히 설명해주세요', '문의하고 싶은 내용을 간략히 설명해주세요',
'희망 납품 일정과 예산 범위', '원하는 회신 방식 (이메일/전화)',
'참고할 만한 사이트나 레퍼런스', '기타 참고 사항',
]} ]}
accentColor="text-[#5ba4ff]" accentColor="text-violet-400"
headerFrom="#04102b" headerFrom="#1e1b4b"
headerTo="#0a2060" headerTo="#020617"
/> />
{/* ══════════════════════════════════════ {/* ══════════════════════════════════════
HERO — AI 상품 중심 HERO — AI Music 중심
══════════════════════════════════════ */} ══════════════════════════════════════ */}
<section <section
className="relative overflow-hidden px-6 py-20 lg:px-14 lg:py-28" className="relative overflow-hidden px-6 py-24 lg:px-14 lg:py-32"
style={{ background: '#04102b' }} style={{
background:
'radial-gradient(circle at 20% 30%, #1e1b4b 0%, #020617 55%), radial-gradient(circle at 80% 70%, #0c4a6e 0%, transparent 50%)',
}}
> >
{/* Noise overlay */}
<div <div
className="absolute inset-0 opacity-[0.025]" className="absolute inset-0 opacity-[0.04] pointer-events-none mix-blend-overlay"
style={{ style={{
backgroundImage: 'repeating-linear-gradient(45deg, #4f8ef7 0, #4f8ef7 1px, transparent 0, transparent 50%)', backgroundImage:
backgroundSize: '20px 20px', "url(\"data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='120' height='120'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.9'/></filter><rect width='100%' height='100%' filter='url(%23n)' opacity='0.5'/></svg>\")",
}} }}
/> />
{/* Waveform decoration */}
<div className="absolute bottom-0 left-0 right-0 h-32 opacity-30 pointer-events-none">
<svg viewBox="0 0 1200 120" preserveAspectRatio="none" className="w-full h-full">
<path
d="M0,60 Q150,10 300,60 T600,60 T900,60 T1200,60 L1200,120 L0,120 Z"
fill="url(#waveGrad)"
/>
<defs>
<linearGradient id="waveGrad" x1="0%" x2="100%">
<stop offset="0%" stopColor="#7c3aed" stopOpacity="0.4" />
<stop offset="50%" stopColor="#06b6d4" stopOpacity="0.3" />
<stop offset="100%" stopColor="#7c3aed" stopOpacity="0.4" />
</linearGradient>
</defs>
</svg>
</div>
<div className="relative max-w-5xl"> <div className="relative max-w-5xl mx-auto">
<div className="flex items-center gap-3 mb-8"> <div className="flex items-center gap-3 mb-8">
<span className="font-mono text-xs text-[#5ba4ff]/60 tracking-[0.2em] uppercase"> <span className="font-mono text-xs text-violet-300/70 tracking-[0.25em] uppercase">
· AI · × AI Music
</span> </span>
<span className="flex items-center gap-1.5 text-xs text-emerald-400/80"> <span className="flex items-center gap-1.5 text-xs text-emerald-400/80">
<span className="w-1.5 h-1.5 rounded-full bg-emerald-400 animate-pulse" /> <span className="w-1.5 h-1.5 rounded-full bg-emerald-400 animate-pulse" />
NEW
</span> </span>
</div> </div>
<h1 <h1
className="text-[2.6rem] md:text-[3.5rem] lg:text-[4.5rem] font-extrabold leading-[1.12] tracking-tight text-white mb-6" className="text-[2.6rem] md:text-[3.5rem] lg:text-[5rem] font-extrabold leading-[1.05] tracking-tight mb-6"
style={{ wordBreak: 'keep-all' }} style={{ wordBreak: 'keep-all' }}
> >
AI로 <span className="text-white"> .</span>
<br /> <br />
<span className="text-[#5ba4ff]">.</span> <span className="bg-gradient-to-r from-violet-300 via-sky-200 to-cyan-300 bg-clip-text text-transparent">
.
</span>
</h1> </h1>
<p <p
className="text-[#8ba5cc] text-lg md:text-xl leading-relaxed mb-3 max-w-2xl" className="text-slate-300 text-lg md:text-xl leading-relaxed mb-4 max-w-2xl"
style={{ wordBreak: 'keep-all' }} style={{ wordBreak: 'keep-all' }}
> >
, AI . 7 <span className="text-white font-semibold">AI 4 </span>.
<br /> <br />
7 . (Suno) (Runway) .
</p> </p>
<p <p className="text-slate-400 text-base mb-10 max-w-2xl">
className="text-white/50 text-base leading-relaxed mb-10 max-w-2xl" 39,000. .
style={{ wordBreak: 'keep-all' }}
>
9,900 AI .
</p> </p>
<div className="flex flex-wrap gap-3 mb-14"> <div className="flex flex-wrap gap-3 mb-14">
<Link <Link
href="/services/prompt" href="/services/music"
className="inline-flex items-center gap-2 bg-[#1a56db] hover:bg-[#1e4fc2] text-white px-8 py-4 rounded-xl font-bold text-sm transition-colors" className="inline-flex items-center gap-2 bg-violet-600 hover:bg-violet-500 text-white px-8 py-4 rounded-xl font-bold text-sm transition-colors shadow-[0_0_40px_rgba(139,92,246,0.4)]"
> >
AI
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth={2}> <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M17 8l4 4m0 0l-4 4m4-4H3" /> <path strokeLinecap="round" strokeLinejoin="round" d="M17 8l4 4m0 0l-4 4m4-4H3" />
</svg> </svg>
</Link> </Link>
<Link <Link
href="/saju" href="/saju"
className="inline-flex items-center gap-2 border border-white/15 hover:border-white/30 text-white/60 hover:text-white px-8 py-4 rounded-xl font-semibold text-sm transition-all" className="inline-flex items-center gap-2 border border-white/15 hover:border-white/40 text-white/80 hover:text-white px-8 py-4 rounded-xl font-semibold text-sm transition-all"
> >
</Link> </Link>
</div> </div>
{/* Live services indicator */}
<div className="border-t border-white/8 pt-8"> <div className="border-t border-white/8 pt-8">
<p className="font-mono text-[11px] text-[#5ba4ff]/40 tracking-[0.25em] uppercase mb-4"> <p className="font-mono text-[11px] text-violet-300/40 tracking-[0.25em] uppercase mb-4">
</p> </p>
<div className="flex flex-wrap gap-6"> <div className="flex flex-wrap gap-6">
{LIVE_SERVICES.map((s) => ( {LIVE_SERVICES.map((s) => (
<span <span key={s.name} className="flex items-center gap-2.5 text-sm text-slate-300">
key={s.name}
className="flex items-center gap-2.5 text-sm text-[#8ba5cc]"
>
<span className="w-1.5 h-1.5 rounded-full bg-emerald-400 animate-pulse flex-shrink-0" /> <span className="w-1.5 h-1.5 rounded-full bg-emerald-400 animate-pulse flex-shrink-0" />
<span className="font-semibold">{s.name}</span> <span className="font-semibold">{s.name}</span>
<span className="font-mono text-[11px] text-white/25">{s.label}</span> <span className="font-mono text-[11px] text-white/30">{s.label}</span>
</span> </span>
))} ))}
</div> </div>
@@ -209,111 +165,108 @@ export default function Home() {
</section> </section>
{/* ══════════════════════════════════════ {/* ══════════════════════════════════════
SECTION 2 — AI 상품 (매출 핵심) SECTION 2 — 서브 상품 카드
══════════════════════════════════════ */} ══════════════════════════════════════ */}
<section className="bg-white px-6 py-16 lg:px-14 lg:py-20"> <section className="bg-white px-6 py-20 lg:px-14">
<div className="max-w-5xl mx-auto"> <div className="max-w-5xl mx-auto">
<div className="reveal mb-10"> <div className="reveal mb-10">
<p className="font-mono text-xs text-[#1a56db]/60 tracking-widest uppercase mb-2"> <p className="font-mono text-xs text-violet-700/70 tracking-widest uppercase mb-2">
AI Products More Products
</p> </p>
<h2 <h2
className="text-2xl md:text-3xl font-extrabold text-[#04102b] leading-tight" className="text-2xl md:text-3xl font-extrabold text-slate-900 leading-tight"
style={{ wordBreak: 'keep-all' }} style={{ wordBreak: 'keep-all' }}
> >
AI .
</h2> </h2>
<p className="text-[#64748b] text-sm mt-2" style={{ wordBreak: 'keep-all' }}> <p className="text-slate-500 text-sm mt-2"> .</p>
. .
</p>
</div> </div>
<div className="grid md:grid-cols-3 gap-5"> <div className="grid sm:grid-cols-2 lg:grid-cols-3 gap-5">
{AI_PRODUCTS.map((p, i) => ( {/* 사주 */}
<Link <Link
key={p.href} href="/saju"
href={p.href} className="reveal reveal-d1 group relative flex flex-col border border-slate-200 hover:border-violet-400/50 rounded-2xl p-6 transition-all hover:shadow-lg bg-gradient-to-br from-white to-violet-50/40"
className={`reveal reveal-d${i + 1} group relative flex flex-col border border-[#e2e8f0] hover:border-[#1a56db]/30 rounded-2xl p-6 transition-all hover:shadow-lg hover:shadow-blue-500/5 bg-white`}
>
<div className="flex items-center justify-between mb-4">
<span className={`text-[10px] font-bold px-2 py-0.5 rounded-full border ${p.tagColor}`}>
{p.tag}
</span>
<span className="text-[#0f172a] font-extrabold text-sm">{p.price}</span>
</div>
<h3 className="text-lg font-extrabold text-[#0f172a] mb-2 group-hover:text-[#1a56db] transition-colors">
{p.title}
</h3>
<p className="text-[#64748b] text-sm leading-relaxed flex-1 mb-5" style={{ wordBreak: 'keep-all' }}>
{p.desc}
</p>
<div className="flex items-center justify-between">
<span className="text-[#1a56db] text-sm font-bold group-hover:underline underline-offset-4">
{p.action}
</span>
</div>
</Link>
))}
</div>
</div>
</section>
{/* ══════════════════════════════════════
SECTION 3 — 무료 도구 (트래픽 엔진)
══════════════════════════════════════ */}
<section className="bg-[#f8faff] px-6 py-16 lg:px-14 lg:py-20">
<div className="max-w-5xl mx-auto">
<div className="reveal mb-10">
<p className="font-mono text-xs text-[#1a56db]/60 tracking-widest uppercase mb-2">
Free Tools
</p>
<h2
className="text-2xl md:text-3xl font-extrabold text-[#04102b] leading-tight"
style={{ wordBreak: 'keep-all' }}
> >
<div className="flex items-center justify-between mb-4">
</h2> <span className="text-[10px] font-bold px-2 py-0.5 rounded-full bg-sky-500/15 text-sky-600 border border-sky-500/20">
<p className="text-[#64748b] text-sm mt-2">
. </span>
</p> <svg className="w-5 h-5 text-violet-500" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth={1.5}>
</div> <path strokeLinecap="round" strokeLinejoin="round" d="M9.813 15.904L9 18.75l-.813-2.846a4.5 4.5 0 00-3.09-3.09L2.25 12l2.846-.813a4.5 4.5 0 003.09-3.09L9 5.25l.813 2.846a4.5 4.5 0 003.09 3.09L15.75 12l-2.846.813a4.5 4.5 0 00-3.09 3.09z" />
<div className="grid sm:grid-cols-3 gap-4">
{FREE_TOOLS.map((t, i) => (
<Link
key={t.href}
href={t.href}
className={`reveal reveal-d${i + 1} group flex items-start gap-4 bg-white border border-[#e2e8f0] hover:border-[#1a56db]/30 rounded-2xl p-5 transition-all hover:shadow-md`}
>
<span className="flex-shrink-0 mt-0.5">{t.icon}</span>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
<h3 className="font-bold text-[#0f172a] text-sm group-hover:text-[#1a56db] transition-colors">
{t.title}
</h3>
<span className="text-[10px] font-bold text-sky-500 bg-sky-500/10 px-1.5 py-0.5 rounded">
{t.badge}
</span>
</div>
<p className="text-[#64748b] text-xs leading-relaxed" style={{ wordBreak: 'keep-all' }}>
{t.desc}
</p>
</div>
<svg className="w-4 h-4 text-[#cbd5e1] group-hover:text-[#1a56db] flex-shrink-0 mt-1 transition-colors" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
</svg> </svg>
</Link> </div>
))} <h3 className="text-lg font-extrabold text-slate-900 mb-2 group-hover:text-violet-700 transition-colors">
AI
</h3>
<p className="text-slate-600 text-sm leading-relaxed flex-1 mb-5" style={{ wordBreak: 'keep-all' }}>
AI가 ··· .
</p>
<span className="text-violet-700 text-sm font-bold group-hover:underline">
</span>
</Link>
{/* 블로그팩 */}
<Link
href="/services/blog"
className="reveal reveal-d2 group relative flex flex-col border border-slate-200 hover:border-blue-400/50 rounded-2xl p-6 transition-all hover:shadow-lg bg-white"
>
<div className="flex items-center justify-between mb-4">
<span className="text-[10px] font-bold px-2 py-0.5 rounded-full bg-blue-500/15 text-blue-600 border border-blue-500/20">
29,000
</span>
<svg className="w-5 h-5 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth={1.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5M18.5 2.5a2.121 2.121 0 013 3L12 15l-4 1 1-4 9.5-9.5z" />
</svg>
</div>
<h3 className="text-lg font-extrabold text-slate-900 mb-2 group-hover:text-blue-700 transition-colors">
</h3>
<p className="text-slate-600 text-sm leading-relaxed flex-1 mb-5" style={{ wordBreak: 'keep-all' }}>
· ·릿· .
</p>
<span className="text-blue-700 text-sm font-bold group-hover:underline">
</span>
</Link>
{/* 일반 문의 */}
<button
onClick={() => {
trackCTAClick('일반 문의', '/');
setModalOpen(true);
}}
className="reveal reveal-d3 group relative flex flex-col text-left border border-slate-200 hover:border-slate-400 rounded-2xl p-6 transition-all hover:shadow-lg bg-white"
>
<div className="flex items-center justify-between mb-4">
<span className="text-[10px] font-bold px-2 py-0.5 rounded-full bg-slate-500/15 text-slate-600 border border-slate-500/20">
·
</span>
<svg className="w-5 h-5 text-slate-500" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth={1.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z" />
</svg>
</div>
<h3 className="text-lg font-extrabold text-slate-900 mb-2 group-hover:text-slate-700 transition-colors">
</h3>
<p className="text-slate-600 text-sm leading-relaxed flex-1 mb-5" style={{ wordBreak: 'keep-all' }}>
·· . 24 .
</p>
<span className="text-slate-700 text-sm font-bold group-hover:underline">
</span>
</button>
</div> </div>
</div> </div>
</section> </section>
{/* ══════════════════════════════════════ {/* ══════════════════════════════════════
SECTION 4누가 만드나요 (About) SECTION 3 — About
══════════════════════════════════════ */} ══════════════════════════════════════ */}
<section className="bg-[#04102b] px-6 py-16 lg:px-14 lg:py-20"> <section className="bg-slate-950 px-6 py-20 lg:px-14">
<div className="max-w-5xl mx-auto"> <div className="max-w-5xl mx-auto">
<p className="reveal font-mono text-xs text-[#5ba4ff]/40 tracking-widest uppercase mb-3"> <p className="reveal font-mono text-xs text-violet-300/50 tracking-widest uppercase mb-3">
About About
</p> </p>
<div className="reveal grid lg:grid-cols-2 gap-10 lg:gap-16 items-start"> <div className="reveal grid lg:grid-cols-2 gap-10 lg:gap-16 items-start">
@@ -324,45 +277,29 @@ export default function Home() {
> >
7 . 7 .
<br /> <br />
<span className="text-[#5ba4ff]"> .</span> <span className="text-violet-300"> AI로 .</span>
</h2> </h2>
<div className="space-y-4 text-[#8ba5cc] text-base leading-relaxed" style={{ wordBreak: 'keep-all' }}> <div className="space-y-4 text-slate-400 text-base leading-relaxed" style={{ wordBreak: 'keep-all' }}>
<p> <p>
IT팀에서 7 API , DB , . IT팀에서 7 API , DB, .
</p> </p>
<p> <p>
<span className="text-white">AI , , AI </span> . <span className="text-white">AI · · AI</span> ·.
</p> </p>
</div> </div>
<div className="mt-8">
<p className="font-mono text-[11px] text-[#5ba4ff]/40 tracking-widest uppercase mb-3">
Tech Stack
</p>
<div className="flex flex-wrap gap-2">
{['Next.js', 'TypeScript', 'Python', 'FastAPI', 'PostgreSQL', 'Supabase', 'Docker', 'AWS'].map((t) => (
<span
key={t}
className="font-mono text-xs text-[#5ba4ff]/70 border border-[#5ba4ff]/15 px-2.5 py-1 rounded"
>
{t}
</span>
))}
</div>
</div>
</div> </div>
<div className="space-y-4"> <div className="space-y-4">
{[ {[
{ value: '7년', label: '대기업 백엔드 개발 경력', sub: '실제 운영 서비스 다수 개발', color: 'border-blue-500/30' }, { value: '7년', label: '대기업 백엔드 경력', sub: '실제 운영 서비스 다수', color: 'border-blue-500/30' },
{ value: '4개', label: '현재 직접 운영 중인 서비스', sub: '이 사이트 포함 — 지금 이 순간도 작동 중', color: 'border-emerald-500/30' }, { value: '3개', label: '운영 중인 AI 서비스', sub: '사주 AI · 블로그팩 · 음악팩', color: 'border-emerald-500/30' },
{ value: '100%', label: '외주 시 소스코드 이관', sub: '납품전체 코드 전달, 락인 없음', color: 'border-violet-500/30' }, { value: '평생', label: '무료 업데이트', sub: '구매Notion 공지로 전달', color: 'border-violet-500/30' },
{ value: '24h', label: '이내 견적 답변', sub: '주말·공휴일 포함', color: 'border-amber-500/30' }, { value: '24h', label: '이내 답변', sub: '주말·공휴일 포함', color: 'border-amber-500/30' },
].map((item) => ( ].map((item) => (
<div key={item.value} className={`border-l-2 ${item.color} pl-5 py-2`}> <div key={item.value} className={`border-l-2 ${item.color} pl-5 py-2`}>
<div className="flex items-baseline gap-3"> <div className="flex items-baseline gap-3">
<span className="text-3xl font-extrabold text-white tracking-tight">{item.value}</span> <span className="text-3xl font-extrabold text-white tracking-tight">{item.value}</span>
<span className="text-[#8ba5cc] text-sm font-medium">{item.label}</span> <span className="text-slate-400 text-sm font-medium">{item.label}</span>
</div> </div>
<p className="text-white/30 text-xs mt-1">{item.sub}</p> <p className="text-white/30 text-xs mt-1">{item.sub}</p>
</div> </div>
@@ -373,92 +310,45 @@ export default function Home() {
</section> </section>
{/* ══════════════════════════════════════ {/* ══════════════════════════════════════
SECTION 5외주 개발 (축소) SECTION 4최종 CTA
══════════════════════════════════════ */} ══════════════════════════════════════ */}
<section className="bg-white px-6 py-16 lg:px-14 lg:py-20"> <section className="bg-gradient-to-b from-slate-950 to-[#0b0530] px-6 py-20 lg:px-14">
<div className="max-w-5xl mx-auto">
<div className="reveal flex items-end justify-between flex-wrap gap-4 mb-8">
<div>
<p className="font-mono text-xs text-[#1a56db]/60 tracking-widest uppercase mb-2">
Custom Development
</p>
<h2 className="text-2xl md:text-3xl font-extrabold text-[#04102b]" style={{ wordBreak: 'keep-all' }}>
?
</h2>
<p className="text-[#64748b] text-sm mt-2" style={{ wordBreak: 'keep-all' }}>
, , API .
</p>
</div>
</div>
<div className="reveal grid sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-8">
{[
{ icon: <svg className="w-6 h-6 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth={1.5}><path strokeLinecap="round" strokeLinejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" /></svg>, title: '계약서 먼저', desc: '범위·금액·납기를 문서로 확정' },
{ icon: <svg className="w-6 h-6 text-emerald-500" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth={1.5}><path strokeLinecap="round" strokeLinejoin="round" d="M3 13.125C3 12.504 3.504 12 4.125 12h2.25c.621 0 1.125.504 1.125 1.125v6.75C7.5 20.496 6.996 21 6.375 21h-2.25A1.125 1.125 0 013 19.875v-6.75zM9.75 8.625c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125v11.25c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 01-1.125-1.125V8.625zM16.5 4.125c0-.621.504-1.125 1.125-1.125h2.25C20.496 3 21 3.504 21 4.125v15.75c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 01-1.125-1.125V4.125z" /></svg>, title: '주 1회 보고', desc: '중간에 사라지는 일 없음' },
{ icon: <svg className="w-6 h-6 text-violet-500" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth={1.5}><path strokeLinecap="round" strokeLinejoin="round" d="M17.25 6.75L22.5 12l-5.25 5.25m-10.5 0L1.5 12l5.25-5.25m7.5-3l-4.5 16.5" /></svg>, title: '소스코드 100%', desc: '전체 코드 이관, 락인 없음' },
{ icon: <svg className="w-6 h-6 text-amber-500" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth={1.5}><path strokeLinecap="round" strokeLinejoin="round" d="M3.75 13.5l10.5-11.25L12 10.5h8.25L9.75 21.75 12 13.5H3.75z" /></svg>, title: '24시간 답변', desc: '주말·공휴일 포함 견적 회신' },
].map((item) => (
<div key={item.title} className="border border-[#e2e8f0] rounded-xl p-4 text-center">
<span className="mb-2 block flex justify-center">{item.icon}</span>
<p className="font-bold text-[#0f172a] text-sm mb-1">{item.title}</p>
<p className="text-[#94a3b8] text-xs" style={{ wordBreak: 'keep-all' }}>{item.desc}</p>
</div>
))}
</div>
<div className="reveal flex flex-wrap gap-3">
<Link
href="/freelance"
className="inline-flex items-center gap-2 bg-[#04102b] hover:bg-[#0a1a3f] text-white px-6 py-3 rounded-xl font-bold text-sm transition-colors"
>
·
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M17 8l4 4m0 0l-4 4m4-4H3" />
</svg>
</Link>
<Link
href="/services/website"
className="inline-flex items-center gap-2 border border-[#e2e8f0] hover:border-[#1a56db]/30 text-[#475569] hover:text-[#1a56db] px-6 py-3 rounded-xl font-semibold text-sm transition-all"
>
</Link>
</div>
</div>
</section>
{/* ══════════════════════════════════════
SECTION 6 — 최종 CTA
══════════════════════════════════════ */}
<section className="bg-[#04102b] px-6 py-20 lg:px-14">
<div className="max-w-5xl mx-auto"> <div className="max-w-5xl mx-auto">
<div className="reveal text-center"> <div className="reveal text-center">
<p className="font-mono text-xs text-[#5ba4ff]/40 tracking-widest uppercase mb-4">Get Started</p> <p className="font-mono text-xs text-violet-300/50 tracking-widest uppercase mb-4">
Get Started
</p>
<h2 <h2
className="text-3xl md:text-5xl font-extrabold text-white mb-4 leading-tight" className="text-3xl md:text-5xl font-extrabold text-white mb-4 leading-tight"
style={{ wordBreak: 'keep-all' }} style={{ wordBreak: 'keep-all' }}
> >
AI ,
<br /> <br />
. <span className="bg-gradient-to-r from-violet-300 via-sky-200 to-cyan-300 bg-clip-text text-transparent">
.
</span>
</h2> </h2>
<p className="text-[#8ba5cc] text-lg mb-10" style={{ wordBreak: 'keep-all' }}> <p className="text-slate-400 text-lg mb-10">
9,900 . . 39,000 ·
</p> </p>
<div className="flex flex-wrap gap-4 justify-center"> <div className="flex flex-wrap gap-4 justify-center">
<Link <Link
href="/services/prompt" href="/services/music"
className="inline-flex items-center gap-2 bg-[#1a56db] hover:bg-[#1e4fc2] text-white px-10 py-4 rounded-xl font-extrabold text-base transition-colors" className="inline-flex items-center gap-2 bg-violet-600 hover:bg-violet-500 text-white px-10 py-4 rounded-xl font-extrabold text-base transition-colors shadow-[0_0_40px_rgba(139,92,246,0.4)]"
> >
AI AI
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth={2}> <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M17 8l4 4m0 0l-4 4m4-4H3" /> <path strokeLinecap="round" strokeLinejoin="round" d="M17 8l4 4m0 0l-4 4m4-4H3" />
</svg> </svg>
</Link> </Link>
<button <button
onClick={() => { trackCTAClick('무료 상담 신청', '/'); setModalOpen(true); }} onClick={() => {
className="inline-flex items-center gap-2 border border-white/15 hover:border-white/30 text-white/70 hover:text-white px-10 py-4 rounded-xl font-extrabold text-base transition-all" trackCTAClick('일반 문의', '/');
setModalOpen(true);
}}
className="inline-flex items-center gap-2 border border-white/15 hover:border-white/40 text-white/80 hover:text-white px-10 py-4 rounded-xl font-extrabold text-base transition-all"
> >
·
</button> </button>
</div> </div>
<p className="text-white/20 text-xs mt-8 font-mono"> <p className="text-white/20 text-xs mt-8 font-mono">

View File

@@ -6,7 +6,7 @@ export default function robots(): MetadataRoute.Robots {
{ {
userAgent: '*', userAgent: '*',
allow: '/', allow: '/',
disallow: ['/admin/', '/api/', '/mypage/', '/payment/'], disallow: ['/admin/', '/api/', '/mypage/', '/payment/', '/freelance', '/services/website', '/portfolio/'],
}, },
], ],
sitemap: 'https://jaengseung-made.com/sitemap.xml', sitemap: 'https://jaengseung-made.com/sitemap.xml',

View File

@@ -1,351 +0,0 @@
'use client';
import { useMemo } from 'react';
import Link from 'next/link';
// 오행 기반 로또 번호 매핑 (하도낙서 원리)
// 水:1,6 / 火:2,7 / 木:3,8 / 金:4,9 / 土:5,10
const ELEMENT_NUMBERS: Record<string, number[]> = {
'水': [1, 6, 11, 16, 21, 26, 31, 36, 41],
'火': [2, 7, 12, 17, 22, 27, 32, 37, 42],
'木': [3, 8, 13, 18, 23, 28, 33, 38, 43],
'金': [4, 9, 14, 19, 24, 29, 34, 39, 44],
'土': [5, 10, 15, 20, 25, 30, 35, 40, 45],
};
const ELEMENT_KR: Record<string, string> = {
'水': '수', '火': '화', '木': '목', '金': '금', '土': '토',
};
const ELEMENT_COLOR: Record<string, { bg: string; text: string; border: string; ball: string }> = {
'水': { bg: 'bg-blue-50', text: 'text-blue-700', border: 'border-blue-300', ball: '#3b82f6' },
'火': { bg: 'bg-red-50', text: 'text-red-700', border: 'border-red-300', ball: '#ef4444' },
'木': { bg: 'bg-green-50', text: 'text-green-700', border: 'border-green-300', ball: '#22c55e' },
'金': { bg: 'bg-amber-50', text: 'text-amber-700', border: 'border-amber-300', ball: '#f59e0b' },
'土': { bg: 'bg-yellow-50', text: 'text-yellow-700', border: 'border-yellow-300', ball: '#eab308' },
};
// 오행별 행운 설명
const ELEMENT_LUCK_DESC: Record<string, string> = {
'水': '흐르는 물처럼 지혜와 직관이 넘치는 수(水) 기운이 당신의 행운을 이끕니다. 1·6 계열의 숫자들이 당신과 공명합니다.',
'火': '활활 타오르는 불처럼 열정과 표현력이 폭발하는 화(火) 기운이 행운의 열쇠입니다. 2·7 계열의 숫자들에서 기운을 찾으세요.',
'木': '하늘을 향해 뻗는 나무처럼 성장과 창의성을 상징하는 목(木) 기운이 길을 열어줍니다. 3·8 계열의 숫자들이 공명합니다.',
'金': '단단하고 순수한 금속처럼 결단력과 정의를 상징하는 금(金) 기운이 행운을 부릅니다. 4·9 계열의 숫자들이 당신과 함께합니다.',
'土': '만물을 품는 대지처럼 안정과 신뢰를 상징하는 토(土) 기운이 당신을 지켜줍니다. 5·10 계열의 숫자들에 행운이 깃들어 있습니다.',
};
// 사주 기반 시드로 결정론적 숫자 선택 (매번 같은 결과)
function seededRandom(seed: number): () => number {
let s = seed;
return () => {
s = (s * 1664525 + 1013904223) & 0xffffffff;
return (s >>> 0) / 0xffffffff;
};
}
function generateSajuLottoNumbers(
yongShin: string,
heeShin: string,
dayBranch: string,
yearNum: number,
monthNum: number,
dayNum: number
): { numbers: number[]; yongShinNums: number[]; heeShinNums: number[] } {
const seed = yearNum * 10000 + monthNum * 100 + dayNum;
const rand = seededRandom(seed);
const yongPool = ELEMENT_NUMBERS[yongShin] ?? ELEMENT_NUMBERS['水'];
const heePool = ELEMENT_NUMBERS[heeShin] ?? ELEMENT_NUMBERS['木'];
// 용신 기반 3개 선택
const shuffledYong = [...yongPool].sort(() => rand() - 0.5);
const yongPick = shuffledYong.slice(0, 3);
// 희신 기반 2개 선택
const shuffledHee = [...heePool].sort(() => rand() - 0.5);
const heePick = shuffledHee.filter(n => !yongPick.includes(n)).slice(0, 2);
// 지지 오행에서 보조 번호 1개
const BRANCH_ELEMENT: Record<string, string> = {
'子': '水', '亥': '水', '寅': '木', '卯': '木', '巳': '火', '午': '火',
'申': '金', '酉': '金', '丑': '土', '辰': '土', '未': '土', '戌': '土',
};
const branchElem = BRANCH_ELEMENT[dayBranch] ?? yongShin;
const branchPool = ELEMENT_NUMBERS[branchElem] ?? [];
const bonusPool = branchPool.filter(n => !yongPick.includes(n) && !heePick.includes(n));
const shuffledBonus = [...bonusPool].sort(() => rand() - 0.5);
const bonusPick = shuffledBonus.length > 0 ? [shuffledBonus[0]] : [];
const combined = [...new Set([...yongPick, ...heePick, ...bonusPick])];
// 6개 채우기 (부족하면 랜덤으로 추가)
while (combined.length < 6) {
const n = Math.floor(rand() * 45) + 1;
if (!combined.includes(n)) combined.push(n);
}
const numbers = combined.slice(0, 6).sort((a, b) => a - b);
return { numbers, yongShinNums: yongPick.sort((a, b) => a - b), heeShinNums: heePick.sort((a, b) => a - b) };
}
// 로또 볼 컴포넌트
function LottoBall({ num, color = '#1d4ed8', size = 44 }: { num: number; color?: string; size?: number }) {
return (
<div style={{
width: size, height: size,
borderRadius: '50%',
background: `radial-gradient(circle at 35% 35%, ${color}dd, ${color})`,
display: 'flex', alignItems: 'center', justifyContent: 'center',
color: 'white', fontWeight: 800,
fontSize: size < 40 ? 11 : 14,
boxShadow: `0 3px 10px ${color}60`,
flexShrink: 0,
}}>
{num}
</div>
);
}
// 오행별 볼 색상
function getElementColor(num: number): string {
const mod = num % 10;
if (mod === 1 || mod === 6) return '#3b82f6'; // 水
if (mod === 2 || mod === 7) return '#ef4444'; // 火
if (mod === 3 || mod === 8) return '#22c55e'; // 木
if (mod === 4 || mod === 9) return '#f59e0b'; // 金
return '#eab308'; // 土 (0, 5)
}
interface Props {
yongShin: string; // 용신 오행 (예: '水')
yongShinKr: string; // 용신 한글 (예: '수')
heeShin: string; // 희신 오행
heeShinKr: string; // 희신 한글
dayBranch: string; // 일지 (예: '子')
dayStemKr: string; // 일간 한글 (예: '갑')
currentDaeun: {
stemKr: string;
branchKr: string;
startYear: number;
endYear: number;
age: number;
} | null;
yearNum: number;
monthNum: number;
dayNum: number;
hasLottoSubscription: boolean; // 로또 구독 여부
}
export default function SajuLottoSection({
yongShin, yongShinKr, heeShin, heeShinKr,
dayBranch, dayStemKr,
currentDaeun,
yearNum, monthNum, dayNum,
hasLottoSubscription,
}: Props) {
const { numbers, yongShinNums, heeShinNums } = useMemo(
() => generateSajuLottoNumbers(yongShin, heeShin, dayBranch, yearNum, monthNum, dayNum),
[yongShin, heeShin, dayBranch, yearNum, monthNum, dayNum]
);
const elemColor = ELEMENT_COLOR[yongShin] ?? ELEMENT_COLOR['水'];
const currentYear = new Date().getFullYear();
return (
<div id="saju-lotto-section" className="bg-white rounded-2xl border border-[#dbe8ff] overflow-hidden">
{/* 헤더 */}
<div className="bg-gradient-to-r from-[#04102b] via-[#0d1f5c] to-[#04102b] px-6 py-5">
<div className="flex items-center gap-3">
<div className="w-9 h-9 rounded-xl bg-gradient-to-br from-amber-400 to-orange-500 flex items-center justify-center flex-shrink-0 shadow">
<span style={{ fontSize: 18 }}>🎱</span>
</div>
<div className="flex-1">
<h2 className="text-sm font-extrabold text-white"> </h2>
<p className="text-blue-300/60 text-[11px] mt-0.5">
({yongShinKr}·{yongShin})
</p>
</div>
<span className="text-[11px] bg-amber-400/20 border border-amber-400/30 text-amber-300 font-bold px-2.5 py-1 rounded-full">
</span>
</div>
</div>
<div className="p-6 space-y-6">
{/* 용신 설명 배너 */}
<div className={`rounded-xl border p-4 ${elemColor.bg} ${elemColor.border}`}>
<div className="flex items-start gap-3">
<div className={`w-10 h-10 rounded-xl flex items-center justify-center font-bold text-lg flex-shrink-0 ${elemColor.text}`}
style={{ background: 'rgba(255,255,255,0.6)' }}>
{yongShin}
</div>
<div>
<div className={`text-xs font-bold mb-1 ${elemColor.text}`}>
: {yongShinKr}({yongShin}) · : {heeShinKr}({heeShin})
</div>
<p className={`text-xs leading-relaxed ${elemColor.text}`} style={{ opacity: 0.85 }}>
{ELEMENT_LUCK_DESC[yongShin]}
</p>
</div>
</div>
</div>
{/* 추천 번호 */}
<div>
<div className="flex items-center justify-between mb-3">
<h3 className="text-sm font-extrabold text-[#04102b]"> </h3>
<span className="text-[11px] text-slate-400">{currentYear} </span>
</div>
{/* 메인 볼 */}
<div className="flex items-center gap-2.5 flex-wrap mb-3">
{numbers.map((n) => (
<LottoBall key={n} num={n} color={getElementColor(n)} size={48} />
))}
</div>
{/* 용신/희신 구분 안내 */}
<div className="grid grid-cols-2 gap-3 mt-3">
<div className={`rounded-lg p-3 border ${elemColor.bg} ${elemColor.border}`}>
<div className={`text-[10px] font-bold mb-1.5 ${elemColor.text}`}>
({yongShinKr})
</div>
<div className="flex gap-1.5 flex-wrap">
{yongShinNums.map(n => (
<LottoBall key={n} num={n} color={elemColor.ball} size={34} />
))}
</div>
</div>
<div className="rounded-lg p-3 border bg-violet-50 border-violet-200">
<div className="text-[10px] font-bold mb-1.5 text-violet-700">
({heeShinKr})
</div>
<div className="flex gap-1.5 flex-wrap">
{heeShinNums.map(n => (
<LottoBall key={n} num={n} color="#7c3aed" size={34} />
))}
</div>
</div>
</div>
</div>
{/* 기본 사주 해석 내러티브 */}
<div className="bg-[#f8faff] rounded-xl border border-[#dbe8ff] p-4">
<div className="flex items-center gap-2 mb-3">
<span className="text-base"></span>
<h4 className="text-xs font-extrabold text-[#04102b]"> ?</h4>
</div>
<div className="space-y-2 text-xs text-slate-600 leading-relaxed">
<p>
<strong className={elemColor.text}>{dayStemKr}()</strong>
<strong className={elemColor.text}>{yongShin}({yongShinKr})</strong> .
() ,{' '}
<strong>{yongShin === '水' ? '1과 6' : yongShin === '火' ? '2와 7' : yongShin === '木' ? '3과 8' : yongShin === '金' ? '4와 9' : '5와 10'}</strong>
{yongShinKr}() , .
</p>
<p>
<strong className="text-violet-700">{heeShin}({heeShinKr})</strong>
, <strong>({dayBranch})</strong>
6 .
</p>
</div>
</div>
{/* 로또 구독 미가입 → 대운 연동 프리미엄 홍보 */}
{!hasLottoSubscription ? (
<div className="bg-gradient-to-br from-[#04102b] via-[#0a1f5c] to-[#04102b] rounded-xl p-5 relative overflow-hidden border border-[#1a3a7a]">
<div className="absolute inset-0 opacity-[0.04]"
style={{ backgroundImage: 'radial-gradient(circle, #a78bfa 1px, transparent 1px)', backgroundSize: '20px 20px' }} />
<div className="relative">
<div className="flex items-center gap-2 mb-3">
<span className="text-base">🔮</span>
<span className="text-xs font-extrabold text-amber-300"> </span>
</div>
{currentDaeun && (
<p className="text-xs text-blue-200/80 leading-relaxed mb-4">
<strong className="text-amber-300">{currentDaeun.stemKr}{currentDaeun.branchKr} </strong>
({currentDaeun.startYear}~{currentDaeun.endYear}) .
<strong className="text-white"> </strong>
.
</p>
)}
<div className="grid grid-cols-2 gap-2 mb-4">
{[
{ icon: '📊', text: '대운 × 사주 교차 분석' },
{ icon: '🔄', text: '매주 업데이트 번호' },
{ icon: '🎯', text: '빅데이터 Monte Carlo 시뮬레이션' },
{ icon: '📈', text: '핫넘버 / 콜드넘버 통계' },
].map((item, i) => (
<div key={i} className="flex items-center gap-1.5 bg-white/5 rounded-lg px-2.5 py-2">
<span className="text-sm">{item.icon}</span>
<span className="text-[11px] text-blue-200/70 font-medium">{item.text}</span>
</div>
))}
</div>
<Link
href="/services/lotto"
className="block w-full text-center bg-gradient-to-r from-amber-500 to-amber-400 hover:from-amber-400 hover:to-amber-300 text-[#04102b] text-sm font-bold px-4 py-2.5 rounded-xl transition-all shadow-lg"
>
</Link>
</div>
</div>
) : (
/* 로또 구독 가입자 → 대운 교차 분석 심화 */
<div className="bg-gradient-to-br from-emerald-50 to-teal-50 rounded-xl border border-emerald-200 p-5">
<div className="flex items-center gap-2 mb-3">
<div className="w-6 h-6 rounded-full bg-emerald-500 flex items-center justify-center flex-shrink-0">
<svg className="w-3.5 h-3.5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M5 13l4 4L19 7" />
</svg>
</div>
<span className="text-xs font-extrabold text-emerald-800"> · </span>
</div>
{currentDaeun ? (
<div className="space-y-3">
<div className="bg-white/70 rounded-lg p-3 border border-emerald-200">
<div className="text-[11px] text-emerald-600 font-semibold mb-1.5"> </div>
<p className="text-xs text-slate-700 leading-relaxed">
<strong className="text-emerald-700">{currentDaeun.stemKr}{currentDaeun.branchKr} </strong>
({currentDaeun.startYear}~{currentDaeun.endYear}, {currentDaeun.age}~{currentDaeun.age + 9})
<strong className="text-[#04102b]">{yongShin}({yongShinKr})</strong>
{currentDaeun.stemKr.includes(yongShinKr) || currentDaeun.branchKr.includes(yongShinKr)
? <strong className="text-emerald-700"> . !</strong>
: ' 상호작용하고 있습니다. 용신 번호를 중심으로 추천합니다.'
}
</p>
</div>
<div className="bg-white/70 rounded-lg p-3 border border-emerald-200">
<div className="text-[11px] text-emerald-600 font-semibold mb-1.5"> </div>
<p className="text-xs text-slate-700 leading-relaxed">
{currentYear} {currentDaeun.stemKr}{currentDaeun.branchKr}
, .
<strong className="text-[#04102b]"> 3</strong> ,
.
</p>
</div>
<Link
href="/services/lotto/recommend"
className="block w-full text-center bg-gradient-to-r from-emerald-500 to-teal-500 hover:from-emerald-400 hover:to-teal-400 text-white text-sm font-bold px-4 py-2.5 rounded-xl transition-all shadow"
>
</Link>
</div>
) : (
<p className="text-xs text-slate-600">
. .
</p>
)}
</div>
)}
{/* 하단 면책 */}
<p className="text-center text-[11px] text-slate-400 leading-relaxed">
/ .<br />
, .
</p>
</div>
</div>
);
}

View File

@@ -6,7 +6,6 @@ import { EARTHLY_BRANCHES_KR, FIVE_ELEMENTS_KR, FIVE_ELEMENTS } from '@/lib/saju
import { calculateElementScore, performFullAnalysis } from '@/lib/ai-interpretation'; import { calculateElementScore, performFullAnalysis } from '@/lib/ai-interpretation';
import { createClient } from '@/lib/supabase/server'; import { createClient } from '@/lib/supabase/server';
import SajuAISection from './SajuAISection'; import SajuAISection from './SajuAISection';
import SajuLottoSection from './SajuLottoSection';
import SajuFortuneSection from './SajuFortuneSection'; import SajuFortuneSection from './SajuFortuneSection';
interface PageProps { interface PageProps {
@@ -569,29 +568,6 @@ export default async function SajuResultPage({ searchParams }: PageProps) {
/> />
)} )}
{/* 사주 연동 로또 번호 추천 (사주 결제 시 표시) */}
{hasPaid && (
<SajuLottoSection
yongShin={analysis.yongShin.yongShin}
yongShinKr={analysis.yongShin.yongShinKr}
heeShin={analysis.yongShin.heeShin}
heeShinKr={analysis.yongShin.heeShinKr}
dayBranch={sajuData.day.branch}
dayStemKr={sajuData.day.stemKr}
currentDaeun={currentDaeun ? {
stemKr: currentDaeun.stemKr,
branchKr: currentDaeun.branchKr,
startYear: currentDaeun.startYear,
endYear: currentDaeun.endYear,
age: currentDaeun.age,
} : null}
yearNum={yearNum}
monthNum={monthNum}
dayNum={dayNum}
hasLottoSubscription={hasLottoSubscription}
/>
)}
{/* 대운 */} {/* 대운 */}
<div className="bg-white rounded-2xl border border-[#dbe8ff] p-6"> <div className="bg-white rounded-2xl border border-[#dbe8ff] p-6">
<h2 className="text-lg font-extrabold text-[#04102b] mb-5 text-center"> <h2 className="text-lg font-extrabold text-[#04102b] mb-5 text-center">

View File

@@ -1,571 +0,0 @@
'use client';
import { useState, useEffect, useRef } from 'react';
import Link from 'next/link';
import ContactModal from '../../components/ContactModal';
import PaymentButton from '../../components/PaymentButton';
const KAKAO_CHANNEL_URL = process.env.NEXT_PUBLIC_KAKAO_CHANNEL_URL ?? null;
const AI_KIT_CHECKLIST = [
'주로 반복하는 업무 종류 (일지, 이메일, 보고서 등)',
'현재 주로 사용하는 AI 도구 (ChatGPT / Claude / Gemini 등)',
'하루 또는 주 단위로 같은 작업을 몇 번이나 반복하는지',
'사용 목적 (개인 효율화 / 팀 도입 / 소상공인 업무 등)',
];
/* ──────────────────────────────────────────────────────────────
Before / After 데이터 — 각 도구별 실제 시간 비교
마케팅 카피의 핵심: 추상적 "빠름"이 아닌 구체적 숫자
────────────────────────────────────────────────────────────── */
const TOOLS = [
{
icon: (
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
),
title: '업무 일지 자동 작성기',
desc: '하루 업무 키워드 5개만 입력하면 AI가 전문적인 일지를 즉시 완성.',
tag: '직장인 필수',
tagColor: 'bg-blue-500/15 text-blue-400 border-blue-500/25',
before: '15분',
after: '40초',
beforeLabel: '직접 쓸 때',
afterLabel: 'AI 사용 시',
failCase: '대충 쓰면 상사 피드백 → 재작성 → 결국 30분',
saving: '월 4.7시간 절약',
},
{
icon: (
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
</svg>
),
title: '이메일 자동 답장 생성기',
desc: '받은 이메일을 붙여넣으면 상황에 맞는 정중한 답장 3가지 버전을 즉시 생성.',
tag: '소상공인 필수',
tagColor: 'bg-violet-500/15 text-violet-400 border-violet-500/25',
before: '23분',
after: '2분',
beforeLabel: '직접 쓸 때',
afterLabel: 'AI 사용 시',
failCase: '어조가 애매하면 상대방 기분 상함 → 계약 취소 위험',
saving: '월 6.4시간 절약',
},
{
icon: (
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M7 12l3-3 3 3 4-4M8 21l4-4 4 4M3 4h18M4 4h16v12a1 1 0 01-1 1H5a1 1 0 01-1-1V4z" />
</svg>
),
title: '월간 매출 분석 리포트',
desc: '숫자만 입력하면 전월 대비 분석·인사이트·개선 방향을 리포트로 정리.',
tag: '소상공인 필수',
tagColor: 'bg-emerald-500/15 text-emerald-400 border-emerald-500/25',
before: '2시간 30분',
after: '5분',
beforeLabel: '엑셀 + 직접 분석',
afterLabel: 'AI 사용 시',
failCase: '분석 없이 감으로 운영 → 손실 트렌드 한 달 늦게 발견',
saving: '월 2.4시간 절약',
},
{
icon: (
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
</svg>
),
title: 'SNS 콘텐츠 캘린더',
desc: '업종과 키워드를 입력하면 한 달치 인스타·블로그 콘텐츠 기획안을 자동 생성.',
tag: 'SNS 마케팅',
tagColor: 'bg-pink-500/15 text-pink-400 border-pink-500/25',
before: '1시간 30분/주',
after: '10분/월',
beforeLabel: '매주 기획할 때',
afterLabel: 'AI로 한 번에',
failCase: 'SNS 3일 공백 → 인스타 도달 -40% · 팔로워 이탈 시작',
saving: '월 5.7시간 절약',
},
{
icon: (
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4" />
</svg>
),
title: '회의록·미팅 노트 정리',
desc: '대화 내용이나 메모를 입력하면 결정 사항·액션아이템·다음 단계를 즉시 구조화.',
tag: '직장인 필수',
tagColor: 'bg-blue-500/15 text-blue-400 border-blue-500/25',
before: '40분',
after: '3분',
beforeLabel: '직접 정리할 때',
afterLabel: 'AI 사용 시',
failCase: '액션아이템 누락 → 후속 지연 → "그때 얘기했잖아요" 분쟁',
saving: '월 3.7시간 절약',
},
{
icon: (
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
),
title: '상품 설명·리뷰 답변 자동화',
desc: '상품명과 특징을 입력하면 스마트스토어·쿠팡 최적화 상품 설명 + 리뷰 답변 즉시 생성.',
tag: '온라인 판매자',
tagColor: 'bg-orange-500/15 text-orange-400 border-orange-500/25',
before: '25분/개',
after: '30초/개',
beforeLabel: '상품 1개 설명 직접 쓸 때',
afterLabel: 'AI 사용 시',
failCase: '밋밋한 설명 → 클릭율 낮음 → 검색 노출 하락 → 매출 감소',
saving: '상품 10개 기준 월 4시간 절약',
},
];
const TESTIMONIALS = [
{
name: '김하윤',
job: '카페 운영 3년차',
text: '매일 SNS 올릴 내용 고민하다 지쳤는데, 이제 30초면 한 달치 아이디어가 나와요. 매출도 15% 올랐어요.',
rating: 5,
},
{
name: '박도현',
job: '중소기업 팀장',
text: '주간 보고서 작성이 2시간에서 20분으로 줄었습니다. 팀원들한테도 공유했어요.',
rating: 5,
},
{
name: '이서진',
job: '프리랜서 디자이너',
text: '클라이언트 이메일 답장을 AI로 생성하니까 전문적으로 보인다는 피드백을 많이 받아요.',
rating: 5,
},
];
const FAQ = [
{
q: 'AI를 전혀 써본 적 없어도 가능한가요?',
a: 'ChatGPT나 Claude에 복사·붙여넣기만 할 수 있으면 됩니다. 모든 도구에 단계별 사용 가이드가 포함되어 있습니다.',
},
{
q: '매달 어떤 것이 업데이트되나요?',
a: '매월 1일에 새로운 자동화 도구 1~2종이 추가됩니다. 트렌드 변화와 구독자 요청을 반영하여 지속적으로 개선합니다.',
},
{
q: '해지는 언제든지 가능한가요?',
a: '네, 언제든지 마이페이지에서 구독을 취소할 수 있습니다. 해지 후에도 해당 월 말일까지 사용 가능합니다.',
},
{
q: '스마트스토어·쿠팡 판매자도 쓸 수 있나요?',
a: '네. 상품 설명 자동화, 리뷰 답변 자동화 등 온라인 판매자를 위한 전용 도구가 포함되어 있습니다.',
},
];
function useScrollReveal() {
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
const el = ref.current;
if (!el) return;
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.classList.add('is-visible');
observer.unobserve(entry.target);
}
});
},
{ threshold: 0.1, rootMargin: '0px 0px -40px 0px' }
);
el.querySelectorAll('.reveal').forEach((child) => observer.observe(child));
return () => observer.disconnect();
}, []);
return ref;
}
export default function AiKitPage() {
const totalMonthlySaving = 27;
const [modalOpen, setModalOpen] = useState(false);
const containerRef = useScrollReveal();
return (
<div ref={containerRef} className="min-h-full bg-[#f0f4ff]">
<style>{`
.reveal {
opacity: 0;
transform: translateY(1.5rem);
transition: opacity 0.7s cubic-bezier(0.16, 1, 0.3, 1),
transform 0.7s cubic-bezier(0.16, 1, 0.3, 1);
}
.reveal.is-visible {
opacity: 1;
transform: translateY(0);
}
.reveal-d1 { transition-delay: 80ms; }
.reveal-d2 { transition-delay: 160ms; }
.reveal-d3 { transition-delay: 240ms; }
.reveal-d4 { transition-delay: 320ms; }
`}</style>
<ContactModal
isOpen={modalOpen}
onClose={() => setModalOpen(false)}
service="AI 자동화 키트 — 월 19,900원"
checklist={AI_KIT_CHECKLIST}
accentColor="text-indigo-400"
headerFrom="#0a0f2e"
headerTo="#0f1a5c"
/>
{/* ─── Hero ─── */}
<div className="relative overflow-hidden bg-[#0a0f2e] px-6 py-14 lg:px-12" style={{ backgroundImage: 'repeating-linear-gradient(135deg, rgba(255,255,255,0.015) 0px, rgba(255,255,255,0.015) 1px, transparent 1px, transparent 40px)' }}>
<div className="relative max-w-3xl mx-auto">
<Link href="/" className="inline-flex items-center gap-1.5 text-indigo-300/60 hover:text-indigo-300 text-sm mb-8 transition">
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" /></svg>
</Link>
<p className="text-indigo-400 text-xs font-bold uppercase tracking-widest mb-4 font-mono">AI · </p>
{/* 핵심 카피 */}
<h1 className="text-4xl md:text-5xl font-extrabold text-white mb-4 tracking-tight leading-tight">
<br />
<span className="text-red-400">{totalMonthlySaving} </span>
</h1>
<p className="text-indigo-100/60 text-base md:text-lg leading-relaxed max-w-2xl mb-8">
, , , SNS <br />
<strong className="text-white"> {totalMonthlySaving} ,</strong> AI로 90% .
</p>
{/* 가격 카드 */}
<div className="inline-flex flex-col bg-white/5 border border-white/10 rounded-xl px-8 py-5 mb-6">
<div className="flex items-center gap-2 mb-1">
<span className="text-xs font-bold bg-red-500 text-white px-2 py-0.5 rounded"> </span>
<span className="text-sm line-through text-white/30"> 39,900</span>
</div>
<div className="text-4xl font-extrabold text-white">
19,900<span className="text-xl font-semibold text-white/50"></span>
<span className="text-base font-normal text-white/40 ml-1">/ </span>
</div>
<p className="text-indigo-300/60 text-xs mt-1"> · </p>
</div>
<div className="flex flex-col gap-3 items-center">
<PaymentButton
productId="ai_kit_monthly"
className="inline-flex items-center justify-center gap-2 bg-indigo-500 hover:bg-indigo-400 text-white text-base font-bold px-8 py-4 rounded-xl transition-colors w-full max-w-xs"
>
19,900/
</PaymentButton>
<button
onClick={() => setModalOpen(true)}
className="text-indigo-300/60 hover:text-indigo-300 text-sm underline underline-offset-2 transition-colors"
>
</button>
</div>
</div>
</div>
{/* ─── 시간 낭비 가시화 섹션 ─── */}
<div className="bg-white px-6 py-12 lg:px-12 border-b border-slate-100">
<div className="max-w-5xl mx-auto">
<div className="reveal text-center mb-8">
<h2 className="text-2xl md:text-3xl font-extrabold text-slate-800 mb-2">
</h2>
<p className="text-slate-500 text-sm">· / 22 </p>
</div>
{/* 총합 카드 */}
<div className="reveal bg-red-50 border border-red-200 rounded-2xl p-6 mb-8">
<div className="flex flex-col md:flex-row items-center justify-between gap-4">
<div>
<p className="text-sm font-bold text-red-600 mb-1">6 </p>
<p className="text-4xl font-extrabold text-slate-800">
<span className="text-red-500">{totalMonthlySaving} 19</span>
</p>
<p className="text-slate-500 text-sm mt-1">
15,000 <span className="font-bold text-slate-700"> 409,000 </span>
</p>
</div>
<div className="text-center md:text-right">
<div className="text-2xl font-extrabold text-indigo-600">AI </div>
<div className="text-4xl font-extrabold text-emerald-500">2 6</div>
<div className="text-slate-500 text-sm mt-1"> <span className="font-bold text-emerald-600">92.3% </span></div>
</div>
</div>
</div>
{/* 개별 도구 Before/After 바 차트 */}
<div className="reveal space-y-3">
{TOOLS.map((tool, i) => {
const beforeVal = tool.before;
const afterVal = tool.after;
return (
<div key={i} className="bg-slate-50 rounded-xl p-4 border border-slate-100">
<div className="flex items-center justify-between mb-2">
<span className="text-sm font-bold text-slate-800">{tool.title}</span>
<span className="text-xs font-bold text-emerald-600 bg-emerald-50 border border-emerald-200 px-2 py-0.5 rounded-md">
{tool.saving}
</span>
</div>
<div className="flex items-center gap-3 text-xs">
<div className="flex items-center gap-2 flex-1">
<span className="text-slate-400 w-20 text-right flex-shrink-0">{tool.beforeLabel}</span>
<div className="flex-1 bg-red-100 rounded-full h-2">
<div className="bg-red-400 h-2 rounded-full" style={{ width: '100%' }} />
</div>
<span className="font-extrabold text-red-500 w-20 flex-shrink-0">{beforeVal}</span>
</div>
</div>
<div className="flex items-center gap-3 text-xs mt-1.5">
<div className="flex items-center gap-2 flex-1">
<span className="text-slate-400 w-20 text-right flex-shrink-0">{tool.afterLabel}</span>
<div className="flex-1 bg-emerald-100 rounded-full h-2">
<div className="bg-emerald-400 h-2 rounded-full" style={{ width: '8%' }} />
</div>
<span className="font-extrabold text-emerald-600 w-20 flex-shrink-0">{afterVal}</span>
</div>
</div>
</div>
);
})}
</div>
</div>
</div>
{/* ─── "안 쓰면 생기는 실패 비용" 섹션 ─── */}
<div className="px-6 py-12 lg:px-12 bg-[#0a0f2e]">
<div className="max-w-5xl mx-auto">
<div className="reveal text-center mb-8">
<div className="inline-flex items-center gap-2 bg-red-500/15 border border-red-500/30 text-red-400 text-xs font-extrabold px-4 py-1.5 rounded-full uppercase tracking-widest mb-3">
<svg className="w-3.5 h-3.5" fill="currentColor" viewBox="0 0 24 24"><path d="M12 9v4m0 4h.01M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z" /></svg>
AI를
</div>
<h2 className="text-2xl md:text-3xl font-extrabold text-white mb-2">
</h2>
<p className="text-slate-400 text-sm"> </p>
</div>
<div className="reveal grid sm:grid-cols-2 lg:grid-cols-3 gap-4">
{TOOLS.map((tool, i) => (
<div key={i} className="bg-slate-900/60 border border-red-900/40 rounded-2xl p-5">
<div className="flex items-start gap-3 mb-3">
<div className="w-8 h-8 rounded-lg bg-red-500/15 border border-red-500/25 flex items-center justify-center text-red-400 flex-shrink-0">
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</div>
<p className="text-xs font-bold text-slate-300">{tool.title} </p>
</div>
<p className="text-sm text-red-300/80 leading-relaxed border-l-2 border-red-500/30 pl-3">
{tool.failCase}
</p>
</div>
))}
</div>
<div className="reveal mt-8 bg-[#0a0f2e] border border-indigo-500/30 rounded-2xl p-6 text-center">
<p className="text-white text-lg font-extrabold mb-1">
1 1 .
</p>
<p className="text-indigo-300/70 text-sm">
19,900 .
</p>
</div>
</div>
</div>
{/* ─── 포함 도구 ─── */}
<div className="px-6 py-12 lg:px-12">
<div className="max-w-5xl mx-auto">
<div className="reveal text-center mb-8">
<div className="inline-flex items-center gap-2 bg-indigo-500/10 border border-indigo-500/25 text-indigo-500 text-xs font-extrabold px-4 py-1.5 rounded-full uppercase tracking-widest mb-3">
6 AI
</div>
<h2 className="text-2xl md:text-3xl font-extrabold text-slate-800"> </h2>
<p className="text-slate-500 text-sm mt-2">ChatGPT · Claude에 · </p>
</div>
<div className="grid sm:grid-cols-2 lg:grid-cols-3 gap-4">
{TOOLS.map((tool, i) => (
<div key={i} className={`reveal reveal-d${(i % 3) + 1} bg-white rounded-2xl p-5 border border-slate-200 hover:border-indigo-300 hover:shadow-lg transition-all group`}>
<div className="flex items-start justify-between mb-4">
<div className="w-11 h-11 rounded-xl bg-indigo-50 border border-indigo-100 flex items-center justify-center text-indigo-500 group-hover:bg-indigo-100 transition-colors">
{tool.icon}
</div>
<span className={`text-xs font-bold px-2 py-0.5 rounded-md border ${tool.tagColor}`}>
{tool.tag}
</span>
</div>
<h3 className="text-sm font-extrabold text-slate-800 mb-1.5 leading-snug">{tool.title}</h3>
<p className="text-xs text-slate-500 leading-relaxed mb-3">{tool.desc}</p>
{/* 인라인 Before/After */}
<div className="flex items-center gap-2 bg-slate-50 rounded-lg px-3 py-2 border border-slate-100">
<span className="text-xs text-red-500 font-bold">{tool.before}</span>
<svg className="w-3 h-3 text-slate-400 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 8l4 4m0 0l-4 4m4-4H3" />
</svg>
<span className="text-xs text-emerald-600 font-bold">{tool.after}</span>
<span className="text-xs text-slate-400 ml-auto">{tool.saving.replace('월 ', '').replace(' 절약', '')}</span>
</div>
</div>
))}
</div>
{/* 업데이트 알림 */}
<div className="reveal mt-6 bg-indigo-50 border border-indigo-200 rounded-2xl p-5 flex items-start gap-4">
<div className="w-10 h-10 rounded-xl bg-indigo-100 flex items-center justify-center text-indigo-600 flex-shrink-0">
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<div>
<p className="text-sm font-bold text-indigo-700"> 1 </p>
<p className="text-xs text-indigo-600/70 mt-0.5"> 1~2 . .</p>
</div>
</div>
</div>
</div>
{/* ─── 누구에게 필요한가 ─── */}
<div className="px-6 py-10 lg:px-12 bg-white">
<div className="max-w-5xl mx-auto">
<div className="reveal text-center mb-8">
<h2 className="text-2xl font-extrabold text-slate-800"> </h2>
</div>
<div className="grid sm:grid-cols-2 lg:grid-cols-4 gap-4">
{[
{
icon: (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M3 3h2l.4 2M7 13h10l4-8H5.4M7 13L5.4 5M7 13l-2.293 2.293c-.63.63-.184 1.707.707 1.707H17m0 0a2 2 0 100 4 2 2 0 000-4zm-8 2a2 2 0 11-4 0 2 2 0 014 0z" />
</svg>
),
title: '소상공인',
pain: '"매일 SNS, 이메일, 리뷰 답변에 2~3시간씩 쓰고 있어요."',
gain: '도구 3개만 써도 월 12시간 이상 확보',
},
{
icon: (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M21 13.255A23.931 23.931 0 0112 15c-3.183 0-6.22-.62-9-1.745M16 6V4a2 2 0 00-2-2h-4a2 2 0 00-2 2v2m4 6h.01M5 20h14a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
</svg>
),
title: '직장인',
pain: '"보고서 쓰다가 퇴근 시간 넘기는 게 일상이에요."',
gain: '보고서·일지·회의록 시간 90% 감소',
},
{
icon: (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
</svg>
),
title: '온라인 판매자',
pain: '"상품 50개 설명 쓰는 데 이틀이 걸렸어요."',
gain: '50개 상품 설명 → 25분 완성',
},
{
icon: (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M11 5.882V19.24a1.76 1.76 0 01-3.417.592l-2.147-6.15M18 13a3 3 0 100-6M5.436 13.683A4.001 4.001 0 017 6h1.832c4.1 0 7.625-1.234 9.168-3v14c-1.543-1.766-5.067-3-9.168-3H7a3.988 3.988 0 01-1.564-.317z" />
</svg>
),
title: '1인 마케터',
pain: '"콘텐츠 아이디어 고갈로 업로드를 자꾸 건너뛰어요."',
gain: '한 달치 콘텐츠 기획 → 10분 완성',
},
].map((item, i) => (
<div key={i} className={`reveal reveal-d${i + 1} p-5 rounded-2xl bg-slate-50 border border-slate-100`}>
<div className="w-9 h-9 rounded-lg bg-indigo-100 text-indigo-600 flex items-center justify-center mb-3">{item.icon}</div>
<p className="text-sm font-extrabold text-slate-800 mb-2">{item.title}</p>
<p className="text-xs text-slate-500 italic leading-relaxed mb-3">{item.pain}</p>
<p className="text-xs font-bold text-indigo-600 bg-indigo-50 border border-indigo-100 rounded-lg px-2 py-1.5">
{item.gain}
</p>
</div>
))}
</div>
</div>
</div>
{/* ─── 사용 후기 ─── */}
<div className="px-6 py-10 lg:px-12">
<div className="max-w-5xl mx-auto">
<div className="reveal text-center mb-8">
<h2 className="text-2xl font-extrabold text-slate-800"> </h2>
</div>
<div className="grid sm:grid-cols-3 gap-4">
{TESTIMONIALS.map((t, i) => (
<div key={i} className={`reveal reveal-d${i + 1} bg-white rounded-2xl p-5 border border-slate-200 shadow-sm`}>
<div className="flex gap-0.5 mb-3">
{Array.from({ length: t.rating }).map((_, j) => (
<svg key={j} className="w-4 h-4 text-yellow-400" fill="currentColor" viewBox="0 0 20 20">
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z" />
</svg>
))}
</div>
<p className="text-sm text-slate-700 leading-relaxed mb-4">"{t.text}"</p>
<div>
<p className="text-xs font-bold text-slate-800">{t.name}</p>
<p className="text-xs text-slate-400">{t.job}</p>
</div>
</div>
))}
</div>
</div>
</div>
{/* ─── FAQ ─── */}
<div className="px-6 py-10 lg:px-12 bg-white">
<div className="max-w-3xl mx-auto">
<div className="reveal text-center mb-8">
<h2 className="text-2xl font-extrabold text-slate-800"> </h2>
</div>
<div className="reveal space-y-3">
{FAQ.map((item, i) => (
<div key={i} className="border border-slate-200 rounded-xl p-5">
<p className="text-sm font-bold text-slate-800 mb-2">Q. {item.q}</p>
<p className="text-sm text-slate-500 leading-relaxed">A. {item.a}</p>
</div>
))}
</div>
</div>
</div>
{/* ─── 최하단 CTA ─── */}
<div className="px-6 py-14 lg:px-12 bg-[#0a0f2e]" style={{ backgroundImage: 'repeating-linear-gradient(135deg, rgba(255,255,255,0.012) 0px, rgba(255,255,255,0.012) 1px, transparent 1px, transparent 40px)' }}>
<div className="reveal max-w-2xl mx-auto text-center">
{/* 마지막 카피: 기회비용 프레이밍 */}
<p className="text-indigo-300/60 text-sm font-bold uppercase tracking-widest mb-3"> </p>
<h2 className="text-2xl md:text-3xl font-extrabold text-white mb-3">
19,900 vs<br />
<span className="text-red-400">
409,000
</span>
</h2>
<p className="text-indigo-200/50 text-sm mb-8">
. , .
<br /> .
</p>
<div className="flex flex-col items-center gap-3">
<PaymentButton
productId="ai_kit_monthly"
className="inline-flex items-center justify-center gap-2 bg-indigo-500 hover:bg-indigo-400 text-white text-base font-bold px-8 py-4 rounded-xl transition-colors w-full max-w-sm"
>
19,900/
</PaymentButton>
<p className="text-white/25 text-xs"> · 19,900 · </p>
</div>
</div>
</div>
</div>
);
}

View File

@@ -1,31 +0,0 @@
import type { Metadata } from 'next';
export const metadata: Metadata = {
title: 'AI 업무 자동화 개발 | 엑셀·이메일·RPA 외주',
description:
'매일 반복하는 엑셀 정리, 이메일 발송, 보고서 작성을 AI와 파이썬으로 자동화합니다. ChatGPT 연동 자동화, Make.com 플로우, Python RPA 개발. 5만원~, 계약서 포함, 1개월 무상 AS.',
keywords: [
'업무 자동화 외주',
'AI 업무 자동화',
'엑셀 자동화 외주',
'파이썬 자동화 개발',
'RPA 개발 외주',
'이메일 자동화',
'반복업무 자동화',
'ChatGPT 자동화',
'Make.com 자동화',
'텔레그램 봇 개발',
'업무 자동화 비용',
'자동화 프리랜서',
],
openGraph: {
title: 'AI 업무 자동화 개발 | 쟁승메이드',
description:
'엑셀·이메일·보고서 반복 업무를 AI로 자동화. 현직 대기업 개발자가 직접 개발. 5만원~, 계약서 포함, 납기 패널티 적용.',
url: 'https://jaengseung-made.com/services/automation',
},
};
export default function AutomationLayout({ children }: { children: React.ReactNode }) {
return children;
}

View File

@@ -1,469 +0,0 @@
'use client';
import { useState, useEffect, useRef } from 'react';
import Link from 'next/link';
import ContactModal from '../../components/ContactModal';
import { trackCTAClick } from '../../../lib/gtag';
function useScrollReveal() {
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
const el = ref.current;
if (!el) return;
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.classList.add('is-visible');
observer.unobserve(entry.target);
}
});
},
{ threshold: 0.1, rootMargin: '0px 0px -40px 0px' }
);
el.querySelectorAll('.reveal').forEach((child) => observer.observe(child));
return () => observer.disconnect();
}, []);
return ref;
}
const tools = [
{
id: 'excel',
title: '엑셀 자동화 도구',
subtitle: 'Excel Macro Toolkit v1.2',
desc: '반복 업무를 버튼 하나로 처리하는 엑셀 매크로 모음. 데이터 정리·집계·보고서 자동 생성 기능 포함.',
tags: ['VBA', 'Excel', '매크로', '무료'],
color: '#16a34a',
bgColor: '#f0fdf4',
borderColor: '#bbf7d0',
icon: (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.5} className="w-7 h-7">
<path strokeLinecap="round" strokeLinejoin="round" d="M3.375 19.5h17.25m-17.25 0a1.125 1.125 0 01-1.125-1.125M3.375 19.5h1.5C5.496 19.5 6 18.996 6 18.375m-3.75.125v-1.125c0-.621.504-1.125 1.125-1.125h1.5m0 0v1.25m0-1.25c0-.621.504-1.125 1.125-1.125h1.5m0 0V7.875m0 0c0-.621.504-1.125 1.125-1.125h8.25c.621 0 1.125.504 1.125 1.125v8.25m0 0v1.125m0-1.125c0 .621-.504 1.125-1.125 1.125H6m12-8.25v8.25" />
</svg>
),
href: '/services/automation/tools/excel',
ready: true,
},
{
id: 'scraper',
title: '웹 스크래핑 도구',
subtitle: 'Web Scraper v1.0',
desc: '공공데이터·쇼핑몰 가격·뉴스를 자동 수집해 엑셀로 저장하는 Python 기반 수집 도구.',
tags: ['Python', 'BeautifulSoup', 'Excel 출력', '무료'],
color: '#2563eb',
bgColor: '#eff6ff',
borderColor: '#bfdbfe',
icon: (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.5} className="w-7 h-7">
<path strokeLinecap="round" strokeLinejoin="round" d="M12 21a9.004 9.004 0 008.716-6.747M12 21a9.004 9.004 0 01-8.716-6.747M12 21c2.485 0 4.5-4.03 4.5-9S14.485 3 12 3m0 18c-2.485 0-4.5-4.03-4.5-9S9.515 3 12 3m0 0a8.997 8.997 0 017.843 4.582M12 3a8.997 8.997 0 00-7.843 4.582m15.686 0A11.953 11.953 0 0112 10.5c-2.998 0-5.74-1.1-7.843-2.918m15.686 0A8.959 8.959 0 0121 12c0 .778-.099 1.533-.284 2.253m0 0A17.919 17.919 0 0112 16.5c-3.162 0-6.133-.815-8.716-2.247m0 0A9.015 9.015 0 013 12c0-1.605.42-3.113 1.157-4.418" />
</svg>
),
href: '/services/automation/tools/scraper',
ready: true,
},
{
id: 'ppt',
title: 'PPT 제작 자동화 도구',
subtitle: 'PPT Automation v1.0',
desc: '엑셀 데이터를 읽어 표지·내용·마무리 슬라이드를 자동 생성하는 Python 기반 PPT 도구.',
tags: ['Python', 'python-pptx', 'openpyxl', '무료'],
color: '#7c3aed',
bgColor: '#f5f3ff',
borderColor: '#ddd6fe',
icon: (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.5} className="w-7 h-7">
<path strokeLinecap="round" strokeLinejoin="round" d="M3.75 3v11.25A2.25 2.25 0 006 16.5h2.25M3.75 3h-1.5m1.5 0h16.5m0 0h1.5m-1.5 0v11.25A2.25 2.25 0 0118 16.5h-2.25m-7.5 0h7.5m-7.5 0l-1 3m8.5-3l1 3m0 0l.5 1.5m-.5-1.5h-9.5m0 0l-.5 1.5M9 11.25v1.5M12 9v3.75m3-6v6" />
</svg>
),
href: '/services/automation/tools/ppt',
ready: true,
},
];
const premiumTools = [
{
id: 'realestate',
title: '부동산 매물 크롤링 프로그램',
subtitle: 'Real Estate Crawler v1.0',
desc: '직방·다방·피터팬·네이버부동산 4개 플랫폼을 동시 수집. 지역·거래유형·매물유형 선택 후 플랫폼별 시트로 Excel 자동 저장.',
tags: ['Python', '4개 플랫폼', '중복제거', 'Excel 출력'],
price: '3만원',
features: ['직방·다방·피터팬·네이버부동산 동시 수집', '지역·거래유형·매물유형 선택', '자동 중복 제거', '플랫폼별 시트 분리 Excel 저장'],
color: '#b45309',
bgFrom: '#451a03',
bgTo: '#78350f',
accentColor: '#f59e0b',
icon: (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.5} className="w-7 h-7">
<path strokeLinecap="round" strokeLinejoin="round" d="M2.25 12l8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25" />
</svg>
),
downloadPath: '/downloads/real_estate_crawler_v1.0.py',
},
{
id: 'accounting',
title: '사업장 회계 장부 자동화 프로그램',
subtitle: 'Accounting Automation v1.0',
desc: '쇼핑몰·식당·제조업·서비스업 업종별 수입/지출 입력 → 매출총이익·영업이익·순이익 자동 계산 + 회계사 관점 조언 리포트.',
tags: ['Python', '업종별 맞춤', '손익계산서', '회계 조언'],
price: '5만원',
features: ['업종별 맞춤 항목 (쇼핑몰·식당·제조·서비스업)', '매출총이익·영업이익·순이익 자동 계산', '부가세 신고 준비 자료 자동 생성', '회계 전문가 관점 경고·조언 리포트'],
color: '#0f766e',
bgFrom: '#042f2e',
bgTo: '#134e4a',
accentColor: '#2dd4bf',
icon: (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.5} className="w-7 h-7">
<path strokeLinecap="round" strokeLinejoin="round" d="M12 6.042A8.967 8.967 0 006 3.75c-1.052 0-2.062.18-3 .512v14.25A8.987 8.987 0 016 18c2.305 0 4.408.867 6 2.292m0-14.25a8.966 8.966 0 016-2.292c1.052 0 2.062.18 3 .512v14.25A8.987 8.987 0 0018 18a8.967 8.967 0 00-6 2.292m0-14.25v14.25" />
</svg>
),
downloadPath: '/downloads/accounting_automation_v1.0.py',
},
];
const CHECKLIST = [
'자동화하고 싶은 업무를 구체적으로 설명해주세요',
'현재 사용 중인 프로그램/시스템 (엑셀, ERP, 쇼핑몰 등)',
'자동화 빈도 (매일 / 주 1회 / 월 1회 등)',
'희망 납품 일정과 예산 범위',
'데이터 민감도 여부 (개인정보 포함 여부)',
];
const automationTypes = [
{
title: '엑셀 / 구글 시트 자동화',
desc: '매일 2시간씩 엑셀에 손가락 아파하던 업무, 밤 11시에 자동 실행되도록 바꿔드립니다.',
examples: ['수작업 매출 집계 → 버튼 하나로 완료', '시트 간 데이터 복사 반복 → 전부 자동화', '보고서 양식이 매번 깨짐 → 서식 고정 자동화'],
accentColor: 'border-emerald-200 bg-emerald-50', dotColor: 'bg-emerald-500', labelColor: 'text-emerald-700 bg-emerald-100 border-emerald-200',
},
{
title: '웹 스크래핑 · 데이터 수집',
desc: '경쟁사 가격 확인하러 탭 10개 열어놓고 복사·붙여넣기 하던 그 시간을 되돌려 드립니다.',
examples: ['경쟁사 가격 변동 → 엑셀에 자동 수집', '상품 리뷰 수백 개 → 자동 취합 분석', '공공입찰 공고 → 조건 맞으면 카톡 알림'],
accentColor: 'border-blue-200 bg-blue-50', dotColor: 'bg-blue-500', labelColor: 'text-blue-700 bg-blue-100 border-blue-200',
},
{
title: '이메일 자동 발송',
desc: '"이 이메일 또 보내야 해?"라는 생각이 드는 모든 발송 업무를 없애드립니다.',
examples: ['주문 접수·발송 확인 이메일 자동 발송', '월말 보고서 관련자에게 자동 배포', '미수금 고객에게 단계별 안내 자동 발송'],
accentColor: 'border-violet-200 bg-violet-50', dotColor: 'bg-violet-500', labelColor: 'text-violet-700 bg-violet-100 border-violet-200',
},
{
title: '업무 프로세스 RPA',
desc: 'ERP에 데이터 입력하고, 파일 정리하고, 같은 버튼 수십 번 클릭하는 일을 대신 합니다.',
examples: ['ERP 입력 작업 → 완전 무인 자동화', '월말 파일 정리·백업 → 예약 실행', '발주서 웹 입력 → 로봇이 대신 클릭'],
accentColor: 'border-orange-200 bg-orange-50', dotColor: 'bg-orange-500', labelColor: 'text-orange-700 bg-orange-100 border-orange-200',
},
{
title: '텔레그램 봇 개발',
desc: '서버가 죽었는데 아무도 모르는 상황, 매출이 터졌는데 혼자만 아는 상황을 없애드립니다.',
examples: ['서버 다운·이상 → 즉시 카톡/텔레그램 알림', '하루 매출 현황 → 저녁 6시에 자동 리포트', '신규 주문 들어오면 → 담당자에게 즉시 알림'],
accentColor: 'border-cyan-200 bg-cyan-50', dotColor: 'bg-cyan-500', labelColor: 'text-cyan-700 bg-cyan-100 border-cyan-200',
},
{
title: 'API 연동 · 시스템 통합',
desc: '두 프로그램이 따로 놀아서 중간에 손으로 옮기고 있다면, 그 사이를 연결해드립니다.',
examples: ['CRM → ERP 고객 정보 자동 동기화', '결제 완료 → 재고 차감 자동 연동', '온라인몰 주문 → 배송 시스템 자동 등록'],
accentColor: 'border-indigo-200 bg-indigo-50', dotColor: 'bg-indigo-500', labelColor: 'text-indigo-700 bg-indigo-100 border-indigo-200',
},
];
const plans = [
{ name: '단순 자동화', price: '5만원~', desc: '단일 작업 · 1~3일 소요', examples: '엑셀 매크로, 단순 스크래핑, 이메일 자동화', highlight: false, productId: 'automation_basic' },
{ name: '자동화 심화', price: '15만원~', desc: '복합 작업 · 1~2주 소요', examples: 'RPA 프로세스, API 연동, 텔레그램 봇', highlight: true, productId: 'automation_advanced' },
{ name: '대형 자동화', price: '협의', desc: '시스템 통합 · 2주 이상 소요', examples: '전사 업무 자동화, 멀티 시스템 통합', highlight: false, productId: null },
];
const process = [
{ step: '01', title: '무료 상담', desc: '반복 업무 파악 및 자동화 가능 여부 확인' },
{ step: '02', title: '요구사항 분석', desc: '상세 프로세스 분석 및 자동화 범위 결정' },
{ step: '03', title: '개발 및 테스트', desc: '실제 데이터로 테스트하며 단계적 개발' },
{ step: '04', title: '납품 및 교육', desc: '사용 방법 교육 + 가이드 문서 제공' },
{ step: '05', title: 'AS 지원', desc: '1개월 무상 기술 지원 및 버그 수정' },
];
export default function AutomationPage() {
const [modalOpen, setModalOpen] = useState(false);
const [modalService, setModalService] = useState('업무 자동화');
const containerRef = useScrollReveal();
const openModal = (service: string) => {
trackCTAClick(service, '/services/automation');
setModalService(service);
setModalOpen(true);
};
return (
<div ref={containerRef} className="min-h-full bg-[#f0f5ff]">
<style>{`
.reveal {
opacity: 0;
transform: translateY(1.5rem);
transition: opacity 0.7s cubic-bezier(0.16, 1, 0.3, 1),
transform 0.7s cubic-bezier(0.16, 1, 0.3, 1);
}
.reveal.is-visible {
opacity: 1;
transform: translateY(0);
}
.reveal-d1 { transition-delay: 80ms; }
.reveal-d2 { transition-delay: 160ms; }
.reveal-d3 { transition-delay: 240ms; }
`}</style>
<ContactModal
isOpen={modalOpen}
onClose={() => setModalOpen(false)}
service={modalService}
checklist={CHECKLIST}
accentColor="text-cyan-400"
headerFrom="#012030"
headerTo="#013d50"
/>
{/* ─── Hero ─── */}
<div className="relative overflow-hidden bg-[#012030] px-6 py-14 lg:px-12" style={{ backgroundImage: 'repeating-linear-gradient(135deg, rgba(255,255,255,0.015) 0px, rgba(255,255,255,0.015) 1px, transparent 1px, transparent 40px)' }}>
<div className="relative max-w-3xl mx-auto">
<Link href="/" className="inline-flex items-center gap-1.5 text-cyan-300/60 hover:text-cyan-300 text-sm mb-8 transition">
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" /></svg>
</Link>
<p className="text-cyan-400 text-xs font-bold uppercase tracking-widest mb-4 font-mono">RPA · </p>
<h1 className="text-4xl md:text-5xl font-extrabold text-white mb-4 tracking-tight leading-tight">
<br />
</h1>
<p className="text-cyan-100/50 text-base md:text-lg leading-relaxed max-w-xl mx-auto mb-6">
&ldquo; &rdquo; , .<br />
, , , RPA까지 .
</p>
<div className="inline-grid grid-cols-3 gap-px bg-cyan-400/10 border border-cyan-400/20 rounded-2xl overflow-hidden">
{[{ v: '1~3일', l: '단순 작업' }, { v: '1~2주', l: '복합 작업' }, { v: '1개월', l: '무상 AS' }].map((s) => (
<div key={s.l} className="bg-[#012030]/80 px-5 py-3 text-center">
<div className="text-white font-extrabold text-base">{s.v}</div>
<div className="text-cyan-400/50 text-xs mt-0.5">{s.l}</div>
</div>
))}
</div>
</div>
</div>
{/* ─── 자동화 유형 ─── */}
<div className="px-6 py-12 lg:px-12">
<div className="max-w-5xl mx-auto">
<div className="text-center mb-8 reveal">
<p className="text-cyan-600 text-xs font-bold uppercase tracking-widest mb-2">AUTOMATION TYPES</p>
<h2 className="text-2xl md:text-3xl font-extrabold text-[#04102b]"> </h2>
</div>
<div className="grid sm:grid-cols-2 lg:grid-cols-3 gap-4">
{automationTypes.map((at, idx) => (
<div key={at.title} className={`bg-white rounded-2xl border-2 ${at.accentColor} p-5 reveal reveal-d${(idx % 3) + 1}`}>
<span className={`inline-block text-xs font-bold px-2 py-0.5 rounded-md border mb-3 ${at.labelColor}`}>{at.title.split(' ')[0]}</span>
<h3 className="font-bold text-[#04102b] text-sm mb-2">{at.title}</h3>
<p className="text-slate-500 text-xs leading-relaxed mb-3">{at.desc}</p>
<div className="space-y-1.5">
{at.examples.map((ex) => (
<div key={ex} className="flex items-center gap-2 text-xs text-slate-600">
<div className={`w-1.5 h-1.5 rounded-full flex-shrink-0 ${at.dotColor}`} />
{ex}
</div>
))}
</div>
</div>
))}
</div>
</div>
</div>
{/* ─── 프로세스 ─── */}
<div className="px-6 pb-12 lg:px-12">
<div className="max-w-5xl mx-auto">
<div className="text-center mb-8 reveal">
<p className="text-cyan-600 text-xs font-bold uppercase tracking-widest mb-2">PROCESS</p>
<h2 className="text-2xl md:text-3xl font-extrabold text-[#04102b]"> </h2>
</div>
<div className="relative">
<div className="hidden sm:block absolute top-10 left-[10%] right-[10%] h-0.5 bg-[#dbe8ff]" />
<div className="grid grid-cols-1 sm:grid-cols-5 gap-4">
{process.map((p, idx) => (
<div key={p.step} className={`relative text-center reveal reveal-d${(idx % 3) + 1}`}>
<div className="w-20 h-20 mx-auto rounded-2xl bg-[#012030] border border-cyan-400/20 flex flex-col items-center justify-center mb-3">
<span className="text-cyan-400 text-xs font-bold">STEP</span>
<span className="text-white font-extrabold text-lg leading-none">{p.step}</span>
</div>
<div className="font-bold text-[#04102b] text-sm mb-1">{p.title}</div>
<div className="text-slate-500 text-xs leading-relaxed">{p.desc}</div>
</div>
))}
</div>
</div>
</div>
</div>
{/* ─── 예상 비용 ─── */}
<div className="px-6 pb-12 lg:px-12">
<div className="max-w-5xl mx-auto">
<div className="text-center mb-8 reveal">
<p className="text-[#1a56db] text-xs font-bold uppercase tracking-widest mb-2">PRICING</p>
<h2 className="text-2xl md:text-3xl font-extrabold text-[#04102b]"> </h2>
</div>
<div className="grid sm:grid-cols-3 gap-5">
{plans.map((plan, idx) => (
<div key={plan.name} className={`rounded-2xl border p-6 relative flex flex-col reveal reveal-d${idx + 1} ${
plan.highlight
? 'bg-[#012030] border-cyan-400/30 shadow-2xl shadow-cyan-900/20 scale-105'
: 'bg-white border-[#dbe8ff]'
}`}>
{plan.highlight && (
<div className="absolute -top-3.5 left-1/2 -translate-x-1/2 bg-cyan-400 text-[#012030] text-xs font-extrabold px-4 py-1 rounded-full tracking-wide"> </div>
)}
<div className={`text-xs font-bold mb-2 tracking-wide ${plan.highlight ? 'text-cyan-400' : 'text-slate-400'}`}>{plan.name.toUpperCase()}</div>
<div className={`text-3xl font-extrabold mb-1 ${plan.highlight ? 'text-white' : 'text-[#04102b]'}`}>{plan.price}</div>
<div className={`text-xs mb-4 ${plan.highlight ? 'text-cyan-300/50' : 'text-slate-400'}`}>{plan.desc}</div>
<div className={`text-xs leading-relaxed mb-6 flex-1 p-3 rounded-xl ${plan.highlight ? 'bg-cyan-400/10 text-cyan-100/70' : 'bg-[#f0f5ff] text-slate-600'}`}>
: {plan.examples}
</div>
<button
onClick={() => openModal(`업무 자동화 - ${plan.name}`)}
className={`block w-full text-center py-3 rounded-xl text-sm font-bold transition ${
plan.highlight ? 'bg-cyan-400 text-[#012030] hover:bg-cyan-300' : 'bg-[#04102b] text-white hover:bg-[#0a1f5c]'
}`}
>
</button>
</div>
))}
</div>
<p className="text-center text-slate-400 text-xs mt-4">
* , , . .
</p>
</div>
</div>
{/* ─── 프리미엄 툴 ─── */}
<div className="px-6 pb-4 lg:px-12">
<div className="max-w-5xl mx-auto">
<div className="text-center mb-8 reveal">
<span className="inline-flex items-center gap-1.5 bg-amber-500/10 border border-amber-400/30 text-amber-700 text-xs font-bold px-3 py-1 rounded-full mb-3">
<svg className="w-3.5 h-3.5" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg>
PREMIUM TOOLS
</span>
<h2 className="text-2xl md:text-3xl font-extrabold text-[#04102b] mb-2"> </h2>
<p className="text-slate-500 text-sm"> . + 1 .</p>
</div>
<div className="grid sm:grid-cols-2 gap-6">
{premiumTools.map((tool, idx) => (
<div key={tool.id}
className={`rounded-2xl overflow-hidden border border-white/10 shadow-xl flex flex-col reveal reveal-d${idx + 1}`}
style={{ background: `linear-gradient(145deg, ${tool.bgFrom}, ${tool.bgTo})` }}>
{/* 카드 헤더 */}
<div className="p-6 pb-4">
<div className="flex items-start justify-between mb-4">
<div style={{ color: tool.accentColor }} className="opacity-90">{tool.icon}</div>
<div className="flex flex-col items-end gap-1.5">
<span className="bg-white/10 text-white text-[10px] font-bold px-2.5 py-1 rounded-full border border-white/20">PREMIUM</span>
<span style={{ color: tool.accentColor }} className="text-xl font-extrabold">{tool.price}</span>
</div>
</div>
<div style={{ color: tool.accentColor }} className="text-xs font-bold mb-1 opacity-80">{tool.subtitle}</div>
<h3 className="text-white font-extrabold text-base mb-2 leading-snug" style={{ wordBreak: 'keep-all' }}>{tool.title}</h3>
<p className="text-white/50 text-xs leading-relaxed" style={{ wordBreak: 'keep-all' }}>{tool.desc}</p>
</div>
{/* 기능 목록 */}
<div className="px-6 pb-4 flex-1">
<div className="border-t border-white/10 pt-4 space-y-2">
{tool.features.map((f) => (
<div key={f} className="flex items-start gap-2">
<svg className="w-3.5 h-3.5 flex-shrink-0 mt-0.5" style={{ color: tool.accentColor }} viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd"/>
</svg>
<span className="text-white/70 text-xs" style={{ wordBreak: 'keep-all' }}>{f}</span>
</div>
))}
</div>
</div>
{/* 태그 + CTA */}
<div className="px-6 pb-6">
<div className="flex flex-wrap gap-1.5 mb-4">
{tool.tags.map((tag) => (
<span key={tag} className="text-[10px] font-bold px-2 py-0.5 rounded-full border border-white/20 text-white/60">{tag}</span>
))}
</div>
<button
onClick={() => openModal(`업무 자동화 프리미엄 - ${tool.title}`)}
className="w-full py-3 rounded-xl text-sm font-extrabold transition-all hover:opacity-90 active:scale-[0.98]"
style={{ backgroundColor: tool.accentColor, color: tool.bgFrom }}
>
·
</button>
<p className="text-white/30 text-[10px] text-center mt-2"> Python + </p>
</div>
</div>
))}
</div>
</div>
</div>
{/* ─── 자동화 툴 무료 다운로드 ─── */}
<div className="px-6 pb-12 lg:px-12">
<div className="max-w-5xl mx-auto">
<div className="text-center mb-8 reveal">
<p className="text-cyan-600 text-xs font-bold uppercase tracking-widest mb-2">FREE TOOLS</p>
<h2 className="text-2xl md:text-3xl font-extrabold text-[#04102b] mb-2"> </h2>
<p className="text-slate-500 text-sm"> .<br /> .</p>
</div>
<div className="grid sm:grid-cols-3 gap-5">
{tools.map((tool, idx) => (
<div key={tool.id} style={{ borderColor: tool.borderColor, backgroundColor: tool.bgColor }}
className={`rounded-2xl border-2 p-5 flex flex-col relative reveal reveal-d${idx + 1}`}>
{!tool.ready && (
<div className="absolute top-3 right-3 bg-slate-200 text-slate-500 text-[10px] font-bold px-2 py-0.5 rounded-full"></div>
)}
<div style={{ color: tool.color }} className="mb-3">{tool.icon}</div>
<div style={{ color: tool.color }} className="text-xs font-bold mb-0.5">{tool.subtitle}</div>
<div className="font-extrabold text-[#04102b] text-sm mb-2">{tool.title}</div>
<p className="text-slate-500 text-xs leading-relaxed mb-4 flex-1">{tool.desc}</p>
<div className="flex flex-wrap gap-1.5 mb-4">
{tool.tags.map((tag) => (
<span key={tag} style={{ color: tool.color, borderColor: tool.borderColor }}
className="text-[10px] font-bold px-2 py-0.5 rounded-full border bg-white">{tag}</span>
))}
</div>
{tool.ready ? (
<Link href={tool.href}
style={{ backgroundColor: tool.color }}
className="block w-full text-center py-2.5 rounded-xl text-white text-sm font-bold hover:opacity-90 transition">
·
</Link>
) : (
<button disabled
className="block w-full text-center py-2.5 rounded-xl bg-slate-200 text-slate-400 text-sm font-bold cursor-not-allowed">
</button>
)}
</div>
))}
</div>
</div>
</div>
{/* ─── CTA ─── */}
<div className="px-6 pb-12 lg:px-12">
<div className="max-w-3xl mx-auto">
<div className="bg-[#012030] rounded-2xl border border-cyan-400/20 p-8 text-center reveal" style={{ backgroundImage: 'repeating-linear-gradient(135deg, rgba(255,255,255,0.015) 0px, rgba(255,255,255,0.015) 1px, transparent 1px, transparent 30px)' }}>
<p className="text-cyan-400 text-xs font-bold uppercase tracking-widest mb-2">FREE CONSULTATION</p>
<h3 className="text-white text-2xl font-extrabold mb-2"> </h3>
<p className="text-cyan-100/40 text-sm mb-6"> </p>
<button
onClick={() => openModal('업무 자동화')}
className="inline-flex items-center gap-2 bg-cyan-400 hover:bg-cyan-300 text-[#012030] px-8 py-3 rounded-xl font-extrabold text-sm transition-all shadow-lg shadow-cyan-900/30"
>
( )
</button>
</div>
</div>
</div>
</div>
);
}

View File

@@ -1,344 +0,0 @@
'use client';
import Link from 'next/link';
import { useState } from 'react';
const features = [
{
icon: (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.5} className="w-5 h-5">
<path strokeLinecap="round" strokeLinejoin="round" d="M3.75 12h16.5m-16.5 3.75h16.5M3.75 19.5h16.5M5.625 4.5h12.75a1.875 1.875 0 010 3.75H5.625a1.875 1.875 0 010-3.75z" />
</svg>
),
title: '중복 데이터 자동 제거',
desc: '여러 시트에 흩어진 데이터를 하나로 합치고 중복 행을 자동으로 찾아 제거합니다. 작업 시간 90% 단축.',
color: 'text-emerald-600',
bg: 'bg-emerald-50',
border: 'border-emerald-200',
},
{
icon: (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.5} className="w-5 h-5">
<path strokeLinecap="round" strokeLinejoin="round" d="M7.5 14.25v2.25m3-4.5v4.5m3-6.75v6.75m3-9v9M6 20.25h12A2.25 2.25 0 0020.25 18V6A2.25 2.25 0 0018 3.75H6A2.25 2.25 0 003.75 6v12A2.25 2.25 0 006 20.25z" />
</svg>
),
title: '일별/월별 집계 자동화',
desc: '날짜 컬럼 기준으로 일별·주별·월별 합계를 자동 계산하고 별도 시트에 보고서를 생성합니다.',
color: 'text-blue-600',
bg: 'bg-blue-50',
border: 'border-blue-200',
},
{
icon: (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.5} className="w-5 h-5">
<path strokeLinecap="round" strokeLinejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m2.25 0H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" />
</svg>
),
title: '폴더 내 파일 일괄 처리',
desc: '지정한 폴더의 엑셀 파일 전체를 자동으로 열고 데이터를 통합합니다. 파일 수가 많아도 OK.',
color: 'text-violet-600',
bg: 'bg-violet-50',
border: 'border-violet-200',
},
{
icon: (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.5} className="w-5 h-5">
<path strokeLinecap="round" strokeLinejoin="round" d="M9.53 16.122a3 3 0 00-5.78 1.128 2.25 2.25 0 01-2.4 2.245 4.5 4.5 0 008.4-2.245c0-.399-.078-.78-.22-1.128zm0 0a15.998 15.998 0 003.388-1.62m-5.043-.025a15.994 15.994 0 011.622-3.395m3.42 3.42a15.995 15.995 0 004.764-4.648l3.876-5.814a1.151 1.151 0 00-1.597-1.597L14.146 6.32a15.996 15.996 0 00-4.649 4.763m3.42 3.42a6.776 6.776 0 00-3.42-3.42" />
</svg>
),
title: '서식·색상 자동 지정',
desc: '값 조건에 따라 셀 색상, 굵기, 테두리를 자동으로 지정합니다. 조건부 서식보다 빠릅니다.',
color: 'text-orange-600',
bg: 'bg-orange-50',
border: 'border-orange-200',
},
{
icon: (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.5} className="w-5 h-5">
<path strokeLinecap="round" strokeLinejoin="round" d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607z" />
</svg>
),
title: '키워드 일괄 검색·치환',
desc: '전체 시트에서 특정 단어를 찾아 일괄 변경하거나 해당 행만 별도 추출합니다.',
color: 'text-cyan-600',
bg: 'bg-cyan-50',
border: 'border-cyan-200',
},
{
icon: (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.5} className="w-5 h-5">
<path strokeLinecap="round" strokeLinejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3" />
</svg>
),
title: 'PDF / CSV 자동 저장',
desc: '작업 완료 후 PDF 또는 CSV 형식으로 자동 저장하고 지정한 폴더에 날짜별로 백업합니다.',
color: 'text-rose-600',
bg: 'bg-rose-50',
border: 'border-rose-200',
},
];
const howToUse = [
{ step: '01', title: '파일 다운로드', desc: '아래 버튼으로 .xlsm 파일을 받습니다. 매크로 포함 형식입니다.' },
{ step: '02', title: '매크로 실행 허용', desc: '파일을 열면 상단 노란 바에서 "콘텐츠 사용" 버튼을 클릭합니다.' },
{ step: '03', title: '데이터 시트에 붙여넣기', desc: '"Data" 시트에 내 데이터를 붙여넣습니다. A1부터 시작하면 됩니다.' },
{ step: '04', title: '매크로 버튼 클릭', desc: '"Control" 시트에서 원하는 기능 버튼을 클릭하면 자동 실행됩니다.' },
];
const faqs = [
{
q: '맥(Mac)에서도 사용할 수 있나요?',
a: 'Excel for Mac에서도 대부분 동작하나, VBA 일부 기능(파일 다이얼로그 등)은 Windows 전용입니다. Mac 사용자는 상담을 통해 호환 버전으로 수정 가능합니다.',
},
{
q: '파일이 열리지 않거나 오류가 발생하면요?',
a: 'Excel 2016 이상 버전을 권장합니다. 보안 정책으로 매크로가 차단된 경우 Excel 옵션 → 보안 센터 → 매크로 설정에서 "알림과 함께 VBA 매크로 사용"으로 변경해 주세요.',
},
{
q: '내 업무에 맞게 수정이 가능한가요?',
a: '파일 내 VBA 코드는 자유롭게 수정할 수 있습니다. 수정이 어려우시면 맞춤 자동화 개발 서비스로 문의해 주세요. 내 업무에 딱 맞는 버전을 만들어 드립니다.',
},
];
export default function ExcelToolPage() {
const [openFaq, setOpenFaq] = useState<number | null>(null);
return (
<div className="min-h-full bg-[#f0f5ff]">
{/* Hero */}
<div className="bg-gradient-to-br from-[#052e16] via-[#14532d] to-[#052e16] px-6 py-12 lg:px-12">
<div className="max-w-4xl mx-auto">
<Link href="/services/automation"
className="inline-flex items-center gap-1.5 text-emerald-300/60 hover:text-emerald-300 text-sm mb-6 transition">
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
</svg>
</Link>
<div className="flex flex-col sm:flex-row sm:items-center gap-6">
<div className="w-20 h-20 rounded-2xl bg-emerald-400/15 border border-emerald-400/30 flex items-center justify-center flex-shrink-0">
<svg className="w-10 h-10 text-emerald-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M3.375 19.5h17.25m-17.25 0a1.125 1.125 0 01-1.125-1.125M3.375 19.5h1.5C5.496 19.5 6 18.996 6 18.375m-3.75.125v-1.125c0-.621.504-1.125 1.125-1.125h1.5m0 0v1.25m0-1.25c0-.621.504-1.125 1.125-1.125h1.5m0 0V7.875m0 0c0-.621.504-1.125 1.125-1.125h8.25c.621 0 1.125.504 1.125 1.125v8.25m0 0v1.125m0-1.125c0 .621-.504 1.125-1.125 1.125H6m12-8.25v8.25" />
</svg>
</div>
<div>
<div className="flex items-center gap-2 mb-2">
<span className="text-emerald-400 text-xs font-bold uppercase tracking-widest">FREE TOOL</span>
<span className="bg-emerald-400/20 border border-emerald-400/40 text-emerald-300 text-[10px] font-bold px-2 py-0.5 rounded-full">v1.2</span>
<span className="bg-white/10 text-white/50 text-[10px] font-bold px-2 py-0.5 rounded-full">VBA · Excel</span>
</div>
<h1 className="text-3xl md:text-4xl font-extrabold text-white mb-2 leading-tight">
<br />
<span className="text-transparent bg-clip-text bg-gradient-to-r from-emerald-400 to-teal-300">
Excel Macro Toolkit
</span>
</h1>
<p className="text-emerald-100/50 text-sm leading-relaxed">
VBA .<br />
, , , .
</p>
</div>
</div>
{/* 통계 배지 */}
<div className="mt-8 inline-grid grid-cols-3 gap-px bg-emerald-400/10 border border-emerald-400/20 rounded-2xl overflow-hidden">
{[
{ v: '6가지', l: '핵심 기능' },
{ v: '무료', l: '완전 무료' },
{ v: 'Excel 2016+', l: '지원 버전' },
].map((s) => (
<div key={s.l} className="bg-[#052e16]/80 px-5 py-3 text-center">
<div className="text-white font-extrabold text-base">{s.v}</div>
<div className="text-emerald-400/50 text-xs mt-0.5">{s.l}</div>
</div>
))}
</div>
</div>
</div>
<div className="px-6 py-10 lg:px-12">
<div className="max-w-4xl mx-auto space-y-10">
{/* 다운로드 카드 */}
<div className="bg-white rounded-2xl border-2 border-emerald-200 p-6 flex flex-col sm:flex-row items-center gap-6">
<div className="flex-1">
<div className="text-emerald-700 text-xs font-bold uppercase tracking-widest mb-1">DOWNLOAD</div>
<div className="font-extrabold text-[#04102b] text-lg mb-1">Excel_Macro_Toolkit_v1.2.xlsm</div>
<div className="text-slate-500 text-xs mb-3">크기: 85KB · · </div>
<div className="flex flex-wrap gap-2">
{['VBA 매크로', '6가지 기능', 'Control 시트 UI', '가이드 시트 포함'].map((t) => (
<span key={t} className="text-[10px] font-bold px-2 py-0.5 rounded-full border border-emerald-200 text-emerald-700 bg-emerald-50">{t}</span>
))}
</div>
</div>
<div className="flex flex-col gap-2 w-full sm:w-auto">
<a
href={`${process.env.NEXT_PUBLIC_CDN_URL ?? ''}/downloads/Excel_Macro_Toolkit_v1.2.xlsm`}
download
className="flex items-center justify-center gap-2 bg-emerald-600 hover:bg-emerald-500 text-white px-6 py-3 rounded-xl font-extrabold text-sm transition-all shadow-lg shadow-emerald-900/20 w-full sm:w-48"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3" />
</svg>
</a>
<p className="text-[10px] text-slate-400 text-center"> </p>
</div>
</div>
{/* 기능 소개 */}
<div>
<div className="text-center mb-6">
<p className="text-emerald-600 text-xs font-bold uppercase tracking-widest mb-1">FEATURES</p>
<h2 className="text-2xl font-extrabold text-[#04102b]"> 6</h2>
</div>
<div className="grid sm:grid-cols-2 lg:grid-cols-3 gap-4">
{features.map((f) => (
<div key={f.title} className={`${f.bg} border-2 ${f.border} rounded-2xl p-5`}>
<div className={`${f.color} mb-3`}>{f.icon}</div>
<div className="font-bold text-[#04102b] text-sm mb-1.5">{f.title}</div>
<p className="text-slate-500 text-xs leading-relaxed">{f.desc}</p>
</div>
))}
</div>
</div>
{/* 사용 방법 */}
<div>
<div className="text-center mb-6">
<p className="text-emerald-600 text-xs font-bold uppercase tracking-widest mb-1">HOW TO USE</p>
<h2 className="text-2xl font-extrabold text-[#04102b]"> </h2>
</div>
<div className="relative">
<div className="hidden sm:block absolute top-10 left-[10%] right-[10%] h-0.5 bg-gradient-to-r from-emerald-200 via-teal-100 to-emerald-200" />
<div className="grid grid-cols-1 sm:grid-cols-4 gap-4">
{howToUse.map((h) => (
<div key={h.step} className="text-center">
<div className="w-20 h-20 mx-auto rounded-2xl bg-gradient-to-br from-[#052e16] to-[#14532d] border border-emerald-400/20 flex flex-col items-center justify-center mb-3 shadow-lg">
<span className="text-emerald-400 text-[10px] font-bold">STEP</span>
<span className="text-white font-extrabold text-lg leading-none">{h.step}</span>
</div>
<div className="font-bold text-[#04102b] text-sm mb-1">{h.title}</div>
<div className="text-slate-500 text-xs leading-relaxed">{h.desc}</div>
</div>
))}
</div>
</div>
</div>
{/* 미리보기 (목업) */}
<div>
<div className="text-center mb-6">
<p className="text-emerald-600 text-xs font-bold uppercase tracking-widest mb-1">PREVIEW</p>
<h2 className="text-2xl font-extrabold text-[#04102b]"> </h2>
</div>
<div className="bg-white rounded-2xl border border-[#dbe8ff] overflow-hidden shadow-sm">
{/* Excel 목업 */}
<div className="bg-[#1e7145] px-4 py-2 flex items-center gap-3">
<div className="flex gap-1.5">
<div className="w-3 h-3 rounded-full bg-white/30" />
<div className="w-3 h-3 rounded-full bg-white/30" />
<div className="w-3 h-3 rounded-full bg-white/30" />
</div>
<span className="text-white/80 text-xs font-medium">Excel_Macro_Toolkit_v1.2.xlsm</span>
</div>
{/* 탭 */}
<div className="flex border-b border-slate-200 bg-slate-50 px-2 pt-1 gap-1 overflow-x-auto">
{['Control', 'Data', 'Report', 'Guide'].map((tab, i) => (
<div key={tab}
className={`px-4 py-1.5 text-xs font-bold rounded-t-lg border-t border-x ${i === 0 ? 'bg-white border-slate-200 text-[#1e7145]' : 'bg-slate-100 border-slate-200 text-slate-400'}`}>
{tab}
</div>
))}
</div>
{/* Control 시트 목업 */}
<div className="p-6 bg-white">
<div className="text-center mb-5">
<div className="text-lg font-extrabold text-[#1e7145] mb-0.5">📊 Excel Macro Toolkit v1.2</div>
<div className="text-slate-400 text-xs"> </div>
</div>
<div className="grid grid-cols-2 sm:grid-cols-3 gap-3">
{[
{ label: '중복 제거', color: '#16a34a' },
{ label: '일별 집계', color: '#2563eb' },
{ label: '파일 통합', color: '#7c3aed' },
{ label: '서식 자동화', color: '#ea580c' },
{ label: '키워드 검색', color: '#0891b2' },
{ label: 'PDF 저장', color: '#dc2626' },
].map((btn) => (
<div key={btn.label}
style={{ borderColor: btn.color, color: btn.color }}
className="border-2 rounded-lg py-3 text-center text-sm font-bold cursor-default hover:opacity-80 transition bg-white shadow-sm">
{btn.label}
</div>
))}
</div>
<div className="mt-4 p-3 bg-slate-50 rounded-xl border border-slate-200">
<div className="text-[10px] text-slate-400 font-bold mb-1"> </div>
<div className="text-xs text-emerald-600 font-medium"> . .</div>
</div>
</div>
</div>
</div>
{/* FAQ */}
<div>
<div className="text-center mb-6">
<p className="text-emerald-600 text-xs font-bold uppercase tracking-widest mb-1">FAQ</p>
<h2 className="text-2xl font-extrabold text-[#04102b]"> </h2>
</div>
<div className="space-y-2">
{faqs.map((faq, i) => (
<div key={i} className="bg-white rounded-2xl border border-[#dbe8ff] overflow-hidden">
<button
onClick={() => setOpenFaq(openFaq === i ? null : i)}
className="w-full flex items-center justify-between px-5 py-4 text-left">
<span className="font-bold text-[#04102b] text-sm">{faq.q}</span>
<svg
className={`w-4 h-4 text-slate-400 flex-shrink-0 transition-transform ${openFaq === i ? 'rotate-180' : ''}`}
fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</button>
{openFaq === i && (
<div className="px-5 pb-4 text-slate-600 text-sm leading-relaxed border-t border-slate-100 pt-3">
{faq.a}
</div>
)}
</div>
))}
</div>
</div>
{/* 하단 CTA */}
<div className="bg-gradient-to-r from-[#052e16] to-[#14532d] rounded-2xl border border-emerald-400/20 p-8 text-center">
<p className="text-emerald-400 text-xs font-bold uppercase tracking-widest mb-2">CUSTOM AUTOMATION</p>
<h3 className="text-white text-xl font-extrabold mb-2"> ?</h3>
<p className="text-emerald-100/40 text-sm mb-6">
, .
</p>
<div className="flex flex-col sm:flex-row gap-3 justify-center">
<a
href={`${process.env.NEXT_PUBLIC_CDN_URL ?? ''}/downloads/Excel_Macro_Toolkit_v1.2.xlsm`}
download
className="inline-flex items-center justify-center gap-2 bg-emerald-400 hover:bg-emerald-300 text-[#052e16] px-6 py-3 rounded-xl font-extrabold text-sm transition-all"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3" />
</svg>
</a>
<Link href="/freelance?service=업무+자동화"
className="inline-flex items-center justify-center gap-2 bg-white/10 hover:bg-white/20 text-white border border-white/20 px-6 py-3 rounded-xl font-extrabold text-sm transition-all">
</Link>
</div>
</div>
</div>
</div>
</div>
);
}

View File

@@ -1,25 +0,0 @@
import type { Metadata } from 'next';
export const metadata: Metadata = {
title: 'PPT 제작 자동화 도구',
description:
'엑셀 데이터로 PPT를 자동 생성하는 Python 스크립트. 표지·내용·마무리 슬라이드 자동 구성, 색상 테마 커스터마이징. python-pptx 기반. 무료 다운로드.',
keywords: [
'PPT 자동화',
'파워포인트 자동 생성',
'python-pptx',
'PPT 제작 도구',
'엑셀 PPT 변환',
'프레젠테이션 자동화',
'무료 PPT 도구',
],
openGraph: {
title: 'PPT 제작 자동화 도구 | 쟁승메이드',
description: 'Python + python-pptx 기반 PPT 자동 생성 도구 무료 다운로드. 엑셀 연동 지원.',
url: 'https://jaengseung-made.com/services/automation/tools/ppt',
},
};
export default function PptLayout({ children }: { children: React.ReactNode }) {
return children;
}

View File

@@ -1,365 +0,0 @@
'use client';
import Link from 'next/link';
const features = [
{
icon: (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.5} className="w-5 h-5">
<path strokeLinecap="round" strokeLinejoin="round" d="M3.75 3v11.25A2.25 2.25 0 006 16.5h2.25M3.75 3h-1.5m1.5 0h16.5m0 0h1.5m-1.5 0v11.25A2.25 2.25 0 0118 16.5h-2.25m-7.5 0h7.5m-7.5 0l-1 3m8.5-3l1 3m0 0l.5 1.5m-.5-1.5h-9.5m0 0l-.5 1.5M9 11.25v1.5M12 9v3.75m3-6v6" />
</svg>
),
title: '표지 · 내용 · 마무리 자동 생성',
desc: '표지(제목/날짜), 내용 슬라이드(불릿 포인트), 마무리 슬라이드까지 3가지 레이아웃을 자동으로 구성합니다.',
color: 'text-orange-600',
bg: 'bg-orange-50',
border: 'border-orange-200',
},
{
icon: (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.5} className="w-5 h-5">
<path strokeLinecap="round" strokeLinejoin="round" d="M3.375 19.5h17.25m-17.25 0a1.125 1.125 0 01-1.125-1.125M3.375 19.5h1.5C5.496 19.5 6 18.996 6 18.375m-3.75.125v-1.125c0-.621.504-1.125 1.125-1.125h1.5m0 0v1.25m0-1.25c0-.621.504-1.125 1.125-1.125h1.5m0 0V7.875m0 0c0-.621.504-1.125 1.125-1.125h8.25c.621 0 1.125.504 1.125 1.125v8.25m0 0v1.125m0-1.125c0 .621-.504 1.125-1.125 1.125H6m12-8.25v8.25" />
</svg>
),
title: '엑셀에서 데이터 일괄 생성',
desc: 'data.xlsx 파일의 A열(제목), B~열(불릿 내용)을 읽어 슬라이드를 자동 생성합니다. 수십 장도 한 번에 처리.',
color: 'text-emerald-600',
bg: 'bg-emerald-50',
border: 'border-emerald-200',
},
{
icon: (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.5} className="w-5 h-5">
<path strokeLinecap="round" strokeLinejoin="round" d="M4.098 19.902a3.75 3.75 0 005.304 0l6.401-6.402M6.75 21A3.75 3.75 0 013 17.25V4.125C3 3.504 3.504 3 4.125 3h5.25c.621 0 1.125.504 1.125 1.125v4.072M6.75 21a3.75 3.75 0 003.75-3.75V8.197M6.75 21h13.125c.621 0 1.125-.504 1.125-1.125v-5.25c0-.621-.504-1.125-1.125-1.125h-4.072M10.5 8.197l2.88-2.88c.438-.439 1.15-.439 1.59 0l3.712 3.713c.44.44.44 1.152 0 1.59l-2.879 2.88" />
</svg>
),
title: '색상 테마 커스터마이징',
desc: '상단 설정 영역에서 PRIMARY, SECONDARY, ACCENT 색상을 RGB로 변경하면 전체 슬라이드에 즉시 반영됩니다.',
color: 'text-violet-600',
bg: 'bg-violet-50',
border: 'border-violet-200',
},
{
icon: (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.5} className="w-5 h-5">
<path strokeLinecap="round" strokeLinejoin="round" d="M7.5 8.25h9m-9 3H12m-9.75 1.51c0 1.6 1.123 2.994 2.707 3.227 1.129.166 2.27.293 3.423.379.35.026.67.21.865.501L12 21l2.755-4.133a1.14 1.14 0 01.865-.501 48.172 48.172 0 003.423-.379c1.584-.233 2.707-1.626 2.707-3.228V6.741c0-1.602-1.123-2.995-2.707-3.228A48.394 48.394 0 0012 3c-2.392 0-4.744.175-7.043.513C3.373 3.746 2.25 5.14 2.25 6.741v6.018z" />
</svg>
),
title: '슬라이드 번호 자동 추가',
desc: '각 내용 슬라이드 우측 상단에 슬라이드 번호(01, 02...)가 자동으로 표시됩니다. 따로 설정할 필요 없음.',
color: 'text-blue-600',
bg: 'bg-blue-50',
border: 'border-blue-200',
},
{
icon: (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.5} className="w-5 h-5">
<path strokeLinecap="round" strokeLinejoin="round" d="M9 17.25v1.007a3 3 0 01-.879 2.122L7.5 21h9l-.621-.621A3 3 0 0115 18.257V17.25m6-12V15a2.25 2.25 0 01-2.25 2.25H5.25A2.25 2.25 0 013 15V5.25m18 0A2.25 2.25 0 0018.75 3H5.25A2.25 2.25 0 003 5.25m18 0V12a2.25 2.25 0 01-2.25 2.25H5.25A2.25 2.25 0 013 12V5.25" />
</svg>
),
title: '16:9 비율 · 맑은 고딕 폰트',
desc: '발표 표준 비율인 16:9(13.33×7.5인치)로 설정되며, 한글 가독성이 좋은 맑은 고딕 폰트를 기본 적용합니다.',
color: 'text-cyan-600',
bg: 'bg-cyan-50',
border: 'border-cyan-200',
},
{
icon: (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.5} className="w-5 h-5">
<path strokeLinecap="round" strokeLinejoin="round" d="M12 9v3.75m9-.75a9 9 0 11-18 0 9 9 0 0118 0zm-9 3.75h.008v.008H12v-.008z" />
</svg>
),
title: '예시 데이터 자동 실행',
desc: 'data.xlsx 파일이 없어도 내장 예시 데이터로 바로 실행됩니다. 처음 사용할 때 결과를 즉시 확인 가능.',
color: 'text-rose-600',
bg: 'bg-rose-50',
border: 'border-rose-200',
},
];
const howToUse = [
{
step: '01',
title: '패키지 설치',
desc: '터미널에서 필요한 Python 패키지를 설치합니다.',
code: 'pip install python-pptx openpyxl',
color: 'bg-orange-500',
},
{
step: '02',
title: '설정 수정',
desc: '스크립트 상단 설정 영역에서 제목, 날짜, 색상 테마를 수정합니다.',
code: 'TITLE_TEXT = "발표 제목"\nCOLOR_PRIMARY = RGBColor(0x1D, 0x4E, 0xD8)',
color: 'bg-emerald-500',
},
{
step: '03',
title: '엑셀 데이터 준비',
desc: 'data.xlsx를 만들어 A열=슬라이드 제목, B~열=불릿 내용을 입력합니다. (없으면 예시 데이터로 실행)',
code: 'A열: 슬라이드 제목\nB~열: 불릿 포인트 내용',
color: 'bg-violet-500',
},
{
step: '04',
title: '실행 후 확인',
desc: '터미널에서 스크립트를 실행하면 같은 폴더에 PPT 파일이 자동 저장됩니다.',
code: 'python ppt_automation_v1.0.py',
color: 'bg-blue-500',
},
];
const faqs = [
{
q: '엑셀 파일이 없어도 실행되나요?',
a: 'data.xlsx 파일이 없으면 내장 예시 데이터로 자동 실행됩니다. 먼저 결과를 확인한 뒤 자신의 데이터로 교체하면 됩니다.',
},
{
q: '슬라이드 수에 제한이 있나요?',
a: '제한 없습니다. 엑셀에 입력한 행 수만큼 슬라이드가 생성됩니다. 단, 슬라이드당 불릿 포인트는 최대 8개입니다.',
},
{
q: '챕터 구분 슬라이드도 넣을 수 있나요?',
a: 'create_divider_slide() 함수가 포함되어 있습니다. main() 함수에서 원하는 위치에 호출하면 챕터 구분 슬라이드를 추가할 수 있습니다.',
},
{
q: '맥(Mac)에서도 사용할 수 있나요?',
a: '맥에서도 동일하게 사용 가능합니다. 단, 맥에는 맑은 고딕 폰트가 없으므로 FONT_NAME을 "AppleGothic" 또는 "Nanum Gothic"으로 변경하세요.',
},
];
export default function PptToolPage() {
return (
<div className="min-h-full bg-[#f0f5ff]">
{/* ─── Hero ─── */}
<div className="relative overflow-hidden bg-gradient-to-br from-[#1a0a3d] via-[#2d1560] to-[#1a0a3d] px-6 py-14 lg:px-12">
<div className="absolute inset-0 opacity-[0.05]">
<svg width="100%" height="100%" viewBox="0 0 800 300" preserveAspectRatio="xMidYMid slice">
<rect x="50" y="40" width="200" height="130" rx="6" fill="none" stroke="#c084fc" strokeWidth="1.5"/>
<line x1="70" y1="70" x2="230" y2="70" stroke="#c084fc" strokeWidth="1"/>
<line x1="70" y1="90" x2="200" y2="90" stroke="#c084fc" strokeWidth="1"/>
<line x1="70" y1="110" x2="210" y2="110" stroke="#c084fc" strokeWidth="1"/>
<rect x="320" y="60" width="200" height="130" rx="6" fill="none" stroke="#c084fc" strokeWidth="1.5"/>
<rect x="340" y="75" width="160" height="20" rx="3" fill="#c084fc" fillOpacity="0.2"/>
<line x1="340" y1="110" x2="500" y2="110" stroke="#c084fc" strokeWidth="1"/>
<line x1="340" y1="130" x2="480" y2="130" stroke="#c084fc" strokeWidth="1"/>
<rect x="590" y="40" width="160" height="130" rx="6" fill="none" stroke="#c084fc" strokeWidth="1.5"/>
<line x1="610" y1="100" x2="730" y2="100" stroke="#c084fc" strokeWidth="1.5"/>
<line x1="660" y1="110" x2="660" y2="160" stroke="#c084fc" strokeWidth="1"/>
<path d="M610 160 L660 110 L710 145 L730 120" fill="none" stroke="#c084fc" strokeWidth="1.5"/>
</svg>
</div>
<div className="relative max-w-3xl mx-auto text-center">
<Link href="/services/automation" className="inline-flex items-center gap-1.5 text-purple-300/60 hover:text-purple-300 text-sm mb-6 transition">
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" /></svg>
</Link>
<div className="w-16 h-16 mx-auto rounded-2xl bg-purple-400/15 border border-purple-400/25 flex items-center justify-center mb-5">
<svg className="w-9 h-9 text-purple-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M3.75 3v11.25A2.25 2.25 0 006 16.5h2.25M3.75 3h-1.5m1.5 0h16.5m0 0h1.5m-1.5 0v11.25A2.25 2.25 0 0118 16.5h-2.25m-7.5 0h7.5m-7.5 0l-1 3m8.5-3l1 3m0 0l.5 1.5m-.5-1.5h-9.5m0 0l-.5 1.5M9 11.25v1.5M12 9v3.75m3-6v6" />
</svg>
</div>
<p className="text-purple-400/70 text-xs font-bold uppercase tracking-widest mb-2">PPT AUTOMATION · </p>
<h1 className="text-4xl md:text-5xl font-extrabold text-white mb-4 tracking-tight leading-tight">
PPT <br />
<span className="text-transparent bg-clip-text bg-gradient-to-r from-purple-400 to-pink-400"> </span>
</h1>
<p className="text-purple-100/50 text-base md:text-lg leading-relaxed max-w-xl mx-auto mb-6">
·· .<br />
python-pptx .
</p>
<div className="flex flex-col sm:flex-row items-center justify-center gap-3">
<a
href="/downloads/ppt_automation_v1.0.py"
download
className="inline-flex items-center gap-2 bg-purple-400 hover:bg-purple-300 text-[#1a0a3d] px-8 py-3.5 rounded-xl font-extrabold text-sm transition-all shadow-lg shadow-purple-900/30"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" /></svg>
(Python .py)
</a>
<Link href="/freelance?service=PPT+자동화+맞춤+개발"
className="inline-flex items-center gap-2 bg-white/10 hover:bg-white/20 text-white border border-white/20 px-6 py-3.5 rounded-xl font-bold text-sm transition-all">
</Link>
</div>
</div>
</div>
{/* ─── 다운로드 카드 ─── */}
<div className="px-6 py-10 lg:px-12">
<div className="max-w-5xl mx-auto">
<div className="bg-white rounded-2xl border-2 border-purple-200 p-6 flex flex-col md:flex-row items-start md:items-center gap-6">
<div className="w-14 h-14 rounded-2xl bg-purple-50 border border-purple-200 flex items-center justify-center flex-shrink-0">
<svg className="w-7 h-7 text-purple-600" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M3.75 3v11.25A2.25 2.25 0 006 16.5h2.25M3.75 3h-1.5m1.5 0h16.5m0 0h1.5m-1.5 0v11.25A2.25 2.25 0 0118 16.5h-2.25m-7.5 0h7.5m-7.5 0l-1 3m8.5-3l1 3m0 0l.5 1.5m-.5-1.5h-9.5m0 0l-.5 1.5M9 11.25v1.5M12 9v3.75m3-6v6" />
</svg>
</div>
<div className="flex-1">
<div className="flex items-center gap-2 mb-1">
<span className="text-purple-600 text-xs font-bold">PPT AUTOMATION v1.0</span>
<span className="bg-purple-100 text-purple-700 text-[10px] font-bold px-2 py-0.5 rounded-full border border-purple-200"></span>
</div>
<div className="font-extrabold text-[#04102b] text-lg mb-1">PPT </div>
<p className="text-slate-500 text-sm">python-pptx · · // · </p>
<div className="flex flex-wrap gap-2 mt-3">
{['Python 3.8+', 'python-pptx', 'openpyxl', '한글 지원', '엑셀 연동'].map((tag) => (
<span key={tag} className="text-[10px] font-bold px-2 py-0.5 rounded-full border border-purple-200 text-purple-600 bg-purple-50">{tag}</span>
))}
</div>
</div>
<a
href="/downloads/ppt_automation_v1.0.py"
download
className="flex-shrink-0 inline-flex items-center gap-2 bg-purple-600 hover:bg-purple-700 text-white px-6 py-3 rounded-xl font-bold text-sm transition"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" /></svg>
</a>
</div>
</div>
</div>
{/* ─── 기능 ─── */}
<div className="px-6 pb-12 lg:px-12">
<div className="max-w-5xl mx-auto">
<div className="text-center mb-8">
<p className="text-purple-600 text-xs font-bold uppercase tracking-widest mb-2">FEATURES</p>
<h2 className="text-2xl md:text-3xl font-extrabold text-[#04102b]"> </h2>
</div>
<div className="grid sm:grid-cols-2 lg:grid-cols-3 gap-4">
{features.map((f) => (
<div key={f.title} className={`bg-white rounded-2xl border-2 ${f.border} p-5`}>
<div className={`w-9 h-9 rounded-xl ${f.bg} flex items-center justify-center mb-3 ${f.color}`}>
{f.icon}
</div>
<h3 className="font-bold text-[#04102b] text-sm mb-2">{f.title}</h3>
<p className="text-slate-500 text-xs leading-relaxed">{f.desc}</p>
</div>
))}
</div>
</div>
</div>
{/* ─── 사용법 ─── */}
<div className="px-6 pb-12 lg:px-12">
<div className="max-w-5xl mx-auto">
<div className="text-center mb-8">
<p className="text-purple-600 text-xs font-bold uppercase tracking-widest mb-2">HOW TO USE</p>
<h2 className="text-2xl md:text-3xl font-extrabold text-[#04102b]"> </h2>
</div>
<div className="grid sm:grid-cols-2 gap-4">
{howToUse.map((h) => (
<div key={h.step} className="bg-white rounded-2xl border border-slate-200 p-6">
<div className="flex items-center gap-3 mb-3">
<div className={`w-8 h-8 rounded-lg ${h.color} flex items-center justify-center text-white text-xs font-extrabold flex-shrink-0`}>
{h.step}
</div>
<h3 className="font-bold text-[#04102b] text-sm">{h.title}</h3>
</div>
<p className="text-slate-500 text-xs leading-relaxed mb-3">{h.desc}</p>
<div className="bg-[#1a0a3d] rounded-xl px-4 py-3 font-mono text-[11px] text-purple-200 whitespace-pre leading-relaxed">
{h.code}
</div>
</div>
))}
</div>
</div>
</div>
{/* ─── 코드 미리보기 ─── */}
<div className="px-6 pb-12 lg:px-12">
<div className="max-w-5xl mx-auto">
<div className="text-center mb-6">
<p className="text-purple-600 text-xs font-bold uppercase tracking-widest mb-2">PREVIEW</p>
<h2 className="text-2xl font-extrabold text-[#04102b]"> </h2>
<p className="text-slate-500 text-sm mt-1"> PPT가 </p>
</div>
<div className="bg-[#1a0a3d] rounded-2xl overflow-hidden border border-purple-900/40">
<div className="flex items-center gap-2 px-5 py-3 border-b border-purple-900/40">
<div className="w-3 h-3 rounded-full bg-rose-400/70" />
<div className="w-3 h-3 rounded-full bg-amber-400/70" />
<div className="w-3 h-3 rounded-full bg-emerald-400/70" />
<span className="ml-2 text-purple-300/50 text-xs font-mono">ppt_automation_v1.0.py</span>
</div>
<pre className="px-5 py-5 text-xs font-mono text-purple-100 leading-6 overflow-x-auto">{`<span class="text-purple-400"># ── 설정 (이 부분을 수정하세요) ──────────────</span>
DATA_FILE = <span class="text-amber-300">"data.xlsx"</span> <span class="text-slate-400"># 입력 엑셀 파일</span>
OUTPUT_FILE = f<span class="text-amber-300">"발표자료_{datetime}.pptx"</span>
<span class="text-slate-400"># 표지 정보</span>
TITLE_TEXT = <span class="text-amber-300">"발표 제목을 입력하세요"</span>
SUBTITLE_TEXT = <span class="text-amber-300">"부제목 또는 발표자 이름"</span>
DATE_TEXT = <span class="text-amber-300">"2025년 01월 01일"</span>
<span class="text-slate-400"># 색상 테마 (RGB 값으로 변경)</span>
COLOR_PRIMARY = RGBColor(<span class="text-emerald-400">0x1D, 0x4E, 0xD8</span>) <span class="text-slate-400"># 파란색</span>
COLOR_SECONDARY = RGBColor(<span class="text-emerald-400">0x0F, 0x17, 0x2A</span>) <span class="text-slate-400"># 다크 네이비</span>
COLOR_ACCENT = RGBColor(<span class="text-emerald-400">0x60, 0xA5, 0xFA</span>) <span class="text-slate-400"># 라이트 블루</span>
FONT_NAME = <span class="text-amber-300">"맑은 고딕"</span> <span class="text-slate-400"># 한글 폰트</span>`}
</pre>
</div>
<p className="text-center text-slate-400 text-xs mt-3">
* . .
</p>
</div>
</div>
{/* ─── FAQ ─── */}
<div className="px-6 pb-12 lg:px-12">
<div className="max-w-3xl mx-auto">
<div className="text-center mb-8">
<p className="text-purple-600 text-xs font-bold uppercase tracking-widest mb-2">FAQ</p>
<h2 className="text-2xl md:text-3xl font-extrabold text-[#04102b]"> </h2>
</div>
<div className="space-y-3">
{faqs.map((faq) => (
<div key={faq.q} className="bg-white rounded-2xl border border-slate-200 p-5">
<div className="flex items-start gap-3">
<span className="text-purple-600 font-extrabold text-sm flex-shrink-0 mt-0.5">Q.</span>
<div>
<div className="font-bold text-[#04102b] text-sm mb-1.5">{faq.q}</div>
<div className="text-slate-500 text-xs leading-relaxed">{faq.a}</div>
</div>
</div>
</div>
))}
</div>
</div>
</div>
{/* ─── CTA ─── */}
<div className="px-6 pb-12 lg:px-12">
<div className="max-w-3xl mx-auto">
<div className="bg-gradient-to-r from-[#1a0a3d] to-[#2d1560] rounded-2xl border border-purple-400/20 p-8 text-center">
<p className="text-purple-400 text-xs font-bold uppercase tracking-widest mb-2">CUSTOM DEVELOPMENT</p>
<h3 className="text-white text-2xl font-extrabold mb-2"> PPT ?</h3>
<p className="text-purple-100/40 text-sm mb-6">
, , 릿 <br />
PPT .
</p>
<div className="flex flex-col sm:flex-row items-center justify-center gap-3">
<a
href="/downloads/ppt_automation_v1.0.py"
download
className="inline-flex items-center gap-2 bg-purple-400 hover:bg-purple-300 text-[#1a0a3d] px-8 py-3 rounded-xl font-extrabold text-sm transition-all shadow-lg shadow-purple-900/30"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" /></svg>
</a>
<Link href="/freelance?service=PPT+자동화+맞춤+개발"
className="inline-flex items-center gap-2 bg-white/10 hover:bg-white/20 text-white border border-white/20 px-6 py-3 rounded-xl font-bold text-sm transition-all">
</Link>
</div>
</div>
</div>
</div>
</div>
);
}

View File

@@ -1,25 +0,0 @@
import type { Metadata } from 'next';
export const metadata: Metadata = {
title: '웹 크롤링·스크래핑 도구',
description:
'공공데이터·쇼핑몰 가격·뉴스를 자동 수집해 엑셀로 저장하는 Python 웹 스크래퍼. requests + BeautifulSoup4 + openpyxl 기반. 무료 다운로드.',
keywords: [
'웹 크롤링',
'웹 스크래핑',
'파이썬 크롤러',
'데이터 수집 자동화',
'BeautifulSoup',
'엑셀 자동화',
'무료 크롤러',
],
openGraph: {
title: '웹 크롤링 자동화 도구 | 쟁승메이드',
description: 'Python 기반 웹 크롤러 무료 다운로드. 페이지네이션·재시도·엑셀 저장 지원.',
url: 'https://jaengseung-made.com/services/automation/tools/scraper',
},
};
export default function ScraperLayout({ children }: { children: React.ReactNode }) {
return children;
}

View File

@@ -1,284 +0,0 @@
'use client';
import Link from 'next/link';
import { useState } from 'react';
const features = [
{
icon: (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.5} className="w-5 h-5">
<path strokeLinecap="round" strokeLinejoin="round" d="M12 21a9.004 9.004 0 008.716-6.747M12 21a9.004 9.004 0 01-8.716-6.747M12 21c2.485 0 4.5-4.03 4.5-9S14.485 3 12 3m0 18c-2.485 0-4.5-4.03-4.5-9S9.515 3 12 3m0 0a8.997 8.997 0 017.843 4.582M12 3a8.997 8.997 0 00-7.843 4.582m15.686 0A11.953 11.953 0 0112 10.5c-2.998 0-5.74-1.1-7.843-2.918m15.686 0A8.959 8.959 0 0121 12c0 .778-.099 1.533-.284 2.253m0 0A17.919 17.919 0 0112 16.5c-3.162 0-6.133-.815-8.716-2.247m0 0A9.015 9.015 0 013 12c0-1.605.42-3.113 1.157-4.418" />
</svg>
),
title: '웹 페이지 데이터 자동 수집',
desc: '공공데이터, 쇼핑몰 가격, 뉴스 기사 등 원하는 페이지의 데이터를 자동으로 수집합니다.',
color: 'text-blue-600', bg: 'bg-blue-50', border: 'border-blue-200',
},
{
icon: (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.5} className="w-5 h-5">
<path strokeLinecap="round" strokeLinejoin="round" d="M9 12h3.75M9 15h3.75M9 18h3.75m3 .75H18a2.25 2.25 0 002.25-2.25V6.108c0-1.135-.845-2.098-1.976-2.192a48.424 48.424 0 00-1.123-.08m-5.801 0c-.065.21-.1.433-.1.664 0 .414.336.75.75.75h4.5a.75.75 0 00.75-.75 2.25 2.25 0 00-.1-.664m-5.8 0A2.251 2.251 0 0113.5 2.25H15c1.012 0 1.867.668 2.15 1.586m-5.8 0c-.376.023-.75.05-1.124.08C9.095 4.01 8.25 4.973 8.25 6.108V8.25m0 0H4.875c-.621 0-1.125.504-1.125 1.125v11.25c0 .621.504 1.125 1.125 1.125h9.75c.621 0 1.125-.504 1.125-1.125V9.375c0-.621-.504-1.125-1.125-1.125H8.25zM6.75 12h.008v.008H6.75V12zm0 3h.008v.008H6.75V15zm0 3h.008v.008H6.75V18z" />
</svg>
),
title: '엑셀 자동 저장',
desc: '수집한 데이터를 열 서식, 헤더 스타일이 적용된 엑셀 파일로 자동 저장합니다.',
color: 'text-emerald-600', bg: 'bg-emerald-50', border: 'border-emerald-200',
},
{
icon: (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.5} className="w-5 h-5">
<path strokeLinecap="round" strokeLinejoin="round" d="M19 7.5v3m0 0v3m0-3h3m-3 0h-3m-2.25-4.125a3.375 3.375 0 11-6.75 0 3.375 3.375 0 016.75 0zM4 19.235v-.11a6.375 6.375 0 0112.75 0v.109A12.318 12.318 0 0110.374 21c-2.331 0-4.512-.645-6.374-1.766z" />
</svg>
),
title: '페이지네이션 자동 탐색',
desc: '다음 페이지 링크를 자동으로 찾아 여러 페이지의 데이터를 연속으로 수집합니다.',
color: 'text-violet-600', bg: 'bg-violet-50', border: 'border-violet-200',
},
{
icon: (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.5} className="w-5 h-5">
<path strokeLinecap="round" strokeLinejoin="round" d="M9 12.75L11.25 15 15 9.75m-3-7.036A11.959 11.959 0 013.598 6 11.99 11.99 0 003 9.749c0 5.592 3.824 10.29 9 11.623 5.176-1.332 9-6.03 9-11.622 0-1.31-.21-2.571-.598-3.751h-.152c-3.196 0-6.1-1.248-8.25-3.285z" />
</svg>
),
title: '재시도 로직 내장',
desc: '네트워크 오류나 일시적 접속 실패 시 자동으로 재시도합니다. 수집 실패 최소화.',
color: 'text-orange-600', bg: 'bg-orange-50', border: 'border-orange-200',
},
{
icon: (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.5} className="w-5 h-5">
<path strokeLinecap="round" strokeLinejoin="round" d="M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
),
title: '요청 간격 자동 조절',
desc: '서버에 부하를 주지 않도록 요청 간격을 자동으로 조절합니다. 차단 위험 최소화.',
color: 'text-cyan-600', bg: 'bg-cyan-50', border: 'border-cyan-200',
},
{
icon: (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.5} className="w-5 h-5">
<path strokeLinecap="round" strokeLinejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" />
</svg>
),
title: '로그 파일 자동 저장',
desc: '수집 과정 전체를 로그로 남겨 나중에 어떤 URL에서 몇 건을 수집했는지 확인 가능합니다.',
color: 'text-rose-600', bg: 'bg-rose-50', border: 'border-rose-200',
},
];
const howToUse = [
{ step: '01', title: 'Python 설치', desc: 'python.org에서 Python 3.10 이상을 설치하세요. "Add to PATH" 체크 필수.' },
{ step: '02', title: '패키지 설치', desc: '터미널에서 pip install requests beautifulsoup4 openpyxl lxml 실행.' },
{ step: '03', title: 'URL 설정', desc: '파일 상단 TARGET_URL에 크롤링할 주소를 입력하세요.' },
{ step: '04', title: '실행', desc: 'python web_scraper_v1.0.py 실행 → 같은 폴더에 엑셀 파일이 생성됩니다.' },
];
const faqs = [
{
q: '크롤링이 법적으로 문제없나요?',
a: '공개된 정보 수집 자체는 일반적으로 허용되지만, 사이트의 robots.txt와 이용약관을 반드시 확인하세요. 로그인이 필요한 페이지, 개인정보, 저작권 데이터 수집은 법적 문제가 생길 수 있습니다.',
},
{
q: '자바스크립트로 렌더링되는 사이트도 되나요?',
a: 'requests + BeautifulSoup은 정적 HTML만 수집합니다. JS 렌더링 사이트(React, Vue 등)는 Selenium/Playwright가 필요하며, 맞춤 개발 서비스로 문의 주시면 구현해 드립니다.',
},
{
q: '원하는 항목만 골라서 수집할 수 있나요?',
a: '파일 내 extract_data 함수를 수정하면 됩니다. HTML 선택자(CSS Selector)로 원하는 요소만 지정할 수 있으며, 코드 내 주석에 예시가 포함되어 있습니다.',
},
];
export default function ScraperToolPage() {
const [openFaq, setOpenFaq] = useState<number | null>(null);
return (
<div className="min-h-full bg-[#f0f5ff]">
{/* Hero */}
<div className="bg-gradient-to-br from-[#1e3a8a] via-[#1d4ed8] to-[#1e3a8a] px-6 py-12 lg:px-12">
<div className="max-w-4xl mx-auto">
<Link href="/services/automation"
className="inline-flex items-center gap-1.5 text-blue-300/60 hover:text-blue-300 text-sm mb-6 transition">
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
</svg>
</Link>
<div className="flex flex-col sm:flex-row sm:items-center gap-6">
<div className="w-20 h-20 rounded-2xl bg-blue-400/15 border border-blue-400/30 flex items-center justify-center flex-shrink-0">
<svg className="w-10 h-10 text-blue-300" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M12 21a9.004 9.004 0 008.716-6.747M12 21a9.004 9.004 0 01-8.716-6.747M12 21c2.485 0 4.5-4.03 4.5-9S14.485 3 12 3m0 18c-2.485 0-4.5-4.03-4.5-9S9.515 3 12 3m0 0a8.997 8.997 0 017.843 4.582M12 3a8.997 8.997 0 00-7.843 4.582m15.686 0A11.953 11.953 0 0112 10.5c-2.998 0-5.74-1.1-7.843-2.918m15.686 0A8.959 8.959 0 0121 12c0 .778-.099 1.533-.284 2.253m0 0A17.919 17.919 0 0112 16.5c-3.162 0-6.133-.815-8.716-2.247m0 0A9.015 9.015 0 013 12c0-1.605.42-3.113 1.157-4.418" />
</svg>
</div>
<div>
<div className="flex items-center gap-2 mb-2">
<span className="text-blue-400 text-xs font-bold uppercase tracking-widest">FREE TOOL</span>
<span className="bg-blue-400/20 border border-blue-400/40 text-blue-300 text-[10px] font-bold px-2 py-0.5 rounded-full">v1.0</span>
<span className="bg-white/10 text-white/50 text-[10px] font-bold px-2 py-0.5 rounded-full">Python · BeautifulSoup</span>
</div>
<h1 className="text-3xl md:text-4xl font-extrabold text-white mb-2 leading-tight">
<br />
<span className="text-transparent bg-clip-text bg-gradient-to-r from-blue-300 to-cyan-300">
Web Scraper
</span>
</h1>
<p className="text-blue-100/50 text-sm leading-relaxed">
, , <br />
. Python .
</p>
</div>
</div>
<div className="mt-8 inline-grid grid-cols-3 gap-px bg-blue-400/10 border border-blue-400/20 rounded-2xl overflow-hidden">
{[
{ v: '6가지', l: '핵심 기능' },
{ v: '무료', l: '완전 무료' },
{ v: 'Python 3.10+', l: '지원 버전' },
].map((s) => (
<div key={s.l} className="bg-[#1e3a8a]/60 px-5 py-3 text-center">
<div className="text-white font-extrabold text-base">{s.v}</div>
<div className="text-blue-400/50 text-xs mt-0.5">{s.l}</div>
</div>
))}
</div>
</div>
</div>
<div className="px-6 py-10 lg:px-12">
<div className="max-w-4xl mx-auto space-y-10">
{/* 다운로드 카드 */}
<div className="bg-white rounded-2xl border-2 border-blue-200 p-6 flex flex-col sm:flex-row items-center gap-6">
<div className="flex-1">
<div className="text-blue-700 text-xs font-bold uppercase tracking-widest mb-1">DOWNLOAD</div>
<div className="font-extrabold text-[#04102b] text-lg mb-1">web_scraper_v1.0.py</div>
<div className="text-slate-500 text-xs mb-3">크기: 8KB · Python · </div>
<div className="flex flex-wrap gap-2">
{['Python 3.10+', '페이지네이션', '재시도 로직', '엑셀 자동 저장', '로그 저장'].map((t) => (
<span key={t} className="text-[10px] font-bold px-2 py-0.5 rounded-full border border-blue-200 text-blue-700 bg-blue-50">{t}</span>
))}
</div>
</div>
<div className="flex flex-col gap-2 w-full sm:w-auto">
<a
href="/downloads/web_scraper_v1.0.py"
download
className="flex items-center justify-center gap-2 bg-blue-600 hover:bg-blue-500 text-white px-6 py-3 rounded-xl font-extrabold text-sm transition-all shadow-lg shadow-blue-900/20 w-full sm:w-48"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3" />
</svg>
</a>
<p className="text-[10px] text-slate-400 text-center"> </p>
</div>
</div>
{/* 기능 목록 */}
<div>
<h2 className="text-xl font-extrabold text-[#04102b] mb-5"> </h2>
<div className="grid sm:grid-cols-2 lg:grid-cols-3 gap-4">
{features.map((f) => (
<div key={f.title} className={`rounded-xl border p-4 ${f.bg} ${f.border}`}>
<div className={`${f.color} mb-3`}>{f.icon}</div>
<div className={`text-xs font-bold mb-1 ${f.color}`}>{f.title}</div>
<p className="text-slate-600 text-xs leading-relaxed">{f.desc}</p>
</div>
))}
</div>
</div>
{/* 사용 방법 */}
<div>
<h2 className="text-xl font-extrabold text-[#04102b] mb-5"> </h2>
<div className="grid sm:grid-cols-2 gap-4">
{howToUse.map((h) => (
<div key={h.step} className="bg-white rounded-xl border border-[#dbe8ff] p-5 flex gap-4">
<div className="text-blue-600 text-2xl font-black leading-none flex-shrink-0">{h.step}</div>
<div>
<div className="font-bold text-[#04102b] text-sm mb-1">{h.title}</div>
<p className="text-slate-500 text-xs leading-relaxed">{h.desc}</p>
</div>
</div>
))}
</div>
</div>
{/* 코드 예시 */}
<div className="bg-[#0f172a] rounded-2xl p-6 overflow-x-auto">
<div className="flex items-center gap-2 mb-4">
<span className="text-xs font-bold text-blue-400 uppercase tracking-widest">CODE PREVIEW</span>
<span className="text-slate-600 text-xs">extract_data </span>
</div>
<pre className="text-sm text-slate-300 leading-relaxed font-mono whitespace-pre">{`def extract_data(soup, page_url):
items = []
# 상품 목록 수집 예시
for item in soup.select(".product-item"):
name = item.select_one(".name")
price = item.select_one(".price")
items.append({
"상품명": name.get_text(strip=True),
"가격": price.get_text(strip=True),
"URL": page_url,
})
return items`}</pre>
</div>
{/* FAQ */}
<div>
<h2 className="text-xl font-extrabold text-[#04102b] mb-5"> </h2>
<div className="space-y-3">
{faqs.map((faq, i) => (
<div key={i} className="bg-white rounded-xl border border-[#dbe8ff] overflow-hidden">
<button
onClick={() => setOpenFaq(openFaq === i ? null : i)}
className="w-full flex items-center justify-between px-5 py-4 text-left"
>
<span className="font-bold text-[#04102b] text-sm">{faq.q}</span>
<svg className={`w-4 h-4 text-slate-400 transition-transform ${openFaq === i ? 'rotate-180' : ''}`}
fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</button>
{openFaq === i && (
<div className="px-5 pb-4 text-slate-500 text-sm leading-relaxed border-t border-[#dbe8ff] pt-3">
{faq.a}
</div>
)}
</div>
))}
</div>
</div>
{/* CTA */}
<div className="bg-gradient-to-r from-[#1e3a8a] to-[#1d4ed8] rounded-2xl p-8 text-center">
<p className="text-blue-300 text-xs font-bold uppercase tracking-widest mb-2">CUSTOM DEVELOPMENT</p>
<h3 className="text-white text-xl font-extrabold mb-2"> ?</h3>
<p className="text-blue-100/50 text-sm mb-6 leading-relaxed">
JS , , , <br />
.
</p>
<div className="flex flex-col sm:flex-row gap-3 justify-center">
<a
href="/downloads/web_scraper_v1.0.py"
download
className="inline-flex items-center justify-center gap-2 bg-blue-400 hover:bg-blue-300 text-[#1e3a8a] px-6 py-3 rounded-xl font-extrabold text-sm transition-all"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3" />
</svg>
</a>
<Link href="/freelance?service=업무+자동화"
className="inline-flex items-center justify-center gap-2 bg-white/10 hover:bg-white/20 text-white border border-white/20 px-6 py-3 rounded-xl font-extrabold text-sm transition-all">
</Link>
</div>
</div>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,26 @@
import type { Metadata } from 'next';
export const metadata: Metadata = {
title: '블로그 자동화 솔루션 팩 | 쟁승메이드',
description:
'쿠팡파트너스·네이버 애드포스트·브랜드커넥트 수익을 자동화하는 프롬프트 조합법 45종 + 구조 템플릿 PDF 80p + 샘플 글 10편. ₩29,000 한 번 결제, 평생 무료 업데이트.',
keywords: [
'블로그 자동화',
'AI 블로그 글쓰기',
'쿠팡파트너스 자동화',
'애드포스트 수익화',
'네이버 블로그 SEO',
'ChatGPT 블로그',
'블로그 프롬프트',
],
openGraph: {
title: '블로그 자동화 솔루션 팩 | 쟁승메이드',
description:
'쿠팡파트너스·애드포스트 수익을 자동화하는 프롬프트 + 템플릿 + 샘플. ₩29,000.',
url: 'https://jaengseung-made.com/services/blog',
},
};
export default function BlogLayout({ children }: { children: React.ReactNode }) {
return children;
}

243
app/services/blog/page.tsx Normal file
View File

@@ -0,0 +1,243 @@
'use client';
import { useState } from 'react';
import Link from 'next/link';
import PurchaseAgreementModal from '../../components/PurchaseAgreementModal';
const PACK_ITEMS = [
{
icon: '📝',
title: '프롬프트 조합법 45종',
desc: '상품리뷰 / 정보글 / 후기 / 비교 / 하우투 글별 최적 프롬프트 조합',
meta: 'PDF 80p',
},
{
icon: '📐',
title: '블로그 글 구조 템플릿 12종',
desc: '쿠팡파트너스 · 애드포스트 클릭을 유도하는 검증된 글 구조',
meta: 'Notion 템플릿',
},
{
icon: '💰',
title: '샘플 글 10편',
desc: '실제로 수익이 발생한 블로그 글 전문 + 해설 주석',
meta: '.docx · .md',
},
{
icon: '🔍',
title: '네이버 SEO 체크리스트',
desc: 'C-Rank · D.I.A. 알고리즘 대응 14가지 체크 포인트',
meta: 'PDF 20p',
},
];
const FAQS = [
{
q: '초보자도 쓸 수 있나요?',
a: 'ChatGPT나 Claude 계정만 있으면 됩니다. 프롬프트를 복붙하는 것부터 시작해서 점차 응용하도록 설계했습니다.',
},
{
q: '어떤 플랫폼에 맞나요?',
a: '네이버 블로그·티스토리·브런치 모두 대응. 쿠팡파트너스·애드포스트·브랜드커넥트 3가지 수익화 흐름을 모두 다룹니다.',
},
{
q: '업데이트는 얼마나 자주 되나요?',
a: '월 1~2회 주요 업데이트. 구매자 전용 Notion 페이지에서 변경 이력과 최신 파일을 제공합니다. 구매 후 12개월간 무료.',
},
{
q: '환불이 되나요?',
a: '전자상거래법상 디지털 콘텐츠는 제공 시작 후 환불이 제한됩니다. 구매 전 샘플 미리보기를 충분히 확인해주세요. 파일 손상·전달 불량은 즉시 재전달 또는 환불됩니다.',
},
];
export default function BlogServicePage() {
const [agreeOpen, setAgreeOpen] = useState(false);
const [openFaq, setOpenFaq] = useState<number | null>(0);
return (
<div className="min-h-full bg-white">
{/* HERO */}
<section className="relative overflow-hidden px-6 py-20 lg:px-14 lg:py-28 bg-gradient-to-br from-blue-50 via-white to-sky-50">
<div className="relative max-w-5xl mx-auto">
<p className="font-mono text-xs text-blue-700/70 tracking-[0.25em] uppercase mb-6">
Blog Automation Pack
</p>
<h1
className="text-[2.4rem] md:text-[3.2rem] lg:text-[4rem] font-extrabold leading-[1.08] tracking-tight text-slate-900 mb-6"
style={{ wordBreak: 'keep-all' }}
>
,
<br />
<span className="text-blue-700">AI에게 .</span>
</h1>
<p
className="text-slate-600 text-lg md:text-xl leading-relaxed mb-4 max-w-2xl"
style={{ wordBreak: 'keep-all' }}
>
· ·
<br />
<span className="text-slate-900 font-semibold"> · · </span>.
</p>
<div className="inline-flex items-center gap-3 bg-white border border-blue-200 rounded-xl px-5 py-3 mb-8 shadow-sm">
<span className="text-3xl font-extrabold text-blue-700 font-mono">29,000</span>
<span className="text-xs text-slate-500"> · 12 </span>
</div>
<div className="flex flex-wrap gap-3">
<button
onClick={() => setAgreeOpen(true)}
className="inline-flex items-center gap-2 bg-blue-700 hover:bg-blue-600 text-white px-8 py-4 rounded-xl font-bold text-sm transition-colors shadow-lg shadow-blue-500/30"
>
</button>
<a
href="#sample"
className="inline-flex items-center gap-2 border border-slate-300 hover:border-blue-500 text-slate-700 hover:text-blue-700 px-8 py-4 rounded-xl font-semibold text-sm transition-all"
>
</a>
</div>
</div>
</section>
{/* PAIN POINTS */}
<section className="px-6 py-16 lg:px-14 bg-white">
<div className="max-w-5xl mx-auto">
<h2
className="text-2xl md:text-3xl font-extrabold text-slate-900 mb-10 text-center"
style={{ wordBreak: 'keep-all' }}
>
.
</h2>
<div className="grid sm:grid-cols-3 gap-5">
{[
{ icon: '🕐', title: '매일 1시간+', desc: '글 소재·구성에 시간 다 쓰고 수익은 제자리' },
{ icon: '📉', title: '수익화 6개월+', desc: '블로그 키워놓고도 수익 구조가 안 잡힘' },
{ icon: '🤖', title: 'AI 글은 어색', desc: 'ChatGPT 그대로 복붙하면 바로 들통' },
].map((p) => (
<div key={p.title} className="border border-slate-200 rounded-2xl p-6 bg-slate-50/50">
<div className="text-3xl mb-3">{p.icon}</div>
<h3 className="font-extrabold text-slate-900 mb-2">{p.title}</h3>
<p className="text-sm text-slate-600 leading-relaxed" style={{ wordBreak: 'keep-all' }}>
{p.desc}
</p>
</div>
))}
</div>
</div>
</section>
{/* PACK CONTENT */}
<section id="sample" className="px-6 py-20 lg:px-14 bg-slate-50">
<div className="max-w-5xl mx-auto">
<p className="font-mono text-xs text-blue-700/70 tracking-widest uppercase mb-2">
Pack Contents
</p>
<h2 className="text-2xl md:text-3xl font-extrabold text-slate-900 mb-10">
4
</h2>
<div className="grid md:grid-cols-2 gap-5">
{PACK_ITEMS.map((it) => (
<div
key={it.title}
className="flex gap-4 bg-white border border-slate-200 rounded-2xl p-6 hover:border-blue-400 transition-colors"
>
<div className="text-3xl flex-shrink-0">{it.icon}</div>
<div>
<div className="flex items-center gap-2 mb-1.5">
<h3 className="font-extrabold text-slate-900">{it.title}</h3>
<span className="text-[10px] font-mono text-blue-700 bg-blue-100 px-1.5 py-0.5 rounded">
{it.meta}
</span>
</div>
<p className="text-sm text-slate-600 leading-relaxed" style={{ wordBreak: 'keep-all' }}>
{it.desc}
</p>
</div>
</div>
))}
</div>
{/* Sample preview */}
<div className="mt-12 bg-white border-2 border-dashed border-blue-300 rounded-2xl p-8 relative overflow-hidden">
<span className="absolute top-4 right-4 text-[10px] font-bold text-blue-700 bg-blue-100 px-2 py-1 rounded">
</span>
<h4 className="font-extrabold text-slate-900 mb-3"> · </h4>
<pre className="text-xs font-mono text-slate-700 bg-slate-50 rounded-lg p-4 overflow-x-auto leading-relaxed">{`당신은 [카테고리] 전문 블로거입니다.
아래 상품의 [핵심 장점 3개]와 [주의점 1개]를 기반으로
C-Rank 알고리즘에 최적화된 1,200자 리뷰 글을 작성하세요.
[구조]
1. 후킹 도입 (공감형 질문)
2. 상품 요약 (스펙 표)
3. 실사용 관점 장점·단점
4. 대안 비교 (쿠팡 링크 삽입 지점: {LINK})
5. 결론 + 재질문 유도
[톤앤매너] 친근한 존댓말, 광고 느낌 최소화 ...`}</pre>
<p className="text-xs text-slate-500 mt-4">
45 .
</p>
</div>
</div>
</section>
{/* FAQ */}
<section className="px-6 py-20 lg:px-14 bg-white">
<div className="max-w-3xl mx-auto">
<h2 className="text-2xl md:text-3xl font-extrabold text-slate-900 mb-8 text-center">
</h2>
<div className="space-y-3">
{FAQS.map((f, i) => (
<div key={i} className="border border-slate-200 rounded-xl overflow-hidden">
<button
onClick={() => setOpenFaq(openFaq === i ? null : i)}
className="w-full flex items-center justify-between px-5 py-4 text-left hover:bg-slate-50 transition-colors"
>
<span className="font-bold text-slate-900 text-sm">{f.q}</span>
<span className={`text-blue-700 text-xl transition-transform ${openFaq === i ? 'rotate-45' : ''}`}>
+
</span>
</button>
{openFaq === i && (
<div className="px-5 pb-5 text-sm text-slate-600 leading-relaxed" style={{ wordBreak: 'keep-all' }}>
{f.a}
</div>
)}
</div>
))}
</div>
</div>
</section>
{/* FINAL CTA */}
<section className="px-6 py-20 lg:px-14 bg-gradient-to-br from-blue-700 to-blue-900">
<div className="max-w-3xl mx-auto text-center">
<h2 className="text-3xl md:text-4xl font-extrabold text-white mb-4" style={{ wordBreak: 'keep-all' }}>
.
</h2>
<p className="text-blue-100 text-lg mb-8">29,000 · </p>
<button
onClick={() => setAgreeOpen(true)}
className="inline-flex items-center gap-2 bg-white text-blue-700 hover:bg-blue-50 px-10 py-4 rounded-xl font-extrabold text-base transition-colors shadow-xl"
>
</button>
<p className="text-blue-200/80 text-xs mt-6">
<Link href="/legal/refund" className="underline hover:text-white"> </Link>
{' · '}
<Link href="/legal/terms" className="underline hover:text-white"></Link>
</p>
</div>
</section>
<PurchaseAgreementModal
isOpen={agreeOpen}
onClose={() => setAgreeOpen(false)}
productName="블로그 자동화 솔루션 팩"
price="₩29,000"
/>
</div>
);
}

View File

@@ -1,25 +0,0 @@
import type { Metadata } from 'next';
export const metadata: Metadata = {
title: '로또 번호 추천',
description:
'1,100+회차 빅데이터 기반 로또 번호 분석. 핫/콜드 번호 통계, 몬테카를로 시뮬레이션으로 매주 최적 번호 조합을 제공합니다. 월 900원부터 구독.',
keywords: [
'로또 번호 추천',
'로또 번호 분석',
'로또 빅데이터',
'로또 통계',
'로또 번호 생성',
'핫넘버 콜드넘버',
],
openGraph: {
title: '로또 번호 추천 서비스 | 쟁승메이드',
description:
'1,100+회차 데이터 분석 · 월 900원 구독 · 이메일/텔레그램 자동 발송.',
url: 'https://jaengseung-made.com/services/lotto',
},
};
export default function LottoLayout({ children }: { children: React.ReactNode }) {
return children;
}

View File

@@ -1,6 +0,0 @@
import { redirect } from 'next/navigation';
// PG 심사 정책상 로또 관련 서비스 비공개 처리
export default function LottoPage() {
redirect('/');
}

View File

@@ -1,184 +0,0 @@
'use client';
import { useState, useEffect } from 'react';
interface PersonalPattern {
total_analyzed: number;
number_frequency: Record<string, number>;
top_picks: number[];
least_picks: number[];
pattern: {
avg_odd_count: number;
avg_sum: number;
avg_range: number;
consecutive_rate: number;
zone_avg: Record<string, number>;
};
vs_draw_avg: {
odd_diff: number;
sum_diff: number;
odd_tendency: string;
sum_tendency: string;
};
}
function getBallStyle(n: number) {
if (n <= 10) return { bg: 'linear-gradient(145deg,#fde68a,#fbbf24,#d97706)', text: '#78350f' };
if (n <= 20) return { bg: 'linear-gradient(145deg,#93c5fd,#3b82f6,#1d4ed8)', text: '#fff' };
if (n <= 30) return { bg: 'linear-gradient(145deg,#fca5a5,#ef4444,#b91c1c)', text: '#fff' };
if (n <= 40) return { bg: 'linear-gradient(145deg,#d1d5db,#9ca3af,#4b5563)', text: '#fff' };
return { bg: 'linear-gradient(145deg,#86efac,#22c55e,#15803d)', text: '#fff' };
}
function SmallBall({ n, size = 30, freq }: { n: number; size?: number; freq?: number }) {
const { bg, text } = getBallStyle(n);
return (
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 3 }}>
<div style={{
width: size, height: size, borderRadius: '50%', background: bg,
display: 'flex', alignItems: 'center', justifyContent: 'center',
fontSize: size * 0.35, fontWeight: 900, color: text, flexShrink: 0,
boxShadow: '0 2px 8px rgba(0,0,0,.3)',
}}>{n}</div>
{freq !== undefined && <div style={{ color: 'rgba(255,255,255,.3)', fontSize: '.55rem', fontFamily: "'JetBrains Mono',monospace" }}>{freq}</div>}
</div>
);
}
function ZoneBar({ label, value, max }: { label: string; value: number; max: number }) {
const pct = max > 0 ? (value / max) * 100 : 0;
return (
<div style={{ display: 'flex', alignItems: 'center', gap: '.6rem' }}>
<div style={{ color: 'rgba(255,255,255,.4)', fontSize: '.65rem', minWidth: 44, fontFamily: "'JetBrains Mono',monospace" }}>{label}</div>
<div style={{ flex: 1, height: 8, background: 'rgba(255,255,255,.06)', borderRadius: 4, overflow: 'hidden' }}>
<div style={{ height: '100%', width: `${pct}%`, background: 'linear-gradient(90deg,#fbbf24,#f97316)', borderRadius: 4, transition: 'width 1s ease' }} />
</div>
<div style={{ color: '#fbbf24', fontSize: '.65rem', minWidth: 24, textAlign: 'right', fontFamily: "'JetBrains Mono',monospace" }}>{value.toFixed(1)}</div>
</div>
);
}
export default function PatternTab() {
const [data, setData] = useState<PersonalPattern | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
useEffect(() => {
fetch('/api/lotto/analysis/personal').then(r => r.json())
.then(d => {
if (d?.error) { setError(d.error === 'NAS_TIMEOUT' ? 'NAS 서버 응답 시간 초과.' : '패턴 분석을 불러오지 못했습니다.'); return; }
setData(d);
})
.catch(() => setError('패턴 분석을 불러오지 못했습니다.'))
.finally(() => setLoading(false));
}, []);
if (loading) return (
<div style={{ textAlign: 'center', padding: '4rem 0' }}>
<div style={{ width: 36, height: 36, borderRadius: '50%', border: '3px solid rgba(251,191,36,.15)', borderTop: '3px solid #fbbf24', animation: 'spin .8s linear infinite', margin: '0 auto 1rem' }} />
</div>
);
if (error) return <div style={{ textAlign: 'center', padding: '4rem 0', color: '#f87171', fontSize: '.85rem' }}>{error}</div>;
if (!data || data.total_analyzed === 0) return (
<div style={{ textAlign: 'center', padding: '5rem 1rem' }}>
<div style={{ fontSize: '3rem', marginBottom: '1rem' }}>📊</div>
<div style={{ color: 'rgba(255,255,255,.4)', fontSize: '.9rem', marginBottom: '.5rem' }}> </div>
<div style={{ color: 'rgba(255,255,255,.2)', fontSize: '.75rem' }}> </div>
</div>
);
const zoneMax = Math.max(...Object.values(data.pattern.zone_avg));
const tendencyColor = (tendency: string) =>
tendency.includes('고') || tendency.includes('홀수') ? '#f87171' : tendency.includes('저') || tendency.includes('짝수') ? '#60a5fa' : '#4ade80';
return (
<div style={{ animation: 'slideUp .4s ease forwards' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem', marginBottom: '1.5rem', flexWrap: 'wrap' }}>
<div>
<div style={{ fontFamily: "'JetBrains Mono',monospace", fontSize: '.6rem', color: 'rgba(251,191,36,.5)', letterSpacing: '.12em', marginBottom: '.3rem' }}>PERSONAL PATTERN ANALYSIS</div>
<h2 style={{ color: '#fff', fontSize: '1.3rem', fontWeight: 900, margin: 0 }}> </h2>
</div>
<div style={{ background: 'rgba(251,191,36,.08)', border: '1px solid rgba(251,191,36,.2)', borderRadius: '.75rem', padding: '.5rem 1rem', marginLeft: 'auto' }}>
<span style={{ color: 'rgba(255,255,255,.4)', fontSize: '.65rem' }}> </span>
<span style={{ color: '#fbbf24', fontWeight: 900, fontFamily: "'JetBrains Mono',monospace" }}>{data.total_analyzed}</span>
<span style={{ color: 'rgba(255,255,255,.4)', fontSize: '.65rem' }}></span>
</div>
</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit,minmax(280px,1fr))', gap: '1rem' }}>
{/* 자주 선택한 번호 */}
<div style={{ background: 'rgba(255,255,255,.03)', border: '1px solid rgba(255,255,255,.07)', borderRadius: '1rem', padding: '1.25rem' }}>
<div style={{ color: '#fbbf24', fontSize: '.72rem', fontWeight: 700, marginBottom: '.75rem' }}> TOP 10</div>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '.5rem' }}>
{data.top_picks.map(n => (
<SmallBall key={n} n={n} size={34} freq={data.number_frequency[String(n)] ?? 0} />
))}
</div>
</div>
{/* 한 번도 안 쓴 번호 */}
<div style={{ background: 'rgba(255,255,255,.03)', border: '1px solid rgba(255,255,255,.07)', borderRadius: '1rem', padding: '1.25rem' }}>
<div style={{ color: '#60a5fa', fontSize: '.72rem', fontWeight: 700, marginBottom: '.75rem' }}>💤 </div>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '.5rem' }}>
{data.least_picks.map(n => <SmallBall key={n} n={n} size={34} />)}
</div>
<div style={{ color: 'rgba(255,255,255,.25)', fontSize: '.65rem', marginTop: '.75rem' }}> </div>
</div>
{/* 패턴 지표 */}
<div style={{ background: 'rgba(255,255,255,.03)', border: '1px solid rgba(255,255,255,.07)', borderRadius: '1rem', padding: '1.25rem' }}>
<div style={{ color: 'rgba(255,255,255,.5)', fontSize: '.72rem', fontWeight: 700, marginBottom: '.75rem' }}>📐 </div>
{[
{ label: '평균 홀수 개수', value: data.pattern.avg_odd_count.toFixed(1) + '개', ref: '역대 평균 3.0개', refColor: 'rgba(255,255,255,.2)' },
{ label: '평균 합계', value: data.pattern.avg_sum.toFixed(0), ref: '역대 평균 138', refColor: 'rgba(255,255,255,.2)' },
{ label: '평균 범위(최대-최소)', value: data.pattern.avg_range.toFixed(1), ref: '', refColor: '' },
{ label: '연속번호 포함률', value: `${(data.pattern.consecutive_rate * 100).toFixed(0)}%`, ref: '', refColor: '' },
].map(({ label, value, ref }) => (
<div key={label} style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-end', padding: '.45rem 0', borderBottom: '1px solid rgba(255,255,255,.05)' }}>
<span style={{ color: 'rgba(255,255,255,.35)', fontSize: '.68rem' }}>{label}</span>
<div style={{ textAlign: 'right' }}>
<div style={{ color: '#fff', fontWeight: 700, fontSize: '.8rem', fontFamily: "'JetBrains Mono',monospace" }}>{value}</div>
{ref && <div style={{ color: 'rgba(255,255,255,.2)', fontSize: '.58rem' }}>{ref}</div>}
</div>
</div>
))}
</div>
{/* 구간별 선택 분포 */}
<div style={{ background: 'rgba(255,255,255,.03)', border: '1px solid rgba(255,255,255,.07)', borderRadius: '1rem', padding: '1.25rem' }}>
<div style={{ color: 'rgba(255,255,255,.5)', fontSize: '.72rem', fontWeight: 700, marginBottom: '.75rem' }}>🎯 </div>
<div style={{ display: 'flex', flexDirection: 'column', gap: '.5rem' }}>
{Object.entries(data.pattern.zone_avg).map(([zone, val]) => (
<ZoneBar key={zone} label={zone} value={val} max={zoneMax} />
))}
</div>
</div>
{/* 역대 당첨과 비교 */}
<div style={{ gridColumn: '1/-1', background: 'rgba(255,255,255,.03)', border: '1px solid rgba(255,255,255,.07)', borderRadius: '1rem', padding: '1.25rem' }}>
<div style={{ color: 'rgba(255,255,255,.5)', fontSize: '.72rem', fontWeight: 700, marginBottom: '1rem' }}> </div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit,minmax(200px,1fr))', gap: '1rem' }}>
<div style={{ background: 'rgba(255,255,255,.03)', borderRadius: '.75rem', padding: '1rem', textAlign: 'center' }}>
<div style={{ color: 'rgba(255,255,255,.3)', fontSize: '.65rem', marginBottom: '.4rem' }}> </div>
<div style={{ color: tendencyColor(data.vs_draw_avg.odd_tendency), fontSize: '1.1rem', fontWeight: 900 }}>{data.vs_draw_avg.odd_tendency}</div>
<div style={{ color: 'rgba(255,255,255,.25)', fontSize: '.65rem', marginTop: '.3rem', fontFamily: "'JetBrains Mono',monospace" }}>
{data.vs_draw_avg.odd_diff > 0 ? '+' : ''}{data.vs_draw_avg.odd_diff.toFixed(1)}
</div>
</div>
<div style={{ background: 'rgba(255,255,255,.03)', borderRadius: '.75rem', padding: '1rem', textAlign: 'center' }}>
<div style={{ color: 'rgba(255,255,255,.3)', fontSize: '.65rem', marginBottom: '.4rem' }}> </div>
<div style={{ color: tendencyColor(data.vs_draw_avg.sum_tendency), fontSize: '1.1rem', fontWeight: 900 }}>{data.vs_draw_avg.sum_tendency}</div>
<div style={{ color: 'rgba(255,255,255,.25)', fontSize: '.65rem', marginTop: '.3rem', fontFamily: "'JetBrains Mono',monospace" }}>
{data.vs_draw_avg.sum_diff > 0 ? '+' : ''}{data.vs_draw_avg.sum_diff.toFixed(1)}
</div>
</div>
</div>
</div>
</div>
</div>
);
}

View File

@@ -1,255 +0,0 @@
'use client';
import { useState, useEffect } from 'react';
interface PurchaseRecord {
id: number;
draw_no: number;
amount: number;
sets: number;
prize: number;
note: string;
created_at: string;
}
interface PurchaseStats {
total_records: number;
total_invested: number;
total_prize: number;
net: number;
return_rate: number;
prize_count: number;
max_prize: number;
}
function StatCard({ label, value, sub, color }: { label: string; value: string; sub?: string; color?: string }) {
return (
<div style={{ background: 'rgba(255,255,255,.03)', border: '1px solid rgba(255,255,255,.07)', borderRadius: '.75rem', padding: '1rem', textAlign: 'center' }}>
<div style={{ color: 'rgba(255,255,255,.35)', fontSize: '.65rem', marginBottom: '.4rem' }}>{label}</div>
<div style={{ color: color ?? '#fff', fontSize: '1.3rem', fontWeight: 900, fontFamily: "'JetBrains Mono',monospace", lineHeight: 1.1 }}>{value}</div>
{sub && <div style={{ color: 'rgba(255,255,255,.25)', fontSize: '.6rem', marginTop: '.2rem' }}>{sub}</div>}
</div>
);
}
export default function PurchaseTab() {
const [records, setRecords] = useState<PurchaseRecord[]>([]);
const [stats, setStats] = useState<PurchaseStats | null>(null);
const [loading, setLoading] = useState(true);
const [editingId, setEditingId] = useState<number | null>(null);
const [editPrize, setEditPrize] = useState('');
const [editNote, setEditNote] = useState('');
const [showAdd, setShowAdd] = useState(false);
const [addForm, setAddForm] = useState({ draw_no: '', amount: '5000', sets: '5', prize: '0', note: '' });
const [saving, setSaving] = useState(false);
const load = async () => {
try {
const [recRes, statRes] = await Promise.all([
fetch('/api/lotto/purchase').then(r => r.json()),
fetch('/api/lotto/purchase/stats').then(r => r.json()),
]);
if (recRes?.error || statRes?.error) throw new Error(recRes?.error ?? statRes?.error);
setRecords(recRes.records ?? []);
setStats(statRes);
} catch { /* 에러 시 빈 상태 유지 */ } finally { setLoading(false); }
};
useEffect(() => { load(); }, []);
const handleAdd = async () => {
if (!addForm.draw_no) return;
setSaving(true);
try {
await fetch('/api/lotto/purchase', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
draw_no: parseInt(addForm.draw_no),
amount: parseInt(addForm.amount),
sets: parseInt(addForm.sets),
prize: parseInt(addForm.prize),
note: addForm.note,
}),
});
setShowAdd(false);
setAddForm({ draw_no: '', amount: '5000', sets: '5', prize: '0', note: '' });
await load();
} finally { setSaving(false); }
};
const handleUpdate = async (id: number) => {
setSaving(true);
try {
await fetch(`/api/lotto/purchase/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ prize: parseInt(editPrize) || 0, note: editNote }),
});
setEditingId(null);
await load();
} finally { setSaving(false); }
};
const handleDelete = async (id: number) => {
if (!confirm('삭제하시겠습니까?')) return;
await fetch(`/api/lotto/purchase/${id}`, { method: 'DELETE' });
await load();
};
const inputStyle: React.CSSProperties = {
background: 'rgba(255,255,255,.06)', border: '1px solid rgba(255,255,255,.12)',
borderRadius: '.4rem', padding: '.35rem .65rem', color: '#fff', fontSize: '.78rem', width: '100%',
outline: 'none',
};
if (loading) return (
<div style={{ textAlign: 'center', padding: '4rem 0' }}>
<div style={{ width: 36, height: 36, borderRadius: '50%', border: '3px solid rgba(251,191,36,.15)', borderTop: '3px solid #fbbf24', animation: 'spin .8s linear infinite', margin: '0 auto 1rem' }} />
</div>
);
return (
<div style={{ animation: 'slideUp .4s ease forwards' }}>
{/* 통계 카드 */}
{stats && (
<div style={{ marginBottom: '1.5rem' }}>
<div style={{ fontFamily: "'JetBrains Mono',monospace", fontSize: '.6rem', color: 'rgba(251,191,36,.5)', letterSpacing: '.12em', marginBottom: '.75rem' }}>INVESTMENT STATS</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit,minmax(120px,1fr))', gap: '.6rem' }}>
<StatCard label="총 구매금액" value={`${(stats.total_invested / 10000).toFixed(1)}만원`} sub={`${stats.total_records}회 구매`} />
<StatCard label="총 당첨금" value={`${(stats.total_prize / 10000).toFixed(1)}만원`} sub={`${stats.prize_count}건 당첨`} color="#4ade80" />
<StatCard
label="순손익"
value={`${stats.net >= 0 ? '+' : ''}${(stats.net / 10000).toFixed(1)}만원`}
sub={`회수율 ${stats.return_rate.toFixed(1)}%`}
color={stats.net >= 0 ? '#4ade80' : '#f87171'}
/>
<StatCard label="최대 당첨금" value={stats.max_prize > 0 ? `${stats.max_prize.toLocaleString()}` : '-'} color="#fbbf24" />
</div>
</div>
)}
{/* 구매 기록 테이블 */}
<div style={{ background: 'rgba(255,255,255,.02)', border: '1px solid rgba(255,255,255,.07)', borderRadius: '1rem', overflow: 'hidden' }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '1rem 1.25rem', borderBottom: '1px solid rgba(255,255,255,.06)' }}>
<div style={{ fontFamily: "'JetBrains Mono',monospace", fontSize: '.65rem', color: 'rgba(255,255,255,.4)', letterSpacing: '.1em' }}>PURCHASE HISTORY</div>
<button
onClick={() => setShowAdd(!showAdd)}
style={{
background: 'rgba(251,191,36,.1)', border: '1px solid rgba(251,191,36,.25)',
color: '#fbbf24', borderRadius: '.5rem', padding: '.3rem .75rem', fontSize: '.72rem', cursor: 'pointer', fontWeight: 700,
}}>
+
</button>
</div>
{/* 추가 폼 */}
{showAdd && (
<div style={{ padding: '1rem 1.25rem', borderBottom: '1px solid rgba(255,255,255,.06)', background: 'rgba(251,191,36,.04)' }}>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit,minmax(100px,1fr))', gap: '.5rem', marginBottom: '.75rem' }}>
<div>
<div style={{ color: 'rgba(255,255,255,.3)', fontSize: '.62rem', marginBottom: '.25rem' }}> *</div>
<input style={inputStyle} placeholder="1181" value={addForm.draw_no} onChange={e => setAddForm(p => ({ ...p, draw_no: e.target.value }))} />
</div>
<div>
<div style={{ color: 'rgba(255,255,255,.3)', fontSize: '.62rem', marginBottom: '.25rem' }}></div>
<input style={inputStyle} placeholder="5000" value={addForm.amount} onChange={e => setAddForm(p => ({ ...p, amount: e.target.value }))} />
</div>
<div>
<div style={{ color: 'rgba(255,255,255,.3)', fontSize: '.62rem', marginBottom: '.25rem' }}></div>
<input style={inputStyle} placeholder="5" value={addForm.sets} onChange={e => setAddForm(p => ({ ...p, sets: e.target.value }))} />
</div>
<div>
<div style={{ color: 'rgba(255,255,255,.3)', fontSize: '.62rem', marginBottom: '.25rem' }}></div>
<input style={inputStyle} placeholder="0" value={addForm.prize} onChange={e => setAddForm(p => ({ ...p, prize: e.target.value }))} />
</div>
<div>
<div style={{ color: 'rgba(255,255,255,.3)', fontSize: '.62rem', marginBottom: '.25rem' }}></div>
<input style={inputStyle} placeholder="5등 1개" value={addForm.note} onChange={e => setAddForm(p => ({ ...p, note: e.target.value }))} />
</div>
</div>
<div style={{ display: 'flex', gap: '.5rem' }}>
<button onClick={handleAdd} disabled={saving || !addForm.draw_no}
style={{ background: '#fbbf24', color: '#020c1e', border: 'none', borderRadius: '.5rem', padding: '.4rem 1rem', fontSize: '.75rem', fontWeight: 700, cursor: saving ? 'not-allowed' : 'pointer' }}>
{saving ? '저장 중...' : '저장'}
</button>
<button onClick={() => setShowAdd(false)}
style={{ background: 'rgba(255,255,255,.06)', color: 'rgba(255,255,255,.4)', border: '1px solid rgba(255,255,255,.1)', borderRadius: '.5rem', padding: '.4rem 1rem', fontSize: '.75rem', cursor: 'pointer' }}>
</button>
</div>
</div>
)}
{/* 레코드 목록 */}
{records.length === 0 ? (
<div style={{ padding: '3rem', textAlign: 'center', color: 'rgba(255,255,255,.2)', fontSize: '.8rem' }}>
. .
</div>
) : (
<div style={{ overflowX: 'auto' }}>
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: '.75rem' }}>
<thead>
<tr style={{ borderBottom: '1px solid rgba(255,255,255,.06)' }}>
{['회차', '구매금액', '세트', '당첨금', '손익', '메모', ''].map(h => (
<th key={h} style={{ padding: '.6rem 1rem', color: 'rgba(255,255,255,.3)', fontWeight: 600, textAlign: 'left', whiteSpace: 'nowrap' }}>{h}</th>
))}
</tr>
</thead>
<tbody>
{records.map(rec => {
const net = rec.prize - rec.amount;
const isEditing = editingId === rec.id;
return (
<tr key={rec.id} style={{ borderBottom: '1px solid rgba(255,255,255,.04)', transition: 'background .15s' }}>
<td style={{ padding: '.7rem 1rem', color: '#fbbf24', fontWeight: 700, fontFamily: "'JetBrains Mono',monospace" }}>{rec.draw_no}</td>
<td style={{ padding: '.7rem 1rem', color: 'rgba(255,255,255,.6)' }}>{rec.amount.toLocaleString()}</td>
<td style={{ padding: '.7rem 1rem', color: 'rgba(255,255,255,.4)' }}>{rec.sets}</td>
<td style={{ padding: '.7rem 1rem' }}>
{isEditing ? (
<input style={{ ...inputStyle, width: 80 }} value={editPrize} onChange={e => setEditPrize(e.target.value)} />
) : (
<span style={{ color: rec.prize > 0 ? '#4ade80' : 'rgba(255,255,255,.3)' }}>
{rec.prize > 0 ? `${rec.prize.toLocaleString()}` : '-'}
</span>
)}
</td>
<td style={{ padding: '.7rem 1rem', color: net > 0 ? '#4ade80' : net < 0 ? '#f87171' : 'rgba(255,255,255,.3)', fontFamily: "'JetBrains Mono',monospace", fontWeight: 700 }}>
{net > 0 ? '+' : ''}{net.toLocaleString()}
</td>
<td style={{ padding: '.7rem 1rem' }}>
{isEditing ? (
<input style={{ ...inputStyle, width: 100 }} value={editNote} onChange={e => setEditNote(e.target.value)} />
) : (
<span style={{ color: 'rgba(255,255,255,.4)' }}>{rec.note || '-'}</span>
)}
</td>
<td style={{ padding: '.7rem 1rem', whiteSpace: 'nowrap' }}>
{isEditing ? (
<div style={{ display: 'flex', gap: '.3rem' }}>
<button onClick={() => handleUpdate(rec.id)} disabled={saving}
style={{ background: '#4ade80', color: '#020c1e', border: 'none', borderRadius: '.35rem', padding: '.25rem .6rem', fontSize: '.65rem', fontWeight: 700, cursor: 'pointer' }}></button>
<button onClick={() => setEditingId(null)}
style={{ background: 'rgba(255,255,255,.08)', color: 'rgba(255,255,255,.4)', border: 'none', borderRadius: '.35rem', padding: '.25rem .6rem', fontSize: '.65rem', cursor: 'pointer' }}></button>
</div>
) : (
<div style={{ display: 'flex', gap: '.3rem' }}>
<button onClick={() => { setEditingId(rec.id); setEditPrize(String(rec.prize)); setEditNote(rec.note); }}
style={{ background: 'rgba(251,191,36,.1)', color: '#fbbf24', border: '1px solid rgba(251,191,36,.2)', borderRadius: '.35rem', padding: '.25rem .6rem', fontSize: '.65rem', cursor: 'pointer' }}></button>
<button onClick={() => handleDelete(rec.id)}
style={{ background: 'rgba(239,68,68,.1)', color: '#f87171', border: '1px solid rgba(239,68,68,.2)', borderRadius: '.35rem', padding: '.25rem .6rem', fontSize: '.65rem', cursor: 'pointer' }}></button>
</div>
)}
</td>
</tr>
);
})}
</tbody>
</table>
</div>
)}
</div>
</div>
);
}

View File

@@ -1,242 +0,0 @@
'use client';
import { useState, useEffect } from 'react';
// ─── Types ───────────────────────────────────────────────────────────────────
interface ReportData {
target_drw_no: number;
based_on_draw: number;
generated_at: string;
hot_numbers: number[];
cold_numbers: number[];
overdue_numbers: number[];
recent_pattern: {
last3_numbers: number[];
triple_appear: number[];
recent_sum_avg: number;
recent_odd_avg: number;
};
recommended_sets: Array<{
strategy: string;
numbers: number[];
description: string;
}>;
confidence_score: number;
confidence_factors: {
data_volume: number;
pattern_consistency: number;
recent_trend: number;
};
}
interface HistoryItem { drw_no: number; generated_at: string; }
function getBallStyle(n: number) {
if (n <= 10) return { bg: 'linear-gradient(145deg,#fde68a,#fbbf24,#d97706)', text: '#78350f' };
if (n <= 20) return { bg: 'linear-gradient(145deg,#93c5fd,#3b82f6,#1d4ed8)', text: '#fff' };
if (n <= 30) return { bg: 'linear-gradient(145deg,#fca5a5,#ef4444,#b91c1c)', text: '#fff' };
if (n <= 40) return { bg: 'linear-gradient(145deg,#d1d5db,#9ca3af,#4b5563)', text: '#fff' };
return { bg: 'linear-gradient(145deg,#86efac,#22c55e,#15803d)', text: '#fff' };
}
function SmallBall({ n, size = 32 }: { n: number; size?: number }) {
const { bg, text } = getBallStyle(n);
return (
<div style={{
width: size, height: size, borderRadius: '50%', background: bg,
display: 'flex', alignItems: 'center', justifyContent: 'center',
fontSize: size * 0.35, fontWeight: 900, color: text, flexShrink: 0,
boxShadow: '0 2px 8px rgba(0,0,0,.3)',
}}>{n}</div>
);
}
function ConfidenceBar({ label, value }: { label: string; value: number }) {
const color = value >= 85 ? '#4ade80' : value >= 70 ? '#fbbf24' : '#f87171';
return (
<div style={{ marginBottom: '.6rem' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '.25rem' }}>
<span style={{ color: 'rgba(255,255,255,.4)', fontSize: '.7rem' }}>{label}</span>
<span style={{ color, fontSize: '.7rem', fontWeight: 700, fontFamily: "'JetBrains Mono',monospace" }}>{value}</span>
</div>
<div style={{ height: 5, background: 'rgba(255,255,255,.07)', borderRadius: 3, overflow: 'hidden' }}>
<div style={{ height: '100%', width: `${value}%`, background: color, borderRadius: 3, transition: 'width 1s ease' }} />
</div>
</div>
);
}
export default function ReportTab() {
const [report, setReport] = useState<ReportData | null>(null);
const [history, setHistory] = useState<HistoryItem[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
const [copiedIdx, setCopiedIdx] = useState<number | null>(null);
useEffect(() => {
Promise.all([
fetch('/api/lotto/report/latest').then(r => r.json()),
fetch('/api/lotto/report/history?limit=10').then(r => r.json()),
]).then(([rep, hist]) => {
if (rep?.error) {
setError(rep.error === 'NAS_TIMEOUT'
? 'NAS 서버 응답 시간 초과. 잠시 후 다시 시도해주세요.'
: '리포트를 불러오지 못했습니다. (' + rep.error + ')');
return;
}
setReport(rep);
setHistory(hist?.reports ?? []);
}).catch(() => setError('리포트를 불러오지 못했습니다.'))
.finally(() => setLoading(false));
}, []);
const copyNumbers = (numbers: number[], idx: number) => {
navigator.clipboard.writeText(numbers.join(', '));
setCopiedIdx(idx);
setTimeout(() => setCopiedIdx(null), 1500);
};
if (loading) return (
<div style={{ textAlign: 'center', padding: '4rem 0' }}>
<div style={{ width: 36, height: 36, borderRadius: '50%', border: '3px solid rgba(251,191,36,.15)', borderTop: '3px solid #fbbf24', animation: 'spin .8s linear infinite', margin: '0 auto 1rem' }} />
<div style={{ color: 'rgba(255,255,255,.3)', fontSize: '.75rem' }}> ...</div>
</div>
);
if (error) return (
<div style={{ textAlign: 'center', padding: '4rem 0', color: '#f87171', fontSize: '.85rem' }}>{error}</div>
);
if (!report || !report.confidence_factors || !report.recommended_sets) return null;
const strategyColors = ['#fbbf24', '#60a5fa', '#a78bfa'];
return (
<div style={{ animation: 'slideUp .4s ease forwards' }}>
{/* 헤더 */}
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '1.5rem', flexWrap: 'wrap', gap: '.75rem' }}>
<div>
<div style={{ fontFamily: "'JetBrains Mono',monospace", fontSize: '.6rem', color: 'rgba(251,191,36,.5)', letterSpacing: '.15em', marginBottom: '.3rem' }}>WEEKLY ATTACK REPORT</div>
<h2 style={{ color: '#fff', fontSize: '1.4rem', fontWeight: 900, margin: 0 }}>
{report.target_drw_no}
</h2>
<div style={{ color: 'rgba(255,255,255,.3)', fontSize: '.72rem', marginTop: '.2rem' }}>
{report.based_on_draw} · {new Date(report.generated_at).toLocaleDateString('ko-KR')}
</div>
</div>
{/* 신뢰도 점수 */}
<div style={{
background: 'rgba(251,191,36,.08)', border: '1px solid rgba(251,191,36,.2)',
borderRadius: '1rem', padding: '.75rem 1.25rem', textAlign: 'center',
}}>
<div style={{ color: 'rgba(251,191,36,.5)', fontSize: '.6rem', fontFamily: "'JetBrains Mono',monospace", letterSpacing: '.1em' }}>CONFIDENCE</div>
<div style={{ color: '#fbbf24', fontSize: '2rem', fontWeight: 900, lineHeight: 1.1, fontFamily: "'JetBrains Mono',monospace" }}>{report.confidence_score}</div>
<div style={{ color: 'rgba(255,255,255,.3)', fontSize: '.6rem' }}>/100</div>
</div>
</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit,minmax(280px,1fr))', gap: '1rem', marginBottom: '1.5rem' }}>
{/* 추천 번호 세트 */}
<div style={{ gridColumn: '1/-1', background: 'rgba(255,255,255,.03)', border: '1px solid rgba(255,255,255,.07)', borderRadius: '1rem', padding: '1.25rem' }}>
<div style={{ fontFamily: "'JetBrains Mono',monospace", fontSize: '.6rem', color: 'rgba(251,191,36,.5)', letterSpacing: '.12em', marginBottom: '1rem' }}>RECOMMENDED SETS</div>
<div style={{ display: 'grid', gap: '.75rem' }}>
{report.recommended_sets.map((set, i) => (
<div key={i} style={{
background: `rgba(${i === 0 ? '251,191,36' : i === 1 ? '96,165,250' : '167,139,250'},.05)`,
border: `1px solid rgba(${i === 0 ? '251,191,36' : i === 1 ? '96,165,250' : '167,139,250'},.15)`,
borderRadius: '.75rem', padding: '1rem',
}}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '.6rem', flexWrap: 'wrap', gap: '.5rem' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '.5rem' }}>
<div style={{ width: 6, height: 6, borderRadius: '50%', background: strategyColors[i] }} />
<span style={{ color: strategyColors[i], fontSize: '.72rem', fontWeight: 700 }}>{set.strategy}</span>
</div>
<button
onClick={() => copyNumbers(set.numbers, i)}
style={{
background: 'rgba(255,255,255,.06)', border: '1px solid rgba(255,255,255,.1)',
color: copiedIdx === i ? '#4ade80' : 'rgba(255,255,255,.4)',
borderRadius: '.4rem', padding: '.2rem .6rem', fontSize: '.65rem', cursor: 'pointer',
}}>
{copiedIdx === i ? '✓ 복사됨' : '복사'}
</button>
</div>
<div style={{ display: 'flex', gap: '.4rem', flexWrap: 'wrap', marginBottom: '.5rem' }}>
{set.numbers.map(n => <SmallBall key={n} n={n} size={36} />)}
</div>
<div style={{ color: 'rgba(255,255,255,.3)', fontSize: '.68rem' }}>{set.description}</div>
</div>
))}
</div>
</div>
{/* 핫/콜드/미출현 */}
{[
{ label: '🔥 최근 과출현', numbers: report.hot_numbers, color: '#f87171', desc: '최근 10회 2회 이상 출현' },
{ label: '❄️ 저빈도 번호', numbers: report.cold_numbers, color: '#60a5fa', desc: '역대 출현 빈도 하위' },
{ label: '⏳ 장기 미출현', numbers: report.overdue_numbers, color: '#a78bfa', desc: '가장 오래 미출현 번호' },
].map(({ label, numbers, color, desc }) => (
<div key={label} style={{ background: 'rgba(255,255,255,.03)', border: '1px solid rgba(255,255,255,.07)', borderRadius: '1rem', padding: '1rem' }}>
<div style={{ color, fontSize: '.75rem', fontWeight: 700, marginBottom: '.3rem' }}>{label}</div>
<div style={{ color: 'rgba(255,255,255,.3)', fontSize: '.65rem', marginBottom: '.75rem' }}>{desc}</div>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '.35rem' }}>
{numbers.map(n => <SmallBall key={n} n={n} size={30} />)}
</div>
</div>
))}
{/* 최근 패턴 */}
<div style={{ background: 'rgba(255,255,255,.03)', border: '1px solid rgba(255,255,255,.07)', borderRadius: '1rem', padding: '1rem' }}>
<div style={{ color: 'rgba(255,255,255,.5)', fontSize: '.72rem', fontWeight: 700, marginBottom: '.75rem' }}>📊 </div>
{[
{ label: '최근 10회 합계 평균', value: report.recent_pattern.recent_sum_avg.toFixed(1) },
{ label: '최근 10회 홀수 평균', value: report.recent_pattern.recent_odd_avg.toFixed(1) + '개' },
].map(({ label, value }) => (
<div key={label} style={{ display: 'flex', justifyContent: 'space-between', padding: '.4rem 0', borderBottom: '1px solid rgba(255,255,255,.05)' }}>
<span style={{ color: 'rgba(255,255,255,.3)', fontSize: '.7rem' }}>{label}</span>
<span style={{ color: '#fbbf24', fontSize: '.7rem', fontWeight: 700, fontFamily: "'JetBrains Mono',monospace" }}>{value}</span>
</div>
))}
{report.recent_pattern.triple_appear.length > 0 && (
<div style={{ marginTop: '.75rem' }}>
<div style={{ color: 'rgba(255,255,255,.3)', fontSize: '.65rem', marginBottom: '.4rem' }}> 3 </div>
<div style={{ display: 'flex', gap: '.3rem' }}>
{report.recent_pattern.triple_appear.map(n => <SmallBall key={n} n={n} size={28} />)}
</div>
</div>
)}
</div>
{/* 신뢰도 상세 */}
<div style={{ background: 'rgba(255,255,255,.03)', border: '1px solid rgba(255,255,255,.07)', borderRadius: '1rem', padding: '1rem' }}>
<div style={{ color: 'rgba(255,255,255,.5)', fontSize: '.72rem', fontWeight: 700, marginBottom: '.75rem' }}>🎯 </div>
<ConfidenceBar label="데이터 충분도" value={report.confidence_factors.data_volume} />
<ConfidenceBar label="패턴 안정성" value={report.confidence_factors.pattern_consistency} />
<ConfidenceBar label="최근 트렌드" value={report.confidence_factors.recent_trend} />
</div>
</div>
{/* 이전 리포트 목록 */}
{history.length > 0 && (
<div style={{ background: 'rgba(255,255,255,.02)', border: '1px solid rgba(255,255,255,.05)', borderRadius: '1rem', padding: '1rem' }}>
<div style={{ color: 'rgba(255,255,255,.4)', fontSize: '.7rem', fontWeight: 700, marginBottom: '.75rem', fontFamily: "'JetBrains Mono',monospace", letterSpacing: '.08em' }}>REPORT HISTORY</div>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '.4rem' }}>
{history.map(h => (
<button key={h.drw_no}
style={{
background: h.drw_no === report.target_drw_no ? 'rgba(251,191,36,.15)' : 'rgba(255,255,255,.05)',
border: `1px solid ${h.drw_no === report.target_drw_no ? 'rgba(251,191,36,.4)' : 'rgba(255,255,255,.1)'}`,
color: h.drw_no === report.target_drw_no ? '#fbbf24' : 'rgba(255,255,255,.4)',
borderRadius: '.5rem', padding: '.3rem .65rem', fontSize: '.68rem', cursor: 'pointer',
}}>
{h.drw_no}
</button>
))}
</div>
</div>
)}
</div>
);
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,28 @@
import type { Metadata } from 'next';
export const metadata: Metadata = {
title: 'AI 음악 마스터 구조 팩 | Suno · MV · 유튜브 쇼츠',
description:
'7년차 개발자가 설계한 4단계 AI 음악 제작 공정. Suno 프롬프트 조합법 + MV 비디오 생성 워크플로우 + 저작권 가이드 + 템플릿 PDF + 샘플 프로젝트. 입문 ₩39k / 프로 ₩99k / 마스터 ₩149k.',
keywords: [
'AI 음악 만들기',
'Suno 프롬프트',
'AI 뮤직비디오',
'AI 커버곡',
'유튜브 쇼츠 음악',
'AI 작곡',
'크리에이터 이코노미',
'Lyria 프롬프트',
'Runway AI 비디오',
],
openGraph: {
title: 'AI 음악 마스터 구조 팩 | 쟁승메이드',
description:
'네 사연을 노래로. 쇼츠까지 한 번에. 4단계 AI 음악 공정 · Suno Pro 검증 · 평생 업데이트.',
url: 'https://jaengseung-made.com/services/music',
},
};
export default function MusicLayout({ children }: { children: React.ReactNode }) {
return children;
}

483
app/services/music/page.tsx Normal file
View File

@@ -0,0 +1,483 @@
'use client';
import { useState } from 'react';
import Link from 'next/link';
import PurchaseAgreementModal from '../../components/PurchaseAgreementModal';
type Tier = 'starter' | 'pro' | 'master';
const TIERS: Record<Tier, { name: string; price: string; priceNum: string; desc: string; features: string[]; highlight?: boolean }> = {
starter: {
name: '입문',
price: '₩39,000',
priceNum: '39,000',
desc: '첫 AI 음악을 위한 필수 구성',
features: [
'Suno 프롬프트 조합법 20종',
'기본 가사 최적화 템플릿',
'구조 템플릿 PDF 40p',
'저작권 가이드 기본판',
'12개월 무료 업데이트',
],
},
pro: {
name: '프로',
price: '₩99,000',
priceNum: '99,000',
desc: '쇼츠 업로드까지 완성하는 풀세트',
highlight: true,
features: [
'입문 전체 포함',
'고급 편집법 (Stems 분리 · 마스터링 프롬프트)',
'MV 비디오 생성 워크플로우 (Runway/Luma/Pika)',
'샘플 프로젝트 1개 (.prj · 영상 포함)',
'이메일/페이지 1:1 Q&A 1회 (30일 이내)',
'유튜브 SEO 템플릿',
],
},
master: {
name: '마스터',
price: '₩149,000',
priceNum: '149,000',
desc: '여러 장르·포맷을 커버하는 마스터피스',
features: [
'프로 전체 포함',
'샘플 프로젝트 다수 (장르별 3종)',
'우선 업데이트 · 베타 기능 선공개',
'저작권 가이드 심화판 + 상업 이용 체크리스트',
'제작 레시피 영상 가이드',
],
},
};
const PROCESS = [
{
num: '01',
title: '크리에이티브 디렉팅',
subtitle: 'Concept & Lyrics',
customer: '원하는 키워드 3개 또는 사연 제공',
value: 'ChatGPT·Claude로 Suno가 이해하는 가사·스타일 태그로 변환',
result: 'AI 최적화 가사 · 메타데이터 시트',
color: 'from-violet-500 to-fuchsia-500',
},
{
num: '02',
title: '오디오 엔지니어링',
subtitle: 'Music Generation',
customer: '결과물 확인 · 방향 피드백',
value: 'Suno Custom Mode로 가사 배치·파트·보컬·악기 세밀 조정. 가장 높은 퀄리티가 나올 때까지 프롬프트 깎기',
result: '고품질 완곡 (Full Track, 스템 분리본)',
color: 'from-fuchsia-500 to-pink-500',
},
{
num: '03',
title: '비주얼 마스터링',
subtitle: 'AI MV Generation',
customer: '-',
value: 'Midjourney · Runway · Luma로 음악 분위기에 맞는 이미지·영상 생성. 비트와 가사에 맞춘 싱크 설계',
result: '쇼츠(9:16) 또는 유튜브(16:9) 고화질 영상',
color: 'from-sky-500 to-cyan-500',
},
{
num: '04',
title: '퍼블리싱 가이드',
subtitle: 'Viral Optimization',
customer: '유튜브 업로드',
value: '제목·해시태그·설명란(SEO) AI 최적화 템플릿 제공',
result: '즉시 업로드 가능한 유튜브 배포 패키지',
color: 'from-cyan-500 to-emerald-500',
},
];
const FAQS = [
{
q: 'Suno 유료 플랜 가입이 꼭 필요한가요?',
a: 'Suno 무료 플랜은 상업적 이용이 제한됩니다. 본인 결과물을 유튜브·SNS에 업로드해 수익화하려면 Suno Pro 이상 권장. 팩 구매 후 가입 전 플랜 선택 가이드가 포함됩니다.',
},
{
q: '제가 만든 결과물의 상업 이용·저작권은?',
a: '결과물의 상업권은 고객이 가입한 AI 서비스의 이용약관을 따릅니다. 팩에는 Suno·Runway·Luma 각 서비스의 최신 약관 요약과 상업 이용 체크리스트가 포함되어 있습니다. (법률 자문이 아닌 참고용 가이드입니다.)',
},
{
q: '결과물 품질을 보장하나요?',
a: 'AI 생성물은 모델 버전·프롬프트 입력에 따라 달라지므로 결과물 자체를 보장하지 않습니다. 다만 팩은 동일 프롬프트로 반복 가능한 고품질 구간을 설계하는 방법을 제공합니다. 샘플 쇼츠·프로젝트로 품질 기대치를 사전 확인하세요.',
},
{
q: '환불이 가능한가요?',
a: '전자상거래법 제17조 제2항 제5호에 따라 디지털 콘텐츠는 제공 시작 후 청약철회가 제한됩니다. 무료 샘플로 사전 확인을 제공하므로 충분히 검토 후 구매해주세요. 파일 손상·전달 불량 등 회사 귀책은 즉시 재전달 또는 환불됩니다.',
},
{
q: '업데이트는 어떻게 받나요?',
a: '구매자 전용 Notion 페이지에서 변경 이력과 최신 파일을 제공. 12개월간 무료 업데이트가 기본, 마스터는 우선 업데이트·베타 선공개가 포함됩니다.',
},
];
export default function MusicServicePage() {
const [selectedTier, setSelectedTier] = useState<Tier | null>(null);
const [openFaq, setOpenFaq] = useState<number | null>(0);
return (
<div className="min-h-full bg-slate-950 text-white">
{/* HERO */}
<section
className="relative overflow-hidden px-6 py-24 lg:px-14 lg:py-32"
style={{
background:
'radial-gradient(circle at 25% 20%, #2e1065 0%, #020617 55%), radial-gradient(circle at 80% 80%, #164e63 0%, transparent 50%)',
}}
>
<div
className="absolute inset-0 opacity-[0.05] pointer-events-none mix-blend-overlay"
style={{
backgroundImage:
"url(\"data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='120' height='120'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.9'/></filter><rect width='100%' height='100%' filter='url(%23n)' opacity='0.5'/></svg>\")",
}}
/>
<div className="relative max-w-5xl mx-auto">
<div className="flex items-center gap-3 mb-8">
<span className="inline-flex h-2 w-2 rounded-full bg-violet-400 animate-pulse" />
<span className="font-mono text-xs text-violet-300/80 tracking-[0.25em] uppercase">
AI Music Pack · v1
</span>
</div>
<h1
className="text-[2.8rem] md:text-[4rem] lg:text-[5.5rem] font-extrabold leading-[1.02] tracking-tight mb-6"
style={{ wordBreak: 'keep-all' }}
>
<span className="text-white"> .</span>
<br />
<span className="bg-gradient-to-r from-violet-300 via-pink-200 to-cyan-300 bg-clip-text text-transparent">
.
</span>
</h1>
<p
className="text-slate-300 text-lg md:text-xl leading-relaxed mb-3 max-w-2xl"
style={{ wordBreak: 'keep-all' }}
>
AI로 , <span className="text-white font-semibold"> </span> .
</p>
<p className="text-slate-400 text-base mb-10 max-w-2xl">
7 <span className="text-white">4 AI </span> · Suno Pro .
</p>
<div className="flex flex-wrap gap-3 mb-10">
<a
href="#pricing"
className="inline-flex items-center gap-2 bg-violet-600 hover:bg-violet-500 text-white px-8 py-4 rounded-xl font-bold text-sm transition-colors shadow-[0_0_40px_rgba(139,92,246,0.45)]"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M17 8l4 4m0 0l-4 4m4-4H3" />
</svg>
</a>
<a
href="#samples"
className="inline-flex items-center gap-2 border border-white/20 hover:border-white/50 text-white/90 hover:text-white px-8 py-4 rounded-xl font-semibold text-sm transition-all"
>
</a>
</div>
<div className="flex flex-wrap gap-5 text-xs text-slate-400">
<span className="flex items-center gap-1.5"> </span>
<span className="flex items-center gap-1.5"> </span>
<span className="flex items-center gap-1.5"> Suno Pro </span>
</div>
</div>
{/* Bottom waveform */}
<div className="absolute bottom-0 left-0 right-0 h-32 opacity-40 pointer-events-none">
<svg viewBox="0 0 1200 120" preserveAspectRatio="none" className="w-full h-full">
<path
d="M0,60 Q150,10 300,60 T600,60 T900,60 T1200,60 L1200,120 L0,120 Z"
fill="url(#wg)"
/>
<defs>
<linearGradient id="wg" x1="0%" x2="100%">
<stop offset="0%" stopColor="#a78bfa" stopOpacity="0.5" />
<stop offset="50%" stopColor="#22d3ee" stopOpacity="0.3" />
<stop offset="100%" stopColor="#a78bfa" stopOpacity="0.5" />
</linearGradient>
</defs>
</svg>
</div>
</section>
{/* BEFORE / AFTER */}
<section className="px-6 py-20 lg:px-14 bg-slate-950 border-t border-white/5">
<div className="max-w-5xl mx-auto">
<p className="font-mono text-xs text-violet-300/70 tracking-widest uppercase mb-2 text-center">
Before vs After
</p>
<h2 className="text-3xl md:text-4xl font-extrabold text-center mb-12">
AI , ?
</h2>
<div className="grid md:grid-cols-2 gap-5">
<div className="border border-rose-500/20 bg-rose-500/5 rounded-2xl p-8">
<div className="text-4xl mb-3">😵</div>
<h3 className="font-extrabold text-rose-300 mb-3 text-lg">Before · </h3>
<ul className="space-y-2 text-sm text-slate-300 leading-relaxed">
<li> Suno 10 </li>
<li> </li>
<li> </li>
<li> 0</li>
</ul>
</div>
<div className="border border-emerald-500/30 bg-emerald-500/5 rounded-2xl p-8 shadow-[0_0_40px_rgba(16,185,129,0.15)]">
<div className="text-4xl mb-3">🎯</div>
<h3 className="font-extrabold text-emerald-300 mb-3 text-lg">After · </h3>
<ul className="space-y-2 text-sm text-slate-200 leading-relaxed">
<li> 1 </li>
<li> 30 </li>
<li> · </li>
<li> SEO 릿 </li>
</ul>
</div>
</div>
</div>
</section>
{/* PROCESS — 4 STEPS */}
<section className="px-6 py-24 lg:px-14 bg-gradient-to-b from-slate-950 to-[#0b0530]">
<div className="max-w-5xl mx-auto">
<p className="font-mono text-xs text-violet-300/70 tracking-widest uppercase mb-2">
Process Architecture
</p>
<h2 className="text-3xl md:text-4xl font-extrabold mb-4" style={{ wordBreak: 'keep-all' }}>
,
</h2>
<p className="text-slate-400 text-lg mb-16"> 4 .</p>
<div className="space-y-6">
{PROCESS.map((step) => (
<div
key={step.num}
className="group relative border border-white/10 hover:border-violet-400/50 rounded-3xl p-8 md:p-10 bg-white/[0.02] backdrop-blur transition-all"
>
<div className="flex flex-col md:flex-row md:items-start gap-6">
<div className="flex-shrink-0">
<div
className={`inline-flex items-center justify-center w-20 h-20 rounded-2xl bg-gradient-to-br ${step.color} font-extrabold text-2xl text-white shadow-lg`}
>
{step.num}
</div>
</div>
<div className="flex-1">
<p className="font-mono text-[11px] text-violet-300/60 uppercase tracking-widest mb-1">
{step.subtitle}
</p>
<h3 className="text-2xl md:text-3xl font-extrabold text-white mb-4">
{step.title}
</h3>
<dl className="grid sm:grid-cols-3 gap-4 text-sm">
<div>
<dt className="text-slate-500 text-xs uppercase font-bold mb-1"> </dt>
<dd className="text-slate-300 leading-relaxed">{step.customer}</dd>
</div>
<div>
<dt className="text-slate-500 text-xs uppercase font-bold mb-1"> </dt>
<dd className="text-slate-300 leading-relaxed">{step.value}</dd>
</div>
<div>
<dt className="text-slate-500 text-xs uppercase font-bold mb-1"></dt>
<dd className="text-white font-semibold leading-relaxed">{step.result}</dd>
</div>
</dl>
</div>
</div>
</div>
))}
</div>
</div>
</section>
{/* SAMPLES */}
<section id="samples" className="px-6 py-20 lg:px-14 bg-[#0b0530]">
<div className="max-w-5xl mx-auto">
<p className="font-mono text-xs text-violet-300/70 tracking-widest uppercase mb-2">
Sample Showcase
</p>
<h2 className="text-3xl md:text-4xl font-extrabold mb-3">
, .
</h2>
<p className="text-slate-400 mb-10">30 .</p>
<div className="grid sm:grid-cols-3 gap-4">
{[1, 2, 3].map((i) => (
<div
key={i}
className="aspect-[9/16] rounded-2xl border border-white/10 bg-gradient-to-br from-violet-900/40 to-slate-900 flex items-center justify-center relative overflow-hidden group cursor-pointer"
>
<div className="absolute inset-0 bg-gradient-to-br from-violet-500/10 to-cyan-500/10 group-hover:from-violet-500/20 group-hover:to-cyan-500/20 transition-all" />
<div className="relative text-center p-6">
<div className="text-5xl mb-3">🎬</div>
<p className="text-xs text-slate-400 font-mono">Sample {i}</p>
<p className="text-sm text-slate-200 mt-1"> </p>
</div>
</div>
))}
</div>
<p className="text-xs text-slate-500 mt-6 text-center">
. .
</p>
</div>
</section>
{/* PRICING */}
<section id="pricing" className="px-6 py-24 lg:px-14 bg-slate-950">
<div className="max-w-6xl mx-auto">
<p className="font-mono text-xs text-violet-300/70 tracking-widest uppercase mb-2 text-center">
Pricing
</p>
<h2 className="text-3xl md:text-4xl font-extrabold text-center mb-4">
3 , .
</h2>
<p className="text-center text-slate-400 mb-14"> .</p>
<div className="grid md:grid-cols-3 gap-5 items-stretch">
{(Object.keys(TIERS) as Tier[]).map((key) => {
const t = TIERS[key];
return (
<div
key={key}
className={`relative rounded-3xl p-8 flex flex-col border transition-all ${
t.highlight
? 'border-violet-400 bg-gradient-to-br from-violet-900/40 to-slate-900 shadow-[0_0_60px_rgba(139,92,246,0.35)] md:scale-[1.03] md:-translate-y-2'
: 'border-white/10 bg-white/[0.02] hover:border-white/30'
}`}
>
{t.highlight && (
<div className="absolute -top-3 left-1/2 -translate-x-1/2">
<span className="inline-flex items-center gap-1 bg-gradient-to-r from-violet-500 to-pink-500 text-white text-[10px] font-extrabold px-3 py-1.5 rounded-full uppercase tracking-wider">
🔥 80%
</span>
</div>
)}
<h3 className="font-extrabold text-2xl mb-1">{t.name}</h3>
<p className="text-sm text-slate-400 mb-6">{t.desc}</p>
<div className="mb-6">
<span className="text-4xl font-extrabold font-mono">{t.price}</span>
<span className="text-xs text-slate-500 ml-2">1 </span>
</div>
<ul className="space-y-3 text-sm text-slate-200 mb-8 flex-1">
{t.features.map((f) => (
<li key={f} className="flex gap-2.5">
<svg className="w-4 h-4 text-emerald-400 flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth={2.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
</svg>
<span className="leading-relaxed">{f}</span>
</li>
))}
</ul>
<button
onClick={() => setSelectedTier(key)}
className={`w-full py-4 rounded-xl font-extrabold text-sm transition-colors ${
t.highlight
? 'bg-violet-500 hover:bg-violet-400 text-white'
: 'bg-white/10 hover:bg-white/20 text-white'
}`}
>
{t.name}
</button>
</div>
);
})}
</div>
<p className="text-xs text-slate-500 text-center mt-8">
<Link href="/legal/refund" className="underline hover:text-white"> </Link> .
.
</p>
</div>
</section>
{/* B2B */}
<section className="px-6 py-16 lg:px-14 bg-gradient-to-br from-slate-900 to-slate-950 border-y border-white/5">
<div className="max-w-5xl mx-auto">
<div className="flex flex-col md:flex-row md:items-center gap-6 border border-amber-500/30 bg-amber-500/5 rounded-2xl p-8">
<div className="flex-1">
<p className="font-mono text-xs text-amber-300/80 tracking-widest uppercase mb-2">
For Business
</p>
<h3 className="text-2xl font-extrabold text-white mb-2">
📣 BGM, .
</h3>
<p className="text-sm text-slate-300 leading-relaxed">
· · · BGM .
1 .
</p>
</div>
<Link
href="/contact?service=bgm"
className="inline-flex items-center gap-2 bg-amber-500 hover:bg-amber-400 text-slate-900 px-6 py-3 rounded-xl font-extrabold text-sm transition-colors"
>
B2B
</Link>
</div>
</div>
</section>
{/* FAQ */}
<section className="px-6 py-20 lg:px-14 bg-slate-950">
<div className="max-w-3xl mx-auto">
<h2 className="text-3xl md:text-4xl font-extrabold text-center mb-10">
</h2>
<div className="space-y-3">
{FAQS.map((f, i) => (
<div key={i} className="border border-white/10 rounded-xl overflow-hidden bg-white/[0.02]">
<button
onClick={() => setOpenFaq(openFaq === i ? null : i)}
className="w-full flex items-center justify-between px-5 py-4 text-left hover:bg-white/5 transition-colors"
>
<span className="font-bold text-white text-sm">{f.q}</span>
<span className={`text-violet-400 text-xl transition-transform ${openFaq === i ? 'rotate-45' : ''}`}>
+
</span>
</button>
{openFaq === i && (
<div className="px-5 pb-5 text-sm text-slate-300 leading-relaxed" style={{ wordBreak: 'keep-all' }}>
{f.a}
</div>
)}
</div>
))}
</div>
</div>
</section>
{/* FINAL CTA */}
<section className="px-6 py-20 lg:px-14 bg-gradient-to-br from-violet-900 via-slate-950 to-[#0b0530]">
<div className="max-w-3xl mx-auto text-center">
<h2 className="text-3xl md:text-5xl font-extrabold text-white mb-4 leading-tight" style={{ wordBreak: 'keep-all' }}>
,
<br />
<span className="bg-gradient-to-r from-violet-300 via-pink-200 to-cyan-300 bg-clip-text text-transparent">
.
</span>
</h2>
<p className="text-slate-400 text-lg mb-10">
39,000 · ·
</p>
<a
href="#pricing"
className="inline-flex items-center gap-2 bg-violet-600 hover:bg-violet-500 text-white px-12 py-5 rounded-xl font-extrabold text-base transition-colors shadow-[0_0_60px_rgba(139,92,246,0.5)]"
>
</a>
</div>
</section>
{selectedTier && (
<PurchaseAgreementModal
isOpen={!!selectedTier}
onClose={() => setSelectedTier(null)}
productName={`AI 음악 마스터 팩 · ${TIERS[selectedTier].name}`}
price={TIERS[selectedTier].price}
/>
)}
</div>
);
}

View File

@@ -1,30 +0,0 @@
import type { Metadata } from 'next';
export const metadata: Metadata = {
title: 'ChatGPT·Claude 프롬프트 엔지니어링 | 업무 AI 자동화',
description:
'ChatGPT, Claude, Gemini를 제대로 활용하는 맞춤형 프롬프트 설계. 이메일·보고서·코드리뷰·고객응대 업무를 AI로 3~5배 빠르게. 이미지 생성 프롬프트 패키지 12,900원, 자소서 첨삭 프롬프트 9,900원.',
keywords: [
'프롬프트 엔지니어링',
'ChatGPT 프롬프트 만들기',
'Claude 프롬프트 최적화',
'AI 업무 자동화 프롬프트',
'ChatGPT 활용법',
'이미지 생성 프롬프트',
'Midjourney 프롬프트',
'자소서 AI 첨삭',
'이력서 AI 교정',
'프롬프트 패키지',
'AI 프롬프트 구매',
],
openGraph: {
title: 'ChatGPT·Claude 프롬프트 엔지니어링 | 쟁승메이드',
description:
'업무 특화 AI 프롬프트 설계. 이미지 생성·자소서 첨삭 패키지 즉시 구매 가능. 9,900원~.',
url: 'https://jaengseung-made.com/services/prompt',
},
};
export default function PromptLayout({ children }: { children: React.ReactNode }) {
return children;
}

View File

@@ -1,656 +0,0 @@
'use client';
import { useState, useEffect, useRef } from 'react';
import Link from 'next/link';
import ContactModal from '../../components/ContactModal';
import PaymentButton from '../../components/PaymentButton';
import { trackCTAClick } from '../../../lib/gtag';
const KAKAO_CHANNEL_URL = process.env.NEXT_PUBLIC_KAKAO_CHANNEL_URL ?? null;
function useScrollReveal() {
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
const el = ref.current;
if (!el) return;
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.classList.add('is-visible');
observer.unobserve(entry.target);
}
});
},
{ threshold: 0.1, rootMargin: '0px 0px -40px 0px' }
);
el.querySelectorAll('.reveal').forEach((child) => observer.observe(child));
return () => observer.disconnect();
}, []);
return ref;
}
const CHECKLIST = [
'주로 어떤 AI 도구를 사용하는지 (ChatGPT / Claude / Gemini)',
'자동화하고 싶은 업무 유형 (이메일 / 보고서 / 코드 등)',
'현재 프롬프트 사용 방식 및 불만족스러운 점',
'필요한 프롬프트 수량 (단건 / 패키지 / 팀 전체)',
'납품 후 사용 가이드 및 1:1 교육 포함 여부 확인',
];
// ─── 프리미엄 상품 ───
const premiumProducts = [
{
id: 'image_gen',
badge: 'IMAGE GENERATION',
badgeColor: '#e879f9',
bgFrom: '#1a0533',
bgTo: '#2d1054',
accentColor: '#d946ef',
accentBg: 'rgba(217,70,239,0.12)',
accentBorder: 'rgba(217,70,239,0.3)',
title: 'AI 이미지 생성 마스터 프롬프트 패키지',
subtitle: 'Midjourney · DALL-E 3 · Stable Diffusion 전용',
price: '45,000원',
salePrice: '12,900원',
discountRate: '72% OFF',
saleLabel: '런칭 기념 특가',
priceNote: '/ 패키지 (즉시 다운로드)',
desc: '수천 장의 이미지 생성 실험을 통해 검증된, 업종별·스타일별 고품질 프롬프트 50종 세트. 단순 키워드 나열이 아닌 구도·조명·분위기·카메라·후처리까지 세밀하게 설계된 전문가급 프롬프트입니다.',
features: [
{ label: '50종 프롬프트 라이브러리', desc: '상업용 · 마케팅 · SNS 콘텐츠 · 제품 사진 · 인물 포트레이트 · 배경 · 로고 컨셉 · 인테리어 등 카테고리별 구성' },
{ label: '구도·조명·후처리 공식', desc: '주제(Subject) → 환경(Environment) → 스타일(Style) → 조명(Lighting) → 카메라(Camera) → 후처리(Post) 6단계 구조 적용' },
{ label: '네거티브 프롬프트 포함', desc: '흐림·왜곡·불필요한 텍스트·비현실적 요소를 제거하는 부정 키워드까지 최적화하여 실패 확률 최소화' },
{ label: 'Midjourney 파라미터 완전 가이드', desc: '--ar / --v / --style / --chaos / --no 등 핵심 파라미터 활용법과 상황별 추천값 포함' },
{ label: '한국어 → 영어 변환 치트시트', desc: '자주 쓰는 한국어 표현을 AI가 잘 이해하는 영어 표현으로 매핑한 빠른 참고 시트' },
{ label: '업종별 특화 세트 5개', desc: '카페/음식 · 패션 · 부동산 인테리어 · 교육/강의 · 뷰티/헬스 분야 특화 프롬프트 세트별 제공' },
{ label: '활용 예시 이미지 50장', desc: '각 프롬프트로 실제 생성한 결과물 예시 이미지 포함 (PDF 가이드북 형태 제공)' },
{ label: '무제한 재사용 가능', desc: '구매 후 본인 사업·작업에 무제한 활용 가능. 상업적 이용 허용' },
],
promptPreview: {
title: '프롬프트 예시 — 프리미엄 카페 음료 사진',
content: `A professional product photograph of a single iced caramel latte in a tall clear glass,
placed on a rustic wooden cafe table. The drink features layers of espresso, milk,
and golden caramel syrup with ice cubes.
Lighting: soft natural window light from the left, warm golden hour tone,
subtle rim lighting highlighting condensation droplets on the glass.
Camera: Canon EOS R5, 85mm f/1.8 lens, shallow depth of field,
foreground blur with coffee beans and a sprig of dried lavender.
Style: editorial food photography, Pinterest aesthetic, warm muted tones,
slightly desaturated with lifted shadows.
Post-processing: film grain texture, subtle vignette,
color grade with warm highlights and cool shadows.
--ar 4:5 --v 6.1 --style raw --q 2
Negative: text, watermark, multiple cups, cartoon, illustration,
overexposed, blurry, plastic look, artificial lighting`,
},
cta: '패키지 구매 문의 →',
productId: 'prompt_image_gen',
},
{
id: 'resume',
badge: 'CAREER COACHING',
badgeColor: '#34d399',
bgFrom: '#052e16',
bgTo: '#064e3b',
accentColor: '#10b981',
accentBg: 'rgba(16,185,129,0.12)',
accentBorder: 'rgba(16,185,129,0.3)',
title: 'AI 자소서·이력서 첨삭 마스터 프롬프트',
subtitle: 'ChatGPT · Claude 전용 · 대기업 HR 기준 적용',
price: '35,000원',
salePrice: '9,900원',
discountRate: '72% OFF',
saleLabel: '런칭 기념 특가',
priceNote: '/ 패키지 (즉시 다운로드)',
desc: '대기업 현직 개발자의 실전 경험과 수십 명의 신입/경력 지원자 첨삭 경험을 바탕으로 설계한 자소서·이력서 최적화 프롬프트 세트. 합격률을 높이는 구체적인 표현과 구조로 AI가 전문 컨설턴트처럼 첨삭하도록 만들어드립니다.',
features: [
{ label: '자기소개서 7가지 유형별 프롬프트', desc: '지원동기 · 성장과정 · 강점/약점 · 직무역량 · 팀워크 경험 · 위기극복 · 입사 후 포부 — 각 항목에 최적화된 별도 프롬프트 제공' },
{ label: 'STAR 기법 자동 적용', desc: '상황(Situation) → 과제(Task) → 행동(Action) → 결과(Result) 구조로 경험을 임팩트 있게 재구성하는 프롬프트 포함' },
{ label: '이력서 불릿포인트 최적화', desc: '단순 업무 나열이 아닌 "무엇을 했고, 어떤 방법으로, 어떤 결과를 냈는지" 3단 구조 + 수치화로 강점을 극대화하는 첨삭 프롬프트' },
{ label: 'ATS 키워드 최적화', desc: '채용 공고의 키워드를 분석하여 자소서에 자연스럽게 녹여내는 ATS(지원자 추적 시스템) 통과 최적화 프롬프트' },
{ label: '업종/직무별 맞춤 톤 설정', desc: 'IT · 금융 · 제조 · 마케팅 · 공공기관 등 업종별, 신입/경력별 적합한 문체와 표현 스타일로 자동 조정' },
{ label: '약점을 강점으로 전환하는 프롬프트', desc: '공백기 · 전공 불일치 · 낮은 학점 · 짧은 재직기간 등 불리한 스펙을 긍정적으로 표현하는 전략적 첨삭 프롬프트' },
{ label: '면접 질문 예측 & 답변 준비', desc: '작성된 자소서를 기반으로 예상 면접 질문을 생성하고, 모범 답변 구조를 잡아주는 면접 대비 프롬프트 포함' },
{ label: '실제 첨삭 Before/After 5사례', desc: '실제로 사용하여 개선된 자소서 전후 비교 예시 5가지를 PDF로 제공 (직무별 다양한 케이스)' },
],
promptPreview: {
title: '프롬프트 예시 — 지원동기 항목 첨삭',
content: `당신은 15년 경력의 대기업 HR 수석 컨설턴트입니다.
수백 명의 합격 자소서를 분석한 전문가 관점에서 다음 자소서를 첨삭해주세요.
[첨삭 기준]
1. STAR 기법(상황-과제-행동-결과) 구조가 명확한가?
2. 지원 동기가 회사의 사업 방향/가치와 구체적으로 연결되는가?
3. "열정", "성장" 등 추상적 단어 대신 구체적 경험과 수치가 있는가?
4. 첫 문장이 면접관의 시선을 끄는 Hook으로 시작하는가?
5. 지원 직무에서 필요한 역량이 자연스럽게 드러나는가?
[첨삭 방식]
① 현재 자소서의 강점 2가지 (구체적 근거 포함)
② 치명적 약점 3가지와 개선 방향
③ 표현이 약한 문장 3개를 지목하여 강화 버전으로 재작성
④ 전체 구조 리뉴얼 버전 (300자 이내로 압축한 임팩트 버전)
⑤ 이 자소서로 예상되는 면접 질문 2가지
[지원 정보]
- 회사/직무: [입력]
- 채용 공고 키워드: [입력]
[첨삭할 자소서]
[여기에 자소서 붙여넣기]`,
},
cta: '패키지 구매 문의 →',
productId: 'prompt_resume',
},
{
id: 'email',
badge: 'BUSINESS EMAIL',
badgeColor: '#60a5fa',
bgFrom: '#0c1a3a',
bgTo: '#1e3a6e',
accentColor: '#3b82f6',
accentBg: 'rgba(59,130,246,0.12)',
accentBorder: 'rgba(59,130,246,0.3)',
title: '비즈니스 이메일 마스터 프롬프트 패키지',
subtitle: 'ChatGPT · Claude 전용 · 상황별 40종 템플릿',
price: '32,000원',
salePrice: '10,900원',
discountRate: '66% OFF',
saleLabel: '런칭 기념 특가',
priceNote: '/ 패키지 (즉시 다운로드)',
desc: '신규 파트너 제안부터 거절 메일, 클레임 응대, 계약 협상까지 — 실무에서 반복 사용하는 비즈니스 이메일 상황 40가지를 AI가 전문 비서처럼 작성하도록 설계한 완성형 프롬프트 패키지입니다.',
features: [
{ label: '40가지 상황별 이메일 프롬프트', desc: '제안·문의·거절·사과·팔로업·계약·협상·내부 보고 등 비즈니스 전 상황 커버' },
{ label: '상대방 직급별 톤 자동 조정', desc: '대표·임원·실무자·외부 파트너·고객별로 적합한 경어와 표현 강도를 자동으로 조정하는 프롬프트' },
{ label: '영어 이메일 동시 출력', desc: '한국어 초안 작성 후 비즈니스 영어 버전으로 즉시 변환하는 이중 출력 프롬프트 포함' },
{ label: '클레임·컴플레인 전문 대응 세트', desc: '감정적 고객·거래처 이메일을 분석하고 상황별 최적 응대 메일을 생성하는 CS 전문 프롬프트 7종' },
{ label: '이메일 후속 조치 자동화', desc: '1차 연락 후 답장 없을 때의 팔로업, 회신 독촉, 미팅 확정 등 타임라인별 후속 이메일 시리즈 프롬프트' },
{ label: '제목 라인 A/B 테스트 생성', desc: '오픈율을 높이는 이메일 제목 5가지 변형을 자동 생성하는 헤드라인 최적화 프롬프트' },
{ label: '실전 예시 40쌍 (Before/After)', desc: '평범한 이메일 → 임팩트 있는 전문 이메일로 변환된 실전 예시 40쌍 PDF 가이드북 포함' },
],
promptPreview: {
title: '프롬프트 예시 — 신규 파트너십 제안 이메일',
content: `당신은 10년 경력의 B2B 영업 전문가이자 비즈니스 라이터입니다.
아래 정보를 바탕으로 상대방이 반드시 읽고 싶어지는 파트너십 제안 이메일을 작성해주세요.
[작성 원칙]
1. 첫 문장에서 상대방의 이익/관심사를 직접 언급 (내 소개 X)
2. 제안 핵심을 3줄 이내로 압축 (바쁜 담당자가 5초 내 파악)
3. 구체적 수치나 레퍼런스로 신뢰도 구축
4. 명확한 CTA 1개만 포함 (회의 일정 링크 OR 짧은 통화 요청)
5. 전체 300자 이내, 첨부 자료는 1개 이하
[제안 정보]
- 우리 회사/서비스: [입력]
- 제안 대상 회사: [입력]
- 협업으로 상대방이 얻는 이익: [입력]
- 기존 레퍼런스/실적: [입력]
- 원하는 다음 액션: [입력]
[출력 형식]
① 이메일 제목 3가지 옵션 (각각 다른 각도)
② 본문 이메일 (원칙 준수)
③ 수신자가 거절할 가능성이 높은 이유와 예방 팁`,
},
cta: '패키지 구매 문의 →',
productId: 'prompt_email',
},
{
id: 'marketing',
badge: 'MARKETING COPY',
badgeColor: '#fb923c',
bgFrom: '#1c0a00',
bgTo: '#431407',
accentColor: '#f97316',
accentBg: 'rgba(249,115,22,0.12)',
accentBorder: 'rgba(249,115,22,0.3)',
title: '마케팅 카피라이팅 마스터 프롬프트',
subtitle: 'SNS · 광고 · 상세페이지 · 유튜브 전용 35종',
price: '38,000원',
salePrice: '12,900원',
discountRate: '66% OFF',
saleLabel: '런칭 기념 특가',
priceNote: '/ 패키지 (즉시 다운로드)',
desc: '인스타그램·유튜브·쿠팡·스마트스토어·카카오 채널까지 — 각 플랫폼의 알고리즘과 소비자 심리를 반영하여 클릭률·전환율을 극대화하는 마케팅 카피 전문 프롬프트 35종 세트입니다.',
features: [
{ label: 'SNS 플랫폼별 최적화 카피 세트', desc: '인스타그램 피드/릴스·유튜브 제목·스레드·카카오 비즈메시지 각 채널 알고리즘 특성을 반영한 별도 프롬프트' },
{ label: '감성 카피 ↔ 이성 카피 전환', desc: '같은 상품을 감성 스토리텔링 방식과 스펙·기능 중심 이성 방식으로 각각 작성하여 A/B 테스트용 카피 쌍 생성' },
{ label: '상세페이지 전환율 최적화 구조', desc: '훅(Hook) → 문제 공감 → 해결책 제시 → 사회적 증거 → CTA의 5단계 전환 공식을 자동 적용하는 상세페이지 카피 프롬프트' },
{ label: '제품 소개글 10초 요약 공식', desc: '핵심 USP(독보적 강점)를 10초 안에 전달하는 엘리베이터 피치형 짧은 소개글 자동 생성 프롬프트' },
{ label: '유튜브/숏폼 썸네일 제목 생성기', desc: '조회수를 높이는 클릭베이트형 제목과 커뮤니티 공감형 제목을 구분하여 생성하는 유튜브 최적화 프롬프트' },
{ label: '리뷰·후기 마케팅 카피 변환', desc: '고객 후기·댓글을 분석하여 그 안의 핵심 감동 포인트를 광고 카피로 변환하는 소셜 프루프 카피 프롬프트' },
{ label: '업종별 금지 표현 자동 필터', desc: '식품·의료·금융·부동산 등 규제 업종의 법적 주의 표현을 사전에 체크하고 대안 표현을 제시하는 컴플라이언스 프롬프트' },
],
promptPreview: {
title: '프롬프트 예시 — 인스타그램 상품 소개 릴스 스크립트',
content: `당신은 팔로워 50만 명의 인스타그램 쇼핑 인플루언서이자
마케팅 전환율 전문가입니다.
다음 상품 정보를 바탕으로 구매 욕구를 자극하는 릴스 스크립트를 작성해주세요.
[스크립트 구조 — 60초 이내]
00~03초: 즉각 멈추게 하는 훅 문장 (의문형 OR 공감형 OR 충격 통계)
04~10초: "이게 뭔데?" 궁금증 유발 — 상품 첫 노출
11~30초: 핵심 기능 3가지를 BEFORE/AFTER 방식으로 시연
31~45초: 소셜 프루프 (실제 고객 반응 또는 수치)
46~55초: 한정 혜택/가격 공개
56~60초: 명확한 CTA (링크 인 바이오, DM, 댓글 단어)
[상품 정보]
- 상품명/카테고리: [입력]
- 핵심 기능/차별점: [입력]
- 타겟 고객: [입력]
- 가격/혜택: [입력]
[출력]
① 릴스 스크립트 본문
② 자막용 텍스트 (10자 이내 임팩트 문구 5개)
③ 해시태그 20개 (도달 최적화)`,
},
cta: '패키지 구매 문의 →',
productId: 'prompt_marketing',
},
{
id: 'report',
badge: 'BUSINESS REPORT',
badgeColor: '#a78bfa',
bgFrom: '#13082b',
bgTo: '#1e1148',
accentColor: '#8b5cf6',
accentBg: 'rgba(139,92,246,0.12)',
accentBorder: 'rgba(139,92,246,0.3)',
title: '업무 보고서·기획서 자동화 프롬프트 패키지',
subtitle: 'ChatGPT · Claude 전용 · 직장인 필수 30종',
price: '30,000원',
salePrice: '10,900원',
discountRate: '64% OFF',
saleLabel: '런칭 기념 특가',
priceNote: '/ 패키지 (즉시 다운로드)',
desc: '주간 업무 보고부터 임원 발표용 기획서, 투자 제안서, 회의록 요약까지 — 직장인이 매주 반복 작성하는 문서를 AI가 체계적으로 작성하도록 설계한 업무 자동화 프롬프트 30종 세트입니다.',
features: [
{ label: '주간·월간 업무 보고서 자동화', desc: '팀원별 진행 현황, 완료 사항, 이슈, 다음 주 계획을 표 형식으로 정리하는 구조화된 보고서 생성 프롬프트' },
{ label: '임원 보고용 1페이지 요약 공식', desc: '"Bottom Line Up Front" 원칙으로 핵심 결론 → 근거 → 요청사항 순서의 임원 친화적 요약 프롬프트' },
{ label: '신사업 기획서 뼈대 자동 생성', desc: '시장 분석 → 목표 설정 → 실행 방안 → 예산 계획 → KPI 설정의 5단계 기획서 프레임워크 자동 구성 프롬프트' },
{ label: '회의록 → 액션 아이템 변환', desc: '날것의 회의 내용을 입력하면 결정사항·담당자·기한·후속 과제로 즉시 분류하는 회의록 구조화 프롬프트' },
{ label: '데이터 분석 결과 스토리텔링', desc: '숫자·표·그래프 데이터를 경영진이 이해하기 쉬운 인사이트 내러티브로 변환하는 데이터 보고 프롬프트' },
{ label: 'RFP·제안서 경쟁력 강화', desc: '발주사의 선정 기준에 맞춰 제안서의 차별점·강점을 부각시키고 약점을 보완하는 제안서 최적화 프롬프트' },
{ label: '프레젠테이션 슬라이드 목차 자동 설계', desc: '보고 목적과 청중에 맞는 슬라이드 구성 순서, 각 슬라이드 핵심 메시지를 자동으로 설계해주는 프롬프트' },
],
promptPreview: {
title: '프롬프트 예시 — 주간 업무 보고서 자동 작성',
content: `당신은 5년 경력의 기업 커뮤니케이션 전문가이자
비즈니스 라이터입니다.
아래 업무 내용을 바탕으로 팀장에게 보고할 주간 업무 보고서를 작성해주세요.
[보고서 작성 원칙]
1. 첫 줄에 이번 주 가장 중요한 성과 1가지를 먼저 명시
2. 수치로 표현 가능한 모든 항목은 반드시 수치화
3. 이슈는 "문제 상황 → 조치 내용 → 현재 상태" 3단 구조로 기술
4. 다음 주 계획은 담당자·기한과 함께 표 형식으로 정리
5. 전체 A4 1장 이내, 핵심 위주
[업무 내용 입력]
- 이번 주 완료 업무: [입력]
- 진행 중인 업무: [입력]
- 발생한 이슈/리스크: [입력]
- 다음 주 계획: [입력]
- 요청 사항 (상급자에게): [입력]
[출력 형식]
① 이번 주 핵심 성과 (1~2줄 요약)
② 완료 업무 목록 (수치 포함)
③ 진행 중 업무 + 달성률
④ 이슈 및 대응 현황
⑤ 다음 주 계획 (담당·기한 포함 표)
⑥ 협조 요청 사항`,
},
cta: '패키지 구매 문의 →',
productId: 'prompt_report',
},
];
const useCases = [
{ label: '이메일 작성', desc: '고객사별, 상황별 최적화된 비즈니스 이메일 프롬프트' },
{ label: '보고서·기획서', desc: '회사 내부 보고서, 제안서, 기획서 자동 작성용 프롬프트' },
{ label: '고객 응대', desc: 'CS 상담, FAQ 응답, 컴플레인 처리를 위한 프롬프트' },
{ label: '마케팅 카피', desc: '제품 소개글, 광고 카피, SNS 콘텐츠 생성 프롬프트' },
{ label: '개발 보조', desc: '코드 리뷰, 버그 설명, 문서화를 위한 개발자 전용 프롬프트' },
{ label: '학습·요약', desc: '문서 요약, 핵심 추출, 번역 최적화 프롬프트' },
];
const plans = [
{
name: '단건 설계',
price: '30,000원',
period: '/ 건',
desc: '특정 업무 1건 프롬프트 설계',
features: ['요구사항 분석 및 인터뷰', '목적별 프롬프트 1개 설계', 'ChatGPT / Claude 최적화', '수정 1회 포함', '사용 가이드 문서 제공'],
highlight: false,
productId: 'prompt_single',
},
{
name: '비즈니스 패키지',
price: '99,000원',
period: '/ 패키지',
desc: '업무 유형별 5개 프롬프트 세트',
features: ['업무 분석 심층 인터뷰', '5개 프롬프트 맞춤 설계', '용도별 프롬프트 라이브러리', '수정 3회 포함', '활용 방법 1:1 교육 (30분)', '1개월 내 추가 조정 가능'],
highlight: true,
productId: 'prompt_business',
},
{
name: '팀/기업 패키지',
price: '249,000원~',
period: '/ 세트',
desc: '부서·팀 전체 프롬프트 시스템 구축',
features: ['팀 업무 프로세스 전체 분석', '10개 이상 프롬프트 설계', '팀 공유 프롬프트 라이브러리', '사내 가이드 문서 작성', '전 직원 교육 자료 제공', '3개월 내 업데이트 지원'],
highlight: false,
productId: 'prompt_team',
},
];
const examples = [
{
type: '회의록 요약',
before: '회의 내용을 요약해줘',
after: '다음 회의록을 분석하여: 1) 핵심 결정사항 3가지, 2) 담당자별 Action Item, 3) 다음 회의 전 완료해야 할 사항을 불릿 형식으로 정리해줘. 회의록: [내용]',
improvement: '구조화된 출력 · 역할 분리 · 명확한 포맷',
},
{
type: '코드 리뷰',
before: '이 코드 리뷰해줘',
after: '시니어 백엔드 개발자 관점에서 다음 코드를 리뷰해줘: 1) 버그 및 잠재적 오류, 2) 성능 개선 포인트, 3) 클린코드 관점에서의 개선사항을 각각 심각도(High/Medium/Low)와 함께 알려줘. 코드: [코드]',
improvement: '페르소나 설정 · 심각도 기준 · 다각도 분석',
},
];
export default function PromptPage() {
const [modalOpen, setModalOpen] = useState(false);
const [modalService, setModalService] = useState('프롬프트 엔지니어링');
const containerRef = useScrollReveal();
const openModal = (service: string) => {
trackCTAClick(service, '/services/prompt');
setModalService(service);
setModalOpen(true);
};
return (
<div ref={containerRef} className="min-h-full bg-[#f0f5ff]">
<style>{`
.prompt-card {
transition: transform 0.4s cubic-bezier(0.16, 1, 0.3, 1),
box-shadow 0.4s cubic-bezier(0.16, 1, 0.3, 1);
}
.prompt-card:hover {
transform: translateY(-4px);
box-shadow: 0 20px 40px -12px rgba(0,0,0,0.15);
}
`}</style>
<ContactModal
isOpen={modalOpen}
onClose={() => setModalOpen(false)}
service={modalService}
checklist={CHECKLIST}
accentColor="text-violet-400"
headerFrom="#0d0a2e"
headerTo="#1a0f5c"
/>
{/* ─── Hero ─── */}
<div className="relative overflow-hidden bg-[#0d0a2e] px-6 py-14 lg:px-12" style={{ backgroundImage: 'repeating-linear-gradient(135deg, rgba(255,255,255,0.015) 0px, rgba(255,255,255,0.015) 1px, transparent 1px, transparent 40px)' }}>
<div className="relative max-w-3xl mx-auto">
<Link href="/" className="inline-flex items-center gap-1.5 text-violet-300/60 hover:text-violet-300 text-sm mb-8 transition">
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" /></svg>
</Link>
<p className="text-violet-400 text-xs font-bold uppercase tracking-widest mb-4 font-mono"> · AI </p>
<h1 className="text-4xl md:text-5xl font-extrabold text-white mb-4 tracking-tight leading-tight">
AI를 <br />
100%
</h1>
<p className="text-violet-100/50 text-base md:text-lg leading-relaxed max-w-xl mx-auto mb-6">
ChatGPT·Claude를 ?<br />
AI를 .
</p>
<div className="flex flex-wrap gap-2 justify-center">
<div className="inline-flex items-center gap-2 bg-violet-400/10 border border-violet-400/20 text-violet-300 text-xs font-medium px-4 py-2 rounded-full">
<span className="text-green-400"></span> 3~5
</div>
<div className="inline-flex items-center gap-2 bg-white/5 border border-white/10 text-white/50 text-xs font-medium px-4 py-2 rounded-full">
ChatGPT · Claude · Gemini
</div>
</div>
</div>
</div>
{/* ─── 프리미엄 상품 ─── */}
<div className="px-6 py-12 lg:px-12">
<div className="max-w-5xl mx-auto">
<div className="text-center mb-8 reveal">
<div className="inline-flex items-center gap-2 bg-fuchsia-500/10 border border-fuchsia-500/30 text-fuchsia-400 text-xs font-extrabold px-4 py-1.5 rounded-full uppercase tracking-widest mb-4">
<svg className="w-3.5 h-3.5" viewBox="0 0 24 24" fill="currentColor"><path d="M11.049 2.927c.3-.921 1.603-.921 1.902 0l1.519 4.674a1 1 0 00.95.69h4.915c.969 0 1.371 1.24.588 1.81l-3.976 2.888a1 1 0 00-.363 1.118l1.518 4.674c.3.922-.755 1.688-1.538 1.118l-3.976-2.888a1 1 0 00-1.176 0l-3.976 2.888c-.783.57-1.838-.197-1.538-1.118l1.518-4.674a1 1 0 00-.363-1.118l-3.976-2.888c-.784-.57-.38-1.81.588-1.81h4.914a1 1 0 00.951-.69l1.519-4.674z" /></svg>
PREMIUM PRODUCTS
</div>
<h2 className="text-2xl md:text-3xl font-extrabold text-[#04102b]"> </h2>
<p className="text-slate-500 text-sm mt-2"> </p>
</div>
<div className="grid lg:grid-cols-2 gap-6">
{premiumProducts.map((product, idx) => (
<div
key={product.id}
className={`rounded-2xl overflow-hidden border prompt-card reveal reveal-d${(idx % 4) + 1}`}
style={{ borderColor: product.accentBorder, background: `linear-gradient(135deg, ${product.bgFrom}, ${product.bgTo})` }}
>
{/* 헤더 */}
<div className="p-6 border-b" style={{ borderColor: product.accentBorder }}>
<div className="flex items-start justify-between gap-3 mb-3">
<span
className="text-xs font-extrabold px-3 py-1 rounded-full uppercase tracking-widest"
style={{ color: product.accentColor, background: product.accentBg, border: `1px solid ${product.accentBorder}` }}
>
{product.badge}
</span>
<div className="text-right">
{/* 할인 배지 */}
<div className="flex items-center justify-end gap-2 mb-1">
<span className="text-xs font-extrabold bg-red-500 text-white px-2 py-0.5 rounded-md animate-pulse">
{product.discountRate}
</span>
<span className="text-xs font-bold" style={{ color: product.accentColor }}>
{product.saleLabel}
</span>
</div>
{/* 원가 취소선 */}
<div className="text-sm line-through opacity-40 text-right text-white mb-0.5">{product.price}</div>
{/* 세일가 */}
<div className="text-2xl font-extrabold text-white">{product.salePrice}</div>
<div className="text-xs" style={{ color: product.accentColor + '99' }}>{product.priceNote}</div>
</div>
</div>
<h3 className="text-lg font-extrabold text-white mb-1 leading-snug">{product.title}</h3>
<p className="text-xs mb-3" style={{ color: product.accentColor + 'aa' }}>{product.subtitle}</p>
<p className="text-sm leading-relaxed" style={{ color: 'rgba(255,255,255,0.55)' }}>{product.desc}</p>
</div>
{/* 기능 목록 */}
<div className="p-6 border-b" style={{ borderColor: product.accentBorder }}>
<p className="text-xs font-bold uppercase tracking-widest mb-3" style={{ color: product.accentColor + 'cc' }}> </p>
<ul className="space-y-2.5">
{product.features.map((f, i) => (
<li key={i} className="flex items-start gap-2.5">
<div className="w-4 h-4 rounded-full flex items-center justify-center flex-shrink-0 mt-0.5" style={{ background: product.accentBg, border: `1px solid ${product.accentBorder}` }}>
<div className="w-1.5 h-1.5 rounded-full" style={{ background: product.accentColor }} />
</div>
<div>
<span className="text-xs font-bold text-white">{f.label}</span>
<span className="text-xs ml-1.5" style={{ color: 'rgba(255,255,255,0.45)' }}> {f.desc}</span>
</div>
</li>
))}
</ul>
</div>
{/* 프롬프트 미리보기 */}
<div className="p-6 border-b" style={{ borderColor: product.accentBorder }}>
<p className="text-xs font-bold uppercase tracking-widest mb-3" style={{ color: product.accentColor + 'cc' }}>
{product.promptPreview.title}
</p>
<div
className="rounded-xl p-4 font-mono text-xs leading-relaxed overflow-auto"
style={{ background: 'rgba(0,0,0,0.35)', color: 'rgba(255,255,255,0.6)', border: `1px solid ${product.accentBorder}`, whiteSpace: 'pre-line', maxHeight: '180px' }}
>
{product.promptPreview.content}
</div>
</div>
{/* CTA */}
<div className="p-6">
<PaymentButton
productId={product.productId}
className="flex items-center justify-center gap-2 w-full py-3.5 rounded-xl text-sm font-extrabold transition-all hover:opacity-90"
style={{ background: product.accentColor, color: product.bgFrom }}
>
</PaymentButton>
<p className="text-center text-xs mt-2" style={{ color: 'rgba(255,255,255,0.3)' }}>
·
</p>
</div>
</div>
))}
</div>
</div>
</div>
{/* ─── Before/After ─── */}
<div className="px-6 py-12 lg:px-12">
<div className="max-w-5xl mx-auto">
<div className="text-center mb-8 reveal">
<p className="text-violet-600 text-xs font-bold uppercase tracking-widest mb-2">BEFORE vs AFTER</p>
<h2 className="text-2xl md:text-3xl font-extrabold text-[#04102b]"> </h2>
</div>
<div className="space-y-5">
{examples.map((ex, idx) => (
<div key={ex.type} className={`bg-white rounded-2xl border border-[#dbe8ff] overflow-hidden reveal reveal-d${idx + 1}`}>
<div className="bg-[#04102b] px-5 py-3 flex items-center justify-between">
<span className="text-white/60 text-xs font-semibold font-mono">{ex.type} </span>
<span className="bg-violet-400/20 border border-violet-400/30 text-violet-300 text-xs px-3 py-1 rounded-full">{ex.improvement}</span>
</div>
<div className="grid md:grid-cols-2 divide-y md:divide-y-0 md:divide-x divide-[#dbe8ff]">
<div className="p-5">
<div className="inline-block bg-red-50 border border-red-200 text-red-600 text-xs font-bold px-2 py-0.5 rounded-md mb-3"> </div>
<div className="bg-slate-50 rounded-xl px-4 py-3 font-mono text-sm text-slate-600 border border-slate-200">&ldquo;{ex.before}&rdquo;</div>
<div className="mt-3 text-xs text-red-500 flex items-center gap-1.5">
<svg className="w-3.5 h-3.5" fill="currentColor" viewBox="0 0 20 20"><path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" /></svg>
</div>
</div>
<div className="p-5">
<div className="inline-block bg-violet-50 border border-violet-200 text-violet-700 text-xs font-bold px-2 py-0.5 rounded-md mb-3"> </div>
<div className="bg-violet-50 rounded-xl px-4 py-3 font-mono text-sm text-slate-700 border border-violet-100 leading-relaxed">&ldquo;{ex.after}&rdquo;</div>
<div className="mt-3 text-xs text-violet-600 flex items-center gap-1.5">
<svg className="w-3.5 h-3.5" fill="currentColor" viewBox="0 0 20 20"><path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" /></svg>
</div>
</div>
</div>
</div>
))}
</div>
</div>
</div>
{/* ─── 활용 분야 ─── */}
<div className="px-6 pb-12 lg:px-12">
<div className="max-w-5xl mx-auto">
<div className="text-center mb-8 reveal">
<p className="text-violet-600 text-xs font-bold uppercase tracking-widest mb-2">USE CASES</p>
<h2 className="text-2xl md:text-3xl font-extrabold text-[#04102b]"> </h2>
</div>
<div className="grid sm:grid-cols-2 lg:grid-cols-3 gap-4">
{useCases.map((uc, i) => (
<div key={uc.label} className={`bg-white rounded-2xl border border-[#dbe8ff] p-5 hover:border-violet-200 transition-all duration-300 reveal reveal-d${(i % 3) + 1}`}>
<div className="flex items-start gap-3">
<div className="w-8 h-8 rounded-lg bg-violet-50 border border-violet-200 flex items-center justify-center flex-shrink-0">
<span className="text-violet-600 font-extrabold text-xs">{String(i + 1).padStart(2, '0')}</span>
</div>
<div>
<h3 className="font-bold text-[#04102b] text-sm mb-1">{uc.label}</h3>
<p className="text-slate-500 text-xs leading-relaxed">{uc.desc}</p>
</div>
</div>
</div>
))}
</div>
</div>
</div>
{/* ─── 요금제 ─── */}
<div className="px-6 pb-12 lg:px-12">
<div className="max-w-5xl mx-auto">
<div className="text-center mb-8 reveal">
<p className="text-[#1a56db] text-xs font-bold uppercase tracking-widest mb-2">PRICING</p>
<h2 className="text-2xl md:text-3xl font-extrabold text-[#04102b]"></h2>
</div>
<div className="grid sm:grid-cols-3 gap-5">
{plans.map((plan, idx) => (
<div key={plan.name} className={`rounded-2xl border p-6 relative flex flex-col reveal reveal-d${idx + 1} ${
plan.highlight
? 'bg-gradient-to-br from-[#0d0a2e] to-[#1a0f5c] border-violet-400/30 shadow-2xl shadow-violet-900/20 scale-105'
: 'bg-white border-[#dbe8ff]'
}`}>
{plan.highlight && (
<div className="absolute -top-3.5 left-1/2 -translate-x-1/2 bg-violet-400 text-[#0d0a2e] text-xs font-extrabold px-4 py-1 rounded-full tracking-wide"></div>
)}
<div className={`text-xs font-bold mb-2 tracking-wide ${plan.highlight ? 'text-violet-400' : 'text-slate-400'}`}>{plan.name.toUpperCase()}</div>
<div className="flex items-baseline gap-1 mb-1">
<span className={`text-3xl font-extrabold ${plan.highlight ? 'text-white' : 'text-[#04102b]'}`}>{plan.price}</span>
<span className={`text-sm ${plan.highlight ? 'text-violet-300/50' : 'text-slate-400'}`}>{plan.period}</span>
</div>
<p className={`text-xs mb-5 ${plan.highlight ? 'text-violet-300/50' : 'text-slate-400'}`}>{plan.desc}</p>
<ul className="space-y-2 mb-6 flex-1">
{plan.features.map((f) => (
<li key={f} className={`flex items-start gap-2 text-xs ${plan.highlight ? 'text-violet-100/80' : 'text-slate-600'}`}>
<div className={`w-4 h-4 rounded-full flex items-center justify-center flex-shrink-0 mt-0.5 ${plan.highlight ? 'bg-violet-400/20 border border-violet-400/40' : 'bg-[#f0f5ff] border border-[#dbe8ff]'}`}>
<div className={`w-1.5 h-1.5 rounded-full ${plan.highlight ? 'bg-violet-400' : 'bg-[#1a56db]'}`} />
</div>
{f}
</li>
))}
</ul>
<button
onClick={() => openModal(`프롬프트 엔지니어링 - ${plan.name}`)}
className={`block w-full text-center py-3 rounded-xl text-sm font-bold transition ${
plan.highlight ? 'bg-violet-400 text-[#0d0a2e] hover:bg-violet-300' : 'bg-[#04102b] text-white hover:bg-[#0a1f5c]'
}`}
>
</button>
</div>
))}
</div>
</div>
</div>
{/* ─── CTA ─── */}
<div className="px-6 pb-12 lg:px-12">
<div className="max-w-3xl mx-auto">
<div className="bg-gradient-to-r from-[#0d0a2e] to-[#1a0f5c] rounded-2xl border border-violet-400/20 p-8 text-center reveal">
<p className="text-violet-400 text-xs font-bold uppercase tracking-widest mb-2">GET STARTED</p>
<h3 className="text-white text-2xl font-extrabold mb-2">AI를 </h3>
<p className="text-violet-100/40 text-sm mb-6"> </p>
<button
onClick={() => openModal('프롬프트 엔지니어링')}
className="inline-flex items-center gap-2 bg-violet-400 hover:bg-violet-300 text-[#0d0a2e] px-8 py-3 rounded-xl font-extrabold text-sm transition-all shadow-lg shadow-violet-900/30"
>
</button>
</div>
</div>
</div>
</div>
);
}

View File

@@ -1,26 +0,0 @@
import type { Metadata } from 'next';
export const metadata: Metadata = {
title: '주식 자동 매매 프로그램',
description:
'NAS 서버에서 직접 운영 중인 주식 자동 매매 시스템. RSI·MACD·볼린저밴드 기반 매매 신호를 텔레그램으로 수신하고 자동 매수·매도합니다. 키움·한국투자 연동.',
keywords: [
'주식 자동 매매',
'알고트레이딩',
'주식 자동화',
'텔레그램 주식 알림',
'키움 자동매매',
'주식 프로그램',
'RSI 매매',
],
openGraph: {
title: '주식 자동 매매 프로그램 | 쟁승메이드',
description:
'직접 운영 중인 알고트레이딩 시스템. 텔레그램 연동 · 자동 매수매도 · 설치 49,000원~.',
url: 'https://jaengseung-made.com/services/stock',
},
};
export default function StockLayout({ children }: { children: React.ReactNode }) {
return children;
}

View File

@@ -1,6 +0,0 @@
import { redirect } from 'next/navigation';
// 토스페이먼츠 심사 정책상 판매 불가 상품으로 분류 — 비공개 처리
export default function StockPage() {
redirect('/');
}

View File

@@ -23,6 +23,7 @@ export const metadata: Metadata = {
'소상공인·스타트업 홈페이지 제작. 템플릿 없이 직접 개발, SEO 포함, 20만원~.', '소상공인·스타트업 홈페이지 제작. 템플릿 없이 직접 개발, SEO 포함, 20만원~.',
url: 'https://jaengseung-made.com/services/website', url: 'https://jaengseung-made.com/services/website',
}, },
robots: { index: false, follow: false },
}; };
export default function WebsiteLayout({ children }: { children: React.ReactNode }) { export default function WebsiteLayout({ children }: { children: React.ReactNode }) {

View File

@@ -5,47 +5,12 @@ export default function sitemap(): MetadataRoute.Sitemap {
const now = new Date(); const now = new Date();
return [ return [
{ { url: base, lastModified: now, changeFrequency: 'weekly', priority: 1.0 },
url: base, { url: `${base}/services/music`, lastModified: now, changeFrequency: 'weekly', priority: 0.95 },
lastModified: now, { url: `${base}/services/blog`, lastModified: now, changeFrequency: 'weekly', priority: 0.9 },
changeFrequency: 'weekly', { url: `${base}/saju`, lastModified: now, changeFrequency: 'monthly', priority: 0.7 },
priority: 1.0, { url: `${base}/legal/terms`, lastModified: now, changeFrequency: 'yearly', priority: 0.3 },
}, { url: `${base}/legal/refund`, lastModified: now, changeFrequency: 'yearly', priority: 0.3 },
{ { url: `${base}/legal/privacy`, lastModified: now, changeFrequency: 'yearly', priority: 0.3 },
url: `${base}/freelance`,
lastModified: now,
changeFrequency: 'weekly',
priority: 0.9,
},
{
url: `${base}/services/automation`,
lastModified: now,
changeFrequency: 'weekly',
priority: 0.9,
},
{
url: `${base}/services/prompt`,
lastModified: now,
changeFrequency: 'weekly',
priority: 0.8,
},
{
url: `${base}/services/website`,
lastModified: now,
changeFrequency: 'monthly',
priority: 0.8,
},
{
url: `${base}/services/ai-kit`,
lastModified: now,
changeFrequency: 'weekly',
priority: 0.9,
},
{
url: `${base}/saju`,
lastModified: now,
changeFrequency: 'monthly',
priority: 0.5,
},
]; ];
} }

View File

@@ -1,541 +0,0 @@
'use client';
import { useState, useCallback } from 'react';
import type { SearchResult as FullSearchResult, FitmentEntry, PriceSource } from '@/lib/ebay-tools/types';
/* ── Types (페이지 전용) ──────────────────────────────────── */
// SearchResult['data']에 해당하는 타입 (API 응답의 data 필드)
type PageSearchResult = FullSearchResult['data'];
interface HistoryItem {
partNumber: string;
partName?: string;
time: string;
resultSummary: string;
}
/* ── Tab IDs ────────────────────────────────────────────────── */
const TABS = [
{ id: 'basic', label: '기본 정보' },
{ id: 'listing', label: '이베이 리스팅' },
{ id: 'fitment', label: '호환 차종' },
{ id: 'pricing', label: '가격 비교' },
{ id: 'raw', label: '원본 데이터' },
] as const;
type TabId = (typeof TABS)[number]['id'];
/* ── Icons (inline SVGs) ────────────────────────────────────── */
const SearchIcon = () => (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
);
const SpinnerIcon = () => (
<svg className="w-5 h-5 animate-spin" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
</svg>
);
const CopyIcon = () => (
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
</svg>
);
const ClockIcon = () => (
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
);
/* ── Component ──────────────────────────────────────────────── */
export default function EbayPartsPage() {
const [partNumber, setPartNumber] = useState('');
const [partName, setPartName] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [result, setResult] = useState<PageSearchResult | null>(null);
const [activeTab, setActiveTab] = useState<TabId>('basic');
const [history, setHistory] = useState<HistoryItem[]>([]);
const [copied, setCopied] = useState(false);
const [rawExpanded, setRawExpanded] = useState(false);
/* ── Search ─────────────────────────────────────────────── */
const handleSearch = useCallback(
async (pn?: string, pnm?: string) => {
const searchPartNumber = pn ?? partNumber;
const searchPartName = pnm ?? partName;
if (!searchPartNumber.trim()) {
setError('품번을 입력해주세요.');
return;
}
setLoading(true);
setError(null);
setResult(null);
setActiveTab('basic');
try {
const res = await fetch('/api/tools/ebay-parts/search', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
partNumber: searchPartNumber.trim(),
partName: searchPartName.trim() || undefined,
}),
});
const json = await res.json();
if (!res.ok || !json.success) {
setError(json.error || '검색에 실패했습니다.');
return;
}
setResult(json.data);
// Update history
setHistory((prev) => {
const entry: HistoryItem = {
partNumber: searchPartNumber.trim(),
partName: searchPartName.trim() || undefined,
time: new Date().toLocaleTimeString('ko-KR'),
resultSummary: `${json.data.fitment.length}개 차종, ${json.data.pricing.sources.length}개 소스`,
};
return [entry, ...prev.filter((h) => h.partNumber !== entry.partNumber)].slice(0, 5);
});
} catch {
setError('네트워크 오류가 발생했습니다. 다시 시도해주세요.');
} finally {
setLoading(false);
}
},
[partNumber, partName]
);
const handleHistoryClick = (item: HistoryItem) => {
setPartNumber(item.partNumber);
setPartName(item.partName || '');
handleSearch(item.partNumber, item.partName);
};
const handleCopy = async (text: string) => {
await navigator.clipboard.writeText(text);
setCopied(true);
setTimeout(() => setCopied(false), 1500);
};
/* ── Confidence Badge ──────────────────────────────────── */
const ConfidenceBadge = ({ level }: { level: string }) => {
const styles: Record<string, string> = {
high: 'bg-emerald-50 text-emerald-700 border-emerald-200',
medium: 'bg-amber-50 text-amber-700 border-amber-200',
low: 'bg-red-50 text-red-700 border-red-200',
};
const labels: Record<string, string> = { high: 'High', medium: 'Medium', low: 'Low' };
return (
<span className={`text-xs font-medium px-2 py-0.5 rounded border ${styles[level] || styles.low}`}>
{labels[level] || level}
</span>
);
};
/* ── Tab Content Renderers ─────────────────────────────── */
const renderBasicInfo = () => {
if (!result) return null;
const { basicInfo } = result;
return (
<div className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{[
['부품명', basicInfo.partName],
['브랜드', basicInfo.brand],
['품번', basicInfo.partNumber],
['카테고리', basicInfo.category],
].map(([label, value]) => (
<div key={label} className="bg-slate-50 rounded-lg p-4 border border-slate-100">
<p className="text-xs text-slate-500 font-medium mb-1">{label}</p>
<p className="text-sm text-slate-900 font-semibold">{value}</p>
</div>
))}
</div>
<div className="bg-slate-50 rounded-lg p-4 border border-slate-100">
<p className="text-xs text-slate-500 font-medium mb-2">OEM </p>
<div className="flex flex-wrap gap-2">
{basicInfo.oemNumbers.map((num) => (
<span key={num} className="bg-white text-sm font-mono text-slate-700 px-3 py-1 rounded border border-slate-200">
{num}
</span>
))}
</div>
</div>
</div>
);
};
const renderListing = () => {
if (!result) return null;
const { listing } = result;
return (
<div className="space-y-4">
{/* Title */}
<div className="bg-slate-50 rounded-lg p-4 border border-slate-100">
<div className="flex items-center justify-between mb-1">
<p className="text-xs text-slate-500 font-medium"> </p>
<button
onClick={() => handleCopy(listing.title)}
className="flex items-center gap-1 text-xs text-blue-600 hover:text-blue-800 transition-colors"
>
<CopyIcon />
{copied ? '복사됨' : '복사'}
</button>
</div>
<p className="text-sm text-slate-900 font-semibold">{listing.title}</p>
</div>
{/* Category */}
<div className="bg-slate-50 rounded-lg p-4 border border-slate-100">
<p className="text-xs text-slate-500 font-medium mb-1"> </p>
<p className="text-sm text-slate-900 font-semibold">{listing.category}</p>
</div>
{/* Item Specifics */}
<div className="bg-slate-50 rounded-lg p-4 border border-slate-100">
<p className="text-xs text-slate-500 font-medium mb-3">Item Specifics</p>
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-slate-200">
<th className="text-left py-2 px-3 font-semibold text-slate-600">Key</th>
<th className="text-left py-2 px-3 font-semibold text-slate-600">Value</th>
</tr>
</thead>
<tbody>
{Object.entries(listing.itemSpecifics).map(([key, value]) => (
<tr key={key} className="border-b border-slate-100">
<td className="py-2 px-3 text-slate-600">{key}</td>
<td className="py-2 px-3 text-slate-900 font-medium">{value}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
);
};
const renderFitment = () => {
if (!result) return null;
return (
<div className="bg-slate-50 rounded-lg p-4 border border-slate-100">
<p className="text-xs text-slate-500 font-medium mb-3"> </p>
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-slate-200">
<th className="text-left py-2 px-3 font-semibold text-slate-600">Year</th>
<th className="text-left py-2 px-3 font-semibold text-slate-600">Make</th>
<th className="text-left py-2 px-3 font-semibold text-slate-600">Model</th>
<th className="text-left py-2 px-3 font-semibold text-slate-600">Engine</th>
<th className="text-left py-2 px-3 font-semibold text-slate-600"></th>
</tr>
</thead>
<tbody>
{result.fitment.map((f, i) => (
<tr key={i} className="border-b border-slate-100">
<td className="py-2 px-3 text-slate-900">{f.year}</td>
<td className="py-2 px-3 text-slate-900">{f.make}</td>
<td className="py-2 px-3 text-slate-900">{f.model}</td>
<td className="py-2 px-3 text-slate-700 font-mono text-xs">{f.engine}</td>
<td className="py-2 px-3">
<ConfidenceBadge level={f.confidence} />
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
);
};
const renderPricing = () => {
if (!result) return null;
const { pricing } = result;
return (
<div className="space-y-4">
{/* Price table */}
<div className="bg-slate-50 rounded-lg p-4 border border-slate-100">
<p className="text-xs text-slate-500 font-medium mb-3"> </p>
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-slate-200">
<th className="text-left py-2 px-3 font-semibold text-slate-600"></th>
<th className="text-right py-2 px-3 font-semibold text-slate-600"> (USD)</th>
<th className="text-right py-2 px-3 font-semibold text-slate-600"> </th>
<th className="text-left py-2 px-3 font-semibold text-slate-600"></th>
</tr>
</thead>
<tbody>
{pricing.sources.map((s) => (
<tr key={s.site} className="border-b border-slate-100">
<td className="py-2 px-3 text-slate-900 font-medium">{s.site}</td>
<td className="py-2 px-3 text-right text-slate-900 font-mono">
${s.price.toFixed(2)}
</td>
<td className="py-2 px-3 text-right text-slate-600 font-mono">
{Math.round(s.price * pricing.exchangeRate.rate).toLocaleString()}
</td>
<td className="py-2 px-3">
<a
href={s.url}
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 hover:text-blue-800 text-xs underline"
>
</a>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
{/* Exchange + customs */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="bg-slate-50 rounded-lg p-4 border border-slate-100">
<p className="text-xs text-slate-500 font-medium mb-2"> </p>
<p className="text-lg font-bold text-slate-900">
1 USD = {pricing.exchangeRate.rate.toLocaleString()}
</p>
<p className="text-xs text-slate-500 mt-1">
{pricing.exchangeRate.source} ({pricing.exchangeRate.date})
</p>
</div>
<div className="bg-slate-50 rounded-lg p-4 border border-slate-100">
<p className="text-xs text-slate-500 font-medium mb-2">/ </p>
{pricing.customs.isExempt && (
<p className="text-sm text-emerald-700 font-semibold mb-2">
$150
</p>
)}
<p className="text-sm text-slate-900">
HS Code: <span className="font-mono font-bold">{pricing.customs.hsCode}</span>
</p>
<p className="text-sm text-slate-900">
: <span className="font-bold">{pricing.customs.dutyRate}</span>
</p>
<p className="text-sm text-slate-900">
: <span className="font-bold text-blue-700">{pricing.customs.estimatedDuty.toLocaleString()}</span>
</p>
<p className="text-sm text-slate-900">
(VAT 10%): <span className="font-bold text-blue-700">{pricing.customs.vat.toLocaleString()}</span>
</p>
<p className="text-sm text-slate-900 mt-1">
: <span className="font-bold text-blue-800 text-base">{pricing.customs.totalImportCost.toLocaleString()}</span>
</p>
<p className="text-xs text-slate-400 mt-2">{pricing.customs.disclaimer}</p>
</div>
</div>
</div>
);
};
const renderRawData = () => {
if (!result) return null;
return (
<div className="bg-slate-50 rounded-lg border border-slate-100">
<button
onClick={() => setRawExpanded(!rawExpanded)}
className="w-full flex items-center justify-between p-4 text-sm font-medium text-slate-700 hover:text-slate-900 transition-colors"
>
<span> JSON </span>
<svg
className={`w-4 h-4 transition-transform ${rawExpanded ? 'rotate-180' : ''}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</button>
{rawExpanded && (
<div className="px-4 pb-4">
<pre className="bg-slate-900 text-slate-100 rounded-lg p-4 overflow-x-auto text-xs leading-relaxed">
{JSON.stringify(result.rawData, null, 2)}
</pre>
</div>
)}
</div>
);
};
const renderTabContent = () => {
switch (activeTab) {
case 'basic':
return renderBasicInfo();
case 'listing':
return renderListing();
case 'fitment':
return renderFitment();
case 'pricing':
return renderPricing();
case 'raw':
return renderRawData();
}
};
/* ── Skeleton ──────────────────────────────────────────── */
const Skeleton = () => (
<div className="space-y-6 animate-pulse">
<div className="flex items-center gap-3 p-4 bg-blue-50 border border-blue-100 rounded-lg">
<SpinnerIcon />
<p className="text-sm text-blue-700 font-medium">AI가 ...</p>
</div>
<div className="space-y-3">
{[1, 2, 3].map((i) => (
<div key={i} className="bg-slate-100 rounded-lg h-20" />
))}
</div>
</div>
);
/* ── Render ────────────────────────────────────────────── */
return (
<div className="max-w-6xl mx-auto px-4 sm:px-6 py-8 space-y-8">
{/* ── Header ──────────────────────────────────────── */}
<div>
<div className="flex items-center gap-3 mb-2">
<h1 className="text-2xl font-bold text-slate-900"> AI </h1>
<span className="text-xs font-bold px-2 py-1 rounded bg-blue-100 text-blue-700 border border-blue-200">
MVP
</span>
</div>
<p className="text-slate-500 text-sm">
AI가
</p>
</div>
{/* ── Input Form ──────────────────────────────────── */}
<div className="bg-white rounded-xl border border-slate-200 shadow-sm p-5">
<div className="flex flex-col sm:flex-row gap-3">
<div className="flex-1">
<label className="block text-xs font-semibold text-slate-600 mb-1.5"> *</label>
<input
type="text"
value={partNumber}
onChange={(e) => setPartNumber(e.target.value)}
placeholder="예: 16610-0H040"
className="w-full px-3 py-2.5 text-sm border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500/30 focus:border-blue-500 transition text-slate-900 placeholder:text-slate-400"
onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
/>
</div>
<div className="flex-1">
<label className="block text-xs font-semibold text-slate-600 mb-1.5">
<span className="text-slate-400 font-normal">()</span>
</label>
<input
type="text"
value={partName}
onChange={(e) => setPartName(e.target.value)}
placeholder="예: Fuel Pump Assembly"
className="w-full px-3 py-2.5 text-sm border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500/30 focus:border-blue-500 transition text-slate-900 placeholder:text-slate-400"
onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
/>
</div>
<div className="flex items-end">
<button
onClick={() => handleSearch()}
disabled={loading}
className="flex items-center gap-2 px-5 py-2.5 bg-[#1a56db] text-white text-sm font-semibold rounded-lg hover:bg-blue-800 disabled:opacity-60 disabled:cursor-not-allowed transition-colors whitespace-nowrap"
>
{loading ? <SpinnerIcon /> : <SearchIcon />}
{loading ? '검색 중...' : '검색 시작'}
</button>
</div>
</div>
</div>
{/* ── Error ───────────────────────────────────────── */}
{error && (
<div className="bg-red-50 border border-red-200 rounded-lg p-4">
<p className="text-sm text-red-700 font-medium">{error}</p>
</div>
)}
{/* ── Loading ─────────────────────────────────────── */}
{loading && <Skeleton />}
{/* ── Result Tabs ─────────────────────────────────── */}
{result && !loading && (
<div className="bg-white rounded-xl border border-slate-200 shadow-sm overflow-hidden">
{/* Tabs */}
<div className="flex border-b border-slate-200 overflow-x-auto">
{TABS.map((tab) => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={`px-4 py-3 text-sm font-medium whitespace-nowrap transition-colors border-b-2 ${
activeTab === tab.id
? 'border-blue-600 text-blue-700 bg-blue-50/50'
: 'border-transparent text-slate-500 hover:text-slate-700 hover:bg-slate-50'
}`}
>
{tab.label}
</button>
))}
</div>
{/* Tab Content */}
<div className="p-5">{renderTabContent()}</div>
{/* Meta footer */}
<div className="border-t border-slate-100 px-5 py-3 flex flex-wrap gap-4 text-xs text-slate-400">
<span> : {new Date(result.meta.searchedAt).toLocaleString('ko-KR')}</span>
<span> : {result.meta.processingTime}</span>
<span>: {result.meta.sourcesChecked.join(', ')}</span>
<span>: {result.meta.aiModel}</span>
</div>
</div>
)}
{/* ── Search History ──────────────────────────────── */}
{history.length > 0 && (
<div className="bg-white rounded-xl border border-slate-200 shadow-sm p-5">
<div className="flex items-center gap-2 mb-3">
<ClockIcon />
<h3 className="text-sm font-semibold text-slate-700"> </h3>
</div>
<div className="space-y-2">
{history.map((item, i) => (
<button
key={i}
onClick={() => handleHistoryClick(item)}
className="w-full flex items-center justify-between px-3 py-2.5 rounded-lg bg-slate-50 hover:bg-slate-100 transition-colors text-left"
>
<div>
<span className="text-sm font-mono font-semibold text-slate-800">{item.partNumber}</span>
{item.partName && (
<span className="text-xs text-slate-500 ml-2">{item.partName}</span>
)}
</div>
<div className="flex items-center gap-3 text-xs text-slate-400">
<span>{item.resultSummary}</span>
<span>{item.time}</span>
</div>
</button>
))}
</div>
</div>
)}
</div>
);
}

View File

@@ -1,16 +0,0 @@
import type { Metadata } from 'next';
export const metadata: Metadata = {
title: '여긴 뭐 만들어요? — 자동화 도구 쇼케이스 | 쟁승메이드',
description:
'실제 고객 프로젝트 기반 자동화 도구를 직접 체험해보세요. 이베이 부품 AI 리스팅, 네이버 블로그 자동화 등 완성형 데모를 무료로 제공합니다.',
openGraph: {
title: '여긴 뭐 만들어요? — 자동화 도구 쇼케이스',
description:
'수작업 30분 → 10초. 실제로 작동하는 자동화 도구를 직접 체험해보세요.',
},
};
export default function ToolsLayout({ children }: { children: React.ReactNode }) {
return children;
}

View File

@@ -1,524 +0,0 @@
'use client';
import { useState, useCallback } from 'react';
import Link from 'next/link';
/* ── Types ─────────────────────────────────────────────── */
interface BlogSection {
heading: string;
body: string;
imageSlot?: boolean;
}
interface ImageGuide {
position: string;
description: string;
searchKeyword: string;
altText: string;
}
interface BlogData {
title: string;
subtitle: string;
content: BlogSection[];
tags: string[];
seoTitle: string;
seoDescription: string;
imageGuides: ImageGuide[];
meta: {
charCount: number;
sectionCount: number;
estimatedReadTime: string;
generatedAt: string;
model: string;
};
}
/* ── Option configs ────────────────────────────────────── */
const STYLES = [
{ value: 'informational', label: '정보 전달', desc: '사실 기반 정보 정리', icon: '📖' },
{ value: 'review', label: '리뷰/후기', desc: '제품·서비스 체험기', icon: '⭐' },
{ value: 'howto', label: '방법/튜토리얼', desc: '단계별 가이드', icon: '🔧' },
{ value: 'listicle', label: '리스트형', desc: 'OO가지 모음', icon: '📋' },
{ value: 'comparison', label: '비교 분석', desc: 'A vs B 비교', icon: '⚖️' },
{ value: 'story', label: '에세이/스토리', desc: '경험 기반 서사', icon: '✍️' },
];
const TONES = [
{ value: 'professional', label: '전문적', color: 'border-blue-500/40 bg-blue-500/10 text-blue-300' },
{ value: 'friendly', label: '친근한', color: 'border-emerald-500/40 bg-emerald-500/10 text-emerald-300' },
{ value: 'casual', label: '캐주얼', color: 'border-amber-500/40 bg-amber-500/10 text-amber-300' },
{ value: 'formal', label: '격식체', color: 'border-violet-500/40 bg-violet-500/10 text-violet-300' },
];
const LENGTHS = [
{ value: 'short', label: '짧게', desc: '800~1,200자', time: '~2분' },
{ value: 'medium', label: '보통', desc: '1,500~2,500자', time: '~5분' },
{ value: 'long', label: '길게', desc: '3,000~4,500자', time: '~9분' },
];
/* ── Component ─────────────────────────────────────────── */
export default function NaverBlogPage() {
// Form state
const [topic, setTopic] = useState('');
const [keywordInput, setKeywordInput] = useState('');
const [keywords, setKeywords] = useState<string[]>([]);
const [style, setStyle] = useState('informational');
const [tone, setTone] = useState('friendly');
const [length, setLength] = useState('medium');
const [sections, setSections] = useState(5);
const [imageGuide, setImageGuide] = useState(true);
// Result state
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [result, setResult] = useState<BlogData | null>(null);
const [viewMode, setViewMode] = useState<'preview' | 'seo' | 'image'>('preview');
const [copied, setCopied] = useState(false);
/* ── Keyword management ───────────────────────────────── */
const addKeyword = () => {
const kw = keywordInput.trim();
if (kw && !keywords.includes(kw) && keywords.length < 10) {
setKeywords([...keywords, kw]);
setKeywordInput('');
}
};
const removeKeyword = (kw: string) => {
setKeywords(keywords.filter((k) => k !== kw));
};
/* ── Generate ─────────────────────────────────────────── */
const handleGenerate = useCallback(async () => {
if (!topic.trim()) {
setError('주제를 입력해주세요.');
return;
}
if (keywords.length === 0) {
setError('키워드를 최소 1개 추가해주세요.');
return;
}
setLoading(true);
setError(null);
setResult(null);
try {
const res = await fetch('/api/tools/naver-blog/generate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ topic: topic.trim(), keywords, style, tone, length, imageGuide, sections }),
});
const json = await res.json();
if (!res.ok || !json.success) {
throw new Error(json.error || '생성에 실패했습니다.');
}
setResult(json.data);
} catch (err) {
setError(err instanceof Error ? err.message : '오류가 발생했습니다.');
} finally {
setLoading(false);
}
}, [topic, keywords, style, tone, length, imageGuide, sections]);
/* ── Copy full content ────────────────────────────────── */
const copyContent = () => {
if (!result) return;
const text = result.content.map((s) => `## ${s.heading}\n\n${s.body}`).join('\n\n');
const full = `# ${result.title}\n\n${result.subtitle}\n\n${text}\n\n태그: ${result.tags.map((t) => '#' + t).join(' ')}`;
navigator.clipboard.writeText(full);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
};
return (
<div className="min-h-screen">
{/* Header */}
<div className="px-6 pt-8 pb-6 max-w-5xl mx-auto">
<Link href="/tools" className="inline-flex items-center gap-1.5 text-slate-500 hover:text-slate-300 text-sm mb-4 transition-colors">
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
</svg>
</Link>
<div className="flex items-center gap-3 mb-2">
<div className="w-10 h-10 rounded-xl bg-gradient-to-br from-emerald-600 to-teal-500 flex items-center justify-center text-white">
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg>
</div>
<div>
<h1 className="text-white text-2xl font-bold"> </h1>
<p className="text-slate-500 text-xs font-mono">Naver Blog AI Writer</p>
</div>
</div>
</div>
<div className="px-6 pb-16 max-w-5xl mx-auto grid grid-cols-1 lg:grid-cols-5 gap-6">
{/* ── Left: Settings Panel ──────────────────────────── */}
<div className="lg:col-span-2 space-y-5">
{/* Topic */}
<div className="bg-slate-900/80 rounded-2xl border border-slate-700/50 p-5">
<label className="text-white text-sm font-semibold block mb-2"></label>
<input
type="text"
value={topic}
onChange={(e) => setTopic(e.target.value)}
placeholder="예: 2026년 제주도 가족여행 추천 코스"
className="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2.5 text-white text-sm placeholder-slate-500 focus:outline-none focus:border-blue-500/50"
maxLength={100}
/>
{/* Keywords */}
<label className="text-white text-sm font-semibold block mt-4 mb-2"> </label>
<div className="flex gap-2">
<input
type="text"
value={keywordInput}
onChange={(e) => setKeywordInput(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && (e.preventDefault(), addKeyword())}
placeholder="키워드 입력 후 Enter"
className="flex-1 bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white text-sm placeholder-slate-500 focus:outline-none focus:border-blue-500/50"
/>
<button
onClick={addKeyword}
className="px-3 py-2 bg-slate-700 hover:bg-slate-600 text-white text-sm rounded-lg transition-colors"
>
</button>
</div>
{keywords.length > 0 && (
<div className="flex flex-wrap gap-1.5 mt-2.5">
{keywords.map((kw) => (
<span key={kw} className="inline-flex items-center gap-1 px-2 py-0.5 rounded bg-blue-500/15 text-blue-300 text-xs">
{kw}
<button onClick={() => removeKeyword(kw)} className="hover:text-white ml-0.5">x</button>
</span>
))}
</div>
)}
</div>
{/* Style */}
<div className="bg-slate-900/80 rounded-2xl border border-slate-700/50 p-5">
<label className="text-white text-sm font-semibold block mb-3"> </label>
<div className="grid grid-cols-2 gap-2">
{STYLES.map((s) => (
<button
key={s.value}
onClick={() => setStyle(s.value)}
className={`text-left px-3 py-2.5 rounded-lg border transition-all text-sm ${
style === s.value
? 'border-blue-500/50 bg-blue-500/10 text-white'
: 'border-slate-700 bg-slate-800/50 text-slate-400 hover:border-slate-600'
}`}
>
<span className="text-base mr-1.5">{s.icon}</span>
<span className="font-medium">{s.label}</span>
<div className="text-[11px] text-slate-500 mt-0.5 ml-6">{s.desc}</div>
</button>
))}
</div>
</div>
{/* Tone */}
<div className="bg-slate-900/80 rounded-2xl border border-slate-700/50 p-5">
<label className="text-white text-sm font-semibold block mb-3"></label>
<div className="flex flex-wrap gap-2">
{TONES.map((t) => (
<button
key={t.value}
onClick={() => setTone(t.value)}
className={`px-4 py-2 rounded-lg border text-sm font-medium transition-all ${
tone === t.value ? t.color : 'border-slate-700 bg-slate-800/50 text-slate-400 hover:border-slate-600'
}`}
>
{t.label}
</button>
))}
</div>
</div>
{/* Length + Sections + Image */}
<div className="bg-slate-900/80 rounded-2xl border border-slate-700/50 p-5 space-y-4">
<div>
<label className="text-white text-sm font-semibold block mb-3"></label>
<div className="grid grid-cols-3 gap-2">
{LENGTHS.map((l) => (
<button
key={l.value}
onClick={() => setLength(l.value)}
className={`px-3 py-2.5 rounded-lg border text-center transition-all ${
length === l.value
? 'border-blue-500/50 bg-blue-500/10 text-white'
: 'border-slate-700 bg-slate-800/50 text-slate-400 hover:border-slate-600'
}`}
>
<div className="text-sm font-medium">{l.label}</div>
<div className="text-[11px] text-slate-500">{l.desc}</div>
</button>
))}
</div>
</div>
<div className="flex items-center justify-between">
<div>
<label className="text-white text-sm font-semibold"> </label>
<p className="text-slate-500 text-xs">3~8</p>
</div>
<div className="flex items-center gap-2">
<button
onClick={() => setSections(Math.max(3, sections - 1))}
className="w-8 h-8 rounded-lg bg-slate-800 border border-slate-700 text-white flex items-center justify-center hover:bg-slate-700"
>
-
</button>
<span className="text-white font-bold w-6 text-center">{sections}</span>
<button
onClick={() => setSections(Math.min(8, sections + 1))}
className="w-8 h-8 rounded-lg bg-slate-800 border border-slate-700 text-white flex items-center justify-center hover:bg-slate-700"
>
+
</button>
</div>
</div>
<label className="flex items-center gap-3 cursor-pointer">
<input
type="checkbox"
checked={imageGuide}
onChange={(e) => setImageGuide(e.target.checked)}
className="w-4 h-4 rounded accent-blue-500"
/>
<div>
<span className="text-white text-sm font-medium"> </span>
<p className="text-slate-500 text-xs"> </p>
</div>
</label>
</div>
{/* Generate Button */}
<button
onClick={handleGenerate}
disabled={loading}
className="w-full py-3.5 rounded-xl bg-emerald-600 hover:bg-emerald-500 disabled:bg-slate-700 text-white font-semibold text-sm transition-colors flex items-center justify-center gap-2"
>
{loading ? (
<>
<svg className="w-4 h-4 animate-spin" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
</svg>
AI ...
</>
) : (
<>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
</>
)}
</button>
{error && (
<div className="bg-red-900/20 border border-red-500/30 rounded-lg px-4 py-3 text-red-400 text-sm">
{error}
</div>
)}
</div>
{/* ── Right: Result Panel ───────────────────────────── */}
<div className="lg:col-span-3">
{!result && !loading && (
<div className="bg-slate-900/80 rounded-2xl border border-slate-700/50 p-12 text-center">
<svg className="w-16 h-16 text-slate-700 mx-auto mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1} d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg>
<p className="text-slate-500 text-sm"> <br /> </p>
</div>
)}
{loading && (
<div className="bg-slate-900/80 rounded-2xl border border-slate-700/50 p-12 text-center">
<svg className="w-10 h-10 animate-spin text-emerald-400 mx-auto mb-4" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
</svg>
<p className="text-white font-medium mb-1">AI가 </p>
<p className="text-slate-500 text-sm"> SEO </p>
</div>
)}
{result && (
<div className="space-y-4">
{/* View mode tabs */}
<div className="flex gap-1 bg-slate-900/80 rounded-xl border border-slate-700/50 p-1">
{[
{ id: 'preview' as const, label: '글 미리보기' },
{ id: 'seo' as const, label: 'SEO 정보' },
{ id: 'image' as const, label: '이미지 가이드' },
].map((tab) => (
<button
key={tab.id}
onClick={() => setViewMode(tab.id)}
className={`flex-1 py-2 rounded-lg text-sm font-medium transition-all ${
viewMode === tab.id
? 'bg-slate-700 text-white'
: 'text-slate-500 hover:text-slate-300'
}`}
>
{tab.label}
</button>
))}
</div>
{/* Meta bar */}
<div className="flex items-center justify-between px-4 py-2 bg-slate-800/60 rounded-lg text-xs text-slate-400">
<div className="flex gap-4">
<span>{result.meta.charCount.toLocaleString()}</span>
<span>{result.meta.sectionCount} </span>
<span> {result.meta.estimatedReadTime}</span>
</div>
<div className="flex gap-2">
<button
onClick={copyContent}
className="px-3 py-1 rounded bg-slate-700 hover:bg-slate-600 text-slate-300 transition-colors"
>
{copied ? '복사됨!' : '전체 복사'}
</button>
</div>
</div>
{/* Content based on viewMode */}
{viewMode === 'preview' && (
<div className="bg-white rounded-2xl border border-slate-200 overflow-hidden">
{/* Blog preview (light theme to mimic Naver Blog) */}
<div className="p-6 md:p-8">
<h1 className="text-[#333] text-2xl font-bold mb-2 leading-tight">{result.title}</h1>
<p className="text-[#888] text-sm mb-6">{result.subtitle}</p>
<div className="w-12 h-0.5 bg-emerald-500 mb-6" />
{result.content.map((section, idx) => (
<div key={idx} className="mb-6">
<h2 className="text-[#222] text-lg font-bold mb-3 flex items-center gap-2">
<span className="w-1.5 h-5 bg-emerald-500 rounded-full inline-block" />
{section.heading}
</h2>
{section.imageSlot && (
<div className="w-full h-32 bg-slate-100 rounded-lg mb-3 flex items-center justify-center border border-dashed border-slate-300">
<span className="text-slate-400 text-sm">
</span>
</div>
)}
<div className="text-[#333] text-sm leading-relaxed whitespace-pre-wrap">{section.body}</div>
</div>
))}
{/* Tags */}
<div className="pt-4 border-t border-slate-200 flex flex-wrap gap-1.5">
{result.tags.map((tag) => (
<span key={tag} className="px-2.5 py-1 bg-emerald-50 text-emerald-700 text-xs rounded-full">
#{tag}
</span>
))}
</div>
</div>
</div>
)}
{viewMode === 'seo' && (
<div className="bg-slate-900/80 rounded-2xl border border-slate-700/50 p-6 space-y-4">
<div>
<label className="text-slate-400 text-xs font-medium block mb-1">SEO </label>
<p className="text-white text-sm bg-slate-800 rounded-lg px-4 py-3">{result.seoTitle}</p>
</div>
<div>
<label className="text-slate-400 text-xs font-medium block mb-1"> </label>
<p className="text-white text-sm bg-slate-800 rounded-lg px-4 py-3">{result.seoDescription}</p>
</div>
<div>
<label className="text-slate-400 text-xs font-medium block mb-1"> </label>
<div className="flex flex-wrap gap-1.5">
{result.tags.map((tag) => (
<span key={tag} className="px-3 py-1.5 bg-slate-800 rounded-lg text-emerald-400 text-sm">
#{tag}
</span>
))}
</div>
</div>
<div>
<label className="text-slate-400 text-xs font-medium block mb-1"> </label>
<div className="bg-slate-800 rounded-lg px-4 py-3 space-y-1.5">
{result.content.map((section, idx) => (
<div key={idx} className="flex items-center gap-2 text-sm">
<span className="text-slate-500 font-mono text-xs w-5">H2</span>
<span className="text-white">{section.heading}</span>
<span className="text-slate-600 text-xs ml-auto">{section.body.length}</span>
</div>
))}
</div>
</div>
<div className="text-slate-500 text-xs pt-2 border-t border-slate-700/50">
: {result.meta.model} · : {new Date(result.meta.generatedAt).toLocaleString('ko-KR')}
</div>
</div>
)}
{viewMode === 'image' && (
<div className="bg-slate-900/80 rounded-2xl border border-slate-700/50 p-6">
{result.imageGuides.length === 0 ? (
<p className="text-slate-500 text-sm text-center py-8"> .</p>
) : (
<div className="space-y-3">
{result.imageGuides.map((guide, idx) => (
<div key={idx} className="bg-slate-800/50 rounded-lg p-4">
<div className="flex items-center gap-2 mb-2">
<span className="w-6 h-6 rounded-full bg-emerald-500/20 text-emerald-400 text-xs flex items-center justify-center font-bold">
{idx + 1}
</span>
<span className="text-white text-sm font-medium">{guide.position}</span>
</div>
<p className="text-slate-300 text-sm mb-2">{guide.description}</p>
<div className="flex gap-4 text-xs">
<span className="text-slate-500">
: <span className="text-blue-400">{guide.searchKeyword}</span>
</span>
<span className="text-slate-500">
Alt: <span className="text-slate-400">{guide.altText}</span>
</span>
</div>
</div>
))}
</div>
)}
</div>
)}
{/* CTA */}
<div className="bg-slate-900/60 rounded-xl border border-slate-700/40 p-5 text-center">
<p className="text-slate-400 text-sm mb-3">
?
</p>
<Link
href="/freelance#contact-form"
className="inline-flex items-center gap-2 px-5 py-2.5 rounded-lg bg-blue-600 hover:bg-blue-500 text-white text-sm font-semibold transition-colors"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 8l4 4m0 0l-4 4m4-4H3" />
</svg>
</Link>
</div>
</div>
)}
</div>
</div>
</div>
);
}

View File

@@ -1,383 +0,0 @@
'use client';
import { useEffect, useRef } from 'react';
import Link from 'next/link';
import { trackToolDemo, trackCTAClick } from '../../lib/gtag';
/* ═══════════════════════════════════════════════════
도구 쇼케이스 — 리디자인 v2
설계 원칙:
1. 홈 페이지 에디토리얼 톤 계승 — 증거 중심, 텍스트 우선
2. Supanova: 비대칭 레이아웃, 스크롤 애니메이션, 프리미엄 카드
3. 사이트 디자인 시스템 완전 통일 (라이트 bg + 다크 카드)
4. 실제 수치와 체험 유도 — 전환율 중심 구조
═══════════════════════════════════════════════════ */
interface ToolCard {
id: string;
title: string;
subtitle: string;
description: string;
tags: string[];
href: string;
status: 'live' | 'beta' | 'coming';
gradient: string;
iconPath: string;
metric: { value: string; label: string };
highlight: string;
}
const TOOLS: ToolCard[] = [
{
id: 'ebay-parts',
title: '이베이 부품 AI 리스팅',
subtitle: 'eBay Auto Parts Listing Tool',
description:
'품번 하나 입력하면 AI가 RockAuto·eBay를 크롤링하고, 리스팅 제목·Fitment·관세까지 자동 생성합니다.',
tags: ['크롤링', 'Claude AI', '관세 계산', 'eBay Motors'],
href: '/tools/ebay-parts',
status: 'live',
gradient: 'from-blue-600 to-cyan-500',
iconPath: 'M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z',
metric: { value: '10초', label: '30분 작업 → 10초로 단축' },
highlight: '수작업 대비 180배 빠름',
},
{
id: 'naver-blog',
title: '네이버 블로그 자동화',
subtitle: 'Naver Blog AI Writer',
description:
'주제·톤·분량만 선택하면 AI가 SEO 최적화된 블로그 글을 자동 작성합니다. 소제목 구조, 이미지 배치 가이드까지.',
tags: ['GPT/Claude', 'SEO 최적화', '자동 포스팅', '이미지 가이드'],
href: '/tools/naver-blog',
status: 'live',
gradient: 'from-emerald-600 to-teal-500',
iconPath: 'M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z',
metric: { value: '3분', label: '1시간 글쓰기 → 3분 자동 완성' },
highlight: 'SEO 최적화 자동 포함',
},
];
const STATUS_BADGE: Record<string, { label: string; className: string }> = {
live: { label: '체험 가능', className: 'bg-emerald-50 text-emerald-700 border-emerald-200' },
beta: { label: 'BETA', className: 'bg-amber-50 text-amber-700 border-amber-200' },
coming: { label: '준비 중', className: 'bg-slate-100 text-slate-500 border-slate-200' },
};
const PROCESS_STEPS = [
{ step: '01', title: '문제 정의', desc: '고객의 반복 업무를 분석합니다' },
{ step: '02', title: '자동화 설계', desc: 'AI + 크롤링 + API 조합을 설계합니다' },
{ step: '03', title: '프로토타입', desc: '실제 데이터로 동작하는 MVP를 만듭니다' },
{ step: '04', title: '체험 배포', desc: '이 페이지에 데모를 올려 직접 테스트합니다' },
];
function useScrollReveal() {
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
const el = ref.current;
if (!el) return;
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.classList.add('is-visible');
observer.unobserve(entry.target);
}
});
},
{ threshold: 0.15, rootMargin: '0px 0px -40px 0px' }
);
const children = el.querySelectorAll('.reveal');
children.forEach((child) => observer.observe(child));
return () => observer.disconnect();
}, []);
return ref;
}
export default function ToolsShowcasePage() {
const containerRef = useScrollReveal();
return (
<div ref={containerRef} className="min-h-screen">
{/* ── Scroll-reveal animation styles ── */}
<style>{`
.reveal {
opacity: 0;
transform: translateY(1.5rem);
transition: opacity 0.7s cubic-bezier(0.16, 1, 0.3, 1),
transform 0.7s cubic-bezier(0.16, 1, 0.3, 1);
}
.reveal.is-visible {
opacity: 1;
transform: translateY(0);
}
.reveal-delay-1 { transition-delay: 80ms; }
.reveal-delay-2 { transition-delay: 160ms; }
.reveal-delay-3 { transition-delay: 240ms; }
.reveal-delay-4 { transition-delay: 320ms; }
.tool-card {
transition: transform 0.4s cubic-bezier(0.16, 1, 0.3, 1),
box-shadow 0.4s cubic-bezier(0.16, 1, 0.3, 1);
}
.tool-card:hover {
transform: translateY(-6px);
box-shadow: 0 24px 48px -12px rgba(26, 86, 219, 0.12),
0 8px 16px -4px rgba(0, 0, 0, 0.06);
}
.metric-pulse {
animation: pulse-ring 2.5s cubic-bezier(0.16, 1, 0.3, 1) infinite;
}
@keyframes pulse-ring {
0%, 100% { box-shadow: 0 0 0 0 rgba(26, 86, 219, 0.15); }
50% { box-shadow: 0 0 0 12px rgba(26, 86, 219, 0); }
}
.arrow-shift {
transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1);
}
.group:hover .arrow-shift {
transform: translateX(4px);
}
`}</style>
{/* ═══════════════════════════════════════════
HERO — 좌측 텍스트 / 우측 수치 비대칭 레이아웃
═══════════════════════════════════════════ */}
<section className="px-6 pt-10 pb-16 max-w-5xl mx-auto">
<div className="grid grid-cols-1 lg:grid-cols-5 gap-10 items-center">
{/* 좌측: 텍스트 블록 */}
<div className="lg:col-span-3 reveal">
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-blue-50 border border-blue-200 text-blue-700 text-xs font-medium mb-5">
<span className="w-1.5 h-1.5 rounded-full bg-emerald-500 animate-pulse" />
</div>
<h1 className="text-3xl md:text-4xl font-bold text-slate-900 leading-tight mb-4" style={{ wordBreak: 'keep-all' as const }}>
?
</h1>
<p className="text-slate-500 text-base md:text-lg leading-relaxed max-w-lg" style={{ wordBreak: 'keep-all' as const }}>
&ldquo; ?&rdquo; .
<span className="text-slate-800 font-semibold"> </span> .
</p>
</div>
{/* 우측: 수치 카드 */}
<div className="lg:col-span-2 reveal reveal-delay-1">
<div className="bg-white rounded-2xl border border-slate-200 p-6 shadow-sm">
<div className="grid grid-cols-2 gap-4">
<div className="text-center p-3">
<div className="text-2xl font-bold text-blue-600">{TOOLS.filter(t => t.status === 'live').length}</div>
<div className="text-slate-400 text-xs mt-1"> </div>
</div>
<div className="text-center p-3">
<div className="text-2xl font-bold text-emerald-600">180x</div>
<div className="text-slate-400 text-xs mt-1"> </div>
</div>
<div className="text-center p-3">
<div className="text-2xl font-bold text-violet-600"></div>
<div className="text-slate-400 text-xs mt-1"> </div>
</div>
<div className="text-center p-3">
<div className="text-2xl font-bold text-cyan-600"></div>
<div className="text-slate-400 text-xs mt-1"> </div>
</div>
</div>
</div>
</div>
</div>
</section>
{/* ═══════════════════════════════════════════
TOOL CARDS — 피처드 카드 (풀와이드) 패턴
═══════════════════════════════════════════ */}
<section className="px-6 pb-16 max-w-5xl mx-auto">
<div className="flex flex-col gap-6">
{TOOLS.map((tool, idx) => {
const badge = STATUS_BADGE[tool.status];
const isEven = idx % 2 === 1;
return (
<Link
key={tool.id}
href={tool.href}
onClick={() => trackToolDemo(tool.title)}
className={`group block tool-card bg-white rounded-2xl border border-slate-200 overflow-hidden active:scale-[0.99] reveal reveal-delay-${idx + 1}`}
>
<div className={`flex flex-col ${isEven ? 'md:flex-row-reverse' : 'md:flex-row'}`}>
{/* 좌측 (또는 우측): 메트릭 비주얼 */}
<div className={`relative flex-shrink-0 md:w-64 lg:w-72 p-4 md:p-8 flex items-center md:flex-col md:justify-center bg-gradient-to-br ${tool.gradient} text-white`}>
{/* 배경 패턴 (모바일 숨김) */}
<div className="absolute inset-0 opacity-10 hidden md:block">
<div className="absolute top-4 right-4 w-32 h-32 border border-white/30 rounded-full" />
<div className="absolute bottom-4 left-4 w-20 h-20 border border-white/20 rounded-full" />
</div>
{/* 모바일: 수평 compact / 데스크톱: 수직 */}
<div className="relative z-10 flex items-center gap-4 md:flex-col md:text-center">
<div className="w-11 h-11 md:w-14 md:h-14 rounded-xl md:rounded-2xl bg-white/20 flex items-center justify-center flex-shrink-0 border border-white/10 shadow-[inset_0_1px_0_rgba(255,255,255,0.2)]">
<svg className="w-5 h-5 md:w-7 md:h-7" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d={tool.iconPath} />
</svg>
</div>
<div className="flex items-center gap-3 md:flex-col">
<div className="metric-pulse inline-flex items-center justify-center w-14 h-14 md:w-20 md:h-20 rounded-full bg-white/15 border border-white/20">
<span className="text-lg md:text-2xl font-bold">{tool.metric.value}</span>
</div>
<p className="text-white/80 text-xs leading-relaxed">{tool.metric.label}</p>
</div>
</div>
</div>
{/* 우측 (또는 좌측): 콘텐츠 */}
<div className="flex-1 p-6 md:p-8 flex flex-col justify-between">
<div>
{/* 상단: 배지 + 하이라이트 */}
<div className="flex items-center gap-2 mb-4 flex-wrap">
<span className={`px-2.5 py-0.5 rounded-full text-[11px] font-semibold border ${badge.className}`}>
{badge.label}
</span>
<span className="px-2.5 py-0.5 rounded-full text-[11px] font-medium bg-blue-50 text-blue-600 border border-blue-100">
{tool.highlight}
</span>
</div>
{/* 제목 */}
<h3 className="text-xl font-bold text-slate-900 mb-1 group-hover:text-blue-700 transition-colors duration-300">
{tool.title}
</h3>
<p className="text-slate-400 text-xs font-mono mb-3 tracking-wide">{tool.subtitle}</p>
{/* 설명 */}
<p className="text-slate-500 text-sm leading-relaxed mb-5" style={{ wordBreak: 'keep-all' as const }}>
{tool.description}
</p>
{/* 태그 */}
<div className="flex flex-wrap gap-1.5 mb-5">
{tool.tags.map((tag) => (
<span
key={tag}
className="px-2.5 py-1 rounded-lg bg-slate-50 text-slate-500 text-[11px] font-medium border border-slate-100"
>
{tag}
</span>
))}
</div>
</div>
{/* CTA */}
<div className="flex items-center gap-2 text-blue-600 text-sm font-semibold">
<svg className="w-4 h-4 arrow-shift" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 8l4 4m0 0l-4 4m4-4H3" />
</svg>
</div>
</div>
</div>
</Link>
);
})}
</div>
</section>
{/* ═══════════════════════════════════════════
PROCESS — 어떻게 만들어지나? (수직 타임라인)
═══════════════════════════════════════════ */}
<section className="px-6 pb-16 max-w-5xl mx-auto">
<div className="reveal mb-10">
<h2 className="text-2xl font-bold text-slate-900 mb-2"> </h2>
<p className="text-slate-400 text-sm"> 4</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-4 gap-0 md:gap-6">
{PROCESS_STEPS.map((item, idx) => (
<div key={item.step} className={`reveal reveal-delay-${idx + 1} relative`}>
{/* 스텝 카드 */}
<div className="bg-white rounded-xl border border-slate-200 p-5 h-full hover:border-blue-200 transition-colors duration-300">
<div className="flex items-center gap-3 mb-3">
<span className="w-8 h-8 rounded-lg bg-blue-50 text-blue-600 text-xs font-bold flex items-center justify-center border border-blue-100">
{item.step}
</span>
{idx < PROCESS_STEPS.length - 1 && (
<div className="hidden md:block absolute -right-3 top-1/2 -translate-y-1/2 z-10">
<svg className="w-6 h-6 text-slate-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M9 5l7 7-7 7" />
</svg>
</div>
)}
</div>
<h4 className="text-slate-900 font-bold text-sm mb-1">{item.title}</h4>
<p className="text-slate-400 text-xs leading-relaxed">{item.desc}</p>
</div>
</div>
))}
</div>
</section>
{/* ═══════════════════════════════════════════
BOTTOM CTA — 풀블리드 전환 섹션
═══════════════════════════════════════════ */}
<section className="px-6 pb-16 max-w-5xl mx-auto">
<div className="reveal relative overflow-hidden rounded-2xl bg-slate-900 p-8 md:p-12">
{/* 배경 데코 */}
<div className="absolute inset-0 pointer-events-none">
<div className="absolute -top-20 -right-20 w-60 h-60 bg-blue-500/10 rounded-full blur-3xl" />
<div className="absolute -bottom-20 -left-20 w-48 h-48 bg-violet-500/10 rounded-full blur-3xl" />
</div>
<div className="relative z-10 flex flex-col md:flex-row items-start md:items-center justify-between gap-6">
<div>
<h3 className="text-white font-bold text-xl md:text-2xl mb-2" style={{ wordBreak: 'keep-all' as const }}>
?
</h3>
<p className="text-slate-400 text-sm leading-relaxed max-w-lg" style={{ wordBreak: 'keep-all' as const }}>
. , .
</p>
</div>
<Link
href="/freelance#contact-form"
onClick={() => trackCTAClick('무료 상담 신청하기', '/tools')}
className="group inline-flex items-center gap-3 px-7 py-4 rounded-xl bg-blue-600 hover:bg-blue-500 active:scale-[0.98] text-white text-sm font-bold transition-all duration-300 shadow-lg shadow-blue-600/20 hover:shadow-blue-500/30 flex-shrink-0"
>
<span className="w-7 h-7 rounded-full bg-white/15 flex items-center justify-center flex-shrink-0">
<svg className="w-3.5 h-3.5 arrow-shift" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M17 8l4 4m0 0l-4 4m4-4H3" />
</svg>
</span>
</Link>
</div>
{/* 하단 신뢰 요소 */}
<div className="relative z-10 mt-8 pt-6 border-t border-slate-700/50 flex flex-wrap gap-x-8 gap-y-2">
<div className="flex items-center gap-2 text-slate-500 text-xs">
<svg className="w-3.5 h-3.5 text-emerald-400" fill="currentColor" viewBox="0 0 24 24">
<path d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<div className="flex items-center gap-2 text-slate-500 text-xs">
<svg className="w-3.5 h-3.5 text-emerald-400" fill="currentColor" viewBox="0 0 24 24">
<path d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<div className="flex items-center gap-2 text-slate-500 text-xs">
<svg className="w-3.5 h-3.5 text-emerald-400" fill="currentColor" viewBox="0 0 24 24">
<path d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
</div>
</div>
</section>
</div>
);
}

View File

@@ -49,7 +49,6 @@ export function formatLottoMessage(
`📊 합계: ${numbers.reduce((a, b) => a + b, 0)} | 홀수: ${numbers.filter((n) => n % 2 !== 0).length}`, `📊 합계: ${numbers.reduce((a, b) => a + b, 0)} | 홀수: ${numbers.filter((n) => n % 2 !== 0).length}`,
``, ``,
`⚠️ 통계 기반 추천이며 당첨을 보장하지 않습니다.`, `⚠️ 통계 기반 추천이며 당첨을 보장하지 않습니다.`,
`🔗 [번호 추천 받기](https://jaengseung.com/services/lotto/recommend)`,
].join('\n'); ].join('\n');
} }