
이 프로젝트의 마지막 단계인 음식점 추천 부분에 대한 코드만 남은 상태여서 이 추천 시스템을 어떤식으로 작동하게 할지에 대한 고민을 상당히 많이 하게됐다.
처음 프로젝트 구상단계에서 네이버 지도나, 카카오 맵에서 정보를 얻어서 TOP 5를 제공한다고 했었다.
그런데 몇가지 걸리는 점이 있었다.
이런 고민을 하다보니 한가지 의문점이 들었다.
그냥 키워드 기반 검색으로 한다고 가정했을 때, 아래와 같은 흐름일 것이다.
20대 남성과 20대 여성 예측 -> 커플 -> "분위기 좋은 맛집" -> 키워드 검색
그런데 과연 이게, 그냥 네이버 지도 앱을 켜서 분위기 좋은 맛집을 검색하는 것보다 좋은 점이 한가지로 있는가?
아무리 생각해봐도 너무 의미가 없는 거 같다고 생각했다.
단 하나, 이런 저런 기술들을 접목해보고 연습 해봤다는 그런 의미밖에 없다고 생각했다.
그래서 결론적으로 목표를 바꾸자고 마음먹었다.
지금 성별과 연령대 예측 기능까지는 만들어져있다.
이를 잘 활용한다면 의미있게 사용할만하다고 느꼈다.
그렇게 결정하된 시스템은,

아메리카노 200잔
카페라떼 150잔
...
녹차라떼 7잔 이처럼 메뉴판 순서 자동 최적화, 가장 잘 팔리는 메뉴 상단 배치, 연령+시간대 조합별 베스트메뉴 분석 및 추천 메뉴 배치 등 다양하게 매장 마케팅에 응용이 가능해보인다고 생각했다.
그래서 전체적으로 보면 웹앱을 하나 만들 생각이다.
0. 접속 후
1. 예를들어 기록시작, 기록중지, 분석 이라는 세가지 버튼을 두고
2. 영업 시작과 동시에 기록시작 버튼을 통해 실시간으로 감지하며 DB에 기록을 시작한다.
3. 하루 영업이 마무리된다면 기록중지를 버튼을 통해 기록을 중지한다.
4. 통계량 등 비교 분석을 할때는 분석 버튼을 통해 확인한다.
이런 흐름을 구상하고 있다.
따라서 가장 먼저 데이터베이스부터 세팅을 진행해보자.
SQLite DB Browser를 이용해서 진행해보고자 한다.

다운받고나서 DB Browser for SQLite.exe 를 실행한다.

이후 새 데이터베이스를 클릭하고서 작업 중인 vscode 루트에 database 폴더를 만들고 visitor_logs.db 파일을 생성한다.
생성을 완료하고 나면 테이블을 정의해줘야 하는데 GUI툴을 이용하면 CREATE TABLE을 통해 Field 들을 직접 작성하지 않고 클릭 몇번 만으로도 편하게 할 수 있다.

id: 주문 식별자 (PK + 자동 증가)timestamp: 일시 (YYYY-MM-DD HH:MM:SS)gender: 성별age_group: 연령대menu_name: 메뉴명quantity: 수량이렇게 생성완료했다면 이제 vscode에서 test폴더를 만들고 insert가 잘 되는 지 확인해보자.
# DB 입력 테스트
import os
import sqlite3
from datetime import datetime
base_dir=os.path.dirname(os.path.abspath(__file__))
db_path=os.path.join(base_dir, "../database/visitor_logs.db")
conn=sqlite3.connect(db_path)
cursor=conn.cursor()
# 테스트 데이터
timestamp=datetime.now().strftime("%Y-%m-%d %H:%M:%S")
gender="male"
age_group="20s"
menu_name="아메리카노"
quantity=2
# INSERT 문
cursor.execute("""
INSERT INTO visitor_logs (timestamp, gender, age_group, menu_name, quantity)
VALUES (?, ?, ?, ?, ?)
""", (timestamp, gender, age_group, menu_name, quantity))
# 저장 및 종료
conn.commit()
conn.close()
print("입력완료")
입력완료
이후 DB Browser에서 쿼리문을 던져보면 다음과 같이 나온다.

