-
Notifications
You must be signed in to change notification settings - Fork 0
fix/라우팅 경로 수정 #15
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fix/라우팅 경로 수정 #15
Changes from 12 commits
fdb757d
12d3e7a
a9682f3
d167b2c
23ed1ff
ef3be4a
a8e15d8
a842dc8
62e3b64
bfcf68f
9910aa7
bf0acb1
9d76887
e3d1d7a
c7eea4c
07ac244
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
| 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> | ||
| ); | ||
| } | ||
| 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> | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| export { LoginButton } from './LoginButton'; | ||
| export { Logo } from './Logo'; |
This file was deleted.
| 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> | ||
| ); | ||
| } |
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,14 +1,43 @@ | ||
| 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 => { | ||
| localStorage.setItem(`${LAST_SLIDE_KEY_PREFIX}${projectId}`, slideId); | ||
| }; | ||
|
|
||
| /** 마지막으로 본 슬라이드 ID 조회 */ | ||
| export const getLastSlideId = (projectId: string): string => { | ||
| return localStorage.getItem(`${LAST_SLIDE_KEY_PREFIX}${projectId}`) ?? DEFAULT_SLIDE_ID; | ||
AndyH0ng marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
AndyH0ng marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| }; | ||
|
|
||
| /** 탭별 경로 생성 */ | ||
| 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); | ||
| const tabSegment = segments[1]; | ||
|
|
||
| if (tabSegment === 'video') return 'video'; | ||
| if (tabSegment === 'insight') return 'insight'; | ||
AndyH0ng marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| return 'slide'; | ||
| }; | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,20 +1,36 @@ | ||||||||||||||||||
| 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 { 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
|
||||||||||||||||||
| <> | |
| <Logo /> | |
| <span className="text-body-m-bold text-gray-800">내 발표</span> | |
| </> | |
| <div> | |
| <Logo /> | |
| <span className="text-body-m-bold text-gray-800">내 발표</span> | |
| </div> |
There was a problem hiding this comment.
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이 적용되지 않게 됩니다.
AndyH0ng marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
AndyH0ng marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
Outdated
Copilot
AI
Dec 31, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
슬라이드 경로의 slideId 파라미터가 optional(?)로 정의되어 있습니다. 그러나 SlidePage.tsx에서는 기본값으로 DEFAULT_SLIDE_ID를 사용하고, router 설정에서도 명시적으로 'slide/1'로 리다이렉트하고 있습니다. slideId를 필수 파라미터로 만들어 타입 안정성을 높이는 것이 좋습니다.
| { path: 'slide/:slideId?', element: <SlidePage /> }, | |
| { path: 'slide/:slideId', element: <SlidePage /> }, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
slideId를 optional로 둔 이유는 /:projectId 접근 시 /:projectId/slide/1로 리다이렉트되기 때문입니다. 실제로 SlidePage가 렌더링될 때는 항상 slideId가 존재합니다.
다만 타입 안정성을 위해 필수로 변경하는 것도 좋은 방법이라 생각하여 반영하겠습니다.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
반영했습니다 ✅
| 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> | ||
| ); | ||
| } |
| 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 { DEFAULT_SLIDE_ID, setLastSlideId } from '@/constants/navigation'; | ||
|
|
||
| export default function SlidePage() { | ||
| const { projectId = '', slideId = DEFAULT_SLIDE_ID } = 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> | ||
| ); | ||
| } |
| 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'; |
| 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'), | ||
| }, | ||
| }, | ||
| }); |
Uh oh!
There was an error while loading. Please reload this page.