From d0e055973e88809cf60baa7c3f756c4ae5e52c9d Mon Sep 17 00:00:00 2001 From: hyunho Date: Wed, 1 Apr 2026 14:50:08 +0900 Subject: [PATCH] refactor: promote 8081 design system and served app structure --- backend/app/main.py | 119 +- docs/DEV_PROD_DB_PROTOCOL.md | 2 +- docs/NEXT_SESSION_CHECKPOINT.md | 275 +- docs/WORK_EXECUTION_FLOW.md | 10 + docs/WORK_RULEBOOK.md | 20 + docs/architecture/8081_SERVING_MAP.md | 112 + docs/architecture/DESIGN_SSOT.md | 129 + frontend/apps/ledger/README.md | 26 + .../apps/ledger/assets}/MH 통합 대시보드_260320.css | 0 .../apps/ledger/assets/ledger-override.css | 328 ++ .../apps/ledger/assets/ledger-override.js | 498 +++ frontend/apps/ledger/index.html | 954 +++++ frontend/apps/payment/README.md | 18 + frontend/apps/payment/index.html | 1622 ++++++++ frontend/apps/team/README.md | 18 + frontend/apps/team/index.html | 3472 +++++++++++++++++ frontend/public/app.js | 71 +- frontend/public/design-patterns.css | 730 ++++ frontend/public/design-tokens.css | 60 + frontend/public/index.html | 17 +- frontend/public/styles-8081-design.css | 100 + frontend/public/styles.css | 145 +- incoming-files/README.md | 38 + incoming-files/mh.html | 668 ++-- incoming-files/payment.html | 250 +- incoming-files/reference/README.md | 25 + .../ledger/MH 통합 대시보드_260320.css | 1377 +++++++ .../ledger}/MH 통합 대시보드_260320.html | 0 .../reference/ledger/ledger-override.css | 328 ++ .../reference/ledger/ledger-override.js | 498 +++ .../ledger}/사업관리대장-1.xlsx | Bin .../사업관리대장/MH 통합 대시보드_260320.css | 1377 +++++++ .../사업관리대장/MH 통합 대시보드_260320.html | 2598 ++++++++++++ .../ledger/사업관리대장/사업관리대장-1.xlsx | Bin 0 -> 939001 bytes incoming-files/served/README.md | 23 + .../served/ledger/MH 통합 대시보드_260320.css | 1377 +++++++ incoming-files/served/ledger/README.md | 21 + incoming-files/served/ledger/index.html | 954 +++++ .../served/ledger/ledger-override.css | 328 ++ .../served/ledger/ledger-override.js | 498 +++ .../served/ledger/사업관리대장-1.xlsx | Bin 0 -> 939001 bytes incoming-files/served/mh.html | 3472 +++++++++++++++++ incoming-files/served/payment.html | 1622 ++++++++ legacy/static/common.css | 77 +- legacy/static/organization.css | 553 ++- legacy/static/organization.js | 117 +- scripts/prepare_dev_worktree.sh | 2 +- scripts/publish_ledger_app.sh | 22 + scripts/publish_payment_app.sh | 13 + scripts/publish_team_app.sh | 13 + 50 files changed, 24157 insertions(+), 820 deletions(-) create mode 100644 docs/architecture/8081_SERVING_MAP.md create mode 100644 docs/architecture/DESIGN_SSOT.md create mode 100644 frontend/apps/ledger/README.md rename {incoming-files/사업관리대장 => frontend/apps/ledger/assets}/MH 통합 대시보드_260320.css (100%) create mode 100644 frontend/apps/ledger/assets/ledger-override.css create mode 100644 frontend/apps/ledger/assets/ledger-override.js create mode 100644 frontend/apps/ledger/index.html create mode 100644 frontend/apps/payment/README.md create mode 100644 frontend/apps/payment/index.html create mode 100644 frontend/apps/team/README.md create mode 100644 frontend/apps/team/index.html create mode 100644 frontend/public/design-patterns.css create mode 100644 frontend/public/design-tokens.css create mode 100644 frontend/public/styles-8081-design.css create mode 100644 incoming-files/README.md create mode 100644 incoming-files/reference/README.md create mode 100644 incoming-files/reference/ledger/MH 통합 대시보드_260320.css rename incoming-files/{사업관리대장 => reference/ledger}/MH 통합 대시보드_260320.html (100%) create mode 100644 incoming-files/reference/ledger/ledger-override.css create mode 100644 incoming-files/reference/ledger/ledger-override.js rename incoming-files/{사업관리대장 => reference/ledger}/사업관리대장-1.xlsx (100%) create mode 100644 incoming-files/reference/ledger/사업관리대장/MH 통합 대시보드_260320.css create mode 100644 incoming-files/reference/ledger/사업관리대장/MH 통합 대시보드_260320.html create mode 100644 incoming-files/reference/ledger/사업관리대장/사업관리대장-1.xlsx create mode 100644 incoming-files/served/README.md create mode 100644 incoming-files/served/ledger/MH 통합 대시보드_260320.css create mode 100644 incoming-files/served/ledger/README.md create mode 100644 incoming-files/served/ledger/index.html create mode 100644 incoming-files/served/ledger/ledger-override.css create mode 100644 incoming-files/served/ledger/ledger-override.js create mode 100644 incoming-files/served/ledger/사업관리대장-1.xlsx create mode 100644 incoming-files/served/mh.html create mode 100644 incoming-files/served/payment.html create mode 100755 scripts/publish_ledger_app.sh create mode 100755 scripts/publish_payment_app.sh create mode 100755 scripts/publish_team_app.sh diff --git a/backend/app/main.py b/backend/app/main.py index 6b086a4..c2c9597 100755 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -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) diff --git a/docs/DEV_PROD_DB_PROTOCOL.md b/docs/DEV_PROD_DB_PROTOCOL.md index 27024b3..b1b1128 100644 --- a/docs/DEV_PROD_DB_PROTOCOL.md +++ b/docs/DEV_PROD_DB_PROTOCOL.md @@ -187,7 +187,7 @@ docker compose -p mh-dashboard-organization-dev --env-file .env -f docker-compos - 로컬 전용 디자인 참고 자산 복사 - `incoming-files/sample style.css` - `incoming-files/260320.html` - - `incoming-files/사업관리대장/` + - `incoming-files/reference/ledger/` - `incoming-files/1.png` - `incoming-files/seat/center_chair_people_map(2).html` diff --git a/docs/NEXT_SESSION_CHECKPOINT.md b/docs/NEXT_SESSION_CHECKPOINT.md index 3623c45..03bae81 100644 --- a/docs/NEXT_SESSION_CHECKPOINT.md +++ b/docs/NEXT_SESSION_CHECKPOINT.md @@ -2,193 +2,152 @@ ## Current Base -- branch: `total` -- latest checked commit: `24852d4` -- main history doc: [DEVELOPMENT_HISTORY.md](/home/hyunho/projects/mh-dashboard-organization/docs/DEVELOPMENT_HISTORY.md) -- work rulebook: [WORK_RULEBOOK.md](/home/hyunho/projects/mh-dashboard-organization/docs/WORK_RULEBOOK.md) -- execution flow: [WORK_EXECUTION_FLOW.md](/home/hyunho/projects/mh-dashboard-organization/docs/WORK_EXECUTION_FLOW.md) -- dev/prod protocol: [DEV_PROD_DB_PROTOCOL.md](/home/hyunho/projects/mh-dashboard-organization/docs/DEV_PROD_DB_PROTOCOL.md) -- regression checklist: [REGRESSION_CHECKLIST.md](/home/hyunho/projects/mh-dashboard-organization/docs/REGRESSION_CHECKLIST.md) -- today prep note: [TODAY_WORK_PREP_2026-03-30.md](/home/hyunho/projects/mh-dashboard-organization/docs/TODAY_WORK_PREP_2026-03-30.md) +- `8080` 공개 기준 브랜치: `total` +- `8081` 작업 기준 브랜치: `work-8081` +- `8080` 공개 기준 커밋: `637b390` +- `8081` worktree 경로: `/home/hyunho/projects/mh-dashboard-organization/.dev-worktree-8081` +- `8081` 실제 서빙 책임 맵: [architecture/8081_SERVING_MAP.md](/home/hyunho/projects/mh-dashboard-organization/.dev-worktree-8081/docs/architecture/8081_SERVING_MAP.md) +- 메인 히스토리: [DEVELOPMENT_HISTORY.md](/home/hyunho/projects/mh-dashboard-organization/.dev-worktree-8081/docs/DEVELOPMENT_HISTORY.md) +- 작업 룰북: [WORK_RULEBOOK.md](/home/hyunho/projects/mh-dashboard-organization/.dev-worktree-8081/docs/WORK_RULEBOOK.md) +- 실행 플로우: [WORK_EXECUTION_FLOW.md](/home/hyunho/projects/mh-dashboard-organization/.dev-worktree-8081/docs/WORK_EXECUTION_FLOW.md) +- dev/prod DB 프로토콜: [DEV_PROD_DB_PROTOCOL.md](/home/hyunho/projects/mh-dashboard-organization/.dev-worktree-8081/docs/DEV_PROD_DB_PROTOCOL.md) +- 회귀 체크리스트: [REGRESSION_CHECKLIST.md](/home/hyunho/projects/mh-dashboard-organization/.dev-worktree-8081/docs/REGRESSION_CHECKLIST.md) ## Mandatory Start Rule -매일 아침 또는 그날의 첫 작업을 시작할 때는 코드를 수정하기 전에 반드시 아래 순서를 먼저 수행해야 한다. +당일 첫 작업 전에는 아래 순서를 먼저 확인한다. -1. Gitea 브랜치 상태 확인 +1. 브랜치 기준 확인 2. 열린 이슈 확인 -3. [WORK_RULEBOOK.md](/home/hyunho/projects/mh-dashboard-organization/docs/WORK_RULEBOOK.md) 확인 +3. [WORK_RULEBOOK.md](/home/hyunho/projects/mh-dashboard-organization/.dev-worktree-8081/docs/WORK_RULEBOOK.md) 확인 4. 이 문서 확인 -5. 현재 워크트리의 미푸시 커밋, 변경 파일, 미추적 파일 확인 +5. `git status`, 변경 파일, 미추적 파일 확인 주의: -- 위 절차를 확인하기 전에는 새 코드 작성이나 기존 코드 수정부터 시작하지 않는다. -- 커밋과 푸시는 자동으로 하지 않고, 사용자 지시가 있을 때만 수행한다. +- `8080` 기준 코드는 직접 수정하지 않는다. +- 새 작업은 항상 `.dev-worktree-8081`에서 진행한다. +- 커밋과 푸시는 사용자 지시가 있을 때만 수행한다. -## What Was Finished +## Confirmed Runtime Rule -### Dashboard Integration +- `8080`은 루트 workspace의 `total` 기준으로 유지한다. +- `8081`은 `.dev-worktree-8081` + `work-8081` 기준으로만 수정한다. +- `main`, `hyunho`는 보류 브랜치이며 현재 작업에 사용하지 않는다. +- `8081` 변경을 `8080`에 올릴 때는 reviewed file diff 기준으로만 반영한다. +- `8081` DB는 운영 정본이 아니라 `8080` 기준 검증용 복제본처럼 다룬다. -- `조직 현황`, `프로젝트별 분석`, `팀/개인별 분석`, `자리배치도`를 하나의 허브에 통합 -- `payment.html`, `mh.html`을 현재 프로젝트에 편입 -- 공통 헤더, 탭, 로그인 정보, 공통 기간 제어 구성 +## What Was Stabilized -### Integrated DB +### Branch / Worktree Safety -- `organization.xlsx`, `MH.xlsx`, `payment.csv`, `ptj.csv` 기반 통합 DB 구성 -- raw/staging/standard 성격의 구조를 PostgreSQL에 반영 -- `members`, `seat_maps`, `seat_slots`, `seat_positions` -- `integration_raw_*`, `integration_work_logs`, `integration_work_log_segments`, `integration_vouchers` -- 프로젝트 카테고리 매핑 반영 +- 기존 `8081` 작업본은 [`.dev-worktree-8081-backup-2026-04-01`](/home/hyunho/projects/mh-dashboard-organization/.dev-worktree-8081-backup-2026-04-01)로 보존 +- 현재 [`.dev-worktree-8081`](/home/hyunho/projects/mh-dashboard-organization/.dev-worktree-8081)는 `work-8081` 기준으로 재생성 +- `8080` 루트 workspace는 그대로 두고 분리 운영 -### Team / Member Analysis +### 8081 Design / Serving Baseline -- `omh.html` 원본 기준으로 계산식/카테고리/디자인 복원 -- DB raw MH 데이터를 원본 입력 구조처럼 다시 공급하는 방식으로 정리 +- 디자인 SSOT 토큰: + - [frontend/public/design-tokens.css](/home/hyunho/projects/mh-dashboard-organization/.dev-worktree-8081/frontend/public/design-tokens.css) +- 디자인 SSOT 패턴: + - [frontend/public/design-patterns.css](/home/hyunho/projects/mh-dashboard-organization/.dev-worktree-8081/frontend/public/design-patterns.css) +- 디자인 기준 문서: + - [architecture/DESIGN_SSOT.md](/home/hyunho/projects/mh-dashboard-organization/.dev-worktree-8081/docs/architecture/DESIGN_SSOT.md) +- 로그인 기본 스타일은 [frontend/public/styles.css](/home/hyunho/projects/mh-dashboard-organization/.dev-worktree-8081/frontend/public/styles.css) 기준으로 유지 +- `8081` 허브 전용 디자인은 [frontend/public/styles-8081-design.css](/home/hyunho/projects/mh-dashboard-organization/.dev-worktree-8081/frontend/public/styles-8081-design.css)에서만 덮어씀 +- 조직현황은 [legacy/static/common.css](/home/hyunho/projects/mh-dashboard-organization/.dev-worktree-8081/legacy/static/common.css), [legacy/static/organization.css](/home/hyunho/projects/mh-dashboard-organization/.dev-worktree-8081/legacy/static/organization.css), [legacy/static/organization.js](/home/hyunho/projects/mh-dashboard-organization/.dev-worktree-8081/legacy/static/organization.js)를 사용 +- 프로젝트별 분석 디자인은 [incoming-files/served/payment.html](/home/hyunho/projects/mh-dashboard-organization/.dev-worktree-8081/incoming-files/served/payment.html) 내부에서 `design-tokens.css` + `design-patterns.css`를 참조 +- 프로젝트별 분석 수정 원본은 [frontend/apps/payment/index.html](/home/hyunho/projects/mh-dashboard-organization/.dev-worktree-8081/frontend/apps/payment/index.html) 이고, 반영은 [scripts/publish_payment_app.sh](/home/hyunho/projects/mh-dashboard-organization/.dev-worktree-8081/scripts/publish_payment_app.sh)로 한다. +- 팀/개인별 분석 수정 원본은 [frontend/apps/team/index.html](/home/hyunho/projects/mh-dashboard-organization/.dev-worktree-8081/frontend/apps/team/index.html) 이고, 반영은 [scripts/publish_team_app.sh](/home/hyunho/projects/mh-dashboard-organization/.dev-worktree-8081/scripts/publish_team_app.sh)로 한다. +- 사업관리대장 실제 서비스 코드는 [incoming-files/served/ledger](/home/hyunho/projects/mh-dashboard-organization/.dev-worktree-8081/incoming-files/served/ledger) 기준으로 본다. +- 사업관리대장 앱 소스 기준은 [frontend/apps/ledger](/home/hyunho/projects/mh-dashboard-organization/.dev-worktree-8081/frontend/apps/ledger) 이고, 반영은 [scripts/publish_ledger_app.sh](/home/hyunho/projects/mh-dashboard-organization/.dev-worktree-8081/scripts/publish_ledger_app.sh)로 한다. +- 사업관리대장 상세 팝업 디자인 수정 원본은 [frontend/apps/ledger/assets/ledger-override.js](/home/hyunho/projects/mh-dashboard-organization/.dev-worktree-8081/frontend/apps/ledger/assets/ledger-override.js) 기준으로 본다. -### Project Analysis +디자인 수정 우선순위: -- `opayment.html` 원본 기준으로 화면 복원 -- `payment.csv` 분류 우선, `ptj.csv` fallback 적용 -- 연장근무는 `연장근무 시간(가공)` 기준으로 반영 - -### Organization / Seat Map - -- 조직도 상세 프로필에 `재석위치` preview 연결 -- 관리자/비관리자 자리배치도 화면 분리 -- 저장 후 조직도와 비관리자 열람에 반영되도록 seat save 흐름 정리 -- seat persistence bug 수정 - - 원인: `seat_positions_map_cell_idx`가 slot 기반 도면에도 적용됨 - - 조치: `seat_slot_id IS NULL`인 grid map에만 적용되도록 수정 - -### Member Data Governance - -- 이름 alias, 퇴사 제외, 조직 override를 DB 테이블 기반으로 전환 -- 사용 테이블: - - `member_aliases` - - `member_retirements` - - `member_overrides` - -### Auth Baseline - -- 실제 로그인 API 연결 완료 -- 프런트 로그인 화면이 `/api/auth/login` 사용 -- 세션/로그아웃/세션 조회 API 구성 완료 -- 사용 테이블: - - `auth.users` - - `auth.sessions` - - `auth.login_audit_logs` -- 현재 남은 범위: - - mock login 정리 - - 역할별 권한 체크 적용 - - 쓰기 API 보호 범위 정리 - -### External Access - -- WSL 내부 8080 리슨 확인 -- 현재 다른 PC에서 접속 확인 -- 현재 기준 주소: - - `http://172.16.40.144:8080` - -## Important Runtime Notes - -### Dev / Prod Protocol - -- 코드 선행은 `8081`, 공개 반영은 `8080` -- 데이터 정본은 `8080` DB -- `8081` DB는 독립 정본이 아니라 `8080` 기준 복제본처럼 관리해야 함 -- `8081` 코드는 `.dev-worktree-8081` 기준으로 유지 -- 조직도, 멤버, 자리배치 검증 전에는 `DEV_PROD_DB_PROTOCOL.md`를 먼저 확인 -- 기능 수정 후 완료 판단은 `REGRESSION_CHECKLIST.md`를 기준으로 해야 함 -- 빠른 재시작은 `./scripts/start_local_dashboards.sh` - -### Seat Map Save - -- 저장이 안 되면 먼저 backend 로그에서 `PUT /api/seat-maps/{id}/layout` 상태코드 확인 -- 과거 핵심 장애는 DB 인덱스 충돌이었다 -- 현재 저장 구조는: - - `seat_positions` - - `members.seat_label` - 둘 다 같이 갱신 - -### External Access - -- Windows LAN IP가 바뀌면 접속 주소가 바뀔 수 있음 -- WSL IP가 바뀌면 `portproxy connectaddress`를 다시 맞춰야 함 -- 다음 확인 명령: - - Windows: `ipconfig` - - WSL: `hostname -I` - - Windows: `netsh interface portproxy show all` - -## Open Issues - -- `#2` 백엔드 영속 저장 구조 운영 마무리 -- `#3` 사무실 좌석 배치도 조회 및 관리자 편집 기능 고도화 -- `#5` 실제 인증 체계 전환 -- `#7` 자리배치도 팀별 색상 오버레이 표시 -- `#8` 자리배치도 좌석 클릭 시 개인 상위 조직 트리 표시 -- `#9` 조직도·자리배치도 변경 이력 버전 누적 저장 - -현재 해석: -- `#6`은 코드 기준 사실상 완료 상태이며 Gitea 정리 대상 -- `#5`는 "로그인 구현"보다 "권한 제어 마무리"가 핵심 -- `#2`의 기존 "스냅샷 검증" 범위는 현재 코드와 불일치하므로 범위 재정의 필요 - -## Unfinished Ideas Discussed Today - -### Seat Map UX - -- 자리배치도 내 인원 등록 시 팀별 색상 표시 -- 좌석 클릭 시 본인까지의 상위 조직 트리 표시 -- 나머지 사무실 2개 도면 추가 - - `한맥빌딩 7층` - - `한맥빌딩 6층` -- 비관리자 열람 화면 품질 추가 점검 - -### History / Versioning - -- 조직도와 자리배치도 수정 이력을 버전 누적형으로 저장 -- 원본 DB와 별도의 history/version 구조 설계 -- `valid_from`, `valid_to` 기반 시점 조회(as-of date) 구조 적용 -- 날짜 또는 revision label 기준으로 버전 묶음 관리 -- 상세 설계 문서: - - [HISTORY_ASOF_DB_PLAN.md](/home/hyunho/projects/mh-dashboard-organization/docs/HISTORY_ASOF_DB_PLAN.md) +1. [frontend/public/design-tokens.css](/home/hyunho/projects/mh-dashboard-organization/.dev-worktree-8081/frontend/public/design-tokens.css) +2. [frontend/public/design-patterns.css](/home/hyunho/projects/mh-dashboard-organization/.dev-worktree-8081/frontend/public/design-patterns.css) +3. 화면별 실제 서빙 파일 주의: -- 현재 코드에는 조직도/자리배치도 버전 이력 기능이 아직 없음 -- 월간 스냅샷 방향은 범위에서 제외 -### Project Analysis Accuracy +- `incoming-files/sample style.css`는 참고 기준이지만 직접 런타임 수정 파일이 아니다. +- `incoming-files` 원본/reference 파일을 먼저 고치지 않는다. +- 새 디자인 수정은 먼저 토큰/패턴 파일에서 해결 가능한지 확인한 뒤, 불가피할 때만 화면별 파일에 내린다. -- 총합은 거의 맞았지만 일부 프로젝트 단위 소수점/분류 오차는 추가 정밀 보정 필요 -- `opayment` 기준으로 특정 프로젝트 차이를 계속 줄여야 함 +### 1차 구조 정리 진행분 -### Auth / Permission +- 이슈 기준: + - `#14` 전체 구조 정리 umbrella + - `#18` 1차: 파일 책임 맵 정리 및 프런트 서빙 경로 정돈 + - `#19` 2차: 백엔드 라우터/서빙 책임 분리 + - `#20` 3차: worktree/스크립트/문서 정리 +- 책임 맵 문서 추가: + - [architecture/8081_SERVING_MAP.md](/home/hyunho/projects/mh-dashboard-organization/.dev-worktree-8081/docs/architecture/8081_SERVING_MAP.md) +- `/integrations/payment`, `/integrations/mh`의 실제 서빙 파일을 분리: + - [incoming-files/served/payment.html](/home/hyunho/projects/mh-dashboard-organization/.dev-worktree-8081/incoming-files/served/payment.html) + - [incoming-files/served/mh.html](/home/hyunho/projects/mh-dashboard-organization/.dev-worktree-8081/incoming-files/served/mh.html) +- 기존 [incoming-files/payment.html](/home/hyunho/projects/mh-dashboard-organization/.dev-worktree-8081/incoming-files/payment.html), [incoming-files/mh.html](/home/hyunho/projects/mh-dashboard-organization/.dev-worktree-8081/incoming-files/mh.html)은 비교/복구용 복사본으로 당분간 유지 +- backend 서빙 경로는 [backend/app/main.py](/home/hyunho/projects/mh-dashboard-organization/.dev-worktree-8081/backend/app/main.py)에서 `incoming-files/served/*`를 보도록 정리 시작 -- mock login을 개발용 fallback 수준으로 제한하거나 제거 -- 역할별 접근 제어 정리 -- 조직도/자리배치도/분석 화면 권한 경계 재정리 +## Current Actual Serving Map + +- `/`: + - [frontend/public/index.html](/home/hyunho/projects/mh-dashboard-organization/.dev-worktree-8081/frontend/public/index.html) +- `/styles.css`: + - [frontend/public/styles.css](/home/hyunho/projects/mh-dashboard-organization/.dev-worktree-8081/frontend/public/styles.css) +- `/styles-8081-design.css`: + - [frontend/public/styles-8081-design.css](/home/hyunho/projects/mh-dashboard-organization/.dev-worktree-8081/frontend/public/styles-8081-design.css) +- `/legacy/organization`: + - [legacy/static/DashBoard-organization.html](/home/hyunho/projects/mh-dashboard-organization/.dev-worktree-8081/legacy/static/DashBoard-organization.html) +- `/integrations/payment`: + - [incoming-files/served/payment.html](/home/hyunho/projects/mh-dashboard-organization/.dev-worktree-8081/incoming-files/served/payment.html) +- `/integrations/ledger`: + - [incoming-files/served/ledger/index.html](/home/hyunho/projects/mh-dashboard-organization/.dev-worktree-8081/incoming-files/served/ledger/index.html) +- `/integrations/mh`: + - [incoming-files/served/mh.html](/home/hyunho/projects/mh-dashboard-organization/.dev-worktree-8081/incoming-files/served/mh.html) + +## Cross Checks Last Confirmed + +- `8080`: `curl http://localhost:8080/api/health` 정상 +- `8081` dev 컨테이너: proxy/backend/frontend/db `healthy` +- `8081` backend 내부 확인: + - `/api/health` 200 + - `/legacy/organization` 200 + - `/integrations/payment` 200 + - `/integrations/ledger` 200 + - `/integrations/mh` 200 +- `incoming-files/served` 내 실제 서빙 파일 존재 확인 + +주의: + +- Codex 터미널 세션에서는 `curl http://localhost:8081`가 간헐적으로 실패할 수 있다. +- 이 경우 브라우저 확인 또는 컨테이너 내부 라우트 확인을 기준으로 판단한다. + +## Open Issues Relevant Now + +- `#14` 누적된 임시 로직 정리 및 중복 코드 제거 +- `#16` 사업관리대장 메인 연동 및 기본 원본 DB화 +- `#17` 8081 분리 worktree 기동 절차와 로컬 디자인 자산 복제 고정 +- `#18` 8081 파일 책임 맵 정리 및 프런트 서빙 경로 정돈 +- `#19` 8081 백엔드 라우터/서빙 책임 분리 +- `#20` 8081 worktree 준비 스크립트·문서·운영 규칙 정리 +- `#21` reference 의존 제거 및 8081 실제 서비스 코드 독립화 ## Recommended Next Work Order -1. `#2` 범위를 현재 코드 기준으로 재정의하고 영속성 운영 검증 완료 -2. `#5`에서 권한 체크, mock login 정리, 쓰기 API 보호 적용 -3. `8081` DB를 `8080` 정본 기준으로 동기화하는 반복 가능한 절차 마련 -4. `#9`를 as-of date 기반 history 구조로 설계 후 `members`, `seat_positions` 부터 이력화 -5. 그 다음 `#8`, 나머지 도면 추가, `#7`, 프로젝트 분석 오차 보정 순으로 진행 +1. `#21` 이후 기준으로 실제 서비스 파일과 reference 파일 경계를 유지 +2. 사업관리대장 세부 데이터 정합성 보정 +3. 그 다음 화면별 앱 구조 승격 검토 +4. 필요 시 `#19`, `#20` 잔여 정리 항목 재평가 ## Quick Resume Prompt 다음 세션 시작 시 아래 기준으로 이어가면 된다. -- 브랜치 `total`에서 시작 -- 최근 커밋 `1d15cf9` 확인 -- `docs/DEVELOPMENT_HISTORY.md` -- `docs/NEXT_SESSION_CHECKPOINT.md` -- `docs/DEV_PROD_DB_PROTOCOL.md` -- `docs/REGRESSION_CHECKLIST.md` -- `docs/HISTORY_ASOF_DB_PLAN.md` -- Gitea 이슈 `#2`, `#5`, `#9` - -그리고 먼저 현재 외부 접속, 자리배치 저장, 실제 로그인 동작을 확인한 뒤 다음 기능 개발로 넘어간다. +- `8080` 기준은 `total` +- `8081` 작업은 `work-8081` + `.dev-worktree-8081` +- 먼저 [WORK_RULEBOOK.md](/home/hyunho/projects/mh-dashboard-organization/.dev-worktree-8081/docs/WORK_RULEBOOK.md), [NEXT_SESSION_CHECKPOINT.md](/home/hyunho/projects/mh-dashboard-organization/.dev-worktree-8081/docs/NEXT_SESSION_CHECKPOINT.md), [architecture/8081_SERVING_MAP.md](/home/hyunho/projects/mh-dashboard-organization/.dev-worktree-8081/docs/architecture/8081_SERVING_MAP.md) 확인 +- 디자인 수정이면 [frontend/public/design-tokens.css](/home/hyunho/projects/mh-dashboard-organization/.dev-worktree-8081/frontend/public/design-tokens.css), [frontend/public/design-patterns.css](/home/hyunho/projects/mh-dashboard-organization/.dev-worktree-8081/frontend/public/design-patterns.css), [architecture/DESIGN_SSOT.md](/home/hyunho/projects/mh-dashboard-organization/.dev-worktree-8081/docs/architecture/DESIGN_SSOT.md) 먼저 확인 +- 현재 구조 독립화 기준 이슈는 `#21` +- 작업 전 `git status`, dev 컨테이너 상태, `/api/health`, `/legacy/organization`, `/integrations/payment`, `/integrations/ledger`, `/integrations/mh`를 먼저 확인 diff --git a/docs/WORK_EXECUTION_FLOW.md b/docs/WORK_EXECUTION_FLOW.md index 9db488f..6136f17 100644 --- a/docs/WORK_EXECUTION_FLOW.md +++ b/docs/WORK_EXECUTION_FLOW.md @@ -40,6 +40,8 @@ - 기능 검증 기준: `8081` - 사업관리대장 디자인 기준: `MH 통합 대시보드_260320.html` - 허브 공통 시각 언어 기준: `sample style.css` +- 런타임 디자인 토큰 기준: `frontend/public/design-tokens.css` +- 런타임 디자인 패턴 기준: `frontend/public/design-patterns.css` - 현재 작업 지시 기준: 연결된 Gitea 이슈 작업 시작 전에 먼저 정해야 하는 질문: @@ -51,6 +53,14 @@ 이걸 모르고 코드를 건드리면 높은 확률로 엉뚱한 파일을 수정하게 된다. +디자인 작업 추가 규칙: + +- 디자인 수정은 항상 `design-tokens.css`와 `design-patterns.css`를 먼저 확인한다. +- 색/패널/버튼/테이블/팝업이 공통 규칙으로 해결 가능한지 먼저 본다. +- 해결 가능하면 화면별 파일을 고치지 않고 토큰/패턴 파일에서 수정한다. +- 화면별 실제 서빙 파일은 마지막 단계에서만 조정한다. +- 원본/reference 파일은 비교용이지 직접 수정 우선 대상이 아니다. + ## 2. 이슈 생성 또는 연결 작업은 이슈 없이 하지 않는다. diff --git a/docs/WORK_RULEBOOK.md b/docs/WORK_RULEBOOK.md index 6895ad9..7aee334 100644 --- a/docs/WORK_RULEBOOK.md +++ b/docs/WORK_RULEBOOK.md @@ -218,6 +218,26 @@ mock, fallback, hotfix, 임시 우회 로직은 허용할 수 있다. ## Rule 13. 8081 Must Start From The Isolated Worktree +`8081` 작업은 항상 `.dev-worktree-8081` 기준으로 시작한다. + +세부 규칙: + +- 디자인 작업도 예외가 아니다. +- 허브/조직현황/프로젝트별 분석/사업관리대장 수정 전에 현재 실제 서빙 파일과 SSOT 파일을 먼저 확인한다. + +디자인 작업 강제 우선순위: + +1. `frontend/public/design-tokens.css` +2. `frontend/public/design-patterns.css` +3. `docs/architecture/DESIGN_SSOT.md` +4. 그 다음 화면별 실제 서빙 파일 + +금지: + +- reference/original 파일을 먼저 수정하기 +- 예전 파란톤/indigo/slate 계열을 새 기본값으로 다시 넣기 +- 토큰/패턴으로 해결 가능한 문제를 화면별 임시 하드코딩으로 처리하기 + `8081` 작업용은 포트만 다른 복제 서버가 아니라, 코드 소스까지 분리된 전용 worktree여야 한다. 세부 규칙: diff --git a/docs/architecture/8081_SERVING_MAP.md b/docs/architecture/8081_SERVING_MAP.md new file mode 100644 index 0000000..f00447f --- /dev/null +++ b/docs/architecture/8081_SERVING_MAP.md @@ -0,0 +1,112 @@ +# 8081 Serving Map + +## Purpose + +이 문서는 `8081` 작업용에서 어떤 URL이 어떤 파일을 실제로 읽는지 고정하기 위한 책임 맵이다. +이번 1차 정리의 목표는 기능 변경이 아니라 `실제 서빙 파일`, `공통 기본 스타일`, `8081 전용 오버라이드`, `참고 원본 자산`의 경계를 분명히 하는 것이다. + +## Runtime Entry Points + +- 허브 엔트리: `/` + - 파일: `frontend/public/index.html` +- 허브 공통 스크립트: + - 파일: `frontend/public/app.js` +- 허브 공통 기본 스타일: + - 파일: `frontend/public/styles.css` +- 허브 8081 전용 디자인 오버라이드: + - 파일: `frontend/public/styles-8081-design.css` + +## Login Rules + +- 로그인 화면 기본 구조와 스타일은 `8080` 공통 기준을 따른다. +- 로그인 기본 스타일은 `frontend/public/styles.css`에서만 정의한다. +- `frontend/public/styles-8081-design.css`에는 로그인 관련 셀렉터를 넣지 않는다. + +## Legacy Organization + +- URL: `/legacy/organization` +- HTML 파일: + - `DashBoard-organization.html` +- 정적 자산: + - `legacy/static/common.css` + - `legacy/static/organization.css` + - `legacy/static/organization.js` + +## Integration Screens + +- URL: `/integrations/payment` + - 현재 실제 서빙 파일: `incoming-files/served/payment.html` + - 앱 소스 기준: `frontend/apps/payment/index.html` + - publish 규칙: `scripts/publish_payment_app.sh` +- URL: `/integrations/ledger` + - 현재 실제 서빙 파일: `incoming-files/served/ledger/index.html` + - 현재 실제 runtime asset 경로: `incoming-files/served/ledger/*` + - 앱 소스 기준: `frontend/apps/ledger/*` + - publish 규칙: `frontend/apps/ledger/index.html` placeholder를 `scripts/publish_ledger_app.sh`가 runtime asset 경로로 치환 +- URL: `/integrations/mh` + - 현재 실제 서빙 파일: `incoming-files/served/mh.html` + - 앱 소스 기준: `frontend/apps/team/index.html` + - publish 규칙: `scripts/publish_team_app.sh` + +정리 원칙: + +- `incoming-files` 아래에서는 `served/`를 실제 서빙 자산용으로 사용한다. +- `reference/`는 원본 참고 파일, 복구 참고 파일, 비교용 자산만 둔다. +- 1차 정리에서는 기존 실제 서빙 파일을 `served/`에 복사하고, backend 서빙 경로를 먼저 `served/`로 갱신한다. +- `사업관리대장`은 `#21`부터 wrapper decode 방식 대신 `served/ledger/index.html`과 `served/ledger/*`를 직접 서빙한다. +- `사업관리대장` 수정 원본은 `#21` 다음 단계부터 `frontend/apps/ledger/*`를 먼저 보고, `scripts/publish_ledger_app.sh`로 runtime served 파일에 반영한다. +- 기존 루트 `incoming-files/payment.html`, `incoming-files/mh.html`는 안전한 비교/복구를 위해 당분간 남겨둔다. + +## Seat Map + +- 허브 화면 구성: + - `frontend/public/index.html` + - `frontend/public/app.js` + - `frontend/public/styles.css` + - `frontend/public/styles-8081-design.css` +- API / viewer: + - `backend/app/main.py` + - `backend/app/db.py` + - `backend/app/center_chair_viewer_template.html` + +## Incoming Files Classification + +### Served + +- 실제 URL에서 직접 읽는 파일 +- 예: + - `served/payment.html` + - `served/mh.html` + +### Reference + +- 원본 HTML/CSS/XLSX/CSV +- 복구 비교용 자산 +- 직접 서빙하지 않는 참고 파일 +- 필요 시 다음 차수에서 `reference/` 하위로 단계적 재배치한다. + +예: + +- `260320.html` +- `sample style.css` +- `opayment.html` +- `omh.html` +- `reference/ledger/MH 통합 대시보드_260320.html` +- `reference/ledger/MH 통합 대시보드_260320.css` +- 원본 xlsx/csv + +## Out Of Scope For Phase 1 + +- DB 스키마 의미 변경 +- 계산식 변경 +- 권한 로직 변경 +- 신규 기능 추가 +- backend 라우터 대분해 + +## Phase 1 Success Criteria + +- 수정 대상 파일을 화면별로 즉시 찾을 수 있다. +- 로그인은 `styles.css`만 본다. +- 허브 8081 디자인은 `styles-8081-design.css`만 본다. +- `/integrations/payment`, `/integrations/mh`의 실제 서빙 파일 위치가 문서와 코드에서 일치한다. +- 기존 참고 자산을 지우지 않고도 실제 서빙 경로와 참고 경로를 구분할 수 있다. diff --git a/docs/architecture/DESIGN_SSOT.md b/docs/architecture/DESIGN_SSOT.md new file mode 100644 index 0000000..03fa7e5 --- /dev/null +++ b/docs/architecture/DESIGN_SSOT.md @@ -0,0 +1,129 @@ +# Design SSOT + +## Source of truth + +- Primary visual source: [incoming-files/sample style.css](/home/hyunho/projects/mh-dashboard-organization/.dev-worktree-8081/incoming-files/sample%20style.css) +- Runtime token file: [design-tokens.css](/home/hyunho/projects/mh-dashboard-organization/.dev-worktree-8081/frontend/public/design-tokens.css) +- Runtime pattern file: [design-patterns.css](/home/hyunho/projects/mh-dashboard-organization/.dev-worktree-8081/frontend/public/design-patterns.css) + +`sample style.css` defines the intended MH visual language. `design-tokens.css` is the token-level SSOT, and `design-patterns.css` is the component-level SSOT that packages those tokens into reusable runtime patterns. + +## Rules + +- New UI must use `design-tokens.css` variables first. +- New UI must use `design-patterns.css` patterns before adding page-local variants. +- Direct hex values are exceptions, not defaults. +- Page files may define layout and composition, but color, panel, border, radius, and shadow values must come from tokens. +- Shared aliases in `legacy/static/common.css` and `frontend/public/styles.css` exist only to bridge older code to the SSOT. +- Reference files under `incoming-files/*` are not visual authority. Runtime visuals must follow `design-tokens.css` and `design-patterns.css`. + +## Fixed vs Flexible + +SSOT is not a pixel-locked screenshot spec. It is a design rule system with two layers. + +### Fixed rules + +These should be treated as stable defaults across screens. + +- Brand color family and accent family +- Surface, border, text, and shadow tokens +- Radius scale +- Button, tab, input, panel, and card visual language +- Typography tone and hierarchy +- Background atmosphere and overall contrast direction + +### Flexible rules + +These must be interpreted per screen based on content density and interaction needs. + +- KPI card width and number of columns +- Sidebar/content split ratios +- Table column widths +- Search/filter placement +- Card stacking and wrap behavior +- Desktop/mobile breakpoint behavior + +Example: + +- Wrong SSOT: `KPI width is 100px` +- Correct SSOT: `KPI cards use the shared panel, radius, spacing, and text hierarchy tokens, and their width adapts to content without collapsing readability` + +## When SSOT does not define a component + +If a screen needs a pattern that SSOT does not explicitly define yet, do not fall back to arbitrary legacy styling. + +Use this order: + +1. Reuse existing tokens and the nearest shared pattern +2. Design the missing component in the same visual grammar +3. If the pattern is likely to repeat, document and promote it into SSOT + +This applies to examples such as: + +- A table pattern that does not exist in the current SSOT +- A KPI strip that needs a different density than the sample +- A new modal layout for a data-heavy screen + +## Candidate and deprecated styles + +Not every style already visible in the product is automatically part of SSOT. + +- `SSOT` + - Approved and repeatable patterns + - Token-backed visual rules +- `candidate` + - Screen-local styles that look usable but do not yet have a documented basis + - Can be promoted later if they prove reusable +- `deprecated` + - Old blue/slate/indigo defaults + - Temporary hardcoded fixes + - Styles that conflict with the sample-based MH visual language + +When a screen has a design with no clear basis, classify it as `candidate` first. Promote it only after it has been checked for reuse and consistency. + +## Token groups + +- Surface: `--ds-bg`, `--ds-panel`, `--ds-panel-soft`, `--ds-panel-strong` +- Text: `--ds-ink`, `--ds-text-soft`, `--ds-text-muted` +- Brand: `--ds-brand`, `--ds-brand-deep`, `--ds-brand-soft`, `--ds-accent`, `--ds-accent-soft`, `--ds-mint` +- Borders and shadows: `--ds-line`, `--ds-line-soft`, `--ds-shadow-*` +- Layout primitives: `--ds-radius-*`, `--ds-space-*`, `--ds-page-max-width` + +## Promoted runtime patterns + +These are now the official reusable patterns for current screens. + +- Panels and heads: `.ds-panel`, `.ds-panel-head` +- KPI cards: `.ds-kpi-card`, `.ds-kpi-people`, `.ds-kpi-inverse` +- Filter surfaces and toggles: `.ds-filter-surface`, `.ds-filter-toggle`, `.ds-reset-button` +- Tables: `.ds-table-head`, `.ds-table-head-row`, `.ds-table-row`, `.ds-axis-cell`, `.ds-axis-cell-idle`, `.ds-axis-cell-active` +- Value emphasis: `.ds-project-cell`, `.ds-income`, `.ds-expense`, `.ds-subhead`, `.ds-empty`, `.ds-strong`, `.ds-muted` +- Breakdown/detail UI: `.ds-progress-track*`, `.ds-mode-chip`, `.ds-name-chip`, `.ds-mini-table-*`, `.ds-group-title` +- Position chips: `.ds-position-*` via `position-*` compatibility classes +- Business ledger popup/detail blocks: `.popup-*`, `.inline-card`, `.project-head-*`, `.summary-*`, `.ledger-*`, `.badge`, `.project-link` +- Organization modal forms/buttons: `.member-form-*`, `.modal-btn*` +- Seatmap action visibility: `.seatmap-actions .ghost-button` + +These patterns may still have compatibility selectors for existing screen classes, but they should now be treated as the official design layer. + +## Migration order + +1. Token file and common aliases +2. Hub shell and shared controls +3. Team/Personal analysis and Organization +4. Project analysis +5. Business ledger detail cleanup + +## Implementation guidance + +- Prefer tokenized ranges over hardcoded single values when layout depends on data volume +- Prefer `design-patterns.css` component rules over one-off inline colors +- If a new pattern is introduced during implementation, update this document once the pattern is stable +- If a screen needs an exception, keep the exception local and explain why it cannot follow the shared pattern + +## Anti-patterns + +- Adding new `#4f46e5`, `#4338ca`, `bg-slate-*`, `text-indigo-*` style defaults +- Reintroducing separate page-level color systems +- Hardcoding “quick fix” brand colors in JS templates when a token/class can carry the same intent +- Letting reference/original files override runtime pattern files diff --git a/frontend/apps/ledger/README.md b/frontend/apps/ledger/README.md new file mode 100644 index 0000000..b160759 --- /dev/null +++ b/frontend/apps/ledger/README.md @@ -0,0 +1,26 @@ +# Ledger App Source + +`사업관리대장` 화면의 앱 구조 source-of-truth 디렉터리다. + +현재 원칙: + +- 실제 runtime 응답은 여전히 `incoming-files/served/ledger/`를 사용한다. +- 하지만 HTML/CSS/JS 수정 원본은 이 디렉터리에서 먼저 관리한다. +- 변경 후에는 `scripts/publish_ledger_app.sh`로 `served/ledger/`에 반영한다. + +구성: + +- `index.html`: ledger 엔트리 HTML 원본 템플릿 +- `assets/MH 통합 대시보드_260320.css`: ledger base stylesheet +- `assets/ledger-override.css`: 8081 ledger 스타일 확장 +- `assets/ledger-override.js`: 8081 ledger UI/상호작용 확장 + +주의: + +- `index.html`은 runtime 경로를 직접 하드코딩하지 않는다. +- `__LEDGER_HEAD_ASSETS__`, `__LEDGER_BODY_SCRIPTS__` placeholder는 publish 시 실제 `/integrations/ledger-assets/*` 경로로 치환된다. + +범위: + +- 이 디렉터리는 `#21` 이후 `사업관리대장`을 화면별 앱 구조로 승격하기 위한 첫 단계다. +- 아직 프레임워크 앱은 아니고, 독립 관리되는 정식 화면 소스 디렉터리다. diff --git a/incoming-files/사업관리대장/MH 통합 대시보드_260320.css b/frontend/apps/ledger/assets/MH 통합 대시보드_260320.css similarity index 100% rename from incoming-files/사업관리대장/MH 통합 대시보드_260320.css rename to frontend/apps/ledger/assets/MH 통합 대시보드_260320.css diff --git a/frontend/apps/ledger/assets/ledger-override.css b/frontend/apps/ledger/assets/ledger-override.css new file mode 100644 index 0000000..505b65e --- /dev/null +++ b/frontend/apps/ledger/assets/ledger-override.css @@ -0,0 +1,328 @@ +html, +body { + margin: 0; + padding: 0; +} + +body.mh-business-theme { + overflow-x: hidden; + background: + radial-gradient(circle at top left, rgba(214, 138, 58, 0.16), transparent 24%), + radial-gradient(circle at top right, rgba(47, 153, 115, 0.10), transparent 20%), + linear-gradient(180deg, #f6efe6 0%, #f1eadf 100%); +} + +body.mh-business-theme .wrap { + width: min(100%, 2000px); + max-width: 2000px; + margin: 0 auto; + padding: 18px 18px 26px; + box-sizing: border-box; +} + +body.mh-business-theme .top, +body.mh-business-theme .status { + display: none !important; +} + +body.mh-business-theme .cards { + display: grid; + grid-template-columns: repeat(12, minmax(0, 1fr)); + gap: 14px; + margin: 0 0 16px; +} + +body.mh-business-theme .business-shell { + width: 100%; + box-sizing: border-box; + margin-top: 2px; + padding: 18px; + border-radius: 32px; + background: + radial-gradient(circle at 16% 14%, rgba(255,255,255,0.05), transparent 18%), + radial-gradient(circle at 88% 8%, rgba(255,255,255,0.04), transparent 16%), + linear-gradient(145deg, #0b352b 0%, #174e41 52%, #245f50 100%); + box-shadow: 0 26px 54px rgba(15, 58, 47, 0.16); + border: 1px solid rgba(255,255,255,0.08); +} + +body.mh-business-theme .cards-toolbar { + grid-column: 1 / -1; + display: flex; + flex-direction: column; + gap: 14px; + padding: 10px 0 2px; +} + +body.mh-business-theme .cards-toolbar-row { + display: flex; + align-items: center; + gap: 10px; + flex-wrap: wrap; +} + +body.mh-business-theme .cards-toolbar-search { + margin-left: auto; + display: flex; + align-items: center; + min-width: min(360px, 100%); + flex: 1 1 320px; + max-width: 520px; +} + +body.mh-business-theme .cards-toolbar-search .search { + width: 100%; + min-width: 0; + border-radius: 999px; + border: 1px solid rgba(255,255,255,0.12); + background: rgba(255,255,255,0.10); + color: #f4efe6; + padding: 14px 18px; + font-size: 14px; + font-weight: 800; + box-shadow: inset 0 1px 0 rgba(255,255,255,0.04); +} + +body.mh-business-theme .cards-toolbar-search .search::placeholder { + color: rgba(244, 239, 230, 0.74); +} + +body.mh-business-theme #btnUpload { + display: none !important; +} + +body.mh-business-theme .cards-toolbar-metrics { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 14px; +} + +body.mh-business-theme .summary-year-chip { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 60px; + padding: 10px 16px; + border-radius: 999px; + border: 1px solid rgba(255,255,255,0.14); + background: rgba(255,255,255,0.08); + color: #f4efe6; + font-size: 12px; + font-weight: 900; + cursor: pointer; +} + +body.mh-business-theme .summary-year-chip.active { + background: linear-gradient(180deg, #fff8ee 0%, #f2dec0 100%); + color: #0a2a22; + border-color: rgba(242, 196, 132, 0.58); + box-shadow: 0 12px 28px rgba(10, 42, 34, 0.18); +} + +body.mh-business-theme .summary-filter-chip { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 6px; + width: 100%; + min-height: 98px; + padding: 18px 22px; + border-radius: 999px; + border: 1px solid rgba(255,255,255,0.14); + background: linear-gradient(180deg, rgba(255,255,255,0.10) 0%, rgba(255,255,255,0.07) 100%); + color: #f4efe6; + box-shadow: inset 0 1px 0 rgba(255,255,255,0.04), 0 16px 30px rgba(7, 28, 22, 0.14); + cursor: pointer; + text-align: center; +} + +body.mh-business-theme .summary-filter-chip.active { + background: linear-gradient(180deg, #fff8ee 0%, #f2dec0 100%); + color: #0a2a22; + border-color: rgba(242, 196, 132, 0.58); +} + +body.mh-business-theme .summary-filter-chip .label { + color: rgba(244, 239, 230, 0.78); + font-size: 13px; + font-weight: 900; +} + +body.mh-business-theme .summary-filter-chip.active .label { + color: rgba(10, 42, 34, 0.78); +} + +body.mh-business-theme .summary-filter-chip .count { + color: #fff7e6; + font-size: 32px; + line-height: 1; + font-weight: 900; +} + +body.mh-business-theme .summary-filter-chip.active .count { + color: #b86b1f; +} + +body.mh-business-theme .summary-filter-chip .meta { + color: #f2c484; + font-size: 11px; + font-weight: 800; + text-align: center; +} + +body.mh-business-theme .summary-filter-chip.active .meta { + color: #7c5a20; +} + +body.mh-business-theme .card { + grid-column: span 2; + min-height: 110px; + border-radius: 24px; + border: 1px solid rgba(217, 197, 168, 0.55); + background: linear-gradient(180deg, rgba(255,250,243,0.96) 0%, rgba(248,242,232,0.96) 100%); + padding: 18px 20px; + box-shadow: 0 18px 32px rgba(15, 58, 47, 0.08); +} + +body.mh-business-theme .card.management { + grid-column: span 2; +} + +body.mh-business-theme .card .k { + color: #5b6d63; + font-size: 12px; + font-weight: 900; +} + +body.mh-business-theme .card .v { + margin-top: 8px; + color: #17392f; + font-size: 30px; + font-weight: 900; +} + +body.mh-business-theme .card .n { + margin-top: 8px; + color: #7b6953; + font-size: 11px; + font-weight: 700; +} + +body.mh-business-theme .panel { + border-radius: 28px; + border: 1px solid rgba(217, 197, 168, 0.55); + box-shadow: 0 18px 32px rgba(15, 58, 47, 0.08); +} + +body.mh-business-theme .table-wrap { + width: 100%; + max-width: 100%; + border-radius: 28px; + overflow-x: hidden !important; +} + +body.mh-business-theme .table-vat-note { + display: none !important; +} + +body.mh-business-theme table { + width: 100% !important; + min-width: 0 !important; + table-layout: fixed; + background: rgba(255, 250, 243, 0.96); +} + +body.mh-business-theme thead th { + background: #0f352b; + color: #fff5e6; + border-right: 1px solid rgba(242, 196, 132, 0.2); +} + +body.mh-business-theme tbody td { + background: rgba(255, 250, 243, 0.96); +} + +body.mh-business-theme .group-row td { + padding: 12px 14px 10px; + background: linear-gradient(180deg, rgba(255, 248, 238, 0.98) 0%, rgba(242, 222, 192, 0.78) 100%); + border-top: 1px solid rgba(214, 138, 58, 0.26); + border-bottom: 1px solid rgba(217, 197, 168, 0.54); +} + +body.mh-business-theme .group-chip { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 6px 12px; + border-radius: 999px; + background: rgba(255, 250, 243, 0.98); + border: 1px solid rgba(214, 138, 58, 0.3); + color: #17392f; + font-size: 12px; + font-weight: 900; + box-shadow: 0 8px 18px rgba(15, 58, 47, 0.08); + cursor: pointer; +} + +body.mh-business-theme .group-chip .group-toggle { + margin-left: 4px; + width: 22px; + height: 22px; + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: 999px; + background: rgba(242, 196, 132, 0.18); + color: #b66e22; + font-size: 14px; + line-height: 1; +} + +body.mh-business-theme .project-link { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 0; + border: 0; + background: none; + color: #17392f; + font: inherit; + font-weight: 900; + text-align: left; + cursor: pointer; +} + +body.mh-business-theme .project-link:hover { + color: #0f6a55; +} + +@media (max-width: 1280px) { + body.mh-business-theme .cards-toolbar-metrics { + grid-template-columns: 1fr; + } + + body.mh-business-theme .card { + grid-column: span 4; + } +} + +@media (max-width: 880px) { + body.mh-business-theme .wrap { + padding: 12px 12px 20px; + } + + body.mh-business-theme .cards { + grid-template-columns: 1fr; + } + + body.mh-business-theme .card { + grid-column: auto; + } + + body.mh-business-theme .cards-toolbar-search { + margin-left: 0; + max-width: none; + flex-basis: 100%; + } +} diff --git a/frontend/apps/ledger/assets/ledger-override.js b/frontend/apps/ledger/assets/ledger-override.js new file mode 100644 index 0000000..853e51c --- /dev/null +++ b/frontend/apps/ledger/assets/ledger-override.js @@ -0,0 +1,498 @@ +(function () { + window.__mhLedgerEnhancementLoaded = false; + if (typeof S === "undefined" || typeof E === "undefined" || typeof render !== "function") return; + window.__mhLedgerEnhancementLoaded = true; + if (!S.dashboard) S.dashboard = { year: "", section: "active" }; + if (!S.collapsedGroups) S.collapsedGroups = {}; + + function bgToday() { + var now = new Date(); + return new Date(now.getFullYear(), now.getMonth(), now.getDate()); + } + + function bgParseDate(value) { + var text = String(value || "").trim(); + if (!text) return null; + var match = text.match(/(20\d{2})\D?(\d{1,2})\D?(\d{1,2})/); + if (match) { + var parsed = new Date(Number(match[1]), Number(match[2]) - 1, Number(match[3])); + return isNaN(parsed.getTime()) ? null : parsed; + } + var fallback = new Date(text); + if (isNaN(fallback.getTime())) return null; + return new Date(fallback.getFullYear(), fallback.getMonth(), fallback.getDate()); + } + + function bgYearFromText(value) { + var match = String(value || "").trim().match(/(20\d{2})/); + return match ? match[1] : ""; + } + + function bgStartYear(row) { + return bgYearFromText(row && row.sDate); + } + + function bgEndYear(row) { + return bgYearFromText(row && row.eDate); + } + + function bgDisplayYear(row) { + var start = bgStartYear(row); + if (start) return start; + var contractMatch = String((row && row.cDate) || "").trim().match(/(20\d{2})/); + if (contractMatch) return contractMatch[1]; + var nameMatch = String((row && row.name) || "").trim().match(/^(20\d{2})/); + if (nameMatch) return nameMatch[1]; + return bgEndYear(row) || "미지정"; + } + + function bgCompletionYear(row) { + return bgEndYear(row) || bgDisplayYear(row); + } + + function bgDateOrYearStart(row) { + var yearText = bgDisplayYear(row); + return bgParseDate(row && row.sDate) || bgParseDate(row && row.cDate) || (/^20\d{2}$/.test(yearText) ? new Date(Number(yearText), 0, 1) : null); + } + + function bgDateOrYearEnd(row) { + var completionYear = bgCompletionYear(row); + return bgParseDate(row && row.eDate) || (/^20\d{2}$/.test(completionYear) ? new Date(Number(completionYear), 11, 31) : null); + } + + function bgYearCutoff(year) { + var targetYear = Number(year || 0); + if (!targetYear) return null; + var today = bgToday(); + if (targetYear < today.getFullYear()) return new Date(targetYear, 11, 31); + if (targetYear === today.getFullYear()) return today; + return null; + } + + function bgYearStartDate(year) { + var targetYear = Number(year || 0); + return targetYear ? new Date(targetYear, 0, 1) : null; + } + + function bgActiveInYear(row, year) { + var cutoff = bgYearCutoff(year); + var yearStart = bgYearStartDate(year); + var startDate = bgDateOrYearStart(row); + var endDate = bgDateOrYearEnd(row); + if (!(cutoff && yearStart && startDate)) return false; + if (startDate > cutoff) return false; + if (endDate && endDate < yearStart) return false; + return !(endDate && endDate <= cutoff); + } + + function bgStartedInYear(row, year) { + var cutoff = bgYearCutoff(year); + var startDate = bgDateOrYearStart(row); + if (!(cutoff && startDate)) return false; + return startDate.getFullYear() === Number(year || 0) && startDate <= cutoff; + } + + function bgCompletedInYear(row, year) { + var cutoff = bgYearCutoff(year); + var endDate = bgDateOrYearEnd(row); + if (!(cutoff && endDate)) return false; + return endDate.getFullYear() === Number(year || 0) && endDate <= cutoff; + } + + function bgYearRange(row) { + var years = []; + var startYear = Number(bgDisplayYear(row) || 0); + var endYear = Number(bgCompletionYear(row) || 0); + if (startYear && endYear && endYear >= startYear) { + for (var year = startYear; year <= endYear; year += 1) years.push(String(year)); + } else if (startYear) { + years.push(String(startYear)); + } + return years; + } + + function bgYears(rows) { + var currentYear = new Date().getFullYear(); + var years = Array.from(new Set((Array.isArray(rows) ? rows : []).flatMap(bgYearRange).filter(function (year) { + return /^20\d{2}$/.test(year); + }))).sort(function (a, b) { + return Number(b) - Number(a); + }); + years = years.filter(function (year) { + var numericYear = Number(year); + return numericYear >= 2018 && numericYear <= currentYear; + }); + return years.length ? years : [String(currentYear)]; + } + + function bgEnsureYear(rows) { + var years = bgYears(rows); + if (!years.includes(S.dashboard.year)) S.dashboard.year = years[0]; + return years; + } + + function bgTotals(targetRows) { + return (Array.isArray(targetRows) ? targetRows : []).reduce(function (acc, row) { + acc.c += Number((row && row.cSup) || 0); + acc.col += Number((row && row.col) || 0); + acc.recv += Number((row && row.recv) || 0); + return acc; + }, { c: 0, col: 0, recv: 0 }); + } + + function isSupportServiceRow(row) { + var category = String((row && row.cat) || "").trim(); + return category.indexOf("경영지원") >= 0 || category.indexOf("서비스") >= 0; + } + + function isBaronProjectRow(row) { + var category = String((row && row.cat) || "").trim(); + if (category.indexOf("바론") < 0) return false; + if (isSupportServiceRow(row)) return false; + return true; + } + + function bgSummarize(rows, selectedYear) { + var items = Array.isArray(rows) ? rows : []; + var targetYear = selectedYear || bgEnsureYear(items)[0]; + var activeRows = items.filter(function (row) { return bgActiveInYear(row, targetYear); }); + var newProjectRows = items.filter(function (row) { return bgStartedInYear(row, targetYear); }); + var completedRows = items.filter(function (row) { return bgCompletedInYear(row, targetYear); }); + var managementRows = newProjectRows.filter(isSupportServiceRow); + return { + targetYear: targetYear, + activeRows: activeRows, + newProjectRows: newProjectRows, + completedRows: completedRows, + managementRows: managementRows, + managementTotals: bgTotals(managementRows) + }; + } + + function bgMatches(row) { + var section = S.dashboard.section || "active"; + var selectedYear = S.dashboard.year || bgEnsureYear(S.all)[0]; + if (section === "new") return bgStartedInYear(row, selectedYear); + if (section === "completed") return bgCompletedInYear(row, selectedYear); + return bgActiveInYear(row, selectedYear); + } + + function normalizeStatusLabel(status) { + var value = String(status || "").trim(); + if (!value) return "-"; + if (value.indexOf("진행") >= 0) return "과업 진행중"; + return value; + } + + function formatSplitPercent(split) { + var numeric = parseFloat(String(split || "").replace(/[^0-9.\-]/g, "")); + if (!Number.isFinite(numeric) || numeric === 0) return "분담율 -%"; + return "분담율 " + numeric.toFixed(2) + "%"; + } + + function projectYear(row) { + var start = String((row && row.sDate) || "").trim(); + var startMatch = start.match(/(20\d{2})/); + if (startMatch) return startMatch[1]; + var name = String((row && row.name) || "").trim(); + var nameMatch = name.match(/^(20\d{2})/); + if (nameMatch) return nameMatch[1]; + var end = String((row && row.eDate) || "").trim(); + var endMatch = end.match(/(20\d{2})/); + if (endMatch) return endMatch[1]; + return "미지정"; + } + + function groupSortRank(row) { + var selectedYear = Number((S.dashboard && S.dashboard.year) || projectYear(row) || 0); + var startYear = Number(projectYear(row) || 0); + if (typeof bgCompletedInYear === "function" && bgCompletedInYear(row, String(selectedYear))) return 9999; + if (!startYear) return 9998; + return startYear; + } + + function tableGroupLabel(row) { + var startYear = projectYear(row); + if (/^20\d{2}$/.test(startYear)) return startYear + "년"; + return "미지정"; + } + + function renderLedgerTable() { + var table = document.querySelector(".panel table"); + if (!table || !E.tbody) return; + var thead = table.querySelector("thead"); + if (thead) { + thead.innerHTML = '' + + '
' + + '
' + + '
' + + '
' + + '
' + + '
' + + '
' + + '
' + + '
' + + '
' + + '
' + + ""; + } + var rows = (Array.isArray(S.viewRows) ? S.viewRows : []).slice().sort(function (a, b) { + var ar = groupSortRank(a); + var br = groupSortRank(b); + if (ar !== br) return ar - br; + return Number(b.recv || 0) - Number(a.recv || 0); + }); + S.viewRows = rows; + var lastGroupLabel = ""; + E.tbody.innerHTML = rows.map(function (r) { + var groupLabel = tableGroupLabel(r); + var isCollapsed = !!S.collapsedGroups[groupLabel]; + var groupRow = ""; + if (groupLabel !== lastGroupLabel) { + groupRow = '"; + lastGroupLabel = groupLabel; + } + if (isCollapsed) return groupRow; + return groupRow + '' + + '
= 0 ? 'badge-baron' : 'badge-family') + '">' + esc(r.cat || "-") + '
' + + '
' + esc(r.code || "-") + '
' + + '
' + esc(r.periodText || "-") + '
' + + '
' + esc((r.client || "").trim() || "-") + '
' + esc(formatSplitPercent(r.split)) + '
' + + '
' + esc(r.order || "-") + '
' + + '
= 0 ? 'ok' : '') + '">' + esc(normalizeStatusLabel(r.status)) + '
' + + '' + esc(won(r.cSup || 0)) + '' + + '' + esc(r.outsourceCost ? won(r.outsourceCost) : "-") + '' + + '' + esc(won(r.recv || 0)) + '' + + '' + esc(won(r.col || 0)) + '' + + '' + esc((Number(r.rate || 0)).toFixed(2) + "%") + '' + + ''; + }).join(""); + } + + function renderCollectionBoard(r) { + var payments = Array.isArray(r.payments) && r.payments.length ? r.payments : [{ + pay: r.pay || "-", + issueDate: r.issueDate || "", + collectDate: r.collectDateSummary || r.colDate || "", + collected: r.col || 0, + receivable: r.recv || Math.max(0, Number(r.sTot || 0) - Number(r.col || 0)), + note: r.note || "", + status: r.status || "" + }]; + return '
C
수금 및 기성 현황
기성 차수별 세금계산서 발행 및 수금 내역
총 수금 ' + esc(won(r.col || 0)) + '
' + + payments.map(function (payment, index) { + var noteParts = []; + if (payment.status) noteParts.push(payment.status); + if (payment.note) noteParts.push(payment.note); + return ''; + }).join("") + + "
기성 차수세금계산서 발행일수금일수금금액미수금액비고
' + esc((index + 1) + "차") + '' + esc(payment.pay || "-") + '' + esc(payment.issueDate ? d(payment.issueDate) : "-") + '' + esc(payment.collectDate ? d(payment.collectDate) : "-") + '' + esc(won(payment.collected || 0)) + '' + esc(won(payment.receivable || 0)) + '' + esc(noteParts.join(" / ") || "-") + '
"; + } + + function renderContactCard(label, name, company, department, phone, email) { + var hasValue = [name, company, department, phone, email].some(function (value) { + return String(value || "").trim() !== ""; + }); + if (!hasValue) { + return '
' + esc(label) + '
등록된 담당자 정보가 없습니다.
'; + } + return '
' + esc(label) + '
' + + '
이름
' + esc(name || "-") + '
' + + '
소속
' + esc(company || "-") + '
' + esc(department || "-") + '
' + + '
연락처
' + esc(phone || "-") + '
' + + '
이메일
' + esc(email || "-") + '
' + + "
"; + } + + function renderProjectInline(r) { + var payments = Array.isArray(r.payments) ? r.payments : []; + var latestCollect = d(r.collectDateSummary || r.colDate); + var hasOutsource = (Array.isArray(r.outsourceItems) && r.outsourceItems.length > 0) || Number(r.outsourceCost || 0) > 0 || Number(r.outsourcePaid || 0) > 0 || Number(r.outsourceRemaining || 0) > 0; + var clientDisplay = typeof normalizeClientDisplay === "function" ? normalizeClientDisplay(r.client) : (String(r.client || "").trim() || "-"); + var splitDisplay = typeof formatSplitDisplay === "function" ? formatSplitDisplay(r.split) : formatSplitPercent(r.split).replace("분담율 ", ""); + var summaryCards = [ + '
계약금
' + esc(won(r.cSup || 0)) + '
', + '
수금액
' + esc(won(r.col || 0)) + '
' + esc(latestCollect === "-" ? "수금일 없음" : "최종 수금일 " + latestCollect) + '
', + '
수금률
' + esc((Number(r.rate || 0)).toFixed(2) + "%") + '
' + esc(payments.length ? "기성 " + payments.length + "차까지 반영" : "차수 정보 없음") + '
', + '
미수금액
' + esc(won(r.recv || 0)) + '
잔여 수금 필요 금액
' + ].join(""); + var boards = [ + hasOutsource && typeof renderOutsourceBoard === "function" ? renderOutsourceBoard(r) : "", + renderCollectionBoard(r) + ].filter(Boolean).join(""); + return '
계약법인
' + esc(r.corp || "-") + '
발주처
' + esc(clientDisplay) + '
' + esc(splitDisplay ? "분담율 " + splitDisplay : "분담율 -") + '
발주방법
' + esc(r.order || "-") + '
PM
' + esc(r.pm || "-") + '
' + summaryCards + '
' + renderContactCard("계약 / 청구 담당자", r.cmNm, r.cmCo, r.cmDp, r.cmPh, r.cmEm) + renderContactCard("부서 담당자", r.dmNm, r.dmCo, r.dmDp, r.dmPh, r.dmEm) + '
' + boards + '
'; + } + + function openProjectWindow(r) { + var popupKey = typeof rowKey === "function" + ? rowKey(r).replace(/[^0-9a-zA-Z]/g, "_") + : String((r.code || "project") + "_" + (r.name || "")).replace(/[^0-9a-zA-Z_]/g, "_"); + var popup = window.open("", "business_project_" + popupKey, "width=1600,height=980,resizable=yes,scrollbars=yes"); + if (!popup) return; + var styleText = Array.from(document.querySelectorAll("style")).map(function (el) { + return el.textContent || ""; + }).join("\n"); + var detailHtml = renderProjectInline(r); + var pageHtml = '' + + esc(r.name || "사업 상세") + + '"; + popup.document.open(); + popup.document.write(pageHtml); + popup.document.close(); + popup.focus(); + } + + async function tryLoadDbDefaultBusinessLedger() { + if (window.__mhBusinessDefaultLoaded) return; + window.__mhBusinessDefaultLoaded = true; + try { + var response = await fetch("/api/integration/business-ledger-default"); + if (!response.ok) throw new Error("기본 사업관리대장 원본을 불러오지 못했습니다."); + var fileName = response.headers.get("x-source-filename") || "사업관리대장-1.xlsx"; + var buffer = await response.arrayBuffer(); + if (!buffer || !buffer.byteLength) throw new Error("기본 사업관리대장 원본 데이터가 비어 있습니다."); + await loadLedgerFile(buffer, fileName); + } catch (error) { + console.error(error); + } + } + + function applyDashboardChrome() { + if (!E.cards) return; + document.body.setAttribute("data-mh-ledger-enhanced", "true"); + var wrap = document.querySelector(".wrap"); + var panel = document.querySelector(".panel"); + if (wrap && panel) { + var shell = wrap.querySelector(".business-shell"); + if (!shell) { + shell = document.createElement("div"); + shell.className = "business-shell"; + wrap.insertBefore(shell, E.cards); + } + if (E.cards.parentNode !== shell) shell.appendChild(E.cards); + if (panel.parentNode !== shell) shell.appendChild(panel); + } + var years = bgEnsureYear(S.all); + var summary = bgSummarize(S.all, S.dashboard.year); + var rows = Array.isArray(S.rows) ? S.rows : []; + var visibleBaronProjectRows = rows.filter(isBaronProjectRow); + var totals = bgTotals(visibleBaronProjectRows); + var totalRate = typeof rate === "function" ? rate("", totals.col, totals.col + totals.recv) : 0; + var toolbarHtml = '
' + + '
' + + years.map(function (year) { + return '"; + }).join("") + + '' + + "
" + + '
' + + '' + + '' + + '' + + "
"; + var cards = [ + { label: summary.targetYear + "년 프로젝트", value: visibleBaronProjectRows.length.toLocaleString("ko-KR") + " 건", note: "" }, + { label: "계약금", value: won(totals.c), note: "" }, + { label: "수금액", value: won(totals.col), note: "" }, + { label: "미수금", value: won(totals.recv), note: "" }, + { label: "수금률(%)", value: totalRate.toFixed(2) + "%", note: "" }, + { label: "경영지원서비스 금액", value: won(summary.managementTotals.c), note: "", className: "management" } + ]; + E.cards.innerHTML = toolbarHtml + cards.map(function (card) { + return '
' + esc(card.label) + '
' + esc(card.value) + '
' + esc(card.note || "") + "
"; + }).join(""); + var searchWrap = E.cards.querySelector(".cards-toolbar-search"); + if (searchWrap && E.search) { + searchWrap.appendChild(E.search); + E.search.placeholder = "전체 검색"; + } + } + + var originalRender = render; + render = function () { + originalRender(); + applyDashboardChrome(); + renderLedgerTable(); + }; + + filter = function () { + bgEnsureYear(S.all); + var q = String(E.search.value || "").trim().toLowerCase(); + var searched = !q ? S.all.slice() : S.all.filter(function (r) { + return [r.code, r.name, r.client, r.pm, r.status, r.cat, r.corp, r.pay, (r.payments || []).map(function (p) { return p.pay; }).join(" "), r.periodText].join(" ").toLowerCase().includes(q); + }); + S.rows = searched.filter(function (r) { + return bgMatches(r) && matchesColumnFilters(r); + }); + render(); + }; + + if (E.cards && !E.cards.dataset.dashboardBound) { + E.cards.dataset.dashboardBound = "true"; + E.cards.addEventListener("click", function (event) { + var yearButton = event.target && event.target.closest ? event.target.closest("[data-dashboard-year]") : null; + if (yearButton) { + S.dashboard.year = yearButton.getAttribute("data-dashboard-year") || S.dashboard.year; + filter(); + return; + } + var sectionButton = event.target && event.target.closest ? event.target.closest("[data-dashboard-section]") : null; + if (sectionButton) { + S.dashboard.section = sectionButton.getAttribute("data-dashboard-section") || "active"; + filter(); + } + }); + } + + if (E.tbody && !E.tbody.dataset.projectBound) { + E.tbody.dataset.projectBound = "true"; + E.tbody.addEventListener("click", function (event) { + var groupButton = event.target && event.target.closest ? event.target.closest("[data-group-label]") : null; + if (groupButton) { + var label = groupButton.getAttribute("data-group-label") || ""; + if (label) { + S.collapsedGroups[label] = !S.collapsedGroups[label]; + render(); + } + return; + } + var trigger = event.target && event.target.closest ? event.target.closest(".project-link") : null; + if (!trigger) return; + var key = trigger.getAttribute("data-project-key") || ""; + var rows = Array.isArray(S.viewRows) ? S.viewRows : []; + var row = rows.find(function (item) { + return (String(item.code || "") + "|" + String(item.name || "")) === key; + }); + if (row) openProjectWindow(row); + }); + } + + setTimeout(function () { + try { + filter(); + if (typeof loadLedgerFile === "function") { + tryLoadDbDefaultBusinessLedger(); + } + } catch (error) { + console.error(error); + } + }, 0); + + window.addEventListener("message", function (event) { + var data = event.data || {}; + if (data.source !== "total-upload" || data.type !== "business") return; + setTimeout(function () { + try { + applyDashboardChrome(); + renderLedgerTable(); + } catch (error) { + console.error(error); + } + }, 50); + }); +})(); diff --git a/frontend/apps/ledger/index.html b/frontend/apps/ledger/index.html new file mode 100644 index 0000000..3035b1c --- /dev/null +++ b/frontend/apps/ledger/index.html @@ -0,0 +1,954 @@ + + + + + + 사업관리대장 Dashboard + +__LEDGER_HEAD_ASSETS__ + + +
+
+
Live Management

