refactor: org 라우팅 구조 수정#569
Conversation
There was a problem hiding this comment.
Code Review
This pull request refactors the application to migrate the organization ID from a query parameter to a path segment (/org/[orgId]/...), updating routes, hooks, and middleware accordingly. The review feedback highlights critical Next.js 15 compatibility issues where layout parameters are now passed as a Promise and must be awaited to prevent runtime errors. Additionally, a bug was identified in the basketball layout where the sport was hardcoded to 'SOCCER'. Finally, defensive programming improvements are suggested across several page components to validate the orgId and id parameters to prevent redirecting to invalid paths containing NaN.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| interface MainLayoutProps extends PropsWithChildren { | ||
| params: { | ||
| orgId: string; | ||
| }; | ||
| } | ||
|
|
||
| const MainLayout = ({ children, params: { orgId } }: MainLayoutProps) => { | ||
| return ( | ||
| <div className="relative flex min-h-dvh flex-col"> | ||
| <Header.Root left={<LeftSlot />} right={<OrgSwitcher />} /> | ||
| <Header.Root | ||
| left={<LeftSlot sport="SOCCER" orgId={Number(orgId)} />} | ||
| right={<OrgSwitcher />} | ||
| /> |
There was a problem hiding this comment.
Next.js 15에서는 layout의 params도 Promise로 전달되므로, 동기적으로 구조 분해 할당을 하면 런타임 에러가 발생할 수 있습니다. MainLayout을 async 함수로 변경하고 params를 await 하도록 수정해야 합니다. 또한, 농구(basketball) 레이아웃임에도 LeftSlot에 sport="SOCCER"가 하드코딩되어 있어 로고 클릭 시 축구 페이지로 이동하는 버그가 있습니다. 이를 sport={SPORT_TYPE}으로 수정해야 합니다.
| interface MainLayoutProps extends PropsWithChildren { | |
| params: { | |
| orgId: string; | |
| }; | |
| } | |
| const MainLayout = ({ children, params: { orgId } }: MainLayoutProps) => { | |
| return ( | |
| <div className="relative flex min-h-dvh flex-col"> | |
| <Header.Root left={<LeftSlot />} right={<OrgSwitcher />} /> | |
| <Header.Root | |
| left={<LeftSlot sport="SOCCER" orgId={Number(orgId)} />} | |
| right={<OrgSwitcher />} | |
| /> | |
| interface MainLayoutProps extends PropsWithChildren { | |
| params: Promise<{ | |
| orgId: string; | |
| }>; | |
| } | |
| const MainLayout = async ({ children, params }: MainLayoutProps) => { | |
| const { orgId } = await params; | |
| return ( | |
| <div className="relative flex min-h-dvh flex-col"> | |
| <Header.Root | |
| left={<LeftSlot sport={SPORT_TYPE} orgId={Number(orgId)} />} | |
| right={<OrgSwitcher />} | |
| /> |
| interface MainLayoutProps extends PropsWithChildren { | ||
| params: { | ||
| orgId: string; | ||
| }; | ||
| } | ||
|
|
||
| const MainLayout = ({ children, params: { orgId } }: MainLayoutProps) => { | ||
| return ( | ||
| <div className="relative flex min-h-dvh flex-col"> | ||
| <Header.Root left={<LeftSlot />} right={<OrgSwitcher />} /> | ||
| <Header.Root | ||
| left={<LeftSlot sport="SOCCER" orgId={Number(orgId)} />} | ||
| right={<OrgSwitcher />} | ||
| /> |
There was a problem hiding this comment.
Next.js 15에서는 layout의 params도 Promise로 전달되므로, 동기적으로 구조 분해 할당을 하면 런타임 에러가 발생할 수 있습니다. MainLayout을 async 함수로 변경하고 params를 await 하도록 수정해야 합니다. 일관성을 위해 sport prop도 SPORT_TYPE을 사용하도록 변경하는 것이 좋습니다.
| interface MainLayoutProps extends PropsWithChildren { | |
| params: { | |
| orgId: string; | |
| }; | |
| } | |
| const MainLayout = ({ children, params: { orgId } }: MainLayoutProps) => { | |
| return ( | |
| <div className="relative flex min-h-dvh flex-col"> | |
| <Header.Root left={<LeftSlot />} right={<OrgSwitcher />} /> | |
| <Header.Root | |
| left={<LeftSlot sport="SOCCER" orgId={Number(orgId)} />} | |
| right={<OrgSwitcher />} | |
| /> | |
| interface MainLayoutProps extends PropsWithChildren { | |
| params: Promise<{ | |
| orgId: string; | |
| }>; | |
| } | |
| const MainLayout = async ({ children, params }: MainLayoutProps) => { | |
| const { orgId } = await params; | |
| return ( | |
| <div className="relative flex min-h-dvh flex-col"> | |
| <Header.Root | |
| left={<LeftSlot sport={SPORT_TYPE} orgId={Number(orgId)} />} | |
| right={<OrgSwitcher />} | |
| /> |
| const { orgId: _orgId, id: _id } = await params; | ||
| const orgId = Number(_orgId); | ||
| const id = Number(_id); | ||
|
|
||
| if (Number.isNaN(id) || id <= 0) { | ||
| redirect(routes.home({ sport: SPORT_TYPE })); | ||
| redirect(routes.home({ orgId, sport: SPORT_TYPE })); | ||
| } |
There was a problem hiding this comment.
orgId가 유효한 숫자가 아닐 경우(예: NaN 또는 <= 0), routes.home으로 리다이렉트할 때 /org/NaN/... 경로로 이동하게 됩니다. 방어적 프로그래밍 관점에서 orgId에 대한 유효성 검증을 추가하여 잘못된 경로일 경우 routes.welcome으로 안전하게 리다이렉트하는 것이 좋습니다.
| const { orgId: _orgId, id: _id } = await params; | |
| const orgId = Number(_orgId); | |
| const id = Number(_id); | |
| if (Number.isNaN(id) || id <= 0) { | |
| redirect(routes.home({ sport: SPORT_TYPE })); | |
| redirect(routes.home({ orgId, sport: SPORT_TYPE })); | |
| } | |
| const { orgId: _orgId, id: _id } = await params; | |
| const orgId = Number(_orgId); | |
| const id = Number(_id); | |
| if (Number.isNaN(orgId) || orgId <= 0) { | |
| redirect(routes.welcome); | |
| } | |
| if (Number.isNaN(id) || id <= 0) { | |
| redirect(routes.home({ orgId, sport: SPORT_TYPE })); | |
| } |
| const { orgId: _orgId, id: _id } = await params; | ||
| const orgId = Number(_orgId); | ||
| const id = Number(_id); | ||
|
|
||
| if (Number.isNaN(id) || id <= 0) { | ||
| redirect(routes.home({ sport: SPORT_TYPE })); | ||
| redirect(routes.home({ orgId, sport: SPORT_TYPE })); | ||
| } |
There was a problem hiding this comment.
orgId가 유효한 숫자가 아닐 경우(예: NaN 또는 <= 0), routes.home으로 리다이렉트할 때 /org/NaN/... 경로로 이동하게 됩니다. 방어적 프로그래밍 관점에서 orgId에 대한 유효성 검증을 추가하여 잘못된 경로일 경우 routes.welcome으로 안전하게 리다이렉트하는 것이 좋습니다.
| const { orgId: _orgId, id: _id } = await params; | |
| const orgId = Number(_orgId); | |
| const id = Number(_id); | |
| if (Number.isNaN(id) || id <= 0) { | |
| redirect(routes.home({ sport: SPORT_TYPE })); | |
| redirect(routes.home({ orgId, sport: SPORT_TYPE })); | |
| } | |
| const { orgId: _orgId, id: _id } = await params; | |
| const orgId = Number(_orgId); | |
| const id = Number(_id); | |
| if (Number.isNaN(orgId) || orgId <= 0) { | |
| redirect(routes.welcome); | |
| } | |
| if (Number.isNaN(id) || id <= 0) { | |
| redirect(routes.home({ orgId, sport: SPORT_TYPE })); | |
| } |
| const { orgId: _orgId, id: _id } = await params; | ||
| const orgId = Number(_orgId); | ||
| const id = Number(_id); | ||
|
|
||
| if (!_id || Number.isNaN(id) || id <= 0) { | ||
| redirect(routes.home({ sport: SPORT_TYPE })); | ||
| redirect(routes.home({ orgId, sport: SPORT_TYPE })); | ||
| } |
There was a problem hiding this comment.
orgId가 유효한 숫자가 아닐 경우(예: NaN 또는 <= 0), routes.home으로 리다이렉트할 때 /org/NaN/... 경로로 이동하게 됩니다. 방어적 프로그래밍 관점에서 orgId에 대한 유효성 검증을 추가하여 잘못된 경로일 경우 routes.welcome으로 안전하게 리다이렉트하는 것이 좋습니다. 또한 기존의 !_id 조건은 Number.isNaN(id)에 의해 이미 커버되므로 중복 검사를 제거하여 단순화할 수 있습니다.
| const { orgId: _orgId, id: _id } = await params; | |
| const orgId = Number(_orgId); | |
| const id = Number(_id); | |
| if (!_id || Number.isNaN(id) || id <= 0) { | |
| redirect(routes.home({ sport: SPORT_TYPE })); | |
| redirect(routes.home({ orgId, sport: SPORT_TYPE })); | |
| } | |
| const { orgId: _orgId, id: _id } = await params; | |
| const orgId = Number(_orgId); | |
| const id = Number(_id); | |
| if (Number.isNaN(orgId) || orgId <= 0) { | |
| redirect(routes.welcome); | |
| } | |
| if (Number.isNaN(id) || id <= 0) { | |
| redirect(routes.home({ orgId, sport: SPORT_TYPE })); | |
| } |
| const { orgId: _orgId, id: _id } = await params; | ||
| const orgId = Number(_orgId); | ||
| const id = Number(_id); |
There was a problem hiding this comment.
다른 상세 페이지(games, leagues)와 달리 teams/[id] 페이지에서는 orgId와 id에 대한 유효성 검증이 누락되어 있습니다. 잘못된 id나 orgId가 들어올 경우 API 요청 실패나 잘못된 경로 렌더링이 발생할 수 있으므로, 일관성 있게 유효성 검증 및 리다이렉트 로직을 추가하는 것이 좋습니다.
const { orgId: _orgId, id: _id } = await params;
const orgId = Number(_orgId);
const id = Number(_id);
if (Number.isNaN(orgId) || orgId <= 0) {
redirect(routes.welcome);
}
if (Number.isNaN(id) || id <= 0) {
redirect(routes.home({ orgId, sport: SPORT_TYPE }));
}
| const { orgId: _orgId, id: _id } = await params; | ||
| const orgId = Number(_orgId); | ||
| const id = Number(_id); |
There was a problem hiding this comment.
다른 상세 페이지(games, leagues)와 달리 teams/[id] 페이지에서는 orgId와 id에 대한 유효성 검증이 누락되어 있습니다. 잘못된 id나 orgId가 들어올 경우 API 요청 실패나 잘못된 경로 렌더링이 발생할 수 있으므로, 일관성 있게 유효성 검증 및 리다이렉트 로직을 추가하는 것이 좋습니다.
const { orgId: _orgId, id: _id } = await params;
const orgId = Number(_orgId);
const id = Number(_id);
if (Number.isNaN(orgId) || orgId <= 0) {
redirect(routes.welcome);
}
if (Number.isNaN(id) || id <= 0) {
redirect(routes.home({ orgId, sport: SPORT_TYPE }));
}
| const Page = async ({ params }: Props) => { | ||
| const { orgId } = await params; | ||
| redirect(routes.home({ orgId: Number(orgId), sport: DEFAULT_SPORT })); | ||
| }; |
There was a problem hiding this comment.
orgId가 유효한 숫자가 아닐 경우(예: NaN 또는 <= 0), routes.home으로 리다이렉트할 때 /org/NaN/... 경로로 이동하게 됩니다. 방어적 프로그래밍 관점에서 orgId에 대한 유효성 검증을 추가하여 잘못된 경로일 경우 routes.welcome으로 안전하게 리다이렉트하는 것이 좋습니다.
| const Page = async ({ params }: Props) => { | |
| const { orgId } = await params; | |
| redirect(routes.home({ orgId: Number(orgId), sport: DEFAULT_SPORT })); | |
| }; | |
| const Page = async ({ params }: Props) => { | |
| const { orgId: _orgId } = await params; | |
| const orgId = Number(_orgId); | |
| if (Number.isNaN(orgId) || orgId <= 0) { | |
| redirect(routes.welcome); | |
| } | |
| redirect(routes.home({ orgId, sport: DEFAULT_SPORT })); | |
| }; |
| const Page = async ({ params }: Props) => { | ||
| const { orgId } = await params; | ||
| redirect(routes.home({ orgId: Number(orgId), sport: SPORT_TYPE })); | ||
| }; |
There was a problem hiding this comment.
orgId가 유효한 숫자가 아닐 경우(예: NaN 또는 <= 0), routes.home으로 리다이렉트할 때 /org/NaN/... 경로로 이동하게 됩니다. 방어적 프로그래밍 관점에서 orgId에 대한 유효성 검증을 추가하여 잘못된 경로일 경우 routes.welcome으로 안전하게 리다이렉트하는 것이 좋습니다.
| const Page = async ({ params }: Props) => { | |
| const { orgId } = await params; | |
| redirect(routes.home({ orgId: Number(orgId), sport: SPORT_TYPE })); | |
| }; | |
| const Page = async ({ params }: Props) => { | |
| const { orgId: _orgId } = await params; | |
| const orgId = Number(_orgId); | |
| if (Number.isNaN(orgId) || orgId <= 0) { | |
| redirect(routes.welcome); | |
| } | |
| redirect(routes.home({ orgId, sport: SPORT_TYPE })); | |
| }; |
| const Page = async ({ params }: Props) => { | ||
| const { orgId } = await params; | ||
| redirect(routes.home({ orgId: Number(orgId), sport: SPORT_TYPE })); | ||
| }; |
There was a problem hiding this comment.
orgId가 유효한 숫자가 아닐 경우(예: NaN 또는 <= 0), routes.home으로 리다이렉트할 때 /org/NaN/... 경로로 이동하게 됩니다. 방어적 프로그래밍 관점에서 orgId에 대한 유효성 검증을 추가하여 잘못된 경로일 경우 routes.welcome으로 안전하게 리다이렉트하는 것이 좋습니다.
| const Page = async ({ params }: Props) => { | |
| const { orgId } = await params; | |
| redirect(routes.home({ orgId: Number(orgId), sport: SPORT_TYPE })); | |
| }; | |
| const Page = async ({ params }: Props) => { | |
| const { orgId: _orgId } = await params; | |
| const orgId = Number(_orgId); | |
| if (Number.isNaN(orgId) || orgId <= 0) { | |
| redirect(routes.welcome); | |
| } | |
| redirect(routes.home({ orgId, sport: SPORT_TYPE })); | |
| }; |
sungwonnoh
left a comment
There was a problem hiding this comment.
라우팅 구조가 더 깔끔해진 거 같아요! 수고하셨습니다
✅ 작업 내용
org정보를 쿼리 파라미터로 관리하고 있었습니다.조직 -> 스포츠 종목 -> 경기 목록 -> 경기 Id처럼 조직 정보는 모든 스포츠, 게임들의 최상위 정보이므로, 이에 맞게 조직 정보를 세그먼트로 변경해야 한다고 판단했습니다.domain/soccer?org=9구조에서domain/org/[orgId]/soccer구조로 마이그레이션 했습니다.