bnbongbnbong
Back to projects

codemaru

Active

GitHub 프로필 README용 개발 역량 요약 SVG 카드 생성기. GitHub, solved.ac, LeetCode, 정올 데이터를 모아 점수화하고 8단계 티어로 표현합니다.

bnbong / codemaruView on GitHub

GitHub 프로필 README용 개발 역량 요약 SVG 카드 생성기. GitHub, solved.ac, LeetCode, 정올 데이터를 모아 점수화하고 8단계 티어로 표현합니다.

PythonFastAPISVGGitHub Actionsbackend
Python4

codemaru

개요

codemaru는 개발자의 활동을 5개 축(Open Source, Impact, Consistency, Problem Solving, Depth)으로 요약해 GitHub 프로필 README에 붙일 수 있는 SVG 카드로 만들어 주는 오픈소스입니다. GitHub, Baekjoon/solved.ac, LeetCode, 정올(JungOl) 데이터를 모아 점수를 계산하고, 결과를 Seed부터 Maru까지 8단계 티어로 표현합니다.

저장소와 데모

주요 특징

  • 멀티 플랫폼 집계: GitHub, BOJ/solved.ac, LeetCode, 정올(JungOl)을 한 번에 모아 점수를 매깁니다.
  • 신뢰도 가중치: 표본이 적을 때 점수가 과대평가되지 않도록 confidence weighting을 적용합니다.
  • 테마와 레이아웃: default, dark, transparent 테마와 compact 레이아웃, 애니메이션 또는 정적 SVG 렌더를 지원합니다.
  • 웹 제너레이터: codemaru.bnbong.com에서 실시간 미리보기로 카드를 만듭니다.
  • GitHub Action 연동: 워크플로로 카드를 자동 갱신합니다. uses: bnbong/codemaru@v1로 붙입니다.
  • 한 플랫폼에 장애가 생겨도 카드는 나옵니다: 어느 플랫폼이 실패해도 카드가 깨지지 않고 partial로 표시되며, 마지막 정상 데이터를 대신 사용합니다.

설계 노트

단순한 배지 합산이 아니라 "역량을 어떻게 검증 가능한 수치로 요약할 것인가"를 핵심 과제로 삼았습니다. 플랫폼별 신호(기여, 영향력, 꾸준함, 문제 해결, 깊이)를 정규화하고 가중치를 두어, 표본이 적은 계정도 과/저평가되지 않도록 점수 분포를 다듬었습니다. FastAPI 서버가 데이터를 모아 점수를 계산하고 SVG를 렌더링하며, 결과는 README에 바로 임베드할 수 있습니다.

운영하며 고친 것

공개한 뒤에 고친 것들은 대부분 점수 공식이 아니라 "카드가 안 보인다"는 부류였습니다.

판정 플랫폼을 레지스트리로. v1.3.0에서 정올(JungOl)을 세 번째 저지로 추가했습니다. 정올의 SvelteKit 페이지가 이미 내려 주는 __data.json을 두 번 읽는 방식이라 로그인도, HTML 파싱도, 새 의존성도 필요하지 않았습니다. 다만 저지를 추가할 때마다 스코어링, 요약, CLI, 스니펫을 모두 찾아 고쳐야 하는 구조가 문제였습니다. 그래서 저지 하나가 곧 한 행에 대응하는 레지스트리 구조로 바꾸었습니다. 이제 저지를 추가하려면 행 하나와 어댑터 하나만 작성하면 됩니다.

여기서 지킨 원칙은 "연결하면 절대 떨어지지 않는다"였습니다. 신뢰도 가중치는 고정 예산을 나눠 갖는 방식이 아니라 가산 방식으로 뒀습니다. 예산을 나누어 쓰는 구조였다면 저지가 하나 늘어날 때마다 기존 사용자의 카드 점수도 함께 내려갔을 것입니다. 정올은 solved.ac와 같은 0~30 티어 척도를 쓰므로 난이도 구간과 티어 이름을 그대로 쓰되, 문제 풀이 규모 차이를 감안해 레이팅 신호에 0.80을 곱했습니다. 카드의 티어 행은 언제나 하나입니다. solved.ac 데이터가 있으면 BOJ Tier, 없으면 JungOl Tier를 보여 줍니다.

