Skip to content

Commit 00afec8

Browse files
committed
feat: 폴더명 패턴 및 상태 코드별 응답 지원
- 새로운 폴더명 패턴 지원: [한글명] 숫자 영문키 → 숫자 영문키 - 예시: [어드민] 7 Admin → 7 Admin 폴더 생성 - 기존 패턴(한글명 [EnglishKey])도 계속 지원 - 상태 코드별 응답 파싱 지원 - docs 블록에서 ## 200 OK, ## 404 Not Found 등 여러 상태 코드 지원 - 현재는 200 OK만 사용 (다른 상태 코드는 문서화 목적) - extractDomain 함수 수정 (3개 파일) - src/generator/index.ts - src/converter/openapiConverter.ts - src/diff/changeDetector.ts - extractJsonFromDocs 함수 개선 - 상태 코드별 응답 정규식 패턴 추가 - 200 OK 우선 추출 - 테스트 추가 - 새로운 폴더명 패턴 테스트 - 상태 코드별 응답 파싱 테스트 - 문서 업데이트 - docs/bruno-guide.md에 새로운 패턴 및 상태 코드 예시 추가 모든 테스트 통과 (10/10)
1 parent b5ea400 commit 00afec8

File tree

7 files changed

+227
-25
lines changed

7 files changed

+227
-25
lines changed

docs/bruno-guide.md

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ docs {
3939

4040
**`docs` 블록이 전부입니다!** 이 블록의 JSON으로 타입과 스키마가 자동 생성됩니다.
4141

42-
**올바른 예시:**
42+
**올바른 예시 (단일 응답):**
4343

4444
`````bru
4545
docs {
@@ -59,6 +59,32 @@ docs {
5959
}
6060
`````
6161

62+
**올바른 예시 (상태 코드별 응답):**
63+
64+
여러 상태 코드를 정의할 수 있지만, **200 OK만 사용**됩니다:
65+
66+
`````bru
67+
docs {
68+
## 200 OK
69+
```
70+
{
71+
"id": 1,
72+
"username": "johndoe",
73+
"email": "[email protected]"
74+
}
75+
```
76+
77+
## 404 Not Found
78+
```
79+
{
80+
"message": "사용자를 찾을 수 없습니다."
81+
}
82+
```
83+
}
84+
`````
85+
86+
**참고**: 현재는 200 OK 응답만 타입 생성에 사용됩니다. 다른 상태 코드(404, 500 등)는 문서화 목적으로만 작성할 수 있습니다.
87+
6288
### 2. JSON 작성 규칙
6389

6490
-**실제 응답과 동일하게** 작성
@@ -120,15 +146,26 @@ docs {
120146

121147
```
122148
bruno/
123-
├── 지원서 [applications]/ # 한글명 [영문키] 형식
149+
├── [어드민] 7 Admin/ # [한글명] 숫자 영문키 형식 → 7 Admin
124150
│ ├── get-list.bru
125151
│ └── create.bru
126-
├── 사용자 [users]/
152+
├── 지원서 [applications]/ # 한글명 [영문키] 형식 → applications
153+
│ ├── get-list.bru
154+
│ └── create.bru
155+
├── 사용자 [users]/ # 한글명 [영문키] 형식 → users
127156
│ └── get-profile.bru
128157
└── bruno.json
129158
```
130159

131-
**폴더명 규칙**: `한글명 [EnglishKey]` 형식으로 작성하면, 대괄호 안의 `EnglishKey`만 사용됩니다.
160+
**폴더명 규칙**:
161+
162+
1. **`[한글명] 숫자 영문키` 형식**: `[어드민] 7 Admin` → 생성 폴더: `7 Admin`
163+
- 대괄호 뒤의 모든 내용(숫자 + 영문키)이 폴더명이 됩니다
164+
- 예시: `[사용자] 8 Users``8 Users`
165+
166+
2. **`한글명 [EnglishKey]` 형식**: `지원서 [applications]` → 생성 폴더: `applications`
167+
- 대괄호 안의 `EnglishKey`만 사용됩니다
168+
- 기존 방식과 호환됩니다
132169

133170
## 실전 예시
134171

src/converter/openapiConverter.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -133,13 +133,21 @@ function extractDomain(filePath: string, brunoDir: string): string {
133133
const parts = rel.split('/');
134134
const folderName = parts[0] || 'default';
135135

136-
// [키] 패턴 추출
137-
const match = folderName.match(/\[([^\]]+)\]/);
138-
if (match) {
139-
return match[1]; // 대괄호 안의 키만 반환
136+
// [한글명] 숫자 영문키 패턴: [어드민] 7 Admin → 7 Admin
137+
const bracketPattern = /^\[[^\]]+\]\s+(.+)$/;
138+
const bracketMatch = folderName.match(bracketPattern);
139+
if (bracketMatch) {
140+
return bracketMatch[1].trim(); // 대괄호 뒤의 모든 내용
140141
}
141142

142-
return folderName; // 대괄호가 없으면 폴더명 그대로 반환
143+
// 기존 패턴: 한글명 [EnglishKey] → EnglishKey
144+
const oldPattern = /\[([^\]]+)\]/;
145+
const oldMatch = folderName.match(oldPattern);
146+
if (oldMatch) {
147+
return oldMatch[1];
148+
}
149+
150+
return folderName; // 패턴이 없으면 폴더명 그대로 반환
143151
}
144152

