docs: 상업화 전략 문서 3종 — MAUI 진단·5대 게이트·UI 경로B #13

Open
windpacer wants to merge 1 commits from docs/commercialization-and-ui-plans into main
3 changed files with 1218 additions and 0 deletions

View File

@@ -0,0 +1,168 @@
# UI 경로 B: 산업 HMI 품질 폴리시 플랜 (vanilla SPA 전문화 — 재작성 없이)
> 목표 = **산업용 HMI/SCADA 제품 수준**의 UI 품질. 프레임워크 전면 재작성(경로 A) 없이,
> 현재 vanilla SPA를 전문화(빌드·디자인시스템·컴포넌트·상태처리)하여 상업 출하 가능 수준으로.
> ([작업플랜-MAUI-UI-이전.md §13.4](작업플랜-MAUI-UI-이전.md) 대안 B의 상세화)
> 2026-06-26 작성. 대상 = `src/Hc900Crawler/wwwroot`.
---
## 0. 현재 상태 실측 (정량)
| 항목 | 실측 | 함의 |
|------|------|------|
| 인라인 `style=` | **252개**(panes+index) | 일관성 **최대 리스크** — 변경·테마 불가 |
| 디자인 토큰 | 22개(`--bd/--t0~2/--s0~4/--red/grn/blu` 등) | 체계는 있으나 빈약·미문서 |
| 토큰 미사용 하드코딩 hex | `#888`×42, `#555`×18, `#4af`×17, `#aaa`×14, `#f66`×11 … | 토큰 우회 → 시각 드리프트 |
| 빌드 파이프라인 | **없음**(번들러·lint·최소화 0) | 품질 게이트 부재, 수작업 유지 |
| 상태 처리(로딩/에러/빈) | 247건 산재(임시방편) | 컴포넌트화 안 됨 → 불일치 |
| 셸 | `paneInit.<tab>` 셀프등록 + innerHTML 주입 | 유지하되 컴포넌트와 공존 설계 |
| 접근성/반응형 | aria≈0, `@media`×4 | 산업 바 수준으로 보강 필요 |
| 차트/시각화 | uPlot·ECharts·mermaid(검증 라이브러리) | **자산 — 유지** |
**전략 원칙:** ① 프레임워크 도입 0(학습·재작성 비용 회피) ② `paneInit` 셸·차트 라이브러리 **유지** ③ 인라인 스타일·하드코딩 색을 **토큰+컴포넌트**로 흡수 ④ 빌드/lint로 품질을 **자동 강제**.
---
## B1. 빌드 파이프라인 도입 (Vite, 프레임워크 없음)
### 목표
번들·최소화·소스맵·lint를 갖추되 **vanilla 유지**(Vite는 무프레임워크 정적 자산도 처리).
### 작업
1. `package.json` + **Vite**(vanilla 템플릿). `wwwroot/`를 소스로, `wwwroot/dist/`를 ASP.NET 정적 서빙 타깃으로.
2. 산출: ES모듈 번들 + 해시 파일명(캐시버스팅) + 소스맵. CDN/로컬 lib는 `lib/`로 유지하거나 npm 의존성화(uPlot/echarts/marked).
3. **lint/format**: ESLint + Prettier + **Stylelint**(하드코딩 hex 금지 룰 → 토큰 강제).
4. ASP.NET 연동: `dotnet build``npm run build` 훅(csproj `Target` 또는 CI 단계).
5. 개발 편의: `npm run dev`(HMR) — 단, 서버 API는 LAN 프록시.
### 공수: **~1주**. 수용: `npm run build` 산출물 서빙, lint가 CI에서 hex/인라인 위반 차단.
---
## B2. 디자인 시스템 정식화 (토큰 + 인라인/하드코딩 제거)
### 목표
22개 토큰을 **문서화된 디자인 시스템**으로 확장, **252 인라인 + 하드코딩 hex를 토큰/유틸로 흡수**.
### 작업
1. **토큰 확장·명명 정리**(`tokens.css`): 색(표면 s0~4, 텍스트 t0~2, 의미색 alarm/run/trip/warn/ok), 간격 스케일, 폰트, 반경, 그림자, z-index, 트랜지션. 축약 별칭은 유지하되 **의미 토큰 추가**(`--c-alarm` 등).
2. **유틸리티 클래스 셋**(경량): `.row/.col/.gap-2/.muted/.card/.badge` 등 — 252 인라인의 80%는 레이아웃 반복(`display:flex;gap:8px`)이므로 유틸로 대체.
3. **Stylelint 규칙으로 회귀 차단**: raw hex·raw px(스케일 외) 금지 → 신규 코드가 토큰만 쓰도록 강제.
4. **마이그레이션**: pane별로 인라인→클래스 치환(점진, 시각 회귀 테스트로 안전망).
5. **스타일 가이드 페이지**(`/styleguide.html`): 토큰·컴포넌트 카탈로그(개발자 단일 참조).
### 공수: **~1.5주**(pane별 분할). 수용: 인라인 style ≤ 20(불가피 동적값만), 하드코딩 hex 0, 스타일가이드 존재.
---
## B3. HMI 컴포넌트 키트 (vanilla, paneInit 공존)
### 목표
산업 HMI 1차 프리미티브를 **재사용 컴포넌트**로 — 화면 간 일관성의 핵심.
### 컴포넌트(예시)
| 컴포넌트 | 용도 |
|---|---|
| `TagCard` | 태그값+단위+품질+추세 스파크 |
| `StatusBadge` | RUN/STOP/TRIP/ALARM 의미색 |
| `AlarmRow` | 심각도·시각·확인 |
| `DataGrid` | 정렬·필터·가상스크롤(대량 태그) |
| `LoadingState`/`EmptyState`/`ErrorState`/`StaleBadge` | 상태 표준화(B4) |
| `Toolbar`/`Field`/`Modal` | 폼·다이얼로그 공통 |
### 작업
1. 구현 방식: **표준 Web Components(Custom Elements)** 또는 경량 팩토리 함수 — 프레임워크 무도입, `paneInit`과 자연 공존.
2. 차트 래퍼: uPlot/ECharts를 `<trend-chart>` 등으로 감싸 옵션 표준화(축·색·다운샘플 일관).
3. 각 pane을 컴포넌트 조합으로 점진 리팩터(전면 재작성 아님).
### 공수: **~2주**. 수용: 8개 핵심 컴포넌트 + 스타일가이드 등재, 2개 이상 pane이 컴포넌트로 재구성.
---
## B4. 시스템 상태 처리 표준화 (HMI 안전 직결)
### 목표
로딩/에러/빈/**stale(낡은 데이터)** 를 일관 컴포넌트로 — 산업 모니터링에서 **stale 표시는 안전 요건**.
### 작업
1. 247건 산재 상태처리를 `LoadingState/EmptyState/ErrorState`로 통일.
2. **데이터 품질 표면화**: 게이트웨이 quality(192=good/0=stale)·`recorded_at` 경과를 **StaleBadge**로 시각화(예: N초 이상 미갱신 시 회색+경고). 데모/실데이터 구분 컨텍스트도 반영.
3. 쓰기·생성 등 액션의 **낙관/실패 피드백** 표준(토스트/인라인 에러). 0 날조 금지 원칙과 일치(N/A 명시).
### 공수: **~1주**. 수용: 모든 데이터 표면에 4상태 일관 적용, stale 임계 초과 시 시각 경고.
---
## B5. HMI 특화 비주얼 품질
### 목표
관제실/패널PC 환경에 맞춘 산업 UX(소비자 SaaS와 다른 바).
### 작업
1. **의미색 표준**: alarm(적)·trip·run(녹)·warn(황)·ok — 색맹 대비 보조(아이콘/패턴 병행).
2. **고대비 다크**: 관제실 조도 대응, 대비비(contrast ratio) 확보.
3. **데이터 밀도**: 정보 우선 레이아웃(여백 과다 지양), 표·그리드 최적화.
4. **대형 터치 타깃**: 패널PC 터치 대응(버튼·행 최소 크기).
5. **새로고침/연결 상태** 글로벌 표시(게이트웨이 health, 폴링 주기).
### 공수: **~1주**. 수용: 의미색·대비 기준 통과, 패널 해상도(예 1920×1080/터치)에서 사용성 확인.
---
## B6. 접근성 · 반응형 (산업 바)
### 목표
풀 WCAG SaaS가 아닌 **현장 실용 수준**: 키보드 운용·포커스·대비·주요 해상도.
### 작업
1. 키보드 네비게이션·포커스 링·`aria-label`(주요 상호작용 요소).
2. 단축키(§MAUI 10.4 참조: 탭전환·검색·새로고침)를 웹에서 구현.
3. 반응형: 패널PC·노트북·태블릿 주요 브레이크포인트(`@media` 확장).
### 공수: **~1주**. 수용: 키보드만으로 핵심 플로우 수행 가능, 3개 해상도 레이아웃 정상.
---
## B7. 품질 게이트 (회귀 방지)
### 작업
1. **CI lint**: ESLint/Stylelint/Prettier 위반 시 빌드 실패(하드코딩 hex·인라인 회귀 차단).
2. **시각 회귀 테스트**: Playwright 스크린샷(밀도 높은 화면 다수 → 자동 비교로 의도치 않은 시각 변화 감지).
3. **번들 예산**: 용량 상한 알림(라이브러리 비대화 방지).
### 공수: **~0.5주**(B1 위에). 수용: CI에 lint+시각회귀 게이트 green.
---
## 1. 타임라인 (~67주, 병렬화 가능)
| 주차 | 작업 |
|------|------|
| 1주 | **B1 빌드 파이프라인** + B7 lint 골격 |
| 23주 | **B2 디자인 시스템** + 인라인/hex 마이그레이션 |
| 35주 | **B3 컴포넌트 키트**(B2와 겹침) |
| 5주 | **B4 상태 표준화** |
| 6주 | **B5 HMI 비주얼** + **B6 접근성/반응형** |
| 67주 | **B7 시각 회귀** 완비 + 잔여 pane 마이그레이션 |
**의존성:** B2/B3 ← B1(빌드). B4 ← B3(상태 컴포넌트). B7 ← B1.
---
## 2. 수용 기준 (산업 HMI 출하)
- [ ] 인라인 style ≤ 20(동적값만), 하드코딩 hex 0 — 전부 토큰/유틸
- [ ] 8개 핵심 컴포넌트 + 스타일가이드 카탈로그
- [ ] 로딩/에러/빈/stale 4상태 일관 적용, stale 시각 경고
- [ ] 의미색·고대비·데이터밀도·터치타깃 기준 통과
- [ ] 키보드 운용 + 주요 3해상도 반응형
- [ ] CI: lint + 시각 회귀 게이트 green, 번들 예산 준수
---
## 3. 경로 A로의 확장 여지 (미래)
B3 컴포넌트 키트를 **표준 Web Components**로 만들면, 후일 Svelte/React(경로 A)로 갈 때도 그대로 재사용 가능 → **경로 B가 경로 A의 가교**가 된다(매몰비용 아님). 산업 바는 B로 충분, SaaS 폴리시가 계약요구가 되면 A로 점진 승격.
> **권고:** 보안 5대 게이트가 출하의 *필수조건*이고, 본 UI 폴리시는 *제품 완성도* 영역. 우선순위는 **보안 게이트 → UI 경로 B 병렬**. UI는 재작성이 아니라 전문화이므로 기존 기능 동작을 깨지 않고 점진 적용한다.

View File

@@ -0,0 +1,236 @@
# 상업화 플랜: 5대 게이트 (인증 · 감사로그 · HTTPS · API계약 · 코드서명)
> HC900-AX를 "사내 도구"에서 "판매 가능한 상업 제품"으로 끌어올리기 위한 백엔드 강화 플랜.
> 상업용 수준을 가르는 것은 데스크톱 UI 프레임워크가 아니라 이 다섯이다.
> ([작업플랜-MAUI-UI-이전.md §13](작업플랜-MAUI-UI-이전.md) 의 후속 상세 계획)
> 2026-06-24 작성. 대상 = `src/Hc900Crawler` (ASP.NET Core, Linux aarch64 서버).
---
## 0. 현재 상태 스냅샷 (코드 실측)
| 항목 | 실측 결과 | 근거 |
|------|-----------|------|
| 태그 쓰기 보안 | **인증 0**`POST /api/gateway/write``[Authorize]` 없음 | `Controllers/Hc900Controllers.cs:83` |
| 기존 인증 | KB 전용 **단일 비번** + 세션토큰(DB·만료·IP) | `Infrastructure/Kb/KbAuthService.cs` |
| 비번 해시 | `PasswordHasher.Hash`(hash+salt) **이미 존재** → 재활용 | `KbAuthService.cs:69,124,145` |
| 인증 미들웨어 | `AddAuthentication/UseAuthorization` **없음**, `[Authorize]` 0건 | `Program.cs` |
| 전송 | **HTTP 평문** `0.0.0.0:5000`, HSTS 없음 | `Program.cs:177` |
| CORS | `UseCors()` **전면 개방** | `Program.cs:221` |
| API 계약 | **익명객체 326곳**, PascalCase 불일치 수정 이력 | `grep new {`, git log |
| 감사 | 사용자 행위 감사 **없음** (event_history는 디지털 상태변경용) | `DbContext.cs:743` |
| 서명/배포 | 없음 | — |
**위협 모델 1순위:** *평문 LAN에서 인증 없는 셋포인트 쓰기.* → 게이트 1(인증)·3(HTTPS)를 가장 먼저.
---
## 게이트 1 — 인증 / 인가 (authN / authZ)
### 1.1 목표
- 공유 비번 → **사용자별 계정 + 역할(RBAC)**.
- 표준 ASP.NET Core 인증 미들웨어로 **쓰기·관리 엔드포인트 보호**.
- 역할: `Viewer`(읽기) · `Operator`(승인된 쓰기) · `Engineer`(쓰기+설정) · `Admin`(사용자·시스템).
### 1.2 방식 결정 (권장)
**커스텀 토큰 인증 핸들러로 기존 자산 재활용** (외부 IdP 불필요, 저위험):
- 이미 있는 `PasswordHasher` + 세션토큰 테이블 패턴을 **앱 전역**으로 승격.
- `AuthenticationHandler<>` 구현 → `Authorization: Bearer <token>`(또는 기존 `X-Kb-Token` 별칭) 검증 → 역할 클레임 부여.
- 향후 SSO 요구 시 OIDC(Keycloak/Entra)로 마이그레이션 경로 열어둠. **1단계는 커스텀, 2단계는 OIDC 옵션.**
> 대안(헤비): ASP.NET Core Identity + JWT/OIDC. 표준적이나 외부 IdP·스키마 도입 비용. SSO가 계약 요구사항일 때만.
### 1.3 작업
1. **`app_user` 테이블**: `id, username UNIQUE, password_hash, salt, role, enabled, created_at, last_login`. (`PasswordHasher` 재사용)
2. **세션토큰 확장**: 기존 토큰행에 `user_id, role` 추가(또는 신규 `auth_session`).
3. **`ApiTokenAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions>`** — 토큰 검증 → `ClaimsPrincipal`(NameIdentifier, Role) 발급, 만료/비활성 거부.
4. **`Program.cs` 배선** (현 `177~221` 구간). **인가 모델 = 기본 공개(읽기 무로그인), 보호는 명시 `[Authorize]`만** (결정 #2):
```csharp
builder.Services.AddAuthentication("ApiToken")
.AddScheme<AuthenticationSchemeOptions, ApiTokenAuthenticationHandler>("ApiToken", null);
builder.Services.AddAuthorization(o => {
o.AddPolicy("CanWrite", p => p.RequireRole("Operator","Engineer","Admin"));
o.AddPolicy("CanConfig", p => p.RequireRole("Engineer","Admin"));
o.AddPolicy("CanAdmin", p => p.RequireRole("Admin"));
// FallbackPolicy 설정 안 함 → 미표시 엔드포인트는 익명 허용(현장 대시보드 무로그인).
});
// app.UseAuthentication(); app.UseAuthorization(); ← MapControllers 앞
```
> 핸들러는 토큰이 없어도 **401을 던지지 않고** 익명으로 통과시킨다(`AuthenticateResult.NoResult()`); 보호는 `[Authorize]`가 붙은 곳에서만 발동. 읽기 조회 컨트롤러는 무표시 → 무로그인 노출.
5. **엔드포인트 보호** (쓰기·변경·관리에만):
- `GatewayController.Write` → `[Authorize(Policy="CanWrite")]` (**최우선**).
- PointBuilder·Setup·Schedule·KbAdmin·사용자관리 → `CanConfig`/`CanAdmin`.
- 로그인·헬스체크·**모든 읽기 조회**(realtime/history/events/metadata/report 조회/summary 등) → 무표시(익명).
- ⚠️ 주의: 읽기여도 **부작용 있는 POST**(예: T2S 임의 SQL 실행, 리포트 생성 등 자원 소모/데이터 노출)는 익명 범위에서 제외할지 개별 검토 — 무로그인은 *순수 조회*로 한정.
6. **로그인 API 통합**: `POST /api/auth/login` → `{token, role, expiresAt}`. 클라이언트가 헤더로 송신.
7. **사용자 관리 API**(Admin): 생성/비활성/역할변경/비번리셋.
8. **KbAuth 통합**: `X-Kb-Token`을 신규 스킴으로 흡수(별칭 유지 후 폐기).
### 1.4 손댈 파일
`Infrastructure/Auth/`(신규: Handler, UserService, AuthSession), `Program.cs`, `Controllers/Hc900Controllers.cs`(write), 관리 컨트롤러 전반, `Infrastructure/Kb/KbAuthService.cs`(흡수).
### 1.5 수용 기준
- **읽기 조회는 무로그인 200**(현장 대시보드). 미인증 **write → 401**, Viewer write → **403**, Engineer write → **200**.
- 토큰 만료/비활성 계정 거부. 비번은 평문 저장 0(해시+솔트).
### 1.6 공수: **~2주** (모든 게이트의 기반)
---
## 게이트 2 — 감사 로그 (audit log)
### 2.1 목표
**누가/언제/무엇을** 변경했는지 **불변(append-only)** 기록. 태그 쓰기·로그인·설정변경·사용자관리 대상.
### 2.2 작업
1. **`audit_log` 테이블**:
`id bigserial, ts timestamptz, user_id, username, role, action, target, old_value, new_value, client_ip, result, detail jsonb`.
- 인덱스: `(ts desc)`, `(user_id, ts)`, `(action, ts)`, `(target, ts)` — `event_history_table` 인덱스 패턴(`DbContext.cs:763~`) 재사용.
- **DB레벨 불변성**: 앱 역할에 `INSERT, SELECT`만 GRANT, `UPDATE/DELETE` REVOKE → 위변조 차단.
2. **`IAuditService.WriteAsync(...)`** — raw ADO 패턴(`FeedforwardAuditService`/`ReportTemplateStore`와 동일) 재사용.
3. **훅 지점**:
- `GatewayController.Write`: 쓰기 직전 캐시값=old, 인자=new, 성공/실패·IP 기록.
- 로그인 성공/실패(잠금 추적 기반).
- PointBuilder 활성화, Schedule 변경, 사용자 CRUD.
- (선택) 변경계열(POST/PUT/DELETE) 자동 캡처용 `IAsyncActionFilter` + 쓰기엔 값 캡처.
4. **감사 조회/내보내기 API**(Admin/Auditor): `GET /api/audit?from=&to=&user=&action=` 페이지네이션 + CSV.
5. **보존**: ≥1년. 대용량 시 TimescaleDB hypertable(=`history_table` 방식).
### 2.3 손댈 파일
`DbContext.cs`(마이그레이션 블록 `743` 패턴), `Infrastructure/Audit/AuditService.cs`(신규), write/login/config 경로 배선, `Controllers/AuditController.cs`(신규).
### 2.4 수용 기준
- 성공한 쓰기마다 **정확히 1개** 불변 감사행(old/new/user/ip).
- 앱 역할로 `audit_log` DELETE 시도 → **실패**.
### 2.5 공수: **~1주** (게이트 1의 user 신원에 의존)
---
## 게이트 3 — HTTPS / TLS
### 3.1 목표
전송 암호화. 평문 `0.0.0.0:5000` 노출 제거.
### 3.2 방식 결정 (권장)
**리버스 프록시(Caddy/nginx)가 TLS 종단**, Kestrel은 `127.0.0.1:5000`으로만 청취:
- Caddy 권장(자동 인증서·간결). 공인 DNS 있으면 Let's Encrypt, 없으면 내부 CA/`tls internal`.
- 플랜트 LAN(공인 DNS 없음): **내부 CA 또는 mkcert** 발급 → Windows 클라이언트에 루트 신뢰 설치(문서화).
> 대안: Kestrel 직접 HTTPS(`UseHttps` + pfx). 인프라 단순하나 앱이 인증서 수명 관리. 단일 호스트면 허용.
### 3.3 작업
1. **Kestrel 바인드 변경**: `Program.cs:177` `0.0.0.0:5000` → `127.0.0.1:5000`(프록시만 도달).
2. **리버스 프록시 설정**(`deploy/Caddyfile`):
```
hc900.plant.local {
tls internal # 또는 공인 인증서
reverse_proxy 127.0.0.1:5000
}
```
3. **프록시 뒤 신원 보존**: `app.UseForwardedHeaders(...)` → 감사 `client_ip` 정확. 운영 `UseHsts()`.
4. **CORS 강화**: 전면 `UseCors()`(`Program.cs:221`) → 명시 origin 화이트리스트 또는 제거(동일출처 SPA + 네이티브 클라이언트는 CORS 불요).
5. **클라이언트 주소**: MAUI/WebView2 `Backend:BaseUrl` → `https://hc900.plant.local`.
6. **gRPC(50051)**: 게이트웨이↔크롤러 내부 통신 — 1단계는 localhost 평문 유지, 추후 mTLS(주석).
### 3.4 손댈 파일
`Program.cs`(177 바인드, 221 CORS, ForwardedHeaders/HSTS 추가), `deploy/Caddyfile`(신규), 클라이언트 설정, 클라이언트 루트인증서 배포 가이드.
### 3.5 수용 기준
- HTTP→HTTPS 강제, 클라이언트에서 인증서 신뢰됨.
- Kestrel이 LAN에서 직접(5000) 접근 불가. 감사 IP 정확.
### 3.6 공수: **~1주** (대부분 인프라+인증서 배포)
---
## 게이트 4 — API 계약 (OpenAPI + 타입 DTO)
### 4.1 목표
익명객체 326곳 → **버전드·타입 계약** → 클라이언트 DTO **자동 생성**(드리프트 0).
### 4.2 전략 (점진, 빅뱅 금지)
1. **JSON 정책 고정**: `JsonSerializerOptions` 전역 1회 결정(camelCase 권장) → PascalCase 불일치 재발 차단(과거 수정 이력의 근본해결).
2. **Swashbuckle(Swagger)** 도입 → OpenAPI 노출. *단, 익명객체는 스키마가 무의미* → 응답 DTO 도입이 전제.
3. **소비 엔드포인트 우선 타입화**(클라이언트가 쓰는 것부터): gateway read/write/health, realtime, history, events, metadata, **report**, setup, t2s. 응답 `record`를 `Hc900.Shared`에 정의.
4. **API 버저닝**: `Asp.Versioning.Mvc`, 경로 `/api/v1/...`. v1 동결.
5. **클라이언트 생성**: NSwag/openapi-generator로 서버 OpenAPI → `Hc900.Shared` 타입 클라이언트(단일 진실원, 수기 매핑 폐기).
6. **계약 테스트**: 응답 스키마 스냅샷/라운드트립 테스트를 CI에 → 모양 변경 자동 감지.
### 4.3 손댈 파일
`Program.cs`(AddSwaggerGen, JsonOptions, AddApiVersioning), 우선순위 컨트롤러(타입 반환), `Hc900.Shared`(DTO 또는 생성물), CI(계약 테스트).
### 4.4 수용 기준
- Swagger UI에 타입 스키마 노출, NSwag 생성 클라이언트 컴파일 성공.
- 전 응답 단일 casing, CI 계약 테스트 green.
### 4.5 공수: **~23주** (최대. 컨트롤러별 단계 분할 가능, 병렬화 용이)
---
## 게이트 5 — 코드 서명 + 배포
### 5.1 목표
서명된 MSIX(앱) + 자동 업데이트. SmartScreen 차단/경고 제거.
### 5.2 작업
1. **코드서명 인증서** — **추천: OV + 클라우드 서명(Azure Trusted Signing)** (결정 #4).
- **근거**: 본 제품은 *플랜트 사이트(알려진 고객)에 사이드로딩 MSIX*로 배포 — MS Store 대중 다운로드가 아님. 따라서 EV의 "즉시 SmartScreen 평판"이 결정적이지 않다(클라이언트에 루트 신뢰 선설치도 가능).
- 2023년 CA/B 규정 이후 **OV·EV 모두 HSM 보관 필수** → "EV만 토큰" 구분은 사라짐. **클라우드 서명**(Azure Trusted Signing ~$10/월, 또는 DigiCert KeyLocker/SSL.com eSigner)이 HSM을 대신 관리하고 **CI 자동서명**에 깔끔.
- Azure Trusted Signing은 MS 자체 서비스라 MSIX/Windows에 최적, 저비용·CI친화. 조직 검증(법인 3년+) 또는 개인 옵션.
- **EV로 갈 경우**: 첫 공개 다운로드부터 무경고가 계약상 필수일 때만. 하드웨어 토큰 → CI 자동화 난이도·비용↑.
- ⏱ **리드타임 주의**: 어느 등급이든 조직 검증에 수일~수주 → **0주차 즉시 조달 착수**.
2. **MSIX 서명**: `dotnet publish` MSIX → `signtool`로 서명. `Package.appxmanifest`의 publisher를 인증서 subject와 일치.
3. **자동 업데이트**: HTTPS 서버에 `.appinstaller` 호스팅 → 앱 실행 시 갱신 확인.
4. **CI**: GitHub Actions `windows-latest` → MSIX 빌드 → 서명(시크릿/HSM) → 아티팩트 + `.appinstaller` 게시.
5. **서버측(선택)**: 릴리스 태그 서명, 컨테이너 이미지 cosign 서명, SBOM 생성(상업 신뢰 가점).
### 5.3 손댈 파일
앱 `csproj`(MSIX 속성), `.appinstaller` 템플릿, CI 워크플로, 서명 스크립트.
### 5.4 수용 기준
- 설치 MSIX가 "확인된 게시자" 표시, SmartScreen 무차단.
- 실행 시 신버전 감지·업데이트.
### 5.5 공수: **~1주 + 인증서 리드타임**(조달이 수일~수주 → 0주차에 착수)
---
## 6. 시퀀스 · 타임라인 (~810주)
| 주차 | 게이트 | 비고 |
|------|--------|------|
| **0주(병렬)** | 5 인증서 조달 시작 | 리드타임 김 — 가장 먼저 발주 |
| **12주** | **1 인증/인가** | 기반. write 보호 즉시 효과 |
| **1주(병렬)** | **3 HTTPS** | 평문 제거 — 인증과 함께 활성리스크 차단 |
| **3주** | **2 감사로그** | 게이트1 신원 의존 |
| **35주(병렬)** | **4 API 계약** | 최대·독립, 다른 게이트와 겹쳐 진행 |
| **6주** | **5 서명·배포** | 인증서 도착 후 |
**의존성:** 2←1, 3 독립(조기), 4 독립(병렬), 5←인증서.
**권장 착수 순서:** (인증서 발주) → **1+3 동시** → 2 → 4(병렬) → 5.
---
## 7. 상업 출하 수용 체크리스트 (전체)
- [ ] 미인증 요청은 보호 엔드포인트에서 401, 권한부족 403
- [ ] 셋포인트 쓰기 = 인증+역할+**감사행 1건**(old/new/user/ip)
- [ ] 감사로그 앱-역할 DELETE 불가(불변)
- [ ] 모든 트래픽 HTTPS, 클라이언트 인증서 신뢰, Kestrel 직접노출 차단
- [ ] OpenAPI 노출 + 생성 클라이언트 컴파일 + 단일 JSON casing + 계약 테스트 CI green
- [ ] 서명 MSIX 무경고 설치 + 자동 업데이트
- [ ] (권장) 비번 정책·계정 잠금·세션 만료·CORS 화이트리스트
---
## 8. 의사결정 — **확정됨 (2026-06-25)**
| # | 결정 | 선택 | 반영 |
|---|------|------|------|
| 1 | 인증 방식 | **커스텀 토큰 핸들러** (기존 `PasswordHasher`+세션토큰 재활용) | 게이트1 §1.2 |
| 2 | 읽기 정책 | **무로그인 대시보드 허용** — FallbackPolicy 없음, 쓰기/설정/관리에만 `[Authorize]` | 게이트1 §1.3.45 |
| 3 | TLS 종단 | **리버스 프록시(Caddy)** — Kestrel은 127.0.0.1만 | 게이트3 §3.2 |
| 4 | 인증서 | **OV + 클라우드 서명(Azure Trusted Signing)** — 0주차 조달 착수 | 게이트5 §5.2 |
→ 4개 모두 확정. 게이트별 착수 가능. **0주차 = 인증서 조달 발주**(리드타임).

View File

@@ -0,0 +1,814 @@
# MAUI Windows Desktop UI 이전 플랜
> 본 문서는 HC900-AX 프로젝트의 UI를 ASP.NET Core Web SPA에서 .NET MAUI Windows Desktop 앱으로 이전하는 계획을 정의한다.
> 2026-06-20 초안 · 2026-06-24 개정 — **개발 환경 분리 명시**: MAUI 앱은 Windows에서 개발/빌드,
> 백엔드(ASP.NET Core·gRPC·PostgreSQL·MCP)는 기존 Linux(aarch64) 서버에 그대로 두고 LAN으로 연결.
---
## 목차
1. [배경 및 목표](#1-배경-및-목표)
2. [전체 아키텍처](#2-전체-아키텍처)
3. [접근 방식: 하이브리드 (Native + BlazorWebView)](#3-접근-방식-하이브리드-native--blazorwebview)
4. [프로젝트 구조](#4-프로젝트-구조)
5. [화면 분류 매트릭스](#5-화면-분류-매트릭스)
6. [타임라인 (12주)](#6-타임라인-12주)
7. [기술 스택](#7-기술-스택)
8. [MVVM 아키텍처](#8-mvvm-아키텍처)
9. [BlazorWebView 연동](#9-blazorwebview-연동)
10. [Windows 전용 기능](#10-windows-전용-기능)
11. [리스크 및 고려사항](#11-리스크-및-고려사항)
12. [개발 환경 구성 (Windows 개발 · Linux 백엔드)](#12-개발-환경-구성-windows-개발--linux-백엔드)
---
## 1. 배경 및 목표
### 1.1 현재 상황
- 현재 UI는 **ASP.NET Core SPA** (`wwwroot/` — vanilla JS, 15개 패널)
- 서버(linux-arm64)에서 포트 5000으로 Web UI 제공
- REST API(16개 컨트롤러) + gRPC + PostgreSQL + Python MCP 로 구성
### 1.4 개발 환경 원칙 (중요)
- **MAUI 앱 = Windows 개발 전용.** WinUI 3는 Windows SDK + MSBuild Windows 타겟을 요구하므로 Linux(aarch64)에서 빌드·실행·디버깅 불가.
- **백엔드(ASP.NET Core·C++ Gateway·PostgreSQL·MCP) = 기존 Linux 서버 유지.** 앱은 LAN을 통해 REST로 접속하는 순수 클라이언트.
- 따라서 개발은 **두 머신 분리**: ① Windows 개발 PC(VS 2022 + MAUI workload)에서 앱, ② Linux 서버에서 백엔드. 상세는 [12장](#12-개발-환경-구성-windows-개발--linux-백엔드).
### 1.2 목표
- **Windows Desktop 네이티브 앱** (WinUI 3) 으로 전환
- 기존 REST API 100% 재사용 (서버 수정 불필요)
- 복잡한 화면(Trend/P&ID/LLM)은 BlazorWebView로 기존 SPA 활용
- Windows 전용 기능: 토스트 알림, 시스템 트레이, MSIX 패키징
### 1.3 비목표 (Out of Scope)
- Linux/macOS 지원 (MAUI 공식 미지원)
- 기존 ASP.NET Core 서버 수정
- 서버 측 인증/권한 추가
---
## 2. 전체 아키텍처
```
┌──────────────────────────────────────────────────────────┐
│ MAUI Windows App │
│ (WinUI 3 Desktop) │
│ │
│ ┌────────────────────────────────────────────────────┐ │
│ │ ShellWindow (NavigationView) │ │
│ │ ├── Native Pages (Dashboard / Realtime / Alarms) │ │
│ │ └── BlazorWebView Pages (Trend / P&ID / LLM) │ │
│ └────────────────────────────────────────────────────┘ │
│ │ │
│ ┌────────────────────────────────────────────────────┐ │
│ │ Services Layer │ │
│ │ ├── Hc900ApiService (HttpClient → REST API) │ │
│ │ ├── WindowsNotificationService (Toast) │ │
│ │ └── OfflineCacheService (SQLite) │ │
│ └────────────────────────────────────────────────────┘ │
└──────────────────────────┬───────────────────────────────┘
│ HTTP REST/JSON
┌──────────────────────────────────────────────────────────┐
│ 기존 ASP.NET Core (port 5000) │
│ 16 REST API Controllers (변경 없음) │
└──────────────────────────────────────────────────────────┘
```
### 2.1 데이터 흐름
```
MAUI Native Page
→ ViewModel [RelayCommand]
→ IHc900ApiService (HttpClient)
→ http://<server>:5000/api/<endpoint>
→ ASP.NET Controller
→ gRPC / EF Core / ADO.NET
→ C++ Gateway / PostgreSQL
```
- 모든 HTTP 호출은 **비동기** (`async/await`)
- ViewModel은 `ObservableObject` 기반, `CommunityToolkit.Mvvm` Source Generator 사용
- API 응답은 `Hc900.Shared` DTO로 역직렬화
---
## 3. 접근 방식: 하이브리드 (Native + BlazorWebView)
| 방식 | 적용 대상 | 이유 |
|------|----------|------|
| **Native (XAML)** | Dashboard, Realtime, Alarms, EventHistory, TagWrite, PointBuilder, Setup, History, TextToSql, Reports | 잦은 조작, 터치/키보드 최적화, Windows 네이티브 피드백 |
| **BlazorWebView** | Trend, P&ID Extraction, P&ID Viewer, LLM Chat, KB Admin, Docs Explorer | 기존 SPA 코드 복잡도 높음, uPlot/ECharts/mermaid 등 JS 라이브러리 의존 |
### 3.1 결정 근거
| 기준 | Native | BlazorWebView |
|------|--------|---------------|
| 개발 비용 | 높음 (재구현) | 낮음 (래핑) |
| UX (터치/키보드) | ★★★★★ | ★★★☆☆ |
| 차트 성능 | ★★★☆☆ (SkiaSharp) | ★★★★★ (uPlot 네이티브) |
| 오프라인 지원 | ★★★★★ (SQLite) | ★☆☆☆☆ (WebView 한계) |
| Windows Toast | ★★★★★ | ★☆☆☆☆ |
| 유지보수 | XAML + C# | 별도 SPA 코드 유지 필요 |
---
## 4. 프로젝트 구조
```
src/
├── Hc900Crawler/ # 기존 ASP.NET Core (변경 없음)
├── Hc900.Shared/ # [신규] 공유 DTO + API 인터페이스
│ ├── Hc900.Shared.csproj # net8.0, 라이브러리
│ ├── Models/
│ │ ├── TagValueDto.cs
│ │ ├── TagMetadataDto.cs
│ │ ├── AlarmEventDto.cs
│ │ ├── EventHistoryDto.cs
│ │ ├── HistoryRecordDto.cs
│ │ ├── GatewayHealthDto.cs
│ │ ├── SetupConfigDto.cs
│ │ ├── PidTagDto.cs
│ │ ├── KbDocumentDto.cs
│ │ └── ...
│ └── Services/
│ └── IHc900ApiService.cs # REST API 인터페이스
└── Hc900MauiApp/ # [신규] MAUI Windows 앱
├── Hc900MauiApp.csproj # TargetFramework: net8.0-windows10.0.19041.0
├── App.xaml / App.xaml.cs
├── MauiProgram.cs
├── Platforms/Windows/
│ ├── App.xaml
│ └── Package.appxmanifest
├── Views/
│ ├── ShellWindow.xaml # NavigationView + Frame
│ ├── DashboardPage.xaml
│ ├── RealtimePage.xaml
│ ├── AlarmsPage.xaml
│ ├── EventHistoryPage.xaml
│ ├── TagWritePage.xaml
│ ├── PointBuilderPage.xaml
│ ├── HistoryPage.xaml
│ ├── TextToSqlPage.xaml
│ ├── SetupPage.xaml
│ ├── ReportsPage.xaml
│ └── Blazor/
│ ├── TrendPage.xaml
│ ├── PidPage.xaml
│ ├── PidViewerPage.xaml
│ ├── LlmChatPage.xaml
│ ├── KbAdminPage.xaml
│ └── DocsPage.xaml
├── ViewModels/
│ ├── DashboardViewModel.cs
│ ├── RealtimeViewModel.cs
│ ├── AlarmsViewModel.cs
│ ├── EventHistoryViewModel.cs
│ ├── TagWriteViewModel.cs
│ ├── PointBuilderViewModel.cs
│ ├── HistoryViewModel.cs
│ ├── TextToSqlViewModel.cs
│ ├── SetupViewModel.cs
│ └── ReportsViewModel.cs
├── Services/
│ ├── Hc900ApiService.cs
│ ├── WindowsNotificationService.cs
│ └── OfflineCacheService.cs
├── Controls/
│ ├── TagCard.xaml
│ ├── AlarmBadge.xaml
│ └── StatusIndicator.xaml
└── Resources/
├── Styles/
│ └── Styles.xaml
├── Fonts/
└── Images/
```
---
## 5. 화면 분류 매트릭스
### 5.1 Native 구현 (10개)
| 화면 | 주요 기능 | API Endpoint | 난이도 |
|------|----------|-------------|--------|
| **Dashboard** | 실시간 요약 카드, 알람 카운트, 컨트롤러 상태 | `/api/gateway/health`, `/api/realtime/summary` | 중 |
| **Realtime** | 태그 목록 CollectionView, 검색, 정렬, 1s 자동 갱신 | `/api/realtime` | 중 |
| **Alarms** | 활성 알람 리스트, area 필터, 심각도 정렬 | `/api/events?event_type=ALARM` | 하 |
| **EventHistory** | 이벤트 목록, 날짜/태그/타입 필터, 무한 스크롤 | `/api/events` | 중 |
| **TagWrite** | 태그 선택, 값 입력(숫자 키패드), 확인, 결과 피드백 | `/api/gateway/write` | 중 |
| **PointBuilder** | Sinam 업로드/파싱, 태그 활성화 토글, bulk CRUD | `/api/pointbuilder`, `/api/hc900/tags` | 상 |
| **Setup** | 컨트롤러 목록, 프로세스 시작/중지/재시작, 상태 표시 | `/api/setup` | 중 |
| **History** | 태그 선택, 기간 선택, 조회, 결과 그리드 + 차트 | `/api/history` | 상 |
| **TextToSql** | 자연어 입력, SQL 표시, 실행, 결과 그리드 | `/api/t2s` | 중 |
| **Reports** | 보고서 템플릿 선택, 파라미터 설정, 생성/다운로드 | `/api/report` | 중 |
### 5.2 BlazorWebView (6개)
| 화면 | 이유 | 기존 파일 |
|------|------|----------|
| **Trend** | uPlot 차트, 실시간 스트리밍, 다중 Y축 | `panes/trend.html`, `js/trend.js` |
| **P&ID Extraction** | netDxf/PdfPig 결과 표시, 복잡한 테이블 | `panes/pid.html`, `js/pid.js` |
| **P&ID Viewer** | mermaid.js 그래프 뷰어 | `panes/pid-viewer.html`, `js/pid-viewer.js` |
| **LLM Chat** | marked.js 렌더링, 마크다운+코드 하이라이트 | `panes/llmchat.html`, `js/llmchat.js` |
| **KB Admin** | 문서 업로드/관리/인증 | `panes/kbadmin.html`, `js/kbadmin.js` |
| **Docs Explorer** | 파일 트리 + 콘텐츠 뷰어 | `panes/docs.html`, `js/docs.js` |
---
## 6. 타임라인 (12주)
### Phase 1: 기반 구축 (1-3주)
| 주차 | 작업 | 상세 |
|------|------|------|
| **1주차** | `Hc900.Shared` DTO 라이브러리 | 기존 API 응답 JSON 기반 모델 클래스 생성 (약 20개 DTO) |
| | `IHc900ApiService` 인터페이스 | 16개 컨트롤러 전부 매핑 (CRUD + Query) |
| **2주차** | MAUI 프로젝트 생성 | `dotnet new maui`, WinUI 3 타겟 설정 |
| | `Hc900ApiService` 구현 | `HttpClient` + `System.Text.Json`, DI 등록 |
| **3주차** | `ShellWindow` 완성 | `NavigationView` + Frame 기반 네비게이션 |
| | MVVM 템플릿 확립 | `CommunityToolkit.Mvvm` Source Generator 세팅 |
### Phase 2: Native 화면 구현 (4-8주)
| 주차 | 작업 | 상세 |
|------|------|------|
| **4주차** | **DashboardPage** | 요약 카드(TagValue, AlarmCount, ControllerStatus), 3초 자동 갱신 Timer |
| **5주차** | **RealtimePage** | CollectionView + SearchHandler + Sorting + PullToRefresh + 1s PollingTimer |
| **6주차** | **AlarmsPage + EventHistoryPage** | Filter UI(ComboBox/DatePicker), InfiniteScroll, 상세 팝업 |
| **7주차** | **TagWritePage + HistoryPage** | 숫자 키패드 (Custom numeric Control), ConfirmDialog, History 차트 (SkiaSharp) |
| **8주차** | **PointBuilderPage + SetupPage + TextToSqlPage + ReportsPage** | 각 페이지 완성 |
### Phase 3: BlazorWebView 연동 (9-10주)
| 주차 | 작업 | 상세 |
|------|------|------|
| **9주차** | BlazorWebView 설정 | `Microsoft.AspNetCore.Components.WebView.WinUI` 패키지, 기존 `wwwroot/` 리소스 포함 |
| | 6개 Blazor Page 구현 | 각 `BlazorWebView` + `RootComponent` + API base URL 전달 |
| **10주차** | JS interop 연동 | Blazor → C# (토스트 알림 호출), C# → Blazor (설정 변경 전파) |
| | 인증 토큰 전달 | KB 관리 `X-Kb-Token`을 Blazor WebView의 `HttpClient`에 주입 |
### Phase 4: Windows 고급 기능 + 배포 (11-12주)
| 주차 | 작업 | 상세 |
|------|------|------|
| **11주차** | **Windows Toast Notification** | `Microsoft.Toolkit.Uwp.Notifications` — 알람 발생 시 데스크탑 알림 |
| | **시스템 트레이** | 최소화 시 트레이 아이콘, 백그라운드 폴링, 더블클릭 복원 |
| **12주차** | **오프라인 SQLite 캐시** | `sqlite-net-pcl`, 최근 1000개 태그 값 로컬 저장, 네트워크 복구 시 동기화 |
| | **MSIX 패키징** | Windows App SDK, 서명, 자동 업데이트 (`Windows.ApplicationModel.Core`) |
| | **Acceptance Test** | 전체 화면 네비게이션, API 연동, 오프라인/알람 시나리오 검증 |
---
## 7. 기술 스택
| 계층 | 기술 | 버전 | 용도 |
|------|------|------|------|
| **Framework** | .NET MAUI | net8.0-windows10.0.19041.0 | Windows Desktop WinUI 3 |
| **MVVM** | CommunityToolkit.Mvvm | 8.x | Source Generator 기반 MVVM |
| **HTTP Client** | Microsoft.Extensions.Http | 8.x | HttpClientFactory + Polly 재시도 |
| **직렬화** | System.Text.Json | 내장 | JSON 역/직렬화 |
| **차트** | SkiaSharp.Views.Maui.Controls | 2.x | History/추세 Native 차트 |
| **WebView** | Microsoft.AspNetCore.Components.WebView.WinUI | 8.x | BlazorWebView 호스팅 |
| **로컬 DB** | sqlite-net-pcl | 1.x | 오프라인 캐시 |
| **토스트** | Microsoft.Toolkit.Uwp.Notifications | 7.x | Windows Toast 알림 |
| **DI** | Microsoft.Extensions.DependencyInjection | 내장 | 생성자 주입 |
| **로깅** | Microsoft.Extensions.Logging | 내장 | 파일/디버그 로깅 |
---
## 8. MVVM 아키텍처
### 8.1 ViewModel 패턴
```csharp
// CommunityToolkit.Mvvm Source Generator 사용
public partial class RealtimeViewModel : ObservableObject
{
private readonly IHc900ApiService _api;
[ObservableProperty]
private ObservableCollection<TagValueDto> _tags = new();
[ObservableProperty]
private bool _isLoading;
[ObservableProperty]
private string _searchText = string.Empty;
private CancellationTokenSource? _pollCts;
public RealtimeViewModel(IHc900ApiService api)
{
_api = api;
}
[RelayCommand]
private async Task LoadTagsAsync()
{
IsLoading = true;
try
{
var result = await _api.GetRealtimeTagsAsync(SearchText);
Tags = new ObservableCollection<TagValueDto>(result);
}
finally
{
IsLoading = false;
}
}
[RelayCommand]
private void StartPolling()
{
_pollCts = new CancellationTokenSource();
_ = PollLoopAsync(_pollCts.Token);
}
private async Task PollLoopAsync(CancellationToken ct)
{
while (!ct.IsCancellationRequested)
{
await LoadTagsAsync();
await Task.Delay(1000, ct);
}
}
public void Dispose()
{
_pollCts?.Cancel();
}
}
```
### 8.2 DI 등록 (MauiProgram.cs)
```csharp
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
.UseSkiaSharp()
.ConfigureFonts(fonts =>
{
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
});
// HttpClient — BaseAddress는 Linux 백엔드 서버 IP(설정 주입). localhost 하드코딩 금지:
// 앱은 Windows PC에서 돌고 서버는 별도 Linux 머신이므로 localhost면 자기 자신을 가리킨다.
var serverUrl = builder.Configuration["Backend:BaseUrl"] ?? "http://192.168.0.x:5000";
builder.Services.AddHttpClient<IHc900ApiService, Hc900ApiService>(client =>
{
client.BaseAddress = new Uri(serverUrl);
client.Timeout = TimeSpan.FromSeconds(10);
})
.AddTransientHttpErrorPolicy(p => p.RetryAsync(2));
// Services
builder.Services.AddSingleton<WindowsNotificationService>();
builder.Services.AddSingleton<OfflineCacheService>();
// ViewModels (Transient = new instance per navigation)
builder.Services.AddTransient<DashboardViewModel>();
builder.Services.AddTransient<RealtimeViewModel>();
builder.Services.AddTransient<AlarmsViewModel>();
// ...
// Pages
builder.Services.AddTransient<DashboardPage>();
builder.Services.AddTransient<RealtimePage>();
// ...
return builder.Build();
}
```
---
## 9. BlazorWebView 연동
### 9.1 Blazor Page 구현
```xml
<!-- Views/Blazor/TrendPage.xaml -->
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:blazor="clr-namespace:Microsoft.AspNetCore.Components.WebView.Maui;assembly=Microsoft.AspNetCore.Components.WebView.Maui">
<blazor:BlazorWebView HostPage="wwwroot/panes/trend.html">
<blazor:BlazorWebView.RootComponents>
<RootComponent Selector="#app"
ComponentType="{x:Type local:TrendBlazor}" />
</blazor:BlazorWebView.RootComponents>
</blazor:BlazorWebView>
</ContentPage>
```
```csharp
// Views/Blazor/TrendBlazor.razor
@inject IJSRuntime JS
@code {
[Parameter]
public string ApiBaseUrl { get; set; } = ""; // Linux 서버 LAN IP 주입(12.6). localhost 금지.
protected override async Task OnInitializedAsync()
{
await JS.InvokeVoidAsync("setApiBaseUrl", ApiBaseUrl);
}
}
```
### 9.2 JS interop 양방향 통신
**C# → JavaScript:**
```csharp
// C#에서 트렌드 페이지에 새 태그 추가 요청
await _jsRuntime.InvokeVoidAsync("trend_addTag", "FICQ-6101.PV");
```
**JavaScript → C# (토스트 알림):**
```javascript
// BlazorWebView 내부 JS에서 C# 호출
window.showToast = (title, body) => {
DotNet.invokeMethodAsync('Hc900MauiApp', 'ShowToast', title, body);
};
```
### 9.3 wwwroot 리소스 포함
```xml
<!-- Hc900MauiApp.csproj -->
<ItemGroup>
<MauiAsset Include="wwwroot\**" LogicalName="wwwroot\%(RecursiveDir)%(Filename)%(Extension)" />
</ItemGroup>
```
기존 SPA의 `wwwroot/` 전체를 MAUI 프로젝트로 복사. 단, `index.html`은 BlazorWebView용 래퍼로 대체하거나 각 pane을 직접 로드.
---
## 10. Windows 전용 기능
### 10.1 Toast Notification
```csharp
public class WindowsNotificationService
{
public void ShowAlarmToast(string tagName, string message)
{
new ToastContentBuilder()
.AddArgument("tagName", tagName)
.AddText($"🔴 {tagName}")
.AddText(message)
.AddButton(new ToastButton()
.SetContent("앱 열기")
.AddArgument("action", "openApp"))
.Show();
}
}
```
### 10.2 시스템 트레이
- 최소화 버튼 클릭 시 트레이로 숨김
- 트레이 아이콘 컨텍스트 메뉴: 열기 / 종료
- 백그라운드에서 알람 폴링 지속
- `Windows.UI.ViewManagement.ApplicationView` + `HNotifyIcon` (또는 CommunityToolkit)
### 10.3 MSIX Packaging
- `Package.appxmanifest`에 시각적 자산(아이콘) 등록
- Windows Application Packaging Project 로 MSIX 번들 생성
- 자동 업데이트: `Windows.Services.Store` 또는 수동 다운로드
### 10.4 단축키
| 단축키 | 동작 |
|--------|------|
| `Ctrl+1~9` | 탭 전환 |
| `Ctrl+F` | 검색 포커스 |
| `F5` | 새로고침 |
| `Ctrl+W` | 태그 쓰기 열기 |
| `Esc` | 현재 팝업 닫기 |
---
## 11. 리스크 및 고려사항
### 11.1 서버 변경 불필요 (리스크 아님)
MAUI 앱은 순수 REST 클라이언트. 기존 `Hc900Crawler` 서버 코드를 전혀 수정하지 않음.
### 11.2 CORS 문제 없음
MAUI `HttpClient`는 브라우저가 아니므로 CORS 제약을 받지 않음.
### 11.3 빌드 환경 (개발 머신 분리 — 핵심)
- **MAUI 앱은 Windows에서만 빌드 가능.** Linux(aarch64) 서버에서는 백엔드만 빌드.
- 두 솔루션 분리로 교차 빌드 사고 방지:
- `Hc900Crawler.sln`(기존) — 서버, Linux에서 빌드. **MAUI 프로젝트 미참조.**
- `Hc900Maui.sln`(신규) — `Hc900.Shared` + `Hc900MauiApp`, **Windows에서만** 열고 빌드.
- CI도 분리: 서버는 Linux/self-hosted-arm 러너, 앱은 GitHub Actions **windows-latest** 러너(MSIX 산출).
- 자세한 워크플로/네트워크/설정은 [12장](#12-개발-환경-구성-windows-개발--linux-백엔드).
### 11.4 배포 전략
| 환경 | 배포 방식 |
|------|----------|
| 개발 | `dotnet build` + F5 디버그 |
| 테스트 | MSIX 사이드로딩 |
| 운영 | MSIX 자동 업데이트 또는 설치 프로그램 배포 |
### 11.5 BlazorWebView 유지보수 부담
- 기존 SPA `wwwroot/` 코드가 변경되면 MAUI 쪽에도 동기화 필요
- 해결: 빌드 스크립트로 `Hc900Crawler/wwwroot/``Hc900MauiApp/wwwroot/` 자동 복사
### 11.6 기존 SPA와 공존
ASP.NET Core Web UI(port 5000)는 그대로 유지. 브라우저 접속과 MAUI 앱 병행 사용 가능.
---
## 12. 개발 환경 구성 (Windows 개발 · Linux 백엔드)
> **이 장 하나로 Windows에서 프로젝트를 0부터 생성·실행할 수 있도록 모든 절차를 기록한다.**
> 백엔드는 기존 Linux(aarch64) 서버를 그대로 쓰고, 앱만 Windows PC에서 새로 만든다.
### 12.1 머신 분리 다이어그램
```
┌─ Windows 개발 PC ────────────────────┐ ┌─ Linux 서버 (aarch64, 기존) ───┐
│ Windows 11 + VS 2022 + MAUI workload │ │ ASP.NET Core :5000 (REST/UI) │
│ Hc900.Shared (net8.0) │ LAN │ C++ Gateway :50051 (gRPC) │
│ Hc900MauiApp (net8.0-windows, WinUI3)│ ──HTTP──▶ │ PostgreSQL :5432 │
│ HttpClient → http://<서버IP>:5000 │ REST/JSON│ Python MCP :5001 │
│ F5 디버그로 데스크톱 앱 실행 │ │ (변경 없음) │
└──────────────────────────────────────┘ └────────────────────────────────┘
```
- Windows PC = **앱 개발/실행 전용**. Linux 서버 = **백엔드 전용**(코드 수정 없음).
- 앱은 `localhost`가 아니라 **서버의 LAN IP**를 가리킨다(localhost면 Windows 자기 자신을 찌름).
### 12.2 사전 준비물 (Windows PC)
| 항목 | 최소 버전 | 비고 |
|------|-----------|------|
| Windows | 11 (또는 10 22H2) | WinUI 3 타겟 `10.0.19041.0` 충족 |
| Visual Studio 2022 | 17.8+ | 워크로드 **".NET 다중 플랫폼 앱 UI 개발"** 체크 |
| .NET SDK | 8.0 (LTS) | VS 설치 시 포함 |
| WebView2 Runtime | Evergreen | Win11 기본 탑재(없으면 MS에서 설치) |
| Git | 임의 | 동일 repo 클론 |
### 12.3 도구 설치 (PowerShell)
```powershell
# 1) VS 2022가 없으면: MAUI 워크로드 포함 설치 (VS Installer GUI에서 체크해도 됨)
# GUI: Visual Studio Installer → 수정 → ".NET 다중 플랫폼 앱 UI 개발" 체크
# 2) CLI로 MAUI 워크로드 설치/갱신 (VS와 별개로도 필요)
dotnet workload install maui
# 3) 설치 확인
dotnet workload list # 'maui' 표시되어야 함
dotnet --version # 8.0.x
```
### 12.4 신규 솔루션·프로젝트 생성 (CLI, 그대로 복붙)
```powershell
# 기존 repo 클론 후 그 안에서 (서버 코드와 같은 repo, src\ 하위에 둔다)
cd <repo>\src
# 솔루션(앱 전용) — Linux의 Hc900Crawler.sln과 분리
dotnet new sln -n Hc900Maui
# 공유 DTO/인터페이스 (플랫폼 중립 net8.0 → 서버·앱 양쪽서 참조 가능)
dotnet new classlib -n Hc900.Shared -f net8.0
# MAUI 앱
dotnet new maui -n Hc900MauiApp
# 솔루션에 추가 + 앱 → Shared 참조
dotnet sln Hc900Maui.sln add Hc900.Shared\Hc900.Shared.csproj Hc900MauiApp\Hc900MauiApp.csproj
dotnet add Hc900MauiApp\Hc900MauiApp.csproj reference Hc900.Shared\Hc900.Shared.csproj
```
**필수: `Hc900MauiApp.csproj`에서 타겟을 Windows로 한정** (Android/iOS/Mac 제거 → Linux CI·불필요 빌드 방지):
```xml
<!-- 변경 전: <TargetFrameworks>net8.0-android;net8.0-ios;net8.0-maccatalyst;net8.0-windows10.0.19041.0</TargetFrameworks> -->
<TargetFramework>net8.0-windows10.0.19041.0</TargetFramework>
<RuntimeIdentifiers>win10-x64;win10-arm64</RuntimeIdentifiers>
<WindowsPackageType>MSIX</WindowsPackageType>
```
### 12.5 NuGet 패키지 (앱 프로젝트)
```powershell
cd <repo>\src\Hc900MauiApp
dotnet add package CommunityToolkit.Mvvm # MVVM 소스생성기 (8.x)
dotnet add package Microsoft.Extensions.Http # HttpClientFactory
dotnet add package Microsoft.Extensions.Http.Polly # 재시도 정책
dotnet add package SkiaSharp.Views.Maui.Controls # Native 차트 (History)
dotnet add package sqlite-net-pcl # (선택) 오프라인 캐시
dotnet add package CommunityToolkit.WinUI.Notifications # Windows Toast (구 Microsoft.Toolkit.Uwp.Notifications 후속)
dotnet add package H.NotifyIcon.WinUI # 시스템 트레이
dotnet add package Microsoft.Web.WebView2 # 기존 SPA 재사용 셸 (★ 13장 대안)
```
> ⚠️ **BlazorWebView 주의:** 기존 패널은 vanilla JS(`paneInit` 전역 + innerHTML 주입)라 `Microsoft.AspNetCore.Components.WebView.Maui`(Blazor/Razor 호스트)로는 그대로 못 올린다. 기존 SPA를 재사용하려면 **WebView2**(위 마지막 패키지)로 서버 URL을 직접 로드한다. 자세한 근거는 [13장](#13-상업용-앱-수준-솔직-리뷰-및-대안).
### 12.6 백엔드 서버 연결 설정
1. **서버 바인드 확인(이미 충족):** ASP.NET Core가 `0.0.0.0:5000`으로 서빙 → LAN에서 접근 가능.
2. **서버 IP 확인(Linux):**
```bash
ip -4 addr show | grep inet # 예: 192.168.0.50
```
3. **Linux 방화벽 개방(필요 시):**
```bash
sudo ufw allow 5000/tcp
```
4. **앱에 서버 주소 주입** — `Hc900MauiApp`에 `appsettings.json`(MauiAsset) 또는 빌드 설정:
```json
{ "Backend": { "BaseUrl": "http://192.168.0.50:5000" } }
```
([8.2장](#82-di-등록-mauiprogramcs)의 `Backend:BaseUrl` 로 주입. localhost 금지.)
5. **Windows에서 연결 테스트(앱 만들기 전 먼저):**
```powershell
curl http://192.168.0.50:5000/api/report/columns # JSON 떨어지면 OK
# 또는 브라우저로 http://192.168.0.50:5000 접속 → 기존 SPA 떠야 함
```
### 12.7 첫 실행 체크리스트 (F5)
- [ ] `dotnet workload list` 에 `maui` 존재
- [ ] `Hc900Maui.sln`을 VS 2022로 열기 (Linux의 `Hc900Crawler.sln` 아님)
- [ ] 시작 프로젝트 = `Hc900MauiApp`, 타겟 = **Windows Machine**
- [ ] `Backend:BaseUrl` = 서버 LAN IP (localhost 아님)
- [ ] 12.6-5 연결 테스트 통과
- [ ] F5 → WinUI 창이 뜨고 한 화면이라도 서버 데이터 표시되면 부트스트랩 성공
### 12.8 솔루션 분리 (교차 빌드 사고 방지)
| 솔루션 | 위치 | 빌드 머신 | 포함 |
|--------|------|-----------|------|
| `Hc900Crawler.sln` | 기존 | **Linux** (aarch64) | 서버·게이트웨이. MAUI 프로젝트 **미참조** |
| `Hc900Maui.sln` | 신규 | **Windows** | `Hc900.Shared` + `Hc900MauiApp` |
- `Hc900.Shared`(net8.0)는 중립이라 양쪽서 참조 가능하지만, **Windows 전용 `Hc900MauiApp`을 Linux 솔루션에 절대 추가하지 말 것**(Linux 빌드가 Windows 타겟을 끌어와 깨짐).
### 12.9 소스 동기화 (같은 repo)
- 같은 git repo이므로 `wwwroot/` 복사 스크립트 불필요. WebView2 셸([13장])을 쓰면 **서버가 서빙하는 SPA를 그대로 로드** → 동기화 문제 자체가 없음.
- DTO 드리프트 방지: 서버 응답이 익명객체(326곳)라 계약이 약함 → `Hc900.Shared` DTO는 **서버가 OpenAPI/Swagger를 노출하도록 먼저 정비**한 뒤 생성 권장([13.3](#133-상업화-게이트-우선순위)).
### 12.10 MSIX 패키징 · 서명 · 배포
```powershell
# CLI 게시 (MSIX)
dotnet publish Hc900MauiApp\Hc900MauiApp.csproj -f net8.0-windows10.0.19041.0 -c Release
```
- **개발/사내**: 자체서명 인증서(self-signed) + 사이드로딩.
- **상업 배포**: **코드 서명 인증서 필수**(SmartScreen 평판 위해 사실상 **EV 인증서** 권장 — 연 비용 발생). 미서명 MSIX는 설치 시 경고/차단.
- **자동 업데이트**: MS Store 미경유 시 `.appinstaller` + 사내 웹서버 호스팅으로 자동 업데이트 채널 구성.
### 12.11 트러블슈팅
| 증상 | 원인 | 해결 |
|------|------|------|
| 앱이 서버에 연결 못 함 | `BaseUrl`이 localhost | 서버 LAN IP로 변경 |
| 연결 거부 | Linux 방화벽 | `ufw allow 5000/tcp` |
| `maui` 워크로드 없음 | 미설치 | `dotnet workload install maui` |
| Linux CI가 MAUI 빌드 시도 | 솔루션에 앱 포함됨 | `Hc900Crawler.sln`에서 MAUI 제거 |
| BlazorWebView 빈 화면 | vanilla JS를 Blazor로 호스트 | WebView2로 서버 URL 직접 로드 |
---
## 13. 상업용 앱 수준 솔직 리뷰 및 대안
> 요청: "상업용 앱 수준이 될 만한지 솔직히 리뷰하고 대안을 제시."
### 13.1 솔직한 결론
**현재 플랜(MAUI 하이브리드 12주)대로 만들면 "잘 만든 사내 도구"는 되지만, 그대로는 "판매 가능한 상업 제품"이 되지 않는다.** 이유는 UI 프레임워크가 아니라 **그 아래의 제품 기반**에 있다. 그리고 상업화의 진짜 병목은 이 플랜이 *비목표(Out of Scope)로 명시한 것들*이다.
### 13.2 상업 제품이 되려면 막아야 할 공백 (UI보다 우선)
| 영역 | 현재 | 상업 제품 요구 | 심각도 |
|------|------|----------------|--------|
| **인증/권한** | 없음(플랜이 비목표로 못박음) | 사용자 계정 + 역할(운전원/엔지니어) RBAC | 🔴 치명 |
| **태그 쓰기 보안** | LAN 누구나 `/api/gateway/write`로 셋포인트 변경 가능 | 인증·권한·**감사로그(audit)** 필수 | 🔴 치명 |
| **전송 보안** | HTTP 평문 | HTTPS/TLS | 🔴 치명 |
| **API 계약** | 익명객체 326곳, PascalCase 불일치 반복 수정 이력 | 버전드 계약(OpenAPI)+타입 DTO | 🟠 높음 |
| **설치/서명** | 없음 | 코드서명(EV)+자동업데이트 | 🟠 높음 |
| **다현장/멀티테넌트** | 단일 서버 가정 | 사이트별 설정·라이선스 | 🟡 중 |
| **관측성** | 파일 로그 | 크래시 리포트·원격 진단 | 🟡 중 |
**핵심:** 산업 모니터링 앱에서 *인증 없는 태그 쓰기*는 안전·책임 관점에서 판매 불가 사유다. MAUI로 화면을 아무리 예쁘게 만들어도 이 공백은 그대로 남는다 — UI 재작성은 상업화에 **가장 비싸면서 가장 덜 중요한** 작업이다.
### 13.3 상업화 게이트 우선순위 (UI 재작성보다 먼저)
> **상세 실행 계획은 별도 문서:** [상업화-플랜-5대게이트.md](상업화-플랜-5대게이트.md) — 코드 실측 기반 작업·공수·수용기준·타임라인.
1. **인증/인가** — ASP.NET Core Identity 또는 OIDC(Keycloak 등), 태그 쓰기에 역할 게이트.
2. **감사 로그** — 누가/언제/무엇을 썼는지 불변 기록(이미 `event_history_table` 패턴 재활용 가능).
3. **HTTPS** — 리버스 프록시(nginx/caddy)로 TLS 종단.
4. **API 계약 안정화** — 익명객체 → 타입 DTO + Swagger 노출(그 후에야 `Hc900.Shared` 자동 생성이 안전).
5. **배포 파이프라인** — 코드서명 + 자동 업데이트.
### 13.4 대안: "UI 재작성 대신, 웹앱을 설치형으로 + 절약분을 상업 게이트에 투자"
기존 SPA는 이미 **크로스플랫폼·무설치·태블릿 대응**이라는 상업적 장점을 가진다. 이를 버리고 Windows 전용 네이티브로 가는 건 자산을 깎는 것. 세 단계 대안:
**대안 A (권장, ~12주): WebView2 얇은 셸 + PWA**
- Windows 설치형이 필요하면 **WebView2 창 1개**로 `http://server:5000` 로드 → 14개 화면 즉시 재사용(재구현 0).
- 동시에 기존 SPA를 **PWA로 전환**(manifest+service worker) → Windows/태블릿에서 "설치" 아이콘·오프라인 셸·푸시. Windows 전용 락인 없음.
- Windows 가치가 분명한 **토스트+트레이만** WebView2 `WebMessageReceived` 브리지로 얇게 추가([10장]에서 그 둘만 채택).
**대안 B (산업 HMI 품질, 권장 경로): 기존 SPA를 재작성 없이 전문화**
- 빌드(Vite)+디자인시스템+컴포넌트+상태표준화로 산업 HMI 출하 품질 도달. 상세: [UI-경로B-산업HMI-폴리시플랜.md](UI-경로B-산업HMI-폴리시플랜.md).
**대안 B (조건부): 네이티브는 검증된 1개 화면만**
- "터치/키패드 UX가 실측으로 필요"가 증명된 화면(예: TagWrite 키패드)만 사후에 네이티브로. 전면 재작성 금지.
**대안 C (상업화 본체): 절약한 ~10주를 13.3 게이트에 투자.**
- 이게 제품을 "팔 수 있게" 만드는 유일한 경로. 프레임워크 교체로는 도달 못 함.
| 비교 | 현 플랜(MAUI 12주) | 대안 A+C |
|------|--------------------|----------|
| UI 재사용 | 6/14 화면(BlazorWebView, 실은 부정합) | 14/14(WebView2) |
| 일정 | 12주(UI에 소진) | 12주(셸) + 10주(상업 게이트) |
| 상업 적합성 | 인증·보안 공백 그대로 | 게이트를 정면 해결 |
| 플랫폼 | Windows 전용 락인 | 웹/태블릿/데스크톱 |
| 유지보수 | SPA+MAUI 이중 | 단일 SPA |
### 13.5 한 줄 권고
> **UI를 MAUI로 다시 쓰지 말고, 기존 웹앱을 설치형(PWA/WebView2)으로 감싼 뒤, 아낀 시간을 인증·감사·HTTPS·API 계약·코드서명에 쓰라.** 상업용 수준을 가르는 것은 데스크톱 프레임워크가 아니라 이 다섯이다.
---
## 부록: IHc900ApiService 인터페이스 (초안)
```csharp
namespace Hc900.Shared.Services;
public interface IHc900ApiService
{
// Gateway
Task<GatewayHealthDto> GetGatewayHealthAsync(string controller = "C1");
Task<List<TagValueDto>> ReadTagsAsync(List<string> tagNames);
Task<bool> WriteTagAsync(string tagName, double value);
Task<List<TagMetadataDto>> ListTagsAsync(string? filter = null, int limit = 100);
// Realtime
Task<List<TagValueDto>> GetRealtimeTagsAsync(string? filter = null);
Task<RealtimeSummaryDto> GetRealtimeSummaryAsync();
// History
Task<List<HistoryRecordDto>> GetHistoryAsync(string tagName, DateTime from, DateTime to, int limit = 1000);
// Events
Task<List<EventHistoryDto>> GetEventsAsync(EventQueryDto query);
// Metadata
Task<List<TagMetadataDto>> GetTagMetadataAsync(string query, int limit = 10);
// Tag Manager (Point Builder)
Task<List<Hc900MapEntryDto>> GetTagManagerEntriesAsync(string? controllerId = null);
Task<bool> ToggleTagActiveAsync(int id, bool isActive);
Task<bool> BulkTagUpdateAsync(List<TagUpdateDto> updates);
// Setup
Task<List<ControllerStatusDto>> GetControllerStatusAsync();
Task<bool> StartControllerAsync(string controllerId);
Task<bool> StopControllerAsync(string controllerId);
// Text-to-SQL
Task<TextToSqlResultDto> TextToSqlAsync(string question);
// Reports
Task<List<ReportTemplateDto>> GetReportTemplatesAsync();
Task<string> GenerateReportAsync(string templateId, Dictionary<string, string> parameters);
// P&ID
Task<PidExtractionResultDto> UploadPidDrawingAsync(Stream fileStream, string fileName);
// KB
Task<List<KbCollectionDto>> GetKbCollectionsAsync();
Task<List<KbDocumentDto>> GetKbDocumentsAsync(string collectionKey);
// Trend
Task<List<TrendWorkspaceDto>> GetTrendWorkspacesAsync();
}
```
> **Note:** 각 DTO 클래스는 실제 API JSON 응답 구조에 맞춰 `System.Text.Json` `JsonPropertyName` 특성으로 매핑.