자 이제 흐름을 정리하자면 다음과 같다.
따라서 카메라 연동과 예측 결과 DB 저장을 연결하는 작업부터 진행하면 된다.
그 전에 구조부터 조금 손보자.
📦ADFOU
┣ 📂data
┃ ┣ 📂processed
┃ ┗ 📂raw
┣ 📂notebooks
┃ ┣ 📂data_preprocessing
┃ ┣ 📂model_pred
┃ ┗ 📂model_training
┣ 📂src
┃ ┣ 📂models
┃ ┃ ┗ 📜09_efficientnetb0_best.pth
┃ ┣ 📜db_utils.py # DB 저장 함수
┃ ┗ 📂results
┣ 📂frontend # 버튼, 페이지 등
┃ ┣ 📜analyze.html
┃ ┣ 📜index.html
┃ ┣ 📜order_done.html
┃ ┗ 📜order.html
┣ 📂backtend # 이벤트 처리
┃ ┗ 📜routers.py
┣ 📂database
┃ ┗ 📜visitor_logs.db
┣ 📂test
┃ ┗ 📜test_insert.py
┣ 📂webcam_app
┃ ┣ 📂result
┃ ┃ ┣ 📜captured_face.jpg
┃ ┃ ┗ 📜prediction.txt
┃ ┣ 📂test
┃ ┃ ┣ 📂image
┃ ┃ ┣ 📂result
┃ ┃ ┗ 📜run_test_image.py
┃ ┣ 📂utils
┃ ┃ ┣ 📜image_utils.py
┃ ┃ ┗ 📜__init__.py
┃ ┣ 📜predictor.py # 성별+연령대 예측
┃ ┣ 📜face_capture.py # 3초 타이머 -> 얼굴 캡처
┃ ┗ 📜__init__.py
┣ 📜create_db.py # db 생성용 파일
┗ 📜main.py # 메인 실행 파일
먼저 log 기록용 함수를 src/db_utils.py 로 하나 만들어놓자.
import sqlite3
def insert_visitor_log(db_path, table_name, timestamp, gender, age_group, menu_name, quantity):
conn=sqlite3.connect(db_path)
cursor=conn.cursor()
cursor.execute(f"""
INSERT INTO "{table_name}" (timestamp, gender, age_group, menu_name, quantity)
VALUES (?, ?, ?, ?, ?)
""", (timestamp, gender, age_group, menu_name, quantity))
conn.commit()
conn.close()
기존 run_webcam.py 였던 파일이 실시간 감지가 아니기 때문에 face_capture.py라고 변경하였고, 지금 스크립트로 동작하는 구조에서 flask에서 import해서 사용하기 위해 함수형으로 바꿨다. 크게 코드는 변경된 부분이 없으며 대신 predict_face로 return 받는 변수들 부분만 조금 변경되었다.
...
...
def capture_face_from_webcam():
# 웹캠 시작
cap=cv2.VideoCapture(0)
...
...
while True:
ret, frame=cap.read()
if not ret:
break
# 좌우 반전 복원
frame=cv2.flip(frame, 1)
# 예측 및 얼굴 감지
result_img, sex_text, age_text, label_text, has_face=predict_face(frame.copy())
result_img_with_countdown=result_img.copy() # 숫자 덧씌울 용
...
...
# 자원 해제
cap.release()
cv2.destroyAllWindows()
# 예측 결과 반환
return sex_text, age_text
이 redictor.py 에서도 앞선 4.2.에서 언급한던 부분과 동일하게 return값 부분을 신경써줘야한다.
...
...
# 클래스 예측 ==========================================================================
# -> image_utils
def predict_face(image):
bboxes=detect_faces(image)
has_face=bool(bboxes) # 얼굴 탐지 여부
# 예측값 return용
predictions=[]
for box in bboxes:
x, y, w_box, h_box=box
face=image[y:y+h_box, x:x+w_box]
if face.size == 0:
continue
# 전처리 및 예측
input_tensor=preprocess_face_rgb(face).to(device)
with torch.no_grad():
outputs=model(input_tensor)
preds=torch.sigmoid(outputs).cpu().numpy()[0]
# 성별 예측
sex_preds=preds[:2]
sex_label=np.argmax(sex_preds)
sex_text="male" if sex_label==0 else "female"
# 연령대 예측
age_preds=preds[2:]
age_label=np.argmax(age_preds)
age_text=LABEL_COLS[2:][age_label]
# 저장
label_text=f"{'남성' if sex_text == 'male' else '여성'}, {age_text}대"
predictions.append((sex_text, age_text, label_text)) # 튜플로 저장
image=draw_prediction(image, box, label_text)
if predictions:
sex, age, label=predictions[0]
return image, sex, age, label, has_face
else:
return image, "Unknown", "Unknown", "Unknown", False
이제 구상하고 있는 테이블은 다음과 같기에,
create_db.py 라는 파일을 하나 만들어서 원하는 년도를 입력하면 해당 구조를 가진 테이블을 생성하도록했다.
먼저 테이블 구조는 다음과 같이 정의했다.
# tb 정의
def create_table(cursor, table_name):
cursor.execute(f"""
CREATE TABLE IF NOT EXISTS "{table_name}" (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp TEXT,
gender TEXT,
age_group TEXT,
menu_name TEXT,
quantity INTEGER
)
""")
그리고 initialize하는 함수는 다음과 같다.
# db 초기화
def initialize_db(year, create_yearly=True, create_monthly=True):
DB_DIR="database"
os.makedirs(DB_DIR, exist_ok=True)
DB_PATH=os<.path.join(DB_DIR, "visitor_logs.db")
conn=sqlite3.connect(DB_PATH)
cursor=conn.cursor()
# 전체 master tb
create_table(cursor, "master")
# 연도별 master tb
# 2025master
if create_yearly:
create_table(cursor, f"{year}master")
# 월별 tb
# 202501
if create_monthly:
for m in range(1, 13):
table_name=f"{year}{m:02d}"
create_table(cursor, table_name)
conn.commit()
conn.close()
print(f"{year}년 기준 테이블 생성 완료 ({DB_PATH})")
이렇게 정의를 했고 이제 main에서 년도를 입력받아서 진행하도록 했다.
if __name__=="__main__":
year=input("생성할 연도를 입력하세요 (YYYY): ").strip()
if not year.isdigit() or len(year) != 4:
print("!!! 4자리 숫자로 입력하세요 !!!")
else:
initialize_db(year)

router를 통해 동작할 각 기능들을 구현해줬다.
이 부분은 html과의 연동이 있으므로 frontend/templates/* 도 함께 정리하겠다.
메인 홈으로, 주문하기 와 분석 보기 버튼 두개를 뒀다.
@app.route("/")
def index():
return render_template("index.html")
index.html<!DOCTYPE html> <html lang="ko"> <head> <meta charset="UTF-8"> <title>스마트 매장</title> </head> <body> <h1>스마트 매장 분석 시스템</h1> <form action="/order" method="get"> <button type="submit">주문하기</button> </form> <form action="/analyze" method="get"> <button type="submit">분석 보기</button> </form> </body> </html>
이제 주문하기 버튼을 통해 들어왔을 때 카메라로 촬영하고, 메뉴를 주문하게 되면 해당 내용들이 모두 db로 저장되도록 했다.
또한 주문이 완료됐다면 3초 후 홈으로 자동 이동되도록 js를 통해 구현했다.
@app.route("/order", methods=["GET", "POST"])
def order():
if request.method=="GET":
gender, age_group=capture_face_from_webcam()
return render_template("order.html", gender=gender, age_group=age_group)
elif request.method=="POST":
gender=request.form["gender"]
age_group=request.form["age_group"]
menu_name=request.form["menu"]
quantity=int(request.form["quantity"])
db_path=os.path.join("database", "visitor_logs.db")
timestamp=datetime.now().strftime("%Y-%m-%d %H:%M:%S")
# 동적으로 연도 및 월 추출
year=datetime.now().strftime("%Y")
year_month=datetime.now().strftime("%Y%m")
# master, YYYYmaster, YYYYMM에 모두 insert
for table in ["master", f"{year}master", year_month]:
insert_visitor_log(
db_path=db_path,
table_name=table,
timestamp=timestamp,
gender=gender,
age_group=age_group,
menu_name=menu_name,
quantity=quantity
)
return render_template("order_done.html")
order.html<!DOCTYPE html> <html lang="ko"> <head> <meta charset="UTF-8"> <title>주문 페이지</title> </head> <body> <!-- <h2>예측된 손님 정보: {{ gender }}, {{ age_group }}</h2> --> <form method="post"> <input type="hidden" name="gender" value="{{ gender }}"> <input type="hidden" name="age_group" value="{{ age_group }}"> <label>메뉴: <select name="menu"> <option value="아메리카노">아메리카노</option> <option value="카페라떼">카페라떼</option> <option value="녹차라떼">녹차라떼</option> </select> </label> <br> <label>수량: <input type="number" name="quantity" value="1" min="1"> </label> <br> <button type="submit">주문 완료</button> </form> </body> </html>
order_done.html<!DOCTYPE html> <html lang="ko"> <head> <meta charset="UTF-8"> <title>주문 완료</title> <!--3초 후 홈으로 자동 이동--> <script> let seconds = 3; function updateCountdown() { const countdownEl=document.getElementById("countdown"); countdownEl.textContent=`${seconds}초 후 메인화면으로 이동합니다.`; if (seconds === 0) { window.location.href = "/"; } else { seconds--; setTimeout(updateCountdown, 1000); } } window.onload=updateCountdown; </script> </head> <body> <h1>주문이 완료되었습니다!</h1> <p id="countdown"></p> </body> </html>
마지막 분석 하기 부분이며, 지금은 자세한 기능으로 구현하지는 않았으며 select all하도록 했다.
@app.route("/analyze")
def analyze():
db_path=os.path.join("database", "visitor_logs.db")
conn=sqlite3.connect(db_path)
conn.row_factory=sqlite3.Row
cursor=conn.cursor()
cursor.execute("SELECT * FROM master ORDER BY timestamp DESC")
logs=cursor.fetchall()
conn.close()
return render_template("analyze.html", logs=logs)
analyze.html<!DOCTYPE html> <html lang="ko"> <head> <meta charset="UTF-8"> <title>분석 페이지</title> </head> <body> <h1>방문자 분석 결과</h1> {% if logs %} <table border="1"> <tr> <th>시간</th> <th>성별</th> <th>연령대</th> <th>메뉴</th> <th>수량</th> </tr> {% for log in logs %} <tr> <td>{{ log.timestamp }}</td> <td>{{ log.gender }}</td> <td>{{ log.age_group }}</td> <td>{{ log.menu_name }}</td> <td>{{ log.quantity }}</td> </tr> {% endfor %} </table> {% else %} <p>저장된 기록이 없습니다!</p> {% endif %} <a href="/">홈으로 돌아가기</a> </body> </html>
이제 최종으로 불러오기만 하면 된다.
from flask import Flask
from backend.routes import init_routes
app=Flask(__name__, template_folder="frontend/templates")
# 본인이 직접 등록하면 된다.
# 개인 프로젝트이므로 간단하게 abc123으로 적음
app.config["SECRET_KEY"]="abc123"
# router 등록
init_routes(app)
if __name__=="__main__":
app.run(debug=True)
이렇게하여 기본적인 기능이 동작하도록 구현을 완료했다.
따라서 기능을 보면 다음과 같다.
python create_db.py를 통해 DB 초기화를 해준다.

main.py를 실행한다.

실행되었다면 http://127.0.0.1:5000 을 통해 접속한다.

분석 보기 버튼을 클릭하면 현재 비어있기에 저장된 기록이 없다고 나온다.

주문하기 버튼을 클릭하면 웹캠이 동작한다.

카운트가 종료되고 촬영이 완료되면 메뉴를 선택하는 화면으로 이동한다.

메뉴를 선택하고 주문 완료 버튼을 누르면 주문이 완료되었다는 메세지와 함께 3초 카운트 후 메인화면으로 자동 이동한다.

이후 돌아온 메인화면에서 분석 보기 을 클릭하면 분석 화면으로, 즉 지금은 저장된 데이터를 전체 조회하도록 해놨으니 저장된 내용을 볼 수 있다.

이렇게하여 기본적인 기능 구현은 완료했다.
이제 웹캠 동작과정을 비동기처리하고, 분석 페이지를 월별, 년도별 비교 할 수 있게 다양하게 바꾸고, UI 개선 정도를 개선할 거 같다.
생각보다 목표하는 방향을 바꿨더니 수정해야하는 부분이 많은 거 같다..