feat: 꾸미기 아이템 삭제 기능 추가
Co-Authored-By: Claude Opus 5
@eae249ebe51971a1d7ffd7cf4a2e05639e10b8ea
+++ app/(protected)/(basic)/decoration-items/_components/decoration-item-row-actions.tsx
... | ... | @@ -0,0 +1,97 @@ |
| 1 | +'use client'; | |
| 2 | + | |
| 3 | +import { useState, useTransition } from 'react'; | |
| 4 | +import { Button } from '@/components/ui/button'; | |
| 5 | +import { useFeedback } from '@/app/_hooks/use-feedback'; | |
| 6 | +import type { DecorationItem } from '@/lib/domain/decoration-item'; | |
| 7 | +import { deleteDecorationItemAction } from '../_actions'; | |
| 8 | +import { DecorationItemEditModal } from './decoration-item-edit-modal'; | |
| 9 | + | |
| 10 | +interface DecorationItemRowActionsProps { | |
| 11 | + item: DecorationItem; | |
| 12 | + /** 수정 팝업의 "(총 등록 N개)" 힌트로 그대로 흘려보낸다(현재 유형 전체 등록 건수). */ | |
| 13 | + typeTotalCount: number; | |
| 14 | +} | |
| 15 | + | |
| 16 | +/** | |
| 17 | + * 목록 행의 "관리" 셀(시안 ADM_ITM_101) — 수정 팝업 열림 상태만 소유하는 최말단 상호작용 | |
| 18 | + * 경계다(테이블 전체를 클라이언트로 내리지 않기 위해 이 셀만 분리했다, admins와 동일 패턴). | |
| 19 | + * | |
| 20 | + * **삭제는 시안대로 확인 얼럿을 거친 뒤 실행한다** — 전역 얼럿(FeedbackProvider)의 actions | |
| 21 | + * 슬롯에 직접 [취소]/[삭제] 버튼을 넣어 구성한다. 삭제는 되돌릴 수 없는 동작이라 얼럿 문구에 | |
| 22 | + * 명시한다. | |
| 23 | + * | |
| 24 | + * 시안은 연필·휴지통 아이콘 버튼을 지정하지만 `components/ui/icons/`에 아직 없어(신규 아이콘 | |
| 25 | + * 추가는 공용 컴포넌트 신설이라 design 레인 소관) admins와 동일하게 텍스트 버튼으로 대체했다. | |
| 26 | + */ | |
| 27 | +export function DecorationItemRowActions({ | |
| 28 | + item, | |
| 29 | + typeTotalCount, | |
| 30 | +}: DecorationItemRowActionsProps) { | |
| 31 | + const { showAlert, hideAlert, showToast } = useFeedback(); | |
| 32 | + const [isEditOpen, setIsEditOpen] = useState(false); | |
| 33 | + const [isDeleting, startDeleting] = useTransition(); | |
| 34 | + | |
| 35 | + function runDelete() { | |
| 36 | + hideAlert(); | |
| 37 | + startDeleting(async () => { | |
| 38 | + const result = await deleteDecorationItemAction(item.itemId); | |
| 39 | + if (result.status === 'error') { | |
| 40 | + showToast({ | |
| 41 | + variant: 'danger', | |
| 42 | + message: result.message ?? '삭제하지 못했습니다.', | |
| 43 | + }); | |
| 44 | + return; | |
| 45 | + } | |
| 46 | + showToast({ variant: 'success', message: '아이템을 삭제했습니다.' }); | |
| 47 | + }); | |
| 48 | + } | |
| 49 | + | |
| 50 | + function confirmDelete() { | |
| 51 | + showAlert({ | |
| 52 | + variant: 'danger', | |
| 53 | + title: '아이템을 삭제하시겠습니까?', | |
| 54 | + message: `${item.name}(${item.itemId})을(를) 삭제합니다. 삭제 후에는 되돌릴 수 없습니다.`, | |
| 55 | + actions: ( | |
| 56 | + <> | |
| 57 | + <Button type="button" variant="ghost" onClick={hideAlert}> | |
| 58 | + 취소 | |
| 59 | + </Button> | |
| 60 | + <Button type="button" variant="primary" onClick={runDelete}> | |
| 61 | + 삭제 | |
| 62 | + </Button> | |
| 63 | + </> | |
| 64 | + ), | |
| 65 | + }); | |
| 66 | + } | |
| 67 | + | |
| 68 | + return ( | |
| 69 | + <div className="flex items-center gap-1"> | |
| 70 | + <Button | |
| 71 | + type="button" | |
| 72 | + variant="ghost" | |
| 73 | + size="sm" | |
| 74 | + onClick={() => setIsEditOpen(true)} | |
| 75 | + > | |
| 76 | + 수정 | |
| 77 | + </Button> | |
| 78 | + <Button | |
| 79 | + type="button" | |
| 80 | + variant="ghost" | |
| 81 | + size="sm" | |
| 82 | + onClick={confirmDelete} | |
| 83 | + disabled={isDeleting} | |
| 84 | + > | |
| 85 | + {isDeleting ? '삭제 중...' : '삭제'} | |
| 86 | + </Button> | |
| 87 | + | |
| 88 | + {isEditOpen && ( | |
| 89 | + <DecorationItemEditModal | |
| 90 | + item={item} | |
| 91 | + typeTotalCount={typeTotalCount} | |
| 92 | + onClose={() => setIsEditOpen(false)} | |
| 93 | + /> | |
| 94 | + )} | |
| 95 | + </div> | |
| 96 | + ); | |
| 97 | +} |
Add a comment
Delete comment
Once you delete this comment, you won't be able to recover it. Are you sure you want to delete this comment?