From a6dd2ef747531bfd12f8142aaa89d34b63e6694b Mon Sep 17 00:00:00 2001 From: gahusb Date: Mon, 27 Apr 2026 14:37:25 +0900 Subject: [PATCH] =?UTF-8?q?feat(portfolio):=20=ED=8F=AC=ED=8A=B8=ED=8F=B4?= =?UTF-8?q?=EB=A6=AC=EC=98=A4=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=A0=84?= =?UTF-8?q?=EC=B2=B4=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 3탭 구조: 프로필&경력, 프로젝트, 자기소개 - 비밀번호 인증 → 편집 모드 - 클립보드 복사, PDF 내보내기 (window.print) - 사이버펑크 테마 CSS, 모바일 반응형 Co-Authored-By: Claude Opus 4.6 (1M context) --- src/components/Icons.jsx | 10 + src/pages/portfolio/IntroTab.jsx | 94 +++ src/pages/portfolio/PasswordModal.jsx | 43 ++ src/pages/portfolio/Portfolio.css | 873 +++++++++++++++++++++++++ src/pages/portfolio/Portfolio.jsx | 121 ++++ src/pages/portfolio/ProfileTab.jsx | 202 ++++++ src/pages/portfolio/ProjectTab.jsx | 133 ++++ src/pages/portfolio/ResumeView.jsx | 82 +++ src/pages/portfolio/usePortfolioApi.js | 100 +++ src/routes.jsx | 15 + 10 files changed, 1673 insertions(+) create mode 100644 src/pages/portfolio/IntroTab.jsx create mode 100644 src/pages/portfolio/PasswordModal.jsx create mode 100644 src/pages/portfolio/Portfolio.css create mode 100644 src/pages/portfolio/Portfolio.jsx create mode 100644 src/pages/portfolio/ProfileTab.jsx create mode 100644 src/pages/portfolio/ProjectTab.jsx create mode 100644 src/pages/portfolio/ResumeView.jsx create mode 100644 src/pages/portfolio/usePortfolioApi.js diff --git a/src/components/Icons.jsx b/src/components/Icons.jsx index 0a8bc49..5a65614 100644 --- a/src/components/Icons.jsx +++ b/src/components/Icons.jsx @@ -102,6 +102,16 @@ export const IconBlogMarketing = () => ); +export const IconPortfolio = () => + svg( + <> + + + + + + ); + export const IconBuilding = () => svg( <> diff --git a/src/pages/portfolio/IntroTab.jsx b/src/pages/portfolio/IntroTab.jsx new file mode 100644 index 0000000..08d30f3 --- /dev/null +++ b/src/pages/portfolio/IntroTab.jsx @@ -0,0 +1,94 @@ +import { useState } from 'react'; + +const emptyIntro = { title: '', content: '', is_main: 0 }; + +export default function IntroTab({ introductions, editing, api, onRefresh }) { + const [form, setForm] = useState(null); + const [copiedId, setCopiedId] = useState(null); + + const save = async () => { + if (form.id) { + await api.editIntro(form.id, { title: form.title, content: form.content }); + } else { + await api.addIntro(form); + } + setForm(null); + onRefresh(); + }; + + const remove = async (id) => { + await api.removeIntro(id); + onRefresh(); + }; + + const setMain = async (id) => { + await api.setMainIntro(id); + onRefresh(); + }; + + const copyToClipboard = async (intro) => { + try { + await navigator.clipboard.writeText(intro.content); + setCopiedId(intro.id); + setTimeout(() => setCopiedId(null), 1500); + } catch { + /* 무시 */ + } + }; + + return ( +
+ {editing && ( +
+ +
+ )} + + {/* 작성/수정 폼 */} + {form && ( +
+ +