⏱️ 예상 읽기 시간: 20분

서론

이전 글에서 Plane의 기본 설치와 설정을 다뤘다면, 이번에는 실무 활용의 핵심인 GitHub 연동과 API 자동화에 집중하겠습니다.

이 가이드에서는 단순한 연동을 넘어서 개발 생산성을 극대화하는 창의적인 방법들을 다룹니다:

  • 🔗 완벽한 GitHub 연동: OAuth, Webhooks, 자동 동기화
  • 터미널 슈퍼 파워: 한 줄 명령어로 이슈 생성부터 배포까지
  • 🤖 스마트 자동화: 커밋 메시지로 이슈 상태 자동 업데이트
  • 🎯 실무 워크플로우: 실제 프로젝트에서 바로 사용 가능한 예제들

GitHub 연동 설정

1. GitHub OAuth 애플리케이션 생성

# GitHub Settings > Developer settings > OAuth Apps
# 또는 다음 URL로 직접 접근
https://github.com/settings/applications/new

필수 설정값:

Application name: Plane Project Management
Homepage URL: https://your-plane-domain.com
Authorization callback URL: https://your-plane-domain.com/auth/github/callback/

2. Plane 환경 변수 설정

# .env 파일 업데이트
GITHUB_CLIENT_ID=your_github_client_id
GITHUB_CLIENT_SECRET=your_github_client_secret
IS_GITHUB_ENABLED=1

# API 서버 재시작
docker compose restart api

3. GitHub Personal Access Token 생성

# GitHub Settings > Developer settings > Personal access tokens > Fine-grained tokens
# 필요한 권한:
# - Repository: Issues (Read/Write)
# - Repository: Pull Requests (Read/Write)
# - Repository: Contents (Read)
# - Repository: Metadata (Read)

API 기본 설정

1. Plane API 토큰 생성

# Plane 웹 인터페이스에서
# Settings > API Tokens > Generate New Token

# 환경 변수로 설정
export PLANE_API_TOKEN="your_plane_api_token"
export PLANE_API_BASE="https://your-plane-domain.com/api/v1"
export PLANE_WORKSPACE_ID="your_workspace_id"
export PLANE_PROJECT_ID="your_project_id"

2. 기본 API 테스트

# 워크스페이스 목록 조회
curl -H "Authorization: Bearer $PLANE_API_TOKEN" \
     "$PLANE_API_BASE/workspaces/"

# 프로젝트 목록 조회
curl -H "Authorization: Bearer $PLANE_API_TOKEN" \
     "$PLANE_API_BASE/workspaces/$PLANE_WORKSPACE_ID/projects/"

터미널 슈퍼 파워: Alias와 함수

1. 기본 Plane CLI 함수들

# ~/.zshrc 또는 ~/.bashrc에 추가

# Plane API 기본 함수
plane_api() {
    local method=${1:-GET}
    local endpoint=$2
    local data=$3
    
    if [ -n "$data" ]; then
        curl -s -X $method \
             -H "Authorization: Bearer $PLANE_API_TOKEN" \
             -H "Content-Type: application/json" \
             -d "$data" \
             "$PLANE_API_BASE$endpoint"
    else
        curl -s -X $method \
             -H "Authorization: Bearer $PLANE_API_TOKEN" \
             "$PLANE_API_BASE$endpoint"
    fi
}

# 이슈 빠른 생성
plane_issue() {
    local title="$1"
    local description="$2"
    local priority="${3:-medium}"
    
    local data=$(cat <<EOF
{
    "name": "$title",
    "description": "$description", 
    "priority": "$priority",
    "state": "$(plane_get_state_id 'Todo')"
}
EOF
)
    
    plane_api POST "/workspaces/$PLANE_WORKSPACE_ID/projects/$PLANE_PROJECT_ID/issues/" "$data" | jq -r '.name + " 이슈가 생성되었습니다. ID: " + .id'
}

# 상태 ID 조회 함수
plane_get_state_id() {
    local state_name="$1"
    plane_api GET "/workspaces/$PLANE_WORKSPACE_ID/projects/$PLANE_PROJECT_ID/states/" | \
    jq -r ".[] | select(.name == \"$state_name\") | .id"
}

