bruno-api-typescript 내장 및 API 스키마 동기화 체계화#415
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Walkthrough이 pull request는 대학교 선택 페이지를 새로 구축하고, Bruno API 자동 생성 도구를 도입하며, API 스키마 동기화 인프라를 추가합니다. 대학교별 필터링 및 검색 기능을 갖춘 새로운 라우팅 구조( Estimated code review effort🎯 5 (Critical) | ⏱️ ~90+ minutes Suggested reviewers
🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
- Add HomeUniversity enum (인하대학교, 인천대학교, 성신여자대학교) - Add HomeUniversitySlug type and slug mappings (inha, incheon, sungshin) - Create university selection onboarding page at /university - Add [homeUniversity] dynamic routes for filtered university lists - Add [homeUniversity]/[id] detail page with context-aware navigation - Add [homeUniversity]/search page with filters - Add backHref prop to TopDetailNavigation component - Add linkPrefix prop to UniversityCard and UniversityCards components - ISR enabled with 1 hour revalidation
- Remove conflicting /university/[id] route - Move UniversityDetail component to [homeUniversity]/[id]/_ui/ - Fix dynamic route slug conflict between [id] and [homeUniversity]
- Rename export from getUnconfirmedMentoringCount to useGetUnconfirmedMentoringCount - Add enabled parameter to control query execution - Fix MentorApplyCountContent to use correct hook name
- Add inha.png, incheon.png, sungshin.jpg to public/images/univs/ - Update HOME_UNIVERSITY_LIST logoUrl to use new image paths
db1773c to
1218cd8
Compare
There was a problem hiding this comment.
Actionable comments posted: 19
Note
Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
apps/web/src/app/university/list/[homeUniversityName]/page.tsx (1)
1-1:⚠️ Potential issue | 🟡 MinorBiome 포맷팅 CI 실패가 이 파일에서도 발생하고 있습니다.
npx biome format --write로 수정해 주세요.apps/web/src/app/university/[homeUniversity]/[id]/page.tsx (1)
135-151:⚠️ Potential issue | 🟡 MinorJSON-LD 구조화 데이터에 XSS 위험이 있습니다.
JSON.stringify(structuredData)를dangerouslySetInnerHTML로 삽입할 때, 서버에서 가져온 데이터(koreanName,englishName,backgroundImageUrl등)에</script>문자열이 포함되면 스크립트 태그를 탈출하여 XSS 공격이 가능합니다.데이터가 서버 제어하에 있으므로 현실적 위험은 낮지만, 방어적으로 처리하는 것이 좋습니다.
🛡️ 안전한 직렬화 제안
</시퀀스를 이스케이프하면 됩니다:- <script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(structuredData) }} /> + <script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(structuredData).replace(/</g, "\\u003c") }} />
🤖 Fix all issues with AI agents
In `@packages/bruno-api-typescript/.github/workflows/api-review.yml`:
- Around line 135-147: The step that calls github.rest.pulls.requestReviewers
currently passes an empty reviewers array (reviewers: []), which will cause a
422 from the GitHub API; update the actions/github-script block so it only calls
github.rest.pulls.requestReviewers when the reviewers list is non-empty (e.g.,
compute a reviewers variable and guard with if (reviewers && reviewers.length >
0) before calling github.rest.pulls.requestReviewers) or temporarily comment
out/remove the requestReviewers call until actual reviewer usernames are
supplied; ensure you modify the script that invokes
github.rest.pulls.requestReviewers and the reviewers property accordingly.
- Around line 88-133: The script directly injects the step output into a JS
template literal via `${{ steps.bruno_changes.outputs.changed_files }}`
(assigned to changedFiles) which allows injection if filenames contain backticks
or `${}`; instead pass that output into the script via an environment variable
(e.g., set env: CHANGED_FILES: ${{ steps.bruno_changes.outputs.changed_files }}
on the github-script step) and inside the script read it as const changedFiles =
process.env.CHANGED_FILES || ''; also perform a simple sanitize/escape (e.g.,
strip or escape backticks and `${`) before embedding into comment to fully
mitigate template injection; update references to changedFiles accordingly.
- Around line 13-21: Update the GitHub Actions steps that currently reference
actions/checkout@v3 and actions/setup-node@v3 to the recommended v6 releases:
replace actions/checkout@v3 with actions/checkout@v6 and actions/setup-node@v3
with actions/setup-node@v6 (use the latest v6 tags such as 6.0.2 and 6.2.0 where
applicable); also scan the workflow for any uses of actions/github-script and
consider upgrading it to v8 if present to align with 2026 recommendations.
In
`@packages/bruno-api-typescript/.github/workflows/bruno-repo-notify-frontend.yml.template`:
- Around line 22-36: 템플릿에서 commit_message 등 값을 JSON에 직접 보간하는 대신 jq로 안전하게 이스케이프된
JSON을 생성하도록 변경하세요: bruno-repo-notify-frontend.yml.template의 curl 호출을 수정해 직접 '-d
'{...}' 대신 jq -n --arg bruno_repo "${{ github.repository }}" --arg commit_sha
"${{ github.sha }}" --arg commit_message "${{ github.event.head_commit.message
}}" --arg author "${{ github.event.head_commit.author.name }}" --arg timestamp
"${{ github.event.head_commit.timestamp }}" '{event_type:"bruno_updated",
client_payload:{bruno_repo:$bruno_repo, commit_sha:$commit_sha,
commit_message:$commit_message, author:$author, timestamp:$timestamp}}' | curl
-X POST ... -d `@-` 형태로 파이프하여 commit_message, author, timestamp 등의 값을 안전하게 이스케이프된
JSON 필드로 전달하도록 하세요.
In
`@packages/bruno-api-typescript/.github/workflows/frontend-sync-bruno.yml.template`:
- Around line 53-54: The workflow's "Install Dependencies" step currently runs
only "npm install" but misses installing the bruno-sync CLI package required by
the later "npx bruno-sync generate" command; add an extra install command (e.g.,
run "npm install -D github:manNomi/bruno-api-typescript" or otherwise install
the bruno-sync package) in the job before the "npx bruno-sync generate"
invocation so the bruno-sync CLI is available at runtime.
- Around line 32-43: Get Bruno Repository Info step currently injects
github.event.client_payload fields directly into the run script (e.g.,
github.event.client_payload.commit_message) which risks shell injection; change
the step to stop direct expression interpolation and instead serialize the
payload with toJson (e.g., use toJson(github.event.client_payload)) and parse it
safely inside the run script with a JSON parser (jq) to extract repo, sha, and
message, and likewise avoid writing unescaped values to $GITHUB_STEP_SUMMARY by
writing the parsed values via a safe printf/echo using shell variables (not
direct ${{ }} expansion); update the "Get Bruno Repository Info" step and the
code that writes to $GITHUB_STEP_SUMMARY to follow this pattern.
In `@packages/bruno-api-typescript/README.md`:
- Line 26: Update the README.md to replace the non-existent clone URL with the
solid-connect monorepo repo URL, state that this package lives at
packages/bruno-api-typescript within the monorepo, and adjust setup instructions
to show cloning the monorepo followed by monorepo-specific steps: install pnpm
(if needed), run pnpm install to bootstrap workspaces, and include turbo
build/dev/test commands (e.g., turbo run build / dev) that apply to the
workspace; also add a brief note on how to run only this package (pnpm --filter
bruno-api-typescript... or turbo run --filter) so readers understand the package
context and workspace workflow.
In `@packages/bruno-api-typescript/scripts/setup-cross-repo.sh`:
- Around line 59-72: The generated workflow inserts
github.event.head_commit.message directly into the JSON body for the curl
dispatch (commit_message), which can break if the commit message contains quotes
or newlines; update the script in setup-cross-repo.sh so the commit message is
JSON-escaped before embedding (e.g., use GitHub Actions toJSON() on
github.event.head_commit.message or generate the payload via github-script and
pass a safe string) and ensure the Authorization/dispatch curl -d payload uses
that escaped value for the "commit_message" field.
In `@packages/bruno-api-typescript/src/cli/index.ts`:
- Around line 95-101: The current CI-fail logic mixes the display filter flag
options.breakingOnly with exit behavior; change this by introducing a separate
boolean flag (e.g., options.failOnBreaking / CLI flag --fail-on-breaking) and
use that flag for the exit logic instead of options.breakingOnly, leaving
options.breakingOnly solely to control output filtering; update the conditional
that currently checks report.summary.breaking > 0 && options.breakingOnly to
check report.summary.breaking > 0 && options.failOnBreaking (and wire up the new
CLI flag parsing where options are created), ensuring process.exit(1) is only
triggered when the new fail-on-breaking option is set.
In `@packages/bruno-api-typescript/src/converter/schemaBuilder.ts`:
- Around line 19-22: The current inferSchema function returns { type: 'null' }
which is invalid for OpenAPI 3.0; update the null branch in inferSchema to avoid
type:'null' and instead return an OpenAPISchema that uses nullable (e.g., return
{ nullable: true } or an empty schema with nullable: true) so it is compatible
with OpenAPI 3.0; adjust any callers expecting type:'null' accordingly and
ensure the OpenAPISchema type annotations reflect that nullable-only shape
rather than a 'null' type.
In `@packages/bruno-api-typescript/src/diff/changeDetector.ts`:
- Around line 307-325: The code records a 'type-changed' entry but still falls
through into the recursive comparison, causing duplicate/incorrect "removed"
reports; update the logic in compareObjectSchemas so that the recursive compare
for nested schemas only runs when the types are equal and are 'object' or
'array' (i.e., guard the nested-call with a condition that newProp.type ===
oldProp.type && (oldProp.type === 'object' || oldProp.type === 'array') or
simply continue/return immediately after pushing the 'type-changed' change),
referencing the existing symbols oldProp, newProp, compareObjectSchemas, and the
changes array.
In `@packages/bruno-api-typescript/src/generator/apiClientGenerator.ts`:
- Around line 46-47: The current hasParams check (const hasParams =
http.url.includes(':') || http.url.includes('{')) incorrectly matches Bruno
template variables like {{URL}}; update the detection to only match real path
parameters by using targeted regexes instead of includes: keep the :param
pattern with /:\w+/ and detect single-brace params with /{[^{}]+}/ so
double-brace templates are ignored (i.e., replace the hasParams calculation in
apiClientGenerator.ts with something like testing http.url against /:\w+/ or
/{[^{}]+}/).
In `@packages/bruno-api-typescript/src/generator/apiDefinitionGenerator.ts`:
- Around line 118-129: The generated line `response: {} as ${meta.response}`
produces invalid TS when meta.response === 'void'; update the generator in
apiDefinitionGenerator.ts so that when generateApiDefinitionMeta returns 'void'
you emit a safe empty-object type instead (e.g., `Record<string, never>`) for
the response field instead of `void`. Locate the loop that writes `response: {}
as ${meta.response}` and change the emitted RHS to use `Record<string, never>`
when `meta.response === 'void'` (or alternatively adjust
generateApiDefinitionMeta to return `Record<string, never>` instead of `void`)
so the produced code compiles.
In `@packages/bruno-api-typescript/src/generator/apiFactoryGenerator.ts`:
- Around line 39-55: The loop using brunoVarPattern.exec(processedUrl) modifies
processedUrl while iterating which breaks the regex internal lastIndex and can
skip matches; fix by first collecting all matches from processedUrl into an
array (using matchAll or repetitive exec on the original string) and building
the list of variable names (updating urlParams, paramsList, and
processedBrunoVars) from that collection, then perform replacements on
processedUrl afterwards (either by iterating matches in reverse order or by
using a single replace with a callback) so brunoVarPattern, processedUrl,
urlParams, paramsList and processedBrunoVars are updated safely without mutating
the input during iteration.
In `@packages/bruno-api-typescript/src/generator/index.ts`:
- Around line 118-137: parsedChangedFiles and allParsedFiles both call
parseBrunoFile (and extractDomain) causing duplicate parsing which is repeated
again later when updating hashes; create a Map (e.g. parsedFileMap) keyed by
filePath that stores {parsed, domain} when you first call parseBrunoFile (use it
when building parsedChangedFiles), then build allParsedFiles by looking up
filePath in parsedFileMap and calling parseBrunoFile only for files not yet
cached, and finally reuse parsedFileMap inside the hash update loop instead of
calling parseBrunoFile(filePath) again so every Bruno file is parsed at most
once.
- Around line 243-246: The MSW handler generation is currently using only
parsedChangedFiles which yields incomplete domain index files; instead call
generateMSWHandlers with the full set of parsed files (allParsedFiles) or merge
parsedChangedFiles into allParsedFiles before invoking it so the MSW domain
index (index.ts) includes every handler; update the call at the msw generation
site to await generateMSWHandlers(allParsedFiles, mswOutputDir) (or equivalent)
and ensure generateMSWHandlers expects/handles the full parsed file list.
- Around line 206-231: The index generator is manually constructing a PascalCase
name for definitionsTypeName using string ops instead of the shared toPascalCase
helper; export the existing toPascalCase from typeGenerator.ts and import it
into this file (packages/bruno-api-typescript/src/generator/index.ts) and
replace the inline PascalCase logic that builds definitionsTypeName (and any
similar ad-hoc conversions) with a call to toPascalCase(domain) to remove
duplication and match apiDefinitionGenerator.ts behavior.
In `@packages/bruno-api-typescript/src/generator/mswGenerator.ts`:
- Around line 99-106: normalizeUrl is currently a no-op leaving query strings in
MSW route patterns and using :param form; update normalizeUrl to strip any query
string (remove everything from '?' onward) and convert path parameters from
:param to {param} (e.g. use a regex like /:([A-Za-z0-9_]+)/g → '{$1}') so MSW
handlers match correctly; implement this change inside the normalizeUrl function
in mswGenerator.ts, ensuring existing {param} tokens are preserved and only the
query portion is removed.
In `@packages/bruno-api-typescript/src/parser/bruParser.ts`:
- Around line 125-132: The parser currently treats any line with trimmed === '}'
as end-of-block (checked alongside currentBlock and inCodeBlock), which causes
body:json blocks to terminate on nested JSON braces; to fix, add brace-depth
tracking for JSON-style blocks: when you set currentBlock to a body:json (where
parseBlock and blockContent are used), initialize a braceDepth counter (e.g.,
jsonBraceDepth = 0) and then, while accumulating blockContent, increment on each
'{' and decrement on each '}' (taking care to ignore braces inside code blocks
or strings if already handled elsewhere); change the block-termination check to
only end the body:json block when braceDepth reaches zero (instead of any
trimmed === '}'), and ensure parseBlock is called only after the full matching
brace is seen.
🟡 Minor comments (24)
packages/bruno-api-typescript/agents.md-86-87 (1)
86-87:⚠️ Potential issue | 🟡 Minor중복된 항목을 제거해주세요.
"Mock Service Worker (MSW)" 항목이 연속으로 두 번 나열되어 있습니다. 하나만 남겨주세요.
🔧 중복 제거 제안
* **OpenAPI (Swagger):** Standard for API documentation and client generation. * **Mock Service Worker (MSW):** API mocking library. -* **Mock Service Worker (MSW):** API mocking library. * **GitHub Apps / GitHub Actions:** For automation and integration with repositories.packages/bruno-api-typescript/README.md-70-70 (1)
70-70:⚠️ Potential issue | 🟡 Minor중첩된 코드 블록에 언어 지정자 추가 필요
docs블록 내부의 JSON 코드 펜스(line 70)에 언어 지정자가 누락되어 markdownlint 경고가 발생하고 있습니다. 문법 하이라이팅과 린팅 통과를 위해json지정자를 추가해주세요.📝 제안하는 수정사항
docs { - ```` + ````json { "id": 1, "username": "johndoe",apps/web/src/app/university/[homeUniversity]/search/_ui/SearchPageContent.tsx-1-17 (1)
1-17:⚠️ Potential issue | 🟡 MinorCI 파이프라인에서 import 정렬 오류가 발생했습니다.
Biome가 import 순서 문제를 보고하고 있습니다. CI를 통과시키려면 Biome의
Organize Imports규칙에 맞게 import 순서를 정리해 주세요.apps/web/src/app/university/(home)/_ui/HomeUniversityCard.tsx-1-1 (1)
1-1:⚠️ Potential issue | 🟡 MinorBiome 포맷팅 CI 실패가 발생하고 있습니다.
파이프라인 로그에서 이 파일에 대해 Biome 포맷팅 체크 실패가 보고되었습니다.
npx biome format --write실행으로 포맷을 맞춰주세요.apps/web/src/app/university/[homeUniversity]/search/page.tsx-1-1 (1)
1-1:⚠️ Potential issue | 🟡 MinorBiome 포맷팅 CI 실패가 이 파일에서도 발생하고 있습니다.
npx biome format --write로 수정해 주세요.apps/web/src/app/university/(home)/_ui/HomeUniversityCard.tsx-28-33 (1)
28-33:⚠️ Potential issue | 🟡 Minor
onError핸들러의 주석과 실제 동작이 불일치합니다.
- 주석에는 "이미지 로드 실패 시 기본 텍스트 표시"라고 되어 있지만, 실제로는 이미지를
display: "none"으로 숨기기만 합니다.- 대체 텍스트(예: 대학 이니셜)가 표시되지 않아 빈 원형 영역만 남게 됩니다.
현재 동작이 의도된 것이라면 주석을 수정해 주세요. 대체 UI가 필요하다면 state 기반 fallback 렌더링을 고려해 보세요.
packages/bruno-api-typescript/tests/fixtures/bruno-empty-object/empty-response.bru-1-12 (1)
1-12:⚠️ Potential issue | 🟡 Minor두 fixture 파일이 완벽히 동일한 내용으로 중복되고 있습니다.
확인해보니 다음 두 파일이 바이트 단위까지 완전히 같은 내용을 담고 있어요:
packages/bruno-api-typescript/tests/fixtures/bruno-empty-object/empty-response.brupackages/bruno-api-typescript/tests/fixtures/bruno-empty-object/test/empty-response.bru두 파일 모두 동일한 구조로
Empty Response Test라는 이름의 GET/test/empty엔드포인트와 빈 JSON 객체를 정의하고 있습니다.의도적으로 디렉토리 깊이별 파싱을 테스트하는 상황이 아니라면, 하위 디렉토리의
test/empty-response.bru파일을 제거하는 것을 권장합니다. 😊apps/web/package.json-47-47 (1)
47-47:⚠️ Potential issue | 🟡 MinorZod의 최소 버전이
^4.0.5에서^4.0.0으로 낮아졌으며, 이로 인해 중요한 수정사항들이 제외됩니다.다음과 같은 우려사항들이 있습니다:
버전 범위 확대로 인한 위험
^4.0.5는>=4.0.5 <5.0.0범위를 지정하는 반면,^4.0.0은>=4.0.0 <5.0.0범위를 허용합니다.- 즉,
4.0.0 ~ 4.0.4의 이전 패치 버전도 설치될 수 있게 됩니다.생략되는 주요 수정사항
- v4.0.4 (중요):
zod-to-json-schema의 모듈 해석 실패를 수정합니다. 이 버전이 없으면 관련 기능에서 문제가 발생할 수 있습니다.- v4.0.5: discriminated union에서 pipe 지원을 추가합니다.
권장사항
- 특별한 이유가 없다면
^4.0.5유지를 권장합니다. 혹시 더 낮은 버전이 필요한 특정 사유가 있다면 그 이유를 공유해 주세요.packages/bruno-api-typescript/docs/usage-guide.md-12-14 (1)
12-14:⚠️ Potential issue | 🟡 Minor문서 내 Node.js 버전 및 패키지 매니저 불일치
- Node.js 버전: 루트
package.json의engines.node는"22.x"인데, 여기서는"Node.js 18+"로 안내하고 있어요.- 패키지 매니저: 이 프로젝트는
pnpm을 사용하는데,npm install및npm run build로 안내되어 있습니다.🔧 제안
-1. **Node.js 18+** +1. **Node.js 22.x** 2. Bruno `.bru` 파일이 있는 디렉토리 -3. `npm install` 및 `npm run build` 완료 +3. `pnpm install` 및 `pnpm run build` 완료As per coding guidelines,
{package.json,pnpm-lock.yaml,**/package.json}: "Usepnpmas the package manager for dependency management" 및package.json: "Use Node.js 22.x".packages/bruno-api-typescript/docs/usage-guide.md-93-101 (1)
93-101:⚠️ Potential issue | 🟡 Minor예시 스크립트에서도
npm→pnpm통일 필요
package.json스크립트 예시 내npm run명령어들도pnpm run으로 맞춰주세요. 프로젝트 전반의 일관성을 위해서요. 😊🔧 제안
- "build": "npm run api:generate && npm run api:clients && tsc", - "dev": "npm run api:generate && npm run api:clients && tsc --watch" + "build": "pnpm run api:generate && pnpm run api:clients && tsc", + "dev": "pnpm run api:generate && pnpm run api:clients && tsc --watch"As per coding guidelines, "Use
pnpmas the package manager for dependency management".packages/bruno-api-typescript/docs/usage-guide.md-42-42 (1)
42-42:⚠️ Potential issue | 🟡 Minor코드 블록에 언어 지정 누락 (markdownlint MD040)
정적 분석 도구에서 Line 42의 fenced code block에 언어가 지정되지 않았다고 알려주고 있어요. 디렉토리 구조이므로
text나plaintext정도로 지정하면 린트 경고를 해소할 수 있습니다.🔧 제안
-``` +```text bruno/turbo.json-10-13 (1)
10-13:⚠️ Potential issue | 🟡 Minor
sync:bruno태스크의 숨겨진 의존성을 명시적으로 표현하기
현재 상황:
sync:bruno스크립트가 내부에서bruno-api-typescript패키지를 빌드한 후 그 결과물(dist/cli/index.js)을 실행합니다.
- 의존성이 스크립트 레벨에서만 처리되고 있어 turbo의 작업 오케스트레이션에서는 보이지 않습니다.
개선 제안:
"dependsOn": ["^build"]를 추가하여 의존성을 명시적으로 선언하는 것이 좋습니다.
- 이렇게 하면 turbo가 작업 순서를 더 명확하게 이해하고 최적화할 수 있습니다.
- 모노레포에서 복잡한 의존성 체인을 추적하기 더 쉬워집니다.
현재 구조는 동작하지만: 스크립트 내부에 빌드 로직이 숨겨져 있어 의존성 관계가 한눈에 들어오지 않습니다.
- turbo.json에 명시하면 프로젝트 전체의 작업 흐름을 더 쉽게 이해할 수 있습니다.
package.json-13-15 (1)
13-15:⚠️ Potential issue | 🟡 Minor
pnpm.onlyBuiltDependencies설정 제거 권장
bruno-api-typescript는 내부 워크스페이스 패키지이며 install/postinstall/preinstall 스크립트가 없습니다. 이 설정은 외부 의존성의 빌드 스크립트 실행을 제어하기 위한 것으로, 다음 이유로 제거해도 무방합니다.
- 내부 워크스페이스 패키지 -
packages/bruno-api-typescript는 워크스페이스의 일부이므로 외부 의존성이 아님- 빌드 스크립트 부재 - install 시간에 실행될 build/install/postinstall 스크립트가 없음
- Turbo 오케스트레이션 - 루트
package.json의turbo스크립트(pnpm build,pnpm dev등)로 빌드 관리 중패키지 정보
"scripts": { "build": "tsc", "dev": "tsc --watch", "test": "node --test" }이 설정을 제거해도 Turbo 워크플로우에는 영향을 주지 않습니다.
packages/bruno-api-typescript/docs/bruno-guide.md-583-640 (1)
583-640:⚠️ Potential issue | 🟡 MinorGET/POST 템플릿의 마크다운 코드 블록 중첩이 깨져 있습니다.
하단의 "빠른 템플릿" 섹션에서 중첩된 코드 펜스가 올바르게 닫히지 않아 마크다운 렌더링 시
}와 닫는 펜스가 코드 블록 밖에 표시됩니다.
- GET 템플릿 (Line 585-607): 내부
```json블록이 닫힌 후 외부````bru블록의}와 닫는 펜스가 누락되었습니다.- POST 템플릿 (Line 611-640): 동일한 문제가 반복됩니다.
내부 JSON 코드 블록보다 외부 bru 코드 블록의 펜스 수가 더 많아야 합니다. 상단의 "실전 예시" 섹션처럼
`````(5개)와````(4개)의 중첩 패턴을 일관되게 적용해 주세요.docs/bruno-typescript-guide.md-50-53 (1)
50-53:⚠️ Potential issue | 🟡 Minor코드 블록에 언어를 지정해 주세요.
정적 분석 도구(markdownlint)에서 Line 50의 코드 블록에 언어가 지정되지 않았다고 경고하고 있습니다. 가독성과 린터 경고 해소를 위해 언어를 추가해 주세요.
🔧 수정 제안
-``` +```text 7) 어드민 [Admin]/ 목록 조회 [list].bru → adminApi.getList ```packages/bruno-api-typescript/.github/workflows/api-docs-deploy.yml-31-32 (1)
31-32:⚠️ Potential issue | 🟡 Minor
npm install대신pnpm install을 사용해 주세요.모노레포의 패키지 매니저는
pnpm입니다.npm install을 사용하면pnpm-lock.yaml과 동기화되지 않아 의존성 불일치가 발생할 수 있습니다. Line 36의npm run build도 마찬가지로pnpm run build로 변경이 필요합니다.🔧 수정 제안
- name: 의존성 설치 - run: npm install + run: pnpm install - name: OpenAPI 생성 및 변경사항 감지 run: | - npm run build + pnpm run buildAs per coding guidelines,
{package.json,pnpm-lock.yaml,**/package.json}: Usepnpmas the package manager for dependency management.packages/bruno-api-typescript/src/generator/typeGenerator.ts-160-173 (1)
160-173:⚠️ Potential issue | 🟡 Minor중첩 타입 추출 시 첫 번째 배열 요소만으로 재귀하고 있어요.
Line 134-157에서
fieldTypes는 배열의 모든 아이템을 순회하며 각 필드의 타입을 수집하지만, Line 168의const val = itemType[key]는 첫 번째 아이템(itemType = value[0])의 값만 사용하여 중첩 타입을 재귀적으로 추출합니다.만약 첫 번째 요소에는 없지만 두 번째 이후 요소에만 있는 중첩 객체 필드가 있으면, 해당 필드의 하위 인터페이스가 생성되지 않아요.
packages/bruno-api-typescript/docs/bruno-tutorial.md-602-604 (1)
602-604:⚠️ Potential issue | 🟡 Minor마크다운 리스트 문법 오류가 있어요.
Line 603에서
--로 시작하는데, 마크다운 리스트 항목은-로 시작해야 합니다. 현재 상태로는 렌더링 시 리스트로 인식되지 않아요.🔧 수정 제안
- [Bruno 파일 작성 가이드](./bruno-guide.md) - 레퍼런스 --- [빠른 시작](./quickstart.md) - 명령어 정리 +- [빠른 시작](./quickstart.md) - 명령어 정리packages/bruno-api-typescript/API_DEFINITIONS_SPECIFICATION.md-13-23 (1)
13-23:⚠️ Potential issue | 🟡 MinorFenced code block에 언어 지정이 빠져있어요.
markdownlint에서 MD040 경고가 발생합니다. 디렉토리 구조 표시에 언어 태그를 추가해주세요.
✏️ 수정 제안
Line 13:
-``` +```text src/apis/Line 229:
-``` +```text src/Also applies to: 229-237
packages/bruno-api-typescript/src/generator/index.ts-56-74 (1)
56-74:⚠️ Potential issue | 🟡 MinorWindows 경로 구분자 문제 및 들여쓰기 오류 (openapiConverter.ts와 동일).
- Line 58:
split('/')은 Windows의 백슬래시 경로를 처리하지 못합니다.- Lines 67-70:
if블록의 닫는 괄호 들여쓰기가 어긋나 있어요.- 이 함수는
openapiConverter.ts의extractDomain과 완전히 동일한 로직이므로, 공통 유틸로 추출하면 두 곳 모두 해결됩니다.🐛 수정 제안
function extractDomain(filePath: string, brunoDir: string): string { const relativePath = relative(brunoDir, filePath); - const parts = relativePath.split('/'); + const parts = relativePath.split(/[\\/]/); // "Solid Connection" 폴더 제거 const filteredParts = parts.filter(part => part !== 'Solid Connection'); // 대괄호 패턴이 있는 첫 번째 폴더 찾기 const bracketPattern = /\[([^\]]+)\]/; for (const part of filteredParts) { const bracketMatch = part.match(bracketPattern); if (bracketMatch) { return bracketMatch[1].trim(); // 대괄호 안의 영문키 + } } - }packages/bruno-api-typescript/src/converter/openapiConverter.ts-133-151 (1)
133-151:⚠️ Potential issue | 🟡 MinorWindows 환경에서
extractDomain의 경로 분리가 실패할 수 있어요.
relative()함수는 Windows에서 백슬래시(\)로 구분된 경로를 반환하지만, Line 135에서split('/')만 사용하고 있어요.- 동일한 문제가
generator/index.ts의extractDomain에도 존재합니다.- 또한 Lines 144-146의 들여쓰기가 어긋나 있어 가독성이 떨어집니다.
🐛 경로 정규화 및 들여쓰기 수정 제안
function extractDomain(filePath: string, brunoDir: string): string { const rel = relative(brunoDir, filePath); - const parts = rel.split('/'); + const parts = rel.split(/[\\/]/); // "Solid Connection" 폴더 제거 const filteredParts = parts.filter(part => part !== 'Solid Connection'); // 대괄호 패턴이 있는 첫 번째 폴더 찾기 const bracketPattern = /\[([^\]]+)\]/; for (const part of filteredParts) { const bracketMatch = part.match(bracketPattern); if (bracketMatch) { return bracketMatch[1].trim(); // 대괄호 안의 영문키 + } } - }packages/bruno-api-typescript/CHANGELOG.md-93-93 (1)
93-93:⚠️ Potential issue | 🟡 Minor버전 날짜가 플레이스홀더로 남아 있어요.
[0.3.0] - 2025-01-XX에서XX를 실제 릴리스 날짜로 채워주세요. 그대로 게시되면 변경 이력의 신뢰도가 떨어질 수 있습니다.packages/bruno-api-typescript/src/index.ts-1-4 (1)
1-4:⚠️ Potential issue | 🟡 MinorJSDoc 주석의 패키지 이름이 실제와 다릅니다.
- Line 2의
bruno-openapi-sync는 실제 패키지 이름bruno-api-typescript와 일치하지 않아요.- 이전 이름이 남은 것으로 보입니다.
📝 패키지 이름 수정
/** - * bruno-openapi-sync + * bruno-api-typescript * Main entry point for programmatic usage */packages/bruno-api-typescript/tests/cli.test.js-409-558 (1)
409-558:⚠️ Potential issue | 🟡 Minor동적으로 생성된 fixture 디렉토리가 정리되지 않아요.
FIXTURES_DIR아래에 동적으로 생성되는 디렉토리들이 테스트 종료 후 삭제되지 않습니다.
bruno-empty-object(Line 409)bruno-invalid-json(Line 449)bruno-empty-array(Line 487)bruno-no-docs(Line 528)
after()훅(Line 24-28)은TEST_OUTPUT_DIR만 정리하고,FIXTURES_DIR하위 동적 디렉토리는 남아 있게 됩니다.반복 실행 시 이전 fixture가 남아 있어 테스트 격리가 깨질 수 있습니다.
🧹 after 훅에 동적 fixture 정리 추가 제안
// 테스트 후 정리 after(() => { if (existsSync(TEST_OUTPUT_DIR)) { rmSync(TEST_OUTPUT_DIR, { recursive: true, force: true }); } + // 동적으로 생성된 fixture 정리 + const dynamicFixtures = [ + 'bruno-empty-object', + 'bruno-invalid-json', + 'bruno-empty-array', + 'bruno-no-docs', + ]; + for (const name of dynamicFixtures) { + const dir = join(FIXTURES_DIR, name); + if (existsSync(dir)) { + rmSync(dir, { recursive: true, force: true }); + } + } });
🧹 Nitpick comments (53)
packages/bruno-api-typescript/tsconfig.json (1)
12-12:strict: false— 새 패키지에서 strict 모드 비활성화가 의도된 것인지 확인이 필요합니다.
- 현재 상태:
strict: false로 설정되어 있어strictNullChecks,noImplicitAny등 주요 타입 안전 장치가 모두 꺼져 있습니다.- 권장 사항: 생성된(generated) 코드가 strict 체크를 통과하지 못해서 의도적으로 끈 것이라면, 주석으로 사유를 남겨두시면 향후 유지보수에 도움이 됩니다.
- 장기적으로: 생성 코드 품질이 개선되면
strict: true로 전환하는 것을 고려해 주세요.packages/bruno-api-typescript/.github/workflows/sync-to-frontend.example.yml (3)
86-113: 동일 업데이트에 대해 PR이 중복 생성될 수 있습니다.
BRANCH_NAME에 타임스탬프(date +%Y%m%d-%H%M%S)가 포함되어 매 실행마다 고유한 브랜치가 생성됩니다. 이전 PR이 머지되기 전에 워크플로가 다시 트리거되면, 유사한 내용의 PR이 여러 개 쌓일 수 있어요.고정 브랜치명(예:
auto/update-api-hooks)을 사용하고,--forcepush를 하면 항상 최신 상태의 PR 하나만 유지할 수 있습니다.♻️ 고정 브랜치명을 사용하는 방안
- BRANCH_NAME="auto/update-api-hooks-$(date +%Y%m%d-%H%M%S)" - git checkout -b $BRANCH_NAME + BRANCH_NAME="auto/update-api-hooks" + git checkout -b $BRANCH_NAME그리고 push 시:
- git push origin $BRANCH_NAME + git push --force origin $BRANCH_NAMEPR 생성 단계에서도 기존 PR 존재 여부를 확인하는 로직을 추가하면 더 견고해집니다:
- name: Create or Update Pull Request if: steps.git-check.outputs.has_changes == 'true' working-directory: frontend-repo run: | EXISTING_PR=$(gh pr list --head auto/update-api-hooks --json number --jq '.[0].number' 2>/dev/null || echo "") if [ -n "$EXISTING_PR" ]; then echo "✅ Existing PR #$EXISTING_PR updated via force push" else gh pr create \ --title "🔄 Update API Hooks from Bruno" \ --body "..." \ --base main \ --head auto/update-api-hooks fi env: GH_TOKEN: ${{ steps.generate-token.outputs.token }}
56-62: GitHub에서 설치 시 버전 고정이 없습니다.
github:manNomi/bruno-api-typescript는 항상 기본 브랜치의 최신 커밋을 설치합니다. 예기치 않은 breaking change가 자동 생성 결과에 영향을 줄 수 있으므로, 태그나 커밋 해시를 명시하면 더 안정적입니다.- npm install -D github:manNomi/bruno-api-typescript + npm install -D github:manNomi/bruno-api-typescript#v1.0.0물론 예제 파일이라 참고용으로 충분하지만, 실제 적용 시 주의할 포인트로 문서에 한 줄 남겨두면 좋겠습니다.
50-54: Node.js 18은 EOL(End of Life) 상태입니다.Node.js 18은 2025년 4월에 EOL에 도달했습니다. 예제 파일이지만 이를 참고하여 셋업하는 사용자가 있을 수 있으므로, 현재 Active LTS인 Node 20 이상으로 업데이트하면 좋겠습니다.
- node-version: '18' + node-version: '20'apps/web/src/types/university.ts (1)
9-11:HomeUniversitySlug과HomeUniversityName간의 동기화 안전장치를 고려해 주세요.
HomeUniversitySlug이 하드코딩된 문자열 리터럴 유니온으로 정의되어 있어, 나중에 대학이 추가될 때HomeUniversityNameenum과 별도로 관리해야 합니다.HOME_UNIVERSITY_LIST(constants/university.ts)의 slug 값과도 수동으로 동기화해야 하므로 누락 위험이 있습니다.
HOME_UNIVERSITY_LIST의 slug 필드에서 타입을 파생시키면 단일 소스로 관리할 수 있습니다.♻️ 파생 타입 제안
-export type HomeUniversitySlug = "inha" | "incheon" | "sungshin";
constants/university.ts에서:export const HOME_UNIVERSITY_SLUGS = ["inha", "incheon", "sungshin"] as const; export type HomeUniversitySlug = (typeof HOME_UNIVERSITY_SLUGS)[number];apps/web/src/components/layout/TopDetailNavigation.tsx (1)
14-27:handleBack과backHref가 동시에 전달되면handleBack이 우선합니다.
- Line 27에서
handleBack || routeBack이므로handleBack이 truthy이면backHref는 무시됩니다.- 의도된 동작이라면 괜찮지만, 두 prop의 우선순위가 코드에서만 파악 가능하므로 JSDoc이나 주석으로 명시하면 소비자 측 혼란을 줄일 수 있습니다.
apps/web/src/app/university/[homeUniversity]/search/_ui/SearchPageContent.tsx (2)
85-88:countrySelectsToRender의useMemo가 실질적으로 매 렌더마다 재계산됩니다.
watchedCountries는watch(["country1", "country2", "country3"])의 반환값인데,watch에 배열을 전달하면 매 렌더마다 새로운 배열 참조를 반환합니다.- 따라서
useMemo의 의존성 비교가 항상 변경으로 판단되어 메모이제이션 효과가 없습니다.각 country를 개별로 watch하면 안정적인 참조를 얻을 수 있습니다.
♻️ 개선 제안
- const watchedCountries = watch(["country1", "country2", "country3"]); + const watchedCountry1 = watch("country1"); + const watchedCountry2 = watch("country2"); + const watchedCountry3 = watch("country3"); + const watchedCountries = [watchedCountry1, watchedCountry2, watchedCountry3];그리고
useMemo의존성도 변경:const countrySelectsToRender = useMemo(() => { - const count = 1 + (watchedCountries[0] ? 1 : 0) + (watchedCountries[1] ? 1 : 0); + const count = 1 + (watchedCountry1 ? 1 : 0) + (watchedCountry2 ? 1 : 0); return Array.from({ length: Math.min(count, 3) }, (_, i) => i + 1); - }, [watchedCountries]); + }, [watchedCountry1, watchedCountry2]);
22-29: Zod 스키마가 form의 실제 사용 패턴에 비해 과도하게 느슨합니다.
searchText가 스키마에 포함되어 있지만, 실제로는useState로 별도 관리되고 있어 form 필드로 사용되지 않습니다.- 불필요한 필드를 스키마에서 제거하면 타입과 실제 사용이 일치하여 혼란을 줄일 수 있습니다.
♻️ 제안
const searchSchema = z.object({ - searchText: z.string().optional(), regions: z.array(z.string()).optional(), languageTestType: z.union([z.nativeEnum(LanguageTestType), z.literal(""), z.null()]).optional(), country1: z.union([z.nativeEnum(CountryCode), z.literal(""), z.null()]).optional(), country2: z.union([z.nativeEnum(CountryCode), z.literal(""), z.null()]).optional(), country3: z.union([z.nativeEnum(CountryCode), z.literal(""), z.null()]).optional(), });apps/web/src/app/university/[homeUniversity]/_ui/RegionFilter.tsx (1)
7-17: "전체" 값에RegionEnumExtend.ALL을 사용하면 일관성이 높아집니다.
- Line 8에서
value: "전체"로 하드코딩되어 있지만, 다른 항목들은 모두RegionEnumExtendenum을 사용합니다.RegionEnumExtend.ALL이"전체"와 동일하므로 런타임에는 문제가 없지만, enum 값이 변경될 경우 이 부분만 누락될 수 있습니다.- 마찬가지로 Line 16의
RegionEnumExtend | "전체"타입은RegionEnumExtend.ALL = "전체"이므로RegionEnumExtend만으로 충분합니다.♻️ 제안
const REGIONS = [ - { value: "전체", label: "전체" }, + { value: RegionEnumExtend.ALL, label: "전체" }, { value: RegionEnumExtend.AMERICAS, label: "미주권" }, { value: RegionEnumExtend.EUROPE, label: "유럽권" }, { value: RegionEnumExtend.ASIA, label: "아시아권" }, { value: RegionEnumExtend.CHINA, label: "중국권" }, ] as const; interface RegionFilterProps { - selectedRegion: RegionEnumExtend | "전체"; - onRegionChange: (region: RegionEnumExtend | "전체") => void; + selectedRegion: RegionEnumExtend; + onRegionChange: (region: RegionEnumExtend) => void; }apps/web/src/app/university/list/[homeUniversityName]/page.tsx (1)
27-27:as HomeUniversitySlug타입 단언 대신 런타임 가드를 우선 적용하는 것을 권장합니다.새로운
[homeUniversity]/page.tsx에서는HOME_UNIVERSITY_SLUGS.includes(...)가드를 먼저 실행하고 있는데, 이 파일에서는 타입 단언 후undefined체크에 의존하고 있습니다. 일관성을 위해 새 페이지와 동일한 패턴을 사용하면 좋겠습니다.apps/web/src/app/university/[homeUniversity]/page.tsx (2)
54-60: 전체 대학 목록을 가져온 뒤 프론트에서 필터링하는 구조에 대한 참고 사항입니다.
- 현재
getSearchUniversitiesAllRegions()로 전체 목록을 가져온 뒤homeUniversityName으로 필터링하고 있습니다.- ISR 캐시 덕분에 매 요청마다 API를 호출하지는 않지만, 대학 수가 늘어나면 서버 측 필터링 API를 도입하는 것이 바람직합니다.
ListUniversity.homeUniversityName이 optional 필드(homeUniversityName?: HomeUniversityName)이므로 해당 값이 없는 대학은 자연스럽게 제외됩니다 — 의도된 동작인지 확인해 주세요.
40-52: 슬러그 검증이 이중으로 수행되고 있습니다.
- Line 44:
HOME_UNIVERSITY_SLUGS.includes(...)체크- Line 48-52:
getHomeUniversityBySlug()결과undefined체크둘 다 같은 데이터 소스(
HOME_UNIVERSITY_LIST)에서 파생되므로 하나만 있어도 충분합니다. 다만 방어적 코딩 관점에서 유지해도 무방합니다.apps/web/src/app/university/(home)/page.tsx (1)
7-7:revalidate = 3600이 이 페이지에서는 불필요할 수 있습니다.이 페이지는 서버 데이터 패칭 없이
HOME_UNIVERSITY_LIST상수만 사용하고 있어서, ISR 재검증 설정이 실질적인 효과가 없습니다. 제거하거나revalidate = false로 완전 정적 페이지로 설정하는 것이 의미적으로 더 정확합니다.apps/web/src/constants/university.ts (1)
14-81: 슬러그 관련 데이터가 4곳에 중복 정의되어 있어 유지보수 시 동기화 누락 위험이 있습니다.현재 동일한 슬러그 정보가 다음에 각각 하드코딩되어 있습니다:
HOME_UNIVERSITY_SLUG_MAP(Line 17-21)HOME_UNIVERSITY_TO_SLUG_MAP(Line 26-30)HOME_UNIVERSITY_LIST내 각 항목의slug필드 (Line 44-69)HOME_UNIVERSITY_SLUGS배열 (Line 81)새 대학을 추가할 때 한 곳이라도 빠뜨리면 런타임 불일치가 발생합니다.
HOME_UNIVERSITY_LIST를 단일 소스로 삼아 나머지를 파생시키는 것을 권장합니다.♻️ 단일 소스에서 파생하는 구조 제안
-export const HOME_UNIVERSITY_SLUG_MAP: Record<HomeUniversitySlug, HomeUniversity> = { - inha: HomeUniversity.INHA, - incheon: HomeUniversity.INCHEON, - sungshin: HomeUniversity.SUNGSHIN, -}; - -export const HOME_UNIVERSITY_TO_SLUG_MAP: Record<HomeUniversity, HomeUniversitySlug> = { - [HomeUniversity.INHA]: "inha", - [HomeUniversity.INCHEON]: "incheon", - [HomeUniversity.SUNGSHIN]: "sungshin", -}; +export const HOME_UNIVERSITY_SLUG_MAP = Object.fromEntries( + HOME_UNIVERSITY_LIST.map((uni) => [uni.slug, uni.name]), +) as Record<HomeUniversitySlug, HomeUniversity>; + +export const HOME_UNIVERSITY_TO_SLUG_MAP = Object.fromEntries( + HOME_UNIVERSITY_LIST.map((uni) => [uni.name, uni.slug]), +) as Record<HomeUniversity, HomeUniversitySlug>;-export const HOME_UNIVERSITY_SLUGS: HomeUniversitySlug[] = ["inha", "incheon", "sungshin"]; +export const HOME_UNIVERSITY_SLUGS: HomeUniversitySlug[] = HOME_UNIVERSITY_LIST.map((uni) => uni.slug);참고: 이 경우
HOME_UNIVERSITY_LIST정의를SLUG_MAP/TO_SLUG_MAP보다 위로 옮겨야 합니다.apps/web/src/app/university/[homeUniversity]/[id]/page.tsx (3)
109-119: 1. 슬러그 검증이 두 번 중복됩니다 — 하나로 합칠 수 있습니다.
HOME_UNIVERSITY_SLUGS.includes()체크(Line 110)와getHomeUniversityBySlug()호출(Line 114)은 본질적으로 동일한 검증입니다.getHomeUniversityBySlug의 결과만으로 유효성 판단이 가능합니다.2.
Number(id)가 NaN을 반환할 가능성이 있습니다.정적 경로 생성 덕분에 실제로는 발생하기 어렵지만, 방어적으로 NaN 체크를 추가하면 더 안전합니다.
♻️ 슬러그 검증 통합 및 NaN 방어 제안
const CollegeDetailPage = async ({ params }: PageProps) => { const { homeUniversity, id } = await params; - // 유효한 슬러그인지 확인 - if (!HOME_UNIVERSITY_SLUGS.includes(homeUniversity as HomeUniversitySlug)) { - notFound(); - } - const homeUniversityInfo = getHomeUniversityBySlug(homeUniversity); if (!homeUniversityInfo) { notFound(); } const collegeId = Number(id); + if (Number.isNaN(collegeId)) { + notFound(); + } const universityData = await getUniversityDetail(collegeId);
58-69:generateMetadata와 페이지 컴포넌트 간 로직이 상당 부분 중복됩니다.다음 로직이 파일 내에서 두 번 반복됩니다:
convertedKoreanName계산 (Lines 58-61, 126-129)- 이미지 URL 분기 처리 (Lines 65-69, 142-146)
pageUrl구성 (Lines 64, 132)Next.js App Router에서
generateMetadata와 페이지 컴포넌트가 별도 실행 맥락이라 일정 수준의 중복은 불가피하지만, 파일 상단에 유틸 함수를 추출하면 유지보수가 한결 편해집니다.♻️ 헬퍼 함수 추출 예시
// 파일 상단에 추가 const getConvertedKoreanName = (koreanName: string, term: string) => term !== process.env.NEXT_PUBLIC_CURRENT_TERM ? `${koreanName}(${term})` : koreanName; const getImageUrl = (baseUrl: string, backgroundImageUrl?: string) => { if (!backgroundImageUrl) return `${baseUrl}/images/article-thumb.png`; return backgroundImageUrl.startsWith("http") ? backgroundImageUrl : `${baseUrl}${backgroundImageUrl}`; };Also applies to: 126-146
43-54:generateMetadata에서도 동일한 슬러그 검증 통합이 가능합니다.페이지 컴포넌트와 마찬가지로, Line 47의
includes체크를 생략하고 Line 57의getHomeUniversityBySlug결과로 유효성을 판단할 수 있습니다. 일관된 패턴을 유지하면 가독성이 올라갑니다.packages/api-schema/package.json (1)
9-13:typecheck스크립트가 있지만typescript가 devDependencies에 없어요.
tsc --noEmit을 실행하려면typescript가 필요한데, 이 패키지의devDependencies에 선언되어 있지 않습니다.- 워크스페이스 루트에서 호이스팅된
typescript에 의존하는 것일 수 있지만, 명시적으로 선언하는 것이 의존성 관리 측면에서 더 안전합니다.🔧 devDependencies에 typescript 추가 제안
"dependencies": { "axios": "^1.6.7" - } + }, + "devDependencies": { + "typescript": "^5.3.3" + } }packages/bruno-api-typescript/tests/fixtures/bruno-v2/7) 어드민 [Admin]/get-list.bru (1)
23-47: JSON 코드 블록 언어 식별자 일관성 개선 권장현재 파일의 200 OK(23-47줄)와 404 응답(51-55줄) 코드 블록이
```만 사용하고 있는데, 다른 fixture들(empty-array.bru,empty-response.bru)은```json으로 통일되어 있습니다.
- 파서 호환성: 다행히 파서(bruParser.ts 261줄의 정규식)는
(?:json)?으로 json 식별자를 선택적으로 처리하므로, 현재 형식도 정상 파싱됩니다- 권장사항: 그럼에도 프로젝트 전체의 일관성을 위해
```json으로 통일하면 좋습니다package.json (1)
7-7:turbo runvsturbo단축 표기 불일치다른 스크립트들(
turbo dev,turbo build,turbo lint등)은turbo <task>단축 형태를 사용하고 있는데,sync:bruno만turbo run sync:bruno로 되어 있어요. 동작에는 문제가 없지만, 일관성을 위해 맞춰주면 좋겠습니다. 😊🔧 제안
- "sync:bruno": "turbo run sync:bruno", + "sync:bruno": "turbo sync:bruno",packages/bruno-api-typescript/docs/migration-guide.md (1)
12-18: 코드 블록에 언어 식별자가 누락되었어요.마크다운 린터(MD040)에서 경고하듯, 디렉터리 구조를 보여주는 코드 블록들에 언어 식별자가 없습니다.
text또는plaintext를 지정해주시면 린트 경고를 해소할 수 있어요.예시 수정
-``` +```text src/apis/ ├── Auth/Also applies to: 26-31, 37-42, 106-114
packages/api-schema/src/axiosInstance.ts (1)
1-7:NEXT_PUBLIC_환경 변수를 공유 패키지에서 사용하고 있어요.
packages/api-schema는 공유 패키지인데,NEXT_PUBLIC_API_SERVER_URL은 Next.js 전용 환경 변수 컨벤션이에요. 이렇게 하면 이 패키지가 Next.js에 암묵적으로 결합됩니다.- 해당 환경 변수가 설정되지 않으면
baseURL이undefined가 되어, axios가 상대 경로로 요청을 보내게 되므로 런타임에서 조용히 실패할 수 있어요.지금 당장은 Next.js 앱에서만 사용하니 큰 문제는 아니지만, 향후 다른 패키지에서도 사용할 가능성이 있다면
baseURL을 외부에서 주입받는 구조를 고려해보셔도 좋겠습니다.packages/bruno-api-typescript/docs/github-apps-simple.md (1)
163-169: 원격 저장소 URL이 하드코딩되어 있어요.Line 165에서
bruno-api.git은 플레이스홀더로 안내되어 있지만, Line 169의bruno-api-typescript.gitURL은solid-connection조직에 하드코딩되어 있어요. 이 패키지가 monorepo에 내장되었다면, 이 부분도 수정 안내가 필요하거나 동적으로 참조하는 방식이 더 적절할 수 있습니다.packages/api-schema/scripts/sync-bruno.mjs (1)
60-68:auto모드에서 로컬 디렉터리가 없고BRUNO_REPO_URL도 미설정 시 에러 메시지가 혼란스러울 수 있어요.
auto모드에서는 로컬 경로를 먼저 시도하고, 없으면 원격으로 폴백하는데요. 이때BRUNO_REPO_URL이 없으면"BRUNO_REPO_URL is required when BRUNO source is remote."에러가 발생합니다.auto모드 사용자 입장에서는 왜 갑자기 원격 URL이 필요한지 맥락을 파악하기 어려울 수 있어요.에러 메시지 개선 제안
function ensureRemoteCollectionDir() { if (!remoteRepoUrl) { - throw new Error("BRUNO_REPO_URL is required when BRUNO source is remote."); + throw new Error( + "BRUNO_REPO_URL is required when BRUNO source is remote.\n" + + "In 'auto' mode, set BRUNO_COLLECTION_DIR for a local path or BRUNO_REPO_URL for remote fallback." + ); }packages/bruno-api-typescript/docs/bruno-tutorial.md (1)
371-377: 코드 블록에 언어 지정이 빠져 있어요.마크다운 린트(MD040)에서 경고가 나오는 부분입니다. 펜스 코드 블록에 언어를 지정해 주시면 좋겠어요.
🔧 수정 제안
-``` +```text bruno/ ├── 7) 어드민 [Admin]/ → Admin으로 인식 ├── 8) 사용자 [Users]/ → Users으로 인식 ├── 지원서 [applications]/ → applications으로 인식 (기존 방식) └── 상품 [products]/ → products으로 인식 (기존 방식) -``` +```packages/bruno-api-typescript/.github/workflows/api-review.yml (1)
35-38: Bruno 파일 복원 단계에 중복 체크아웃이 있어요.Line 37의
git checkout ${{ github.head_ref }}후 Line 38에서git checkout HEAD를 다시 실행하고 있는데요,fetch-depth: 0으로 전체 히스토리를 가져왔고 원래 체크아웃이 PR 브랜치이므로HEAD가 이미 PR 커밋입니다. Line 38 한 줄이면 충분해요.♻️ 간결한 복원 로직
- name: 현재 PR의 Bruno로 복원 run: | - git checkout ${{ github.head_ref }} -- bruno/ || true git checkout HEAD -- bruno/packages/bruno-api-typescript/src/generator/apiFactoryGenerator.ts (3)
83-83:let을const로 변경해 주세요.
urlExpression은 선언 후 재할당되지 않으므로const가 적절합니다. Biome 린터에서도 동일하게 지적하고 있어요.🔧 수정 제안
- let urlExpression = `\`${processedUrl}\``; + const urlExpression = `\`${processedUrl}\``;
13-20: ReDoS 위험은 낮지만 방어적으로 이스케이프하면 더 안전해요.Line 15에서
typeName을new RegExp()에 직접 넣고 있는데,typeName은 내부적으로 생성된 PascalCase 식별자이므로 실질적 위험은 낮습니다. 그래도 방어적으로 정규식 특수문자를 이스케이프하면 더 안전해요.🛡️ 수정 제안
function convertEmptyInterfaceToType(content: string, typeName: string): string { - const emptyInterfacePattern = new RegExp(`export interface ${typeName}\\s*\\{\\s*\\}`); + const escaped = typeName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + const emptyInterfacePattern = new RegExp(`export interface ${escaped}\\s*\\{\\s*\\}`); if (emptyInterfacePattern.test(content)) {
25-103: URL 파라미터 추출 로직을 공통 유틸리티로 통합하세요.현재 다음 3개 파일에서 URL에서
{{변수}},:param,{param}패턴을 추출하는 로직이 중복되어 있습니다.
apiFactoryGenerator.ts(36~67줄): 전체 패턴 처리 로직이 함수 내에 인라인으로 구현apiDefinitionGenerator.ts(27~52줄):extractPathParams함수로 구현되어 있음apiClientGenerator.ts(75~80줄)::param과{param}만 처리 (불완전한 구현)제안:
packages/bruno-api-typescript/src/utils/urlUtils.ts에extractUrlParams(url: string)같은 통합 함수를 만들어서 3개 파일에서 재사용하면 유지보수가 한결 수월해집니다. 특히apiClientGenerator.ts의 불완전한 구현도 함께 개선될 것입니다.packages/bruno-api-typescript/scripts/setup-cross-repo.sh (1)
31-31: 사용자 입력값에 대한 유효성 검증이 없어요.
FRONTEND_REPO,BRUNO_REPO,OPENAPI_PATH등의 입력값이 검증 없이 YAML 파일에 직접 삽입됩니다.owner/repo형식인지, 경로에 위험한 문자가 없는지 정도는 간단히 확인하면 좋겠어요.🛡️ 검증 예시
# owner/repo 형식 검증 if [[ ! "$FRONTEND_REPO" =~ ^[a-zA-Z0-9_.-]+/[a-zA-Z0-9_.-]+$ ]]; then echo -e "${RED}올바른 owner/repo 형식이 아닙니다.${NC}" exit 1 fiAlso applies to: 105-107
packages/bruno-api-typescript/src/converter/schemaBuilder.ts (1)
24-34: 배열 스키마 추론 시 첫 번째 요소만 사용하는 점, 괜찮은지 확인해 주세요.Line 33에서
value[0]만으로 배열 아이템 스키마를 추론합니다. 배열 내 요소 구조가 이질적인 경우(예:[{"a":1}, {"a":1, "b":"x"}]) 두 번째 이후 요소의 추가 필드가 누락돼요. 현재 사용 패턴상 동질적 배열만 다룬다면 문제없지만, 향후 다양한 응답 구조를 다루게 되면 주의가 필요합니다.packages/bruno-api-typescript/src/generator/apiDefinitionGenerator.ts (2)
27-41: Biome 린터 지적:let processedUrl은const로 변경 가능해요.Line 30의
processedUrl은 선언 이후 재할당되지 않으므로const가 적절합니다. Line 33의match변수도 타입을 명시하면 Biome의noImplicitAnyLet경고가 해소돼요.🔧 수정 제안
- let processedUrl = url.replace(/\{\{URL\}\}/g, ''); + const processedUrl = url.replace(/\{\{URL\}\}/g, ''); const brunoVarPattern = /\{\{([^}]+)\}\}/g; - let match; + let match: RegExpExecArray | null;
89-136:generateApiDefinitionMeta가 각 API 함수에 대해 두 번 호출되고 있어요.Line 98에서 타입 이름 수집용으로 한 번, Line 119에서 코드 생성용으로 한 번 호출됩니다. 결과를 캐싱하면 불필요한 중복 계산을 줄일 수 있어요.
♻️ 수정 제안
export function generateApiDefinitionsFile( apiFunctions: Array<{ apiFunc: ApiFunction; parsed: ParsedBrunoFile }>, domain: string ): string { const lines: string[] = []; const typeNames = new Set<string>(); + const metas = apiFunctions.map(({ apiFunc, parsed }) => ({ + apiFunc, + meta: generateApiDefinitionMeta(apiFunc, parsed), + })); + - for (const { apiFunc, parsed } of apiFunctions) { - const meta = generateApiDefinitionMeta(apiFunc, parsed); + for (const { meta } of metas) { if (meta.response !== 'void') { typeNames.add(meta.response); } // ... } // ... - for (const { apiFunc, parsed } of apiFunctions) { - const meta = generateApiDefinitionMeta(apiFunc, parsed); + for (const { apiFunc, meta } of metas) { lines.push(` ${apiFunc.name}: {`); // ... }packages/bruno-api-typescript/src/generator/typeGenerator.ts (3)
19-29: Line 23의itemType이 객체 배열일 때 사용되지 않아요.
inferTypeScriptType에서 Line 23에서itemType을 계산하지만, Line 25-27의 객체 분기에서는 이 값을 사용하지 않고${typeName}Item[]을 직접 반환합니다. 불필요한 재귀 호출이 발생하는 셈이에요.🔧 수정 제안
if (Array.isArray(value)) { if (value.length === 0) { return 'any[]'; } - const itemType = inferTypeScriptType(value[0], `${typeName}Item`, indent); // 배열 아이템이 객체면 별도 인터페이스로 추출 if (typeof value[0] === 'object' && !Array.isArray(value[0])) { return `${typeName}Item[]`; } + const itemType = inferTypeScriptType(value[0], `${typeName}Item`, indent); return `${itemType}[]`; }
275-284: 타입 이름 접두사 중복 방지 로직이 취약할 수 있어요.Line 279의
typeName.startsWith(parentTypeName)검사는 우연히 이름이 겹치는 경우 오작동할 수 있습니다. 예를 들어parentTypeName="User",typeName="UserGroup"이면 접두사가 이미 있다고 판단해서"UserGroup"을 반환하지만, 실제로는"UserUserGroup"이 의도된 결과일 수 있어요.정확한 일치 여부를 확인하려면 접두사 뒤 문자까지 확인하는 등 보다 정밀한 로직이 필요합니다.
290-296:toPascalCase를 export하면apiDefinitionGenerator.ts에서 중복 구현을 제거할 수 있어요.현재
toPascalCase는 이 파일에서 private이지만,apiDefinitionGenerator.ts의 Line 19-22에서toCamelCase를 감싸 동일한 기능을 다시 구현하고 있습니다.toPascalCase를 export하면 중복을 없앨 수 있어요.♻️ 수정 제안
-function toPascalCase(str: string): string { +export function toPascalCase(str: string): string {packages/bruno-api-typescript/src/generator/apiClientGenerator.ts (2)
6-7: 동일 모듈에서의 import를 하나로 합칠 수 있어요.♻️ 수정 제안
-import { ParsedBrunoFile } from '../parser/bruParser'; -import { extractJsonFromDocs } from '../parser/bruParser'; +import { ParsedBrunoFile, extractJsonFromDocs } from '../parser/bruParser';
87-90:responseType.replace('Response', 'Request')치환이 의도치 않은 타입명을 만들 수 있어요.Line 89에서 단순 문자열 치환으로 Request 타입명을 유도하고 있습니다. 예를 들어
responseType이"GetResponseDataResponse"라면 결과가"GetRequestDataResponse"가 되어 실제 존재하지 않는 타입을 참조하게 돼요.이 패턴은
apiFactoryGenerator.tsLine 76,apiDefinitionGenerator.tsLine 72에서도 동일하게 사용되고 있으니, 공통 헬퍼로 통합하면서 접미사 치환 로직(Response가 마지막에 올 때만 교체)으로 개선하는 게 좋겠어요.🔧 수정 제안 (접미사만 치환)
- paramsList.push(`data?: ${responseType.replace('Response', 'Request')}`); + const requestType = responseType.endsWith('Response') + ? responseType.slice(0, -'Response'.length) + 'Request' + : `${responseType}Request`; + paramsList.push(`data?: ${requestType}`);packages/bruno-api-typescript/src/generator/mswGenerator.ts (2)
111-137:handlerName필드가 파라미터 타입에 선언되어 있지만 함수 내부에서 사용되지 않아요.
generateDomainHandlersIndex의handlers파라미터에handlerName이 포함되지만, 실제로는fileName과 인덱스 기반handler${index + 1}만 사용합니다. 불필요한 필드가 인터페이스에 남아있으면 호출부에서 혼란을 줄 수 있어요.♻️ 사용하지 않는 필드 제거 제안
export function generateDomainHandlersIndex( domain: string, - handlers: { fileName: string; handlerName: string }[] + handlers: { fileName: string }[] ): string {
19-57:generateMSWHandler가 현재 로직상null을 반환할 수 없습니다.
- 기본 응답이 항상 생성되기 때문에
responseJson은 절대null로 남지 않아요.- 반환 타입
MSWHandler | null은 방어적 설계로 이해되지만, 호출부(index.tsLine 266)에서null체크를 하고 있어서 실제로는 도달 불가능한 코드가 됩니다.현재로서는 큰 문제가 아니며, 추후 필터링 로직이 추가될 가능성을 고려한 설계라면 그대로 두셔도 괜찮습니다.
packages/bruno-api-typescript/src/converter/openapiConverter.ts (1)
105-125:collectBrunoFiles가generator/index.ts의findBrunoFiles와 거의 동일한 로직이에요.두 함수 모두 디렉토리를 재귀 순회하며
.bru파일을 수집합니다. 차이점은findBrunoFiles가collection.bru를 제외하는 것뿐이에요. 공통 유틸리티로 추출하면 중복을 줄일 수 있습니다.packages/bruno-api-typescript/src/generator/brunoHashCache.ts (2)
67-72:calculateHash에서 파일 읽기 실패 시 예외가 전파됩니다.
hasChanged(Line 115)에서 호출될 때, Bruno 파일이 캐시 체크와 해시 계산 사이에 삭제되면readFileSync가 예외를 던져요.cleanup()이 먼저 실행되더라도, 다른 프로세스가 파일을 삭제하는 경우 TOCTOU 문제가 발생할 수 있습니다.🛡️ 방어적 에러 처리 제안
calculateHash(brunoFilePath: string): string { - const content = readFileSync(brunoFilePath, 'utf-8'); - // 개행 문자 정규화 (Windows/Unix 호환성) - const normalized = content.replace(/\r\n/g, '\n').trim(); - return createHash('sha256').update(normalized, 'utf-8').digest('hex'); + try { + const content = readFileSync(brunoFilePath, 'utf-8'); + // 개행 문자 정규화 (Windows/Unix 호환성) + const normalized = content.replace(/\r\n/g, '\n').trim(); + return createHash('sha256').update(normalized, 'utf-8').digest('hex'); + } catch (error) { + throw new Error(`Failed to calculate hash for ${brunoFilePath}: ${(error as Error).message}`); + } }
167-172:getStats()의total이 항상0으로 반환됩니다.주석에 "generateHooks에서 설정"이라고 되어있지만, 반환된 plain object를 외부에서 수정하는 패턴은 API 의도를 파악하기 어렵게 해요.
total을 파라미터로 받거나, 클래스 내부에totalFiles프로퍼티를 추가하는 편이 더 명확합니다.packages/bruno-api-typescript/.github/workflows/frontend-workflow.example.yml (1)
34-44: 예제 워크플로우에서 버전 고정 없이 GitHub에서 직접 설치하고 있어요.
- Line 36:
github:manNomi/bruno-api-typescript는 항상 최신 main 브랜치를 설치합니다. 실제 프로덕션 워크플로우에서는 특정 태그나 커밋 SHA를 고정하는 것이 안전해요.- Line 70:
workflow_dispatch로 수동 실행 시github.event.client_payload.sha가undefined가 되어 커밋 메시지에 빈 값이 들어갈 수 있습니다.예제 파일이므로 큰 문제는 아니지만, 주석으로 버전 고정 권장 사항을 추가하면 사용자에게 도움이 될 거예요.
packages/bruno-api-typescript/.github/workflows/frontend-sync-bruno.yml.template (1)
20-30: GitHub Actions 버전이 예제 워크플로우(frontend-workflow.example.yml)와 불일치해요.
- 이 템플릿은
actions/checkout@v3,actions/setup-node@v3을 사용하지만- 예제 워크플로우는
@v4를 사용합니다.- 두 파일 모두 동일한 패키지에 포함되어 배포되므로 통일하는 것이 좋아요.
packages/bruno-api-typescript/tests/cli.test.js (2)
8-8:writeFileSync를 상단에서 한 번만 import하면 깔끔해집니다.
- Line 8에서
readFileSync,mkdirSync등은 구조 분해로 가져오고 있지만writeFileSync는 빠져 있어요.- 그래서 Line 324, 359, 430, 469, 510, 543에서
require('fs').writeFileSync(...)를 반복 호출하고 있습니다.♻️ 상단 import에 writeFileSync 추가
-const { existsSync, rmSync, mkdirSync, readFileSync } = require('fs'); +const { existsSync, rmSync, mkdirSync, readFileSync, writeFileSync } = require('fs');이후
require('fs').writeFileSync(...)→writeFileSync(...)로 변경하면 됩니다.Also applies to: 324-324, 359-359, 430-430, 469-469, 510-510, 543-543
561-561: 모듈 레벨console.log는 테스트 완료 전에 출력될 수 있어요.
node:testrunner는 비동기로describe/test블록을 실행하므로, 이 라인은 모든 테스트가 끝나기 전에 실행됩니다. 정확한 완료 로그가 필요하다면after()훅 안에 넣는 것이 안전합니다.packages/bruno-api-typescript/CHANGELOG.md (1)
29-39: 디렉토리 구조 코드 블록에 언어가 지정되지 않았습니다.markdownlint에서
MD040경고가 나오고 있어요. 아래와 같이 언어를 지정하면 해결됩니다.📝 코드 블록 언어 지정
-``` +```text src/mocks/ ├── admin/packages/bruno-api-typescript/src/cli/index.ts (1)
54-59:.old백업 파일이 diff 완료 후 정리되지 않아요.
--diff모드에서 기존 spec을outputFile + '.old'로 복사합니다.- 비교가 끝난 후에도
.old파일이 디스크에 남아 있어, 반복 실행 시 불필요한 파일이 누적됩니다.🧹 diff 완료 후 .old 파일 정리
+import { existsSync, writeFileSync, copyFileSync, unlinkSync } from 'fs';diff 비교 블록(Line 74-104) 끝에 추가:
} catch (error: any) { console.warn(`⚠️ Failed to detect changes: ${error.message}`); + } finally { + // 백업 파일 정리 + if (oldSpecPath && existsSync(oldSpecPath)) { + unlinkSync(oldSpecPath); + } }packages/bruno-api-typescript/src/diff/changelogGenerator.ts (2)
352-389: HTML 생성 시 사용자 입력값이 이스케이프 없이 삽입되고 있어요.
- Line 365:
change.method, Line 367:change.path, Line 376:formatFieldChange(fc)등이 HTML에 직접 삽입됩니다..bru파일의 경로나 필드명에<,>,&같은 문자가 포함되면 HTML이 깨지거나, 의도치 않은 렌더링이 발생할 수 있습니다.- 현재는 로컬 개발 도구이므로 실질적 XSS 위험은 낮지만, HTML 출력의 견고함을 위해 이스케이프 처리를 권장합니다.
🛡️ HTML 이스케이프 헬퍼 추가
+function escapeHtml(str: string): string { + return str + .replace(/&/g, '&') + .replace(/</g, '<') + .replace(/>/g, '>') + .replace(/"/g, '"'); +}이후 HTML 템플릿 내에서 동적 값 삽입 시
escapeHtml()로 감싸주세요.
394-407:formatFieldChange가 HTML 태그를 사용하는데, Markdown에서도 같은 함수를 호출합니다.
- 이 함수는
<code>태그를 반환합니다.generateMarkdown(Line 84, 141)과generateHtmlChanges(Line 376) 양쪽에서 호출되고 있어요.- Markdown에서는 백틱(
`)이 관례이므로, 포맷별로 분리하거나 Markdown용 별도 포맷터를 두면 더 일관됩니다.packages/bruno-api-typescript/src/diff/changeDetector.ts (2)
10-10:ChangeSeverity의'patch'값이 코드 어디에서도 사용되지 않아요.
- 타입 정의에는
'breaking' | 'minor' | 'patch'가 있지만, 실제 할당은'breaking'과'minor'만 사용됩니다.- 향후 사용 예정이라면 주석으로 남겨두고, 아니라면 제거하는 것이 깔끔합니다.
53-54: JSON.parse 실패 시 어떤 파일에서 문제가 발생했는지 알기 어려워요.
readFileSync→JSON.parse과정에서 잘못된 JSON이 있으면 일반적인SyntaxError가 발생합니다.- 두 파일 중 어느 쪽에서 실패했는지 에러 메시지에 포함되면 디버깅이 훨씬 수월해집니다.
💡 파일별 에러 래핑
- const oldSpec: OpenAPISpec = JSON.parse(readFileSync(oldSpecPath, 'utf-8')); - const newSpec: OpenAPISpec = JSON.parse(readFileSync(newSpecPath, 'utf-8')); + let oldSpec: OpenAPISpec; + let newSpec: OpenAPISpec; + try { + oldSpec = JSON.parse(readFileSync(oldSpecPath, 'utf-8')); + } catch (e) { + throw new Error(`Failed to parse old spec (${oldSpecPath}): ${(e as Error).message}`); + } + try { + newSpec = JSON.parse(readFileSync(newSpecPath, 'utf-8')); + } catch (e) { + throw new Error(`Failed to parse new spec (${newSpecPath}): ${(e as Error).message}`); + }
| - name: 체크아웃 | ||
| uses: actions/checkout@v3 | ||
| with: | ||
| fetch-depth: 0 # 전체 히스토리 | ||
|
|
||
| - name: Node 설정 | ||
| uses: actions/setup-node@v3 | ||
| with: | ||
| node-version: '18' |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
actions/checkout latest version 2025 2026
💡 Result:
As of February 14, 2026, the latest actions/checkout release is v6.0.2 (released January 9, 2026). [1]
Use the latest major in workflows like this:
- uses: actions/checkout@v6GitHub’s Marketplace docs already show @v6 as the current recommended major. [2]
🌐 Web query:
actions/setup-node latest version stable 2025 2026
💡 Result:
As of February 14, 2026, the latest stable release of actions/setup-node is v6.2.0 (released January 15, 2026). [1]
Recommended usage (pin to the major for stability + updates within v6):
- uses: actions/setup-node@v6If you need to pin to an exact version:
- uses: actions/setup-node@v6.2.0(For maximum supply-chain safety, pin to the full commit SHA shown on the release page.) [1]
Sources: GitHub releases for actions/setup-node [1] and the repo README examples (showing @v6 usage). [2]
🌐 Web query:
actions/github-script latest version v7 v8
💡 Result:
- Latest
actions/github-scriptmajor: v8 (latest release v8.0.0; useactions/github-script@v8). [1][2] - Latest v7 line: v7.1.0 (use
actions/github-script@v7to stay on v7). [1] - Key change in v8 vs v7: v8 runs on Node.js 24 (v7 runs on Node.js 20) and requires GitHub Actions Runner ≥ v2.327.1. [1][3]
Sources: [1] [2] [3]
GitHub Actions를 최신 버전으로 업데이트해야 합니다.
actions/checkout@v3와 actions/setup-node@v3는 더 이상 최신 권장사항이 아닙니다. 현재 기준(2026년 2월)으로는 다음과 같이 업데이트하는 것이 좋습니다:
-
actions/checkout 업데이트
- 현재: v3
- 최신: v6.0.2 (2026년 1월 9일 릴리스)
- 권장:
actions/checkout@v6
-
actions/setup-node 업데이트
- 현재: v3
- 최신: v6.2.0 (2026년 1월 15일 릴리스)
- 권장:
actions/setup-node@v6
GitHub의 공식 마켓플레이스에서도 이미 v6이 권장되는 버전입니다. 또한 워크플로우에 actions/github-script가 있다면 v8으로 업데이트도 고려해 주세요.
🤖 Prompt for AI Agents
In `@packages/bruno-api-typescript/.github/workflows/api-review.yml` around lines
13 - 21, Update the GitHub Actions steps that currently reference
actions/checkout@v3 and actions/setup-node@v3 to the recommended v6 releases:
replace actions/checkout@v3 with actions/checkout@v6 and actions/setup-node@v3
with actions/setup-node@v6 (use the latest v6 tags such as 6.0.2 and 6.2.0 where
applicable); also scan the workflow for any uses of actions/github-script and
consider upgrading it to v8 if present to align with 2026 recommendations.
| - name: PR에 변경사항 코멘트 | ||
| uses: actions/github-script@v6 | ||
| with: | ||
| script: | | ||
| const fs = require('fs'); | ||
|
|
||
| let comment = '## 🔄 API 변경사항\n\n'; | ||
|
|
||
| // Breaking changes 경고 | ||
| const hasBreaking = '${{ steps.breaking.outputs.has_breaking }}' === 'true'; | ||
| if (hasBreaking) { | ||
| comment += '### ⚠️ **Breaking Changes 발견!**\n\n'; | ||
| comment += '> 기존 코드를 깨뜨릴 수 있는 변경사항이 있습니다. 프론트엔드 팀과 상의 후 머지해주세요.\n\n'; | ||
| } | ||
|
|
||
| // 변경된 Bruno 파일 목록 | ||
| const changedFiles = `${{ steps.bruno_changes.outputs.changed_files }}`; | ||
| if (changedFiles) { | ||
| comment += '### 📝 변경된 Bruno 파일\n\n'; | ||
| comment += '```\n' + changedFiles + '\n```\n\n'; | ||
| } | ||
|
|
||
| // Changelog 내용 | ||
| if (fs.existsSync('CHANGELOG.md')) { | ||
| const changelog = fs.readFileSync('CHANGELOG.md', 'utf8'); | ||
| comment += '### 📊 상세 변경사항\n\n'; | ||
| comment += changelog; | ||
| } else { | ||
| comment += '### ✨ 새로운 API가 추가되었습니다!\n\n'; | ||
| comment += '변경사항을 확인하려면 Bruno 파일을 참조하세요.\n'; | ||
| } | ||
|
|
||
| // API 문서 링크 (배포 후) | ||
| comment += '\n---\n\n'; | ||
| comment += '### 🔗 유용한 링크\n\n'; | ||
| comment += '- 📖 [API 명세서 보기](https://' + context.repo.owner + '.github.io/' + context.repo.repo + '/api-viewer.html)\n'; | ||
| comment += '- 🔄 [변경사항 시각화](https://' + context.repo.owner + '.github.io/' + context.repo.repo + '/changelog.html)\n'; | ||
| comment += '- 📥 [OpenAPI 다운로드](https://' + context.repo.owner + '.github.io/' + context.repo.repo + '/openapi.json)\n'; | ||
|
|
||
| // PR에 코멘트 작성 | ||
| await github.rest.issues.createComment({ | ||
| issue_number: context.issue.number, | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| body: comment | ||
| }); |
There was a problem hiding this comment.
🔒 GitHub Actions 스크립트 인젝션 위험이 있어요.
Line 104에서 ${{ steps.bruno_changes.outputs.changed_files }}를 JavaScript 템플릿 리터럴에 직접 삽입하고 있습니다. .bru 파일 이름에 백틱(`)이나 ${...} 같은 문자가 포함되면 임의 코드가 실행될 수 있어요.
안전한 방식은 환경 변수를 통해 값을 전달하는 것입니다:
🛡️ 수정 제안
- name: PR에 변경사항 코멘트
uses: actions/github-script@v6
with:
+ env:
+ CHANGED_FILES: ${{ steps.bruno_changes.outputs.changed_files }}
+ HAS_BREAKING: ${{ steps.breaking.outputs.has_breaking }}
script: |
const fs = require('fs');
let comment = '## 🔄 API 변경사항\n\n';
- const hasBreaking = '${{ steps.breaking.outputs.has_breaking }}' === 'true';
+ const hasBreaking = process.env.HAS_BREAKING === 'true';
if (hasBreaking) {
comment += '### ⚠️ **Breaking Changes 발견!**\n\n';
comment += '> 기존 코드를 깨뜨릴 수 있는 변경사항이 있습니다. 프론트엔드 팀과 상의 후 머지해주세요.\n\n';
}
- const changedFiles = `${{ steps.bruno_changes.outputs.changed_files }}`;
+ const changedFiles = process.env.CHANGED_FILES || '';🤖 Prompt for AI Agents
In `@packages/bruno-api-typescript/.github/workflows/api-review.yml` around lines
88 - 133, The script directly injects the step output into a JS template literal
via `${{ steps.bruno_changes.outputs.changed_files }}` (assigned to
changedFiles) which allows injection if filenames contain backticks or `${}`;
instead pass that output into the script via an environment variable (e.g., set
env: CHANGED_FILES: ${{ steps.bruno_changes.outputs.changed_files }} on the
github-script step) and inside the script read it as const changedFiles =
process.env.CHANGED_FILES || ''; also perform a simple sanitize/escape (e.g.,
strip or escape backticks and `${`) before embedding into comment to fully
mitigate template injection; update references to changedFiles accordingly.
| - name: Breaking이 있으면 리뷰 요청 | ||
| if: steps.breaking.outputs.has_breaking == 'true' | ||
| uses: actions/github-script@v6 | ||
| with: | ||
| script: | | ||
| // Breaking이 있으면 특정 팀원에게 리뷰 요청 | ||
| // 필요시 팀원 username으로 변경 | ||
| await github.rest.pulls.requestReviewers({ | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| pull_number: context.issue.number, | ||
| reviewers: [], // 예: ['frontend-lead', 'tech-lead'] | ||
| }); |
There was a problem hiding this comment.
빈 reviewers 배열로 API 호출 시 오류가 발생할 수 있어요.
Line 146에서 reviewers: []로 설정되어 있는데, GitHub API에 빈 배열로 리뷰어 요청을 보내면 422 에러가 발생할 수 있습니다. 실제 사용자명이 추가될 때까지 이 스텝 자체를 주석 처리하거나, 배열이 비어있지 않을 때만 실행하도록 가드를 추가해 주세요.
🔧 수정 제안
- name: Breaking이 있으면 리뷰 요청
if: steps.breaking.outputs.has_breaking == 'true'
uses: actions/github-script@v6
with:
script: |
- // Breaking이 있으면 특정 팀원에게 리뷰 요청
- // 필요시 팀원 username으로 변경
- await github.rest.pulls.requestReviewers({
- owner: context.repo.owner,
- repo: context.repo.repo,
- pull_number: context.issue.number,
- reviewers: [], // 예: ['frontend-lead', 'tech-lead']
- });
+ // TODO: 실제 리뷰어 username을 추가한 뒤 주석 해제
+ // const reviewers = ['frontend-lead', 'tech-lead'];
+ // if (reviewers.length > 0) {
+ // await github.rest.pulls.requestReviewers({
+ // owner: context.repo.owner,
+ // repo: context.repo.repo,
+ // pull_number: context.issue.number,
+ // reviewers,
+ // });
+ // }
+ core.info('⚠️ Breaking changes detected — reviewer list is not configured yet.');🤖 Prompt for AI Agents
In `@packages/bruno-api-typescript/.github/workflows/api-review.yml` around lines
135 - 147, The step that calls github.rest.pulls.requestReviewers currently
passes an empty reviewers array (reviewers: []), which will cause a 422 from the
GitHub API; update the actions/github-script block so it only calls
github.rest.pulls.requestReviewers when the reviewers list is non-empty (e.g.,
compute a reviewers variable and guard with if (reviewers && reviewers.length >
0) before calling github.rest.pulls.requestReviewers) or temporarily comment
out/remove the requestReviewers call until actual reviewer usernames are
supplied; ensure you modify the script that invokes
github.rest.pulls.requestReviewers and the reviewers property accordingly.
| run: | | ||
| curl -X POST \ | ||
| -H "Accept: application/vnd.github+json" \ | ||
| -H "Authorization: Bearer ${{ secrets.FRONTEND_REPO_TOKEN }}" \ | ||
| https://api.github.com/repos/YOUR-ORG/FRONTEND-REPO/dispatches \ | ||
| -d '{ | ||
| "event_type": "bruno_updated", | ||
| "client_payload": { | ||
| "bruno_repo": "${{ github.repository }}", | ||
| "commit_sha": "${{ github.sha }}", | ||
| "commit_message": "${{ github.event.head_commit.message }}", | ||
| "author": "${{ github.event.head_commit.author.name }}", | ||
| "timestamp": "${{ github.event.head_commit.timestamp }}" | ||
| } | ||
| }' |
There was a problem hiding this comment.
commit_message 필드에서 JSON 인젝션/파싱 오류가 발생할 수 있습니다.
${{ github.event.head_commit.message }}가 JSON 문자열 내에 직접 보간되고 있습니다. 커밋 메시지에 큰따옴표("), 줄바꿈, 백슬래시 등이 포함되면 JSON이 깨져서 워크플로우가 실패하거나 예기치 않은 동작이 발생합니다.
jq를 사용하여 안전하게 JSON을 구성하는 방식을 권장합니다.
🔧 수정 제안
- name: Repository Dispatch to Frontend
+ env:
+ COMMIT_MESSAGE: ${{ github.event.head_commit.message }}
+ AUTHOR_NAME: ${{ github.event.head_commit.author.name }}
run: |
+ PAYLOAD=$(jq -n \
+ --arg repo "${{ github.repository }}" \
+ --arg sha "${{ github.sha }}" \
+ --arg msg "$COMMIT_MESSAGE" \
+ --arg author "$AUTHOR_NAME" \
+ --arg ts "${{ github.event.head_commit.timestamp }}" \
+ '{
+ event_type: "bruno_updated",
+ client_payload: {
+ bruno_repo: $repo,
+ commit_sha: $sha,
+ commit_message: $msg,
+ author: $author,
+ timestamp: $ts
+ }
+ }')
curl -X POST \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer ${{ secrets.FRONTEND_REPO_TOKEN }}" \
https://api.github.com/repos/YOUR-ORG/FRONTEND-REPO/dispatches \
- -d '{
- "event_type": "bruno_updated",
- "client_payload": {
- "bruno_repo": "${{ github.repository }}",
- "commit_sha": "${{ github.sha }}",
- "commit_message": "${{ github.event.head_commit.message }}",
- "author": "${{ github.event.head_commit.author.name }}",
- "timestamp": "${{ github.event.head_commit.timestamp }}"
- }
- }'
+ -d "$PAYLOAD"🤖 Prompt for AI Agents
In
`@packages/bruno-api-typescript/.github/workflows/bruno-repo-notify-frontend.yml.template`
around lines 22 - 36, 템플릿에서 commit_message 등 값을 JSON에 직접 보간하는 대신 jq로 안전하게 이스케이프된
JSON을 생성하도록 변경하세요: bruno-repo-notify-frontend.yml.template의 curl 호출을 수정해 직접 '-d
'{...}' 대신 jq -n --arg bruno_repo "${{ github.repository }}" --arg commit_sha
"${{ github.sha }}" --arg commit_message "${{ github.event.head_commit.message
}}" --arg author "${{ github.event.head_commit.author.name }}" --arg timestamp
"${{ github.event.head_commit.timestamp }}" '{event_type:"bruno_updated",
client_payload:{bruno_repo:$bruno_repo, commit_sha:$commit_sha,
commit_message:$commit_message, author:$author, timestamp:$timestamp}}' | curl
-X POST ... -d `@-` 형태로 파이프하여 commit_message, author, timestamp 등의 값을 안전하게 이스케이프된
JSON 필드로 전달하도록 하세요.
| - name: Get Bruno Repository Info | ||
| id: bruno_info | ||
| run: | | ||
| if [ "${{ github.event_name }}" = "repository_dispatch" ]; then | ||
| echo "repo=${{ github.event.client_payload.bruno_repo }}" >> $GITHUB_OUTPUT | ||
| echo "sha=${{ github.event.client_payload.commit_sha }}" >> $GITHUB_OUTPUT | ||
| echo "message=${{ github.event.client_payload.commit_message }}" >> $GITHUB_OUTPUT | ||
| else | ||
| echo "repo=${{ github.event.inputs.bruno_repo }}" >> $GITHUB_OUTPUT | ||
| echo "sha=main" >> $GITHUB_OUTPUT | ||
| echo "message=Manual sync" >> $GITHUB_OUTPUT | ||
| fi |
There was a problem hiding this comment.
client_payload 값이 쉘 명령에 직접 주입되어 스크립트 인젝션 위험이 있어요.
- Line 38:
${{ github.event.client_payload.commit_message }}가run:블록에 직접 보간됩니다. - 악의적인
repository_dispatch이벤트를 보낼 수 있는 사용자가commit_message에 쉘 메타문자를 포함시키면 임의 명령 실행이 가능합니다. - 동일한 문제가 Line 187의
$GITHUB_STEP_SUMMARY출력에도 존재합니다.
템플릿 파일이지만 사용자가 그대로 복사하여 사용할 가능성이 높으므로, 환경 변수를 통한 안전한 패턴을 안내해주세요.
🔒 안전한 패턴 제안
- name: Get Bruno Repository Info
id: bruno_info
+ env:
+ PAYLOAD_MESSAGE: ${{ github.event.client_payload.commit_message }}
+ PAYLOAD_REPO: ${{ github.event.client_payload.bruno_repo }}
+ PAYLOAD_SHA: ${{ github.event.client_payload.commit_sha }}
run: |
if [ "${{ github.event_name }}" = "repository_dispatch" ]; then
- echo "repo=${{ github.event.client_payload.bruno_repo }}" >> $GITHUB_OUTPUT
- echo "sha=${{ github.event.client_payload.commit_sha }}" >> $GITHUB_OUTPUT
- echo "message=${{ github.event.client_payload.commit_message }}" >> $GITHUB_OUTPUT
+ echo "repo=${PAYLOAD_REPO}" >> $GITHUB_OUTPUT
+ echo "sha=${PAYLOAD_SHA}" >> $GITHUB_OUTPUT
+ echo "message=${PAYLOAD_MESSAGE}" >> $GITHUB_OUTPUT🤖 Prompt for AI Agents
In
`@packages/bruno-api-typescript/.github/workflows/frontend-sync-bruno.yml.template`
around lines 32 - 43, Get Bruno Repository Info step currently injects
github.event.client_payload fields directly into the run script (e.g.,
github.event.client_payload.commit_message) which risks shell injection; change
the step to stop direct expression interpolation and instead serialize the
payload with toJson (e.g., use toJson(github.event.client_payload)) and parse it
safely inside the run script with a JSON parser (jq) to extract repo, sha, and
message, and likewise avoid writing unescaped values to $GITHUB_STEP_SUMMARY by
writing the parsed values via a safe printf/echo using shell variables (not
direct ${{ }} expansion); update the "Get Bruno Repository Info" step and the
code that writes to $GITHUB_STEP_SUMMARY to follow this pattern.
| const parsedChangedFiles = changedFiles.map(filePath => { | ||
| try { | ||
| const parsed = parseBrunoFile(filePath); | ||
| const domain = extractDomain(filePath, brunoDir); | ||
| return { filePath, parsed, domain }; | ||
| } catch (error) { | ||
| console.error(`❌ Error parsing ${filePath}:`, error); | ||
| return null; | ||
| } | ||
| }).filter(Boolean) as Array<{ filePath: string; parsed: any; domain: string }>; | ||
|
|
||
| const allParsedFiles = brunoFiles.map(filePath => { | ||
| try { | ||
| const parsed = parseBrunoFile(filePath); | ||
| const domain = extractDomain(filePath, brunoDir); | ||
| return { filePath, parsed, domain }; | ||
| } catch (error) { | ||
| return null; | ||
| } | ||
| }).filter(Boolean) as Array<{ filePath: string; parsed: any; domain: string }>; |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Bruno 파일이 최대 3번 중복 파싱됩니다.
- Line 120:
parsedChangedFiles생성 시 변경된 파일 파싱 - Line 131:
allParsedFiles생성 시 전체 파일 파싱 (변경된 파일 포함) - Line 189: 해시 업데이트 시
parseBrunoFile(filePath)를 다시 호출
파일 수가 많아지면 빌드 성능에 영향을 줄 수 있어요. allParsedFiles에서 이미 파싱된 결과를 Map으로 캐싱하고, 해시 업데이트 시 기존 파싱 결과를 재사용하면 중복을 제거할 수 있습니다.
♻️ 파싱 결과 캐싱 제안 (개념 예시)
+ // 전체 파일을 한 번만 파싱하고 Map에 캐싱
+ const parsedFileMap = new Map<string, { parsed: any; domain: string }>();
+ for (const filePath of brunoFiles) {
+ try {
+ const parsed = parseBrunoFile(filePath);
+ const domain = extractDomain(filePath, brunoDir);
+ parsedFileMap.set(filePath, { parsed, domain });
+ } catch (error) {
+ console.error(`❌ Error parsing ${filePath}:`, error);
+ }
+ }
+
+ const allParsedFiles = Array.from(parsedFileMap.entries())
+ .map(([filePath, data]) => ({ filePath, ...data }));
+ const parsedChangedFiles = allParsedFiles
+ .filter(f => changedFiles.includes(f.filePath));해시 업데이트 루프(Lines 187-204)에서도 parsedFileMap을 활용하면 재파싱을 피할 수 있습니다.
Also applies to: 187-204
🤖 Prompt for AI Agents
In `@packages/bruno-api-typescript/src/generator/index.ts` around lines 118 - 137,
parsedChangedFiles and allParsedFiles both call parseBrunoFile (and
extractDomain) causing duplicate parsing which is repeated again later when
updating hashes; create a Map (e.g. parsedFileMap) keyed by filePath that stores
{parsed, domain} when you first call parseBrunoFile (use it when building
parsedChangedFiles), then build allParsedFiles by looking up filePath in
parsedFileMap and calling parseBrunoFile only for files not yet cached, and
finally reuse parsedFileMap inside the hash update loop instead of calling
parseBrunoFile(filePath) again so every Bruno file is parsed at most once.
| console.log('\n📄 Generating index files...'); | ||
| for (const domain of affectedDomains) { | ||
| const domainDir = join(outputDir, domain); | ||
| const files = readdirSync(domainDir).filter(f => f.endsWith('.ts') && f !== 'index.ts'); | ||
|
|
||
| const indexContent = files | ||
| .map(file => { | ||
| const name = file.replace('.ts', ''); | ||
| if (name === 'api') { | ||
| const factoryName = `${toCamelCase(domain)}Api`; | ||
| return `export { ${factoryName} } from './api';`; | ||
| } | ||
| if (name === 'apiDefinitions') { | ||
| const camelDomain = toCamelCase(domain); | ||
| const definitionsValueName = `${camelDomain}ApiDefinitions`; | ||
| const definitionsTypeName = `${camelDomain.charAt(0).toUpperCase()}${camelDomain.slice(1)}ApiDefinitions`; | ||
| return `export { ${definitionsValueName}, ${definitionsTypeName} } from './apiDefinitions';`; | ||
| } | ||
| return `export * from './${name}';`; | ||
| }) | ||
| .join('\n') + '\n'; | ||
|
|
||
| const indexPath = join(domainDir, 'index.ts'); | ||
| writeFileSync(indexPath, indexContent, 'utf-8'); | ||
| console.log(`✅ Generated: ${indexPath}`); | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# typeGenerator 모듈 위치 찾기
fd typeGenerator --type fRepository: solid-connection/solid-connect-web
Length of output: 138
🏁 Script executed:
# toPascalCase 함수 존재 여부 확인
rg -t ts "toPascalCase" packages/bruno-api-typescript/src/generator/Repository: solid-connection/solid-connect-web
Length of output: 2179
🏁 Script executed:
# typeGenerator 파일의 export 구문 확인
cat packages/bruno-api-typescript/src/generator/typeGenerator.ts | grep -A 2 "export"Repository: solid-connection/solid-connect-web
Length of output: 1513
🏁 Script executed:
# index.ts 파일의 현재 import 구문 확인
head -20 packages/bruno-api-typescript/src/generator/index.tsRepository: solid-connection/solid-connect-web
Length of output: 849
🏁 Script executed:
# typeGenerator.ts에서 toPascalCase가 export되는지 명확히 확인
rg "export.*toPascalCase" packages/bruno-api-typescript/src/generator/typeGenerator.tsRepository: solid-connection/solid-connect-web
Length of output: 60
🏁 Script executed:
# typeGenerator.ts 전체 구조 확인
cat packages/bruno-api-typescript/src/generator/typeGenerator.ts | head -100Repository: solid-connection/solid-connect-web
Length of output: 2704
🏁 Script executed:
# index.ts 라인 206-231 확인
sed -n '206,231p' packages/bruno-api-typescript/src/generator/index.tsRepository: solid-connection/solid-connect-web
Length of output: 1195
🏁 Script executed:
# typeGenerator.ts에서 function toPascalCase 정의 위치 확인
rg -n "function toPascalCase" packages/bruno-api-typescript/src/generator/Repository: solid-connection/solid-connect-web
Length of output: 307
🏁 Script executed:
# apiDefinitionGenerator.ts에서 toPascalCase 사용 확인
rg -B 5 -A 5 "function toPascalCase" packages/bruno-api-typescript/src/generator/apiDefinitionGenerator.tsRepository: solid-connection/solid-connect-web
Length of output: 291
PascalCase 변환 로직 추상화 검토 필요
라인 221의 수동 PascalCase 변환은 개선할 수 있지만, 제안된 방식에는 문제가 있습니다:
-
현재 상황
toPascalCase는typeGenerator.ts에서 내부 함수로만 존재하며 export되지 않습니다apiDefinitionGenerator.ts도 동일한 로직을 로컬에서 구현하고 있어 중복이 있습니다
-
올바른 개선 방법
toPascalCase를typeGenerator.ts에서 export하거나index.ts에서apiDefinitionGenerator.ts처럼 로컬 헬퍼 함수로 정의하거나- 공용 유틸 모듈을 만들어 코드 중복을 제거할 수 있습니다
-
제안
typeGenerator.ts에서 이미 사용 중인toPascalCase함수를 export한 후 import하는 방식이 가장 깔끔합니다
🤖 Prompt for AI Agents
In `@packages/bruno-api-typescript/src/generator/index.ts` around lines 206 - 231,
The index generator is manually constructing a PascalCase name for
definitionsTypeName using string ops instead of the shared toPascalCase helper;
export the existing toPascalCase from typeGenerator.ts and import it into this
file (packages/bruno-api-typescript/src/generator/index.ts) and replace the
inline PascalCase logic that builds definitionsTypeName (and any similar ad-hoc
conversions) with a call to toPascalCase(domain) to remove duplication and match
apiDefinitionGenerator.ts behavior.
| if (mswOutputDir) { | ||
| console.log('\n🎭 Generating MSW handlers...'); | ||
| await generateMSWHandlers(parsedChangedFiles, mswOutputDir); | ||
| } |
There was a problem hiding this comment.
MSW 핸들러 생성이 변경된 파일만으로 수행되어 도메인 인덱스가 불완전해질 수 있어요.
- Line 245에서
parsedChangedFiles만 전달하고 있습니다. - 예를 들어
Auth도메인에 5개 엔드포인트가 있고 1개만 변경되면, MSW 도메인 인덱스(index.ts)에 핸들러 1개만 포함돼요. - 반면 API 팩토리 생성(Lines 148-175)은
allParsedFiles를 사용하여 도메인의 전체 함수를 올바르게 포함합니다.
🐛 전체 파일 기반으로 MSW 핸들러 생성 제안
if (mswOutputDir) {
console.log('\n🎭 Generating MSW handlers...');
- await generateMSWHandlers(parsedChangedFiles, mswOutputDir);
+ // 영향받는 도메인의 전체 파일을 전달하여 완전한 도메인 인덱스 생성
+ const affectedDomainFiles = allParsedFiles.filter(f => affectedDomains.has(f.domain));
+ await generateMSWHandlers(affectedDomainFiles, mswOutputDir);
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if (mswOutputDir) { | |
| console.log('\n🎭 Generating MSW handlers...'); | |
| await generateMSWHandlers(parsedChangedFiles, mswOutputDir); | |
| } | |
| if (mswOutputDir) { | |
| console.log('\n🎭 Generating MSW handlers...'); | |
| // 영향받는 도메인의 전체 파일을 전달하여 완전한 도메인 인덱스 생성 | |
| const affectedDomainFiles = allParsedFiles.filter(f => affectedDomains.has(f.domain)); | |
| await generateMSWHandlers(affectedDomainFiles, mswOutputDir); | |
| } |
🤖 Prompt for AI Agents
In `@packages/bruno-api-typescript/src/generator/index.ts` around lines 243 - 246,
The MSW handler generation is currently using only parsedChangedFiles which
yields incomplete domain index files; instead call generateMSWHandlers with the
full set of parsed files (allParsedFiles) or merge parsedChangedFiles into
allParsedFiles before invoking it so the MSW domain index (index.ts) includes
every handler; update the call at the msw generation site to await
generateMSWHandlers(allParsedFiles, mswOutputDir) (or equivalent) and ensure
generateMSWHandlers expects/handles the full parsed file list.
| /** | ||
| * URL 정규화 | ||
| * :param -> {param} 형식으로 변환 (MSW에서 사용) | ||
| */ | ||
| function normalizeUrl(url: string): string { | ||
| // :param을 :param 형식으로 유지 (MSW는 :param 형식 지원) | ||
| return url; | ||
| } |
There was a problem hiding this comment.
normalizeUrl이 no-op이라 쿼리 파라미터가 MSW 핸들러 URL 패턴에 그대로 남습니다.
- OpenAPI 쪽
normalizeUrl은 쿼리 파라미터를 제거하고:param→{param}변환을 수행하지만, 여기서는return url만 하고 있어요. - MSW 라우트 패턴에
?foo=bar같은 쿼리 스트링이 포함되면 매칭이 실패할 수 있습니다.
🐛 쿼리 파라미터 제거 제안
function normalizeUrl(url: string): string {
- // :param을 :param 형식으로 유지 (MSW는 :param 형식 지원)
- return url;
+ // Query parameters 제거 (MSW URL 패턴에는 불필요)
+ const withoutQuery = url.split('?')[0];
+ // :param 형식은 MSW가 지원하므로 그대로 유지
+ return withoutQuery;
}🤖 Prompt for AI Agents
In `@packages/bruno-api-typescript/src/generator/mswGenerator.ts` around lines 99
- 106, normalizeUrl is currently a no-op leaving query strings in MSW route
patterns and using :param form; update normalizeUrl to strip any query string
(remove everything from '?' onward) and convert path parameters from :param to
{param} (e.g. use a regex like /:([A-Za-z0-9_]+)/g → '{$1}') so MSW handlers
match correctly; implement this change inside the normalizeUrl function in
mswGenerator.ts, ensuring existing {param} tokens are preserved and only the
query portion is removed.
| // 블록 종료 감지 | ||
| if (trimmed === '}' && currentBlock && !inCodeBlock) { | ||
| // 블록 파싱 | ||
| parseBlock(result, currentBlock, blockContent); | ||
| currentBlock = null; | ||
| blockContent = []; | ||
| continue; | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, let's read the bruParser.ts file to understand the full context
wc -l packages/bruno-api-typescript/src/parser/bruParser.tsRepository: solid-connection/solid-connect-web
Length of output: 135
🏁 Script executed:
# Read the critical section and surrounding context (lines 80-150)
sed -n '80,150p' packages/bruno-api-typescript/src/parser/bruParser.tsRepository: solid-connection/solid-connect-web
Length of output: 2151
🏁 Script executed:
# Let's also check if there are any test files or fixtures with body:json content
find . -name '*.bru' -type f 2>/dev/null | head -20Repository: solid-connection/solid-connect-web
Length of output: 1176
🏁 Script executed:
# Search for body:json patterns in test or example files
rg 'body:json' -A 10 -B 2 2>/dev/null | head -100Repository: solid-connection/solid-connect-web
Length of output: 7825
🏁 Script executed:
# Check what's in the test files to see if they test the nested JSON body scenario
cat packages/bruno-api-typescript/tests/fixtures/bruno-v2/applications/submit-application.bruRepository: solid-connection/solid-connect-web
Length of output: 414
🏁 Script executed:
# Look at the parseBlock function to understand how it handles incomplete JSON
sed -n '180,250p' packages/bruno-api-typescript/src/parser/bruParser.tsRepository: solid-connection/solid-connect-web
Length of output: 1878
🏁 Script executed:
# Check the test file to see if parsing tests exist
cat packages/bruno-api-typescript/tests/cli.test.js | head -100Repository: solid-connection/solid-connect-web
Length of output: 3428
🏁 Script executed:
# Look for parser-specific tests
find packages/bruno-api-typescript/tests -name '*parser*' -o -name '*bru*test*' 2>/dev/nullRepository: solid-connection/solid-connect-web
Length of output: 60
🏁 Script executed:
# Check if there's a test that verifies the body parsing specifically
grep -r "body:json" packages/bruno-api-typescript/tests --include="*.js" -A 5 -B 5 2>/dev/null | head -60Repository: solid-connection/solid-connect-web
Length of output: 727
🏁 Script executed:
# Let's run a quick test to see if the parser actually fails on the submit-application fixture
cd packages/bruno-api-typescript && npm test 2>&1 | head -150 || echo "Tests may not exist yet"Repository: solid-connection/solid-connect-web
Length of output: 6254
🏁 Script executed:
# Check what happens when we parse the submit-application.bru file to see if it preserves the body correctly
# First, let's see if there's a simple test we can inspect
cat packages/bruno-api-typescript/tests/fixtures/bruno-v2/applications/submit-application.bru | grep -A 10 "body:json"Repository: solid-connection/solid-connect-web
Length of output: 211
🏁 Script executed:
# Let's verify the exact issue by simulating what happens with the parser logic
# Create a simple test to trace through the parsing
python3 << 'EOF'
# Simulate the bug with the exact file content
file_content = """meta {
name: Submit Application
type: http
seq: 3
}
post /applications/submit?region=미주권&size=3&page=1
headers {
Authorization: Bearer {{token}}
Content-Type: application/json
}
body:json {
{
"applicationId": 123
}
}
docs {
```json
{
"success": true,
"submittedAt": "2025-11-12T05:00:00Z"
}
```
}
"""
lines = file_content.split('\n')
currentBlock = None
blockContent = []
inCodeBlock = False
blocks_parsed = []
for i, line in enumerate(lines):
trimmed = line.strip()
# Check block start conditions
if trimmed == 'body:json {':
currentBlock = 'body'
blockContent = []
print(f"Line {i+1}: Block START 'body'")
continue
elif trimmed == 'docs {':
currentBlock = 'docs'
blockContent = []
inCodeBlock = False
print(f"Line {i+1}: Block START 'docs'")
continue
# Check block end condition (THIS IS WHERE THE BUG HAPPENS)
if trimmed == '}' and currentBlock and not inCodeBlock:
print(f"Line {i+1}: Block END detected for '{currentBlock}'")
print(f" Block content collected: {blockContent}")
blocks_parsed.append((currentBlock, blockContent.copy()))
currentBlock = None
blockContent = []
continue
# Track docs code blocks
if currentBlock == 'docs':
if trimmed == '```json' or trimmed == '```':
inCodeBlock = not inCodeBlock
print(f"Line {i+1}: Code block toggle in docs: inCodeBlock={inCodeBlock}")
# Collect block content
if currentBlock:
blockContent.append(line)
print(f"Line {i+1}: Added to '{currentBlock}': {repr(line[:40])}")
print("\n=== PARSED BLOCKS ===")
for block_type, content in blocks_parsed:
content_str = '\n'.join(content).strip()
print(f"\nBlock type: {block_type}")
print(f"Content:\n{content_str}")
if block_type == 'body':
try:
import json
json.loads(content_str)
print("✓ Valid JSON")
except:
print("✗ INVALID JSON - BUG CONFIRMED!")
EOFRepository: solid-connection/solid-connect-web
Length of output: 1076
body:json 블록에서 중첩 JSON의 }가 블록 종료로 오인돼 JSON이 잘려나가는 버그입니다.
블록 종료 감지 로직(126줄)이 trimmed === '}' && currentBlock && !inCodeBlock을 확인하는데, docs 블록에서는 inCodeBlock 가드로 보호되지만 body:json 블록에는 이런 보호가 없습니다. 따라서 JSON 내부의 단독 }라인이 실제 블록 종료로 잘못 인식됩니다.
예시 파일에서 나타나는 문제:
body:json {
{
"applicationId": 123
} ← 이 } 에서 블록이 조기 종료됨
}
결과: { "applicationId": 123 (닫히지 않은 JSON)
제안하는 개선 방법:
- 블록 깊이 추적 — 블록 시작 시 중괄호 카운팅을 초기화하고, 내부에서
{와}를 추적해 올바른 깊이에서만 블록을 종료하기 - body 블록 전용 로직 — 첫
{부터 매칭되는}까지를 명시적으로 수집하는 별도 파서 구현
🤖 Prompt for AI Agents
In `@packages/bruno-api-typescript/src/parser/bruParser.ts` around lines 125 -
132, The parser currently treats any line with trimmed === '}' as end-of-block
(checked alongside currentBlock and inCodeBlock), which causes body:json blocks
to terminate on nested JSON braces; to fix, add brace-depth tracking for
JSON-style blocks: when you set currentBlock to a body:json (where parseBlock
and blockContent are used), initialize a braceDepth counter (e.g.,
jsonBraceDepth = 0) and then, while accumulating blockContent, increment on each
'{' and decrement on each '}' (taking care to ignore braces inside code blocks
or strings if already handled elsewhere); change the block-termination check to
only end the body:json block when braceDepth reaches zero (instead of any
trimmed === '}'), and ensure parseBlock is called only after the full matching
brace is seen.
요약
변경 사항
테스트
참고