From f4b421674f958ad8e10756d23afc7cf099286fdb Mon Sep 17 00:00:00 2001 From: windpacer Date: Fri, 26 Jun 2026 02:50:09 +0900 Subject: [PATCH] =?UTF-8?q?docs:=20=EC=83=81=EC=97=85=ED=99=94=20=EC=A0=84?= =?UTF-8?q?=EB=9E=B5=20=EB=AC=B8=EC=84=9C=203=EC=A2=85=20=E2=80=94=20MAUI?= =?UTF-8?q?=20=EC=A7=84=EB=8B=A8=C2=B75=EB=8C=80=20=EA=B2=8C=EC=9D=B4?= =?UTF-8?q?=ED=8A=B8=C2=B7UI=20=EA=B2=BD=EB=A1=9CB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 제품을 사내 도구에서 상업 제품으로 끌어올리기 위한 기획 문서. 코드 실측 기반. - 작업플랜-MAUI-UI-이전.md: Windows 개발/Linux 백엔드 분리 개정 + §12 신규 프로젝트 부트스트랩 완결 가이드 + §13 상업용 솔직 리뷰(인증·보안 공백 진단) + WebView2/PWA 대안. MAUI 하이브리드의 BlazorWebView 부정합·익명 DTO 계약 지적. - 상업화-플랜-5대게이트.md: 인증(커스텀 토큰)·감사로그(불변)·HTTPS(리버스 프록시)·API계약(OpenAPI)·코드서명(OV+Azure Trusted Signing). 4개 결정 확정, 작업·공수·수용기준·타임라인(~8-10주). 위협 1순위=평문 무인증 셋포인트 쓰기. - UI-경로B-산업HMI-폴리시플랜.md: vanilla SPA 재작성 없이 전문화. 인라인 style 252개·하드코딩 hex를 토큰/컴포넌트로 흡수, Vite 빌드, HMI 컴포넌트 키트, stale 상태 표준화. ~6-7주. Co-Authored-By: Claude Opus 4.8 --- docs/UI-경로B-산업HMI-폴리시플랜.md | 168 ++++++ docs/상업화-플랜-5대게이트.md | 236 ++++++++ docs/작업플랜-MAUI-UI-이전.md | 814 ++++++++++++++++++++++++++++ 3 files changed, 1218 insertions(+) create mode 100644 docs/UI-경로B-산업HMI-폴리시플랜.md create mode 100644 docs/상업화-플랜-5대게이트.md create mode 100644 docs/작업플랜-MAUI-UI-이전.md diff --git a/docs/UI-경로B-산업HMI-폴리시플랜.md b/docs/UI-경로B-산업HMI-폴리시플랜.md new file mode 100644 index 0000000..3df90e3 --- /dev/null +++ b/docs/UI-경로B-산업HMI-폴리시플랜.md @@ -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.` 셀프등록 + 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를 `` 등으로 감싸 옵션 표준화(축·색·다운샘플 일관). +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. 타임라인 (~6–7주, 병렬화 가능) + +| 주차 | 작업 | +|------|------| +| 1주 | **B1 빌드 파이프라인** + B7 lint 골격 | +| 2–3주 | **B2 디자인 시스템** + 인라인/hex 마이그레이션 | +| 3–5주 | **B3 컴포넌트 키트**(B2와 겹침) | +| 5주 | **B4 상태 표준화** | +| 6주 | **B5 HMI 비주얼** + **B6 접근성/반응형** | +| 6–7주 | **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는 재작성이 아니라 전문화이므로 기존 기능 동작을 깨지 않고 점진 적용한다. diff --git a/docs/상업화-플랜-5대게이트.md b/docs/상업화-플랜-5대게이트.md new file mode 100644 index 0000000..4b87f67 --- /dev/null +++ b/docs/상업화-플랜-5대게이트.md @@ -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 `(또는 기존 `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`** — 토큰 검증 → `ClaimsPrincipal`(NameIdentifier, Role) 발급, 만료/비활성 거부. +4. **`Program.cs` 배선** (현 `177~221` 구간). **인가 모델 = 기본 공개(읽기 무로그인), 보호는 명시 `[Authorize]`만** (결정 #2): + ```csharp + builder.Services.AddAuthentication("ApiToken") + .AddScheme("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 공수: **~2–3주** (최대. 컨트롤러별 단계 분할 가능, 병렬화 용이) + +--- + +## 게이트 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. 시퀀스 · 타임라인 (~8–10주) + +| 주차 | 게이트 | 비고 | +|------|--------|------| +| **0주(병렬)** | 5 인증서 조달 시작 | 리드타임 김 — 가장 먼저 발주 | +| **1–2주** | **1 인증/인가** | 기반. write 보호 즉시 효과 | +| **1주(병렬)** | **3 HTTPS** | 평문 제거 — 인증과 함께 활성리스크 차단 | +| **3주** | **2 감사로그** | 게이트1 신원 의존 | +| **3–5주(병렬)** | **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.4–5 | +| 3 | TLS 종단 | **리버스 프록시(Caddy)** — Kestrel은 127.0.0.1만 | 게이트3 §3.2 | +| 4 | 인증서 | **OV + 클라우드 서명(Azure Trusted Signing)** — 0주차 조달 착수 | 게이트5 §5.2 | + +→ 4개 모두 확정. 게이트별 착수 가능. **0주차 = 인증서 조달 발주**(리드타임). diff --git a/docs/작업플랜-MAUI-UI-이전.md b/docs/작업플랜-MAUI-UI-이전.md new file mode 100644 index 0000000..0b28e8a --- /dev/null +++ b/docs/작업플랜-MAUI-UI-이전.md @@ -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://:5000/api/ + → 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 _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(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() + .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(client => + { + client.BaseAddress = new Uri(serverUrl); + client.Timeout = TimeSpan.FromSeconds(10); + }) + .AddTransientHttpErrorPolicy(p => p.RetryAsync(2)); + + // Services + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + + // ViewModels (Transient = new instance per navigation) + builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + // ... + + // Pages + builder.Services.AddTransient(); + builder.Services.AddTransient(); + // ... + + return builder.Build(); +} +``` + +--- + +## 9. BlazorWebView 연동 + +### 9.1 Blazor Page 구현 + +```xml + + + + + + + + +``` + +```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 + + + + +``` + +기존 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 \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 + +net8.0-windows10.0.19041.0 +win10-x64;win10-arm64 +MSIX +``` + +### 12.5 NuGet 패키지 (앱 프로젝트) + +```powershell +cd \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 (권장, ~1–2주): 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에 소진) | 1–2주(셸) + 10주(상업 게이트) | +| 상업 적합성 | 인증·보안 공백 그대로 | 게이트를 정면 해결 | +| 플랫폼 | Windows 전용 락인 | 웹/태블릿/데스크톱 | +| 유지보수 | SPA+MAUI 이중 | 단일 SPA | + +### 13.5 한 줄 권고 + +> **UI를 MAUI로 다시 쓰지 말고, 기존 웹앱을 설치형(PWA/WebView2)으로 감싼 뒤, 아낀 시간을 인증·감사·HTTPS·API 계약·코드서명에 쓰라.** 상업용 수준을 가르는 것은 데스크톱 프레임워크가 아니라 이 다섯이다. + +--- + +## 부록: IHc900ApiService 인터페이스 (초안) + +```csharp +namespace Hc900.Shared.Services; + +public interface IHc900ApiService +{ + // Gateway + Task GetGatewayHealthAsync(string controller = "C1"); + Task> ReadTagsAsync(List tagNames); + Task WriteTagAsync(string tagName, double value); + Task> ListTagsAsync(string? filter = null, int limit = 100); + + // Realtime + Task> GetRealtimeTagsAsync(string? filter = null); + Task GetRealtimeSummaryAsync(); + + // History + Task> GetHistoryAsync(string tagName, DateTime from, DateTime to, int limit = 1000); + + // Events + Task> GetEventsAsync(EventQueryDto query); + + // Metadata + Task> GetTagMetadataAsync(string query, int limit = 10); + + // Tag Manager (Point Builder) + Task> GetTagManagerEntriesAsync(string? controllerId = null); + Task ToggleTagActiveAsync(int id, bool isActive); + Task BulkTagUpdateAsync(List updates); + + // Setup + Task> GetControllerStatusAsync(); + Task StartControllerAsync(string controllerId); + Task StopControllerAsync(string controllerId); + + // Text-to-SQL + Task TextToSqlAsync(string question); + + // Reports + Task> GetReportTemplatesAsync(); + Task GenerateReportAsync(string templateId, Dictionary parameters); + + // P&ID + Task UploadPidDrawingAsync(Stream fileStream, string fileName); + + // KB + Task> GetKbCollectionsAsync(); + Task> GetKbDocumentsAsync(string collectionKey); + + // Trend + Task> GetTrendWorkspacesAsync(); +} +``` + +> **Note:** 각 DTO 클래스는 실제 API JSON 응답 구조에 맞춰 `System.Text.Json` `JsonPropertyName` 특성으로 매핑.