Update WEHAGO comparison data and tools
This commit is contained in:
@@ -3,3 +3,7 @@ __pycache__/
|
||||
*.pyc
|
||||
data.db-shm
|
||||
data.db-wal
|
||||
backups/
|
||||
tmp_*.py
|
||||
static/exports/
|
||||
scripts/data_download/
|
||||
|
||||
@@ -23,10 +23,12 @@ from fastapi.staticfiles import StaticFiles
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from openpyxl import load_workbook
|
||||
from sqlalchemy import bindparam, create_engine, event, text
|
||||
from sqlalchemy.exc import OperationalError
|
||||
from datasette.app import Datasette
|
||||
from wehago_compare import (
|
||||
enqueue_default_pair_recommend_precompute,
|
||||
get_erp_filtered_rows,
|
||||
get_compare_snapshot_status,
|
||||
get_individual_pair_recommendations,
|
||||
get_last_action_summary,
|
||||
get_status_field_suggestions,
|
||||
@@ -36,6 +38,7 @@ from wehago_compare import (
|
||||
get_wehago_filtered_rows,
|
||||
import_uploaded_erp_voucher_file,
|
||||
init_wehago_compare_db,
|
||||
request_compare_snapshot_rebuild,
|
||||
recommend_pair_matches,
|
||||
save_recommended_pair_matches,
|
||||
save_manual_pair_matches,
|
||||
@@ -54,8 +57,13 @@ _DB_BACKUP_STATE: dict[str, Any] = {
|
||||
"last_run_at": 0.0,
|
||||
}
|
||||
_DB_BACKUP_LOCK = threading.Lock()
|
||||
_DB_INIT_LOCK = threading.Lock()
|
||||
_DB_INIT_DONE = False
|
||||
_DB_ANALYZE_LOCK = threading.Lock()
|
||||
_DB_ANALYZE_LAST_ATTEMPT_AT = 0.0
|
||||
DB_BACKUP_MIN_INTERVAL_SECONDS = 900.0
|
||||
DB_BACKUP_KEEP_COUNT = 24
|
||||
DB_ANALYZE_MIN_INTERVAL_SECONDS = 6 * 60 * 60
|
||||
PROCESS_COST_CACHE_TTL_SECONDS = 120.0
|
||||
_PROCESS_COST_RUNTIME_CACHE_LOCK = threading.Lock()
|
||||
_PROCESS_COST_PROJECT_OPTIONS_CACHE: dict[tuple[Any, ...], dict[str, Any]] = {}
|
||||
@@ -395,8 +403,30 @@ DEFAULT_APP_KEYWORD_RULES = {
|
||||
|
||||
|
||||
def ensure_default_app_config(conn: Any) -> None:
|
||||
def _clean_text(value: Any) -> str:
|
||||
return "" if value is None else str(value).strip()
|
||||
|
||||
existing_option_rows = conn.execute(
|
||||
text(
|
||||
"""
|
||||
SELECT group_key, item_key, label, value_text, sort_order
|
||||
FROM app_option_items
|
||||
"""
|
||||
)
|
||||
).mappings().all()
|
||||
existing_options = {
|
||||
(_clean_text(row.get("group_key")), _clean_text(row.get("item_key"))): (
|
||||
_clean_text(row.get("label")),
|
||||
_clean_text(row.get("value_text")),
|
||||
int(row.get("sort_order") or 0),
|
||||
)
|
||||
for row in existing_option_rows
|
||||
}
|
||||
for group_key, items in DEFAULT_APP_OPTION_ITEMS.items():
|
||||
for sort_order, (item_key, label, value_text) in enumerate(items):
|
||||
existing_value = existing_options.get((group_key, item_key))
|
||||
if existing_value == (label, value_text, sort_order):
|
||||
continue
|
||||
conn.execute(
|
||||
text(
|
||||
"""
|
||||
@@ -420,8 +450,22 @@ def ensure_default_app_config(conn: Any) -> None:
|
||||
},
|
||||
)
|
||||
|
||||
existing_rule_rows = conn.execute(
|
||||
text(
|
||||
"""
|
||||
SELECT rule_group, category_key, keyword, sort_order
|
||||
FROM app_keyword_rules
|
||||
"""
|
||||
)
|
||||
).mappings().all()
|
||||
existing_rules = {
|
||||
(_clean_text(row.get("rule_group")), _clean_text(row.get("category_key")), _clean_text(row.get("keyword"))): int(row.get("sort_order") or 0)
|
||||
for row in existing_rule_rows
|
||||
}
|
||||
for rule_group, items in DEFAULT_APP_KEYWORD_RULES.items():
|
||||
for sort_order, (category_key, keyword) in enumerate(items):
|
||||
if existing_rules.get((rule_group, category_key, keyword)) == sort_order:
|
||||
continue
|
||||
conn.execute(
|
||||
text(
|
||||
"""
|
||||
@@ -445,8 +489,34 @@ def ensure_default_app_config(conn: Any) -> None:
|
||||
load_app_config.cache_clear()
|
||||
|
||||
|
||||
def init_db() -> None:
|
||||
def _maybe_run_db_analyze() -> None:
|
||||
global _DB_ANALYZE_LAST_ATTEMPT_AT
|
||||
now = time.time()
|
||||
if now - _DB_ANALYZE_LAST_ATTEMPT_AT < DB_ANALYZE_MIN_INTERVAL_SECONDS:
|
||||
return
|
||||
with _DB_ANALYZE_LOCK:
|
||||
now = time.time()
|
||||
if now - _DB_ANALYZE_LAST_ATTEMPT_AT < DB_ANALYZE_MIN_INTERVAL_SECONDS:
|
||||
return
|
||||
_DB_ANALYZE_LAST_ATTEMPT_AT = now
|
||||
try:
|
||||
with engine.begin() as conn:
|
||||
conn.execute(text("ANALYZE"))
|
||||
except OperationalError as exc:
|
||||
logger.warning("ANALYZE skipped due to database lock: %s", exc)
|
||||
except Exception as exc:
|
||||
logger.warning("ANALYZE skipped due to unexpected error: %s", exc)
|
||||
|
||||
|
||||
def init_db() -> None:
|
||||
global _DB_INIT_DONE
|
||||
if _DB_INIT_DONE:
|
||||
return
|
||||
with _DB_INIT_LOCK:
|
||||
if _DB_INIT_DONE:
|
||||
return
|
||||
conn = engine.connect()
|
||||
trans = conn.begin()
|
||||
conn.execute(
|
||||
text(
|
||||
"""
|
||||
@@ -1333,8 +1403,11 @@ def init_db() -> None:
|
||||
migrate_project_status_entries(conn)
|
||||
migrate_project_basic_info(conn)
|
||||
ensure_default_app_config(conn)
|
||||
conn.execute(text("ANALYZE"))
|
||||
trans.commit()
|
||||
conn.close()
|
||||
init_wehago_compare_db(engine)
|
||||
_DB_INIT_DONE = True
|
||||
_maybe_run_db_analyze()
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
@@ -8459,11 +8532,36 @@ def render_wehago_compare_page(
|
||||
return templates.TemplateResponse(request, "wehago_compare.html", context)
|
||||
|
||||
|
||||
def build_wehago_compare_health_payload() -> dict[str, Any]:
|
||||
init_db()
|
||||
payload = get_wehago_compare_dashboard(
|
||||
engine,
|
||||
include_metric_counts=False,
|
||||
warm_caches=False,
|
||||
)
|
||||
return {
|
||||
"status": "ok",
|
||||
"selected_start_year": payload.get("selected_start_year"),
|
||||
"selected_end_year": payload.get("selected_end_year"),
|
||||
"available_year_count": len(payload.get("available_years") or []),
|
||||
"metric_section_count": len(payload.get("metric_sections") or []),
|
||||
}
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
async def health() -> dict[str, str]:
|
||||
return build_health_payload()
|
||||
|
||||
|
||||
@app.get("/health/wehago-compare")
|
||||
async def health_wehago_compare() -> JSONResponse:
|
||||
try:
|
||||
return JSONResponse(content=jsonable_encoder(build_wehago_compare_health_payload()))
|
||||
except Exception as exc:
|
||||
logger.exception("전표비교 readiness 에러: %s", exc)
|
||||
return JSONResponse(content={"status": "error", "error": str(exc)}, status_code=500)
|
||||
|
||||
|
||||
@app.get("/")
|
||||
async def home(request: Request, edit_id: int | None = None, overview_year: int | None = None):
|
||||
try:
|
||||
@@ -9114,6 +9212,44 @@ async def wehago_compare_summary(
|
||||
return JSONResponse(content={"error": str(exc)}, status_code=500)
|
||||
|
||||
|
||||
@app.get("/wehago-compare/api/snapshot-status")
|
||||
async def wehago_compare_snapshot_status(
|
||||
start_year: int | None = None,
|
||||
end_year: int | None = None,
|
||||
):
|
||||
try:
|
||||
payload = get_compare_snapshot_status(
|
||||
engine,
|
||||
start_year=start_year,
|
||||
end_year=end_year,
|
||||
)
|
||||
return JSONResponse(content=jsonable_encoder(payload))
|
||||
except Exception as exc:
|
||||
logger.exception("전표비교 스냅샷 상태 조회 에러: %s", exc)
|
||||
return JSONResponse(content={"error": str(exc)}, status_code=500)
|
||||
|
||||
|
||||
@app.post("/wehago-compare/api/snapshot-rebuild")
|
||||
async def wehago_compare_snapshot_rebuild(request: Request):
|
||||
try:
|
||||
payload = await request.json()
|
||||
if not isinstance(payload, dict):
|
||||
payload = {}
|
||||
start_year = payload.get("start_year")
|
||||
end_year = payload.get("end_year")
|
||||
include_metric_counts = normalize_text(payload.get("include_metric_counts")) not in {"0", "false", "n", "no", "off"}
|
||||
response = request_compare_snapshot_rebuild(
|
||||
engine,
|
||||
start_year=int(start_year) if start_year is not None else None,
|
||||
end_year=int(end_year) if end_year is not None else None,
|
||||
include_metric_counts=include_metric_counts,
|
||||
)
|
||||
return JSONResponse(content=jsonable_encoder({"ok": True, **response}))
|
||||
except Exception as exc:
|
||||
logger.exception("전표비교 스냅샷 재생성 요청 에러: %s", exc)
|
||||
return JSONResponse(content={"error": str(exc)}, status_code=500)
|
||||
|
||||
|
||||
@app.get("/wehago-compare/api/status-suggestions")
|
||||
async def wehago_compare_status_suggestions(
|
||||
start_year: int | None = None,
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
@echo off
|
||||
set "WEHAGO_CHROME_DEBUGGER_ADDRESS=127.0.0.1:9225"
|
||||
cd /d C:\Windows
|
||||
python -u "\\wsl.localhost\Ubuntu\home\b17301\my-intranet-app\scripts\click_left_code_9225.py" %*
|
||||
@@ -0,0 +1,69 @@
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
|
||||
from selenium import webdriver
|
||||
from selenium.webdriver.chrome.options import Options
|
||||
|
||||
|
||||
target = sys.argv[1] if len(sys.argv) > 1 else "110"
|
||||
options = Options()
|
||||
options.add_experimental_option("debuggerAddress", os.environ.get("WEHAGO_CHROME_DEBUGGER_ADDRESS", "127.0.0.1:9225"))
|
||||
driver = webdriver.Chrome(options=options)
|
||||
|
||||
info = driver.execute_script(
|
||||
r"""
|
||||
const code = arguments[0];
|
||||
const out = [];
|
||||
for (const cell of document.querySelectorAll('.rg-data-cell, td[class*=rg-data-cell], [class*=rg-data-cell], td')) {
|
||||
const r = cell.getBoundingClientRect();
|
||||
const text = (cell.textContent || '').trim();
|
||||
if (text === code && r.left < 220 && r.top > 145 && r.width > 0 && r.height > 0) {
|
||||
out.push({left:r.left, top:r.top, width:r.width, height:r.height, text});
|
||||
}
|
||||
}
|
||||
return out.sort((a,b)=>a.top-b.top)[0] || null;
|
||||
""",
|
||||
target,
|
||||
)
|
||||
print("target", target, "info", json.dumps(info, ensure_ascii=False))
|
||||
if info:
|
||||
x = info["left"] + info["width"] / 2
|
||||
y = info["top"] + info["height"] / 2
|
||||
for typ in ["mouseMoved", "mousePressed", "mouseReleased"]:
|
||||
payload = {"type": typ, "x": x, "y": y, "button": "left", "clickCount": 1}
|
||||
if typ == "mouseMoved":
|
||||
payload["button"] = "none"
|
||||
driver.execute_cdp_cmd("Input.dispatchMouseEvent", payload)
|
||||
time.sleep(0.5)
|
||||
|
||||
visible = driver.execute_script(
|
||||
r"""
|
||||
const rows = [];
|
||||
const colorScore = (color) => {
|
||||
const m = String(color || '').match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
|
||||
if (!m) return 0;
|
||||
const r = Number(m[1]), g = Number(m[2]), b = Number(m[3]);
|
||||
return Math.max(0, b - r) + Math.max(0, g - r) + (b > 120 ? 20 : 0);
|
||||
};
|
||||
for (const cell of document.querySelectorAll('.rg-data-cell, td[class*=rg-data-cell], [class*=rg-data-cell], td')) {
|
||||
const r = cell.getBoundingClientRect();
|
||||
const code = (cell.textContent || '').trim();
|
||||
if (!/^\d{3,6}$/.test(code) || r.left > 220 || r.top < 145 || r.width <= 0 || r.height <= 0) continue;
|
||||
let node = cell, score = 0, cls = '';
|
||||
while (node && node !== document.body) {
|
||||
const style = window.getComputedStyle(node);
|
||||
score = Math.max(score, colorScore(style.backgroundColor));
|
||||
cls += ' ' + String(node.className || '');
|
||||
node = node.parentElement;
|
||||
}
|
||||
if (/selected|select|focus|current|active|checked/i.test(cls)) score += 80;
|
||||
rows.push({code, top:r.top, score, cls: cls.slice(0,120)});
|
||||
}
|
||||
rows.sort((a,b)=>(b.score-a.score)||(a.top-b.top));
|
||||
return rows.slice(0,8);
|
||||
"""
|
||||
)
|
||||
print(json.dumps(visible, ensure_ascii=False, indent=2))
|
||||
driver.quit()
|
||||
@@ -0,0 +1,5 @@
|
||||
@echo off
|
||||
set "PYTHONIOENCODING=utf-8"
|
||||
set "WEHAGO_CHROME_DEBUGGER_ADDRESS=127.0.0.1:9225"
|
||||
cd /d C:\Windows
|
||||
python -u "\\wsl.localhost\Ubuntu\home\b17301\my-intranet-app\scripts\click_left_search_button_9225.py"
|
||||
@@ -0,0 +1,27 @@
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
|
||||
from selenium import webdriver
|
||||
from selenium.webdriver.chrome.options import Options
|
||||
|
||||
options = Options()
|
||||
options.add_experimental_option("debuggerAddress", os.environ.get("WEHAGO_CHROME_DEBUGGER_ADDRESS", "127.0.0.1:9225"))
|
||||
driver = webdriver.Chrome(options=options)
|
||||
|
||||
for typ in ["mouseMoved", "mousePressed", "mouseReleased"]:
|
||||
driver.execute_cdp_cmd(
|
||||
"Input.dispatchMouseEvent",
|
||||
{"type": typ, "x": 130, "y": 188, "button": "left" if typ != "mouseMoved" else "none", "clickCount": 1},
|
||||
)
|
||||
time.sleep(0.8)
|
||||
items = driver.execute_script(
|
||||
r"""
|
||||
return Array.from(document.querySelectorAll('input,button,[role=button],a,div')).map(el=>{
|
||||
const r=el.getBoundingClientRect(); const st=getComputedStyle(el);
|
||||
return {tag:el.tagName,id:el.id||'',cls:String(el.className||'').slice(0,80),text:(el.textContent||'').trim().slice(0,80),value:el.value||'',placeholder:el.getAttribute('placeholder')||'',title:el.getAttribute('title')||'',left:Math.round(r.left),top:Math.round(r.top),width:Math.round(r.width),height:Math.round(r.height),display:st.display,vis:st.visibility};
|
||||
}).filter(x=>x.width>0&&x.height>0&&x.display!=='none'&&x.vis!=='hidden'&&x.top<400&&x.left<700).sort((a,b)=>(a.top-b.top)||(a.left-b.left)).slice(0,200);
|
||||
"""
|
||||
)
|
||||
print(json.dumps(items, ensure_ascii=False, indent=2))
|
||||
driver.quit()
|
||||
@@ -0,0 +1,437 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any, Iterable
|
||||
|
||||
from openpyxl import Workbook
|
||||
from openpyxl.cell import WriteOnlyCell
|
||||
from openpyxl.styles import Alignment, Font, PatternFill
|
||||
from openpyxl.utils import get_column_letter
|
||||
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
if str(BASE_DIR) not in sys.path:
|
||||
sys.path.insert(0, str(BASE_DIR))
|
||||
|
||||
from main import engine
|
||||
from wehago_compare import (
|
||||
_get_cached_status_rows_by_range,
|
||||
_get_cached_voucher_sections_by_range,
|
||||
discover_available_years,
|
||||
)
|
||||
|
||||
EXPORT_DIR = BASE_DIR / "static" / "exports"
|
||||
HEADER_FILL = PatternFill(fill_type="solid", fgColor="1F2937")
|
||||
HEADER_FONT = Font(color="FFFFFF", bold=True)
|
||||
SUMMARY_FILL = PatternFill(fill_type="solid", fgColor="E5E7EB")
|
||||
RECHECK_FILL = PatternFill(fill_type="solid", fgColor="FEF3C7")
|
||||
UNMATCHED_FILL = PatternFill(fill_type="solid", fgColor="FDE68A")
|
||||
ERP_UNMATCHED_FILL = PatternFill(fill_type="solid", fgColor="DBEAFE")
|
||||
|
||||
|
||||
def clean(value: Any) -> str:
|
||||
return "" if value is None else str(value).strip()
|
||||
|
||||
|
||||
def amount(value: Any) -> float:
|
||||
text = clean(value).replace(",", "")
|
||||
if not text:
|
||||
return 0.0
|
||||
try:
|
||||
return float(text)
|
||||
except ValueError:
|
||||
return 0.0
|
||||
|
||||
|
||||
def autosize(ws) -> None:
|
||||
widths: dict[int, int] = {}
|
||||
for row in ws.iter_rows():
|
||||
for cell in row:
|
||||
value = "" if cell.value is None else str(cell.value)
|
||||
widths[cell.column] = min(max(widths.get(cell.column, 0), len(value) + 2), 42)
|
||||
for col_idx, width in widths.items():
|
||||
ws.column_dimensions[get_column_letter(col_idx)].width = width
|
||||
|
||||
|
||||
def style_sheet(ws) -> None:
|
||||
ws.freeze_panes = "A2"
|
||||
ws.auto_filter.ref = ws.dimensions
|
||||
for cell in ws[1]:
|
||||
cell.fill = HEADER_FILL
|
||||
cell.font = HEADER_FONT
|
||||
cell.alignment = Alignment(horizontal="center", vertical="center")
|
||||
for row in ws.iter_rows(min_row=2):
|
||||
status_value = clean(row[0].value)
|
||||
if status_value == "Recheck":
|
||||
fill = RECHECK_FILL
|
||||
elif status_value == "Unmatched":
|
||||
fill = UNMATCHED_FILL
|
||||
elif status_value == "ERP Unmatched":
|
||||
fill = ERP_UNMATCHED_FILL
|
||||
else:
|
||||
fill = None
|
||||
if fill:
|
||||
for cell in row:
|
||||
cell.fill = fill
|
||||
for cell in row:
|
||||
cell.alignment = Alignment(vertical="top", wrap_text=True)
|
||||
autosize(ws)
|
||||
|
||||
|
||||
def append_rows(ws, headers: list[str], rows: Iterable[list[Any]]) -> None:
|
||||
ws.append(headers)
|
||||
for row in rows:
|
||||
ws.append(row)
|
||||
style_sheet(ws)
|
||||
|
||||
|
||||
def append_rows_write_only(workbook: Workbook, title: str, headers: list[str], rows: Iterable[list[Any]]) -> None:
|
||||
ws = workbook.create_sheet(title)
|
||||
header_row: list[WriteOnlyCell] = []
|
||||
for value in headers:
|
||||
cell = WriteOnlyCell(ws, value=value)
|
||||
cell.fill = HEADER_FILL
|
||||
cell.font = HEADER_FONT
|
||||
cell.alignment = Alignment(horizontal="center", vertical="center")
|
||||
header_row.append(cell)
|
||||
ws.append(header_row)
|
||||
for row in rows:
|
||||
ws.append(list(row))
|
||||
|
||||
|
||||
def flatten_groups(
|
||||
groups: list[dict[str, Any]],
|
||||
card_label: str,
|
||||
only_statuses: set[str] | None = None,
|
||||
) -> list[list[Any]]:
|
||||
rows: list[list[Any]] = []
|
||||
for group_index, group in enumerate(groups, start=1):
|
||||
summary = group.get("summary", {})
|
||||
link_count = len(summary.get("erp_group_keys") or summary.get("wehago_group_keys") or [])
|
||||
group_rows = group.get("rows", [])
|
||||
for row_index, row in enumerate(group_rows, start=1):
|
||||
status_label = clean(row.get("status_label"))
|
||||
if only_statuses and status_label not in only_statuses:
|
||||
continue
|
||||
rows.append(
|
||||
[
|
||||
card_label,
|
||||
group_index,
|
||||
row_index,
|
||||
status_label,
|
||||
summary.get("fiscal_year"),
|
||||
clean(summary.get("ledger_date")),
|
||||
clean(summary.get("proof_date")),
|
||||
clean(summary.get("voucher_no")),
|
||||
clean(summary.get("draft_no")),
|
||||
clean(summary.get("ledger_accounts")),
|
||||
clean(summary.get("voucher_accounts")),
|
||||
clean(summary.get("ledger_vendors")),
|
||||
clean(summary.get("voucher_vendors")),
|
||||
clean(summary.get("review_reason")),
|
||||
len(group_rows),
|
||||
link_count,
|
||||
clean(row.get("ledger_date")),
|
||||
clean(row.get("proof_date")),
|
||||
clean(row.get("voucher_no")),
|
||||
clean(row.get("draft_no")),
|
||||
clean(row.get("ledger_account_name")),
|
||||
clean(row.get("voucher_account_name")),
|
||||
clean(row.get("ledger_vendor")),
|
||||
clean(row.get("voucher_vendor")),
|
||||
amount(row.get("ledger_debit")),
|
||||
amount(row.get("ledger_credit")),
|
||||
amount(row.get("voucher_debit")),
|
||||
amount(row.get("voucher_credit")),
|
||||
clean(row.get("ledger_desc")),
|
||||
clean(row.get("voucher_desc")),
|
||||
]
|
||||
)
|
||||
return rows
|
||||
|
||||
|
||||
def iter_card_issue_rows(groups: list[dict[str, Any]], card_kind: str) -> list[list[Any]]:
|
||||
issues: list[list[Any]] = []
|
||||
for group in groups:
|
||||
summary = group.get("summary", {})
|
||||
parent_year = summary.get("fiscal_year")
|
||||
parent_date = clean(summary.get("ledger_date") or summary.get("proof_date"))
|
||||
parent_voucher_no = clean(summary.get("voucher_no"))
|
||||
parent_draft_no = clean(summary.get("draft_no"))
|
||||
for row in group.get("rows", []):
|
||||
status_label = clean(row.get("status_label"))
|
||||
if card_kind == "wehago" and status_label not in {"ERP Unmatched", "Recheck"}:
|
||||
continue
|
||||
if card_kind == "voucher" and status_label not in {"Unmatched", "Recheck"}:
|
||||
continue
|
||||
issues.append(
|
||||
[
|
||||
status_label,
|
||||
parent_year,
|
||||
parent_date,
|
||||
parent_voucher_no,
|
||||
parent_draft_no,
|
||||
clean(summary.get("ledger_accounts")),
|
||||
clean(summary.get("voucher_accounts")),
|
||||
clean(summary.get("ledger_vendors")),
|
||||
clean(summary.get("voucher_vendors")),
|
||||
clean(summary.get("review_reason")),
|
||||
clean(row.get("ledger_date")),
|
||||
clean(row.get("proof_date")),
|
||||
clean(row.get("voucher_no")),
|
||||
clean(row.get("draft_no")),
|
||||
clean(row.get("ledger_account_name")),
|
||||
clean(row.get("voucher_account_name")),
|
||||
clean(row.get("ledger_vendor")),
|
||||
clean(row.get("voucher_vendor")),
|
||||
amount(row.get("ledger_debit")),
|
||||
amount(row.get("ledger_credit")),
|
||||
amount(row.get("voucher_debit")),
|
||||
amount(row.get("voucher_credit")),
|
||||
clean(row.get("ledger_desc")),
|
||||
clean(row.get("voucher_desc")),
|
||||
]
|
||||
)
|
||||
return issues
|
||||
|
||||
|
||||
def build_relation_rows(
|
||||
wehago_groups: list[dict[str, Any]],
|
||||
voucher_groups: list[dict[str, Any]],
|
||||
) -> list[list[Any]]:
|
||||
rows: list[list[Any]] = []
|
||||
for group in wehago_groups:
|
||||
summary = group.get("summary", {})
|
||||
erp_keys = summary.get("erp_group_keys") or set()
|
||||
if len(erp_keys) <= 1:
|
||||
continue
|
||||
rows.append(
|
||||
[
|
||||
"WEHAGO 1 : ERP N",
|
||||
summary.get("fiscal_year"),
|
||||
clean(summary.get("ledger_date")),
|
||||
clean(summary.get("proof_date")),
|
||||
clean(summary.get("voucher_no")),
|
||||
clean(summary.get("draft_no")),
|
||||
clean(summary.get("ledger_accounts")),
|
||||
clean(summary.get("voucher_accounts")),
|
||||
len(erp_keys),
|
||||
len(group.get("rows", [])),
|
||||
clean(summary.get("review_reason")),
|
||||
" | ".join(sorted(clean(key) for key in erp_keys if clean(key))),
|
||||
]
|
||||
)
|
||||
for group in voucher_groups:
|
||||
summary = group.get("summary", {})
|
||||
wehago_keys = summary.get("wehago_group_keys") or set()
|
||||
if len(wehago_keys) <= 1:
|
||||
continue
|
||||
rows.append(
|
||||
[
|
||||
"ERP 1 : WEHAGO N",
|
||||
summary.get("fiscal_year"),
|
||||
clean(summary.get("ledger_date")),
|
||||
clean(summary.get("proof_date")),
|
||||
clean(summary.get("voucher_no")),
|
||||
clean(summary.get("draft_no")),
|
||||
clean(summary.get("ledger_accounts")),
|
||||
clean(summary.get("voucher_accounts")),
|
||||
len(wehago_keys),
|
||||
len(group.get("rows", [])),
|
||||
clean(summary.get("review_reason")),
|
||||
" | ".join(sorted(clean(key) for key in wehago_keys if clean(key))),
|
||||
]
|
||||
)
|
||||
return rows
|
||||
|
||||
|
||||
def export_voucher_card_diff(start_year: int, end_year: int) -> Path:
|
||||
EXPORT_DIR.mkdir(parents=True, exist_ok=True)
|
||||
workbook = Workbook(write_only=True)
|
||||
|
||||
detail_headers = [
|
||||
"카드",
|
||||
"그룹순번",
|
||||
"행순번",
|
||||
"행상태",
|
||||
"연도",
|
||||
"부모 WEHAGO 일자",
|
||||
"부모 ERP 일자",
|
||||
"부모 전표번호",
|
||||
"부모 가전표번호",
|
||||
"부모 WEHAGO 계정",
|
||||
"부모 ERP 계정",
|
||||
"부모 WEHAGO 거래처",
|
||||
"부모 ERP 거래처",
|
||||
"부모 검증근거",
|
||||
"부모 그룹 행수",
|
||||
"연결된 반대 그룹 수",
|
||||
"행 WEHAGO 일자",
|
||||
"행 ERP 일자",
|
||||
"행 전표번호",
|
||||
"행 가전표번호",
|
||||
"행 WEHAGO 계정",
|
||||
"행 ERP 계정",
|
||||
"행 WEHAGO 거래처",
|
||||
"행 ERP 거래처",
|
||||
"행 WEHAGO 차변",
|
||||
"행 WEHAGO 대변",
|
||||
"행 ERP 차변",
|
||||
"행 ERP 대변",
|
||||
"행 WEHAGO 적요",
|
||||
"행 ERP 적요",
|
||||
]
|
||||
|
||||
all_wehago_match_rows: list[list[Any]] = []
|
||||
all_wehago_unmatched_rows: list[list[Any]] = []
|
||||
all_voucher_match_rows: list[list[Any]] = []
|
||||
all_voucher_unmatched_rows: list[list[Any]] = []
|
||||
diff_summary_rows: list[list[Any]] = []
|
||||
relation_rows_all: list[list[Any]] = []
|
||||
|
||||
for year in range(start_year, end_year + 1):
|
||||
rows_by_status = _get_cached_status_rows_by_range(engine, year, year)
|
||||
sections = _get_cached_voucher_sections_by_range(engine, year, year, rows_by_status)
|
||||
wehago_groups = sections.get("voucher_matched", [])
|
||||
voucher_groups = sections.get("erp_voucher_matched", [])
|
||||
wehago_unmatched_groups = sections.get("voucher_unmatched", [])
|
||||
wehago_issue_rows = iter_card_issue_rows(wehago_groups, "wehago")
|
||||
voucher_issue_rows = iter_card_issue_rows(voucher_groups, "voucher")
|
||||
relation_rows = build_relation_rows(wehago_groups, voucher_groups)
|
||||
wehago_multi = sum(1 for row in relation_rows if row[0] == "WEHAGO 1 : ERP N")
|
||||
voucher_multi = sum(1 for row in relation_rows if row[0] == "ERP 1 : WEHAGO N")
|
||||
all_wehago_match_rows.extend(flatten_groups(wehago_groups, "WEHAGO"))
|
||||
all_wehago_unmatched_rows.extend(flatten_groups(wehago_unmatched_groups, "WEHAGO Unmatched"))
|
||||
all_voucher_match_rows.extend(flatten_groups(voucher_groups, "Voucher"))
|
||||
all_voucher_unmatched_rows.extend(flatten_groups(voucher_groups, "Voucher", {"Unmatched", "Recheck"}))
|
||||
relation_rows_all.extend(relation_rows)
|
||||
diff_summary_rows.append(
|
||||
[
|
||||
"요약",
|
||||
year,
|
||||
len(wehago_groups),
|
||||
len(wehago_unmatched_groups),
|
||||
len(voucher_groups),
|
||||
len(voucher_issue_rows),
|
||||
len(wehago_groups) - len(voucher_groups),
|
||||
wehago_multi,
|
||||
voucher_multi,
|
||||
len(wehago_issue_rows),
|
||||
len(voucher_issue_rows),
|
||||
"WEHAGO 카드는 WEHAGO 전표 기준 그룹 수, Voucher 카드는 ERP 전표 기준 그룹 수입니다.",
|
||||
]
|
||||
)
|
||||
|
||||
append_rows_write_only(
|
||||
workbook,
|
||||
"1_WEHAGO_Match",
|
||||
detail_headers,
|
||||
all_wehago_match_rows or [["WEHAGO", "", "", "", "", "", "", "", "", "", "", "", "", "데이터가 없습니다.", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ""]],
|
||||
)
|
||||
append_rows_write_only(
|
||||
workbook,
|
||||
"2_WEHAGO_Unmatched",
|
||||
detail_headers,
|
||||
all_wehago_unmatched_rows or [["WEHAGO Unmatched", "", "", "", "", "", "", "", "", "", "", "", "", "데이터가 없습니다.", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ""]],
|
||||
)
|
||||
append_rows_write_only(
|
||||
workbook,
|
||||
"3_Voucher_Match",
|
||||
detail_headers,
|
||||
all_voucher_match_rows or [["Voucher", "", "", "", "", "", "", "", "", "", "", "", "", "데이터가 없습니다.", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ""]],
|
||||
)
|
||||
append_rows_write_only(
|
||||
workbook,
|
||||
"4_Voucher_Unmatched",
|
||||
detail_headers,
|
||||
all_voucher_unmatched_rows or [["Voucher", "", "", "", "", "", "", "", "", "", "", "", "", "Voucher 카드 안에서 Unmatched/Recheck로 남은 행이 없습니다.", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ""]],
|
||||
)
|
||||
|
||||
diff_headers = [
|
||||
"구분",
|
||||
"연도",
|
||||
"WEHAGO 매치 그룹 수",
|
||||
"WEHAGO Unmatched 그룹 수",
|
||||
"Voucher 매치 그룹 수",
|
||||
"Voucher unmatched 행 수",
|
||||
"WEHAGO-Voucher 그룹 수 차이",
|
||||
"WEHAGO 1:ERP N 건수",
|
||||
"ERP 1:WEHAGO N 건수",
|
||||
"WEHAGO 카드 내 ERP Unmatched/Recheck 행 수",
|
||||
"Voucher 카드 내 Unmatched/Recheck 행 수",
|
||||
"설명",
|
||||
]
|
||||
diff_rows: list[list[Any]] = []
|
||||
diff_rows.append(diff_headers)
|
||||
diff_rows.extend(diff_summary_rows)
|
||||
if diff_summary_rows:
|
||||
total_wehago = sum(int(row[2]) for row in diff_summary_rows)
|
||||
total_wehago_unmatched = sum(int(row[3]) for row in diff_summary_rows)
|
||||
total_voucher = sum(int(row[4]) for row in diff_summary_rows)
|
||||
total_voucher_unmatched = sum(int(row[5]) for row in diff_summary_rows)
|
||||
total_diff = sum(int(row[6]) for row in diff_summary_rows)
|
||||
total_wehago_multi = sum(int(row[7]) for row in diff_summary_rows)
|
||||
total_voucher_multi = sum(int(row[8]) for row in diff_summary_rows)
|
||||
total_wehago_issues = sum(int(row[9]) for row in diff_summary_rows)
|
||||
total_voucher_issues = sum(int(row[10]) for row in diff_summary_rows)
|
||||
diff_rows.append(
|
||||
[
|
||||
"전체",
|
||||
f"{start_year}~{end_year}",
|
||||
total_wehago,
|
||||
total_wehago_unmatched,
|
||||
total_voucher,
|
||||
total_voucher_unmatched,
|
||||
total_diff,
|
||||
total_wehago_multi,
|
||||
total_voucher_multi,
|
||||
total_wehago_issues,
|
||||
total_voucher_issues,
|
||||
"그룹 수 차이는 한쪽 전표 1건이 반대쪽 여러 전표와 연결되는 경우에서 주로 발생합니다.",
|
||||
]
|
||||
)
|
||||
diff_rows.append([])
|
||||
diff_rows.append(["관계 유형", "연도", "WEHAGO 일자", "ERP 일자", "전표번호", "가전표번호", "WEHAGO 계정", "ERP 계정", "연결된 반대 그룹 수", "그룹 행 수", "검증 근거", "연결된 그룹 키"])
|
||||
diff_rows.extend(relation_rows_all or [["", "", "", "", "", "", "", "", "", "", "수 차이를 만든 다대일/일대다 사례가 없습니다.", ""]])
|
||||
diff_ws = workbook.create_sheet("5_Count_Diff")
|
||||
for index, row in enumerate(diff_rows, start=1):
|
||||
if index in {1, len(diff_summary_rows) + 3}:
|
||||
cells: list[WriteOnlyCell] = []
|
||||
for value in row:
|
||||
cell = WriteOnlyCell(diff_ws, value=value)
|
||||
cell.fill = HEADER_FILL
|
||||
cell.font = HEADER_FONT
|
||||
cell.alignment = Alignment(horizontal="center", vertical="center")
|
||||
cells.append(cell)
|
||||
diff_ws.append(cells)
|
||||
else:
|
||||
diff_ws.append(row)
|
||||
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
output_path = EXPORT_DIR / f"voucher_card_diff_{start_year}_{end_year}_{timestamp}.xlsx"
|
||||
workbook.save(output_path)
|
||||
return output_path
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
years = discover_available_years()
|
||||
default_start = min(years) if years else datetime.now().year
|
||||
default_end = max(years) if years else default_start
|
||||
parser = argparse.ArgumentParser(description="WEHAGO/Voucher 카드 차이 분석 엑셀 생성")
|
||||
parser.add_argument("--start-year", type=int, default=default_start)
|
||||
parser.add_argument("--end-year", type=int, default=default_end)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def main() -> None:
|
||||
args = parse_args()
|
||||
start_year = min(args.start_year, args.end_year)
|
||||
end_year = max(args.start_year, args.end_year)
|
||||
output_path = export_voucher_card_diff(start_year, end_year)
|
||||
print(output_path)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,316 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
from collections import Counter, defaultdict, deque
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from openpyxl import Workbook
|
||||
from openpyxl.cell import WriteOnlyCell
|
||||
from openpyxl.styles import Alignment, Font, PatternFill
|
||||
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
if str(BASE_DIR) not in sys.path:
|
||||
sys.path.insert(0, str(BASE_DIR))
|
||||
|
||||
from main import engine
|
||||
from wehago_compare import _get_cached_status_rows_by_range, _get_cached_voucher_sections_by_range
|
||||
|
||||
EXPORT_DIR = BASE_DIR / "static" / "exports"
|
||||
HEADER_FILL = PatternFill(fill_type="solid", fgColor="111827")
|
||||
HEADER_FONT = Font(color="FFFFFF", bold=True)
|
||||
POS_FILL = PatternFill(fill_type="solid", fgColor="DCFCE7")
|
||||
NEG_FILL = PatternFill(fill_type="solid", fgColor="DBEAFE")
|
||||
|
||||
|
||||
def clean(value: Any) -> str:
|
||||
return "" if value is None else str(value).strip()
|
||||
|
||||
|
||||
def amount(value: Any) -> float:
|
||||
text = clean(value).replace(",", "")
|
||||
if not text:
|
||||
return 0.0
|
||||
try:
|
||||
return float(text)
|
||||
except ValueError:
|
||||
return 0.0
|
||||
|
||||
|
||||
def write_row(ws, values: list[Any], fill: PatternFill | None = None, header: bool = False) -> None:
|
||||
row = []
|
||||
for value in values:
|
||||
cell = WriteOnlyCell(ws, value=value)
|
||||
cell.alignment = Alignment(vertical="top", wrap_text=True)
|
||||
if header:
|
||||
cell.fill = HEADER_FILL
|
||||
cell.font = HEADER_FONT
|
||||
cell.alignment = Alignment(horizontal="center", vertical="center")
|
||||
elif fill:
|
||||
cell.fill = fill
|
||||
row.append(cell)
|
||||
ws.append(row)
|
||||
|
||||
|
||||
def summarize_group(summary: dict[str, Any], side: str) -> dict[str, Any]:
|
||||
if side == "w":
|
||||
return {
|
||||
"date": clean(summary.get("ledger_date")),
|
||||
"voucher_no": clean(summary.get("voucher_no")),
|
||||
"draft_no": clean(summary.get("draft_no")),
|
||||
"accounts": clean(summary.get("ledger_accounts")),
|
||||
"vendors": clean(summary.get("ledger_vendors")),
|
||||
"debit": amount(summary.get("ledger_debit")),
|
||||
"credit": amount(summary.get("ledger_credit")),
|
||||
"links": len(summary.get("erp_group_keys") or []),
|
||||
"reason": clean(summary.get("review_reason")),
|
||||
}
|
||||
return {
|
||||
"date": clean(summary.get("proof_date")) or clean(summary.get("ledger_date")),
|
||||
"voucher_no": clean(summary.get("voucher_no")),
|
||||
"draft_no": clean(summary.get("draft_no")),
|
||||
"accounts": clean(summary.get("voucher_accounts")),
|
||||
"vendors": clean(summary.get("voucher_vendors")),
|
||||
"debit": amount(summary.get("voucher_debit")),
|
||||
"credit": amount(summary.get("voucher_credit")),
|
||||
"links": len(summary.get("wehago_group_keys") or []),
|
||||
"reason": clean(summary.get("review_reason")),
|
||||
}
|
||||
|
||||
|
||||
def build_gap_report(year: int) -> Path:
|
||||
rows = _get_cached_status_rows_by_range(engine, year, year)
|
||||
sections = _get_cached_voucher_sections_by_range(engine, year, year, rows)
|
||||
wehago_groups = sections["voucher_matched"]
|
||||
voucher_groups = sections["erp_voucher_matched"]
|
||||
|
||||
wehago_by_key: dict[Any, dict[str, Any]] = {}
|
||||
voucher_by_key: dict[Any, dict[str, Any]] = {}
|
||||
adj: dict[tuple[str, Any], set[tuple[str, Any]]] = defaultdict(set)
|
||||
|
||||
for group in wehago_groups:
|
||||
summary = group["summary"]
|
||||
w_key = summary.get("wehago_group_key")
|
||||
wehago_by_key[w_key] = group
|
||||
for e_key in summary.get("erp_group_keys") or set():
|
||||
adj[("w", w_key)].add(("e", e_key))
|
||||
adj[("e", e_key)].add(("w", w_key))
|
||||
for group in voucher_groups:
|
||||
summary = group["summary"]
|
||||
e_key = summary.get("erp_group_key")
|
||||
voucher_by_key[e_key] = group
|
||||
for w_key in summary.get("wehago_group_keys") or set():
|
||||
adj[("e", e_key)].add(("w", w_key))
|
||||
adj[("w", w_key)].add(("e", e_key))
|
||||
|
||||
components: list[dict[str, Any]] = []
|
||||
visited: set[tuple[str, Any]] = set()
|
||||
for node in list(adj):
|
||||
if node in visited:
|
||||
continue
|
||||
q = deque([node])
|
||||
visited.add(node)
|
||||
w_keys: list[Any] = []
|
||||
e_keys: list[Any] = []
|
||||
while q:
|
||||
side, key = q.popleft()
|
||||
if side == "w":
|
||||
w_keys.append(key)
|
||||
else:
|
||||
e_keys.append(key)
|
||||
for nxt in adj[node if False else (side, key)]:
|
||||
if nxt not in visited:
|
||||
visited.add(nxt)
|
||||
q.append(nxt)
|
||||
components.append({"w_keys": w_keys, "e_keys": e_keys})
|
||||
|
||||
shape_counter = Counter((len(item["w_keys"]), len(item["e_keys"])) for item in components)
|
||||
gap_components = [item for item in components if len(item["w_keys"]) != len(item["e_keys"])]
|
||||
gap_components.sort(key=lambda item: (abs(len(item["w_keys"]) - len(item["e_keys"])) * -1, len(item["w_keys"]) * -1, len(item["e_keys"]) * -1))
|
||||
|
||||
EXPORT_DIR.mkdir(parents=True, exist_ok=True)
|
||||
wb = Workbook(write_only=True)
|
||||
|
||||
ws_summary = wb.create_sheet("summary")
|
||||
write_row(ws_summary, ["항목", "값", "설명"], header=True)
|
||||
write_row(ws_summary, ["연도", year, "분석 기준 연도"])
|
||||
write_row(ws_summary, ["WEHAGO 매치 그룹 수", len(wehago_groups), "WEHAGO 카드에 표시되는 매치 전표 그룹 수"])
|
||||
write_row(ws_summary, ["Voucher 매치 그룹 수", len(voucher_groups), "Voucher 카드에 표시되는 매치 전표 그룹 수"])
|
||||
write_row(ws_summary, ["그룹 수 차이", len(wehago_groups) - len(voucher_groups), "WEHAGO 그룹 수 - Voucher 그룹 수"])
|
||||
write_row(ws_summary, ["전체 연결 성분 수", len(components), "WEHAGO/ERP 매치 관계를 그래프로 묶은 연결 단위 수"])
|
||||
write_row(ws_summary, ["1:1 성분 수", shape_counter.get((1, 1), 0), "양쪽 그룹 수가 같은 일반적인 경우"])
|
||||
write_row(ws_summary, ["차이 발생 성분 수", len(gap_components), "WEHAGO 그룹 수와 Voucher 그룹 수가 다른 연결 성분 수"])
|
||||
write_row(ws_summary, ["WEHAGO 쪽 초과 합계", sum(max(len(item['w_keys']) - len(item['e_keys']), 0) for item in gap_components), "WEHAGO 그룹 수가 더 많은 만큼의 합계"])
|
||||
write_row(ws_summary, ["Voucher 쪽 초과 합계", sum(max(len(item['e_keys']) - len(item['w_keys']), 0) for item in gap_components), "Voucher 그룹 수가 더 많은 만큼의 합계"])
|
||||
write_row(ws_summary, [])
|
||||
write_row(ws_summary, ["형태", "건수", "설명"], header=True)
|
||||
for (w_count, e_count), count in shape_counter.most_common(20):
|
||||
desc = "정상 1:1" if (w_count, e_count) == (1, 1) else ("WEHAGO가 더 많이 쪼개짐" if w_count > e_count else "Voucher가 더 많이 쪼개짐")
|
||||
write_row(ws_summary, [f"{w_count}:{e_count}", count, desc])
|
||||
|
||||
ws_gap = wb.create_sheet("gap_components")
|
||||
write_row(
|
||||
ws_gap,
|
||||
[
|
||||
"component_id",
|
||||
"형태",
|
||||
"WEHAGO 그룹 수",
|
||||
"Voucher 그룹 수",
|
||||
"차이(WEHAGO-Voucher)",
|
||||
"WEHAGO 대표 전표",
|
||||
"Voucher 대표 전표",
|
||||
"WEHAGO 전표 목록",
|
||||
"Voucher 전표 목록",
|
||||
"WEHAGO 계정 요약",
|
||||
"Voucher 계정 요약",
|
||||
],
|
||||
header=True,
|
||||
)
|
||||
for idx, item in enumerate(gap_components, start=1):
|
||||
w_summaries = [summarize_group(wehago_by_key[key]["summary"], "w") for key in item["w_keys"] if key in wehago_by_key]
|
||||
e_summaries = [summarize_group(voucher_by_key[key]["summary"], "e") for key in item["e_keys"] if key in voucher_by_key]
|
||||
fill = POS_FILL if len(item["w_keys"]) > len(item["e_keys"]) else NEG_FILL
|
||||
write_row(
|
||||
ws_gap,
|
||||
[
|
||||
idx,
|
||||
f"{len(item['w_keys'])}:{len(item['e_keys'])}",
|
||||
len(item["w_keys"]),
|
||||
len(item["e_keys"]),
|
||||
len(item["w_keys"]) - len(item["e_keys"]),
|
||||
" | ".join(f"{row['date']} {row['voucher_no']}" for row in w_summaries[:3]),
|
||||
" | ".join(f"{row['date']} {row['voucher_no']}/{row['draft_no']}" for row in e_summaries[:3]),
|
||||
"\n".join(f"{row['date']} / {row['voucher_no']} / {row['accounts']}" for row in w_summaries),
|
||||
"\n".join(f"{row['date']} / {row['voucher_no']} / {row['draft_no']} / {row['accounts']}" for row in e_summaries),
|
||||
"\n".join(row["accounts"] for row in w_summaries),
|
||||
"\n".join(row["accounts"] for row in e_summaries),
|
||||
],
|
||||
fill=fill,
|
||||
)
|
||||
|
||||
ws_wehago = wb.create_sheet("wehago_extra_groups")
|
||||
write_row(
|
||||
ws_wehago,
|
||||
[
|
||||
"component_id",
|
||||
"형태",
|
||||
"WEHAGO 일자",
|
||||
"WEHAGO 전표번호",
|
||||
"WEHAGO 계정",
|
||||
"WEHAGO 거래처",
|
||||
"WEHAGO 차변",
|
||||
"WEHAGO 대변",
|
||||
"연결 ERP 그룹 수",
|
||||
"연결 ERP 전표 목록",
|
||||
"검증 근거",
|
||||
],
|
||||
header=True,
|
||||
)
|
||||
for idx, item in enumerate(gap_components, start=1):
|
||||
if len(item["w_keys"]) <= len(item["e_keys"]):
|
||||
continue
|
||||
linked_erp = [summarize_group(voucher_by_key[key]["summary"], "e") for key in item["e_keys"] if key in voucher_by_key]
|
||||
linked_erp_text = "\n".join(f"{row['date']} / {row['voucher_no']} / {row['draft_no']}" for row in linked_erp)
|
||||
for w_key in item["w_keys"]:
|
||||
group = wehago_by_key.get(w_key)
|
||||
if not group:
|
||||
continue
|
||||
s = summarize_group(group["summary"], "w")
|
||||
write_row(
|
||||
ws_wehago,
|
||||
[
|
||||
idx,
|
||||
f"{len(item['w_keys'])}:{len(item['e_keys'])}",
|
||||
s["date"],
|
||||
s["voucher_no"],
|
||||
s["accounts"],
|
||||
s["vendors"],
|
||||
s["debit"],
|
||||
s["credit"],
|
||||
s["links"],
|
||||
linked_erp_text,
|
||||
s["reason"],
|
||||
],
|
||||
fill=POS_FILL,
|
||||
)
|
||||
|
||||
ws_voucher = wb.create_sheet("voucher_extra_groups")
|
||||
write_row(
|
||||
ws_voucher,
|
||||
[
|
||||
"component_id",
|
||||
"형태",
|
||||
"ERP 일자",
|
||||
"ERP 전표번호",
|
||||
"ERP 가전표번호",
|
||||
"ERP 계정",
|
||||
"ERP 거래처",
|
||||
"ERP 차변",
|
||||
"ERP 대변",
|
||||
"연결 WEHAGO 그룹 수",
|
||||
"연결 WEHAGO 전표 목록",
|
||||
"검증 근거",
|
||||
],
|
||||
header=True,
|
||||
)
|
||||
for idx, item in enumerate(gap_components, start=1):
|
||||
if len(item["e_keys"]) <= len(item["w_keys"]):
|
||||
continue
|
||||
linked_wehago = [summarize_group(wehago_by_key[key]["summary"], "w") for key in item["w_keys"] if key in wehago_by_key]
|
||||
linked_wehago_text = "\n".join(f"{row['date']} / {row['voucher_no']}" for row in linked_wehago)
|
||||
for e_key in item["e_keys"]:
|
||||
group = voucher_by_key.get(e_key)
|
||||
if not group:
|
||||
continue
|
||||
s = summarize_group(group["summary"], "e")
|
||||
write_row(
|
||||
ws_voucher,
|
||||
[
|
||||
idx,
|
||||
f"{len(item['w_keys'])}:{len(item['e_keys'])}",
|
||||
s["date"],
|
||||
s["voucher_no"],
|
||||
s["draft_no"],
|
||||
s["accounts"],
|
||||
s["vendors"],
|
||||
s["debit"],
|
||||
s["credit"],
|
||||
s["links"],
|
||||
linked_wehago_text,
|
||||
s["reason"],
|
||||
],
|
||||
fill=NEG_FILL,
|
||||
)
|
||||
|
||||
ws_examples = wb.create_sheet("top_patterns")
|
||||
write_row(ws_examples, ["형태", "건수", "설명"], header=True)
|
||||
for (w_count, e_count), count in shape_counter.most_common(50):
|
||||
if (w_count, e_count) == (1, 1):
|
||||
continue
|
||||
write_row(
|
||||
ws_examples,
|
||||
[
|
||||
f"{w_count}:{e_count}",
|
||||
count,
|
||||
"WEHAGO 전표 그룹 수가 더 많음" if w_count > e_count else "Voucher 전표 그룹 수가 더 많음",
|
||||
],
|
||||
fill=POS_FILL if w_count > e_count else NEG_FILL,
|
||||
)
|
||||
|
||||
output = EXPORT_DIR / f"voucher_count_gap_{year}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx"
|
||||
wb.save(output)
|
||||
return output
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description="WEHAGO/Voucher 매치 그룹 수 차이 분석 엑셀 생성")
|
||||
parser.add_argument("--year", type=int, default=2025)
|
||||
args = parser.parse_args()
|
||||
output = build_gap_report(args.year)
|
||||
print(output)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,281 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from openpyxl import load_workbook
|
||||
from sqlalchemy import bindparam, create_engine, text
|
||||
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
if str(BASE_DIR) not in sys.path:
|
||||
sys.path.insert(0, str(BASE_DIR))
|
||||
|
||||
from wehago_compare import (
|
||||
WEHAGO_SOURCE_ROOT,
|
||||
_fallback_metric_counts_from_db,
|
||||
compute_file_hash,
|
||||
detect_file_kind,
|
||||
import_ledger_rows,
|
||||
import_voucher_rows,
|
||||
infer_year_hint,
|
||||
init_wehago_compare_db,
|
||||
rebuild_comparison_results,
|
||||
upsert_source_file,
|
||||
)
|
||||
|
||||
|
||||
DB_PATH = BASE_DIR / "data.db"
|
||||
ENGINE = create_engine(f"sqlite:///{DB_PATH}", connect_args={"check_same_thread": False})
|
||||
|
||||
LEDGER_FILES = {
|
||||
2022: WEHAGO_SOURCE_ROOT / "data_download/2022/ledger_2022_20220101_20221231_260507.xlsx",
|
||||
2023: WEHAGO_SOURCE_ROOT / "data_download/wehago_account_ledger_20260506_2023_compare/ledger_2023_20230101_20231231_260506.xlsx",
|
||||
2024: WEHAGO_SOURCE_ROOT / "data_download/wehago_account_ledger_20260506_2024_compare/ledger_2024_20240101_20241231_260504.xlsx",
|
||||
2025: WEHAGO_SOURCE_ROOT / "data_download/wehago_account_ledger_20260423/ledger_2025_20250101_20251231_260423.xlsx",
|
||||
}
|
||||
VOUCHER_FILES = [
|
||||
{
|
||||
"path": WEHAGO_SOURCE_ROOT / "voucher_sort_20220101_20221231_260507.xlsx",
|
||||
"expected_year": 2022,
|
||||
"year_hint": 2022,
|
||||
"allowed_years": {2022},
|
||||
},
|
||||
{
|
||||
"path": WEHAGO_SOURCE_ROOT / "voucher_sort_20230101_20251231_260424.xlsx",
|
||||
"expected_year": 2023,
|
||||
"year_hint": None,
|
||||
"allowed_years": {2023, 2024},
|
||||
},
|
||||
{
|
||||
"path": WEHAGO_SOURCE_ROOT / "data_download/wehago_account_ledger_20260423/voucher_2025_20250101_20251231_260422.xlsx",
|
||||
"expected_year": 2025,
|
||||
"year_hint": 2025,
|
||||
"allowed_years": {2025},
|
||||
},
|
||||
]
|
||||
_DEFAULT_YEAR_HINT = object()
|
||||
|
||||
|
||||
def _load_sheet(path: Path):
|
||||
workbook = load_workbook(path, read_only=True, data_only=True)
|
||||
return workbook, workbook.worksheets[0]
|
||||
|
||||
|
||||
def _delete_year_data(conn, *, ledger_years: set[int], voucher_years: set[int]) -> dict[str, int]:
|
||||
ledger_deleted = conn.execute(
|
||||
text("DELETE FROM wehago_ledger_rows WHERE fiscal_year IN :years").bindparams(bindparam("years", expanding=True)),
|
||||
{"years": sorted(ledger_years)},
|
||||
).rowcount
|
||||
voucher_deleted = conn.execute(
|
||||
text("DELETE FROM wehago_voucher_rows WHERE fiscal_year IN :years").bindparams(bindparam("years", expanding=True)),
|
||||
{"years": sorted(voucher_years)},
|
||||
).rowcount
|
||||
|
||||
unused_source_ids = {
|
||||
int(row[0])
|
||||
for row in conn.execute(
|
||||
text(
|
||||
"""
|
||||
SELECT sf.id
|
||||
FROM wehago_source_files sf
|
||||
LEFT JOIN wehago_ledger_rows l ON l.source_file_id = sf.id
|
||||
LEFT JOIN wehago_voucher_rows v ON v.source_file_id = sf.id
|
||||
GROUP BY sf.id
|
||||
HAVING COUNT(l.id) = 0 AND COUNT(v.id) = 0
|
||||
"""
|
||||
)
|
||||
)
|
||||
}
|
||||
if unused_source_ids:
|
||||
conn.execute(
|
||||
text("DELETE FROM wehago_source_files WHERE id IN :ids").bindparams(bindparam("ids", expanding=True)),
|
||||
{"ids": sorted(unused_source_ids)},
|
||||
)
|
||||
conn.execute(
|
||||
text("DELETE FROM wehago_comparison_results WHERE fiscal_year IN :years").bindparams(bindparam("years", expanding=True)),
|
||||
{"years": sorted(ledger_years | voucher_years)},
|
||||
)
|
||||
conn.execute(text("DELETE FROM wehago_metric_count_cache"))
|
||||
conn.execute(text("DELETE FROM wehago_result_row_cache"))
|
||||
conn.execute(text("DELETE FROM wehago_pair_recommend_cache"))
|
||||
conn.execute(text("DELETE FROM wehago_background_jobs WHERE job_type = 'pair_recommend_precompute'"))
|
||||
return {"ledger_deleted": int(ledger_deleted or 0), "voucher_deleted": int(voucher_deleted or 0), "source_deleted": len(unused_source_ids)}
|
||||
|
||||
|
||||
def _import_one(
|
||||
conn,
|
||||
path: Path,
|
||||
expected_kind: str,
|
||||
expected_year: int,
|
||||
*,
|
||||
allowed_years: set[int] | None = None,
|
||||
year_hint_override: int | None | object = _DEFAULT_YEAR_HINT,
|
||||
) -> dict[str, object]:
|
||||
file_kind, header, sheet_name = detect_file_kind(path)
|
||||
if file_kind != expected_kind:
|
||||
raise ValueError(f"{path} 형식이 {expected_kind}가 아닙니다: {file_kind}")
|
||||
|
||||
workbook, sheet = _load_sheet(path)
|
||||
try:
|
||||
sample_rows = list(sheet.iter_rows(min_row=2, max_row=51, values_only=True))
|
||||
if year_hint_override is _DEFAULT_YEAR_HINT:
|
||||
year_hint = infer_year_hint(path, file_kind, sample_rows) or expected_year
|
||||
else:
|
||||
year_hint = year_hint_override
|
||||
source_id, _changed = upsert_source_file(conn, path, file_kind, year_hint, sheet_name, header)
|
||||
if file_kind == "ledger":
|
||||
conn.execute(text("DELETE FROM wehago_ledger_rows WHERE source_file_id = :source_id"), {"source_id": source_id})
|
||||
inserted = import_ledger_rows(conn, source_id, sheet_name, sheet.iter_rows(min_row=2, values_only=True), year_hint)
|
||||
else:
|
||||
conn.execute(text("DELETE FROM wehago_voucher_rows WHERE source_file_id = :source_id"), {"source_id": source_id})
|
||||
inserted = import_voucher_rows(conn, source_id, sheet_name, sheet.iter_rows(min_row=2, values_only=True), year_hint)
|
||||
allowed = sorted(allowed_years or {expected_year})
|
||||
if allowed:
|
||||
conn.execute(
|
||||
text(
|
||||
"""
|
||||
DELETE FROM wehago_ledger_rows
|
||||
WHERE source_file_id = :source_id
|
||||
AND fiscal_year NOT IN :allowed_years
|
||||
"""
|
||||
).bindparams(bindparam("allowed_years", expanding=True)),
|
||||
{"source_id": source_id, "allowed_years": allowed},
|
||||
)
|
||||
conn.execute(
|
||||
text(
|
||||
"""
|
||||
DELETE FROM wehago_voucher_rows
|
||||
WHERE source_file_id = :source_id
|
||||
AND fiscal_year NOT IN :allowed_years
|
||||
"""
|
||||
).bindparams(bindparam("allowed_years", expanding=True)),
|
||||
{"source_id": source_id, "allowed_years": allowed},
|
||||
)
|
||||
inserted = int(
|
||||
conn.execute(
|
||||
text(f"SELECT COUNT(*) FROM wehago_{file_kind}_rows WHERE source_file_id = :source_id"),
|
||||
{"source_id": source_id},
|
||||
).scalar_one()
|
||||
or 0
|
||||
)
|
||||
conn.execute(
|
||||
text("UPDATE wehago_source_files SET row_count = :row_count, imported_at = CURRENT_TIMESTAMP WHERE id = :source_id"),
|
||||
{"row_count": inserted, "source_id": source_id},
|
||||
)
|
||||
return {
|
||||
"path": str(path),
|
||||
"kind": file_kind,
|
||||
"year_hint": year_hint,
|
||||
"allowed_years": allowed,
|
||||
"source_id": source_id,
|
||||
"inserted": inserted,
|
||||
"file_hash": compute_file_hash(path),
|
||||
}
|
||||
finally:
|
||||
workbook.close()
|
||||
|
||||
|
||||
def _fetch_counts(conn) -> dict[str, object]:
|
||||
counts: dict[str, object] = {}
|
||||
for table in ("wehago_ledger_rows", "wehago_voucher_rows", "wehago_comparison_results"):
|
||||
rows = conn.execute(
|
||||
text(f"SELECT fiscal_year, COUNT(*) AS row_count FROM {table} GROUP BY fiscal_year ORDER BY fiscal_year")
|
||||
).mappings()
|
||||
counts[table] = {str(row["fiscal_year"]): int(row["row_count"]) for row in rows}
|
||||
return counts
|
||||
|
||||
|
||||
def _fetch_quality(conn) -> dict[str, object]:
|
||||
total_rows = conn.execute(
|
||||
text(
|
||||
"""
|
||||
SELECT COUNT(*)
|
||||
FROM wehago_ledger_rows
|
||||
WHERE REPLACE(COALESCE(description, ''), ' ', '') IN ('[월계]', '[누계]', '월계', '누계')
|
||||
OR REPLACE(COALESCE(ledger_date, ''), ' ', '') IN ('[월계]', '[누계]', '월계', '누계')
|
||||
"""
|
||||
)
|
||||
).scalar_one()
|
||||
blank_account_rows = conn.execute(
|
||||
text(
|
||||
"""
|
||||
SELECT COUNT(*)
|
||||
FROM wehago_ledger_rows
|
||||
WHERE fiscal_year IN (2022, 2023, 2024)
|
||||
AND (COALESCE(account_code, '') = '' OR COALESCE(account_name, '') = '')
|
||||
"""
|
||||
)
|
||||
).scalar_one()
|
||||
account_names = conn.execute(
|
||||
text(
|
||||
"""
|
||||
SELECT fiscal_year, account_code, COUNT(DISTINCT account_name) AS name_count,
|
||||
GROUP_CONCAT(DISTINCT account_name) AS names
|
||||
FROM wehago_ledger_rows
|
||||
WHERE fiscal_year IN (2022, 2023, 2024)
|
||||
AND COALESCE(account_code, '') <> ''
|
||||
GROUP BY fiscal_year, account_code
|
||||
HAVING COUNT(DISTINCT account_name) > 1
|
||||
ORDER BY fiscal_year, account_code
|
||||
LIMIT 20
|
||||
"""
|
||||
)
|
||||
).mappings()
|
||||
voucher_counts_by_source = conn.execute(
|
||||
text(
|
||||
"""
|
||||
SELECT sf.file_name, v.fiscal_year, COUNT(*) AS row_count
|
||||
FROM wehago_voucher_rows v
|
||||
JOIN wehago_source_files sf ON sf.id = v.source_file_id
|
||||
WHERE v.fiscal_year IN (2022, 2023, 2024, 2025)
|
||||
GROUP BY sf.file_name, v.fiscal_year
|
||||
ORDER BY v.fiscal_year, sf.file_name
|
||||
"""
|
||||
)
|
||||
).mappings()
|
||||
return {
|
||||
"monthly_cumulative_rows": int(total_rows or 0),
|
||||
"blank_account_rows_2022_2024": int(blank_account_rows or 0),
|
||||
"multi_name_accounts": [dict(row) for row in account_names],
|
||||
"voucher_counts_by_source": [dict(row) for row in voucher_counts_by_source],
|
||||
}
|
||||
|
||||
|
||||
def main() -> None:
|
||||
voucher_paths = [item["path"] for item in VOUCHER_FILES]
|
||||
missing = [path for path in [*LEDGER_FILES.values(), *voucher_paths] if not path.exists()]
|
||||
if missing:
|
||||
raise FileNotFoundError("\\n".join(str(path) for path in missing))
|
||||
|
||||
init_wehago_compare_db(ENGINE)
|
||||
summary: dict[str, object] = {"deleted": {}, "imported": []}
|
||||
with ENGINE.begin() as conn:
|
||||
summary["deleted"] = _delete_year_data(conn, ledger_years={2022, 2023, 2024, 2025}, voucher_years={2022, 2023, 2024, 2025})
|
||||
for year, path in LEDGER_FILES.items():
|
||||
summary["imported"].append(_import_one(conn, path, "ledger", year, allowed_years={year}))
|
||||
for item in VOUCHER_FILES:
|
||||
summary["imported"].append(
|
||||
_import_one(
|
||||
conn,
|
||||
item["path"],
|
||||
"voucher",
|
||||
int(item["expected_year"]),
|
||||
allowed_years=set(item["allowed_years"]),
|
||||
year_hint_override=item["year_hint"],
|
||||
)
|
||||
)
|
||||
rebuild_comparison_results(conn)
|
||||
summary["dashboard_counts"] = {
|
||||
str(year): _fallback_metric_counts_from_db(conn, year, year)
|
||||
for year in range(2022, 2026)
|
||||
}
|
||||
summary["counts"] = _fetch_counts(conn)
|
||||
summary["quality"] = _fetch_quality(conn)
|
||||
|
||||
print(json.dumps(summary, ensure_ascii=False, indent=2))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,4 @@
|
||||
@echo off
|
||||
set "WEHAGO_CHROME_DEBUGGER_ADDRESS=127.0.0.1:9225"
|
||||
cd /d C:\Windows
|
||||
python -u "\\wsl.localhost\Ubuntu\home\b17301\WEHAGO_DB\find_realgrid_registry.py"
|
||||
@@ -0,0 +1,4 @@
|
||||
@echo off
|
||||
set "WEHAGO_CHROME_DEBUGGER_ADDRESS=127.0.0.1:9225"
|
||||
cd /d C:\Windows
|
||||
python -u "\\wsl.localhost\Ubuntu\home\b17301\my-intranet-app\scripts\inspect_realgrid_views_9225.py"
|
||||
@@ -0,0 +1,49 @@
|
||||
import json
|
||||
import os
|
||||
|
||||
from selenium import webdriver
|
||||
from selenium.webdriver.chrome.options import Options
|
||||
|
||||
|
||||
options = Options()
|
||||
options.add_experimental_option("debuggerAddress", os.environ.get("WEHAGO_CHROME_DEBUGGER_ADDRESS", "127.0.0.1:9225"))
|
||||
driver = webdriver.Chrome(options=options)
|
||||
driver.set_script_timeout(10)
|
||||
|
||||
script = r"""
|
||||
const ids = Array.from(document.querySelectorAll('.rg-body')).map((el) => ({
|
||||
view: String(el.$_view || ''),
|
||||
text: (el.textContent || '').slice(0, 200),
|
||||
rect: (() => { const r = el.getBoundingClientRect(); return {left:r.left, top:r.top, width:r.width, height:r.height}; })()
|
||||
}));
|
||||
const summarize = (name, value) => {
|
||||
const out = {name, type: typeof value, text: String(value).slice(0, 300)};
|
||||
if (value && typeof value === 'object') {
|
||||
let keys = [];
|
||||
try { keys = Object.getOwnPropertyNames(value).concat(Object.keys(value)); } catch(e) {}
|
||||
out.keys = Array.from(new Set(keys)).slice(0, 250);
|
||||
out.samples = out.keys.slice(0, 80).map((key) => {
|
||||
try {
|
||||
const v = value[key];
|
||||
return {key, type: typeof v, text: String(v).slice(0, 160)};
|
||||
} catch (e) {
|
||||
return {key, error: String(e)};
|
||||
}
|
||||
});
|
||||
}
|
||||
return out;
|
||||
};
|
||||
const out = {ids, globals: []};
|
||||
const names = ['Grids', 'RealGridJS', 'RealGrid', 'realgrid'];
|
||||
for (const name of names) out.globals.push(summarize(name, window[name]));
|
||||
for (const key of Object.keys(window)) {
|
||||
if (/grid|real|view|provider/i.test(key)) {
|
||||
try { out.globals.push(summarize('window.' + key, window[key])); } catch(e) {}
|
||||
}
|
||||
if (out.globals.length > 80) break;
|
||||
}
|
||||
return out;
|
||||
"""
|
||||
|
||||
print(json.dumps(driver.execute_script(script), ensure_ascii=False, indent=2)[:100000])
|
||||
driver.quit()
|
||||
@@ -0,0 +1,4 @@
|
||||
@echo off
|
||||
set "WEHAGO_CHROME_DEBUGGER_ADDRESS=127.0.0.1:9225"
|
||||
cd /d C:\Windows
|
||||
python -u "\\wsl.localhost\Ubuntu\home\b17301\my-intranet-app\scripts\inspect_rg_body_props_9225.py"
|
||||
@@ -0,0 +1,43 @@
|
||||
import json
|
||||
import os
|
||||
|
||||
from selenium import webdriver
|
||||
from selenium.webdriver.chrome.options import Options
|
||||
|
||||
|
||||
options = Options()
|
||||
options.add_experimental_option("debuggerAddress", os.environ.get("WEHAGO_CHROME_DEBUGGER_ADDRESS", "127.0.0.1:9225"))
|
||||
driver = webdriver.Chrome(options=options)
|
||||
driver.set_script_timeout(10)
|
||||
|
||||
script = r"""
|
||||
const out = [];
|
||||
for (const body of document.querySelectorAll('.rg-body')) {
|
||||
const item = {view: String(body.$_view || ''), props: []};
|
||||
const keys = [];
|
||||
let node = body;
|
||||
while (node) {
|
||||
try { keys.push(...Object.getOwnPropertyNames(node)); } catch(e) {}
|
||||
node = Object.getPrototypeOf(node);
|
||||
if (!node || node === Object.prototype) break;
|
||||
}
|
||||
for (const key of Array.from(new Set(keys)).slice(0, 500)) {
|
||||
if (!/^\$_|grid|view|data|provider|handler|container|model|_/.test(key)) continue;
|
||||
try {
|
||||
const v = body[key];
|
||||
const rec = {key, type: typeof v, text: String(v).slice(0, 200)};
|
||||
if (v && typeof v === 'object') {
|
||||
let sub = [];
|
||||
try { sub = Object.getOwnPropertyNames(v).concat(Object.keys(v)); } catch(e) {}
|
||||
rec.keys = Array.from(new Set(sub)).slice(0, 80);
|
||||
rec.funcs = rec.keys.filter(k => { try { return typeof v[k] === 'function'; } catch(e) { return false; } }).slice(0, 40);
|
||||
}
|
||||
item.props.push(rec);
|
||||
} catch(e) {}
|
||||
}
|
||||
out.push(item);
|
||||
}
|
||||
return out;
|
||||
"""
|
||||
print(json.dumps(driver.execute_script(script), ensure_ascii=False, indent=2)[:80000])
|
||||
driver.quit()
|
||||
@@ -0,0 +1,4 @@
|
||||
@echo off
|
||||
set "WEHAGO_CHROME_DEBUGGER_ADDRESS=127.0.0.1:9225"
|
||||
cd /d C:\Windows
|
||||
python -u "\\wsl.localhost\Ubuntu\home\b17301\my-intranet-app\scripts\inspect_wehago_grid_9225.py"
|
||||
@@ -0,0 +1,49 @@
|
||||
import json
|
||||
import os
|
||||
|
||||
from selenium import webdriver
|
||||
from selenium.webdriver.chrome.options import Options
|
||||
|
||||
|
||||
options = Options()
|
||||
options.add_experimental_option("debuggerAddress", os.environ.get("WEHAGO_CHROME_DEBUGGER_ADDRESS", "127.0.0.1:9225"))
|
||||
driver = webdriver.Chrome(options=options)
|
||||
driver.set_script_timeout(10)
|
||||
|
||||
script = r"""
|
||||
const g = window.Grids && window.Grids.realgrid;
|
||||
const safe = (fn) => {
|
||||
try { return fn(); } catch (e) { return {error: String(e)}; }
|
||||
};
|
||||
const out = {};
|
||||
out.current = safe(() => g && g.getCurrent && g.getCurrent());
|
||||
out.fullItemCount = safe(() => g && g.fullItemCount && g.fullItemCount());
|
||||
out.columns = safe(() => g && g.getColumns && g.getColumns().map(c => ({
|
||||
name: c.name || c.fieldName || c.field,
|
||||
fieldName: c.fieldName,
|
||||
header: c.header && (c.header.text || c.header)
|
||||
})).slice(0, 80));
|
||||
out.rows = [];
|
||||
if (g) {
|
||||
const n = Math.min(30, Number(out.fullItemCount) || 30);
|
||||
for (let i = 0; i < n; i += 1) {
|
||||
out.rows.push({
|
||||
item: i,
|
||||
dataRow: safe(() => g.getDataRow(i)),
|
||||
values: safe(() => g.getValues(i)),
|
||||
display: safe(() => g.getDisplayValuesOfRow ? g.getDisplayValuesOfRow(i) : g.getDisplayValues(i))
|
||||
});
|
||||
}
|
||||
}
|
||||
out.visibleText = Array.from(document.querySelectorAll('.rg-data-cell, td[class*=rg-data-cell], [class*=rg-data-cell], td'))
|
||||
.map(el => {
|
||||
const r = el.getBoundingClientRect();
|
||||
return {text: (el.textContent || '').trim(), left: Math.round(r.left), top: Math.round(r.top), width: Math.round(r.width), height: Math.round(r.height)};
|
||||
})
|
||||
.filter(x => x.text && x.width > 0 && x.height > 0 && x.top > 120)
|
||||
.slice(0, 120);
|
||||
return out;
|
||||
"""
|
||||
|
||||
print(json.dumps(driver.execute_script(script), ensure_ascii=False, indent=2)[:60000])
|
||||
driver.quit()
|
||||
@@ -0,0 +1,4 @@
|
||||
@echo off
|
||||
set "WEHAGO_CHROME_DEBUGGER_ADDRESS=127.0.0.1:9225"
|
||||
cd /d C:\Windows
|
||||
python -u "\\wsl.localhost\Ubuntu\home\b17301\my-intranet-app\scripts\inspect_wehago_inputs_9225.py"
|
||||
@@ -0,0 +1,35 @@
|
||||
import json
|
||||
import os
|
||||
|
||||
from selenium import webdriver
|
||||
from selenium.webdriver.chrome.options import Options
|
||||
|
||||
|
||||
options = Options()
|
||||
options.add_experimental_option("debuggerAddress", os.environ.get("WEHAGO_CHROME_DEBUGGER_ADDRESS", "127.0.0.1:9225"))
|
||||
driver = webdriver.Chrome(options=options)
|
||||
|
||||
script = r"""
|
||||
const items = [];
|
||||
for (const el of document.querySelectorAll('input, textarea, select, button, [role=button], a')) {
|
||||
const r = el.getBoundingClientRect();
|
||||
const style = getComputedStyle(el);
|
||||
if (r.width <= 0 || r.height <= 0 || style.display === 'none' || style.visibility === 'hidden') continue;
|
||||
items.push({
|
||||
tag: el.tagName,
|
||||
type: el.getAttribute('type'),
|
||||
text: (el.textContent || '').trim(),
|
||||
value: el.value || '',
|
||||
placeholder: el.getAttribute('placeholder') || '',
|
||||
title: el.getAttribute('title') || '',
|
||||
aria: el.getAttribute('aria-label') || '',
|
||||
name: el.getAttribute('name') || '',
|
||||
id: el.id || '',
|
||||
cls: String(el.className || '').slice(0, 120),
|
||||
left: Math.round(r.left), top: Math.round(r.top), width: Math.round(r.width), height: Math.round(r.height)
|
||||
});
|
||||
}
|
||||
return items.sort((a,b)=>(a.top-b.top)||(a.left-b.left));
|
||||
"""
|
||||
print(json.dumps(driver.execute_script(script), ensure_ascii=False, indent=2)[:50000])
|
||||
driver.quit()
|
||||
@@ -0,0 +1,3 @@
|
||||
@echo off
|
||||
cd /d C:\Windows
|
||||
python -u "\\wsl.localhost\Ubuntu\home\b17301\my-intranet-app\scripts\mark_wehago_2022_no_data.py"
|
||||
@@ -0,0 +1,33 @@
|
||||
from pathlib import Path
|
||||
|
||||
from openpyxl import load_workbook
|
||||
|
||||
|
||||
base = Path(r"\\wsl.localhost\Ubuntu\home\b17301\WEHAGO_DB\data_download\2022")
|
||||
target = max(base.glob("ledger_*.xlsx"), key=lambda item: item.stat().st_mtime)
|
||||
no_data = {"377", "400", "636", "643", "805", "819", "820", "824", "843", "932", "936", "950", "951"}
|
||||
|
||||
workbook = load_workbook(target)
|
||||
status_ws = workbook["계정별상태"]
|
||||
error_ws = workbook["오류_누락"]
|
||||
|
||||
for row in status_ws.iter_rows(min_row=2):
|
||||
code = str(row[0].value)
|
||||
if code in no_data:
|
||||
row[2].value = "데이터없음"
|
||||
row[5].value = "2022년 WEHAGO 계정별원장 화면에서 거래 데이터가 없어 다운로드 파일을 생성하지 않았습니다."
|
||||
|
||||
for row_index in range(2, error_ws.max_row + 1):
|
||||
for col_index in range(1, error_ws.max_column + 1):
|
||||
error_ws.cell(row_index, col_index).value = None
|
||||
|
||||
write_row = 2
|
||||
for row in status_ws.iter_rows(min_row=2, values_only=True):
|
||||
code, name, status, file_name, _row_count, message = row[:6]
|
||||
if status != "정상":
|
||||
for col_index, value in enumerate([code, name, status, file_name, message], start=1):
|
||||
error_ws.cell(write_row, col_index).value = value
|
||||
write_row += 1
|
||||
|
||||
workbook.save(target)
|
||||
print(target)
|
||||
@@ -0,0 +1,3 @@
|
||||
@echo off
|
||||
cd /d C:\Windows
|
||||
python -u "\\wsl.localhost\Ubuntu\home\b17301\my-intranet-app\scripts\mark_wehago_no_data.py" %*
|
||||
@@ -0,0 +1,42 @@
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from openpyxl import load_workbook
|
||||
|
||||
|
||||
YEAR_NO_DATA = {
|
||||
"2022": {"377", "400", "636", "643", "805", "819", "820", "824", "843", "932", "936", "950", "951"},
|
||||
"2023": {"263", "417", "636", "805", "819", "843", "932", "933", "936", "950", "951"},
|
||||
"2024": {"116", "263", "417", "805", "950", "951"},
|
||||
}
|
||||
|
||||
|
||||
year = sys.argv[1]
|
||||
base = Path(r"\\wsl.localhost\Ubuntu\home\b17301\WEHAGO_DB\data_download") / year
|
||||
target = max(base.glob("ledger_*.xlsx"), key=lambda item: item.stat().st_mtime)
|
||||
no_data = YEAR_NO_DATA[year]
|
||||
|
||||
workbook = load_workbook(target)
|
||||
status_ws = workbook["계정별상태"]
|
||||
error_ws = workbook["오류_누락"]
|
||||
|
||||
for row in status_ws.iter_rows(min_row=2):
|
||||
code = str(row[0].value)
|
||||
if code in no_data:
|
||||
row[2].value = "데이터없음"
|
||||
row[5].value = f"{year}년 WEHAGO 계정별원장 화면에서 거래 데이터가 없어 다운로드 파일을 생성하지 않았습니다."
|
||||
|
||||
for row_index in range(2, error_ws.max_row + 1):
|
||||
for col_index in range(1, error_ws.max_column + 1):
|
||||
error_ws.cell(row_index, col_index).value = None
|
||||
|
||||
write_row = 2
|
||||
for row in status_ws.iter_rows(min_row=2, values_only=True):
|
||||
code, name, status, file_name, _row_count, message = row[:6]
|
||||
if status != "정상":
|
||||
for col_index, value in enumerate([code, name, status, file_name, message], start=1):
|
||||
error_ws.cell(write_row, col_index).value = value
|
||||
write_row += 1
|
||||
|
||||
workbook.save(target)
|
||||
print(target)
|
||||
@@ -0,0 +1,4 @@
|
||||
@echo off
|
||||
set "WEHAGO_DOWNLOAD_DIR=\\wsl.localhost\Ubuntu\home\b17301\WEHAGO_DB\data_download\2022"
|
||||
cd /d C:\Windows
|
||||
python -u "\\wsl.localhost\Ubuntu\home\b17301\my-intranet-app\scripts\wehago_data_download_2022_work.py" --merge-only
|
||||
@@ -0,0 +1,4 @@
|
||||
@echo off
|
||||
set "WEHAGO_DOWNLOAD_DIR=\\wsl.localhost\Ubuntu\home\b17301\WEHAGO_DB\data_download\2023"
|
||||
cd /d C:\Windows
|
||||
python -u "\\wsl.localhost\Ubuntu\home\b17301\my-intranet-app\scripts\wehago_data_download_2022_work.py" --merge-only
|
||||
@@ -0,0 +1,4 @@
|
||||
@echo off
|
||||
set "WEHAGO_DOWNLOAD_DIR=\\wsl.localhost\Ubuntu\home\b17301\WEHAGO_DB\data_download\2024"
|
||||
cd /d C:\Windows
|
||||
python -u "\\wsl.localhost\Ubuntu\home\b17301\my-intranet-app\scripts\wehago_data_download_2022_work.py" --merge-only
|
||||
@@ -0,0 +1,4 @@
|
||||
@echo off
|
||||
set "WEHAGO_CHROME_DEBUGGER_ADDRESS=127.0.0.1:9225"
|
||||
cd /d C:\Windows
|
||||
python -u "\\wsl.localhost\Ubuntu\home\b17301\my-intranet-app\scripts\probe_realgrid_active_9225.py"
|
||||
@@ -0,0 +1,71 @@
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
|
||||
from selenium import webdriver
|
||||
from selenium.webdriver.chrome.options import Options
|
||||
|
||||
|
||||
options = Options()
|
||||
options.add_experimental_option("debuggerAddress", os.environ.get("WEHAGO_CHROME_DEBUGGER_ADDRESS", "127.0.0.1:9225"))
|
||||
driver = webdriver.Chrome(options=options)
|
||||
driver.set_script_timeout(10)
|
||||
|
||||
script = r"""
|
||||
const body = Array.from(document.querySelectorAll('.rg-body')).find(el => String(el.$_view) === '75');
|
||||
if (body) {
|
||||
const r = body.getBoundingClientRect();
|
||||
const x = r.left + 80, y = r.top + 85;
|
||||
for (const type of ['mouseover','mousemove','mousedown','mouseup','click']) {
|
||||
body.dispatchEvent(new MouseEvent(type,{bubbles:true,cancelable:true,view:window,clientX:x,clientY:y,button:0}));
|
||||
}
|
||||
}
|
||||
const summarize = (obj) => {
|
||||
if (!obj) return null;
|
||||
let keys = [];
|
||||
try { keys = Object.getOwnPropertyNames(obj).concat(Object.keys(obj)); } catch(e) {}
|
||||
const funcs = Array.from(new Set(keys)).filter(k => {
|
||||
try { return typeof obj[k] === 'function'; } catch(e) { return false; }
|
||||
}).slice(0, 160);
|
||||
const props = {};
|
||||
for (const k of keys.slice(0, 100)) {
|
||||
try {
|
||||
const v = obj[k];
|
||||
if (['string','number','boolean'].includes(typeof v)) props[k]=v;
|
||||
} catch(e) {}
|
||||
}
|
||||
return {text:String(obj).slice(0,200), keys:Array.from(new Set(keys)).slice(0,200), funcs, props};
|
||||
};
|
||||
const active = Grids.getActiveGrid && Grids.getActiveGrid();
|
||||
const handler = active && active.getHandler && active.getHandler();
|
||||
const out = {
|
||||
active: summarize(active),
|
||||
handler: summarize(handler),
|
||||
current: null,
|
||||
itemCount: null,
|
||||
columns: null,
|
||||
rows: []
|
||||
};
|
||||
const targets = [active, handler].filter(Boolean);
|
||||
for (const g of targets) {
|
||||
if (!out.current) {
|
||||
try { if (g.getCurrent) out.current = g.getCurrent(); } catch(e) { out.current = {error:String(e)}; }
|
||||
}
|
||||
if (!out.itemCount) {
|
||||
try { if (g.getItemCount) out.itemCount = g.getItemCount(); } catch(e) {}
|
||||
try { if (!out.itemCount && g.fullItemCount) out.itemCount = g.fullItemCount(); } catch(e) {}
|
||||
}
|
||||
if (!out.columns) {
|
||||
try { if (g.getColumns) out.columns = g.getColumns().map(c => ({name:c.name, fieldName:c.fieldName, text:c.header && c.header.text})); } catch(e) {}
|
||||
}
|
||||
try {
|
||||
if (g.getValues) {
|
||||
for (let i=0; i<Math.min(10, out.itemCount || 10); i++) out.rows.push({i, values:g.getValues(i)});
|
||||
break;
|
||||
}
|
||||
} catch(e) { out.rowsError = String(e); }
|
||||
}
|
||||
return out;
|
||||
"""
|
||||
print(json.dumps(driver.execute_script(script), ensure_ascii=False, indent=2)[:80000])
|
||||
driver.quit()
|
||||
@@ -0,0 +1,4 @@
|
||||
@echo off
|
||||
set "WEHAGO_CHROME_DEBUGGER_ADDRESS=127.0.0.1:9225"
|
||||
cd /d C:\Windows
|
||||
python -u "\\wsl.localhost\Ubuntu\home\b17301\my-intranet-app\scripts\probe_realgrid_view_methods_9225.py"
|
||||
@@ -0,0 +1,37 @@
|
||||
import json
|
||||
import os
|
||||
|
||||
from selenium import webdriver
|
||||
from selenium.webdriver.chrome.options import Options
|
||||
|
||||
|
||||
options = Options()
|
||||
options.add_experimental_option("debuggerAddress", os.environ.get("WEHAGO_CHROME_DEBUGGER_ADDRESS", "127.0.0.1:9225"))
|
||||
driver = webdriver.Chrome(options=options)
|
||||
driver.set_script_timeout(10)
|
||||
|
||||
script = r"""
|
||||
const rg = Grids && Grids.realgrid;
|
||||
const ids = [75, '75', 327, '327'];
|
||||
const methods = ['getCurrent','fullItemCount','getColumns','getValues','getDataRow','getDisplayValues','getDisplayValuesOfRow','setCurrent','setSelectionItem','getSelectionItem','getSelectionItems'];
|
||||
const out = {};
|
||||
for (const m of methods) {
|
||||
out[m] = [];
|
||||
for (const id of ids) {
|
||||
try {
|
||||
if (!rg || typeof rg[m] !== 'function') { out[m].push({id, missing:true}); continue; }
|
||||
let value;
|
||||
if (m === 'getValues' || m === 'getDataRow' || m === 'getDisplayValues' || m === 'getDisplayValuesOfRow') value = rg[m](id, 0);
|
||||
else if (m === 'setCurrent') value = rg[m](id, {itemIndex: 5, column: 'Code'});
|
||||
else if (m === 'setSelectionItem') value = rg[m](id, 5);
|
||||
else value = rg[m](id);
|
||||
out[m].push({id, ok:true, value});
|
||||
} catch(e) {
|
||||
out[m].push({id, error:String(e).slice(0,300)});
|
||||
}
|
||||
}
|
||||
}
|
||||
return out;
|
||||
"""
|
||||
print(json.dumps(driver.execute_script(script), ensure_ascii=False, indent=2)[:80000])
|
||||
driver.quit()
|
||||
@@ -5,6 +5,7 @@ cd "$(dirname "$0")/.."
|
||||
|
||||
PORT="${1:-${INTRANET_PORT:-8010}}"
|
||||
HEALTH_URL="http://127.0.0.1:${PORT}/health"
|
||||
COMPARE_HEALTH_URL="http://127.0.0.1:${PORT}/health/wehago-compare"
|
||||
|
||||
if [ ! -x ".venv/bin/python" ]; then
|
||||
echo "가상환경이 없습니다. 먼저 아래를 실행하세요."
|
||||
@@ -15,9 +16,17 @@ if [ ! -x ".venv/bin/python" ]; then
|
||||
fi
|
||||
|
||||
if curl -fsS --max-time 2 "$HEALTH_URL" >/dev/null 2>&1; then
|
||||
if curl -fsS --max-time 4 "$COMPARE_HEALTH_URL" >/dev/null 2>&1; then
|
||||
echo "이미 실행 중입니다: $HEALTH_URL"
|
||||
exit 0
|
||||
fi
|
||||
echo "서버 포트는 열려 있지만 전표비교 readiness 가 실패했습니다. 재시작합니다."
|
||||
EXISTING_PID="$(ss -ltnp "( sport = :${PORT} )" | awk 'NR>1 {print $NF}' | sed -n 's/.*pid=\([0-9][0-9]*\).*/\1/p' | head -n 1)"
|
||||
if [ -n "${EXISTING_PID}" ]; then
|
||||
kill "${EXISTING_PID}" >/dev/null 2>&1 || true
|
||||
sleep 2
|
||||
fi
|
||||
fi
|
||||
|
||||
if ss -ltn "( sport = :${PORT} )" | tail -n +2 | grep -q .; then
|
||||
echo "포트 ${PORT} 는 이미 다른 프로세스가 사용 중입니다."
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
@echo off
|
||||
set "WEHAGO_CHROME_DEBUGGER_ADDRESS=127.0.0.1:9225"
|
||||
set "WEHAGO_OPEN_ACCOUNT_LEDGER_URL=1"
|
||||
set "WEHAGO_REFRESH_BEFORE_RUN="
|
||||
set "WEHAGO_ALLOW_UNCHANGED_DETAIL="
|
||||
set "WEHAGO_SCAN_EXISTING_ACCOUNT_MODE="
|
||||
set "WEHAGO_EXPECTED_PERIOD_START=2022.01.01"
|
||||
set "WEHAGO_EXPECTED_PERIOD_END=2022.12.31"
|
||||
set "WEHAGO_DOWNLOAD_DIR=\\wsl.localhost\Ubuntu\home\b17301\WEHAGO_DB\data_download\2022"
|
||||
set "WEHAGO_ACCOUNT_LEDGER_URL=https://smarta.wehago.com/#/smarta/account/SABK0107?sao&cno=1173867&cd_com=biz202103030006368&gisu=27&yminsa=2026&searchData=2022010120221231&color=#1C90FB&companyName=(%%EC%%A3%%BC)%%ED%%95%%9C%%EB%%A7%%A5%%EA%%B8%%B0%%EC%%88%%A0&companyID=b21344"
|
||||
cd /d C:\Windows
|
||||
python -u "\\wsl.localhost\Ubuntu\home\b17301\my-intranet-app\scripts\wehago_data_download_2022_work.py" --no-pause-on-failure --no-merge %*
|
||||
@@ -0,0 +1,12 @@
|
||||
@echo off
|
||||
set "WEHAGO_CHROME_DEBUGGER_ADDRESS=127.0.0.1:9225"
|
||||
set "WEHAGO_OPEN_ACCOUNT_LEDGER_URL=1"
|
||||
set "WEHAGO_REFRESH_BEFORE_RUN="
|
||||
set "WEHAGO_ALLOW_UNCHANGED_DETAIL=1"
|
||||
set "WEHAGO_SCAN_EXISTING_ACCOUNT_MODE="
|
||||
set "WEHAGO_EXPECTED_PERIOD_START=2022.01.01"
|
||||
set "WEHAGO_EXPECTED_PERIOD_END=2022.12.31"
|
||||
set "WEHAGO_DOWNLOAD_DIR=\\wsl.localhost\Ubuntu\home\b17301\WEHAGO_DB\data_download\2022"
|
||||
set "WEHAGO_ACCOUNT_LEDGER_URL=https://smarta.wehago.com/#/smarta/account/SABK0107?sao&cno=1173867&cd_com=biz202103030006368&gisu=27&yminsa=2026&searchData=2022010120221231&color=#1C90FB&companyName=(%%EC%%A3%%BC)%%ED%%95%%9C%%EB%%A7%%A5%%EA%%B8%%B0%%EC%%88%%A0&companyID=b21344"
|
||||
cd /d C:\Windows
|
||||
python -u "\\wsl.localhost\Ubuntu\home\b17301\my-intranet-app\scripts\wehago_data_download_2022_work.py" --no-pause-on-failure --no-merge %*
|
||||
@@ -0,0 +1,12 @@
|
||||
@echo off
|
||||
set "WEHAGO_CHROME_DEBUGGER_ADDRESS=127.0.0.1:9225"
|
||||
set "WEHAGO_OPEN_ACCOUNT_LEDGER_URL=1"
|
||||
set "WEHAGO_REFRESH_BEFORE_RUN="
|
||||
set "WEHAGO_ALLOW_UNCHANGED_DETAIL=1"
|
||||
set "WEHAGO_SCAN_EXISTING_ACCOUNT_MODE="
|
||||
set "WEHAGO_EXPECTED_PERIOD_START=2023.01.01"
|
||||
set "WEHAGO_EXPECTED_PERIOD_END=2023.12.31"
|
||||
set "WEHAGO_DOWNLOAD_DIR=\\wsl.localhost\Ubuntu\home\b17301\WEHAGO_DB\data_download\2023"
|
||||
set "WEHAGO_ACCOUNT_LEDGER_URL=https://smarta.wehago.com/#/smarta/account/SABK0107?sao&cno=1173867&cd_com=biz202103030006368&gisu=28&yminsa=2026&searchData=2023010120231231&color=#1C90FB&companyName=(%%EC%%A3%%BC)%%ED%%95%%9C%%EB%%A7%%A5%%EA%%B8%%B0%%EC%%88%%A0&companyID=b21344"
|
||||
cd /d C:\Windows
|
||||
python -u "\\wsl.localhost\Ubuntu\home\b17301\my-intranet-app\scripts\wehago_data_download_2022_work.py" --no-pause-on-failure --no-merge %*
|
||||
@@ -0,0 +1,12 @@
|
||||
@echo off
|
||||
set "WEHAGO_CHROME_DEBUGGER_ADDRESS=127.0.0.1:9225"
|
||||
set "WEHAGO_OPEN_ACCOUNT_LEDGER_URL=1"
|
||||
set "WEHAGO_REFRESH_BEFORE_RUN="
|
||||
set "WEHAGO_ALLOW_UNCHANGED_DETAIL=1"
|
||||
set "WEHAGO_SCAN_EXISTING_ACCOUNT_MODE="
|
||||
set "WEHAGO_EXPECTED_PERIOD_START=2024.01.01"
|
||||
set "WEHAGO_EXPECTED_PERIOD_END=2024.12.31"
|
||||
set "WEHAGO_DOWNLOAD_DIR=\\wsl.localhost\Ubuntu\home\b17301\WEHAGO_DB\data_download\2024"
|
||||
set "WEHAGO_ACCOUNT_LEDGER_URL=https://smarta.wehago.com/#/smarta/account/SABK0107?sao&cno=1173867&cd_com=biz202103030006368&gisu=29&yminsa=2026&searchData=2024010120241231&color=#1C90FB&companyName=(%%EC%%A3%%BC)%%ED%%95%%9C%%EB%%A7%%A5%%EA%%B8%%B0%%EC%%88%%A0&companyID=b21344"
|
||||
cd /d C:\Windows
|
||||
python -u "\\wsl.localhost\Ubuntu\home\b17301\my-intranet-app\scripts\wehago_data_download_2022_work.py" --no-pause-on-failure --no-merge %*
|
||||
@@ -0,0 +1,4 @@
|
||||
@echo off
|
||||
set "WEHAGO_CHROME_DEBUGGER_ADDRESS=127.0.0.1:9225"
|
||||
cd /d C:\Windows
|
||||
python -u "\\wsl.localhost\Ubuntu\home\b17301\my-intranet-app\scripts\test_left_click_modes_9225.py" %*
|
||||
@@ -0,0 +1,55 @@
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
|
||||
from selenium import webdriver
|
||||
from selenium.webdriver.chrome.options import Options
|
||||
|
||||
|
||||
target = sys.argv[1] if len(sys.argv) > 1 else "110"
|
||||
mode = sys.argv[2] if len(sys.argv) > 2 else "name-double"
|
||||
options = Options()
|
||||
options.add_experimental_option("debuggerAddress", os.environ.get("WEHAGO_CHROME_DEBUGGER_ADDRESS", "127.0.0.1:9225"))
|
||||
driver = webdriver.Chrome(options=options)
|
||||
|
||||
result = driver.execute_script(
|
||||
r"""
|
||||
const code = arguments[0], mode = arguments[1];
|
||||
const cells = Array.from(document.querySelectorAll('.rg-data-cell, td[class*=rg-data-cell], [class*=rg-data-cell], td'));
|
||||
const codeCell = cells.find(cell => {
|
||||
const r = cell.getBoundingClientRect();
|
||||
return (cell.textContent || '').trim() === code && r.left < 220 && r.top > 145 && r.width > 0 && r.height > 0;
|
||||
});
|
||||
if (!codeCell) return {error:'code not visible'};
|
||||
const cr = codeCell.getBoundingClientRect();
|
||||
const nameCell = cells.find(cell => {
|
||||
const r = cell.getBoundingClientRect();
|
||||
return Math.abs(r.top - cr.top) < 3 && r.left > 180 && r.left < 360 && r.width > 0 && r.height > 0;
|
||||
});
|
||||
const targetEl = mode.includes('name') && nameCell ? nameCell : codeCell;
|
||||
const r = targetEl.getBoundingClientRect();
|
||||
const x = r.left + (mode.includes('left') ? 8 : r.width / 2);
|
||||
const y = r.top + r.height / 2;
|
||||
const send = (type, detail=1) => {
|
||||
targetEl.dispatchEvent(new MouseEvent(type, {bubbles:true,cancelable:true,view:window,clientX:x,clientY:y,button:0,detail}));
|
||||
};
|
||||
const seq = ['mouseover','mousemove','mousedown','mouseup','click'];
|
||||
for (const type of seq) send(type, 1);
|
||||
if (mode.includes('double')) {
|
||||
for (const type of ['mousedown','mouseup','click','dblclick']) send(type, 2);
|
||||
}
|
||||
return {clicked: code, mode, x, y, codeTop: cr.top, nameText: nameCell && nameCell.textContent};
|
||||
""",
|
||||
target,
|
||||
mode,
|
||||
)
|
||||
time.sleep(1.0)
|
||||
right = driver.execute_script(
|
||||
r"""
|
||||
const body = Array.from(document.querySelectorAll('.rg-body')).find(el => String(el.$_view) === '327');
|
||||
return body ? (body.textContent || '').slice(0, 500) : '';
|
||||
"""
|
||||
)
|
||||
print(json.dumps({"result": result, "right": right}, ensure_ascii=False, indent=2))
|
||||
driver.quit()
|
||||
@@ -0,0 +1,4 @@
|
||||
@echo off
|
||||
set "WEHAGO_CHROME_DEBUGGER_ADDRESS=127.0.0.1:9225"
|
||||
cd /d C:\Windows
|
||||
python -u "\\wsl.localhost\Ubuntu\home\b17301\my-intranet-app\scripts\test_left_list_search_9225.py" %*
|
||||
@@ -0,0 +1,75 @@
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
|
||||
from selenium import webdriver
|
||||
from selenium.webdriver.chrome.options import Options
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.common.keys import Keys
|
||||
|
||||
|
||||
code = sys.argv[1] if len(sys.argv) > 1 else "263"
|
||||
options = Options()
|
||||
options.add_experimental_option("debuggerAddress", os.environ.get("WEHAGO_CHROME_DEBUGGER_ADDRESS", "127.0.0.1:9225"))
|
||||
driver = webdriver.Chrome(options=options)
|
||||
|
||||
def visible_controls():
|
||||
return driver.execute_script(
|
||||
r"""
|
||||
const items=[];
|
||||
for (const el of document.querySelectorAll('input,button,[role=button],a')) {
|
||||
const r=el.getBoundingClientRect();
|
||||
const st=getComputedStyle(el);
|
||||
if (r.width<=0 || r.height<=0 || st.display==='none' || st.visibility==='hidden') continue;
|
||||
items.push({tag:el.tagName,id:el.id||'',cls:String(el.className||'').slice(0,80),text:(el.textContent||'').trim(),value:el.value||'',placeholder:el.getAttribute('placeholder')||'',title:el.getAttribute('title')||'',left:Math.round(r.left),top:Math.round(r.top),width:Math.round(r.width),height:Math.round(r.height)});
|
||||
}
|
||||
return items.sort((a,b)=>(a.top-b.top)||(a.left-b.left));
|
||||
"""
|
||||
)
|
||||
|
||||
before = visible_controls()
|
||||
button = None
|
||||
for el in driver.find_elements(By.CSS_SELECTOR, "button, [role=button], a"):
|
||||
if not el.is_displayed():
|
||||
continue
|
||||
r = driver.execute_script("const r=arguments[0].getBoundingClientRect(); return {left:r.left,top:r.top,width:r.width,height:r.height};", el)
|
||||
text = (el.text or "").strip()
|
||||
title = el.get_attribute("title") or ""
|
||||
cls = el.get_attribute("class") or ""
|
||||
if r["left"] < 180 and 150 <= r["top"] <= 210 and (text or title or "LSbutton" in cls):
|
||||
button = el
|
||||
break
|
||||
if button:
|
||||
button.click()
|
||||
time.sleep(0.5)
|
||||
|
||||
after_click = visible_controls()
|
||||
inputs = driver.find_elements(By.CSS_SELECTOR, "input")
|
||||
typed = []
|
||||
for inp in inputs:
|
||||
if not inp.is_displayed() or not inp.is_enabled():
|
||||
continue
|
||||
r = driver.execute_script("const r=arguments[0].getBoundingClientRect(); return {left:r.left,top:r.top,width:r.width,height:r.height};", inp)
|
||||
if 120 <= r["top"] <= 260 and r["left"] < 500:
|
||||
try:
|
||||
inp.click()
|
||||
inp.send_keys(Keys.CONTROL, "a")
|
||||
inp.send_keys(code)
|
||||
inp.send_keys(Keys.ENTER)
|
||||
typed.append(r)
|
||||
time.sleep(0.8)
|
||||
break
|
||||
except Exception as exc:
|
||||
typed.append({"error": str(exc), **r})
|
||||
|
||||
visible = driver.execute_script(
|
||||
r"""
|
||||
return Array.from(document.querySelectorAll('.rg-data-cell, td[class*=rg-data-cell], [class*=rg-data-cell], td')).map(cell=>{
|
||||
const r=cell.getBoundingClientRect();
|
||||
return {text:(cell.textContent||'').trim(),left:Math.round(r.left),top:Math.round(r.top),width:Math.round(r.width),height:Math.round(r.height)};
|
||||
}).filter(x=>x.text && x.left<500 && x.top>140 && x.width>0 && x.height>0).slice(0,80);
|
||||
"""
|
||||
)
|
||||
print(json.dumps({"clickedButton": bool(button), "typed": typed, "before": before[:40], "afterClick": after_click[:80], "visible": visible}, ensure_ascii=False, indent=2))
|
||||
driver.quit()
|
||||
@@ -0,0 +1,4 @@
|
||||
@echo off
|
||||
set "WEHAGO_CHROME_DEBUGGER_ADDRESS=127.0.0.1:9225"
|
||||
cd /d C:\Windows
|
||||
python -u "\\wsl.localhost\Ubuntu\home\b17301\my-intranet-app\scripts\test_selenium_left_click_9225.py" %*
|
||||
@@ -0,0 +1,33 @@
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
|
||||
from selenium import webdriver
|
||||
from selenium.webdriver.chrome.options import Options
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.common.action_chains import ActionChains
|
||||
|
||||
|
||||
target = sys.argv[1] if len(sys.argv) > 1 else "110"
|
||||
options = Options()
|
||||
options.add_experimental_option("debuggerAddress", os.environ.get("WEHAGO_CHROME_DEBUGGER_ADDRESS", "127.0.0.1:9225"))
|
||||
driver = webdriver.Chrome(options=options)
|
||||
|
||||
clicked = None
|
||||
for el in driver.find_elements(By.XPATH, f"//*[normalize-space(.)='{target}']"):
|
||||
if not el.is_displayed():
|
||||
continue
|
||||
r = driver.execute_script("const r=arguments[0].getBoundingClientRect(); return {left:r.left, top:r.top, width:r.width, height:r.height};", el)
|
||||
if r["left"] < 220 and r["top"] > 145 and r["width"] > 0 and r["height"] > 0:
|
||||
clicked = r
|
||||
try:
|
||||
el.click()
|
||||
except Exception:
|
||||
ActionChains(driver).move_to_element(el).click(el).perform()
|
||||
break
|
||||
|
||||
time.sleep(1.0)
|
||||
right = driver.execute_script("const b=Array.from(document.querySelectorAll('.rg-body')).find(el=>String(el.$_view)==='327'); return b ? (b.textContent||'').slice(0,500) : '';")
|
||||
print(json.dumps({"clicked": clicked, "right": right}, ensure_ascii=False, indent=2))
|
||||
driver.quit()
|
||||
Executable
+3024
File diff suppressed because it is too large
Load Diff
@@ -217,6 +217,157 @@
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.snapshot-panel {
|
||||
margin-top: 10px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 12px;
|
||||
background: #fff;
|
||||
padding: 10px 12px;
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.snapshot-panel[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.snapshot-panel-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.snapshot-panel-title {
|
||||
display: grid;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.snapshot-panel-title strong {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.snapshot-policy {
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.snapshot-summary-badges {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.snapshot-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 5px 8px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid #dbe3eb;
|
||||
background: #f8fafc;
|
||||
font-size: 12px;
|
||||
color: #334155;
|
||||
}
|
||||
|
||||
.snapshot-badge strong {
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.snapshot-years {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(148px, 1fr));
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.snapshot-year-card {
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 10px;
|
||||
background: #fff;
|
||||
padding: 8px 9px;
|
||||
display: grid;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.snapshot-year-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.snapshot-year-head strong {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.snapshot-state-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 2px 7px;
|
||||
border-radius: 999px;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
border: 1px solid transparent;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.snapshot-state-pill.ready { background: #ecfdf5; border-color: #bbf7d0; color: #166534; }
|
||||
.snapshot-state-pill.queued { background: #eff6ff; border-color: #bfdbfe; color: #1d4ed8; }
|
||||
.snapshot-state-pill.running { background: #eff6ff; border-color: #93c5fd; color: #1d4ed8; }
|
||||
.snapshot-state-pill.stale { background: #fff7ed; border-color: #fed7aa; color: #c2410c; }
|
||||
.snapshot-state-pill.failed { background: #fef2f2; border-color: #fecaca; color: #b91c1c; }
|
||||
.snapshot-state-pill.missing { background: #f8fafc; border-color: #cbd5e1; color: #475569; }
|
||||
|
||||
.snapshot-year-meta,
|
||||
.snapshot-job-meta {
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.snapshot-jobs {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.snapshot-job-row {
|
||||
border-top: 1px solid #eef2f7;
|
||||
padding-top: 6px;
|
||||
display: grid;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.snapshot-job-row:first-child {
|
||||
border-top: 0;
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.snapshot-job-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.icon-button-label {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.icon-button-label svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
stroke: currentColor;
|
||||
fill: none;
|
||||
stroke-width: 2;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
}
|
||||
|
||||
.status-card {
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
@@ -1048,7 +1199,14 @@
|
||||
</select>
|
||||
<button type="submit" class="button-secondary">적용</button>
|
||||
</form>
|
||||
<button type="button" class="button-secondary" id="undoLastActionBtn">직전 작업 취소</button>
|
||||
<button type="button" class="button-secondary" id="snapshotToggleBtn" aria-expanded="false" aria-controls="snapshotPanel">스냅샷 상태</button>
|
||||
<button type="button" class="button-secondary button-icon" id="undoLastActionBtn" aria-label="직전 작업 취소" title="직전 작업 취소">
|
||||
<span class="icon-button-label" aria-hidden="true">
|
||||
<svg viewBox="0 0 24 24">
|
||||
<path d="M15 18l-6-6 6-6"></path>
|
||||
</svg>
|
||||
</span>
|
||||
</button>
|
||||
<span class="panel-meta" id="lastActionMeta">
|
||||
{% if wehago_compare.last_action %}
|
||||
최근 작업: {{ wehago_compare.last_action.action_type }} / {{ "{:,}".format(wehago_compare.last_action.count or 0) }}건
|
||||
@@ -1155,6 +1313,22 @@
|
||||
</section>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<section class="snapshot-panel" id="snapshotPanel" hidden>
|
||||
<div class="snapshot-panel-head">
|
||||
<div class="snapshot-panel-title">
|
||||
<strong>스냅샷 상태</strong>
|
||||
<div class="snapshot-policy" id="snapshotPolicyText">전표 스냅샷 상태를 불러오는 중입니다.</div>
|
||||
</div>
|
||||
<div style="display:flex; align-items:center; gap:8px; flex-wrap:wrap;">
|
||||
<div class="panel-meta" id="snapshotSummaryMeta"></div>
|
||||
<button type="button" class="button-secondary" id="snapshotRebuildBtn">현재 기간 다시 준비</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="snapshot-summary-badges" id="snapshotSummaryBadges"></div>
|
||||
<div class="snapshot-years" id="snapshotYears"></div>
|
||||
<div class="snapshot-jobs" id="snapshotJobs"></div>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -1440,6 +1614,15 @@
|
||||
return payload;
|
||||
};
|
||||
|
||||
const stateLabelMap = {
|
||||
ready: '준비됨',
|
||||
queued: '대기중',
|
||||
running: '생성중',
|
||||
stale: '구버전',
|
||||
failed: '실패',
|
||||
missing: '없음',
|
||||
};
|
||||
|
||||
const applySummaryPayload = (payload) => {
|
||||
const sections = Array.isArray(payload?.metric_sections) ? payload.metric_sections : [];
|
||||
sections.forEach((section) => {
|
||||
@@ -1462,6 +1645,84 @@
|
||||
}
|
||||
};
|
||||
|
||||
const renderSnapshotStatus = (payload) => {
|
||||
const snapshotPolicyText = document.getElementById('snapshotPolicyText');
|
||||
const snapshotSummaryMeta = document.getElementById('snapshotSummaryMeta');
|
||||
const snapshotSummaryBadges = document.getElementById('snapshotSummaryBadges');
|
||||
const snapshotYears = document.getElementById('snapshotYears');
|
||||
const snapshotJobs = document.getElementById('snapshotJobs');
|
||||
if (!snapshotPolicyText || !snapshotSummaryMeta || !snapshotSummaryBadges || !snapshotYears || !snapshotJobs) {
|
||||
return;
|
||||
}
|
||||
const aggregate = payload?.aggregate || payload?.snapshot_aggregate || {};
|
||||
const policy = payload?.snapshot_policy || {};
|
||||
const years = Array.isArray(payload?.years || payload?.snapshot_years) ? (payload.years || payload.snapshot_years) : [];
|
||||
const recentJobs = Array.isArray(payload?.recent_jobs) ? payload.recent_jobs : [];
|
||||
const modeSummary = Array.isArray(policy.year_modes)
|
||||
? policy.year_modes.map((item) => `${item.year}:${item.mode}`).join(', ')
|
||||
: '';
|
||||
snapshotPolicyText.textContent = `백필 시작 ${policy.backfill_hint_start_year || '-'} / 운영 연도 ${policy.rolling_update_start_year || '-'}+ / 월별 반영 기준 익월 ${policy.monthly_expected_ready_day || '-'}일 이후`;
|
||||
snapshotSummaryMeta.textContent = modeSummary || '연도별 스냅샷 정책 정보가 없습니다.';
|
||||
const badgeOrder = ['ready', 'queued', 'running', 'stale', 'failed', 'missing'];
|
||||
snapshotSummaryBadges.innerHTML = badgeOrder.map((key) => {
|
||||
const label = stateLabelMap[key] || key;
|
||||
const value = Number(aggregate[key] || 0).toLocaleString();
|
||||
return `<span class="snapshot-badge"><span>${escapeHtml(label)}</span><strong>${escapeHtml(value)}</strong></span>`;
|
||||
}).join('');
|
||||
snapshotYears.innerHTML = years.length
|
||||
? years.map((item) => {
|
||||
const stateKey = String(item.state || 'missing');
|
||||
const counts = item.row_counts || {};
|
||||
const summaryBits = [
|
||||
`M ${Number(counts.matched || 0).toLocaleString()}`,
|
||||
`U ${Number(counts.ledger_only || 0).toLocaleString()}`,
|
||||
`R ${Number(counts.amount_mismatch || 0).toLocaleString()}`,
|
||||
`E ${Number(counts.voucher_only || 0).toLocaleString()}`,
|
||||
].join(' / ');
|
||||
const metaBits = [
|
||||
item.mode ? `mode ${item.mode}` : '',
|
||||
item.last_built_at ? `built ${item.last_built_at}` : '',
|
||||
item.last_requested_at ? `req ${item.last_requested_at}` : '',
|
||||
item.cache_state ? `cache ${item.cache_state}` : '',
|
||||
].filter(Boolean).join(' / ');
|
||||
return `
|
||||
<article class="snapshot-year-card">
|
||||
<div class="snapshot-year-head">
|
||||
<strong>${escapeHtml(item.fiscal_year)}</strong>
|
||||
<span class="snapshot-state-pill ${escapeHtml(stateKey)}">${escapeHtml(stateLabelMap[stateKey] || stateKey)}</span>
|
||||
</div>
|
||||
<div class="snapshot-year-meta">${escapeHtml(summaryBits)}</div>
|
||||
<div class="snapshot-year-meta">${escapeHtml(metaBits)}</div>
|
||||
${item.error_message ? `<div class="snapshot-year-meta" style="color:#b91c1c;">${escapeHtml(item.error_message)}</div>` : ''}
|
||||
</article>
|
||||
`;
|
||||
}).join('')
|
||||
: '<div class="panel-meta">표시할 연도 상태가 없습니다.</div>';
|
||||
snapshotJobs.innerHTML = recentJobs.length
|
||||
? recentJobs.slice(0, 8).map((job) => {
|
||||
const bits = [];
|
||||
if (job.job_type === 'year_snapshot_rebuild' && job.payload?.fiscal_year) {
|
||||
bits.push(`연도 ${job.payload.fiscal_year}`);
|
||||
}
|
||||
if (job.job_type === 'metric_counts_rebuild') {
|
||||
bits.push(`범위 ${job.payload?.start_year ?? '-'}~${job.payload?.end_year ?? '-'}`);
|
||||
}
|
||||
bits.push(`우선순위 ${job.priority ?? 0}`);
|
||||
return `
|
||||
<div class="snapshot-job-row">
|
||||
<div class="snapshot-job-head">
|
||||
<strong>${escapeHtml(job.job_type)}</strong>
|
||||
<span class="snapshot-state-pill ${escapeHtml(job.state || 'missing')}">${escapeHtml(stateLabelMap[job.state] || job.state || '')}</span>
|
||||
</div>
|
||||
<div class="snapshot-job-meta">${escapeHtml(bits.join(' / '))}</div>
|
||||
<div class="snapshot-job-meta">${escapeHtml([job.created_at, job.started_at, job.finished_at].filter(Boolean).join(' / '))}</div>
|
||||
${job.error_message ? `<div class="snapshot-job-meta" style="color:#b91c1c;">${escapeHtml(job.error_message)}</div>` : ''}
|
||||
</div>
|
||||
`;
|
||||
}).join('')
|
||||
: '<div class="panel-meta">최근 재생성 이력이 없습니다.</div>';
|
||||
};
|
||||
|
||||
const loadDashboardSummary = async (attempt = 0) => {
|
||||
const params = new URLSearchParams();
|
||||
if (startYearValue) params.set('start_year', startYearValue);
|
||||
@@ -1469,6 +1730,7 @@
|
||||
try {
|
||||
const payload = await fetchJson(`/wehago-compare/api/summary?${params.toString()}`);
|
||||
applySummaryPayload(payload);
|
||||
renderSnapshotStatus(payload);
|
||||
if (payload?.pending && attempt < 6) {
|
||||
window.setTimeout(() => loadDashboardSummary(attempt + 1), 1200);
|
||||
}
|
||||
@@ -1477,6 +1739,30 @@
|
||||
}
|
||||
};
|
||||
|
||||
const loadSnapshotStatus = async () => {
|
||||
const params = new URLSearchParams();
|
||||
if (startYearValue) params.set('start_year', startYearValue);
|
||||
if (endYearValue) params.set('end_year', endYearValue);
|
||||
try {
|
||||
const payload = await fetchJson(`/wehago-compare/api/snapshot-status?${params.toString()}`);
|
||||
renderSnapshotStatus(payload);
|
||||
snapshotStatusLoaded = true;
|
||||
return payload;
|
||||
} catch (_error) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const setSnapshotPanelOpen = async (open) => {
|
||||
if (!snapshotPanel || !snapshotToggleBtn) return;
|
||||
snapshotPanel.hidden = !open;
|
||||
snapshotToggleBtn.setAttribute('aria-expanded', open ? 'true' : 'false');
|
||||
snapshotToggleBtn.textContent = open ? '스냅샷 상태 숨기기' : '스냅샷 상태';
|
||||
if (open && !snapshotStatusLoaded) {
|
||||
await loadSnapshotStatus();
|
||||
}
|
||||
};
|
||||
|
||||
const buildQuery = (form, extra = {}) => {
|
||||
const params = new URLSearchParams();
|
||||
if (startYearValue) params.set('start_year', startYearValue);
|
||||
@@ -1509,11 +1795,15 @@
|
||||
const recommendClose = document.getElementById('recommendClose');
|
||||
const recommendSortTools = document.getElementById('recommendSortTools');
|
||||
const recommendSortButtons = Array.from(document.querySelectorAll('[data-recommend-sort]'));
|
||||
const snapshotToggleBtn = document.getElementById('snapshotToggleBtn');
|
||||
const snapshotPanel = document.getElementById('snapshotPanel');
|
||||
const undoLastActionBtn = document.getElementById('undoLastActionBtn');
|
||||
const lastActionMeta = document.getElementById('lastActionMeta');
|
||||
const snapshotRebuildBtn = document.getElementById('snapshotRebuildBtn');
|
||||
const recommendSelections = new Map();
|
||||
let recommendSortKey = 'score';
|
||||
let recommendMode = 'batch';
|
||||
let snapshotStatusLoaded = false;
|
||||
const individualState = {
|
||||
sourceStatus: '',
|
||||
sourceRow: null,
|
||||
@@ -2551,6 +2841,45 @@
|
||||
});
|
||||
}
|
||||
|
||||
if (snapshotRebuildBtn) {
|
||||
snapshotRebuildBtn.addEventListener('click', async () => {
|
||||
const confirmed = window.confirm('현재 기간의 전표 스냅샷과 현황 캐시를 다시 준비할까요?');
|
||||
if (!confirmed) return;
|
||||
snapshotRebuildBtn.disabled = true;
|
||||
try {
|
||||
const response = await fetch('/wehago-compare/api/snapshot-rebuild', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
start_year: startYearValue || null,
|
||||
end_year: endYearValue || null,
|
||||
include_metric_counts: true,
|
||||
}),
|
||||
});
|
||||
const payload = await response.json();
|
||||
if (!response.ok) {
|
||||
throw new Error(payload.error || '스냅샷 재생성 요청 중 오류가 발생했습니다.');
|
||||
}
|
||||
await loadSnapshotStatus();
|
||||
window.setTimeout(() => loadDashboardSummary(), 1200);
|
||||
} catch (error) {
|
||||
alert(error.message);
|
||||
} finally {
|
||||
snapshotRebuildBtn.disabled = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (snapshotToggleBtn) {
|
||||
snapshotToggleBtn.addEventListener('click', async () => {
|
||||
const nextOpen = snapshotPanel ? snapshotPanel.hidden : true;
|
||||
await setSnapshotPanelOpen(nextOpen);
|
||||
});
|
||||
}
|
||||
|
||||
document.querySelectorAll('[data-table-wrap]').forEach((wrap) => {
|
||||
const statusKey = wrap.dataset.tableWrap;
|
||||
if (!statusKey) {
|
||||
|
||||
+975
-83
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user