diff --git a/mission10_3/config/db.config.js b/mission10_3/config/db.config.js new file mode 100644 index 00000000..b4b4e32b --- /dev/null +++ b/mission10_3/config/db.config.js @@ -0,0 +1,17 @@ +import mysql from 'mysql2/promise'; +import dotenv from 'dotenv'; + +dotenv.config(); + +export const pool = mysql.createPool({ + host: process.env.DB_HOST || 'umc-database-week7.cx64ak0w82c4.ap-northeast-2.rds.amazonaws.com', // mysql의 hostname + user: process.env.DB_USER || 'root', // user 이름 + port: process.env.DB_PORT || 3306, // 포트 번호 + database: process.env.DB_TABLE || 'umc-week9-liv', // 데이터베이스 이름 + password: process.env.DB_PASSWORD || 'Lhiso329k!', // 비밀번호 + waitForConnections: true, + // Pool에 획득할 수 있는 connection이 없을 때, + // true면 요청을 queue에 넣고 connection을 사용할 수 있게 되면 요청을 실행하며, false이면 즉시 오류를 내보내고 다시 요청 + connectionLimit: 10, // 몇 개의 커넥션을 가지게끔 할 것인지 + queueLimit: 0, // getConnection에서 오류가 발생하기 전에 Pool에 대기할 요청의 개수 한도 +}); \ No newline at end of file diff --git a/mission10_3/config/error.js b/mission10_3/config/error.js new file mode 100644 index 00000000..4e6be065 --- /dev/null +++ b/mission10_3/config/error.js @@ -0,0 +1,6 @@ +export class BaseError extends Error { + constructor(data){ + super(data.message); + this.data = data; + } +} \ No newline at end of file diff --git a/mission10_3/config/response.js b/mission10_3/config/response.js new file mode 100644 index 00000000..5c2ec8ae --- /dev/null +++ b/mission10_3/config/response.js @@ -0,0 +1,9 @@ +export const response = ({isSuccess, code, message, status}, result) => { + return { + isSuccess: isSuccess, + code: code, + message: message, + status: status, + result: result + } +}; \ No newline at end of file diff --git a/mission10_3/config/response.status.js b/mission10_3/config/response.status.js new file mode 100644 index 00000000..719ec826 --- /dev/null +++ b/mission10_3/config/response.status.js @@ -0,0 +1,28 @@ +import { StatusCodes } from "http-status-codes"; + +export const status = { + // success + SUCCESS: {status: StatusCodes.OK, "isSuccess": true, "code": 2000, "message": "success!"}, + + // error + // common err + INTERNAL_SERVER_ERROR: {status: StatusCodes.INTERNAL_SERVER_ERROR, "isSuccess": false, "code": "COMMON000", "message": "서버 에러, 관리자에게 문의 바랍니다." }, + BAD_REQUEST: {status: StatusCodes.BAD_REQUEST, "isSuccess": false, "code": "COMMON001", "message": "잘못된 요청입니다." }, + UNAUTHORIZED: {status: StatusCodes.UNAUTHORIZED, "isSuccess": false, "code": "COMMON002", "message": "권한이 잘못되었습니다." }, + METHOD_NOT_ALLOWED: {status: StatusCodes.METHOD_NOT_ALLOWED, "isSuccess": false, "code": "COMMON003", "message": "지원하지 않는 Http Method 입니다." }, + FORBIDDEN: {status: StatusCodes.FORBIDDEN, "isSuccess": false, "code": "COMMON004", "message": "금지된 요청입니다." }, + NOT_FOUND: {status: StatusCodes.NOT_FOUND, "isSuccess": false, "code": "COMMON005", "message": "요청한 페이지를 찾을 수 없습니다. 관리자에게 문의 바랍니다." }, + PARAMETER_IS_WRONG : {status : StatusCodes.PARAMETER_IS_WRONG, "isSuccess" : false, "code": "COMMON006", "message":"잘못된 파라미터가 전달되었습니다."}, + + // member err + MEMBER_NOT_FOUND: {status: StatusCodes.BAD_REQUEST, "isSuccess": false, "code": "MEMBER4001", "message": "사용자가 없습니다."}, + NICKNAME_NOT_EXIST: {status: StatusCodes.BAD_REQUEST, "isSuccess": false, "code": "MEMBER4002", "message": "닉네임은 필수입니다."}, + EMAIL_ALREADY_EXIST: {status: StatusCodes.BAD_REQUEST, "isSuccess": false, "code": "MEMBER4003", "message": "이미 가입된 이메일이 존재합니다."}, + + // store err + STORE_ALREADY_EXIST: {status: StatusCodes.BAD_REQUEST, "isSuccess": false, "code": "STORE4001", "message": "이미 추가된 가게입니다."}, + + // article err + ARTICLE_NOT_FOUND: {status: StatusCodes.NOT_FOUND, "isSuccess": false, "code": "ARTICLE4001", "message": "게시글이 없습니다."} + +}; \ No newline at end of file diff --git a/mission10_3/config/swagger.config.js b/mission10_3/config/swagger.config.js new file mode 100644 index 00000000..e3740e35 --- /dev/null +++ b/mission10_3/config/swagger.config.js @@ -0,0 +1,16 @@ +import SwaggerJsdoc from "swagger-jsdoc"; + +const options = { + definition: { + info: { + title: 'UMC Study API', + version: '1.0.0', + description: 'UMC Study API with express, API 설명' + }, + host: 'localhost:3000', + basepath: '../' + }, + apis: ['./src/routes/*.js', './swagger/*'] +}; + +export const specs = SwaggerJsdoc(options); \ No newline at end of file diff --git a/mission10_3/index.js b/mission10_3/index.js new file mode 100644 index 00000000..83693417 --- /dev/null +++ b/mission10_3/index.js @@ -0,0 +1,56 @@ +import express from 'express'; +import { response } from './config/response.js'; +import { BaseError } from './config/error.js'; +import { status } from './config/response.status.js'; +import dotenv from 'dotenv'; +import { specs } from './config/swagger.config.js'; +import SwaggerUi from 'swagger-ui-express'; +import cors from 'cors'; + +import { userRouter } from './src/routes/user.route.js'; +import { storeRouter } from './src/routes/store.route.js'; + + +dotenv.config(); // .env 파일 사용 (환경 변수 관리) + +const app = express(); +const port = 3000; + +// server setting - veiw, static, body-parser etc.. +app.set('port', process.env.PORT || 3000) // 서버 포트 지정 +app.use(cors()); // cors 방식 허용 +app.use(express.static('public')); // 정적 파일 접근 +app.use(express.json()); // request의 본문을 json으로 해석할 수 있도록 함 (JSON 형태의 요청 body를 파싱하기 위함) +app.use(express.urlencoded({extended: false})); // 단순 객체 문자열 형태로 본문 데이터 해석 +app.use(express.urlencoded({extended: false})); // 단순 객체 문자열 형태로 본문 데이터 해석 + +// swagger +app.use('/api-docs', SwaggerUi.serve, SwaggerUi.setup(specs)); + +// router setting +app.use('/user', userRouter); +app.use('/store', storeRouter); +app.use('/:storeId', storeRouter); +app.use('/:userId', userRouter); + + +// error handling +app.use((req, res, next) => { + const err = new BaseError(status.NOT_FOUND); + next(err); +}); + +app.use((err, req, res, next) => { + console.log(err.data.status); + console.log(err.data.message); + // 템플릿 엔진 변수 설정 + res.locals.message = err.data.message; + // 개발환경이면 에러를 출력하고 아니면 출력하지 않기 + res.locals.error = process.env.NODE_ENV !== 'production' ? err : {}; + console.log("error", err); + res.status(err.data.status || status.INTERNAL_SERVER_ERROR).send(response(err.data)); +}); + +app.listen(app.get('port'), () => { + console.log(`Example app listening on port ${app.get('port')}`); +}); \ No newline at end of file diff --git a/mission10_3/src/controllers/user.controller.js b/mission10_3/src/controllers/user.controller.js new file mode 100644 index 00000000..4623dfea --- /dev/null +++ b/mission10_3/src/controllers/user.controller.js @@ -0,0 +1,26 @@ +import { response } from "../../config/response.js"; +import { status } from "../../config/response.status.js"; +import { joinUser, addUserMission } from "../services/user.service.js"; +import { getReview, getMission } from "../providers/user.provider.js"; + +export const userSignin = async (req, res, next) => { + console.log("회원가입을 요청하였습니다!"); + console.log("body:", req.body); // 값이 잘 들어오나 찍어보기 위한 테스트용 + + res.send(response(status.SUCCESS, await joinUser(req.body))); +} + +export const userMission = async (req, res, next) => { + console.log("미션 추가를 요청하였습니다!"); + console.log("body:", req.body); // 값이 잘 들어오나 찍어보기 위한 테스트용 + + res.send(response(status.SUCCESS, await addUserMission(req.body))); +} + +export const reviewPreview = async (req, res, next) => { + return res.send(response(status.SUCCESS, await getReview(req.params.storeId, req.query))); +} + +export const missionPreview = async (req, res, next) => { + return res.send(response(status.SUCCESS, await getMission(req.params.storeId, req.query))); +} \ No newline at end of file diff --git a/mission10_3/src/dtos/user.dto.js b/mission10_3/src/dtos/user.dto.js new file mode 100644 index 00000000..d1f211e9 --- /dev/null +++ b/mission10_3/src/dtos/user.dto.js @@ -0,0 +1,50 @@ +// sign in response DTO +export const signinResponseDTO = (user, prefer) => { + const preferFood = []; + for (let i = 0; i < prefer[0].length; i++) { + preferFood.push(prefer[0][i].f_category_name); + } + return {"email": user[0].email, "name": user[0].user_name, "preferCategory": preferFood}; +} + +// mission response DTO +export const MissionResponseDTO = (mission, user_mission) => { + const userName = []; + const reward = []; + userName.push(user_mission[0][0].user_name); + reward.push(user_mission[0][0].reward); + return {"user": userName, "mission": mission[0].mission_id, "reward": reward, "status":mission[0].status}; +} + +export const previewReviewResponseDTO = (data) => { + + const reviews = []; + + for (let i = 0; i < data.length; i++) { + reviews.push({ + "store_name": data[i].store_name, + "score": data[i].score, + "review_body": data[i].review_content, + "create_date": formatDate(data[i].created_at) + }) + } + return {"reviewData": reviews, "cursorId": data[data.length-1].review_id}; +} + +export const previewMissionResponseDTO = (data) => { + + const missions = []; + + for (let i = 0; i < data.length; i++) { + missions.push({ + "store_name": data[i].store_name, + "reward": data[i].reward, + "mission": data[i].mission_spec + }) + } + return {"missionData": missions, "cursorId": data[data.length-1].mission_id}; +} + +const formatDate = (date) => { + return new Intl.DateTimeFormat('kr').format(new Date(date)).replaceAll(" ", "").slice(0, -1); +} \ No newline at end of file diff --git a/mission10_3/src/models/user.dao.js b/mission10_3/src/models/user.dao.js new file mode 100644 index 00000000..e65623d4 --- /dev/null +++ b/mission10_3/src/models/user.dao.js @@ -0,0 +1,166 @@ +import { pool } from "../../config/db.config.js"; +import { BaseError } from "../../config/error.js"; +import { status } from "../../config/response.status.js"; +import { connectFoodCategory, confirmEmail, getUserID, insertUserSql, getPreferToUserID } from "./user.sql.js"; +import { insertMissionSql, getMissionID, getUserToMissionID } from "./user.sql.js"; +import { getReviewByReviewIdAtFirst, getReviewByReviewId } from "./user.sql.js"; + +// User 데이터 삽입 +export const addUser = async (data) => { + try{ + const conn = await pool.getConnection(); + + const [confirm] = await pool.query(confirmEmail, data.email); + + if(confirm[0].isExistEmail){ + conn.release(); + return -1; + } + + const result = await pool.query(insertUserSql, [data.email, data.name, data.gender, data.birth, data.addr, data.specAddr, data.phone]); + + conn.release(); + return result[0].insertId; + + }catch (err) { + throw new BaseError(status.PARAMETER_IS_WRONG); + } +} + +// 사용자 정보 얻기 +export const getUser = async (userId) => { + try { + const conn = await pool.getConnection(); + const [user] = await pool.query(getUserID, userId); + + console.log(user); + + if(user.length == 0){ + return -1; + } + + conn.release(); + return user; + + } catch (err) { + throw new BaseError(status.PARAMETER_IS_WRONG); + } +} + +// 음식 선호 카테고리 매핑 +export const setPrefer = async (userId, foodCategoryId) => { + try { + const conn = await pool.getConnection(); + + await pool.query(connectFoodCategory, [foodCategoryId, userId]); + + conn.release(); + + return; + } catch (err) { + throw new BaseError(status.PARAMETER_IS_WRONG); + } +} + +// 사용자 선호 카테고리 반환 +export const getUserPreferToUserID = async (userID) => { + try { + const conn = await pool.getConnection(); + const prefer = await pool.query(getPreferToUserID, userID); + + conn.release(); + + return prefer; + } catch (err) { + throw new BaseError(status.PARAMETER_IS_WRONG); + } +} + +// mission 데이터 삽입 +export const addMission = async (data) => { + try{ + const conn = await pool.getConnection(); + + const result = await pool.query(insertMissionSql, [data.user, data.mission, data.status]); + conn.release(); + + return result[0].insertId; + + }catch (err) { + throw new BaseError(status.PARAMETER_IS_WRONG); + } +} + +// mission 정보 얻기 +export const getMission = async (missionId) => { + try { + const conn = await pool.getConnection(); + const [mission] = await pool.query(getMissionID, missionId); + + console.log(mission); + + if(mission.length == 0){ + return -1; + } + + conn.release(); + return mission; + + } catch (err) { + throw new BaseError(status.PARAMETER_IS_WRONG); + } +} + +// user 반환 +export const getUserNameToMissionID = async (missionId) => { + try { + const conn = await pool.getConnection(); + const user_mission = await pool.query(getUserToMissionID, missionId); + console.log("\n 1 \n"); + + conn.release(); + + return user_mission; + } catch (err) { + throw new BaseError(status.PARAMETER_IS_WRONG); + } +} + + +export const getPreviewReview = async (cursorId, size, storeId) => { + try { + const conn = await pool.getConnection(); + + if(cursorId == "undefined" || typeof cursorId == "undefined" || cursorId == null){ + const [reviews] = await pool.query(getReviewByReviewIdAtFirst, [parseInt(storeId), parseInt(size)]); + conn.release(); + return reviews; + + }else{ + const [reviews] = await pool.query(getReviewByReviewId, [parseInt(storeId), parseInt(cursorId), parseInt(size)]); + conn.release(); + return reviews; + } + } catch (err) { + throw new BaseError(status.PARAMETER_IS_WRONG); + } +} + +export const getPreviewMission = async (cursorId, size, userId) => { + try { + const conn = await pool.getConnection(); + + if(cursorId == "undefined" || typeof cursorId == "undefined" || cursorId == null){ + const [missions] = await pool.query(getMissionByMissionIdAtFirst, [parseInt(userId), parseInt(size)]); + conn.release(); + return missions; + + }else{ + const [missions] = await pool.query(getMissionByMissionId, [parseInt(userId), parseInt(cursorId), parseInt(size)]); + conn.release(); + return missions; + } + } catch (err) { + throw new BaseError(status.PARAMETER_IS_WRONG); + } +} \ No newline at end of file diff --git a/mission10_3/src/models/user.sql.js b/mission10_3/src/models/user.sql.js new file mode 100644 index 00000000..0ef4ddcb --- /dev/null +++ b/mission10_3/src/models/user.sql.js @@ -0,0 +1,46 @@ +export const insertUserSql = "INSERT INTO user (email, user_name, gender, birth, user_address, user_spec_address, user_phone) VALUES (?, ?, ?, ?, ?, ?, ?);"; + +export const getUserID = "SELECT * FROM user WHERE user_id = ?"; + +export const connectFoodCategory = "INSERT INTO user_favor_category (f_category_id, user_id) VALUES (?, ?);"; + +export const confirmEmail = "SELECT EXISTS(SELECT 1 FROM user WHERE email = ?) as isExistEmail"; + +export const getPreferToUserID = +"SELECT ufc.uf_category_id, ufc.f_category_id, ufc.user_id, fcl.f_category_name " ++ "FROM user_favor_category ufc JOIN food_category_list fcl on ufc.f_category_id = fcl.f_category_id " ++ "WHERE ufc.user_id = ? ORDER BY ufc.f_category_id ASC;"; + +export const insertMissionSql = "INSERT INTO user_mission (user_id, mission_id, status) VALUES (?, ?, ?);"; + +export const getMissionID = "SELECT * FROM user_mission WHERE id = ?"; + +export const getUserToMissionID = +"SELECT user.user_name, mission.reward " ++ "FROM user_mission um JOIN user on um.user_id = user.user_id " ++ " JOIN mission on um.mission_id = mission.id " ++ "WHERE user_mission.id = ?;"; + +export const getReviewByReviewId = +"SELECT u.user_name, u.user_id, r.review_id, r.rate, r.review_content, r.created_at " ++ "FROM review r JOIN user u on r.user_id = u.user_id " ++ "WHERE r.restaurant_id = ? AND r.review_id < ? " ++ "ORDER BY r.review_id DESC LIMIT ? ;" + +export const getReviewByReviewIdAtFirst = +"SELECT u.user_name, u.user_id, r.review_id, r.rate, r.review_content, r.created_at " ++ "FROM review r JOIN user u on r.user_id = u.user_id " ++ "WHERE r.restaurant_id = ? " ++ "ORDER BY r.review_id DESC LIMIT ? ;" + +export const getMissionByMissionId = +"SELECT u.user_name, u.user_id, m.mission_id, m.reward" ++ "FROM mission m JOIN user u on mission.user_id = u.user_id " ++ "WHERE m.id = ? AND m.id < ? " ++ "ORDER BY m.id DESC LIMIT ? ;" + +export const getMissionByMissionIdAtFirst = +"SELECT u.user_name, u.user_id, m.mission_id, m.reward" ++ "FROM mission m JOIN user u on mission.user_id = u.user_id " ++ "WHERE m.id = ? " ++ "ORDER BY m.id DESC LIMIT ? ;" \ No newline at end of file diff --git a/mission10_3/src/providers/user.provider.js b/mission10_3/src/providers/user.provider.js new file mode 100644 index 00000000..1e9375b9 --- /dev/null +++ b/mission10_3/src/providers/user.provider.js @@ -0,0 +1,12 @@ +import { previewReviewResponseDTO, previewMissionResponseDTO } from "../dtos/user.dto.js" +import { getPreviewReview, getPreviewMission } from "../models/user.dao.js" + +export const getReview = async (storeId, query) => { + const {reviewId, size = 3} = query; + return previewReviewResponseDTO(await getPreviewReview(reviewId, size, userId)); +} + +export const getMission = async (userId, query) => { + const {missionId, size = 3} = query; + return previewMissionResponseDTO(await getPreviewMission(missionId, size, userId)); +} \ No newline at end of file diff --git a/mission10_3/src/routes/user.route.js b/mission10_3/src/routes/user.route.js new file mode 100644 index 00000000..1fbe0f1c --- /dev/null +++ b/mission10_3/src/routes/user.route.js @@ -0,0 +1,10 @@ +import express from "express"; +import asyncHandler from 'express-async-handler'; +import { userSignin, userMission, reviewPreview, missionPreview } from "../controllers/user.controller.js"; + +export const userRouter = express.Router({mergeParams: true}); + +userRouter.post('/signin', asyncHandler(userSignin)); +userRouter.post('/mission', asyncHandler(userMission)); +storeRouter.get('/reviews', asyncHandler(reviewPreview)); +storeRouter.get('/missions', asyncHandler(missionPreview)); \ No newline at end of file diff --git a/mission10_3/src/services/user.service.js b/mission10_3/src/services/user.service.js new file mode 100644 index 00000000..11154f7a --- /dev/null +++ b/mission10_3/src/services/user.service.js @@ -0,0 +1,40 @@ +import { BaseError } from "../../config/error.js"; +import { status } from "../../config/response.status.js"; +import { signinResponseDTO, MissionResponseDTO } from "../dtos/user.dto.js"; +import { addUser, getUser, getUserPreferToUserID, setPrefer } from "../models/user.dao.js"; +import { addMission, getMission, getUserNameToMissionID } from "../models/user.dao.js"; + + +export const joinUser = async (body) => { + const birth = new Date(body.birthYear, body.birthMonth, body.birthDay); + const prefer = body.prefer; + + const joinUserData = await addUser({ + 'email': body.email, + 'name': body.name, + 'gender': body.gender, + 'birth': birth, + 'addr': body.addr, + 'specAddr': body.specAddr, + 'phone': body.phone + }); + + if(joinUserData == -1){ + throw new BaseError(status.EMAIL_ALREADY_EXIST); + }else{ + for (let i = 0; i < prefer.length; i++) { + await setPrefer(joinUserData, prefer[i]); + } + return signinResponseDTO(await getUser(joinUserData), await getUserPreferToUserID(joinUserData)); + } +} + +export const addUserMission = async (body) => { + + const addStoreMissionData = await addMission({ + 'user': body.user, + 'mission': body.mission, + 'status': body.status + }); + return MissionResponseDTO(await getMission(addStoreMissionData), await getUserNameToMissionID(addStoreMissionData)); +} \ No newline at end of file diff --git a/mission10_3/swagger/userMission.yaml b/mission10_3/swagger/userMission.yaml new file mode 100644 index 00000000..2cfbb18b --- /dev/null +++ b/mission10_3/swagger/userMission.yaml @@ -0,0 +1,185 @@ +paths: + /user/mission: + post: + tags: + - UserMission + summary: 미션 추가 + parameters: + - name: addMission + in: body + required: true + schema: + properties: + user: + type: integer + description: 유저 id + example: 1 + mission: + type: integer + description: 미션 id + example: 1 + status: + type: string + description: 미션 상태 + example: 도전 중 + + responses: + '200': + description: 미션 추가 성공! + schema: + type: object + properties: + status: + type: integer + example: 200 + isSuccess: + type: boolean + example: true + code: + type: integer + example: 2000 + message: + type: string + example: "success!" + data: + type: object + example: { + "user": "swagger", + "mission": 1, + "reward": 500, + "status": "도전 중" + } + + '400': + description: 잘못된 요청 + schema: + type: object + properties: + status: + type: integer + example: 400 + isSuccess: + type: boolean + example: false + code: + type: integer + example: COMMON001 + message: + type: string + example: 잘못된 요청입니다 + + '500': + description: 서버 에러 + schema: + type: object + properties: + status: + type: integer + example: 500 + isSuccess: + type: boolean + example: false + code: + type: integer + example: COMMON000 + message: + type: string + example: 서버 에러, 관리자에게 문의 바랍니다. + + paths: + /{userId}/missions: + get: + tags: + - User + summary: 유저 별 미션 조회 + parameters: + - name: userId + in: path + schema: + type: integer + required: true + - name: missionId + in: query + required: false + schema: + properties: + reviewId: + type: integer + - name: paging + in: query + required: true + schema: + properties: + size: + type: integer + responses: + '200': + description: 미션 조회 성공 + schema: + type: object + properties: + status: + type: integer + example: 200 + isSuccess: + type: boolean + example: true + code: + type: integer + example: 2000 + message: + type: string + example: "success!" + data: + type: array + example: { + "missionData": [ + { + "store_name": "가게1", + "reward": 500, + "mission": "미션1" + }, + { + "store_name": "가게2", + "reward": 500, + "mission": "미션2" + } + ], + "cursorId": 1 + } + + '400': + description: 잘못된 요청 + schema: + type: object + properties: + status: + type: integer + example: 400 + isSuccess: + type: boolean + example: false + code: + type: integer + example: COMMON001 + message: + type: string + example: 잘못된 요청입니다 + + '500': + description: 서버 에러 + schema: + type: object + properties: + status: + type: integer + example: 500 + isSuccess: + type: boolean + example: false + code: + type: integer + example: COMMON000 + message: + type: string + example: 서버 에러, 관리자에게 문의 바랍니다. \ No newline at end of file