라우팅
이 페이지는 프론트엔드 라우팅 표면에 대한 reference 문서입니다. Lumie는 Next.js App Router만 사용합니다. 라우트 그룹이 주요 사용자 영역을 조직하고, 레이아웃이 접근을 강제하며, app/api/[...path]/route.ts는 브 라우저 측 모든 API 호출이 지나는 same-origin 프록시입니다.
소스 경로
| 경로 | 역할 |
|---|---|
lumie-frontend/app/(marketing)/ | /, /about, /pricing, /features, /blog 같은 공개 페이지 |
lumie-frontend/app/(auth)/ | 로그인과 회원가입 진입점 |
lumie-frontend/app/(onboarding)/layout.tsx | owner 온보딩 가드와 리다이렉트 |
lumie-frontend/app/admin/layout.tsx | 직원 전용 가드와 로그인 후 복귀 경로 처리 |
lumie-frontend/app/dashboard/layout.tsx | 학생 대시보드 셸 |
lumie-frontend/app/auth/callback/page.tsx | 로그인 후 쿼리 refresh와 최종 리다이렉트 |
lumie-frontend/app/auth/refresh/page.tsx | 무중단 refresh 경계 |
lumie-frontend/app/[customId]/page.tsx | 공개 academy 랜딩 페이지 |
lumie-frontend/app/api/[...path]/route.ts | NEXT_PUBLIC_API_BASE로 향하는 same-origin 프록시 |
lumie-frontend/src/entities/session/api/getServerUser.ts | 보호 레이아웃이 사용하는 서버 측 /v1/me 읽기 |
lumie-frontend/src/entities/tenant/lib/useTenantCustomIdFromPath.ts | pathname에서 클라이언트 측 tenant custom ID 파싱 |
lumie-frontend/src/shared/api/serverFetch.ts | /api를 다시 거치는 SSR fetch 경계 |
라우트 그룹과 주요 경로
| 경로 형태 | 목적 | 비고 |
|---|---|---|
/ 및 /about, /pricing, /features, /blog 같은 마케팅 경로 | 공개 사이트 | 공유 헤더와 푸터를 가진 app/(marketing) 아래 구현 |
/login, /signup | 인증 진입점 | app/(auth) 아래 구현 |
/onboarding | owner 온보딩 플로우 | app/(onboarding)/layout.tsx에서 보호 |
/admin/... | 직원 및 academy 운영 | app/admin/layout.tsx에서 보호 |
/dashboard/... | 학생 경험 | 자체 클라이언트 레이아웃과 사이드바 셸 사용 |
/auth/callback | 로그인 후 리다이렉트 결정 | me 쿼리를 refresh하고 기본 인증 경로로 이동 |
/auth/refresh | 무중단 refresh 브리지 | refresh 쿠키는 있지만 access token을 다시 세워야 할 때 사용 |
/:customId | 공개 academy 랜딩 페이지 | app/[customId]/page.tsx로 구현 |
/api/[...path] | same-origin 프록시 라우트 | 브라우저 요청을 백엔드 게이트웨이로 전달 |
app/page.tsx는 없습니다. 사이트 루트는 app/(marketing)/page.tsx에서 옵니다.
/api/health는 app/api/health/route.ts에 별도로 있는 로컬 라우트입니다. 프론트엔드 liveness 용도로만 사용해야 하며, 백엔드 프록시는 검사하지 않습니다.
접근 제어
라우트 보호는 클라이언트 컴포넌트 곳곳에 흩어두지 않고 레이아웃에서 처리합니다.
온보딩
app/(onboarding)/layout.tsx는 다음을 수행합니다.
getServerUser()로 현재 사용자를 조회- 익명 사용자를 로그인 또는 세션 refresh로 리다이렉트
- 학생을
/dashboard로 리다이렉트 - 이미 온보딩을 마친 owner를
/admin으로 리다이렉트
관리자
app/admin/layout.tsx는 다음을 수행합니다.
- 서버에서 현재 사용자를 확인
- 로그인 후 복귀를 위해 요청된 관리자 경로를 보존
- 학생을
/dashboard로 리다이렉트 - 아직 온보딩하지 않은 owner를
/onboarding으로 리다이렉트
대시보드
app/dashboard/layout.tsx는 대시보드 셸을 제공하는 클라이언트 레이아웃입니다. 자체적으로 서버 리다이렉트를 하지는 않으며, 세션 플로우가 이미 수립되었다고 가정합니다.
테넌트 인지 라우팅
현재 두 가지 테넌트 인지 패턴이 있습니다.
/:customId는 academy의 공개 랜딩 페이지와 로그인 진입점을 렌더링합니다/api프록시는 커스텀 도메인 해석이 활성화되면 요청 host에서 tenant slug를 해석합니다
클라이언트 코드는 useTenantCustomIdFromPath()로 현재 tenant custom ID를 pathname에서 가져올 수도 있습니다. 이는 academy 브랜딩 랜딩 페이지에서 academy별 로그인과 회원가입이 동작하도록 인증 플로우에서 사용됩니다.
라우트 상태로서의 쿼리 파라미터
일부 UI 상태는 의도적으로 URL에 인코딩됩니다.
?auth=login|register&callbackUrl=/target은 공유 인증 모달을 엽니다- 학생 목록과 Q&A 같은 리스트 페이지는 필터, 검색, 정렬, 페이지 상태를 search param에 유지합니다
parseStudentListParams()와parseQnaListParams()같은 헬퍼 파서는 라우트 코드와 query key 사이의 URL 파싱을 맞춰줍니다
이렇게 하면 리스트 뷰를 북마크할 수 있고, TanStack Query도 같은 라우트 상태에 대해 안정적인 key를 사용할 수 있습니다.
프록시 라우트 동작
app/api/[...path]/route.ts는 UI를 렌더링하지 않더라도 라우팅 표면의 일부입니다. 이 라우트는 다음을 수행합니다.
- 요청을
NEXT_PUBLIC_API_BASE로 전달 origin같은 브라우저 전용 또는 안전하지 않은 전달 헤더 제거- 커스텀 도메인 해석이 성공하면
X-Tenant-Slug부착 - localhost 프록시 모드에 맞게
Set-Cookie헤더 재작성 - Node fetch 압축 해제 후 오래된 압축 헤더 제거
현재 프록시 불변식은 소스에서 직접 확인할 수 있습니다.
const targetUrl = `${getApiBase()}${url.pathname}${url.search}`;
headers.delete('host');
headers.delete('x-tenant-id');
headers.delete('x-user-id');
headers.delete('x-user-role');
headers.delete('origin');
같은 handler는 Domain과 Secure를 제거하고, SameSite=None을 SameSite=Lax로 낮춰 로컬 개발 중 http://localhost:3000에 인증 쿠키가 저장되도록 Set-Cookie를 다시 씁니다.
검증 가능한 프록시 요청
next dev가 http://localhost:3000에서 실행 중이고 NEXT_PUBLIC_API_BASE가 도달 가능한 백엔드를 가리킬 때, 프론트엔드 리포지토리에서 다음을 실행합니다.
cd lumie-frontend
curl -i -H 'X-Tenant-Slug: demo' http://localhost:3000/api/v1/me
예상 성공 신호:
- 요청은 로컬
app/api/health/route.ts가 아니라app/api/[...path]/route.ts에서 처리됩니다 - 인증 쿠키가 없으면 응답은
401또는403같은 백엔드 인증 응답이어야 합니다 - 유효한
lumie_access_token쿠키가 있으면 같은 경로가200을 반환해야 합니다 - 두 경우 모두 프록시된 응답이 오면 프론트엔드 라우트가 존재하고 Next.js
404를 반환하는 대신NEXT_PUBLIC_API_BASE로 전달했다는 뜻입니다