Project/[약올림] Final Project

[Milestone Week 3] 복약 정보 제공 및 관리 기능

HJChung 2021. 1. 22. 09:05

3주차 때는 2주차의 복용 일정 등록 및 관리 기능에 이어서 약올림의 또다른 핵심 서비스인 복약 정보 제공 및 관리 기능을 구현했다. 

이 기능은 구현 이전부터 '약 상세 정보를 제공해주는 곳이 있나, 있으면 어디인가?' ,  '이 데이터를 우리 DB에 저장 할 것이냐 아니면 매번 OpenAPI로 요청할 것이냐', '어떤 정보를 제공해 줄 것인가?',  'OpenAPI를 요청하는 곳을 Client에서 할 것인가 Server에서 할 것인가?' 등에 대한 팀원간 논의가 활발하게 이루어진 부분이기도 하다. 

 

이번에 구현했던 기능들은 기술적인 이슈나 원리를 아는 것 보다 '사례와 사용법'을 아는 것이 핵심이다. 그만큼 공식문서보다는 나보다 먼저 공공데이터, OpenAPI, xml정보의 json화를 고민했던 사람들이 기록으로 남겨둔 블로그 및 관련 기사를 더 많이 보았고, 도움이 많이 되었다. 

얼굴도 모르는 분들이 열심히 작성해주신 글들의 도움을 받으면서 내 글도 누군가에게 이런 존재가 되었으면 좋겠다는 마음으로 더 꼼꼼하고 열심히 블로그를 작성해야겠다고 생각했다. 누군가 그런 말을 한 적이 있다고 한다. '개발자들은 신기해 자신이 그렇게 고생고생해서 얻은 해답, 코드를 왜 블로그에 통으로 다 올리는거지? 그걸 왜 다 공개하는거지?'
그런데 나는 이런게 개발자 문화의 아름다움이라고 생각한다. 개인만 알고 그걸 꽁꽁 숨기면 거기서 지식의 흐름은 멈추게 된다. 
이미 길이 나온 것을 공유하지 않아서 누군가 그걸 알기 위해 또 써야하는 시간을, 정보를 자유롭게 접할 수 있게 공유함으로써 그 시간을 기존의 것을 더 발전시키거나 뒤이은 다른 문제를 해결하는데 쓰는게 더 좋지 않은가? 
이런 문화, 생각을 페어 프로그래밍과 팀원 분들 (+스택오버플로우)에게서 배웠고 프로젝트를 하면서 연습했으며 깃헙을 관리하고 블로그를 정리하면서 실천하고 있다. 

그리고 앞으로도 그런 문화에 푹 빠져있는 개발자로 성장하고 싶다. 

 

1.  '약 상세 정보를 제공해주는 곳이 있나, 있으면 어디인가?' 

약올림 서비스에서 (직접 등록한 약이 아닌)이미지 인식을 통해 등록한 약의 경우 공공데이터에서 제공하는 상세 정보를 받아볼 수 있도록 구현하고자 하였다. 이 부분을 담당해주시기로 한 HI님께서 해당 데이터를 어디서 얻을 수 있을지 질문을 주셔서 생각보다 빨리 찾아보게 되었다. 

(앞서 약 이미지 데이터를 얻기 위해 사용한  의약품안전나라에서 제공하는 공공데이터개방_낱알식별목록 csv 파일에는 약에 대한 복용법, 효능효법, 유효기간 등 상세 정보는 나와있지 않았기 때문이다. )

 

※ 아 그리고 이 프로젝트 기획을 제안드릴 때 부터 약 상세정보에 해당하는 공공데이터 제공이 반드시 있을것이라 생각했다. 왜냐하면 
<식약처, '의약품 정보 제공 프로그램 개발' 지원한다…효능효과 등 '의약품 정보 약 12만건' 제공>이라는 뉴스도 보았고, <약학정보원>에 올라온 질문과 답변에서 '식약처의 공개 API 서비스를 활용해 보아라의약품 정보는 누구에게나 공개되어 있다.' 등의 답변을 보았기 때문이다. 

 

