Skip to content

Commit 60920e6

Browse files
authored
[25.02.27 / TASK-130] Feature - 빠른 조회 기능 구현 및 오류 수정 (#17)
* refactor: DRY 원칙 맞춤 * hotfix: process.env 관련 오류 수정 process.env의 동작 방식으로 인해 구조분해할당이 안 된다고 합니다.. * modify: 타입 단언 * feature: 빠른 조회 기능 구현
1 parent 246cbe6 commit 60920e6

File tree

3 files changed

+87
-70
lines changed

3 files changed

+87
-70
lines changed

src/app/layout.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,13 @@ export const metadata: Metadata = {
1717
description: '어디서든 편리하게 확인하는 Velog 통계 서비스, Velog Dashboard',
1818
icons: { icon: '/favicon.png' },
1919
alternates: {
20-
canonical: 'https://velog-dashboard.kro.kr/',
20+
canonical: BASE,
2121
},
2222
openGraph: {
2323
siteName: 'Velog Dashboard',
2424
description:
2525
'어디서든 편리하게 확인하는 Velog 통계 서비스, Velog Dashboard',
26-
url: 'https://velog-dashboard.kro.kr/',
26+
url: BASE,
2727
images: [{ url: '/opengraph-image.png', alt: 'Velog Dashboard' }],
2828
type: 'website',
2929
},

src/components/auth-required/main/Section/Graph.tsx

+75-56
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
'use client';
22

33
import { Line } from 'react-chartjs-2';
4-
54
import {
65
Chart as ChartJS,
76
CategoryScale,
@@ -13,12 +12,12 @@ import {
1312
Legend,
1413
} from 'chart.js';
1514
import { useQuery } from '@tanstack/react-query';
16-
import { useState } from 'react';
15+
import { useEffect, useState } from 'react';
1716
import { COLORS, PATHS, SCREENS } from '@/constants';
1817
import { Dropdown, Input } from '@/components';
18+
import { PostDetailValue } from '@/types';
1919
import { useResponsive } from '@/hooks';
2020
import { postDetail } from '@/apis';
21-
import { PostDetailValue } from '@/types';
2221

2322
ChartJS.register(
2423
CategoryScale,
@@ -45,60 +44,94 @@ interface IProp {
4544
releasedAt: string;
4645
}
4746

47+
type ModeType = 'none' | 'weekly' | 'monthly' | 'custom';
48+
4849
export const Graph = ({ id, releasedAt }: IProp) => {
4950
const width = useResponsive();
5051

5152
const isMBI = width < SCREENS.MBI;
5253

53-
const [type, setType] = useState({
54-
start: '',
55-
end: '',
56-
type: 'View',
57-
});
54+
const [type, setType] = useState({ start: '', end: '', type: 'View' });
55+
const [mode, setMode] = useState<ModeType>('none');
5856

5957
const { data: datas } = useQuery({
6058
queryKey: [PATHS.DETAIL, type],
6159
queryFn: async () => await postDetail(id, type.start, type.end),
62-
select: ({ post }) => ({
63-
labels: post.map((i) => i.date.split('T')[0]),
64-
datasets: [
65-
{
66-
label: type.type,
67-
data: post.map(
68-
(i) => i[`daily${type.type}Count` as keyof PostDetailValue],
69-
),
70-
...datasets,
71-
},
72-
],
73-
}),
60+
select: ({ post }) => {
61+
post = post.sort(
62+
(a, b) => new Date(a.date).getTime() - new Date(b.date).getTime(),
63+
);
64+
return {
65+
labels: post.map((i) => i.date.split('T')[0]),
66+
datasets: [
67+
{
68+
label: type.type,
69+
data: post.map(
70+
(i) => i[`daily${type.type}Count` as keyof PostDetailValue],
71+
),
72+
...datasets,
73+
},
74+
],
75+
};
76+
},
7477
enabled: !!type.start && !!type.end,
7578
});
7679

80+
useEffect(() => {
81+
if (mode === 'none' || mode === 'custom') {
82+
setType((prev) => ({ ...prev, start: '', end: '' }));
83+
} else {
84+
const start = new Date();
85+
if (mode === 'monthly') start.setMonth(start.getMonth() - 1);
86+
else start.setDate(start.getDate() - 7);
87+
88+
setType((prev) => ({
89+
...prev,
90+
start: start.toISOString().split('T')[0],
91+
end: new Date().toISOString().split('T')[0],
92+
}));
93+
}
94+
}, [mode]);
95+
7796
return (
7897
<div className="w-full bg-BG-SUB flex flex-col items-center px-[25px] pb-[30px] gap-[30px] max-MBI:px-5 max-MBI:pb-10">
7998
<div className="flex items-center gap-[20px] flex-wrap justify-center max-TBL:gap-[10px]">
80-
<Input
81-
size={isMBI ? 'SMALL' : 'MEDIUM'}
82-
form="SMALL"
83-
value={type.start}
84-
min={releasedAt.split('T')[0]}
85-
onChange={(e) => setType({ ...type, start: e.target.value })}
86-
placeholder="시작 날짜"
87-
type="date"
88-
/>
89-
<span className="text-ST4 max-TBL:text-T5 text-TEXT-MAIN">~</span>
90-
<Input
91-
size={isMBI ? 'SMALL' : 'MEDIUM'}
92-
form="SMALL"
93-
value={type.end}
94-
min={type.start ? type.start : releasedAt.split('T')[0]}
95-
onChange={(e) => setType({ ...type, end: e.target.value })}
96-
placeholder="종료 날짜"
97-
type="date"
99+
{mode === 'custom' && (
100+
<>
101+
<Input
102+
size={isMBI ? 'SMALL' : 'MEDIUM'}
103+
form="SMALL"
104+
value={type.start}
105+
min={releasedAt.split('T')[0]}
106+
onChange={(e) => setType({ ...type, start: e.target.value })}
107+
placeholder="시작 날짜"
108+
type="date"
109+
/>
110+
<span className="text-ST4 max-TBL:text-T5 text-TEXT-MAIN">~</span>
111+
<Input
112+
size={isMBI ? 'SMALL' : 'MEDIUM'}
113+
form="SMALL"
114+
value={type.end}
115+
min={type.start ? type.start : releasedAt.split('T')[0]}
116+
onChange={(e) => setType({ ...type, end: e.target.value })}
117+
placeholder="종료 날짜"
118+
type="date"
119+
/>
120+
</>
121+
)}
122+
<Dropdown
123+
onChange={(e) => setMode(e as ModeType)}
124+
defaultValue="미선택"
125+
options={[
126+
['미선택', 'none'],
127+
['지난 7일', 'weekly'],
128+
['지난 30일', 'monthly'],
129+
['직접선택', 'custom'],
130+
]}
98131
/>
99132
<Dropdown
100133
onChange={(e) => setType({ ...type, type: e as string })}
101-
defaultValue={'조회수'}
134+
defaultValue="조회수"
102135
options={[
103136
['조회수', 'View'],
104137
['좋아요', 'Like'],
@@ -120,24 +153,10 @@ export const Graph = ({ id, releasedAt }: IProp) => {
120153
maintainAspectRatio: false,
121154
animation: false,
122155
interaction: { mode: 'nearest', intersect: false },
123-
plugins: {
124-
legend: {
125-
display: false,
126-
},
127-
},
156+
plugins: { legend: { display: false } },
128157
scales: {
129-
x: {
130-
axis: 'x',
131-
grid: {
132-
color: COLORS.BORDER.SUB,
133-
},
134-
},
135-
y: {
136-
axis: 'y',
137-
grid: {
138-
color: COLORS.BORDER.SUB,
139-
},
140-
},
158+
x: { axis: 'x', grid: { color: COLORS.BORDER.SUB } },
159+
y: { axis: 'y', grid: { color: COLORS.BORDER.SUB } },
141160
},
142161
}}
143162
className="w-[100%_!important] h-[auto_!important] max-h-[300px]"

src/constants/env.constant.ts

+10-12
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,16 @@
11
import { EnvNotFoundError } from '../errors/fetch.error';
22

33
export const env = (() => {
4-
const requiredEnv = process?.env as Record<string, string>;
5-
64
const env = {
7-
NODE_ENV: requiredEnv.NODE_ENV,
8-
BASE_URL: requiredEnv.NEXT_PUBLIC_BASE_URL,
9-
ABORT_MS: requiredEnv.NEXT_PUBLIC_ABORT_MS,
10-
EVENT_LOG: requiredEnv.NEXT_PUBLIC_EVENT_LOG,
11-
VELOG_URL: requiredEnv.NEXT_PUBLIC_VELOG_URL,
12-
CHANNELTALK_PLUGIN_KEY: requiredEnv.NEXT_PUBLIC_CHANNELTALK_PLUGIN_KEY,
13-
GA_ID: requiredEnv.NEXT_PUBLIC_GA_ID,
14-
SENTRY_AUTH_TOKEN: requiredEnv.NEXT_PUBLIC_SENTRY_AUTH_TOKEN,
15-
SENTRY_DSN: requiredEnv.NEXT_PUBLIC_SENTRY_DSN,
5+
NODE_ENV: process.env.NODE_ENV,
6+
BASE_URL: process.env.NEXT_PUBLIC_BASE_URL,
7+
ABORT_MS: process.env.NEXT_PUBLIC_ABORT_MS,
8+
EVENT_LOG: process.env.NEXT_PUBLIC_EVENT_LOG,
9+
VELOG_URL: process.env.NEXT_PUBLIC_VELOG_URL,
10+
CHANNELTALK_PLUGIN_KEY: process.env.NEXT_PUBLIC_CHANNELTALK_PLUGIN_KEY,
11+
GA_ID: process.env.NEXT_PUBLIC_GA_ID,
12+
SENTRY_AUTH_TOKEN: process.env.NEXT_PUBLIC_SENTRY_AUTH_TOKEN,
13+
SENTRY_DSN: process.env.NEXT_PUBLIC_SENTRY_DSN,
1614
} as const;
1715

1816
if (env.NODE_ENV) {
@@ -21,5 +19,5 @@ export const env = (() => {
2119
});
2220
}
2321

24-
return env;
22+
return env as Record<string, string>;
2523
})();

0 commit comments

Comments
 (0)