feat: add portfolio edit/delete UI with confirmation dialogs

Add hover-visible edit (rename) and delete buttons to portfolio cards
on the list page, with modal dialogs for name editing and delete
confirmation. Uses existing PUT/DELETE API endpoints.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
머니페니 2026-03-18 22:22:37 +09:00
parent 815f255ff5
commit ee0de0504c

View File

@ -5,9 +5,21 @@ import { useRouter } from 'next/navigation';
import Link from 'next/link';
import { DashboardLayout } from '@/components/layout/dashboard-layout';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { PortfolioCard } from '@/components/portfolio/portfolio-card';
import { Skeleton } from '@/components/ui/skeleton';
import { api } from '@/lib/api';
import { toast } from 'sonner';
import { Pencil, Trash2 } from 'lucide-react';
interface HoldingWithValue {
ticker: string;
@ -33,6 +45,17 @@ export default function PortfolioListPage() {
const [portfolios, setPortfolios] = useState<Portfolio[]>([]);
const [error, setError] = useState<string | null>(null);
// Edit modal state
const [editModalOpen, setEditModalOpen] = useState(false);
const [editTarget, setEditTarget] = useState<Portfolio | null>(null);
const [editName, setEditName] = useState('');
const [editSaving, setEditSaving] = useState(false);
// Delete modal state
const [deleteModalOpen, setDeleteModalOpen] = useState(false);
const [deleteTarget, setDeleteTarget] = useState<Portfolio | null>(null);
const [deleting, setDeleting] = useState(false);
useEffect(() => {
const init = async () => {
try {
@ -88,6 +111,53 @@ export default function PortfolioListPage() {
return (portfolio.total_profit_loss / portfolio.total_invested) * 100;
};
const handleOpenEdit = (e: React.MouseEvent, portfolio: Portfolio) => {
e.preventDefault();
e.stopPropagation();
setEditTarget(portfolio);
setEditName(portfolio.name);
setEditModalOpen(true);
};
const handleSaveEdit = async () => {
if (!editTarget || !editName.trim()) return;
setEditSaving(true);
try {
await api.put(`/api/portfolios/${editTarget.id}`, { name: editName.trim() });
toast.success('포트폴리오 이름이 변경되었습니다.');
setEditModalOpen(false);
await fetchPortfolios();
} catch (err) {
console.error('Failed to update portfolio:', err);
toast.error('포트폴리오 수정에 실패했습니다.');
} finally {
setEditSaving(false);
}
};
const handleOpenDelete = (e: React.MouseEvent, portfolio: Portfolio) => {
e.preventDefault();
e.stopPropagation();
setDeleteTarget(portfolio);
setDeleteModalOpen(true);
};
const handleConfirmDelete = async () => {
if (!deleteTarget) return;
setDeleting(true);
try {
await api.delete(`/api/portfolios/${deleteTarget.id}`);
toast.success('포트폴리오가 삭제되었습니다.');
setDeleteModalOpen(false);
setPortfolios((prev) => prev.filter((p) => p.id !== deleteTarget.id));
} catch (err) {
console.error('Failed to delete portfolio:', err);
toast.error('포트폴리오 삭제에 실패했습니다.');
} finally {
setDeleting(false);
}
};
if (loading) {
return (
<DashboardLayout>
@ -121,15 +191,32 @@ export default function PortfolioListPage() {
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{portfolios.map((portfolio) => (
<PortfolioCard
key={portfolio.id}
id={portfolio.id}
name={portfolio.name}
portfolioType={portfolio.portfolio_type}
totalValue={portfolio.total_value ?? null}
returnPercent={calculateReturnPercent(portfolio)}
holdings={portfolio.holdings ?? []}
/>
<div key={portfolio.id} className="relative group">
<PortfolioCard
id={portfolio.id}
name={portfolio.name}
portfolioType={portfolio.portfolio_type}
totalValue={portfolio.total_value ?? null}
returnPercent={calculateReturnPercent(portfolio)}
holdings={portfolio.holdings ?? []}
/>
<div className="absolute top-3 right-14 flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity z-10">
<button
onClick={(e) => handleOpenEdit(e, portfolio)}
className="p-1.5 rounded-md bg-background/80 border border-border hover:bg-muted text-muted-foreground hover:text-foreground"
title="이름 변경"
>
<Pencil className="h-3.5 w-3.5" />
</button>
<button
onClick={(e) => handleOpenDelete(e, portfolio)}
className="p-1.5 rounded-md bg-background/80 border border-border hover:bg-destructive/10 text-muted-foreground hover:text-destructive"
title="삭제"
>
<Trash2 className="h-3.5 w-3.5" />
</button>
</div>
</div>
))}
</div>
@ -161,6 +248,57 @@ export default function PortfolioListPage() {
</Button>
</div>
)}
{/* Edit Modal */}
<Dialog open={editModalOpen} onOpenChange={setEditModalOpen}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle> </DialogTitle>
<DialogDescription>
.
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="edit-name"></Label>
<Input
id="edit-name"
value={editName}
onChange={(e) => setEditName(e.target.value)}
placeholder="포트폴리오 이름"
onKeyDown={(e) => e.key === 'Enter' && handleSaveEdit()}
/>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setEditModalOpen(false)} disabled={editSaving}>
</Button>
<Button onClick={handleSaveEdit} disabled={editSaving || !editName.trim()}>
{editSaving ? '저장 중...' : '저장'}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* Delete Confirmation Modal */}
<Dialog open={deleteModalOpen} onOpenChange={setDeleteModalOpen}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle> </DialogTitle>
<DialogDescription>
&quot;{deleteTarget?.name}&quot; ? .
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="outline" onClick={() => setDeleteModalOpen(false)} disabled={deleting}>
</Button>
<Button variant="destructive" onClick={handleConfirmDelete} disabled={deleting}>
{deleting ? '삭제 중...' : '삭제'}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</DashboardLayout>
);
}