feat: manager/admin can view/edit any employee's portfolio

This commit is contained in:
pray4skylark
2026-06-02 15:14:51 +09:00
parent 37e5c65ac9
commit f5899aec46
+83 -10
View File
@@ -2,9 +2,10 @@
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 { ArrowLeft, Building2, BadgeCheck, Calendar, Ticket, FileText, Edit3, Save, X, Loader2, Search, User } from 'lucide-react';
import { useAuthStore } from '@/stores';
import employeeService, { type PortfolioEntry } from '@/services/employeeService';
import type { EmployeeSearchResult } from '@/types';
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'];
@@ -16,6 +17,13 @@ const initialColor = (name: string) => {
export default function PortfolioPage() {
const router = useRouter();
const user = useAuthStore((s) => s.user);
const isMgmt = user?.roleCode === 'MANAGER' || user?.roleCode === 'ADMIN';
const [targetEmpId, setTargetEmpId] = useState<number | null>(null);
const [targetName, setTargetName] = useState('');
const [searchKeyword, setSearchKeyword] = useState('');
const [searchResults, setSearchResults] = useState<EmployeeSearchResult[]>([]);
const [showSearch, setShowSearch] = useState(false);
const [searchTimeout, setSearchTimeout] = useState<NodeJS.Timeout | null>(null);
const [portfolio, setPortfolio] = useState<PortfolioEntry[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
@@ -23,16 +31,45 @@ export default function PortfolioPage() {
const [editText, setEditText] = useState('');
const [saving, setSaving] = useState(false);
useEffect(() => {
if (!user) return;
const loadPortfolio = (empId: number) => {
setLoading(true);
employeeService.getPortfolio(user.empId)
setError('');
employeeService.getPortfolio(empId)
.then((res) => {
setPortfolio(Array.isArray(res.data) ? res.data : []);
})
.catch(() => setError('포트폴리오를 불러오는데 실패했습니다.'))
.finally(() => setLoading(false));
}, [user]);
};
useEffect(() => {
if (!user) return;
const id = targetEmpId || user.empId;
setTargetName(targetEmpId ? targetName : user.name);
loadPortfolio(id);
}, [user, targetEmpId]);
// Employee search (MANAGER/ADMIN only)
const handleSearch = (keyword: string) => {
setSearchKeyword(keyword);
if (searchTimeout) clearTimeout(searchTimeout);
if (!keyword.trim()) { setSearchResults([]); return; }
const t = setTimeout(async () => {
try {
const res = await employeeService.search(keyword);
if (res.success) setSearchResults(res.data.slice(0, 10));
} catch { setSearchResults([]); }
}, 300);
setSearchTimeout(t);
};
const selectEmployee = (emp: EmployeeSearchResult) => {
setTargetEmpId(emp.empId);
setTargetName(emp.name);
setShowSearch(false);
setSearchKeyword('');
setSearchResults([]);
};
const handleEditAchievements = (entry: PortfolioEntry) => {
setEditingId(entry.dispatchId);
@@ -40,10 +77,11 @@ export default function PortfolioPage() {
};
const handleSaveAchievements = async () => {
if (!user || !editingId) return;
const empId = targetEmpId || user?.empId;
if (!empId || !editingId) return;
setSaving(true);
try {
await employeeService.updateAchievements(user.empId, editingId, editText);
await employeeService.updateAchievements(empId, editingId, editText);
setPortfolio((prev) => prev.map((p) => p.dispatchId === editingId ? { ...p, achievements: editText } : p));
setEditingId(null);
} catch {
@@ -65,14 +103,49 @@ export default function PortfolioPage() {
<div className="p-4 sm:p-6 lg:p-8">
<div className="max-w-5xl mx-auto">
{/* Header */}
<div className="flex items-center gap-4 mb-8">
<div className="flex items-center gap-4 mb-6">
<button onClick={() => router.back()} className="p-2 hover:bg-surface-container rounded-xl transition-colors">
<ArrowLeft className="w-5 h-5 text-text-muted" />
</button>
<div>
<div className="flex-1">
<h1 className="font-outfit text-2xl font-semibold text-text-main tracking-tight">경력 기술서</h1>
<p className="text-text-muted text-sm mt-0.5">{user.name}님의 포트폴리오</p>
<p className="text-text-muted text-sm mt-0.5">{targetName || user.name}님의 포트폴리오</p>
</div>
{isMgmt && (
<div className="relative">
<button onClick={() => setShowSearch(!showSearch)}
className="flex items-center gap-2 bg-surface border border-border px-4 py-2.5 rounded-xl text-sm font-medium text-text-main hover:bg-surface-container transition-colors">
<Search className="w-4 h-4" /> 직원 검색
</button>
{showSearch && (
<div className="absolute right-0 top-full mt-2 w-72 bg-surface rounded-2xl border border-border shadow-xl z-50 overflow-hidden">
<div className="p-3">
<input value={searchKeyword} onChange={(e) => handleSearch(e.target.value)}
placeholder="이름 또는 ID를 입력하세요..."
className="w-full px-3 py-2 border border-border rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-primary/20"
autoFocus />
</div>
{searchResults.length > 0 && (
<div className="pb-2">
{searchResults.map((emp) => (
<button key={emp.empId} onClick={() => selectEmployee(emp)}
className="w-full flex items-center gap-3 px-4 py-2.5 hover:bg-surface-container-low transition-colors text-left">
<User className="w-4 h-4 text-text-muted" />
<div>
<p className="text-sm font-medium text-text-main">{emp.name}</p>
<p className="text-xs text-text-muted">{emp.roleCode} · {emp.email}</p>
</div>
</button>
))}
</div>
)}
{searchKeyword && searchResults.length === 0 && (
<p className="text-xs text-text-muted text-center pb-3">검색 결과가 없습니다.</p>
)}
</div>
)}
</div>
)}
</div>
{/* Summary */}