포켓몬 플러그인 만들어서 배포하기

Table of contents

이제 앞서 만든 목업 API와 연동하는 코파일럿 플러그인을 만들 차례 입니다. 기본적으로 팀즈 툴킷이 제공하는 템플릿으로 NPM 검색 플러그인을 만들고 그것의 코드를 포켓몬 API를 호출하여 동작하도록 변경하는 방식으로 진행하겠습니다.


1. 템플릿을 이용하여 프로젝트 만들기

팀즈 툴킷이 제공하는 메세지 익스텐션 템플릿으로 프로젝트를 생성합니다. 앞서 진행한 최초 프로젝트 생성하여 테스트한 내용과 유사합니다.

VS Code를 새 창으로 엽니다. 왼쪽 메뉴에서 팀즈 툴킷 아이콘을 클릭합니다. 패널에서 Create a new App 을 클릭합니다. 생성할 수 있는 팀즈 앱 종류 중에서 Message Extension을 선택하여 클릭합니다.

*******


Custom Search Results 를 선택합니다.

*******


Start with a Bot을 선택합니다.

*******


JavaScript 를 선택합니다.

*******


Default folder를 선택합니다.

*******


플러그인 프로젝트의 이름을 정해 줍니다.

*******


프로젝트가 만들어지고 새 창으로 열립니다.

*******


2. 포켓몬 플러그인으로 코드 수정

플러그인의 코드를 수정하겠습니다.

구체적으로는 세 개의 파일 내용을 수정할 것입니다.

  • searchApp.js
  • pokemonCard.js
  • manifest.json

2-1. searchApp.js 수정

searchApp.js 는 사용자가 입력한 파라미터를 전달받아 데이터 소스 (포켓몬 API)에서 검색결과를 받아 코파일럿에게 반환하는 역할을 합니다. searchApp.js 는 src 폴더 아래에 있습니다.

searchApp.js 파일을 열어 class SearchApp extends TeamsActivityHandler 코드를 찾습니다. 코드의 7번 라인입니다.

*******


이 위치 바로 위에 아래의 코드 세줄을 삽입합니다.

1
2
3
const pokemonCard = require("./adaptiveCards/pokemonCard.json");
const { Console } = require("console");
const { type } = require("os");

그러면 아래와 같이 될 것입니다.

*******


그 바로 아래에 아래의 코드 (두개의 함수) 를 삽입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function search(nameKey, myArray, returnVal){
  for (let i=0; i < myArray.length; i++) {
      if (myArray[i].name === nameKey) {
        if (returnVal){
          return myArray[i].value;
        }
        else{
          return myArray[i];
        }
      }
  }
}

function timestamp(){
  var today = new Date();
  today.setHours(today.getHours() + 9);
  return today.toISOString().replace('T', ' ').substring(0, 19);
}

그러면 아래와 같이 될 것입니다.

*******


코드에서 SearchApp 클래스를 찾아 선택합니다. 이 클래스를 통째로 지우고 교체할 것입니다.

*******


SearchApp 클래스를 아래의 코드로 교체합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
class SearchApp extends TeamsActivityHandler {
  constructor() {
    super();
  }

