feat: unify 8081 dashboard design system and views

This commit is contained in:
hyunho
2026-04-01 14:02:05 +09:00
parent 637b390024
commit fb5b0f00c2
27 changed files with 12596 additions and 818 deletions

View File

@@ -21,7 +21,7 @@ import ezdxf
from ezdxf import recover
from fastapi import FastAPI, File, Form, Header, HTTPException, Request, UploadFile
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse, HTMLResponse
from fastapi.responses import FileResponse, HTMLResponse, Response
from fastapi.staticfiles import StaticFiles
from openpyxl import load_workbook
from pydantic import BaseModel, Field
@@ -42,6 +42,11 @@ app.add_middleware(
LEGACY_STATIC_DIR = LEGACY_DIR / "static"
INCOMING_FILES_DIR = BASE_DIR / "incoming-files"
INCOMING_SERVED_DIR = INCOMING_FILES_DIR / "served"
INCOMING_REFERENCE_DIR = INCOMING_FILES_DIR / "reference"
BUSINESS_DASHBOARD_DIR = INCOMING_FILES_DIR / "사업관리대장"
BUSINESS_DASHBOARD_WRAPPER_PATH = BUSINESS_DASHBOARD_DIR / "MH 통합 대시보드_260320.html"
BUSINESS_DASHBOARD_THEME_CSS = BUSINESS_DASHBOARD_DIR / "MH 통합 대시보드_260320.css"
FIXED_OFFICE_SOURCE_KEY = "technical-development-center"
FIXED_OFFICE_CONFIGS = {
"technical-development-center": {
@@ -61,6 +66,8 @@ FIXED_OFFICE_CONFIGS = {
},
}
_fixed_office_cache: dict[str, dict[str, object]] = {}
_business_ledger_html_cache: str | None = None
BUSINESS_LEDGER_DEFAULT_SOURCE_KEY = "business_ledger_default"
AUTH_DEFAULT_PASSWORD = "1111"
AUTH_PASSWORD_ITERATIONS = 390000
AUTH_SESSION_HOURS = 12
@@ -83,6 +90,86 @@ MH_HEADER_ORDER = [
]
def build_business_ledger_html() -> str:
global _business_ledger_html_cache
if _business_ledger_html_cache is not None:
return _business_ledger_html_cache
if not BUSINESS_DASHBOARD_WRAPPER_PATH.exists():
raise FileNotFoundError("Business dashboard wrapper file not found.")
source = BUSINESS_DASHBOARD_WRAPPER_PATH.read_text(encoding="utf-8-sig")
match = re.search(r"const BUSINESS_HTML_B64='([^']+)';", source)
if not match:
raise ValueError("Embedded business ledger source was not found.")
decoded = base64.b64decode(match.group(1)).decode("utf-8")
head_injection = (
'<base href="/integrations/ledger-assets/">'
'<link rel="stylesheet" href="/integrations/ledger-assets/MH%20통합%20대시보드_260320.css">'
'<link rel="stylesheet" href="/integrations/ledger-assets/ledger-override.css?v=20260401-03">'
)
html = decoded.replace("</head>", f"{head_injection}</head>", 1)
html = html.replace("<body>", '<body class="mh-business-theme">', 1)
html = html.replace("</body>", '<script src="/integrations/ledger-assets/ledger-override.js?v=20260401-03"></script></body>', 1)
_business_ledger_html_cache = html
return html
def sync_default_business_ledger_source(cur) -> None:
if not BUSINESS_DASHBOARD_DIR.exists():
return
candidates = [
BUSINESS_DASHBOARD_DIR / "사업관리대장-1.xlsx",
BUSINESS_DASHBOARD_DIR / "사업관리 대장-1.xlsx",
BUSINESS_DASHBOARD_DIR / "사업관리대장.xlsx",
BUSINESS_DASHBOARD_DIR / "사업관리 대장.xlsx",
]
source_path = next((candidate for candidate in candidates if candidate.exists()), None)
if source_path is None:
return
content = source_path.read_bytes()
content_sha256 = hashlib.sha256(content).hexdigest()
meta_json = {
"byte_size": len(content),
"source_path": str(source_path),
"synced_from": "startup",
}
cur.execute(
"""
INSERT INTO integration_binary_sources (
source_key, source_name, filename, mime_type, content, content_sha256, meta_json, imported_at
)
VALUES (%s, %s, %s, %s, %s, %s, %s::jsonb, NOW())
ON CONFLICT (source_key) DO UPDATE
SET source_name = EXCLUDED.source_name,
filename = EXCLUDED.filename,
mime_type = EXCLUDED.mime_type,
content = EXCLUDED.content,
content_sha256 = EXCLUDED.content_sha256,
meta_json = EXCLUDED.meta_json,
imported_at = NOW()
WHERE integration_binary_sources.content_sha256 IS DISTINCT FROM EXCLUDED.content_sha256
OR integration_binary_sources.filename IS DISTINCT FROM EXCLUDED.filename
OR integration_binary_sources.mime_type IS DISTINCT FROM EXCLUDED.mime_type
OR integration_binary_sources.meta_json IS DISTINCT FROM EXCLUDED.meta_json
""",
(
BUSINESS_LEDGER_DEFAULT_SOURCE_KEY,
"사업관리대장 기본 원본",
source_path.name,
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
content,
content_sha256,
json.dumps(meta_json, ensure_ascii=False),
),
)
app.mount(
"/integrations/ledger-assets",
StaticFiles(directory=str(BUSINESS_DASHBOARD_DIR), check_dir=False),
name="integration-ledger-assets",
)
class MemberPayload(BaseModel):
id: int | None = None
name: str = Field(min_length=1)
@@ -3910,6 +3997,7 @@ def startup() -> None:
init_db()
with get_conn() as conn:
with conn.cursor() as cur:
sync_default_business_ledger_source(cur)
sync_auth_users_from_members(cur)
conn.commit()
@@ -3939,6 +4027,37 @@ def health() -> dict[str, object]:
}
@app.get("/api/integration/business-ledger-default")
def integration_business_ledger_default() -> Response:
with get_conn() as conn:
with conn.cursor() as cur:
cur.execute(
"""
SELECT filename, mime_type, content
FROM integration_binary_sources
WHERE source_key = %s
ORDER BY imported_at DESC
LIMIT 1
""",
(BUSINESS_LEDGER_DEFAULT_SOURCE_KEY,),
)
row = cur.fetchone()
if not row:
raise HTTPException(status_code=404, detail="Business ledger default source not found.")
filename = str(row["filename"] or "사업관리대장-1.xlsx")
headers = {
"Content-Disposition": 'inline; filename="business-ledger-default.xlsx"',
"X-Source-Filename": "business-ledger-default.xlsx",
"Cache-Control": "no-store, no-cache, must-revalidate, max-age=0",
"Pragma": "no-cache",
}
return Response(
content=bytes(row["content"]),
media_type=str(row["mime_type"] or "application/octet-stream"),
headers=headers,
)
@app.post("/api/auth/login")
def auth_login(
request: Request,
@@ -4500,15 +4619,34 @@ def legacy_organization_backup() -> FileResponse:
@app.get("/integrations/payment")
def integration_payment() -> FileResponse:
target = INCOMING_FILES_DIR / "payment.html"
# 8081 phase-1 cleanup: integration HTML is served only from incoming-files/served.
target = INCOMING_SERVED_DIR / "payment.html"
if not target.exists():
raise HTTPException(status_code=404, detail="Payment integration file not found.")
return FileResponse(target)
@app.get("/integrations/ledger")
def integration_ledger() -> HTMLResponse:
try:
html = build_business_ledger_html()
except FileNotFoundError:
raise HTTPException(status_code=404, detail="Business ledger integration file not found.")
except ValueError:
raise HTTPException(status_code=500, detail="Business ledger integration source is invalid.")
return HTMLResponse(
html,
headers={
"Cache-Control": "no-store, no-cache, must-revalidate, max-age=0",
"Pragma": "no-cache",
},
)
@app.get("/integrations/mh")
def integration_mh() -> FileResponse:
target = INCOMING_FILES_DIR / "mh.html"
# Keep the served path explicit so comparison/reference copies are never picked up by accident.
target = INCOMING_SERVED_DIR / "mh.html"
if not target.exists():
raise HTTPException(status_code=404, detail="MH integration file not found.")
return FileResponse(target)