diff --git a/@types/express.d.ts b/@types/express.d.ts index 7134b6e..8ed2b8e 100644 --- a/@types/express.d.ts +++ b/@types/express.d.ts @@ -23,4 +23,4 @@ declare global { }): this; } } -} +} \ No newline at end of file diff --git a/package.json b/package.json index 954711b..4fe0f83 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "@google-cloud/vision": "^4.3.2", "@prisma/client": "^6.1.0", "@quixo3/prisma-session-store": "^3.1.13", + "@types/navermaps": "^3.7.8", "@types/cookie-parser": "^1.4.8", "cookie-parser": "^1.4.7", "@tsoa/runtime": "^6.6.0", diff --git a/src/controllers/challenge.controllers.ts b/src/controllers/challenge.controllers.ts index 2e2431b..4378934 100644 --- a/src/controllers/challenge.controllers.ts +++ b/src/controllers/challenge.controllers.ts @@ -1,9 +1,10 @@ import { Request, Response, NextFunction } from 'express'; -import { serviceDeleteChallenge, serviceUpdateChallenge } from '../services/challenge.services.js'; +import { serviceAcceptChallenge, serviceCompleteChallenge, serviceDeleteChallenge, serviceGetByUserId, serviceUpdateChallenge } from '../services/challenge.services.js'; import { StatusCodes } from 'http-status-codes'; import { getIdNumber } from '../utils/challenge.utils.js'; - - +import { Challenge } from '@prisma/client'; +import { ResponseFromGetByUserIdReform } from '../models/challenge.entities.js'; +import { DataValidationError } from '../errors.js'; export const handleUpdateChallenge = async ( req: Request, @@ -52,9 +53,17 @@ export const handleUpdateChallenge = async ( } }; */ - serviceUpdateChallenge(req.body); - res.status(StatusCodes.OK).success(req.body); - console.log(req.body); + try{ + if(!req.body){ + throw new DataValidationError({reason: '업데이트 내용이 없습니다.'}); + } + + serviceUpdateChallenge(req.body); + res.status(StatusCodes.OK).success(req.body); + console.log(req.body); + } catch(error){ + next(error); + } }; export const handleRemoveChallenge = async ( @@ -63,9 +72,9 @@ export const handleRemoveChallenge = async ( next: NextFunction ): Promise => { /* - #swagger.tags = ['challenge-controller'] + #swagger.tags = ['challenge-controller']; #swagger.summary = '챌린지 삭제 API'; - #swagger.description = '챌린지를 삭제하는 API입니다.' + #swagger.description = '챌린지를 삭제하는 API입니다.'; #swagger.requestBody = { required: true, content: { @@ -101,7 +110,204 @@ export const handleRemoveChallenge = async ( } }; */ - serviceDeleteChallenge(getIdNumber(req.body)); - res.status(StatusCodes.OK).success(req.body); - console.log(req.body); + try{ + if(!req.body){ + throw new DataValidationError({reason: '삭제할 챌린지의 정보가 없습니다.'}); + } + serviceDeleteChallenge(getIdNumber(req.body)); + res.status(StatusCodes.OK).success(req.body); + console.log(req.body); + } catch(error){ + next(error); + } +}; + +export const handleAcceptChallenge = async ( + req: Request<{id: string}>, + res: Response, + next: NextFunction +): Promise => { + /* + #swagger.tags = ['challenge-controller']; + #swagger.summary = '챌린지 수락 API'; + #swagger.description = '챌린지를 수락하는 API입니다.'; + #swagger.parameters['id'] = { + in: 'path', + required: true, + description: "챌린지 ID 입력", + '@schema': { + type: "string", + } + }; + #swagger.requestBody = { + required: false + }; + #swagger.responses[200] = { + description: "챌린지 수락 성공 응답", + content: { + "application/json": { + schema: { + type: "object", + properties: { + resultType: { type: "string", example: "SUCCESS" }, + error: { type: "object", nullable: true, example: null }, + success: { + type: "object", + properties: { + id: { type: "string", example: "1" }, + title: { type: "string", example: "challenge-title" }, + context: { type: "string", example: "challenge-context" }, + requiredCount: { type: "number", example: 10 }, + remainingCount: { type: "number", example: 10 }, + userId: { type: "string", example: "1" }, + createdAt: { type: "string", format: "date-time", example: "2025-01-20T18:19:47.415Z" }, + updatedAt: { type: "string", format: "date-time", example: "2025-01-20T18:19:47.415Z" }, + acceptedAt: { type: "string", format: "date-time", example: "2025-01-20T18:19:47.415Z" }, + completedAt: { type: "string", format: "date-time", example: "2025-01-20T18:19:47.415Z" }, + status: { type: "number", example: 1 } + } + } + } + } + } + } + }; + */ + try{ + if(!req.params.id){ + throw new DataValidationError({reason: '올바른 parameter값이 필요합니다.'}); + } + + const result: Challenge = await serviceAcceptChallenge(BigInt(req.params.id)); + res.status(StatusCodes.OK).success(result); + } catch(error){ + next(error); + } +}; + +export const handleCompleteChallenge = async ( + req: Request<{id: string}>, + res: Response, + next: NextFunction +): Promise => { + /* + #swagger.tags = ['challenge-controller']; + #swagger.summary = '챌린지 완료 API'; + #swagger.description = '챌린지를 완료하는 API입니다.'; + #swagger.parameters['id'] = { + in: 'path', + required: true, + description: "챌린지 ID 입력", + '@schema': { + type: "string", + } + }; + #swagger.requestBody = { + required: false + }; + #swagger.responses[200] = { + description: "챌린지 완료 성공 응답", + content: { + "application/json": { + schema: { + type: "object", + properties: { + resultType: { type: "string", example: "SUCCESS" }, + error: { type: "object", nullable: true, example: null }, + success: { + type: "object", + properties: { + id: { type: "string", example: "1" }, + title: { type: "string", example: "challenge-title" }, + context: { type: "string", example: "challenge-context" }, + requiredCount: { type: "number", example: 10 }, + remainingCount: { type: "number", example: 10 }, + userId: { type: "string", example: "1" }, + createdAt: { type: "string", format: "date-time", example: "2025-01-20T18:19:47.415Z" }, + updatedAt: { type: "string", format: "date-time", example: "2025-01-20T18:19:47.415Z" }, + acceptedAt: { type: "string", format: "date-time", example: "2025-01-20T18:19:47.415Z" }, + completedAt: { type: "string", format: "date-time", example: "2025-01-20T18:19:47.415Z" }, + status: { type: "number", example: 1} + } + } + } + } + } + } + }; + */ + try{ + if(!req.params.id){ + throw new DataValidationError({reason: '올바른 parameter값이 필요합니다.'}); + } + + const result: Challenge = await serviceCompleteChallenge(BigInt(req.params.id)); + res.status(StatusCodes.OK).success(result); + } catch(error){ + next(error); + } +}; + +export const handleGetByUserId = async ( + req: Request<{userId: string}>, + res: Response, + next: NextFunction +): Promise => { + /* + #swagger.tags = ['challenge-controller']; + #swagger.summary = '특정 유저의 챌린지 조회 API'; + #swagger.description = '특정 유저의 모든 챌린지를 조회하는 API입니다.'; + #swagger.parameters['userId'] = { + in: 'path', + required: true, + description: "유저 ID 입력", + '@schema': { + type: "string" + } + }; + #swagger.requestBody = { + required: false + }; + #swagger.responses[200] = { + description: "유저 챌린지 조회 성공 응답", + content: { + "application/json": { + schema: { + type: "object", + properties: { + resultType: { type: "string", example: "SUCCESS" }, + error: { type: "object", nullable: true, example: null }, + success: { + type: "array", + items: { + type: "object", + properties: { + id: {type: "string", example: "1"}, + title: {type: "string"}, + context: {type: "string"}, + challengeLocation: {type: "string"}, + challengeDate: {type: "string", format: "date-time"}, + requiredCount: {type: "number"}, + remainingCount: {type: "number"}, + userId: {type: "string"}, + createdAt: {type: "string", format: "date-time"}, + updatedAt: {type: "string", format: "date-time"}, + acceptedAt: {type: "string", format: "date-time"}, + completedAt: {type: "string", format: "date-time"}, + status: {type: "number"} + } + } + } + } + } + } + } + }; + */ + try{ + const result: ResponseFromGetByUserIdReform[] = await serviceGetByUserId(BigInt(req.params.userId)); + res.status(StatusCodes.OK).success(result); + } catch(error){ + next(error); + } }; \ No newline at end of file diff --git a/src/controllers/challenge.location.controllers.ts b/src/controllers/challenge.location.controllers.ts index 8c5c973..292d1fc 100644 --- a/src/controllers/challenge.location.controllers.ts +++ b/src/controllers/challenge.location.controllers.ts @@ -4,6 +4,7 @@ import { serviceCreateNewLocationChallenge, serviceGetLocationChallenge, service import { StatusCodes } from 'http-status-codes'; import { LocationChallengeCreation, ResponseFromChallenge } from '../models/challenge.entities.js'; import { bodyToLocationCreation } from '../dtos/challenge.dtos.js'; +import { DataValidationError } from '../errors.js'; export const handleNewLocationChallenge = async ( req: Request, @@ -62,10 +63,18 @@ export const handleNewLocationChallenge = async ( } }; */ - const data: LocationChallengeCreation = bodyToLocationCreation(req.body); - const result: ResponseFromChallenge = await serviceCreateNewLocationChallenge(data); - res.status(StatusCodes.OK).success(result); - console.log(req.body); + try{ + if(!req.body){ + throw new DataValidationError({reason: '위치 챌린지를 생성할 데이터가 없습니다.'}); + } + + const data: LocationChallengeCreation = bodyToLocationCreation(req.body); + const result: ResponseFromChallenge = await serviceCreateNewLocationChallenge(data); + res.status(StatusCodes.OK).success(result); + console.log(req.body); + } catch(error){ + next(error); + } }; export const handleGetLocationChallenge = async ( @@ -120,9 +129,17 @@ export const handleGetLocationChallenge = async ( } }; */ - const result = await serviceGetLocationChallenge(BigInt(req.params.id)); - res.status(StatusCodes.OK).success(result); - console.log(req.params.id); + try{ + if(!req.params.id){ + throw new DataValidationError({reason: '올바른 parameter 값이 필요합니다.'}); + } + + const result = await serviceGetLocationChallenge(BigInt(req.params.id)); + res.status(StatusCodes.OK).success(result); + console.log(req.params.id); + } catch(error){ + next(error); + } }; export const handleLocationLogic = async ( @@ -183,6 +200,10 @@ export const handleLocationLogic = async ( } }; */ - const result = await serviceLocationLogic(req.body); - res.status(StatusCodes.OK).success(result); + try{ + const result = await serviceLocationLogic(req.body); + res.status(StatusCodes.OK).success(result); + } catch(error){ + next(error); + } }; \ No newline at end of file diff --git a/src/controllers/challenge.weekly.controllers.ts b/src/controllers/challenge.weekly.controllers.ts index 96afebf..df78c8c 100644 --- a/src/controllers/challenge.weekly.controllers.ts +++ b/src/controllers/challenge.weekly.controllers.ts @@ -3,6 +3,7 @@ import { ResponseFromChallenge, WeeklyChallengeCreation } from '../models/challe import { bodyToWeeklyCreation } from '../dtos/challenge.dtos.js'; import { StatusCodes } from 'http-status-codes'; import { serviceCreateNewWeeklyChallenge, serviceGetWeeklyChallenge } from '../services/challenge.weekly.services.js'; +import { DataValidationError } from '../errors.js'; export const handleNewWeeklyChallenge = async( req: Request, @@ -61,10 +62,18 @@ export const handleNewWeeklyChallenge = async( } }; */ - const data: WeeklyChallengeCreation = bodyToWeeklyCreation(req.body); - const result: ResponseFromChallenge = await serviceCreateNewWeeklyChallenge(data); - res.status(StatusCodes.OK).success(result); - console.log(req.headers); + try{ + if(!req.body){ + throw new DataValidationError({reason: '날짜 챌린지를 생성할 데이터가 없습니다.'}); + } + + const data: WeeklyChallengeCreation = bodyToWeeklyCreation(req.body); + const result: ResponseFromChallenge = await serviceCreateNewWeeklyChallenge(data); + res.status(StatusCodes.OK).success(result); + console.log(req.headers); + } catch(error){ + next(error); + } }; export const handleGetWeeklyChallenge = async( @@ -119,6 +128,14 @@ export const handleGetWeeklyChallenge = async( } }; */ - const result = await serviceGetWeeklyChallenge(BigInt(req.params.id)); - res.status(StatusCodes.OK).success(result); + try{ + if(!req.params.id){ + throw new DataValidationError({reason: '올바른 parameter값이 필요합니다.'}); + } + + const result = await serviceGetWeeklyChallenge(BigInt(req.params.id)); + res.status(StatusCodes.OK).success(result); + } catch(error){ + next(error); + } }; \ No newline at end of file diff --git a/src/controllers/memo-createFolderOCR.Controller.ts b/src/controllers/memo-createFolderOCR.Controller.ts index 7ffe2b1..0d799b3 100644 --- a/src/controllers/memo-createFolderOCR.Controller.ts +++ b/src/controllers/memo-createFolderOCR.Controller.ts @@ -13,7 +13,7 @@ export const createFolderOCR = async ( #swagger.tags = ['memo-ai'] #swagger.summary = '폴더 생성 및 OCR 수행' #swagger.description = '새로운 폴더를 생성하고, 이미지에서 OCR 텍스트를 추출하여 이미지와 텍스트를 저장하는 API입니다.' - #swagger.requestBody = {W + #swagger.requestBody = { required: true, content: { "application/json": { @@ -55,7 +55,7 @@ export const createFolderOCR = async ( properties: { folder_id: { type: "string", example: "1" }, image_text: { type: "string", example: "이번 수업 시간은 사회 과학 시간이다." }, - + } } } } diff --git a/src/dtos/challenge.dtos.ts b/src/dtos/challenge.dtos.ts index 421b439..d8cf921 100644 --- a/src/dtos/challenge.dtos.ts +++ b/src/dtos/challenge.dtos.ts @@ -1,5 +1,5 @@ import { Challenge, LocationChallenge, DateChallenge } from '@prisma/client'; -import { BodyToLocationCreation, BodyToWeeklyCreation, PhotoInfo } from '../models/challenge.entities.js'; +import { BodyToLocationCreation, BodyToWeeklyCreation, PhotoInfo, ResponseFromChallenge, ResponseFromGetByUserId, ResponseFromGetByUserIdReform, ResponseFromLocationChallenge, ResponseFromWeeklyChallenge } from '../models/challenge.entities.js'; export const responseFromLocationChallenge = ({ location, @@ -7,7 +7,7 @@ export const responseFromLocationChallenge = ({ }: { location: LocationChallenge; challenge: Challenge; -}) => { +}): ResponseFromLocationChallenge => { const {id, title, context, requiredCount, remainingCount, userId, createdAt, updatedAt, acceptedAt, completedAt, status } = challenge; @@ -35,7 +35,7 @@ export const responseFromWeeklyChallenge = ({ }: { weekly: DateChallenge; challenge: Challenge -}) => { +}): ResponseFromWeeklyChallenge => { const {id, title, context, requiredCount, remainingCount, userId, createdAt, updatedAt, acceptedAt, completedAt, status } = challenge; @@ -58,7 +58,7 @@ export const responseFromWeeklyChallenge = ({ }; }; -export const responseFromChallenge = (challenge: Challenge) => { +export const responseFromChallenge = (challenge: Challenge): ResponseFromChallenge => { const {id, title, context, requiredCount, remainingCount, userId, createdAt, updatedAt, acceptedAt, completedAt, status } = challenge; @@ -78,7 +78,33 @@ export const responseFromChallenge = (challenge: Challenge) => { }; }; -export const bodyToLocationLogic = (photo: PhotoInfo[]) => { +export const responseFromGetByUserId = ( + challenges: ResponseFromGetByUserId[] +): ResponseFromGetByUserIdReform[] => { + return challenges.map((value: ResponseFromGetByUserId) => { + const {id, title, context, requiredCount, remainingCount, userId, + createdAt, updatedAt, acceptedAt, completedAt, status, locationChallenge, dateChallenge + } = value; + + return { + id: id.toString(), + title, + context, + challengeLocation: locationChallenge?.challengeLocation, + challengeDate: dateChallenge?.challengeDate, + requiredCount, + remainingCount, + userId: userId.toString(), + createdAt, + updatedAt, + acceptedAt, + completedAt, + status + }; + }); +}; + +export const bodyToLocationLogic = (photo: PhotoInfo[]): PhotoInfo[] => { return photo; }; diff --git a/src/errors.ts b/src/errors.ts index 671a9ca..54cd5e5 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -115,7 +115,7 @@ export class DataValidationError extends BaseError { // 생성 관련 에러 (CHL) export class LocationChallengeCreationError extends BaseError { - constructor(details: {latitude: number; longitude: number}) { + constructor(details: {reason: string}) { super( 400, 'CHL-400', @@ -126,14 +126,14 @@ export class LocationChallengeCreationError extends BaseError { } // 업데이트 관련 에러 (CHL-Challenge) -export class LocationChallengeUpdateError extends BaseError { +export class ChallengeUpdateError extends BaseError { constructor(details: {challengeId: bigint; userId?: bigint}) { - super(400, 'CHL-400', '위치 기반 챌린지 업데이트 실패.', details); + super(400, 'CHL-400', '챌린지 업데이트 실패.', details); } } // 삭제 관련 에러 (CHL) -export class LocationChallengeDeletionError extends BaseError { +export class ChallengeDeletionError extends BaseError { constructor(details: {challengeId: bigint}) { super(400, 'CHL-400', '위치 기반 챌린지 삭제 실패.', details); } @@ -146,6 +146,46 @@ export class LocationChallengeNotFoundError extends BaseError { } } +// 챌린지 수락 관련 에러 (CHL) +export class ChallengeAcceptError extends BaseError { + constructor(details: {challengeId: bigint; reason: string}) { + super(400, 'CHL-400', '해당 챌린지를 수락할 수 없습니다.', details); + } +} + +// 챌린지 완료 관련 에러 (CHL) +export class ChallengeCompleteError extends BaseError { + constructor(details: {challengeId: bigint; reason: string}){ + super(400, 'CHL-400', '챌린지 완료 실패', details); + } +} + +// 챌린지 조회 관련 에러 (CHL) +export class ChallengeNotFoundError extends BaseError { + constructor(details: {userId: bigint}){ + super(404, 'CHL-404', '해당 유저의 챌린지를 찾을 수 없습니다.', details); + } +} + +// 날짜 챌린지 생성 관련 에러 (CHL) +export class DateChallengeCreationError extends BaseError { + constructor(details: {reason: string}) { + super( + 400, + 'CHL-400', + '날짜 기반 챌린지 생성 중 오류가 발생했습니다.', + details, + ); + } +} + +// 조회 관련 에러 (CHL) +export class DateChallengeNotFoundError extends BaseError { + constructor(details: {challengeId: bigint}) { + super(404, 'CHL-404', '해당 날짜 기반 챌린지를 찾을 수 없습니다.', details); + } +} + // 사진 데이터 관련 에러 (PHO-photo) export class PhotoDataNotFoundError extends BaseError { constructor(details?: ErrorDetails) { diff --git a/src/models/challenge.entities.ts b/src/models/challenge.entities.ts index 870988c..7df5bb2 100644 --- a/src/models/challenge.entities.ts +++ b/src/models/challenge.entities.ts @@ -70,7 +70,7 @@ export interface ResponseFromWeeklyChallenge { userId: string; title: string; context: string; - challengeDate: string; + challengeDate: Date; requiredCount: number; remainingCount: number; createdAt: Date; @@ -89,6 +89,43 @@ export interface ResponseFromUpdateChallenge { status: number; } +export interface ResponseFromGetByUserId { + locationChallenge: { + challengeLocation: string; + } | null; + dateChallenge: { + challengeDate: Date; + } | null; + + id: bigint; + userId: bigint; + title: string; + context: string; + requiredCount: number; + remainingCount: number; + createdAt: Date; + updatedAt: Date | null; + acceptedAt: Date | null; + completedAt: Date | null; + status: number; +} + +export interface ResponseFromGetByUserIdReform { + id: string; + userId: string; + title: string; + context: string; + challengeLocation: string | undefined; + challengeDate: Date | undefined; + requiredCount: number; + remainingCount: number; + createdAt: Date; + updatedAt: Date | null; + acceptedAt: Date | null; + completedAt: Date | null; + status: number; +} + export interface PhotoInfo { id: string; displayName: string; diff --git a/src/repositories/challenge.repositories.ts b/src/repositories/challenge.repositories.ts index cc1b7ab..6e671fc 100644 --- a/src/repositories/challenge.repositories.ts +++ b/src/repositories/challenge.repositories.ts @@ -1,6 +1,7 @@ import { prisma } from '../db.config.js'; -import { ChallengeModify, LocationChallengeCreation } from '../models/challenge.entities.js'; -import { Challenge, LocationChallenge } from '@prisma/client'; +import { ChallengeAcceptError, ChallengeCompleteError, ChallengeNotFoundError } from '../errors.js'; +import { ChallengeModify, LocationChallengeCreation, ResponseFromGetByUserId } from '../models/challenge.entities.js'; +import { Challenge, DateChallenge, LocationChallenge } from '@prisma/client'; export const newLocationChallenge = async (data: LocationChallengeCreation): Promise => { const existingChallenge = await prisma.challenge.findFirst({ @@ -59,19 +60,91 @@ export const deleteChallenge = async (data: bigint): Promise => { }; export const getChallenge = async (data: bigint): Promise => { - const idBigNum = data; const userChallenge = await prisma.challenge.findFirst({ - where: {id: idBigNum} + where: {id: data} }); return userChallenge; }; export const getLocation = async (data: bigint): Promise => { - const idBigNum = data; const challengeLocation = await prisma.locationChallenge.findFirst({ - where: {challengeId: idBigNum} + where: {challengeId: data} }); return challengeLocation; +}; + +export const acceptChallenge = async (data: bigint): Promise => { + const state: {status: number} | null = await prisma.challenge.findFirst({ + where: {id: data}, + select: { + status: true + } + }); + + if(!state){ + throw new ChallengeAcceptError({challengeId: data, reason: '챌린지가 존재하지 않습니다.'}); + } + + if(state.status === 2 || state.status === 3){ + throw new ChallengeAcceptError({challengeId: data, reason: '챌린지가 이미 수락되거나 완료되었습니다.'}); + } + + const updatedChallenge = await prisma.challenge.update({ + where: {id: data}, + data: { + status: 2, + acceptedAt: new Date() + } //status: 1 = created, 2 = accepted, 3 = completed + }); + + return updatedChallenge; +}; + +export const completeChallenge = async (data: bigint): Promise => { + const state: {status: number} | null = await prisma.challenge.findFirst({ + where: {id: data}, + select: { + status: true + } + }); + + if(!state){ + throw new ChallengeCompleteError({challengeId: data, reason: '챌린지가 존재하지 않습니다.'}); + } + + if(state.status !== 2){ + throw new ChallengeCompleteError({challengeId: data, reason: '챌린지가 수락되지 않았습니다.'}); + } + + const updatedChallenge = await prisma.challenge.update({ + where: {id: data}, + data: { + status: 3, + completedAt: new Date() + } //status: 1 = created, 2 = accepted, 3 = completed + }); + + return updatedChallenge; +}; + +export const getChallengeByUserId = async (data: bigint): Promise => { + const challenges = await prisma.challenge.findMany({ + where: {userId: data}, + include: { + locationChallenge: { + select: { + challengeLocation: true + } + }, + dateChallenge: { + select: { + challengeDate: true + } + } + } + }); + + return challenges; }; \ No newline at end of file diff --git a/src/routers/challenge.router.ts b/src/routers/challenge.router.ts index d5b771b..cc5b528 100644 --- a/src/routers/challenge.router.ts +++ b/src/routers/challenge.router.ts @@ -1,11 +1,14 @@ import express from 'express'; export const challengeRouter = express.Router(); import { handleGetLocationChallenge, handleLocationLogic, handleNewLocationChallenge } from '../controllers/challenge.location.controllers.js'; -import { handleUpdateChallenge, handleRemoveChallenge } from '../controllers/challenge.controllers.js'; +import { handleUpdateChallenge, handleRemoveChallenge, handleAcceptChallenge, handleCompleteChallenge, handleGetByUserId } from '../controllers/challenge.controllers.js'; import { handleGetWeeklyChallenge, handleNewWeeklyChallenge } from '../controllers/challenge.weekly.controllers.js'; challengeRouter.patch('/update', handleUpdateChallenge); challengeRouter.delete('/delete', handleRemoveChallenge); +challengeRouter.patch('/accept/:id', handleAcceptChallenge); +challengeRouter.patch('/complete/:id', handleCompleteChallenge); +challengeRouter.get('/get/:userId', handleGetByUserId); challengeRouter.get('/location_challenge/get/:id', handleGetLocationChallenge); challengeRouter.post('/location_logic/test', handleLocationLogic); challengeRouter.post('/location_challenge/create', handleNewLocationChallenge); diff --git a/src/services/challenge.location.services.ts b/src/services/challenge.location.services.ts index cfbe8c9..db2ae85 100644 --- a/src/services/challenge.location.services.ts +++ b/src/services/challenge.location.services.ts @@ -3,62 +3,80 @@ import { LocationChallengeCreation, PhotoInfo, ResponseFromChallenge, ResponseFr import { getChallenge, getLocation, newLocationChallenge } from '../repositories/challenge.repositories.js'; import { responseFromChallenge, responseFromLocationChallenge } from '../dtos/challenge.dtos.js'; import { getHashedLocation } from '../utils/challenge.utils.js'; +import { LocationChallengeCreationError, LocationChallengeNotFoundError, PhotoDataNotFoundError } from '../errors.js'; export const serviceCreateNewLocationChallenge = async (data: LocationChallengeCreation): Promise => { - const newChallenge: Challenge | null = await newLocationChallenge(data); - if(newChallenge === null){ - throw new Error('Existing challenge.'); - } + try{ + const newChallenge: Challenge | null = await newLocationChallenge(data); + if(newChallenge === null){ + throw new LocationChallengeCreationError({reason: '이미 존재하는 챌린지입니다.'}); + } - return responseFromChallenge(newChallenge); + return responseFromChallenge(newChallenge); + } catch (error){ + console.error('Error creating location challenge:', error); + throw error; + } }; export const serviceGetLocationChallenge = async (data: bigint): Promise => { - const challenge = await getChallenge(data); - const location = await getLocation(data); + try{ + const challenge = await getChallenge(data); + const location = await getLocation(data); - if(challenge === null || location === null){ - throw new Error('No challenge found.'); - } + if(challenge === null || location === null){ + throw new LocationChallengeNotFoundError({challengeId: data}); + } - console.log(challenge); - console.log(location); + console.log(challenge); + console.log(location); - return responseFromLocationChallenge({location, challenge}); + return responseFromLocationChallenge({location, challenge}); + } catch(error){ + console.error('Error getting location challenge:', error); + throw error; + } }; export const serviceLocationLogic = async (data: PhotoInfo[]): Promise => { - if(!data){ - throw new Error('No photos data found.'); - } - - let isCreateChallenge: boolean = false; - let challengePics: PhotoInfo[] = []; - const iterator: ArrayIterator<[number, PhotoInfo]> = data.entries(); - let hashPosition: Map = new Map(); //hash된 위치의 map 각 key를 위치로, value를 개수로 설정함 - - for(const [index, photo] of iterator){ - if(photo.latitude === null || photo.longitude === null){ - continue; + try{ + if(!data || data.length === 0){ + throw new PhotoDataNotFoundError({ + reason: '사진 데이터가 존재하지 않습니다.', + }); } - photo.location = getHashedLocation(photo.latitude + ' ' + photo.longitude); + let isCreateChallenge: boolean = false; + let challengePics: PhotoInfo[] = []; + const iterator: ArrayIterator<[number, PhotoInfo]> = data.entries(); + let hashPosition: Map = new Map(); //hash된 위치의 map 각 key를 위치로, value를 개수로 설정함 - const currentValue: number = hashPosition.get(photo.location) || 0; - hashPosition.set(photo.location, currentValue + 1); - console.log(photo.location + ' ' + hashPosition.get(photo.location)); - } + for(const [index, photo] of iterator){ + if(photo.latitude === null || photo.longitude === null){ + continue; + } - hashPosition.forEach((value: number, key: string, map: Map) => { - if(value > 0 && value < 5){ - isCreateChallenge = true; - challengePics = data.filter((photo: PhotoInfo) => photo.location === key); + photo.location = getHashedLocation(photo.latitude + ' ' + photo.longitude); + + const currentValue: number = hashPosition.get(photo.location) || 0; + hashPosition.set(photo.location, currentValue + 1); + console.log(photo.location + ' ' + hashPosition.get(photo.location)); } - }); - console.log('created: ' + isCreateChallenge); - console.log(challengePics); + hashPosition.forEach((value: number, key: string, map: Map) => { + if(value > 0 && value < 5){ + isCreateChallenge = true; + challengePics = data.filter((photo: PhotoInfo) => photo.location === key); + } + }); + + console.log('created: ' + isCreateChallenge); + console.log(challengePics); - return challengePics; + return challengePics; + } catch (error) { + console.error('Error processing location logic:', error); + throw error; + } }; \ No newline at end of file diff --git a/src/services/challenge.services.ts b/src/services/challenge.services.ts index 465b522..6945193 100644 --- a/src/services/challenge.services.ts +++ b/src/services/challenge.services.ts @@ -1,15 +1,15 @@ import { Challenge } from '@prisma/client'; -import { responseFromChallenge } from '../dtos/challenge.dtos.js'; -import { ChallengeModify, ResponseFromUpdateChallenge } from '../models/challenge.entities.js'; -import { updateChallenge, deleteChallenge } from '../repositories/challenge.repositories.js'; -import { LocationChallengeUpdateError, LocationChallengeDeletionError } from '../errors.js'; +import { responseFromChallenge, responseFromGetByUserId } from '../dtos/challenge.dtos.js'; +import { ChallengeModify, ResponseFromGetByUserId, ResponseFromGetByUserIdReform, ResponseFromUpdateChallenge } from '../models/challenge.entities.js'; +import { updateChallenge, deleteChallenge, acceptChallenge, getChallengeByUserId, completeChallenge } from '../repositories/challenge.repositories.js'; +import { ChallengeUpdateError, ChallengeDeletionError, ChallengeAcceptError, ChallengeCompleteError, ChallengeNotFoundError } from '../errors.js'; export const serviceUpdateChallenge = async (data: ChallengeModify): Promise => { try{ const updatedChallenge: Challenge | null = await updateChallenge(data); if (updatedChallenge === null) { - throw new LocationChallengeUpdateError({challengeId: data.id}); + throw new ChallengeUpdateError({challengeId: data.id}); } console.log( @@ -18,7 +18,7 @@ export const serviceUpdateChallenge = async (data: ChallengeModify): Promise => { - try { - const deletedChallengeId: bigint | null = - await deleteChallenge(data); + try { + const deletedChallengeId: bigint | null = + await deleteChallenge(data); + + if (deletedChallengeId === null) { + throw new ChallengeDeletionError({challengeId: data}); + } + + console.log('Deleted challenge with ID:', deletedChallengeId); + } catch (error) { + console.error('Error deleting challenge:', error); + throw error; + } +}; + +export const serviceAcceptChallenge = async (data: bigint): Promise => { + try{ + const accepted: Challenge | null = await acceptChallenge(data); + + if(!accepted){ + throw new ChallengeAcceptError({challengeId: data, reason: '챌린지를 수락할 수 없습니다.'}); + } - if (deletedChallengeId === null) { - throw new LocationChallengeDeletionError({challengeId: data}); + return accepted; + } catch(error){ + console.error('Error accepting challenge:', error); + throw error; } +}; - console.log('Deleted challenge with ID:', deletedChallengeId); - } catch (error) { - console.error('Error deleting location challenge:', error); - throw error; - } +export const serviceCompleteChallenge = async (data: bigint): Promise => { + try{ + const completed: Challenge | null = await completeChallenge(data); + + if(!completed){ + throw new ChallengeCompleteError({challengeId: data, reason: '해당 챌린지를 완료할 수 없습니다.'}); + } + + return completed; + } catch(error){ + console.error('Error completing challenge:', error); + throw error; + } +}; + +export const serviceGetByUserId = async (data: bigint): Promise => { + try{ + const challenges: ResponseFromGetByUserId[] = await getChallengeByUserId(data); + + if(challenges.length === 0){ + throw new ChallengeNotFoundError({userId: data}); + } + + return responseFromGetByUserId(challenges); + } catch (error){ + console.error('Error getting user challenges:', error); + throw error; + } }; \ No newline at end of file diff --git a/src/services/challenge.weekly.services.ts b/src/services/challenge.weekly.services.ts index cb8211c..e137861 100644 --- a/src/services/challenge.weekly.services.ts +++ b/src/services/challenge.weekly.services.ts @@ -3,23 +3,34 @@ import { ResponseFromChallenge, WeeklyChallengeCreation } from '../models/challe import { getWeekly, newWeeklyChallenge } from '../repositories/weekly.repositories.js'; import { responseFromChallenge, responseFromWeeklyChallenge } from '../dtos/challenge.dtos.js'; import { getChallenge } from '../repositories/challenge.repositories.js'; +import { DateChallengeCreationError, DateChallengeNotFoundError } from '../errors.js'; export const serviceCreateNewWeeklyChallenge = async(data: WeeklyChallengeCreation): Promise => { - const newChallenge: Challenge | null = await newWeeklyChallenge(data); - if(newChallenge === null){ - throw new Error('Existing challenge.'); - } + try{ + const newChallenge: Challenge | null = await newWeeklyChallenge(data); + if(newChallenge === null){ + throw new DateChallengeCreationError({reason: '이미 존재하는 챌린지입니다.'}); + } - return responseFromChallenge(newChallenge); + return responseFromChallenge(newChallenge); + } catch(error){ + console.error('Error creating date challenge:', error); + throw error; + } }; export const serviceGetWeeklyChallenge = async(data: bigint) => { - const challenge: Challenge | null = await getChallenge(data); - const weekly: DateChallenge | null = await getWeekly(data); + try{ + const challenge: Challenge | null = await getChallenge(data); + const weekly: DateChallenge | null = await getWeekly(data); - if(challenge === null || weekly === null){ - throw new Error(`Could not get Challenge ID ${data}.`); - } + if(challenge === null || weekly === null){ + throw new DateChallengeNotFoundError({challengeId: data}); + } - return responseFromWeeklyChallenge({weekly, challenge}); + return responseFromWeeklyChallenge({weekly, challenge}); + } catch(error){ + console.error('Error getting date challenge:', error); + throw error; + } }; \ No newline at end of file diff --git a/swagger/openapi.json b/swagger/openapi.json index 5045a07..e17fdde 100644 --- a/swagger/openapi.json +++ b/swagger/openapi.json @@ -531,11 +531,105 @@ "summary": "폴더 생성 및 OCR 수행", "description": "새로운 폴더를 생성하고, 이미지에서 OCR 텍스트를 추출하여 이미지와 텍스트를 저장하는 API입니다.", "responses": { - "default": { - "description": "" + "201": { + "description": "폴더 생성 및 텍스트 변환", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "resultType": { + "type": "string", + "example": "SUCCESS" + }, + "error": { + "type": "object", + "nullable": true, + "example": null + }, + "success": { + "type": "object", + "properties": { + "folder_id": { + "type": "string", + "example": "1" + }, + "image_text": { + "type": "string", + "example": "이번 수업 시간은 사회 과학 시간이다." + } + } + } + } + } + } + } + }, + "400": { + "description": "잘못된 요청 데이터", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "example": "폴더의 이름을 입력해주세요." + } + } + } + } + } + }, + "500": { + "description": "서버 내부 오류", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "example": "서버에러발생." + } + } + } + } + } } }, - "requestBody": {} + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "image_url", + "user_id", + "folder_name" + ], + "properties": { + "base64_image": { + "type": "string", + "description": "OCR 처리를 위한 이미지 URL", + "example": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUA..." + }, + "user_id": { + "type": "number", + "description": "사용자 ID", + "example": 1 + }, + "folder_name": { + "type": "string", + "description": "생성할 폴더의 이름", + "example": "공부" + } + } + } + } + } + } } }, "/memo/text-format/folders/{folderId}": { @@ -728,7 +822,117 @@ }, "/challenge/delete": { "delete": { + "tags": [ + "challenge-controller" + ], + "summary": "챌린지 삭제 API", + "description": "챌린지를 삭제하는 API입니다.", + "responses": { + "200": { + "description": "챌린지 삭제 성공 응답", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "resultType": { + "type": "string", + "example": "SUCCESS" + }, + "error": { + "type": "object", + "nullable": true, + "example": null + }, + "success": { + "type": "object", + "properties": { + "id": { + "type": "string", + "example": "1" + } + } + } + } + } + } + } + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "id" + ], + "properties": { + "id": { + "type": "string", + "description": "챌린지 ID" + } + } + } + } + } + } + } + }, + "/challenge/accept/{id}": { + "patch": { "description": "", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "default": { + "description": "" + } + } + } + }, + "/challenge/complete/{id}": { + "patch": { + "description": "", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "default": { + "description": "" + } + } + } + }, + "/challenge/get/{userId}": { + "get": { + "description": "", + "parameters": [ + { + "name": "userId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], "responses": { "default": { "description": "" @@ -976,7 +1180,11 @@ }, "/challenge/weekly_challenge/get/{id}": { "get": { - "description": "", + "tags": [ + "challenge-weekly-controller" + ], + "summary": "날짜 기반 챌린지 불러오기 API", + "description": "날짜 기반 챌린지를 불러오는 API입니다.", "parameters": [ { "name": "id", @@ -984,13 +1192,93 @@ "required": true, "schema": { "type": "string" - } + }, + "description": "챌린지 ID 입력" } ], "responses": { - "default": { - "description": "" + "200": { + "description": "날짜 챌린지 불러오기 성공 응답", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "resultType": { + "type": "string", + "example": "SUCCESS" + }, + "error": { + "type": "object", + "nullable": true, + "example": null + }, + "success": { + "type": "object", + "properties": { + "id": { + "type": "string", + "example": "1" + }, + "title": { + "type": "string", + "example": "challenge-title" + }, + "context": { + "type": "string", + "example": "challenge-context" + }, + "challengeDate": { + "type": "string", + "format": "date-time", + "example": "challenge-location" + }, + "requiredCount": { + "type": "number", + "example": 10 + }, + "remainingCount": { + "type": "number", + "example": 10 + }, + "userId": { + "type": "string", + "example": "1" + }, + "createdAt": { + "type": "string", + "format": "date-time", + "example": "2025-01-20T18:19:47.415Z" + }, + "updatedAt": { + "type": "string", + "format": "date-time", + "example": "2025-01-20T18:19:47.415Z" + }, + "acceptedAt": { + "type": "string", + "format": "date-time", + "example": "2025-01-20T18:19:47.415Z" + }, + "completedAt": { + "type": "string", + "format": "date-time", + "example": "2025-01-20T18:19:47.415Z" + }, + "status": { + "type": "number", + "example": 1 + } + } + } + } + } + } + } } + }, + "requestBody": { + "required": false } } }, diff --git a/swagger/swagger.ts b/swagger/swagger.ts index 21bdeb2..1e4cb83 100644 --- a/swagger/swagger.ts +++ b/swagger/swagger.ts @@ -14,7 +14,7 @@ const doc = { description: 'Sweepic 프로젝트입니다.', version: '1.0.0', }, - host: 'http://3.37.137.212:3000/', + host: 'http://localhost:3000/', }; swaggerAutogen(options)(outputFile, routes, doc); diff --git a/yarn.lock b/yarn.lock index 30fc8f6..a157f35 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2081,6 +2081,11 @@ "@types/qs" "*" "@types/serve-static" "*" +"@types/geojson@*": + version "7946.0.16" + resolved "https://registry.yarnpkg.com/@types/geojson/-/geojson-7946.0.16.tgz#8ebe53d69efada7044454e3305c19017d97ced2a" + integrity sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg== + "@types/http-assert@*": version "1.5.6" resolved "https://registry.yarnpkg.com/@types/http-assert/-/http-assert-1.5.6.tgz#b6b657c38a2350d21ce213139f33b03b2b5fa431" @@ -2153,6 +2158,13 @@ dependencies: "@types/express" "*" +"@types/navermaps@^3.7.8": + version "3.7.8" + resolved "https://registry.yarnpkg.com/@types/navermaps/-/navermaps-3.7.8.tgz#e4a3a4f351537dfc7dd66c29f48db88a489d2fe1" + integrity sha512-LzQffMWcUfhKzOuPpUONaXmMN6sAkNf92q1nycRplqorIl2oDjgdPftOw0LttTS0/k/YsotizawK+PtcRWbuog== + dependencies: + "@types/geojson" "*" + "@types/node@*", "@types/node@>=13.7.0", "@types/node@^22.10.3": version "22.10.10" resolved "https://registry.yarnpkg.com/@types/node/-/node-22.10.10.tgz#85fe89f8bf459dc57dfef1689bd5b52ad1af07e6"