코딩 교육 TIL

2024-05-22 AI 코딩 TIL

HyunjunPark 2024. 5. 22. 20:14
오늘의 프로잭트 진행도
  • 숙제: 멘토링 결과 다음 주까지 해올 일
    • 어드민 페이지 커스텀
    • 음성으로 메뉴 선택까지 같이 할 수 있는 기능 추가?
    • AI추천 음료 하이라이트
    • 실버를 타겟으로 하는 만큼 더 확실하게 컨셉을 가져갈 수 있는 프론트엔드를 구성해보기 : 현재 MTV를 사용하기 때문에 차별점이 필요하다
      • AI가 추천한 음료를 제일 크게 보여주고, 해시태그로 가져온 다른 관련 음료들은 작게 구성해주기
      • 요소들의 글씨, 버튼 등을 더 크게 구성하고 상대적으로 심플한 UI를 구성해보기

이제 기본적인 기능이 조금씩 다듬어가면서 만들어지고 있습니다.

예상했던 부분이 살살 만들어지면서 모형을 갖추어 가니 기분이 좋군요 ㅎㅎ

진척도

음성인식 Templates - 기능 보완

음성인식을 이용해서 음료 추천 및 주문을 받는 형식

  • 코드
</head>
<body>
<div class="container mt-5">
    <h2 class="text-center mb-4">Silver Lining</h2>
    <h2>음성 입력하기</h2>
    <form id="speechForm">
        {% csrf_token %}
        <button type="button" id="startButton">음성 입력 시작</button>
    </form>
    <p id="transcription"></p>

    <div class="button d-flex flex-wrap justify-content-center" id="menuContainer">
        {% for menu in menus %}
            <div class="menu-item card" onclick="addItem('{{ menu.food_name }}', {{ menu.price }}, '{{ menu.img.url }}', this)">
                <img src="{% if menu.img %}{{ menu.img.url }}{% endif %}" alt="{{ menu.food_name }}" class="card-img-top">
                <div class="card-body text-center">
                    <h5 class="card-title text-primary">{{ menu.food_name }}</h5>
                    <p class="card-text text-muted">{{ menu.price }}원</p>
                </div>
            </div>
        {% endfor %}
    </div>

    <div class="selected-items mt-4">
        <h3 class="text-center">선택한 상품</h3>
        <div id="selectedItemsList"></div>
    </div>
    <div class="total-price mt-3">
        <h3 class="text-center">총 금액: <span id="totalPrice">0원</span></h3>
    </div>
    <div class="actions text-center">
        <button class="btn btn-danger" onclick="clearItems()">전체삭제</button>
        <button class="btn btn-success" id="submitOrderBtn">결제하기</button>
    </div>
</div>

<script>
    window.addEventListener('load', function () {
        const welcomeMessage = "반갑습니다. 원하시는 메뉴를 추천해 드리겠습니다. 필요한 것이 있다면 말씀해주세요.";
        speak(welcomeMessage, startSpeechRecognition);
    });

    const startButton = document.getElementById('startButton');
    const transcription = document.getElementById('transcription');

    function getCsrfToken() {
        const csrfTokenElement = document.querySelector('input[name="csrfmiddlewaretoken"]');
        if (csrfTokenElement) {
            return csrfTokenElement.value;
        } else {
            console.error('CSRF 토큰을 찾을 수 없습니다.');
            return null;
        }
    }

    function speak(text, callback) {
        const synth = window.speechSynthesis;
        const utterance = new SpeechSynthesisUtterance(text);
        utterance.lang = 'ko-KR';
        utterance.onend = function () {
            console.log("음성 안내가 끝났습니다.");
            if (callback) {
                callback();
            }
        };
        synth.speak(utterance);
    }

    function startSpeechRecognition() {
        if (!('webkitSpeechRecognition' in window)) {
            alert("음성 인식이 지원되지 않는 브라우저입니다.");
        } else {
            const recognition = new webkitSpeechRecognition();
            recognition.lang = 'ko-KR';
            recognition.start();
            recognition.onresult = function (event) {
                const transcript = event.results[0][0].transcript;
                transcription.textContent = transcript;
                const csrfToken = getCsrfToken();
                axios.post('{% url "orders:aibot" %}', {inputText: transcript}, {
                    headers: {
                        'X-CSRFToken': csrfToken
                    }
                })
                .then(function (response) {
                    const responseText = response.data.responseText;
                    const hashtags = response.data.hashtags;
                    console.log('서버 응답:', responseText);
                    speak(responseText, function() {
                        updateMenus(hashtags);
                    });
                })
                .catch(function (error) {
                    console.error('에러:', error);
                });
            };
            recognition.onend = function () {
                startButton.textContent = '음성 입력 다시 시작';
            };
        }
    }

