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:
parent
815f255ff5
commit
ee0de0504c
@ -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,8 +191,8 @@ export default function PortfolioListPage() {
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{portfolios.map((portfolio) => (
|
||||
<div key={portfolio.id} className="relative group">
|
||||
<PortfolioCard
|
||||
key={portfolio.id}
|
||||
id={portfolio.id}
|
||||
name={portfolio.name}
|
||||
portfolioType={portfolio.portfolio_type}
|
||||
@ -130,6 +200,23 @@ export default function PortfolioListPage() {
|
||||
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>
|
||||
"{deleteTarget?.name}" 포트폴리오를 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user