티스토리 뷰

반응형

 

지난번 Part5에 이어서 향상된 샌드위치 주문하기를 만들어 보도록 하겠습니다. 지난번 샌드 위치 주문은 다들 잘 하셨는지 궁금하군요 ㅋㅋ


Part1 : Bot Framework 시작하기

Part2 : Azure에 배포하고, Bot Framework에 등록하기

Part3 : Facebook 메신저와 연동하기

Part4 : 다이얼로그 처리

Part5 : 카키 샌드위치 주문하기

Part6 : 향상된 카키 샌드위치 주문하기


지난 시간에 사용했던 페이지에서 계속 이어 갑니다.

http://docs.botframework.com/sdkreference/csharp/forms.html



1. 뭘 향상 시킬까요?


1) 문자열과 날짜을 추가합니다.

2) 여러가지 속성을 추가합니다. (설명, 약관, 프롬프트, 템플릿)

3) Switching from fields to properties to incorporate business logic

4) 메시지를 추가하고, 흐름을 컨트롤 할 수 있습니다.



2. 추가 가능한 속성은 뭐가 있나요?


1) Describe

필드 또는 값이 텍스트로 표시되는 방식을 변경합니다.

2) Numeric

숫자 필드에 허용된 값에 대한 제한을 걸 수 있습니다.

3) Optional

하나의 선택 값을 제공하지 않는 것 같은 선택적 필드를 만들 수 있습니다.

4) Prompt

필드에 입력을 요구하는 프로프트를 정의 할 수 있습니다.

5) Template

프롬프트에서 사용되는 템플릿을 정의 합니다.

6) Terms

필드나 값과 일치하는 입력 조건을 정의합니다.



3. 정리를 좀 해보죠


그러니까 샌드위치를 주문할 때 "Please select a sandwich"라고 이야기하는 것과 "What kind of sandwich would you like?"라고 이야기하는 것 중 어떤 것이 더 좋겠나요? 당근 후자겠죠? 아니면, 한글로 "어떤 종류의 샌드위치를 주문하시겠어요?"라고 한다면, 금상첨화 일 것 같네요


KakiSandwichOrder.cs에서 아래 내용을 수정 하도록 하겠습니다.


[Prompt("어떤 종류의 {&}로 주문을 도와드릴까요? {||}")]
[Describe("메뉴")]
public KakiMenuOptions? Menu;


여기서 한가지 재미있는 문자열이 보이는데요~ "{&}"와 "{||}" 입니다. Pattern Language라고 하는데, 이 부분에 문자열이 대체되어서 생성이 됩니다. "{&}" 부분은 필드의 설명이 들어가고, "{||}" 부분은 선택 가능한 목록이 나열됩니다. 그 외 다른 녀석들에 대한 설명은 뒤에서 하도록 하고, 실행을 해서 결과를 확인 합니다.

메뉴라는 글씨와 3개의 선택 가능 내용이 리스트업이 됩니다. 그렇다면 "{||}"을 삭제하면 어떻게 될까요??


정답은 아무것도 출력되지 않는다~입니다. 으흠..그럼 뭘 주문할지 어떻게 알죠? 알 수 있는 방법은 ?를 입력하시면 알 수 있습니다.


음음.. 아무래도 나오는 것이 더 보기 좋은 듯하네요~ 흐흐;;

그렇다면 왜 Pattern Language를 사용하는 것일까? 하는 의문이 생깁니다. 각 단계마다 프롬프트를 추가해주면 될 듯한데 왜?? 굳이??

이유는~ Pattern Language를 이용해서 Template로 하나의 문장을 만들어 놓으면 모든 프롬프트가 변경되기 때문입니다. 다음과 같이 수정을 해 보도록 하겠습니다.


KakiSandwichOrder.cs 클래스 선언 위에 아래와 같은 속성을 추가합니다.


[Serializable]
[Template(TemplateUsage.EnumSelectOne, "고객님의 샌드위치에 어떤 종류의 {&}을(를) 선택 하시겠습니까?{||}",
    ChoiceStyle = ChoiceStyleOptions.PerLine)]
