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_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_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"
|
||||
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_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_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"
|
||||
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_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_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"
|
||||
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
|
||||
if not "%errorlevel%"=="0" (
|
||||
echo This script must be run as Administrator.
|
||||
echo.
|
||||
echo 1. Open Windows Start menu.
|
||||
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
|
||||
echo Requesting Administrator privileges...
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -Command "Start-Process -FilePath '%~f0' -ArgumentList '%*' -Verb RunAs"
|
||||
exit /b %errorlevel%
|
||||
)
|
||||
|
||||
set SCRIPT_DIR=%~dp0
|
||||
|
||||
@@ -5,11 +5,81 @@ param(
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$apps = @(
|
||||
@{ Name = "hm-biz-process"; Port = 8000; RuleName = "HM-BIZ-PROCESS-8000" },
|
||||
@{ Name = "my-intranet-app"; Port = 8010; RuleName = "MyIntranetApp-8010" },
|
||||
@{ Name = "b17301"; Port = 8020; RuleName = "B17301-8020" }
|
||||
)
|
||||
$knownPorts = @{
|
||||
8000 = @{ Name = "hm-biz-process"; RuleName = "HM-BIZ-PROCESS-8000" }
|
||||
8010 = @{ Name = "my-intranet-app"; RuleName = "MyIntranetApp-8010" }
|
||||
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)) {
|
||||
$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
|
||||
|
||||
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"
|
||||
$portEntries = @(Get-WslListeningPorts)
|
||||
if (-not $portEntries -or $portEntries.Count -eq 0) {
|
||||
throw "외부에서 접근 가능한 WSL TCP 포트를 찾지 못했습니다."
|
||||
}
|
||||
|
||||
foreach ($app in $apps) {
|
||||
$port = [int]$app.Port
|
||||
$ruleName = [string]$app.RuleName
|
||||
Write-Host "Detected ports: $((@($portEntries | ForEach-Object { $_.Port })) -join ', ')"
|
||||
|
||||
$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 "$($app.Name): $port" -ForegroundColor Cyan
|
||||
Write-Host "$displayName : $port ($($entry.Hosts -join ', '))" -ForegroundColor Cyan
|
||||
|
||||
foreach ($address in $listenAddresses) {
|
||||
netsh interface portproxy delete v4tov4 listenport=$port listenaddress=$address | Out-Null
|
||||
@@ -63,6 +150,10 @@ foreach ($app in $apps) {
|
||||
-Action Allow `
|
||||
-Protocol TCP `
|
||||
-LocalPort $port | Out-Null
|
||||
|
||||
if (-not $configuredPorts.Contains($port)) {
|
||||
$configuredPorts.Add($port)
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
@@ -70,14 +161,27 @@ Write-Host "Completed." -ForegroundColor Green
|
||||
Write-Host "Use these URLs from another PC on the intranet:"
|
||||
if ($windowsIps) {
|
||||
foreach ($ip in $windowsIps) {
|
||||
Write-Host "hm-biz-process : http://$ip`:8000"
|
||||
Write-Host "my-intranet-app: http://$ip`:8010"
|
||||
Write-Host "b17301 : http://$ip`:8020"
|
||||
foreach ($port in $configuredPorts) {
|
||||
$appInfo = $knownPorts[$port]
|
||||
$label = if ($appInfo) { [string]$appInfo.Name } else { "wsl-port-$port" }
|
||||
Write-Host ("{0,-15}: http://{1}:{2}" -f $label, $ip, $port)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Write-Host "hm-biz-process : http://<Windows-IP>:8000"
|
||||
Write-Host "my-intranet-app: http://<Windows-IP>:8010"
|
||||
Write-Host "b17301 : http://<Windows-IP>:8020"
|
||||
foreach ($port in $configuredPorts) {
|
||||
$appInfo = $knownPorts[$port]
|
||||
$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 "Tip: if WSL restarts and its IP changes, run this script again."
|
||||
|
||||
@@ -3,16 +3,9 @@ setlocal
|
||||
|
||||
net session >nul 2>&1
|
||||
if not "%errorlevel%"=="0" (
|
||||
echo This script must be run as Administrator.
|
||||
echo.
|
||||
echo 1. Open Windows Start menu.
|
||||
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
|
||||
echo Requesting Administrator privileges...
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -Command "Start-Process -FilePath '%~f0' -ArgumentList '%*' -Verb RunAs"
|
||||
exit /b %errorlevel%
|
||||
)
|
||||
|
||||
set SCRIPT_DIR=%~dp0
|
||||
|
||||
@@ -8,23 +8,61 @@ WINDOWS_IPS="$(
|
||||
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
|
||||
WSL 앱 정보
|
||||
- WSL 내부 IP: ${WSL_IP}
|
||||
- Windows 호스트와 연결된 주소 추정값: ${WSL_HOST_IP}
|
||||
- Windows IPv4 후보: ${WINDOWS_IPS:-확인 실패}
|
||||
- 포트 배치:
|
||||
- hm-biz-process: 8000
|
||||
- my-intranet-app: 8010
|
||||
- b17301: 8020
|
||||
- 현재 내부망 공개 가능 포트: ${ACTIVE_PORTS:-없음}
|
||||
- 루프백 전용 포트: ${LOOPBACK_ONLY_PORTS:-없음}
|
||||
|
||||
중요
|
||||
- WSL2에서는 다른 PC가 WSL 내부 IP(${WSL_IP})로 직접 접속하지 못하는 경우가 많습니다.
|
||||
- 내부망 다른 PC에서는 보통 Windows 호스트 IP:8010 으로 접속해야 합니다.
|
||||
- 내부망 다른 PC에서는 Windows 호스트 IP와 각 포트로 접속해야 합니다.
|
||||
- 따라서 Windows 관리자 PowerShell에서 portproxy + 방화벽 규칙을 설정해야 합니다.
|
||||
- 루프백 전용 포트(127.0.0.1/::1)는 앱을 0.0.0.0으로 실행해야 내부망 공개가 가능합니다.
|
||||
|
||||
다음 단계
|
||||
1. WSL에서 my-intranet-app 실행: ./scripts/run_server.sh 8010
|
||||
2. Windows 관리자 PowerShell 또는 CMD에서 scripts/setup_windows_all_portproxies 실행
|
||||
3. 다른 PC 브라우저에서 http://Windows호스트IP:8010 접속
|
||||
1. WSL에서 필요한 앱 실행
|
||||
2. Windows 관리자 PowerShell 또는 CMD에서 scripts/setup_windows_all_portproxies.cmd 실행
|
||||
3. 다른 PC 브라우저에서 http://Windows호스트IP:포트 로 접속
|
||||
EOF
|
||||
|
||||
@@ -72,10 +72,12 @@ CHROME_PROFILE_NAME = "Default"
|
||||
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"}
|
||||
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"}
|
||||
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"}
|
||||
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 형태로 저장됩니다.
|
||||
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:
|
||||
grid = find_left_account_grid(driver)
|
||||
if grid is None:
|
||||
return False
|
||||
|
||||
try:
|
||||
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)
|
||||
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
|
||||
except WebDriverException:
|
||||
return False
|
||||
|
||||
|
||||
def reset_left_account_list_scroll(driver: WebDriver) -> None:
|
||||
drag_left_realgrid_scrollbar_to_fraction(driver, 0.0)
|
||||
|
||||
grid = find_left_account_grid(driver)
|
||||
if grid is None:
|
||||
return
|
||||
|
||||
try:
|
||||
cdp_wheel_element(driver, grid, -10000)
|
||||
time.sleep(ACCOUNT_SCROLL_SETTLE_SECONDS)
|
||||
cdp_click_element(driver, grid)
|
||||
except WebDriverException:
|
||||
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:
|
||||
"""계정 코드 순번으로 가상 스크롤 위치를 대략 맞춰 긴 순차 스크롤을 줄입니다."""
|
||||
@@ -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:
|
||||
if candidate_code == previous_code:
|
||||
return None
|
||||
before_detail = detail_grid_signature(driver)
|
||||
if not click_left_account_text(driver, Account(candidate_code, "")):
|
||||
return None
|
||||
time.sleep(DELAY_AFTER_ACCOUNT_CLICK_SECONDS)
|
||||
wait_for_blocking_overlay_gone(driver)
|
||||
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:
|
||||
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
|
||||
try:
|
||||
return assert_selected_account(driver, Account(candidate_code, ""), timeout=0.7)
|
||||
except TimeoutException:
|
||||
return None
|
||||
|
||||
previous_num: int | None
|
||||
try:
|
||||
@@ -1945,6 +1995,11 @@ def wait_for_detail_change(
|
||||
return
|
||||
if expected_account is not None and current_signature and find_detail_data_cell(driver) is not None:
|
||||
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:
|
||||
log(
|
||||
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
|
||||
|
||||
|
||||
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]:
|
||||
dirs = [download_dir]
|
||||
if FALLBACK_DOWNLOAD_DIR.resolve() != download_dir.resolve():
|
||||
@@ -2439,15 +2507,62 @@ def build_summary_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(
|
||||
accounts: list[Account],
|
||||
download_dir: Path,
|
||||
failures: list[tuple[Account, str]] | None = None,
|
||||
skipped: list[tuple[Account, str]] | None = None,
|
||||
output_path: Path | None = None,
|
||||
) -> Path:
|
||||
failures = failures 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_by_code = {str(row["계정코드"]): row for row in summary_rows}
|
||||
|
||||
@@ -2519,6 +2634,19 @@ def consolidate_downloads(
|
||||
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]:
|
||||
"""취합 대상은 계정코드 100 이상 전체로 고정합니다."""
|
||||
scoped: list[Account] = []
|
||||
@@ -2757,7 +2885,7 @@ def download_account_ledger(driver: WebDriver, account: Account, index: int, tot
|
||||
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()
|
||||
close_open_menus(driver)
|
||||
wait_for_blocking_overlay_gone(driver)
|
||||
@@ -2786,9 +2914,18 @@ def run_scan_existing_accounts(
|
||||
selected_accounts: list[Account],
|
||||
failures: 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}
|
||||
seen_codes: set[str] = set()
|
||||
seen_accounts: dict[str, Account] = {}
|
||||
expected_existing_count = len(target_by_code)
|
||||
reset_left_account_list_scroll(driver)
|
||||
selected = select_first_existing_left_account(driver)
|
||||
@@ -2805,17 +2942,27 @@ def run_scan_existing_accounts(
|
||||
else:
|
||||
no_move_count = 0
|
||||
seen_codes.add(code)
|
||||
seen_accounts[code] = Account(selected.code, selected.name)
|
||||
|
||||
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)
|
||||
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)
|
||||
if SKIP_ALREADY_DOWNLOADED and existing_file is not None:
|
||||
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:
|
||||
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:
|
||||
message = f"{type(exc).__name__}: {exc}"
|
||||
failures.append((account, message))
|
||||
@@ -2831,9 +2978,14 @@ def run_scan_existing_accounts(
|
||||
break
|
||||
selected = next_selected
|
||||
|
||||
if SCAN_ALL_VISIBLE_ACCOUNT_MODE:
|
||||
return list(seen_accounts.values())
|
||||
|
||||
for account in selected_accounts:
|
||||
if account.code not in seen_codes:
|
||||
skipped_accounts.append((account, "현재 2022년 계정별원장 좌측 실제 계정 목록에 표시되지 않았습니다."))
|
||||
skipped_accounts.append((account, "현재 계정별원장 좌측 실제 계정 목록에 표시되지 않았습니다."))
|
||||
|
||||
return list(seen_accounts.values())
|
||||
|
||||
|
||||
def run(
|
||||
@@ -2862,11 +3014,17 @@ def run(
|
||||
total = len(selected_accounts)
|
||||
if SCAN_EXISTING_ACCOUNT_MODE:
|
||||
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()}")
|
||||
if merge:
|
||||
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()}")
|
||||
if failures:
|
||||
should_keep_browser_open = True
|
||||
|
||||
Reference in New Issue
Block a user