hfexample's picture
Deploy clean snapshot of the repository
e221c83
from flask import Blueprint, render_template, session, redirect, url_for, jsonify, request, current_app
from sqlalchemy import extract, String # String ์ถ”๊ฐ€
import datetime
import time
from . import db
from .models import Diary, User
from .emotion_engine import predict_emotion
from .recommender import Recommender
import logging
import os
import google.generativeai as genai
bp = Blueprint('main', __name__)
recommender = Recommender()
# --- Gemini API ์„ค์ • ---
try:
api_key = os.environ.get('GEMINI_API_KEY')
if not api_key:
logging.warning("๐Ÿ”ฅ๐Ÿ”ฅ๐Ÿ”ฅ GEMINI_API_KEY ํ™˜๊ฒฝ ๋ณ€์ˆ˜๊ฐ€ ์„ค์ •๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค. ๐Ÿ”ฅ๐Ÿ”ฅ๐Ÿ”ฅ")
genai.configure(api_key=api_key)
except Exception as e:
logging.error(f"๐Ÿ”ฅ๐Ÿ”ฅ๐Ÿ”ฅ Gemini API ์„ค์ • ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {e} ๐Ÿ”ฅ๐Ÿ”ฅ๐Ÿ”ฅ")
# ๊ฐ์ •๋ณ„ ์ด๋ชจ์ง€ ๋งต
emotion_emoji_map = {
'๋ถ„๋…ธ': '๐Ÿ˜ ', '๋ถˆ์•ˆ': '๐Ÿ˜Ÿ', '์Šฌํ””': '๐Ÿ˜ข',
'๋‹นํ™ฉ': '๐Ÿ˜ฎ', '๊ธฐ์จ': '๐Ÿ˜„', '์ƒ์ฒ˜': '๐Ÿ’”',
}
default_recommendations = {
'๋ถ„๋…ธ': 'ํ™”๊ฐ€ ๋‚  ๋•Œ๋Š” ์‹ ๋‚˜๋Š” ์Œ์•…์„ ๋“ฃ๊ฑฐ๋‚˜, ๊ฐ€๋ฒผ์šด ์ฝ”๋ฏธ๋”” ์˜ํ™”๋ฅผ ๋ณด๋ฉฐ ๊ธฐ๋ถ„์„ ์ „ํ™˜ํ•ด ๋ณด์„ธ์š”.',
'๋ถˆ์•ˆ': '๋ถˆ์•ˆํ•  ๋•Œ๋Š” ์ฐจ๋ถ„ํ•œ ํด๋ž˜์‹ ์Œ์•…์„ ๋“ฃ๊ฑฐ๋‚˜, ๋”ฐ๋œปํ•œ ์ฐจ๋ฅผ ๋งˆ์‹œ๋ฉฐ ๋ช…์ƒ์„ ํ•ด๋ณด๋Š” ๊ฑด ์–ด๋–จ๊นŒ์š”?',
'์Šฌํ””': '์Šฌํ”Œ ๋•Œ๋Š” ์œ„๋กœ๊ฐ€ ๋˜๋Š” ์˜ํ™”๋‚˜ ์ฑ…์„ ๋ณด๋ฉฐ ๊ฐ์ •์„ ์ถฉ๋ถ„ํžˆ ๋А๊ปด๋ณด๋Š” ๊ฒƒ๋„ ์ข‹์•„์š”. ํ˜น์€ ์นœ๊ตฌ์™€ ๋Œ€ํ™”๋ฅผ ๋‚˜๋ˆ ๋ณด์„ธ์š”.',
'๋‹นํ™ฉ': '๋‹นํ™ฉ์Šค๋Ÿฌ์šธ ๋•Œ๋Š” ์ž ์‹œ ์ˆจ์„ ๊ณ ๋ฅด๊ณ , ์ข‹์•„ํ•˜๋Š” ์Œ์•…์„ ๋“ค์œผ๋ฉฐ ๋งˆ์Œ์„ ์ง„์ •์‹œ์ผœ ๋ณด์„ธ์š”.',
'๊ธฐ์จ': '๊ธฐ์  ๋•Œ๋Š” ์‹ ๋‚˜๋Š” ๋Œ„์Šค ์Œ์•…๊ณผ ํ•จ๊ป˜ ์ถค์„ ์ถ”๊ฑฐ๋‚˜, ์นœ๊ตฌ๋“ค๊ณผ ๋งŒ๋‚˜ ์ฆ๊ฑฐ์›€์„ ๋‚˜๋ˆ ๋ณด์„ธ์š”!',
'์ƒ์ฒ˜': '๋งˆ์Œ์˜ ์ƒ์ฒ˜๋ฅผ ๋ฐ›์•˜์„ ๋•Œ๋Š”, ์œ„๋กœ๊ฐ€ ๋˜๋Š” ์Œ์•…์„ ๋“ฃ๊ฑฐ๋‚˜, ์กฐ์šฉํ•œ ๊ณณ์—์„œ ์ฑ…์„ ์ฝ์œผ๋ฉฐ ๋งˆ์Œ์„ ๋‹ฌ๋ž˜๋ณด์„ธ์š”.'
}
def generate_recommendation(user_diary, predicted_emotion):
"""
์ฃผ์–ด์ง„ ์ผ๊ธฐ ๋‚ด์šฉ๊ณผ ๊ฐ์ •์„ ๋ฐ”ํƒ•์œผ๋กœ Gemini API๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋ฌธํ™”์ƒํ™œ ์ถ”์ฒœ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
"""
start_time = time.time()
logging.info("Gemini API ํ˜ธ์ถœ ์‹œ์ž‘...")
try:
model = genai.GenerativeModel('gemini-flash-latest')
prompt = f"""
์‚ฌ์šฉ์ž์˜ ์ผ๊ธฐ ๋‚ด์šฉ๊ณผ ๊ฐ์ •์„ ๋ฐ”ํƒ•์œผ๋กœ ๋ฌธํ™”์ƒํ™œ์„ ์ถ”์ฒœํ•ด์ค˜.
์‚ฌ์šฉ์ž๋Š” ํ˜„์žฌ '{predicted_emotion}' ๊ฐ์ •์„ ๋А๋ผ๊ณ  ์žˆ์–ด.
์ผ๊ธฐ ๋‚ด์šฉ:
---
{user_diary}
---
์•„๋ž˜ ๋‘ ๊ฐ€์ง€ ์‹œ๋‚˜๋ฆฌ์˜ค์— ๋งž์ถฐ ์˜ํ™”, ์Œ์•…, ๋„์„œ๋งŒ ์ถ”์ฒœํ•ด์ค˜.
๊ฐ ์ถ”์ฒœ ํ•ญ๋ชฉ์€ "์ข…๋ฅ˜: ์ถ”์ฒœ ์ฝ˜ํ…์ธ  ์ œ๋ชฉ (์•„ํ‹ฐ์ŠคํŠธ/๊ฐ๋…/์ž‘๊ฐ€ ๋“ฑ)" ํ˜•์‹์œผ๋กœ ์ž‘์„ฑํ•˜๊ณ , ๊ฐ„๋‹จํ•œ ์ถ”์ฒœ ์ด์œ ๋ฅผ ๋ง๋ถ™์—ฌ์ค˜.
๊ฒฐ๊ณผ๋Š” Markdown ํ˜•์‹์œผ๋กœ ๋ณด๊ธฐ ์ข‹๊ฒŒ ์ •๋ฆฌํ•ด์ค˜.
## [์ˆ˜์šฉ]
ํ˜„์žฌ ๊ฐ์ •์„ ๋” ๊นŠ์ด ๋А๋ผ๊ฑฐ๋‚˜ ์œ„๋กœ๋ฐ›๊ณ  ์‹ถ์„ ๋•Œ.
## [์ „ํ™˜]
ํ˜„์žฌ ๊ฐ์ •์—์„œ ๋ฒ—์–ด๋‚˜ ์ƒˆ๋กœ์šด ํ™œ๋ ฅ์„ ์–ป๊ณ  ์‹ถ์„ ๋•Œ.
"""
response = model.generate_content(prompt)
end_time = time.time()
logging.info(f"Gemini API ํ˜ธ์ถœ ์™„๋ฃŒ. ์†Œ์š” ์‹œ๊ฐ„: {end_time - start_time:.2f}์ดˆ")
return response.text
except Exception as e:
logging.error(f"๐Ÿ”ฅ๐Ÿ”ฅ๐Ÿ”ฅ Gemini API ํ˜ธ์ถœ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {e} ๐Ÿ”ฅ๐Ÿ”ฅ๐Ÿ”ฅ")
return default_recommendations.get(predicted_emotion, "์˜ค๋Š˜์€ ์ข‹์•„ํ•˜๋Š” ์Œ์•…์„ ๋“ค์œผ๋ฉฐ ํŽธ์•ˆํ•œ ํ•˜๋ฃจ๋ฅผ ๋ณด๋‚ด๋Š” ๊ฑด ์–ด๋– ์„ธ์š”?")
@bp.route("/")
def home():
if 'user_id' not in session:
return redirect(url_for('auth.login'))
logged_in = 'user_id' in session
display_name = None
if logged_in:
user_id = session.get('user_id')
user = User.query.get(user_id)
if user:
display_name = user.nickname if user.nickname else user.username
else:
display_name = session.get('username') # Fallback if user not found
logging.info(f"๋ฉ”์ธ ํŽ˜์ด์ง€ ์ ‘์†: ๋กœ๊ทธ์ธ ์ƒํƒœ: {logged_in}, ์‚ฌ์šฉ์ž: {display_name}")
return render_template("main.html", logged_in=logged_in, display_name=display_name)
@bp.route("/api/predict", methods=["POST"])
def api_predict():
if 'user_id' not in session:
return jsonify({"error": "๋กœ๊ทธ์ธ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค."}), 401
user_diary = request.json.get("diary")
if not user_diary:
return jsonify({"error": "์ผ๊ธฐ ๋‚ด์šฉ์ด ์—†์Šต๋‹ˆ๋‹ค."}), 400
try:
# 1. Predict top 3 emotions
emotion_results = predict_emotion(user_diary, top_k=3)
if not emotion_results:
logging.error("[/api/predict] ๊ฐ์ • ๋ถ„์„ ๊ฒฐ๊ณผ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.")
return jsonify({"error": "๊ฐ์ •์„ ๋ถ„์„ํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."}), 500
# 2. Process results
top_emotion_data = emotion_results[0]
top_emotion_label = top_emotion_data['label']
top_emotion_score = top_emotion_data['score']
# 3. Create candidates list
candidates = []
for result in emotion_results:
emotion_label = result['label']
candidates.append({
'emotion': emotion_label,
'score': result['score'],
'emoji': emotion_emoji_map.get(emotion_label, '๐Ÿค”')
})
# 4. Generate recommendation ONLY for the top emotion initially
recommendation_text = generate_recommendation(user_diary, top_emotion_label)
# 5. Return the new structure
# Note: Diary is NOT saved here. It will be saved via a separate '/diary/save' call later.
return jsonify({
"top_emotion": top_emotion_label,
"top_score": top_emotion_score,
"candidates": candidates,
"recommendation": recommendation_text
})
except Exception as e:
logging.error(f"[/api/predict] ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {e}")
db.session.rollback() # ํ˜น์‹œ ๋ชจ๋ฅผ ํŠธ๋žœ์žญ์…˜ ๋กค๋ฐฑ
return jsonify({"error": "์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค."}), 500
@bp.route("/api/recommend", methods=["POST"])
def api_recommend():
logging.info("[/api/recommend] ์š”์ฒญ ์ˆ˜์‹ ๋จ.")
user_diary = request.json.get("diary")
predicted_emotion = request.json.get("emotion") # ๊ฐ์ •์„ ์ง์ ‘ ๋ฐ›์Œ
if not user_diary or not predicted_emotion:
logging.warning("[/api/recommend] ์ผ๊ธฐ ๋‚ด์šฉ ๋˜๋Š” ๊ฐ์ •์ด ์—†์Šต๋‹ˆ๋‹ค.")
return jsonify({"error": "์ผ๊ธฐ ๋‚ด์šฉ ๋˜๋Š” ๊ฐ์ •์ด ์—†์Šต๋‹ˆ๋‹ค."}), 400
recommendation_text = generate_recommendation(user_diary, predicted_emotion)
response_data = {
"emotion": predicted_emotion,
"emoji": emotion_emoji_map.get(predicted_emotion, '๐Ÿค”'),
"recommendation": recommendation_text
}
return jsonify(response_data)
@bp.route('/api/diaries')
def api_diaries():
if 'user_id' not in session:
return jsonify({"error": "๋กœ๊ทธ์ธ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค."}), 401
user_id = session['user_id']
year = request.args.get('year', type=int)
month = request.args.get('month', type=int)
logging.info(f"API ์š”์ฒญ: user_id={user_id}, year={year}, month={month}")
if not year or not month:
today = datetime.date.today()
year = today.year
month = today.month
start_date = datetime.date(year, month, 1)
if month == 12:
end_date = datetime.date(year + 1, 1, 1)
else:
end_date = datetime.date(year, month + 1, 1)
logging.info(f"DB ์ฟผ๋ฆฌ ๋ฒ”์œ„: {start_date} <= created_at < {end_date}")
user_diaries = Diary.query.filter(
Diary.user_id == user_id,
Diary.created_at >= start_date,
Diary.created_at < end_date
).order_by(Diary.created_at.asc()).all()
diaries_data = []
utc_tz = datetime.timezone.utc
kst_tz = datetime.timezone(datetime.timedelta(hours=9))
for diary in user_diaries:
created_at_utc = diary.created_at
# ํƒ€์ž„์กด ์ •๋ณด๊ฐ€ ์—†๋Š” ๊ฒฝ์šฐ UTC๋กœ ๊ฐ„์ฃผ
if created_at_utc.tzinfo is None:
created_at_utc = created_at_utc.replace(tzinfo=utc_tz)
created_at_kst = created_at_utc.astimezone(kst_tz)
diaries_data.append({
"id": diary.id,
"date": created_at_kst.strftime('%Y-%m-%d'),
"createdAt": created_at_kst.strftime('%Y-%m-%d %H:%M:%S'),
"content": diary.content,
"emotion": diary.emotion,
"recommendation": diary.recommendation
})
return jsonify(diaries_data)
@bp.route('/api/diaries/counts')
def api_diaries_counts():
if 'user_id' not in session:
return jsonify({"error": "๋กœ๊ทธ์ธ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค."}), 401
user_id = session['user_id']
year = request.args.get('year', type=int)
if not year:
year = datetime.date.today().year
# PostgreSQL์€ extract๋ฅผ ์ง€์›ํ•˜๋ฏ€๋กœ ์›๋ž˜ ๋กœ์ง์œผ๋กœ ๋ณต์›
counts = db.session.query(
extract('month', Diary.created_at).cast(String), # ์›”์„ ๋ฌธ์ž์—ด๋กœ ์บ์ŠคํŒ…
db.func.count(Diary.id)
).filter(
Diary.user_id == user_id,
extract('year', Diary.created_at) == year
).group_by(
extract('month', Diary.created_at)
).all()
counts_dict = {month: count for month, count in counts}
return jsonify(counts_dict)
@bp.route('/my_diary')
def my_diary():
if 'user_id' not in session:
return redirect(url_for('auth.login'))
user_id = session.get('user_id')
user = User.query.get(user_id)
display_name = user.nickname if user.nickname else user.username
return render_template('diary.html', display_name=display_name)
@bp.route('/mypage')
def mypage():
if 'user_id' not in session:
return redirect(url_for('auth.login'))
user_id = session['user_id']
user = User.query.get(user_id)
user_info = {
'username': user.username,
'nickname': user.nickname,
'display_name': user.nickname if user.nickname else user.username
}
return render_template('page.html', user_info=user_info)
@bp.route('/update_nickname', methods=['POST'])
def update_nickname():
if 'user_id' not in session:
return redirect(url_for('auth.login'))
user_id = session['user_id']
user = User.query.get(user_id)
new_nickname = request.form.get('nickname')
# ๋‹‰๋„ค์ž„์ด ๋น„์–ด์žˆ๊ฑฐ๋‚˜, ๊ณต๋ฐฑ๋งŒ ์žˆ์„ ๊ฒฝ์šฐ None์œผ๋กœ ์ €์žฅ
if not new_nickname or not new_nickname.strip():
user.nickname = None
else:
user.nickname = new_nickname
db.session.commit()
# ์„ธ์…˜ ์ •๋ณด ์—…๋ฐ์ดํŠธ (์„ ํƒ ์‚ฌํ•ญ, ๋‹‰๋„ค์ž„์„ ์„ธ์…˜์— ์ €์žฅํ•  ๊ฒฝ์šฐ)
# session['nickname'] = user.nickname
return redirect(url_for('main.mypage'))
@bp.route('/diary/save', methods=['POST'])
def diary_save():
if 'user_id' not in session:
return jsonify({"error": "๋กœ๊ทธ์ธ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค."}), 401
user_id = session['user_id']
diary_content = request.form.get('diary')
predicted_emotion = request.form.get('emotion')
if not diary_content or not predicted_emotion:
return jsonify({"error": "์ผ๊ธฐ ๋‚ด์šฉ์ด๋‚˜ ๊ฐ์ •์ด ์—†์Šต๋‹ˆ๋‹ค."}), 400
try:
# ์ถ”์ฒœ ์ƒ์„ฑ
recommendation_text = generate_recommendation(diary_content, predicted_emotion)
# Gemini API ์‹คํŒจ ์‹œ Recommender ํด๋ž˜์Šค๋กœ ๋Œ€์ฒด
if recommendation_text is None:
logging.info("Gemini ์ถ”์ฒœ ์‹คํŒจ. Recommender ํด๋ž˜์Šค๋กœ ๋Œ€์ฒดํ•ฉ๋‹ˆ๋‹ค.")
su_yoong_recs = recommender.recommend(predicted_emotion, '์ˆ˜์šฉ')
jeon_hwan_recs = recommender.recommend(predicted_emotion, '์ „ํ™˜')
# diary_logic.js๊ฐ€ ํŒŒ์‹ฑํ•  ์ˆ˜ ์žˆ๋Š” ํ˜•์‹์œผ๋กœ ๋งŒ๋“ญ๋‹ˆ๋‹ค.
recommendation_text = f"## [์ˆ˜์šฉ]\n"
for rec in su_yoong_recs:
recommendation_text += f"* {rec}\n"
recommendation_text += f"\n## [์ „ํ™˜]\n"
for rec in jeon_hwan_recs:
recommendation_text += f"* {rec}\n"
# ์ผ๊ธฐ ์ €์žฅ
new_diary = Diary(
content=diary_content,
emotion=predicted_emotion,
recommendation=recommendation_text,
user_id=user_id
)
db.session.add(new_diary)
db.session.commit()
return jsonify({
"success": "์ผ๊ธฐ๊ฐ€ ์„ฑ๊ณต์ ์œผ๋กœ ์ €์žฅ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.",
"recommendation": recommendation_text # ํด๋ผ์ด์–ธํŠธ์—์„œ ๋ฐ”๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก ์ถ”์ฒœ ๋‚ด์šฉ ๋ฐ˜ํ™˜
}), 200
except Exception as e:
db.session.rollback()
logging.error(f"์ผ๊ธฐ ์ €์žฅ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {e}")
return jsonify({"error": "์ผ๊ธฐ ์ €์žฅ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค."}), 500
@bp.route('/diary/delete/<string:diary_id>', methods=['DELETE'])
def delete_diary(diary_id):
if 'user_id' not in session:
return jsonify({"error": "๋กœ๊ทธ์ธ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค."}), 401
diary_to_delete = Diary.query.get(diary_id)
if not diary_to_delete:
return jsonify({"error": "์ผ๊ธฐ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."}), 404
if diary_to_delete.user_id != session['user_id']:
return jsonify({"error": "์‚ญ์ œ ๊ถŒํ•œ์ด ์—†์Šต๋‹ˆ๋‹ค."}), 403
try:
db.session.delete(diary_to_delete)
db.session.commit()
return jsonify({"success": "์ผ๊ธฐ๊ฐ€ ์‚ญ์ œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค."}), 200
except Exception as e:
db.session.rollback()
return jsonify({"error": "์‚ญ์ œ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค."}), 500
@bp.route('/test/animation')
def test_animation():
return render_template('test_animation.html', display_name='ํ…Œ์ŠคํŠธ')