본문 바로가기
web/AI

[AI] JIRA를 연동하여 주간업무 Agent 만들기(1)

by 뽀리님 2025. 11. 18.

요새 PM 을 하고 있는데 내가 매일같이 하는일은 JIRA 칸반보드 뚫어지게 쳐다보는 일이다 O_O

 

일정이 지연된건 없는지, 이슈가 뭔지, 할당된 업무들은 어디까지 진행이 되고있는지, Due Date 체크등 

 

매일같이 들여다보고 Gantt 차트로 변환해서 진행률 보고 아주 눈이 빠질꺼 같단말이지...

 

아 이거 매일같이 좀 누가 비서로 티켓은 몇개고 어디까지 일이 진행됬고 진행률은 뭐고 이런거 알려주는 비서가 있었음 좋겠는데............... 하지만 내 주변에 나의 잡일을 담당해줄 팀원은 없지 ^^

 

그래서 Agent를 간단하게 만들어보기로 했다.

 

만들고보니 신호등같네..

 

간단하게 요런 구성으로 만들어볼 참이다.

 

지라에서 단순히 이슈만 수집한 후 거기에 대한 리스크관리와 분석은 LLM모델을 써서 시킬 참이다. 

 

 

[ 개발환경 ]

Windows 11

IDE : VsCode

Python 3.13

LLM : gemini-2.5-flash  (다른건 다 유료임 ㅠㅠ)

 

제미나이도 일일토큰 제한이 250이라 이걸 초과하면 과금이 된다 ㅠㅠ

 

 

 

 

☑️ 사전작업

 

1. JIRA에서 토큰 발급 받기

https://id.atlassian.com/manage-profile/security/api-tokens

 

Atlassian account

 

id.atlassian.com

해당 페이지가서 현재 내가 속한 소속의 API 키를 발급받는다!

 

그런다음 API 호출을 위해

 

이메일:[받은토큰] 을 Base64로 인코딩하여 헤더로 쓰자!

 

난 윈도우 계열이라 PowerShell을 이용했다

[Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes("이메일:YOUR_NEW_API_TOKEN"))

 

 

요러면 아주 잘 변환해준다.

 

참고로 API 에 대한 Document는 

https://developer.atlassian.com/cloud/jira/platform/rest/v3/intro/

 

https://developer.atlassian.com/cloud/jira/platform/rest/v3/intro/

 

developer.atlassian.com

여기에 잘 나와있다.

 

 

일단 Postman으로 냅다 호출부터 해보자

[GET] https://[본인소속].atlassian.net/rest/api/3/project
-h Authorization "Basic xxxxx"

 

해보면 잘된다. 좋아 나중에 긁어오면 되겠군

 

 

2. Gemini 토큰 발급하기.

https://aistudio.google.com/api-keys

 

로그인 - Google 계정

이메일 또는 휴대전화

accounts.google.com

 

해당 페이지 접속후 발급할수 있다.

 

토큰에 대한 할당량 확인은

https://console.cloud.google.com/iam-admin

 

Google 클라우드 플랫폼

로그인 Google 클라우드 플랫폼으로 이동

accounts.google.com

해당 페이지에서 확인할 수 있다.

 

할당량 및 시스템 한도에서 

 

 

이와 같이 확인이 가능하다.

 

3. Slack WebHook 추가하기

https://slack.com/apps/A0F7XDUAZ-incoming-webhooks

 

수신 웹후크

> _Please note, this is a legacy custom integration - an outdated way for teams to integrate with Slack. These integrations lack newer features and they will be deprecated and possibly removed in the

slack.com

해당페이지에서 추가할수 있다.

 

✔️ Python API 로 서버 세팅하기

 

세팅전 Python 설치와 아래 해당패키지들은 필수로 설치한다.(나는 vscode 터미널에서 설치했다.)

# 가상환경 생성
py -m venv venv

# 가상환경 활성화
venv\Scripts\activate

# 패키지 설치
pip install -r requirements.txt
pip install google-generativeai
pip install google-genai
pip install --upgrade jira

# .env 파일 만들기
cp .env.example .env
code .env

 

 

1. JIRA API로 이슈 수집하기.

