From de8e72b9bb05a621ee5da556328316790aea1d3f Mon Sep 17 00:00:00 2001 From: wkdjh Date: Sun, 17 Aug 2025 13:34:00 +0900 Subject: [PATCH 01/29] =?UTF-8?q?feat:=20=EC=9D=B4=EB=A9=94=EC=9D=BC=20?= =?UTF-8?q?=EC=B0=BE=EA=B8=B0,=20=EB=B9=84=EB=B0=80=EB=B2=88=ED=98=B8=20?= =?UTF-8?q?=EC=9E=AC=EC=84=A4=EC=A0=95=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/login/find-email/page.tsx | 75 +++++++++++ app/login/forgot-password/page.tsx | 79 ++++++++++++ app/login/page.tsx | 54 +++++--- app/login/reset-password/page.tsx | 191 +++++++++++++++++++++++++++++ app/mypage/applications/page.tsx | 2 +- app/mypage/my-posts/page.tsx | 6 +- app/mypage/page.tsx | 2 +- app/mypage/profile-edit/page.tsx | 2 +- components/post-detail.tsx | 57 ++++++++- lib/api-client.ts | 32 ++++- 10 files changed, 468 insertions(+), 32 deletions(-) create mode 100644 app/login/find-email/page.tsx create mode 100644 app/login/forgot-password/page.tsx create mode 100644 app/login/reset-password/page.tsx diff --git a/app/login/find-email/page.tsx b/app/login/find-email/page.tsx new file mode 100644 index 0000000..af586d7 --- /dev/null +++ b/app/login/find-email/page.tsx @@ -0,0 +1,75 @@ +"use client" + +import { useState } from "react" +import { apiClient } from "@/lib/api-client" +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" +import Link from "next/link" + +export default function FindEmailPage() { + const [nickname, setNickname] = useState("") + const [loading, setLoading] = useState(false) + const [error, setError] = useState(null) + const [maskedEmail, setMaskedEmail] = useState(null) + + const onSubmit = async (e: React.FormEvent) => { + e.preventDefault() + setLoading(true); setError(null); setMaskedEmail(null) + try { + const res = await apiClient.findEmail(nickname.trim()) + const email = (res as any)?.data?.email ?? (res as any)?.email + if (!email) { + setError("일치하는 계정이 없습니다.") + } else { + setMaskedEmail(email) // 마스킹된 이메일이 내려옴 + } + } catch (e: any) { + setError(e?.message || "조회 중 오류가 발생했습니다.") + } finally { + setLoading(false) + } + } + + return ( +
+
+

이메일 찾기

+ + {error &&
{error}
} + {maskedEmail && ( +
+ 이메일: {maskedEmail} +
+ )} + +
+
+ + setNickname(e.target.value)} + placeholder="닉네임을 입력하세요" + required + /> +
+ +
+ +
+ 비밀번호가 기억나지 않나요?{" "} + + 비밀번호 재설정 + +
+ +
+ ← 로그인으로 돌아가기 +
+
+
+ ) +} diff --git a/app/login/forgot-password/page.tsx b/app/login/forgot-password/page.tsx new file mode 100644 index 0000000..8298359 --- /dev/null +++ b/app/login/forgot-password/page.tsx @@ -0,0 +1,79 @@ +"use client" + +import { useState } from "react" +import { apiClient } from "@/lib/api-client" +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" +import Link from "next/link" + +export default function ForgotPasswordPage() { + const [email, setEmail] = useState("") + const [loading, setLoading] = useState(false) + const [notice, setNotice] = useState(null) + const [error, setError] = useState(null) + + const onSubmit = async (e: React.FormEvent) => { + e.preventDefault() + setLoading(true); setError(null); setNotice(null) + try { + const res = await apiClient.requestPasswordReset(email.trim()) + + if (res?.code === "USER208") { + setNotice("비밀번호 재설정 링크를 이메일로 보냈습니다. 받은 편지함/스팸함을 확인하세요.") + return + } + + if (res?.code === "USER408") { + setError("가입된 이메일이 아닙니다.") + return + } + + } catch (err: any) { + const code = + err?.code || + err?.response?.data?.code || + err?.data?.code + + if (code === "USRE208") { + setNotice("비밀번호 재설정 링크를 이메일로 보냈습니다. 받은 편지함/스팸함을 확인하세요.") + } else if (code === "USER408") { + setError("가입된 이메일이 아닙니다.") + } + else { + setError(err?.response?.data?.message || err?.message || "비밀번호 재설정 요청 중 오류가 발생했습니다.") + } + } finally { + setLoading(false) + } +} + + + return ( +
+
+

