feat: 서비스 관리 삭제 기능 추가 (#49) #50
|
|
@ -79,3 +79,10 @@ export function deleteApns(serviceCode: string) {
|
||||||
`/v1/in/service/${serviceCode}/apns/delete`,
|
`/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 {
|
interface ServiceHeaderCardProps {
|
||||||
service: ServiceDetail;
|
service: ServiceDetail;
|
||||||
onShowApiKey: () => void;
|
onShowApiKey: () => void;
|
||||||
|
onDelete: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ServiceHeaderCard({
|
export default function ServiceHeaderCard({
|
||||||
service,
|
service,
|
||||||
onShowApiKey,
|
onShowApiKey,
|
||||||
|
onDelete,
|
||||||
}: ServiceHeaderCardProps) {
|
}: ServiceHeaderCardProps) {
|
||||||
return (
|
return (
|
||||||
<div className="bg-white border border-gray-200 rounded-lg shadow-sm p-6 mb-6">
|
<div className="bg-white border border-gray-200 rounded-lg shadow-sm p-6 mb-6">
|
||||||
|
|
@ -62,6 +64,14 @@ export default function ServiceHeaderCard({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<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
|
<Link
|
||||||
to={`/services/${service.serviceCode}/edit`}
|
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"
|
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"
|
||||||
|
|
@ -70,6 +80,7 @@ export default function ServiceHeaderCard({
|
||||||
<span>수정하기</span>
|
<span>수정하기</span>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* 메타 정보 */}
|
{/* 메타 정보 */}
|
||||||
<div className="mt-6 pt-5 border-t border-gray-100 grid grid-cols-2 sm:grid-cols-4 gap-6">
|
<div className="mt-6 pt-5 border-t border-gray-100 grid grid-cols-2 sm:grid-cols-4 gap-6">
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,19 @@
|
||||||
import { useState, useEffect, useCallback } from "react";
|
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 PageHeader from "@/components/common/PageHeader";
|
||||||
import CopyButton from "@/components/common/CopyButton";
|
import CopyButton from "@/components/common/CopyButton";
|
||||||
import ServiceHeaderCard from "../components/ServiceHeaderCard";
|
import ServiceHeaderCard from "../components/ServiceHeaderCard";
|
||||||
import ServiceStatsCards from "../components/ServiceStatsCards";
|
import ServiceStatsCards from "../components/ServiceStatsCards";
|
||||||
import PlatformManagement from "../components/PlatformManagement";
|
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 { fetchDashboard } from "@/api/dashboard.api";
|
||||||
import type { ServiceDetail } from "../types";
|
import type { ServiceDetail } from "../types";
|
||||||
import type { DashboardKpi } from "@/features/dashboard/types";
|
import type { DashboardKpi } from "@/features/dashboard/types";
|
||||||
|
|
||||||
export default function ServiceDetailPage() {
|
export default function ServiceDetailPage() {
|
||||||
const { id } = useParams<{ id: string }>();
|
const { id } = useParams<{ id: string }>();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
// 데이터 상태
|
// 데이터 상태
|
||||||
const [service, setService] = useState<ServiceDetail | null>(null);
|
const [service, setService] = useState<ServiceDetail | null>(null);
|
||||||
|
|
@ -26,6 +28,10 @@ export default function ServiceDetailPage() {
|
||||||
const [fullApiKey, setFullApiKey] = useState<string | null>(null);
|
const [fullApiKey, setFullApiKey] = useState<string | null>(null);
|
||||||
const [apiKeyLoading, setApiKeyLoading] = useState(false);
|
const [apiKeyLoading, setApiKeyLoading] = useState(false);
|
||||||
|
|
||||||
|
// 삭제 모달 상태
|
||||||
|
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
||||||
|
const [deleting, setDeleting] = useState(false);
|
||||||
|
|
||||||
// 서비스 상세 + 통계 로드
|
// 서비스 상세 + 통계 로드
|
||||||
const loadData = useCallback(async (serviceCode: string) => {
|
const loadData = useCallback(async (serviceCode: string) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
@ -105,6 +111,21 @@ export default function ServiceDetailPage() {
|
||||||
setFullApiKey(null);
|
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) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
|
|
@ -168,6 +189,7 @@ export default function ServiceDetailPage() {
|
||||||
<ServiceHeaderCard
|
<ServiceHeaderCard
|
||||||
service={service}
|
service={service}
|
||||||
onShowApiKey={handleShowApiKey}
|
onShowApiKey={handleShowApiKey}
|
||||||
|
onDelete={() => setShowDeleteConfirm(true)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ServiceStatsCards
|
<ServiceStatsCards
|
||||||
|
|
@ -183,6 +205,55 @@ export default function ServiceDetailPage() {
|
||||||
|
|
||||||
<PlatformManagement service={service} onRefresh={handleRefresh} />
|
<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 키 확인 모달 */}
|
{/* API 키 확인 모달 */}
|
||||||
{showApiKey && (
|
{showApiKey && (
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user