Skip to content

Commit

Permalink
[25.02.16 / TASK-118] Feature: 채널톡 추가 및 헤더 로고 클릭 동작 추가 (#13)
Browse files Browse the repository at this point in the history
* hotfix: 일부 오류 수정
username 관련 오류 및 SSR 렌더린 관련 오류로 추정되는 오류 수정

* feat: 채널톡 추가 및 헤더 로고 동작 추가

* refactor: console.log 제거

* refactor: 메세지 내용 변경

* refactor: 코멘트 반영 겸 자잘한 오류 해결

* refactor: username 관련 코드 수정

* refactor: ENV 오류 전용 공통 오류 객체 추가
  • Loading branch information
six-standard authored Feb 16, 2025
1 parent 6fc9c58 commit ecb2d5a
Show file tree
Hide file tree
Showing 20 changed files with 111 additions and 65 deletions.
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
File renamed without changes.
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

0 comments on commit ecb2d5a

Please sign in to comment.