MixedCode

본격적으로 헤어 살롱 자동 예약 챗봇 서비스를 개발해보도록 하겠습니다.
이해를 돕고자 HOL에서 진행했던 개발소스 와 발표자료도  공유드립니다. 

목표하는 미용실 예약 자동화 챗봇의 모습을 먼저 확인해보겠습니다.



개발 샘플 솔루션 다운로드 : 
http://www.chatbotmate.com/MyChatBotSolution.zip

발표자료 다운로드 : http://www.chatbotmate.com/AzurebootCamp2018-SmartChatbot2.pptx


챗봇개발의 시작은 챗봇이 적용되는 해당 비지니스의 대한 이해에서 시작됩니다.

미용실 예약자동화 챗봇을 개발하기에 앞서 미용실 예약 프로세스를 파악하고 챗봇으로 적용할 시나리오를 아래와 같이 구성합니다.

시나리오 작성은 파워포인트의 순서도나 UML 툴을 이용한 액티비티 다이어그램으로 작성할수 있습니다.




시나리오를 확인해 보면 사용자 클라이언트 채널에서 최초 메시지가 넘어오기 전에 채널과 챗봇이 연결되면 챗봇이 먼저 환영인사를

건네고 회원여부를 물어봅니다. 비회원이더라도 바로 예약할수 있도록  비인증 시나리오를 통해 예약할수 있어야합니다.


미용실의 서비스 목록을 제공하고 사용자가 예약서비스를 선택하면 미용실내 헤어디자이너의 목록을 챗봇이 제시하는등의  프로세스를 이해하고

관련 시나리오를 구체적으로 수립 후 챗봇개발을 시작해야합니다. 

상기 시나리오중 비회원이 미용실 예약을 진행하는 시나리오를 중심으로 미용실 예약자동화 챗봇 기능을 구현해도보록 하겠습니다.


1) 사용자에게 먼저 말걸어주는 친절한 챗봇 기능구현하기

- 기존의 심플챗봇은 사용자가 최초 메시지를  보내주지 않으면 아무런 반응이 없는 불친철한 챗봇이였습니다.

- 사용자 채널(에뮬레이터,채팅 클라이언트) 과 챗봇이 최초 연결되면 사용자가 최초 메시지를 보내지 않더라도 사용자(채널)에게 먼저 인사를 건네는 친절한 챗봇기능을 구현합니다.

- 사용자 채널이 접속하는 OPEN API EndPoint 주소로 제공되는  Controllers폴더의 MessagesController.cs 파일을 오픈합니다.

- 사용자로부터 전달되는 메시지를 처리하는 HandleSystemMessage메소드의 내용을 아래 코드와 같이 변경처리합니다.

- 그리고 MessagesController.cs 상단 참조 영역에 아래 using 구문을 추가합니다.

using System;

using System.Linq;


        private Activity HandleSystemMessage(Activity message)

        {

            string messageType = message.GetActivityType();

            if (messageType == ActivityTypes.DeleteUserData)

            {

                // Implement user deletion here

                // If we handle user deletion, return a real message

            }

            else if (messageType == ActivityTypes.ConversationUpdate)

            {

                //챗봇이 먼저 대화를 건다.

                if (message.MembersAdded.Any(o => o.Id == message.Recipient.Id))

                {

                    var reply = message.CreateReply("안녕하세요.\n\n 비긴메이트 헤어살롱 예약 도우미 챗봇입니다.^^");

                    ConnectorClient connector = new ConnectorClient(new Uri(message.ServiceUrl));

                    connector.Conversations.ReplyToActivityAsync(reply);

                }

            }

            else if (messageType == ActivityTypes.ContactRelationUpdate)

            {

                var test = "";

                // Handle add/remove from contact lists

                // Activity.From + Activity.Action represent what happened

            }

            else if (messageType == ActivityTypes.Typing)

            {

                // Handle knowing that the user is typing

            }

            else if (messageType == ActivityTypes.Ping)

            {

            }

            return null;

        }




-MessagesController.cs 의 Post 메소드는 사용자 채널로 부터 전달되는 각종 메시지와 상태정보를 Activity객체를 통해 전달받습니다.

-사용자가 메시지를 입력해 Post 메소드로 전달되는  Activity 파라메터의 유형은 ActivityTypes.Message 이며 최초에 사용자 채널과 챗봇이 연결되거나 사용자 메시지 전송없이 대화 상태만 변경되는

경우의 Activity 파라메터 유형은 ActivityTypes.ConversationUpdate 입니다.

-사용자 채널과 챗봇이 최초 연결되면 챗봇에서는 사용자 인식 및 인증을 위해 아래와 같이 2단계과정을 거칩니다.

-첫번째는  ActivityTypes.ConversationUpdate 유형으로  Activity 가 전달되면 챗봇은  최초 연결되는 사용자 채널을 위한 고유ID토큰값을 발급하고 챗봇에 동일 ID값과 관련정보를 저장 합니다.

-두번째로 사용자 채널은 챗봇으로부터 발급된 토큰을 이용해  챗봇에 재연결을 시도합니다.

-두번째 연결시도시  챗봇은 이미 등록된 사용자임을 감지하고  환영인사 메시지를 작성한 후 ConnectorClient 객체를 통해 환영 메시지를 사용자 채널에 전송합니다.


2) 코딩된 환영인사가 적용되는지 에뮬레이터를 통해 확인해보겠습니다.

-Visual Studio에서 F5 또는 상단 아이콘중 녹색 디버깅 아이콘 또는 상단 메뉴중 디버그>디버깅 시작 메뉴를 클릭해 브라우저를 통한 디버깅을 실시합니다.

-디버깅 상태에서 여러분 컴퓨터에 설치되어 있는 챗봇 에뮬레이터(아이콘이 바탕화면에 존재함)를 실행합니다.

-애물레이터의 BOT EXPLOER 상단 Welcome 탭 화면 하단 My Bots 에 기존에 등록해둔 챗봇 설정파일( ChatBotApplication )을 클릭하거나 Open Bot 버튼을 클릭해 기존에 챗봇 Endpoint정보를 

저장해둔 ~.bot 파일을 찾아 오픈합니다. 

-챗봇 주소 설정파일( ~.bot) 파일위치가 기억나지 않으면 Welcome탭내 create a new bot configuration 메뉴를 클릭해 관련정보를 입력 후 로컬에 설정파일을 저장합니다.

-챗봇 EndPoint 주소를 클릭해보면 아래 화면처럼 정상적으로 챗봇이 먼저 인사를 건네는 내용을 확인할수 있습니다.



3) 미용실 예약 전용 루트 다이얼로그 생성하기

-Dialogs폴더에 오른쪽 마우스 클릭 후  추가>새항목을 클릭합니다.

-템플릿 선택 팝업창에서 Bot Dialog 템플릿을 선택하고 이름란에 "BeautySalonRootDialog.cs" 라고 입력 후 추가버튼을 클릭합니다.

-생성된 BeautySalonRootDialog.cs 내 MessageReceivedAsync 메소드안에 아래 코드를 추가합니다.

- Dialog 클래스는  특정 대화주제별로 다양하게 추가 생성이 가능하며 주제별 다이얼로그간 이동이 가능하여 전체 채봇 시스템을 대화 주제별로 각종 다이얼로그 클래스를

생성하여 분리해 챗봇 시나리오를 체계적으로 관리할수 있습니다. 

<그림>


    [Serializable]

    public class BeautySalonRootDialog : IDialog<object>

    {

        public Task StartAsync(IDialogContext context)

        {

            context.Wait(MessageReceivedAsync);


            return Task.CompletedTask;

        }


        private async Task MessageReceivedAsync(IDialogContext context, IAwaitable<object> result)

        {

            var activity = await result as IMessageActivity;

            await context.PostAsync($"고객님이 입력한 메시지는 {activity.Text} 입니다.");

            context.Wait(MessageReceivedAsync);

        }

    }


-아래부터는 챗봇의 최초 코딩부분이라 다이얼로그 클래스의 주요기능들에 대해 좀 더 자세히 설명드리겠습니다.

-BeautySalonRootDialog  클래스를 포함해 모든 Dialog 클래스들은 기본적으로 IDialog 인터페이스를 상속받습니다.

-IDialog 인터페이스에서 정의된 StartAsync()메소드는 반드시 상속받은 클래스에서는 구현해야 하며 구현된 StartAsync() 메소드는 해당 다이얼로그의 객체가 생성시

최초로 실행되는 진입점 역할을 수행합니다.

-StartAsync() 실행시 해당 다이얼로그 전체 컨텍스트 정보를 관리해주는 IDialogContext 객체가 파라메터로 전달되어 context 객체를 통해 해당 다이얼로그 객체의 정보 및 상태를 관리할수

환경 및 다이얼로그 컨텍스트의 공통기능을 제공해줍니다.  


- MessageReceivedAsync() 메소드내의 context.PostAsync("메시지") 주요 코드를 확인합니다.

- context 객체는 DialogContext 클래스를 말하며 DialogContext 클래스의 PostAsync("메시지") 메소드는 챗봇의 각종 다이얼로그 객체에서 사용자 채널로 메시지를 전송할때 주로 사용합니다.

- context.Wait(MessageReceivedAsync) 코드의  Wait( 특정 메소드명)의 역할은 말그대로 대화 흐름을 멈추고 사용자로부터 다음 메시지가 올때까지 대기하고 다음메시지가 왔을때 Wait 메소드내에

지정한 특정 메소드가 실행되면서 대화의 흐름을 이어갈수 있게하는 기능을 제공합니다.

-즉 MessageReceivedAsync() 메소의 내용을 말로 풀어보면  사용자로 부터 전달된 메시지를 받아 새로운 답변 메시지를 만들고 PostAsync() 메소드를 통해 즉시 사용자 채널로 챗봇 메시지를 전송한 후 

다음 사용자 메시지가 도착하면 다시 MessageReceivedAsync()메소드가 대화의 흐름을 받아 진행하게 코딩한것입니다. 

-현재 코드에서는 MessageReceivedAsync()메소드가 재귀호출되는 구조로 되어 있지만 추후 코드에서는 진행 시나리오별로 별도의 대기 메소드들이 지정됩니다.



4) 루트 다이얼로그 변경하기

-환영 메시지를 받은 사용자가 채팅 메시지를 입력하고 챗봇에 메지를 보내면 MessagesController.cs 클래스의 Post 메소드를 통해 Activity 타입의 파라메터가 

전달되며 Activity 파라메터의 유형은 ActivityTypes.Message 로 전달됩니다.

-ActivityTypes.Message 인경우 대화 흐름을 제어하는  Conversation 클래스를 통해 최상위 대화 주제 다이얼로그를 지정할수 있는데 

기본적으로 Dialogs폴더내 RootDialog 클래스로 지정되어있습니다.

-디폴트 RootDialog 대신 방금 만든 미용실 예약 루트 다이얼로그 BeautySalonRootDialog.cs() 로 아래와 같이 다이얼로그를 변경합니다.

-사용자 채널과의 대화 흐름제어는 아래 코드처럼 Conversation.SendAsync() 를 통해  특정 대화 주제 다이얼로그 지정하고 사용자 메시지가 포함된 액티비티 객체를 다이얼로그에 전달합니다.


       public async Task<HttpResponseMessage> Post([FromBody]Activity activity)

        {

            if (activity.GetActivityType() == ActivityTypes.Message)

            {

                await Conversation.SendAsync(activity, () => new Dialogs.BeautySalonRootDialog());

            }

            else

            {

                HandleSystemMessage(activity);

            }

            var response = Request.CreateResponse(HttpStatusCode.OK);

            return response;

     }



4) 디버깅을 하고 애물레이터를 통해 적용된 내용을 확인해보겠습니다.

