From 44c0271ab3d502c624e0182d3384ff983eb26555 Mon Sep 17 00:00:00 2001 From: xo-yeon <27xo.yeon@gmail.com> Date: Mon, 29 Apr 2024 16:04:29 +0900 Subject: [PATCH 01/22] =?UTF-8?q?fix:=20=ED=8C=8C=ED=8B=B0=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=ED=97=A4=EB=8D=94=20?= =?UTF-8?q?css=20=EC=88=98=EC=A0=95=20=EB=B0=8F=20=EC=84=BC=ED=84=B0=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=20=EB=AC=B8=EC=A0=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pages/party/create.tsx | 241 ++++++++--------- pages/party/edit/[id].tsx | 307 +++++++++++----------- src/components/common/Map.tsx | 26 +- src/components/party/create/SearchMap.tsx | 83 +++--- src/hooks/useSearchPlace.ts | 24 +- 5 files changed, 340 insertions(+), 341 deletions(-) diff --git a/pages/party/create.tsx b/pages/party/create.tsx index 721ece8..d99e08c 100644 --- a/pages/party/create.tsx +++ b/pages/party/create.tsx @@ -1,140 +1,145 @@ -import { NextPage } from 'next'; -import { ChangeEvent } from 'react'; -import styled from '@emotion/styled'; -import Create from '@components/party/create/Create'; -import SearchMap from '@components/party/create/SearchMap'; -import useSearchPlace from '@hooks/useSearchPlace'; -import { DefaultHeader } from '@components/common/DefaultHeader'; -import { postParty } from 'src/api/postParty'; -import * as yup from 'yup'; -import { SubmitHandler, useForm } from 'react-hook-form'; -import { yupResolver } from '@hookform/resolvers/yup'; -import router from 'next/router'; -import { useMutation, useQueryClient } from '@tanstack/react-query'; -import { postUploadImage } from 'src/api/postUploadImage'; -import { useRecoilValue } from 'recoil'; -import { API_GET_MAIN_PAGE } from 'src/api/getPartyMainPage'; -import { PositionSate } from 'src/recoil-states/positionStates'; +import { NextPage } from "next"; +import { ChangeEvent } from "react"; +import styled from "@emotion/styled"; +import Create from "@components/party/create/Create"; +import SearchMap from "@components/party/create/SearchMap"; +import useSearchPlace from "@hooks/useSearchPlace"; +import { DefaultHeader } from "@components/common/DefaultHeader"; +import { postParty } from "src/api/postParty"; +import * as yup from "yup"; +import { SubmitHandler, useForm } from "react-hook-form"; +import { yupResolver } from "@hookform/resolvers/yup"; +import router from "next/router"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { postUploadImage } from "src/api/postUploadImage"; +import { useRecoilValue } from "recoil"; +import { API_GET_MAIN_PAGE } from "src/api/getPartyMainPage"; +import { PositionSate } from "src/recoil-states/positionStates"; const Form = styled.form` - display: flex; - flex-direction: column; - justify-content: space-between; - height: 100%; - min-height: calc(100vh); - padding: 45px 0 75px 0; + display: flex; + flex-direction: column; + justify-content: space-between; + height: 100%; + min-height: calc(100vh); + padding: 45px 0 75px 0; `; export const partySchema = yup.object({ - partyTitle: yup.string().min(2, '2자 이상 입력해주세요').required(), - partyContent: yup.string().required(), - partyTime: yup.string().required(), - gender: yup.string().required(), - category: yup.string().required(), - age: yup.string().required(), - totalParticipant: yup.number().required(), - menu: yup.string().required(), - thumbnail: yup.string(), - status: yup.string(), + partyTitle: yup.string().min(2, "2자 이상 입력해주세요").required(), + partyContent: yup.string().required(), + partyTime: yup.string().required(), + gender: yup.string().required(), + category: yup.string().required(), + age: yup.string().required(), + totalParticipant: yup.number().required(), + menu: yup.string().required(), + thumbnail: yup.string(), + status: yup.string(), }); const CreatePage: NextPage = () => { - const queryClient = useQueryClient(); - const position = useRecoilValue(PositionSate); + const queryClient = useQueryClient(); + const position = useRecoilValue(PositionSate); - const { mutate: postPartyCreate } = useMutation({ - mutationFn: postParty, - }); + const { mutate: postPartyCreate } = useMutation({ + mutationFn: postParty, + }); - const { mutate: postImage } = useMutation({ - mutationFn: postUploadImage, - }); + const { mutate: postImage } = useMutation({ + mutationFn: postUploadImage, + }); - const { marker, setMap, keyword, resultList, reset, handleChangeSearchBox, handleClickPlace } = - useSearchPlace(); + const { + marker, + keyword, + resultList, + reset, + handleChangeSearchBox, + handleClickPlace, + } = useSearchPlace(); - const { - handleSubmit, - register, - formState: { isValid }, - setValue, - getValues, - } = useForm({ - resolver: yupResolver(partySchema), - mode: 'onSubmit', - defaultValues: { - thumbnail: '/images/default_thumbnail.jpg', - }, - }); - - const onSubmitPartyForm: SubmitHandler = (formData: PartyForm) => { - if (!marker || !marker.position) return; + const { + handleSubmit, + register, + formState: { isValid }, + setValue, + getValues, + } = useForm({ + resolver: yupResolver(partySchema), + mode: "onSubmit", + defaultValues: { + thumbnail: "/images/default_thumbnail.jpg", + }, + }); - postPartyCreate( - { - ...formData, - partyPlaceName: marker.content, - latitude: marker.position.lat, - longitude: marker.position.lng, - }, - { - onSuccess: async ({ data }) => { - if (data) { - await queryClient.invalidateQueries({ - queryKey: [ - API_GET_MAIN_PAGE, - { latitude: position.coords.x, longitude: position.coords.y }, - ], - }); + const onSubmitPartyForm: SubmitHandler = (formData: PartyForm) => { + if (!marker || !marker.position) return; - router.replace(`/partydetail/${data.partyId}`); - } - }, - }, - ); - }; + postPartyCreate( + { + ...formData, + partyPlaceName: marker.content, + latitude: marker.position.lat, + longitude: marker.position.lng, + }, + { + onSuccess: async ({ data }) => { + if (data) { + await queryClient.invalidateQueries({ + queryKey: [ + API_GET_MAIN_PAGE, + { latitude: position.coords.x, longitude: position.coords.y }, + ], + }); - const rightHeaderArea = ( - + router.replace(`/partydetail/${data.partyId}`); + } + }, + } ); + }; - const handleChangeThumbnail = (e: ChangeEvent) => { - e.preventDefault(); - const { files } = e.target; + const rightHeaderArea = ( + + ); - if (files) { - postImage(files[0], { - onSuccess({ imgUrl }) { - if (imgUrl) { - setValue('thumbnail', imgUrl); - } - }, - }); - } - }; + const handleChangeThumbnail = (e: ChangeEvent) => { + e.preventDefault(); + const { files } = e.target; - return ( -
- - - - - - ); + if (files) { + postImage(files[0], { + onSuccess({ imgUrl }) { + if (imgUrl) { + setValue("thumbnail", imgUrl); + } + }, + }); + } + }; + + return ( +
+ + + + + + ); }; export default CreatePage; diff --git a/pages/party/edit/[id].tsx b/pages/party/edit/[id].tsx index d173d05..f15a0ef 100644 --- a/pages/party/edit/[id].tsx +++ b/pages/party/edit/[id].tsx @@ -1,162 +1,167 @@ -import { NextPage } from 'next'; -import { ChangeEvent, useEffect } from 'react'; -import styled from '@emotion/styled'; -import Create from '@components/party/create/Create'; -import SearchMap from '@components/party/create/SearchMap'; -import useSearchPlace from '@hooks/useSearchPlace'; -import { DefaultHeader } from '@components/common/DefaultHeader'; -import { patchParty } from 'src/api/patchParty'; -import getPartyDetail, { API_GET_PARTY_DETAIL_KEY } from 'src/api/getPartyDetail'; -import { SubmitHandler, useForm } from 'react-hook-form'; -import { yupResolver } from '@hookform/resolvers/yup'; -import { useRouter } from 'next/router'; -import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; -import { postUploadImage } from 'src/api/postUploadImage'; -import { partySchema } from '../create'; -import { PositionSate } from 'src/recoil-states/positionStates'; -import { useRecoilValue } from 'recoil'; -import { API_GET_MAIN_PAGE } from 'src/api/getPartyMainPage'; +import { NextPage } from "next"; +import { ChangeEvent, useEffect } from "react"; +import styled from "@emotion/styled"; +import Create from "@components/party/create/Create"; +import SearchMap from "@components/party/create/SearchMap"; +import useSearchPlace from "@hooks/useSearchPlace"; +import { DefaultHeader } from "@components/common/DefaultHeader"; +import { patchParty } from "src/api/patchParty"; +import getPartyDetail, { + API_GET_PARTY_DETAIL_KEY, +} from "src/api/getPartyDetail"; +import { SubmitHandler, useForm } from "react-hook-form"; +import { yupResolver } from "@hookform/resolvers/yup"; +import { useRouter } from "next/router"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { postUploadImage } from "src/api/postUploadImage"; +import { partySchema } from "../create"; +import { PositionSate } from "src/recoil-states/positionStates"; +import { useRecoilValue } from "recoil"; +import { API_GET_MAIN_PAGE } from "src/api/getPartyMainPage"; const Form = styled.form` - display: flex; - flex-direction: column; - justify-content: space-between; - height: calc(100vh - 72px - 45px); + display: flex; + flex-direction: column; + justify-content: space-between; + height: 100%; + min-height: calc(100vh); + padding: 45px 0 75px 0; `; const CreatePage: NextPage = () => { - const queryClient = useQueryClient(); - const router = useRouter(); - const { id } = router.query as { id: string }; - const position = useRecoilValue(PositionSate); - const userId = '0'; // 임시 - - const { data } = useQuery({ - queryKey: [API_GET_PARTY_DETAIL_KEY, { id }], - queryFn: () => getPartyDetail({ id, userId }), - enabled: !!id, - }); - - const { mutate: updateParty } = useMutation({ - mutationFn: patchParty, - }); - - const { mutate: uploadImage } = useMutation({ - mutationFn: postUploadImage, - }); - - const { - setMap, - marker, - keyword, - resultList, - handleChangeSearchBox, - handleClickPlace, - setPlace, - } = useSearchPlace(); - - const { - handleSubmit, - register, - formState: { isValid }, - setValue, - getValues, - reset, - } = useForm({ - resolver: yupResolver(partySchema), - mode: 'onSubmit', - defaultValues: { - thumbnail: '/images/default_thumbnail.jpg', + const queryClient = useQueryClient(); + const router = useRouter(); + const { id } = router.query as { id: string }; + const position = useRecoilValue(PositionSate); + const userId = "0"; // 임시 + + const { data } = useQuery({ + queryKey: [API_GET_PARTY_DETAIL_KEY, { id }], + queryFn: () => getPartyDetail({ id, userId }), + enabled: !!id, + }); + + const { mutate: updateParty } = useMutation({ + mutationFn: patchParty, + }); + + const { mutate: uploadImage } = useMutation({ + mutationFn: postUploadImage, + }); + + const { + marker, + keyword, + resultList, + handleChangeSearchBox, + handleClickPlace, + setPlace, + } = useSearchPlace(); + + const { + handleSubmit, + register, + formState: { isValid }, + setValue, + getValues, + reset, + } = useForm({ + resolver: yupResolver(partySchema), + mode: "onSubmit", + defaultValues: { + thumbnail: "/images/default_thumbnail.jpg", + }, + }); + + const onSubmitPartyForm: SubmitHandler = (formData: PartyForm) => { + if (!marker || !marker.position) return; + + updateParty( + { + id, + params: { + ...formData, + partyPlaceName: marker.content, + latitude: marker.position.lat, + longitude: marker.position.lng, }, - }); - - const onSubmitPartyForm: SubmitHandler = (formData: PartyForm) => { - if (!marker || !marker.position) return; - - updateParty( - { - id, - params: { - ...formData, - partyPlaceName: marker.content, - latitude: marker.position.lat, - longitude: marker.position.lng, - }, - }, - { - onSuccess: async () => { - await queryClient.invalidateQueries({ - queryKey: [API_GET_PARTY_DETAIL_KEY, { id, userId }], - }); - await queryClient.invalidateQueries({ - queryKey: [ - API_GET_MAIN_PAGE, - { latitude: position.coords.x, longitude: position.coords.y }, - ], - }); - - router.replace(`/party/${id}`); - }, - }, - ); - }; - - const handleChangeThumbnail = (e: ChangeEvent) => { - e.preventDefault(); - const { files } = e.target; - - if (files) { - uploadImage(files[0], { - onSuccess({ imgUrl }) { - if (imgUrl) { - setValue('thumbnail', imgUrl); - } - }, - }); - } - }; - - useEffect(() => { - if (!data) return; - - setPlace({ - lat: data?.latitude, - lng: data?.longitude, - placeName: data?.partyPlaceName, - }); - setValue('thumbnail', data?.thumbnail); - }, [data, setPlace, setValue]); - - const rightHeaderArea = ( - + }, + { + onSuccess: async () => { + await queryClient.invalidateQueries({ + queryKey: [API_GET_PARTY_DETAIL_KEY, { id, userId }], + }); + await queryClient.invalidateQueries({ + queryKey: [ + API_GET_MAIN_PAGE, + { latitude: position.coords.x, longitude: position.coords.y }, + ], + }); + + router.replace(`/party/${id}`); + }, + } ); + }; + + const handleChangeThumbnail = (e: ChangeEvent) => { + e.preventDefault(); + const { files } = e.target; + + if (files) { + uploadImage(files[0], { + onSuccess({ imgUrl }) { + if (imgUrl) { + setValue("thumbnail", imgUrl); + } + }, + }); + } + }; - if (!data) return <>; - - return ( -
- - - - - - ); + useEffect(() => { + if (!data) return; + + setPlace({ + lat: data?.latitude, + lng: data?.longitude, + placeName: data?.partyPlaceName, + }); + setValue("thumbnail", data?.thumbnail); + }, [data, setPlace, setValue]); + + const rightHeaderArea = ( + + ); + + if (!data) return <>; + + return ( +
+ + + + + + ); }; export default CreatePage; diff --git a/src/components/common/Map.tsx b/src/components/common/Map.tsx index 04bbd88..ee90004 100644 --- a/src/components/common/Map.tsx +++ b/src/components/common/Map.tsx @@ -1,4 +1,4 @@ -import { Dispatch, ReactNode, SetStateAction } from "react"; +import { ReactNode, memo } from "react"; import { Map } from "react-kakao-maps-sdk"; import { MapCoordinatet } from "types/map"; @@ -6,20 +6,16 @@ interface KakaoMapProps { center: MapCoordinatet; children?: ReactNode; zoom?: number; - onCreate?: Dispatch>; } -const KakaoMap = ({ center, zoom = 3, children, onCreate }: KakaoMapProps) => { - return ( - - {children} - - ); -}; +const KakaoMap = ({ center, zoom = 3, children }: KakaoMapProps) => ( + + {children} + +); -export default KakaoMap; +export default memo(KakaoMap); diff --git a/src/components/party/create/SearchMap.tsx b/src/components/party/create/SearchMap.tsx index 6d4278d..4d43794 100644 --- a/src/components/party/create/SearchMap.tsx +++ b/src/components/party/create/SearchMap.tsx @@ -4,7 +4,7 @@ import KakaoMap from "@components/common/Map"; import { MapMarker } from "react-kakao-maps-sdk"; import HighlightOffIcon from "@mui/icons-material/HighlightOff"; import SearchBox from "./SearchBox"; -import { ChangeEvent, Dispatch, SetStateAction } from "react"; +import { ChangeEvent, Dispatch, SetStateAction, useEffect } from "react"; import { Marker } from "types/map"; const SearchWrapper = styled.div` @@ -26,11 +26,8 @@ const MarkerText = styled.div` padding: 3px 5px; `; -type Place = { lat: number; lng: number; placeName: string }; - interface SearchMapProps { marker: Marker | null; - setMap: Dispatch>; resultList: kakao.maps.services.PlacesSearchResult | null; keyword: string; reset: () => void; @@ -40,49 +37,55 @@ interface SearchMapProps { const SearchMap = ({ marker, - setMap, keyword, resultList, reset, handleChangeSearchBox, handleClickPlace, -}: SearchMapProps) => { - return ( - <> - - -
- -
-
- - +}: SearchMapProps) => ( + <> + + +
+ +
+
+ + + {marker ? ( - {marker ? ( - - {marker.content} - - ) : null} + + {marker.content} + - - - ); -}; + ) : ( + + )} +
+ +); export default SearchMap; diff --git a/src/hooks/useSearchPlace.ts b/src/hooks/useSearchPlace.ts index 090bcbb..ffa19cf 100644 --- a/src/hooks/useSearchPlace.ts +++ b/src/hooks/useSearchPlace.ts @@ -3,7 +3,6 @@ import { Marker, Place } from "types/map"; const useSearchPlace = () => { const [marker, setMarker] = useState(null); - const [map, setMap] = useState(); const [keyword, setKeyword] = useState(""); const [resultList, setResultList] = useState(null); @@ -15,31 +14,25 @@ const useSearchPlace = () => { }; const setPlace = useCallback( - ({ lat, lng, placeName }: Place) => { - if (!map) return; - const bounds = new kakao.maps.LatLngBounds(); - bounds.extend(new kakao.maps.LatLng(lat, lng)); + ({ lat, lng, placeName }: Place) => setMarker({ position: { lat: lat, lng: lng, }, content: placeName, - }); - - map.setBounds(bounds); - }, - [map] + }), + [] ); const handleClickPlace = useCallback( ({ - x: lat, - y: lng, + x: lng, + y: lat, place_name: placeName, }: kakao.maps.services.PlacesSearchResultItem) => { - setKeyword(placeName); setResultList(null); + setKeyword(placeName); setPlace({ lat: Number(lat), lng: Number(lng), placeName }); }, [setPlace] @@ -47,7 +40,6 @@ const useSearchPlace = () => { const handleChangeSearchBox = useCallback( (e: ChangeEvent) => { - if (!map) return; const places = new kakao.maps.services.Places(); setKeyword(e.target.value); @@ -72,14 +64,12 @@ const useSearchPlace = () => { } ); }, - [map] + [] ); return { marker, setMarker, - map, - setMap, resultList, setResultList, keyword, From 4c6993fe44761af9fa74001fbc2898f7977a3b65 Mon Sep 17 00:00:00 2001 From: xo-yeon <27xo.yeon@gmail.com> Date: Mon, 29 Apr 2024 16:37:36 +0900 Subject: [PATCH 02/22] =?UTF-8?q?fix:=20=ED=8C=8C=ED=8B=B0=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20=ED=9B=84=20=EC=9D=B4=EB=8F=99=20=EA=B2=BD=EB=A1=9C?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pages/party/create.tsx | 240 ++++++++++++++++++++--------------------- 1 file changed, 117 insertions(+), 123 deletions(-) diff --git a/pages/party/create.tsx b/pages/party/create.tsx index d99e08c..01e01ff 100644 --- a/pages/party/create.tsx +++ b/pages/party/create.tsx @@ -1,145 +1,139 @@ -import { NextPage } from "next"; -import { ChangeEvent } from "react"; -import styled from "@emotion/styled"; -import Create from "@components/party/create/Create"; -import SearchMap from "@components/party/create/SearchMap"; -import useSearchPlace from "@hooks/useSearchPlace"; -import { DefaultHeader } from "@components/common/DefaultHeader"; -import { postParty } from "src/api/postParty"; -import * as yup from "yup"; -import { SubmitHandler, useForm } from "react-hook-form"; -import { yupResolver } from "@hookform/resolvers/yup"; -import router from "next/router"; -import { useMutation, useQueryClient } from "@tanstack/react-query"; -import { postUploadImage } from "src/api/postUploadImage"; -import { useRecoilValue } from "recoil"; -import { API_GET_MAIN_PAGE } from "src/api/getPartyMainPage"; -import { PositionSate } from "src/recoil-states/positionStates"; +import { NextPage } from 'next'; +import { ChangeEvent } from 'react'; +import styled from '@emotion/styled'; +import Create from '@components/party/create/Create'; +import SearchMap from '@components/party/create/SearchMap'; +import useSearchPlace from '@hooks/useSearchPlace'; +import { DefaultHeader } from '@components/common/DefaultHeader'; +import { postParty } from 'src/api/postParty'; +import * as yup from 'yup'; +import { SubmitHandler, useForm } from 'react-hook-form'; +import { yupResolver } from '@hookform/resolvers/yup'; +import router from 'next/router'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { postUploadImage } from 'src/api/postUploadImage'; +import { useRecoilValue } from 'recoil'; +import { API_GET_MAIN_PAGE } from 'src/api/getPartyMainPage'; +import { PositionSate } from 'src/recoil-states/positionStates'; const Form = styled.form` - display: flex; - flex-direction: column; - justify-content: space-between; - height: 100%; - min-height: calc(100vh); - padding: 45px 0 75px 0; + display: flex; + flex-direction: column; + justify-content: space-between; + height: 100%; + min-height: calc(100vh); + padding: 45px 0 75px 0; `; export const partySchema = yup.object({ - partyTitle: yup.string().min(2, "2자 이상 입력해주세요").required(), - partyContent: yup.string().required(), - partyTime: yup.string().required(), - gender: yup.string().required(), - category: yup.string().required(), - age: yup.string().required(), - totalParticipant: yup.number().required(), - menu: yup.string().required(), - thumbnail: yup.string(), - status: yup.string(), + partyTitle: yup.string().min(2, '2자 이상 입력해주세요').required(), + partyContent: yup.string().required(), + partyTime: yup.string().required(), + gender: yup.string().required(), + category: yup.string().required(), + age: yup.string().required(), + totalParticipant: yup.number().required(), + menu: yup.string().required(), + thumbnail: yup.string(), + status: yup.string(), }); const CreatePage: NextPage = () => { - const queryClient = useQueryClient(); - const position = useRecoilValue(PositionSate); + const queryClient = useQueryClient(); + const position = useRecoilValue(PositionSate); - const { mutate: postPartyCreate } = useMutation({ - mutationFn: postParty, - }); + const { mutate: postPartyCreate } = useMutation({ + mutationFn: postParty, + }); - const { mutate: postImage } = useMutation({ - mutationFn: postUploadImage, - }); + const { mutate: postImage } = useMutation({ + mutationFn: postUploadImage, + }); - const { - marker, - keyword, - resultList, - reset, - handleChangeSearchBox, - handleClickPlace, - } = useSearchPlace(); + const { marker, keyword, resultList, reset, handleChangeSearchBox, handleClickPlace } = + useSearchPlace(); - const { - handleSubmit, - register, - formState: { isValid }, - setValue, - getValues, - } = useForm({ - resolver: yupResolver(partySchema), - mode: "onSubmit", - defaultValues: { - thumbnail: "/images/default_thumbnail.jpg", - }, - }); + const { + handleSubmit, + register, + formState: { isValid }, + setValue, + getValues, + } = useForm({ + resolver: yupResolver(partySchema), + mode: 'onSubmit', + defaultValues: { + thumbnail: '/images/default_thumbnail.jpg', + }, + }); - const onSubmitPartyForm: SubmitHandler = (formData: PartyForm) => { - if (!marker || !marker.position) return; + const onSubmitPartyForm: SubmitHandler = (formData: PartyForm) => { + if (!marker || !marker.position) return; - postPartyCreate( - { - ...formData, - partyPlaceName: marker.content, - latitude: marker.position.lat, - longitude: marker.position.lng, - }, - { - onSuccess: async ({ data }) => { - if (data) { - await queryClient.invalidateQueries({ - queryKey: [ - API_GET_MAIN_PAGE, - { latitude: position.coords.x, longitude: position.coords.y }, - ], - }); + postPartyCreate( + { + ...formData, + partyPlaceName: marker.content, + latitude: marker.position.lat, + longitude: marker.position.lng, + }, + { + onSuccess: async ({ data }) => { + if (data) { + await queryClient.invalidateQueries({ + queryKey: [ + API_GET_MAIN_PAGE, + { latitude: position.coords.x, longitude: position.coords.y }, + ], + }); - router.replace(`/partydetail/${data.partyId}`); - } - }, - } - ); - }; + router.replace(`/party/${data.partyId}`); + } + }, + }, + ); + }; - const rightHeaderArea = ( - - ); + const rightHeaderArea = ( + + ); - const handleChangeThumbnail = (e: ChangeEvent) => { - e.preventDefault(); - const { files } = e.target; + const handleChangeThumbnail = (e: ChangeEvent) => { + e.preventDefault(); + const { files } = e.target; - if (files) { - postImage(files[0], { - onSuccess({ imgUrl }) { - if (imgUrl) { - setValue("thumbnail", imgUrl); - } - }, - }); - } - }; + if (files) { + postImage(files[0], { + onSuccess({ imgUrl }) { + if (imgUrl) { + setValue('thumbnail', imgUrl); + } + }, + }); + } + }; - return ( -
- - - - - - ); + return ( +
+ + + + + + ); }; export default CreatePage; From 339cab350b45f1ca33cc1b5ad1d0f80456800568 Mon Sep 17 00:00:00 2001 From: xo-yeon <27xo.yeon@gmail.com> Date: Tue, 30 Apr 2024 12:01:54 +0900 Subject: [PATCH 03/22] =?UTF-8?q?feature:=20=EC=B1=84=ED=8C=85=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20api=20=EC=A0=81=EC=9A=A9=20=EC=9E=91=EC=97=85?= =?UTF-8?q?=EC=A4=91=20=EC=A4=91=EA=B0=84=20=EC=BB=A4=EB=B0=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pages/chat/[id].tsx | 69 +++---- pages/chat/index.tsx | 33 +++ pages/chat/list.tsx | 142 ------------- src/api/deleteChatUser.ts | 18 ++ src/api/getChatMessage.ts | 29 +++ src/api/getChatRoomInfo.ts | 20 ++ src/api/getChatRooms.ts | 23 +++ src/api/getChatUserList.ts | 18 ++ src/components/chat/HeaderBtnGroup.tsx | 47 ----- src/components/chat/MessageList.tsx | 131 ------------ src/components/chat/PartyUserList.tsx | 127 ------------ src/components/chat/list/ChatRoomList.tsx | 132 ++++++++++++ .../chat/{ => room}/BottomInputGroup.tsx | 0 src/components/chat/room/ChatRoom.tsx | 191 ++++++++++++++++++ src/components/chat/room/HeaderBtnGroup.tsx | 60 ++++++ src/components/chat/room/MessageList.tsx | 118 +++++++++++ src/components/chat/room/PartyUserList.tsx | 138 +++++++++++++ src/components/common/BottomBar.tsx | 2 +- types/chat/chat.ts | 15 ++ types/chat/chatRooms.ts | 28 +++ 20 files changed, 847 insertions(+), 494 deletions(-) create mode 100644 pages/chat/index.tsx delete mode 100644 pages/chat/list.tsx create mode 100644 src/api/deleteChatUser.ts create mode 100644 src/api/getChatMessage.ts create mode 100644 src/api/getChatRoomInfo.ts create mode 100644 src/api/getChatRooms.ts create mode 100644 src/api/getChatUserList.ts delete mode 100644 src/components/chat/HeaderBtnGroup.tsx delete mode 100644 src/components/chat/MessageList.tsx delete mode 100644 src/components/chat/PartyUserList.tsx create mode 100644 src/components/chat/list/ChatRoomList.tsx rename src/components/chat/{ => room}/BottomInputGroup.tsx (100%) create mode 100644 src/components/chat/room/ChatRoom.tsx create mode 100644 src/components/chat/room/HeaderBtnGroup.tsx create mode 100644 src/components/chat/room/MessageList.tsx create mode 100644 src/components/chat/room/PartyUserList.tsx create mode 100644 types/chat/chat.ts create mode 100644 types/chat/chatRooms.ts diff --git a/pages/chat/[id].tsx b/pages/chat/[id].tsx index cff969b..67fa9e5 100644 --- a/pages/chat/[id].tsx +++ b/pages/chat/[id].tsx @@ -1,51 +1,28 @@ -import BottomInputGroup from "@components/chat/BottomInputGroup"; -import HeaderBtnGroup from "@components/chat/HeaderBtnGroup"; -import MessageList from "@components/chat/MessageList"; -import styled from "@emotion/styled"; -import { ReactElement, MouseEvent, useState } from "react"; - -const Wrapper = styled.div({ - display: "flex", - flexDirection: "column", - justifyContent: "space-between", - height: "100vh", -}); - -const Contents = styled.main({ - display: "flex", - flexDirection: "column", - justifyContent: "flex-end", -}); +import { ReactNode } from "react"; +import { GetServerSideProps } from "next"; +import QuerySuspenseErrorBoundary from "@components/hoc/QuerySuspenseErrorBoundary"; +import ChatRoom from "@components/chat/room/ChatRoom"; + +interface ChatRoomPageProps { + roomId: string; +} + +const ChatRoomPage = ({ roomId }: ChatRoomPageProps) => ( + + + +); + +ChatRoomPage.getLayout = (page: ReactNode) => { + return <>{page}; +}; -const ChattingRoom = () => { - const [isOpenUserList, setIsOpenUserList] = useState(false); +export default ChatRoomPage; - const handleCloseUserList = (e: MouseEvent) => { - e.stopPropagation(); - setIsOpenUserList(false); - }; +export const getServerSideProps: GetServerSideProps = async ({ params }) => { + const { id: roomId } = params as { id: string }; - const handleOpenUserList = (e: MouseEvent) => { - e.stopPropagation(); - setIsOpenUserList(true); + return { + props: { roomId }, }; - - return ( - - - - - - - - ); }; - -ChattingRoom.getLayout = (page: ReactElement) => { - return <>{page}; -}; - -export default ChattingRoom; diff --git a/pages/chat/index.tsx b/pages/chat/index.tsx new file mode 100644 index 0000000..d6a2b51 --- /dev/null +++ b/pages/chat/index.tsx @@ -0,0 +1,33 @@ +import styled from "@emotion/styled"; +import { NextPage } from "next"; +import { DefaultHeader } from "@components/common/DefaultHeader"; +import { DefaultText } from "@components/common/DefaultText"; +import QuerySuspenseErrorBoundary from "@components/hoc/QuerySuspenseErrorBoundary"; +import ChatRoomList from "@components/chat/list/ChatRoomList"; + +const Wrapper = styled.div` + height: 100%; + min-height: calc(100vh); + padding: 45px 0 75px 0; +`; + +const NotList = styled.div` + text-align: center; + padding: 10rem; + font-size: 3vw; +`; + +const ChatListPage: NextPage = () => ( + + } + /> + 참여중인 방이 없습니다.} + > + + + +); + +export default ChatListPage; diff --git a/pages/chat/list.tsx b/pages/chat/list.tsx deleted file mode 100644 index f428e5d..0000000 --- a/pages/chat/list.tsx +++ /dev/null @@ -1,142 +0,0 @@ -import TextInput from '@components/common/TextInput'; -import styled from '@emotion/styled'; -import Image from 'next/image'; -import { NextPage } from 'next'; -import { DefaultHeader } from '@components/common/DefaultHeader'; -import { DefaultText } from '@components/common/DefaultText'; - -const mockupData = [ - { - img: 'https://cdn.pixabay.com/photo/2023/09/04/06/59/dog-8232158_1280.jpg', - nickName: 'gafar Usman', - time: '12:34 AM', - message: '안녕하세요', - }, - { - img: 'https://cdn.pixabay.com/photo/2023/08/18/01/32/cat-8197577_1280.jpg', - nickName: 'HOD', - time: '12:34 AM', - message: '신청합니다.', - }, - { - img: 'https://cdn.pixabay.com/photo/2023/06/18/07/31/willow-warbler-8071472_1280.jpg', - nickName: 'Habeeb', - time: '12:34 AM', - message: '반갑습니다.', - }, -]; - -const Wrapper = styled.div` - height: 100%; - min-height: calc(100vh); - padding: 45px 0 75px 0; -`; - -const Header = styled.header({ - height: '45px !important', - display: 'flex', - justifyContent: 'center', - alignItems: 'center', - boxShadow: '0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24)', -}); - -const Contents = styled.div({ - padding: '2rem', -}); - -const SearchBox = styled.div({ - position: 'relative', - display: 'flex', - justifyContent: 'center', -}); - -const SearchButton = styled.button({ - minWidth: '50px', -}); - -const RoomList = styled.ul({ - padding: 0, - margin: '0 auto', - listStyle: 'none', -}); - -const Room = styled.li({ - display: 'flex', - margin: '1rem 0', - padding: '1rem 0', -}); - -const ImageBox = styled.div({ - width: 60, - height: 60, - borderRadius: '50%', - overflow: 'hidden', - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - marginRight: '5%', -}); - -const RightBox = styled.div({ - width: 'calc(100% - 60px)', - display: 'flex', - justifyContent: 'space-between', - alignItems: 'center', -}); - -const TextBox = styled.div({ - display: 'flex', - flexDirection: 'column', - justifyContent: 'center', -}); - -const NickName = styled.p({ - margin: 0, - fontSize: '18px', - fontWeight: 'bold', -}); - -const Message = styled.p({ - margin: 0, -}); - -const Recentime = styled.p({}); - -const ChatListPage: NextPage = () => { - const handleOnChangeSearch = () => {}; - const handleOnClickSearch = () => {}; - - return ( - - } /> - - - - 검색 - - - {mockupData.map((item) => { - const { img, nickName, time, message } = item; - - return ( - - - profile - - - - {nickName} - {message} - - {time} - - - ); - })} - - - - ); -}; - -export default ChatListPage; diff --git a/src/api/deleteChatUser.ts b/src/api/deleteChatUser.ts new file mode 100644 index 0000000..addc9d1 --- /dev/null +++ b/src/api/deleteChatUser.ts @@ -0,0 +1,18 @@ +import variableAssignMent from "@utils/variableAssignment"; +import defaultRequest from "src/lib/axios/defaultRequest"; + +interface ChatUserParams { + roomId: string; + targetId: string; +} + +export const API_DELETE_CHAT_USER_KEY = + "/api/chat/{{roomId}}?targetId={{targetId}}"; + +const deletePartyDetail = async ({ roomId, targetId }: ChatUserParams) => { + await defaultRequest.delete( + variableAssignMent(API_DELETE_CHAT_USER_KEY, { roomId, targetId }) + ); +}; + +export default deletePartyDetail; diff --git a/src/api/getChatMessage.ts b/src/api/getChatMessage.ts new file mode 100644 index 0000000..60663e5 --- /dev/null +++ b/src/api/getChatMessage.ts @@ -0,0 +1,29 @@ +import variableAssignMent from '@utils/variableAssignment'; +import defaultRequest from 'src/lib/axios/defaultRequest'; +import { ChatMessageResponse } from 'types/chat/chat'; + +interface ChatMessageParams { + roomId: string; + size: string; + lastChatId: string; +} + +export const API_GET_CHAT_MESSAGE_KEY = + '/api/chat/{{roomId}}?size={{size}}&lastChatId={{lastChatId}}'; + +const getChatMessage = async ({ + roomId, + size, + lastChatId, +}: ChatMessageParams): Promise => { + const { data } = await defaultRequest.get( + variableAssignMent(API_GET_CHAT_MESSAGE_KEY, { + size, + lastChatId, + roomId, + }), + ); + return data; +}; + +export default getChatMessage; diff --git a/src/api/getChatRoomInfo.ts b/src/api/getChatRoomInfo.ts new file mode 100644 index 0000000..e88489b --- /dev/null +++ b/src/api/getChatRoomInfo.ts @@ -0,0 +1,20 @@ +import variableAssignMent from "@utils/variableAssignment"; +import defaultRequest from "src/lib/axios/defaultRequest"; +import { ChatRoomInfoResponse } from "types/chat/chatRooms"; + +interface ChatRoomInfoParams { + chatRoomId: string; +} + +export const API_GET_CHAT_ROOM_INFO_KEY = "/api/chat-rooms/{{chatRoomId}}"; + +const getChatRoomInfo = async ({ + chatRoomId, +}: ChatRoomInfoParams): Promise => { + const { data } = await defaultRequest.get( + variableAssignMent(API_GET_CHAT_ROOM_INFO_KEY, { chatRoomId }) + ); + return data; +}; + +export default getChatRoomInfo; diff --git a/src/api/getChatRooms.ts b/src/api/getChatRooms.ts new file mode 100644 index 0000000..b06c659 --- /dev/null +++ b/src/api/getChatRooms.ts @@ -0,0 +1,23 @@ +import variableAssignMent from "@utils/variableAssignment"; +import defaultRequest from "src/lib/axios/defaultRequest"; +import { ChatRoomsResponse } from "types/chat/chatRooms"; + +interface ChatRoomsParams { + size?: string; + lastChatRoomId?: string; +} + +export const API_GET_CHAT_ROOMS_KEY = + "/api/chat-rooms?size={{size}}&lastChatRoomId={{lastChatRoomId}}"; + +const getChatRooms = async ({ + size = "5", + lastChatRoomId = "0", +}: ChatRoomsParams): Promise => { + const { data } = await defaultRequest.get( + variableAssignMent(API_GET_CHAT_ROOMS_KEY, { size, lastChatRoomId }) + ); + return data; +}; + +export default getChatRooms; diff --git a/src/api/getChatUserList.ts b/src/api/getChatUserList.ts new file mode 100644 index 0000000..f9beb16 --- /dev/null +++ b/src/api/getChatUserList.ts @@ -0,0 +1,18 @@ +import variableAssignMent from "@utils/variableAssignment"; +import defaultRequest from "src/lib/axios/defaultRequest"; +import { ChatUserListResponse } from "types/chat/chatRooms"; + +type RoomId = { roomId: string }; + +export const API_GET_CHAT_USER_LIST_KEY = "/api/chat-rooms/user/{{roomId}}"; + +const getChatUserList = async ({ + roomId, +}: RoomId): Promise => { + const { data } = await defaultRequest.get( + variableAssignMent(API_GET_CHAT_USER_LIST_KEY, { roomId }) + ); + return data; +}; + +export default getChatUserList; diff --git a/src/components/chat/HeaderBtnGroup.tsx b/src/components/chat/HeaderBtnGroup.tsx deleted file mode 100644 index a10377b..0000000 --- a/src/components/chat/HeaderBtnGroup.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import { ReactElement, MouseEventHandler } from "react"; -import PartyUserList from "./PartyUserList"; -import styled from "@emotion/styled"; -import router from "next/router"; - -interface HeaderBtnGroupProps { - isOpenUserList: boolean; - handleOpenUserList: MouseEventHandler; -} - -const Wrapper = styled.header({ - display: "flex", - justifyContent: "space-between", - alignItems: "center", - padding: "0 2rem", - height: "50px", - backgroundColor: "#ddd", -}); - -const BackBtn = styled.button({ - border: "none", - backgroundColor: "transparent", -}); - -const ChatTitle = styled.h3({}); - -const HeaderBtnGroup = ({ - isOpenUserList, - handleOpenUserList, -}: HeaderBtnGroupProps) => { - return ( - - router.back()}>뒤로가기 - 채팅 방 이름 - 파티원 보기 - {isOpenUserList ? ( - - ) : null} - - ); -}; - -HeaderBtnGroup.getLayout = (page: ReactElement) => { - return <>{page}; -}; - -export default HeaderBtnGroup; diff --git a/src/components/chat/MessageList.tsx b/src/components/chat/MessageList.tsx deleted file mode 100644 index 9b77fad..0000000 --- a/src/components/chat/MessageList.tsx +++ /dev/null @@ -1,131 +0,0 @@ -import styled from "@emotion/styled"; -import { ReactElement } from "react"; -import { shouldNotForwardProp } from "@utils/common"; -// import Image from "next/image"; - -const messages = [ - { - img: "", - readCheck: true, - message: "안녕하세요", - id: "me", - }, - { - img: "", - readCheck: true, - message: "신청합니다.", - id: "me", - }, - { - img: "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ2Gz1Gq9Lp3gtG9pm5qT9W8D2PxWMCmb2FLBeoyPo&s", - nickName: "Habeeb", - readCheck: false, - message: "반갑습니다.", - }, -]; - -const List = styled.ul({ - padding: "0 2rem", - margin: "0 auto", - listStyle: "none", - width: "100%", - display: "flex", - flexDirection: "column", -}); - -const ListItem = styled( - "li", - shouldNotForwardProp("userCheck") -)<{ userCheck?: string }>(({ userCheck }) => ({ - display: "flex", - alignItems: "center", - flexDirection: userCheck === "me" ? "row-reverse" : "row", - margin: "1rem 0", -})); - -const ImageBox = styled.div({ - width: "50px", - height: "50px", - borderRadius: "50%", - overflow: "hidden", - display: "flex", - alignItems: "center", - justifyContent: "center", - marginRight: "20px", -}); - -const MessageBox = styled( - "div", - shouldNotForwardProp("userCheck") -)<{ userCheck?: string }>(({ userCheck }) => ({ - display: "flex", - justifyContent: "space-between", - alignItems: "center", - padding: "1rem", - backgroundColor: userCheck === "me" ? "#efebec" : "#efebec", - borderRadius: "10px", -})); - -const TextBox = styled.div({ - display: "flex", - flexDirection: "column", - justifyContent: "center", -}); - -const NickName = styled( - "p", - shouldNotForwardProp("userCheck") -)<{ userCheck?: string }>(({ userCheck }) => ({ - marginTop: 0, - marginBottom: userCheck === "me" ? 0 : "10px", - fontSize: "18px", - fontWeight: "bold", -})); - -const Message = styled( - "p", - shouldNotForwardProp("userCheck") -)<{ userCheck?: string }>(({ userCheck }) => ({ - margin: 0, - color: userCheck === "me" ? "#fff" : "#000", -})); - -const ReadMark = styled( - "div", - shouldNotForwardProp("userCheck") -)<{ userCheck?: string }>(({ userCheck }) => ({ - marginLeft: userCheck === "me" ? "15px" : "0px", - marginRight: userCheck === "me" ? "0px" : "15px", - alignSelf: "flex-end", - color: "rosybrown", -})); - -const MessageList = () => { - return ( - - {messages.reverse().map(({ img, nickName, readCheck, message, id }) => ( - - {img && ( - - {/* nextjs 도메인 설정 후 next Image로 변경 */} - profile - - )} - - - {nickName} - {message} - - - {readCheck ? 1 : ""} - - ))} - - ); -}; - -MessageList.getLayout = (page: ReactElement) => { - return <>{page}; -}; - -export default MessageList; diff --git a/src/components/chat/PartyUserList.tsx b/src/components/chat/PartyUserList.tsx deleted file mode 100644 index eb86a94..0000000 --- a/src/components/chat/PartyUserList.tsx +++ /dev/null @@ -1,127 +0,0 @@ -import styled from "@emotion/styled"; -import { shouldNotForwardProp } from "@utils/common"; -import { ReactElement } from "react"; - -const userList = [ - { - img: "", - nickName: "아무개", - id: "me", - }, - { - img: "", - nickName: "아무개2", - id: 0, - }, - { - img: "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ2Gz1Gq9Lp3gtG9pm5qT9W8D2PxWMCmb2FLBeoyPo&s", - nickName: "아무개3", - id: 1, - }, -]; - -interface PartyUserListProps { - isOpenUserList: boolean; -} - -const Wrapper = styled( - "div", - shouldNotForwardProp("isOpenUserList") -)<{ isOpenUserList: boolean }>(({ isOpenUserList }) => ({ - position: "fixed", - top: 0, - right: isOpenUserList ? 0 : "-40vw", - zIndex: 99999, - width: "40vw", - maxWidth: "310px", - height: "100%", - backgroundColor: "#fff", - transition: "all 0.5s ease-out", -})); - -const Header = styled.header({ - padding: "1rem 2rem", - height: "50px", -}); - -const List = styled.ul({ - padding: "0 2rem", -}); - -const ListItem = styled.li({ - display: "flex", - justifyContent: "space-between", - alignItems: "center", - margin: "10px 0", -}); - -const UserInfo = styled.div({ - display: "flex", - alignItems: "center", -}); - -const ImageBox = styled.div({ - width: "30px", - height: "30px", - borderRadius: "50%", - overflow: "hidden", - display: "flex", - alignItems: "center", - justifyContent: "center", - marginRight: "10px", - backgroundColor: "skyblue", -}); - -const NickName = styled.p({}); - -const Expulsion = styled.button({}); - -const Label = styled.div({ - display: "flex", - justifyContent: "center", - alignItems: "center", - marginRight: "5px", - width: "20px", - height: "20px", - borderRadius: "50%", - fontSize: "8px", - fontWeight: "bold", - color: "#fff", - backgroundColor: "#6c6c6c", -}); - -const PartyUserList = ({ isOpenUserList }: PartyUserListProps) => { - const handleClickUserExpulsion = () => {}; - - return ( - -
파티원 리스트 입니다.
- - {userList - .slice(0) - .reverse() - .map(({ img, nickName, id }) => ( - - - - {img && ( - profile - )} - - {id === "me" && } - {nickName} - - {/* 방장일 경우 표시 */} - 강퇴하기 - - ))} - -
- ); -}; - -PartyUserList.getLayout = (page: ReactElement) => { - return <>{page}; -}; - -export default PartyUserList; diff --git a/src/components/chat/list/ChatRoomList.tsx b/src/components/chat/list/ChatRoomList.tsx new file mode 100644 index 0000000..b6235ff --- /dev/null +++ b/src/components/chat/list/ChatRoomList.tsx @@ -0,0 +1,132 @@ +import TextInput from '@components/common/TextInput'; +import styled from '@emotion/styled'; +import router from 'next/router'; +import Image from 'next/image'; +import dayjs from 'dayjs'; +import { NextPage } from 'next'; +import { useQuery } from '@tanstack/react-query'; +import getChatRooms, { API_GET_CHAT_ROOMS_KEY } from 'src/api/getChatRooms'; + +const Wrapper = styled.div` + padding: 2rem; +`; + +const SearchBox = styled.div` + position: relative; + display: flex; + justify-content: center; +`; + +const SearchButton = styled.button` + min-width: 50px; +`; + +const RoomList = styled.ul` + padding: 0; + margin: 0 auto; + list-style: none; +`; + +const Room = styled.li` + display: flex; + margin: 1rem 0; + padding: 1rem 0; +`; + +const ImageBox = styled.div` + position: relative; + width: 60px; + height: 60px; + border-radius: 50%; + overflow: hidden; + display: flex; + align-items: center; + justify-content: center; + margin-right: 5%; +`; + +const RightBox = styled.div` + width: calc(100% - 60px); + display: flex; + justify-content: space-between; + align-items: center; +`; + +const TextBox = styled.div` + display: flex; + flex-direction: column; + justify-content: center; +`; + +const Title = styled.p` + margin: 0; + font-size: 18px; + font-weight: bold; +`; + +const Message = styled.p` + margin: 0; +`; + +const Recentime = styled.p``; + +const ChatListPage: NextPage = () => { + const { data } = useQuery({ + queryKey: [API_GET_CHAT_ROOMS_KEY], + queryFn: () => getChatRooms({}), + }); + + const handleOnChangeSearch = () => {}; + const handleOnClickSearch = () => {}; + + const handleClickRouteRoom = (roomId: string) => router.push(`/chat/${roomId}`); + + return ( + + + + 검색 + + + {data?.responseChatRoomDtoList.map((item) => { + const { roomId, title, lastMessageTime, lastMessage, thumbnail } = item; + + return ( + handleClickRouteRoom(roomId)}> + + profile + + + + {title} + {lastMessage} + + + {lastMessageTime ? displayTime(lastMessageTime) : ''} + + + + ); + })} + + + ); +}; + +export default ChatListPage; + +export const displayTime = (time: string) => { + const lastMessageTime = dayjs(time).format('YYYY.MM.MM'); + const currentTime = dayjs(Date.now()).format('YYYY.MM.MM'); + + if (lastMessageTime === currentTime) { + return dayjs(time).format('HH:mm'); + } + + return dayjs(time).format('YYYY.MM.MM'); +}; diff --git a/src/components/chat/BottomInputGroup.tsx b/src/components/chat/room/BottomInputGroup.tsx similarity index 100% rename from src/components/chat/BottomInputGroup.tsx rename to src/components/chat/room/BottomInputGroup.tsx diff --git a/src/components/chat/room/ChatRoom.tsx b/src/components/chat/room/ChatRoom.tsx new file mode 100644 index 0000000..7b5880a --- /dev/null +++ b/src/components/chat/room/ChatRoom.tsx @@ -0,0 +1,191 @@ +import BottomInputGroup from '@components/chat/room/BottomInputGroup'; +import HeaderBtnGroup from '@components/chat/room/HeaderBtnGroup'; +import MessageList from '@components/chat/room/MessageList'; +import styled from '@emotion/styled'; +import { useQuery, useQueryClient } from '@tanstack/react-query'; +import { MouseEvent, useState, useEffect, useRef, useLayoutEffect } from 'react'; +import getChatMessage, { API_GET_CHAT_MESSAGE_KEY } from 'src/api/getChatMessage'; +import * as StompJs from '@stomp/stompjs'; +import getChatUserList, { API_GET_CHAT_USER_LIST_KEY } from 'src/api/getChatUserList'; +import getChatRoomInfo, { API_GET_CHAT_ROOM_INFO_KEY } from 'src/api/getChatRoomInfo'; +import dayjs from 'dayjs'; + +const Wrapper = styled.div` + display: flex; + flex-direction: column; + justify-content: space-between; + height: 100%; +`; + +const Contents = styled.main` + display: flex; + flex-direction: column; + justify-content: flex-end; +`; + +interface ChattingRoomProps { + roomId: string; +} + +export type ChatMessagesType = { + // userId: string; + // userImage: string; + // userNickname: string; + // userMessage: string; + + chatId?: number; + senderId?: number; + nickname?: string; + message: string; + createAt?: string; +}; + +let count = 0; + +const ChatRoom = ({ roomId }: ChattingRoomProps) => { + const queryClient = useQueryClient(); + const client = useRef({}); + const [isOpenUserList, setIsOpenUserList] = useState(false); + + const { data: roomInfo } = useQuery({ + queryKey: [ + API_GET_CHAT_ROOM_INFO_KEY, + { + chatRoomId: roomId, + }, + ], + queryFn: () => + getChatRoomInfo({ + chatRoomId: roomId, + }), + }); + + const { data: userList } = useQuery({ + queryKey: [ + API_GET_CHAT_USER_LIST_KEY, + { + roomId, + }, + ], + queryFn: () => + getChatUserList({ + roomId, + }), + }); + + const { data: messages } = useQuery({ + queryKey: [ + API_GET_CHAT_MESSAGE_KEY, + { + roomId, + size: '5', + lastChatId: '0', + }, + ], + queryFn: () => + getChatMessage({ + roomId, + size: '5', + lastChatId: '0', + }), + }); + + const [chatMessages, setChatMessages] = useState([ + { message: '채팅을 시작할 수 있습니다.' }, + ]); + + // const handleCloseUserList = (e: MouseEvent) => { + // e.stopPropagation(); + // setIsOpenUserList(false); + // }; + + const handleOpenUserList = (e: MouseEvent) => { + e.stopPropagation(); + setIsOpenUserList(!isOpenUserList); + }; + + useEffect(() => { + if (!roomId) return; + + const connect = () => { + client.current = new StompJs.Client({ + brokerURL: 'ws://localhost:8080/ws', + connectHeaders: { + Authorization: + 'Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJBY2Nlc3NUb2tlbiIsInJvbGUiOiJST0xFX1VTRVIiLCJzb2NpYWxJZCI6IjEyMzA5ODEyMzA5MTI4MzAxIiwiZXhwIjoxNzE1NDc3ODAzfQ.o3WxWeaLzUEtoDjJgay_KR3uAvdEjjrX00jmjmIXoantYlUsWgWCCj1gbkdOdElqiKuYIdfuTA23tRAy4B_zfA', + }, + reconnectDelay: 0, // 자동 재연결 + heartbeatIncoming: 10000, + heartbeatOutgoing: 10000, + onConnect: (frame) => { + client.current.subscribe( + `/sub/chat/room/${roomId}`, + (message: { body: string }) => { + //구독하는 채널 + const newMessage = JSON.parse(message.body); + + setChatMessages((prev) => { + if (prev) return [...prev, newMessage]; + + return [newMessage]; + }); + }, + ); + }, + }); + + client.current.activate(); + }; + + connect(); + + return () => client.current.deactivate(); + }, [roomId]); + + const handleClickSubmit = async () => { + count += 1; + + client.current.publish({ + destination: `/sub/chat/room/${roomId}`, + body: JSON.stringify({ + roomId: Number(roomId), + userImage: '', + nickname: 'test', + chatUserId: 11, + message: `채팅 메시지 테스트 ${count}`, + createAt: Date.now(), + }), + }); + + // await queryClient.invalidateQueries({ + // queryKey: [ + // API_GET_CHAT_MESSAGE_KEY, + // { roomId: "11", size: "5", lastChatId: "0" }, + // ], + // }); + }; + + useEffect(() => { + console.log(chatMessages); + }, [chatMessages]); + + return ( + + {userList && roomInfo ? ( + + ) : null} + + + + + + + ); +}; + +export default ChatRoom; diff --git a/src/components/chat/room/HeaderBtnGroup.tsx b/src/components/chat/room/HeaderBtnGroup.tsx new file mode 100644 index 0000000..4ecba0a --- /dev/null +++ b/src/components/chat/room/HeaderBtnGroup.tsx @@ -0,0 +1,60 @@ +import { ReactElement, MouseEventHandler } from "react"; +import PartyUserList from "./PartyUserList"; +import ListIcon from "@mui/icons-material/List"; +import styled from "@emotion/styled"; +import { + ChatRoomInfoResponse, + ChatUserListResponse, +} from "types/chat/chatRooms"; +import { HeaderBackButton } from "@components/common/HeaderBackButton"; + +interface HeaderBtnGroupProps { + roomInfo: ChatRoomInfoResponse; + userList: ChatUserListResponse[]; + isOpenUserList: boolean; + handleOpenUserList: MouseEventHandler; +} + +const Wrapper = styled.header` + display: flex; + justify-content: space-between; + align-items: center; + position: relative; + padding: 0 2rem; + height: 50px; + background-color: #ddd; +`; + +const MenuBtn = styled.button` + border: none; + background-color: transparent; + padding: 0; +`; + +const ChatTitle = styled.h3``; + +const HeaderBtnGroup = ({ + roomInfo, + userList, + isOpenUserList, + handleOpenUserList, +}: HeaderBtnGroupProps) => { + return ( + + + {roomInfo.title} + + + + {isOpenUserList ? ( + + ) : null} + + ); +}; + +HeaderBtnGroup.getLayout = (page: ReactElement) => { + return <>{page}; +}; + +export default HeaderBtnGroup; diff --git a/src/components/chat/room/MessageList.tsx b/src/components/chat/room/MessageList.tsx new file mode 100644 index 0000000..1601e2d --- /dev/null +++ b/src/components/chat/room/MessageList.tsx @@ -0,0 +1,118 @@ +import styled from '@emotion/styled'; +import { ReactElement } from 'react'; +import { shouldNotForwardProp } from '@utils/common'; +import { ChatMessagesType } from './ChatRoom'; +import { displayTime } from '../list/ChatRoomList'; + +const List = styled.ul({ + padding: '0 2rem', + margin: '0 auto', + listStyle: 'none', + width: '100%', + display: 'flex', + flexDirection: 'column', +}); + +const ListItem = styled( + 'li', + shouldNotForwardProp('userCheck'), +)<{ userCheck?: string }>(({ userCheck }) => ({ + display: 'flex', + alignItems: 'center', + flexDirection: userCheck === 'me' ? 'row-reverse' : 'row', + margin: '1rem 0', +})); + +const ImageBox = styled.div({ + width: '50px', + height: '50px', + borderRadius: '50%', + overflow: 'hidden', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + marginRight: '20px', +}); + +const MessageBox = styled( + 'div', + shouldNotForwardProp('userCheck'), +)<{ userCheck?: string }>(({ userCheck }) => ({ + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + padding: '1rem', + backgroundColor: userCheck === 'me' ? '#efebec' : '#efebec', + borderRadius: '10px', +})); + +const TextBox = styled.div({ + display: 'flex', + flexDirection: 'column', + justifyContent: 'center', +}); + +const NickName = styled( + 'p', + shouldNotForwardProp('userCheck'), +)<{ userCheck?: string }>(({ userCheck }) => ({ + marginTop: 0, + marginBottom: userCheck === 'me' ? 0 : '10px', + fontSize: '18px', + fontWeight: 'bold', +})); + +const Message = styled( + 'p', + shouldNotForwardProp('userCheck'), +)<{ userCheck?: string }>(({ userCheck }) => ({ + margin: 0, + color: userCheck === 'me' ? '#fff' : '#000', +})); + +const ReadMark = styled( + 'div', + shouldNotForwardProp('userCheck'), +)<{ userCheck?: string }>(({ userCheck }) => ({ + marginLeft: userCheck === 'me' ? '15px' : '0px', + marginRight: userCheck === 'me' ? '0px' : '15px', + alignSelf: 'flex-end', + color: 'rosybrown', +})); + +const NotMessage = styled.div` + margin: 2rem auto; +`; + +interface MessageListProps { + messages: ChatMessagesType[]; +} + +const MessageList = ({ messages }: MessageListProps) => { + return ( + + {messages.map(({ message, nickname, createAt }, index) => { + return nickname ? ( + + {/* {img && } */} + + + {nickname} + {message} + + + {createAt ? displayTime(createAt) : ''} + + ) : ( + {message} + ); + })} + + ); +}; + +MessageList.getLayout = (page: ReactElement) => { + return <>{page}; +}; + +export default MessageList; diff --git a/src/components/chat/room/PartyUserList.tsx b/src/components/chat/room/PartyUserList.tsx new file mode 100644 index 0000000..14ed353 --- /dev/null +++ b/src/components/chat/room/PartyUserList.tsx @@ -0,0 +1,138 @@ +import styled from "@emotion/styled"; +import { ReactElement } from "react"; +import Image from "next/image"; +import { ChatUserListResponse } from "types/chat/chatRooms"; + +interface PartyUserListProps { + isOpenUserList: boolean; + userList: ChatUserListResponse[]; +} + +const Wrapper = styled.div<{ isOpenUserList: boolean }>` + position: absolute; + top: 50px; + left: 0; + transform: ${(props) => + props.isOpenUserList ? "translateX(0)" : "translateX(-100%)"}; + z-index: 99999; + width: 100%; + height: calc(100vh - 50px); + background-color: #fff; +`; + +const Title = styled.h5` + padding: 1rem 2rem; + height: 50px; +`; + +const List = styled.ul` + padding: 0 2rem; +`; + +const ListItem = styled.li` + display: flex; + justify-content: space-between; + align-items: center; + margin: 10px 0; +`; + +const UserInfo = styled.div` + display: flex; + align-items: center; +`; + +const ImageBox = styled.div` + width: 30px; + height: 30px; + border-radius: 50%; + overflow: hidden; + display: flex; + align-items: center; + justify-content: center; + margin-right: 10px; + background-color: skyblue; +`; + +const NickName = styled.p``; + +const Expulsion = styled.button``; + +const Label = styled.div` + display: flex; + justify-content: center; + align-items: center; + margin-right: 5px; + width: 20px; + height: 20px; + border-radius: 50%; + font-size: 8px; + font-weight: bold; + color: #fff; + background-color: #6c6c6c; +`; + +const PartyUserList = ({ userList, isOpenUserList }: PartyUserListProps) => { + const handleClickUserExpulsion = () => {}; + console.log(isOpenUserList); + + return ( + + 내 정보 + + + + {/* {"host-image"} */} + + + {/* 본인일경우 표시 */} + {/* {chatUserId && } */} + + +
+ 파티원 리스트 입니다. + + {userList?.map( + ({ nickname, userProfileImg, role, leader, chatUserId }) => ( + + + + {/* {"host-image"} */} + + {role === "HOST" && } + {/* 본인일경우 표시 */} + {/* {chatUserId && } */} + {nickname} + + {{ + /* 사용자가 방장일 경우 표시 */ + } && + { + /* 본인일경우 제외 */ + } && ( + + 강퇴하기 + + )} + + ) + )} + +
+ ); +}; + +PartyUserList.getLayout = (page: ReactElement) => { + return <>{page}; +}; + +export default PartyUserList; diff --git a/src/components/common/BottomBar.tsx b/src/components/common/BottomBar.tsx index 4270239..11f059a 100644 --- a/src/components/common/BottomBar.tsx +++ b/src/components/common/BottomBar.tsx @@ -51,7 +51,7 @@ const navList = [ }, { title: '파티 채팅', - href: '/chat/list', + href: '/chat', icon: ChatIcon, }, { diff --git a/types/chat/chat.ts b/types/chat/chat.ts new file mode 100644 index 0000000..cf58f6d --- /dev/null +++ b/types/chat/chat.ts @@ -0,0 +1,15 @@ +export interface ChatMessageResponse { + responseChatDtoList: { + chatId: number; + senderId: number; + nickname: string; + message: string; + createAt: string; + + userNickname: string; + }[]; + pageInfo: { + lastPartyId: number; + hasNext: boolean; + }; +} diff --git a/types/chat/chatRooms.ts b/types/chat/chatRooms.ts new file mode 100644 index 0000000..c780afd --- /dev/null +++ b/types/chat/chatRooms.ts @@ -0,0 +1,28 @@ +export interface ChatRoomsResponse { + responseChatRoomDtoList: { + roomId: string; + title: string; + lastUpdate: string; + lastMessage: string; + lastMessageTime: string; + thumbnail: string; + }[]; + pageInfo: { + lastPartyId: number; + hasNext: boolean; + }[]; +} + +export interface ChatRoomInfoResponse { + chatRoomId: number; + title: string; + masterId: number; + partyId: number; +} +export interface ChatUserListResponse { + chatUserId: number; + leader: boolean; + role: string; + nickname: string; + userProfileImg: string; +} From 0c51699b9bc1d7545deb6507611e7fefc0aac974 Mon Sep 17 00:00:00 2001 From: xo-yeon <27xo.yeon@gmail.com> Date: Fri, 3 May 2024 14:22:56 +0900 Subject: [PATCH 04/22] =?UTF-8?q?=ED=8C=8C=ED=8B=B0=20=EC=9C=A0=EC=A0=80?= =?UTF-8?q?=20=EA=B0=95=ED=87=B4=20api=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/deleteChatUser.ts | 19 +-- src/components/chat/room/PartyUserList.tsx | 180 ++++++++++----------- 2 files changed, 98 insertions(+), 101 deletions(-) diff --git a/src/api/deleteChatUser.ts b/src/api/deleteChatUser.ts index addc9d1..b057311 100644 --- a/src/api/deleteChatUser.ts +++ b/src/api/deleteChatUser.ts @@ -1,18 +1,15 @@ -import variableAssignMent from "@utils/variableAssignment"; -import defaultRequest from "src/lib/axios/defaultRequest"; +import variableAssignMent from '@utils/variableAssignment'; +import defaultRequest from 'src/lib/axios/defaultRequest'; interface ChatUserParams { - roomId: string; - targetId: string; + roomId: string; + targetId: string; } -export const API_DELETE_CHAT_USER_KEY = - "/api/chat/{{roomId}}?targetId={{targetId}}"; +export const API_DELETE_CHAT_USER_KEY = '/api/chat/{{roomId}}?targetId={{targetId}}'; -const deletePartyDetail = async ({ roomId, targetId }: ChatUserParams) => { - await defaultRequest.delete( - variableAssignMent(API_DELETE_CHAT_USER_KEY, { roomId, targetId }) - ); +const deletePartyUser = async ({ roomId, targetId }: ChatUserParams) => { + await defaultRequest.delete(variableAssignMent(API_DELETE_CHAT_USER_KEY, { roomId, targetId })); }; -export default deletePartyDetail; +export default deletePartyUser; diff --git a/src/components/chat/room/PartyUserList.tsx b/src/components/chat/room/PartyUserList.tsx index 14ed353..fd17a53 100644 --- a/src/components/chat/room/PartyUserList.tsx +++ b/src/components/chat/room/PartyUserList.tsx @@ -1,56 +1,57 @@ -import styled from "@emotion/styled"; -import { ReactElement } from "react"; -import Image from "next/image"; -import { ChatUserListResponse } from "types/chat/chatRooms"; +import styled from '@emotion/styled'; +import { ReactElement } from 'react'; +import router from 'next/router'; +import Image from 'next/image'; +import { ChatUserListResponse } from 'types/chat/chatRooms'; +import deletePartyUser from 'src/api/deleteChatUser'; interface PartyUserListProps { - isOpenUserList: boolean; - userList: ChatUserListResponse[]; + isOpenUserList: boolean; + userList: ChatUserListResponse[]; } const Wrapper = styled.div<{ isOpenUserList: boolean }>` - position: absolute; - top: 50px; - left: 0; - transform: ${(props) => - props.isOpenUserList ? "translateX(0)" : "translateX(-100%)"}; - z-index: 99999; - width: 100%; - height: calc(100vh - 50px); - background-color: #fff; + position: absolute; + top: 50px; + left: 0; + transform: ${(props) => (props.isOpenUserList ? 'translateX(0)' : 'translateX(-100%)')}; + z-index: 99999; + width: 100%; + height: calc(100vh - 50px); + background-color: #fff; `; const Title = styled.h5` - padding: 1rem 2rem; - height: 50px; + padding: 1rem 2rem; + height: 50px; `; const List = styled.ul` - padding: 0 2rem; + padding: 0 2rem; `; const ListItem = styled.li` - display: flex; - justify-content: space-between; - align-items: center; - margin: 10px 0; + display: flex; + justify-content: space-between; + align-items: center; + margin: 10px 0; `; const UserInfo = styled.div` - display: flex; - align-items: center; + display: flex; + align-items: center; `; const ImageBox = styled.div` - width: 30px; - height: 30px; - border-radius: 50%; - overflow: hidden; - display: flex; - align-items: center; - justify-content: center; - margin-right: 10px; - background-color: skyblue; + width: 30px; + height: 30px; + border-radius: 50%; + overflow: hidden; + display: flex; + align-items: center; + justify-content: center; + margin-right: 10px; + background-color: skyblue; `; const NickName = styled.p``; @@ -58,81 +59,80 @@ const NickName = styled.p``; const Expulsion = styled.button``; const Label = styled.div` - display: flex; - justify-content: center; - align-items: center; - margin-right: 5px; - width: 20px; - height: 20px; - border-radius: 50%; - font-size: 8px; - font-weight: bold; - color: #fff; - background-color: #6c6c6c; + display: flex; + justify-content: center; + align-items: center; + margin-right: 5px; + width: 20px; + height: 20px; + border-radius: 50%; + font-size: 8px; + font-weight: bold; + color: #fff; + background-color: #6c6c6c; `; const PartyUserList = ({ userList, isOpenUserList }: PartyUserListProps) => { - const handleClickUserExpulsion = () => {}; - console.log(isOpenUserList); + const roomId = router.query.id; + const handleClickUserExpulsion = () => { + deletePartyUser({ + roomId: String(roomId), + targetId: '', + }); + }; - return ( - - 내 정보 - - - - {/* + 내 정보 + + + + {/* {"host-image"} */} - + - {/* 본인일경우 표시 */} - {/* {chatUserId && } */} - - -
- 파티원 리스트 입니다. - - {userList?.map( - ({ nickname, userProfileImg, role, leader, chatUserId }) => ( - - - - {/* 나} */} + + +
+ 파티원 리스트 입니다. + + {userList?.map(({ nickname, userProfileImg, role, leader, chatUserId }) => ( + + + + {/* {"host-image"} */} - - {role === "HOST" && } - {/* 본인일경우 표시 */} - {/* {chatUserId && } */} - {nickname} - - {{ - /* 사용자가 방장일 경우 표시 */ - } && - { - /* 본인일경우 제외 */ - } && ( - - 강퇴하기 - - )} - - ) - )} - -
- ); + + {role === 'HOST' && } + {/* 본인일경우 표시 */} + {/* {chatUserId && } */} + {nickname} + + {{ + /* 사용자가 방장일 경우 표시 */ + } && + { + /* 본인일경우 제외 */ + } && 강퇴하기} + + ))} + + + ); }; PartyUserList.getLayout = (page: ReactElement) => { - return <>{page}; + return <>{page}; }; export default PartyUserList; From a1d3391ff6919dbc845bf0d09f0b6147896191c1 Mon Sep 17 00:00:00 2001 From: xo-yeon <27xo.yeon@gmail.com> Date: Thu, 23 May 2024 17:33:31 +0900 Subject: [PATCH 05/22] =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=EB=84=A4?= =?UTF-8?q?=EC=9D=B4=EC=85=98=20=EC=A0=81=EC=9A=A9=20=EB=B0=8F=20api=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 --- pages/chat/[id].tsx | 27 ++-- pages/chat/index.tsx | 44 +++--- pages/party/create.tsx | 2 + src/api/getChatMessage.ts | 35 +++-- src/api/getChatRoomInfo.ts | 21 +-- src/api/getChatRooms.ts | 27 ++-- src/api/getChatUserList.ts | 18 --- src/components/chat/list/ChatRoomList.tsx | 66 +++++---- src/components/chat/room/BottomInputGroup.tsx | 100 +++++++------ src/components/chat/room/ChatRoom.tsx | 132 ++++++------------ src/components/chat/room/HeaderBtnGroup.tsx | 78 +++++------ src/components/chat/room/MessageList.tsx | 128 ++++++++--------- src/components/chat/room/PartyUserList.tsx | 69 +++++---- types/chat/chat.ts | 21 +-- types/chat/chatRooms.ts | 19 ++- 15 files changed, 369 insertions(+), 418 deletions(-) delete mode 100644 src/api/getChatUserList.ts diff --git a/pages/chat/[id].tsx b/pages/chat/[id].tsx index 67fa9e5..0ad46f3 100644 --- a/pages/chat/[id].tsx +++ b/pages/chat/[id].tsx @@ -1,28 +1,29 @@ -import { ReactNode } from "react"; -import { GetServerSideProps } from "next"; -import QuerySuspenseErrorBoundary from "@components/hoc/QuerySuspenseErrorBoundary"; -import ChatRoom from "@components/chat/room/ChatRoom"; +import { ReactNode } from 'react'; +import { GetServerSideProps } from 'next'; +import QuerySuspenseErrorBoundary from '@components/hoc/QuerySuspenseErrorBoundary'; +import ChatRoom from '@components/chat/room/ChatRoom'; +import ProfileLoading from '@components/profile/ProfileLoading'; interface ChatRoomPageProps { - roomId: string; + roomId: string; } const ChatRoomPage = ({ roomId }: ChatRoomPageProps) => ( - - - + }> + + ); ChatRoomPage.getLayout = (page: ReactNode) => { - return <>{page}; + return <>{page}; }; export default ChatRoomPage; export const getServerSideProps: GetServerSideProps = async ({ params }) => { - const { id: roomId } = params as { id: string }; + const { id: roomId } = params as { id: string }; - return { - props: { roomId }, - }; + return { + props: { roomId }, + }; }; diff --git a/pages/chat/index.tsx b/pages/chat/index.tsx index d6a2b51..3cfcd47 100644 --- a/pages/chat/index.tsx +++ b/pages/chat/index.tsx @@ -1,33 +1,33 @@ -import styled from "@emotion/styled"; -import { NextPage } from "next"; -import { DefaultHeader } from "@components/common/DefaultHeader"; -import { DefaultText } from "@components/common/DefaultText"; -import QuerySuspenseErrorBoundary from "@components/hoc/QuerySuspenseErrorBoundary"; -import ChatRoomList from "@components/chat/list/ChatRoomList"; +import styled from '@emotion/styled'; +import { NextPage } from 'next'; +import { DefaultHeader } from '@components/common/DefaultHeader'; +import { DefaultText } from '@components/common/DefaultText'; +import QuerySuspenseErrorBoundary from '@components/hoc/QuerySuspenseErrorBoundary'; +import ChatRoomList from '@components/chat/list/ChatRoomList'; +import ProfileLoading from '@components/profile/ProfileLoading'; const Wrapper = styled.div` - height: 100%; - min-height: calc(100vh); - padding: 45px 0 75px 0; + height: 100%; + min-height: calc(100vh); + padding: 45px 0 75px 0; `; const NotList = styled.div` - text-align: center; - padding: 10rem; - font-size: 3vw; + text-align: center; + padding: 10rem; + font-size: 3vw; `; const ChatListPage: NextPage = () => ( - - } - /> - 참여중인 방이 없습니다.} - > - - - + + } /> + 참여중인 방이 없습니다.} + suspenseFallback={} + > + + + ); export default ChatListPage; diff --git a/pages/party/create.tsx b/pages/party/create.tsx index 01e01ff..d4a7cce 100644 --- a/pages/party/create.tsx +++ b/pages/party/create.tsx @@ -15,6 +15,7 @@ import { postUploadImage } from 'src/api/postUploadImage'; import { useRecoilValue } from 'recoil'; import { API_GET_MAIN_PAGE } from 'src/api/getPartyMainPage'; import { PositionSate } from 'src/recoil-states/positionStates'; +import { getCookie } from 'cookies-next'; const Form = styled.form` display: flex; @@ -40,6 +41,7 @@ export const partySchema = yup.object({ const CreatePage: NextPage = () => { const queryClient = useQueryClient(); + const refreshToken = getCookie('refreshToken'); const position = useRecoilValue(PositionSate); const { mutate: postPartyCreate } = useMutation({ diff --git a/src/api/getChatMessage.ts b/src/api/getChatMessage.ts index 60663e5..3a92377 100644 --- a/src/api/getChatMessage.ts +++ b/src/api/getChatMessage.ts @@ -1,28 +1,27 @@ -import variableAssignMent from '@utils/variableAssignment'; import defaultRequest from 'src/lib/axios/defaultRequest'; -import { ChatMessageResponse } from 'types/chat/chat'; +import { ChatMessageResponse, ChatMessagesType } from 'types/chat/chat'; + +type InfinitePaginationDataType = { + [key in K]: T[]; +} & { + pageInfo: { + page: number; + hasNext: boolean; + }; +}; interface ChatMessageParams { roomId: string; - size: string; - lastChatId: string; + page: number; } -export const API_GET_CHAT_MESSAGE_KEY = - '/api/chat/{{roomId}}?size={{size}}&lastChatId={{lastChatId}}'; +export const API_GET_CHAT_MESSAGE_KEY = '/api/chat/{{roomId}}?page={{page}}'; + +const getChatMessage = async ({ roomId, page }: ChatMessageParams) => { + const { data } = await defaultRequest.get< + InfinitePaginationDataType<'responseChatDtoList', ChatMessagesType> + >(`/api/chat/${roomId}`, { params: { page, size: 5 } }); -const getChatMessage = async ({ - roomId, - size, - lastChatId, -}: ChatMessageParams): Promise => { - const { data } = await defaultRequest.get( - variableAssignMent(API_GET_CHAT_MESSAGE_KEY, { - size, - lastChatId, - roomId, - }), - ); return data; }; diff --git a/src/api/getChatRoomInfo.ts b/src/api/getChatRoomInfo.ts index e88489b..707dc34 100644 --- a/src/api/getChatRoomInfo.ts +++ b/src/api/getChatRoomInfo.ts @@ -1,20 +1,21 @@ -import variableAssignMent from "@utils/variableAssignment"; -import defaultRequest from "src/lib/axios/defaultRequest"; -import { ChatRoomInfoResponse } from "types/chat/chatRooms"; +import variableAssignMent from '@utils/variableAssignment'; +import defaultRequest from 'src/lib/axios/defaultRequest'; +import { ChatRoomInfoResponse } from 'types/chat/chatRooms'; interface ChatRoomInfoParams { - chatRoomId: string; + chatRoomId: string; } -export const API_GET_CHAT_ROOM_INFO_KEY = "/api/chat-rooms/{{chatRoomId}}"; +export const API_GET_CHAT_ROOM_INFO_KEY = '/api/chat-rooms/{{chatRoomId}}'; const getChatRoomInfo = async ({ - chatRoomId, + chatRoomId, }: ChatRoomInfoParams): Promise => { - const { data } = await defaultRequest.get( - variableAssignMent(API_GET_CHAT_ROOM_INFO_KEY, { chatRoomId }) - ); - return data; + const { data } = await defaultRequest.get( + variableAssignMent(API_GET_CHAT_ROOM_INFO_KEY, { chatRoomId }), + ); + + return data; }; export default getChatRoomInfo; diff --git a/src/api/getChatRooms.ts b/src/api/getChatRooms.ts index b06c659..1981be5 100644 --- a/src/api/getChatRooms.ts +++ b/src/api/getChatRooms.ts @@ -1,23 +1,14 @@ -import variableAssignMent from "@utils/variableAssignment"; -import defaultRequest from "src/lib/axios/defaultRequest"; -import { ChatRoomsResponse } from "types/chat/chatRooms"; +import variableAssignMent from '@utils/variableAssignment'; +import defaultRequest from 'src/lib/axios/defaultRequest'; +import { ChatRoomsResponse } from 'types/chat/chatRooms'; +export const API_GET_CHAT_ROOMS_KEY = '/api/chat-rooms?page={{page}}&size={{size}}'; -interface ChatRoomsParams { - size?: string; - lastChatRoomId?: string; -} +const getChatRooms = async (page: number): Promise => { + const { data } = await defaultRequest.get( + variableAssignMent(API_GET_CHAT_ROOMS_KEY, { page: String(page), size: '5' }), + ); -export const API_GET_CHAT_ROOMS_KEY = - "/api/chat-rooms?size={{size}}&lastChatRoomId={{lastChatRoomId}}"; - -const getChatRooms = async ({ - size = "5", - lastChatRoomId = "0", -}: ChatRoomsParams): Promise => { - const { data } = await defaultRequest.get( - variableAssignMent(API_GET_CHAT_ROOMS_KEY, { size, lastChatRoomId }) - ); - return data; + return data; }; export default getChatRooms; diff --git a/src/api/getChatUserList.ts b/src/api/getChatUserList.ts deleted file mode 100644 index f9beb16..0000000 --- a/src/api/getChatUserList.ts +++ /dev/null @@ -1,18 +0,0 @@ -import variableAssignMent from "@utils/variableAssignment"; -import defaultRequest from "src/lib/axios/defaultRequest"; -import { ChatUserListResponse } from "types/chat/chatRooms"; - -type RoomId = { roomId: string }; - -export const API_GET_CHAT_USER_LIST_KEY = "/api/chat-rooms/user/{{roomId}}"; - -const getChatUserList = async ({ - roomId, -}: RoomId): Promise => { - const { data } = await defaultRequest.get( - variableAssignMent(API_GET_CHAT_USER_LIST_KEY, { roomId }) - ); - return data; -}; - -export default getChatUserList; diff --git a/src/components/chat/list/ChatRoomList.tsx b/src/components/chat/list/ChatRoomList.tsx index b6235ff..16f55fe 100644 --- a/src/components/chat/list/ChatRoomList.tsx +++ b/src/components/chat/list/ChatRoomList.tsx @@ -4,8 +4,9 @@ import router from 'next/router'; import Image from 'next/image'; import dayjs from 'dayjs'; import { NextPage } from 'next'; -import { useQuery } from '@tanstack/react-query'; +import { useSuspenseInfiniteQuery } from '@tanstack/react-query'; import getChatRooms, { API_GET_CHAT_ROOMS_KEY } from 'src/api/getChatRooms'; +import { ObserverTrigger } from '@components/hoc/ObserverTrigger'; const Wrapper = styled.div` padding: 2rem; @@ -71,9 +72,13 @@ const Message = styled.p` const Recentime = styled.p``; const ChatListPage: NextPage = () => { - const { data } = useQuery({ + const { fetchNextPage, hasNextPage, data } = useSuspenseInfiniteQuery({ queryKey: [API_GET_CHAT_ROOMS_KEY], - queryFn: () => getChatRooms({}), + queryFn: ({ pageParam = 0 }) => getChatRooms(pageParam), + initialPageParam: 0, + getNextPageParam: (lastPage) => { + if (lastPage.pageInfo.hasNext) return lastPage.pageInfo.page + 1; + }, }); const handleOnChangeSearch = () => {}; @@ -81,6 +86,9 @@ const ChatListPage: NextPage = () => { const handleClickRouteRoom = (roomId: string) => router.push(`/chat/${roomId}`); + const chatRooms = data.pages.map((page) => page.responseChatRoomDtoList).flat(); + const onObserve = () => hasNextPage && fetchNextPage(); + return ( @@ -88,31 +96,33 @@ const ChatListPage: NextPage = () => { 검색 - {data?.responseChatRoomDtoList.map((item) => { - const { roomId, title, lastMessageTime, lastMessage, thumbnail } = item; - - return ( - handleClickRouteRoom(roomId)}> - - profile - - - - {title} - {lastMessage} - - - {lastMessageTime ? displayTime(lastMessageTime) : ''} - - - - ); - })} + + {chatRooms?.map((item) => { + const { roomId, title, lastMessageTime, lastMessage, thumbnail } = item; + + return ( + handleClickRouteRoom(roomId)}> + + profile + + + + {title} + {lastMessage} + + + {lastMessageTime ? displayTime(lastMessageTime) : ''} + + + + ); + })} + ); diff --git a/src/components/chat/room/BottomInputGroup.tsx b/src/components/chat/room/BottomInputGroup.tsx index 81ab9e8..8126968 100644 --- a/src/components/chat/room/BottomInputGroup.tsx +++ b/src/components/chat/room/BottomInputGroup.tsx @@ -1,48 +1,60 @@ -import { ChangeEvent, MouseEvent, ReactElement } from "react"; -import TextInput from "@components/common/TextInput"; -import styled from "@emotion/styled"; +import { ChangeEvent, MouseEvent, ReactElement } from 'react'; +import TextInput from '@components/common/TextInput'; +import styled from '@emotion/styled'; +import { UseFormRegister } from 'react-hook-form'; -const Wrapper = styled.div({ - display: "flex", - justifyContent: "space-between", - gap: "10px", - padding: "1rem 2rem", - backgroundColor: "#ddd", +const Wrapper = styled.form` + display: flex; + justify-content: space-between; + gap: 10px; + padding: 1rem 2rem; + background-color: #ddd; - "& input": { - height: "50px", - }, -}); + input: { + height: 50px; + } +`; -const ImageUploadButton = styled.label({ - display: "flex", - justifyContent: "center", - alignItems: "center", - width: "50px", - border: "none", - borderRadius: "10px", - backgroundColor: "#efebec", - cursor: "pointer", -}); +const ImageUploadButton = styled.label` + display: flex; + justify-content: center; + align-items: center; + width: 50px; + border: none; + border-radius: 10px; + background-color: #efebec; + cursor: pointer; +`; -const SubmitButton = styled.button({ - width: "50px", - border: "none", - borderRadius: "10px", - backgroundColor: "#efebec", -}); +const SubmitButton = styled.button` + width: 50px; + border: none; + border-radius: 10px; + background-color: #efebec; +`; -const BottomInputGroup = () => { - const handleClickSubmit = (e: MouseEvent) => {}; +interface BottomInputGroupProps { + handleClickSubmit: (e: MouseEvent) => Promise; + register: UseFormRegister; +} - const handleChangeImageFile = (e: ChangeEvent) => { - e.preventDefault(); - }; +const BottomInputGroup = ({ register, handleClickSubmit }: BottomInputGroupProps) => { + // const handleClickSubmit = (e: MouseEvent) => {}; - return ( - - - + const handleChangeImageFile = (e: ChangeEvent) => { + e.preventDefault(); + }; + + return ( + + +
+ {/* + { type="file" accept=".jpeg,.png" onChange={handleChangeImageFile} - /> - 전송 -
- ); + /> */} + + 전송 + +
+ ); }; BottomInputGroup.getLayout = (page: ReactElement) => { - return <>{page}; + return <>{page}; }; export default BottomInputGroup; diff --git a/src/components/chat/room/ChatRoom.tsx b/src/components/chat/room/ChatRoom.tsx index 7b5880a..f2bc30a 100644 --- a/src/components/chat/room/ChatRoom.tsx +++ b/src/components/chat/room/ChatRoom.tsx @@ -3,18 +3,21 @@ import HeaderBtnGroup from '@components/chat/room/HeaderBtnGroup'; import MessageList from '@components/chat/room/MessageList'; import styled from '@emotion/styled'; import { useQuery, useQueryClient } from '@tanstack/react-query'; -import { MouseEvent, useState, useEffect, useRef, useLayoutEffect } from 'react'; +import { MouseEvent, useState, useEffect, useRef } from 'react'; import getChatMessage, { API_GET_CHAT_MESSAGE_KEY } from 'src/api/getChatMessage'; import * as StompJs from '@stomp/stompjs'; -import getChatUserList, { API_GET_CHAT_USER_LIST_KEY } from 'src/api/getChatUserList'; import getChatRoomInfo, { API_GET_CHAT_ROOM_INFO_KEY } from 'src/api/getChatRoomInfo'; -import dayjs from 'dayjs'; +import { getCookie } from 'cookies-next'; +import { useForm } from 'react-hook-form'; +import { useSuspenseInfiniteQuery } from '@tanstack/react-query'; +import { ObserverTrigger } from '@components/hoc/ObserverTrigger'; const Wrapper = styled.div` display: flex; flex-direction: column; justify-content: space-between; height: 100%; + overflow: hidden; `; const Contents = styled.main` @@ -27,24 +30,11 @@ interface ChattingRoomProps { roomId: string; } -export type ChatMessagesType = { - // userId: string; - // userImage: string; - // userNickname: string; - // userMessage: string; - - chatId?: number; - senderId?: number; - nickname?: string; - message: string; - createAt?: string; -}; - -let count = 0; - const ChatRoom = ({ roomId }: ChattingRoomProps) => { + const refreshToken = getCookie('refreshToken'); const queryClient = useQueryClient(); const client = useRef({}); + const { register, handleSubmit, getValues } = useForm(); const [isOpenUserList, setIsOpenUserList] = useState(false); const { data: roomInfo } = useQuery({ @@ -60,40 +50,17 @@ const ChatRoom = ({ roomId }: ChattingRoomProps) => { }), }); - const { data: userList } = useQuery({ - queryKey: [ - API_GET_CHAT_USER_LIST_KEY, - { - roomId, - }, - ], - queryFn: () => - getChatUserList({ - roomId, - }), + const { fetchNextPage, hasNextPage, data } = useSuspenseInfiniteQuery({ + queryKey: [API_GET_CHAT_MESSAGE_KEY, { roomId }], + queryFn: ({ pageParam = 0 }) => getChatMessage({ roomId, page: pageParam }), + initialPageParam: 0, + getNextPageParam: (lastPage) => { + if (lastPage.pageInfo.hasNext) { + return lastPage.pageInfo.page + 1; + } + }, }); - const { data: messages } = useQuery({ - queryKey: [ - API_GET_CHAT_MESSAGE_KEY, - { - roomId, - size: '5', - lastChatId: '0', - }, - ], - queryFn: () => - getChatMessage({ - roomId, - size: '5', - lastChatId: '0', - }), - }); - - const [chatMessages, setChatMessages] = useState([ - { message: '채팅을 시작할 수 있습니다.' }, - ]); - // const handleCloseUserList = (e: MouseEvent) => { // e.stopPropagation(); // setIsOpenUserList(false); @@ -105,32 +72,23 @@ const ChatRoom = ({ roomId }: ChattingRoomProps) => { }; useEffect(() => { - if (!roomId) return; + if (!roomId && !refreshToken) return; const connect = () => { client.current = new StompJs.Client({ brokerURL: 'ws://localhost:8080/ws', connectHeaders: { - Authorization: - 'Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJBY2Nlc3NUb2tlbiIsInJvbGUiOiJST0xFX1VTRVIiLCJzb2NpYWxJZCI6IjEyMzA5ODEyMzA5MTI4MzAxIiwiZXhwIjoxNzE1NDc3ODAzfQ.o3WxWeaLzUEtoDjJgay_KR3uAvdEjjrX00jmjmIXoantYlUsWgWCCj1gbkdOdElqiKuYIdfuTA23tRAy4B_zfA', + Authorization: String(refreshToken), }, reconnectDelay: 0, // 자동 재연결 heartbeatIncoming: 10000, heartbeatOutgoing: 10000, onConnect: (frame) => { - client.current.subscribe( - `/sub/chat/room/${roomId}`, - (message: { body: string }) => { - //구독하는 채널 - const newMessage = JSON.parse(message.body); - - setChatMessages((prev) => { - if (prev) return [...prev, newMessage]; - - return [newMessage]; - }); - }, - ); + client.current.subscribe(`/sub/chat/room/${Number(roomId)}`); + + queryClient.invalidateQueries({ + queryKey: [API_GET_CHAT_MESSAGE_KEY, { roomId }], + }); }, }); @@ -140,49 +98,47 @@ const ChatRoom = ({ roomId }: ChattingRoomProps) => { connect(); return () => client.current.deactivate(); - }, [roomId]); + }, [queryClient, refreshToken, roomId]); - const handleClickSubmit = async () => { - count += 1; + const handleClickSubmit = async (e: MouseEvent) => { + e.preventDefault(); + if (!roomInfo?.responseChatUserList) return; client.current.publish({ - destination: `/sub/chat/room/${roomId}`, + destination: `/pub/message`, body: JSON.stringify({ + type: 'TALK', roomId: Number(roomId), - userImage: '', - nickname: 'test', - chatUserId: 11, - message: `채팅 메시지 테스트 ${count}`, + userImage: roomInfo?.responseChatUserList.myInfo?.userProfileImg, + nickname: roomInfo?.responseChatUserList.myInfo.nickname, + chatUserId: Number(roomInfo?.responseChatUserList.myInfo.chatUserId), + message: getValues('message'), createAt: Date.now(), }), }); - // await queryClient.invalidateQueries({ - // queryKey: [ - // API_GET_CHAT_MESSAGE_KEY, - // { roomId: "11", size: "5", lastChatId: "0" }, - // ], - // }); + await queryClient.invalidateQueries({ + queryKey: [API_GET_CHAT_MESSAGE_KEY, { roomId }], + }); }; - useEffect(() => { - console.log(chatMessages); - }, [chatMessages]); + const onObserve = () => hasNextPage && fetchNextPage(); + const messages = data.pages.map((page) => page.responseChatDtoList).flat(); return ( - {userList && roomInfo ? ( + {roomInfo ? ( ) : null} - - - + + + + ); diff --git a/src/components/chat/room/HeaderBtnGroup.tsx b/src/components/chat/room/HeaderBtnGroup.tsx index 4ecba0a..3bafd44 100644 --- a/src/components/chat/room/HeaderBtnGroup.tsx +++ b/src/components/chat/room/HeaderBtnGroup.tsx @@ -1,60 +1,54 @@ -import { ReactElement, MouseEventHandler } from "react"; -import PartyUserList from "./PartyUserList"; -import ListIcon from "@mui/icons-material/List"; -import styled from "@emotion/styled"; -import { - ChatRoomInfoResponse, - ChatUserListResponse, -} from "types/chat/chatRooms"; -import { HeaderBackButton } from "@components/common/HeaderBackButton"; +import { ReactElement, MouseEventHandler } from 'react'; +import PartyUserList from './PartyUserList'; +import ListIcon from '@mui/icons-material/List'; +import styled from '@emotion/styled'; +import { ChatRoomInfoResponse, ChatUserResponse } from 'types/chat/chatRooms'; +import { HeaderBackButton } from '@components/common/HeaderBackButton'; interface HeaderBtnGroupProps { - roomInfo: ChatRoomInfoResponse; - userList: ChatUserListResponse[]; - isOpenUserList: boolean; - handleOpenUserList: MouseEventHandler; + roomInfo: ChatRoomInfoResponse; + isOpenUserList: boolean; + handleOpenUserList: MouseEventHandler; } const Wrapper = styled.header` - display: flex; - justify-content: space-between; - align-items: center; - position: relative; - padding: 0 2rem; - height: 50px; - background-color: #ddd; + display: flex; + justify-content: space-between; + align-items: center; + position: relative; + padding: 0 2rem; + height: 50px; + background-color: #ddd; `; const MenuBtn = styled.button` - border: none; - background-color: transparent; - padding: 0; + border: none; + background-color: transparent; + padding: 0; `; const ChatTitle = styled.h3``; -const HeaderBtnGroup = ({ - roomInfo, - userList, - isOpenUserList, - handleOpenUserList, -}: HeaderBtnGroupProps) => { - return ( - - - {roomInfo.title} - - - - {isOpenUserList ? ( - - ) : null} - - ); +const HeaderBtnGroup = ({ roomInfo, isOpenUserList, handleOpenUserList }: HeaderBtnGroupProps) => { + return ( + + + {roomInfo?.chatRoomInfoRes.title} + + + + {isOpenUserList ? ( + + ) : null} + + ); }; HeaderBtnGroup.getLayout = (page: ReactElement) => { - return <>{page}; + return <>{page}; }; export default HeaderBtnGroup; diff --git a/src/components/chat/room/MessageList.tsx b/src/components/chat/room/MessageList.tsx index 1601e2d..98149e0 100644 --- a/src/components/chat/room/MessageList.tsx +++ b/src/components/chat/room/MessageList.tsx @@ -1,84 +1,70 @@ import styled from '@emotion/styled'; import { ReactElement } from 'react'; -import { shouldNotForwardProp } from '@utils/common'; -import { ChatMessagesType } from './ChatRoom'; import { displayTime } from '../list/ChatRoomList'; +import { ChatMessagesType } from 'types/chat/chat'; -const List = styled.ul({ - padding: '0 2rem', - margin: '0 auto', - listStyle: 'none', - width: '100%', - display: 'flex', - flexDirection: 'column', -}); +const List = styled.ul` + padding: 0 2rem; + margin: 0 auto; + list-style: none; + width: 100%; + height: calc(100vh - 119px); + display: flex; + flex-direction: column-reverse; + overflow-y: auto; +`; -const ListItem = styled( - 'li', - shouldNotForwardProp('userCheck'), -)<{ userCheck?: string }>(({ userCheck }) => ({ - display: 'flex', - alignItems: 'center', - flexDirection: userCheck === 'me' ? 'row-reverse' : 'row', - margin: '1rem 0', -})); +const ListItem = styled.li<{ userCheck?: string }>` + display: flex; + align-items: center; + flex-direction: ${(props) => (props.userCheck === 'me' ? 'row-reverse' : 'row')}; + margin: 1rem 0; +`; -const ImageBox = styled.div({ - width: '50px', - height: '50px', - borderRadius: '50%', - overflow: 'hidden', - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - marginRight: '20px', -}); +const ImageBox = styled.div` + width: 50px; + height: 50px; + border-radius: 50%; + overflow: hidden; + display: flex; + align-items: center; + justify-content: center; + margin-right: 20px; +`; -const MessageBox = styled( - 'div', - shouldNotForwardProp('userCheck'), -)<{ userCheck?: string }>(({ userCheck }) => ({ - display: 'flex', - justifyContent: 'space-between', - alignItems: 'center', - padding: '1rem', - backgroundColor: userCheck === 'me' ? '#efebec' : '#efebec', - borderRadius: '10px', -})); +const MessageBox = styled.div<{ userCheck?: string }>` + display: flex; + justify-content: space-between; + align-items: center; + padding: 1rem; + background-color: ${(props) => (props.userCheck === 'me' ? '#efebec' : '#efebec')}; + border-radius: 10px; +`; -const TextBox = styled.div({ - display: 'flex', - flexDirection: 'column', - justifyContent: 'center', -}); +const TextBox = styled.div` + display: flex; + flex-direction: column; + justify-content: center; +`; -const NickName = styled( - 'p', - shouldNotForwardProp('userCheck'), -)<{ userCheck?: string }>(({ userCheck }) => ({ - marginTop: 0, - marginBottom: userCheck === 'me' ? 0 : '10px', - fontSize: '18px', - fontWeight: 'bold', -})); +const NickName = styled.p<{ userCheck?: string }>` + margin-top: 0; + margin-bottom: ${(props) => (props.userCheck === 'me' ? 0 : '10px')}; + font-size: 18px; + font-weight: bold; +`; -const Message = styled( - 'p', - shouldNotForwardProp('userCheck'), -)<{ userCheck?: string }>(({ userCheck }) => ({ - margin: 0, - color: userCheck === 'me' ? '#fff' : '#000', -})); +const Message = styled.p<{ userCheck?: string }>` + margin: 0; + color: ${(props) => (props.userCheck === 'me' ? '#fff' : '#000')}; +`; -const ReadMark = styled( - 'div', - shouldNotForwardProp('userCheck'), -)<{ userCheck?: string }>(({ userCheck }) => ({ - marginLeft: userCheck === 'me' ? '15px' : '0px', - marginRight: userCheck === 'me' ? '0px' : '15px', - alignSelf: 'flex-end', - color: 'rosybrown', -})); +const ReadMark = styled.div<{ userCheck?: string }>` + margin-left: ${(props) => (props.userCheck === 'me' ? '15px' : '0px')}; + margin-right: ${(props) => (props.userCheck === 'me' ? ' 0px' : '15px')}; + align-self: flex-end; + color: rosybrown; +`; const NotMessage = styled.div` margin: 2rem auto; @@ -101,7 +87,7 @@ const MessageList = ({ messages }: MessageListProps) => { {message} - {createAt ? displayTime(createAt) : ''} + {createAt ? displayTime(String(createAt)) : ''} ) : ( {message} diff --git a/src/components/chat/room/PartyUserList.tsx b/src/components/chat/room/PartyUserList.tsx index fd17a53..0a86aa8 100644 --- a/src/components/chat/room/PartyUserList.tsx +++ b/src/components/chat/room/PartyUserList.tsx @@ -2,12 +2,12 @@ import styled from '@emotion/styled'; import { ReactElement } from 'react'; import router from 'next/router'; import Image from 'next/image'; -import { ChatUserListResponse } from 'types/chat/chatRooms'; +import { ChatUserResponse } from 'types/chat/chatRooms'; import deletePartyUser from 'src/api/deleteChatUser'; interface PartyUserListProps { isOpenUserList: boolean; - userList: ChatUserListResponse[]; + chatUser: ChatUserResponse; } const Wrapper = styled.div<{ isOpenUserList: boolean }>` @@ -43,6 +43,7 @@ const UserInfo = styled.div` `; const ImageBox = styled.div` + position: relative; width: 30px; height: 30px; border-radius: 50%; @@ -72,7 +73,7 @@ const Label = styled.div` background-color: #6c6c6c; `; -const PartyUserList = ({ userList, isOpenUserList }: PartyUserListProps) => { +const PartyUserList = ({ chatUser, isOpenUserList }: PartyUserListProps) => { const roomId = router.query.id; const handleClickUserExpulsion = () => { deletePartyUser({ @@ -87,45 +88,43 @@ const PartyUserList = ({ userList, isOpenUserList }: PartyUserListProps) => { - {/* {"host-image"} */} + - {/* 본인일경우 표시 */} - {/* {chatUserId && } */} + + {chatUser?.myInfo?.nickname}
파티원 리스트 입니다. - {userList?.map(({ nickname, userProfileImg, role, leader, chatUserId }) => ( - - - - {/* {"host-image"} */} - - {role === 'HOST' && } - {/* 본인일경우 표시 */} - {/* {chatUserId && } */} - {nickname} - - {{ - /* 사용자가 방장일 경우 표시 */ - } && - { - /* 본인일경우 제외 */ - } && 강퇴하기} - - ))} + {chatUser?.chatRoomUserDto.map( + ({ nickname, userProfileImg, role, leader, chatUserId }) => ( + + + + + + {role === 'HOST' && } + {nickname} + + + {role === 'HOST' && chatUserId !== chatUser.myInfo.chatUserId && ( + 강퇴하기 + )} + + ), + )} ); diff --git a/types/chat/chat.ts b/types/chat/chat.ts index cf58f6d..b67cd5f 100644 --- a/types/chat/chat.ts +++ b/types/chat/chat.ts @@ -1,15 +1,16 @@ export interface ChatMessageResponse { - responseChatDtoList: { - chatId: number; - senderId: number; - nickname: string; - message: string; - createAt: string; - - userNickname: string; - }[]; + responseChatDtoList: ChatMessagesType[]; pageInfo: { - lastPartyId: number; + page: number; hasNext: boolean; }; } + +export type ChatMessagesType = { + chatId: string; + createAt: string; + imgUrl: string; + message: string; + nickname: string; + senderId: number; +}; diff --git a/types/chat/chatRooms.ts b/types/chat/chatRooms.ts index c780afd..7396e44 100644 --- a/types/chat/chatRooms.ts +++ b/types/chat/chatRooms.ts @@ -8,17 +8,32 @@ export interface ChatRoomsResponse { thumbnail: string; }[]; pageInfo: { - lastPartyId: number; + page: number; hasNext: boolean; - }[]; + }; } export interface ChatRoomInfoResponse { + chatRoomInfoRes: ChatRoomInfo; + responseChatUserList: ChatUserResponse; +} + +export interface ChatRoomInfo { chatRoomId: number; title: string; masterId: number; partyId: number; } + +export interface ChatUserResponse { + chatRoomUserDto: ChatUserListResponse[]; + myInfo: { + chatUserId: number; + nickname: string; + role: string; + userProfileImg: string; + }; +} export interface ChatUserListResponse { chatUserId: number; leader: boolean; From 46818033c415a9d707f79e25a1e14a3937ab0fb1 Mon Sep 17 00:00:00 2001 From: xo-yeon <27xo.yeon@gmail.com> Date: Thu, 23 May 2024 17:45:44 +0900 Subject: [PATCH 06/22] =?UTF-8?q?=EC=B1=84=ED=8C=85=EB=B0=A9=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=EC=97=90=EC=84=9C=20=ED=95=98=EB=8B=A8=20?= =?UTF-8?q?=ED=83=AD=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/common/Layout.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/components/common/Layout.tsx b/src/components/common/Layout.tsx index 1be5074..5da0a31 100644 --- a/src/components/common/Layout.tsx +++ b/src/components/common/Layout.tsx @@ -28,9 +28,7 @@ const BottomSection = styled.section` function Layout({ children }: LayoutProps) { const router = useRouter(); const isVisibleBottom = useMemo(() => { - return ['/', '/search', '/profile', '/party/create', '/chat/list'].includes( - router.pathname, - ); + return ['/', '/search', '/profile', '/party/create', '/chat'].includes(router.pathname); }, [router.pathname]); return ( From 4fefb39530b1e5d2a0ca2ff81d0590435dcb0998 Mon Sep 17 00:00:00 2001 From: xo-yeon <27xo.yeon@gmail.com> Date: Fri, 28 Jun 2024 01:36:40 +0900 Subject: [PATCH 07/22] =?UTF-8?q?=EC=B1=84=ED=8C=85=20=EB=A9=94=EC=8B=9C?= =?UTF-8?q?=EC=A7=80=20=EC=82=AC=EC=9A=A9=EC=9E=90=20=ED=83=80=EC=9E=85?= =?UTF-8?q?=EB=B3=84=20ui=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pages/chat/index.tsx | 2 - pages/party/create.tsx | 5 + src/api/deleteChatUser.ts | 14 ++- src/api/getChatMessage.ts | 4 +- src/api/getChatRooms.ts | 4 +- src/components/chat/room/BottomInputGroup.tsx | 28 ++--- src/components/chat/room/ChatRoom.tsx | 103 ++++++++++++++---- src/components/chat/room/HeaderBtnGroup.tsx | 2 +- src/components/chat/room/MessageList.tsx | 89 +++++++++------ src/components/chat/room/PartyUserList.tsx | 50 ++++----- src/components/hoc/ObserverTrigger.tsx | 26 ++--- src/lib/axios/defaultRequest.ts | 83 +++++++------- types/chat/chatRooms.ts | 14 ++- 13 files changed, 251 insertions(+), 173 deletions(-) diff --git a/pages/chat/index.tsx b/pages/chat/index.tsx index 3cfcd47..0c426a0 100644 --- a/pages/chat/index.tsx +++ b/pages/chat/index.tsx @@ -4,7 +4,6 @@ import { DefaultHeader } from '@components/common/DefaultHeader'; import { DefaultText } from '@components/common/DefaultText'; import QuerySuspenseErrorBoundary from '@components/hoc/QuerySuspenseErrorBoundary'; import ChatRoomList from '@components/chat/list/ChatRoomList'; -import ProfileLoading from '@components/profile/ProfileLoading'; const Wrapper = styled.div` height: 100%; @@ -23,7 +22,6 @@ const ChatListPage: NextPage = () => ( } /> 참여중인 방이 없습니다.} - suspenseFallback={} > diff --git a/pages/party/create.tsx b/pages/party/create.tsx index d4a7cce..f589e4b 100644 --- a/pages/party/create.tsx +++ b/pages/party/create.tsx @@ -16,6 +16,7 @@ import { useRecoilValue } from 'recoil'; import { API_GET_MAIN_PAGE } from 'src/api/getPartyMainPage'; import { PositionSate } from 'src/recoil-states/positionStates'; import { getCookie } from 'cookies-next'; +import { API_GET_CHAT_ROOMS_KEY } from 'src/api/getChatRooms'; const Form = styled.form` display: flex; @@ -89,6 +90,10 @@ const CreatePage: NextPage = () => { ], }); + queryClient.invalidateQueries({ + queryKey: [API_GET_CHAT_ROOMS_KEY], + }); + router.replace(`/party/${data.partyId}`); } }, diff --git a/src/api/deleteChatUser.ts b/src/api/deleteChatUser.ts index b057311..8b66585 100644 --- a/src/api/deleteChatUser.ts +++ b/src/api/deleteChatUser.ts @@ -3,13 +3,19 @@ import defaultRequest from 'src/lib/axios/defaultRequest'; interface ChatUserParams { roomId: string; - targetId: string; + targetChatUserId: number; } -export const API_DELETE_CHAT_USER_KEY = '/api/chat/{{roomId}}?targetId={{targetId}}'; +export const API_DELETE_CHAT_USER_KEY = + '/api/chat/{{roomId}}?targetChatUserId={{targetChatUserId}}'; -const deletePartyUser = async ({ roomId, targetId }: ChatUserParams) => { - await defaultRequest.delete(variableAssignMent(API_DELETE_CHAT_USER_KEY, { roomId, targetId })); +const deletePartyUser = async ({ roomId, targetChatUserId }: ChatUserParams) => { + await defaultRequest.delete( + variableAssignMent(API_DELETE_CHAT_USER_KEY, { + roomId, + targetChatUserId: String(targetChatUserId), + }), + ); }; export default deletePartyUser; diff --git a/src/api/getChatMessage.ts b/src/api/getChatMessage.ts index 3a92377..f7c6c0f 100644 --- a/src/api/getChatMessage.ts +++ b/src/api/getChatMessage.ts @@ -1,5 +1,5 @@ import defaultRequest from 'src/lib/axios/defaultRequest'; -import { ChatMessageResponse, ChatMessagesType } from 'types/chat/chat'; +import { ChatMessagesType } from 'types/chat/chat'; type InfinitePaginationDataType = { [key in K]: T[]; @@ -20,7 +20,7 @@ export const API_GET_CHAT_MESSAGE_KEY = '/api/chat/{{roomId}}?page={{page}}'; const getChatMessage = async ({ roomId, page }: ChatMessageParams) => { const { data } = await defaultRequest.get< InfinitePaginationDataType<'responseChatDtoList', ChatMessagesType> - >(`/api/chat/${roomId}`, { params: { page, size: 5 } }); + >(`/api/chat/${roomId}`, { params: { page } }); return data; }; diff --git a/src/api/getChatRooms.ts b/src/api/getChatRooms.ts index 1981be5..93becb0 100644 --- a/src/api/getChatRooms.ts +++ b/src/api/getChatRooms.ts @@ -1,11 +1,11 @@ import variableAssignMent from '@utils/variableAssignment'; import defaultRequest from 'src/lib/axios/defaultRequest'; import { ChatRoomsResponse } from 'types/chat/chatRooms'; -export const API_GET_CHAT_ROOMS_KEY = '/api/chat-rooms?page={{page}}&size={{size}}'; +export const API_GET_CHAT_ROOMS_KEY = '/api/chat-rooms?page={{page}}'; const getChatRooms = async (page: number): Promise => { const { data } = await defaultRequest.get( - variableAssignMent(API_GET_CHAT_ROOMS_KEY, { page: String(page), size: '5' }), + variableAssignMent(API_GET_CHAT_ROOMS_KEY, { page: String(page) }), ); return data; diff --git a/src/components/chat/room/BottomInputGroup.tsx b/src/components/chat/room/BottomInputGroup.tsx index 8126968..36c940f 100644 --- a/src/components/chat/room/BottomInputGroup.tsx +++ b/src/components/chat/room/BottomInputGroup.tsx @@ -39,31 +39,23 @@ interface BottomInputGroupProps { } const BottomInputGroup = ({ register, handleClickSubmit }: BottomInputGroupProps) => { - // const handleClickSubmit = (e: MouseEvent) => {}; - const handleChangeImageFile = (e: ChangeEvent) => { e.preventDefault(); }; return ( - -
+ {/* - + - - */} + + + + */} 전송 diff --git a/src/components/chat/room/ChatRoom.tsx b/src/components/chat/room/ChatRoom.tsx index f2bc30a..e8f9d65 100644 --- a/src/components/chat/room/ChatRoom.tsx +++ b/src/components/chat/room/ChatRoom.tsx @@ -2,15 +2,15 @@ import BottomInputGroup from '@components/chat/room/BottomInputGroup'; import HeaderBtnGroup from '@components/chat/room/HeaderBtnGroup'; import MessageList from '@components/chat/room/MessageList'; import styled from '@emotion/styled'; -import { useQuery, useQueryClient } from '@tanstack/react-query'; +import { InfiniteData, useQuery, useQueryClient } from '@tanstack/react-query'; import { MouseEvent, useState, useEffect, useRef } from 'react'; import getChatMessage, { API_GET_CHAT_MESSAGE_KEY } from 'src/api/getChatMessage'; -import * as StompJs from '@stomp/stompjs'; +import { IMessage, Client } from '@stomp/stompjs'; import getChatRoomInfo, { API_GET_CHAT_ROOM_INFO_KEY } from 'src/api/getChatRoomInfo'; import { getCookie } from 'cookies-next'; import { useForm } from 'react-hook-form'; import { useSuspenseInfiniteQuery } from '@tanstack/react-query'; -import { ObserverTrigger } from '@components/hoc/ObserverTrigger'; +import { ChatMessagesType } from 'types/chat/chat'; const Wrapper = styled.div` display: flex; @@ -34,7 +34,7 @@ const ChatRoom = ({ roomId }: ChattingRoomProps) => { const refreshToken = getCookie('refreshToken'); const queryClient = useQueryClient(); const client = useRef({}); - const { register, handleSubmit, getValues } = useForm(); + const { register, getValues, setValue } = useForm(); const [isOpenUserList, setIsOpenUserList] = useState(false); const { data: roomInfo } = useQuery({ @@ -55,7 +55,7 @@ const ChatRoom = ({ roomId }: ChattingRoomProps) => { queryFn: ({ pageParam = 0 }) => getChatMessage({ roomId, page: pageParam }), initialPageParam: 0, getNextPageParam: (lastPage) => { - if (lastPage.pageInfo.hasNext) { + if (lastPage.pageInfo?.hasNext) { return lastPage.pageInfo.page + 1; } }, @@ -75,20 +75,73 @@ const ChatRoom = ({ roomId }: ChattingRoomProps) => { if (!roomId && !refreshToken) return; const connect = () => { - client.current = new StompJs.Client({ + client.current = new Client({ brokerURL: 'ws://localhost:8080/ws', connectHeaders: { - Authorization: String(refreshToken), + // Authorization: String(refreshToken), + Authorization: + 'Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJSZWZyZXNoVG9rZW4iLCJyb2xlIjoiUk9MRV9VU0VSIiwic29jaWFsSWQiOiIxMjMwOTgxMjMwOTEyODMwMSIsImV4cCI6MTc0NTQyMjUzMX0.fFVxWepJEz4RmDKw4-impGvzC9U7gMf8AC4g57Z9eBQ-xCNWXBsFNgMoCMrcwtdU1FOMVQ_di1dZAZZgw6Gzyw', }, reconnectDelay: 0, // 자동 재연결 heartbeatIncoming: 10000, heartbeatOutgoing: 10000, - onConnect: (frame) => { - client.current.subscribe(`/sub/chat/room/${Number(roomId)}`); - - queryClient.invalidateQueries({ - queryKey: [API_GET_CHAT_MESSAGE_KEY, { roomId }], - }); + onConnect: () => { + client.current.subscribe( + `/sub/chat/room/${Number(roomId)}`, + async (res: IMessage) => { + const LIST_QUERY_KEY = [API_GET_CHAT_MESSAGE_KEY, { roomId }]; + type LIST_QUERY_TYPE = InfiniteData< + InfinitePaginationDataType< + 'responseChatDtoList', + ChatMessagesType | string + >, + unknown + >; + + await queryClient.cancelQueries({ queryKey: LIST_QUERY_KEY }); + + const regex = /chatUserId/g; + + if (regex.test(res.body)) { + const message = JSON.parse(res.body); + + await queryClient.setQueryData( + LIST_QUERY_KEY, + (prev: LIST_QUERY_TYPE) => { + let newList = prev; + + // last pages unshift + newList.pages[0].responseChatDtoList.unshift({ + chatId: message.createAt, + createAt: message.createAt, + imgUrl: message.userImage, + message: message.message, + nickname: message.nickname, + senderId: message.chatUserId, + }); + + return newList; + }, + ); + } else { + await queryClient.setQueryData( + LIST_QUERY_KEY, + (prev: LIST_QUERY_TYPE) => { + let newList = prev; + + // last pages unshift + newList.pages[0].responseChatDtoList.unshift(res.body); + + return newList; + }, + ); + } + + await queryClient.invalidateQueries({ + queryKey: [API_GET_CHAT_MESSAGE_KEY, { roomId }], + }); + }, + ); }, }); @@ -104,7 +157,10 @@ const ChatRoom = ({ roomId }: ChattingRoomProps) => { e.preventDefault(); if (!roomInfo?.responseChatUserList) return; - client.current.publish({ + const currentTime = Date.now(); + const message = getValues('message'); + + await client.current.publish({ destination: `/pub/message`, body: JSON.stringify({ type: 'TALK', @@ -112,14 +168,12 @@ const ChatRoom = ({ roomId }: ChattingRoomProps) => { userImage: roomInfo?.responseChatUserList.myInfo?.userProfileImg, nickname: roomInfo?.responseChatUserList.myInfo.nickname, chatUserId: Number(roomInfo?.responseChatUserList.myInfo.chatUserId), - message: getValues('message'), - createAt: Date.now(), + message: message, + createAt: currentTime, }), }); - await queryClient.invalidateQueries({ - queryKey: [API_GET_CHAT_MESSAGE_KEY, { roomId }], - }); + setValue('message', ''); }; const onObserve = () => hasNextPage && fetchNextPage(); @@ -135,9 +189,14 @@ const ChatRoom = ({ roomId }: ChattingRoomProps) => { /> ) : null} - - - + {roomInfo ? ( + + ) : null}
diff --git a/src/components/chat/room/HeaderBtnGroup.tsx b/src/components/chat/room/HeaderBtnGroup.tsx index 3bafd44..c7e4fc2 100644 --- a/src/components/chat/room/HeaderBtnGroup.tsx +++ b/src/components/chat/room/HeaderBtnGroup.tsx @@ -2,7 +2,7 @@ import { ReactElement, MouseEventHandler } from 'react'; import PartyUserList from './PartyUserList'; import ListIcon from '@mui/icons-material/List'; import styled from '@emotion/styled'; -import { ChatRoomInfoResponse, ChatUserResponse } from 'types/chat/chatRooms'; +import { ChatRoomInfoResponse } from 'types/chat/chatRooms'; import { HeaderBackButton } from '@components/common/HeaderBackButton'; interface HeaderBtnGroupProps { diff --git a/src/components/chat/room/MessageList.tsx b/src/components/chat/room/MessageList.tsx index 98149e0..278dcd5 100644 --- a/src/components/chat/room/MessageList.tsx +++ b/src/components/chat/room/MessageList.tsx @@ -2,6 +2,9 @@ import styled from '@emotion/styled'; import { ReactElement } from 'react'; import { displayTime } from '../list/ChatRoomList'; import { ChatMessagesType } from 'types/chat/chat'; +import { ObserverTrigger } from '@components/hoc/ObserverTrigger'; +import Image from 'next/image'; +import { MyInfo } from 'types/chat/chatRooms'; const List = styled.ul` padding: 0 2rem; @@ -14,14 +17,15 @@ const List = styled.ul` overflow-y: auto; `; -const ListItem = styled.li<{ userCheck?: string }>` +const ListItem = styled.li<{ userCheck: boolean }>` display: flex; align-items: center; - flex-direction: ${(props) => (props.userCheck === 'me' ? 'row-reverse' : 'row')}; + flex-direction: ${(props) => (props.userCheck ? 'row-reverse' : 'row')}; margin: 1rem 0; `; -const ImageBox = styled.div` +const ImageBox = styled.div<{ userCheck: boolean }>` + position: relative; width: 50px; height: 50px; border-radius: 50%; @@ -29,15 +33,17 @@ const ImageBox = styled.div` display: flex; align-items: center; justify-content: center; - margin-right: 20px; + margin-right: ${(props) => (props.userCheck ? 0 : '10px')}; + margin-left: ${(props) => (props.userCheck ? '10px' : 0)}; `; -const MessageBox = styled.div<{ userCheck?: string }>` +const MessageBox = styled.div<{ userCheck: boolean }>` display: flex; justify-content: space-between; align-items: center; padding: 1rem; - background-color: ${(props) => (props.userCheck === 'me' ? '#efebec' : '#efebec')}; + min-width: 20%; + background-color: ${(props) => (props.userCheck ? '#efebec' : '#efebec')}; border-radius: 10px; `; @@ -47,21 +53,21 @@ const TextBox = styled.div` justify-content: center; `; -const NickName = styled.p<{ userCheck?: string }>` +const NickName = styled.p<{ userCheck: boolean }>` margin-top: 0; - margin-bottom: ${(props) => (props.userCheck === 'me' ? 0 : '10px')}; - font-size: 18px; + margin-bottom: 10px; + font-size: 14px; font-weight: bold; `; -const Message = styled.p<{ userCheck?: string }>` +const Message = styled.p` margin: 0; - color: ${(props) => (props.userCheck === 'me' ? '#fff' : '#000')}; + color: '#000'; `; -const ReadMark = styled.div<{ userCheck?: string }>` - margin-left: ${(props) => (props.userCheck === 'me' ? '15px' : '0px')}; - margin-right: ${(props) => (props.userCheck === 'me' ? ' 0px' : '15px')}; +const ReadMark = styled.div<{ userCheck: boolean }>` + margin-left: ${(props) => (props.userCheck ? '0px' : '10px')}; + margin-right: ${(props) => (props.userCheck ? '10px' : '0px')}; align-self: flex-end; color: rosybrown; `; @@ -71,31 +77,42 @@ const NotMessage = styled.div` `; interface MessageListProps { + myInfo: MyInfo; messages: ChatMessagesType[]; + onObserve: VoidFunction; + observerMinHeight: string; } -const MessageList = ({ messages }: MessageListProps) => { - return ( - - {messages.map(({ message, nickname, createAt }, index) => { - return nickname ? ( - - {/* {img && } */} - - - {nickname} - {message} - - - {createAt ? displayTime(String(createAt)) : ''} - - ) : ( - {message} - ); - })} - - ); -}; +const MessageList = ({ messages, onObserve, observerMinHeight, myInfo }: MessageListProps) => ( + + {messages.map(({ message, nickname, createAt, imgUrl }) => { + return nickname ? ( + + + + + + + {nickname} + {message} + + + + {createAt ? displayTime(String(createAt)) : ''} + + + ) : ( + {message} + ); + })} + + +); MessageList.getLayout = (page: ReactElement) => { return <>{page}; diff --git a/src/components/chat/room/PartyUserList.tsx b/src/components/chat/room/PartyUserList.tsx index 0a86aa8..da2dc25 100644 --- a/src/components/chat/room/PartyUserList.tsx +++ b/src/components/chat/room/PartyUserList.tsx @@ -75,10 +75,10 @@ const Label = styled.div` const PartyUserList = ({ chatUser, isOpenUserList }: PartyUserListProps) => { const roomId = router.query.id; - const handleClickUserExpulsion = () => { + const handleClickUserExpulsion = (chatUserId: number) => { deletePartyUser({ roomId: String(roomId), - targetId: '', + targetChatUserId: chatUserId, }); }; @@ -89,13 +89,12 @@ const PartyUserList = ({ chatUser, isOpenUserList }: PartyUserListProps) => { - {chatUser?.myInfo?.nickname} @@ -103,28 +102,27 @@ const PartyUserList = ({ chatUser, isOpenUserList }: PartyUserListProps) => {
파티원 리스트 입니다. - {chatUser?.chatRoomUserDto.map( - ({ nickname, userProfileImg, role, leader, chatUserId }) => ( - - - - - - {role === 'HOST' && } - {nickname} - - - {role === 'HOST' && chatUserId !== chatUser.myInfo.chatUserId && ( - 강퇴하기 - )} - - ), - )} + {chatUser?.chatRoomUserDto.map(({ nickname, userProfileImg, role, chatUserId }) => ( + + + + + + {role === 'HOST' && } + {nickname} + + {chatUser.myInfo.role === 'HOST' && role !== 'HOST' && ( + handleClickUserExpulsion(chatUserId)}> + 강퇴하기 + + )} + + ))} ); diff --git a/src/components/hoc/ObserverTrigger.tsx b/src/components/hoc/ObserverTrigger.tsx index e95548f..1ab3b26 100644 --- a/src/components/hoc/ObserverTrigger.tsx +++ b/src/components/hoc/ObserverTrigger.tsx @@ -1,20 +1,20 @@ -import Observer from "@components/common/Observer"; -import { FC, PropsWithChildren } from "react"; +import Observer from '@components/common/Observer'; +import { FC, PropsWithChildren } from 'react'; interface ObserverTriggerProps { - onObserve: VoidFunction; - observerMinHeight: string; + onObserve: VoidFunction; + observerMinHeight: string; } export const ObserverTrigger: FC> = ({ - children, - onObserve, - observerMinHeight, + children, + onObserve, + observerMinHeight, }) => { - return ( - <> - {children} - - - ); + return ( + <> + + {children} + + ); }; diff --git a/src/lib/axios/defaultRequest.ts b/src/lib/axios/defaultRequest.ts index 50bd1fa..9025eb3 100644 --- a/src/lib/axios/defaultRequest.ts +++ b/src/lib/axios/defaultRequest.ts @@ -1,50 +1,51 @@ -import axios from "axios"; -import { getCookie } from "cookies-next"; +import axios from 'axios'; +import { getCookie } from 'cookies-next'; const defaultRequest = axios.create({ - baseURL: process.env.MATITTING_HOST_URL, - headers: { - "Content-Type": "application/json", - }, - withCredentials: true, + baseURL: process.env.MATITTING_HOST_URL, + headers: { + 'Content-Type': 'application/json', + }, + withCredentials: true, }); defaultRequest.interceptors.response.use( - async function (response) { - return response; - }, - async function (error) { - if (error.response && error.response.status === 401) { - const refreshToken = getCookie("refreshToken"); - - if (!refreshToken) { - await alert("로그인이 필요합니다. 로그인 해 주세요."); - window.location.href = "/signin"; + async function (response) { + return response; + }, + + async function (error) { + if (error.response && error.response.status === 401) { + const refreshToken = getCookie('refreshToken'); + + if (!refreshToken) { + await alert('로그인이 필요합니다. 로그인 해 주세요.'); + window.location.href = '/signin'; + return Promise.reject(error); + } + + try { + const response = await defaultRequest.get('/oauth2/renew-token', { + headers: { + 'Authorization-Refresh': refreshToken, + }, + }); + + // 토큰 갱신 성공 시 새로운 토큰으로 요청 재시도 + defaultRequest.defaults.headers['Authorization'] = + response.headers['authorization']; + return defaultRequest.request(error.config); + } catch (refreshError) { + // 토큰 갱신에 실패하면 에러 반환 + await alert('토큰 갱신에 실패했습니다. 로그인 페이지로 이동합니다.'); + window.location.href = '/signin'; // Redirect to sign-in page + return Promise.reject(refreshError); + } + } + + // 401 상태 코드가 아닌 경우에는 그대로 오류 반환 return Promise.reject(error); - } - - try { - const response = await defaultRequest.get("/oauth2/renew-token", { - headers: { - "Authorization-Refresh": refreshToken, - }, - }); - - // 토큰 갱신 성공 시 새로운 토큰으로 요청 재시도 - defaultRequest.defaults.headers["Authorization"] = - response.headers["authorization"]; - return defaultRequest.request(error.config); - } catch (refreshError) { - // 토큰 갱신에 실패하면 에러 반환 - await alert("토큰 갱신에 실패했습니다. 로그인 페이지로 이동합니다."); - window.location.href = "/signin"; // Redirect to sign-in page - return Promise.reject(refreshError); - } - } - - // 401 상태 코드가 아닌 경우에는 그대로 오류 반환 - return Promise.reject(error); - } + }, ); export default defaultRequest; diff --git a/types/chat/chatRooms.ts b/types/chat/chatRooms.ts index 7396e44..472196b 100644 --- a/types/chat/chatRooms.ts +++ b/types/chat/chatRooms.ts @@ -27,12 +27,14 @@ export interface ChatRoomInfo { export interface ChatUserResponse { chatRoomUserDto: ChatUserListResponse[]; - myInfo: { - chatUserId: number; - nickname: string; - role: string; - userProfileImg: string; - }; + myInfo: MyInfo; +} + +export interface MyInfo { + chatUserId: number; + nickname: string; + role: string; + userProfileImg: string; } export interface ChatUserListResponse { chatUserId: number; From 47d1ed558be2f787b65a4a8e0d360d5140bb3042 Mon Sep 17 00:00:00 2001 From: xo-yeon <27xo.yeon@gmail.com> Date: Mon, 15 Jul 2024 15:38:27 +0900 Subject: [PATCH 08/22] =?UTF-8?q?=EA=B2=80=EC=83=89=20=EA=B8=B0=EB=8A=A5?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pages/party/create.tsx | 2 - src/api/getChatMessage.ts | 13 +-- src/api/getSearchChatRooms.ts | 19 ++++ src/components/chat/list/ChatRoomItem.tsx | 90 ++++++++++++++++ src/components/chat/list/ChatRoomList.tsx | 124 +++++++++------------- src/components/chat/room/ChatRoom.tsx | 4 +- src/components/common/TextInput.tsx | 103 +++++++++--------- src/components/hoc/ObserverTrigger.tsx | 2 +- types/chat/chat.ts | 16 ++- types/chat/chatRooms.ts | 18 ++-- 10 files changed, 242 insertions(+), 149 deletions(-) create mode 100644 src/api/getSearchChatRooms.ts create mode 100644 src/components/chat/list/ChatRoomItem.tsx diff --git a/pages/party/create.tsx b/pages/party/create.tsx index f589e4b..988dfb2 100644 --- a/pages/party/create.tsx +++ b/pages/party/create.tsx @@ -15,7 +15,6 @@ import { postUploadImage } from 'src/api/postUploadImage'; import { useRecoilValue } from 'recoil'; import { API_GET_MAIN_PAGE } from 'src/api/getPartyMainPage'; import { PositionSate } from 'src/recoil-states/positionStates'; -import { getCookie } from 'cookies-next'; import { API_GET_CHAT_ROOMS_KEY } from 'src/api/getChatRooms'; const Form = styled.form` @@ -42,7 +41,6 @@ export const partySchema = yup.object({ const CreatePage: NextPage = () => { const queryClient = useQueryClient(); - const refreshToken = getCookie('refreshToken'); const position = useRecoilValue(PositionSate); const { mutate: postPartyCreate } = useMutation({ diff --git a/src/api/getChatMessage.ts b/src/api/getChatMessage.ts index f7c6c0f..54e3f07 100644 --- a/src/api/getChatMessage.ts +++ b/src/api/getChatMessage.ts @@ -1,14 +1,5 @@ import defaultRequest from 'src/lib/axios/defaultRequest'; -import { ChatMessagesType } from 'types/chat/chat'; - -type InfinitePaginationDataType = { - [key in K]: T[]; -} & { - pageInfo: { - page: number; - hasNext: boolean; - }; -}; +import { ChatMessagesType, InfinitePaginationChatDataType } from 'types/chat/chat'; interface ChatMessageParams { roomId: string; @@ -19,7 +10,7 @@ export const API_GET_CHAT_MESSAGE_KEY = '/api/chat/{{roomId}}?page={{page}}'; const getChatMessage = async ({ roomId, page }: ChatMessageParams) => { const { data } = await defaultRequest.get< - InfinitePaginationDataType<'responseChatDtoList', ChatMessagesType> + InfinitePaginationChatDataType<'responseChatDtoList', ChatMessagesType> >(`/api/chat/${roomId}`, { params: { page } }); return data; diff --git a/src/api/getSearchChatRooms.ts b/src/api/getSearchChatRooms.ts new file mode 100644 index 0000000..402e647 --- /dev/null +++ b/src/api/getSearchChatRooms.ts @@ -0,0 +1,19 @@ +import defaultRequest from 'src/lib/axios/defaultRequest'; +import { InfinitePaginationChatDataType, SearchChatRoomsResponse } from 'types/chat/chat'; + +interface SearchChatRoomsParams { + title: string; + page: number; +} + +export const API_GET_SEARCH_CHAT_ROOMS_KEY = '/api/chat-rooms/search'; + +const getSearchChatRooms = async ({ title, page }: SearchChatRoomsParams) => { + const { data } = await defaultRequest.get< + InfinitePaginationChatDataType<'responseChatRoomDtoList', SearchChatRoomsResponse> + >(`/api/chat-rooms/search`, { params: { title, page } }); + + return data; +}; + +export default getSearchChatRooms; diff --git a/src/components/chat/list/ChatRoomItem.tsx b/src/components/chat/list/ChatRoomItem.tsx new file mode 100644 index 0000000..1676a11 --- /dev/null +++ b/src/components/chat/list/ChatRoomItem.tsx @@ -0,0 +1,90 @@ +import { displayTime } from './ChatRoomList'; +import styled from '@emotion/styled'; +import Image from 'next/image'; +import { ChatRoomList } from 'types/chat/chatRooms'; + +const Room = styled.li` + display: flex; + margin: 1rem 0; + padding: 1rem 0; +`; + +const ImageBox = styled.div` + position: relative; + width: 60px; + height: 60px; + border-radius: 50%; + overflow: hidden; + display: flex; + align-items: center; + justify-content: center; + margin-right: 5%; +`; + +const RightBox = styled.div` + width: calc(100% - 60px); + display: flex; + justify-content: space-between; + align-items: center; +`; + +const TextBox = styled.div` + display: flex; + flex-direction: column; + justify-content: center; +`; + +const Title = styled.p` + margin: 0; + font-size: 18px; + font-weight: bold; +`; + +const Message = styled.p` + margin: 0; +`; + +const Recentime = styled.p``; + +const NoList = styled.div` + text-align: center; + padding: 10% 0; +`; + +interface ChatRoomItemProps { + list?: ChatRoomList[]; + onClickRouteRoom: (roomId: number) => Promise; + noListText: string; +} + +const ChatRoomItem = ({ list, onClickRouteRoom, noListText }: ChatRoomItemProps) => { + return list?.length ? ( + list?.map((item, index) => { + const { roomId, title, lastMessageTime, lastMessage, thumbnail } = item; + + return ( + onClickRouteRoom(roomId)}> + + thumbnail + + + + {title} + {lastMessage} + + {lastMessageTime ? displayTime(lastMessageTime) : ''} + + + ); + }) + ) : ( + {noListText} + ); +}; + +export default ChatRoomItem; diff --git a/src/components/chat/list/ChatRoomList.tsx b/src/components/chat/list/ChatRoomList.tsx index 16f55fe..7abb488 100644 --- a/src/components/chat/list/ChatRoomList.tsx +++ b/src/components/chat/list/ChatRoomList.tsx @@ -1,12 +1,15 @@ import TextInput from '@components/common/TextInput'; import styled from '@emotion/styled'; import router from 'next/router'; -import Image from 'next/image'; import dayjs from 'dayjs'; import { NextPage } from 'next'; -import { useSuspenseInfiniteQuery } from '@tanstack/react-query'; +import { useQueryClient, useSuspenseInfiniteQuery } from '@tanstack/react-query'; import getChatRooms, { API_GET_CHAT_ROOMS_KEY } from 'src/api/getChatRooms'; import { ObserverTrigger } from '@components/hoc/ObserverTrigger'; +import getSearchChatRooms, { API_GET_SEARCH_CHAT_ROOMS_KEY } from 'src/api/getSearchChatRooms'; +import { useState } from 'react'; +import ChatRoomItem from './ChatRoomItem'; +import { useForm } from 'react-hook-form'; const Wrapper = styled.div` padding: 2rem; @@ -26,52 +29,13 @@ const RoomList = styled.ul` padding: 0; margin: 0 auto; list-style: none; + height: 100%; `; -const Room = styled.li` - display: flex; - margin: 1rem 0; - padding: 1rem 0; -`; - -const ImageBox = styled.div` - position: relative; - width: 60px; - height: 60px; - border-radius: 50%; - overflow: hidden; - display: flex; - align-items: center; - justify-content: center; - margin-right: 5%; -`; - -const RightBox = styled.div` - width: calc(100% - 60px); - display: flex; - justify-content: space-between; - align-items: center; -`; - -const TextBox = styled.div` - display: flex; - flex-direction: column; - justify-content: center; -`; - -const Title = styled.p` - margin: 0; - font-size: 18px; - font-weight: bold; -`; - -const Message = styled.p` - margin: 0; -`; - -const Recentime = styled.p``; - const ChatListPage: NextPage = () => { + const queryClient = useQueryClient(); + const [isSearch, setIsSearch] = useState(false); + const { register, getValues, setValue } = useForm<{ searchText: string }>(); const { fetchNextPage, hasNextPage, data } = useSuspenseInfiniteQuery({ queryKey: [API_GET_CHAT_ROOMS_KEY], queryFn: ({ pageParam = 0 }) => getChatRooms(pageParam), @@ -81,47 +45,59 @@ const ChatListPage: NextPage = () => { }, }); - const handleOnChangeSearch = () => {}; - const handleOnClickSearch = () => {}; + const { data: searchData } = useSuspenseInfiniteQuery({ + queryKey: [API_GET_SEARCH_CHAT_ROOMS_KEY, getValues('searchText')], + queryFn: ({ pageParam = 0 }) => + getValues('searchText')?.length + ? getSearchChatRooms({ title: getValues('searchText'), page: pageParam }) + : null, + initialPageParam: 0, + getNextPageParam: (lastPage) => + lastPage?.pageInfo.hasNext ? lastPage?.pageInfo.page + 1 : null, + }); + + const handleClickReset = () => { + setIsSearch(false); + setValue('searchText', ''); + }; + const handleOnClickSearch = async () => { + setIsSearch(true); + + await queryClient.invalidateQueries({ + queryKey: [API_GET_SEARCH_CHAT_ROOMS_KEY, getValues('searchText')], + }); + }; - const handleClickRouteRoom = (roomId: string) => router.push(`/chat/${roomId}`); + const handleClickRouteRoom = (roomId: number) => router.push(`/chat/${roomId}`); const chatRooms = data.pages.map((page) => page.responseChatRoomDtoList).flat(); + const searchRooms = searchData.pages + .map((page) => (page ? page?.responseChatRoomDtoList : [])) + .flat(); const onObserve = () => hasNextPage && fetchNextPage(); return ( - + 검색 - {chatRooms?.map((item) => { - const { roomId, title, lastMessageTime, lastMessage, thumbnail } = item; - - return ( - handleClickRouteRoom(roomId)}> - - profile - - - - {title} - {lastMessage} - - - {lastMessageTime ? displayTime(lastMessageTime) : ''} - - - - ); - })} + diff --git a/src/components/chat/room/ChatRoom.tsx b/src/components/chat/room/ChatRoom.tsx index e8f9d65..01eba95 100644 --- a/src/components/chat/room/ChatRoom.tsx +++ b/src/components/chat/room/ChatRoom.tsx @@ -10,7 +10,7 @@ import getChatRoomInfo, { API_GET_CHAT_ROOM_INFO_KEY } from 'src/api/getChatRoom import { getCookie } from 'cookies-next'; import { useForm } from 'react-hook-form'; import { useSuspenseInfiniteQuery } from '@tanstack/react-query'; -import { ChatMessagesType } from 'types/chat/chat'; +import { ChatMessagesType, InfinitePaginationChatDataType } from 'types/chat/chat'; const Wrapper = styled.div` display: flex; @@ -91,7 +91,7 @@ const ChatRoom = ({ roomId }: ChattingRoomProps) => { async (res: IMessage) => { const LIST_QUERY_KEY = [API_GET_CHAT_MESSAGE_KEY, { roomId }]; type LIST_QUERY_TYPE = InfiniteData< - InfinitePaginationDataType< + InfinitePaginationChatDataType< 'responseChatDtoList', ChatMessagesType | string >, diff --git a/src/components/common/TextInput.tsx b/src/components/common/TextInput.tsx index 62761e3..7cb0fc8 100644 --- a/src/components/common/TextInput.tsx +++ b/src/components/common/TextInput.tsx @@ -1,72 +1,79 @@ -import styled from "@emotion/styled"; -import { ChangeEvent, InputHTMLAttributes, forwardRef } from "react"; -import { ColorToken } from "styles/Color"; +import styled from '@emotion/styled'; +import { ChangeEvent, InputHTMLAttributes, MouseEventHandler, forwardRef } from 'react'; +import { ColorToken } from 'styles/Color'; interface InputStyleProps { - isBorderRadius?: boolean; - errorMessage?: string; + isBorderRadius?: boolean; + errorMessage?: string; } const Container = styled.div` - position: relative; - width: 100%; + position: relative; + width: 100%; `; const Input = styled.input` - width: 100%; - height: 100%; - padding: 10px 14px; - border: ${({ errorMessage }) => - errorMessage ? `1px solid red` : `1px solid ${ColorToken.text_primary}`}; - background: "#f9f9f9"; - border-radius: ${({ isBorderRadius }) => (isBorderRadius ? "10px" : "0")}; - &:focus { - outline: "none"; - } + width: 100%; + height: 100%; + padding: 10px 14px; + border: ${({ errorMessage }) => + errorMessage ? `1px solid red` : `1px solid ${ColorToken.text_primary}`}; + background: '#f9f9f9'; + border-radius: ${({ isBorderRadius }) => (isBorderRadius ? '10px' : '0')}; + &:focus { + outline: 'none'; + } `; +const Reset = styled.button` + position: absolute; + right: 0; + top: 50%; + transform: translate(-20%, -50%); + font-size: 16px; +`; const ErrorText = styled.p` - color: red; - margin-top: 10px; - margin-left: 5px; + margin-top: 10px; + margin-left: 5px; `; -type InputProps = Omit, "type">; +type InputProps = Omit, 'type'>; interface TextInputProps extends InputProps { - whiteSpace?: boolean; - isBorderRadius?: boolean; - errorMessage?: string; + whiteSpace?: boolean; + isBorderRadius?: boolean; + errorMessage?: string; + isReset?: boolean; + onClickReset?: MouseEventHandler; } - const TextInput = forwardRef( - ({ whiteSpace, isBorderRadius, errorMessage, ...rest }, ref) => { - const onChangeHandler = (e: ChangeEvent) => { - if (!whiteSpace) { - e.target.value = e.target.value.replace(/\s/gi, ""); - } - rest.onChange && rest.onChange(e); - }; + ({ whiteSpace, isBorderRadius, errorMessage, isReset, onClickReset, ...rest }, ref) => { + const onChangeHandler = (e: ChangeEvent) => { + if (!whiteSpace) { + e.target.value = e.target.value.replace(/\s/gi, ''); + } + rest.onChange && rest.onChange(e); + }; - return ( - - - {errorMessage && {errorMessage}} - - ); - } + return ( + + + {isReset ? x : null} + {errorMessage && {errorMessage}} + + ); + }, ); - -TextInput.displayName = "TextInput"; +TextInput.displayName = 'TextInput'; export default TextInput; diff --git a/src/components/hoc/ObserverTrigger.tsx b/src/components/hoc/ObserverTrigger.tsx index 1ab3b26..64fd0d2 100644 --- a/src/components/hoc/ObserverTrigger.tsx +++ b/src/components/hoc/ObserverTrigger.tsx @@ -13,8 +13,8 @@ export const ObserverTrigger: FC> = ({ }) => { return ( <> - {children} + ); }; diff --git a/types/chat/chat.ts b/types/chat/chat.ts index b67cd5f..437fceb 100644 --- a/types/chat/chat.ts +++ b/types/chat/chat.ts @@ -1,10 +1,11 @@ -export interface ChatMessageResponse { - responseChatDtoList: ChatMessagesType[]; +export type InfinitePaginationChatDataType = { + [key in K]: T[]; +} & { pageInfo: { page: number; hasNext: boolean; }; -} +}; export type ChatMessagesType = { chatId: string; @@ -14,3 +15,12 @@ export type ChatMessagesType = { nickname: string; senderId: number; }; + +export interface SearchChatRoomsResponse { + roomId: number; + title: string; + thumbnail: string; + lastMessage: string; + lastUpdate: string; + lastMessageTime: string; +} diff --git a/types/chat/chatRooms.ts b/types/chat/chatRooms.ts index 472196b..e6e74e4 100644 --- a/types/chat/chatRooms.ts +++ b/types/chat/chatRooms.ts @@ -1,18 +1,20 @@ export interface ChatRoomsResponse { - responseChatRoomDtoList: { - roomId: string; - title: string; - lastUpdate: string; - lastMessage: string; - lastMessageTime: string; - thumbnail: string; - }[]; + responseChatRoomDtoList: ChatRoomList[]; pageInfo: { page: number; hasNext: boolean; }; } +export interface ChatRoomList { + roomId: number; + title: string; + lastUpdate: string; + lastMessage: string; + lastMessageTime: string; + thumbnail: string; +} + export interface ChatRoomInfoResponse { chatRoomInfoRes: ChatRoomInfo; responseChatUserList: ChatUserResponse; From 7593f7adc57e34095838ae07adf8dda104008ec0 Mon Sep 17 00:00:00 2001 From: xo-yeon <27xo.yeon@gmail.com> Date: Mon, 15 Jul 2024 15:49:46 +0900 Subject: [PATCH 09/22] =?UTF-8?q?=ED=86=A0=ED=81=B0=20=EC=B2=B4=ED=81=AC?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pages/party/create.tsx | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/pages/party/create.tsx b/pages/party/create.tsx index 988dfb2..f55d85b 100644 --- a/pages/party/create.tsx +++ b/pages/party/create.tsx @@ -1,4 +1,4 @@ -import { NextPage } from 'next'; +import { GetServerSideProps, NextPage } from 'next'; import { ChangeEvent } from 'react'; import styled from '@emotion/styled'; import Create from '@components/party/create/Create'; @@ -142,3 +142,22 @@ const CreatePage: NextPage = () => { }; export default CreatePage; + +export const getServerSideProps: GetServerSideProps = async (context) => { + const { req } = context; + + const refreshToken = req.cookies.refreshToken; + + if (!refreshToken) { + return { + redirect: { + permanent: false, + destination: '/signin', + }, + }; + } + + return { + props: {}, + }; +}; From 8b4668ce45de6a97a86214d00c8824d9f9b54889 Mon Sep 17 00:00:00 2001 From: xo-yeon <27xo.yeon@gmail.com> Date: Tue, 23 Jul 2024 18:13:07 +0900 Subject: [PATCH 10/22] =?UTF-8?q?=EB=B9=84=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=EC=8B=9C=20alert=20=EB=9C=AC=20=ED=9B=84=20=EC=9D=B4=EB=8F=99?= =?UTF-8?q?=20=EB=90=98=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pages/party/create.tsx | 39 +++++++++++++++++++++++++-------------- 1 file changed, 25 insertions(+), 14 deletions(-) diff --git a/pages/party/create.tsx b/pages/party/create.tsx index f55d85b..a51c9a5 100644 --- a/pages/party/create.tsx +++ b/pages/party/create.tsx @@ -1,5 +1,5 @@ import { GetServerSideProps, NextPage } from 'next'; -import { ChangeEvent } from 'react'; +import { ChangeEvent, useEffect } from 'react'; import styled from '@emotion/styled'; import Create from '@components/party/create/Create'; import SearchMap from '@components/party/create/SearchMap'; @@ -39,7 +39,11 @@ export const partySchema = yup.object({ status: yup.string(), }); -const CreatePage: NextPage = () => { +interface CreatePageProviderProps { + loginMessage: string; +} + +export const CreatePage = () => { const queryClient = useQueryClient(); const position = useRecoilValue(PositionSate); @@ -141,23 +145,30 @@ const CreatePage: NextPage = () => { ); }; -export default CreatePage; +const CreatePageProvider: NextPage = ({ loginMessage }) => { + useEffect(() => { + const loginRoutting = async () => { + if (loginMessage.length) { + alert('로그인이 필요합니다. 로그인 해 주세요.'); + await router.replace('/signin'); + } + }; + + loginRoutting(); + }, [loginMessage.length]); + + return loginMessage ? <> : ; +}; + +export default CreatePageProvider; export const getServerSideProps: GetServerSideProps = async (context) => { const { req } = context; - const refreshToken = req.cookies.refreshToken; - if (!refreshToken) { - return { - redirect: { - permanent: false, - destination: '/signin', - }, - }; - } - return { - props: {}, + props: { + loginMessage: refreshToken ? '' : '로그인 필요', + }, }; }; From 30ef84669517b5f822ffc09422c26d5a9a69d6c6 Mon Sep 17 00:00:00 2001 From: xo-yeon <27xo.yeon@gmail.com> Date: Tue, 30 Jul 2024 11:41:05 +0900 Subject: [PATCH 11/22] =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/deleteChatUser.ts | 11 +---- src/components/chat/room/BottomInputGroup.tsx | 49 ++++--------------- src/components/chat/room/ChatRoom.tsx | 18 +++---- src/components/chat/room/HeaderBtnGroup.tsx | 32 ++++++------ src/components/chat/room/MessageList.tsx | 10 ++-- types/chat/chat.ts | 1 + 6 files changed, 37 insertions(+), 84 deletions(-) diff --git a/src/api/deleteChatUser.ts b/src/api/deleteChatUser.ts index 8b66585..3e826a7 100644 --- a/src/api/deleteChatUser.ts +++ b/src/api/deleteChatUser.ts @@ -1,4 +1,3 @@ -import variableAssignMent from '@utils/variableAssignment'; import defaultRequest from 'src/lib/axios/defaultRequest'; interface ChatUserParams { @@ -6,16 +5,8 @@ interface ChatUserParams { targetChatUserId: number; } -export const API_DELETE_CHAT_USER_KEY = - '/api/chat/{{roomId}}?targetChatUserId={{targetChatUserId}}'; - const deletePartyUser = async ({ roomId, targetChatUserId }: ChatUserParams) => { - await defaultRequest.delete( - variableAssignMent(API_DELETE_CHAT_USER_KEY, { - roomId, - targetChatUserId: String(targetChatUserId), - }), - ); + await defaultRequest.delete(`/api/chat/${roomId}`, { data: { targetChatUserId } }); }; export default deletePartyUser; diff --git a/src/components/chat/room/BottomInputGroup.tsx b/src/components/chat/room/BottomInputGroup.tsx index 36c940f..566fba8 100644 --- a/src/components/chat/room/BottomInputGroup.tsx +++ b/src/components/chat/room/BottomInputGroup.tsx @@ -1,4 +1,4 @@ -import { ChangeEvent, MouseEvent, ReactElement } from 'react'; +import { MouseEvent, ReactElement } from 'react'; import TextInput from '@components/common/TextInput'; import styled from '@emotion/styled'; import { UseFormRegister } from 'react-hook-form'; @@ -9,21 +9,6 @@ const Wrapper = styled.form` gap: 10px; padding: 1rem 2rem; background-color: #ddd; - - input: { - height: 50px; - } -`; - -const ImageUploadButton = styled.label` - display: flex; - justify-content: center; - align-items: center; - width: 50px; - border: none; - border-radius: 10px; - background-color: #efebec; - cursor: pointer; `; const SubmitButton = styled.button` @@ -38,30 +23,14 @@ interface BottomInputGroupProps { register: UseFormRegister; } -const BottomInputGroup = ({ register, handleClickSubmit }: BottomInputGroupProps) => { - const handleChangeImageFile = (e: ChangeEvent) => { - e.preventDefault(); - }; - - return ( - - - {/* - + - - */} - - 전송 - - - ); -}; +const BottomInputGroup = ({ register, handleClickSubmit }: BottomInputGroupProps) => ( + + + + 전송 + + +); BottomInputGroup.getLayout = (page: ReactElement) => { return <>{page}; diff --git a/src/components/chat/room/ChatRoom.tsx b/src/components/chat/room/ChatRoom.tsx index 01eba95..c6f68b0 100644 --- a/src/components/chat/room/ChatRoom.tsx +++ b/src/components/chat/room/ChatRoom.tsx @@ -33,7 +33,7 @@ interface ChattingRoomProps { const ChatRoom = ({ roomId }: ChattingRoomProps) => { const refreshToken = getCookie('refreshToken'); const queryClient = useQueryClient(); - const client = useRef({}); + const client = useRef(null); const { register, getValues, setValue } = useForm(); const [isOpenUserList, setIsOpenUserList] = useState(false); @@ -61,11 +61,6 @@ const ChatRoom = ({ roomId }: ChattingRoomProps) => { }, }); - // const handleCloseUserList = (e: MouseEvent) => { - // e.stopPropagation(); - // setIsOpenUserList(false); - // }; - const handleOpenUserList = (e: MouseEvent) => { e.stopPropagation(); setIsOpenUserList(!isOpenUserList); @@ -78,15 +73,13 @@ const ChatRoom = ({ roomId }: ChattingRoomProps) => { client.current = new Client({ brokerURL: 'ws://localhost:8080/ws', connectHeaders: { - // Authorization: String(refreshToken), - Authorization: - 'Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJSZWZyZXNoVG9rZW4iLCJyb2xlIjoiUk9MRV9VU0VSIiwic29jaWFsSWQiOiIxMjMwOTgxMjMwOTEyODMwMSIsImV4cCI6MTc0NTQyMjUzMX0.fFVxWepJEz4RmDKw4-impGvzC9U7gMf8AC4g57Z9eBQ-xCNWXBsFNgMoCMrcwtdU1FOMVQ_di1dZAZZgw6Gzyw', + Authorization: String(refreshToken), }, reconnectDelay: 0, // 자동 재연결 heartbeatIncoming: 10000, heartbeatOutgoing: 10000, onConnect: () => { - client.current.subscribe( + client.current?.subscribe( `/sub/chat/room/${Number(roomId)}`, async (res: IMessage) => { const LIST_QUERY_KEY = [API_GET_CHAT_MESSAGE_KEY, { roomId }]; @@ -116,6 +109,7 @@ const ChatRoom = ({ roomId }: ChattingRoomProps) => { createAt: message.createAt, imgUrl: message.userImage, message: message.message, + messageType: 'TALK', nickname: message.nickname, senderId: message.chatUserId, }); @@ -150,7 +144,7 @@ const ChatRoom = ({ roomId }: ChattingRoomProps) => { connect(); - return () => client.current.deactivate(); + // return () => client.current?.deactivate(); }, [queryClient, refreshToken, roomId]); const handleClickSubmit = async (e: MouseEvent) => { @@ -160,7 +154,7 @@ const ChatRoom = ({ roomId }: ChattingRoomProps) => { const currentTime = Date.now(); const message = getValues('message'); - await client.current.publish({ + await client.current?.publish({ destination: `/pub/message`, body: JSON.stringify({ type: 'TALK', diff --git a/src/components/chat/room/HeaderBtnGroup.tsx b/src/components/chat/room/HeaderBtnGroup.tsx index c7e4fc2..0549eee 100644 --- a/src/components/chat/room/HeaderBtnGroup.tsx +++ b/src/components/chat/room/HeaderBtnGroup.tsx @@ -29,23 +29,21 @@ const MenuBtn = styled.button` const ChatTitle = styled.h3``; -const HeaderBtnGroup = ({ roomInfo, isOpenUserList, handleOpenUserList }: HeaderBtnGroupProps) => { - return ( - - - {roomInfo?.chatRoomInfoRes.title} - - - - {isOpenUserList ? ( - - ) : null} - - ); -}; +const HeaderBtnGroup = ({ roomInfo, isOpenUserList, handleOpenUserList }: HeaderBtnGroupProps) => ( + + + {roomInfo?.chatRoomInfoRes.title} + + + + {isOpenUserList ? ( + + ) : null} + +); HeaderBtnGroup.getLayout = (page: ReactElement) => { return <>{page}; diff --git a/src/components/chat/room/MessageList.tsx b/src/components/chat/room/MessageList.tsx index 278dcd5..9024709 100644 --- a/src/components/chat/room/MessageList.tsx +++ b/src/components/chat/room/MessageList.tsx @@ -72,8 +72,8 @@ const ReadMark = styled.div<{ userCheck: boolean }>` color: rosybrown; `; -const NotMessage = styled.div` - margin: 2rem auto; +const Notification = styled.div` + margin: 1rem auto; `; interface MessageListProps { @@ -85,8 +85,8 @@ interface MessageListProps { const MessageList = ({ messages, onObserve, observerMinHeight, myInfo }: MessageListProps) => ( - {messages.map(({ message, nickname, createAt, imgUrl }) => { - return nickname ? ( + {messages.map(({ message, nickname, createAt, imgUrl, messageType }) => { + return messageType === 'TALK' ? ( ) : ( - {message} + {message} ); })} diff --git a/types/chat/chat.ts b/types/chat/chat.ts index 437fceb..78ef644 100644 --- a/types/chat/chat.ts +++ b/types/chat/chat.ts @@ -12,6 +12,7 @@ export type ChatMessagesType = { createAt: string; imgUrl: string; message: string; + messageType: 'TALK' | 'ENTER' | 'EXIT'; nickname: string; senderId: number; }; From dcfb762ee1f9c6320918746e927e21244da90d08 Mon Sep 17 00:00:00 2001 From: xo-yeon <27xo.yeon@gmail.com> Date: Wed, 7 Aug 2024 17:50:36 +0900 Subject: [PATCH 12/22] =?UTF-8?q?fix:=20getlayout=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pages/chat/[id].tsx | 4 ---- src/components/chat/room/BottomInputGroup.tsx | 4 ---- src/components/chat/room/HeaderBtnGroup.tsx | 4 ---- src/components/chat/room/MessageList.tsx | 4 ---- src/components/chat/room/PartyUserList.tsx | 4 ---- 5 files changed, 20 deletions(-) diff --git a/pages/chat/[id].tsx b/pages/chat/[id].tsx index 0ad46f3..b99aa8b 100644 --- a/pages/chat/[id].tsx +++ b/pages/chat/[id].tsx @@ -14,10 +14,6 @@ const ChatRoomPage = ({ roomId }: ChatRoomPageProps) => ( ); -ChatRoomPage.getLayout = (page: ReactNode) => { - return <>{page}; -}; - export default ChatRoomPage; export const getServerSideProps: GetServerSideProps = async ({ params }) => { diff --git a/src/components/chat/room/BottomInputGroup.tsx b/src/components/chat/room/BottomInputGroup.tsx index 566fba8..812fe83 100644 --- a/src/components/chat/room/BottomInputGroup.tsx +++ b/src/components/chat/room/BottomInputGroup.tsx @@ -32,8 +32,4 @@ const BottomInputGroup = ({ register, handleClickSubmit }: BottomInputGroupProps ); -BottomInputGroup.getLayout = (page: ReactElement) => { - return <>{page}; -}; - export default BottomInputGroup; diff --git a/src/components/chat/room/HeaderBtnGroup.tsx b/src/components/chat/room/HeaderBtnGroup.tsx index 0549eee..8407d4f 100644 --- a/src/components/chat/room/HeaderBtnGroup.tsx +++ b/src/components/chat/room/HeaderBtnGroup.tsx @@ -45,8 +45,4 @@ const HeaderBtnGroup = ({ roomInfo, isOpenUserList, handleOpenUserList }: Header ); -HeaderBtnGroup.getLayout = (page: ReactElement) => { - return <>{page}; -}; - export default HeaderBtnGroup; diff --git a/src/components/chat/room/MessageList.tsx b/src/components/chat/room/MessageList.tsx index 9024709..616b0bc 100644 --- a/src/components/chat/room/MessageList.tsx +++ b/src/components/chat/room/MessageList.tsx @@ -114,8 +114,4 @@ const MessageList = ({ messages, onObserve, observerMinHeight, myInfo }: Message ); -MessageList.getLayout = (page: ReactElement) => { - return <>{page}; -}; - export default MessageList; diff --git a/src/components/chat/room/PartyUserList.tsx b/src/components/chat/room/PartyUserList.tsx index da2dc25..22d7d4c 100644 --- a/src/components/chat/room/PartyUserList.tsx +++ b/src/components/chat/room/PartyUserList.tsx @@ -128,8 +128,4 @@ const PartyUserList = ({ chatUser, isOpenUserList }: PartyUserListProps) => { ); }; -PartyUserList.getLayout = (page: ReactElement) => { - return <>{page}; -}; - export default PartyUserList; From 322b31be0a23a7004aba30373a6406d31c225fa3 Mon Sep 17 00:00:00 2001 From: xo-yeon <27xo.yeon@gmail.com> Date: Wed, 7 Aug 2024 18:28:16 +0900 Subject: [PATCH 13/22] =?UTF-8?q?fix:=20=EB=84=A4=EC=9D=B4=EB=B0=8D=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=20=EB=B0=8F=20alt=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pages/party/edit/[id].tsx | 301 +++++++++--------- src/components/chat/list/ChatRoomList.tsx | 6 +- .../{HeaderBtnGroup.tsx => ChatHeader.tsx} | 6 +- .../{BottomInputGroup.tsx => ChatInput.tsx} | 6 +- src/components/chat/room/ChatRoom.tsx | 16 +- src/components/chat/room/MessageList.tsx | 47 +-- src/components/chat/room/PartyUserList.tsx | 14 +- src/contexts/PartyCreateSearchMap.tsx | 0 types/chat/chatRooms.ts | 4 +- 9 files changed, 199 insertions(+), 201 deletions(-) rename src/components/chat/room/{HeaderBtnGroup.tsx => ChatHeader.tsx} (88%) rename src/components/chat/room/{BottomInputGroup.tsx => ChatInput.tsx} (84%) create mode 100644 src/contexts/PartyCreateSearchMap.tsx diff --git a/pages/party/edit/[id].tsx b/pages/party/edit/[id].tsx index f15a0ef..f68bfc5 100644 --- a/pages/party/edit/[id].tsx +++ b/pages/party/edit/[id].tsx @@ -1,167 +1,156 @@ -import { NextPage } from "next"; -import { ChangeEvent, useEffect } from "react"; -import styled from "@emotion/styled"; -import Create from "@components/party/create/Create"; -import SearchMap from "@components/party/create/SearchMap"; -import useSearchPlace from "@hooks/useSearchPlace"; -import { DefaultHeader } from "@components/common/DefaultHeader"; -import { patchParty } from "src/api/patchParty"; -import getPartyDetail, { - API_GET_PARTY_DETAIL_KEY, -} from "src/api/getPartyDetail"; -import { SubmitHandler, useForm } from "react-hook-form"; -import { yupResolver } from "@hookform/resolvers/yup"; -import { useRouter } from "next/router"; -import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; -import { postUploadImage } from "src/api/postUploadImage"; -import { partySchema } from "../create"; -import { PositionSate } from "src/recoil-states/positionStates"; -import { useRecoilValue } from "recoil"; -import { API_GET_MAIN_PAGE } from "src/api/getPartyMainPage"; +import { NextPage } from 'next'; +import { ChangeEvent, useEffect } from 'react'; +import styled from '@emotion/styled'; +import Create from '@components/party/create/Create'; +import SearchMap from '@components/party/create/SearchMap'; +import useSearchPlace from '@hooks/useSearchPlace'; +import { DefaultHeader } from '@components/common/DefaultHeader'; +import { patchParty } from 'src/api/patchParty'; +import getPartyDetail, { API_GET_PARTY_DETAIL_KEY } from 'src/api/getPartyDetail'; +import { SubmitHandler, useForm } from 'react-hook-form'; +import { yupResolver } from '@hookform/resolvers/yup'; +import { useRouter } from 'next/router'; +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { postUploadImage } from 'src/api/postUploadImage'; +import { partySchema } from '../create'; +import { PositionSate } from 'src/recoil-states/positionStates'; +import { useRecoilValue } from 'recoil'; +import { API_GET_MAIN_PAGE } from 'src/api/getPartyMainPage'; const Form = styled.form` - display: flex; - flex-direction: column; - justify-content: space-between; - height: 100%; - min-height: calc(100vh); - padding: 45px 0 75px 0; + display: flex; + flex-direction: column; + justify-content: space-between; + height: 100%; + min-height: calc(100vh); + padding: 45px 0 75px 0; `; const CreatePage: NextPage = () => { - const queryClient = useQueryClient(); - const router = useRouter(); - const { id } = router.query as { id: string }; - const position = useRecoilValue(PositionSate); - const userId = "0"; // 임시 - - const { data } = useQuery({ - queryKey: [API_GET_PARTY_DETAIL_KEY, { id }], - queryFn: () => getPartyDetail({ id, userId }), - enabled: !!id, - }); - - const { mutate: updateParty } = useMutation({ - mutationFn: patchParty, - }); - - const { mutate: uploadImage } = useMutation({ - mutationFn: postUploadImage, - }); - - const { - marker, - keyword, - resultList, - handleChangeSearchBox, - handleClickPlace, - setPlace, - } = useSearchPlace(); - - const { - handleSubmit, - register, - formState: { isValid }, - setValue, - getValues, - reset, - } = useForm({ - resolver: yupResolver(partySchema), - mode: "onSubmit", - defaultValues: { - thumbnail: "/images/default_thumbnail.jpg", - }, - }); - - const onSubmitPartyForm: SubmitHandler = (formData: PartyForm) => { - if (!marker || !marker.position) return; - - updateParty( - { - id, - params: { - ...formData, - partyPlaceName: marker.content, - latitude: marker.position.lat, - longitude: marker.position.lng, - }, - }, - { - onSuccess: async () => { - await queryClient.invalidateQueries({ - queryKey: [API_GET_PARTY_DETAIL_KEY, { id, userId }], - }); - await queryClient.invalidateQueries({ - queryKey: [ - API_GET_MAIN_PAGE, - { latitude: position.coords.x, longitude: position.coords.y }, - ], - }); - - router.replace(`/party/${id}`); - }, - } - ); - }; - - const handleChangeThumbnail = (e: ChangeEvent) => { - e.preventDefault(); - const { files } = e.target; - - if (files) { - uploadImage(files[0], { - onSuccess({ imgUrl }) { - if (imgUrl) { - setValue("thumbnail", imgUrl); - } - }, - }); - } - }; + const queryClient = useQueryClient(); + const router = useRouter(); + const { id } = router.query as { id: string }; + const position = useRecoilValue(PositionSate); + const userId = '0'; // 임시 + + const { data } = useQuery({ + queryKey: [API_GET_PARTY_DETAIL_KEY, { id }], + queryFn: () => getPartyDetail({ id, userId }), + enabled: !!id, + }); - useEffect(() => { - if (!data) return; + const { mutate: updateParty } = useMutation({ + mutationFn: patchParty, + }); - setPlace({ - lat: data?.latitude, - lng: data?.longitude, - placeName: data?.partyPlaceName, + const { mutate: uploadImage } = useMutation({ + mutationFn: postUploadImage, }); - setValue("thumbnail", data?.thumbnail); - }, [data, setPlace, setValue]); - - const rightHeaderArea = ( - - ); - - if (!data) return <>; - - return ( -
- - - - - - ); + + const { marker, keyword, resultList, handleChangeSearchBox, handleClickPlace, setPlace } = + useSearchPlace(); + + const { + handleSubmit, + register, + formState: { isValid }, + setValue, + getValues, + reset, + } = useForm({ + resolver: yupResolver(partySchema), + mode: 'onSubmit', + defaultValues: { + thumbnail: '/images/default_thumbnail.jpg', + }, + }); + + const onSubmitPartyForm: SubmitHandler = (formData: PartyForm) => { + if (!marker || !marker.position) return; + + updateParty( + { + id, + params: { + ...formData, + partyPlaceName: marker.content, + latitude: marker.position.lat, + longitude: marker.position.lng, + }, + }, + { + onSuccess: async () => { + await queryClient.invalidateQueries({ + queryKey: [API_GET_PARTY_DETAIL_KEY, { id, userId }], + }); + await queryClient.invalidateQueries({ + queryKey: [ + API_GET_MAIN_PAGE, + { latitude: position.coords.x, longitude: position.coords.y }, + ], + }); + + router.replace(`/party/${id}`); + }, + }, + ); + }; + + const handleChangeThumbnail = (e: ChangeEvent) => { + e.preventDefault(); + const { files } = e.target; + + if (files) { + uploadImage(files[0], { + onSuccess({ imgUrl }) { + if (imgUrl) { + setValue('thumbnail', imgUrl); + } + }, + }); + } + }; + + useEffect(() => { + if (!data) return; + + setPlace({ + lat: data?.latitude, + lng: data?.longitude, + placeName: data?.partyPlaceName, + }); + setValue('thumbnail', data?.thumbnail); + }, [data, setPlace, setValue]); + + const rightHeaderArea = ( + + ); + + if (!data) return <>; + + return ( +
+ + + + + + ); }; export default CreatePage; diff --git a/src/components/chat/list/ChatRoomList.tsx b/src/components/chat/list/ChatRoomList.tsx index 7abb488..7e86eaf 100644 --- a/src/components/chat/list/ChatRoomList.tsx +++ b/src/components/chat/list/ChatRoomList.tsx @@ -107,12 +107,12 @@ const ChatListPage: NextPage = () => { export default ChatListPage; export const displayTime = (time: string) => { - const lastMessageTime = dayjs(time).format('YYYY.MM.MM'); - const currentTime = dayjs(Date.now()).format('YYYY.MM.MM'); + const lastMessageTime = dayjs(time).format('YYYY.MM.DD'); + const currentTime = dayjs().format('YYYY.MM.DD'); if (lastMessageTime === currentTime) { return dayjs(time).format('HH:mm'); } - return dayjs(time).format('YYYY.MM.MM'); + return dayjs(time).format('YYYY.MM.DD'); }; diff --git a/src/components/chat/room/HeaderBtnGroup.tsx b/src/components/chat/room/ChatHeader.tsx similarity index 88% rename from src/components/chat/room/HeaderBtnGroup.tsx rename to src/components/chat/room/ChatHeader.tsx index 8407d4f..813f7d1 100644 --- a/src/components/chat/room/HeaderBtnGroup.tsx +++ b/src/components/chat/room/ChatHeader.tsx @@ -5,7 +5,7 @@ import styled from '@emotion/styled'; import { ChatRoomInfoResponse } from 'types/chat/chatRooms'; import { HeaderBackButton } from '@components/common/HeaderBackButton'; -interface HeaderBtnGroupProps { +interface ChatHeaderProps { roomInfo: ChatRoomInfoResponse; isOpenUserList: boolean; handleOpenUserList: MouseEventHandler; @@ -29,7 +29,7 @@ const MenuBtn = styled.button` const ChatTitle = styled.h3``; -const HeaderBtnGroup = ({ roomInfo, isOpenUserList, handleOpenUserList }: HeaderBtnGroupProps) => ( +const ChatHeader = ({ roomInfo, isOpenUserList, handleOpenUserList }: ChatHeaderProps) => ( {roomInfo?.chatRoomInfoRes.title} @@ -45,4 +45,4 @@ const HeaderBtnGroup = ({ roomInfo, isOpenUserList, handleOpenUserList }: Header ); -export default HeaderBtnGroup; +export default ChatHeader; diff --git a/src/components/chat/room/BottomInputGroup.tsx b/src/components/chat/room/ChatInput.tsx similarity index 84% rename from src/components/chat/room/BottomInputGroup.tsx rename to src/components/chat/room/ChatInput.tsx index 812fe83..a3e5db5 100644 --- a/src/components/chat/room/BottomInputGroup.tsx +++ b/src/components/chat/room/ChatInput.tsx @@ -18,12 +18,12 @@ const SubmitButton = styled.button` background-color: #efebec; `; -interface BottomInputGroupProps { +interface ChatInputProps { handleClickSubmit: (e: MouseEvent) => Promise; register: UseFormRegister; } -const BottomInputGroup = ({ register, handleClickSubmit }: BottomInputGroupProps) => ( +const ChatInput = ({ register, handleClickSubmit }: ChatInputProps) => ( @@ -32,4 +32,4 @@ const BottomInputGroup = ({ register, handleClickSubmit }: BottomInputGroupProps ); -export default BottomInputGroup; +export default ChatInput; diff --git a/src/components/chat/room/ChatRoom.tsx b/src/components/chat/room/ChatRoom.tsx index c6f68b0..ed67d8d 100644 --- a/src/components/chat/room/ChatRoom.tsx +++ b/src/components/chat/room/ChatRoom.tsx @@ -1,5 +1,5 @@ -import BottomInputGroup from '@components/chat/room/BottomInputGroup'; -import HeaderBtnGroup from '@components/chat/room/HeaderBtnGroup'; +import ChatInput from '@components/chat/room/ChatInput'; +import ChatHeader from '@components/chat/room/ChatHeader'; import MessageList from '@components/chat/room/MessageList'; import styled from '@emotion/styled'; import { InfiniteData, useQuery, useQueryClient } from '@tanstack/react-query'; @@ -159,9 +159,9 @@ const ChatRoom = ({ roomId }: ChattingRoomProps) => { body: JSON.stringify({ type: 'TALK', roomId: Number(roomId), - userImage: roomInfo?.responseChatUserList.myInfo?.userProfileImg, - nickname: roomInfo?.responseChatUserList.myInfo.nickname, - chatUserId: Number(roomInfo?.responseChatUserList.myInfo.chatUserId), + userImage: roomInfo?.responseChatUserList.chatUserInfo?.userProfileImg, + nickname: roomInfo?.responseChatUserList.chatUserInfo.nickname, + chatUserId: Number(roomInfo?.responseChatUserList.chatUserInfo.chatUserId), message: message, createAt: currentTime, }), @@ -176,7 +176,7 @@ const ChatRoom = ({ roomId }: ChattingRoomProps) => { return ( {roomInfo ? ( - { {roomInfo ? ( ) : null} - + ); diff --git a/src/components/chat/room/MessageList.tsx b/src/components/chat/room/MessageList.tsx index 616b0bc..ff52a70 100644 --- a/src/components/chat/room/MessageList.tsx +++ b/src/components/chat/room/MessageList.tsx @@ -4,7 +4,7 @@ import { displayTime } from '../list/ChatRoomList'; import { ChatMessagesType } from 'types/chat/chat'; import { ObserverTrigger } from '@components/hoc/ObserverTrigger'; import Image from 'next/image'; -import { MyInfo } from 'types/chat/chatRooms'; +import { ChatUserInfo } from 'types/chat/chatRooms'; const List = styled.ul` padding: 0 2rem; @@ -17,14 +17,14 @@ const List = styled.ul` overflow-y: auto; `; -const ListItem = styled.li<{ userCheck: boolean }>` +const ListItem = styled.li<{ isChecked: boolean }>` display: flex; align-items: center; - flex-direction: ${(props) => (props.userCheck ? 'row-reverse' : 'row')}; + flex-direction: ${(props) => (props.isChecked ? 'row-reverse' : 'row')}; margin: 1rem 0; `; -const ImageBox = styled.div<{ userCheck: boolean }>` +const ImageBox = styled.div<{ isChecked: boolean }>` position: relative; width: 50px; height: 50px; @@ -33,17 +33,17 @@ const ImageBox = styled.div<{ userCheck: boolean }>` display: flex; align-items: center; justify-content: center; - margin-right: ${(props) => (props.userCheck ? 0 : '10px')}; - margin-left: ${(props) => (props.userCheck ? '10px' : 0)}; + margin-right: ${(props) => (props.isChecked ? 0 : '10px')}; + margin-left: ${(props) => (props.isChecked ? '10px' : 0)}; `; -const MessageBox = styled.div<{ userCheck: boolean }>` +const MessageBox = styled.div<{ isChecked: boolean }>` display: flex; justify-content: space-between; align-items: center; padding: 1rem; min-width: 20%; - background-color: ${(props) => (props.userCheck ? '#efebec' : '#efebec')}; + background-color: ${(props) => (props.isChecked ? '#efebec' : '#efebec')}; border-radius: 10px; `; @@ -53,7 +53,7 @@ const TextBox = styled.div` justify-content: center; `; -const NickName = styled.p<{ userCheck: boolean }>` +const NickName = styled.p<{ isChecked: boolean }>` margin-top: 0; margin-bottom: 10px; font-size: 14px; @@ -65,9 +65,9 @@ const Message = styled.p` color: '#000'; `; -const ReadMark = styled.div<{ userCheck: boolean }>` - margin-left: ${(props) => (props.userCheck ? '0px' : '10px')}; - margin-right: ${(props) => (props.userCheck ? '10px' : '0px')}; +const ReadMark = styled.div<{ isChecked: boolean }>` + margin-left: ${(props) => (props.isChecked ? '0px' : '10px')}; + margin-right: ${(props) => (props.isChecked ? '10px' : '0px')}; align-self: flex-end; color: rosybrown; `; @@ -77,32 +77,39 @@ const Notification = styled.div` `; interface MessageListProps { - myInfo: MyInfo; + chatUserInfo: ChatUserInfo; messages: ChatMessagesType[]; onObserve: VoidFunction; observerMinHeight: string; } -const MessageList = ({ messages, onObserve, observerMinHeight, myInfo }: MessageListProps) => ( +const MessageList = ({ + messages, + onObserve, + observerMinHeight, + chatUserInfo, +}: MessageListProps) => ( {messages.map(({ message, nickname, createAt, imgUrl, messageType }) => { return messageType === 'TALK' ? ( - - + + - + - {nickname} + + {nickname} + {message} - + {createAt ? displayTime(String(createAt)) : ''} diff --git a/src/components/chat/room/PartyUserList.tsx b/src/components/chat/room/PartyUserList.tsx index 22d7d4c..6d9e1c3 100644 --- a/src/components/chat/room/PartyUserList.tsx +++ b/src/components/chat/room/PartyUserList.tsx @@ -1,5 +1,4 @@ import styled from '@emotion/styled'; -import { ReactElement } from 'react'; import router from 'next/router'; import Image from 'next/image'; import { ChatUserResponse } from 'types/chat/chatRooms'; @@ -89,14 +88,17 @@ const PartyUserList = ({ chatUser, isOpenUserList }: PartyUserListProps) => { - {chatUser?.myInfo?.nickname} + {chatUser?.chatUserInfo?.nickname}
@@ -110,13 +112,13 @@ const PartyUserList = ({ chatUser, isOpenUserList }: PartyUserListProps) => { src="/images/profile/profile.png" fill style={{ objectFit: 'cover' }} - alt="" + alt="프로필 이미지" /> {role === 'HOST' && } {nickname} - {chatUser.myInfo.role === 'HOST' && role !== 'HOST' && ( + {chatUser.chatUserInfo.role === 'HOST' && role !== 'HOST' && ( handleClickUserExpulsion(chatUserId)}> 강퇴하기 diff --git a/src/contexts/PartyCreateSearchMap.tsx b/src/contexts/PartyCreateSearchMap.tsx new file mode 100644 index 0000000..e69de29 diff --git a/types/chat/chatRooms.ts b/types/chat/chatRooms.ts index e6e74e4..97cf85f 100644 --- a/types/chat/chatRooms.ts +++ b/types/chat/chatRooms.ts @@ -29,10 +29,10 @@ export interface ChatRoomInfo { export interface ChatUserResponse { chatRoomUserDto: ChatUserListResponse[]; - myInfo: MyInfo; + chatUserInfo: ChatUserInfo; } -export interface MyInfo { +export interface ChatUserInfo { chatUserId: number; nickname: string; role: string; From ecc9c52fc3b30cc8cdb5b35b6f39a0d157f87bb3 Mon Sep 17 00:00:00 2001 From: xo-yeon <27xo.yeon@gmail.com> Date: Fri, 9 Aug 2024 13:46:09 +0900 Subject: [PATCH 14/22] =?UTF-8?q?fix:=20=ED=8C=8C=ED=8B=B0=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=88=98=EC=A0=95?= =?UTF-8?q?=EC=A4=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pages/party/create.tsx | 106 ++++------ pages/party/edit/[id].tsx | 92 +++------ src/api/getPartyDetail.ts | 24 +-- src/components/common/Map.tsx | 22 +- src/components/party/create/Create.tsx | 236 ++++++++++++---------- src/components/party/create/MapSearch.tsx | 44 ++++ src/components/party/create/SearchBox.tsx | 161 ++++++++------- src/components/party/create/SearchMap.tsx | 91 --------- src/components/party/create/Thumbnail.tsx | 80 ++++---- src/contexts/PartyCreateSearchMap.tsx | 0 src/hooks/usePlaceSearch.ts | 60 ++++++ src/hooks/useSearchPlace.ts | 84 -------- types/map.ts | 10 +- types/party/place/map.ts | 1 + types/partyData.d.ts | 51 ++--- types/profile/user/UserProfileResponse.ts | 23 ++- 16 files changed, 494 insertions(+), 591 deletions(-) create mode 100644 src/components/party/create/MapSearch.tsx delete mode 100644 src/components/party/create/SearchMap.tsx delete mode 100644 src/contexts/PartyCreateSearchMap.tsx create mode 100644 src/hooks/usePlaceSearch.ts delete mode 100644 src/hooks/useSearchPlace.ts create mode 100644 types/party/place/map.ts diff --git a/pages/party/create.tsx b/pages/party/create.tsx index a51c9a5..7673ea0 100644 --- a/pages/party/create.tsx +++ b/pages/party/create.tsx @@ -2,12 +2,10 @@ import { GetServerSideProps, NextPage } from 'next'; import { ChangeEvent, useEffect } from 'react'; import styled from '@emotion/styled'; import Create from '@components/party/create/Create'; -import SearchMap from '@components/party/create/SearchMap'; -import useSearchPlace from '@hooks/useSearchPlace'; import { DefaultHeader } from '@components/common/DefaultHeader'; import { postParty } from 'src/api/postParty'; import * as yup from 'yup'; -import { SubmitHandler, useForm } from 'react-hook-form'; +import { SubmitHandler, useForm, FormProvider } from 'react-hook-form'; import { yupResolver } from '@hookform/resolvers/yup'; import router from 'next/router'; import { useMutation, useQueryClient } from '@tanstack/react-query'; @@ -37,6 +35,9 @@ export const partySchema = yup.object({ menu: yup.string().required(), thumbnail: yup.string(), status: yup.string(), + partyPlaceName: yup.string().required(), + latitude: yup.number().required(), + longitude: yup.number().required(), }); interface CreatePageProviderProps { @@ -55,59 +56,37 @@ export const CreatePage = () => { mutationFn: postUploadImage, }); - const { marker, keyword, resultList, reset, handleChangeSearchBox, handleClickPlace } = - useSearchPlace(); - - const { - handleSubmit, - register, - formState: { isValid }, - setValue, - getValues, - } = useForm({ + const methods = useForm({ resolver: yupResolver(partySchema), mode: 'onSubmit', defaultValues: { thumbnail: '/images/default_thumbnail.jpg', + totalParticipant: 2, + age: 'ALL', + category: 'KOREAN', + gender: 'ALL', }, }); - const onSubmitPartyForm: SubmitHandler = (formData: PartyForm) => { - if (!marker || !marker.position) return; - - postPartyCreate( - { - ...formData, - partyPlaceName: marker.content, - latitude: marker.position.lat, - longitude: marker.position.lng, - }, - { - onSuccess: async ({ data }) => { - if (data) { - await queryClient.invalidateQueries({ - queryKey: [ - API_GET_MAIN_PAGE, - { latitude: position.coords.x, longitude: position.coords.y }, - ], - }); - - queryClient.invalidateQueries({ - queryKey: [API_GET_CHAT_ROOMS_KEY], - }); - - router.replace(`/party/${data.partyId}`); - } - }, + const onSubmitPartyForm: SubmitHandler = (formData: PartyForm) => + postPartyCreate(formData, { + onSuccess: async ({ data }) => { + if (data) { + await queryClient.invalidateQueries({ + queryKey: [ + API_GET_MAIN_PAGE, + { latitude: position.coords.x, longitude: position.coords.y }, + ], + }); + + await queryClient.invalidateQueries({ + queryKey: [API_GET_CHAT_ROOMS_KEY], + }); + + router.replace(`/party/${data.partyId}`); + } }, - ); - }; - - const rightHeaderArea = ( - - ); + }); const handleChangeThumbnail = (e: ChangeEvent) => { e.preventDefault(); @@ -117,31 +96,26 @@ export const CreatePage = () => { postImage(files[0], { onSuccess({ imgUrl }) { if (imgUrl) { - setValue('thumbnail', imgUrl); + methods.setValue('thumbnail', imgUrl); } }, }); } }; + const rightHeaderArea = ( + + ); + return ( -
- - - - - + +
+ + + +
); }; diff --git a/pages/party/edit/[id].tsx b/pages/party/edit/[id].tsx index f68bfc5..1654a29 100644 --- a/pages/party/edit/[id].tsx +++ b/pages/party/edit/[id].tsx @@ -1,13 +1,11 @@ import { NextPage } from 'next'; -import { ChangeEvent, useEffect } from 'react'; +import { ChangeEvent } from 'react'; import styled from '@emotion/styled'; import Create from '@components/party/create/Create'; -import SearchMap from '@components/party/create/SearchMap'; -import useSearchPlace from '@hooks/useSearchPlace'; import { DefaultHeader } from '@components/common/DefaultHeader'; import { patchParty } from 'src/api/patchParty'; import getPartyDetail, { API_GET_PARTY_DETAIL_KEY } from 'src/api/getPartyDetail'; -import { SubmitHandler, useForm } from 'react-hook-form'; +import { FormProvider, SubmitHandler, useForm } from 'react-hook-form'; import { yupResolver } from '@hookform/resolvers/yup'; import { useRouter } from 'next/router'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; @@ -16,6 +14,7 @@ import { partySchema } from '../create'; import { PositionSate } from 'src/recoil-states/positionStates'; import { useRecoilValue } from 'recoil'; import { API_GET_MAIN_PAGE } from 'src/api/getPartyMainPage'; +import getProfile, { API_GET_PROFILE_KEY } from 'src/api/getProfile'; const Form = styled.form` display: flex; @@ -31,12 +30,16 @@ const CreatePage: NextPage = () => { const router = useRouter(); const { id } = router.query as { id: string }; const position = useRecoilValue(PositionSate); - const userId = '0'; // 임시 + + const { data: profileData } = useQuery({ + queryKey: [API_GET_PROFILE_KEY], + queryFn: () => getProfile(), + }); const { data } = useQuery({ queryKey: [API_GET_PARTY_DETAIL_KEY, { id }], - queryFn: () => getPartyDetail({ id, userId }), - enabled: !!id, + queryFn: () => getPartyDetail({ id, userId: String(profileData?.userId) }), + enabled: !!id && !!profileData, }); const { mutate: updateParty } = useMutation({ @@ -47,41 +50,28 @@ const CreatePage: NextPage = () => { mutationFn: postUploadImage, }); - const { marker, keyword, resultList, handleChangeSearchBox, handleClickPlace, setPlace } = - useSearchPlace(); - - const { - handleSubmit, - register, - formState: { isValid }, - setValue, - getValues, - reset, - } = useForm({ + const methods = useForm({ resolver: yupResolver(partySchema), mode: 'onSubmit', - defaultValues: { + defaultValues: data || { thumbnail: '/images/default_thumbnail.jpg', + totalParticipant: 2, + age: 'ALL', + category: 'KOREAN', + gender: 'ALL', }, }); - const onSubmitPartyForm: SubmitHandler = (formData: PartyForm) => { - if (!marker || !marker.position) return; - + const onSubmitPartyForm: SubmitHandler = (formData: PartyForm) => updateParty( { id, - params: { - ...formData, - partyPlaceName: marker.content, - latitude: marker.position.lat, - longitude: marker.position.lng, - }, + params: formData, }, { onSuccess: async () => { await queryClient.invalidateQueries({ - queryKey: [API_GET_PARTY_DETAIL_KEY, { id, userId }], + queryKey: [API_GET_PARTY_DETAIL_KEY, { id, userId: profileData?.userId }], }); await queryClient.invalidateQueries({ queryKey: [ @@ -89,12 +79,10 @@ const CreatePage: NextPage = () => { { latitude: position.coords.x, longitude: position.coords.y }, ], }); - router.replace(`/party/${id}`); }, }, ); - }; const handleChangeThumbnail = (e: ChangeEvent) => { e.preventDefault(); @@ -104,26 +92,15 @@ const CreatePage: NextPage = () => { uploadImage(files[0], { onSuccess({ imgUrl }) { if (imgUrl) { - setValue('thumbnail', imgUrl); + methods.setValue('thumbnail', imgUrl); } }, }); } }; - useEffect(() => { - if (!data) return; - - setPlace({ - lat: data?.latitude, - lng: data?.longitude, - placeName: data?.partyPlaceName, - }); - setValue('thumbnail', data?.thumbnail); - }, [data, setPlace, setValue]); - const rightHeaderArea = ( - ); @@ -131,25 +108,16 @@ const CreatePage: NextPage = () => { if (!data) return <>; return ( -
- - - + + + - - + + ); }; diff --git a/src/api/getPartyDetail.ts b/src/api/getPartyDetail.ts index d84ce4c..1039037 100644 --- a/src/api/getPartyDetail.ts +++ b/src/api/getPartyDetail.ts @@ -1,22 +1,22 @@ -import variableAssignMent from "@utils/variableAssignment"; -import defaultRequest from "src/lib/axios/defaultRequest"; -import { PartyDetailResponse } from "types/party/detail/PartyDetailResponse"; +import variableAssignMent from '@utils/variableAssignment'; +import defaultRequest from 'src/lib/axios/defaultRequest'; +import { PartyDetailResponse } from 'types/party/detail/PartyDetailResponse'; interface GetPartyDetailParameter { - id: string; - userId: string; + id: string; + userId: string; } -export const API_GET_PARTY_DETAIL_KEY = "/api/party/{{id}}?userId={{userId}}"; +export const API_GET_PARTY_DETAIL_KEY = '/api/party/{{id}}?userId={{userId}}'; const getPartyDetail = async ({ - id, - userId, + id, + userId, }: GetPartyDetailParameter): Promise => { - const { data } = await defaultRequest.get( - variableAssignMent(API_GET_PARTY_DETAIL_KEY, { id, userId }) - ); - return data; + const { data } = await defaultRequest.get( + variableAssignMent(API_GET_PARTY_DETAIL_KEY, { id, userId }), + ); + return data; }; export default getPartyDetail; diff --git a/src/components/common/Map.tsx b/src/components/common/Map.tsx index ee90004..9c73b26 100644 --- a/src/components/common/Map.tsx +++ b/src/components/common/Map.tsx @@ -1,21 +1,17 @@ -import { ReactNode, memo } from "react"; -import { Map } from "react-kakao-maps-sdk"; -import { MapCoordinatet } from "types/map"; +import { ReactNode, memo } from 'react'; +import { Map } from 'react-kakao-maps-sdk'; +import { MapCoordinatet } from 'types/map'; interface KakaoMapProps { - center: MapCoordinatet; - children?: ReactNode; - zoom?: number; + center: MapCoordinatet; + children?: ReactNode; + zoom?: number; } const KakaoMap = ({ center, zoom = 3, children }: KakaoMapProps) => ( - - {children} - + + {children} + ); export default memo(KakaoMap); diff --git a/src/components/party/create/Create.tsx b/src/components/party/create/Create.tsx index f8b4fab..d175d5d 100644 --- a/src/components/party/create/Create.tsx +++ b/src/components/party/create/Create.tsx @@ -1,133 +1,145 @@ -import styled from "@emotion/styled"; -import TextInput from "@components/common/TextInput"; -import Thumbnail from "./Thumbnail"; -import { ChangeEventHandler, FC, PropsWithChildren } from "react"; -import { UseFormGetValues, UseFormRegister } from "react-hook-form"; -import SelectContent from "./SelectContent"; +import styled from '@emotion/styled'; +import TextInput from '@components/common/TextInput'; +import Thumbnail from './Thumbnail'; +import { ChangeEventHandler, FC, PropsWithChildren } from 'react'; +import { useFormContext } from 'react-hook-form'; +import SelectContent from './SelectContent'; import { - PARTY_AGE_LABEL, - PARTY_CATEGORY_LABEL, - PARTY_GENDER_LABEL, - PARTY_STATUS_LABEL, - PARTY_TOTAL_LABEL, -} from "src/constants/options"; -import { PartyDetailResponse } from "types/party/detail/PartyDetailResponse"; -import dayjs from "dayjs"; + PARTY_AGE_LABEL, + PARTY_CATEGORY_LABEL, + PARTY_GENDER_LABEL, + PARTY_STATUS_LABEL, + PARTY_TOTAL_LABEL, +} from 'src/constants/options'; +import { PartyDetailResponse } from 'types/party/detail/PartyDetailResponse'; +import dayjs from 'dayjs'; +import MapSearch from './MapSearch'; const Wrapper = styled.div` - height: 100%; - overflow-y: auto; + height: 100%; + overflow-y: auto; +`; + +const Section = styled.section` + margin-bottom: 10%; `; const TextArea = styled.textarea` - display: block; - padding: 10px 14px; - width: 100%; - outline: none; - border: none; - resize: none; - background-color: #f9f9f9; - border-top: 1px solid #ddd; + display: block; + padding: 10px 14px; + width: 100%; + outline: none; + border: none; + resize: none; + background-color: #f9f9f9; + border: 1px solid #ddd; `; const Contents = styled.div` - display: flex; - align-items: center; - margin: 0 1rem; - height: 60px; + display: flex; + align-items: center; + margin: 0 1rem; + height: 60px; - & input { - width: 200px; - } + & input { + width: 200px; + } `; const Label = styled.h5` - min-width: 80px; + min-width: 80px; `; interface CreateProps { - partyId?: number; - defaultData?: PartyDetailResponse; - onChangeThumbnail: ChangeEventHandler; - register: UseFormRegister; - getValues: UseFormGetValues; + partyId?: number; + defaultData?: PartyDetailResponse; + onChangeThumbnail: ChangeEventHandler; } +// textinput과 ui수정 필요 select -> checkbox const Create: FC> = ({ - partyId, - defaultData, - children, - register, - getValues, - onChangeThumbnail, -}) => ( - - -