soyailabs / app\__init__.py
wiizm's picture
Upload app\__init__.py with huggingface_hub
2cfce29 verified
"""
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
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:
"""
๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์‹คํ–‰
Args:
app: Flask ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ธ์Šคํ„ด์Šค
"""
try:
db_uri = app.config['SQLALCHEMY_DATABASE_URI']
if not db_uri.startswith('sqlite:///'):
logger.warning(f"SQLite๊ฐ€ ์•„๋‹Œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋Š” ์ž๋™ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜์ด ์ง€์›๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค: {db_uri}")
return
db_path_str = db_uri.replace('sqlite:///', '')
db_path = Path(db_path_str)
# ์ƒ๋Œ€ ๊ฒฝ๋กœ์ธ ๊ฒฝ์šฐ instance ํด๋” ๊ธฐ์ค€์œผ๋กœ ์ฒ˜๋ฆฌ
if not db_path.is_absolute():
db_path = Path(app.instance_path) / db_path
if not db_path.exists():
logger.info(f"๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํŒŒ์ผ์ด ์—†์Šต๋‹ˆ๋‹ค (์ƒˆ๋กœ ์ƒ์„ฑ๋จ): {db_path}")
return
logger.info(f"๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์‹œ์ž‘: {db_path}")
conn = sqlite3.connect(str(db_path))
cursor = conn.cursor()
# user ํ…Œ์ด๋ธ”์— nickname ์ปฌ๋Ÿผ์ด ์žˆ๋Š”์ง€ ํ™•์ธ
cursor.execute("PRAGMA table_info(user)")
user_columns = [column[1] for column in cursor.fetchall()]
if 'nickname' not in user_columns:
logger.info("user ํ…Œ์ด๋ธ”์— nickname ์ปฌ๋Ÿผ ์ถ”๊ฐ€ ์ค‘...")
cursor.execute("ALTER TABLE user ADD COLUMN nickname VARCHAR(80)")
conn.commit()
logger.info("user.nickname ์ปฌ๋Ÿผ ์ถ”๊ฐ€ ์™„๋ฃŒ")
# uploaded_file ํ…Œ์ด๋ธ”์ด ์กด์žฌํ•˜๋Š”์ง€ ํ™•์ธ
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='uploaded_file'")
if cursor.fetchone():
cursor.execute("PRAGMA table_info(uploaded_file)")
uploaded_file_columns = [column[1] for column in cursor.fetchall()]
if 'uploaded_by' not in uploaded_file_columns:
logger.info("uploaded_file ํ…Œ์ด๋ธ”์— uploaded_by ์ปฌ๋Ÿผ ์ถ”๊ฐ€ ์ค‘...")
cursor.execute("ALTER TABLE uploaded_file ADD COLUMN uploaded_by INTEGER")
conn.commit()
logger.info("uploaded_file.uploaded_by ์ปฌ๋Ÿผ ์ถ”๊ฐ€ ์™„๋ฃŒ")
if 'parent_file_id' not in uploaded_file_columns:
logger.info("uploaded_file ํ…Œ์ด๋ธ”์— parent_file_id ์ปฌ๋Ÿผ ์ถ”๊ฐ€ ์ค‘...")
cursor.execute("ALTER TABLE uploaded_file ADD COLUMN parent_file_id INTEGER")
conn.commit()
logger.info("uploaded_file.parent_file_id ์ปฌ๋Ÿผ ์ถ”๊ฐ€ ์™„๋ฃŒ")
# document_chunk ํ…Œ์ด๋ธ”์ด ์กด์žฌํ•˜๋Š”์ง€ ํ™•์ธ
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='document_chunk'")
if cursor.fetchone():
cursor.execute("PRAGMA table_info(document_chunk)")
document_chunk_columns = [column[1] for column in cursor.fetchall()]
if 'chunk_metadata' not in document_chunk_columns:
logger.info("document_chunk ํ…Œ์ด๋ธ”์— chunk_metadata ์ปฌ๋Ÿผ ์ถ”๊ฐ€ ์ค‘...")
cursor.execute("ALTER TABLE document_chunk ADD COLUMN chunk_metadata TEXT")
conn.commit()
logger.info("document_chunk.chunk_metadata ์ปฌ๋Ÿผ ์ถ”๊ฐ€ ์™„๋ฃŒ")
# chat_session ํ…Œ์ด๋ธ”์ด ์กด์žฌํ•˜๋Š”์ง€ ํ™•์ธ
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='chat_session'")
if cursor.fetchone():
cursor.execute("PRAGMA table_info(chat_session)")
chat_session_columns = [column[1] for column in cursor.fetchall()]
if 'analysis_model' not in chat_session_columns:
logger.info("chat_session ํ…Œ์ด๋ธ”์— analysis_model ์ปฌ๋Ÿผ ์ถ”๊ฐ€ ์ค‘...")
cursor.execute("ALTER TABLE chat_session ADD COLUMN analysis_model VARCHAR(100)")
conn.commit()
logger.info("chat_session.analysis_model ์ปฌ๋Ÿผ ์ถ”๊ฐ€ ์™„๋ฃŒ")
if 'answer_model' not in chat_session_columns:
logger.info("chat_session ํ…Œ์ด๋ธ”์— answer_model ์ปฌ๋Ÿผ ์ถ”๊ฐ€ ์ค‘...")
cursor.execute("ALTER TABLE chat_session ADD COLUMN answer_model VARCHAR(100)")
conn.commit()
logger.info("chat_session.answer_model ์ปฌ๋Ÿผ ์ถ”๊ฐ€ ์™„๋ฃŒ")
conn.close()
logger.info("๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์™„๋ฃŒ")
except sqlite3.Error as e:
logger.error(f"๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์ค‘ SQLite ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {e}", exc_info=True)
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()