145153
/**

src/diff/changeDetector.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -190,10 +190,18 @@ function extractDomain(operation: any): string {
190190
if (operation.tags && operation.tags.length > 0) {
191191
const tag = operation.tags[0];
192192

193-
// [키] 패턴 추출
194-
const match = tag.match(/\[([^\]]+)\]/);
195-
if (match) {
196-
return match[1]; // 대괄호 안의 키만 반환
193+
// [한글명] 숫자 영문키 패턴: [어드민] 7 Admin → 7 Admin
194+
const bracketPattern = /^\[[^\]]+\]\s+(.+)$/;
195+
const bracketMatch = tag.match(bracketPattern);
196+
if (bracketMatch) {
197+
return bracketMatch[1].trim(); // 대괄호 뒤의 모든 내용
198+
}
199+
200+
// 기존 패턴: 한글명 [EnglishKey] → EnglishKey
201+
const oldPattern = /\[([^\]]+)\]/;
202+
const oldMatch = tag.match(oldPattern);
203+
if (oldMatch) {
204+
return oldMatch[1];
197205
}
198206

199207
return tag;

src/generator/index.ts

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -45,20 +45,29 @@ function findBrunoFiles(dir: string): string[] {
4545

4646
/**
4747
* 파일 경로에서 도메인 추출
48-
* "한글명 [EnglishKey]" 형식에서 EnglishKey만 추출
48+
* - "[한글명] 숫자 영문키" 형식: [어드민] 7 Admin → 7 Admin
49+
* - "한글명 [EnglishKey]" 형식: 사용자 [users] → users
4950
*/
5051
function extractDomain(filePath: string, brunoDir: string): string {
5152
const relativePath = relative(brunoDir, filePath);
5253
const parts = relativePath.split('/');
5354
const folderName = parts[0]; // 첫 번째 폴더가 도메인
5455

55-
// [키] 패턴 추출
56-
const match = folderName.match(/\[([^\]]+)\]/);
57-
if (match) {
58-
return match[1]; // 대괄호 안의 키만 반환
56+
// [한글명] 숫자 영문키 패턴: [어드민] 7 Admin → 7 Admin
57+
const bracketPattern = /^\[[^\]]+\]\s+(.+)$/;
58+
const bracketMatch = folderName.match(bracketPattern);
59+
if (bracketMatch) {
60+
return bracketMatch[1].trim(); // 대괄호 뒤의 모든 내용
5961
}
6062

