@echo off
rem Blog program installer/updater - ONE file. The PowerShell part starts at the PS-START marker line below (read as text, never run as a .ps1 file).
powershell -NoProfile -ExecutionPolicy Bypass -Command "$t=[IO.File]::ReadAllText('%~f0',[Text.Encoding]::UTF8); $i=$t.IndexOf('#PS-START'+'#'); if ($i -lt 0) { Write-Host 'install.bat is damaged - please download it again'; exit 1 }; . ([ScriptBlock]::Create($t.Substring($i)))"
pause
exit /b
#PS-START#
# 사장님 PC 프로그램 — 윈도우 한 번에 설치·업데이트 (2026-09-29, 문제기록 #80)
# 사용법: install.bat 을 두 번 클릭. 다시 실행하면 최신 버전으로 갱신됩니다(글·로그인 기록은 C:\BlogPC\data 에 그대로).
# ⚠️ 파일 하나(install.bat) 안에 cmd 머리말 + 이 파워셸 내용이 같이 있다. 실측(집 윈도우 PC 2026-09-29): 크롬으로 받은 별도 install.ps1 은
#   AhnLab V3 Lite 가 "인터넷에서 받은 새 파일"로 붙잡아 한 줄도 못 돌았다(드라이브 폴더의 같은 파일은 됨) → .ps1 파일을 실행하지 않고, 머리말이
#   이 파일을 글자로 읽어 PS-START 표시 줄부터 파워셸로 돌린다. 두 파일·같은 폴더·"install (1)" 이름 함정도 같이 없어진다.
#   머리말(맨 위 5줄)은 영어·ASCII 만(cmd 가 한글 코드로 읽는다), 파일은 BOM 없는 UTF-8 + CRLF.
# 이 PC 에 두는 것: Node.js, 프로그램(C:\BlogPC\app), 설정 4줄(.env.local). 우리 열쇠(AI·검색광고·팀 계정)는 하나도 안 둔다 — 글쓰기는 웹사이트 서버가 한다.
$ErrorActionPreference = "Stop"
trap { Write-Host "`n오류: $($_.Exception.Message)" -ForegroundColor Red; Write-Host "이 화면을 캡처해 담당자에게 보내 주세요." -ForegroundColor Yellow; Read-Host "Enter 를 누르면 닫힙니다"; exit 1 }
$Root = "C:\BlogPC"; $App = "$Root\app"; $New = "$Root\app.new"; $Old = "$Root\app.old"; $Logs = "$Root\logs"; $Port = 4123
function Say($m) { Write-Host "`n■ $m" -ForegroundColor Cyan }
function Has($cmd) { return [bool](Get-Command $cmd -ErrorAction SilentlyContinue) }
Write-Host "블로그 프로그램 설치 파일 v5 (2026-09-30, 한 파일)" -ForegroundColor Green
# 실측(집 윈도우 PC 2026-09-29, #80): 창 안을 누르면 제목이 "선택"이 되고 설치가 그 자리에서 멈춘다(윈도우 빠른 편집) → 이 창만 끈다. 실패해도 설치는 그대로.
#   글자 입력(Read-Host)이 콘솔 설정을 다시 만질 수 있어 입력이 끝난 뒤(2/8 끝)에 한 번 더 부른다.
function NoQuickEdit() { try { if (-not ("BlogPc.Con" -as [type])) { Add-Type -Namespace BlogPc -Name Con -MemberDefinition '[DllImport("kernel32.dll")] public static extern System.IntPtr GetStdHandle(int n); [DllImport("kernel32.dll")] public static extern bool GetConsoleMode(System.IntPtr h, out uint m); [DllImport("kernel32.dll")] public static extern bool SetConsoleMode(System.IntPtr h, uint m);' }; $qh = [BlogPc.Con]::GetStdHandle(-10); $qm = [uint32]0; if ([BlogPc.Con]::GetConsoleMode($qh, [ref]$qm)) { [void][BlogPc.Con]::SetConsoleMode($qh, (($qm -band (-bnot [uint32]0x40)) -bor [uint32]0x80)) } } catch {} }
NoQuickEdit
New-Item -ItemType Directory -Force -Path $Root, $Logs, "$Root\setup" | Out-Null
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12