그렇게 찾아보다가 <의약품안전나라>에서 <의약품 제품허가 상세정보> 중 <의약품 제품허가 상세정보조회>를 위한 open API를 제공한다는 것을 알아냈다.   

 

여기에서 우리가 사용할 것은 9페이지의 < (2) [의약품 제품 허가 상세정보조회] 오퍼레이션 명세 > 이다. 

활용신청을 하고, 

여기서 

이 요청주소를 엔드포인트로, 

로그인 후 <의약품 제품 허가 정보 서비스>에 들어가면 내 일반인증키(UTF-8)가 있다. 

이것과 

여기서 요청변수(Request Parameter)에 해당하는 것들을 queryParams에 넣어서 요청하면

출력결과(Response Element)에 해당하는 것들을 출력해준다. 

'필'이라고 되어있지만

아래 코드에서도 알 수 있듯이 주석처리 해줬을 때도 데이터가 잘 받아와졌다. 

2. '이 데이터를 우리 DB에 저장 할 것이냐 아니면 매번 OpenAPI로 요청할 것이냐'

Medicines라는 DB테이블에 직접 입력한 약에 대한 정보와 카메라 인식을 통한 약 정보를 모두 관리하고 있기 때문에 카메라 인식을 통한 약 정보를 저장도 가능하긴 했다. 그래서 '이 데이터를 우리 DB에 저장 할 것이냐 아니면 매번 OpenAPI로 요청할 것이냐' 라는 논의가 나왔다. 

 

이 부분은 HI님께서 OpenAPI 관리 측면에 대해서 잘 알아보시고 '알아보니 하루에 한 번 변경사항이 있는 약들의 업데이트가 진행되기 때문에 사용자에게 최신의 정보를 제공하기 위해선 매번 요청하는 것이 바람직 할 것 같다'는 의견을 주셨다.

해당 질문에 대해서 UX적인 측면을 고려하시고, 정확히 알아보신 후 논리로 의견을 내주셔서 깔끔하게 해결이 되었는데, HI님은 이런 부분에서 배울 점이 참 많은 분인 것 같다.

3.  'OpenAPI를 요청하는 곳을 Client에서 할 것인가 Server에서 할 것인가?' , '어떤 정보를 어떻게 제공해 줄 것인가?', 

상세 정보를 OpenAI로 요청할 때는 공공데이터에서 제공하는 endpoint로 약의 이름을 parameter로 요청하는 것이기 때문에 client에서도 충분히 요청을 보낼 수 있다. 그럼에도 Server에서 요청을 처리하기로 한 것은 모바일 앱이라는 특성상 Client의 코드를 최대한 가볍게 하려는 의도도 있고, 한 알약에 대한 정보를 Server에서 한번에 모아서 json형태로 만든 후 전달하면 Client에서는 그냥 랜더링만 시켜주도록 하는게 효율적이라고 생각했기 때문이다. 

 

어떤 정보를 어떤 형태로 응답해주는지는 제공되는 API문서를 꼼꼼히 읽어보는 것이 중요했다. 

IROS_05_의약품_제품_허가_서비스_v1.4 (1).doc
0.68MB

 

 

이 API문서에서 <1) [의약품 제품 허가 상세정보조회] 오퍼레이션 명세> 을 보면 <요청 메시지 명세>에 요청 parameter정보가 있고, <응답 메시지 명세>에 응답되는 것들이 매우 자세히 나와있다. 이 응답들 중에서 validity(약의 유효기간), capacity(용법/용량), effect(효과/효능)을 제공하고자 했다. 

 

이제 응답 xml을 json화 시키는 것이 중요한데, 이게 정말 힘들었다. xml형식이 트리 자료구조처럼 계층적으로 되어있었기 때문이다. 

HI님과 함께 고민하면서 처음에는 단순히 

