Skip to content

refactor: unify frontend API contract types#3

Merged
alexization merged 2 commits intodevelopfrom
feat/grc-01-unify-api-contract-types
Mar 25, 2026
Merged

refactor: unify frontend API contract types#3
alexization merged 2 commits intodevelopfrom
feat/grc-01-unify-api-contract-types

Conversation

@alexization
Copy link
Copy Markdown
Owner

@alexization alexization commented Mar 25, 2026

1) 요약

  • 서버 응답 계약 기준을 src/shared/types/api.ts로 모으고 validations.ts의 API 응답 중복 선언을 제거했습니다.
  • EMERALD 누락과 /ranking, /users/{username}, /auth/me 직접 소비자의 계약 drift를 정리했습니다.

2) 연관 이슈

3) 사용자 관점 결과

  • 해결하려는 문제: 프런트 내부에 같은 API 계약이 여러 형태로 존재해 enum/type drift가 발생하던 문제
  • 사용자에게 달라지는 점: 랭킹/유저/인증 응답을 소비하는 코드가 backend 계약과 일치하는 단일 타입 기준을 사용합니다.
  • 비목표: OpenAPI generated type 도입, 전체 컴포넌트 리팩터링, Playwright 도입

4) 영향 범위

  • 변경된 route / page / component: /ranking, /users/[username], /auth/callback, /oauth2/redirect, sitemap, OG image
  • 상태 관리 / 데이터 로딩 영향: ranking/user/auth 응답 parsing이 공용 ApiResponse, AuthMeResponse, RankingListResponse, RegisterUserResponse를 사용하도록 정리
  • API 계약 영향: backend 동작 변경 없음. client 타입만 backend OpenAPI 계약에 정합화
  • 접근성 / 반응형 영향: UI 변경 없음. OG image의 img alt 누락 warning 1건만 함께 정리

5) 스크린샷 또는 영상

  • before: 불필요(타입/계약 정리 작업)
  • after: 불필요(타입/계약 정리 작업)

6) 검증 증거

유형 명령어 / 증거 결과
Lint npm run lint 성공(0 error, 26 warning). 기존 lint debt만 남음
Build npm run build 실패. JetBrains Mono Google Fonts fetch 실패와 middleware deprecated warning 확인
Automated Test npx tsc --noEmit 성공
Playwright 미구축(GRC-04에서 도입 예정) 해당 없음
Browser QA 미실행(UI 동작 변경 없음) 해당 없음

7) API / 백엔드 연동 확인

  • 필요한 API 응답: /api/v1/ranking, /api/v1/users/{username}, /api/v1/auth/me
  • Mock 여부: 없음
  • 관련 backend 변경: 없음. git-ranker/docs/openapi/openapi.json과 수동 대조로 정합성 확인

8) 리스크 및 롤백

  • 리스크: 아직 generated type이 아니라 수동 동기화 단계라 backend 계약 변경 시 후속 동기화가 필요합니다. build 실패 원인인 외부 폰트 의존성은 이번 범위 밖입니다.
  • 롤백 계획: 이 PR의 단일 commit revert

9) 체크리스트

  • 연관 이슈가 연결되어 있음
  • 스크린샷 또는 영상이 포함되었거나 불필요 사유를 적음
  • Lint / Build 결과가 기입되어 있음
  • 브라우저 QA 또는 Playwright 결과가 기입되어 있음
  • API 계약 영향이 반영되었거나 없음을 명시함

Summary by CodeRabbit

  • Bug Fixes

    • Enhanced image accessibility with descriptive alt attributes.
  • Refactor

    • Consolidated tier value definitions into a shared constant for improved consistency.
    • Unified API response type definitions across the application and removed legacy interfaces.

