From d697c910ee251a53b7a9275911c2a5bb215ae302 Mon Sep 17 00:00:00 2001 From: jeongkyueun Date: Thu, 27 Nov 2025 03:16:13 +0900 Subject: [PATCH 01/10] =?UTF-8?q?refactor:=208=EC=A3=BC=EC=B0=A8=20?= =?UTF-8?q?=EB=AF=B8=EC=85=98=20=ED=94=BC=EB=93=9C=EB=B0=B1=20=EB=B0=98?= =?UTF-8?q?=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/controllers/mission.controller.js | 2 +- src/controllers/store.controller.js | 27 +++++---------- src/controllers/user.controller.js | 48 +++++++++++++++++++++++++++ src/dtos/user.dto.js | 3 -- src/index.js | 28 ++++------------ 5 files changed, 63 insertions(+), 45 deletions(-) diff --git a/src/controllers/mission.controller.js b/src/controllers/mission.controller.js index 8f146b9..b084100 100644 --- a/src/controllers/mission.controller.js +++ b/src/controllers/mission.controller.js @@ -962,7 +962,7 @@ export const getUserReviews = async (req, res, next) => { * type: string * example: "미션 목록 조회 중 오류가 발생했습니다." */ -export const getStoreMissions = async (req, res, next) => { +export const getMissionsByStore = async (req, res, next) => { try { const storeId = parseInt(req.params.storeId); const missions = await missionService.getMissionsByStoreId(storeId); diff --git a/src/controllers/store.controller.js b/src/controllers/store.controller.js index e2a7343..8c79b53 100644 --- a/src/controllers/store.controller.js +++ b/src/controllers/store.controller.js @@ -76,16 +76,10 @@ export const handleAddStore = async (req, res) => { try { const result = await storeService.addNewStore({ name, address, region }); - return res.status(201).json(result); // 201 Created + return res.success(result, '가게가 성공적으로 등록되었습니다.', 201); } catch (error) { - if (error instanceof NotFoundError) { - throw error; - } - console.error('가게 추가 중 오류 발생:', error); - return res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ - message: '가게 추가 중 오류가 발생했습니다.', - error: process.env.NODE_ENV === 'development' ? error.message : undefined - }); + // 에러를 next로 전달하여 전역 에러 핸들러에서 처리하도록 함 + next(error); } }; @@ -289,19 +283,14 @@ export const getStoreById = async (req, res, next) => { try { const store = await getStoreByIdService(req.params.storeId); if (!store) { - return res.status(StatusCodes.NOT_FOUND).json({ - success: false, - message: '가게를 찾을 수 없습니다.' - }); + const error = new Error('가게를 찾을 수 없습니다.'); + error.statusCode = StatusCodes.NOT_FOUND; + throw error; } - res.status(StatusCodes.OK).json({ - success: true, - data: store - }); + return res.success(store); } catch (error) { - console.error('Error listing store reviews:', error); - res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ message: '가게 리뷰 조회 중 오류가 발생했습니다.' }); + next(error); } }; diff --git a/src/controllers/user.controller.js b/src/controllers/user.controller.js index a63fb20..47376ae 100644 --- a/src/controllers/user.controller.js +++ b/src/controllers/user.controller.js @@ -6,6 +6,54 @@ import { prisma } from '../db.config.js'; /** * @swagger + * components: + * schemas: + * User: + * type: object + * properties: + * id: + * type: integer + * description: 사용자 고유 ID + * example: 1 + * email: + * type: string + * format: email + * description: 사용자 이메일 + * example: "user@example.com" + * name: + * type: string + * description: 사용자 이름 + * example: "홍길동" + * gender: + * type: string + * enum: [MALE, FEMALE, OTHER] + * description: 성별 + * example: "MALE" + * birth: + * type: string + * format: date + * description: 생년월일 (YYYY-MM-DD) + * example: "1990-01-01" + * address: + * type: string + * description: 기본 주소 + * example: "서울특별시 강남구 테헤란로 123" + * detailAddress: + * type: string + * description: 상세 주소 + * example: "101동 101호" + * phoneNumber: + * type: string + * nullable: true + * description: 휴대폰 번호 ('-' 제외) + * example: "01012345678" + * preferences: + * type: array + * description: 선호 카테고리 목록 + * items: + * type: string + * example: ["한식", "중식"] + * * /api/users/signup: * post: * summary: 사용자 회원가입 diff --git a/src/dtos/user.dto.js b/src/dtos/user.dto.js index 581e13c..8cfd6b2 100644 --- a/src/dtos/user.dto.js +++ b/src/dtos/user.dto.js @@ -45,9 +45,6 @@ export const bodyToUser = (body) => { - - - export const responseFromUser = ({ user, preferences = [] }) => { const preferFoods = preferences.length > 0 ? preferences.map((preference) => diff --git a/src/index.js b/src/index.js index e94dc6f..cb4a3cf 100644 --- a/src/index.js +++ b/src/index.js @@ -120,15 +120,20 @@ app.post('/api/v1/users/signup', signUp); // 가게 관련 라우트 app.get('/api/v1/stores/:storeId', getStoreById); app.post('/api/v1/stores', handleAddStore); + +// 가게 리뷰 관련 라우트 app.get('/api/v1/stores/:storeId/reviews', handleListStoreReviews); -app.post('/api/v1/stores/:storeId/reviews', handleCreateStoreReview); // 미션 관련 라우트 app.get('/api/v1/stores/:storeId/missions', getStoreMissions); app.get('/api/v1/users/:userId/missions', getUserMissions); app.patch('/api/v1/users/:userId/missions/:missionId/complete', completeUserMission); app.post('/api/v1/users/:userId/missions', assignMissionToUser); + +// 리뷰 관련 라우트 app.get('/api/v1/users/:userId/reviews', getUserReviews); +app.post('/api/v1/reviews', handleCreateStoreReview); +app.post('/api/v1/stores/:storeId/reviews', handleCreateStoreReview); // 미션 도전 관련 라우트 app.post('/api/v1/missions/:missionId/challenge', handleChallengeMission); @@ -362,27 +367,6 @@ app.use((err, req, res, next) => { res.status(response.statusCode).json(response); }); -// 가게 관련 라우트 -app.get('/api/v1/stores/:storeId', getStoreById); -app.post('/api/v1/stores', handleAddStore); -app.get('/api/v1/stores/:storeId/reviews', handleListStoreReviews); -app.post('/api/v1/stores/:storeId/reviews', handleCreateStoreReview); - -// 미션 관련 라우트 -app.get('/api/v1/stores/:storeId/missions', getStoreMissions); -app.get('/api/v1/users/:userId/missions', getUserMissions); -app.patch('/api/v1/users/:userId/missions/:missionId/complete', completeUserMission); -app.post('/api/v1/users/:userId/missions', assignMissionToUser); -app.get('/api/v1/users/:userId/reviews', getUserReviews); - -// 리뷰 관련 라우트 -app.post('/api/v1/reviews', handleCreateStoreReview); - -// 미션 도전 관련 라우트 -app.post('/api/v1/missions/:missionId/challenge', handleChallengeMission); - -// 미션 추가 (관리자용) -app.post('/api/v1/missions', handleAddMission); // 서버 시작 async function startServer() { From 8455c92bfa2b451439e01e57fd030ba712bc1fa0 Mon Sep 17 00:00:00 2001 From: jeongkyueun Date: Thu, 27 Nov 2025 04:22:37 +0900 Subject: [PATCH 02/10] =?UTF-8?q?feat:=209=EC=A3=BC=EC=B0=A8=20=EC=8B=A4?= =?UTF-8?q?=EC=8A=B51=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 106 +++++++++++++++++++++++++++++++++++++++++++++ package.json | 3 ++ src/auth.config.js | 106 +++++++++++++++++++++++++++++++++++++++++++++ src/index.js | 74 +++++++++++++++++++------------ 4 files changed, 261 insertions(+), 28 deletions(-) create mode 100644 src/auth.config.js diff --git a/package-lock.json b/package-lock.json index 5e88075..e24401b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,9 @@ "http-status-codes": "^2.3.0", "jsonwebtoken": "^9.0.2", "morgan": "^1.10.1", + "passport": "^0.7.0", + "passport-google-oauth20": "^2.0.0", + "passport-jwt": "^4.0.1", "prisma": "^6.19.0", "swagger-autogen": "^2.23.7", "swagger-jsdoc": "^6.2.8", @@ -227,6 +230,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", @@ -1589,6 +1601,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", @@ -1662,6 +1680,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", @@ -1697,6 +1783,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", @@ -2237,6 +2328,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/undefsafe": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", @@ -2253,6 +2350,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/validator": { "version": "13.15.23", "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.23.tgz", diff --git a/package.json b/package.json index d9330f6..9de268a 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,9 @@ "http-status-codes": "^2.3.0", "jsonwebtoken": "^9.0.2", "morgan": "^1.10.1", + "passport": "^0.7.0", + "passport-google-oauth20": "^2.0.0", + "passport-jwt": "^4.0.1", "prisma": "^6.19.0", "swagger-autogen": "^2.23.7", "swagger-jsdoc": "^6.2.8", diff --git a/src/auth.config.js b/src/auth.config.js new file mode 100644 index 0000000..00d4ee7 --- /dev/null +++ b/src/auth.config.js @@ -0,0 +1,106 @@ +import dotenv from "dotenv"; +import { Strategy as GoogleStrategy } from "passport-google-oauth20"; +import { prisma } from "./db.config.js"; +import jwt from "jsonwebtoken"; // JWT 생성을 위해 import + +dotenv.config(); +const secret = process.env.JWT_SECRET; // .env의 비밀 키 + +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); + } + } +); + +import { Strategy as JwtStrategy, ExtractJwt } from 'passport-jwt'; + +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/index.js b/src/index.js index cb4a3cf..7793d9d 100644 --- a/src/index.js +++ b/src/index.js @@ -5,6 +5,9 @@ import path from 'path'; import { fileURLToPath } from 'url'; import swaggerJsdoc from 'swagger-jsdoc'; import swaggerUiExpress from "swagger-ui-express"; +import passport from "passport"; +import { googleStrategy, jwtStrategy } from "./auth.config.js"; +import { prisma } from "./db.config.js"; // Get the current directory name in ES module const __filename = fileURLToPath(import.meta.url); @@ -21,7 +24,6 @@ try { import morgan from 'morgan'; import cookieParser from 'cookie-parser'; -import { prisma } from './db.config.js'; // 컨트롤러 임포트 import { signUp } from './controllers/user.controller.js'; @@ -43,8 +45,9 @@ import { handleChallengeMission } from './controllers/mission.controller.js'; -// .env 파일 로드 -dotenv.config(); +// Passport 설정 +passport.use(googleStrategy); +passport.use(jwtStrategy); // Prisma 클라이언트 연결 확인 async function checkDatabaseConnection() { @@ -71,7 +74,7 @@ if (process.env.NODE_ENV !== 'production') { // 기본 미들웨어 설정 app.use(express.json()); -app.use(express.urlencoded({ extended: true })); +app.use(express.urlencoded({ extended: false })); // 원래 true였음 app.use(cookieParser()); app.use(cors({ origin: [ @@ -81,6 +84,7 @@ app.use(cors({ credentials: true })); app.use(express.static('public')); // 정적 파일 제공 +app.use(passport.initialize()); // 성공/에러 응답 메서드 추가 app.use((req, res, next) => { @@ -235,6 +239,34 @@ const options = { apis: ["./src/**/*.js"] }; + +/* 9주차실습시작 */ +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": "..." } + } + }); + } +); +/* 9주차실습끝 */ + const swaggerSpec = swaggerJsdoc(options); // Swagger UI @@ -447,24 +479,8 @@ startServer().catch(error => { app.get("/api/v1/stores/:storeId/reviews", handleListStoreReviews); - - -//7주차 시작 -const isLogin = (req, res, next) => { - // cookie-parser가 만들어준 req.cookies 객체에서 username을 확인 - const { username } = req.cookies; - - if (username) { - - console.log(`[인증 성공] ${username}님, 환영합니다.`); - next(); - } else { - - console.log('[인증 실패] 로그인이 필요합니다.'); - res.status(401).send(''); - } -}; - +// JWT 인증 미들웨어 +const isLogin = passport.authenticate('jwt', { session: false }); app.get('/', (req, res) => { res.send(` @@ -481,16 +497,18 @@ app.get('/login', (req, res) => { res.send('

로그인 페이지

로그인이 필요한 페이지에서 튕겨나오면 여기로 옵니다.

'); }); - app.get('/mypage', isLogin, (req, res) => { - res.send(` -

마이페이지

-

환영합니다, ${req.cookies.username}님!

-

이 페이지는 로그인한 사람만 볼 수 있습니다.

- `); + res.status(200).json({ + success: true, + message: `인증 성공! ${req.user.name}님의 마이페이지입니다.`, + data: { + user: req.user + } + }); }); + app.get('/set-login', (req, res) => { res.cookie('username', 'UMC9th', { maxAge: 3600000 }); res.send('로그인 쿠키(username=UMC9th) 생성 완료! 마이페이지로 이동'); From dafc72024f87a0c94f484465fe90fa26dbda12c5 Mon Sep 17 00:00:00 2001 From: jeongkyueun Date: Thu, 27 Nov 2025 04:27:30 +0900 Subject: [PATCH 03/10] =?UTF-8?q?feat:=209=EC=A3=BC=EC=B0=A8=20=EB=AF=B8?= =?UTF-8?q?=EC=85=981=20=EC=82=AC=EC=9A=A9=EC=9E=90=20=EC=A0=95=EB=B3=B4?= =?UTF-8?q?=20=EC=A0=91=EA=B7=BC=20=EC=8B=9C=20=ED=95=98=EB=93=9C=EC=BD=94?= =?UTF-8?q?=EB=94=A9=20=EC=A0=9C=EA=B1=B0=20=EB=B0=8F=20=EB=8F=99=EC=A0=81?= =?UTF-8?q?=20=EC=A1=B0=ED=9A=8C=20=EB=A1=9C=EC=A7=81=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/controllers/review.controller.js | 6 +++--- src/controllers/userChallenge.controller.js | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/controllers/review.controller.js b/src/controllers/review.controller.js index fead75e..abffd1f 100644 --- a/src/controllers/review.controller.js +++ b/src/controllers/review.controller.js @@ -3,11 +3,11 @@ import * as reviewService from '../services/review.service.js'; /** * POST /api/v1/stores/{storeId}/reviews 엔드포인트 핸들러 */ -export const handleAddReview = async (req, res) => { +export const addReview = async (req, res) => { // URL 경로에서 storeId 획득 const storeId = parseInt(req.params.storeId); - // 인증된 사용자 ID (현재는 ID 1로 가정) - const userId = 1; + // 인증된 사용자 ID 사용 + const userId = req.user.id; const { rating, content } = req.body; if (!rating || !content) { diff --git a/src/controllers/userChallenge.controller.js b/src/controllers/userChallenge.controller.js index 350049e..bfdf50e 100644 --- a/src/controllers/userChallenge.controller.js +++ b/src/controllers/userChallenge.controller.js @@ -4,8 +4,8 @@ import * as challengeService from '../services/userChallenge.service.js'; * POST /api/v1/users/{userId}/challenges 엔드포인트 핸들러 */ export const handleChallengeMission = async (req, res) => { - // URL 경로에서 userId 획득 (현재는 ID 1로 가정) - const userId = 1; + // 인증된 사용자 ID 사용 + const userId = req.user.id; // Body에서 도전할 미션 ID 획득 const { missionId } = req.body; From 47df8a2b92c81f21cbac154a0f6be6a1939286d4 Mon Sep 17 00:00:00 2001 From: jeongkyueun Date: Thu, 27 Nov 2025 04:32:21 +0900 Subject: [PATCH 04/10] =?UTF-8?q?feat:=209=EC=A3=BC=EC=B0=A8=20=EB=AF=B8?= =?UTF-8?q?=EC=85=982=20=EC=9D=B8=EC=A6=9D=EB=90=9C=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=EC=9E=90=20=EC=A0=95=EB=B3=B4=20=EA=B0=B1=EC=8B=A0=EC=9D=84=20?= =?UTF-8?q?=EC=9C=84=ED=95=9C=20PATCH=20/users/me=20API=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/controllers/user.controller.js | 84 +++++++++++++++++++++++++++- src/index.js | 3 +- src/services/user.service.js | 88 +++++++++++++++++++++++++++++- 3 files changed, 171 insertions(+), 4 deletions(-) diff --git a/src/controllers/user.controller.js b/src/controllers/user.controller.js index 47376ae..cc59acb 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, updateUser } from '../services/user.service.js'; import { ValidationError } from '../errors.js'; import { prisma } from '../db.config.js'; @@ -210,6 +210,88 @@ export const handleUserSignUp = async (req, res, next) => { } }; +/** + * @swagger + * /api/v1/users/me: + * put: + * tags: [User] + * summary: Update user information + * description: Update the authenticated user's information + * security: + * - bearerAuth: [] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * name: + * type: string + * description: User's name + * gender: + * type: string + * enum: [MALE, FEMALE, OTHER] + * description: User's gender + * birth: + * type: string + * format: date + * description: User's birth date (YYYY-MM-DD) + * address: + * type: string + * description: User's address + * detailAddress: + * type: string + * description: User's detailed address + * phoneNumber: + * type: string + * description: User's phone number (without hyphens) + * preferences: + * type: array + * items: + * type: string + * enum: [한식, 일식, 중식, 양식, 치킨, 분식, 고기/구이, 도시락, 야식, 패스트푸드, 디저트, 아시안푸드] + * description: User's food preferences + * responses: + * 200: + * description: User information updated successfully + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/User' + * 400: + * description: Invalid input data + * 401: + * description: Unauthorized + * 404: + * description: User not found + * 500: + * description: Internal server error + */ +export const updateMyProfile = async (req, res, next) => { + try { + const userId = req.user.id; + const updateData = req.body; + + // Validate input data + if (!updateData || Object.keys(updateData).length === 0) { + throw new ValidationError('업데이트할 정보를 입력해주세요.'); + } + + // Update user information + const updatedUser = await updateUser(userId, updateData); + + res.status(StatusCodes.OK).json({ + success: true, + message: '사용자 정보가 성공적으로 업데이트되었습니다.', + data: updatedUser + }); + } catch (error) { + console.error('사용자 정보 업데이트 중 오류 발생:', error); + next(error); + } +}; + export const signUp = async (req, res, next) => { try { console.log('\n=== 회원가입 요청 시작 ==='); diff --git a/src/index.js b/src/index.js index 7793d9d..5249fd1 100644 --- a/src/index.js +++ b/src/index.js @@ -26,7 +26,7 @@ import morgan from 'morgan'; import cookieParser from 'cookie-parser'; // 컨트롤러 임포트 -import { signUp } from './controllers/user.controller.js'; +import { signUp, updateMyProfile } from './controllers/user.controller.js'; import { handleAddStore, handleListStoreReviews, @@ -120,6 +120,7 @@ app.use((req, res, next) => { // 4. 라우트 설정 // 사용자 관련 라우트 app.post('/api/v1/users/signup', signUp); +app.put('/api/v1/users/me', passport.authenticate('jwt', { session: false }), updateMyProfile); // 가게 관련 라우트 app.get('/api/v1/stores/:storeId', getStoreById); diff --git a/src/services/user.service.js b/src/services/user.service.js index d5bd3c5..6a75a83 100644 --- a/src/services/user.service.js +++ b/src/services/user.service.js @@ -219,12 +219,96 @@ function mapPreferencesToCategoryIds(preferences = []) { * @param {Object} user - 사용자 객체 * @returns {Object} 민감 정보가 제거된 사용자 객체 */ -function excludeSensitiveData(user) { +export const excludeSensitiveData = (user) => { if (!user) return null; const { password, ...userWithoutPassword } = user; return userWithoutPassword; -} +}; + +/** + * 사용자 정보 업데이트 + * @param {number} userId - 업데이트할 사용자 ID + * @param {Object} updateData - 업데이트할 사용자 정보 + * @returns {Promise} 업데이트된 사용자 정보 + * @throws {NotFoundError} 사용자를 찾을 수 없을 때 + * @throws {ValidationError} 유효성 검사 실패 시 + */ +export const updateUser = async (userId, updateData) => { + try { + // 1. 사용자 존재 여부 확인 + const existingUser = await prisma.user.findUnique({ + where: { id: userId }, + include: { + preferences: true + } + }); + + if (!existingUser) { + throw new NotFoundError('사용자를 찾을 수 없습니다.'); + } + + // 2. 업데이트할 데이터 준비 + const dataToUpdate = { + name: updateData.name, + gender: updateData.gender, + birth: updateData.birth ? new Date(updateData.birth) : null, + address: updateData.address, + detailAddress: updateData.detailAddress || null, + phoneNumber: updateData.phoneNumber || null + }; + + // 3. 선호 카테고리 업데이트 + let foodCategoryIds = []; + if (updateData.preferences && updateData.preferences.length > 0) { + foodCategoryIds = mapPreferencesToCategoryIds(updateData.preferences); + } + + // 4. 트랜잭션으로 사용자 정보와 선호 카테고리 업데이트 + const [updatedUser] = await prisma.$transaction([ + // 사용자 정보 업데이트 + prisma.user.update({ + where: { id: userId }, + data: dataToUpdate, + include: { + preferences: { + include: { + foodCategory: true + } + } + } + }), + // 기존 선호 카테고리 삭제 + prisma.userPreference.deleteMany({ + where: { userId } + }), + // 새로운 선호 카테고리 추가 + ...(foodCategoryIds.length > 0 ? [ + prisma.userPreference.createMany({ + data: foodCategoryIds.map(categoryId => ({ + userId, + foodCategoryId: categoryId + })) + }) + ] : []) + ]); + + // 5. 업데이트된 사용자 정보 조회 (선호 카테고리 포함) + const userWithPreferences = await getUser(userId); + + // 6. 민감 정보 제거 후 반환 + return excludeSensitiveData(userWithPreferences); + + } catch (error) { + console.error('사용자 정보 업데이트 중 오류 발생:', error); + + if (error.code === 'P2002') { + throw new ValidationError('이미 사용 중인 이메일입니다.'); + } + + throw error; + } +}; /** * 이메일로 사용자 조회 From afb56fd504d6be0fa2976392c41cad21afceddb0 Mon Sep 17 00:00:00 2001 From: jeongkyueun Date: Thu, 27 Nov 2025 04:47:32 +0900 Subject: [PATCH 05/10] =?UTF-8?q?feat:=209=EC=A3=BC=EC=B0=A8=20=EB=AF=B8?= =?UTF-8?q?=EC=85=983=20JWT=20=EC=9D=B8=EC=A6=9D=20=EC=8B=9C=EC=8A=A4?= =?UTF-8?q?=ED=85=9C=EC=9D=84=20=EC=A3=BC=EC=9A=94=20API=EC=97=90=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9=ED=95=98=EC=97=AC=20=EC=A0=91=EA=B7=BC=20?= =?UTF-8?q?=EB=B3=B4=ED=98=B8=20=EA=B0=95=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/auth.config.js | 58 +++++++++++++- src/controllers/mission.controller.js | 52 ++++++++++--- src/controllers/store.controller.js | 105 +++++++++++++++----------- src/index.js | 51 +++++++------ 4 files changed, 182 insertions(+), 84 deletions(-) diff --git a/src/auth.config.js b/src/auth.config.js index 00d4ee7..2b65b2d 100644 --- a/src/auth.config.js +++ b/src/auth.config.js @@ -100,7 +100,59 @@ export const jwtStrategy = new JwtStrategy(jwtOptions, async (payload, done) => } else { return done(null, false); } - } catch (err) { - return done(err, false); + } catch (error) { + return done(error, false); } -}); \ No newline at end of file +}); + +/** + * JWT 인증 미들웨어 + * 인증이 필요한 라우트에 사용 + */ +export const authenticateJWT = (req, res, next) => { + return passport.authenticate('jwt', { session: false }, (err, user, info) => { + if (err) { + console.error('JWT 인증 오류:', err); + return res.status(500).json({ + success: false, + message: '인증 처리 중 오류가 발생했습니다.' + }); + } + + if (!user) { + return res.status(401).json({ + success: false, + message: '로그인이 필요합니다.' + }); + } + + // 인증된 사용자 정보를 req.user에 저장 + req.user = user; + next(); + })(req, res, next); +}; + +/** + * 관리자 권한 확인 미들웨어 + * 관리자만 접근 가능한 라우트에 사용 + */ +export const requireAdmin = (req, res, next) => { + // authenticateJWT 미들웨어를 먼저 통과해야 함 + if (!req.user) { + return res.status(401).json({ + success: false, + message: '인증이 필요합니다.' + }); + } + + // 여기서는 간단히 isAdmin 플래그로 관리자 여부를 확인 + // 실제 구현에서는 사용자 역할(role)을 확인하는 로직으로 대체해야 함 + if (req.user.isAdmin) { + return next(); + } + + return res.status(403).json({ + success: false, + message: '관리자 권한이 필요합니다.' + }); +}; \ No newline at end of file diff --git a/src/controllers/mission.controller.js b/src/controllers/mission.controller.js index b084100..1ca1da9 100644 --- a/src/controllers/mission.controller.js +++ b/src/controllers/mission.controller.js @@ -1,7 +1,7 @@ import * as missionService from '../services/mission.service.js'; import { StatusCodes } from 'http-status-codes'; -// Error handling with standard Error and status codes +// 표준 Error와 상태 코드를 사용한 에러 처리 class AppError extends Error { constructor(message, statusCode) { super(message); @@ -343,10 +343,22 @@ export const handleAddMission = async (req, res, next) => { */ export const getUserMissions = async (req, res, next) => { try { - const userId = parseInt(req.params.userId); - const missions = await missionService.getUserMissions(userId); + const userId = req.user.id; // Use authenticated user's ID + const { status } = req.query; + const page = parseInt(req.query.page) || 1; + const limit = parseInt(req.query.limit) || 10; - res.success(missions); + const result = await missionService.getUserMissions(userId, { status, page, limit }); + + res.success({ + data: result.missions, + pagination: { + totalItems: result.total, + totalPages: Math.ceil(result.total / limit), + currentPage: page, + itemsPerPage: limit + } + }); } catch (error) { next(error); } @@ -518,10 +530,17 @@ export const getUserMissions = async (req, res, next) => { */ export const completeUserMission = async (req, res, next) => { try { - const { userId, missionId } = req.params; - const result = await missionService.completeMission(parseInt(userId), parseInt(missionId)); + const userId = req.user.id; // Use authenticated user's ID + const { missionId } = req.params; - res.success(result, '미션이 성공적으로 완료되었습니다.'); + const result = await missionService.completeMission(userId, parseInt(missionId)); + + res.success({ + id: result.id, + status: result.status, + completedAt: result.completedAt, + rewardEarned: result.rewardEarned + }, '미션이 성공적으로 완료되었습니다!'); } catch (error) { next(error); } @@ -708,7 +727,7 @@ export const completeUserMission = async (req, res, next) => { */ export const assignMissionToUser = async (req, res, next) => { try { - const userId = parseInt(req.params.userId); + const userId = req.user.id; // Use authenticated user's ID const { missionId } = req.body; if (!missionId) { @@ -875,10 +894,21 @@ export const assignMissionToUser = async (req, res, next) => { */ export const getUserReviews = async (req, res, next) => { try { - const userId = parseInt(req.params.userId); - const reviews = await missionService.getUserReviews(userId); + const userId = req.user.id; // Use authenticated user's ID + const page = parseInt(req.query.page) || 1; + const limit = parseInt(req.query.limit) || 10; + + const result = await missionService.getUserReviews(userId, { page, limit }); - res.success(reviews); + res.success({ + data: result.reviews, + pagination: { + totalItems: result.total, + totalPages: Math.ceil(result.total / limit), + currentPage: page, + itemsPerPage: limit + } + }); } catch (error) { next(error); } diff --git a/src/controllers/store.controller.js b/src/controllers/store.controller.js index 8c79b53..3f49bed 100644 --- a/src/controllers/store.controller.js +++ b/src/controllers/store.controller.js @@ -67,18 +67,29 @@ import { NotFoundError } from '../errors.js'; * type: string * example: "가게 추가 중 오류가 발생했습니다." */ -export const handleAddStore = async (req, res) => { - const { name, address, region } = req.body; +export const handleAddStore = async (req, res, next) => { + try { + const { name, address, region } = req.body; - if (!name || !address || !region) { - return res.status(400).json({ message: '모든 가게 정보를 입력해야 합니다.' }); - } + if (!name || !address || !region) { + return res.status(StatusCodes.BAD_REQUEST).json({ + success: false, + message: '모든 가게 정보를 입력해야 합니다.' + }); + } - try { - const result = await storeService.addNewStore({ name, address, region }); - return res.success(result, '가게가 성공적으로 등록되었습니다.', 201); + const result = await storeService.addNewStore({ + name, + address, + region + }); + + return res.status(StatusCodes.CREATED).json({ + success: true, + message: '가게가 성공적으로 등록되었습니다.', + data: result + }); } catch (error) { - // 에러를 next로 전달하여 전역 에러 핸들러에서 처리하도록 함 next(error); } }; @@ -413,47 +424,49 @@ export const getStoreMissions = async (req, res, next) => { * example: "리뷰 작성 중 오류가 발생했습니다." */ export const handleCreateStoreReview = async (req, res, next) => { - try { - const { content, rating, userId } = req.body; - const storeId = parseInt(req.params.storeId); + try { + const userId = req.user.id; + const { content, rating } = req.body; + const storeId = parseInt(req.params.storeId); - // 테스트를 위해 인증 검사 일시 비활성화 - // if (!userId) { - // return res.status(StatusCodes.UNAUTHORIZED).json({ - // message: '로그인이 필요합니다.' - // }); - // } + if (!content || !rating) { + return res.status(StatusCodes.BAD_REQUEST).json({ + success: false, + message: '리뷰 내용과 평점은 필수입니다.' + }); + } - const review = await storeService.createStoreReview({ - content, - rating, - userId, - storeId - }); + const review = await storeService.createStoreReview({ + content, + rating, + userId, + storeId + }); - res.status(StatusCodes.CREATED).json({ - message: '리뷰가 성공적으로 등록되었습니다.', - data: review - }); - } catch (error) { - console.error('Error creating store review:', error); - - if (error.message.includes('가게를 찾을 수 없습니다')) { - return res.status(StatusCodes.NOT_FOUND).json({ - message: error.message - }); - } - - if (error.message.includes('필수') || error.message.includes('평점')) { - return res.status(StatusCodes.BAD_REQUEST).json({ - message: error.message - }); + return res.status(StatusCodes.CREATED).json({ + success: true, + message: '리뷰가 성공적으로 등록되었습니다.', + data: review + }); + } catch (error) { + console.error('Error creating store review:', error); + + if (error.message.includes('가게를 찾을 수 없습니다')) { + return res.status(StatusCodes.NOT_FOUND).json({ + success: false, + message: error.message + }); + } + + if (error.message.includes('필수') || error.message.includes('평점')) { + return res.status(StatusCodes.BAD_REQUEST).json({ + success: false, + message: error.message + }); + } + + next(error); } - - res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ - message: '리뷰 등록 중 오류가 발생했습니다.' - }); - } }; // handleAddReview는 handleCreateStoreReview의 별칭으로 사용 diff --git a/src/index.js b/src/index.js index 5249fd1..3ec446a 100644 --- a/src/index.js +++ b/src/index.js @@ -9,11 +9,11 @@ import passport from "passport"; import { googleStrategy, jwtStrategy } from "./auth.config.js"; import { prisma } from "./db.config.js"; -// Get the current directory name in ES module +// ES 모듈에서 현재 디렉토리 이름 가져오기 const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); -// Load .env file from the root directory +// 루트 디렉토리에서 .env 파일 로드 const envPath = path.resolve(__dirname, '../../.env'); try { dotenv.config({ path: envPath }); @@ -25,6 +25,9 @@ try { import morgan from 'morgan'; import cookieParser from 'cookie-parser'; +// 미들웨어 임포트 +import { authenticateJWT, requireAdmin } from './auth.config.js'; + // 컨트롤러 임포트 import { signUp, updateMyProfile } from './controllers/user.controller.js'; import { @@ -120,31 +123,31 @@ app.use((req, res, next) => { // 4. 라우트 설정 // 사용자 관련 라우트 app.post('/api/v1/users/signup', signUp); -app.put('/api/v1/users/me', passport.authenticate('jwt', { session: false }), updateMyProfile); +app.put('/api/v1/users/me', authenticateJWT, updateMyProfile); // 가게 관련 라우트 app.get('/api/v1/stores/:storeId', getStoreById); -app.post('/api/v1/stores', handleAddStore); +app.post('/api/v1/stores', authenticateJWT, handleAddStore); // 가게 리뷰 관련 라우트 app.get('/api/v1/stores/:storeId/reviews', handleListStoreReviews); // 미션 관련 라우트 app.get('/api/v1/stores/:storeId/missions', getStoreMissions); -app.get('/api/v1/users/:userId/missions', getUserMissions); -app.patch('/api/v1/users/:userId/missions/:missionId/complete', completeUserMission); -app.post('/api/v1/users/:userId/missions', assignMissionToUser); +app.get('/api/v1/users/:userId/missions', authenticateJWT, getUserMissions); +app.patch('/api/v1/users/:userId/missions/:missionId/complete', authenticateJWT, completeUserMission); +app.post('/api/v1/users/:userId/missions', authenticateJWT, assignMissionToUser); // 리뷰 관련 라우트 -app.get('/api/v1/users/:userId/reviews', getUserReviews); -app.post('/api/v1/reviews', handleCreateStoreReview); -app.post('/api/v1/stores/:storeId/reviews', handleCreateStoreReview); +app.get('/api/v1/users/:userId/reviews', authenticateJWT, getUserReviews); +app.post('/api/v1/reviews', authenticateJWT, handleCreateStoreReview); +app.post('/api/v1/stores/:storeId/reviews', authenticateJWT, handleCreateStoreReview); // 미션 도전 관련 라우트 -app.post('/api/v1/missions/:missionId/challenge', handleChallengeMission); +app.post('/api/v1/missions/:missionId/challenge', authenticateJWT, handleChallengeMission); // 미션 추가 (관리자용) -app.post('/api/v1/missions', handleAddMission); +app.post('/api/v1/missions', authenticateJWT, requireAdmin, handleAddMission); // API 상태 확인을 위한 엔드포인트 app.get('/api/health', (req, res) => { @@ -309,16 +312,16 @@ app.get('/openapi.json', (req, res) => { res.send(swaggerSpec); }); -// Global error handler middleware +// 전역 에러 핸들링 미들웨어 app.use((err, req, res, next) => { console.error('Error:', err); - // If headers are already sent, delegate to the default Express error handler + // 헤더가 이미 전송된 경우 기본 Express 에러 핸들러에 위임 if (res.headersSent) { return next(err); } - // Default error response + // 기본 에러 응답 const statusCode = err.statusCode || 500; const response = { success: false, @@ -334,9 +337,9 @@ app.use((err, req, res, next) => { timestamp: new Date().toISOString() }; - // Handle specific error types + // 특정 에러 유형 처리 switch (true) { - // Validation errors (400) + // 유효성 검사 에러 (400) case err.name === 'ValidationError': case statusCode === 400: response.error.code = 'VALIDATION_ERROR'; @@ -352,7 +355,7 @@ app.use((err, req, res, next) => { response.statusCode = 404; break; - // Authentication errors (401) + // 인증 에러 (401) case err.name === 'UnauthorizedError': case statusCode === 401: response.error.code = 'UNAUTHORIZED'; @@ -360,7 +363,7 @@ app.use((err, req, res, next) => { response.statusCode = 401; break; - // Forbidden errors (403) + // 접근 거부 에러 (403) case err.name === 'ForbiddenError': case statusCode === 403: response.error.code = 'FORBIDDEN'; @@ -368,7 +371,7 @@ app.use((err, req, res, next) => { response.statusCode = 403; break; - // Conflict errors (409) + // 충돌 에러 (409) case err.name === 'ConflictError': case statusCode === 409: response.error.code = 'CONFLICT'; @@ -376,27 +379,27 @@ app.use((err, req, res, next) => { response.statusCode = 409; break; - // Rate limiting (429) + // 요청 한도 초과 (429) case err.name === 'RateLimitError': response.error.code = 'RATE_LIMIT_EXCEEDED'; response.error.message = err.message || '요청 한도를 초과했습니다. 잠시 후 다시 시도해주세요.'; response.statusCode = 429; break; - // Default to 500 for unhandled errors + // 처리되지 않은 에러는 기본적으로 500 에러로 처리 default: response.statusCode = 500; response.error.code = 'INTERNAL_SERVER_ERROR'; response.error.message = '서버에서 오류가 발생했습니다.'; - // Don't leak error details in production + // 프로덕션 환경에서는 에러 상세 정보 노출 방지 if (process.env.NODE_ENV !== 'development') { delete response.error.stack; delete response.error.name; } } - // Send the error response + // 에러 응답 전송 res.status(response.statusCode).json(response); }); From e79aa3731631b88a0e7d6092ad4a1577c29ce333 Mon Sep 17 00:00:00 2001 From: jeongkyueun Date: Thu, 27 Nov 2025 04:57:50 +0900 Subject: [PATCH 06/10] =?UTF-8?q?docs:=20=EC=98=81=EC=96=B4=20=EB=AC=B8?= =?UTF-8?q?=EC=84=9C/=EC=84=A4=EB=AA=85=20=ED=95=9C=EA=B8=80=20=EB=B2=88?= =?UTF-8?q?=EC=97=AD=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/controllers/mission.controller.js | 8 +++++--- src/controllers/user.controller.js | 28 +++++++++++++-------------- 2 files changed, 19 insertions(+), 17 deletions(-) diff --git a/src/controllers/mission.controller.js b/src/controllers/mission.controller.js index 1ca1da9..12ac507 100644 --- a/src/controllers/mission.controller.js +++ b/src/controllers/mission.controller.js @@ -918,7 +918,7 @@ export const getUserReviews = async (req, res, next) => { * @swagger * /api/v1/stores/{storeId}/missions: * get: - * summary: Get all missions for a store + * summary: 가게의 모든 미션 조회 * tags: [Missions] * parameters: * - in: path @@ -926,11 +926,13 @@ export const getUserReviews = async (req, res, next) => { * required: true * schema: * type: integer + * minimum: 1 + * description: 미션을 조회할 가게의 고유 ID * responses: * 200: - * description: List of store's missions + * description: 가게의 미션 목록이 성공적으로 조회됨 * 500: - * description: Internal server error + * description: 서버 내부 오류 발생 */ /** * @swagger diff --git a/src/controllers/user.controller.js b/src/controllers/user.controller.js index cc59acb..33abb0e 100644 --- a/src/controllers/user.controller.js +++ b/src/controllers/user.controller.js @@ -215,8 +215,8 @@ export const handleUserSignUp = async (req, res, next) => { * /api/v1/users/me: * put: * tags: [User] - * summary: Update user information - * description: Update the authenticated user's information + * summary: 사용자 정보 수정 + * description: 인증된 사용자의 정보를 업데이트합니다. * security: * - bearerAuth: [] * requestBody: @@ -228,45 +228,45 @@ export const handleUserSignUp = async (req, res, next) => { * properties: * name: * type: string - * description: User's name + * description: 사용자 이름 * gender: * type: string * enum: [MALE, FEMALE, OTHER] - * description: User's gender + * description: 사용자 성별 * birth: * type: string * format: date - * description: User's birth date (YYYY-MM-DD) + * description: 사용자 생년월일 (YYYY-MM-DD 형식) * address: * type: string - * description: User's address + * description: 사용자 주소 * detailAddress: * type: string - * description: User's detailed address + * description: 사용자 상세 주소 * phoneNumber: * type: string - * description: User's phone number (without hyphens) + * description: 사용자 전화번호 (하이픈 없이 입력) * preferences: * type: array * items: * type: string * enum: [한식, 일식, 중식, 양식, 치킨, 분식, 고기/구이, 도시락, 야식, 패스트푸드, 디저트, 아시안푸드] - * description: User's food preferences + * description: 사용자 음식 선호도 * responses: * 200: - * description: User information updated successfully + * description: 사용자 정보가 성공적으로 업데이트됨 * content: * application/json: * schema: * $ref: '#/components/schemas/User' * 400: - * description: Invalid input data + * description: 잘못된 입력 데이터 * 401: - * description: Unauthorized + * description: 인증 실패 * 404: - * description: User not found + * description: 사용자를 찾을 수 없음 * 500: - * description: Internal server error + * description: 서버 내부 오류 */ export const updateMyProfile = async (req, res, next) => { try { From 4daac18bc823a35e965ac6b3bd63e0f0917c0069 Mon Sep 17 00:00:00 2001 From: jeongkyueun Date: Mon, 1 Dec 2025 23:25:05 +0900 Subject: [PATCH 07/10] =?UTF-8?q?refactor:=209=EC=A3=BC=EC=B0=A8=20?= =?UTF-8?q?=EB=AF=B8=EC=85=98=20=ED=94=BC=EB=93=9C=EB=B0=B1=20=EB=B0=98?= =?UTF-8?q?=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/auth.config.js | 28 ++++++++++++++++++++++------ src/services/user.service.js | 19 +++++++++++++++---- 2 files changed, 37 insertions(+), 10 deletions(-) diff --git a/src/auth.config.js b/src/auth.config.js index 2b65b2d..4117704 100644 --- a/src/auth.config.js +++ b/src/auth.config.js @@ -34,17 +34,33 @@ const googleVerify = async (profile) => { if (user !== null) { return { id: user.id, email: user.email, name: user.name }; } - + /* + // 기본 프로필 이미지 설정 (필요시) + const defaultProfileImage = 'https://example.com/default-profile.png'; +*/ const created = await prisma.user.create({ data: { email, name: profile.displayName, - gender: "추후 수정", - birth: new Date(1970, 0, 1), - address: "추후 수정", - detailAddress: "추후 수정", - phoneNumber: "추후 수정", + gender: "Other", // 기본값을 Other로 설정 (Male/Female/Other) + birth: new Date(2000, 0, 1), // 기본 생년월일 설정 (2000-01-01) + address: "", + detailAddress: "", + phoneNumber: "", + profileImage: profile.photos?.[0]?.value || defaultProfileImage, + password: "", // 소셜 로그인 사용자는 비밀번호 없음 + // 선호 카테고리 초기화 (빈 배열로 생성) + preferences: { + create: [] + } }, + include: { + preferences: { + include: { + foodCategory: true + } + } + } }); return { id: created.id, email: created.email, name: created.name }; diff --git a/src/services/user.service.js b/src/services/user.service.js index 6a75a83..ffbead5 100644 --- a/src/services/user.service.js +++ b/src/services/user.service.js @@ -279,12 +279,12 @@ export const updateUser = async (userId, updateData) => { } }), // 기존 선호 카테고리 삭제 - prisma.userPreference.deleteMany({ + prisma.userFavorCategory.deleteMany({ where: { userId } }), // 새로운 선호 카테고리 추가 ...(foodCategoryIds.length > 0 ? [ - prisma.userPreference.createMany({ + prisma.userFavorCategory.createMany({ data: foodCategoryIds.map(categoryId => ({ userId, foodCategoryId: categoryId @@ -317,9 +317,19 @@ export const updateUser = async (userId, updateData) => { */ export const getUserByEmail = async (email) => { try { - const user = await getUser(email); + // 비밀번호 포함하여 사용자 정보 조회 (excludeSensitiveData 호출 제거) + const user = await prisma.user.findUnique({ + where: { email }, + include: { + preferences: { + include: { + foodCategory: true + } + } + } + }); - return user ? excludeSensitiveData(user) : null; + return user || null; } catch (error) { console.error('사용자 조회 중 오류 발생:', error); throw new InternalServerError('사용자 정보를 가져오는 중 오류가 발생했습니다.'); @@ -348,5 +358,6 @@ export const authenticateUser = async (email, password) => { ]); } + // 인증 성공 시에만 민감한 정보 제거 return excludeSensitiveData(user); }; \ No newline at end of file From 235fec6e8118adb47b22f760ff10ce4c194b4939 Mon Sep 17 00:00:00 2001 From: jeongkyueun Date: Tue, 2 Dec 2025 00:57:37 +0900 Subject: [PATCH 08/10] =?UTF-8?q?chore:=20GitHub=20Actions=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=B4=ED=94=84=EB=9D=BC=EC=9D=B8=20=EC=9E=91=EC=84=B1?= 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..c79e7f9 --- /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 c0c350c33606504c7ac5323866e6b82149a9eedd Mon Sep 17 00:00:00 2001 From: jeongkyueun Date: Sun, 7 Dec 2025 01:12:08 +0900 Subject: [PATCH 09/10] =?UTF-8?q?feat:=2010=EC=A3=BC=EC=B0=A8=20=EB=AF=B8?= =?UTF-8?q?=EC=85=98=20-=20=ED=8C=8C=EC=9D=B4=ED=94=84=EB=9D=BC=EC=9D=B8?= =?UTF-8?q?=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 | 79 +++++++++++++++++++++++++++++-- 1 file changed, 74 insertions(+), 5 deletions(-) diff --git a/.github/workflows/deploy-main.yml b/.github/workflows/deploy-main.yml index c79e7f9..7748d88 100644 --- a/.github/workflows/deploy-main.yml +++ b/.github/workflows/deploy-main.yml @@ -1,13 +1,82 @@ -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 + + # Prisma 폴더가 변경되었는지 감지하는 단계 + - 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_ZERO" > ~/.ssh/id_rsa + chmod 600 ~/.ssh/id_rsa + + + cat >>~/.ssh/config < Date: Sun, 7 Dec 2025 01:18:26 +0900 Subject: [PATCH 10/10] =?UTF-8?q?feat:=2010=EC=A3=BC=EC=B0=A8=20=EB=AF=B8?= =?UTF-8?q?=EC=85=98=20-=20=EB=B0=B0=ED=8F=AC=20=EB=B8=8C=EB=9E=9C?= =?UTF-8?q?=EC=B9=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy-main.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/deploy-main.yml b/.github/workflows/deploy-main.yml index 7748d88..edc2459 100644 --- a/.github/workflows/deploy-main.yml +++ b/.github/workflows/deploy-main.yml @@ -4,6 +4,7 @@ on: push: branches: - 제로/main + - feature/mission-10/제로 workflow_dispatch: jobs: