티스토리 뷰

곰두리봇 소개

곰두리봇은 강원대학교 생활에 있어 대학생활 꿀팁, 학식메뉴, 대학공지 등을 알려주는 카카오봇 입니다.

* 카카오톡 친구추가 : knubot

* 곰두리봇 개발 프로젝트 소개 (자세히) : https://kbs4674.tistory.com/60

* Github : https://github.com/kbs4674/kakao-bot_public

 

 

 

코로나가 온 세상에 공포를 떨게한지 약 1개월이 되어갑니다.

하지만 코로나 확진자 수는 줄어들기는 커녕 오히려 증가하고 있는 추세입니다😥

그렇다보니 사람들은 점점 밖에 나가길 꺼려하고, 부득이하게 밖에 나가기 위해선 감염을 최소화 하고자 마스크를 써야하다보니 마스크 구하는게 점점 하늘의 별따기가 되어갑니다.

 

마스크 수량의 부족이 점점 저희 피부에 와닿게 되어, 정부에서는 원할한 수량을 배급하고자 3월 9일부터 마스크 2부제가 시행되었지만, 정작 마스크를 판매하는 약국이 어딘지 모르다보니 초기에는 많은 혼선이 오갔습니다.

 

하지만 그러한 혼선도 잠시, 3월 10일에 정부에서는 마스크를 판매하는 약국 정보를 알려주는 OPEN API 데이터를 공개했습니다!

 

공공데이터에서 공개한 '공적 마스크 판매정보'

마스크를 판매하는 약국의 위치, 마스크 수량이 얼마나 남았는지 대략적으로 알려주는 위 API를 활용해서 곰두리봇에도 바로 적용하게 되었습니다!

 

 

1. 개발 전 API 분석하기

 

3월 13일 기준, 공적 마스크 판매정보는 다른 API와는 특이하게 API 명세서가 data.go.kr에서 안내되는게 아닌 swagger을 통해 API 명세서 및 DEMO를 돌려볼 수 있었습니다. (뭔가 진짜 급하게 내놓은 것 같지만, 그래도 중요한 내용은 다 담겨져 있었습니다.)

 

일단 해당 API 정보에서 안내하는 Swagger API명세서 사이트로 이동했습니다.

 

API는 크게 4개의 URI가 제공됩니다.


1) GET /stores/json : 약국, 우체국, 농협 등의 마스크 판매처 정보 제공 (마스크 재고 관련 정보는 제공하지 않음)

  type 정의
page integer 페이지 번호
perPage integer 한 페이지에 몇 개의 데이터가 보여질 것인지 정의 [500 (min/default) ~ 5000 (max)]

 

2) GET /sales/json : 마스크 재고 상태 등의 판매 정보 제공(판매처 관련 정보는 제공하지 않음)

  type 정의
page integer 페이지 번호
perPage integer 한 페이지당 출력할 판매처 수 [500 (min/default) ~ 5000 (max)]

 

3) GET /storesByGeo/json : 중심 좌표(위/경도)를 기준으로 반경(미터단위) 안에 존재하는 판매처 및 재고 상태 등의 판매 정보 제공

  type 정의
lat number (float) 위도(wgs84 좌표계) / 최소:33.0, 최대:43.0
lng number (float) 경도(wgs84 표준) / 최소:124.0, 최대:132.0
m integer 반경(미터) / 최대 5000(5km)까지 조회 가능

 

4) GET /storesByAddr/json

주소를 기준으로 해당 구 또는 동내에 존재하는 판매처 및 재고 상태 등의 판매 정보 제공.
예- '서울특별시 강남구' or '서울특별시 강남구 논현동'
('서울특별시' 와 같이 '시'단위만 입력하는 것은 불가능합니다.)

  type 정의
address string 검색 기준이 될 주소 (주소 검색에 대해 여러 실험 결과 행정동이랑 법정동이랑 명칭이 같아야 하고, 법정동이어야만 검색이 되는 것 같아보임, 이 규칙을 파악해주는데 도움을 준 같은 카톡방에 계신 하루20 인문🌸 님 감사합니다.)

Swagger 사이트에서 역시 API 통신 결과에 대한 DEMO를 돌려볼 수 있습니다.

 

 

일단 저는 위 API 규칙 중, /storesByAddr/json 하나만을 가지고 곰두리봇에 마스크 조회기능을 추가해보기로 했습니다.

 

 

2. 정부 공공데이터 API ↔ 웹서버 통신

웹서버의 개발은 Ruby on Rails(웹서버) 기반으로 하게 되었습니다.

 