# 이슈 상태 변경
plane_move() {
    local issue_id="$1"
    local state_name="$2"
    local state_id=$(plane_get_state_id "$state_name")
    
    local data="{\"state\": \"$state_id\"}"
    plane_api PATCH "/workspaces/$PLANE_WORKSPACE_ID/projects/$PLANE_PROJECT_ID/issues/$issue_id/" "$data"
}

2. 실무 활용 Alias 모음

# 빠른 이슈 관리
alias pi='plane_issue'                    # 이슈 생성
alias pls='plane_list_issues'            # 이슈 목록
alias pshow='plane_show_issue'           # 이슈 상세보기
alias pmv='plane_move'                   # 이슈 상태 변경

# 사이클(스프린트) 관리  
alias pcs='plane_create_cycle'           # 사이클 생성
alias pcl='plane_list_cycles'            # 사이클 목록
alias pca='plane_add_to_cycle'           # 이슈를 사이클에 추가

# GitHub 연동
alias gh2plane='github_to_plane_sync'    # GitHub 이슈 동기화
alias plane2gh='plane_to_github_sync'    # Plane 이슈를 GitHub로

# 대시보드 조회
alias pdash='plane_dashboard'            # 간단 대시보드
alias pstats='plane_statistics'          # 프로젝트 통계

3. 고급 함수들

# GitHub 이슈를 Plane으로 가져오기
github_to_plane_sync() {
    local github_repo="$1"
    local github_token="$GITHUB_TOKEN"
    
    # GitHub 이슈 목록 조회
    local issues=$(curl -s -H "Authorization: token $github_token" \
                       "https://api.github.com/repos/$github_repo/issues?state=open")
    
    # 각 이슈를 Plane으로 생성
    echo "$issues" | jq -r '.[] | @base64' | while read encoded_issue; do
        local issue=$(echo "$encoded_issue" | base64 --decode)
        local title=$(echo "$issue" | jq -r '.title')
        local body=$(echo "$issue" | jq -r '.body // ""')
        local github_id=$(echo "$issue" | jq -r '.number')
        
        # Plane에 이슈 생성 (GitHub ID를 설명에 포함)
        plane_issue "$title" "GitHub Issue #$github_id\n\n$body"
    done
}

# 간단 대시보드 출력
plane_dashboard() {
    echo "🎯 Plane 프로젝트 대시보드"
    echo "=========================="
    
    # 이슈 상태별 개수
    local issues=$(plane_api GET "/workspaces/$PLANE_WORKSPACE_ID/projects/$PLANE_PROJECT_ID/issues/")
    echo "📊 이슈 현황:"
    echo "$issues" | jq -r 'group_by(.state_detail.name) | .[] | "  \(.[0].state_detail.name): \(length)개"'
    
    # 최근 생성된 이슈 5개
    echo -e "\n📝 최근 이슈 (5개):"
    echo "$issues" | jq -r 'sort_by(.created_at) | reverse | .[0:5] | .[] | "  • \(.name) (\(.state_detail.name))"'
    
    # 내가 담당한 이슈
    local my_user_id=$(plane_api GET "/users/me/" | jq -r '.id')
    echo -e "\n👤 내 할당 이슈:"
    echo "$issues" | jq -r --arg uid "$my_user_id" '.[] | select(.assignees[]? == $uid) | "  • \(.name) (\(.state_detail.name))"'
}

GitHub Webhooks 자동화

1. Webhook 서버 구성

# webhook_server.py
from flask import Flask, request, jsonify
import requests
import os
import hmac
import hashlib

app = Flask(__name__)

PLANE_API_TOKEN = os.getenv('PLANE_API_TOKEN')
PLANE_API_BASE = os.getenv('PLANE_API_BASE')
WORKSPACE_ID = os.getenv('PLANE_WORKSPACE_ID')
PROJECT_ID = os.getenv('PLANE_PROJECT_ID')
WEBHOOK_SECRET = os.getenv('GITHUB_WEBHOOK_SECRET')