Say "1/8 준비물 확인 — Node.js (없으면 받기, 1~3분)"
# Node 22 (서버와 같은 줄). PC 에 이미 22 이상이 있으면 그것을 쓴다(대표님 집 PC 실측 — v24).
# 없으면 nodejs.org 의 "휴대용"(zip)을 C:\BlogPC\node 에 풀어 쓴다 — 관리자 허락(UAC)이 필요 없다 (2026-09-30 점검).
#   예전 방식(msi 를 /qn 으로 조용히 설치)은 모든 사용자용 설치라 관리자 권한이 없으면 창 없이 실패할 수 있었고, Node 가 없는 PC 로는 한 번도 시험되지 않았다.
#   start-blog.bat 도 C:\BlogPC\node 가 있으면 그것을 먼저 쓴다. winget 이 없는 PC 도 있다(집 PC 실측 2026-09-19).
$NodeDir = "$Root\node"
function UsePortable() { if ((Test-Path "$NodeDir\node.exe") -and ($env:Path -notlike "$NodeDir;*")) { $env:Path = "$NodeDir;" + $env:Path } }
function NodeMajor() { try { $v = (& node -v) 2>$null; if ($v -match '^v(\d+)') { return [int]$Matches[1] } } catch {} return 0 }
function InstallNode() {
  $idx = Invoke-RestMethod "https://nodejs.org/dist/index.json"
  $v = ($idx | Where-Object { $_.version -like "v22.*" } | Select-Object -First 1).version
  if (-not $v) { $v = ($idx | Where-Object { $_.lts } | Select-Object -First 1).version }
  $zip = Join-Path $env:TEMP "node-$v-win-x64.zip"
  Write-Host "  Node.js $v 받는 중 (약 30MB, 관리자 허락 없이 C:\BlogPC\node 에)"; Invoke-WebRequest -UseBasicParsing "https://nodejs.org/dist/$v/node-$v-win-x64.zip" -OutFile $zip
  if (Test-Path "$Root\node-$v-win-x64") { Remove-Item "$Root\node-$v-win-x64" -Recurse -Force }
  tar -xf $zip -C $Root
  if (Test-Path $NodeDir) { Remove-Item $NodeDir -Recurse -Force }
  Rename-Item "$Root\node-$v-win-x64" $NodeDir
  Remove-Item $zip -Force -ErrorAction SilentlyContinue
  UsePortable
}
UsePortable
if ((NodeMajor) -lt 22) { InstallNode }
if ((NodeMajor) -lt 22) { throw "Node.js 를 준비하지 못했습니다 — 인터넷 연결을 확인하고 install.bat 을 한 번 더 실행해 주세요" }
Write-Host ("  node " + (node -v))

Say "2/8 웹사이트 주소·사장님 계정 (비밀번호는 저장하지 않습니다)"
$ServerFile = "$Root\server.txt"; $Default = "https://52-78-86-17.sslip.io"
if (Test-Path $ServerFile) { $Default = (Get-Content $ServerFile -Raw).Trim() }
$Server = Read-Host "웹사이트 주소 (그냥 Enter 면 $Default)"
if (-not $Server.Trim()) { $Server = $Default }
$Server = $Server.Trim().TrimEnd('/')
if ($Server -notmatch '^https?://') { throw "웹사이트 주소는 https:// 로 시작해야 합니다: $Server" }
$Id = (Read-Host "사장님 아이디").Trim()
$PwSecure = Read-Host "비밀번호 (글자가 화면에 안 보입니다)" -AsSecureString
$PwPlain = [Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToBSTR($PwSecure))
$bodyBytes = [Text.Encoding]::UTF8.GetBytes((@{ id = $Id; password = $PwPlain } | ConvertTo-Json -Compress))
$PwPlain = $null
try { Invoke-WebRequest -UseBasicParsing -Method POST "$Server/api/shop/login" -ContentType "application/json; charset=utf-8" -Body $bodyBytes -SessionVariable S -TimeoutSec 30 | Out-Null }
catch {
  $t = $_.ErrorDetails.Message
  if (-not $t -and $_.Exception.Response) { try { $sr = New-Object IO.StreamReader($_.Exception.Response.GetResponseStream()); $t = $sr.ReadToEnd() } catch {} }
  if ($t) { try { $t = ($t | ConvertFrom-Json).error } catch {}; throw "로그인하지 못했습니다: $t" }
  throw "웹사이트에 연결하지 못했습니다 ($Server) — 인터넷과 주소를 확인해 주세요"
}
$bodyBytes = $null
NoQuickEdit
Set-Content -Path $ServerFile -Value $Server -Encoding ascii
Write-Host "  로그인 OK"

