Add data refresh assets and local access scripts
This commit is contained in:
@@ -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,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
|
||||||
|
|||||||
@@ -5,11 +5,81 @@ 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
|
||||||
@@ -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,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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,30 +1548,74 @@ 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)
|
||||||
cdp_wheel_element(driver, grid, ACCOUNT_SCROLL_PIXELS)
|
if grid is not None:
|
||||||
|
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)
|
time.sleep(ACCOUNT_SCROLL_SETTLE_SECONDS)
|
||||||
after = visible_left_account_codes(driver)
|
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)
|
||||||
|
after = visible_left_account_codes(driver)
|
||||||
return after != before
|
return after != before
|
||||||
except WebDriverException:
|
except WebDriverException:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
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,20 +1718,24 @@ 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:
|
except TimeoutException:
|
||||||
pass
|
return None
|
||||||
selected = selected_left_account(driver)
|
try:
|
||||||
if selected is not None and selected.code != previous_code:
|
return assert_selected_account(driver, Account(candidate_code, ""), timeout=0.7)
|
||||||
return selected
|
except TimeoutException:
|
||||||
if selected is not None and selected.code == candidate_code:
|
return None
|
||||||
return selected
|
|
||||||
return None
|
|
||||||
|
|
||||||
previous_num: int | None
|
previous_num: int | None
|
||||||
try:
|
try:
|
||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user