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/', 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='ํ…Œ์ŠคํŠธ')