깃헙 이슈 테스트 완료

This commit is contained in:
Lectom C Han
2025-07-21 17:58:09 +09:00
commit d3b8270f41
10 changed files with 939 additions and 0 deletions

206
.gitignore vendored Normal file
View File

@@ -0,0 +1,206 @@
.env
.venv
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[codz]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py.cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# UV
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
#uv.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
#poetry.toml
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
# https://pdm-project.org/en/latest/usage/project/#working-with-version-control
#pdm.lock
#pdm.toml
.pdm-python
.pdm-build/
# pixi
# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
#pixi.lock
# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
# in the .venv directory. It is recommended not to include this directory in version control.
.pixi
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.envrc
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
# Abstra
# Abstra is an AI-powered process automation framework.
# Ignore directories containing user credentials, local state, and settings.
# Learn more at https://abstra.io/docs
.abstra/
# Visual Studio Code
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
# and can be added to the global gitignore or merged into this file. However, if you prefer,
# you could uncomment the following to ignore the entire vscode folder
# .vscode/
# Ruff stuff:
.ruff_cache/
# PyPI configuration file
.pypirc
# Marimo
marimo/_static/
marimo/_lsp/
__marimo__/
# Streamlit
.streamlit/secrets.toml

1
.python-version Normal file
View File

@@ -0,0 +1 @@
3.13

77
README.md Normal file
View File

