# requires -version 5.1 <# 서버노트북 만들기 — 1단계 (AI 없이 도는 부분) ================================================ 깡통 윈도우 노트북에는 Claude 가 없다. 그래서 이 단계만은 사람이 붙여넣는 순수 PowerShell 이어야 한다. 여기서 Claude 를 깨워 놓으면, 그 다음부터는 Claude 가 스킬을 읽고 스스로 이어받는다. 쓰는 법 (깡통 노트북에서 딱 한 줄): irm https://www.jjcreative.co.kr/lab/server.ps1 | iex 하는 일 1) 덮어도·가만히 둬도 잠들지 않게 (AC·배터리 양쪽) 2) 기본 도구 셋 — Node · Python · Git 3) Claude Code 설치 4) 된 것/안 된 것을 표로 알려 주고, 다음에 칠 말을 알려 준다 안 하는 일 - 계정 로그인 (사람이 해야 한다) - 개인 설정 이식 (2단계 import.ps1 이 한다) © 2026 JJ CREATIVE Edu with AI #> $ErrorActionPreference = 'Continue' $ProgressPreference = 'SilentlyContinue' chcp 65001 > $null $Host.UI.RawUI.WindowTitle = '서버노트북 만들기 — 1단계' $script:Steps = @() function Note($name, $state, $detail) { $script:Steps += [pscustomobject]@{ 항목=$name; 상태=$state; 내용=$detail } $mark = switch ($state) { '완료' {'[OK]'} '이미' {'[--]'} '건너뜀' {'[SKIP]'} default {'[!!]'} } Write-Host (" {0,-6} {1,-16} {2}" -f $mark, $name, $detail) } function Head($t) { Write-Host ""; Write-Host "== $t ==" -ForegroundColor Cyan } function Has($c) { return [bool](Get-Command $c -ErrorAction SilentlyContinue) } Write-Host "" Write-Host " 서버노트북 만들기 — 1단계" -ForegroundColor Yellow Write-Host " 집에서 노는 윈도우 노트북을, 24시간 일하는 노트북으로." -ForegroundColor DarkGray Write-Host "" # ── 관리자 권한 ─────────────────────────────────────────── # powercfg 로 «전원 설정»을 바꾸려면 관리자가 필요하다. 나머지는 필요 없다. $isAdmin = ([Security.Principal.WindowsPrincipal] ` [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole( [Security.Principal.WindowsBuiltInRole]::Administrator) Head '1. 덮어도 잠들지 않게' if (-not $isAdmin) { Note '전원 설정' '건너뜀' '관리자 권한이 없습니다 — 아래 안내대로 한 번만 따로 하십시오' } else { # 0 = 안 함. AC(전원 연결) 와 DC(배터리) 를 둘 다 잠가야 «덮어도» 안 잔다. $ok = $true $cmds = @( @('절전', '/change','standby-timeout-ac','0'), @('절전(배터리)','/change','standby-timeout-dc','0'), @('화면', '/change','monitor-timeout-ac','0'), @('화면(배터리)','/change','monitor-timeout-dc','0'), @('디스크', '/change','disk-timeout-ac','0'), @('최대절전', '/change','hibernate-timeout-ac','0') ) foreach ($c in $cmds) { & powercfg $c[1] $c[2] $c[3] 2>$null; if ($LASTEXITCODE -ne 0) { $ok = $false } } # 덮개를 닫아도 계속 일하게 — 서버노트북의 핵심 $LID = '5ca83367-6e45-459f-a27b-476b1d01c936' $BTN = '4f971e89-eebd-4455-a8de-9e59040e7347' & powercfg /setacvalueindex SCHEME_CURRENT $BTN $LID 0 2>$null & powercfg /setdcvalueindex SCHEME_CURRENT $BTN $LID 0 2>$null & powercfg /setactive SCHEME_CURRENT 2>$null if ($ok) { Note '전원 설정' '완료' '절전·화면·덮개 = 안 함 (전원·배터리 양쪽)' } else { Note '전원 설정' '실패' 'powercfg 일부 실패 — 아래 «직접 하기» 참고' } } # ── 기본 도구 셋 ────────────────────────────────────────── Head '2. 기본 도구 셋' # 🔴 옛 윈도우 10 은 기본이 TLS 1.0 이라 https 내려받기가 조용히 실패한다. [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 function Refresh-Path { $env:Path = [Environment]::GetEnvironmentVariable('Path','Machine') + ';' + [Environment]::GetEnvironmentVariable('Path','User') } # ── 2겹: 공식 배포처에서 직접 받아 깐다 (winget 이 없을 때) ── function Install-Direct($kind) { try { $tmp = $env:TEMP switch ($kind) { 'node' { # 최신 LTS 를 목록에서 골라낸다 — 버전을 박아 두면 언젠가 썩는다 $idx = Invoke-RestMethod 'https://nodejs.org/dist/index.json' -UseBasicParsing $lts = $idx | Where-Object { $_.lts } | Select-Object -First 1 if (-not $lts) { return $false } $f = Join-Path $tmp 'node-lts.msi' Invoke-WebRequest "https://nodejs.org/dist/$($lts.version)/node-$($lts.version)-x64.msi" ` -OutFile $f -UseBasicParsing Start-Process msiexec -ArgumentList "/i `"$f`" /qn /norestart" -Wait -NoNewWindow } 'python' { $f = Join-Path $tmp 'python-setup.exe' Invoke-WebRequest 'https://www.python.org/ftp/python/3.12.10/python-3.12.10-amd64.exe' ` -OutFile $f -UseBasicParsing # 관리자가 아니면 모든 사용자용 설치가 막힌다 — 그때는 내 계정에만 깐다 $all = if ($isAdmin) { 1 } else { 0 } Start-Process $f -ArgumentList "/quiet InstallAllUsers=$all PrependPath=1 Include_pip=1" ` -Wait -NoNewWindow } 'git' { $rel = Invoke-RestMethod 'https://api.github.com/repos/git-for-windows/git/releases/latest' ` -UseBasicParsing -Headers @{ 'User-Agent' = 'jj-server-laptop' } $asset = $rel.assets | Where-Object { $_.name -like 'Git-*-64-bit.exe' } | Select-Object -First 1 if (-not $asset) { return $false } $f = Join-Path $tmp 'git-setup.exe' Invoke-WebRequest $asset.browser_download_url -OutFile $f -UseBasicParsing Start-Process $f -ArgumentList '/VERYSILENT /NORESTART /NOCANCEL /SP-' -Wait -NoNewWindow } default { return $false } } Refresh-Path return $true } catch { return $false } } $winget = Has 'winget' if (-not $winget) { Note 'winget' '건너뜀' '이 노트북엔 없습니다 — 공식 설치 파일로 직접 깔겠습니다' } function Ensure($label, $probe, $wingetId, $kind) { if (Has $probe) { Note $label '이미' (& $probe '--version' 2>&1 | Select-Object -First 1) return $true } if ($winget) { Write-Host " $label 설치 중... (몇 분 걸립니다)" -ForegroundColor DarkGray & winget install --id $wingetId -e --source winget ` --accept-package-agreements --accept-source-agreements --silent 2>&1 | Out-Null Refresh-Path if (Has $probe) { Note $label '완료' (& $probe '--version' 2>&1 | Select-Object -First 1) return $true } } # winget 이 없었거나, 있었는데 안 먹었다 — 공식 설치 파일로 다시 시도 Write-Host " $label 을(를) 공식 배포처에서 직접 받는 중... (몇 분 걸립니다)" -ForegroundColor DarkGray $null = Install-Direct $kind if (Has $probe) { Note $label '완료' ((& $probe '--version' 2>&1 | Select-Object -First 1).ToString() + ' (직접 설치)') return $true } Note $label '실패' '설치 후에도 인식되지 않습니다 — 창을 닫았다 다시 여십시오' return $false } $hasNode = Ensure 'Node.js' 'node' 'OpenJS.NodeJS.LTS' 'node' $null = Ensure 'Python' 'python' 'Python.Python.3.12' 'python' $null = Ensure 'Git' 'git' 'Git.Git' 'git' # ── Claude Code ─────────────────────────────────────────── Head '3. Claude Code' if (Has 'claude') { Note 'Claude Code' '이미' (& claude --version 2>&1 | Select-Object -First 1) } elseif (-not $hasNode) { Note 'Claude Code' '건너뜀' 'Node.js 가 없어 설치할 수 없습니다' } else { Write-Host ' 설치 중... (2~3분)' -ForegroundColor DarkGray & npm install -g @anthropic-ai/claude-code 2>&1 | Out-Null $env:Path = [Environment]::GetEnvironmentVariable('Path','Machine') + ';' + [Environment]::GetEnvironmentVariable('Path','User') if (Has 'claude') { Note 'Claude Code' '완료' (& claude --version 2>&1 | Select-Object -First 1) } else { Note 'Claude Code' '실패' 'npm 설치가 끝나지 않았습니다 — 창을 닫았다 다시 여십시오' } } # ── 실습 스킬 ───────────────────────────────────────────── # 1단계 끝에 사용자가 «서버노트북 만들어줘» 라고 친다. 그때 스킬이 없으면 # Claude 는 그 말을 알아듣지 못한다. 그래서 여기서 미리 넣어 둔다. Head '4. 실습 스킬 넣기' $SkillDst = Join-Path $env:USERPROFILE '.claude\skills' New-Item -ItemType Directory -Force -Path $SkillDst | Out-Null # 옆에 skills 폴더가 있나? (ZIP 을 풀어 쓴 경우) # 🔴 irm | iex 로 돌면 $PSScriptRoot 가 비어 있다 — 그래서 두 갈래를 다 본다. $SkillSrc = $null if ($PSScriptRoot) { foreach ($c in @((Join-Path $PSScriptRoot 'skills'), (Join-Path (Split-Path $PSScriptRoot -Parent) 'skills'))) { if (Test-Path $c) { $SkillSrc = $c; break } } } # 없으면 홈페이지에서 받아 푼다 if (-not $SkillSrc) { $tmp = Join-Path $env:TEMP ('jj-skills-' + [guid]::NewGuid().ToString('N')) try { $zip = "$tmp.zip" Invoke-WebRequest -Uri 'https://www.jjcreative.co.kr/lab/skills.zip' -OutFile $zip -UseBasicParsing New-Item -ItemType Directory -Force -Path $tmp | Out-Null Expand-Archive -Path $zip -DestinationPath $tmp -Force Remove-Item $zip -Force -ErrorAction SilentlyContinue $SkillSrc = (Get-ChildItem $tmp -Directory -Recurse | Where-Object { $_.Name -eq 'skills' } | Select-Object -First 1 -ExpandProperty FullName) if (-not $SkillSrc) { $SkillSrc = $tmp } } catch { Note '실습 스킬' '실패' '내려받지 못했습니다 — Claude 에게 «실습 스킬 넣어줘» 라고 하십시오' $SkillSrc = $null } } if ($SkillSrc) { $made = @(); $kept = @() foreach ($d in (Get-ChildItem $SkillSrc -Directory -ErrorAction SilentlyContinue)) { if (-not (Test-Path (Join-Path $d.FullName 'SKILL.md'))) { continue } $target = Join-Path $SkillDst $d.Name # 🔴 이미 있는 스킬은 건드리지 않는다. 본체 노트북에서 실수로 돌려도 안전하게. if (Test-Path $target) { $kept += $d.Name; continue } Copy-Item $d.FullName $target -Recurse -Force $made += $d.Name } if ($made.Count -gt 0) { Note '실습 스킬' '완료' ("$($made.Count) 개 — " + ($made -join ', ')) } elseif ($kept.Count -gt 0) { Note '실습 스킬' '이미' ("$($kept.Count) 개가 이미 있습니다 — 건드리지 않았습니다") } else { Note '실습 스킬' '실패' '스킬 폴더에서 SKILL.md 를 찾지 못했습니다' } } # ── 결과 ────────────────────────────────────────────────── Write-Host "" Write-Host " 결과" -ForegroundColor Yellow $script:Steps | Format-Table -AutoSize | Out-String | Write-Host $bad = @($script:Steps | Where-Object { $_.상태 -in @('실패','건너뜀') }) if ($bad.Count -gt 0) { Write-Host " 손이 더 가야 하는 것" -ForegroundColor Yellow foreach ($b in $bad) { Write-Host (" - {0} : {1}" -f $b.항목, $b.내용) } if (-not $isAdmin) { Write-Host "" Write-Host " 전원 설정만 따로 하려면 — 시작 버튼 우클릭 > 터미널(관리자) 에서:" -ForegroundColor DarkGray Write-Host " irm https://www.jjcreative.co.kr/lab/server.ps1 | iex" -ForegroundColor White } Write-Host "" } Write-Host " 다음 할 일" -ForegroundColor Yellow Write-Host " 1) 이 창을 닫고, 새 터미널을 엽니다 (PATH 가 새로 잡혀야 합니다)" Write-Host " 2) claude 라고 칩니다. 처음이면 로그인 안내가 뜹니다" Write-Host " 3) 로그인 후, Claude 에게 이렇게 말합니다:" Write-Host "" Write-Host " 서버노트북 만들어줘" -ForegroundColor Green Write-Host "" Write-Host " 나머지는 Claude 가 알아서 합니다." -ForegroundColor DarkGray Write-Host ""