soyailabs / app /__init__.py
GitHub Actions
Auto-deploy from GitHub Actions - 2025-12-11 09:40:37
8ff5131
raw
history blame
14.1 kB
"""
Flask ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ดˆ๊ธฐํ™”
"""
from flask import Flask, request, send_from_directory, jsonify
from flask_login import LoginManager
import sqlite3
from pathlib import Path
from typing import Optional
from sqlalchemy import create_engine, text, inspect
from app.database import db, User
from app.core.config import Config, get_config
from app.core.logger import get_logger
logger = get_logger(__name__)
login_manager = LoginManager()
login_manager.login_view = 'main.login'
login_manager.login_message = '๋กœ๊ทธ์ธ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.'
login_manager.login_message_category = 'info'
@login_manager.unauthorized_handler
def unauthorized():
"""์ธ์ฆ๋˜์ง€ ์•Š์€ ์‚ฌ์šฉ์ž ์ฒ˜๋ฆฌ"""
from flask import redirect, url_for, request
# API ์š”์ฒญ์ธ ๊ฒฝ์šฐ JSON ์‘๋‹ต
if request.path.startswith('/api/'):
from flask import jsonify
return jsonify({'error': '๋กœ๊ทธ์ธ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.'}), 401
# ์ผ๋ฐ˜ ์š”์ฒญ์ธ ๊ฒฝ์šฐ ๋กœ๊ทธ์ธ ํŽ˜์ด์ง€๋กœ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ
return redirect(url_for('main.login', next=request.path))
@login_manager.user_loader
def load_user(user_id: str) -> Optional[User]:
"""
์‚ฌ์šฉ์ž ๋กœ๋“œ ํ•จ์ˆ˜ (Flask-Login์šฉ)
Args:
user_id: ์‚ฌ์šฉ์ž ID (๋ฌธ์ž์—ด)
Returns:
User ๊ฐ์ฒด ๋˜๋Š” None
"""
try:
return User.query.get(int(user_id))
except (ValueError, TypeError):
return None
def create_app() -> Flask:
"""
Flask ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ํŒฉํ† ๋ฆฌ ํ•จ์ˆ˜
Returns:
์„ค์ •๋œ Flask ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ธ์Šคํ„ด์Šค
"""
config = get_config()
# ํ•„์ˆ˜ ๋””๋ ‰ํ† ๋ฆฌ ์ƒ์„ฑ
config.ensure_directories()
# ํ…œํ”Œ๋ฆฟ ํด๋” ๋ฐ static ํด๋” ๊ฒฝ๋กœ ์„ค์ •
template_folder = str(config.TEMPLATES_FOLDER)
static_folder = str(config.STATIC_FOLDER)
app = Flask(__name__, template_folder=template_folder, static_folder=static_folder)
# Flask ์„ค์ • ์ ์šฉ
app.config['SECRET_KEY'] = config.SECRET_KEY
app.config['SQLALCHEMY_DATABASE_URI'] = config.SQLALCHEMY_DATABASE_URI
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = config.SQLALCHEMY_TRACK_MODIFICATIONS
app.config['MAX_CONTENT_LENGTH'] = config.MAX_CONTENT_LENGTH
# SQLAlchemy ์—ฐ๊ฒฐ ํ’€ ์„ค์ • (ํด๋ผ์šฐ๋“œ ํ™˜๊ฒฝ์—์„œ ์œ ํœด ์—ฐ๊ฒฐ ๋Š๊น€ ๋ฐฉ์ง€)
# pool_pre_ping: ์—ฐ๊ฒฐ ์‚ฌ์šฉ ์ „์— ์‚ด์•„์žˆ๋Š”์ง€ ํ™•์ธ (ping)
# pool_recycle: ์—ฐ๊ฒฐ์„ ์žฌ์‚ฌ์šฉํ•˜๊ธฐ ์ „ ์ตœ๋Œ€ ์‹œ๊ฐ„(์ดˆ) - 300์ดˆ(5๋ถ„)
app.config['SQLALCHEMY_ENGINE_OPTIONS'] = {
'pool_pre_ping': True,
'pool_recycle': 300
}
# ์„ธ์…˜ ์ฟ ํ‚ค ์„ค์ • (๋ธŒ๋ผ์šฐ์ € ํ˜ธํ™˜์„ฑ ๊ฐœ์„ )
app.config['SESSION_COOKIE_SECURE'] = True # HTTPS์—์„œ๋งŒ ์ „์†ก
app.config['SESSION_COOKIE_HTTPONLY'] = True # JavaScript ์ ‘๊ทผ ๋ฐฉ์ง€
app.config['SESSION_COOKIE_SAMESITE'] = 'Lax' # CSRF ๋ฐฉ์ง€ ๋ฐ ๋ธŒ๋ผ์šฐ์ € ํ˜ธํ™˜์„ฑ
app.config['PERMANENT_SESSION_LIFETIME'] = 86400 # 24์‹œ๊ฐ„
# ํ™•์žฅ ์ดˆ๊ธฐํ™”
db.init_app(app)
login_manager.init_app(app)
# Blueprint ๋“ฑ๋ก
from app.routes import main_bp
app.register_blueprint(main_bp)
# ๋“ฑ๋ก๋œ ๋ผ์šฐํŠธ ํ™•์ธ (๋””๋ฒ„๊น…์šฉ)
with app.app_context():
logger.info(f"๋“ฑ๋ก๋œ ๋ผ์šฐํŠธ ์ˆ˜: {len([r for r in app.url_map.iter_rules()])}")
logger.info(f"๋“ฑ๋ก๋œ Blueprint: {list(app.blueprints.keys())}")
# ์ฃผ์š” ๋ผ์šฐํŠธ ํ™•์ธ
routes = [str(r) for r in app.url_map.iter_rules() if r.endpoint.startswith('main.')]
logger.info(f"๋“ฑ๋ก๋œ main ๋ผ์šฐํŠธ: {routes[:10]}...") # ์ฒ˜์Œ 10๊ฐœ๋งŒ
# favicon.ico ํ•ธ๋“ค๋Ÿฌ ์ถ”๊ฐ€
@app.route('/favicon.ico')
def favicon():
"""favicon.ico ์š”์ฒญ ์ฒ˜๋ฆฌ"""
try:
return send_from_directory(
str(config.STATIC_FOLDER),
'logo.webp',
mimetype='image/webp'
)
except Exception as e:
logger.warning(f"favicon.ico ์ฒ˜๋ฆฌ ์‹คํŒจ: {e}")
return '', 204 # No Content
# 404 ์—๋Ÿฌ ํ•ธ๋“ค๋Ÿฌ (์ผ์‹œ์ ์œผ๋กœ ๋น„ํ™œ์„ฑํ™”ํ•˜์—ฌ ๋””๋ฒ„๊น…)
# ๋ผ์šฐํŠธ๊ฐ€ ์ œ๋Œ€๋กœ ๋“ฑ๋ก๋˜์—ˆ๋Š”์ง€ ํ™•์ธ ํ›„ ๋‹ค์‹œ ํ™œ์„ฑํ™”
# @app.errorhandler(404)
# def not_found(error):
# """404 ์—๋Ÿฌ ์ฒ˜๋ฆฌ"""
# logger.warning(f"404 ์—๋Ÿฌ: {request.path} - {request.method}")
# if request.path.startswith('/api/'):
# return jsonify({'error': '๋ฆฌ์†Œ์Šค๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.', 'path': request.path}), 404
# return '', 404
# ์š”์ฒญ ๋กœ๊น… ๋ฏธ๋“ค์›จ์–ด ์ถ”๊ฐ€
@app.before_request
def log_request_info():
"""๊ฐ HTTP ์š”์ฒญ ์ •๋ณด๋ฅผ ๋กœ๊น…"""
logger.info(f"[์š”์ฒญ] {request.method} {request.path} - IP: {request.remote_addr}")
if request.args:
logger.debug(f"[์š”์ฒญ ํŒŒ๋ผ๋ฏธํ„ฐ] {dict(request.args)}")
@app.after_request
def log_response_info(response):
"""๊ฐ HTTP ์‘๋‹ต ์ •๋ณด๋ฅผ ๋กœ๊น…"""
logger.info(f"[์‘๋‹ต] {request.method} {request.path} - ์ƒํƒœ: {response.status_code}")
# Permissions-Policy ํ—ค๋” ์ถ”๊ฐ€ (์ตœ์‹  ๋ธŒ๋ผ์šฐ์ €์—์„œ ์ง€์›ํ•˜๋Š” ๊ธฐ๋Šฅ๋งŒ ์‚ฌ์šฉ)
# ์ธ์‹๋˜์ง€ ์•Š๋Š” ๊ธฐ๋Šฅ๋“ค์€ ์ œ๊ฑฐํ•˜์—ฌ ๊ฒฝ๊ณ  ๋ฐฉ์ง€
# ์ตœ์‹  ํ‘œ์ค€์— ๋งž๋Š” ๊ธฐ๋Šฅ๋“ค๋งŒ ํฌํ•จ
permissions_policy = (
"camera=(), "
"microphone=(), "
"geolocation=(), "
"payment=(), "
"usb=()"
)
response.headers['Permissions-Policy'] = permissions_policy
# ๋ธŒ๋ผ์šฐ์ € ์บ์‹œ ์ œ์–ด ํ—ค๋” ์ถ”๊ฐ€ (๋ธŒ๋ผ์šฐ์ € ํ˜ธํ™˜์„ฑ ๊ฐœ์„ )
# HTML ํŽ˜์ด์ง€๋Š” ์บ์‹œํ•˜์ง€ ์•Š๋„๋ก ์„ค์ •ํ•˜์—ฌ ๋ธŒ๋ผ์šฐ์ €๋ณ„ ์ฐจ์ด ์ตœ์†Œํ™”
if request.path == '/' or request.path.startswith('/login') or request.path.startswith('/admin') or request.path.startswith('/webnovels'):
response.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate'
response.headers['Pragma'] = 'no-cache'
response.headers['Expires'] = '0'
return response
# ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ดˆ๊ธฐํ™” ๋ฐ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜
with app.app_context():
# ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๊ฒฐ ์ •๋ณด ๋กœ๊น…
db_uri = app.config['SQLALCHEMY_DATABASE_URI']
if db_uri.startswith('postgresql://') or db_uri.startswith('postgres://'):
# PostgreSQL ์—ฐ๊ฒฐ ์ •๋ณด (๋ณด์•ˆ์„ ์œ„ํ•ด ๋น„๋ฐ€๋ฒˆํ˜ธ๋Š” ๋งˆ์Šคํ‚น)
masked_uri = db_uri.split('@')[0].split('://')[0] + '://***@' + '@'.join(db_uri.split('@')[1:]) if '@' in db_uri else db_uri
logger.info(f"[๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค] PostgreSQL ์—ฐ๊ฒฐ ์‹œ๋„: {masked_uri}")
# ์—ฐ๊ฒฐ ํ…Œ์ŠคํŠธ
try:
engine = create_engine(
db_uri,
pool_pre_ping=True,
pool_recycle=300
)
with engine.connect() as conn:
result = conn.execute(text("SELECT version()"))
version = result.fetchone()[0]
logger.info(f"[๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค] PostgreSQL ์—ฐ๊ฒฐ ์„ฑ๊ณต! ๋ฒ„์ „: {version[:50]}...")
except Exception as e:
logger.error(f"[๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค] PostgreSQL ์—ฐ๊ฒฐ ์‹คํŒจ: {str(e)}")
logger.warning("[๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค] SQLite๋กœ ํด๋ฐฑํ•ฉ๋‹ˆ๋‹ค.")
# SQLite๋กœ ํด๋ฐฑ
db_uri = f'sqlite:///{config.INSTANCE_FOLDER / "finance_analysis.db"}'
app.config['SQLALCHEMY_DATABASE_URI'] = db_uri
else:
logger.info(f"[๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค] SQLite ์‚ฌ์šฉ: {db_uri}")
# ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํ…Œ์ด๋ธ” ์ƒ์„ฑ
try:
db.create_all()
logger.info("[๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค] ํ…Œ์ด๋ธ” ์ƒ์„ฑ ์™„๋ฃŒ")
except Exception as e:
logger.error(f"[๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค] ํ…Œ์ด๋ธ” ์ƒ์„ฑ ์‹คํŒจ: {str(e)}")
raise
# ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์‹คํ–‰ (SQLite๋งŒ ์ง€์›)
migrate_database(app)
# ๊ด€๋ฆฌ์ž ๊ณ„์ • ์ƒ์„ฑ
create_admin_user()
logger.info("Flask ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์ด ์ดˆ๊ธฐํ™”๋˜์—ˆ์Šต๋‹ˆ๋‹ค.")
return app
def migrate_database(app: Flask) -> None:
"""
๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์‹คํ–‰ (SQLAlchemy Inspector ์‚ฌ์šฉ)
Args:
app: Flask ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ธ์Šคํ„ด์Šค
"""
try:
logger.info("๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ํ™•์ธ ์ค‘...")
with app.app_context():
inspector = inspect(db.engine)
# 1. user ํ…Œ์ด๋ธ” - nickname ์ปฌ๋Ÿผ
if inspector.has_table('user'):
columns = [c['name'] for c in inspector.get_columns('user')]
if 'nickname' not in columns:
logger.info("user ํ…Œ์ด๋ธ”์— nickname ์ปฌ๋Ÿผ ์ถ”๊ฐ€ ์ค‘...")
with db.engine.begin() as conn:
conn.execute(text("ALTER TABLE \"user\" ADD COLUMN nickname VARCHAR(80)"))
# 2. uploaded_file ํ…Œ์ด๋ธ” - uploaded_by, parent_file_id ์ปฌ๋Ÿผ
if inspector.has_table('uploaded_file'):
columns = [c['name'] for c in inspector.get_columns('uploaded_file')]
with db.engine.begin() as conn:
if 'uploaded_by' not in columns:
logger.info("uploaded_file ํ…Œ์ด๋ธ”์— uploaded_by ์ปฌ๋Ÿผ ์ถ”๊ฐ€ ์ค‘...")
conn.execute(text("ALTER TABLE uploaded_file ADD COLUMN uploaded_by INTEGER"))
if 'parent_file_id' not in columns:
logger.info("uploaded_file ํ…Œ์ด๋ธ”์— parent_file_id ์ปฌ๋Ÿผ ์ถ”๊ฐ€ ์ค‘...")
conn.execute(text("ALTER TABLE uploaded_file ADD COLUMN parent_file_id INTEGER"))
# 3. document_chunk ํ…Œ์ด๋ธ” - chunk_metadata ์ปฌ๋Ÿผ
if inspector.has_table('document_chunk'):
columns = [c['name'] for c in inspector.get_columns('document_chunk')]
if 'chunk_metadata' not in columns:
logger.info("document_chunk ํ…Œ์ด๋ธ”์— chunk_metadata ์ปฌ๋Ÿผ ์ถ”๊ฐ€ ์ค‘...")
with db.engine.begin() as conn:
conn.execute(text("ALTER TABLE document_chunk ADD COLUMN chunk_metadata TEXT"))
# 4. chat_session ํ…Œ์ด๋ธ” - analysis_model, answer_model ์ปฌ๋Ÿผ
if inspector.has_table('chat_session'):
columns = [c['name'] for c in inspector.get_columns('chat_session')]
with db.engine.begin() as conn:
if 'analysis_model' not in columns:
logger.info("chat_session ํ…Œ์ด๋ธ”์— analysis_model ์ปฌ๋Ÿผ ์ถ”๊ฐ€ ์ค‘...")
conn.execute(text("ALTER TABLE chat_session ADD COLUMN analysis_model VARCHAR(100)"))
if 'answer_model' not in columns:
logger.info("chat_session ํ…Œ์ด๋ธ”์— answer_model ์ปฌ๋Ÿผ ์ถ”๊ฐ€ ์ค‘...")
conn.execute(text("ALTER TABLE chat_session ADD COLUMN answer_model VARCHAR(100)"))
# 5. chat_message ํ…Œ์ด๋ธ” - ํ† ํฐ ์ •๋ณด ๋ฐ ๋ชจ๋ธ ์ •๋ณด ์ปฌ๋Ÿผ (PostgreSQL ์˜ค๋ฅ˜ ํ•ด๊ฒฐ์šฉ)
if inspector.has_table('chat_message'):
columns = [c['name'] for c in inspector.get_columns('chat_message')]
with db.engine.begin() as conn:
# session_id ์ปฌ๋Ÿผ์˜ NOT NULL ์ œ์•ฝ์กฐ๊ฑด ์ œ๊ฑฐ (PostgreSQL)
if db.engine.dialect.name == 'postgresql':
logger.info("PostgreSQL: chat_message.session_id์˜ NOT NULL ์ œ์•ฝ์กฐ๊ฑด ์ œ๊ฑฐ ์‹œ๋„...")
conn.execute(text("ALTER TABLE chat_message ALTER COLUMN session_id DROP NOT NULL"))
if 'input_tokens' not in columns:
logger.info("chat_message ํ…Œ์ด๋ธ”์— input_tokens ์ปฌ๋Ÿผ ์ถ”๊ฐ€ ์ค‘...")
conn.execute(text("ALTER TABLE chat_message ADD COLUMN input_tokens INTEGER"))
if 'output_tokens' not in columns:
logger.info("chat_message ํ…Œ์ด๋ธ”์— output_tokens ์ปฌ๋Ÿผ ์ถ”๊ฐ€ ์ค‘...")
conn.execute(text("ALTER TABLE chat_message ADD COLUMN output_tokens INTEGER"))
if 'model_name' not in columns:
logger.info("chat_message ํ…Œ์ด๋ธ”์— model_name ์ปฌ๋Ÿผ ์ถ”๊ฐ€ ์ค‘...")
conn.execute(text("ALTER TABLE chat_message ADD COLUMN model_name VARCHAR(100)"))
if 'usage_type' not in columns:
logger.info("chat_message ํ…Œ์ด๋ธ”์— usage_type ์ปฌ๋Ÿผ ์ถ”๊ฐ€ ์ค‘...")
conn.execute(text("ALTER TABLE chat_message ADD COLUMN usage_type VARCHAR(20) DEFAULT 'user'"))
logger.info("๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์™„๋ฃŒ")
except Exception as e:
logger.error(f"๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {e}", exc_info=True)
def create_admin_user() -> None:
"""
์ดˆ๊ธฐ ๊ด€๋ฆฌ์ž ๊ณ„์ • ์ƒ์„ฑ
"""
admin_username = 'soymedia'
admin_password = 's0ymedi@1@34'
try:
admin = User.query.filter_by(username=admin_username).first()
if not admin:
admin = User(username=admin_username, is_admin=True, is_active=True)
admin.set_password(admin_password)
db.session.add(admin)
db.session.commit()
logger.info(f'๊ด€๋ฆฌ์ž ๊ณ„์ •์ด ์ƒ์„ฑ๋˜์—ˆ์Šต๋‹ˆ๋‹ค: {admin_username}')
else:
logger.debug(f'๊ด€๋ฆฌ์ž ๊ณ„์ •์ด ์ด๋ฏธ ์กด์žฌํ•ฉ๋‹ˆ๋‹ค: {admin_username}')
except Exception as e:
logger.error(f'๊ด€๋ฆฌ์ž ๊ณ„์ • ์ƒ์„ฑ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {e}', exc_info=True)
db.session.rollback()