diff --git a/src/API/UserService.js b/src/API/UserService.js index 49921c20d24cdfc12d5956cf3eda5f8619e2fffc..f7c858cad29ae85ec854fa93ef843895d9286165 100644 --- a/src/API/UserService.js +++ b/src/API/UserService.js @@ -3,7 +3,6 @@ import env from '../Components/Common/dotenv'; const API_User_URL = env.REACT_APP_API_BASE_URL + "/user"; const accessToken = localStorage.getItem("accessToken"); - const UserService = { getFriendList: async () => { try { diff --git a/src/Components/Chat/ChatScreen.js b/src/Components/Chat/ChatScreen.js index 133130e4da77b45fa8ed3853c7b0d302e8b83820..c9735122c0ec9fff3b9f692933faa46240f977e2 100644 --- a/src/Components/Chat/ChatScreen.js +++ b/src/Components/Chat/ChatScreen.js @@ -1,7 +1,7 @@ import React, { useState, useEffect } from "react"; import { useSocket } from "../../Contexts/SocketContext"; -import chatService from "../../API/ChatService" -import userService from "../../API/UserService" +import chatService from "../../API/ChatService"; +import userService from "../../API/UserService"; import Message from "./Message"; function ChatScreen({ roomId, userInfo }) { @@ -10,18 +10,30 @@ function ChatScreen({ roomId, userInfo }) { const [message, setMessage] = useState(""); const [userId, setUserId] = useState(); const [startId, setStartId] = useState(null); + const [isNearBottom, setIsNearBottom] = useState(true); + const [scrollPosition, setScrollPosition] = useState(null); useEffect(() => { fetchUserInfo(); - fetchMesssages(); - + fetchMessages(); sendMessageWhenReady(socket, { type: "joinRoom", data: { roomId } }); + socket.addEventListener("message", handleSocketMessage); + return () => { socket.removeEventListener("message", handleSocketMessage); }; }, []); + useEffect(() => { + if(scrollPosition == 0) { + fetchMessages(); + window.scrollTo({ + top: document.documentElement.scrollHeight-10, + }); + } + }, [scrollPosition]); + function sendMessageWhenReady(client, message) { if (client.readyState === WebSocket.OPEN) { client.send(JSON.stringify(message)); @@ -30,10 +42,18 @@ function ChatScreen({ roomId, userInfo }) { } } - const fetchMesssages = async () => { + const fetchMessages = async () => { const response = await chatService.getMessages(roomId, startId); setStartId(response.nextId); - setMessages(response.messages); + setMessages((prevMessages) => { + let newMessages = []; + if (startId) { + newMessages = response.messages.concat(prevMessages); + } else { + newMessages = response.messages; + } + return newMessages; + }); }; const fetchUserInfo = async () => { @@ -41,18 +61,14 @@ function ChatScreen({ roomId, userInfo }) { setUserId(response.id); }; - // useEffect(() => { - // console.log(messages); - // }, [messages]); - const handleSocketMessage = (event) => { const receivedMessage = JSON.parse(event.data); switch (receivedMessage.type) { case "getmsg": - setMessages((messages) => { - return !messages ? [receivedMessage.data] : [...messages, receivedMessage.data]; - }); + setMessages((messages) => { + return !messages ? [receivedMessage.data] : [...messages, receivedMessage.data]; + }); break; default: break; @@ -69,8 +85,9 @@ function ChatScreen({ roomId, userInfo }) { data: { userName: userId, roomId, message }, }); setMessage(""); + handleScrollToBottom(true); }; - + const getUserInfo = (id) => { for (let el of userInfo) { if (el.id == id) { @@ -79,33 +96,71 @@ function ChatScreen({ roomId, userInfo }) { } }; + const handleScroll = () => { + setScrollPosition(window.scrollY); + const { scrollTop, scrollHeight, clientHeight } = document.documentElement; + const num = scrollTop + clientHeight >= scrollHeight - 100; + setIsNearBottom(num); + }; + + useEffect(() => { + window.addEventListener("scroll", handleScroll); + return () => { + window.removeEventListener("scroll", handleScroll); + }; + }, []); + + useEffect(() => { + handleNewMessage(); + }, [messages]); + + const handleScrollToBottom = () => { + window.scrollTo({ + top: document.documentElement.scrollHeight, + }); + }; + + const handleNewMessage = () => { + if(isNearBottom) { + window.scrollTo({ + top: document.documentElement.scrollHeight, + }); + } + }; + return ( - <main className="main-screen main-chat"> - {Array.isArray(messages) && - messages.map((msg) => ( - <Message - message={msg} - userId={userId} - senderInfo={getUserInfo(msg.sender)} - /> - ))} - <div className="reply"> - <div className="reply__column"> - <input - type="text" - placeholder="Write a message..." - value={message} - onChange={handleMessageChange} - onKeyPress={(event) => { - if (event.key === "Enter") { - handleSendMessage(); - } - }} - /> - </div> + <main> + <div className="main-screen main-chat"> + {!isNearBottom && ( + <button className="scroll-to-bottom-btn" onClick={handleScrollToBottom}> + 이동 + </button> + )} + {Array.isArray(messages) && + messages.map((msg) => ( + <Message + key={msg.id} + message={msg} + userId={userId} + senderInfo={getUserInfo(msg.sender)} + /> + ))} </div> + <footer className="reply"> + <input + type="text" + placeholder="Write a message..." + value={message} + onChange={handleMessageChange} + onKeyPress={(event) => { + if (event.key === "Enter") { + handleSendMessage(); + } + }} + /> + </footer> </main> ); } -export default ChatScreen; +export default ChatScreen; \ No newline at end of file diff --git a/src/Components/Chat/Message.js b/src/Components/Chat/Message.js index c8b19957c0b51d5e37b99c6b0a2603dd761c7852..8d818033365c14a382de03d8e12173ae9a3f3d4e 100644 --- a/src/Components/Chat/Message.js +++ b/src/Components/Chat/Message.js @@ -1,5 +1,6 @@ import React from "react"; import "../../css/components/message.css"; +import env from "../Common/dotenv"; function Message({ message, userId, senderInfo }) { const isOwnMessage = message.sender === userId; @@ -8,20 +9,20 @@ function Message({ message, userId, senderInfo }) { }`; return ( <div className={messageRowClass}> - {!isOwnMessage && ( + {!isOwnMessage ? ( <img src={ senderInfo.profileImage == null || senderInfo.profileImage == "" - ? "http://localhost:8040/uploads/default-profile.png" + ? `${env.REACT_APP_IMAGE_BASE_URL}/uploads/default-profile.png` : senderInfo.profileImage } alt={senderInfo.name} /> - )} + ):""} <div className="message-row__content"> - {!isOwnMessage && ( + {!isOwnMessage ? ( <span className="message__author">{senderInfo.name}</span> - )} + ):""} <div className="message__info"> <span className="message__bubble">{message.content}</span> <span className="message__time">{message.timestamp}</span> diff --git a/src/Components/Chat/UserMessage.js b/src/Components/Chat/UserMessage.js index 66e77ffac1f355bf46b4fc05fc62d963e43b535a..e141a72222eedb2633d5a8de693b1ef9b655d609 100644 --- a/src/Components/Chat/UserMessage.js +++ b/src/Components/Chat/UserMessage.js @@ -1,3 +1,5 @@ +import env from "../Common/dotenv" + function UserMessage({ avatar, name, @@ -14,7 +16,7 @@ function UserMessage({ <img src={ avatar == null || avatar == "" - ? "http://localhost:8040/uploads/default-profile.png" + ? `${env.REACT_APP_IMAGE_BASE_URL}/uploads/default-profile.png` : avatar } className="user-component__avatar user-component__avatar--xl" diff --git a/src/Components/Common/CommonForm.js b/src/Components/Common/CommonForm.js index 45bb78b4ccc4afc3add3e084e99fb1cfbc5ef0df..07c3788ca5e49ee44a55e236a5fb97f4a831c205 100644 --- a/src/Components/Common/CommonForm.js +++ b/src/Components/Common/CommonForm.js @@ -3,7 +3,6 @@ import { useState } from "react"; import { EmailButton } from '../Auth/Signin/EmailButton.js'; import authService from "../../API/AuthService.js"; - function CommonForm({ onSubmit, buttonText, fields, showVerificationButton, setWarningMessage }) { const [emailMessage, setEmailMessage] = useState("전송"); const [emailValue, setEmailValue] = useState(""); diff --git a/src/Components/Common/dotenv.js b/src/Components/Common/dotenv.js index 66caa7b45f2849ed71a571f839d84ce391323348..c4b158dcd9406147987d1767bf4fcb25248d814e 100644 --- a/src/Components/Common/dotenv.js +++ b/src/Components/Common/dotenv.js @@ -1,6 +1,7 @@ export const loadEnv = { REACT_APP_API_BASE_URL: "http://localhost:8080/api", // include /api prefix - REACT_APP_IMAGE_BASE_URL: "http://localhost:8040" + REACT_APP_IMAGE_BASE_URL: "http://localhost:8040", + REACT_APP_SOCKET_BASE_URL: "ws://localhost:3001" }; export default loadEnv; \ No newline at end of file diff --git a/src/Components/User/FriendsList.js b/src/Components/User/FriendsList.js index 1debb41f81c0075c52bf61deb3cb494c8218420a..5b76f189b2a746287159352fe5af3e82f05ac8a7 100644 --- a/src/Components/User/FriendsList.js +++ b/src/Components/User/FriendsList.js @@ -4,6 +4,7 @@ import UserService from "../../API/UserService"; import ChatService from "../../API/ChatService"; import AuthService from "../../API/AuthService"; import Comment from "../../assets/speech-bubble.png"; +import env from "../Common/dotenv" import { useEffect, useState } from "react"; @@ -86,7 +87,7 @@ function FriendsList() { key={friend.id} avatar={ friend.profileImage == null || friend.profileImage == "" - ? "http://localhost:8040/uploads/default-profile.png" + ? `${env.REACT_APP_IMAGE_BASE_URL}/uploads/default-profile.png` : friend.profileImage } name={friend.name} diff --git a/src/Components/User/ProfileUpdate.js b/src/Components/User/ProfileUpdate.js index d1781eeb8852138b9ff5e7727930f2d59c6b4112..25e0adc0d8a06bba20ea655803373ead78855316 100644 --- a/src/Components/User/ProfileUpdate.js +++ b/src/Components/User/ProfileUpdate.js @@ -1,6 +1,6 @@ import { useRef, useEffect, useState } from "react"; import UserService from "../../API/UserService"; -const baseURL = "http://localhost:8040"; +import env from "../Common/dotenv" function ProfileUpdate({ onClose, origin_image, origin_comment, name }) { const [image, setImage] = useState(null); @@ -20,7 +20,7 @@ function ProfileUpdate({ onClose, origin_image, origin_comment, name }) { formData.append("image", file); try { - const response = await fetch(`${baseURL}/api/upload`, { + const response = await fetch(`${env.REACT_APP_IMAGE_BASE_URL}/api/upload`, { method: "POST", body: formData, }); @@ -30,7 +30,7 @@ function ProfileUpdate({ onClose, origin_image, origin_comment, name }) { } const data = await response.json(); - setImageUrl(`${baseURL}${data.imageUrl}`); // 서버로부터 받은 이미지 URL + setImageUrl(`${env.REACT_APP_IMAGE_BASE_URL}${data.imageUrl}`); // 서버로부터 받은 이미지 URL } catch (error) { console.error(error); } diff --git a/src/Components/User/UserComponent.js b/src/Components/User/UserComponent.js index cc35fb6d935deef8786a9e8350e1f83ccad90280..481e2f5ea87ad3ddd86f387a9be7c65fad9df452 100644 --- a/src/Components/User/UserComponent.js +++ b/src/Components/User/UserComponent.js @@ -1,4 +1,5 @@ import React from "react"; +import env from "../Common/dotenv" import "../../css/components/userProfile.css"; function UserComponent({ avatar, name, subtitle, additionalContent }) { @@ -14,7 +15,7 @@ function UserComponent({ avatar, name, subtitle, additionalContent }) { <img src={ !avatar - ? "http://localhost:8040/uploads/default-profile.png" + ? `${env.REACT_APP_IMAGE_BASE_URL}/uploads/default-profile.png` : avatar } className="user-component__avatar" diff --git a/src/Components/User/UserList.js b/src/Components/User/UserList.js index 850cf7dbe2c50a9473795b63643bdb9eb788d345..13022161669648f1a8244ab6fc99b1f95307092c 100644 --- a/src/Components/User/UserList.js +++ b/src/Components/User/UserList.js @@ -5,7 +5,7 @@ import AuthService from "../../API/AuthService"; import Add from "../../assets/add-friend-icon.png"; -import { useContext, useEffect, useState } from "react"; +import { useEffect, useState } from "react"; function UserList() { const [showAddedFriends, setShowAddedFriends] = useState(false); diff --git a/src/Components/User/UserProfile.js b/src/Components/User/UserProfile.js index 0b18665c63d646093f0b876ad9eed1b8c2104ae2..f5b239a8c275a9de7835e4f2e962d9d944d3d6dc 100644 --- a/src/Components/User/UserProfile.js +++ b/src/Components/User/UserProfile.js @@ -2,6 +2,7 @@ import React, { useState, useEffect } from "react"; import UserService from "../../API/UserService"; import AuthService from "../../API/AuthService"; import ProfileComponent from "./ProfileComponent"; +import env from "../Common/dotenv" function UserProfile() { const accessToken = localStorage.getItem("accessToken"); @@ -17,7 +18,7 @@ function UserProfile() { if (response) { setSubtitle(response.comment); if (response.profileImage == null || response.profileImage == "") { - setUserProfile("http://localhost:8040/uploads/default-profile.png"); + setUserProfile(`${env.REACT_APP_IMAGE_BASE_URL}/uploads/default-profile.png`); } else { setUserProfile(response.profileImage); } diff --git a/src/Contexts/SocketContext.js b/src/Contexts/SocketContext.js index 8b3612fb28b6fc3228ee8ed09429ffd904387fe7..1a96a5ff5324a52b5b947b4404d3fca8c3389d40 100644 --- a/src/Contexts/SocketContext.js +++ b/src/Contexts/SocketContext.js @@ -1,8 +1,9 @@ import React, { createContext, useContext, useEffect } from "react"; +import env from "../Components/Common/dotenv" const SocketContext = createContext(); -const socket = new WebSocket("ws://localhost:3001"); +const socket = new WebSocket(`${env.REACT_APP_SOCKET_BASE_URL}`); export function useSocket() { return useContext(SocketContext); diff --git a/src/Pages/Chat/Chat.js b/src/Pages/Chat/Chat.js index 971fc25fb8cba4a9679e15b0c08971dadb8081a0..b0ff269156634394a6c849fc47a9677bf83918df 100644 --- a/src/Pages/Chat/Chat.js +++ b/src/Pages/Chat/Chat.js @@ -16,10 +16,10 @@ function Chat() { useEffect(() => { fetchAllUserInfo(); }, []); - + return ( <WebSocketProvider> - <div id="chat-screen"> + <div> <ChatHeader roomId={roomId} /> {userInfo ? <ChatScreen roomId={roomId} userInfo={userInfo} /> : ""} </div> diff --git a/src/css/components/chat.css b/src/css/components/chat.css index b53abcb5b81819348b20a16c504d8ad1d818de5b..c1af22be81d3e0edc4fbd48e5b920dbf5e0222db 100644 --- a/src/css/components/chat.css +++ b/src/css/components/chat.css @@ -107,6 +107,7 @@ position: fixed; bottom: 0; width: 100%; + height: 3%; background-color: white; display: flex; justify-content: space-between; diff --git a/src/css/components/chatHeader.css b/src/css/components/chatHeader.css index 31ddc8de6085e04c8136b39273917fa382f8b56e..eddc2480d27ae0d7fa0e863026cfadaa88c810da 100644 --- a/src/css/components/chatHeader.css +++ b/src/css/components/chatHeader.css @@ -1,8 +1,14 @@ .alt-header { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 3%; + z-index: 100; display: flex; align-items: center; justify-content: space-between; - padding: 1rem; + padding: 2vh; background-color: #0054a6; /* 아주대학교 색상인 파란색으로 변경 */ color: #fff; } diff --git a/src/css/components/message.css b/src/css/components/message.css index 3d840bc60c135b7c24f03f888786d3090ec30e07..ee526c09907aedadbbdde7540ef0c8b412c28b0a 100644 --- a/src/css/components/message.css +++ b/src/css/components/message.css @@ -1,8 +1,12 @@ .main-screen.main-chat { /* background-color: #f9f9f9; */ - padding: 20px; + padding-left: 3vw; + padding-right: 3vw; overflow-y: auto; + margin-top: 7vh; + margin-bottom: 7vh; } + .message { margin-bottom: 20px; padding: 10px; @@ -16,15 +20,6 @@ margin-bottom: 5px; } -.reply { - position: fixed; - bottom: 0; - left: 0; - right: 0; - padding: 10px; - background-color: #fff; - box-shadow: 0 -2px 4px rgba(0, 0, 0, 0.1); -} .reply__column { display: flex; align-items: center; @@ -97,10 +92,29 @@ background-color: #fee500; } +.scroll-to-bottom-btn { + position: fixed; + bottom: 5%; + left: 50%; + transform: translateX(-50%); + background-color: #0054a6; + color: #fff; + border: none; + border-radius: 25%; + padding: 1%; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + white-space: nowrap; + margin-bottom: 1%; +} + .reply { - /* display: flex; */ - justify-content: space-between; - padding: 10px; + display: flex; + justify-content: start; + padding: 1vh; + height: 3vh; background-color: #fff; position: fixed; bottom: 0; diff --git a/src/css/components/styles.css b/src/css/components/styles.css index 0ac4d4bea5e77d2fe847b0b4a150fcd20189b784..44fbad67e81f9b22e816c938350fd76d020f010a 100644 --- a/src/css/components/styles.css +++ b/src/css/components/styles.css @@ -6,26 +6,6 @@ body, h1, p, input, button, span { -moz-osx-font-smoothing: grayscale; } -.status-bar { - background-color: #0054a6; - color: white; - padding: 10px 20px; - display: flex; - justify-content: space-between; - align-items: center; - font-size: 14px; -} - -.status-bar__column span { - display: flex; - align-items: center; -} - -.status-bar__column span:not(:last-child) { - margin-right: 10px; -} - - input::placeholder { color: #bbb; }