본문 바로가기

좋아하는 것_매직IT/7.gin

Golang 과 텔레그램 봇을 활용한 구글 스프레드시트 모니터링

반응형

블로그 목적 

Golang 과 텔레그램 봇(bot)을 활용한 구글 스프레드 시트 모니터링 기능에 대해서 공부및 정리후 나만의 노하우와 지식을 공유한다.

블로그 요약

1. 텔레그램 봇(Bot)를 생성해본다.
2. Golang 프로그래밍과 구글 스프레드 시트를 연동해서 텔레그램 봇을 통해 변동사항을 모니터링 해본다.  


블로그 상세내용

같은 주제 하에 각기 다른 사람이 작성한 내용을 취합해 하나의 파일로 만들어 본 경험이 다들 있으실 겁니다. 
업무상 첨부파일로 주고 받기에는 내용의 변동이 너무 잦아서 번거로울 때 한 번쯤 이용해 봤을 구글 시트
(데보션에서도 활용을 많이 하죠?)

참고로 구글 스프레드 시트는 구글에서 제공하는 무료 웹 기반 소프트웨어 중 하나입니다.

그런데 굳이 구글 스프레드 시트의 변동사항을 직접확인하지 않고 구글 스프레드 시트의 변동사항을 어떻게 하면 손쉽게 모니터링 할 수 있을까? 라는 고민해서 시작한게 바로 오늘의 블로그의 내용입니다. 

예를들어 재택과 회사출근에 대해서 공유를 하기 위해서 구글 스프레드 시트를 활용할 수 있는데요
간단하게 주간출근계획에 대해서 공유 폼을 작성해 보면 아래와 같은 형식이 되겠죠?

우선, 텔레그램 봇 생성하기 입니다. 
아래와 같이 텔레그램 검색창에 @BotFather 를 검색해봅니다. 

검색하면요....위와 같이 유사한 bot이 많이 나오는데요...
저는 가장 위에 나오는 @BotFather 를 선택하려고 합니다. 

선택하면, 아래와 같이 나오고요..
맨 하단의 START(시작) 을 클릭합니다. 

시작버튼을 누르시면 아래와 같이 여러가지 명령어를 소개를 받게되고요..

그중에 "메시지를 작성하는 공간에 새로운 bot" 을 생성하겠다라는 의미로 아래 명령어를 입력합니다. 
 - /newbot

/newbot 이라는 명령어를 입력하면요.. 
아래와 같이 메시지가 출력되고요...

간단하게 번역하자면, 새로운 bot의 이름을 정하라는 메시지입니다. 
저는 magic118 을 입력하겠습니다. 
그다음 bot의 이름을 정했으니, "bot"으로 끝나는 이름을 알려달라는 이야기고요..
그래서 저는 magic118Bot 을 입력합니다. 

이제 정상적으로 bot 생성이 완료된건데요....
그리고, HTTP API에 접근할 수 있는 토큰이 생성되었습니다.
(참고로, 우리는 추후에 이 토큰을 활용할것이고요..저는 빨간색으로 감추어두었습니다. ㅎㅎ)

그리고 해당 magic118Bot 찾아서  /start 명령을 통해서 활성화 해줍니다. 

여기까지가 텔레그램관련 Bot을 생성하는 방법이고요..(참쉽죠??)

그럼 본격적으로 구글시트 연동에 대해서 알아보겠습니다. 

우선, 구글 사용자 인증 설정을 해야하는데요. 다음과 같은 3가지 절차로 이루어집니다. 
하나, 만약 프로젝트가 없다면 GCP 사용자 인증정보 사이트에 접속하여 프로젝트를 생성하셔야합니다.
둘, 생성한 프로젝트 하에서 "API 사용자 인증 정보 만들기 > 서비스 계정 키" 생성해야 합니다. 
셋, API 사용 설정을 해야합니다.

하나, 프로젝트 생성을 위해서는 아래 사이트에 로그인을 합니다 .

https://console.cloud.google.com/

 

Google 클라우드 플랫폼

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

accounts.google.com

그리고, 아래와 같이 새프로젝트를 만듭니다.
저는 monitoring-google-sheet 라고 만들었습니다. 

