본문 바로가기
Changelog · Dev Notes

개발 노트

F1 by 324.ing 개발 히스토리 — 매일 오전 7시 자동 갱신

34총 커밋
6업데이트 일수
10정적 항목

8월 31일 (월)

GitHub 커밋1
문서
캘린더 우승자가 빌드 429 에 굳는 문제 기록 (TODOS #6)

배포 후 실측한 것만 적었다. - /season 우승자 TBD 가 9분(90초 간격 7회) 동안 안 바뀐다. revalidate 300 창을 두 번 넘겼는데도 x-vercel-cache: HIT 만 나오고 함수 호출 흔적이 없다. - 데이터·로직 문제 아님을 확인: Jolpica /current 200(0.5초), 결과 3페이지 200, 우승자 12건 전부 파싱, 라운드 매핑도 정상. - Jolpica 가 건강한 직후 재배포해도 안 고쳐진다. 그 빌드도 429 가 127건 났다. 306 페이지를 7~11 워커가 동시에 찍으며 빌드가 스스로 레이트리밋을 만든다. - 증폭 요인: 성공한 fetch 는 캐시되어 공유되지만 실패는 캐시되지 않아, 뒤따르는 페이지마다 다시 찔러 429 를 유지시킨다.

2f33a3e

8월 27일 (목)

GitHub 커밋17
업데이트
docs(qa): SessionTabs 렌더 순수성 TODO 추가

/qa 결과: 305 라우트 점검, 4건 수정(전부 재검증), 회귀 테스트 16개 추가 (37 → 53). 리포트는 .gstack/qa-reports/ (gitignore 대상).

a640a0d
업데이트
fix(qa): ISSUE-004 — 스프린트 주말에 FP1 탭이 사라졌다

스프린트 주말은 FP1 → SQ → 스프린트 → 퀄리 → 레이스다. 없는 건 FP2·FP3 뿐인데 셋을 다 숨기고 있었다. /season/race/14/fp1 페이지도, r14-fp1-review 리뷰 글도 멀쩡히 살아있는데 세션 탭에서만 빠져 있었다(세션 일정 목록으로는 갈 수 있어 완전한 고립은 아니었다).

6fc7c1d
업데이트
fix(qa): ISSUE-003 — 시즌 진행 API 가 Jolpica 429 에 재시도 없이 500 을 냈다

이 라우트만 공용 fetch 계층을 안 거치고 raw fetch 를 썼다. Jolpica 는 연속 조회에 429 를 잘 뱉으므로 아카이브에서 지난 시즌을 몇 개 넘기면 그래프가 "Failed" 로 죽었다 — 1976 은 재현이 100% 였다. 이제 fetchWithRetry(429 재시도)를 쓰고, 남은 페이지도 batchedParallel 로 묶어 스스로 429 를 부르지 않게 했다. 같이 고친 두 가지: - 순위표 조회 실패를 치명적으로 보지 않는다. 없으면 계산된 랭킹으로 이어간다. - 200 이어도 StandingsTable 이 없으면 TypeError 로 500 이 났다. 라운드별 포인트 추이를 다 계산해 놓고 마지막에 버리던 자리라 옵셔널 체이닝으로 막았다. (회귀 테스트를 쓰다가 발견)

570a8da
업데이트
fix(qa): ISSUE-002 — 결과 API 가 429 면 이미 아는 우승자가 지워지던 것

캘린더는 로컬 항목을 뼈대로 쓰면서 winner 만 API 값으로 통째로 덮어썼다. 일정(getRaceSchedule)은 성공했는데 결과(getAllResults)가 429 로 죽으면 winnerByApiRound 가 비어 모든 라운드의 우승자가 undefined 가 되고, 캘린더에 이미 끝난 레이스가 TBD 로 떴다. 빌드 중 429 가 잦아 그 상태가 프리렌더에 그대로 굳는다. 이제 API 우승자가 있으면 그것을, 없으면 로컬 값을 쓴다. koreanName·circuitId 같은 다른 로컬 필드와 같은 규칙이다 — API 는 덮어쓰는 게 아니라 채운다.

b39d048
업데이트
test(qa): regression test for ISSUE-001 — 캘린더 라운드 정체성

실제 Jolpica 2026 응답의 문제 구간(재번호, vegas/madring/sepang 미매핑)을 그대로 재현해 8가지를 검증한다: 라운드 번호 보존, 라스베이거스 R22 생존, madring→마드리드 R16, 라운드 번호 중복 없음, 미매핑 서킷 폐기+로그, API 부분/실패 응답에서도 라운드 집합 불변, 취소 라운드 유지.

f0b2d5a
업데이트
fix(qa): ISSUE-001 — 캘린더가 API round 번호를 정체성으로 쓰던 것을 고침

Jolpica 2026 스케줄은 23경기다. 취소된 바레인·사우디를 빼고 재번호를 매기고, 매핑표에 없는 서킷(vegas, madring, sepang)은 API round 를 그대로 들고 들어와 로컬 라운드와 충돌했다. 그 결과 /season 캘린더에 R14(네덜란드/Spanish)와 R21(상파울루/Las Vegas)이 두 줄씩 뜨고, R16 이 "Bahrain Grand Prix in Malaysia" 로 덮이고, R22(라스베이거스)는 아예 사라져 /season/race/22 가 404 였다. 프로덕션에서도 같은 404 를 확인했다 — 이번 리팩토링이 만든 문제는 아니다. 이제 로컬 캘린더가 뼈대다. API 는 circuitId 로 찾아 날짜·세션시간·우승자만 채운다. 라운드 집합은 API 응답과 무관하게 항상 로컬과 같으므로, 빌드 워커마다 Jolpica 성공/실패가 갈려 라우트 집합이 달라지던 문제도 같이 사라진다 (같은 라운드가 개요는 404, 세션은 200 이던 원인). vegas → las-vegas, madring → madrid 매핑을 추가했다. 로컬 캘린더에 대응이 없는 API 서킷은 버리고 warn 으로 남긴다 — 없는 일정을 지어내 캘린더에 올리지 않는다.

5632a9d
문서
refresh TODOS after the refactor pass

Replaces the monolith-split item with its re-measurement, which was the condition it was filed under: the round page went 1369 -> 595 lines when the session tab moved to a path route, so there is little left to split. Adds the build-time 429 finding this pass surfaced. Prerendered routes went 140 -> 284 and Jolpica 429s went 82 -> 130 with it — 77 of them fetchCalendar, because build workers do not share the ISR cache. Pages still render (the calendar falls back to real static data), but it is worth fixing before it reaches data that has no fallback.

97b37e3
업데이트
refactor(format): one timing formatter, shared by the pages that need it

fmtLap and fmtGap were copied into the session page and LiveSessionDashboard. The dashboard's copy carried the same latent crash the session page hit last commit — it called toFixed on a value OpenF1 can return as "+1 LAP" — so merging fixes it there too. lib/format.ts now owns toNumber/fmtLap/fmtGap and the TimingValue type, and format.test.ts (moved from openf1-format.test.ts) tests the real module rather than a copy of its logic. of1get in the session page stays where it is, with a comment explaining why it is not the same thing as fetchOpenF1: that one throws, this one absorbs failures into an empty array. The page fans out to eight endpoints and must still render when one is missing — an FP session has no starting_grid — and with 120 of these routes prerendered, throwing would turn one transient failure into a failed build. Merging them would mean giving openf1.ts an absorbing variant, not bending this page onto the throwing one.

3a76839
업데이트
refactor(api): share one fetch layer, with per-upstream retry policy

Retry lived only in jolpica.ts; openf1.ts and fastf1.ts threw on the first non-ok response. Rather than copy the same loop three times, the policy moves to lib/api/http.ts and each client declares what is worth retrying — the three upstreams do not fail alike: Jolpica 429 → retry with backoff; a burst usually succeeds shortly after OpenF1 429 → same, but 403 (live-session paywall) is NOT retried, because it stays 403 and only burns time FastF1 502/503/504 → retry twice for Railway cold starts; 202 means "still computing" and is res.ok, so it passes through fetchChampionshipProgress also now uses the shared batcher. Its comment claimed "batched, 3 at a time" while the code was Promise.all over every completed round — 12 simultaneous requests today, 23 by season end. The existing jolpica tests pass unchanged, which is the point: behaviour is preserved. http.test.ts adds the policy cases, including that a 403 is tried exactly once and that batchedParallel never exceeds its concurrency. Note: this does not reduce build-time 429s. Those went 82 -> 130 because prerendered routes went 140 -> 284 — more pages, more calls, and build workers do not share the ISR cache. The calendar falls back to real static data so pages still render correctly.

d347d72
업데이트
fix(proxy): allowlist the fastf1 proxy instead of forwarding any path

The catch-all joined the URL segments and forwarded them to the Python service with no check, on a route with no auth and maxDuration = 60. Any GET the service exposes was publicly reachable, and anything added there later would open silently along with it. Outsiders could also hold 60-second Railway requests on our bandwidth. Allows the 12 endpoints the client actually calls plus the read-only ones lib/api/fastf1.ts uses, and 404s everything else — /admin/cache-upload and /cron/generate-replay stay unreachable (they are POST, which this route never handled, but the allowlist makes that explicit rather than incidental). allowlist.test.ts scans the source for proxy calls and fails when one is missing from the list, so a new screen hitting a new endpoint surfaces as a test failure instead of a 404 someone has to find in the network tab.

acc5a34
업데이트
fix(archive): stop hardcoding the season upper bound at 2025

progress/route.ts rejected any year above 2025 with a 400, so the whole of the 2026 season was unreachable through it. archive/page.tsx built its year list with the same literal, which is the only reason nobody hit the 400 — the UI never offered 2026. Both now derive the bound from the current year, so a new season appears without a code change.

d368f92
업데이트
fix(session): handle OpenF1 gap fields that aren't numbers

The session page typed gap_to_leader and duration as number | null, but OpenF1 returns an array for qualifying-type sessions ([Q1, Q2, Q3]) and a string like "+1 LAP" for lapped cars. Calling toFixed on the string threw. This was live before, not new: the route was force-dynamic, so the crash hit users opening a race session view rather than failing a build. Prerendering it (previous commit) turned a silent per-request 500 into a build failure at /season/race/3/race. Adds toNumber() to normalise array/string/number, widens the type to match what the API actually sends, and routes the gap cell through fmtGap so a lapped car shows "+1 LAP" instead of crashing. openf1-format.test.ts pins the shapes so the next regression fails in 200ms instead of in a six-minute deploy.

23843f8
업데이트
fix(data): stop serving stale standings when the API fails

f1-data.ts's driverStandings is a snapshot from just after Round 1 — Russell 25, Antonelli 18. live.ts returned it whenever Jolpica threw or came back empty, so an API failure in late August showed March standings as if they were current, 13 rounds out of date, with nothing on screen to say so. Only console.warn recorded it, and a production build was already hitting Jolpica 429s. Standings now return null on failure and [] on a successful-but-empty response, so callers can tell the two apart. /season renders an explicit "순위표를 불러오지 못했습니다"; the home page drops the section rather than putting an error box in the middle of a scrolling page. The calendar keeps its static fallback — mockCalendar is hand-maintained real 2026 schedule data, not a stale snapshot, so falling back to it is correct. That distinction is the whole point: remove the fallback that lies, keep the one that doesn't.

edb1e60
업데이트
test: add vitest and cover the data layer's riskiest paths

First tests in the repo. Vitest only — no coverage reporter or DOM environment until something needs them. jolpica.test.ts covers the 100-row pagination fix: that offset advances until total is reached, that a round straddling a page boundary has its results merged rather than duplicated, that rounds sort numerically ("10" after "2"), and that an empty page stops the loop instead of spinning. Also the 429 path: retry-then-succeed, exhaust-then-throw (1 + 4 attempts), and that non-429 errors throw immediately without retrying. Fake timers keep the backoff from making the suite slow — 5.99s to 172ms. live.fallback.test.ts pins the CURRENT fallback contract rather than the one we want: a failed or empty upstream silently returns mock standings and only console.warn records it. Pinning it now means the T3 change to explicit failure shows up as a test diff instead of slipping through.

9fc1fd4
업데이트
perf(render): move the session tab to a path route so GP pages prerender

The round page read searchParams to drive its ?session= tab, which makes the whole route dynamic in the App Router — revalidate = 300 and generateStaticParams were both being ignored. Measured against prerender-manifest.json: /season/race/N appeared zero times, and so did every session view. The two most-visited pages were server-rendered on every request. The session view was also implemented twice. The round page's SessionDataView and the [session] route hit the same eight OpenF1 endpoints (session_result, drivers, laps, stints, pit, race_control, weather, starting_grid) and rendered the same sections. This deletes the round page's copy and keeps the path route. Ported the one refinement only the deleted copy had: fetch session_result first and drop the rest to a 60s ttl when it comes back empty, so a session that has just ended does not bake an empty response into a 24h cache. generateStaticParams now filters to combinations that actually have a schedule — sprint weekends have no fp2/fp3, regular weekends no sq/sprint, so 48 of the 168 permutations were built only to call notFound(). SessionTabs moves to a shared component and now renders on the session page too, so tab navigation no longer dies when you open a session. Prerendered routes 140 -> 284: /season/race/N 0 -> 24, session views 0 -> 120. Round page 1369 -> 595 lines. URLs change from ?session=qualifying to /qualifying; all internal links were updated.

11ec6d1
문서
add TODOS.md with items deferred by the engineering review

Three items, each with reproduction context and dependencies: CI absence (no .github, vercel git deploys disabled), /api/posts?round=N returning 500 for a missing Firestore composite index, and the GP page monolith split — which should be re-measured after the session tab moves to a path route, since that will shrink the file substantially.

80cbc24
업데이트
perf(bundle): render info/history on the server, ship only the tab toggle

Both pages were client components importing heavy data modules, so the whole payload was bundled for the browser even though neither page renders it. /info imported allSections (~400KB of regulation article text) to display a section list that only reads 7 metadata fields per section; /history imported f1-data (92KB) + f1-eras (52KB) + f1-champions (16KB) for a 3-tab view. Measured: a 332KB client chunk carried the regulation body text. Static chunks 2.8M -> 2.4M, and that chunk is gone entirely. New TabSwitcher holds the useState and receives server-rendered panels as props, so the data modules stay on the server. Both pages remain prerendered (140 routes, unchanged) and the URLs and tab behaviour are untouched. Each page carries a comment explaining why it must not go back to "use client".

e1431f1

8월 26일 (수)

GitHub 커밋16
업데이트
feat(reviews): add --update to republish an existing review in place

Resolves the post id from the slug page's canonical link and PATCHes it, so rewriting an article keeps its URL and slug.

02e0439
업데이트
feat(reviews): session/round review posts linked from GP pages

Official review articles live in the community as admin posts with the slug convention r{round}-{key}-review (key: fp1|fp2|fp3|sq|sprint| qualifying|race|round). getRoundReviews() queries by slug, the GP page shows a 세션 리뷰 button row on the overview tab and a per-session button in each session tab — buttons appear automatically once the matching post exists. scripts/post-review.py publishes an article markdown file via POST /api/posts (duplicate-safe by slug).

6c0c2b8
업데이트
fix(api): HEAD-check R2 object before issuing presigned replay redirect

generate_presigned_url signs blindly even for missing keys, so a cache miss 307'd clients to a NoSuchKey 404 instead of falling through to compute/503.

0bb2f31
업데이트
fix(season): map local round numbers to Jolpica API rounds when races are cancelled

Cancelled rounds (Bahrain/Saudi) drop out of Jolpica's season numbering, so local round N is API round N minus cancelled-before-N. Standings queries now use the API round while the UI keeps the local number.

888f8cf
업데이트
fix(api): retry Jolpica 429s with backoff, paginate past 100-row results cap

limit=1000 silently returns only the first 100 rows (rows, not races), so later rounds vanished. Paginate by offset and merge rounds that straddle page boundaries. Also retry 429s with jittered backoff so parallel build workers don't collapse pages to mock data.

9862adb
업데이트
feat(community): add Firebase-backed community layer

Chat, posts, comments, likes, predictions with leaderboard, bot badge, auth context — components and Firestore/RTDB client libs referenced by earlier community commits but never added to the index.

cc16982

3월 8일 (일)

기능

관리자 대시보드 — 레이스 결과 수동 입력 & 메타태그 관리

  • 관리자 대시보드 신설 (/admin/dashboard): 코드 삽입·메뉴 관리·메타태그·서킷 코너·레이스 결과 5개 섹션
  • 레이스 결과 수동 입력: 스프린트/메인 레이스 독립 폼, Jolpica API 미업데이트 시 즉시 반영 가능
  • 스프린트 주말 지원: 스프린트·메인 레이스 서브탭으로 분리, 각각 독립 API 호출 (isSprint 플래그)
  • F1 포인트 계산 유틸 (f1-points.ts): 레이스 25-18-15...+FL보너스, 스프린트 8-7-6-5-4-3-2-1
  • f1-data.ts 자동 업데이트: 드라이버·컨스트럭터 스탠딩, 캘린더 completed+winner, 커리어 통계 regex 치환
  • Telegram 알림 + 비동기 빌드·배포 (exec npm run build && vercel --prod)
  • 메타태그 관리 탭 추가: 사이트 타이틀, 타이틀 템플릿, description, OG Title/Description/Image, 키워드
  • layout.tsx generateMetadata와 admin-config.json meta 필드 연동 — 빈 값 시 기본값 fallback

3월 6일 (금)

기능

핵심 페이지 & 기능 전체 구현

  • 메인 홈 페이지: 다음 레이스 히어로, 챔피언십 순위, AI 브리핑, 뉴스 피드, 시즌 캘린더
  • 드라이버 페이지: 22명 전체 아카이브, 개인 상세 페이지 (커리어 통계, 시즌 결과)
  • 팀 페이지: 11개 팀 (캐딜락 포함) 목록 및 상세 (역대 순위, 챔피언십 기록)
  • 서킷 페이지: 24개 서킷 목록 및 상세 (사양, 랩 레코드, 역대 우승자, 트랙 맵)
  • 시즌 트래커: 드라이버/컨스트럭터 챔피언십 표, 레이스 캘린더, 다음 레이스 세션 일정
  • 뉴스 허브: 실시간 F1 뉴스 피드 (RSS 집계), AI 데일리 브리핑
  • 정보 허브 (info): 2026 주요 변경사항 카드 + FIA 공식 규정 전문 한국어 번역
데이터

2026 F1 공식 규정 전문 한국어 번역 완료

  • Section A: 기술 규정 일반사항 (T0 기준 원문 전문)
  • Section B: 파워유닛 기술 규정 (PU 구조, MGU-K, 배터리, 연료 시스템)
  • Section C: 차량 기술 규정 (공기역학, 차체 치수, 언더바디, 서스펜션)
  • Section D: 재정 규정 (코스트 캡 $135M, 감사 절차, 제재 체계)
  • Section F: 운영 규정 (공장 가동 중단, 공기역학 시험 제한, ATR·RWTT·RCFD)
  • FIA 공식 HTML 원문 파싱 → TypeScript 데이터 구조 변환 (RegArticle 인터페이스)
디자인

정보 허브 페이지 리디자인 — 주요 변경사항 카드

  • 2026 주요 변경사항 8→9개 항목으로 확장 (오버테이크 모드 & 부스트 버튼 추가)
  • 각 항목에 초보자용 한국어 설명(beginner) 필드 신규 추가
  • 핵심 변경사항(impact:high) 4건 → 글로우 효과 대형 카드 (2열 그리드)
  • 추가 변경사항(impact:medium) 5건 → 컴팩트 카드 (3열 그리드)
  • 카드별 컬러 테마, 주요 수치(stat), '💡 쉽게 말하면' 설명 박스
  • 주요 변경사항 섹션을 페이지 최상단으로 이동, 규정 전문은 하단 배치
기능

GP 개별 상세 페이지 신설 (/season/race/[round])

  • 라운드별 GP 상세 페이지: 24개 라운드 정적 생성 (generateStaticParams)
  • 히어로 섹션: GP명, 서킷 스펙 요약 4종, 우승자(완료)/D-day 카운트다운(예정)
  • 세션 일정 (KST): FP1~3·스프린트·퀄리파잉·레이스 전체, 완료 상태 표시
  • 결승 결과 테이블: 순위, 드라이버, 팀, 그리드, 기록/상태(DNF 빨간색), 포인트, FL 뱃지
  • 퀄리파잉 결과 테이블: 순위, Q1/Q2/Q3 타임
  • 스프린트 결과 테이블: 스프린트 주말 한정 표시
  • live.ts에 fetchRaceResult·fetchQualifyingResult·fetchSprintResult 추가
  • 시즌 캘린더 및 홈 캘린더 링크 → /season/race/[round] 로 변경
인프라

GitHub 배포 및 Vercel 프로덕션 배포

  • GitHub 저장소 nohdaeyoung/f1_portal 원격 연결 및 초기 push
  • Vercel CLI로 프로덕션 배포 완료 (f1-delta-flame.vercel.app)
  • Vercel ↔ GitHub 자동 배포 파이프라인 연결 — 이후 push 시 자동 재배포
  • Next.js 16.1.6 (Turbopack) 빌드 성공, 전체 페이지 정적 생성 확인
기능

라이브 세션 대시보드 & AI 다이제스트 자동 갱신

  • 라이브 세션 대시보드 신설 (/api/live/session → LiveSessionDashboard 컴포넌트)
  • 카운트다운 타이머: 다음 세션까지 실시간 D-day/H:M:S 표시
  • 홈 페이지 라이브 세션 인디케이터 및 세션 종료 후 결과 데이터 자동 표시
  • 홈 세션 타임테이블 → 완료 세션은 결과 페이지로 링크 연결
  • OpenF1 date_end 기반 라이브 세션 감지 로직 수정 (종료 후 유령 live 상태 방지)
  • AI 다이제스트 크론 KST 01시·12시 이중 갱신 (UTC 16:00·03:00) — 별도 엔드포인트 분리
  • Claude max_tokens 2048→4096 (JSON 잘림 방지), 응답 파싱 regex 추출 방식으로 개선
  • 크론→warm-digest 분리 호출 구조로 revalidateTag 순서 문제 해결
기능

SEO/GEO 최적화 & FastF1 텔레메트리 분석 페이지

  • sitemap.ts, robots.ts 추가 (250+ URL 자동 생성)
  • 전체 페이지 Open Graph + Twitter Card 메타데이터 적용
  • JSON-LD 구조화 데이터 추가 (홈, 드라이버, 팀, 서킷, 레이스) — lib/jsonld.ts
  • layout.tsx metadataBase + title.template 전역 설정
  • FastF1 Python 서버 연동 → /api/fastf1/[...path] 프록시 API 라우트
  • 텔레메트리 분석 페이지 신설 (/season/race/[round]/analysis): 랩 분석, 속도맵, 전략 탭
  • FP1~3 결과 상세화: 헤드샷, 섹터 타임, 타이어 화합물, 속도 트랩 컬럼 추가
  • F1 역사 시대별 상세 페이지 신설 (/history/era/[slug]) — 시대 카드에서 링크 연결
  • 예정 세션 페이지 404 수정 → 미리보기 UI(세션 일정 안내) 표시로 변경
  • 메인 메뉴 '정보' → '규정' 레이블 변경, dev 서버 포트 4000으로 통일
디자인

디자인 시스템 전면 개편 (Phase A-C) & 성능 최적화

  • Phase A: CSS 디자인 토큰 30개 정의, UI 공통 컴포넌트 (Button, Card, SectionHeader, Badge) 신설, GNB 현재 페이지 활성 인디케이터
  • Phase B: Inter 폰트 적용, 모바일 햄버거 드로어 메뉴, F1 전용 컴포넌트 (PodiumBadge, CompoundBadge, TeamColorBar) 추가
  • Phase B: 세션 결과 테이블 — 헤더/좌측 컬럼 sticky, 스켈레톤 로딩 UI
  • Phase C: 홈 page.tsx → 8개 독립 컴포넌트 분리 (NextRaceHero, RaceWeekendHero, ChampionshipsSection 등)
  • Phase C: 전체 페이지 CSS 토큰 마이그레이션, WCAG 명도 대비 수정
  • 성능: 홈 force-dynamic → ISR revalidate:300 (CDN 캐시 활용)
  • 성능: fetchCalendar/fetchDriverStandings/fetchConstructorStandings unstable_cache 적용
  • 성능: 완료 세션 페이지 revalidate:86400 (기존 60s에서 개선)
  • GNB 개발노트 항목 제거, 푸터 GitHub 링크 정리

3월 5일 (목)

초기화

프로젝트 초기 설정

  • Next.js 15 App Router 기반 프로젝트 생성 (create-next-app)
  • TypeScript + Tailwind CSS v4 환경 구성
  • 프로젝트명 PitLane — 2026 F1 종합 포털로 방향 설정
  • Vercel 배포 파이프라인 연결 (nohdaeyoung/f1_portal)
  • Jolpica F1 API (Ergast 호환) 및 OpenF1 API 클라이언트 설계
🏎

2026년 3월 — F1 by 324.ing 프로젝트 시작