diff --git a/auction-server/.env-example b/auction-server/.env-example deleted file mode 100644 index e8460690..00000000 --- a/auction-server/.env-example +++ /dev/null @@ -1,10 +0,0 @@ -DB_TYPE=mysql -DB_HOST=honeybee.palda.shop -DB_DOCKER_COMPOSE_SERVICE_HOST=mysql-server -DB_PORT=3306 -DB_USER=boost -DB_PASSWORD=boost -DB_NAME=palda - -CHAT_SERVER=http://chat.palda.shop:4000 -CHAT_DEV_SERVER=http://chat-server:4000 diff --git a/auction-server/.env.example b/auction-server/.env.example index 518cae70..db9c8f36 100644 --- a/auction-server/.env.example +++ b/auction-server/.env.example @@ -1,3 +1,15 @@ +DB_TYPE= +DB_HOST= +DB_DOCKER_COMPOSE_SERVICE_HOST= +DB_PORT= +DB_USER= +DB_PASSWORD= +DB_NAME= + +CHAT_SERVER= +CHAT_DEV_SERVER= + + ### Mail MAIL_ID= MAIL_PASSWORD= diff --git a/auction-server/index.js b/auction-server/index.js index 4d1cdaad..8001eefa 100644 --- a/auction-server/index.js +++ b/auction-server/index.js @@ -55,7 +55,7 @@ let queue = []; //1분 반복 예시 "*/1 * * * *" //[작업 할당 클론] -cron.schedule("*/10 * * * * *", () => { +cron.schedule("*/2 * * * * *", () => { const now = moment().format("YYYY-MM-DD HH:mm:ss"); console.log(now); pool.query( @@ -87,7 +87,7 @@ cron.schedule("*/10 * * * * *", () => { }); // [작업 처리 클론] -cron.schedule("*/10 * * * * *", () => { +cron.schedule("*/2 * * * * *", () => { if (queue.length === 0) return console.log("QUEUE IS EMPTY"); console.dir(queue); console.dir(queue.length); diff --git a/auction-server/mailService.js b/auction-server/mailService.js index c3448ca4..ecbe2b74 100644 --- a/auction-server/mailService.js +++ b/auction-server/mailService.js @@ -1,20 +1,26 @@ -const axios = require("axios") +const axios = require("axios"); const mailTemplate = (isSeller, isSold, content) => `

세상의 모든 중고 경매 팔다 알림메일 입니다.

