diff --git a/package-lock.json b/package-lock.json index d368edf..3caa740 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,9 @@ "name": "web-business", "version": "0.1.0", "dependencies": { + "@fortawesome/fontawesome-svg-core": "^6.5.1", + "@fortawesome/free-solid-svg-icons": "^6.5.1", + "@fortawesome/react-fontawesome": "^0.2.0", "@heroicons/react": "^2.1.1", "@material-tailwind/react": "^2.1.9", "@stomp/stompjs": "^7.0.0", @@ -26,6 +29,7 @@ "react-dom": "^17.0.2", "react-i18next": "^14.0.5", "react-icons": "^5.0.1", + "react-modal": "^3.16.1", "react-redux": "^7.2.9", "react-router-dom": "^6.22.1", "react-scripts": "5.0.1", @@ -2468,6 +2472,51 @@ "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.1.tgz", "integrity": "sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q==" }, + "node_modules/@fortawesome/fontawesome-common-types": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.5.1.tgz", + "integrity": "sha512-GkWzv+L6d2bI5f/Vk6ikJ9xtl7dfXtoRu3YGE6nq0p/FFqA1ebMOAWg3XgRyb0I6LYyYkiAo+3/KrwuBp8xG7A==", + "hasInstallScript": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/fontawesome-svg-core": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.5.1.tgz", + "integrity": "sha512-MfRCYlQPXoLlpem+egxjfkEuP9UQswTrlCOsknus/NcMoblTH2g0jPrapbcIb04KGA7E2GZxbAccGZfWoYgsrQ==", + "hasInstallScript": true, + "dependencies": { + "@fortawesome/fontawesome-common-types": "6.5.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/free-solid-svg-icons": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.5.1.tgz", + "integrity": "sha512-S1PPfU3mIJa59biTtXJz1oI0+KAXW6bkAb31XKhxdxtuXDiUIFsih4JR1v5BbxY7hVHsD1RKq+jRkVRaf773NQ==", + "hasInstallScript": true, + "dependencies": { + "@fortawesome/fontawesome-common-types": "6.5.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/react-fontawesome": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-0.2.0.tgz", + "integrity": "sha512-uHg75Rb/XORTtVt7OS9WoK8uM276Ufi7gCzshVWkUJbHhh3svsUUeqXerrM96Wm7fRiDzfKRwSoahhMIkGAYHw==", + "dependencies": { + "prop-types": "^15.8.1" + }, + "peerDependencies": { + "@fortawesome/fontawesome-svg-core": "~1 || ~6", + "react": ">=16.3" + } + }, "node_modules/@heroicons/react": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/@heroicons/react/-/react-2.1.1.tgz", @@ -8548,6 +8597,11 @@ "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, + "node_modules/exenv": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/exenv/-/exenv-1.2.2.tgz", + "integrity": "sha512-Z+ktTxTwv9ILfgKCk32OX3n/doe+OcLTRtqK9pcL+JsP3J1/VW8Uvl4ZjLlKqeW4rzK4oesDOGMEMRIZqtP4Iw==" + }, "node_modules/exit": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", @@ -16804,6 +16858,29 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" }, + "node_modules/react-lifecycles-compat": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", + "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" + }, + "node_modules/react-modal": { + "version": "3.16.1", + "resolved": "https://registry.npmjs.org/react-modal/-/react-modal-3.16.1.tgz", + "integrity": "sha512-VStHgI3BVcGo7OXczvnJN7yT2TWHJPDXZWyI/a0ssFNhGZWsPmB8cF0z33ewDXq4VfYMO1vXgiv/g8Nj9NDyWg==", + "dependencies": { + "exenv": "^1.2.0", + "prop-types": "^15.7.2", + "react-lifecycles-compat": "^3.0.0", + "warning": "^4.0.3" + }, + "engines": { + "node": ">=8" + }, + "peerDependencies": { + "react": "^0.14.0 || ^15.0.0 || ^16 || ^17 || ^18", + "react-dom": "^0.14.0 || ^15.0.0 || ^16 || ^17 || ^18" + } + }, "node_modules/react-redux": { "version": "7.2.9", "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.9.tgz", @@ -19922,6 +19999,14 @@ "makeerror": "1.0.12" } }, + "node_modules/warning": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", + "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, "node_modules/watchpack": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", diff --git a/package.json b/package.json index 9a70c2f..590b0ad 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,9 @@ "version": "0.1.0", "private": true, "dependencies": { + "@fortawesome/fontawesome-svg-core": "^6.5.1", + "@fortawesome/free-solid-svg-icons": "^6.5.1", + "@fortawesome/react-fontawesome": "^0.2.0", "@heroicons/react": "^2.1.1", "@material-tailwind/react": "^2.1.9", "@stomp/stompjs": "^7.0.0", @@ -21,6 +24,7 @@ "react-dom": "^17.0.2", "react-i18next": "^14.0.5", "react-icons": "^5.0.1", + "react-modal": "^3.16.1", "react-redux": "^7.2.9", "react-router-dom": "^6.22.1", "react-scripts": "5.0.1", diff --git a/src/App.js b/src/App.js index bdfa21a..485e353 100644 --- a/src/App.js +++ b/src/App.js @@ -1,4 +1,4 @@ -import {BrowserRouter, Navigate, Route, Routes} from "react-router-dom"; +import {BrowserRouter, Route, Routes} from "react-router-dom"; import React from "react"; @@ -20,9 +20,6 @@ import Plans from "./pages/business/plans"; import BusinessPlanDetail from "./pages/business/plan"; import AdminPlainDetail from "./pages/administrator/adminPlanDetail"; import AdminChat from "./pages/administrator/adminChat"; -import Chat from "./pages/business/chat"; -import BusinessChatRoom from "./pages/business/businessChatRoom"; -import AdminChatRoom from "./pages/administrator/adminChatRoom"; /** * @since 2024.02.25 @@ -31,13 +28,13 @@ import AdminChatRoom from "./pages/administrator/adminChatRoom"; function App() { const businessColor = "bg-main-color-600 border-r border-gray-200 sm:translate-x-0 dark:bg-gray-800 dark:border-gray-700"; - const businessSideBarColor = "bg-main-color-600 dark:bg-blue-600"; + const businessSideBarColor = "bg-main-color-600 text-white dark:bg-blue-600"; const adminColor = "bg-main-blue-600 border-r border-gray-200 sm:translate-x-0 dark:bg-gray-800 dark:border-gray-700"; const adminSideBarColor = "bg-main-blue-600 dark:bg-blue-600"; const businessSideBarList = [ - ['대시보드', '/dashboard'], ['나의 사업계획서 목록', '/plans'], ['팝업 스토어 제안', '/plan/create'], ['광고 신청', '/ad/create'], ['1:1 채팅상담', '/chat'] + ['대시보드', '/dashboard'], ['나의 사업계획서 목록', '/plans'], ['팝업 스토어 제안', '/plan/create'], ['광고 신청', '/ad/create'] ]; const adminSideBarList = [ ['사용자 관리', [['일반 사용자 관리', '/admin/users'], ['사업체 관리', '/admin/business']]], @@ -61,8 +58,6 @@ function App() { {generateRoute(businessColor, businessSideBarColor, businessSideBarList, businessHomeUrl, "/plans/:planId", BusinessPlanDetail)} {generateRoute(businessColor, businessSideBarColor, businessSideBarList, businessHomeUrl, "/plan/create", CreatePlan)} {generateRoute(businessColor, businessSideBarColor, businessSideBarList, businessHomeUrl, "/ad/create", Ad)} - {generateRoute(businessColor, businessSideBarColor, businessSideBarList, businessHomeUrl, "/chat", Chat)} - {generateRoute(businessColor, businessSideBarColor, businessSideBarList, businessHomeUrl, "/chat/:roomId", BusinessChatRoom)} {generateRoute(adminColor, adminSideBarColor, adminSideBarList, adminHomeUrl, '/admin/users', User)} {generateRoute(adminColor, adminSideBarColor, adminSideBarList, adminHomeUrl, '/admin/business', Business)} @@ -70,23 +65,20 @@ function App() { {generateRoute(adminColor, adminSideBarColor, adminSideBarList, adminHomeUrl, '/admin/plan/:planId', AdminPlainDetail)} {generateRoute(adminColor, adminSideBarColor, adminSideBarList, adminHomeUrl, '/admin/community', Community)} {generateRoute(adminColor, adminSideBarColor, adminSideBarList, adminHomeUrl, '/admin/chat', AdminChat)} - {generateRoute(adminColor, adminSideBarColor, adminSideBarList, adminHomeUrl, '/admin/chat/:roomId', AdminChatRoom)} + ); } const generateRoute = (color, sideBarColor, sideBarList, homeUrl, path, Component) => ( - } homeUrl={homeUrl} /> - ]} /> ); diff --git a/src/api/administrator/adminChatApi.js b/src/api/administrator/adminChatApi.js index b87de87..72ce1fc 100644 --- a/src/api/administrator/adminChatApi.js +++ b/src/api/administrator/adminChatApi.js @@ -1,14 +1,7 @@ -import axios from "axios"; - -import GetTokenFromLocalStorage from "../Common/token"; - -const Token = GetTokenFromLocalStorage('admin') -if (Token) { - axios.defaults.headers.common['Authorization'] = `Bearer ${Token}` -} +import adminInstance from "../adminBaseApi"; /** - * @since 2024.03.52 + * @since 2024.03.02 * @author 이상민 */ const ChatApi = { @@ -19,7 +12,17 @@ const ChatApi = { * @author 이상민 */ getChatRooms: async (pageNo = 0, amount = 10) => { - return await axios.get(`/api/v1/chat/rooms/admin?pageNo=${pageNo}&amount=${amount}`); + return await adminInstance.get(`/chat/rooms/admin?pageNo=${pageNo}&amount=${amount}`); + }, + + /** + * 나의 채팅방 메시지 리스트 조회 + * + * @since 2024.03.02 + * @author 이상민 + */ + getMessages: async (roomId = 0) => { + return await adminInstance.get(`/chat/rooms/${roomId}`); }, } diff --git a/src/api/chatApi.js b/src/api/chatApi.js index 10c7e63..5e40dee 100644 --- a/src/api/chatApi.js +++ b/src/api/chatApi.js @@ -1,10 +1,4 @@ -import GetTokenFromLocalStorage from "./Common/token"; -import axios from "axios"; - -const Token = GetTokenFromLocalStorage('user') -if (Token) { - axios.defaults.headers.common['Authorization'] = `Bearer ${Token}` -} +import userBaseApi from "./userBaseApi"; /** * @since 2024.03.52 @@ -18,7 +12,7 @@ const ChatApi = { * @author 이상민 */ getChatRooms: async (pageNo = 0, amount = 10) => { - return await axios.get(`/api/v1/chat/rooms?pageNo=${pageNo}&amount=${amount}`); + return await userBaseApi.get(`/chat/rooms?pageNo=${pageNo}&amount=${amount}`); }, /** @@ -28,7 +22,7 @@ const ChatApi = { * @author 이상민 */ getMessages: async (roomId = 0) => { - return await axios.get(`/api/v1/chat/rooms/${roomId}`); + return await userBaseApi.get(`/chat/rooms/${roomId}`); }, } diff --git a/src/components/common/Chat/ChatPage.jsx b/src/components/administrator/ChatPage.jsx similarity index 91% rename from src/components/common/Chat/ChatPage.jsx rename to src/components/administrator/ChatPage.jsx index 3c6b62e..99e85cc 100644 --- a/src/components/common/Chat/ChatPage.jsx +++ b/src/components/administrator/ChatPage.jsx @@ -1,8 +1,7 @@ import React, { useEffect, useState } from "react"; -import ContentBox from "../../common/ContentBox/ContentBox"; -import Pagination from "../../common/Pagination/Pagination"; -import Table from "../Table/Table"; -import ChatTable from "../Table/ChatTable"; +import ContentBox from "../../components/common/ContentBox/ContentBox"; +import Pagination from "../../components/common/Pagination/Pagination"; +import ChatTable from "./ChatTable"; const ChatPage = ({ api, title }) => { const [data, setData] = useState(null); @@ -63,4 +62,4 @@ const ChatPage = ({ api, title }) => { ); }; -export default ChatPage; +export default ChatPage; \ No newline at end of file diff --git a/src/components/administrator/ChatRoomDetail.jsx b/src/components/administrator/ChatRoomDetail.jsx new file mode 100644 index 0000000..f1a628e --- /dev/null +++ b/src/components/administrator/ChatRoomDetail.jsx @@ -0,0 +1,227 @@ +import React, {useEffect, useRef, useState} from "react"; +import SockJS from "sockjs-client"; +import {Stomp} from "@stomp/stompjs"; +import GetTokenFromLocalStorage from "../../api/Common/token"; +import AdminChatApi from "../../api/administrator/adminChatApi"; +import MeChatMessage from "../common/Chat/MeChatMessage"; +import OtherChatMessage from "../common/Chat/OtherChatMessage"; + +/** + * 채팅방 컴포넌트 + * + * @since 2024.03.02 + * @author 이상민 + */ +const ChatRoomDetail = ({selectedChatRoomId, setModalIsOpen}) => { + + const [stompClient, setStompClient] = useState(null); + const [messages, setMessages] = useState([]); + const [newMessage, setNewMessage] = useState(""); + + const roomId = selectedChatRoomId; + const role = 'admin' + console.log("채팅방 : " + roomId) + + const messagesContainerRef = useRef(null); + const token = GetTokenFromLocalStorage(role) + + const [chatRoomDetail, setChatRoomDetail] = useState([]); + + /** + * WebSocket 연결 + * + * @since 2024.03.03 + * @author 이상민 + */ + useEffect(() => { + const socket = new SockJS(`http://15.164.236.13:8080/ws`); + const stomp = Stomp.over(socket); + setStompClient(stomp); + // 고유한 ID 생성 + const uniqueId = `sub-${Math.random().toString(36).substr(2, 9)}`; + stomp.connect({ Authorization: `Bearer ${token}` }, (frame) => { + console.log("연결 성공!", frame); + console.log("1 : " + role) + + // 2. 특정 채팅방에 구독 + stomp.subscribe(`/topic/messages`, (message) => { + const newMessage = JSON.parse(message.body); + + if(newMessage.role === role){ + newMessage.checkedMe = true; + }else{ + newMessage.checkedMe = false; + } + + // 기존 메시지와 새 메시지를 합쳐서 상태 업데이트 + // setMessages((prevMessages) => [...prevMessages, newMessage]); + + // 새 메시지가 현재 채팅방에 대한 것인지 확인 + if (roomId === newMessage.roomId) { + showMessage(newMessage); + } + }, { id: uniqueId }); // 고유한 ID로 구독 + }, (error) => { + console.error("연결 실패:", error); + }); + // 3. 이전 메시지 불러오기 + loadPreviousMessages(); + return () => { + stomp.disconnect(); + }; + }, [roomId]); + + useEffect(() => { + scrollToBottom(); // 새로운 메시지가 추가될 때마다 스크롤을 최하단으로 이동 + }, [messages]); + + const loadPreviousMessages = async () => { + try { + const response = await AdminChatApi.getMessages(roomId); + console.log(response) + + const data = response.data; + setMessages(data?.data?.chatMessageDetailResponse || []); + setChatRoomDetail(data?.data?.chatRoomDetailResponse || []); + } catch (error) { + console.error("이전 메시지 불러오기 실패:", error); + } + }; + + /** + * 메시지 전송 + * + * @since 2024.03.03 + * @author 이상민 + */ + const sendMessage = () => { + if (newMessage.trim() !== '' && stompClient) { + const chatMessageRequest = { + roomId: roomId, + message: newMessage, + role : role, + sendingTime: new Date().toISOString(), + }; + + // 'send' 메서드의 옵션으로 'Authorization' 헤더를 설정 + const headers = { Authorization: `Bearer ${token}` }; + + // 헤더를 포함하여 메시지를 전송 + stompClient.send("/app/sendMessage", headers, JSON.stringify(chatMessageRequest)); + + // 메시지 입력을 지웁니다 + setNewMessage(''); + } + }; + + /** + * 메시지 보여주기 + * + * @since 2024.03.03 + * @author 이상민 + */ + const showMessage = (message) => { + const formattedMessage = { + sender: message.senderNickname, + sendingTime: message.sendingTime, + message: message.message, + checkedMe: message.checkedMe, + }; + + console.log("포맷 :: " + formattedMessage); + + setMessages((prevMessages) => [...prevMessages, formattedMessage]); + scrollToBottom(); + }; + + /** + * 최하단으로 스크롤 이동 + * + * @since 2024.03.03 + * @author 이상민 + */ + const scrollToBottom = () => { + if (messagesContainerRef.current) { + messagesContainerRef.current.scrollTop = messagesContainerRef.current.scrollHeight; + } + }; + + const handleGoBack = () => { + setModalIsOpen(false) + }; + + return ( +
+
+ +