일단 개발의 첫 과정은 정부 공공데이터를 내 웹서버로 가져온다음, API에 살을 붙여야 하는 과정이 필요했습니다.

 

1) 웹서버 ↔ 카카오봇 과의 통신을 고려를 한 밑기반 준비를 합니다.

 

카카오봇↔웹서버 통신 시, 웹서버가 받게되는 Parameter(address)

웹서버는 카카오봇으로 부터 address 라는 이름을 가진 주소 Parameter 를 받고 카카오봇의 요청을 처리하는데, 웹서버는 카카오봇으로 부터 받은 Parameter(adress)를 통해 정부 데이터와 통신을 하게 됩니다.

 

하지만 Parameter에 담겨져 있는 한글은 Unicode 방식으로 전달받다보니 Rails에서는 이를 제대로 인식을 못하는 문제가 발생합니다.

 부록  아스키(ASCII)코드와 유니코드(Unicode)의 이해

 

그래서 저는 Parameter을 UTF-8로 인코딩 및 UTF-8로 된 한글을 다시 인코딩 함으로서 위의 인코딩 인식문제를 해결합니다.

address = params[:address].encode!("UTF-8")
address = ERB::Util.url_encode(address)

 

그리고 공공데이터 와의 API 통신 및 Return 받은 JSON 데이터를 Parsing 합니다.

maskEndPoint = "https://8oi9s0nnth.apigw.ntruss.com/corona19-masks/v1/storesByAddr/json?address=#{address}"
maskRemainJson = RestClient::Request.execute :method => 'GET', :url => maskEndPoint
maskRemainJsonParse = JSON.parse(maskRemainJson)

 

그리고 정부로부터 받은 json 데이터 결과 중, 일부를 재가공 해냅니다.

## maskRemainJsonParse : 공공데이터로 부터 Return받은 JSON 데이터를 가지고있는 변수
# 아래 보여지는 코드에 있어, nil은 NULL이라고 보면 됩니다.

maskRemainJsonParse["stores"].each do |data|
  ## 새로운 JSON Attribute 추가 : 위도/경도를 기반으로 한 구글 지도 URL
  data.store("google_map", "https://www.google.co.kr/maps/search/#{data["lat"]},#{data["lng"]}/@#{data["lat"]},#{data["lng"]}")
        
  if data["stock_at"].nil?
    data["stock_at"] = "전산 확인불가(데이터 없음)"
  end
        
  if data["type"] == "01"
    data["type"] = "약국"
  elsif data["type"] == "02"
    data["type"] = "우체국"
  elsif data["type"] == "03"
    data["type"] = "농협"
  end
        
  if data["remain_stat"] == "empty"
    data["remain_stat"] = "없음 (1개 이하)"
  elsif data["remain_stat"] == "few"
    data["remain_stat"] = "거의 없음 (2~30개)"
  elsif data["remain_stat"] == "some"
    data["remain_stat"] = "조금 있음 (30개~100개)"
  elsif data["remain_stat"] == "plenty"
    data["remain_stat"] = "마스크 수저 (100개 이상)"
  elsif data["remain_stat"].nil?
    data["remain_stat"] = "재고 확인불가(데이터 없음)"
  end
end

 

이제 여기까지가 웹서버↔공공데이터 통신 및 재가공 과정이 되겠습니다.

최종 완성코드는 아래와 같습니다.

def mask_remain
  address = params[:address].encode!("UTF-8")
  address = ERB::Util.url_encode(address)

  maskEndPoint = "https://8oi9s0nnth.apigw.ntruss.com/corona19-masks/v1/storesByAddr/json?address=#{address}"
  maskRemainJson = RestClient::Request.execute :method => 'GET', :url => maskEndPoint
  maskRemainJsonParse = JSON.parse(maskRemainJson)

  maskRemainJsonParse["stores"].each do |data|
    data.store("google_map", "https://www.google.co.kr/maps/search/#{data["lat"]},#{data["lng"]}/@#{data["lat"]},#{data["lng"]}")

    if data["stock_at"].nil?
      data["stock_at"] = "전산 확인불가(데이터 없음)"
    end

    if data["type"] == "01"
      data["type"] = "약국"
    elsif data["type"] == "02"
      data["type"] = "우체국"
    elsif data["type"] == "03"
      data["type"] = "농협"
    end

    if data["remain_stat"] == "empty"
      data["remain_stat"] = "없음 (1개 이하)"
    elsif data["remain_stat"] == "few"
      data["remain_stat"] = "거의 없음 (2~30개)"
    elsif data["remain_stat"] == "some"
      data["remain_stat"] = "조금 있음 (30개~100개)"
    elsif data["remain_stat"] == "plenty"
      data["remain_stat"] = "마스크 수저 (100개 이상)"
    elsif data["remain_stat"].nil?
      data["remain_stat"] = "재고 확인불가(데이터 없음)"
    end
  end

  maskRemainJsonResult = { :query => maskRemainJsonParse["address"], :count => maskRemainJsonParse["count"], :pharmacy_info => maskRemainJsonParse["stores"] }
  render :json => maskRemainJsonResult
