이제 마지막 최종적으로 관리자용 페이지를 좀 더 다듬기만 하면 된다.
현재 로그인하면 그냥 로그인이 완료되었고 네비게이션을 통해 주문 내역으로 들어가도록 되어있으나, 딱 로그인 하여 들어가면 한눈에 볼 수 있는 대시보드를 만드는 것이 좋아보인다.


구현하고자 하는 UI 예시는 위와 같다.
총 세 군대로 나눠서 보면 된다.
금액 매출액, 막대그래프로 전날과 금일 매출금액을 비교, 금일 판매기록을 기록된 테이블
def get_today_sales_amount(db_path):
conn=sqlite3.connect(db_path)
cursor=conn.cursor()
today=datetime.now().strftime("%Y-%m-%d")
cursor.execute("""
SELECT SUM(m.price)
FROM sales s
JOIN menu m ON s.menu_id=m.id
WHERE DATE(s.timestamp)=?
""", (today,))
total=cursor.fetchone()[0]
conn.close()
return total or 0
def get_sales_amount_comparison(db_path):
conn=sqlite3.connect(db_path)
cursor=conn.cursor()
# 오늘 날짜
today=datetime.now().date()
# 어제 날짜
yesterday=today - timedelta(days=1)
# 오늘 매출
cursor.execute(
"""
SELECT COALESCE(SUM(m.price), 0)
FROM sales s
JOIN menu m ON s.menu_id=m.id
WHERE DATE(s.timestamp)=?
""",
(today.strftime("%Y-%m-%d"),)
)
today_amount=cursor.fetchone()[0]
# 어제 매출
cursor.execute(
"""
SELECT COALESCE(SUM(m.price), 0)
FROM sales s
JOIN menu m ON s.menu_id=m.id
WHERE DATE(s.timestamp)=?
""",
(yesterday.strftime("%Y-%m-%d"),)
)
yesterday_amount=cursor.fetchone()[0]
conn.close()
return {"today": today_amount, "yesterday": yesterday_amount}
def get_today_sales_records(db_path):
conn=sqlite3.connect(db_path)
cursor=conn.cursor()
today=datetime.now().strftime("%Y-%m-%d")
cursor.execute("""
SELECT
s.order_id,
s.timestamp,
s.sex,
s.age_group,
m.name,
COUNT(*) AS quantity,
m.price * COUNT(*) AS total_price
FROM sales s
JOIN menu m ON s.menu_id=m.id
WHERE DATE(s.timestamp)=?
GROUP BY s.order_id, m.id
ORDER BY s.timestamp ASC
""", (today,))
raw_rows=cursor.fetchall()
conn.close()
# order_id -> 순번 매핑
order_map={}
seq=1
processed=[]
for order_id, timestamp, sex, age_group, menu_name, qty, total_price in raw_rows:
if order_id not in order_map:
order_map[order_id]=seq
seq+=1
processed.append((
order_map[order_id],
timestamp,
sex,
age_group,
menu_name,
qty,
total_price
))
return processed
from src.db_utils.sales import insert_sales, get_sales_analysis, get_today_sales_amount, get_today_sales_records, get_sales_amount_comparison
# 관리자 대시보드
@app.route("/admin/dashboard")
def admin_dashboard():
if not session.get("admin_logged_in"):
return redirect(url_for("admin_login"))
today_amount = get_today_sales_amount(DB_PATH)
sales_compare = get_sales_amount_comparison(DB_PATH)
today_records = get_today_sales_records(DB_PATH)
return render_template(
"admin/dashboard.html",
today_amount=today_amount,
sales_compare=sales_compare,
today_records=today_records
)