def verify_signature(payload, signature):
    """GitHub Webhook 서명 검증"""
    expected = hmac.new(
        WEBHOOK_SECRET.encode(),
        payload,
        hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(f"sha256={expected}", signature)

def create_plane_issue(title, description, labels=None):
    """Plane에 이슈 생성"""
    headers = {
        'Authorization': f'Bearer {PLANE_API_TOKEN}',
        'Content-Type': 'application/json'
    }
    
    data = {
        'name': title,
        'description': description,
        'labels': labels or []
    }
    
    response = requests.post(
        f"{PLANE_API_BASE}/workspaces/{WORKSPACE_ID}/projects/{PROJECT_ID}/issues/",
        headers=headers,
        json=data
    )
    return response.json()

@app.route('/github/webhook', methods=['POST'])
def github_webhook():
    signature = request.headers.get('X-Hub-Signature-256')
    if not verify_signature(request.data, signature):
        return jsonify({'error': 'Invalid signature'}), 403
    
    event_type = request.headers.get('X-GitHub-Event')
    payload = request.json
    
    if event_type == 'issues' and payload['action'] == 'opened':
        # 새 GitHub 이슈가 생성되면 Plane에도 생성
        issue = payload['issue']
        title = f"[GitHub] {issue['title']}"
        description = f"GitHub Issue #{issue['number']}\n\n{issue['body']}"
        labels = [label['name'] for label in issue['labels']]
        
        plane_issue = create_plane_issue(title, description, labels)
        return jsonify({'plane_issue_id': plane_issue.get('id')})
    
    elif event_type == 'push':
        # 커밋 메시지에서 이슈 번호 추출하여 자동 상태 변경
        for commit in payload['commits']:
            message = commit['message']
            # "fixes #123", "closes #456" 형태의 메시지 처리
            import re
            issues = re.findall(r'(?:fixes|closes|resolves)\s+#(\d+)', message, re.IGNORECASE)
            
            for issue_id in issues:
                # Plane 이슈 상태를 'Done'으로 변경
                update_issue_status(issue_id, 'Done')
    
    return jsonify({'status': 'processed'})

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000)

2. Docker로 Webhook 서버 실행

# Dockerfile.webhook
FROM python:3.11-alpine

WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt

COPY webhook_server.py .

EXPOSE 5000
CMD ["python", "webhook_server.py"]
# docker-compose.webhook.yml에 추가
webhook-server:
  build:
    context: .
    dockerfile: Dockerfile.webhook
  ports:
    - "5000:5000"
  environment:
    - PLANE_API_TOKEN=${PLANE_API_TOKEN}
    - PLANE_API_BASE=${PLANE_API_BASE}
    - PLANE_WORKSPACE_ID=${PLANE_WORKSPACE_ID}
    - PLANE_PROJECT_ID=${PLANE_PROJECT_ID}
    - GITHUB_WEBHOOK_SECRET=${GITHUB_WEBHOOK_SECRET}
  restart: unless-stopped

창의적인 자동화 예제들

1. 커밋 기반 자동 시간 추적

# Git hook: .git/hooks/post-commit
#!/bin/bash

# 커밋 메시지에서 시간 정보 추출
commit_message=$(git log -1 --pretty=%B)
time_spent=$(echo "$commit_message" | grep -oE 'time:([0-9]+[hm])' | cut -d: -f2)

if [ -n "$time_spent" ]; then
    # 현재 브랜치에서 이슈 번호 추출 (예: feature/issue-123)
    branch_name=$(git branch --show-current)
    issue_id=$(echo "$branch_name" | grep -oE 'issue-([0-9]+)' | cut -d- -f2)
    
    if [ -n "$issue_id" ]; then
        # Plane에 시간 기록 추가
        plane_api POST "/workspaces/$PLANE_WORKSPACE_ID/projects/$PLANE_PROJECT_ID/issues/$issue_id/time-logs/" \
        "{\"time_spent\": \"$time_spent\", \"description\": \"$commit_message\"}"
        
        echo "✅ 시간 추적이 기록되었습니다: $time_spent"
    fi
