온라인 구매 후 매장 수령: Bonjour Meal - 2부 - 장바구니 빌드

1. 소개

53003251caaf2be5.png 8826bd8cb0c0f1c7.png

최종 업데이트: 2020년 10월 30일

Business Messages에서 장바구니 빌드!

온라인 구매 후 매장 수령 사용자 경험을 빌드하기 위한 시리즈 중에서 두 번째 Codelab입니다. 많은 전자상거래 여정에서 장바구니는 성공적으로 사용자를 유료 고객으로 전환하기 위한 핵심입니다. 또한 장바구니는 고객을 더 잘 이해하고 고객이 관심을 가질 수 있는 다른 항목을 제안하기 위한 수단으로도 사용됩니다. 이 Codelab에서는 장바구니 경험을 빌드하고 Google App Engine에 애플리케이션을 배포하는 과정을 집중적으로 다룹니다.

좋은 장바구니란 어떤 것일까요?

장바구니는 성공적인 온라인 쇼핑 경험을 위한 핵심입니다. 잘 알려진 대로 Business Messages는 잠재 고객과의 제품 Q&A를 촉진할 뿐만 아니라 대화 내에서 결제 완료까지 전체 쇼핑 경험을 촉진하는 데 유용한 도구입니다.

9d17537b980d0e62.png

장바구니뿐만 아니라 우수한 쇼핑 경험은 고객의 카테고리별 항목 검색을 지원하고 구매자가 관심을 가질 만한 기타 항목 추천을 가능하게 해줍니다. 사용자는 장바구니에 항목을 추가한 후 전체 장바구니 내용을 확인하고 결제를 진행하기 전 항목을 추가하거나 삭제할 수 있습니다.

빌드할 항목

이 Codelab 시리즈 섹션에서는 가상의 회사인 Bonjour Meal을 위해 1부에서 빌드한 디지털 에이전트를 가지고 사용자가 항목 카탈로그를 둘러보고 장바구니에 항목을 추가할 수 있도록 확장합니다.

이 Codelab에서 앱은 다음 기능을 수행합니다.

  • Business Messages 내에서 질문 카탈로그 표시
  • 사용자가 관심을 가질 만한 항목 추천
  • 장바구니 내용 검토와 총 금액 요약 만들기

ab2fb6a4ed33a129.png

과정 내용

  • Google Cloud Platform에서 App Engine에 웹 애플리케이션을 배포하는 방법
  • 영구 스토리지 메커니즘을 사용하여 장바구니 상태를 저장하는 방법

이 Codelab은 이 Codelab 시리즈 1부에서 만든 디지털 에이전트를 확장하는 데 집중합니다.

필요한 항목

2. 설정

이 Codelab에서는 첫 번째 에이전트를 만들고 Codelab 1부를 완료했다고 가정합니다. 따라서 Business Messages 및 Business Communications API 사용 설정에 관한 기본 사항, 서비스 계정 키 만들기, 애플리케이션 배포, Business Communications Console에서 웹훅 설정 등에 관한 내용에 대해서는 다시 설명하지 않습니다. 그렇더라도 샘플 애플리케이션을 클론하여 애플리케이션이 빌드 중인 애플리케이션과 일치하는지 확인하고 장바구니와 관련된 데이터를 유지할 수 있도록 Google Cloud Platform에서 Datastore용 API를 사용 설정합니다.

GitHub에서 애플리케이션 클론

터미널에서 다음 명령어를 사용하여 Django Echo Bot 샘플을 프로젝트 작업 디렉터리로 클론합니다.

$ git clone https://github.com/google-business-communications/bm-bonjour-meal-django-starter-code

서비스 계정에 대해 생성된 JSON 사용자 인증 정보를 샘플 리소스 폴더에 복사하고 사용자 인증 정보 이름을 'bm-agent-service-account-credentials.json'으로 바꿉니다.

bm-bonjour-meal-django-starter-code/bonjourmeal-codelab/step-2/resources/bm-agent-service-account-credentials.json

Google Datastore API 사용 설정

이 Codelab의 1부에서는 Business Messages API, Business Communications API, Cloud Build API를 사용 설정했습니다.

이 Codelab에서는 Google Datastore가 사용되므로 이 API도 사용 설정해야 합니다.

  1. Google Cloud Console에서 Google Datastore API를 엽니다.
  2. 사용하려는 GCP 프로젝트가 올바른지 확인합니다.
  3. 사용 설정을 클릭합니다.

샘플 애플리케이션 배포

터미널에서 샘플의 step-2 디렉터리로 이동합니다.

터미널에서 다음 명령어를 실행하여 샘플을 배포합니다.

$ gcloud config set project PROJECT_ID*
$ gcloud app deploy
  • PROJECT_ID는 API 등록을 위해 사용되는 프로젝트의 프로젝트 ID입니다.

마지막 명령어 결과에서 배포된 애플리케이션의 URL을 기록해 둡니다.

