diff --git a/package-lock.json b/package-lock.json index dae07e32..66f97db6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "dependencies": { "@sentry/nextjs": "^8.34.0", "@stomp/stompjs": "^7.0.0", - "@tanstack/react-query": "^5.51.11", + "@tanstack/react-query": "^5.62.10", "axios": "^1.7.2", "draft-js": "^0.11.7", "ffmpeg-static": "^5.2.0", @@ -4071,8 +4071,9 @@ } }, "node_modules/@tanstack/query-core": { - "version": "5.59.20", - "license": "MIT", + "version": "5.62.9", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.62.9.tgz", + "integrity": "sha512-lwePd8hNYhyQ4nM/iRQ+Wz2cDtspGeZZHFZmCzHJ7mfKXt+9S301fULiY2IR2byJYY6Z03T427E5PoVfMexHjw==", "funding": { "type": "github", "url": "https://github.com/sponsors/tannerlinsley" @@ -4088,10 +4089,11 @@ } }, "node_modules/@tanstack/react-query": { - "version": "5.59.20", - "license": "MIT", + "version": "5.62.10", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.62.10.tgz", + "integrity": "sha512-1e1WpHM5oGf27nWM/NWLY62/X9pbMBWa6ErWYmeuK0OqB9/g9UzA59ogiWbxCmS2wtAFQRhOdHhfSofrkhPl2g==", "dependencies": { - "@tanstack/query-core": "5.59.20" + "@tanstack/query-core": "5.62.9" }, "funding": { "type": "github", diff --git a/package.json b/package.json index 4841ba01..90eb928c 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "dependencies": { "@sentry/nextjs": "^8.34.0", "@stomp/stompjs": "^7.0.0", - "@tanstack/react-query": "^5.51.11", + "@tanstack/react-query": "^5.62.10", "axios": "^1.7.2", "draft-js": "^0.11.7", "ffmpeg-static": "^5.2.0", diff --git a/src/app/main/_components/DaedLineGather/DeadLineGather.module.scss b/src/app/main/_components/DeadLineGather/DeadLineGather.module.scss similarity index 100% rename from src/app/main/_components/DaedLineGather/DeadLineGather.module.scss rename to src/app/main/_components/DeadLineGather/DeadLineGather.module.scss diff --git a/src/app/main/_components/DaedLineGather/index.tsx b/src/app/main/_components/DeadLineGather/index.tsx similarity index 95% rename from src/app/main/_components/DaedLineGather/index.tsx rename to src/app/main/_components/DeadLineGather/index.tsx index ca720c4a..1e89aae5 100644 --- a/src/app/main/_components/DaedLineGather/index.tsx +++ b/src/app/main/_components/DeadLineGather/index.tsx @@ -1,4 +1,5 @@ /* eslint-disable indent */ +'use client'; import React from 'react'; import { Swiper, SwiperSlide } from 'swiper/react'; import 'swiper/css'; // Swiper 스타일 @@ -27,6 +28,7 @@ interface IMeetingProps { interface DeadLineGatherProps { meetingList: IMeetingProps[] | undefined; + refetch: () => void; } export default function DeadLineGather({ meetingList }: DeadLineGatherProps) { @@ -56,8 +58,7 @@ export default function DeadLineGather({ meetingList }: DeadLineGatherProps) { if (timeDiff < 60 * 1000) return `1분 후 마감`; // 1분 미만 if (timeDiff < 60 * 60 * 1000) return `${minutesLeft}분 후 마감`; // 1시간 미만 - if (timeDiff < 24 * 60 * 60 * 1000) - return `${hoursLeft}시간 ${minutesLeft > 0 ? `${minutesLeft}분` : ''} 후 마감`; // 하루 미만 + if (timeDiff < 24 * 60 * 60 * 1000) return `${hoursLeft}시간 후 마감`; // 하루 미만 return `${daysLeft}일 ${hoursLeft > 0 ? `${hoursLeft}시간` : ''} 후 마감`; // 하루 이상 }; @@ -93,10 +94,6 @@ export default function DeadLineGather({ meetingList }: DeadLineGatherProps) { /> - {/*
-

추리게임

-
*/} -
{Array.isArray(filteredMeetingList) && filteredMeetingList.length > 0 ? ( @@ -144,6 +141,10 @@ export default function DeadLineGather({ meetingList }: DeadLineGatherProps) { width={224} height={224} unoptimized={true} + onError={e => { + e.currentTarget.src = + '/assets/images/emptyThumbnail.png'; + }} /> diff --git a/src/app/main/_components/Header/Header.module.scss b/src/app/main/_components/Header/Header.module.scss index 640712a1..8aaf1027 100644 --- a/src/app/main/_components/Header/Header.module.scss +++ b/src/app/main/_components/Header/Header.module.scss @@ -168,7 +168,7 @@ } .right { - width: 100%; + // width: 100%; justify-content: end; .headerMyapgeButton { width: 28px; diff --git a/src/app/main/_components/Header/Header.tsx b/src/app/main/_components/Header/Header.tsx index 6be2b971..a3eb4506 100644 --- a/src/app/main/_components/Header/Header.tsx +++ b/src/app/main/_components/Header/Header.tsx @@ -4,6 +4,7 @@ import React, { useEffect, useState } from 'react'; import styles from './Header.module.scss'; import Image from 'next/image'; import Link from 'next/link'; +// import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { usePathname, useRouter } from 'next/navigation'; import { getLikeList, getPersonalInfo } from '@/api/apis/mypageApis'; import { getAlrmList } from '@/api/apis/headerApis'; @@ -25,6 +26,7 @@ interface INotification { } export default function Header() { + // const queryClient = useQueryClient(); const [info, setInfo] = useState(null); const [loggedIn, setLoggedIn] = useState(false); const [loading, setLoading] = useState(true); @@ -70,6 +72,10 @@ export default function Header() { fetchGetLikeList(); }, []); // Empty dependency array ensures this runs only once when the component mounts + // // 찜 리스트를 가져오는 Query + // const { data: likeList, isLoading } = useQuery(['likeList'], getLikeList, { + // staleTime: 1000 * 60 * 5, // 5분 동안 데이터 유지 + // }); useEffect(() => { const token = localStorage.getItem('accessToken'); diff --git a/src/app/main/_components/MainNav/MainNav.module.scss b/src/app/main/_components/MainNav/MainNav.module.scss index eeeb836c..633a976c 100644 --- a/src/app/main/_components/MainNav/MainNav.module.scss +++ b/src/app/main/_components/MainNav/MainNav.module.scss @@ -45,11 +45,11 @@ } // 430px 이하 화면에 대한 반응형 스타일 추가 -@media (max-width: 430px) { +@media (max-width: 540px) { .mainNav { flex-direction: column; width: 90%; - margin-top: 20px; + margin-top: 50px; padding: 0px; .mainNavContent { diff --git a/src/app/main/_components/gameRank/gameRank.module.scss b/src/app/main/_components/gameRank/gameRank.module.scss index 8f612ab6..35253cb1 100644 --- a/src/app/main/_components/gameRank/gameRank.module.scss +++ b/src/app/main/_components/gameRank/gameRank.module.scss @@ -43,15 +43,16 @@ width: 20%; height: 125px; display: block; - padding: 10px; + padding: 5px; box-sizing: border-box; border: 1px solid #ddd; - border-radius: 5px; + border-radius: 3px; img { width: 100%; height: 100%; display: block; + background-size: contain; } } @@ -71,7 +72,7 @@ .name { display: block; width: 70%; - font-size: 22px; + font-size: 20px; font-weight: bold; line-clamp: 2; -webkit-line-clamp: 2; diff --git a/src/app/main/_components/gameRank/index.tsx b/src/app/main/_components/gameRank/index.tsx index e31c402a..ba565456 100644 --- a/src/app/main/_components/gameRank/index.tsx +++ b/src/app/main/_components/gameRank/index.tsx @@ -62,9 +62,13 @@ export default function GameRank() { // objectFit="cover" width={125} height={125} - src={`https://${cloud}/${e.thumbnail}`} + src={`https://${cloud}/${e?.thumbnail}`} alt="게임랭크 이미지" unoptimized={true} + onError={e => { + e.currentTarget.src = + '/assets/images/emptyGameThumbnail.png'; + }} />
diff --git a/src/app/main/_components/newGather/page.tsx b/src/app/main/_components/newGather/index.tsx similarity index 94% rename from src/app/main/_components/newGather/page.tsx rename to src/app/main/_components/newGather/index.tsx index 778910f0..b8571dd8 100644 --- a/src/app/main/_components/newGather/page.tsx +++ b/src/app/main/_components/newGather/index.tsx @@ -1,5 +1,5 @@ 'use client'; - +/* eslint-disable indent */ import React from 'react'; import { Swiper, SwiperSlide } from 'swiper/react'; import 'swiper/css'; // Swiper 스타일 @@ -10,7 +10,7 @@ import Link from 'next/link'; import Image from 'next/image'; import SaveGatheringButton from '@/components/common/SaveGatheringButton'; -interface IMeetingProps { +interface IMeeting { id: number; title: string; city: string; @@ -27,7 +27,8 @@ interface IMeetingProps { } interface NewGatherProps { - meetingList: IMeetingProps[] | undefined; + meetingList: IMeeting[] | undefined; + refetch: () => void; } export default function NewGather({ meetingList }: NewGatherProps) { @@ -65,15 +66,10 @@ export default function NewGather({ meetingList }: NewGatherProps) { />
- {/*
-

모임 목록

-
*/} -
{ + e.currentTarget.src = + '/assets/images/emptyThumbnail.png'; + }} /> diff --git a/src/app/main/page.tsx b/src/app/main/page.tsx index 1cf031f5..0b10736e 100644 --- a/src/app/main/page.tsx +++ b/src/app/main/page.tsx @@ -1,13 +1,13 @@ 'use client'; -import React, { useState, useRef, useEffect } from 'react'; +import React, { useRef, useEffect } from 'react'; import RecommendCase from './_components/RecommendCase'; -import DeadLineGather from './_components/DaedLineGather'; +import DeadLineGather from './_components/DeadLineGather'; import MainNav from './_components/MainNav/MainNav'; import GameRank from './_components/gameRank'; import styles from './main.module.scss'; import { getMeetingList } from '@/api/apis/mypageApis'; import { usePostWishList } from '@/api/queryHooks/wishList'; -import NewGather from './_components/newGather/page'; +import NewGather from './_components/newGather'; import MainSearch from './_components/mainSearch'; import AppInstallPrompt from '@/components/common/AppInstallPrompt'; import { handleAllowNotification } from '@/service/notificationPermission'; @@ -16,9 +16,10 @@ import { getPersonalInfo } from '@/api/apis/mypageApis'; import { app } from '@/service/initFirebase'; import FCMDisabledPrompt from '@/components/common/FCMDisabledPrompt'; import { useInApp } from '@/hooks/useInApp'; +import { useQuery } from '@tanstack/react-query'; // Meeting 타입 정의 -interface IMeetingProps { +interface IMeeting { id: number; title: string; city: string; @@ -34,10 +35,12 @@ interface IMeetingProps { tags: string[]; } +// API 응답 타입 +interface IMeetingProps { + content: IMeeting[]; +} + export default function Main() { - const [meetingList, setMeetingList] = useState( - undefined - ); const deadlineRef = useRef(null); const popularRef = useRef(null); const token = localStorage.getItem('accessToken'); @@ -81,17 +84,23 @@ export default function Main() { }); } }, []); - - useEffect(() => { - const fetchMeetingList = async () => { + const { + data: meetingListInfo, + // isLoading, + refetch, + } = useQuery({ + queryKey: ['meetingList'], + queryFn: async () => { try { - const res = await getMeetingList(); - setMeetingList(res.data.content); - } catch (error) {} - }; - - fetchMeetingList(); - }, []); + const response = await getMeetingList(); + // console.log('API 응답:', response); // 응답 로깅 + return response.data; + } catch (error) { + return null; // 에러 시 null 반환 + } + }, + staleTime: 5 * 60 * 1000, // 5분으로 설정 (밀리초 단위) + }); const scrollToSection = ( ref: React.RefObject, @@ -137,11 +146,20 @@ export default function Main() {
- {/* */} - + {meetingListInfo?.content && ( + + )}
- + {meetingListInfo?.content && ( + + )}
diff --git a/src/app/mypage/_components/Info/Info.tsx b/src/app/mypage/_components/Info/Info.tsx index 98e31124..4eee83ce 100644 --- a/src/app/mypage/_components/Info/Info.tsx +++ b/src/app/mypage/_components/Info/Info.tsx @@ -79,26 +79,39 @@ export default function Info({

내 프로필

- + {loggedIn && ( + + )}
- 프로필사진 + {loggedIn ? ( + 나의 프로필사진 + ) : ( + 기본 프로필사진 + )}
@@ -121,9 +134,9 @@ export default function Info({ {loggedIn ? (
    {/*
  • - company. -

    BoardGo

    -
  • */} + company. +

    BoardGo

    + */}
  • E-mail.

    {mypageInfo?.email}

    diff --git a/src/app/mypage/_components/Info/info.module.scss b/src/app/mypage/_components/Info/info.module.scss index 0ac35d0f..59bbf3f6 100644 --- a/src/app/mypage/_components/Info/info.module.scss +++ b/src/app/mypage/_components/Info/info.module.scss @@ -205,6 +205,13 @@ // 반응형 스타일 추가 @media (max-width: 430px) { + .noneResi { + font-size: 13px; + display: block; + margin-top: 30px; + text-align: center; + line-height: 140%; + } .card { .top { padding: 15px; diff --git a/src/app/mypage/_components/infoEdit/infoEdit.module.scss b/src/app/mypage/_components/infoEdit/infoEdit.module.scss index c9db8977..ac959d8d 100644 --- a/src/app/mypage/_components/infoEdit/infoEdit.module.scss +++ b/src/app/mypage/_components/infoEdit/infoEdit.module.scss @@ -14,7 +14,7 @@ .title { display: block; margin-top: 15px; - font-size: 26px; + font-size: 24px; font-weight: bold; } .nameInput { @@ -157,7 +157,7 @@ .infoEditModal { .logoTitle { display: block; - font-size: 32px; + font-size: 24px; font-weight: bold; margin-top: 30px; color: #007aff; @@ -170,7 +170,7 @@ .title { display: block; margin-top: 15px; - font-size: 26px; + font-size: 20px; font-weight: bold; } .nameInput { diff --git a/src/app/mypage/_components/infoEdit/infoEdit.tsx b/src/app/mypage/_components/infoEdit/infoEdit.tsx index efe7152f..ba81e9e7 100644 --- a/src/app/mypage/_components/infoEdit/infoEdit.tsx +++ b/src/app/mypage/_components/infoEdit/infoEdit.tsx @@ -13,7 +13,7 @@ interface IInfoEditProps { handleEditOpen: () => void; updateInfo: () => void; mypageInfo: { - profileImage: string | null; + profileImage: string; nickName: string; }; } @@ -52,6 +52,8 @@ export default function InfoEdit({ const [showConfirmPassword, setShowConfirmPassword] = useState(false); const [isPasswordMatched, setIsPasswordMatched] = useState(true); + // 비밀번호 유효성 검사 + const passwordRegex = /^[a-zA-Z0-9!@#$%^&*()_+=[\]{};':"\\|,.<>/?~-]{8,50}$/; const togglePasswordVisibility = () => setShowPassword(prev => !prev); const toggleConfirmPasswordVisibility = () => @@ -63,9 +65,29 @@ export default function InfoEdit({ window.location.reload(); }, 1000); }; - - // 비밀번호와 비밀번호 확인 비교 함수 + const validatePassword = () => { + if (!passwordValue) { + clearErrors('password'); // 비밀번호가 없으면 에러 제거 + return; + } + if (!passwordRegex.test(passwordValue)) { + setError('password', { + type: 'manual', + message: + '비밀번호는 8자 이상 50자 이하, 영어, 숫자, 특수문자만 사용 가능합니다.', + }); + setIsPasswordMatched(false); // 비밀번호 확인도 일치하지 않게 처리 + } else { + clearErrors('password'); + validatePasswords(); // 비밀번호 확인과의 일치 여부 확인 + } + }; const validatePasswords = () => { + if (!passwordValue || !confirmPasswordValue) { + setIsPasswordMatched(false); // 입력이 없을 때 기본적으로 불일치로 간주 + return; + } + if (passwordValue !== confirmPasswordValue) { setIsPasswordMatched(false); setError('confirmPassword', { @@ -73,14 +95,18 @@ export default function InfoEdit({ message: '비밀번호가 일치하지 않습니다.', }); } else { - setIsPasswordMatched(true); clearErrors('confirmPassword'); + setIsPasswordMatched(true); } }; useEffect(() => { - validatePasswords(); - }, [passwordValue, confirmPasswordValue]); + validatePassword(); // 비밀번호 유효성 검사 + }, [passwordValue]); + + useEffect(() => { + validatePasswords(); // 비밀번호 확인 검사 + }, [confirmPasswordValue]); // 닉네임 유효성 검사 함수 const validateNickname = (nickName: string): boolean => { @@ -113,16 +139,17 @@ export default function InfoEdit({ clearErrors('nickName'); } catch (error: any) { if (error.response?.status === 400) { + setIsNameChecked(false); // 닉네임 체크 실패 시 상태 초기화 setIsNameDuplicate(true); setError('nickName', { type: 'manual', message: '이미 사용 중인 닉네임입니다.', }); setErrorMessage(''); - addToast('이미 사용 중인 닉네임입니다.', 'error'); + // addToast('이미 사용 중인 닉네임입니다.', 'error'); } else { setErrorMessage('닉네임 확인 중 오류가 발생했습니다.'); - addToast('닉네임 확인 중 오류가 발생했습니다.', 'error'); + // addToast('닉네임 확인 중 오류가 발생했습니다.', 'error'); } setIsNameChecked(true); } @@ -130,46 +157,50 @@ export default function InfoEdit({ // 개인정보 수정 제출 함수 const onSubmit: SubmitHandler = async _data => { - if (!isPasswordMatched) { - addToast( - '비밀번호가 일치하지 않습니다. 다시 한번 확인 해주세요.', - 'error' - ); - return; - } - + console.log('onSubmit 호출됨'); // 로그 확인용 try { + // 업데이트 데이터 생성 const updateData: { nickName?: string; password?: string } = {}; - if (nameValue && isNameChecked && !isNameDuplicate) { - updateData.nickName = nameValue; + // 닉네임 수정 조건 + if (nameValue && (isNameChecked || !isNameDuplicate)) { + updateData.nickName = nameValue; // 닉네임이 비어있어도 isNameChecked가 true이면 업데이트 } - if (passwordValue.trim()) { + // 비밀번호 수정 조건 + if (passwordValue && passwordValue.trim()) { updateData.password = passwordValue; } - if (Object.keys(updateData).length === 0) { + // console.log('updateData:', updateData); // 값 확인 + + // 수정 항목이 없을 경우 처리 + if (!updateData.nickName && !updateData.password) { addToast('수정할 항목이 없습니다.', 'error'); return; } - // `updatePersonalInfo` 호출 시, `undefined`인 매개변수를 처리 + // `updatePersonalInfo` 호출 await updatePersonalInfo( - updateData.nickName || '', // `nickName`이 `undefined`일 경우 빈 문자열로 처리 - updateData.password || '' // `password`가 `undefined`일 경우 빈 문자열로 처리 + updateData.nickName || '', // 닉네임: 빈 문자열로 기본값 설정 + updateData.password || '' // 비밀번호: 빈 문자열로 기본값 설정 ); + // 성공 메시지 및 UI 업데이트 + addToast('개인정보가 수정되었습니다.', 'success'); updateInfo(); reset(); handleEditOpen(); - addToast('개인정보가 수정되었습니다.', 'success'); } catch (error) { - addToast('정보 수정 중 오류가 발생했습니다.', 'error'); + console.error('정보 수정 중 오류가 발생했습니다.', error); + } finally { + console.log('파이널리'); } }; - const isFormValid = isPasswordMatched && (isNameChecked || !!passwordValue); + const isFormValid = + (passwordValue && isPasswordMatched && !errors.password) || // 비밀번호 관련 조건 + (nameValue.trim() && !isNameDuplicate && isNameChecked); // 닉네임 관련 조건 return (
    @@ -201,9 +232,10 @@ export default function InfoEdit({ type="text" placeholder="변경하고 싶은 닉네임을 입력해주세요." {...register('nickName', { - required: '닉네임을 입력해주세요.', + // required: '닉네임을 입력해주세요.', validate: validateNickname, })} + defaultValue={mypageInfo.nickName} onChange={e => { setNameValue(e.currentTarget.value); setIsNameChecked(false); @@ -261,9 +293,13 @@ export default function InfoEdit({ )}
    + {errors.password && ( +
    {errors.password.message}
    + )}
+
- 비밀번호 확인 (선택) + 비밀번호 확인
- {!isPasswordMatched && ( + {errors.confirmPassword && (
- 비밀번호가 일치하지 않습니다. + {errors.confirmPassword.message}
)}
- + */} + {/* + */}
); diff --git a/src/app/mypage/_components/profileImageEdit/profileImageEdit.module.scss b/src/app/mypage/_components/profileImageEdit/profileImageEdit.module.scss index 0891072f..dc5da002 100644 --- a/src/app/mypage/_components/profileImageEdit/profileImageEdit.module.scss +++ b/src/app/mypage/_components/profileImageEdit/profileImageEdit.module.scss @@ -99,14 +99,14 @@ } .preview { - width: 200px; - height: 200px; + width: 100px; + height: 100px; margin: 0 auto; position: relative; .previewImgWrap { - width: 200px; - height: 200px; + width: 100px; + height: 100px; display: block; overflow: hidden; margin: 0 auto; @@ -122,18 +122,19 @@ .inputWrap { display: block; margin-top: 20px; - margin-bottom: 50px; + margin-bottom: 0px; position: absolute; - bottom: -20%; - right: 0; + bottom: 0%; + right: -25%; input { display: none; } input + label { + width: auto; img { display: block; - width: 100%; - height: 100%; + width: 50%; + height: 50%; background: #fff; border-radius: 100%; cursor: pointer; diff --git a/src/app/mypage/myGatherings/_components/deleteModal/deleteModal.module.scss b/src/app/mypage/myGatherings/_components/deleteModal/deleteModal.module.scss index 9c7d60fd..cd90002c 100644 --- a/src/app/mypage/myGatherings/_components/deleteModal/deleteModal.module.scss +++ b/src/app/mypage/myGatherings/_components/deleteModal/deleteModal.module.scss @@ -27,6 +27,11 @@ padding: 60px 15px; box-sizing: border-box; font-size: 18px; + @media (max-width: 430px) { + font-size: 16px; + padding: 30px 15px; + text-align: center; + } span { color: #007aff; font-weight: bold; @@ -44,6 +49,10 @@ padding: 25px 15px; box-sizing: border-box; font-size: 18px; + @media (max-width: 430px) { + font-size: 14px; + padding: 15px; + } &.ok { background: #fff; color: #333; diff --git a/src/app/mypage/myGatherings/_components/deleteModal/index.tsx b/src/app/mypage/myGatherings/_components/deleteModal/index.tsx index 9a3fd54e..23ac5643 100644 --- a/src/app/mypage/myGatherings/_components/deleteModal/index.tsx +++ b/src/app/mypage/myGatherings/_components/deleteModal/index.tsx @@ -10,25 +10,25 @@ interface IOutModalProps { meetingId: string; meetingTitle: string; handleModalClose: () => void; // props 인터페이스 정의 + removeMeeting: (_id: string) => void; // 수정된 부분: 함수 타입으로 변경 } export default function DeleteModal({ handleModalClose, meetingId, meetingTitle, -}: IOutModalProps) { + removeMeeting, +}: IOutModalProps & { removeMeeting: (_id: string) => void }) { const { addToast } = useToast(); const HandleDeleteMeeting = async (id: string) => { try { await deleteMeeting(id); // API 호출 addToast('모임을 삭제하였습니다.', 'success'); - setTimeout(() => { - window.location.reload(); - }, 500); + removeMeeting(id); // 부모 상태 업데이트 호출 } catch (error) { if (axios.isAxiosError(error) && error.response) { - if (error.response.status === 400) { + if (error.response.status === 400 || error.response.status === 404) { addToast('참가인원이 존재하여 삭제할수 없습니다', 'error'); } else { alert( @@ -39,6 +39,8 @@ export default function DeleteModal({ alert('오류가 발생했습니다. 다시 시도해주세요.'); } // console.error('모임 삭제 중 오류 발생:', error); + } finally { + handleModalClose(); } }; @@ -51,10 +53,7 @@ export default function DeleteModal({
-
diff --git a/src/app/mypage/myGatherings/_components/outModal/outModal.module.scss b/src/app/mypage/myGatherings/_components/outModal/outModal.module.scss index 9c7d60fd..ade70319 100644 --- a/src/app/mypage/myGatherings/_components/outModal/outModal.module.scss +++ b/src/app/mypage/myGatherings/_components/outModal/outModal.module.scss @@ -22,15 +22,32 @@ justify-content: center; border-radius: 15px; overflow: hidden; + @media (max-width: 430px) { + border-radius: 10px; + } h1 { display: block; padding: 60px 15px; box-sizing: border-box; font-size: 18px; + @media (max-width: 430px) { + padding: 30px 15px; + font-size: 16px; + text-align: center; + } span { color: #007aff; font-weight: bold; } + p { + display: block; + margin-top: 10px; + font-size: 14px; + @media (max-width: 430px) { + font-size: 12px; + line-height: 140%; + } + } } .btnWrap { display: flex; @@ -44,6 +61,10 @@ padding: 25px 15px; box-sizing: border-box; font-size: 18px; + @media (max-width: 430px) { + font-size: 14px; + padding: 15px; + } &.ok { background: #fff; color: #333; diff --git a/src/app/mypage/myGatherings/create/page.tsx b/src/app/mypage/myGatherings/create/page.tsx index 4148c55d..d54ebd69 100644 --- a/src/app/mypage/myGatherings/create/page.tsx +++ b/src/app/mypage/myGatherings/create/page.tsx @@ -92,6 +92,9 @@ export default function Finish() { meetingId={selectedMeetingId} meetingTitle={selectedMeetingTitle} handleModalClose={handleModalClose} + removeMeeting={id => { + setGatherings(gatherings.filter(g => g.meetingId !== id)); // 부모 상태 업데이트 + }} /> ) : null} @@ -127,10 +130,14 @@ export default function Finish() { src={ `https://${cloud}/${gathering?.thumbnail}` || '/assets/mainImages/game.png' - } // Use imageUrl if available + } alt="참여 중 모임 썸네일" width={150} height={200} + unoptimized={true} + onError={e => { + e.currentTarget.src = '/assets/images/emptyThumbnail.png'; + }} />
diff --git a/src/app/mypage/myGatherings/finish/page.tsx b/src/app/mypage/myGatherings/finish/page.tsx index 4be47f7e..2eee9bee 100644 --- a/src/app/mypage/myGatherings/finish/page.tsx +++ b/src/app/mypage/myGatherings/finish/page.tsx @@ -104,6 +104,10 @@ export default function Finish() { alt="참여 중 모임 썸네일" width={150} height={200} + unoptimized={true} + onError={e => { + e.currentTarget.src = '/assets/images/emptyThumbnail.png'; + }} />
diff --git a/src/app/mypage/myGatherings/myGatherings.module.scss b/src/app/mypage/myGatherings/myGatherings.module.scss index 2cbcf007..287817be 100644 --- a/src/app/mypage/myGatherings/myGatherings.module.scss +++ b/src/app/mypage/myGatherings/myGatherings.module.scss @@ -72,9 +72,10 @@ font-weight: bold; } - @media (max-width: 430px) { + @media (max-width: 530px) { font-size: 16px; gap: 30px; + word-break: keep-all; } } } @@ -102,6 +103,7 @@ border-bottom: 3px dashed #e5e7eb; @media (max-width: 430px) { width: 47%; + border-bottom: 0; } .img { @@ -183,6 +185,7 @@ border: 1px solid #007aff; border-radius: 15px; width: 35%; + word-break: keep-all; @media (max-width: 430px) { padding: 10px 15px; @@ -193,7 +196,7 @@ } } - @media (max-width: 430px) { + @media (max-width: 480px) { padding: 0 16px; padding-top: 60px; padding-bottom: 100px; @@ -213,21 +216,44 @@ h1 { font-size: 16px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + display: block; + width: 100%; } b { font-size: 14px; padding: 10px 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + display: block; + width: 100%; } p { flex-direction: column; align-items: flex-start; gap: 5px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + display: block; + width: 100%; - .time, + .time { + font-size: 12px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + display: block; + width: 100%; + } .person { font-size: 12px; + margin-top: 10px; } } @@ -249,6 +275,7 @@ justify-content: center; flex-direction: column; padding: 100px 0; + margin: 0 auto; h1 { display: block; diff --git a/src/app/mypage/myGatherings/participant/page.tsx b/src/app/mypage/myGatherings/participant/page.tsx index e6d5687e..c7de761c 100644 --- a/src/app/mypage/myGatherings/participant/page.tsx +++ b/src/app/mypage/myGatherings/participant/page.tsx @@ -53,6 +53,12 @@ export default function Finish() { // 포맷된 문자열 반환 return `${year}년 ${month}월 ${day}일 ${hours}시 ${minutes}분`; }; + // 모임 목록에서 특정 모임 제거 함수 추가 + const removeMeetingFromList = (meetingId: string) => { + setGatherings(prevGatherings => + prevGatherings.filter(gathering => gathering.meetingId !== meetingId) + ); + }; useEffect(() => { const fetchGatherings = async () => { @@ -122,6 +128,7 @@ export default function Finish() { meetingId={selectedMeetingId} meetingTitle={selectedMeetingTitle} handleModalClose={handleModalClose} + removeMeeting={removeMeetingFromList} // 전달 /> ) : null} {deleteModal === true && selectedMeetingId && selectedMeetingTitle ? ( @@ -129,6 +136,7 @@ export default function Finish() { meetingId={selectedMeetingId} meetingTitle={selectedMeetingTitle} handleModalClose={handleDeleteModalClose} + removeMeeting={removeMeetingFromList} // 전달 /> ) : null} @@ -169,12 +177,15 @@ export default function Finish() { 참여 중 모임 썸네일 { + e.currentTarget.src = '/assets/images/emptyThumbnail.png'; + }} />
diff --git a/src/app/mypage/mypage.module.scss b/src/app/mypage/mypage.module.scss index c7221867..2343592e 100644 --- a/src/app/mypage/mypage.module.scss +++ b/src/app/mypage/mypage.module.scss @@ -6,6 +6,23 @@ font-weight: 900; font-family: 'LaundryGothicOTF-Bold'; } +.infoSkeleton { + width: 100%; + background: #333; + padding: 100px 0; + border-radius: 15px; + background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%); + background-size: 200% 100%; + animation: skeleton-animation 1.5s infinite linear; +} +@keyframes skeleton-animation { + 0% { + background-position: 200% 0; + } + 100% { + background-position: -200% 0; + } +} .relative { position: relative; diff --git a/src/app/mypage/page.tsx b/src/app/mypage/page.tsx index 097923b4..e889c2c5 100644 --- a/src/app/mypage/page.tsx +++ b/src/app/mypage/page.tsx @@ -1,4 +1,5 @@ 'use client'; +import { useQuery } from '@tanstack/react-query'; import styles from './mypage.module.scss'; import Info from './_components/Info/Info'; import Link from 'next/link'; @@ -7,100 +8,116 @@ import { getPersonalInfo } from '@/api/apis/mypageApis'; import InfoEdit from './_components/infoEdit/infoEdit'; import Image from 'next/image'; -// UserProfile 인터페이스의 이름을 I로 시작하도록 수정 interface IUserProfile { - email: string; // 회원 고유 ID - nickName: string; // 닉네임 - profileImage: string; // 프로필 이미지 - averageRating: number; // 평균 별점 - prTags: string[]; // PR 태그 (없을 경우 빈 배열 반환) + email: string; + nickName: string; + profileImage: string; + averageRating: number; + prTags: string[]; } export default function MyPage() { - const [info, setInfo] = useState(null); // IUserProfile 타입 사용 const [editOpen, setEditOpen] = useState(false); const [prOpen, setPrOpen] = useState(false); - const [animatedRating, setAnimatedRating] = useState(0); // Initial rating state - const [checkedLogin, setCheckedLogin] = useState(null); // state to hold checkedLogin - // const [loading, setLoading] = useState(true); // 로딩 상태 추가 - // 클라이언트 사이드에서만 localStorage를 접근 + const [animatedRating, setAnimatedRating] = useState(0); + const [checkedLogin, setCheckedLogin] = useState(null); + useEffect(() => { const token = localStorage.getItem('accessToken'); setCheckedLogin(token); - // setLoading(false); // 로딩 완료 }, []); - // 정보 수정 모달 열기/닫기 핸들러 - const handleEditOpen = () => { - setEditOpen(prev => !prev); - }; - // 사용자 정보 가져오기 - const fetchPersonalInfo = async () => { - try { - const response = await getPersonalInfo(); - const targetRating = response.data.averageRating; - - // Animate rating - let currentRating = 0; - const increment = 0.1; // Increment step - const animationDuration = 1000; // Duration in milliseconds - const interval = 16; // Interval in milliseconds (roughly 60fps) - - const step = (animationDuration / interval) * increment; // Calculate increment per frame - - const animate = () => { - if (currentRating < targetRating) { - currentRating += step; - if (currentRating > targetRating) currentRating = targetRating; // Cap at targetRating - setAnimatedRating(currentRating); - requestAnimationFrame(animate); - } else { - setAnimatedRating(targetRating); // Ensure final value is set - } - }; - - animate(); // Start animation - - setInfo(response.data); - } catch (err) { - // console.error('Failed to fetch personal info:', err); + const { + data: info, + isLoading, + refetch, + } = useQuery({ + queryKey: ['personalInfo'], + queryFn: async () => { + try { + const response = await getPersonalInfo(); + // console.log('API 응답:', response); // 응답 로깅 + return response.data; + } catch (error) { + // console.error('Error fetching personal info:', error); + return null; // 에러 시 null 반환 + } + }, + staleTime: 5 * 60 * 1000, // 5분으로 설정 (밀리초 단위) + enabled: !!checkedLogin, + }); + + useEffect(() => { + if (checkedLogin) { + refetch(); } - }; + }, [checkedLogin, refetch]); - // 컴포넌트가 처음 렌더링될 때 사용자 정보를 가져옵니다. useEffect(() => { - fetchPersonalInfo(); - }, []); + if (info) { + animateRating(info.averageRating); + } + }, [info]); + + const animateRating = (targetRating: number) => { + let currentRating = 0; + const increment = 0.1; + const animationDuration = 1000; + const interval = 16; + + const step = (animationDuration / interval) * increment; + + const animate = () => { + if (currentRating < targetRating) { + currentRating += step; + if (currentRating > targetRating) currentRating = targetRating; + setAnimatedRating(currentRating); + requestAnimationFrame(animate); + } else { + setAnimatedRating(targetRating); + } + }; + + animate(); + }; - // if (loading) { - // return
Loading...
; // 로딩 중 상태를 표시합니다. - // } + const handleEditOpen = () => { + setEditOpen(prev => !prev); + }; + + const updateInfo = () => { + refetch(); + }; const ratingPercentage = (animatedRating / 5) * 100; return (
- {/* Conditionally render InfoEdit only if info is not null */} {info && (
)}

마이페이지

-
- -
+ {isLoading ? ( +
+ ) : ( +
+ +
+ )} +
매너능력치
@@ -108,21 +125,14 @@ export default function MyPage() { className={styles.averageLine} style={{ width: `${ratingPercentage <= 20 ? 15 : Math.min(ratingPercentage, 100)}%`, - }} // Animate width based on animatedRating - > + }}>

{animatedRating.toFixed(1)}

@@ -140,9 +150,6 @@ export default function MyPage() {

    - {/*
  • - PR 태그 수정 -
  • */}
  • 내 모임 @@ -191,8 +198,3 @@ export default function MyPage() {
); } -{ - /*
  • - 친구 목록 -
  • */ -} diff --git a/src/app/mypage/prEdit/page.tsx b/src/app/mypage/prEdit/page.tsx index 8ac59021..c4770cc1 100644 --- a/src/app/mypage/prEdit/page.tsx +++ b/src/app/mypage/prEdit/page.tsx @@ -1,3 +1,4 @@ +/* eslint-disable */ 'use client'; import React from 'react'; import styles from './prEdit.module.scss'; @@ -10,13 +11,17 @@ export default function PrEdit() { const [newTag, setNewTag] = useState(''); const [error, setError] = useState(null); const [inputLengthError, setInputLengthError] = useState(null); // 태그 길이 에러 상태 + const [loading, setLoading] = useState(true); const fetchPersonalInfo = async () => { try { + setLoading(true); const response = await getPersonalInfo(); setPrTags(response.data.prTags); } catch (err) { // console.error('err:', err); + } finally { + setLoading(false); } }; @@ -49,6 +54,7 @@ export default function PrEdit() { const value = e.target.value; setNewTag(value); validateTagLength(value); // 실시간 길이 검사 + setError(null); // 다른 오류 메시지 초기화 }; // 유효성 검사 및 엔터 키를 눌렀을 때 태그 추가하는 함수 @@ -56,8 +62,12 @@ export default function PrEdit() { if (e.key === 'Enter') { e.preventDefault(); const trimmedTag = newTag.trim(); - const tagPattern = /^[a-zA-Z0-9가-힣]+$/; // 띄어쓰기 없는 한글, 영어, 숫자만 허용 + if (!trimmedTag) { + // 빈 문자열이면 아무 작업도 하지 않음 + return; + } + const tagPattern = /^[a-zA-Z0-9가-힣]+$/; // 띄어쓰기 없는 한글, 영어, 숫자만 허용 let errorMessage = ''; if (prTags.includes(trimmedTag)) { @@ -66,20 +76,17 @@ export default function PrEdit() { errorMessage = '태그는 최대 10개까지만 추가할 수 있습니다.'; } else if (!tagPattern.test(trimmedTag)) { errorMessage = '한글, 영어, 숫자만 허용됩니다.'; - } else if (!trimmedTag) { - errorMessage = '태그를 입력해주세요.'; } if (errorMessage) { setError(errorMessage); - setNewTag(''); return; } - setError(null); + setError(null); // 오류 초기화 const updatedTags = [...prTags, trimmedTag]; setPrTags(updatedTags); // UI 먼저 업데이트 - setNewTag(''); + setNewTag(''); // 입력 필드 초기화 try { await updateTagsOnServer(updatedTags); // 서버 업데이트 @@ -107,24 +114,28 @@ export default function PrEdit() {

    PR태그

      - {prTags.map((tag, index) => ( -
    • - -
    • - ))} + {loading + ? Array.from({ length: 4 }).map((_, index) => ( +
    • + )) + : prTags.map((tag, index) => ( +
    • + +
    • + ))}
    PR태그는 입력해주세요.
    10개 까지 추가 할 수 있습니다! diff --git a/src/app/mypage/prEdit/prEdit.module.scss b/src/app/mypage/prEdit/prEdit.module.scss index d249acbb..c70fb5b9 100644 --- a/src/app/mypage/prEdit/prEdit.module.scss +++ b/src/app/mypage/prEdit/prEdit.module.scss @@ -32,6 +32,22 @@ } } } + .skeletonTag { + display: block; + padding: 15px 30px; + background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%); + background-size: 200% 100%; + animation: skeleton-animation 1.5s infinite linear; + border-radius: 15px; + } + @keyframes skeleton-animation { + 0% { + background-position: 200% 0; + } + 100% { + background-position: -200% 0; + } + } } input { width: 100%; diff --git a/src/app/mypage/settingAlarm/settingAlarm.module.scss b/src/app/mypage/settingAlarm/settingAlarm.module.scss index eb4704e3..eab1803b 100644 --- a/src/app/mypage/settingAlarm/settingAlarm.module.scss +++ b/src/app/mypage/settingAlarm/settingAlarm.module.scss @@ -3,6 +3,7 @@ padding: 0 24px; box-sizing: border-box; padding-top: 60px; + padding-bottom: 150px; .popup { position: fixed;