Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions src/api/Myplace/myPlace.api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,14 @@ export async function getSaveStatus(contentId: string): Promise<boolean> {
}

//내 저장 목록
export async function getSavedPlaces(page = 0, size = 20): Promise<SavedPlacePage> {
const res = await api.get<SavedPlacePage>('/my/places', { params: { page, size } });
return res.data;
export async function getSavedPlaces(
params: { page?: number; size?: number; keyword?: string } = {},
): Promise<SavedPlacePage> {
const { page = 0, size = 20, keyword } = params;
const kw = (keyword ?? '').trim();

const res = await api.get<ApiResponse<SavedPlacePage>>('/my/places', {
params: { page, size, ...(kw ? { keyword: kw } : {}) },
});
return res.data.data;
}
13 changes: 13 additions & 0 deletions src/api/user/profile.api.ts
Original file line number Diff line number Diff line change
@@ -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<ApiResponse<null>>('/my/profile/withdraw');
return data;
}
56 changes: 56 additions & 0 deletions src/component/common/Modal/ConfirmModal.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="fixed inset-0 z-50 flex items-center justify-center">
<div className="absolute inset-0 bg-black/40" onClick={() => !loading && onClose()} />
<div className="relative w-[90%] max-w-[360px] rounded-l bg-white p-6 shadow-xl">
{/* 제목 */}
<h2 className="text-title4 text-green1 font-semibold">{title}</h2>

{/* 내용 */}
<p className="text-body1 text-green1 mt-3 leading-relaxed whitespace-pre-line">
{description}
</p>

{/* 버튼 영역 */}
<div className="mt-8 flex justify-end gap-4">
<button
type="button"
disabled={loading}
onClick={onClose}
className="text-caption2 text-green-muted hover:text-green1 disabled:opacity-50"
>
{cancelText}
</button>
<button
type="button"
disabled={loading}
onClick={onConfirm}
className="rounded-m bg-red1 text-caption2 btn-text-white px-4 py-2 hover:brightness-95 disabled:opacity-60"
>
{loading ? '처리 중…' : confirmText}
</button>
</div>
</div>
</div>
);
}
2 changes: 2 additions & 0 deletions src/component/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion src/pages/ai/MainAI.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Empty file removed src/pages/explore/Detail.tsx
Empty file.
64 changes: 61 additions & 3 deletions src/pages/home/MyPage.tsx
Original file line number Diff line number Diff line change
@@ -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 = {
Expand All @@ -16,6 +16,8 @@ export default function MyPage() {
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [profile, setProfile] = useState<Profile | null>(null);
const [withdrawing, setWithdrawing] = useState(false);
const [withdrawOpen, setWithdrawOpen] = useState(false);

const [nickname, setNickname] = useState('');
const [saving, setSaving] = useState(false);
Expand Down Expand Up @@ -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 (
<div className="bg-beige3 flex min-h-screen flex-col">
<Header onMenuClick={handleMenuClick} />
Expand Down Expand Up @@ -227,14 +268,31 @@ export default function MyPage() {
</span>
<button
type="button"
className="text-caption2 text-red-600/80 underline underline-offset-[3px] hover:text-red-700 focus:outline-none"
onClick={openWithdrawModal}
disabled={withdrawing}
className="text-caption2 cursor-pointer text-red-600/80 underline underline-offset-[3px] hover:text-red-700 focus:outline-none"
>
회원 탈퇴
</button>
</div>
</div>
</div>
</div>
<ConfirmModal
open={withdrawOpen}
title="회원 탈퇴 안내"
description={`회원 탈퇴를 신청하시면 계정이 탈퇴 대기 상태로 전환되며,
30일 동안 다시 로그인하지 않으면 모든 정보가 영구 삭제됩니다.

30일 이내 로그인 시 탈퇴가 자동으로 취소됩니다.

정말 탈퇴를 진행하시겠습니까?`}
confirmText="탈퇴하기"
cancelText="취소"
loading={withdrawing}
onConfirm={handleWithdrawConfirm}
onClose={() => !withdrawing && setWithdrawOpen(false)}
/>
</div>
);
}
52 changes: 16 additions & 36 deletions src/pages/home/MyTravelList.tsx
Original file line number Diff line number Diff line change
@@ -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<Row[]>([]);
Expand All @@ -24,21 +17,28 @@ const MyTravelList = () => {
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(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);
Expand All @@ -51,7 +51,7 @@ const MyTravelList = () => {
};

useEffect(() => {
loadPage(0, true);
loadPage(0, true, '');
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

Expand All @@ -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 (
<div className="min-h-screen">
<Header onMenuClick={handleMenuClick} />
Expand All @@ -104,26 +95,15 @@ const MyTravelList = () => {
<img src={SearchIcon} alt="search" className="h-4 w-4" />
</span>
</div>
{/*정렬*/}
<div className="-mt-2 mb-4 flex justify-end px-9">
<SortPillSelect
value={arrange}
options={arrangeOptions}
onChange={setArrange}
size="sm"
/>
</div>

{error && (
<div className="mb-3 rounded-md bg-red-100 px-3 py-2 text-sm text-red-700">{error}</div>
)}

<div className="flex flex-col gap-3 px-9">
{filtered.map((row) => {
{items.map((row) => {
const { contentId, themeName, likeCount, cnctrLevel } = row;

const title = row.placeName || String(contentId);

return (
<PlaceCard
key={String(row.contentId)}
Expand All @@ -143,7 +123,7 @@ const MyTravelList = () => {
<div className="mt-6 flex justify-center">
{!last && (
<button
onClick={() => loadPage(page + 1)}
onClick={() => loadPage(page + 1, false, q.trim())}
disabled={loading}
className="rounded-full bg-[var(--color-green4)] px-4 py-2 text-sm text-white disabled:opacity-50"
>
Expand Down