chore(phase0): eBay 세트 제거 — 문진·문서 admin/API/lib/CONTENT + cheerio

Delete:
- app/api/questionnaire/ (submit/route.ts)
- app/admin/questionnaire/ (page.tsx)
- app/api/admin/questionnaire/ (route.ts + [id]/route.ts)
- app/admin/documents/ (page.tsx)
- app/api/admin/documents/ ([filename]/route.ts)
- lib/ebay-tools/ (crawler.ts·pricing.ts·ai-analyzer.ts·types.ts)
- CONTENT/ebay-tool-{questionnaire,proposal}.html
- CONTENT/ARCHITECTURE_EBAY_PARTS_TOOL.md

Modify:
- app/admin/components/AdminSidebar.tsx: Remove NAV_ITEMS for /admin/documents & /admin/questionnaire
- package.json: Remove cheerio dependency

Verify: npm test (4 files, 20 tests PASS), npm run build OK

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-02 14:02:47 +09:00
parent 0c6ebb2eaa
commit 88fe56163d
16 changed files with 0 additions and 4904 deletions

View File

@@ -76,18 +76,6 @@ const NAV_ITEMS = [
</svg>
),
},
{
href: '/admin/documents',
label: '프로젝트 문서',
icon: (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
d="M13 3v5a2 2 0 002 2h4M9 13h6M9 17h4" />
</svg>
),
},
{
href: '/admin/packs',
label: '팩 자료',
@@ -98,16 +86,6 @@ const NAV_ITEMS = [
</svg>
),
},
{
href: '/admin/questionnaire',
label: '질문지 응답',
icon: (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
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-3 7h3m-3 4h3m-6-4h.01M9 16h.01" />
</svg>
),
},
{
href: '/admin/marketing',
label: '마케팅 에셋',

View File

@@ -1,160 +0,0 @@
'use client';
import { useState, useEffect } from 'react';
interface Document {
id: string;
title: string;
description: string;
category: '제안서' | '질문지' | '계약서';
fileName: string;
updatedAt: string;
status: 'draft' | 'sent' | 'accepted';
}
const documents: Document[] = [
{
id: 'ebay-proposal',
title: '이베이 부품 AI 자동화 — 제안서',
description: '프로젝트 개요, 3단 패키지 견적(120/198/330만원), 기술 스택, 진행 절차',
category: '제안서',
fileName: 'ebay-tool-proposal.html',
updatedAt: '2026-04-02',
status: 'draft',
},
{
id: 'ebay-questionnaire',
title: '이베이 부품 AI 자동화 — 요구사항 질문지',
description: '고객 사전 확인 17항목 (타겟 사이트, 샘플 품번, eBay 셀러 티어 등)',
category: '질문지',
fileName: 'ebay-tool-questionnaire.html',
updatedAt: '2026-04-02',
status: 'draft',
},
];
const CATEGORY_COLORS: Record<string, string> = {
'제안서': 'bg-blue-900/40 text-blue-400 border-blue-500/30',
'질문지': 'bg-amber-900/40 text-amber-400 border-amber-500/30',
'계약서': 'bg-green-900/40 text-green-400 border-green-500/30',
};
const STATUS_CONFIG: Record<string, { label: string; color: string }> = {
draft: { label: '초안', color: 'bg-slate-700/60 text-slate-300' },
sent: { label: '발송', color: 'bg-blue-900/40 text-blue-400' },
accepted: { label: '수락', color: 'bg-green-900/40 text-green-400' },
};
export default function AdminDocumentsPage() {
const [previewDoc, setPreviewDoc] = useState<Document | null>(null);
const [previewHtml, setPreviewHtml] = useState<string>('');
const [previewLoading, setPreviewLoading] = useState(false);
// iframe src 대신 fetch + srcdoc 방식으로 X-Frame-Options 우회
useEffect(() => {
if (!previewDoc) { setPreviewHtml(''); return; }
setPreviewLoading(true);
fetch(`/api/admin/documents/${previewDoc.fileName}`)
.then(res => res.ok ? res.text() : Promise.reject('문서를 불러올 수 없습니다'))
.then(html => setPreviewHtml(html))
.catch(() => setPreviewHtml('<p style="padding:2rem;color:red;">문서를 불러올 수 없습니다.</p>'))
.finally(() => setPreviewLoading(false));
}, [previewDoc]);
return (
<div className="p-6 max-w-6xl mx-auto">
{/* 헤더 */}
<div className="mb-6">
<h1 className="text-white text-2xl font-bold"> </h1>
<p className="text-slate-400 text-sm mt-0.5">
, ,
</p>
</div>
{/* 문서 카드 그리드 */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
{documents.map((doc) => (
<div
key={doc.id}
className="bg-slate-900 rounded-2xl border border-slate-700/50 p-5 flex flex-col"
>
{/* 카테고리 + 상태 뱃지 */}
<div className="flex items-center gap-2 mb-3">
<span className={`px-2.5 py-0.5 rounded-full text-xs font-medium border ${CATEGORY_COLORS[doc.category]}`}>
{doc.category}
</span>
<span className={`px-2.5 py-0.5 rounded-full text-xs font-medium ${STATUS_CONFIG[doc.status].color}`}>
{STATUS_CONFIG[doc.status].label}
</span>
</div>
{/* 제목 + 설명 */}
<h3 className="text-white font-semibold text-sm mb-1.5">{doc.title}</h3>
<p className="text-slate-400 text-xs leading-relaxed mb-4 flex-1">{doc.description}</p>
{/* 수정일 + 버튼 */}
<div className="flex items-center justify-between">
<span className="text-slate-600 text-xs">: {doc.updatedAt}</span>
<div className="flex gap-2">
<button
onClick={() => setPreviewDoc(doc)}
className="px-3 py-1.5 rounded-lg text-xs font-medium bg-red-600/20 text-red-400 hover:bg-red-600/30 transition border border-red-500/20"
>
</button>
<button
onClick={() => window.open(`/api/admin/documents/${doc.fileName}`, '_blank')}
className="px-3 py-1.5 rounded-lg text-xs font-medium bg-slate-700 text-slate-300 hover:bg-slate-600 hover:text-white transition"
>
</button>
</div>
</div>
</div>
))}
</div>
{/* 미리보기 섹션 */}
{previewDoc && (
<div className="bg-slate-900 rounded-2xl border border-slate-700/50 overflow-hidden">
{/* 미리보기 헤더 */}
<div className="flex items-center justify-between px-5 py-3 border-b border-slate-700/50">
<div className="flex items-center gap-3">
<svg className="w-4 h-4 text-slate-400" 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 className="text-white text-sm font-medium">{previewDoc.title}</span>
</div>
<button
onClick={() => setPreviewDoc(null)}
className="p-1.5 rounded-lg text-slate-500 hover:text-white hover:bg-slate-800 transition"
aria-label="미리보기 닫기"
>
<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>
</button>
</div>
{/* 문서 미리보기 (fetch + srcdoc 방식) */}
{previewLoading ? (
<div className="flex items-center justify-center bg-white" style={{ height: '80vh' }}>
<div className="text-slate-400 text-sm"> ...</div>
</div>
) : (
<iframe
srcDoc={previewHtml}
className="w-full bg-white"
style={{ height: '80vh' }}
title={previewDoc.title}
sandbox="allow-same-origin"
/>
)}
</div>
)}
</div>
);
}

View File

@@ -1,256 +0,0 @@
'use client';
import { useState, useEffect } from 'react';
interface QuestionnaireResponse {
id: string;
questionnaire_type: string;
client_name: string;
client_email: string;
client_phone: string | null;
responses: Record<string, unknown>;
status: string;
admin_notes: string | null;
created_at: string;
reviewed_at: string | null;
}
const STATUS_CONFIG: Record<string, { label: string; color: string }> = {
submitted: { label: '접수', color: 'bg-blue-900/40 text-blue-400 border-blue-500/30' },
reviewed: { label: '검토완료', color: 'bg-green-900/40 text-green-400 border-green-500/30' },
archived: { label: '보관', color: 'bg-slate-700/60 text-slate-400 border-slate-500/30' },
};
const QUESTION_LABELS: Record<string, string> = {
q1: '주 사용 부품 사이트 URL',
q2: '주요 취급 부품 카테고리',
q3: '샘플 품번 목록',
q4: '현재 eBay 리스팅 URL',
q5: 'eBay 셀러 계정 등급',
q6: '주 판매 카테고리',
q7: '예상 월간 리스팅 건수',
q8: 'Fitment 정확도 기대치',
q8_detail: 'Fitment 추가 의견',
q9_selected: '타겟 마켓',
q9_detail: '타겟 마켓 기타',
q10: '리스팅 1건 소요 시간',
q11: '기존 리스팅 관리 방식',
q11_detail: '서드파티 툴 이름',
q12: '관세/통관 계산 방식',
q13: 'eBay Developer API 키 보유',
q14: '선호 AI 모델',
q15: '현재 자동화 도구',
q16: 'AI API 키 보유 여부',
q17: '포트폴리오 활용 동의',
additional: '추가 요청사항',
};
export default function AdminQuestionnairePage() {
const [responses, setResponses] = useState<QuestionnaireResponse[]>([]);
const [loading, setLoading] = useState(true);
const [selected, setSelected] = useState<QuestionnaireResponse | null>(null);
const [adminNotes, setAdminNotes] = useState('');
const [saving, setSaving] = useState(false);
useEffect(() => {
fetchResponses();
}, []);
async function fetchResponses() {
try {
const res = await fetch('/api/admin/questionnaire');
if (!res.ok) throw new Error();
const json = await res.json();
setResponses(json.data || []);
} catch {
setResponses([]);
} finally {
setLoading(false);
}
}
function openDetail(item: QuestionnaireResponse) {
setSelected(item);
setAdminNotes(item.admin_notes || '');
}
async function updateStatus(id: string, status: string) {
setSaving(true);
try {
await fetch(`/api/admin/questionnaire/${id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ status, admin_notes: adminNotes }),
});
await fetchResponses();
if (selected?.id === id) {
setSelected(prev => prev ? { ...prev, status, admin_notes: adminNotes } : null);
}
} finally {
setSaving(false);
}
}
function formatDate(dateStr: string) {
return new Date(dateStr).toLocaleDateString('ko-KR', {
year: 'numeric', month: '2-digit', day: '2-digit',
hour: '2-digit', minute: '2-digit',
});
}
return (
<div className="p-6 max-w-6xl mx-auto">
<div className="mb-6">
<h1 className="text-white text-2xl font-bold"> </h1>
<p className="text-slate-400 text-sm mt-0.5">
</p>
</div>
{loading ? (
<div className="text-slate-400 text-sm py-12 text-center"> ...</div>
) : responses.length === 0 ? (
<div className="bg-slate-900 rounded-2xl border border-slate-700/50 p-12 text-center">
<svg className="w-12 h-12 text-slate-600 mx-auto mb-3" 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>
<p className="text-slate-400 text-sm"> </p>
</div>
) : (
<div className="space-y-4">
{/* 목록 */}
<div className="bg-slate-900 rounded-2xl border border-slate-700/50 overflow-hidden">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-slate-700/50">
<th className="text-left text-slate-400 font-medium px-5 py-3"></th>
<th className="text-left text-slate-400 font-medium px-5 py-3"></th>
<th className="text-left text-slate-400 font-medium px-5 py-3"></th>
<th className="text-left text-slate-400 font-medium px-5 py-3"></th>
<th className="text-left text-slate-400 font-medium px-5 py-3"></th>
<th className="text-right text-slate-400 font-medium px-5 py-3"></th>
</tr>
</thead>
<tbody>
{responses.map((item) => {
const st = STATUS_CONFIG[item.status] || STATUS_CONFIG.submitted;
return (
<tr
key={item.id}
className={`border-b border-slate-800/50 hover:bg-slate-800/30 cursor-pointer transition ${
selected?.id === item.id ? 'bg-slate-800/50' : ''
}`}
onClick={() => openDetail(item)}
>
<td className="px-5 py-3 text-white font-medium">{item.client_name}</td>
<td className="px-5 py-3 text-slate-300">{item.client_email}</td>
<td className="px-5 py-3 text-slate-400">{item.questionnaire_type}</td>
<td className="px-5 py-3">
<span className={`px-2.5 py-0.5 rounded-full text-xs font-medium border ${st.color}`}>
{st.label}
</span>
</td>
<td className="px-5 py-3 text-slate-400">{formatDate(item.created_at)}</td>
<td className="px-5 py-3 text-right">
<svg className="w-4 h-4 text-slate-500 inline" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
{/* 상세 패널 */}
{selected && (
<div className="bg-slate-900 rounded-2xl border border-slate-700/50 overflow-hidden">
<div className="flex items-center justify-between px-5 py-4 border-b border-slate-700/50">
<div>
<h3 className="text-white font-semibold">
{selected.client_name}
</h3>
<p className="text-slate-400 text-xs mt-0.5">
{selected.client_email}
{selected.client_phone && ` · ${selected.client_phone}`}
{' · '}: {formatDate(selected.created_at)}
</p>
</div>
<button
onClick={() => setSelected(null)}
className="p-1.5 rounded-lg text-slate-500 hover:text-white hover:bg-slate-800 transition"
>
<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>
</button>
</div>
<div className="p-5 space-y-4 max-h-[60vh] overflow-y-auto">
{Object.entries(selected.responses).map(([key, value]) => (
<div key={key} className="bg-slate-800/50 rounded-lg p-4">
<div className="text-xs text-slate-400 font-medium mb-1.5">
{QUESTION_LABELS[key] || key}
</div>
<div className="text-white text-sm whitespace-pre-wrap">
{Array.isArray(value) ? (value as string[]).join(', ') : String(value)}
</div>
</div>
))}
{Object.keys(selected.responses).length === 0 && (
<p className="text-slate-500 text-sm text-center py-4"> </p>
)}
</div>
{/* 관리자 메모 + 상태 변경 */}
<div className="px-5 py-4 border-t border-slate-700/50 space-y-3">
<div>
<label className="text-slate-400 text-xs font-medium block mb-1.5"> </label>
<textarea
value={adminNotes}
onChange={(e) => setAdminNotes(e.target.value)}
className="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white text-sm resize-none focus:outline-none focus:border-red-500/50"
rows={2}
placeholder="내부 참고용 메모..."
/>
</div>
<div className="flex gap-2">
{selected.status !== 'reviewed' && (
<button
onClick={() => updateStatus(selected.id, 'reviewed')}
disabled={saving}
className="px-4 py-2 rounded-lg text-xs font-medium bg-green-600/20 text-green-400 hover:bg-green-600/30 transition border border-green-500/20 disabled:opacity-50"
>
{saving ? '저장 중...' : '검토 완료'}
</button>
)}
{selected.status !== 'archived' && (
<button
onClick={() => updateStatus(selected.id, 'archived')}
disabled={saving}
className="px-4 py-2 rounded-lg text-xs font-medium bg-slate-700 text-slate-300 hover:bg-slate-600 transition disabled:opacity-50"
>
</button>
)}
{selected.status !== 'submitted' && (
<button
onClick={() => updateStatus(selected.id, 'submitted')}
disabled={saving}
className="px-4 py-2 rounded-lg text-xs font-medium bg-blue-600/20 text-blue-400 hover:bg-blue-600/30 transition border border-blue-500/20 disabled:opacity-50"
>
</button>
)}
</div>
</div>
</div>
)}
</div>
)}
</div>
);
}