Skip to content

Commit 852f369

Browse files
authored
Merge pull request #207 from boostcamp-2020/develop
Release Thread Page, User Modal
2 parents 60a4060 + 52d14c3 commit 852f369

File tree

76 files changed

+1293
-186
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

76 files changed

+1293
-186
lines changed

README.md

+111-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
# 팀 협업 툴 - Black
2-
![black](https://user-images.githubusercontent.com/33643752/99354886-990a9d00-28ea-11eb-9f75-784658906994.png)
2+
3+
<div align="middle">
4+
<img src="https://user-images.githubusercontent.com/59037261/102005062-4d1c0e00-3d59-11eb-8eff-3505540fc468.gif">
5+
</div>
36

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

31-
| 강동훈 | 김도호 | 탁성건 |
34+
## :house: [HomePage](http://black-boostcamp.kro.kr)
35+
36+
<br />
37+
38+
## :bookmark_tabs: 프로젝트 소개
39+
40+
팀 협업 메신저 Black은 Slack을 Clone한 프로젝트입니다. 또한 채널을 통한 메신저를 구축하면서 업무간 필요한 정보를 공유하는 웹 플랫폼입니다.
41+
42+
<br />
43+
44+
## :gear: 주요 기능
45+
46+
### :speech_balloon: Channel / DM
47+
48+
- 채널 목록 조회
49+
- 채널 생성 (Private / Public)
50+
- 채널에 참여중인 사용자 조회
51+
- DM 생성
52+
53+
### :family: User
54+
55+
- GitHub 로그인
56+
- 프로필 확인
57+
- 로그아웃
58+
59+
### :email: Message
60+
61+
- 실시간 메시지 보내기
62+
- 실시간 메시지 리액션 추가
63+
64+
### :incoming_envelope: Thread
65+
66+
- 실시간 댓글 추가
67+
- 실시간 댓글 리액션 추가
68+
69+
<br />
70+
71+
## :man_cartwheeling: 팀원 소개
72+
73+
<div align="middle">
74+
75+
| J003 강동훈 | J030 김도호 | J211 탁성건 |
3276
| :----------------------------------------------------------: | :----------------------------------------------------------: | :----------------------------------------------------------: |
33-
| <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%> |
77+
| <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> |
3478
| 아.. 폰 보느라<br />코딩 못했다.. :joy: | 나는 개발자인<br />티를 내기 위해<br />노력했다 :computer: | 나는 내 위가 없기 때문에<br />밑만 바라본다 :see_no_evil: |
3579

80+
</div>
81+
82+
<br />
83+
84+
## :hammer_and_wrench: 기술 스택
85+
86+
![기술 스택](https://user-images.githubusercontent.com/59037261/102005071-5a38fd00-3d59-11eb-8988-74c3d8d00767.JPG)
87+
88+
<br />
89+
90+
## :pushpin: 기술 특장점
91+
92+
### :page_with_curl: Swagger Hub를 이용한 API 명세서 작성
93+
94+
Swagger Hub를 이용해 API 명세서를 작성함으로써 FE/BE 협업을 쉽게 할 수 있도록 했습니다. 실제 사용되는 Parameter로 테스트할 수 있고, 어떤 방식으로 데이터를 주고받을지 확인할 수 있어서 개발 시간을 단축하고 불필요한 의사소통 비용을 줄일 수 있었습니다.
95+
96+
___
97+
98+
### :rainbow: CI/CD Pipeline
99+
100+
CI/CD Pipeline을 구축해서 배포를 자동화했습니다. develop branch에서 개발을 진행하고, 배포 버전을 master branch에 PR을 남긴 후 merge를 하면 GitHub WebHook이 발생하도록 했습니다. Jenkins가 이를 감지해서 새롭게 작성한 코드를 기존에 작성한 스크립트를 활용하여 자동화된 통합, 빌드 및 배포를 진행합니다.
101+
102+
이렇게 자동화된 지속적 통합, 지속적 배포를 통해 개발자는 편리한 개발 환경을 구축할 수 있습니다.
103+
104+
___
105+
106+
### :cyclone: docker-compose를 활용한 무중단 배포 (blue/green)
107+
108+
docker-compose와 nginx를 이용하여 blue/green 배포 전략을 활용해 무중단 배포를 구현했습니다. nginx의 load balancing을 활용해 2개의 포트로 트래픽이 갈 수 있도록 설정합니다. 그리고 새롭게 배포되는 과정에서 docker-compose를 활용해 기존에 배포된 컨테이너와 다른 포트로 컨테이너를 생성하고 완료되면 기존의 컨테이너를 삭제합니다.
109+
110+
이를 통해 사용자는 새롭게 배포되는 과정에서 끊임 없는 서비스를 제공받을 수 있습니다.
111+
112+
___
113+
114+
### :closed_book: Atomic Design과 Storybook
115+
116+
슬랙을 구현함에 있어서 프로필 이미지나 메시지, 입력창 등 일관된 디자인의 컴포넌트들이 많다는 생각을 했습니다. 그래서 Atomic Design을 적용해 단계를 나눠 작은 컴포넌트를 만들고, 그것들을 결합해 조금 더 큰 단위의 뷰를 만들었습니다.
117+
118+
이를 통해 재사용 가능하고 일관된 디자인의 컴포넌트를 제작할 수 있었습니다. 또한 Storybook을 도입하여 디자인을 쉽게 수정하고 확인할 수 있도록 했습니다.
119+
120+
___
121+
122+
### :atom_symbol: Redux를 사용한 상태관리
123+
124+
슬랙은 다수의 사용자가 공동으로 한 채널에서 작업을 할 수 있다는 점에서 트랜잭션 상태 관리가 중요하다고 생각이 들었습니다. 그래서 컴포넌트 간 상태 관리 로직을 관리하고 사용자의 액션과 데이터의 변경을 전역으로 관찰할 수 있다는 점에서 Redux와 비동기적 작업을 처리하기 위한 Redux-Saga를 채택하였습니다.
125+
126+
___
127+
128+
### :page_facing_up: Message Paging
129+
130+
채널에 메시지가 늘어남에 따라 한 번에 여러 메시지를 받아오는 방식은 좋지 않다고 생각했습니다. 결국 Message Paging에 대해서 고려하게 되었고 offset과 limit을 두어 구현하게 되었습니다. offset으로는 가지고 있는 메시지의 가장 오래된 메시지 ID를 보내게 됩니다.
131+
132+
Client 측에서는 Infinite Scroll을 구현하여 첫 렌더링을 빠르게 하기 위한 노력을 했습니다. 일정한 스크롤의 위치를 넘게 되면 요청을 보내게 되어 다음 메시지를 계속적으로 받아오게 됩니다.
133+
134+
___
135+
136+
### :blue_book: 프로젝트 전체에 TypeScript 도입
137+
138+
TypeScript는 에러를 사전에 방지하고, 코드 자동 완성 및 가이드 기능을 사용할 수 있다는 장점을 가지고 있습니다. 이러한 장점을 프로젝트에 적용하고자 프로젝트 시작 전 다 같이 강좌를 구매하여 학습하였고 FE, BE 프로젝트에 직접 기술적으로 도전하게 되었습니다.
139+
140+
___
141+
142+
<br />
143+
36144
### 프로젝트가 궁금하다면 [Wiki](https://github.com/boostcamp-2020/Project12-B-Slack-Web/wiki)~ :airplane:

client/public/imgs/close-icon.png

22.1 KB
Loading

client/src/common/constants/index.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { DefaultSectionName } from './default-section-name';
22
import { KeyCode } from './key-code';
3-
import { ChatroomEventType } from './chatroom-event-type';
3+
import { ScrollEventType } from './scroll-event-type';
44

5-
export { DefaultSectionName, KeyCode, ChatroomEventType };
5+
export { DefaultSectionName, KeyCode, ScrollEventType };

client/src/common/constants/chatroom-event-type.ts renamed to client/src/common/constants/scroll-event-type.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
export const ChatroomEventType = {
1+
export const ScrollEventType = {
22
COMMON: 'Common',
33
LOADING: 'Loading',
44
COMPLETELOADING: 'Complete loading',
+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import { JOIN_CHATROOM, joinChatroomState } from '@socket/types/chatroom-types';
1+
import { JOIN_CHATROOM } from '@socket/types/chatroom-types';
22
import socket from '../socketIO';
33

4-
export const joinChatroom = (chatroomId: joinChatroomState) => {
4+
export const joinChatroom = (chatroomId: number) => {
55
socket.emit(JOIN_CHATROOM, { chatroomId });
66
};
+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { CREATE_REPLY, createThreadState } from '@socket/types/thread-types';
2+
import socket from '../socketIO';
3+
4+
export const createReply = (reply: createThreadState) => {
5+
socket.emit(CREATE_REPLY, reply);
6+
};
Original file line numberDiff line numberDiff line change
@@ -1,5 +1 @@
11
export const JOIN_CHATROOM = 'join chatroom';
2-
3-
export interface joinChatroomState {
4-
chatroomId: number;
5-
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export const CREATE_REPLY = 'create reply';
2+
3+
export interface createThreadState {
4+
content: string;
5+
messageId: number | null;
6+
}

client/src/common/store/actions/chatroom-action.ts

+5-2
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,16 @@ import {
55
INSERT_MESSAGE,
66
ADD_CHANNEL_ASYNC,
77
RESET_SELECTED_CHANNEL,
8-
LOAD_NEXT_MESSAGES_ASYNC
8+
LOAD_NEXT_MESSAGES_ASYNC,
9+
UPDATE_THREAD,
10+
insertMessageState
911
} from '../types/chatroom-types';
1012

1113
export const loadAsync = (payload: any) => ({ type: LOAD_ASYNC, payload });
1214
export const initSidebarAsync = () => ({ type: INIT_SIDEBAR_ASYNC });
1315
export const pickChannel = (payload: any) => ({ type: PICK_CHANNEL_ASYNC, payload });
14-
export const insertMessage = (payload: any) => ({ type: INSERT_MESSAGE, payload });
16+
export const insertMessage = (payload: insertMessageState) => ({ type: INSERT_MESSAGE, payload });
1517
export const addChannel = (payload: any) => ({ type: ADD_CHANNEL_ASYNC, payload });
1618
export const resetSelectedChannel = () => ({ type: RESET_SELECTED_CHANNEL });
1719
export const loadNextMessages = (payload: any) => ({ type: LOAD_NEXT_MESSAGES_ASYNC, payload });
20+
export const updateThread = (payload: any) => ({ type: UPDATE_THREAD, payload });

client/src/common/store/actions/modal-action.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ import {
44
CHANNEL_MODAL_OPEN,
55
CHANNEL_MODAL_CLOSE,
66
USERBOX_MODAL_OPEN,
7-
USERBOX_MODAL_CLOSE
7+
USERBOX_MODAL_CLOSE,
8+
PROFILE_MODAL_OPEN,
9+
PROFILE_MODAL_CLOSE
810
} from '@store/types/modal-types';
911

1012
export const createModalOpen = () => ({ type: CREATE_MODAL_OPEN });
@@ -13,3 +15,5 @@ export const channelModalOpen = (payload: any) => ({ type: CHANNEL_MODAL_OPEN, p
1315
export const channelModalClose = () => ({ type: CHANNEL_MODAL_CLOSE });
1416
export const userboxModalOpen = () => ({ type: USERBOX_MODAL_OPEN });
1517
export const userboxModalClose = () => ({ type: USERBOX_MODAL_CLOSE });
18+
export const profileModalOpen = (payload: any) => ({ type: PROFILE_MODAL_OPEN, payload });
19+
export const profileModalClose = () => ({ type: PROFILE_MODAL_CLOSE });
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { LOAD_THREAD_ASYNC, INSERT_REPLY, LOAD_NEXT_REPLIES_ASYNC, replyState } from '@store/types/thread-types';
2+
3+
export const loadThread = (messageId: number) => ({ type: LOAD_THREAD_ASYNC, payload: { messageId } });
4+
export const InsertReply = (payload: replyState) => ({ type: INSERT_REPLY, payload });
5+
export const loadNextReplies = (payload: any) => ({ type: LOAD_NEXT_REPLIES_ASYNC, payload });

client/src/common/store/reducers/chatroom-reducer.ts

+24-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1+
/* eslint-disable no-param-reassign */
12
/* eslint-disable no-case-declarations */
23
import { uriParser } from '@utils/index';
4+
import { joinChatroom } from '@socket/emits/chatroom';
5+
import { messageState } from '@store/types/message-types';
36
import {
47
chatroomState,
58
LOAD,
@@ -9,7 +12,8 @@ import {
912
INSERT_MESSAGE,
1013
ADD_CHANNEL,
1114
RESET_SELECTED_CHANNEL,
12-
LOAD_NEXT_MESSAGES
15+
LOAD_NEXT_MESSAGES,
16+
UPDATE_THREAD
1317
} from '../types/chatroom-types';
1418

1519
const initialState: chatroomState = {
@@ -30,7 +34,7 @@ const initialState: chatroomState = {
3034
messages: []
3135
};
3236

33-
export default function chatroomReducer(state = initialState, action: ChatroomTypes) {
37+
const chatroomReducer = (state = initialState, action: ChatroomTypes) => {
3438
switch (action.type) {
3539
case LOAD:
3640
return {
@@ -63,6 +67,7 @@ export default function chatroomReducer(state = initialState, action: ChatroomTy
6367
case ADD_CHANNEL:
6468
const newChannels = state.channels;
6569
newChannels.push(action.payload);
70+
joinChatroom(action.payload.chatroomId);
6671
return {
6772
...state,
6873
channels: newChannels
@@ -90,7 +95,23 @@ export default function chatroomReducer(state = initialState, action: ChatroomTy
9095
...state,
9196
messages: nextMessages
9297
};
98+
case UPDATE_THREAD:
99+
const updateMessages = state.messages;
100+
const { messageId, profileUri } = action.payload;
101+
updateMessages.forEach((message: messageState) => {
102+
if (message.messageId === messageId) {
103+
message.thread.profileUris.push(profileUri);
104+
message.thread.replyCount += 1;
105+
message.thread.lastReplyAt = new Date();
106+
}
107+
});
108+
return {
109+
...state,
110+
messages: updateMessages
111+
};
93112
default:
94113
return state;
95114
}
96-
}
115+
};
116+
117+
export default chatroomReducer;

client/src/common/store/reducers/index.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,14 @@ import userReducer from './user-reducer';
33
import chatroomReducer from './chatroom-reducer';
44
import modalReducer from './modal-reducer';
55
import channelReducer from './channel-reducer';
6+
import threadReducer from './thread-reducer';
67

78
export const rootReducer = combineReducers({
89
user: userReducer,
910
chatroom: chatroomReducer,
1011
modal: modalReducer,
11-
channel: channelReducer
12+
channel: channelReducer,
13+
thread: threadReducer
1214
});
1315

1416
export type RootState = ReturnType<typeof rootReducer>;

client/src/common/store/reducers/modal-reducer.ts

+10-2
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,16 @@ import {
66
CHANNEL_MODAL_OPEN,
77
CHANNEL_MODAL_CLOSE,
88
USERBOX_MODAL_OPEN,
9-
USERBOX_MODAL_CLOSE
9+
USERBOX_MODAL_CLOSE,
10+
PROFILE_MODAL_OPEN,
11+
PROFILE_MODAL_CLOSE
1012
} from '@store/types/modal-types';
1113

1214
const initialState: ModalState = {
1315
createModal: { isOpen: false },
1416
channelModal: { isOpen: false, x: 0, y: 0 },
15-
userboxModal: { isOpen: false }
17+
userboxModal: { isOpen: false },
18+
profileModal: { isOpen: false, x: 0, y: 0, userId: 0, profileUri: '', displayName: '' }
1619
};
1720

1821
const ModalReducer = (state = initialState, action: ModalTypes) => {
@@ -29,6 +32,11 @@ const ModalReducer = (state = initialState, action: ModalTypes) => {
2932
return { ...state, userboxModal: { isOpen: true } };
3033
case USERBOX_MODAL_CLOSE:
3134
return { ...state, userboxModal: { isOpen: false } };
35+
case PROFILE_MODAL_OPEN:
36+
const { userId, profileUri, displayName } = action.payload;
37+
return { ...state, profileModal: { isOpen: true, x: action.payload.x, y: action.payload.y, userId, profileUri, displayName } };
38+
case PROFILE_MODAL_CLOSE:
39+
return { ...state, profileModal: { isOpen: false } };
3240
default:
3341
return state;
3442
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { LOAD_THREAD, threadState, INSERT_REPLY, ThreadTypes, LOAD_NEXT_REPLIES } from '@store/types/thread-types';
2+
3+
const initialState: threadState = {
4+
message: {
5+
messageId: 0,
6+
content: '',
7+
createdAt: new Date(),
8+
updateAt: new Date(),
9+
deleteAt: new Date(),
10+
user: {
11+
userId: 0,
12+
profileUri: '',
13+
displayName: ''
14+
},
15+
chatroom: {},
16+
messageReactions: []
17+
},
18+
replies: []
19+
};
20+
21+
export default function threadReducer(state = initialState, action: ThreadTypes) {
22+
switch (action.type) {
23+
case LOAD_THREAD:
24+
return {
25+
...state,
26+
message: action.payload.message,
27+
replies: action.payload.replies
28+
};
29+
case INSERT_REPLY:
30+
const newReplies = state.replies;
31+
if (action.payload.messageId === state.message.messageId) newReplies.push(action.payload);
32+
33+
return {
34+
...state,
35+
messages: newReplies
36+
};
37+
case LOAD_NEXT_REPLIES:
38+
const nextreplies = action.payload.replies;
39+
nextreplies.push(...state.replies);
40+
41+
return {
42+
...state,
43+
replies: nextreplies
44+
};
45+
default:
46+
return state;
47+
}
48+
}

client/src/common/store/sagas/channel-saga.ts

+2-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { call, put, takeEvery } from 'redux-saga/effects';
22
import API from '@utils/api';
3-
import { joinChatroom } from '@socket/emits/chatroom';
43
import {
54
INIT_CHANNELS,
65
INIT_CHANNELS_ASYNC,
@@ -9,7 +8,7 @@ import {
98
LOAD_NEXT_CHANNELS,
109
LOAD_NEXT_CHANNELS_ASYNC
1110
} from '../types/channel-types';
12-
import { ADD_CHANNEL } from '../types/chatroom-types';
11+
import { ADD_CHANNEL, PICK_CHANNEL_ASYNC } from '../types/chatroom-types';
1312

1413
function* initChannelsSaga() {
1514
try {
@@ -38,8 +37,8 @@ function* joinChannel(action: any) {
3837
const chatroom = yield call(API.getChatroom, chatroomId);
3938
const { chatType, isPrivate, title } = chatroom;
4039
const payload = { chatroomId, chatType, isPrivate, title };
41-
joinChatroom(chatroomId);
4240
yield put({ type: ADD_CHANNEL, payload });
41+
yield put({ type: PICK_CHANNEL_ASYNC, payload: { selectedChatroomId: chatroomId } });
4342
} catch (e) {
4443
console.log(e);
4544
}

0 commit comments

Comments
 (0)