- 3탭 구조: 프로필&경력, 프로젝트, 자기소개 - 비밀번호 인증 → 편집 모드 - 클립보드 복사, PDF 내보내기 (window.print) - 사이버펑크 테마 CSS, 모바일 반응형 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
122 lines
3.8 KiB
JavaScript
122 lines
3.8 KiB
JavaScript
import { useCallback, useEffect, useState } from 'react';
|
|
import { useIsMobile } from '../../hooks/useIsMobile';
|
|
import SwipeableView from '../../components/SwipeableView';
|
|
import usePortfolioApi from './usePortfolioApi';
|
|
import PasswordModal from './PasswordModal';
|
|
import ProfileTab from './ProfileTab';
|
|
import ProjectTab from './ProjectTab';
|
|
import IntroTab from './IntroTab';
|
|
import ResumeView from './ResumeView';
|
|
import './Portfolio.css';
|
|
|
|
export default function Portfolio() {
|
|
const isMobile = useIsMobile();
|
|
const api = usePortfolioApi();
|
|
|
|
const [data, setData] = useState(null);
|
|
const [intros, setIntros] = useState([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState('');
|
|
const [editing, setEditing] = useState(false);
|
|
const [showPwModal, setShowPwModal] = useState(false);
|
|
const [showResume, setShowResume] = useState(false);
|
|
|
|
const load = useCallback(async () => {
|
|
setLoading(true);
|
|
setError('');
|
|
try {
|
|
const d = await api.fetchPublic();
|
|
setData(d);
|
|
} catch (err) {
|
|
setError(err.message);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, []);
|
|
|
|
useEffect(() => { load(); }, [load]);
|
|
|
|
const handleEditToggle = () => {
|
|
if (editing) {
|
|
setEditing(false);
|
|
return;
|
|
}
|
|
if (api.token) {
|
|
setEditing(true);
|
|
} else {
|
|
setShowPwModal(true);
|
|
}
|
|
};
|
|
|
|
const handleAuth = async (pw) => {
|
|
const ok = await api.login(pw);
|
|
if (ok) {
|
|
setShowPwModal(false);
|
|
setEditing(true);
|
|
try {
|
|
const list = await api.fetchIntros();
|
|
setIntros(list);
|
|
} catch { /* 무시 */ }
|
|
}
|
|
};
|
|
|
|
const refresh = useCallback(async () => {
|
|
try {
|
|
const d = await api.fetchPublic();
|
|
setData(d);
|
|
if (api.token) {
|
|
const list = await api.fetchIntros();
|
|
setIntros(list);
|
|
}
|
|
} catch { /* 무시 */ }
|
|
}, [api.token]);
|
|
|
|
if (loading && !data) return <div className="pf-page"><p className="pf-loading">불러오는 중...</p></div>;
|
|
if (error && !data) return <div className="pf-page"><p className="pf-error">{error}</p></div>;
|
|
if (!data) return null;
|
|
|
|
if (showResume) {
|
|
return <ResumeView data={data} onClose={() => setShowResume(false)} />;
|
|
}
|
|
|
|
const tabs = [
|
|
{
|
|
key: 'profile',
|
|
label: '프로필',
|
|
content: <ProfileTab data={data} editing={editing} api={api} onRefresh={refresh} />,
|
|
},
|
|
{
|
|
key: 'projects',
|
|
label: '프로젝트',
|
|
content: <ProjectTab projects={data.projects} editing={editing} api={api} onRefresh={refresh} />,
|
|
},
|
|
{
|
|
key: 'intro',
|
|
label: '자기소개',
|
|
content: <IntroTab introductions={editing ? intros : (data.main_introduction ? [data.main_introduction] : [])} editing={editing} api={api} onRefresh={refresh} />,
|
|
},
|
|
];
|
|
|
|
return (
|
|
<div className="pf-page">
|
|
<div className="pf-toolbar">
|
|
<button className={`button ${editing ? 'primary' : 'ghost'}`} onClick={handleEditToggle}>
|
|
{editing ? '편집 완료' : '편집'}
|
|
</button>
|
|
<button className="button ghost" onClick={() => setShowResume(true)}>
|
|
PDF 내보내기
|
|
</button>
|
|
</div>
|
|
|
|
<SwipeableView tabs={tabs} />
|
|
|
|
<PasswordModal
|
|
open={showPwModal}
|
|
onAuth={handleAuth}
|
|
onClose={() => setShowPwModal(false)}
|
|
error={api.authError}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|