- Visual Studio에서 F5를 누르고 디버깅을 실시합니다.

- 챗봇 에뮬레이터 오픈하고 챗봇 엔드포인트를 클릭해 챗봇과 연결 후 환영 메시지 이후 직접 메시지를 입력 후 전송버튼을 클릭합니다.

- 미용실 예약 루트 다이얼로그의 변경된 답변 메시지가 챗봇으로부터 전달되면 정상적용된것입니다.



5) 회원여부 물어보기

-이제 본격적으로 미용실 예약 시나리오를  챗봇 코드로 구현해보도록 합니다.

-BeautySalonRootDialog.cs 클래스를 오픈하고 이전에 코딩한 MessageReceivedAsync() 메소드 내용을 아래 MessageReceivedAsync()메소드 내용으로 변경합니다.

-HelpReplyReceivedAsync() 메소드를 추가합니다.

-context.PostAsync(메시지)를 통해 챗봇에서 연결된 사용자 채널로 즉시 메시지를 전송합니다.

-Wait메소드를 통해 사용자로부터 다음 메시지를 수신할때까지 대화 흐름상태를 대기상태로 전환하며 다음 메시지가 도착하면  HelpReplyReceivedAsync()메소드가 실행될수있게 메소드명을

지정해둡니다. 


        /// <summary>

        /// Step1. 회원 여부 묻기

        /// </summary>

        /// <param name="context"></param>

        /// <param name="result"></param>

        /// <returns></returns>

        private async Task MessageReceivedAsync(IDialogContext context, IAwaitable<object> result)

        {

            await context.PostAsync($"비긴메이트 헤어살롱 회원이시면 예를 그렇지 않으면 아니오 를  입력해주세요.");


            //2.다음단계 응답처리 함수 설정 및 수신대기

            context.Wait(HelpReplyReceivedAsync);

        }


-HelpReplyReceivedAsync() 는 사용자 메시지 대기함수입니다.

-HelpReplyReceivedAsync() 는 사용로부터 응답 메시지가 도착하면 실행되는 메소드로 정상적인 시나리오를 기대한다면 사용자가 예 또는 아니오 글자를 입력해 메시지를 전송했을것이고

예 또는 아니오란 메시지 또는 기타 텍스트가 전송되었을 시나리오를 대비해 코드를 아래와 같이 작성합니다.

-activity 객체의 Text란 속성은 사용자 보낸 메시지내용을 확인할수 있습니다.

-사용자가 보낸 메시지 내용에 따라 조건문으로 다음 시나리오를 처리하기 위한 메소드들을 실행합니다.

-하기 코드에서 else 블록 다시 이전 질문하기 부분을 보면 예 또는 아니오 텍스트가 아니면 다시 이전 동일질문을 보내기 위해 this.MessageReceivedAsync()를 실행합니다.


        /// <summary>

        /// Step2. 회원여부 사용자 답변 분석하기

        /// </summary>

        /// <param name="context"></param>

        /// <param name="result"></param>

        /// <returns></returns>

        private async Task HelpReplyReceivedAsync(IDialogContext context, IAwaitable<object> result)

        {

            //1.채널로부터 전달된 Activity 파라메터 수신

            var activity = await result as Activity;


            //2.대화 로직처리하기

            if (activity.Text.ToLower().Equals("yes") == true || activity.Text.ToLower().Equals("y") || activity.Text.Equals("예"))

            {

                //2.1 서비스 유형 선택 요청

                //await this.ConfirmServiceTypeMessageAsync(context);

            }

            else if (activity.Text.ToLower().Equals("no") == true || activity.Text.Equals("아니오") == true || activity.Text.IndexOf("아니") > -1)

            {

                //2.2 신규회원가입 다이얼로그 전환

                //context.Call(new MembershipDialog(), ReturnRootDialogAsync);

            }

            else

            {

                //다시 이전 질문하기

                await this.MessageReceivedAsync(context, null);

            }

      }


6) 사용자가 예를 답변하여 회원인 경우 미용실 서비스 정보 제공 하기 

- 상기 시나리오에서 사용자가 "예" 메시지를 보내오면 미용실에서 제공하는 서비스 정보를 제공하기 위한 this.ConfirmServiceTypeMessageAsync() 메소드를 실행할수 있게

  해당 조건문 블럭의  //await this.ConfirmServiceTypeMessageAsync(context);  코드 주석( ' // ')을 제거한후 아래 ConfirmServiceTypeMessageAsync()메소드를 추가합니다.

- 아래 코드에서는 HeroCard라는 클래스를 이용해 단순 문자메시지가 아닌  <이미지+제목+서브제목+버튼목록> 조합의 카드형태의 메시지로 사용자에게

다양한 정보담아 전달하는 기능을 구현합니다. 


      /// <summary>

        /// 3.서비스 유형 선택

        /// </summary>

        /// <param name="context"></param>

        /// <returns></returns>

        private async Task ConfirmServiceTypeMessageAsync(IDialogContext context)

        {

            var reply = context.MakeMessage();


            var options = new[]

            {

                "예약하기",

                "개인정보변경하기",

                "헤어살롱둘러보기"

            };

            reply.AddHeroCard("서비스선택", "아래 원하시는 서비스유형을 선택해주세요.", options, new[] { "http://chat.beginmate.com/Images/Eelection/hairshop1.jpg" });

            await context.PostAsync(reply);


            //context.Wait(this.OnServiceTypeSelected);

     }


7) AddHeroCard() 확장 메소드 구현하기

-MessageActivity에 AddHeroCard() 확장메소드 기능을 구현하기 위해  ChatBotApplication 프로젝트에 오른쪽 마우스 클릭 후 추가>새폴더 를 클릭하고 폴더명으로 "Extensions"로 입력합니다.

-"Extensions" 폴더에 오른쪽 마우스 클릭>추가 >클래스를 클릭하여 HeroCardExtensions.cs 클래스를 아래와 같이 생성합니다.

