Skip to content

Commit ea0407f

Browse files
committed
refactor: SVG Badge API 구조 개선 및 응답 Json으로 변경
refactor: SVG Badge API 구조 개선 및 응답 Json으로 변경 - assets, withrank 쿼리 파라미터 삭제 - API 응답 Svg에서 Json으로 변경
1 parent 1b8086b commit ea0407f

File tree

8 files changed

+164
-166
lines changed

8 files changed

+164
-166
lines changed

src/controllers/svg.controller.ts

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,11 @@ export class SvgController {
1313
) => {
1414
try {
1515
const { username } = req.params;
16-
const { type = 'default', assets = 'views,likes,posts', withrank = 'false'} = req.query;
16+
const { type = 'default'} = req.query;
1717

18-
const svgString = await this.svgService.generateBadgeSvg(
19-
username,
20-
type,
21-
assets,
22-
withrank === 'true',
23-
);
18+
const data = await this.svgService.getBadgeData(username, type);
2419

25-
res.setHeader('Content-Type', 'image/svg+xml');
26-
res.setHeader('Cache-Control', 'public, max-age=1800');
27-
res.send(svgString);
20+
res.json(data);
2821
} catch (error) {
2922
logger.error('SVG Badge 생성 실패: ', error);
3023
next(error);

src/repositories/leaderboard.repository.ts

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import logger from '@/configs/logger.config';
22
import { Pool } from 'pg';
3-
import { DBError } from '@/exception';
3+
import { DBError, NotFoundError } from '@/exception';
44
import { UserLeaderboardSortType, PostLeaderboardSortType } from '@/types/index';
55
import { getCurrentKSTDateString, getKSTDateStringWithOffset } from '@/utils/date.util';
66

@@ -82,6 +82,74 @@ export class LeaderboardRepository {
8282
}
8383
}
8484

85+
async getUserStats(username: string, dateRange: number = 30) {
86+
try {
87+
const pastDateKST = getKSTDateStringWithOffset(-dateRange * 24 * 60);
88+
const cteQuery = this.buildLeaderboardCteQuery(dateRange, pastDateKST);
89+
90+
const query = `
91+
${cteQuery}
92+
SELECT
93+
u.username,
94+
COALESCE(SUM(ts.today_view), 0) AS total_views,
95+
COALESCE(SUM(ts.today_like), 0) AS total_likes,
96+
COUNT(DISTINCT CASE WHEN p.is_active = true THEN p.id END) AS total_posts,
97+
SUM(COALESCE(ts.today_view, 0) - COALESCE(ss.start_view, 0)) AS view_diff,
98+
SUM(COALESCE(ts.today_like, 0) - COALESCE(ss.start_like, 0)) AS like_diff,
99+
COUNT(DISTINCT CASE WHEN p.released_at >= '${pastDateKST}' AND p.is_active = true THEN p.id END) AS post_diff
100+
FROM users_user u
101+
LEFT JOIN posts_post p ON p.user_id = u.id
102+
LEFT JOIN today_stats ts ON ts.post_id = p.id
103+
LEFT JOIN start_stats ss ON ss.post_id = p.id
104+
WHERE u.username = $1
105+
GROUP BY u.username
106+
`;
107+
108+
const result = await this.pool.query(query, [username]);
109+
110+
if (result.rows.length === 0) {
111+
throw new NotFoundError(`사용자를 찾을 수 없습니다: ${username}`);
112+
}
113+
114+
return result.rows[0];
115+
} catch (error) {
116+
if (error instanceof NotFoundError) throw error;
117+
logger.error('LeaderboardRepository getUserStats error:', error);
118+
throw new DBError('사용자 통계 조회 중 문제가 발생했습니다.');
119+
}
120+
}
121+
122+
async getRecentPosts(username: string, dateRange: number = 30, limit: number = 3) {
123+
try {
124+
const pastDateKST = getKSTDateStringWithOffset(-dateRange * 24 * 60);
125+
const cteQuery = this.buildLeaderboardCteQuery(dateRange, pastDateKST);
126+
127+
const query = `
128+
${cteQuery}
129+
SELECT
130+
p.title,
131+
p.released_at,
132+
COALESCE(ts.today_view, 0) AS today_view,
133+
COALESCE(ts.today_like, 0) AS today_like,
134+
(COALESCE(ts.today_view, 0) - COALESCE(ss.start_view, 0)) AS view_diff
135+
FROM posts_post p
136+
JOIN users_user u ON u.id = p.user_id
137+
LEFT JOIN today_stats ts ON ts.post_id = p.id
138+
LEFT JOIN start_stats ss ON ss.post_id = p.id
139+
WHERE u.username = $1
140+
AND p.is_active = true
141+
ORDER BY p.released_at DESC
142+
LIMIT $2
143+
`;
144+
145+
const result = await this.pool.query(query, [username, limit]);
146+
return result.rows;
147+
} catch (error) {
148+
logger.error('LeaderboardRepository getRecentPosts error:', error);
149+
throw new DBError('최근 게시글 조회 중 문제가 발생했습니다.');
150+
}
151+
}
152+
85153
// 오늘 날짜와 기준 날짜의 통계를 가져오는 CTE(임시 결과 집합) 쿼리 빌드
86154
private buildLeaderboardCteQuery(dateRange: number, pastDateKST?: string) {
87155
// KST 기준 00시~01시 (UTC 15:00~16:00) 사이라면 전날 데이터를 사용

src/repositories/svg.repository.ts

Lines changed: 0 additions & 100 deletions
This file was deleted.

src/routes/svg.router.ts

Lines changed: 31 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,22 @@
11
import pool from '@/configs/db.config';
22
import express, { Router } from 'express';
3-
import { SvgRepository } from '@/repositories/svg.repository';
3+
import { LeaderboardRepository } from '@/repositories/leaderboard.repository';
44
import { SvgService } from '@/services/svg.service';
55
import { SvgController } from '@/controllers/svg.controller';
66
import { validateRequestDto } from '@/middlewares/validation.middleware';
77
import { GetSvgBadgeQueryDto } from '@/types';
88

99
const router: Router = express.Router();
1010

11-
const svgRepository = new SvgRepository(pool);
12-
const svgService = new SvgService(svgRepository);
11+
const leaderboardRepository = new LeaderboardRepository(pool);
12+
const svgService = new SvgService(leaderboardRepository);
1313
const svgController = new SvgController(svgService);
1414

1515
/**
1616
* @swagger
1717
* /api/{username}/badge:
1818
* get:
19-
* summary: 사용자 배지 SVG 조회
19+
* summary: 사용자 배지 데이터 조회
2020
* tags:
2121
* - SVG
2222
* security: []
@@ -27,37 +27,46 @@ const svgController = new SvgController(svgService);
2727
* schema:
2828
* type: string
2929
* description: 조회할 사용자명
30-
* example: six-standard
30+
* example: ljh3478
3131
* - in: query
3232
* name: type
3333
* schema:
3434
* type: string
3535
* enum: [default, simple]
3636
* default: default
37-
* - in: query
38-
* name: assets
39-
* schema:
40-
* type: string
41-
* default: views,likes,posts
42-
* example: views,likes,posts
43-
* - in: query
44-
* name: withrank
45-
* schema:
46-
* type: string
47-
* enum: [true, false]
48-
* default: false
4937
* responses:
5038
* '200':
51-
* description: SVG 배지 생성 성공
39+
* description: 배지 데이터 조회 성공
5240
* content:
53-
* image/svg+xml:
41+
* application/json:
5442
* schema:
55-
* type: string
56-
* example: <svg>...</svg>
43+
* type: object
44+
* properties:
45+
* user:
46+
* type: object
47+
* properties:
48+
* username:
49+
* type: string
50+
* totalViews:
51+
* type: number
52+
* totalLikes:
53+
* type: number
54+
* totalPosts:
55+
* type: number
56+
* viewDiff:
57+
* type: number
58+
* likeDiff:
59+
* type: number
60+
* postDiff:
61+
* type: number
62+
* recentPosts:
63+
* type: array
64+
* items:
65+
* type: object
5766
* '404':
5867
* description: 사용자를 찾을 수 없음
5968
* '500':
60-
* description: 서버 오류 / 데이터 베이스 조회 오류
69+
* description: 서버 오류
6170
*/
6271
router.get(
6372
'/:username/badge',

src/services/svg.service.ts

Lines changed: 31 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,41 @@
11
import logger from '@/configs/logger.config';
2-
import { SvgBadgeType } from '@/types';
3-
import { SvgRepository } from '@/repositories/svg.repository';
2+
import { BadgeDataResponseDto, SvgBadgeType } from '@/types';
3+
import { LeaderboardRepository } from '@/repositories/leaderboard.repository';
44

55
export class SvgService {
6-
constructor(private svgRepo: SvgRepository) {}
6+
constructor(private leaderboardRepo: LeaderboardRepository) {}
77

8-
async generateBadgeSvg(
8+
async getBadgeData(
99
username: string,
1010
type: SvgBadgeType,
11-
assets: string,
12-
withRank: boolean,
13-
): Promise<string> {
11+
dateRange: number = 30,
12+
): Promise<BadgeDataResponseDto> {
1413
try {
15-
const data = await this.svgRepo.getUserBadgeData(username, withRank);
16-
17-
if (type === 'simple') {
18-
return this.generateSimpleSvg(data, assets);
19-
} else {
20-
return this.generateDefaultSvg(data, assets, withRank);
21-
}
14+
const userStats = await this.leaderboardRepo.getUserStats(username, dateRange);
15+
const recentPosts = type === 'default'
16+
? await this.leaderboardRepo.getRecentPosts(username, dateRange, 3)
17+
: [];
18+
19+
return new BadgeDataResponseDto(
20+
{
21+
username: userStats.username,
22+
totalViews: Number(userStats.total_views),
23+
totalLikes: Number(userStats.total_likes),
24+
totalPosts: Number(userStats.total_posts),
25+
viewDiff: Number(userStats.view_diff),
26+
likeDiff: Number(userStats.like_diff),
27+
postDiff: Number(userStats.post_diff),
28+
},
29+
recentPosts.map(post => ({
30+
title: post.title,
31+
releasedAt: post.released_at,
32+
viewCount: Number(post.today_view),
33+
likeCount: Number(post.today_like),
34+
viewDiff: Number(post.view_diff),
35+
}))
36+
)
2237
} catch (error) {
23-
logger.error('SvgService generateBadgeSvg error: ', error);
38+
logger.error('SvgService getBadgeData error: ', error);
2439
throw error;
2540
}
2641
}
@@ -43,16 +58,4 @@ export class SvgService {
4358
${withRank ? `<text x="20" y="120" fill="white">Rank: #${data.view_rank || 'N/A'}</text>` : ''}
4459
</svg>`;
4560
}
46-
47-
private formatNumber(num: number): string {
48-
if (num >= 1000000) {
49-
return (num / 1000000).toFixed(1) + 'm';
50-
}
51-
52-
if (num >= 1000) {
53-
return (num / 1000).toFixed(1) + 'k';
54-
}
55-
56-
return num.toString();
57-
}
58-
}
61+
}

src/types/dto/requests/svgRequest.type.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,6 @@ export interface GetSvgBadgeParams {
88

99
export interface GetSvgBadgeQuery {
1010
type?: SvgBadgeType;
11-
assets?: string;
12-
withrank?: string;
1311
}
1412

1513
export class GetSvgBadgeQueryDto {
@@ -25,9 +23,7 @@ export class GetSvgBadgeQueryDto {
2523
@IsEnum(['true', 'false'])
2624
withrank?: string;
2725

28-
constructor(type?: SvgBadgeType, assets?: string, withrank?: string) {
26+
constructor(type?: SvgBadgeType) {
2927
this.type = type || 'default';
30-
this.assets = assets || 'views,likes,posts'
31-
this.withrank = withrank || 'false';
3228
}
3329
}

0 commit comments

Comments
 (0)