├── .gitignore ├── client ├── src │ ├── routes │ │ ├── index.ts │ │ └── MenuRoute.tsx │ ├── components │ │ ├── menu │ │ │ ├── index.ts │ │ │ └── MenuSideBar.tsx │ │ ├── signup │ │ │ ├── index.ts │ │ │ ├── Header.tsx │ │ │ └── Content.tsx │ │ ├── chatting │ │ │ ├── index.ts │ │ │ ├── Header.tsx │ │ │ ├── Content.tsx │ │ │ └── NewChattingWindow.tsx │ │ ├── profile │ │ │ ├── index.ts │ │ │ ├── Menu.tsx │ │ │ ├── ProfileInputWindow.tsx │ │ │ ├── SettingBlock.tsx │ │ │ └── UserProfile.tsx │ │ ├── login │ │ │ ├── index.ts │ │ │ ├── Header.tsx │ │ │ ├── Footer.tsx │ │ │ └── Content.tsx │ │ ├── chattingRoom │ │ │ ├── index.ts │ │ │ ├── Header.tsx │ │ │ ├── Footer.tsx │ │ │ ├── ChatBlock.tsx │ │ │ ├── Content.tsx │ │ │ └── InfoBlock.tsx │ │ └── friends │ │ │ ├── index.ts │ │ │ ├── Header.tsx │ │ │ ├── FindFriendWindow.tsx │ │ │ ├── Content.tsx │ │ │ └── FoundFriendProfile.tsx │ ├── types │ │ ├── friend.ts │ │ ├── base.ts │ │ ├── auth.ts │ │ ├── profile.ts │ │ ├── user.ts │ │ └── chatting.ts │ ├── pages │ │ ├── index.ts │ │ ├── Menu.tsx │ │ ├── ChattingRoom.tsx │ │ ├── Signup.tsx │ │ ├── Login.tsx │ │ └── Modal.tsx │ ├── store │ │ ├── sagas │ │ │ ├── index.ts │ │ │ ├── chat.ts │ │ │ ├── auth.ts │ │ │ ├── profile.ts │ │ │ └── user.ts │ │ ├── reducers │ │ │ ├── index.ts │ │ │ ├── profile │ │ │ │ └── index.ts │ │ │ ├── auth │ │ │ │ └── index.ts │ │ │ ├── user │ │ │ │ └── index.ts │ │ │ └── chat │ │ │ │ └── index.ts │ │ ├── index.ts │ │ └── actions │ │ │ ├── auth.ts │ │ │ ├── profile.ts │ │ │ ├── chat.ts │ │ │ └── user.ts │ ├── constants.ts │ ├── index.tsx │ ├── containers │ │ ├── index.ts │ │ ├── signup │ │ │ └── SignupContainer.tsx │ │ ├── login │ │ │ └── LoginContainer.tsx │ │ ├── friends │ │ │ └── FriendsContainer.tsx │ │ ├── chatting │ │ │ └── ChattingContainer.tsx │ │ ├── profile │ │ │ └── ProfileContainer.tsx │ │ ├── menu │ │ │ └── MenuContainer.tsx │ │ └── chattingRoom │ │ │ └── ChattingRoomContainer.tsx │ ├── App.tsx │ ├── apis │ │ ├── friend.ts │ │ ├── auth.ts │ │ ├── chat.ts │ │ └── user.ts │ └── styles │ │ ├── GlobalStyle.ts │ │ └── BaseStyle.ts ├── public │ ├── asset │ │ ├── kakao_logo.png │ │ └── base_profile.jpg │ └── index.html ├── .babelrc ├── tsconfig.json ├── webpack.config.js └── package.json ├── server ├── nodemon.json ├── src │ ├── config.ts.template │ ├── models │ │ ├── Room.ts │ │ ├── Chatting.ts │ │ ├── Friend.ts │ │ ├── Participant.ts │ │ ├── User.ts │ │ └── index.ts │ ├── logger.ts │ ├── routes │ │ ├── auth.ts │ │ ├── user.ts │ │ ├── friend.ts │ │ └── chat.ts │ ├── types │ │ └── chat.ts │ ├── web.ts │ └── sockets │ │ └── index.ts ├── tsconfig.json └── package.json ├── package.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log 3 | config.ts 4 | *.lock 5 | uploads -------------------------------------------------------------------------------- /client/src/routes/index.ts: -------------------------------------------------------------------------------- 1 | export { default as MenuRoute } from './MenuRoute'; 2 | -------------------------------------------------------------------------------- /client/src/components/menu/index.ts: -------------------------------------------------------------------------------- 1 | export { default as MenuSideBar } from './MenuSideBar'; -------------------------------------------------------------------------------- /client/public/asset/kakao_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eastshine94/KakaoTalk/HEAD/client/public/asset/kakao_logo.png -------------------------------------------------------------------------------- /client/public/asset/base_profile.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eastshine94/KakaoTalk/HEAD/client/public/asset/base_profile.jpg -------------------------------------------------------------------------------- /client/src/components/signup/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Header } from './Header'; 2 | export { default as Content } from './Content'; -------------------------------------------------------------------------------- /client/src/components/chatting/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Header } from './Header'; 2 | export { default as Content } from './Content'; 3 | -------------------------------------------------------------------------------- /client/src/components/profile/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Menu } from './Menu'; 2 | export { default as UserProfile } from './UserProfile'; -------------------------------------------------------------------------------- /server/nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": ["./src/**/*.ts"], 3 | "ext": "ts", 4 | "exec": "NODE_ENV=development ts-node ./src/web.ts" 5 | } 6 | -------------------------------------------------------------------------------- /client/src/types/friend.ts: -------------------------------------------------------------------------------- 1 | export interface AddFriendRequestDto { 2 | my_id: number; 3 | friend_id: number; 4 | friend_name: string; 5 | } 6 | -------------------------------------------------------------------------------- /client/src/components/login/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Header } from './Header'; 2 | export { default as Content } from './Content'; 3 | export { default as Footer } from './Footer'; 4 | -------------------------------------------------------------------------------- /server/src/config.ts.template: -------------------------------------------------------------------------------- 1 | export default { 2 | auth: { 3 | key: 'auth-key' 4 | }, 5 | db: { 6 | url: 'mysql://USER:PASSWORD@@DB_URL:DB_PORT/DB_NAME' 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /client/src/components/chattingRoom/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Header } from './Header'; 2 | export { default as Content } from './Content'; 3 | export { default as Footer } from './Footer'; 4 | -------------------------------------------------------------------------------- /client/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets" : ["@babel/preset-env", "@babel/preset-react"], 3 | "env": { 4 | "development": { 5 | "plugins": ["babel-plugin-styled-components"] 6 | } 7 | } 8 | } -------------------------------------------------------------------------------- /client/src/components/friends/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Header } from './Header'; 2 | export { default as Content } from './Content'; 3 | export { default as FindFriendWindow } from './FindFriendWindow'; 4 | -------------------------------------------------------------------------------- /client/src/types/base.ts: -------------------------------------------------------------------------------- 1 | import { AxiosResponse } from 'axios'; 2 | 3 | interface Response { 4 | data: T; 5 | count?: number; 6 | msg?: string; 7 | } 8 | 9 | export type ApiResponse = AxiosResponse>; 10 | -------------------------------------------------------------------------------- /client/src/pages/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Menu } from './Menu'; 2 | export { default as Login } from './Login'; 3 | export { default as Signup } from './Signup'; 4 | export { default as Modal } from './Modal'; 5 | export { default as ChattingRoom } from './ChattingRoom'; 6 | -------------------------------------------------------------------------------- /client/src/types/auth.ts: -------------------------------------------------------------------------------- 1 | export interface LoginData { 2 | userId: string; 3 | password: string; 4 | } 5 | 6 | export interface SignupData { 7 | userId: string; 8 | password: string; 9 | name: string; 10 | } 11 | 12 | // token Decord 시 나오는 유저 정보 13 | export interface Auth { 14 | id: number; 15 | user_id: string; 16 | } 17 | -------------------------------------------------------------------------------- /server/src/models/Room.ts: -------------------------------------------------------------------------------- 1 | import { Model } from 'sequelize'; 2 | 3 | export default class Room extends Model { 4 | public id!: number; 5 | public identifier!: string; 6 | public type!: 'individual' | 'group'; 7 | public last_chat!: string; 8 | 9 | public readonly createdAt!: Date; 10 | public readonly updatedAt!: Date; 11 | } 12 | -------------------------------------------------------------------------------- /server/src/models/Chatting.ts: -------------------------------------------------------------------------------- 1 | import { Model } from 'sequelize'; 2 | 3 | export default class Chatting extends Model { 4 | public id!: number; 5 | public room_id!: number; 6 | public send_user_id!: number; 7 | public message!: string; 8 | public not_read!: number; 9 | 10 | public readonly createdAt!: Date; 11 | public readonly updatedAt!: Date; 12 | } 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chatting", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "server": "cd server && yarn start", 8 | "client": "cd client && yarn start", 9 | "dev": "concurrently --kill-others-on-fail \"yarn server\" \"yarn client\"" 10 | }, 11 | "author": "", 12 | "license": "ISC" 13 | } 14 | -------------------------------------------------------------------------------- /client/src/pages/Menu.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import { MenuContainer } from '~/containers'; 4 | 5 | const Wrapper = styled.div` 6 | width: 100%; 7 | `; 8 | 9 | const Menu: React.FC = () => { 10 | return ( 11 | 12 | 13 | 14 | ); 15 | }; 16 | 17 | export default Menu; 18 | -------------------------------------------------------------------------------- /client/src/types/profile.ts: -------------------------------------------------------------------------------- 1 | // 프로필 변경 요청 시 2 | export interface ProfileChangeRequestDto { 3 | id: number; 4 | name?: string; 5 | status_msg?: string; 6 | profile_img_url?: string; 7 | background_img_url?: string; 8 | } 9 | 10 | // 친구 이름 변경 요청 시 11 | export interface ChangeFriendNameRequestDto { 12 | my_id: number; 13 | friend_id: number; 14 | friend_name: string; 15 | } 16 | -------------------------------------------------------------------------------- /server/src/models/Friend.ts: -------------------------------------------------------------------------------- 1 | import { Model } from 'sequelize'; 2 | import User from './User'; 3 | 4 | export default class Friend extends Model { 5 | public id!: number; 6 | public my_id!: number; 7 | public friend_id!: number; 8 | public friend_name!: string; 9 | 10 | public readonly User?: User; 11 | public readonly createdAt!: Date; 12 | public readonly updatedAt!: Date; 13 | } 14 | -------------------------------------------------------------------------------- /client/src/store/sagas/index.ts: -------------------------------------------------------------------------------- 1 | import { all, fork } from 'redux-saga/effects'; 2 | import authSaga from './auth'; 3 | import userSaga from './user'; 4 | import profileSaga from './profile'; 5 | import chatSaga from './chat'; 6 | export default function* rootSaga() { 7 | yield all([ 8 | fork(authSaga), 9 | fork(userSaga), 10 | fork(profileSaga), 11 | fork(chatSaga) 12 | ]); 13 | } 14 | -------------------------------------------------------------------------------- /client/src/pages/ChattingRoom.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import { ChattingRoomContainer } from '~/containers'; 4 | 5 | const Wrapper = styled.div` 6 | width: 100%; 7 | `; 8 | 9 | const ChattingRoom: React.FC = () => { 10 | return ( 11 | 12 | 13 | 14 | ); 15 | }; 16 | 17 | export default ChattingRoom; 18 | -------------------------------------------------------------------------------- /client/src/constants.ts: -------------------------------------------------------------------------------- 1 | export enum PAGE_PATHS { 2 | HOME = '/', 3 | LOGIN = '/login', 4 | SIGNUP = '/signup', 5 | MENU = '/menu', 6 | FRIENDS = '/menu/friends', 7 | CHATTING = '/menu/chatting', 8 | CHATTING_ROOM = '/room' 9 | } 10 | 11 | export const HOST = process.env.HOST || 'http://localhost:8001'; 12 | 13 | export const API_HOST = process.env.API_HOST || `${HOST}/api`; 14 | 15 | export const BASE_IMG_URL = '/asset/base_profile.jpg'; 16 | -------------------------------------------------------------------------------- /client/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import {Provider} from 'react-redux'; 4 | import App from "./App"; 5 | import GlobalStyle from '~/styles/GlobalStyle'; 6 | import store from '~/store'; 7 | ReactDOM.render( 8 | 9 | 10 | 11 | 12 | 13 | 14 | ,document.querySelector("#root")); -------------------------------------------------------------------------------- /server/src/models/Participant.ts: -------------------------------------------------------------------------------- 1 | import { Model } from 'sequelize'; 2 | import Room from './Room'; 3 | 4 | export default class Participant extends Model { 5 | public id!: number; 6 | public user_id!: number; 7 | public room_id!: number; 8 | public room_name!: string; 9 | public not_read_chat!: number; 10 | public last_read_chat_id!: number; 11 | 12 | public readonly Room?: Room; 13 | public readonly createdAt!: Date; 14 | public readonly updatedAt!: Date; 15 | } 16 | -------------------------------------------------------------------------------- /client/src/components/login/Header.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | const Wrapper = styled.header` 5 | width: 100%; 6 | height: 200px; 7 | padding-top: 100px; 8 | & img { 9 | display: block; 10 | margin: 0 auto; 11 | } 12 | `; 13 | 14 | const Header: React.FC = () => { 15 | return ( 16 | 17 | logo 18 | 19 | ); 20 | }; 21 | 22 | export default Header; 23 | -------------------------------------------------------------------------------- /client/src/pages/Signup.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import { SignupContainer } from '~/containers'; 4 | 5 | const Wrapper = styled.div` 6 | display: flex; 7 | justify-content: center; 8 | align-items: center; 9 | width: 100%; 10 | min-height: 100vh; 11 | background-color: #f5f6f7; 12 | `; 13 | 14 | const Signup: React.FC = () => { 15 | return ( 16 | 17 | 18 | 19 | ); 20 | }; 21 | 22 | export default Signup; 23 | -------------------------------------------------------------------------------- /client/src/pages/Login.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import { LoginContainer } from '~/containers'; 4 | const Wrapper = styled.div` 5 | display: flex; 6 | justify-content: center; 7 | align-items: center; 8 | width: 100%; 9 | min-height: 100vh; 10 | background-color: #f5f6f7; 11 | padding: 25px 0; 12 | `; 13 | 14 | const Login: React.FC = () => { 15 | return ( 16 | 17 | 18 | 19 | ); 20 | }; 21 | 22 | export default Login; 23 | -------------------------------------------------------------------------------- /client/src/containers/index.ts: -------------------------------------------------------------------------------- 1 | export { default as FriendsContainer } from './friends/FriendsContainer'; 2 | export { default as ChattingContainer } from './chatting/ChattingContainer'; 3 | export { default as LoginContainer } from './login/LoginContainer'; 4 | export { default as SignupContainer } from './signup/SignupContainer'; 5 | export { default as MenuContainer } from './menu/MenuContainer'; 6 | export { default as ProfileContainer } from './profile/ProfileContainer'; 7 | export { default as ChattingRoomContainer } from './chattingRoom/ChattingRoomContainer'; 8 | -------------------------------------------------------------------------------- /client/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Kakao Talk 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | -------------------------------------------------------------------------------- /server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": true, 3 | "compilerOptions": { 4 | "module": "commonjs", 5 | "strict": true, 6 | "baseUrl": "./", 7 | "outDir": "build", 8 | "sourceMap": false, 9 | "removeComments": true, 10 | "experimentalDecorators": true, 11 | "target": "es6", 12 | "emitDecoratorMetadata": true, 13 | "moduleResolution": "node", 14 | "importHelpers": true, 15 | }, 16 | "include": [ 17 | "src/**/*" 18 | ], 19 | "exclude": [ 20 | "node_modules", 21 | "**/*.spec.ts", 22 | "build", 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /server/src/models/User.ts: -------------------------------------------------------------------------------- 1 | import { Model } from 'sequelize'; 2 | import * as bcrypt from 'bcrypt-nodejs'; 3 | 4 | export default class User extends Model { 5 | public id!: number; 6 | public user_id!: string; 7 | public password!: string; 8 | 9 | validPassword(password: string) { 10 | return bcrypt.compareSync(password, this.password); 11 | } 12 | public name!: string; 13 | public status_msg!: string; 14 | public profile_img_url!: string; 15 | public background_img_url!: string; 16 | public readonly createdAt!: Date; 17 | public readonly updatedAt!: Date; 18 | } 19 | -------------------------------------------------------------------------------- /client/src/types/user.ts: -------------------------------------------------------------------------------- 1 | import { RoomListResponse } from './chatting'; 2 | 3 | // user state type 4 | export interface UserData { 5 | id: number; 6 | user_id: string; 7 | name: string; 8 | status_msg: string; 9 | profile_img_url: string; 10 | background_img_url: string; 11 | friends_list: Array; 12 | room_list: Array; 13 | } 14 | 15 | // 서버에서 가져온 유저 정보 16 | export interface UserResponseDto { 17 | id: number; 18 | user_id: string; 19 | name: string; 20 | status_msg: string; 21 | profile_img_url: string; 22 | background_img_url: string; 23 | } 24 | -------------------------------------------------------------------------------- /client/src/store/reducers/index.ts: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import auth, { AuthState } from '~/store/reducers/auth'; 3 | import user, { UserState } from '~/store/reducers/user'; 4 | import profile, { ProfileState } from '~/store/reducers/profile'; 5 | import chat, { ChatState } from '~/store/reducers/chat'; 6 | 7 | export interface RootState { 8 | auth: AuthState; 9 | user: UserState; 10 | profile: ProfileState; 11 | chat: ChatState; 12 | } 13 | 14 | const rootReducer = combineReducers({ 15 | auth, 16 | user, 17 | profile, 18 | chat 19 | }); 20 | 21 | export default rootReducer; 22 | -------------------------------------------------------------------------------- /server/src/logger.ts: -------------------------------------------------------------------------------- 1 | import * as winston from 'winston'; 2 | 3 | const logger = winston.createLogger({ 4 | level: 'info', 5 | format: winston.format.combine( 6 | winston.format.timestamp({ 7 | format: 'YYYY-MM-DD HH:mm:ss' 8 | }), 9 | winston.format.json() 10 | ), 11 | transports: [ 12 | new winston.transports.Console({ 13 | format: winston.format.combine( 14 | winston.format.colorize(), 15 | winston.format.printf( 16 | info => `${info.timestamp} ${info.level}: ${info.message}` 17 | ) 18 | ) 19 | }) 20 | ] 21 | }); 22 | 23 | export default logger; 24 | -------------------------------------------------------------------------------- /client/src/components/login/Footer.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import styled from 'styled-components'; 4 | import { PAGE_PATHS } from '../../constants'; 5 | 6 | const Wrapper = styled.header` 7 | & ul { 8 | display: flex; 9 | justify-content: center; 10 | & li { 11 | color: #5a5a5a; 12 | } 13 | } 14 | `; 15 | 16 | const Footer: React.FC = () => { 17 | return ( 18 | 19 |
    20 |
  • 21 | 회원 가입 22 |
  • 23 |