- 해당 클래스 상단에 using Microsoft.Bot.Connector; 구문을 추가합니다.

-HeroCardExtensions.cs 에 아래  클래스 관련코드를 추가합니다.

-HeroCardExtensions.cs 클래스는 MessageActivity 액티비티 객체에 HeroCard 관련 각종 확장메소드를 추가 제공하는 기능을 담당합니다.

-BeautySalonRootDialog.cs 클래스로 이동해 HeroCardExtensions.cs 사용을 위해 상단 참조영역에 using ChatBotApplication.Extensions; 참조를 추가합니다.

-Visual Studio 상단메뉴중 빌드>솔루션 다시 빌드를 클릭하여 개발한 내용을 저장하고 정상적으로 코딩이 되었는지 확인합니다.

-빌드시 빌드 에러가 나지 않으면 코드상에 문제가 없는것입니다.



8) 디버깅을 하고 애물레이터를 통해 지금까지 적용된 시나리오를 확인해보겠습니다.

- Visual Studio에서 F5를 누르고 디버깅을 실시합니다.

- 챗봇 에뮬레이터 오픈하고 챗봇 엔드포인트를 클릭해 챗봇과 연결 후 지금까지의 시나리오가 정상적으로 진행되는지 확인합니다.

- 회원여부에서 예를 입력하고 미용실 서비스 히어로카드가 나타나면 정상적으로 진행된것입다.



9) 사용자가 "아니오" 를 답변한 경우 회원 가입 다이얼로그 로 이동하기 

-윗부분에서는 회원여부 질문에 대해 예를 답한경우에 대한 시나리오를 어느정도 구현해보았으며 지금부터는 "아니오" 답변을 했을때의 시나리오를 구현하겠습니다.

-HelpReplyReceivedAsync()메소드내에서 주석처리되어있는 //context.Call(new MembershipDialog(), ReturnRootDialogAsync);의 주석( '//' )을 제거합니다.

- 코드를 분석해 보면 개별 주제별 Dialog에서 다른주제의 Dialog로 이동하고자 할때는 DialogContext 의 Call()메소드를 이용해 이동합니다.

- 현재 대화의 흐름이 진행되고 있는 BeautySalonRootDialog 에서 신규 회원가입을 위한 새로운 MembershipDialog 를 생성하고 해당 다이얼로그 이동해 

회원가입이라는 새로운 대화흐름을 진행 후 회원가입이 완료되거나 관련 주제가 종료되면 다시 BeautySalonRootDialog 에 정의되어 있는 ReturnRootDialogAsync()로 돌아오는 흐름을 

제어하는 시나리오를 보여주는 예시입니다.

-기존처럼 Dialogs 폴더에 오른쪽 마우스 클릭 > 추가 > 새항목을 클릭하고 Bot Dialog 템플릿을 선택 후 "MembershipDialog.cs"를 추가합니다.

-신규회원 가입프로세스가 진행되며 신규 회원 가입이 완료되면 context.Done(true); 메소드를 통해 해당 주제 다이얼로그를 종료합니다.

-현재 진행되는 다이얼로그가 종료되면 해당 다이얼로그를 호출한 상위 다이얼로그로 자동 이동되며 기존에 지정한 상위다이얼로그의 지정메소드로 흐름이 돌아갑니다.


using System;

using System.Threading.Tasks;

using Microsoft.Bot.Builder.Dialogs;

using Microsoft.Bot.Connector;


namespace ChatBotApplication.Dialogs

{

    [Serializable]

    public class MembershipDialog : IDialog<object>

    {

        public Task StartAsync(IDialogContext context)

        {

            context.PostAsync($"신규 회원 가입 프로세스를 진행합니다.\n 성함을 입력해주세요.");

            context.Wait(MessageReceivedAsync);

            return Task.CompletedTask;

        }


        private async Task MessageReceivedAsync(IDialogContext context, IAwaitable<object> result)

        {

            var activity = await result as IMessageActivity;

            await context.PostAsync($"전화번호를 입력해주세요.");

            context.Wait(EntryNewMember);

        }


        private async Task EntryNewMember(IDialogContext context, IAwaitable<object> result)

        {

            //신규 회원 가입로직 처리 

            await context.PostAsync($"신규 회원 가입이 완료되었습니다.");

            context.Done(true);

        }

    }

}



-BeautySalonRootDialog 에 ReturnRootDialogAsync() 메소드 코드를 아래와 같이 추가합니다.


     /// <summary>

        /// MembershipDialog에서 루트 다이얼로그로 돌아옴

        /// </summary>

        /// <param name="context"></param>

        /// <param name="result"></param>

        /// <returns></returns>

        private async Task ReturnRootDialogAsync(IDialogContext context, IAwaitable<object> result)

        {

            await context.PostAsync($"신규 회원 가입 완료 후 서비스 메뉴로 이동합니다.");

            //서비스 메뉴 이동

            await this.ConfirmServiceTypeMessageAsync(context);

        }



10) 디버깅을 하고 애물레이터를 통해 지금까지 적용된 시나리오를 확인해보겠습니다.

- Visual Studio에서 F5를 누르고 디버깅을 실시합니다.

- 챗봇 에뮬레이터 오픈하고 챗봇 엔드포인트를 클릭해 챗봇과 연결 후 지금까지의 시나리오가 정상적으로 진행되는지 확인합니다.

- 관련된 메시지가 출력되고 최종적으로 미용실 서비스 메뉴가 출력되면 성공적으로 관련 시나리오가 진행된것입니다.



11) 미용실 제공 서비스 메뉴 선택 및 예약하기 기능 구현하기

- 6번 항목 ConfirmServiceTypeMessageAsync() 에서 제공한 서비스 메뉴 선택 코드내의 //context.Wait(this.OnServiceTypeSelected);  이부분의 주석('//')을 삭제합니다.

- context.Wait(this.OnServiceTypeSelected); 이부분은 사용자에게 제공된 서비스 메뉴를 선택 클릭하거나 메뉴명을 입력 후 전송 버튼을 클릭하면 사용자의 메시지를

