feat: 마케팅 에셋 전면 재설계 + 어드민 체크리스트 추가
- 어드민 마케팅 페이지: 4대 전문가 체크리스트(디자인/PM/품질/마케팅) 추가, localStorage 저장, 크몽 등록 가이드 패널, 품질 점수 바 - SVG 썸네일 6개 전면 재설계: 헤드라인 강화, 목업 세밀화, 실제 제품가 반영 - 신규 썸네일 2개 제작: thumb-lotto.svg (로또 번호 추천), thumb-saju.svg (AI 사주 분석) - 사이드바 상호명 추가 (토스페이먼츠 심사 준비) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,16 +1,17 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
|
||||
const ASSETS = [
|
||||
{
|
||||
file: '/marketing/thumb-homepage-A.svg',
|
||||
name: '홈페이지 제작 썸네일 A',
|
||||
desc: '신뢰형 — 브라우저 목업 포함 다크 테마',
|
||||
desc: '신뢰형 — 브라우저 목업 + 경력 강조',
|
||||
size: '1200 × 675',
|
||||
platform: '크몽 메인',
|
||||
color: '#2563eb',
|
||||
service: '홈페이지 제작',
|
||||
price: '스타터 50만원~',
|
||||
},
|
||||
{
|
||||
file: '/marketing/thumb-homepage-B.svg',
|
||||
@@ -19,6 +20,8 @@ const ASSETS = [
|
||||
size: '1200 × 675',
|
||||
platform: '크몽 서브',
|
||||
color: '#7c3aed',
|
||||
service: '홈페이지 제작',
|
||||
price: '스타터 50만원~',
|
||||
},
|
||||
{
|
||||
file: '/marketing/thumb-automation.svg',
|
||||
@@ -27,14 +30,18 @@ const ASSETS = [
|
||||
size: '1200 × 675',
|
||||
platform: '크몽 메인',
|
||||
color: '#10b981',
|
||||
service: '업무 자동화',
|
||||
price: '33만원~',
|
||||
},
|
||||
{
|
||||
file: '/marketing/thumb-prompt.svg',
|
||||
name: '프롬프트 엔지니어링 썸네일',
|
||||
desc: 'Before/After 말풍선 비교형',
|
||||
desc: 'Before/After 비교형 — AI 최적화 결과 시각화',
|
||||
size: '1200 × 675',
|
||||
platform: '크몽 메인',
|
||||
color: '#7c3aed',
|
||||
service: '프롬프트 엔지니어링',
|
||||
price: '10만원~',
|
||||
},
|
||||
{
|
||||
file: '/marketing/thumb-stock.svg',
|
||||
@@ -43,6 +50,28 @@ const ASSETS = [
|
||||
size: '1200 × 675',
|
||||
platform: '크몽 메인',
|
||||
color: '#22c55e',
|
||||
service: '주식 자동매매',
|
||||
price: '9만9천원~',
|
||||
},
|
||||
{
|
||||
file: '/marketing/thumb-lotto.svg',
|
||||
name: '로또 번호 추천 썸네일',
|
||||
desc: '빅데이터 분석형 — 번호 통계 시각화',
|
||||
size: '1200 × 675',
|
||||
platform: '크몽 메인',
|
||||
color: '#f59e0b',
|
||||
service: '로또 번호 추천',
|
||||
price: '900원~/월',
|
||||
},
|
||||
{
|
||||
file: '/marketing/thumb-saju.svg',
|
||||
name: 'AI 사주 분석 썸네일',
|
||||
desc: '사주팔자 + AI 해석 — 전통+현대 비주얼',
|
||||
size: '1200 × 675',
|
||||
platform: '크몽 메인',
|
||||
color: '#8b5cf6',
|
||||
service: 'AI 사주 분석',
|
||||
price: '4,900원',
|
||||
},
|
||||
{
|
||||
file: '/marketing/banner-homepage.svg',
|
||||
@@ -51,12 +80,80 @@ const ASSETS = [
|
||||
size: '1200 × 400',
|
||||
platform: '블로그/SNS',
|
||||
color: '#2563eb',
|
||||
service: '홈페이지 제작',
|
||||
price: '스타터 50만원~',
|
||||
},
|
||||
];
|
||||
|
||||
const CHECKLIST_ITEMS = {
|
||||
design: [
|
||||
'시각적 위계가 명확하다 (헤드라인 → 서브 → 기능 → 가격)',
|
||||
'색상 대비가 가독성 기준을 충족한다 (어두운 배경/밝은 텍스트)',
|
||||
'브랜드 색상이 사이트와 일관되게 사용되었다',
|
||||
'정보가 과밀하지 않고 여백이 충분하다',
|
||||
'폰트 크기가 썸네일 목록에서도 가독성이 있다 (헤드 52px+)',
|
||||
'오른쪽 비주얼(목업)이 서비스 내용과 직결된다',
|
||||
],
|
||||
pm: [
|
||||
'서비스명이 한눈에 들어온다 (1초 이내 파악)',
|
||||
'핵심 가치 제안이 1~2줄 이내로 명확히 전달된다',
|
||||
'가격 또는 플랜이 뱃지 형태로 명확히 표시된다',
|
||||
'URL 또는 브랜드명이 하단에 포함된다',
|
||||
'대상 고객의 니즈가 암묵적으로 전달된다',
|
||||
'파일 사이즈가 플랫폼 요구사항(1200×675)을 충족한다',
|
||||
],
|
||||
quality: [
|
||||
'텍스트에 오탈자·맞춤법 오류가 없다',
|
||||
'가격 정보가 실제 서비스 가격과 일치한다',
|
||||
'깨진 이미지나 렌더링 오류가 없다',
|
||||
'동일 색상/레이아웃을 다른 썸네일과 중복 사용하지 않는다',
|
||||
'법적 문제(허위광고, 저작권) 소지가 없다',
|
||||
'PNG 변환 후에도 품질이 유지된다 (벡터 기반)',
|
||||
],
|
||||
marketing: [
|
||||
'타겟 고객의 핵심 페인포인트를 헤드라인에서 직접 해소한다',
|
||||
'"7년차 대기업 개발자" 등 차별화 신뢰 요소가 포함된다',
|
||||
'경쟁사 대비 명확한 차별점이 드러난다',
|
||||
'첫 3초 안에 무슨 서비스인지 파악 가능하다',
|
||||
'클릭 충동을 자극하는 강력한 헤드라인이다',
|
||||
'크몽 검색 목록에서 눈에 띄는 디자인이다',
|
||||
],
|
||||
};
|
||||
|
||||
type CheckKey = string;
|
||||
|
||||
export default function MarketingPage() {
|
||||
const [preview, setPreview] = useState<(typeof ASSETS)[0] | null>(null);
|
||||
const [preview, setPreview] = useState<typeof ASSETS[0] | null>(null);
|
||||
const [copied, setCopied] = useState<string | null>(null);
|
||||
const [checks, setChecks] = useState<Record<CheckKey, boolean>>({});
|
||||
const [showGuide, setShowGuide] = useState(false);
|
||||
const [activeTab, setActiveTab] = useState<'design' | 'pm' | 'quality' | 'marketing'>('design');
|
||||
|
||||
useEffect(() => {
|
||||
const saved = localStorage.getItem('marketing_checks');
|
||||
if (saved) setChecks(JSON.parse(saved));
|
||||
}, []);
|
||||
|
||||
const toggleCheck = useCallback((key: string) => {
|
||||
setChecks(prev => {
|
||||
const next = { ...prev, [key]: !prev[key] };
|
||||
localStorage.setItem('marketing_checks', JSON.stringify(next));
|
||||
return next;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const getCheckScore = (assetFile: string, category: keyof typeof CHECKLIST_ITEMS) => {
|
||||
const items = CHECKLIST_ITEMS[category];
|
||||
const done = items.filter((_, i) => checks[`${assetFile}_${category}_${i}`]).length;
|
||||
return { done, total: items.length };
|
||||
};
|
||||
|
||||
const getTotalScore = (assetFile: string) => {
|
||||
const all = Object.keys(CHECKLIST_ITEMS).flatMap(cat =>
|
||||
CHECKLIST_ITEMS[cat as keyof typeof CHECKLIST_ITEMS].map((_, i) => checks[`${assetFile}_${cat}_${i}`])
|
||||
);
|
||||
return { done: all.filter(Boolean).length, total: all.length };
|
||||
};
|
||||
|
||||
function copyPath(file: string) {
|
||||
const url = `${window.location.origin}${file}`;
|
||||
@@ -72,108 +169,345 @@ export default function MarketingPage() {
|
||||
a.click();
|
||||
}
|
||||
|
||||
const TABS = [
|
||||
{ key: 'design', label: '디자인', icon: '🎨', color: 'blue' },
|
||||
{ key: 'pm', label: 'PM', icon: '📋', color: 'violet' },
|
||||
{ key: 'quality', label: '품질', icon: '✅', color: 'emerald' },
|
||||
{ key: 'marketing', label: '마케팅', icon: '📣', color: 'amber' },
|
||||
] as const;
|
||||
|
||||
const tabColors: Record<string, string> = {
|
||||
blue: 'bg-blue-500/20 text-blue-300 border-blue-500/50',
|
||||
violet: 'bg-violet-500/20 text-violet-300 border-violet-500/50',
|
||||
emerald: 'bg-emerald-500/20 text-emerald-300 border-emerald-500/50',
|
||||
amber: 'bg-amber-500/20 text-amber-300 border-amber-500/50',
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-8">
|
||||
<div className="flex items-center justify-between mb-8">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-white">마케팅 에셋</h1>
|
||||
<p className="text-slate-400 text-sm mt-1">크몽/숨고 등록용 썸네일 및 배너 — SVG 파일 다운로드 가능</p>
|
||||
<div className="p-8 max-w-[1400px]">
|
||||
{/* 헤더 */}
|
||||
<div className="mb-8">
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-white mb-1">마케팅 에셋</h1>
|
||||
<p className="text-slate-400 text-sm">크몽·숨고 등록용 썸네일 및 배너 — 4대 전문가 품질 체크리스트 포함</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setShowGuide(v => !v)}
|
||||
className="flex items-center gap-2 px-4 py-2 rounded-lg bg-slate-800 hover:bg-slate-700 text-slate-300 text-sm transition-all"
|
||||
>
|
||||
<span>📖</span>
|
||||
{showGuide ? '가이드 닫기' : '등록 가이드 보기'}
|
||||
</button>
|
||||
</div>
|
||||
<Link href="/admin/dashboard" className="text-slate-400 hover:text-white text-sm transition-colors">
|
||||
← 대시보드
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* 안내 */}
|
||||
<div className="bg-blue-900/20 border border-blue-500/30 rounded-xl p-4 mb-8 flex items-start gap-3">
|
||||
<span className="text-blue-400 text-xl mt-0.5">ℹ️</span>
|
||||
<div>
|
||||
<p className="text-blue-300 font-semibold text-sm mb-1">SVG → PNG 변환 방법</p>
|
||||
<p className="text-slate-400 text-sm">브라우저에서 파일 열기 → 우클릭 → "이미지 다른 이름으로 저장" (PNG), 또는 <strong className="text-slate-300">Figma에 SVG 드래그 후 PNG Export</strong>를 추천합니다. 크몽은 JPG/PNG만 허용합니다.</p>
|
||||
{/* 통계 */}
|
||||
<div className="grid grid-cols-4 gap-4 mt-6">
|
||||
{[
|
||||
{ label: '전체 에셋', value: ASSETS.length, unit: '개', color: 'text-white' },
|
||||
{ label: '썸네일', value: ASSETS.filter(a => a.size.includes('675')).length, unit: '개', color: 'text-blue-400' },
|
||||
{ label: '배너', value: ASSETS.filter(a => a.size.includes('400')).length, unit: '개', color: 'text-violet-400' },
|
||||
{ label: '크몽 등록 가능', value: ASSETS.length, unit: '개', color: 'text-emerald-400' },
|
||||
].map(stat => (
|
||||
<div key={stat.label} className="bg-slate-900 rounded-xl border border-slate-800 px-4 py-3">
|
||||
<p className="text-slate-500 text-xs mb-1">{stat.label}</p>
|
||||
<p className={`text-2xl font-bold ${stat.color}`}>{stat.value}<span className="text-sm font-normal text-slate-500 ml-1">{stat.unit}</span></p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 그리드 */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-6">
|
||||
{ASSETS.map((asset) => (
|
||||
<div key={asset.file} className="bg-slate-900 rounded-2xl border border-slate-800 overflow-hidden hover:border-slate-600 transition-all group">
|
||||
{/* 미리보기 */}
|
||||
<button
|
||||
onClick={() => setPreview(asset)}
|
||||
className="w-full block relative overflow-hidden bg-slate-950"
|
||||
style={{ aspectRatio: asset.size.includes('400') ? '3/1' : '16/9' }}
|
||||
>
|
||||
<img
|
||||
src={asset.file}
|
||||
alt={asset.name}
|
||||
className="w-full h-full object-contain group-hover:scale-105 transition-transform duration-300"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-black/0 group-hover:bg-black/30 transition-all flex items-center justify-center">
|
||||
<span className="opacity-0 group-hover:opacity-100 text-white font-semibold text-sm bg-black/60 px-4 py-2 rounded-full transition-all">
|
||||
크게 보기
|
||||
</span>
|
||||
{/* 크몽 등록 가이드 */}
|
||||
{showGuide && (
|
||||
<div className="mb-8 bg-slate-900 rounded-2xl border border-slate-700 overflow-hidden">
|
||||
<div className="px-6 py-4 border-b border-slate-800 flex items-center gap-2">
|
||||
<span className="text-yellow-400">⚡</span>
|
||||
<h2 className="text-white font-bold text-sm">크몽 썸네일 등록 완전 가이드</h2>
|
||||
</div>
|
||||
<div className="p-6 grid grid-cols-3 gap-6">
|
||||
<div>
|
||||
<h3 className="text-blue-400 font-semibold text-sm mb-3 flex items-center gap-2"><span>1️⃣</span> SVG → PNG 변환</h3>
|
||||
<ol className="space-y-2 text-slate-400 text-sm">
|
||||
<li className="flex gap-2"><span className="text-slate-600 shrink-0">①</span>SVG 파일 다운로드</li>
|
||||
<li className="flex gap-2"><span className="text-slate-600 shrink-0">②</span>브라우저에서 SVG 파일 열기</li>
|
||||
<li className="flex gap-2"><span className="text-slate-600 shrink-0">③</span>우클릭 → 이미지로 저장 (PNG)</li>
|
||||
<li className="flex gap-2"><span className="text-slate-600 shrink-0">또는</span><span className="text-slate-300">Figma에 드래그 후 Export</span></li>
|
||||
</ol>
|
||||
<div className="mt-3 px-3 py-2 bg-amber-900/20 border border-amber-500/30 rounded-lg text-amber-300 text-xs">
|
||||
크몽은 JPG/PNG만 허용합니다. SVG 직접 업로드 불가.
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{/* 정보 */}
|
||||
<div className="p-4">
|
||||
<div className="flex items-start justify-between gap-2 mb-2">
|
||||
<div>
|
||||
<h3 className="text-white font-semibold text-sm">{asset.name}</h3>
|
||||
<p className="text-slate-500 text-xs mt-0.5">{asset.desc}</p>
|
||||
</div>
|
||||
<span className="text-xs font-semibold px-2 py-1 rounded-full shrink-0" style={{ background: asset.color + '20', color: asset.color }}>
|
||||
{asset.platform}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-slate-600 text-xs mb-3">{asset.size}px</p>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => download(asset.file, asset.name)}
|
||||
className="flex-1 py-2 rounded-lg text-xs font-semibold bg-slate-800 hover:bg-slate-700 text-white transition-all"
|
||||
>
|
||||
SVG 다운로드
|
||||
</button>
|
||||
<button
|
||||
onClick={() => copyPath(asset.file)}
|
||||
className={`px-3 py-2 rounded-lg text-xs font-semibold transition-all ${copied === asset.file ? 'bg-green-900/40 text-green-400 border border-green-500/30' : 'bg-slate-800 hover:bg-slate-700 text-slate-400'}`}
|
||||
>
|
||||
{copied === asset.file ? '✓ 복사됨' : 'URL 복사'}
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-violet-400 font-semibold text-sm mb-3 flex items-center gap-2"><span>2️⃣</span> 크몽 서비스 등록 체크</h3>
|
||||
<ul className="space-y-2 text-slate-400 text-sm">
|
||||
{['썸네일: 1200×675px (권장)', '파일 크기: 10MB 이하', '형식: JPG, PNG', '서비스 카테고리 정확히 선택', '가격 설정: 기본/스탠다드/프리미엄', '패키지 설명 500자 이상'].map(item => (
|
||||
<li key={item} className="flex gap-2"><span className="text-emerald-400">✓</span>{item}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-emerald-400 font-semibold text-sm mb-3 flex items-center gap-2"><span>3️⃣</span> 전문가 검토 순서</h3>
|
||||
<div className="space-y-2">
|
||||
{TABS.map(tab => (
|
||||
<div key={tab.key} className="flex items-center gap-3 text-sm">
|
||||
<span>{tab.icon}</span>
|
||||
<div>
|
||||
<span className="text-white font-medium">{tab.label} 전문가</span>
|
||||
<p className="text-slate-500 text-xs">
|
||||
{tab.key === 'design' && '시각 위계·색상·가독성 검토'}
|
||||
{tab.key === 'pm' && '정보 완전성·CTA·플랫폼 요건'}
|
||||
{tab.key === 'quality' && '오탈자·가격 정확성·파일 품질'}
|
||||
{tab.key === 'marketing' && '전환율·차별화·클릭 유도'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 에셋 그리드 */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-6">
|
||||
{ASSETS.map((asset) => {
|
||||
const score = getTotalScore(asset.file);
|
||||
const pct = score.total > 0 ? Math.round((score.done / score.total) * 100) : 0;
|
||||
const isReady = pct >= 80;
|
||||
return (
|
||||
<div key={asset.file} className="bg-slate-900 rounded-2xl border border-slate-800 overflow-hidden hover:border-slate-600 transition-all group flex flex-col">
|
||||
{/* 미리보기 */}
|
||||
<button
|
||||
onClick={() => setPreview(asset)}
|
||||
className="w-full block relative overflow-hidden bg-slate-950 flex-shrink-0"
|
||||
style={{ aspectRatio: asset.size.includes('400') ? '3/1' : '16/9' }}
|
||||
>
|
||||
<img
|
||||
src={asset.file}
|
||||
alt={asset.name}
|
||||
className="w-full h-full object-contain group-hover:scale-105 transition-transform duration-300"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-black/0 group-hover:bg-black/40 transition-all flex items-center justify-center">
|
||||
<span className="opacity-0 group-hover:opacity-100 text-white font-semibold text-sm bg-black/70 px-4 py-2 rounded-full transition-all flex items-center gap-2">
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/></svg>
|
||||
체크리스트 열기
|
||||
</span>
|
||||
</div>
|
||||
{/* 품질 뱃지 */}
|
||||
<div className={`absolute top-2 right-2 px-2 py-1 rounded-full text-xs font-bold ${isReady ? 'bg-emerald-500/90 text-white' : pct > 0 ? 'bg-amber-500/90 text-white' : 'bg-slate-700/90 text-slate-300'}`}>
|
||||
{isReady ? '✓ 등록 준비됨' : pct > 0 ? `${pct}% 완료` : '미검토'}
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{/* 카드 정보 */}
|
||||
<div className="p-4 flex-1 flex flex-col">
|
||||
<div className="flex items-start justify-between gap-2 mb-2">
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="text-white font-semibold text-sm truncate">{asset.name}</h3>
|
||||
<p className="text-slate-500 text-xs mt-0.5">{asset.desc}</p>
|
||||
</div>
|
||||
<span className="text-xs font-semibold px-2 py-1 rounded-full shrink-0 whitespace-nowrap" style={{ background: asset.color + '20', color: asset.color }}>
|
||||
{asset.platform}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<span className="text-slate-600 text-xs">{asset.size}px</span>
|
||||
<span className="text-slate-700">·</span>
|
||||
<span className="text-slate-500 text-xs font-medium">{asset.price}</span>
|
||||
</div>
|
||||
|
||||
{/* 체크리스트 진행 바 */}
|
||||
<div className="mb-3">
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<span className="text-slate-600 text-xs">품질 체크</span>
|
||||
<span className="text-slate-500 text-xs">{score.done}/{score.total}</span>
|
||||
</div>
|
||||
<div className="h-1.5 bg-slate-800 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full rounded-full transition-all duration-500"
|
||||
style={{ width: `${pct}%`, background: isReady ? '#10b981' : pct > 0 ? '#f59e0b' : '#334155' }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 4대 전문가 점수 */}
|
||||
<div className="grid grid-cols-4 gap-1 mb-3">
|
||||
{TABS.map(tab => {
|
||||
const s = getCheckScore(asset.file, tab.key);
|
||||
const ok = s.done === s.total && s.total > 0;
|
||||
return (
|
||||
<div key={tab.key} className={`text-center py-1 rounded-md text-xs ${ok ? 'bg-emerald-900/30 text-emerald-400' : 'bg-slate-800 text-slate-500'}`}>
|
||||
<div>{tab.icon}</div>
|
||||
<div className="mt-0.5">{s.done}/{s.total}</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* 액션 버튼 */}
|
||||
<div className="flex gap-2 mt-auto">
|
||||
<button
|
||||
onClick={() => download(asset.file, asset.name)}
|
||||
className="flex-1 py-2 rounded-lg text-xs font-semibold bg-slate-800 hover:bg-slate-700 text-white transition-all flex items-center justify-center gap-1.5"
|
||||
>
|
||||
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 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>
|
||||
SVG 다운로드
|
||||
</button>
|
||||
<button
|
||||
onClick={() => copyPath(asset.file)}
|
||||
className={`px-3 py-2 rounded-lg text-xs font-semibold transition-all ${copied === asset.file ? 'bg-emerald-900/40 text-emerald-400 border border-emerald-500/30' : 'bg-slate-800 hover:bg-slate-700 text-slate-400'}`}
|
||||
>
|
||||
{copied === asset.file ? '✓' : 'URL'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* 크게 보기 모달 */}
|
||||
{/* 모달 — 크게 보기 + 체크리스트 */}
|
||||
{preview && (
|
||||
<div
|
||||
className="fixed inset-0 z-50 bg-black/90 flex items-center justify-center p-6"
|
||||
className="fixed inset-0 z-50 bg-black/95 flex items-start justify-center overflow-y-auto p-6"
|
||||
onClick={() => setPreview(null)}
|
||||
>
|
||||
<div className="max-w-6xl w-full" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="max-w-7xl w-full my-4" onClick={(e) => e.stopPropagation()}>
|
||||
{/* 모달 헤더 */}
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<h2 className="text-white font-bold text-lg">{preview.name}</h2>
|
||||
<p className="text-slate-400 text-sm">{preview.size}px · {preview.desc}</p>
|
||||
<h2 className="text-white font-bold text-xl">{preview.name}</h2>
|
||||
<p className="text-slate-400 text-sm mt-0.5">{preview.size}px · {preview.desc}</p>
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
onClick={() => download(preview.file, preview.name)}
|
||||
className="px-4 py-2 rounded-lg text-sm font-semibold bg-blue-600 hover:bg-blue-500 text-white transition-all"
|
||||
className="px-4 py-2 rounded-lg text-sm font-semibold bg-blue-600 hover:bg-blue-500 text-white transition-all flex items-center gap-2"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 10v6m0 0l-3-3m3 3l3-3M3 17V7a2 2 0 012-2h6l2 2h6a2 2 0 012 2v8a2 2 0 01-2 2H5a2 2 0 01-2-2z"/></svg>
|
||||
SVG 다운로드
|
||||
</button>
|
||||
<button onClick={() => setPreview(null)} className="text-slate-400 hover:text-white text-2xl leading-none px-2">×</button>
|
||||
<button onClick={() => setPreview(null)} className="text-slate-400 hover:text-white w-10 h-10 rounded-lg bg-slate-800 hover:bg-slate-700 flex items-center justify-center transition-all text-xl">
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 xl:grid-cols-[1fr_400px] gap-6">
|
||||
{/* 미리보기 */}
|
||||
<div>
|
||||
<img
|
||||
src={preview.file}
|
||||
alt={preview.name}
|
||||
className="w-full rounded-xl border border-slate-700"
|
||||
/>
|
||||
<div className="mt-4 grid grid-cols-4 gap-3">
|
||||
{TABS.map(tab => {
|
||||
const s = getCheckScore(preview.file, tab.key);
|
||||
const pct2 = Math.round((s.done / s.total) * 100);
|
||||
return (
|
||||
<div key={tab.key} className="bg-slate-900 rounded-xl p-3 text-center">
|
||||
<div className="text-xl mb-1">{tab.icon}</div>
|
||||
<div className="text-white font-bold text-sm">{tab.label}</div>
|
||||
<div className="text-slate-400 text-xs mt-0.5">{s.done}/{s.total} 항목</div>
|
||||
<div className="mt-2 h-1.5 bg-slate-800 rounded-full overflow-hidden">
|
||||
<div className="h-full rounded-full bg-blue-500 transition-all" style={{ width: `${pct2}%` }} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 체크리스트 패널 */}
|
||||
<div className="bg-slate-900 rounded-xl border border-slate-800 overflow-hidden flex flex-col">
|
||||
<div className="flex border-b border-slate-800">
|
||||
{TABS.map(tab => (
|
||||
<button
|
||||
key={tab.key}
|
||||
onClick={() => setActiveTab(tab.key)}
|
||||
className={`flex-1 py-3 text-xs font-semibold transition-all ${
|
||||
activeTab === tab.key
|
||||
? 'text-white border-b-2 border-blue-500 bg-slate-800/50'
|
||||
: 'text-slate-500 hover:text-slate-300'
|
||||
}`}
|
||||
>
|
||||
{tab.icon} {tab.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="p-4 flex-1 overflow-y-auto">
|
||||
<div className="mb-3">
|
||||
<span className={`inline-flex items-center gap-1 text-xs font-semibold px-2 py-1 rounded-full border ${tabColors[TABS.find(t => t.key === activeTab)?.color ?? 'blue']}`}>
|
||||
{TABS.find(t => t.key === activeTab)?.icon}
|
||||
{TABS.find(t => t.key === activeTab)?.label} 전문가 관점
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<ul className="space-y-2.5">
|
||||
{CHECKLIST_ITEMS[activeTab].map((item, i) => {
|
||||
const key = `${preview.file}_${activeTab}_${i}`;
|
||||
const checked = !!checks[key];
|
||||
return (
|
||||
<li key={i}>
|
||||
<label className="flex items-start gap-3 cursor-pointer group/item">
|
||||
<div className={`w-5 h-5 rounded-md border-2 flex items-center justify-center flex-shrink-0 mt-0.5 transition-all ${
|
||||
checked ? 'bg-emerald-500 border-emerald-500' : 'border-slate-700 group-hover/item:border-slate-500'
|
||||
}`}
|
||||
onClick={() => toggleCheck(key)}
|
||||
>
|
||||
{checked && (
|
||||
<svg className="w-3 h-3 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={3} d="M5 13l4 4L19 7"/>
|
||||
</svg>
|
||||
)}
|
||||
</div>
|
||||
<span
|
||||
className={`text-sm leading-relaxed transition-all ${checked ? 'text-slate-500 line-through' : 'text-slate-300'}`}
|
||||
onClick={() => toggleCheck(key)}
|
||||
>
|
||||
{item}
|
||||
</span>
|
||||
</label>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* 전체 점수 */}
|
||||
<div className="p-4 border-t border-slate-800">
|
||||
{(() => {
|
||||
const s = getTotalScore(preview.file);
|
||||
const pct3 = Math.round((s.done / s.total) * 100);
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-slate-400 text-sm">전체 품질 점수</span>
|
||||
<span className={`font-bold text-sm ${pct3 >= 80 ? 'text-emerald-400' : pct3 >= 50 ? 'text-amber-400' : 'text-slate-400'}`}>
|
||||
{pct3}% ({s.done}/{s.total})
|
||||
</span>
|
||||
</div>
|
||||
<div className="h-2 bg-slate-800 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full rounded-full transition-all duration-500"
|
||||
style={{ width: `${pct3}%`, background: pct3 >= 80 ? '#10b981' : pct3 >= 50 ? '#f59e0b' : '#64748b' }}
|
||||
/>
|
||||
</div>
|
||||
{pct3 >= 80 && (
|
||||
<div className="mt-2 text-center text-emerald-400 text-xs font-semibold">🎉 크몽 등록 준비 완료!</div>
|
||||
)}
|
||||
{pct3 < 80 && pct3 > 0 && (
|
||||
<div className="mt-2 text-center text-amber-400 text-xs">추가 검토 후 등록을 권장합니다</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<img
|
||||
src={preview.file}
|
||||
alt={preview.name}
|
||||
className="w-full rounded-xl border border-slate-700"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -192,6 +192,17 @@ export default function Sidebar({ isOpen, onClose }: SidebarProps) {
|
||||
})}
|
||||
</nav>
|
||||
|
||||
{/* 상호명 / 사업자 정보 — 토스페이먼츠 심사 필수 표기 */}
|
||||
<div className="px-4 pt-3 pb-1 border-t border-[#1a3a7a]/30 flex-shrink-0">
|
||||
<p className="text-slate-600 text-[10px] leading-relaxed">
|
||||
<span className="block font-semibold text-slate-500 mb-0.5">상호명 : 쟁승메이드</span>
|
||||
<span className="block">대표 : 박재오</span>
|
||||
<span className="block">이메일 : bgg8988@gmail.com</span>
|
||||
<span className="block">문의 : 010-3907-1392</span>
|
||||
<span className="block mt-1 text-slate-700">© 2025 쟁승메이드</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Bottom: 로그인 상태 */}
|
||||
<div className="p-4 border-t border-[#1a3a7a]/50 flex-shrink-0">
|
||||
{userEmail ? (
|
||||
|
||||
Reference in New Issue
Block a user