diff --git a/package.json b/package.json index d6cc6a8..31bbdaa 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "maru", - "version": "0.11.1", + "version": "0.11.2", "private": true, "scripts": { "dev": "next dev", @@ -17,6 +17,7 @@ "next": "14.2.3", "react": "^18", "react-dom": "^18", + "react-loader-spinner": "^6.1.6", "react-redux": "^9.1.0", "recoil": "^0.7.7", "sass": "^1.71.1", diff --git a/src/app/pages/main-page.tsx b/src/app/pages/main-page.tsx index db654aa..71e73be 100644 --- a/src/app/pages/main-page.tsx +++ b/src/app/pages/main-page.tsx @@ -11,6 +11,7 @@ import { UserCard } from '@/components/main-page'; import { useAuthValue } from '@/features/auth'; import { fromAddrToCoord, getGeolocation } from '@/features/geocoding'; import { useRecommendMates } from '@/features/profile'; +import { ColorRing } from 'react-loader-spinner'; const styles = { container: styled.div` @@ -143,6 +144,8 @@ export function MainPage() { const router = useRouter(); + const [permission, setPermission] = useState(); + const handleScrollRight = () => { if (scrollRef.current !== null) { scrollRef.current.scrollBy({ left: 300, behavior: 'smooth' }); @@ -155,6 +158,22 @@ export function MainPage() { } }; + useEffect(() => { + (async () => { + try { + const permissionStatus = await navigator.permissions.query({ + name: 'geolocation', + }); + + permissionStatus.onchange = () => { + setPermission(permissionStatus.state); + }; + } catch (error) { + console.error('Error checking geolocation permission:', error); + } + })(); + }, []); + useEffect(() => { getGeolocation({ onSuccess: position => { @@ -180,9 +199,9 @@ export function MainPage() { const [createdMarkers, setCreatedMarkers] = useState([]); useEffect(() => { - if (map == null) return; + if (map == null || recommendationMates == null) return; - recommendationMates?.data.forEach(mate => { + recommendationMates.forEach(mate => { fromAddrToCoord({ query: mate.location }).then(res => { const address = res.shift(); if (address == null) return; @@ -209,7 +228,7 @@ export function MainPage() { setCreatedMarkers(prev => prev.concat(marker)); }); }); - }, [map, recommendationMates?.data, router]); + }, [map, recommendationMates, router]); useEffect(() => { if (map == null) return () => {}; @@ -256,23 +275,33 @@ export function MainPage() { }; }, [createdMarkers, map]); + const renderIndicator = () => { + if (permission != null && permission !== 'granted') + return

(위치 권한이 필요합니다)

; + if (map == null) + return ( + + ); + return <>; + }; + return ( - - {map == null && ( - <> -

지도를 불러오는 중입니다.

-

(위치 권한이 필요합니다)

- - )} -
+ {renderIndicator()} {auth?.user?.name}님의 추천 메이트 - {recommendationMates?.data != null && - recommendationMates.data.length > 0 ? ( + {recommendationMates != null && recommendationMates.length > 0 ? ( <> - {recommendationMates.data.map( + {recommendationMates.map( ({ memberId, score, diff --git a/src/app/pages/mobile/mobile-main-page.tsx b/src/app/pages/mobile/mobile-main-page.tsx index e600050..dca7bba 100644 --- a/src/app/pages/mobile/mobile-main-page.tsx +++ b/src/app/pages/mobile/mobile-main-page.tsx @@ -11,6 +11,7 @@ import { UserCard } from '@/components/main-page'; import { useAuthValue } from '@/features/auth'; import { fromAddrToCoord, getGeolocation } from '@/features/geocoding'; import { useRecommendMates } from '@/features/profile'; +import { ColorRing } from 'react-loader-spinner'; const styles = { container: styled.div` @@ -148,6 +149,8 @@ export function MobileMainPage() { const router = useRouter(); + const [permission, setPermission] = useState(); + useEffect(() => { getGeolocation({ onSuccess: position => { @@ -170,12 +173,28 @@ export function MobileMainPage() { }); }, []); + useEffect(() => { + (async () => { + try { + const permissionStatus = await navigator.permissions.query({ + name: 'geolocation', + }); + + permissionStatus.onchange = () => { + setPermission(permissionStatus.state); + }; + } catch (error) { + console.error('Error checking geolocation permission:', error); + } + })(); + }, []); + const [createdMarkers, setCreatedMarkers] = useState([]); useEffect(() => { - if (map == null) return; + if (map == null || recommendationMates == null) return; - recommendationMates?.data.forEach(mate => { + recommendationMates.forEach(mate => { fromAddrToCoord({ query: mate.location }).then(res => { const address = res.shift(); if (address == null) return; @@ -202,7 +221,7 @@ export function MobileMainPage() { setCreatedMarkers(prev => prev.concat(marker)); }); }); - }, [map, recommendationMates?.data, router]); + }, [map, recommendationMates, router]); useEffect(() => { if (map == null) return () => {}; @@ -249,24 +268,34 @@ export function MobileMainPage() { }; }, [createdMarkers, map]); + const renderIndicator = () => { + if (permission != null && permission !== 'granted') + return

(위치 권한이 필요합니다)

; + if (map == null) + return ( + + ); + return <>; + }; + return ( - - {map == null && ( - <> -

지도를 불러오는 중입니다.

-

(위치 권한이 필요합니다)

- - )} -
+ {renderIndicator()}

{auth?.user?.name}님의 추천 메이트

- {recommendationMates?.data != null && - recommendationMates.data.length > 0 ? ( + {recommendationMates != null && recommendationMates.length > 0 ? ( - {recommendationMates.data.map( + {recommendationMates.map( ({ memberId, score, diff --git a/src/app/pages/mobile/mobile-shared-posts-page.tsx b/src/app/pages/mobile/mobile-shared-posts-page.tsx index 78d4ff9..9b4bc11 100644 --- a/src/app/pages/mobile/mobile-shared-posts-page.tsx +++ b/src/app/pages/mobile/mobile-shared-posts-page.tsx @@ -201,9 +201,9 @@ export function MobileSharedPostsPage() { useEffect(() => { if (selected === 'hasRoom' && sharedPosts != null) { - setTotalPageCount(sharedPosts.data.totalPages); + setTotalPageCount(sharedPosts.totalPages); } else if (selected === 'dormitory' && dormitorySharedPosts != null) { - setTotalPageCount(dormitorySharedPosts.data.totalPages); + setTotalPageCount(dormitorySharedPosts.totalPages); } }, [selected, dormitorySharedPosts, sharedPosts]); @@ -227,8 +227,8 @@ export function MobileSharedPostsPage() { {selected === 'hasRoom' || selected === 'dormitory' ? ( <> - {posts?.data != null && posts.data.content.length > 0 ? ( - posts?.data.content.map(post => ( + {posts != null && posts.data.length > 0 ? ( + posts.data.map(post => ( )} - {posts?.data.content.length !== 0 && ( + {posts?.data.length !== 0 && ( ) : ( - {recommendationMates?.data != null && - recommendationMates.data.length > 0 ? ( - recommendationMates.data.map( + {recommendationMates != null && recommendationMates.length > 0 ? ( + recommendationMates.map( ({ memberId, score, diff --git a/src/app/pages/shared-posts-page.tsx b/src/app/pages/shared-posts-page.tsx index 95dbec5..b5db444 100644 --- a/src/app/pages/shared-posts-page.tsx +++ b/src/app/pages/shared-posts-page.tsx @@ -19,10 +19,14 @@ import { import { useAuthValue } from '@/features/auth'; import { useRecommendMates } from '@/features/profile'; import { + getDormitorySharedPosts, + getSharedPosts, useDormitorySharedPosts, usePaging, useSharedPosts, } from '@/features/shared'; +import { MagnifyingGlass } from 'react-loader-spinner'; +import { useQueryClient } from '@tanstack/react-query'; const styles = { container: styled.div` @@ -124,10 +128,16 @@ const styles = { width: 100%; justify-content: center; `, + loadingIndicatorContainer: styled.div` + display: flex; + width: 100%; + justify-content: center; + `, }; export function SharedPostsPage() { const router = useRouter(); + const queryClient = useQueryClient(); const auth = useAuthValue(); const [selected, setSelected] = useState('hasRoom'); @@ -188,20 +198,71 @@ export function SharedPostsPage() { useEffect(() => { if (selected === 'hasRoom' && sharedPosts != null) { - setTotalPageCount(sharedPosts.data.totalPages); + setTotalPageCount(sharedPosts.totalPages); } else if (selected === 'dormitory' && dormitorySharedPosts != null) { - setTotalPageCount(dormitorySharedPosts.data.totalPages); + setTotalPageCount(dormitorySharedPosts.totalPages); } }, [selected, dormitorySharedPosts, sharedPosts]); + useEffect(() => { + if (!isLastPage) { + if (selected === 'hasRoom') { + queryClient.prefetchQuery({ + queryKey: [ + '/shared/posts/studio', + { + cardOption: filter.cardType ?? 'my', + debounceFilter: derivedFilter, + page, + }, + ], + queryFn: async () => + await getSharedPosts({ + cardOption: filter.cardType ?? 'my', + filter: derivedFilter, + page, + }).then(res => res), + }); + } else if (selected === 'dormitory') { + queryClient.prefetchQuery({ + queryKey: [ + '/shared/posts/dormitory', + { + cardOption: filter.cardType ?? 'my', + debounceFilter: derivedFilter, + page, + }, + ], + queryFn: async () => + await getDormitorySharedPosts({ + cardOption: filter.cardType ?? 'my', + filter: derivedFilter, + page, + }).then(res => res), + }); + } + } + }, [page, selected, filter.cardType, derivedFilter, queryClient, isLastPage]); + const renderPosts = useMemo(() => { if (isSharedPostsLoading || isDormitorySharedPostsLoading) { return ( - 잠시만 기다려주세요.. + + + ); } - if (posts?.data == null || posts.data.content.length === 0) { + if (posts == null || posts.data.length === 0) { return (

추천되는 게시글이 없습니다.

@@ -209,7 +270,7 @@ export function SharedPostsPage() { ); } - return posts?.data.content.map(post => ( + return posts.data.map(post => ( { if (isMatesLoading) { return ( - 잠시만 기다려주세요.. + + + ); } - if ( - recommendationMates?.data == null || - recommendationMates.data.length === 0 - ) { + if (recommendationMates == null || recommendationMates.length === 0) { return (

추천되는 메이트가 없습니다.

@@ -246,7 +315,7 @@ export function SharedPostsPage() { ); } - return recommendationMates.data.map( + return recommendationMates.map( ({ memberId, score, nickname, location, profileImageUrl, options }) => ( ), ); - }, [isMatesLoading, recommendationMates?.data]); + }, [isMatesLoading, recommendationMates]); return ( @@ -284,7 +353,7 @@ export function SharedPostsPage() { {selected === 'hasRoom' || selected === 'dormitory' ? ( <> {renderPosts} - {posts != null && posts.data.content.length !== 0 && ( + {posts != null && posts.data.length !== 0 && ( >; onEnter: React.Dispatch>; }) { @@ -46,6 +48,7 @@ export function SearchBox({ { setContent(e.target.value); diff --git a/src/components/UserSearchBox.tsx b/src/components/UserSearchBox.tsx index a277b17..d58730c 100644 --- a/src/components/UserSearchBox.tsx +++ b/src/components/UserSearchBox.tsx @@ -51,5 +51,11 @@ export function UserSearchBox() { if (searchError != null) router.replace('/error'); }, [searchError]); - return ; + return ( + + ); } diff --git a/src/components/chat/ChattingRoom.tsx b/src/components/chat/ChattingRoom.tsx index 906e825..aaf8664 100644 --- a/src/components/chat/ChattingRoom.tsx +++ b/src/components/chat/ChattingRoom.tsx @@ -76,6 +76,7 @@ const styles = { height: calc(100% - 7.5rem); box-shadow: 0px -1px 0px 0px #e5e5ea inset; position: relative; + overflow-x: hidden; &::-webkit-scrollbar { width: 0.5rem; @@ -88,6 +89,7 @@ const styles = { senderFrame: styled.div` display: flex; width: 20rem; + max-width: 20rem; justify-content: flex-end; padding-right: 0.8rem; margin: 0.8rem 0 0.8rem 5rem; @@ -95,6 +97,7 @@ const styles = { receiverFrame: styled.div` display: flex; width: 23rem; + max-width: 20rem; justify-content: flex-start; padding-left: 0.8rem; margin: 0.8rem 0; diff --git a/src/components/chat/ReceiverMessage.tsx b/src/components/chat/ReceiverMessage.tsx index 9b19449..71203b3 100644 --- a/src/components/chat/ReceiverMessage.tsx +++ b/src/components/chat/ReceiverMessage.tsx @@ -77,14 +77,14 @@ const styles = { `, messageBody: styled.div` display: flex; + flex-direction: column; align-items: flex-start; - gap: 0.5rem; + max-width: 20rem; + gap: 0.25rem; `, message: styled.div` - display: flex; - width: 80%; - flex-direction: column; - align-items: flex-start; + width: 100%; + max-width: 20rem; gap: 0.625rem; color: var(--Text-gray, #666668); font-family: 'Noto Sans KR'; @@ -92,6 +92,8 @@ const styles = { font-style: normal; font-weight: 400; line-height: 1.25rem; + word-break: break-word; + white-space: pre-wrap; `, time: styled.p` color: var(--Text-gray, #666668); @@ -138,9 +140,7 @@ export function ReceiverMessage({ {message} - - {getLocalTime(time, type)} - + {getLocalTime(time, type)} diff --git a/src/components/chat/SenderMessage.tsx b/src/components/chat/SenderMessage.tsx index 88045f1..dbc844e 100644 --- a/src/components/chat/SenderMessage.tsx +++ b/src/components/chat/SenderMessage.tsx @@ -24,21 +24,22 @@ const styles = { `, messageBody: styled.div` display: flex; + flex-direction: column; align-items: flex-end; - gap: 0.5rem; + max-width: 20rem; + gap: 0.25rem; `, - message: styled.div` - display: flex; - width: 85%; - flex-direction: column; - align-items: flex-start; - gap: 0.625rem; + message: styled.p` + width: 100%; + max-width: 20rem; color: var(--White, #fff); font-family: 'Noto Sans KR'; font-size: 0.875rem; font-style: normal; font-weight: 400; line-height: 1.25rem; + word-break: break-word; + white-space: pre-wrap; `, messageInfo: styled.div` display: flex; @@ -95,9 +96,7 @@ export function SenderMessage({ {message} - - {getLocalTime(time, type)} - + {getLocalTime(time, type)} diff --git a/src/features/profile/profile.api.ts b/src/features/profile/profile.api.ts index 06d4396..e7718e0 100644 --- a/src/features/profile/profile.api.ts +++ b/src/features/profile/profile.api.ts @@ -101,7 +101,7 @@ export const getRecommendMates = async (cardOption: CardType) => { const res = await axios.get( `/maru-api/profile/recommend?cardOption=${cardOption}`, ); - return res.data; + return res.data.data.filter(mate => mate.score > 0); }; export const patchProfileImage = async (fileName: string) => diff --git a/src/features/profile/profile.hook.ts b/src/features/profile/profile.hook.ts index 23767ec..a52a2e5 100644 --- a/src/features/profile/profile.hook.ts +++ b/src/features/profile/profile.hook.ts @@ -101,7 +101,7 @@ export const useRecommendMates = ({ }) => useQuery({ queryKey: ['/profile/recommend', cardOption], - queryFn: async () => await getRecommendMates(cardOption), + queryFn: async () => await getRecommendMates(cardOption).then(res => res), enabled, }); diff --git a/src/features/shared/shared.api.ts b/src/features/shared/shared.api.ts index c8435fa..0b584f0 100644 --- a/src/features/shared/shared.api.ts +++ b/src/features/shared/shared.api.ts @@ -33,7 +33,10 @@ export const getSharedPosts = async ({ return `${baseURL}?${encodeURI(query)}&cardOption=${cardOption}`; }; - return await axios.get(getURI()); + return await axios.get(getURI()).then(res => ({ + totalPages: res.data.data.totalPages, + data: res.data.data.content, + })); }; export const createSharedPost = async (postData: SharedPostProps) => @@ -85,7 +88,10 @@ export const getDormitorySharedPosts = async ({ return `${baseURL}?${encodeURI(query)}&cardOption=${cardOption}`; }; - return await axios.get(getURI()); + return await axios.get(getURI()).then(res => ({ + totalPages: res.data.data.totalPages, + data: res.data.data.content, + })); }; export const createDormitorySharedPost = async (postData: SharedPostProps) => diff --git a/src/features/shared/shared.hook.ts b/src/features/shared/shared.hook.ts index ce911a4..660acc8 100644 --- a/src/features/shared/shared.hook.ts +++ b/src/features/shared/shared.hook.ts @@ -414,7 +414,7 @@ export const useSharedPosts = ({ filter: debounceFilter, search, page, - }).then(response => response.data), + }).then(res => res), enabled, }); }; @@ -477,7 +477,7 @@ export const useDormitorySharedPosts = ({ filter: debounceFilter, search, page, - }).then(response => response.data), + }).then(res => res), enabled, }); }; diff --git a/yarn.lock b/yarn.lock index 40fc701..19f255e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3927,6 +3927,19 @@ react-is@^16.13.1: resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== +react-is@^18.2.0: + version "18.3.1" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.3.1.tgz#e83557dc12eae63a99e003a46388b1dcbb44db7e" + integrity sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg== + +react-loader-spinner@^6.1.6: + version "6.1.6" + resolved "https://registry.yarnpkg.com/react-loader-spinner/-/react-loader-spinner-6.1.6.tgz#744d84a9763e963b565054f4f281dadfbd17e9e9" + integrity sha512-x5h1Jcit7Qn03MuKlrWcMG9o12cp9SNDVHVJTNRi9TgtGPKcjKiXkou4NRfLAtXaFB3+Z8yZsVzONmPzhv2ErA== + dependencies: + react-is "^18.2.0" + styled-components "^6.1.2" + react-redux@^9.1.0: version "9.1.2" resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-9.1.2.tgz#deba38c64c3403e9abd0c3fbeab69ffd9d8a7e4b" @@ -4287,7 +4300,7 @@ strip-json-comments@^3.1.1: resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== -styled-components@^6.1.8: +styled-components@^6.1.2, styled-components@^6.1.8: version "6.1.11" resolved "https://registry.yarnpkg.com/styled-components/-/styled-components-6.1.11.tgz#01948e5195bf1d39e57e0a85b41958c80e40cfb8" integrity sha512-Ui0jXPzbp1phYij90h12ksljKGqF8ncGx+pjrNPsSPhbUUjWT2tD1FwGo2LF6USCnbrsIhNngDfodhxbegfEOA==