fi

2. AI 기반 이슈 우선순위 자동 설정

# priority_ai.py
import openai
import requests
import json

def analyze_issue_priority(title, description):
    """OpenAI를 사용하여 이슈 우선순위 분석"""
    prompt = f"""
    다음 이슈의 우선순위를 urgent, high, medium, low 중에서 결정해주세요:
    
    제목: {title}
    설명: {description}
    
    JSON 형태로 응답해주세요: `"priority": "medium", "reason": "이유"`
    """
    
    response = openai.ChatCompletion.create(
        model="gpt-3.5-turbo",
        messages=[{"role": "user", "content": prompt}]
    )
    
    return json.loads(response.choices[0].message.content)

def auto_prioritize_issues():
    """모든 이슈의 우선순위를 AI로 자동 설정"""
    issues = requests.get(
        f"{PLANE_API_BASE}/workspaces/{WORKSPACE_ID}/projects/{PROJECT_ID}/issues/",
        headers={'Authorization': f'Bearer {PLANE_API_TOKEN}'}
    ).json()
    
    for issue in issues:
        if not issue.get('priority'):  # 우선순위가 설정되지 않은 이슈만
            analysis = analyze_issue_priority(issue['name'], issue['description'])
            
            # 우선순위 업데이트
            requests.patch(
                f"{PLANE_API_BASE}/workspaces/{WORKSPACE_ID}/projects/{PROJECT_ID}/issues/{issue['id']}/",
                headers={'Authorization': f'Bearer {PLANE_API_TOKEN}'},
                json={'priority': analysis['priority']}
            )
            
            print(f"✅ {issue['name']}: {analysis['priority']} ({analysis['reason']})")

3. Slack 통합 알림 시스템

# slack_notify.sh
#!/bin/bash

slack_notify() {
    local message="$1"
    local channel="${2:-#dev-team}"
    local webhook_url="$SLACK_WEBHOOK_URL"
    
    curl -X POST -H 'Content-type: application/json' \
         --data "{\"channel\":\"$channel\",\"text\":\"$message\"}" \
         "$webhook_url"
}

# 새 이슈 생성 시 Slack 알림
plane_issue_with_slack() {
    local title="$1"
    local description="$2"
    local assignee="$3"
    
    # Plane에 이슈 생성
    local result=$(plane_issue "$title" "$description")
    local issue_id=$(echo "$result" | jq -r '.id')
    
    # 담당자 할당
    if [ -n "$assignee" ]; then
        plane_assign_issue "$issue_id" "$assignee"
    fi
    
    # Slack 알림
    local message="🎯 새 이슈가 생성되었습니다!\n*$title*\n담당자: $assignee\n링크: https://your-plane-domain.com/projects/$PLANE_PROJECT_ID/issues/$issue_id"
    slack_notify "$message"
}

# 스프린트 완료 리포트
plane_sprint_report() {
    local cycle_id="$1"
    
    # 사이클 정보 조회
    local cycle_info=$(plane_api GET "/workspaces/$PLANE_WORKSPACE_ID/projects/$PLANE_PROJECT_ID/cycles/$cycle_id/")
    local cycle_name=$(echo "$cycle_info" | jq -r '.name')
    
    # 완료된 이슈 수 계산
    local completed_issues=$(plane_api GET "/workspaces/$PLANE_WORKSPACE_ID/projects/$PLANE_PROJECT_ID/cycles/$cycle_id/issues/" | \
                            jq '[.[] | select(.state_detail.group == "completed")] | length')
    
    local total_issues=$(plane_api GET "/workspaces/$PLANE_WORKSPACE_ID/projects/$PLANE_PROJECT_ID/cycles/$cycle_id/issues/" | jq 'length')
    
    local completion_rate=$((completed_issues * 100 / total_issues))
    
    # Slack 리포트 전송
    local report="📊 *$cycle_name* 완료 리포트\n완료율: $completion_rate% ($completed_issues/$total_issues)\n"
    slack_notify "$report" "#management"
}

