전편을 안보신분은 #1을 먼저 보실것을 추천드립니다.

 

C#으로 JIRA API REST 제어/연동하기 #1

프로젝트를 진행하다 보면 JIRA를 사용하는 경우가 많다. 흔히 우리가 알고있는 아틀라시안 JIRA를 내가 있는 현장에서도 많이 사용하고 있었다. Atlassian | 소프트웨어 개발 및 협업 도구 전 세계

racer42.tistory.com

#2에서는 메인화면의 구성과 내용에 대해서 설명하겠습니다.

필요했던 기능은 크게 두가지입니다.
  1. JIRA 티켓생성기능(Create Ticket)
  2. JRIA 티켓검색기능(Search Ticket)

JIRA 티켓생성기능(Create Ticket)
이 티켓을 어떻게 만들어야할까...
고민하면서 여러가지 자료를 검토해 보았습니다.
우선 먼저 만들어두신 분들이 있기에 JAVA 관련 자료는 아래를 참고하시면 될것 같습니다.

 

Jira Software Rest API 이용한 이슈 등록

"Jira Rest API 이용한 이슈 등록 "● REST API 기존에 사용하는 서비스에서 이슈를...

blog.naver.com

저는 C#으로 만들예정이므로 아래의 페이지를 참고하였습니다.

 

.NET Framework: 780. C# - JIRA REST API 사용 정리

.NET Framework: 780. C# - JIRA REST API 사용 정리 [링크 복사], [링크+제목 복사] 조회: 6293 글쓴 사람 정성태 (techsharer at outlook.com) 홈페이지 첨부 파일 [jira_rest_api_sample.zip]     부모글 보이기/감추기 C# - J

www.sysnet.pe.kr

JIRA REST API 의 샘플은 공식문서를 참고하였습니다.

 

Jira REST API examples

Jira REST API examples This guide contains different examples of how to use the Jira REST API, including how to query issues, create an issue, edit an issue, and others. The reference documentation for the Jira Server platform REST API is here:  Jira Serv

developer.atlassian.com

우선, 사용되는 클래스 정의는 아래와 같습니다.

using System;
using System.Collections.Generic;

public class Progress
{
    public int progress { get; set; }
    public int total { get; set; }
}

public class Issuetype
{
    public string self { get; set; }
    public string id { get; set; }
    public string description { get; set; }
    public string iconUrl { get; set; }
    public string name { get; set; }
    public bool subtask { get; set; }
}

public class Votes
{
    public string self { get; set; }
    public int votes { get; set; }
    public bool hasVoted { get; set; }
}

public class Resolution
{
    public string self { get; set; }
    public string id { get; set; }
    public string description { get; set; }
    public string name { get; set; }
}

public class AvatarUrls
{
    public string __invalid_name__16x16 { get; set; }
    public string __invalid_name__24x24 { get; set; }
    public string __invalid_name__32x32 { get; set; }
    public string __invalid_name__48x48 { get; set; }
}

public class Reporter
{
    public string self { get; set; }
    public string name { get; set; }
    public string emailAddress { get; set; }
    public AvatarUrls avatarUrls { get; set; }
    public string displayName { get; set; }
    public bool active { get; set; }
}

public class Priority
{
    public string self { get; set; }
    public string iconUrl { get; set; }
    public string name { get; set; }
    public string id { get; set; }
}

public class Watches
{
    public string self { get; set; }
    public int watchCount { get; set; }
    public bool isWatching { get; set; }
}

public class Status
{
    public string self { get; set; }
    public string description { get; set; }
    public string iconUrl { get; set; }
    public string name { get; set; }
    public string id { get; set; }
}

public class AvatarUrls2
{
    public string __invalid_name__16x16 { get; set; }
    public string __invalid_name__24x24 { get; set; }
    public string __invalid_name__32x32 { get; set; }
    public string __invalid_name__48x48 { get; set; }
}

public class Assignee
{
    public string self { get; set; }
    public string name { get; set; }
    public string emailAddress { get; set; }
    public AvatarUrls2 avatarUrls { get; set; }
    public string displayName { get; set; }
    public bool active { get; set; }
}

public class AvatarUrls3
{
    public string __invalid_name__16x16 { get; set; }
    public string __invalid_name__24x24 { get; set; }
    public string __invalid_name__32x32 { get; set; }
    public string __invalid_name__48x48 { get; set; }
}

public class Project
{
    public string self { get; set; }
    public string id { get; set; }
    public string key { get; set; }
    public string name { get; set; }
    public AvatarUrls3 avatarUrls { get; set; }
}

public class Customfield10300
{
    public string self { get; set; }
    public string value { get; set; }
    public string id { get; set; }
}

public class Aggregateprogress
{
    public int progress { get; set; }
    public int total { get; set; }
}

public class Status2
{
    public string self { get; set; }
    public string description { get; set; }
    public string iconUrl { get; set; }
    public string name { get; set; }
    public string id { get; set; }
}

public class Issuetype2
{
    public string self { get; set; }
    public string id { get; set; }
    public string description { get; set; }
    public string iconUrl { get; set; }
    public string name { get; set; }
    public bool subtask { get; set; }
}

public class Fields2
{
    public string summary { get; set; }
    public Status2 status { get; set; }
    public Issuetype2 issuetype { get; set; }
}

public class Parent
{
    public string id { get; set; }
    public string key { get; set; }
    public string self { get; set; }
    public Fields2 fields { get; set; }
}

public class Fields
{
    public string summary { get; set; }
    public Progress progress { get; set; }
    public Issuetype issuetype { get; set; }
    public Votes votes { get; set; }
    public List<object> fixVersions { get; set; }
    public Resolution resolution { get; set; }
    public DateTime? resolutiondate { get; set; }
    public object timespent { get; set; }
    public Reporter reporter { get; set; }
    public object aggregatetimeoriginalestimate { get; set; }
    public DateTime updated { get; set; }
    public DateTime created { get; set; }
    public string description { get; set; }
    public Priority priority { get; set; }
    public string duedate { get; set; }
    public List<object> issuelinks { get; set; }
    public Watches watches { get; set; }
    public object customfield_10600 { get; set; }
    public List<object> subtasks { get; set; }
    public Status status { get; set; }
    public List<object> labels { get; set; }
    public long workratio { get; set; }
    public Assignee assignee { get; set; }
    public string customfield_10500 { get; set; }
    public object aggregatetimeestimate { get; set; }
    public Project project { get; set; }
    public List<object> versions { get; set; }
    public string environment { get; set; }
    public object timeestimate { get; set; }
    public Customfield10300 customfield_10300 { get; set; }
    public Aggregateprogress aggregateprogress { get; set; }
    public DateTime? lastViewed { get; set; }
    public object timeoriginalestimate { get; set; }
    public object aggregatetimespent { get; set; }
    public Parent parent { get; set; }
}

public class Issue
{
    public string expand { get; set; }
    public string id { get; set; }
    public string self { get; set; }
    public string key { get; set; }
    public Fields fields { get; set; }
}

public class SearchResult
{
    public string expand { get; set; }
    public int startAt { get; set; }
    public int maxResults { get; set; }
    public int total { get; set; }
    public List<Issue> issues { get; set; }
}

 

