Articles

팀 프로젝트 기여도를 대화로 풀어 주는 서비스를 만든 이유

WhoIsLoafing이 어떤 문제에서 출발했고, 레포 링크 하나가 분석 결과가 되기까지 어떤 단계를 거치는지 전체 구조를 정리했어요.

팀 프로젝트가 끝나면 꼭 나오는 질문이 있어요. "그래서 누가 뭘 했지?" 답은 이미 GitHub에 다 남아 있는데, 막상 확인하려면 커밋 수백 개를 하나씩 열어 봐야 해요. 저희 숨은빚청년들 팀이 WhoIsLoafing을 만든 이유예요.

이 글에서는 서비스가 무엇을 하는지와, 레포 링크 하나가 분석 결과가 되기까지의 과정을 처음부터 끝까지 훑어볼게요. 뒤에 이어지는 글들은 각 단계에서 부딪힌 문제를 하나씩 자세히 다뤄요.

무엇을 하는 서비스인가요

공개 GitHub 레포 링크를 넣으면 아래 내용을 채팅 형식으로 풀어서 보여줘요.

  • 어떤 서비스의 어떤 레포인지 한 문장 정의와 기술 스택
  • 참여자별 커밋 수, 추가하고 삭제한 라인 수, 기여도
  • 참여자마다 맡은 기능 목록과 코드 스타일
  • 활동 시간대, 진행 흐름, 커밋 메시지 규칙, PR 현황 같은 추가 질문의 답

로그인은 없어요. 공개 레포만 분석하고, 비공개 레포는 서버 토큰으로 볼 수 있더라도 결과를 내보내지 않아요.

숫자만 보여주면 안 되는 이유

GitHub의 Insights 탭에도 기여자별 커밋 수와 라인 수가 나와요. 그런데 그 숫자만으로는 알 수 있는 것이 별로 없어요.

  • 패키지 잠금 파일 하나가 수천 줄을 차지해서 라인 수가 부풀어요.
  • 같은 사람이 이메일 두 개로 커밋하면 두 사람으로 나뉘어요.
  • 커밋 30개가 무엇을 만든 것인지는 숫자가 말해 주지 않아요.

그래서 목표를 숫자를 보여주는 것이 아니라 숫자가 뜻하는 바를 설명하는 것으로 잡았어요. 숫자는 직접 계산하고, 그 숫자와 커밋 내용을 AI에 넘겨서 "누가 무엇을 맡았는지"를 사람이 읽는 문장으로 받아요.

전체 흐름

레포 링크가 들어오면 서버는 아래 순서로 움직여요.

  1. 레포 확인: 링크가 GitHub 레포 주소인지 판별하고, 레포가 있는지와 공개 레포인지 확인해요.
  2. 레포 소개: README, 폴더 구조, 사용 언어, 의존성 파일을 읽어서 한 문장 정의와 기술 스택을 만들어요.
  3. 메뉴: 무엇부터 볼지 물어봐요. 기여도 분석을 바로 시작할 수도 있고, 궁금한 질문만 골라서 볼 수도 있어요.
  4. 수치 수집: GitHub 통계 API로 사람별 커밋 수와 라인 수를 가져와요. 잠금 파일을 빼고 계산하기를 고르면 커밋을 하나씩 읽어요.
  5. 사람 합치기: 여러 이메일로 커밋한 사람을 GitHub 계정 기준으로 합치고, 병합 커밋과 봇은 빼요.
  6. 순위: 커밋 수 기준과 라인 수 기준 기여도를 각각 계산해요.
  7. 기능과 코드 스타일: 참여자마다 대표 커밋을 골라 diff를 AI에 보내고, 맡은 기능과 코드 스타일을 목록으로 받아요.
  8. 전체 비교: 두 가지 기준의 기여도를 막대 그래프로 나란히 보여줘요.

각 단계의 결과는 준비되는 대로 한 말풍선씩 브라우저로 흘려보내요. 분석이 다 끝날 때까지 빈 화면을 보여주지 않으려는 선택이에요.

기술 스택

영역 선택
프론트엔드 React, TypeScript, Vite, Tailwind CSS
서버 Express와 Cloudflare Workers에서 같은 로직을 실행
저장소 Supabase(Postgres)
AI Claude API, 구조화된 출력으로 JSON을 받고 zod로 검증
외부 API GitHub REST API

GitHub와 Claude 호출은 모두 서버에서 해요. 키와 토큰이 브라우저로 나가지 않게 하려는 것이에요.

기여도를 하나의 숫자로 만들지 않았어요

가장 오래 고민한 부분이에요. "기여도 37%"처럼 숫자 하나로 보여주면 보기에는 깔끔하지만, 그 숫자를 어떻게 만들든 누군가에게는 불공평해요.

  • 커밋 수로 계산하면 작게 자주 올리는 사람이 유리해요.
  • 라인 수로 계산하면 화면 코드를 많이 쓰는 사람이 유리해요.

그래서 커밋 수 기준과 라인 수 기준을 따로 계산해서 나란히 보여주기로 했어요. 소개 순서만 두 값의 평균으로 정해요.

const percent = (part: number, total: number) =>
  total > 0 ? Math.round((part / total) * 1000) / 10 : 0

people
  .map((p) => ({
    ...p,
    commitShare: percent(p.commits, totalCommits),
    lineShare: percent(p.additions + p.deletions, totalLines),
  }))
  .sort((a, b) => b.commitShare + b.lineShare - (a.commitShare + a.lineShare))

두 숫자가 크게 어긋나는 사람이 있으면 그 자체가 읽을거리가 돼요. 한 번에 크게 올리는 사람인지, 자잘한 수정을 주로 맡은 사람인지가 드러나기 때문이에요.

사람을 평가하는 도구가 되지 않게

서비스 이름이 "누가 놀고 있나요"라서 오해받기 쉬운데, 누군가를 지목하려고 만든 도구는 아니에요. 그래서 몇 가지 원칙을 정했어요.

  • 기여도 숫자는 항상 맡은 기능 목록과 함께 보여줘요.
  • AI에게 보내는 지시문에 "사람을 깎아내리거나 평가하지 않고 사실을 담백하게 전해요"라고 적어 뒀어요.
  • 자료에서 확인되는 내용만 말하게 해요. 당사자가 직접 읽는 글이라 틀린 내용은 바로 드러나기 때문이에요.

기획, 디자인, 회의처럼 커밋에 남지 않는 기여는 이 서비스가 볼 수 없어요. 이 한계를 숨기지 않는 것도 원칙 중 하나예요.

다음 글에서

여기까지가 전체 그림이에요. 이어지는 글에서는 각 단계에서 만난 구체적인 문제를 다뤄요. GitHub 통계 API가 결과 대신 "아직 계산 중"이라고 답할 때 어떻게 했는지부터 시작할게요.

다음 글 GitHub 통계 API가 200 대신 202를 돌려줄 때