24 |
25 | ); 26 | }; 27 | 28 | export default Footer; 29 | -------------------------------------------------------------------------------- /client/src/routes/MenuRoute.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Route, Switch, Redirect } from 'react-router-dom'; 3 | import { PAGE_PATHS } from '~/constants'; 4 | import { FriendsContainer, ChattingContainer } from '~/containers'; 5 | 6 | const MenuRoute: React.FC = () => { 7 | return ( 8 | 9 | 10 | 11 | } 14 | /> 15 | 16 | ); 17 | }; 18 | 19 | export default MenuRoute; 20 | -------------------------------------------------------------------------------- /client/src/store/sagas/chat.ts: -------------------------------------------------------------------------------- 1 | import { put, all, call, takeLatest } from 'redux-saga/effects'; 2 | import { ChatTypes, FetchChattingAction } from '~/store/actions/chat'; 3 | import * as chatApi from '~/apis/chat'; 4 | export default function* chatSaga() { 5 | yield all([takeLatest(ChatTypes.FETCH_CHATTING_REQUEST, $fetchChatting)]); 6 | } 7 | 8 | function* $fetchChatting(action: FetchChattingAction) { 9 | try { 10 | const chatting = yield call(chatApi.fetchChatting, action.payload); 11 | yield put({ 12 | type: ChatTypes.FETCH_CHATTING_SUCCESS, 13 | payload: chatting 14 | }); 15 | } catch (err) { 16 | yield put({ 17 | type: ChatTypes.FETCH_CHATTING_FAILUER, 18 | payload: '채팅 목록을 불러오지 못했습니다.' 19 | }); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /client/src/store/index.ts: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware, compose } from 'redux'; 2 | import { createLogger } from 'redux-logger'; 3 | import { composeWithDevTools } from 'redux-devtools-extension'; 4 | import createSagaMiddleware from 'redux-saga'; 5 | import rootReducer from './reducers'; 6 | import rootSaga from './sagas'; 7 | const sagaMiddleware = createSagaMiddleware(); 8 | 9 | const logger = createLogger({ 10 | collapsed: true 11 | }); 12 | 13 | const middleware = [logger, sagaMiddleware]; 14 | const enhancer = 15 | process.env.NODE_ENV === 'production' 16 | ? compose(applyMiddleware(sagaMiddleware)) 17 | : composeWithDevTools(applyMiddleware(...middleware)); 18 | 19 | const store = createStore(rootReducer, enhancer); 20 | sagaMiddleware.run(rootSaga); 21 | 22 | export default store; 23 | -------------------------------------------------------------------------------- /client/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { 3 | BrowserRouter as Router, 4 | Switch, 5 | Route, 6 | Redirect 7 | } from 'react-router-dom'; 8 | import { Menu, Login, Signup } from '~/pages'; 9 | import { PAGE_PATHS } from '~/constants'; 10 | 11 | class App extends Component { 12 | render() { 13 | return ( 14 | 15 | 16 | 17 | 18 | 19 | } 22 | /> 23 | 24 | 25 | ); 26 | } 27 | } 28 | 29 | export default App; 30 | -------------------------------------------------------------------------------- /client/src/components/signup/Header.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import { Link } from 'react-router-dom'; 4 | import { PAGE_PATHS } from '~/constants'; 5 | 6 | const Wrapper = styled.header` 7 | width: 100%; 8 | height: 100px; 9 | & h2 { 10 | text-align: center; 11 | } 12 | `; 13 | 14 | const LogoLink = styled(Link)` 15 | font-size: 50px; 16 | font-weight: bold; 17 | text-transform: uppercase; 18 | letter-spacing: 8px; 19 | color: #ffeb33; 20 | text-shadow: -1px 0 #dcdcdc, 0 1px #dcdcdc, 1px 0 #dcdcdc, 0 -1px #dcdcdc; 21 | `; 22 | 23 | const Header: React.FC = () => { 24 | return ( 25 | 26 |

27 | kakao 28 |

29 |
30 | ); 31 | }; 32 | 33 | export default Header; 34 | -------------------------------------------------------------------------------- /client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "esnext", 4 | "target": "es5", 5 | "lib": ["es6", "dom"], 6 | "sourceMap": true, 7 | "allowJs": true, 8 | "jsx": "preserve", 9 | "moduleResolution": "node", 10 | "rootDir": "src", 11 | "forceConsistentCasingInFileNames": true, 12 | "noImplicitReturns": true, 13 | "noImplicitThis": true, 14 | "noImplicitAny": true, 15 | "noUnusedParameters": true, 16 | "importHelpers": true, 17 | "strictNullChecks": true, 18 | "suppressImplicitAnyIndexErrors": true, 19 | "noUnusedLocals": true, 20 | "skipLibCheck": true, 21 | "esModuleInterop": true, 22 | "allowSyntheticDefaultImports": true, 23 | "strict": true, 24 | "resolveJsonModule": true, 25 | "isolatedModules": true, 26 | "experimentalDecorators": true, 27 | "baseUrl": ".", 28 | "paths": { 29 | "~/*": ["src/*"] 30 | } 31 | }, 32 | "exclude": ["node_modules", "build", ".cache"], 33 | "include": ["src/**/*.tsx", "src/**/*.ts"] 34 | } -------------------------------------------------------------------------------- /client/src/apis/friend.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { API_HOST } from '~/constants'; 3 | import { UserResponseDto } from '~/types/user'; 4 | import { AddFriendRequestDto } from '~/types/friend'; 5 | import { ChangeFriendNameRequestDto } from '~/types/profile'; 6 | import { ApiResponse } from '~/types/base'; 7 | 8 | // 친구 추가 요청 9 | export const addFriendRequest = async (request: AddFriendRequestDto) => { 10 | const addedFriend: ApiResponse = await axios.post( 11 | `${API_HOST}/friend/add`, 12 | request 13 | ); 14 | return addedFriend.data.data; 15 | }; 16 | 17 | // 친구 목록 가져옴 18 | export const fecthFriendsRequest = async (id: number) => { 19 | const friends: ApiResponse> = await axios.get( 20 | `${API_HOST}/friend/${id}` 21 | ); 22 | return friends.data.data; 23 | }; 24 | 25 | // 친구 이름 변경 요청 26 | export const changeFriendNameRequest = async ( 27 | request: ChangeFriendNameRequestDto 28 | ) => { 29 | const response: ApiResponse = await axios.post( 30 | `${API_HOST}/friend/profile/change`, 31 | request 32 | ); 33 | return response.data.data; 34 | }; 35 | -------------------------------------------------------------------------------- /client/src/store/sagas/auth.ts: -------------------------------------------------------------------------------- 1 | import { all, put, call, takeLatest } from 'redux-saga/effects'; 2 | import jwtDecode from 'jwt-decode'; 3 | import { AuthTypes, LoginAction } from '~/store/actions/auth'; 4 | import { UserTypes } from '~/store/actions/user'; 5 | import * as authApi from '~/apis/auth'; 6 | 7 | export default function* authSaga() { 8 | yield all([ 9 | takeLatest(AuthTypes.LOGIN_REQUEST, login$), 10 | takeLatest(AuthTypes.LOGOUT, logout$) 11 | ]); 12 | } 13 | 14 | function* login$(action: LoginAction) { 15 | try { 16 | const loginData = action.payload; 17 | const token = yield call(authApi.login, loginData); 18 | const auth = yield call(jwtDecode, token); 19 | yield put({ 20 | type: AuthTypes.LOGIN_SUCCESS, 21 | payload: { 22 | token, 23 | auth 24 | } 25 | }); 26 | yield window.sessionStorage.setItem('jwt', token); 27 | } catch { 28 | yield put({ 29 | type: AuthTypes.LOGIN_FAILURE, 30 | payload: '계정 또는 비밀번호를 다시 확인해주세요.' 31 | }); 32 | } 33 | } 34 | 35 | function* logout$() { 36 | yield call(authApi.logout); 37 | yield put({ 38 | type: UserTypes.RESET_USER 39 | }); 40 | } 41 | -------------------------------------------------------------------------------- /client/src/apis/auth.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { API_HOST } from '~/constants'; 3 | import { LoginData, SignupData } from '~/types/auth'; 4 | import { ApiResponse } from '~/types/base'; 5 | 6 | interface SignupRequestDto { 7 | user_id: string; 8 | password: string; 9 | name: string; 10 | } 11 | 12 | interface LoginResponseDto { 13 | token: string; 14 | } 15 | 16 | // 서버에 회원가입 요청 17 | export const signup = async (signupData: SignupData) => { 18 | const signupRequest: SignupRequestDto = { 19 | user_id: signupData.userId, 20 | password: signupData.password, 21 | name: signupData.name 22 | }; 23 | await axios.post(`${API_HOST}/auth/signup`, signupRequest); 24 | }; 25 | 26 | // 서버에 로그인 요청 27 | export const login = async (loginData: LoginData) => { 28 | const request = { 29 | user_id: loginData.userId, 30 | password: loginData.password 31 | }; 32 | const response: ApiResponse = await axios.post( 33 | `${API_HOST}/auth/login`, 34 | request 35 | ); 36 | const token = response.data.data.token; 37 | 38 | return token; 39 | }; 40 | 41 | export const logout = () => { 42 | window.sessionStorage.removeItem('jwt'); 43 | }; 44 | -------------------------------------------------------------------------------- /client/src/components/profile/Menu.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | const Wrapper = styled.section` 5 | border-top: 1px solid #fff; 6 | margin-top: 20px; 7 | display: flex; 8 | justify-content: center; 9 | padding: 30px 20px; 10 | & div { 11 | text-align: center; 12 | cursor: pointer; 13 | & i { 14 | color: #fff; 15 | font-size: 20px; 16 | margin-bottom: 5px; 17 | } 18 | } 19 | `; 20 | 21 | interface Props { 22 | isMe: boolean; 23 | isFriend: boolean; 24 | onChatClick(): void; 25 | onAddFriendClick(): void; 26 | } 27 | 28 | const Menu: React.FC = props => { 29 | const { isMe, isFriend, onChatClick, onAddFriendClick } = props; 30 | return ( 31 | 32 | {isFriend || isMe ? ( 33 |
34 | 35 |