받아 시나리오를 진행하는 메소드입니다.

- 아래처럼 OnServiceTypeSelected() 메소드를 구현합니다.

- 코드를 확인해보면  사용자가 선택한 메뉴에 따라 시나리오를 분기처리해주는것을 확인할수 있습니다.

- 먼저 예약하기 메뉴의 기능을구현하기 위해 관련 분기문의 주석을 해제하고 OnServiceTypeSelected() 메소드 상단의 예약정보 저장 멤버변수 정의내용을 확인합니다.

- 사용자 예약정보 저장을 위한 ReservationModel 클래스를  솔루션 탐색기 프로젝트내의 Models 란 새폴더를 추가하시고 Models폴더에 오른쪽 마우스 클릭> 추가 > 클래스를 클릭 후 

"ReservationModel.cs" 로 클래스명을 지정후 추가합니다.

- ReservationModel.cs 클래스의 코드를 아래와 같이 코딩합니다.

- BeautySalonRootDialog.cs 파일 상단 참조영역에  using ChatBotApplication.Models; 모델 네임스페이스를 참조추가합니다.

- 사용자가 예약하기 메시지를 보내오면 예약정보를 저장하기 위한 ReservationModel의 인스턴스를 만들고 DesignerListMessageAsync() 메소드를 통해 

  미용실의 헤어디자이너 목록정보를 생성해 사용자 채널로 전송합니다. 


       //예약정보 멤버변수 선언

       private ReservationModel memberReservation = null;


        /// <summary>

        /// 3.1 서비스 유형 선택 결과 처리

        /// </summary>

        /// <param name="context"></param>

        /// <param name="result"></param>

        /// <returns></returns>

        private async Task OnServiceTypeSelected(IDialogContext context, IAwaitable<IMessageActivity> result)

        {

            var message = await result;


            if (message.Text == "예약하기")

            {

                memberReservation = new ReservationModel();

                await this.DesignerListMessageAsync(context, result);

            }

            else if (message.Text == "개인정보변경하기")

            {

                //await context.PostAsync($"사용자 인증을 진행합니다.");

            }

            else if (message.Text == "헤어살롱둘러보기")

            {

                //await this.WelcomeVideoMessageAsync(context, result);

            }

            else

            {

                //await this.StartOverAsync(context, "죄송합니다. 요청사항을 이해하지 못했습니다.^^; ");

            }

        }



using System;

using System.Collections.Generic;

using System.Linq;

using System.Web;


namespace ChatBotApplication.Models

{

    [Serializable]

    public class ReservationModel

    {

        public string MemberName { get; set; }

        public string DesignerName { get; set; }

        public string Date { get; set; }

        public string Time { get; set; }

        public string Telephone { get; set; }

        public string Sample2 { get; set; }

        public string Sample3 { get; set; }

        public string Sample4 { get; set; }

    }

}


12) 미용실 소속 헤어디자이너 목록정보 생성 및 사용자 메시지 보내기

- 헤어 디자이너 정보 메시지를 처리해주는 DesignerListMessageAsync() 메소드를 아래와 같이 구현합니다. 

- 관련 정보는 여러장의 히어로 카드를 목록으로 구성하고 좌우 이동 버튼을 제공하여 히어로 카드가 슬라이드 되는 캐로셀 형태로 제공됩니다. 

- 관련 프로그램을 위해 BeautySalonRootDialog.cs 파일 상단 참조영역에  using System.Collections.Generic; 네임스페이스를 참조추가합니다.


      /// <summary>

        /// 헤어디자이너 캐로셀 목록 메시지 발송

        /// </summary>

        /// <param name="context"></param>

        /// <param name="beforeActivity"></param>

        /// <returns></returns>

        private async Task DesignerListMessageAsync(IDialogContext context, IAwaitable<object> beforeActivity)

        {

            var activity = await beforeActivity as Activity;

            var carouselCards = new List<HeroCard>();


            carouselCards.Add(new HeroCard

            {

                Title = "1.김준오",

                Images = new List<CardImage> { new CardImage("http://chat.beginmate.com/Images/Eelection/Designer1.jpg", "1.김준오") },

                Buttons = new List<CardAction> { new CardAction(ActionTypes.ImBack, "선택하기", value: "1.김준오") }

            });


            carouselCards.Add(new HeroCard

            {

                Title = "2.박승철",

                Images = new List<CardImage> { new CardImage("http://chat.beginmate.com/Images/Eelection/Designer2.jpg", "2.박승철") },

                Buttons = new List<CardAction> { new CardAction(ActionTypes.ImBack, "선택하기", value: "2.박승철") }

            });


            carouselCards.Add(new HeroCard

            {

                Title = "3.권홍",

                Images = new List<CardImage> { new CardImage("http://chat.beginmate.com/Images/Eelection/Designer3.jpg", "3.권홍") },

                Buttons = new List<CardAction> { new CardAction(ActionTypes.ImBack, "선택하기", value: "3.권홍") }

            });


            carouselCards.Add(new HeroCard

            {

                Title = "4.이리안",

                Images = new List<CardImage> { new CardImage("http://chat.beginmate.com/Images/Eelection/Designer4.jpg", "4.이리안") },

                Buttons = new List<CardAction> { new CardAction(ActionTypes.ImBack, "선택하기", value: "4.이리안") }

            });


            var carousel = new PagedCarouselCards

            {

                Cards = carouselCards,

                TotalCount = 4

            };


            var reply = context.MakeMessage();

            reply.AttachmentLayout = AttachmentLayoutTypes.Carousel;

            reply.Attachments = new List<Attachment>();


            foreach (HeroCard productCard in carousel.Cards)

            {

                reply.Attachments.Add(productCard.ToAttachment());

            }


            await context.PostAsync(reply);


    //사용자의 디자이너 선택정보 처리

            context.Wait(this.OnDesignerItemSelected);

        }