- API 응답 계약 기준을 src/shared/types/api.ts로 통합
- validations.ts의 API 응답 중복 선언 제거
- ranking user auth 소비자를 backend OpenAPI 기준에 맞춤
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 25, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0d1c445c-abaa-4d57-8aca-c756b121ab36

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/grc-01-unify-api-contract-types

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@alexization alexization self-assigned this Mar 25, 2026
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e3925415f3

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +20 to +22
const payload = await response.json() as ApiResponse<RankingListResponse>
if (payload.result !== "SUCCESS" || !payload.data) {
return null
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Handle legacy ranking payloads in sitemap fetch

This parser now accepts only the { result, data } envelope, so any environment still returning the older shape (raw rankings/pageInfo or success) will make getRankingPage return null, which causes getTopUsers() to return an empty list and drops all dynamic user URLs from the sitemap. The repo still indicates mixed-format support via api-client's fallback (response.data?.success ?? response.data), so removing fallback handling here introduces an SEO-visible regression for those deployments.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

최신 커밋 bb10f60에서 반영했습니다. sitemap 파서를 legacy 호환 형태(result/data, success, raw payload`)까지 다시 처리하도록 바꿨고, 이로 인한 동적 사용자 URL 누락 회귀를 막았습니다.

Comment on lines +32 to +35
const payload = await response.json() as ApiResponse<RegisterUserResponse>
if (payload.result === 'SUCCESS' && payload.data) {
user = payload.data
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep backward-compatible user payload parsing for OG image

The OG image route now treats only payload.result === 'SUCCESS' with payload.data as valid, so a legacy /users/{username} response shape (e.g., success) will leave user as null and render the "User not found" fallback for real users. Since the rest of the client still explicitly supports legacy payloads, this strict parse can break social preview cards in environments that have not fully migrated.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

최신 커밋 bb10f60에서 반영했습니다. OG image 라우트도 legacy 응답 형태(result/data, success, raw payload)를 다시 허용하도록 바꿔서 실제 사용자가 User not found`로 떨어지지 않게 했습니다.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
src/app/sitemap.ts (1)

31-40: Reuse firstPageData instead of fetching page 0 twice.

getTopUsers() already has page 0 from the probe request, but the Promise.all block asks for it again. That adds one avoidable API call to every sitemap generation/revalidation.

♻️ Suggested change
-        const pages = Array.from({ length: totalPages }, (_, i) => i)
-        const responses = await Promise.all(pages.map((page) => getRankingPage(page).catch(() => null)))
+        const pages = Array.from({ length: Math.max(totalPages - 1, 0) }, (_, i) => i + 1)
+        const responses = [
+            firstPageData,
+            ...(await Promise.all(pages.map((page) => getRankingPage(page).catch(() => null)))),
+        ]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/sitemap.ts` around lines 31 - 40, The code currently fetches page 0
twice by calling getRankingPage(0) for firstPageData and then including page 0
again in the Promise.all; change the logic in getTopUsers()/sitemap generation
to reuse firstPageData: compute totalPages from firstPageData.pageInfo, build
the pages array for pages 1..totalPages-1 (not including 0), create an array of
promises where the first element is a resolved value of firstPageData and the
rest are getRankingPage(page).catch(() => null) for pages > 0, then await
Promise.all on that array so getRankingPage is not called for page 0 twice.
src/shared/constants/tier-styles.ts (1)

1-1: Keep the style maps exhaustive over Tier.

Deriving TIER_ORDER from TIER_VALUES is a good start, but the maps above are still typed as Record<Tier | string, ...>, which effectively widens them back to string and removes compile-time coverage for new tiers. The next tier addition can still compile with a missing style entry.

♻️ Suggested pattern
-export const TIER_COLOR_STYLES: Record<Tier | string, string> = {
+export const TIER_COLOR_STYLES: Record<Tier, string> = {
   ...
 }

-export const getTierColorClass = (tier: Tier | string) =>
-    TIER_COLOR_STYLES[tier] || TIER_COLOR_STYLES['IRON']
+export const getTierColorClass = (tier: string) =>
+    TIER_COLOR_STYLES[tier as Tier] ?? TIER_COLOR_STYLES.IRON

Apply the same pattern to TIER_STYLES, TIER_BADGE_STYLES, TIER_DOT_COLORS, and TIER_TEXT_COLORS.

Also applies to: 150-166

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/shared/constants/tier-styles.ts` at line 1, TIER_* maps (TIER_STYLES,
TIER_BADGE_STYLES, TIER_DOT_COLORS, TIER_TEXT_COLORS) are typed too loosely as
Record<Tier | string,...> which loses compile-time exhaustiveness; change their
types to an exact mapped type over Tier (e.g. { [K in Tier]: ... } or
Record<Tier, ...>) and construct/annotate the object literals accordingly (use
TIER_VALUES/TIER_ORDER for construction but assert the resulting object with the
mapped type) so the compiler enforces an entry for every Tier and fails when a
new Tier is added without a corresponding style. Ensure TIER_ORDER remains
derived from TIER_VALUES and apply the same strict typing pattern to the maps
referenced above.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/app/auth/callback/page.tsx`:
- Around line 50-51: The /auth/me response is assumed to be successful and
destructured directly, which can pass undefined to getUser; modify the call
sites that use apiClient.get<void, AuthMeResponse>('/auth/me') (e.g., in auth
callback and oauth2 redirect pages) to validate the response shape before
calling getUser: check that the returned object has result === 'SUCCESS' and
that response.data?.username (or the destructured username) is defined; if not,
handle the error path (reject/throw or call the existing error handler) instead
of calling getUser(undefined). Alternatively, if you prefer a centralized fix,
change the API client's response interceptor (the logic around result ===
'SUCCESS') to reject non-success 2xx responses so callers always receive a valid
success shape.

In `@src/app/users/`[username]/opengraph-image.tsx:
- Line 25: The code indexes tierColors using user.tier but user is parsed with a
cast and tier may be missing or unexpected at runtime, so replace direct
indexing with a safe fallback: compute accent = tierColors[user?.tier as keyof
typeof tierColors] ?? tierColors['default'] (or a known default key like 'free')
wherever accent is set (references: user, tierColors, accent, opengraph image
generation lines ~25 and ~41) so an unknown backend tier doesn't yield undefined
and the OG accent/border styling remains intact.

---

Nitpick comments:
In `@src/app/sitemap.ts`:
- Around line 31-40: The code currently fetches page 0 twice by calling
getRankingPage(0) for firstPageData and then including page 0 again in the
Promise.all; change the logic in getTopUsers()/sitemap generation to reuse
firstPageData: compute totalPages from firstPageData.pageInfo, build the pages
array for pages 1..totalPages-1 (not including 0), create an array of promises
where the first element is a resolved value of firstPageData and the rest are
getRankingPage(page).catch(() => null) for pages > 0, then await Promise.all on
that array so getRankingPage is not called for page 0 twice.

In `@src/shared/constants/tier-styles.ts`:
- Line 1: TIER_* maps (TIER_STYLES, TIER_BADGE_STYLES, TIER_DOT_COLORS,
TIER_TEXT_COLORS) are typed too loosely as Record<Tier | string,...> which loses
compile-time exhaustiveness; change their types to an exact mapped type over
Tier (e.g. { [K in Tier]: ... } or Record<Tier, ...>) and construct/annotate the
object literals accordingly (use TIER_VALUES/TIER_ORDER for construction but
assert the resulting object with the mapped type) so the compiler enforces an
entry for every Tier and fails when a new Tier is added without a corresponding
style. Ensure TIER_ORDER remains derived from TIER_VALUES and apply the same
strict typing pattern to the maps referenced above.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 37ef9bbf-19fc-4380-909d-284b6feed825

📥 Commits

Reviewing files that changed from the base of the PR and between e84fb97 and e392541.

📒 Files selected for processing (9)
  • src/app/auth/callback/page.tsx
  • src/app/oauth2/redirect/page.tsx
  • src/app/ranking/page.tsx
  • src/app/sitemap.ts
  • src/app/users/[username]/opengraph-image.tsx
  • src/shared/constants/tier-styles.ts
  • src/shared/lib/api-client.ts
  • src/shared/lib/validations.ts
  • src/shared/types/api.ts
💤 Files with no reviewable changes (1)
  • src/shared/lib/validations.ts

- legacy 응답 fallback을 sitemap과 OG image에 복구
- 비정상 2xx API envelope를 ApiError로 처리
- tier 스타일 맵을 exhaustive typing으로 강화
@alexization alexization merged commit 9515b1e into develop Mar 25, 2026
3 checks passed
@alexization alexization deleted the feat/grc-01-unify-api-contract-types branch March 25, 2026 01:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant