[FEAT] API 연동 공통 구조 구축 - #5
Conversation
- Add Vite proxy for /api → localhost:8080 - Add baseApi environment variable support (VITE_API_BASE_URL) - Add .env.example for team reference - Add .env* to gitignore - Add build:staging script
- Add apiResponseSchema() for Zod validation of backend response wrapper - Add ApiResponse<T> type for reuse across features
- Add collectMarketData API with useMutation + cache invalidation - Add fetchMarketSnapshots API with useQuery - Add CollectMarketButton and MarketSnapshotTable UI components
- Add /admin route with market snapshot table and collect button
- Add axios response interceptor to convert backend error responses to ApiError - ApiError exposes status, code, message for structured error handling
- Add collectNews and crawlNews API functions - Add useCollectNews and useCrawlNews mutation hooks - Add NewsCollectButtons UI component - Add news section to admin page
📝 Walkthrough워크스루새로운 관리자 페이지 기능을 추가하는 PR입니다. 시장 지표 수집/조회 및 뉴스 수집/크롤링 기능을 포함하며, API 응답 검증 스키마, 환경 변수 기반 API URL 설정, 개발 서버 프록시 구성, 그리고 CI/CD 배포 시 환경변수 주입 로직이 추가됩니다. Changes
추정 코드 리뷰 노력🎯 3 (Moderate) | ⏱️ ~25 minutes 시
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 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 |
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (4)
.github/PULL_REQUEST_TEMPLATE.md (1)
7-7: 체크리스트 기본값은 미체크(- [ ])로 두는 것이 좋습니다.현재
- [x]는 템플릿 생성 시점부터 완료로 표시되어 추적 신뢰도가 떨어집니다. 기본값을- [ ]로 바꿔 실제 완료 항목만 체크되도록 해주세요.수정 제안
- - [x] + - [ ]🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In @.github/PULL_REQUEST_TEMPLATE.md at line 7, Change the default checked box in the PR template from "- [x]" to an unchecked "- [ ]" so that checklist items start unchecked; locate the "- [x]" token in .github/PULL_REQUEST_TEMPLATE.md (the checklist line) and replace it with "- [ ]" to ensure only completed items are manually checked..env.example (1)
1-1: 환경 변수에 대한 설명 주석 추가를 권장합니다.개발자가 이 변수의 용도와 예상 값을 이해하는 데 도움이 되도록 간단한 주석을 추가하면 좋겠습니다.
💡 제안된 변경
+# API 기본 URL (비어있으면 '/api'로 폴백, 프록시를 통해 localhost:8080으로 전달) +# 프로덕션 예시: https://api.wefin.com VITE_API_BASE_URL=🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In @.env.example at line 1, 추가 환경 변수 설명이 필요합니다: .env.example에 있는 VITE_API_BASE_URL 항목 옆에 간단한 주석을 추가하여 이 변수의 목적(프론트엔드에서 사용하는 API 엔드포인트 기본 URL), 예상 형식(예: https://api.example.com 또는 http://localhost:3000) 및 로컬/프로덕션에서의 권장 값이나 사용 예를 명시하세요; 변수 이름 VITE_API_BASE_URL를 찾아 그 줄에 주석을 달아 개발자가 바로 이해할 수 있도록 만드세요.src/features/admin-news/api/collect-news.ts (1)
8-16: 중복된 POST+parse 로직은 헬퍼로 묶어도 좋겠습니다.현재도 동작은 문제없지만, 엔드포인트가 늘어나면 유지보수 비용이 커질 수 있습니다.
리팩터링 예시
const collectNewsResponseSchema = apiResponseSchema(z.string()) +async function postAndParse(path: '/admin/news/collect' | '/admin/news/crawl') { + const response = await baseApi.post(path) + return collectNewsResponseSchema.parse(response.data) +} + export async function collectNews() { - const response = await baseApi.post('/admin/news/collect') - return collectNewsResponseSchema.parse(response.data) + return postAndParse('/admin/news/collect') } export async function crawlNews() { - const response = await baseApi.post('/admin/news/crawl') - return collectNewsResponseSchema.parse(response.data) + return postAndParse('/admin/news/crawl') }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/features/admin-news/api/collect-news.ts` around lines 8 - 16, Two functions, collectNews and crawlNews, duplicate the same baseApi.post + collectNewsResponseSchema.parse logic; extract that into a small helper (e.g., postAndParse or postAndParseCollectNews) and call it from both functions. Specifically, create a helper that accepts the endpoint string, calls baseApi.post(endpoint), then returns collectNewsResponseSchema.parse(response.data), and update collectNews and crawlNews to call this helper instead of repeating baseApi.post and collectNewsResponseSchema.parse.src/features/admin-market/ui/market-snapshot-table.tsx (1)
32-47:changeDirection조건부 로직 중복 개선 고려.
changeDirection에 따른 스타일(className)과 아이콘(▲/▼/−) 렌더링 로직이 반복됩니다. 유지보수를 위해 매핑 객체로 추출할 수 있습니다.♻️ 리팩토링 제안
+const DIRECTION_CONFIG = { + UP: { className: 'text-red-600', icon: '▲' }, + DOWN: { className: 'text-blue-600', icon: '▼' }, + FLAT: { className: 'text-wefin-subtle', icon: '-' }, +} as const + +function getDirectionConfig(direction: string | null | undefined) { + return DIRECTION_CONFIG[direction as keyof typeof DIRECTION_CONFIG] ?? DIRECTION_CONFIG.FLAT +} // 사용 예시: - <span - className={ - snapshot.changeDirection === 'UP' - ? 'text-red-600' - : snapshot.changeDirection === 'DOWN' - ? 'text-blue-600' - : 'text-wefin-subtle' - } - > - {snapshot.changeDirection === 'UP' - ? '▲' - : snapshot.changeDirection === 'DOWN' - ? '▼' - : '-'} - </span> + <span className={getDirectionConfig(snapshot.changeDirection).className}> + {getDirectionConfig(snapshot.changeDirection).icon} + </span>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/features/admin-market/ui/market-snapshot-table.tsx` around lines 32 - 47, The span renders both className and symbol based on snapshot.changeDirection with duplicated conditional logic; extract a small mapping object (e.g., DIRECTION_MAP) that maps 'UP'/'DOWN'/default to { className, icon } and use that single lookup to set the span's className and inner text, replacing the repeated ternaries in the market snapshot table rendering (refer to snapshot.changeDirection, the span element, and the className/icon logic).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@package.json`:
- Line 9: The package.json defines a "build:staging" script but it's unused in
CI/CD and the staging env is not provided, causing VITE_API_BASE_URL to fall
back in src/shared/api/base-api.ts and no special handling in vite.config.ts for
--mode staging; either remove the unused "build:staging" script or wire it up:
add a .env.staging.example (and .env.staging) with VITE_API_BASE_URL, update
ci.yml and frontend-deploy.yml to call pnpm run build:staging (or document in
README how to run pnpm run build:staging locally), and add mode-specific
handling in vite.config.ts to read process.env for staging mode so the staging
build picks up VITE_API_BASE_URL instead of relying on the fallback.
In `@src/app/router/app-router.tsx`:
- Line 21: The /admin Route currently renders AdminPage without auth; wrap it
with your authentication/authorization guard so only logged-in admins can access
it. Replace the element prop to render ProtectedRoute (or AuthGuard) around
AdminPage, e.g. use <ProtectedRoute
requiredRole="admin"><AdminPage/></ProtectedRoute>, ensuring ProtectedRoute
checks isAuthenticated and user.role/isAdmin from your auth context and
redirects unauthenticated users to the login page (or shows 403 for
unauthorized). Ensure the guarded component name (ProtectedRoute/AuthGuard)
matches your existing implementation and that AdminPage remains the child
component.
In `@src/features/admin-news/ui/news-collect-buttons.tsx`:
- Around line 27-34: The success/error messages rendered for collect and crawl
(collect.isSuccess, collect.isError, crawl.isSuccess, crawl.isError) need an
ARIA live region so screen readers announce updates; wrap the message area (or
each conditional <p>) in an element with an appropriate aria-live (e.g.,
aria-live="polite" for non-critical success and aria-live="assertive" for
errors) and ensure role="status" or role="alert" is set where needed so
assistive tech immediately announces the change in news-collect-buttons.tsx.
In `@src/shared/api/base-api.ts`:
- Line 4: The change to baseApi's baseURL causes requests to '/mock/*' to be
routed to '/api/mock/*' and break static resource loading; fix by
adding/exporting a separate axios instance (e.g., staticApi) in base-api.ts that
mirrors baseApi's configuration but uses no baseURL or baseURL='/' for raw
static requests, then update
src/features/auth-dialog/api/fetch-login-dialog-data.ts to import staticApi
instead of baseApi and call staticApi.get('/mock/login-dialog.json') so the
request resolves to the public static path.
---
Nitpick comments:
In @.env.example:
- Line 1: 추가 환경 변수 설명이 필요합니다: .env.example에 있는 VITE_API_BASE_URL 항목 옆에 간단한 주석을
추가하여 이 변수의 목적(프론트엔드에서 사용하는 API 엔드포인트 기본 URL), 예상 형식(예: https://api.example.com
또는 http://localhost:3000) 및 로컬/프로덕션에서의 권장 값이나 사용 예를 명시하세요; 변수 이름
VITE_API_BASE_URL를 찾아 그 줄에 주석을 달아 개발자가 바로 이해할 수 있도록 만드세요.
In @.github/PULL_REQUEST_TEMPLATE.md:
- Line 7: Change the default checked box in the PR template from "- [x]" to an
unchecked "- [ ]" so that checklist items start unchecked; locate the "- [x]"
token in .github/PULL_REQUEST_TEMPLATE.md (the checklist line) and replace it
with "- [ ]" to ensure only completed items are manually checked.
In `@src/features/admin-market/ui/market-snapshot-table.tsx`:
- Around line 32-47: The span renders both className and symbol based on
snapshot.changeDirection with duplicated conditional logic; extract a small
mapping object (e.g., DIRECTION_MAP) that maps 'UP'/'DOWN'/default to {
className, icon } and use that single lookup to set the span's className and
inner text, replacing the repeated ternaries in the market snapshot table
rendering (refer to snapshot.changeDirection, the span element, and the
className/icon logic).
In `@src/features/admin-news/api/collect-news.ts`:
- Around line 8-16: Two functions, collectNews and crawlNews, duplicate the same
baseApi.post + collectNewsResponseSchema.parse logic; extract that into a small
helper (e.g., postAndParse or postAndParseCollectNews) and call it from both
functions. Specifically, create a helper that accepts the endpoint string, calls
baseApi.post(endpoint), then returns
collectNewsResponseSchema.parse(response.data), and update collectNews and
crawlNews to call this helper instead of repeating baseApi.post and
collectNewsResponseSchema.parse.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: a874708c-ea06-4e91-beca-b10ae721ae82
📒 Files selected for processing (18)
.env.example.github/PULL_REQUEST_TEMPLATE.md.gitignorepackage.jsonsrc/app/router/app-router.tsxsrc/features/admin-market/api/collect-market-data.tssrc/features/admin-market/api/fetch-market-snapshots.tssrc/features/admin-market/model/use-collect-market.tssrc/features/admin-market/model/use-market-snapshots-query.tssrc/features/admin-market/ui/collect-market-button.tsxsrc/features/admin-market/ui/market-snapshot-table.tsxsrc/features/admin-news/api/collect-news.tssrc/features/admin-news/model/use-collect-news.tssrc/features/admin-news/ui/news-collect-buttons.tsxsrc/pages/admin/ui/admin-page.tsxsrc/shared/api/api-response.tssrc/shared/api/base-api.tsvite.config.ts
| <Route path="history" element={<HistoryPage />} /> | ||
| <Route path="chat" element={<ChatPage />} /> | ||
| <Route path="settings" element={<SettingsPage />} /> | ||
| <Route path="admin" element={<AdminPage />} /> |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# 인증 관련 컴포넌트나 가드가 있는지 확인
rg -l -i 'protected|authguard|requireauth|private.*route' --type=ts --type=tsx
ast-grep --pattern 'function ProtectedRoute($$$) { $$$ }'Repository: S0L-V/wefin-fe
Length of output: 85
🏁 Script executed:
#!/bin/bash
# Check the router file to see how admin route is defined
cat -n src/app/router/app-router.tsx | head -40
# Search for authentication-related components/files
find . -type f \( -name "*.ts" -o -name "*.tsx" \) | xargs rg -l -i 'protected|authguard|requireauth' | head -20
# Look for ProtectedRoute specifically
rg -A 5 'ProtectedRoute|function.*Route.*\(' --type ts | head -40Repository: S0L-V/wefin-fe
Length of output: 4353
🏁 Script executed:
#!/bin/bash
# Search for authentication-related implementations
rg -i 'auth|context|provider' --type ts src/ | head -30
# Check AdminPage implementation
cat -n src/pages/admin/ui/admin-page.tsx 2>/dev/null || cat -n src/pages/admin/ui/admin-page.ts 2>/dev/null
# Check AppLayout
cat -n src/widgets/app-layout/ui/app-layout.tsx 2>/dev/null || cat -n src/widgets/app-layout/ui/app-layout.ts 2>/dev/null
# Search for any permission checks or auth guards
rg -i 'permission|role|isadmin|admin.*check' --type ts src/ | head -20Repository: S0L-V/wefin-fe
Length of output: 2735
🏁 Script executed:
#!/bin/bash
# Search for context hooks and auth usage
rg 'useContext|useAuth|createContext' --type ts src/
# Check admin feature implementations
echo "=== Checking CollectMarketButton ==="
find src -name "collect-market-button*" -type f
echo "=== Checking NewsCollectButtons ==="
find src -name "news-collect-buttons*" -type f
# Check for any auth context creation
rg 'createContext|AuthContext|AuthProvider' --type ts src/
# Search for useUser or similar hooks that might track auth state
rg 'useUser|getCurrentUser|isAuthenticated' --type ts src/ | head -20Repository: S0L-V/wefin-fe
Length of output: 237
🏁 Script executed:
#!/bin/bash
# Check the admin feature implementations
echo "=== CollectMarketButton ==="
cat -n src/features/admin-market/ui/collect-market-button.tsx
echo "=== NewsCollectButtons ==="
cat -n src/features/admin-news/ui/news-collect-buttons.tsx
# Check if there are any route guards or middleware
rg 'middleware|beforeEnter|canActivate|guard' --type ts src/
# Search for any environment-based checks or admin-only logic
rg -i 'admin.*check|isadmin|role.*check' --type ts src/Repository: S0L-V/wefin-fe
Length of output: 2690
🏁 Script executed:
#!/bin/bash
# Check the hook implementations for any auth attempts
echo "=== useCollectMarket hook ==="
find src -name "use-collect-market*" -type f -exec cat {} \;
echo -e "\n=== useCollectNews hook ==="
find src -name "use-collect-news*" -type f -exec cat {} \;
# Check if routes have any middleware or protection mechanisms
rg 'react-router|createBrowserRouter|RouteGuard' --type ts src/app/
# Final check: see if there's any authentication state management
rg 'useState.*auth|useAuth|login.*state' --type ts src/ | head -10Repository: S0L-V/wefin-fe
Length of output: 860
관리자 페이지에 인증/인가 보호가 필요합니다.
/admin 라우트가 인증 없이 누구나 접근할 수 있습니다. 시장 데이터 수집, 뉴스 크롤링 같은 관리자 기능이 보호되지 않아 보안 위험이 있습니다.
ProtectedRoute 컴포넌트나 인증 가드를 추가하여 로그인한 관리자만 접근할 수 있도록 구현해야 합니다.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/app/router/app-router.tsx` at line 21, The /admin Route currently renders
AdminPage without auth; wrap it with your authentication/authorization guard so
only logged-in admins can access it. Replace the element prop to render
ProtectedRoute (or AuthGuard) around AdminPage, e.g. use <ProtectedRoute
requiredRole="admin"><AdminPage/></ProtectedRoute>, ensuring ProtectedRoute
checks isAuthenticated and user.role/isAdmin from your auth context and
redirects unauthenticated users to the login page (or shows 403 for
unauthorized). Ensure the guarded component name (ProtectedRoute/AuthGuard)
matches your existing implementation and that AdminPage remains the child
component.
| {collect.isSuccess && <p className="text-sm text-green-600">뉴스 수집 완료</p>} | ||
| {collect.isError && ( | ||
| <p className="text-sm text-red-600">뉴스 수집 실패: {collect.error.message}</p> | ||
| )} | ||
| {crawl.isSuccess && <p className="text-sm text-green-600">뉴스 크롤링 완료</p>} | ||
| {crawl.isError && ( | ||
| <p className="text-sm text-red-600">뉴스 크롤링 실패: {crawl.error.message}</p> | ||
| )} |
There was a problem hiding this comment.
비동기 결과 메시지 영역에 aria-live를 추가해주세요.
현재 성공/실패 텍스트가 시각적으로만 갱신되어 보조기기 사용자에게는 변경이 즉시 전달되지 않을 수 있습니다.
수정 예시
- {collect.isSuccess && <p className="text-sm text-green-600">뉴스 수집 완료</p>}
- {collect.isError && (
- <p className="text-sm text-red-600">뉴스 수집 실패: {collect.error.message}</p>
- )}
- {crawl.isSuccess && <p className="text-sm text-green-600">뉴스 크롤링 완료</p>}
- {crawl.isError && (
- <p className="text-sm text-red-600">뉴스 크롤링 실패: {crawl.error.message}</p>
- )}
+ <div aria-live="polite" role="status">
+ {collect.isSuccess && <p className="text-sm text-green-600">뉴스 수집 완료</p>}
+ {collect.isError && (
+ <p className="text-sm text-red-600">뉴스 수집 실패: {collect.error.message}</p>
+ )}
+ {crawl.isSuccess && <p className="text-sm text-green-600">뉴스 크롤링 완료</p>}
+ {crawl.isError && (
+ <p className="text-sm text-red-600">뉴스 크롤링 실패: {crawl.error.message}</p>
+ )}
+ </div>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/features/admin-news/ui/news-collect-buttons.tsx` around lines 27 - 34,
The success/error messages rendered for collect and crawl (collect.isSuccess,
collect.isError, crawl.isSuccess, crawl.isError) need an ARIA live region so
screen readers announce updates; wrap the message area (or each conditional <p>)
in an element with an appropriate aria-live (e.g., aria-live="polite" for
non-critical success and aria-live="assertive" for errors) and ensure
role="status" or role="alert" is set where needed so assistive tech immediately
announces the change in news-collect-buttons.tsx.
|
|
||
| export const baseApi = axios.create({ | ||
| baseURL: '/', | ||
| baseURL: import.meta.env.VITE_API_BASE_URL || '/api', |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# baseApi로 /mock 경로를 호출하는 지점 확인
rg -nP --type ts --type tsx "baseApi\.(get|post|put|patch|delete)\(\s*['\"]/mock/" -C2
# '/api' prefix 변경 영향 후보 확인 (필요 시 패턴 조정)
rg -nP --type ts --type tsx "baseApi\.(get|post|put|patch|delete)\(\s*['\"]/(?!admin|api)[^'\"]+" -C1Repository: S0L-V/wefin-fe
Length of output: 113
🏁 Script executed:
#!/bin/bash
# First, check the base-api.ts file to confirm the change
echo "=== Checking src/shared/api/base-api.ts ==="
cat -n src/shared/api/base-api.ts | head -20
echo ""
echo "=== Finding baseApi usages with non-API paths ==="
# Search for baseApi method calls without using unsupported file types
rg "baseApi\.(get|post|put|patch|delete)" --max-count=50 -A2
echo ""
echo "=== Specifically checking fetch-login-dialog-data.ts ==="
if [ -f "src/features/auth-dialog/api/fetch-login-dialog-data.ts" ]; then
cat -n src/features/auth-dialog/api/fetch-login-dialog-data.ts
fiRepository: S0L-V/wefin-fe
Length of output: 2507
🏁 Script executed:
#!/bin/bash
echo "=== Checking for public/mock directory ==="
find . -type d -name "mock" -o -type d -name "public" | head -20
echo ""
echo "=== Looking for mock JSON files ==="
find . -name "*.json" -path "*mock*" 2>/dev/null | head -20
echo ""
echo "=== Complete baseApi method calls (exhaustive search) ==="
rg "baseApi\.(get|post|put|patch|delete|request)" -A1 | wc -l
echo ""
echo "=== All baseApi calls with their paths ==="
rg "baseApi\.(get|post|put|patch|delete)" -o --only-matching | sort | uniq -cRepository: S0L-V/wefin-fe
Length of output: 589
기본 baseURL 변경으로 mock 데이터 로딩이 깨집니다.
Line 4 변경으로 baseApi의 baseURL이 /api로 설정되면서, src/features/auth-dialog/api/fetch-login-dialog-data.ts에서 baseApi.get('/mock/login-dialog.json')을 호출할 때 실제 요청이 /api/mock/login-dialog.json으로 라우팅됩니다. 하지만 정적 리소스는 /public/mock/login-dialog.json에 있어 요청이 실패합니다.
/admin/* 경로는 API 엔드포인트이므로 영향을 받지 않지만, /mock 리소스만 별도 처리가 필요합니다.
해결 방향 예시
export const baseApi = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL || '/api',
timeout: 5_000
})
+
+export const staticApi = axios.create({
+ baseURL: '/',
+ timeout: 5_000
+})// src/features/auth-dialog/api/fetch-login-dialog-data.ts
-import { baseApi } from '@/shared/api/base-api'
+import { staticApi } from '@/shared/api/base-api'
-const response = await baseApi.get('/mock/login-dialog.json')
+const response = await staticApi.get('/mock/login-dialog.json')🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/shared/api/base-api.ts` at line 4, The change to baseApi's baseURL causes
requests to '/mock/*' to be routed to '/api/mock/*' and break static resource
loading; fix by adding/exporting a separate axios instance (e.g., staticApi) in
base-api.ts that mirrors baseApi's configuration but uses no baseURL or
baseURL='/' for raw static requests, then update
src/features/auth-dialog/api/fetch-login-dialog-data.ts to import staticApi
instead of baseApi and call staticApi.get('/mock/login-dialog.json') so the
request resolves to the public static path.
- Inject VITE_API_BASE_URL from GitHub Secrets at build time - Remove build:staging script (CI handles env per branch)
📌 PR 설명
프론트엔드 공통 API 처리 구조를 구축하였습니다.
✅ 완료한 기능 명세
📸 스크린샷
💭 고민과 해결과정
1. GET vs POST 패턴 분리
useQuery→ 자동 캐싱 및 리패칭useMutation→ 수동 실행 +onSuccess에서 캐시 무효화2. 공통 응답 처리
status/code/message/data)를apiResponseSchema()로 래핑하여 중복 제거ApiError로 변환하여 일관성 유지Summary by CodeRabbit
릴리즈 노트
새 기능
설정