// ── AdminPage.jsx ──────────────────────────────────────────────
// Admin dashboard: login, submission list, settings
function getFirebaseAdminAuth() {
return window.CIQ_FIREBASE_AUTH || null;
}
function AdminLogin({ onLogin, initialError }) {
const [loading, setLoading] = React.useState(false);
const [status, setStatus] = React.useState('Firebase Auth 프로젝트 auth-f7b2b를 사용합니다.');
const [err, setErr] = React.useState(initialError || '');
React.useEffect(() => {
setErr(initialError || '');
}, [initialError]);
async function handleLogin() {
const firebaseAuth = getFirebaseAdminAuth();
if (!firebaseAuth) {
setErr('Firebase 로그인 설정을 찾을 수 없습니다.');
return;
}
setLoading(true);
setErr('');
setStatus('Google 로그인 창을 여는 중입니다.');
try {
const user = await firebaseAuth.signInWithGoogle();
setStatus('로그인이 완료되었습니다.');
onLogin(user);
} catch (error) {
setErr(firebaseAuth.loginMessage(error));
setStatus('허용된 Google 계정으로 다시 시도해 주세요.');
} finally {
setLoading(false);
}
}
return (
{/* Top bar */}
⚓
Yacht CIQ Admin
{adminUser.email}
e.target.style.background = 'rgba(255,255,255,0.2)'}
onMouseLeave={e => e.target.style.background = 'rgba(255,255,255,0.1)'}>
로그아웃
{/* Tab nav */}
{[
{ key: 'submissions', label: '📋 접수 목록' },
{ key: 'documents', label: '🗂️ 서류 관리' },
{ key: 'settings', label: '⚙️ 설정' },
].map(t => (
setTab(t.key)} style={{
padding: '1rem 1.5rem', background: 'none', border: 'none', fontFamily: 'inherit',
fontWeight: tab === t.key ? '600' : '500', fontSize: '0.9375rem', cursor: 'pointer',
color: tab === t.key ? 'var(--primary)' : 'var(--text-secondary)',
borderBottom: tab === t.key ? '2px solid var(--primary)' : '2px solid transparent',
transition: 'all 0.2s', marginBottom: '-1px'
}}>{t.label}
))}
{tab === 'submissions' && (
{/* Stats */}
{[
{ label: '전체 접수', value: submissions.length, color: '#0f172a' },
{ label: '검토 대기', value: submissions.filter(s=>s.status==='received').length, color: '#b91c1c' },
{ label: '검토 중', value: submissions.filter(s=>s.status==='reviewing').length, color: '#0284c7' },
{ label: '자동화 가능', value: submissions.filter(s=>s.status==='validated').length, color: '#15803d' },
].map(s => (
))}
{/* Search */}
setSearch(e.target.value)} placeholder="이름, 선박명, 접수번호 검색..."
className="form-control" style={{ width: '300px' }} />
{submissionError && (
{submissionError}
)}
{submissionLoading && (
접수 목록을 불러오는 중입니다.
)}
{/* Table */}
{['접수번호','접수일','신청자','선박명','항로','파일','상태',''].map(h => (
{h}
))}
{filtered.map((s, i) => (
e.currentTarget.style.background='#f8fafc'} onMouseLeave={e=>e.currentTarget.style.background='transparent'}>
{s.id}
{s.date}
{s.name}
{s.vessel}
{s.route}
{s.files}건
{STATUS_LABELS[s.status] || s.status}
{ setSelected(s); setStatusDraft(s.status || 'received'); }} className="btn btn-outline" style={{ padding: '0.375rem 0.75rem', fontSize: '0.75rem' }}>상세
))}
{filtered.length === 0 && !submissionLoading && (
저장된 접수가 없습니다.
)}
{localDrafts.length > 0 && (
임시저장 중인 신청서 ({localDrafts.length}건)
{localDrafts.map(d => (
{d.id} | {d.docName || '서류 이름 미설정'} | {d.data?.applicant?.name || '이름 미입력'}
))}
)}
)}
{tab === 'documents' && (
서류 관리
고객에게 안내할 서류 이름과 불러오기 비밀번호/PIN을 생성하거나 수정합니다.
{docForm.originalId && (
신규 생성으로 전환
)}
저장된 서류 목록
{localDrafts.length}건
{localDrafts.length === 0 ? (
아직 생성된 서류가 없습니다.
) : (
{['저장 ID','서류 이름','신청자','선박명','수정일','비밀번호',''].map(h => (
{h}
))}
{localDrafts.map((record, i) => {
const vessel = [record.data?.vessel?.vesselNameKo, record.data?.vessel?.vesselNameEn].filter(Boolean).join(' / ');
return (
{record.id}
{record.docName || '서류 이름 미설정'}
{record.data?.applicant?.name || '미입력'}
{vessel || '미입력'}
{adminFormatDate(record.updatedAt)}
{record.pin ? '설정됨' : '미설정'}
editDocument(record)} className="btn btn-outline" style={{ padding: '0.375rem 0.75rem', fontSize: '0.75rem' }}>수정
);
})}
)}
)}
{tab === 'settings' && (
🔒 보안 설정
{[
{ label: 'HTTPS 강제 리디렉션', on: true },
{ label: '업로드 파일 직접 URL 접근 차단', on: true },
{ label: '허용 확장자 제한 (JPG·PNG·PDF)', on: true },
{ label: '접수 JSON 외부 노출 차단', on: true },
].map((item, idx, arr) => (
{item.label}
{item.on ? '✓ 적용됨' : '⚠ 미적용'}
))}
{saved ? '✓ 설정이 저장되었습니다' : '설정 저장하기'}
)}
{/* Detail modal */}
{selected && (
setSelected(null)}>
e.stopPropagation()}>
{selected.name} — {selected.vessel}
{selected.id}
setSelected(null)} style={{ background: 'none', border: 'none', color: 'var(--text-muted)', fontSize: '1.5rem', cursor: 'pointer', lineHeight: '1' }}>✕
{[
{ label: '접수일', value: selected.date },
{ label: '항로', value: selected.route },
{ label: '첨부파일', value: `${selected.files}건` },
{ label: '상태', value:
{STATUS_LABELS[selected.status] || selected.status} },
{ label: '자동화 폴더', value: selected.project_path || '미기록' },
].map((row, idx, arr) => (
{row.label}
{row.value}
))}
setStatusDraft(e.target.value)} className="form-control" style={{ flex: 1 }}>
접수됨
검토 중
보완 요청
자동화 가능
서류 생성
패키지 완료
제출 완료
보관
updateSubmissionStatus(selected.id, statusDraft || selected.status || 'received')} className="btn btn-primary" style={{ whiteSpace: 'nowrap', background: '#0f172a' }}>상태 변경
downloadSubmission(selected.id)} className="btn btn-outline" style={{ whiteSpace: 'nowrap' }}>자동화 ZIP
)}
);
}
function AdminLoading() {
return (