diff --git a/Components/Chat/ChatHeader.tsx b/Components/Chat/ChatHeader.tsx new file mode 100644 index 0000000..ba3be08 --- /dev/null +++ b/Components/Chat/ChatHeader.tsx @@ -0,0 +1,51 @@ +import { getCookie } from '@/Components/Login/Cookie'; +import { useRouter } from 'next/navigation'; +import { Socket } from 'socket.io-client'; +import { DefaultEventsMap } from 'socket.io/dist/typed-events'; + +const ChatHeader = ({ + socket, + chatId, + chatName, + chatUsers, +}: { + socket: Socket; + chatId: string; + chatName: string; + chatUsers: number; +}) => { + const router = useRouter(); + + const accessToken = getCookie('accessToken'); + + const handleBackChat = () => { + socket.disconnect(); + router.back(); + }; + + const handleLeaveChat = async () => { + await fetch('https://fastcampus-chat.net/chat/leave', { + method: 'PATCH', + headers: { + 'content-type': 'application/json', + serverId: process.env.NEXT_PUBLIC_SERVER_ID as string, + Authorization: `Bearer ${accessToken}`, + }, + body: JSON.stringify({ chatId }), + }); + + socket.disconnect(); + router.back(); + }; + + return ( + <> + + {chatName} + {chatUsers} + + + ); +}; + +export default ChatHeader; diff --git a/Components/Chat/ChatRoom.tsx b/Components/Chat/ChatRoom.tsx new file mode 100644 index 0000000..d65bac2 --- /dev/null +++ b/Components/Chat/ChatRoom.tsx @@ -0,0 +1,147 @@ +'use client'; + +import React, { useEffect, useState } from 'react'; +import { Socket } from 'socket.io-client'; +import { Chat, Message, chatUsersObject } from '@/types'; +import { DefaultEventsMap } from 'socket.io/dist/typed-events'; +import ChatHeader from '@/Components/Chat/ChatHeader'; +import RenderChats from '@/Components/Chat/RenderChats'; + +const ChatRoom = ({ + socket, + chatId, + privateValue, + accessToken, +}: { + socket: Socket; + chatId: string; + privateValue: string; + accessToken: string; +}) => { + const initChatusers = { + id: '', + name: '', + users: [], // 속한 유저 정보 + isPrivate: JSON.parse(privateValue), + latestMessage: null, + updatedAt: new Date(), + }; + + const [messages, setMessages] = useState([]); + const [newMessage, setNewMessage] = useState(''); + const [chatUsers, setChatUsers] = useState(initChatusers); + + // 채팅 참여자 fetch, socket 이벤트 등록 (1회 동작) + useEffect(() => { + const fetchChatUsers = async () => { + const res = await fetch( + `https://fastcampus-chat.net/chat/only?chatId=${chatId}`, + { + method: 'GET', + headers: { + 'content-type': 'application/json', + serverId: process.env.NEXT_PUBLIC_SERVER_ID as string, + Authorization: `Bearer ${accessToken}`, + }, + cache: 'no-cache', + }, + ); + const chatUsersObject: chatUsersObject = await res.json(); + const chatUsers: Chat = chatUsersObject.chat; + + setChatUsers(chatUsers); + }; + + fetchChatUsers(); + + socket.on('connect', () => { + socket.on('messages-to-client', (responseData) => { + setMessages(responseData.messages); + }); + + socket.on('message-to-client', (messageObject) => { + setMessages((prevMessages) => [...prevMessages, messageObject]); + }); + + socket.emit('fetch-messages'); + }); + + socket.on('connect_error', (error) => { + throw error; + }); + + return () => { + socket.off('message-to-client'); + socket.off('messages-to-client'); + }; + }, [accessToken, chatId, socket]); + + // 서버로 메시지 전송 + const sendMessage = () => { + if (newMessage.trim() !== '') { + socket.emit('message-to-server', newMessage); + setNewMessage(''); + } + }; + + // enter 입력 시 메시지 전송 + const handleKeyPress = (e: React.KeyboardEvent) => { + if (e.key === 'Enter') { + sendMessage(); + } + }; + + return ( + <> + + + {privateValue === 'true' ? ( + <> + {chatUsers.users.length === 2 || chatUsers.users.length === 1 ? ( + <> +

1대1 채팅방 입니다.

+ + + ) : ( + <> +

true 그룹 채팅방 입니다.

+ + + )} + + ) : ( + <> +

false 오픈 채팅방 입니다.

+ + + )} + + setNewMessage(e.target.value)} + onKeyPress={handleKeyPress} + /> + + + ); +}; + +export default ChatRoom; diff --git a/Components/Chat/Chats.tsx b/Components/Chat/Chats.tsx new file mode 100644 index 0000000..d9ec1ca --- /dev/null +++ b/Components/Chat/Chats.tsx @@ -0,0 +1,50 @@ +import { useRouter } from 'next/navigation'; +import Image from 'next/image'; +import { Message, User } from '@/types'; + +const Chats = ({ + message, + user, + myId, + useModal, +}: { + message: Message; + user: User; + myId: string; + useModal: boolean; +}) => { + const router = useRouter(); + + const openProfile = () => { + router.push(`/profile/${user.id}?isMyProfile=false`); + }; + + return ( + <> + {user.id === myId ? null : ( + <> + {useModal ? ( + User Picture + ) : ( + User Picture + )} + + )} +

{user.username}

+

{message.text}

+ + ); +}; + +export default Chats; diff --git a/Components/Chat/RenderChats.tsx b/Components/Chat/RenderChats.tsx new file mode 100644 index 0000000..ddea9e3 --- /dev/null +++ b/Components/Chat/RenderChats.tsx @@ -0,0 +1,72 @@ +import { getCookie } from '@/Components/Login/Cookie'; +import React, { useEffect, useRef } from 'react'; +import { Chat, Message } from '@/types'; +import Chats from '@/Components/Chat/Chats'; + +const RenderChats = ({ + messages, + chatUsers, + useModal, +}: { + messages: Message[]; + chatUsers: Chat; + useModal: boolean; +}) => { + const myId = getCookie('userId'); + const messageEndRef = useRef(null); + + // 새로운 메세지 전송시 하단 스크롤 + useEffect(() => { + const setTimeoutId = setTimeout(() => { + if (messageEndRef.current) { + messageEndRef.current.scrollIntoView({ behavior: 'smooth' }); + } + }, 500); + + return () => { + clearTimeout(setTimeoutId); + }; + }, [messages.length]); + + return ( + <> +
    + {messages.map((message, index) => { + const myUser = chatUsers.users.find( + (user) => user.id === message.userId, + ); + + if (myUser) { + return ( +
  • + {index === 0 || + new Date(message.createdAt).toDateString() !== + new Date(messages[index - 1].createdAt).toDateString() ? ( +
    + {new Date(message.createdAt).toDateString()} +
    + ) : null} + + +
  • + ); + } + })} +
    +
+ + ); +}; + +export default RenderChats; diff --git a/Components/Search/OpenChatModal.tsx b/Components/Search/OpenChatModal.tsx index 41c8190..8018726 100644 --- a/Components/Search/OpenChatModal.tsx +++ b/Components/Search/OpenChatModal.tsx @@ -21,10 +21,8 @@ const OpenChatModal = ({ modalChat }: { modalChat: Chat }) => { }, body: JSON.stringify({ chatId: process.env.NEXT_PUBLIC_CHAT_ID }), }); - // http://localhost:3000/chat/id 로 이동하기 - /// 이동 후 id에서 채팅 데이터 다시 fetch 하기 - // 에러 처리 필요 - router.push(`chat/${modalChat.id}?isPrivate=false`); + + router.push(`/chat/${modalChat.id}?isPrivate=false`); }; return ( @@ -67,12 +65,7 @@ const OpenChatModal = ({ modalChat }: { modalChat: Chat }) => {
- diff --git a/Components/Users/ProfileModal.tsx b/Components/Users/ProfileModal.tsx index 5a7292e..191805a 100644 --- a/Components/Users/ProfileModal.tsx +++ b/Components/Users/ProfileModal.tsx @@ -7,9 +7,29 @@ import { import { Chat, User } from '@/types'; import Image from 'next/image'; import { useSearchParams, useRouter } from 'next/navigation'; -import React from 'react'; +import React, { useRef, useState } from 'react'; import { getCookie } from '../Login/Cookie'; import { convertPictureURL } from '@/hooks/Common/users'; +import { Input } from '@material-tailwind/react'; +import axios from 'axios'; +import { editUser } from '@/app/users/users.utils'; + +type FetchImageProps = { + file: string; + id: string; + password: string; + name: string; +}; + +const fetchImage = async (params: FetchImageProps) => { + const data = await axios.post('/api/image/post', { + id: params.id, + name: params.name, + password: params.password, + file: params.file, + }); + return data.data; +}; const ProfileModal = ({ user, @@ -21,47 +41,145 @@ const ProfileModal = ({ const accessToken = getCookie('accessToken'); const router = useRouter(); const searchParams = useSearchParams(); + + const [isEdit, setIsEdit] = useState(false); + const [userInput, setUserInput] = useState({ + ...user, + picture: convertPictureURL(user.picture), + }); + const fileInputRef = useRef(null); + const isMyProfile = searchParams?.get('isMyProfile') === 'true'; + /* 1:1 채팅방 참여 */ const chattingParticipateHandler = async () => { if (existPrivateChat) { - const message = await participateChat(accessToken, existPrivateChat.id); - console.log('참여하기', message); - /* 채팅방으로 이동 시키기*/ + await participateChat(accessToken, existPrivateChat.id); router.push(`/chat/${existPrivateChat.id}?isPrivate=true`); } else { const chat = await createPrivateChat(accessToken, user); - const message = await participateChat(accessToken, chat.id); - console.log('새로 만든 후 참여하기', message); - /* 채팅방으로 이동 시키기*/ + await participateChat(accessToken, chat.id); router.push(`/chat/${chat.id}?isPrivate=true`); } }; - const editProfileHandler = () => { - console.log('click editProfileHandler'); + const handleEditUserData = async () => { + let isEditUserImage = false; + let isEditUserName = false; + let photoUrl: string = ''; + + try { + /* 이미지가 변경 됐을 경우*/ + if (userInput.picture.slice(0, 10) === 'data:image') { + isEditUserImage = true; + const req = await fetchImage({ + file: userInput.picture, + id: userInput.id, + password: '', + name: userInput.name, + }); + photoUrl = req.data.picture; + } + /* 이름이 변경 됐을 경우*/ + if (userInput.name !== user.name) { + isEditUserName = true; + } + + if (isEditUserImage) { + /* url가지고 정보 수정 */ + const message = await editUser(accessToken, userInput.name, photoUrl); + setUserInput({ ...user, picture: photoUrl }); + console.log(message); + } else if (isEditUserName) { + /* 이름만 정보 수정 */ + const message = await editUser(accessToken, userInput.name); + console.log(message); + } + } catch (e) { + console.error(e); + } finally { + setIsEdit(false); + } + }; + + const handleChange = (event: React.ChangeEvent) => { + const userInputTemp = { ...userInput, name: event.target.value }; + setUserInput(userInputTemp); + }; + + const handleCancelEdit = () => { + setIsEdit(false); + setUserInput(user); + }; + + const handleEditUserImage = () => { + if (!isEdit) return; + if (fileInputRef?.current) { + fileInputRef.current.click(); + } + }; + + const handleFileChange = () => { + let file = null; + if (fileInputRef.current?.files) { + const reader = new FileReader(); + file = fileInputRef.current.files[0]; + reader.onload = (e) => { + const base64DataUrl = e.target!.result as string; + console.log({ ...userInput, picture: base64DataUrl }); + setUserInput({ ...userInput, picture: base64DataUrl }); + }; + reader.readAsDataURL(file); + } }; return (
- + {isEdit ? ( + + ) : ( + + )} + + {isEdit && ( + + )}
-
+
{/* Flex 레이아웃용 */}
-
+ +
{user.name} + {isEdit && ( + 프로필 변경하기 + )}
-

{user.name}

+ + ) + } + variant="static" + size="lg" + value={userInput.name} + onChange={handleChange} + className="text-center text-white disabled:bg-transparent" + style={{ fontSize: '24px' }} + /> +
{isMyProfile ? (
setIsEdit((prev) => !prev)} > 프로필 편집하기 { + const chatId = params.id; const query = useSearchParams(); - console.log(params); - const router = useRouter(); - + const privateValue = query?.get('isPrivate') as string; const accessToken = getCookie('accessToken'); - const privateValue: string | null | undefined = query?.get('isPrivate'); - const chatId = 'bdd3fa0a-82d6-4655-b02a-e4aecadfa0fc'; - const serverId = process.env.NEXT_PUBLIC_SERVER_ID as string; - - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const [users, setUsers] = useState([]); - // 이전 메시지 - const [messages, setMessages] = useState([]); - // 새로운 메세지 - const [newMessage, setNewMessage] = useState(''); - // 소켓 연결 - const socket = io(`https://fastcampus-chat.net/chat?chatId=${params.id}`, { + const socket = io(`https://fastcampus-chat.net/chat?chatId=${chatId}`, { extraHeaders: { Authorization: `Bearer ${accessToken}`, - serverId: serverId, + serverId: process.env.NEXT_PUBLIC_SERVER_ID as string, }, }); - useEffect(() => { - console.log(socket); - console.log(privateValue); - socket.on('connect', () => { - console.log('소켓 연결 성공!'); - }); - // // 소켓 연결 시 users 요청 - // socket.emit('users'); - - // // users 접속 상태인 유저 목록 - // socket.on('users-to-client', (responseData) => { - // setUsers(responseData.user); - // console.log('유저 접속 상태 받아오기'); - // console.log('유저 접속 정보', users); - // }); - // fetch-messages 이벤트 핸들러 등록 - socket.emit('fetch-messages'); - - // messages-to-client 이벤트 핸들러 등록 - // eslint-disable-next-line @typescript-eslint/no-explicit-any - socket.on('messages-to-client', (responseData: any) => { - setMessages(responseData.messages); - console.log('이전 대화 목록 가져오기'); - console.log('이전 대화 목록 ', messages); - }); - - return () => { - socket.off('users-to-client'); - socket.off('messages-to-client'); - }; - }, [messages, params, privateValue, socket]); - const handleLeaveChat = () => { - const confirmLeave = window.confirm('채팅방을 나가시겠습니까?'); - if (confirmLeave) { - // 사용자가 확인하면 소켓 연결 끊기 - const socket = io(`https://fastcampus-chat.net/chat?chatId=${chatId}`); - socket.disconnect(); - } - }; - - const sendMessage = () => { - if (newMessage.trim() !== '') { - socket.emit('message-to-server', newMessage); - setNewMessage(''); - } - }; - return ( -
-
- -
-
- -
- - {privateValue === 'true' ? ( -
- {users && users.length == 2 ? ( -

1대1 채팅방 입니다.

- ) : ( -
-
- setNewMessage(e.target.value)} - placeholder="메시지를 입력하세요" - /> - -
- -
-

이전 대화 목록:

-
    - {messages.map((message) => ( -
  • -

    {message.text}

    -

    Sent by: {message.userId}

    -
  • - ))} -
-
-
- )} -
- ) : ( -
-
flase 오픈 채팅
-
-

이전 대화 목록:

-
    - {messages.map((message) => ( -
  • -

    {message.text}

    -

    Sent by: {message.userId}

    -
  • - ))} -
-
-
- )} -
+ ); }; diff --git a/app/users/users.utils.ts b/app/users/users.utils.ts index 55fb95d..e0c8cb1 100644 --- a/app/users/users.utils.ts +++ b/app/users/users.utils.ts @@ -27,3 +27,24 @@ export const fetchMyUser = async (token: string) => { const data = await res.json(); return data.user; }; + +export const editUser = async ( + token: string, + name?: string, + picture?: string, +) => { + const res = await fetch('https://fastcampus-chat.net/user', { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}`, + serverId: process.env.NEXT_PUBLIC_SERVER_ID as string, + }, + body: JSON.stringify({ + name, + picture, + }), + }); + const message = await res.json(); + return message; +}; diff --git a/public/icon_edit.svg b/public/icon_edit.svg index 52e6be9..657025f 100644 --- a/public/icon_edit.svg +++ b/public/icon_edit.svg @@ -1 +1,10 @@ - \ No newline at end of file + + + + + + + + + + diff --git a/public/icon_edit2.svg b/public/icon_edit2.svg new file mode 100644 index 0000000..52e6be9 --- /dev/null +++ b/public/icon_edit2.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/types/index.ts b/types/index.ts index 882d219..1320404 100644 --- a/types/index.ts +++ b/types/index.ts @@ -1,9 +1,10 @@ export type User = { id: string; - password: string; + password?: string; name: string; + username?: string; picture: string; - chats: string[]; // chat id만 속합니다. + chats?: string[]; // chat id만 속합니다. }; export type Message = { @@ -25,3 +26,7 @@ export type Chat = { export type AllOpenChatJSON = { chats: Chat[]; }; + +export type chatUsersObject = { + chat: Chat; +};