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
17 changes: 16 additions & 1 deletion src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,22 @@
import { Outlet, useLocation } from 'react-router-dom';

import './App.css';
import { Gnb, Layout } from './components/layout';
import { DEFAULT_TAB, PATH_TO_TAB } from './constants/navigation';

function App() {
return <></>;
const location = useLocation();
const activeTab = PATH_TO_TAB[location.pathname] ?? DEFAULT_TAB;

return (
<Layout
headerLeft={<div>로고</div>}
headerCenter={<Gnb activeTab={activeTab} />}
headerRight={<div>로그인</div>}
Comment on lines +13 to +15
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

로고와 로그인 UI가 div 태그로 직접 작성되어 있습니다. 이를 각각의 컴포넌트(예: <Logo />, <UserAuth />)로 분리하는 것을 고려해보세요. 이렇게 하면 App 컴포넌트가 더 선언적으로 되고, 각 UI 요소의 재사용성과 관리 용이성이 향상됩니다.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

로고와 공유, 로그인 버튼은 추후 추가될 예정이므로 현행을 유지합니다.

>
<Outlet />
</Layout>
);
}

export default App;
22 changes: 11 additions & 11 deletions src/components/layout/GNB.tsx → src/components/layout/Gnb.tsx
Original file line number Diff line number Diff line change
@@ -1,40 +1,40 @@
import { Link } from 'react-router-dom';

import clsx from 'clsx';

import { TABS, type Tab } from '../../constants/navigation';
import { DEFAULT_TAB, TABS, type Tab } from '../../constants/navigation';