사업관리대장 | Dashboard

+
+
+
CSV/XLSX 파일을 업로드하면 데이터가 표시됩니다.
+
+
+
+ + + + + + + + + + + + + + +
+
+ +
+
+
+
+ +
+
+
+
+ +
+
+
+
+ +
+
+
+
+ +
+
+
+
+ +
+
+
+
+ +
+
+
+
+ +
+
+
+
+
표시할 데이터가 없습니다.
+
+
+ + + + +__LEDGER_BODY_SCRIPTS__ + diff --git a/frontend/apps/payment/README.md b/frontend/apps/payment/README.md new file mode 100644 index 0000000..15f7593 --- /dev/null +++ b/frontend/apps/payment/README.md @@ -0,0 +1,18 @@ +# Payment App Source + +`프로젝트별 분석` 화면의 앱 구조 source-of-truth 디렉터리다. + +원칙: + +- 실제 runtime 응답은 여전히 `incoming-files/served/payment.html`을 사용한다. +- 수정 원본은 이 디렉터리의 `index.html`만 본다. +- 반영은 `scripts/publish_payment_app.sh`로 수행한다. + +구성: + +- `index.html`: 프로젝트별 분석 standalone app 원본 + +주의: + +- runtime을 수정할 때 `incoming-files/served/payment.html`부터 고치지 않는다. +- 먼저 `frontend/apps/payment/index.html`을 수정한 뒤 publish 한다. diff --git a/frontend/apps/payment/index.html b/frontend/apps/payment/index.html new file mode 100644 index 0000000..24ca0c2 --- /dev/null +++ b/frontend/apps/payment/index.html @@ -0,0 +1,1622 @@ + + + + + + 프로젝트 대시보드 + + + + + + + + +
+ + + + + + + + + + + + + + + + diff --git a/frontend/apps/team/README.md b/frontend/apps/team/README.md new file mode 100644 index 0000000..811cd8a --- /dev/null +++ b/frontend/apps/team/README.md @@ -0,0 +1,18 @@ +# Team App Source + +`팀/개인별 분석` 화면의 앱 구조 source-of-truth 디렉터리다. + +원칙: + +- 실제 runtime 응답은 `incoming-files/served/mh.html`을 사용한다. +- 수정 원본은 이 디렉터리의 `index.html`만 본다. +- 반영은 `scripts/publish_team_app.sh`로 수행한다. + +구성: + +- `index.html`: 팀/개인별 분석 standalone app 원본 + +주의: + +- runtime을 수정할 때 `incoming-files/served/mh.html`부터 고치지 않는다. +- 먼저 `frontend/apps/team/index.html`을 수정한 뒤 publish 한다. diff --git a/frontend/apps/team/index.html b/frontend/apps/team/index.html new file mode 100644 index 0000000..4518e67 --- /dev/null +++ b/frontend/apps/team/index.html @@ -0,0 +1,3472 @@ + + + + + + + + + + + 팀/개인별 분석 + + + + + + + + + + + + + + + + + + + +
+
+

