From e329d9a1c20f490950784db1ccef36bc00e65442 Mon Sep 17 00:00:00 2001 From: STak <59037261+profornnan@users.noreply.github.com> Date: Fri, 11 Dec 2020 12:16:34 +0900 Subject: [PATCH 1/4] =?UTF-8?q?feat:=20Browse=20Page=20=EC=B1=84=EB=84=90?= =?UTF-8?q?=20=EB=AA=A9=EB=A1=9D=20=EA=B5=AC=ED=98=84=20(#183)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: Browse Page 채널 목록 구현 - Browse Page 접속 시 채널 목록 가져오기 - Browse Page 컴포넌트 CSS 수정 * feat: Browse Page UI 수정 --- .../BrowsePageChannelBody.stories.tsx | 2 +- .../BrowsePageChannelBody.tsx | 6 +-- .../BrowsePageChannelHeader.stories.tsx | 2 +- .../BrowsePageChannelHeader.tsx | 6 +-- .../BrowsePageControls/BrowsePageControls.tsx | 6 ++- .../BrowsePageSearchBar.tsx | 2 +- .../BrowsePageChannel.stories.tsx | 4 +- .../BrowsePageChannel/BrowsePageChannel.tsx | 15 +++---- .../BrowsePageChannelList.stories.tsx | 39 +++++++++++++++++++ .../BrowsePageChannelList.tsx | 34 ++++++++++++++++ client/src/components/organisms/index.ts | 14 ++++++- .../pages/ChannelBrowser/ChannelBrowser.tsx | 20 ++++++++-- 12 files changed, 127 insertions(+), 23 deletions(-) create mode 100644 client/src/components/organisms/BrowsePageChannelList/BrowsePageChannelList.stories.tsx create mode 100644 client/src/components/organisms/BrowsePageChannelList/BrowsePageChannelList.tsx diff --git a/client/src/components/molecules/BrowsePageChannelBody/BrowsePageChannelBody.stories.tsx b/client/src/components/molecules/BrowsePageChannelBody/BrowsePageChannelBody.stories.tsx index a954773..c72c31c 100644 --- a/client/src/components/molecules/BrowsePageChannelBody/BrowsePageChannelBody.stories.tsx +++ b/client/src/components/molecules/BrowsePageChannelBody/BrowsePageChannelBody.stories.tsx @@ -12,6 +12,6 @@ const Template: Story = (args) => ` } `; -const BrowsePageChannelBody: React.FC = ({ isJoined, memberCount, description, ...props }) => { +const BrowsePageChannelBody: React.FC = ({ isJoined, members, description, ...props }) => { return ( {isJoined && ( @@ -25,7 +25,7 @@ const BrowsePageChannelBody: React.FC = ({ isJoined, )} - {`${memberCount} members`} + {`${members} members`} {description && ( diff --git a/client/src/components/molecules/BrowsePageChannelHeader/BrowsePageChannelHeader.stories.tsx b/client/src/components/molecules/BrowsePageChannelHeader/BrowsePageChannelHeader.stories.tsx index e4aa9df..85632ae 100644 --- a/client/src/components/molecules/BrowsePageChannelHeader/BrowsePageChannelHeader.stories.tsx +++ b/client/src/components/molecules/BrowsePageChannelHeader/BrowsePageChannelHeader.stories.tsx @@ -11,6 +11,6 @@ const Template: Story = (args) => ` } `; -const BrowsePageChannelHeader: React.FC = ({ name, isPrivate, ...props }) => { +const BrowsePageChannelHeader: React.FC = ({ title, isPrivate, ...props }) => { return ( - {name} + {title} ); diff --git a/client/src/components/molecules/BrowsePageControls/BrowsePageControls.tsx b/client/src/components/molecules/BrowsePageControls/BrowsePageControls.tsx index c010b73..831a22d 100644 --- a/client/src/components/molecules/BrowsePageControls/BrowsePageControls.tsx +++ b/client/src/components/molecules/BrowsePageControls/BrowsePageControls.tsx @@ -24,7 +24,11 @@ const BrowsePageControlsWrap = styled.div` display: flex; justify-content: space-between; align-items: center; - padding: 0.4rem 1.5rem; + padding-top: 0.4rem; + padding-bottom: 1rem; + margin-left: 1.5rem; + margin-right: 2.5rem; + border-bottom: 0.5px solid ${color.border_secondary}; `; const BrowsePageControlsButtonWrap = styled.div` diff --git a/client/src/components/molecules/BrowsePageSearchBar/BrowsePageSearchBar.tsx b/client/src/components/molecules/BrowsePageSearchBar/BrowsePageSearchBar.tsx index 7eec056..d255900 100644 --- a/client/src/components/molecules/BrowsePageSearchBar/BrowsePageSearchBar.tsx +++ b/client/src/components/molecules/BrowsePageSearchBar/BrowsePageSearchBar.tsx @@ -20,7 +20,7 @@ const StyledInput = styled.input` border: 0 none; outline: none; width: fill-available; - font-size: 0.8rem; + font-size: 0.9rem; `; const InputHintWrap = styled.div` diff --git a/client/src/components/organisms/BrowsePageChannel/BrowsePageChannel.stories.tsx b/client/src/components/organisms/BrowsePageChannel/BrowsePageChannel.stories.tsx index 1842163..8f64527 100644 --- a/client/src/components/organisms/BrowsePageChannel/BrowsePageChannel.stories.tsx +++ b/client/src/components/organisms/BrowsePageChannel/BrowsePageChannel.stories.tsx @@ -14,9 +14,9 @@ const handlingLeaveButton = () => {}; export const BlackBrowsePageChannel = Template.bind({}); BlackBrowsePageChannel.args = { - name: 'notice', + title: 'notice', isJoined: true, - memberCount: 4, + members: 4, description: '공지사항을 안내하는 채널', isPrivate: true, handlingJoinButton, diff --git a/client/src/components/organisms/BrowsePageChannel/BrowsePageChannel.tsx b/client/src/components/organisms/BrowsePageChannel/BrowsePageChannel.tsx index e145eab..652a35a 100644 --- a/client/src/components/organisms/BrowsePageChannel/BrowsePageChannel.tsx +++ b/client/src/components/organisms/BrowsePageChannel/BrowsePageChannel.tsx @@ -6,11 +6,11 @@ import { BrowsePageChannelBody } from '@components/molecules/BrowsePageChannelBo import { BrowsePageChannelButton } from '@components/molecules/BrowsePageChannelButton/BrowsePageChannelButton'; interface BrowsePageChannelProps { - name: string; - isJoined?: boolean; - memberCount: number; + title: string; description?: string; isPrivate?: boolean; + members: number; + isJoined?: boolean; handlingJoinButton?: () => void; handlingLeaveButton?: () => void; } @@ -19,6 +19,7 @@ const BrowsePageChannelContainer = styled.div` display: flex; justify-content: space-between; padding: 1rem 1rem; + border-bottom: 1px solid ${color.border_secondary}; &:hover { background-color: ${color.hover_primary}; button { @@ -43,9 +44,9 @@ const ButtonWrap = styled.div` `; const BrowsePageChannel: React.FC = ({ - name, + title, isJoined, - memberCount, + members, description, isPrivate, handlingJoinButton, @@ -55,8 +56,8 @@ const BrowsePageChannel: React.FC = ({ return ( - - + + = (args) => ; + +export const BlackBrowsePageChannelList = Template.bind({}); +BlackBrowsePageChannelList.args = { + channels: [ + { + channelId: 1, + title: 'notice', + description: '공지사항을 안내하는 채널', + isPrivate: false, + members: 110, + isJoined: true + }, + { + channelId: 2, + title: '질의응답', + isPrivate: false, + members: 10, + isJoined: false + }, + { + channelId: 3, + title: 'black', + description: 'black', + isPrivate: true, + members: 3, + isJoined: true + } + ] +}; diff --git a/client/src/components/organisms/BrowsePageChannelList/BrowsePageChannelList.tsx b/client/src/components/organisms/BrowsePageChannelList/BrowsePageChannelList.tsx new file mode 100644 index 0000000..acdcfac --- /dev/null +++ b/client/src/components/organisms/BrowsePageChannelList/BrowsePageChannelList.tsx @@ -0,0 +1,34 @@ +import React from 'react'; +import styled from 'styled-components'; +import { BrowsePageChannel } from '@components/organisms'; + +interface BrowsePageChannelListProps { + channels: Array; +} + +const BrowsePageChannelListContainter = styled.div` + display: flex; + flex-direction: column; + overflow-y: scroll; + padding: 0rem 1.5rem; + height: 71%; +`; + +const BrowsePageChannelList: React.FC = ({ channels, ...props }) => { + const createMessages = () => { + return channels.map((channel: any) => ( + + )); + }; + + return {createMessages()}; +}; + +export { BrowsePageChannelList, BrowsePageChannelListProps }; diff --git a/client/src/components/organisms/index.ts b/client/src/components/organisms/index.ts index 2e3043f..0c079d8 100644 --- a/client/src/components/organisms/index.ts +++ b/client/src/components/organisms/index.ts @@ -7,5 +7,17 @@ import { BrowsePageChannel } from './BrowsePageChannel/BrowsePageChannel'; import { CreateChannelModal } from './CreateChannelModal/CreateChannelModal'; import { BrowsePageHeader } from './BrowsePageHeader/BrowsePageHeader'; import { UserBoxModal } from './UserBoxModal/UserBoxModal'; +import { BrowsePageChannelList } from './BrowsePageChannelList/BrowsePageChannelList'; -export { Header, Sidebar, ChatroomHeader, ChatroomBody, LoginForm, BrowsePageChannel, CreateChannelModal, BrowsePageHeader, UserBoxModal }; +export { + Header, + Sidebar, + ChatroomHeader, + ChatroomBody, + LoginForm, + BrowsePageChannel, + CreateChannelModal, + BrowsePageHeader, + UserBoxModal, + BrowsePageChannelList +}; diff --git a/client/src/pages/ChannelBrowser/ChannelBrowser.tsx b/client/src/pages/ChannelBrowser/ChannelBrowser.tsx index 1b8c648..01676f3 100644 --- a/client/src/pages/ChannelBrowser/ChannelBrowser.tsx +++ b/client/src/pages/ChannelBrowser/ChannelBrowser.tsx @@ -1,13 +1,18 @@ -import React from 'react'; +import React, { useEffect } from 'react'; import styled from 'styled-components'; -import { BrowsePageHeader } from '@components/organisms'; -import { BrowsePageSearchBar } from '@components/molecules'; +import { BrowsePageChannelList, BrowsePageHeader } from '@components/organisms'; +import { BrowsePageControls, BrowsePageSearchBar } from '@components/molecules'; +import { useDispatch, useSelector } from 'react-redux'; +import { RootState } from '@store/reducers/index'; +import { initChannels } from '@store/actions/channel-action'; interface ChannelBrowserProps { children: React.ReactNode; } const ChannelBrowserContainer = styled.div` + display: flex; + flex-direction: column; height: 100%; `; @@ -16,12 +21,21 @@ const SearchBarWrap = styled.div` `; const ChannelBrowser: React.FC = ({ children, ...props }) => { + const dispatch = useDispatch(); + const { channelCount, channels } = useSelector((store: RootState) => store.channel); + + useEffect(() => { + dispatch(initChannels()); + }, []); + return ( + + ); }; From cea8486dbbcf2f965d9ef5b0e429f26eb8faca3a Mon Sep 17 00:00:00 2001 From: ypd01018 <37091190+ypd01018@users.noreply.github.com> Date: Fri, 11 Dec 2020 13:17:03 +0900 Subject: [PATCH 2/4] =?UTF-8?q?feat:=20=EC=A0=84=EC=B2=B4=20chatroom=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=EC=97=90=20isJoined=20=ED=94=84=EB=A1=9C?= =?UTF-8?q?=ED=8D=BC=ED=8B=B0,=20=ED=97=A4=EB=8D=94=EC=97=90=20=EC=B1=84?= =?UTF-8?q?=EB=84=90=20=EC=A0=84=EC=B2=B4=20=EA=B0=AF=EC=88=98=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20=EA=B5=AC=ED=98=84=20(#182)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 전체 채널 조회 api에 isjoined 속성 추가 * feat: getchatroom 헤더에 chatroom 갯수 추가 * refactor: 코드 리뷰 반영 --- server/src/controller/chatroom-controller.ts | 4 +- server/src/service/chatroom-service.ts | 47 ++++++++++++++------ 2 files changed, 36 insertions(+), 15 deletions(-) diff --git a/server/src/controller/chatroom-controller.ts b/server/src/controller/chatroom-controller.ts index 3604fc6..0b5af22 100644 --- a/server/src/controller/chatroom-controller.ts +++ b/server/src/controller/chatroom-controller.ts @@ -27,7 +27,9 @@ const ChatroomController = { try { const { userId } = req.user; const { offsetTitle } = req.query; - const chatrooms = await ChatroomService.getInstance().getChatrooms(Number(userId), Number(offsetTitle)); + const chatrooms = await ChatroomService.getInstance().getChatrooms(Number(userId), String(offsetTitle)); + const chatroomCount = await ChatroomService.getInstance().getChatroomCount(userId); + res.setHeader('X-total-count', chatroomCount); res.status(HttpStatusCode.OK).json(chatrooms); } catch (err) { next(err); diff --git a/server/src/service/chatroom-service.ts b/server/src/service/chatroom-service.ts index 347df4f..39a6008 100644 --- a/server/src/service/chatroom-service.ts +++ b/server/src/service/chatroom-service.ts @@ -108,9 +108,9 @@ class ChatroomService { return newUserChatroom; } - async getChatrooms(userId: Number, offsetTitle) { + async getChatrooms(userId: Number, offsetTitle: String) { let where = 'chatroom.chatType = :chatType'; - if (offsetTitle) where += ' AND chatroom.title > :offsetTitle'; + if (offsetTitle !== 'undefined') where += ' AND chatroom.title > :offsetTitle'; const chatrooms = await this.chatroomRepository .createQueryBuilder('chatroom') .where(where, { chatType: 'Channel', offsetTitle }) @@ -121,27 +121,30 @@ class ChatroomService { .addSelect(['user.userId']) .orderBy('chatroom.title') .getMany(); - const filterChatrooms = this.getFilterPrivateChatrooms(chatrooms, userId); - const customChatrooms = this.getCustomChatrooms(filterChatrooms); + const { filterChatrooms, isJoinedArr } = this.getFilterPrivateChatrooms(chatrooms, userId); + const customChatrooms = this.getCustomChatrooms(filterChatrooms, isJoinedArr); return customChatrooms.slice(0, 20); } private getFilterPrivateChatrooms(chatrooms: any, userId: Number) { - return chatrooms.filter((chatroom) => { - let isJoin; - if (chatroom.isPrivate) - chatroom.userChatrooms.forEach((userChatroom) => { - if (userChatroom.user.userId === userId) isJoin = true; - }); - return !chatroom.isPrivate || isJoin; + let isJoinedArr = []; + const filterChatrooms = chatrooms.filter((chatroom) => { + let isJoined = false; + chatroom.userChatrooms.forEach((userChatroom) => { + if (userChatroom.user.userId === userId) isJoined = true; + }); + if (isJoined || !chatroom.isPrivate) isJoinedArr.push(isJoined); + return !chatroom.isPrivate || isJoined; }); + return { filterChatrooms, isJoinedArr }; } - private getCustomChatrooms(chatrooms: any) { - return chatrooms.map((chatroom) => { + private getCustomChatrooms(chatrooms: any, isJoinedArr: any) { + return chatrooms.map((chatroom, idx) => { const { chatroomId, title, description, isPrivate, userChatrooms } = chatroom; const members = userChatrooms.length; - return { chatroomId, title, description, isPrivate, members }; + const isJoinend = isJoinedArr[idx]; + return { chatroomId, title, description, isPrivate, members, isJoinend }; }); } @@ -212,6 +215,22 @@ class ChatroomService { async deleteChatroom(chatroomId: number) { await this.chatroomRepository.softDelete(chatroomId); } + + async getChatroomCount(userId: number) { + const chatrooms = await this.chatroomRepository + .createQueryBuilder('chatroom') + .where('chatroom.chatType = :chatType', { chatType: ChatType.Channel }) + .leftJoin('chatroom.userChatrooms', 'userChatrooms') + .leftJoin('userChatrooms.user', 'user') + .select(['chatroom.chatroomId', 'chatroom.title', 'chatroom.description', 'chatroom.isPrivate']) + .addSelect(['userChatrooms.userChatroomId']) + .addSelect(['user.userId']) + .orderBy('chatroom.title') + .getMany(); + + const { filterChatrooms } = this.getFilterPrivateChatrooms(chatrooms, userId); + return filterChatrooms.length; + } } export default ChatroomService; From 837fe8bc3c37b56e40d1968ae7ea5033084e0e45 Mon Sep 17 00:00:00 2001 From: STak <59037261+profornnan@users.noreply.github.com> Date: Fri, 11 Dec 2020 13:48:27 +0900 Subject: [PATCH 3/4] =?UTF-8?q?feat:=20=EC=B1=84=EB=84=90=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20=EC=8B=9C=20join=20chatroom=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20(socket)=20(#184)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 채팅방 생성에 성공한 경우 join chatroom을 하도록 구현 --- client/src/common/store/sagas/chatroom-saga.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/client/src/common/store/sagas/chatroom-saga.ts b/client/src/common/store/sagas/chatroom-saga.ts index eca1af4..bc4075b 100644 --- a/client/src/common/store/sagas/chatroom-saga.ts +++ b/client/src/common/store/sagas/chatroom-saga.ts @@ -1,5 +1,6 @@ import { call, put, takeEvery } from 'redux-saga/effects'; import API from '@utils/api'; +import socket from '@socket/socketIO'; import { LOAD, LOAD_ASYNC, @@ -53,6 +54,7 @@ function* addChannel(action: any) { try { const chatroomId = yield call(API.createChannel, action.payload.title, action.payload.description, action.payload.isPrivate); const payload = { chatroomId, chatType: 'Channel', isPrivate: action.payload.isPrivate, title: action.payload.title }; + socket.emit('join chatroom', { chatroomId }); yield put({ type: ADD_CHANNEL, payload }); } catch (e) { alert('같은 이름의 채널이 존재합니다.'); From c76909f6d002de99e1dd5b42e7808661bafd5052 Mon Sep 17 00:00:00 2001 From: STak <59037261+profornnan@users.noreply.github.com> Date: Sun, 13 Dec 2020 01:14:13 +0900 Subject: [PATCH 4/4] =?UTF-8?q?feat:=20Browse=20Page=20=EC=B1=84=EB=84=90?= =?UTF-8?q?=20=EB=AA=A9=EB=A1=9D=20=EA=B5=AC=ED=98=84(Infinite=20Scroll=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84,=20=EC=B1=84=EB=84=90=20Join=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EA=B5=AC=ED=98=84)=20(#185)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: 채널 목록 조회 API 오류 해결 - x-total-count header를 사용할 수 있도록 수정 - 오타 수정 * feat: 채널 개수와 join 여부 연동 - channel-saga, api에 channelCount 추가 * feat: getNextChannels API 추가 - offsetTitle을 기준으로 다음 채널목록을 가져오는 API 추가 * feat: Channel Store에 loadNextChannels 추가 - 다음 채널 목록을 가져오는 loadNextChannels type, action, reducer, saga 구현 * feat: 채널 목록 infinite scroll 구현 * feat: 채팅방 JOIN 기능 구현 - joinChannel API 추가 - Channel Store에 joinChannel types, action, reducer, saga 구현 - 채팅방 목록에서 Join 버튼 클릭 시 채팅방 들어가기 동작 구현 - join chatroom socket 리팩토링 --- client/.eslintrc.json | 1 + client/src/common/socket/emits/chatroom.ts | 6 +++ .../src/common/socket/types/chatroom-types.ts | 5 +++ .../common/store/actions/channel-action.ts | 4 +- .../common/store/reducers/channel-reducer.ts | 14 ++++++- client/src/common/store/sagas/channel-saga.ts | 41 +++++++++++++++++-- .../src/common/store/sagas/chatroom-saga.ts | 4 +- .../src/common/store/types/channel-types.ts | 24 +++++++++-- client/src/common/utils/api.ts | 11 +++++ client/src/components/atoms/Button/Button.tsx | 5 +++ .../BrowsePageChannelButton.tsx | 9 +++- .../BrowsePageChannel.stories.tsx | 8 +--- .../BrowsePageChannel/BrowsePageChannel.tsx | 21 ++++------ .../BrowsePageChannelList.stories.tsx | 6 +-- .../BrowsePageChannelList.tsx | 25 +++++++++-- server/src/controller/chatroom-controller.ts | 3 +- server/src/service/chatroom-service.ts | 4 +- 17 files changed, 153 insertions(+), 38 deletions(-) create mode 100644 client/src/common/socket/emits/chatroom.ts create mode 100644 client/src/common/socket/types/chatroom-types.ts diff --git a/client/.eslintrc.json b/client/.eslintrc.json index 146fd9b..f8aae6f 100644 --- a/client/.eslintrc.json +++ b/client/.eslintrc.json @@ -40,6 +40,7 @@ } ], "linebreak-style": 0, + "no-case-declarations": 0, "no-use-before-define": "off", "import/prefer-default-export": "off", "import/no-unresolved": "off", diff --git a/client/src/common/socket/emits/chatroom.ts b/client/src/common/socket/emits/chatroom.ts new file mode 100644 index 0000000..f56ef3f --- /dev/null +++ b/client/src/common/socket/emits/chatroom.ts @@ -0,0 +1,6 @@ +import { JOIN_CHATROOM, joinChatroomState } from '@socket/types/chatroom-types'; +import socket from '../socketIO'; + +export const joinChatroom = (chatroomId: joinChatroomState) => { + socket.emit(JOIN_CHATROOM, { chatroomId }); +}; diff --git a/client/src/common/socket/types/chatroom-types.ts b/client/src/common/socket/types/chatroom-types.ts new file mode 100644 index 0000000..ce02f9c --- /dev/null +++ b/client/src/common/socket/types/chatroom-types.ts @@ -0,0 +1,5 @@ +export const JOIN_CHATROOM = 'join chatroom'; + +export interface joinChatroomState { + chatroomId: number; +} diff --git a/client/src/common/store/actions/channel-action.ts b/client/src/common/store/actions/channel-action.ts index d379834..6ef4b87 100644 --- a/client/src/common/store/actions/channel-action.ts +++ b/client/src/common/store/actions/channel-action.ts @@ -1,3 +1,5 @@ -import { INIT_CHANNELS_ASYNC } from '../types/channel-types'; +import { INIT_CHANNELS_ASYNC, LOAD_NEXT_CHANNELS_ASYNC, JOIN_CHANNEL_ASYNC } from '../types/channel-types'; export const initChannels = () => ({ type: INIT_CHANNELS_ASYNC }); +export const loadNextChannels = (payload: any) => ({ type: LOAD_NEXT_CHANNELS_ASYNC, payload }); +export const joinChannel = (payload: any) => ({ type: JOIN_CHANNEL_ASYNC, payload }); diff --git a/client/src/common/store/reducers/channel-reducer.ts b/client/src/common/store/reducers/channel-reducer.ts index a9afef2..d9dbe71 100644 --- a/client/src/common/store/reducers/channel-reducer.ts +++ b/client/src/common/store/reducers/channel-reducer.ts @@ -1,4 +1,4 @@ -import { channelsState, ChannelTypes, INIT_CHANNELS } from '../types/channel-types'; +import { channelState, channelsState, ChannelTypes, INIT_CHANNELS, LOAD_NEXT_CHANNELS, JOIN_CHANNEL } from '../types/channel-types'; const initialState: channelsState = { channelCount: 0, @@ -12,6 +12,18 @@ export default function channelReducer(state = initialState, action: ChannelType channelCount: action.payload.channelCount, channels: action.payload.channels }; + case LOAD_NEXT_CHANNELS: + return { + ...state, + channels: [...state.channels, ...action.payload.channels] + }; + case JOIN_CHANNEL: + const { chatroomId } = action.payload; + const channels = state.channels.map((channel: channelState) => { + if (channel.chatroomId === chatroomId) return { ...channel, isJoined: true }; + return channel; + }); + return { ...state, channels }; default: return state; } diff --git a/client/src/common/store/sagas/channel-saga.ts b/client/src/common/store/sagas/channel-saga.ts index 9fda8f5..f034ec6 100644 --- a/client/src/common/store/sagas/channel-saga.ts +++ b/client/src/common/store/sagas/channel-saga.ts @@ -1,17 +1,52 @@ import { call, put, takeEvery } from 'redux-saga/effects'; import API from '@utils/api'; -import { INIT_CHANNELS, INIT_CHANNELS_ASYNC } from '../types/channel-types'; +import { joinChatroom } from '@socket/emits/chatroom'; +import { + INIT_CHANNELS, + INIT_CHANNELS_ASYNC, + JOIN_CHANNEL, + JOIN_CHANNEL_ASYNC, + LOAD_NEXT_CHANNELS, + LOAD_NEXT_CHANNELS_ASYNC +} from '../types/channel-types'; +import { ADD_CHANNEL } from '../types/chatroom-types'; function* initChannelsSaga() { try { - const channelCount = 0; - const channels = yield call(API.getChannels); + const { channels, channelCount } = yield call(API.getChannels); yield put({ type: INIT_CHANNELS, payload: { channelCount, channels } }); } catch (e) { console.log(e); } } +function* loadNextChannels(action: any) { + try { + const { title } = action.payload; + const nextChannels = yield call(API.getNextChannels, title); + yield put({ type: LOAD_NEXT_CHANNELS, payload: { channels: nextChannels } }); + } catch (e) { + console.log(e); + } +} + +function* joinChannel(action: any) { + try { + const { chatroomId } = action.payload; + yield call(API.joinChannel, chatroomId); + yield put({ type: JOIN_CHANNEL, payload: { chatroomId } }); + const chatroom = yield call(API.getChatroom, chatroomId); + const { chatType, isPrivate, title } = chatroom; + const payload = { chatroomId, chatType, isPrivate, title }; + joinChatroom(chatroomId); + yield put({ type: ADD_CHANNEL, payload }); + } catch (e) { + console.log(e); + } +} + export function* channelSaga() { yield takeEvery(INIT_CHANNELS_ASYNC, initChannelsSaga); + yield takeEvery(LOAD_NEXT_CHANNELS_ASYNC, loadNextChannels); + yield takeEvery(JOIN_CHANNEL_ASYNC, joinChannel); } diff --git a/client/src/common/store/sagas/chatroom-saga.ts b/client/src/common/store/sagas/chatroom-saga.ts index bc4075b..8a5577a 100644 --- a/client/src/common/store/sagas/chatroom-saga.ts +++ b/client/src/common/store/sagas/chatroom-saga.ts @@ -1,6 +1,6 @@ import { call, put, takeEvery } from 'redux-saga/effects'; import API from '@utils/api'; -import socket from '@socket/socketIO'; +import { joinChatroom } from '@socket/emits/chatroom'; import { LOAD, LOAD_ASYNC, @@ -54,7 +54,7 @@ function* addChannel(action: any) { try { const chatroomId = yield call(API.createChannel, action.payload.title, action.payload.description, action.payload.isPrivate); const payload = { chatroomId, chatType: 'Channel', isPrivate: action.payload.isPrivate, title: action.payload.title }; - socket.emit('join chatroom', { chatroomId }); + joinChatroom(chatroomId); yield put({ type: ADD_CHANNEL, payload }); } catch (e) { alert('같은 이름의 채널이 존재합니다.'); diff --git a/client/src/common/store/types/channel-types.ts b/client/src/common/store/types/channel-types.ts index c594326..303499e 100644 --- a/client/src/common/store/types/channel-types.ts +++ b/client/src/common/store/types/channel-types.ts @@ -1,10 +1,14 @@ export const INIT_CHANNELS = 'INIT_CHANNELS'; export const INIT_CHANNELS_ASYNC = 'INIT_CHANNELS_ASYNC'; +export const LOAD_NEXT_CHANNELS = 'LOAD_NEXT_CHANNELS'; +export const LOAD_NEXT_CHANNELS_ASYNC = 'LOAD_NEXT_CHANNELS_ASYNC'; +export const JOIN_CHANNEL = 'JOIN_CHANNEL'; +export const JOIN_CHANNEL_ASYNC = 'JOIN_CHANNEL_ASYNC'; export interface channelState { - channelId: number; + chatroomId: number; title: string; - description: string; + description?: string; isPrivate: boolean; members: number; isJoined: boolean; @@ -15,9 +19,23 @@ export interface channelsState { channels: Array; } +export interface joinChannelState { + chatroomId: number; +} + interface InitChannelsAction { type: typeof INIT_CHANNELS; payload: channelsState; } -export type ChannelTypes = InitChannelsAction; +interface LoadNextChannels { + type: typeof LOAD_NEXT_CHANNELS; + payload: channelsState; +} + +interface JoinChannel { + type: typeof JOIN_CHANNEL; + payload: joinChannelState; +} + +export type ChannelTypes = InitChannelsAction | LoadNextChannels | JoinChannel; diff --git a/client/src/common/utils/api.ts b/client/src/common/utils/api.ts index 3359584..4a08fff 100644 --- a/client/src/common/utils/api.ts +++ b/client/src/common/utils/api.ts @@ -71,6 +71,17 @@ export default { getChannels: async () => { const response = await axios.get(`api/chatrooms`); + const channelCount = response.headers['x-total-count']; + return { channels: response.data, channelCount }; + }, + + getNextChannels: async (title: string) => { + const response = await axios.get(`api/chatrooms?offsetTitle=${title}`); + return response.data; + }, + + joinChannel: async (chatroomId: number) => { + const response = await axios.post(`api/user-chatrooms`, { chatroomId }); return response.data; } }; diff --git a/client/src/components/atoms/Button/Button.tsx b/client/src/components/atoms/Button/Button.tsx index 2e951c8..9afb993 100644 --- a/client/src/components/atoms/Button/Button.tsx +++ b/client/src/components/atoms/Button/Button.tsx @@ -9,12 +9,15 @@ interface ButtonProps { fontColor: string; isBold?: boolean; hoverColor?: string; + width?: string; + height?: string; onClick?: () => void; } const StyledButton = styled.button` display: flex; align-items: center; + justify-content: center; background-color: ${(props) => props.backgroundColor}; border: 2px solid ${(props) => props.borderColor}; color: ${(props) => props.fontColor}; @@ -24,6 +27,8 @@ const StyledButton = styled.button` cursor: pointer; font-weight: ${(props) => (props.isBold ? 'bold' : null)}; ${(props) => (props.hoverColor ? `&:hover { background-color: ${color.hover_primary}}` : '')} + ${(props) => (props.width ? `width: ${props.width}}` : '')} + ${(props) => (props.height ? `height: ${props.height}}` : '')} `; const Button: React.FC = ({ children, backgroundColor, borderColor, fontColor, isBold, hoverColor, ...props }) => { diff --git a/client/src/components/molecules/BrowsePageChannelButton/BrowsePageChannelButton.tsx b/client/src/components/molecules/BrowsePageChannelButton/BrowsePageChannelButton.tsx index f0089ee..1d69525 100644 --- a/client/src/components/molecules/BrowsePageChannelButton/BrowsePageChannelButton.tsx +++ b/client/src/components/molecules/BrowsePageChannelButton/BrowsePageChannelButton.tsx @@ -12,7 +12,13 @@ const BrowsePageChannelButton: React.FC = ({ isJoi return ( <> {isJoined ? ( - ) : ( @@ -21,6 +27,7 @@ const BrowsePageChannelButton: React.FC = ({ isJoi backgroundColor={color.button_secondary} borderColor={color.button_secondary} fontColor={color.text_secondary} + width="5rem" {...props}> Join diff --git a/client/src/components/organisms/BrowsePageChannel/BrowsePageChannel.stories.tsx b/client/src/components/organisms/BrowsePageChannel/BrowsePageChannel.stories.tsx index 8f64527..4238d06 100644 --- a/client/src/components/organisms/BrowsePageChannel/BrowsePageChannel.stories.tsx +++ b/client/src/components/organisms/BrowsePageChannel/BrowsePageChannel.stories.tsx @@ -9,16 +9,12 @@ export default { const Template: Story = (args) => ; -const handlingJoinButton = () => {}; -const handlingLeaveButton = () => {}; - export const BlackBrowsePageChannel = Template.bind({}); BlackBrowsePageChannel.args = { + chatroomId: 1, title: 'notice', isJoined: true, members: 4, description: '공지사항을 안내하는 채널', - isPrivate: true, - handlingJoinButton, - handlingLeaveButton + isPrivate: true }; diff --git a/client/src/components/organisms/BrowsePageChannel/BrowsePageChannel.tsx b/client/src/components/organisms/BrowsePageChannel/BrowsePageChannel.tsx index 652a35a..d683e64 100644 --- a/client/src/components/organisms/BrowsePageChannel/BrowsePageChannel.tsx +++ b/client/src/components/organisms/BrowsePageChannel/BrowsePageChannel.tsx @@ -4,15 +4,16 @@ import { color } from '@theme/index'; import { BrowsePageChannelHeader } from '@components/molecules/BrowsePageChannelHeader/BrowsePageChannelHeader'; import { BrowsePageChannelBody } from '@components/molecules/BrowsePageChannelBody/BrowsePageChannelBody'; import { BrowsePageChannelButton } from '@components/molecules/BrowsePageChannelButton/BrowsePageChannelButton'; +import { useDispatch } from 'react-redux'; +import { joinChannel } from '@store/actions/channel-action'; interface BrowsePageChannelProps { + chatroomId: number; title: string; description?: string; isPrivate?: boolean; members: number; isJoined?: boolean; - handlingJoinButton?: () => void; - handlingLeaveButton?: () => void; } const BrowsePageChannelContainer = styled.div` @@ -43,16 +44,12 @@ const ButtonWrap = styled.div` } `; -const BrowsePageChannel: React.FC = ({ - title, - isJoined, - members, - description, - isPrivate, - handlingJoinButton, - handlingLeaveButton, - ...props -}) => { +const BrowsePageChannel: React.FC = ({ chatroomId, title, isJoined, members, description, isPrivate, ...props }) => { + const dispatch = useDispatch(); + const handlingJoinButton = () => { + dispatch(joinChannel({ chatroomId })); + }; + const handlingLeaveButton = () => {}; return ( diff --git a/client/src/components/organisms/BrowsePageChannelList/BrowsePageChannelList.stories.tsx b/client/src/components/organisms/BrowsePageChannelList/BrowsePageChannelList.stories.tsx index b216834..93dc35b 100644 --- a/client/src/components/organisms/BrowsePageChannelList/BrowsePageChannelList.stories.tsx +++ b/client/src/components/organisms/BrowsePageChannelList/BrowsePageChannelList.stories.tsx @@ -13,7 +13,7 @@ export const BlackBrowsePageChannelList = Template.bind({}); BlackBrowsePageChannelList.args = { channels: [ { - channelId: 1, + chatroomId: 1, title: 'notice', description: '공지사항을 안내하는 채널', isPrivate: false, @@ -21,14 +21,14 @@ BlackBrowsePageChannelList.args = { isJoined: true }, { - channelId: 2, + chatroomId: 2, title: '질의응답', isPrivate: false, members: 10, isJoined: false }, { - channelId: 3, + chatroomId: 3, title: 'black', description: 'black', isPrivate: true, diff --git a/client/src/components/organisms/BrowsePageChannelList/BrowsePageChannelList.tsx b/client/src/components/organisms/BrowsePageChannelList/BrowsePageChannelList.tsx index acdcfac..1d0bae1 100644 --- a/client/src/components/organisms/BrowsePageChannelList/BrowsePageChannelList.tsx +++ b/client/src/components/organisms/BrowsePageChannelList/BrowsePageChannelList.tsx @@ -1,9 +1,12 @@ -import React from 'react'; +import React, { useState } from 'react'; import styled from 'styled-components'; import { BrowsePageChannel } from '@components/organisms'; +import { useDispatch } from 'react-redux'; +import { channelState } from '@store/types/channel-types'; +import { loadNextChannels } from '@store/actions/channel-action'; interface BrowsePageChannelListProps { - channels: Array; + channels: Array; } const BrowsePageChannelListContainter = styled.div` @@ -15,10 +18,22 @@ const BrowsePageChannelListContainter = styled.div` `; const BrowsePageChannelList: React.FC = ({ channels, ...props }) => { + const dispatch = useDispatch(); + const [lastRequestChannelTitle, setLastRequestChannelTitle] = useState(''); + const onScrollHandler = (e: any) => { + const title: string | null = channels[channels.length - 1]?.title; + if (e.target.scrollTop >= e.target.scrollHeight / 2) { + if (title === lastRequestChannelTitle) return; + dispatch(loadNextChannels({ title })); + setLastRequestChannelTitle(title); + } + }; + const createMessages = () => { return channels.map((channel: any) => ( = ({ channels, )); }; - return {createMessages()}; + return ( + + {createMessages()} + + ); }; export { BrowsePageChannelList, BrowsePageChannelListProps }; diff --git a/server/src/controller/chatroom-controller.ts b/server/src/controller/chatroom-controller.ts index 0b5af22..add4c0a 100644 --- a/server/src/controller/chatroom-controller.ts +++ b/server/src/controller/chatroom-controller.ts @@ -29,7 +29,8 @@ const ChatroomController = { const { offsetTitle } = req.query; const chatrooms = await ChatroomService.getInstance().getChatrooms(Number(userId), String(offsetTitle)); const chatroomCount = await ChatroomService.getInstance().getChatroomCount(userId); - res.setHeader('X-total-count', chatroomCount); + res.header('Access-Control-Expose-Headers', 'x-total-count'); + res.setHeader('x-total-count', chatroomCount); res.status(HttpStatusCode.OK).json(chatrooms); } catch (err) { next(err); diff --git a/server/src/service/chatroom-service.ts b/server/src/service/chatroom-service.ts index 39a6008..3a07733 100644 --- a/server/src/service/chatroom-service.ts +++ b/server/src/service/chatroom-service.ts @@ -143,8 +143,8 @@ class ChatroomService { return chatrooms.map((chatroom, idx) => { const { chatroomId, title, description, isPrivate, userChatrooms } = chatroom; const members = userChatrooms.length; - const isJoinend = isJoinedArr[idx]; - return { chatroomId, title, description, isPrivate, members, isJoinend }; + const isJoined = isJoinedArr[idx]; + return { chatroomId, title, description, isPrivate, members, isJoined }; }); }