비밀번호 재설정

+ + {error &&
{error}
} + {notice &&
{notice}
} + +
+
+ + setEmail(e.target.value)} placeholder="email@example.com" required /> +
+ +
+ +
+ + ← 로그인으로 돌아가기 + +
+
+
+ ) +} diff --git a/app/login/page.tsx b/app/login/page.tsx index a2bfb1e..fa2e91b 100644 --- a/app/login/page.tsx +++ b/app/login/page.tsx @@ -40,6 +40,7 @@ export default function LoginPage() { password: formData.password, }) + const token = response?.data?.token; if (response.data?.token && response.code === "USER201") { router.push("/") } else if (response.code === "USER404") { @@ -63,6 +64,7 @@ export default function LoginPage() { console.log('kakaoToken:', kakaoToken); if (kakaoToken) { // 토큰 저장 + //sessionStorage.setItem("accessToken", kakaoToken) localStorage.setItem("auth_token", kakaoToken) // 메인페이지로 이동 window.location.replace("/") // router.replace("/")도 가능, 그러나 확실히 새로고침할 땐 window.location 추천 @@ -177,28 +179,40 @@ export default function LoginPage() { -
- { - const keep = !!checked - setFormData((prev) => ({ ...prev, keepLoggedIn: keep })) - - if (keep) { - localStorage.setItem("rememberedEmail", formData.email) - } else { - localStorage.removeItem("rememberedEmail") - setFormData((prev) => ({ ...prev, email: "" })) - } - }} - className="w-5 h-5 border-2 border-gray-300 rounded data-[state=checked]:bg-blue-500 data-[state=checked]:border-blue-500" - /> - + {/* 로그인 상태 유지 + 오른쪽 링크 */} +
+ {/* 왼쪽: 로그인 상태 유지 */} +
+ { + const keep = !!checked + setFormData((prev) => ({ ...prev, keepLoggedIn: keep })) + + if (keep) { + localStorage.setItem("rememberedEmail", formData.email) + } else { + localStorage.removeItem("rememberedEmail") + setFormData((prev) => ({ ...prev, email: "" })) + } + }} + className="w-5 h-5 border-2 border-gray-300 rounded data-[state=checked]:bg-blue-500 data-[state=checked]:border-blue-500" + /> + +
+ + {/* 오른쪽: 아이디/비번 찾기 링크 */} +
+ 이메일 찾기 + | + 비밀번호 재설정 +
+ {/* 로그인 버튼 */} +
+ +
    +
  • + {hasLen ? "✓" : "○"} 8자 이상 +
  • +
  • + {hasLetter ? "✓" : "○"} 영문 포함 +
  • +
  • + {hasDigit ? "✓" : "○"} 숫자 포함 +
  • +
  • + {hasSpecial ? "✓" : "○"} 특수문자 포함 +
  • +
+ + + {/* Confirm Password */} +
+ +
+ + setFormData((prev) => ({ ...prev, confirmPassword: e.target.value })) + } + className="pr-10" + required + /> + +
+ + {formData.confirmPassword.length > 0 && ( +

+ {passwordsMatch ? "비밀번호가 일치합니다." : "비밀번호가 일치하지 않습니다."} +

+ )} +
+ + + + )} + +
+ + ← 로그인으로 돌아가기 + +
+ + + ) +} diff --git a/app/mypage/applications/page.tsx b/app/mypage/applications/page.tsx index 35325ae..1776abb 100644 --- a/app/mypage/applications/page.tsx +++ b/app/mypage/applications/page.tsx @@ -53,7 +53,7 @@ export default function ApplicationsPage() { const getAuthToken = () => { if (typeof window === 'undefined') return null - return localStorage.getItem("auth_token") || localStorage.getItem("accessToken") + return sessionStorage.getItem('auth_token') } const makeAuthenticatedRequest = async (url: string) => { diff --git a/app/mypage/my-posts/page.tsx b/app/mypage/my-posts/page.tsx index 90c90f4..6cc40e1 100644 --- a/app/mypage/my-posts/page.tsx +++ b/app/mypage/my-posts/page.tsx @@ -113,7 +113,7 @@ function MyPostsContentComponent() { const getAuthToken = () => { if (typeof window === 'undefined') return null - return localStorage.getItem("auth_token") + return sessionStorage.getItem('auth_token') } const addToast = (message: string, type: 'success' | 'error') => { @@ -143,8 +143,8 @@ function MyPostsContentComponent() { }) if (response.status === 401 || response.status === 403) { - localStorage.removeItem('auth_token') - localStorage.removeItem('accessToken') + sessionStorage.removeItem('auth_token') + sessionStorage.removeItem('accessToken') router.push('/login') throw new Error("인증이 만료되었습니다. 다시 로그인해주세요.") } diff --git a/app/mypage/page.tsx b/app/mypage/page.tsx index d53c39c..bc9c4e0 100644 --- a/app/mypage/page.tsx +++ b/app/mypage/page.tsx @@ -115,7 +115,7 @@ export default function MyPage() { const getToken = () => { if (typeof window === 'undefined') return null - return localStorage.getItem('auth_token') || sessionStorage.getItem('auth_token') + return localStorage.getItem('auth_token') } const makeAuthenticatedRequest = async (url: string, options?: RequestInit) => { diff --git a/app/mypage/profile-edit/page.tsx b/app/mypage/profile-edit/page.tsx index bb1b86b..4ddcc99 100644 --- a/app/mypage/profile-edit/page.tsx +++ b/app/mypage/profile-edit/page.tsx @@ -42,7 +42,7 @@ export default function ProfileEditPage() { message: "" }) - const getAuthToken = () => localStorage.getItem("auth_token") || localStorage.getItem("accessToken") + const getAuthToken = () => localStorage.getItem('auth_token') const makeAuthenticatedRequest = async (url: string, options: RequestInit = {}) => { const token = getAuthToken() diff --git a/components/post-detail.tsx b/components/post-detail.tsx index bb45d97..5897256 100644 --- a/components/post-detail.tsx +++ b/components/post-detail.tsx @@ -5,6 +5,7 @@ import { Button } from "@/components/ui/button" import { Badge } from "@/components/ui/badge" import { Card, CardContent } from "@/components/ui/card" import { ArrowLeft, Share2, Heart, Users, Clock, MapPin, AlertTriangle, Bell, User, Eye, CheckCircle, XCircle, Shield, Zap, Trophy, Star, X } from "lucide-react" +import { useRouter } from "next/navigation"; import { API_BASE_URL } from "@/lib/api-client"; interface PostData { @@ -67,6 +68,7 @@ export default function EventDetailModal({ postId, isOpen, onClose, onLogin }: E const [currentUserEmail, setCurrentUserEmail] = useState(null) const [isAuthor, setIsAuthor] = useState(false) const [isLoggedIn, setIsLoggedIn] = useState(false) + const router = useRouter(); // 인증 토큰 가져오기 및 API 호출 함수 const getAuthToken = () => { @@ -746,7 +748,7 @@ export default function EventDetailModal({ postId, isOpen, onClose, onLogin }: E 지금 바로 참여하세요 -
+ {/*
-
+
*/} +
+ {/* 참가신청 버튼 */} + + + {/* 찜하기 버튼 */} + +
)} diff --git a/lib/api-client.ts b/lib/api-client.ts index de653b5..db97d18 100644 --- a/lib/api-client.ts +++ b/lib/api-client.ts @@ -16,9 +16,9 @@ class ApiClient { constructor(baseURL: string) { this.baseURL = baseURL - // 클라이언트 사이드에서만 localStorage 접근 + // 클라이언트 사이드에서만 sessionStorage 접근 if (typeof window !== "undefined") { - this.token = localStorage.getItem("auth_token") + this.token = sessionStorage.getItem("auth_token") } } @@ -64,11 +64,35 @@ class ApiClient { if (response.code === "USER201" && response.data?.token) { this.token = response.data.token if (typeof window !== "undefined") { - localStorage.setItem("auth_token", response.data.token) + sessionStorage.setItem("auth_token", response.data.token) } } return response } + + async findEmail(nickname: string): Promise> { + return this.request>("/user/find-email", { + method: "POST", + body: JSON.stringify({ nickname }), + }) + } + + // 비밀번호 재설정 요청 (메일 발송) + async requestPasswordReset(email: string) { + return this.request<{ message?: string; code?: string }>("/user/request-password", { + method: "POST", + body: JSON.stringify({ email }), + }) + } + + // 비밀번호 재설정 확정 + async confirmPasswordReset(token: string, newPassword: string) { + return this.request<{ message?: string; code?: string }>("/user/confirm-password", { + method: "POST", + body: JSON.stringify({ token, newPassword }), + }) + } + async signup(data: SignupData): Promise> { return this.request>("/user/signup", { @@ -86,7 +110,7 @@ class ApiClient { } finally { this.token = null if (typeof window !== "undefined") { - localStorage.removeItem("auth_token") + sessionStorage.removeItem("auth_token") } } } From 4858769dd2f15f9e94648403c8c4d6034317e01d Mon Sep 17 00:00:00 2001 From: wkdjh Date: Sun, 17 Aug 2025 14:35:49 +0900 Subject: [PATCH 02/29] =?UTF-8?q?fix:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=EC=83=81=ED=83=9C=20=EC=9C=A0=EC=A7=80=20=EB=AC=B8=EC=A0=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/login/page.tsx | 10 +++++----- app/mypage/favorites/page.tsx | 2 +- app/mypage/my-posts/page.tsx | 2 +- app/mypage/page.tsx | 2 +- app/mypage/profile-edit/page.tsx | 2 +- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/app/login/page.tsx b/app/login/page.tsx index fa2e91b..46f6250 100644 --- a/app/login/page.tsx +++ b/app/login/page.tsx @@ -64,8 +64,8 @@ export default function LoginPage() { console.log('kakaoToken:', kakaoToken); if (kakaoToken) { // 토큰 저장 - //sessionStorage.setItem("accessToken", kakaoToken) - localStorage.setItem("auth_token", kakaoToken) + sessionStorage.setItem("accessToken", kakaoToken) + //localStorage.setItem("auth_token", kakaoToken) // 메인페이지로 이동 window.location.replace("/") // router.replace("/")도 가능, 그러나 확실히 새로고침할 땐 window.location 추천 } @@ -73,7 +73,7 @@ export default function LoginPage() { useEffect(() => { - const savedEmail = localStorage.getItem("rememberedEmail") + const savedEmail = sessionStorage.getItem("rememberedEmail") if (savedEmail) { setFormData((prev) => ({ ...prev, email: savedEmail })) setRememberEmail(true) @@ -191,9 +191,9 @@ export default function LoginPage() { setFormData((prev) => ({ ...prev, keepLoggedIn: keep })) if (keep) { - localStorage.setItem("rememberedEmail", formData.email) + sessionStorage.setItem("rememberedEmail", formData.email) } else { - localStorage.removeItem("rememberedEmail") + sessionStorage.removeItem("rememberedEmail") setFormData((prev) => ({ ...prev, email: "" })) } }} diff --git a/app/mypage/favorites/page.tsx b/app/mypage/favorites/page.tsx index 520c360..7e7db41 100644 --- a/app/mypage/favorites/page.tsx +++ b/app/mypage/favorites/page.tsx @@ -111,7 +111,7 @@ export default function FavoritesPage() { const getToken = () => { if (typeof window === 'undefined') return null - return localStorage.getItem('auth_token') || sessionStorage.getItem('auth_token') + return sessionStorage.getItem('auth_token') } const addToast = (message: string, type: 'success' | 'error') => { diff --git a/app/mypage/my-posts/page.tsx b/app/mypage/my-posts/page.tsx index 6cc40e1..4338cda 100644 --- a/app/mypage/my-posts/page.tsx +++ b/app/mypage/my-posts/page.tsx @@ -144,7 +144,7 @@ function MyPostsContentComponent() { if (response.status === 401 || response.status === 403) { sessionStorage.removeItem('auth_token') - sessionStorage.removeItem('accessToken') + localStorage.removeItem('accessToken') router.push('/login') throw new Error("인증이 만료되었습니다. 다시 로그인해주세요.") } diff --git a/app/mypage/page.tsx b/app/mypage/page.tsx index bc9c4e0..9b99a13 100644 --- a/app/mypage/page.tsx +++ b/app/mypage/page.tsx @@ -115,7 +115,7 @@ export default function MyPage() { const getToken = () => { if (typeof window === 'undefined') return null - return localStorage.getItem('auth_token') + return sessionStorage.getItem('auth_token') } const makeAuthenticatedRequest = async (url: string, options?: RequestInit) => { diff --git a/app/mypage/profile-edit/page.tsx b/app/mypage/profile-edit/page.tsx index 4ddcc99..b905cec 100644 --- a/app/mypage/profile-edit/page.tsx +++ b/app/mypage/profile-edit/page.tsx @@ -42,7 +42,7 @@ export default function ProfileEditPage() { message: "" }) - const getAuthToken = () => localStorage.getItem('auth_token') + const getAuthToken = () => sessionStorage.getItem('auth_token') const makeAuthenticatedRequest = async (url: string, options: RequestInit = {}) => { const token = getAuthToken() From 6b808319c6f1695b171e25838bc5b7a0ef17da6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=A0=95=EB=AA=A8?= <116088682+alex052525@users.noreply.github.com> Date: Sun, 17 Aug 2025 15:40:28 +0900 Subject: [PATCH 03/29] =?UTF-8?q?Revert=20"fix:=20=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=EB=84=A4=EC=9D=B4=EC=85=98=20+=20=EB=AA=A8=EB=8B=AC?= =?UTF-8?q?=20=ED=95=B4=EA=B2=B0=20=EC=99=84=EB=A3=8C"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/page.tsx | 53 +++------------------------------------------------- 1 file changed, 3 insertions(+), 50 deletions(-) diff --git a/app/page.tsx b/app/page.tsx index 8f86a1e..dc60165 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -75,15 +75,10 @@ export default function MainPage() { const [posts, setPosts] = useState([]) const [loading, setLoading] = useState(true) const [error, setError] = useState("") - + const [modalOpen, setModalOpen] = useState(false); const [selectedPostId, setSelectedPostId] = useState(null); - // pagination states (추가) - const [page, setPage] = useState(0) - const [size] = useState(10) // 기본 10개 - const [totalElements, setTotalElements] = useState(0); // 총 게시물 수 - const [totalPages, setTotalPages] = useState(1); // 총 페이지 수 - + const handleCreatePost = () => { const token = getAuthToken(); if (!token) { @@ -729,7 +724,6 @@ export default function MainPage() { ))} -
@@ -756,7 +750,6 @@ export default function MainPage() { ))} {modalOpen && selectedPostId !== null && ( - router.push('/login')} /> )} -
- - {/* ---------- Pagination controls ---------- */} -
- - - {pageRange.map((p) => ( - - ))} - - -
- - -

- 총 {totalElements}건 · {page + 1}/{totalPages} 페이지 -

- {/* ------------------------------------------ */} - +
)} )} From 2359639d13e50004e5862738ff08b4007415f1b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=A0=95=EB=AA=A8?= <116088682+alex052525@users.noreply.github.com> Date: Wed, 13 Aug 2025 16:18:54 +0900 Subject: [PATCH 04/29] =?UTF-8?q?Merge=20conflict=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/page.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/page.tsx b/app/page.tsx index 4ce0daa..267a8d5 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -79,6 +79,7 @@ export default function MainPage() { const [modalOpen, setModalOpen] = useState(false); const [selectedPostId, setSelectedPostId] = useState(null); + // pagination states (추가) const [page, setPage] = useState(0) const [size] = useState(10) // 기본 10개 @@ -753,6 +754,7 @@ export default function MainPage() { : `${Number(post.cost).toLocaleString()}원`}

+ + @@ -781,6 +784,7 @@ export default function MainPage() { onLogin={() => router.push('/login')} /> )} + {/* ---------- Pagination controls ---------- */} From d65abd2a041e4e4ad437022c317a5c4424d88f0d Mon Sep 17 00:00:00 2001 From: wkdjh Date: Sun, 17 Aug 2025 16:13:05 +0900 Subject: [PATCH 05/29] =?UTF-8?q?=EC=B6=A9=EB=8F=8C=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/post-detail.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/post-detail.tsx b/components/post-detail.tsx index 4a82931..6fdd34f 100644 --- a/components/post-detail.tsx +++ b/components/post-detail.tsx @@ -87,7 +87,7 @@ export default function EventDetailModal({ postId, isOpen, onClose, onLogin }: E // 인증 토큰 가져오기 및 API 호출 함수 const getAuthToken = () => { if (typeof window !== 'undefined') { - return localStorage.getItem("auth_token") || localStorage.getItem("accessToken") + return sessionStorage.getItem("auth_token") || sessionStorage.getItem("accessToken") } return null } From fe2b6042c28640dfd6f40b72fd6964d1bb937717 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=A0=95=EB=AA=A8?= <116088682+alex052525@users.noreply.github.com> Date: Sun, 17 Aug 2025 16:28:45 +0900 Subject: [PATCH 06/29] Update api-client.ts --- lib/api-client.ts | 54 +++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 45 insertions(+), 9 deletions(-) diff --git a/lib/api-client.ts b/lib/api-client.ts index 95cf14b..485bbba 100644 --- a/lib/api-client.ts +++ b/lib/api-client.ts @@ -2,6 +2,16 @@ import type { Post, User, CreatePostData, LoginData, SignupData, ApiResponse } f export const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || "http://localhost:8080/api" +export interface PostListResponse { + posts: Post[] + page: number + size: number + totalElements: number + totalPages: number + } + + + class ApiClient { private baseURL: string private token: string | null = null @@ -116,29 +126,55 @@ class ApiClient { // Posts methods async getPosts(params?: { sport?: string - sortBy?: string + sortType?: string search?: string region?: string gender?: string - date?: string - }): Promise { + date?: string, + page?: number, + size?: number + }): Promise { try { const searchParams = new URLSearchParams() if (params) { - Object.entries(params).forEach(([key, value]) => { - if (value) searchParams.append(key, value) + Object.entries(params).forEach(([key, value]) => { + if (value !== undefined && value !== null) { + searchParams.append(key, String(value)) // 값이 숫자여도 문자열로 변환해서 넣기 + } }) } + const queryString = searchParams.toString() const endpoint = `/posts/list${queryString ? `?${queryString}` : ""}` - const response = await this.request>(endpoint) - return response.data?.posts || [] + const response = await this.request>(endpoint) + + // response.data가 { posts, page, size, totalElements, totalPages } 형태라고 가정 + if (response.data) { + return response.data + } else { + // fallback: 빈 배열 등 초기값 반환 + return { + posts: [], + page: 0, + size: 10, + totalElements: 0, + totalPages: 0, + } + } + } catch (error) { console.error("Failed to fetch posts:", error) - return [] + return { + posts: [], + page: 0, + size: 10, + totalElements: 0, + totalPages: 0, + } + } } @@ -260,4 +296,4 @@ export async function fetchPostsCalender(year: number, month: number) { const response = await fetch(`${API_BASE_URL}/posts/calender?month=${year}-${monthStr}`); if (!response.ok) throw new Error("Failed to fetch calendar events"); return await response.json(); -} \ No newline at end of file +} From 6afe2d53290baebf17371a60a103c9f9bd7b3527 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=A0=95=EB=AA=A8?= <116088682+alex052525@users.noreply.github.com> Date: Sun, 17 Aug 2025 16:31:09 +0900 Subject: [PATCH 07/29] Update page.tsx --- app/page.tsx | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/app/page.tsx b/app/page.tsx index 6c95ff9..b352e49 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -58,7 +58,7 @@ const getAuthToken = () => localStorage.getItem("auth_token"); export default function MainPage() { const router = useRouter(); - const [sortBy, setSortBy] = useState("recent") + const [sortType, setSortType] = useState("DATE") const [selectedSport, setSelectedSport] = useState("전체") const [searchQuery, setSearchQuery] = useState("") const [viewMode, setViewMode] = useState<"list" | "calendar">("list"); @@ -141,7 +141,7 @@ export default function MainPage() { useEffect(() => { fetchPosts() fetchFavorites() - }, [selectedSport, sortBy, searchQuery, selectedRegion, selectedGender, selectedDate]) + }, [selectedSport, sortType, searchQuery, selectedRegion, selectedGender, selectedDate]) const fetchPosts = async () => { try { @@ -149,7 +149,7 @@ export default function MainPage() { setError("") const params = { sports: selectedSport !== "전체" ? selectedSport : undefined, - sortBy, + sortType, search: searchQuery || undefined, gender: genderMap[selectedGender as keyof typeof genderMap], date: selectedDate || undefined, @@ -235,10 +235,10 @@ export default function MainPage() { }); const sortedPosts = (() => { - if (sortBy === "popular") { + if (sortType === "POPULAR") { return [...filteredPosts].sort((a, b) => (b.viewCount || 0) - (a.viewCount || 0)); } - if (sortBy === "recent") { + if (sortType === "DATE") { return [...filteredPosts].sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()); } return filteredPosts; @@ -585,9 +585,9 @@ export default function MainPage() { {loading ? "로딩중..." : ""} + +
+
+

참가비

+

+ {post.cost === 0 || post.cost === undefined + ? "무료" + : `${Number(post.cost).toLocaleString()}원`} +

+ )} - - - {/* ---------- Pagination controls ---------- */} + {/* ---------- Pagination controls ---------- */}
@@ -843,4 +892,4 @@ export default function MainPage() { ) -} +} \ No newline at end of file From f9fbfb49b0920f636eeac4b54b9313e4d1fd9f0a Mon Sep 17 00:00:00 2001 From: wkdjh10 Date: Mon, 18 Aug 2025 10:00:09 +0900 Subject: [PATCH 09/29] Update ci-cd.yaml From 6dab782ed29f5a2930dde6914b2482932fb3e05d Mon Sep 17 00:00:00 2001 From: sinascode Date: Mon, 18 Aug 2025 10:36:23 +0900 Subject: [PATCH 10/29] =?UTF-8?q?feat:=20=EC=83=81=EC=84=B8=20=EC=9C=84?= =?UTF-8?q?=EC=B9=98=20=EC=9E=85=EB=A0=A5=EC=8B=9C=20=EC=A7=80=EC=97=AD=20?= =?UTF-8?q?=EC=9E=90=EB=8F=99=EC=99=84=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/edit-post.tsx | 183 ++++++++++++++++++++++++++------------- 1 file changed, 121 insertions(+), 62 deletions(-) diff --git a/components/edit-post.tsx b/components/edit-post.tsx index f0e6c6f..3d1ce35 100644 --- a/components/edit-post.tsx +++ b/components/edit-post.tsx @@ -48,6 +48,100 @@ const genderOptions = [ { id: "FEMALE", name: "여성만" }, ] +// 지역 매핑 함수 +const getRegionFromAddress = (address: string): string => { + const addressLower = address.toLowerCase(); + + // 1단계: 도/광역시명이 직접 포함된 경우 우선 매칭 + const primaryMapping = [ + { keywords: ['서울특별시', '서울시', '서울'], value: 'SEOUL' }, + { keywords: ['경기도', '경기'], value: 'GYEONGGI' }, + { keywords: ['강원도', '강원특별자치도', '강원'], value: 'GANGWON' }, + { keywords: ['대전광역시', '대전시'], value: 'DAEJEON' }, + { keywords: ['대구광역시', '대구시'], value: 'DAEGU' }, + { keywords: ['인천광역시', '인천시'], value: 'INCHEON' }, + { keywords: ['광주광역시', '광주시'], value: 'GWANGJU' }, + { keywords: ['울산광역시', '울산시'], value: 'ULSAN' }, + { keywords: ['부산광역시', '부산시'], value: 'BUSAN' }, + { keywords: ['세종특별자치시', '세종시'], value: 'SEJONG' }, + { keywords: ['충청남도', '충남'], value: 'CHUNGNAM' }, + { keywords: ['충청북도', '충북'], value: 'CHUNGBUK' }, + { keywords: ['전라북도', '전북'], value: 'JEONBUK' }, + { keywords: ['전라남도', '전남'], value: 'JEONNAM' }, + { keywords: ['경상북도', '경북'], value: 'GYEONGBUK' }, + { keywords: ['경상남도', '경남'], value: 'GYEONGNAM' }, + { keywords: ['제주특별자치도', '제주도'], value: 'JEJU' }, + ]; + + // 1단계 매칭 시도 + for (const region of primaryMapping) { + for (const keyword of region.keywords) { + if (addressLower.includes(keyword)) { + return region.value; + } + } + } + + // 2단계: 고유한 시/군명으로 매칭 (중복되지 않는 것들만) + const uniqueCityMapping = [ + // 경기도 고유 시/군 + { keywords: ['수원시', '성남시', '고양시', '용인시', '부천시', '안산시', '안양시', '남양주시', '화성시', '평택시', '의정부시', '시흥시', '파주시', '광명시', '김포시', '군포시', '이천시', '양주시', '오산시', '구리시', '안성시', '포천시', '의왕시', '하남시', '여주시', '여주군', '양평군', '동두천시', '과천시', '가평군', '연천군'], value: 'GYEONGGI' }, + + // 강원도 고유 시/군 + { keywords: ['춘천시', '원주시', '강릉시', '동해시', '태백시', '속초시', '삼척시', '홍천군', '횡성군', '영월군', '평창군', '정선군', '철원군', '화천군', '양구군', '인제군', '고성군', '양양군'], value: 'GANGWON' }, + + // 충청남도 고유 시/군 + { keywords: ['천안시', '공주시', '보령시', '아산시', '서산시', '논산시', '계룡시', '당진시', '금산군', '부여군', '서천군', '청양군', '홍성군', '예산군', '태안군'], value: 'CHUNGNAM' }, + + // 충청북도 고유 시/군 + { keywords: ['청주시', '충주시', '제천시', '보은군', '옥천군', '영동군', '증평군', '진천군', '괴산군', '음성군', '단양군'], value: 'CHUNGBUK' }, + + // 전라북도 고유 시/군 + { keywords: ['전주시', '군산시', '익산시', '정읍시', '남원시', '김제시', '완주군', '진안군', '무주군', '장수군', '임실군', '순창군', '고창군', '부안군'], value: 'JEONBUK' }, + + // 전라남도 고유 시/군 + { keywords: ['목포시', '여수시', '순천시', '나주시', '광양시', '담양군', '곡성군', '구례군', '고흥군', '보성군', '화순군', '장흥군', '강진군', '해남군', '영암군', '무안군', '함평군', '영광군', '장성군', '완도군', '진도군', '신안군'], value: 'JEONNAM' }, + + // 경상북도 고유 시/군 + { keywords: ['포항시', '경주시', '김천시', '안동시', '구미시', '영주시', '영천시', '상주시', '문경시', '경산시', '군위군', '의성군', '청송군', '영양군', '영덕군', '청도군', '고령군', '성주군', '칠곡군', '예천군', '봉화군', '울진군', '울릉군'], value: 'GYEONGBUK' }, + + // 경상남도 고유 시/군 + { keywords: ['창원시', '진주시', '통영시', '사천시', '김해시', '밀양시', '거제시', '양산시', '의령군', '함안군', '창녕군', '남해군', '하동군', '산청군', '함양군', '거창군', '합천군'], value: 'GYEONGNAM' }, + + // 제주도 고유 시 + { keywords: ['제주시', '서귀포시'], value: 'JEJU' }, + ]; + + // 2단계 매칭 시도 + for (const region of uniqueCityMapping) { + for (const keyword of region.keywords) { + if (addressLower.includes(keyword)) { + return region.value; + } + } + } + + // 3단계: 특별한 경우 처리 (대전, 대구, 광주의 경우 시명만으로도 매칭) + if (addressLower.includes('대전')) return 'DAEJEON'; + if (addressLower.includes('대구')) return 'DAEGU'; + if (addressLower.includes('광주')) return 'GWANGJU'; + if (addressLower.includes('울산')) return 'ULSAN'; + if (addressLower.includes('부산')) return 'BUSAN'; + if (addressLower.includes('인천')) return 'INCHEON'; + if (addressLower.includes('세종')) return 'SEJONG'; + if (addressLower.includes('제주')) return 'JEJU'; + + return ''; +}; + +const cleanAddress = (address: string): string => { + return address + .replace(/대한민국\s*/, '') + .replace(/Republic of Korea\s*/, '') + .replace(/South Korea\s*/, '') + .trim(); +}; + // Google Maps API 타입 선언 declare global { interface Window { @@ -164,7 +258,6 @@ export default function EditPostModal({ isOpen, postId, onClose }: EditPostModal const predictionsRef = useRef(null) const GOOGLE_PLACES_API_KEY = process.env.NEXT_PUBLIC_GOOGLE_PLACES_API_KEY || "" - // Google Places Autocomplete Service 초기화 useEffect(() => { const loadGoogleMapsScript = () => { @@ -272,8 +365,14 @@ export default function EditPostModal({ isOpen, postId, onClose }: EditPostModal // 예측 결과 선택 처리 const handlePredictionSelect = (prediction: PlacePrediction) => { - // 주요 장소명만 설정 (주소는 제외) - setFormData(prev => ({ ...prev, location: prediction.structured_formatting.main_text })) + const cleanedAddress = cleanAddress(prediction.description); + const detectedRegion = getRegionFromAddress(prediction.description); + + setFormData(prev => ({ + ...prev, + location: cleanedAddress, + town: detectedRegion + })); setShowPredictions(false) setPredictions([]) } @@ -293,7 +392,7 @@ export default function EditPostModal({ isOpen, postId, onClose }: EditPostModal } // 인증 토큰 가져오기 - const getAuthToken = () => sessionStorage.getItem("auth_token") + const getAuthToken = () => localStorage.getItem("auth_token") // JWT 토큰에서 이메일 추출 const getEmailFromToken = () => { @@ -689,65 +788,8 @@ export default function EditPostModal({ isOpen, postId, onClose }: EditPostModal -
- -
- opt.value === formData.town)?.label || ""} - readOnly - className="h-14 text-lg border-2 border-gray-200 rounded-2xl focus:border-black focus:ring-0 bg-gray-50 pr-14 cursor-pointer" - onClick={() => settownModalOpen(true)} - required - /> - -
- - {/* 지역 선택 모달 */} - {townModalOpen && ( -
-
-

지역 선택

-
- {townOptions.map((option) => ( - - ))} -
- -
-
- )} -
+ - {/* Google Places API 자동완성이 적용된 상세 위치 입력 */}
+ {/* 지역 선택 (자동으로 설정됨) */} +
+ +
+ opt.value === formData.town)?.label || ""} + readOnly + className="h-14 text-lg border-2 border-gray-200 rounded-2xl focus:border-black focus:ring-0 bg-gray-100 pr-14 cursor-not-allowed" + required + /> + +
+ +
+