end

 

그리고 Rails 웹서버에 통신을 시도 후 얻게되는 JSON 결과는 아래와 같습니다.

(아래 JSON 결과는 Rails 웹서버에서 재가공을 통해 Response를 하는 데이터 입니다.) 

 

 

3. 웹서버 ↔ 카카오봇 통신

카카오봇의 개발은 사용자로부터 전달받은 카카오톡에 대해 자동으로 응답을 해주도록 도와주는 MessengerBot이라는 어플을 이용해서 하게 되었는데, 해당 어플에서는 Customize 코딩을 제공해주는 덕분에 Javascript 을 기반으로 개발을 진행했습니다.

* 안드로이드만 사용 가능하며, iOS는 지원이 안됩니다.

 

이전의 웹개발은 서버를 담당하는 축이었으면, 이번 카카오봇은 웹서버에 요청을 보내는 클라이언트 라고 보면 됩니다.

 

1) 카카오톡에서 사용자로부터 특정 단어를 듣게 될 경우, 자동으로 반응하는 코드를 짜야합니다.

저는 ".마스크 " + "주소" 를 사용자로부터 들을 경우, 이에 응답하도록 코드를 짰습니다.

if(msg.indexOf(".마스크 ") != -1 || msg.indexOf(".ㅁㅅㅋ ") != -1)
{
  address = msg.substring(5,200);
}

address는 ".마스크 " 라는 단어 이후로 입력하는 문장을 가지고 있는 변수입니다.

예를들면 ".마스크 강원도 춘천시 후평동" 이라고 사용자가 카톡을 보낼 경우, address에는 "강원도 춘천시 후평동" 이라는 문장을 가지고 있게 되는 셈입니다.

 

2) 이어서 나의 Rails 웹서버에 문장을 보냅니다.

그리고 return 받은 JSON 데이터에 대해 파싱 후, crawlResult 변수에 담아둡니다.

if(msg.indexOf(".마스크 ") != -1 || msg.indexOf(".ㅁㅅㅋ ") != -1)
{
  address = msg.substring(5,200);
  
  crawl = org.jsoup.Jsoup.connect("http://kakao-bot-api.herokuapp.com/crawl_chun_notice/mask-remain.json/"+ address +"").ignoreContentType(true).get().text();
  crawlResult = JSON.parse(crawl);
}

 

3) 이어서 crawlResult에 담겨져 있는 JSON 데이터를 사용자에게 예쁘게 보여주고자 카카오봇 내에서 재가공 합니다.

if(msg.indexOf(".마스크 ") != -1 || msg.indexOf(".ㅁㅅㅋ ") != -1)
{
  ## [내용 생략] ##

  var arr = [];
  for (var i = 0 ; i < crawlResult.pharmacy_info.length ; i++)
  {
    arr.push([]);
    arr[i][0] = crawlResult.pharmacy_info[i].name;
    arr[i][1] = crawlResult.pharmacy_info[i].type;
    arr[i][2] = crawlResult.pharmacy_info[i].remain_stat;
    arr[i][3] = crawlResult.pharmacy_info[i].addr;
    arr[i][4] = crawlResult.pharmacy_info[i].google_map;
    arr[i][5] = crawlResult.pharmacy_info[i].stock_at;
  }

  var maskInfo = "";
  for (var i = 0 ; i < crawlResult.pharmacy_info.length ; i++)
  {
    maskInfo += "["+ arr[i][0] +" ("+ arr[i][1] +")]\n ↳ "+ arr[i][2] +" [수량 기록 : "+ arr[i][5] +"]\n ↳ "+arr[i][3]+"\n ↳ 구글 지도 : "+ arr[i][4] +"\n\n";
  }
}

 

이제 위 작업을 통해 maskInfo는 아래과 같은 패턴의 문장을 가지고 있게 됩니다.