${isSeller ? "등록하신" : "입찰하신"} ${content}이 ${ - isSeller ? (isSold ? "판매되었" : "유찰되었") : isSold ? "낙찰되었" : "유찰되었" - }습니다. + isSeller + ? isSold + ? "판매되었" + : "유찰되었" + : isSold + ? "낙찰되었" + : "유찰되었" + }습니다.
팔다에 접속해 확인하시기 바랍니다.
-` +`; const mailTitleTemplate = (isSeller, isSold) => { - if (isSeller) return `${isSold ? "판매" : "유찰"} 되었습니다.` - return `${isSold ? "낙찰" : "유찰"} 되었습니다.` -} + if (isSeller) return `${isSold ? "판매" : "유찰"} 되었습니다.`; + return `${isSold ? "낙찰" : "유찰"} 되었습니다.`; +}; const mailFooter = ` 중고 물건은 이곳 팔다에서! http://palda.shop - ` + `; const user = { id: process.env.MAIL_ID, password: process.env.MAIL_PASSWORD -} +}; const mailService = (toEmail, content, isSeller, isSold) => { if (!toEmail) return; // text html 택1 (html이 우선순위 높음) const mail = { to: toEmail, - subject: `[팔다] 상품 ${content}이(가) ${mailTitleTemplate(isSeller, isSold)}`, + subject: `[팔다] 상품 ${content}이(가) ${mailTitleTemplate( + isSeller, + isSold + )}`, html: mailTemplate(isSeller, isSold, content) + mailFooter - } + }; const instance = axios.create({ baseURL: process.env.MAIL_BASE, @@ -55,44 +64,49 @@ const mailService = (toEmail, content, isSeller, isSold) => { "content-type": "application/json", Accept: "application/json" } - }) + }); - let cookie = "" + let cookie = ""; const getCookie = async () => { - const response = await instance.post("/auth/login", user) - cookie = response.headers["set-cookie"][0] - } + const response = await instance.post("/auth/login", user); + cookie = response.headers["set-cookie"][0]; + }; const sendMail = () => instance.post("/mail", mail, { headers: { Cookie: cookie } - }) + }); const run = async () => { try { - await sendMail() - return true + await sendMail(); + return true; } catch (error) { if (error.response.status !== 401) { - return error + return error; } } - await getCookie() - const result = await sendMail() - } + await getCookie(); + const result = await sendMail(); + }; - run() -} + run(); +}; const sendMail = (pool, userid, title, isSeller, isSold) => { - pool.query('select email from users where id = ?', [userid], + pool.query( + "select email from users where id = ?", + [userid], (err, row, field) => { if (!row) return; - mailService(row[0].email, title, isSeller, isSold) - }) -} + if (!row[0]) return; + if (!row[0].email) return; + mailService(row[0].email, title, isSeller, isSold); + } + ); +}; -module.exports = { sendMail } +module.exports = { sendMail }; diff --git a/chat-server/index.js b/chat-server/index.js index 5d160490..ca0b9664 100644 --- a/chat-server/index.js +++ b/chat-server/index.js @@ -70,13 +70,17 @@ io.on("connection", function(socket) { userMapper[userId] = sessionId; }); - socket.on("leaveRoom", (roomId, sessionId, userId) => { + socket.on("leaveRoom", ({ roomId, sessionId, user }) => { console.log( - `##### USER(${sessionId}, ${userId})가 ${roomId}방에서 나감 #####` + `##### USER(${sessionId}, ${user.id})가 ${roomId}방에서 나감 #####` ); socket.leave(roomId, () => { //rooms 정보에서 삭제한다. delete rooms[roomId][sessionId]; }); }); + + socket.on("disconnect", () => { + console.log("DISCONNECTED"); + }); }); diff --git a/client/src/App.js b/client/src/App.js index 522799a0..bb8ce2be 100644 --- a/client/src/App.js +++ b/client/src/App.js @@ -7,6 +7,8 @@ import ModalContext from "./context/ModalContext"; import { Modal } from "../src/components/Molecules/CustomModal/Modal"; import NotificationContext from "./context/NotificationContext"; import MessengerContext from "./context/MessengerContext"; +import SocketContext from "./context/SocketContext"; +import { useSocket } from "./hooks/useSocket"; function App() { const [user, setUser] = useState({}); @@ -17,20 +19,23 @@ function App() { }); const [notifications, setNotifications] = useState([]); const [messengerOpen, setMessengerOpen] = useState(false); + const { socket } = useSocket(user, setNotifications); return ( - - {modal.isOpen ? : null} -
- -
-
+ + + {modal.isOpen ? : null} +
+ +
+
+
diff --git a/client/src/components/Molecules/CustomModal/Modal.jsx b/client/src/components/Molecules/CustomModal/Modal.jsx index f10f8375..de5be4d2 100644 --- a/client/src/components/Molecules/CustomModal/Modal.jsx +++ b/client/src/components/Molecules/CustomModal/Modal.jsx @@ -1,4 +1,4 @@ -import React, { useContext } from "react"; +import React, { useEffect, useContext } from "react"; import styled from "styled-components"; import ModalContext from "../../../context/ModalContext"; diff --git a/client/src/components/Molecules/NotifyList/NotifyItem.jsx b/client/src/components/Molecules/NotifyList/NotifyItem.jsx index 970015dd..b7e7c7d7 100644 --- a/client/src/components/Molecules/NotifyList/NotifyItem.jsx +++ b/client/src/components/Molecules/NotifyList/NotifyItem.jsx @@ -10,7 +10,8 @@ const NotifyItemStyle = styled.div` const NotifyBadge = styled.div` width: 52px; - background: ${props => (props.success ? "var(--color-success)" : "var(--color-primary)")}; + background: ${props => + props.success ? "var(--color-success)" : "var(--color-primary)"}; padding: var(--padding-sm); color: white; font-weight: bold; @@ -22,7 +23,9 @@ const NotifyContent = styled.div` width: 100%; display: flex; border: 1px solid var(--color-gray); - /* border-color: ${props => (props.success ? "var(--color-success)" : "var(--color-primary)")}; */ + background-color:white; + /* border-color: ${props => + props.success ? "var(--color-success)" : "var(--color-primary)"}; */ `; const NotifyLeftContent = styled.div` @@ -84,9 +87,9 @@ export const NotifyItem = props => { return ( {isSuccess(type) ? ( - 구매 성공 + 성공 ) : ( - 구매 실패 + 실패 )} diff --git a/client/src/components/Molecules/NotifyList/index.jsx b/client/src/components/Molecules/NotifyList/index.jsx index b2b98b62..fd978162 100644 --- a/client/src/components/Molecules/NotifyList/index.jsx +++ b/client/src/components/Molecules/NotifyList/index.jsx @@ -31,7 +31,7 @@ function Component() { {notifications.map(noti => { return ( ); diff --git a/client/src/components/Molecules/ShareBox/index.jsx b/client/src/components/Molecules/ShareBox/index.jsx index d3c37f3c..28f58ca0 100644 --- a/client/src/components/Molecules/ShareBox/index.jsx +++ b/client/src/components/Molecules/ShareBox/index.jsx @@ -109,7 +109,7 @@ const Component = ({ url, object, width }) => { - + alert("URL이 복사되었습니다.")} /> URL diff --git a/client/src/components/Organism/Chat/Chat.jsx b/client/src/components/Organism/Chat/Chat.jsx index 92caec33..c894b225 100644 --- a/client/src/components/Organism/Chat/Chat.jsx +++ b/client/src/components/Organism/Chat/Chat.jsx @@ -30,7 +30,7 @@ const SellerIdText = styled.div` `; const MessageText = styled.div` font-size: 0.8rem; - word-break: break-all; + word-break: break-word; `; const Chat = ({ chat, isSeller }) => { diff --git a/client/src/components/Organism/Chat/ChatBox.jsx b/client/src/components/Organism/Chat/ChatBox.jsx index 16eeb950..c5f35646 100644 --- a/client/src/components/Organism/Chat/ChatBox.jsx +++ b/client/src/components/Organism/Chat/ChatBox.jsx @@ -5,6 +5,7 @@ import ChatSend from "./ChatSend"; import ModalContext from "../../../context/ModalContext"; import FailModal from "../../Molecules/CustomModal/FailModal"; import ProductPageContext from "../../../context/ProductPageContext"; +import SocketContext from "../../../context/SocketContext"; const ChatBoxStyle = styled.div` display: flex; @@ -61,8 +62,9 @@ const ChatAlertWithPurchase = styled(ChatAlertWithBid)` const ChatBox = ({ productId, sellerId, user }) => { const chatBodyRef = useRef(); + const { socket } = useContext(SocketContext); const [productPageState] = useContext(ProductPageContext); - const { socketClient, chats } = productPageState; + const { chats } = productPageState; const [modal, setModal] = useContext(ModalContext); @@ -73,6 +75,10 @@ const ChatBox = ({ productId, sellerId, user }) => { const onSubmit = e => { e.preventDefault(); + const text = e.target.message.value; + + if (text === "" || text.trim() === "") return; + if (Object.keys(user).length === 0) { return setModal({ isOpen: true, @@ -80,11 +86,11 @@ const ChatBox = ({ productId, sellerId, user }) => { props: { message: "로그인이 필요합니다." } }); } - socketClient.emit("message", { + socket.emit("message", { roomId: productId, - sender: { ...user, sessionId: socketClient.id }, + sender: { ...user, sessionId: socket.id }, type: "message", - text: e.target.message.value, + text, createdAt: Date.now() }); @@ -98,7 +104,11 @@ const ChatBox = ({ productId, sellerId, user }) => { {chats.map(chat => { return chat.type === "message" ? ( - + ) : ( {chat.type === "bid" ? ( diff --git a/client/src/components/Organism/ProductInfo/index.jsx b/client/src/components/Organism/ProductInfo/index.jsx index a4872cc7..7e1f1468 100644 --- a/client/src/components/Organism/ProductInfo/index.jsx +++ b/client/src/components/Organism/ProductInfo/index.jsx @@ -16,6 +16,7 @@ import ReportButton from "../../Atoms/ReportButton"; import MessengerCreateButton from "../../Messenger/CreateButton"; import Carousel from "../../Molecules/Carousel"; import { getNowDateTime, isTerminated } from "../../../utils/dateUtil"; +import SocketContext from "../../../context/SocketContext"; const { apiUrl } = apiConfig; @@ -195,11 +196,11 @@ const ShareWrapper = styled.div` const ProductInfo = () => { const [user] = useContext(UserContext); - + const { socket } = useContext(SocketContext); const [productPageState, dispatchProductPage] = useContext( ProductPageContext ); - const { socketClient, product, chats, bids } = productPageState; + const { product, chats, bids } = productPageState; const [modal, setModal] = useContext(ModalContext); /* @@ -298,7 +299,7 @@ const ProductInfo = () => { axios .post(`${baseURL}${pathConfig.bids}`, params) .then(response => { - socketClient.emit("bid", { + socket.emit("bid", { type: "bid", roomId: id, sender: { ...user, sessionId: user.sessionId }, @@ -366,9 +367,9 @@ const ProductInfo = () => { axios .patch(`${baseURL}${pathConfig.products}/${id}`, params) .then(response => { - socketClient.emit("purchase", { + socket.emit("purchase", { roomId: id, - sender: { ...user, sessionId: socketClient.id }, + sender: { ...user, sessionId: socket.id }, sold: response.data, createdAt: getNowDateTime() }); diff --git a/client/src/context/SocketContext.js b/client/src/context/SocketContext.js new file mode 100644 index 00000000..96ab088c --- /dev/null +++ b/client/src/context/SocketContext.js @@ -0,0 +1,5 @@ +import React from "react"; + +const SocketContext = React.createContext(); + +export default SocketContext; diff --git a/client/src/hooks/useSocket.js b/client/src/hooks/useSocket.js new file mode 100644 index 00000000..022f13a7 --- /dev/null +++ b/client/src/hooks/useSocket.js @@ -0,0 +1,29 @@ +import React, { useState, useEffect } from "react"; +import io from "socket.io-client"; +import apiConfig from "../config/api"; + +const { chatUrl } = apiConfig; + +export const useSocket = (user, setNotifications) => { + const [socket, setSocket] = useState(null); + + useEffect(() => { + if (Object.keys(user).length === 0) return; + + const socket = io(chatUrl); + + socket.on("connect", () => { + setSocket(socket); + }); + + socket.on("auctionResult", ({ type, product }) => { + setNotifications(notis => [...notis, { type, product }]); + }); + + return () => { + socket.close(); + }; + }, [user]); + + return { socket }; +}; diff --git a/client/src/pages/Product/index.jsx b/client/src/pages/Product/index.jsx index 65b7bf3c..be473ced 100644 --- a/client/src/pages/Product/index.jsx +++ b/client/src/pages/Product/index.jsx @@ -8,15 +8,15 @@ import { useFetch } from "../../hooks/useFetch"; import pathConfig from "../../config/path"; import UserContext from "../../context/UserContext"; import apiConfig from "../../config/api"; -import io from "socket.io-client"; import ProductPageContext from "../../context/ProductPageContext"; import { convert2Price } from "../../utils/converter"; import NotificationContext from "../../context/NotificationContext"; import { getFetch } from "../../services/fetchService"; import SmallCardContainer from "../../components/Molecules/SmallCardContainer"; import ErrorPage from "../../pages/ErrorPage"; +import SocketContext from "../../context/SocketContext"; -const { chatUrl, apiUrl } = apiConfig; +const { apiUrl } = apiConfig; const ProductPageStyle = styled.div` display: flex; @@ -48,8 +48,7 @@ const initialProductPageState = { loading: true, product: {}, bids: [], - chats: [], - socketClient: null + chats: [] }; const productPageReducer = (state, action) => { @@ -80,8 +79,6 @@ const productPageReducer = (state, action) => { ...state, chats: [...state.chats, action.chat] }; - case "SET_SOCKET": - return { ...state, socketClient: action.socket }; case "UPDATE_PRODUCT": return { ...state, product: { ...state.product, ...action.product } }; default: @@ -89,16 +86,17 @@ const productPageReducer = (state, action) => { } }; -const DEFAULT_PROFILE_URL = "https://kr.object.ncloudstorage.com/palda/img/default-profile-img.jpg"; +const DEFAULT_PROFILE_URL = + "https://kr.object.ncloudstorage.com/palda/img/default-profile-img.jpg"; const ProductPage = ({ match }) => { + const [user] = useContext(UserContext); + const { socket } = useContext(SocketContext); + const [, setNotifications] = useContext(NotificationContext); const [productPageState, dispatchProductPage] = useReducer( productPageReducer, initialProductPageState ); - - const [user] = useContext(UserContext); - const [notifications, setNotifications] = useContext(NotificationContext); const [relatedItemList, setRelatedItemList] = useState([]); const productId = match.params.id; @@ -116,29 +114,19 @@ const ProductPage = ({ match }) => { dispatchProductPage({ type: "FETCH_ERROR", error }); }; - useFetch(`${pathConfig.productsWithBids}/${productId}`, handleFetchSuccess, handleFetchError); - - const getRelatedItemList = async () => { - if (!productPageState.loading) { - setRelatedItemList([]); - const { categoryCode, id } = productPageState.product; - const url = `${apiUrl}${pathConfig.items.related}/${categoryCode}/${id}`; - let result = await getFetch(url, {}, {}); - - setRelatedItemList(result[0]); - } - }; + useFetch( + `${pathConfig.productsWithBids}/${productId}`, + handleFetchSuccess, + handleFetchError + ); useEffect(() => { - if (Object.keys(user).length === 0) return; - const socket = io(chatUrl); - socket.on("connect", () => { - dispatchProductPage({ type: "SET_SOCKET", socket }); - socket.emit("joinRoom", { - roomId: productId, - sessionId: socket.id, - user: user - }); + if (!socket) return; + + socket.emit("joinRoom", { + roomId: productId, + sessionId: socket.id, + user: user }); socket.on("message", ({ roomId, sender, type, text, createdAt }) => { @@ -160,7 +148,9 @@ const ProductPage = ({ match }) => { sessionId: sender.sessionId, id: sender.loginId, src: sender.profileUrl || DEFAULT_PROFILE_URL, - text: `${sender.name}님께서 ${convert2Price(bid.bidPrice)}원에 입찰 하셨습니다.`, + text: `${sender.name}님께서 ${convert2Price( + bid.bidPrice + )}원에 입찰 하셨습니다.`, key: `B.${createdAt}.${sender.id}` }; @@ -173,7 +163,9 @@ const ProductPage = ({ match }) => { sessionId: sender.sessionId, id: sender.loginId, src: sender.profileUrl || DEFAULT_PROFILE_URL, - text: `${sender.name}님이 ${convert2Price(sold.soldPrice)}원에 즉시 구매하셨습니다.`, + text: `${sender.name}님이 ${convert2Price( + sold.soldPrice + )}원에 즉시 구매하셨습니다.`, key: `P.${createdAt}.${sender.id}` }; @@ -182,22 +174,25 @@ const ProductPage = ({ match }) => { return dispatchProductPage({ type: "ADD_PURCHASE", chat, product }); }); - socket.on("auctionResult", ({ type, product }) => { - setNotifications(notis => [...notis, { type, product }]); - }); - - socket.on("joinRoom", message => { - // console.log(message); - }); - - socket.on("disconnect", reason => { - // console.log(reason); - }); - return () => { - socket.close(); + socket.emit("leaveRoom", { + roomId: productId, + sessionId: socket.id, + user: user + }); }; - }, [user, chatUrl, dispatchProductPage]); + }, [socket]); + + const getRelatedItemList = async () => { + if (!productPageState.loading) { + setRelatedItemList([]); + const { categoryCode, id } = productPageState.product; + const url = `${apiUrl}${pathConfig.items.related}/${categoryCode}/${id}`; + let result = await getFetch(url, {}, {}); + + setRelatedItemList(result[0]); + } + }; useEffect(() => { getRelatedItemList(); @@ -206,11 +201,15 @@ const ProductPage = ({ match }) => { if (productPageState.error) { return ; } - const sellerId = productPageState.product.seller ? productPageState.product.seller.id : undefined; + const sellerId = productPageState.product.seller + ? productPageState.product.seller.id + : undefined; return productPageState.loading ? ( ) : ( - +
@@ -222,11 +221,19 @@ const ProductPage = ({ match }) => {
) : null}
- +
- +