61-
return folderName; // 대괄호가 없으면 폴더명 그대로 반환
63+
// 기존 패턴: 한글명 [EnglishKey] → EnglishKey
64+
const oldPattern = /\[([^\]]+)\]/;
65+
const oldMatch = folderName.match(oldPattern);
66+
if (oldMatch) {
67+
return oldMatch[1];
68+
}
69+
70+
return folderName; // 패턴이 없으면 폴더명 그대로 반환
6271
}
6372

6473
/**

src/parser/bruParser.ts

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -128,10 +128,8 @@ export function parseBrunoFile(filePath: string): ParsedBrunoFile {
128128
if (currentBlock === 'docs') {
129129
if (trimmed === '```json' || trimmed === '```') {
130130
inCodeBlock = !inCodeBlock;
131-
if (!inCodeBlock) {
132-
// 코드 블록 종료
133-
continue;
134-
}
131+
// 코드 블록 라인도 포함 (정규식 매칭을 위해)
132+
blockContent.push(line);
135133
continue;
136134
}
137135
}
@@ -230,15 +228,43 @@ function parseDocs(result: ParsedBrunoFile, lines: string[]): void {
230228

231229
/**
232230
* docs 블록에서 JSON 추출
231+
* 상태 코드별 응답 지원: ## 200 OK 형식
233232
*/
234233
export function extractJsonFromDocs(docs: string): any {
235234
try {
236-
// JSON 코드 블록 찾기
235+
// ## 200 OK 형식의 상태 코드별 응답 지원
236+
// 패턴: ## 200 OK (또는 ## 200) 다음에 빈 줄과 코드 블록
237+
const statusCodePattern = /##\s*(\d+)\s+[^\n]*(?:\n|$)(?:[^\n]*\n)*?\s*```(?:json)?\s*\n?([\s\S]*?)\n?\s*```/g;
238+
let match;
239+
let json200 = null;
240+
241+
// 모든 상태 코드 응답 찾기
242+
while ((match = statusCodePattern.exec(docs)) !== null) {
243+
const statusCode = parseInt(match[1]);
244+
const jsonContent = match[2].trim();
245+
246+
// 200 OK만 사용
247+
if (statusCode === 200) {
248+
try {
249+
json200 = JSON.parse(jsonContent);
250+
break; // 200 OK를 찾으면 중단
251+
} catch (e) {
252+
// JSON 파싱 실패시 무시
253+
}
254+
}
255+
}
256+
257+
// 200 OK를 찾지 못한 경우 기존 로직 사용
258+
if (json200) {
259+
return json200;
260+
}
261+
262+
// 기존 로직: 단일 JSON 코드 블록
237263
const jsonMatch = docs.match(/```json\s*([\s\S]*?)\s*```/);
238264
if (jsonMatch && jsonMatch[1]) {
239265
return JSON.parse(jsonMatch[1].trim());
240266
}
241-
267+
242268
// 일반 JSON 파싱 시도
243269
return JSON.parse(docs.trim());
244270
} catch (error) {

tests/cli.test.js

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,5 +213,62 @@ describe('변경사항 감지 테스트', () => {
213213
});
214214
});
215215