분석 결과 부분에서 다양하게 볼 수 있도록 구현하느라 시간이 좀 많이 들었다.
@app.route("/admin/analyze")
def admin_analyze():
if not session.get("admin_logged_in"):
return redirect(url_for("admin_login"))
# request 파라미터 get
start=request.args.get("start_date", "")
end=request.args.get("end_date", "")
group_by=request.args.get("group_by", "weekday")
gender=request.args.get("gender", "all")
age_group=request.args.get("age_group", "all")
metric=request.args.get("metric", "revenue")
# 결과 초기화
summary_rows=[] # 요약
detail_rows=[] # 상세
chart_data=[] # -> Chart.js
# 날짜 선택 해야지만 진행
if start and end:
# [WHERE 절 조립과정]
cond=["1=1"] # 항상 True 조건
params=[] # 파라미터 값 저장용
# 날짜 범위 between
cond.append("DATE(s.timestamp) BETWEEN ? AND ?")
params.extend([start, end])
# 성별 필터링
if gender != "all":
cond.append("s.sex=?")
params.append(gender)
# 연령대 필터링
if age_group != "all":
cond.append("s.age_group=?")
params.append(age_group)
# 최종 where절 문자열
where_clause=" AND ".join(cond)
# [그룹 필드 매핑]
group_map={
"year": "strftime('%Y', s.timestamp)",
"month": "strftime('%Y-%m', s.timestamp)",
"weekday": "strftime('%w', s.timestamp)", # 0~6
"hour": "strftime('%H', s.timestamp)",
"age_group": "s.age_group"
}
group_field=group_map[group_by]
# 정렬 컬럼
# revenue: 매출 합계, quantity: 판매 건수
order_by="revenue" if metric == "revenue" else "quantity"
# [집계 SQL문]
# LIMIT 20: 상위 20개 그룹만
sql=f"""
SELECT
{group_field} AS grp,
COUNT(*) AS quantity,
SUM(m.price) AS revenue
FROM sales s
JOIN menu m ON s.menu_id = m.id
WHERE {where_clause}
GROUP BY grp
ORDER BY {order_by} DESC
LIMIT 20
"""
# DB 접속 및 실행
conn=sqlite3.connect(DB_PATH)
corsor=conn.cursor()
corsor.execute(sql, params)
rows=corsor.fetchall()
conn.close()
# Chart.js용 데이터 변환
chart_data=[
{"grp": r[0], "quantity": r[1], "revenue": r[2]}
for r in rows
]
# 요약/상세 테이블
# 그룹X성별 통합
for grp, quantity, revenue in rows:
summary_rows.append((grp, gender, quantity, revenue))
# [메뉴별 상세 집계 SQL문]
sql2=f"""
SELECT
{group_field} AS grp,
s.sex,
m.name,
COUNT(*) AS quantity,
SUM(m.price) AS revenue
FROM sales s
JOIN menu m ON s.menu_id = m.id
WHERE {where_clause}
GROUP BY grp, s.sex, m.name
ORDER BY grp, s.sex, {order_by} DESC
"""
# DB 접속
conn=sqlite3.connect(DB_PATH)
corsor=conn.cursor()
corsor.execute(sql2, params)
# 상세 데이터 fetchall()
for grp, sex, menu, quantity, revenue in corsor.fetchall():
detail_rows.append((grp, sex, menu, quantity, revenue))
conn.close()
# [결과 analyze.html 렌더링]
return render_template("admin/analyze.html", request_args=request.args, group_by=group_by, metric=metric,
summary_rows=summary_rows, detail_rows=detail_rows, chart_data=chart_data)
이렇게해서 그룹핑을 통해 조건 조회를 많이 걸었다.
실제로 /admin/analyze 를 통해 보면 다음과 같다.


이와 같이 기간을 선택해야만 조회가 가능하도록 설정했으며,

그룹핑조건은 이와 같다.
이렇게하여 그룹핑으로 1차로 묶고, 필터링 조건을 추가로 걸었다.
요일별 -> 성별/연령대 전체 -> 매출순 정렬

시간별 -> 남성/20대 -> 판매량순 정렬

본인의 얼굴을 기준으로 넣었기 때문에 여성으로는 현재 확인이 불가능하다.
전체 동작으로 보면 다음과 같다.

이 프로젝트는 이정도에서 마무리 짓고자 한다.
여기서 더 발전될 사항은 모델적인 부분에서 더 많은 샘플들로 일반화 성능을 끌어올리는 것이 중요하다고 생각한다.
진행하다보니 자꾸 모델적인 부분이 아닌 다른 쪽에 치중하게 되는 거 같아서 이쯤에서 마무리 짓고자 한다.
자세한 코드들은 깃허브를 참고하시길.
https://github.com/itsjulianjeong/ADFOU/tree/main