interface GNBProps {
interface GnbProps {
activeTab?: Tab;
onTabChange?: (tab: Tab) => void;
}

export function GNB({ activeTab = 'slide', onTabChange }: GNBProps) {
export function Gnb({ activeTab = DEFAULT_TAB }: GnbProps) {
return (
<nav
className="absolute left-1/2 flex h-15 -translate-x-1/2 items-center justify-center"
className="flex h-15 items-center justify-center"
role="tablist"
aria-label="네비게이션 메뉴"
>
{TABS.map(({ key, label }) => {
{TABS.map(({ key, label, path }) => {
const isActive = activeTab === key;
return (
<button
<Link
key={key}
type="button"
to={path}
role="tab"
id={`tab-${key}`}
aria-selected={isActive}
aria-controls={`tabpanel-${key}`}
onClick={() => onTabChange?.(key)}
className={clsx(
`flex h-full w-25 items-end justify-center px-2.5 pb-4 pt-4 text-body-m-bold border-b-2`,
'flex h-full w-25 items-end justify-center px-2.5 pb-4 pt-4 text-body-m-bold border-b-2',
{
'border-main text-main': isActive,
'border-transparent text-gray-600': !isActive,
},
)}
>
{label}
</button>
</Link>
);
})}
</nav>
Expand Down
12 changes: 8 additions & 4 deletions src/components/layout/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import type { ReactNode } from 'react';

interface HeaderProps {
children?: ReactNode;
left?: ReactNode;
center?: ReactNode;
right?: ReactNode;
}

export function Header({ children }: HeaderProps) {
export function Header({ left, center, right }: HeaderProps) {
return (
<header className="fixed top-0 left-0 right-0 z-50 flex h-15 items-center justify-center border-b border-gray-200 bg-white px-18">
{children}
<header className="fixed top-0 left-0 right-0 z-50 flex h-15 items-center justify-between border-b border-gray-200 bg-white px-18">
<div className="flex items-center">{left}</div>
<div className="absolute left-1/2 -translate-x-1/2">{center}</div>
<div className="flex items-center">{right}</div>
</header>
);
}
8 changes: 5 additions & 3 deletions src/components/layout/Layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,16 @@ import type { ReactNode } from 'react';
import { Header } from './Header';

interface LayoutProps {
header: ReactNode;
headerLeft?: ReactNode;
headerCenter?: ReactNode;
headerRight?: ReactNode;
children?: ReactNode;
}

export function Layout({ children, header }: LayoutProps) {
export function Layout({ headerLeft, headerCenter, headerRight, children }: LayoutProps) {
return (
<div className="min-h-screen bg-gray-100">
<Header>{header}</Header>
<Header left={headerLeft} center={headerCenter} right={headerRight} />
<main className="pt-15">{children}</main>
</div>
);
Expand Down
2 changes: 1 addition & 1 deletion src/components/layout/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export { GNB } from './GNB';
export { Gnb } from './Gnb';
export { Header } from './Header';
export { Layout } from './Layout';
13 changes: 10 additions & 3 deletions src/constants/navigation.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
export const TABS = [
{ key: 'slide', label: '슬라이드' },
{ key: 'video', label: '영상' },
{ key: 'insight', label: '인사이트' },
{ key: 'slide', label: '슬라이드', path: '/slide' },
{ key: 'video', label: '영상', path: '/video' },
{ key: 'insight', label: '인사이트', path: '/insight' },
] as const;

export type Tab = (typeof TABS)[number]['key'];

export const DEFAULT_TAB: Tab = 'slide';

export const PATH_TO_TAB: Record<string, Tab> = {
'/': DEFAULT_TAB,
...Object.fromEntries(TABS.map((tab) => [tab.path, tab.key])),
};
21 changes: 19 additions & 2 deletions src/main.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,28 @@
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { RouterProvider, createBrowserRouter } from 'react-router-dom';

import App from './App.tsx';
import App from './App';
import InsightPage from './pages/InsightPage';
import SlidePage from './pages/SlidePage';
import VideoPage from './pages/VideoPage';
import './styles/index.css';

const router = createBrowserRouter([
{
path: '/',
element: <App />,
children: [
{ index: true, element: <SlidePage /> }, // DEFAULT_TAB 변경 시 동기화 필요
{ path: 'slide', element: <SlidePage /> },
{ path: 'video', element: <VideoPage /> },
{ path: 'insight', element: <InsightPage /> },
],
},
]);
Comment on lines +11 to +22
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

라우팅 설정이 src/constants/navigation.ts에 정의된 TABS 배열과 동기화되지 않고 하드코딩되어 있습니다. 이로 인해 TABS가 변경될 때마다 라우트 설정을 수동으로 업데이트해야 하며, 이는 유지보수성을 저해하고 잠재적인 버그를 유발할 수 있습니다. (// DEFAULT_TAB 변경 시 동기화 필요 주석 참고)

TABSDEFAULT_TAB 상수를 사용하여 라우트를 동적으로 생성하면 이 문제를 해결하고 코드를 더 견고하게 만들 수 있습니다.

아래 제안을 적용하기 전에 파일 상단에 다음 import 구문을 추가하거나 수정해주세요.

import { StrictMode, type ReactElement } from 'react';
import { TABS, DEFAULT_TAB, type Tab } from './constants/navigation';
Suggested change
const router = createBrowserRouter([
{
path: '/',
element: <App />,
children: [
{ index: true, element: <SlidePage /> }, // DEFAULT_TAB 변경 시 동기화 필요
{ path: 'slide', element: <SlidePage /> },
{ path: 'video', element: <VideoPage /> },
{ path: 'insight', element: <InsightPage /> },
],
},
]);
const pageComponentMap: Record<Tab, ReactElement> = {
slide: <SlidePage />,
video: <VideoPage />,
insight: <InsightPage />,
};
const router = createBrowserRouter([
{
path: '/',
element: <App />,
children: [
{ index: true, element: pageComponentMap[DEFAULT_TAB] },
...TABS.map((tab) => ({
path: tab.path.substring(1),
element: pageComponentMap[tab.key],
})),
],
},
]);

Copy link
Member Author

@AndyH0ng AndyH0ng Dec 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

동적으로 만들어봤자 TABSpageComponentMap을 건드려야 합니다.
TABSchildren을 수정해야 하는 현행과 크게 다를 바 없으며,
라우팅이 명시적으로 보이지 않아서 가독성도 떨어집니다.

이에 현행으로 유지합니다.


createRoot(document.querySelector('#root')!).render(
<StrictMode>
<App />
<RouterProvider router={router} />
</StrictMode>,
);
7 changes: 7 additions & 0 deletions src/pages/InsightPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export default function InsightPage() {
return (
<div role="tabpanel" id="tabpanel-insight" aria-labelledby="tab-insight" className="p-8">
<h1 className="text-body-m-bold">인사이트</h1>
</div>
);
}
7 changes: 7 additions & 0 deletions src/pages/SlidePage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export default function SlidePage() {
return (
<div role="tabpanel" id="tabpanel-slide" aria-labelledby="tab-slide" className="p-8">
<h1 className="text-body-m-bold">슬라이드</h1>
</div>
);
}
7 changes: 7 additions & 0 deletions src/pages/VideoPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export default function VideoPage() {
return (
<div role="tabpanel" id="tabpanel-video" aria-labelledby="tab-video" className="p-8">
<h1 className="text-body-m-bold">영상</h1>
</div>
);
}
4 changes: 4 additions & 0 deletions src/styles/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,8 @@
--font-size-caption: var(--font-size-caption);
--font-size-body-s: var(--font-size-body-s);
--font-size-body-m: var(--font-size-body-m);

--spacing-15: 3.75rem;
--spacing-18: 4.5rem;
--spacing-25: 6.25rem;
}
6 changes: 3 additions & 3 deletions src/styles/theme.css
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@
--line-height: 1.5;

/* 폰트 크기 */
--font-size-caption: 12px;
--font-size-body-s: 14px;
--font-size-body-m: 16px;
--font-size-caption: 0.75rem;
--font-size-body-s: 0.875rem;
--font-size-body-m: 1rem;

/* 폰트 굵기 */
--font-weight-regular: 400;
Expand Down
Loading