"학교 컴퓨터실 40대 PC와 교사용 노트북은 빔프로젝터 연결, 듀얼 모니터 분리, 저해상도 전환 등 디스플레이 토폴로지가 극단적으로 수시 변경됩니다. 기존 데스크톱 앱들은 듀얼 모니터에서 작업하던 창을 노트북 단일 화면으로 전환했을 때 창이 화면 밖(Off-Screen) 허공에 갇혀 마우스로 끌어올 수 없는 '유령 창' 현상을 자주 겪습니다. 또한 무설치(Unpackaged) 환경에서 UWP식 ApplicationData를 호출하다 앱 전체가 비정상 종료되는 사고도 빈번합니다. ZeliDesk 런처 코어는 40여 종 전문 유틸리티를 단 한 줄로 연결하는 WindowKit.Attach와 DisplayArea 좌표 유효성 검증 가드를 통해, 어떤 하드웨어 환경에서도 창 크기와 위치를 100% 안전하게 복원하고 오케스트레이션합니다."
1. 학교 컴퓨터실과 교단 디스플레이의 현실: 유령 창, 화면 이탈, 무설치 크래시
수업 교실과 군부대 전산실의 하드웨어 환경은 일반적인 단일 모니터 사무실과 완전히 다릅니다. 교사는 교무실에서 듀얼 모니터로 작업하다가 수업 시간이 되면 노트북을 들고 교실로 이동하여 빔프로젝터에 연결하며, 학생 실습실 PC는 다양한 해상도의 모니터가 혼용되어 사용됩니다.
이 과정에서 기존 Windows 소프트웨어들은 다음과 같은 심각한 UI 결함을 노출해 왔습니다:
- 화면 밖 허공에 갇히는 '유령 창(Off-Screen Window)' 현상: 교무실 2번 모니터(예: X=1920~3840)에 띄워두었던 창을 프로젝터(1920x1080 단일 화면) 환경에서 다시 열면, 창이 여전히 존재하지 않는 X=2560 좌표에 배치되어 작업 표시줄에는 실행 중으로 뜨지만 화면에서는 마우스로 건드릴 수 없는 유령 상태가 됩니다.
- 최소화 상태 종료 시 좌표 영구 오염 (-32000, -32000): 창이 작업 표시줄로 최소화된 상태에서 사용자가 시스템을 종료하거나 창을 닫으면, Windows OS는 특수 최소화 좌표인
(-32000, -32000)를 반환합니다. 이를 검증 없이 저장하는 소프트웨어는 다음번 실행 시 우주 밖 좌표로 이동하여 영구적으로 화면에서 사라지는 버그를 유발합니다. - 무설치(Unpackaged) 환경의 런타임 크래시: 교육청 보안 정책상 MSIX 설치가 금지된 학교에서 포터블(Unpackaged) ZIP 압축 해제 방식으로 실행할 경우, 닷넷의
ApplicationData.CurrentAPI가InvalidOperationException을 발생시켜 앱이 켜지자마자 꺼지는 대형 사고가 발생합니다.
| 비교 항목 | 전통적 WinForms/WPF 레지스트리 | UWP/일반 WinUI 3 ApplicationData | ZeliDesk 런처 코어 & WindowKit |
|---|---|---|---|
| 배포 호환성 | 관리자 권한 필요 (HKLM 접근 시 오류) | MSIX 패키징 필수 (Unpackaged 시 크래시) | 완벽 호환 (로컬 JSON 영속화로 Zero-UAC 무설치 완벽 지원) |
| 모니터 분리 방어 | 화면 밖 허공에 창 갇힘 (유령 창) | 단순 복원 (화면 밖 이탈) | DisplayArea 가드 (중심 좌표 이탈 시 위치 버리고 크기만 복원) |
| 최소화 좌표 가드 | -32000 좌표 오염 빈번 | 상태 확인 미비 | OverlappedPresenterState.Minimized 사전 검사로 원천 방어 |
| 통합 오케스트레이션 | 개별 프로그램 분절 실행 | 단일 윈도우 한계 | 40+ 전문 유틸리티 윈도우 라이프사이클 & 단축키 중앙 통제 |
| 플러그인 확장성 | 정적 링크 | 샌드박스 제한 | PluginLoader 동적 감지 & 런처 서랍/퀵런치 자동 결합 |
2. 핵심 아키텍처 1: WindowKit의 4대 안전 복원 가드 (`WindowKit.cs`)
ZeliDesk의 모든 유틸리티 창은 생성자에서 WindowKit.Attach(this, "key") 단 한 줄을 선언하는 것만으로 완벽한 상태 복원 능력을 부여받습니다.
WindowKit.cs는 4단계의 철저한 방어선으로 유령 창 현상을 원천 박멸합니다.
public static class WindowKit
{
public static void Attach(Window window, string key)
{
bool restored = false;
// 복원 시점: 첫 Activated 이벤트 (생성자의 기본 크기 Resize가 끝난 뒤 적용되어 저장값이 승리)
window.Activated += (s, e) =>
{
if (restored) return;
restored = true;
TryRestore(window, key);
};
// 저장 시점: 창이 정상적으로 닫힐 때
window.Closed += (s, e) => TrySave(window, key);
}
private static void TryRestore(Window window, string key)
{
EnsureLoaded();
int[] b;
lock (_lock) { if (!_state.TryGetValue(key, out b)) return; }
int x = b[0], y = b[1], w = b[2], h = b[3];
// 가드 1: 비정상적인 극소/극대 크기 필터링
if (w < 200 || h < 150 || w > 20000 || h > 20000) return;
var appWindow = window.AppWindow;
if (appWindow == null) return;
// 가드 2: DisplayArea 가드 — 저장된 중심 좌표가 현재 연결된 어떤 모니터에도 걸치지 않으면
// 위치는 버리고 크기만 안전하게 복원하여 유령 창 방지!
var area = DisplayArea.GetFromPoint(
new Windows.Graphics.PointInt32(x + w / 2, y + h / 2),
DisplayAreaFallback.None);
if (area != null)
appWindow.MoveAndResize(new Windows.Graphics.RectInt32(x, y, w, h));
else
appWindow.Resize(new Windows.Graphics.SizeInt32(w, h));
}
}
창이 최소화되어 작업 표시줄로 내려가 있을 때 창을 닫으면 윈도우 OS는 Position = (-32000, -32000)을 반환합니다.
WindowKit은 TrySave에서 if (p.State == OverlappedPresenterState.Minimized) return; 조건을 두어 최소화 중 닫힘 이벤트에서는 저장을 건너뜀으로써 직전의 정상 좌표를 영구 보존합니다.
3. 핵심 아키텍처 2: Unpackaged 호환 순수 JSON 원자적 영속화 파이프라인
MSIX 패키징 환경에 종속된 ApplicationData.Current 대신, ZeliDesk는 순수 C#과 System.Text.Json을 기반으로
%LOCALAPPDATA%\ZeliDesk\window_state.json에 창 상태를 원자적으로 영속화합니다.
private static void TrySave(Window window, string key)
{
var appWindow = window.AppWindow;
var pos = appWindow.Position;
var size = appWindow.Size;
if (size.Width < 200 || size.Height < 150) return;
EnsureLoaded();
lock (_lock)
{
_state[key] = new[] { pos.X, pos.Y, size.Width, size.Height };
// 디렉터리가 없으면 자동 생성하여 IO 예외 방지
Directory.CreateDirectory(Path.GetDirectoryName(StatePath));
File.WriteAllText(StatePath, JsonSerializer.Serialize(_state));
}
}
단 하나의 JSON 파일에 40개 유틸리티의 좌표가 배열([x, y, w, h])로 직렬화되므로,
USB 메모리에 window_state.json을 복사하여 다른 실습실 PC에 붙여넣는 것만으로 교사가 선호하는 맞춤형 창 레이아웃을 1초 만에 그대로 복원할 수 있습니다.
4. 핵심 아키텍처 3: 40여 종 유틸리티 동적 디스패치 (`LaunchInternalApp`)
MainWindow.xaml.cs는 분절된 40여 종의 유틸리티 창을 단일 접점에서 생성하고 제어하는 마에스트로 역할을 수행합니다.
private void LaunchInternalApp(string target)
{
// 사용 통계 실시간 집계 (대시보드 자체 실행은 제외)
if (target != "zelidash")
UsageTracker.RecordLaunch(target);
// 40여 종 전문 유틸리티 인스턴스화 및 활성화
switch (target)
{
case "zelitranslate": new ZeliTranslate.Views.ZeliTranslateWindow().Activate(); break;
case "nlm": new nLM.Views.nLMWindow().Activate(); break;
case "ylm": new yLM.Views.yLMWindow().Activate(); break;
case "tlm": new tLM.Views.tLMWindow().Activate(); break;
case "slmdown": new sLMdown.Views.sLMdownWindow().Activate(); break;
case "zelioffice": new ZeliOffice.Views.ZeliOfficeWindow().Activate(); break;
case "zeligen": new ZeliGen.Views.ZeliGenWindow().Activate(); break;
case "zelidash": new ZeliDash.Views.ZeliDashWindow().Activate(); break;
// ... 40여 종 유틸리티 매핑 ...
default:
// 동적 로드된 서드파티 플러그인 실행
if (PluginLoader.Plugins.TryGetValue(target, out var plugin))
plugin.Launch();
break;
}
}
각 유틸리티가 실행될 때마다 UsageTracker를 통해 온디바이스 로컬 실행 카운트가 갱신되어,
교사나 학생이 어떤 도구를 가장 빈번하게 사용하는지 ZeliDash 히트맵에서 시각적으로 분석할 수 있습니다.
5. 핵심 아키텍처 4: 교사용 퀵런치 & 선언적 XML 스키마 오케스트레이션
컴퓨터실 40대 PC의 기본 브라우저 탭과 퀵런치 아이콘을 일괄 배치하기 위해, 런처 코어는 선언적 XML 스키마 거버넌스를 완벽히 지원합니다:
- 선언적 브라우저 탭 (`HJHinit_BrowserTab.xml`): 교내 포털, AI 디지털교과서, EBS 등 수업 필수 웹사이트를 런처 상단 탭에 사전 배치하며, ZeliAI 오로라 글로우 헤더를 지원합니다.
- 선언적 퀵런치 핀 배열 (`HJHinit_UtilList.xml`): 정보 교과 및 AI 실습에 최적화된 유틸리티 순서를 선언적으로 정의하여 첫 부팅 시 즉시 노출합니다.
- 단축키 오케스트레이션:
Ctrl+1~9로 브라우저 탭 즉시 전환,Ctrl+M으로 유틸리티 서랍 토글,Ctrl+W로 탭 닫기를 지원하여 마우스 없이도 신속한 수업 진행이 가능합니다. - 시스템 트레이 (`TrayIconService`): 백그라운드 상주를 통해 창을 닫아도 트레이 아이콘 우클릭으로 언제든 0초 만에 원하는 유틸리티를 호출할 수 있습니다.
6. 결론: "복잡한 40개 도구를 하나로 묶어내는 완벽한 데스크톱 마에스트로"
아무리 뛰어난 인공지능 도구와 유틸리티가 많더라도, 사용자가 접근하기 어렵고 화면 설정이 제멋대로 풀려버린다면 현장에서 외면받습니다.
ZeliDesk의 런처 코어(LauncherCore)와 WindowKit은 모니터 구성 변경에도 끄떡없는 견고한 창 복원력, 관리자 권한 없는 무설치 완벽 호환, 그리고 선언적 XML 워크스페이스 배포를 통해 대한민국 공교육 교실과 국방 워크스테이션에 가장 완성도 높은 올인원 데스크톱 환경을 완성했습니다.