From a129d286487dfc3166d89369dad4cf119d51772c Mon Sep 17 00:00:00 2001 From: thxx Date: Thu, 6 Feb 2025 16:15:15 +0900 Subject: [PATCH] Add: routes/favoriteRoutes.ts, Add: services/favoriteRoutes.ts, Add: favoriteRoutesSchema.ts, Refactor: index.ts, Refactor: mongo.ts, Refactor: routes/index.ts, Refactor: mongo.d.ts --- src/index.ts | 2 + src/modules/stores/mongo.ts | 30 +++++ .../docs/schemas/favoriteRoutesSchema.ts | 13 +++ src/routes/favoriteRoutes.ts | 29 +++++ src/routes/index.ts | 1 + src/services/favoriteRoutes.ts | 110 ++++++++++++++++++ src/types/mongo.d.ts | 11 ++ 7 files changed, 196 insertions(+) create mode 100644 src/routes/docs/schemas/favoriteRoutesSchema.ts create mode 100644 src/routes/favoriteRoutes.ts create mode 100644 src/services/favoriteRoutes.ts diff --git a/src/index.ts b/src/index.ts index ced953e8..595c3d47 100644 --- a/src/index.ts +++ b/src/index.ts @@ -25,6 +25,7 @@ import { reportRouter, roomRouter, userRouter, + favoriteRoutesRouter, } from "@/routes"; import { initializeApp as initializeFirebase } from "@/modules/fcm"; @@ -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); diff --git a/src/modules/stores/mongo.ts b/src/modules/stores/mongo.ts index 41ccfa3e..4e352e86 100755 --- a/src/modules/stores/mongo.ts +++ b/src/modules/stores/mongo.ts @@ -14,6 +14,7 @@ import type { AdminIPWhitelist, AdminLog, TaxiFare, + FavoriteRoute, } from "@/types/mongo"; const userSchema = new Schema({ @@ -263,6 +264,35 @@ database.on("error", function (err) { mongoose.disconnect(); }); +const favoriteRouteSchema = new Schema({ + 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초 후 재연결을 시도합니다. diff --git a/src/routes/docs/schemas/favoriteRoutesSchema.ts b/src/routes/docs/schemas/favoriteRoutesSchema.ts new file mode 100644 index 00000000..d4581174 --- /dev/null +++ b/src/routes/docs/schemas/favoriteRoutesSchema.ts @@ -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; diff --git a/src/routes/favoriteRoutes.ts b/src/routes/favoriteRoutes.ts new file mode 100644 index 00000000..37fca1e0 --- /dev/null +++ b/src/routes/favoriteRoutes.ts @@ -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; diff --git a/src/routes/index.ts b/src/routes/index.ts index 83bc81dc..e375ce9f 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -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"; diff --git a/src/services/favoriteRoutes.ts b/src/services/favoriteRoutes.ts new file mode 100644 index 00000000..5c7e50f8 --- /dev/null +++ b/src/services/favoriteRoutes.ts @@ -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 => { + 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 => { + 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 => { + 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" }); + } +}; diff --git a/src/types/mongo.d.ts b/src/types/mongo.d.ts index 9848f4e0..24165bac 100644 --- a/src/types/mongo.d.ts +++ b/src/types/mongo.d.ts @@ -193,3 +193,14 @@ export interface TaxiFare extends Document { /** 예상 택시 요금. */ fare: number; } + +export interface FavoriteRoute extends Document { + /** 즐겨찾기를 등록한 사용자의 User ObjectID. */ + user: Types.ObjectId; + /** 경로의 출발지 Location ObjectID. */ + from: Types.ObjectId; + /** 경로의 도착지 Location ObjectID. */ + to: Types.ObjectId; + /** 즐겨찾기 등록 시각. */ + createdAt?: Date; +}