medicines = xmlobj.findAll('item')
find_list = ["ITEM_NAME", "EE_DOC_DATA","UD_DOC_DATA", "VALID_TERM"]

for i in find_list:
    for result in xmlobj.find_all(i):
    	if i == "ITEM_NAME":
        	print("약품명: ", result.text)
        elif i == "EE_DOC_DATA":
           for data in result.find_all("PARAGRAPH"):
        elif i == "UD_DOC_DATA":
           for data in result.find_all("PARAGRAPH"):
              print("용법용량: ", data.text)
        elif i == "VALID_TERM":
            print("유효기간: ", result.text)

이렇게 태그 기준으로만 뽑아주면 될 것이라고 생각했다. 그런데 이러면 해당 태그 안의 태그 들의 내용(text)는 뽑아주지 못하였다. 

 

이 방법으로는 시작을 잘못 잡은 듯 하여 API문서에서 응답을 최대 어디까지 계층적으로 줄 수 있는지 파악 한 뒤, etree를 사용하여 그 패턴에 맞게 xml태그를 한꺼풀 한꺼풀 탐색하도록 해주었다. 그리고 가끔 text에 '&nbsp;' 이런 문자가 있는 등, 한 두개의 예외들은 모두 검사하여 따로 처리해주었다.  기능 구현이 아니라 알고리즘 완전탐색 문제를 푸는 것 같았다.😂

 

그렇게 완성되니 코드는 <더보기>를 클릭!

더보기
def get_open_api_info(name):
  my_api_Key = unquote(MY_API_KEY)
  url = OPEN_API_URI
  queryParams = '?' + urlencode({ quote_plus('ServiceKey') : my_api_Key, quote_plus('item_name') : name })

  request = Request(url + queryParams)
  request.get_method = lambda: 'GET'
  response_body = urlopen(request).read()

  tree = etree.fromstring(response_body)
  json_string = json.dumps(xmltodict.parse(response_body))

  load_json_string = json.loads(json_string)

  open_api_data = load_json_string["response"]["body"]["items"]["item"]

  group_data = dict()

  group_data["ITEM_NAME"] = open_api_data["ITEM_NAME"]#약이름
  group_data["VALID_TERM"] = open_api_data["VALID_TERM"]#유효기간
  group_data["EE_DOC_DATA"] = open_api_data["EE_DOC_DATA"]["DOC"]["SECTION"]["ARTICLE"]#효능효과
  group_data["UD_DOC_DATA"] = open_api_data["UD_DOC_DATA"]["DOC"]["SECTION"]["ARTICLE"]#용법용량

  results = {}
  field_lists = ["ITEM_NAME", "VALID_TERM", "EE_DOC_DATA", "UD_DOC_DATA"]

  for field in field_lists:
    if field == "EE_DOC_DATA":
        effect = []
        if type(group_data["EE_DOC_DATA"])==list:
            for i in range(len(group_data["EE_DOC_DATA"])):
                effect.append(group_data["EE_DOC_DATA"][i]['@title'])
                try:
                    if type(group_data["EE_DOC_DATA"][i]['PARAGRAPH'])==list:
                        for y in group_data["EE_DOC_DATA"][i]['PARAGRAPH']:
                            value_ = list(y.values())
                            if value_[0] == 'p' and value_[3]!='&nbsp;':
                                del value_[0:3]
                                effect.append(''.join(value_))
                    else:
                        effect.append(group_data["EE_DOC_DATA"][i]['PARAGRAPH']['#text'])
                except KeyError:
                    if type(group_data['EE_DOC_DATA'][i])==list:
                        for y in group_data["EE_DOC_DATA"][i]['PARAGRAPH']:
                            value_ = list(y.values())
                            if value_[0] == 'p' and value_[3]!='&nbsp;':
                                del value_[0:3]
                                effect.append(''.join(value_))     
        else:
            if type(group_data["EE_DOC_DATA"]['PARAGRAPH'])==list:
                for y in group_data['EE_DOC_DATA']['PARAGRAPH']:
                    value_ = list(y.values())
                    if value_[0] == 'p' and value_[3]!='&nbsp;':
                        del value_[0:3]
                        effect.append(''.join(value_)) 
            else:
                effect.append(group_data['EE_DOC_DATA']['PARAGRAPH']['#text'])
        results['effect'] = effect

    elif field == "UD_DOC_DATA":
        capacity = []
        if type(group_data["UD_DOC_DATA"])==list:
            for i in range(len(group_data["UD_DOC_DATA"])):
                capacity.append(group_data["UD_DOC_DATA"][i]['@title'])
                try:
                    if type(group_data["UD_DOC_DATA"][i]['PARAGRAPH'])==list:
                        for y in group_data["UD_DOC_DATA"][i]['PARAGRAPH']:
                            value_ = list(y.values())
                            if value_[0] == 'p' and value_[3]!='&nbsp;':
                                del value_[0:3]
                                capacity.append(''.join(value_))
                    else:
                        capacity.append(group_data["UD_DOC_DATA"][i]['PARAGRAPH']['#text'])
                except KeyError:
                    if type(group_data['UD_DOC_DATA'][i])==list:
                        for y in group_data["UD_DOC_DATA"][i]['PARAGRAPH']:
                            value_ = list(y.values())
                            if value_[0] == 'p' and value_[3]!='&nbsp;':
                                del value_[0:3]
                                capacity.append(''.join(value_))     
        else:
            if type(group_data["UD_DOC_DATA"]['PARAGRAPH'])==list:
                for y in group_data['UD_DOC_DATA']['PARAGRAPH']:
                    value_ = list(y.values())
                    if value_[0] == 'p' and value_[3]!='&nbsp;':
                        del value_[0:3]
                        capacity.append(''.join(value_)) 
            else:
                capacity.append(group_data['UD_DOC_DATA']['PARAGRAPH']['#text'])
        results['capacity'] = capacity

    elif field == "ITEM_NAME":
        results['name'] = group_data["ITEM_NAME"]

    else: 
        results['validity'] = group_data["VALID_TERM"]

  return results

 

