소개
방 변경 명령은 주최자의 신뢰를 얻어야 합니다. 미리 보기는 작업을 남기지 않고 거절은 아무 일도 하지 않으며 승인은 자격을 갖춘 전체 계획만 정확히 적용해야 합니다. WhatIf 지원 선언만으로 모든 파일 쓰기 방법을 보호할 수는 없습니다.
이 독립 실습은 자체 이벤트와 미완성 스크립트로 시작합니다. 앞 안내형 실습의 ID 기반 검증, JSON, 매개변수, 오류 처리 지식을 사용하며 이전 VM 파일은 필요 없습니다. PowerShell과 합성 리소스가 준비되어 있습니다. 기본 Terminal에서 실제 네이티브 확인을 수행하고 실제 저장한 이벤트를 다시 엽니다. 시간은 읽기와 복구를 포함한 저자의 예상치입니다.
모든 쓰기를 하나의 결정 뒤에 두기
이 단계에서는 오해를 일으키는 WhatIf 구현을 고쳐 미리 보기와 확인 거절 모두 작업 영역을 그대로 두도록 합니다.
시작 스크립트가 위험한 이유 살펴보기
처음에 Linux 셸인 Terminal을 엽니다. 이 역할의 세션에만 UTF-8을 설정해 깨끗한 PowerShell 세션을 시작합니다. env는 이 프로그램에만 설정을 전달하고 -NoLogo는 배너를 숨기며 -NoProfile은 개인 프로필을 건너뜁니다.
env LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8 pwsh -NoLogo -NoProfile
PS 프롬프트를 기다립니다. 나머지는 모두 PowerShell 명령입니다. 이 실습에는 이전 VM과 독립된 자체 준비 환경이 있습니다.
Set-Location /home/labex/project/room-changes
원본 이벤트와 요청은 앞 실습과 같은 형식입니다. 스크립트를 바꾸기 전에 읽습니다.
Get-Content ./requests.json -Raw
./Show-RoomSchedule.ps1 -Path ./event.json
초기 apply-room-changes.ps1은 의도적으로 미완성입니다. WhatIf 지원을 선언하지만 허가를 구하지 않고 .NET으로 쓰며 완전한 방 변경도 구현하지 않았습니다. 이 새로운 일회용 데모 출력으로만 시험합니다.
pwsh -NoLogo -NoProfile -File ./apply-room-changes.ps1 -EventPath ./event.json -PlanPath ./requests.json -OutputPath ./unsafe-preview.json -WhatIf
Plan applied.가 출력됩니다. 이 메시지는 올바른 계획을 적용했거나 미리 보기를 지켰음을 증명하지 못합니다. 실제 파일을 확인합니다.
Test-Path ./unsafe-preview.json
True
공통 매개변수 선언은 임의의 .NET 쓰기를 자동으로 보호하지 않습니다. 원본 이벤트는 온전하지만 이 미리 보기는 출력 파일을 만들지 말았어야 합니다. Remove-Item -LiteralPath로 이 일회용 파일만 제거하고 입력과 노트는 그대로 둡니다.
Remove-Item -LiteralPath ./unsafe-preview.json
완전한 이벤트 보존하기
입력 event.json, requests.json, small-request.json, bad-request.json과 무관한 notes.txt의 바이트, 파일 모드, 소유자, mtime은 보존해야 합니다. 준비된 읽기 도구를 바꾸지 마세요. 자신의 apply-room-changes.ps1을 교체하고 지정된 새 결과를 만들 수 있습니다. 이벤트와 요청은 올바른 UTF-8 JSON으로 제공됩니다. 이벤트에는 고유하고 정확한 ID, 음이 아닌 정수 인원과 정원, 승인 목록이 있습니다. 요청은 고유 세션 ID와 목적지 방 ID를 담은 비어 있지 않은 배열입니다. 이름은 표시용 라벨이고 ID는 대소문자를 구분합니다.
알 수 없는 값에는 20단계 안에서 중첩 객체, 순서 있는 배열, null, 불리언, 문자열, 범위가 제한된 유한 JSON 수를 포함할 수 있습니다. 완전히 중복된 객체 키와 임의의 잘못된 스키마는 범위 밖입니다. ConvertFrom-Json -AsHashtable -DateKind String으로 대소문자가 다른 미지의 키와 날짜 같은 문자열의 정확한 원래 오프셋을 보존합니다. 이 스위치는 앞 실습에서 설명했습니다. 원본에는 When과 when이 모두 있으므로 합치지 마세요. 전체 이벤트에는 ConvertTo-Json -Depth 100을 사용해 알 수 없는 중첩 객체를 보존합니다. 기본 깊이 2로는 부족합니다. 1과 1.0 같은 같은 유한 수의 표기는 허용됩니다. 불리언은 불리언으로, 배열 순서는 그대로 유지해야 하며 객체 속성 순서는 외형상의 차이일 뿐입니다.
출력은 이미 존재하는 실제 부모 디렉터리 아래의 새 파일이며 모든 입력과 달라야 합니다. 과정 디렉터리 체인은 실제 디렉터리이고 다른 쓰기 작업이 동시에 파일을 바꾸지 않습니다. 기존 파일, 디렉터리, 링크는 대상 없는 링크를 포함해 변경 없이 거부합니다. 잘못된 계획은 쓰기 전에 호출 전체를 거부해야 합니다. 앞의 유효한 행이 뒤의 알 수 없는 ID, 미승인 세션, 중복, 정원 초과를 정당화하지 않습니다. 정원과 같은 인원은 유효합니다. 목적지 방은 해당 시간에 이미 비어 있습니다.
네이티브 ShouldProcess 사용하기
재사용 호출은 검증기처럼 EventPath, PlanPath, OutputPath를 받습니다. 인접 도우미 없이 이 인수와 설치된 명령으로 독립적으로 동작해야 합니다. 권한 승인 결정 전에 전체 검증을 유지한 뒤 요청된 RoomId 필드만 바뀐 전체 이벤트를 저장합니다.
코드를 저장하기 전에 추가 사항을 읽습니다.
[CmdletBinding(SupportsShouldProcess)]는 네이티브-WhatIf와-Confirm공통 매개변수를 추가합니다. 미리 보기만 출력하는 불리언 매개변수를 따로 만들지 마세요.$PSCmdlet은 PowerShell의 자동 명령 객체입니다.$PSCmdlet.ShouldProcess(target, action)는 작업 수행 여부를 반환합니다. WhatIf에서는 제안 작업을 표시하고 false를 반환하며 Confirm에서는 질문하고 승인에만 true를 반환합니다.- 완전히 검증된 전체 계획에 한 번만 호출합니다. 대상은 출력을 식별하고 작업 설명에는 각 정확한 세션 ID와 이전/새 방 ID가 들어갑니다.
ForEach-Object가 설명을 만들고-join '; '이 문자열을 합칩니다. true 분기만 객체를 바꾸거나 파일을 쓸 수 있습니다. 거절은 출력 디렉터리, 파일, 임시 작업을 만들거나 기존 작업을 바꾸면 안 됩니다. - 전체에 대한 한 번의 확인이지 세션별 질문이 아닙니다. 여기서 .NET 메서드는 두 번째 네이티브 질문을 만들지 않습니다. 나중에 확인을 지원하는 내부 cmdlet을 쓰면 이미 승인된 분기 안에서만 추가 질문을 억제하세요. 외부 결정을 우회해서는 안 됩니다.
- 승인 후 저장된 각
Session은 전체 이벤트 내부의 일치하는 객체를 참조합니다. 그RoomId를 갱신하면 나머지 필드가 모두 유지됩니다. .NETWriteAllText호출과 명시적 UTF-8 인코딩은 앞 실습의 같은 방식이며 이제 ShouldProcess로 보호됩니다.
경로 해석, 원본 객체 파싱, 일치 배열, 대소문자 구분 비교, 오류 처리, 사용 전 전체 변경 수집은 앞 실습과 같습니다. 리터럴 here-string으로 전체 대체 내용을 저장합니다. 자신의 스크립트만 바뀝니다.
Set-Content -Path ./apply-room-changes.ps1 -Encoding utf8 -Value @'
[CmdletBinding(SupportsShouldProcess)]
param(
[Parameter(Mandatory)][string]$EventPath,
[Parameter(Mandatory)][string]$PlanPath,
[Parameter(Mandatory)][string]$OutputPath
)
try {
$ErrorActionPreference = 'Stop'
$fullOutput = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($OutputPath)
if (Get-Item -LiteralPath $fullOutput -Force -ErrorAction SilentlyContinue) {
throw 'Output already exists; choose a new path.'
}
$parent = Split-Path -LiteralPath $fullOutput
if (-not (Test-Path -LiteralPath $parent -PathType Container)) {
throw 'Output parent must already exist.'
}
$event = Get-Content -LiteralPath $EventPath -Raw | ConvertFrom-Json -AsHashtable -DateKind String
$requests = Get-Content -LiteralPath $PlanPath -Raw | ConvertFrom-Json -AsHashtable -DateKind String -NoEnumerate
if ($requests -isnot [array] -or $requests.Count -eq 0) { throw 'Plan must be a nonempty array.' }
$changes = @()
$seen = @()
foreach ($request in $requests) {
if ($seen -ccontains $request.SessionId) { throw "Duplicate session ID: $($request.SessionId)" }
$seen += $request.SessionId
$sessions = @($event.Sessions | Where-Object { $_.SessionId -ceq $request.SessionId })
$rooms = @($event.Rooms | Where-Object { $_.RoomId -ceq $request.RoomId })
if ($sessions.Count -ne 1) { throw "Unknown or ambiguous session ID: $($request.SessionId)" }
if ($rooms.Count -ne 1) { throw "Unknown or ambiguous room ID: $($request.RoomId)" }
if ($event.ApprovedSessionIds -cnotcontains $request.SessionId) { throw "Unapproved session ID: $($request.SessionId)" }
if ($sessions[0].Attendees -gt $rooms[0].Capacity) { throw "Capacity exceeded: $($request.SessionId) -> $($request.RoomId)" }
$changes += [pscustomobject]@{
Session = $sessions[0]
SessionId = $request.SessionId
OldRoomId = $sessions[0].RoomId
NewRoomId = $request.RoomId
}
}
$description = ($changes | ForEach-Object { "$($_.SessionId): $($_.OldRoomId) -> $($_.NewRoomId)" }) -join '; '
if ($PSCmdlet.ShouldProcess($fullOutput, "Apply complete room plan [$description]")) {
foreach ($change in $changes) { $change.Session.RoomId = $change.NewRoomId }
$json = ConvertTo-Json -InputObject $event -Depth 100
[System.IO.File]::WriteAllText($fullOutput, $json + [Environment]::NewLine, [System.Text.UTF8Encoding]::new($false))
Write-Output "Saved: $fullOutput"
}
}
catch {
Write-Error ("Room change failed: {0}" -f $_.Exception.Message) -ErrorAction Continue
exit 1
}
'@
미리 본 뒤 전체 작업 거절하기
다음 명령은 새 출력 경로를 대상으로 s-a: r-old -> r-equal과 s-c: r-old -> r-large 모두를 설명하는 네이티브 WhatIf 메시지를 내야 합니다.
pwsh -NoLogo -NoProfile -File ./apply-room-changes.ps1 -EventPath ./event.json -PlanPath ./requests.json -OutputPath ./event-updated.json -WhatIf
자식의 네이티브 상태를 즉시 읽습니다.
$LASTEXITCODE
0
이제 네이티브 확인을 요청합니다. 실제 질문을 기다리고 전체 작업을 읽은 다음 N을 입력하고 Enter를 누릅니다. 질문이 나오기 전에 N을 붙여 넣지 마세요.
pwsh -NoLogo -NoProfile -File ./apply-room-changes.ps1 -EventPath ./event.json -PlanPath ./requests.json -OutputPath ./event-updated.json -Confirm
Confirm, 전체 작업, [Y] Yes와 [N] No 등의 선택지가 보여야 합니다. 두 변경에 질문은 한 번입니다. No는 작업을 건너뜁니다. 검증 오류가 아니므로 자식은 아무 일도 하지 않고 성공 종료합니다.
$LASTEXITCODE
0
Test-Path ./event-updated.json
False
메모리의 변수를 믿는 대신 원본을 다시 읽습니다.
Get-Content ./event.json -Raw
모든 세션의 RoomId는 r-old이고 중첩 노트도 남아 있습니다. WhatIf와 No 모두 파일, 디렉터리, 임시 후보를 남기면 안 됩니다. 기능 검사는 원본을 읽고 실제 N/Y 입력을 포함해 일회용 PowerShell PTY에서 정확한 스크립트를 테스트합니다. 이 탐색은 프로그램 동작을 시험하며 실제 환경에서 과거에 한 확인의 이력을 만들거나 증명하지 않습니다.
경로가 틀리면 Get-Location과 Get-ChildItem을 살핀 뒤 Set-Location /home/labex/project/room-changes로 돌아옵니다. 스크립트 오타는 수정한 전체 here-string을 다시 저장해 복구합니다. 실수로 일찍 승인했다면 원본을 보존하고 Remove-Item -LiteralPath ./event-updated.json으로 자신의 event-updated.json만 지운 후 거절을 다시 연습합니다. 실수를 숨기려고 원본 이벤트, 요청, 노트를 삭제하거나 편집하지 마세요.
승인하고 실제 일정 다시 열기
이 단계에서는 완전히 검증된 전체 계획을 승인하고 저장된 이벤트를 읽으며 뒤의 잘못된 요청이 거부되는지 확인합니다.
같은 PowerShell Terminal 세션의 /home/labex/project/room-changes에서 시작합니다. 수정한 스크립트는 앞 단계에서 저장되었고 최종 출력은 아직 없습니다. 새 Terminal을 열었다면 앞 단계와 같은 명시적 UTF-8 PowerShell 실행 명령과 Set-Location을 사용하세요. 모든 경로를 인수로 받으므로 세션 변수는 필요하지 않습니다.
완전한 승인 계획 적용하기
확인 호출을 다시 실행합니다. 네이티브 질문을 기다리고 두 ID 매핑을 읽은 뒤 Y를 입력하고 Enter를 누릅니다.
pwsh -NoLogo -NoProfile -File ./apply-room-changes.ps1 -EventPath ./event.json -PlanPath ./requests.json -OutputPath ./event-updated.json -Confirm
질문 한 번 뒤에 Saved:와 출력 경로가 나와야 합니다. 네이티브 상태를 즉시 읽습니다.
$LASTEXITCODE
0
전체 검증 계획을 한 번 확인하는 것입니다. 프로그램 비정상 종료, 전원 손실, 동시 쓰기에 대한 트랜잭션 보장은 아닙니다. 입력은 원본으로 남고 결과는 별도의 완전한 이벤트입니다. 쓰기 실패는 오류이지 일정을 적용했다고 주장할 허가가 아닙니다.
준비된 읽기 도구는 -Path로 전달한 바로 그 파일을 읽습니다. 없는 결과를 재생성하거나 event.json으로 대체하지 않습니다.
./Show-RoomSchedule.ps1 -Path ./event-updated.json
실제 저장된 행은 s-a가 r-equal, s-b는 여전히 r-old, s-c는 r-large임을 보여 줍니다. 두 세션의 제목은 여전히 Shared maps이고 두 방은 Studio입니다. ID가 이들을 구분합니다.
전체 출력을 사전으로 다시 열고 타임스탬프 같은 문자열은 문자열로 유지합니다.
$saved = Get-Content ./event-updated.json -Raw | ConvertFrom-Json -AsHashtable -DateKind String
점은 중첩 속성을 따라 내려갑니다. 첫 세션의 원래 메타데이터에는 날짜 같은 문자열과 대소문자가 다른 키가 있습니다. 대괄호 조회는 각 정확한 사전 키를 사용합니다. 따로 읽습니다.
$saved.Sessions[0].Metadata.contact['When']
2026-11-07T09:30:00+05:30
$saved.Sessions[0].Metadata.contact['when']
Keep this distinct key
다른 미지의 값과 무관한 원본 노트를 읽습니다.
$saved.Unknown.offset
2027-01-02T12:34:56-07:00
Get-Content ./notes.txt
Venue manager: preserve this personal access note.
이 관찰은 완전한 의미 비교를 보완합니다. 필드 두 개의 보존만으로 모든 미지의 객체가 남았다고 할 수는 없습니다. 표시 열만 담은 투영으로 이벤트를 다시 만들지 마세요.
뒤의 잘못된 요청이 부분 변경을 게시하지 못하게 하기
준비된 bad-request.json은 첫 요청은 유효하지만 두 번째에 알 수 없는 방이 있습니다. 완성된 결과를 그대로 두도록 다른 새 출력을 사용합니다. 다음 호출은 확인 전에 의도적으로 실패합니다.
pwsh -NoLogo -NoProfile -File ./apply-room-changes.ps1 -EventPath ./event.json -PlanPath ./bad-request.json -OutputPath ./rejected-event.json -Confirm
Room change failed:와 알 수 없는 방 설명이 나오고 확인 질문은 없어야 합니다. 결과를 즉시 읽습니다.
$LASTEXITCODE
1
Test-Path ./rejected-event.json
False
앞의 유효한 요청이 단독으로 적용되거나 게시되지 않았습니다. 이후 정상 작업에서는 인수를 requests.json으로 돌리세요. 제공된 잘못된 예는 편집하지 않습니다. 원본 이벤트와 완성된 event-updated.json은 계속 사용할 수 있습니다.
기존 목적지도 의도적으로 거부합니다. WhatIf로도 확인할 수 있으며 무엇이든 승인하기 전에 전제 조건을 계속 검증합니다.
pwsh -NoLogo -NoProfile -File ./apply-room-changes.ps1 -EventPath ./event.json -PlanPath ./requests.json -OutputPath ./event-updated.json -WhatIf
$LASTEXITCODE
1
Output already exists; choose a new path.가 나와야 합니다. 미리 보기는 잘못된 인수를 무시할 허가가 아닙니다. 기존 결과를 보존하세요. 새 호출에는 새 출력 경로가 필요합니다. 스크립트를 고친 뒤 이 실습의 표준 결과를 의도적으로 다시 만들려면 자신의 출력만 지우고 유효한 확인을 반복합니다. 원본은 복구 기준으로 남깁니다.
최종 검사는 실제 전체 결과와 보존 기준을 읽습니다. 다른 ID, 정원 일치, 같은 제목 레코드, 중첩 데이터, 실패 경로를 가진 격리 이벤트에서 원본 스크립트 바이트를 시험합니다. 동등한 반복문과 객체 구현도 환영합니다. JSON 값과 형식을 비교하며 객체 속성 순서, 명령 표기, 편집기 선택, 성공 메시지는 비교하지 않습니다. 검사 반복은 실제 이벤트를 적용, 초기화, 복구하지 않습니다.
요약
오해를 일으키는 미리 보기를 고치고 쓰기 전에 전체 계획을 검증했으며 한 번의 네이티브 ShouldProcess 결정을 사용했습니다. WhatIf와 No는 작업을 그대로 두었고 Yes는 요청된 ID 관계와 나머지 모든 중첩 값을 보존한 전체 이벤트를 저장했습니다. 실제 파일을 다시 열고 뒤의 잘못된 요청이나 기존 출력은 몰래 게시되거나 대체되지 않고 거부됨을 확인했습니다.