  // Message extension Code
  // Search.
  async handleTeamsMessagingExtensionQuery(context, query) {
    //const searchQuery = query.parameters[0].value;
    console.log(search('poke_name', query.parameters, false));
    console.log(search('poke_type', query.parameters, false));
    console.log(timestamp());
    
    let poke_name = "", poke_type = "";
    if (search('poke_name', query.parameters, false)){
      poke_name = search('poke_name', query.parameters, false).value;
    }
    if (search('poke_type', query.parameters, false)){
      poke_type = search('poke_type', query.parameters, false).value;
    }

    //주소 변경 필요
    const response = await axios.get(
      `https://**************.azurewebsites.net/api/**********?${querystring.stringify({
        name: poke_name,
        type: poke_type,
      })}`
    );

    const attachments = [];
    response.data.pokemons.forEach((pokemon) => {
      const evols = [];
      if (pokemon.prev_evolution){
        pokemon.prev_evolution.forEach((evol) => {
          evols.push(evol);
        })
      }
      evols.push({
        "num":pokemon.num,
        "name":pokemon.name
      });
      if (pokemon.next_evolution){
        pokemon.next_evolution.forEach((evol) => {
          evols.push(evol);
        })
      }

      let temp_evolimg1 = "", temp_evolimg2 = "", temp_evolimg3 = "", temp_evolimg4 = ""
      let temp_evolname1 = "", temp_evolname2 = "", temp_evolname3 = "", temp_evolname4 = "";
      if (evols[0]) {
        temp_evolimg1 = "http://www.serebii.net/pokemongo/pokemon/" + evols[0].num + ".png";
        temp_evolname1 = evols[0].name;
      }
      if (evols[1]) {
        temp_evolimg2 = "http://www.serebii.net/pokemongo/pokemon/" + evols[1].num + ".png";
        temp_evolname2 = evols[1].name;
      }
      if (evols[2]) {
        temp_evolimg3 = "http://www.serebii.net/pokemongo/pokemon/" + evols[2].num + ".png";
        temp_evolname3 = evols[2].name;
      }
      if (evols[3]) {
        temp_evolimg4 = "http://www.serebii.net/pokemongo/pokemon/" + evols[3].num + ".png";
        temp_evolname4 = evols[3].name;
      }

      const template = new ACData.Template(pokemonCard);
      const card = template.expand({
        $root: {
          num: pokemon.num,
          name: pokemon.name,
          img: pokemon.img,
          type: pokemon.type,
          height: pokemon.height,
          weight: pokemon.weight,
          candy: pokemon.candy,
          weaknesses: pokemon.weaknesses.join(", "),
          evolimg1: temp_evolimg1,
          evolname1: temp_evolname1,
          evolimg2: temp_evolimg2,
          evolname2: temp_evolname2,
          evolimg3: temp_evolimg3,
          evolname3: temp_evolname3,
          evolimg4: temp_evolimg4,
          evolname4: temp_evolname4,
        },
      });
      const preview = CardFactory.heroCard(pokemon.name);
      const attachment = { ...CardFactory.adaptiveCard(card), preview };
      attachments.push(attachment);
    });

    return {
      composeExtension: {
        type: "result",
        attachmentLayout: "list",
        attachments: attachments,
      },
    };
  }
}

코드상의 24행에 위치한 포켓몬 데이터 API 의 주소를 나의 API URL로 교체하셔야 합니다. https://****.azurewebsites.net/api/****

2-2. pokemonCard.json 추가

이제 커스텀 메세지 카드 즉, 어댑티브 카드를 만들어 줄 차례 입니다.

src 폴더 아래에 있는 adaptiveCards 폴더를 선택한 채로 파일추가 버튼을 클릭합니다. 파일 이름은 pokemonCard.json 으로 입력해 주십시오.

*******


