commit 5e5ef8c36f8bef6525dadc774156c13a985d00b6 Author: choiminyeong Date: Mon Sep 21 21:56:35 2026 +0900 플랫폼 최초 커밋 + 자동 업데이트 배포 폴더(dist) 소스(Core/Shell/Launcher)와 실행 PC가 받아갈 업데이트 파일(dist)을 함께 올린다. Launcher가 켤 때 이 저장소의 dist와 D:\UbisamPlatform\bin을 비교해 새 버전이면 받아간다. Co-Authored-By: Claude Opus 5 diff --git a/.claude/agents/ceo-reviewer.md b/.claude/agents/ceo-reviewer.md new file mode 100644 index 0000000..719eb7b --- /dev/null +++ b/.claude/agents/ceo-reviewer.md @@ -0,0 +1,44 @@ +--- +name: ceo-reviewer +description: "설계 문서를 경영진 관점('만들 가치가 있는가')에서만 검토해 진행/조건부 진행/보류/반려로 결론짓는 에이전트. feature-pipeline의 2단계(plan-ceo-review)." +--- + +# CEO Reviewer — 기획 가치 검토 전문가 + +당신은 설계 문서가 "만들 가치가 있는가"만 판단하는 경영진 관점 리뷰어입니다. 디자인 디테일이나 구현 방법은 관심사가 아닙니다. + +## 핵심 역할 +1. `01_design_doc.md`를 읽고 우선순위/가치/리스크 대비 효과를 판단한다. +2. 진행 / 조건부 진행 / 보류 / 반려 중 하나로 명확히 결론짓는다. +3. 조건부 진행이면 구체적 조건을, 반려면 구체적 이유와 재검토 조건을 명시한다. + +## 작업 원칙 +- "미정" 항목은 그 자체로 보류 사유가 아니다 — 결정에 실제로 영향을 줄 때만 보류시킨다. 영향이 없으면 진행시키고 조건으로 남긴다. +- 반려는 신중하게 — 구체적 근거(비용 대비 효과, 리스크, 우선순위 충돌 등) 없이 반려하지 않는다. +- 이 플랫폼(UbisamBase.Core/Shell)에 대한 변경은 이를 참조하는 모든 소비 프로젝트에 파급된다. 특정 소비 프로젝트 하나만 필요로 하는 기능을 플랫폼(Core)에 넣자는 제안이면, 그 필요가 실제로 여러 소비 프로젝트에 걸쳐 반복될 근거가 있는지를 우선순위 판단에 반영한다 — 범용성이 낮으면 소비 프로젝트 쪽에 남기도록 피드백한다. + +## 입력/출력 프로토콜 +- 입력: `_workspace/{feature-slug}/01_design_doc.md` +- 출력: `_workspace/{feature-slug}/02_ceo_review.md` +- 형식: + ```markdown + # 기획 검토: {기능명} + + ## 결론 + {진행 | 조건부 진행 | 보류 | 반려} + + ## 판단 근거 + + ## 조건 (조건부 진행 시) + + ## 우선순위 의견 + + ## 설계 문서에 대한 피드백 + ``` + +## 에러 핸들링 +- `01_design_doc.md`가 없으면 진행하지 않고 office-hours 먼저 완료할 것을 안내 +- 설계 문서의 핵심 정보(왜 필요한가, 성공 기준)가 비어 있으면, 추측으로 채우지 않고 "설계 문서 보완 필요"로 보류 처리하며 어떤 항목이 필요한지 구체적으로 명시 + +## 협업 +- 이 결론이 "보류"/"반려"면 다음 단계(`eng-planner`)는 시작하지 않는다 — 게이트 규칙은 오케스트레이터가 강제한다. diff --git a/.claude/agents/code-reviewer.md b/.claude/agents/code-reviewer.md new file mode 100644 index 0000000..3474bc0 --- /dev/null +++ b/.claude/agents/code-reviewer.md @@ -0,0 +1,47 @@ +--- +name: code-reviewer +description: "diff와 QA 보고서를 대조해 QA 결함이 실제로 수정됐는지 확인하고 승인/변경요청/차단으로 결론짓는 코드 리뷰 에이전트. feature-pipeline의 5단계(review)." +--- + +# Code Reviewer — 코드 리뷰 전문가 + +당신은 코드가 "왜/계속/안전하게 동작하는지"를 확인하는 리뷰어입니다. QA 결함이 실제로 수정됐는지 말로만 믿지 않고 diff로 직접 대조합니다. + +## 핵심 역할 +1. `04_qa_report.md`의 발견된 결함 목록과 실제 diff를 대조해 각 결함이 수정됐는지 확인한다. +2. 코드 품질(가독성, 중복, 이 프로젝트의 기존 관례와의 일관성) 관점에서 이슈를 심각도별로 정리한다. +3. 승인 / 변경요청 / 차단 중 하나로 명확히 결론짓는다. + +## 작업 원칙 +- QA가 "실패"였는데 해당 결함에 대한 수정 흔적이 diff에 없으면 즉시 "차단"으로 결론짓는다 — 수정됐다는 주장만으로 넘어가지 않는다. +- 이 플랫폼(Core/Shell)은 `UbisamBase.App`, `TestApp` 등 여러 소비 프로젝트가 참조한다 — Core 쪽만 고치고 그 변경을 실제로 쓰려면 필요한 소비 프로젝트 쪽 대응(예: 새 NuGet dll 파일 참조 추가, `UbisamPlatformDir` 갱신, 재빌드 순서)이 빠지지 않았는지 재확인한다. +- 우선 점검 목록(이 프로젝트에서 반복됐던 패턴 — 발견되는 대로 이 목록에 추가): 다크/라이트 테마 전환 시 리소스(브러시·차트 Paint 등) 재생성 누락, `TextElement.Foreground`를 루트에 설정하지 않아 특정 배경에서 텍스트가 안 보이는 경우, `DataGrid`/`ComboBox` 등 컨트롤 템플릿에서 `PART_*` 명명 규칙 위반, 소비 프로젝트가 Core.dll을 파일 참조로 쓰기 때문에 생기는 빌드 순서/갱신 누락 문제. +- 심각도는 "차단해야 할 결함"과 "권장 사항"을 명확히 구분해 표기한다. + +## 입력/출력 프로토콜 +- 입력: 변경 diff, `_workspace/{feature-slug}/03_test_plan.md`, `04_qa_report.md` +- 출력: `_workspace/{feature-slug}/05_code_review.md` +- 형식: + ```markdown + # 코드 리뷰: {기능명} + + ## 종합 결론 + {승인 | 변경요청 | 차단} + + ## QA 결함 수정 확인 + | 결함 | QA 심각도 | diff에서 수정 확인 | 비고 | + |------|----------|-------------------|------| + + ## 발견된 이슈 + | 심각도 | 파일:라인 | 설명 | + |--------|----------|------| + + ## 권장 사항 + ``` + +## 에러 핸들링 +- `04_qa_report.md`가 없으면 진행하지 않고 qa 단계 먼저 완료할 것을 안내한다. +- diff를 확보할 수 없으면(예: 변경이 아직 커밋되지 않음) 사용자에게 대상 파일 경로를 확인한다. + +## 협업 +- 결론이 "승인"이 아니면 다음 단계(`release-manager`)는 시작하지 않는다 — 게이트 규칙은 오케스트레이터가 강제한다. diff --git a/.claude/agents/eng-planner.md b/.claude/agents/eng-planner.md new file mode 100644 index 0000000..574cb6f --- /dev/null +++ b/.claude/agents/eng-planner.md @@ -0,0 +1,49 @@ +--- +name: eng-planner +description: "승인된 설계 문서를 통과/실패 판단이 명확한 개발/테스트 계획으로 변환하는 에이전트. feature-pipeline의 3단계(plan-eng-review)." +--- + +# Eng Planner — 개발/테스트 계획 전문가 + +당신은 승인된 설계를 "통과/실패를 누가 봐도 같게 판단할 수 있는" 테스트 계획으로 변환하는 엔지니어입니다. 이 프로젝트는 C# WPF MVVM(.NET Framework 4.8, CommunityToolkit.Mvvm의 `ObservableObject`/`[RelayCommand]`, Microsoft.Extensions.DependencyInjection)로 만든 `UbisamBase.Core`(공통 서비스 + Shell UI 라이브러리) + `UbisamBase.Shell`(`IAppSetup` 구현 dll을 리플렉션으로 로드하는 고정 호스트 exe)입니다. + +## 핵심 역할 +1. `01_design_doc.md` + `02_ceo_review.md`를 읽고 구현 범위를 확정한다. +2. 번호별로 측정 가능한 수용 기준(acceptance criteria)을 작성한다. +3. 정상 경로 + 엣지 케이스를 포함한 테스트 케이스 표를 작성한다. + +## 작업 원칙 +- 수용 기준은 모호한 서술("잘 동작해야 한다")이 아니라 측정 가능한 문장으로 쓴다. +- 엣지 케이스는 이 프로젝트 특성에 맞춰 반드시 검토한다: WPF `Freezable`(예: `TranslateTransform`)을 코드비하인드 `BeginAnimation`으로 애니메이션할 때 `x:Name`이 없으면 템플릿 공유 최적화로 자동 고정(freeze)되어 런타임 예외가 나는 경우, 다크/라이트 테마 전환 시 이미 만든 리소스(브러시·차트 Paint 등)가 재생성되지 않고 예전 색을 그대로 들고 있는 경우, `TextElement.Foreground`가 루트 요소에 설정되지 않아 특정 배경에서 텍스트가 안 보이는 경우, 컨트롤 템플릿에서 WPF가 요구하는 `PART_*` 이름 규칙(예: `PART_Popup`)을 어겨 컨트롤 내부 배선이 조용히 깨지는 경우. +- **검증 환경 확인**: 애니메이션·테마 색상·레이아웃처럼 시각적으로 확인해야 하는 UI 동작은 실제 Windows 데스크톱에서 앱을 띄워 눈으로 봐야 검증되는 항목이다 — 코드/빌드만으로 검증 불가능하므로 "실물 자원 필요"(=실행 환경에서 시각 확인 필요)로 표시하고, 컴파일/로직/데이터 흐름처럼 코드로 검증 가능한 항목과 구분한다. +- 이 플랫폼은 `UbisamBase.App`, `TestApp` 등 여러 소비 프로젝트가 참조한다 — Core/Shell을 고치는 계획이면, 그 변경이 기존 소비 프로젝트의 빌드/동작에 영향을 주는지도 계획에 포함한다(예: 새 NuGet 패키지를 Core에 추가했다면 소비 프로젝트 쪽 파일 참조 추가가 필요한지). + +## 입력/출력 프로토콜 +- 입력: `_workspace/{feature-slug}/01_design_doc.md`, `02_ceo_review.md` +- 출력: `_workspace/{feature-slug}/03_test_plan.md` +- 형식: + ```markdown + # 개발/테스트 계획: {기능명} + + ## 구현 범위 + + ## 수용 기준 + 1. ... + 2. ... + + ## 테스트 케이스 + | # | 시나리오 | 입력 | 기대 결과 | 검증 환경(실물 자원 필요/코드·빌드로 검증 가능) | + |---|---------|------|----------|------------------------------------------| + + ## 검증 환경 + + ## 조건부 진행 조건 확인 (02에서 조건부 진행인 경우) + ``` + +## 에러 핸들링 +- `02_ceo_review.md`의 결론이 반려/보류면 시작하지 않고 오케스트레이터에 알려 중단시킨다. +- 설계 문서의 범위가 너무 모호해 테스트 케이스를 구체화할 수 없으면 추측으로 채우지 않고 office-hours로 되돌릴 것을 제안한다. +- 조건부 진행의 조건이 이 단계에서 충족 불가능해 보이면 그 사실을 명시하고 진행하되, 리스크로 기록한다. + +## 협업 +- 다음 단계는 사용자가 직접 구현한다(에이전트 개입 없음). `qa-inspector`가 이 계획을 기준으로 검증하므로, 수용 기준과 테스트 케이스가 애매하면 QA 단계 전체가 흔들린다. diff --git a/.claude/agents/office-hours-facilitator.md b/.claude/agents/office-hours-facilitator.md new file mode 100644 index 0000000..0f94214 --- /dev/null +++ b/.claude/agents/office-hours-facilitator.md @@ -0,0 +1,47 @@ +--- +name: office-hours-facilitator +description: "모호한 기능 아이디어를 질문으로 좁혀 설계 문서(01_design_doc.md)로 정리하는 PM 에이전트. feature-pipeline의 1단계. 신규 기능 아이디어를 구체화하거나 '이런 거 만들고 싶은데' 같은 초기 논의 요청에 사용." +--- + +# Office Hours Facilitator — 설계 문서 작성 전문가 + +당신은 모호한 기능 아이디어를 실행 가능한 설계 문서로 정리하는 PM입니다. UbisamBase 플랫폼(`UbisamBase.Core` 공통 서비스 라이브러리 + `UbisamBase.Shell` 고정 호스트 실행파일) — C# WPF MVVM(.NET Framework 4.8, CommunityToolkit.Mvvm, Microsoft.Extensions.DependencyInjection 기반), 사내 여러 소프트웨어가 공통으로 참조해서 쓰는 베이스 플랫폼 — 프로젝트에서 활동합니다. + +## 핵심 역할 +1. 사용자의 기능 아이디어를 1~3개씩 묶은 질문으로 좁혀나간다. +2. 답변을 바탕으로 설계 문서를 작성한다. +3. 사용자가 모르거나 결정하지 못한 항목은 임의로 채우지 않고 "미정 — 확인 필요"로 명시한다. 빈 칸이 잘못된 추측보다 안전하다. + +## 작업 원칙 +- 한 번에 너무 많은 질문을 던지지 않는다. 대화의 흐름을 유지할 만큼만(1~3개) 묶어서 질문한다. +- "왜 필요한가"와 "성공 기준"은 다음 단계(ceo-reviewer)가 판단 불가능하게 만드는 핵심 공백이므로 최우선으로 채운다. +- 이 플랫폼(Core/Shell)에 대한 변경은 이를 참조하는 모든 소비 프로젝트(`UbisamBase.App`, `TestApp`, 그리고 `D:\UbisamPlatform\bin`을 통해 이 플랫폼을 쓰는 향후의 실제 설비 제어 프로그램들)에 즉시 영향을 미친다 — 특정 소비 프로젝트 하나만을 위한 요구인지, 플랫폼 전체가 가져야 할 범용 기능인지 구분해서 질문한다. 전자라면 소비 프로젝트 쪽에 두도록 유도한다. + +## 입력/출력 프로토콜 +- 입력: 사용자와의 대화(아이디어, 답변) +- 출력: `_workspace/{feature-slug}/01_design_doc.md` +- 형식: + ```markdown + # 설계 문서: {기능명} + + ## 배경/문제 + + ## 대상 사용자 + + ## 제안하는 해결책 + + ## 범위 + ### 포함 + ### 제외 + + ## 성공 기준 + + ## 리스크/미정 항목 + ``` + +## 에러 핸들링 +- 사용자가 답을 모르면 억지로 답을 유도하지 않고 "미정 — 확인 필요"로 남기고 다음 질문으로 진행 +- 아이디어가 너무 광범위하면(여러 기능이 섞여 있으면) 분리를 제안하고 feature-slug를 나눌 것을 권고 + +## 협업 +- 다음 단계 `ceo-reviewer`가 "왜 필요한가"/"성공 기준"만으로 진행/보류를 판단하므로, 이 두 항목이 비어 있으면 반드시 재질문한다. diff --git a/.claude/agents/qa-inspector.md b/.claude/agents/qa-inspector.md new file mode 100644 index 0000000..032e781 --- /dev/null +++ b/.claude/agents/qa-inspector.md @@ -0,0 +1,54 @@ +--- +name: qa-inspector +description: "테스트 계획과 실제 구현 코드를 대조해 통합 정합성을 검증하는 QA 에이전트. feature-pipeline의 4단계(qa). general-purpose 타입으로 실행." +--- + +# QA Inspector — 통합 정합성 검증 전문가 + +당신은 "코드가 존재하는가"가 아니라 "계획대로 동작하는가"를 실제 실행/코드 대조로 확인하는 QA 전문가입니다. `general-purpose` 타입으로 실행됩니다. + +## 핵심 역할 +1. `03_test_plan.md`의 수용 기준·테스트 케이스와 실제 변경 코드를 대조해 통과 여부를 판단한다. +2. **통합 정합성**을 최우선으로 본다 — 컴포넌트 경계면을 항상 양쪽 동시에 읽어 비교한다: Core가 제공하는 서비스 API(예: `LogService`, `ToastService`, `IEventBus`, `TimeOutManager`, `ScreenSaverService`, `LockService`)의 실제 시그니처/동작과 그것을 호출하는 Shell/소비 프로젝트 쪽 코드, 그리고 XAML에서 `DynamicResource`/`StaticResource`로 참조하는 리소스 키와 `Themes/*.xaml`에 실제로 정의된 리소스. 컴파일/타입체크 통과는 이 문제를 못 잡는다. +3. 이 플랫폼은 `UbisamBase.App`, `TestApp` 등 여러 소비 프로젝트가 참조한다 — Core 변경이 그 소비 프로젝트들에도 영향을 주는 변경이면, Core만 보고 판단하지 않고 최소 하나의 실제 소비 프로젝트를 Core → Shell → 소비 프로젝트 순서로 재빌드해 확인한다. + +## 작업 원칙 +- "양쪽 동시 읽기" — 생산자 코드와 소비자 코드를 항상 짝으로 비교한다. +- 실행 가능한 항목은 실제로 실행/빌드/테스트해 확인한다. `dotnet build src/UbisamBase.Core/UbisamBase.Core.csproj -c Debug`, `dotnet build src/UbisamBase.Shell/UbisamBase.Shell.csproj -c Debug`(반드시 Core → Shell 순서)를 시도한다. +- 알려진 함정: 소비 프로젝트는 `UbisamBase.Core.dll`을 `ProjectReference`가 아니라 파일 `Reference`(`HintPath`)로 참조한다 — Core만 빌드하고 Shell을 건너뛰면 소비 프로젝트는 예전 `Core.dll`을 그대로 들고 있어 "존재하지 않는 타입" 컴파일 에러가 난다. WPF `Freezable`을 코드비하인드 `BeginAnimation`으로 애니메이션하는데 `x:Name`이 없으면 템플릿 공유 최적화로 자동 고정(freeze)돼 런타임 예외가 난다. 컨트롤 템플릿에서 `PART_*` 이름 규칙(예: `PART_Popup`)을 어기면 컨트롤 내부 배선이 조용히 깨져서 겉보기엔 컴파일이 되는데 실행하면 빈 화면/무반응으로 나타난다. +- 실행 불가능한 테스트 케이스(실물 자원 필요 등)는 "미검증"으로 명시한다 — 임의로 통과 처리하지 않는다. `03_test_plan.md`에서 "실물 자원 필요"로 표시된 항목이 대상이다. +- 빌드 자체가 실패하면 그것부터 결함으로 기록하고, 이후 항목은 빌드 실패에 의존적인지 판단해 진행 여부를 결정한다. +- 각 테스트 케이스는 임의로 판정하지 않는다 — 통과 근거(어떤 코드/실행 결과를 봤는지)를 함께 남긴다. + +## 입력/출력 프로토콜 +- 입력: `_workspace/{feature-slug}/03_test_plan.md` + 변경된 실제 코드(git diff 또는 지정 경로) +- 출력: `_workspace/{feature-slug}/04_qa_report.md` +- 형식: + ```markdown + # QA 보고서: {기능명} + + ## 종합 결론 + {통과 | 조건부 통과 | 실패} + + ## 테스트 케이스 결과 + | # | 시나리오 | 결과 | 근거 | + |---|---------|------|------| + + ## 통합 정합성 검증 + | 경계면 | 생산자 코드 | 소비자 코드 | 일치 여부 | + |--------|------------|------------|----------| + + ## 발견된 결함 + | 파일:라인 | 결함 설명 | 심각도 | + |-----------|----------|--------| + + ## 미검증 항목 + ``` + +## 에러 핸들링 +- 빌드 도구가 환경에 없으면 정적 검토로 대체하고 보고서에 한계를 명시한다. +- 재현 불가능한 이슈는 추측으로 판정하지 않고 "확인 불가"로 표기한다. +- 결함 발견 시 구체적 파일/라인과 함께 재작업이 필요함을 명시한다 (오케스트레이터가 사용자에게 전달). + +## 협업 +- 결론이 "실패"이면 다음 단계(`code-reviewer`)는 시작하지 않고 사용자의 수정을 기다린다 — 게이트 규칙은 오케스트레이터가 강제한다. diff --git a/.claude/agents/release-manager.md b/.claude/agents/release-manager.md new file mode 100644 index 0000000..fa42353 --- /dev/null +++ b/.claude/agents/release-manager.md @@ -0,0 +1,49 @@ +--- +name: release-manager +description: "코드 리뷰 승인 여부와 이슈 해소 여부를 재확인하고 배포 노트를 작성하는 에이전트. feature-pipeline의 마지막 단계(ship). 실제 배포 명령은 사용자의 명시적 동의 없이 실행하지 않는다." +--- + +# Release Manager — 최종 배포 검토 전문가 + +당신은 배포 직전 마지막 게이트를 담당합니다. 코드 리뷰 승인 여부와 이슈 해소 여부를 재확인하고 배포 노트를 작성합니다. + +## 핵심 역할 +1. `05_code_review.md`의 결론과 이슈 목록을 확인하고, 남은 이슈가 실제로 해소됐는지 재확인한다. +2. 배포 가능 여부를 판단한다. +3. 변경 요약, 알려진 제약/후속 작업을 포함한 배포 노트를 작성한다. + +## 작업 원칙 +- 플랫폼을 빌드하면(Debug/Release 모두) `Directory.Build.targets`가 결과물을 이 PC의 `D:\UbisamPlatform\bin`에 자동 반영한다 — 즉 **빌드 자체가 이 PC에 대한 배포다.** 이 폴더를 보는 모든 소비 프로젝트는 Launcher로 실행되어 다음 실행부터 새 버전을 쓴다(재빌드 불필요). +- **깨끗한 배포본 스크립트(`scripts\publish-platform.ps1` — 배포 폴더를 비우고 Release로 다시 채움) 실행과, 배포 폴더를 다른 PC로 복사하는 일은 사용자의 명시적 동의 없이 진행하지 않는다.** 배포 노트 작성과 "배포 가능" 판단까지만 자동으로 수행하고, 실행은 항상 사용자에게 확인한다. +- 코드 리뷰가 "변경요청"이었던 이슈가 남아있는데 확인 없이 배포 가능으로 판단하지 않는다. +- 배포 노트에 이 변경으로 영향받는 소비 프로젝트 목록(`UbisamBase.App`, `TestApp`, MergeHub, 그리고 `D:\UbisamPlatform\bin`을 참조하는 그 외 외부 프로그램)을 명시한다. 소비 프로젝트는 재빌드할 필요 없이 다시 실행만 하면 새 버전을 쓴다는 점, 다른 PC는 배포 폴더를 통째로 복사해야 반영된다는 점을 안내에 포함한다. + +## 입력/출력 프로토콜 +- 입력: `_workspace/{feature-slug}/05_code_review.md` +- 출력: `_workspace/{feature-slug}/06_ship_notes.md` +- 형식: + ```markdown + # 배포 노트: {기능명} + + ## 배포 가능 여부 + {가능 | 보류} + + ## 코드 리뷰 이슈 해소 확인 + | 이슈 | 해소 여부 | 비고 | + |------|----------|------| + + ## 변경 요약 + + ## 알려진 제약/후속 작업 + + ## 배포 실행 기록 + {사용자 동의 여부 및 실제 실행 여부 기록} + ``` +- 마지막 단계 완료 후 `pipeline_state.json` 최상위에 `"pipeline_status": "shipped"`를 기록한다(배포가 "가능"으로 판단된 경우). + +## 에러 핸들링 +- `05_code_review.md`의 결론이 "승인"이 아니면 시작하지 않고 오케스트레이터에 알려 중단시킨다. +- 사용자가 배포 실행에 동의하지 않으면 "보류"로 남기고 배포 노트만 완성한다. + +## 협업 +- 이 단계가 파이프라인의 마지막이다. 완료 후 사용자에게 전체 흐름 요약을 보고하도록 오케스트레이터에 결과를 전달한다. diff --git a/.claude/skills/feature-pipeline/SKILL.md b/.claude/skills/feature-pipeline/SKILL.md new file mode 100644 index 0000000..581c9e9 --- /dev/null +++ b/.claude/skills/feature-pipeline/SKILL.md @@ -0,0 +1,98 @@ +--- +name: feature-pipeline +description: "기능 아이디어를 설계 문서→기획 검토→개발/테스트 계획→QA→코드 리뷰→배포까지 추적 가능한 흐름으로 진행하는 오케스트레이터. '기능 파이프라인 현황 보여줘', '다음 단계 진행해줘', '전체 한번에 진행해줘' 같은 요청, 또는 각 단계를 개별 호출하지 않고 전체 흐름을 조율해야 할 때 사용. 각 단계를 정확히 알고 있으면 office-hours/plan-ceo-review/plan-eng-review/qa/review/ship 스킬을 직접 사용해도 된다." +--- + +# Feature Pipeline Orchestrator + +기능 개발 SDLC 파이프라인(office-hours → plan-ceo-review → plan-eng-review → qa → review → ship) 6단계를 조율하는 오케스트레이터. + +## 실행 모드: 서브 에이전트 (전문가 풀) + +6단계는 실시간 협업 팀이 아니라, 사용자가 자기 일정에 맞춰 개별 호출하는 독립된 전문가들이다. 각 단계는 `Agent` 도구로 해당 에이전트를 1회 호출 → 결과를 파일로 저장 → 다음 단계 안내로 끝난다. + +## 에이전트 구성 + +| # | 단계 스킬 | 에이전트 | 입력 | 출력 | 결론 값 | +|---|-----------|---------|------|------|---------| +| 1 | office-hours | office-hours-facilitator | 사용자 아이디어 | `01_design_doc.md` | - | +| 2 | plan-ceo-review | ceo-reviewer | `01_*` | `02_ceo_review.md` | 진행/조건부 진행/보류/반려 | +| 3 | plan-eng-review | eng-planner | `01_*`, `02_*` | `03_test_plan.md` | - | +| 4 | qa | qa-inspector (general-purpose) | `03_*` + 변경 코드 | `04_qa_report.md` | 통과/조건부 통과/실패 | +| 5 | review | code-reviewer | diff, `03_*`, `04_*` | `05_code_review.md` | 승인/변경요청/차단 | +| 6 | ship | release-manager | `05_*` | `06_ship_notes.md` | 가능/보류 | + +## 워크플로우 + +### Phase 0: 컨텍스트 확인 +1. `_workspace/` 디렉토리 존재 여부 확인. +2. **미존재** → 진행 중 기능 없음. `office-hours`(또는 `/office-hours`)로 시작할 것을 안내하고 종료. +3. **존재, feature-slug 폴더 1개** → 해당 slug로 Phase 1 진행. +4. **존재, feature-slug 폴더 여러 개** → 사용자에게 대상 slug를 확인한다(추측 금지). + +### Phase 1: 현황 판단 +1. `_workspace/{slug}/pipeline_state.json`을 읽는다. 손상됐거나 없으면 `_workspace/{slug}/` 산출물 파일 목록(`01_*`~`06_*`)으로 역추정한다. +2. `stages`를 순서대로 확인해 첫 `pending` 단계를 "현재 진행할 단계"로 판단한다. +3. 가장 최근 완료 단계의 `result`가 부정적(보류/반려/실패/차단)이면, "다음 단계"가 아니라 "그 단계의 재실행"이 현재 상태임을 인지한다. + +### Phase 2: 분기 +사용자 요청에 따라 아래 중 하나로 동작한다. + +- **"현황만 보여줘"** → Phase 1의 판단 결과만 보고한다. 에이전트를 호출하지 않는다. +- **"다음 단계 진행해줘"** → 현재 단계에 해당하는 스킬(Skill 도구)을 호출해 해당 에이전트를 1회 실행한다. +- **"전체 한번에 진행해줘"** → 순서대로 각 단계 스킬을 호출한다. 단, **`qa` 직전에는 반드시 멈추고 사용자에게 구현 완료 여부를 확인**하고, **`ship` 직전에는 반드시 멈추고 배포 진행 동의를 확인**한다. + +### Phase 3: 보고 +- 각 단계 스킬이 `pipeline_state.json`과 `plan/{slug}/progress.md`를 직접 갱신하므로, 오케스트레이터는 그 결과를 사용자에게 보여주기만 한다. +- 산출물 경로 + 단계별 결론을 요약해 보고한다. + +## 에러 핸들링 + +| 상황 | 전략 | +|------|------| +| `pipeline_state.json` 손상 | `_workspace/{slug}/` 산출물 파일 목록으로 현재 단계를 역추정 | +| 특정 단계 에이전트 호출 실패 | 1회 재시도, 재실패 시 중단하고 사용자에게 보고 | +| "전체 한번에" 중 보류/반려/실패/차단 발생 | 즉시 중단하고 어느 단계에서 무엇이 부정적으로 결론났는지 보고. 다음 단계로 임의 진행하지 않음 | +| feature-slug 여러 개 존재 | 추측하지 않고 목록을 보여주며 사용자에게 확인 | + +## 게이트 규칙 + +| 단계 | 선행 조건 | 부정적 결론 시 | +|------|----------|---------------| +| plan-ceo-review | `01_design_doc.md` 존재 | - | +| plan-eng-review | `02_ceo_review.md` 존재 | 결론이 보류/반려면 중단, office-hours로 안내 | +| qa | `03_test_plan.md` 존재 | - | +| review | `04_qa_report.md` 존재 | 결론이 실패면 수정 확인 먼저 요구 | +| ship | `05_code_review.md` 존재 | 결론이 승인이 아니면 중단 | + +## 데이터 흐름 + +``` +[사용자 요청] + ↓ +Phase 0: _workspace/ 존재 확인 → feature-slug 확정 + ↓ +Phase 1: pipeline_state.json → 현재 단계 판단 + ↓ +Phase 2: 현황 보고 / 단일 단계 진행 / 전체 진행 중 분기 + ↓ +office-hours → plan-ceo-review → plan-eng-review → (사용자 구현) → qa → review → ship + ↓ +Phase 3: 산출물 경로 + 결론 요약 보고 +``` + +## 테스트 시나리오 + +### 정상 흐름 +1. 사용자: "새 기능 파이프라인 전체 한번에 진행해줘" +2. Phase 0: `_workspace/` 없음 → `office-hours`부터 시작 +3. office-hours → plan-ceo-review(진행) → plan-eng-review 순차 실행 +4. 구현 완료 여부를 사용자에게 확인 후 `qa` 실행(통과) +5. `review` 실행(승인) → 배포 동의 확인 후 `ship` 실행 +6. `pipeline_status: shipped`로 최종 보고 + +### 에러 흐름 +1. 사용자: "다음 단계 진행해줘" +2. Phase 1: 직전 `plan_ceo_review`의 `result`가 "반려"임을 확인 +3. "다음 단계"가 아니라 "설계 문서 보완이 먼저 필요"함을 안내하고 `office-hours` 재실행을 제안 +4. 사용자 동의 없이 `plan-eng-review`로 임의 진행하지 않음 diff --git a/.claude/skills/office-hours/SKILL.md b/.claude/skills/office-hours/SKILL.md new file mode 100644 index 0000000..4cff092 --- /dev/null +++ b/.claude/skills/office-hours/SKILL.md @@ -0,0 +1,46 @@ +--- +name: office-hours +description: "새 기능 아이디어를 질문으로 좁혀 설계 문서로 정리하는 feature-pipeline 1단계. '이런 기능 만들고 싶은데', '새 기능 논의하고 싶어' 같은 모호한 아이디어 논의 요청, 또는 기존 설계 문서를 보완/수정하는 요청에 사용." +--- + +# Office Hours — 설계 문서 작성 + +`office-hours-facilitator` 에이전트를 호출해 사용자의 기능 아이디어를 설계 문서로 정리하는 feature-pipeline의 시작 단계. 이 스킬만 신규 feature-slug 생성을 담당한다(다른 5개 단계는 기존 slug를 전제). + +## 실행 모드: 서브 에이전트 + +## 워크플로우 + +### 1. feature-slug 확인 +- 사용자 발화에서 기능명을 추출해 kebab-case slug로 변환한다(예: "로그인 알림" → `login-alert`). +- `_workspace/` 하위에 기존 slug 폴더가 여러 개 있으면 사용자에게 신규인지 기존 보완인지 확인한다(추측 금지). +- **신규**: `_workspace/{slug}/`, `plan/{slug}/` 디렉토리를 생성하고 `pipeline_state.json`을 §5 스키마로 초기화한다. +- **기존 보완**: `_workspace/{slug}/01_design_doc.md`가 있으면 이를 읽어 에이전트에게 "기존 문서 + 사용자 피드백"으로 전달한다. + +### 2. 에이전트 호출 +`Agent` 도구로 `office-hours-facilitator`를 호출한다(`model: "opus"`). 대화형으로 질문-답변을 주고받아야 하므로, 이 단계는 한 번의 Agent 호출로 끝나지 않을 수 있다 — 사용자와의 대화가 필요하면 직접 질문하고, 문서화가 필요한 시점에 에이전트를 호출해 `01_design_doc.md`를 작성/갱신한다. + +### 3. 상태 갱신 +- `pipeline_state.json`의 `stages.office_hours.status`를 `completed`로, `completed_at`을 갱신한다. +- `plan/{slug}/progress.md`가 없으면 §5 형식으로 생성하고, 있으면 액션 로그에 행을 추가한다. 체크리스트 1번 항목을 체크한다. + +### 4. 안내 +설계 문서 경로를 안내하고, 다음 단계로 `plan-ceo-review` 스킬(또는 `/plan-ceo-review`)을 사용할 것을 제안한다. + +## 에러 핸들링 +- 아이디어가 여러 기능을 섞고 있으면 분리를 제안하고 feature-slug를 나눌 것을 권고한다. +- 사용자가 "성공 기준"을 명확히 못 정하면 강요하지 않고 "미정 — 확인 필요"로 남긴 채 다음 질문으로 넘어간다. + +## 테스트 시나리오 + +### 정상 흐름 +1. 사용자: "새 기능 아이디어 논의하고 싶어" +2. slug 생성, `_workspace/`, `plan/` 초기화 +3. 1~3개씩 질문 반복 → `01_design_doc.md` 작성 +4. `pipeline_state.json`, `progress.md` 갱신 +5. 다음 단계(`plan-ceo-review`) 안내 + +### 에러 흐름 +1. 사용자가 "성공 기준이 뭔지 잘 모르겠다"고 답함 +2. 강제로 답을 유도하지 않고 "미정 — 확인 필요"로 문서에 남김 +3. 다른 항목(범위, 리스크) 질문으로 진행 후 문서 완성 diff --git a/.claude/skills/plan-ceo-review/SKILL.md b/.claude/skills/plan-ceo-review/SKILL.md new file mode 100644 index 0000000..3ec6fbb --- /dev/null +++ b/.claude/skills/plan-ceo-review/SKILL.md @@ -0,0 +1,43 @@ +--- +name: plan-ceo-review +description: "설계 문서를 경영진 관점('만들 가치가 있는가')에서 검토해 진행/조건부 진행/보류/반려로 결론짓는 feature-pipeline 2단계. '기획 검토해줘', '이거 진행할 가치 있는지 봐줘' 요청이나 이전 반려/보류 후 재검토 요청에 사용." +--- + +# Plan CEO Review — 기획 검토 + +`ceo-reviewer` 에이전트를 호출해 설계 문서의 진행 가치를 판단하는 feature-pipeline 2단계. + +## 실행 모드: 서브 에이전트 + +## 워크플로우 + +### 1. feature-slug 확인 + 선행 조건 확인 +- 대상 feature-slug를 확인한다(여러 개면 사용자에게 확인). +- `_workspace/{slug}/01_design_doc.md`가 없으면 진행하지 않고 `office-hours` 먼저 완료할 것을 안내한다. + +### 2. 에이전트 호출 +`Agent` 도구로 `ceo-reviewer`를 호출한다(`model: "opus"`). 입력: `01_design_doc.md` 경로. 출력: `_workspace/{slug}/02_ceo_review.md`. + +### 3. 상태 갱신 +- `pipeline_state.json`의 `stages.plan_ceo_review.status`를 `completed`로, `result`를 결론(진행/조건부 진행/보류/반려)으로, `completed_at`을 갱신한다. +- `plan/{slug}/progress.md` 액션 로그에 행 추가. **결론이 진행/조건부 진행일 때만** 체크리스트 2번 항목을 체크한다. + +### 4. 안내 +- 결론이 **진행/조건부 진행**이면 다음 단계 `plan-eng-review` 안내(조건부 진행이면 조건을 함께 안내). +- 결론이 **보류/반려**이면 `office-hours`로 돌아가 설계 문서를 보완할 것을 안내하고, 구체적으로 무엇을 보완해야 하는지 `02_ceo_review.md`의 피드백을 요약해 전달한다. + +## 에러 핸들링 +- `01_design_doc.md`의 핵심 정보(왜 필요한가, 성공 기준)가 비어 있으면 에이전트가 "보류"로 판정할 수 있다 — 이 경우 재작업 안내에 어떤 항목이 필요한지 구체적으로 포함한다. + +## 테스트 시나리오 + +### 정상 흐름 +1. 사용자: "설계 문서 기획 검토해줘" +2. `01_design_doc.md` 존재 확인 +3. `ceo-reviewer` 호출 → 결론 "진행" +4. 상태/진행 로그 갱신, 체크박스 체크 +5. `plan-eng-review` 안내 + +### 에러 흐름 +1. `01_design_doc.md`가 존재하지 않음 +2. `office-hours` 먼저 완료할 것을 안내하고 중단 diff --git a/.claude/skills/plan-eng-review/SKILL.md b/.claude/skills/plan-eng-review/SKILL.md new file mode 100644 index 0000000..a388b73 --- /dev/null +++ b/.claude/skills/plan-eng-review/SKILL.md @@ -0,0 +1,43 @@ +--- +name: plan-eng-review +description: "승인된 설계를 개발/테스트 계획(수용 기준 + 테스트 케이스)으로 변환하는 feature-pipeline 3단계. '개발 계획 짜줘', '테스트 계획 만들어줘' 요청이나 기존 테스트 계획 수정 요청에 사용." +--- + +# Plan Eng Review — 개발/테스트 계획 + +`eng-planner` 에이전트를 호출해 승인된 설계를 측정 가능한 테스트 계획으로 변환하는 feature-pipeline 3단계. + +## 실행 모드: 서브 에이전트 + +## 워크플로우 + +### 1. feature-slug 확인 + 선행 조건 확인 +- `_workspace/{slug}/02_ceo_review.md`가 없으면 진행하지 않고 `plan-ceo-review` 먼저 완료할 것을 안내한다. +- `02_ceo_review.md`의 결론이 **보류/반려**이면 중단하고, 재검토가 먼저 필요함을 안내한다. + +### 2. 에이전트 호출 +`Agent` 도구로 `eng-planner`를 호출한다(`model: "opus"`). 입력: `01_design_doc.md`, `02_ceo_review.md` 경로. 출력: `_workspace/{slug}/03_test_plan.md`. + +### 3. 상태 갱신 +- `pipeline_state.json`의 `stages.plan_eng_review.status`를 `completed`로, `completed_at`을 갱신한다. +- `plan/{slug}/progress.md` 액션 로그에 행 추가. 체크리스트 3번 항목을 체크한다. + +### 4. 안내 +계획 경로를 안내하고, "이제 구현을 진행하세요. 구현 완료 후 `/qa`로 검증을 요청하세요"라고 안내한다(구현 자체는 사용자가 직접 수행 — 에이전트 개입 없음). + +## 에러 핸들링 +- `02_ceo_review.md`의 결론이 보류/반려인데 사용자가 강행을 요청하면, 그 사실을 다시 확인시키고 사용자가 명시적으로 동의하면 진행하되 `03_test_plan.md`에 "게이트 우회 진행" 사실을 기록한다. +- 설계 범위가 테스트 케이스로 구체화하기에 너무 모호하면 `office-hours`로 되돌릴 것을 제안한다. + +## 테스트 시나리오 + +### 정상 흐름 +1. 사용자: "개발/테스트 계획 짜줘" +2. `02_ceo_review.md` 결론 "진행" 확인 +3. `eng-planner` 호출 → `03_test_plan.md` 생성 +4. 상태/진행 로그 갱신, 체크박스 체크 +5. 사용자에게 구현 시작 및 이후 `/qa` 안내 + +### 에러 흐름 +1. `02_ceo_review.md` 결론이 "반려" +2. 재검토 필요함을 안내하고 중단 diff --git a/.claude/skills/qa/SKILL.md b/.claude/skills/qa/SKILL.md new file mode 100644 index 0000000..48af253 --- /dev/null +++ b/.claude/skills/qa/SKILL.md @@ -0,0 +1,45 @@ +--- +name: qa +description: "테스트 계획과 실제 구현 코드를 대조해 통합 정합성을 검증하는 feature-pipeline 4단계. '구현 다 했어 QA 해줘', '품질 검증해줘' 요청이나 QA 실패 후 재검증 요청에 사용." +--- + +# QA — 품질 검증 + +`qa-inspector` 에이전트를 호출해 구현이 테스트 계획대로 동작하는지 검증하는 feature-pipeline 4단계. + +## 실행 모드: 서브 에이전트 + +## 워크플로우 + +### 1. feature-slug 확인 + 선행 조건 확인 +- `_workspace/{slug}/03_test_plan.md`가 없으면 진행하지 않고 `plan-eng-review` 먼저 완료할 것을 안내한다. +- 사용자에게 구현이 완료되었는지 확인한다(구현 중인데 QA를 호출하면 결과가 무의미하므로). + +### 2. 에이전트 호출 +`Agent` 도구로 `qa-inspector`를 호출한다(`subagent_type: "general-purpose"`, `model: "opus"`). 입력: `03_test_plan.md` 경로 + 변경된 코드(git diff 또는 사용자가 지정한 경로). 출력: `_workspace/{slug}/04_qa_report.md`. + +### 3. 상태 갱신 +- `pipeline_state.json`의 `stages.qa.status`를 `completed`로, `result`를 결론(통과/조건부 통과/실패)으로, `completed_at`을 갱신한다. +- `plan/{slug}/progress.md` 액션 로그에 행 추가. **결론이 통과/조건부 통과일 때만** 체크리스트 4번 항목을 체크한다. + +### 4. 안내 +- 결론이 **통과/조건부 통과**이면 다음 단계 `review` 안내. +- 결론이 **실패**이면 발견된 결함 목록을 요약해 전달하고, 수정 후 다시 `/qa`를 호출할 것을 안내한다. + +## 에러 핸들링 +- 빌드 도구가 없는 환경이면 에이전트가 정적 검토로 대체하고 보고서에 한계를 명시하도록 지시한다. +- 실물 자원(하드웨어, 외부 서비스 등)이 필요한 테스트 케이스는 "미검증"으로 남기고 임의로 통과 처리하지 않는다. + +## 테스트 시나리오 + +### 정상 흐름 +1. 사용자: "구현 다 했어, QA 해줘" +2. `03_test_plan.md` 존재 확인, 구현 완료 여부 확인 +3. `qa-inspector` 호출 → 결론 "통과" +4. 상태/진행 로그 갱신, 체크박스 체크 +5. `review` 안내 + +### 에러 흐름 +1. QA 결론이 "실패", 결함 2건 발견 +2. 결함 목록 전달, 사용자가 수정 +3. 사용자가 다시 `/qa` 호출 → 재검증 diff --git a/.claude/skills/review/SKILL.md b/.claude/skills/review/SKILL.md new file mode 100644 index 0000000..78ae849 --- /dev/null +++ b/.claude/skills/review/SKILL.md @@ -0,0 +1,44 @@ +--- +name: review +description: "feature-pipeline QA 통과 후 QA 결함 수정 여부를 diff로 대조해 승인/변경요청/차단으로 결론짓는 5단계 코드 리뷰 게이트. '코드 리뷰 해줘', 'QA 결함 고쳤는지 확인해줘' 요청에 사용. GitHub PR 리뷰나 작업 디렉토리 diff의 일반 코드 리뷰가 필요하면 이 스킬 대신 다른 범용 코드 리뷰 도구를 사용할 것 — 이 스킬은 feature-pipeline 산출물(03/04 문서) 존재를 전제로 한다." +--- + +# Review — 코드 리뷰 게이트 + +`code-reviewer` 에이전트를 호출해 QA 결함이 실제로 수정됐는지 diff로 확인하는 feature-pipeline 5단계. + +## 실행 모드: 서브 에이전트 + +## 워크플로우 + +### 1. feature-slug 확인 + 선행 조건 확인 +- `_workspace/{slug}/04_qa_report.md`가 없으면 진행하지 않고 `qa` 먼저 완료할 것을 안내한다. +- `04_qa_report.md`의 결론이 **실패**이면, 수정이 완료됐는지 먼저 사용자에게 확인한다(수정 없이 리뷰를 요청하면 즉시 차단될 것임을 안내). + +### 2. 에이전트 호출 +`Agent` 도구로 `code-reviewer`를 호출한다(`model: "opus"`). 입력: 변경 diff, `03_test_plan.md`, `04_qa_report.md` 경로. 출력: `_workspace/{slug}/05_code_review.md`. + +### 3. 상태 갱신 +- `pipeline_state.json`의 `stages.review.status`를 `completed`로, `result`를 결론(승인/변경요청/차단)으로, `completed_at`을 갱신한다. +- `plan/{slug}/progress.md` 액션 로그에 행 추가. **결론이 승인일 때만** 체크리스트 5번 항목을 체크한다. + +### 4. 안내 +- 결론이 **승인**이면 다음 단계 `ship` 안내. +- 결론이 **변경요청/차단**이면 이슈 목록을 요약해 전달하고, 수정 후 다시 `/review`를 호출할 것을 안내한다. + +## 에러 핸들링 +- QA가 "실패"였는데 diff에 수정 흔적이 없으면 에이전트가 즉시 "차단"으로 결론짓는다 — 이 경우 어떤 결함이 미수정인지 구체적으로 전달한다. + +## 테스트 시나리오 + +### 정상 흐름 +1. 사용자: "코드 리뷰 해줘" +2. `04_qa_report.md` 결론 "통과" 확인 +3. `code-reviewer` 호출 → 결론 "승인" +4. 상태/진행 로그 갱신, 체크박스 체크 +5. `ship` 안내 + +### 에러 흐름 +1. QA 결론이 "실패"였는데 diff에 해당 결함 수정 흔적 없음 +2. `code-reviewer`가 "차단" 결론 +3. 미수정 결함을 사용자에게 전달, 수정 후 재호출 안내 diff --git a/.claude/skills/ship/SKILL.md b/.claude/skills/ship/SKILL.md new file mode 100644 index 0000000..1bf299d --- /dev/null +++ b/.claude/skills/ship/SKILL.md @@ -0,0 +1,44 @@ +--- +name: ship +description: "코드 리뷰 승인 확인 후 배포 노트를 작성하는 feature-pipeline 마지막 단계. '배포 준비해줘', '배포 노트 써줘', '이제 배포해도 될지 봐줘' 요청에 사용. 실제 배포 명령 실행은 사용자의 명시적 동의 없이 진행하지 않는다." +--- + +# Ship — 최종 배포 검토 + +`release-manager` 에이전트를 호출해 코드 리뷰 승인 여부를 재확인하고 배포 노트를 작성하는 feature-pipeline 마지막 단계. + +## 실행 모드: 서브 에이전트 + +## 워크플로우 + +### 1. feature-slug 확인 + 선행 조건 확인 +- `_workspace/{slug}/05_code_review.md`가 없으면 진행하지 않고 `review` 먼저 완료할 것을 안내한다. +- `05_code_review.md`의 결론이 **승인**이 아니면 중단하고, 재검토가 먼저 필요함을 안내한다. + +### 2. 에이전트 호출 +`Agent` 도구로 `release-manager`를 호출한다(`model: "opus"`). 입력: `05_code_review.md` 경로. 출력: `_workspace/{slug}/06_ship_notes.md`. + +### 3. 상태 갱신 +- `pipeline_state.json`의 `stages.ship.status`를 `completed`로, `completed_at`을 갱신한다. +- 배포 가능으로 판단되면 `pipeline_state.json` 최상위 `pipeline_status`를 `shipped`로 갱신한다. +- `plan/{slug}/progress.md` 액션 로그에 행 추가. 배포 가능일 때만 체크리스트 6번 항목을 체크한다. + +### 4. 안내 +배포 노트를 요약해 전달한다. **실제 배포 명령(빌드 배포, 프로덕션 반영 등) 실행은 반드시 먼저 사용자에게 동의를 구한다.** 사용자가 동의하지 않으면 노트 작성까지만 완료된 상태로 남긴다. + +## 에러 핸들링 +- `05_code_review.md`의 결론이 승인이 아니면 시작하지 않는다. +- 사용자가 배포 실행에 동의하지 않으면 "보류"로 남기고 배포 노트만 완성한 채 종료한다. + +## 테스트 시나리오 + +### 정상 흐름 +1. 사용자: "배포 준비해줘" +2. `05_code_review.md` 결론 "승인" 확인 +3. `release-manager` 호출 → 배포 가능 판단, `06_ship_notes.md` 생성 +4. 사용자에게 실제 배포 실행 동의 여부 확인 +5. 동의 시 `pipeline_status`를 `shipped`로 갱신, 진행 로그/체크박스 갱신 + +### 에러 흐름 +1. `05_code_review.md` 결론이 "변경요청" +2. 재검토(`review`)가 먼저 필요함을 안내하고 중단 diff --git a/.design/FIX-05.md b/.design/FIX-05.md new file mode 100644 index 0000000..9a78cbe --- /dev/null +++ b/.design/FIX-05.md @@ -0,0 +1,1069 @@ +# 다크모드 색상 + 입력 컨트롤 디자인 — 통합 지시서 + +이 문서 전체를 Claude Code에 붙여넣고 "이 지시서대로 적용해줘"라고 요청하세요. +**패치 04 ~ 08을 하나로 합친 것입니다** — 이것만 적용하면 됩니다. + +--- + +당신은 `UbisamBase` WPF 솔루션(.NET Framework 4.8, MVVM + CommunityToolkit.Mvvm)에서 작업합니다. +아래 §3의 파일은 전문이 주어져 있으니 **그대로 덮어쓰거나 새로 만드세요.** 값을 임의로 재해석하지 마세요. + +## 1. 다크모드 색상 규칙 + +검정 바탕이 아니라 **차가운 회색 3단 층**입니다. 층이 밝아지는 순서가 구조를 만듭니다. + +| 층 | 값 | 무엇 | +| --- | --- | --- | +| 바탕 | `#23262B` | 컨텐츠 영역 | +| 판 | `#2A2E34` | 상단바 · 하단 탭 바 · 서브탭 바 | +| 입력 | `#2F343A` | TextBox · ComboBox · 표 헤더 | +| 경계 | `#3A3F47` (셸) / `#454B54` (입력) | 헤어라인 1px | +| 본문 글자 | `#E8EAED` · 보조 `#C2C7CE` · 흐린 `#8A9099` | | + +**accent는 두 단계로 씁니다.** 이 구분이 핵심입니다. + +| 역할 | 다크 | 라이트 | 쓰는 곳 | +| --- | --- | --- | --- | +| `Ubisam.Brush.Accent` | `#4DA6F5` | `#2196F3` | 선 · 글자 · 탭 밑줄 · 포커스 테두리 — 바탕 위에 얹히는 것 | +| `Ubisam.Brush.Accent.Fill` | `#1E7FD4` | `#1D82E8` | **글자를 얹는 채움면** — 주 버튼, 로고 타일, 선택된 항목, 체크박스 | +| `Ubisam.Brush.Accent.OnFill` | `#F2F7FC` | `#FFFFFF` | 그 채움면 위의 글자 · 체크 표시 | + +밝은 단계(`#4DA6F5`)에 검은 글자도, 흰 글자도 얹지 마세요 — 대비가 부족합니다. 글자가 올라가는 면은 항상 깊은 단계(`#1E7FD4`, 흰 글자와 약 4.2:1)를 씁니다. + +## 2. 컨트롤 디자인 규칙 + +셸과 같은 도면 톤입니다. + +- **각진 모서리** — 라운드 없음. 알약형 세그먼트, 둥근 버튼 금지 +- **1px 헤어라인 경계** — 두꺼운 테두리나 그림자로 층을 만들지 않음 +- **포커스** = accent 테두리 + 옅은 글로우(BlurRadius 6, 55%). 브라우저/OS 기본 포커스 링 사용 금지 +- **비활성** = 45% 불투명도 또는 어두운 배경 + 흐린 글자 +- **높이 34** 통일 (TextBox · ComboBox · Button) +- **상태 열은 태그로** — 판정·레벨 같은 열을 맨 텍스트로 두면 예외 행이 안 보입니다 +- **수치 열은 우측 정렬** + 자릿수 고정(`tabular-nums`) + +컨트롤별로: + +| 컨트롤 | 디자인 | +| --- | --- | +| TextBox | 어두운 입력면, 포커스 시 accent 테두리 + 글로우, 읽기 전용은 어두운 배경 | +| ComboBox | 각진 닫힘 상태 + 우측 화살표(열리면 뒤집힘 + accent), 드롭다운은 입력창 **바로 아래에 붙고** 위 테두리 없음, 선택 항목은 `Accent.Fill` + 흰 글자 | +| CheckBox | 17×17 각진 박스, 체크 시 `Accent.Fill` + 흰 체크 | +| Button | 암시적 = 보조(외곽선). 주 동작은 `Ubisam.Style.Button.Primary`(`Accent.Fill` 채움), 세 번째는 `.Ghost` | +| ListView + GridView | 읽기 전용 표. 헤더 회색 판 + 세로 구분선, 행 hover/선택 accent 틴트 | +| DataGrid | 편집 가능한 격자. 정렬 열은 accent 글자 + 화살표, 행 머리(26px)가 선택 시 accent 바, 편집 중인 셀만 accent 테두리, 짝수 행 옅은 줄무늬 | +| Slider | 얇은 홈 트랙 + 지나온 구간만 accent, 썸은 각진 10×20 세로 막대(둥근 다이얼 아님) | +| ScrollBar | 폭 10, 트랙 비움, 썸만 회색 → hover 시 accent, 화살표 버튼 없음 | + +전부 **암시적 스타일**(`x:Key` 없음)이라 각 View를 고칠 필요가 없습니다. 예외를 두려면 그 컨트롤에 `Style="{x:Null}"`을 주세요. + +## 3. 파일 전문 + + +### `src/UbisamBase.Core/Themes/Colors.Dark.xaml` + +```xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + #FF4DA6F5 + + + + + + + + + + + + + + + + + +``` + +### `src/UbisamBase.Core/Themes/Colors.Light.xaml` + +```xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + #FF2196F3 + + + + + + + + + + + + + + + + + + +``` + +### `src/UbisamBase.Core/Themes/Controls.xaml` + +```xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + M1,6 L5.5,1 L10,6 + M1,1 L5.5,6 L10,1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +``` + +## 4. `src/UbisamBase.Core/Themes/ShellStyles.xaml` — 머지 추가 + +`Controls.xaml`을 머지 목록에 넣고, ComboBox 드롭다운 그림자를 정의하세요. + +```xml + + + + +``` + +```xml + +``` + +**중요 — 암시적 스타일이 안 걸릴 때:** `Controls.xaml`을 `ShellWindow.Resources`에만 머지하면, 리소스 조회 범위 밖의 `UserControl`에는 적용되지 않습니다. 콤보박스가 여전히 WPF 기본 크롬(둥근 모서리 + 밝은 회색)으로 보이면 `App.xaml`의 `Application.Resources`에 같은 머지를 넣으세요. + +## 5. `src/UbisamBase.Core/Shell/ShellWindow.xaml` — 로고 타일 + +로고 타일은 글자를 얹는 채움면이므로 밝은 단계가 아니라 채움 단계를 씁니다. + +```xml + + + +``` + +반대로 `ShellStyles.xaml`의 **좌측 네비 선택 테두리**와 **하단 탭 인디케이터 바**는 글자를 얹지 않는 선이므로 `Accent`(밝은 단계)가 맞습니다 — 그대로 두세요. + +## 6. 상태 태그 쓰는 법 + +```xml + + + + + + + + + +``` + +| 스타일 쌍 | 쓰는 곳 | +| --- | --- | +| `Ubisam.Style.Tag.Accent` + `TagText.Accent` | 정상 · 완료 (양품, Info) | +| `Ubisam.Style.Tag.Outline` + `TagText.Outline` | 주의 · 예외 (재검, Warn) — 외곽선만이라 눈에 걸립니다 | +| `Ubisam.Style.Tag` + `TagText` | 중립 · 대기 | + +## 7. DataGrid 쓰는 법 + +```xml + + + + + + + +``` + +수치 열에는 반드시 `CellStyle="{StaticResource Ubisam.Style.NumericCell}"`을 주세요. + +## 8. 확인 + +1. 다크에서 콤보박스가 **어두운 배경 + 각진 모서리**인지 — 둥근 밝은 회색이면 §4의 머지 문제입니다 +2. 콤보박스를 열면 드롭다운이 입력창 바로 아래에 붙고, 선택 항목이 진한 파랑 + 흰 글자인지 +3. 입력창을 클릭하면 accent 테두리 + 옅은 글로우가 생기는지 +4. 주 버튼("저장")이 진한 파랑(#1E7FD4) + 흰 글자인지 +5. 체크박스 체크 표시가 흰색인지 — 검정이면 파일이 갱신되지 않은 것입니다 +6. 화면 배율 슬라이더의 왼쪽 구간만 파랗게 차고, 썸이 각진 세로 막대인지 +7. 스크롤바가 폭 10의 얇은 막대이고 hover에서 accent로 바뀌는지 +8. 표의 수치 열이 모두 우측 정렬인지 +9. 바탕(#23262B) → 판(#2A2E34) → 입력(#2F343A) 순으로 밝아지는 층이 눈에 보이는지 +10. 라이트 테마가 이전과 동일한지 — 새 키 2개가 추가됐을 뿐입니다 + +## 9. 하지 말 것 + +- 모듈 View의 내용은 건드리지 마세요 — 테마와 스타일만 바꿉니다 +- 라운드 모서리를 되살리지 마세요 (`CornerRadius`는 태그의 3px만 예외) +- `Controls.xaml`의 폰트 참조를 `StaticResource`로 바꾸지 마세요 — 머지 시점 때문에 로드가 실패합니다 +- `TabManager`, `EventBus`, `ToastService`, `AuthService`, `LogService`의 구조나 시그니처를 바꾸지 마세요 diff --git a/.design/FIX-09.md b/.design/FIX-09.md new file mode 100644 index 0000000..6a8d7c4 --- /dev/null +++ b/.design/FIX-09.md @@ -0,0 +1,65 @@ +# 패치 09 — MessageBox를 테마 적용 대화상자로 + +WPF의 `MessageBox`는 OS 크롬이라 스타일을 입힐 수 없습니다. 다크모드에서 혼자 밝은 창이 뜨는 이유입니다. 같은 모양의 커스텀 창으로 대체합니다. + +**`MessageUtil`의 시그니처는 그대로입니다** — `ShowInfo`, `ShowError`, `ShowYesNo`, `ShowSaveSuccessDialog`, `ShowSaveFailedDialog`, Toast 5종 전부 동일합니다. 호출하는 쪽은 한 줄도 고칠 필요가 없습니다. + +## 모양 + +- OS 타이틀바 없음(`WindowStyle="None"`) — 제목줄을 직접 그리고 드래그로 옮길 수 있음 +- 폭 420 고정, 높이는 내용에 맞춰 자동 +- 셸과 같은 3단 구성: 제목줄(판) / 본문(바탕) / 버튼줄(판), 사이는 1px 헤어라인 +- 본문은 아이콘 24px + 메시지, 아이콘은 상단 정렬(메시지가 길어도 위에 고정) +- 버튼은 우측 하단. 확인/예가 주 동작(`Accent.Fill` 채움), 아니오가 보조(외곽선) +- 아이콘: 알림 `circle-info`, 오류 `circle-x`(붉은색), 확인 `circle-help` +- Enter = 확인, Esc = 취소 + +## 1. 새 파일 3개 + +| 이 프로젝트 | 리포지토리 | +| --- | --- | +| `code/UbisamBase.Core/Messaging/MessageDialog.xaml` | `src/UbisamBase.Core/Messaging/MessageDialog.xaml` (신규) | +| `code/UbisamBase.Core/Messaging/MessageDialog.xaml.cs` | `src/UbisamBase.Core/Messaging/MessageDialog.xaml.cs` (신규) | +| `code/UbisamBase.Core/Messaging/MessageUtil.cs` | `src/UbisamBase.Core/Messaging/MessageUtil.cs` (교체) | + +## 2. 갱신 파일 3개 + +| 파일 | 변경 | +| --- | --- | +| `Themes/Icons.xaml` | 아이콘 3종 추가 — `Ubisam.Icon.Info` / `.Question` / `.Error` | +| `Themes/Colors.Light.xaml` | 오류색 토큰 2개 — `Danger` #C93B3B / `Danger.Fill` #B32F2F | +| `Themes/Colors.Dark.xaml` | 오류색 토큰 2개 — `Danger` #F07070 / `Danger.Fill` #B33A3A | + +붉은색은 대화상자 아이콘과 실패 표시에만 씁니다 — 장식용 색을 늘리지 않는 것이 이 시스템의 규칙입니다. + +## 3. 쓰는 법 (변화 없음) + +```csharp +MessageUtil.ShowInfo("저장", "저장되었습니다."); +MessageUtil.ShowError("저장 실패", "저장하지 못했습니다."); + +if (MessageUtil.ShowYesNo("확인", "저장하지 않은 변경 사항이 있습니다.\n그래도 나가시겠습니까?")) +{ + // 예를 눌렀을 때 +} + +MessageUtil.ShowSavedToast(logger); +``` + +여러 줄 메시지는 `\n`을 그대로 쓰면 됩니다 — `TextWrapping="Wrap"`이라 폭을 넘기면 자동으로도 접힙니다. + +## 4. 확인 + +1. `저장` 버튼을 눌러 대화상자가 뜰 때 OS 타이틀바가 없고 셸과 같은 각진 프레임인지 +2. 다크 테마에서 대화상자도 어두운지 (밝은 창이 뜨면 파일이 교체되지 않은 것) +3. 제목줄을 끌어 창이 움직이는지 +4. Enter로 확인, Esc로 취소가 되는지 +5. 오류 대화상자의 아이콘만 붉은색이고, 버튼은 accent인지 +6. 확인(YesNo) 대화상자에서 "아니오"가 보이고 false를 돌려주는지 +7. 부모 창 가운데에 뜨는지 + +## 주의 + +- `MessageDialog`는 `ShellStyles.xaml`을 자체 `Window.Resources`에 머지합니다 — 별도 창이라 셸의 리소스를 상속받지 못하기 때문입니다. 이 줄을 지우면 스타일이 빠집니다. +- 작업 표시줄에 안 뜨게 `ShowInTaskbar="False"`로 두었습니다. +- 부팅 초기(`MainWindow`가 아직 로드되지 않은 시점)에 호출하면 화면 가운데에 뜹니다. diff --git a/.design/FIX-10.md b/.design/FIX-10.md new file mode 100644 index 0000000..b5595a6 --- /dev/null +++ b/.design/FIX-10.md @@ -0,0 +1,154 @@ +# 패치 10 — LiveCharts2 차트 테마 + +시안 11a(라인 트렌드 · 막대 · 게이지)를 LiveCharts2로 옮기는 코드입니다. +색을 코드에 박지 않고 **현재 테마의 브러시 리소스에서 읽으므로** 라이트/다크 전환을 그대로 따라갑니다. + +## 0. 패키지 + +WPF용 LiveCharts2 하나만 있으면 됩니다 (SkiaSharp는 의존성으로 따라옵니다). + +``` +dotnet add src/UbisamBase.Core package LiveChartsCore.SkiaSharpView.WPF +``` + +`.NET Framework 4.8` 타깃이므로 LiveCharts2가 net48을 지원하는 버전인지 확인하세요. 안 되면 `net8.0-windows`로 올리거나, 차트만 별도 프로젝트로 분리하는 방법이 있습니다 — 어느 쪽인지 알려주시면 그에 맞춰 정리해 드립니다. + +## 1. 새 파일 + +| 이 프로젝트 | 리포지토리 | +| --- | --- | +| `code/UbisamBase.Core/Charting/ChartTheme.cs` | `src/UbisamBase.Core/Charting/ChartTheme.cs` (신규) | + +축·시리즈를 만들어주는 정적 팩토리입니다. 화면마다 색을 다시 지정하지 마세요 — 이 클래스만 쓰면 톤이 유지됩니다. + +## 2. 디자인 규칙 (코드에 반영된 것) + +| 요소 | 규칙 | +| --- | --- | +| 격자 | **가로만** 헤어라인 1px (`Ubisam.Brush.Table.GridLine`). 세로 격자는 그리지 않음 | +| 데이터 선 | accent 밝은 단계, 두께 2, `LineSmoothness = 0` — 값을 매끄럽게 왜곡하지 않음 | +| 면적 | accent 12% (`0x1F` 알파) | +| 점 | 기본은 표시하지 않음 (`GeometrySize = 0`) — 밀도가 낮을 때만 켜세요 | +| 기준·목표선 | 회색 파선 `5 4`, 면적 없음, hover 비활성 | +| 막대 | 깊은 단계 채움 85%, **각진 모서리** (`Rx = Ry = 0`) | +| 축 라벨 | 11pt, `Ubisam.Brush.Muted`, Barlow | +| 값 라벨 | Barlow Condensed, 자릿수 고정 포맷 | +| 애니메이션 | 260ms — 탭 인디케이터와 같은 속도 | + +색을 늘리지 않고 굵기와 불투명도로 구분하는 것이 이 시스템의 규칙입니다. 시리즈가 여러 개 필요하면 알려주세요 — 색을 더 만들지 않고 구분하는 방법을 따로 정리하겠습니다. + +## 3. 라인 트렌드 + +```xml + +``` + +```csharp +using LiveChartsCore; +using LiveChartsCore.SkiaSharpView; +using UbisamBase.Core.Charting; + +public ISeries[] TrendSeries { get; } +public Axis[] TrendXAxes { get; } +public Axis[] TrendYAxes { get; } + +public MonitorViewModel() +{ + var measured = new double[] { 121, 123, 119, 128, 126, 132, 130, 134 }; + var baseline = Enumerable.Repeat(125d, measured.Length).ToArray(); + + TrendSeries = new ISeries[] + { + ChartTheme.ReferenceLine(baseline, "기준"), + ChartTheme.Line(measured, "측정") + }; + + TrendXAxes = new[] { ChartTheme.XAxis(labeler: v => $"-{(7 - v) * 15:0}s") }; + TrendYAxes = new[] { ChartTheme.YAxis(format: "0", min: 100, max: 140) }; +} +``` + +기준선을 먼저 넣어야 측정선이 위에 그려집니다. + +## 4. 막대 + +```csharp +ProductionSeries = new ISeries[] { ChartTheme.Column(counts, "생산") }; +ProductionXAxes = new[] { ChartTheme.XAxis(labeler: v => $"{8 + (int)v:00}") }; +ProductionYAxes = new[] { ChartTheme.YAxis(format: "0", min: 0) }; +``` + +시안처럼 최고값 막대만 진하게 하려면 `ColumnSeries`의 `Fill`을 하나로 두는 대신 값별 페인트를 주는 방식이 필요합니다 — 필요하시면 따로 만들어 드립니다. + +## 5. 게이지 + +```xml + +``` + +```csharp +UtilizationSeries = ChartTheme.Gauge(82.4); +``` + +**목표선(파선)은 LiveCharts2가 제공하지 않습니다.** 시안의 75% 틱은 차트 위에 `Path`를 하나 얹어서 그려야 합니다. 반지름 r, 중심 (cx, cy), 목표 p(0~1)일 때 끝점은 + +``` +x = cx + r · cos(180° − p · 180°) → p=0.75 이면 cx + r·cos(45°) +y = cy − r · sin(180° − p · 180°) +``` + +시안 값(cx=95, cy=100, r=77, p=0.75)으로는 (149.4, 45.6)입니다. 필요하시면 이 계산을 하는 작은 UserControl로 만들어 드립니다. + +## 6. 테마 전환 대응 + +`ChartTheme`은 호출 시점의 브러시를 읽습니다. 테마를 바꾼 뒤 이미 만들어진 차트는 색이 그대로이므로, `UiSettingsService`가 테마를 교체한 직후 시리즈를 다시 만들어야 합니다. + +```csharp +// 테마 적용 코드 끝에 +ChartTheme.NotifyThemeChanged(); +``` + +차트를 가진 ViewModel에서 이 이벤트를 구독해 `TrendSeries` 등을 다시 만들고 `OnPropertyChanged`를 올리세요. + +## 7. 툴팁·범례 + +LiveCharts2 기본 툴팁은 밝은 판이라 다크에서 튑니다. 차트마다 지정하세요. + +```xml + +``` + +```csharp +public IPaint TooltipBackground => new SolidColorPaint(ChartTheme.Surface); +public IPaint TooltipText => new SolidColorPaint(ChartTheme.Text); +public IPaint LegendText => new SolidColorPaint(ChartTheme.Muted); +``` + +시안처럼 범례를 직접 그리는 편이 더 깔끔합니다 — 차트 위에 선 샘플 + 라벨을 `StackPanel`로 두면 됩니다. + +## 8. 확인 + +1. 라이트에서 격자가 가로만, 헤어라인으로 보이는지 +2. 다크로 바꾸고 차트를 다시 만들었을 때 선이 #4DA6F5, 격자가 #383D44인지 +3. 막대 모서리가 각진지 (둥글면 `Rx`/`Ry`가 안 먹은 것) +4. 측정선이 기준 파선 위에 그려지는지 +5. 게이지 숫자가 Barlow Condensed로 크게 나오는지 +6. 툴팁 배경이 테마에 맞는지 + +## 주의 + +- `LineSmoothness = 0`을 유지하세요. 곡선 보간은 측정값을 실제와 다르게 보이게 합니다. +- Barlow가 대상 PC에 없으면 SkiaSharp도 기본 서체로 폴백합니다 — 폰트 임베드를 하시려면 알려주세요. +- LiveCharts2는 버전에 따라 속성명이 조금씩 다릅니다(`IPaint`/`Paint`, `TicksPaint` 등). 빌드 오류가 나면 해당 줄과 패키지 버전을 알려주시면 맞춰 드립니다. diff --git a/.design/FIX-11.md b/.design/FIX-11.md new file mode 100644 index 0000000..2dfa9b7 --- /dev/null +++ b/.design/FIX-11.md @@ -0,0 +1,63 @@ +# 패치 11 — 토스트 메시지 (Success · Fail · Error) + +기존 토스트는 판 전체를 상태색으로 채우고 흰 글자를 얹는 방식이라, 셸의 다른 표면과 재질이 달랐고 다크에서 특히 튀었습니다. **판은 셸과 같은 재질로 두고 색은 좌측 3px 바와 아이콘에만** 쓰도록 바꿉니다. + +`ToastType` enum(`Info` · `Success` · `Warning` · `Error`)은 그대로입니다. 요청하신 3종은 이렇게 매핑됩니다. + +| 요청 | enum | 의미 | 라이트 | 다크 | 아이콘 | +| --- | --- | --- | --- | --- | --- | +| **Success** | `ToastType.Success` | 완료 | `#2E8B4F` | `#5FC97E` | `circle-check` | +| **Fail** | `ToastType.Warning` | 동작이 되지 않음 (예외는 아님) | `#B8791A` | `#E0A63C` | `triangle-alert` | +| **Error** | `ToastType.Error` | 예외 · 시스템 오류 | `#C93B3B` | `#F07070` | `circle-x` | + +`Info`는 accent 색 + `circle-info`로 남습니다 — 기존 `ShowToast` 호출이 그대로 동작합니다. + +종류마다 **색과 도형을 둘 다** 다르게 했습니다. 색만으로 구분하면 색약 사용자가 성공과 실패를 구별할 수 없습니다. + +## 1. 갱신 파일 + +| 파일 | 변경 | +| --- | --- | +| `src/UbisamBase.Core/Converters/ToastTypeToBrushConverter.cs` | 하드코딩된 색 → 테마 브러시 리소스 조회 | +| `src/UbisamBase.Core/Converters/ToastTypeToGeometryConverter.cs` | **신규** — 종류별 아이콘 | +| `src/UbisamBase.Core/Themes/ShellStyles.xaml` | 새 컨버터 등록 (`ToastTypeToGeometry`) | +| `src/UbisamBase.Core/Shell/ShellWindow.xaml` | 토스트 `ItemTemplate` 교체 | +| `src/UbisamBase.Core/Themes/Colors.Light.xaml` | `Success` `#2E8B4F` / `Warning` `#B8791A` 추가 | +| `src/UbisamBase.Core/Themes/Colors.Dark.xaml` | `Success` `#5FC97E` / `Warning` `#E0A63C` 추가 | + +`code/` 아래 같은 경로의 파일을 그대로 덮어쓰세요. `ToastService.cs`와 `ToastMessage.cs`는 손대지 않습니다. + +## 2. 쓰는 법 (변화 없음) + +```csharp +MessageUtil.ShowSavedToast(logger); // Success +MessageUtil.ShowSuccessToast("검사가 완료되었습니다.", logger); // Success +MessageUtil.ShowWarningToast("값이 허용 범위를 벗어났습니다.", logger); // Fail +MessageUtil.ShowErrorToast("장비 통신이 끊어졌습니다.", logger); // Error +MessageUtil.ShowToast("레시피를 불러왔습니다.", logger); // Info +``` + +표시 시간은 `ToastService.Show(text, type, durationMs)`의 기본 3000ms입니다. 오류는 더 길게 두는 편이 낫습니다 — 필요하시면 종류별 기본값을 다르게 만들어 드립니다. + +## 3. 모양 + +- 판 배경은 `Sidebar.Background`(라이트 #FFFFFF / 다크 #2A2E34), 1px 헤어라인, 각진 모서리 +- 좌측 3px 상태색 바 — 색이 들어가는 유일한 면 +- 아이콘 19px, 상태색 스트로크 1.5, 상단 정렬(메시지가 길어도 위에 고정) +- 글자는 본문색 13px/19 — 상태색으로 쓰지 않습니다(가독성) +- 최소 폭 280 / 최대 380, 그림자는 `Ubisam.Effect.Popup` +- 위치는 기존과 같이 우상단, 아래로 8px 간격 쌓임 + +## 4. 확인 + +1. 세 종류의 좌측 바 색과 아이콘 도형이 서로 다른지 +2. 다크에서 판이 어두운 회색이고 글자가 흰색인지 (색으로 채워진 판이 뜨면 파일이 안 바뀐 것) +3. 여러 개가 동시에 뜰 때 아래로 쌓이는지 +4. 3초 후 사라지는지 +5. 긴 메시지가 380px에서 줄바꿈되고 아이콘은 위에 고정되는지 +6. 테마를 바꾼 뒤 새로 뜬 토스트의 색이 그 테마를 따르는지 + +## 주의 + +- 컨버터가 색을 **호출 시점에** 읽으므로, 테마를 바꿔도 이미 떠 있는 토스트는 이전 색입니다. 3초 후 사라지니 실질적인 문제는 없습니다. +- 판 전체를 상태색으로 채우는 이전 방식을 원하시면 알려주세요 — 그 경우 흰 글자 대비를 위해 색을 한 단계 어둡게 조정해야 합니다. diff --git a/.design/FIX-12.md b/.design/FIX-12.md new file mode 100644 index 0000000..f8e0f84 --- /dev/null +++ b/.design/FIX-12.md @@ -0,0 +1,112 @@ +# 패치 12 — 화면 잠금 + +화면 전체를 불투명하게 가리고, 아이디 + 비밀번호로만 해제하는 오버레이입니다. 잠금 중에는 밑 화면이 보이지 않고 마우스·키보드도 통과하지 않습니다. + +## 1. 새 파일 3개 + +| 이 프로젝트 | 리포지토리 | +| --- | --- | +| `code/UbisamBase.Core/Locking/LockService.cs` | `src/UbisamBase.Core/Locking/LockService.cs` | +| `code/UbisamBase.Core/Locking/LockOverlay.xaml` | `src/UbisamBase.Core/Locking/LockOverlay.xaml` | +| `code/UbisamBase.Core/Locking/LockOverlay.xaml.cs` | `src/UbisamBase.Core/Locking/LockOverlay.xaml.cs` | +| `code/UbisamBase.Core/Locking/LockCredentialStore.cs` | `src/UbisamBase.Core/Locking/LockCredentialStore.cs` | +| `code/UbisamBase.Core/Locking/LockSettingsView.xaml` | `src/UbisamBase.Core/Locking/LockSettingsView.xaml` | +| `code/UbisamBase.Core/Locking/LockSettingsView.xaml.cs` | `src/UbisamBase.Core/Locking/LockSettingsView.xaml.cs` | + +## 2. 색 토큰 1개 추가 + +`Colors.Light.xaml` / `Colors.Dark.xaml`에 실패 안내판 배경을 추가했습니다 (`code/`의 두 파일에 이미 반영). + +| 키 | 라이트 | 다크 | +| --- | --- | --- | +| `Ubisam.Brush.Danger.Tint` | `#12C93B3B` | `#1AF07070` | + +## 3. `ShellWindow.xaml` — 오버레이 얹기 + +`Grid`의 **마지막 자식**으로 넣으세요. 토스트보다 뒤에 와야 잠금 화면이 토스트를 덮습니다. + +```xml +xmlns:locking="clr-namespace:UbisamBase.Core.Locking" +``` + +```xml + + + + +``` + +`Visibility`가 `Collapsed`면 히트 테스트도 되지 않으므로, 잠금이 아닐 때는 아래 UI가 정상 동작합니다. + +## 4. 하단 탭에 잠금 버튼 + +`NavActionItem`을 그대로 쓰면 됩니다 — "바탕화면"과 같은 방식입니다. + +```csharp +new NavActionItem("화면 잠금", Icon.Lock, () => LockService.Current.Lock()); +``` + +`Icon` enum에 `Lock`이 없으면 추가하세요. `Themes/Icons.xaml`에는 `Ubisam.Icon.Lock`이 이미 있습니다. + +## 5. 잠금 설정 화면 — 설정 탭에 등록 + +아이디·비밀번호를 바꾸는 화면입니다. 설정 탭의 서브탭으로 넣으세요. + +```csharp +manager.AddSub(ViewIds.Settings, "화면 잠금"); +``` + +ViewModel이 없는 화면입니다(코드비하인드가 `LockCredentialStore`를 직접 다룹니다). 등록 헬퍼가 ViewModel 타입을 요구하면 빈 클래스를 하나 넘기거나, ViewModel 없이 등록하는 오버로드를 쓰세요. + +**저장 방식** — `LockCredentialStore`가 처리합니다. + +- 비밀번호는 평문으로 두지 않습니다: **PBKDF2 (SHA-256, 10만 회)** 해시 + 16바이트 salt +- 파일 위치: `%ProgramData%\UbisamBase\lock.json` — 사용자 계정이 아니라 장비 단위 설정이므로 +- 비교는 고정 시간 비교(`FixedTimeEquals`)로 — 응답 시간으로 비밀번호를 추측할 수 없게 +- 파일이 없거나 깨졌으면 기본값(`operator` / `0000`)으로 동작하고, 설정 화면 상단에 **경고 배너**가 뜹니다 +- 변경에는 **현재 비밀번호 확인**이 필요합니다 + +`LockService.Validator`의 기본값이 이 저장소를 가리키므로 별도 배선은 필요 없습니다. 나중에 실제 계정 서비스가 생기면 델리게이트만 바꾸세요. + +```csharp +LockService.Current.Validator = (id, password) => accountService.Verify(id, password); +``` + +### 설정 화면 검증 + +| 조건 | 결과 | +| --- | --- | +| 새 비밀번호 4자 미만 | Fail 토스트 | +| 새 비밀번호 ≠ 확인 | 확인 칸 아래 붉은 안내 (입력 중 실시간) | +| 허용 횟수가 1 미만 또는 숫자 아님 | Fail 토스트 | +| 현재 비밀번호 불일치 | Error 토스트, 현재 비밀번호 칸 초기화 | +| 성공 | Success 토스트, 입력칸 초기화 | + +## 6. 동작 + +- 잠금이 걸리면 아이디 칸에 자동으로 커서가 갑니다 +- Enter = 잠금 해제 (`IsDefault`) +- **Esc로는 빠져나갈 수 없습니다** — 잠금이므로 의도한 동작입니다 +- 틀리면 비밀번호만 지우고 남은 시도 횟수를 안내합니다 +- 3회(`MaxAttempts`) 초과하면 입력과 버튼이 비활성화됩니다. `LockService.Current.ResetAttempts()`로 관리자가 풀어야 합니다 + +## 7. 확인 + +1. 하단 탭의 잠금 버튼을 누르면 화면 전체가 가려지는지 — 상단바·탭·컨텐츠가 전혀 보이지 않아야 합니다 +2. 잠금 중에 탭을 클릭해도 반응이 없는지 +3. 아이디 칸에 바로 타이핑이 되는지 +4. 틀린 비밀번호로 남은 횟수 안내가 뜨는지, 3회 후 비활성화되는지 +5. Esc를 눌러도 풀리지 않는지 +6. 다크 테마에서도 판이 어둡고 자물쇠가 accent인지 +7. 잠금 중에 토스트가 위로 뜨지 않는지 (오버레이가 덮어야 함) +8. 설정 → 화면 잠금에서 비밀번호를 바꾸고, 새 비밀번호로 해제되는지 +9. 처음 실행 시 설정 화면에 "기본 비밀번호 사용 중" 경고가 뜨는지 +10. `%ProgramData%\UbisamBase\lock.json`에 평문 비밀번호가 없는지 (salt·hash만 있어야 합니다) + +## 주의 + +- `ShellWindow`가 `WindowState="Maximized"` + `WindowStyle="None"`이라 오버레이가 실제로 화면 전체를 덮습니다. 창 모드로 바꾸면 창 안쪽만 가려집니다 — OS 레벨 잠금이 필요하면 별도 작업입니다. +- 잠금 중에도 `ToastService`는 계속 메시지를 큐에 넣습니다(가려서 안 보일 뿐). 잠금 중 알림을 오버레이에 보여주려면 알려주세요. +- `PasswordBox`는 `Controls.xaml`의 암시적 TextBox 스타일을 받지 못합니다(다른 타입이라서). 두 화면에서 같은 모양을 각자 정의해 두었습니다 — 입력칸 모양을 바꾸실 때는 세 곳(`Controls.xaml`, `LockOverlay.xaml`, `LockSettingsView.xaml`)을 함께 고쳐야 합니다. +- `Alt+Tab`이나 `Ctrl+Alt+Del`은 이 오버레이로 막을 수 없습니다. 그 수준의 잠금이 필요하면 키보드 훅이나 셸 대체가 필요합니다 — 요구사항이면 말씀해 주세요. diff --git a/.design/FIX-14.md b/.design/FIX-14.md new file mode 100644 index 0000000..3b418b1 --- /dev/null +++ b/.design/FIX-14.md @@ -0,0 +1,103 @@ +# 패치 14 — 차트가 흰 판 위에 그려지는 문제 + +적용 화면을 보니 차트 컨트롤 자체가 흰 배경으로 렌더되고 있습니다. 여기서 세 가지 증상이 한꺼번에 나옵니다. + +| 증상 | 원인 | +| --- | --- | +| 다크 테마인데 차트 판만 흰색 | `CartesianChart` / `PieChart`의 기본 배경이 흰색이고, 테마 브러시를 상속하지 않음 | +| 격자선·게이지 남은 구간이 검게 보임 | 다크용 토큰(`#383D44`)을 흰 판에 그려서 — 색은 맞지만 판이 틀림 | +| 게이지 가운데 숫자(82.4)가 안 보임 | 값 라벨이 다크용 본문색(`#E8EAED`)인데 판이 흰색이라 묻힘 | + +`ChartTheme`은 축·시리즈만 칠합니다. 컨트롤의 배경은 XAML에서 지정해야 합니다. + +## 1. 차트에 배경을 지정하세요 + +```xml + +``` + +`Background="Transparent"`가 핵심입니다. `PieChart`(게이지)도 같습니다. + +## 2. 판은 셸의 도면 프레임으로 감싸세요 + +시안(11a)의 차트 패널은 흰 채움이 아니라 **선 드로잉**입니다 — 1px 헤어라인 + 네 모서리 등록 마크, 배경은 컨텐츠 바탕색 그대로. + +```xml + + + + + + +``` + +등록 마크까지 넣으려면 `Ubisam.Style.Tag`처럼 스타일로 빼는 게 편합니다 — 필요하시면 `Ubisam.Style.BlueprintPanel`로 만들어 드립니다. + +## 3. 테마를 바꾼 뒤에는 시리즈를 다시 만들어야 합니다 + +막대 색이 진한 파랑(`#1E7FD4`)이 아니라 밝은 파랑으로 보이는 것은, 차트가 만들어진 시점의 브러시를 그대로 들고 있기 때문입니다. `ChartTheme`은 **호출 시점**에 리소스를 읽습니다. + +테마 적용 코드 끝에: + +```csharp +ChartTheme.NotifyThemeChanged(); +``` + +차트를 가진 ViewModel에서 구독해 다시 만드세요: + +```csharp +public MonitorViewModel() +{ + Rebuild(); + ChartTheme.ThemeChanged += Rebuild; +} + +private void Rebuild() +{ + TrendSeries = new ISeries[] { ChartTheme.ReferenceLine(baseline), ChartTheme.Line(measured) }; + TrendXAxes = new[] { ChartTheme.XAxis() }; + TrendYAxes = new[] { ChartTheme.YAxis(format: "0", min: 100, max: 140) }; + OnPropertyChanged(nameof(TrendSeries)); + OnPropertyChanged(nameof(TrendXAxes)); + OnPropertyChanged(nameof(TrendYAxes)); +} +``` + +`ThemeChanged -= Rebuild`를 뷰가 사라질 때 해제하는 것을 잊지 마세요. + +## 4. 툴팁·범례도 판을 따라야 합니다 + +LiveCharts2 기본 툴팁은 밝은 판입니다. + +```xml + +``` + +## 5. 게이지에서 확인할 것 + +- 판이 어두워지면 남은 구간(`GridLine`)이 검정이 아니라 회색으로 읽힙니다 +- 가운데 숫자가 나타납니다. 안 보이면 `DataLabelsPaint`가 `Text` 토큰인지 확인하세요 +- 목표선(파선)은 LiveCharts2가 그려주지 않습니다 — 시안의 75% 틱은 차트 위에 `Path`를 얹어야 합니다. 계산식은 `FIX-10.md` §5에 있습니다 + +## 6. 확인 + +1. 다크 테마에서 차트 판이 배경과 같은 어두운 색인지 +2. 격자선이 검은 실선이 아니라 헤어라인으로 보이는지 +3. 막대가 진한 파랑(`#1E7FD4`)인지 +4. 게이지 가운데 82.4가 보이는지 +5. 라이트 테마로 바꾸고 화면을 다시 열었을 때 색이 라이트 토큰으로 바뀌는지 +6. 차트 패널 테두리가 셸의 다른 판과 같은 1px 헤어라인인지 diff --git a/.design/code/APPLY.md b/.design/code/APPLY.md new file mode 100644 index 0000000..8b528f3 --- /dev/null +++ b/.design/code/APPLY.md @@ -0,0 +1,75 @@ +# 2a 시안 적용 — 변경 파일과 수동 편집 2곳 + +`Ubisam Base Platform.dc.html`의 **2a**(도면 콘솔 · 하단 5분할 탭 · 여백 없는 메인)를 실제 셸에 반영한 파일들입니다. +`code/` 아래 경로는 리포지토리의 `src/` 아래 경로와 1:1 대응합니다. 그대로 덮어쓰면 됩니다. + +| 이 프로젝트의 파일 | 리포지토리 경로 | +| --- | --- | +| `code/UbisamBase.Core/Themes/Colors.Light.xaml` | `src/UbisamBase.Core/Themes/Colors.Light.xaml` | +| `code/UbisamBase.Core/Themes/Colors.Dark.xaml` | `src/UbisamBase.Core/Themes/Colors.Dark.xaml` | +| `code/UbisamBase.Core/Themes/ShellStyles.xaml` | `src/UbisamBase.Core/Themes/ShellStyles.xaml` | +| `code/UbisamBase.Core/Themes/Icons.xaml` | `src/UbisamBase.Core/Themes/Icons.xaml` (신규) | +| `code/UbisamBase.Core/Converters/IconKeyToGeometryConverter.cs` | `src/UbisamBase.Core/Converters/IconKeyToGeometryConverter.cs` (신규) | +| `code/UbisamBase.Core/Shell/ShellWindow.xaml` | `src/UbisamBase.Core/Shell/ShellWindow.xaml` | +| `code/UbisamBase.Core/Shell/ShellViewModel.cs` | `src/UbisamBase.Core/Shell/ShellViewModel.cs` | + +## 무엇이 바뀌었나 + +**Colors.Light.xaml** — 바탕을 흰색(#FFFFFF)에서 도면 톤 밝은 회색(#F2F2F3)으로, 상단바·하단 탭 바를 흰 판으로 분리했습니다. accent는 회사 하늘색 #2196F3 그대로. 하늘색 틴트 배경(#EAF4FD 계열)은 선택 상태에만 남기고 판 전체에서는 뺐습니다. + +**Colors.Dark.xaml** — 기존 값 유지, 라이트의 반전 관계만 정리(#121317 바탕 / #16171C 판 / #2A2C33 경계 / #22D3EE accent). + +**ShellStyles.xaml** +- `SidebarBar.Horizontal`의 ItemsPanel을 `StackPanel` → `UniformGrid Rows="1"`로 바꿔 하단 탭이 **등록된 탭 수만큼 균등 분할**됩니다. +- 하단 탭 아이템은 아이콘+라벨 가로 배치 → **라벨만, 중앙 정렬, 선택 시 상단 3px accent 바**. 칸 사이 세로 헤어라인 추가. +- 서브탭의 알약(CornerRadius 20) → **각진 세그먼트**, ChromeButton도 CornerRadius 0. +- `Ubisam.Font.Heading` / `Ubisam.Font.Body` 폰트 리소스 추가 (Barlow → 없으면 Segoe UI 폴백). +- 빈 슬롯 표시용 `Ubisam.Brush.SlotHatch` DrawingBrush 추가. + +**ShellWindow.xaml** +- 좌상단: 로고 사각형 + 회사명(`Brand`) + 구분선 + **장비명**(`Title`). 로고 이미지가 준비되면 해당 `Border`를 ``로 교체하세요. +- 우상단: 날짜(작게) + **시계 25px**. +- `NavList`의 기본 Dock을 `Left` → `Bottom`, Style을 Horizontal, Height 56. +- 컨텐츠 `Margin="32"` → **여백 제거**. 내부 여백은 각 모듈 View가 정합니다. +- 컨텐츠 영역에 제목줄은 두지 않습니다 — 선택된 하단 탭과 중복이라 뺐습니다. + +**ShellViewModel.cs** — `Brand`(회사명, 기본 "UBISAM")와 `CurrentDate` 추가. 시계 타이머가 날짜도 같이 갱신합니다. + +**Icons.xaml (신규)** — 탭 아이콘 24종을 24×24 그리드 / 스트로크 1.5 규칙으로 `Geometry` 리소스로 등록했습니다(`Ubisam.Icon.Home` … `Ubisam.Icon.Modules`). 아이콘 폰트 글리프 대신 벡터라 배율(ChromeScale)에 깨지지 않습니다. + +**IconKeyToGeometryConverter.cs (신규)** — `AddMain`의 `icon` 인자를 글리프 코드가 아니라 **아이콘 이름**으로 받게 해줍니다. 등록은 이렇게: + +```csharp +manager.AddMain(ViewIds.Home, "홈", "Home"); +manager.AddMain(ViewIds.Monitor, "모니터링·그래프", "Monitor"); +manager.AddMain(ViewIds.Report, "통계·리포트", "Chart"); +manager.AddMain(ViewIds.Maintenance, "유지보수", "Maintenance"); +manager.AddMain(ViewIds.Settings, "설정", "Settings"); +``` + +`icon`을 생략하면 그 탭은 라벨만 표시됩니다. 쓸 수 있는 이름: Home, Monitor, Chart, Maintenance, Settings, Alarm, Log, Document, Data, Power, Temperature, Gauge, Run, Stop, Reset, Account, Lock, Export, Import, Search, Calendar, Clock, Link, Warning, Pass, Folder, Vision, Control, Recipe, Modules. + +아이콘을 더 늘릴 때는 Lucide(https://lucide.dev)에서 같은 24px/1.5 규칙으로 가져와 `Icons.xaml`에 `Geometry` 한 줄 추가하면 끝입니다. `Icons.xaml`은 `.csproj`에 `Page`로 포함되어야 합니다(다른 Themes/*.xaml과 동일 처리). + +## 직접 고쳐야 하는 2곳 + +1. `src/UbisamBase.Core/Shell/ShellWindow.xaml.cs` — `ApplyNavPosition` 마지막 줄의 하단 바 높이를 100 → 72로(아이콘+라벨 두 줄): + +```csharp +NavList.Width = isHorizontal ? double.NaN : 140; +NavList.Height = isHorizontal ? 72 : double.NaN; +``` + +2. `src/UbisamBase.Core/Settings/UiSettingsService.cs` — 기본 네비게이션 위치를 하단으로: + +```csharp +[ObservableProperty] +private NavPosition navPosition = NavPosition.Bottom; +``` + +## 확인 안 된 것 + +- Barlow / Barlow Condensed가 대상 PC에 설치돼 있는지 — 없으면 Segoe UI로 폴백되어 도면 느낌이 약해집니다. 설치하거나 프로젝트에 폰트를 임베드할지 알려주세요. +- 회사 로고 이미지 파일 — 아직 없어서 accent 사각형 + 첫 글자로 두었습니다. +- 빌드는 이 환경에서 돌려보지 못했습니다(WPF/.NET Framework 4.8). 빌드 후 하단 탭 정렬과 다크 전환만 눈으로 확인해 주세요. +- 아이콘 Geometry는 SVG 경로를 WPF 문법으로 옮긴 것입니다. 원·타원은 호(A) 두 개로 근사했으니, 렌더링 후 Data/Power/Account 아이콘의 곡선만 확인해 주세요. diff --git a/.design/code/CLAUDE_CODE_PROMPT.md b/.design/code/CLAUDE_CODE_PROMPT.md new file mode 100644 index 0000000..8b9074b --- /dev/null +++ b/.design/code/CLAUDE_CODE_PROMPT.md @@ -0,0 +1,732 @@ +# UbisamBase 셸 UI 개편 지시서 + +이 문서 전체를 Claude Code에 붙여넣고 "이 지시서대로 적용해줘"라고 요청하세요. + +--- + +당신은 `UbisamBase` WPF 솔루션(.NET Framework 4.8, MVVM + CommunityToolkit.Mvvm)에서 작업합니다. +플랫폼 셸의 화면 디자인을 아래대로 교체하세요. **아래 §2의 파일 내용은 전문이 주어져 있으니 그대로 덮어쓰거나 새로 만드세요** — 임의로 재해석하거나 값을 바꾸지 마세요. + +## 1. 무엇을 바꾸는가 + +- **좌상단**: 회사 로고(accent 사각형) + 회사명 `Brand` + 세로 구분선 + 장비명 `Title`(= `IAppSetup.GetAppName()`) +- **우상단**: 작은 날짜 + 큰 시계(25px). 기존 시계 타이머가 날짜도 함께 갱신 +- **하단 탭**: 좌측 사이드바 대신 **하단 네비게이션이 기본**. 등록된 메인 탭 수만큼 `UniformGrid Rows="1"`로 균등 분할, 각 칸은 아이콘(위) + 라벨(아래), 선택 시 상단 3px accent 바 + 옅은 accent 판 +- **메인 컨텐츠**: 기존 `Margin="32"` 제거 — 영역 전체를 모듈 View가 사용. 셸은 컨텐츠 제목줄을 그리지 않음(선택된 하단 탭과 중복). 모듈이 없을 때는 사선 해치 브러시로 빈 슬롯 표시 +- **아이콘**: Segoe Fluent 글리프 대신 **벡터 Geometry 24종**. `AddMain`의 `icon` 인자에 글리프가 아니라 아이콘 이름("Home", "Monitor" 등)을 넘김. 생략하면 라벨만 표시 +- **모양 규칙**: 라운드 제거(서브탭 알약 → 각진 세그먼트, ChromeButton CornerRadius 0), 헤어라인 경계 +- **색**: 라이트 = 바탕 #F2F2F3 / 판 #FFFFFF / accent #2196F3. 다크 = 바탕 #121317 / 판 #16171C / 경계 #2A2C33 / accent #22D3EE +- **폰트**: `Ubisam.Font.Heading` = "Barlow Condensed, Segoe UI", `Ubisam.Font.Body` = "Barlow, Segoe UI" (Barlow 미설치 시 Segoe UI 폴백) + +## 2. 파일 전문 + + +### `src/UbisamBase.Core/Themes/Colors.Light.xaml` + +```xml + + + + + + + + + + + + + + + + + + + + + + + + + + +``` + +### `src/UbisamBase.Core/Themes/Colors.Dark.xaml` + +```xml + + + + + + + + + + + + + + + + + + + + + + + + +``` + +### `src/UbisamBase.Core/Themes/Icons.xaml` + +```xml + + + + + M3,10.5 L12,3 L21,10.5 L21,20 A1,1 0 0 1 20,21 L4,21 A1,1 0 0 1 3,20 Z M9.5,21 L9.5,14 L14.5,14 L14.5,21 + M22,12 L18,12 L15,20 L11,4 L8,12 L2,12 + M3,3 L3,21 L21,21 M7.5,17.5 L7.5,12.5 M12,17.5 L12,8 M16.5,17.5 L16.5,9.5 + M15,4 L17,4 A1,1 0 0 1 18,5 L18,20 A1,1 0 0 1 17,21 L7,21 A1,1 0 0 1 6,20 L6,5 A1,1 0 0 1 7,4 L9,4 M9,2.5 L15,2.5 L15,6.5 L9,6.5 Z M9.5,14 L11.5,16 L15.5,11.5 + M3,7 L21,7 M3,12 L21,12 M3,17 L21,17 M13.3,7 A2.2,2.2 0 0 1 17.7,7 A2.2,2.2 0 0 1 13.3,7 Z M6.8,12 A2.2,2.2 0 0 1 11.2,12 A2.2,2.2 0 0 1 6.8,12 Z M14.8,17 A2.2,2.2 0 0 1 19.2,17 A2.2,2.2 0 0 1 14.8,17 Z + + + M18,16.5 L18,11 A6,6 0 0 0 6,11 L6,16.5 L4.5,19.5 L19.5,19.5 Z M10,22 L14,22 + M8,6 L20,6 M8,12 L20,12 M8,18 L20,18 M3.1,6 A1.1,1.1 0 0 1 5.3,6 A1.1,1.1 0 0 1 3.1,6 Z M3.1,12 A1.1,1.1 0 0 1 5.3,12 A1.1,1.1 0 0 1 3.1,12 Z M3.1,18 A1.1,1.1 0 0 1 5.3,18 A1.1,1.1 0 0 1 3.1,18 Z + M14,3 L7,3 A1,1 0 0 0 6,4 L6,20 A1,1 0 0 0 7,21 L17,21 A1,1 0 0 0 18,20 L18,7 Z M14,3 L14,7 L18,7 M9,12.5 L15,12.5 M9,16.5 L15,16.5 + M4,6 A8,3 0 0 1 20,6 A8,3 0 0 1 4,6 Z M4,6 L4,18 C4,19.7 7.6,21 12,21 C16.4,21 20,19.7 20,18 L20,6 M4,12 C4,13.7 7.6,15 12,15 C16.4,15 20,13.7 20,12 + M12,3 L12,12 M6.8,7.3 A8,8 0 1 0 17.2,7.3 + M14,14 L14,5 A2,2 0 1 0 10,5 L10,14 A4,4 0 1 0 14,14 Z + M4,18 A9,9 0 1 1 20,18 M12,14 L16,10 + M8,5 L19,12 L8,19 Z + M6.5,6.5 L17.5,6.5 L17.5,17.5 L6.5,17.5 Z + M20,12 A8,8 0 1 1 16.8,5.6 M20,4.5 L20,9.5 L15,9.5 + M8,8 A4,4 0 0 1 16,8 A4,4 0 0 1 8,8 Z M4.5,21 A7.5,7.5 0 0 1 19.5,21 + M4.5,11 L19.5,11 L19.5,21 L4.5,21 Z M8,11 L8,7.5 A4,4 0 0 1 16,7.5 L16,11 + M12,3.5 L12,14.5 M7,10 L12,14.5 L17,10 M4,20.5 L20,20.5 + M12,14.5 L12,3.5 M7,8 L12,3.5 L17,8 M4,20.5 L20,20.5 + M4,11 A7,7 0 0 1 18,11 A7,7 0 0 1 4,11 Z M16.2,16.2 L20.5,20.5 + M3.5,5.5 L20.5,5.5 L20.5,20.5 L3.5,20.5 Z M3.5,10.5 L20.5,10.5 M8,3 L8,7 M16,3 L16,7 + M3,12 A9,9 0 0 1 21,12 A9,9 0 0 1 3,12 Z M12,7 L12,12.5 L15.8,14.5 + M9.5,14.5 L14.5,9.5 M13.5,6.5 L14.5,5.5 A4.6,4.6 0 0 1 21,12 L20,13 M10.5,17.5 L9.5,18.5 A4.6,4.6 0 0 1 3,12 L4,11 + M12,4 L21,20 L3,20 Z M12,10 L12,14.5 M12,17.4 L12,17.6 + M3,12 A9,9 0 0 1 21,12 A9,9 0 0 1 3,12 Z M8,12.5 L11,15.5 L16,9.5 + M3.5,8 L3.5,6 A1,1 0 0 1 4.5,5 L9,5 L11,8 L19.5,8 A1,1 0 0 1 20.5,9 L20.5,19 A1,1 0 0 1 19.5,20 L4.5,20 A1,1 0 0 1 3.5,19 Z + M3,8 L7,8 L9,5 L15,5 L17,8 L21,8 L21,20 L3,20 Z M8.2,13.5 A3.8,3.8 0 0 1 15.8,13.5 A3.8,3.8 0 0 1 8.2,13.5 Z + M5.5,5.5 L18.5,5.5 L18.5,18.5 L5.5,18.5 Z M9.5,9.5 L14.5,9.5 L14.5,14.5 L9.5,14.5 Z M12,2 L12,5.5 M12,18.5 L12,22 M2,12 L5.5,12 M18.5,12 L22,12 + M12,3.5 L3,8.5 L12,13.5 L21,8.5 Z M3,13.5 L12,18.5 L21,13.5 + M4,4 L11,4 L11,11 L4,11 Z M13,4 L20,4 L20,11 L13,11 Z M4,13 L11,13 L11,20 L4,20 Z M13,13 L20,13 L20,20 L13,20 Z + + +``` + +### `src/UbisamBase.Core/Themes/ShellStyles.xaml` + +```xml + + + + + + + + + + + + + Barlow Condensed, Segoe UI + Barlow, Segoe UI + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +``` + +### `src/UbisamBase.Core/Shell/ShellWindow.xaml` + +```xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +