diff --git a/src/api/Myplace/myPlace.api.ts b/src/api/Myplace/myPlace.api.ts index 66263c3..e1cffa2 100644 --- a/src/api/Myplace/myPlace.api.ts +++ b/src/api/Myplace/myPlace.api.ts @@ -75,7 +75,14 @@ export async function getSaveStatus(contentId: string): Promise { } //내 저장 목록 -export async function getSavedPlaces(page = 0, size = 20): Promise { - const res = await api.get('/my/places', { params: { page, size } }); - return res.data; +export async function getSavedPlaces( + params: { page?: number; size?: number; keyword?: string } = {}, +): Promise { + const { page = 0, size = 20, keyword } = params; + const kw = (keyword ?? '').trim(); + + const res = await api.get>('/my/places', { + params: { page, size, ...(kw ? { keyword: kw } : {}) }, + }); + return res.data.data; } diff --git a/src/api/user/profile.api.ts b/src/api/user/profile.api.ts new file mode 100644 index 0000000..3e3c99b --- /dev/null +++ b/src/api/user/profile.api.ts @@ -0,0 +1,13 @@ +import api from '../api'; +import type { ApiResponse } from '@/types/api-response'; + +export type profile = { + userId: number; + email: string; + nickname: string; +}; + +export async function withdrawAccount() { + const { data } = await api.patch>('/my/profile/withdraw'); + return data; +} diff --git a/src/component/common/Modal/ConfirmModal.tsx b/src/component/common/Modal/ConfirmModal.tsx new file mode 100644 index 0000000..f9e4fb7 --- /dev/null +++ b/src/component/common/Modal/ConfirmModal.tsx @@ -0,0 +1,56 @@ +export default function ConfirmModal({ + open, + title = '확인', + description = '이 작업을 진행하시겠습니까?', + confirmText = '확인', + cancelText = '취소', + loading = false, + onConfirm, + onClose, +}: { + open: boolean; + title?: string; + description?: string; + confirmText?: string; + cancelText?: string; + loading?: boolean; + onConfirm: () => void; + onClose: () => void; +}) { + if (!open) return null; + + return ( +
+
!loading && onClose()} /> +
+ {/* 제목 */} +

{title}

+ + {/* 내용 */} +

+ {description} +

