diff --git a/backend/app/main.py b/backend/app/main.py index 6096819..ca329ee 100755 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -45,8 +45,8 @@ INCOMING_FILES_DIR = BASE_DIR / "incoming-files" INCOMING_SERVED_DIR = INCOMING_FILES_DIR / "served" INCOMING_REFERENCE_DIR = INCOMING_FILES_DIR / "reference" BUSINESS_DASHBOARD_DIR = INCOMING_FILES_DIR / "사업관리대장" -BUSINESS_DASHBOARD_WRAPPER_PATH = BUSINESS_DASHBOARD_DIR / "MH 통합 대시보드_260320.html" -BUSINESS_DASHBOARD_THEME_CSS = BUSINESS_DASHBOARD_DIR / "MH 통합 대시보드_260320.css" +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": { @@ -66,7 +66,6 @@ FIXED_OFFICE_CONFIGS = { }, } _fixed_office_cache: dict[str, dict[str, object]] = {} -_business_ledger_html_cache: str | None = None BUSINESS_LEDGER_DEFAULT_SOURCE_KEY = "business_ledger_default" AUTH_DEFAULT_PASSWORD = "1111" AUTH_PASSWORD_ITERATIONS = 390000 @@ -89,34 +88,9 @@ MH_HEADER_ORDER = [ "사업 종류", "연장근무 프로젝트 코드", "연장근무 프로젝트명", "연장근무 서브코드", "연장근무 시간(실제)", "연장근무 시간(가공)" ] - -def build_business_ledger_html() -> str: - global _business_ledger_html_cache - if _business_ledger_html_cache is not None: - return _business_ledger_html_cache - if not BUSINESS_DASHBOARD_WRAPPER_PATH.exists(): - raise FileNotFoundError("Business dashboard wrapper file not found.") - source = BUSINESS_DASHBOARD_WRAPPER_PATH.read_text(encoding="utf-8-sig") - match = re.search(r"const BUSINESS_HTML_B64='([^']+)';", source) - if not match: - raise ValueError("Embedded business ledger source was not found.") - decoded = base64.b64decode(match.group(1)).decode("utf-8") - head_injection = ( - '' - '' - '' - ) - html = decoded.replace("", f"{head_injection}", 1) - html = html.replace("", '', 1) - html = html.replace("", '', 1) - _business_ledger_html_cache = html - return html - - def sync_default_business_ledger_source(cur) -> None: - if not BUSINESS_DASHBOARD_DIR.exists(): - return candidates = [ + BUSINESS_LEDGER_SERVED_DIR / "사업관리대장-1.xlsx", BUSINESS_DASHBOARD_DIR / "사업관리대장-1.xlsx", BUSINESS_DASHBOARD_DIR / "사업관리 대장-1.xlsx", BUSINESS_DASHBOARD_DIR / "사업관리대장.xlsx", @@ -165,7 +139,7 @@ def sync_default_business_ledger_source(cur) -> None: app.mount( "/integrations/ledger-assets", - StaticFiles(directory=str(BUSINESS_DASHBOARD_DIR), check_dir=False), + StaticFiles(directory=str(BUSINESS_LEDGER_SERVED_DIR), check_dir=False), name="integration-ledger-assets", ) @@ -4627,20 +4601,16 @@ def integration_payment() -> FileResponse: @app.get("/integrations/ledger") -def integration_ledger() -> HTMLResponse: - try: - html = build_business_ledger_html() - except FileNotFoundError: +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.") - except ValueError: - raise HTTPException(status_code=500, detail="Business ledger integration source is invalid.") - return HTMLResponse( - html, - headers={ - "Cache-Control": "no-store, no-cache, must-revalidate, max-age=0", - "Pragma": "no-cache", - }, - ) + 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") 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 b28d7a0..03bae81 100644 --- a/docs/NEXT_SESSION_CHECKPOINT.md +++ b/docs/NEXT_SESSION_CHECKPOINT.md @@ -57,7 +57,11 @@ - `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`를 참조 -- 사업관리대장 상세 팝업 디자인은 [incoming-files/사업관리대장/ledger-override.js](/home/hyunho/projects/mh-dashboard-organization/.dev-worktree-8081/incoming-files/사업관리대장/ledger-override.js)에서 `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) 기준으로 본다. 디자인 수정 우선순위: @@ -98,6 +102,8 @@ - [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) @@ -109,6 +115,7 @@ - `/api/health` 200 - `/legacy/organization` 200 - `/integrations/payment` 200 + - `/integrations/ledger` 200 - `/integrations/mh` 200 - `incoming-files/served` 내 실제 서빙 파일 존재 확인 @@ -125,13 +132,14 @@ - `#18` 8081 파일 책임 맵 정리 및 프런트 서빙 경로 정돈 - `#19` 8081 백엔드 라우터/서빙 책임 분리 - `#20` 8081 worktree 준비 스크립트·문서·운영 규칙 정리 +- `#21` reference 의존 제거 및 8081 실제 서비스 코드 독립화 ## Recommended Next Work Order -1. `#18` 범위에서 실제 서빙 파일과 비교용 파일 경계를 더 명확히 정리 -2. 사업관리대장 탭 기능 추가 전에 수정 대상 파일을 고정 -3. 그 다음 `#19`로 backend 라우터/서빙 책임 분리 -4. 마지막으로 `#20`에서 스크립트/문서/운영 규칙 정리 +1. `#21` 이후 기준으로 실제 서비스 파일과 reference 파일 경계를 유지 +2. 사업관리대장 세부 데이터 정합성 보정 +3. 그 다음 화면별 앱 구조 승격 검토 +4. 필요 시 `#19`, `#20` 잔여 정리 항목 재평가 ## Quick Resume Prompt @@ -141,5 +149,5 @@ - `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) 먼저 확인 -- 현재 1차 구조 정리 기준 이슈는 `#18` -- 작업 전 `git status`, dev 컨테이너 상태, `/api/health`, `/legacy/organization`, `/integrations/payment`, `/integrations/mh`를 먼저 확인 +- 현재 구조 독립화 기준 이슈는 `#21` +- 작업 전 `git status`, dev 컨테이너 상태, `/api/health`, `/legacy/organization`, `/integrations/payment`, `/integrations/ledger`, `/integrations/mh`를 먼저 확인 diff --git a/docs/architecture/8081_SERVING_MAP.md b/docs/architecture/8081_SERVING_MAP.md index f9d82f2..f00447f 100644 --- a/docs/architecture/8081_SERVING_MAP.md +++ b/docs/architecture/8081_SERVING_MAP.md @@ -36,14 +36,25 @@ - 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 @@ -80,7 +91,8 @@ - `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 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/incoming-files/사업관리대장/ledger-override.css b/frontend/apps/ledger/assets/ledger-override.css similarity index 100% rename from incoming-files/사업관리대장/ledger-override.css rename to frontend/apps/ledger/assets/ledger-override.css diff --git a/incoming-files/사업관리대장/ledger-override.js b/frontend/apps/ledger/assets/ledger-override.js similarity index 100% rename from incoming-files/사업관리대장/ledger-override.js rename to frontend/apps/ledger/assets/ledger-override.js 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/incoming-files/README.md b/incoming-files/README.md index 000dad1..eacefdb 100644 --- a/incoming-files/README.md +++ b/incoming-files/README.md @@ -8,10 +8,12 @@ - 현재 사용 파일: - `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 @@ -26,6 +28,8 @@ - 샘플 스타일 파일 - 원본/백업 HTML - 디자인 비교용 파일 +- `reference/ledger/MH 통합 대시보드_260320.html` +- `reference/ledger/MH 통합 대시보드_260320.css` ## Temporary Comparison Copies diff --git a/incoming-files/reference/README.md b/incoming-files/reference/README.md index cd52bc4..ddf5ad6 100644 --- a/incoming-files/reference/README.md +++ b/incoming-files/reference/README.md @@ -1,9 +1,21 @@ # Reference Assets -이 디렉터리는 앞으로 `8081`에서 직접 서빙하지 않는 참고 원본/복구 비교 자산을 모으기 위한 공간이다. +이 디렉터리는 `8081`에서 직접 서빙하지 않는 참고 원본/복구 비교 자산을 모으는 공간이다. -1차 정리에서는 위험한 대량 이동을 피하기 위해 기존 참고 파일을 즉시 옮기지 않는다. -대신 실제 서빙 파일은 `incoming-files/served/`로 고정하고, 다음 차수에서 참고 자산을 단계적으로 재배치한다. +`#21` 2차부터 실제 reference 재배치를 시작했다. + +현재 포함: + +- `ledger/` + - 사업관리대장 원본 wrapper/html/css/xlsx + - 이전 override 복사본 + - 중첩 백업 디렉터리 + +규칙: + +- runtime은 이 디렉터리를 직접 서빙하지 않는다. +- 실제 서비스 수정은 `incoming-files/served/` 기준으로 먼저 반영한다. +- reference는 비교, 복구, 출처 확인이 필요할 때만 본다. 예상 대상: diff --git a/incoming-files/사업관리대장/사업관리대장/MH 통합 대시보드_260320.css b/incoming-files/reference/ledger/MH 통합 대시보드_260320.css similarity index 100% rename from incoming-files/사업관리대장/사업관리대장/MH 통합 대시보드_260320.css rename to incoming-files/reference/ledger/MH 통합 대시보드_260320.css diff --git a/incoming-files/사업관리대장/MH 통합 대시보드_260320.html b/incoming-files/reference/ledger/MH 통합 대시보드_260320.html similarity index 100% rename from incoming-files/사업관리대장/MH 통합 대시보드_260320.html rename to incoming-files/reference/ledger/MH 통합 대시보드_260320.html diff --git a/incoming-files/reference/ledger/ledger-override.css b/incoming-files/reference/ledger/ledger-override.css new file mode 100644 index 0000000..505b65e --- /dev/null +++ b/incoming-files/reference/ledger/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/incoming-files/reference/ledger/ledger-override.js b/incoming-files/reference/ledger/ledger-override.js new file mode 100644 index 0000000..853e51c --- /dev/null +++ b/incoming-files/reference/ledger/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/incoming-files/사업관리대장/사업관리대장-1.xlsx b/incoming-files/reference/ledger/사업관리대장-1.xlsx similarity index 100% rename from incoming-files/사업관리대장/사업관리대장-1.xlsx rename to incoming-files/reference/ledger/사업관리대장-1.xlsx diff --git a/incoming-files/reference/ledger/사업관리대장/MH 통합 대시보드_260320.css b/incoming-files/reference/ledger/사업관리대장/MH 통합 대시보드_260320.css new file mode 100644 index 0000000..8b948d4 --- /dev/null +++ b/incoming-files/reference/ledger/사업관리대장/MH 통합 대시보드_260320.css @@ -0,0 +1,1377 @@ +:root { + --bg: #f1eadf; + --panel: #fffaf3; + --panel-soft: #f4e9d7; + --ink: #10251d; + --muted: #66756d; + --line: #d9c5a8; + --brand: #0f3a2f; + --brand-deep: #0a2a22; + --brand-soft: #1a5645; + --accent: #d68a3a; + --accent-soft: #f2c484; + --mint: #2f9973; + --blue: #4b87b3; + --shadow: 0 22px 54px rgba(15, 58, 47, 0.12); + } + + * { box-sizing: border-box; } + body { + margin: 0; + font-family: "Pretendard", "Malgun Gothic", sans-serif; + color: var(--ink); + background: + 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%); + } + + .page { + max-width: 1720px; + margin: 0 auto; + padding: 26px 22px 40px; + } + + .hero { + position: relative; + overflow: hidden; + background: + radial-gradient(circle at 12% 18%, rgba(242, 196, 132, 0.18), transparent 24%), + radial-gradient(circle at 88% 12%, rgba(255, 255, 255, 0.10), transparent 18%), + linear-gradient(145deg, var(--brand-deep) 0%, var(--brand) 52%, var(--brand-soft) 100%); + color: #f7f0e4; + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: 30px; + padding: 34px 32px; + box-shadow: 0 28px 70px rgba(15, 58, 47, 0.22); + } + + .hero::before { + content: ""; + position: absolute; + inset: auto -6% -46% auto; + width: 320px; + height: 320px; + border-radius: 50%; + background: radial-gradient(circle, rgba(242, 196, 132, 0.22), transparent 68%); + pointer-events: none; + } + + .hero::after { + content: ""; + position: absolute; + top: -90px; + right: 80px; + width: 220px; + height: 220px; + border-radius: 50%; + border: 1px solid rgba(242, 196, 132, 0.16); + pointer-events: none; + } + + h1 { + margin: 0; + font-size: 52px; + line-height: 1.12; + letter-spacing: -0.03em; + position: relative; + z-index: 1; + } + + .summary { + margin-top: 10px; + display: grid; + grid-template-columns: repeat(5, minmax(0, 1fr)); + gap: 14px; + } + + .summary-card { + padding: 14px 16px; + border-radius: 22px; + background: linear-gradient(180deg, rgba(255, 255, 255, 0.12) 0%, rgba(255, 255, 255, 0.07) 100%); + border: 1px solid rgba(255, 255, 255, 0.14); + backdrop-filter: blur(8px); + position: relative; + z-index: 1; + } + + .summary-label { + font-size: 11px; + letter-spacing: 0.16em; + text-transform: uppercase; + color: rgba(255, 244, 230, 0.68); + font-weight: 900; + } + + .summary-value { + margin-top: 8px; + font-size: 24px; + font-weight: 900; + } + + .summary-sub { + margin-top: 4px; + font-size: 12px; + color: rgba(255, 244, 230, 0.82); + font-weight: 700; + } + + .tabs { + margin-top: 18px; + display: flex; + gap: 10px; + flex-wrap: wrap; + } + + .hero-actions { + margin-top: 0; + display: flex; + gap: 10px; + flex-wrap: wrap; + position: absolute; + top: 0; + right: 0; + justify-content: flex-end; + } + + .hero-action { + padding: 12px 16px; + border-radius: 999px; + border: 1px solid rgba(242, 196, 132, 0.34); + background: rgba(255, 255, 255, 0.08); + color: #f4efe6; + font-size: 14px; + font-weight: 900; + cursor: pointer; + } + + .hero-action:hover { + background: rgba(242, 196, 132, 0.16); + border-color: rgba(242, 196, 132, 0.52); + } + + .tab { + padding: 12px 16px; + border-radius: 999px; + border: 1px solid #d6c1a3; + background: linear-gradient(180deg, #fffdf8 0%, #f5ebdd 100%); + color: #244638; + font-size: 14px; + font-weight: 900; + cursor: pointer; + transition: all 0.16s ease; + } + + .tab.active { + background: linear-gradient(145deg, var(--brand-deep) 0%, var(--brand) 100%); + color: #f4efe6; + border-color: var(--brand); + box-shadow: 0 12px 28px rgba(15, 58, 47, 0.2); + } + + .layout { + margin-top: 18px; + display: grid; + grid-template-columns: 320px minmax(0, 1fr); + gap: 18px; + } + + .panel { + background: var(--panel); + border: 1px solid var(--line); + border-radius: 26px; + box-shadow: var(--shadow); + overflow: hidden; + } + + .panel-head { + padding: 18px 20px 0; + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + } + + .panel-title { + margin: 0; + font-size: 18px; + font-weight: 900; + } + + .panel-note { + color: var(--muted); + font-size: 12px; + font-weight: 800; + } + + .panel-body { + padding: 18px 20px 20px; + } + + .company-card { + display: grid; + gap: 10px; + } + + .metric { + padding: 14px 16px; + border-radius: 18px; + background: linear-gradient(180deg, #fffdf8 0%, #f2e7d6 100%); + border: 1px solid var(--line); + } + + .metric-label { + font-size: 11px; + font-weight: 900; + letter-spacing: 0.16em; + color: #8a6b3d; + text-transform: uppercase; + } + + .metric-value { + margin-top: 8px; + font-size: 20px; + font-weight: 900; + line-height: 1.2; + } + + .metric-sub { + margin-top: 5px; + font-size: 12px; + line-height: 1.45; + color: #425148; + font-weight: 700; + } + + .metric-sub-list { + display: grid; + gap: 4px; + margin-top: 6px; + max-height: 132px; + overflow: auto; + padding-right: 4px; + } + +.metric-sub-item { + font-size: 12px; + line-height: 1.4; + color: #425148; + font-weight: 700; + } + + /* MH embedded business dashboard theme */ + body.mh-business-theme { + color: var(--ink); + background: + 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%) !important; + font-family: "Pretendard", "Malgun Gothic", sans-serif; + } + + body.mh-business-theme .wrap { + width: calc(100vw - 60px); + max-width: calc(100vw - 60px); + margin: 0 auto; + padding: 18px 18px 16px; + } + + body.mh-business-theme .top { + position: relative; + overflow: hidden; + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(320px, 520px); + gap: 18px 24px; + align-items: start; + background: + radial-gradient(circle at 12% 18%, rgba(242, 196, 132, 0.18), transparent 24%), + radial-gradient(circle at 88% 12%, rgba(255, 255, 255, 0.10), transparent 18%), + linear-gradient(145deg, var(--brand-deep) 0%, var(--brand) 52%, var(--brand-soft) 100%); + color: #f7f0e4; + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: 30px; + padding: 30px 30px 26px; + box-shadow: 0 28px 70px rgba(15, 58, 47, 0.22); + margin-bottom: 14px; + } + + body.mh-business-theme .top::before { + content: ""; + position: absolute; + inset: auto -6% -46% auto; + width: 320px; + height: 320px; + border-radius: 50%; + background: radial-gradient(circle, rgba(242, 196, 132, 0.22), transparent 68%); + pointer-events: none; + } + + body.mh-business-theme .top::after { + content: ""; + position: absolute; + top: -90px; + right: 80px; + width: 220px; + height: 220px; + border-radius: 50%; + border: 1px solid rgba(242, 196, 132, 0.16); + pointer-events: none; + } + + body.mh-business-theme .top > div:first-child { + min-width: 0; + } + + body.mh-business-theme .sub, + body.mh-business-theme .title, + body.mh-business-theme .today-date-label, + body.mh-business-theme #btnUpload { + display: none !important; + } + + body.mh-business-theme .brand-head { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 10px; + min-width: 0; + position: relative; + z-index: 1; + } + + body.mh-business-theme .brand-ci { + display: flex; + align-items: center; + gap: 10px; + min-width: 0; + } + + body.mh-business-theme .brand-logo-wrap { + display: inline-flex; + align-items: center; + justify-content: center; + flex: 0 0 auto; + } + + body.mh-business-theme .brand-logo { + width: 50px; + height: 28px; + flex: 0 0 auto; + display: block; + } + + body.mh-business-theme .brand-company { + color: #8db4ff; + font-size: 20px; + font-weight: 900; + letter-spacing: -0.03em; + white-space: nowrap; + line-height: 1; + } + + body.mh-business-theme .brand-copy { + min-width: 0; + } + + body.mh-business-theme .brand-title { + min-width: 0; + color: #f4efe6; + font-size: clamp(28px, 2.5vw, 46px); + font-weight: 900; + letter-spacing: -0.03em; + line-height: 1.1; + word-break: keep-all; + } + + body.mh-business-theme .brand-subtitle { + display: inline; + color: rgba(244, 239, 230, 0.92); + font-size: 0.5em; + font-weight: 700; + letter-spacing: 0.01em; + margin-left: 6px; + white-space: nowrap; + } + + body.mh-business-theme .controls { + display: flex; + flex-direction: column; + align-items: stretch; + justify-self: end; + gap: 12px; + min-width: min(100%, 520px); + position: relative; + z-index: 1; + } + + body.mh-business-theme .controls-top-row, + body.mh-business-theme .controls-top-actions { + display: flex; + align-items: center; + justify-content: flex-end; + gap: 10px; + flex-wrap: wrap; + } + + body.mh-business-theme .search { + width: min(520px, 100%); + align-self: flex-end; + min-height: 52px; + border-radius: 18px; + border: 1px solid rgba(255, 255, 255, 0.18); + background: rgba(255, 255, 255, 0.14); + color: #fff7eb; + padding: 14px 18px; + font-size: 15px; + font-weight: 800; + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.06); + } + + body.mh-business-theme .search::placeholder { + color: rgba(247, 240, 228, 0.66); + } + + body.mh-business-theme .status { + display: none !important; + } + + body.mh-business-theme .cards { + grid-column: 1 / -1; + display: grid; + grid-template-columns: repeat(6, minmax(0, 1fr)); + gap: 14px; + margin-top: 6px; + margin-bottom: 0; + position: relative; + z-index: 1; + } + + body.mh-business-theme .business-shell { + position: relative; + margin-top: 12px; + padding: 18px 18px 20px; + border-radius: 30px; + background: + radial-gradient(circle at 12% 12%, rgba(242, 196, 132, 0.10), transparent 26%), + radial-gradient(circle at 88% 10%, rgba(255, 255, 255, 0.08), transparent 18%), + linear-gradient(145deg, rgba(15, 58, 47, 0.96) 0%, rgba(26, 86, 69, 0.94) 100%); + border: 1px solid rgba(255, 255, 255, 0.10); + box-shadow: 0 26px 70px rgba(15, 58, 47, 0.16); + overflow: hidden; + } + + body.mh-business-theme .business-shell::before { + content: ""; + position: absolute; + inset: auto -10% -32% auto; + width: 360px; + height: 360px; + border-radius: 50%; + background: radial-gradient(circle, rgba(242, 196, 132, 0.16), transparent 70%); + pointer-events: none; + } + + body.mh-business-theme .business-shell::after { + content: ""; + position: absolute; + top: -110px; + right: 90px; + width: 260px; + height: 260px; + border-radius: 50%; + border: 1px solid rgba(242, 196, 132, 0.12); + pointer-events: none; + } + + body.mh-business-theme .cards-toolbar { + grid-column: 1 / -1; + display: flex; + flex-direction: column; + gap: 10px; + margin-bottom: 6px; + } + + body.mh-business-theme .cards-toolbar-row { + display: flex; + align-items: center; + gap: 10px; + flex-wrap: wrap; + } + + body.mh-business-theme .summary-filter-chip, + body.mh-business-theme .summary-year-chip { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 8px; + min-width: 58px; + padding: 9px 14px; + 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; + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04); + } + + body.mh-business-theme .summary-filter-chip { + flex-direction: column; + align-items: center; + justify-content: center; + min-height: 96px; + gap: 8px; + text-align: center; + } + + body.mh-business-theme .summary-filter-chip .label { + color: rgba(244, 239, 230, 0.92); + letter-spacing: 0.01em; + } + + body.mh-business-theme .summary-filter-chip .count { + color: #ffd08a; + font-size: 30px; + line-height: 1; + letter-spacing: -0.03em; + } + + body.mh-business-theme .summary-filter-chip .meta { + color: rgba(255, 230, 190, 0.92); + font-size: 11px; + font-weight: 800; + } + + body.mh-business-theme .summary-filter-chip.active, + 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.active .count { + color: #b86b1f; + } + + body.mh-business-theme .summary-filter-chip.active .label { + color: rgba(10, 42, 34, 0.78); + } + + body.mh-business-theme .summary-filter-chip.active .meta { + color: #7c5a20; + } + + body.mh-business-theme .card { + padding: 16px 16px 14px; + min-height: 108px; + border-radius: 22px; + background: linear-gradient(180deg, rgba(255, 250, 243, 0.98) 0%, rgba(247, 238, 225, 0.94) 100%); + border: 1px solid rgba(214, 193, 163, 0.78); + color: #173328; + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.75), + 0 14px 30px rgba(15, 58, 47, 0.08); + backdrop-filter: blur(10px); + display: flex; + flex-direction: column; + justify-content: center; + } + + body.mh-business-theme .card.management { + background: linear-gradient(180deg, rgba(255, 246, 232, 0.98) 0%, rgba(250, 236, 208, 0.96) 100%); + border-color: rgba(214, 138, 58, 0.42); + } + + body.mh-business-theme .card .k { + color: rgba(23, 51, 40, 0.68); + font-size: 11px; + font-weight: 900; + letter-spacing: 0.04em; + } + + body.mh-business-theme .card .v { + margin-top: 7px; + color: #173328; + font-size: clamp(19px, 1.55vw, 27px); + font-weight: 900; + letter-spacing: -0.03em; + white-space: nowrap; + } + + body.mh-business-theme .card .n { + margin-top: 5px; + color: rgba(63, 84, 74, 0.88); + font-size: 12px; + font-weight: 700; + line-height: 1.4; + } + + body.mh-business-theme .panel { + background: var(--panel); + border: 1px solid var(--line); + border-radius: 26px; + box-shadow: var(--shadow); + overflow: hidden; + position: relative; + z-index: 1; + } + + body.mh-business-theme .table-wrap { + overflow: auto; + } + + body.mh-business-theme table { + width: 100%; + min-width: 1250px; + border-collapse: collapse; + } + + body.mh-business-theme thead th { + background: #122b23; + color: rgba(247, 240, 228, 0.92); + font-size: 11px; + letter-spacing: 0.08em; + padding: 12px 10px; + text-align: left; + white-space: nowrap; + vertical-align: middle; + } + + body.mh-business-theme .th-trigger, + body.mh-business-theme .th-trigger:hover, + body.mh-business-theme .th-trigger.active, + body.mh-business-theme .th-trigger.open { + color: rgba(247, 240, 228, 0.92); + } + + body.mh-business-theme .th-mark, + body.mh-business-theme .th-caret, + body.mh-business-theme .th-meta { + color: #f2c484; + } + + body.mh-business-theme tbody td { + padding: 12px 10px; + border-bottom: 1px solid #ece1cf; + font-size: 13px; + background: #fffaf3; + } + + body.mh-business-theme th:nth-child(1), + body.mh-business-theme td:nth-child(1) { width: 5%; } + body.mh-business-theme th:nth-child(2), + body.mh-business-theme td:nth-child(2) { width: 6%; } + body.mh-business-theme th:nth-child(3), + body.mh-business-theme td:nth-child(3) { width: 31%; } + body.mh-business-theme th:nth-child(4), + body.mh-business-theme td:nth-child(4) { width: 12%; } + body.mh-business-theme th:nth-child(5), + body.mh-business-theme td:nth-child(5) { width: 7%; } + body.mh-business-theme th:nth-child(6), + body.mh-business-theme td:nth-child(6) { width: 6%; } + body.mh-business-theme th:nth-child(7), + body.mh-business-theme td:nth-child(7) { width: 9%; } + body.mh-business-theme th:nth-child(8), + body.mh-business-theme td:nth-child(8) { width: 8%; } + body.mh-business-theme th:nth-child(9), + body.mh-business-theme td:nth-child(9) { width: 8%; } + body.mh-business-theme th:nth-child(10), + body.mh-business-theme td:nth-child(10) { width: 8%; } + body.mh-business-theme th:nth-child(11), + body.mh-business-theme td:nth-child(11) { width: 5%; } + + body.mh-business-theme tbody tr:hover td { + background: #fff5e8; + } + + body.mh-business-theme .name, + body.mh-business-theme td strong { + color: #10251d; + } + + body.mh-business-theme .subline { + color: #7c8b82; + } + + body.mh-business-theme .client-main { + display: block; + font-weight: 800; + color: #10251d; + } + + body.mh-business-theme .badge.badge-baron { + border-color: #f0bb75; + background: #fff2df; + color: #b96820; + } + + body.mh-business-theme .badge.badge-family { + border-color: #a5cbb6; + background: #eef7f1; + color: #2d6a4f; + } + + body.mh-business-theme .group-row td { + padding: 12px 14px 10px; + background: linear-gradient(180deg, #fff9ef 0%, #f5ebdd 100%); + border-top: 1px solid #ead8bc; + border-bottom: 1px solid #ead8bc; + } + + body.mh-business-theme .group-chip { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 7px 13px; + border-radius: 999px; + background: #fffdfa; + border: 1px solid #d6c1a3; + color: #244638; + font-size: 12px; + font-weight: 900; + box-shadow: 0 10px 24px rgba(15, 58, 47, 0.10); + 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.16); + color: #8a5a1f; + font-size: 14px; + font-weight: 900; + line-height: 1; + } + + body.mh-business-theme .detail-row td { + background: linear-gradient(180deg, #fff7ec 0%, #fffdf8 100%); + border-top: 1px solid #ecd8ba; + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.65); + } + + body.mh-business-theme .detail-row .inline-panel { + background: transparent; + border-left: 3px solid var(--accent); + } + + body.mh-business-theme .detail-row .inline-card, + body.mh-business-theme .detail-row .ledger-block { + box-shadow: 0 10px 24px rgba(15, 58, 47, 0.08); + } + + body.mh-business-theme .table-top-note { + display: flex; + justify-content: flex-end; + margin: 0 2px 10px; + } + + body.mh-business-theme .table-vat-note { + display: inline-flex; + align-items: center; + padding: 6px 10px; + border-radius: 999px; + background: rgba(255, 248, 238, 0.96); + border: 1px solid rgba(242, 196, 132, 0.46); + color: #6f5528; + font-size: 11px; + font-weight: 900; + letter-spacing: -0.01em; + } + + @media (max-width: 1280px) { + body.mh-business-theme .top { + grid-template-columns: 1fr; + } + + body.mh-business-theme .controls { + min-width: 0; + justify-self: stretch; + } + + body.mh-business-theme .controls-top-row, + body.mh-business-theme .controls-top-actions { + justify-content: flex-start; + } + + body.mh-business-theme .search { + width: 100%; + align-self: stretch; + } + + body.mh-business-theme .cards { + grid-template-columns: repeat(2, minmax(140px, 1fr)); + } + } + + @media (max-width: 720px) { + body.mh-business-theme .wrap { + width: calc(100vw - 30px); + max-width: calc(100vw - 30px); + padding: 12px 10px; + } + + body.mh-business-theme .top { + padding: 18px 14px 16px; + gap: 14px; + } + + body.mh-business-theme .brand-head { + gap: 8px; + align-items: flex-start; + } + + body.mh-business-theme .brand-company { + font-size: 16px; + } + + body.mh-business-theme .brand-title { + font-size: 30px; + } + + body.mh-business-theme .brand-subtitle { + display: inline; + margin-left: 4px; + margin-top: 0; + white-space: normal; + } + } + + .filter-row { + display: grid; + grid-template-columns: 1fr 160px 180px 180px; + gap: 12px; + margin-bottom: 14px; + } + + .year-summary { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); + gap: 10px; + margin-bottom: 14px; + } + + .year-card { + padding: 14px 16px; + border-radius: 18px; + background: linear-gradient(180deg, #fffdf8 0%, #f3e7d6 100%); + border: 1px solid var(--line); + cursor: pointer; + transition: transform 0.16s ease, box-shadow 0.16s ease, border-color 0.16s ease, background 0.16s ease; + } + + .year-card:hover { + transform: translateY(-2px); + box-shadow: 0 10px 22px rgba(18, 48, 30, 0.10); + } + + .year-card.active { + background: linear-gradient(145deg, var(--brand-deep) 0%, var(--brand) 62%, var(--brand-soft) 100%); + border-color: rgba(214, 138, 58, 0.42); + box-shadow: 0 14px 30px rgba(15, 58, 47, 0.2); + } + + .year-card.active .year-card-label, + .year-card.active .year-card-value, + .year-card.active .year-card-sub { + color: #f4efe6; + } + + .year-card-label { + font-size: 11px; + font-weight: 900; + letter-spacing: 0.14em; + color: #7a684d; + } + + .year-card-value { + margin-top: 8px; + font-size: clamp(15px, 1.45vw, 18px); + font-weight: 900; + color: var(--brand); + line-height: 1.2; + white-space: nowrap; + letter-spacing: -0.04em; + } + + .year-card-sub { + margin-top: 4px; + font-size: 12px; + font-weight: 700; + color: var(--muted); + } + + .input, .select { + width: 100%; + border: 1px solid #d7c4a7; + background: #fffdf8; + border-radius: 14px; + padding: 12px 14px; + font-size: 14px; + font-weight: 700; + color: var(--ink); + outline: none; + } + + .input:focus, .select:focus { + border-color: var(--brand-soft); + box-shadow: 0 0 0 3px rgba(47, 153, 115, 0.12); + } + + .chip-row { + display: flex; + gap: 8px; + flex-wrap: wrap; + margin-bottom: 12px; + } + + .chip { + padding: 8px 12px; + border-radius: 999px; + background: #efe3cf; + color: #315243; + font-size: 12px; + font-weight: 900; + } + + .table-wrap { + overflow: auto; + border-top: 1px solid var(--line); + } + + .table-summary { + display: flex; + justify-content: flex-end; + gap: 18px; + padding: 14px 20px 18px; + border-top: 1px solid var(--line); + background: #fbf4ea; + font-size: 13px; + font-weight: 800; + color: #244233; + } + + .table-summary strong { + color: #12301e; + font-size: 15px; + } + + table { + width: 100%; + min-width: 1180px; + border-collapse: separate; + border-spacing: 0; + } + + th, td { + border-right: 1px solid #ecdfcc; + border-bottom: 1px solid #ecdfcc; + padding: 11px 12px; + vertical-align: middle; + text-align: left; + font-size: 13px; + line-height: 1.55; + white-space: nowrap; + } + + th:last-child, td:last-child { border-right: none; } + + thead th { + position: sticky; + top: 0; + z-index: 1; + background: linear-gradient(180deg, var(--brand-deep) 0%, var(--brand) 100%); + color: #f4efe6; + font-size: 12px; + font-weight: 900; + text-align: center; + border-bottom: 1px solid rgba(242, 196, 132, 0.3); + } + + tbody tr:nth-child(even) td { + background: #fbf4ea; + } + + .money { + white-space: nowrap; + font-weight: 900; + color: var(--brand-soft); + } + + .muted { + color: var(--muted); + font-size: 12px; + } + + .cell-filter { + padding: 0; + border: none; + background: transparent; + color: #12301e; + font: inherit; + font-weight: 800; + cursor: pointer; + white-space: nowrap; + text-decoration: underline; + text-decoration-thickness: 1px; + text-underline-offset: 3px; + } + + .cell-filter:hover { + color: var(--mint); + } + + .chip-action { + border: 1px solid #d9c8af; + background: #fffaf2; + cursor: pointer; + } + + .chip-action:hover { + border-color: var(--mint); + color: var(--mint); + } + + .empty { + padding: 24px; + text-align: center; + color: var(--muted); + font-weight: 800; + } + + .page { + position: relative; + } + + .page::before { + content: ""; + position: fixed; + inset: 0; + background: + linear-gradient(rgba(15, 58, 47, 0.03) 1px, transparent 1px), + linear-gradient(90deg, rgba(15, 58, 47, 0.03) 1px, transparent 1px); + background-size: 32px 32px; + mask-image: linear-gradient(180deg, rgba(0, 0, 0, 0.34), transparent 82%); + pointer-events: none; + z-index: 0; + } + + .hero { + padding: 28px 34px 24px; + border-radius: 32px; + box-shadow: 0 30px 70px rgba(15, 58, 47, 0.22); + } + + .hero-grid { + position: relative; + z-index: 1; + display: grid; + grid-template-columns: minmax(0, 1fr); + gap: 24px; + align-items: end; + } + + .hero-grid > div:first-child { + position: relative; + min-height: 108px; + padding-right: 260px; + } + + .brand-kicker { + display: inline-flex; + align-items: center; + gap: 10px; + padding: 9px 14px; + border-radius: 999px; + background: rgba(255, 255, 255, 0.08); + border: 1px solid rgba(242, 196, 132, 0.24); + color: rgba(255, 244, 230, 0.86); + font-size: 11px; + font-weight: 900; + letter-spacing: 0.22em; + text-transform: uppercase; + } + + .brand-kicker::before { + content: ""; + width: 8px; + height: 8px; + border-radius: 50%; + background: var(--accent-soft); + box-shadow: 0 0 0 6px rgba(242, 196, 132, 0.12); + } + + .brand-line { + display: grid; + grid-template-columns: 76px minmax(0, 1fr); + gap: 20px; + align-items: center; + margin-top: 0; + } + + .hero-logo { + position: relative; + width: 76px; + height: 76px; + display: grid; + place-items: center; + border-radius: 24px; + background: + radial-gradient(circle at 30% 26%, rgba(255, 255, 255, 0.24), transparent 20%), + radial-gradient(circle at 68% 72%, rgba(242, 196, 132, 0.34), transparent 18%), + linear-gradient(145deg, rgba(242, 196, 132, 0.18) 0%, rgba(255, 255, 255, 0.04) 100%); + border: 1px solid rgba(242, 196, 132, 0.24); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.12), 0 18px 36px rgba(10, 42, 34, 0.24); + } + + .hero-logo::before { + content: ""; + position: absolute; + inset: 10px; + border-radius: 18px; + border: 1px solid rgba(242, 196, 132, 0.16); + } + + .hero-logo::after { + content: ""; + position: absolute; + left: 50%; + top: 50%; + width: 28px; + height: 28px; + transform: translate(-50%, -50%); + border-radius: 50%; + background: radial-gradient(circle, rgba(242, 196, 132, 0.92), rgba(214, 138, 58, 0.18)); + box-shadow: + 0 0 0 8px rgba(242, 196, 132, 0.06), + 0 0 24px rgba(242, 196, 132, 0.24); + } + + .hero-logo-core { + display: none; + } + + .hero-copy { + display: none; + } + + .hero-side { + display: grid; + gap: 12px; + } + + .hero-mini-card { + position: relative; + padding: 16px 18px 16px 58px; + border-radius: 22px; + background: linear-gradient(180deg, rgba(255, 255, 255, 0.11) 0%, rgba(255, 255, 255, 0.06) 100%); + border: 1px solid rgba(255, 255, 255, 0.12); + min-height: 88px; + } + + .hero-mini-card::before { + content: attr(data-mark); + position: absolute; + left: 16px; + top: 16px; + width: 30px; + height: 30px; + border-radius: 12px; + display: grid; + place-items: center; + background: rgba(242, 196, 132, 0.18); + color: #ffe6bf; + font-size: 11px; + font-weight: 1000; + letter-spacing: 0.08em; + } + + .hero-mini-label { + color: rgba(255, 244, 230, 0.62); + font-size: 11px; + font-weight: 900; + letter-spacing: 0.16em; + text-transform: uppercase; + } + + .hero-mini-value { + margin-top: 8px; + color: #fff7eb; + font-size: 24px; + font-weight: 1000; + letter-spacing: -0.03em; + } + + .hero-mini-sub { + margin-top: 6px; + color: rgba(255, 244, 230, 0.76); + font-size: 13px; + line-height: 1.5; + font-weight: 700; + } + + .tabs-shell { + margin-top: 18px; + padding: 12px; + border-radius: 24px; + background: linear-gradient(180deg, rgba(255, 255, 255, 0.68) 0%, rgba(255, 250, 243, 0.92) 100%); + border: 1px solid rgba(217, 197, 168, 0.8); + box-shadow: 0 18px 36px rgba(15, 58, 47, 0.08); + backdrop-filter: blur(10px); + } + + .tabs-shell .tabs { + margin-top: 0; + } + + .page > .hero:not(.hero-reimagined), + .page > .tabs:not(.tabs-reimagined) { + display: none; + } + + .summary-card { + min-height: 122px; + overflow: hidden; + transition: transform 0.18s ease, box-shadow 0.18s ease; + } + + .summary-card:hover { + transform: translateY(-3px); + box-shadow: 0 18px 30px rgba(10, 42, 34, 0.16); + } + + .summary-card::after { + content: ""; + position: absolute; + inset: auto -14px -28px auto; + width: 96px; + height: 96px; + border-radius: 50%; + background: radial-gradient(circle, rgba(242, 196, 132, 0.14), transparent 70%); + } + + .summary-top { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + display: none; + } + + .summary-icon { + width: 42px; + height: 42px; + border-radius: 14px; + display: grid; + place-items: center; + background: rgba(255, 255, 255, 0.14); + border: 1px solid rgba(255, 255, 255, 0.12); + color: #fff3df; + font-size: 11px; + font-weight: 1000; + letter-spacing: 0.08em; + } + + .summary-tail { + margin-top: auto; + padding-top: 8px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + color: rgba(255, 244, 230, 0.74); + font-size: 11px; + font-weight: 800; + } + + .summary-card .summary-value { + margin-top: 0; + } + + .summary-line { + flex: 1; + height: 4px; + border-radius: 999px; + background: rgba(255, 255, 255, 0.12); + overflow: hidden; + } + + .summary-line span { + display: block; + height: 100%; + width: 64%; + border-radius: inherit; + background: linear-gradient(90deg, rgba(242, 196, 132, 0.96), rgba(47, 153, 115, 0.88)); + } + + .panel { + position: relative; + } + + .panel::before { + content: ""; + position: absolute; + inset: 0 0 auto 0; + height: 4px; + background: linear-gradient(90deg, var(--accent), #edd6af 38%, var(--mint) 100%); + opacity: 0.9; + } + + tbody tr:hover td { + background: #f5ecdf; + } + + @media (max-width: 1180px) { + .hero-grid { + grid-template-columns: 1fr; + } + + .hero-grid > div:first-child { + min-height: auto; + padding-right: 0; + } + + .brand-line { + grid-template-columns: 92px minmax(0, 1fr); + gap: 16px; + } + + .hero-logo { + width: 68px; + height: 68px; + } + + .hero-actions { + position: static; + margin-top: 18px; + justify-content: flex-start; + } + + .layout { + grid-template-columns: 1fr; + } + + .filter-row { + grid-template-columns: 1fr; + } + + .summary { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + } + + @media (max-width: 720px) { + h1 { + font-size: 34px; + } + + .summary { + grid-template-columns: 1fr; + } + } diff --git a/incoming-files/사업관리대장/사업관리대장/MH 통합 대시보드_260320.html b/incoming-files/reference/ledger/사업관리대장/MH 통합 대시보드_260320.html similarity index 100% rename from incoming-files/사업관리대장/사업관리대장/MH 통합 대시보드_260320.html rename to incoming-files/reference/ledger/사업관리대장/MH 통합 대시보드_260320.html diff --git a/incoming-files/사업관리대장/사업관리대장/사업관리대장-1.xlsx b/incoming-files/reference/ledger/사업관리대장/사업관리대장-1.xlsx similarity index 100% rename from incoming-files/사업관리대장/사업관리대장/사업관리대장-1.xlsx rename to incoming-files/reference/ledger/사업관리대장/사업관리대장-1.xlsx diff --git a/incoming-files/served/README.md b/incoming-files/served/README.md index eae0a30..d079717 100644 --- a/incoming-files/served/README.md +++ b/incoming-files/served/README.md @@ -6,9 +6,18 @@ - `payment.html` - `mh.html` +- `ledger/index.html` +- `ledger/ledger-override.css` +- `ledger/ledger-override.js` +- `ledger/MH 통합 대시보드_260320.css` +- `ledger/사업관리대장-1.xlsx` 규칙: - `/integrations/payment` 는 이 디렉터리의 `payment.html`을 읽는다. - `/integrations/mh` 는 이 디렉터리의 `mh.html`을 읽는다. +- `/integrations/ledger` 는 `ledger/index.html`을 읽는다. +- `/integrations/ledger-assets/*` 는 `ledger/` 하위 파일만 읽는다. +- `payment.html` 수정 원본은 `frontend/apps/payment/index.html`이고, `scripts/publish_payment_app.sh`로 반영한다. +- `mh.html` 수정 원본은 `frontend/apps/team/index.html`이고, `scripts/publish_team_app.sh`로 반영한다. - 원본 참고 파일이나 비교용 파일은 이 디렉터리에 두지 않는다. diff --git a/incoming-files/served/ledger/MH 통합 대시보드_260320.css b/incoming-files/served/ledger/MH 통합 대시보드_260320.css new file mode 100644 index 0000000..8b948d4 --- /dev/null +++ b/incoming-files/served/ledger/MH 통합 대시보드_260320.css @@ -0,0 +1,1377 @@ +:root { + --bg: #f1eadf; + --panel: #fffaf3; + --panel-soft: #f4e9d7; + --ink: #10251d; + --muted: #66756d; + --line: #d9c5a8; + --brand: #0f3a2f; + --brand-deep: #0a2a22; + --brand-soft: #1a5645; + --accent: #d68a3a; + --accent-soft: #f2c484; + --mint: #2f9973; + --blue: #4b87b3; + --shadow: 0 22px 54px rgba(15, 58, 47, 0.12); + } + + * { box-sizing: border-box; } + body { + margin: 0; + font-family: "Pretendard", "Malgun Gothic", sans-serif; + color: var(--ink); + background: + 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%); + } + + .page { + max-width: 1720px; + margin: 0 auto; + padding: 26px 22px 40px; + } + + .hero { + position: relative; + overflow: hidden; + background: + radial-gradient(circle at 12% 18%, rgba(242, 196, 132, 0.18), transparent 24%), + radial-gradient(circle at 88% 12%, rgba(255, 255, 255, 0.10), transparent 18%), + linear-gradient(145deg, var(--brand-deep) 0%, var(--brand) 52%, var(--brand-soft) 100%); + color: #f7f0e4; + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: 30px; + padding: 34px 32px; + box-shadow: 0 28px 70px rgba(15, 58, 47, 0.22); + } + + .hero::before { + content: ""; + position: absolute; + inset: auto -6% -46% auto; + width: 320px; + height: 320px; + border-radius: 50%; + background: radial-gradient(circle, rgba(242, 196, 132, 0.22), transparent 68%); + pointer-events: none; + } + + .hero::after { + content: ""; + position: absolute; + top: -90px; + right: 80px; + width: 220px; + height: 220px; + border-radius: 50%; + border: 1px solid rgba(242, 196, 132, 0.16); + pointer-events: none; + } + + h1 { + margin: 0; + font-size: 52px; + line-height: 1.12; + letter-spacing: -0.03em; + position: relative; + z-index: 1; + } + + .summary { + margin-top: 10px; + display: grid; + grid-template-columns: repeat(5, minmax(0, 1fr)); + gap: 14px; + } + + .summary-card { + padding: 14px 16px; + border-radius: 22px; + background: linear-gradient(180deg, rgba(255, 255, 255, 0.12) 0%, rgba(255, 255, 255, 0.07) 100%); + border: 1px solid rgba(255, 255, 255, 0.14); + backdrop-filter: blur(8px); + position: relative; + z-index: 1; + } + + .summary-label { + font-size: 11px; + letter-spacing: 0.16em; + text-transform: uppercase; + color: rgba(255, 244, 230, 0.68); + font-weight: 900; + } + + .summary-value { + margin-top: 8px; + font-size: 24px; + font-weight: 900; + } + + .summary-sub { + margin-top: 4px; + font-size: 12px; + color: rgba(255, 244, 230, 0.82); + font-weight: 700; + } + + .tabs { + margin-top: 18px; + display: flex; + gap: 10px; + flex-wrap: wrap; + } + + .hero-actions { + margin-top: 0; + display: flex; + gap: 10px; + flex-wrap: wrap; + position: absolute; + top: 0; + right: 0; + justify-content: flex-end; + } + + .hero-action { + padding: 12px 16px; + border-radius: 999px; + border: 1px solid rgba(242, 196, 132, 0.34); + background: rgba(255, 255, 255, 0.08); + color: #f4efe6; + font-size: 14px; + font-weight: 900; + cursor: pointer; + } + + .hero-action:hover { + background: rgba(242, 196, 132, 0.16); + border-color: rgba(242, 196, 132, 0.52); + } + + .tab { + padding: 12px 16px; + border-radius: 999px; + border: 1px solid #d6c1a3; + background: linear-gradient(180deg, #fffdf8 0%, #f5ebdd 100%); + color: #244638; + font-size: 14px; + font-weight: 900; + cursor: pointer; + transition: all 0.16s ease; + } + + .tab.active { + background: linear-gradient(145deg, var(--brand-deep) 0%, var(--brand) 100%); + color: #f4efe6; + border-color: var(--brand); + box-shadow: 0 12px 28px rgba(15, 58, 47, 0.2); + } + + .layout { + margin-top: 18px; + display: grid; + grid-template-columns: 320px minmax(0, 1fr); + gap: 18px; + } + + .panel { + background: var(--panel); + border: 1px solid var(--line); + border-radius: 26px; + box-shadow: var(--shadow); + overflow: hidden; + } + + .panel-head { + padding: 18px 20px 0; + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + } + + .panel-title { + margin: 0; + font-size: 18px; + font-weight: 900; + } + + .panel-note { + color: var(--muted); + font-size: 12px; + font-weight: 800; + } + + .panel-body { + padding: 18px 20px 20px; + } + + .company-card { + display: grid; + gap: 10px; + } + + .metric { + padding: 14px 16px; + border-radius: 18px; + background: linear-gradient(180deg, #fffdf8 0%, #f2e7d6 100%); + border: 1px solid var(--line); + } + + .metric-label { + font-size: 11px; + font-weight: 900; + letter-spacing: 0.16em; + color: #8a6b3d; + text-transform: uppercase; + } + + .metric-value { + margin-top: 8px; + font-size: 20px; + font-weight: 900; + line-height: 1.2; + } + + .metric-sub { + margin-top: 5px; + font-size: 12px; + line-height: 1.45; + color: #425148; + font-weight: 700; + } + + .metric-sub-list { + display: grid; + gap: 4px; + margin-top: 6px; + max-height: 132px; + overflow: auto; + padding-right: 4px; + } + +.metric-sub-item { + font-size: 12px; + line-height: 1.4; + color: #425148; + font-weight: 700; + } + + /* MH embedded business dashboard theme */ + body.mh-business-theme { + color: var(--ink); + background: + 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%) !important; + font-family: "Pretendard", "Malgun Gothic", sans-serif; + } + + body.mh-business-theme .wrap { + width: calc(100vw - 60px); + max-width: calc(100vw - 60px); + margin: 0 auto; + padding: 18px 18px 16px; + } + + body.mh-business-theme .top { + position: relative; + overflow: hidden; + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(320px, 520px); + gap: 18px 24px; + align-items: start; + background: + radial-gradient(circle at 12% 18%, rgba(242, 196, 132, 0.18), transparent 24%), + radial-gradient(circle at 88% 12%, rgba(255, 255, 255, 0.10), transparent 18%), + linear-gradient(145deg, var(--brand-deep) 0%, var(--brand) 52%, var(--brand-soft) 100%); + color: #f7f0e4; + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: 30px; + padding: 30px 30px 26px; + box-shadow: 0 28px 70px rgba(15, 58, 47, 0.22); + margin-bottom: 14px; + } + + body.mh-business-theme .top::before { + content: ""; + position: absolute; + inset: auto -6% -46% auto; + width: 320px; + height: 320px; + border-radius: 50%; + background: radial-gradient(circle, rgba(242, 196, 132, 0.22), transparent 68%); + pointer-events: none; + } + + body.mh-business-theme .top::after { + content: ""; + position: absolute; + top: -90px; + right: 80px; + width: 220px; + height: 220px; + border-radius: 50%; + border: 1px solid rgba(242, 196, 132, 0.16); + pointer-events: none; + } + + body.mh-business-theme .top > div:first-child { + min-width: 0; + } + + body.mh-business-theme .sub, + body.mh-business-theme .title, + body.mh-business-theme .today-date-label, + body.mh-business-theme #btnUpload { + display: none !important; + } + + body.mh-business-theme .brand-head { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 10px; + min-width: 0; + position: relative; + z-index: 1; + } + + body.mh-business-theme .brand-ci { + display: flex; + align-items: center; + gap: 10px; + min-width: 0; + } + + body.mh-business-theme .brand-logo-wrap { + display: inline-flex; + align-items: center; + justify-content: center; + flex: 0 0 auto; + } + + body.mh-business-theme .brand-logo { + width: 50px; + height: 28px; + flex: 0 0 auto; + display: block; + } + + body.mh-business-theme .brand-company { + color: #8db4ff; + font-size: 20px; + font-weight: 900; + letter-spacing: -0.03em; + white-space: nowrap; + line-height: 1; + } + + body.mh-business-theme .brand-copy { + min-width: 0; + } + + body.mh-business-theme .brand-title { + min-width: 0; + color: #f4efe6; + font-size: clamp(28px, 2.5vw, 46px); + font-weight: 900; + letter-spacing: -0.03em; + line-height: 1.1; + word-break: keep-all; + } + + body.mh-business-theme .brand-subtitle { + display: inline; + color: rgba(244, 239, 230, 0.92); + font-size: 0.5em; + font-weight: 700; + letter-spacing: 0.01em; + margin-left: 6px; + white-space: nowrap; + } + + body.mh-business-theme .controls { + display: flex; + flex-direction: column; + align-items: stretch; + justify-self: end; + gap: 12px; + min-width: min(100%, 520px); + position: relative; + z-index: 1; + } + + body.mh-business-theme .controls-top-row, + body.mh-business-theme .controls-top-actions { + display: flex; + align-items: center; + justify-content: flex-end; + gap: 10px; + flex-wrap: wrap; + } + + body.mh-business-theme .search { + width: min(520px, 100%); + align-self: flex-end; + min-height: 52px; + border-radius: 18px; + border: 1px solid rgba(255, 255, 255, 0.18); + background: rgba(255, 255, 255, 0.14); + color: #fff7eb; + padding: 14px 18px; + font-size: 15px; + font-weight: 800; + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.06); + } + + body.mh-business-theme .search::placeholder { + color: rgba(247, 240, 228, 0.66); + } + + body.mh-business-theme .status { + display: none !important; + } + + body.mh-business-theme .cards { + grid-column: 1 / -1; + display: grid; + grid-template-columns: repeat(6, minmax(0, 1fr)); + gap: 14px; + margin-top: 6px; + margin-bottom: 0; + position: relative; + z-index: 1; + } + + body.mh-business-theme .business-shell { + position: relative; + margin-top: 12px; + padding: 18px 18px 20px; + border-radius: 30px; + background: + radial-gradient(circle at 12% 12%, rgba(242, 196, 132, 0.10), transparent 26%), + radial-gradient(circle at 88% 10%, rgba(255, 255, 255, 0.08), transparent 18%), + linear-gradient(145deg, rgba(15, 58, 47, 0.96) 0%, rgba(26, 86, 69, 0.94) 100%); + border: 1px solid rgba(255, 255, 255, 0.10); + box-shadow: 0 26px 70px rgba(15, 58, 47, 0.16); + overflow: hidden; + } + + body.mh-business-theme .business-shell::before { + content: ""; + position: absolute; + inset: auto -10% -32% auto; + width: 360px; + height: 360px; + border-radius: 50%; + background: radial-gradient(circle, rgba(242, 196, 132, 0.16), transparent 70%); + pointer-events: none; + } + + body.mh-business-theme .business-shell::after { + content: ""; + position: absolute; + top: -110px; + right: 90px; + width: 260px; + height: 260px; + border-radius: 50%; + border: 1px solid rgba(242, 196, 132, 0.12); + pointer-events: none; + } + + body.mh-business-theme .cards-toolbar { + grid-column: 1 / -1; + display: flex; + flex-direction: column; + gap: 10px; + margin-bottom: 6px; + } + + body.mh-business-theme .cards-toolbar-row { + display: flex; + align-items: center; + gap: 10px; + flex-wrap: wrap; + } + + body.mh-business-theme .summary-filter-chip, + body.mh-business-theme .summary-year-chip { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 8px; + min-width: 58px; + padding: 9px 14px; + 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; + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04); + } + + body.mh-business-theme .summary-filter-chip { + flex-direction: column; + align-items: center; + justify-content: center; + min-height: 96px; + gap: 8px; + text-align: center; + } + + body.mh-business-theme .summary-filter-chip .label { + color: rgba(244, 239, 230, 0.92); + letter-spacing: 0.01em; + } + + body.mh-business-theme .summary-filter-chip .count { + color: #ffd08a; + font-size: 30px; + line-height: 1; + letter-spacing: -0.03em; + } + + body.mh-business-theme .summary-filter-chip .meta { + color: rgba(255, 230, 190, 0.92); + font-size: 11px; + font-weight: 800; + } + + body.mh-business-theme .summary-filter-chip.active, + 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.active .count { + color: #b86b1f; + } + + body.mh-business-theme .summary-filter-chip.active .label { + color: rgba(10, 42, 34, 0.78); + } + + body.mh-business-theme .summary-filter-chip.active .meta { + color: #7c5a20; + } + + body.mh-business-theme .card { + padding: 16px 16px 14px; + min-height: 108px; + border-radius: 22px; + background: linear-gradient(180deg, rgba(255, 250, 243, 0.98) 0%, rgba(247, 238, 225, 0.94) 100%); + border: 1px solid rgba(214, 193, 163, 0.78); + color: #173328; + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.75), + 0 14px 30px rgba(15, 58, 47, 0.08); + backdrop-filter: blur(10px); + display: flex; + flex-direction: column; + justify-content: center; + } + + body.mh-business-theme .card.management { + background: linear-gradient(180deg, rgba(255, 246, 232, 0.98) 0%, rgba(250, 236, 208, 0.96) 100%); + border-color: rgba(214, 138, 58, 0.42); + } + + body.mh-business-theme .card .k { + color: rgba(23, 51, 40, 0.68); + font-size: 11px; + font-weight: 900; + letter-spacing: 0.04em; + } + + body.mh-business-theme .card .v { + margin-top: 7px; + color: #173328; + font-size: clamp(19px, 1.55vw, 27px); + font-weight: 900; + letter-spacing: -0.03em; + white-space: nowrap; + } + + body.mh-business-theme .card .n { + margin-top: 5px; + color: rgba(63, 84, 74, 0.88); + font-size: 12px; + font-weight: 700; + line-height: 1.4; + } + + body.mh-business-theme .panel { + background: var(--panel); + border: 1px solid var(--line); + border-radius: 26px; + box-shadow: var(--shadow); + overflow: hidden; + position: relative; + z-index: 1; + } + + body.mh-business-theme .table-wrap { + overflow: auto; + } + + body.mh-business-theme table { + width: 100%; + min-width: 1250px; + border-collapse: collapse; + } + + body.mh-business-theme thead th { + background: #122b23; + color: rgba(247, 240, 228, 0.92); + font-size: 11px; + letter-spacing: 0.08em; + padding: 12px 10px; + text-align: left; + white-space: nowrap; + vertical-align: middle; + } + + body.mh-business-theme .th-trigger, + body.mh-business-theme .th-trigger:hover, + body.mh-business-theme .th-trigger.active, + body.mh-business-theme .th-trigger.open { + color: rgba(247, 240, 228, 0.92); + } + + body.mh-business-theme .th-mark, + body.mh-business-theme .th-caret, + body.mh-business-theme .th-meta { + color: #f2c484; + } + + body.mh-business-theme tbody td { + padding: 12px 10px; + border-bottom: 1px solid #ece1cf; + font-size: 13px; + background: #fffaf3; + } + + body.mh-business-theme th:nth-child(1), + body.mh-business-theme td:nth-child(1) { width: 5%; } + body.mh-business-theme th:nth-child(2), + body.mh-business-theme td:nth-child(2) { width: 6%; } + body.mh-business-theme th:nth-child(3), + body.mh-business-theme td:nth-child(3) { width: 31%; } + body.mh-business-theme th:nth-child(4), + body.mh-business-theme td:nth-child(4) { width: 12%; } + body.mh-business-theme th:nth-child(5), + body.mh-business-theme td:nth-child(5) { width: 7%; } + body.mh-business-theme th:nth-child(6), + body.mh-business-theme td:nth-child(6) { width: 6%; } + body.mh-business-theme th:nth-child(7), + body.mh-business-theme td:nth-child(7) { width: 9%; } + body.mh-business-theme th:nth-child(8), + body.mh-business-theme td:nth-child(8) { width: 8%; } + body.mh-business-theme th:nth-child(9), + body.mh-business-theme td:nth-child(9) { width: 8%; } + body.mh-business-theme th:nth-child(10), + body.mh-business-theme td:nth-child(10) { width: 8%; } + body.mh-business-theme th:nth-child(11), + body.mh-business-theme td:nth-child(11) { width: 5%; } + + body.mh-business-theme tbody tr:hover td { + background: #fff5e8; + } + + body.mh-business-theme .name, + body.mh-business-theme td strong { + color: #10251d; + } + + body.mh-business-theme .subline { + color: #7c8b82; + } + + body.mh-business-theme .client-main { + display: block; + font-weight: 800; + color: #10251d; + } + + body.mh-business-theme .badge.badge-baron { + border-color: #f0bb75; + background: #fff2df; + color: #b96820; + } + + body.mh-business-theme .badge.badge-family { + border-color: #a5cbb6; + background: #eef7f1; + color: #2d6a4f; + } + + body.mh-business-theme .group-row td { + padding: 12px 14px 10px; + background: linear-gradient(180deg, #fff9ef 0%, #f5ebdd 100%); + border-top: 1px solid #ead8bc; + border-bottom: 1px solid #ead8bc; + } + + body.mh-business-theme .group-chip { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 7px 13px; + border-radius: 999px; + background: #fffdfa; + border: 1px solid #d6c1a3; + color: #244638; + font-size: 12px; + font-weight: 900; + box-shadow: 0 10px 24px rgba(15, 58, 47, 0.10); + 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.16); + color: #8a5a1f; + font-size: 14px; + font-weight: 900; + line-height: 1; + } + + body.mh-business-theme .detail-row td { + background: linear-gradient(180deg, #fff7ec 0%, #fffdf8 100%); + border-top: 1px solid #ecd8ba; + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.65); + } + + body.mh-business-theme .detail-row .inline-panel { + background: transparent; + border-left: 3px solid var(--accent); + } + + body.mh-business-theme .detail-row .inline-card, + body.mh-business-theme .detail-row .ledger-block { + box-shadow: 0 10px 24px rgba(15, 58, 47, 0.08); + } + + body.mh-business-theme .table-top-note { + display: flex; + justify-content: flex-end; + margin: 0 2px 10px; + } + + body.mh-business-theme .table-vat-note { + display: inline-flex; + align-items: center; + padding: 6px 10px; + border-radius: 999px; + background: rgba(255, 248, 238, 0.96); + border: 1px solid rgba(242, 196, 132, 0.46); + color: #6f5528; + font-size: 11px; + font-weight: 900; + letter-spacing: -0.01em; + } + + @media (max-width: 1280px) { + body.mh-business-theme .top { + grid-template-columns: 1fr; + } + + body.mh-business-theme .controls { + min-width: 0; + justify-self: stretch; + } + + body.mh-business-theme .controls-top-row, + body.mh-business-theme .controls-top-actions { + justify-content: flex-start; + } + + body.mh-business-theme .search { + width: 100%; + align-self: stretch; + } + + body.mh-business-theme .cards { + grid-template-columns: repeat(2, minmax(140px, 1fr)); + } + } + + @media (max-width: 720px) { + body.mh-business-theme .wrap { + width: calc(100vw - 30px); + max-width: calc(100vw - 30px); + padding: 12px 10px; + } + + body.mh-business-theme .top { + padding: 18px 14px 16px; + gap: 14px; + } + + body.mh-business-theme .brand-head { + gap: 8px; + align-items: flex-start; + } + + body.mh-business-theme .brand-company { + font-size: 16px; + } + + body.mh-business-theme .brand-title { + font-size: 30px; + } + + body.mh-business-theme .brand-subtitle { + display: inline; + margin-left: 4px; + margin-top: 0; + white-space: normal; + } + } + + .filter-row { + display: grid; + grid-template-columns: 1fr 160px 180px 180px; + gap: 12px; + margin-bottom: 14px; + } + + .year-summary { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); + gap: 10px; + margin-bottom: 14px; + } + + .year-card { + padding: 14px 16px; + border-radius: 18px; + background: linear-gradient(180deg, #fffdf8 0%, #f3e7d6 100%); + border: 1px solid var(--line); + cursor: pointer; + transition: transform 0.16s ease, box-shadow 0.16s ease, border-color 0.16s ease, background 0.16s ease; + } + + .year-card:hover { + transform: translateY(-2px); + box-shadow: 0 10px 22px rgba(18, 48, 30, 0.10); + } + + .year-card.active { + background: linear-gradient(145deg, var(--brand-deep) 0%, var(--brand) 62%, var(--brand-soft) 100%); + border-color: rgba(214, 138, 58, 0.42); + box-shadow: 0 14px 30px rgba(15, 58, 47, 0.2); + } + + .year-card.active .year-card-label, + .year-card.active .year-card-value, + .year-card.active .year-card-sub { + color: #f4efe6; + } + + .year-card-label { + font-size: 11px; + font-weight: 900; + letter-spacing: 0.14em; + color: #7a684d; + } + + .year-card-value { + margin-top: 8px; + font-size: clamp(15px, 1.45vw, 18px); + font-weight: 900; + color: var(--brand); + line-height: 1.2; + white-space: nowrap; + letter-spacing: -0.04em; + } + + .year-card-sub { + margin-top: 4px; + font-size: 12px; + font-weight: 700; + color: var(--muted); + } + + .input, .select { + width: 100%; + border: 1px solid #d7c4a7; + background: #fffdf8; + border-radius: 14px; + padding: 12px 14px; + font-size: 14px; + font-weight: 700; + color: var(--ink); + outline: none; + } + + .input:focus, .select:focus { + border-color: var(--brand-soft); + box-shadow: 0 0 0 3px rgba(47, 153, 115, 0.12); + } + + .chip-row { + display: flex; + gap: 8px; + flex-wrap: wrap; + margin-bottom: 12px; + } + + .chip { + padding: 8px 12px; + border-radius: 999px; + background: #efe3cf; + color: #315243; + font-size: 12px; + font-weight: 900; + } + + .table-wrap { + overflow: auto; + border-top: 1px solid var(--line); + } + + .table-summary { + display: flex; + justify-content: flex-end; + gap: 18px; + padding: 14px 20px 18px; + border-top: 1px solid var(--line); + background: #fbf4ea; + font-size: 13px; + font-weight: 800; + color: #244233; + } + + .table-summary strong { + color: #12301e; + font-size: 15px; + } + + table { + width: 100%; + min-width: 1180px; + border-collapse: separate; + border-spacing: 0; + } + + th, td { + border-right: 1px solid #ecdfcc; + border-bottom: 1px solid #ecdfcc; + padding: 11px 12px; + vertical-align: middle; + text-align: left; + font-size: 13px; + line-height: 1.55; + white-space: nowrap; + } + + th:last-child, td:last-child { border-right: none; } + + thead th { + position: sticky; + top: 0; + z-index: 1; + background: linear-gradient(180deg, var(--brand-deep) 0%, var(--brand) 100%); + color: #f4efe6; + font-size: 12px; + font-weight: 900; + text-align: center; + border-bottom: 1px solid rgba(242, 196, 132, 0.3); + } + + tbody tr:nth-child(even) td { + background: #fbf4ea; + } + + .money { + white-space: nowrap; + font-weight: 900; + color: var(--brand-soft); + } + + .muted { + color: var(--muted); + font-size: 12px; + } + + .cell-filter { + padding: 0; + border: none; + background: transparent; + color: #12301e; + font: inherit; + font-weight: 800; + cursor: pointer; + white-space: nowrap; + text-decoration: underline; + text-decoration-thickness: 1px; + text-underline-offset: 3px; + } + + .cell-filter:hover { + color: var(--mint); + } + + .chip-action { + border: 1px solid #d9c8af; + background: #fffaf2; + cursor: pointer; + } + + .chip-action:hover { + border-color: var(--mint); + color: var(--mint); + } + + .empty { + padding: 24px; + text-align: center; + color: var(--muted); + font-weight: 800; + } + + .page { + position: relative; + } + + .page::before { + content: ""; + position: fixed; + inset: 0; + background: + linear-gradient(rgba(15, 58, 47, 0.03) 1px, transparent 1px), + linear-gradient(90deg, rgba(15, 58, 47, 0.03) 1px, transparent 1px); + background-size: 32px 32px; + mask-image: linear-gradient(180deg, rgba(0, 0, 0, 0.34), transparent 82%); + pointer-events: none; + z-index: 0; + } + + .hero { + padding: 28px 34px 24px; + border-radius: 32px; + box-shadow: 0 30px 70px rgba(15, 58, 47, 0.22); + } + + .hero-grid { + position: relative; + z-index: 1; + display: grid; + grid-template-columns: minmax(0, 1fr); + gap: 24px; + align-items: end; + } + + .hero-grid > div:first-child { + position: relative; + min-height: 108px; + padding-right: 260px; + } + + .brand-kicker { + display: inline-flex; + align-items: center; + gap: 10px; + padding: 9px 14px; + border-radius: 999px; + background: rgba(255, 255, 255, 0.08); + border: 1px solid rgba(242, 196, 132, 0.24); + color: rgba(255, 244, 230, 0.86); + font-size: 11px; + font-weight: 900; + letter-spacing: 0.22em; + text-transform: uppercase; + } + + .brand-kicker::before { + content: ""; + width: 8px; + height: 8px; + border-radius: 50%; + background: var(--accent-soft); + box-shadow: 0 0 0 6px rgba(242, 196, 132, 0.12); + } + + .brand-line { + display: grid; + grid-template-columns: 76px minmax(0, 1fr); + gap: 20px; + align-items: center; + margin-top: 0; + } + + .hero-logo { + position: relative; + width: 76px; + height: 76px; + display: grid; + place-items: center; + border-radius: 24px; + background: + radial-gradient(circle at 30% 26%, rgba(255, 255, 255, 0.24), transparent 20%), + radial-gradient(circle at 68% 72%, rgba(242, 196, 132, 0.34), transparent 18%), + linear-gradient(145deg, rgba(242, 196, 132, 0.18) 0%, rgba(255, 255, 255, 0.04) 100%); + border: 1px solid rgba(242, 196, 132, 0.24); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.12), 0 18px 36px rgba(10, 42, 34, 0.24); + } + + .hero-logo::before { + content: ""; + position: absolute; + inset: 10px; + border-radius: 18px; + border: 1px solid rgba(242, 196, 132, 0.16); + } + + .hero-logo::after { + content: ""; + position: absolute; + left: 50%; + top: 50%; + width: 28px; + height: 28px; + transform: translate(-50%, -50%); + border-radius: 50%; + background: radial-gradient(circle, rgba(242, 196, 132, 0.92), rgba(214, 138, 58, 0.18)); + box-shadow: + 0 0 0 8px rgba(242, 196, 132, 0.06), + 0 0 24px rgba(242, 196, 132, 0.24); + } + + .hero-logo-core { + display: none; + } + + .hero-copy { + display: none; + } + + .hero-side { + display: grid; + gap: 12px; + } + + .hero-mini-card { + position: relative; + padding: 16px 18px 16px 58px; + border-radius: 22px; + background: linear-gradient(180deg, rgba(255, 255, 255, 0.11) 0%, rgba(255, 255, 255, 0.06) 100%); + border: 1px solid rgba(255, 255, 255, 0.12); + min-height: 88px; + } + + .hero-mini-card::before { + content: attr(data-mark); + position: absolute; + left: 16px; + top: 16px; + width: 30px; + height: 30px; + border-radius: 12px; + display: grid; + place-items: center; + background: rgba(242, 196, 132, 0.18); + color: #ffe6bf; + font-size: 11px; + font-weight: 1000; + letter-spacing: 0.08em; + } + + .hero-mini-label { + color: rgba(255, 244, 230, 0.62); + font-size: 11px; + font-weight: 900; + letter-spacing: 0.16em; + text-transform: uppercase; + } + + .hero-mini-value { + margin-top: 8px; + color: #fff7eb; + font-size: 24px; + font-weight: 1000; + letter-spacing: -0.03em; + } + + .hero-mini-sub { + margin-top: 6px; + color: rgba(255, 244, 230, 0.76); + font-size: 13px; + line-height: 1.5; + font-weight: 700; + } + + .tabs-shell { + margin-top: 18px; + padding: 12px; + border-radius: 24px; + background: linear-gradient(180deg, rgba(255, 255, 255, 0.68) 0%, rgba(255, 250, 243, 0.92) 100%); + border: 1px solid rgba(217, 197, 168, 0.8); + box-shadow: 0 18px 36px rgba(15, 58, 47, 0.08); + backdrop-filter: blur(10px); + } + + .tabs-shell .tabs { + margin-top: 0; + } + + .page > .hero:not(.hero-reimagined), + .page > .tabs:not(.tabs-reimagined) { + display: none; + } + + .summary-card { + min-height: 122px; + overflow: hidden; + transition: transform 0.18s ease, box-shadow 0.18s ease; + } + + .summary-card:hover { + transform: translateY(-3px); + box-shadow: 0 18px 30px rgba(10, 42, 34, 0.16); + } + + .summary-card::after { + content: ""; + position: absolute; + inset: auto -14px -28px auto; + width: 96px; + height: 96px; + border-radius: 50%; + background: radial-gradient(circle, rgba(242, 196, 132, 0.14), transparent 70%); + } + + .summary-top { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + display: none; + } + + .summary-icon { + width: 42px; + height: 42px; + border-radius: 14px; + display: grid; + place-items: center; + background: rgba(255, 255, 255, 0.14); + border: 1px solid rgba(255, 255, 255, 0.12); + color: #fff3df; + font-size: 11px; + font-weight: 1000; + letter-spacing: 0.08em; + } + + .summary-tail { + margin-top: auto; + padding-top: 8px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + color: rgba(255, 244, 230, 0.74); + font-size: 11px; + font-weight: 800; + } + + .summary-card .summary-value { + margin-top: 0; + } + + .summary-line { + flex: 1; + height: 4px; + border-radius: 999px; + background: rgba(255, 255, 255, 0.12); + overflow: hidden; + } + + .summary-line span { + display: block; + height: 100%; + width: 64%; + border-radius: inherit; + background: linear-gradient(90deg, rgba(242, 196, 132, 0.96), rgba(47, 153, 115, 0.88)); + } + + .panel { + position: relative; + } + + .panel::before { + content: ""; + position: absolute; + inset: 0 0 auto 0; + height: 4px; + background: linear-gradient(90deg, var(--accent), #edd6af 38%, var(--mint) 100%); + opacity: 0.9; + } + + tbody tr:hover td { + background: #f5ecdf; + } + + @media (max-width: 1180px) { + .hero-grid { + grid-template-columns: 1fr; + } + + .hero-grid > div:first-child { + min-height: auto; + padding-right: 0; + } + + .brand-line { + grid-template-columns: 92px minmax(0, 1fr); + gap: 16px; + } + + .hero-logo { + width: 68px; + height: 68px; + } + + .hero-actions { + position: static; + margin-top: 18px; + justify-content: flex-start; + } + + .layout { + grid-template-columns: 1fr; + } + + .filter-row { + grid-template-columns: 1fr; + } + + .summary { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + } + + @media (max-width: 720px) { + h1 { + font-size: 34px; + } + + .summary { + grid-template-columns: 1fr; + } + } diff --git a/incoming-files/served/ledger/README.md b/incoming-files/served/ledger/README.md new file mode 100644 index 0000000..f88ae40 --- /dev/null +++ b/incoming-files/served/ledger/README.md @@ -0,0 +1,21 @@ +# Ledger Served Assets + +`8081` 사업관리대장 화면이 실제로 읽는 런타임 파일 모음이다. + +source-of-truth: + +- [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) + +- `index.html`: `/integrations/ledger` 응답 본문 + - `frontend/apps/ledger/index.html` 템플릿에서 publish 시 생성 +- `MH 통합 대시보드_260320.css`: ledger base stylesheet +- `ledger-override.css`: 8081 ledger 디자인/레이아웃 오버라이드 +- `ledger-override.js`: 8081 ledger 상호작용/테이블/팝업 오버라이드 +- `사업관리대장-1.xlsx`: startup 시 기본 원본 DB 동기화에 사용하는 기본 데이터 파일 + +규칙: + +- backend는 `사업관리대장` 원본 wrapper를 더 이상 직접 읽지 않는다. +- runtime asset 수정은 `frontend/apps/ledger` 기준으로 먼저 반영하고, 이 디렉터리로 publish 한다. +- 원본 비교가 필요하면 `incoming-files/reference/ledger/`를 본다. diff --git a/incoming-files/served/ledger/index.html b/incoming-files/served/ledger/index.html new file mode 100644 index 0000000..4098019 --- /dev/null +++ b/incoming-files/served/ledger/index.html @@ -0,0 +1,954 @@ + + + + + + 사업관리대장 Dashboard + + + + +
+
+
Live Management