216+
describe('새로운 폴더명 패턴 테스트', () => {
217+
test('[한글명] 숫자 영문키 패턴 추출', () => {
218+
const inputDir = join(FIXTURES_DIR, 'bruno');
219+
const outputDir = join(TEST_OUTPUT_DIR, 'apis-pattern');
220+
221+
execSync(`node dist/cli/index.js generate-hooks -i ${inputDir} -o ${outputDir}`, {
222+
cwd: join(__dirname, '..'),
223+
});
224+
225+
// [어드민] 7 Admin 폴더가 생성되었는지 확인
226+
const adminDir = join(outputDir, '7 Admin');
227+
assert.ok(existsSync(adminDir), '[어드민] 7 Admin 폴더가 생성되어야 함');
228+
229+
// 훅 파일 생성 확인
230+
const hookFile = join(adminDir, 'get-getList.ts');
231+
assert.ok(existsSync(hookFile), 'getList 훅이 생성되어야 함');
232+
233+
console.log('✅ [한글명] 숫자 영문키 패턴 테스트 통과');
234+
});
235+
});
236+
237+
describe('상태 코드별 응답 파싱 테스트', () => {
238+
test('200 OK만 추출 (404 무시)', () => {
239+
const inputDir = join(FIXTURES_DIR, 'bruno');
240+
const outputFile = join(TEST_OUTPUT_DIR, 'openapi-status-codes.json');
241+
242+
execSync(`node dist/cli/index.js generate -i ${inputDir} -o ${outputFile}`, {
243+
cwd: join(__dirname, '..'),
244+
});
245+
246+
const spec = JSON.parse(readFileSync(outputFile, 'utf-8'));
247+
248+
// /mentors 엔드포인트 확인
249+
const mentorsPath = spec.paths['/mentors'];
250+
assert.ok(mentorsPath, '/mentors 엔드포인트가 있어야 함');
251+
252+
// 200 응답만 있는지 확인 (404는 무시되어야 함)
253+
const getMethod = mentorsPath.get;
254+
assert.ok(getMethod, 'GET 메서드가 있어야 함');
255+
assert.ok(getMethod.responses['200'], '200 응답이 있어야 함');
256+
assert.ok(!getMethod.responses['404'], '404 응답은 포함되지 않아야 함');
257+
258+
// 200 응답의 스키마 확인
259+
const response200 = getMethod.responses['200'];
260+
assert.ok(response200.content, 'content가 있어야 함');
261+
assert.ok(response200.content['application/json'], 'application/json이 있어야 함');
262+
assert.ok(response200.content['application/json'].schema, 'schema가 있어야 함');
263+
264+
const schema = response200.content['application/json'].schema;
265+
assert.ok(schema.properties, 'properties가 있어야 함');
266+
assert.ok(schema.properties.nextPageNumber, 'nextPageNumber 필드가 있어야 함');
267+
assert.ok(schema.properties.content, 'content 필드가 있어야 함');
268+
269+
console.log('✅ 상태 코드별 응답 파싱 테스트 통과');
270+
});
271+
});
272+
216273
console.log('\n🎉 모든 테스트 완료!');
217274

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
meta {
2+
name: 멘토 목록 조회
3+
type: http
4+
seq: 1
5+
}
6+
7+
get /mentors?region=미주권&size=3&page=1
8+
9+
headers {
10+
Authorization: Bearer {{token}}
11+
}
12+
13+
docs {
14+
RequestParam중, region에 올 수 있는 것들
15+
- 미주권
16+
- 아시아권
17+
- 중국권
18+
- 유럽권
19+
20+
---
21+
22+
## 200 OK
23+
```
24+
{
25+
"nextPageNumber": 1,
26+
"content": [
27+
{
28+
"id": 1,
29+
"profileImageUrl": "https://example.com/image.jpg",
30+
"nickname": "닉네임",
31+
"country": "프랑스",
32+
"universityName": "파리 대학교",
33+
"term": "2025-1",
34+
"menteeCount": 7,
35+
"hasBadge": true,
36+
"introduction": "안녕하세요",
37+
"channels": [
38+
{
39+
"type": "BLOG",
40+
"url": "https://blog.example.com"
41+
}
42+
],
43+
"isApplied": true
44+
}
45+
]
46+
}
47+
```
48+
49+
## 404 Not Found
50+
51+
```
52+
{
53+
"message": "이름에 해당하는 지역을 찾을 수 없습니다."
54+
}
55+
```
56+
}
57+

0 commit comments

Comments
 (0)