[안국온누리약국 (약국)]
 ↳ 재고 확인불가(데이터 없음) [수량 기록 : 전산 확인불가(데이터 없음)]
 ↳ 강원도 춘천시 후석로 312 (후평동)
 ↳ 구글 지도 : https://www.google.co.kr/maps/search/37.8789722,127.7508984/@37.8789722,127.7508984

[한일온누리약국 (약국)]
 ↳ 없음 (1개 이하) [수량 기록 : 2020/03/12 09:17:00]
 ↳ 강원도 춘천시 후석로 376 (후평동)
 ↳ 구글 지도 : https://www.google.co.kr/maps/search/37.8842378,127.7476276/@37.8842378,127.7476276
 
 ... (이하 생략) ...

 

마지막으로, 유저에게 자동응답을 할 내용을 꾸며줍니다.

if(msg.indexOf(".마스크 ") != -1 || msg.indexOf(".ㅁㅅㅋ ") != -1)
{
  ## [내용 생략] ##
  
  // 빈 공란 생성
  var blank = "\u200b".repeat(501)
  
  replier.reply(".:: 우리동네 마스크 현황 ::.\n- 검색위치 : "+ crawlResult.query +"\n- 탐색된 마스크 판매처 수 : "+ crawlResult.count +""+ blank +"\n\n"+ maskInfo +"* 외출 후 손씻기 철처!\n* 감염 의심 신고는 국번없이 1339\n\n* 마스크 데이터 출처 : 정부 공공데이터(data.go.kr)\n* 카카오봇 ID : knubot\n* 🎉 피드백 Thanks to : 속초감쟈, 하루, 19학번 국교~!, 김태희 님");
}

 

4) 여기까지 하고 카카오봇에게 톡을 보내볼까요!

오오 일단 응답을 잘 해냈습니다!

하지만 여기서 한가지 문제가 발생합니다.

 

일단 오픈API에서는 경상북도의 줄임말인 경북 이라고 주소를 줄여서 입력을 하면 API가 이를 제대로 이해를 못하는 문제가 발생합니다.

 

이는 카카오봇에도 영향을 끼치다 보니 결국 카카오봇에서도 아래와 같이 데이터를 못찾는 결과를 얻게 됩니다.

이로 인해 서버 혹은 클라이언트에서는 위 사례와 같은 상황을 대비한 alias 처리를 해줘야 합니다.

 

저는 일단 서버에는 큰 부담은 주기 싫어서 클라이언트 쪽(곰두리봇)에서 alias 처리를 시키기로 합니다.

일단 alias 처리를 위해선 string 속에서 단어를 찾아내야 합니다. 저같은 경우는 간단히 indexOf 라는 메소드를 활용했습니다.

 

indexOf는 다음과 같은 규칙이 있습니다.

address = "서울시"

address.indexOf("서울시")
=> -1

address.indexOf("서울특별시")
=> 0

-1을 반환할 경우 "탐색단어가 있음", 0은 "탐색단어가 존재하지 않음" 입니다.

이러한 패턴을 이용해서 다음과 같이 코드를 짜면 됩니다.

if (address.indexOf("서울시") != -1 && address.indexOf("서울특별시") != 0)
{
  address = address.replace("서울시", "서울특별시");
} else if (address.indexOf("서울") != -1 && address.indexOf("서울특별시") != 0) {
  address = address.replace("서울", "서울특별시");
}

 유의사항  if 우선순위

address = "서울시"

if (address.indexOf("서울") != -1 && address.indexOf("서울특별시") != 0)
{
  address = address.replace("서울", "서울특별시");
} else if (address.indexOf("서울시") != -1 && address.indexOf("서울특별시") != 0) {
  address = address.replace("서울시", "서울특별시");
}

만약 제가 위와같이 코드를 짰다고 쳤을 때, 저희가 원래 의도한 정답은 "서울특별시" 이긴 하나, 위 코드를 통해 돌리게 될 경우 "서울특별시시" 라는 결과를 얻게 됩니다.

 

indexOf 메소드는 재밌게도 단어가 완전히 매칭하지 않더라도 어느정도 단어가 맞으면 -1을 반환하는 문제가 있습니다.

"서울시".indexOf("서울")
=> -1

위와같은 원리로 봤을 때, 결국 첫번 째 if문 조건에 TRUE이다 보니, "서울" 에서 결국 "서울특별시" 로 치환이 되고, if문은 종료됩니다.


alias 관련해서 최종적으로 코드를 짜게되면 아래와 같이 짜여지게 됩니다.

address = msg.substring(5,200);