public class KakiSandwichOrder


TemplateUsage.EnumSelectOne 부분은 Enum으로 만들어진 녀석들 중 단일 선택인 필드에 적용할 문자열을 미리 정의하는 것입니다. 추가적으로, 필드들에 Describe을 추가해서 출력될 문자열을 이쁘게 만듭니다.


[Describe("빵")]
public KakiBreadOptions? Bread;
[Describe("추가재료")]
public KakiAdditionOptions? Addition;


이제 실행해서 결과를 확인 합니다.

영어로 나오는 것에 비해서 확실히 부드럽다는 생각이 드네요.



4. 세트는 싫어요~


역시가 저도 세트를 꼭 먹어야 한다고 생각하지는 않습니다. 으흐흐..고객의 선택인거죠? 이렇게 선택적인 필드는 어떻게 처리할까요?

Optional이라는 속성을 필드에 추가하면 간단하게 해결할 수 있습니다.


[Optional]
public List<KakiSetOptions> Set;


실행을 해보면 아래와 같이 표시됩니다.


no라고 입력하니 다음으로 넘어가는 군요 후후.. current choice라는 글씨도 한글로 바꾸고 싶어지내요;;;



5. 똑같은 맨트는 싫어요~


주문을 하는 동안 잘못된 단어를 이야기 했을 때 동일한 단어를 반복적으로 대답하면 싫겠죠? 다음과 같이 수정합니다.


public class KakiSandwichOrder
[Template(TemplateUsage.EnumSelectOne, "고객님의 샌드위치에 어떤 종류의 {&}을(를) 선택 하시겠습니까?{||}",
    ChoiceStyle = ChoiceStyleOptions.PerLine)]
[Template(TemplateUsage.NotUnderstood, "이야기하신 단어를 이해하지 못 했습니다.\"{0}\".", "제가 난독증 일까요?\"{0}\".", "머라카노? 자꾸 그럴끼가?\"{0}\"")]
public class KakiSandwichOrder


입력한 단어를 처리할 수 없는 경우 3가지 경우 중에 하나를 반환 합니다. 음..도움말을 보여줄 때도 있으니 총 4가지 형태가 되겠네요



6. 대충 이야기해도 알아 들었으면 좋겠넹~


지금 카키 샌드위치에는 적용을 하기가 좀 어렵지만, Terms 속성에 대해 설명을 하도록 하겠습니다.

1) 대소문자가 변하거나 _가 있는 경우 단어로 인정

2) 쵀대 길이까지 구문 생성;;;

3) 단어 끝에 s?가 붙으면 복수형을 지원

예를 들어

[Terms(@"rotis\w* style chicken", MaxPhrase = 3)]RotisserieStyleChicken

이렇게 속성이 들어가있다고 하면, 기본적으로 대소문자가 변하는 부분이 3부분이니까 3개의 단어가 있다고 생각하고, rotis로 대충 시작하면 Rotisserie로 인식해라~라는 것과 최대 3개의 단어로 구성된다..뭐 그런 내용입니다.



7. 비지니스 로직을 추가해 보죠


어떤 고객님이 오셔서 난 모든 야채를 다 넣고 싶어~라고 한다면, 네~ 그렇다면 1,2,3,4,5,6....을 모두 입력하세요~라고 이야기를 해야겠죠?? 쿨럭;;  하지만, 고객이 왕인데..그렇게 이야기하면 싫어할 것 같네요. 대신, 고객님~ 전체를 선택하시면 됩니다~라고 안내를 해주세요~

먼저, 야채 부분을 약간 손대 보죠


public enum KakiVegetableOptions
{
    [Terms("모두", "몽땅", "다", "전부")]전체 = 1,
    양상추, 토마토, 오이, 피망, 양파, 피클, 올리브, 할라피뇨
}


여기서 Terms라는 속성을 이용해서 전체라는 단어를 여러가지 다른 단어로도 입력 할 수 있도록 했습니다. 그럼 1번을 선택하면 양상추, 토마토, 오이, 피망, 양파, 피클, 올리브, 할라피뇨가 모두 선택이 되어야 겠죠?