@@ -0,0 +1,77 @@
# GitHub 이슈 관리 스크립트
이 프로젝트는 CSV 파일을 사용하여 GitHub 이슈를 일괄 생성하고, 모든 이슈를 삭제하는 등의 관리 작업을 자동화하는 Python 스크립트 모음입니다.
## 주요 기능
- **CSV로부터 이슈 일괄 생성**: `github_issues_from_csv.py`
- CSV 파일에 정의된 내용(제목, 본문, 라벨, 담당자, 마일스톤 등)을 바탕으로 GitHub 이슈를 자동으로 생성합니다.
- 필요한 라벨과 마일스톤이 없으면 자동으로 생성합니다.
- **모든 이슈 일괄 삭제**: `delete_all_issues.py`
- 리포지토리에 있는 모든 이슈(Open, Closed 상태 모두)를 영구적으로 삭제합니다.
- 삭제 전 사용자 확인 절차를 거칩니다.
- **샘플 CSV 파일 생성**: `generate_sample_csv.py`
- 이슈 생성 스크립트에서 사용할 수 있는 형식의 샘플 `github_issues.csv` 파일을 생성합니다.
## 사전 준비
1. **Python 설치**: Python 3.6 이상이 필요합니다.
2. **필요 라이브러리 설치**:
```bash
pip install requests
```
3. **GitHub 개인용 액세스 토큰(PAT) 발급**:
- GitHub에서 `repo` 스코프 권한을 가진 개인용 액세스 토큰을 발급받아야 합니다. [GitHub 문서 참고](https://docs.github.com/ko/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens)
- 발급받은 토큰을 환경 변수에 설정합니다.
```bash
# Linux/macOS
export GITHUB_TOKEN='your_personal_access_token'
# Windows (Command Prompt)
set GITHUB_TOKEN='your_personal_access_token'
```
4. **스크립트 내 리포지토리 정보 수정**:
- `delete_all_issues.py`와 `github_issues_from_csv.py` 파일 상단에 있는 `REPO_OWNER`와 `REPO_NAME` 변수를 자신의 GitHub 리포지토리 정보에 맞게 수정해야 합니다.
## 스크립트 사용법
### 1. 샘플 CSV 파일 생성하기
이슈 생성을 테스트하기 위해 샘플 CSV 파일을 생성할 수 있습니다.
```bash
python generate_sample_csv.py
```
- `github_issues.csv` 파일이 생성됩니다. 이 파일을 수정하여 원하<EC9B90><ED9598> 이슈 내용을 추가할 수 있습니다.
### 2. CSV 파일로 GitHub 이슈 생성하기
`github_issues.csv` 파일을 기반으로 이슈를 생성합니다.
```bash
python github_issues_from_csv.py github_issues.csv
```
- **Dry Run**: 실제 생성 없이 실행 결과만 미리 보려면 `--dry-run` 플래그를 사용하세요.
```bash
python github_issues_from_csv.py github_issues.csv --dry-run
```
- **라벨/마일스톤 생성 건너뛰기**: 라벨이나 마일스톤 생성을 원치 않을 경우 아래 플래그를 사용할 수 있습니다.
```bash
python github_issues_from_csv.py github_issues.csv --skip-labels --skip-milestones
```
### 3. 리포지토리의 모든 이슈 삭제하기
**주의: 이 작업은 되돌릴 수 없습니다!**
리포지토리의 모든 이슈를 삭제하려면 아래 명령어를 실행하세요.
```bash
python delete_all_issues.py
```
- 스크립트가 실행되면 삭제할 이슈 목록을 보여주고, 최종 확인을 위해 `yes`를 입력해야만 삭제가 진행됩니다.

130
delete_all_issues.py Normal file
View File

@@ -0,0 +1,130 @@
import requests
import os
# --- 설정 ---
# GitHub 개인용 액세스 토큰(PAT)이 필요합니다. 'repo' 권한이 필요합니다.
# 보안을 위해 환경 변수로 설정하는 것을 권장합니다.
# Linux/macOS: export GITHUB_TOKEN='your_token_here'
# Windows: set GITHUB_TOKEN='your_token_here'
GITHUB_TOKEN = os.getenv('GITHUB_TOKEN')
REPO_OWNER = 'baron-consultant' # 리포지토리 소유자 (예: 'octocat')
REPO_NAME = 'llm-gateway-plan' # 리포지토리 이름 (예: 'Hello-World')
# --- ---
GRAPHQL_URL = 'https://api.github.com/graphql'
HEADERS = {
'Authorization': f'bearer {GITHUB_TOKEN}',
'Content-Type': 'application/json',
}
def run_graphql_query(query, variables):
"""GraphQL 쿼리를 실행하고 결과를 반환하는 헬퍼 함수"""
response = requests.post(
GRAPHQL_URL,
headers=HEADERS,
json={'query': query, 'variables': variables}
)
response.raise_for_status()
json_response = response.json()
if 'errors' in json_response:
raise Exception(f"GraphQL query failed: {json_response['errors']}")
return json_response['data']
def get_all_issue_ids():
"""리포지토리의 모든 이슈 ID (GraphQL Node ID)를 가져옵니다."""
issue_details = []
has_next_page = True
cursor = None
get_issues_query = """
query($owner: String!, $name: String!, $cursor: String) {
repository(owner: $owner, name: $name) {
issues(first: 100, after: $cursor, states: [OPEN, CLOSED]) {
nodes {
id
number
title
}
pageInfo {
endCursor
hasNextPage
}
}
}
}
"""
print(f"{REPO_OWNER}/{REPO_NAME} 리포지토리에서 모든 이슈를 가져오는 중...")
while has_next_page:
variables = {"owner": REPO_OWNER, "name": REPO_NAME, "cursor": cursor}
data = run_graphql_query(get_issues_query, variables)
issues_data = data['repository']['issues']
for issue in issues_data['nodes']:
issue_details.append({
"id": issue['id'],
"number": issue['number'],
"title": issue['title']
})
has_next_page = issues_data['pageInfo']['hasNextPage']
cursor = issues_data['pageInfo']['endCursor']
print(f" 지금까지 찾은 이슈: {len(issue_details)}")
return issue_details
def delete_issue(issue_id, issue_number, issue_title):
"""GraphQL `deleteIssue` 뮤테이션을 사용하여 이슈를 삭제합니다."""
delete_mutation = """
mutation($issueId: ID!) {
deleteIssue(input: {issueId: $issueId}) {
clientMutationId
}
}
"""
variables = {"issueId": issue_id}
try:
run_graphql_query(delete_mutation, variables)
print(f"성공적으로 이슈 #{issue_number}를 삭제했습니다: '{issue_title}'")
except Exception as e:
print(f"이슈 #{issue_number} 삭제 실패: {e}")
def main():
"""메인 실행 함수"""
if not GITHUB_TOKEN:
print("오류: GITHUB_TOKEN 환경 변수가 설정되지 않았습니다.")
print("'repo' 권한을 가진 GitHub 개인용 액세스 토큰을 설정해주세요.")
return
if REPO_OWNER == 'your_repo_owner' or REPO_NAME == 'your_repo_name':
print("오류: 스크립트 상단의 REPO_OWNER와 REPO_NAME을 당신의 리포지토리 정보로 수정해주세요.")
return
try:
issues_to_delete = get_all_issue_ids()
if not issues_to_delete:
print(f"{REPO_OWNER}/{REPO_NAME} 리포지토리에서 이슈를 찾지 못했습니다.")
return
print(f"\n{len(issues_to_delete)}개의 이슈를 {REPO_OWNER}/{REPO_NAME}에서 찾았습니다.")
# 되돌릴 수 없는 작업이므로 사용자 확인을 받습니다.
confirm = input("이 모든 이슈를 영구적으로 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다. (yes/no): ").lower()
if confirm != 'yes':
print("삭제 작업이 취소되었습니다.")
return
print("\n삭제를 시작합니다...")
for issue in issues_to_delete:
delete_issue(issue['id'], issue['number'], issue['title'])
print("\n모든 이슈 처리가 완료되었습니다.")
except Exception as e:
print(f"\n오류가 발생했습니다: {e}")
if __name__ == '__main__':
main()

94
generate_sample_csv.py Normal file
View File

@@ -0,0 +1,94 @@
#!/usr/bin/env python3
"""
샘플 CSV 파일 생성 스크립트
"""
import csv
def create_sample_csv():
"""샘플 CSV 파일 생성"""
sample_issues = [
{
'title': '[1-1] Docker Compose 인프라 설정',
'body': '''## 목표
PostgreSQL, Redis, Celery 컨테이너를 포함한 기본 인프라를 Docker Compose로 설정합니다.
## 작업 상세
- [ ] Docker Compose 파일 작성 (PostgreSQL, Redis, Celery)
- [ ] 환경 변수 설정 및 관리
- [ ] 네트워크 및 볼륨 설정
- [ ] 헬스체크 설정
## 완료 조건
- Docker Compose로 모든 서비스가 정상 실행되는 것
- 각 서비스 간 통신이 원활한 것
## 예상 작업 시간
1주''',
'labels': 'priority:high,milestone-1,infrastructure',
'assignees': 'developer-a',
'milestone': 1,
'priority': 'High',
'estimated_weeks': 1
},
{
'title': '[1-2] CI/CD 파이프라인 구축',
'body': '''## 목표
GitHub Actions 또는 GitLab CI를 사용하여 자동 빌드/배포 파이프라인을 구축합니다.
## 작업 상세
- [ ] GitHub Actions workflow 파일 작성
- [ ] 자동 테스트 실행 설정
- [ ] Docker 이미지 빌드 및 푸시
- [ ] 배포 자동화 설정
## 완료 조건
- Push 시 자동 빌드/테스트가 실행되는 것
- 성공적인 배포가 자동으로 이루어지는 것''',
'labels': 'priority:high,milestone-1,infrastructure',
'assignees': 'developer-b',
'milestone': 1,
'priority': 'High',
'estimated_weeks': 2
},
{
'title': '[2-1] 기본 CRUD API 구현',
'body': '''## 목표
팀과 사용자 관리를 위한 기본 CRUD API 엔드포인트를 구현합니다.
## 작업 상세
- [ ] /teams CRUD 엔드포인트 구현
- [ ] /users CRUD 엔드포인트 구현
- [ ] 입력 검증 및 에러 처리
## 완료 조건
- 모든 CRUD 작업이 정상 동작하는 것''',
'labels': 'priority:high,milestone-2,api,backend',
'assignees': 'developer-a',
'milestone': 2,
'priority': 'High',
'estimated_weeks': 2
}
]
with open('github_issues.csv', 'w', newline='', encoding='utf-8') as csvfile:
fieldnames = ['title', 'body', 'labels', 'assignees', 'milestone', 'priority', 'estimated_weeks']
writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
writer.writeheader()
for issue in sample_issues:
writer.writerow(issue)
print("✅ 샘플 CSV 파일이 'github_issues.csv'로 생성되었습니다.")
print("\nCSV 파일 구조:")
print("- title: 이슈 제목")
print("- body: 이슈 본문 (마크다운 지원)")
print("- labels: 라벨들 (쉼표로 구분)")
print("- assignees: 담당자들 (쉼표로 구분, GitHub 사용자명)")
print("- milestone: 마일스톤 번호 (1, 2, 3)")
print("- priority: 우선순위")
print("- estimated_weeks: 예상 작업 시간 (주)")
if __name__ == "__main__":
create_sample_csv()

38
github_issues.csv Normal file
View File

@@ -0,0 +1,38 @@
title,body,labels,assignees,milestone,priority,estimated_weeks
[1-1] Docker Compose 인프라 설정,"## 목표
PostgreSQL, Redis, Celery 컨테이너를 포함한 기본 인프라를 Docker Compose로 설정합니다.
## 작업 상세
- [ ] Docker Compose 파일 작성 (PostgreSQL, Redis, Celery)
- [ ] 환경 변수 설정 및 관리
- [ ] 네트워크 및 볼륨 설정
- [ ] 헬스체크 설정
## 완료 조건
- Docker Compose로 모든 서비스가 정상 실행되는 것
- 각 서비스 간 통신이 원활한 것
## 예상 작업 시간
1주","priority:high,milestone-1,infrastructure",developer-a,1,High,1
[1-2] CI/CD 파이프라인 구축,"## 목표
GitHub Actions 또는 GitLab CI를 사용하여 자동 빌드/배포 파이프라인을 구축합니다.
## 작업 상세
- [ ] GitHub Actions workflow 파일 작성
- [ ] 자동 테스트 실행 설정
- [ ] Docker 이미지 빌드 및 푸시
- [ ] 배포 자동화 설정
## 완료 조건
- Push 시 자동 빌드/테스트가 실행되는 것
- 성공적인 배포가 자동으로 이루어지는 것","priority:high,milestone-1,infrastructure",developer-b,1,High,2
[2-1] 기본 CRUD API 구현,"## 목표
팀과 사용자 관리를 위한 기본 CRUD API 엔드포인트를 구현합니다.
## 작업 상세
- [ ] /teams CRUD 엔드포인트 구현
- [ ] /users CRUD 엔드포인트 구현
- [ ] 입력 검증 및 에러 처리
## 완료 조건
- 모든 CRUD 작업이 정상 동작하는 것","priority:high,milestone-2,api,backend",developer-a,2,High,2
1 title body labels assignees milestone priority estimated_weeks
2 [1-1] Docker Compose 인프라 설정 ## 목표 PostgreSQL, Redis, Celery 컨테이너를 포함한 기본 인프라를 Docker Compose로 설정합니다. ## 작업 상세 - [ ] Docker Compose 파일 작성 (PostgreSQL, Redis, Celery) - [ ] 환경 변수 설정 및 관리 - [ ] 네트워크 및 볼륨 설정 - [ ] 헬스체크 설정 ## 완료 조건 - Docker Compose로 모든 서비스가 정상 실행되는 것 - 각 서비스 간 통신이 원활한 것 ## 예상 작업 시간 1주 priority:high,milestone-1,infrastructure developer-a 1 High 1
3 [1-2] CI/CD 파이프라인 구축 ## 목표 GitHub Actions 또는 GitLab CI를 사용하여 자동 빌드/배포 파이프라인을 구축합니다. ## 작업 상세 - [ ] GitHub Actions workflow 파일 작성 - [ ] 자동 테스트 실행 설정 - [ ] Docker 이미지 빌드 및 푸시 - [ ] 배포 자동화 설정 ## 완료 조건 - Push 시 자동 빌드/테스트가 실행되는 것 - 성공적인 배포가 자동으로 이루어지는 것 priority:high,milestone-1,infrastructure developer-b 1 High 2
4 [2-1] 기본 CRUD API 구현 ## 목표 팀과 사용자 관리를 위한 기본 CRUD API 엔드포인트를 구현합니다. ## 작업 상세 - [ ] /teams CRUD 엔드포인트 구현 - [ ] /users CRUD 엔드포인트 구현 - [ ] 입력 검증 및 에러 처리 ## 완료 조건 - 모든 CRUD 작업이 정상 동작하는 것 priority:high,milestone-2,api,backend developer-a 2 High 2

374
github_issues_from_csv.py Normal file
View File

@@ -0,0 +1,374 @@
#!/usr/bin/env python3
"""
GitHub Issues Creator from CSV
CSV 파일을 읽어서 GitHub 이슈를 자동으로 생성하는 스크립트
"""
import os
import csv
import time
from datetime import datetime, timedelta
from typing import List, Dict, Optional
import requests
from dataclasses import dataclass
import argparse
# 설정 상수
GITHUB_OWNER = "baron-consultant" # 실제 GitHub 사용자명으로 변경 필요
GITHUB_REPO = "llm-gateway-plan" # 실제 리포지토리명으로 변경 필요
# 마일스톤 설정 (순서대로 1, 2, 3)
MILESTONES = [
{
"title": "마일스톤 1: 통합 AI 게이트웨이 및 기본 로깅 백엔드 구축",
"description": "모든 LLM 및 내부 분석 모델을 위한 안정적인 단일 엔드포인트 제공 및 로깅 시스템 구축",
"weeks": 12
},
{
"title": "마일스톤 2: API 기반 거버넌스 및 분석 기능 강화",
"description": "플랫폼 관리자를 위한 API 거버넌스 및 사용량 분석 기능 구현",
"weeks": 24
},
{
"title": "마일스톤 3: 성능 및 지능 최적화 (LLM 중심)",
"description": "시맨틱 캐싱, A/B 테스트, 성능 최적화 기능 구현",
"weeks": 40
}
]
@dataclass
class IssueData:
title: str
body: str
labels: List[str]
assignees: List[str]
milestone: int
priority: str
estimated_weeks: int
class GitHubIssueCreator:
def __init__(self, token: str, owner: str = GITHUB_OWNER, repo: str = GITHUB_REPO):
self.token = token
self.owner = owner
self.repo = repo
self.base_url = "https://api.github.com"
self.headers = {
"Authorization": f"Bearer {token}",
"Accept": "application/vnd.github.v3+json",
"X-GitHub-Api-Version": "2022-11-28"
}
self.milestones = {}
self.rate_limit_remaining = 5000 # 초기값
def check_rate_limit(self):
"""API 사용량 확인"""
url = f"{self.base_url}/rate_limit"
response = requests.get(url, headers=self.headers)
if response.status_code == 200:
data = response.json()
self.rate_limit_remaining = data['rate']['remaining']
print(f"남은 API 호출 수: {self.rate_limit_remaining}")
def wait_if_needed(self):
"""API 제한을 고려한 대기"""
if self.rate_limit_remaining < 10:
print("API 사용량이 부족합니다. 1분 대기...")
time.sleep(60)
self.check_rate_limit()
else:
time.sleep(0.5) # 기본 대기 시간
def get_existing_milestones(self) -> Dict[str, int]:
"""기존 마일스톤 조회"""
url = f"{self.base_url}/repos/{self.owner}/{self.repo}/milestones"
response = requests.get(url, headers=self.headers)
if response.status_code == 200:
milestones = response.json()
return {m['title']: m['number'] for m in milestones}
else:
print(f"마일스톤 조회 실패: {response.status_code}")
return {}
def create_milestone(self, title: str, description: str, due_date: str) -> Optional[int]:
"""마일스톤 생성"""
url = f"{self.base_url}/repos/{self.owner}/{self.repo}/milestones"
data = {
"title": title,
"description": description,
"due_on": due_date
}
response = requests.post(url, headers=self.headers, json=data)
if response.status_code == 201:
milestone = response.json()
print(f"✅ 마일스톤 생성 완료: {title}")
return milestone['number']
else:
print(f"❌ 마일스톤 생성 실패: {title}, Status: {response.status_code}")
if response.status_code == 422:
print(f" 오류 내용: {response.text}")
return None
def setup_milestones(self):
"""마일스톤들 설정"""
print("🔄 마일스톤 설정 중...")
self.milestones = self.get_existing_milestones()
for i, milestone_data in enumerate(MILESTONES):
title = milestone_data["title"]
if title not in self.milestones:
due_date = (datetime.now() + timedelta(weeks=milestone_data["weeks"])).strftime("%Y-%m-%dT%H:%M:%SZ")
milestone_number = self.create_milestone(
title,
milestone_data["description"],
due_date
)
if milestone_number:
self.milestones[title] = milestone_number
self.wait_if_needed()
else:
print(f"✅ 기존 마일스톤 사용: {title}")
def get_milestone_number(self, milestone_index: int) -> Optional[int]:
"""마일스톤 번호 조회 (1, 2, 3 형태의 인덱스를 받음)"""
if 1 <= milestone_index <= len(MILESTONES):
milestone_title = MILESTONES[milestone_index - 1]["title"]
return self.milestones.get(milestone_title)
return None
def create_labels(self):
"""필요한 라벨들 생성"""
print("🔄 라벨 설정 중...")
labels_to_create = [
{"name": "priority:high", "color": "d73a4a", "description": "높은 우선순위"},
{"name": "priority:medium", "color": "fbca04", "description": "중간 우선순위"},
{"name": "priority:low", "color": "0e8a16", "description": "낮은 우선순위"},
{"name": "milestone-1", "color": "1f77b4", "description": "마일스톤 1 관련"},
{"name": "milestone-2", "color": "ff7f0e", "description": "마일스톤 2 관련"},
{"name": "milestone-3", "color": "2ca02c", "description": "마일스톤 3 관련"},
{"name": "infrastructure", "color": "8c564b", "description": "인프라 관련"},
{"name": "backend", "color": "e377c2", "description": "백엔드 개발"},
{"name": "database", "color": "7f7f7f", "description": "데이터베이스 관련"},
{"name": "api", "color": "bcbd22", "description": "API 개발"},
{"name": "integration", "color": "17becf", "description": "통합 작업"},
{"name": "documentation", "color": "aec7e8", "description": "문서화"},
{"name": "testing", "color": "ffbb78", "description": "테스트 관련"},
{"name": "optimization", "color": "ff9896", "description": "성능 최적화"},
{"name": "monitoring", "color": "c5b0d5", "description": "모니터링"},
]
url = f"{self.base_url}/repos/{self.owner}/{self.repo}/labels"
# 기존 라벨 조회
existing_labels = requests.get(url, headers=self.headers)
existing_label_names = []
if existing_labels.status_code == 200:
existing_label_names = [label['name'] for label in existing_labels.json()]
created_count = 0
for label in labels_to_create:
if label['name'] not in existing_label_names:
response = requests.post(url, headers=self.headers, json=label)
if response.status_code == 201:
print(f"✅ 라벨 생성: {label['name']}")
created_count += 1
else:
print(f"❌ 라벨 생성 실패: {label['name']}")
self.wait_if_needed()
if created_count == 0:
print("✅ 모든 라벨이 이미 존재합니다.")
def create_issue(self, issue_data: IssueData) -> bool:
"""이슈 생성"""
url = f"{self.base_url}/repos/{self.owner}/{self.repo}/issues"
milestone_number = self.get_milestone_number(issue_data.milestone)
data = {
"title": issue_data.title,
"body": issue_data.body,
"labels": issue_data.labels,
}
# assignees가 비어있지 않은 경우에만 추가
if issue_data.assignees:
data["assignees"] = issue_data.assignees
if milestone_number:
data["milestone"] = milestone_number
response = requests.post(url, headers=self.headers, json=data)
if response.status_code == 201:
issue = response.json()
print(f"✅ 이슈 생성 완료: #{issue['number']} - {issue_data.title}")
return True
else:
print(f"❌ 이슈 생성 실패: {issue_data.title}")
print(f" Status: {response.status_code}")
if response.status_code == 422:
error_data = response.json()
print(f" 오류: {error_data}")
return False
def parse_csv(csv_file: str) -> List[IssueData]:
"""CSV 파일을 파싱하여 IssueData 리스트 반환"""
issues = []
with open(csv_file, 'r', encoding='utf-8') as file:
reader = csv.DictReader(file)
for row in reader:
# 라벨 파싱 (쉼표 구분) - None 값 처리 개선
labels_str = row.get('labels', '') or ''
labels = [label.strip() for label in labels_str.split(',') if label.strip()]
# 담당자 파싱 (쉼표 구분) - None 값 처리 개선
assignees_str = row.get('assignees', '') or ''
assignees = [assignee.strip() for assignee in assignees_str.split(',') if assignee.strip()]
# 마일스톤 파싱 (숫자)
try:
milestone_str = row.get('milestone', '1') or '1'
milestone = int(milestone_str)
except (ValueError, TypeError):
milestone = 1
# 예상 작업 시간 파싱
try:
weeks_str = row.get('estimated_weeks', '1') or '1'
estimated_weeks = int(weeks_str)
except (ValueError, TypeError):
estimated_weeks = 1
# 제목과 본문도 None 값 처리
title = row.get('title', '') or ''
body = row.get('body', '') or ''
priority = row.get('priority', 'Medium') or 'Medium'
issue = IssueData(
title=title.strip(),
body=body.strip(),
labels=labels,
assignees=assignees,
milestone=milestone,
priority=priority.strip(),
estimated_weeks=estimated_weeks
)
if issue.title: # 제목이 있는 경우에만 추가
issues.append(issue)
return issues
def main():
parser = argparse.ArgumentParser(description='CSV에서 GitHub 이슈 생성')
parser.add_argument('csv_file', help='이슈 데이터가 포함된 CSV 파일 경로')
parser.add_argument('--token', help='GitHub 개인 액세스 토큰 (환경변수 GITHUB_TOKEN 사용 가능)')
parser.add_argument('--dry-run', action='store_true', help='실제 생성 없이 미리보기만 실행')
parser.add_argument('--skip-labels', action='store_true', help='라벨 생성 건너뛰기')
parser.add_argument('--skip-milestones', action='store_true', help='마일스톤 생성 건너뛰기')
parser.add_argument('--debug', action='store_true', help='CSV 파일 디버그 정보 표시')
args = parser.parse_args()
# GitHub 토큰 확인
token = args.token or os.environ.get('GITHUB_TOKEN')
if not token:
print("❌ GitHub 토큰이 필요합니다. --token 또는 GITHUB_TOKEN 환경변수를 설정하세요.")
return
# CSV 파일 확인
if not os.path.exists(args.csv_file):
print(f"❌ CSV 파일을 찾을 수 없습니다: {args.csv_file}")
return
# 디버그 모드로 CSV 구조 확인
if args.debug:
print(f"🔍 CSV 파일 디버그 정보: {args.csv_file}")
with open(args.csv_file, 'r', encoding='utf-8') as file:
reader = csv.DictReader(file)
print(f"📋 컬럼: {reader.fieldnames}")
for i, row in enumerate(reader):
if i >= 3: # 처음 3개 행만 표시
break
print(f"🔍 행 {i+1}: {dict(row)}")
return
print(f"📁 CSV 파일 읽는 중: {args.csv_file}")
try:
issues = parse_csv(args.csv_file)
except Exception as e:
print(f"❌ CSV 파일 파싱 중 오류 발생: {e}")
print("💡 --debug 옵션을 사용하여 CSV 파일 구조를 확인해보세요.")
return
if not issues:
print("❌ 생성할 이슈가 없습니다.")
return
print(f"📊 총 {len(issues)}개의 이슈를 발견했습니다.")
if args.dry_run:
print("\n🔍 [DRY RUN] 생성될 이슈들:")
for i, issue in enumerate(issues, 1):
print(f" {i:2d}. [{issue.milestone}] {issue.title}")
print(f" 라벨: {', '.join(issue.labels)}")
print(f" 담당자: {', '.join(issue.assignees)}")
print(f" 우선순위: {issue.priority}")
print()
return
# GitHub API 클라이언트 생성
creator = GitHubIssueCreator(token)
print(f"🎯 GitHub 리포지토리: {GITHUB_OWNER}/{GITHUB_REPO}")
# API 사용량 확인
creator.check_rate_limit()
# 마일스톤 설정
if not args.skip_milestones:
creator.setup_milestones()
# 라벨 설정
if not args.skip_labels:
creator.create_labels()
# 이슈 생성
print(f"\n🚀 {len(issues)}개 이슈 생성 시작...")
success_count = 0
fail_count = 0
for i, issue in enumerate(issues, 1):
print(f"\n[{i}/{len(issues)}] 이슈 생성 중...")
if creator.create_issue(issue):
success_count += 1
else:
fail_count += 1
creator.wait_if_needed()
print(f"\n📈 완료: 성공 {success_count}개, 실패 {fail_count}")
if fail_count > 0:
print("\n⚠️ 일부 이슈 생성에 실패했습니다. 다음 사항을 확인하세요:")
print(" - GitHub 토큰 권한")
print(" - 리포지토리 접근 권한")
print(" - 담당자 사용자명")
print(" - API 사용량 제한")
if __name__ == "__main__":
main()

6
main.py Normal file
View File

@@ -0,0 +1,6 @@
def main():
print("Hello from llm-gateway-plan!")
if __name__ == "__main__":
main()

7
pyproject.toml Normal file
View File

@@ -0,0 +1,7 @@
[project]
name = "llm-gateway-plan"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.13"
dependencies = []

6
requirements.txt Normal file
View File

@@ -0,0 +1,6 @@
argparse==1.4.0
certifi==2025.7.14
charset-normalizer==3.4.2
idna==3.10
requests==2.32.4
urllib3==2.5.0