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}
+
+ )}
+
+
+
+
+ 비밀번호가 기억나지 않나요?{" "}
+
+ 비밀번호 재설정
+
+
+
+
+ ← 로그인으로 돌아가기
+
+
+
+ )
+}
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}
}
+
+
+
+
+
+ ← 로그인으로 돌아가기
+
+
+
+
+ )
+}
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"
+ />
+
+
+
+ {/* 오른쪽: 아이디/비번 찾기 링크 */}
+
+ 이메일 찾기
+ |
+ 비밀번호 재설정
+
+
{/* 로그인 버튼 */}
)}
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 ---------- */}
-
- setPage((p) => Math.max(0, p - 1))}
- disabled={page === 0}
- className={`px-4 py-2 rounded-lg ${page === 0 ? "bg-gray-100 text-gray-400" : "bg-white border border-gray-200 hover:bg-gray-50"}`}
- aria-label="Previous page"
- >
- 이전
-
-
- {pageRange.map((p) => (
- setPage(p)}
-
- className={`px-4 py-2 rounded-lg ${p === page ? "bg-black text-white" : "bg-white border border-gray-200 hover:bg-gray-50"}`}
- aria-current={p === page ? "page" : undefined}
- >
- {p + 1}
-
- ))}
-
- setPage((p) => Math.min(totalPages - 1, p + 1))}
- disabled={page >= totalPages - 1}
- className={`px-4 py-2 rounded-lg ${page >= totalPages - 1 ? "bg-gray-100 text-gray-400" : "bg-white border border-gray-200 hover:bg-gray-50"}`}
- aria-label="Next page"
- >
- 다음
-
-
-
-
-
- 총 {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()}원`}
+
{
setSelectedPostId(post.id);
@@ -763,6 +765,7 @@ export default function MainPage() {
상세보기
+
@@ -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 ? "로딩중..." : ""}
setSortBy("popular")}
+ onClick={() => setSortType("POPULAR")}
className={`px-4 py-2 rounded-xl font-medium transition-colors ${
- sortBy === "popular"
+ sortType === "POPULAR"
? "bg-gray-900 text-white"
: "text-gray-600 hover:bg-gray-100"
}`}
@@ -595,9 +595,9 @@ export default function MainPage() {
인기순
setSortBy("nearest")}
+ onClick={() => setSortType("DATE")}
className={`px-4 py-2 rounded-xl font-medium transition-colors ${
- sortBy === "nearest"
+ sortType === "nearest"
? "bg-gray-900 text-white"
: "text-gray-600 hover:bg-gray-100"
}`}
From 1e66c0c9eece66350a417958c20f84dc7ad10907 Mon Sep 17 00:00:00 2001
From: alex052525
Date: Sun, 17 Aug 2025 16:44:54 +0900
Subject: [PATCH 08/29] =?UTF-8?q?fix:=20pagination=20=EC=9E=AC=EC=A0=81?=
=?UTF-8?q?=EC=9A=A9?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
app/page.tsx | 203 ++++++++++++++++++++++++++++++++-------------------
1 file changed, 126 insertions(+), 77 deletions(-)
diff --git a/app/page.tsx b/app/page.tsx
index b352e49..b7458a9 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 [sortType, setSortType] = useState("DATE")
+ const [sortBy, setSortBy] = useState("recent")
const [selectedSport, setSelectedSport] = useState("전체")
const [searchQuery, setSearchQuery] = useState("")
const [viewMode, setViewMode] = useState<"list" | "calendar">("list");
@@ -75,16 +75,16 @@ 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 [modalOpen, setModalOpen] = useState(false);
+ const [selectedPostId, setSelectedPostId] = useState(null);
+
const handleCreatePost = () => {
const token = getAuthToken();
if (!token) {
@@ -123,8 +123,13 @@ export default function MainPage() {
}, []);
useEffect(() => {
- apiClient.getPosts().then(setPosts);
- }, []);
+ apiClient.getPosts()
+ .then(response => setPosts(response.posts)) // posts 배열만 setPosts에 전달
+ .catch(error => {
+ console.error(error);
+ setPosts([]);
+ });
+}, []);
useEffect(() => {
const fetchMyFollows = async () => {
@@ -141,21 +146,40 @@ export default function MainPage() {
useEffect(() => {
fetchPosts()
fetchFavorites()
- }, [selectedSport, sortType, searchQuery, selectedRegion, selectedGender, selectedDate])
+ }, [selectedSport, sortBy, searchQuery, selectedRegion, selectedGender, selectedDate, page])
+ useEffect(() => {
+ setPage(0) // <-- 추가: 필터/정렬 바뀌면 1페이지로
+ }, [selectedSport, sortBy, searchQuery, selectedRegion, selectedGender, selectedDate])
+
const fetchPosts = async () => {
try {
setLoading(true)
setError("")
const params = {
sports: selectedSport !== "전체" ? selectedSport : undefined,
- sortType,
+ sortBy,
search: searchQuery || undefined,
gender: genderMap[selectedGender as keyof typeof genderMap],
date: selectedDate || undefined,
+ page,
+ size,
+ }
+ const res = await apiClient.getPosts(params);
+ console.log('API response:', res);
+ // 서버가 { posts, page, size, totalElements, totalPages } 형태로 응답하면 posts 추출
+ if (res && typeof res === "object" && Array.isArray((res as any).posts)) {
+ setPosts((res).posts);
+ // setPage(res.page);
+ setTotalElements(res.totalElements);
+ setTotalPages(res.totalPages);
+ // (옵션) 서버 페이지 정보를 사용하려면 setPage((res as any).page || 0) 등으로 처리
+ } else if (Array.isArray(res)) {
+ // 기존 방식: 배열 바로 사용
+ setPosts(res);
+ } else {
+ setPosts([]);
}
- const posts = await apiClient.getPosts(params);
- setPosts(posts);
} catch (error) {
console.error("Failed to fetch posts:", error)
setError("게시글을 불러오는데 실패했습니다.")
@@ -165,6 +189,12 @@ export default function MainPage() {
}
}
+ const handlePageChange = (newPage: number) => {
+ if (newPage >= 0 && newPage < totalPages) {
+ setPage(newPage);
+ }
+ };
+
useEffect(() => {
console.log(posts);
if (posts.length > 0) {
@@ -207,42 +237,74 @@ export default function MainPage() {
const now = new Date();
const myMainRegion = extractMainRegion(myRegion);
// tempRegion이 selectedRegion으로
- const filteredPosts = posts.filter(post => {
- // 1. 지역 필터
- const regionMatch = selectedRegion === "모든 지역"
- ? true
- : selectedRegion === "내 지역"
- ? extractMainRegion(post.town) === myMainRegion
- : post.town === selectedRegion;
-
- if (!regionMatch) return false;
-
- // 2. 현재 시각 이후 모집글만 (항상 적용)
- if (post.date) {
- const postDateTime = new Date(post.date.replace(" ", "T"));
- // 현재 시각 이후만 남김
- if (postDateTime <= now) return false;
+ // const filteredPosts = posts.filter(post => {
+ // // 1. 지역 필터
+ // const regionMatch = selectedRegion === "모든 지역"
+ // ? true
+ // : selectedRegion === "내 지역"
+ // ? extractMainRegion(post.town) === myMainRegion
+ // : post.town === selectedRegion;
+
+ // if (!regionMatch) return false;
+
+ // // 2. 현재 시각 이후 모집글만 (항상 적용)
+ // if (post.date) {
+ // const postDateTime = new Date(post.date.replace(" ", "T"));
+ // // 현재 시각 이후만 남김
+ // if (postDateTime <= now) return false;
+ // }
+
+ // // 3. 특정 날짜가 선택된 경우 해당 날짜만 필터링
+ // if (selectedDate) {
+ // const postDateStr = post.date?.split("T")[0];
+ // if (postDateStr !== selectedDate) return false;
+ // }
+
+ // // selectedDate가 없으면 모두 통과
+ // return true;
+ // });
+
+ // const sortedPosts = (() => {
+ // if (sortBy === "popular") {
+ // return [...filteredPosts].sort((a, b) => (b.viewCount || 0) - (a.viewCount || 0));
+ // }
+ // if (sortBy === "recent") {
+ // return [...filteredPosts].sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
+ // }
+ // return filteredPosts;
+ // })();
+
+ // NEW: 보정 - 필터 변경 등으로 현재 page가 초과하면 마지막 페이지로 이동
+ useEffect(() => {
+ if (page >= totalPages) {
+ setPage(Math.max(0, totalPages - 1));
}
+
+ }, [totalPages, page]);
- // 3. 특정 날짜가 선택된 경우 해당 날짜만 필터링
- if (selectedDate) {
- const postDateStr = post.date?.split("T")[0];
- if (postDateStr !== selectedDate) return false;
- }
+ // NEW: 현재 페이지에 해당하는 slice
+ const fromIndex = page * size;
+ const toIndex = Math.min(fromIndex + size, totalElements);
+ // const pagedPosts = sortedPosts.slice(fromIndex, toIndex);
+ const pagedPosts = posts;
- // selectedDate가 없으면 모두 통과
- return true;
- });
+
- const sortedPosts = (() => {
- if (sortType === "POPULAR") {
- return [...filteredPosts].sort((a, b) => (b.viewCount || 0) - (a.viewCount || 0));
- }
- if (sortType === "DATE") {
- return [...filteredPosts].sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
+
+ // NEW: 페이지 번호 창(최대 5개) 계산 헬퍼
+ const getPageRange = (current: number, last: number, maxShown = 5) => {
+ const half = Math.floor(maxShown / 2);
+ let start = Math.max(0, current - half);
+ let end = Math.min(last - 1, start + maxShown - 1);
+ if (end - start + 1 < maxShown) {
+ start = Math.max(0, end - maxShown + 1);
}
- return filteredPosts;
- })();
+ const range: number[] = [];
+ for (let i = start; i <= end; i++) range.push(i);
+ return range;
+ }
+ const pageRange = getPageRange(page, totalPages, 5);
+
return (
@@ -559,7 +621,7 @@ export default function MainPage() {
{selectedDate ? `${selectedDate} 모집글` : "모집글 목록"}
- 총 {filteredPosts.length}개의 모집글
+ 총 {posts.length}개의 모집글
@@ -585,9 +647,9 @@ export default function MainPage() {
{loading ? "로딩중..." : ""}
setSortType("POPULAR")}
+ onClick={() => setSortBy("popular")}
className={`px-4 py-2 rounded-xl font-medium transition-colors ${
- sortType === "POPULAR"
+ sortBy === "popular"
? "bg-gray-900 text-white"
: "text-gray-600 hover:bg-gray-100"
}`}
@@ -595,9 +657,9 @@ export default function MainPage() {
인기순
setSortType("DATE")}
+ onClick={() => setSortBy("nearest")}
className={`px-4 py-2 rounded-xl font-medium transition-colors ${
- sortType === "nearest"
+ sortBy === "nearest"
? "bg-gray-900 text-white"
: "text-gray-600 hover:bg-gray-100"
}`}
@@ -629,7 +691,7 @@ export default function MainPage() {
)}
- {!loading && !error && filteredPosts.length === 0 && (
+ {!loading && !error && posts.length === 0 && (
@@ -647,9 +709,9 @@ export default function MainPage() {
)}
- {!loading && !error && filteredPosts.length > 0 && (
+ {!loading && !error && posts.length > 0 && (
- {sortedPosts.map((post) => (
+ {posts.map((post) => (
@@ -730,26 +792,15 @@ export default function MainPage() {
))}
-
-
-
참가비
-
- {post.cost === 0 || post.cost === undefined
- ? "무료"
- : `${Number(post.cost).toLocaleString()}원`}
-
-
-
-
{
- setSelectedPostId(post.id);
- setModalOpen(true);
- }}
- className="flex items-center gap-2 px-6 py-3 bg-black text-white rounded-xl hover:bg-gray-800 transition-colors font-semibold group-hover:scale-105"
- >
- 상세보기
-
-
+
+
+
+
참가비
+
+ {post.cost === 0 || post.cost === undefined
+ ? "무료"
+ : `${Number(post.cost).toLocaleString()}원`}
+
{
@@ -779,10 +830,10 @@ export default function MainPage() {
onLogin={() => router.push('/login')}
/>
)}
+
+ )}
-
-
- {/* ---------- Pagination controls ---------- */}
+ {/* ---------- Pagination controls ---------- */}
setPage((p) => Math.max(0, p - 1))}
@@ -820,8 +871,6 @@ export default function MainPage() {
총 {totalElements}건 · {page + 1}/{totalPages} 페이지
{/* ------------------------------------------ */}
- >
- )}
>
)}
@@ -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
- />
- settownModalOpen(true)}
- >
-
-
-
-
- {/* 지역 선택 모달 */}
- {townModalOpen && (
-
-
-
지역 선택
-
- {townOptions.map((option) => (
- {
- setFormData(prev => ({ ...prev, town: option.value }))
- settownModalOpen(false)
- }}
- >
- {option.label}
-
- ))}
-
-
settownModalOpen(false)}
- >
- 닫기
-
-
-
- )}
-
+
- {/* 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
+ />
+
+
+
+
+