GitHub과 연동되어 AI가 자동으로 코드 리뷰를 제공하고, 팀원들이 실시간으로 협업할 수 있는 개발자 협업 플랫폼
- 주니어 개발자들은 제대로 된 코드 리뷰를 받기 어려움
- 소규모 팀에서는 시니어 개발자 부족으로 리뷰가 지연됨
- 코드 리뷰 과정에서 비효율적인 커뮤니케이션 발생
CodeMate는 AI를 활용한 자동 코드 리뷰와 실시간 협업 기능으로 이 문제를 해결합니다.
- 🎓 주니어 개발자 (코드 리뷰 학습)
- 🚀 소규모 스타트업 팀 (시니어 개발자 부족)
- 📦 오픈소스 프로젝트 관리자
- 💻 코딩 부트캠프 수강생
- GitHub OAuth 로그인
- Repository 선택적 연동
- PR 생성 시 자동 Webhook 트리거
- PR Diff 자동 분석
- 버그 가능성 탐지 (Null pointer, Race condition 등)
- 성능 이슈 분석 (불필요한 리렌더링, 메모리 누수)
- 보안 취약점 검사 (SQL Injection, XSS)
- 베스트 프랙티스 제안 (디자인 패턴, 코딩 컨벤션)
- 코드 품질 점수 산정 (0-100점)
- 코드 라인별 댓글
- 팀원 멘션 (@username)
- 댓글 스레드 (답글 기능)
- 실시간 타이핑 인디케이터
- 읽음 표시
- 이모지 반응 (👍❤️🎉)
- PR별 품질 점수 추이
- 팀원별 리뷰 참여율
- 가장 많이 발견되는 이슈 TOP 5
- 평균 리뷰 응답 시간
- Repository별 통계
Next.js 15 - React 프레임워크
TypeScript - 타입 안정성
Tailwind CSS - 스타일링
shadcn/ui - UI 컴포넌트
Recharts - 데이터 시각화
Zod - 스키마 검증
Next.js API Routes - RESTful API
Next.js Server Actions - 서버 사이드 로직
Prisma ORM - 데이터베이스 ORM
PostgreSQL - 메인 데이터베이스
Socket.io - WebSocket 서버
Claude API (Anthropic) - AI 코드 분석
GitHub API - Repository & PR 데이터
GitHub OAuth - 인증
GitHub Webhooks - 이벤트 트리거
Vercel - 호스팅 & 배포
Supabase - PostgreSQL 호스팅
AWS S3 - 파일 저장소
GitHub Actions - CI/CD
Docker - 컨테이너화
ESLint - 코드 린팅
Prettier - 코드 포맷팅
Jest - 단위 테스트
Sentry - 에러 모니터링
┌─────────────────────────────────────────────────────┐
│ Client Browser │
│ ┌──────────────┐ ┌──────────────┐ ┌────────────┐ │
│ │ Next.js │ │ WebSocket │ │ GitHub │ │
│ │ UI │ │ Client │ │ OAuth │ │
│ └──────┬───────┘ └──────┬───────┘ └─────┬──────┘ │
└─────────┼──────────────────┼────────────────┼────────┘
│ │ │
▼ ▼ ▼
┌─────────────────────────────────────────────────────┐
│ Next.js Server (Vercel/Local) │
│ │
│ ┌────────────────┐ ┌──────────────────────┐ │
│ │ API Routes │◄────►│ Server Actions │ │
│ │ /api/review │ │ (Direct DB Access) │ │
│ │ /api/webhook │ └──────────┬───────────┘ │
│ │ /api/github │ │ │
│ └────────┬───────┘ │ │
│ │ ▼ │
│ │ ┌─────────────────────┐ │
│ └─────────────►│ Prisma ORM │ │
│ └──────────┬──────────┘ │
│ │ │
│ ┌────────────────┐ │ │
│ │ Socket.io │ │ │
│ │ Server │ │ │
│ └────────────────┘ │ │
└──────────┬──────────────────────────┼──────────────┘
│ │
▼ ▼
┌─────────────────┐ ┌──────────────────┐
│ WebSocket │ │ PostgreSQL │
│ Clients │ │ (Supabase) │
└─────────────────┘ └──────────────────┘
┌──────────────┐ ┌──────────────┐
│ Claude API │ │ GitHub API │
│ (Anthropic) │ │ & Webhooks │
└──────────────┘ └──────────────┘
┌──────────────┐ ┌──────────────┐
│ User │1 *│ Repository │
│──────────────│─────────│──────────────│
│ id │ │ id │
│ email │ │ githubId │
│ name │ │ name │
│ githubId │ │ fullName │
│ githubToken │ │ userId │
└──────────────┘ └──────┬───────┘
│ │
│ │1
│ │
│ │*
│ ┌──────┴───────┐
│ │ PullRequest │
│ │──────────────│
│ │ id │
│ │ githubId │
│ │ number │
│ │ title │
│ │ status │
│ │ repoId │
│ └──────┬───────┘
│ │1
│ │
│ │*
│1 ┌──────┴───────┐
│ │ Review │
│ │──────────────│
│ │ id │
│ │ prId │
│ │ aiSuggestions│
│ │ qualityScore │
│ │ severity │
│ └──────────────┘
│
│* ┌──────────────┐
└──────────│ Comment │
│──────────────│
│ id │
│ content │
│ lineNumber │
│ prId │
│ authorId │
│ parentId │
└──────────────┘
model User {
id String @id @default(cuid())
email String @unique
name String?
image String?
githubId Int? @unique
githubToken String? @db.Text
repositories Repository[]
comments Comment[]
notifications Notification[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Repository {
id String @id @default(cuid())
githubId Int @unique
name String
fullName String
isActive Boolean @default(true)
webhookId Int?
owner User @relation(fields: [userId], references: [id])
userId String
pullRequests PullRequest[]
createdAt DateTime @default(now())
@@index([userId])
}
model PullRequest {
id String @id @default(cuid())
githubId Int @unique
number Int
title String
description String? @db.Text
status PRStatus
repo Repository @relation(fields: [repoId], references: [id])
repoId String
reviews Review[]
comments Comment[]
createdAt DateTime @default(now())
@@index([repoId, status])
}
enum PRStatus {
OPEN
CLOSED
MERGED
DRAFT
}
model Review {
id String @id @default(cuid())
pullRequest PullRequest @relation(fields: [pullRequestId], references: [id])
pullRequestId String
aiSuggestions Json
qualityScore Int
severity Severity
issueCount Int @default(0)
status ReviewStatus @default(PENDING)
reviewedAt DateTime @default(now())
@@index([pullRequestId])
}
enum Severity {
LOW
MEDIUM
HIGH
CRITICAL
}
model Comment {
id String @id @default(cuid())
content String @db.Text
lineNumber Int?
filePath String?
isResolved Boolean @default(false)
pullRequest PullRequest @relation(fields: [pullRequestId], references: [id])
pullRequestId String
author User @relation(fields: [authorId], references: [id])
authorId String
parentId String?
parent Comment? @relation("CommentThread", fields: [parentId], references: [id])
replies Comment[] @relation("CommentThread")
mentions String[]
reactions Json?
createdAt DateTime @default(now())
@@index([pullRequestId])
}- Node.js 18+
- npm 또는 yarn
- PostgreSQL (또는 Supabase 계정)
- GitHub 계정
- Anthropic API Key
# 1. Next.js 프로젝트 생성
npx create-next-app@latest codemate \
--typescript \
--tailwind \
--app \
--use-npm
cd codemate
# 2. 필수 패키지 설치
npm install @prisma/client prisma
npm install next-auth @auth/prisma-adapter
npm install @anthropic-ai/sdk
npm install socket.io
npm install @octokit/rest
npm install zod
npm install recharts lucide-react
npm install date-fns
# 3. 개발 도구
npm install -D @types/node
npm install -D prisma
npm install -D eslint-config-prettier
# 4. UI 라이브러리 (shadcn/ui)
npx shadcn@latest init
npx shadcn@latest add button card input textarea
npx shadcn@latest add dropdown-menu avatar badge.env.local 파일 생성:
# Database
DATABASE_URL="postgresql://username:password@localhost:5432/codemate"
DIRECT_DATABASE_URL="postgresql://username:password@localhost:5432/codemate"
# NextAuth
NEXTAUTH_URL="http://localhost:3000"
NEXTAUTH_SECRET="your-secret-key-here" # openssl rand -base64 32
# GitHub OAuth
GITHUB_ID="your-github-oauth-app-id"
GITHUB_SECRET="your-github-oauth-app-secret"
# Anthropic API
ANTHROPIC_API_KEY="sk-ant-xxxxx"
# GitHub Webhook
GITHUB_WEBHOOK_SECRET="your-webhook-secret"
# WebSocket
NEXT_PUBLIC_WS_URL="http://localhost:3000"
# AWS S3 (Optional)
AWS_ACCESS_KEY_ID="your-access-key"
AWS_SECRET_ACCESS_KEY="your-secret-key"
AWS_REGION="ap-northeast-2"
AWS_S3_BUCKET="codemate-files"- GitHub Settings → Developer settings → OAuth Apps
- New OAuth App 클릭
- 설정값:
- Application name:
CodeMate Dev - Homepage URL:
http://localhost:3000 - Authorization callback URL:
http://localhost:3000/api/auth/callback/github
- Application name:
- Client ID와 Client Secret을
.env.local에 추가
# Prisma 초기화
npx prisma init
# schema.prisma 파일 작성 후
npx prisma migrate dev --name init
# Prisma Client 생성
npx prisma generate
# (Optional) Prisma Studio로 DB 확인
npx prisma studioSupabase pooler URL을 DATABASE_URL로 쓰는 경우:
DIRECT_DATABASE_URL에는 direct connection URL을 따로 넣어두는 것을 권장합니다.
런타임 쿼리는 pooler로 처리해도 되지만, Prisma migration은 direct URL이 더 안정적입니다.
npm run dev브라우저에서 http://localhost:3000 접속
E2E는 production 데이터와 분리된 codemate_e2e PostgreSQL만 사용합니다.
아래 예시는 PowerShell 기준입니다.
docker compose -f docker-compose.e2e.yml up -d --wait
npx playwright install chromium
$env:E2E_TEST_MODE = "1"
$env:DATABASE_URL = "postgresql://postgres:postgres@localhost:5433/codemate_e2e"
$env:DIRECT_DATABASE_URL = $env:DATABASE_URL
$env:AUTH_URL = "http://localhost:3000"
$env:NEXTAUTH_URL = $env:AUTH_URL
$env:AUTH_SECRET = [guid]::NewGuid().ToString()
$env:NEXTAUTH_SECRET = [guid]::NewGuid().ToString()
$env:GITHUB_ID = "e2e-client-id"
$env:GITHUB_SECRET = [guid]::NewGuid().ToString()
npm run test:e2etest:e2e는 실행 전에 대상 DB가 localhost의 codemate_e2e인지 검사한 뒤
해당 전용 DB만 초기화합니다. 테스트 종료 후 컨테이너는 다음 명령으로 정리합니다.
docker compose -f docker-compose.e2e.yml down목표: Next.js 프로젝트 셋업 및 GitHub OAuth 인증 구현
- Next.js 15 프로젝트 생성
- TypeScript 설정
- Tailwind CSS 설정
- Prisma 스키마 작성
- PostgreSQL 연결 확인
- NextAuth.js 설정
- GitHub OAuth 연동
- 로그인/로그아웃 UI
- 사용자 세션 관리
- Protected Routes 구현
app/
├── api/
│ └── auth/
│ └── [...nextauth]/route.ts
├── (auth)/
│ ├── login/page.tsx
│ └── layout.tsx
├── layout.tsx
└── page.tsx
lib/
├── auth.ts
├── prisma.ts
└── utils.ts
prisma/
└── schema.prisma
- GitHub 로그인 성공
- 로그아웃 정상 작동
- 로그인 상태 유지
- Protected page 접근 제어
목표: GitHub API 연동 및 Repository/PR 데이터 가져오기
- Octokit 설정
- Repository 목록 API
- Repository 연동 기능
- Webhook 생성 API
- Webhook 핸들러 (
/api/webhook/github) - PR 목록 페이지
- PR 상세 페이지
- PR Diff 표시
- 파일별 변경사항 표시
app/
├── api/
│ ├── github/
│ │ ├── repos/route.ts
│ │ └── webhook/route.ts
│ └── repositories/
│ ├── route.ts
│ └── [id]/
│ └── prs/route.ts
├── dashboard/
│ ├── page.tsx
│ └── [repoId]/
│ ├── page.tsx
│ └── pr/
│ └── [prId]/page.tsx
lib/
├── github.ts
└── webhook-validator.ts
- Repository 목록 로드
- Repository 연동/해제
- Webhook 생성 확인
- PR 목록 표시
- PR Diff 정상 표시
- 실제 PR 생성 시 Webhook 트리거 확인
목표: Claude API 연동 및 자동 코드 리뷰 구현
- Anthropic SDK 설정
- AI 분석 프롬프트 작성
-
/api/review/analyze엔드포인트 - AI 응답 파싱 로직
- 품질 점수 계산 알고리즘
- 심각도 분류 로직
- 리뷰 결과 저장
- 리뷰 결과 UI 표시
- 라인별 제안사항 표시
- 코드 수정 제안 적용 기능
app/
├── api/
│ └── review/
│ ├── analyze/route.ts
│ └── [reviewId]/route.ts
lib/
├── ai/
│ ├── claude.ts
│ ├── prompts.ts
│ └── parsers.ts
└── scoring.ts
components/
├── ReviewPanel.tsx
├── SuggestionCard.tsx
└── CodeDiff.tsx
const REVIEW_PROMPT = `
당신은 숙련된 시니어 개발자입니다.
다음 코드 변경사항을 리뷰해주세요.
[분석 관점]
1. 버그 가능성 (Null pointer, Race condition 등)
2. 성능 이슈 (불필요한 리렌더링, O(n²) 알고리즘 등)
3. 보안 취약점 (SQL Injection, XSS 등)
4. 코드 품질 (중복, 복잡도, 가독성)
5. 베스트 프랙티스 준수
[출력 형식]
각 이슈를 다음 JSON 형식으로 제공:
{
"issues": [
{
"lineNumber": 15,
"severity": "HIGH|MEDIUM|LOW",
"category": "BUG|PERFORMANCE|SECURITY|QUALITY",
"title": "간단한 제목",
"description": "상세 설명",
"suggestion": "개선 방법",
"exampleCode": "예시 코드"
}
]
}
`;- AI 분석 요청 성공
- 응답 파싱 정상
- 품질 점수 계산 정확
- UI에 제안사항 표시
- 라인별 주석 표시
- 에러 핸들링 (API 실패 시)
목표: WebSocket 서버 구축 및 실시간 댓글 기능
- Socket.io 서버 설정
- WebSocket 연결 관리
- Room 관리 (PR별)
- 실시간 댓글 전송/수신
- 타이핑 인디케이터
- 멘션 기능 (@username)
- 댓글 스레드 (답글)
- 이모지 반응
- 읽음 표시
- 알림 시스템
- 연결 끊김 처리
lib/
├── socket/
│ ├── emitter.ts
│ └── types.ts # client/server shared Socket.IO protocol
socket-server/
├── index.ts # standalone Socket.IO runtime
├── handlers.ts
└── auth.ts
hooks/
└── useRealtimeComments.ts
components/
├── CommentSection.tsx
├── CommentInput.tsx
└── CommentThread.tsx
// socket-server/index.ts
import { createServer } from "http"
import { Server } from "socket.io"
import { setupSocketHandlers } from "./handlers"
import type { TypedServer } from "../lib/socket/types"
const httpServer = createServer()
const io: TypedServer = new Server(httpServer, {
cors: {
origin: process.env.NEXTJS_URL || "http://localhost:3000",
credentials: true,
},
})
setupSocketHandlers(io)
httpServer.listen(process.env.PORT || 4000)Next.js 앱은 next dev / next start로 HTTP와 DB API를 담당하고,
Socket.IO는 별도 프로세스인 npm run dev:socket으로 실행한다.
일반 PR 댓글과 알림은 REST API와 React Query polling으로 동작하며,
브라우저는 일반 화면에서 Socket.IO client를 로드하지 않는다.
standalone Socket.IO 서버는 이후 협업방 전용 realtime runtime으로 사용한다.
로컬 Redis adapter를 사용하려면 docker compose up -d redis로 Redis를 시작하고
Socket 서버 환경에 REDIS_URL=redis://localhost:6379를 설정한다.
REDIS_URL이 없으면 단일 인스턴스 모드로 실행하고, 설정했는데 연결할 수 없으면 서버 시작이 실패한다.
/health는 Redis 연결이 끊긴 동안 503을 반환한다.
Railway에서는 Socket 서비스와 같은 환경의 비공개 Redis REDIS_URL을 참조한다.
Redis가 설정되면 정원, presence, 소켓 lease, 코드 위치도 Redis에서 공유한다.
같은 사용자의 여러 탭은 정원 한 자리로 계산한다. 연결이 끊기면 30초간 재접속 자리를 유지한다.
서버 강제 종료 시 마지막 heartbeat 이후 80초가 지나면 다음 조회/입장에서 자리를 정리한다.
다른 참여자의 heartbeat로 화면에 반영되는 시간은 최대 약 105초다.
Redis 또는 adapter 연결이 끊기면 새 입장과 인원 조회는 실패(503)하고 기존 소켓은 재연결한다.
재연결 중 메시지는 REST로 4초마다 조회하며, 재입장 뒤 메시지와 코드 대화를 다시 불러온다.
Socket 서버는 15초마다 만료 lease와 실패한 DB 퇴장 동기화를 재시도하고,
60초마다 오래된 DB 참여 기록을 현재 presence와 대조한다. 이 내부 요청에는
SOCKET_INTERNAL_SECRET과 Socket 서비스에서 접근 가능한 NEXTJS_URL이 필요하다.
종료 신호를 받으면 신규 입장을 막고 로컬 소켓의 퇴장을 Redis에 반영한 뒤 서버를 닫는다.
Socket 서버 로그는 JSON 구조화 로그로 남기며 replicaId, roomId, event,
errorCode, 입장 지연, broadcast 지연을 포함한다. 메시지 본문과 토큰은 기록하지 않는다.
내부 secret이 있는 요청만 GET /metrics에서 활성 연결 수, 입장 거절, Redis 오류,
broadcast 지연 지표를 조회할 수 있다.
Socket 서비스를 2개 이상으로 늘리기 전에는 REDIS_URL을 설정한 상태에서
__tests__/socket-server/redis-adapter.test.ts 통합 테스트를 통과시킨다. 이 테스트는
두 Socket 복제본, Redis 공유 presence, 동시 9명 입장 중 8명 성공, 한 사용자의 두 탭,
복제본 간 메시지와 코드 위치 전달, 서버 종료 후 재연결 폭주를 검증한다. 이 검증 전에는
운영 Socket 서비스 replica를 1개로 유지하고, 통과 후 스테이징 2개, 운영 2개 순서로 확장한다.
- 협업방 WebSocket 연결 성공
- 협업방 메시지 실시간 전송/수신
- 여러 클라이언트 동시 접속
- 협업방 presence 작동
- 멘션 알림
- 연결 끊김 후 재연결
목표: 통계 대시보드 완성 및 프로덕션 배포
- Repository 통계 API
- 품질 점수 추이 차트
- 팀원별 통계
- 이슈 분포 차트
- 필터링 기능
- 검색 기능
- Dockerfile 작성
- Docker Compose 설정
- Vercel 배포 설정
- 환경 변수 설정
- GitHub Actions CI/CD
- 에러 모니터링 (Sentry)
- 성능 모니터링
app/
└── dashboard/
└── [repoId]/
├── page.tsx
├── stats/
└── team/
components/
├── charts/
│ ├── QualityTrendChart.tsx
│ ├── IssueDistribution.tsx
│ └── TeamStats.tsx
└── filters/
Dockerfile
docker-compose.yml
.github/
└── workflows/
├── ci.yml
└── deploy.yml
FROM node:18-alpine AS base
FROM base AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npx prisma generate
RUN npm run build
FROM base AS runner
WORKDIR /app
ENV NODE_ENV production
COPY --from=builder /app/public ./public
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
EXPOSE 3000
CMD ["node", "server.js"]- 차트 데이터 로드
- 필터링 작동
- Docker 이미지 빌드 성공
- 로컬 Docker 실행
- Vercel 배포 성공
- 프로덕션 환경 테스트
- CI/CD 파이프라인 작동
- 에러 로그 수집 확인
GitHub OAuth 로그인
Response:
{
"user": {
"id": "user_123",
"email": "user@example.com",
"name": "John Doe",
"image": "https://..."
}
}사용자의 GitHub Repository 목록
Headers:
Authorization: Bearer {token}
Response:
{
"repos": [
{
"id": 123,
"name": "awesome-project",
"fullName": "username/awesome-project",
"language": "TypeScript",
"isConnected": false
}
]
}Repository 연동
Body:
{
"githubId": 123,
"name": "awesome-project",
"fullName": "username/awesome-project"
}GitHub Webhook 핸들러
PR 자동 분석 요청
Body:
{
"prId": "pr_123"
}Response:
{
"reviewId": "review_456",
"qualityScore": 87,
"severity": "MEDIUM",
"issueCount": 3,
"suggestions": [
{
"lineNumber": 15,
"severity": "MEDIUM",
"category": "QUALITY",
"title": "타입 명시 필요",
"description": "useState에 타입을 명시하세요",
"suggestion": "const [data, setData] = useState<Data | null>()",
"exampleCode": "..."
}
]
}리뷰 결과 조회
댓글 작성
Body:
{
"prId": "pr_123",
"lineNumber": 15,
"content": "이 부분 타입 체크가 필요할 것 같아요",
"mentions": ["user_456"]
}PR의 댓글 목록
// PR 룸 참여
socket.emit('join-pr', prId);
// 새 댓글 작성
socket.emit('new-comment', {
prId: 'pr_123',
lineNumber: 15,
content: '댓글 내용',
userId: 'user_123'
});
// 타이핑 중
socket.emit('typing', {
prId: 'pr_123',
userId: 'user_123',
username: 'John'
});
// 멘션
socket.emit('mention', {
prId: 'pr_123',
mentionedUserId: 'user_456',
commentId: 'comment_789'
});// 새 댓글 수신
socket.on('comment-added', (comment) => {
// UI 업데이트
});
// 타이핑 인디케이터
socket.on('user-typing', ({ username }) => {
// "John이 입력 중..." 표시
});
// 새 알림
socket.on('new-notification', (notification) => {
// 알림 표시
});- Vercel 프로젝트 생성
npm i -g vercel
vercel login
vercel-
환경 변수 설정
- Vercel Dashboard → Settings → Environment Variables
- 모든 환경 변수 추가
-
자동 배포 설정
- GitHub 연동
- main 브랜치 푸시 시 자동 배포
# 이미지 빌드
docker build -t codemate:latest .
# 컨테이너 실행
docker run -p 3000:3000 \
-e DATABASE_URL="..." \
-e GITHUB_ID="..." \
codemate:latest
# Docker Compose
docker-compose up -d.github/workflows/deploy.yml:
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
- name: Build
run: npm run build
- name: Deploy to Vercel
uses: amondnet/vercel-action@v20
with:
vercel-token: ${{ secrets.VERCEL_TOKEN }}
vercel-org-id: ${{ secrets.ORG_ID }}
vercel-project-id: ${{ secrets.PROJECT_ID }}- 모바일 앱 (React Native)
- VS Code Extension
- Slack/Discord 봇 통합
- 코드 메트릭스 분석
- AI 학습 모델 커스터마이징
- 다국어 지원
- 이미지 최적화 (Next/Image)
- 코드 스플리팅
- React Query 캐싱
- Edge Functions 활용
MIT License
기여는 언제나 환영합니다!
- Fork the Project
- Create your Feature Branch (
git checkout -b feature/AmazingFeature) - Commit your Changes (
git commit -m 'Add some AmazingFeature') - Push to the Branch (
git push origin feature/AmazingFeature) - Open a Pull Request
프로젝트 관련 문의: [juyung0903@naver.com]
GitHub: @phnml1
⭐️ 이 프로젝트가 도움이 되었다면 Star를 눌러주세요!