코드가 잘못되었다면 기계를 탓하도록 하자.

최초 작성일 : 24.05.31
최종 수정일 : 24.10.18
수정 내역 
- 코드 세부 내용 수정 및 작동 확인 완료(24.10.18)

작성자 : 맹알 (sniwoo98@naver.com)

참고 자료

https://brunch.co.kr/@cocosociety/7

 

자동화 끝판왕 슬랙봇 만들기(입문)

구글스프레드시트 + Apps Script + 슬랙으로 채용운영 효율 개선 | 채용 업무를 하다보면(아마 채용업무 뿐만 아니라 대부분의 일이 그렇듯) 몇가지 불편함, 번거로움이 존재하기 마련이다. 나는 채

brunch.co.kr

 

https://brunch.co.kr/@cocosociety/9

 

자동화 끝판왕 슬랙봇 만들기(2)

슬랙 DM 메시지 사용자 응답 버튼을 구글스프레드시트에 자동 기록하기 | 슬랙봇을 활용하여 워크스페이스에 메시지에 발송하고 버튼 혹은 부수적인 피드백을 통해 응답을 구글스프레드시트에

brunch.co.kr

 

https://brunch.co.kr/@cocosociety/10

 

[자동화 끝판왕] 슬랙봇 메시지로 설문폼 응답 받기

메세지에 버튼, 입력폼 삽입으로 구성원 응답률 높이기 | 구글스프레드시트로 저장 슬랙봇을 활용하여 HR업무에 다양하게 적용 월 평균 60시간 가량의 리소스 효율화를 이뤄낼 수 있었다. 근태,

brunch.co.kr

 

1. 구글 스프레드 시트

1.1 구글 스프레드 시트 열기

https://workspace.google.com/intl/ko/products/sheets/

 

Google Sheets: 온라인 스프레드시트 & 템플릿 | Google Workspace

Sheets의 Gemini를 사용하면 패턴을 감지하고, 제안사항을 제공하며, 시간을 단축하고, 오류를 줄여 데이터를 분석하는 데 도움이 될 수 있습니다.

workspace.google.com

 

우상단의 로그인을 누른 뒤, 구글 아이디로 접속 후 

제일 왼쪽의 빈 스프레드 시트를 만들도록 하자. 

1.2 스프레드 시트 뼈대 작성

시트 기준으로 A열은 이름, C열에 슬랙에 등록된 이메일을 입력하자. 

E열의 정보를 확인해서 AppScript가 출석체크 리마인드 메세지를 보내고

F열에 답변 내용을 기록한다.

A열부터 1이니, 원하는 열이 있다면 그에 맞는 숫자로 아래 템플릿을 수정하자. 

2. AppsScript 설정

2.1 AppsScript 들어가기

출석정보를 받아오는 시트를 연 뒤, 확장 프로그램 - Apps Script 로 앱 스크립트를 연다.

2.2 템플릿 삽입