13) 헤어 디자이너 목록 제공을 위한 페이징 가능 캐로셀 다이얼로그 생성

-페이징 가능한 캐로셀 다이얼로그 클래스 생성을 위해 프로젝트 Dialogs 폴더에 오른쪽 마우스 클릭 >추가>새항목 >Bot Dialog를 선택 후 

PagedCarouselDialog.cs 를 추가합니다.

- PagedCarouselDialog.cs 클래스 내 모든 코드를 삭제하고 아래 관련코드로 해당 다이얼로그 클래스 전체를 덮어 씁니다.


using System;

using System.Collections.Generic;

using System.Threading.Tasks;

using Microsoft.Bot.Builder.Dialogs;

using Microsoft.Bot.Connector;


namespace ChatBotApplication.Dialogs

{

    [Serializable]

    public abstract class PagedCarouselDialog<T> : IDialog<T>

    {

        private int pageNumber = 1;

        private int pageSize = 5;


        public virtual string Prompt { get; }


        public async Task StartAsync(IDialogContext context)

        {

            await context.PostAsync(this.Prompt ?? "캐로셀");


            await this.ShowProducts(context);


            context.Wait(this.MessageReceivedAsync);

        }


        public abstract PagedCarouselCards GetCarouselCards(int pageNumber, int pageSize);


        public abstract Task ProcessMessageReceived(IDialogContext context, string message);


        protected async Task ShowProducts(IDialogContext context)

        {

            var reply = context.MakeMessage();

            reply.AttachmentLayout = AttachmentLayoutTypes.Carousel;

            reply.Attachments = new List<Attachment>();


            var productsResult = this.GetCarouselCards(this.pageNumber, this.pageSize);

            foreach (HeroCard productCard in productsResult.Cards)

            {

                reply.Attachments.Add(productCard.ToAttachment());

            }


            await context.PostAsync(reply);


            if (productsResult.TotalCount > this.pageNumber * this.pageSize)

            {

                await this.ShowMoreOptions(context);

            }

        }


        protected async Task MessageReceivedAsync(IDialogContext context, IAwaitable<IMessageActivity> result)

        {

            var message = await result;

            if (message.Text.Equals("ShowMe", StringComparison.InvariantCultureIgnoreCase))

            {

                this.pageNumber++;

                await this.StartAsync(context);

            }

            else

            {

                await this.ProcessMessageReceived(context, message.Text);

            }

        }


        private async Task ShowMoreOptions(IDialogContext context)

        {

            var moreOptionsReply = context.MakeMessage();

            moreOptionsReply.Attachments = new List<Attachment>

            {

                    new HeroCard()

                {

                    Text = "MoreOptions",

                    Buttons = new List<CardAction>

                    {

                        new CardAction(ActionTypes.ImBack, "ShowMe", value: "ShowMe")

                    }

                }.ToAttachment()

            };


            await context.PostAsync(moreOptionsReply);

        }

    }


    public class PagedCarouselCards

    {

        public IEnumerable<HeroCard> Cards { get; set; }


        public int TotalCount { get; set; }

    }

}


14) 디버깅을 하고 애물레이터를 통해 지금까지 적용된 시나리오를 확인해보겠습니다.

- Visual Studio에서 F5를 누르고 디버깅을 실시합니다.

- 챗봇 에뮬레이터 오픈하고 챗봇 엔드포인트를 클릭해 챗봇과 연결 후 지금까지의 시나리오가 정상적으로 진행되는지 확인합니다.

- 관련된 메시지가 출력되고 최종적으로 미용실 서비스 메뉴에서 예약하기를 선택하고 디자이너 목록이 나타나고 좌우 이동 버튼이 나오면 성공적으로 관련 시나리오가 진행된것입니다.



15) 헤어 디자이너 선택 예약정보 저장 및 예약가능일자 메시지 발송하기 

-DesignerListMessageAsync() 메소드내의  //context.Wait(this.OnDesignerItemSelected); 사용자가 선택한 헤어디자이너 선택정보를 처리하는 OnDesignerItemSelected() 메소드의 주석을 해제합니다.

-아래와 같이 사용자의 헤어디자이너 선택정보를 처리하는 OnDesignerItemSelected() 메소드를 구현합니다.

-헤어 디자이너 선택버튼을 클릭하거나 이름을 입력하거나 관련번호를 입력해도 정보를 받아 분기처리할수 있게 로직을 처리했습니다.

-사용자가 디자이너를 선택하지 않고 다른 질문의 메시지를 입력 후 전송하면 예외처리를 위한 분기처리를 맨 하단에 추가하였습니다.

await this.StartOverAsync(context, "죄송합니다. 요청사항을 이해하지 못했습니다.^^; ");

-사용자 메시지의 의도 파악이 안되는 경우에 대한 챗봇 메시지 처리를 위해  다음과 같이 추가로 StartOverAsync() 메소드들을 구현해줍니다.

-사용자 메시지 의도 파악이 안되거나 기본 시나리오에서 벗어나는 메시지가 사용자로부터 전달되는 경우에는 사용자에게 정중하게 사용자 의도 분석 실패 메시지를 발송하고 

미용실 서비스 목록 페이지로 흐름을 되돌려 줍니다.


TIP) 사용자 메시지를 통한 좀더 정확한 사용자 의도를 파악하는 방법

