1- import { createFileRoute , redirect } from "@tanstack/react-router" ;
1+ import { createFileRoute } from "@tanstack/react-router" ;
22import { Copy , Play , RotateCcw } from "lucide-react" ;
33import { useMemo , useState } from "react" ;
44import { toast } from "sonner" ;
@@ -11,10 +11,8 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
1111import { Textarea } from "@/components/ui/textarea" ;
1212import { axiosInstance } from "@/lib/api/client" ;
1313import { cn } from "@/lib/utils" ;
14- import { isTokenExpired } from "@/lib/utils/jwtUtils" ;
15- import { loadAccessToken } from "@/lib/utils/localStorage" ;
1614
17- type DefinitionMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE" ;
15+ type DefinitionMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD" | "OPTIONS" | "TRACE" | "CONNECT" ;
1816
1917interface ApiDefinitionEntry {
2018 method : DefinitionMethod ;
@@ -25,6 +23,10 @@ interface ApiDefinitionEntry {
2523 response : unknown ;
2624}
2725
26+ interface ApiDefinitionModule {
27+ [ key : string ] : unknown ;
28+ }
29+
2830interface EndpointItem {
2931 domain : string ;
3032 name : string ;
@@ -38,48 +40,64 @@ interface RequestResult {
3840 body : unknown ;
3941}
4042
41- const definitionFileContents = import . meta. glob ( "../../../../../packages/api-schema/src/apis/*/apiDefinitions.ts" , {
42- eager : true ,
43- query : "?raw" ,
44- import : "default" ,
45- } ) as Record < string , string > ;
43+ const definitionModules = import . meta. glob (
44+ "../../../../../packages/api-client/src/generated/apis/*/apiDefinitions.ts" ,
45+ {
46+ eager : true ,
47+ import : "*" ,
48+ } ,
49+ ) as Record < string , ApiDefinitionModule > ;
4650
4751const normalizeTokenKey = ( value : string ) => value . replace ( / [ ^ a - z A - Z 0 - 9 ] / g, "" ) . toLowerCase ( ) ;
4852
53+ const isDefinitionMethod = ( value : unknown ) : value is DefinitionMethod => {
54+ if ( typeof value !== "string" ) {
55+ return false ;
56+ }
57+
58+ return [ "GET" , "POST" , "PUT" , "PATCH" , "DELETE" , "HEAD" , "OPTIONS" , "TRACE" , "CONNECT" ] . includes ( value ) ;
59+ } ;
60+
61+ const isApiDefinitionEntry = ( value : unknown ) : value is ApiDefinitionEntry => {
62+ if ( typeof value !== "object" || value === null ) {
63+ return false ;
64+ }
65+
66+ const candidate = value as Partial < ApiDefinitionEntry > ;
67+ return isDefinitionMethod ( candidate . method ) && typeof candidate . path === "string" ;
68+ } ;
69+
4970const parseDefinitionRegistry = ( ) : EndpointItem [ ] => {
5071 const endpoints : EndpointItem [ ] = [ ] ;
5172
52- for ( const [ modulePath , fileContent ] of Object . entries ( definitionFileContents ) ) {
73+ for ( const [ modulePath , module ] of Object . entries ( definitionModules ) ) {
5374 const domainMatch = modulePath . match ( / a p i s \/ ( [ ^ / ] + ) \/ a p i D e f i n i t i o n s \. t s $ / ) ;
5475 if ( ! domainMatch ) {
5576 continue ;
5677 }
5778
5879 const domain = domainMatch [ 1 ] ;
59- const endpointPattern =
60- / ^ \s * ( [ ^ : \n ] + ) : \s * \{ \s * \n \s * m e t h o d : \s * ' ( [ A - Z ] + ) ' \s + a s c o n s t , \s * \n \s * p a t h : \s * ' ( [ ^ ' ] + ) ' \s + a s c o n s t , / gm;
6180
62- for ( const match of fileContent . matchAll ( endpointPattern ) ) {
63- const endpointName = match [ 1 ] ?. trim ( ) ;
64- const method = match [ 2 ] ?. trim ( ) as DefinitionMethod | undefined ;
65- const path = match [ 3 ] ?. trim ( ) ;
81+ for ( const [ exportName , exportValue ] of Object . entries ( module ) ) {
82+ if ( ! exportName . endsWith ( "ApiDefinitions" ) ) {
83+ continue ;
84+ }
6685
67- if ( ! endpointName || ! method || ! path ) {
86+ if ( typeof exportValue !== "object" || exportValue === null ) {
6887 continue ;
6988 }
7089
71- endpoints . push ( {
72- domain,
73- name : endpointName ,
74- definition : {
75- method,
76- path,
77- pathParams : { } ,
78- queryParams : { } ,
79- body : { } ,
80- response : { } ,
81- } ,
82- } ) ;
90+ for ( const [ endpointName , endpointDefinition ] of Object . entries ( exportValue ) ) {
91+ if ( ! isApiDefinitionEntry ( endpointDefinition ) ) {
92+ continue ;
93+ }
94+
95+ endpoints . push ( {
96+ domain,
97+ name : endpointName ,
98+ definition : endpointDefinition ,
99+ } ) ;
100+ }
83101 }
84102 }
85103
@@ -95,12 +113,24 @@ const ALL_ENDPOINTS = parseDefinitionRegistry();
95113
96114const toPrettyJson = ( value : unknown ) => JSON . stringify ( value , null , 2 ) ;
97115
98- const parseJsonRecord = ( text : string , label : string ) : Record < string , unknown > => {
116+ const parseJsonValue = ( text : string , label : string ) : unknown => {
99117 if ( ! text . trim ( ) ) {
118+ return undefined ;
119+ }
120+
121+ try {
122+ return JSON . parse ( text ) as unknown ;
123+ } catch {
124+ throw new Error ( `${ label } 는 유효한 JSON이어야 합니다.` ) ;
125+ }
126+ } ;
127+
128+ const parseJsonRecord = ( text : string , label : string ) : Record < string , unknown > => {
129+ const parsed = parseJsonValue ( text , label ) ;
130+ if ( parsed === undefined ) {
100131 return { } ;
101132 }
102133
103- const parsed = JSON . parse ( text ) as unknown ;
104134 if ( typeof parsed !== "object" || parsed === null || Array . isArray ( parsed ) ) {
105135 throw new Error ( `${ label } 는 JSON 객체여야 합니다.` ) ;
106136 }
@@ -115,10 +145,10 @@ const toStringRecord = (value: Record<string, unknown>): Record<string, string>
115145const resolvePath = ( rawPath : string , pathParams : Record < string , unknown > ) => {
116146 const withoutBaseToken = rawPath . replace ( "{{URL}}" , "" ) ;
117147
118- return withoutBaseToken . replace ( / \{ \{ ( [ ^ } ] + ) \} \} / g , ( _full , tokenName : string ) => {
148+ const findPathParam = ( tokenName : string ) : unknown => {
119149 const exact = pathParams [ tokenName ] ;
120150 if ( exact !== undefined && exact !== null ) {
121- return encodeURIComponent ( String ( exact ) ) ;
151+ return exact ;
122152 }
123153
124154 const normalizedToken = normalizeTokenKey ( tokenName ) ;
@@ -127,22 +157,30 @@ const resolvePath = (rawPath: string, pathParams: Record<string, unknown>) => {
127157 ) ;
128158
129159 if ( similarEntry ) {
130- return encodeURIComponent ( String ( similarEntry [ 1 ] ) ) ;
160+ return similarEntry [ 1 ] ;
131161 }
132162
133163 throw new Error ( `경로 파라미터 '${ tokenName } ' 값이 필요합니다.` ) ;
164+ } ;
165+
166+ let resolved = withoutBaseToken ;
167+
168+ resolved = resolved . replace ( / \{ \{ ( [ ^ } ] + ) \} \} / g, ( _full , tokenName : string ) => {
169+ return encodeURIComponent ( String ( findPathParam ( tokenName ) ) ) ;
134170 } ) ;
171+
172+ resolved = resolved . replace ( / : ( [ a - z A - Z 0 - 9 _ - ] + ) / g, ( _full , tokenName : string ) => {
173+ return encodeURIComponent ( String ( findPathParam ( tokenName ) ) ) ;
174+ } ) ;
175+
176+ resolved = resolved . replace ( / \{ ( [ a - z A - Z 0 - 9 _ - ] + ) \} / g, ( _full , tokenName : string ) => {
177+ return encodeURIComponent ( String ( findPathParam ( tokenName ) ) ) ;
178+ } ) ;
179+
180+ return resolved ;
135181} ;
136182
137183export const Route = createFileRoute ( "/bruno/" ) ( {
138- beforeLoad : ( ) => {
139- if ( typeof window !== "undefined" ) {
140- const token = loadAccessToken ( ) ;
141- if ( ! token || isTokenExpired ( token ) ) {
142- throw redirect ( { to : "/auth/login" } ) ;
143- }
144- }
145- } ,
146184 component : BrunoApiPage ,
147185} ) ;
148186
@@ -204,7 +242,7 @@ function BrunoApiPage() {
204242 const pathParams = parseJsonRecord ( pathParamsText , "Path Params" ) ;
205243 const queryParams = parseJsonRecord ( queryParamsText , "Query Params" ) ;
206244 const headers = toStringRecord ( parseJsonRecord ( headersText , "Headers" ) ) ;
207- const body = parseJsonRecord ( bodyText , "Body" ) ;
245+ const body = parseJsonValue ( bodyText , "Body" ) ;
208246
209247 const path = resolvePath ( selectedEndpoint . definition . path , pathParams ) ;
210248 const startedAt = performance . now ( ) ;
@@ -213,7 +251,10 @@ function BrunoApiPage() {
213251 url : path ,
214252 method : selectedEndpoint . definition . method ,
215253 params : queryParams ,
216- data : selectedEndpoint . definition . method === "GET" ? undefined : body ,
254+ data :
255+ selectedEndpoint . definition . method === "GET" || selectedEndpoint . definition . method === "HEAD"
256+ ? undefined
257+ : body ,
217258 headers,
218259 validateStatus : ( ) => true ,
219260 } ) ;
0 commit comments