아래 자동화 코드 템플릿을 넣어주도록 하자.

  • 자동화 코드 템플릿
    const mainSheet = SpreadsheetApp.getActiveSpreadsheet();
     
    ///////////// 데이터 정보를 바꾸려면 여기만 수정하세요///////////////////////////////
     
    // 사용하려는 시트의 이름을 입력해주세요.
    const formSheet = mainSheet.getSheetByName('시트1'); 
     
    // 봇 유저 토큰 정보
    var bot_user_token = "";
     
    // 이름이 기록되는 열 번호
    var name_column = 1; 
     
    // 이메일이 기록되는 열 번호
    var email_column = 3; 
     
    // 출석 정보가 반영되는 열 번호
    var attendance_status_column = 5; 
     
    // 유저의 답변이 저장되는 열 번호
    var answer_column = 6; 
     
    // redash에서 받아오는 인원의 수보다 더 많은 값을 입력해주세요. 
    var student_amount = 10; 
     
    // 개강 일자 
    var startDate = new Date("2024-10-15"); 
     
    // 출석체크 하러가기를 눌렀을 때 나오는 페이지 링크 // "https://"가 포함되어야 합니다.  
    var attend_url = "https://www.naver.com";
     
    ///////////////////////////////////////////////////////////////////////
     
    // 오늘 날짜 구하는 함수로 변환
    var currentDate = new Date();
    var timeDiff = currentDate.getTime() - startDate.getTime();
    var weeksPassed = 1 + Math.floor(timeDiff / (1000 * 60 * 60 * 24 * 7));
    var daysPassed = 1 + Math.floor(timeDiff / (1000 * 60 * 60 * 24));
     
     
     
    function settingMessage() {
      var name = ""; // 이름 변수 선언 
      var email = ""; // 이메일 변수 선언
      for (var i = 1; i <= student_amount; i++) {
        name = formSheet.getRange(i, name_column).getValue(); 
        email = formSheet.getRange(i, email_column).getValue(); 
        if (formSheet.getRange(i, attendance_status_column).isBlank()) {
          var slackID = getSlackIDByEmail(email); // 이메일로 Slack ID 가져오기
          if (slackID) {
            sendMessage(name, slackID);
          }
        }
      }
    }
     
    function getSlackIDByEmail(email) {
      var url = "https://slack.com/api/users.lookupByEmail?email=" + encodeURIComponent(email);
      var options = {
        "method": "get",
        "headers": {
          "Authorization": "Bearer " + bot_user_token,
          "Content-Type": "application/json; charset=utf-8"
        }
      };
      try {
        let response = UrlFetchApp.fetch(url, options);
        var data = JSON.parse(response.getContentText());
        if (data.ok) {
          return data.user.id;
        } else {
          console.log("Failed to get Slack ID for email: " + email);
          return null;
        }
      } catch (err) {
        console.log("Error fetching Slack ID for email: " + email + " - " + err);
        return null;
      }
    }
     
    function sendMessage(name, slackID) {
      console.log("remindMessageSend 진입");
      var payload = {
        channel: slackID,
    	  blocks: [
    		{
    			"type": "header",
    			"text": {
    				"type": "plain_text",
    				"text": "출석체크를 해주세요!",
    				"emoji": true
    			}
    		},
    		{
    			"type": "section",
    			"text": {
    				"type": "mrkdwn",
            "text": "안녕하세요 " + name + "님! 즐거운 " + weeksPassed + "주차 입니다! \n출석체크를 잊으셨다면 오른쪽 버튼을 눌러주세요!"
    			},
    			"accessory": {
    				"type": "button",
    				"text": {
    					"type": "plain_text",
    					"text": "출석체크 하러가기",
    					"emoji": true
    				},
    				"value": "attend_link",
    				"url": attend_url,
    				"action_id": "button-action"
    			}
    		},
    		{
    			"type": "divider"
    		},
    		{
    			"type": "section",
    			"text": {
    				"type": "plain_text",
    				"text": "오늘 모임에 늦거나 참석이 힘드신가요?"
    			},
    			"accessory": {
    				"type": "radio_buttons",
    				"options": [
    					{
    						"text": {
    							"type": "plain_text",
    							"text": "참석이 가능해요.",
    							"emoji": true
    						},
    						"value": "value-1"
    					},
    					{
    						"text": {
    							"type": "plain_text",
    							"text": "오늘 늦을 것 같아요.",
    							"emoji": true
    						},
    						"value": "value-2"
    					},
    					{
    						"text": {
    							"type": "plain_text",
    							"text": "오늘 참석이 어려워요.",
    							"emoji": true
    						},
    						"value": "value-3"
    					}
    				],
    				"action_id": "answer_1_value"
    			}
    		},
    		{
    			"type": "input",
    			"element": {
    				"type": "plain_text_input",
    				"action_id": "answer_2_value"
    			},
    			"label": {
    				"type": "plain_text",
    				"text": "늦거나 참여가 어려우시면 사유가 어떻게 되시나요? ",
    				"emoji": true
    			}
    		},
    		{
    			"type": "actions",
    			"elements": [
    				{
    					"type": "button",
    					"text": {
    						"type": "plain_text",
    						"text": "확인",
    						"emoji": true
    					},
    					"value": "final_submit",
    					"action_id": "final_submit"
    				}
    			]
    		}
    	],
        text: "[알림] 출석체크 리마인드 메세지"
      };
      var options = {
        "method": "post",
        "headers": {
          "Authorization": "Bearer " + bot_user_token,
          "Content-Type": "application/json; charset=utf-8"
        },
        "payload": JSON.stringify(payload)
      };
      try {
        let response = UrlFetchApp.fetch("https://slack.com/api/chat.postMessage", options);
        var data = JSON.parse(response.getContentText());
        console.log(data);
      } catch (err) {
        console.log("Error sending message to Slack ID: " + slackID + " - " + err);
      }
    }
     
    // 슬랙봇에서 무언가 액션이 있을 때 작동하는 함수 doPost(e)
    function doPost(e) {
      var parameter = e.parameter;
      var data = parameter.payload;
      var json = JSON.parse(data);
     
      if (json.actions[0].action_id == "final_submit") {
        var answerID = json.user.id; // 답변한 사람의 id를 변수에 저장
        var answer_1 = json.state.values[Object.keys(json.state.values)[0]].answer_1_value.selected_option.text.text;
        var answer_2 = json.state.values[Object.keys(json.state.values)[1]].answer_2_value.value;
     
        var channel = json.channel.id;
        var ts = json.message.ts;
        
        var email = getEmailBySlackID(answerID);
        if (email) {
          writeResponse(email, answer_1, answer_2);
          messageChangeResult(channel, ts);
        } else {
          console.log("Failed to get email for Slack ID: " + answerID);
        }
      }
    }
     
    function getEmailBySlackID(slackID) {
      var url = "https://slack.com/api/users.info?user=" + slackID;
      var options = {
        "method": "get",
        "headers": {
          "Authorization": "Bearer " + bot_user_token,
          "Content-Type": "application/json; charset=utf-8"
        }
      };
      try {
        let response = UrlFetchApp.fetch(url, options);
        var data = JSON.parse(response.getContentText());
        if (data.ok) {
          return data.user.profile.email;
        } else {
          console.log("Failed to get user info for Slack ID: " + slackID);
          return null;
        }
      } catch (err) {
        console.log("Error fetching user info for Slack ID: " + slackID + " - " + err);
        return null;
      }
    }
     
    // 사용자의 답변을 해당하는 이메일과 매칭하여 시트에 기록해주는 함수
    function writeResponse(email, answer_1, answer_2) {
      console.log("writeResponse 진입");
      for (var i = 1; i <= formSheet.getLastRow(); i++) { // 이거였네 찾았다~~~
        if (formSheet.getRange(i, email_column).getValue() == email) { // 
          if (answer_1 == "참석이 가능해요.") {
            answer_1 = "참석";
          }
          if (answer_1 == "오늘 늦을 것 같아요.") {
            answer_1 = "지각";
          }
          if (answer_1 == "오늘 참석이 어려워요.") {
            answer_1 = "불참";
          }
          formSheet.getRange(i, answer_column).setValue(answer_1);
          formSheet.getRange(i, answer_column).setNote(answer_2);
        }
      }
    }
     
    // 제출 완료 버튼을 누르면 제출 완료 메세지로 바꿔주는 함수
    function messageChangeResult(channel, ts) {
      var payload = {
        channel: channel,
        ts: ts,
        blocks: [
          {
            "type": "divider"
          },
          {
            "type": "section",
            "text": {
              "type": "plain_text",
              "text": "응답이 제출되었습니다. 소중한 의견 감사합니다.",
              "emoji": true
            }
          },
          {
            "type": "divider"
          }
        ]
      };
      var options = {
        "method": "post",
        "headers": {
          "Authorization": "Bearer " + bot_user_token,
          "Content-Type": "application/json; charset=utf-8"
        },
        "payload": JSON.stringify(payload)
      };
      UrlFetchApp.fetch("https://slack.com/api/chat.update", options);
    }

