currentGroupPrefix . '/' . trim($path, '/'), '/'); $route = [ 'method' => strtoupper($method), 'path' => $fullPath, 'callback' => $callback, 'middlewares' => array_merge($this->currentGroupMiddlewares, $middlewares) ]; // 동적 파라미터({param})가 있으면 정규식 패턴 생성 if (strpos($fullPath, '{') !== false) { $pattern = preg_replace_callback('/\{(\w+)\}/', function ($matches) { return '(?P<' . $matches[1] . '>[^/]+)'; }, $fullPath); $route['pattern'] = '/^' . str_replace('/', '\/', $pattern) . '(\/.*)?$/'; } else { // SEO key/value 형태를 위해 접두사 이후의 추가 파라미터를 허용 $route['pattern'] = '/^' . str_replace('/', '\/', $fullPath) . '(\/(?P.*))?$/'; } $this->routes[] = $route; } /** * 라우트 그룹: 공통 접두사와 공통 미들웨어를 적용 */ public function group(string $prefix, callable $callback, array $middlewares = []): void { $previousGroupPrefix = $this->currentGroupPrefix; $previousGroupMiddlewares = $this->currentGroupMiddlewares; $this->currentGroupPrefix = trim($previousGroupPrefix . '/' . trim($prefix, '/'), '/'); $this->currentGroupMiddlewares = array_merge($previousGroupMiddlewares, $middlewares); $callback($this); $this->currentGroupPrefix = $previousGroupPrefix; $this->currentGroupMiddlewares = $previousGroupMiddlewares; } /** * 라우트 매칭 및 실행 */ public function dispatch(string $uri, string $method = 'GET'): mixed { $uri = trim(parse_url($uri, PHP_URL_PATH), '/'); // 정적 파일 처리: public 폴더 내에 파일이 있으면 직접 서빙 $staticFile = DOCUMENTROOT_PATH . $uri; if (file_exists($staticFile) && is_file($staticFile)) { return $this->serveStaticFile($staticFile); } foreach ($this->routes as $route) { if ($route['method'] !== strtoupper($method)) { //GET, POST 등 HTTP 메소드 체크 continue; } if (preg_match($route['pattern'], $uri, $matches)) { $params = []; // 패턴에 named 그룹이 있다면 추출 foreach ($matches as $key => $value) { if (is_string($key)) { $params[$key] = $value; } } //추가 SEO key/value 파라미터로 파싱 if (isset($params['extra']) && $params['extra'] !== '') { $extra = trim($params['extra'], '/'); $extraSegments = explode('/', $extra); // 짝수개여야 key/value 쌍으로 변환 가능 if (count($extraSegments) % 2 === 0) { for ($i = 0; $i < count($extraSegments); $i += 2) { $key = $extraSegments[$i]; $value = $extraSegments[$i + 1]; $params[$key] = $value; } } unset($params['extra']); } else { // 패턴에 extra 그룹이 없으면, 기본 GET 파라미터와 병합 $params = array_merge($_GET, $params); } return $this->handle($route, $params); } } // 404 응답 객체 반환 return (new Response('해당 Route 경로를 찾을수 없습니다.' . $uri, 404)); } /** * 미들웨어 체인을 거쳐 최종 콜백 실행 */ private function handle(array $route, array $params): mixed { $handler = $route['callback']; $middlewares = $route['middlewares']; $pipeline = array_reduce( array_reverse($middlewares), fn($next, $middleware) => fn($params) => (new $middleware)->handle($params, $next), $handler ); return $pipeline($params); } /** * 정적 파일 서빙 (이미지, CSS, JS 등) */ private function serveStaticFile(string $file): Response { return (new Response()) ->header('Content-Type', mime_content_type($file)) ->setContent(file_get_contents($file)); } }