UNPKG

ts5deco-express-controller

Version:

TypeScript 5 Modern Decorator Express Controller Framework

4 lines 80.7 kB
{ "version": 3, "sources": ["../src/metadata.ts", "../src/decorators/controller.ts", "../src/decorators/route.ts", "../src/decorators/middleware.ts", "../src/utils/openapi-path-converter.ts", "../src/responses/BaseResponse.ts", "../src/responses/JsonResponse.ts", "../src/types/branded-response.ts", "../src/decorators/typed-route.ts", "../src/router.ts", "../src/responses/TextResponse.ts", "../src/responses/NoContentResponse.ts", "../src/responses/RedirectResponse.ts", "../src/responses/FileResponse.ts", "../src/responses/index.ts", "../src/lib/type-generator.ts", "../src/lib/init.ts", "../src/utils/path-converter.ts"], "sourcesContent": ["import { ControllerMetadata, RouteMetadata } from './types';\n\n// Modern decorator\uC6A9 \uBA54\uD0C0\uB370\uC774\uD130 \uC800\uC7A5\uC18C\nconst metadataStorage = new WeakMap<any, Map<string | symbol, any>>();\n\n// \uBA54\uD0C0\uB370\uC774\uD130 \uC720\uD2F8\uB9AC\uD2F0 \uD568\uC218\nfunction getMetadataMap(target: any): Map<string | symbol, any> {\n if (!metadataStorage.has(target)) {\n metadataStorage.set(target, new Map());\n }\n return metadataStorage.get(target)!;\n}\n\nfunction defineMetadata(key: string | symbol, value: any, target: any): void {\n const metadataMap = getMetadataMap(target);\n metadataMap.set(key, value);\n}\n\nfunction getMetadata<T = any>(key: string | symbol, target: any): T | undefined {\n const metadataMap = metadataStorage.get(target);\n return metadataMap?.get(key);\n}\n\n/**\n * \uBA54\uD0C0\uB370\uC774\uD130 \uD0A4 \uC0C1\uC218\n */\nexport const METADATA_KEYS = {\n CONTROLLER: Symbol('controller'),\n ROUTES: Symbol('routes'),\n PARAMETERS: Symbol('parameters'),\n MIDDLEWARES: Symbol('middlewares')\n} as const;\n\n/**\n * \uCEE8\uD2B8\uB864\uB7EC \uBA54\uD0C0\uB370\uC774\uD130\uB97C \uC124\uC815\uD569\uB2C8\uB2E4\n */\nexport function setControllerMetadata(target: any, metadata: ControllerMetadata): void {\n defineMetadata(METADATA_KEYS.CONTROLLER, metadata, target);\n}\n\n/**\n * \uCEE8\uD2B8\uB864\uB7EC \uBA54\uD0C0\uB370\uC774\uD130\uB97C \uAC00\uC838\uC635\uB2C8\uB2E4\n */\nexport function getControllerMetadata(target: any): ControllerMetadata | undefined {\n return getMetadata(METADATA_KEYS.CONTROLLER, target);\n}\n\n/**\n * \uB77C\uC6B0\uD2B8 \uBA54\uD0C0\uB370\uC774\uD130\uB97C \uC124\uC815\uD569\uB2C8\uB2E4\n */\nexport function setRouteMetadata(target: any, metadata: RouteMetadata[]): void {\n defineMetadata(METADATA_KEYS.ROUTES, metadata, target);\n}\n\n/**\n * \uB77C\uC6B0\uD2B8 \uBA54\uD0C0\uB370\uC774\uD130\uB97C \uAC00\uC838\uC635\uB2C8\uB2E4\n */\nexport function getRouteMetadata(target: any): RouteMetadata[] {\n return getMetadata(METADATA_KEYS.ROUTES, target) || [];\n}\n\n/**\n * \uB77C\uC6B0\uD2B8 \uBA54\uD0C0\uB370\uC774\uD130\uB97C \uCD94\uAC00\uD569\uB2C8\uB2E4\n */\nexport function addRouteMetadata(target: any, metadata: RouteMetadata): void {\n const existingRoutes = getRouteMetadata(target);\n setRouteMetadata(target, [...existingRoutes, metadata]);\n}\n\n", "import { MiddlewareFunction } from '../types';\nimport { setControllerMetadata } from '../metadata';\n\n/**\n * \uCEE8\uD2B8\uB864\uB7EC \uB370\uCF54\uB808\uC774\uD130 \uC635\uC158\n */\nexport interface ControllerOptions {\n path?: string;\n middlewares?: MiddlewareFunction[];\n}\n\n/**\n * \uCEE8\uD2B8\uB864\uB7EC\uB97C \uC815\uC758\uD558\uB294 \uB370\uCF54\uB808\uC774\uD130 - TypeScript 5 Modern Decorator\n * \n * @param pathOrOptions - \uAE30\uBCF8 \uACBD\uB85C \uB610\uB294 \uC635\uC158 \uAC1D\uCCB4\n * @returns \uD074\uB798\uC2A4 \uB370\uCF54\uB808\uC774\uD130\n * \n * @example\n * ```typescript\n * @Controller('/api/users')\n * export class UserController {\n * // ...\n * }\n * \n * @Controller({ path: '/api/users', middlewares: [authMiddleware] })\n * export class UserController {\n * // ...\n * }\n * ```\n */\nexport function Controller(pathOrOptions?: string | ControllerOptions) {\n return function <T extends new (...args: any[]) => any>(\n target: T,\n context: ClassDecoratorContext\n ): T {\n let path = '';\n let middlewares: MiddlewareFunction[] = [];\n\n if (typeof pathOrOptions === 'string') {\n path = pathOrOptions;\n } else if (pathOrOptions) {\n path = pathOrOptions.path || '';\n middlewares = pathOrOptions.middlewares || [];\n }\n\n // \uACBD\uB85C \uC815\uADDC\uD654 (\uC911\uBCF5 \uC2AC\uB798\uC2DC \uC81C\uAC70, \uC2DC\uC791 \uC2AC\uB798\uC2DC \uBCF4\uC7A5)\n path = ('/' + path).replace(/\\/+/g, '/').replace(/\\/$/, '') || '/';\n\n setControllerMetadata(target, {\n path,\n middlewares\n });\n\n return target;\n };\n}\n", "import { HttpMethod, MiddlewareFunction } from '../types';\nimport { addRouteMetadata } from '../metadata';\n\n/**\n * \uB77C\uC6B0\uD2B8 \uB370\uCF54\uB808\uC774\uD130 \uC635\uC158\n */\nexport interface RouteOptions {\n path?: string;\n middlewares?: MiddlewareFunction[];\n}\n\n/**\n * \uAE30\uBCF8 \uB77C\uC6B0\uD2B8 \uB370\uCF54\uB808\uC774\uD130 \uD329\uD1A0\uB9AC - TypeScript 5 Modern Decorator\n */\nfunction createRouteDecorator(method: HttpMethod) {\n return function (pathOrOptions?: string | RouteOptions) {\n return function (\n target: any,\n context: ClassMethodDecoratorContext\n ): void {\n let path = '';\n let middlewares: MiddlewareFunction[] = [];\n\n if (typeof pathOrOptions === 'string') {\n path = pathOrOptions;\n } else if (pathOrOptions) {\n path = pathOrOptions.path || '';\n middlewares = pathOrOptions.middlewares || [];\n }\n\n // \uACBD\uB85C \uC815\uADDC\uD654\n path = ('/' + path).replace(/\\/+/g, '/').replace(/\\/$/, '') || '/';\n\n // context.addInitializer\uB97C \uC0AC\uC6A9\uD558\uC5EC \uD074\uB798\uC2A4 \uCD08\uAE30\uD654 \uC2DC \uBA54\uD0C0\uB370\uC774\uD130 \uCD94\uAC00\n context.addInitializer(function (this: any) {\n addRouteMetadata(this.constructor, {\n path,\n method,\n middlewares,\n propertyKey: context.name\n });\n });\n };\n };\n}\n\n/**\n * GET \uC694\uCCAD\uC744 \uCC98\uB9AC\uD558\uB294 \uB77C\uC6B0\uD2B8 \uB370\uCF54\uB808\uC774\uD130\n * \n * @param pathOrOptions - \uACBD\uB85C \uB610\uB294 \uC635\uC158 \uAC1D\uCCB4\n * @returns \uBA54\uC11C\uB4DC \uB370\uCF54\uB808\uC774\uD130\n * \n * @example\n * ```typescript\n * @Get('/users')\n * async getUsers() {\n * // ...\n * }\n * \n * @Get({ path: '/users/:id', middlewares: [validateId] })\n * async getUser() {\n * // ...\n * }\n * ```\n */\nexport const Get = createRouteDecorator('GET');\n\n/**\n * POST \uC694\uCCAD\uC744 \uCC98\uB9AC\uD558\uB294 \uB77C\uC6B0\uD2B8 \uB370\uCF54\uB808\uC774\uD130\n */\nexport const Post = createRouteDecorator('POST');\n\n/**\n * PUT \uC694\uCCAD\uC744 \uCC98\uB9AC\uD558\uB294 \uB77C\uC6B0\uD2B8 \uB370\uCF54\uB808\uC774\uD130\n */\nexport const Put = createRouteDecorator('PUT');\n\n/**\n * DELETE \uC694\uCCAD\uC744 \uCC98\uB9AC\uD558\uB294 \uB77C\uC6B0\uD2B8 \uB370\uCF54\uB808\uC774\uD130\n */\nexport const Delete = createRouteDecorator('DELETE');\n\n/**\n * PATCH \uC694\uCCAD\uC744 \uCC98\uB9AC\uD558\uB294 \uB77C\uC6B0\uD2B8 \uB370\uCF54\uB808\uC774\uD130\n */\nexport const Patch = createRouteDecorator('PATCH');\n\n/**\n * HEAD \uC694\uCCAD\uC744 \uCC98\uB9AC\uD558\uB294 \uB77C\uC6B0\uD2B8 \uB370\uCF54\uB808\uC774\uD130\n */\nexport const Head = createRouteDecorator('HEAD');\n\n/**\n * OPTIONS \uC694\uCCAD\uC744 \uCC98\uB9AC\uD558\uB294 \uB77C\uC6B0\uD2B8 \uB370\uCF54\uB808\uC774\uD130\n */\nexport const Options = createRouteDecorator('OPTIONS');\n\n/**\n * \uBAA8\uB4E0 HTTP \uBA54\uC11C\uB4DC\uB97C \uCC98\uB9AC\uD558\uB294 \uB77C\uC6B0\uD2B8 \uB370\uCF54\uB808\uC774\uD130 - TypeScript 5 Modern Decorator\n */\nexport function All(pathOrOptions?: string | RouteOptions) {\n return function (\n target: any,\n context: ClassMethodDecoratorContext\n ): void {\n const methods: HttpMethod[] = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS'];\n \n let path = '';\n let middlewares: MiddlewareFunction[] = [];\n\n if (typeof pathOrOptions === 'string') {\n path = pathOrOptions;\n } else if (pathOrOptions) {\n path = pathOrOptions.path || '';\n middlewares = pathOrOptions.middlewares || [];\n }\n\n // \uACBD\uB85C \uC815\uADDC\uD654\n path = ('/' + path).replace(/\\/+/g, '/').replace(/\\/$/, '') || '/';\n\n // context.addInitializer\uB97C \uC0AC\uC6A9\uD558\uC5EC \uBAA8\uB4E0 \uBA54\uC11C\uB4DC\uC5D0 \uB300\uD574 \uB77C\uC6B0\uD2B8 \uBA54\uD0C0\uB370\uC774\uD130 \uCD94\uAC00\n context.addInitializer(function (this: any) {\n methods.forEach(method => {\n addRouteMetadata(this.constructor, {\n path,\n method,\n middlewares,\n propertyKey: context.name\n });\n });\n });\n };\n}\n", "import { MiddlewareFunction } from '../types';\nimport { addRouteMetadata, getRouteMetadata, setRouteMetadata, METADATA_KEYS } from '../metadata';\n\n/**\n * \uBBF8\uB4E4\uC6E8\uC5B4\uB97C \uC801\uC6A9\uD558\uB294 \uB370\uCF54\uB808\uC774\uD130 - TypeScript 5 Modern Decorator\n * \n * @param middlewares - \uC801\uC6A9\uD560 \uBBF8\uB4E4\uC6E8\uC5B4 \uD568\uC218\uB4E4\n * @returns \uBA54\uC11C\uB4DC \uB370\uCF54\uB808\uC774\uD130\n * \n * @example\n * ```typescript\n * @Use(authMiddleware, loggingMiddleware)\n * @Get('/protected')\n * async getProtectedData() {\n * // ...\n * }\n * ```\n */\nexport function Use(...middlewares: MiddlewareFunction[]) {\n return function (\n target: any,\n context: ClassMethodDecoratorContext\n ): void {\n // context.addInitializer\uB97C \uC0AC\uC6A9\uD558\uC5EC \uBBF8\uB4E4\uC6E8\uC5B4 \uC124\uC815\n context.addInitializer(function (this: any) {\n const routes = getRouteMetadata(this.constructor);\n const updatedRoutes = routes.map(route => {\n if (route.propertyKey === context.name) {\n return {\n ...route,\n middlewares: [...middlewares, ...route.middlewares]\n };\n }\n return route;\n });\n\n // \uC5C5\uB370\uC774\uD2B8\uB41C \uB77C\uC6B0\uD2B8 \uBA54\uD0C0\uB370\uC774\uD130 \uC124\uC815\n if (updatedRoutes.length > 0) {\n setRouteMetadata(this.constructor, updatedRoutes);\n }\n });\n };\n}\n\n/**\n * \uC778\uC99D\uC774 \uD544\uC694\uD55C \uB77C\uC6B0\uD2B8\uC784\uC744 \uB098\uD0C0\uB0B4\uB294 \uB370\uCF54\uB808\uC774\uD130\n * \uC2E4\uC81C \uC778\uC99D \uBBF8\uB4E4\uC6E8\uC5B4\uB294 \uC0AC\uC6A9\uC790\uAC00 \uC81C\uACF5\uD574\uC57C \uD568\n * \n * @param authMiddleware - \uC778\uC99D \uBBF8\uB4E4\uC6E8\uC5B4 \uD568\uC218\n * @returns \uBA54\uC11C\uB4DC \uB370\uCF54\uB808\uC774\uD130\n * \n * @example\n * ```typescript\n * @Authenticated(jwtAuthMiddleware)\n * @Get('/profile')\n * async getProfile() {\n * // ...\n * }\n * ```\n */\nexport function Authenticated(authMiddleware: MiddlewareFunction) {\n return Use(authMiddleware);\n}\n\n/**\n * \uAD8C\uD55C \uD655\uC778\uC774 \uD544\uC694\uD55C \uB77C\uC6B0\uD2B8\uC784\uC744 \uB098\uD0C0\uB0B4\uB294 \uB370\uCF54\uB808\uC774\uD130\n * \n * @param authorizeMiddleware - \uAD8C\uD55C \uD655\uC778 \uBBF8\uB4E4\uC6E8\uC5B4 \uD568\uC218\n * @returns \uBA54\uC11C\uB4DC \uB370\uCF54\uB808\uC774\uD130\n * \n * @example\n * ```typescript\n * @Authorized(adminOnlyMiddleware)\n * @Delete('/admin/users/:id')\n * async deleteUser() {\n * // ...\n * }\n * ```\n */\nexport function Authorized(authorizeMiddleware: MiddlewareFunction) {\n return Use(authorizeMiddleware);\n}\n\n/**\n * \uC694\uCCAD \uAC80\uC99D\uC774 \uD544\uC694\uD55C \uB77C\uC6B0\uD2B8\uC784\uC744 \uB098\uD0C0\uB0B4\uB294 \uB370\uCF54\uB808\uC774\uD130\n * \n * @param validationMiddleware - \uAC80\uC99D \uBBF8\uB4E4\uC6E8\uC5B4 \uD568\uC218\n * @returns \uBA54\uC11C\uB4DC \uB370\uCF54\uB808\uC774\uD130\n * \n * @example\n * ```typescript\n * @Validated(validateUserSchema)\n * @Post('/users')\n * async createUser() {\n * // ...\n * }\n * ```\n */\nexport function Validated(validationMiddleware: MiddlewareFunction) {\n return Use(validationMiddleware);\n}\n", "/**\n * OpenAPI \uACBD\uB85C\uB97C Express \uACBD\uB85C\uB85C \uBCC0\uD658\uD558\uB294 \uC720\uD2F8\uB9AC\uD2F0\n */\n\n/**\n * OpenAPI \uACBD\uB85C\uB97C Express \uACBD\uB85C\uB85C \uBCC0\uD658\n * \uC608: '/users/{id}' -> '/users/:id'\n * \uC608: '/users/{userId}/posts/{postId}' -> '/users/:userId/posts/:postId'\n */\nexport function convertOpenApiPathToExpressPath(openApiPath: string): string {\n return openApiPath.replace(/{([^}]+)}/g, ':$1');\n}\n\n/**\n * Express \uACBD\uB85C\uB97C OpenAPI \uACBD\uB85C\uB85C \uBCC0\uD658 (\uC5ED\uBCC0\uD658)\n * \uC608: '/users/:id' -> '/users/{id}'\n */\nexport function convertExpressPathToOpenApiPath(expressPath: string): string {\n return expressPath.replace(/:([^/]+)/g, '{$1}');\n}\n\n/**\n * \uACBD\uB85C\uC5D0\uC11C \uD30C\uB77C\uBBF8\uD130 \uC774\uB984\uB4E4\uC744 \uCD94\uCD9C\n * \uC608: '/users/{id}/posts/{postId}' -> ['id', 'postId']\n */\nexport function extractPathParameterNames(openApiPath: string): string[] {\n const matches = openApiPath.match(/{([^}]+)}/g);\n if (!matches) return [];\n \n return matches.map(match => match.slice(1, -1)); // {} \uC81C\uAC70\n}\n\n/**\n * \uACBD\uB85C\uAC00 \uC720\uD6A8\uD55C OpenAPI \uACBD\uB85C \uD615\uC2DD\uC778\uC9C0 \uAC80\uC99D\n */\nexport function isValidOpenApiPath(path: string): boolean {\n // \uAE30\uBCF8\uC801\uC778 \uAC80\uC99D: {param} \uD615\uC2DD\uC758 \uD30C\uB77C\uBBF8\uD130\uAC00 \uC62C\uBC14\uB978\uC9C0 \uD655\uC778\n const braceCount = (path.match(/{/g) || []).length;\n const closeBraceCount = (path.match(/}/g) || []).length;\n \n if (braceCount !== closeBraceCount) {\n return false;\n }\n \n // \uC911\uCCA9\uB41C \uBE0C\uB808\uC774\uC2A4\uAC00 \uC5C6\uB294\uC9C0 \uD655\uC778\n let openBraces = 0;\n for (const char of path) {\n if (char === '{') {\n openBraces++;\n if (openBraces > 1) return false;\n } else if (char === '}') {\n openBraces--;\n if (openBraces < 0) return false;\n }\n }\n \n return openBraces === 0;\n}\n\n/**\n * \uACBD\uB85C \uB9E4\uCE6D\uC744 \uC704\uD55C \uC815\uADDC\uC2DD \uC0DD\uC131\n * Express \uACBD\uB85C \uB9E4\uCE6D\uACFC \uC720\uC0AC\uD55C \uAE30\uB2A5\n */\nexport function createPathMatcher(openApiPath: string): RegExp {\n const expressPath = convertOpenApiPathToExpressPath(openApiPath);\n const regexPath = expressPath\n .replace(/:[^/]+/g, '([^/]+)') // \uD30C\uB77C\uBBF8\uD130\uB97C \uCEA1\uCC98 \uADF8\uB8F9\uC73C\uB85C \uBCC0\uD658\n .replace(/\\//g, '\\\\/'); // \uC2AC\uB798\uC2DC \uC774\uC2A4\uCF00\uC774\uD504\n \n return new RegExp(`^${regexPath}$`);\n}\n\n/**\n * \uC2E4\uC81C \uACBD\uB85C\uC5D0\uC11C \uD30C\uB77C\uBBF8\uD130 \uAC12\uC744 \uCD94\uCD9C\n */\nexport function extractPathParameterValues(\n openApiPath: string, \n actualPath: string\n): Record<string, string> {\n const paramNames = extractPathParameterNames(openApiPath);\n const matcher = createPathMatcher(openApiPath);\n const matches = actualPath.match(matcher);\n \n if (!matches || matches.length !== paramNames.length + 1) {\n return {};\n }\n \n const result: Record<string, string> = {};\n paramNames.forEach((paramName, index) => {\n const value = matches[index + 1];\n if (value !== undefined) {\n result[paramName] = value; // matches[0]\uC740 \uC804\uCCB4 \uB9E4\uCE58\n }\n });\n \n return result;\n}", "import { Response } from 'express';\n\n/**\n * Abstract base class for all HTTP responses\n * Provides a consistent interface for handling different response types\n */\nexport abstract class BaseResponse {\n constructor(public readonly statusCode: number) {}\n\n /**\n * Sends the response using the Express Response object\n * @param res Express Response object\n */\n abstract send(res: Response): void;\n}\n", "import { Response } from 'express';\nimport { BaseResponse } from './BaseResponse';\n\n/**\n * JSON response class for sending JSON data with appropriate status codes\n * \n * @template TData - The type of the JSON data\n * @template TStatus - The HTTP status code (defaults to number)\n * \n * @example\n * ```typescript\n * // Success response with typed data\n * return new JsonResponse<User[], 200>(200, users);\n * \n * // Created response with specific type\n * return new JsonResponse<CreateUserResponse, 201>(201, { id: 1, name: 'John' });\n * \n * // Error response with error object\n * return new JsonResponse<ErrorResponse, 400>(400, { error: 'Invalid input' });\n * \n * // Using without generics (still works)\n * return new JsonResponse(200, { users: [...] });\n * ```\n */\nexport class JsonResponse<TData = any, TStatus extends number = number> extends BaseResponse {\n constructor(statusCode: TStatus, public readonly data?: TData) {\n super(statusCode);\n }\n\n send(res: Response): void {\n res.status(this.statusCode).json(this.data);\n }\n\n /**\n * Static convenience methods for common JSON responses\n */\n\n /**\n * 200 OK with JSON data\n */\n static ok<TData = any>(data?: TData): JsonResponse<TData, 200> {\n return new JsonResponse<TData, 200>(200, data);\n }\n\n /**\n * 201 Created with JSON data\n */\n static created<TData = any>(data?: TData): JsonResponse<TData, 201> {\n return new JsonResponse<TData, 201>(201, data);\n }\n\n /**\n * 400 Bad Request with error data\n */\n static badRequest<TError = any>(error?: TError): JsonResponse<TError, 400> {\n return new JsonResponse<TError, 400>(400, error);\n }\n\n /**\n * 401 Unauthorized with error data\n */\n static unauthorized<TError = any>(error?: TError): JsonResponse<TError, 401> {\n return new JsonResponse<TError, 401>(401, error);\n }\n\n /**\n * 403 Forbidden with error data\n */\n static forbidden<TError = any>(error?: TError): JsonResponse<TError, 403> {\n return new JsonResponse<TError, 403>(403, error);\n }\n\n /**\n * 404 Not Found with error data\n */\n static notFound<TError = any>(error?: TError): JsonResponse<TError, 404> {\n return new JsonResponse<TError, 404>(404, error);\n }\n\n /**\n * 500 Internal Server Error with error data\n */\n static internalError<TError = any>(error?: TError): JsonResponse<TError, 500> {\n return new JsonResponse<TError, 500>(500, error);\n }\n}\n", "/**\n * \uBE0C\uB79C\uB4DC \uD0C0\uC785\uC744 \uC0AC\uC6A9\uD55C \uC5C4\uACA9\uD55C \uC751\uB2F5 \uC2DC\uC2A4\uD15C\n * TypeScript\uC758 \uAD6C\uC870\uC801 \uD0C0\uC774\uD551\uC744 \uC6B0\uD68C\uD558\uC5EC \uB354 \uC5C4\uACA9\uD55C \uD0C0\uC785 \uAC80\uC99D\uC744 \uC81C\uACF5\n */\n\nimport { JsonResponse } from '../responses/JsonResponse';\n\n/**\n * \uD0C0\uC785 \uC548\uC804\uD55C JSON \uC751\uB2F5\n */\nexport interface TypedJsonResponse<TData, TStatus extends number, TBrand extends string>\n extends JsonResponse<TData, TStatus> {\n readonly __brand: TBrand;\n readonly __statusCode: TStatus;\n readonly __data: TData;\n}\n\n/**\n * \uD2B9\uC815 API \uC5D4\uB4DC\uD3EC\uC778\uD2B8\uC5D0 \uB300\uD55C \uBE0C\uB79C\uB4DC \uC0DD\uC131\n */\nexport type CreateApiBrand<\n TPath extends string,\n TMethod extends string,\n TStatus extends number\n> = `api:${TPath}:${TMethod}:${TStatus}`;\n\n/**\n * OpenAPI \uACBD\uB85C\uC640 \uBA54\uC11C\uB4DC\uC5D0 \uB300\uD55C \uD5C8\uC6A9\uB41C \uC0C1\uD0DC \uCF54\uB4DC \uCCB4\uD06C\n */\nexport type ValidStatusCodes<\n TPaths,\n TPath extends keyof TPaths,\n TMethod extends string\n> = TPath extends keyof TPaths\n ? TMethod extends keyof TPaths[TPath]\n ? TPaths[TPath][TMethod] extends { responses: infer TResponses }\n ? keyof TResponses & number\n : never\n : never\n : never;\n\n/**\n * \uC0C1\uD0DC \uCF54\uB4DC \uC720\uD6A8\uC131 \uAC80\uC99D \uD0C0\uC785\n */\nexport type AssertValidStatus<\n TPaths,\n TPath extends keyof TPaths,\n TMethod extends string,\n TStatus extends number\n> = TStatus extends ValidStatusCodes<TPaths, TPath, TMethod>\n ? TStatus\n : {\n error: `Status code ${TStatus} is not valid for ${TMethod} ${string & TPath}`;\n allowedCodes: ValidStatusCodes<TPaths, TPath, TMethod>;\n };\n\n/**\n * \uBE0C\uB79C\uB4DC\uB41C \uC751\uB2F5 \uD0C0\uC785 \uC0DD\uC131\n */\nexport type CreateTypedResponse<\n TPaths,\n TPath extends keyof TPaths,\n TMethod extends string,\n TStatus extends number,\n TData = any\n> = AssertValidStatus<TPaths, TPath, TMethod, TStatus> extends TStatus\n ? TypedJsonResponse<TData, TStatus, CreateApiBrand<string & TPath, TMethod, TStatus>>\n : never;\n\n/**\n * \uBAA8\uB4E0 \uD5C8\uC6A9\uB41C \uC751\uB2F5\uC758 Union \uD0C0\uC785 (\uBE0C\uB79C\uB4DC \uD3EC\uD568)\n */\nexport type ApiResponse<\n TPaths,\n TPath extends keyof TPaths,\n TMethod extends string\n> = TPath extends keyof TPaths\n ? TMethod extends keyof TPaths[TPath]\n ? TPaths[TPath][TMethod] extends { responses: infer TResponses }\n ? TResponses extends Record<number, any>\n ? {\n [TStatus in keyof TResponses & number]: TResponses[TStatus] extends {\n content: { 'application/json': infer TData }\n }\n ? CreateTypedResponse<TPaths, TPath, TMethod, TStatus, TData>\n : TResponses[TStatus] extends { content?: never }\n ? CreateTypedResponse<TPaths, TPath, TMethod, TStatus, never>\n : never\n }[keyof TResponses & number]\n : never\n : never\n : never\n : never;\n\n/**\n * \uBE0C\uB79C\uB4DC\uB41C \uC751\uB2F5 \uC0DD\uC131 \uD329\uD1A0\uB9AC\n */\nexport class ResponseFactory {\n /**\n * \uD0C0\uC785\uACFC \uBE0C\uB79C\uB4DC\uAC00 \uC77C\uCE58\uD558\uB294 \uC751\uB2F5\uB9CC \uC0DD\uC131\n */\n static create<\n TPaths,\n TPath extends keyof TPaths,\n TMethod extends string,\n TStatus extends number,\n TData\n >(\n statusCode: TStatus,\n data?: TData\n ): CreateTypedResponse<TPaths, TPath, TMethod, TStatus, TData> {\n const response = new JsonResponse(statusCode, data) as any;\n response.__brand = `api:${String(statusCode)}:${String(statusCode)}:${statusCode}`;\n response.__statusCode = statusCode;\n response.__data = data;\n return response;\n }\n\n /**\n * 200 OK \uC804\uC6A9 \uD329\uD1A0\uB9AC\n */\n static ok<\n TPaths,\n TPath extends keyof TPaths,\n TMethod extends string,\n TData\n >(\n data?: TData\n ): CreateTypedResponse<TPaths, TPath, TMethod, 200, TData> {\n return this.create<TPaths, TPath, TMethod, 200, TData>(200, data);\n }\n\n /**\n * 201 Created \uC804\uC6A9 \uD329\uD1A0\uB9AC\n */\n static created<\n TPaths,\n TPath extends keyof TPaths,\n TMethod extends string,\n TData\n >(\n data?: TData\n ): CreateTypedResponse<TPaths, TPath, TMethod, 201, TData> {\n return this.create<TPaths, TPath, TMethod, 201, TData>(201, data);\n }\n\n /**\n * 400 Bad Request \uC804\uC6A9 \uD329\uD1A0\uB9AC\n */\n static badRequest<\n TPaths,\n TPath extends keyof TPaths,\n TMethod extends string,\n TData\n >(\n data?: TData\n ): CreateTypedResponse<TPaths, TPath, TMethod, 400, TData> {\n return this.create<TPaths, TPath, TMethod, 400, TData>(400, data);\n }\n\n /**\n * 404 Not Found \uC804\uC6A9 \uD329\uD1A0\uB9AC\n */\n static notFound<\n TPaths,\n TPath extends keyof TPaths,\n TMethod extends string,\n TData\n >(\n data?: TData\n ): CreateTypedResponse<TPaths, TPath, TMethod, 404, TData> {\n return this.create<TPaths, TPath, TMethod, 404, TData>(404, data);\n }\n\n /**\n * 500 Internal Server Error \uC804\uC6A9 \uD329\uD1A0\uB9AC\n */\n static internalError<\n TPaths,\n TPath extends keyof TPaths,\n TMethod extends string,\n TData\n >(\n data?: TData\n ): CreateTypedResponse<TPaths, TPath, TMethod, 500, TData> {\n return this.create<TPaths, TPath, TMethod, 500, TData>(500, data);\n }\n}\n\n/**\n * \uD0C0\uC785 \uC548\uC804\uD55C \uC751\uB2F5 \uC0DD\uC131 \uD5EC\uD37C\n * OpenAPI \uC2A4\uD399\uC5D0 \uC815\uC758\uB41C \uC0C1\uD0DC \uCF54\uB4DC\uB9CC \uD5C8\uC6A9\n */\nexport function createApiResponse<\n TPaths,\n TPath extends keyof TPaths,\n TMethod extends string\n>(): TypedApiResponseFactory<TPaths, TPath, TMethod> {\n return new TypedApiResponseFactory<TPaths, TPath, TMethod>();\n}\n\n/**\n * \uC5C4\uACA9\uD55C API \uC751\uB2F5 \uD329\uD1A0\uB9AC \uD074\uB798\uC2A4\n * \uD5C8\uC6A9\uB418\uC9C0 \uC54A\uB294 \uC0C1\uD0DC \uCF54\uB4DC \uC0AC\uC6A9 \uC2DC \uCEF4\uD30C\uC77C \uC5D0\uB7EC \uBC1C\uC0DD\n */\nexport class TypedApiResponseFactory<\n TPaths,\n TPath extends keyof TPaths,\n TMethod extends string\n> {\n private _allowedStatuses!: ValidStatusCodes<TPaths, TPath, TMethod>;\n\n /**\n * 200 OK \uC751\uB2F5 (\uD5C8\uC6A9\uB41C \uACBD\uC6B0\uC5D0\uB9CC \uC0AC\uC6A9 \uAC00\uB2A5)\n */\n ok<TData>(data?: TData): AssertValidResponse<TPaths, TPath, TMethod, 200, TData> {\n return ResponseFactory.ok<TPaths, TPath, TMethod, TData>(data) as any;\n }\n\n /**\n * 201 Created \uC751\uB2F5 (\uD5C8\uC6A9\uB41C \uACBD\uC6B0\uC5D0\uB9CC \uC0AC\uC6A9 \uAC00\uB2A5)\n */\n created<TData>(data?: TData): AssertValidResponse<TPaths, TPath, TMethod, 201, TData> {\n return ResponseFactory.created<TPaths, TPath, TMethod, TData>(data) as any;\n }\n\n /**\n * 400 Bad Request \uC751\uB2F5 (\uD5C8\uC6A9\uB41C \uACBD\uC6B0\uC5D0\uB9CC \uC0AC\uC6A9 \uAC00\uB2A5)\n */\n badRequest<TData>(data?: TData): AssertValidResponse<TPaths, TPath, TMethod, 400, TData> {\n return ResponseFactory.badRequest<TPaths, TPath, TMethod, TData>(data) as any;\n }\n\n /**\n * 404 Not Found \uC751\uB2F5 (\uD5C8\uC6A9\uB41C \uACBD\uC6B0\uC5D0\uB9CC \uC0AC\uC6A9 \uAC00\uB2A5)\n */\n notFound<TData>(data?: TData): AssertValidResponse<TPaths, TPath, TMethod, 404, TData> {\n return ResponseFactory.notFound<TPaths, TPath, TMethod, TData>(data) as any;\n }\n\n /**\n * 500 Internal Server Error \uC751\uB2F5 (\uD5C8\uC6A9\uB41C \uACBD\uC6B0\uC5D0\uB9CC \uC0AC\uC6A9 \uAC00\uB2A5)\n */\n internalError<TData>(data?: TData): AssertValidResponse<TPaths, TPath, TMethod, 500, TData> {\n return ResponseFactory.internalError<TPaths, TPath, TMethod, TData>(data) as any;\n }\n}\n\n/**\n * \uC751\uB2F5 \uC720\uD6A8\uC131 \uAC80\uC99D \uD0C0\uC785\n * \uD5C8\uC6A9\uB418\uC9C0 \uC54A\uB294 \uC0C1\uD0DC \uCF54\uB4DC \uC0AC\uC6A9 \uC2DC \uBA85\uC2DC\uC801 \uC5D0\uB7EC \uD0C0\uC785 \uBC18\uD658\n */\nexport type AssertValidResponse<\n TPaths,\n TPath extends keyof TPaths,\n TMethod extends string,\n TStatus extends number,\n TData\n> = AssertValidStatus<TPaths, TPath, TMethod, TStatus> extends TStatus\n ? CreateTypedResponse<TPaths, TPath, TMethod, TStatus, TData>\n : AssertValidStatus<TPaths, TPath, TMethod, TStatus>;", "/**\n * OpenAPI \uD0C0\uC785 \uC548\uC804 \uB77C\uC6B0\uD2B8 \uB370\uCF54\uB808\uC774\uD130\n * OpenAPI \uC2A4\uD399\uACFC TypeScript \uD0C0\uC785\uC744 \uD1B5\uD569\uD558\uC5EC \uCEF4\uD30C\uC77C \uD0C0\uC784 \uAC80\uC99D \uC81C\uACF5\n */\n\nimport { HttpMethod, MiddlewareFunction } from '../types';\nimport { addRouteMetadata } from '../metadata';\nimport { convertOpenApiPathToExpressPath } from '../utils/openapi-path-converter';\nimport type { ExtractPaths, LowercaseMethod } from '../types/openapi';\nimport type {\n ApiResponse,\n ValidStatusCodes\n} from '../types/branded-response';\n\n// Re-export for convenience\nexport type { ApiResponse } from '../types/branded-response';\nimport { createApiResponse } from '../types/branded-response';\n\n/**\n * \uD0C0\uC785 \uC548\uC804 \uB77C\uC6B0\uD2B8 \uB370\uCF54\uB808\uC774\uD130 \uC635\uC158\n */\nexport interface TypedRouteOptions {\n middlewares?: MiddlewareFunction[];\n}\n\n/**\n * \uD0C0\uC785 \uC548\uC804 \uB77C\uC6B0\uD2B8 \uB370\uCF54\uB808\uC774\uD130 \uD329\uD1A0\uB9AC\n */\nfunction createTypedRouteDecorator<TPaths = any>(method: HttpMethod) {\n return function <TPath extends ExtractPaths<TPaths> & string>(\n path: TPath,\n options: TypedRouteOptions = {}\n ) {\n // \uCEF4\uD30C\uC77C \uD0C0\uC784\uC5D0 \uACBD\uB85C\uC640 \uBA54\uC11C\uB4DC\uAC00 \uC720\uD6A8\uD55C\uC9C0 \uAC80\uC99D\n type ValidatedMethod = LowercaseMethod<typeof method>;\n type ValidatedPath = TPath extends keyof TPaths ? TPath : never;\n\n // \uD5C8\uC6A9\uB41C \uC0C1\uD0DC \uCF54\uB4DC\uB4E4\uC744 \uBBF8\uB9AC \uACC4\uC0B0\n type AllowedStatuses = ValidStatusCodes<TPaths, ValidatedPath, ValidatedMethod>;\n\n return function <\n TReturn extends ApiResponse<TPaths, ValidatedPath, ValidatedMethod>\n >(\n target: (this: any, ...args: any[]) => Promise<TReturn>,\n context: ClassMethodDecoratorContext\n ): void {\n const { middlewares = [] } = options;\n\n // \uCEF4\uD30C\uC77C \uD0C0\uC784 \uAC80\uC99D: \uACBD\uB85C\uAC00 TPaths\uC5D0 \uC874\uC7AC\uD558\uB294\uC9C0 \uD655\uC778\n const _pathValidation: ValidatedPath = path as ValidatedPath;\n\n // \uCEF4\uD30C\uC77C \uD0C0\uC784 \uAC80\uC99D: \uD5C8\uC6A9\uB41C \uC0C1\uD0DC \uCF54\uB4DC\uAC00 \uC788\uB294\uC9C0 \uD655\uC778\n const _statusValidation: AllowedStatuses extends never\n ? \"No valid status codes for this path and method\"\n : AllowedStatuses = {} as any;\n\n // OpenAPI \uACBD\uB85C\uB97C Express \uACBD\uB85C\uB85C \uBCC0\uD658\n const expressPath = convertOpenApiPathToExpressPath(path);\n\n // \uACBD\uB85C \uC815\uADDC\uD654\n const normalizedPath = ('/' + expressPath).replace(/\\/+/g, '/').replace(/\\/$/, '') || '/';\n\n // context.addInitializer\uB97C \uC0AC\uC6A9\uD558\uC5EC \uD074\uB798\uC2A4 \uCD08\uAE30\uD654 \uC2DC \uBA54\uD0C0\uB370\uC774\uD130 \uCD94\uAC00\n context.addInitializer(function (this: any) {\n addRouteMetadata(this.constructor, {\n path: normalizedPath,\n method,\n middlewares,\n propertyKey: context.name\n });\n });\n };\n };\n}\n\n/**\n * \uD0C0\uC785 \uC548\uC804 GET \uB370\uCF54\uB808\uC774\uD130\n */\nexport function TypedGet<TPaths = any>() {\n return createTypedRouteDecorator<TPaths>('GET');\n}\n\n/**\n * \uD0C0\uC785 \uC548\uC804 POST \uB370\uCF54\uB808\uC774\uD130\n */\nexport function TypedPost<TPaths = any>() {\n return createTypedRouteDecorator<TPaths>('POST');\n}\n\n/**\n * \uD0C0\uC785 \uC548\uC804 PUT \uB370\uCF54\uB808\uC774\uD130\n */\nexport function TypedPut<TPaths = any>() {\n return createTypedRouteDecorator<TPaths>('PUT');\n}\n\n/**\n * \uD0C0\uC785 \uC548\uC804 DELETE \uB370\uCF54\uB808\uC774\uD130\n */\nexport function TypedDelete<TPaths = any>() {\n return createTypedRouteDecorator<TPaths>('DELETE');\n}\n\n/**\n * \uD0C0\uC785 \uC548\uC804 \uB77C\uC6B0\uD2B8 \uC9D1\uD569 \uC0DD\uC131\n */\nexport function createTypedRoutes<TPaths = any>() {\n return {\n Get: createTypedRouteDecorator<TPaths>('GET'),\n Post: createTypedRouteDecorator<TPaths>('POST'),\n Put: createTypedRouteDecorator<TPaths>('PUT'),\n Delete: createTypedRouteDecorator<TPaths>('DELETE'),\n Patch: createTypedRouteDecorator<TPaths>('PATCH'),\n Head: createTypedRouteDecorator<TPaths>('HEAD'),\n Options: createTypedRouteDecorator<TPaths>('OPTIONS')\n };\n}\n\n/**\n * \uD0C0\uC785 \uC548\uC804 \uC751\uB2F5 \uC0DD\uC131 \uB3C4\uC6B0\uBBF8\n * \uD2B9\uC815 \uACBD\uB85C\uC640 \uBA54\uC11C\uB4DC\uC5D0 \uB300\uD574 \uD0C0\uC785\uC774 \uBBF8\uB9AC \uBC14\uC778\uB529\uB41C \uC751\uB2F5 \uD329\uD1A0\uB9AC \uC81C\uACF5\n */\nexport function createResponseFor<\n TPaths,\n TPath extends keyof TPaths,\n TMethod extends string\n>() {\n return createApiResponse<TPaths, TPath, TMethod>();\n}\n\n/**\n * \uD0C0\uC785 \uAC80\uC99D \uD5EC\uD37C\n */\nexport type TypeValidation<TPaths, TPath extends keyof TPaths, TMethod extends string> = {\n pathExists: TPath extends keyof TPaths ? true : \"Path does not exist in OpenAPI spec\";\n methodExists: TPath extends keyof TPaths\n ? TMethod extends keyof TPaths[TPath]\n ? true\n : \"Method does not exist for this path\"\n : \"Cannot validate method because path does not exist\";\n hasResponses: TPath extends keyof TPaths\n ? TMethod extends keyof TPaths[TPath]\n ? TPaths[TPath][TMethod] extends { responses: any }\n ? true\n : \"No responses defined for this endpoint\"\n : \"Cannot check responses because method does not exist\"\n : \"Cannot check responses because path does not exist\";\n allowedStatuses: ValidStatusCodes<TPaths, TPath, TMethod>;\n};\n\n/**\n * API \uC5D4\uB4DC\uD3EC\uC778\uD2B8 \uC815\uBCF4 \uD0C0\uC785\n */\nexport type ApiEndpointInfo<TPaths, TPath extends keyof TPaths, TMethod extends string> = {\n validation: TypeValidation<TPaths, TPath, TMethod>;\n allowedStatusCodes: ValidStatusCodes<TPaths, TPath, TMethod>;\n responseType: ApiResponse<TPaths, TPath, TMethod>;\n};\n", "import { Router } from 'express';\nimport { Request, Response, NextFunction } from 'express';\nimport { ControllerConstructor } from './types';\nimport { getControllerMetadata, getRouteMetadata } from './metadata';\nimport { BaseResponse } from './responses/BaseResponse';\n\n/**\n * \uCEE8\uD2B8\uB864\uB7EC\uC5D0\uC11C Express Router\uB97C \uC0DD\uC131\uD569\uB2C8\uB2E4\n * \n * @param controllers - \uB4F1\uB85D\uD560 \uCEE8\uD2B8\uB864\uB7EC \uD074\uB798\uC2A4\uB4E4\n * @returns Express Router \uC778\uC2A4\uD134\uC2A4\n * \n * @example\n * ```typescript\n * import { createRouter } from 'ts5deco-express-controller';\n * import { UserController, PostController } from './controllers';\n * \n * const router = createRouter([UserController, PostController]);\n * app.use('/api', router);\n * ```\n */\nexport function createRouter(controllers: ControllerConstructor[]): Router {\n const router = Router();\n\n controllers.forEach(ControllerClass => {\n registerController(router, ControllerClass);\n });\n\n return router;\n}\n\n/**\n * \uB2E8\uC77C \uCEE8\uD2B8\uB864\uB7EC\uB97C \uB77C\uC6B0\uD130\uC5D0 \uB4F1\uB85D\uD569\uB2C8\uB2E4\n * \n * @param router - Express Router \uC778\uC2A4\uD134\uC2A4\n * @param ControllerClass - \uB4F1\uB85D\uD560 \uCEE8\uD2B8\uB864\uB7EC \uD074\uB798\uC2A4\n * \n * @example\n * ```typescript\n * import { registerController } from 'ts5deco-express-controller';\n * import { UserController } from './controllers/user.controller';\n * \n * const router = Router();\n * registerController(router, UserController);\n * app.use('/api', router);\n * ```\n */\nexport function registerController(router: Router, ControllerClass: ControllerConstructor): void {\n const controllerMetadata = getControllerMetadata(ControllerClass);\n const routeMetadata = getRouteMetadata(ControllerClass);\n\n if (!controllerMetadata) {\n console.warn(`Controller ${ControllerClass.name} does not have @Controller decorator`);\n return;\n }\n\n // \uCEE8\uD2B8\uB864\uB7EC \uC778\uC2A4\uD134\uC2A4 \uC0DD\uC131\n const controllerInstance = new ControllerClass();\n\n // \uAC01 \uB77C\uC6B0\uD2B8\uC5D0 \uB300\uD574 \uB4F1\uB85D\n routeMetadata.forEach(route => {\n const fullPath = combinePaths(controllerMetadata.path, route.path);\n const method = route.method.toLowerCase() as keyof Router;\n \n // \uBA54\uC11C\uB4DC \uD578\uB4E4\uB7EC \uC0DD\uC131\n const handler = createMethodHandler(controllerInstance, route.propertyKey);\n \n // \uBBF8\uB4E4\uC6E8\uC5B4 \uBC30\uC5F4 \uC0DD\uC131 (\uCEE8\uD2B8\uB864\uB7EC \uBBF8\uB4E4\uC6E8\uC5B4 + \uB77C\uC6B0\uD2B8 \uBBF8\uB4E4\uC6E8\uC5B4 + \uD578\uB4E4\uB7EC)\n const middlewares = [\n ...controllerMetadata.middlewares,\n ...route.middlewares,\n handler\n ];\n\n // \uB77C\uC6B0\uD130\uC5D0 \uB4F1\uB85D\n if (typeof router[method] === 'function') {\n (router[method] as any)(fullPath, ...middlewares);\n }\n });\n}\n\n/**\n * \uBA54\uC11C\uB4DC \uD578\uB4E4\uB7EC\uB97C \uC0DD\uC131\uD569\uB2C8\uB2E4 - Request, Response, NextFunction\uC744 \uC9C1\uC811 \uC804\uB2EC\n */\nfunction createMethodHandler(controllerInstance: any, propertyKey: string | symbol) {\n return async (req: Request, res: Response, next: NextFunction) => {\n try {\n // \uBA54\uC11C\uB4DC \uC2E4\uD589 - req, res, next\uB97C \uC9C1\uC811 \uC804\uB2EC\n const result = await controllerInstance[propertyKey](req, res, next);\n\n // \uC751\uB2F5\uC774 \uC774\uBBF8 \uC804\uC1A1\uB418\uC5C8\uB2E4\uBA74 \uC544\uBB34\uAC83\uB3C4 \uD558\uC9C0 \uC54A\uC74C\n if (res.headersSent) {\n return;\n }\n\n // BaseResponse \uC778\uC2A4\uD134\uC2A4\uC778 \uACBD\uC6B0 send \uBA54\uC11C\uB4DC \uD638\uCD9C\n if (result instanceof BaseResponse) {\n result.send(res);\n return;\n }\n\n // \uACB0\uACFC\uAC00 \uC788\uB2E4\uBA74 JSON\uC73C\uB85C \uC751\uB2F5 (\uAE30\uC874 \uBC29\uC2DD \uC720\uC9C0)\n if (result !== undefined) {\n res.json(result);\n }\n } catch (error) {\n next(error);\n }\n };\n}\n\n/**\n * \uACBD\uB85C\uB97C \uACB0\uD569\uD569\uB2C8\uB2E4\n */\nfunction combinePaths(basePath: string, routePath: string): string {\n const combined = `${basePath}${routePath}`.replace(/\\/+/g, '/');\n return combined === '/' ? '/' : combined.replace(/\\/$/, '');\n}\n\n/**\n * Express \uC571\uC5D0 \uCEE8\uD2B8\uB864\uB7EC\uB4E4\uC744 \uB4F1\uB85D\uD558\uB294 \uD5EC\uD37C \uD568\uC218\n * \n * @param app - Express \uC571 \uC778\uC2A4\uD134\uC2A4\n * @param controllers - \uB4F1\uB85D\uD560 \uCEE8\uD2B8\uB864\uB7EC \uD074\uB798\uC2A4\uB4E4\n * @param basePath - \uAE30\uBCF8 \uACBD\uB85C (\uAE30\uBCF8\uAC12: '')\n * \n * @example\n * ```typescript\n * import express from 'express';\n * import { registerControllers } from 'ts5deco-express-controller';\n * import { UserController, PostController } from './controllers';\n * \n * const app = express();\n * registerControllers(app, [UserController, PostController], '/api');\n * ```\n */\nexport function registerControllers(\n app: any, \n controllers: ControllerConstructor[], \n basePath: string = ''\n): void {\n const router = createRouter(controllers);\n app.use(basePath, router);\n}\n", "import { Response } from 'express';\nimport { BaseResponse } from './BaseResponse';\n\n/**\n * Text response class for sending plain text with appropriate status codes\n * \n * @template TText - The type of the text data (defaults to string)\n * @template TStatus - The HTTP status code (defaults to number)\n * \n * @example\n * ```typescript\n * // Success text response with type safety\n * return new TextResponse<string, 200>(200, 'Operation completed successfully');\n * \n * // Health check with literal type\n * return new TextResponse<'OK', 200>(200, 'OK');\n * \n * // Error text with specific status\n * return new TextResponse<string, 400>(400, 'Bad Request');\n * \n * // Using without generics (still works)\n * return new TextResponse(200, 'Hello World');\n * ```\n */\nexport class TextResponse<TText extends string = string, TStatus extends number = number> extends BaseResponse {\n constructor(statusCode: TStatus, public readonly text?: TText) {\n super(statusCode);\n }\n\n send(res: Response): void {\n res.status(this.statusCode).send(this.text || '');\n }\n\n /**\n * Static convenience methods for common text responses\n */\n\n /**\n * 200 OK with text\n */\n static ok<TText extends string = string>(text?: TText): TextResponse<TText | 'OK', 200> {\n return new TextResponse<TText | 'OK', 200>(200, (text || 'OK') as TText | 'OK');\n }\n\n /**\n * 201 Created with text\n */\n static created<TText extends string = string>(text?: TText): TextResponse<TText | 'Created', 201> {\n return new TextResponse<TText | 'Created', 201>(201, (text || 'Created') as TText | 'Created');\n }\n\n /**\n * 400 Bad Request with text\n */\n static badRequest<TText extends string = string>(text?: TText): TextResponse<TText | 'Bad Request', 400> {\n return new TextResponse<TText | 'Bad Request', 400>(400, (text || 'Bad Request') as TText | 'Bad Request');\n }\n\n /**\n * 401 Unauthorized with text\n */\n static unauthorized<TText extends string = string>(text?: TText): TextResponse<TText | 'Unauthorized', 401> {\n return new TextResponse<TText | 'Unauthorized', 401>(401, (text || 'Unauthorized') as TText | 'Unauthorized');\n }\n\n /**\n * 403 Forbidden with text\n */\n static forbidden<TText extends string = string>(text?: TText): TextResponse<TText | 'Forbidden', 403> {\n return new TextResponse<TText | 'Forbidden', 403>(403, (text || 'Forbidden') as TText | 'Forbidden');\n }\n\n /**\n * 404 Not Found with text\n */\n static notFound<TText extends string = string>(text?: TText): TextResponse<TText | 'Not Found', 404> {\n return new TextResponse<TText | 'Not Found', 404>(404, (text || 'Not Found') as TText | 'Not Found');\n }\n\n /**\n * 500 Internal Server Error with text\n */\n static internalError<TText extends string = string>(text?: TText): TextResponse<TText | 'Internal Server Error', 500> {\n return new TextResponse<TText | 'Internal Server Error', 500>(500, (text || 'Internal Server Error') as TText | 'Internal Server Error');\n }\n}\n\n", "import { Response } from 'express';\nimport { BaseResponse } from './BaseResponse';\n\n/**\n * No content response class for 204 No Content responses\n * Typically used for successful DELETE operations or updates with no response body\n * \n * @example\n * ```typescript\n * // After successful delete\n * return new NoContentResponse();\n * \n * // After successful update with no response data\n * return new NoContentResponse();\n * ```\n */\nexport class NoContentResponse extends BaseResponse {\n constructor() {\n super(204);\n }\n\n send(res: Response): void {\n res.status(204).end();\n }\n}\n", "import { Response } from 'express';\nimport { BaseResponse } from './BaseResponse';\n\n/**\n * Redirect response class for HTTP redirects\n * \n * @example\n * ```typescript\n * // Temporary redirect (302)\n * return new RedirectResponse('/login');\n * \n * // Permanent redirect (301)\n * return new RedirectResponse('/new-path', true);\n * \n * // Custom status redirect\n * return new RedirectResponse('/path', 307);\n * ```\n */\nexport class RedirectResponse extends BaseResponse {\n public readonly url: string;\n\n constructor(url: string, permanent?: boolean);\n constructor(url: string, statusCode: number);\n constructor(\n url: string,\n permanentOrStatusCode: boolean | number = false\n ) {\n const statusCode = typeof permanentOrStatusCode === 'number' \n ? permanentOrStatusCode \n : permanentOrStatusCode ? 301 : 302;\n \n super(statusCode);\n this.url = url;\n }\n\n send(res: Response): void {\n res.status(this.statusCode).redirect(this.url);\n }\n}\n\n/**\n * Convenience methods for common redirect responses\n */\nexport class RedirectResponses {\n /**\n * 302 Found (temporary redirect)\n */\n static temporary(url: string): RedirectResponse {\n return new RedirectResponse(url, 302);\n }\n\n /**\n * 301 Moved Permanently (permanent redirect)\n */\n static permanent(url: string): RedirectResponse {\n return new RedirectResponse(url, 301);\n }\n\n /**\n * 307 Temporary Redirect (preserves method)\n */\n static temporaryPreserveMethod(url: string): RedirectResponse {\n return new RedirectResponse(url, 307);\n }\n\n /**\n * 308 Permanent Redirect (preserves method)\n */\n static permanentPreserveMethod(url: string): RedirectResponse {\n return new RedirectResponse(url, 308);\n }\n}\n", "import { Response } from 'express';\nimport { BaseResponse } from './BaseResponse';\n\n/**\n * File response class for sending files\n * \n * @example\n * ```typescript\n * // Send file with original name\n * return new FileResponse('/path/to/file.pdf');\n * \n * // Send file with custom download name\n * return new FileResponse('/path/to/document.pdf', 'report.pdf');\n * \n * // Send file as attachment (force download)\n * return new FileResponse('/path/to/file.zip', 'archive.zip', true);\n * ```\n */\nexport class FileResponse extends BaseResponse {\n constructor(\n public readonly filePath: string,\n public readonly filename?: string,\n public readonly asAttachment: boolean = false\n ) {\n super(200);\n }\n\n send(res: Response): void {\n if (this.asAttachment) {\n if (this.filename) {\n res.status(this.statusCode).download(this.filePath, this.filename);\n } else {\n res.status(this.statusCode).download(this.filePath);\n }\n } else if (this.filename) {\n res.status(this.statusCode).sendFile(this.filePath, { \n headers: { 'Content-Disposition': `inline; filename=\"${this.filename}\"` }\n });\n } else {\n res.status(this.statusCode).sendFile(this.filePath);\n }\n }\n}\n\n/**\n * Convenience methods for common file responses\n */\nexport class FileResponses {\n /**\n * Send file for inline viewing\n */\n static inline(filePath: string, filename?: string): FileResponse {\n return new FileResponse(filePath, filename, false);\n }\n\n /**\n * Send file as attachment (force download)\n */\n static attachment(filePath: string, filename?: string): FileResponse {\n return new FileResponse(filePath, filename, true);\n }\n}\n", "// Base response class\nexport { BaseResponse } from './BaseResponse';\n\n// Concrete response classes\nexport { JsonResponse } from './JsonResponse';\nexport { TextResponse } from './TextResponse';\nexport { NoContentResponse } from './NoContentResponse';\nexport { RedirectResponse, RedirectResponses } from './RedirectResponse';\nexport { FileResponse, FileResponses } from './FileResponse';\n\n// Import BaseResponse for the type guard function\nimport { BaseResponse } from './BaseResponse';\n\n// Type guard to check if a value is a BaseResponse\nexport function isBaseResponse(value: any): value is BaseResponse {\n return value instanceof BaseResponse;\n}\n", "/**\n * OpenAPI TypeScript \uD0C0\uC785 \uC0DD\uC131\uAE30\n * CLI\uC640 programmatic API\uB97C \uC704\uD55C \uD575\uC2EC \uB85C\uC9C1\n */\n\nimport fs from 'fs/promises';\nimport path from 'path';\nimport { execSync } from 'child_process';\n\nexport interface GenerateTypesOptions {\n /** OpenAPI \uC2A4\uD399 \uD30C\uC77C \uACBD\uB85C */\n input: string;\n /** \uC0DD\uC131\uB41C \uD0C0\uC785 \uD30C\uC77C \uCD9C\uB825 \uB514\uB809\uD1A0\uB9AC */\n outputDir: string;\n /** API \uD0C0\uC785 \uBCC4\uCE6D \uD30C\uC77C \uACBD\uB85C */\n apiTypesPath?: string;\n /** \uC720\uD2F8\uB9AC\uD2F0 \uD0C0\uC785 \uD30C\uC77C \uACBD\uB85C */\n utilsPath?: string;\n /** \uD0C0\uC785 \uBCC4\uCE6D \uC0DD\uC131 \uC5EC\uBD80 */\n generateAliases?: boolean;\n /** \uC720\uD2F8\uB9AC\uD2F0 \uD0C0\uC785 \uC0DD\uC131 \uC5EC\uBD80 */\n generateUtils?: boolean;\n /** \uCD94\uAC00 openapi-typescript \uC635\uC158 */\n openapiTypescriptOptions?: string[];\n}\n\n/**\n * OpenAPI \uC2A4\uD399\uC5D0\uC11C TypeScript \uD0C0\uC785\uC744 \uC0DD\uC131\uD569\uB2C8\uB2E4\n */\nexport async function generateTypes(options: GenerateTypesOptions): Promise<void> {\n const {\n input,\n outputDir,\n apiTypesPath,\n utilsPath,\n generateAliases = true,\n generateUtils = true,\n openapiTypescriptOptions = []\n } = options;\n\n // \uC785\uB825 \uD30C\uC77C \uC874\uC7AC \uD655\uC778\n try {\n await fs.access(input);\n } catch (error) {\n throw new Error(`OpenAPI specification file not found: ${input}`);\n }\n\n // \uCD9C\uB825 \uB514\uB809\uD1A0\uB9AC \uC0DD\uC131\n await fs.mkdir(outputDir, { recursive: true });\n\n const outputFile = path.join(outputDir, 'api.d.ts');\n\n // openapi-typescript \uC2E4\uD589\n console.log(`\uD83D\uDCC4 Processing ${input}...`);\n \n const openapiCmd = [\n 'npx openapi-typescript',\n `\"${input}\"`,\n '-o',\n `\"${outputFile}\"`,\n ...openapiTypescriptOptions\n ].join(' ');\n\n try {\n execSync(openapiCmd, { stdio: 'inherit' });\n console.log(`\u2705 Generated types: ${outputFile}`);\n } catch (error) {\n throw new Error(`Failed to generate types: ${error instanceof Error ? error.message : String(error)}`);\n }\n\n // \uC2A4\uD0A4\uB9C8 \uC774\uB984 \uCD94\uCD9C\n const schemaNames = await extractSchemaNames(outputFile);\n \n // \uC720\uD2F8\uB9AC\uD2F0 \uD0C0\uC785 \uC0DD\uC131\n if (generateUtils && utilsPath) {\n await generateUtilityTypes(utilsPath, outputFile);\n console.log(`\u2705 Generated utility types: ${utilsPath}`);\n }\n\n // API \uD0C0\uC785 \uBCC4\uCE6D \uC0DD\uC131\n if (generateAliases && apiTypesPath) {\n await generateApiTypes(apiTypesPath, schemaNames, utilsPath);\n console.log(`\u2705 Generated API type aliases: ${apiTypesPath}`);\n }\n}\n\n/**\n * \uC0DD\uC131\uB41C \uD0C0\uC785 \uD30C\uC77C\uC5D0\uC11C \uC2A4\uD0A4\uB9C8 \uC774\uB984\uB4E4\uC744 \uCD94\uCD9C\uD569\uB2C8\uB2E4\n */\nasync function extractSchemaNames(generatedFilePath: string): Promise<string[]> {\n try {\n const content = await fs.readFile(generatedFilePath, 'utf8');\n const schemaMatch = content.match(/schemas:\\s*{([^}]+)}/s);\n \n if (!schemaMatch || !schemaMatch[1]) {\n return [];\n }\n \n const schemas = schemaMatch[1];\n const schemaNames: string[] = [];\n const regex = /(\\w+):\\s*{/g;\n let match;\n \n while ((match = regex.exec(schemas)) !== null) {\n if (match[1]) {\n schemaNames.push(match[1]);\n }\n }\n \n return schemaNames;\n } catch (error) {\n console.warn('Warning: Could not extract schema names:', error instanceof Error ? error.message : String(error));\n return [];\n }\n}\n\n/**\n * \uC720\uD2F8\uB9AC\uD2F0 \uD0C0\uC785 \uD30C\uC77C\uC744 \uC0DD\uC131\uD569\uB2C8\uB2E4\n */\nasync function generateUtilityTypes(utilsPath: string,