Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#561 경로 즐겨찾기 기능 추가 #562

Open
wants to merge 1 commit into
base: dev
Choose a base branch
from
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
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
reportRouter,
roomRouter,
userRouter,
favoriteRoutesRouter,
} from "@/routes";

import { initializeApp as initializeFirebase } from "@/modules/fcm";
Expand Down Expand Up @@ -89,6 +90,7 @@ app.use("/notifications", notificationRouter);
app.use("/reports", reportRouter);
app.use("/rooms", roomRouter);
app.use("/users", userRouter);
app.use("/favoriteRoutes", favoriteRoutesRouter);

// [Middleware] 전역 에러 핸들러. 에러 핸들러는 router들보다 아래에 등록되어야 합니다.
app.use(errorHandler);
Expand Down
30 changes: 30 additions & 0 deletions src/modules/stores/mongo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import type {
AdminIPWhitelist,
AdminLog,
TaxiFare,
FavoriteRoute,
} from "@/types/mongo";

const userSchema = new Schema<User>({
Expand Down Expand Up @@ -263,6 +264,35 @@ database.on("error", function (err) {
mongoose.disconnect();
});

const favoriteRouteSchema = new Schema<FavoriteRoute>({
user: {
// 사용자 ID
type: Schema.Types.ObjectId,
ref: "User",
required: true,
},
from: {
// 출발지
type: Schema.Types.ObjectId,
ref: "Location",
required: true,
},
to: {
// 도착지
type: Schema.Types.ObjectId,
ref: "Location",
required: true,
},
createdAt: {
// 생성시간
type: Date,
default: Date.now, // 생성 시간 자동 기록
},
});

// 즐겨찾기 모델 생성
export const favoriteRouteModel = model("FavoriteRoute", favoriteRouteSchema);

export const connectDatabase = (mongoUrl: string) => {
database.on("disconnected", () => {
// 데이터베이스 연결이 끊어지면 5초 후 재연결을 시도합니다.
Expand Down
13 changes: 13 additions & 0 deletions src/routes/docs/schemas/favoriteRoutesSchema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { z } from "zod";
import patterns from "../../../modules/patterns";

const objectId = patterns.objectId;

export const favoriteRoutesZod = {
createHandler: z.object({
from: z.string().regex(objectId, "Invalid from location ID"),
to: z.string().regex(objectId, "Invalid to location ID"),
}),
};

export type FavoriteRoutesSchema = typeof favoriteRoutesZod;
29 changes: 29 additions & 0 deletions src/routes/favoriteRoutes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import express from "express";
import { validateBody, validateParams, validateQuery } from "@/middlewares/zod"; // Zod 검증 미들웨어
import { favoriteRoutesZod } from "./docs/schemas/favoriteRoutesSchema"; // Zod 스키마
import {
createHandler,
getHandler,
deleteHandler,
} from "@/services/favoriteRoutes";
import authMiddleware from "@/middlewares/auth"; // 인증 미들웨어

const router = express.Router();

// 라우터 접근 시 로그인 필요
router.use(authMiddleware);

// 즐겨찾기 생성
router.post(
"/create",
validateBody(favoriteRoutesZod.createHandler),
createHandler
);

// 즐겨찾기 조회
router.get("/", getHandler);

// 즐겨찾기 삭제
router.delete("/:id", deleteHandler);

export default router;
1 change: 1 addition & 0 deletions src/routes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ export { default as notificationRouter } from "./notifications";
export { default as reportRouter } from "./reports";
export { default as roomRouter } from "./rooms";
export { default as userRouter } from "./users";
export { default as favoriteRoutesRouter } from "./favoriteRoutes";
110 changes: 110 additions & 0 deletions src/services/favoriteRoutes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { Request, Response } from "express";
import { favoriteRouteModel } from "@/modules/stores/mongo";
import { Types } from "mongoose";
import { getLoginInfo } from "@/modules/auths/login"; // 로그인 정보 가져오기

// ✅ 즐겨찾기 생성 (POST)
export const createHandler = async (
req: Request,
res: Response
): Promise<void> => {
try {
const user = getLoginInfo(req); // 로그인된 사용자 정보 가져오기
if (!user.oid) {
res.status(401).json({ error: "Unauthorized: No valid session found" });
return;
}

const userId = user.oid;
const { from, to } = req.body;

const existingRoute = await favoriteRouteModel.findOne({
user: userId,
from,
to,
});
if (existingRoute) {
res
.status(400)
.json({ error: "FavoriteRoutes/create: route already exists" });
return;
}

const newRoute = new favoriteRouteModel({ user: userId, from, to });
await newRoute.save();

res.status(201).json(newRoute);
} catch (error) {
res
.status(500)
.json({ error: "FavoriteRoutes/create: internal server error" });
}
};

// ✅ 즐겨찾기 조회 (GET)
export const getHandler = async (
req: Request,
res: Response
): Promise<void> => {
try {
const user = getLoginInfo(req); // 로그인된 사용자 정보 가져오기
if (!user.oid) {
res.status(401).json({ error: "Unauthorized: No valid session found" });
return;
}

const userId = user.oid;
const routes = await favoriteRouteModel
.find({ user: userId })
.populate("from to");

res.status(200).json(routes);
} catch (error) {
res
.status(500)
.json({ error: "FavoriteRoutes/get: internal server error" });
}
};

// ✅ 즐겨찾기 삭제 (DELETE)
export const deleteHandler = async (
req: Request<{ id: string }>,
res: Response
): Promise<void> => {
try {
const user = getLoginInfo(req); // 로그인된 사용자 정보 가져오기

if (!user.oid) {
res.status(401).json({ error: "Unauthorized: No valid session found" });
return;
}

const userId = user.oid;
const { id } = req.params;

if (!id || !Types.ObjectId.isValid(id)) {
res
.status(400)
.json({ error: "Invalid request: Missing or invalid route ID" });
return;
}

const route = await favoriteRouteModel.findOneAndDelete({
_id: id,
user: userId,
});

if (!route) {
res
.status(400)
.json({ error: "FavoriteRoutes/delete: no corresponding route" });
return;
}

res.status(200).json(route);
} catch (error) {
res
.status(500)
.json({ error: "FavoriteRoutes/delete: internal server error" });
}
};
11 changes: 11 additions & 0 deletions src/types/mongo.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,3 +193,14 @@ export interface TaxiFare extends Document<Types.ObjectId> {
/** 예상 택시 요금. */
fare: number;
}

export interface FavoriteRoute extends Document {
/** 즐겨찾기를 등록한 사용자의 User ObjectID. */
user: Types.ObjectId;
/** 경로의 출발지 Location ObjectID. */
from: Types.ObjectId;
/** 경로의 도착지 Location ObjectID. */
to: Types.ObjectId;
/** 즐겨찾기 등록 시각. */
createdAt?: Date;
}