From c5102961b71906d883a8ad61bdaa8df6f2d25459 Mon Sep 17 00:00:00 2001 From: wkdjh Date: Wed, 13 Aug 2025 16:30:13 +0900 Subject: [PATCH 1/9] =?UTF-8?q?fix:=20=ED=9A=8C=EC=9B=90=EA=B0=80=EC=9E=85?= =?UTF-8?q?=EA=B3=BC=20=EA=B0=9C=EC=9D=B8=EC=A0=95=EB=B3=B4=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20=EB=8B=89=EB=84=A4=EC=9E=84=20=EA=B8=B0=EC=A4=80=20?= =?UTF-8?q?=EC=9D=BC=EC=B9=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/create-post/page.tsx | 6 ++-- app/signup/page.tsx | 58 ++++++++++++++++++++++++++++++++++---- components/post-detail.tsx | 21 -------------- 3 files changed, 55 insertions(+), 30 deletions(-) diff --git a/app/create-post/page.tsx b/app/create-post/page.tsx index ac6f619..e625bae 100644 --- a/app/create-post/page.tsx +++ b/app/create-post/page.tsx @@ -241,10 +241,9 @@ export default function CreatePostPage() { return () => document.removeEventListener('mousedown', handleClickOutside) }, []) - // 예측 결과 선택 처리 const handlePredictionSelect = (prediction: PlacePrediction) => { - // 주요 장소명만 설정 (주소는 제외) - setFormData(prev => ({ ...prev, location: prediction.structured_formatting.main_text })) + // 표시용은 전체 설명을 사용 + setFormData(prev => ({ ...prev, location: prediction.description })) setShowPredictions(false) setPredictions([]) } @@ -756,7 +755,6 @@ export default function CreatePostPage() { -
- {nicknameChecked === true && ( + {formData.nickname && + !validateNickname(formData.nickname) && + ( +
+
+

+ 2-10자의 한글, 영문, 숫자만 사용할 수 있어요 +

+
+ )} + + {!isInvalidNickname && nicknameChecked === true && (

✓ 사용 가능한 닉네임입니다.

)} - {nicknameChecked === false && ( + {!isInvalidNickname && nicknameChecked === false && (

이미 사용 중인 닉네임입니다.

)} - {nicknameError && ( + {!isInvalidNickname && nicknameError && (

{nicknameError}

)} diff --git a/components/post-detail.tsx b/components/post-detail.tsx index 16dc744..fd0e91b 100644 --- a/components/post-detail.tsx +++ b/components/post-detail.tsx @@ -231,27 +231,6 @@ export default function EventDetailModal({ postId, isOpen, onClose, onLogin }: E onClose() } - // const handleShare = async () => { - // if (navigator.share) { - // try { - // await navigator.share({ - // title: post?.title || '스포츠 메이트 모집', - // text: `${formatTimeToKorean12Hour(post?.date || '')} ${getSportName(post?.sports || '')} 모집`, - // url: window.location.href - // }) - // } catch (error) { - // console.error('공유 실패:', error) - // } - // } else { - // try { - // await navigator.clipboard.writeText(window.location.href) - // addToast('링크가 복사되었습니다!', 'success') - // } catch (error) { - // console.error('클립보드 복사 실패:', error) - // } - // } - // } - const toggleFavorite = async () => { if (!isLoggedIn) { if (onLogin) onLogin() From 8cff8c3c67419a4cc53ca7e0b405b9a64b6b079a Mon Sep 17 00:00:00 2001 From: sinascode Date: Wed, 13 Aug 2025 16:34:47 +0900 Subject: [PATCH 2/9] =?UTF-8?q?fix:=20=EB=A1=9C=EA=B7=B8=EC=95=84=EC=9B=83?= =?UTF-8?q?=EC=8B=9C=20=EB=A9=94=EC=9D=B8=ED=8E=98=EC=9D=B4=EC=A7=80?= =?UTF-8?q?=EB=A1=9C=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/mypage/page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/mypage/page.tsx b/app/mypage/page.tsx index d53c39c..54a5da8 100644 --- a/app/mypage/page.tsx +++ b/app/mypage/page.tsx @@ -241,7 +241,7 @@ export default function MyPage() { localStorage.removeItem('auth_token') sessionStorage.removeItem('auth_token') } - router.push("/login") + window.location.replace("/") } catch (error) { console.error("Logout error:", error) } From 3ea55dbb3fea401c63712d8af0be7239092858e8 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: Thu, 14 Aug 2025 13:43:27 +0900 Subject: [PATCH 3/9] Revert "Develop" --- app/page.tsx | 389 +++++++++++++++++++++------------------------- lib/api-client.ts | 51 ++---- 2 files changed, 183 insertions(+), 257 deletions(-) diff --git a/app/page.tsx b/app/page.tsx index 6497349..312d3b8 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -32,7 +32,6 @@ const genderMap = { "여자": "FEMALE", }; - function formatTimeToKorean12Hour(dateString: string) { if (!dateString) return ""; let fixedDateString = dateString.replace(" ", "T"); @@ -59,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"); @@ -76,12 +75,10 @@ export default function MainPage() { const [posts, setPosts] = useState([]) const [loading, setLoading] = useState(true) const [error, setError] = useState("") - // 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) { @@ -114,13 +111,8 @@ export default function MainPage() { }, []); useEffect(() => { - apiClient.getPosts() - .then(response => setPosts(response.posts)) // posts 배열만 setPosts에 전달 - .catch(error => { - console.error(error); - setPosts([]); - }); -}, []); + apiClient.getPosts().then(setPosts); + }, []); useEffect(() => { const fetchMyFollows = async () => { @@ -137,12 +129,7 @@ export default function MainPage() { useEffect(() => { fetchPosts() fetchFavorites() - }, [selectedSport, sortType, searchQuery, selectedRegion, selectedGender, selectedDate, page]) - - useEffect(() => { - setPage(0) // <-- 추가: 필터/정렬 바뀌면 1페이지로 - }, [selectedSport, sortType, searchQuery, selectedRegion, selectedGender, selectedDate]) - + }, [selectedSport, sortBy, searchQuery, selectedRegion, selectedGender, selectedDate]) const fetchPosts = async () => { try { @@ -150,28 +137,13 @@ export default function MainPage() { setError("") const params = { sports: selectedSport !== "전체" ? selectedSport : undefined, - sortType: 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("게시글을 불러오는데 실패했습니다.") @@ -181,12 +153,6 @@ export default function MainPage() { } } - const handlePageChange = (newPage: number) => { - if (newPage >= 0 && newPage < totalPages) { - setPage(newPage); - } -}; - useEffect(() => { console.log(posts); if (posts.length > 0) { @@ -228,41 +194,43 @@ 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; + } - // NEW: 보정 - 필터 변경 등으로 현재 page가 초과하면 마지막 페이지로 이동 - useEffect(() => { - if (page >= totalPages) { - setPage(Math.max(0, totalPages - 1)); + // 3. 특정 날짜가 선택된 경우 해당 날짜만 필터링 + if (selectedDate) { + const postDateStr = post.date?.split("T")[0]; + if (postDateStr !== selectedDate) return false; } - - }, [totalPages, page]); - // 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; + }); - - console.log('Current page:', page); - console.log('fromIndex, toIndex:', fromIndex, toIndex); - console.log('pagedPosts:', pagedPosts); - - // 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); + const sortedPosts = (() => { + if (sortBy === "popular") { + return [...filteredPosts].sort((a, b) => (b.viewCount || 0) - (a.viewCount || 0)); } - const range: number[] = []; - for (let i = start; i <= end; i++) range.push(i); - return range; - } - const pageRange = getPageRange(page, totalPages, 5); - - + if (sortBy === "recent") { + return [...filteredPosts].sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()); + } + return filteredPosts; + })(); return (
@@ -581,7 +549,7 @@ export default function MainPage() { {selectedDate ? `${selectedDate} 모집글` : "모집글 목록"}

- 총 {posts.length}개의 모집글 + 총 {filteredPosts.length}개의 모집글

@@ -597,9 +565,9 @@ export default function MainPage() { )}
)} - {!loading && !error && posts.length === 0 && ( + {!loading && !error && filteredPosts.length === 0 && (
@@ -659,156 +627,145 @@ export default function MainPage() {
)} - {!loading && !error && posts.length > 0 && ( - <> -
- {pagedPosts.map((post) => ( - - -
-
- - {post.sports} - - - {post.status} - -
- + {post.status} +
- -

- {post.title} -

- -
-
-
- -
- {post.town} + +
+ +

+ {post.title} +

+ +
+
+
+
-
-
- -
- - {post.date?.split("T")[0]} {post.date && formatTimeToKorean12Hour(post.date)} - + {post.town} +
+
+
+
-
-
- -
- - {post.currentPeople}/{post.maxPeople}명 참여 - + + {post.date?.split("T")[0]} {post.date && formatTimeToKorean12Hour(post.date)} + +
+
+
+
+ + {post.currentPeople}/{post.maxPeople}명 참여 +
- -
-
-
- {post.participants?.slice(0, 3).map((participant, idx) => ( -
- {participant.nickName?.charAt(0) || "?"} -
- ))} - {post.currentPeople > 3 && ( -
- +{post.currentPeople - 3} -
- )} -
+
+ +
+
+
+ {post.participants?.slice(0, 3).map((participant, idx) => ( +
+ {participant.nickName?.charAt(0) || "?"} +
+ ))} + {/* {post.currentPeople > 3 && ( +
+ +{post.currentPeople - 3} +
+ )} */}
-
-
-

참가비

-

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

-
- - - +
+
+
+

참가비

+

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

+
- - - ))} -
- - {/* ---------- Pagination controls ---------- */} -
- - - {pageRange.map((p) => ( - - ))} - - -
- - -

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

- {/* ------------------------------------------ */} - +
+ + + ))} + {modalOpen && selectedPostId !== null && ( + { + setModalOpen(false); + setSelectedPostId(null); + fetchPosts(); + fetchFavorites(); + }} + onLogin={() => router.push('/login')} + /> + )} +
)} )}
- +
+
+

+ 운동 메이트를 모집해보세요 +

+ +
+
) } \ No newline at end of file diff --git a/lib/api-client.ts b/lib/api-client.ts index de653b5..95cf14b 100644 --- a/lib/api-client.ts +++ b/lib/api-client.ts @@ -2,14 +2,6 @@ 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 @@ -124,52 +116,29 @@ class ApiClient { // Posts methods async getPosts(params?: { sport?: string - sortType?: string + sortBy?: string search?: string region?: string gender?: string - date?: string, - page?: number // 추가 - size?: number // 추가 - }): Promise { // 변경: 배열이 아니라 객체 리턴 + date?: string + }): Promise { try { const searchParams = new URLSearchParams() if (params) { - Object.entries(params).forEach(([key, value]) => { - if (value !== undefined && value !== null) { - searchParams.append(key, String(value)) // 값이 숫자여도 문자열로 변환해서 넣기 - } - }) - } + Object.entries(params).forEach(([key, value]) => { + if (value) searchParams.append(key, value) + }) + } const queryString = searchParams.toString() const endpoint = `/posts/list${queryString ? `?${queryString}` : ""}` - 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, - } - } + const response = await this.request>(endpoint) + return response.data?.posts || [] } catch (error) { console.error("Failed to fetch posts:", error) - return { - posts: [], - page: 0, - size: 10, - totalElements: 0, - totalPages: 0, - } + return [] } } From 94d16293530bcfdb4a87323b0a1ff120e2fa448f Mon Sep 17 00:00:00 2001 From: sinascode Date: Thu, 14 Aug 2025 14:38:20 +0900 Subject: [PATCH 4/9] =?UTF-8?q?feat:=20=EB=AA=A8=EC=A7=91=EA=B8=80=20?= =?UTF-8?q?=EB=A6=AC=EC=8A=A4=ED=8A=B8=20=EC=A1=B0=ED=9A=8C=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EC=A1=B0=ED=9A=8C=EC=88=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/page.tsx | 35 ++++++++++++++++++++++++++--------- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/app/page.tsx b/app/page.tsx index 312d3b8..dc60165 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -5,7 +5,7 @@ import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { Badge } from "@/components/ui/badge" import { Card, CardContent } from "@/components/ui/card" -import { Search, MapPin, Clock, Users, Bell, User, Heart, Calendar, List, ChevronDown, Plus, Filter, ArrowRight, Play, Star, Trophy, Target } from "lucide-react" +import { Search, MapPin, Clock, Users, Bell, User, Heart, Calendar, List, ChevronDown, Plus, Filter, ArrowRight, Play, Star, Trophy, Target, Eye } from "lucide-react" import Link from "next/link" import { useRouter } from "next/navigation" import CalendarView from "@/components/calendar-view" @@ -88,6 +88,12 @@ export default function MainPage() { router.push('/create-post'); }; + + const handleRefresh = async () => { + await fetchPosts(); + await fetchFavorites(); + }; + useEffect(() => { setSelectedSport("전체"); setSelectedRegion("모든 지역"); @@ -372,8 +378,6 @@ export default function MainPage() {
- -
@@ -564,6 +568,16 @@ export default function MainPage() { 전체보기 )} +
))} - {/* {post.currentPeople > 3 && ( -
- +{post.currentPeople - 3} -
- )} */}
@@ -768,4 +785,4 @@ export default function MainPage() {
) -} \ No newline at end of file +} From 76cacf5e8804f2bf1bbf9e1932369dbda0c72471 Mon Sep 17 00:00:00 2001 From: sinascode Date: Thu, 14 Aug 2025 16:10:27 +0900 Subject: [PATCH 5/9] =?UTF-8?q?feat:=20=EC=83=81=EC=84=B8=EC=9C=84?= =?UTF-8?q?=EC=B9=98=EB=A1=9C=20=EC=A7=80=EC=97=AD=20=EC=9E=90=EB=8F=99=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/create-post/page.tsx | 187 +++++++++++++++++++++++++-------------- 1 file changed, 123 insertions(+), 64 deletions(-) diff --git a/app/create-post/page.tsx b/app/create-post/page.tsx index c10440c..a0ddf81 100644 --- a/app/create-post/page.tsx +++ b/app/create-post/page.tsx @@ -106,6 +106,100 @@ const Toast = ({ message, type, onClose }: { message: string; type: 'success' |
) +// 지역 매핑 함수 +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(); +}; + export default function CreatePostPage() { const router = useRouter() const [formData, setFormData] = useState({ @@ -135,7 +229,7 @@ export default function CreatePostPage() { const locationInputRef = useRef(null) const predictionsRef = useRef(null) const GOOGLE_PLACES_API_KEY = process.env.NEXT_PUBLIC_GOOGLE_PLACES_API_KEY || "" - + // Google Places Autocomplete Service 초기화 useEffect(() => { const loadGoogleMapsScript = () => { @@ -157,7 +251,6 @@ export default function CreatePostPage() { loadGoogleMapsScript().catch(console.error) }, []) - // Google Places Autocomplete Service를 사용한 장소 예측 const fetchPlacePredictions = async (input: string) => { if (!input.trim() || input.length < 1) { setPredictions([]) @@ -219,7 +312,7 @@ export default function CreatePostPage() { setPredictions([]) setShowPredictions(false) } - }, 200) // 200ms로 줄여서 더 빠른 반응 + }, 200) return () => clearTimeout(timeoutId) }, [formData.location]) @@ -242,8 +335,16 @@ export default function CreatePostPage() { }, []) const handlePredictionSelect = (prediction: PlacePrediction) => { - // 표시용은 전체 설명을 사용 - setFormData(prev => ({ ...prev, location: prediction.description })) + + const cleanedAddress = cleanAddress(prediction.description); + const detectedRegion = getRegionFromAddress(prediction.description); + + setFormData(prev => ({ + ...prev, + location: cleanedAddress, + town: detectedRegion + })); + setShowPredictions(false) setPredictions([]) } @@ -474,65 +575,6 @@ export default function CreatePostPage() { -
- -
- 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 + /> + +
+ +
+
- {/* */} + {/* 신청 버튼 - 신청 상태에 따라 다른 버튼 표시 */} + {isLoggedIn && currentApplication ? ( + + ) : ( + + )} +
+) + export default function ApplicationsPage() { const router = useRouter() const [activeTab, setActiveTab] = useState("all") @@ -50,29 +73,49 @@ export default function ApplicationsPage() { const [mounted, setMounted] = useState(false) const [modalOpen, setModalOpen] = useState(false); const [selectedPostId, setSelectedPostId] = useState(null); + const [toasts, setToasts] = useState([]) + const [cancelingPostId, setCancelingPostId] = useState(null) const getAuthToken = () => { if (typeof window === 'undefined') return null return localStorage.getItem("auth_token") || localStorage.getItem("accessToken") } - const makeAuthenticatedRequest = async (url: string) => { + // 토스트 메시지 추가 + const addToast = (message: string, type: 'success' | 'error') => { + const id = Date.now() + setToasts(prev => [...prev, { id, message, type }]) + + setTimeout(() => { + setToasts(prev => prev.filter(toast => toast.id !== id)) + }, 3000) + } + + const removeToast = (id: number) => { + setToasts(prev => prev.filter(toast => toast.id !== id)) + } + + const makeAuthenticatedRequest = async (url: string, options: RequestInit = {}) => { const token = getAuthToken() if (!token) throw new Error("인증 토큰이 없습니다.") const response = await fetch(url, { + ...options, headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json', + ...options.headers, }, }) if (response.status === 403) { await new Promise(resolve => setTimeout(resolve, 500)) return fetch(url, { + ...options, headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json', + ...options.headers, }, }) } @@ -121,6 +164,73 @@ export default function ApplicationsPage() { } } + // 참가신청 취소 + const handleCancelApplication = async (postId: number, event: React.MouseEvent) => { + event.stopPropagation() + + try { + setCancelingPostId(postId) + + const response = await makeAuthenticatedRequest(`${API_BASE_URL}/posts/${postId}/apply`, { + method: 'DELETE' + }) + + if (response.ok) { + let data = null + const contentType = response.headers.get('content-type') + + if (contentType && contentType.includes('application/json')) { + try { + data = await response.json() + } catch (jsonError) { + data = { message: '참가 신청이 취소되었습니다!' } + } + } else { + data = { message: '참가 신청이 취소되었습니다!' } + } + + addToast(data.message || '참가 신청이 취소되었습니다!', 'success') + + // 취소 후 목록 다시 가져오기 + await fetchApplications() + } else { + let errorData = null + const contentType = response.headers.get('content-type') + + try { + if (contentType && contentType.includes('application/json')) { + errorData = await response.json() + } else { + const textResponse = await response.text() + errorData = { message: textResponse || '요청 처리 중 오류가 발생했습니다.' } + } + } catch (parseError) { + errorData = { message: '요청 처리 중 오류가 발생했습니다.' } + } + + if (response.status === 400 && errorData?.code === "PARTICIPATION400") { + addToast(errorData.message, 'error') + } else if (response.status === 401) { + router.push('/login') + } else { + const errorMessage = (errorData?.message && errorData.message !== '요청 처리 중 오류가 발생했습니다.') + ? errorData.message + : '참가 신청 취소에 실패했습니다.' + addToast(errorMessage, 'error') + } + } + } catch (error) { + if (error instanceof Error && error.message.includes('인증')) { + router.push('/login') + } else { + const errorMessage = error instanceof Error ? error.message : '참가 신청 취소에 실패했습니다.' + addToast(errorMessage, 'error') + } + } finally { + setCancelingPostId(null) + } + } + useEffect(() => { setMounted(true) }, []) @@ -197,6 +307,16 @@ export default function ApplicationsPage() { return (
+ {/* 토스트 메시지들 */} + {toasts.map((toast) => ( + removeToast(toast.id)} + /> + ))} + {/* Header */}
@@ -279,55 +399,71 @@ export default function ApplicationsPage() {
{filteredApplications.map((application) => ( - // - // 모달 추가 부분 283-290 - { - setSelectedPostId(application.id) - setModalOpen(true) - }} - > - -
-
- - {application.postStatus} - -
- - {application.status} + { + setSelectedPostId(application.id) + setModalOpen(true) + }} + > + +
+
+ + {application.postStatus}
+ + {application.status} + +
-

{application.title}

- -
-
- - {application.location} -
-
- - {application.time} -
-
- - {application.participants} -
+

{application.title}

+ +
+
+ + {application.location} +
+
+ + {application.time}
+
+ + {application.participants} +
+
-
-
-

{application.cost}

-
+
+
+

{application.cost}

- - - // + + {/* 신청취소 버튼 - 거절된 상태가 아닐 때만 표시 */} + {application.status !== "거절" && ( + + )} +
+ + ))} - {/* 모달 추가 부분 330-339 */} + + {/* 모달 */} {modalOpen && selectedPostId !== null && ( { setModalOpen(false); setSelectedPostId(null); + // 모달 닫힐 때 목록 새로고침 + fetchApplications(); }} /> )} @@ -359,4 +497,4 @@ export default function ApplicationsPage() {
) -} \ No newline at end of file +} From 5aa38d831df47618d84d1f35afae2c21cfa570f2 Mon Sep 17 00:00:00 2001 From: sinascode Date: Sun, 17 Aug 2025 14:54:25 +0900 Subject: [PATCH 8/9] =?UTF-8?q?fix:=20=EA=B1=B0=EC=A0=88=EB=90=9C=20?= =?UTF-8?q?=EB=AA=A8=EC=A7=91=EA=B8=80=EC=9D=80=20=EC=8B=A0=EC=B2=AD=20?= =?UTF-8?q?=EC=B7=A8=EC=86=8C=20=EB=B6=88=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/post-detail.tsx | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/components/post-detail.tsx b/components/post-detail.tsx index 7d9d776..25e156f 100644 --- a/components/post-detail.tsx +++ b/components/post-detail.tsx @@ -900,12 +900,15 @@ export default function EventDetailModal({ postId, isOpen, onClose, onLogin }: E
{/* 신청 버튼 - 신청 상태에 따라 다른 버튼 표시 */} {isLoggedIn && currentApplication ? ( - + currentApplication.status === "REJECTED" ? ( +
+ 참가 신청이 거절되었습니다 +
+ ) : ( + + ) ) : (
))}
-
@@ -756,7 +750,6 @@ export default function MainPage() { ))} {modalOpen && selectedPostId !== null && ( - router.push('/login')} /> )} -
- - {/* ---------- Pagination controls ---------- */} -
- - - {pageRange.map((p) => ( - - ))} - - -
- - -

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

- {/* ------------------------------------------ */} - +
)} )}