Say "3/8 프로그램 받기"
$zip = "$Root\blog-pc.zip"
Invoke-WebRequest -UseBasicParsing "$Server/api/shop/pc-package" -WebSession $S -OutFile $zip -TimeoutSec 300
$ver = ""; try { $ver = (Invoke-RestMethod "$Server/api/shop/pc-version" -WebSession $S -TimeoutSec 30).version } catch {}
Write-Host ("  받음: " + [math]::Round((Get-Item $zip).Length / 1KB) + " KB" + $(if ($ver) { " · 버전 $ver" } else { "" }))
if (Test-Path $New) { Remove-Item $New -Recurse -Force }
New-Item -ItemType Directory -Force -Path $New | Out-Null
if (Has tar) { tar -xf $zip -C $New } else { Expand-Archive -Path $zip -DestinationPath $New -Force }
if (-not (Test-Path "$New\package.json")) { throw "받은 프로그램 파일이 온전하지 않습니다 (package.json 없음)" }

Say "4/8 설정 쓰기"
# .env.local 은 BOM 없는 UTF-8 이어야 첫 줄이 읽힌다. DATA_DIR 은 프로그램 폴더 밖(업데이트로 app 을 갈아 끼워도 기록이 남게)
$envText = "APP_MODE=pc`nSERVER_URL=$Server`nDATA_DIR=$Root\data`nNEXT_TELEMETRY_DISABLED=1`n"
[IO.File]::WriteAllText("$New\.env.local", $envText, (New-Object Text.UTF8Encoding $false))
New-Item -ItemType Directory -Force -Path "$Root\data" | Out-Null
Write-Host "  설정 OK"