먼저 검색기능을 구현해 보았습니다.

        /// <summary>
        /// 作業中
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private async void btnSearch_Click(object sender, EventArgs e)
        {
            // Grid初期化
            dgDoing.Rows.Clear();

            // 件数カウンター
            lblCount1.Text = 0.ToString();

            // ①.JIRA認証を行う
            Jira jira = new Jira();
            if (await jira.Login(jiraServer, id, password) == false)
            {
                Console.WriteLine("Auth failed: " + id);
                return;
            }

            // ②.アサインを整理する
            assignee = SetAssignMember();

            // ③.fixversionを整理する
            fixversion = SetFixVersion();

            // ③.検索を実施する
            SearchResult resultDoing = await jira.GetIssuesByAssignee(txtDate.Text, fixversion, assignee, setLabels(), 0, true);

            // チケット設定
            ListDoing = resultDoing.issues;

            // ◆◆◆◆◆ DOINGリスト ◆◆◆◆◆
            lblCount1.Text = 0.ToString();
            if (ListDoing != null)
            {
                ListDoing.Sort((a, b) => string.Compare(a.fields.assignee.displayName, b.fields.assignee.displayName));
                ListDoing = resultDoing.issues;
                int count = 1;

                foreach (var issue in ListDoing)
                {
                    dgDoing.Rows.Add(count++, issue.fields.assignee.displayName, jiraUri + issue.key, issue.fields.summary, issue.fields.status.name, issue.fields.updated, issue.fields.duedate);
                }

                if (ListDoing.Count == 51)
                {
                    resultDoing = await jira.GetIssuesByAssignee(txtDate.Text, fixversion, assignee, setLabels(), count, true);
                    ListDoing = resultDoing.issues;
                    ListDoing.Sort((a, b) => string.Compare(a.fields.assignee.displayName, b.fields.assignee.displayName));

                    count = 51;
                    foreach (var issue in ListDoing)
                    {
                        dgDoing.Rows.Add(count++, issue.fields.assignee.displayName, jiraUri + issue.key, issue.fields.summary, issue.fields.status.name, issue.fields.updated, issue.fields.duedate);
                    }
                }

                if (ListDoing.Count == 101)
                {
                    resultDoing = await jira.GetIssuesByAssignee(txtDate.Text, fixversion, assignee, setLabels(), count, true);
                    ListDoing = resultDoing.issues;
                    ListDoing.Sort((a, b) => string.Compare(a.fields.assignee.displayName, b.fields.assignee.displayName));

                    count = 51;
                    foreach (var issue in ListDoing)
                    {
                        dgDoing.Rows.Add(count++, issue.fields.assignee.displayName, jiraUri + issue.key, issue.fields.summary, issue.fields.status.name, issue.fields.updated, issue.fields.duedate);
                    }
                }

                if (ListDoing.Count == 151)
                {
                    resultDoing = await jira.GetIssuesByAssignee(txtDate.Text, fixversion, assignee, setLabels(), count, true);
                    ListDoing = resultDoing.issues;
                    ListDoing.Sort((a, b) => string.Compare(a.fields.assignee.displayName, b.fields.assignee.displayName));

                    count = 51;
                    foreach (var issue in ListDoing)
                    {
                        dgDoing.Rows.Add(count++, issue.fields.assignee.displayName, jiraUri + issue.key, issue.fields.summary, issue.fields.status.name, issue.fields.updated, issue.fields.duedate);
                    }
                }

                // 件数カウンター
                lblCount1.Text = (count - 1).ToString();
            }
        }

여기서 왜 ??
아래처럼 건수별 분기를 하였는지 설명드리자면, json 형식으로 데이터를 수신하면서 최대 수신 문자열을 초과하는 상황이 발생하므로 카운팅된 건수 다음을 다시 가져오게끔 하기위해서 두번째 세번째 요구를 보내고 있습니다.
지금은 200건까지 데이터를 가져오도록 하였는데 더 확장하는것도 가능합니다.

                foreach (var issue in ListDoing)
                {
                    dgDoing.Rows.Add(count++, issue.fields.assignee.displayName, jiraUri + issue.key, issue.fields.summary, issue.fields.status.name, issue.fields.updated, issue.fields.duedate);
                }

                if (ListDoing.Count == 51)
                {
                    resultDoing = await jira.GetIssuesByAssignee(txtDate.Text, fixversion, assignee, setLabels(), count, true);
                    ListDoing = resultDoing.issues;
                    ListDoing.Sort((a, b) => string.Compare(a.fields.assignee.displayName, b.fields.assignee.displayName));

                    count = 51;
                    foreach (var issue in ListDoing)
                    {
                        dgDoing.Rows.Add(count++, issue.fields.assignee.displayName, jiraUri + issue.key, issue.fields.summary, issue.fields.status.name, issue.fields.updated, issue.fields.duedate);
                    }
                }

                if (ListDoing.Count == 101)
                {
                    resultDoing = await jira.GetIssuesByAssignee(txtDate.Text, fixversion, assignee, setLabels(), count, true);
                    ListDoing = resultDoing.issues;
                    ListDoing.Sort((a, b) => string.Compare(a.fields.assignee.displayName, b.fields.assignee.displayName));

                    count = 51;
                    foreach (var issue in ListDoing)
                    {
                        dgDoing.Rows.Add(count++, issue.fields.assignee.displayName, jiraUri + issue.key, issue.fields.summary, issue.fields.status.name, issue.fields.updated, issue.fields.duedate);
                    }
                }

                if (ListDoing.Count == 151)
                {
                    resultDoing = await jira.GetIssuesByAssignee(txtDate.Text, fixversion, assignee, setLabels(), count, true);
                    ListDoing = resultDoing.issues;
                    ListDoing.Sort((a, b) => string.Compare(a.fields.assignee.displayName, b.fields.assignee.displayName));

                    count = 51;
                    foreach (var issue in ListDoing)
                    {
                        dgDoing.Rows.Add(count++, issue.fields.assignee.displayName, jiraUri + issue.key, issue.fields.summary, issue.fields.status.name, issue.fields.updated, issue.fields.duedate);
                    }
                }