둘, 생성한 프로젝트 하에서 "API 사용자 인증 정보 만들기" > 서비스 계정 키’ 생성
새 서비스 계정 만들기를 선택하여 이름과 역할을 지정해주고 JSON 유형의 키를 생성하면 JSON 파일이 내려받아집니다.

완료버튼을 누르게 되면 아래와 같이 서비스 계정이 생깁니다. 

서비스계정을 클릭하시고, 서비스 계정키를 만드시면됩니다. 

그럼 아래와 같이 키관련 json 파일이 다운로드가 됩니다. 

주요 형식은 아래와 같습니다. (주요정보는 가리겠습니다. ^^)

{
  "type": "service_account",
  "project_id": "steel-totality-423100-a7",
  "private_key_id": "...",
  "private_key": "-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n",
  "client_email": "djlee118-google-sheet@....",
  "client_id": ".....",
  "auth_uri": "https://accounts.google.com/o/oauth2/auth",
  "token_uri": "https://oauth2.googleapis.com/token",
  "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
  "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/.......com",
  "universe_domain": "googleapis.com"
}

셋, API 사용 설정을 해야합니다.

 

그리고 모니터링하고자 하는 구글시트에 아래와 같이 해당 client_email을 공유해 줍니다. 

 

그럼, 구글시트를 모니터링하기위한 사전작업은 마무리가 되었고요.

golang 으로 해당 구글 시트를 모니터링하는 간단한 프로그램을 구현해볼께요~

package main

import (
	"context"
	"fmt"
	"reflect"
	"strconv"
	"time"
	"unicode/utf8"

	"google.golang.org/api/option"
	"google.golang.org/api/sheets/v4"
	tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api"
)

// 텔레그램 봇 토큰
const telegramBotToken = "발급받은 본인의 텔레그램 토큰"

// 텔레그램 채널 ID (봇을 초대한 채널 ID)
const telegramChannelID = ${봇을 초대한 채널ID}

var prevData [][]interface{}

func MonitoringGoogleSheet() {
	// Google Sheets API 사용을 위한 권한 파일 경로
	credentialsFile := "발급받은 본인의 credentialsFile"

	// Google Sheets 서비스 생성
	srv, err := createSheetsService(credentialsFile)
	if err != nil {
		fmt.Println("Unable to create Sheets service: %v", err)
	}

	// 변경사항을 확인할 스프레드시트 ID
	spreadsheetID := "1G2tcPf-i875DajawqwMr_HcMfFavAFQ2bK4DEpnTILI"
	
	// 변경사항을 확인할 시트 이름
	sheetName := "2024출근"

	// 변경사항을 실시간으로 감지하기 위해 백그라운드에서 실행
	go watchChanges(srv, spreadsheetID, sheetName)
}

// Google Sheets 서비스 생성 함수
func createSheetsService(credentialsFile string) (*sheets.Service, error) {
	ctx := context.Background()

	// 서비스 생성
	srv, err := sheets.NewService(ctx, option.WithCredentialsFile(credentialsFile))
	if err != nil {
		return nil, fmt.Errorf("unable to create Google Sheets service: %v", err)
	}

	return srv, nil
}

// 변경사항을 감지하여 출력하는 함수
func watchChanges(srv *sheets.Service, spreadsheetID, sheetName string) {
	ctx := context.Background()

	rangeData := sheetName + "!A:Z"

	// 변경 사항 감지 루프
	for {
		time.Sleep(5 * time.Second) // 5초마다 감지

		// 변경 사항을 감지하기 위해 변경내역 가져오기
		resp, err := srv.Spreadsheets.Values.Get(spreadsheetID, rangeData).Context(ctx).Do()
		if err != nil {
			fmt.Println("Error retrieving changes: %v\n", err)
			continue
		}

		// 변경 사항이 있는지 확인
		if len(resp.Values) > 0 {

			// 변경된 값 출력
			compareAndPrintChanges(resp.Values)

		} else {
			fmt.Println("No changes detected.")
		}
	}
}

