diff --git a/.github/workflows/deploy-main.yml b/.github/workflows/deploy-main.yml new file mode 100644 index 0000000..c985f73 --- /dev/null +++ b/.github/workflows/deploy-main.yml @@ -0,0 +1,81 @@ +name: deploy-main + +on: + push: + branches: + - feature/mission-10/조우 + workflow_dispatch: + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - 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 + echo "$EC2_SSH_KEY" > ~/.ssh/id_rsa + chmod 600 ~/.ssh/id_rsa + + # 접속 별명 설정 + cat >>~/.ssh/config <=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..e6d9f61 --- /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