Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
380 changes: 379 additions & 1 deletion package-lock.json

Large diffs are not rendered by default.

8 changes: 7 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,14 @@
"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",
"prisma": "^6.19.0"
"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-ui-express": "^5.0.1"
}
}
115 changes: 115 additions & 0 deletions prisma/migrations/20251203163625_make_fields_optional/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
-- CreateTable
CREATE TABLE `user` (
`id` INTEGER NOT NULL AUTO_INCREMENT,
`email` VARCHAR(255) NOT NULL,
`name` VARCHAR(100) NOT NULL,
`gender` VARCHAR(15) NULL,
`birth` DATE NULL,
`address` VARCHAR(255) NULL,
`detail_address` VARCHAR(255) NULL,
`phone_number` VARCHAR(15) NULL,
`point` INTEGER NOT NULL DEFAULT 0,

UNIQUE INDEX `email`(`email`),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

-- CreateTable
CREATE TABLE `restaurant` (
`id` INTEGER NOT NULL AUTO_INCREMENT,
`name` VARCHAR(100) NOT NULL,
`address` VARCHAR(255) NOT NULL,
`detail_address` VARCHAR(255) NULL,
`phone_number` VARCHAR(15) NOT NULL,
`region_id` INTEGER NOT NULL,
`food_category_id` INTEGER NOT NULL,

PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

-- CreateTable
CREATE TABLE `region` (
`id` INTEGER NOT NULL AUTO_INCREMENT,
`name` VARCHAR(100) NOT NULL,

PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

-- CreateTable
CREATE TABLE `review` (
`id` INTEGER NOT NULL AUTO_INCREMENT,
`content` VARCHAR(100) NOT NULL,
`rating` FLOAT NOT NULL,
`user_id` INTEGER NOT NULL,
`restaurant_id` INTEGER NOT NULL,

PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

-- CreateTable
CREATE TABLE `food_category` (
`id` INTEGER NOT NULL AUTO_INCREMENT,
`name` VARCHAR(100) NOT NULL,

PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

-- CreateTable
CREATE TABLE `user_favor_category` (
`id` INTEGER NOT NULL AUTO_INCREMENT,
`user_id` INTEGER NOT NULL,
`food_category_id` INTEGER NOT NULL,

INDEX `f_category_id`(`food_category_id`),
INDEX `user_id`(`user_id`),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

-- CreateTable
CREATE TABLE `mission` (
`id` INTEGER NOT NULL AUTO_INCREMENT,
`point` INTEGER NOT NULL,
`content` TEXT NOT NULL,
`deadline` DATE NULL,
`restaurant_id` INTEGER NOT NULL,

PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

-- CreateTable
CREATE TABLE `user_mission` (
`id` INTEGER NOT NULL AUTO_INCREMENT,
`status` VARCHAR(20) NOT NULL DEFAULT '진행중',
`user_id` INTEGER NOT NULL,
`mission_id` INTEGER NOT NULL,

UNIQUE INDEX `user_mission_user_id_mission_id_key`(`user_id`, `mission_id`),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

-- AddForeignKey
ALTER TABLE `restaurant` ADD CONSTRAINT `restaurant_region_id_fkey` FOREIGN KEY (`region_id`) REFERENCES `region`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE `restaurant` ADD CONSTRAINT `restaurant_food_category_id_fkey` FOREIGN KEY (`food_category_id`) REFERENCES `food_category`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE `review` ADD CONSTRAINT `review_user_id_fkey` FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE `review` ADD CONSTRAINT `review_restaurant_id_fkey` FOREIGN KEY (`restaurant_id`) REFERENCES `restaurant`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE `user_favor_category` ADD CONSTRAINT `user_favor_category_user_id_fkey` FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE `user_favor_category` ADD CONSTRAINT `user_favor_category_food_category_id_fkey` FOREIGN KEY (`food_category_id`) REFERENCES `food_category`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE `mission` ADD CONSTRAINT `mission_restaurant_id_fkey` FOREIGN KEY (`restaurant_id`) REFERENCES `restaurant`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE `user_mission` ADD CONSTRAINT `user_mission_user_id_fkey` FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE `user_mission` ADD CONSTRAINT `user_mission_mission_id_fkey` FOREIGN KEY (`mission_id`) REFERENCES `mission`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
3 changes: 3 additions & 0 deletions prisma/migrations/migration_lock.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (e.g., Git)
provider = "mysql"
8 changes: 4 additions & 4 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,11 @@ model User {
id Int @id @default(autoincrement())
email String @unique(map: "email") @db.VarChar(255)
name String @db.VarChar(100)
gender String @db.VarChar(15)
birth DateTime @db.Date
address String @db.VarChar(255)
gender String? @db.VarChar(15)
birth DateTime? @db.Date
address String? @db.VarChar(255)
detailAddress String? @map("detail_address") @db.VarChar(255)
phoneNumber String @map("phone_number") @db.VarChar(15)
phoneNumber String? @map("phone_number") @db.VarChar(15)
point Int @default(0)

userFavorCategories UserFavorCategory[]
Expand Down
98 changes: 98 additions & 0 deletions src/auth.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import dotenv from 'dotenv';
import {Strategy as GoogleStrategy} from 'passport-google-oauth20';
import {prisma} from "./db.config.js";
import jwt from 'jsonwebtoken'; //json 생성을 위해 import
import { Strategy as JwtStrategy, ExtractJwt } from 'passport-jwt';

dotenv.config();
const secret = process.env.JWT_SECRET; // JWT 비밀 키

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, email: user.email},
secret,
{expiresIn: '14d'} // 리프레시 토큰 유효 기간 설정
);
};

// Google Verify
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) {
return {id: user.id, email: user.email, name: user.name};
}

const created = await prisma.user.create({
data: {
email,
name: profile.displayName,
gender: null,
birth: null,
address: null,
detailAddress: null,
phoneNumber: null,
},
});

return {id: created.id, email: created.email, name: created.name};
};


// Google Strategy
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, cd) => {
try {
const user = await googleVerify(profile);

const jwtAccessToken = generateAccessToken(user);
const jwtRefreshToken = generateRefreshToken(user);

return cd(null, {
accessToken: jwtAccessToken,
refreshToken: jwtRefreshToken,
});
} catch (err) {
return cd(err);
}
}

)

const jwtOptions = {
// 요청 헤더의 'Autiorization'에서 'Bearer <token>' 토큰을 추출
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);
}
});
122 changes: 114 additions & 8 deletions src/controllers/mission.controller.js
Original file line number Diff line number Diff line change
@@ -1,31 +1,137 @@
import { StatusCodes } from "http-status-codes";
import {
bodyToMission,
bodyToChallenge,
bodyToChallenge,
paramsToCompleteMission
} from "../dtos/mission.dto.js";
import {
createMission,
challengeMission,
completeMission,
} from "../services/mission.service.js";

