From 95307885e6a99bb179dd885ab166dbb8eb4b4b4e Mon Sep 17 00:00:00 2001 From: sispo3314 Date: Wed, 15 Oct 2025 11:44:23 +0900 Subject: [PATCH 1/4] =?UTF-8?q?feat:=20=ED=9A=8C=EC=9B=90=20=ED=83=88?= =?UTF-8?q?=ED=87=B4=20API=20=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/user/profile.api.ts | 13 +++++++++++ src/pages/home/MyPage.tsx | 46 ++++++++++++++++++++++++++++++++++++- 2 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 src/api/user/profile.api.ts 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/pages/home/MyPage.tsx b/src/pages/home/MyPage.tsx index 48f3660..40ccabb 100644 --- a/src/pages/home/MyPage.tsx +++ b/src/pages/home/MyPage.tsx @@ -3,6 +3,7 @@ import { useNavigate } from 'react-router-dom'; import Header from '@/component/Header'; import Sidebar from '@/component/SideBar'; import api from '@/api/api'; +import { withdrawAccount } from '@/api/user/profile.api'; import type { ApiResponse } from '@/types/api-response'; type Profile = { @@ -16,6 +17,7 @@ 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 [nickname, setNickname] = useState(''); const [saving, setSaving] = useState(false); @@ -142,6 +144,46 @@ export default function MyPage() { return !saving && trimmed.length >= 2 && trimmed !== prev; })(); + const handleWithdraw = async () => { + if (withdrawing) return; + const ok = window.confirm( + '정말로 회원 탈퇴하시겠어요?\n탈퇴 시 계정 및 데이터가 삭제될 수 있습니다.', + ); + if (!ok) 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 {} + 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'); + 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 +269,9 @@ export default function MyPage() { From 94e9077fb1c8c691fd89d772223fdaf0807f0a24 Mon Sep 17 00:00:00 2001 From: sispo3314 Date: Wed, 15 Oct 2025 17:53:23 +0900 Subject: [PATCH 2/4] =?UTF-8?q?feat:=20=ED=9A=8C=EC=9B=90=20=ED=83=88?= =?UTF-8?q?=ED=87=B4=20=ED=99=95=EC=9D=B8=20=EB=AA=A8=EB=8B=AC=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/component/common/Modal/ConfirmModal.tsx | 56 +++++++++++++++++++++ src/component/index.tsx | 2 + src/pages/home/MyPage.tsx | 32 ++++++++---- 3 files changed, 81 insertions(+), 9 deletions(-) create mode 100644 src/component/common/Modal/ConfirmModal.tsx 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/home/MyPage.tsx b/src/pages/home/MyPage.tsx index 40ccabb..9d6b08d 100644 --- a/src/pages/home/MyPage.tsx +++ b/src/pages/home/MyPage.tsx @@ -1,7 +1,6 @@ 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'; @@ -18,6 +17,7 @@ export default function MyPage() { 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); @@ -144,13 +144,10 @@ export default function MyPage() { return !saving && trimmed.length >= 2 && trimmed !== prev; })(); - const handleWithdraw = async () => { - if (withdrawing) return; - const ok = window.confirm( - '정말로 회원 탈퇴하시겠어요?\n탈퇴 시 계정 및 데이터가 삭제될 수 있습니다.', - ); - if (!ok) return; + const openWithdrawModal = () => setWithdrawOpen(true); + const handleWithdrawConfirm = async () => { + if (withdrawing) return; setWithdrawing(true); try { const res = await withdrawAccount(); @@ -161,6 +158,7 @@ export default function MyPage() { localStorage.removeItem('userId'); localStorage.removeItem('nickname'); } catch {} + setWithdrawOpen(false); navigate('/', { replace: true }); return; } @@ -170,6 +168,7 @@ export default function MyPage() { if (status === 401) { localStorage.removeItem('accessToken'); localStorage.removeItem('refreshToken'); + setWithdrawOpen(false); navigate('/', { replace: true }); return; } @@ -269,7 +268,7 @@ export default function MyPage() {
+ !withdrawing && setWithdrawOpen(false)} + /> ); } From c8f56c6eaff6ca5e5dcc0f442d980bbc1b535823 Mon Sep 17 00:00:00 2001 From: sispo3314 Date: Wed, 15 Oct 2025 18:25:44 +0900 Subject: [PATCH 3/4] =?UTF-8?q?feat:=20=EA=B2=80=EC=83=89=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/Myplace/myPlace.api.ts | 13 +++++++-- src/pages/explore/Detail.tsx | 0 src/pages/home/MyTravelList.tsx | 52 ++++++++++----------------------- 3 files changed, 26 insertions(+), 39 deletions(-) delete mode 100644 src/pages/explore/Detail.tsx 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/pages/explore/Detail.tsx b/src/pages/explore/Detail.tsx deleted file mode 100644 index e69de29..0000000 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 && (