fix(core): 끝 슬래시가 붙은 경로(/admin/)가 라우트에 매칭되지 않던 문제
라우트 패턴은 끝 슬래시 없는 형태(*/admin 등)로만 정의되는데(routes.json 규약), Router.match 가 들어온 경로의 끝 슬래시를 정규화하지 않아 /admin/ (끝 슬래시)이 어떤 라우트에도 매칭되지 않고 404 로 떨어졌다. 특히 미인증 상태로 /admin/ 진입 시 대시보드 → 로그인 리다이렉트 흐름 대신 404 가 노출됐다. match 진입에 normalizePathname 을 두어 루트(/)를 제외한 경로의 끝 슬래시(연속 포함)를 제거한 뒤 대조한다. match 는 라우팅 단일 진입점이라 모든 호출부가 함께 정상화되고, 다운스트림(auth back-URL, getAuthType)은 원본 pathname 을 그대로 써 의미 변화가 없다. 기존 릴리스부터 있던 라우터 견고성 결함이며 자산 URL 이중 모드 변경과는 무관하다(matchPattern 은 미변경, routes.json 은 81건 정상 로드 확인).
This commit is contained in:
@@ -40,6 +40,7 @@
|
||||
- 일부 안내가 실제 문장 대신 내부 식별자(`messages.auth.unauthenticated` 같은 문자열)로 표시되던 문제를 수정했습니다. 로그인하지 않았거나 역할 권한이 없어 요청이 거절될 때, 메뉴에 역할을 지정할 때, 확장 식별자를 잘못 입력했을 때, 활동 로그를 기간·유형으로 검색할 때가 해당됩니다.
|
||||
- 플러그인을 GitHub 주소나 ZIP 파일로 설치할 때의 입력 안내가 내부 식별자로 표시되던 문제를 수정했습니다. 주소 형식이 잘못됐거나, ZIP이 아닌 파일을 올렸거나, 파일 용량을 초과했을 때 정상 문장으로 안내합니다.
|
||||
- 레이아웃 편집기 화면이 정상 동작하는데도 서버가 "페이지 없음"(404)으로 응답하던 문제를 수정했습니다. 화면은 그대로 보였지만 브라우저 캐싱과 서버 접속 기록에서 없는 페이지로 취급되던 상태입니다.
|
||||
- 주소 끝에 슬래시가 붙은 형태(예: `/admin/`)로 접속하면 화면이 "페이지 없음"(404)으로 뜨던 문제를 수정했습니다. 특히 로그인이 풀린 상태에서 이렇게 접속하면 로그인 화면으로 넘어가지 않고 404가 나타났습니다. 이제 끝 슬래시가 있어도 정상적으로 같은 페이지로 연결됩니다.
|
||||
|
||||
## [7.0.5] - 2026-07-16
|
||||
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
@@ -153,8 +153,10 @@ export class Router {
|
||||
* @returns 매칭된 라우트와 파라미터, 없으면 null
|
||||
*/
|
||||
match(pathname: string): RouteMatch | null {
|
||||
const normalizedPath = this.normalizePathname(pathname);
|
||||
|
||||
for (const route of this.routes) {
|
||||
const params = this.matchPattern(route.path, pathname);
|
||||
const params = this.matchPattern(route.path, normalizedPath);
|
||||
|
||||
if (params !== null) {
|
||||
return {
|
||||
@@ -167,6 +169,27 @@ export class Router {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* URL 경로의 끝 슬래시를 정규화합니다.
|
||||
*
|
||||
* 브라우저가 `/admin/` 처럼 끝 슬래시가 붙은 경로로 진입해도 끝 슬래시 없는
|
||||
* 라우트 패턴(언어 prefix 형태 포함)에 매칭되도록, 루트(`/`)를 제외한 나머지
|
||||
* 경로의 끝 슬래시를 제거한다. 라우트 패턴은 끝 슬래시 없는 형태로만 정의되므로
|
||||
* (routes.json 규약), 미정규화 시 `/admin/` 이 어떤 라우트에도 매칭되지 않아
|
||||
* 404 로 떨어지고 — 미인증 진입 시 로그인 리다이렉트 대신 404 가 노출된다.
|
||||
*
|
||||
* @param pathname 원본 URL 경로
|
||||
* @returns 끝 슬래시가 정규화된 경로 (루트는 `/` 유지)
|
||||
* @since engine-v1.54.1
|
||||
*/
|
||||
private normalizePathname(pathname: string): string {
|
||||
if (pathname.length > 1 && pathname.endsWith('/')) {
|
||||
return pathname.replace(/\/+$/, '') || '/';
|
||||
}
|
||||
|
||||
return pathname;
|
||||
}
|
||||
|
||||
/**
|
||||
* 패턴과 경로를 매칭하고 파라미터를 추출합니다.
|
||||
*
|
||||
|
||||
@@ -192,6 +192,50 @@ describe('Router', () => {
|
||||
expect(result?.route.layout).toBe('user-list');
|
||||
expect(result?.params).toEqual({});
|
||||
});
|
||||
|
||||
// 브라우저가 끝 슬래시가 붙은 경로(`/admin/`)로 진입해도, 끝 슬래시 없는
|
||||
// 라우트 패턴(`/admin`)에 매칭되어야 한다. 미정규화 시 어떤 라우트에도
|
||||
// 매칭되지 않아 404 로 떨어지고, 미인증 진입 시 로그인 리다이렉트 대신
|
||||
// 404 가 노출되는 회귀가 발생한다.
|
||||
it('끝 슬래시가 붙은 경로도 끝 슬래시 없는 라우트에 매칭해야 합니다', () => {
|
||||
const result = router.match('/admin/');
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
expect(result?.route.layout).toBe('dashboard');
|
||||
expect(result?.params).toEqual({});
|
||||
});
|
||||
|
||||
it('끝 슬래시가 붙은 하위 경로도 매칭해야 합니다', () => {
|
||||
const result = router.match('/admin/users/');
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
expect(result?.route.layout).toBe('user-list');
|
||||
expect(result?.params).toEqual({});
|
||||
});
|
||||
|
||||
it('끝 슬래시가 붙은 동적 파라미터 경로도 매칭하고 파라미터를 추출해야 합니다', () => {
|
||||
const result = router.match('/admin/users/123/');
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
expect(result?.route.layout).toBe('user-detail');
|
||||
expect(result?.params).toEqual({ id: '123' });
|
||||
});
|
||||
|
||||
it('연속된 끝 슬래시도 정규화해야 합니다', () => {
|
||||
const result = router.match('/admin//');
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
expect(result?.route.layout).toBe('dashboard');
|
||||
});
|
||||
|
||||
it('루트 경로(/)는 끝 슬래시를 제거하지 않아야 합니다', () => {
|
||||
// 루트만 있는 라우트를 임시로 주입해 루트 정규화 안전성을 검증한다.
|
||||
router.setRoutes([{ path: '/', layout: 'home' }]);
|
||||
const result = router.match('/');
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
expect(result?.route.layout).toBe('home');
|
||||
});
|
||||
});
|
||||
|
||||
describe('matchPattern (간접 테스트)', () => {
|
||||
|
||||
@@ -5,6 +5,14 @@
|
||||
>
|
||||
> 형식: [Keep a Changelog](https://keepachangelog.com/ko/1.1.0/)
|
||||
|
||||
## [engine-v1.54.1] - 2026-07-23
|
||||
|
||||
### Fixed
|
||||
|
||||
#### 끝 슬래시가 붙은 경로(`/admin/`)가 라우트에 매칭되지 않던 문제
|
||||
|
||||
- `routing/Router.ts` — `match()` 진입 시 경로의 끝 슬래시를 정규화(`normalizePathname`)한 뒤 패턴과 대조한다. 라우트 패턴은 끝 슬래시 없는 형태(`*/admin` 등)로만 정의되므로(routes.json 규약), 종전에는 브라우저가 `/admin/` 처럼 끝 슬래시가 붙은 경로로 진입하면 `matchPattern` 이 만드는 앵커 정규식(`^…/admin$`)에 걸리지 않아 어떤 라우트에도 매칭되지 않고 404 로 떨어졌다. 특히 미인증 상태로 `/admin/` 에 진입하면 대시보드 리다이렉트 → 로그인 화면 흐름 대신 404 가 노출됐다. 이제 루트(`/`)를 제외한 경로의 끝 슬래시(연속 슬래시 포함)를 제거해 정상 매칭한다.
|
||||
|
||||
## [engine-v1.54.0] - 2026-07-20
|
||||
|
||||
### Added
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* E2E: 끝 슬래시가 붙은 관리자 경로(`/admin/`) 라우팅
|
||||
*
|
||||
* 회귀: 클라이언트 라우터가 라우트 패턴을 끝 슬래시 없는 형태(언어 prefix 형태
|
||||
* 포함)로만 정의하는데, `match()` 가 들어온 경로의 끝 슬래시를 정규화하지 않아 `/admin/`
|
||||
* (끝 슬래시)가 어떤 라우트에도 매칭되지 않고 404 로 떨어졌다. 특히 로그인이
|
||||
* 풀린 상태로 `/admin/` 에 접근하면 대시보드 → 로그인 리다이렉트 흐름 대신 404 가
|
||||
* 노출됐다. 수정: `Router.match()` 진입 시 끝 슬래시를 정규화한다.
|
||||
*
|
||||
* 단위 테스트(Router.test.ts)가 매칭 로직을 잠그지만, PO 가 실제로 브라우저에서
|
||||
* 재현한 증상이므로(단위 green + 브라우저 깨짐 방지) 브라우저 레벨로도 잠근다.
|
||||
*
|
||||
* @scenario admin_trailing_slash_unauthenticated + admin_no_trailing_slash_unauthenticated
|
||||
* @effects redirect_to_login_not_404
|
||||
*/
|
||||
import { test, expect } from '../../fixtures/auth';
|
||||
|
||||
/** 404 에러 페이지 특유의 안내 문구 — 이 문구가 보이면 라우팅 실패다. */
|
||||
const NOT_FOUND_TEXT = '페이지를 찾을 수 없습니다';
|
||||
|
||||
test.describe('관리자 경로 끝 슬래시 정규화', () => {
|
||||
// @scenario admin_trailing_slash_unauthenticated
|
||||
// @effects redirect_to_login_not_404
|
||||
test('미인증 상태로 /admin/ (끝 슬래시) 접근 시 404 가 아니라 로그인으로 이동한다', async ({ page }) => {
|
||||
// authenticatePage 미호출 — 로그인 풀린 상태 모사(localStorage 에 auth_token 없음).
|
||||
await page.goto('/admin/');
|
||||
await page.waitForLoadState('domcontentloaded', { timeout: 30_000 });
|
||||
|
||||
// 미인증 대시보드 진입 → 로그인 화면으로 리다이렉트. 끝 슬래시 미정규화 시
|
||||
// 여기서 404 페이지가 떴다.
|
||||
await expect(page).toHaveURL(/\/admin\/login/, { timeout: 20_000 });
|
||||
await expect(page.getByText(NOT_FOUND_TEXT)).toHaveCount(0);
|
||||
});
|
||||
|
||||
// @scenario admin_no_trailing_slash_unauthenticated
|
||||
// @effects redirect_to_login_not_404
|
||||
test('미인증 상태로 /admin (끝 슬래시 없음) 접근도 동일하게 로그인으로 이동한다', async ({ page }) => {
|
||||
await page.goto('/admin');
|
||||
await page.waitForLoadState('domcontentloaded', { timeout: 30_000 });
|
||||
|
||||
await expect(page).toHaveURL(/\/admin\/login/, { timeout: 20_000 });
|
||||
await expect(page.getByText(NOT_FOUND_TEXT)).toHaveCount(0);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user