사업관리대장 | Dashboard

+
+
+
CSV/XLSX 파일을 업로드하면 데이터가 표시됩니다.
+
+
+
+ + + + + + + + + + + + + + +
+
+ +
+
+
+
+ +
+
+
+
+ +
+
+
+
+ +
+
+
+
+ +
+
+
+
+ +
+
+
+
+ +
+
+
+
+ +
+
+
+
+
표시할 데이터가 없습니다.
+
+
+ + + + + + diff --git a/incoming-files/served/ledger/ledger-override.css b/incoming-files/served/ledger/ledger-override.css new file mode 100644 index 0000000..505b65e --- /dev/null +++ b/incoming-files/served/ledger/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/incoming-files/served/ledger/ledger-override.js b/incoming-files/served/ledger/ledger-override.js new file mode 100644 index 0000000..853e51c --- /dev/null +++ b/incoming-files/served/ledger/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/incoming-files/served/ledger/사업관리대장-1.xlsx b/incoming-files/served/ledger/사업관리대장-1.xlsx new file mode 100644 index 0000000..a6d20ac Binary files /dev/null and b/incoming-files/served/ledger/사업관리대장-1.xlsx differ diff --git a/scripts/prepare_dev_worktree.sh b/scripts/prepare_dev_worktree.sh index 904c209..c3b961b 100755 --- a/scripts/prepare_dev_worktree.sh +++ b/scripts/prepare_dev_worktree.sh @@ -44,7 +44,7 @@ copy_optional_path "incoming-files/1.png" copy_optional_path "incoming-files/260320.html" copy_optional_path "incoming-files/sample style.css" copy_optional_path "incoming-files/seat/center_chair_people_map(2).html" -copy_optional_path "incoming-files/사업관리대장" +copy_optional_path "incoming-files/reference/ledger" echo "[6/6] Dev worktree ready" echo "Path: ${DEV_DIR}" diff --git a/scripts/publish_ledger_app.sh b/scripts/publish_ledger_app.sh new file mode 100755 index 0000000..2665244 --- /dev/null +++ b/scripts/publish_ledger_app.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash + +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +APP_DIR="${ROOT_DIR}/frontend/apps/ledger" +TARGET_DIR="${ROOT_DIR}/incoming-files/served/ledger" +LEDGER_ASSET_VERSION="${LEDGER_ASSET_VERSION:-20260401-03}" + +mkdir -p "${TARGET_DIR}" + +cp "${APP_DIR}/assets/MH 통합 대시보드_260320.css" "${TARGET_DIR}/MH 통합 대시보드_260320.css" +cp "${APP_DIR}/assets/ledger-override.css" "${TARGET_DIR}/ledger-override.css" +cp "${APP_DIR}/assets/ledger-override.js" "${TARGET_DIR}/ledger-override.js" + +HEAD_ASSETS='' +BODY_SCRIPTS='' + +perl -0pe 's|__LEDGER_HEAD_ASSETS__|'"${HEAD_ASSETS}"'|g; s|__LEDGER_BODY_SCRIPTS__|'"${BODY_SCRIPTS}"'|g' \ + "${APP_DIR}/index.html" > "${TARGET_DIR}/index.html" + +echo "Published ledger app source to ${TARGET_DIR}" diff --git a/scripts/publish_payment_app.sh b/scripts/publish_payment_app.sh new file mode 100755 index 0000000..81e2543 --- /dev/null +++ b/scripts/publish_payment_app.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash + +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +APP_DIR="${ROOT_DIR}/frontend/apps/payment" +TARGET_FILE="${ROOT_DIR}/incoming-files/served/payment.html" +COMPARE_FILE="${ROOT_DIR}/incoming-files/payment.html" + +cp "${APP_DIR}/index.html" "${TARGET_FILE}" +cp "${APP_DIR}/index.html" "${COMPARE_FILE}" + +echo "Published payment app source to ${TARGET_FILE}" diff --git a/scripts/publish_team_app.sh b/scripts/publish_team_app.sh new file mode 100755 index 0000000..f6cf187 --- /dev/null +++ b/scripts/publish_team_app.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash + +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +APP_DIR="${ROOT_DIR}/frontend/apps/team" +TARGET_FILE="${ROOT_DIR}/incoming-files/served/mh.html" +COMPARE_FILE="${ROOT_DIR}/incoming-files/mh.html" + +cp "${APP_DIR}/index.html" "${TARGET_FILE}" +cp "${APP_DIR}/index.html" "${COMPARE_FILE}" + +echo "Published team app source to ${TARGET_FILE}"