Skip to content

Commit

Permalink
Merge pull request #207 from boostcamp-2020/develop
Browse files Browse the repository at this point in the history
Release Thread Page, User Modal
  • Loading branch information
Do-ho authored Dec 14, 2020
2 parents 60a4060 + 52d14c3 commit 852f369
Show file tree
Hide file tree
Showing 76 changed files with 1,293 additions and 186 deletions.
114 changes: 111 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# 팀 협업 툴 - Black
![black](https://user-images.githubusercontent.com/33643752/99354886-990a9d00-28ea-11eb-9f75-784658906994.png)

<div align="middle">
<img src="https://user-images.githubusercontent.com/59037261/102005062-4d1c0e00-3d59-11eb-8eff-3505540fc468.gif">
</div>

<p align="middle">
<!-- tag -->
Expand Down Expand Up @@ -28,9 +31,114 @@
<img src='https://img.shields.io/static/v1?label=Jest&message=26.6.1&color=important'/>
</p>

| 강동훈 | 김도호 | 탁성건 |
## :house: [HomePage](http://black-boostcamp.kro.kr)

<br />

## :bookmark_tabs: 프로젝트 소개

팀 협업 메신저 Black은 Slack을 Clone한 프로젝트입니다. 또한 채널을 통한 메신저를 구축하면서 업무간 필요한 정보를 공유하는 웹 플랫폼입니다.

<br />

## :gear: 주요 기능

### :speech_balloon: Channel / DM

- 채널 목록 조회
- 채널 생성 (Private / Public)
- 채널에 참여중인 사용자 조회
- DM 생성

### :family: User

- GitHub 로그인
- 프로필 확인
- 로그아웃

### :email: Message

- 실시간 메시지 보내기
- 실시간 메시지 리액션 추가

### :incoming_envelope: Thread

- 실시간 댓글 추가
- 실시간 댓글 리액션 추가

<br />

## :man_cartwheeling: 팀원 소개

<div align="middle">

| J003 강동훈 | J030 김도호 | J211 탁성건 |
| :----------------------------------------------------------: | :----------------------------------------------------------: | :----------------------------------------------------------: |
| <img src="https://avatars0.githubusercontent.com/u/37091190?s=400&u=d358f361db0c43c0fccdcbd31de5ded89efe0169&v=4" width=100%> | <img src="https://avatars2.githubusercontent.com/u/33643752?s=460&u=a9a75e7c6922a23eb365b258a60499bbb9a9c655&v=4" width=100%> | <img src="https://avatars2.githubusercontent.com/u/59037261?s=460&u=7b7a0a2f151c1f49c5bc8068d4d6a5bf50c94c7b&v=4" width=100%> |
| <img src="https://avatars0.githubusercontent.com/u/37091190?s=400&u=d358f361db0c43c0fccdcbd31de5ded89efe0169&v=4" width=200> | <img src="https://avatars2.githubusercontent.com/u/33643752?s=460&u=a9a75e7c6922a23eb365b258a60499bbb9a9c655&v=4" width=200> | <img src="https://avatars2.githubusercontent.com/u/59037261?s=460&u=7b7a0a2f151c1f49c5bc8068d4d6a5bf50c94c7b&v=4" width=200> |
| 아.. 폰 보느라<br />코딩 못했다.. :joy: | 나는 개발자인<br />티를 내기 위해<br />노력했다 :computer: | 나는 내 위가 없기 때문에<br />밑만 바라본다 :see_no_evil: |

</div>

<br />

## :hammer_and_wrench: 기술 스택

![기술 스택](https://user-images.githubusercontent.com/59037261/102005071-5a38fd00-3d59-11eb-8988-74c3d8d00767.JPG)

<br />

## :pushpin: 기술 특장점

### :page_with_curl: Swagger Hub를 이용한 API 명세서 작성

Swagger Hub를 이용해 API 명세서를 작성함으로써 FE/BE 협업을 쉽게 할 수 있도록 했습니다. 실제 사용되는 Parameter로 테스트할 수 있고, 어떤 방식으로 데이터를 주고받을지 확인할 수 있어서 개발 시간을 단축하고 불필요한 의사소통 비용을 줄일 수 있었습니다.

___

### :rainbow: CI/CD Pipeline

CI/CD Pipeline을 구축해서 배포를 자동화했습니다. develop branch에서 개발을 진행하고, 배포 버전을 master branch에 PR을 남긴 후 merge를 하면 GitHub WebHook이 발생하도록 했습니다. Jenkins가 이를 감지해서 새롭게 작성한 코드를 기존에 작성한 스크립트를 활용하여 자동화된 통합, 빌드 및 배포를 진행합니다.

이렇게 자동화된 지속적 통합, 지속적 배포를 통해 개발자는 편리한 개발 환경을 구축할 수 있습니다.

___

### :cyclone: docker-compose를 활용한 무중단 배포 (blue/green)

docker-compose와 nginx를 이용하여 blue/green 배포 전략을 활용해 무중단 배포를 구현했습니다. nginx의 load balancing을 활용해 2개의 포트로 트래픽이 갈 수 있도록 설정합니다. 그리고 새롭게 배포되는 과정에서 docker-compose를 활용해 기존에 배포된 컨테이너와 다른 포트로 컨테이너를 생성하고 완료되면 기존의 컨테이너를 삭제합니다.

이를 통해 사용자는 새롭게 배포되는 과정에서 끊임 없는 서비스를 제공받을 수 있습니다.

___

### :closed_book: Atomic Design과 Storybook

슬랙을 구현함에 있어서 프로필 이미지나 메시지, 입력창 등 일관된 디자인의 컴포넌트들이 많다는 생각을 했습니다. 그래서 Atomic Design을 적용해 단계를 나눠 작은 컴포넌트를 만들고, 그것들을 결합해 조금 더 큰 단위의 뷰를 만들었습니다.

이를 통해 재사용 가능하고 일관된 디자인의 컴포넌트를 제작할 수 있었습니다. 또한 Storybook을 도입하여 디자인을 쉽게 수정하고 확인할 수 있도록 했습니다.

___

### :atom_symbol: Redux를 사용한 상태관리

슬랙은 다수의 사용자가 공동으로 한 채널에서 작업을 할 수 있다는 점에서 트랜잭션 상태 관리가 중요하다고 생각이 들었습니다. 그래서 컴포넌트 간 상태 관리 로직을 관리하고 사용자의 액션과 데이터의 변경을 전역으로 관찰할 수 있다는 점에서 Redux와 비동기적 작업을 처리하기 위한 Redux-Saga를 채택하였습니다.

___

### :page_facing_up: Message Paging

채널에 메시지가 늘어남에 따라 한 번에 여러 메시지를 받아오는 방식은 좋지 않다고 생각했습니다. 결국 Message Paging에 대해서 고려하게 되었고 offset과 limit을 두어 구현하게 되었습니다. offset으로는 가지고 있는 메시지의 가장 오래된 메시지 ID를 보내게 됩니다.

Client 측에서는 Infinite Scroll을 구현하여 첫 렌더링을 빠르게 하기 위한 노력을 했습니다. 일정한 스크롤의 위치를 넘게 되면 요청을 보내게 되어 다음 메시지를 계속적으로 받아오게 됩니다.

___

### :blue_book: 프로젝트 전체에 TypeScript 도입

TypeScript는 에러를 사전에 방지하고, 코드 자동 완성 및 가이드 기능을 사용할 수 있다는 장점을 가지고 있습니다. 이러한 장점을 프로젝트에 적용하고자 프로젝트 시작 전 다 같이 강좌를 구매하여 학습하였고 FE, BE 프로젝트에 직접 기술적으로 도전하게 되었습니다.

___

<br />

### 프로젝트가 궁금하다면 [Wiki](https://github.com/boostcamp-2020/Project12-B-Slack-Web/wiki)~ :airplane:
Binary file added client/public/imgs/close-icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions client/src/common/constants/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { DefaultSectionName } from './default-section-name';
import { KeyCode } from './key-code';
import { ChatroomEventType } from './chatroom-event-type';
import { ScrollEventType } from './scroll-event-type';

export { DefaultSectionName, KeyCode, ChatroomEventType };
export { DefaultSectionName, KeyCode, ScrollEventType };
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export const ChatroomEventType = {
export const ScrollEventType = {
COMMON: 'Common',
LOADING: 'Loading',
COMPLETELOADING: 'Complete loading',
Expand Down
4 changes: 2 additions & 2 deletions client/src/common/socket/emits/chatroom.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { JOIN_CHATROOM, joinChatroomState } from '@socket/types/chatroom-types';
import { JOIN_CHATROOM } from '@socket/types/chatroom-types';
import socket from '../socketIO';

export const joinChatroom = (chatroomId: joinChatroomState) => {
export const joinChatroom = (chatroomId: number) => {
socket.emit(JOIN_CHATROOM, { chatroomId });
};
6 changes: 6 additions & 0 deletions client/src/common/socket/emits/thread.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { CREATE_REPLY, createThreadState } from '@socket/types/thread-types';
import socket from '../socketIO';

export const createReply = (reply: createThreadState) => {
socket.emit(CREATE_REPLY, reply);
};
4 changes: 0 additions & 4 deletions client/src/common/socket/types/chatroom-types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1 @@
export const JOIN_CHATROOM = 'join chatroom';

export interface joinChatroomState {
chatroomId: number;
}
6 changes: 6 additions & 0 deletions client/src/common/socket/types/thread-types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export const CREATE_REPLY = 'create reply';

export interface createThreadState {
content: string;
messageId: number | null;
}
7 changes: 5 additions & 2 deletions client/src/common/store/actions/chatroom-action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,16 @@ import {
INSERT_MESSAGE,
ADD_CHANNEL_ASYNC,
RESET_SELECTED_CHANNEL,
LOAD_NEXT_MESSAGES_ASYNC
LOAD_NEXT_MESSAGES_ASYNC,
UPDATE_THREAD,
insertMessageState
} from '../types/chatroom-types';

export const loadAsync = (payload: any) => ({ type: LOAD_ASYNC, payload });
export const initSidebarAsync = () => ({ type: INIT_SIDEBAR_ASYNC });
export const pickChannel = (payload: any) => ({ type: PICK_CHANNEL_ASYNC, payload });
export const insertMessage = (payload: any) => ({ type: INSERT_MESSAGE, payload });
export const insertMessage = (payload: insertMessageState) => ({ type: INSERT_MESSAGE, payload });
export const addChannel = (payload: any) => ({ type: ADD_CHANNEL_ASYNC, payload });
export const resetSelectedChannel = () => ({ type: RESET_SELECTED_CHANNEL });
export const loadNextMessages = (payload: any) => ({ type: LOAD_NEXT_MESSAGES_ASYNC, payload });
export const updateThread = (payload: any) => ({ type: UPDATE_THREAD, payload });
6 changes: 5 additions & 1 deletion client/src/common/store/actions/modal-action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import {
CHANNEL_MODAL_OPEN,
CHANNEL_MODAL_CLOSE,
USERBOX_MODAL_OPEN,
USERBOX_MODAL_CLOSE
USERBOX_MODAL_CLOSE,
PROFILE_MODAL_OPEN,
PROFILE_MODAL_CLOSE
} from '@store/types/modal-types';

export const createModalOpen = () => ({ type: CREATE_MODAL_OPEN });
Expand All @@ -13,3 +15,5 @@ export const channelModalOpen = (payload: any) => ({ type: CHANNEL_MODAL_OPEN, p
export const channelModalClose = () => ({ type: CHANNEL_MODAL_CLOSE });
export const userboxModalOpen = () => ({ type: USERBOX_MODAL_OPEN });
export const userboxModalClose = () => ({ type: USERBOX_MODAL_CLOSE });
export const profileModalOpen = (payload: any) => ({ type: PROFILE_MODAL_OPEN, payload });
export const profileModalClose = () => ({ type: PROFILE_MODAL_CLOSE });
5 changes: 5 additions & 0 deletions client/src/common/store/actions/thread-action.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { LOAD_THREAD_ASYNC, INSERT_REPLY, LOAD_NEXT_REPLIES_ASYNC, replyState } from '@store/types/thread-types';

export const loadThread = (messageId: number) => ({ type: LOAD_THREAD_ASYNC, payload: { messageId } });
export const InsertReply = (payload: replyState) => ({ type: INSERT_REPLY, payload });
export const loadNextReplies = (payload: any) => ({ type: LOAD_NEXT_REPLIES_ASYNC, payload });
27 changes: 24 additions & 3 deletions client/src/common/store/reducers/chatroom-reducer.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
/* eslint-disable no-param-reassign */
/* eslint-disable no-case-declarations */
import { uriParser } from '@utils/index';
import { joinChatroom } from '@socket/emits/chatroom';
import { messageState } from '@store/types/message-types';
import {
chatroomState,
LOAD,
Expand All @@ -9,7 +12,8 @@ import {
INSERT_MESSAGE,
ADD_CHANNEL,
RESET_SELECTED_CHANNEL,
LOAD_NEXT_MESSAGES
LOAD_NEXT_MESSAGES,
UPDATE_THREAD
} from '../types/chatroom-types';

const initialState: chatroomState = {
Expand All @@ -30,7 +34,7 @@ const initialState: chatroomState = {
messages: []
};

export default function chatroomReducer(state = initialState, action: ChatroomTypes) {
const chatroomReducer = (state = initialState, action: ChatroomTypes) => {
switch (action.type) {
case LOAD:
return {
Expand Down Expand Up @@ -63,6 +67,7 @@ export default function chatroomReducer(state = initialState, action: ChatroomTy
case ADD_CHANNEL:
const newChannels = state.channels;
newChannels.push(action.payload);
joinChatroom(action.payload.chatroomId);
return {
...state,
channels: newChannels
Expand Down Expand Up @@ -90,7 +95,23 @@ export default function chatroomReducer(state = initialState, action: ChatroomTy
...state,
messages: nextMessages
};
case UPDATE_THREAD:
const updateMessages = state.messages;
const { messageId, profileUri } = action.payload;
updateMessages.forEach((message: messageState) => {
if (message.messageId === messageId) {
message.thread.profileUris.push(profileUri);
message.thread.replyCount += 1;
message.thread.lastReplyAt = new Date();
}
});
return {
...state,
messages: updateMessages
};
default:
return state;
}
}
};

export default chatroomReducer;
4 changes: 3 additions & 1 deletion client/src/common/store/reducers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@ import userReducer from './user-reducer';
import chatroomReducer from './chatroom-reducer';
import modalReducer from './modal-reducer';
import channelReducer from './channel-reducer';
import threadReducer from './thread-reducer';

export const rootReducer = combineReducers({
user: userReducer,
chatroom: chatroomReducer,
modal: modalReducer,
channel: channelReducer
channel: channelReducer,
thread: threadReducer
});

export type RootState = ReturnType<typeof rootReducer>;
12 changes: 10 additions & 2 deletions client/src/common/store/reducers/modal-reducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,16 @@ import {
CHANNEL_MODAL_OPEN,
CHANNEL_MODAL_CLOSE,
USERBOX_MODAL_OPEN,
USERBOX_MODAL_CLOSE
USERBOX_MODAL_CLOSE,
PROFILE_MODAL_OPEN,
PROFILE_MODAL_CLOSE
} from '@store/types/modal-types';

const initialState: ModalState = {
createModal: { isOpen: false },
channelModal: { isOpen: false, x: 0, y: 0 },
userboxModal: { isOpen: false }
userboxModal: { isOpen: false },
profileModal: { isOpen: false, x: 0, y: 0, userId: 0, profileUri: '', displayName: '' }
};

const ModalReducer = (state = initialState, action: ModalTypes) => {
Expand All @@ -29,6 +32,11 @@ const ModalReducer = (state = initialState, action: ModalTypes) => {
return { ...state, userboxModal: { isOpen: true } };
case USERBOX_MODAL_CLOSE:
return { ...state, userboxModal: { isOpen: false } };
case PROFILE_MODAL_OPEN:
const { userId, profileUri, displayName } = action.payload;
return { ...state, profileModal: { isOpen: true, x: action.payload.x, y: action.payload.y, userId, profileUri, displayName } };
case PROFILE_MODAL_CLOSE:
return { ...state, profileModal: { isOpen: false } };
default:
return state;
}
Expand Down
48 changes: 48 additions & 0 deletions client/src/common/store/reducers/thread-reducer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { LOAD_THREAD, threadState, INSERT_REPLY, ThreadTypes, LOAD_NEXT_REPLIES } from '@store/types/thread-types';

const initialState: threadState = {
message: {
messageId: 0,
content: '',
createdAt: new Date(),
updateAt: new Date(),
deleteAt: new Date(),
user: {
userId: 0,
profileUri: '',
displayName: ''
},
chatroom: {},
messageReactions: []
},
replies: []
};

export default function threadReducer(state = initialState, action: ThreadTypes) {
switch (action.type) {
case LOAD_THREAD:
return {
...state,
message: action.payload.message,
replies: action.payload.replies
};
case INSERT_REPLY:
const newReplies = state.replies;
if (action.payload.messageId === state.message.messageId) newReplies.push(action.payload);

return {
...state,
messages: newReplies
};
case LOAD_NEXT_REPLIES:
const nextreplies = action.payload.replies;
nextreplies.push(...state.replies);

return {
...state,
replies: nextreplies
};
default:
return state;
}
}
5 changes: 2 additions & 3 deletions client/src/common/store/sagas/channel-saga.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { call, put, takeEvery } from 'redux-saga/effects';
import API from '@utils/api';
import { joinChatroom } from '@socket/emits/chatroom';
import {
INIT_CHANNELS,
INIT_CHANNELS_ASYNC,
Expand All @@ -9,7 +8,7 @@ import {
LOAD_NEXT_CHANNELS,
LOAD_NEXT_CHANNELS_ASYNC
} from '../types/channel-types';
import { ADD_CHANNEL } from '../types/chatroom-types';
import { ADD_CHANNEL, PICK_CHANNEL_ASYNC } from '../types/chatroom-types';

function* initChannelsSaga() {
try {
Expand Down Expand Up @@ -38,8 +37,8 @@ function* joinChannel(action: any) {
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 });
yield put({ type: PICK_CHANNEL_ASYNC, payload: { selectedChatroomId: chatroomId } });
} catch (e) {
console.log(e);
}
Expand Down
Loading

0 comments on commit 852f369

Please sign in to comment.