cc/startup_app/app.py
원본 내용을 읽기 전용으로 표시합니다.
"""세종 상권 살펴보기 API. 운영 환경에서는 waitress와 HTTPS 프록시 사용."""import loggingimport mathimport osfrom pathlib import Pathfrom functools import lru_cachefor key in ("OMP_NUM_THREADS", "OPENBLAS_NUM_THREADS", "MKL_NUM_THREADS"):os.environ.setdefault(key, "1")import numpy as npfrom flask import Flask, jsonify, render_template, requestfrom werkzeug.exceptions import HTTPExceptiontry:from .engine import 엔진except ImportError:from engine import 엔진def 숫자정리(value):if isinstance(value, dict):return {key: 숫자정리(item) for key, item in value.items()}if isinstance(value, (list, tuple)):return [숫자정리(item) for item in value]if isinstance(value, np.generic):value = value.item()return None if isinstance(value, float) and not math.isfinite(value) else value@lru_cache(maxsize=1)def default_engine():return 엔진()def create_app(engine=None):app = Flask(__name__)from .materials import bpapp.config['MATERIALS_DIR'] = Path(__file__).resolve().parents[1] / 'public_materials'app.register_blueprint(bp)app.json.ensure_ascii = Falseapp.config["MAX_CONTENT_LENGTH"] = 16 * 1024if engine is not None:app.extensions["sejong_engine"] = enginedef model():return app.extensions.get("sejong_engine") or default_engine()def parameters(category_required=False):data = request.get_json(silent=True)if not isinstance(data, dict) or isinstance(data.get("lat"), bool) or isinstance(data.get("lon"), bool):return Nonetry:lat, lon = float(data["lat"]), float(data["lon"])except (KeyError, ValueError, TypeError, OverflowError):return Noneif not model().위치확인(lat, lon):return Nonecategory = data.get("mid")if category_required and (not isinstance(category, str) or category not in model().mid_to_cat):return Nonereturn round(lat, 6), round(lon, 6), category@app.get("/")def index():return render_template("index.html")@app.get("/healthz")def health():current = model()return jsonify({"status": "ok", "rows": len(current.now), "model_count": len(current.모델), "version": "2"})@app.get("/api/meta")def metadata():response = jsonify(숫자정리(model().메타()))response.headers["Cache-Control"] = "public, max-age=300"return response@app.get("/api/boundary")def boundary():return jsonify(model().boundary or {"type": "FeatureCollection", "features": []})@app.post("/api/analyze")def analyze():values = parameters(True)if values is None:return jsonify({"오류": "세종시 경계 안의 위치와 목록에 있는 업종을 골라 주세요."}), 400return jsonify(숫자정리(model().분석(*values)))@app.post("/api/recommend")def compare_categories():values = parameters()if values is None:return jsonify({"오류": "세종시 경계 안의 위치를 골라 주세요."}), 400return jsonify(숫자정리(model().업종추천(values[0], values[1])))@app.after_requestdef headers(response):response.headers.update({"X-Content-Type-Options": "nosniff", "Referrer-Policy": "strict-origin-when-cross-origin", "X-Frame-Options": "SAMEORIGIN"})response.headers.setdefault("Content-Security-Policy", "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https://*.tile.openstreetmap.org https://*.tile.openstreetmap.fr; connect-src 'self'; frame-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'self'; form-action 'self'")if request.path.startswith("/api/") and request.method == "POST":response.headers["Cache-Control"] = "no-store"return response@app.errorhandler(HTTPException)def http_error(error):messages = {404: "요청한 주소가 없습니다.", 405: "지원하지 않는 요청 방식입니다.", 413: "요청 내용이 너무 큽니다."}return jsonify({"오류": messages.get(error.code, "요청을 처리하지 못했습니다.")}), error.code@app.errorhandler(Exception)def unexpected(error):logging.getLogger(__name__).exception("요청 처리 실패")return jsonify({"오류": "일시적으로 분석하지 못했습니다. 잠시 후 다시 시도해 주세요."}), 500return appapp = create_app()if __name__ == "__main__":from waitress import servelogging.basicConfig(level=logging.INFO)default_engine()serve(app, host=os.environ.get("HOST", "127.0.0.1"), port=int(os.environ.get("PORT", "5000")), threads=4)
파일 확인 정보
SHA-256: 93264d6286c5e7a0559efa06b35c6b587c47564f1ff3c966a671aab3a969f133