Deployed service [default] to [https://PROJECT_ID.appspot.com]

방금 배포한 코드에는 Business Messages 메시지 수신을 위해 웹훅이 있는 웹 애플리케이션이 포함되어 있습니다. 여기에는 Codelab 1부에서 수행한 모든 작업이 포함되어 있습니다. 아직 구성하지 않으면 웹훅을 구성합니다.

이 애플리케이션은 사용자의 Bonjour Meal 영업시간 문의와 같은 간단한 질문에 응답합니다. Business Communications Console의 에이전트 정보에서 검색할 수 있는 테스트 URL을 통해 모바일 기기에서 이를 테스트해야 합니다. 테스트 URL로 모바일 기기에서 Business Messages 경험이 시작되고 여기에서 에이전트와 상호작용을 시작할 수 있습니다.

3. 제품 카탈로그

인벤토리 시스템

대부분의 경우 내부 API를 통해 브랜드 인벤토리와 직접 통합할 수 있습니다. 다른 경우에는 웹페이지를 만들거나 자체 인벤토리 추적 시스템을 빌드할 수 있습니다. 인벤토리 시스템 빌드는 여기에서 핵심 사항이 아닙니다. 여기에서는 에이전트에 필요한 이미지와 제품 정보가 포함된 간단한 정적 파일을 사용합니다. 이 섹션에서는 이 정적 파일에서 정보를 가져오고 이 정보를 대화에 표시하고 사용자가 장바구니에 추가할 수 있는 항목을 둘러볼 수 있도록 설정합니다.

정적 인벤토리 파일은 다음과 같습니다.

bonjourmeal-codelab/step-2/resources/inventory.json

{

    "food": [
        {
            "id":0,
            "name": "Ham and cheese sandwich",
            "price": "6.99",
            "image_url": "https://storage.googleapis.com/bonjour-rail.appspot.com/ham-and-cheese.png",
            "remaining": 8
        },
        {
            "id":1,
            "name": "Chicken veggie wrap",
            "price": "9.99",
            "image_url": "https://storage.googleapis.com/bonjour-rail.appspot.com/chicken-veggie-wrap.png",
            "remaining": 2
        },
        {
            "id":2,
            "name": "Assorted cheese plate",
            "price": "7.99",
            "image_url": "https://storage.googleapis.com/bonjour-rail.appspot.com/assorted-cheese-plate.png",
            "remaining": 6
        },
        {
            "id":3,
            "name": "Apple walnut salad",
            "price": "12.99",
            "image_url": "https://storage.googleapis.com/bonjour-rail.appspot.com/apple-walnut-salad.png",
            "remaining": 1
        }
    ]
}

이제 이 파일을 읽기 위해 Python 애플리케이션을 준비해야 합니다.

인벤토리에서 읽기

인벤토리는 ./resources 디렉터리에 있는 'inventory.json'이라는 정적 파일입니다. JSON 파일의 콘텐츠를 읽기 위해 views.py에 약간의 Python 로직을 추가한 후 이를 대화에 표시해야 합니다. JSON 파일에서 데이터를 읽고 사용 가능한 제품 목록을 반환하는 함수를 만들어 보겠습니다.

이 함수 정의는 views.py 내 어디에든 배치할 수 있습니다.

bonjourmeal-codelab/step-2/bopis/views.py

...
def get_inventory_data():
        f = open(INVENTORY_FILE)
        inventory = json.load(f)
        return inventory
...

이렇게 하면 인벤토리에서 데이터를 읽는 데 필요한 준비가 완료됩니다. 이제 이 제품 정보를 대화에 표시할 방법이 필요합니다.

제품 카탈로그 표시

이 Codelab에서는 간단한 진행을 위해 단일 리치 카드 캐러셀을 통해 모든 인벤토리 항목을 Business Messages 대화에 표시하는 일반 제품 카탈로그가 사용됩니다.

제품 카탈로그를 보기 위해 여기에서는 '메뉴 표시' 텍스트와 postbackData 'show-product-catalog'가 포함된 제안 응답을 만듭니다. 사용자가 제안 응답을 탭하면 웹 애플리케이션이 포스트백 데이터를 수신하고 리치 카드 캐러셀이 전송됩니다. 이제 views.py 위에서 이 제안 응답에 대한 새 상수를 추가하겠습니다.

bonjourmeal-codelab/step-2/bopis/views.py

...
CMD_SHOW_PRODUCT_CATALOG = 'show-product-catalog'
...

여기에서는 메시지를 파싱하고 이를 제품 카탈로그가 포함된 리치 카드 캐러셀을 전송하는 새 함수로 라우팅합니다. 먼저 제안 응답이 눌렸을 때 'send_product_catalog' 함수를 호출하도록 route_message 함수를 확장하고 메시지 라우팅 함수를 정의합니다.

다음 스니펫에서 route_message 함수의 if 문에 조건을 추가하고 normalized_message가 앞에서 정의한 상수인 CMD_SHOW_PRODUCT_CATALOG와 동일한지 확인합니다.

bonjourmeal-codelab/step-2/bopis/views.py

...
def route_message(message, conversation_id):
    '''
    Routes the message received from the user to create a response.

    Args:
        message (str): The message text received from the user.
        conversation_id (str): The unique id for this user and agent.
    '''
    normalized_message = message.lower()

    if normalized_message == CMD_RICH_CARD:
        send_rich_card(conversation_id)
    elif normalized_message == CMD_CAROUSEL_CARD:
        send_carousel(conversation_id)
    elif normalized_message == CMD_SUGGESTIONS:
        send_message_with_suggestions(conversation_id)
    elif normalized_message == CMD_BUSINESS_HOURS_INQUIRY:
        send_message_with_business_hours(conversation_id)
    elif normalized_message == CMD_ONLINE_SHOPPING_INQUIRY:
        send_online_shopping_info_message(conversation_id)
    elif normalized_message == CMD_SHOW_PRODUCT_CATALOG:
        send_product_catalog(conversation_id)
    else:
        echo_message(message, conversation_id)
...

이제 이 흐름을 완료하고 send_product_catalog를 정의합니다. send_product_catalog는 이전에 읽은 인벤토리 파일에서 리치 카드의 캐러셀을 생성하는 get_menu_carousel,을 호출합니다.

함수 정의는 views.py 내 어디에든 배치될 수 있습니다. 다음 스니펫에는 파일 상단에 추가되어야 하는 2개의 새 상수가 사용됩니다.

bonjourmeal-codelab/step-2/bopis/views.py

...

CMD_ADD_ITEM = 'add-item'
CMD_SHOW_CART = 'show-cart'

...

def get_menu_carousel():
    """Creates a sample carousel rich card.

    Returns:
       A :obj: A BusinessMessagesCarouselCard object with three cards.
    """

    inventory = get_inventory_data()

    card_content = []

    for item in inventory['food']:
        card_content.append(BusinessMessagesCardContent(
            title=item['name'],
            description=item['price'],
            suggestions=[
                BusinessMessagesSuggestion(
                    reply=BusinessMessagesSuggestedReply(
                        text='Add item',
                        postbackData='{'+f'"action":"{CMD_ADD_ITEM}","item_name":"{item["id"]}"'+'}'))

                ],
            media=BusinessMessagesMedia(
                height=BusinessMessagesMedia.HeightValueValuesEnum.MEDIUM,
                contentInfo=BusinessMessagesContentInfo(
                    fileUrl=item['image_url'],
                    forceRefresh=False))))

    return BusinessMessagesCarouselCard(
        cardContents=card_content,
        cardWidth=BusinessMessagesCarouselCard.CardWidthValueValuesEnum.MEDIUM)

def send_product_catalog(conversation_id):
    """Sends the product catalog to the conversation_id.

    Args:
        conversation_id (str): The unique id for this user and agent.
    """
    rich_card = BusinessMessagesRichCard(carouselCard=get_menu_carousel())

    fallback_text = ''

    # Construct a fallback text for devices that do not support carousels
    for card_content in rich_card.carouselCard.cardContents:
        fallback_text += (card_content.title + '\n\n' + card_content.description
                          + '\n\n' + card_content.media.contentInfo.fileUrl
                          + '\n---------------------------------------------\n\n')

    message_obj = BusinessMessagesMessage(
        messageId=str(uuid.uuid4().int),
        representative=BOT_REPRESENTATIVE,
        richCard=rich_card,
        fallback=fallback_text,
        suggestions=[
        BusinessMessagesSuggestion(
            reply=BusinessMessagesSuggestedReply(
                text='See my cart',
                postbackData=CMD_SHOW_CART)
            ),
        BusinessMessagesSuggestion(
            reply=BusinessMessagesSuggestedReply(
                text='See the menu',
                postbackData=CMD_SHOW_PRODUCT_CATALOG)
            ),
        ]
        )

    send_message(message_obj, conversation_id)
...

캐러셀 항목 만들기를 검사하면 BusinessMessagesSuggestion 클래스 인스턴스도 생성됩니다. 각 제안은 캐러셀에서 사용자가 선택한 제품을 나타냅니다. 사용자가 제안 응답을 탭하면 Business Messages가 항목 및 사용자가 수행하려는 작업(장바구니 항목 추가 또는 삭제)을 기술하는 JSON이 포함된 postbackData를 웹훅으로 전송합니다. 다음 섹션에서는 장바구니에 항목을 실제로 추가할 수 있도록 이와 비슷한 메시지를 파싱합니다.

이제 변경 작업이 완료되었으므로 웹 애플리케이션을 Google App Engine에 배포하고 환경을 시험해보겠습니다.

$ gcloud app deploy

모바일 기기에 대화 표시 경로를 로드하고 'show-product-catalog' 메시지를 전송하면 다음과 같은 제품 캐러셀이 표시됩니다.

4639da46bcc5230c.png

Add item을 탭하면 에이전트에서 제안 응답의 postback 데이터가 에코되는 것 말고는 실제 아무것도 수행되지 않습니다. 다음 섹션에서는 제품 카탈로그를 사용해서 항목을 추가할 장바구니를 빌드해보겠습니다.

방금 빌드한 제품 카탈로그는 여러 방법으로 확장할 수 있습니다. 다른 음료 메뉴 옵션이나 채식주의자 옵션을 제공할 수도 있습니다. 캐러셀 또는 제안 칩은 고객이 원하는 제품 집합에 도달하기 위해 메뉴 옵션을 드릴다운할 수 있게 해주는 훌륭한 방법입니다. 이 Codelab에 대한 확장으로, 사용자가 메뉴에서 음식과 별도로 음료를 확인할 수 있도록 아니면 심지어 채식주의자 옵션을 지정할 수 있도록 제품 카탈로그 시스템을 확장해보겠습니다.

4. 장바구니

이 Codelab 섹션에서는 사용 가능한 제품을 둘러볼 수 있도록 이전 섹션에서 만든 장바구니 기능을 강화합니다.

무엇보다도 사용자가 장바구니에 항목을 추가하고, 장바구니에서 항목 제거하고, 장바구니에 포함된 각 항목의 개수를 추적하고, 장바구니 항목을 검토할 수 있게 하는 것이 장바구니 경험의 핵심입니다.

장바구니 상태를 추적하기 위해서는 웹 애플리케이션에서 데이터를 보존할 수 있어야 합니다. 간단한 실험과 배포를 위해 여기에서는 Google Cloud Platform에서 Google Datastore를 사용하여 데이터를 보존합니다. 대화 ID가 사용자와 비즈니스 간에 상수로 보존되므로 이를 사용해서 사용자를 장바구니 항목과 연결할 수 있습니다.

먼저 Google Datastore에 연결하고 대화 ID가 표시될 때 이를 보존하는 것으로 시작합니다.

Datastore에 연결

사용자가 항목을 추가하거나 삭제할 때와 같이 장바구니에서 상호작용이 실행될 때마다 Google Datastore에 연결합니다. 이 클라이언트 라이브러리를 사용한 Google Datastore 상호작용 방법은 공식 문서를 참조하세요.

다음 스니펫은 장바구니 업데이트 함수를 정의합니다. 이 함수에는 conversation_idmessage가 입력으로 사용됩니다. message에는 제품 카탈로그를 표시하는 캐러셀에 이미 빌드되어 있는, 사용자가 수행할 작업이 기술된 JSON이 포함되어 있습니다. 이 함수는 Google Datastore 클라이언트를 만들고 키가 대화 ID인 ShoppingCart 항목을 즉시 가져옵니다.

다음 함수를 view.py 파일에 복사합니다. 이후 섹션에서도 이 함수에 대한 확장 작업이 계속 수행됩니다.

bonjourmeal-codelab/step-2/bopis/views.py

from google.oauth2 import service_account
from google.cloud import datastore

def update_shopping_cart(conversation_id, message):
        credentials = service_account.Credentials.from_service_account_file(
        SERVICE_ACCOUNT_LOCATION)

        client = datastore.Client(credentials=credentials)
        key = client.key('ShoppingCart', conversation_id)
        entity = datastore.Entity(key=key)
        result = client.get(key)
        
        # TODO: Add logic to add and remove items from cart
        
        entity.update(result)
        client.put(entity)

이제 장바구니에 항목을 추가하도록 이 함수를 확장합니다.

장바구니에 항목 추가

사용자가 제품 캐러셀에서 제품 추가 제안 작업을 탭하면 postbackData에 사용자가 수행할 작업이 기술된 JSON이 포함됩니다. JSON 딕셔너리에 'action'과 'item_name'의 두 가지 키가 포함되고 이 JSON 딕셔너리가 웹훅으로 전송됩니다. 'item_name' 필드는 inventory.json의 항목과 연결된 고유 식별자입니다.

장바구니 명령어와 메시지에서 파싱된 장바구니 항목이 준비되었으면 이제 항목을 추가하는 조건문을 작성할 수 있습니다. 여기에서 생각할 수 있는 일부 극단적인 사례로는 Datastore에 대화 ID가 확인되지 않은 경우 또는 장바구니가 이 항목을 처음으로 수신하는 경우를 들 수 있습니다. 다음은 위에 정의된 update_shopping_cart 기능의 확장을 보여줍니다. 이렇게 변경하면 Google Datastore에서 보존되는 장바구니에 항목이 추가됩니다.

다음 스니펫은 views.py에 추가한 이전 함수를 확장한 것입니다. 원하는 대로 다른 부분을 추가하거나 스니펫을 복사해서 update_shopping_cart 함수의 기존 버전을 바꿀 수 있습니다.

bonjourmeal-codelab/step-2bopis/views.py

def update_shopping_cart(conversation_id, message):
    credentials = service_account.Credentials.from_service_account_file(
      SERVICE_ACCOUNT_LOCATION)
    inventory = get_inventory_data()

    cart_request = json.loads(message)
    cart_cmd = cart_request["action"]
    cart_item = cart_request["item_name"]

    item_name = inventory['food'][int(cart_item)]['name']

    client = datastore.Client(credentials=credentials)
    key = client.key('ShoppingCart', conversation_id)
    entity = datastore.Entity(key=key)
    result = client.get(key)

    if result is None:
        if cart_cmd == CMD_ADD_ITEM:
            entity.update({
                item_name: 1
            })

    else:
        if cart_cmd == CMD_ADD_ITEM:
            if result.get(item_name) is None:
                result[item_name] = 1
            else:
                result[item_name] = result[item_name] + 1

        entity.update(result)
    client.put(entity)

이 함수는 나중에 CMD_DEL_ITEM에 정의된 ‘del-item' 문자열이 cart_cmd에 포함되는 시나리오를 지원할 수 있도록 확장됩니다

모두 연결하기

카트에 항목 추가 메시지가 수신되면 해당 update_shopping_cart 함수가 호출되도록 route_message 함수에 배관을 추가하는 것을 잊지 마세요. 또한 이 Codelab 전체에 사용되는 규칙을 사용해서 항목 추가를 위한 상수를 정의해야 합니다.

bonjourmeal-codelab/step-2bopis/views.py

...

CMD_DEL_ITEM = 'del-item'

...

def route_message(message, conversation_id):
    '''
    Routes the message received from the user to create a response.

    Args:
        message (str): The message text received from the user.
        conversation_id (str): The unique id for this user and agent.
    '''
    normalized_message = message.lower()

    if normalized_message == CMD_RICH_CARD:
        send_rich_card(conversation_id)
    elif normalized_message == CMD_CAROUSEL_CARD:
        send_carousel(conversation_id)
    elif normalized_message == CMD_SUGGESTIONS:
        send_message_with_suggestions(conversation_id)
    elif normalized_message == CMD_BUSINESS_HOURS_INQUIRY:
        send_message_with_business_hours(conversation_id)
    elif normalized_message == CMD_ONLINE_SHOPPING_INQUIRY:
        send_online_shopping_info_message(conversation_id)
    elif normalized_message == CMD_SHOW_PRODUCT_CATEGORY:
        send_product_catalog(conversation_id)
    elif CMD_ADD_ITEM in normalized_message or CMD_DEL_ITEM in normalized_message:
       update_shopping_cart(conversation_id, message)
    else:
        echo_message(message, conversation_id)

...

이제 장바구니에 항목을 추가할 수 있습니다. Google App Engine에 변경사항을 배포하면 GCP Console에 있는 Google Datastore 대시보드에 장바구니 변경사항이 반영되어 표시됩니다. 아래의 Google Datastore Console 스크린샷을 참조하세요. 여기에는 대화 ID와 장바구니에 있는 인벤토리 항목과의 관계 및 이러한 항목의 수량에 따라 이름이 지정된 단일 항목이 표시되어 있습니다.

619dc18a8136ea69.png

다음 섹션에서는 장바구니에 있는 항목을 나열하는 방법을 만듭니다. 장바구니 검토 메커니즘은 장바구니에 있는 모든 항목, 해당 항목의 수량, 장바구니에서 항목을 삭제하는 옵션을 표시해야 합니다.

장바구니 항목 검토

장바구니 상태를 파악하고 삭제할 수 있는 항목을 확인하기 위한 유일한 방법은 장바구니 항목을 나열해보는 것입니다.

먼저 'Remove one' 또는 'Add one'에 연결된 제안 응답과 함께 리치 카드 캐러셀이 포함된 다른 메시지와 함께 'Here's your shopping cart:'와 같은 친숙한 메시지를 전송합니다. 리치 카드 캐러셀은 장바구니에 저장된 항목 수량을 조건에 따라 나열합니다.

실제로 함수를 작성하기 전에 주의해야 할 점이 몇 가지 있습니다. 장바구니에 항목 유형이 하나만 있으면 이를 캐러셀로 렌더링할 수 없습니다. 리치 카드 캐러셀에는 카드가 최소 2개 이상 있어야 합니다. 다른 한편으로, 장바구니에 항목이 없으면 장바구니가 비어 있음을 알려주는 간단한 메시지를 표시해야 합니다.

이를 고려해서 send_shopping_cart라는 함수를 정의해보겠습니다. 이 함수는 Google Datastore에 연결해서 대화 ID를 기반으로 ShoppingCart 항목을 요청합니다. 이후에는 get_inventory_data 함수를 호출하고 리치 카드 캐러셀을 사용해서 장바구니 상태를 보고합니다. 또한 이름별로 제품 ID를 가져와야 하고, Google Datastore를 조회하는 함수를 선언해서 해당 값을 결정할 수 있습니다. 캐러셀이 생성되면 제품 ID로 항목을 삭제하거나 추가할 수 있도록 제안 응답을 연결할 수 있습니다. 아래 스니펫은 이러한 작업을 모두 수행합니다. 코드를 views.py 내 어디에든 복사합니다.

bonjourmeal-codelab/step-2/bopis/views.py

...
def get_id_by_product_name(product_name):
  inventory = get_inventory_data()
  for item in inventory['food']:
    if item['name'] == product_name:
      return int(item['id'])
  return False


def send_shopping_cart(conversation_id):
  credentials = service_account.Credentials.from_service_account_file(
      SERVICE_ACCOUNT_LOCATION)

  # Retrieve the inventory data
  inventory = get_inventory_data()

  # Pull the data from Google Datastore
  client = datastore.Client(credentials=credentials)
  key = client.key('ShoppingCart', conversation_id)
  result = client.get(key)

  shopping_cart_suggestions = [
      BusinessMessagesSuggestion(
          reply=BusinessMessagesSuggestedReply(
              text='See total price', postbackData='show-cart-price')),
      BusinessMessagesSuggestion(
          reply=BusinessMessagesSuggestedReply(
              text='Empty the cart', postbackData='empty-cart')),
      BusinessMessagesSuggestion(
          reply=BusinessMessagesSuggestedReply(
              text='See the menu', postbackData=CMD_SHOW_PRODUCT_CATALOG)),
  ]

  if result is None or len(result.items()) == 0:
    message_obj = BusinessMessagesMessage(
        messageId=str(uuid.uuid4().int),
        representative=BOT_REPRESENTATIVE,
        text='There are no items in your shopping cart.',
        suggestions=shopping_cart_suggestions)

    send_message(message_obj, conversation_id)
  elif len(result.items()) == 1:

    for product_name, quantity in result.items():
      product_id = get_id_by_product_name(product_name)

      fallback_text = ('You have one type of item in the shopping cart')

      rich_card = BusinessMessagesRichCard(
          standaloneCard=BusinessMessagesStandaloneCard(
              cardContent=BusinessMessagesCardContent(
                  title=product_name,
                  description=f'{quantity} in cart.',
                  suggestions=[
                      BusinessMessagesSuggestion(
                          reply=BusinessMessagesSuggestedReply(
                              text='Remove one',
                              postbackData='{'+f'"action":"{CMD_DEL_ITEM}","item_name":"{product_id}"'+'}'))
                  ],
                  media=BusinessMessagesMedia(
                      height=BusinessMessagesMedia.HeightValueValuesEnum.MEDIUM,
                      contentInfo=BusinessMessagesContentInfo(
                          fileUrl=inventory['food'][product_id]
                          ['image_url'],
                          forceRefresh=False)))))

      message_obj = BusinessMessagesMessage(
          messageId=str(uuid.uuid4().int),
          representative=BOT_REPRESENTATIVE,
          richCard=rich_card,
          suggestions=shopping_cart_suggestions,
          fallback=fallback_text)

      send_message(message_obj, conversation_id)
  else:
    cart_carousel_items = []

    # Iterate through the cart and generate a carousel of items
    for product_name, quantity in result.items():
      product_id = get_id_by_product_name(product_name)

      cart_carousel_items.append(
          BusinessMessagesCardContent(
              title=product_name,
              description=f'{quantity} in cart.',
              suggestions=[
                  BusinessMessagesSuggestion(
                      reply=BusinessMessagesSuggestedReply(
                          text='Remove one',
                          postbackData='{'+f'"action":"{CMD_DEL_ITEM}","item_name":"{product_id}"'+'}'))
              ],
              media=BusinessMessagesMedia(
                  height=BusinessMessagesMedia.HeightValueValuesEnum.MEDIUM,
                  contentInfo=BusinessMessagesContentInfo(
                      fileUrl=inventory['food'][product_id]
                      ['image_url'],
                      forceRefresh=False))))

    rich_card = BusinessMessagesRichCard(
        carouselCard=BusinessMessagesCarouselCard(
            cardContents=cart_carousel_items,
            cardWidth=BusinessMessagesCarouselCard.CardWidthValueValuesEnum
            .MEDIUM))

    fallback_text = ''

    # Construct a fallback text for devices that do not support carousels
    for card_content in rich_card.carouselCard.cardContents:
      fallback_text += (
          card_content.title + '\n\n' + card_content.description + '\n\n' +
          card_content.media.contentInfo.fileUrl +
          '\n---------------------------------------------\n\n')

    message_obj = BusinessMessagesMessage(
        messageId=str(uuid.uuid4().int),
        representative=BOT_REPRESENTATIVE,
        richCard=rich_card,
        suggestions=shopping_cart_suggestions,
        fallback=fallback_text,
    )

    send_message(message_obj, conversation_id)

...

views.py 상단에서 CMD_SHOW_CART를 이미 정의했는지 확인하고 사용자가 ‘show-cart' 포함 메시지를 전송하면 send_shopping_cart를 호출합니다.

bonjourmeal-codelab/step-2/bopis/views.py

...
def route_message(message, conversation_id):
    '''
    Routes the message received from the user to create a response.

    Args:
        message (str): The message text received from the user.
        conversation_id (str): The unique id for this user and agent.
    '''
    normalized_message = message.lower()

    if normalized_message == CMD_RICH_CARD:
        send_rich_card(conversation_id)
    elif normalized_message == CMD_CAROUSEL_CARD:
        send_carousel(conversation_id)
    elif normalized_message == CMD_SUGGESTIONS:
        send_message_with_suggestions(conversation_id)
    elif normalized_message == CMD_BUSINESS_HOURS_INQUIRY:
        send_message_with_business_hours(conversation_id)
    elif normalized_message == CMD_ONLINE_SHOPPING_INQUIRY:
        send_online_shopping_info_message(conversation_id)
    elif normalized_message == CMD_SHOW_PRODUCT_CATEGORY:
        send_product_catalog(conversation_id)
    elif CMD_ADD_ITEM in normalized_message or CMD_DEL_ITEM in normalized_message:
        update_shopping_cart(conversation_id, message)
    elif normalized_message == CMD_SHOW_CART:
        send_shopping_cart(conversation_id)
    else:
        echo_message(message, conversation_id)
...

34801776a97056ac.png

send_shopping_cart 함수에 도입된 로직을 기준으로 ‘show-cart'를 입력하면 장바구니에 항목이 없음을 나타내는 메시지, 장바구니에 있는 하나의 항목을 보여주는 리치 카드, 여러 항목을 보여주는 카드 캐러셀을 가져옵니다. 또한 '총 금액 보기', '장바구니 비우기', '메뉴 보기'의 세 가지 제안 응답이 있습니다.

아래 스크린샷에 표시된 것처럼 위 코드 변경사항을 배포해서 추가한 항목이 장바구니에서 추적되는지 그리고 대화 표시로부터 장바구니를 검토할 수 있는지 테스트합니다. 변경사항을 추가하는 step-2 디렉터리에서 이 명령어를 실행하여 변경사항을 배포할 수 있습니다.

$ gcloud app deploy

다음 섹션에서는 장바구니에서 항목을 삭제하는 기능을 빌드한 후 '총 금액 보기' 기능을 빌드합니다. get_cart_price 함수는 장바구니의 총 금액을 생성하기 위해 inventory.json 파일을 사용해서 Datastore의 데이터를 교차 참조한다는 점에서 '장바구니 보기' 기능과 비슷하게 작동합니다. 이 함수는 결제와 통합되는 다음 Codelab 부분에서 편리하게 사용됩니다.

장바구니에서 항목 삭제

마지막으로 장바구니 삭제 기능을 포함하기 전 장바구니 동작을 완료할 수 있습니다. 기존 update_shopping_cart 함수를 다음 스니펫으로 바꿉니다.

bonjourmeal-codelab/step-2/ bopis/views.py

def update_shopping_cart(conversation_id, message):
    credentials = service_account.Credentials.from_service_account_file(
      SERVICE_ACCOUNT_LOCATION)
    inventory = get_inventory_data()

    cart_request = json.loads(message)
    cart_cmd = cart_request["action"]
    cart_item = cart_request["item_name"]

    item_name = inventory['food'][int(cart_item)]['name']


    client = datastore.Client(credentials=credentials)
    key = client.key('ShoppingCart', conversation_id)
    entity = datastore.Entity(key=key)
    result = client.get(key)

    if result is None:
        if cart_cmd == CMD_ADD_ITEM:
            entity.update({
                item_name: 1
            })
        elif cart_cmd == CMD_DEL_ITEM:
            # The user is trying to delete an item from an empty cart. Pass and skip
            pass

    else:
        if cart_cmd == CMD_ADD_ITEM:
            if result.get(item_name) is None:
                result[item_name] = 1
            else:
                result[item_name] = result[item_name] + 1

        elif cart_cmd == CMD_DEL_ITEM:
            if result.get(item_name) is None:
                # The user is trying to remove an item that's no in the shopping cart. Pass and skip
                pass
            elif result[item_name] - 1 > 0:
                result[item_name] = result[item_name] - 1
            else:
                del result[item_name]

        entity.update(result)
    client.put(entity)

확인 메시지 보내기

사용자가 장바구니에 항목을 추가하면 이 작업을 확인하고 요청이 처리되었음을 알리는 확인 메시지를 전송해야 합니다. 이러한 방식은 앞으로 전개될 상황을 예상하고 대화가 계속 진행될 수 있게 해줍니다.

항목이 추가되었거나 삭제되었음을 알리는 메시지를 대화 ID에 전송하고 장바구니 검토 또는 메뉴 표시를 제안할 수 있도록 update_shopping_cart 함수를 확장해보겠습니다.

bonjourmeal-codelab/step-2/bopis/views.py

def update_shopping_cart(conversation_id, message):

     # No changes to the function, except appending the following logic
     ...
   
    if cart_cmd == CMD_ADD_ITEM:
        message = 'Great! You\'ve added an item to the cart.'
    else:
        message = 'You\'ve removed an item from the cart.'

    message_obj = BusinessMessagesMessage(
        messageId=str(uuid.uuid4().int),
        representative=BOT_REPRESENTATIVE,
        text=message,
        suggestions=[
            BusinessMessagesSuggestion(
            reply=BusinessMessagesSuggestedReply(
                text='Review shopping cart',
                postbackData=CMD_SHOW_CART)
            ),
            BusinessMessagesSuggestion(
            reply=BusinessMessagesSuggestedReply(
                text='See menu again',
                postbackData=CMD_SHOW_PRODUCT_CATALOG)
            ),
            ])
    send_message(message_obj, conversation_id)

905a1f3d89893ba0.png

이제 다 되었습니다! 사용자가 항목을 추가 및 삭제하고 장바구니 항목을 검토할 수 있게 해주는 완전한 기능의 장바구니 환경이 갖춰졌습니다.

이제 Business Messages 대화에서 장바구니 기능을 확인하려면 에이전트와 상호작용하도록 애플리케이션을 배포하면 됩니다. 이렇게 하려면 step-2 디렉터리에서 다음 명령어를 실행하면 됩니다.

$ gcloud app deploy

5. 결제 준비

이 시리즈의 다음 부분에서 진행되는 결제 프로세서와의 통합을 준비하기 위해서는 장바구니의 가격을 가져오는 방법이 필요합니다. 이제 Google Datastore에서 장바구니 데이터를 교차 참조하고, 인벤토리에서 각 항목의 가격을 검색하고, 가격에 장바구니의 각 항목 수량을 곱하여 가격을 검색하는 함수를 빌드해보겠습니다.

bonjourmeal-codelab/step-2/bopis/views.py

...
def get_cart_price(conversation_id):
    # Pull the data from Google Datastore
    credentials = service_account.Credentials.from_service_account_file(
    SERVICE_ACCOUNT_LOCATION)
    client = datastore.Client(credentials=credentials)
    key = client.key('ShoppingCart', conversation_id)
    entity = datastore.Entity(key=key)
    result = client.get(key)

    # Retrieve the inventory data
    inventory = get_inventory_data()
   
    # Start off with a total of 0 before adding up the total
    total_price = 0

    if len(result.items()) != 0:
      for product_name, quantity in result.items():
        total_price = total_price + float(
            inventory['food'][get_id_by_product_name(product_name)]['price']) * int(quantity)

    return total_price

...

그리고 마지막으로 이 함수를 사용해서 사용자에게 메시지를 전송할 수 있습니다.

bonjourmeal-codelab/step-2/bopis/views.py

...

def send_shopping_cart_total_price(conversation_id):
    cart_price = get_cart_price(conversation_id)

    message_obj = BusinessMessagesMessage(
        messageId=str(uuid.uuid4().int),
        representative=BOT_REPRESENTATIVE,
        suggestions=[],
        text=f'Your cart\'s total price is ${cart_price}.')

    send_message(message_obj, conversation_id)
...

모든 것을 연결하기 위해 위 로직을 트리거하도록 route_message 함수 및 상수를 업데이트하겠습니다.

bonjourmeal-codelab/step-2/bopis/views.py

...
CMD_GET_CART_PRICE = 'show-cart-price'
...
def route_message(message, conversation_id):
    '''
    Routes the message received from the user to create a response.

    Args:
        message (str): The message text received from the user.
        conversation_id (str): The unique id for this user and agent.
    '''
    normalized_message = message.lower()

    if normalized_message == CMD_RICH_CARD:
        send_rich_card(conversation_id)
    elif normalized_message == CMD_CAROUSEL_CARD:
        send_carousel(conversation_id)
    elif normalized_message == CMD_SUGGESTIONS:
        send_message_with_suggestions(conversation_id)
    elif normalized_message == CMD_BUSINESS_HOURS_INQUIRY:
        send_message_with_business_hours(conversation_id)
    elif normalized_message == CMD_ONLINE_SHOPPING_INQUIRY:
        send_online_shopping_info_message(conversation_id)
    elif normalized_message == CMD_SHOW_PRODUCT_CATEGORY:
        send_product_catalog(conversation_id)
    elif CMD_ADD_ITEM in normalized_message or CMD_DEL_ITEM in normalized_message:
        update_shopping_cart(conversation_id, message)
    elif normalized_message == CMD_SHOW_CART:
        send_shopping_cart(conversation_id)
    elif normalized_message == CMD_GET_CART_PRICE:
        send_shopping_cart_total_price(conversation_id)
    else:
        echo_message(message, conversation_id)
...

다음은 위 로직으로 얻을 수 있는 결과를 보여주기 위한 스크린샷입니다.

8feacf94ed0ac6c4.png

다음 Codelab 부분에서 진행할 결제 프로세서와 통합할 준비가 되었으면 get_cart_price 함수를 호출하여 데이터를 결제 프로세서로 전달하고 결제 흐름을 시작합니다.

다시 말하지만 애플리케이션을 배포하고 에이전트와 상호작용해서 Business Messages 대화에서 이 장바구니 기능을 시험해볼 수 있습니다.

$ gcloud app deploy

6. 축하합니다

수고하셨습니다. 지금까지 Business Messages에서 장바구니 경험을 빌드하는 과정을 성공적으로 마쳤습니다.

이 Codelab에서 전체 장바구니를 비우는 기능 등은 다뤄지지 않았습니다. 필요에 따라 애플리케이션을 확장하여 '장바구니 비우기' 기능을 작성해볼 수 있습니다. 이 솔루션은 클론한 소스 코드의 step-3에 있습니다.

이후 섹션에서는 외부 결제 대행업체와의 통합으로 자신의 브랜드로 결제 트랜잭션이 완료될 수 있게 하는 과정이 진행됩니다.

좋은 장바구니란 어떤 것일까요?

대화를 통해 진행되는 장바구니 환경은 모바일 앱 또는 실제 매장에서 진행되는 것과 다릅니다. 항목을 추가 및 삭제하고 장바구니 가격을 계산하는 것은 이 Codelab에서 다뤄진 몇 가지 기능일 뿐입니다. 실제 장바구니에서는 항목을 추가하거나 삭제할 때 언제든지 장바구니의 모든 항목 가격을 볼 수 있게 하는 등의 다른 기능들이 필요합니다. 자신의 대화형 상거래 환경을 돋보이게 만들기 위해서는 이러한 고급 기능들이 필요합니다.

다음 단계

준비되었으면 다음 주제를 체크아웃해서 Business Messages에서 수행할 수 있는 복잡한 상호작용에 대해 자세히 알아보세요.

참조 문서