diff --git a/.dockerignore b/.dockerignore index 3776604..3cec456 100644 --- a/.dockerignore +++ b/.dockerignore @@ -2,4 +2,4 @@ node_modules .git .env -dist \ No newline at end of file +dist diff --git a/jest.config.ts b/jest.config.ts index 5799ac5..86cb583 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -4,6 +4,9 @@ const config: Config = { preset: 'ts-jest', testEnvironment: 'node', testMatch: ['**/src/**/*.test.ts'], + moduleNameMapper: { + '^@/(.*)$': '/src/$1', + }, moduleFileExtensions: ['ts', 'js'], transform: { '^.+\\.ts$': 'ts-jest', diff --git a/src/controllers/post.controller.ts b/src/controllers/post.controller.ts index e3c60c3..af099fa 100644 --- a/src/controllers/post.controller.ts +++ b/src/controllers/post.controller.ts @@ -1,88 +1,82 @@ import { NextFunction, Request, RequestHandler, Response } from 'express'; -import logger from '../configs/logger.config'; -import { PostService } from '../services/post.service'; -import { GetAllPostsQuery, PostResponse } from '../types'; -import { GetPostQuery } from '../types/requests/getPostQuery.type'; +import logger from '@/configs/logger.config'; +import { PostService } from '@/services/post.service'; +import { + GetAllPostsQuery, + PostsResponseDto, + PostResponseDto, + GetPostQuery, + PostParam, + PostStatisticsResponseDto, +} from '@/types'; export class PostController { constructor(private postService: PostService) {} - private validateQueryParams(query: GetAllPostsQuery): { - cursor: string | undefined; - sort: string; - isAsc: boolean; - } { - return { - cursor: query.cursor, - sort: query.sort || '', - isAsc: query.asc === 'true', - }; - } - private validateQueryParams2(query: Partial): { - start: string; - end: string; - } { - return { - start: query.start || '', - end: query.end || '', - }; - } getAllPost: RequestHandler = async ( req: Request, - res: Response, + res: Response, next: NextFunction, ) => { try { const { id } = req.user; - const { cursor, sort, isAsc } = this.validateQueryParams(req.query); + const { cursor, sort, asc } = req.query; - const result = await this.postService.getAllposts(id, cursor, sort, isAsc); + const result = await this.postService.getAllposts(id, cursor, sort, asc); - res.status(200).json({ - success: true, - message: 'post 전체 조회에 성공하였습니다.', - data: { - nextCursor: result.nextCursor, - posts: result.posts, - }, - error: null, - }); + const response = new PostsResponseDto( + true, + '전체 post 조회에 성공하였습니다.', + { nextCursor: result.nextCursor, posts: result.posts }, + null, + ); + + res.status(200).json(response); } catch (error) { logger.error('전체 조회 실패:', error); next(error); } }; - getAllPostStatistics: RequestHandler = async (req: Request, res: Response, next: NextFunction) => { + getAllPostStatistics: RequestHandler = async ( + req: Request, + res: Response, + next: NextFunction, + ) => { try { const { id } = req.user; - const result = await this.postService.getAllPostStatistics(id); + const stats = await this.postService.getAllPostStatistics(id); const totalPostCount = await this.postService.getTotalPostCounts(id); - res.status(200).json({ - success: true, - message: 'post 전체 통계 조회에 성공하였습니다.', - data: { totalPostCount, stats: result }, - error: null, - }); + const response = new PostStatisticsResponseDto( + true, + '전체 post 통계 조회에 성공하였습니다.', + { totalPostCount, stats }, + null, + ); + + res.status(200).json(response); } catch (error) { logger.error('전체 통계 조회 실패:', error); next(error); } }; - getPost: RequestHandler = async (req: Request, res: Response, next: NextFunction) => { + getPost: RequestHandler = async ( + req: Request, + res: Response, + next: NextFunction, + ) => { try { - const postId = parseInt(req.params.postId); - const { start, end } = this.validateQueryParams2(req.query); + const postId = Number(req.params.postId); + const { start, end } = req.query; + const post = await this.postService.getPost(postId, start, end); - res.status(200).json({ - success: true, - message: 'post 단건 조회에 성공하였습니다', - data: { post }, - error: null, - }); + + const response = new PostResponseDto(true, '단건 post 조회에 성공하였습니다.', { post }, null); + + res.status(200).json(response); } catch (error) { logger.error('단건 조회 실패 : ', error); next(error); diff --git a/src/controllers/tracking.controller.ts b/src/controllers/tracking.controller.ts index 44a94ab..a9009c2 100644 --- a/src/controllers/tracking.controller.ts +++ b/src/controllers/tracking.controller.ts @@ -1,34 +1,40 @@ import { NextFunction, Request, RequestHandler, Response } from 'express'; -import logger from '../configs/logger.config'; -import { TrackingService } from '../services/tracking.service'; -import { TrackingResponse } from '../types'; +import logger from '@/configs/logger.config'; +import { TrackingService } from '@/services/tracking.service'; +import { EmptyResponseDto } from '@/types'; export class TrackingController { constructor(private trackingService: TrackingService) {} - event = (async (req: Request, res: Response, next: NextFunction) => { + event: RequestHandler = async (req: Request, res: Response, next: NextFunction) => { try { const { eventType } = req.body; const { id } = req.user; await this.trackingService.tracking(eventType, id); - return res.status(200).json({ success: true, message: '이벤트 데이터 저장완료', data: {}, error: null }); + + const response = new EmptyResponseDto(true, '이벤트 데이터 저장완료', {}, null); + + res.status(200).json(response); } catch (error) { logger.error('user tracking 실패 : ', error); next(error); } - }) as RequestHandler; + }; - stay = (async (req: Request, res: Response, next: NextFunction) => { + stay: RequestHandler = async (req: Request, res: Response, next: NextFunction) => { try { const { loadDate, unloadDate } = req.body; const { id } = req.user; await this.trackingService.stay({ loadDate, unloadDate }, id); - return res.status(200).json({ success: true, message: '체류시간 데이터 저장 완료', data: {}, error: null }); + + const response = new EmptyResponseDto(true, '체류시간 데이터 저장 완료', {}, null); + + res.status(200).json(response); } catch (error) { logger.error('user stay time 저장 실패 : ', error); next(error); } - }) as RequestHandler; + }; } diff --git a/src/controllers/user.controller.ts b/src/controllers/user.controller.ts index 0c25844..36ae3e9 100644 --- a/src/controllers/user.controller.ts +++ b/src/controllers/user.controller.ts @@ -1,7 +1,7 @@ import { NextFunction, Request, Response, RequestHandler, CookieOptions } from 'express'; -import logger from '../configs/logger.config'; -import { LoginResponse, UserWithTokenDto } from '../types'; -import { UserService } from '../services/user.service'; +import logger from '@/configs/logger.config'; +import { EmptyResponseDto, LoginResponseDto, UserWithTokenDto } from '@/types'; +import { UserService } from '@/services/user.service'; export class UserController { constructor(private userService: UserService) {} @@ -21,7 +21,7 @@ export class UserController { return baseOptions; } - login: RequestHandler = async (req: Request, res: Response, next: NextFunction): Promise => { + login: RequestHandler = async (req: Request, res: Response, next: NextFunction): Promise => { try { const { id, email, profile, username } = req.user; const { accessToken, refreshToken } = req.tokens; @@ -35,32 +35,39 @@ export class UserController { res.cookie('access_token', accessToken, this.cookieOption()); res.cookie('refresh_token', refreshToken, this.cookieOption()); - res.status(200).json({ - success: true, - message: '로그인에 성공하였습니다.', - data: { id: isExistUser.id, username, profile }, - error: null, - }); + const response = new LoginResponseDto( + true, + '로그인에 성공하였습니다.', + { id: isExistUser.id, username, profile }, + null, + ); + + res.status(200).json(response); } catch (error) { logger.error('로그인 실패 : ', error); next(error); } }; - logout: RequestHandler = async (req: Request, res: Response) => { + logout: RequestHandler = async (req: Request, res: Response) => { res.clearCookie('access_token'); res.clearCookie('refresh_token'); - res.status(200).json({ success: true, message: '로그아웃에 성공하였습니다.', data: {}, error: null }); + const response = new EmptyResponseDto(true, '로그아웃에 성공하였습니다.', {}, null); + + res.status(200).json(response); }; - fetchCurrentUser: RequestHandler = (req: Request, res: Response) => { + fetchCurrentUser: RequestHandler = (req: Request, res: Response) => { const { user } = req; - res.status(200).json({ - success: true, - message: '프로필 조회에 성공하였습니다.', - data: { user }, - error: null, - }); + + const response = new LoginResponseDto( + true, + '유저 정보 조회에 성공하였습니다.', + { id: user.id, username: user.username, profile: user.profile }, + null, + ); + + res.status(200).json(response); }; } diff --git a/src/middlewares/auth.middleware.ts b/src/middlewares/auth.middleware.ts index 0a537f7..fdc63aa 100644 --- a/src/middlewares/auth.middleware.ts +++ b/src/middlewares/auth.middleware.ts @@ -1,10 +1,10 @@ import { NextFunction, Request, Response } from 'express'; import axios from 'axios'; import { isUUID } from 'class-validator'; -import logger from '../configs/logger.config'; -import pool from '../configs/db.config'; -import { DBError, InvalidTokenError } from '../exception'; -import { VELOG_API_URL, VELOG_QUERIES } from '../constants/velog.constans'; +import logger from '@/configs/logger.config'; +import pool from '@/configs/db.config'; +import { DBError, InvalidTokenError } from '@/exception'; +import { VELOG_API_URL, VELOG_QUERIES } from '@/constants/velog.constans'; /** * 요청에서 토큰을 추출하는 함수 @@ -58,7 +58,6 @@ const fetchVelogApi = async (query: string, accessToken: string, refreshToken: s } }; - /** * JWT 토큰에서 페이로드를 추출하고 디코딩하는 함수 * @param token - 디코딩할 JWT 토큰 문자열 diff --git a/src/middlewares/errorHandling.middleware.ts b/src/middlewares/errorHandling.middleware.ts index 2380093..67513ee 100644 --- a/src/middlewares/errorHandling.middleware.ts +++ b/src/middlewares/errorHandling.middleware.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ import { Request, Response, NextFunction, ErrorRequestHandler } from 'express'; -import { CustomError } from '../exception'; -import logger from '../configs/logger.config'; +import { CustomError } from '@/exception'; +import logger from '@/configs/logger.config'; export const errorHandlingMiddleware = ((err: CustomError, req: Request, res: Response, next: NextFunction) => { if (err instanceof CustomError) { diff --git a/src/middlewares/validation.middleware.ts b/src/middlewares/validation.middleware.ts index 7285008..e635011 100644 --- a/src/middlewares/validation.middleware.ts +++ b/src/middlewares/validation.middleware.ts @@ -1,12 +1,12 @@ import { NextFunction, Request, Response, RequestHandler } from 'express'; import { plainToInstance } from 'class-transformer'; import { validate } from 'class-validator'; -import logger from '../configs/logger.config'; +import logger from '@/configs/logger.config'; -type RequestKey = 'body' | 'user'; +type RequestKey = 'body' | 'user' | 'query'; // eslint-disable-next-line @typescript-eslint/no-explicit-any -export const validateDto = (dtoClass: new (...args: any) => T, key: RequestKey) => { +export const validateRequestDto = (dtoClass: new (...args: any) => T, key: RequestKey) => { return (async (req: Request, res: Response, next: NextFunction) => { try { const value = plainToInstance(dtoClass, req[key]); diff --git a/src/modules/__test__/test.aes.encryption.test.ts b/src/modules/__test__/test.aes.encryption.test.ts index 4624ca7..9c0be8f 100644 --- a/src/modules/__test__/test.aes.encryption.test.ts +++ b/src/modules/__test__/test.aes.encryption.test.ts @@ -1,5 +1,5 @@ import crypto from 'crypto'; -import AESEncryption from '../token_encryption/aes_encryption'; +import AESEncryption from '@/modules/token_encryption/aes_encryption'; describe('AESEncryption 클래스 테스트', () => { const validKey = crypto.randomBytes(32).toString('hex').slice(0, 32); // 32바이트 키 생성 @@ -14,9 +14,7 @@ describe('AESEncryption 클래스 테스트', () => { }); test('잘못된 키 길이를 사용하면 오류가 발생해야 한다', () => { - expect(() => new AESEncryption(invalidKey)).toThrow( - '키는 256비트(32바이트)여야 합니다.' - ); + expect(() => new AESEncryption(invalidKey)).toThrow('키는 256비트(32바이트)여야 합니다.'); }); test('암호화 결과는 base64 형식이어야 한다', () => { diff --git a/src/repositories/post.repository.ts b/src/repositories/post.repository.ts index 8c5b49f..784be5e 100644 --- a/src/repositories/post.repository.ts +++ b/src/repositories/post.repository.ts @@ -1,6 +1,6 @@ import { Pool } from 'pg'; -import logger from '../configs/logger.config'; -import { DBError } from '../exception'; +import logger from '@/configs/logger.config'; +import { DBError } from '@/exception'; export class PostRepository { constructor(private pool: Pool) {} @@ -10,10 +10,10 @@ export class PostRepository { // 1) 정렬 컬럼 매핑 let sortCol = 'p.released_at'; switch (sort) { - case 'daily_view_count': + case 'dailyViewCount': sortCol = 'pds.daily_view_count'; break; - case 'daily_like_count': + case 'dailyLikeCount': sortCol = 'pds.daily_like_count'; break; default: @@ -96,9 +96,9 @@ export class PostRepository { // nextCursor = `${정렬 컬럼 값},${p.id}` // 예: 만약 sortCol이 p.title인 경우, lastPost.title + ',' + lastPost.id let sortValueForCursor = ''; - if (sort === 'daily_view_count') { + if (sort === 'dailyViewCount') { sortValueForCursor = lastPost.daily_view_count; - } else if (sort === 'daily_like_count') { + } else if (sort === 'dailyLikeCount') { sortValueForCursor = lastPost.daily_like_count; } else { sortValueForCursor = new Date(lastPost.post_released_at).toISOString(); @@ -127,7 +127,7 @@ export class PostRepository { } async getYesterdayAndTodayViewLikeStats(userId: number) { - // pds.updated_at 은 FE 화면을 위해 억지로 9h 시간 더한 값임 주의 + // ! pds.updated_at 은 FE 화면을 위해 억지로 9h 시간 더한 값임 주의 try { const query = ` SELECT diff --git a/src/repositories/tracking.repository.ts b/src/repositories/tracking.repository.ts index 459b083..ee47853 100644 --- a/src/repositories/tracking.repository.ts +++ b/src/repositories/tracking.repository.ts @@ -1,7 +1,7 @@ import { Pool } from 'pg'; -import logger from '../configs/logger.config'; -import { DBError } from '../exception'; -import { EventRequestDto } from '../types'; +import logger from '@/configs/logger.config'; +import { DBError } from '@/exception'; +import { EventRequestDto } from '@/types'; export class TrackingRepository { constructor(private readonly pool: Pool) {} @@ -22,6 +22,7 @@ export class TrackingRepository { throw new DBError('User Tracking 정보 저장 중 문제가 발생하였습니다.'); } } + async createStayTime(loadDate: Date, unloadDate: Date, userId: number) { try { await this.pool.query( diff --git a/src/repositories/user.repository.ts b/src/repositories/user.repository.ts index d715083..ed344d7 100644 --- a/src/repositories/user.repository.ts +++ b/src/repositories/user.repository.ts @@ -1,7 +1,7 @@ import { Pool } from 'pg'; -import logger from '../configs/logger.config'; -import { User } from '../types'; -import { DBError } from '../exception'; +import logger from '@/configs/logger.config'; +import { User } from '@/types'; +import { DBError } from '@/exception'; export class UserRepository { constructor(private readonly pool: Pool) {} @@ -16,6 +16,7 @@ export class UserRepository { throw new DBError('유저 조회 중 문제가 발생했습니다.'); } } + async updateTokens(uuid: string, encryptedAccessToken: string, encryptedRefreshToken: string): Promise { try { const query = ` @@ -68,6 +69,7 @@ export class UserRepository { if (!result.rows[0]) { throw new DBError('유저 생성에 실패했습니다.'); } + return result.rows[0]; } catch (error) { logger.error('User Repo createUser Error : ', error); diff --git a/src/routes/post.router.ts b/src/routes/post.router.ts index 793a7fb..8a6ed31 100644 --- a/src/routes/post.router.ts +++ b/src/routes/post.router.ts @@ -1,10 +1,12 @@ import express, { Router } from 'express'; -import pool from '../configs/db.config'; import dotenv from 'dotenv'; -import { authMiddleware } from '../middlewares/auth.middleware'; -import { PostRepository } from '../repositories/post.repository'; -import { PostService } from '../services/post.service'; -import { PostController } from '../controllers/post.controller'; +import pool from '@/configs/db.config'; +import { authMiddleware } from '@/middlewares/auth.middleware'; +import { PostRepository } from '@/repositories/post.repository'; +import { PostService } from '@/services/post.service'; +import { PostController } from '@/controllers/post.controller'; +import { validateRequestDto } from '@/middlewares/validation.middleware'; +import { GetAllPostsQueryDto } from '@/types/dto/requests/getAllPostsQuery.type'; const router: Router = express.Router(); dotenv.config(); @@ -13,7 +15,12 @@ const postRepository = new PostRepository(pool); const postService = new PostService(postRepository); const postController = new PostController(postService); -router.get('/posts', authMiddleware.verify, postController.getAllPost); +router.get( + '/posts', + authMiddleware.verify, + validateRequestDto(GetAllPostsQueryDto, 'query'), + postController.getAllPost, +); router.get('/posts-stats', authMiddleware.verify, postController.getAllPostStatistics); router.get('/post/:postId', authMiddleware.verify, postController.getPost); diff --git a/src/routes/tracking.router.ts b/src/routes/tracking.router.ts index 313c59c..2a08a97 100644 --- a/src/routes/tracking.router.ts +++ b/src/routes/tracking.router.ts @@ -1,13 +1,13 @@ import express, { Router } from 'express'; -import pool from '../configs/db.config'; +import pool from '@/configs/db.config'; import dotenv from 'dotenv'; -import { TrackingRepository } from '../repositories/tracking.repository'; -import { TrackingService } from '../services/tracking.service'; -import { TrackingController } from '../controllers/tracking.controller'; -import { validateDto } from '../middlewares/validation.middleware'; -import { EventRequestDto } from '../types'; -import { authMiddleware } from '../middlewares/auth.middleware'; -import { StayTimeRequestDto } from '../types'; +import { TrackingRepository } from '@/repositories/tracking.repository'; +import { TrackingService } from '@/services/tracking.service'; +import { TrackingController } from '@/controllers/tracking.controller'; +import { validateRequestDto } from '@/middlewares/validation.middleware'; +import { EventRequestDto } from '@/types'; +import { authMiddleware } from '@/middlewares/auth.middleware'; +import { StayTimeRequestDto } from '@/types'; const router: Router = express.Router(); dotenv.config(); @@ -16,7 +16,7 @@ const trackingRepository = new TrackingRepository(pool); const trackingService = new TrackingService(trackingRepository); const trackingController = new TrackingController(trackingService); -router.post('/event', authMiddleware.verify, validateDto(EventRequestDto, 'body'), trackingController.event); -router.post('/stay', authMiddleware.verify, validateDto(StayTimeRequestDto, 'body'), trackingController.stay); +router.post('/event', authMiddleware.verify, validateRequestDto(EventRequestDto, 'body'), trackingController.event); +router.post('/stay', authMiddleware.verify, validateRequestDto(StayTimeRequestDto, 'body'), trackingController.stay); export default router; diff --git a/src/routes/user.router.ts b/src/routes/user.router.ts index aecac14..4206790 100644 --- a/src/routes/user.router.ts +++ b/src/routes/user.router.ts @@ -1,12 +1,12 @@ import express, { Router } from 'express'; -import { UserController } from '../controllers/user.controller'; -import { UserRepository } from '../repositories/user.repository'; -import { UserService } from '../services/user.service'; -import pool from '../configs/db.config'; -import { authMiddleware } from '../middlewares/auth.middleware'; -import { validateDto } from '../middlewares/validation.middleware'; +import { UserController } from '@/controllers/user.controller'; +import { UserRepository } from '@/repositories/user.repository'; +import { UserService } from '@/services/user.service'; +import pool from '@/configs/db.config'; +import { authMiddleware } from '@/middlewares/auth.middleware'; +import { validateRequestDto } from '@/middlewares/validation.middleware'; import dotenv from 'dotenv'; -import { VelogUserLoginDto } from '../types'; +import { VelogUserLoginDto } from '@/types'; const router: Router = express.Router(); dotenv.config(); @@ -15,7 +15,7 @@ const userRepository = new UserRepository(pool); const userService = new UserService(userRepository); const userController = new UserController(userService); -router.post('/login', authMiddleware.login, validateDto(VelogUserLoginDto, 'user'), userController.login); +router.post('/login', authMiddleware.login, validateRequestDto(VelogUserLoginDto, 'user'), userController.login); router.post('/logout', authMiddleware.login, userController.logout); router.get('/me', authMiddleware.login, userController.fetchCurrentUser); export default router; diff --git a/src/services/post.service.ts b/src/services/post.service.ts index 6ef0367..0297281 100644 --- a/src/services/post.service.ts +++ b/src/services/post.service.ts @@ -1,5 +1,5 @@ -import logger from '../configs/logger.config'; -import { PostRepository } from '../repositories/post.repository'; +import logger from '@/configs/logger.config'; +import { PostRepository } from '@/repositories/post.repository'; export class PostService { constructor(private postRepo: PostRepository) {} @@ -53,14 +53,19 @@ export class PostService { } async getPost(postId: number, start?: string, end?: string) { - const posts = await this.postRepo.findPostByPostId(postId, start, end); + try { + const posts = await this.postRepo.findPostByPostId(postId, start, end); - const transformedPosts = posts.map((post) => ({ - date: post.date, - dailyViewCount: parseInt(post.daily_view_count), - dailyLikeCount: parseInt(post.daily_like_count), - })); + const transformedPosts = posts.map((post) => ({ + date: post.date, + dailyViewCount: parseInt(post.daily_view_count), + dailyLikeCount: parseInt(post.daily_like_count), + })); - return transformedPosts; + return transformedPosts; + } catch (error) { + logger.error('PostService getTotalPostCounts error : ', error); + throw error; + } } } diff --git a/src/services/tracking.service.ts b/src/services/tracking.service.ts index 350acd5..9dfe354 100644 --- a/src/services/tracking.service.ts +++ b/src/services/tracking.service.ts @@ -1,7 +1,7 @@ -import { EventRequestDto, StayTimeRequestDto } from '../types'; -import { TrackingRepository } from '../repositories/tracking.repository'; -import logger from '../configs/logger.config'; -import { BadRequestError } from '../exception'; +import { EventRequestDto, StayTimeRequestDto } from '@/types'; +import { TrackingRepository } from '@/repositories/tracking.repository'; +import logger from '@/configs/logger.config'; +import { BadRequestError } from '@/exception'; export class TrackingService { constructor(private trackingRepo: TrackingRepository) {} @@ -9,14 +9,17 @@ export class TrackingService { async tracking(eventType: EventRequestDto, id: number) { return await this.trackingRepo.createEvent(eventType, id); } + async stay(data: StayTimeRequestDto, userId: number) { try { const { loadDate, unloadDate } = data; if (new Date(loadDate) > new Date(unloadDate)) { throw new BadRequestError('시간 정보가 올바르지 않습니다.'); } + const stayTime = new Date(unloadDate).getTime() - new Date(loadDate).getTime(); await this.trackingRepo.createStayTime(loadDate, unloadDate, userId); + return stayTime; } catch (error) { logger.error('Tracking Service stay error : ', error); diff --git a/src/services/user.service.ts b/src/services/user.service.ts index 1876b9a..d7e3113 100644 --- a/src/services/user.service.ts +++ b/src/services/user.service.ts @@ -1,10 +1,10 @@ -import logger from '../configs/logger.config'; -import { TokenError } from '../exception/'; -import { getKeyByGroup } from '../utils/key.util'; -import AESEncryption from '../modules/token_encryption/aes_encryption'; -import { UserRepository } from '../repositories/user.repository'; -import { UserWithTokenDto, User } from '../types'; -import { generateRandomGroupId } from '../utils/generateGroupId.util'; +import logger from '@/configs/logger.config'; +import { TokenError } from '@/exception/'; +import { getKeyByGroup } from '@/utils/key.util'; +import AESEncryption from '@/modules/token_encryption/aes_encryption'; +import { UserRepository } from '@/repositories/user.repository'; +import { UserWithTokenDto, User } from '@/types'; +import { generateRandomGroupId } from '@/utils/generateGroupId.util'; export class UserService { constructor(private userRepo: UserRepository) {} diff --git a/src/types/dto/eventRequest.dto.ts b/src/types/dto/requests/eventRequest.type.ts similarity index 64% rename from src/types/dto/eventRequest.dto.ts rename to src/types/dto/requests/eventRequest.type.ts index b210709..cb255c5 100644 --- a/src/types/dto/eventRequest.dto.ts +++ b/src/types/dto/requests/eventRequest.type.ts @@ -1,5 +1,12 @@ import { IsEnum, IsNotEmpty } from 'class-validator'; -import { UserEventType } from '../userEvent.type'; + +export enum UserEventType { + LOGIN = '01', + POST_CLICK = '02', + POST_GRAPH_CLICK = '03', + EXIT = '04', + NOTHING = '99', +} export class EventRequestDto { @IsEnum(UserEventType) diff --git a/src/types/dto/requests/getAllPostsQuery.type.ts b/src/types/dto/requests/getAllPostsQuery.type.ts new file mode 100644 index 0000000..ee6bec4 --- /dev/null +++ b/src/types/dto/requests/getAllPostsQuery.type.ts @@ -0,0 +1,31 @@ +import { Transform } from 'class-transformer'; +import { IsBoolean, IsOptional, IsString } from 'class-validator'; + +export type PostSortType = '' | 'dailyViewCount' | 'dailyLikeCount'; + +export interface GetAllPostsQuery { + cursor?: string; + sort?: PostSortType; + asc?: boolean; +} + +export class GetAllPostsQueryDto { + @IsOptional() + @IsString() + cursor?: string; + + @IsOptional() + @IsString() + sort?: PostSortType; + + @IsOptional() + @IsBoolean() + @Transform(({ value }) => value === 'true') + asc?: boolean; + + constructor(cursor: string | undefined, sort: PostSortType, asc: boolean = false) { + this.cursor = cursor; + this.sort = sort || ''; + this.asc = asc; + } +} diff --git a/src/types/dto/requests/getPostQuery.type.ts b/src/types/dto/requests/getPostQuery.type.ts new file mode 100644 index 0000000..c4dffe4 --- /dev/null +++ b/src/types/dto/requests/getPostQuery.type.ts @@ -0,0 +1,28 @@ +import { Type } from 'class-transformer'; +import { IsDate, IsOptional } from 'class-validator'; + +export interface PostParam { + postId?: string; +} + +export interface GetPostQuery { + start?: string; + end?: string; +} + +export class GetPostQueryDto { + @IsOptional() + @IsDate() + @Type(() => Date) + start?: string; + + @IsOptional() + @IsDate() + @Type(() => Date) + end?: string; + + constructor(start: string, end: string) { + this.start = start; + this.end = end; + } +} diff --git a/src/types/dto/stayTimeRequest.dto.ts b/src/types/dto/requests/stayTimeRequest.dto.ts similarity index 100% rename from src/types/dto/stayTimeRequest.dto.ts rename to src/types/dto/requests/stayTimeRequest.dto.ts diff --git a/src/types/dto/responses/baseResponse.type.ts b/src/types/dto/responses/baseResponse.type.ts new file mode 100644 index 0000000..29e0a2b --- /dev/null +++ b/src/types/dto/responses/baseResponse.type.ts @@ -0,0 +1,13 @@ +export class BaseResponseDto { + success: boolean; + message: string; + data?: T; + error?: string | null; + + constructor(success: boolean, message: string, data?: T, error?: string | null) { + this.success = success; + this.message = message; + this.data = data; + this.error = error; + } +} diff --git a/src/types/dto/responses/emptyReponse.type.ts b/src/types/dto/responses/emptyReponse.type.ts new file mode 100644 index 0000000..e708962 --- /dev/null +++ b/src/types/dto/responses/emptyReponse.type.ts @@ -0,0 +1,4 @@ +import { BaseResponseDto } from '@/types/dto/responses/baseResponse.type'; + +type EmptyResponseData = Record; +export class EmptyResponseDto extends BaseResponseDto {} diff --git a/src/types/dto/responses/loginResponse.type.ts b/src/types/dto/responses/loginResponse.type.ts new file mode 100644 index 0000000..f842020 --- /dev/null +++ b/src/types/dto/responses/loginResponse.type.ts @@ -0,0 +1,13 @@ +import { BaseResponseDto } from '@/types/dto/responses/baseResponse.type'; + +interface ProfileType { + thumbnail: string; +} + +interface LoginResponseData { + id: number; + username: string; + profile: ProfileType; +} + +export class LoginResponseDto extends BaseResponseDto {} diff --git a/src/types/dto/responses/postResponse.type.ts b/src/types/dto/responses/postResponse.type.ts new file mode 100644 index 0000000..94d0414 --- /dev/null +++ b/src/types/dto/responses/postResponse.type.ts @@ -0,0 +1,49 @@ +import { BaseResponseDto } from '@/types/dto/responses/baseResponse.type'; + +// ------ 전체 조회 ------ +interface GetAllPostType { + id: number; + title: string; + views: number; + likes: number; + yesterdayViews: number; + yesterdayLikes: number; + createdAt: string; + releasedAt: string; +} + +interface PostsResponseData { + nextCursor: string | null; + posts: GetAllPostType[]; +} + +export class PostsResponseDto extends BaseResponseDto {} + +// ------ 단건 조회 ------ +interface GetPostType { + date: string; + dailyViewCount: number; + dailyLikeCount: number; +} + +interface PostResponseData { + post: GetPostType[]; +} + +export class PostResponseDto extends BaseResponseDto {} + +// ------ 전체 통계 ------ +interface PostStatisticsType { + totalViews: number; + totalLikes: number; + yesterdayViews: number; + yesterdayLikes: number; + lastUpdatedDate: string; +} + +interface PostStatisticsData { + totalPostCount: number; + stats: PostStatisticsType; +} + +export class PostStatisticsResponseDto extends BaseResponseDto {} diff --git a/src/types/dto/userWithToken.dto.ts b/src/types/dto/userWithToken.type.ts similarity index 100% rename from src/types/dto/userWithToken.dto.ts rename to src/types/dto/userWithToken.type.ts diff --git a/src/types/dto/velogUser.type.ts b/src/types/dto/velogUser.type.ts new file mode 100644 index 0000000..11859df --- /dev/null +++ b/src/types/dto/velogUser.type.ts @@ -0,0 +1,32 @@ +import { IsEmail, IsString, IsUUID, ValidateNested } from 'class-validator'; +import { Type } from 'class-transformer'; + +export class VelogUserLoginDto { + @IsUUID() + id: string; + + @IsString() + username: string; + + @IsEmail() + email: string; + + @ValidateNested() + @Type(() => ProfileDTO) + profile: ProfileDTO; + + constructor(id: string, username: string, email: string, profile: ProfileDTO) { + this.id = id; + this.username = username; + this.email = email; + this.profile = profile; + } +} +class ProfileDTO { + @IsString() + thumbnail: string; + + constructor(thumbnail: string) { + this.thumbnail = thumbnail; + } +} diff --git a/src/types/express.d.ts b/src/types/express.d.ts index 8c3231a..03a95c3 100644 --- a/src/types/express.d.ts +++ b/src/types/express.d.ts @@ -1,4 +1,4 @@ -import { VelogUserLoginResponse } from '../velog.type'; +import { VelogUserLoginResponse } from '@/velog.type'; declare global { namespace Express { diff --git a/src/types/index.ts b/src/types/index.ts index 3132ca0..318c3a8 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,15 +1,18 @@ -export type { User } from './models/User.type'; -export type { Post } from './models/Post.type'; -export type { PostDailyStatistics } from './models/PostDailyStatistics.type'; -export type { PostStatistics } from './models/PostStatistics.type'; -export type { UserEventTracking } from './models/UserEventTracking.type'; -export { EventRequestDto } from './dto/eventRequest.dto'; -export { UserWithTokenDto } from './dto/userWithToken.dto'; -export { VelogUserLoginDto } from './dto/velogUser.dto'; -export { StayTimeRequestDto } from './dto/stayTimeRequest.dto'; -export type { UserEventType } from './userEvent.type'; -export type { VelogUserLoginResponse } from './velog.type'; -export type { PostResponse } from './responses/postResponse.type'; -export type { LoginResponse } from './responses/loginResponse.type'; -export type { TrackingResponse } from './responses/trackingReponse.type'; -export type { GetAllPostsQuery } from './requests/getAllPostsQuery.type'; +export type { User } from '@/types/models/User.type'; +export type { Post } from '@/types/models/Post.type'; +export type { PostDailyStatistics } from '@/types/models/PostDailyStatistics.type'; +export type { PostStatistics } from '@/types/models/PostStatistics.type'; +export type { UserEventTracking } from '@/types/models/UserEventTracking.type'; +export type { VelogUserLoginResponse } from '@/types/velog.type'; +export type { GetAllPostsQuery } from '@/types/dto/requests/getAllPostsQuery.type'; +export type { GetPostQuery, PostParam } from '@/types/dto/requests/getPostQuery.type'; + +export { StayTimeRequestDto } from '@/types/dto/requests/stayTimeRequest.dto'; +export { GetAllPostsQueryDto } from '@/types/dto/requests/getAllPostsQuery.type'; +export { GetPostQueryDto } from '@/types/dto/requests/getPostQuery.type'; +export { LoginResponseDto } from '@/types/dto/responses/loginResponse.type'; +export { EmptyResponseDto } from '@/types/dto/responses/emptyReponse.type'; +export { PostsResponseDto, PostResponseDto, PostStatisticsResponseDto } from '@/types/dto/responses/postResponse.type'; +export { EventRequestDto } from '@/types/dto/requests/eventRequest.type'; +export { UserWithTokenDto } from '@/types/dto/userWithToken.type'; +export { VelogUserLoginDto } from '@/types/dto/velogUser.type'; diff --git a/src/types/models/UserEventTracking.type.ts b/src/types/models/UserEventTracking.type.ts index 0e799c0..a58e450 100644 --- a/src/types/models/UserEventTracking.type.ts +++ b/src/types/models/UserEventTracking.type.ts @@ -1,4 +1,4 @@ -import { UserEventType } from '../userEvent.type'; +import { UserEventType } from '@/types/userEvent.type'; export interface UserEventTracking { id: number; diff --git a/src/types/requests/getAllPostsQuery.type.ts b/src/types/requests/getAllPostsQuery.type.ts deleted file mode 100644 index e1409fd..0000000 --- a/src/types/requests/getAllPostsQuery.type.ts +++ /dev/null @@ -1,5 +0,0 @@ -export interface GetAllPostsQuery { - cursor?: string; - sort?: '' | 'daily_view_count' | 'daily_like_count'; - asc?: string; -} diff --git a/src/types/requests/getPostQuery.type.ts b/src/types/requests/getPostQuery.type.ts deleted file mode 100644 index 464f9fc..0000000 --- a/src/types/requests/getPostQuery.type.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface GetPostQuery { - start: string; - end: string; -} diff --git a/src/types/responses/baseResponse.type.ts b/src/types/responses/baseResponse.type.ts deleted file mode 100644 index 941e1a3..0000000 --- a/src/types/responses/baseResponse.type.ts +++ /dev/null @@ -1,6 +0,0 @@ -export interface BaseResponse { - success: boolean; - message: string; - data: T | null; - error: string | null; -} diff --git a/src/types/responses/loginResponse.type.ts b/src/types/responses/loginResponse.type.ts deleted file mode 100644 index 7374882..0000000 --- a/src/types/responses/loginResponse.type.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { BaseResponse } from './baseResponse.type'; - -interface ProfileType { - thumbnail: string; -} - -interface LoginResponseData { - id: number; - username: string; - profile: ProfileType; -} - -export type LoginResponse = BaseResponse; diff --git a/src/types/responses/postResponse.type.ts b/src/types/responses/postResponse.type.ts deleted file mode 100644 index 1dcfb25..0000000 --- a/src/types/responses/postResponse.type.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { BaseResponse } from './baseResponse.type'; - -interface PostType { - id: number; - title: string; - views: number; - likes: number; - yesterdayViews: number; - yesterdayLikes: number; - createdAt: string; - releasedAt: string; -} - -interface PostResponseData { - nextCursor: string | null; - posts: PostType[]; -} - -export type PostResponse = BaseResponse; diff --git a/src/types/responses/trackingReponse.type.ts b/src/types/responses/trackingReponse.type.ts deleted file mode 100644 index c3e0c8c..0000000 --- a/src/types/responses/trackingReponse.type.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { BaseResponse } from './baseResponse.type'; - -type TrackingResponseData = Record; -export type TrackingResponse = BaseResponse; diff --git a/tsconfig.json b/tsconfig.json index 0360352..20f7064 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -25,5 +25,5 @@ "plugins": [] }, "include": ["src/**/*"], - "exclude": ["node_modules", "dist"] + "exclude": ["node_modules", "dist", "jest.config.ts"] }