diff --git a/.env.sample b/.env.sample index 2fbd861..58e8216 100644 --- a/.env.sample +++ b/.env.sample @@ -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'> \ No newline at end of file diff --git a/package.json b/package.json index 1511097..2cab35a 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d0df869..1d23805 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: dependencies: + '@channel.io/channel-web-sdk-loader': + specifier: ^2.0.0 + version: 2.0.0 '@sentry/nextjs': specifier: ^8.47.0 version: 8.47.0(@opentelemetry/core@1.30.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.56.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.0(@opentelemetry/api@1.9.0))(next@14.2.18(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)(webpack@5.97.1) @@ -794,6 +797,9 @@ packages: '@bundled-es-modules/tough-cookie@0.1.6': resolution: {integrity: sha512-dvMHbL464C0zI+Yqxbz6kZ5TOEp7GLW+pry/RWndAR8MJQAXZ2rPmIs8tziTZjeIyhSNZgZbCePtfSbdWqStJw==} + '@channel.io/channel-web-sdk-loader@2.0.0': + resolution: {integrity: sha512-Z8DDpf2lAaYr/3aAnwQtxg0L8MYWgi/hhGV8c5/SLCV6Fx5Gssj7mfyHHrCVC315B0icmLYqZsXBlmbf6cN8Jg==} + '@cspotcode/source-map-support@0.8.1': resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} engines: {node: '>=12'} @@ -5524,6 +5530,8 @@ snapshots: '@types/tough-cookie': 4.0.5 tough-cookie: 4.1.4 + '@channel.io/channel-web-sdk-loader@2.0.0': {} + '@cspotcode/source-map-support@0.8.1': dependencies: '@jridgewell/trace-mapping': 0.3.9 diff --git a/src/apis/dashboard.request.ts b/src/apis/dashboard.request.ts index 3137056..4ca2615 100644 --- a/src/apis/dashboard.request.ts +++ b/src/apis/dashboard.request.ts @@ -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, - sort: SortType, - cursor?: string, -) => +export const postList = async (sort: SortType, cursor?: string) => await instance( 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) => - await instance(PATHS.SUMMARY, props); +export const postSummary = async () => + await instance(PATHS.SUMMARY); export const postDetail = async (path: string, start: string, end: string) => await instance( diff --git a/src/apis/instance.request.ts b/src/apis/instance.request.ts index 4addb91..89ee58a 100644 --- a/src/apis/instance.request.ts +++ b/src/apis/instance.request.ts @@ -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 = { @@ -60,9 +60,20 @@ export const instance = async ( init?: InitType, error?: Record, ): Promise => { + 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) diff --git a/src/apis/user.request.ts b/src/apis/user.request.ts index 5713d94..459bdbe 100644 --- a/src/apis/user.request.ts +++ b/src/apis/user.request.ts @@ -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( @@ -15,8 +15,7 @@ export const login = async (body: LoginVo) => }, ); -export const me = async (props: InitType) => - await instance(PATHS.ME, props); +export const me = async () => await instance(PATHS.ME); export const logout = async () => await instance(PATHS.LOGOUT, { method: 'POST', body: undefined }); diff --git a/src/app/(with-tracker)/(auth-required)/layout.tsx b/src/app/(with-tracker)/(auth-required)/layout.tsx index 1067058..f819dca 100644 --- a/src/app/(with-tracker)/(auth-required)/layout.tsx +++ b/src/app/(with-tracker)/(auth-required)/layout.tsx @@ -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'; @@ -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 ( diff --git a/src/app/(with-tracker)/(auth-required)/main/Content.tsx b/src/app/(with-tracker)/(auth-required)/main/Content.tsx index 27acfc8..867214e 100644 --- a/src/app/(with-tracker)/(auth-required)/main/Content.tsx +++ b/src/app/(with-tracker)/(auth-required)/main/Content.tsx @@ -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, ), @@ -41,7 +40,7 @@ export const Content = () => { const { data: summaries } = useQuery({ queryKey: [PATHS.SUMMARY], - queryFn: async () => await postSummary({}), + queryFn: postSummary, }); useEffect(() => { diff --git a/src/app/(with-tracker)/(auth-required)/main/page.tsx b/src/app/(with-tracker)/(auth-required)/main/page.tsx index ab424b2..ae508a7 100644 --- a/src/app/(with-tracker)/(auth-required)/main/page.tsx +++ b/src/app/(with-tracker)/(auth-required)/main/page.tsx @@ -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'; @@ -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 ( diff --git a/src/app/layout.tsx b/src/app/layout.tsx index e748e00..0c9c449 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -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', @@ -23,8 +23,10 @@ export default function RootLayout({ - - {children} + + + {children} + diff --git a/src/components/auth-required/header/Section.tsx b/src/components/auth-required/header/Section.tsx index 9a46851..b61ad7f 100644 --- a/src/components/auth-required/header/Section.tsx +++ b/src/components/auth-required/header/Section.tsx @@ -24,13 +24,13 @@ type PropType = T extends 'link' ? Partial & { clickType: 'function'; action: () => void; - children: React.ReactNode | React.ReactNode[]; + children: React.ReactNode; } : T extends 'none' ? Partial & { clickType: 'none'; action?: undefined; - children: React.ReactNode | React.ReactNode[]; + children: React.ReactNode; } : never; diff --git a/src/components/auth-required/header/index.tsx b/src/components/auth-required/header/index.tsx index 3e63fa9..9219fcc 100644 --- a/src/components/auth-required/header/index.tsx +++ b/src/components/auth-required/header/index.tsx @@ -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(() => { @@ -62,7 +60,10 @@ export const Header = () => { return (