diff --git a/next.config.js b/next.config.js index c8342b9..cd1f01f 100644 --- a/next.config.js +++ b/next.config.js @@ -10,6 +10,7 @@ const nextConfig = { MATITTING_HOST_URL: process.env.MATITTING_HOST_URL, NAVER_CLIENT_ID: process.env.NAVER_CLIENT_ID, //ClientID SNS_CALLBACK_URL: process.env.SNS_CALLBACK_URL, // Callback URL + WEB_SOCKET_URL: process.env.WEB_SOCKET_URL, }, images: { domains: [ diff --git a/pages/chat/[id].tsx b/pages/chat/[id].tsx index cff969b..3b6a6e0 100644 --- a/pages/chat/[id].tsx +++ b/pages/chat/[id].tsx @@ -1,51 +1,27 @@ -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", -}); - -const ChattingRoom = () => { - const [isOpenUserList, setIsOpenUserList] = useState(false); - - const handleCloseUserList = (e: MouseEvent) => { - e.stopPropagation(); - setIsOpenUserList(false); - }; - - const handleOpenUserList = (e: MouseEvent) => { - e.stopPropagation(); - setIsOpenUserList(true); - }; - - return ( - - - - - - - - ); -}; - -ChattingRoom.getLayout = (page: ReactElement) => { - return <>{page}; +import { GetServerSideProps } from 'next'; +import QuerySuspenseErrorBoundary from '@components/hoc/QuerySuspenseErrorBoundary'; +import ChatRoom from '@components/chat/room/ChatRoom'; +import ProfileLoading from '@components/profile/ProfileLoading'; +import ChatProvider from '@contexts/ChatProvider'; + +interface ChatRoomPageProps { + roomId: number; +} + +const ChatRoomPage = ({ roomId }: ChatRoomPageProps) => ( + + }> + + + +); + +export default ChatRoomPage; + +export const getServerSideProps: GetServerSideProps = async ({ params }) => { + const { id: roomId } = params as { id: string }; + + return { + props: { roomId: Number(roomId) }, + }; }; - -export default ChattingRoom; diff --git a/pages/chat/index.tsx b/pages/chat/index.tsx new file mode 100644 index 0000000..0d2ebe7 --- /dev/null +++ b/pages/chat/index.tsx @@ -0,0 +1,34 @@ +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 ChatList from '@components/chat/list/ChatList'; + +const Wrapper = styled.div` + margin: 0 auto; + padding: 45px 0 75px 0; + height: 100%; + min-height: calc(100vh); + max-width: 760px; + min-width: 320px; +`; + +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/pages/party/create.tsx b/pages/party/create.tsx index 721ece8..b325fa2 100644 --- a/pages/party/create.tsx +++ b/pages/party/create.tsx @@ -1,13 +1,11 @@ -import { NextPage } from 'next'; -import { ChangeEvent } from 'react'; +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 Create from '@components/party/Create'; 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'; @@ -15,30 +13,43 @@ 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 { API_GET_CHAT_ROOMS } from 'src/api/getChatRooms'; +import dayjs from 'dayjs'; const Form = styled.form` display: flex; flex-direction: column; justify-content: space-between; + max-width: 760px; + min-width: 320px; height: 100%; min-height: calc(100vh); - padding: 45px 0 75px 0; + margin: 0 auto; + padding: 60px 0 76px 0; `; export const partySchema = yup.object({ partyTitle: yup.string().min(2, '2자 이상 입력해주세요').required(), - partyContent: yup.string().required(), + partyContent: yup.string().max(50, '50자 이하로 작성해주세요').required(), partyTime: yup.string().required(), gender: yup.string().required(), category: yup.string().required(), + // age: yup.mixed().required(), age: yup.string().required(), totalParticipant: yup.number().required(), menu: yup.string().required(), thumbnail: yup.string(), status: yup.string(), + partyPlaceName: yup.string().required(), + latitude: yup.number().required(), + longitude: yup.number().required(), }); -const CreatePage: NextPage = () => { +interface CreatePageProviderProps { + loginMessage: string; +} + +export const CreatePage = () => { const queryClient = useQueryClient(); const position = useRecoilValue(PositionSate); @@ -50,55 +61,37 @@ const CreatePage: NextPage = () => { mutationFn: postUploadImage, }); - const { marker, setMap, 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', + partyTime: dayjs().format('YYYY-MM-DDTHH:mm'), }, }); - const onSubmitPartyForm: SubmitHandler = (formData: PartyForm) => { - if (!marker || !marker.position) return; + 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 }, + ], + }); - 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}`); - } - }, - }, - ); - }; + await queryClient.invalidateQueries({ + queryKey: [API_GET_CHAT_ROOMS], + }); - const rightHeaderArea = ( - - ); + router.replace(`/party/${data.partyId}`); + } + }, + }); const handleChangeThumbnail = (e: ChangeEvent) => { e.preventDefault(); @@ -108,7 +101,7 @@ const CreatePage: NextPage = () => { postImage(files[0], { onSuccess({ imgUrl }) { if (imgUrl) { - setValue('thumbnail', imgUrl); + methods.setValue('thumbnail', imgUrl); } }, }); @@ -116,25 +109,39 @@ const CreatePage: NextPage = () => { }; return ( -
- - - - - + +
+ + + +
); }; -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; + + return { + props: { + loginMessage: refreshToken ? '' : '로그인 필요', + }, + }; +}; diff --git a/pages/party/edit/[id].tsx b/pages/party/edit/[id].tsx index d173d05..c9875b7 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 styled from '@emotion/styled'; -import Create from '@components/party/create/Create'; -import SearchMap from '@components/party/create/SearchMap'; -import useSearchPlace from '@hooks/useSearchPlace'; +import Create from '@components/party/Create'; 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,12 +14,16 @@ 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'; +import { HeaderBackButton } from '@components/common/HeaderBackButton'; const Form = styled.form` display: flex; flex-direction: column; justify-content: space-between; - height: calc(100vh - 72px - 45px); + height: 100%; + min-height: calc(100vh); + padding: 45px 1rem 0 1rem; `; const CreatePage: NextPage = () => { @@ -29,12 +31,16 @@ const CreatePage: NextPage = () => { const router = useRouter(); const { id } = router.query as { id: string }; const position = useRecoilValue(PositionSate); - const userId = '0'; // 임시 - const { data } = useQuery({ + const { data: profileData } = useQuery({ + queryKey: [API_GET_PROFILE_KEY], + queryFn: () => getProfile(), + }); + + const { data, isFetched } = 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({ @@ -45,48 +51,21 @@ const CreatePage: NextPage = () => { mutationFn: postUploadImage, }); - const { - setMap, - 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: { - thumbnail: '/images/default_thumbnail.jpg', - }, }); - 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: [ @@ -94,12 +73,10 @@ const CreatePage: NextPage = () => { { latitude: position.coords.x, longitude: position.coords.y }, ], }); - router.replace(`/party/${id}`); }, }, ); - }; const handleChangeThumbnail = (e: ChangeEvent) => { e.preventDefault(); @@ -109,7 +86,7 @@ const CreatePage: NextPage = () => { uploadImage(files[0], { onSuccess({ imgUrl }) { if (imgUrl) { - setValue('thumbnail', imgUrl); + methods.setValue('thumbnail', imgUrl); } }, }); @@ -117,45 +94,23 @@ const CreatePage: NextPage = () => { }; 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 ( -
- - - - - + // useForm에서는 초기값이 비동기적으로 적용이 되지 않아 따로 적용 + if (isFetched) { + methods.reset({ + ...data, + }); + } + }, [data, isFetched, methods]); + + return data?.partyTitle ? ( + +
+ } /> + + +
+ ) : ( + <> ); }; diff --git a/public/images/profile/profile.webp b/public/images/profile/profile.webp new file mode 100644 index 0000000..759b833 Binary files /dev/null and b/public/images/profile/profile.webp differ diff --git a/public/images/profile/profile_fill.webp b/public/images/profile/profile_fill.webp new file mode 100644 index 0000000..5d764ed Binary files /dev/null and b/public/images/profile/profile_fill.webp differ diff --git a/src/api/deleteChatUser.ts b/src/api/deleteChatUser.ts new file mode 100644 index 0000000..3e826a7 --- /dev/null +++ b/src/api/deleteChatUser.ts @@ -0,0 +1,12 @@ +import defaultRequest from 'src/lib/axios/defaultRequest'; + +interface ChatUserParams { + roomId: string; + targetChatUserId: number; +} + +const deletePartyUser = async ({ roomId, targetChatUserId }: ChatUserParams) => { + await defaultRequest.delete(`/api/chat/${roomId}`, { data: { targetChatUserId } }); +}; + +export default deletePartyUser; diff --git a/src/api/getChatMessage.ts b/src/api/getChatMessage.ts new file mode 100644 index 0000000..e643fcc --- /dev/null +++ b/src/api/getChatMessage.ts @@ -0,0 +1,19 @@ +import defaultRequest from 'src/lib/axios/defaultRequest'; +import { ChatMessagesType, InfinitePaginationChatDataType } from 'types/chat/chat'; + +interface ChatMessageParams { + roomId: number; + page: number; +} + +export const API_GET_CHAT_MESSAGE_KEY = '/api/chat/{{roomId}}?page={{page}}'; + +const getChatMessage = async ({ roomId, page }: ChatMessageParams) => { + const { data } = await defaultRequest.get< + InfinitePaginationChatDataType<'responseChatDtoList', ChatMessagesType> + >(`/api/chat/${roomId}`, { params: { page } }); + + return data; +}; + +export default getChatMessage; diff --git a/src/api/getChatRoomInfo.ts b/src/api/getChatRoomInfo.ts new file mode 100644 index 0000000..d43227c --- /dev/null +++ b/src/api/getChatRoomInfo.ts @@ -0,0 +1,18 @@ +import defaultRequest from 'src/lib/axios/defaultRequest'; +import { ChatRoomInfoResponse } from 'types/chat/chatRooms'; + +interface ChatRoomInfoParams { + chatRoomId: number; +} + +export const API_GET_CHAT_ROOM_INFO = '/api/chat-rooms'; + +const getChatRoomInfo = async ({ + chatRoomId, +}: ChatRoomInfoParams): Promise => { + const { data } = await defaultRequest.get(`${API_GET_CHAT_ROOM_INFO}/${chatRoomId}`); + + return data; +}; + +export default getChatRoomInfo; diff --git a/src/api/getChatRooms.ts b/src/api/getChatRooms.ts new file mode 100644 index 0000000..afd9369 --- /dev/null +++ b/src/api/getChatRooms.ts @@ -0,0 +1,11 @@ +import defaultRequest from 'src/lib/axios/defaultRequest'; +import { ChatRoomsResponse } from 'types/chat/chatRooms'; +export const API_GET_CHAT_ROOMS = '/api/chat-rooms'; + +const getChatRooms = async (params: number): Promise => { + const { data } = await defaultRequest.get(API_GET_CHAT_ROOMS, { params }); + + return data; +}; + +export default getChatRooms; 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/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/BottomInputGroup.tsx b/src/components/chat/BottomInputGroup.tsx deleted file mode 100644 index 81ab9e8..0000000 --- a/src/components/chat/BottomInputGroup.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import { ChangeEvent, MouseEvent, ReactElement } from "react"; -import TextInput from "@components/common/TextInput"; -import styled from "@emotion/styled"; - -const Wrapper = styled.div({ - display: "flex", - justifyContent: "space-between", - gap: "10px", - padding: "1rem 2rem", - backgroundColor: "#ddd", - - "& input": { - height: "50px", - }, -}); - -const ImageUploadButton = styled.label({ - display: "flex", - justifyContent: "center", - alignItems: "center", - width: "50px", - border: "none", - borderRadius: "10px", - backgroundColor: "#efebec", - cursor: "pointer", -}); - -const SubmitButton = styled.button({ - width: "50px", - border: "none", - borderRadius: "10px", - backgroundColor: "#efebec", -}); - -const BottomInputGroup = () => { - const handleClickSubmit = (e: MouseEvent) => {}; - - const handleChangeImageFile = (e: ChangeEvent) => { - e.preventDefault(); - }; - - return ( - - - - + - - - 전송 - - ); -}; - -BottomInputGroup.getLayout = (page: ReactElement) => { - return <>{page}; -}; - -export default BottomInputGroup; 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/ImageCard.tsx b/src/components/chat/ImageCard.tsx new file mode 100644 index 0000000..057ab9b --- /dev/null +++ b/src/components/chat/ImageCard.tsx @@ -0,0 +1,86 @@ +import { css } from '@emotion/react'; +import styled from '@emotion/styled'; +import Image from 'next/image'; +import { NewColor } from 'styles/Color'; + +type ImageType = 'chatUserListprofile' | 'thumbnail' | 'chatProfile'; + +interface ImageCardProps { + src: string; + alt: string; + imageType: ImageType; +} + +const ImageBox = styled.div<{ imageType: ImageType; isURL: boolean }>` + position: relative; + display: flex; + align-items: center; + justify-content: center; + overflow: hidden; + margin-right: 10px; + height: auto; + aspect-ratio: 1/1; + + ${({ imageType, isURL }) => + imageType === 'chatProfile' + ? chatProfileImageStyle + : imageType === 'thumbnail' + ? thumbnailImageStyle + : imageType === 'chatUserListprofile' && isURL + ? chatUserListUserProfileImageStyle + : imageType === 'chatUserListprofile' && !isURL + ? chatUserListDefaultProfileImageStyle + : null} +`; + +const chatProfileImageStyle = css` + width: 30px; + border-radius: 50%; + background-color: #fff; +`; + +const thumbnailImageStyle = css` + width: 45px; + border-radius: 10px; + border: 1px solid ${NewColor.border}; +`; + +const chatUserListProfileImageStyle = css` + position: relative; + width: 30px; + border-radius: 50%; +`; + +const chatUserListUserProfileImageStyle = css` + ${chatUserListProfileImageStyle} + border: 1px solid ${NewColor.border}; +`; + +const chatUserListDefaultProfileImageStyle = css` + ${chatUserListProfileImageStyle} + border: 1px solid ${NewColor.primary}; +`; + +const ImageCard = ({ imageType, src, alt }: ImageCardProps) => { + let DEFAULT_IMAGE_PATH = ''; + + switch (imageType) { + case 'chatProfile': + DEFAULT_IMAGE_PATH = '/images/profile/profile.webp'; + break; + case 'chatUserListprofile': + DEFAULT_IMAGE_PATH = '/images/profile/profile_fill.webp'; + break; + case 'thumbnail': + DEFAULT_IMAGE_PATH = '/images/default_thumbnail.jpg'; + break; + } + + return ( + + {alt} + + ); +}; + +export default ImageCard; 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/RecentTime.tsx b/src/components/chat/RecentTime.tsx new file mode 100644 index 0000000..44c1369 --- /dev/null +++ b/src/components/chat/RecentTime.tsx @@ -0,0 +1,29 @@ +import styled from '@emotion/styled'; +import dayjs from 'dayjs'; +import { NewColor } from 'styles/Color'; + +interface RecentTimeProps { + time: string; +} + +const Time = styled.p<{ color?: string; fontSize?: string }>` + color: ${({ color }) => (color ? color : NewColor.text_secondary)}; + font-size: ${({ fontSize }) => (fontSize ? fontSize : '12px')}; +`; + +const RecentTime = ({ time, ...props }: RecentTimeProps) => { + return time ? : null; +}; + +export default RecentTime; + +export const displayTime = (time: string) => { + 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.DD'); +}; diff --git a/src/components/chat/list/ChatItem.tsx b/src/components/chat/list/ChatItem.tsx new file mode 100644 index 0000000..5250c46 --- /dev/null +++ b/src/components/chat/list/ChatItem.tsx @@ -0,0 +1,78 @@ +import styled from '@emotion/styled'; +import router from 'next/router'; +import { NewColor } from 'styles/Color'; +import { ChatRoomList } from 'types/chat/chatRooms'; +import ImageCard from '../ImageCard'; +import RecentTime from '../RecentTime'; + +const Wrapper = styled.li` + display: flex; + justify-content: space-between; + align-items: center; + padding: 10px 0; + border-radius: 15px; +`; + +const DetailBox = 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-bottom: 5px; + font-size: 12px; + font-weight: bold; +`; + +const Message = styled.p` + margin: 0; + color: ${NewColor.text_secondary}; + font-size: 14px; + max-width: 50vw; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +`; + +const NoList = styled.div` + text-align: center; + padding: 10% 0; +`; + +interface ChatRoomItemProps { + list?: ChatRoomList[]; + noListText: string; +} + +const ChatRoomItem = ({ list, noListText }: ChatRoomItemProps) => { + return list?.length ? ( + list?.map((item, index) => { + const { roomId, title, lastMessageTime, lastMessage, thumbnail } = item; + + return ( + router.push(`/chat/${roomId}`)}> + + + + {title} + {lastMessage} + + + + + ); + }) + ) : ( + {noListText} + ); +}; + +export default ChatRoomItem; diff --git a/src/components/chat/list/ChatList.tsx b/src/components/chat/list/ChatList.tsx new file mode 100644 index 0000000..62c2e1b --- /dev/null +++ b/src/components/chat/list/ChatList.tsx @@ -0,0 +1,143 @@ +import styled from '@emotion/styled'; +import dayjs from 'dayjs'; +import { NextPage } from 'next'; +import { useQueryClient, useSuspenseInfiniteQuery } from '@tanstack/react-query'; +import getChatRooms, { API_GET_CHAT_ROOMS } from 'src/api/getChatRooms'; +import { ObserverTrigger } from '@components/hoc/ObserverTrigger'; +import getSearchChatRooms, { API_GET_SEARCH_CHAT_ROOMS_KEY } from 'src/api/getSearchChatRooms'; +import { ChangeEvent, useState } from 'react'; +import ChatRoomItem from './ChatItem'; +import { useForm } from 'react-hook-form'; +import SearchIcon from '@mui/icons-material/Search'; +import CloseIcon from '@mui/icons-material/Close'; +import { NewColor } from 'styles/Color'; + +const Wrapper = styled.div` + padding: 2rem; +`; + +const SearchhForm = styled.form` + position: relative; + display: flex; + justify-content: space-between; + padding: 5px 15px; + height: 34px; + border-radius: 25px; + border: 1px solid ${NewColor.border}; +`; + +const SearchInput = styled.input` + border: none; + outline: none; + height: 100%; + width: 100%; + color: ${NewColor.text_secondary}; +`; + +const SearchButton = styled.button` + color: ${NewColor.text_secondary}; + cursor: pointer; +`; + +const CloseButton = styled(CloseIcon)` + color: ${NewColor.text_secondary}; + cursor: pointer; +`; + +const ChatRoomList = styled.ul` + padding: 1rem 0; + margin: 0 auto; + list-style: none; + height: 100%; +`; + +const ChatList: NextPage = () => { + const queryClient = useQueryClient(); + const [isSearch, setIsSearch] = useState(false); + const { register, getValues, handleSubmit, reset, formState } = useForm<{ + searchText: string; + }>(); + + const { fetchNextPage, hasNextPage, data } = useSuspenseInfiniteQuery({ + queryKey: [API_GET_CHAT_ROOMS], + queryFn: ({ pageParam = 0 }) => getChatRooms(pageParam), + initialPageParam: 0, + getNextPageParam: (lastPage) => { + if (lastPage.pageInfo.hasNext) return lastPage.pageInfo.page + 1; + }, + }); + + const { data: searchData } = useSuspenseInfiniteQuery({ + queryKey: [API_GET_SEARCH_CHAT_ROOMS_KEY, getValues('searchText')], + queryFn: ({ pageParam = 0 }) => + formState.isValid + ? getSearchChatRooms({ title: getValues('searchText'), page: pageParam }) + : null, + initialPageParam: 0, + getNextPageParam: (lastPage) => + lastPage?.pageInfo.hasNext ? lastPage?.pageInfo.page + 1 : null, + }); + + const close = () => { + setIsSearch(false); + reset(); + }; + + const handleChangeSearch = (e: ChangeEvent) => { + if (!e.target.value.length) close(); + }; + + const onSearch = async () => { + setIsSearch(true); + + await queryClient.invalidateQueries({ + queryKey: [API_GET_SEARCH_CHAT_ROOMS_KEY, getValues('searchText')], + }); + }; + + const chatRooms = data.pages.map((page) => page.responseChatRoomDtoList).flat(); + const searchRooms = searchData.pages + .map((page) => (page ? page?.responseChatRoomDtoList : [])) + .flat(); + const onObserve = () => hasNextPage && fetchNextPage(); + + return ( + + + + {isSearch ? ( + + ) : ( + + + + )} + + + + + + + + ); +}; + +export default ChatList; + +export const displayTime = (time: string) => { + 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.DD'); +}; diff --git a/src/components/chat/room/ChatForm.tsx b/src/components/chat/room/ChatForm.tsx new file mode 100644 index 0000000..c07adfa --- /dev/null +++ b/src/components/chat/room/ChatForm.tsx @@ -0,0 +1,48 @@ +import styled from '@emotion/styled'; +import { FieldValues, SubmitHandler, useFormContext } from 'react-hook-form'; +import { NewColor } from 'styles/Color'; + +const Form = styled.form` + display: flex; + justify-content: space-between; + gap: 10px; + padding: 10px 1rem; + background-color: ${NewColor.primary}; +`; + +const SubmitButton = styled.button` + width: 50px; + border: none; + color: #fff; +`; + +const TextArea = styled.textarea` + display: flex; + align-items: center; + padding: 5px 10px; + width: 100%; + font-size: 14px; + border-radius: 5px; + color: ${NewColor.text_primary}; + line-height: 1.5; + outline: none; + border: none; + resize: none; +`; + +interface ChahFormProps { + onSubmit: SubmitHandler; +} + +const ChatForm = ({ onSubmit }: ChahFormProps) => { + const { handleSubmit, register } = useFormContext(); + + return ( +
+