export const handleAddMission = async (req, res, next) => {
/*
#swagger.summary = '미션 추가 API';
#swagger.parameters['restaurantId'] = { description: '가게 ID', type: 'number' };
#swagger.requestBody = {
required: true,
content: {
"application/json": {
schema: {
type: "object",
properties: {
point: { type: "number", example: 500 },
content: { type: "string", example: "1만원 이상 구매" },
deadline: { type: "string", format: "date", example: "2025-12-31" }
}
}
}
}
};
#swagger.responses[201] = {
description: "미션 추가 성공",
content: {
"application/json": {
schema: {
type: "object",
properties: {
resultType: { type: "string", example: "SUCCESS" },
success: {
type: "object",
properties: {
id: { type: "number" },
content: { type: "string" }
}
}
}
}
}
}
};
*/
console.log("가게에 미션 추가를 요청했습니다!");
console.log("params (restaurantId):", req.params);
console.log("body (point, content, deadline):", req.body);

const missionData = bodyToMission(req.body, req.params);
const newMission = await createMission(missionData);

res.status(StatusCodes.CREATED).success(newMission);
};

export const handleChallengeMission = async (req, res, next) => {
/*
#swagger.summary = '미션 도전하기 API';
#swagger.security = [{ "bearerAuth": [] }]
#swagger.parameters['missionId'] = { description: '미션 ID', type: 'number' };
#swagger.requestBody = {
required: true,
content: {
"application/json": {
schema: {
type: "object",
properties: {
}
}
}
}
};
#swagger.responses[201] = {
description: "미션 도전 성공",
content: {
"application/json": {
schema: {
type: "object",
properties: {
resultType: { type: "string", example: "SUCCESS" },
success: {
type: "object",
properties: {
id: { type: "number" },
status: { type: "string", example: "진행중" }
}
}
}
}
}
}
};
*/
console.log("미션 도전하기를 요청했습니다");
console.log("params (missionId):", req.params);
console.log("body (userId):", req.body);
console.log("user (from jwt):", req.user); // 토큰 정보 확인용

const challengeData = bodyToChallenge(req.body, req.params);
const newChallenge = await challengeMission(challengeData);
// req.user.id를 DTO의 3번째 인자로 전달
const challengeData = bodyToChallenge(req.body, req.params, req.user.id);

const newChallenge = await challengeMission(challengeData);
res.status(StatusCodes.CREATED).success(newChallenge);
};

export const handleCompleteMission = async (req, res, next) => {
/*
#swagger.summary = '미션 완료하기 API';
#swagger.parameters['userMissionId'] = { description: '도전 내역 ID', type: 'number' };
#swagger.responses[200] = {
description: "미션 완료 성공",
content: {
"application/json": {
schema: {
type: "object",
properties: {
resultType: { type: "string", example: "SUCCESS" },
success: {
type: "object",
properties: {
id: { type: "number" },
status: { type: "string", example: "진행완료" }
}
}
}
}
}
}
};
*/
console.log("미션 완료를 요청했습니다!");
const { userMissionId } = paramsToCompleteMission(req.params);
const completedMission = await completeMission(userMissionId);
res.status(StatusCodes.OK).success(completedMission);
};
Loading