{isMe ? '나와의 채팅' : '1:1 채팅'}

36 |
37 | ) : ( 38 |
39 | 40 |

친구 추가

41 |
42 | )} 43 |
44 | ); 45 | }; 46 | 47 | export default Menu; 48 | -------------------------------------------------------------------------------- /client/src/apis/chat.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { ApiResponse } from '~/types/base'; 3 | import { API_HOST } from '~/constants'; 4 | import { 5 | CreateRoomRequest, 6 | CreateRoomResponse, 7 | RoomListResponse, 8 | FetchChattingRequest, 9 | ChattingResponseDto 10 | } from '~/types/chatting'; 11 | 12 | // 채팅방 입장 시, 채팅방 정보를 얻음 13 | export const createRoom = async (param: CreateRoomRequest) => { 14 | const room: ApiResponse = await axios.post( 15 | `${API_HOST}/chat/room/create`, 16 | param 17 | ); 18 | 19 | return room.data.data; 20 | }; 21 | 22 | // 현재 채팅방 목록을 가져옴 23 | export const fetchRoomList = async (userId: number) => { 24 | const roomList: ApiResponse> = await axios.get( 25 | `${API_HOST}/chat/roomList/${userId}` 26 | ); 27 | 28 | return roomList.data.data; 29 | }; 30 | 31 | // 채팅방의 채팅 데이터를 가져옴 32 | export const fetchChatting = async (param: FetchChattingRequest) => { 33 | const { room_id, cursor } = param; 34 | const chatting: ApiResponse> = await axios.get( 35 | `${API_HOST}/chat/room?room_id=${room_id}&cursor=${cursor}` 36 | ); 37 | 38 | return chatting.data.data; 39 | }; 40 | -------------------------------------------------------------------------------- /client/src/components/chattingRoom/Header.tsx: -------------------------------------------------------------------------------- 1 | import React, { MouseEvent } from 'react'; 2 | import styled from 'styled-components'; 3 | const Wrapper = styled.header` 4 | width: 100%; 5 | background-color: #a9bdce; 6 | height: 50px; 7 | & span { 8 | display: inline-block; 9 | font-weight: bold; 10 | font-size: 20px; 11 | margin-left: 10px; 12 | margin-top: 10px; 13 | } 14 | & button { 15 | font-size: 20px; 16 | padding: 10px 10px 10px 30px; 17 | background-color: #a9bdce; 18 | outline: none; 19 | cursor: pointer; 20 | &:hover { 21 | color: #dcdcdc; 22 | } 23 | } 24 | `; 25 | 26 | interface Props { 27 | room_name: string; 28 | hideRoom(): void; 29 | } 30 | 31 | const Header: React.FC = props => { 32 | const { room_name, hideRoom } = props; 33 | const onBackBtnClick = (event: MouseEvent) => { 34 | event.preventDefault(); 35 | hideRoom(); 36 | }; 37 | return ( 38 | 39 | 42 | {room_name} 43 | 44 | ); 45 | }; 46 | 47 | export default Header; 48 | -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "server", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "build": "tsc", 9 | "start": "nodemon --config nodemon.json" 10 | }, 11 | "author": "", 12 | "license": "ISC", 13 | "dependencies": { 14 | "@types/axios": "^0.14.0", 15 | "@types/bcrypt-nodejs": "^0.0.30", 16 | "@types/cors": "^2.8.6", 17 | "@types/express": "^4.17.2", 18 | "@types/jwt-simple": "^0.5.33", 19 | "@types/multer": "^1.4.3", 20 | "@types/node": "^13.5.0", 21 | "@types/sequelize": "^4.28.8", 22 | "@types/socket.io": "^2.1.11", 23 | "@types/winston": "^2.4.4", 24 | "axios": "^0.19.2", 25 | "bcrypt-nodejs": "^0.0.3", 26 | "cors": "^2.8.5", 27 | "express": "^4.17.1", 28 | "jwt-simple": "^0.5.6", 29 | "multer": "^1.4.2", 30 | "mysql2": "^2.1.0", 31 | "nodemon": "^2.0.2", 32 | "passport": "^0.4.1", 33 | "passport-jwt": "^4.0.0", 34 | "path": "^0.12.7", 35 | "sequelize": "^5.21.3", 36 | "socket.io": "^2.3.0", 37 | "winston": "^3.2.1" 38 | }, 39 | "devDependencies": { 40 | "tslib": "^2.0.1", 41 | "win-node-env": "^0.4.0" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /client/src/containers/signup/SignupContainer.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import styled from 'styled-components'; 3 | import { connect } from 'react-redux'; 4 | import { Redirect } from 'react-router-dom'; 5 | import { Header, Content } from '~/components/signup'; 6 | import { RootState } from '~/store/reducers'; 7 | import { AuthState } from '~/store/reducers/auth'; 8 | import { PAGE_PATHS } from '~/constants'; 9 | 10 | const Wrapper = styled.div` 11 | margin: 0 auto; 12 | width: 50%; 13 | min-height: 95vh; 14 | border: 1px solid #dadada; 15 | @media only screen and (max-width: 800px) { 16 | width: 95%; 17 | } 18 | `; 19 | 20 | interface Props { 21 | authState: AuthState; 22 | } 23 | 24 | class SignupContainer extends Component { 25 | render() { 26 | const { token } = this.props.authState; 27 | if (token) return ; 28 | 29 | return ( 30 | 31 |
32 | 33 | 34 | ); 35 | } 36 | } 37 | 38 | const mapStateToProps = (state: RootState) => ({ 39 | authState: state.auth 40 | }); 41 | 42 | const mapDispatchToProps = () => ({}); 43 | 44 | export default connect(mapStateToProps, mapDispatchToProps)(SignupContainer); 45 | -------------------------------------------------------------------------------- /client/src/components/friends/Header.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, ChangeEvent } from 'react'; 2 | import { MainHeader, TitleBlock } from '~/styles/BaseStyle'; 3 | import { FindFriendWindow } from '~/components/friends'; 4 | 5 | interface Props { 6 | changeSearch(value: string): void; 7 | } 8 | 9 | const Header: React.FC = ({ changeSearch }) => { 10 | const [isopenFindFriend, openFindFriend] = useState(false); 11 | 12 | // 친구 찾기 창(modal) 13 | const showFindFriend = isopenFindFriend ? ( 14 | openFindFriend(false)} 16 | overlayClose={false} 17 | /> 18 | ) : null; 19 | 20 | const onSearchChange = (event: ChangeEvent) => { 21 | event.preventDefault(); 22 | changeSearch(event.target.value); 23 | }; 24 | return ( 25 | 26 | {showFindFriend} 27 | 28 | 29 |

친구

30 | openFindFriend(true)} 34 | /> 35 |
36 | 37 |
38 |
39 | ); 40 | }; 41 | 42 | export default Header; 43 | -------------------------------------------------------------------------------- /client/src/styles/GlobalStyle.ts: -------------------------------------------------------------------------------- 1 | import { createGlobalStyle } from 'styled-components'; 2 | 3 | const GlobalStyle = createGlobalStyle` 4 | * { 5 | box-sizing: border-box; 6 | } 7 | body { 8 | width: 100%; 9 | height: 100%; 10 | } 11 | body, div, ul, li, dl, dd, dt, ol, h1, h2, h3, h4, h5, h6, input, fieldset, legend, p, select, table, th, td, tr, textarea, button, form, figure, figcaption { 12 | padding: 0; 13 | margin: 0; 14 | } 15 | 16 | a{ 17 | color: #222; 18 | text-decoration:none; 19 | } 20 | body, input, textarea, select, button, table { 21 | font-family: 'Nanum Gothic', sans-serif; 22 | color: #222; 23 | font-size: 13px; 24 | line-height: 1.5; 25 | } 26 | /* 폰트 스타일 초기화 */ 27 | em, address { 28 | font-style: normal 29 | } 30 | /* 불릿 기호 초기화 */ 31 | dl, ul, li, ol, menu { 32 | list-style: none; 33 | } 34 | 35 | /* 제목 태그 초기화 */ 36 | h1, h2, h3, h4, h5, h6 { 37 | font-size: 13px; 38 | font-weight: normal; 39 | } 40 | /* 버튼 초기화 */ 41 | button { 42 | border: none; 43 | outline: none; 44 | } 45 | 46 | /* 테두리 초기화 */ 47 | img, fieldset { 48 | border: 0 none; 49 | } 50 | `; 51 | 52 | export default GlobalStyle; 53 | -------------------------------------------------------------------------------- /client/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const HtmlWebpackPlugin = require("html-webpack-plugin"); 3 | const { CleanWebpackPlugin } = require('clean-webpack-plugin'); 4 | const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin'); 5 | module.exports = { 6 | mode: process.env.NODE_ENV, 7 | 8 | entry: "./src/index.tsx", 9 | 10 | resolve: { 11 | extensions: [".ts", ".tsx", '.js'], 12 | plugins: [new TsconfigPathsPlugin({ configFile: "./tsconfig.json" })] 13 | }, 14 | devtool: "source-map", 15 | module: { 16 | rules: [ 17 | { 18 | test: /\.tsx?$/, 19 | loader: "babel-loader", 20 | exclude: /node_modules/ 21 | }, 22 | { 23 | test: /\.tsx?$/, 24 | loader: 'ts-loader' 25 | } 26 | ] 27 | }, 28 | devServer: { 29 | contentBase: path.resolve(__dirname, "public"), 30 | compress: true, 31 | historyApiFallback: true, 32 | port:3000, 33 | }, 34 | output: { 35 | path: path.resolve(__dirname, "build"), 36 | publicPath: "/", 37 | filename: "[chunkhash]_bundle.js" 38 | }, 39 | 40 | plugins: [ 41 | new HtmlWebpackPlugin({ 42 | template: path.resolve(__dirname, "./public/index.html"), 43 | }), 44 | new CleanWebpackPlugin({ 45 | cleanAfterEveryBuildPatterns: ['build'] 46 | }) 47 | ] 48 | }; -------------------------------------------------------------------------------- /client/src/apis/user.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { HOST, API_HOST } from '~/constants'; 3 | import { ApiResponse } from '~/types/base'; 4 | import { UserResponseDto } from '~/types/user'; 5 | import { ProfileChangeRequestDto } from '../types/profile'; 6 | 7 | // 서버에서 User ID를 통해 해당 유저의 정보를 가져옴, 회원 가입 여부 등에 사용 8 | export const findUser = async (userId: string) => { 9 | const foundUser: ApiResponse = await axios.get( 10 | `${API_HOST}/user/${userId}` 11 | ); 12 | return foundUser.data.data; 13 | }; 14 | 15 | // UID를 이용하여 유저 정보를 찾는다. 채팅방 참가자의 정보를 가져오기 위해 사용 16 | export const findUserUsingId = async (id: number) => { 17 | const foundUser: ApiResponse = await axios.get( 18 | `${API_HOST}/user/find/${id}` 19 | ); 20 | return foundUser.data.data; 21 | }; 22 | 23 | // 프로필 변경(사진, 배경, 이름 등등) 24 | export const changeProfile = async (profileData: ProfileChangeRequestDto) => { 25 | await axios.post(`${API_HOST}/user/profile/change`, profileData); 26 | return profileData; 27 | }; 28 | 29 | // 이미지를 서버로 업로드 30 | export const uploadImageFile = async (image: File) => { 31 | const formData = new FormData(); 32 | formData.append('image', image); 33 | const imageUrl: ApiResponse = await axios.post( 34 | `${API_HOST}/user/upload`, 35 | formData 36 | ); 37 | return `${HOST}/${imageUrl.data.data}`; 38 | }; 39 | -------------------------------------------------------------------------------- /client/src/store/reducers/profile/index.ts: -------------------------------------------------------------------------------- 1 | import { UserResponseDto } from '~/types/user'; 2 | import { ProfileTypes, ProfileActionTypes } from '~/store/actions/profile'; 3 | export interface ProfileState extends UserResponseDto { 4 | isProfileShown: boolean; 5 | } 6 | 7 | const initialState: ProfileState = { 8 | id: -1, 9 | user_id: '', 10 | name: '', 11 | status_msg: '', 12 | profile_img_url: '', 13 | background_img_url: '', 14 | isProfileShown: false 15 | }; 16 | 17 | const profileReducer = (state = initialState, action: ProfileActionTypes) => { 18 | switch (action.type) { 19 | case ProfileTypes.SHOW_PROFILE: 20 | return { 21 | ...state, 22 | ...action.payload, 23 | isProfileShown: true 24 | }; 25 | case ProfileTypes.HIDE_PROFILE: 26 | return { 27 | ...state, 28 | id: -1, 29 | user_id: '', 30 | name: '', 31 | status_msg: '', 32 | profile_img_url: '', 33 | background_img_url: '', 34 | isProfileShown: false 35 | }; 36 | case ProfileTypes.CHANGE_PROFILE_SUCCESS: 37 | return { 38 | ...state, 39 | ...action.payload 40 | }; 41 | case ProfileTypes.CHANGE_FRIEND_NAME_SUCCESS: 42 | return { 43 | ...state, 44 | name: action.payload 45 | }; 46 | default: 47 | return state; 48 | } 49 | }; 50 | 51 | export default profileReducer; 52 | -------------------------------------------------------------------------------- /client/src/store/actions/auth.ts: -------------------------------------------------------------------------------- 1 | import { LoginData, Auth } from '~/types/auth'; 2 | 3 | export enum AuthTypes { 4 | LOGIN_REQUEST = 'auth/LOGIN_REQUEST', 5 | LOGIN_SUCCESS = 'auth/LOGIN_SUCCESS', 6 | LOGIN_FAILURE = 'auth/LOGIN_FAILURE', 7 | LOGOUT = 'auth/LOGOUT', 8 | CHANGE_MESSAGE = 'auth/CHANGE_MESSAGE' 9 | } 10 | 11 | export interface LoginAction { 12 | type: AuthTypes.LOGIN_REQUEST; 13 | payload: LoginData; 14 | } 15 | export interface LoginSuccessAction { 16 | type: AuthTypes.LOGIN_SUCCESS; 17 | payload: { 18 | token: string; 19 | auth: Auth; 20 | }; 21 | } 22 | export interface LoginFailureAction { 23 | type: AuthTypes.LOGIN_FAILURE; 24 | payload: string; 25 | } 26 | 27 | export interface LogoutAction { 28 | type: AuthTypes.LOGOUT; 29 | } 30 | 31 | // 로그인 실패 등에 따른 message 32 | export interface ChangeMessageAction { 33 | type: AuthTypes.CHANGE_MESSAGE; 34 | payload: string; 35 | } 36 | 37 | export type AuthActionTypes = 38 | | LoginAction 39 | | LoginSuccessAction 40 | | LoginFailureAction 41 | | LogoutAction 42 | | ChangeMessageAction; 43 | 44 | export const login = (loginData: LoginData): LoginAction => ({ 45 | type: AuthTypes.LOGIN_REQUEST, 46 | payload: loginData 47 | }); 48 | 49 | export const logout = (): LogoutAction => ({ 50 | type: AuthTypes.LOGOUT 51 | }); 52 | export const changeMessage = (message: string): ChangeMessageAction => ({ 53 | type: AuthTypes.CHANGE_MESSAGE, 54 | payload: message 55 | }); 56 | export const AuthActions = { 57 | login, 58 | logout, 59 | changeMessage 60 | }; 61 | -------------------------------------------------------------------------------- /server/src/routes/auth.ts: -------------------------------------------------------------------------------- 1 | import * as express from 'express'; 2 | import User from '../models/User'; 3 | import config from '../config'; 4 | import * as jwt from 'jwt-simple'; 5 | 6 | const router = express.Router(); 7 | 8 | router.post('/login', async (req, res) => { 9 | const { user_id, password } = req.body; 10 | try { 11 | const user = await User.findOne({ 12 | where: { user_id } 13 | }); 14 | 15 | if (user) { 16 | const isValidPassword = await user.validPassword(password); 17 | if (isValidPassword) { 18 | const token = jwt.encode( 19 | { id: user.id, user_id: user.user_id }, 20 | config.auth.key 21 | ); 22 | return res.json({ data: { token }, msg: '로그인 성공!' }); 23 | } 24 | } 25 | return res 26 | .status(404) 27 | .json({ msg: '계정 또는 비밀번호를 다시 확인해주세요.' }); 28 | } catch (err) { 29 | return res.status(500).json({ msg: '로그인 실패' }); 30 | } 31 | }); 32 | 33 | router.post('/signup', async (req, res) => { 34 | const { user_id, password, name } = req.body; 35 | 36 | try { 37 | const user = await User.findOne({ 38 | where: { user_id } 39 | }); 40 | if (user) { 41 | return res 42 | .status(400) 43 | .json({ msg: '이미 사용중이거나 탈퇴한 아이디입니다.' }); 44 | } 45 | await User.create({ user_id, password, name }); 46 | return res.json({ 47 | msg: '회원가입 되었습니다.' 48 | }); 49 | } catch (err) { 50 | return res.status(500).json({ 51 | msg: '회원가입 실패' 52 | }); 53 | } 54 | }); 55 | 56 | export default router; 57 | -------------------------------------------------------------------------------- /client/src/components/chatting/Header.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, ChangeEvent } from 'react'; 2 | import { MainHeader, TitleBlock } from '~/styles/BaseStyle'; 3 | import NewChattingWindow from './NewChattingWindow'; 4 | import { UserData } from '~/types/user'; 5 | import { CreateRoomRequest } from '~/types/chatting'; 6 | 7 | interface Props { 8 | userState: UserData; 9 | changeSearch(value: string): void; 10 | showChattingRoom(param: CreateRoomRequest): void; 11 | } 12 | 13 | const Header: React.FC = props => { 14 | const { userState, changeSearch, showChattingRoom } = props; 15 | const [isOpenNewChatting, openNewChatting] = useState(false); 16 | const onSearchChange = (event: ChangeEvent) => { 17 | event.preventDefault(); 18 | changeSearch(event.target.value); 19 | }; 20 | const showCreateNewChatting = isOpenNewChatting ? ( 21 | openNewChatting(false)} 24 | showChattingRoom={showChattingRoom} 25 | /> 26 | ) : null; 27 | return ( 28 | 29 | {showCreateNewChatting} 30 | 31 | 32 |