json 파일의 내용을 채워줍니다. 채울 코드 내용은 여기 (https://lanslote.github.io/copilot/plugin-ME-HOL/42/) 에 있습니다.

*******


2-3. 플러그인 아이콘 파일 준비

플러그인이 적절한 아이콘을 가지게 되면, 사용자 경험에 많은 도움이 됩니다. 다음 경로에서 두개의 이미지 파일을 다운로드 하여 프로젝트의 appPakage 폴더에 추가해 주십시오.

  • https://lanslote.github.io/copilot/plugin-ME-HOL/assets/40/color_ball.png
  • https://lanslote.github.io/copilot/plugin-ME-HOL/assets/40/outline_ball.png

*******


2-3. manifest.json 수정

이제 매니페스트 파일을 수정할 차례 입니다. 매니페스트 파일은 앱의 중요정보를 가지고 있는 파일로 앱 배포의 핵심이라고 할 수 있습니다. appPakage 폴더 아래에 있는 manafest.json 파일을 열어 아래 항목들을 찾아 변경해 주십시오.

1
2
3
4
"icons": {
  "color": "color_ball.png",
  "outline": "outline_ball.png"
},
1
2
3
4
"name": {
  "short": "PokemonME${{APP_NAME_SUFFIX}}",
  "full": "포켓몬 정보 조회 코파일럿 플러그인"
},
1
2
3
4
"description": {
  "short": "포켓몬 정보를 이름과 타입으로 검색합니다.",
  "full": "포켓몬 정보를 이름과 타입으로 검색합니다. \n예) 꼬부기라는 이름의 포켓몬 정보를 찾아줘 \n예) 전기 타입의 포켓몬 정보를 찾아줘"
},
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
"commands": [
  {
    "id": "nameSearch",
    "context": [
        "compose",
        "commandBox"
    ],
    "description": "이름으로 포켓몬을 검색",
    "title": "포켓몬 이름",
    "type": "query",
    "semanticDescription": "이 커맨드는 제공된 이름으로 찾은 포켓몬의 정보를 받습니다.",
    "parameters": [
        {
            "name": "poke_name",
            "title": "포켓몬 이름",
            "description": "검색할 포켓몬의 이름",
            "inputType": "text",
            "semanticDescription": "이 매개변수는 검색할 포켓몬을 식별하기 위한 것입니다. 사용자는 찾고자 하는 포켓몬의 이름을 이 매개변수의 값으로 제공해야 합니다."
        }
    ]
  },
  {
    "id": "typeSearch",
    "context": [
        "compose",
        "commandBox"
    ],
    "description": "타입으로 포켓몬을 검색",
    "title": "포켓몬 타입",
    "type": "query",
    "semanticDescription": "이 커맨드는 제공된 타입으로 찾은 포켓몬의 정보를 받습니다. 예를 들어 '전기 타입인 포켓몬 찾아줘'라고 사용자가 말하면 검색을 원하는 타입이 '전기'입니다",
    "parameters": [
        {
            "name": "poke_type",
            "title": "포켓몬 타입",
            "description": "검색할 포켓몬의 타입",
            "inputType": "text",
            "semanticDescription": "이 매개변수는 검색할 포켓몬을 식별하기 위한 것입니다. 사용자는 찾고자 하는 포켓몬의 타입을 이 매개변수의 값으로 제공해야 합니다."
        }
    ]
  }
]

3. 포켓몬 플러그인 테스트

플러그인의 코드 수정이 완료되었습니다. 이제 로컬 환경에서 실행해서 정상동작하는지 확인해 보겠습니다.

디버깅을 하기 전에 디버그 환경 설정을 해주어야 합니다. VS Code 왼쪽 메뉴에서 Run and Debug 아이콘을 클릭하여 디버깅 패널을 엽니다. 패널 위쪽에 Debug in Teams (Edge) 혹은 Debug in Teams (Chrome) 을 선택합니다. 그리고 F5 키를 눌러 디버깅을 시작합니다.

*******


프로젝트가 빌드되고 관련 서비스의 프로비저닝이 완료되면 디버깅 실행을 위해 웹브라우저가 뜹니다. (로그인을 요구할 수도 있습니다.)

*******


로그인한 브라우저에서 팀즈가 실행되고 디버깅할 플러그인 앱이 추가를 기다리고 있습니다. 추가 버튼을 클릭합니다.

*******


배포된 앱이 메세지 익스텐션으로 정상동작하는지 확인하는 것이 먼저 입니다. 팀즈 챗의 대화입력 창에서 + 를 클릭하여 메세지 익스텐션 앱의 목록을 부릅니다. 디버깅할 포켓몬 메세지 익스텐션을 찾아 클릭합니다.

*******


포켓몬 메세지 익스텐션은 이름과 타입으로 검색할 수 있습니다. 간단하게 포켓몬 이름을 검색해서 잘 동작하는지 확인합니다. 검색된 포켓몬의 이름 하나를 클릭해 봅니다.

*******


개별 포켓몬의 상세정보를 보여주는 어댑티브 카드가 잘 동작하는지 확인해 봅니다.

*******


메세지 익스텐션으로 정상동작하는 것이 확인되었으니 코파일럿 플러그인으로도 정상동작하는지 확인할 차례 입니다. 팀즈의 M365 Chat 에서 플러그인 목록을 열어 나의 플러그인을 찾아 활성화 시켜줍니다.

*******


플러그인을 사용하는 프롬프트를 적어줍니다. 저는 플러그인이름, 피카츄 포켓몬 찾아줘 라고 입력했습니다.

*******


지금 디버깅 실행중이므로, VS Code 에서 실시간으로 어떤 파라미터가 전달되는지 터미널 창을 통해 볼 수 있습니다.

*******


코파일럿 플러그인으로도 잘 동작하는 것을 확인해 봅니다.

*******


4. 포켓몬 플러그인을 클라우드에 배포

개발된 코파일럿 플러그인을 애저 클라우드에 배포할 차례 입니다.

  • 로컬 환경의 디버깅 앱은 VS Code 로 디버깅할 때에만 동작합니다.
  • 클라우드에 앱을 배포하면 상시 구동상태의 앱이 됩니다.
  • 이 작업의 결과로 패키징 된 앱(zip 파일)을 내 동료에게 전달하여 같이 테스트 해볼 수 있습니다.
  • 애저 구독을 사용하므로 비용이 발생할 수 있습니다.

4-1. 팀즈 툴킷을 프리릴리즈 버전으로 전환

정상적인 상황은 아니지만 팀즈 툴킷의 정식 버전에서는 게스트 계정으로 애저 구독을 사용하는데 문제가 발생하는 경우도 있습니다. 이 경우 팀즈 툴킷을 프리릴리즈 버전으로 바꾸면 문제가 해결되는 경우도 있었습니다.

*******


4-2. 팀즈 툴킷의 Azure 구독 계정 로그인

팀즈 툴킷으로 개발된 앱을 클라우드에 배포하기 위해 애저 구독권한을 가진 계정으로 로그인을 해야 합니다. 기존의 M365 로그인과는 별도로 진행됩니다.

팀즈 툴킷에서 Accounts 섹션아래에 있는 Sign in Azure를 클릭합니다.

*******


비용이 발생할 것을 경고하는 확인창이 뜹니다. 자세히 읽어보고 Sign in 버튼을 클릭합니다.

*******


환경에 따라 사용자의 애저 로그인을 요구할 수도 있습니다.

*******


구독에 적절한 권한이 있는 계정을 선택합니다.

*******


애저 계정 로그인이 완료되었습니다.

*******


4-2. 플러그인을 클라우드에 프로비전

팀즈 툴킷의 패널에서 LifeCycle 섹션 아래에 있는 Provision을 클릭합니다. 리소스 그룹을 새로 만들 건지, 기존의 리소스 그룹을 사용할 것인지 묻습니다. 애저 리소스 관리 정책에 따라 선택하면 됩니다. 이 핸즈온에서는 New resource group을 선택하여 새로 리소스 그룹을 만들겠습니다.

*******


새로 만들 리소스 그룹의 이름을 짓습니다. 될 수 있으면 나중에 찾아서 관리하기 쉬운 이름으로 지어주십시오.

*******


리소스 그룹의 기본 배포 위치를 정해 주십시오.

*******


지금까지 선택한 내용과 이걸 배포하면 비용이 발생할 수 있다는 내용을 확인하는 창이 뜹니다. Provision을 클릭합니다.

*******


프로비전이 완료되었습니다.

*******


4-3. 플러그인을 클라우드에 배포

프로비전에 이어 디플로이 작업도 이어서 진행해야 합니다. 팀즈툴킷의 LifeCycle 섹션에서 Deploy를 클릭합니다. 확인창이 뜨면 Deploy를 클릭합니다.

*******


디플로이 까지도 잘 완료되었습니다.

*******


4-4. 플러그인 테스트

팀즈에 로그인하여 앱 관리 로 이동합니다. 포켓몬앱이름에 dev라는 접미사가 붙은 앱이 이미 배포되어 있는 것을 볼 수 있습니다.

*******


코파일럿에서 플러그인을 사용하기 전에 활성화 해야 합니다.

*******


플러그인이 잘 동작하는지 테스트 해 봅니다.

*******


5. [중요] 배포된 플러그인 앱 비용관리

앞서의 프로비전과 배포절차에서 지속적으로 애저 구독의 비용이 발생할 수 있다는 경고가 뜹니다. 실제로 지금까지의 상태로 배포를 완료하면 월 57,216원 이상의 비용이 발생하여 청구됩니다. “나중에 조치해야지” 해놓고 미뤄두면 나중에 큰 후회를 하실 수 있습니다. 다음과 같은 조치를 통해 테스트 단계의 앱에서 발생하는 비용을 최소화 할 수 있습니다.

애저 포탈에 로그인하여 이 플러그인을 위해 만든 리소스 그룹을 찾습니다.

*******


리소스 그룹안에서 App Service 를 찾습니다.

*******


앱 서비스의 서비스 플랜이 B1 등급인 것이 확인됩니다.

*******


이 앱 서비스의 플랜을 다운그레이드 하려면 사전에 Always on 설정을 꺼야 합니다. 이 옵션을 끄면 아이들 타임(idle time)이 길어지면 최초 실행시 조금 느리게 반응할 수 있습니다.

*******


자동으로 앱이 재시작합니다.

*******


이제 다시 앱서비스 플랜을 클릭합니다.

*******


프라이싱 플랜이 B1 인 것을 확인할 수 있습니다. B1을 클릭합니다.

*******


가격 플랜을 B1 에서 F1 으로 변경 합니다.

*******


Select 버튼을 클릭하면 다운그레이드가 맞냐고 확인합니다. Downgrade 를 클릭합니다.

*******


가격 플랜이 F1 으로 변경되었습니다.

*******



Table of contents