실무 워크플로우 예제

1. 완전한 개발 워크플로우

# 새 기능 개발 시작
start_feature() {
    local feature_name="$1"
    local description="$2"
    
    # 1. Plane에 이슈 생성
    local issue_result=$(plane_issue "[Feature] $feature_name" "$description" "high")
    local issue_id=$(echo "$issue_result" | grep -oE 'ID: [a-z0-9-]+' | cut -d' ' -f2)
    
    # 2. Git 브랜치 생성
    git checkout -b "feature/issue-$issue_id"
    
    # 3. 이슈를 현재 스프린트에 추가
    local current_cycle=$(plane_api GET "/workspaces/$PLANE_WORKSPACE_ID/projects/$PLANE_PROJECT_ID/cycles/" | \
                         jq -r '.[] | select(.is_current == true) | .id')
    plane_api POST "/workspaces/$PLANE_WORKSPACE_ID/projects/$PLANE_PROJECT_ID/cycles/$current_cycle/cycle-issues/" \
              "{\"issues\": [\"$issue_id\"]}"
    
    # 4. 이슈 상태를 'In Progress'로 변경
    plane_move "$issue_id" "In Progress"
    
    # 5. Slack 알림
    slack_notify "🚀 새 기능 개발 시작: *$feature_name*\n이슈 ID: $issue_id"
    
    echo "✅ 개발 환경이 준비되었습니다!"
    echo "   이슈 ID: $issue_id"
    echo "   브랜치: feature/issue-$issue_id"
}

# 기능 완료 및 배포
finish_feature() {
    local commit_message="$1"
    
    # 현재 브랜치에서 이슈 ID 추출
    local branch_name=$(git branch --show-current)
    local issue_id=$(echo "$branch_name" | grep -oE 'issue-([a-z0-9-]+)' | cut -d- -f2)
    
    if [ -n "$issue_id" ]; then
        # 1. 커밋 및 푸시
        git add .
        git commit -m "$commit_message (fixes #$issue_id time:2h)"
        git push origin "$branch_name"
        
        # 2. PR 생성 (GitHub CLI 사용)
        gh pr create --title "Feature: $(plane_api GET "/workspaces/$PLANE_WORKSPACE_ID/projects/$PLANE_PROJECT_ID/issues/$issue_id/" | jq -r '.name')" \
                     --body "Closes #$issue_id"
        
        # 3. 이슈 상태를 'In Review'로 변경
        plane_move "$issue_id" "In Review"
        
        # 4. 리뷰어에게 알림
        slack_notify "👀 코드 리뷰 요청: $(git log -1 --pretty=%s)\nPR: $(gh pr view --json url -q .url)"
        
        echo "✅ 기능 개발이 완료되고 리뷰 요청이 전송되었습니다!"
    fi
}

2. 데일리 스탠드업 자동화

# daily_standup.sh
#!/bin/bash