다음을 진행하기 위해서는 LINQ를 사용해야 하는데, 프로젝트에 System.Xml.Linq가 추가되어야 할 것 같내요.

KakiSandwichOrder.cs 상단에

using System.Linq;

문을 추가한 후에 아래 코드를 입력합니다.


private List<KakiVegetableOptions> _vegetable;
[Describe("야채")]
public List<KakiVegetableOptions> Vegetable
{
    get { return _vegetable; }
    set
    {
        if (value != null && value.Contains(KakiVegetableOptions.전체))
        {
            _vegetable = (from KakiVegetableOptions kkk in Enum.GetValues(typeof(KakiVegetableOptions))
                where kkk != KakiVegetableOptions.전체
                select kkk).ToList();
        }
        else
        {
            _vegetable = value;
        }
    }
}


위 코드는 '전체'가 선택이 되어있는 경우 '전체'를 제외한 나머지 항목을 모두 선택하는 것 입니다.

그런데.. 이렇게 하고나면..야채 선택하는 메시지가 제일 아래에서 출력이 되네요..이런 이런;;



8. Form Builder을 이용해서 최종 주문 폼을 만들어 보죠


폼 빌더를 이용해서 좀더 사용자 친화적인 대화 환경을 조성할 수 있습니다. 아래 코드를 볼까요?


public static IForm<KakiSandwichOrder> BuildForm()
{
    OnCompletionAsyncDelegate<KakiSandwichOrder> processOrder =
        async (context, state) => { await context.PostAsync("현재 고객님의 샌드위치를 만들고 있습니다. 준비되면 알려 드리겠습니다."); };

    return new FormBuilder<KakiSandwichOrder>()
        .Message("안녕하세요 카키 샌드위치 주문 봇입니다. help 혹은 ? 를 입력하시면 도움말을 보실 수 있습니다.")
        .Field(nameof(Menu))
        .Field(nameof(Bread))
        .Field(nameof(Addition))
        .Field(nameof(Vegetable))
        .Message("아래의 야채들을 선택 하셨습니다.{Vegetable}")
        .Field(nameof(Source))
        .Field(nameof(Set))
        .Field(nameof(OrderCount))
        .Field(new FieldReflector<KakiSandwichOrder>(nameof(Specials))
            .SetType(null)
            .SetActive(state => state.Menu == KakiMenuOptions.Menu30cm)
            .SetDefine(async (state, field) =>
            {
                field
                    .AddDescription("콜라", "무료 콜라")
                    .AddTerms("콜라", "무료 콜라")
                    .AddDescription("음료", "큰 음료 무료")
                    .AddTerms("음료", "무료 음료");
                return true;
            }))
        .Confirm(async state =>
        {
            var cost = 0.0;
            switch (state.Menu)
            {
                case KakiMenuOptions.Menu15cm:
                    cost = 5000*state.OrderCount;
                    break;
                case KakiMenuOptions.Menu30cm:
                    cost = 6500*state.OrderCount;
                    break;
                case KakiMenuOptions.찹샐러드:
                    cost = 7500*state.OrderCount;
                    break;
            }
            return new PromptAttribute($"고객님이 주문하신 총 금액은 {cost:N0}원 입니다. 맞습니까?(yes/no)");
        })
        .Field(nameof(DeliveryAddress),
            validate:async (state, response) =>
            {
                var result = new ValidateResult {IsValid = true, Value = response };
                var address = (response as string).Trim();
                if (address.Length == 0)
                {
                    result.Feedback = "주소를 입력하세요.";
                    result.IsValid = false;
                }
                return result;
            })
        .Field(nameof(DeliveryTime), "언제까지 배달해 드릴까요? 시간:분으로 입력해 주세요.{||}")
        .Confirm("최종 주문 확인 하겠습니다. {Menu} 메뉴에 빵은 {Bread}, 추가재료는 {Addition}, 야채는 {Vegetable}, 소스는 {Source}를 선택하신 샌드위치 {OrderCount}개를 주문하셨으며, 배송지는 {DeliveryAddress} {?at {DeliveryTime:t}} 입니다. 주문하신 내용이 맞습니까?")
        .AddRemainingFields()
        .Message("주문해주셔서 감사합니다!")
        .OnCompletionAsync(processOrder)
        .Build();
}


