samchun-gemini / utils /cache_utils.py
JHyeok5's picture
Upload folder using huggingface_hub
0f3460d verified
"""
Social Caching ์œ ํ‹ธ๋ฆฌํ‹ฐ
๋‹ค๋ฅธ ์‚ฌ์šฉ์ž๊ฐ€ ์ƒ์„ฑํ•œ ์ธ๊ธฐ ์ฝ”์Šค๋ฅผ ์žฌ์‚ฌ์šฉํ•˜์—ฌ AI ๋น„์šฉ ์ ˆ๊ฐ.
์บ์‹œ ํžˆํŠธ ์‹œ 0.5์ดˆ ์ด๋‚ด ์‘๋‹ต ๋ชฉํ‘œ.
@module cache_utils
@description
- ์ฝ”์Šค ์ƒ์„ฑ ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ํ•ด์‹œ๋กœ ๋ณ€ํ™˜
- ์œ ์‚ฌํ•œ ์กฐ๊ฑด์˜ ์ฝ”์Šค๋ฅผ ๋น ๋ฅด๊ฒŒ ๋งค์นญ
- ์œ„์น˜๋Š” ์•ฝ 1km ๋ฐ˜๊ฒฝ์œผ๋กœ ๊ทธ๋ฃนํ™”
- ์‹œ๊ฐ„์€ 10๋ถ„ ๋‹จ์œ„๋กœ ๋ฐ˜์˜ฌ๋ฆผ
@changelog
- v1.1.0 (2026-01-26): ํ•ด์‹œ ๊ธธ์ด ์ฆ๊ฐ€ (L001)
- 16์ž๋ฆฌ -> 32์ž๋ฆฌ๋กœ ๋ณ€๊ฒฝ
- ์ถฉ๋Œ ํ™•๋ฅ  ๊ฐ์†Œ: 2^64 -> 2^128 ๊ฐ€๋Šฅ ์กฐํ•ฉ
- v1.0.0 (2026-01-25): ์ดˆ๊ธฐ ๊ตฌํ˜„
"""
import hashlib
import json
from typing import Dict, Any, Optional, List
def create_params_hash(
theme: Optional[str],
duration_minutes: int,
location_lat: float,
location_lng: float,
activity_level: Optional[str] = None,
mood: Optional[List[str]] = None
) -> str:
"""
์ฝ”์Šค ์ƒ์„ฑ ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ํ•ด์‹œ๋กœ ๋ณ€ํ™˜
์œ ์‚ฌํ•œ ์กฐ๊ฑด์˜ ์ฝ”์Šค๋ฅผ ๋งค์นญํ•˜๊ธฐ ์œ„ํ•ด ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ์ •๊ทœํ™”ํ•˜์—ฌ ํ•ด์‹œ ์ƒ์„ฑ.
- ์‹œ๊ฐ„: 10๋ถ„ ๋‹จ์œ„๋กœ ๋ฐ˜์˜ฌ๋ฆผ (55๋ถ„ โ†’ 60๋ถ„, 65๋ถ„ โ†’ 70๋ถ„)
- ์œ„์น˜: ์†Œ์ˆ˜์  2์ž๋ฆฌ๋กœ ๋ฐ˜์˜ฌ๋ฆผ (์•ฝ 1km ๋ฐ˜๊ฒฝ ๊ทธ๋ฃนํ™”)
- mood: ์ •๋ ฌํ•˜์—ฌ ์ˆœ์„œ ๋ฌด๊ด€ํ•˜๊ฒŒ ๋™์ผ ํ•ด์‹œ ์ƒ์„ฑ
Args:
theme: ํ…Œ๋งˆ (history, nature, food, photo, healing)
duration_minutes: ํฌ๋ง ์‹œ๊ฐ„ (๋ถ„)
location_lat: ์‚ฌ์šฉ์ž ์œ„๋„
location_lng: ์‚ฌ์šฉ์ž ๊ฒฝ๋„
activity_level: ํ™œ๋™ ์ˆ˜์ค€ (light, moderate, active) (optional)
mood: ๋ถ„์œ„๊ธฐ ๋ฆฌ์ŠคํŠธ (quiet, vibrant, romantic, family) (optional)
Returns:
SHA256 ํ•ด์‹œ (32์ž๋ฆฌ) - ์˜ˆ: "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6"
Examples:
>>> create_params_hash("history", 60, 33.45, 126.32)
'a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6'
>>> create_params_hash("nature", 55, 33.456, 126.321) # ์‹œ๊ฐ„ 55โ†’60, ์œ„์น˜ ๋ฐ˜์˜ฌ๋ฆผ
'b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7'
>>> create_params_hash("food", 90, 33.45, 126.32, mood=["romantic", "quiet"])
>>> create_params_hash("food", 90, 33.45, 126.32, mood=["quiet", "romantic"]) # ๋™์ผ ํ•ด์‹œ
"""
# ์‹œ๊ฐ„์„ 10๋ถ„ ๋‹จ์œ„๋กœ ๋ฐ˜์˜ฌ๋ฆผ
# 55๋ถ„ โ†’ 60๋ถ„, 65๋ถ„ โ†’ 70๋ถ„, 30๋ถ„ โ†’ 30๋ถ„
duration_rounded = round(duration_minutes / 10) * 10
# ์œ„์น˜๋ฅผ ์•ฝ 1km ๋ฐ˜๊ฒฝ์œผ๋กœ ๊ทธ๋ฃนํ™” (์†Œ์ˆ˜์  2์ž๋ฆฌ)
# ์œ„๋„ 0.01๋„ โ‰ˆ 1.1km, ๊ฒฝ๋„ 0.01๋„ โ‰ˆ 0.9km (์ œ์ฃผ ๊ธฐ์ค€)
lat_rounded = round(location_lat, 2)
lng_rounded = round(location_lng, 2)
# ํ•ด์‹œ ๋ฐ์ดํ„ฐ ๊ตฌ์„ฑ
hash_data: Dict[str, Any] = {
"theme": theme or "general",
"duration": duration_rounded,
"lat": lat_rounded,
"lng": lng_rounded,
}
# ์„ ํƒ์  ํ•„๋“œ ์ถ”๊ฐ€ (์กด์žฌํ•˜๋Š” ๊ฒฝ์šฐ๋งŒ)
if activity_level:
hash_data["activity"] = activity_level
if mood and len(mood) > 0:
# mood ์ •๋ ฌํ•˜์—ฌ ์ˆœ์„œ ๋ฌด๊ด€ํ•˜๊ฒŒ ๋™์ผ ํ•ด์‹œ ๋ณด์žฅ
hash_data["mood"] = sorted(mood)
# JSON ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜ ํ›„ ํ•ด์‹œ ์ƒ์„ฑ
# sort_keys=True๋กœ ํ‚ค ์ˆœ์„œ ์ผ๊ด€์„ฑ ๋ณด์žฅ
json_str = json.dumps(hash_data, sort_keys=True, ensure_ascii=False)
hash_obj = hashlib.sha256(json_str.encode('utf-8'))
# 32์ž๋ฆฌ ํ•ด์‹œ ๋ฐ˜ํ™˜ (์ถฉ๋Œ ํ™•๋ฅ  ๊ทนํžˆ ๋‚ฎ์Œ, 16์ž๋ฆฌ์—์„œ ์ฆ๊ฐ€)
# 16์ž๋ฆฌ: 2^64 ๊ฐ€๋Šฅ ์กฐํ•ฉ โ†’ 32์ž๋ฆฌ: 2^128 ๊ฐ€๋Šฅ ์กฐํ•ฉ
HASH_LENGTH = 32
return hash_obj.hexdigest()[:HASH_LENGTH]
def normalize_params_for_cache(
theme: Optional[str],
duration_minutes: int,
location_lat: float,
location_lng: float,
activity_level: Optional[str] = None,
mood: Optional[List[str]] = None
) -> Dict[str, Any]:
"""
์บ์‹œ ์กฐํšŒ๋ฅผ ์œ„ํ•ด ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ์ •๊ทœํ™”
create_params_hash์™€ ๋™์ผํ•œ ์ •๊ทœํ™” ๋กœ์ง์„ ์ ์šฉํ•˜์—ฌ ๋””๋ฒ„๊น…/๋กœ๊น…์šฉ ๋ฐ์ดํ„ฐ ๋ฐ˜ํ™˜.
Args:
(create_params_hash์™€ ๋™์ผ)
Returns:
์ •๊ทœํ™”๋œ ํŒŒ๋ผ๋ฏธํ„ฐ ๋”•์…”๋„ˆ๋ฆฌ
"""
duration_rounded = round(duration_minutes / 10) * 10
lat_rounded = round(location_lat, 2)
lng_rounded = round(location_lng, 2)
result: Dict[str, Any] = {
"theme": theme or "general",
"duration": duration_rounded,
"lat": lat_rounded,
"lng": lng_rounded,
}
if activity_level:
result["activity"] = activity_level
if mood and len(mood) > 0:
result["mood"] = sorted(mood)
return result
def calculate_cache_similarity(
params1: Dict[str, Any],
params2: Dict[str, Any]
) -> float:
"""
๋‘ ํŒŒ๋ผ๋ฏธํ„ฐ ์„ธํŠธ์˜ ์œ ์‚ฌ๋„ ๊ณ„์‚ฐ (0.0 ~ 1.0)
ํ•ด์‹œ๊ฐ€ ๋‹ค๋ฅธ ๊ฒฝ์šฐ์—๋„ ์œ ์‚ฌํ•œ ์ฝ”์Šค๋ฅผ ์ฐพ๊ธฐ ์œ„ํ•œ ๋ณด์กฐ ํ•จ์ˆ˜.
ํ–ฅํ›„ fuzzy matching ๊ตฌํ˜„ ์‹œ ์‚ฌ์šฉ ์˜ˆ์ •.
Args:
params1: ์ฒซ ๋ฒˆ์งธ ์ •๊ทœํ™”๋œ ํŒŒ๋ผ๋ฏธํ„ฐ
params2: ๋‘ ๋ฒˆ์งธ ์ •๊ทœํ™”๋œ ํŒŒ๋ผ๋ฏธํ„ฐ
Returns:
์œ ์‚ฌ๋„ ์ ์ˆ˜ (0.0 = ์™„์ „ ๋‹ค๋ฆ„, 1.0 = ์™„์ „ ์ผ์น˜)
"""
score = 0.0
max_score = 0.0
# ํ…Œ๋งˆ ์ผ์น˜ (๊ฐ€์ค‘์น˜: 30%)
max_score += 0.3
if params1.get("theme") == params2.get("theme"):
score += 0.3
# ์‹œ๊ฐ„ ์œ ์‚ฌ๋„ (๊ฐ€์ค‘์น˜: 25%)
max_score += 0.25
d1 = params1.get("duration", 60)
d2 = params2.get("duration", 60)
time_diff = abs(d1 - d2)
if time_diff == 0:
score += 0.25
elif time_diff <= 10:
score += 0.2
elif time_diff <= 20:
score += 0.15
elif time_diff <= 30:
score += 0.1
# ์œ„์น˜ ์œ ์‚ฌ๋„ (๊ฐ€์ค‘์น˜: 25%)
max_score += 0.25
lat_diff = abs(params1.get("lat", 0) - params2.get("lat", 0))
lng_diff = abs(params1.get("lng", 0) - params2.get("lng", 0))
dist_approx = (lat_diff ** 2 + lng_diff ** 2) ** 0.5
if dist_approx <= 0.01: # ~1km
score += 0.25
elif dist_approx <= 0.02: # ~2km
score += 0.2
elif dist_approx <= 0.05: # ~5km
score += 0.1
# ํ™œ๋™ ์ˆ˜์ค€ ์ผ์น˜ (๊ฐ€์ค‘์น˜: 10%)
max_score += 0.1
if params1.get("activity") == params2.get("activity"):
score += 0.1
# ๋ถ„์œ„๊ธฐ ์ผ์น˜ (๊ฐ€์ค‘์น˜: 10%)
max_score += 0.1
mood1 = set(params1.get("mood", []))
mood2 = set(params2.get("mood", []))
if mood1 and mood2:
overlap = len(mood1 & mood2)
total = len(mood1 | mood2)
score += 0.1 * (overlap / total) if total > 0 else 0
elif not mood1 and not mood2:
score += 0.1
return round(score / max_score, 2) if max_score > 0 else 0.0