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
Empty file removed src/App.css
Empty file.
22 changes: 0 additions & 22 deletions src/App.tsx

This file was deleted.

13 changes: 13 additions & 0 deletions src/assets/logo-full.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/assets/logo-full@4x.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions src/assets/logo-icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/assets/logo-icon@4x.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 9 additions & 0 deletions src/components/common/LoginButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Link } from 'react-router-dom';

export function LoginButton() {
return (
<Link to="/login" className="flex items-center gap-1 text-body-s-bold text-gray-800">
로그인
</Link>
);
}
15 changes: 15 additions & 0 deletions src/components/common/Logo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Link, useLocation } from 'react-router-dom';

import logoFull from '@/assets/logo-full@4x.webp';
import logoIcon from '@/assets/logo-icon@4x.webp';

export function Logo() {
const { pathname } = useLocation();
const isHome = pathname === '/';

return (
<Link to="/">
<img src={isHome ? logoFull : logoIcon} alt="또랑" className="h-8" />
</Link>
);
}
2 changes: 2 additions & 0 deletions src/components/common/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { LoginButton } from './LoginButton';
export { Logo } from './Logo';
18 changes: 10 additions & 8 deletions src/components/layout/Gnb.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,28 @@
import { Link } from 'react-router-dom';
import { Link, useLocation, useParams } from 'react-router-dom';

import clsx from 'clsx';

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