Say "5/8 부품 설치 (5~10분, 창을 닫지 마세요)"
Set-Location $New
# 실측(집 PC 2026-09-19, 문제기록 #29): npm 경고가 빨간 글자로 나오면 PowerShell 이 오류로 잡아 멈춤 → 이 단계만 오류 잡기를 끈다.
#   better-sqlite3 는 윈도우용 완성본을 갖고 있는데도 컴파일부터 시도해 실패 → --ignore-scripts. 브라우저 부품은 받지 않는다(이 PC 의 크롬을 쓴다)
$ErrorActionPreference = "Continue"
$ok = $false
$done = { (Test-Path "$New\node_modules\next\dist\bin\next") -and (Test-Path "$New\node_modules\better-sqlite3\prebuilds\win32-x64.node") -and (Test-Path "$New\node_modules\@img\sharp-win32-x64") }
foreach ($try in 1, 2) {
  if ($try -eq 2) { Say "부품 설치를 한 번 더 시도합니다 (npm 캐시 정리 후)"; cmd /c "npm cache clean --force" 2>$null | Out-Null; if (Test-Path "$New\node_modules") { cmd /c "rmdir /s /q `"$New\node_modules`"" 2>$null } }
  cmd /c "npm ci --ignore-scripts --no-audit --no-fund > `"$Logs\npm.log`" 2>&1"
  if ($LASTEXITCODE -eq 0 -and (& $done)) { $ok = $true; break }
  Get-Content "$Logs\npm.log" -Tail 5
}
if (-not $ok) { Write-Host "`n부품 설치(npm ci)가 실패했습니다. 이 화면을 캡처해 담당자에게 보내 주세요. (전체 기록: $Logs\npm.log)" -ForegroundColor Red; Get-Content "$Logs\npm.log" -Tail 30; Read-Host "Enter 를 누르면 닫힙니다"; exit 1 }
Write-Host "  부품 OK"

# 서버가 미리 만든 프로그램(.next — 묶음에 들어 있음, scripts/pc-add-next.py, 2026-09-30)이 있으면 이 PC 에서 만들지 않는다(2~5분·실패 지점 하나 줄임).
#   다른 컴퓨터에서 만든 것이라 쓰기 전에 잠깐 켜 본다(포트 4129, 임시 데이터 폴더 — 쓰던 기록은 건드리지 않음). 안 뜨면 지우고 예전처럼 여기서 만든다.
$built = $false
if (Test-Path "$New\.next\BUILD_ID") {
  Say "6/8 프로그램 확인 (미리 만든 것, 1분 안)"
  $smoke = "$Root\smoke-data"; New-Item -ItemType Directory -Force -Path $smoke | Out-Null
  $env:DATA_DIR = $smoke
  $sp = Start-Process -FilePath "node" -ArgumentList "node_modules\next\dist\bin\next","start","-p","4129","-H","localhost" -WorkingDirectory $New -NoNewWindow -PassThru -RedirectStandardOutput "$Logs\smoke.log" -RedirectStandardError "$Logs\smoke-err.log"
  for ($i = 0; $i -lt 60 -and -not $built; $i++) { Start-Sleep 1; try { $r = Invoke-WebRequest -UseBasicParsing "http://localhost:4129/shop/login" -TimeoutSec 3; if ($r.StatusCode -eq 200) { $built = $true } } catch {} }
  cmd /c "taskkill /PID $($sp.Id) /T /F" 2>$null | Out-Null
  Remove-Item Env:DATA_DIR -ErrorAction SilentlyContinue; Start-Sleep 1; Remove-Item $smoke -Recurse -Force -ErrorAction SilentlyContinue
  if ($built) { Write-Host "  미리 만든 프로그램 OK" } else { Write-Host "  미리 만든 프로그램이 이 PC 에서 안 떠서 여기서 새로 만듭니다 (기록: $Logs\smoke-err.log)" -ForegroundColor Yellow; Remove-Item "$New\.next" -Recurse -Force -ErrorAction SilentlyContinue }
}
if (-not $built) {
  Say "6/8 프로그램 만들기 (2~5분)"
  cmd /c "npx next build > `"$Logs\build.log`" 2>&1"
  if ($LASTEXITCODE -ne 0 -or -not (Test-Path "$New\.next\BUILD_ID")) { Write-Host "`n프로그램 만들기가 실패했습니다. 이 화면을 캡처해 담당자에게 보내 주세요. (전체 기록: $Logs\build.log)" -ForegroundColor Red; Get-Content "$Logs\build.log" -Tail 30; Read-Host "Enter 를 누르면 닫힙니다"; exit 1 }
}
$ErrorActionPreference = "Stop"
Write-Host "  만들기 OK"

Say "7/8 크롬 확인·켜져 있던 프로그램 끄기"
# 네이버에 넣을 때 쓰는 진짜 크롬(없으면 엣지) — lib/naver/realChrome.ts 가 찾는 경로와 같다
$browser = @("$env:ProgramFiles\Google\Chrome\Application\chrome.exe", "${env:ProgramFiles(x86)}\Google\Chrome\Application\chrome.exe", "$env:LOCALAPPDATA\Google\Chrome\Application\chrome.exe", "${env:ProgramFiles(x86)}\Microsoft\Edge\Application\msedge.exe", "$env:ProgramFiles\Microsoft\Edge\Application\msedge.exe") | Where-Object { Test-Path $_ } | Select-Object -First 1
if ($browser) { Write-Host "  브라우저: $browser" } else { Write-Host "  크롬(또는 엣지)이 없습니다 — 네이버에 넣으려면 크롬을 설치해 주세요: https://www.google.com/chrome/" -ForegroundColor Yellow }
Set-Location $Root
# 실측(집 윈도우 PC 2026-09-29, 문제기록 #80): 포트의 프로그램만 끄면, 그것을 띄운 [블로그 프로그램] 검은 창(start-blog.bat 의 cmd — 현재 폴더가 app)이
#   "프로그램이 꺼졌습니다"·pause 로 남아 app 폴더를 붙잡고 → 8/8 바꿔 끼우기가 "폴더를 바꾸지 못했습니다"로 실패했다. 그 창과 그 아래 node 까지 끈다.
try { Get-CimInstance Win32_Process -Filter "Name='cmd.exe'" | Where-Object { $_.CommandLine -like '*start-blog.bat*' } | ForEach-Object { cmd /c "taskkill /PID $($_.ProcessId) /T /F" 2>$null | Out-Null } } catch {}
$listen = { netstat -ano -p tcp | Select-String "LISTENING" | Select-String ":$Port\s" }
$pids = (& $listen) | ForEach-Object { ($_.ToString().Trim() -split '\s+')[-1] } | Sort-Object -Unique
foreach ($p in $pids) { if ($p -match '^\d+$' -and [int]$p -gt 0) { cmd /c "taskkill /PID $p /T /F" 2>$null | Out-Null } }
for ($i = 0; $i -lt 20; $i++) { if (-not (& $listen)) { break }; Start-Sleep 1 }

Say "8/8 바꿔 끼우기·바탕화면 아이콘"
if (Test-Path $Old) { Remove-Item $Old -Recurse -Force -ErrorAction SilentlyContinue }
if (Test-Path $App) {
  $moved = $false
  foreach ($i in 1..5) { try { Rename-Item $App $Old -ErrorAction Stop; $moved = $true; break } catch { Start-Sleep 3 } }
  if (-not $moved) { throw "실행 중인 프로그램 폴더를 바꾸지 못했습니다 — 검은 창을 모두 닫고 install.bat 을 다시 실행해 주세요" }
}
Rename-Item $New $App
Copy-Item "$App\pc\start-blog.bat" "$Root\start-blog.bat" -Force
try { Copy-Item "$App\pc\install.bat" "$Root\setup\install.bat" -Force; Remove-Item "$Root\setup\install.ps1" -Force -ErrorAction SilentlyContinue } catch { Write-Host "  (설치 파일 복사는 건너뜀 — 다음 업데이트 때 됩니다)" }
Remove-Item $zip -Force -ErrorAction SilentlyContinue
$desk = [Environment]::GetFolderPath("Desktop")
$ws = New-Object -ComObject WScript.Shell
$s1 = $ws.CreateShortcut("$desk\블로그 프로그램.lnk"); $s1.TargetPath = "$Root\start-blog.bat"; $s1.WorkingDirectory = $App; $s1.IconLocation = "$env:SystemRoot\System32\shell32.dll,220"; $s1.Save()
$s2 = $ws.CreateShortcut("$desk\블로그 프로그램 설치·업데이트.lnk"); $s2.TargetPath = "$Root\setup\install.bat"; $s2.WorkingDirectory = "$Root\setup"; $s2.IconLocation = "$env:SystemRoot\System32\shell32.dll,162"; $s2.Save()
Write-Host "  바탕화면에 [블로그 프로그램]·[블로그 프로그램 설치·업데이트] 아이콘을 만들었습니다"

Write-Host ("`n설치 완료" + $(if ($ver) { " (버전 $ver)" } else { "" })) -ForegroundColor Green
Write-Host "프로그램을 켭니다 — 검은 창이 하나 열리고, 잠시 뒤 브라우저에 사장님 화면이 뜹니다. 검은 창은 프로그램을 쓰는 동안 열어 두세요."
Start-Process "$Root\start-blog.bat"
Read-Host "Enter 를 누르면 이 창을 닫습니다"
