feat: 서비스 관리 삭제 기능 추가 (#49) #50
|
|
@ -79,3 +79,10 @@ export function deleteApns(serviceCode: string) {
|
|||
`/v1/in/service/${serviceCode}/apns/delete`,
|
||||
);
|
||||
}
|
||||
|
||||
/** 서비스 삭제 */
|
||||
export function deleteService(serviceCode: string) {
|
||||
return apiClient.post<ApiResponse<null>>("/v1/in/service/delete", {
|
||||
service_code: serviceCode,
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,11 +9,13 @@ import { SERVICE_STATUS } from "../types";
|
|||
interface ServiceHeaderCardProps {
|
||||
service: ServiceDetail;
|
||||
onShowApiKey: () => void;
|
||||
onDelete: () => void;
|
||||
}
|
||||
|
||||
export default function ServiceHeaderCard({
|
||||
service,
|
||||
onShowApiKey,
|
||||
onDelete,
|
||||
}: ServiceHeaderCardProps) {
|
||||
return (
|
||||
<div className="bg-white border border-gray-200 rounded-lg shadow-sm p-6 mb-6">
|
||||
|
|
@ -62,13 +64,22 @@ export default function ServiceHeaderCard({
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Link
|
||||
to={`/services/${service.serviceCode}/edit`}
|
||||
className="bg-[#2563EB] hover:bg-[#1d4ed8] text-white px-5 py-2.5 rounded text-sm font-medium transition shadow-sm flex items-center gap-2"
|
||||
>
|
||||
<span className="material-symbols-outlined text-base">edit</span>
|
||||
<span>수정하기</span>
|
||||
</Link>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={onDelete}
|
||||
className="border border-red-200 text-red-500 hover:bg-red-50 px-5 py-2.5 rounded text-sm font-medium transition flex items-center gap-2"
|
||||
>
|
||||
<span className="material-symbols-outlined text-base">delete</span>
|
||||
<span>삭제하기</span>
|
||||
</button>
|
||||
<Link
|
||||
to={`/services/${service.serviceCode}/edit`}
|
||||
className="bg-[#2563EB] hover:bg-[#1d4ed8] text-white px-5 py-2.5 rounded text-sm font-medium transition shadow-sm flex items-center gap-2"
|
||||
>
|
||||
<span className="material-symbols-outlined text-base">edit</span>
|
||||
<span>수정하기</span>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 메타 정보 */}
|
||||
|
|
|
|||
|
|
@ -1,17 +1,19 @@
|
|||
import { useState, useEffect, useCallback } from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { useParams, useNavigate } from "react-router-dom";
|
||||
import { toast } from "sonner";
|
||||
import PageHeader from "@/components/common/PageHeader";
|
||||
import CopyButton from "@/components/common/CopyButton";
|
||||
import ServiceHeaderCard from "../components/ServiceHeaderCard";
|
||||
import ServiceStatsCards from "../components/ServiceStatsCards";
|
||||
import PlatformManagement from "../components/PlatformManagement";
|
||||
import { fetchServiceDetail, fetchApiKey } from "@/api/service.api";
|
||||
import { fetchServiceDetail, fetchApiKey, deleteService } from "@/api/service.api";
|
||||
import { fetchDashboard } from "@/api/dashboard.api";
|
||||
import type { ServiceDetail } from "../types";
|
||||
import type { DashboardKpi } from "@/features/dashboard/types";
|
||||
|
||||
export default function ServiceDetailPage() {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const navigate = useNavigate();
|
||||
|
||||
// 데이터 상태
|
||||
const [service, setService] = useState<ServiceDetail | null>(null);
|
||||
|
|
@ -26,6 +28,10 @@ export default function ServiceDetailPage() {
|
|||
const [fullApiKey, setFullApiKey] = useState<string | null>(null);
|
||||
const [apiKeyLoading, setApiKeyLoading] = useState(false);
|
||||
|
||||
// 삭제 모달 상태
|
||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
||||
const [deleting, setDeleting] = useState(false);
|
||||
|
||||
// 서비스 상세 + 통계 로드
|
||||
const loadData = useCallback(async (serviceCode: string) => {
|
||||
setLoading(true);
|
||||
|
|
@ -105,6 +111,21 @@ export default function ServiceDetailPage() {
|
|||
setFullApiKey(null);
|
||||
};
|
||||
|
||||
// 서비스 삭제
|
||||
const handleDelete = async () => {
|
||||
if (!id) return;
|
||||
setDeleting(true);
|
||||
try {
|
||||
await deleteService(id);
|
||||
toast.success("서비스가 삭제되었습니다.");
|
||||
navigate("/services");
|
||||
} catch {
|
||||
toast.error("서비스 삭제에 실패했습니다.");
|
||||
} finally {
|
||||
setDeleting(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 로딩 스켈레톤
|
||||
if (loading) {
|
||||
return (
|
||||
|
|
@ -168,6 +189,7 @@ export default function ServiceDetailPage() {
|
|||
<ServiceHeaderCard
|
||||
service={service}
|
||||
onShowApiKey={handleShowApiKey}
|
||||
onDelete={() => setShowDeleteConfirm(true)}
|
||||
/>
|
||||
|
||||
<ServiceStatsCards
|
||||
|
|
@ -183,6 +205,55 @@ export default function ServiceDetailPage() {
|
|||
|
||||
<PlatformManagement service={service} onRefresh={handleRefresh} />
|
||||
|
||||
{/* 삭제 확인 모달 */}
|
||||
{showDeleteConfirm && (
|
||||
<div className="fixed inset-0 z-[60] flex items-center justify-center">
|
||||
<div
|
||||
className="absolute inset-0 bg-black/40"
|
||||
onClick={() => setShowDeleteConfirm(false)}
|
||||
/>
|
||||
<div className="relative bg-white rounded-xl shadow-2xl max-w-md w-full mx-4 p-6 border border-gray-200">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="size-10 rounded-full bg-red-100 flex items-center justify-center flex-shrink-0">
|
||||
<span className="material-symbols-outlined text-red-600 text-xl">
|
||||
warning
|
||||
</span>
|
||||
</div>
|
||||
<h3 className="text-lg font-bold text-[#0f172a]">서비스 삭제</h3>
|
||||
</div>
|
||||
<p className="text-sm text-[#0f172a] mb-2">
|
||||
<span className="font-semibold">{service.serviceName}</span> 서비스를 삭제하시겠습니까?
|
||||
</p>
|
||||
<div className="bg-red-50 border border-red-200 rounded-lg px-4 py-3 mb-5 flex flex-col gap-1.5">
|
||||
<div className="flex items-center gap-1.5 text-red-700 text-xs font-medium">
|
||||
<span className="material-symbols-outlined flex-shrink-0" style={{ fontSize: "14px" }}>info</span>
|
||||
<span>삭제된 서비스는 복구할 수 없습니다.</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5 text-red-700 text-xs font-medium">
|
||||
<span className="material-symbols-outlined flex-shrink-0" style={{ fontSize: "14px" }}>info</span>
|
||||
<span>해당 서비스의 모든 기기 및 발송 이력이 비활성화됩니다.</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-end gap-3">
|
||||
<button
|
||||
onClick={() => setShowDeleteConfirm(false)}
|
||||
disabled={deleting}
|
||||
className="bg-white border border-gray-300 hover:bg-gray-50 text-[#0f172a] px-5 py-2.5 rounded text-sm font-medium transition disabled:opacity-50"
|
||||
>
|
||||
취소
|
||||
</button>
|
||||
<button
|
||||
onClick={handleDelete}
|
||||
disabled={deleting}
|
||||
className="bg-red-500 hover:bg-red-600 text-white px-5 py-2.5 rounded text-sm font-medium transition shadow-sm disabled:opacity-50"
|
||||
>
|
||||
{deleting ? "삭제 중..." : "삭제"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* API 키 확인 모달 */}
|
||||
{showApiKey && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user