Add data refresh assets and local access scripts

This commit is contained in:
b17301
2026-05-14 20:44:17 +09:00
parent 542dd7d536
commit 49a7a45070
11 changed files with 497 additions and 72 deletions
BIN
View File
Binary file not shown.
+3 -2
View File
@@ -6,7 +6,8 @@ set "WEHAGO_ALLOW_UNCHANGED_DETAIL="
set "WEHAGO_SCAN_EXISTING_ACCOUNT_MODE=" set "WEHAGO_SCAN_EXISTING_ACCOUNT_MODE="
set "WEHAGO_EXPECTED_PERIOD_START=2022.01.01" set "WEHAGO_EXPECTED_PERIOD_START=2022.01.01"
set "WEHAGO_EXPECTED_PERIOD_END=2022.12.31" set "WEHAGO_EXPECTED_PERIOD_END=2022.12.31"
set "WEHAGO_DOWNLOAD_DIR=\\wsl.localhost\Ubuntu\home\b17301\WEHAGO_DB\data_download\2022" set "WEHAGO_DOWNLOAD_DIR=\\wsl.localhost\Ubuntu\home\b17301\WEHAGO_DB\data_download\hanmac\2022"
set "WEHAGO_MERGED_FILENAME=2022_계정별원장_취합.xlsx"
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" 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 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 %* python -u "\\wsl.localhost\Ubuntu\home\b17301\my-intranet-app\scripts\wehago_data_download_2022_work.py" --no-pause-on-failure %*
@@ -6,7 +6,8 @@ set "WEHAGO_ALLOW_UNCHANGED_DETAIL=1"
set "WEHAGO_SCAN_EXISTING_ACCOUNT_MODE=" set "WEHAGO_SCAN_EXISTING_ACCOUNT_MODE="
set "WEHAGO_EXPECTED_PERIOD_START=2023.01.01" set "WEHAGO_EXPECTED_PERIOD_START=2023.01.01"
set "WEHAGO_EXPECTED_PERIOD_END=2023.12.31" set "WEHAGO_EXPECTED_PERIOD_END=2023.12.31"
set "WEHAGO_DOWNLOAD_DIR=\\wsl.localhost\Ubuntu\home\b17301\WEHAGO_DB\data_download\2023" set "WEHAGO_DOWNLOAD_DIR=\\wsl.localhost\Ubuntu\home\b17301\WEHAGO_DB\data_download\hanmac\2023"
set "WEHAGO_MERGED_FILENAME=2023_계정별원장_취합.xlsx"
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" 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 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 %* python -u "\\wsl.localhost\Ubuntu\home\b17301\my-intranet-app\scripts\wehago_data_download_2022_work.py" --no-pause-on-failure %*
@@ -6,7 +6,8 @@ set "WEHAGO_ALLOW_UNCHANGED_DETAIL=1"
set "WEHAGO_SCAN_EXISTING_ACCOUNT_MODE=" set "WEHAGO_SCAN_EXISTING_ACCOUNT_MODE="
set "WEHAGO_EXPECTED_PERIOD_START=2024.01.01" set "WEHAGO_EXPECTED_PERIOD_START=2024.01.01"
set "WEHAGO_EXPECTED_PERIOD_END=2024.12.31" set "WEHAGO_EXPECTED_PERIOD_END=2024.12.31"
set "WEHAGO_DOWNLOAD_DIR=\\wsl.localhost\Ubuntu\home\b17301\WEHAGO_DB\data_download\2024" set "WEHAGO_DOWNLOAD_DIR=\\wsl.localhost\Ubuntu\home\b17301\WEHAGO_DB\data_download\hanmac\2024"
set "WEHAGO_MERGED_FILENAME=2024_계정별원장_취합.xlsx"
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" 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 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 %* python -u "\\wsl.localhost\Ubuntu\home\b17301\my-intranet-app\scripts\wehago_data_download_2022_work.py" --no-pause-on-failure %*
@@ -0,0 +1,3 @@
@echo off
set "WEHAGO_CHROME_DEBUGGER_ADDRESS=127.0.0.1:9226"
python -u "\\wsl.localhost\Ubuntu\home\b17301\my-intranet-app\scripts\run_wehago_jangheon_2023_2025_visible.py" %*
@@ -0,0 +1,133 @@
from __future__ import annotations
import os
import re
import sys
import time
from pathlib import Path
import wehago_data_download_2022_work as wehago
def detect_debugger_address() -> str:
explicit = os.environ.get("WEHAGO_CHROME_DEBUGGER_ADDRESS", "").strip()
if explicit:
return explicit
try:
for line in Path("/etc/resolv.conf").read_text(encoding="utf-8").splitlines():
if line.startswith("nameserver "):
host_ip = line.split()[1].strip()
if host_ip:
return f"{host_ip}:9226"
except Exception:
pass
return "127.0.0.1:9226"
DEBUGGER_ADDRESS = detect_debugger_address()
WEHAGO_HOME_URL = "https://www.wehago.com/"
if os.name == "nt":
DOWNLOAD_ROOT = Path(os.environ.get("WEHAGO_WINDOWS_DOWNLOAD_ROOT", r"C:\Users\User\Downloads\wehago\jangheon_construction"))
else:
DOWNLOAD_ROOT = Path("/home/b17301/WEHAGO_DB/data_download/jangheon_construction")
TARGET_PERIODS = [
(15, 2023),
(16, 2024),
(17, 2025),
]
def log(message: str) -> None:
print(message, flush=True)
def year_bounds(year: int) -> tuple[str, str]:
return f"{year}.01.01", f"{year}.12.31"
def build_period_url(base_url: str, gisu: int, year: int) -> str:
search_data = f"{year}0101{year}1231"
updated = re.sub(r"gisu=\d+", f"gisu={gisu}", base_url)
updated = re.sub(r"searchData=\d{16}", f"searchData={search_data}", updated)
return updated
def wait_until_ledger_ready(driver) -> str:
log("WEHAGO 로그인 및 장헌산업 15기(2023) 계정별원장 화면 준비를 기다립니다.")
while True:
try:
current_url = driver.current_url
except Exception:
current_url = ""
try:
ready = "SABK0107" in current_url and wehago.ledger_screen_ready(driver)
except Exception:
ready = False
if ready:
start_value, end_value = wehago.read_top_period_values(driver)
log(f"화면 확인 완료: {current_url}")
log(f"상단 기간: {start_value or '?'} ~ {end_value or '?'}")
return current_url
time.sleep(2)
def run_period(base_url: str, gisu: int, year: int, first_run: bool) -> None:
download_dir = DOWNLOAD_ROOT / str(year)
download_dir.mkdir(parents=True, exist_ok=True)
wehago.CHROME_DEBUGGER_ADDRESS = DEBUGGER_ADDRESS
wehago.DOWNLOAD_DIR = download_dir
wehago.SCAN_EXISTING_ACCOUNT_MODE = True
wehago.SCAN_ALL_VISIBLE_ACCOUNT_MODE = True
wehago.REFRESH_BEFORE_RUN = False
wehago.OPEN_ACCOUNT_LEDGER_URL = True
wehago.ACCOUNT_LEDGER_URL = build_period_url(base_url, gisu, year)
wehago.EXPECTED_PERIOD_START, wehago.EXPECTED_PERIOD_END = year_bounds(year)
wehago.AVAILABLE_ACCOUNT_CODES = None
wehago.MERGED_FILENAME = f"{year}_계정별원장_취합.xlsx"
log(f"{gisu}기({year}) 다운로드 시작")
log(f"저장 위치: {download_dir}")
wehago.run(wehago.ACCOUNTS, merge=False, pause_on_failure=False)
merged_path = wehago.consolidate_download_dir(download_dir, output_name=f"{year}_계정별원장_취합.xlsx")
log(f"{gisu}기({year}) 취합 완료: {merged_path}")
log(f"{gisu}기({year}) 다운로드 완료")
def main() -> int:
DOWNLOAD_ROOT.mkdir(parents=True, exist_ok=True)
wehago.CHROME_DEBUGGER_ADDRESS = DEBUGGER_ADDRESS
wehago.DOWNLOAD_DIR = DOWNLOAD_ROOT / "bootstrap"
log(f"디버그 Chrome 연결 주소: {DEBUGGER_ADDRESS}")
driver = wehago.build_driver(wehago.DOWNLOAD_DIR)
try:
try:
current_url = driver.current_url
except Exception:
current_url = ""
if "wehago" not in current_url.lower():
driver.get(WEHAGO_HOME_URL)
base_url = wait_until_ledger_ready(driver)
finally:
try:
driver.quit()
except Exception:
pass
for index, (gisu, year) in enumerate(TARGET_PERIODS):
run_period(base_url, gisu, year, first_run=index == 0)
log("장헌산업 15기~17기 계정별원장 다운로드가 모두 끝났습니다.")
return 0
if __name__ == "__main__":
sys.exit(main())
+3 -10
View File
@@ -3,16 +3,9 @@ setlocal
net session >nul 2>&1 net session >nul 2>&1
if not "%errorlevel%"=="0" ( if not "%errorlevel%"=="0" (
echo This script must be run as Administrator. echo Requesting Administrator privileges...
echo. powershell -NoProfile -ExecutionPolicy Bypass -Command "Start-Process -FilePath '%~f0' -ArgumentList '%*' -Verb RunAs"
echo 1. Open Windows Start menu. exit /b %errorlevel%
echo 2. Type cmd.
echo 3. Right-click Command Prompt and choose "Run as administrator".
echo 4. Run this command:
echo %~f0 %*
echo.
pause
exit /b 1
) )
set SCRIPT_DIR=%~dp0 set SCRIPT_DIR=%~dp0
+119 -15
View File
@@ -5,12 +5,82 @@ param(
$ErrorActionPreference = "Stop" $ErrorActionPreference = "Stop"
$apps = @( $knownPorts = @{
@{ Name = "hm-biz-process"; Port = 8000; RuleName = "HM-BIZ-PROCESS-8000" }, 8000 = @{ Name = "hm-biz-process"; RuleName = "HM-BIZ-PROCESS-8000" }
@{ Name = "my-intranet-app"; Port = 8010; RuleName = "MyIntranetApp-8010" }, 8010 = @{ Name = "my-intranet-app"; RuleName = "MyIntranetApp-8010" }
@{ Name = "b17301"; Port = 8020; RuleName = "B17301-8020" } 8020 = @{ Name = "b17301"; RuleName = "B17301-8020" }
}
function Get-WslListeningPorts {
$raw = wsl.exe bash -lc "ss -ltnH 2>/dev/null"
if (-not $raw) {
throw "WSL listening TCP 포트를 확인하지 못했습니다."
}
$portMap = @{}
foreach ($line in ($raw -split "`r?`n")) {
if ([string]::IsNullOrWhiteSpace($line)) {
continue
}
$parts = ($line -split "\s+") | Where-Object { $_ -ne "" }
if ($parts.Count -lt 4) {
continue
}
$localAddress = $parts[3].Trim()
if ([string]::IsNullOrWhiteSpace($localAddress)) {
continue
}
$hostPart = $localAddress
$portPart = ""
$lastColon = $localAddress.LastIndexOf(":")
if ($lastColon -lt 0) {
continue
}
$hostPart = $localAddress.Substring(0, $lastColon)
$portPart = $localAddress.Substring($lastColon + 1)
$parsedPort = 0
if (-not [int]::TryParse($portPart, [ref]$parsedPort)) {
continue
}
$port = [int]$parsedPort
if ($port -le 0) {
continue
}
$normalizedHost = $hostPart.Trim("[", "]")
$isLoopback = (
$normalizedHost -eq "127.0.0.1" -or
$normalizedHost -eq "::1" -or
$normalizedHost -eq "localhost"
) )
if (-not $portMap.ContainsKey($port)) {
$portMap[$port] = [ordered]@{
Port = $port
Hosts = New-Object System.Collections.Generic.List[string]
ExternallyReachable = $false
}
}
$entry = $portMap[$port]
if (-not $entry.Hosts.Contains($normalizedHost)) {
$entry.Hosts.Add($normalizedHost)
}
if (-not $isLoopback) {
$entry.ExternallyReachable = $true
}
}
return $portMap.Values | Sort-Object Port
}
if ([string]::IsNullOrWhiteSpace($WslIp)) { if ([string]::IsNullOrWhiteSpace($WslIp)) {
$detected = wsl.exe hostname -I 2>$null $detected = wsl.exe hostname -I 2>$null
if (-not $detected) { if (-not $detected) {
@@ -37,15 +107,32 @@ if ($ListenAddress -eq "0.0.0.0") {
} }
$listenAddresses = $listenAddresses | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | Select-Object -Unique $listenAddresses = $listenAddresses | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | Select-Object -Unique
Write-Host "Setting Windows portproxy for all WSL apps..." -ForegroundColor Cyan Write-Host "Setting Windows portproxy for all active WSL TCP ports..." -ForegroundColor Cyan
Write-Host "WSL IP: $WslIp" Write-Host "WSL IP: $WslIp"
$portEntries = @(Get-WslListeningPorts)
if (-not $portEntries -or $portEntries.Count -eq 0) {
throw "외부에서 접근 가능한 WSL TCP 포트를 찾지 못했습니다."
}
foreach ($app in $apps) { Write-Host "Detected ports: $((@($portEntries | ForEach-Object { $_.Port })) -join ', ')"
$port = [int]$app.Port
$ruleName = [string]$app.RuleName $reachableEntries = @($portEntries | Where-Object { $_.ExternallyReachable })
$loopbackOnlyEntries = @($portEntries | Where-Object { -not $_.ExternallyReachable })
if (-not $reachableEntries -or $reachableEntries.Count -eq 0) {
throw "현재 WSL에서 내부망에 공개 가능한 포트를 찾지 못했습니다. 앱이 0.0.0.0 또는 WSL IP에 바인딩되어 있는지 확인해 주세요."
}
$configuredPorts = New-Object System.Collections.Generic.List[int]
foreach ($entry in $reachableEntries) {
$port = [int]$entry.Port
$appInfo = $knownPorts[$port]
$displayName = if ($appInfo) { [string]$appInfo.Name } else { "wsl-port-$port" }
$ruleName = if ($appInfo) { [string]$appInfo.RuleName } else { "WSL-PORT-$port" }
Write-Host "" Write-Host ""
Write-Host "$($app.Name): $port" -ForegroundColor Cyan Write-Host "$displayName : $port ($($entry.Hosts -join ', '))" -ForegroundColor Cyan
foreach ($address in $listenAddresses) { foreach ($address in $listenAddresses) {
netsh interface portproxy delete v4tov4 listenport=$port listenaddress=$address | Out-Null netsh interface portproxy delete v4tov4 listenport=$port listenaddress=$address | Out-Null
@@ -63,6 +150,10 @@ foreach ($app in $apps) {
-Action Allow ` -Action Allow `
-Protocol TCP ` -Protocol TCP `
-LocalPort $port | Out-Null -LocalPort $port | Out-Null
if (-not $configuredPorts.Contains($port)) {
$configuredPorts.Add($port)
}
} }
Write-Host "" Write-Host ""
@@ -70,14 +161,27 @@ Write-Host "Completed." -ForegroundColor Green
Write-Host "Use these URLs from another PC on the intranet:" Write-Host "Use these URLs from another PC on the intranet:"
if ($windowsIps) { if ($windowsIps) {
foreach ($ip in $windowsIps) { foreach ($ip in $windowsIps) {
Write-Host "hm-biz-process : http://$ip`:8000" foreach ($port in $configuredPorts) {
Write-Host "my-intranet-app: http://$ip`:8010" $appInfo = $knownPorts[$port]
Write-Host "b17301 : http://$ip`:8020" $label = if ($appInfo) { [string]$appInfo.Name } else { "wsl-port-$port" }
Write-Host ("{0,-15}: http://{1}:{2}" -f $label, $ip, $port)
}
} }
} else { } else {
Write-Host "hm-biz-process : http://<Windows-IP>:8000" foreach ($port in $configuredPorts) {
Write-Host "my-intranet-app: http://<Windows-IP>:8010" $appInfo = $knownPorts[$port]
Write-Host "b17301 : http://<Windows-IP>:8020" $label = if ($appInfo) { [string]$appInfo.Name } else { "wsl-port-$port" }
Write-Host ("{0,-15}: http://<Windows-IP>:{1}" -f $label, $port)
}
}
if ($loopbackOnlyEntries.Count -gt 0) {
Write-Host ""
Write-Host "Loopback-only ports (not published to intranet):" -ForegroundColor Yellow
foreach ($entry in $loopbackOnlyEntries) {
Write-Host ("- {0}: {1}" -f $entry.Port, ($entry.Hosts -join ', '))
}
Write-Host "These ports are bound only to 127.0.0.1/::1 inside WSL. Run those apps on 0.0.0.0 (or the WSL IP) to expose them to the intranet." -ForegroundColor Yellow
} }
Write-Host "" Write-Host ""
Write-Host "Tip: if WSL restarts and its IP changes, run this script again." Write-Host "Tip: if WSL restarts and its IP changes, run this script again."
+3 -10
View File
@@ -3,16 +3,9 @@ setlocal
net session >nul 2>&1 net session >nul 2>&1
if not "%errorlevel%"=="0" ( if not "%errorlevel%"=="0" (
echo This script must be run as Administrator. echo Requesting Administrator privileges...
echo. powershell -NoProfile -ExecutionPolicy Bypass -Command "Start-Process -FilePath '%~f0' -ArgumentList '%*' -Verb RunAs"
echo 1. Open Windows Start menu. exit /b %errorlevel%
echo 2. Type cmd.
echo 3. Right-click Command Prompt and choose "Run as administrator".
echo 4. Run this command:
echo %~f0 %*
echo.
pause
exit /b 1
) )
set SCRIPT_DIR=%~dp0 set SCRIPT_DIR=%~dp0
+46 -8
View File
@@ -8,23 +8,61 @@ WINDOWS_IPS="$(
2>/dev/null | tr -d '\r' 2>/dev/null | tr -d '\r'
)" )"
PORT_REPORT="$(ss -ltnH 2>/dev/null | awk '
{
local_addr=$4
n=split(local_addr, parts, ":")
if (n < 2) next
port=parts[n]
host=substr(local_addr, 1, length(local_addr) - length(port) - 1)
gsub(/^\[/, "", host)
gsub(/\]$/, "", host)
key=port SUBSEP host
if (!seen[key]++) {
hosts[port] = hosts[port] ? hosts[port] "," host : host
}
}
END {
for (port in hosts) {
print port "|" hosts[port]
}
}
' | sort -n)"
ACTIVE_PORTS="$(printf '%s\n' "$PORT_REPORT" | awk -F'|' '
NF >= 2 {
if ($2 !~ /(^|,)(127\.0\.0\.1|::1|localhost)(,|$)/ || $2 ~ /(^|,)(0\.0\.0\.0|\*|::)(,|$)/) {
ports = ports ? ports "," $1 : $1
}
}
END { print ports }
')"
LOOPBACK_ONLY_PORTS="$(printf '%s\n' "$PORT_REPORT" | awk -F'|' '
NF >= 2 {
if ($2 ~ /(^|,)(127\.0\.0\.1|::1|localhost)(,|$)/ && $2 !~ /(^|,)(0\.0\.0\.0|\*|::)(,|$)/) {
ports = ports ? ports "," $1 : $1
}
}
END { print ports }
')"
cat <<EOF cat <<EOF
WSL 앱 정보 WSL 앱 정보
- WSL 내부 IP: ${WSL_IP} - WSL 내부 IP: ${WSL_IP}
- Windows 호스트와 연결된 주소 추정값: ${WSL_HOST_IP} - Windows 호스트와 연결된 주소 추정값: ${WSL_HOST_IP}
- Windows IPv4 후보: ${WINDOWS_IPS:-확인 실패} - Windows IPv4 후보: ${WINDOWS_IPS:-확인 실패}
- 포트 배치: - 현재 내부망 공개 가능 포트: ${ACTIVE_PORTS:-없음}
- hm-biz-process: 8000 - 루프백 전용 포트: ${LOOPBACK_ONLY_PORTS:-없음}
- my-intranet-app: 8010
- b17301: 8020
중요 중요
- WSL2에서는 다른 PC가 WSL 내부 IP(${WSL_IP})로 직접 접속하지 못하는 경우가 많습니다. - WSL2에서는 다른 PC가 WSL 내부 IP(${WSL_IP})로 직접 접속하지 못하는 경우가 많습니다.
- 내부망 다른 PC에서는 보통 Windows 호스트 IP:8010 으로 접속해야 합니다. - 내부망 다른 PC에서는 Windows 호스트 IP와 각 포트로 접속해야 합니다.
- 따라서 Windows 관리자 PowerShell에서 portproxy + 방화벽 규칙을 설정해야 합니다. - 따라서 Windows 관리자 PowerShell에서 portproxy + 방화벽 규칙을 설정해야 합니다.
- 루프백 전용 포트(127.0.0.1/::1)는 앱을 0.0.0.0으로 실행해야 내부망 공개가 가능합니다.
다음 단계 다음 단계
1. WSL에서 my-intranet-app 실행: ./scripts/run_server.sh 8010 1. WSL에서 필요한 앱 실행
2. Windows 관리자 PowerShell 또는 CMD에서 scripts/setup_windows_all_portproxies 실행 2. Windows 관리자 PowerShell 또는 CMD에서 scripts/setup_windows_all_portproxies.cmd 실행
3. 다른 PC 브라우저에서 http://Windows호스트IP:8010 접속 3. 다른 PC 브라우저에서 http://Windows호스트IP:포트 로 접속
EOF EOF
+178 -20
View File
@@ -72,10 +72,12 @@ CHROME_PROFILE_NAME = "Default"
CHROME_DEBUGGER_ADDRESS = os.environ.get("WEHAGO_CHROME_DEBUGGER_ADDRESS", "").strip() CHROME_DEBUGGER_ADDRESS = os.environ.get("WEHAGO_CHROME_DEBUGGER_ADDRESS", "").strip()
SEQUENTIAL_ACCOUNT_MODE = os.environ.get("WEHAGO_SEQUENTIAL_ACCOUNT_MODE", "").strip().lower() in {"1", "true", "yes", "y"} SEQUENTIAL_ACCOUNT_MODE = os.environ.get("WEHAGO_SEQUENTIAL_ACCOUNT_MODE", "").strip().lower() in {"1", "true", "yes", "y"}
SCAN_EXISTING_ACCOUNT_MODE = os.environ.get("WEHAGO_SCAN_EXISTING_ACCOUNT_MODE", "").strip().lower() in {"1", "true", "yes", "y"} SCAN_EXISTING_ACCOUNT_MODE = os.environ.get("WEHAGO_SCAN_EXISTING_ACCOUNT_MODE", "").strip().lower() in {"1", "true", "yes", "y"}
SCAN_ALL_VISIBLE_ACCOUNT_MODE = os.environ.get("WEHAGO_SCAN_ALL_VISIBLE_ACCOUNT_MODE", "").strip().lower() in {"1", "true", "yes", "y"}
FORCE_REDOWNLOAD = os.environ.get("WEHAGO_FORCE_REDOWNLOAD", "").strip().lower() in {"1", "true", "yes", "y"} FORCE_REDOWNLOAD = os.environ.get("WEHAGO_FORCE_REDOWNLOAD", "").strip().lower() in {"1", "true", "yes", "y"}
REFRESH_BEFORE_RUN = os.environ.get("WEHAGO_REFRESH_BEFORE_RUN", "").strip().lower() in {"1", "true", "yes", "y"} REFRESH_BEFORE_RUN = os.environ.get("WEHAGO_REFRESH_BEFORE_RUN", "").strip().lower() in {"1", "true", "yes", "y"}
ALLOW_UNCHANGED_DETAIL = os.environ.get("WEHAGO_ALLOW_UNCHANGED_DETAIL", "").strip().lower() in {"1", "true", "yes", "y"} ALLOW_UNCHANGED_DETAIL = os.environ.get("WEHAGO_ALLOW_UNCHANGED_DETAIL", "").strip().lower() in {"1", "true", "yes", "y"}
OPEN_ACCOUNT_LEDGER_URL = os.environ.get("WEHAGO_OPEN_ACCOUNT_LEDGER_URL", "").strip().lower() in {"1", "true", "yes", "y"} OPEN_ACCOUNT_LEDGER_URL = os.environ.get("WEHAGO_OPEN_ACCOUNT_LEDGER_URL", "").strip().lower() in {"1", "true", "yes", "y"}
MERGED_FILENAME = os.environ.get("WEHAGO_MERGED_FILENAME", "").strip()
# 다운로드 폴더입니다. 결과 파일은 이 폴더 안에 계정코드_계정명.xlsx 형태로 저장됩니다. # 다운로드 폴더입니다. 결과 파일은 이 폴더 안에 계정코드_계정명.xlsx 형태로 저장됩니다.
DATA_DOWNLOAD_DIR = BASE_DIR / "data_download" DATA_DOWNLOAD_DIR = BASE_DIR / "data_download"
@@ -1546,12 +1548,23 @@ def find_left_account_grid(driver: WebDriver) -> WebElement | None:
def scroll_left_account_list(driver: WebDriver) -> bool: def scroll_left_account_list(driver: WebDriver) -> bool:
grid = find_left_account_grid(driver) grid = find_left_account_grid(driver)
if grid is None:
return False
try: try:
before = visible_left_account_codes(driver) before = visible_left_account_codes(driver)
if grid is not None:
cdp_wheel_element(driver, grid, ACCOUNT_SCROLL_PIXELS) cdp_wheel_element(driver, grid, ACCOUNT_SCROLL_PIXELS)
else:
driver.execute_cdp_cmd(
"Input.dispatchMouseEvent",
{"type": "mouseWheel", "x": 250, "y": 500, "deltaX": 0, "deltaY": ACCOUNT_SCROLL_PIXELS},
)
time.sleep(ACCOUNT_SCROLL_SETTLE_SECONDS)
after = visible_left_account_codes(driver)
if after == before:
driver.execute_cdp_cmd(
"Input.dispatchMouseEvent",
{"type": "mouseWheel", "x": 250, "y": 500, "deltaX": 0, "deltaY": ACCOUNT_SCROLL_PIXELS},
)
time.sleep(ACCOUNT_SCROLL_SETTLE_SECONDS) time.sleep(ACCOUNT_SCROLL_SETTLE_SECONDS)
after = visible_left_account_codes(driver) after = visible_left_account_codes(driver)
return after != before return after != before
@@ -1560,16 +1573,49 @@ def scroll_left_account_list(driver: WebDriver) -> bool:
def reset_left_account_list_scroll(driver: WebDriver) -> None: def reset_left_account_list_scroll(driver: WebDriver) -> None:
drag_left_realgrid_scrollbar_to_fraction(driver, 0.0)
grid = find_left_account_grid(driver) grid = find_left_account_grid(driver)
if grid is None: if grid is None:
return return
try: try:
cdp_wheel_element(driver, grid, -10000) cdp_click_element(driver, grid)
time.sleep(ACCOUNT_SCROLL_SETTLE_SECONDS)
except WebDriverException: except WebDriverException:
pass pass
for _ in range(3):
try:
driver.execute_cdp_cmd("Input.dispatchKeyEvent", {"type": "keyDown", "key": "Home", "code": "Home"})
driver.execute_cdp_cmd("Input.dispatchKeyEvent", {"type": "keyUp", "key": "Home", "code": "Home"})
time.sleep(ACCOUNT_SCROLL_SETTLE_SECONDS)
except WebDriverException:
break
previous_first_code = ""
for _ in range(8):
try:
visible_codes = visible_left_account_codes(driver)
current_first_code = visible_codes[0] if visible_codes else ""
cdp_wheel_element(driver, grid, -6000)
time.sleep(ACCOUNT_SCROLL_SETTLE_SECONDS)
visible_codes = visible_left_account_codes(driver)
next_first_code = visible_codes[0] if visible_codes else ""
except WebDriverException:
break
if next_first_code and next_first_code == previous_first_code:
break
previous_first_code = next_first_code
try:
if next_first_code and int(next_first_code) <= 103:
break
if current_first_code and next_first_code and int(next_first_code) >= int(current_first_code):
break
except ValueError:
continue
def jump_left_account_list_near(driver: WebDriver, account: Account) -> None: def jump_left_account_list_near(driver: WebDriver, account: Account) -> None:
"""계정 코드 순번으로 가상 스크롤 위치를 대략 맞춰 긴 순차 스크롤을 줄입니다.""" """계정 코드 순번으로 가상 스크롤 위치를 대략 맞춰 긴 순차 스크롤을 줄입니다."""
@@ -1672,19 +1718,23 @@ def advance_to_next_existing_left_account(driver: WebDriver, previous_code: str)
def click_and_confirm(candidate_code: str) -> Account | None: def click_and_confirm(candidate_code: str) -> Account | None:
if candidate_code == previous_code: if candidate_code == previous_code:
return None return None
before_detail = detail_grid_signature(driver)
if not click_left_account_text(driver, Account(candidate_code, "")): if not click_left_account_text(driver, Account(candidate_code, "")):
return None return None
time.sleep(DELAY_AFTER_ACCOUNT_CLICK_SECONDS) time.sleep(DELAY_AFTER_ACCOUNT_CLICK_SECONDS)
wait_for_blocking_overlay_gone(driver) wait_for_blocking_overlay_gone(driver)
try: try:
wait_for_detail_data_cell(driver, timeout=DETAIL_ROW_WAIT_SECONDS) wait_for_detail_change(
driver,
before_detail,
expected_account=Account(candidate_code, ""),
timeout=max(DETAIL_CHANGE_WAIT_SECONDS, DETAIL_ROW_WAIT_SECONDS),
)
except TimeoutException:
return None
try:
return assert_selected_account(driver, Account(candidate_code, ""), timeout=0.7)
except TimeoutException: except TimeoutException:
pass
selected = selected_left_account(driver)
if selected is not None and selected.code != previous_code:
return selected
if selected is not None and selected.code == candidate_code:
return selected
return None return None
previous_num: int | None previous_num: int | None
@@ -1945,6 +1995,11 @@ def wait_for_detail_change(
return return
if expected_account is not None and current_signature and find_detail_data_cell(driver) is not None: if expected_account is not None and current_signature and find_detail_data_cell(driver) is not None:
selected = selected_left_account(driver) selected = selected_left_account(driver)
if selected is None:
try:
selected = assert_selected_account(driver, expected_account, timeout=0.2)
except TimeoutException:
selected = recently_clicked_left_account(driver, expected_account)
if selected is not None and selected.code == expected_account.code: if selected is not None and selected.code == expected_account.code:
log( log(
f"{expected_account.code} {expected_account.name}: 선택 계정 확인 완료. " f"{expected_account.code} {expected_account.name}: 선택 계정 확인 완료. "
@@ -2184,6 +2239,19 @@ def account_with_inferred_name(account: Account, path: Path | None = None) -> Ac
return account return account
def is_generated_output_file(path: Path) -> bool:
stem = path.stem.strip()
if not stem:
return True
generated_markers = [
"계정별원장_취합",
"ledger_계정별원장",
"ledger_merge",
"resume2025",
]
return any(marker in stem for marker in generated_markers)
def download_scan_dirs(download_dir: Path) -> list[Path]: def download_scan_dirs(download_dir: Path) -> list[Path]:
dirs = [download_dir] dirs = [download_dir]
if FALLBACK_DOWNLOAD_DIR.resolve() != download_dir.resolve(): if FALLBACK_DOWNLOAD_DIR.resolve() != download_dir.resolve():
@@ -2439,15 +2507,62 @@ def build_summary_rows(
return rows return rows
def discover_downloaded_accounts(download_dir: Path) -> list[Account]:
discovered: dict[str, Account] = {}
for path in sorted(download_dir.glob("*")):
if not path.is_file():
continue
if path.suffix.lower() not in {".xlsx", ".xls", ".csv"}:
continue
stem = path.stem.strip()
if not stem or stem.startswith("ledger_"):
continue
if "계정별원장_취합" in stem or "ledger_merge" in stem or stem.startswith("resume2025"):
continue
if "_" not in stem:
continue
code, name = stem.split("_", 1)
code = code.strip()
name = name.strip()
if not code.isdigit():
continue
discovered[code] = Account(code, name)
return list(discovered.values())
def verify_download_coverage(
expected_accounts: list[Account],
download_dir: Path,
skipped: list[tuple[Account, str]] | None = None,
) -> tuple[list[Account], list[Account]]:
skipped_codes = {account.code for account, _ in (skipped or [])}
expected_by_code = {account.code: account for account in expected_accounts if account.code not in skipped_codes}
downloaded_by_code = {account.code: account for account in discover_downloaded_accounts(download_dir)}
missing = [
expected_by_code[code]
for code in sorted(expected_by_code.keys(), key=lambda value: int(value))
if code not in downloaded_by_code
]
extra = [
downloaded_by_code[code]
for code in sorted(downloaded_by_code.keys(), key=lambda value: int(value))
if code not in expected_by_code
]
return missing, extra
def consolidate_downloads( def consolidate_downloads(
accounts: list[Account], accounts: list[Account],
download_dir: Path, download_dir: Path,
failures: list[tuple[Account, str]] | None = None, failures: list[tuple[Account, str]] | None = None,
skipped: list[tuple[Account, str]] | None = None, skipped: list[tuple[Account, str]] | None = None,
output_path: Path | None = None,
) -> Path: ) -> Path:
failures = failures or [] failures = failures or []
skipped = skipped or [] skipped = skipped or []
output_path = download_dir / f"ledger_계정별원장_{datetime.now():%Y%m%d_%H%M%S}.xlsx" if output_path is None:
output_path = download_dir / (MERGED_FILENAME or f"ledger_계정별원장_{datetime.now():%Y%m%d_%H%M%S}.xlsx")
summary_rows = build_summary_rows(accounts, download_dir, failures, skipped) summary_rows = build_summary_rows(accounts, download_dir, failures, skipped)
summary_by_code = {str(row["계정코드"]): row for row in summary_rows} summary_by_code = {str(row["계정코드"]): row for row in summary_rows}
@@ -2519,6 +2634,19 @@ def consolidate_downloads(
return output_path return output_path
def consolidate_download_dir(
download_dir: Path,
failures: list[tuple[Account, str]] | None = None,
skipped: list[tuple[Account, str]] | None = None,
output_name: str | None = None,
) -> Path:
accounts = discover_downloaded_accounts(download_dir)
if not accounts:
raise ValueError(f"취합할 계정별원장 파일이 없습니다: {download_dir}")
output_path = download_dir / output_name if output_name else None
return consolidate_downloads(accounts, download_dir, failures, skipped, output_path=output_path)
def merge_scope_accounts(all_accounts: list[Account]) -> list[Account]: def merge_scope_accounts(all_accounts: list[Account]) -> list[Account]:
"""취합 대상은 계정코드 100 이상 전체로 고정합니다.""" """취합 대상은 계정코드 100 이상 전체로 고정합니다."""
scoped: list[Account] = [] scoped: list[Account] = []
@@ -2757,7 +2885,7 @@ def download_account_ledger(driver: WebDriver, account: Account, index: int, tot
return renamed return renamed
def download_selected_account_ledger(driver: WebDriver, account: Account, index: int, total: int) -> Path: def download_selected_account_ledger(driver: WebDriver, account: Account, index: int, total: int | str) -> Path:
started_at = time.perf_counter() started_at = time.perf_counter()
close_open_menus(driver) close_open_menus(driver)
wait_for_blocking_overlay_gone(driver) wait_for_blocking_overlay_gone(driver)
@@ -2786,9 +2914,18 @@ def run_scan_existing_accounts(
selected_accounts: list[Account], selected_accounts: list[Account],
failures: list[tuple[Account, str]], failures: list[tuple[Account, str]],
skipped_accounts: list[tuple[Account, str]], skipped_accounts: list[tuple[Account, str]],
) -> None: ) -> list[Account]:
if SCAN_ALL_VISIBLE_ACCOUNT_MODE:
available_codes = sorted(ensure_available_account_codes(driver), key=lambda value: int(value) if value.isdigit() else value)
if available_codes:
log(
f"메모리에서 읽은 계정코드 {len(available_codes)}개는 참고만 하고, "
"누락 방지를 위해 맨 위부터 실제 화면 스캔 방식으로 전체 계정을 순차 다운로드합니다."
)
target_by_code = {account.code: account for account in selected_accounts} target_by_code = {account.code: account for account in selected_accounts}
seen_codes: set[str] = set() seen_codes: set[str] = set()
seen_accounts: dict[str, Account] = {}
expected_existing_count = len(target_by_code) expected_existing_count = len(target_by_code)
reset_left_account_list_scroll(driver) reset_left_account_list_scroll(driver)
selected = select_first_existing_left_account(driver) selected = select_first_existing_left_account(driver)
@@ -2805,17 +2942,27 @@ def run_scan_existing_accounts(
else: else:
no_move_count = 0 no_move_count = 0
seen_codes.add(code) seen_codes.add(code)
seen_accounts[code] = Account(selected.code, selected.name)
target = target_by_code.get(code) target = target_by_code.get(code)
if target is not None: if SCAN_ALL_VISIBLE_ACCOUNT_MODE:
account = Account(selected.code, selected.name)
display_total: int | str = "?"
elif target is not None:
account = Account(target.code, target.name or selected.name) account = Account(target.code, target.name or selected.name)
display_total = expected_existing_count
else:
account = None
display_total = expected_existing_count
if account is not None:
existing_file = find_account_file(account, DOWNLOAD_DIR) existing_file = find_account_file(account, DOWNLOAD_DIR)
if SKIP_ALREADY_DOWNLOADED and existing_file is not None: if SKIP_ALREADY_DOWNLOADED and existing_file is not None:
add_account_columns_to_excel(existing_file, account) add_account_columns_to_excel(existing_file, account)
log(f"[{row_number}/{expected_existing_count}] {account.code} {account.name}: 이미 있음, 건너뜀") log(f"[{row_number}/{display_total}] {account.code} {account.name}: 이미 있음, 건너뜀")
else: else:
try: try:
download_selected_account_ledger(driver, account, row_number, expected_existing_count) download_selected_account_ledger(driver, account, row_number, display_total)
except Exception as exc: except Exception as exc:
message = f"{type(exc).__name__}: {exc}" message = f"{type(exc).__name__}: {exc}"
failures.append((account, message)) failures.append((account, message))
@@ -2831,9 +2978,14 @@ def run_scan_existing_accounts(
break break
selected = next_selected selected = next_selected
if SCAN_ALL_VISIBLE_ACCOUNT_MODE:
return list(seen_accounts.values())
for account in selected_accounts: for account in selected_accounts:
if account.code not in seen_codes: if account.code not in seen_codes:
skipped_accounts.append((account, "현재 2022년 계정별원장 좌측 실제 계정 목록에 표시되지 않았습니다.")) skipped_accounts.append((account, "현재 계정별원장 좌측 실제 계정 목록에 표시되지 않았습니다."))
return list(seen_accounts.values())
def run( def run(
@@ -2862,11 +3014,17 @@ def run(
total = len(selected_accounts) total = len(selected_accounts)
if SCAN_EXISTING_ACCOUNT_MODE: if SCAN_EXISTING_ACCOUNT_MODE:
log("좌측 실제 계정 목록을 순차 스캔하는 방식으로 다운로드합니다.") log("좌측 실제 계정 목록을 순차 스캔하는 방식으로 다운로드합니다.")
run_scan_existing_accounts(driver, accounts, selected_accounts, failures, skipped_accounts) scanned_accounts = run_scan_existing_accounts(driver, accounts, selected_accounts, failures, skipped_accounts)
expected_accounts = scanned_accounts if SCAN_ALL_VISIBLE_ACCOUNT_MODE and scanned_accounts else merge_scope_accounts(accounts)
missing_after_run, extra_after_run = verify_download_coverage(expected_accounts, DOWNLOAD_DIR, skipped_accounts)
for account in missing_after_run:
failures.append((account, "화면에서 확인된 계정인데 다운로드 파일이 생성되지 않았습니다."))
if extra_after_run:
log("검증 참고: 기대 목록 밖의 파일이 있어도 보존합니다: " + ", ".join(f"{account.code} {account.name}" for account in extra_after_run))
log(f"작업 완료. 저장 위치: {DOWNLOAD_DIR.resolve()}") log(f"작업 완료. 저장 위치: {DOWNLOAD_DIR.resolve()}")
if merge: if merge:
log("다운로드 파일 취합을 시작합니다.") log("다운로드 파일 취합을 시작합니다.")
merged_file = consolidate_downloads(merge_scope_accounts(accounts), DOWNLOAD_DIR, failures, skipped_accounts) merged_file = consolidate_downloads(expected_accounts, DOWNLOAD_DIR, failures, skipped_accounts)
log(f"통합 엑셀 생성 완료: {merged_file.resolve()}") log(f"통합 엑셀 생성 완료: {merged_file.resolve()}")
if failures: if failures:
should_keep_browser_open = True should_keep_browser_open = True