실제 검색을 날리는 부분을 살펴보면
등장하는 jql 은 
duedate : 기간
assignee : 담당
fixversion : 픽스버젼
labels : 라벨
project = 프로젝트명 <- 삭제하였음
등이 있습니다.
jql 의 사용법에 대해서는 developer.atlassian.com/server/jira/platform/jira-rest-api-examples/ 페이지를 참고하세요.

        /// <summary>
        /// 複合的な自由検索
        /// </summary>
        /// <param name="date"></param>
        /// <param name="fixversion"></param>
        /// <param name="assignee"></param>
        /// <returns></returns>
        public async Task<SearchResult> GetIssuesByAssignee(string date, string fixversion, string assignee, string labels, int startAt, bool doneJudge)
        {
            string url = _baseUrl + "search?jql=duedate < " + date + "d and assignee in (" + assignee + ")";

            string url2 = 0.Equals(startAt) ?
                string.Empty : " and startAt = " + startAt.ToString();

            url2 = !string.IsNullOrEmpty(fixversion) ?
                " and fixVersion in (" + fixversion + ")" : string.Empty;

            if (!string.IsNullOrEmpty(labels))
            {
                url2 += " and labels in (" + labels + ")";
            }

            string url3 = " and status not in (Closed,Resolved)";
            string url4 = " and status in (Closed,Resolved)";

            HttpResponseMessage hrm = doneJudge ? await _httpClient.GetAsync(url + url2 + url3) : await _httpClient.GetAsync(url + url2 + url4);

            //string text = await hrm.Content.ReadAsStringAsync();
            SearchResult result = Newtonsoft.Json.JsonConvert.DeserializeObject<SearchResult>(await hrm.Content.ReadAsStringAsync());
            return result;
        }

이것을 

Newtonsoft.Json.JsonConvert.DeserializeObject<SearchResult>(await hrm.Content.ReadAsStringAsync());

을 통해서 요청하고 검색결과를 SearchResult 클래스 형태로 담아냅니다.

Done검색에서는 Jql만 조금 바뀐형태인데, Status 의 쿼리를 약간 변형한 형태입니다.

await _httpClient.GetAsync(url + url2 + url4)

url / url2 / url4 를 합쳐서 쿼리를 날리고있습니다.

url3 과 url4 의 차이점

여기까지 완료되면, 아래처럼 검색결과가 나오는것을 보실수 있습니다.

검색결과

데이터 그리드의 링크는 클릭하시면, 지정된 브라우져를 통해서 새창으로 보실수있습니다.

 

DataGridView 클래스 (System.Windows.Forms)

사용자 지정 가능한 표에 데이터를 표시합니다.Displays data in a customizable grid.

docs.microsoft.com

#3에서 등록에 대해서 다루어 보겠습니다.
현재 다루고있는 소스의 전체코드는 아래에 있습니다.

 

 

sungmanko/JiraRestAPI

This is a JIRA ticket automatic creation and management project created in C#. - sungmanko/JiraRestAPI

github.com

 

+ Recent posts