+ + {/* 버튼 영역 */} +
+ + +
+
+
+ ); +} diff --git a/src/component/index.tsx b/src/component/index.tsx index 242abd3..bdbded1 100644 --- a/src/component/index.tsx +++ b/src/component/index.tsx @@ -10,8 +10,10 @@ import SelectorMulti from './selector/SelectorMulti'; import Selector from './selector/Selector'; import ParkingTable from './ai_explore/ParkingInfo'; import Loader from './common/Loading/Loading'; +import ConfirmModal from './common/Modal/ConfirmModal'; export { + ConfirmModal, Button, TagButton, Badge, diff --git a/src/pages/ai/MainAI.tsx b/src/pages/ai/MainAI.tsx index 2c7dbbe..5b4b6ee 100644 --- a/src/pages/ai/MainAI.tsx +++ b/src/pages/ai/MainAI.tsx @@ -12,7 +12,7 @@ export default function AiExplorePage() { const navigate = useNavigate(); const address = useAIExploreStore((s) => s.address); const theme = useAIExploreStore((s) => s.theme); - const themeCodes = useAIExploreStore((s) => s.themeCodes); // ✅ 추가 + const themeCodes = useAIExploreStore((s) => s.themeCodes); const distanceKm = useAIExploreStore((s) => s.distanceKm); const setAddress = useAIExploreStore((s) => s.setAddress); const setDistanceKm = useAIExploreStore((s) => s.setDistanceKm); diff --git a/src/pages/explore/Detail.tsx b/src/pages/explore/Detail.tsx deleted file mode 100644 index e69de29..0000000 diff --git a/src/pages/home/MyPage.tsx b/src/pages/home/MyPage.tsx index 48f3660..9d6b08d 100644 --- a/src/pages/home/MyPage.tsx +++ b/src/pages/home/MyPage.tsx @@ -1,8 +1,8 @@ import { useEffect, useState } from 'react'; import { useNavigate } from 'react-router-dom'; -import Header from '@/component/Header'; -import Sidebar from '@/component/SideBar'; +import { Header, Sidebar, ConfirmModal } from '@/component'; import api from '@/api/api'; +import { withdrawAccount } from '@/api/user/profile.api'; import type { ApiResponse } from '@/types/api-response'; type Profile = { @@ -16,6 +16,8 @@ export default function MyPage() { const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [profile, setProfile] = useState(null); + const [withdrawing, setWithdrawing] = useState(false); + const [withdrawOpen, setWithdrawOpen] = useState(false); const [nickname, setNickname] = useState(''); const [saving, setSaving] = useState(false); @@ -142,6 +144,45 @@ export default function MyPage() { return !saving && trimmed.length >= 2 && trimmed !== prev; })(); + const openWithdrawModal = () => setWithdrawOpen(true); + + const handleWithdrawConfirm = async () => { + if (withdrawing) return; + setWithdrawing(true); + try { + const res = await withdrawAccount(); + if (res?.success) { + try { + localStorage.removeItem('accessToken'); + localStorage.removeItem('refreshToken'); + localStorage.removeItem('userId'); + localStorage.removeItem('nickname'); + } catch {} + setWithdrawOpen(false); + navigate('/', { replace: true }); + return; + } + alert(res?.message || '회원 탈퇴 처리 중 문제가 발생했습니다. 다시 시도해 주세요.'); + } catch (err: any) { + const status = err?.response?.status as number | undefined; + if (status === 401) { + localStorage.removeItem('accessToken'); + localStorage.removeItem('refreshToken'); + setWithdrawOpen(false); + navigate('/', { replace: true }); + return; + } + const msg = + err?.response?.data?.error?.message || + err?.response?.data?.message || + err?.message || + '서버 오류가 발생했습니다. 잠시 후 다시 시도해 주세요.'; + alert(msg); + } finally { + setWithdrawing(false); + } + }; + return (
@@ -227,7 +268,9 @@ export default function MyPage() { @@ -235,6 +278,21 @@ export default function MyPage() {
+ !withdrawing && setWithdrawOpen(false)} + /> ); } diff --git a/src/pages/home/MyTravelList.tsx b/src/pages/home/MyTravelList.tsx index 1ec10ff..c0834d4 100644 --- a/src/pages/home/MyTravelList.tsx +++ b/src/pages/home/MyTravelList.tsx @@ -1,21 +1,14 @@ -import { useEffect, useMemo, useState } from 'react'; +import { useEffect, useState } from 'react'; import Header from '@/component/Header'; import Sidebar from '@/component/SideBar'; import SearchIcon from '@/image/Search.svg'; import PlaceCard from '@/component/common/Card/PlaceCard'; import { getSavedPlaces, unsavePlace, type SavedPlaceItem } from '@/api/Myplace/myPlace.api'; import { useNavigate } from 'react-router-dom'; -import SortPillSelect, { type Option } from '@/component/selector/SortPillSelect'; const PAGE_SIZE = 20; type Row = SavedPlaceItem; -const arrangeOptions: Option<'O' | 'Q' | 'R' | 'S'>[] = [ - { value: 'O', label: '기본순' }, - { value: 'Q', label: '수정일순' }, - { value: 'R', label: '등록일순' }, - { value: 'S', label: '거리순' }, -]; const MyTravelList = () => { const [isSidebarOpen, setIsSidebarOpen] = useState(false); const [items, setItems] = useState([]); @@ -24,21 +17,28 @@ const MyTravelList = () => { const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [q, setQ] = useState(''); - const [arrange, setArrange] = useState<'O' | 'Q' | 'R' | 'S'>('O'); + + //검색 디바운스 + useEffect(() => { + const h = setTimeout(() => { + const kw = q.trim(); + loadPage(0, true, kw); + }, 300); + return () => clearTimeout(h); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [q]); const handleMenuClick = () => setIsSidebarOpen(true); const handleCloseSidebar = () => setIsSidebarOpen(false); const navigate = useNavigate(); - const loadPage = async (p: number, replace = false) => { + const loadPage = async (p: number, replace = false, keyword = '') => { if (loading) return; setLoading(true); setError(null); try { - const raw = (await getSavedPlaces(p, PAGE_SIZE)) as any; - const pageData = raw?.content ? raw : (raw?.data ?? {}); + const pageData = await getSavedPlaces({ page: p, size: PAGE_SIZE, keyword }); const content = Array.isArray(pageData.content) ? pageData.content : []; - setItems((prev) => (replace ? content : [...prev, ...content])); setPage(pageData.page ?? p); setLast(!!pageData.last); @@ -51,7 +51,7 @@ const MyTravelList = () => { }; useEffect(() => { - loadPage(0, true); + loadPage(0, true, ''); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); @@ -73,15 +73,6 @@ const MyTravelList = () => { } }; - const filtered = useMemo(() => { - if (!q.trim()) return items; - const kw = q.trim().toLowerCase(); - return items.filter((it) => { - const hay = `${it.placeName ?? ''} ${it.themeName ?? ''} ${it.contentId}`.toLowerCase(); - return hay.includes(kw); - }); - }, [items, q]); - return (
@@ -104,26 +95,15 @@ const MyTravelList = () => { search
- {/*정렬*/} -
- -
{error && (
{error}
)}
- {filtered.map((row) => { + {items.map((row) => { const { contentId, themeName, likeCount, cnctrLevel } = row; - const title = row.placeName || String(contentId); - return ( {
{!last && (