feat(portfolio): 포트폴리오 페이지 전체 구현
- 3탭 구조: 프로필&경력, 프로젝트, 자기소개 - 비밀번호 인증 → 편집 모드 - 클립보드 복사, PDF 내보내기 (window.print) - 사이버펑크 테마 CSS, 모바일 반응형 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
121
src/pages/portfolio/Portfolio.jsx
Normal file
121
src/pages/portfolio/Portfolio.jsx
Normal file
@@ -0,0 +1,121 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user