|
|
""" |
|
|
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 |
|
|
|
|
|
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() |
|
|
|
|
|
|
|
|
template_folder = str(config.TEMPLATES_FOLDER) |
|
|
static_folder = str(config.STATIC_FOLDER) |
|
|
|
|
|
app = Flask(__name__, template_folder=template_folder, static_folder=static_folder) |
|
|
|
|
|
|
|
|
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 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app.config['SQLALCHEMY_ENGINE_OPTIONS'] = { |
|
|
'pool_pre_ping': True, |
|
|
'pool_recycle': 300 |
|
|
} |
|
|
|
|
|
|
|
|
app.config['SESSION_COOKIE_SECURE'] = True |
|
|
app.config['SESSION_COOKIE_HTTPONLY'] = True |
|
|
app.config['SESSION_COOKIE_SAMESITE'] = 'Lax' |
|
|
app.config['PERMANENT_SESSION_LIFETIME'] = 86400 |
|
|
|
|
|
|
|
|
db.init_app(app) |
|
|
login_manager.init_app(app) |
|
|
|
|
|
|
|
|
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]}...") |
|
|
|
|
|
|
|
|
@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 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@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 = ( |
|
|
"camera=(), " |
|
|
"microphone=(), " |
|
|
"geolocation=(), " |
|
|
"payment=(), " |
|
|
"usb=()" |
|
|
) |
|
|
response.headers['Permissions-Policy'] = permissions_policy |
|
|
|
|
|
|
|
|
|
|
|
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://'): |
|
|
|
|
|
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๋ก ํด๋ฐฑํฉ๋๋ค.") |
|
|
|
|
|
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 |
|
|
|
|
|
|
|
|
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) |
|
|
|
|
|
|
|
|
if inspector.has_table('user'): |
|
|
columns = [c['name'] for c in inspector.get_columns('user')] |
|
|
with db.engine.begin() as conn: |
|
|
if 'nickname' not in columns: |
|
|
logger.info("user ํ
์ด๋ธ์ nickname ์ปฌ๋ผ ์ถ๊ฐ ์ค...") |
|
|
conn.execute(text("ALTER TABLE \"user\" ADD COLUMN nickname VARCHAR(80)")) |
|
|
|
|
|
if 'must_change_password' not in columns: |
|
|
logger.info("user ํ
์ด๋ธ์ must_change_password ์ปฌ๋ผ ์ถ๊ฐ ์ค...") |
|
|
|
|
|
conn.execute(text("ALTER TABLE \"user\" ADD COLUMN must_change_password BOOLEAN DEFAULT FALSE NOT NULL")) |
|
|
|
|
|
|
|
|
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")) |
|
|
|
|
|
if 'tags' not in columns: |
|
|
logger.info("uploaded_file ํ
์ด๋ธ์ tags ์ปฌ๋ผ ์ถ๊ฐ ์ค...") |
|
|
conn.execute(text("ALTER TABLE uploaded_file ADD COLUMN tags TEXT")) |
|
|
|
|
|
|
|
|
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")) |
|
|
|
|
|
|
|
|
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)")) |
|
|
|
|
|
|
|
|
if inspector.has_table('chat_message'): |
|
|
columns = [c['name'] for c in inspector.get_columns('chat_message')] |
|
|
with db.engine.begin() as conn: |
|
|
|
|
|
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() |
|
|
|