Skip to content

Commit

Permalink
feat: Browse Page 채널 목록 구현(Infinite Scroll 구현, 채널 Join 기능 구현) (#185)
Browse files Browse the repository at this point in the history
* 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 리팩토링
  • Loading branch information
profornnan authored Dec 12, 2020
1 parent 837fe8b commit c76909f
Show file tree
Hide file tree
Showing 17 changed files with 153 additions and 38 deletions.
1 change: 1 addition & 0 deletions client/.eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
6 changes: 6 additions & 0 deletions client/src/common/socket/emits/chatroom.ts
Original file line number Diff line number Diff line change
@@ -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 });
};
5 changes: 5 additions & 0 deletions client/src/common/socket/types/chatroom-types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export const JOIN_CHATROOM = 'join chatroom';

export interface joinChatroomState {
chatroomId: number;
}
4 changes: 3 additions & 1 deletion client/src/common/store/actions/channel-action.ts
Original file line number Diff line number Diff line change
@@ -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 });
14 changes: 13 additions & 1 deletion client/src/common/store/reducers/channel-reducer.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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;
}
Expand Down
41 changes: 38 additions & 3 deletions client/src/common/store/sagas/channel-saga.ts
Original file line number Diff line number Diff line change
@@ -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);
}
4 changes: 2 additions & 2 deletions client/src/common/store/sagas/chatroom-saga.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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('같은 이름의 채널이 존재합니다.');
Expand Down
24 changes: 21 additions & 3 deletions client/src/common/store/types/channel-types.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -15,9 +19,23 @@ export interface channelsState {
channels: Array<channelState>;
}

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;
11 changes: 11 additions & 0 deletions client/src/common/utils/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
};
5 changes: 5 additions & 0 deletions client/src/components/atoms/Button/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,15 @@ interface ButtonProps {
fontColor: string;
isBold?: boolean;
hoverColor?: string;
width?: string;
height?: string;
onClick?: () => void;
}

const StyledButton = styled.button<any>`
display: flex;
align-items: center;
justify-content: center;
background-color: ${(props) => props.backgroundColor};
border: 2px solid ${(props) => props.borderColor};
color: ${(props) => props.fontColor};
Expand All @@ -24,6 +27,8 @@ const StyledButton = styled.button<any>`
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<ButtonProps> = ({ children, backgroundColor, borderColor, fontColor, isBold, hoverColor, ...props }) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,13 @@ const BrowsePageChannelButton: React.FC<BrowsePageChannelButtonProps> = ({ isJoi
return (
<>
{isJoined ? (
<Button onClick={handlingLeaveButton} backgroundColor={color.tertiary} borderColor={color.secondary} fontColor={color.primary} {...props}>
<Button
onClick={handlingLeaveButton}
backgroundColor={color.tertiary}
borderColor={color.secondary}
fontColor={color.primary}
width="5rem"
{...props}>
Leave
</Button>
) : (
Expand All @@ -21,6 +27,7 @@ const BrowsePageChannelButton: React.FC<BrowsePageChannelButtonProps> = ({ isJoi
backgroundColor={color.button_secondary}
borderColor={color.button_secondary}
fontColor={color.text_secondary}
width="5rem"
{...props}>
Join
</Button>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,12 @@ export default {

const Template: Story<BrowsePageChannelProps> = (args) => <BrowsePageChannel {...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
};
Original file line number Diff line number Diff line change
Expand Up @@ -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<any>`
Expand Down Expand Up @@ -43,16 +44,12 @@ const ButtonWrap = styled.div<any>`
}
`;

const BrowsePageChannel: React.FC<BrowsePageChannelProps> = ({
title,
isJoined,
members,
description,
isPrivate,
handlingJoinButton,
handlingLeaveButton,
...props
}) => {
const BrowsePageChannel: React.FC<BrowsePageChannelProps> = ({ chatroomId, title, isJoined, members, description, isPrivate, ...props }) => {
const dispatch = useDispatch();
const handlingJoinButton = () => {
dispatch(joinChannel({ chatroomId }));
};
const handlingLeaveButton = () => {};
return (
<BrowsePageChannelContainer {...props}>
<BrowsePageChannelContent>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,22 +13,22 @@ export const BlackBrowsePageChannelList = Template.bind({});
BlackBrowsePageChannelList.args = {
channels: [
{
channelId: 1,
chatroomId: 1,
title: 'notice',
description: '공지사항을 안내하는 채널',
isPrivate: false,
members: 110,
isJoined: true
},
{
channelId: 2,
chatroomId: 2,
title: '질의응답',
isPrivate: false,
members: 10,
isJoined: false
},
{
channelId: 3,
chatroomId: 3,
title: 'black',
description: 'black',
isPrivate: true,
Expand Down
Original file line number Diff line number Diff line change
@@ -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<object>;
channels: Array<channelState>;
}

const BrowsePageChannelListContainter = styled.div<any>`
Expand All @@ -15,10 +18,22 @@ const BrowsePageChannelListContainter = styled.div<any>`
`;

const BrowsePageChannelList: React.FC<BrowsePageChannelListProps> = ({ 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) => (
<BrowsePageChannel
key={channel.chatroomId}
chatroomId={channel.chatroomId}
title={channel.title}
description={channel.description}
isPrivate={channel.isPrivate}
Expand All @@ -28,7 +43,11 @@ const BrowsePageChannelList: React.FC<BrowsePageChannelListProps> = ({ channels,
));
};

return <BrowsePageChannelListContainter {...props}>{createMessages()}</BrowsePageChannelListContainter>;
return (
<BrowsePageChannelListContainter onScroll={onScrollHandler} {...props}>
{createMessages()}
</BrowsePageChannelListContainter>
);
};

export { BrowsePageChannelList, BrowsePageChannelListProps };
3 changes: 2 additions & 1 deletion server/src/controller/chatroom-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
4 changes: 2 additions & 2 deletions server/src/service/chatroom-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
});
}

Expand Down

0 comments on commit c76909f

Please sign in to comment.