Tr_Code/app.py

543 lines
22 KiB
Python

import os
from typing import Optional, List, Dict
import httpx
from flask import Flask, g, render_template, request, abort
from urllib.parse import urlencode
from dotenv import load_dotenv
APP_NAME = "1호선 고장코드"
def create_app() -> Flask:
app = Flask(__name__)
app.config.update(TEMPLATES_AUTO_RELOAD=True)
# 환경변수 로드 및 Supabase 기본값 설정
load_dotenv()
# 기본: Kong 프록시(8000) 또는 사용자가 지정한 URL
app.config.setdefault("SUPABASE_URL", os.environ.get("SUPABASE_URL", "http://192.168.0.180:8000"))
app.config.setdefault("SUPABASE_ANON_KEY", os.environ.get("SUPABASE_ANON_KEY", "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzU4NTUxNjY2LCJleHAiOjQxMDI0NDQ4MDB9.jMCGL3Q-N2o_l7JQE_HrO7Uoct86CMgLsVxpabisG4I"))
# Kong Basic Auth(선택)
app.config.setdefault("SUPABASE_BASIC_USER", os.environ.get("SUPABASE_BASIC_USER", ""))
app.config.setdefault("SUPABASE_BASIC_PASSWORD", os.environ.get("SUPABASE_BASIC_PASSWORD", ""))
# 더 이상 SQLite 초기화/연결을 사용하지 않음 (Supabase만 사용)
# PostgREST(REST) 클라이언트 빌더
def build_pg_client() -> httpx.Client:
base = app.config["SUPABASE_URL"].rstrip("/") + "/rest/v1"
basic_user = app.config.get("SUPABASE_BASIC_USER") or ""
basic_pass = app.config.get("SUPABASE_BASIC_PASSWORD") or ""
headers = {
"apikey": app.config.get("SUPABASE_ANON_KEY", ""),
"Accept-Profile": "public",
"Content-Profile": "public",
}
auth = httpx.BasicAuth(basic_user, basic_pass) if basic_user else None
if not auth and app.config.get("SUPABASE_ANON_KEY"):
headers["Authorization"] = f"Bearer {app.config['SUPABASE_ANON_KEY']}"
return httpx.Client(base_url=base, headers=headers, auth=auth, timeout=10)
def pg_unique(col: str) -> List[str]:
with build_pg_client() as c:
r = c.get("/Fault_Code_Table", params={"select": col})
r.raise_for_status()
vals = [row.get(col) for row in (r.json() or []) if row.get(col)]
seen: Dict[str, bool] = {}
out: List[str] = []
for v in vals:
if v not in seen:
seen[v] = True
out.append(v)
return out
def pg_unique_from(table: str, col: str) -> List[str]:
"""지정 테이블에서 고유값 리스트를 반환한다."""
with build_pg_client() as c:
r = c.get(f"/{table}", params={"select": col})
r.raise_for_status()
vals = [row.get(col) for row in (r.json() or []) if row.get(col)]
seen: Dict[str, bool] = {}
out: List[str] = []
for v in vals:
if v not in seen:
seen[v] = True
out.append(v)
return out
@app.route("/")
def index():
return render_template(
"index.html",
app_name=APP_NAME,
)
# 기존 SQLite 기반 라우트 제거됨
@app.route("/modal/close")
def modal_close():
return ""
@app.route("/health")
def health():
return {"status": "ok"}
# 빈 파비콘 응답으로 404 제거
@app.route("/favicon.ico")
def favicon():
return ("", 204, {"Content-Type": "image/x-icon"})
# 브라우저/툴 호환을 위해 루트 경로에도 매니페스트 노출 (정적 파일 경로 사용 권장)
# 더 이상 /api/* 동기화 엔드포인트 제공하지 않음
# ----------------- Supabase 기반 라우트 -----------------
@app.route("/sb/tabs")
def sb_tabs():
try:
with build_pg_client() as c:
r1 = c.get("/Fault_Code_Table", params={"select": "manufacturer"})
r1.raise_for_status()
vals1 = [row.get("manufacturer") for row in (r1.json() or []) if row.get("manufacturer")]
vals2: List[str] = []
for path in ("/Signals", "/signals"):
try:
r2 = c.get(path, params={"select": "manufacturer"})
r2.raise_for_status()
vals2 = [row.get("manufacturer") for row in (r2.json() or []) if row.get("manufacturer")]
break
except httpx.HTTPError:
continue
vals = vals1 + vals2
seen: Dict[str, bool] = {}
manufacturers: List[str] = []
for v in vals:
if v not in seen:
seen[v] = True
manufacturers.append(v)
return render_template("partials/sb_tabs.html", manufacturers=manufacturers)
except Exception as e:
return render_template(
"partials/sb_error.html",
error_message=str(e),
supabase_url=app.config.get("SUPABASE_URL"),
)
@app.route("/sb")
def sb_home():
try:
# ---- MMI 코드 분기 ----
if request.args.get("section", "fault").strip() == "mmicode":
PAGE_SIZE = 50
page = int(request.args.get("page", "0"))
offset = page * PAGE_SIZE
# 셀렉트박스/검색 필드
with build_pg_client() as c:
# 제조사
res = c.get("/MMI_Code", params={"select": "manufacturer"})
res.raise_for_status()
manufacturers = sorted({row.get("manufacturer") for row in (res.json() or []) if row.get("manufacturer")})
# 차량분류(alias_name)
res2 = c.get("/MMI_Code", params={"select": "alias_name"})
res2.raise_for_status()
alias_names = sorted({row.get("alias_name") for row in (res2.json() or []) if row.get("alias_name")})
selected_manufacturer = request.args.get("manufacturer", "").strip()
selected_alias_name = request.args.get("alias_name", "").strip()
q = request.args.get("q", "").strip()
group_code = request.args.get("group_code", "").strip().lower() == "on"
# MMI 쿼리
params = {
"select": "id,code_name,code_description,data_type,car_id,alias_name,manufacturer",
"order": "code_name.asc",
"limit": str(PAGE_SIZE),
"offset": str(offset),
}
if selected_manufacturer:
params["manufacturer"] = f"eq.{selected_manufacturer}"
if selected_alias_name:
params["alias_name"] = f"eq.{selected_alias_name}"
if q:
# 여러 컬럼에 대해 ilike filter
params["or"] = f"(code_name.ilike.*{q}*,code_description.ilike.*{q}*,alias_name.ilike.*{q}*)"
with build_pg_client() as c:
res = c.get("/MMI_Code", params=params)
res.raise_for_status()
rows = res.json() or []
# dedup if group_code on
if group_code:
seen = set()
dedup = []
for r in rows:
code = r.get("code_name")
if code and code not in seen:
seen.add(code)
dedup.append(r)
rows = dedup
query_params = request.args.to_dict()
if "page" in query_params:
del query_params["page"]
query_params_string = urlencode(query_params)
# 템플릿 렌더 및 선택 파라미터 전달
return render_template(
"partials/sb_mmi_list.html",
rows=rows,
manufacturers=manufacturers,
alias_names=alias_names,
selected_manufacturer=selected_manufacturer,
selected_alias_name=selected_alias_name,
q=q,
group_code="on" if group_code else "off",
section="mmicode",
page=page,
page_size=PAGE_SIZE,
query_params_string=query_params_string,
)
# ---- 기존(고장코드/TCMS) ----
mf_fault = pg_unique("manufacturer")
mf_signal = pg_unique_from("Signals", "manufacturer")
seen_mf: Dict[str, bool] = {}
manufacturers: List[str] = []
for v in (mf_fault + mf_signal):
if v and v not in seen_mf:
seen_mf[v] = True
manufacturers.append(v)
devices = pg_unique("device")
car_types = pg_unique("car_type")
car_ids = pg_unique("car_id")
# alias_name 필터는 제작사에 따라 값 제한: 우진/로템/다대 포함
all_alias = pg_unique("alias_name")
selected_manufacturer = request.args.get("manufacturer", "").strip()
def alias_allowed(name: str) -> bool:
if not name:
return False
low = name.lower()
if selected_manufacturer.lower() == "woojin":
return ("우진" in name) or ("woojin" in low)
if selected_manufacturer.lower() == "rotem":
return ("로템" in name) or ("다대" in name) or ("rotem" in low)
return True
alias_names = [a for a in all_alias if alias_allowed(a)]
signal_classifications = pg_unique_from("Signals", "classification")
# 선택값 처리 (쿼리스트링에 manufacturer 키가 있으면 빈 문자열이라도 그대로 유지)
if "manufacturer" in request.args:
selected_manufacturer = (request.args.get("manufacturer") or "").strip()
else:
selected_manufacturer = manufacturers[0] if manufacturers else ""
# 다른 필터의 현재 선택값 유지
selected_device = request.args.get("device", "").strip()
selected_car_type = request.args.get("car_type", "").strip()
selected_alias_name = request.args.get("alias_name", "").strip()
selected_classification = request.args.get("classification", "").strip()
q = request.args.get("q", "").strip()
section = request.args.get("section", "fault").strip() or "fault"
return render_template(
"partials/sb_manufacturer.html",
manufacturers=manufacturers,
devices=devices,
car_types=car_types,
car_ids=car_ids,
alias_names=alias_names,
signal_classifications=signal_classifications,
selected_manufacturer=selected_manufacturer,
selected_device=selected_device,
selected_car_type=selected_car_type,
selected_alias_name=selected_alias_name,
selected_classification=selected_classification,
q=q,
section=section,
)
except Exception as e:
return render_template(
"partials/sb_error.html",
error_message=str(e),
supabase_url=app.config.get("SUPABASE_URL"),
)
@app.route("/sb/faults/list")
def sb_faults_list():
try:
PAGE_SIZE = 50
page = int(request.args.get("page", "0"))
offset = page * PAGE_SIZE
manufacturer = request.args.get("manufacturer", "").strip()
device = request.args.get("device", "").strip()
car_type = request.args.get("car_type", "").strip()
car_id = request.args.get("car_id", "").strip()
alias_name = request.args.get("alias_name", "").strip()
q = request.args.get("q", "").strip()
section = request.args.get("section", "fault").strip()
params: Dict[str, str] = {
"select": "f_code,f_code_num,f_name,manufacturer,device,car_type,car_id,fault_detail,alias_name",
"order": "f_code.asc",
"limit": str(PAGE_SIZE),
"offset": str(offset),
}
if manufacturer:
params["manufacturer"] = f"eq.{manufacturer}"
if device:
params["device"] = f"eq.{device}"
if car_type:
params["car_type"] = f"eq.{car_type}"
if car_id:
params["car_id"] = f"eq.{car_id}"
if alias_name:
params["alias_name"] = f"eq.{alias_name}"
if q:
params["or"] = f"(f_code.ilike.*{q}*,f_name.ilike.*{q}*)"
with build_pg_client() as c:
r = c.get("/Fault_Code_Table", params=params)
r.raise_for_status()
rows = r.json() or []
# 그룹핑 옵션: 같은 f_code를 첫 항목만 남김
group_code = request.args.get("group_code", "").strip().lower()
if group_code == "on":
seen: Dict[str, bool] = {}
dedup: List[Dict] = []
for r in rows:
code = r.get("f_code")
if code and code not in seen:
seen[code] = True
dedup.append(r)
rows = dedup
query_params = request.args.to_dict()
if "page" in query_params:
del query_params["page"]
query_params_string = urlencode(query_params)
return render_template(
"partials/sb_fault_list.html",
rows=rows,
page=page,
page_size=PAGE_SIZE,
query_params_string=query_params_string,
)
except Exception as e:
return render_template(
"partials/sb_error.html",
error_message=str(e),
supabase_url=app.config.get("SUPABASE_URL"),
)
@app.route("/sb/faults/<string:f_code>")
def sb_fault_detail(f_code: str):
try:
params = {
"select": "f_code,f_code_num,f_name,car_type,f_class,grade,device,fault_detail,fault_reaction,fault_detection,fault_clear,fault_action,fault_schematics,car_id,manufacturer",
"f_code": f"eq.{f_code}",
"limit": "1",
}
with build_pg_client() as c:
r = c.get("/Fault_Code_Table", params=params)
r.raise_for_status()
rows = r.json() or []
if not rows:
abort(404)
row = rows[0]
return render_template("partials/sb_fault_detail.html", row=row)
except Exception as e:
return render_template(
"partials/sb_error.html",
error_message=str(e),
supabase_url=app.config.get("SUPABASE_URL"),
)
# ----------------- Signals (TCMS) -----------------
@app.route("/sb/signals/list")
def sb_signals_list():
try:
PAGE_SIZE = 50
page = int(request.args.get("page", "0"))
offset = page * PAGE_SIZE
manufacturer = request.args.get("manufacturer", "").strip()
classification = request.args.get("classification", "").strip()
q = request.args.get("q", "").strip()
alias_name = request.args.get("alias_name", "").strip()
params: Dict[str, str] = {
"select": "id,sig_num,signal_abbreviation,signal_description,status_value,manufacturer,classification,alias_name",
"order": "sig_num.asc",
"limit": str(PAGE_SIZE),
"offset": str(offset),
}
if manufacturer:
params["manufacturer"] = f"eq.{manufacturer}"
if classification:
params["classification"] = f"eq.{classification}"
if alias_name:
params["alias_name"] = f"eq.{alias_name}"
if q:
params["or"] = f"(signal_abbreviation.ilike.*{q}*,signal_description.ilike.*{q}*)"
with build_pg_client() as c:
rows = []
last_error: Exception | None = None
for path in ("/Signals", "/signals"):
try:
r = c.get(path, params=params)
r.raise_for_status()
rows = r.json() or []
last_error = None
break
except Exception as e:
last_error = e
rows = []
continue
if last_error and not rows:
raise last_error
# TCMS도 group_code=on이면 sig_num/dedup(또는 id?)
group_code = request.args.get("group_code", "").strip().lower()
if group_code == "on":
seen: Dict[str, bool] = {}
dedup: List[Dict] = []
for r in rows:
code = r.get("id")
if code and code not in seen:
seen[code] = True
dedup.append(r)
rows = dedup
query_params = request.args.to_dict()
if "page" in query_params:
del query_params["page"]
query_params_string = urlencode(query_params)
return render_template(
"partials/sb_signal_list.html",
rows=rows,
page=page,
page_size=PAGE_SIZE,
query_params_string=query_params_string,
)
except Exception as e:
return render_template(
"partials/sb_error.html",
error_message=str(e),
supabase_url=app.config.get("SUPABASE_URL"),
)
@app.route("/sb/signals/<string:item_id>")
def sb_signal_detail(item_id: str):
try:
params = {
"select": "id,sig_num,signal_abbreviation,signal_description,status_value,manufacturer,classification,alias_name,original_data,created_at,updated_at",
"id": f"eq.{item_id}",
"limit": "1",
}
with build_pg_client() as c:
rows = []
last_error: Exception | None = None
for path in ("/Signals", "/signals"):
try:
r = c.get(path, params=params)
r.raise_for_status()
rows = r.json() or []
last_error = None
break
except Exception as e:
last_error = e
rows = []
continue
if last_error and not rows:
raise last_error
if not rows:
abort(404)
row = rows[0]
return render_template("partials/sb_signal_detail.html", row=row)
except Exception as e:
return render_template(
"partials/sb_error.html",
error_message=str(e),
supabase_url=app.config.get("SUPABASE_URL"),
)
@app.route("/sb/health")
def sb_health():
try:
with build_pg_client() as c:
r = c.get("/Fault_Code_Table", params={"select": "f_code", "limit": "1"})
r.raise_for_status()
return {"sb": "ok", "url": app.config.get("SUPABASE_URL")}
except Exception as e:
return {"sb": "error", "url": app.config.get("SUPABASE_URL"), "error": str(e)}
# 간단한 Signals 테스트 엔드포인트: 상위 5개 레코드를 JSON으로 반환
@app.route("/sb/signals/test")
def sb_signals_test():
try:
params = {
"select": "uuid,sig_num,signal_abbreviation,manufacturer,classification,alias_name",
"order": "sig_num.asc",
"limit": "5",
}
with build_pg_client() as c:
rows = []
used_path = None
last_error = None
for path in ("/Signals", "/signals"):
try:
r = c.get(path, params=params)
r.raise_for_status()
rows = r.json() or []
used_path = path
last_error = None
break
except Exception as e:
last_error = str(e)
rows = []
continue
if last_error and not rows:
return {"ok": False, "url": app.config.get("SUPABASE_URL"), "tried": ["/Signals", "/signals"], "error": last_error}, 500
return {"ok": True, "url": app.config.get("SUPABASE_URL"), "path": used_path, "rows": rows}
except Exception as e:
return {"ok": False, "url": app.config.get("SUPABASE_URL"), "error": str(e)}, 500
# 상세 디버그: 두 경로 각각의 상태/본문을 그대로 반환
@app.route("/sb/signals/debug")
def sb_signals_debug():
try:
out = []
with build_pg_client() as c:
for path in ("/Signals", "/signals"):
try:
r = c.get(path, params={"select": "*", "limit": "5"})
ct = r.headers.get("content-type", "")
body = None
try:
body = r.json()
except Exception:
body = r.text
out.append({
"path": path,
"status": r.status_code,
"content_type": ct,
"body": body,
})
except httpx.HTTPError as e:
out.append({"path": path, "error": str(e)})
return {"ok": True, "url": app.config.get("SUPABASE_URL"), "results": out}
except Exception as e:
return {"ok": False, "url": app.config.get("SUPABASE_URL"), "error": str(e)}, 500
return app
app = create_app()
if __name__ == "__main__":
port = int(os.environ.get("PORT", "5000"))
app.run(host="0.0.0.0", port=port, debug=True)