refactor: promote 8081 design system and served app structure

This commit is contained in:
hyunho
2026-04-01 14:50:08 +09:00
parent 637b390024
commit d0e055973e
50 changed files with 24157 additions and 820 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_LEDGER_SERVED_DIR = INCOMING_SERVED_DIR / "ledger"
BUSINESS_LEDGER_INDEX_PATH = BUSINESS_LEDGER_SERVED_DIR / "index.html"
FIXED_OFFICE_SOURCE_KEY = "technical-development-center"
FIXED_OFFICE_CONFIGS = {
"technical-development-center": {
@@ -61,6 +66,7 @@ FIXED_OFFICE_CONFIGS = {
},
}
_fixed_office_cache: dict[str, dict[str, object]] = {}
BUSINESS_LEDGER_DEFAULT_SOURCE_KEY = "business_ledger_default"
AUTH_DEFAULT_PASSWORD = "1111"
AUTH_PASSWORD_ITERATIONS = 390000
AUTH_SESSION_HOURS = 12
@@ -82,6 +88,66 @@ MH_HEADER_ORDER = [
"사업 종류", "연장근무 프로젝트 코드", "연장근무 프로젝트명", "연장근무 서브코드", "연장근무 시간(실제)", "연장근무 시간(가공)"
]
def sync_default_business_ledger_source(cur) -> None:
cur.execute("SELECT to_regclass('public.integration_binary_sources') IS NOT NULL AS table_exists")
row = cur.fetchone()
table_exists = bool(row["table_exists"]) if row is not None else False
if not table_exists:
return
candidates = [
BUSINESS_LEDGER_SERVED_DIR / "사업관리대장-1.xlsx",
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_LEDGER_SERVED_DIR), check_dir=False),
name="integration-ledger-assets",
)
class MemberPayload(BaseModel):
id: int | None = None
@@ -3910,6 +3976,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 +4006,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 +4598,30 @@ 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() -> FileResponse:
# #21 phase-1: runtime no longer decodes reference wrapper HTML. Serve the promoted
# ledger entry file from incoming-files/served/ledger only.
target = BUSINESS_LEDGER_INDEX_PATH
if not target.exists():
raise HTTPException(status_code=404, detail="Business ledger integration file not found.")
response = FileResponse(target)
response.headers["Cache-Control"] = "no-store, no-cache, must-revalidate, max-age=0"
response.headers["Pragma"] = "no-cache"
return response
@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)