Initial commit: Tasteby - YouTube restaurant map service

Backend (FastAPI + Oracle ADB), Frontend (Next.js), daemon worker.
Features: channel/video/restaurant management, semantic search,
Google OAuth, user reviews.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
joungmin
2026-03-06 13:47:19 +09:00
commit 36bec10bd0
54 changed files with 9727 additions and 0 deletions

189
backend/core/review.py Normal file
View File

@@ -0,0 +1,189 @@
"""User review DB operations."""
from __future__ import annotations
from datetime import date
import oracledb
from core.db import conn
def create_review(
user_id: str,
restaurant_id: str,
rating: float,
review_text: str | None = None,
visited_at: date | None = None,
) -> dict:
"""Create a new review. Returns the created review dict."""
sql = """
INSERT INTO user_reviews (user_id, restaurant_id, rating, review_text, visited_at)
VALUES (:user_id, :restaurant_id, :rating, :review_text, :visited_at)
RETURNING id INTO :out_id
"""
with conn() as c:
cur = c.cursor()
out_id = cur.var(oracledb.STRING)
cur.execute(sql, {
"user_id": user_id,
"restaurant_id": restaurant_id,
"rating": rating,
"review_text": review_text,
"visited_at": visited_at,
"out_id": out_id,
})
new_id = out_id.getvalue()[0]
return get_review_by_id(new_id)
def update_review(
review_id: str,
user_id: str,
rating: float | None = None,
review_text: str | None = None,
visited_at: date | None = None,
) -> dict:
"""Update an existing review. Only the owner can update.
Returns the updated review dict, or None if not found / not owner.
"""
sql = """
UPDATE user_reviews
SET rating = COALESCE(:rating, rating),
review_text = COALESCE(:review_text, review_text),
visited_at = COALESCE(:visited_at, visited_at),
updated_at = SYSTIMESTAMP
WHERE id = :id AND user_id = :user_id
"""
with conn() as c:
cur = c.cursor()
cur.execute(sql, {
"rating": rating,
"review_text": review_text,
"visited_at": visited_at,
"id": review_id,
"user_id": user_id,
})
if cur.rowcount == 0:
return None
return get_review_by_id(review_id)
def delete_review(review_id: str, user_id: str) -> bool:
"""Delete a review. Only the owner can delete. Returns True if deleted."""
sql = "DELETE FROM user_reviews WHERE id = :id AND user_id = :user_id"
with conn() as c:
cur = c.cursor()
cur.execute(sql, {"id": review_id, "user_id": user_id})
return cur.rowcount > 0
def get_review_by_id(review_id: str) -> dict | None:
"""Get a single review by ID."""
sql = """
SELECT r.id, r.user_id, r.restaurant_id, r.rating, r.review_text,
r.visited_at, r.created_at, r.updated_at,
u.nickname, u.avatar_url
FROM user_reviews r
JOIN tasteby_users u ON u.id = r.user_id
WHERE r.id = :id
"""
with conn() as c:
cur = c.cursor()
cur.execute(sql, {"id": review_id})
row = cur.fetchone()
if not row:
return None
return _row_to_dict(row)
def get_reviews_for_restaurant(
restaurant_id: str,
limit: int = 20,
offset: int = 0,
) -> list[dict]:
"""List reviews for a restaurant, including user nickname/avatar."""
sql = """
SELECT r.id, r.user_id, r.restaurant_id, r.rating, r.review_text,
r.visited_at, r.created_at, r.updated_at,
u.nickname, u.avatar_url
FROM user_reviews r
JOIN tasteby_users u ON u.id = r.user_id
WHERE r.restaurant_id = :restaurant_id
ORDER BY r.created_at DESC
OFFSET :off ROWS FETCH NEXT :lim ROWS ONLY
"""
with conn() as c:
cur = c.cursor()
cur.execute(sql, {
"restaurant_id": restaurant_id,
"off": offset,
"lim": limit,
})
return [_row_to_dict(row) for row in cur.fetchall()]
def get_user_reviews(
user_id: str,
limit: int = 20,
offset: int = 0,
) -> list[dict]:
"""List reviews by a specific user."""
sql = """
SELECT r.id, r.user_id, r.restaurant_id, r.rating, r.review_text,
r.visited_at, r.created_at, r.updated_at,
u.nickname, u.avatar_url
FROM user_reviews r
JOIN tasteby_users u ON u.id = r.user_id
WHERE r.user_id = :user_id
ORDER BY r.created_at DESC
OFFSET :off ROWS FETCH NEXT :lim ROWS ONLY
"""
with conn() as c:
cur = c.cursor()
cur.execute(sql, {
"user_id": user_id,
"off": offset,
"lim": limit,
})
return [_row_to_dict(row) for row in cur.fetchall()]
def get_restaurant_avg_rating(restaurant_id: str) -> dict:
"""Get average rating and review count for a restaurant."""
sql = """
SELECT ROUND(AVG(rating), 1) AS avg_rating, COUNT(*) AS review_count
FROM user_reviews
WHERE restaurant_id = :restaurant_id
"""
with conn() as c:
cur = c.cursor()
cur.execute(sql, {"restaurant_id": restaurant_id})
row = cur.fetchone()
return {
"avg_rating": float(row[0]) if row[0] else None,
"review_count": int(row[1]),
}
def _row_to_dict(row) -> dict:
"""Convert a review query row to a dict."""
review_text = row[4]
if hasattr(review_text, "read"):
review_text = review_text.read()
return {
"id": row[0],
"user_id": row[1],
"restaurant_id": row[2],
"rating": float(row[3]),
"review_text": review_text,
"visited_at": row[5].isoformat() if row[5] else None,
"created_at": row[6].isoformat() if row[6] else None,
"updated_at": row[7].isoformat() if row[7] else None,
"user_nickname": row[8],
"user_avatar_url": row[9],
}