generate_daily_standup() {
    local team_members=("user_id_1" "user_id_2" "user_id_3")
    local today=$(date -I)
    local yesterday=$(date -I -d '1 day ago')
    
    echo "📅 Daily Standup Report - $today"
    echo "=================================="
    
    for user_id in "${team_members[@]}"; do
        # 사용자 정보 조회
        local user_info=$(plane_api GET "/users/$user_id/")
        local user_name=$(echo "$user_info" | jq -r '.display_name')
        
        echo -e "\n👤 **$user_name**"
        
        # 어제 완료한 작업
        local completed_yesterday=$(plane_api GET "/workspaces/$PLANE_WORKSPACE_ID/projects/$PLANE_PROJECT_ID/issues/" | \
                                   jq -r --arg uid "$user_id" --arg date "$yesterday" \
                                   '.[] | select(.assignees[]? == $uid and .completed_at[0:10] == $date) | "  ✅ \(.name)"')
        
        if [ -n "$completed_yesterday" ]; then
            echo "  어제 완료:"
            echo "$completed_yesterday"
        else
            echo "  어제 완료: 없음"
        fi
        
        # 오늘 진행 중인 작업
        local in_progress=$(plane_api GET "/workspaces/$PLANE_WORKSPACE_ID/projects/$PLANE_PROJECT_ID/issues/" | \
                           jq -r --arg uid "$user_id" \
                           '.[] | select(.assignees[]? == $uid and .state_detail.group == "started") | "  🔄 \(.name)"')
        
        if [ -n "$in_progress" ]; then
            echo "  진행 중:"
            echo "$in_progress"
        else
            echo "  진행 중: 없음"
        fi
        
        # 블로커 확인
        local blocked=$(plane_api GET "/workspaces/$PLANE_WORKSPACE_ID/projects/$PLANE_PROJECT_ID/issues/" | \
                       jq -r --arg uid "$user_id" \
                       '.[] | select(.assignees[]? == $uid and (.labels[]? == "blocked" or .priority == "urgent")) | "  🚫 \(.name)"')
        
        if [ -n "$blocked" ]; then
            echo "  블로커:"
            echo "$blocked"
        fi
    done
    
    # Slack으로 전송
    local report=$(generate_daily_standup)
    slack_notify "$report" "#daily-standup"
}

# 크론잡으로 매일 오전 9시에 실행
# 0 9 * * 1-5 /path/to/daily_standup.sh

3. 릴리즈 노트 자동 생성

# release_notes.py
import requests
import json
from datetime import datetime, timedelta

def generate_release_notes(version, since_date=None):
    """특정 기간의 완료된 이슈들로부터 릴리즈 노트 생성"""
    
    if not since_date:
        since_date = (datetime.now() - timedelta(days=14)).isoformat()
    
    # 완료된 이슈들 조회
    issues_response = requests.get(
        f"{PLANE_API_BASE}/workspaces/{WORKSPACE_ID}/projects/{PROJECT_ID}/issues/",
        headers={'Authorization': f'Bearer {PLANE_API_TOKEN}'},
        params={
            'state_group': 'completed',
            'completed_at__gte': since_date
        }
    )
    
    issues = issues_response.json()
    
    # 카테고리별로 분류
    features = []
    bug_fixes = []
    improvements = []
    
    for issue in issues:
        labels = [label['name'].lower() for label in issue.get('labels', [])]
        
        if 'feature' in labels or issue['name'].lower().startswith('[feature]'):
            features.append(issue)
        elif 'bug' in labels or 'fix' in labels:
            bug_fixes.append(issue)
        else:
            improvements.append(issue)
    
    # 릴리즈 노트 생성
    release_notes = f"""# Release {version}
    
Released: {datetime.now().strftime('%Y-%m-%d')}

## 🚀 New Features
"""
    
    for feature in features:
        release_notes += f"- {feature['name']} (#{feature['sequence_id']})\n"
    
    if bug_fixes:
        release_notes += "\n## 🐛 Bug Fixes\n"
        for bug in bug_fixes:
            release_notes += f"- {bug['name']} (#{bug['sequence_id']})\n"
    
    if improvements:
        release_notes += "\n## ✨ Improvements\n"
        for improvement in improvements:
            release_notes += f"- {improvement['name']} (#{improvement['sequence_id']})\n"
    
    # 통계 추가
    release_notes += f"""
## 📊 Statistics
- Total Issues Resolved: {len(issues)}
- Features: {len(features)}
- Bug Fixes: {len(bug_fixes)}
- Improvements: {len(improvements)}
"""
    
    return release_notes

# GitHub Release 생성
def create_github_release(version, release_notes, repo):
    """GitHub에 릴리즈 생성"""
    release_data = {
        'tag_name': f'v{version}',
        'name': f'Release {version}',
        'body': release_notes,
        'draft': False,
        'prerelease': False
    }
    
    response = requests.post(
        f'https://api.github.com/repos/{repo}/releases',
        headers={
            'Authorization': f'token {GITHUB_TOKEN}',
            'Accept': 'application/vnd.github.v3+json'
        },
        json=release_data
    )
    
    return response.json()

