feat: 서비스 관리 삭제 기능 추가 (#49) #50

Merged
seonkyu.kim merged 1 commits from feature/SPMS-49-service-delete into develop 2026-03-18 04:00:24 +00:00
3 changed files with 98 additions and 9 deletions
Showing only changes of commit c872985f96 - Show all commits

View File

@ -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,
});
}

View File

@ -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>
{/* 메타 정보 */}

View File

@ -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">