+ {chatRoomDetail.name} +

+
+ +
+ +
    + {messages.map((message, index) => ( +
  • + {message.checkedMe === true ? ( + + ) : ( + + )} +
    +
  • + ))} +
+
+ +
+
+ setNewMessage(e.target.value)} + className="border p-1 rounded-full w-80 border-gray-" + style={{fontSize: '12px', padding: '6px'}} + /> + +
+
+ +
+ ); +}; + +export default ChatRoomDetail; diff --git a/src/components/administrator/ChatTable.jsx b/src/components/administrator/ChatTable.jsx new file mode 100644 index 0000000..5f321c4 --- /dev/null +++ b/src/components/administrator/ChatTable.jsx @@ -0,0 +1,70 @@ +import React, {useState} from "react"; +import TableHeader from "../common/Table/TableHeader"; +import TableRow from "../common/Table/TableRow"; +import Modal from "react-modal"; +import ChatRoomDetail from "./ChatRoomDetail"; + +/** + * Table 컴포넌트 생성 + * + * @since 2024.02.25 + * @author 이상민 + */ +const ChatTable = ({ headerTitles, sampleData }) => { + + const [modalIsOpen, setModalIsOpen] = useState(false); + const [selectedChatRoomId, setSelectedChatRoomId] = useState(null); + + const handleRowClick = (id) => { + console.log("테이블 : " + id) + + setSelectedChatRoomId(id); + setModalIsOpen(true); + }; + + const renderTableBody = (sampleData) => ( + + {sampleData.map((rowData, index) => ( + handleRowClick(rowData[0])} /> + ))} + + ); + + return ( +
+ + + {renderTableBody(sampleData)} +
+ + setModalIsOpen(false)} + contentLabel="ChatRoomDetail Modal" + style={{ + overlay: { + backgroundColor: 'rgba(0, 0, 0, 0.5)', + }, + content: { + top: '50%', + left: '50%', + right: 'auto', + bottom: 'auto', + marginRight: '-50%', + transform: 'translate(-50%, -50%)', + maxWidth: '80%', // 필요에 따라 최대 너비 조정 + padding: 0, + borderRadius: 25, + boxShadow: '0px 4px 8px rgba(0, 0, 0, 0.1)', + }, + }} + > + {selectedChatRoomId && ( + + )} + +
+ ); +}; + +export default ChatTable; diff --git a/src/components/business/Chat/ChatList.jsx b/src/components/business/Chat/ChatList.jsx new file mode 100644 index 0000000..f7c66ad --- /dev/null +++ b/src/components/business/Chat/ChatList.jsx @@ -0,0 +1,69 @@ +import React, {useEffect, useState} from 'react'; +import '../../common/Chat/ChatApp.css'; + +/** + * 채팅 리스트 컴포넌트 + * + * @since 2024.03.02 + * @author 이상민 + */ +const ChatList = ({api, onChatRoomClick, closeModal}) => { + + const [data, setData] = useState([]); + const [limit, setLimit] = useState(10); + const [page, setPage] = useState(1); + + const handleChatRoomClick = (chatRoom) => { + onChatRoomClick(chatRoom[0]); + }; + + useEffect(() => { + const fetchData = async () => { + try { + const response = await api.getChatRooms(page - 1, limit); + const mappedData = response.data.data.list.map((chatRoom, index) => { + const sequenceNumber = index + 1 + (page - 1) * limit; + return { + pkId: chatRoom.chatRoomId || "-", + sequenceNumber, + title: chatRoom.name ? chatRoom.name : "-", + createdDate: chatRoom.createdDate ? chatRoom.createdDate : "-", + modifiedDate: chatRoom.modifiedDate ? chatRoom.modifiedDate : "-", + }; + }); + setData( mappedData.map((chatRoom) => [...Object.values(chatRoom)])); + } catch (error) { + console.error("사용자 데이터를 가져오는 중 오류 발생:", error); + } + }; + fetchData(); + }, [page, limit]); + + return ( +
+
+

대화

+ {data && data.map((chatRoom, index) => ( +
handleChatRoomClick(chatRoom)}> +

{chatRoom[0]} {chatRoom[2]} {chatRoom[3]}{' '}

+
+
+ ))} +
+
+ +
+ +
+ ); +}; + +export default ChatList; diff --git a/src/components/common/Chat/ChatRoom.jsx b/src/components/business/Chat/ChatRoom.jsx similarity index 60% rename from src/components/common/Chat/ChatRoom.jsx rename to src/components/business/Chat/ChatRoom.jsx index bc2c364..bd7c1ea 100644 --- a/src/components/common/Chat/ChatRoom.jsx +++ b/src/components/business/Chat/ChatRoom.jsx @@ -1,26 +1,32 @@ +import ChatApi from "../../../api/chatApi"; import React, {useEffect, useRef, useState} from "react"; -import SockJS from "sockjs-client"; -import { Stomp } from "@stomp/stompjs"; -import { useParams } from "react-router-dom"; import GetTokenFromLocalStorage from "../../../api/Common/token"; -import ChatApi from "../../../api/chatApi"; -import MeChatMessage from "./MeChatMessage"; -import OtherChatMessage from "./OtherChatMessage"; +import SockJS from "sockjs-client"; +import {Stomp} from "@stomp/stompjs"; +import MeChatMessage from "../../common/Chat/MeChatMessage"; +import OtherChatMessage from "../../common/Chat/OtherChatMessage"; +import '../../common/Chat/ChatApp.css'; /** - * 채팅방 + * 채팅방 컴포넌트 * - * @since 2024.03.03 + * @since 2024.03.02 * @author 이상민 */ -const ChatRoom = ({role}) => { +const ChatRoom= ({selectedChatRoom, setSelectedChatRoom}) => { + + console.log("선택된 채팅방:", selectedChatRoom); + const [stompClient, setStompClient] = useState(null); const [messages, setMessages] = useState([]); const [newMessage, setNewMessage] = useState(""); - const { roomId } = useParams(); + const roomId = selectedChatRoom; + + + const role = 'user' const messagesContainerRef = useRef(null); - const token = GetTokenFromLocalStorage(role) + const token = GetTokenFromLocalStorage('role') const [chatRoomDetail, setChatRoomDetail] = useState([]); @@ -38,14 +44,21 @@ const ChatRoom = ({role}) => { const uniqueId = `sub-${Math.random().toString(36).substr(2, 9)}`; stomp.connect({ Authorization: `Bearer ${token}` }, (frame) => { console.log("연결 성공!", frame); + console.log("1 : " + role) + // 2. 특정 채팅방에 구독 stomp.subscribe(`/topic/messages`, (message) => { const newMessage = JSON.parse(message.body); - newMessage.checkedMe = true; + if(newMessage.role === role){ + newMessage.checkedMe = true; + }else{ + newMessage.checkedMe = false; + } // 기존 메시지와 새 메시지를 합쳐서 상태 업데이트 - setMessages((prevMessages) => [...prevMessages, newMessage]); + // setMessages((prevMessages) => [...prevMessages, newMessage]); + // 새 메시지가 현재 채팅방에 대한 것인지 확인 if (roomId === newMessage.roomId) { showMessage(newMessage); @@ -68,6 +81,8 @@ const ChatRoom = ({role}) => { const loadPreviousMessages = async () => { try { const response = await ChatApi.getMessages(roomId); + console.log(response) + const data = response.data; setMessages(data?.data?.chatMessageDetailResponse || []); setChatRoomDetail(data?.data?.chatRoomDetailResponse || []); @@ -87,6 +102,7 @@ const ChatRoom = ({role}) => { const chatMessageRequest = { roomId: roomId, message: newMessage, + role : role, sendingTime: new Date().toISOString(), }; @@ -133,20 +149,45 @@ const ChatRoom = ({role}) => { } }; + const handleGoBack = () => { + setSelectedChatRoom(null); + }; + return ( -
-

{chatRoomDetail.name}

+
+
+ +

+ {chatRoomDetail.name} +

+
+
-

Messages:

+ className="h-[475px] w-[325px] overflow-y-auto mb-4 + scrollbar-thin scrollbar-thumb-blue-500 scrollbar-track-gray-200" + style={{ + margin: '16px' + }}> +
    {messages.map((message, index) => (
  • {message.checkedMe === true ? ( - + ) : ( {
-
- setNewMessage(e.target.value)} - className="flex-grow border p-2 rounded" - /> - +
+
+ setNewMessage(e.target.value)} + className="border p-1 rounded-full w-80 border-gray-" + style={{fontSize: '12px', padding: '6px'}} + /> + +
+
); }; diff --git a/src/components/business/DashBoard/PopupRanking.js b/src/components/business/DashBoard/PopupRanking.js index b825555..9c216b0 100644 --- a/src/components/business/DashBoard/PopupRanking.js +++ b/src/components/business/DashBoard/PopupRanking.js @@ -23,23 +23,20 @@ function PopupRanking() { }, []); return ( -
+
{myPopup.length > 0 && ( -
+
MY
-
+
{myPopup[0].popupName}
-
-
- {myPopup[0].popupView} -
+
- 회 + {myPopup[0].popupView} 회
diff --git a/src/components/business/FloatingButton/FloatingButton.css b/src/components/business/FloatingButton/FloatingButton.css new file mode 100644 index 0000000..ed98fe7 --- /dev/null +++ b/src/components/business/FloatingButton/FloatingButton.css @@ -0,0 +1,33 @@ +.floating-button { + position: fixed; + bottom: 20px; + right: 20px; + width: 50px; + height: 50px; + border-radius: 50%; + background-color: #49675a; + color: #fff; + font-size: 24px; + border: none; + cursor: pointer; + outline: none; + box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.1); + transition: background-color 0.3s ease; +} + +.floating-button:hover { + background-color: #96b2a4; +} + +.floating-button-modal { + position: fixed; + top: calc(64%); /* 버튼 바로 위에 위치하도록 수정 */ + right: 20px; /* 화면 오른쪽에 위치하도록 수정 */ + transform: translate(0, -50%); + overflow: auto; + outline: none; + border-radius: 25px; + box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.1); + background-color: #fff; /* 배경 색상 추가 */ + /* 추가적인 스타일 설정 */ +} diff --git a/src/components/business/FloatingButton/FloatingButton.jsx b/src/components/business/FloatingButton/FloatingButton.jsx new file mode 100644 index 0000000..89bd0f3 --- /dev/null +++ b/src/components/business/FloatingButton/FloatingButton.jsx @@ -0,0 +1,79 @@ +import React, {useState} from 'react'; +import './FloatingButton.css'; // 스타일 파일을 추가합니다. +import Modal from 'react-modal'; +import ChatApi from "../../../api/chatApi"; +import ChatRoom from "../../business/Chat/ChatRoom"; +import ChatList from "../../business/Chat/ChatList"; + +/** + * FloatingButton 컴포넌트 + * + * @since 2024.03.04 + * @author 이상민 + */ +const FloatingButton = () => { + const [modalIsOpen, setModalIsOpen] = useState(false); + // const [buttonPosition, setButtonPosition] = useState({ top: 0, left: 0 }); + const [selectedChatRoom, setSelectedChatRoom] = useState(null); + + const openModal = () => { + const button = document.querySelector('.floating-button'); + if (button) { + const rect = button.getBoundingClientRect(); + // setButtonPosition({ top: rect.bottom + window.scrollY, left: rect.left + window.scrollX }); + } + setModalIsOpen(true); + document.body.style.overflow = 'hidden'; + }; + + /** + * 모달 닫을 때 사용 + * + * @since 2024.03.04 + * @author 이상민 + */ + const closeModal = () => { + setModalIsOpen(false); + // 모달이 닫힐 때 선택된 채팅방 정보 초기화 + document.body.style.overflow = 'auto'; + setSelectedChatRoom(null); + }; + + const handleChatRoomClick = (chatRoom) => { + console.log("chat room id : " + chatRoom) + setSelectedChatRoom(chatRoom); + }; + + return ( + <> +
+ +
+ + + {/* 선택된 채팅방이 없을 때에만 ChatList를 모달 안에 렌더링 */} + {!selectedChatRoom && } + + {/* 선택된 채팅방이 있을 때 ChatRoomDetail을 표시 */} + {selectedChatRoom && } + + + ); +}; + +export default FloatingButton; diff --git a/src/components/common/Chat/ChatApp.css b/src/components/common/Chat/ChatApp.css new file mode 100644 index 0000000..827a4dc --- /dev/null +++ b/src/components/common/Chat/ChatApp.css @@ -0,0 +1,62 @@ + +.rounded-rectangle { + position: relative; + width: 350px; + height: 600px; + border-radius: 20px; + overflow: hidden; + background-color: #ffffff; + box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.1); +} + +.content { + padding: 20px; + overflow-y: auto; +} + +.bottom-nav { + position: absolute; + bottom: 0; + width: 100%; + background-color: #f0f0f0; + padding: 10px; +} + +.bottom-nav ul { + list-style: none; + padding: 0; + margin: 0; + display: flex; + justify-content: space-around; +} + +.close-button { + position: absolute; + top: 17px; + right: 10px; + padding: 8px 12px; + border: none; + cursor: pointer; + font-size: 15px; +} + +.chat-room:hover { + background-color: #f0f0f0; /* 호버 시 바뀔 배경색을 지정하세요 */ + cursor: pointer; +} + +/* 스크롤바의 너비를 조절합니다. */ +#messagesContainer::-webkit-scrollbar { + width: 8px; /* 웹킷 브라우저에서 스크롤바의 너비를 설정합니다. */ +} + +/* 스크롤바의 색상을 설정합니다. */ +#messagesContainer::-webkit-scrollbar-thumb { + background-color: #3498db; /* 스크롤바 색상 설정 */ + border-radius: 4px; /* 스크롤바의 둥근 모서리를 설정합니다. */ +} + +/* 스크롤바 트랙의 색상을 설정합니다. */ +#messagesContainer::-webkit-scrollbar-track { + background-color: #f1f1f1; /* 스크롤바 트랙 색상 설정 */ +} diff --git a/src/components/common/Chat/FormattedTime.jsx b/src/components/common/Chat/FormattedTime.jsx new file mode 100644 index 0000000..1e9bf1c --- /dev/null +++ b/src/components/common/Chat/FormattedTime.jsx @@ -0,0 +1,19 @@ +import React from 'react'; + +/** + * 시간 + * + * @since 2024.03.04 + * @author 이상민 + */ +const FormattedTime = ({ time }) => { + const dateObject = new Date(time); + const formattedTime = `${(dateObject.getMonth() + 1).toString().padStart(2, '0')}.${dateObject.getDate().toString().padStart(2, '0')} ${dateObject.getHours().toString().padStart(2, '0')}:${dateObject.getMinutes().toString().padStart(2, '0')}`; + + return ( + {formattedTime} + ); +}; + +export default FormattedTime; + diff --git a/src/components/common/Chat/MeChatMessage.jsx b/src/components/common/Chat/MeChatMessage.jsx index 0172786..2958c12 100644 --- a/src/components/common/Chat/MeChatMessage.jsx +++ b/src/components/common/Chat/MeChatMessage.jsx @@ -1,4 +1,5 @@ import React from "react"; +import FormattedTime from "./FormattedTime"; /** * 나의 채팅 메시지 컴포넌트 @@ -7,16 +8,16 @@ import React from "react"; * @author 이상민 */ const MeChatMessage = ({ nickname, time, text }) => { + return (
+ className="flex flex-col w-full max-w-[240px] + leading-1.5 p-4 border-gray-200 rounded-tl-xl rounded-bl-xl rounded-br-xl bg-gray-700" + style={{ height: 'auto' }} >
{nickname} - {time} +

{text}

diff --git a/src/components/common/Chat/OtherChatMessage.jsx b/src/components/common/Chat/OtherChatMessage.jsx index b56b8b2..33dc0e2 100644 --- a/src/components/common/Chat/OtherChatMessage.jsx +++ b/src/components/common/Chat/OtherChatMessage.jsx @@ -1,4 +1,5 @@ import React from "react"; +import FormattedTime from "./FormattedTime"; /** * 나 이외의 채팅 메시지 컴포넌트 @@ -10,13 +11,14 @@ const OtherChatMessage = ({ nickname, time, text }) => { return (
+ className="flex flex-col w-full max-w-[240px] + leading-1.5 p-4 border-gray-200 bg-gray-100 rounded-e-xl rounded-es-xl" + style={{ height: 'auto' }} >
{nickname} - {time} +

{text}

- Delivered
); diff --git a/src/components/common/InfoList/RankingList.js b/src/components/common/InfoList/RankingList.js index 025fe64..bd038e8 100644 --- a/src/components/common/InfoList/RankingList.js +++ b/src/components/common/InfoList/RankingList.js @@ -9,16 +9,16 @@ export default RankingList; function RankingList(props) { return (
-
+
-
+
{props.id}위
-
+
{props.title}
-
+
{formatNumberWithCommas(props.content)} 회
diff --git a/src/components/common/Table/ChatTable.jsx b/src/components/common/Table/ChatTable.jsx deleted file mode 100644 index 99ded25..0000000 --- a/src/components/common/Table/ChatTable.jsx +++ /dev/null @@ -1,42 +0,0 @@ -import React from "react"; -import TableHeader from "./TableHeader"; -import TableRow from "./TableRow"; - -/** - * Table 컴포넌트 생성 - * - * @since 2024.02.25 - * @author 이상민 - */ -const ChatTable = ({ headerTitles, sampleData }) => { - - const handleRowClick = (id) => { - const currentUrl = window.location.pathname; - const newUrl = `${currentUrl}/${id}`; - // 팝업으로 새로운 페이지 열기 - window.open(newUrl, '_blank', 'width=400,height=700,toolbar=no,scrollbars=yes,resizable=yes'); - - // const currentUrl = window.location.pathname; - // window.location.href = `${currentUrl}/${id}`; - }; - - const renderTableBody = (sampleData) => ( - - {sampleData.map((rowData, index) => ( - handleRowClick(rowData[0])} /> - ))} - - ); - - return ( -
- - - {renderTableBody(sampleData)} -
-
- ); - }; - - export default ChatTable; - diff --git a/src/components/common/Table/MyPostListTable.js b/src/components/common/Table/MyPostListTable.js index 7aae700..0ad9088 100644 --- a/src/components/common/Table/MyPostListTable.js +++ b/src/components/common/Table/MyPostListTable.js @@ -23,17 +23,16 @@ function MyPostListTable({ posts }) { <> - {displayedData.map((post, index) => ( - - - - - - ))} + {displayedData.map((post, index) => ( + + + + + ))}
-
{index + 1}
-
-
{post.title}
-
{post.pulledDate}
+
{index + 1}
+
+
{post.title}
+
diff --git a/src/components/common/Table/Table.jsx b/src/components/common/Table/Table.jsx index 4b320c7..c3b0869 100644 --- a/src/components/common/Table/Table.jsx +++ b/src/components/common/Table/Table.jsx @@ -22,16 +22,15 @@ const Table = ({ headerTitles, sampleData }) => { ))} ); - + return (
- - - {renderTableBody(sampleData)} -
+ + + {renderTableBody(sampleData)} +
- ); - }; - - export default Table; - + ); +}; + +export default Table; diff --git a/src/components/common/Table/TableRow.jsx b/src/components/common/Table/TableRow.jsx index 2064ca0..1546687 100644 --- a/src/components/common/Table/TableRow.jsx +++ b/src/components/common/Table/TableRow.jsx @@ -26,4 +26,4 @@ const TableRow = ({ rowData, onRowClick }) => { } }; -export default TableRow; \ No newline at end of file +export default TableRow; diff --git a/src/pages/administrator/adminChat.jsx b/src/pages/administrator/adminChat.jsx index 405e121..43ec963 100644 --- a/src/pages/administrator/adminChat.jsx +++ b/src/pages/administrator/adminChat.jsx @@ -1,6 +1,6 @@ import React from "react"; import AdminChatApi from "../../api/administrator/adminChatApi"; -import ChatPage from "../../components/common/Chat/ChatPage"; +import ChatPage from "../../components/administrator/ChatPage"; /** * 채팅 페이지 제작 diff --git a/src/pages/administrator/adminChatRoom.jsx b/src/pages/administrator/adminChatRoom.jsx deleted file mode 100644 index f1c7b87..0000000 --- a/src/pages/administrator/adminChatRoom.jsx +++ /dev/null @@ -1,16 +0,0 @@ -import React from "react"; -import ChatRoom from "../../components/common/Chat/ChatRoom"; - -/** - * 관리자 채팅방 - * - * @since 2024.03.03 - * @author 이상민 - */ -const AdminChatRoom = () => { - return ( - - ); -}; - -export default AdminChatRoom; diff --git a/src/pages/business/ad.jsx b/src/pages/business/ad.jsx index b96fd39..083acab 100644 --- a/src/pages/business/ad.jsx +++ b/src/pages/business/ad.jsx @@ -13,6 +13,7 @@ import MyCommunityApi from "../../api/business/createAd/myCommunityApi"; import FileUpload from "../../components/common/Input/FileUpload"; import MyPopupApi from "../../api/business/createAd/myPopupApi"; import Button from "../../components/common/Button/Button"; +import FloatingButton from "../../components/business/FloatingButton/FloatingButton"; /** * Ad 페이지 제작 @@ -103,6 +104,7 @@ const Ad = () => {
diff --git a/src/pages/business/businessChatRoom.jsx b/src/pages/business/businessChatRoom.jsx deleted file mode 100644 index 75bf8a7..0000000 --- a/src/pages/business/businessChatRoom.jsx +++ /dev/null @@ -1,16 +0,0 @@ -import React from "react"; -import ChatRoom from "../../components/common/Chat/ChatRoom"; - -/** - * 사업체 채팅방 - * - * @since 2024.03.03 - * @author 이상민 - */ -const BusinessChatRoom = () => { - return ( - - ); -}; - -export default BusinessChatRoom; diff --git a/src/pages/business/chat.jsx b/src/pages/business/chat.jsx deleted file mode 100644 index e6b872f..0000000 --- a/src/pages/business/chat.jsx +++ /dev/null @@ -1,20 +0,0 @@ -import React from "react"; -import ChatApi from "../../api/chatApi"; -import ChatPage from "../../components/common/Chat/ChatPage"; - -/** - * 채팅 페이지 제작 - * - * @since 2024.03.02 - * @author 이상민 - */ -const Chat = () => { - return ( - - ); -}; - -export default Chat; diff --git a/src/pages/business/createPlan.jsx b/src/pages/business/createPlan.jsx index 4eff523..b6876db 100644 --- a/src/pages/business/createPlan.jsx +++ b/src/pages/business/createPlan.jsx @@ -7,7 +7,8 @@ import DepartmentDropdown from "../../components/business/DepartmentDropdown/Dep import FloorDropdown from "../../components/business/FloorDropdown/FloorDropdown"; import Button from "../../components/common/Button/Button"; import PlanApi from "../../api/business/createPlan/planApi"; -import {useRef, useState} from "react"; +import React, {useRef, useState} from "react"; +import FloatingButton from "../../components/business/FloatingButton/FloatingButton"; /** * CreatePlan 페이지 제작 @@ -86,6 +87,7 @@ const PlanContentBox = ({onOpenDateChange, onCloseDateChange, onPhoneNumberChang secondInput={}/> +
) } diff --git a/src/pages/business/dashboard.jsx b/src/pages/business/dashboard.jsx index 5c3cacc..48c8899 100644 --- a/src/pages/business/dashboard.jsx +++ b/src/pages/business/dashboard.jsx @@ -5,6 +5,7 @@ import PopupCurrent from "../../components/business/DashBoard/PopupCurrent"; import PopupRanking from "../../components/business/DashBoard/PopupRanking"; import PopupStatistics from "../../components/business/DashBoard/PopupStatistics"; import PopupPostList from "../../components/business/DashBoard/PopupPostList"; +import FloatingButton from "../../components/business/FloatingButton/FloatingButton"; /** * DashBoard 페이지 제작 @@ -13,6 +14,7 @@ import PopupPostList from "../../components/business/DashBoard/PopupPostList"; * @author 이승민 */ const DashBoard = () => { + return (
@@ -32,6 +34,7 @@ const DashBoard = () => { }/>
+
) } diff --git a/src/pages/business/plan.jsx b/src/pages/business/plan.jsx index 5b07d5e..e97bcf1 100644 --- a/src/pages/business/plan.jsx +++ b/src/pages/business/plan.jsx @@ -14,6 +14,7 @@ import colorSyntax from '@toast-ui/editor-plugin-color-syntax'; import 'tui-color-picker/dist/tui-color-picker.css'; import '@toast-ui/editor-plugin-color-syntax/dist/toastui-editor-plugin-color-syntax.css'; import './planViewer.css'; +import FloatingButton from "../../components/business/FloatingButton/FloatingButton"; /** * Plan 페이지 제작 @@ -102,6 +103,7 @@ const Plan = () => { }/> }/> +
); }; diff --git a/src/pages/business/plans.jsx b/src/pages/business/plans.jsx index 3aef05d..cd529bf 100644 --- a/src/pages/business/plans.jsx +++ b/src/pages/business/plans.jsx @@ -2,9 +2,10 @@ import ContentBox from "../../components/common/ContentBox/ContentBox"; import CategoryDropdown from "../../components/business/CategoryDropdown/CategoryDropdown"; import EntranceStatusDropdown from "../../components/business/EntranceStatusDropdown/EntranceStatusDropdown"; import MyPlanTable from "../../components/business/MyPlanTable/MyPlanTable"; -import {useEffect, useState} from "react"; +import React, {useEffect, useState} from "react"; import MyPlansApi from "../../api/business/plans/myPlansApi"; import SearchButton from "../../components/common/Button/SearchButton"; +import FloatingButton from "../../components/business/FloatingButton/FloatingButton"; /** * Plans 페이지 제작 @@ -38,6 +39,7 @@ function Plans() { onClick={getPlans(category, entranceStatus, page, limit, setTotal, setPlans)}/>
+ }/>