if (address.indexOf("서울시") != -1 && address.indexOf("서울특별시") != 0)
{
	address = address.replace("서울시", "서울특별시");
} else if (address.indexOf("서울") != -1 && address.indexOf("서울특별시") != 0) {
	address = address.replace("서울", "서울특별시");
} else if (address.indexOf("부산시") != -1 && address.indexOf("부산광역시") != 0) {
	address = address.replace("부산시", "부산광역시");
} else if (address.indexOf("부산") != -1 && address.indexOf("부산광역시") != 0) {
	address = address.replace("부산", "부산광역시");
} else if (address.indexOf("인천시") != -1 && address.indexOf("인천광역시") != 0) {
	address = address.replace("인천시", "인천광역시");
} else if (address.indexOf("인천") != -1 && address.indexOf("인천광역시") != 0) {
	address = address.replace("인천", "인천광역시");
} else if (address.indexOf("광주시") != -1 && address.indexOf("광주광역시") != 0) {
	address = address.replace("광주시", "광주광역시");
} else if (address.indexOf("광주") != -1 && address.indexOf("광주광역시") != 0) {
	address = address.replace("광주", "광주광역시");
} else if (address.indexOf("대전시") != -1 && address.indexOf("대전광역시") != 0) {
	address = address.replace("대전시", "대전광역시");
} else if (address.indexOf("대전") != -1 && address.indexOf("대전광역시") != 0) {
	address = address.replace("대전", "대전광역시");
} else if (address.indexOf("대구시") != -1 && address.indexOf("대구광역시") != 0) {
	address = address.replace("대구시", "대구광역시");
} else if (address.indexOf("대구") != -1 && address.indexOf("대구광역시") != 0) {
	address = address.replace("대구", "대구광역시");
} else if (address.indexOf("울산시") != -1 && address.indexOf("울산광역시") != 0) {
	address = address.replace("울산시", "울산광역시");
} else if (address.indexOf("울산") != -1 && address.indexOf("울산광역시") != 0) {
	address = address.replace("울산", "울산광역시");
} else if (address.indexOf("충남") != -1 && address.indexOf("충청남도") != 0) {
	address = address.replace("충남", "충청남도");
} else if (address.indexOf("충북") != -1 && address.indexOf("충청북도") != 0) {
	address = address.replace("충북", "충청북도");
} else if (address.indexOf("전남") != -1 && address.indexOf("전라남도") != 0) {
	address = address.replace("전남", "전라남도");
} else if (address.indexOf("전북") != -1 && address.indexOf("전라북도") != 0) {
	address = address.replace("전북", "전라북도");
} else if (address.indexOf("경남") != -1 && address.indexOf("경상남도") != 0) {
	address = address.replace("경남", "경상남도");
} else if (address.indexOf("경북") != -1 && address.indexOf("경상북도") != 0) {
	address = address.replace("경북", "경상북도");
}

 

위 코드를 카카오봇에 적용 후, 다시한번 카톡을 보내게 되면

"경북"이 이제 "경상북도" 로 잘 치환이 됩니다!

더불어 데이터 값도 잘 가져오는것을 확인할 수 있습니다!

 

 

4. 마무리

API 통신을 활용한 구현을 많이해본 덕분에 공공데이터 ↔ Rails 웹서버 ↔ 카카오봇 통신은 어렵지 않게 구축을 할 수 있었으나, "경북"↔"경상북도"와 같이 문자열 처리 때문에 살짝 좀 고생을 했었습니다.

하지만 그래도 오작동 하던 문제는 심플하고 빨리 찾아낼 수 있었고, 마스크를 위해 바깥에 나서는 분들에게 있어 조금이라도 덜 고생시킬 수 있다는 마음으로 개발할 수 있어서 뿌듯한 개발경험이 될 수 있었습니다 :D

 

빨리 이 코로나가 끝났으면 좋겠네요..ㅠ

다들 건강 조심하시고 해당 기능으로 조금이나마 많은 도움이 되었으면 좋겠습니다 :) 😘

 

마지막으로 봇이 응답하는 텍스트 배치 관련해서 피드백을 주신 속초감쟈 님,

주소 관련해서 단어 규칙을 파악하는데 도움을 주신 하루, 19학번 국교~!, 김태희, 20 인문🌸 님,

마스크 곰두리 캐릭터 사용에 허용을 해주신 해당 에브리타임 게시글 작성자 님께 감사합니다.

 

감사합니다!

댓글
댓글쓰기 폼
공지사항
Total
54,001
Today
29
Yesterday
104
링크
«   2020/11   »
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          
글 보관함