오류 개선 - menu.html
오류 : Unresolved attribute reference 'any' for class 'Sequence’
수정 전
if faces.any():
print("faces>>>>>>>", faces)
break
문제의 원인은 faces가 비어있는지 확인하는 방법에서 발생합니다. faces는 Numpy 배열이므로 faces.any() 메서드는 사용할 수 없습니다. 대신 faces가 비어있는지 확인하기 위해 Numpy 배열의 크기를 확인하는 방법을 사용해야 합니다.
수정 후
if len(faces) > 0:
print("faces>>>>>>>", faces)
break
탬플릿 메뉴 추천 후 음성 안내 ( 완료 )
음성안내 후 다시 음성 받을 준비 ( 완료 )
→ 마이크 입력을 계속해서 넣지 않으면 마이크가 꺼져버림 → 고연령 ( 마이크입력시간 길게 + 필요 없는 말은 필터링해서 메뉴 반영 x ) → 저연령 ( 질문 마스코트를 만들어서 버튼식으로 입력을 받기 )
메뉴에 담기는 모션 수정 필요
얼굴인식 후에 마이크 입력 단에 오류가 발생 수정 必
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Silver Lining</title>
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" rel="stylesheet">
<style>
/* 기존 스타일 */
.menu-item,
.selected-item {
display: flex;
justify-content: center;
align-items: center;
padding: 10px;
margin: 10px;
border: 1px solid #ddd;
border-radius: 5px;
flex-direction: column;
transition: transform 0.2s;
}
.menu-item img {
width: 250px;
height: 250px;
}
.menu-item:hover {
transform: scale(1.05);
}
.selected-item span {
margin: 0 5px;
}
.actions button {
margin: 5px;
}
.card-body {
text-align: center;
}
.fly-to-cart {
position: absolute;
z-index: 1000;
transition: transform 1s ease-in-out;
}
#selectedItemsList {
display: flex;
flex-direction: column;
gap: 10px;
align-items: center;
}
.selected-item {
display: flex;
align-items: center;
padding: 10px;
border: 1px solid #ddd;
border-radius: 5px;
flex-direction: row;
width: 600px;
margin: 5px 0;
}
.selected-item img {
width: 50px;
height: 50px;
margin-right: 10px;
}
</style>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-cookie/1.4.1/jquery.cookie.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
</head>
<body>
<div class="container mt-5">
<h2 class="text-center mb-4">Silver Lining</h2>
<form id="speechForm">
{% csrf_token %}
</form>
<p id="transcription"></p>
<div class="button d-flex flex-wrap justify-content-center" id="menuContainer">
</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 hashtags = "";
updateMenus(hashtags)
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);
updateMenus(hashtags); // 메뉴 업데이트 먼저 실행
speak(responseText, startSpeechRecognition); // 그 다음에 음성 안내 실행
})
.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>
<script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.5.4/dist/umd/popper.min.js"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script>
</body>
</html>
페이지네이션 - javascript로 변환
Template를 가져올때 javascript로 가져올 수 있도록 변환
기존에 음료 종류가 많아서 한 페이지에 담기 어려움이 있어, 버튼 형식의 1,2 페이지로 나누어서 적용
function updateMenus(hashtags, page = 1) {
$.ajax({
url: '/orders/get_menus/',
data: {hashtags: hashtags, page: page},
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);
});
console.log(data.page_count)
updatePaginationButtons(data.page_count, page);
},
error: function (error) {
console.error('메뉴 업데이트 중 오류 발생:', error);
}
});
}
function updatePaginationButtons(totalPages, currentPage) {
const paginationButtons = $('#paginationButtons');
paginationButtons.empty();
if (currentPage > 1) {
const prevButton = `<button class="btn btn-outline-primary mr-1" onclick="changePage(${currentPage - 1})">이전</button>`;
paginationButtons.append(prevButton);
}
for (let i = 1; i <= totalPages; i++) {
const button = `<button class="btn btn-outline-primary mr-1" onclick="changePage(${i})">${i}</button>`;
paginationButtons.append(button);
}
if (currentPage < totalPages) {
const nextButton = `<button class="btn btn-outline-primary" onclick="changePage(${currentPage + 1})">다음</button>`;
paginationButtons.append(nextButton);
}
}
function changePage(pageNumber) {
const hashtags = "";
updateMenus(hashtags, pageNumber);
}
def get_menus(request):
hashtags = request.GET.get('hashtags', None)
if hashtags:
menus = Menu.objects.filter(hashtags__hashtag=hashtags)
else:
menus = Menu.objects.all()
# 페이지네이션 설정
paginator = Paginator(menus, 9) # 페이지 당 9개의 메뉴
page_number = request.GET.get('page')
try:
menus = paginator.page(page_number)
except PageNotAnInteger:
# 페이지 번호가 정수가 아닌 경우, 첫 번째 페이지를 반환
menus = paginator.page(1)
except EmptyPage:
# 페이지가 비어있는 경우, 마지막 페이지를 반환
menus = paginator.page(paginator.num_pages)
menu_list = [
{
'food_name': menu.food_name,
'price': menu.price,
'img_url': menu.img.url if menu.img else ''
} for menu in menus
]
# 총 페이지 수 계산
total_pages = paginator.num_pages
return JsonResponse({'menus': menu_list, 'page_count': total_pages})
음성AI - 버튼이 없어도 음성호출
- 버튼 형식의 음성인식 대신, 음성만 호출
- 기존에는 AI가 추천음료를 말한다음, template상 음료가 변경되었다면, 추천 음료를 먼저 보여주고 설명해 주는 AI형식으로 개선
- 기존 한번의 음성인식 기능으로 한 번의 주문만 받았다면, 여러 번의 음성대화형식으로 나오게끔 개선
<div class="container mt-5">
<h2 class="text-center mb-4">Silver Lining</h2>
<form id="speechForm">
{% csrf_token %}
</form>
<p id="transcription"></p>
<div class="button d-flex flex-wrap justify-content-center" id="menuContainer">
</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 hashtags = "";
updateMenus(hashtags)
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);
updateMenus(hashtags); // 메뉴 업데이트 먼저 실행
speak(responseText, startSpeechRecognition); // 그 다음에 음성 안내 실행
})
.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 flyToCart(element, targetElement) {
const imgToDrag = element.querySelector("img");
if (imgToDrag) {
const imgClone = imgToDrag.cloneNode(true);
let rect = imgToDrag.getBoundingClientRect();
imgClone.style.position = 'absolute';
imgClone.style.top = rect.top + 'px';
imgClone.style.left = rect.left + 'px';
imgClone.style.width = '250px'; // 초기 이미지 크기
imgClone.style.height = '250px'; // 초기 이미지 크기
imgClone.classList.add('fly-to-cart');
document.body.appendChild(imgClone);
// 🛒 아이콘 위치 설정
const cartIconRect = targetElement.getBoundingClientRect();
// 카트 아이콘 중앙 위치 계산
const cartCenterX = cartIconRect.left + cartIconRect.width / 2;
const cartCenterY = cartIconRect.top + cartIconRect.height / 2;
// 이미지 이동 속도 계산
const dx = (cartCenterX - rect.left) / 120; // x 방향 이동 속도
const dy = (cartCenterY - rect.top) / 120; // y 방향 이동 속도
// 이미지 크기 감소 속도 계산
const dw = (250 - 100) / 120; // 이미지 크기 감소 속도
// 이미지 이동 및 크기 조절 함수
function moveImage() {
rect = imgClone.getBoundingClientRect();
if ((dx > 0 && rect.left < cartCenterX) || (dx < 0 && rect.left > cartCenterX) ||
(dy > 0 && rect.top < cartCenterY) || (dy < 0 && rect.top > cartCenterY)) {
imgClone.style.left = (rect.left + dx) + 'px';
imgClone.style.top = (rect.top + dy) + 'px';
// 이미지 크기 조절
const newWidth = parseFloat(imgClone.style.width) - dw;
imgClone.style.width = newWidth + 'px';
imgClone.style.height = newWidth + 'px';
requestAnimationFrame(moveImage);
} else {
imgClone.remove();
}
}
// 이미지 이동 시작
moveImage();
}
}
주문하기 - 얼굴인식 안내
주문하기를 누르면, 얼굴이 인식되기까지의 시간동안 로딩중과 같은 문구를 적용
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Order Page</title>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
<style>
body {
font-family: Arial, sans-serif;
text-align: center;
background-color: #f8f9fa;
}
h1 {
color: #ef4040;
margin-top: 70px;
font-size: 70px;
}
p {
font-size: 45px;
margin: 20px;
}
.button-container {
display: flex;
flex-direction: column;
align-items: center;
margin-top: 70px;
}
button {
width: 700px;
height: 600px;
font-size: 120px;
font-weight: bold;
padding: 40px 80px;
color: white;
background-color: #ef4040;
border: none;
border-radius: 20px;
cursor: pointer;
}
button:hover {
background-color: #d93636;
}
.spinner-container {
display: none;
}
.status {
font-size: 50px;
margin-top: 100px;
display: none;
}
</style>
</head>
<body>
<h1>여기에서 주문하세요!</h1>
<p>화면을 터치해 주세요</p>
<div class="button-container">
<form id="order-form" method="post" action="{% url 'orders:face_recognition' %}">
{% csrf_token %}
<button type="submit" id="order-button">주문하기</button>
<div class="spinner-container" id="spinner">
<div class="spinner-border" style="width: 15rem; height: 15rem;" role="status">
<span class="visually-hidden"></span>
</div>
</div>
<div class="status" id="status">얼굴 인식을 진행하고 있습니다</div>
</form>
</div>
<script>
document.getElementById("order-form").addEventListener("submit", function () {
document.getElementById("spinner").style.display = "block";
document.getElementById("status").style.display = "block";
document.getElementById("order-button").style.display = "none";
});
</script>
</body>
</html>
주문 페이지 - 크기 수정 / Pagination
고연령층도 간편하고 쉽게 찾을 수 있게, 글자 크기와 버튼 크기 수정 (세로기준)
/* 기존 스타일 */
body {
font-size: 2rem;
}
.h2, h2 {
font-size: 4rem;
background-color: #ef4040;
color: #f8f9fa;
padding: 20px;
}
.h3, h3 {
font-size: 2.75rem;
}
.h5, h5 {
font-size: 1.5rem;
}
.menu-item {
display: flex;
justify-content: center;
align-items: center;
padding: 10px;
margin: 10px;
border: 1px solid #ddd;
border-radius: 5px;
flex-direction: column;
transition: transform 0.2s;
width: 280px;
}
→ 추가 문제 : 장바구니에 담긴 상품들의 종류의 양이 많을 시, 페이지를 넘어버리는 점 (장바구니의 스크롤을 만든다.)
<style>
#selectedItemsList {
height: 350px;
overflow-y: auto;
flex-grow: 1;
padding: 10px;
border-radius: 5px;
}
.scroll-button {
margin: 5px 0;
height: 155px;
width: 50px;
display: flex;
justify-content: center;
align-items: center;
}
.selected-item {
display: flex;
align-items: center;
padding: 10px;
border: 1px solid #ddd;
border-radius: 5px;
flex-direction: row;
width: 850px;
margin: 5px 0;
justify-content: space-between;
}
.btn-custom {
padding: 30px 95px;
font-size: 3.5rem;
}
.btn-page {
padding: .375rem 1.75rem;
font-size: 3rem;
}
.scroll-button {
margin: 10px 0;
padding: 10px 20px;
font-size: 1.5rem;
}
.selected-item img {
width: 50px;
height: 50px;
margin-right: 10px;
}
</style>
<body>
<div class="pagination justify-content-center" id="paginationButtons">
</div>
<h3 class="text-center">선택한 상품 🛒</h3>
<div class="selected-items mt-4 d-flex">
<div id="selectedItemsList" class="flex-grow-1"></div>
<div class="d-flex flex-column justify-content-center align-items-center">
<button class="btn btn-primary scroll-button mb-2" onclick="scrollSelectedItemsList(-100)">
<i class="fas fa-arrow-up"></i>
</button>
<button class="btn btn-primary scroll-button" onclick="scrollSelectedItemsList(100)">
<i class="fas fa-arrow-down"></i>
</button>
</div>
</div>
</body>
'개발 일지' 카테고리의 다른 글
키오스크 - DRF로 변환 (1) | 2024.05.28 |
---|---|
AI 프롬프트 - 추천 메뉴 설정 방식 변경 (0) | 2024.05.27 |
음성인식 Templates - 기능 보완 (0) | 2024.05.23 |
프론트 엔드 - 키오스크 탬플릿 작성 (1) | 2024.05.22 |
프레임워크 - Django app, model 구현 (1) | 2024.05.21 |