From 099c206a25dc4dbf19e1795639cf0c6504d4959f Mon Sep 17 00:00:00 2001 From: suyeon0421 <157108684+suyeon0421@users.noreply.github.com> Date: Thu, 27 Nov 2025 17:04:43 +0900 Subject: [PATCH 1/7] =?UTF-8?q?=EB=AF=B8=EC=85=98=2009=20jwt,=20google=20?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=9D=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 219 ++++++++++++++++++ package.json | 4 + .../migration.sql | 2 + prisma/schema.prisma | 2 +- src/auth.config.js | 105 +++++++++ src/controllers/restaurant.controller.js | 6 +- src/controllers/review.controller.js | 3 +- src/controllers/user.controller.js | 19 +- src/controllers/user_mission.controller.js | 4 +- src/dtos/user.dto.js | 1 + src/index.js | 56 ++++- src/repositories/user.repository.js | 51 +++- src/services/restaurant.service.js | 6 +- src/services/user.service.js | 29 ++- 14 files changed, 487 insertions(+), 20 deletions(-) create mode 100644 prisma/migrations/20251127064415_add_optional_password/migration.sql create mode 100644 src/auth.config.js diff --git a/package-lock.json b/package-lock.json index 8229d1f..a41df57 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,8 +13,12 @@ "dotenv": "^17.2.3", "express": "^5.1.0", "http-status-codes": "^2.3.0", + "jsonwebtoken": "^9.0.2", "morgan": "^1.10.1", "mysql2": "^3.15.3", + "passport": "^0.7.0", + "passport-google-oauth20": "^2.0.0", + "passport-jwt": "^4.0.1", "prisma": "^6.18.0", "swagger-autogen": "^2.23.7", "swagger-ui-express": "^5.0.1" @@ -152,6 +156,15 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "license": "MIT" }, + "node_modules/base64url": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz", + "integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/basic-auth": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", @@ -226,6 +239,12 @@ "concat-map": "0.0.1" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -533,6 +552,15 @@ "node": ">= 0.4" } }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -958,12 +986,97 @@ "node": ">=6" } }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "license": "MIT", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jwa": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz", + "integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "license": "MIT", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "license": "MIT" }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, "node_modules/long": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", @@ -1192,6 +1305,12 @@ "node": "^14.16.0 || >=16.10.0" } }, + "node_modules/oauth": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.10.2.tgz", + "integrity": "sha512-JtFnB+8nxDEXgNyniwz573xxbKSOu3R8D40xQKqcjwJ2CDkYqUDI53o6IuzDJBx60Z8VKCm271+t8iFjakrl8Q==", + "license": "MIT" + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -1258,6 +1377,74 @@ "node": ">= 0.8" } }, + "node_modules/passport": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/passport/-/passport-0.7.0.tgz", + "integrity": "sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==", + "license": "MIT", + "dependencies": { + "passport-strategy": "1.x.x", + "pause": "0.0.1", + "utils-merge": "^1.0.1" + }, + "engines": { + "node": ">= 0.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jaredhanson" + } + }, + "node_modules/passport-google-oauth20": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/passport-google-oauth20/-/passport-google-oauth20-2.0.0.tgz", + "integrity": "sha512-KSk6IJ15RoxuGq7D1UKK/8qKhNfzbLeLrG3gkLZ7p4A6DBCcv7xpyQwuXtWdpyR0+E0mwkpjY1VfPOhxQrKzdQ==", + "license": "MIT", + "dependencies": { + "passport-oauth2": "1.x.x" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/passport-jwt": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/passport-jwt/-/passport-jwt-4.0.1.tgz", + "integrity": "sha512-UCKMDYhNuGOBE9/9Ycuoyh7vP6jpeTp/+sfMJl7nLff/t6dps+iaeE0hhNkKN8/HZHcJ7lCdOyDxHdDoxoSvdQ==", + "license": "MIT", + "dependencies": { + "jsonwebtoken": "^9.0.0", + "passport-strategy": "^1.0.0" + } + }, + "node_modules/passport-oauth2": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.8.0.tgz", + "integrity": "sha512-cjsQbOrXIDE4P8nNb3FQRCCmJJ/utnFKEz2NX209f7KOHPoX18gF7gBzBbLLsj2/je4KrgiwLLGjf0lm9rtTBA==", + "license": "MIT", + "dependencies": { + "base64url": "3.x.x", + "oauth": "0.10.x", + "passport-strategy": "1.x.x", + "uid2": "0.0.x", + "utils-merge": "1.x.x" + }, + "engines": { + "node": ">= 0.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jaredhanson" + } + }, + "node_modules/passport-strategy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", + "integrity": "sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==", + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", @@ -1283,6 +1470,11 @@ "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", "license": "MIT" }, + "node_modules/pause": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", + "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==" + }, "node_modules/perfect-debounce": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", @@ -1458,6 +1650,18 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/send": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", @@ -1661,6 +1865,12 @@ "node": ">= 0.6" } }, + "node_modules/uid2": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.4.tgz", + "integrity": "sha512-IevTus0SbGwQzYh3+fRsAMTVVPOoIVufzacXcHPmdlle1jUpq7BRL+mw3dgeLanvGZdwwbWhRV6XrcFNdBmjWA==", + "license": "MIT" + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -1670,6 +1880,15 @@ "node": ">= 0.8" } }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", diff --git a/package.json b/package.json index 891340c..afd79a6 100644 --- a/package.json +++ b/package.json @@ -9,8 +9,12 @@ "dotenv": "^17.2.3", "express": "^5.1.0", "http-status-codes": "^2.3.0", + "jsonwebtoken": "^9.0.2", "morgan": "^1.10.1", "mysql2": "^3.15.3", + "passport": "^0.7.0", + "passport-google-oauth20": "^2.0.0", + "passport-jwt": "^4.0.1", "prisma": "^6.18.0", "swagger-autogen": "^2.23.7", "swagger-ui-express": "^5.0.1" diff --git a/prisma/migrations/20251127064415_add_optional_password/migration.sql b/prisma/migrations/20251127064415_add_optional_password/migration.sql new file mode 100644 index 0000000..e4426f6 --- /dev/null +++ b/prisma/migrations/20251127064415_add_optional_password/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE `user` MODIFY `password` VARCHAR(100) NULL; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index c720475..2110905 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -16,7 +16,7 @@ datasource db { model User { id Int @id @default(autoincrement()) email String @unique(map: "email") @db.VarChar(255) - password String @db.VarChar(100) + password String? @db.VarChar(100) name String @db.VarChar(100) gender String @db.VarChar(15) birth DateTime @db.Date diff --git a/src/auth.config.js b/src/auth.config.js new file mode 100644 index 0000000..3ea9fc4 --- /dev/null +++ b/src/auth.config.js @@ -0,0 +1,105 @@ +import dotenv from "dotenv"; +import { Strategy as GoogleStrategy } from "passport-google-oauth20"; +import { Strategy as JwtStrategy, ExtractJwt } from 'passport-jwt'; +import { prisma } from "./db.config.js"; +import jwt from "jsonwebtoken"; // JWT 생성을 위해 import + +dotenv.config(); +const secret = process.env.JWT_SECRET; // .env의 비밀 키 +console.log('JWT SECRET:', secret); + +export const generateAccessToken = (user) => { + return jwt.sign( + { id: user.id, email: user.email }, + secret, + { expiresIn: '1h' } + ); +}; + +export const generateRefreshToken = (user) => { + return jwt.sign( + { id: user.id }, + secret, + { expiresIn: '14d' } + ); +}; + +// GoogleVerify +const googleVerify = async (profile) => { + const email = profile.emails?.[0]?.value; + if (!email) { + throw new Error(`profile.email was not found: ${profile}`); + } + + const user = await prisma.user.findFirst({ where: { email } }); + if (user !== null) { + return { id: user.id, email: user.email, name: user.name }; + } + + const created = await prisma.user.create({ + data: { + email, + name: profile.displayName, + gender: "추후 수정", + birth: new Date(1970, 0, 1), + address: "추후 수정", + detailAddress: "추후 수정", + phoneNumber: "추후 수정", + }, + }); + + return { id: created.id, email: created.email, name: created.name }; +}; + +// GoogleStrategy + +export const googleStrategy = new GoogleStrategy( + { + clientID: process.env.PASSPORT_GOOGLE_CLIENT_ID, + clientSecret: process.env.PASSPORT_GOOGLE_CLIENT_SECRET, + callbackURL: "/oauth2/callback/google", + scope: ["email", "profile"], + }, + + + async (accessToken, refreshToken, profile, cb) => { + try { + + const user = await googleVerify(profile); + + + const jwtAccessToken = generateAccessToken(user); + const jwtRefreshToken = generateRefreshToken(user); + + + + return cb(null, { + accessToken: jwtAccessToken, + refreshToken: jwtRefreshToken, + }); + + } catch (err) { + return cb(err); + } + } +); + +const jwtOptions = { + // 요청 헤더의 'Authorization'에서 'Bearer ' 토큰을 추출 + jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), + secretOrKey: process.env.JWT_SECRET, +}; + +export const jwtStrategy = new JwtStrategy(jwtOptions, async (payload, done) => { + try { + const user = await prisma.user.findFirst({ where: { id: payload.id } }); + + if (user) { + return done(null, user); + } else { + return done(null, false); + } + } catch (err) { + return done(err, false); + } +}); \ No newline at end of file diff --git a/src/controllers/restaurant.controller.js b/src/controllers/restaurant.controller.js index 2d3ca03..e31bd9b 100644 --- a/src/controllers/restaurant.controller.js +++ b/src/controllers/restaurant.controller.js @@ -75,7 +75,7 @@ export const regionForRestaurant = async (req, res, next) => { try { console.log("body:", req.body); const restaurant = await restaurantAdd(req.body); - res.status(StatusCodes.CREATED(201)).success(restaurant); + res.status(StatusCodes.CREATED).success(restaurant); } catch (error) { next(error); } @@ -123,7 +123,7 @@ export const handleListRestaurantReviews = async (req, res, next) => { parseInt(req.params.restaurant_id), typeof req.query.cursor === "string" ? parseInt(req.query.cursor) : 0 ); - res.status(StatusCodes.CREATED(201)).success(reviews); + res.status(StatusCodes.CREATED).success(reviews); } catch (error) { next(error); } @@ -209,7 +209,7 @@ export const getMissionsByRestaurantController = async (req, res, next) => { Number(limit) || 5 ); - res.status(StatusCodes.CREATED(201)).success(result); + res.status(StatusCodes.CREATED).success(result); } catch (error) { next(error); } diff --git a/src/controllers/review.controller.js b/src/controllers/review.controller.js index fae4fb1..eaa6bd9 100644 --- a/src/controllers/review.controller.js +++ b/src/controllers/review.controller.js @@ -67,10 +67,11 @@ export const addReviewController = async (req, res, next) => { const review = await reviewAdd( Number(user_id), Number(mission_id), + Number(restaurant_id), req.body // content, rating, photo 등이 담긴 객체 ); - res.status(StatusCodes.CREATED(201)).success(review); + res.status(StatusCodes.CREATED).success(review); } catch (error) { next(error); } diff --git a/src/controllers/user.controller.js b/src/controllers/user.controller.js index 9ef4f24..3a4e5fa 100644 --- a/src/controllers/user.controller.js +++ b/src/controllers/user.controller.js @@ -1,6 +1,6 @@ import { StatusCodes } from "http-status-codes"; import { bodyToUser } from "../dtos/user.dto.js"; -import { userSignUp } from "../services/user.service.js"; +import { userSignUp, userUpdateInfo } from "../services/user.service.js"; export const handleUserSignUp = async (req, res, next) => { /* @@ -73,12 +73,27 @@ export const handleUserSignUp = async (req, res, next) => { console.log("회원가입을 요청했습니다!"); console.log("body:", req.body); // 값이 잘 들어오나 확인하기 위한 테스트용 + try { const user = await userSignUp(bodyToUser(req.body)); - res.status(StatusCodes.CREATED(201)).success(user); + res.status(StatusCodes.CREATED).success(user); } catch (error) { next(error); } +}; + +export const handleUserUpdateInfo = async (req, res, next) => { +  const userId = req.user.id; +  +  const updateData = req.body; + +  try { +    const updatedUser = await userUpdateInfo(userId, updateData); + +    res.status(StatusCodes.OK).success(updatedUser); +  } catch (error) { +    next(error); +  } }; \ No newline at end of file diff --git a/src/controllers/user_mission.controller.js b/src/controllers/user_mission.controller.js index a95fef9..73f28dc 100644 --- a/src/controllers/user_mission.controller.js +++ b/src/controllers/user_mission.controller.js @@ -66,7 +66,7 @@ export const startMissionController = async (req, res, next) => { const userMission = await startMission(user_id, missionIdAsNumber); - res.status(StatusCodes.CREATED(201)).success(userMission); + res.status(StatusCodes.CREATED).success(userMission); } catch (error) { next(error); // 에러 핸들러로 넘김 } @@ -166,7 +166,7 @@ export const handleOngoingMissions = async (req, res, next) => { Number(limit) || 5 ); - res.status(StatusCodes.CREATED(201)).success(result); + res.status(StatusCodes.CREATED).success(result); } catch (error) { next(error); } diff --git a/src/dtos/user.dto.js b/src/dtos/user.dto.js index f21a0e2..9b99b09 100644 --- a/src/dtos/user.dto.js +++ b/src/dtos/user.dto.js @@ -11,6 +11,7 @@ export const bodyToUser = (body) => { detailAddress: body.detailAddress || "", //선택 phoneNumber: body.phoneNumber,//필수 preferences: body.preferences,// 필수 + token: token, }; }; diff --git a/src/index.js b/src/index.js index ae3cf49..582d56d 100644 --- a/src/index.js +++ b/src/index.js @@ -5,14 +5,20 @@ import cookieParser from 'cookie-parser'; import morgan from "morgan"; import swaggerAutogen from "swagger-autogen"; import swaggerUiExpress from "swagger-ui-express"; +import passport from "passport"; +import { googleStrategy, jwtStrategy } from "./auth.config.js"; +import { prisma } from "./db.config.js"; import { addMissionController } from "./controllers/mission.controller.js"; -import { handleUserSignUp } from "./controllers/user.controller.js"; +import { handleUserSignUp, handleUserUpdateInfo } from "./controllers/user.controller.js"; import { regionForRestaurant, handleListRestaurantReviews, getMissionsByRestaurantController } from "./controllers/restaurant.controller.js"; import { addReviewController, handleUserReviewList } from "./controllers/review.controller.js"; import { startMissionController, handleOngoingMissions } from "./controllers/user_mission.controller.js"; dotenv.config(); +passport.use(googleStrategy); +passport.use(jwtStrategy); + const app = express(); const port = process.env.PORT; @@ -24,6 +30,8 @@ app.use(express.urlencoded({ extended: false })); // 단순 객체 문자열 형 app.use(express.static("public")); // 정적 파일 접근 +app.use(passport.initialize()); + /** * 공통 응답을 사용할 수 있는 헬퍼 함수 등록 */ @@ -53,6 +61,31 @@ app.use( }) ); +app.get("/oauth2/login/google", + passport.authenticate("google", { + session: false + }) +); +app.get( + "/oauth2/callback/google", + passport.authenticate("google", { + session: false, + failureRedirect: "/login-failed", + }), + (req, res) => { + const tokens = req.user; + + res.status(200).json({ + resultType: "SUCCESS", + error: null, + success: { + message: "Google 로그인 성공!", + tokens: tokens, // { "accessToken": "...", "refreshToken": "..." } + } + }); + } +); + app.get("/openapi.json", async (req, res, next) => { // #swagger.ignore = true const options = { @@ -111,19 +144,32 @@ app.get('/getcookie', (req, res) => { } }); +const isLogin = passport.authenticate('jwt', { session: false }); + + app.post("/api/users/signup", handleUserSignUp); app.post("/api/restaurants", regionForRestaurant); app.post("/api/restaurants/:restaurant_id/missions", addMissionController); -app.post("/api/restaurants/:restaurant_id/reviews", addReviewController); +app.post("/api/restaurants/:restaurant_id/reviews", isLogin, addReviewController); app.post( - "/api/missions/:mission_id/start", + "/api/missions/:mission_id/start", isLogin, startMissionController ); + +app.get('/mypage', isLogin, (req, res) => { + res.status(200).success({ + message: `인증 성공! ${req.user.name}님의 마이페이지입니다.`, + user: req.user, + }); +}); + app.get("/api/restaurants/:restaurant_id/reviews", handleListRestaurantReviews); -app.get("/api/users/:user_id/reviews", handleUserReviewList); +app.get("/api/users/:user_id/reviews", isLogin, handleUserReviewList); app.get("/api/restaurants/:restaurant_id/missions", getMissionsByRestaurantController); -app.get("/api/users/:user_id/ongoing-missions", handleOngoingMissions); +app.get("/api/users/:user_id/ongoing-missions", isLogin, handleOngoingMissions); +app.patch("/api/users/me", isLogin, handleUserUpdateInfo); + /** * 전역 오류를 처리하기 위한 미들웨어 diff --git a/src/repositories/user.repository.js b/src/repositories/user.repository.js index f790a6d..5471af2 100644 --- a/src/repositories/user.repository.js +++ b/src/repositories/user.repository.js @@ -39,14 +39,17 @@ export const getUserPreferencesByUserId = async (userId) => { }, where: { userId: userId }, orderBy: { - foodCategory: "asc" - }, + foodCategory: { + name: "asc" // foodCategory 관계를 통해 name 필드를 기준으로 오름차순 정렬 + } + } }); return preferences; }; export const responseFromUser = (user, preferences) => ({ id:user.id, + token: token, email: user.email, name: user.name, address: user.address, @@ -57,3 +60,47 @@ export const responseFromUser = (user, preferences) => ({ name: pref.name })) }); + +export const updateUser = async (userId, updateData) => { + try { + const updatedUser = await prisma.user.update({ + where: { id: userId }, + data: updateData, + select: { + id: true, + email: true, + name: true, + gender: true, + birth: true, + address: true, + detailAddress: true, + phoneNumber: true, + }, + }); + return updatedUser; + } catch (error) { + return null; + } +}; + +export const updateUserPreferences = async (userId, foodCategoryIds) => { + await prisma.$transaction(async (tx) => { + // 기존 선호 카테고리 전체 삭제 + await tx.userFavorCategory.deleteMany({ + where: { userId: userId }, + }); + + // 새로운 카테고리 레코드 준비 및 생성 + if (foodCategoryIds && foodCategoryIds.length > 0) { + const preferenceRecords = foodCategoryIds.map(foodCategoryId => ({ + userId: userId, + foodCategoryId: foodCategoryId + })); + + await tx.userFavorCategory.createMany({ + data: preferenceRecords, + skipDuplicates: true, + }); + } + }); +}; diff --git a/src/services/restaurant.service.js b/src/services/restaurant.service.js index 28e1d46..0705fa9 100644 --- a/src/services/restaurant.service.js +++ b/src/services/restaurant.service.js @@ -17,10 +17,10 @@ export const restaurantAdd = async (body) => { //특정 레스토랑의 리뷰 조회 export const listRestaurantReviews = async (restaurant_id, cursor) => { const { reviews, nextCursor } = await getAllRestaurantReviews(restaurant_id, cursor); + const missionsDto = result.missions.map((m) => responseFromMission(m)); return { - reviews: reviews.map(responseFromReview), - nextCursor: nextCursor - }; + missions: missionsDto, + }; }; //특정 레스토링 미션 조회 diff --git a/src/services/user.service.js b/src/services/user.service.js index af635be..6c3094a 100644 --- a/src/services/user.service.js +++ b/src/services/user.service.js @@ -1,14 +1,18 @@ import { responseFromUser } from "../dtos/user.dto.js"; import { DuplicateUserEmailError } from "../errors.js"; +import jwt from "jsonwebtoken"; import bcrypt from 'bcrypt'; import { addUser, getUser, + updateUser, + updateUserPreferences, getUserPreferencesByUserId, setPreference, } from "../repositories/user.repository.js"; +const JWT_SECRET = process.env.JWT_SECRET; export const userSignUp = async (data) => { const hashedPassword = await bcrypt.hash(data.password, 10); // 10은 salt rounds @@ -36,5 +40,28 @@ export const userSignUp = async (data) => { const user = await getUser(joinUserId); const preferences = await getUserPreferencesByUserId(joinUserId); - return responseFromUser({ user, preferences }); + const accessToken = jwt.sign( + { id: user.id, email: user.email }, // Payload: 사용자 ID와 이메일 + JWT_SECRET, + { expiresIn: '1h' } // 만료 시간 1시간 설정 + ); + + return responseFromUser({ user, preferences, token: accessToken }); +}; + +export const userUpdateInfo = async (userId, data) => { + const { preferences, ...updateData } = data; + const updatedUser = await updateUser(userId, updateData); + + if (!updatedUser) { + throw new Error("사용자를 찾을 수 없거나 업데이트할 데이터가 유효하지 않습니다."); + } + + //선호 카테고리 갱신하기 + if (preferences && preferences.length > 0) { + await updateUserPreferences(userId, preferences) + } + + const finalPreferences = await getUserPreferencesByUserId(userId); + return responseFromUser({ user: updatedUser, preferences: finalPreferences }); }; \ No newline at end of file From 6b10ec3a4afdc95e79b869ae8a8588ed5312540d Mon Sep 17 00:00:00 2001 From: suyeon0421 <157108684+suyeon0421@users.noreply.github.com> Date: Sun, 30 Nov 2025 20:55:28 +0900 Subject: [PATCH 2/7] =?UTF-8?q?=ED=8C=8C=EC=9D=B4=ED=94=84=EB=9D=BC?= =?UTF-8?q?=EC=9D=B8=20=EA=B5=AC=EC=B6=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy-main.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 .github/workflows/deploy-main.yml diff --git a/.github/workflows/deploy-main.yml b/.github/workflows/deploy-main.yml new file mode 100644 index 0000000..812e56e --- /dev/null +++ b/.github/workflows/deploy-main.yml @@ -0,0 +1,13 @@ +name: deploy-main # 파이프라인 이름은 자유롭게 지어주세요 + +on: + push: + branches: + - 조우/main # main 브랜치에 새로운 커밋이 올라왔을 떄 실행되도록 합니다 + workflow_dispatch: # 필요한 경우 수동으로 실행할 수도 있도록 합니다 + +jobs: + deploy: + runs-on: ubuntu-latest # CI/CD 파이프라인이 실행될 운영체제 환경을 지정합니다 + steps: + - TODO \ No newline at end of file From c2e146ac7c8785c22781c15c5d0607c676c448b9 Mon Sep 17 00:00:00 2001 From: suyeon0421 <157108684+suyeon0421@users.noreply.github.com> Date: Sun, 30 Nov 2025 23:03:32 +0900 Subject: [PATCH 3/7] =?UTF-8?q?=EC=9E=90=EB=8F=99=20=EC=BB=A4=EB=B0=8B=20?= =?UTF-8?q?=ED=8C=8C=EC=9D=B4=ED=94=84=EB=9D=BC=EC=9D=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy-main.yml | 65 ++++++++++++++++++++++++++++--- src/auth.config.js | 8 ++-- 2 files changed, 64 insertions(+), 9 deletions(-) diff --git a/.github/workflows/deploy-main.yml b/.github/workflows/deploy-main.yml index 812e56e..8716e94 100644 --- a/.github/workflows/deploy-main.yml +++ b/.github/workflows/deploy-main.yml @@ -1,13 +1,68 @@ -name: deploy-main # 파이프라인 이름은 자유롭게 지어주세요 +name: deploy-main on: push: branches: - - 조우/main # main 브랜치에 새로운 커밋이 올라왔을 떄 실행되도록 합니다 - workflow_dispatch: # 필요한 경우 수동으로 실행할 수도 있도록 합니다 + - 조우/main + workflow_dispatch: jobs: deploy: - runs-on: ubuntu-latest # CI/CD 파이프라인이 실행될 운영체제 환경을 지정합니다 + runs-on: ubuntu-latest steps: - - TODO \ No newline at end of file + - name: Checkout + uses: actions/checkout@v4 + + - name: Configure SSH + run: | + mkdir -p ~/.ssh + echo "$EC2_SSH_KEY" > ~/.ssh/id_rsa + chmod 600 ~/.ssh/id_rsa + + # 접속 별명 설정 + cat >>~/.ssh/config < { data: { email, name: profile.displayName, - gender: "추후 수정", + gender: "", birth: new Date(1970, 0, 1), - address: "추후 수정", - detailAddress: "추후 수정", - phoneNumber: "추후 수정", + address: "", + detailAddress: "", + phoneNumber: "", }, }); From de083cab88d4d7ddf66e1d495824ffcb72c38a0b Mon Sep 17 00:00:00 2001 From: suyeon0421 <157108684+suyeon0421@users.noreply.github.com> Date: Thu, 4 Dec 2025 15:02:37 +0900 Subject: [PATCH 4/7] =?UTF-8?q?=EB=B8=8C=EB=9E=9C=EC=B9=98=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy-main.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deploy-main.yml b/.github/workflows/deploy-main.yml index 8716e94..901f9d0 100644 --- a/.github/workflows/deploy-main.yml +++ b/.github/workflows/deploy-main.yml @@ -1,9 +1,9 @@ -name: deploy-main +name: deploy-main-zhou on: push: branches: - - 조우/main + - feature/mission-10/조우 workflow_dispatch: jobs: From 0d209b6d3a449f739e930dac50b6030b5e09c273 Mon Sep 17 00:00:00 2001 From: suyeon0421 <157108684+suyeon0421@users.noreply.github.com> Date: Thu, 4 Dec 2025 15:04:36 +0900 Subject: [PATCH 5/7] =?UTF-8?q?.yml=20=ED=8C=8C=EC=9D=BC=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy-main.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/deploy-main.yml b/.github/workflows/deploy-main.yml index 901f9d0..5431b9a 100644 --- a/.github/workflows/deploy-main.yml +++ b/.github/workflows/deploy-main.yml @@ -32,6 +32,14 @@ jobs: EC2_HOST: ${{ secrets.EC2_HOST }} EC2_SSH_KEY: ${{ secrets.EC2_SSH_KEY }} + - name: SSH Connection Test + run: | + ssh -vvv umc9thworkbook exit + env: + EC2_USER: ubuntu + EC2_HOST: ${{ secrets.EC2_HOST }} + EC2_SSH_KEY: ${{ secrets.EC2_SSH_KEY }} + - name: Copy Workspace run: | ssh umc9thworkbook 'sudo mkdir -p /opt/app' From 5c115a1e05a0dd9adaa54eec862649e37e468ae2 Mon Sep 17 00:00:00 2001 From: suyeon0421 <157108684+suyeon0421@users.noreply.github.com> Date: Thu, 4 Dec 2025 15:07:20 +0900 Subject: [PATCH 6/7] =?UTF-8?q?.yml=20=ED=8C=8C=EC=9D=BC=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy-main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy-main.yml b/.github/workflows/deploy-main.yml index 5431b9a..0cd7c2e 100644 --- a/.github/workflows/deploy-main.yml +++ b/.github/workflows/deploy-main.yml @@ -1,4 +1,4 @@ -name: deploy-main-zhou +name: deploy-main on: push: From 8ee2bfe36e4aa5ecac5ef33acd8484436fc38770 Mon Sep 17 00:00:00 2001 From: suyeon0421 <157108684+suyeon0421@users.noreply.github.com> Date: Thu, 4 Dec 2025 15:29:24 +0900 Subject: [PATCH 7/7] =?UTF-8?q?=ED=8C=8C=EC=9D=B4=ED=94=84=EB=9D=BC?= =?UTF-8?q?=EC=9D=B8=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy-main.yml | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/.github/workflows/deploy-main.yml b/.github/workflows/deploy-main.yml index 0cd7c2e..c985f73 100644 --- a/.github/workflows/deploy-main.yml +++ b/.github/workflows/deploy-main.yml @@ -13,6 +13,13 @@ jobs: - name: Checkout uses: actions/checkout@v4 + - name: Check prisma has changes + uses: dorny/paths-filter@v3 + id: paths-filter + with: + filters: | + prisma: ["prisma/**"] + - name: Configure SSH run: | mkdir -p ~/.ssh @@ -32,14 +39,6 @@ jobs: EC2_HOST: ${{ secrets.EC2_HOST }} EC2_SSH_KEY: ${{ secrets.EC2_SSH_KEY }} - - name: SSH Connection Test - run: | - ssh -vvv umc9thworkbook exit - env: - EC2_USER: ubuntu - EC2_HOST: ${{ secrets.EC2_HOST }} - EC2_SSH_KEY: ${{ secrets.EC2_SSH_KEY }} - - name: Copy Workspace run: | ssh umc9thworkbook 'sudo mkdir -p /opt/app' @@ -49,6 +48,12 @@ jobs: - name: Install dependencies run: | ssh umc9thworkbook 'npm install --prefix /opt/app/' + ssh umc9thworkbook 'cd /opt/app; npx prisma generate' + + - name: Apply prisma migrations + if: steps.paths-filter.outputs.prisma == 'true' + run: | + ssh umc9thworkbook 'cd /opt/app; npx prisma migrate deploy' - name: Copy systemd service file run: | @@ -59,7 +64,7 @@ jobs: [Service] User=ubuntu - ExecStart=/usr/bin/npm run dev --prefix /opt/app/ + ExecStart=/usr/bin/npm run start --prefix /opt/app/ Restart=always [Install]