-사용자 메시지의 정확한 의도 파악은 퍼블릭 클라우드 업체들에서 제공하는 인공지능의 자연어처리 및 머신러닝등의 기능을 통해 사용자 메시지 기반 사용자 의도를 좀더 정확히 파악하여 

 관련 시나리오에 적용할수 있는 방법이 있습니다. 물론 추가적인 금전적 비용이 발생합니다.


        /// <summary>

        /// 헤어디자이너 선택 처리

        /// </summary>

        /// <param name="context"></param>

        /// <param name="result"></param>

        /// <returns></returns>

        private async Task OnDesignerItemSelected(IDialogContext context, IAwaitable<IMessageActivity> result)

        {

            var message = await result;


//사용자 선택 디자이너 예약정보 저장

            memberReservation.DesignerName = message.Text;


            if (message.Text == "1.김준오" || message.Text == "1")

            {

                await context.PostAsync($"김준호 디자이너를 선택하셨습니다.");

                await this.ReservationDateListMessageAsync(context, result);

            }

            else if (message.Text == "2.박승철" || message.Text == "2")

            {

                await context.PostAsync($"김준호 디자이너를 선택하셨습니다.");

                await this.ReservationDateListMessageAsync(context, result);

            }

            else if (message.Text == "3.권홍!" || message.Text == "3")

            {

                await context.PostAsync($"김준호 디자이너를 선택하셨습니다.");

                await this.ReservationDateListMessageAsync(context, result);

            }

            else if (message.Text == "4.이리안" || message.Text == "4")

            {

                await context.PostAsync($"김준호 디자이너를 선택하셨습니다.");

                await this.ReservationDateListMessageAsync(context, result);

            }

            else

            {

                await this.StartOverAsync(context, "죄송합니다. 요청사항을 이해하지 못했습니다.^^; ");

            }

        }



        /// <summary>

        /// 예외 메시지 처리 

        /// </summary>

        /// <param name="context"></param>

        /// <param name="text"></param>

        /// <returns></returns>

        private async Task StartOverAsync(IDialogContext context, string text)

        {

            var message = context.MakeMessage();

            message.Text = text;

            await this.StartOverAsync(context, message);

        }


        //예외 메시지 처리

        private async Task StartOverAsync(IDialogContext context, IMessageActivity message)

        {

            await context.PostAsync(message);

            await this.ConfirmServiceTypeMessageAsync(context);

        }



16) 담당 헤어 디자이너의 예약가능날짜 정보 제공하기

- 사용자의 헤어디자이너 선택정보를 수신 후 해당 선택 디자이너의 예약가능일자를 알려주는 기능을 ReservationDateListMessageAsync()를 통해 다음과 같이 구현합니다.

- 히어로 카드를 이용해 선택 디자이너 정보와  에약 가능일자 정보를 구성해 사용자 채널로 메시지를 전달합니다.



       /// <summary>

        /// 예약날자 선택하기

        /// </summary>

        /// <param name="context"></param>

        /// <param name="beforeActivity"></param>

        /// <returns></returns>

        private async Task ReservationDateListMessageAsync(IDialogContext context, IAwaitable<object> beforeActivity)

        {

            var activity = await beforeActivity as Activity;

            var reply = context.MakeMessage();


            var options = new[]

            {

                "04월 21일 토요일",

                "04월 22일 일요일",

                "04월 23일 월요일",

                "04월 24일 화요일",

                "04월 25일 수요일",

                "04월 26일 목요일",

            };


            reply.AddHeroCard(activity.Text, "아래 원하시는 에약날짜을 선택해주세요.", options, new[] { "http://chat.beginmate.com/Images/Eelection/hairshop2.jpg" });

            await context.PostAsync(reply);


            //context.Wait(this.ReservationListMessageAsync);

        }



17) 디버깅을 하고 애물레이터를 통해 지금까지 적용된 시나리오를 확인해보겠습니다.

- 사용자가 디자이너를 선택하고 해당 디자이너의 일정이 표시되면 정상 적으로 작동되는것입니다.

- 사용자가 디자이너를 선택하지 않고 시나리오에서 벗어난  메시지를 작성해 전달하면 사용자 의도 파악실패 메시지가 출력되고 서비스 메뉴로 이동하는것을 확인합니다.



18) 사용자 선택 디자이너 예약일자 예약정보 저장 및 예약시간 메시지 발송 

- ReservationDateListMessageAsync()내의 선택예약일자 저장 및 시간선택 메시지 발송 기능인 //context.Wait(this.ReservationListMessageAsync();의 주석을 해제합니다.

- ReservationListMessageAsync()는 사용자 선택 예약일자를  예약정보 객체의 예약일자 속성에 저장하고 예약시간 메시지를 히어로 카드로 구성하여 

사용자에게 메시지를 발송합니다.ReservationListMessageAsync() 메소드를 아래와 같이 구현합니다.


        /// <summary>

        /// 선택 디자이너 예약 가능 시간 알림

        /// </summary>

        /// <param name="context"></param>

        /// <param name="beforeActivity"></param>

        /// <returns></returns>

        private async Task ReservationListMessageAsync(IDialogContext context, IAwaitable<object> beforeActivity)

        {

            var activity = await beforeActivity as Activity;

   

           //사용자 예약일시 정보 저장

            memberReservation.Date = activity.Text;

            var reply = context.MakeMessage();

            var options = new[]

            {

                "오전 11시~12시",

                "오후 1시~2시",

                "오후 4시~5시",

                "이전으로",

            };


            reply.AddHeroCard(activity.Text, "아래 원하시는 에약시간을 선택해주세요.", options, new[] { "http://chat.beginmate.com/Images/Eelection/hairshop2.jpg" });

            await context.PostAsync(reply);

            //context.Wait(this.OnReservationTimeSelected);

        }


19) 사용자 예약시간 저장 및 예약정보 확인 메시지 발송하기

- 상기 예약시간 메시지 발송 ReservationListMessageAsync() 메소드내에  //context.Wait(this.OnReservationTimeSelected); 주석을 해제하고 