interface GnbProps {
activeTab?: Tab;
}
export function Gnb() {
const { projectId } = useParams<{ projectId: string }>();
const location = useLocation();
const activeTab = getTabFromPathname(location.pathname);

if (!projectId) return null;

export function Gnb({ activeTab = DEFAULT_TAB }: GnbProps) {
return (
<nav
className="flex h-15 items-center justify-center"
role="tablist"
aria-label="네비게이션 메뉴"
>
{TABS.map(({ key, label, path }) => {
{TABS.map(({ key, label }) => {
const isActive = activeTab === key;
return (
<Link
key={key}
to={path}
to={getTabPath(projectId, key)}
role="tab"
id={`tab-${key}`}
aria-selected={isActive}
Expand Down
17 changes: 0 additions & 17 deletions src/components/layout/Header.tsx

This file was deleted.

22 changes: 14 additions & 8 deletions src/components/layout/Layout.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,25 @@
import type { ReactNode } from 'react';
import { Outlet } from 'react-router-dom';

import { Header } from './Header';
import { Logo } from '@/components/common';

interface LayoutProps {
headerLeft?: ReactNode;
headerCenter?: ReactNode;
headerRight?: ReactNode;
children?: ReactNode;
left?: ReactNode;
center?: ReactNode;
right?: ReactNode;
}

export function Layout({ headerLeft, headerCenter, headerRight, children }: LayoutProps) {
export function Layout({ left, center, right }: LayoutProps) {
return (
<div className="min-h-screen bg-gray-100">
<Header left={headerLeft} center={headerCenter} right={headerRight} />
<main className="pt-15">{children}</main>
<header className="fixed top-0 right-0 left-0 z-50 flex h-15 items-center justify-between border-b border-gray-200 bg-white px-18">
<div className="flex items-center gap-6">{left ?? <Logo />}</div>
<div className="absolute left-1/2 -translate-x-1/2">{center}</div>
<div className="flex items-center gap-8">{right}</div>
</header>
<main className="pt-15">
<Outlet />
</main>
</div>
);
}
3 changes: 0 additions & 3 deletions src/components/layout/index.ts

This file was deleted.

55 changes: 48 additions & 7 deletions src/constants/navigation.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,55 @@
export const TABS = [
{ key: 'slide', label: '슬라이드', path: '/slide' },
{ key: 'video', label: '영상', path: '/video' },
{ key: 'insight', label: '인사이트', path: '/insight' },
{ key: 'slide', label: '슬라이드' },
{ key: 'video', label: '영상' },
{ key: 'insight', label: '인사이트' },
] as const;

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

export const DEFAULT_TAB: Tab = 'slide';
export const DEFAULT_SLIDE_ID = '1';

export const PATH_TO_TAB: Record<string, Tab> = {
'/': DEFAULT_TAB,
...Object.fromEntries(TABS.map((tab) => [tab.path, tab.key])),
const LAST_SLIDE_KEY_PREFIX = 'lastSlideId:';

/** 마지막으로 본 슬라이드 ID 저장 */
export const setLastSlideId = (projectId: string, slideId: string): void => {
try {
localStorage.setItem(`${LAST_SLIDE_KEY_PREFIX}${projectId}`, slideId);
} catch {
// localStorage 사용 불가 시 무시 (프라이빗 모드, 저장 공간 부족 등)
}
};

/** 마지막으로 본 슬라이드 ID 조회 */
export const getLastSlideId = (projectId: string): string => {
try {
return localStorage.getItem(`${LAST_SLIDE_KEY_PREFIX}${projectId}`) ?? DEFAULT_SLIDE_ID;
} catch {
return DEFAULT_SLIDE_ID;
}
};

/** 탭별 경로 생성 */
export const getTabPath = (projectId: string, tab: Tab, slideId?: string): string => {
switch (tab) {
case 'slide':
return `/${projectId}/slide/${slideId ?? getLastSlideId(projectId)}`;
case 'video':
return `/${projectId}/video`;
case 'insight':
return `/${projectId}/insight`;
}
};

/** pathname에서 탭 추출 (/:projectId/:tab/...) */
export const getTabFromPathname = (pathname: string): Tab => {
const segments = pathname.split('/').filter(Boolean);

// 프로젝트 경로 형태가 아닌 경우 (예: '/', '/settings' 등)
if (segments.length < 2) return 'slide';

const tabSegment = segments[1];

if (tabSegment === 'video') return 'video';
if (tabSegment === 'insight') return 'insight';
return 'slide';
};
35 changes: 26 additions & 9 deletions src/main.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,37 @@
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { RouterProvider, createBrowserRouter } from 'react-router-dom';
import { Navigate, RouterProvider, createBrowserRouter } from 'react-router-dom';

import App from './App';
import InsightPage from './pages/InsightPage';
import SlidePage from './pages/SlidePage';
import VideoPage from './pages/VideoPage';
import './styles/index.css';
import { LoginButton, Logo } from '@/components/common';
import { Gnb } from '@/components/layout/Gnb';
import { Layout } from '@/components/layout/Layout';
import { DEFAULT_SLIDE_ID } from '@/constants/navigation';
import { HomePage, InsightPage, SlidePage, VideoPage } from '@/pages';
import '@/styles/index.css';

const router = createBrowserRouter([
{
path: '/',
element: <App />,
element: <Layout right={<LoginButton />} />,
children: [{ index: true, element: <HomePage /> }],
},
{
path: '/:projectId',
element: (
<Layout
left={
<>
<Logo />
<span className="text-body-m-bold text-gray-800">내 발표</span>
</>
Comment on lines +23 to +26
Copy link

Copilot AI Dec 31, 2025

Choose a reason for hiding this comment

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

Fragment(<>...</>)를 사용하면 Layout 컴포넌트의 gap-6 스타일이 자식 요소들 사이에 적용되지 않습니다. Fragment는 DOM에 렌더링되지 않기 때문입니다. div로 감싸거나, 배열로 전달하되 각 요소에 key를 추가해주세요.

Suggested change
<>
<Logo />
<span className="text-body-m-bold text-gray-800">내 발표</span>
</>
<div>
<Logo />
<span className="text-body-m-bold text-gray-800">내 발표</span>
</div>

Copilot uses AI. Check for mistakes.
Copy link
Member Author

@AndyH0ng AndyH0ng Dec 31, 2025

Choose a reason for hiding this comment

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

Fragment는 DOM에 렌더링되지 않고 풀어지기 때문에, <Logo /><span>이 Layout의 gap-6이 적용된 div의 직접적인 자식이 됩니다. 따라서 gap이 정상 적용됩니다.

오히려 div로 감싸면 Logo와 span 사이에 gap이 적용되지 않게 됩니다.

}
center={<Gnb />}
right={<LoginButton />}
/>
),
children: [
{ index: true, element: <SlidePage /> }, // DEFAULT_TAB 변경 시 동기화 필요
{ path: 'slide', element: <SlidePage /> },
{ index: true, element: <Navigate to={`slide/${DEFAULT_SLIDE_ID}`} replace /> },
{ path: 'slide/:slideId', element: <SlidePage /> },
{ path: 'video', element: <VideoPage /> },
{ path: 'insight', element: <InsightPage /> },
],
Expand Down
7 changes: 7 additions & 0 deletions src/pages/HomePage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export default function HomePage() {
return (
<div className="p-8">
<h1 className="text-body-m-bold">홈</h1>
</div>
);
}
18 changes: 17 additions & 1 deletion src/pages/SlidePage.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,23 @@
import { useEffect } from 'react';
import { useParams } from 'react-router-dom';

import { setLastSlideId } from '@/constants/navigation';

export default function SlidePage() {
const { projectId, slideId } = useParams<{
projectId: string;
slideId: string;
}>();

useEffect(() => {
if (projectId && slideId) {
setLastSlideId(projectId, slideId);
}
}, [projectId, slideId]);

return (
<div role="tabpanel" id="tabpanel-slide" aria-labelledby="tab-slide" className="p-8">
<h1 className="text-body-m-bold">슬라이드</h1>
<h1 className="text-body-m-bold">슬라이드 {slideId}</h1>
</div>
);
}
4 changes: 4 additions & 0 deletions src/pages/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export { default as HomePage } from './HomePage';
export { default as InsightPage } from './InsightPage';
export { default as SlidePage } from './SlidePage';
export { default as VideoPage } from './VideoPage';
8 changes: 7 additions & 1 deletion tsconfig.app.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,13 @@
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
"noUncheckedSideEffectImports": true,

/* Paths */
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src"]
}
7 changes: 6 additions & 1 deletion vite.config.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import tailwindcss from '@tailwindcss/vite';
import react from '@vitejs/plugin-react-swc';

import path from 'path';
import { defineConfig } from 'vite';

// https://vite.dev/config/
export default defineConfig({
plugins: [react(), tailwindcss()],
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
});