아래 코드 부분은 따로 손 댈 필요 없이,

봇 토큰 정보는 아직 모르니 비워두고 시트의 이름, 열 정보, 인원수, 개강 일자 를 입력하도록 하자.

여기까지 왔다면 앱 스크립트에서 할 일은 거의 끝났다.

2.3 날짜 바꾸기

현재 질문은 ~주차로 표시되는데, 매일 발송하고 싶다면, sendMessage에 weeksPassed 대신 daysPassed 로 교체하도록 하자.

그러면 주차 대신 개강일자 기준 날짜가 계산된다. 

그에 맞게 보낼 문구도 ~주차 가 아닌 ~일차 로 자유롭게 바꾸도록 하자.

3. 슬랙 봇 생성 및 설정

3.1 슬랙봇 생성

이제 슬랙 봇을 만들어보자

https://api.slack.com/

 

AI 업무 관리 및 생산성 도구

Slack은 팀과 커뮤니케이션할 수 있는 새로운 방법입니다. 이메일보다 빠르고, 더 조직적이며, 훨씬 안전합니다.

slack.com

 

슬랙 api 사이트에 들어가서 로그인 한 뒤, 우상단의 Your apps로 들어간다.

우상단의 Create New App 클릭 From scratch 선택

앱 이름과 이 앱을 사용할 슬랙 워크스페이스를 선택해준다.

