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
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"@next/third-parties": "^15.1.7",
"@sentry/core": "^8.47.0",
"@sentry/nextjs": "^8.47.0",
"@tailwindcss/typography": "^0.5.16",
"@tanstack/react-query": "^5.61.3",
"@tanstack/react-query-devtools": "^5.62.11",
"chart.js": "^4.4.7",
Expand All @@ -31,7 +32,8 @@
"react-intersection-observer": "^9.14.0",
"react-toastify": "^10.0.6",
"return-fetch": "^0.4.6",
"sharp": "^0.33.5"
"sharp": "^0.33.5",
"zustand": "^5.0.3"
},
"devDependencies": {
"@eslint/js": "^9.15.0",
Expand Down
56 changes: 56 additions & 0 deletions pnpm-lock.yaml

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

1 change: 1 addition & 0 deletions src/apis/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './dashboard.request';
export * from './instance.request';
export * from './notice.request';
export * from './user.request';
6 changes: 6 additions & 0 deletions src/apis/notice.request.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { PATHS } from '@/constants';
import { NotiListDto } from '@/types';
import { instance } from './instance.request';

export const notiList = async () =>
await instance<null, NotiListDto>(PATHS.NOTIS);
11 changes: 10 additions & 1 deletion src/app/(auth-required)/components/header/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ import { PATHS, SCREENS } from '@/constants';
import { NameType } from '@/components';
import { useResponsive } from '@/hooks';
import { logout, me } from '@/apis';
import { useModal } from '@/hooks/useModal';
import { defaultStyle, Section, textStyle } from './Section';
import { Modal } from '../notice/Modal';

const PARAMS = {
MAIN: '?asc=false&sort=',
Expand All @@ -28,6 +30,7 @@ const layouts: Array<{ icon: NameType; title: string; path: string }> = [

export const Header = () => {
const [open, setOpen] = useState(false);
const { open: ModalOpen } = useModal();
const menu = useRef<HTMLDivElement | null>(null);
const path = usePathname();
const router = useRouter();
Expand Down Expand Up @@ -116,13 +119,19 @@ export const Header = () => {
{open && (
<div className="flex flex-col items-center max-MBI:items-end absolute self-center top-[50px] max-MBI:right-[6px]">
<div className="w-0 h-0 border-[15px] ml-3 mr-3 border-TRANSPARENT border-b-BG-SUB" />
<div className="cursor-pointer h-fit flex-col rounded-[4px] bg-BG-SUB hover:bg-BG-ALT shadow-BORDER-MAIN shadow-md">
<div className="cursor-pointer h-fit flex-col rounded-[4px] bg-BG-SUB shadow-BORDER-MAIN shadow-md">
<button
className="text-DESTRUCTIVE-SUB text-I3 p-5 max-MBI:p-4 flex whitespace-nowrap w-auto"
onClick={() => out()}
>
로그아웃
</button>
<button
className="text-TEXT-MAIN text-I3 p-5 max-MBI:p-4 flex whitespace-nowrap w-auto hover:bg-BG-ALT"
onClick={() => ModalOpen(<Modal />)}
>
공지사항
</button>
</div>
</div>
)}
Expand Down
46 changes: 46 additions & 0 deletions src/app/(auth-required)/components/notice/Modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
'use client';

import { useEffect } from 'react';
import { useQuery } from '@tanstack/react-query';
import { notiList } from '@/apis';
import { PATHS } from '@/constants';
import { useModal } from '@/hooks/useModal';
import { Icon } from '@/components';

export const Modal = () => {
const { close } = useModal();
const { data } = useQuery({ queryKey: [PATHS.NOTIS], queryFn: notiList });

useEffect(() => {
const handleClose = (e: KeyboardEvent) => e.key === 'Escape' && close();

window.addEventListener('keydown', handleClose);
return () => window.removeEventListener('keydown', handleClose);
}, [close]);

return (
<div className="w-[800px] h-[500px] max-MBI:w-[450px] max-MBI:h-[200px] overflow-auto flex flex-col gap-3 p-10 max-MBI:p-7 rounded-md bg-BG-SUB">
<div className="flex items-center justify-between">
<h2 className="text-TEXT-MAIN items-cenetr gap-3 text-T3 max-MBI:text-T4">
공지사항
</h2>
<Icon name="Close" onClick={close} className="cursor-pointer" />
</div>

{data?.posts?.map(({ content, created_at, id, title }) => (
<div key={id} className="flex flex-col gap-2">
<div className="flex items-center gap-3">
<h3 className="text-TEXT-MAIN text-T4 max-MBI:text-T5">{title}</h3>
<h4 className="text-TEXT-ALT text-T5 max-MBI:text-ST5">
{created_at.split('T')[0]}
</h4>
</div>
<div
className="text-TEXT-MAIN text-I4 prose"
dangerouslySetInnerHTML={{ __html: content }}
/>
</div>
))}
</div>
);
};
66 changes: 66 additions & 0 deletions src/app/(auth-required)/components/notice/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
'use client';

import { useEffect, useState } from 'react';
import { useQuery } from '@tanstack/react-query';
import { notiList } from '@/apis';
import { PATHS } from '@/constants';
import { useModal } from '@/hooks/useModal';
import { Modal } from './Modal';

const DAY_IN_MS = 1000 * 60 * 60 * 24;
const TTL = DAY_IN_MS * 2;
const RECENT_POST_THRESHOLD_DAYS = 4;
const NOTIFICATION_STORAGE_KEY = 'noti_expiry';

export const Notice = () => {
const { data } = useQuery({ queryKey: [PATHS.NOTIS], queryFn: notiList });
const [show, setShow] = useState(false);
const { open } = useModal();

useEffect(() => {
try {
const lastUpdated = new Date(
data?.posts[0].created_at?.split('T')[0] as string,
).getTime();

const daysSinceUpdate = Math.ceil(
(new Date().getTime() - lastUpdated) / DAY_IN_MS,
);

if (daysSinceUpdate <= RECENT_POST_THRESHOLD_DAYS) {
const expiry = localStorage.getItem(NOTIFICATION_STORAGE_KEY);
if (!expiry || parseInt(expiry, 10) < new Date().getTime()) {
setShow(true);
}
}
} catch (error) {
console.error('알림 날짜 처리 중 오류 발생:', error);
}
}, [data]);

return (
<>
<div
className={`transition-all shrink-0 duration-300 flex items-center justify-center gap-2 w-full overflow-hidden bg-BORDER-SUB ${show ? 'h-[50px]' : 'h-[0px]'}`}
>
<h1 className="text-TEXT-MAIN text-ST4 max-MBI:text-ST5">
📣 새로운 업데이트를 확인해보세요!
</h1>
<button
className="text-PRIMARY-MAIN hover:text-PRIMARY-SUB text-ST4 transition-all duration-300 max-MBI:text-ST5"
onClick={() => {
setShow(false);
localStorage.setItem(
'noti_expiry',
JSON.stringify(new Date().getTime() + TTL),
);

open(<Modal />);
}}
>
확인하기
</button>
</div>
</>
);
};
23 changes: 15 additions & 8 deletions src/app/(auth-required)/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import { dehydrate, HydrationBoundary } from '@tanstack/react-query';
import { ReactElement } from 'react';
import { getQueryClient } from '@/utils/queryUtil';
import { PATHS } from '@/constants';
import { me } from '@/apis';
import { me, notiList } from '@/apis';
import { Notice } from './components/notice';
import { Header } from './components/header';

interface IProp {
Expand All @@ -14,14 +15,20 @@ export default async function Layout({ children }: IProp) {

await client.prefetchQuery({ queryKey: [PATHS.ME], queryFn: me });

await client.prefetchQuery({
queryKey: [PATHS.NOTIS],
queryFn: notiList,
});

return (
<main className="items-center w-full h-full flex flex-col p-[50px_70px_70px_70px] transition-all max-TBL:p-[20px_30px_30px_30px] max-MBI:p-[10px_25px_25px_25px]">
<div className="w-full max-w-[1740px] h-full overflow-hidden flex flex-col gap-[30px] max-TBL:gap-[20px]">
<HydrationBoundary state={dehydrate(client)}>
<HydrationBoundary state={dehydrate(client)}>
<main className="items-center w-full h-full flex flex-col">
<Notice />
<div className="w-full max-w-[1740px] h-full overflow-hidden flex flex-col gap-[30px] p-[50px_70px_70px_70px] transition-all duration-300 max-TBL:gap-[20px] max-TBL:p-[20px_30px_30px_30px] max-MBI:p-[10px_25px_25px_25px]">
<Header />
</HydrationBoundary>
{children}
</div>
</main>
{children}
</div>
</main>
</HydrationBoundary>
);
}
9 changes: 7 additions & 2 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,13 @@ import 'react-toastify/dist/ReactToastify.css';
import { ErrorBoundary } from '@sentry/nextjs';
import { ReactNode, Suspense } from 'react';
import type { Metadata } from 'next';
import { ChannelTalkProvider, QueryProvider } from '@/components';
import { env } from '@/constants';
import './globals.css';
import {
ChannelTalkProvider,
QueryProvider,
ModalProvider,
} from '@/components';
import { env } from '@/constants';

export const BASE = 'https://velog-dashboard.kro.kr/';

Expand Down Expand Up @@ -39,6 +43,7 @@ export default function RootLayout({
<QueryProvider>
<ChannelTalkProvider>
<ToastContainer autoClose={2000} />
<ModalProvider />
<Suspense>{children}</Suspense>
</ChannelTalkProvider>
</QueryProvider>
Expand Down
3 changes: 3 additions & 0 deletions src/components/Icon/icons/Close.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions src/components/Icon/icons/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ export { default as Compare } from './Compare.svg';
export { default as LeaderBoards } from './Leaderboards.svg';
export { default as Like } from './Like.svg';
export { default as Shortcut } from './Shortcut.svg';
export { default as Close } from './Close.svg';
Loading