if __name__ == '__main__':
    import sys
    version = sys.argv[1] if len(sys.argv) > 1 else '1.0.0'
    notes = generate_release_notes(version)
    print(notes)

유용한 팁과 트릭

1. 환경 변수 관리

# ~/.plane_env
export PLANE_API_TOKEN="your_token_here"
export PLANE_API_BASE="https://your-domain.com/api/v1" 
export PLANE_WORKSPACE_ID="workspace_id"
export PLANE_PROJECT_ID="project_id"
export GITHUB_TOKEN="github_token"
export SLACK_WEBHOOK_URL="slack_webhook_url"

# ~/.zshrc에 추가
[ -f ~/.plane_env ] && source ~/.plane_env

2. JSON 데이터 템플릿

# issue_templates.sh
create_bug_report() {
    local title="$1"
    local steps="$2"
    local expected="$3"
    local actual="$4"
    
    local description=$(cat <<EOF
## 🐛 Bug Report

### Steps to Reproduce
$steps

### Expected Behavior
$expected

### Actual Behavior
$actual

### Environment
- OS: $(uname -s)
- Browser: $(echo $USER_AGENT)
- Date: $(date)
EOF
)
    
    plane_issue "🐛 $title" "$description" "high"
}

create_feature_request() {
    local title="$1"
    local user_story="$2"
    local acceptance_criteria="$3"
    
    local description=$(cat <<EOF
## 🚀 Feature Request

### User Story
$user_story

### Acceptance Criteria
$acceptance_criteria

### Additional Context
- Priority: Medium
- Estimated effort: TBD
EOF
)
    
    plane_issue "🚀 $title" "$description" "medium"
}

3. 배치 작업 스크립트

# batch_operations.sh

# 모든 이슈에 라벨 일괄 적용
bulk_add_label() {
    local label_id="$1"
    local filter="$2"  # 예: "priority=high"
    
    local issues=$(plane_api GET "/workspaces/$PLANE_WORKSPACE_ID/projects/$PLANE_PROJECT_ID/issues/?$filter")
    
    echo "$issues" | jq -r '.[].id' | while read issue_id; do
        plane_api POST "/workspaces/$PLANE_WORKSPACE_ID/projects/$PLANE_PROJECT_ID/issues/$issue_id/issue-labels/" \
                  "{\"labels\": [\"$label_id\"]}"
        echo "✅ 라벨이 $issue_id에 추가되었습니다."
    done
}

# 완료된 이슈들을 아카이브로 이동
archive_completed_issues() {
    local days_ago="${1:-30}"  # 기본 30일 전
    local cutoff_date=$(date -I -d "$days_ago days ago")
    
    local completed_issues=$(plane_api GET "/workspaces/$PLANE_WORKSPACE_ID/projects/$PLANE_PROJECT_ID/issues/" | \
                            jq -r --arg date "$cutoff_date" \
                            '.[] | select(.state_detail.group == "completed" and .completed_at[0:10] < $date) | .id')
    
    echo "$completed_issues" | while read issue_id; do
        plane_api PATCH "/workspaces/$PLANE_WORKSPACE_ID/projects/$PLANE_PROJECT_ID/issues/$issue_id/" \
                  '{"archived_at": "'$(date -Iseconds)'"}'
        echo "📦 이슈 $issue_id가 아카이브되었습니다."
    done
}

모니터링과 알림

1. 프로젝트 건강도 체크

# health_check.sh
#!/bin/bash