class JiraCollector:
    """JIRA data collector"""
    
    def __init__(self, settings: Settings):
        self.settings = settings
        self.jira = JIRA(
            server=settings.jira_url,
            basic_auth=(settings.jira_email, settings.jira_api_token),
            options={'rest_api_version': '3'}
        )
        self.tz = pytz.timezone(settings.timezone)
    
    def get_my_issues(self) -> List[Dict[str, Any]]:
        """
        Fetch issues based on criteria:
        - Reporter = me
        - Status != Done
        - Due date is this week OR (No due date AND High priority)
        """
        # Get week end date (Sunday)
        now = datetime.now(self.tz)
        week_end = now + timedelta(days=(6 - now.weekday()))
        week_end_str = week_end.strftime('%Y-%m-%d')
        
        # JQL query
        jql = f"""
            project = {self.settings.jira_project_key}
            AND reporter = "{self.settings.jira_email}"
            AND statusCategory != Done
            AND (
                duedate <= {week_end_str}
                OR (duedate is EMPTY AND priority = High)
            )
            ORDER BY priority DESC, duedate ASC
        """
        
        issues = self.jira.search_issues(
            jql,
            maxResults=100,
            fields='summary,status,priority,duedate,assignee,labels,issuetype,updated'
        )
        
        return self._format_issues(issues)
    
    def _format_issues(self, issues) -> List[Dict[str, Any]]:
        """Format issues for analysis"""
        formatted = []
        
        for issue in issues:
            due_date = None
            if issue.fields.duedate:
                due_date = datetime.strptime(issue.fields.duedate, '%Y-%m-%d')
            
            assignee_name = "Unassigned"
            if issue.fields.assignee:
                assignee_name = issue.fields.assignee.displayName
            
            formatted.append({
                'key': issue.key,
                'summary': issue.fields.summary,
                'status': issue.fields.status.name,
                'priority': issue.fields.priority.name,
                'due_date': issue.fields.duedate,
                'assignee': assignee_name,
                'issue_type': issue.fields.issuetype.name,
                'labels': issue.fields.labels,
                'url': f"{self.settings.jira_url}/browse/{issue.key}",
                'updated': issue.fields.updated
            })
        
        return formatted
    
    def get_overdue_count(self, issues: List[Dict[str, Any]]) -> int:
        """Count overdue issues"""
        today = datetime.now(self.tz).date()
        overdue = 0
        
        for issue in issues:
            if issue['due_date']:
                due = datetime.strptime(issue['due_date'], '%Y-%m-%d').date()
                if due < today:
                    overdue += 1
        
        return overdue
    
    def get_high_priority_no_due(self, issues: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
        """Get high priority issues without due date"""
        return [
            issue for issue in issues
            if issue['priority'] == 'High' and not issue['due_date']
        ]

 

내가 속한 프로젝트에서 내가 생성한 이슈중, 완료상태가 아닌, Due date가 이번주 이내인거나, 없는거중 우선순위가 High인 이슈들을 가져온다.

 

 

 

2. Gemini 로 분석하여 보고서 생성하기.

"""
Gemini AI analyzer for JIRA issues
"""
import google.generativeai as genai
from typing import List, Dict, Any
import json
from src.config import Settings


class IssueAnalyzer:
    """Analyze JIRA issues using Gemini AI"""
    
    def __init__(self, settings: Settings):
        genai.configure(api_key=settings.gemini_api_key)
        self.model = genai.GenerativeModel('gemini-2.5-flash')
    
    def analyze_issues(self, issues: List[Dict[str, Any]]) -> str:
        """
        Analyze issues and generate report
        Returns formatted text for Slack
        """
        if not issues:
            return self._generate_no_issues_message()
        
        prompt = self._create_analysis_prompt(issues)
        
        response = self.model.generate_content(prompt)
        
        return response.text
    
    def _create_analysis_prompt(self, issues: List[Dict[str, Any]]) -> str:
        """Create prompt for Gemini"""
        issues_json = json.dumps(issues, ensure_ascii=False, indent=2)
        
        return f"""당신은 프로젝트 관리 AI 어시스턴트입니다.
아래 JIRA 이슈들을 분석하여 PM에게 간결한 일일 리포트를 작성해주세요.

## 이슈 데이터
```json
{issues_json}
```

## 리포트 작성 지침
1. **전체 요약**: 총 이슈 수, 우선순위 분포, 마감 임박 건수
2. **긴급 대응 필요**: 오늘/내일 마감, 지연된 이슈
3. **주의 필요**: High priority인데 Due date가 없는 이슈
4. **담당자별 현황**: assignee별 이슈 수 (미할당 포함)
5. **권장 조치**: 구체적이고 실행 가능한 액션 아이템

## 출력 형식
- Slack 메시지 형식 (마크다운)
- 이모지 적절히 활용
- 각 이슈는 [이슈키](URL) 형식으로 링크
- 간결하고 핵심만 (불필요한 설명 제거)
- 한국어로 작성

리포트를 작성해주세요:"""
    
    def _generate_no_issues_message(self) -> str:
        """Generate message when no issues found"""
        return """🎉 *오늘의 JIRA 리포트*

현재 조건에 맞는 이슈가 없습니다!
- 이번 주 마감 이슈: 없음
- High priority 미할당 이슈: 없음

✅ 모든 이슈가 정리되었거나 다음 주 이후 마감입니다."""

 

 

 

3. Slack에 알림봇으로 보고서 전송하기.

"""
Slack notification module
"""
import requests
import json
from datetime import datetime
from typing import Dict, Any
import pytz
from src.config import Settings


class SlackNotifier:
    """Send notifications to Slack"""
    
    def __init__(self, settings: Settings):
        self.webhook_url = settings.slack_webhook_url
        self.tz = pytz.timezone(settings.timezone)
    
    def send_report(self, report: str, issue_count: int) -> bool:
        """
        Send daily report to Slack
        Returns True if successful
        """
        now = datetime.now(self.tz)
        timestamp = now.strftime('%Y-%m-%d %H:%M KST')
        
        # Build Slack message
        message = {
            "blocks": [
                {
                    "type": "header",
                    "text": {
                        "type": "plain_text",
                        "text": f"📊 JIRA 일일 리포트 ({now.strftime('%m/%d %H:%M')})",
                        "emoji": True
                    }
                },
                {
                    "type": "section",
                    "text": {
                        "type": "mrkdwn",
                        "text": report
                    }
                },
                {
                    "type": "context",
                    "elements": [
                        {
                            "type": "mrkdwn",
                            "text": f"총 {issue_count}개 이슈 | {timestamp}"
                        }
                    ]
                }
            ]
        }
        
        try:
            response = requests.post(
                self.webhook_url,
                data=json.dumps(message),
                headers={'Content-Type': 'application/json'},
                timeout=10
            )
            
            if response.status_code == 200:
                print(f"✅ Slack notification sent successfully at {timestamp}")
                return True
            else:
                print(f"❌ Slack notification failed: {response.status_code} - {response.text}")
                return False
                
        except Exception as e:
            print(f"❌ Error sending Slack notification: {str(e)}")
            return False
    
    def send_error(self, error_message: str) -> bool:
        """Send error notification to Slack"""
        now = datetime.now(self.tz)
        
        message = {
            "blocks": [
                {
                    "type": "header",
                    "text": {
                        "type": "plain_text",
                        "text": "⚠️ JIRA Agent Error",
                        "emoji": True
                    }
                },
                {
                    "type": "section",
                    "text": {
                        "type": "mrkdwn",
                        "text": f"```{error_message}```"
                    }
                },
                {
                    "type": "context",
                    "elements": [
                        {
                            "type": "mrkdwn",
                            "text": f"{now.strftime('%Y-%m-%d %H:%M KST')}"
                        }
                    ]
                }
            ]
        }
        
        try:
            response = requests.post(
                self.webhook_url,
                data=json.dumps(message),
                headers={'Content-Type': 'application/json'},
                timeout=10
            )
            return response.status_code == 200
        except Exception as e:
            print(f"❌ Error sending error notification: {str(e)}")
            return False

 

 

 

 

해보면 요렇게 대충 잘나온다! 

 

흠... 좀 다듬어야 할꺼 같긴한데, 일단 구성은 했따!

 

 

 

 

'web > AI' 카테고리의 다른 글

[AI] RAG 와 VectorDB  (1) 2025.12.17
[AI] Qdrant 로 RAG 구축하기(1)  (1) 2025.12.16
[AI] 챗봇만들기 프로젝트 (1)  (0) 2025.12.04
[AI] MCP 프로토콜  (0) 2025.12.03
[AI] JIRA를 연동하여 주간업무 Agent 만들기(2)  (0) 2025.11.28