오늘의 프로잭트 진행도
- 숙제: 멘토링 결과 다음 주까지 해올 일
- 어드민 페이지 커스텀
- 음성으로 메뉴 선택까지 같이 할 수 있는 기능 추가?
- 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를 통하여 이미지를 불러 올 수 있으며, 각 메뉴이름에 맞추어 이미지가 함께 나올 수 있도록 하였습니다
- 추가 사항 :
프론트 엔드상의 키오스화면의 버튼 효과를 넣었으며 해당음료를 클릭하면 음료가 장바구니에 담기는 이모션이 들어감 ( 현재 장바구니 위치에 맞게 들어가지 않는 문제가 발생하여 수정 필요. )
'AI 코딩 교육 TIL' 카테고리의 다른 글
2024-05-24 AI 코딩 TI (0) | 2024.05.24 |
---|---|
2024-05-23 AI 코딩 TI (0) | 2024.05.23 |
2024-05-21 AI 코딩 TIL (0) | 2024.05.21 |
2024-05-17 AI 코딩 TIL (0) | 2024.05.17 |
2024-05-16 AI 코딩 TIL (0) | 2024.05.16 |