콜드 스타트. 카드 글자는 폰트를 아웃라인으로 떠서 그립니다. GitHub가 README의 SVG에서 웹폰트를 끄기 때문인데, 문제는 가변 폰트를 요청 시점에 인스턴스화하는 비용이었습니다. (패밀리, 굵기) 조합마다 200ms 남짓이 들고 카드 한 장에 다섯 개가 필요하기 때문에, 새로 기동한 서버리스 프로세스는 글자 하나를 그리기도 전에 1초 가까운 시간을 폰트 처리에 사용했습니다. 정적 인스턴스를 개발 시점에 미리 만들어 패키지에 포함하는 방식으로 바꾸었고, 새 프로세스의 첫 렌더링 시간이 약 940ms에서 약 25ms로 줄었습니다. 좌표가 정수로 반올림되는 부수 효과 덕에 카드 용량도 27% 정도 줄었습니다.

카드 빌드 전체에 예산. 어댑터 하나에만 요청 제한을 두면 GitHub 페이지네이션이 순차로 진행되기 때문에 최악의 경우 서버리스 함수 제한을 넘어섭니다. 그렇게 함수가 플랫폼 차원에서 강제로 종료되면 에러 카드도, 캐시 기록도 남지 않습니다. 그래서 수집 단계 전체에 6초 상한을 두고, 이미 끝난 플랫폼의 결과는 유지하며 남은 요청은 취소해 partial로 그립니다.

부분 실패를 부분 실패로. GitHub 저장소 목록의 2페이지 이후가 타임아웃되면 이미 받아 둔 1페이지까지 버려서, 활동이 활발한 계정이 Seed 카드로 나오는 일이 있었습니다. 지금은 1페이지를 지키고 partial로만 떨어집니다. 비슷한 맥락으로, 토큰 만료나 GitHub 장애 때문에 발생한 비200 응답을 "그런 사용자 없음"으로 캐싱해서, 장애가 지속되는 동안 요청된 핸들이 모두 존재하지 않는 사용자로 고정되던 버그도 고쳤습니다. 실제로 사용자가 없을 때만 긴 TTL을 씁니다.

저하 상태가 CDN에 남는 문제. 장애 중에 만든 stale 응답과 partial 응답에 정상 응답과 동일한 CDN 캐시 헤더를 부여해서, 플랫폼이 복구된 뒤에도 엣지에 그 카드가 오래 남았습니다. 저하 상태 응답에만 짧은 TTL을 적용하도록 구분했습니다. 이제는 하루가 아니라 1분이면 회복합니다.

카드 SVG의 CSP. 방어 차원에서 카드 SVG에 default-src 'none'을 걸었는데 img-src가 없었습니다. 카드 URL을 브라우저 탭에서 바로 열면 base64로 삽입해 둔 티어 명판 PNG가 차단되어 사라졌습니다. GitHub Camo의 SVG 정책을 따라 img-src data:만 허용하는 것으로 정리했습니다. 같은 CSP 때문에 /docs와 /redoc이 빈 화면으로 뜨는 문제도 있어서, 문서 경로에만 별도 정책을 두고 카드 엔드포인트는 엄격한 쪽을 유지했습니다.

어떤 예외도 500을 내지 않게. README에 삽입된 이미지가 500 응답을 받으면 깨진 이미지로 표시됩니다. 예상하지 못한 예외는 사용자가 확인할 수 있는 에러 카드(HTTP 200)로 응답하고, 트레이스백은 이벤트 로그로 남겨서 버그가 조용히 묻히지 않도록 했습니다.

PythonFastAPISVGGitHub Actionsbackend2026