Files
tasteby/backend/api/routes/admin_users.py
joungmin 3694730501 Add admin features, responsive UI, user reviews, visit stats, and channel-colored markers
- Admin: video management with Google Maps match status, manual restaurant mapping, restaurant remap on name change
- Admin: user management tab with favorites/reviews detail
- Admin: channel deletion fix for IDs with slashes
- Frontend: responsive mobile layout (map top, list bottom, 2-row header)
- Frontend: channel-colored map markers with legend
- Frontend: my reviews list, favorites toggle, visit counter overlay
- Frontend: force light mode for dark theme devices
- Backend: visit tracking (site_visits table), user reviews endpoint
- Backend: bulk transcript/extract streaming, geocode key fixes
- Nginx config for production deployment

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 14:52:20 +09:00

94 lines
3.1 KiB
Python

"""Admin user management API routes."""
from __future__ import annotations
from fastapi import APIRouter, Query
from core.db import conn
router = APIRouter()
@router.get("")
def list_users(
limit: int = Query(50, le=200),
offset: int = Query(0, ge=0),
):
"""List all users with favorite/review counts."""
sql = """
SELECT u.id, u.email, u.nickname, u.avatar_url, u.provider, u.created_at,
NVL(fav.cnt, 0) AS favorite_count,
NVL(rev.cnt, 0) AS review_count
FROM tasteby_users u
LEFT JOIN (
SELECT user_id, COUNT(*) AS cnt FROM user_favorites GROUP BY user_id
) fav ON fav.user_id = u.id
LEFT JOIN (
SELECT user_id, COUNT(*) AS cnt FROM user_reviews GROUP BY user_id
) rev ON rev.user_id = u.id
ORDER BY u.created_at DESC
OFFSET :off ROWS FETCH NEXT :lim ROWS ONLY
"""
count_sql = "SELECT COUNT(*) FROM tasteby_users"
with conn() as c:
cur = c.cursor()
cur.execute(count_sql)
total = cur.fetchone()[0]
cur.execute(sql, {"off": offset, "lim": limit})
cols = [d[0].lower() for d in cur.description]
rows = [dict(zip(cols, row)) for row in cur.fetchall()]
for r in rows:
if r.get("created_at"):
r["created_at"] = r["created_at"].isoformat()
return {"users": rows, "total": total}
@router.get("/{user_id}/favorites")
def get_user_favorites(user_id: str):
"""Get a user's favorite restaurants."""
sql = """
SELECT r.id, r.name, r.address, r.region, r.cuisine_type,
r.rating, r.business_status, f.created_at
FROM user_favorites f
JOIN restaurants r ON r.id = f.restaurant_id
WHERE f.user_id = :u
ORDER BY f.created_at DESC
"""
with conn() as c:
cur = c.cursor()
cur.execute(sql, {"u": user_id})
cols = [d[0].lower() for d in cur.description]
rows = [dict(zip(cols, row)) for row in cur.fetchall()]
for r in rows:
if r.get("created_at"):
r["created_at"] = r["created_at"].isoformat()
return rows
@router.get("/{user_id}/reviews")
def get_user_reviews(user_id: str):
"""Get a user's reviews with restaurant names."""
sql = """
SELECT r.id, r.restaurant_id, r.rating, r.review_text,
r.visited_at, r.created_at,
rest.name AS restaurant_name
FROM user_reviews r
LEFT JOIN restaurants rest ON rest.id = r.restaurant_id
WHERE r.user_id = :u
ORDER BY r.created_at DESC
"""
with conn() as c:
cur = c.cursor()
cur.execute(sql, {"u": user_id})
cols = [d[0].lower() for d in cur.description]
rows = [dict(zip(cols, row)) for row in cur.fetchall()]
for r in rows:
# Handle CLOB
if hasattr(r.get("review_text"), "read"):
r["review_text"] = r["review_text"].read()
if r.get("visited_at"):
r["visited_at"] = r["visited_at"].isoformat()
if r.get("created_at"):
r["created_at"] = r["created_at"].isoformat()
return rows