+
+
+

팀/개인별 분석

+
+
+
+ + + +
+
+ +
+ +
+
+
+
+
+ +
+
+ + + + + +
+ +
+ +
+ +
+ +
+ +
+ +
+
+ +
+ +
+

+ + 팀별 진행 프로젝트 + +

+ + +
+ +
+ + + +
+ +
+ +
파일을 업로드하면 프로젝트 현황이 표시됩니다.
+ +
+ +
+ +
+
+ +
+
+ +
+ +
+ + + +
+ +
+ +

분석 데이터를 기다리는 중..

+ + +
+ +
+ +
+ +

※ 인정시간: 평일(8시간+연장 3시간) 및 주말(5시간)

+
+
+
+ +
+
+ + + +
+
+ +
+ + + + + diff --git a/frontend/public/app.js b/frontend/public/app.js index 6ec6aec..88e736a 100644 --- a/frontend/public/app.js +++ b/frontend/public/app.js @@ -15,6 +15,8 @@ const organizationHistoryControls = document.getElementById("organization-histor const organizationMonthSelect = document.getElementById("organization-month-select"); const organizationCompareBtn = document.getElementById("organization-compare-btn"); const navButtons = Array.from(document.querySelectorAll(".header-center [data-view]")); +const ledgerFrame = document.getElementById("ledger-frame"); +const ledgerStage = document.getElementById("ledger-stage"); const organizationFrame = document.getElementById("organization-frame"); const organizationStage = document.getElementById("organization-stage"); const projectFrame = document.getElementById("project-frame"); @@ -151,7 +153,7 @@ const seatMapState = { forceReadOnly: false, }; -let currentView = "project"; +let currentView = "ledger"; const globalDateState = { loaded: false, startDate: "", @@ -364,6 +366,10 @@ function buildSeatMapAsOfQuery() { } function notifyEmbeddedTabActivated() { + if (currentView === "ledger" && ledgerFrame?.contentWindow) { + ledgerFrame.contentWindow.postMessage({ source: "total-control", type: "embedded-host" }, window.location.origin); + ledgerFrame.contentWindow.postMessage({ source: "total-control", type: "tab-activated", tab: "business" }, window.location.origin); + } if (currentView === "project" && projectFrame?.contentWindow) { projectFrame.contentWindow.postMessage({ source: "total-control", type: "tab-activated", tab: "project" }, window.location.origin); } @@ -372,6 +378,49 @@ function notifyEmbeddedTabActivated() { } } +let ledgerDefaultSourcePromise = null; + +async function fetchDefaultLedgerSource() { + if (!ledgerDefaultSourcePromise) { + ledgerDefaultSourcePromise = fetch("/api/integration/business-ledger-default") + .then(async (response) => { + if (!response.ok) { + throw new Error("기본 사업관리대장 원본을 불러오지 못했습니다."); + } + const fileName = response.headers.get("x-source-filename") || "사업관리대장-1.xlsx"; + const buffer = await response.arrayBuffer(); + if (!buffer || !buffer.byteLength) { + throw new Error("기본 사업관리대장 원본 데이터가 비어 있습니다."); + } + return { fileName, buffer }; + }) + .catch((error) => { + ledgerDefaultSourcePromise = null; + throw error; + }); + } + return ledgerDefaultSourcePromise; +} + +async function pushDefaultLedgerSourceToFrame(force = false) { + if (!ledgerFrame?.contentWindow) return; + if (ledgerFrame.dataset.defaultLedgerLoaded === "true" && !force) return; + try { + const { fileName, buffer } = await fetchDefaultLedgerSource(); + ledgerFrame.contentWindow.postMessage( + { source: "total-control", type: "embedded-host" }, + window.location.origin, + ); + ledgerFrame.contentWindow.postMessage( + { source: "total-upload", type: "business", fileName, buffer }, + window.location.origin, + ); + ledgerFrame.dataset.defaultLedgerLoaded = "true"; + } catch (error) { + console.error("사업관리대장 기본 원본 전달에 실패했습니다.", error); + } +} + async function ensureGlobalDateRangeLoaded() { if (globalDateState.loaded) return; try { @@ -1571,10 +1620,15 @@ function setActiveView(view) { }); const isOrganization = currentView === "organization"; + const isLedger = currentView === "ledger"; const isProject = currentView === "project"; const isTeam = currentView === "team"; const isSeatMapAdmin = currentView === "seatmap-admin"; const isSeatMapReadonly = currentView === "seatmap-readonly"; + if (ledgerStage) { + ledgerStage.hidden = !isLedger; + ledgerStage.style.display = isLedger ? "flex" : "none"; + } if (organizationStage) { organizationStage.hidden = !isOrganization; organizationStage.style.display = isOrganization ? "flex" : "none"; @@ -1596,11 +1650,15 @@ function setActiveView(view) { seatMapReadonlyStage.style.display = isSeatMapReadonly ? "flex" : "none"; } if (emptyStage) { - const showEmpty = !isOrganization && !isProject && !isTeam && !isSeatMapAdmin && !isSeatMapReadonly; + const showEmpty = !isLedger && !isOrganization && !isProject && !isTeam && !isSeatMapAdmin && !isSeatMapReadonly; emptyStage.hidden = !showEmpty; emptyStage.style.display = showEmpty ? "flex" : "none"; } + if (isLedger && previousView !== "ledger" && ledgerFrame) { + const frameSrc = ledgerFrame.dataset.src || ledgerFrame.src; + ledgerFrame.src = resolveAppUrl(frameSrc); + } if (isOrganization && previousView !== "organization" && organizationFrame) { const frameSrc = organizationFrame.dataset.src || organizationFrame.src; organizationFrame.src = resolveAppUrl(frameSrc); @@ -1671,7 +1729,7 @@ if (loginForm) { body: formData, }); setSession(payload); - setActiveView("project"); + setActiveView("ledger"); loginForm.reset(); loginMessage.textContent = ""; renderAuth(); @@ -1728,6 +1786,13 @@ organizationFrame?.addEventListener("load", () => { postOrganizationHistoryState(); }); +ledgerFrame?.addEventListener("load", () => { + if (currentView === "ledger") { + notifyEmbeddedTabActivated(); + } + void pushDefaultLedgerSourceToFrame(true); +}); + projectFrame?.addEventListener("load", () => { postGlobalDateRangeToFrame(projectFrame); if (currentView === "project") { diff --git a/frontend/public/design-patterns.css b/frontend/public/design-patterns.css new file mode 100644 index 0000000..f457752 --- /dev/null +++ b/frontend/public/design-patterns.css @@ -0,0 +1,730 @@ +@import url("/design-tokens.css?v=20260401-01"); + +:root { + --ds-hero-text: #f7f0e4; + --ds-hero-border: rgba(242, 196, 132, 0.22); + --ds-hero-surface: rgba(255, 255, 255, 0.08); + --ds-hero-surface-strong: rgba(255, 255, 255, 0.1); + --ds-hero-text-muted: rgba(255, 244, 230, 0.72); + --ds-hero-text-soft: rgba(255, 244, 230, 0.56); + --ds-hero-line: rgba(242, 196, 132, 0.18); + --ds-danger-soft: rgba(169, 72, 50, 0.1); + --ds-danger-line: rgba(169, 72, 50, 0.22); + --ds-success-soft: rgba(47, 153, 115, 0.14); + --ds-success-line: rgba(47, 153, 115, 0.24); + --ds-brand-soft-surface: rgba(15, 58, 47, 0.1); + --ds-brand-soft-line: rgba(15, 58, 47, 0.18); + --ds-accent-soft-surface: rgba(242, 196, 132, 0.18); + --ds-accent-soft-line: rgba(214, 138, 58, 0.24); +} + +.ds-panel, +.payment-panel { + background: rgba(255, 250, 243, 0.96); + border: 1px solid var(--ds-line-soft); + box-shadow: var(--ds-shadow-soft); +} + +.ds-panel-head, +.payment-panel-head { + background: rgba(255, 250, 243, 0.92); + border-bottom: 1px solid var(--ds-line-soft); +} + +.ds-kpi-card, +.payment-kpi-card { + border: 1px solid var(--ds-line-soft); + background: linear-gradient(180deg, rgba(255, 250, 243, 0.96) 0%, rgba(248, 242, 232, 0.96) 100%); + box-shadow: var(--ds-shadow-soft); + color: var(--ds-ink); +} + +.ds-kpi-inverse, +.payment-kpi-inverse { + color: #fffaf3; +} + +.ds-kpi-people, +.payment-kpi-people { + background: linear-gradient(135deg, var(--ds-brand) 0%, var(--ds-brand-soft) 100%); + border-color: rgba(15, 58, 47, 0.2); +} + +.ds-subhead, +.payment-subhead { + color: var(--ds-text-muted); +} + +.ds-empty, +.payment-empty { + color: #9b937f; +} + +.ds-tooltip, +.payment-tooltip { + background: var(--ds-brand-deep); + color: #fffaf3; +} + +.ds-filter-surface, +.payment-filter-bar { + background: rgba(246, 237, 221, 0.8); + border: 1px solid var(--ds-line); +} + +.ds-filter-toggle, +.payment-filter-toggle { + background: var(--ds-brand); + border-color: rgba(15, 58, 47, 0.28); + color: #fffaf3; +} + +.ds-reset-button, +.payment-reset-btn { + background: rgba(255, 250, 243, 0.96); + border: 1px solid var(--ds-line); + color: var(--ds-text-muted); +} + +.ds-reset-button:hover, +.payment-reset-btn:hover { + color: var(--ds-brand-soft); + background: rgba(255, 255, 255, 0.98); +} + +.ds-table-head, +.payment-table-head { + background: rgba(246, 237, 221, 0.82); +} + +.ds-table-head-row, +.payment-table-head-row { + color: var(--ds-brand-deep); + border-bottom: 1px solid var(--ds-line); +} + +.ds-table-row, +.payment-data-row { + border-color: #f0e5d2; +} + +.ds-table-row:hover, +.payment-data-row:hover { + background: #f6eddd; +} + +.ds-axis-cell, +.payment-axis-cell { + border-right: 1px solid var(--ds-line-soft); +} + +.ds-axis-cell-idle, +.payment-axis-cell-idle { + background: rgba(255, 250, 243, 0.96); + color: var(--ds-ink); +} + +.ds-axis-cell-idle:hover, +.payment-axis-cell-idle:hover { + background: rgba(234, 220, 196, 0.52); + color: var(--ds-brand-deep); +} + +.ds-axis-cell-active, +.payment-axis-cell-active { + background: rgba(234, 220, 196, 0.78); + color: var(--ds-brand-deep); +} + +.ds-project-cell, +.payment-project-cell { + color: var(--ds-brand-deep); + font-weight: 800; +} + +.ds-project-cell:hover, +.payment-project-cell:hover { + background: #efe2ca; + color: #214634; +} + +.ds-income, +.payment-income { + color: var(--ds-status-success); +} + +.ds-expense, +.payment-expense { + color: var(--ds-status-danger); +} + +.ds-progress-track, +.payment-progress-track { + background: rgba(217, 197, 168, 0.45); +} + +.ds-progress-track-grand, +.payment-progress-track-grand { + background: rgba(75, 135, 179, 0.24); +} + +.ds-progress-track-mid, +.payment-progress-track-mid { + background: rgba(214, 138, 58, 0.22); +} + +.ds-mode-chip, +.payment-mode-chip { + color: var(--ds-brand-soft); + background: rgba(242, 196, 132, 0.22); + border: 1px solid rgba(214, 138, 58, 0.28); +} + +.ds-name-chip, +.payment-name-chip { + background: rgba(246, 237, 221, 0.76); + border: 1px solid var(--ds-line-soft); + color: var(--ds-text-soft); +} + +.ds-divider-top, +.payment-divider-top { + border-top: 1px solid var(--ds-line-soft); +} + +.ds-divider-left, +.payment-divider-left { + border-left: 1px solid var(--ds-line-soft); +} + +.ds-divider-mark, +.payment-divider-mark { + color: rgba(183, 170, 147, 0.92); +} + +.ds-mini-table-shell, +.payment-mini-table-shell { + border: 1px solid var(--ds-line-soft); +} + +.ds-mini-table-head, +.payment-mini-table-head { + background: rgba(246, 237, 221, 0.68); + color: var(--ds-text-muted); +} + +.ds-mini-table-row, +.payment-mini-table-row { + border-top: 1px solid rgba(217, 197, 168, 0.36); + color: var(--ds-text-soft); +} + +.ds-group-title, +.payment-group-title { + background: var(--ds-brand); + color: #fffaf3; +} + +.ds-strong, +.payment-strong { + color: var(--ds-ink); +} + +.ds-muted, +.payment-muted { + color: var(--ds-text-soft); +} + +.ds-accent-text, +.payment-icon-accent { + color: var(--ds-brand-soft); +} + +.ds-position-chip, +.position-chip { + background: rgba(246, 237, 221, 0.76); +} + +.ds-position-text, +.position-text { + color: var(--ds-text-soft); +} + +.ds-position-border, +.position-border { + border-color: rgba(217, 197, 168, 0.46); +} + +.ds-position-dot, +.position-dot { + box-shadow: 0 0 0 2px rgba(255, 250, 243, 0.9); +} + +.position-executive.position-chip { background: rgba(15, 58, 47, 0.1); } +.position-executive.position-text { color: var(--ds-brand); } +.position-executive.position-border { border-color: rgba(15, 58, 47, 0.22); } +.position-executive.position-dot { background: var(--ds-brand); } + +.position-principal.position-chip { background: rgba(26, 86, 69, 0.1); } +.position-principal.position-text { color: var(--ds-brand-soft); } +.position-principal.position-border { border-color: rgba(26, 86, 69, 0.22); } +.position-principal.position-dot { background: var(--ds-brand-soft); } + +.position-senior.position-chip { background: rgba(47, 153, 115, 0.12); } +.position-senior.position-text { color: var(--ds-mint); } +.position-senior.position-border { border-color: rgba(47, 153, 115, 0.26); } +.position-senior.position-dot { background: var(--ds-mint); } + +.position-associate.position-chip { background: rgba(75, 135, 179, 0.12); } +.position-associate.position-text { color: var(--ds-info); } +.position-associate.position-border { border-color: rgba(75, 135, 179, 0.22); } +.position-associate.position-dot { background: var(--ds-info); } + +.position-staff.position-chip { background: rgba(214, 138, 58, 0.12); } +.position-staff.position-text { color: var(--ds-status-warning); } +.position-staff.position-border { border-color: rgba(214, 138, 58, 0.24); } +.position-staff.position-dot { background: var(--ds-status-warning); } + +.position-member.position-chip { background: rgba(102, 117, 109, 0.12); } +.position-member.position-text { color: var(--ds-text-soft); } +.position-member.position-border { border-color: rgba(102, 117, 109, 0.24); } +.position-member.position-dot { background: var(--ds-text-soft); } + +.position-unset.position-chip { background: rgba(183, 170, 147, 0.18); } +.position-unset.position-text { color: #8b7e69; } +.position-unset.position-border { border-color: rgba(183, 170, 147, 0.3); } +.position-unset.position-dot { background: #b7aa93; } + +.popup-wrap { + max-width: 1680px; + margin: 0 auto; + padding: 20px; +} + +.popup-head { + margin-bottom: 14px; + padding: 18px 20px; + border: 1px solid rgba(217, 197, 168, 0.62); + border-radius: 24px; + background: linear-gradient(180deg, #fff8ee 0%, #f4e9d7 100%); + box-shadow: 0 18px 36px rgba(15, 58, 47, 0.08); +} + +.popup-title { + font-size: 28px; + font-weight: 900; + line-height: 1.2; + color: var(--ds-ink); +} + +.popup-sub { + margin-top: 6px; + font-size: 13px; + font-weight: 800; + color: var(--ds-text-muted); +} + +.inline-panel { + padding: 0; + display: grid; + gap: 14px; +} + +.project-head-grid { + display: grid; + grid-template-columns: minmax(0, 1.95fr) minmax(280px, 0.72fr); + gap: 10px; + align-items: start; +} + +.project-head-main { + display: flex; + flex-direction: column; + gap: 10px; + min-width: 0; +} + +.project-contact-stack { + display: grid; + grid-template-columns: 1fr; + gap: 8px; + width: 100%; +} + +.inline-card, +.ledger-block, +.popup-wrap .ledger-block.collect { + background: rgba(255, 250, 243, 0.98) !important; + border: 1px solid rgba(217, 197, 168, 0.56) !important; + border-radius: 24px !important; + box-shadow: 0 16px 32px rgba(15, 58, 47, 0.08) !important; +} + +.inline-card { + padding: 16px 18px; +} + +.project-meta-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 10px 12px; +} + +.kv { + padding: 12px 14px; + border-radius: 18px; + background: linear-gradient(180deg, #fffdf8 0%, #f4e9d7 100%); + border: 1px solid rgba(217, 197, 168, 0.46); +} + +.kvk, +.summary-label { + font-size: 11px; + font-weight: 900; + letter-spacing: 0.08em; + text-transform: uppercase; + color: #8a6b3d; +} + +.kvv { + font-size: 15px; + font-weight: 900; + color: var(--ds-ink); + line-height: 1.35; + word-break: keep-all; +} + +.summary-note { + margin-top: 6px; + font-size: 12px; + font-weight: 800; + color: var(--ds-text-muted); + line-height: 1.45; +} + +.summary-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 10px; +} + +.summary-card { + padding: 14px 16px; + border-radius: 18px; + background: linear-gradient(180deg, #fffdf8 0%, #f4e9d7 100%); + border: 1px solid rgba(217, 197, 168, 0.46); + box-shadow: none; +} + +.summary-card.receivable { + background: linear-gradient(180deg, var(--ds-danger-soft) 0%, rgba(255, 248, 238, 0.98) 100%); + border-color: var(--ds-danger-line); +} + +.summary-value { + margin-top: 8px; + font-size: 24px; + font-weight: 900; + color: var(--ds-ink); + line-height: 1.15; +} + +.summary-card.receivable .summary-value { + color: var(--ds-status-danger); +} + +.project-progress { + margin-top: 10px; + height: 12px; + border-radius: var(--ds-radius-pill); + overflow: hidden; + background: rgba(217, 197, 168, 0.48); + box-shadow: inset 0 1px 2px rgba(15, 58, 47, 0.08); +} + +.progress .bar { + height: 100%; + border-radius: var(--ds-radius-pill); + background: linear-gradient(90deg, var(--ds-brand-soft) 0%, var(--ds-mint) 100%); + box-shadow: 0 8px 18px rgba(47, 153, 115, 0.18); +} + +.ledger-stack { + display: grid; + gap: 12px; +} + +.ledger-head { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 12px; + padding: 18px 18px 14px; + border-bottom: 1px solid rgba(217, 197, 168, 0.38) !important; + background: linear-gradient(180deg, #fffdf8 0%, #f4e9d7 100%) !important; +} + +.ledger-head-left { + display: flex; + align-items: flex-start; + gap: 12px; + min-width: 0; +} + +.ledger-icon { + width: 36px; + height: 36px; + border-radius: 12px; + display: inline-flex; + align-items: center; + justify-content: center; + background: linear-gradient(180deg, #fff8ee 0%, #f2dec0 100%) !important; + color: var(--ds-accent-strong) !important; + font-weight: 900; + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.65); +} + +.ledger-name { + font-size: 16px; + font-weight: 900; + color: var(--ds-ink) !important; + line-height: 1.2; +} + +.ledger-sub { + margin-top: 4px; + font-size: 12px; + font-weight: 800; + color: var(--ds-text-muted) !important; + line-height: 1.45; +} + +.ledger-pill { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 8px 12px; + border-radius: var(--ds-radius-pill); + background: var(--ds-brand-soft-surface) !important; + border: 1px solid var(--ds-brand-soft-line) !important; + color: var(--ds-brand-soft) !important; + font-size: 12px; + font-weight: 900; + white-space: nowrap; +} + +.ledger-table-wrap { + padding: 0 16px 16px; + background: transparent !important; +} + +.ledger-table { + width: 100%; + border-collapse: collapse; + background: transparent !important; +} + +.ledger-table thead th { + padding: 12px 10px; + background: var(--ds-brand) !important; + color: #fff5e6 !important; + font-size: 12px; + font-weight: 900; + text-align: left; + border-right: 1px solid rgba(242, 196, 132, 0.18) !important; +} + +.ledger-table thead th:last-child { + border-right: 0; +} + +.ledger-table tbody td { + padding: 12px 10px; + border-bottom: 1px solid rgba(217, 197, 168, 0.34) !important; + vertical-align: top; + font-size: 13px; + font-weight: 800; + color: var(--ds-ink) !important; + background: rgba(255, 250, 243, 0.72) !important; +} + +.ledger-table tbody tr:last-child td { + border-bottom: 0; +} + +.ledger-main { + display: block; + color: var(--ds-ink) !important; + font-weight: 900; +} + +.ledger-muted, +.ledger-note { + display: block; + margin-top: 4px; + color: var(--ds-text-muted) !important; + font-size: 12px; + font-weight: 800; + line-height: 1.45; +} + +.ledger-amount { + font-weight: 900; + text-align: right; +} + +.badge { + display: inline-flex; + align-items: center; + justify-content: center; + min-height: 30px; + padding: 0 12px; + border-radius: var(--ds-radius-pill); + border: 1px solid rgba(217, 197, 168, 0.5); + background: rgba(255, 250, 243, 0.96); + color: #17392f; + font-size: 12px; + font-weight: 900; +} + +.badge.badge-baron { + background: var(--ds-brand-soft-surface); + border-color: var(--ds-brand-soft-line); + color: var(--ds-brand-soft); +} + +.badge.badge-family { + background: var(--ds-accent-soft-surface); + border-color: var(--ds-accent-soft-line); + color: var(--ds-status-warning); +} + +.badge.ok { + background: var(--ds-success-soft); + border-color: var(--ds-success-line); + color: var(--ds-brand-soft); +} + +.project-link { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 0; + border: 0; + background: none; + color: #17392f; + font: inherit; + font-weight: 900; + text-align: left; + cursor: pointer; +} + +.project-link:hover { + color: #0f6a55; +} + +.member-form-label { + color: var(--ds-text-soft); + font-size: 12px; + font-weight: 900; + letter-spacing: 0.04em; +} + +.member-form-input, +.member-form-select, +.member-form-time { + border: 1px solid var(--ds-line-soft); + border-radius: 16px; + background: rgba(255, 250, 243, 0.92); + color: var(--ds-ink); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.65); +} + +.member-form-input:focus, +.member-form-select:focus, +.member-form-time:focus { + border-color: rgba(47, 153, 115, 0.45); + box-shadow: 0 0 0 4px rgba(47, 153, 115, 0.1); +} + +.modal-btn { + min-height: 40px; + padding: 0 16px; + border-radius: var(--ds-radius-pill); + font-size: 12px; + font-weight: 900; + border: 1px solid transparent; +} + +.modal-btn-cancel { + background: rgba(255, 250, 243, 0.96); + border-color: var(--ds-line); + color: var(--ds-text-soft); +} + +.modal-btn-save { + background: var(--ds-brand-soft); + border-color: rgba(15, 58, 47, 0.22); + color: #fffaf3; +} + +.modal-btn-delete { + background: rgba(169, 72, 50, 0.12); + border-color: rgba(169, 72, 50, 0.24); + color: var(--ds-status-danger); +} + +.modal-btn-close { + background: rgba(242, 196, 132, 0.18); + border-color: rgba(214, 138, 58, 0.24); + color: var(--ds-status-warning); +} + +.seatmap-actions .ghost-button { + min-height: 40px; + padding: 0 16px; + border-width: 1px; + border-style: solid; + border-radius: var(--ds-radius-pill); + font-size: 12px; + letter-spacing: -0.01em; + box-shadow: var(--ds-shadow-soft); +} + +@media (max-width: 1180px) { + .project-head-grid { + grid-template-columns: 1fr; + } + + .summary-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .project-meta-grid { + grid-template-columns: 1fr; + } +} + +@media (max-width: 760px) { + .popup-wrap { + padding: 14px; + } + + .summary-grid { + grid-template-columns: 1fr; + } + + .ledger-head { + flex-direction: column; + align-items: flex-start; + } + + .ledger-pill { + white-space: normal; + } + + .ledger-table-wrap { + padding: 0 10px 12px; + overflow-x: auto; + } +} diff --git a/frontend/public/design-tokens.css b/frontend/public/design-tokens.css new file mode 100644 index 0000000..be5b46d --- /dev/null +++ b/frontend/public/design-tokens.css @@ -0,0 +1,60 @@ +:root { + --ds-font-sans: "Pretendard", "Malgun Gothic", sans-serif; + + --ds-bg: #f1eadf; + --ds-bg-soft: #f4e9d7; + --ds-bg-gradient: + radial-gradient(circle at top left, rgba(214, 138, 58, 0.18), transparent 24%), + radial-gradient(circle at top right, rgba(47, 153, 115, 0.12), transparent 22%), + linear-gradient(180deg, #f6efe6 0%, #f1eadf 100%); + + --ds-panel: #fffaf3; + --ds-panel-soft: rgba(255, 250, 243, 0.9); + --ds-panel-strong: #eadcc4; + + --ds-ink: #10251d; + --ds-text-soft: #425148; + --ds-text-muted: #66756d; + + --ds-line: #d9c5a8; + --ds-line-soft: rgba(217, 197, 168, 0.45); + + --ds-brand: #0f3a2f; + --ds-brand-deep: #0a2a22; + --ds-brand-soft: #1a5645; + --ds-accent: #d68a3a; + --ds-accent-soft: #f2c484; + --ds-accent-strong: #b66e22; + --ds-mint: #2f9973; + --ds-info: #4b87b3; + + --ds-status-success: #2f6b52; + --ds-status-warning: #9a6422; + --ds-status-danger: #a94832; + + --ds-surface-tint: rgba(255, 255, 255, 0.72); + --ds-surface-tint-strong: rgba(255, 255, 255, 0.88); + --ds-glass-dark: rgba(20, 45, 37, 0.34); + --ds-glass-dark-soft: rgba(16, 37, 29, 0.16); + --ds-glass-line: rgba(255, 255, 255, 0.14); + + --ds-shadow-soft: 0 10px 24px rgba(15, 58, 47, 0.08); + --ds-shadow-card: 0 22px 54px rgba(15, 58, 47, 0.12); + --ds-shadow-float: 0 18px 36px rgba(15, 58, 47, 0.16); + --ds-shadow-hero: 0 28px 70px rgba(15, 58, 47, 0.22); + + --ds-radius-sm: 8px; + --ds-radius-md: 12px; + --ds-radius-lg: 18px; + --ds-radius-xl: 24px; + --ds-radius-pill: 999px; + + --ds-space-1: 4px; + --ds-space-2: 8px; + --ds-space-3: 12px; + --ds-space-4: 16px; + --ds-space-5: 20px; + --ds-space-6: 24px; + + --ds-page-max-width: 2000px; +} diff --git a/frontend/public/index.html b/frontend/public/index.html index 0f0d244..9e2b7b5 100644 --- a/frontend/public/index.html +++ b/frontend/public/index.html @@ -12,8 +12,13 @@ + + + + + - + diff --git a/frontend/public/styles-8081-design.css b/frontend/public/styles-8081-design.css new file mode 100644 index 0000000..213a1b5 --- /dev/null +++ b/frontend/public/styles-8081-design.css @@ -0,0 +1,100 @@ +.dashboard-header { + min-height: 68px; + background: + radial-gradient(circle at 12% 18%, rgba(242, 196, 132, 0.16), transparent 24%), + linear-gradient(145deg, rgba(10, 42, 34, 0.96) 0%, rgba(15, 58, 47, 0.96) 52%, rgba(26, 86, 69, 0.96) 100%); + color: #f7f0e4; + border-bottom: 1px solid rgba(242, 196, 132, 0.22); + backdrop-filter: blur(16px); + box-shadow: var(--ds-shadow-float); +} + +.dashboard-header .eyebrow { + color: rgba(242, 196, 132, 0.94); +} + +.dashboard-header h2 { + color: #fff7ea; +} + +.nav-pill { + min-height: 42px; + padding: 0 14px; + border-radius: 999px; + border: 1px solid rgba(242, 196, 132, 0.28); + background: rgba(255, 255, 255, 0.08); + color: rgba(255, 244, 230, 0.78); + font-size: 14px; + font-weight: 800; +} + +.nav-pill.active { + background: linear-gradient(180deg, rgba(255, 253, 248, 0.98), rgba(245, 235, 221, 0.94)); + border-color: rgba(242, 196, 132, 0.34); + color: var(--ds-ink); + box-shadow: var(--ds-shadow-float); +} + +.nav-pill.muted { + color: rgba(255, 244, 230, 0.48); +} + +.nav-pill:hover { + color: #fff7ea; + border-color: rgba(242, 196, 132, 0.48); +} + +.header-actions { + border-left: 1px solid rgba(242, 196, 132, 0.2); +} + +.header-date-label { + color: rgba(255, 244, 230, 0.72); +} + +.header-date-field { + border: 1px solid rgba(242, 196, 132, 0.22); + background: rgba(255, 255, 255, 0.1); +} + +.header-date-field input, +.header-date-field select { + color: #fff7ea; +} + +.header-date-sep { + color: rgba(255, 244, 230, 0.56); +} + +.ghost-button { + border: 1px solid rgba(242, 196, 132, 0.22); + background: rgba(255, 255, 255, 0.08); + color: #fff7ea; +} + +.icon-button { + background: rgba(255, 255, 255, 0.1); +} + +.icon-button:hover { + background: rgba(242, 196, 132, 0.14); + border-color: rgba(242, 196, 132, 0.32); + color: #fff7ea; +} + +.ghost-button-soft { + background: rgba(239, 228, 208, 0.92); +} + +.seatmap-status[data-tone="error"] { + color: var(--ds-status-danger); +} + +.seatmap-status[data-tone="success"] { + color: var(--ds-status-success); +} + +.seatmap-board-wrap, +.seatmap-dxf-canvas { + background: var(--ds-panel); +} diff --git a/frontend/public/styles.css b/frontend/public/styles.css index d622efe..6003b4e 100644 --- a/frontend/public/styles.css +++ b/frontend/public/styles.css @@ -1,3 +1,30 @@ +:root { + --color-bg: var(--ds-bg); + --color-surface: var(--ds-panel); + --color-surface-soft: var(--ds-panel-soft); + --color-surface-strong: var(--ds-panel-strong); + --color-text: var(--ds-ink); + --color-text-soft: var(--ds-text-soft); + --color-text-muted: var(--ds-text-muted); + --color-border: var(--ds-line); + --color-border-soft: var(--ds-line-soft); + --color-brand: var(--ds-brand); + --color-brand-deep: var(--ds-brand-deep); + --color-brand-soft: var(--ds-brand-soft); + --color-accent: var(--ds-accent); + --color-accent-soft: var(--ds-accent-soft); + --color-success: var(--ds-status-success); + --color-danger: var(--ds-status-danger); + --radius-sm: var(--ds-radius-sm); + --radius-md: var(--ds-radius-md); + --radius-lg: var(--ds-radius-lg); + --radius-xl: var(--ds-radius-xl); + --radius-pill: var(--ds-radius-pill); + --shadow-soft: var(--ds-shadow-soft); + --shadow-card: var(--ds-shadow-card); + --shadow-float: var(--ds-shadow-float); +} + .dashboard-shell, .dashboard-main, .main-stage, @@ -31,7 +58,7 @@ body { min-height: 100vh; padding: 24px; background: - linear-gradient(135deg, rgba(15, 23, 42, 0.42), rgba(30, 41, 59, 0.18)), + linear-gradient(135deg, rgba(10, 42, 34, 0.42), rgba(26, 86, 69, 0.18)), url("https://images.unsplash.com/photo-1486406146926-c627a92ad1ab?auto=format&fit=crop&w=1800&q=80") center center / cover no-repeat; } @@ -54,10 +81,10 @@ body { display: grid; grid-template-columns: 1.3fr 0.7fr; overflow: hidden; - border: 1px solid rgba(255, 255, 255, 0.14); + border: 1px solid var(--ds-glass-line); border-radius: var(--radius-lg); - background: rgba(71, 85, 105, 0.34); - box-shadow: 0 24px 60px rgba(15, 23, 42, 0.24); + background: var(--ds-glass-dark); + box-shadow: var(--ds-shadow-hero); backdrop-filter: blur(14px); } @@ -68,8 +95,8 @@ body { padding: 30px 30px; border-right: 1px solid rgba(255, 255, 255, 0.08); background: - linear-gradient(90deg, rgba(15, 23, 42, 0.08), rgba(255, 255, 255, 0.02)), - linear-gradient(180deg, rgba(255, 255, 255, 0.02), rgba(15, 23, 42, 0.08)); + linear-gradient(90deg, rgba(10, 42, 34, 0.08), rgba(255, 255, 255, 0.02)), + linear-gradient(180deg, rgba(255, 255, 255, 0.02), rgba(10, 42, 34, 0.08)); } .login-brand .eyebrow { @@ -83,7 +110,7 @@ body { font-size: clamp(1.7rem, 3.2vw, 2.5rem); line-height: 0.96; letter-spacing: -0.04em; - color: #f8fafc; + color: #f7f0e4; } .login-form-wrap { @@ -91,7 +118,7 @@ body { display: grid; align-content: center; gap: 10px; - background: rgba(15, 23, 42, 0.12); + background: var(--ds-glass-dark-soft); } .login-card label { @@ -140,8 +167,8 @@ body { margin-top: 2px; border: none; color: #fff; - background: rgba(31, 41, 55, 0.82); - box-shadow: 0 14px 30px rgba(15, 23, 42, 0.22); + background: rgba(10, 42, 34, 0.82); + box-shadow: var(--shadow-float); min-height: 34px; border-radius: 999px; font-size: 11px; @@ -167,9 +194,9 @@ body { .dashboard-header { min-height: 68px; - background: rgba(255, 255, 255, 0.94); + background: rgba(255, 250, 243, 0.94); color: var(--color-text); - border-bottom: 1px solid #d7dee8; + border-bottom: 1px solid var(--color-border); display: flex; align-items: center; justify-content: space-between; @@ -241,7 +268,7 @@ body { border: none; border-bottom: 3px solid transparent; background: transparent; - color: #64748b; + color: var(--color-text-muted); font-size: 15px; font-weight: 700; cursor: pointer; @@ -255,7 +282,7 @@ body { } .nav-pill.muted { - color: #94a3b8; + color: rgba(102, 117, 109, 0.64); } .nav-pill:hover { @@ -269,7 +296,7 @@ body { gap: 6px; position: relative; padding-left: 18px; - border-left: 1px solid #dbe2ea; + border-left: 1px solid var(--color-border); } .header-date-controls { @@ -284,7 +311,7 @@ body { .header-date-label { font-size: 12px; font-weight: 800; - color: #64748b; + color: var(--color-text-muted); } .header-date-field { @@ -292,9 +319,9 @@ body { align-items: center; min-height: 36px; padding: 0 10px; - border: 1px solid #dbe2ea; + border: 1px solid var(--color-border); border-radius: 999px; - background: #fff; + background: var(--color-surface); } .header-date-field input { @@ -318,15 +345,15 @@ body { } .header-date-sep { - color: #94a3b8; + color: var(--color-text-muted); font-size: 12px; font-weight: 800; } .ghost-button { min-height: 34px; - border: 1px solid #dbe2ea; - background: #fff; + border: 1px solid var(--color-border); + background: var(--color-surface); color: var(--color-text); padding: 0 12px; border-radius: 999px; @@ -342,12 +369,12 @@ body { display: inline-flex; align-items: center; justify-content: center; - background: #f8fafc; + background: var(--color-surface-soft); } .icon-button:hover { - background: #f1f5f9; - border-color: #cbd5e1; + background: var(--ds-bg-soft); + border-color: var(--color-border); color: var(--color-accent); transform: translateY(-1px); } @@ -363,7 +390,7 @@ body { } .ghost-button-soft { - background: #f8fafc; + background: var(--color-surface-soft); } .user-chip { @@ -381,8 +408,8 @@ body { width: 18px; height: 18px; border-radius: 50%; - background: #e2e8f0; - color: #475569; + background: var(--color-surface-strong); + color: var(--color-text-soft); font-size: 10px; font-weight: 900; flex: 0 0 auto; @@ -421,10 +448,10 @@ body { right: 0; min-width: 220px; padding: 14px; - border: 1px solid #dbe2ea; + border: 1px solid var(--color-border); border-radius: 16px; - background: rgba(255, 255, 255, 0.96); - box-shadow: 0 18px 36px rgba(15, 23, 42, 0.14); + background: rgba(255, 250, 243, 0.96); + box-shadow: var(--shadow-float); backdrop-filter: blur(12px); z-index: 30; } @@ -440,7 +467,7 @@ body { } .user-popover-row + .user-popover-row { - border-top: 1px solid #eef2f7; + border-top: 1px solid rgba(217, 197, 168, 0.4); } .user-popover-label { @@ -454,7 +481,7 @@ body { min-height: 38px; border: none; border-radius: 12px; - background: #0f172a; + background: var(--color-brand); color: #fff; font-size: 11px; font-weight: 800; @@ -485,7 +512,7 @@ body { width: 100%; height: 100%; border: 0; - background: #fff; + background: var(--color-surface); } .stage-empty { @@ -502,9 +529,7 @@ body { gap: 12px; padding: 18px; overflow: hidden; - background: - linear-gradient(180deg, rgba(248, 250, 252, 0.94), rgba(241, 245, 249, 0.92)), - radial-gradient(circle at top left, rgba(14, 165, 233, 0.1), transparent 32%); + background: var(--ds-bg-gradient); } .seatmap-topbar { @@ -561,6 +586,54 @@ body { display: none !important; } +.seatmap-actions .ghost-button { + min-height: 40px; + padding: 0 16px; + border-width: 1px; + border-style: solid; + border-radius: var(--radius-pill); + font-size: 12px; + letter-spacing: -0.01em; + box-shadow: var(--shadow-soft); +} + +#seatmap-admin-save-btn { + border-color: var(--color-brand-soft); + background: var(--color-brand-soft); + color: #fffaf3; +} + +#seatmap-admin-save-btn:hover:not(:disabled) { + background: var(--color-brand); + border-color: var(--color-brand); + transform: translateY(-1px); + box-shadow: var(--shadow-float); +} + +#seatmap-admin-save-btn:disabled { + opacity: 1; + cursor: not-allowed; + border-color: rgba(26, 86, 69, 0.24); + background: rgba(26, 86, 69, 0.18); + color: rgba(16, 37, 29, 0.72); + box-shadow: none; +} + +#seatmap-admin-exit-btn, +#seatmap-readonly-exit-btn { + border-color: rgba(214, 138, 58, 0.48); + background: rgba(242, 196, 132, 0.22); + color: var(--color-brand-deep); +} + +#seatmap-admin-exit-btn:hover, +#seatmap-readonly-exit-btn:hover { + background: rgba(242, 196, 132, 0.34); + border-color: rgba(182, 110, 34, 0.56); + color: var(--color-brand); + transform: translateY(-1px); +} + .seatmap-status { min-height: 20px; margin: 0; diff --git a/incoming-files/README.md b/incoming-files/README.md new file mode 100644 index 0000000..eacefdb --- /dev/null +++ b/incoming-files/README.md @@ -0,0 +1,38 @@ +# incoming-files Layout + +`8081` 1차 구조 정리 기준으로 `incoming-files`는 아래처럼 해석한다. + +## Served + +- 실제 URL에서 직접 서빙되는 HTML +- 현재 사용 파일: + - `served/payment.html` + - `served/mh.html` + - `served/ledger/index.html` + +주의: + +- backend `/integrations/payment`, `/integrations/mh`는 위 `served/*`만 읽는다. +- backend `/integrations/ledger`와 `/integrations/ledger-assets/*`도 `served/ledger/*`만 읽는다. +- 새 기능을 붙일 때도 실제 서비스 파일은 `served/` 기준으로 수정한다. + +## Reference + +- 원본 참고 자산 +- 복구 비교용 자산 +- 직접 서빙하지 않는 파일 + +예: + +- 원본 `xlsx`, `csv` +- 샘플 스타일 파일 +- 원본/백업 HTML +- 디자인 비교용 파일 +- `reference/ledger/MH 통합 대시보드_260320.html` +- `reference/ledger/MH 통합 대시보드_260320.css` + +## Temporary Comparison Copies + +- 현재 루트의 `payment.html`, `mh.html`은 당장 삭제하지 않는다. +- 이 두 파일은 기존 recovery 작업본과 현재 `served/*`를 비교하거나 되돌릴 때만 본다. +- 다음 차수에서 안전성이 확보되면 `reference/` 하위로 재배치 여부를 검토한다. diff --git a/incoming-files/mh.html b/incoming-files/mh.html index a32fa73..4518e67 100644 --- a/incoming-files/mh.html +++ b/incoming-files/mh.html @@ -19,41 +19,52 @@ + .mh-person-calendar-day { + min-height: 72px; + padding: 0.45rem; + } + } + + :root { + --theme-info-bg: #e8f1ee; + --theme-info-line: #bfd3cb; + --theme-info-text: #1f5f49; + --theme-neutral-bg: #f4ede1; + --theme-neutral-text: #5e6d63; + --theme-success-bg: #e1f0e7; + --theme-success-text: #256548; + --theme-overlay: rgba(44, 39, 31, 0.42); + } + #mh-date-row .mh-date-label { color: var(--theme-primary); } + .mh-kpi-subitem { background: var(--theme-surface-soft); border-color: var(--theme-border); } + .mh-kpi-subitem .label { color: var(--theme-text-muted); } + .mh-kpi-subitem .value { color: var(--theme-text-dark); } + .mh-kpi-chip.warn { background: var(--theme-warning-bg); border-color: var(--theme-warning-line); color: var(--theme-warning-text); } + .mh-kpi-empty { color: var(--theme-text-muted); } + .mh-matrix-card-head { background: var(--biz-bg, var(--theme-primary-bg)); border-bottom-color: var(--biz-border, var(--theme-primary-line)); } + .mh-matrix-card-count, .mh-matrix-total-badge, .mh-matrix-toggle-icon { color: var(--biz-text, var(--theme-primary)); } + .mh-person-calendar-day.today { border-color: var(--theme-primary-line); background: var(--theme-primary-bg); box-shadow: inset 0 0 0 1px var(--theme-primary-line); } + .mh-person-calendar-day.today .mh-person-calendar-daynum { background: var(--theme-primary); } + .mh-person-calendar-note { background: var(--theme-primary-bg); color: var(--theme-primary); } + .mh-person-calendar-note.neutral { background: var(--theme-neutral-bg); color: var(--theme-neutral-text); } + .mh-person-calendar-note.warn { background: var(--theme-danger-bg); color: var(--theme-danger-text); } + .mh-person-calendar-note.orange { background: var(--theme-warning-bg); color: var(--theme-warning-text); } + .mh-person-calendar-note.green { background: var(--theme-success-bg); color: var(--theme-success-text); } + .mh-person-calendar-summary .attendance { color: var(--theme-text-base); } + .mh-person-calendar-summary .extraHoliday { color: var(--theme-warning-text); } + .mh-person-calendar-summary .tripEdu { color: var(--theme-success-text); } + .mh-person-calendar-summary .late { color: var(--theme-danger-text); } + .mh-person-calendar-nav button:hover:not(:disabled) { border-color: var(--theme-primary-line); background: var(--theme-primary-bg); color: var(--theme-primary); } + #project-members-modal .absolute.inset-0, #person-calendar-overlay { background: var(--theme-overlay) !important; } + #project-members-dialog, #person-calendar-modal > .bg-white { background: var(--theme-surface) !important; border: 1px solid var(--theme-border) !important; box-shadow: var(--theme-shadow) !important; } + #project-members-dialog .bg-slate-50\/50, #person-calendar-modal .bg-slate-50\/50, #mh-matrix-section .bg-slate-50\/50, #mh-person-section .bg-slate-50\/50 { background: var(--theme-surface-soft) !important; } + #project-members-dialog .text-slate-900, #person-calendar-modal .text-slate-900, #mh-topbar .text-slate-900, #mh-matrix-section .text-slate-900, #mh-person-section .text-slate-900 { color: var(--theme-text-dark) !important; } + #project-members-dialog .text-slate-400, #person-calendar-modal .text-slate-400, #mh-matrix-section .text-slate-300, #mh-person-section .text-slate-400, #chart_div.text-slate-200 { color: var(--theme-text-muted) !important; } + #project-members-dialog .border-b, #project-members-dialog .border-t, #person-calendar-modal .border-b, #mh-topbar .border-slate-200, #mh-filter-grid.border-slate-200, #search-dropdown.border-slate-200, #mh-inline-search .border-slate-200, #mh-inline-team .border-slate-200, #kpi-container .border-slate-200, #mh-matrix-section.border-slate-100, #mh-person-section.border-slate-100, #mh-person-section .border-red-100\/50 { border-color: var(--theme-border) !important; } + #project-members-dialog button.hover\:bg-slate-200:hover, #person-calendar-modal button.hover\:bg-slate-200:hover { background: var(--theme-primary-bg) !important; color: var(--theme-primary) !important; } + #project-members-dialog .bg-slate-900, #person-calendar-modal .bg-slate-900 { background: var(--theme-primary) !important; } + #project-members-dialog .hover\:bg-slate-800:hover, #person-calendar-modal .hover\:bg-slate-800:hover { background: var(--theme-primary-hover) !important; } + #mh-topbar .text-indigo-600, #mh-matrix-section .text-indigo-500, #mh-person-section .text-indigo-600, #mh-filter-grid .text-indigo-600 { color: var(--theme-primary) !important; } + #mh-person-section .bg-red-50\/50 { background: var(--theme-danger-bg) !important; } + #mh-person-section .text-red-400, #mh-person-section .text-red-600 { color: var(--theme-danger-text) !important; } + .mh-soft-surface { + background: var(--theme-surface-soft) !important; + border-color: var(--theme-border) !important; + } + .mh-overlay { + background: var(--theme-overlay) !important; + } + .mh-icon-btn { + color: var(--theme-text-muted) !important; + } + .mh-icon-btn:hover { + background: var(--theme-primary-bg) !important; + color: var(--theme-primary) !important; + } + .mh-primary-btn { + background: var(--theme-primary) !important; + } + .mh-primary-btn:hover { + background: var(--theme-primary-hover) !important; + } + .mh-accent-icon { + color: var(--theme-primary) !important; + } + .mh-empty-copy { + color: var(--theme-text-muted) !important; + } + .mh-title-dark { color: var(--theme-text-dark) !important; } + .mh-text-muted { color: var(--theme-text-muted) !important; } + .mh-text-base { color: var(--theme-text-base) !important; } + .mh-border-ui { border-color: var(--theme-border) !important; } + .mh-card-shell { background: var(--theme-surface) !important; border-color: var(--theme-border) !important; } + .mh-loading-card { background: var(--theme-surface) !important; border-color: var(--theme-border) !important; } + .mh-search-input:focus { border-color: var(--theme-primary-line) !important; } + .mh-muted-sep { color: var(--theme-text-muted) !important; } + +