Skip to content

Commit fbabcb9

Browse files
HA0N1Nuung
andauthored
[24.12.23/ TASK-59] feature/fetching-all-posts (#8)
* refactor: 응답값 통일 * feature: 전체 포스트 조회, 커서 기반 페이지네이션 적용 전 * feature: 로그인 시 token HttpCookie에 저장 * feature: 최신순 cursor pagination * modify: 역할 분리 * modify: error log 통일 * modify: 필요없는 주석 제거 * modify: 변수명 수정 * modify: get all post 부분 SQL 수정 * feature: sort 와 asc 값, 러프한 interface 들 추가 * hotfix: user token cookie set 부분 핫픽스 * hotfix: user token cookie set 부분 핫픽스 v2 * feature: timezone 먹은 post created-at 값도 같이 return * hotfix: 카멜 케이스 업데이트 * modify: limit 5 개에서 15 개로 * refactor: 전체 post 조회 시 yesterday views/likes 추가 * feature: 패널 통계 전용 API * refactor: null일 시 0으로 반환 * refactor: 전체 post 조회 api 오늘과 어제 저장한 데이터가 없다면 빈배열로 반환 * modify: 쿠키 개발 환경울 위한 세팅 분리, 포스트 릴리즈 데이터 추가 * refactor: 로그인 시 쿠키 저장 수정 * refactor: 각 api 응답값 타입화 * feature: 로그아웃 기능 추가 * modify: 로그인 쿠키 클리어 추가 * hotfix: 좌측 통계 어제값 null 에서 0으로 조회되게 변경 * feture: 유저 조회 API * modify: 미들 웨어 auth, 줄 바꿈과 jsdocs 추가 * hotfix: 콘솔로그 제거 * hotfix: cookie maxAge 제거 * hotfix: 어제 데이터 값 정상적으로 나오게 수정 * hotfix: 전체 포스트 조회 시 어제 데이터 정상적으로 나오게 수정 * modify: conflict clear * modify: timezone 연산 이슈 V2 * feature: 커서 기반 페이지네이션 업데이트 * modify: 컨트롤러 롤백 이슈 원복 --------- Co-authored-by: Nuung <[email protected]>
1 parent cc7e25f commit fbabcb9

29 files changed

+494
-75
lines changed

.env.sample

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,22 @@
11
PORT=8080
2-
3-
DATABASE_NAME=""
4-
POSTGRES_USER=""
5-
POSTGRES_PASSWORD=""
6-
POSTGRES_HOST=""
7-
POSTGRES_PORT=5432
8-
AES_KEY_0="13ccb93c17a8d6e49ba3c5d91e3a6f45"
9-
AES_KEY_1="76e2a34bf23cd45876bc91e6a87d3f22"
10-
AES_KEY_2="93a4d7e6b34ac8f1092fd5e87a93bc56"
11-
AES_KEY_3="b87f6c34d92fa17e3b2a67e58c93fa56"
12-
AES_KEY_4="27e3b8f67c4a12d8f93a6b17d4e58fa9"
13-
AES_KEY_5="6b83c9e21a7d48fa3f9b27e45c8a6f12"
14-
AES_KEY_6="81f92ab73c4e59d8a67f83b21d6e4c53"
15-
AES_KEY_7="d87b61e9f34a2c85f19a7e53c6d8f21a"
16-
AES_KEY_8="7c58f92ae1d3b67a4c29f8b36e17d4f9"
17-
AES_KEY_9="a93b4f7e2c6d81a7f5c3b2e89d47f612"
182
ALLOWED_ORIGINS="http://localhost:3000,https://myapp.com"
193
NODE_ENV="development"
4+
5+
# 토큰 단방향 암호화 전용 핵심 키
6+
AES_KEY_0=13ccb93c17a8d6e49ba3c5d91e3a6f45
7+
AES_KEY_1=76e2a34bf23cd45876bc91e6a87d3f22
8+
AES_KEY_2=93a4d7e6b34ac8f1092fd5e87a93bc56
9+
AES_KEY_3=b87f6c34d92fa17e3b2a67e58c93fa56
10+
AES_KEY_4=27e3b8f67c4a12d8f93a6b17d4e58fa9
11+
AES_KEY_5=6b83c9e21a7d48fa3f9b27e45c8a6f12
12+
AES_KEY_6=81f92ab73c4e59d8a67f83b21d6e4c53
13+
AES_KEY_7=d87b61e9f34a2c85f19a7e53c6d8f21a
14+
AES_KEY_8=7c58f92ae1d3b67a4c29f8b36e17d4f9
15+
AES_KEY_9=a93b4f7e2c6d81a7f5c3b2e89d47f612
16+
17+
# Database
18+
DATABASE_NAME=vd2
19+
POSTGRES_USER=vd2
20+
POSTGRES_PASSWORD=vd2
21+
POSTGRES_HOST=localhost
22+
POSTGRES_PORT=5432

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
```bash
1212
pnpm install
13+
NODE_ENV=development pnpm install # devDpe 설치 위해
1314

1415
# 만약 pnpm 이 없다면
1516
brew install pnpm

eslint.config.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ export default typescriptEslint.config(
4141
},
4242
],
4343
'@typescript-eslint/naming-convention': [
44-
'error',
44+
'warn',
4545
{
4646
selector: 'typeLike',
4747
format: ['PascalCase'],

src/app.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ app.use(
1717
cors({
1818
origin: process.env.NODE_ENV === 'production' ? process.env.ALLOWED_ORIGINS?.split(',') : 'http://localhost:3000',
1919
methods: ['GET', 'POST'],
20-
allowedHeaders: ['Content-Type', 'Authorization', 'Cookie'],
20+
allowedHeaders: ['Content-Type', 'Authorization', 'Cookie', 'access_token', 'refresh_token'],
2121
credentials: true,
2222
}),
2323
);

src/configs/db.config.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,16 @@ const { Pool } = pg;
77
dotenv.config();
88

99
const pool = new Pool({
10+
database: process.env.DATABASE_NAME,
1011
user: process.env.POSTGRES_USER,
1112
host: process.env.POSTGRES_HOST,
12-
database: process.env.DATABASE_NAME,
1313
password: process.env.POSTGRES_PASSWORD,
1414
port: Number(process.env.POSTGRES_PORT),
1515
ssl: {
1616
rejectUnauthorized: false,
1717
},
1818
});
1919

20-
// timescaleDB 확장. 최초 1회 이므로 즉시실행 함수로
2120
(async () => {
2221
const client = await pool.connect();
2322
try {

src/constants/velog.constans.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/* eslint-disable @typescript-eslint/naming-convention */
2+
export const VELOG_API_URL = 'https://v3.velog.io/graphql';
3+
export const VELOG_QUERIES = {
4+
LOGIN: `query currentUser {
5+
currentUser {
6+
id
7+
username
8+
email
9+
profile {
10+
thumbnail
11+
}
12+
}
13+
}`,
14+
};

src/controllers/post.controller.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { NextFunction, Request, RequestHandler, Response } from 'express';
2+
import logger from '../configs/logger.config';
3+
import { PostService } from '../services/post.service';
4+
import { GetAllPostsQuery, PostResponse } from '../types';
5+
6+
export class PostController {
7+
constructor(private postService: PostService) {}
8+
9+
private validateQueryParams(query: GetAllPostsQuery): {
10+
cursor: string | undefined;
11+
sort: string;
12+
isAsc: boolean;
13+
} {
14+
return {
15+
cursor: query.cursor,
16+
sort: query.sort || '',
17+
isAsc: query.asc === 'true',
18+
};
19+
}
20+
21+
getAllPost: RequestHandler = async (
22+
req: Request<object, object, object, GetAllPostsQuery>,
23+
res: Response<PostResponse>,
24+
next: NextFunction,
25+
) => {
26+
try {
27+
const { id } = req.user;
28+
const { cursor, sort, isAsc } = this.validateQueryParams(req.query);
29+
30+
const result = await this.postService.getAllposts(id, cursor, sort, isAsc);
31+
32+
res.status(200).json({
33+
success: true,
34+
message: 'post 전체 조회에 성공하였습니다.',
35+
data: {
36+
nextCursor: result.nextCursor,
37+
posts: result.posts,
38+
},
39+
error: null,
40+
});
41+
} catch (error) {
42+
logger.error('전체 조회 실패:', error);
43+
next(error);
44+
}
45+
};
46+
getAllPostStatistics: RequestHandler = async (req: Request, res: Response, next: NextFunction) => {
47+
try {
48+
const { id } = req.user;
49+
50+
const result = await this.postService.getAllPostStatistics(id);
51+
const totalPostCount = await this.postService.getTotalPostCounts(id);
52+
53+
res.status(200).json({
54+
success: true,
55+
message: 'post 전체 통계 조회에 성공하였습니다.',
56+
data: { totalPostCount, stats: result },
57+
error: null,
58+
});
59+
} catch (error) {
60+
logger.error('전체 통계 조회 실패:', error);
61+
next(error);
62+
}
63+
};
64+
}

src/controllers/tracking.controller.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,31 @@
11
import { NextFunction, Request, RequestHandler, Response } from 'express';
22
import logger from '../configs/logger.config';
33
import { TrackingService } from '../services/tracking.service';
4+
import { TrackingResponse } from '../types';
45

56
export class TrackingController {
67
constructor(private trackingService: TrackingService) {}
78

8-
event = (async (req: Request, res: Response, next: NextFunction) => {
9+
event = (async (req: Request, res: Response<TrackingResponse>, next: NextFunction) => {
910
try {
1011
const { type } = req.body;
1112
const { id } = req.user;
1213

1314
await this.trackingService.tracking(type, id);
14-
return res.status(200).json({ success: true, message: '이벤트 데이터 저장완료' });
15+
return res.status(200).json({ success: true, message: '이벤트 데이터 저장완료', data: {}, error: null });
1516
} catch (error) {
1617
logger.error('user tracking 실패 : ', error);
1718
next(error);
1819
}
1920
}) as RequestHandler;
2021

21-
stay = (async (req: Request, res: Response, next: NextFunction) => {
22+
stay = (async (req: Request, res: Response<TrackingResponse>, next: NextFunction) => {
2223
try {
2324
const { loadDate, unloadDate } = req.body;
2425
const { id } = req.user;
2526

2627
await this.trackingService.stay({ loadDate, unloadDate }, id);
27-
return res.status(200).json({ success: true, message: '체류시간 데이터 완료' });
28+
return res.status(200).json({ success: true, message: '체류시간 데이터 저장 완료', data: {}, error: null });
2829
} catch (error) {
2930
logger.error('user stay time 저장 실패 : ', error);
3031
next(error);

src/controllers/user.controller.ts

Lines changed: 48 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,66 @@
1-
import { NextFunction, Request, Response, RequestHandler } from 'express';
1+
import { NextFunction, Request, Response, RequestHandler, CookieOptions } from 'express';
22
import logger from '../configs/logger.config';
3-
import { UserWithTokenDto } from '../types';
3+
import { LoginResponse, UserWithTokenDto } from '../types';
44
import { UserService } from '../services/user.service';
5-
65
export class UserController {
76
constructor(private userService: UserService) {}
87

9-
login = (async (req: Request, res: Response, next: NextFunction) => {
8+
private cookieOption(): CookieOptions {
9+
const isProd = process.env.NODE_ENV === 'production';
10+
11+
const baseOptions: CookieOptions = {
12+
httpOnly: isProd,
13+
secure: isProd,
14+
domain: process.env.COOKIE_DOMAIN || 'localhost',
15+
};
16+
17+
if (isProd) {
18+
baseOptions.sameSite = 'lax';
19+
}
20+
21+
return baseOptions;
22+
}
23+
24+
login: RequestHandler = async (req: Request, res: Response<LoginResponse>, next: NextFunction): Promise<void> => {
1025
try {
11-
const { id, email, profile } = req.user;
26+
const { id, email, profile, username } = req.user;
1227
const { accessToken, refreshToken } = req.tokens;
1328

1429
const userWithToken: UserWithTokenDto = { id, email, accessToken, refreshToken };
1530
const isExistUser = await this.userService.handleUserTokensByVelogUUID(userWithToken);
16-
return res.status(200).json({
31+
32+
res.clearCookie('access_token');
33+
res.clearCookie('refresh_token');
34+
35+
res.cookie('access_token', accessToken, this.cookieOption());
36+
res.cookie('refresh_token', refreshToken, this.cookieOption());
37+
38+
res.status(200).json({
1739
success: true,
1840
message: '로그인에 성공하였습니다.',
19-
data: { id: isExistUser.id, email: isExistUser.email, profile },
41+
data: { id: isExistUser.id, username, profile },
42+
error: null,
2043
});
2144
} catch (error) {
2245
logger.error('로그인 실패 : ', error);
2346
next(error);
2447
}
25-
}) as RequestHandler;
48+
};
49+
50+
logout: RequestHandler = async (req: Request, res: Response) => {
51+
res.clearCookie('access_token');
52+
res.clearCookie('refresh_token');
53+
54+
res.status(200).json({ success: true, message: '로그아웃에 성공하였습니다.', data: {}, error: null });
55+
};
56+
57+
fetchCurrentUser: RequestHandler = (req: Request, res: Response) => {
58+
const { user } = req;
59+
res.status(200).json({
60+
success: true,
61+
message: '프로필 조회에 성공하였습니다.',
62+
data: { user },
63+
error: null,
64+
});
65+
};
2666
}

src/exception/token.exception.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ export class TokenError extends CustomError {
77
}
88
}
99

10-
// todo : 추후 만료 여부를 위해 token 에러 구체화 필요. 웬만한 인증관련은 unauthorized로 넘기는게 나을듯
1110
export class TokenExpiredError extends UnauthorizedError {
1211
constructor(message = '토큰이 만료되었습니다') {
1312
super(message, 'TOKEN_EXPIRED');

0 commit comments

Comments
 (0)