1탄 https://marlboroyw.tistory.com/623
git-push > AI 코드리뷰 도입기
배경팀 프로젝트를 진행하면서 PR 코드리뷰에 드는 시간이 점점 부담이 됐다.사람이 직접 리뷰하기 전에, 아키텍처 패턴 위반이나 명백한 버그 같은 것들을 먼저 걸러줄 수 있다면 리뷰 품질이
marlboroyw.tistory.com
1탄에서는 claude code 로만 리뷰를 진행했는데,
생각해보니
- claude code
- gemini cli
- codex
3개 모두 활용하면 좋을것 같아서 스크립트를 변경했다.
일단, local.properties 에서 각각 ai 에게 review 를 받을지 여부를 flag 로 관리하게 해 놓았고,
# push AI code review flag
use.claude.review=true
use.gemini.review=true
use.codex.review=true
# code review result to SLACK flag
use.notify.slack=true
# Slack Bot Token
slack.bot.token=xoxb-your-token
# Slack channel id
slack.channel.id=your-channel
이 상태에서 push 하게 되면, nohup 으로 백그라운드로 리뷰를 진행한다.

완료시 아래와 같이 slack 으로 3개의 리뷰를 전송하게 해 놓았다.

이렇게 각각의 3개 리뷰가 하나의 푸시 메세지에 대한 리뷰를 슬랙 댓글로 남기게 된다.
아래는 전체 pre-push-hook, ai-review 스크립트 전문이다.
#!/bin/sh
echo "🧪 Pre-push: 단위 테스트 실행 중..."
./gradlew :feature:motion:testDebugUnitTest :motion:testDebugUnitTest --quiet 2>&1
if [ $? -ne 0 ]; then
echo "❌ 테스트 실패 — push 중단"
exit 1
fi
echo "✅ 테스트 통과"
echo "🔨 Pre-push: 빌드 검증 중..."
./gradlew :app-kneefresh-vsc:assembleDebug --quiet 2>&1
if [ $? -ne 0 ]; then
echo "❌ 빌드 실패 — push 중단"
exit 1
fi
echo "✅ 빌드 성공"
# AI 리뷰는 백그라운드로 — push를 막지 않음
UPSTREAM=$(git rev-parse --abbrev-ref --symbolic-full-name @{upstream} 2>/dev/null || echo "origin/develop")
DIFF=$(git diff "${UPSTREAM}"...HEAD -- \
"*.kt" "*.kts" "*.gradle" \
"*AndroidManifest.xml" \
"gradle.properties" "*/gradle.properties" \
"*/gradle-wrapper.properties" \
"*.pro" \
2>/dev/null | head -n 600)
if [ -z "$DIFF" ]; then
echo "ℹ️ 변경된 파일 없음 — AI 리뷰 스킵"
else
TMP_DIFF=$(mktemp)
printf '%s\n' "$DIFF" > "$TMP_DIFF"
BASE_SHA=$(git merge-base HEAD "${UPSTREAM}" 2>/dev/null)
BASE_BRANCH=$(echo "$UPSTREAM" | sed 's|^[^/]*/||')
BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null)
AUTHOR=$(git config user.name 2>/dev/null)
REPO_ROOT=$(git rev-parse --show-toplevel 2>/dev/null)
HOOK_DIR=$(cd "$(dirname "$0")" && pwd)
nohup sh "${HOOK_DIR}/ai-review.sh" \
"$TMP_DIFF" "$BASE_SHA" "$BASE_BRANCH" "$BRANCH" "$AUTHOR" "$REPO_ROOT" \
</dev/null >>"${TMPDIR:-/tmp}/athome-ai-review.log" 2>&1 &
echo "🤖 AI 코드 리뷰 백그라운드 실행 중... (완료 시 Slack 알림)"
fi
echo "✅ Push 진행"
exit 0
ai-review hook
#!/bin/sh
# 백그라운드 AI 코드 리뷰 스크립트
# pre-push hook에서 nohup으로 실행됨
TMP_DIFF="$1"
BASE_SHA="$2"
BASE_BRANCH="$3"
BRANCH="$4"
AUTHOR="$5"
REPO_ROOT="$6"
# 프로젝트 루트로 이동 (local.properties 접근을 위해)
cd "$REPO_ROOT" || exit 1
# local.properties에서 값을 읽는 헬퍼
prop_enabled() {
val=$(grep "^${1}=" local.properties 2>/dev/null | cut -d'=' -f2-)
[ "$val" = "false" ] && echo "false" || echo "true"
}
prop_value() {
grep "^${1}=" local.properties 2>/dev/null | cut -d'=' -f2-
}
USE_CLAUDE=$(prop_enabled "use.claude.review")
USE_GEMINI=$(prop_enabled "use.gemini.review")
USE_CODEX=$(prop_enabled "use.codex.review")
USE_SLACK=$(prop_enabled "use.notify.slack")
DIFF=$(cat "$TMP_DIFF")
rm -f "$TMP_DIFF"
if [ -z "$DIFF" ]; then
exit 0
fi
TMP_PROMPT=$(mktemp)
TMP_CLAUDE=$(mktemp); TMP_CLAUDE_ERR=$(mktemp)
TMP_GEMINI=$(mktemp); TMP_GEMINI_ERR=$(mktemp)
TMP_CODEX=$(mktemp); TMP_CODEX_ERR=$(mktemp)
cat > "$TMP_PROMPT" << 'PROMPT_EOF'
You are reviewing Android project changes including Kotlin source, Gradle build scripts, and AndroidManifest.
Architecture rules (Kotlin):
- Composable layers must follow: Destination → Screen → View
- View composables must NOT have default values for the modifier parameter
- Each screen has a contract/ package with XxxEvent, XxxState, XxxEffect
- Two BaseVM implementations coexist (BaseVM with runningFold, BaseViewModel with MutableStateFlow) — flag inconsistency within a feature
Review Kotlin (.kt) for:
1. Architecture pattern violations
2. Potential bugs or missing error handling
3. Memory leaks (context leaks, unregistered listeners, coroutine scope issues)
4. Code quality issues
Review AndroidManifest.xml for:
1. Dangerous permissions added without necessity
2. Components exported=true without proper permission protection
3. Intent filters that may expose unintended attack surface
4. Missing or incorrect security-related attributes (allowBackup, usesCleartextTraffic, etc.)
Review build scripts (.kts / .gradle) and gradle.properties for:
1. Dependency version downgrades or known-vulnerable versions
2. Debug/test configurations leaking into release builds
3. Signing config or ProGuard/R8 rule changes that could affect security
4. SDK version changes (minSdk, targetSdk, compileSdk) and their implications
5. Gradle wrapper version changes
Respond in Korean. Format:
🔴 심각: [내용]
🟡 주의: [내용]
🟢 양호: [내용]
If nothing to flag, just say "🟢 전반적으로 양호합니다"
Diff:
PROMPT_EOF
printf '%s\n' "$DIFF" >> "$TMP_PROMPT"
# 병렬 실행
PIDS=""
if [ "$USE_CLAUDE" = "true" ] && command -v claude > /dev/null 2>&1; then
claude -p "$(cat "$TMP_PROMPT")" > "$TMP_CLAUDE" 2>"$TMP_CLAUDE_ERR" &
PIDS="$PIDS $!"
fi
if [ "$USE_GEMINI" = "true" ] && command -v gemini > /dev/null 2>&1; then
gemini --skip-trust -p "$(cat "$TMP_PROMPT")" > "$TMP_GEMINI" 2>"$TMP_GEMINI_ERR" &
PIDS="$PIDS $!"
fi
if [ "$USE_CODEX" = "true" ] && command -v codex > /dev/null 2>&1; then
if [ -n "$BASE_SHA" ]; then
codex review --base "$BASE_SHA" > "$TMP_CODEX" 2>"$TMP_CODEX_ERR" &
else
codex review > "$TMP_CODEX" 2>"$TMP_CODEX_ERR" &
fi
PIDS="$PIDS $!"
fi
[ -n "$PIDS" ] && wait $PIDS 2>/dev/null
# ── Slack 스레드 전송 ────────────────────────────────────
if [ "$USE_SLACK" = "true" ]; then
SLACK_BOT_TOKEN=$(prop_value "slack.bot.token")
SLACK_CHANNEL_ID=$(prop_value "slack.channel.id")
if [ -n "$SLACK_BOT_TOKEN" ] && [ -n "$SLACK_CHANNEL_ID" ]; then
# GitHub compare 링크 — SHA 기반
REMOTE_URL=$(git remote get-url origin 2>/dev/null)
REPO_URL=$(printf '%s' "$REMOTE_URL" \
| sed 's|git@\([^:]*\):\(.*\)\.git$|https://\1/\2|; s|\.git$||')
COMPARE_URL=""
[ -n "$REPO_URL" ] && [ -n "$BASE_SHA" ] && \
COMPARE_URL="${REPO_URL}/compare/${BASE_SHA}...${BRANCH}"
# 파일 내용 → JSON 문자열 인코딩
json_escape() {
cat "$1" | head -c 2800 \
| sed 's/\\/\\\\/g; s/"/\\"/g' \
| awk '{printf "%s\\n", $0}' \
| sed 's/\\n$//'
}
# 1. 초기 메시지
ESC_AUTHOR=$(printf '%s' "$AUTHOR" | sed 's/\\/\\\\/g; s/"/\\"/g')
ESC_BRANCH=$(printf '%s' "$BRANCH" | sed 's/\\/\\\\/g; s/"/\\"/g')
if [ -n "$COMPARE_URL" ]; then
HEADER_TEXT="🔍 *${ESC_AUTHOR}의 Push에 대한 AI 리뷰*\\n*브랜치:* \`${ESC_BRANCH}\` | <${COMPARE_URL}|📎 Diff 보기>"
else
HEADER_TEXT="🔍 *${ESC_AUTHOR}의 Push에 대한 AI 리뷰*\\n*브랜치:* \`${ESC_BRANCH}\`"
fi
RESPONSE=$(curl -s -X POST "https://slack.com/api/chat.postMessage" \
-H "Authorization: Bearer ${SLACK_BOT_TOKEN}" \
-H "Content-Type: application/json; charset=utf-8" \
-d "{\"channel\":\"${SLACK_CHANNEL_ID}\",\"blocks\":[{\"type\":\"section\",\"text\":{\"type\":\"mrkdwn\",\"text\":\"${HEADER_TEXT}\"}}]}")
TS=$(printf '%s' "$RESPONSE" | grep -o '"ts":"[^"]*"' | head -n1 | cut -d'"' -f4)
if [ -n "$TS" ]; then
# 2. 각 AI 리뷰를 스레드 답글로 전송
post_thread() {
icon="$1"; label="$2"; tmpout="$3"
[ -s "$tmpout" ] || return
ESC=$(json_escape "$tmpout")
curl -s -X POST "https://slack.com/api/chat.postMessage" \
-H "Authorization: Bearer ${SLACK_BOT_TOKEN}" \
-H "Content-Type: application/json; charset=utf-8" \
-d "{\"channel\":\"${SLACK_CHANNEL_ID}\",\"thread_ts\":\"${TS}\",\"blocks\":[{\"type\":\"section\",\"text\":{\"type\":\"mrkdwn\",\"text\":\"${icon} *${label}*\\n\\n${ESC}\"}}]}" \
> /dev/null
}
[ "$USE_CLAUDE" = "true" ] && command -v claude > /dev/null 2>&1 && \
post_thread "🤖" "Claude" "$TMP_CLAUDE"
[ "$USE_GEMINI" = "true" ] && command -v gemini > /dev/null 2>&1 && \
post_thread "✨" "Gemini" "$TMP_GEMINI"
[ "$USE_CODEX" = "true" ] && command -v codex > /dev/null 2>&1 && \
post_thread "⚡" "Codex" "$TMP_CODEX"
fi
fi
fi
rm -f "$TMP_PROMPT" \
"$TMP_CLAUDE" "$TMP_CLAUDE_ERR" \
"$TMP_GEMINI" "$TMP_GEMINI_ERR" \
"$TMP_CODEX" "$TMP_CODEX_ERR"'Computer > Android&iOS' 카테고리의 다른 글
| git-push > AI 코드리뷰 도입기 (0) | 2026.05.06 |
|---|---|
| AAB로도 해결 안 됐던 문제 — audio-asset-pack 도입기 (0) | 2026.04.13 |
| Android) strings.xml 을 Dev(Debug), Staging, Release 로 따로 설정하는 방법 (0) | 2026.04.01 |
| [Android] ADB MCP + Claude Code 로 qa 자동화 하기 (0) | 2026.02.27 |
| 안드로이드 웹뷰 로그인이 자꾸 풀린다면? (0) | 2026.01.14 |