793 lines
42 KiB
Markdown
793 lines
42 KiB
Markdown
# UbisamBase 플랫폼 사용 가이드 (AI/개발자 참고용)
|
||
|
||
이 문서는 다른 AI 세션이나 개발자가 이 플랫폼으로 새 프로그램을 만들거나 기존 코드를 이식할 때
|
||
빠르게 참고할 수 있도록 정리한 기술 레퍼런스입니다. 사람이 읽는 슬라이드 버전은
|
||
`doc/UbisamBase_플랫폼_사용_매뉴얼.pptx`를 참고하세요.
|
||
|
||
## 0. 구조
|
||
|
||
```
|
||
UbisamBase.Core 공통 서비스 + Shell UI 라이브러리 (여기 손대면 플랫폼 전체가 바뀜)
|
||
UbisamBase.Shell 고정 호스트. 같은 폴더의 IAppSetup 구현 dll을 리플렉션으로 로드
|
||
UbisamBase.Launcher 실행 진입점. 실행할 때마다 D:\UbisamPlatform\bin을 자기 폴더로 복사한 뒤
|
||
Shell을 같은 프로세스 안에서 띄운다 (F5도, 사용자 실행도 이걸로 시작)
|
||
소비 프로젝트 UbisamBase.Core를 참조하고 IAppSetup만 구현하는 실제 프로그램
|
||
```
|
||
|
||
**저장소 위치**: Core/Shell 소스는 `D:\업무\03_개발\Source\용접기\08_UbisamPlatform`
|
||
저장소에 있다. 소비 프로젝트(예제 `UbisamBase.App`)는 `08_UbisamBase` 저장소에 있다.
|
||
Core/Shell을 참조하는 방식(모든 프로그램이 D드라이브 배포 폴더 하나를 본다)의 전체 설명은 10-2 참고 — 아래는
|
||
그 요약이자, 새 프로젝트를 만드는 순간부터 바로 따라 할 수 있는 실전 절차다.
|
||
|
||
---
|
||
|
||
## 0-1. 새 소비 프로젝트 만들기 — 생성 직후 플랫폼 연결
|
||
|
||
### 프로젝트 생성 버전 / 설정
|
||
|
||
이 플랫폼은 **.NET Framework 4.8(`net48`) 전용**이다. Visual Studio의 "새 프로젝트 만들기"
|
||
마법사로 시작한다면:
|
||
|
||
| 선택 항목 | 값 |
|
||
| --- | --- |
|
||
| 대상 프레임워크 | **.NET Framework 4.8** (netcoreapp/net6+ 아님) |
|
||
| 프로젝트 형식 | **클래스 라이브러리** (WPF 앱 템플릿 아님 — 실행은 `UbisamBase.Launcher.exe`가 하고, 이 프로젝트는 그 안에서 Shell이 리플렉션으로 로드하는 dll일 뿐이다) |
|
||
| 플랫폼 | **Any CPU만** (`<Platforms>`에 x64를 넣지 않는다 — 출력 폴더가 `bin\x64\...`로 바뀌어 F5 실행 경로와 어긋난다) |
|
||
| SDK 스타일 | 새 SDK 스타일 csproj (`<Project Sdk="Microsoft.NET.Sdk">`) |
|
||
|
||
VS의 클래스 라이브러리(.NET Framework) 템플릿에는 WPF 활성화 옵션이 따로 없고, 아래에 정리한
|
||
`CopyPlatformRuntime` 타겟 등 이 플랫폼 특유의 설정도 마법사가 만들어주지 않는다. 그래서
|
||
실무적으로는 둘 중 하나를 쓴다:
|
||
|
||
- **(권장) 기존 소비 프로젝트를 복사해서 시작** — `08_UbisamBase\src\UbisamBase.App` 폴더
|
||
전체를 복사하고 폴더/파일 이름, `AssemblyName`/`RootNamespace`만 바꾼다. 아래 csproj의
|
||
모든 설정이 이미 정답 상태로 들어있다.
|
||
- **처음부터 만들기** — 빈 폴더에 아래 `.csproj`를 그대로 붙여넣는다.
|
||
|
||
### `.csproj` 템플릿
|
||
|
||
```xml
|
||
<Project Sdk="Microsoft.NET.Sdk">
|
||
|
||
<PropertyGroup>
|
||
<OutputType>Library</OutputType>
|
||
<TargetFramework>net48</TargetFramework>
|
||
<UseWPF>true</UseWPF>
|
||
<LangVersion>latest</LangVersion>
|
||
<Nullable>enable</Nullable>
|
||
<RootNamespace>MyApp</RootNamespace>
|
||
<AssemblyName>MyApp</AssemblyName>
|
||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||
|
||
<!-- 플랫폼 배포 폴더. 소스 코드가 없는 컴파일된 dll/exe만 있다(.pdb/.cs 없음).
|
||
플랫폼 원본(08_UbisamPlatform)을 빌드하면 여기에 자동 반영된다. 모든 프로그램이 이 값을
|
||
그대로 쓴다 - 플랫폼을 고치는 개발자도 마찬가지(10-2 참고). -->
|
||
<UbisamPlatformDir>D:\UbisamPlatform\bin\</UbisamPlatformDir>
|
||
|
||
<!-- F5(디버그 시작) 시 Launcher.exe가 실행되도록 지정 — 이 프로젝트 자체는 dll이라 직접 실행 불가.
|
||
Launcher는 실행할 때마다 D:\UbisamPlatform\bin의 최신 내용을 이 폴더로 복사한 뒤 Shell을
|
||
같은 프로세스에서 띄운다. 그래서 다시 빌드하지 않아도 최신 플랫폼으로 뜨고, 이 프로젝트에
|
||
찍은 중단점도 정상으로 걸린다.
|
||
주의: 여기서 $(TargetDir)를 쓰면 안 된다. 이 시점엔 아직 비어 있어서 VS가
|
||
"UbisamBase.Launcher.exe 프로그램을 시작할 수 없습니다 / 파일을 찾을 수 없습니다"로 실패한다. -->
|
||
<StartAction>Program</StartAction>
|
||
<StartProgram>$(MSBuildProjectDirectory)\bin\$(Configuration)\$(TargetFramework)\UbisamBase.Launcher.exe</StartProgram>
|
||
<StartWorkingDirectory>$(MSBuildProjectDirectory)\bin\$(Configuration)\$(TargetFramework)\</StartWorkingDirectory>
|
||
|
||
<!-- VS의 "빠른 최신 상태 확인"이 아래 CopyPlatformRuntime 복사를 추적 못 해서
|
||
소스 변경 없으면 빌드를 건너뛰려 하는 문제를 막는다 -->
|
||
<DisableFastUpToDateCheck>true</DisableFastUpToDateCheck>
|
||
</PropertyGroup>
|
||
|
||
<ItemGroup>
|
||
<!-- 프로젝트 참조가 아니라 배포된 dll을 그대로 가져다 쓰는 파일 참조 -->
|
||
<Reference Include="UbisamBase.Core">
|
||
<HintPath>$(UbisamPlatformDir)UbisamBase.Core.dll</HintPath>
|
||
<Private>false</Private>
|
||
</Reference>
|
||
<!-- IAppSetup.RegisterServices(IServiceCollection) 구현에 필요 -->
|
||
<Reference Include="Microsoft.Extensions.DependencyInjection.Abstractions">
|
||
<HintPath>$(UbisamPlatformDir)Microsoft.Extensions.DependencyInjection.Abstractions.dll</HintPath>
|
||
<Private>false</Private>
|
||
</Reference>
|
||
<!-- [ObservableProperty]/[RelayCommand]를 쓰려면 반드시 PackageReference로 받아야 한다
|
||
(소스 제너레이터가 필요해서 dll 파일 참조만으로는 안 됨) -->
|
||
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.3.2" />
|
||
</ItemGroup>
|
||
|
||
<!-- 차트를 쓴다면 이 4개도 같은 방식(HintPath, Private=false)으로 추가:
|
||
LiveChartsCore, LiveChartsCore.SkiaSharpView, LiveChartsCore.SkiaSharpView.WPF, SkiaSharp -->
|
||
|
||
<!-- 플랫폼 런타임 전체(Shell.exe + Core.dll + nuget 런타임)를 이 프로젝트 출력 폴더로 복사 -->
|
||
<Target Name="CopyPlatformRuntime" BeforeTargets="Build">
|
||
<ItemGroup>
|
||
<_PlatformFiles Include="$(UbisamPlatformDir)**\*.*" />
|
||
</ItemGroup>
|
||
<MakeDir Directories="$(TargetDir)" Condition="!Exists('$(TargetDir)')" />
|
||
<Copy SourceFiles="@(_PlatformFiles)" DestinationFiles="@(_PlatformFiles->'$(TargetDir)%(RecursiveDir)%(Filename)%(Extension)')" SkipUnchangedFiles="true" />
|
||
</Target>
|
||
|
||
</Project>
|
||
```
|
||
|
||
### 확인 순서
|
||
|
||
1. `dotnet build`(또는 VS 빌드) — 출력 폴더 `bin\Debug\net48\`(※ `bin\x64\...`가 아니어야 함)에
|
||
`UbisamBase.Launcher.exe`, `UbisamBase.Launcher.exe.config`, `UbisamBase.Shell.exe`,
|
||
`UbisamBase.Core.dll`, 이 프로젝트의 dll이 같이 생겼는지 확인.
|
||
2. 아직 `IAppSetup`을 구현하지 않았으면 F5 시 "IAppSetup을 구현한 dll을 … 찾지 못했습니다"
|
||
예외로 멈춘다 — 이 메시지가 나오면 참조·복사·실행 경로 연결은 성공한 것이다.
|
||
3. 다음(0-2)에서 `IAppSetup`을 구현하면 실제 화면이 붙는다.
|
||
4. F5로 뜬 상태에서 작업 관리자를 보면 **`UbisamBase.Launcher` 프로세스 하나만** 있어야 한다.
|
||
`UbisamBase.Shell`이 별도 프로세스로 따로 떠 있으면 옛 Launcher(별도 프로세스 방식)라서
|
||
디버거가 안 붙고 중단점이 안 걸린다 → `D:\UbisamPlatform\bin`을 최신 배포본으로 교체.
|
||
5. (선택) 이 저장소에 `.slnx` 솔루션이 있다면 `<Project Path="src/MyApp/MyApp.csproj" />`를
|
||
추가해 VS 솔루션 탐색기에 노출시킨다. 이때 `<Platform Name="x64" />` 같은 x64 구성은 넣지 않는다.
|
||
|
||
### 자주 겪는 실행 오류
|
||
|
||
| 증상 | 원인 | 조치 |
|
||
| --- | --- | --- |
|
||
| F5 시 "`UbisamBase.Launcher.exe` 프로그램을 시작할 수 없습니다 / 지정된 파일을 찾을 수 없습니다" | `StartProgram`에 `$(TargetDir)`를 썼다 — PropertyGroup 시점엔 비어 있어 파일명만 남는다 | 위 템플릿처럼 `$(MSBuildProjectDirectory)\bin\$(Configuration)\$(TargetFramework)\` 절대경로로 |
|
||
| 빌드 출력이 `bin\x64\Debug\net48\`로 나온다 | csproj `<Platforms>`에 x64가 들어갔거나 솔루션이 x64로 매핑됨 | csproj에서 `<Platforms>` 삭제, `.slnx`의 x64 `Platform` 항목 삭제, 구성 관리자 활성 플랫폼 = Any CPU |
|
||
| VS 상단에 "알 수 없는 프로젝트 구성 매핑이 있습니다" 배너 | `.slnx`에 프로젝트에 없는 플랫폼(x64) 매핑이 남음 | `.slnx`에서 해당 `<Platform .../>` 삭제 후 VS 재시작 |
|
||
| 켜졌다가 꺼지고 새 창으로 다시 뜬다 / 중단점이 안 걸린다 | 배포본의 Launcher가 옛 버전(Shell을 별도 프로세스로 띄움) | `D:\UbisamPlatform\bin` 최신화 후 재빌드 |
|
||
| 실행 직후 창 없이 조용히 종료 (이벤트 뷰어에 `FileLoadException`) | `UbisamBase.Launcher.exe.config`에 바인딩 리디렉트가 없음(옛 배포본) | `D:\UbisamPlatform\bin` 최신화 — 플랫폼을 빌드하면 Shell 설정이 Launcher 설정으로 복사된다(다른 PC면 최신 배포 폴더를 통째로 다시 복사) |
|
||
| 컴파일 에러 "…형식이 없습니다", "…정의가 포함되어 있지 않습니다" | 코드가 기대하는 플랫폼 API가 배포본에 없다 | 배포본 최신화. 그래도 없으면 플랫폼에 추가할지, 코드에서 뺄지 결정 |
|
||
|
||
---
|
||
|
||
## 0-2. `IAppSetup` 구현
|
||
|
||
새 프로그램 = `IAppSetup` 구현 클래스 하나 + 필요한 View/ViewModel. 나머지(테마, 네비게이션,
|
||
설정 저장, 로그, 대화상자, 토스트, 잠금 등)는 전부 플랫폼이 제공합니다.
|
||
|
||
```csharp
|
||
public class AppSetup : IAppSetup
|
||
{
|
||
public string GetAppName() => "내 프로그램 이름"; // 상단 브랜딩 바에 표시
|
||
|
||
public void RegisterViews(ITabManager manager)
|
||
{
|
||
manager.AddMain<HomeView, HomeViewModel>(Icon.Home, ViewIds.Home, "홈");
|
||
manager.AddSub<DetailView, DetailViewModel>(ViewIds.Home, "상세");
|
||
}
|
||
|
||
public void RegisterSettings(ITabManager manager)
|
||
{
|
||
manager.AddSettingsSub<MySettings>(TabManager.SettingsTabKey, "내 설정");
|
||
}
|
||
|
||
public void RegisterServices(IServiceCollection services)
|
||
{
|
||
services.AddSingleton<MyLegacyService>(); // 기존 코드를 여기서 DI에 등록
|
||
}
|
||
}
|
||
```
|
||
|
||
`AddMain`/`AddSub`로 등록한 View는 DI가 자동으로 ViewModel을 만들어 `DataContext`에 연결합니다.
|
||
ViewModel 생성자에 필요한 서비스(플랫폼 제공이든 RegisterServices로 직접 등록한 것이든)를
|
||
선언하기만 하면 주입됩니다 — `ActivatorUtilities.CreateInstance` 기반이라 별도 배선이 필요 없습니다.
|
||
|
||
---
|
||
|
||
## 1. 설정 화면
|
||
|
||
### 1-1. 자동 생성 (View 없이)
|
||
|
||
```csharp
|
||
public class MySettings
|
||
{
|
||
[Category("장비")]
|
||
[DisplayName("측정값 개수")]
|
||
public int MeasureCount { get; set; }
|
||
|
||
[Category("장비")]
|
||
public bool UseAutoSave { get; set; }
|
||
|
||
[Category("데이터")]
|
||
public ObservableCollection<Row> Rows { get; set; } = new();
|
||
}
|
||
|
||
manager.AddSettingsSub<MySettings>(TabManager.SettingsTabKey, "내 설정");
|
||
```
|
||
|
||
- 프로퍼티 타입 → 컨트롤 자동 매핑: `bool`→체크박스, `int/long/short`→정수 텍스트박스,
|
||
`float/double/decimal`→소수 텍스트박스, `DateTime`→달력, `string`→텍스트박스,
|
||
`ObservableCollection<T>`→DataGrid.
|
||
- `[Category("말머리")]` 같은 것끼리 화면에서 그룹으로 묶임 (없으면 말머리 없는 그룹).
|
||
- `[DisplayName("이름")]` 생략 시 프로퍼티 이름 그대로 표시.
|
||
- `[Browsable(false)]`면 화면에서 숨김.
|
||
- 저장 파일: `D:\UbisamConfig\Settings\{클래스이름}.json` — 어느 탭에 붙여도 클래스가 같으면
|
||
같은 파일 공유.
|
||
|
||
### 1-2. 직접 만든 View + 수동 등록
|
||
|
||
View를 직접 그리고 싶으면 `ISettingsService`를 주입받아 등록합니다.
|
||
|
||
```csharp
|
||
public MyViewModel(ISettingsService settingsService)
|
||
{
|
||
settings = settingsService.RegisterSettings("MyKey", new MySettingsClass());
|
||
}
|
||
// settingsService.Save("MyKey"); / settingsService.Reload("MyKey");
|
||
```
|
||
|
||
### 1-3. 저장/새로고침 버튼 (플랫폼 자동 지원)
|
||
|
||
모든 설정 화면 우상단에 파란색 저장/새로고침 버튼이 동일하게 표시됩니다. 직접 만든 화면에서도
|
||
`SettingsToolbar` 컴포넌트를 붙이면 됩니다 — `DataContext`를 상속받아 호스트 ViewModel의
|
||
`RefreshCommand`/`SaveCommand`(`[RelayCommand] private void Refresh()/Save()`)에 바인딩됩니다.
|
||
|
||
```xml
|
||
<local:SettingsToolbar DockPanel.Dock="Top" Margin="0,0,0,16"/>
|
||
```
|
||
|
||
저장/새로고침 성공·실패 시 Success/Error 토스트가 자동으로 뜨도록 구현부에서
|
||
`MessageUtil.ShowSavedToast()` / `MessageUtil.ShowErrorToast(...)`를 호출하는 패턴을 따르세요
|
||
(플랫폼이 등록한 기본 4개 설정 화면이 이미 이 패턴입니다).
|
||
|
||
---
|
||
|
||
## 2. 로깅 — `UbisamBase.Core.Logging`
|
||
|
||
```csharp
|
||
private readonly Logger log = new Logger("MyFeature"); // DI 없이 어디서든 생성 가능
|
||
|
||
log.V("verbose"); log.D("debug"); log.I("info");
|
||
log.W("warning"); log.E("error"); log.E("error", ex);
|
||
```
|
||
|
||
- 호출 즉시 `D:\UbisamConfig\Log\yyyyMMdd.log`에 기록 + 설정 > 로그 탭(`LogService.Current.RecentEntries`)에 실시간 반영.
|
||
- **버튼 클릭 자동 로깅**: `ButtonClickLogger.Install()`이 부팅 시 한 번 걸려 있어서
|
||
(`EventManager.RegisterClassHandler(typeof(ButtonBase), ButtonBase.ClickEvent, ...)`),
|
||
플랫폼 전체(Core + 모든 소비 프로그램)의 모든 `Button`/`ToggleButton` 클릭이
|
||
코드 한 줄 없이 `"UI"` 태그로 자동 기록됩니다. 버튼마다 로그 코드를 넣을 필요가 없습니다.
|
||
- 로그 뷰어: DataGrid, 레벨 필터(ALL = 전부, 특정 레벨 = 정확히 일치), 검색(LIKE, 부분 포함),
|
||
우클릭 복사.
|
||
|
||
---
|
||
|
||
## 3. 대화상자 / 토스트 — `UbisamBase.Core.Messaging`
|
||
|
||
```csharp
|
||
// 대화상자 — OS MessageBox 대신 테마 적용된 MessageDialog가 자동으로 뜬다
|
||
MessageUtil.ShowInfo("알림", "저장되었습니다.");
|
||
MessageUtil.ShowError("오류", "저장하지 못했습니다.");
|
||
bool yes = MessageUtil.ShowYesNo("확인", "계속하시겠습니까?");
|
||
|
||
// 토스트 — 3초 후 자동 소멸, 화면 끝에서 슬라이드 인/아웃
|
||
MessageUtil.ShowToast("정보 메시지");
|
||
MessageUtil.ShowSuccessToast("저장되었습니다.");
|
||
MessageUtil.ShowWarningToast("값이 허용 범위를 벗어났습니다.");
|
||
MessageUtil.ShowErrorToast("장비 통신이 끊어졌습니다.");
|
||
```
|
||
|
||
토스트 표시 위치(좌상단/좌하단/우상단/우하단)는 설정 > UI Setting에서 사용자가 바꿀 수 있습니다
|
||
(`UiSettingsService.ToastPosition`).
|
||
|
||
---
|
||
|
||
## 4. 차트 — `UbisamBase.Core.Charting.ChartTheme` (LiveCharts2)
|
||
|
||
```csharp
|
||
public ISeries[] TrendSeries { get; private set; }
|
||
public Axis[] TrendXAxes { get; private set; }
|
||
public Axis[] TrendYAxes { get; private set; }
|
||
|
||
public MyViewModel()
|
||
{
|
||
Rebuild();
|
||
ChartTheme.ThemeChanged += Rebuild; // 뷰가 사라질 때 -= 로 해제할 것
|
||
}
|
||
|
||
private void Rebuild()
|
||
{
|
||
TrendSeries = new ISeries[] {
|
||
ChartTheme.ReferenceLine(baseline, "기준"),
|
||
ChartTheme.Line(measured, "측정")
|
||
};
|
||
TrendXAxes = new[] { ChartTheme.XAxis(v => $"{v}s") };
|
||
TrendYAxes = new[] { ChartTheme.YAxis("0", min: 0, max: 100) };
|
||
OnPropertyChanged(nameof(TrendSeries)); // 등등
|
||
}
|
||
```
|
||
|
||
```xml
|
||
<lvc:CartesianChart Series="{Binding TrendSeries}" XAxes="{Binding TrendXAxes}"
|
||
YAxes="{Binding TrendYAxes}" Background="Transparent" Height="200"/>
|
||
```
|
||
|
||
- **`Background="Transparent"`를 꼭 지정** — 기본값은 흰 배경이라 다크 테마에서 튐.
|
||
- **툴팁·범례는 따로 입혀야 한다** — 지정하지 않으면 LiveCharts 기본(밝은 바탕 + 검은 글자)이 나와
|
||
다크 테마에서 튄다. 차트를 만든 뒤와 테마가 바뀐 뒤에 `ChartTheme.Apply(chart)`를 한 번 부른다
|
||
(`CartesianChart` / `PieChart` 둘 다 있음).
|
||
|
||
### 4-1. 차트 종류별 — 무엇을 언제 쓰나
|
||
|
||
| 만들기 | 생김새 | 쓰는 자리 |
|
||
| --- | --- | --- |
|
||
| `Line(values, name)` | accent 선 2px, 보간 없음, 면적 12% | 단일 측정값 추이(기본) |
|
||
| `Line(values, name, i)` | 팔레트 i번 선, 면적 없음 | 여러 계열을 한 그래프에 겹칠 때 |
|
||
| `ReferenceLine(values, name)` | 회색 파선, hover 없음 | 기준·목표·규격 상하한. **측정선보다 먼저** 넣어야 아래에 깔린다 |
|
||
| `SmoothLine(values, name, i)` | 곡선 보간(0.65) | 추세만 보여줄 때. **공정 측정값에는 쓰지 말 것** — 없던 중간값을 지어낸다 |
|
||
| `Area(values, name, i)` | 선 + 채움 24% | 누적량·점유량처럼 "쌓인 양" |
|
||
| `StepLine(values, name, i)` | 계단선 | ON/OFF·단계값처럼 다음 값까지 유지되는 신호 |
|
||
| `Column(values, name[, i])` | 각진 세로 막대, 채움 85% | 항목별 비교(수량·횟수) |
|
||
| `StackedColumn(values, name, i)` | 누적 세로 막대 | 전체 대비 구성비를 시간축으로 |
|
||
| `Bar(values, name[, i])` | 가로 막대 | 항목 이름이 길어 세로 막대 라벨이 겹칠 때(설비명·불량 유형) |
|
||
| `Pie(values[, names])` | 원형, 조각 사이 판 색 경계 | 구성비. 조각 6개 이하일 때만 |
|
||
| `Donut(values[, names])` | 가운데 빈 원형 | 구성비 + 가운데에 총계 표시 |
|
||
| `Gauge(percent)` | 0~100 도넛 게이지 | 가동률·달성률 한 값 |
|
||
| `Scatter(points, name, i)` | 점 9px, 판 색 테두리 | 두 값의 상관 |
|
||
|
||
- **색은 직접 고르지 않는다.** 단일 계열은 accent 하나, 여러 계열은 `seriesIndex`를 0,1,2…로 넘기면
|
||
`Ubisam.Brush.Chart.Series1~6` 팔레트를 순서대로 쓴다(라이트/다크 각각 따로 정의돼 있다).
|
||
7개째부터는 색이 처음으로 돌아가므로, 계열이 그보다 많으면 차트를 나누는 편이 낫다.
|
||
- 팔레트 색이 필요하면 `ChartTheme.SeriesColor(i)`로 꺼내 쓴다(범례 표식 등).
|
||
- 소비 프로젝트에서 차트를 직접 쓰려면 `LiveChartsCore`, `LiveChartsCore.SkiaSharpView`,
|
||
`LiveChartsCore.SkiaSharpView.WPF`, `SkiaSharp` — 4개를 `$(UbisamPlatformDir)`에서
|
||
HintPath로 참조 추가해야 함(TestApp.csproj 예시 참고).
|
||
|
||
---
|
||
|
||
## 5. 화면 잠금 — `UbisamBase.Core.Locking`
|
||
|
||
- 하단 네비 "화면 잠금" → `LockConfirmDialog`가 뜨고, **거기서 입력한 아이디/비밀번호가 그 순간
|
||
해제 키가 됨**(고정 계정 저장 방식 아님). `LockService.Current.Validator`를 그 자리에서 교체.
|
||
- `LockOverlay`가 화면 전체를 불투명하게 덮음(Esc로 못 나감), 3회 실패 시 입력 잠김 →
|
||
구석의 "강제 잠금 해제" 버튼만 예외(`LockService.Current.ForceUnlock()`, 인증 없음).
|
||
- 코드에서 직접 잠그려면: `LockService.Current.Lock()` (주의: 이 경우 `Validator`를 미리
|
||
설정해두지 않으면 아무도 못 풂 — 보통은 `LockConfirmDialog.ShowAndLock()`을 통해서만 잠금).
|
||
- 잠글 때 입력값은 평문으로 `D:\UbisamConfig\ScreenSaver\last-lock.json`에 기록됨(요청 사양).
|
||
|
||
---
|
||
|
||
## 6. 스크린 세이버 — `UbisamBase.Core.ScreenSaving.ScreenSaverService`
|
||
|
||
화면 잠금과 달리 **인증 없이** 코드에서 자유롭게 켜고 끕니다 — 특정 작업 중 사용자 조작을
|
||
막을 때 사용.
|
||
|
||
```csharp
|
||
ScreenSaverService.Current.Show("작업을 진행 중입니다..."); // 즉시 켜기
|
||
ScreenSaverService.Current.Hide(); // 즉시 끄기
|
||
|
||
// 시간을 넘기면, 그 시간이 지나도 서비스가 스스로 끄지 않고 이벤트만 올린다 —
|
||
// 실제로 끌지는 구독한 쪽이 결정한다(항상 자유롭게 켜고 끌 수 있다는 원칙 유지).
|
||
ScreenSaverService.Current.TimerElapsed += OnElapsed;
|
||
ScreenSaverService.Current.Show("잠시만요...", seconds: 5);
|
||
|
||
void OnElapsed()
|
||
{
|
||
ScreenSaverService.Current.TimerElapsed -= OnElapsed;
|
||
ScreenSaverService.Current.Hide();
|
||
}
|
||
```
|
||
|
||
반투명 스크림 + 화면 가운데 accent색 큰 글자로 `Show()`에 넘긴 문구가 표시됩니다.
|
||
|
||
---
|
||
|
||
## 7. 이벤트 버스 — `UbisamBase.Core.Messaging.IEventBus`
|
||
|
||
문자열 subject 기반 Pub/Sub. **`AddUbisamBaseCore`에서 이미 DI 싱글턴으로 등록되어 있음.**
|
||
|
||
```csharp
|
||
public MyViewModel(IEventBus bus)
|
||
{
|
||
subscription = bus.Subscribe("Order.Completed", OnOrderCompleted);
|
||
}
|
||
|
||
private void OnOrderCompleted(EventMessage msg)
|
||
{
|
||
// msg.Subject, msg.Param, msg.From
|
||
}
|
||
|
||
// 다른 곳에서:
|
||
bus.Publish(new EventMessage("Order.Completed", orderId, from: "OrderService"));
|
||
|
||
// 화면이 사라질 때
|
||
subscription.Dispose();
|
||
```
|
||
|
||
---
|
||
|
||
## 8. 타임아웃 관리자 — `UbisamBase.Core.Utils.TimeOutManager`
|
||
|
||
문자열 키 기반 타이머 모음. **DI 싱글턴으로 등록되어 있음.**
|
||
|
||
```csharp
|
||
public MyViewModel(TimeOutManager timers) { _timers = timers; }
|
||
|
||
_timers["Ch1"].Start(limitMs: 5000);
|
||
if (_timers["Ch1"].IsTimeout()) { /* 5초 초과 */ }
|
||
long ms = _timers["Ch1"].End();
|
||
_timers.Remove("Ch1");
|
||
```
|
||
|
||
키는 미리 정의할 필요 없음(인덱서 접근 시 없으면 자동 생성). Start/End 시 `"TimeUtil"` 태그로
|
||
자동 로그가 남습니다.
|
||
|
||
---
|
||
|
||
## 8-1. 발표자 모드 — `UbisamBase.Core.Shell.PlatformChrome`
|
||
|
||
플랫폼이 그리는 껍데기(상단 브랜딩 바 · 네비게이션 탭 바 · 서브탭 줄)를 한 번에 숨긴다.
|
||
발표 자료를 띄울 때처럼 모듈 화면만 꽉 차게 보여주고 싶을 때 쓴다.
|
||
|
||
```csharp
|
||
using UbisamBase.Core.Shell;
|
||
|
||
PlatformChrome.IsVisible = false; // 플랫폼 껍데기 전부 숨김 (발표자 모드)
|
||
PlatformChrome.IsVisible = true; // 원래대로
|
||
PlatformChrome.Toggle(); // 뒤집기 — 버튼/단축키에 걸어 쓰기 좋다
|
||
```
|
||
|
||
- 기본값은 `true`. **저장하지 않는다** — 앱을 다시 켜면 항상 보이는 상태로 시작한다.
|
||
- 숨기면 종료 · 화면 잠금 · 로그 · 캡처 버튼도 같이 사라진다. **다시 켤 방법(모듈 화면의 버튼이나
|
||
키 입력)을 반드시 모듈 쪽에 만들어 둬야 한다.**
|
||
- 숨기면 **컨텐츠 여백(26,22)도 0이 된다** — 껍데기를 걷었는데 액자처럼 테두리가 남지 않는다.
|
||
- 토스트 메시지, 화면 잠금, 스크린 세이버 오버레이는 이 스위치와 무관하게 그대로 뜬다.
|
||
- 바뀔 때마다 `VisibleChanged` 이벤트가 온다. 설정의 "상단 바 접기"나 다른 모듈이 바꾼 경우에도
|
||
오므로, 발표 모드를 나갈 때 타이머를 멈추거나 띄워둔 창을 닫는 정리는 여기서 한다.
|
||
|
||
```csharp
|
||
PlatformChrome.VisibleChanged += OnChromeChanged;
|
||
Unloaded += (_, _) => PlatformChrome.VisibleChanged -= OnChromeChanged; // 정적 이벤트라 해제 필수
|
||
|
||
private void OnChromeChanged(object? sender, bool visible)
|
||
{
|
||
if (visible) { /* 발표 모드 종료 정리 — 타이머 정지, 발표자 창 닫기 등 */ }
|
||
}
|
||
```
|
||
|
||
## 8-2. 보조 모니터 — `UbisamBase.Core.Shell.Displays`
|
||
|
||
발표자 보기처럼 "본 창이 없는 쪽 모니터"에 창을 띄울 때 쓴다. 모듈이 `System.Windows.Forms`의
|
||
`Screen`을 직접 읽으면 실제 픽셀이 나와서, 배율(DPI)이 걸린 환경에서 DIP 환산을 빠뜨리면 창이
|
||
엉뚱한 자리에 뜬다 — 그 환산까지 여기서 처리한다.
|
||
|
||
```csharp
|
||
var area = Displays.SecondaryWorkArea(Window.GetWindow(this)); // 모니터가 하나면 null
|
||
if (area is Rect r)
|
||
{
|
||
var win = new PresentWindow { WindowStartupLocation = WindowStartupLocation.Manual };
|
||
win.Left = r.Left; win.Top = r.Top; win.Width = r.Width; win.Height = r.Height;
|
||
win.Show();
|
||
}
|
||
```
|
||
|
||
- `Displays.HasSecondary` — 모니터가 두 대 이상인지.
|
||
- `Displays.WorkArea(owner)` — owner 창이 있는 모니터의 작업 영역(DIP).
|
||
- 값은 전부 **작업표시줄을 뺀 작업 영역**이고 WPF 좌표(DIP)다. 모니터가 셋 이상이면 owner가 없는
|
||
것 중 첫 번째(주 모니터 우선)를 돌려준다.
|
||
|
||
## 8-3. 닫기 확인 — `UbisamBase.Core.Modules.ICloseGuard`
|
||
|
||
저장하지 않은 변경이 있을 때 창이 그냥 닫히는 것을 막는 표준 통로. 모듈 View나 그 ViewModel이
|
||
구현하면 셸이 닫기 전에 물어본다. **셸 창의 `Closing`에 직접 붙지 말 것** — 모듈이 여럿이면
|
||
서로 Cancel/Close를 걸어 충돌한다.
|
||
|
||
```csharp
|
||
public partial class DeckViewModel : ObservableObject, ICloseGuard
|
||
{
|
||
public async Task<bool> CanCloseAsync()
|
||
{
|
||
if (!IsDirty) return true;
|
||
return await AskUserAsync("저장하지 않은 변경이 있습니다. 닫을까요?");
|
||
}
|
||
}
|
||
```
|
||
|
||
- **한 번이라도 연 탭**만 물어본다(아직 안 연 탭은 화면을 만들지 않는다).
|
||
- 하나라도 false를 돌려주면 거기서 멈추고 창은 닫히지 않는다(뒤의 화면에는 묻지 않는다).
|
||
- 하단 "종료" 버튼도 이 경로를 탄다. 반대로 `Application.Current.Shutdown()`을 직접 부르면
|
||
이 확인을 건너뛴다 — 모듈에서는 쓰지 말 것.
|
||
- UI 설정의 "상단 바 접기"는 사용자가 저장해 두는 별개 취향이다. 둘 중 하나라도 숨기라고 하면 상단 바는 숨는다.
|
||
|
||
## 8-4. 화면을 꽉 쓰는 모듈 — `PlatformChrome.FullBleed`
|
||
|
||
셸은 모듈 화면 둘레에 여백(26,22)을 준다. 설정 화면이나 폼처럼 글을 읽는 화면에는 그 여백이
|
||
있어야 보기 좋지만, 편집기·지도·카메라처럼 **자기 안에서 이미 칸을 나누는 모듈**은 여백이 한 겹 더
|
||
붙으면 화면만 좁아지고 테두리처럼 보인다. 그런 모듈은 View에 첨부 속성 하나만 붙이면 된다.
|
||
|
||
```xml
|
||
<UserControl x:Class="MyModule.Views.EditorView"
|
||
xmlns:shell="clr-namespace:UbisamBase.Core.Shell;assembly=UbisamBase.Core"
|
||
shell:PlatformChrome.FullBleed="True">
|
||
```
|
||
|
||
- 기본은 여백 있음(False). 이 화면이 떠 있는 동안만 여백이 사라지고, 다른 탭으로 옮기면 그 탭의
|
||
설정을 따른다.
|
||
- 발표자 모드(`PlatformChrome.IsVisible = false`)에서는 이 값과 무관하게 여백이 0이다.
|
||
- 코드에서 바꿔도 된다 — `PlatformChrome.SetFullBleed(view, true)`. 떠 있는 동안 바꿔도 셸이 다시
|
||
계산한다(`FullBleedChanged`).
|
||
- **비주얼 트리를 거슬러 올라가 Border의 Padding을 직접 0으로 만들지 말 것.** 플랫폼 레이아웃이
|
||
바뀌면 그대로 깨진다 — 이 속성이 그 자리를 대신한다.
|
||
|
||
## 9. 테마 · UI 컨벤션
|
||
|
||
- `Ubisam.Brush.*`, `Ubisam.Style.*`로 시작하는 리소스가 다크/라이트 테마 전체를 정의
|
||
(`src/UbisamBase.Core/Themes/Colors.Dark.xaml`, `Colors.Light.xaml`, `Controls.xaml`).
|
||
- `TextBox`/`ComboBox`/`CheckBox`/`Button`/`ToggleButton`/`DataGrid`는 전부 **암시적 스타일**
|
||
(`x:Key` 없음) — 그냥 `<Button>`, `<TextBox>`를 쓰면 자동으로 테마 적용됨. 예외 처리하려면
|
||
`Style="{x:Null}"`.
|
||
- 새 창(Window)을 별도로 띄울 땐(대화상자 등) `Window.Resources`에 `ShellStyles.xaml`을 다시
|
||
merge해야 하고, 루트 컨테이너에 `TextElement.Foreground="{DynamicResource Ubisam.Brush.ContentForeground}"`를
|
||
꼭 걸어야 함 — 안 그러면 스타일 없는 텍스트가 검정으로 보임(이 세션에서 실제로 겪은 버그).
|
||
- 테마가 바뀌면 `Application.Current.Resources.MergedDictionaries`에서 `Colors.*.xaml`을
|
||
갈아끼우는 방식(`UiSettingsService.ApplyTheme`) — DynamicResource로 참조해야 실시간 반영됨
|
||
(StaticResource는 최초 1회만 고정).
|
||
|
||
### 9-1. 설정 > UI Setting 항목 (`UiSettingsService`)
|
||
|
||
| 화면 항목 | 속성 | 설명 |
|
||
| --- | --- | --- |
|
||
| 테마 | `ThemeMode` | 시스템 설정 따르기 / 라이트 / 다크 |
|
||
| 네비게이션 위치 | `NavPosition` | 왼쪽 / 오른쪽 / 하단 |
|
||
| 상단 바 접기 | `TopBarCollapsed` | 상단 브랜딩 바(로고·장비명·시계와 로그/캡처/모드 변경 버튼)를 숨긴다 |
|
||
| 토스트 메시지 위치 | `ToastPosition` | 좌상단 / 우상단 / 좌하단 / 우하단 |
|
||
| 화면 배율 | `ChromeScale` | 상단 바·탭 바 배율. `BaselineChromeScale`(1.2)이 화면상 "100%" |
|
||
| 언어 | `LanguageService.CurrentCulture` | 등록된 언어 리소스가 있을 때만 보인다 |
|
||
|
||
- 바꾸면 화면에 즉시 반영되고, **"저장"을 눌러야** `<설정 루트>\Settings\UiSettingsService.json`에 남아
|
||
다음 실행에도 유지된다("새로고침"은 저장된 값으로 되돌린다).
|
||
- **상단 바 접기**는 사용자가 저장해 두는 취향이고, 8-1의 `PlatformChrome`(발표자 모드)은 코드에서 잠깐
|
||
껐다 켜는 모드다. 둘 중 하나라도 숨기라고 하면 상단 바는 숨는다.
|
||
|
||
### 9-2. 창 배치 (키오스크 / 창모드)
|
||
|
||
- 시작하면 키오스크(작업표시줄까지 덮는 전체화면). 상단 바의 "모드 변경" 버튼이나 **Ctrl+Shift+F**로
|
||
창모드와 오간다. Topmost는 걸지 않는다 — 현장의 다른 경보 창이 가려지면 안 되기 때문.
|
||
- 키오스크는 모니터 경계를 창 크기로 쓰되, **만들어진 창을 실측해 모니터보다 큰 만큼 되돌린다**
|
||
(`ShellWindow.FitToMonitor`). 화면 배율이 100%가 아닌 환경(예: 2560×1600 / 150%)에서 창이 20~30px
|
||
크게 만들어져 하단 네비게이션 탭 글자가 잘리던 문제 대응이다. 하단이 잘려 보이면 이 지점을 먼저 본다.
|
||
|
||
---
|
||
|
||
### 9-3. WebView2를 쓰는 모듈 (참고)
|
||
|
||
- **WebView2는 WPF의 앞뒤 순서를 따르지 않는다.** 자기 자식 윈도우에 직접 그리기 때문에 WebView2
|
||
위에 WPF 버튼을 올려도 보이지 않는다. 겹쳐 쓰려면 아래쪽 WebView2를 `Visibility.Hidden`으로 내린다.
|
||
- **사용자 데이터 폴더를 모듈마다 나눈다.** 같은 폴더를 쓰면 쿠키·로컬 저장소가 섞인다
|
||
(예: UbisamDeck은 `D:\UbisamConfig\WebView2`).
|
||
- WebView2 안쪽은 크로미움이라 **플랫폼 테마(WPF 스타일)가 닿지 않는다.** 스크롤바 같은 것은
|
||
그 웹 페이지 CSS에서 직접 맞춰야 한다(`::-webkit-scrollbar`).
|
||
|
||
---
|
||
|
||
## 10. 기존 C# 코드를 이 플랫폼에 연동하는 방법
|
||
|
||
### 10-1. 어디에 둘지 먼저 정한다
|
||
|
||
| 상황 | 두는 곳 |
|
||
| --- | --- |
|
||
| 여러 프로그램이 공유해서 쓸 공통 기능 | `UbisamBase.Core`에 새 폴더/네임스페이스로 옮기고 DI 싱글턴 등록 |
|
||
| 이 프로그램(소비 프로젝트)만 쓰는 기능 | 소비 프로젝트 안에 그대로 두고 `RegisterServices`에서 등록 |
|
||
|
||
공통으로 옮길 경우, `TimeOutManager`를 옮긴 실제 예시:
|
||
|
||
1. `src/UbisamBase.Core/Utils/TimeOutManager.cs`로 파일 복사, 네임스페이스를
|
||
`UbisamBase.Core.Utils`로 변경.
|
||
2. **외부 의존성 정리** — 원래 코드가 `Aurora.Api.Logger` 같은 이 플랫폼에 없는 타입을 쓰고
|
||
있으면, 시그니처가 호환되는 플랫폼 타입으로 바꿔치기한다. (`Logger(string tag)`,
|
||
`.I/.W/.E(string)` 시그니처가 동일해서 `using Aurora.Api;` → `using UbisamBase.Core.Logging;`
|
||
한 줄 교체로 끝났다.) 호환 안 되면 그 부분만 새로 짠다.
|
||
3. `src/UbisamBase.Core/DependencyInjection/ServiceCollectionExtensions.cs`의
|
||
`AddUbisamBaseCore`에 `services.AddSingleton<TimeOutManager>();` 추가.
|
||
4. 이제 어떤 ViewModel이든 생성자에 `TimeOutManager`를 선언하면 주입받는다.
|
||
|
||
### 10-2. 소비 프로젝트에서 플랫폼을 참조하는 방식 — 모두 D드라이브 배포 폴더 하나
|
||
|
||
플랫폼 원본 소스는 `D:\업무\03_개발\Source\용접기\08_UbisamPlatform`에 있고, 그걸 빌드한
|
||
결과물은 `D:\UbisamPlatform\bin`에 모인다. 플랫폼을 쓰는 모든 프로그램은 이 배포 폴더만 본다 —
|
||
플랫폼을 고치는 개발자의 테스트 프로그램(`TestApp`)도 예외 없이 같다.
|
||
|
||
소비 프로젝트(App, TestApp, MergeHub 등)는 `UbisamBase.Core`를 **ProjectReference가 아니라 파일
|
||
Reference**로, 항상 `D:\UbisamPlatform\bin\`을 가리켜 참조한다:
|
||
|
||
```xml
|
||
<UbisamPlatformDir>D:\UbisamPlatform\bin\</UbisamPlatformDir>
|
||
|
||
<Reference Include="UbisamBase.Core">
|
||
<HintPath>$(UbisamPlatformDir)UbisamBase.Core.dll</HintPath>
|
||
<Private>false</Private>
|
||
</Reference>
|
||
```
|
||
|
||
#### 전체 흐름
|
||
|
||
```
|
||
08_UbisamPlatform 빌드 (Debug/Release 아무거나)
|
||
└─ Directory.Build.targets가 결과물을 D:\UbisamPlatform\bin 에 반영
|
||
(.pdb 제외, Shell 설정 → Launcher 설정 복사, README 갱신)
|
||
|
||
소비 프로젝트 실행 (F5 또는 exe 더블클릭)
|
||
└─ UbisamBase.Launcher.exe
|
||
1) D:\UbisamPlatform\bin 전체를 자기 폴더로 복사
|
||
2) 같은 폴더의 Shell을 같은 프로세스 안에서 로드
|
||
3) Shell이 같은 폴더의 IAppSetup 구현 dll(소비 프로젝트)을 찾아 화면을 띄움
|
||
```
|
||
|
||
- 플랫폼을 고쳤으면 **플랫폼만 빌드하고 소비 프로젝트를 실행**하면 된다. 소비 프로젝트를 다시
|
||
빌드할 필요가 없다(빌드 절차가 없는 사용자 PC에서도 배포 폴더만 바뀌면 다음 실행부터 반영).
|
||
- Launcher가 Shell을 같은 프로세스에서 띄우므로 VS 디버거가 그대로 붙어 소비 프로젝트 중단점이 걸린다.
|
||
- 소비 프로젝트를 빌드할 때도 `CopyPlatformRuntime` 타겟이 배포 폴더 전체를 출력 폴더로 복사한다.
|
||
Launcher.exe 자체가 출력 폴더에 처음 자리 잡게 하는 용도다.
|
||
- Debug 빌드도 바로 반영되므로, 고치다 만 코드도 빌드하는 순간 이 PC의 모든 플랫폼 프로그램에
|
||
적용된다.
|
||
- Core만 빌드하면 Core.dll만, Shell을 빌드하면 Shell 출력 전체가 반영된다. 헷갈리면 솔루션
|
||
(`UbisamBase.Platform.slnx`) 단위로 빌드한다.
|
||
|
||
#### 깨끗한 배포본 — `scripts\publish-platform.ps1`
|
||
|
||
자동 반영은 덮어쓰기만 하므로, 의존 패키지를 빼는 등으로 쓰지 않게 된 옛 dll이 배포 폴더에
|
||
남는다. 다른 PC에 넘기기 전처럼 깨끗한 Release 배포본이 필요하면 이 스크립트를 실행한다:
|
||
|
||
```powershell
|
||
powershell -ExecutionPolicy Bypass -File scripts\publish-platform.ps1
|
||
```
|
||
|
||
```
|
||
1) D:\UbisamPlatform\bin 을 비운다
|
||
2) UbisamBase.Platform.slnx 를 Release로 빌드 → Directory.Build.targets가 폴더를 다시 채움
|
||
3) 필수 파일(Core.dll, Shell.exe, Launcher.exe, Launcher 설정, README) 확인
|
||
4) Launcher 설정에 바인딩 리디렉트가 있는지 확인
|
||
— Launcher 프로세스 안에서 Shell이 돌기 때문에 실제로 적용되는 건 Launcher 쪽 설정이다.
|
||
이게 없으면 실행 직후 FileLoadException으로 조용히 죽는다.
|
||
5) .pdb/.cs가 남아 있으면 에러로 중단 (안전장치)
|
||
```
|
||
|
||
다른 PC에서 쓰려면 `D:\UbisamPlatform\bin` 폴더를 통째로 그 PC의 같은 위치에 복사한다.
|
||
|
||
외부 NuGet 패키지가 새로 필요한 경우(예: LiveChartsCore)도 Core에 `PackageReference`를
|
||
추가하면 빌드 시 그 dll들이 배포 폴더에 자동으로 포함된다. 소비 프로젝트가 컴파일 타임에 그
|
||
타입을 쓰려면 소비 프로젝트 쪽에도 `$(UbisamPlatformDir)해당dll.dll`을 가리키는 파일
|
||
Reference를 추가해야 한다.
|
||
|
||
### 10-3. 체크리스트
|
||
|
||
- [ ] 네임스페이스를 `UbisamBase.Core.*`(공통) 또는 소비 프로젝트 네임스페이스(전용)로 정리했는가
|
||
- [ ] 이 플랫폼에 없는 외부 타입(로거, 설정 저장 등)을 플랫폼 대응 타입으로 교체했는가
|
||
- [ ] 공통이면 `AddUbisamBaseCore`에 등록, 전용이면 `IAppSetup.RegisterServices`에 등록했는가
|
||
- [ ] 플랫폼(Core/Shell/Launcher)을 빌드했는가 (빌드하면 `D:\UbisamPlatform\bin`에 자동 반영)
|
||
- [ ] `UbisamPlatformDir`이 `D:\UbisamPlatform\bin\`이고, `StartProgram`이 `$(MSBuildProjectDirectory)` 기반 절대경로로 Launcher를 가리키는가
|
||
- [ ] 플랫폼이 Any CPU인가 (csproj `<Platforms>`, `.slnx`에 x64 없음)
|
||
- [ ] F5로 떴을 때 `UbisamBase.Launcher` 프로세스 하나만 있고, 중단점이 걸리는가
|
||
- [ ] (외부 NuGet이 새로 필요하면) 소비 프로젝트에도 컴파일 타임 참조를 추가했는가
|
||
|
||
---
|
||
|
||
## 11. 저장 위치 요약
|
||
|
||
| 경로 | 내용 |
|
||
| --- | --- |
|
||
| `D:\UbisamConfig\Settings\` | 설정 클래스별 JSON |
|
||
| `D:\UbisamConfig\Log\` | 날짜별 로그 (`yyyyMMdd.log`) |
|
||
| `D:\UbisamConfig\ScreenSaver\` | 화면 잠금 시 마지막 아이디/비밀번호 |
|
||
|
||
여러 프로그램이 이 플랫폼으로 만들어져도 전부 `D:\UbisamConfig` 한 곳에 모입니다.
|
||
|
||
---
|
||
|
||
## 12. 플랫폼 자동 업데이트 (깃)
|
||
|
||
프로그램을 켤 때 깃 저장소의 업데이트 파일과 지금 깔린 플랫폼(`D:\UbisamPlatform\bin`)을 비교해,
|
||
새 버전이면 받아서 켠다. 확인과 내려받기는 **Launcher**가 Shell을 띄우기 전에 한다.
|
||
|
||
### 12-1. 동작 순서
|
||
|
||
```
|
||
설정에서 켜져 있나? → 아니오면 끝
|
||
인터넷이 연결됐나? → 아니오면 끝(조용히)
|
||
저장소에 새 커밋이 있나?(git ls-remote) → 없으면 끝
|
||
dist의 버전이 지금 것보다 새것인가? → 아니면 커밋만 기록하고 끝
|
||
자동 업데이트인가? → 예: 바로 받기 / 아니오: "업데이트할까요?" 물어보고 예일 때만 받기
|
||
```
|
||
|
||
- 확인·받기는 몇 초가 걸리므로 그동안 **진행 창**이 뜬다(확인 중 → 새 버전을 찾았습니다 → 받기 완료,
|
||
진행률은 git이 흘리는 값을 그대로 읽어 표시한다). 받기가 **끝난 뒤에** 설치 여부를 묻는다 —
|
||
"예"를 누르고 또 기다리지 않게 하기 위함이다.
|
||
- 어느 단계에서 막히든(설정 꺼짐 · 네트워크 없음 · git 미설치 · 저장소 접근 실패) **조용히 넘어가고
|
||
기존 버전으로 실행한다.** 업데이트 때문에 프로그램이 안 켜지는 일은 없어야 하기 때문이다.
|
||
- 물어봤을 때 "나중에"를 고르면 아무것도 기록하지 않는다 — 다음에 켤 때 다시 물어본다.
|
||
- 확인 과정은 `D:\UbisamPlatform\update-log.txt`에 한 줄씩 남는다(실행할 때마다 덮어씀).
|
||
"왜 업데이트 창이 안 뜨지?" 싶을 때 이 파일을 먼저 본다.
|
||
- **프로그램의 진입점은 항상 런처다.** 앱 폴더에는 `UbisamBase.Shell.exe`도 같이 있지만 그것을 직접
|
||
실행하면 갱신 없이 옛 파일로 뜨게 되므로, Shell은 자기가 직접 실행된 것을 알아채면 조용히 런처를
|
||
띄우고 종료한다(`App.RedirectToLauncher`). 바로가기를 만들 때도 런처를 가리켜야 한다.
|
||
- 받는 것은 `dist` 폴더뿐이다(`--depth 1 --filter=blob:none --sparse`). 소스 이력은 받지 않는다.
|
||
- git 자격 증명 창이 뜨지 않게 막아 두었다(`GIT_TERMINAL_PROMPT=0`). 사내 비공개 저장소라면 그 PC에서
|
||
한 번은 `git` 자격 증명이 저장돼 있어야 한다.
|
||
|
||
### 12-2. 쓰는 쪽 설정 — 설정 > UI Setting > "플랫폼 업데이트"
|
||
|
||
| 항목 | 값 |
|
||
| --- | --- |
|
||
| 사용 / 사용 안 함 | 업데이트 확인 기능 자체를 켜고 끈다(기본: 사용 안 함) |
|
||
| 자동으로 업데이트 / 업데이트할지 물어보기 | 새 버전을 찾았을 때 묻지 않고 받을지 |
|
||
| 저장소 주소 | 플랫폼 저장소 URL. 비어 있으면 확인하지 않는다 |
|
||
|
||
값은 `D:\UbisamPlatform\update-settings.json`에 저장된다(배포 폴더 **바깥** — 업데이트가 `bin`을
|
||
통째로 덮어쓰기 때문). 스키마는 아래와 같고, Core(`PlatformUpdateSettings`)와
|
||
Launcher(`PlatformUpdater.Settings`)가 같은 이름으로 읽고 쓴다. **이름을 바꾸면 양쪽을 같이 고칠 것.**
|
||
|
||
```json
|
||
{
|
||
"Enabled": true,
|
||
"AutoUpdate": false,
|
||
"RepositoryUrl": "https://.../UbisamPlatform.git",
|
||
"Branch": "main",
|
||
"DistPath": "dist",
|
||
"InstalledVersion": "2026-09-21 21:53:23",
|
||
"InstalledCommit": "a1b2c3..."
|
||
}
|
||
```
|
||
|
||
`InstalledVersion`/`InstalledCommit`은 Launcher가 기록한다(손대지 않는다).
|
||
|
||
### 12-3. 내는 쪽 — 새 버전 배포하기
|
||
|
||
1. `scripts\publish-platform.ps1` 실행 — `D:\UbisamPlatform\bin`을 비우고 Release로 다시 채운 뒤,
|
||
저장소의 `dist\` 폴더를 그 내용과 똑같이 맞춘다(robocopy /MIR).
|
||
2. `git add dist && git commit && git push`.
|
||
3. 다른 PC는 다음 실행 때 새 커밋을 보고 업데이트한다.
|
||
|
||
- 버전 기준은 `platform-version.json`의 `version`(= 배포 시각)이다. 빌드할 때마다
|
||
`Directory.Build.targets`가 `D:\UbisamPlatform\bin`에 찍고, 그 파일이 `dist`에 함께 올라간다.
|
||
- 소스만 바꾼 커밋(=`dist`가 그대로)에는 아무에게도 업데이트를 묻지 않는다.
|
||
- 업데이트는 **덮어쓰기**다. 쓰지 않게 된 옛 파일까지 정리하려면 `publish-platform.ps1`로 `bin`을
|
||
비우고 다시 채운 뒤 `dist`를 올리면 된다.
|
||
|
||
---
|
||
|
||
## 13. 사내 API 호출 — `UbisamBase.Core.Net.ApiService`
|
||
|
||
토큰이 필요한 사내 API를 **앱이 대신 호출해** 응답 본문(JSON 문자열)만 돌려준다. 화면(WebView2 등)이
|
||
직접 부르면 토큰이 그 문서에 섞여 들어가므로, 앱이 부르고 값만 넘기는 구조를 공용으로 둔 것이다.
|
||
브라우저에서 막히는 CORS도 이 경로로는 문제가 되지 않는다.
|
||
|
||
```csharp
|
||
using UbisamBase.Core.Net;
|
||
|
||
var json = await ApiService.Current.FetchAsync(url); // 실패하면 예외
|
||
var (ok, body, error) = await ApiService.Current.TryFetchAsync(url); // 예외 대신 결과로 받기
|
||
```
|
||
|
||
- 인증 헤더는 서비스 안에서 붙는다 — **부르는 쪽은 토큰을 몰라도 된다.**
|
||
- 같은 주소를 `MinIntervalMs`(기본 1초) 안에 다시 부르면 서버를 때리지 않고 직전 응답을 돌려준다.
|
||
화면 갱신 주기가 짧아도 호출 제한에 걸리지 않게 하기 위한 것이다.
|
||
- 로그에는 주소의 `?` 앞부분만 남긴다(쿼리에 토큰이 섞인 경우 대비). 응답 본문도 남기지 않는다.
|
||
|
||
### 13-1. 설정 — `D:\UbisamConfig\Settings\ApiSettings.json`
|
||
|
||
```json
|
||
{
|
||
"AuthMode": "BearerHeader",
|
||
"HeaderName": "X-Api-Key",
|
||
"Token": "...",
|
||
"MinIntervalMs": 1000,
|
||
"TimeoutSeconds": 10
|
||
}
|
||
```
|
||
|
||
| AuthMode | 붙는 헤더 |
|
||
| --- | --- |
|
||
| `None` | 없음(사내망 IP 제한 등) |
|
||
| `BearerHeader` | `Authorization: Bearer {Token}` |
|
||
| `CustomHeader` | `{HeaderName}: {Token}` |
|
||
| `Cookie` | `Cookie: {Token}` |
|
||
|
||
**토큰은 이 파일에만 있다.** 내보낸 HTML·`.ub` 같은 산출물에는 들어가지 않는다.
|
||
|
||
### 13-2. 내보낸 파일에서는 인증 API를 못 부른다
|
||
|
||
앱 밖으로 내보낸 HTML은 혼자 도는 파일이라 이 서비스가 없다. 그래서:
|
||
|
||
- **앱 안에서 볼 때** → `ApiService`로 최신 값
|
||
- **내보낸 파일을 남에게 줄 때** → 인증이 필요한 API는 호출 불가, 내보낼 때 담긴 마지막 값이 보인다
|
||
(인증이 없는 공개 API라면 내보낸 파일에서도 실시간으로 돈다)
|
||
|
||
이는 제약이라기보다 의도된 동작이다 — 토큰이 파일에 들어가지 않기 때문이다.
|
||
|