Files
jaengseung-made/app/admin/documents/page.tsx
gahusb e27d13b6ec feat: 질문지 제출 기능 + 관리자 응답 관리 + iframe 미리보기 수정
- 질문지 HTML에 제출/임시저장 JavaScript 추가 (localStorage 임시저장, API 제출)
- questionnaire_responses 테이블 마이그레이션 (005)
- /api/questionnaire/submit POST 엔드포인트
- 관리자 질문지 응답 목록/상세/상태변경 페이지 및 API
- 관리자 문서 미리보기를 fetch+srcdoc 방식으로 변경 (X-Frame-Options 우회)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03 00:44:27 +09:00

161 lines
6.6 KiB
TypeScript

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