diff --git a/README.md b/README.md new file mode 100644 index 0000000..739dbf7 --- /dev/null +++ b/README.md @@ -0,0 +1,72 @@ +# DeveloPic + +# 디벨로픽 + +![AnimatedImage.gif](https://res.craft.do/user/full/b1bfd0d5-f25f-9cb2-4956-fa895ea5961c/doc/124075C2-FDE8-4DBA-8070-67F4FEE980FE/5B388B2A-20AB-4D17-92CA-C8E28F3D3E38_2/AnimatedImage.gif) + +사진작가를 위한 블로그 플랫폼입니다. + +> **디벨로퍼 + 픽쳐** + +> 사진을 좋아하는 개발자가 사진가를 위한 블로그 플랫폼을 만들어 보면 어떨까? 에서 시작한 프로젝트입니다. + +- 개발기간 : 2021.3 ~ 2021.6 ( 중간 공부기간 포함 ) +- 인원 : 프론트 + 백앤드 - 1인 / 프론트 - 2인 / 총 3인 + +## 관련 링크 + +#### 사이트 링크 + +[DeveloPic](https://developic.shop/) + +#### 클라이언트 깃헙 저장소 링크 + +[Conradmaker/developic2-client](https://github.com/Conradmaker/developic2-client) + +#### 백앤드 서버 깃헙 저장소 링크 + +[Conradmaker/developic-ver2-server](https://github.com/Conradmaker/developic-ver2-server) + +#### API 문서 + +[DeveloPic ver.2 API Documentation](https://documenter.getpostman.com/view/13075087/TzXtKLoR) + +## 프로젝트 스택 + +#### 클라이언트 + +- TypeScript +- React +- Next.js +- Redux (redux-toolkit) + redux-thunk +- emotion.js +- toast-ui +- 배포도구 - netlify + route53 + +#### 백앤드 서버 + +- TypeScript +- Node.js +- Express +- passport.js +- Sequelize +- MySQL +- AWS - EC2, RDS, S3, Lambda, route53 + +## 프로젝트 Architecture + +![developic (1).png.png](https://res.craft.do/user/full/b1bfd0d5-f25f-9cb2-4956-fa895ea5961c/doc/124075C2-FDE8-4DBA-8070-67F4FEE980FE/61FE36FC-3A39-4AD9-89D7-EE91369B58CD_2/developic%201.png.png) + +## ERD + +![developic2.png.png](https://res.craft.do/user/full/b1bfd0d5-f25f-9cb2-4956-fa895ea5961c/doc/124075C2-FDE8-4DBA-8070-67F4FEE980FE/DDC9883B-C169-4FAA-A3E4-CFB47B5C44DB_2/developic2.png.png) + +## 구현 기능 + +- 서버 모든 부분 +- DB설계 및 관리 +- API 서버 생성 및 관리 문서화 +- OAuth2.0 소셜로그인 및 이메일 인증 서비스 +- AWS S3와 Lambda를 이용한 이미지 리사이징 및 업로드 +- AWS 클라우드상에 배포 및 보안화 작업 + diff --git a/ecosystem.config.js b/ecosystem.config.js new file mode 100644 index 0000000..0eff0ec --- /dev/null +++ b/ecosystem.config.js @@ -0,0 +1,13 @@ +'use strict'; + +module.exports = { + apps: [ + { + name: 'developic', + script: './build/app.js', + watch: true, + env: { NODE_ENV: 'development' }, + env_production: { NODE_ENV: 'production' }, + }, + ], +}; diff --git a/package.json b/package.json index fd4cac8..f58e931 100644 --- a/package.json +++ b/package.json @@ -6,10 +6,9 @@ "license": "MIT", "scripts": { "dev": "nodemon", - "se": "sequelize init", - "build": "tsc", - "deploy": "pm2 start build/app.js", - "pro-start": "cross-env NODE_ENV=production pm2 start build/app.js" + "build": "cross-env NODE_OPTIONS=--max_old_space_size=800 tsc", + "start":"pm2 start ecosystem.config.js --env production", + "win-start": "cross-env NODE_ENV=production sudo pm2 start build/app.js" }, "dependencies": { "@types/express-session": "^1.17.3", diff --git a/src/app.ts b/src/app.ts index ae6aef2..ea3a8f8 100644 --- a/src/app.ts +++ b/src/app.ts @@ -5,15 +5,13 @@ import logger from 'morgan'; import cookieParser from 'cookie-parser'; import helmet from 'helmet'; import hpp from 'hpp'; -import expressSession from 'express-session'; +import expressSession, { CookieOptions } from 'express-session'; import path from 'path'; import passport from 'passport'; import { sequelize } from './db/models'; import router from './routes'; import passportConfig from './passport'; -console.log(process.env.NODE_ENV); - dotenv.config(); const prod = process.env.NODE_ENV === 'production'; const PORT = prod ? process.env.PROD_PORT : process.env.DEV_PORT; @@ -24,45 +22,68 @@ sequelize .catch(e => console.error(e)); const app = express(); + passportConfig(); + +let CookieConf: CookieOptions; if (prod) { app.use(logger('combined')); app.use(helmet()); app.use(hpp()); - //app.use(cors({ origin: ['https://developic.netlify.app/'], credentials: true })); app.use( cors({ - origin: ['http://localhost:3000', 'https://developic.netlify.app/'], + origin: [ + 'http://localhost:3000', + 'https://www.developic.shop', + 'https://developic.shop', + 'https://developic.netlify.app', + ], credentials: true, }) ); + CookieConf = { + httpOnly: true, + secure: true, + sameSite: 'none', + path: '/', + domain: '.developic.shop', + }; } else { app.use(logger('dev')); app.use(cors({ origin: true, credentials: true })); + CookieConf = { + httpOnly: true, + secure: false, + path: '/', + domain: undefined, + }; } + +app.use('/post/photo', express.static(path.join(__dirname, 'uploads/photos'))); app.use('/image', express.static(path.join(__dirname, 'uploads'))); + app.use(express.json()); app.use(express.urlencoded({ extended: true })); app.use(cookieParser(process.env.COOKIE_KEY)); +app.set('trust proxy', 1); app.use( expressSession({ resave: false, saveUninitialized: false, secret: process.env.COOKIE_KEY as string, - cookie: { - httpOnly: true, - secure: false, - // sameSite: 'none', - domain: prod ? process.env.CLIENT_DOMAIN : undefined, - }, + proxy: true, + cookie: CookieConf, name: 'develuth', }) ); app.use(passport.initialize()); app.use(passport.session()); -app.use('/post/photo', express.static(path.join(__dirname, 'uploads/photos'))); -app.use('/test', (req, res) => res.send('서버 작동중')); -app.use('/', router); -app.use('/err', (req, res) => res.send('에러')); + +app.use('/test', (req, res) => { + console.log(process.env.NODE_ENV); + res.send(process.env.NODE_ENV); +}); + +app.use('/api', router); app.listen(PORT, () => console.log(`${PORT}포트에서 서버가 실행되었습니다.`)); diff --git a/src/controllers/auth.ts b/src/controllers/auth.ts index a4edbdd..db236f1 100644 --- a/src/controllers/auth.ts +++ b/src/controllers/auth.ts @@ -34,7 +34,7 @@ export const signUpController: RequestHandler = async (req, res, next) => { name: req.body.name, nickname: req.body.nickname, loginType: 'local', - avatar: `${process.env.SERVER_DOMAIN}/image/avatar/initial_avatar.png`, + avatar: 'https://i.ibb.co/V28FxS7/images.png', verificationCode: randomNumber, }); if (!user) return res.status(400).send('회원가입중 오류 발생'); diff --git a/src/controllers/blog.ts b/src/controllers/blog.ts index 9a0d60b..a7b56ab 100644 --- a/src/controllers/blog.ts +++ b/src/controllers/blog.ts @@ -69,6 +69,7 @@ export const getBloggerPostListController: GetBloggerPostListHandler = async ( 'updatedAt', 'createdAt', 'UserId', + 'isPublic', ], order: [['createdAt', 'DESC']], include: [{ model: User, as: 'likers', attributes: ['id'] }], @@ -85,6 +86,7 @@ export const getBloggerPostListController: GetBloggerPostListHandler = async ( updatedAt: list[i].updatedAt, createdAt: list[i].createdAt, UserId: list[i].UserId, + isPublic: list[i].isPublic, } as Post; } res.status(200).json(list); @@ -114,9 +116,11 @@ export const getBloggerPicstoryListController: GetBloggerPicstoryListHandler = a 'thumbnail', 'updatedAt', 'createdAt', + 'UserId', ], order: [['createdAt', 'DESC']], include: [ + { model: User, attributes: ['id', 'nickname'] }, { model: Post, where: { state: 1 }, @@ -129,6 +133,7 @@ export const getBloggerPicstoryListController: GetBloggerPicstoryListHandler = a 'UserId', 'updatedAt', 'createdAt', + 'isPublic', ], include: [{ model: User, as: 'likers', attributes: ['id'] }], }, @@ -155,8 +160,9 @@ export const getBloggerPicPostListController: GetBloggerPicPostListHandler = asy where: { id: req.params.PicstoryId }, limit, offset, - attributes: ['id', 'title', 'description', 'thumbnail'], + attributes: ['id', 'title', 'description', 'thumbnail', 'UserId'], include: [ + { model: User, attributes: ['id', 'nickname'] }, { model: Post, where: { state: 1 }, @@ -169,6 +175,7 @@ export const getBloggerPicPostListController: GetBloggerPicPostListHandler = asy 'UserId', 'updatedAt', 'createdAt', + 'isPublic', ], order: [['createdAt', 'DESC']], include: [{ model: User, as: 'likers', attributes: ['id'] }], @@ -176,6 +183,10 @@ export const getBloggerPicPostListController: GetBloggerPicPostListHandler = asy ], }); if (!picPosts) return res.status(404).send('픽스토리를 찾을 수 없습니다.'); + const picUser = { + id: (picPosts.User as User).id, + nickname: (picPosts.User as User).nickname, + }; const computedPosts = (picPosts.Posts as Post[]).map(post => ({ id: post.id, title: post.title, @@ -186,12 +197,15 @@ export const getBloggerPicPostListController: GetBloggerPicPostListHandler = asy UserId: post.UserId, updatedAt: post.updatedAt, createdAt: post.createdAt, + isPublic: post.isPublic, })); return res.status(200).json({ id: picPosts.id, title: picPosts.title, - description: picPosts.title, + description: picPosts.description, thumbnail: picPosts.thumbnail, + UserId: picPosts.UserId, + User: picUser, Posts: computedPosts, }); } catch (e) { diff --git a/src/controllers/list.ts b/src/controllers/list.ts index 01a52e9..48b100b 100644 --- a/src/controllers/list.ts +++ b/src/controllers/list.ts @@ -33,6 +33,7 @@ export const getWriterList: GetWriterListHandler = async (req, res, next) => { from POST P join SUBSCRIBE S on S.WriterId = P.UserId where P.state = 1 + and P.isPublic = 1 and S.SubscriberId = :loginUserId order by P.createdAt DESC LIMIT 18446744073709551615) P2 @@ -46,6 +47,7 @@ from (select * from (select P.createdAt, P.UserId from POST P where P.state = 1 + and P.isPublic = 1 order by P.createdAt DESC LIMIT 18446744073709551615) P2 group by UserId) P3 @@ -77,6 +79,7 @@ export const getFeedList: GetFeedListHandler = async (req, res, next) => { ), }, state: 1, + isPublic: 1, }, limit, offset, @@ -119,7 +122,7 @@ export const getPostList: GetPostListHandler = async (req, res, next) => { list = await Post.findAll({ limit, offset, - where: { state: 1 }, + where: { state: 1, isPublic: 1 }, attributes: [ 'id', 'title', @@ -166,7 +169,7 @@ export const getPostList: GetPostListHandler = async (req, res, next) => { 'UserId', 'hits', ], - where: { state: 1 }, + where: { state: 1, isPublic: 1 }, include: [ { model: User, @@ -276,7 +279,7 @@ export const getHashTaggedPostController: GetHashTaggedPostHandler = async ( } const resultList = await tag.getPosts({ - where: { state: 1 }, + where: { state: 1, isPublic: 1 }, limit, offset, order: [[sort, 'DESC']], @@ -373,11 +376,13 @@ export const getSearchedListController: GetSearchedListHandler = async ( 'thumbnail', 'updatedAt', 'createdAt', + 'UserId', ], include: [ + { model: User, attributes: ['id', 'nickname', 'avatar'] }, { model: Post, - where: { state: 1 }, + where: { state: 1, isPublic: 1 }, attributes: [ 'id', 'title', @@ -387,6 +392,7 @@ export const getSearchedListController: GetSearchedListHandler = async ( 'UserId', 'updatedAt', 'createdAt', + 'isPublic', ], through: { attributes: [] }, include: [ @@ -411,7 +417,7 @@ export const getSearchedListController: GetSearchedListHandler = async ( resultList = await Post.findAll({ where: { [Op.and]: [ - { state: 1 }, + { state: 1, isPublic: 1 }, { [Op.or]: [ { title: { [Op.like]: `%${keyword}%` } }, @@ -457,7 +463,7 @@ export const getSearchedListController: GetSearchedListHandler = async ( attributes: ['id'], where: { [Op.and]: [ - { state: 1 }, + { state: 1, isPublic: 1 }, { [Op.or]: [ { title: { [Op.like]: `%${keyword}%` } }, @@ -539,7 +545,7 @@ export const getSearchedListController: GetSearchedListHandler = async ( { model: Post, attributes: ['id', 'thumbnail'], - where: { state: 1 }, + where: { state: 1, isPublic: 1 }, }, ], order: [['createdAt', 'DESC']], @@ -613,7 +619,7 @@ export const getSearchedListController: GetSearchedListHandler = async ( { model: Post, attributes: ['id', 'thumbnail'], - where: { state: 1 }, + where: { state: 1, isPublic: 1 }, }, ], }) diff --git a/src/controllers/picstory.ts b/src/controllers/picstory.ts index 5d0ccf0..25824a9 100644 --- a/src/controllers/picstory.ts +++ b/src/controllers/picstory.ts @@ -6,6 +6,7 @@ import { DestroyPicstoryHandler, GetUserPicstoryListHandler, RemovePostPicstoryHandler, + UpdatePicstoryHandler, } from '../types/picsotory'; //NOTE: 새 픽스토리 생성 @@ -32,6 +33,31 @@ export const createPicstoryController: CreatePicstoryHandler = async ( } }; + +//픽스토리 정보 수정 +export const updatePicstoryController: UpdatePicstoryHandler = async ( + req, + res, + next +) => { + try { + const picstory = await PicStory.findOne({ + where: { id: req.body.PicstoryId }, + }); + if (!picstory) return res.status(404).send('픽스토리를 찾을 수 없습니다.'); + await picstory.update({ + title: req.body.title, + description: req.body.description, + thumbnail: req.body.thumbnail, + }); + return res.status(201).json(req.body); + } catch (e) { + console.error(e); + next(e); + } +}; + + //NOTE: 픽스토리에 게시글 추가 export const addPostPicstoryController: AddPostPicstoryHandler = async ( req, diff --git a/src/db/models/picStory.ts b/src/db/models/picStory.ts index 7dd3440..90eddaf 100644 --- a/src/db/models/picStory.ts +++ b/src/db/models/picStory.ts @@ -8,6 +8,7 @@ import { import { DbType } from '.'; import Post from './post'; import sequelize from './sequelize'; +import User from './user'; class PicStory extends Model { public readonly id!: string; @@ -16,6 +17,7 @@ class PicStory extends Model { public description!: string; public isPrivate!: boolean; public thumbnail?: string; + public UserId?: number; public readonly createdAt!: Date; public readonly updatedAt!: Date; @@ -25,6 +27,7 @@ class PicStory extends Model { public getPosts!: BelongsToManyGetAssociationsMixin; public Posts?: Post[]; + public User?: User; } PicStory.init( diff --git a/src/passport/facebook.ts b/src/passport/facebook.ts index c4343eb..327a9b9 100644 --- a/src/passport/facebook.ts +++ b/src/passport/facebook.ts @@ -8,7 +8,7 @@ export default (): void => { { clientID: process.env.FACEBOOK_API as string, clientSecret: process.env.FACEBOOK_CLIENT_SECRET as string, - callbackURL: `${process.env.SERVER_DOMAIN}auth/facebook/callback`, + callbackURL: `${process.env.SERVER_DOMAIN}/auth/facebook/callback`, profileFields: ['id', 'displayName', 'photos', 'email'], }, async (accessToken, refreshToken, profile, done) => { diff --git a/src/passport/kakao.ts b/src/passport/kakao.ts index 6ddc9e5..5a7dbf9 100644 --- a/src/passport/kakao.ts +++ b/src/passport/kakao.ts @@ -8,7 +8,7 @@ export default (): void => { { clientID: process.env.KAKAO_API as string, clientSecret: '', - callbackURL: 'http://192.168.1.189:8000/auth/kakao/callback', + callbackURL: `${process.env.SERVER_DOMAIN}/auth/kakao/callback`, }, async (accessToken, refreshToken, profile, done) => { console.log(profile); diff --git a/src/routes/picstory.ts b/src/routes/picstory.ts index 03c10cf..674bfca 100644 --- a/src/routes/picstory.ts +++ b/src/routes/picstory.ts @@ -5,6 +5,7 @@ import { destroyPicstoryController, getUserPicstoryListController, removePostPicstoryController, + updatePicstoryController, } from '../controllers/picstory'; import { isLoggedIn } from '../middlewares/isLoggedIn'; @@ -20,5 +21,6 @@ picstoryRouter.patch('/post', isLoggedIn, removePostPicstoryController); picstoryRouter.delete('/:PicstoryId', isLoggedIn, destroyPicstoryController); //NOTE: 픽스토리 목록 조회 picstoryRouter.get('/:UserId', getUserPicstoryListController); +picstoryRouter.patch('/detail', updatePicstoryController); export default picstoryRouter; diff --git a/src/routes/user.ts b/src/routes/user.ts index daa33d2..21076fd 100644 --- a/src/routes/user.ts +++ b/src/routes/user.ts @@ -37,7 +37,7 @@ userRouter.post('/subscribe/add', isLoggedIn, subscribeWriter); userRouter.post('/subscribe/remove', isLoggedIn, unSubscribeWriter); //NOTE: 구독작가 목록 조회 -userRouter.get('/subscribe/:UserId', isLoggedIn, getSubscribeList); +userRouter.get('/subscribe/:UserId', getSubscribeList); //NOTE: 게시글 좋아요 userRouter.post('/like/post', isLoggedIn, addLikePostController); diff --git a/src/types/picsotory.ts b/src/types/picsotory.ts index b17199c..de5973d 100644 --- a/src/types/picsotory.ts +++ b/src/types/picsotory.ts @@ -15,6 +15,13 @@ export type AddPostPicstoryHandler = RequestHandler< Record >; +export type UpdatePicstoryHandler = RequestHandler< + { PicstoryId: string }, + unknown | string, + { title: string; description: string; thumbnail: string; PicstoryId: number }, + Record +>; + export type RemovePostPicstoryHandler = RequestHandler< unknown, { id: string } | string,