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
1 change: 1 addition & 0 deletions .env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@ NEXT_PUBLIC_BASE_URL=<'server url here'>
NEXT_PUBLIC_VELOG_URL=https://velog.io
NEXT_PUBLIC_ABORT_MS=<'abort time(ms) for fetch here'>
SENTRY_AUTH_TOKEN=<'sentry auth token here'>
NEXT_PUBLIC_CHANNELTALK_PLUGIN_KEY=<'channelTalk plugin key here'>
NEXT_PUBLIC_EVENT_LOG=<'Whether to send an event log here (true | false)'>
SENTRY_DSN=<'sentry dsn here'>
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"test": "jest"
},
"dependencies": {
"@channel.io/channel-web-sdk-loader": "^2.0.0",
"@sentry/nextjs": "^8.47.0",
"@tanstack/react-query": "^5.61.3",
"@tanstack/react-query-devtools": "^5.62.11",
Expand Down
8 changes: 8 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 4 additions & 9 deletions src/apis/dashboard.request.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,21 @@
import { PostDetailDto, PostListDto, PostSummaryDto } from '@/types';
import { PATHS } from '@/constants';
import { InitType, instance } from './instance.request';
import { instance } from './instance.request';

type SortType = {
asc: boolean;
sort: string;
};

export const postList = async (
props: InitType<PostListDto>,
sort: SortType,
cursor?: string,
) =>
export const postList = async (sort: SortType, cursor?: string) =>
await instance<null, PostListDto>(
cursor
? `${PATHS.POSTS}?cursor=${cursor}&asc=${sort.asc}&sort=${sort.sort}`
: `${PATHS.POSTS}?asc=${sort.asc}&sort=${sort.sort}`,
props,
);

export const postSummary = async (props: InitType<PostSummaryDto>) =>
await instance<null, PostSummaryDto>(PATHS.SUMMARY, props);
export const postSummary = async () =>
await instance<null, PostSummaryDto>(PATHS.SUMMARY);

export const postDetail = async (path: string, start: string, end: string) =>
await instance<null, PostDetailDto>(
Expand Down
17 changes: 14 additions & 3 deletions src/apis/instance.request.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
import returnFetch, { FetchArgs } from 'return-fetch';

import { captureException, setContext } from '@sentry/nextjs';
import { ServerNotRespondingError } from '@/errors';
import { EnvNotFoundError, ServerNotRespondingError } from '@/errors';

const BASE_URL = process.env.NEXT_PUBLIC_BASE_URL;
const ABORT_MS = Number(process.env.NEXT_PUBLIC_ABORT_MS);

if (Number.isNaN(ABORT_MS)) {
throw new Error('ABORT_MS가 ENV에서 설정되지 않았습니다');
throw new EnvNotFoundError('ABORT_MS');
}

if (!BASE_URL) {
throw new Error('BASE_URL이 ENV에서 설정되지 않았습니다.');
throw new EnvNotFoundError('BASE_URL');
}

type ErrorType = {
Expand Down Expand Up @@ -60,9 +60,20 @@ export const instance = async <I, R>(
init?: InitType<I>,
error?: Record<string, Error>,
): Promise<R> => {
let cookieHeader = '';
if (typeof window === 'undefined') {
cookieHeader = (await import('next/headers')).cookies().toString();
}

try {
const data = await fetch('/api' + input, {
...init,
headers: cookieHeader
? {
...init?.headers,
Cookie: cookieHeader,
}
: init?.headers,
body: init?.body ? JSON.stringify(init.body) : undefined,
signal: AbortSignal.timeout
? AbortSignal.timeout(ABORT_MS)
Expand Down
5 changes: 2 additions & 3 deletions src/apis/user.request.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { NotFoundError } from '@/errors';
import { PATHS } from '@/constants';
import { LoginVo, UserDto } from '@/types';
import { InitType, instance } from './instance.request';
import { instance } from './instance.request';

export const login = async (body: LoginVo) =>
await instance(
Expand All @@ -15,8 +15,7 @@ export const login = async (body: LoginVo) =>
},
);

export const me = async (props: InitType<UserDto>) =>
await instance<null, UserDto>(PATHS.ME, props);
export const me = async () => await instance<null, UserDto>(PATHS.ME);

export const logout = async () =>
await instance(PATHS.LOGOUT, { method: 'POST', body: undefined });
Expand Down
5 changes: 1 addition & 4 deletions src/app/(with-tracker)/(auth-required)/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import { ReactElement } from 'react';
import { dehydrate, HydrationBoundary } from '@tanstack/react-query';
import { cookies } from 'next/headers';
import { Header } from '@/components';
import { PATHS } from '@/constants';
import { getCookieForAuth } from '@/utils/cookieUtil';
import { me } from '@/apis';
import { getQueryClient } from '@/utils/queryUtil';

Expand All @@ -16,8 +14,7 @@ export default async function Layout({ children }: IProp) {

await client.prefetchQuery({
queryKey: [PATHS.ME],
queryFn: async () =>
await me(getCookieForAuth(cookies, ['access_token', 'refresh_token'])),
queryFn: me,
});

return (
Expand Down
3 changes: 1 addition & 2 deletions src/app/(with-tracker)/(auth-required)/main/Content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ export const Content = () => {
queryKey: [PATHS.POSTS, [searchParams.asc, searchParams.sort]], // Query Key
queryFn: async ({ pageParam = '' }) =>
await postList(
{},
{ asc: searchParams.asc === 'true', sort: searchParams.sort || '' },
pageParam,
),
Expand All @@ -41,7 +40,7 @@ export const Content = () => {

const { data: summaries } = useQuery({
queryKey: [PATHS.SUMMARY],
queryFn: async () => await postSummary({}),
queryFn: postSummary,
});

useEffect(() => {
Expand Down
18 changes: 5 additions & 13 deletions src/app/(with-tracker)/(auth-required)/main/page.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import { dehydrate, HydrationBoundary } from '@tanstack/react-query';
import { Metadata } from 'next';
import { cookies } from 'next/headers';
import { PATHS } from '@/constants';
import { postList, postSummary } from '@/apis';
import { getCookieForAuth } from '@/utils/cookieUtil';
import { getQueryClient } from '@/utils/queryUtil';
import { Content } from './Content';

Expand All @@ -25,22 +23,16 @@ export default async function Page({ searchParams }: IProp) {
await client.prefetchInfiniteQuery({
queryKey: [PATHS.POSTS, [searchParams.asc, searchParams.sort]],
queryFn: async () =>
await postList(
getCookieForAuth(cookies, ['access_token', 'refresh_token']),
{
asc: searchParams.asc === 'true',
sort: searchParams.sort || '',
},
),
await postList({
asc: searchParams.asc === 'true',
sort: searchParams.sort || '',
}),
initialPageParam: undefined,
});

await client.prefetchQuery({
queryKey: [PATHS.SUMMARY],
queryFn: async () =>
await postSummary(
getCookieForAuth(cookies, ['access_token', 'refresh_token']),
),
queryFn: postSummary,
});

return (
Expand Down
8 changes: 5 additions & 3 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import * as sentry from '@sentry/nextjs';
import type { Metadata } from 'next';
import { ReactNode } from 'react';
import './globals.css';
import { QueryProvider } from '@/components';
import { ChannelTalkProvider, QueryProvider } from '@/components';

export const metadata: Metadata = {
title: 'Velog Dashboard',
Expand All @@ -23,8 +23,10 @@ export default function RootLayout({
<body className={`${NotoSansKr.className} w-full bg-BG-MAIN`}>
<sentry.ErrorBoundary>
<QueryProvider>
<ToastContainer autoClose={2000} />
{children}
<ChannelTalkProvider>
<ToastContainer autoClose={2000} />
{children}
</ChannelTalkProvider>
</QueryProvider>
</sentry.ErrorBoundary>
</body>
Expand Down
4 changes: 2 additions & 2 deletions src/components/auth-required/header/Section.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,13 @@ type PropType<T extends clickType> = T extends 'link'
? Partial<BaseType> & {
clickType: 'function';
action: () => void;
children: React.ReactNode | React.ReactNode[];
children: React.ReactNode;
}
: T extends 'none'
? Partial<BaseType> & {
clickType: 'none';
action?: undefined;
children: React.ReactNode | React.ReactNode[];
children: React.ReactNode;
}
: never;

Expand Down
13 changes: 7 additions & 6 deletions src/components/auth-required/header/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,15 +37,13 @@ export const Header = () => {

const { mutate: out } = useMutation({
mutationFn: logout,
onSuccess: () => {
client.removeQueries();
router.replace('/');
},
onMutate: () => router.replace('/'),
onSuccess: () => client.removeQueries(),
});

const { data: profiles } = useQuery({
queryKey: [PATHS.ME],
queryFn: async () => me({}),
queryFn: me,
});

useEffect(() => {
Expand All @@ -62,7 +60,10 @@ export const Header = () => {
return (
<nav className="w-full max-MBI:flex max-MBI:justify-center">
<div className="flex w-fit">
<Section clickType="none">
<Section
clickType="function"
action={() => router.replace(`/main${PARAMS.MAIN}`)}
>
<Image
width={35}
height={35}
Expand Down
16 changes: 3 additions & 13 deletions src/components/auth-required/main/Section/Graph.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@ import {
Tooltip,
Legend,
} from 'chart.js';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { useMemo, useState } from 'react';
import { useQuery } from '@tanstack/react-query';
import { useState } from 'react';
import { COLORS, PATHS, SCREENS } from '@/constants';
import { Dropdown, Input } from '@/components';
import { useResponsive } from '@/hooks';
import { postDetail } from '@/apis';
import { PostDetailValue, PostSummaryDto } from '@/types';
import { PostDetailValue } from '@/types';

ChartJS.register(
CategoryScale,
Expand Down Expand Up @@ -47,14 +47,6 @@ interface IProp {

export const Graph = ({ id, releasedAt }: IProp) => {
const width = useResponsive();
const client = useQueryClient();
const maxDate = useMemo(
() =>
(
client.getQueryData([PATHS.SUMMARY]) as PostSummaryDto
)?.stats.lastUpdatedDate.split('T')[0],
[],
);

const isMBI = width < SCREENS.MBI;

Expand Down Expand Up @@ -90,7 +82,6 @@ export const Graph = ({ id, releasedAt }: IProp) => {
form="SMALL"
value={type.start}
min={releasedAt.split('T')[0]}
max={maxDate}
onChange={(e) => setType({ ...type, start: e.target.value })}
placeholder="시작 날짜"
type="date"
Expand All @@ -101,7 +92,6 @@ export const Graph = ({ id, releasedAt }: IProp) => {
form="SMALL"
value={type.end}
min={type.start ? type.start : releasedAt.split('T')[0]}
max={maxDate}
onChange={(e) => setType({ ...type, end: e.target.value })}
placeholder="종료 날짜"
type="date"
Expand Down
22 changes: 15 additions & 7 deletions src/components/auth-required/main/Section/index.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,30 @@
'use client';

import { useState } from 'react';
import { useQueryClient } from '@tanstack/react-query';
import { useState } from 'react';
import { EnvNotFoundError, UserNameNotFoundError } from '@/errors';
import { trackUserEvent, MessageEnum } from '@/utils/trackUtil';
import { parseNumber } from '@/utils/numberUtil';
import { COLORS, PATHS } from '@/constants';
import { Icon } from '@/components';
import { PostType, UserDto } from '@/types';
import { trackUserEvent, MessageEnum } from '@/utils/trackUtil';
import { Icon } from '@/components';
import { Graph } from './Graph';

export const Section = (p: PostType) => {
const [open, setOpen] = useState(false);

const client = useQueryClient();

const { username } = client.getQueryData([PATHS.ME]) as UserDto;
const { NEXT_PUBLIC_VELOG_URL } = process.env;

if (!username) {
throw new UserNameNotFoundError();
}
if (NEXT_PUBLIC_VELOG_URL) {
throw new EnvNotFoundError('NEXT_PUBLIC_VELOG_URL');
}

const url = `${process.env.NEXT_PUBLIC_VELOG_URL}/@${username}/${p.slug}`;

return (
<section className="flex flex-col w-full h-fit relative">
Expand All @@ -31,9 +41,7 @@ export const Section = (p: PostType) => {
title="해당 글로 바로가기"
onClick={(e) => {
e.stopPropagation();
window.open(
`${process.env.NEXT_PUBLIC_VELOG_URL}/@${username}/${p.slug}`,
);
window.open(url);
}}
>
<Icon name="Shortcut" color="#ECECEC" size={20} />
Expand Down
23 changes: 23 additions & 0 deletions src/components/common/ChannelTalkProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
'use client';

import * as ChannelService from '@channel.io/channel-web-sdk-loader';
import { useEffect } from 'react';

const ChannelTalkServiceLoader = () => {
const CHANNELTALK_PLUGIN_KEY = process.env.NEXT_PUBLIC_CHANNELTALK_PLUGIN_KEY;
if (!CHANNELTALK_PLUGIN_KEY) {
throw new Error('CHANNELTALK_PLUGIN_KEY가 ENV에서 설정되지 않았습니다');
}

ChannelService.loadScript();
ChannelService.boot({ pluginKey: CHANNELTALK_PLUGIN_KEY });
};

export const ChannelTalkProvider = ({
children,
}: {
children: React.ReactNode;
}) => {
useEffect(() => ChannelTalkServiceLoader(), []);
return <>{children}</>;
};
Loading