diff --git a/src/app/(main)/profile/portfolio/page.tsx b/src/app/(main)/profile/portfolio/page.tsx new file mode 100644 index 0000000..0785c45 --- /dev/null +++ b/src/app/(main)/profile/portfolio/page.tsx @@ -0,0 +1,229 @@ +'use client'; + +import React, { useState, useEffect } from 'react'; +import { useRouter } from 'next/navigation'; +import { ArrowLeft, Building2, BadgeCheck, Calendar, Wrench, Ticket, FileText, Edit3, Save, X, Loader2 } from 'lucide-react'; +import { useAuthStore } from '@/stores'; +import employeeService, { type PortfolioEntry } from '@/services/employeeService'; + +const initialColor = (name: string) => { + const colors = ['from-emerald-400 to-emerald-500', 'from-orange-400 to-orange-500', 'from-blue-400 to-blue-500', 'from-pink-400 to-pink-500', 'from-purple-400 to-purple-500']; + let hash = 0; + for (let i = 0; i < name.length; i++) hash = name.charCodeAt(i) + ((hash << 5) - hash); + return colors[Math.abs(hash) % colors.length]; +}; + +export default function PortfolioPage() { + const router = useRouter(); + const user = useAuthStore((s) => s.user); + const [portfolio, setPortfolio] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(''); + const [editingId, setEditingId] = useState(null); + const [editText, setEditText] = useState(''); + const [saving, setSaving] = useState(false); + + useEffect(() => { + if (!user) return; + setLoading(true); + employeeService.getPortfolio(user.empId) + .then((res) => { + setPortfolio(Array.isArray(res.data) ? res.data : []); + }) + .catch(() => setError('포트폴리오를 불러오는데 실패했습니다.')) + .finally(() => setLoading(false)); + }, [user]); + + const handleEditAchievements = (entry: PortfolioEntry) => { + setEditingId(entry.dispatchId); + setEditText(entry.achievements || ''); + }; + + const handleSaveAchievements = async () => { + if (!user || !editingId) return; + setSaving(true); + try { + await employeeService.updateAchievements(user.empId, editingId, editText); + setPortfolio((prev) => prev.map((p) => p.dispatchId === editingId ? { ...p, achievements: editText } : p)); + setEditingId(null); + } catch { + alert('저장에 실패했습니다.'); + } finally { + setSaving(false); + } + }; + + if (!user) { + return ( +
+
로그인이 필요합니다.
+
+ ); + } + + return ( +
+
+ {/* Header */} +
+ +
+

경력 기술서

+

{user.name}님의 포트폴리오

+
+
+ + {/* Summary */} + {!loading && portfolio.length > 0 && ( +
+
+

Total Projects

+

{portfolio.length}

+
+
+

Current

+

{portfolio.filter((p) => !p.endDate).length}

+
+
+

SR Tickets

+

{portfolio.reduce((a, p) => a + p.resolvedTickets.length, 0)}

+
+
+

Reports

+

{portfolio.reduce((a, p) => a + p.reports.length, 0)}

+
+
+ )} + + {/* Content */} + {loading ? ( +
로딩 중...
+ ) : error ? ( +
+

{error}

+
+ ) : portfolio.length === 0 ? ( +
+ +

등록된 포트폴리오 항목이 없습니다.

+
+ ) : ( +
+ {portfolio.map((entry, idx) => ( +
+ {/* Timeline rail */} +
+
+ {idx < portfolio.length - 1 &&
} +
+ + {/* Card */} +
+ {/* Header */} +
+
+

{entry.projectName}

+

+ {entry.roleTitle} + {entry.startDate}{entry.endDate ? ` ~ ${entry.endDate}` : ' ~ Present'} + 공수 {entry.contributionRate}% +

+
+ {entry.contributionRate}% +
+ + {/* Body */} +
+ {/* Tech Stack */} +
+

Tech Stack

+
+ {entry.individualTech.split(',').map((tech) => ( + {tech.trim()} + ))} +
+
+ + {/* Performed Tasks */} + {entry.performedTasks && ( +
+

Performed Tasks

+

{entry.performedTasks}

+
+ )} + + {/* Achievements */} +
+

Achievements

+ {editingId === entry.dispatchId ? ( +
+