Skip to content
Merged
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: 1 addition & 1 deletion .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
node_modules
.git
.env
dist
dist
3 changes: 3 additions & 0 deletions jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ const config: Config = {
preset: 'ts-jest',
testEnvironment: 'node',
testMatch: ['**/src/**/*.test.ts'],
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1',
},
moduleFileExtensions: ['ts', 'js'],
transform: {
'^.+\\.ts$': 'ts-jest',
Expand Down
100 changes: 47 additions & 53 deletions src/controllers/post.controller.ts
Original file line number Diff line number Diff line change
@@ -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<GetPostQuery>): {
start: string;
end: string;
} {
return {
start: query.start || '',
end: query.end || '',
};
}
getAllPost: RequestHandler = async (
req: Request<object, object, object, GetAllPostsQuery>,
res: Response<PostResponse>,
res: Response<PostsResponseDto>,
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<PostStatisticsResponseDto>,
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<PostParam, object, object, GetPostQuery>,
res: Response<PostResponseDto>,
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);
Expand Down
24 changes: 15 additions & 9 deletions src/controllers/tracking.controller.ts
Original file line number Diff line number Diff line change
@@ -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<TrackingResponse>, next: NextFunction) => {
event: RequestHandler = async (req: Request, res: Response<EmptyResponseDto>, 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<TrackingResponse>, next: NextFunction) => {
stay: RequestHandler = async (req: Request, res: Response<EmptyResponseDto>, 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;
};
}
45 changes: 26 additions & 19 deletions src/controllers/user.controller.ts
Original file line number Diff line number Diff line change
@@ -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) {}

Expand All @@ -21,7 +21,7 @@ export class UserController {
return baseOptions;
}

login: RequestHandler = async (req: Request, res: Response<LoginResponse>, next: NextFunction): Promise<void> => {
login: RequestHandler = async (req: Request, res: Response<LoginResponseDto>, next: NextFunction): Promise<void> => {
try {
const { id, email, profile, username } = req.user;
const { accessToken, refreshToken } = req.tokens;
Expand All @@ -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<EmptyResponseDto>) => {
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<LoginResponseDto>) => {
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);
};
}
9 changes: 4 additions & 5 deletions src/middlewares/auth.middleware.ts
Original file line number Diff line number Diff line change
@@ -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';

/**
* 요청에서 토큰을 추출하는 함수
Expand Down Expand Up @@ -58,7 +58,6 @@ const fetchVelogApi = async (query: string, accessToken: string, refreshToken: s
}
};


/**
* JWT 토큰에서 페이로드를 추출하고 디코딩하는 함수
* @param token - 디코딩할 JWT 토큰 문자열
Expand Down
4 changes: 2 additions & 2 deletions src/middlewares/errorHandling.middleware.ts
Original file line number Diff line number Diff line change
@@ -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) {
Expand Down
6 changes: 3 additions & 3 deletions src/middlewares/validation.middleware.ts
Original file line number Diff line number Diff line change
@@ -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 = <T extends object>(dtoClass: new (...args: any) => T, key: RequestKey) => {
export const validateRequestDto = <T extends object>(dtoClass: new (...args: any) => T, key: RequestKey) => {
return (async (req: Request, res: Response, next: NextFunction) => {
try {
const value = plainToInstance(dtoClass, req[key]);
Expand Down
6 changes: 2 additions & 4 deletions src/modules/__test__/test.aes.encryption.test.ts
Original file line number Diff line number Diff line change
@@ -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바이트 키 생성
Expand All @@ -14,9 +14,7 @@ describe('AESEncryption 클래스 테스트', () => {
});

test('잘못된 키 길이를 사용하면 오류가 발생해야 한다', () => {
expect(() => new AESEncryption(invalidKey)).toThrow(
'키는 256비트(32바이트)여야 합니다.'
);
expect(() => new AESEncryption(invalidKey)).toThrow('키는 256비트(32바이트)여야 합니다.');
});

test('암호화 결과는 base64 형식이어야 한다', () => {
Expand Down
14 changes: 7 additions & 7 deletions src/repositories/post.repository.ts
Original file line number Diff line number Diff line change
@@ -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) {}
Expand All @@ -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:
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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
Expand Down
Loading