추가

  • 메뉴를 추천해주고 메뉴를 고른다음 추가 메뉴를 담지 못함
  • 노인분들의 맞춤으로 편하게 볼 수 있는 템플릿 필요 (크기 확장)

데이터 베이스 - 템플릿 메뉴 변경 및 저장

오류 : 처음 orders/menu/로 들어가면 현재 매장에서 나오는 모든 메뉴가 나올 수 있도록 설정이 되어있고 음성인식으로 나오는 hashtag에 맞추어 해당하는 메뉴를 새로 가져온다음 js를 통하여 새로 메뉴를 보여주는 단계에서 데이터 베이스 조회가 안되어 오류가 발생 및 탬플릿이 새로 고침 되어 음성안내가 다시 나오는 문제 발생

문제 해결 : 해시태그로 역참조를 하여 목록을 가져오고 json으로 데이터 값을 template로 넘겨주어 새로고침이 없이 바로 데이터를 가져와서 메뉴를 새로 만들어주는 단계로 변경하여 음성안내 및 메뉴 변경이 가능 하도록 수정 되었습니다.

  • 코드
<script>
    window.addEventListener('load', function () {
        const welcomeMessage = "반갑습니다. 원하시는 메뉴를 추천해 드리겠습니다. 필요한 것이 있다면 말씀해주세요.";
        speak(welcomeMessage, startSpeechRecognition);
    });

    const startButton = document.getElementById('startButton');
    const transcription = document.getElementById('transcription');

    function getCsrfToken() {
        const csrfTokenElement = document.querySelector('input[name="csrfmiddlewaretoken"]');
        if (csrfTokenElement) {
            return csrfTokenElement.value;
        } else {
            console.error('CSRF 토큰을 찾을 수 없습니다.');
            return null;
        }
    }

    function speak(text, callback) {
        const synth = window.speechSynthesis;
        const utterance = new SpeechSynthesisUtterance(text);
        utterance.lang = 'ko-KR';
        utterance.onend = function () {
            console.log("음성 안내가 끝났습니다.");
            if (callback) {
                callback();
            }
        };
        synth.speak(utterance);
    }

    function startSpeechRecognition() {
        if (!('webkitSpeechRecognition' in window)) {
            alert("음성 인식이 지원되지 않는 브라우저입니다.");
        } else {
            const recognition = new webkitSpeechRecognition();
            recognition.lang = 'ko-KR';
            recognition.start();
            recognition.onresult = function (event) {
                const transcript = event.results[0][0].transcript;
                transcription.textContent = transcript;
                const csrfToken = getCsrfToken();
                axios.post('{% url "orders:aibot" %}', {inputText: transcript}, {
                    headers: {
                        'X-CSRFToken': csrfToken
                    }
                })
                .then(function (response) {
                    const responseText = response.data.responseText;
                    const hashtags = response.data.hashtags;
                    console.log('서버 응답:', responseText);
                    speak(responseText, function() {
                        updateMenus(hashtags);
                    });
                })
                .catch(function (error) {
                    console.error('에러:', error);
                });
            };
            recognition.onend = function () {
                startButton.textContent = '음성 입력 다시 시작';
            };
        }
    }

    function updateMenus(hashtags) {
        $.ajax({
            url: '/orders/get_menus/',
            data: {hashtags: hashtags},
            dataType: 'json',
            success: function (data) {
                const menus = data.menus;
                const menuContainer = $('#menuContainer');
                menuContainer.empty();
                menus.forEach(menu => {
                    const menuItem = `
                        <div class="menu-item card" onclick="addItem('${menu.food_name}', ${menu.price}, '${menu.img_url}', this)">
                            <img src="${menu.img_url}" alt="${menu.food_name}" class="card-img-top">
                            <div class="card-body text-center">
                                <h5 class="card-title text-primary">${menu.food_name}</h5>
                                <p class="card-text text-muted">${menu.price}원</p>
                            </div>
                        </div>
                    `;
                    menuContainer.append(menuItem);
                });
            },
            error: function (error) {
                console.error('메뉴 업데이트 중 오류 발생:', error);
            }
        });
    }

    const selectedItems = {};

    function addItem(name, price, imgUrl, element) {
        if (!selectedItems[name]) {
            selectedItems[name] = {price: price, count: 1, imgUrl: imgUrl};
        } else {
            selectedItems[name].count += 1;
        }
        updateSelectedItemsList();
        flyToCart(element, document.getElementById('selectedItemsList'));
    }

    function updateSelectedItemsList() {
        const selectedItemsList = document.getElementById('selectedItemsList');
        selectedItemsList.innerHTML = '';
        let totalPrice = 0;
        for (const [name, item] of Object.entries(selectedItems)) {
            const itemElement = document.createElement('div');
            itemElement.classList.add('selected-item');
            itemElement.innerHTML = `
                <img src="${item.imgUrl}" alt="${name}">
                <span>${name}</span>
                <span>${item.price}원</span>
                <span>${item.count}개</span>
                <button class="btn btn-danger btn-sm" onclick="removeItem('${name}')">삭제</button>
            `;
            selectedItemsList.appendChild(itemElement);
            totalPrice += item.price * item.count;
        }
        document.getElementById('totalPrice').textContent = `${totalPrice}원`;
    }

    function removeItem(name) {
        if (selectedItems[name]) {
            delete selectedItems[name];
            updateSelectedItemsList();
        }
    }

    function clearItems() {
        for (const key in selectedItems) {
            delete selectedItems[key];
        }
        updateSelectedItemsList();
    }

    function flyToCart(element, targetElement) {
        const imgToDrag = element.querySelector("img");
        if (imgToDrag) {
            const imgClone = imgToDrag.cloneNode(true);
            const rect = imgToDrag.getBoundingClientRect();
            imgClone.style.position = 'absolute';
            imgClone.style.top = rect.top + 'px';
            imgClone.style.left = rect.left + 'px';
            imgClone.style.width = '100px';
            imgClone.style.height = '100px';
            imgClone.classList.add('fly-to-cart');
            document.body.appendChild(imgClone);

            const targetRect = targetElement.getBoundingClientRect();
            setTimeout(() => {
                imgClone.style.transform = `translate(${targetRect.left - rect.left}px, ${targetRect.top - rect.top}px) scale(0.5)`;
            }, 10);

            setTimeout(() => {
                imgClone.remove();
            }, 1000);
        }
    }

    document.getElementById('submitOrderBtn').addEventListener('click', function () {
        const selectedItemsArray = Object.entries(selectedItems).map(([name, item]) => {
            return {name: name, count: item.count};
        });

        const totalPrice = calculateTotalPrice(selectedItems);

        $.ajax({
            url: '/orders/submit_order/',
            cache: false,
            dataType: 'json',
            type: 'POST',
            contentType: 'application/json',
            data: JSON.stringify({items: selectedItemsArray, total_price: totalPrice}),
            beforeSend: function (xhr) {
                xhr.setRequestHeader('X-CSRFToken', $.cookie('csrftoken'));
            },
            success: function (data) {
                console.log('주문이 성공적으로 처리되었습니다.');
                window.location.href = '/orders/order_complete/' + data.order_number + '/';
            },
            error: function (error) {
                console.error('주문 처리 중 오류가 발생했습니다:', error);
            }
        });
    });

    function calculateTotalPrice(selectedItems) {
        let totalPrice = 0;
        for (const item of Object.values(selectedItems)) {
            totalPrice += item.price * item.count;
        }
        return totalPrice;
    }
</script>

모델 추가 - Menu 모델 img컬럼 생성 + 버튼효과

기존 menu의 model상에는 img를 넣을 수가 없어 model을 추가하고 admin page에서 이미지를 추가 할 수 있으며, 생성으로 인하여 추가된 이미지는 media파일에 저장이 되어 template에서 image.url를 통하여 이미지를 불러 올 수 있으며, 각 메뉴이름에 맞추어 이미지가 함께 나올 수 있도록 하였습니다

  • 추가 사항 :

프론트 엔드상의 키오스화면의 버튼 효과를 넣었으며 해당음료를 클릭하면 음료가 장바구니에 담기는 이모션이 들어감 ( 현재 장바구니 위치에 맞게 들어가지 않는 문제가 발생하여 수정 필요. )