project_health_check() {
    local issues=$(plane_api GET "/workspaces/$PLANE_WORKSPACE_ID/projects/$PLANE_PROJECT_ID/issues/")
    
    # 기본 통계
    local total_issues=$(echo "$issues" | jq 'length')
    local overdue_issues=$(echo "$issues" | jq --arg today "$(date -I)" \
                          '[.[] | select(.target_date != null and .target_date < $today and .state_detail.group != "completed")] | length')
    local unassigned_issues=$(echo "$issues" | jq '[.[] | select(.assignees | length == 0)] | length')
    local high_priority_open=$(echo "$issues" | jq '[.[] | select(.priority == "urgent" or .priority == "high") | select(.state_detail.group != "completed")] | length')
    
    # 건강도 점수 계산 (100점 만점)
    local health_score=100
    
    # 과도한 미할당 이슈
    if [ $unassigned_issues -gt $((total_issues / 4)) ]; then
        health_score=$((health_score - 20))
    fi
    
    # 연체된 이슈
    if [ $overdue_issues -gt 0 ]; then
        health_score=$((health_score - overdue_issues * 5))
    fi
    
    # 높은 우선순위 이슈가 너무 많음
    if [ $high_priority_open -gt $((total_issues / 3)) ]; then
        health_score=$((health_score - 15))
    fi
    
    # 최소 점수 보장
    [ $health_score -lt 0 ] && health_score=0
    
    # 결과 출력
    local status_emoji="✅"
    [ $health_score -lt 70 ] && status_emoji="⚠️"
    [ $health_score -lt 50 ] && status_emoji="🚨"
    
    local report=$(cat <<EOF
$status_emoji **프로젝트 건강도 리포트** 
점수: $health_score/100

📊 **통계**
- 전체 이슈: $total_issues개
- 연체된 이슈: $overdue_issues개  
- 미할당 이슈: $unassigned_issues개
- 높은 우선순위 (미완료): $high_priority_open개

🎯 **권장사항**
EOF
)
    
    if [ $overdue_issues -gt 0 ]; then
        report="$report\n- 연체된 이슈 $overdue_issues개를 우선 처리하세요"
    fi
    
    if [ $unassigned_issues -gt 5 ]; then
        report="$report\n- 미할당 이슈 $unassigned_issues개에 담당자를 지정하세요"
    fi
    
    echo -e "$report"
    
    # 심각한 상황일 때 알림
    if [ $health_score -lt 50 ]; then
        slack_notify "$report" "#alerts"
    fi
}

# 매일 오전 8시에 건강도 체크
# 0 8 * * * /path/to/health_check.sh

시리즈 연결

이 글은 Plane 완전 정복 시리즈의 두 번째 글입니다:

시리즈 목록

  1. Plane 프로젝트 관리 도구 완전 가이드 - 기본 설치와 설정
  2. Plane GitHub 연동 완전 정복 (현재 글) - API 자동화와 워크플로우 최적화
  3. Plane 고급 커스터마이징과 확장 (예정) - 플러그인 개발과 고급 설정

결론

Plane과 GitHub의 완벽한 연동을 통해 개발 생산성을 극대화할 수 있는 다양한 방법들을 살펴봤습니다. 단순한 이슈 추적을 넘어서 스마트한 자동화로 팀의 워크플로우를 혁신할 수 있습니다.

🚀 핵심 성과

  1. 시간 절약: 터미널 alias로 반복 작업을 90% 단축
  2. 자동화: GitHub Webhooks로 수동 동기화 작업 제거
  3. 통합 워크플로우: 개발부터 배포까지 끊김없는 연결
  4. 팀 협업: Slack 통합으로 실시간 소통 강화
  5. 데이터 기반 의사결정: 자동 리포트로 프로젝트 건강도 모니터링

💡 다음 단계

이제 여러분의 프로젝트에 이 가이드를 적용해보세요:

# 환경 설정
source ~/.plane_env

# 첫 번째 자동화된 이슈 생성
pi "API 자동화 테스트" "GitHub 연동 스크립트 테스트용 이슈입니다."

# 대시보드 확인  
pdash

# 팀에게 공유
slack_notify "🎉 Plane 자동화가 설정되었습니다! 이제 더 스마트하게 일할 수 있어요."

36.9k⭐ GitHub 스타를 받은 Plane에 이런 자동화 기능까지 더해지면, 정말 강력한 프로젝트 관리 환경을 구축할 수 있습니다.

다음 시리즈에서는 Plane 플러그인 개발고급 커스터마이징을 다룰 예정입니다. 더 궁금한 점이 있으시면 댓글로 남겨주세요!


🔗 참고 자료