사용자가 선택한 예약시간정보  저장 및 예약정보 확인 OnReservationTimeSelected() 메소드를 아래와 같이 구현합니다.


        /// <summary>

        /// 예약시간 선택완료

        /// </summary>

        /// <param name="context"></param>

        /// <param name="result"></param>

        /// <returns></returns>

        private async Task OnReservationTimeSelected(IDialogContext context, IAwaitable<IMessageActivity> result)

        {

            var message = await result;

            memberReservation.Time = message.Text;


            if (message.Text == "오전 11시~12시" || message.Text == "1")

            {

                await context.PostAsync($" { memberReservation.DesignerName}디자이너로 { memberReservation.Date}에 { memberReservation.Time}에 예약하셨습니다.\n\n 성함을 입력해주세요.\n\n ");

                context.Wait(this.GetUserNameAsync);

            }

            else if (message.Text == "오후 1시~2시" || message.Text == "2")

            {

                await context.PostAsync($" { memberReservation.DesignerName}디자이너로 { memberReservation.Date}에 { memberReservation.Time}에 예약하셨습니다.\n\n 성함을 입력해주세요.\n\n ");

                context.Wait(this.GetUserNameAsync);

            }

            else if (message.Text == "오후 4시~5시" || message.Text == "3")

            {

                await context.PostAsync($" { memberReservation.DesignerName}디자이너로 { memberReservation.Date}에 { memberReservation.Time}에 예약하셨습니다.\n\n 성함을 입력해주세요.\n\n  ");

                context.Wait(this.GetUserNameAsync);

            }

            else if (message.Text == "이전으로")

            {

                await this.ReservationListMessageAsync(context, result);

            }

            else

            {

                await this.StartOverAsync(context, "죄송합니다. 요청사항을 이해하지 못했습니다.^^; ");

            }

        }


20) 사용자 입력 예약자명 저장 및 전화번호 요청 메시지 발송

- 사용자로부터 예약자명을 전달받아 예약정보객체에 저장하고 예약자 전화번호를 요청합니다.


      /// <summary>

        /// 이름받기

        /// </summary>

        /// <param name="context"></param>

        /// <param name="result"></param>

        /// <returns></returns>

        private async Task GetUserNameAsync(IDialogContext context, IAwaitable<object> result)

        {

            var activity = await result as Activity;

            memberReservation.MemberName = activity.Text;


            await context.PostAsync($"전화번호를 입력해주세요.");

            context.Wait(this.GetUserTelephoneAsync);

        }



21) 사용자 입력 전화번호 저장 및 예약완료 메시지 발송

- 사용자로부터 예약자 전화번호를을 전달받아 예약정보객체에 저장하고 예약작업을 완료합니다.

- 예약 작업 완료 후 미용실 서비스메뉴 제공 시나리오로 이동합니다.

        /// <summary>

        /// 전화번호 받기

        /// </summary>

        /// <param name="context"></param>

        /// <param name="result"></param>

        /// <returns></returns>

        private async Task GetUserTelephoneAsync(IDialogContext context, IAwaitable<IMessageActivity> result)

        {

            var activity = await result as Activity;

            memberReservation.Telephone = activity.Text;


            await context.PostAsync($"감사합니다. 예약이 완료되었습니다.\n\n ");

            await this.ConfirmServiceTypeMessageAsync(context);  

        }



21) 미용실 서비스 메뉴 시나리오로 돌아가 분기된 다른 시나리오의 주석을 해제합니다.

- 사용자 서비스 선택처리 메소드인 OnServiceTypeSelected() 내용을 확인합니다.

- 개인정보변경하기 분기문의 주석을 해제합니다.

- 사용자 인증을 위한 메시지를 출력한 후 인증시나리오가 있다면 코드를 구현합니다.

- 사용자 의도 파악 실패 분기문의 주석도 해제합니다. //await this.StartOverAsync(context, "죄송합니다. 요청사항을 이해하지 못했습니다.^^; ");


22) 미용실 서비스 메뉴의 미용실 소개 메시지 처리부분인 헤어살롱 둘러보기 기능을 마지막으로 구현합니다.

- 사용자 서비스 선택처리 메소드인 OnServiceTypeSelected() 내용을 확인합니다.

- 헤어살롱 둘러보기 분기문의 주석을 해제하고 WelcomeVideoMessageAsync()메소드를 아래 코드와 같이 구현합니다.

- 미용실 소개는 동영상 포맷으로 미용실 내부를 소개하는 시나리오로 제공됩니다.


        /// <summary>

        /// 미용실 소개 동영상 메시지 처리

        /// </summary>

        /// <param name="context"></param>

        /// <param name="result"></param>

        /// <returns></returns>

        private async Task WelcomeVideoMessageAsync(IDialogContext context, IAwaitable<object> beforeActivity)

        {

            var reply = context.MakeMessage();


            var videoCard = new VideoCard

            {

                Title = "비긴메이트 헤어살롱",

                Subtitle = "스타트업 종사분들만 모시는 스타트업 O2O 헤어살롱입니다.\n\n  커피공짜,사무실 공짜, 언제든 머리고 복잡할때 찾아주세요.\n\n 시원하게 밀어드립니다.",

                Text = "",

                Image = new ThumbnailUrl

                {

                    Url = "https://upload.wikimedia.org/wikipedia/commons/thumb/c/c5/Big_buck_bunny_poster_big.jpg/220px-Big_buck_bunny_poster_big.jpg"

                },

                Media = new List<MediaUrl>

                {

                    new MediaUrl()

                    {

                        Url = "http://download.blender.org/peach/bigbuckbunny_movies/BigBuckBunny_320x180.mp4"

                    }

                },

                Buttons = new List<CardAction>

                {

                    new CardAction()

                    {

                        Title = "자세히 보기",

                        Type = ActionTypes.OpenUrl,

                        Value = "http://www.beginmate.com"

                    },

                    new CardAction()

                    {

                        Title = "이전으로",

                        Type = ActionTypes.ImBack,

                        Value = "이전으로"

                    }

                }

            };


            reply.Attachments.Add(videoCard.ToAttachment());

            await context.PostAsync(reply);

            context.Wait(this.OnWelcomMsgSelected);

        }



        private async Task OnWelcomMsgSelected(IDialogContext context, IAwaitable<IMessageActivity> result)

        {

            var message = await result;

            await this.StartOverAsync(context, "감사합니다. 서비스 목록으로 이동합니다.");

        }



23) 디버깅을 하고 애물레이터를 통해 지금까지 적용된 시나리오를 확인해봅니다.



*