채팅

33 | openNewChatting(true)} 37 | /> 38 |
39 | 43 |
44 |
45 | ); 46 | }; 47 | 48 | export default Header; 49 | -------------------------------------------------------------------------------- /client/src/pages/Modal.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import { createPortal } from 'react-dom'; 3 | import styled from 'styled-components'; 4 | 5 | const Overlay = styled.div` 6 | position: fixed; 7 | top: 0px; 8 | left: 0px; 9 | width: 100%; 10 | height: 100%; 11 | min-height: 100vh; 12 | background: #c8c8c8; 13 | opacity: 0.5; 14 | z-index: 99; 15 | overflow: hidden; 16 | `; 17 | const Wrapper = styled.div` 18 | position: fixed; 19 | top: 50%; 20 | left: 50%; 21 | transform: translate(-50%, -50%); 22 | z-index: 100; 23 | `; 24 | 25 | export interface ModalProps { 26 | overlayClose?: boolean; 27 | onClose(): void; 28 | } 29 | 30 | export const Portal: React.FC = ({ children }) => { 31 | // 모달 창이 나올 경우, 스크롤을 움직이지 못하도록 합니다. 32 | useEffect(() => { 33 | document.body.style.cssText = `position: fixed; top: -${window.scrollY}px`; 34 | return () => { 35 | const scrollY = document.body.style.top; 36 | document.body.style.cssText = `position: ""; top: "";`; 37 | window.scrollTo(0, parseInt(scrollY || '0') * -1); 38 | }; 39 | }, []); 40 | // id가 modal인 DOM 노드에 모달 창을 render합니다. 41 | const rootElement = document.getElementById('modal') as Element; 42 | return createPortal(children, rootElement); 43 | }; 44 | 45 | const Modal: React.FC = ({ 46 | overlayClose = true, 47 | onClose, 48 | children 49 | }) => { 50 | // 바깥 영역을 클릭 시, 모달 창을 닫을 지 여부 51 | const onOverlayClick = () => { 52 | if (overlayClose) { 53 | onClose(); 54 | } 55 | }; 56 | return ( 57 | 58 | 59 | {children} 60 | 61 | ); 62 | }; 63 | 64 | export default Modal; 65 | -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "webpack --mode production", 8 | "start": "webpack-dev-server --open" 9 | }, 10 | "author": "", 11 | "license": "ISC", 12 | "devDependencies": { 13 | "@babel/core": "^7.10.2", 14 | "@babel/preset-env": "^7.10.2", 15 | "@babel/preset-react": "^7.10.1", 16 | "@types/react": "^16.9.35", 17 | "@types/react-dom": "^16.9.8", 18 | "@types/react-router-dom": "^5.1.5", 19 | "@types/styled-components": "^5.1.0", 20 | "babel-loader": "^8.1.0", 21 | "babel-plugin-styled-components": "^1.10.7", 22 | "clean-webpack-plugin": "^3.0.0", 23 | "html-webpack-plugin": "^4.3.0", 24 | "nodemon": "^2.0.4", 25 | "react": "^16.13.1", 26 | "react-dom": "^16.13.1", 27 | "react-router-dom": "^5.2.0", 28 | "styled-components": "^5.1.1", 29 | "ts-loader": "^7.0.5", 30 | "tsconfig-paths-webpack-plugin": "^3.2.0", 31 | "typescript": "^3.9.3", 32 | "webpack": "^4.43.0", 33 | "webpack-cli": "^3.3.11", 34 | "webpack-dev-server": "^3.11.0" 35 | }, 36 | "dependencies": { 37 | "@types/axios": "^0.14.0", 38 | "@types/jwt-decode": "^2.2.1", 39 | "@types/react-redux": "^7.1.9", 40 | "@types/redux": "^3.6.0", 41 | "@types/redux-logger": "^3.0.8", 42 | "@types/redux-saga": "^0.10.5", 43 | "@types/socket.io-client": "^1.4.33", 44 | "axios": "^0.19.2", 45 | "jwt-decode": "^2.2.0", 46 | "react-redux": "^7.2.1", 47 | "redux": "^4.0.5", 48 | "redux-devtools-extension": "^2.13.8", 49 | "redux-logger": "^3.0.6", 50 | "redux-saga": "^1.1.3", 51 | "socket.io-client": "^2.3.0" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /client/src/store/sagas/profile.ts: -------------------------------------------------------------------------------- 1 | import { all, call, put, takeLatest } from 'redux-saga/effects'; 2 | import { 3 | ProfileTypes, 4 | ChangeProfileAction, 5 | ChangeFriendNameAction 6 | } from '~/store/actions/profile'; 7 | import { changeProfile } from '~/apis/user'; 8 | import { UserTypes } from '~/store/actions/user'; 9 | import { changeFriendNameRequest } from '~/apis/friend'; 10 | export default function* profileSaga() { 11 | yield all([ 12 | takeLatest(ProfileTypes.CHANGE_PROFILE_REQUEST, changeProfile$), 13 | takeLatest(ProfileTypes.CHANGE_FRIEND_NAME_REQUEST, changeFriendName$) 14 | ]); 15 | } 16 | 17 | function* changeProfile$(action: ChangeProfileAction) { 18 | const profileData = action.payload; 19 | try { 20 | yield call(changeProfile, profileData); 21 | yield put({ 22 | type: ProfileTypes.CHANGE_PROFILE_SUCCESS, 23 | payload: profileData 24 | }); 25 | yield put({ 26 | type: UserTypes.CHANGE_PROFILE, 27 | payload: profileData 28 | }); 29 | } catch (err) { 30 | yield put({ 31 | type: ProfileTypes.CHANGE_PROFILE_FAILUER, 32 | payload: '프로필 변경 실패' 33 | }); 34 | } 35 | } 36 | 37 | function* changeFriendName$(action: ChangeFriendNameAction) { 38 | const { friend_name } = action.payload; 39 | try { 40 | yield call(changeFriendNameRequest, action.payload); 41 | yield put({ 42 | type: ProfileTypes.CHANGE_FRIEND_NAME_SUCCESS, 43 | payload: friend_name 44 | }); 45 | yield put({ 46 | type: UserTypes.CHANGE_FRIEND_NAME, 47 | payload: action.payload 48 | }); 49 | } catch (err) { 50 | yield put({ 51 | type: ProfileTypes.CHANGE_FRIEND_NAME_FAILUER, 52 | payload: '친구 이름 변경 실패' 53 | }); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /server/src/types/chat.ts: -------------------------------------------------------------------------------- 1 | export type RoomType = 'individual' | 'group'; 2 | 3 | export interface UserResponseDto { 4 | id: number; 5 | user_id: string; 6 | name: string; 7 | status_msg: string; 8 | profile_img_url: string; 9 | background_img_url: string; 10 | } 11 | 12 | export interface CreateRoomRequest { 13 | my_id: number; 14 | type: RoomType; 15 | identifier: string; 16 | room_name: string; 17 | participant: Array; 18 | } 19 | export interface CreateRoomResponse { 20 | room_id: number; 21 | identifier: string; 22 | type: RoomType; 23 | room_name: string; 24 | last_chat: string; 25 | not_read_chat: number; 26 | last_read_chat_id: number; 27 | updatedAt: Date; 28 | } 29 | 30 | export interface MessageRequest { 31 | room_id: number; 32 | type: RoomType; 33 | participant: Array; 34 | send_user_id: number; 35 | message: string; 36 | not_read: number; 37 | } 38 | export interface MessageResponse { 39 | id: number; 40 | room_id: number; 41 | send_user_id: number; 42 | message: string; 43 | not_read: number; 44 | createdAt: Date; 45 | } 46 | 47 | export interface RoomListResponse { 48 | room_id: number; 49 | type: RoomType; 50 | identifier: string; 51 | room_name: string; 52 | participant: Array; 53 | last_chat: string; 54 | not_read_chat: number; 55 | last_read_chat_id: number; 56 | updatedAt: Date; 57 | } 58 | 59 | export interface ReadChatRequest { 60 | user_id: number; 61 | room_id: number; 62 | type: RoomType; 63 | participant: Array; 64 | last_read_chat_id_range: Array; 65 | } 66 | 67 | export interface ReadChatResponse { 68 | room_id: number; 69 | last_read_chat_id_range: Array; 70 | } 71 | -------------------------------------------------------------------------------- /client/src/containers/login/LoginContainer.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import styled from 'styled-components'; 3 | import { Dispatch, bindActionCreators } from 'redux'; 4 | import { connect } from 'react-redux'; 5 | import { Redirect } from 'react-router-dom'; 6 | import { Header, Content, Footer } from '~/components/login'; 7 | import { AuthActions } from '~/store/actions/auth'; 8 | import { RootState } from '~/store/reducers'; 9 | import { AuthState } from '~/store/reducers/auth'; 10 | import { PAGE_PATHS } from '~/constants'; 11 | 12 | const Wrapper = styled.div` 13 | width: 360px; 14 | height: 600px; 15 | background-color: #ffeb33; 16 | `; 17 | interface Props { 18 | authActions: typeof AuthActions; 19 | authState: AuthState; 20 | } 21 | 22 | class LoginContainer extends Component { 23 | // 로그인 실패 메시지 등을 제거 24 | componentWillUnmount() { 25 | this.props.authActions.changeMessage(''); 26 | } 27 | 28 | render() { 29 | const { login, changeMessage } = this.props.authActions; 30 | const { token, loginFailuerMsg, loggingIn } = this.props.authState; 31 | 32 | const contentProps = { 33 | login, 34 | changeMessage, 35 | loginFailuerMsg, 36 | loggingIn 37 | }; 38 | if (token) return ; 39 | return ( 40 | 41 |
42 | 43 |