├── .gitignore ├── .idea ├── .gitignore ├── markdown-navigator-enh.xml ├── markdown-navigator.xml ├── misc.xml ├── modules.xml ├── rSettings.xml ├── ts-nodebird.iml └── vcs.xml ├── axios ├── index.html ├── index.ts ├── node.ts ├── package-lock.json ├── package.json ├── tsconfig.json └── webpack.config.js ├── axiosTs ├── axios │ ├── index.ts │ └── module.ts ├── index.html ├── index.ts ├── node.ts ├── package-lock.json ├── package.json ├── tsconfig.json └── webpack.config.js ├── ch1 └── back │ ├── index.ts │ ├── package-lock.json │ ├── package.json │ └── tsconfig.json ├── ch2 └── back │ ├── config │ ├── config.js │ └── config.ts │ ├── index.ts │ ├── models │ ├── comment.ts │ ├── hashtag.ts │ ├── image.ts │ ├── index.ts │ ├── post.ts │ ├── sequelize.ts │ └── user.ts │ ├── nodemon.json │ ├── package-lock.json │ ├── package.json │ ├── passport │ ├── index.ts │ └── local.ts │ ├── routes │ ├── hashtag.ts │ ├── middleware.ts │ ├── post.ts │ ├── posts.ts │ └── user.ts │ ├── tsconfig.json │ └── types │ ├── express.d.ts │ ├── express │ └── index.d.ts │ └── index.d.ts ├── ch3 └── back │ ├── config │ ├── config.js │ └── config.ts │ ├── index.ts │ ├── models │ ├── comment.ts │ ├── hashtag.ts │ ├── image.ts │ ├── index.ts │ ├── post.ts │ ├── sequelize.ts │ └── user.ts │ ├── nodemon.json │ ├── package-lock.json │ ├── package.json │ ├── passport │ ├── index.ts │ └── local.ts │ ├── routes │ ├── hashtag.ts │ ├── middleware.ts │ ├── post.ts │ ├── posts.ts │ └── user.ts │ ├── tsconfig.json │ └── types │ ├── express.d.ts │ ├── express │ └── index.d.ts │ └── index.d.ts ├── js ├── back │ ├── config │ │ └── config.js │ ├── index.js │ ├── models │ │ ├── comment.js │ │ ├── hashtag.js │ │ ├── image.js │ │ ├── index.js │ │ ├── post.js │ │ └── user.js │ ├── nodemon.json │ ├── package-lock.json │ ├── package.json │ ├── passport │ │ ├── index.js │ │ └── local.js │ └── routes │ │ ├── hashtag.js │ │ ├── middleware.js │ │ ├── post.js │ │ ├── posts.js │ │ └── user.js ├── front │ ├── .babelrc │ ├── .eslintrc │ ├── bundles │ │ └── client.html │ ├── components │ │ ├── AppLayout.js │ │ ├── FollowButton.js │ │ ├── FollowList.js │ │ ├── ImagesZoom │ │ │ ├── index.js │ │ │ └── style.js │ │ ├── PostCardContent.js │ │ └── PostImages.js │ ├── config │ │ └── config.js │ ├── containers │ │ ├── CommentForm.js │ │ ├── LoginForm.js │ │ ├── NicknameEditForm.js │ │ ├── PostCard.js │ │ ├── PostForm.js │ │ └── UserProfile.js │ ├── next.config.js │ ├── nodemon.json │ ├── package-lock.json │ ├── package.json │ ├── pages │ │ ├── _app.js │ │ ├── _document.js │ │ ├── _error.js │ │ ├── hashtag.js │ │ ├── hashtag │ │ │ └── [tag].js │ │ ├── index.js │ │ ├── post.js │ │ ├── post │ │ │ └── [id].js │ │ ├── profile.js │ │ ├── signup.js │ │ ├── user.js │ │ └── user │ │ │ └── [id].js │ ├── public │ │ └── favicon.ico │ ├── reducers │ │ ├── index.js │ │ ├── post.js │ │ └── user.js │ ├── sagas │ │ ├── index.js │ │ ├── post.js │ │ └── user.js │ └── store │ │ └── configureStore.js └── lambda │ ├── index.js │ ├── package-lock.json │ └── package.json ├── lecture └── back │ ├── config │ └── config.ts │ ├── index.ts │ ├── models │ ├── comment.ts │ ├── hashtag.ts │ ├── image.ts │ ├── index.ts │ ├── post.ts │ ├── sequelize.ts │ └── user.ts │ ├── nodemon.json │ ├── package-lock.json │ ├── package.json │ ├── passport │ ├── index.ts │ └── local.ts │ ├── routes │ ├── hashtag.ts │ └── posts.ts │ ├── tsconfig.json │ └── types │ ├── express │ └── index.d.ts │ ├── index.d.ts │ └── passport-local.d.ts ├── redux ├── index.ts ├── package-lock.json ├── package.json └── tsconfig.json ├── test ├── index.js ├── tsconfig.json └── webpack.config.js └── ts ├── back ├── .eslintrc ├── config │ └── config.ts ├── index.ts ├── models │ ├── comment.ts │ ├── hashtag.ts │ ├── image.ts │ ├── index.ts │ ├── post.ts │ ├── sequelize.ts │ └── user.ts ├── nodemon.json ├── package-lock.json ├── package.json ├── passport │ ├── index.ts │ └── local.ts ├── routes │ ├── hashtag.ts │ ├── middleware.ts │ ├── post.ts │ ├── posts.ts │ └── user.ts ├── tsconfig.json └── types │ ├── express │ └── index.d.ts │ ├── index.d.ts │ └── passport-local.d.ts └── front ├── .babelrc ├── .eslintrc ├── bundles └── client.html ├── components ├── AppLayout.tsx ├── FollowButton.tsx ├── FollowList.tsx ├── ImagesZoom │ ├── index.tsx │ └── style.ts ├── PostCardContent.tsx └── PostImages.tsx ├── config └── config.ts ├── containers ├── CommentForm.tsx ├── LoginForm.tsx ├── NicknameEditForm.tsx ├── PostCard.tsx ├── PostForm.tsx └── UserProfile.tsx ├── next-env.d.ts ├── next.config.js ├── nodemon.json ├── package-lock.json ├── package.json ├── pages ├── _app.tsx ├── _document.tsx ├── _error.tsx ├── hashtag │ └── [tag].tsx ├── index.tsx ├── post │ └── [id].tsx ├── profile.tsx ├── signup.tsx └── user │ └── [id].tsx ├── public └── favicon.ico ├── reducers ├── index.ts ├── post.ts └── user.ts ├── sagas ├── index.ts ├── post.ts └── user.ts ├── tsconfig.json └── utils └── useInput.ts /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | .idea/**/workspace.xml 3 | .idea/**/tasks.xml 4 | .idea/**/usage.statistics.xml 5 | .idea/**/dictionaries 6 | .idea/**/shelf 7 | .idea/**/contentModel.xml 8 | .idea/**/dataSources/ 9 | .idea/**/dataSources.ids 10 | .idea/**/dataSources.local.xml 11 | .idea/**/sqlDataSources.xml 12 | .idea/**/dynamic.xml 13 | .idea/**/uiDesigner.xml 14 | .idea/**/dbnavigator.xml 15 | .idea/**/gradle.xml 16 | .idea/**/libraries 17 | cmake-build-*/ 18 | .idea/**/mongoSettings.xml 19 | *.iws 20 | out/ 21 | .idea_modules/ 22 | atlassian-ide-plugin.xml 23 | .idea/replstate.xml 24 | com_crashlytics_export_strings.xml 25 | crashlytics.properties 26 | crashlytics-build.properties 27 | fabric.properties 28 | .idea/httpRequests 29 | .idea/caches/build_file_checksums.ser 30 | logs 31 | *.log 32 | npm-debug.log* 33 | yarn-debug.log* 34 | yarn-error.log* 35 | lerna-debug.log* 36 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 37 | pids 38 | *.pid 39 | *.seed 40 | *.pid.lock 41 | lib-cov 42 | coverage 43 | *.lcov 44 | .nyc_output 45 | .grunt 46 | bower_components 47 | .lock-wscript 48 | build/Release 49 | node_modules/ 50 | jspm_packages/ 51 | typings/ 52 | *.tsbuildinfo 53 | .npm 54 | .eslintcache 55 | .rpt2_cache/ 56 | .rts2_cache_cjs/ 57 | .rts2_cache_es/ 58 | .rts2_cache_umd/ 59 | .node_repl_history 60 | *.tgz 61 | .yarn-integrity 62 | .env 63 | .env.test 64 | .cache 65 | .next 66 | .nuxt 67 | dist 68 | .cache/ 69 | .vuepress/dist 70 | .serverless/ 71 | .fusebox/ 72 | .dynamodb/ 73 | .tern-port 74 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /workspace.xml -------------------------------------------------------------------------------- /.idea/markdown-navigator-enh.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/rSettings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.idea/ts-nodebird.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /axios/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 18 | 25 | -------------------------------------------------------------------------------- /axios/node.ts: -------------------------------------------------------------------------------- 1 | import axios from './index'; 2 | const axios2 = require('./index'); 3 | 4 | console.log(axios); 5 | console.log(axios2); 6 | console.log(axios2.default); 7 | -------------------------------------------------------------------------------- /axios/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "axios", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "jest", 8 | "build": "webpack", 9 | "prepublish": "npm run build" 10 | }, 11 | "author": "ZeroCho", 12 | "license": "ISC", 13 | "dependencies": { 14 | "@babel/core": "^7.9.0", 15 | "@babel/plugin-transform-modules-commonjs": "^7.9.0", 16 | "@babel/preset-env": "^7.9.5", 17 | "@types/node": "^13.13.2", 18 | "awesome-typescript-loader": "^5.2.1", 19 | "core-js": "^3.6.5", 20 | "typescript": "^3.8.3", 21 | "webpack": "^4.42.1" 22 | }, 23 | "types": "./index.d.ts", 24 | "devDependencies": { 25 | "babel-plugin-add-module-exports": "^1.0.2", 26 | "webpack-cli": "^3.3.11" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /axios/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "declaration": true, 5 | "lib": ["dom", "es2020"], 6 | "target": "esnext", 7 | "module": "esnext" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /axios/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | mode: 'development', 5 | entry: { 6 | index: './index.ts', 7 | }, 8 | module: { 9 | rules: [{ 10 | test: /.[jt]s?$/, 11 | loader: 'awesome-typescript-loader', 12 | options: { 13 | useBabel: true, 14 | babelOptions: { 15 | babelrc: false, 16 | presets: ['@babel/preset-env'], 17 | plugins: [ 18 | 'add-module-exports', 19 | ] 20 | }, 21 | babelCore: '@babel/core', 22 | } 23 | }], 24 | }, 25 | output: { 26 | path: path.join(__dirname), 27 | filename: '[name].js', 28 | library: 'axios', 29 | libraryTarget: "umd" 30 | }, 31 | resolve: { 32 | extensions: ['.ts', '.js'], 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /axiosTs/axios/index.ts: -------------------------------------------------------------------------------- 1 | export * as axios from './module'; 2 | export default axios; 3 | -------------------------------------------------------------------------------- /axiosTs/axios/module.ts: -------------------------------------------------------------------------------- 1 | import 'core-js/stable'; 2 | 3 | export async function get(url: string): Promise 4 | { 5 | let response: Response = await fetch(url, { method: "GET" }); 6 | return { 7 | status: response.status, 8 | data: await response.text() 9 | }; 10 | } 11 | 12 | interface Output 13 | { 14 | status: number; 15 | data: string; 16 | } 17 | -------------------------------------------------------------------------------- /axiosTs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 18 | 25 | -------------------------------------------------------------------------------- /axiosTs/index.ts: -------------------------------------------------------------------------------- 1 | import defaultAxios from "./axios/index"; 2 | 3 | export default defaultAxios; 4 | 5 | declare module globalThis { 6 | export var axios: any; 7 | } 8 | 9 | declare interface CustomGlobal 10 | { 11 | axios: typeof defaultAxios; 12 | } 13 | declare var window: CustomGlobal & Window; 14 | -------------------------------------------------------------------------------- /axiosTs/node.ts: -------------------------------------------------------------------------------- 1 | import axios from './axios'; 2 | const axios2 = require('./axios'); 3 | 4 | console.log(axios); 5 | console.log(axios2); 6 | console.log(axios2.default); 7 | -------------------------------------------------------------------------------- /axiosTs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "axios", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "webpack", 8 | "prepublish": "npm run build" 9 | }, 10 | "author": "ZeroCho", 11 | "license": "ISC", 12 | "dependencies": { 13 | "@babel/core": "^7.9.0", 14 | "@babel/preset-env": "^7.9.5", 15 | "@types/node": "^13.13.2", 16 | "awesome-typescript-loader": "^5.2.1", 17 | "babel-plugin-add-module-exports": "^1.0.2", 18 | "core-js": "^3.6.5", 19 | "typescript": "^3.8.3", 20 | "webpack": "^4.42.1", 21 | "webpack-cli": "^3.3.11" 22 | }, 23 | "types": "./index.d.ts" 24 | } 25 | -------------------------------------------------------------------------------- /axiosTs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "declaration": true, 5 | "lib": ["dom", "es2020"], 6 | "target": "es5", 7 | "module": "umd" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /axiosTs/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | mode: 'production', 5 | entry: { 6 | index: './axios/index.ts', 7 | }, 8 | module: { 9 | rules: [{ 10 | test: /.tsx?$/, 11 | loader: 'awesome-typescript-loader', 12 | }] 13 | }, 14 | output: { 15 | path: path.join(__dirname), 16 | filename: '[name].js', 17 | }, 18 | resolve: { 19 | extensions: ['.ts', '.js'], 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /ch1/back/index.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import morgan from 'morgan'; 3 | import cors from 'cors'; 4 | import cookieParser from 'cookie-parser'; 5 | import expressSession from 'express-session'; 6 | import dotenv from 'dotenv'; 7 | import passport from 'passport'; 8 | import hpp from 'hpp'; 9 | import helmet from 'helmet'; 10 | 11 | dotenv.config(); 12 | const app = express(); 13 | const prod: boolean = process.env.NODE_ENV === 'production'; 14 | 15 | app.set('port', prod ? process.env.PORT : 3065); 16 | 17 | if (prod) { 18 | app.use(hpp()); 19 | app.use(helmet()); 20 | app.use(morgan('combined')); 21 | app.use(cors({ 22 | origin: /nodebird\.com$/, 23 | credentials: true, 24 | })); 25 | } else { 26 | app.use(morgan('dev')); 27 | app.use(cors({ 28 | origin: true, 29 | credentials: true, 30 | })) 31 | } 32 | 33 | app.use('/', express.static('uploads')); 34 | app.use(express.json()); 35 | app.use(express.urlencoded({ extended: true })); 36 | app.use(cookieParser(process.env.COOKIE_SECRET)); 37 | app.use(expressSession({ 38 | resave: false, 39 | saveUninitialized: false, 40 | secret: process.env.COOKIE_SECRET!, 41 | cookie: { 42 | httpOnly: true, 43 | secure: false, // https -> true 44 | domain: prod ? '.nodebird.com' : undefined, 45 | }, 46 | name: 'rnbck', 47 | })); 48 | app.use(passport.initialize()); 49 | app.use(passport.session()); 50 | 51 | app.get('/', (req, res, next) => { 52 | res.send('react nodebird 백엔드 정상 동작!'); 53 | }); 54 | 55 | app.listen(app.get('port'), () => { 56 | console.log(`server is running on ${app.get('port')}`); 57 | }); 58 | -------------------------------------------------------------------------------- /ch1/back/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nodebird-server", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "dev": "nodemon", 8 | "start": "tsc && node index" 9 | }, 10 | "author": "ZeroCho", 11 | "license": "MIT", 12 | "dependencies": { 13 | "@types/bcrypt": "^3.0.0", 14 | "@types/bluebird": "^3.5.30", 15 | "@types/cookie-parser": "^1.4.2", 16 | "@types/cors": "^2.8.6", 17 | "@types/dotenv": "^8.2.0", 18 | "@types/express": "^4.17.3", 19 | "@types/express-session": "^1.17.0", 20 | "@types/helmet": "0.0.45", 21 | "@types/hpp": "^0.2.1", 22 | "@types/morgan": "^1.9.0", 23 | "@types/node": "^13.9.3", 24 | "@types/passport": "^1.0.3", 25 | "@types/passport-local": "^1.0.33", 26 | "@types/validator": "^12.0.1", 27 | "bcrypt": "^5.0.0", 28 | "cookie-parser": "^1.4.5", 29 | "cors": "^2.8.5", 30 | "dotenv": "^8.2.0", 31 | "express": "^4.18.2", 32 | "express-session": "^1.17.0", 33 | "helmet": "^3.21.3", 34 | "hpp": "^0.2.3", 35 | "morgan": "^1.10.0", 36 | "mysql2": "^2.1.0", 37 | "passport": "^0.4.1", 38 | "passport-local": "^1.0.0", 39 | "sequelize": "^6.29.0", 40 | "sequelize-cli": "^5.5.1", 41 | "typescript": "^3.8.3" 42 | }, 43 | "devDependencies": { 44 | "nodemon": "^2.0.2", 45 | "ts-node": "^8.8.1" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /ch1/back/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "lib": ["es2020"], 5 | "esModuleInterop": true, 6 | "moduleResolution": "node", 7 | "typeRoots": ["./types"] 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /ch2/back/config/config.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | exports.__esModule = true; 3 | var dotenv = require("dotenv"); 4 | dotenv.config(); 5 | var config = { 6 | "development": { 7 | "username": "root", 8 | "password": process.env.DB_PASSWORD, 9 | "database": "react-nodebird", 10 | "host": "127.0.0.1", 11 | "dialect": "mysql" 12 | }, 13 | "test": { 14 | "username": "root", 15 | "password": process.env.DB_PASSWORD, 16 | "database": "react-nodebird", 17 | "host": "127.0.0.1", 18 | "dialect": "mysql" 19 | }, 20 | "production": { 21 | "username": "root", 22 | "password": process.env.DB_PASSWORD, 23 | "database": "react-nodebird", 24 | "host": "127.0.0.1", 25 | "dialect": "mysql" 26 | } 27 | }; 28 | exports["default"] = config; 29 | -------------------------------------------------------------------------------- /ch2/back/config/config.ts: -------------------------------------------------------------------------------- 1 | import * as dotenv from 'dotenv'; 2 | dotenv.config(); 3 | 4 | type Config = { 5 | username: string, 6 | password: string, 7 | database: string, 8 | host: string, 9 | [key: string]: string, 10 | } 11 | interface IConfigGroup { 12 | development: Config; 13 | test: Config; 14 | production: Config; 15 | } 16 | const config: IConfigGroup = { 17 | "development": { 18 | "username": "root", 19 | "password": process.env.DB_PASSWORD!, 20 | "database": "react-nodebird", 21 | "host": "127.0.0.1", 22 | "dialect": "mysql" 23 | }, 24 | "test": { 25 | "username": "root", 26 | "password": process.env.DB_PASSWORD!, 27 | "database": "react-nodebird", 28 | "host": "127.0.0.1", 29 | "dialect": "mysql" 30 | }, 31 | "production": { 32 | "username": "root", 33 | "password": process.env.DB_PASSWORD!, 34 | "database": "react-nodebird", 35 | "host": "127.0.0.1", 36 | "dialect": "mysql" 37 | } 38 | }; 39 | 40 | export default config; 41 | -------------------------------------------------------------------------------- /ch2/back/index.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import morgan from 'morgan'; 3 | import cors from 'cors'; 4 | import cookieParser from 'cookie-parser'; 5 | import expressSession from 'express-session'; 6 | import dotenv from 'dotenv'; 7 | import passport from 'passport'; 8 | import hpp from 'hpp'; 9 | import helmet from 'helmet'; 10 | 11 | import { sequelize } from './models'; 12 | import userRouter from './routes/user'; 13 | import postRouter from './routes/post' 14 | import postsRouter from './routes/posts' 15 | import hashtagRouter from './routes/hashtag' 16 | 17 | dotenv.config(); 18 | const app = express(); 19 | const prod: boolean = process.env.NODE_ENV === 'production'; 20 | 21 | app.set('port', prod ? process.env.PORT : 3065); 22 | sequelize.sync({ force: false }) 23 | .then(() => { 24 | console.log('데이터베이스 연결 성공'); 25 | }) 26 | .catch((err: Error) => { 27 | console.error(err); 28 | }); 29 | 30 | if (prod) { 31 | app.use(hpp()); 32 | app.use(helmet()); 33 | app.use(morgan('combined')); 34 | app.use(cors({ 35 | origin: /nodebird\.com$/, 36 | credentials: true, 37 | })); 38 | } else { 39 | app.use(morgan('dev')); 40 | app.use(cors({ 41 | origin: true, 42 | credentials: true, 43 | })) 44 | } 45 | 46 | app.use('/', express.static('uploads')); 47 | app.use(express.json()); 48 | app.use(express.urlencoded({ extended: true })); 49 | app.use(cookieParser(process.env.COOKIE_SECRET)); 50 | app.use(expressSession({ 51 | resave: false, 52 | saveUninitialized: false, 53 | secret: process.env.COOKIE_SECRET!, 54 | cookie: { 55 | httpOnly: true, 56 | secure: false, // https -> true 57 | domain: prod ? '.nodebird.com' : undefined, 58 | }, 59 | name: 'rnbck', 60 | })); 61 | app.use(passport.initialize()); 62 | app.use(passport.session()); 63 | app.use('/user', userRouter); 64 | app.use('/post', postRouter); 65 | app.use('/posts', postsRouter); 66 | app.use('/hashtag', hashtagRouter); 67 | 68 | app.get('/', (req, res, next) => { 69 | res.send('react nodebird 백엔드 정상 동작!'); 70 | }); 71 | 72 | app.listen(app.get('port'), () => { 73 | console.log(`server is running on ${app.get('port')}`); 74 | }); 75 | -------------------------------------------------------------------------------- /ch2/back/models/comment.ts: -------------------------------------------------------------------------------- 1 | import { DataTypes, Model } from 'sequelize'; 2 | import { dbType } from './index'; 3 | import { sequelize } from './sequelize'; 4 | 5 | class Comment extends Model { 6 | public readonly id!: number; 7 | public content!: string; 8 | public readonly createdAt!: Date; 9 | public readonly updatedAt!: Date; 10 | } 11 | 12 | Comment.init({ 13 | content: { 14 | type: DataTypes.TEXT, 15 | allowNull: false, 16 | } 17 | }, { 18 | sequelize, 19 | modelName: 'Comment', 20 | tableName: 'comment', 21 | charset: 'utf8mb4', 22 | collate: 'utf8mb4_general_ci', 23 | }); 24 | 25 | export const associate =(db: dbType) => { 26 | 27 | }; 28 | 29 | export default Comment; 30 | -------------------------------------------------------------------------------- /ch2/back/models/hashtag.ts: -------------------------------------------------------------------------------- 1 | import { DataTypes, Model } from 'sequelize'; 2 | import { dbType } from './index'; 3 | import { sequelize } from './sequelize'; 4 | 5 | class Hashtag extends Model { 6 | public readonly id!: number; 7 | public name!: string; 8 | public readonly createdAt!: Date; 9 | public readonly updatedAt!: Date; 10 | } 11 | 12 | Hashtag.init({ 13 | name: { 14 | type: DataTypes.STRING(20), 15 | allowNull: false, 16 | }, 17 | }, { 18 | sequelize, 19 | modelName: 'Hashtag', 20 | tableName: 'hashtag', 21 | charset: 'utf8mb4', 22 | collate: 'utf8mb4_general_ci', 23 | }); 24 | 25 | export const associate = (db: dbType) => {}; 26 | 27 | export default Hashtag; 28 | -------------------------------------------------------------------------------- /ch2/back/models/image.ts: -------------------------------------------------------------------------------- 1 | import { DataTypes, Model } from 'sequelize'; 2 | import { dbType } from './index'; 3 | import { sequelize } from './sequelize'; 4 | 5 | class Image extends Model { 6 | public readonly id!: number; 7 | public src!: number; 8 | public readonly createdAt!: Date; 9 | public readonly updatedAt!: Date; 10 | } 11 | 12 | Image.init({ 13 | src: { 14 | type: DataTypes.STRING(200), 15 | allowNull: false, 16 | }, 17 | }, { 18 | sequelize, 19 | modelName: 'Image', 20 | tableName: 'image', 21 | charset: 'utf8', 22 | collate: 'utf8_general_ci', 23 | }); 24 | 25 | export const associate = (db: dbType) => { 26 | 27 | }; 28 | 29 | export default Image; 30 | -------------------------------------------------------------------------------- /ch2/back/models/index.ts: -------------------------------------------------------------------------------- 1 | import User, { associate as associateUser } from './user'; 2 | import Comment, { associate as associateComment } from './comment'; 3 | import Hashtag, { associate as associateHashtag } from './hashtag'; 4 | import Image, { associate as associateImage } from './image'; 5 | import Post, { associate as associatePost } from './post'; 6 | 7 | export * from './sequelize'; 8 | const db = { 9 | User, 10 | Comment, 11 | Hashtag, 12 | Image, 13 | Post, 14 | }; 15 | export type dbType = typeof db; 16 | 17 | associateUser(db); 18 | associateComment(db); 19 | associateHashtag(db); 20 | associateImage(db); 21 | associatePost(db); 22 | -------------------------------------------------------------------------------- /ch2/back/models/post.ts: -------------------------------------------------------------------------------- 1 | import { 2 | DataTypes, Model, BelongsToManyAddAssociationsMixin, HasManyAddAssociationsMixin, 3 | HasManyAddAssociationMixin, BelongsToManyAddAssociationMixin, BelongsToManyRemoveAssociationMixin, 4 | } from 'sequelize'; 5 | import { dbType } from './index'; 6 | import { sequelize } from './sequelize'; 7 | import Hashtag from './hashtag'; 8 | import Image from './image'; 9 | import User from './user'; 10 | 11 | class Post extends Model { 12 | public readonly id!: number; 13 | public content!: string; 14 | public readonly createdAt!: Date; 15 | public readonly updatedAt!: Date; 16 | 17 | public UserId!: number; 18 | public readonly Retweet?: Post; 19 | public RetweetId?: number; 20 | 21 | public addHashtags!: BelongsToManyAddAssociationsMixin 22 | public addImages!: HasManyAddAssociationsMixin 23 | public addImage!: HasManyAddAssociationMixin 24 | public addLiker!: BelongsToManyAddAssociationMixin 25 | public removeLiker!: BelongsToManyRemoveAssociationMixin 26 | } 27 | 28 | Post.init({ 29 | content: { 30 | type: DataTypes.TEXT, 31 | allowNull: false, 32 | }, 33 | }, { 34 | sequelize, 35 | modelName: 'Post', 36 | tableName: 'post', 37 | charset: 'utf8mb4', 38 | collate: 'utf8mb4_general_ci', 39 | }); 40 | 41 | export const associate = (db: dbType) => { 42 | db.Post.belongsTo(db.User); 43 | db.Post.hasMany(db.Comment); 44 | db.Post.hasMany(db.Image); 45 | db.Post.belongsTo(db.Post, { as: 'Retweet' }); 46 | db.Post.belongsToMany(db.Hashtag, { through: 'PostHashtag' }); 47 | db.Post.belongsToMany(db.User, { through: 'Like', as: 'Likers' }); 48 | }; 49 | 50 | export default Post; 51 | -------------------------------------------------------------------------------- /ch2/back/models/sequelize.ts: -------------------------------------------------------------------------------- 1 | import { Sequelize } from 'sequelize'; 2 | import config from '../config/config'; 3 | 4 | const env = process.env.NODE_ENV as ('production' | 'test' | 'development') || 'development'; 5 | const { database, username, password } = config[env]; 6 | const sequelize = new Sequelize(database, username, password, config[env]); 7 | 8 | export { sequelize }; 9 | export default sequelize; 10 | -------------------------------------------------------------------------------- /ch2/back/models/user.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Model, DataTypes, BelongsToManyGetAssociationsMixin, 3 | HasManyGetAssociationsMixin, BelongsToManyRemoveAssociationMixin, 4 | BelongsToManyAddAssociationMixin, 5 | } from 'sequelize'; 6 | import { dbType } from './index'; 7 | import Post from './post'; 8 | import { sequelize } from './sequelize'; 9 | 10 | class User extends Model { 11 | public readonly id!: number; 12 | public nickname!: string; 13 | public userId!: string; 14 | public password!: string; 15 | public readonly createdAt!: Date; 16 | public readonly updatedAt!: Date; 17 | 18 | public readonly Posts?: Post[]; 19 | public readonly Followers?: User[]; 20 | public readonly Followings?: User[]; 21 | 22 | public addFollowing!: BelongsToManyAddAssociationMixin; 23 | public getFollowings!: BelongsToManyGetAssociationsMixin; 24 | public removeFollowing!: BelongsToManyRemoveAssociationMixin; 25 | public getFollowers!: BelongsToManyGetAssociationsMixin; 26 | public removeFollower!: BelongsToManyRemoveAssociationMixin; 27 | public getPosts!: HasManyGetAssociationsMixin; 28 | } 29 | 30 | User.init({ 31 | nickname: { 32 | type: DataTypes.STRING(20), 33 | }, 34 | userId: { 35 | type: DataTypes.STRING(20), 36 | allowNull: false, 37 | unique: true, 38 | }, 39 | password: { 40 | type: DataTypes.STRING(100), 41 | allowNull: false, 42 | } 43 | }, { 44 | sequelize, 45 | modelName: 'User', 46 | tableName: 'user', 47 | charset: 'utf8', 48 | collate: 'utf8_general_ci', 49 | }); 50 | 51 | export const associate = (db: dbType) => { 52 | db.User.hasMany(db.Post, { as: 'Posts' }); 53 | db.User.hasMany(db.Comment); 54 | db.User.belongsToMany(db.Post, { through: 'Like', as: 'Liked' }); 55 | db.User.belongsToMany(db.User, { through: 'Follow', as: 'Followers', foreignKey: 'followingId' }); 56 | db.User.belongsToMany(db.User, { through: 'Follow', as: 'Followings', foreignKey: 'followerId' }); 57 | }; 58 | 59 | export default User; 60 | -------------------------------------------------------------------------------- /ch2/back/nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "exec": "ts-node index.ts", 3 | "ext": "js json ts" 4 | } -------------------------------------------------------------------------------- /ch2/back/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nodebird-server", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "dev": "nodemon", 8 | "start": "tsc && node index" 9 | }, 10 | "author": "ZeroCho", 11 | "license": "MIT", 12 | "dependencies": { 13 | "@types/bcrypt": "^3.0.0", 14 | "@types/bluebird": "^3.5.33", 15 | "@types/cookie-parser": "^1.4.2", 16 | "@types/cors": "^2.8.8", 17 | "@types/dotenv": "^8.2.0", 18 | "@types/express": "^4.17.8", 19 | "@types/express-session": "^1.17.0", 20 | "@types/helmet": "0.0.45", 21 | "@types/hpp": "^0.2.1", 22 | "@types/morgan": "^1.9.2", 23 | "@types/multer": "^1.4.4", 24 | "@types/multer-s3": "^2.7.8", 25 | "@types/node": "^13.13.30", 26 | "@types/passport": "^1.0.4", 27 | "@types/passport-local": "^1.0.33", 28 | "@types/validator": "^12.0.1", 29 | "aws-sdk": "^2.786.0", 30 | "bcrypt": "^5.0.0", 31 | "cookie-parser": "^1.4.5", 32 | "cors": "^2.8.5", 33 | "dotenv": "^8.2.0", 34 | "express": "^4.18.2", 35 | "express-session": "^1.17.1", 36 | "helmet": "^3.23.3", 37 | "hpp": "^0.2.3", 38 | "morgan": "^1.10.0", 39 | "multer": "^1.4.2", 40 | "multer-s3": "^2.9.0", 41 | "mysql2": "^2.2.5", 42 | "passport": "^0.4.1", 43 | "passport-local": "^1.0.0", 44 | "sequelize": "^6.29.0", 45 | "sequelize-cli": "^5.5.1", 46 | "typescript": "^4.0.5" 47 | }, 48 | "devDependencies": { 49 | "nodemon": "^2.0.6", 50 | "ts-node": "^8.10.2" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /ch2/back/passport/index.ts: -------------------------------------------------------------------------------- 1 | import passport from 'passport'; 2 | 3 | import User from '../models/user'; 4 | import local from './local'; 5 | 6 | export default () => { 7 | passport.serializeUser((user, done) => { 8 | done(null, user.id); 9 | }); 10 | 11 | passport.deserializeUser(async (id, done) => { 12 | try { 13 | const user = await User.findOne({ 14 | where: { id }, 15 | }); 16 | if (!user) { 17 | return done(new Error('no user')); 18 | } 19 | return done(null, user); // req.user 20 | } catch (err) { 21 | console.error(err); 22 | return done(err); 23 | } 24 | }); 25 | 26 | local(); 27 | } 28 | -------------------------------------------------------------------------------- /ch2/back/passport/local.ts: -------------------------------------------------------------------------------- 1 | import passport from 'passport'; 2 | import bcrypt from 'bcrypt'; 3 | import { Strategy } from 'passport-local'; 4 | 5 | import User from '../models/user'; 6 | 7 | export default () => { 8 | passport.use('local', new Strategy({ 9 | usernameField: 'userId', 10 | passwordField: 'password', 11 | }, async (userId, password, done) => { 12 | try { 13 | const user = await User.findOne({ where: { userId } }); 14 | if (!user) { 15 | return done(null, false, { message: '존재하지 않는 사용자입니다!' }); 16 | } 17 | const result = await bcrypt.compare(password, user.password); 18 | if (result) { 19 | return done(null, user); 20 | } 21 | return done(null, false, { message: '비밀번호가 틀립니다.' }); 22 | } catch (err) { 23 | console.error(err); 24 | return done(err); 25 | } 26 | })) 27 | }; 28 | -------------------------------------------------------------------------------- /ch2/back/routes/hashtag.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import Sequelize from 'sequelize'; 3 | 4 | import Hashtag from '../models/hashtag'; 5 | import Image from '../models/image'; 6 | import Post from '../models/post'; 7 | import User from '../models/user'; 8 | 9 | const router = express.Router(); 10 | 11 | router.get('/:tag', async (req, res, next) => { 12 | try { 13 | let where = {}; 14 | if (parseInt(req.query.lastId, 10)) { 15 | where = { 16 | id: { 17 | [Sequelize.Op.lt]: parseInt(req.query.lastId, 10), 18 | }, 19 | }; 20 | } 21 | const posts = await Post.findAll({ 22 | where, 23 | include: [{ 24 | model: Hashtag, 25 | where: { name: decodeURIComponent(req.params.tag) }, 26 | }, { 27 | model: User, 28 | attributes: ['id', 'nickname'], 29 | }, { 30 | model: Image, 31 | }, { 32 | model: User, 33 | as: 'Likers', 34 | attributes: ['id'], 35 | }, { 36 | model: Post, 37 | as: 'Retweet', 38 | include: [{ 39 | model: User, 40 | attributes: ['id', 'nickname'], 41 | }, { 42 | model: Image, 43 | }] 44 | }], 45 | order: [['createdAt', 'DESC']], 46 | limit: parseInt(req.query.limit, 10), 47 | }); 48 | res.json(posts); 49 | } catch (err) { 50 | console.error(err); 51 | return next(err); 52 | } 53 | }); 54 | 55 | export default router; 56 | -------------------------------------------------------------------------------- /ch2/back/routes/middleware.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from "express"; 2 | 3 | const isLoggedIn = (req: Request, res: Response, next: NextFunction) => { 4 | if (req.isAuthenticated()) { 5 | next(); 6 | } else { 7 | res.status(401).send('로그인이 필요합니다.'); 8 | } 9 | }; 10 | 11 | const isNotLoggedIn = (req: Request, res: Response, next: NextFunction) => { 12 | if (!req.isAuthenticated()) { 13 | next(); 14 | } else { 15 | res.status(401).send('로그인한 사용자는 접근할 수 없습니다.'); 16 | } 17 | }; 18 | 19 | export { isLoggedIn, isNotLoggedIn }; 20 | -------------------------------------------------------------------------------- /ch2/back/routes/posts.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import Sequelize from 'sequelize'; 3 | import Image from '../models/image'; 4 | import Post from '../models/post'; 5 | import User from '../models/user'; 6 | 7 | const router = express.Router(); 8 | 9 | router.get('/', async (req, res, next) => { 10 | try { 11 | let where = {}; 12 | if (parseInt(req.query.lastId, 10)) { 13 | where = { 14 | id: { 15 | [Sequelize.Op.lt]: parseInt(req.query.lastId, 10), // less than 16 | }, 17 | }; 18 | } 19 | const posts = await Post.findAll({ 20 | where, 21 | include: [{ 22 | model: User, 23 | attributes: ['id', 'nickname'], 24 | }, { 25 | model: Image, 26 | }, { 27 | model: User, 28 | as: 'Likers', 29 | attributes: ['id'], 30 | }, { 31 | model: Post, 32 | as: 'Retweet', 33 | include: [{ 34 | model: User, 35 | attributes: ['id', 'nickname'], 36 | }, { 37 | model: Image, 38 | }], 39 | }], 40 | order: [['createdAt', 'DESC']], // DESC는 내림차순, ASC는 오름차순 41 | limit: parseInt(req.query.limit, 10), 42 | }); 43 | return res.json(posts); 44 | } catch (err) { 45 | console.error(err); 46 | return next(err); 47 | } 48 | }); 49 | 50 | export default router; 51 | -------------------------------------------------------------------------------- /ch2/back/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "ts-node": { 3 | "files": true 4 | }, 5 | "compilerOptions": { 6 | "strict": true, 7 | "lib": ["es2020"], 8 | "esModuleInterop": true, 9 | "moduleResolution": "node", 10 | "typeRoots": ["./types"] 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /ch2/back/types/express.d.ts: -------------------------------------------------------------------------------- 1 | import User from '../models/user'; 2 | 3 | declare module "express-serve-static-core" { 4 | interface Request { 5 | user?: User; 6 | } 7 | } -------------------------------------------------------------------------------- /ch2/back/types/express/index.d.ts: -------------------------------------------------------------------------------- 1 | import User from '../../models/user'; 2 | 3 | declare module "express-serve-static-core" { 4 | interface Request { 5 | user?: User; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /ch2/back/types/index.d.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZeroCho/ts-nodebird/af29bc483ea6f0630e426f0dc82f4d1b789aa423/ch2/back/types/index.d.ts -------------------------------------------------------------------------------- /ch3/back/config/config.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | exports.__esModule = true; 3 | var dotenv = require("dotenv"); 4 | dotenv.config(); 5 | var config = { 6 | "development": { 7 | "username": "root", 8 | "password": process.env.DB_PASSWORD, 9 | "database": "react-nodebird", 10 | "host": "127.0.0.1", 11 | "dialect": "mysql" 12 | }, 13 | "test": { 14 | "username": "root", 15 | "password": process.env.DB_PASSWORD, 16 | "database": "react-nodebird", 17 | "host": "127.0.0.1", 18 | "dialect": "mysql" 19 | }, 20 | "production": { 21 | "username": "root", 22 | "password": process.env.DB_PASSWORD, 23 | "database": "react-nodebird", 24 | "host": "127.0.0.1", 25 | "dialect": "mysql" 26 | } 27 | }; 28 | exports["default"] = config; 29 | -------------------------------------------------------------------------------- /ch3/back/config/config.ts: -------------------------------------------------------------------------------- 1 | import * as dotenv from 'dotenv'; 2 | dotenv.config(); 3 | 4 | type Config = { 5 | username: string, 6 | password: string, 7 | database: string, 8 | host: string, 9 | [key: string]: string, 10 | } 11 | interface IConfigGroup { 12 | development: Config; 13 | test: Config; 14 | production: Config; 15 | } 16 | const config: IConfigGroup = { 17 | "development": { 18 | "username": "root", 19 | "password": process.env.DB_PASSWORD!, 20 | "database": "react-nodebird", 21 | "host": "127.0.0.1", 22 | "dialect": "mysql" 23 | }, 24 | "test": { 25 | "username": "root", 26 | "password": process.env.DB_PASSWORD!, 27 | "database": "react-nodebird", 28 | "host": "127.0.0.1", 29 | "dialect": "mysql" 30 | }, 31 | "production": { 32 | "username": "root", 33 | "password": process.env.DB_PASSWORD!, 34 | "database": "react-nodebird", 35 | "host": "127.0.0.1", 36 | "dialect": "mysql" 37 | } 38 | }; 39 | 40 | export default config; 41 | -------------------------------------------------------------------------------- /ch3/back/index.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import { RequestHandler, ErrorRequestHandler, Request, Response, NextFunction } from 'express'; 3 | import morgan from 'morgan'; 4 | import cors from 'cors'; 5 | import cookieParser from 'cookie-parser'; 6 | import expressSession from 'express-session'; 7 | import dotenv from 'dotenv'; 8 | import passport from 'passport'; 9 | import hpp from 'hpp'; 10 | import helmet from 'helmet'; 11 | 12 | import passportConfig from './passport'; 13 | import { sequelize } from './models'; 14 | import userRouter from './routes/user'; 15 | import postRouter from './routes/post' 16 | import postsRouter from './routes/posts' 17 | import hashtagRouter from './routes/hashtag' 18 | 19 | dotenv.config(); 20 | const app = express(); 21 | const prod: boolean = process.env.NODE_ENV === 'production'; 22 | 23 | app.set('port', prod ? process.env.PORT : 3065); 24 | passportConfig(); 25 | sequelize.sync({ force: false }) 26 | .then(() => { 27 | console.log('데이터베이스 연결 성공'); 28 | }) 29 | .catch((err: Error) => { 30 | console.error(err); 31 | }); 32 | 33 | if (prod) { 34 | app.use(hpp()); 35 | app.use(helmet()); 36 | app.use(morgan('combined')); 37 | app.use(cors({ 38 | origin: /nodebird\.com$/, 39 | credentials: true, 40 | })); 41 | } else { 42 | app.use(morgan('dev')); 43 | app.use(cors({ 44 | origin: true, 45 | credentials: true, 46 | })) 47 | } 48 | 49 | app.use('/', express.static('uploads')); 50 | app.use(express.json()); 51 | app.use(express.urlencoded({ extended: true })); 52 | app.use(cookieParser(process.env.COOKIE_SECRET)); 53 | app.use(expressSession({ 54 | resave: false, 55 | saveUninitialized: false, 56 | secret: process.env.COOKIE_SECRET!, 57 | cookie: { 58 | httpOnly: true, 59 | secure: false, // https -> true 60 | domain: prod ? '.nodebird.com' : undefined, 61 | }, 62 | name: 'rnbck', 63 | })); 64 | app.use(passport.initialize()); 65 | app.use(passport.session()); 66 | 67 | app.use('/post', postRouter); 68 | app.use('/posts', postsRouter); 69 | app.use('/hashtag', hashtagRouter); 70 | app.use('/user', userRouter); 71 | 72 | app.get('/', (req, res, next) => { 73 | res.send('react nodebird 백엔드 정상 동작!'); 74 | }); 75 | 76 | app.use((err: any, req: Request, res: Response, next: NextFunction) => { 77 | console.error(err); 78 | res.status(500).send('서버 에러 발생! 서버 콘솔을 확인하세요.'); 79 | }); 80 | 81 | app.listen(app.get('port'), () => { 82 | console.log(`server is running on ${app.get('port')}`); 83 | }); 84 | -------------------------------------------------------------------------------- /ch3/back/models/comment.ts: -------------------------------------------------------------------------------- 1 | import { DataTypes, Model } from 'sequelize'; 2 | import { dbType } from './index'; 3 | import { sequelize } from './sequelize'; 4 | 5 | class Comment extends Model { 6 | public readonly id!: number; 7 | public content!: string; 8 | public readonly createdAt!: Date; 9 | public readonly updatedAt!: Date; 10 | } 11 | 12 | Comment.init({ 13 | content: { 14 | type: DataTypes.TEXT, 15 | allowNull: false, 16 | } 17 | }, { 18 | sequelize, 19 | modelName: 'Comment', 20 | tableName: 'comment', 21 | charset: 'utf8mb4', 22 | collate: 'utf8mb4_general_ci', 23 | }); 24 | 25 | export const associate =(db: dbType) => { 26 | 27 | }; 28 | 29 | export default Comment; 30 | -------------------------------------------------------------------------------- /ch3/back/models/hashtag.ts: -------------------------------------------------------------------------------- 1 | import { DataTypes, Model } from 'sequelize'; 2 | import { dbType } from './index'; 3 | import { sequelize } from './sequelize'; 4 | 5 | class Hashtag extends Model { 6 | public readonly id!: number; 7 | public name!: string; 8 | public readonly createdAt!: Date; 9 | public readonly updatedAt!: Date; 10 | } 11 | 12 | Hashtag.init({ 13 | name: { 14 | type: DataTypes.STRING(20), 15 | allowNull: false, 16 | }, 17 | }, { 18 | sequelize, 19 | modelName: 'Hashtag', 20 | tableName: 'hashtag', 21 | charset: 'utf8mb4', 22 | collate: 'utf8mb4_general_ci', 23 | }); 24 | 25 | export const associate = (db: dbType) => {}; 26 | 27 | export default Hashtag; 28 | -------------------------------------------------------------------------------- /ch3/back/models/image.ts: -------------------------------------------------------------------------------- 1 | import { DataTypes, Model } from 'sequelize'; 2 | import { dbType } from './index'; 3 | import { sequelize } from './sequelize'; 4 | 5 | class Image extends Model { 6 | public readonly id!: number; 7 | public src!: number; 8 | public readonly createdAt!: Date; 9 | public readonly updatedAt!: Date; 10 | } 11 | 12 | Image.init({ 13 | src: { 14 | type: DataTypes.STRING(200), 15 | allowNull: false, 16 | }, 17 | }, { 18 | sequelize, 19 | modelName: 'Image', 20 | tableName: 'image', 21 | charset: 'utf8', 22 | collate: 'utf8_general_ci', 23 | }); 24 | 25 | export const associate = (db: dbType) => { 26 | 27 | }; 28 | 29 | export default Image; 30 | -------------------------------------------------------------------------------- /ch3/back/models/index.ts: -------------------------------------------------------------------------------- 1 | import User, { associate as associateUser } from './user'; 2 | import Comment, { associate as associateComment } from './comment'; 3 | import Hashtag, { associate as associateHashtag } from './hashtag'; 4 | import Image, { associate as associateImage } from './image'; 5 | import Post, { associate as associatePost } from './post'; 6 | 7 | export * from './sequelize'; 8 | const db = { 9 | User, 10 | Comment, 11 | Hashtag, 12 | Image, 13 | Post, 14 | }; 15 | export type dbType = typeof db; 16 | 17 | associateUser(db); 18 | associateComment(db); 19 | associateHashtag(db); 20 | associateImage(db); 21 | associatePost(db); 22 | -------------------------------------------------------------------------------- /ch3/back/models/post.ts: -------------------------------------------------------------------------------- 1 | import { 2 | DataTypes, Model, BelongsToManyAddAssociationsMixin, HasManyAddAssociationsMixin, 3 | HasManyAddAssociationMixin, BelongsToManyAddAssociationMixin, BelongsToManyRemoveAssociationMixin, 4 | } from 'sequelize'; 5 | import { dbType } from './index'; 6 | import { sequelize } from './sequelize'; 7 | import Hashtag from './hashtag'; 8 | import Image from './image'; 9 | import User from './user'; 10 | 11 | class Post extends Model { 12 | public readonly id!: number; 13 | public content!: string; 14 | public readonly createdAt!: Date; 15 | public readonly updatedAt!: Date; 16 | 17 | public UserId!: number; 18 | public readonly Retweet?: Post; 19 | public RetweetId?: number; 20 | 21 | public addHashtags!: BelongsToManyAddAssociationsMixin 22 | public addImages!: HasManyAddAssociationsMixin 23 | public addImage!: HasManyAddAssociationMixin 24 | public addLiker!: BelongsToManyAddAssociationMixin 25 | public removeLiker!: BelongsToManyRemoveAssociationMixin 26 | } 27 | 28 | Post.init({ 29 | content: { 30 | type: DataTypes.TEXT, 31 | allowNull: false, 32 | }, 33 | }, { 34 | sequelize, 35 | modelName: 'Post', 36 | tableName: 'post', 37 | charset: 'utf8mb4', 38 | collate: 'utf8mb4_general_ci', 39 | }); 40 | 41 | export const associate = (db: dbType) => { 42 | db.Post.belongsTo(db.User); 43 | db.Post.hasMany(db.Comment); 44 | db.Post.hasMany(db.Image); 45 | db.Post.belongsTo(db.Post, { as: 'Retweet' }); 46 | db.Post.belongsToMany(db.Hashtag, { through: 'PostHashtag' }); 47 | db.Post.belongsToMany(db.User, { through: 'Like', as: 'Likers' }); 48 | }; 49 | 50 | export default Post; 51 | -------------------------------------------------------------------------------- /ch3/back/models/sequelize.ts: -------------------------------------------------------------------------------- 1 | import { Sequelize } from 'sequelize'; 2 | import config from '../config/config'; 3 | 4 | const env = process.env.NODE_ENV as ('production' | 'test' | 'development') || 'development'; 5 | const { database, username, password } = config[env]; 6 | const sequelize = new Sequelize(database, username, password, config[env]); 7 | 8 | export { sequelize }; 9 | export default sequelize; 10 | -------------------------------------------------------------------------------- /ch3/back/models/user.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Model, DataTypes, BelongsToManyGetAssociationsMixin, 3 | HasManyGetAssociationsMixin, BelongsToManyRemoveAssociationMixin, 4 | BelongsToManyAddAssociationMixin, 5 | } from 'sequelize'; 6 | import { dbType } from './index'; 7 | import Post from './post'; 8 | import { sequelize } from './sequelize'; 9 | 10 | class User extends Model { 11 | public readonly id!: number; 12 | public nickname!: string; 13 | public userId!: string; 14 | public password!: string; 15 | public readonly createdAt!: Date; 16 | public readonly updatedAt!: Date; 17 | 18 | public readonly Posts?: Post[]; 19 | public readonly Followers?: User[]; 20 | public readonly Followings?: User[]; 21 | 22 | public addFollowing!: BelongsToManyAddAssociationMixin; 23 | public getFollowings!: BelongsToManyGetAssociationsMixin; 24 | public removeFollowing!: BelongsToManyRemoveAssociationMixin; 25 | public getFollowers!: BelongsToManyGetAssociationsMixin; 26 | public removeFollower!: BelongsToManyRemoveAssociationMixin; 27 | public getPosts!: HasManyGetAssociationsMixin; 28 | } 29 | 30 | User.init({ 31 | nickname: { 32 | type: DataTypes.STRING(20), 33 | }, 34 | userId: { 35 | type: DataTypes.STRING(20), 36 | allowNull: false, 37 | unique: true, 38 | }, 39 | password: { 40 | type: DataTypes.STRING(100), 41 | allowNull: false, 42 | } 43 | }, { 44 | sequelize, 45 | modelName: 'User', 46 | tableName: 'user', 47 | charset: 'utf8', 48 | collate: 'utf8_general_ci', 49 | }); 50 | 51 | export const associate = (db: dbType) => { 52 | db.User.hasMany(db.Post, { as: 'Posts' }); 53 | db.User.hasMany(db.Comment); 54 | db.User.belongsToMany(db.Post, { through: 'Like', as: 'Liked' }); 55 | db.User.belongsToMany(db.User, { through: 'Follow', as: 'Followers', foreignKey: 'followingId' }); 56 | db.User.belongsToMany(db.User, { through: 'Follow', as: 'Followings', foreignKey: 'followerId' }); 57 | }; 58 | 59 | export default User; 60 | -------------------------------------------------------------------------------- /ch3/back/nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "exec": "npx ts-node index.ts", 3 | "ext": "js json ts" 4 | } 5 | -------------------------------------------------------------------------------- /ch3/back/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nodebird-server", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "dev": "nodemon", 8 | "start": "tsc && node index" 9 | }, 10 | "author": "ZeroCho", 11 | "license": "MIT", 12 | "dependencies": { 13 | "@types/bcrypt": "^3.0.0", 14 | "@types/cookie-parser": "^1.4.2", 15 | "@types/cors": "^2.8.8", 16 | "@types/dotenv": "^8.2.0", 17 | "@types/express": "^4.17.8", 18 | "@types/express-session": "^1.17.0", 19 | "@types/helmet": "0.0.48", 20 | "@types/hpp": "^0.2.1", 21 | "@types/morgan": "^1.9.2", 22 | "@types/multer": "^1.4.4", 23 | "@types/multer-s3": "^2.7.8", 24 | "@types/node": "^16.11.26", 25 | "@types/passport": "^1.0.7", 26 | "@types/passport-local": "^1.0.34", 27 | "aws-sdk": "^2.786.0", 28 | "bcrypt": "^5.0.0", 29 | "cookie-parser": "^1.4.5", 30 | "cors": "^2.8.5", 31 | "dotenv": "^8.2.0", 32 | "express": "^4.17.1", 33 | "express-session": "^1.17.1", 34 | "helmet": "^4.2.0", 35 | "hpp": "^0.2.3", 36 | "morgan": "^1.10.0", 37 | "multer": "^1.4.2", 38 | "multer-s3": "^2.9.0", 39 | "mysql2": "^2.2.5", 40 | "passport": "^0.5.2", 41 | "passport-local": "^1.0.0", 42 | "sequelize": "^6.29.0", 43 | "sequelize-cli": "^6.2.0", 44 | "typescript": "^4.0.5" 45 | }, 46 | "devDependencies": { 47 | "nodemon": "^2.0.6", 48 | "ts-node": "^9.1.1" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /ch3/back/passport/index.ts: -------------------------------------------------------------------------------- 1 | import passport from 'passport'; 2 | import User from '../models/user'; 3 | import local from './local'; 4 | 5 | export default () => { 6 | passport.serializeUser((user, done) => { 7 | done(null, user.id); 8 | }); 9 | 10 | passport.deserializeUser(async (id, done) => { 11 | try { 12 | const user = await User.findOne({ 13 | where: { id }, 14 | }); 15 | if (!user) { 16 | return done(new Error('no user')); 17 | } 18 | return done(null, user); // req.user 19 | } catch (err) { 20 | console.error(err); 21 | return done(err); 22 | } 23 | }); 24 | 25 | local(); 26 | } 27 | -------------------------------------------------------------------------------- /ch3/back/passport/local.ts: -------------------------------------------------------------------------------- 1 | import passport from 'passport'; 2 | import bcrypt from 'bcrypt'; 3 | import { Strategy } from 'passport-local'; 4 | import User from '../models/user'; 5 | 6 | export default () => { 7 | passport.use('local', new Strategy({ 8 | usernameField: 'userId', 9 | passwordField: 'password', 10 | }, async (userId, password, done) => { 11 | try { 12 | const user = await User.findOne({ where: { userId } }); 13 | if (!user) { 14 | return done(null, false, { message: '존재하지 않는 사용자입니다!' }); 15 | } 16 | const result = await bcrypt.compare(password, user.password); 17 | if (result) { 18 | return done(null, user); 19 | } 20 | return done(null, false, { message: '비밀번호가 틀립니다.' }); 21 | } catch (err) { 22 | console.error(err); 23 | return done(err); 24 | } 25 | })) 26 | }; 27 | -------------------------------------------------------------------------------- /ch3/back/routes/hashtag.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import { Request } from 'express'; 3 | import Sequelize from 'sequelize'; 4 | 5 | import Hashtag from '../models/hashtag'; 6 | import Image from '../models/image'; 7 | import Post from '../models/post'; 8 | import User from '../models/user'; 9 | 10 | const router = express.Router(); 11 | 12 | router.get('/:tag', async (req: Request, res, next) => { 13 | try { 14 | let where = {}; 15 | if (parseInt(req.query.lastId, 10)) { 16 | where = { 17 | id: { 18 | [Sequelize.Op.lt]: parseInt(req.query.lastId, 10), 19 | }, 20 | }; 21 | } 22 | const posts = await Post.findAll({ 23 | where, 24 | include: [{ 25 | model: Hashtag, 26 | where: { name: decodeURIComponent(req.params.tag) }, 27 | }, { 28 | model: User, 29 | attributes: ['id', 'nickname'], 30 | }, { 31 | model: Image, 32 | }, { 33 | model: User, 34 | as: 'Likers', 35 | attributes: ['id'], 36 | }, { 37 | model: Post, 38 | as: 'Retweet', 39 | include: [{ 40 | model: User, 41 | attributes: ['id', 'nickname'], 42 | }, { 43 | model: Image, 44 | }] 45 | }], 46 | order: [['createdAt', 'DESC']], 47 | limit: parseInt(req.query.limit, 10), 48 | }) 49 | res.json(posts); 50 | } catch (err) { 51 | console.error(err); 52 | return next(err); 53 | } 54 | }); 55 | 56 | export default router; 57 | -------------------------------------------------------------------------------- /ch3/back/routes/middleware.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from "express"; 2 | 3 | const isLoggedIn = (req: Request, res: Response, next: NextFunction) => { 4 | if (req.isAuthenticated()) { 5 | next(); 6 | } else { 7 | res.status(401).send('로그인이 필요합니다.'); 8 | } 9 | }; 10 | 11 | const isNotLoggedIn = (req: Request, res: Response, next: NextFunction) => { 12 | if (!req.isAuthenticated()) { 13 | next(); 14 | } else { 15 | res.status(401).send('로그인한 사용자는 접근할 수 없습니다.'); 16 | } 17 | }; 18 | 19 | export { isLoggedIn, isNotLoggedIn }; 20 | -------------------------------------------------------------------------------- /ch3/back/routes/posts.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import { Request } from 'express'; 3 | import Sequelize from 'sequelize'; 4 | 5 | import Image from '../models/image'; 6 | import Post from '../models/post'; 7 | import User from '../models/user'; 8 | 9 | const router = express.Router(); 10 | 11 | router.get('/', async (req: Request, res, next) => { 12 | try { 13 | let where = {}; 14 | if (parseInt(req.query.lastId, 10)) { 15 | where = { 16 | id: { 17 | [Sequelize.Op.lt]: parseInt(req.query.lastId, 10), // less than 18 | }, 19 | }; 20 | } 21 | const posts = await Post.findAll({ 22 | where, 23 | include: [{ 24 | model: User, 25 | attributes: ['id', 'nickname'], 26 | }, { 27 | model: Image, 28 | }, { 29 | model: User, 30 | as: 'Likers', 31 | attributes: ['id'], 32 | }, { 33 | model: Post, 34 | as: 'Retweet', 35 | include: [{ 36 | model: User, 37 | attributes: ['id', 'nickname'], 38 | }, { 39 | model: Image, 40 | }], 41 | }], 42 | order: [['createdAt', 'DESC']], // DESC는 내림차순, ASC는 오름차순 43 | limit: parseInt(req.query.limit, 10), 44 | }); 45 | return res.json(posts); 46 | } catch (err) { 47 | console.error(err); 48 | return next(err); 49 | } 50 | }); 51 | 52 | export default router; 53 | -------------------------------------------------------------------------------- /ch3/back/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "ts-node": { 3 | "files": true 4 | }, 5 | "compilerOptions": { 6 | "strict": true, 7 | "lib": ["es2020"], 8 | "esModuleInterop": true, 9 | "moduleResolution": "node", 10 | "typeRoots": ["./types"] 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /ch3/back/types/express.d.ts: -------------------------------------------------------------------------------- 1 | import User from '../models/user'; 2 | 3 | declare module "express-serve-static-core" { 4 | interface Request { 5 | user?: User; 6 | } 7 | } -------------------------------------------------------------------------------- /ch3/back/types/express/index.d.ts: -------------------------------------------------------------------------------- 1 | import User from '../../models/user'; 2 | 3 | declare module "express-serve-static-core" { 4 | interface Request { 5 | user?: User; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /ch3/back/types/index.d.ts: -------------------------------------------------------------------------------- 1 | import IUser from '../models/user'; 2 | 3 | declare global { 4 | namespace Express { 5 | export interface User extends IUser {} 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /js/back/config/config.js: -------------------------------------------------------------------------------- 1 | const dotenv = require('dotenv'); 2 | 3 | dotenv.config(); 4 | 5 | module.exports = { 6 | development: { 7 | username: 'root', 8 | password: process.env.DB_PASSWORD, 9 | database: 'react-nodebird', 10 | host: '127.0.0.1', 11 | dialect: 'mysql', 12 | }, 13 | test: { 14 | username: 'root', 15 | password: process.env.DB_PASSWORD, 16 | database: 'react-nodebird', 17 | host: '127.0.0.1', 18 | dialect: 'mysql', 19 | }, 20 | production: { 21 | username: 'root', 22 | password: process.env.DB_PASSWORD, 23 | database: 'react-nodebird', 24 | host: '127.0.0.1', 25 | dialect: 'mysql', 26 | }, 27 | }; 28 | -------------------------------------------------------------------------------- /js/back/index.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const morgan = require('morgan'); 3 | const cors = require('cors'); 4 | const cookieParser = require('cookie-parser'); 5 | const expressSession = require('express-session'); 6 | const dotenv = require('dotenv'); 7 | const passport = require('passport'); 8 | const hpp = require('hpp'); 9 | const helmet = require('helmet'); 10 | 11 | const passportConfig = require('./passport'); 12 | const db = require('./models'); 13 | const userAPIRouter = require('./routes/user'); 14 | const postAPIRouter = require('./routes/post'); 15 | const postsAPIRouter = require('./routes/posts'); 16 | const hashtagAPIRouter = require('./routes/hashtag'); 17 | 18 | const prod = process.env.NODE_ENV === 'production'; 19 | dotenv.config(); 20 | const app = express(); 21 | app.set('port', prod ? process.env.PORT : 3065); 22 | db.sequelize.sync(); 23 | passportConfig(); 24 | 25 | if (prod) { 26 | app.use(hpp()); 27 | app.use(helmet()); 28 | app.use(morgan('combined')); 29 | app.use(cors({ 30 | origin: /nodebird\.com$/, 31 | credentials: true, 32 | })); 33 | } else { 34 | app.use(morgan('dev')); 35 | app.use(cors({ 36 | origin: true, 37 | credentials: true, 38 | })); 39 | } 40 | 41 | app.use('/', express.static('uploads')); 42 | app.use(express.json()); 43 | app.use(express.urlencoded({ extended: true })); 44 | app.use(cookieParser(process.env.COOKIE_SECRET)); 45 | app.use(expressSession({ 46 | resave: false, 47 | saveUninitialized: false, 48 | secret: process.env.COOKIE_SECRET, 49 | cookie: { 50 | httpOnly: true, 51 | secure: false, // https를 쓸 때 true 52 | domain: prod && '.nodebird.com', 53 | }, 54 | name: 'rnbck', 55 | })); 56 | app.use(passport.initialize()); 57 | app.use(passport.session()); 58 | 59 | app.get('/', (req, res) => { 60 | res.send('react nodebird 백엔드 정상 동작!'); 61 | }); 62 | 63 | // API는 다른 서비스가 내 서비스의 기능을 실행할 수 있게 열어둔 창구 64 | app.use('/api/user', userAPIRouter); 65 | app.use('/api/post', postAPIRouter); 66 | app.use('/api/posts', postsAPIRouter); 67 | app.use('/api/hashtag', hashtagAPIRouter); 68 | 69 | app.listen(app.get('port'), () => { 70 | console.log(`server is running on ${app.get('port')}`); 71 | }); 72 | -------------------------------------------------------------------------------- /js/back/models/comment.js: -------------------------------------------------------------------------------- 1 | module.exports = (sequelize, DataTypes) => { 2 | const Comment = sequelize.define('Comment', { 3 | content: { 4 | type: DataTypes.TEXT, // 긴 글 5 | allowNull: false, 6 | }, 7 | }, { 8 | charset: 'utf8mb4', 9 | collate: 'utf8mb4_general_ci', 10 | }); 11 | Comment.associate = (db) => { 12 | db.Comment.belongsTo(db.User); 13 | db.Comment.belongsTo(db.Post); 14 | }; 15 | return Comment; 16 | }; 17 | -------------------------------------------------------------------------------- /js/back/models/hashtag.js: -------------------------------------------------------------------------------- 1 | module.exports = (sequelize, DataTypes) => { 2 | const Hashtag = sequelize.define('Hashtag', { 3 | name: { 4 | type: DataTypes.STRING(20), 5 | allowNull: false, 6 | }, 7 | }, { 8 | charset: 'utf8mb4', 9 | collate: 'utf8mb4_general_ci', 10 | }); 11 | Hashtag.associate = (db) => { 12 | db.Hashtag.belongsToMany(db.Post, { through: 'PostHashtag' }); 13 | }; 14 | return Hashtag; 15 | }; 16 | -------------------------------------------------------------------------------- /js/back/models/image.js: -------------------------------------------------------------------------------- 1 | module.exports = (sequelize, DataTypes) => { 2 | const Image = sequelize.define('Image', { 3 | src: { // S3 저장 4 | type: DataTypes.STRING(200), 5 | allowNull: false, 6 | }, 7 | }, { 8 | charset: 'utf8', 9 | collate: 'utf8_general_ci', 10 | }); 11 | Image.associate = (db) => { 12 | db.Image.belongsTo(db.Post); 13 | }; 14 | return Image; 15 | }; 16 | -------------------------------------------------------------------------------- /js/back/models/index.js: -------------------------------------------------------------------------------- 1 | const Sequelize = require('sequelize'); 2 | const env = process.env.NODE_ENV || 'development'; 3 | const config = require('../config/config')[env]; 4 | const db = {}; 5 | 6 | const sequelize = new Sequelize(config.database, config.username, config.password, config); 7 | 8 | db.Comment = require('./comment')(sequelize, Sequelize); 9 | db.Hashtag = require('./hashtag')(sequelize, Sequelize); 10 | db.Image = require('./image')(sequelize, Sequelize); 11 | db.Post = require('./post')(sequelize, Sequelize); 12 | db.User = require('./user')(sequelize, Sequelize); 13 | 14 | Object.keys(db).forEach(modelName => { 15 | if (db[modelName].associate) { 16 | db[modelName].associate(db); 17 | } 18 | }); 19 | 20 | db.sequelize = sequelize; 21 | db.Sequelize = Sequelize; 22 | 23 | module.exports = db; 24 | -------------------------------------------------------------------------------- /js/back/models/post.js: -------------------------------------------------------------------------------- 1 | module.exports = (sequelize, DataTypes) => { 2 | const Post = sequelize.define('Post', { // 테이블명은 posts 3 | content: { 4 | type: DataTypes.TEXT, // 매우 긴 글 5 | allowNull: false, 6 | }, 7 | }, { 8 | charset: 'utf8mb4', // 한글+이모티콘 9 | collate: 'utf8mb4_general_ci', 10 | }); 11 | Post.associate = (db) => { 12 | db.Post.belongsTo(db.User); // 테이블에 UserId 컬럼이 생겨요 13 | db.Post.hasMany(db.Comment); 14 | db.Post.hasMany(db.Image); 15 | db.Post.belongsTo(db.Post, { as: 'Retweet' }); // RetweetId 컬럼 생겨요 16 | db.Post.belongsToMany(db.Hashtag, { through: 'PostHashtag' }); 17 | db.Post.belongsToMany(db.User, { through: 'Like', as: 'Likers' }); 18 | }; 19 | return Post; 20 | }; 21 | -------------------------------------------------------------------------------- /js/back/models/user.js: -------------------------------------------------------------------------------- 1 | module.exports = (sequelize, DataTypes) => { 2 | const User = sequelize.define('User', { // 테이블명은 users 3 | nickname: { 4 | type: DataTypes.STRING(20), // 20글자 이하 5 | allowNull: false, // 필수 6 | }, 7 | userId: { 8 | type: DataTypes.STRING(20), 9 | allowNull: false, 10 | unique: true, // 고유한 값 11 | }, 12 | password: { 13 | type: DataTypes.STRING(100), // 100글자 이하 14 | allowNull: false, 15 | }, 16 | }, { 17 | charset: 'utf8', 18 | collate: 'utf8_general_ci', // 한글이 저장돼요 19 | }); 20 | 21 | User.associate = (db) => { 22 | db.User.hasMany(db.Post, { as: 'Posts' }); 23 | db.User.hasMany(db.Comment); 24 | db.User.belongsToMany(db.Post, { through: 'Like', as: 'Liked' }); 25 | db.User.belongsToMany(db.User, { through: 'Follow', as: 'Followers', foreignKey: 'followingId' }); 26 | db.User.belongsToMany(db.User, { through: 'Follow', as: 'Followings', foreignKey: 'followerId' }); 27 | }; 28 | 29 | return User; 30 | }; 31 | -------------------------------------------------------------------------------- /js/back/nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": [ 3 | "index.js", 4 | "routes", 5 | "config", 6 | "passport", 7 | "models", 8 | "nodemon.json" 9 | ], 10 | "exec": "node index.js", 11 | "ext": "js json" 12 | } 13 | -------------------------------------------------------------------------------- /js/back/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-nodebird-server", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "dev": "nodemon", 8 | "start": "cross-env NODE_ENV=production PORT=80 pm2 start index.js" 9 | }, 10 | "author": "ZeroCho", 11 | "license": "MIT", 12 | "dependencies": { 13 | "aws-sdk": "^2.814.0", 14 | "axios": "^0.21.0", 15 | "bcrypt": "^5.0.0", 16 | "cookie-parser": "^1.4.5", 17 | "cors": "^2.8.5", 18 | "cross-env": "^7.0.2", 19 | "dotenv": "^8.2.0", 20 | "express": "^4.18.2", 21 | "express-session": "^1.17.1", 22 | "helmet": "^3.22.0", 23 | "hpp": "^0.2.3", 24 | "morgan": "^1.10.0", 25 | "multer": "^1.4.2", 26 | "multer-s3": "^2.9.0", 27 | "mysql2": "^2.1.0", 28 | "passport": "^0.4.1", 29 | "passport-local": "^1.0.0", 30 | "pm2": "^5.2.2", 31 | "sequelize": "^6.29.0", 32 | "sequelize-cli": "^5.5.1" 33 | }, 34 | "devDependencies": { 35 | "eslint": "^7.1.0", 36 | "eslint-config-airbnb": "^18.1.0", 37 | "eslint-plugin-jsx-a11y": "^6.2.3", 38 | "nodemon": "^2.0.4" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /js/back/passport/index.js: -------------------------------------------------------------------------------- 1 | const passport = require('passport'); 2 | const db = require('../models'); 3 | const local = require('./local'); 4 | 5 | module.exports = () => { 6 | passport.serializeUser((user, done) => { // 서버쪽에 [{ id: 3, cookie: 'asdfgh' }] 7 | return done(null, user.id); 8 | }); 9 | 10 | passport.deserializeUser(async (id, done) => { 11 | try { 12 | const user = await db.User.findOne({ 13 | where: { id }, 14 | include: [{ 15 | model: db.Post, 16 | as: 'Posts', 17 | attributes: ['id'], 18 | }, { 19 | model: db.User, 20 | as: 'Followings', 21 | attributes: ['id'], 22 | }, { 23 | model: db.User, 24 | as: 'Followers', 25 | attributes: ['id'], 26 | }], 27 | }); 28 | return done(null, user); // req.user 29 | } catch (e) { 30 | console.error(e); 31 | return done(e); 32 | } 33 | }); 34 | 35 | local(); 36 | }; 37 | 38 | // 프론트에서 서버로는 cookie만 보내요(asdfgh) 39 | // 서버가 쿠키파서, 익스프레스 세션으로 쿠키 검사 후 id: 3 발견 40 | // id: 3이 deserializeUser에 들어감 41 | // req.user로 사용자 정보가 들어감 42 | 43 | // 요청 보낼때마다 deserializeUser가 실행됨(db 요청 1번씩 실행) 44 | // 실무에서는 deserializeUser 결과물 캐싱 45 | -------------------------------------------------------------------------------- /js/back/passport/local.js: -------------------------------------------------------------------------------- 1 | const passport = require('passport'); 2 | const { Strategy: LocalStrategy } = require('passport-local'); 3 | const bcrypt = require('bcrypt'); 4 | const db = require('../models'); 5 | 6 | module.exports = () => { 7 | passport.use(new LocalStrategy({ 8 | usernameField: 'userId', 9 | passwordField: 'password', 10 | }, async (userId, password, done) => { 11 | try { 12 | const user = await db.User.findOne({ where: { userId } }); 13 | if (!user) { 14 | return done(null, false, { reason: '존재하지 않는 사용자입니다!' }); 15 | } 16 | const result = await bcrypt.compare(password, user.password); 17 | if (result) { 18 | return done(null, user); 19 | } 20 | return done(null, false, { reason: '비밀번호가 틀립니다.' }); 21 | } catch (e) { 22 | console.error(e); 23 | return done(e); 24 | } 25 | })); 26 | }; 27 | -------------------------------------------------------------------------------- /js/back/routes/hashtag.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const db = require('../models'); 3 | 4 | const router = express.Router(); 5 | 6 | router.get('/:tag', async (req, res, next) => { 7 | try { 8 | let where = {}; 9 | if (parseInt(req.query.lastId, 10)) { 10 | where = { 11 | id: { 12 | [db.Sequelize.Op.lt]: parseInt(req.query.lastId, 10), 13 | }, 14 | }; 15 | } 16 | const posts = await db.Post.findAll({ 17 | where, 18 | include: [{ 19 | model: db.Hashtag, 20 | where: { name: decodeURIComponent(req.params.tag) }, 21 | }, { 22 | model: db.User, 23 | attributes: ['id', 'nickname'], 24 | }, { 25 | model: db.Image, 26 | }, { 27 | model: db.User, 28 | through: 'Like', 29 | as: 'Likers', 30 | attributes: ['id'], 31 | }, { 32 | model: db.Post, 33 | as: 'Retweet', 34 | include: [{ 35 | model: db.User, 36 | attributes: ['id', 'nickname'], 37 | }, { 38 | model: db.Image, 39 | }], 40 | }], 41 | order: [['createdAt', 'DESC']], 42 | limit: parseInt(req.query.limit, 10), 43 | }); 44 | res.json(posts); 45 | } catch (e) { 46 | console.error(e); 47 | next(e); 48 | } 49 | }); 50 | 51 | module.exports = router; 52 | -------------------------------------------------------------------------------- /js/back/routes/middleware.js: -------------------------------------------------------------------------------- 1 | exports.isLoggedIn = (req, res, next) => { 2 | if (req.isAuthenticated()) { 3 | next(); 4 | } else { 5 | res.status(401).send('로그인이 필요합니다.'); 6 | } 7 | }; 8 | 9 | exports.isNotLoggedIn = (req, res, next) => { 10 | if (!req.isAuthenticated()) { 11 | next(); 12 | } else { 13 | res.status(401).send('로그인한 사용자는 접근할 수 없습니다.'); 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /js/back/routes/posts.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const db = require('../models'); 3 | 4 | const router = express.Router(); 5 | 6 | router.get('/', async (req, res, next) => { // GET /api/posts 7 | try { 8 | let where = {}; 9 | if (parseInt(req.query.lastId, 10)) { 10 | where = { 11 | id: { 12 | [db.Sequelize.Op.lt]: parseInt(req.query.lastId, 10), // less than 13 | }, 14 | }; 15 | } 16 | const posts = await db.Post.findAll({ 17 | where, 18 | include: [{ 19 | model: db.User, 20 | attributes: ['id', 'nickname'], 21 | }, { 22 | model: db.Image, 23 | }, { 24 | model: db.User, 25 | through: 'Like', 26 | as: 'Likers', 27 | attributes: ['id'], 28 | }, { 29 | model: db.Post, 30 | as: 'Retweet', 31 | include: [{ 32 | model: db.User, 33 | attributes: ['id', 'nickname'], 34 | }, { 35 | model: db.Image, 36 | }], 37 | }], 38 | order: [['createdAt', 'DESC']], // DESC는 내림차순, ASC는 오름차순 39 | limit: parseInt(req.query.limit, 10), 40 | }); 41 | res.json(posts); 42 | } catch (e) { 43 | console.error(e); 44 | next(e); 45 | } 46 | }); 47 | 48 | module.exports = router; 49 | -------------------------------------------------------------------------------- /js/front/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | [ 4 | "styled-components", 5 | { 6 | "ssr": true, 7 | "displayName": true, 8 | "preprocess": false 9 | } 10 | ] 11 | ], 12 | "presets": [ 13 | "next/babel" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /js/front/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "parserOptions": { 4 | "ecmaVersion": 2018, 5 | "sourceType": "module", 6 | "ecmaFeatures": { 7 | "jsx": true 8 | } 9 | }, 10 | "env": { 11 | "browser": true, 12 | "node": true, 13 | "es6": true 14 | }, 15 | "extends": [ 16 | "airbnb" 17 | ], 18 | "plugins": [ 19 | "import", 20 | "react-hooks" 21 | ], 22 | "rules": { 23 | "react/jsx-props-no-spreading": 0, 24 | "react/jsx-filename-extension": 0, 25 | "no-underscore-dangle":0, 26 | "react/forbid-prop-types": 0, 27 | "object-curly-newline": 0, 28 | "no-nested-ternary": 0 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /js/front/components/AppLayout.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Link from 'next/link'; 3 | import PropTypes from 'prop-types'; 4 | import { Col, Input, Menu, Row } from 'antd'; 5 | import { useSelector } from 'react-redux'; 6 | import Router from 'next/router'; 7 | import LoginForm from '../containers/LoginForm'; 8 | import UserProfile from '../containers/UserProfile'; 9 | 10 | const AppLayout = ({ children }) => { 11 | const { me } = useSelector((state) => state.user); 12 | 13 | const onSearch = (value) => { 14 | Router.push({ pathname: '/hashtag', query: { tag: value } }, `/hashtag/${value}`); 15 | }; 16 | 17 | return ( 18 |
19 | 20 | 노드버드 21 | 프로필 22 | 23 | 28 | 29 | 30 | 31 | 32 | {me 33 | ? 34 | : } 35 | 36 | 37 | {children} 38 | 39 | 40 | Made by ZeroCho 41 | 42 | 43 |
44 | ); 45 | }; 46 | 47 | AppLayout.propTypes = { 48 | children: PropTypes.node.isRequired, 49 | }; 50 | 51 | export default AppLayout; 52 | -------------------------------------------------------------------------------- /js/front/components/FollowButton.js: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react'; 2 | import { Button } from 'antd'; 3 | import PropTypes from 'prop-types'; 4 | import { useSelector } from 'react-redux'; 5 | 6 | const FollowButton = memo(({ post, onUnfollow, onFollow }) => { 7 | const { me } = useSelector((state) => state.user); 8 | return !me || post.User.id === me.id 9 | ? null 10 | : me.Followings && me.Followings.find((v) => v.id === post.User.id) 11 | ? 12 | : ; 13 | }); 14 | 15 | FollowButton.propTypes = { 16 | post: PropTypes.object.isRequired, 17 | onUnfollow: PropTypes.func.isRequired, 18 | onFollow: PropTypes.func.isRequired, 19 | }; 20 | 21 | export default FollowButton; 22 | -------------------------------------------------------------------------------- /js/front/components/FollowList.js: -------------------------------------------------------------------------------- 1 | import { Button, Card, List } from 'antd'; 2 | import { StopOutlined } from '@ant-design/icons'; 3 | import React, { memo } from 'react'; 4 | import PropTypes from 'prop-types'; 5 | 6 | const FollowList = memo(({ header, hasMore, onClickMore, data, onClickStop }) => ( 7 | {header}} 12 | loadMore={hasMore && } 13 | bordered 14 | dataSource={data} 15 | renderItem={(item) => ( 16 | 17 | ]}> 18 | 19 | 20 | 21 | )} 22 | /> 23 | )); 24 | 25 | FollowList.propTypes = { 26 | header: PropTypes.string.isRequired, 27 | hasMore: PropTypes.bool.isRequired, 28 | onClickMore: PropTypes.func.isRequired, 29 | data: PropTypes.array.isRequired, 30 | onClickStop: PropTypes.func.isRequired, 31 | }; 32 | 33 | export default FollowList; 34 | -------------------------------------------------------------------------------- /js/front/components/ImagesZoom/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import Slick from 'react-slick'; 4 | import { Overlay, Header, CloseBtn, SlickWrapper, ImgWrapper, Indicator } from './style'; 5 | import { backUrl } from '../../config/config'; 6 | 7 | const ImagesZoom = ({ images, onClose }) => { 8 | const [currentSlide, setCurrentSlide] = useState(0); 9 | 10 | return ( 11 | 12 |
13 |

상세 이미지

14 | 15 |
16 | 17 |
18 | setCurrentSlide(slide)} 21 | infinite={false} 22 | arrows 23 | slidesToShow={1} 24 | slidesToScroll={1} 25 | > 26 | {images.map((v) => ( 27 | 28 | 29 | 30 | ))} 31 | 32 | 33 |
34 | {currentSlide + 1} 35 | {' '} 36 | / 37 | {images.length} 38 |
39 |
40 |
41 |
42 |
43 | ); 44 | }; 45 | 46 | ImagesZoom.propTypes = { 47 | images: PropTypes.arrayOf(PropTypes.shape({ 48 | src: PropTypes.string, 49 | })).isRequired, 50 | onClose: PropTypes.func.isRequired, 51 | }; 52 | 53 | export default ImagesZoom; 54 | -------------------------------------------------------------------------------- /js/front/components/ImagesZoom/style.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { CloseOutlined } from '@ant-design/icons'; 3 | 4 | export const Overlay = styled.div` 5 | position: fixed; 6 | z-index: 5000; 7 | top: 0; 8 | left: 0; 9 | right: 0; 10 | bottom: 0; 11 | `; 12 | 13 | export const Header = styled.header` 14 | height: 44px; 15 | background: white; 16 | position: relative; 17 | padding: 0; 18 | text-align: center; 19 | 20 | & h1 { 21 | margin: 0; 22 | font-size: 17px; 23 | color: #333; 24 | line-height: 44px; 25 | } 26 | `; 27 | 28 | export const SlickWrapper = styled.div` 29 | height: calc(100% - 44px); 30 | background: #090909; 31 | `; 32 | 33 | export const CloseBtn = styled(CloseOutlined)` 34 | position: absolute; 35 | right: 0; 36 | top: 0; 37 | padding: 15px; 38 | line-height: 14px; 39 | cursor: pointer; 40 | `; 41 | 42 | export const Indicator = styled.div` 43 | text-align: center; 44 | 45 | & > div { 46 | width: 75px; 47 | height: 30px; 48 | line-height: 30px; 49 | border-radius: 15px; 50 | background: #313131; 51 | display: inline-block; 52 | text-align: center; 53 | color: white; 54 | font-size: 15px; 55 | } 56 | `; 57 | 58 | export const ImgWrapper = styled.div` 59 | padding: 32px; 60 | text-align: center; 61 | 62 | & img { 63 | margin: 0 auto; 64 | max-height: 750px; 65 | } 66 | `; 67 | -------------------------------------------------------------------------------- /js/front/components/PostCardContent.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Link from 'next/link'; 3 | import PropTypes from 'prop-types'; 4 | 5 | const PostCardContent = ({ postData }) => ( 6 |
7 | {postData.split(/(#[^\s]+)/g).map((v) => { 8 | if (v.match(/#[^\s]+/)) { 9 | return ( 10 | 15 | {v} 16 | 17 | ); 18 | } 19 | return v; 20 | })} 21 |
22 | ); 23 | 24 | PostCardContent.propTypes = { 25 | postData: PropTypes.string.isRequired, 26 | }; 27 | 28 | export default PostCardContent; 29 | -------------------------------------------------------------------------------- /js/front/components/PostImages.js: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useState } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { PlusOutlined } from '@ant-design/icons'; 4 | 5 | import ImagesZoom from './ImagesZoom'; 6 | 7 | const PostImages = ({ images }) => { 8 | const [showImagesZoom, setShowImagesZoom] = useState(false); 9 | 10 | const onZoom = useCallback(() => { 11 | setShowImagesZoom(true); 12 | }, []); 13 | 14 | const onClose = useCallback(() => { 15 | setShowImagesZoom(false); 16 | }, []); 17 | 18 | if (images.length === 1) { 19 | return ( 20 | <> 21 | 22 | {showImagesZoom && } 23 | 24 | ); 25 | } 26 | if (images.length === 2) { 27 | return ( 28 | <> 29 |
30 | 31 | 32 |
33 | {showImagesZoom && } 34 | 35 | ); 36 | } 37 | return ( 38 | <> 39 |
40 | 41 |
45 | 46 |
47 | {images.length - 1} 48 | 개의 사진 더보기 49 |
50 |
51 | {showImagesZoom && } 52 | 53 | ); 54 | }; 55 | 56 | PostImages.propTypes = { 57 | images: PropTypes.arrayOf(PropTypes.shape({ 58 | src: PropTypes.string, 59 | })).isRequired, 60 | }; 61 | 62 | export default PostImages; 63 | -------------------------------------------------------------------------------- /js/front/config/config.js: -------------------------------------------------------------------------------- 1 | const backUrl = process.env.NODE_ENV === 'production' ? 'https://api.nodebird.com' : 'http://localhost:3065'; 2 | 3 | export { backUrl }; 4 | -------------------------------------------------------------------------------- /js/front/containers/CommentForm.js: -------------------------------------------------------------------------------- 1 | import { Button, Form, Input } from 'antd'; 2 | import React, { useCallback, useEffect, useState } from 'react'; 3 | import { useSelector, useDispatch } from 'react-redux'; 4 | import PropTypes from 'prop-types'; 5 | import { ADD_COMMENT_REQUEST } from '../reducers/post'; 6 | 7 | const CommentForm = ({ post }) => { 8 | const [commentText, setCommentText] = useState(''); 9 | const { commentAdded, isAddingComment } = useSelector((state) => state.post); 10 | const { me } = useSelector((state) => state.user); 11 | const dispatch = useDispatch(); 12 | 13 | const onSubmitComment = useCallback((e) => { 14 | e.preventDefault(); 15 | if (!me) { 16 | return alert('로그인이 필요합니다.'); 17 | } 18 | return dispatch({ 19 | type: ADD_COMMENT_REQUEST, 20 | data: { 21 | postId: post.id, 22 | content: commentText, 23 | }, 24 | }); 25 | }, [me && me.id, commentText]); 26 | 27 | useEffect(() => { 28 | setCommentText(''); 29 | }, [commentAdded === true]); 30 | 31 | const onChangeCommentText = useCallback((e) => { 32 | setCommentText(e.target.value); 33 | }, []); 34 | 35 | return ( 36 |
37 | 38 | 39 | 40 | 41 |
42 | ); 43 | }; 44 | 45 | CommentForm.propTypes = { 46 | post: PropTypes.object.isRequired, 47 | }; 48 | 49 | export default CommentForm; 50 | -------------------------------------------------------------------------------- /js/front/containers/LoginForm.js: -------------------------------------------------------------------------------- 1 | import React, { useCallback } from 'react'; 2 | import { Button, Form, Input } from 'antd'; 3 | import Link from 'next/link'; 4 | import { useDispatch, useSelector } from 'react-redux'; 5 | import styled from 'styled-components'; 6 | import { useInput } from '../pages/signup'; // TODO: util 폴더로 옮기기 7 | import { LOG_IN_REQUEST } from '../reducers/user'; 8 | 9 | const LoginError = styled.div` 10 | color: red; 11 | `; 12 | 13 | const LoginForm = () => { 14 | const [id, onChangeId] = useInput(''); 15 | const [password, onChangePassword] = useInput(''); 16 | const { isLoggingIn, logInErrorReason } = useSelector((state) => state.user); 17 | const dispatch = useDispatch(); 18 | 19 | const onSubmitForm = useCallback((e) => { 20 | e.preventDefault(); 21 | dispatch({ 22 | type: LOG_IN_REQUEST, 23 | data: { 24 | userId: id, 25 | password, 26 | }, 27 | }); 28 | }, [id, password]); 29 | 30 | return ( 31 |
32 |
33 | 34 |
35 | 36 |
37 |
38 | 39 |
40 | 41 |
42 | {logInErrorReason} 43 |
44 | 45 | 46 |
47 |
48 | ); 49 | }; 50 | 51 | export default LoginForm; 52 | -------------------------------------------------------------------------------- /js/front/containers/NicknameEditForm.js: -------------------------------------------------------------------------------- 1 | import { Button, Form, Input } from 'antd'; 2 | import React, { useState, useCallback } from 'react'; 3 | import { useDispatch, useSelector } from 'react-redux'; 4 | import { EDIT_NICKNAME_REQUEST } from '../reducers/user'; 5 | 6 | const NicknameEditForm = () => { 7 | const [editedName, setEditedName] = useState(''); 8 | const dispatch = useDispatch(); 9 | const { me, isEditingNickname } = useSelector((state) => state.user); 10 | 11 | const onChangeNickname = useCallback((e) => { 12 | setEditedName(e.target.value); 13 | }, []); 14 | 15 | const onEditNickname = useCallback((e) => { 16 | e.preventDefault(); 17 | dispatch({ 18 | type: EDIT_NICKNAME_REQUEST, 19 | data: editedName, 20 | }); 21 | }, [editedName]); 22 | 23 | return ( 24 |
25 | 26 | 27 |
28 | ); 29 | }; 30 | 31 | export default NicknameEditForm; 32 | -------------------------------------------------------------------------------- /js/front/containers/PostForm.js: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useState, useEffect, useRef } from 'react'; 2 | import { Form, Input, Button } from 'antd'; 3 | import { useSelector, useDispatch } from 'react-redux'; 4 | import { ADD_POST_REQUEST, REMOVE_IMAGE, UPLOAD_IMAGES_REQUEST } from '../reducers/post'; 5 | 6 | const PostForm = () => { 7 | const dispatch = useDispatch(); 8 | const [text, setText] = useState(''); 9 | const { imagePaths, isAddingPost, postAdded } = useSelector((state) => state.post); 10 | const imageInput = useRef(); 11 | 12 | useEffect(() => { 13 | if (postAdded) { 14 | setText(''); 15 | } 16 | }, [postAdded]); 17 | 18 | const onSubmitForm = useCallback((e) => { 19 | e.preventDefault(); 20 | if (!text || !text.trim()) { 21 | return alert('게시글을 작성하세요.'); 22 | } 23 | const formData = new FormData(); 24 | imagePaths.forEach((i) => { 25 | formData.append('image', i); 26 | }); 27 | formData.append('content', text); 28 | dispatch({ 29 | type: ADD_POST_REQUEST, 30 | data: formData, 31 | }); 32 | }, [text, imagePaths]); 33 | 34 | const onChangeText = useCallback((e) => { 35 | setText(e.target.value); 36 | }, []); 37 | 38 | const onChangeImages = useCallback((e) => { 39 | console.log(e.target.files); 40 | const imageFormData = new FormData(); 41 | [].forEach.call(e.target.files, (f) => { 42 | imageFormData.append('image', f); 43 | }); 44 | dispatch({ 45 | type: UPLOAD_IMAGES_REQUEST, 46 | data: imageFormData, 47 | }); 48 | }, []); 49 | 50 | const onClickImageUpload = useCallback(() => { 51 | imageInput.current.click(); 52 | }, [imageInput.current]); 53 | 54 | const onRemoveImage = useCallback((index) => () => { 55 | dispatch({ 56 | type: REMOVE_IMAGE, 57 | index, 58 | }); 59 | }, []); 60 | 61 | return ( 62 |
63 | 64 |
65 | 66 | 67 | 68 |
69 |
70 | {imagePaths.map((v, i) => ( 71 |
72 | {v} 73 |
74 | 75 |
76 |
77 | ))} 78 |
79 | 80 | ); 81 | }; 82 | 83 | export default PostForm; 84 | -------------------------------------------------------------------------------- /js/front/containers/UserProfile.js: -------------------------------------------------------------------------------- 1 | import { Avatar, Button, Card } from 'antd'; 2 | import React, { useCallback } from 'react'; 3 | import Link from 'next/link'; 4 | import { useDispatch, useSelector } from 'react-redux'; 5 | import { LOG_OUT_REQUEST } from '../reducers/user'; 6 | 7 | const UserProfile = () => { 8 | const { me } = useSelector((state) => state.user); 9 | const dispatch = useDispatch(); 10 | 11 | const onLogout = useCallback(() => { 12 | dispatch({ 13 | type: LOG_OUT_REQUEST, 14 | }); 15 | }, []); 16 | 17 | return ( 18 | 21 | 22 |
23 | 짹짹 24 |
25 | {me.Posts.length} 26 |
27 |
28 | , 29 | 30 | 31 |
32 | 팔로잉 33 |
34 | {me.Followings.length} 35 |
36 |
37 | , 38 | 39 | 40 |
41 | 팔로워 42 |
43 | {me.Followers.length} 44 |
45 |
46 | , 47 | ]} 48 | > 49 | {me.nickname[0]}} 51 | title={me.nickname} 52 | /> 53 | 54 |
55 | ); 56 | }; 57 | 58 | export default UserProfile; 59 | -------------------------------------------------------------------------------- /js/front/next.config.js: -------------------------------------------------------------------------------- 1 | const withBundleAnalyzer = require('@next/bundle-analyzer')({ 2 | enabled: process.env.ANALYZE === 'true', 3 | }); 4 | const webpack = require('webpack'); 5 | const CompressionPlugin = require('compression-webpack-plugin'); 6 | 7 | module.exports = withBundleAnalyzer({ 8 | distDir: '.next', 9 | webpack(config) { 10 | const prod = process.env.NODE_ENV === 'production'; 11 | const plugins = [ 12 | ...config.plugins, 13 | new webpack.ContextReplacementPlugin(/moment[/\\]locale$/, /^\.\/ko$/), 14 | ]; 15 | if (prod) { 16 | plugins.push(new CompressionPlugin()); // main.js.gz 17 | } 18 | return { 19 | ...config, 20 | mode: prod ? 'production' : 'development', 21 | devtool: prod ? 'hidden-source-map' : 'eval', 22 | plugins, 23 | }; 24 | }, 25 | }); 26 | -------------------------------------------------------------------------------- /js/front/nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": [ 3 | "server.js", 4 | "nodemon.json" 5 | ], 6 | "exec": "node server.js", 7 | "ext": "js json jsx" 8 | } 9 | -------------------------------------------------------------------------------- /js/front/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-nodebird-front", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "dev": "next", 8 | "build": "cross-env ANALYZE=true next build", 9 | "prestart": "npm run build", 10 | "start": "cross-env NODE_ENV=production PORT=80 pm2 start server.js" 11 | }, 12 | "author": "ZeroCho", 13 | "license": "MIT", 14 | "dependencies": { 15 | "@ant-design/icons": "^4.1.0", 16 | "@next/bundle-analyzer": "^9.4.4", 17 | "antd": "^4.2.5", 18 | "axios": "^0.19.2", 19 | "compression-webpack-plugin": "^4.0.0", 20 | "cookie-parser": "^1.4.5", 21 | "cross-env": "^7.0.2", 22 | "dotenv": "^8.2.0", 23 | "express": "^4.17.1", 24 | "express-session": "^1.17.1", 25 | "immer": "^6.0.9", 26 | "moment": "^2.26.0", 27 | "morgan": "^1.10.0", 28 | "next": "^12.1.0", 29 | "next-redux-saga": "^4.1.2", 30 | "next-redux-wrapper": "^6.0.0", 31 | "pm2": "^4.4.0", 32 | "prop-types": "^15.7.2", 33 | "react": "^16.13.1", 34 | "react-dom": "^16.13.1", 35 | "react-helmet": "^6.0.0", 36 | "react-redux": "^7.2.0", 37 | "react-slick": "^0.26.1", 38 | "redux": "^4.0.5", 39 | "redux-devtools-extension": "^2.13.8", 40 | "redux-saga": "^1.1.3", 41 | "styled-components": "^5.1.1" 42 | }, 43 | "devDependencies": { 44 | "babel-eslint": "^10.1.0", 45 | "babel-plugin-styled-components": "^1.10.7", 46 | "eslint": "^7.1.0", 47 | "eslint-config-airbnb": "^18.1.0", 48 | "eslint-plugin-import": "^2.20.2", 49 | "eslint-plugin-jsx-a11y": "^6.2.3", 50 | "eslint-plugin-react": "^7.20.0", 51 | "eslint-plugin-react-hooks": "^4.0.4", 52 | "nodemon": "^2.0.4", 53 | "webpack": "^4.43.0" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /js/front/pages/_app.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { applyMiddleware, compose, createStore } from 'redux'; 4 | import { Provider } from 'react-redux'; 5 | import createSagaMiddleware from 'redux-saga'; 6 | import axios from 'axios'; 7 | import { Helmet } from 'react-helmet'; 8 | 9 | import wrapper from "../store/configureStore"; 10 | import AppLayout from '../components/AppLayout'; 11 | import reducer from '../reducers'; 12 | import rootSaga from '../sagas'; 13 | import { LOAD_USER_REQUEST } from '../reducers/user'; 14 | 15 | // class NodeBird extends App { 16 | // static getInitialProps(context) { 17 | // 18 | // } 19 | // render() { 20 | // 21 | // } 22 | // } 23 | 24 | const NodeBird = ({ Component, pageProps }) => { 25 | return ( 26 | <> 27 | 58 | 59 | 60 | 61 | 62 | ); 63 | } 64 | 65 | NodeBird.propTypes = { 66 | Component: PropTypes.elementType.isRequired, 67 | pageProps: PropTypes.object.isRequired, 68 | }; 69 | 70 | NodeBird.getInitialProps = async (context) => { 71 | const { ctx, Component } = context; 72 | let pageProps = {}; 73 | const state = ctx.store.getState(); 74 | const cookie = ctx.isServer ? ctx.req.headers.cookie : ''; 75 | axios.defaults.headers.Cookie = ''; 76 | if (ctx.isServer && cookie) { 77 | axios.defaults.headers.Cookie = cookie; 78 | } 79 | if (!state.user.me) { 80 | ctx.store.dispatch({ 81 | type: LOAD_USER_REQUEST, 82 | }); 83 | } 84 | if (Component.getInitialProps) { 85 | pageProps = await Component.getInitialProps(ctx) || {}; 86 | } 87 | return { pageProps }; 88 | }; 89 | 90 | export default wrapper.withRedux(NodeBird); 91 | -------------------------------------------------------------------------------- /js/front/pages/_document.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Helmet } from 'react-helmet'; 3 | import PropTypes from 'prop-types'; 4 | import Document, { Main, NextScript } from 'next/document'; 5 | import { ServerStyleSheet } from 'styled-components'; 6 | 7 | class MyDocument extends Document { 8 | static async getInitialProps(context) { 9 | const sheet = new ServerStyleSheet(); 10 | const originalRenderPage = context.renderPage; 11 | 12 | try { 13 | context.renderPage = () => originalRenderPage({ 14 | enhanceApp: (App) => (props) => sheet.collectStyles(), 15 | }); 16 | const initialProps = await Document.getInitialProps(context); 17 | return { 18 | ...initialProps, 19 | helmet: Helmet.renderStatic(), 20 | styles: ( 21 | <> 22 | {initialProps.styles} 23 | {sheet.getStyleElement()} 24 | 25 | ), 26 | }; 27 | } finally { 28 | sheet.seal(); 29 | } 30 | } 31 | 32 | render() { 33 | const { htmlAttributes, bodyAttributes, ...helmet } = this.props.helmet; 34 | const htmlAttrs = htmlAttributes.toComponent(); 35 | const bodyAttrs = bodyAttributes.toComponent(); 36 | 37 | return ( 38 | 39 | 40 | {this.props.styles} 41 | {Object.values(helmet).map((el) => el.toComponent())} 42 | 43 | 44 |
45 | {process.env.NODE_ENV === 'production' 46 | &&