Files
web-page/src/pages/portfolio/ProjectTab.jsx
gahusb a6dd2ef747 feat(portfolio): 포트폴리오 페이지 전체 구현
- 3탭 구조: 프로필&경력, 프로젝트, 자기소개
- 비밀번호 인증 → 편집 모드
- 클립보드 복사, PDF 내보내기 (window.print)
- 사이버펑크 테마 CSS, 모바일 반응형

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-27 14:37:25 +09:00

134 lines
6.5 KiB
JavaScript

import { useState } from 'react';
const CATEGORIES = [
{ key: 'all', label: '전체' },
{ key: 'company', label: '회사' },
{ key: 'personal', label: '개인' },
{ key: 'academy', label: '아카데미' },
];
const emptyProject = {
category: 'personal', title: '', description: '', tech_stack: [],
role: '', start_date: '', end_date: '', url: '', image_url: '', sort_order: 0,
};
export default function ProjectTab({ projects, editing, api, onRefresh }) {
const [filter, setFilter] = useState('all');
const [form, setForm] = useState(null);
const [techInput, setTechInput] = useState('');
const filtered = filter === 'all' ? projects : projects.filter(p => p.category === filter);
const addTech = () => {
const tag = techInput.trim();
if (tag && !form.tech_stack.includes(tag)) {
setForm(f => ({ ...f, tech_stack: [...f.tech_stack, tag] }));
}
setTechInput('');
};
const removeTech = (tag) => {
setForm(f => ({ ...f, tech_stack: f.tech_stack.filter(t => t !== tag) }));
};
const save = async () => {
if (form.id) {
await api.editProject(form.id, form);
} else {
await api.addProject(form);
}
setForm(null);
setTechInput('');
onRefresh();
};
const remove = async (id) => {
await api.removeProject(id);
onRefresh();
};
return (
<div className="pf-project-tab">
{/* 카테고리 필터 */}
<div className="pf-filter-bar">
{CATEGORIES.map(c => (
<button
key={c.key}
className={`pf-filter-btn${filter === c.key ? ' is-active' : ''}`}
onClick={() => setFilter(c.key)}
>
{c.label}
</button>
))}
{editing && <button className="button ghost" onClick={() => { setForm({...emptyProject}); setTechInput(''); }}>+ 추가</button>}
</div>
{/* 추가/수정 폼 */}
{form && (
<div className="pf-edit-form">
<label>구분
<select value={form.category} onChange={(e) => setForm(f => ({...f, category: e.target.value}))}>
{CATEGORIES.filter(c => c.key !== 'all').map(c => <option key={c.key} value={c.key}>{c.label}</option>)}
</select>
</label>
<label>프로젝트명 <input value={form.title} onChange={(e) => setForm(f => ({...f, title: e.target.value}))} /></label>
<label>설명 <textarea value={form.description} rows={3} onChange={(e) => setForm(f => ({...f, description: e.target.value}))} /></label>
<label>담당 역할 <input value={form.role} onChange={(e) => setForm(f => ({...f, role: e.target.value}))} /></label>
<div className="pf-edit-form__row">
<label>시작 <input type="month" value={form.start_date} onChange={(e) => setForm(f => ({...f, start_date: e.target.value}))} /></label>
<label>종료 <input type="month" value={form.end_date} onChange={(e) => setForm(f => ({...f, end_date: e.target.value}))} placeholder="현재" /></label>
</div>
<label>URL <input value={form.url} onChange={(e) => setForm(f => ({...f, url: e.target.value}))} /></label>
<label>기술 스택
<div className="pf-tech-input">
<input
value={techInput}
onChange={(e) => setTechInput(e.target.value)}
onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ',') { e.preventDefault(); addTech(); } }}
placeholder="기술명 입력 후 Enter"
/>
<div className="pf-tech-tags">
{form.tech_stack.map(t => (
<span key={t} className="pf-tech-tag">{t} <button onClick={() => removeTech(t)}>&times;</button></span>
))}
</div>
</div>
</label>
<div className="pf-edit-form__actions">
<button className="button ghost" onClick={() => { setForm(null); setTechInput(''); }}>취소</button>
<button className="button primary" onClick={save}>저장</button>
</div>
</div>
)}
{/* 프로젝트 카드 그리드 */}
<div className="pf-project-grid">
{filtered.length === 0 && <p className="pf-empty">프로젝트가 없습니다.</p>}
{filtered.map(p => (
<div key={p.id} className="pf-project-card">
<div className="pf-project-card__header">
<span className={`pf-project-card__cat pf-project-card__cat--${p.category}`}>{CATEGORIES.find(c => c.key === p.category)?.label}</span>
<span className="pf-project-card__period">{p.start_date} {p.end_date || '현재'}</span>
</div>
<h4 className="pf-project-card__title">{p.title}</h4>
{p.role && <p className="pf-project-card__role">{p.role}</p>}
{p.description && <p className="pf-project-card__desc">{p.description}</p>}
{p.tech_stack?.length > 0 && (
<div className="pf-project-card__tags">
{p.tech_stack.map(t => <span key={t} className="pf-tech-tag">{t}</span>)}
</div>
)}
{p.url && <a className="pf-project-card__link" href={p.url} target="_blank" rel="noreferrer">링크 &rarr;</a>}
{editing && (
<div className="pf-project-card__actions">
<button className="button ghost" onClick={() => { setForm({...p}); setTechInput(''); }}>수정</button>
<button className="button ghost" onClick={() => remove(p.id)}>삭제</button>
</div>
)}
</div>
))}
</div>
</div>
);
}