컥 갑자기 내용이 늘었다고 긴장하실 필요는 없습니다. 제가 이미 삽질 다해서 파악을 해 놓았으니 설명들어보시면 쉽게 이해가 되실 것이라고 생각합니다. 보시다가 이해가 어려운 부분은 리플 남겨주시면 추가 설명을 하도록 하겠습니다.


* Dynamically Defined Fields, Confirmations and Messages

주문을 하는 중에 유동적으로 필드를 생성하거나, 확인 메시지를 출력하기 위한 경우에 대해서 설명을 드립니다.

state는 현재 주문 진행 중인 KakiSandwichOrder를 이야기 합니다.


        [Optional] [Template(TemplateUsage.NoPreference, "None")]
        [Describe("스페셜")]
        public string Specials;


        .Field(new FieldReflector<KakiSandwichOrder>(nameof(Specials))
            .SetType(null)
            .SetActive(state => state.Menu == KakiMenuOptions.Menu30cm)
            .SetDefine(async (state, field) =>
            {
                field
                    .AddDescription("콜라", "무료 콜라")
                    .AddTerms("콜라", "무료 콜라")
                    .AddDescription("음료", "큰 음료 무료")
                    .AddTerms("음료", "무료 음료");
                return true;
            }))


스페셜이라는 필드가 있습니다. 이 필드는 메뉴(맨 처음 선택하는 녀석 3가지) 중 Menu30cm를 선택하는 경우에만 출력되는 녀석입니다. 콜라와 음료를 무료로 제공하는 녀석이죠.


        .Confirm(async state =>
        {
            var cost = 0.0;
            switch (state.Menu)
            {
                case KakiMenuOptions.Menu15cm:
                    cost = 5000*state.OrderCount;
                    break;
                case KakiMenuOptions.Menu30cm:
                    cost = 6500*state.OrderCount;
                    break;
                case KakiMenuOptions.찹샐러드:
                    cost = 7500*state.OrderCount;
                    break;
            }
            return new PromptAttribute($"고객님이 주문하신 총 금액은 {cost:N0}원 입니다. 맞습니까?(yes/no)");
        })


이 녀석은 금액을 계산해 주고 있습니다. 현재 주문에서 선택한 Menu가 어떤 것인지 확인하고, 주문 수량 곱해서 금액을 출력해 줍니다.


이 내용을 입력하면 코드에 경고가 출력될 수 있습니다. 음..제가 사옹하고 있는 툴에서 나온 경고인지, 비주얼 스튜디오 경고인지 확실하지는 않치만.. async 뒤에 구문 때문에 경고가 출력이 되는데, 무시하셔도 됩니다. 그래도, 신경이 쓰이신다면,


파일 맨 위에 아래와 같은 코드를 추가해서 경고 메시지가 출력되지 않도록 처리할 수 있습니다.

#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously



9. MessagesController.cs 도 약간 수정해 줍니다.

internal static IDialog<KakiSandwichOrder> KakiMakeRootDialog()
{
    return Chain.From(() => FormDialog.FromForm(KakiSandwichOrder.BuildForm))
        .Do(async (context, order) =>
        {
            try
            {
                var completed = await order;
                if (completed == null) return;
                // Actually process the sandwich order...
                await context.PostAsync("Processed your order!");
            }
            catch (FormCanceledException<KakiSandwichOrder> e)
            {
                string reply;
                if (e.InnerException == null)
                {
                    reply = $"You quit on {e.Last}--maybe you can finish next time!";
                }
                else
                {
                    reply = "Sorry, I've had a short circuit.  Please try again.";
                }
                await context.PostAsync(reply);
            }
        });
}


모든 주문이 끝나면 completed에 주문완료된 내용이 반환되면, 이 주문 내용을 DB에 저장하고 매장에 알려서 만들고 배달을 시키면 되겠죠?




10. 소스

KakiSandwichOrder.cs




반응형
댓글