// 변경된 내용을 비교하고 출력하는 함수
func compareAndPrintChanges(currentData [][]interface{}) {

	// 이전 데이터가 없다면 현재 데이터를 이전 데이터로 설정하고 종료
	if prevData == nil {
		prevData = currentData
		return
	}

	// 현재 데이터와 이전 데이터의 행 수 비교
	prevRows := len(prevData)
	currentRows := len(currentData)	
	rows := prevRows

	if currentRows > rows {
		rows = currentRows
	}

	for i := 0; i < rows; i++ {
		// 현재 셀이나 이전 셀이 존재하지 않는 경우 건너뜀
		var prevRow, currentRow []interface{}
		if i < prevRows {
			prevRow = prevData[i]
		}
		if i < currentRows {
			currentRow = currentData[i]
		}

		// 열 수 비교
		prevCols := len(prevRow)
		currentCols := len(currentRow)
		cols := prevCols
		if currentCols > cols {
			cols = currentCols
		}

		for j := 0; j < cols; j++ {
			var prevCell, currentCell interface{}
			if j < prevCols {
				if prevRow[j] == nil {
					prevCell = ""
				} else {
					prevCell = prevRow[j]
				}
			}
			if j < currentCols {
				if currentRow[j] == nil {
					currentCell = ""
				} else {
					currentCell = currentRow[j]
				}
			}

			// 이전 데이터와 현재 데이터가 서로 다른 경우에만 변경된 셀로 간주
			if !reflect.DeepEqual(prevCell, currentCell) {

				prevCellStr := fmt.Sprintf("%s", prevCell)
				currentCellStr := fmt.Sprintf("%s", currentCell)

				if hasHangul(prevCellStr) || hasHangul(currentCellStr) {
					numStr := fmt.Sprintf("%s", currentData[i][0])

					num, err := strconv.Atoi(numStr)
					if err != nil {
						fmt.Println("문자열을 정수로 변환하는 데 실패했습니다.")
						return
					}

					var status string

					if hasHangul(prevCellStr) {
						status = "취소"
						
						if hasHangul(currentCellStr) {
							status = "취소후 등록"
						}
					} else {
						status = "등록"
					}


					message := fmt.Sprintf("주간 출근 계획 변동!\n[%s]%s\n변경 Cell [%d,%d]: [%v] -> [%v]", status, currentData[i-num][j], i+1, j+1, prevCell, currentCell)

					sendToTelegram(message)
				}
			}
		}
	}

	// 이전 데이터 업데이트
	prevData = currentData
}

func hasHangul(s string) bool {
	for _, r := range s {
		if utf8.RuneLen(r) > 1 { // 한글은 UTF-8에서 여러 바이트를 차지합니다.
			return true
		}
	}
	return false
}



// 메시지를 텔레그램 채널로 전송하는 함수
func sendToTelegram(message string) error {
	bot, err := tgbotapi.NewBotAPI(telegramBotToken)
	if err != nil {
		return err
	}

	msg := tgbotapi.NewMessage(telegramChannelID, message)
	_, err = bot.Send(msg)
	return err
}

func main() {

	MonitoringGoogleSheet()
	select{}
}

위의 코드를 간단하게 구현해 봤고요...여기서 좀 봐야할곳이 있는데..
우선 모니터링하고자 구글시트 정보가 2개 필요합니다.
바로 구글시트ID과 시트이름 인데요.

아래 그림에서 URI에 구글시트ID 와  구글시트 하단의 시트 이름을 가져와서 golang 프로그램에 세팅해 줍니다. 

아래와 같이 입력해 줍니다.

그리고 아래 3가지 정보도 본인의 정보로 세팅해 줍니다. 
발급받은 본인의 텔레그램 토큰 / 봇을 초대한 채널 ID / 발급받은 본인의 credntialsFile 

그럼 모든 것이 세팅이 되었고요....golang 프로그램을 build 하고 실행해보도록 할께요~

 

그럼, 프로그램이 실행중인 상태고요...

여기서 구글시트를 변경해보도록 하겠습니다. 
시나리오는 첫번째등록 / 두번째등록 / 취소 후 등록 / 취소 로 진행할께요~

1.첫번째 등록 (아이바오 월요일 출근)


2.두번째 등록 (아이바오 목요일 출근)



3.취소후 등록 (아이바오 -> 러바오, 목요일 출근 변경)


4.취소 (러바오 목요일 출근 취소)

위의 시나리오대로 진행하시면 아래와 같이 텔레그램으로 변동사항에 대해서 거의 실시간으로 모니터링 할 수 있습니다. 


여기까지가 오늘의 블로그 내용이고요..
항상 믿고 봐주셔서 감사합니다. 

728x90
300x250