⚠️ 주의 ) 워크스페이스에 권한이 있어야 선택해서 앱을 추가할 수 있다. 게스트로 추가된 워크스페이스라면 권한을 추가하거나, 권한 있는 계정으로 작업을 하도록 하자.

앱 생성이 끝났다. 앱 설정만 하면 끝이다. 거의 다 왔다.

3.2 슬랙 봇 권한 설정

Scope를 먼저 할당하라고 나와있다. 

아래 초록 버튼 Review Scopes to add를 누른다. 
만약에 해당 버튼이 보이지 않는다면

좌측 메뉴 - Features - OAuth & Permissions 를 통해 들어가도록 하자. 

아래로 내리면 Scopes가 있다. Bot Token Scopes 에 권한을 추가한다.

위 네 개 권한을 추가해준다.

3.3 봇 이름 및 프로필 설정

좌측 메뉴 - Features - App Home

아래로 내리면 Your App’s Presence in Slack 이 있다.

슬랙에서 이 봇이 보이는 이름을 설정 해준다.

처음 항목은 마음대로 적어도 되지만, 두번째 항목은 꼭 영어로 적어야한다.

⚠️ 주의 ) 두 번째 항목은 꼭 영어로 적어야 함.

좌측 메뉴 - Settings - Basic Information에서 아래로 스크롤

꾸미기 전

꾸민 후

꾸며줄 수 있다. 센스를 발휘하여 꾸며주도록 하자.

3.4 슬랙 워크스페이스에 슬랙 봇 설치

좌측 메뉴 - Settings - Install App

Install to Workspace 버튼을 통해 워크스페이스에 봇을 설치하도록 하자.

허용 버튼을 누르면 봇이 슬랙 워크스페이스에 성공적으로 설치된다.

⚠️ 주의 ) 만약에 워크스페이스에 권한이 없다면 여기서 앱이 설치되지 않는다. 권한이 있는 계정으로 앱을 설치하도록 하자.

⚠️주의2 ) 슬랙 무료판에는 앱을 최대 10개만 설치할 수 있다고 하니, 사용하지 않는 앱은 삭제하도록하자.

