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
17 changes: 17 additions & 0 deletions root/week06/server/src/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
export class AppError extends Error {
code: string;
statusCode: number;

constructor(message: string, code: string, statusCode: number) {
super(message);
this.code = code;
this.statusCode = statusCode;
}
}

export const errors = {
USER_NOT_FOUND: new AppError("존재하지 않는 유저입니다.", "4040", 404),
STORE_NOT_FOUND: new AppError("존재하지 않는 가게입니다.", "4041", 404),
MISSION_NOT_FOUND: new AppError("존재하지 않는 미션입니다.", "4042", 404),
ALREADY_COMPLETED: new AppError("이미 완료된 미션입니다.", "4090", 409),
};
70 changes: 56 additions & 14 deletions root/week06/server/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,75 @@
import "dotenv/config";
import express from "express";
import express, { Request, Response, NextFunction } from "express";
import morgan from "morgan";
import cookieParser from "cookie-parser";
import * as repo from "./repository";
import { AppError } from "./errors";

const app = express();

// 미들웨어
app.use(morgan("dev"));
app.use(cookieParser());
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

// 성공 응답 헬퍼
const success = (result: any) => ({
isSuccess: true,
code: "2000",
message: "성공",
result,
});

// 내가 작성한 리뷰 목록
app.get("/api/users/:userId/reviews", async (req, res) => {
const reviews = await repo.getMyReviews(Number(req.params.userId));
res.json({ isSuccess: true, result: reviews });
app.get("/api/users/:userId/reviews", async (req, res, next) => {
try {
const result = await repo.getMyReviews(Number(req.params.userId));
res.json(success(result));
} catch (err) { next(err); }
});

// 특정 가게의 미션 목록
app.get("/api/stores/:storeId/missions", async (req, res) => {
const missions = await repo.getStoreMissions(Number(req.params.storeId));
res.json({ isSuccess: true, result: missions });
app.get("/api/stores/:storeId/missions", async (req, res, next) => {
try {
const result = await repo.getStoreMissions(Number(req.params.storeId));
res.json(success(result));
} catch (err) { next(err); }
});

// 내가 진행 중인 미션 목록
app.get("/api/users/:userId/missions", async (req, res) => {
const status = (req.query.status as string) ?? "IN_PROGRESS";
const missions = await repo.getMyMissions(Number(req.params.userId), status);
res.json({ isSuccess: true, result: missions });
app.get("/api/users/:userId/missions", async (req, res, next) => {
try {
const status = (req.query.status as string) ?? "IN_PROGRESS";
const result = await repo.getMyMissions(Number(req.params.userId), status);
res.json(success(result));
} catch (err) { next(err); }
});

// 미션 완료 처리
app.patch("/api/user-missions/:userMissionId/complete", async (req, res) => {
const updated = await repo.completeMission(Number(req.params.userMissionId));
res.json({ isSuccess: true, result: updated });
app.patch("/api/user-missions/:userMissionId/complete", async (req, res, next) => {
try {
const result = await repo.completeMission(Number(req.params.userMissionId));
res.json(success(result));
} catch (err) { next(err); }
});

// 에러 핸들러
app.use((err: any, req: Request, res: Response, next: NextFunction) => {
if (err instanceof AppError) {
res.status(err.statusCode).json({
isSuccess: false,
code: err.code,
message: err.message,
});
} else {
console.error(err);
res.status(500).json({
isSuccess: false,
code: "5000",
message: "서버 에러가 발생했습니다.",
});
}
});

app.listen(3000, () => console.log("Server is running at http://localhost:3000"));
32 changes: 23 additions & 9 deletions root/week06/server/src/repository.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,45 @@
import prisma from "./prisma";
import { errors } from "./errors";

// 내가 작성한 리뷰 목록
export const getMyReviews = (userId: number) =>
prisma.review.findMany({
export const getMyReviews = async (userId: number) => {
const user = await prisma.user.findUnique({ where: { id: userId } });
if (!user) throw errors.USER_NOT_FOUND;
return prisma.review.findMany({
where: { userId },
include: { store: true },
orderBy: { createdAt: "desc" },
});
};

// 특정 가게의 미션 목록
export const getStoreMissions = (storeId: number) =>
prisma.mission.findMany({
export const getStoreMissions = async (storeId: number) => {
const store = await prisma.store.findUnique({ where: { id: storeId } });
if (!store) throw errors.STORE_NOT_FOUND;
return prisma.mission.findMany({
where: { storeId },
orderBy: { createdAt: "desc" },
});
};

// 내가 진행 중인 미션 목록
export const getMyMissions = (userId: number, status: string) =>
prisma.userMission.findMany({
export const getMyMissions = async (userId: number, status: string) => {
const user = await prisma.user.findUnique({ where: { id: userId } });
if (!user) throw errors.USER_NOT_FOUND;
return prisma.userMission.findMany({
where: { userId, status: status as any },
include: { mission: { include: { store: true } } },
orderBy: { createdAt: "desc" },
});
};

// 미션 완료 처리
export const completeMission = (userMissionId: number) =>
prisma.userMission.update({
export const completeMission = async (userMissionId: number) => {
const userMission = await prisma.userMission.findUnique({ where: { id: userMissionId } });
if (!userMission) throw errors.MISSION_NOT_FOUND;
if (userMission.status === "COMPLETED") throw errors.ALREADY_COMPLETED;
return prisma.userMission.update({
where: { id: userMissionId },
data: { status: "COMPLETED" },
});
});
};
49 changes: 9 additions & 40 deletions root/week06/server/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,44 +1,13 @@
{
// Visit https://aka.ms/tsconfig to read more about this file
"compilerOptions": {
// File Layout
// "rootDir": "./src",
// "outDir": "./dist",

// Environment Settings
// See also https://aka.ms/tsconfig/module
"module": "nodenext",
"target": "esnext",
"types": [],
// For nodejs:
// "lib": ["esnext"],
// "types": ["node"],
// and npm install -D @types/node

// Other Outputs
"sourceMap": true,
"declaration": true,
"declarationMap": true,

// Stricter Typechecking Options
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,

// Style Options
// "noImplicitReturns": true,
// "noImplicitOverride": true,
// "noUnusedLocals": true,
// "noUnusedParameters": true,
// "noFallthroughCasesInSwitch": true,
// "noPropertyAccessFromIndexSignature": true,

// Recommended Options
"target": "ES2020",
"module": "commonjs",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"jsx": "react-jsx",
"verbatimModuleSyntax": true,
"isolatedModules": true,
"noUncheckedSideEffectImports": true,
"moduleDetection": "force",
"esModuleInterop": true,
"skipLibCheck": true,
}
}
"sourceMap": true
},
"include": ["src"]
}