여기까지 약통 페이지, 약 상세 페이지 기능 구현이 마무리 되었다. 

 

이번에 구현했던 기능들은 기술적인 이슈나 원리를 아는 것 보다 '사례와 사용법'을 아는 것이 핵심이다. 그만큼 공식문서보다는 나보다 먼저 공공데이터, OpenAPI, xml정보의 json화를 고민했던 사람들이 기록으로 남겨둔 블로그 및 관련 기사를 더 많이 보았고, 도움이 많이 되었다. 

 

얼굴도 모르는 분들이 열심히 작성해주신 글들의 도움을 받으면서 내 글도 누군가에게 이런 존재가 되었으면 좋겠다는 마음으로 더 꼼꼼하고 열심히 블로그를 작성해야겠다고 생각했다. 누군가 그런 말을 한 적이 있다고 한다. '개발자들은 신기해 자신이 그렇게 고생고생해서 얻은 해답, 코드를 왜 블로그에 통으로 다 올리는거지? 그걸 왜 다 공개하는거지?'

그런데 나는 이런게 개발자 문화의 아름다움이라고 생각한다. 개인만 알고 그걸 꽁꽁 숨기면 거기서 지식의 흐름은 멈추게 된다. 

이미 길이 나온 것을 공유하지 않아서 누군가 그걸 알기 위해 또 써야하는 시간을, 정보를 자유롭게 접할 수 있게 공유함으로써 그 시간을 기존의 것을 더 발전시키거나 뒤이은 다른 문제를 해결하는데 쓰는게 더 좋지 않은가? 

이런 문화, 생각을 페어 프로그래밍과 팀원 분들 (+스택오버플로우)에게서 배웠고 프로젝트를 하면서 연습했으며 깃헙을 관리하고 블로그를 정리하면서 실천하고 있다. 

 

그리고 앞으로도 그런 문화에 푹 빠져있는 개발자로 성장하고 싶다.