설치가 완료되었다면, 다음과 같은 화면이 뜰 것이다. 여기서 만들어진 봇 토큰을 처음에 App Script에 봇 토큰 입력하는 부분에 넣도록 하자.

⚠️ 주의 ) 여기서 사용하는 봇 토큰은 외부로 유출되면 안된다. 유출에 조심하도록 하자.

슬랙 앱 설정도 거의 끝났다. 이제 실행할 시간이다.

4. 배포 및 실행

4.1 Apps Script 배포

처음에 비워두었던 봇 유저 토큰 정보를 입력해준다.

우상단 배포 버튼 - 새 배포 클릭

좌상단 유형 선택 톱니바퀴 - 웹 앱 선택

액세스 권한이 있는 사용자 - 모든 사용자로 변경

배포 버튼을 누르도록 하자.

(10.17 변경사항) 액세스를 여기서 받네요.

액세스 받기.

구글에서는 내가 만든 앱이 위험하다고 한다. (진짜 위험한지는 모르겠다.)

고급 - 제목 없는 프로젝트(으)로 이동 을 누르자. (본인은 책임 안 진다.)

허용 버튼

여기서 나오는 웹 앱 - URL 이 필요하다 복사하도록 하자.

4.2 슬랙봇과 연결

Slack api 좌측 메뉴 - Features - Interactivity & Shortcuts

Interactivity를 켜준다.

켜 주면 위와 같이 URL 입력할 수 있는 공간이 있다. 여기에 방금 복사한 URL을 입력하자.

우하단 Save Changes 버튼 잊지 말자.

⚠️ 주의 ) Apps Script에 변경사항이 생기면 다시 배포하고, 새로 생긴 URL을 입력해줘야 한다.

4.3 Apps Script 실행

좌상단의 실행 버튼을 누른다. 

5. 결과

확인을 누르면 응답이 제출된다.

6. 자동 발송

현재는 실행 버튼을 누르면 각 행 데이터를 돌면서 발송하는데, 매 주(매일) 보내려고 하면 AppS Script 좌측 메뉴에 트리거를 이용하도록 하자.

우하단 트리거 추가 클릭

이벤트 소스 선택 - 시간 기반

일 단위로 보내길 원하면, 일 단위 타이머, 주 단위로 보내고 싶다면, 주 단위 타이머

이제 매일 오전 9시~10시 사이에 실행 될 것이다.

7. 유지보수

⚠️ Apps Script 코드 내에 변경이 있을 경우 새로 배포해서 슬랙 api에 Interactivity에 새로 적용해주어야한다.

 

8. 추가기능

설문조사

https://brunch.co.kr/@cocosociety/10

 

[자동화 끝판왕] 슬랙봇 메시지로 설문폼 응답 받기

메세지에 버튼, 입력폼 삽입으로 구성원 응답률 높이기 | 구글스프레드시트로 저장 슬랙봇을 활용하여 HR업무에 다양하게 적용 월 평균 60시간 가량의 리소스 효율화를 이뤄낼 수 있었다. 근태,

brunch.co.kr

 

블록킷 빌더

https://app.slack.com/workspace-signin?redir=%2Fgantry%2Fblock-kit-builder

 

 

 

Slack

nav.top { position: relative; } #page_contents > h1 { width: 920px; margin-right: auto; margin-left: auto; } h2, .align_margin { padding-left: 50px; } .card { width: 920px; margin: 0 auto; .card { width: 880px; } } .linux_col { display: none; } .platform_i

app.slack.com

 

블록 킷 빌더를 통해서 메세지를 간편하게 만들 수 있다.

왼쪽에서 원하는 내용을 넣으면 오른쪽에 코드가 만들어진다.

Apps Script내의 payload의 blocks에 코드를 복사해서 넣으면 보낼 수 있다.

 

 

** 궁금한 점 or 오류 있으시면 오류는 해당 오류 메세지 복사해서 댓글로 달아주시면 확인 후 답변 드리겠습니다.