플랫폼 최초 커밋 + 자동 업데이트 배포 폴더(dist)
소스(Core/Shell/Launcher)와 실행 PC가 받아갈 업데이트 파일(dist)을 함께 올린다. Launcher가 켤 때 이 저장소의 dist와 D:\UbisamPlatform\bin을 비교해 새 버전이면 받아간다. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
44
.claude/agents/ceo-reviewer.md
Normal file
@ -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`)는 시작하지 않는다 — 게이트 규칙은 오케스트레이터가 강제한다.
|
||||
47
.claude/agents/code-reviewer.md
Normal file
@ -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`)는 시작하지 않는다 — 게이트 규칙은 오케스트레이터가 강제한다.
|
||||
49
.claude/agents/eng-planner.md
Normal file
@ -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 단계 전체가 흔들린다.
|
||||
47
.claude/agents/office-hours-facilitator.md
Normal file
@ -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`가 "왜 필요한가"/"성공 기준"만으로 진행/보류를 판단하므로, 이 두 항목이 비어 있으면 반드시 재질문한다.
|
||||
54
.claude/agents/qa-inspector.md
Normal file
@ -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`)는 시작하지 않고 사용자의 수정을 기다린다 — 게이트 규칙은 오케스트레이터가 강제한다.
|
||||
49
.claude/agents/release-manager.md
Normal file
@ -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`의 결론이 "승인"이 아니면 시작하지 않고 오케스트레이터에 알려 중단시킨다.
|
||||
- 사용자가 배포 실행에 동의하지 않으면 "보류"로 남기고 배포 노트만 완성한다.
|
||||
|
||||
## 협업
|
||||
- 이 단계가 파이프라인의 마지막이다. 완료 후 사용자에게 전체 흐름 요약을 보고하도록 오케스트레이터에 결과를 전달한다.
|
||||
98
.claude/skills/feature-pipeline/SKILL.md
Normal file
@ -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`로 임의 진행하지 않음
|
||||
46
.claude/skills/office-hours/SKILL.md
Normal file
@ -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. 다른 항목(범위, 리스크) 질문으로 진행 후 문서 완성
|
||||
43
.claude/skills/plan-ceo-review/SKILL.md
Normal file
@ -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` 먼저 완료할 것을 안내하고 중단
|
||||
43
.claude/skills/plan-eng-review/SKILL.md
Normal file
@ -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. 재검토 필요함을 안내하고 중단
|
||||
45
.claude/skills/qa/SKILL.md
Normal file
@ -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` 호출 → 재검증
|
||||
44
.claude/skills/review/SKILL.md
Normal file
@ -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. 미수정 결함을 사용자에게 전달, 수정 후 재호출 안내
|
||||
44
.claude/skills/ship/SKILL.md
Normal file
@ -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`)가 먼저 필요함을 안내하고 중단
|
||||
1069
.design/FIX-05.md
Normal file
65
.design/FIX-09.md
Normal file
@ -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`가 아직 로드되지 않은 시점)에 호출하면 화면 가운데에 뜹니다.
|
||||
154
.design/FIX-10.md
Normal file
@ -0,0 +1,154 @@
|
||||
# 패치 10 — LiveCharts2 차트 테마
|
||||
|
||||
시안 <a>11a</a>(라인 트렌드 · 막대 · 게이지)를 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
|
||||
<lvc:CartesianChart Series="{Binding TrendSeries}"
|
||||
XAxes="{Binding TrendXAxes}"
|
||||
YAxes="{Binding TrendYAxes}"
|
||||
TooltipPosition="Top"
|
||||
Height="240"/>
|
||||
```
|
||||
|
||||
```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
|
||||
<lvc:PieChart Series="{Binding UtilizationSeries}"
|
||||
InitialRotation="-180"
|
||||
MaxAngle="180"
|
||||
MinValue="0"
|
||||
MaxValue="100"
|
||||
Height="150"/>
|
||||
```
|
||||
|
||||
```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
|
||||
<lvc:CartesianChart TooltipBackgroundPaint="{Binding TooltipBackground}"
|
||||
TooltipTextPaint="{Binding TooltipText}"
|
||||
LegendTextPaint="{Binding LegendText}"
|
||||
LegendPosition="Top"/>
|
||||
```
|
||||
|
||||
```csharp
|
||||
public IPaint<SkiaSharpDrawingContext> TooltipBackground => new SolidColorPaint(ChartTheme.Surface);
|
||||
public IPaint<SkiaSharpDrawingContext> TooltipText => new SolidColorPaint(ChartTheme.Text);
|
||||
public IPaint<SkiaSharpDrawingContext> 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` 등). 빌드 오류가 나면 해당 줄과 패키지 버전을 알려주시면 맞춰 드립니다.
|
||||
63
.design/FIX-11.md
Normal file
@ -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초 후 사라지니 실질적인 문제는 없습니다.
|
||||
- 판 전체를 상태색으로 채우는 이전 방식을 원하시면 알려주세요 — 그 경우 흰 글자 대비를 위해 색을 한 단계 어둡게 조정해야 합니다.
|
||||
112
.design/FIX-12.md
Normal file
@ -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
|
||||
<!-- 화면 잠금. 마지막 자식이라 다른 모든 것을 덮는다. -->
|
||||
<locking:LockOverlay
|
||||
Visibility="{Binding IsLocked, Source={x:Static locking:LockService.Current}, Converter={StaticResource BoolToVis}}"/>
|
||||
</Grid>
|
||||
</Window>
|
||||
```
|
||||
|
||||
`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<LockSettingsView, object>(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`은 이 오버레이로 막을 수 없습니다. 그 수준의 잠금이 필요하면 키보드 훅이나 셸 대체가 필요합니다 — 요구사항이면 말씀해 주세요.
|
||||
103
.design/FIX-14.md
Normal file
@ -0,0 +1,103 @@
|
||||
# 패치 14 — 차트가 흰 판 위에 그려지는 문제
|
||||
|
||||
적용 화면을 보니 차트 컨트롤 자체가 흰 배경으로 렌더되고 있습니다. 여기서 세 가지 증상이 한꺼번에 나옵니다.
|
||||
|
||||
| 증상 | 원인 |
|
||||
| --- | --- |
|
||||
| 다크 테마인데 차트 판만 흰색 | `CartesianChart` / `PieChart`의 기본 배경이 흰색이고, 테마 브러시를 상속하지 않음 |
|
||||
| 격자선·게이지 남은 구간이 검게 보임 | 다크용 토큰(`#383D44`)을 흰 판에 그려서 — 색은 맞지만 판이 틀림 |
|
||||
| 게이지 가운데 숫자(82.4)가 안 보임 | 값 라벨이 다크용 본문색(`#E8EAED`)인데 판이 흰색이라 묻힘 |
|
||||
|
||||
`ChartTheme`은 축·시리즈만 칠합니다. 컨트롤의 배경은 XAML에서 지정해야 합니다.
|
||||
|
||||
## 1. 차트에 배경을 지정하세요
|
||||
|
||||
```xml
|
||||
<lvc:CartesianChart Series="{Binding TrendSeries}"
|
||||
XAxes="{Binding TrendXAxes}"
|
||||
YAxes="{Binding TrendYAxes}"
|
||||
Background="Transparent"
|
||||
Height="240"/>
|
||||
```
|
||||
|
||||
`Background="Transparent"`가 핵심입니다. `PieChart`(게이지)도 같습니다.
|
||||
|
||||
## 2. 판은 셸의 도면 프레임으로 감싸세요
|
||||
|
||||
시안(<a>11a</a>)의 차트 패널은 흰 채움이 아니라 **선 드로잉**입니다 — 1px 헤어라인 + 네 모서리 등록 마크, 배경은 컨텐츠 바탕색 그대로.
|
||||
|
||||
```xml
|
||||
<Border BorderBrush="{DynamicResource Ubisam.Brush.Input.Border}"
|
||||
BorderThickness="1"
|
||||
Background="Transparent"
|
||||
Padding="20,18,20,12">
|
||||
<StackPanel>
|
||||
<TextBlock Text="라인 트렌드"
|
||||
FontFamily="{StaticResource Ubisam.Font.Heading}"
|
||||
FontSize="19" FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource Ubisam.Brush.ContentForeground}"
|
||||
Margin="0,0,0,16"/>
|
||||
<lvc:CartesianChart … Background="Transparent" Height="200"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
```
|
||||
|
||||
등록 마크까지 넣으려면 `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
|
||||
<lvc:CartesianChart TooltipBackgroundPaint="{Binding TooltipBackground}"
|
||||
TooltipTextPaint="{Binding TooltipText}"
|
||||
LegendTextPaint="{Binding LegendText}"
|
||||
LegendPosition="Top"/>
|
||||
```
|
||||
|
||||
## 5. 게이지에서 확인할 것
|
||||
|
||||
- 판이 어두워지면 남은 구간(`GridLine`)이 검정이 아니라 회색으로 읽힙니다
|
||||
- 가운데 숫자가 나타납니다. 안 보이면 `DataLabelsPaint`가 `Text` 토큰인지 확인하세요
|
||||
- 목표선(파선)은 LiveCharts2가 그려주지 않습니다 — 시안의 75% 틱은 차트 위에 `Path`를 얹어야 합니다. 계산식은 `FIX-10.md` §5에 있습니다
|
||||
|
||||
## 6. 확인
|
||||
|
||||
1. 다크 테마에서 차트 판이 배경과 같은 어두운 색인지
|
||||
2. 격자선이 검은 실선이 아니라 헤어라인으로 보이는지
|
||||
3. 막대가 진한 파랑(`#1E7FD4`)인지
|
||||
4. 게이지 가운데 82.4가 보이는지
|
||||
5. 라이트 테마로 바꾸고 화면을 다시 열었을 때 색이 라이트 토큰으로 바뀌는지
|
||||
6. 차트 패널 테두리가 셸의 다른 판과 같은 1px 헤어라인인지
|
||||
75
.design/code/APPLY.md
Normal file
@ -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`를 `<Image Source="..."/>`로 교체하세요.
|
||||
- 우상단: 날짜(작게) + **시계 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<HomeView, HomeViewModel>(ViewIds.Home, "홈", "Home");
|
||||
manager.AddMain<MonitorView, MonitorViewModel>(ViewIds.Monitor, "모니터링·그래프", "Monitor");
|
||||
manager.AddMain<ReportView, ReportViewModel>(ViewIds.Report, "통계·리포트", "Chart");
|
||||
manager.AddMain<MaintenanceView, MaintenanceViewModel>(ViewIds.Maintenance, "유지보수", "Maintenance");
|
||||
manager.AddMain<SettingsView, SettingsViewModel>(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 아이콘의 곡선만 확인해 주세요.
|
||||
732
.design/code/CLAUDE_CODE_PROMPT.md
Normal file
@ -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
|
||||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
|
||||
<!-- 라이트: 기술 도면 톤의 밝은 바탕 + 회사 하늘색 accent -->
|
||||
<SolidColorBrush x:Key="Ubisam.Brush.ContentBackground" Color="#FFF2F2F3"/>
|
||||
<SolidColorBrush x:Key="Ubisam.Brush.ContentForeground" Color="#FF1D1F20"/>
|
||||
<SolidColorBrush x:Key="Ubisam.Brush.Accent" Color="#FF2196F3"/>
|
||||
|
||||
<!-- 상단 브랜딩 바 = 흰 판, 하단 탭 바와 같은 계열 -->
|
||||
<SolidColorBrush x:Key="Ubisam.Brush.Sidebar.Background" Color="#FFFFFFFF"/>
|
||||
<SolidColorBrush x:Key="Ubisam.Brush.Sidebar.Foreground" Color="#FF5D5D60"/>
|
||||
<SolidColorBrush x:Key="Ubisam.Brush.Sidebar.HoverBackground" Color="#14000000"/>
|
||||
<SolidColorBrush x:Key="Ubisam.Brush.Sidebar.SelectedBackground" Color="#122196F3"/>
|
||||
<SolidColorBrush x:Key="Ubisam.Brush.Sidebar.SelectedForeground" Color="#FF2196F3"/>
|
||||
|
||||
<SolidColorBrush x:Key="Ubisam.Brush.SubTabBar.Background" Color="#FFFFFFFF"/>
|
||||
<SolidColorBrush x:Key="Ubisam.Brush.SubTabBar.Border" Color="#FFD6D6D9"/>
|
||||
<SolidColorBrush x:Key="Ubisam.Brush.SubTabBar.Foreground" Color="#FF5D5D60"/>
|
||||
<SolidColorBrush x:Key="Ubisam.Brush.SubTabBar.HoverBackground" Color="#14000000"/>
|
||||
<SolidColorBrush x:Key="Ubisam.Brush.SubTabBar.SelectedBackground" Color="#FF2196F3"/>
|
||||
<SolidColorBrush x:Key="Ubisam.Brush.SubTabBar.SelectedForeground" Color="#FFFFFFFF"/>
|
||||
|
||||
<!-- 빈 뷰 슬롯의 사선 해치 색 -->
|
||||
<SolidColorBrush x:Key="Ubisam.Brush.Slot.Hatch" Color="#172196F3"/>
|
||||
<SolidColorBrush x:Key="Ubisam.Brush.Muted" Color="#FF98989B"/>
|
||||
|
||||
</ResourceDictionary>
|
||||
```
|
||||
|
||||
### `src/UbisamBase.Core/Themes/Colors.Dark.xaml`
|
||||
|
||||
```xml
|
||||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
|
||||
<!-- 다크: 라이트의 반전 — 검은 바탕 + 시안 accent -->
|
||||
<SolidColorBrush x:Key="Ubisam.Brush.ContentBackground" Color="#FF121317"/>
|
||||
<SolidColorBrush x:Key="Ubisam.Brush.ContentForeground" Color="#FFE6E6E9"/>
|
||||
<SolidColorBrush x:Key="Ubisam.Brush.Accent" Color="#FF22D3EE"/>
|
||||
|
||||
<SolidColorBrush x:Key="Ubisam.Brush.Sidebar.Background" Color="#FF16171C"/>
|
||||
<SolidColorBrush x:Key="Ubisam.Brush.Sidebar.Foreground" Color="#FFB8BCC4"/>
|
||||
<SolidColorBrush x:Key="Ubisam.Brush.Sidebar.HoverBackground" Color="#18FFFFFF"/>
|
||||
<SolidColorBrush x:Key="Ubisam.Brush.Sidebar.SelectedBackground" Color="#1A22D3EE"/>
|
||||
<SolidColorBrush x:Key="Ubisam.Brush.Sidebar.SelectedForeground" Color="#FF22D3EE"/>
|
||||
|
||||
<SolidColorBrush x:Key="Ubisam.Brush.SubTabBar.Background" Color="#FF16171C"/>
|
||||
<SolidColorBrush x:Key="Ubisam.Brush.SubTabBar.Border" Color="#FF2A2C33"/>
|
||||
<SolidColorBrush x:Key="Ubisam.Brush.SubTabBar.Foreground" Color="#FFB8BCC4"/>
|
||||
<SolidColorBrush x:Key="Ubisam.Brush.SubTabBar.HoverBackground" Color="#18FFFFFF"/>
|
||||
<SolidColorBrush x:Key="Ubisam.Brush.SubTabBar.SelectedBackground" Color="#FF22D3EE"/>
|
||||
<SolidColorBrush x:Key="Ubisam.Brush.SubTabBar.SelectedForeground" Color="#FF0B0C0F"/>
|
||||
|
||||
<SolidColorBrush x:Key="Ubisam.Brush.Slot.Hatch" Color="#1A22D3EE"/>
|
||||
<SolidColorBrush x:Key="Ubisam.Brush.Muted" Color="#FF6A6F79"/>
|
||||
|
||||
</ResourceDictionary>
|
||||
```
|
||||
|
||||
### `src/UbisamBase.Core/Themes/Icons.xaml`
|
||||
|
||||
```xml
|
||||
<!-- Ubisam Base 아이콘 세트 — 24×24 그리드, 스트로크 렌더링(StrokeThickness 1.5).
|
||||
Path의 Fill은 비우고 Stroke만 쓴다. 색은 상위 Foreground를 상속시키려면
|
||||
Stroke="{Binding Foreground, RelativeSource={RelativeSource AncestorType=ListBoxItem}}" 처럼 묶으면 된다.
|
||||
아이콘을 추가할 때는 Lucide(https://lucide.dev)에서 같은 24px 그리드/스트로크 1.5 규칙으로 가져와
|
||||
여기에 Geometry 한 줄로 등록하고, AddMain의 icon 인자에 키 뒷부분("Home" 등)을 넘긴다. -->
|
||||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
|
||||
<!-- 현재 탭 5개 -->
|
||||
<Geometry x:Key="Ubisam.Icon.Home">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</Geometry>
|
||||
<Geometry x:Key="Ubisam.Icon.Monitor">M22,12 L18,12 L15,20 L11,4 L8,12 L2,12</Geometry>
|
||||
<Geometry x:Key="Ubisam.Icon.Chart">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</Geometry>
|
||||
<Geometry x:Key="Ubisam.Icon.Maintenance">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</Geometry>
|
||||
<Geometry x:Key="Ubisam.Icon.Settings">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</Geometry>
|
||||
|
||||
<!-- 예비 아이콘 — 탭이 늘어날 때 사용 -->
|
||||
<Geometry x:Key="Ubisam.Icon.Alarm">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</Geometry>
|
||||
<Geometry x:Key="Ubisam.Icon.Log">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</Geometry>
|
||||
<Geometry x:Key="Ubisam.Icon.Document">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</Geometry>
|
||||
<Geometry x:Key="Ubisam.Icon.Data">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</Geometry>
|
||||
<Geometry x:Key="Ubisam.Icon.Power">M12,3 L12,12 M6.8,7.3 A8,8 0 1 0 17.2,7.3</Geometry>
|
||||
<Geometry x:Key="Ubisam.Icon.Temperature">M14,14 L14,5 A2,2 0 1 0 10,5 L10,14 A4,4 0 1 0 14,14 Z</Geometry>
|
||||
<Geometry x:Key="Ubisam.Icon.Gauge">M4,18 A9,9 0 1 1 20,18 M12,14 L16,10</Geometry>
|
||||
<Geometry x:Key="Ubisam.Icon.Run">M8,5 L19,12 L8,19 Z</Geometry>
|
||||
<Geometry x:Key="Ubisam.Icon.Stop">M6.5,6.5 L17.5,6.5 L17.5,17.5 L6.5,17.5 Z</Geometry>
|
||||
<Geometry x:Key="Ubisam.Icon.Reset">M20,12 A8,8 0 1 1 16.8,5.6 M20,4.5 L20,9.5 L15,9.5</Geometry>
|
||||
<Geometry x:Key="Ubisam.Icon.Account">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</Geometry>
|
||||
<Geometry x:Key="Ubisam.Icon.Lock">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</Geometry>
|
||||
<Geometry x:Key="Ubisam.Icon.Export">M12,3.5 L12,14.5 M7,10 L12,14.5 L17,10 M4,20.5 L20,20.5</Geometry>
|
||||
<Geometry x:Key="Ubisam.Icon.Import">M12,14.5 L12,3.5 M7,8 L12,3.5 L17,8 M4,20.5 L20,20.5</Geometry>
|
||||
<Geometry x:Key="Ubisam.Icon.Search">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</Geometry>
|
||||
<Geometry x:Key="Ubisam.Icon.Calendar">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</Geometry>
|
||||
<Geometry x:Key="Ubisam.Icon.Clock">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</Geometry>
|
||||
<Geometry x:Key="Ubisam.Icon.Link">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</Geometry>
|
||||
<Geometry x:Key="Ubisam.Icon.Warning">M12,4 L21,20 L3,20 Z M12,10 L12,14.5 M12,17.4 L12,17.6</Geometry>
|
||||
<Geometry x:Key="Ubisam.Icon.Pass">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</Geometry>
|
||||
<Geometry x:Key="Ubisam.Icon.Folder">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</Geometry>
|
||||
<Geometry x:Key="Ubisam.Icon.Vision">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</Geometry>
|
||||
<Geometry x:Key="Ubisam.Icon.Control">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</Geometry>
|
||||
<Geometry x:Key="Ubisam.Icon.Recipe">M12,3.5 L3,8.5 L12,13.5 L21,8.5 Z M3,13.5 L12,18.5 L21,13.5</Geometry>
|
||||
<Geometry x:Key="Ubisam.Icon.Modules">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</Geometry>
|
||||
|
||||
</ResourceDictionary>
|
||||
```
|
||||
|
||||
### `src/UbisamBase.Core/Themes/ShellStyles.xaml`
|
||||
|
||||
```xml
|
||||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:conv="clr-namespace:UbisamBase.Core.Converters"
|
||||
xmlns:sys="clr-namespace:System;assembly=mscorlib">
|
||||
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<ResourceDictionary Source="pack://application:,,,/UbisamBase.Core;component/Themes/Icons.xaml"/>
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
|
||||
<conv:BoolToVisibilityConverter x:Key="BoolToVis"/>
|
||||
<conv:FirstCharacterConverter x:Key="FirstCharConverter"/>
|
||||
<conv:ToastTypeToBrushConverter x:Key="ToastTypeToBrush"/>
|
||||
<conv:IconKeyToGeometryConverter x:Key="IconToGeometry"/>
|
||||
|
||||
<!-- 타이포: Barlow가 설치돼 있으면 쓰고, 없으면 Segoe UI로 폴백 -->
|
||||
<FontFamily x:Key="Ubisam.Font.Heading">Barlow Condensed, Segoe UI</FontFamily>
|
||||
<FontFamily x:Key="Ubisam.Font.Body">Barlow, Segoe UI</FontFamily>
|
||||
|
||||
<!-- 좌/우측 네비게이션 아이템: 아이콘 위 + 라벨 아래, 선택 시 좌측 accent 바 -->
|
||||
<Style x:Key="Ubisam.Style.SidebarItem" TargetType="ListBoxItem">
|
||||
<Setter Property="Padding" Value="14,20"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource Ubisam.Brush.Sidebar.Foreground}"/>
|
||||
<Setter Property="Cursor" Value="Hand"/>
|
||||
<Setter Property="HorizontalContentAlignment" Value="Center"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="ListBoxItem">
|
||||
<Border x:Name="Bd"
|
||||
Background="Transparent"
|
||||
BorderThickness="3,0,0,0"
|
||||
BorderBrush="Transparent"
|
||||
Padding="{TemplateBinding Padding}">
|
||||
<StackPanel Orientation="Vertical" HorizontalAlignment="Center">
|
||||
<TextBlock Text="{Binding Icon}"
|
||||
FontFamily="Segoe Fluent Icons, Segoe MDL2 Assets"
|
||||
FontSize="26"
|
||||
HorizontalAlignment="Center"/>
|
||||
<TextBlock Text="{Binding Title}"
|
||||
FontSize="13"
|
||||
Margin="0,8,0,0"
|
||||
HorizontalAlignment="Center"
|
||||
TextWrapping="Wrap"
|
||||
TextAlignment="Center"
|
||||
MaxWidth="100"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsSelected" Value="True">
|
||||
<Setter TargetName="Bd" Property="BorderBrush" Value="{DynamicResource Ubisam.Brush.Accent}"/>
|
||||
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource Ubisam.Brush.Sidebar.SelectedBackground}"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource Ubisam.Brush.Sidebar.SelectedForeground}"/>
|
||||
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource Ubisam.Brush.Sidebar.HoverBackground}"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style x:Key="Ubisam.Style.SidebarBar.Vertical" TargetType="ListBox">
|
||||
<Setter Property="Background" Value="{DynamicResource Ubisam.Brush.Sidebar.Background}"/>
|
||||
<Setter Property="BorderThickness" Value="0,0,1,0"/>
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource Ubisam.Brush.SubTabBar.Border}"/>
|
||||
<Setter Property="Padding" Value="0,20"/>
|
||||
<Setter Property="ItemContainerStyle" Value="{StaticResource Ubisam.Style.SidebarItem}"/>
|
||||
<Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Disabled"/>
|
||||
<Setter Property="ScrollViewer.VerticalScrollBarVisibility" Value="Auto"/>
|
||||
<Setter Property="ItemsPanel">
|
||||
<Setter.Value>
|
||||
<ItemsPanelTemplate>
|
||||
<StackPanel Orientation="Vertical"/>
|
||||
</ItemsPanelTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<!-- 하단 네비게이션 아이템: 균등 분할 칸, 아이콘 위 + 라벨 아래.
|
||||
선택 시 상단 3px accent 바 + 옅은 accent 판. icon 인자가 비면 라벨만 나온다. -->
|
||||
<Style x:Key="Ubisam.Style.SidebarItem.Horizontal" TargetType="ListBoxItem">
|
||||
<Setter Property="Padding" Value="10,12,10,13"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource Ubisam.Brush.Sidebar.Foreground}"/>
|
||||
<Setter Property="FontFamily" Value="{StaticResource Ubisam.Font.Body}"/>
|
||||
<Setter Property="FontSize" Value="13.5"/>
|
||||
<Setter Property="Cursor" Value="Hand"/>
|
||||
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="ListBoxItem">
|
||||
<Grid>
|
||||
<!-- 칸 구분선 -->
|
||||
<Border BorderThickness="1,0,0,0"
|
||||
BorderBrush="{DynamicResource Ubisam.Brush.SubTabBar.Border}"/>
|
||||
<Border x:Name="Bd"
|
||||
Background="Transparent"
|
||||
BorderThickness="0,3,0,0"
|
||||
BorderBrush="Transparent"
|
||||
Padding="{TemplateBinding Padding}">
|
||||
<StackPanel Orientation="Vertical" HorizontalAlignment="Center" VerticalAlignment="Center">
|
||||
<Path Data="{Binding Icon, Converter={StaticResource IconToGeometry}}"
|
||||
Stroke="{Binding Foreground, RelativeSource={RelativeSource TemplatedParent}}"
|
||||
StrokeThickness="1.5"
|
||||
StrokeStartLineCap="Round"
|
||||
StrokeEndLineCap="Round"
|
||||
StrokeLineJoin="Round"
|
||||
Width="22" Height="22"
|
||||
Stretch="Uniform"
|
||||
Margin="0,0,0,7"
|
||||
HorizontalAlignment="Center"/>
|
||||
<TextBlock Text="{Binding Title}"
|
||||
HorizontalAlignment="Center"
|
||||
TextTrimming="CharacterEllipsis"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Grid>
|
||||
<ControlTemplate.Triggers>
|
||||
<!-- icon 인자를 생략한 탭은 Path를 접어 라벨만 남긴다 -->
|
||||
<DataTrigger Binding="{Binding Icon, Converter={StaticResource IconToGeometry}}" Value="{x:Null}">
|
||||
<Setter Property="Padding" Value="10,16,10,15"/>
|
||||
</DataTrigger>
|
||||
<Trigger Property="IsSelected" Value="True">
|
||||
<Setter TargetName="Bd" Property="BorderBrush" Value="{DynamicResource Ubisam.Brush.Accent}"/>
|
||||
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource Ubisam.Brush.Sidebar.SelectedBackground}"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource Ubisam.Brush.Sidebar.SelectedForeground}"/>
|
||||
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource Ubisam.Brush.Sidebar.HoverBackground}"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<!-- 하단 탭 바: 등록된 탭 수만큼 균등 분할(UniformGrid Rows=1) -->
|
||||
<Style x:Key="Ubisam.Style.SidebarBar.Horizontal" TargetType="ListBox">
|
||||
<Setter Property="Background" Value="{DynamicResource Ubisam.Brush.Sidebar.Background}"/>
|
||||
<Setter Property="BorderThickness" Value="0,1,0,0"/>
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource Ubisam.Brush.SubTabBar.Border}"/>
|
||||
<Setter Property="Padding" Value="0"/>
|
||||
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
|
||||
<Setter Property="ItemContainerStyle" Value="{StaticResource Ubisam.Style.SidebarItem.Horizontal}"/>
|
||||
<Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Disabled"/>
|
||||
<Setter Property="ScrollViewer.VerticalScrollBarVisibility" Value="Disabled"/>
|
||||
<Setter Property="ItemsPanel">
|
||||
<Setter.Value>
|
||||
<ItemsPanelTemplate>
|
||||
<UniformGrid Rows="1"/>
|
||||
</ItemsPanelTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<!-- 하단 서브탭: 각진 세그먼트(라운드 제거) -->
|
||||
<Style x:Key="Ubisam.Style.SubTabItem" TargetType="ListBoxItem">
|
||||
<Setter Property="Padding" Value="22,11"/>
|
||||
<Setter Property="FontSize" Value="14"/>
|
||||
<Setter Property="FontFamily" Value="{StaticResource Ubisam.Font.Body}"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource Ubisam.Brush.SubTabBar.Foreground}"/>
|
||||
<Setter Property="Cursor" Value="Hand"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="ListBoxItem">
|
||||
<Border x:Name="Bd"
|
||||
Background="Transparent"
|
||||
BorderThickness="1,0,0,0"
|
||||
BorderBrush="{DynamicResource Ubisam.Brush.SubTabBar.Border}"
|
||||
Padding="{TemplateBinding Padding}">
|
||||
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsSelected" Value="True">
|
||||
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource Ubisam.Brush.SubTabBar.SelectedBackground}"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource Ubisam.Brush.SubTabBar.SelectedForeground}"/>
|
||||
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource Ubisam.Brush.SubTabBar.HoverBackground}"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style x:Key="Ubisam.Style.SubTabBar" TargetType="ListBox">
|
||||
<Setter Property="Background" Value="{DynamicResource Ubisam.Brush.SubTabBar.Background}"/>
|
||||
<Setter Property="BorderThickness" Value="0,1,0,0"/>
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource Ubisam.Brush.SubTabBar.Border}"/>
|
||||
<Setter Property="Padding" Value="0"/>
|
||||
<Setter Property="ItemContainerStyle" Value="{StaticResource Ubisam.Style.SubTabItem}"/>
|
||||
<Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Auto"/>
|
||||
<Setter Property="ScrollViewer.VerticalScrollBarVisibility" Value="Disabled"/>
|
||||
<Setter Property="ItemsPanel">
|
||||
<Setter.Value>
|
||||
<ItemsPanelTemplate>
|
||||
<StackPanel Orientation="Horizontal"/>
|
||||
</ItemsPanelTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<!-- 상단바의 계정/바탕화면/종료 같은 유틸리티 버튼: 각진 헤어라인 박스 -->
|
||||
<Style x:Key="Ubisam.Style.ChromeButton" TargetType="Button">
|
||||
<Setter Property="Padding" Value="14,7"/>
|
||||
<Setter Property="FontSize" Value="13"/>
|
||||
<Setter Property="FontFamily" Value="{StaticResource Ubisam.Font.Body}"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource Ubisam.Brush.Sidebar.Foreground}"/>
|
||||
<Setter Property="Cursor" Value="Hand"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Border x:Name="Bd"
|
||||
Background="Transparent"
|
||||
CornerRadius="0"
|
||||
BorderThickness="1"
|
||||
BorderBrush="{DynamicResource Ubisam.Brush.SubTabBar.Border}"
|
||||
Padding="{TemplateBinding Padding}">
|
||||
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource Ubisam.Brush.Sidebar.HoverBackground}"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<!-- 빈 뷰 슬롯 배경(사선 해치). 모듈이 마운트되기 전 영역 표시용. -->
|
||||
<DrawingBrush x:Key="Ubisam.Brush.SlotHatch"
|
||||
TileMode="Tile"
|
||||
Viewport="0,0,9,9"
|
||||
ViewportUnits="Absolute">
|
||||
<DrawingBrush.Drawing>
|
||||
<GeometryDrawing>
|
||||
<GeometryDrawing.Pen>
|
||||
<Pen Thickness="1" Brush="{DynamicResource Ubisam.Brush.Slot.Hatch}"/>
|
||||
</GeometryDrawing.Pen>
|
||||
<GeometryDrawing.Geometry>
|
||||
<LineGeometry StartPoint="0,9" EndPoint="9,0"/>
|
||||
</GeometryDrawing.Geometry>
|
||||
</GeometryDrawing>
|
||||
</DrawingBrush.Drawing>
|
||||
</DrawingBrush>
|
||||
|
||||
</ResourceDictionary>
|
||||
```
|
||||
|
||||
### `src/UbisamBase.Core/Shell/ShellWindow.xaml`
|
||||
|
||||
```xml
|
||||
<Window x:Class="UbisamBase.Core.Shell.ShellWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:messaging="clr-namespace:UbisamBase.Core.Messaging"
|
||||
Title="{Binding Title}"
|
||||
Height="760" Width="1200"
|
||||
WindowStartupLocation="CenterScreen"
|
||||
WindowState="Maximized"
|
||||
Background="{DynamicResource Ubisam.Brush.ContentBackground}">
|
||||
|
||||
<Window.Resources>
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<ResourceDictionary Source="pack://application:,,,/UbisamBase.Core;component/Themes/ShellStyles.xaml"/>
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
</ResourceDictionary>
|
||||
</Window.Resources>
|
||||
|
||||
<Grid>
|
||||
<DockPanel TextElement.Foreground="{DynamicResource Ubisam.Brush.ContentForeground}"
|
||||
TextElement.FontFamily="{StaticResource Ubisam.Font.Body}">
|
||||
|
||||
<!-- 상단 브랜딩 바: 좌측 = 로고 + 회사명 + 장비명, 우측 = 날짜 + 시계, 그 뒤 유틸리티 버튼.
|
||||
플랫폼 고정 요소 — 모듈이 건드리지 않는다. -->
|
||||
<Border DockPanel.Dock="Top"
|
||||
Background="{DynamicResource Ubisam.Brush.Sidebar.Background}"
|
||||
BorderBrush="{DynamicResource Ubisam.Brush.SubTabBar.Border}"
|
||||
BorderThickness="0,0,0,1"
|
||||
Padding="26,14">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<StackPanel Grid.Column="0" Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<!-- 로고 자리: 이미지 파일이 준비되면 이 Border를 <Image Source="..."/>로 교체 -->
|
||||
<Border Width="30" Height="30" Background="{DynamicResource Ubisam.Brush.Accent}">
|
||||
<TextBlock Text="{Binding Brand, Converter={StaticResource FirstCharConverter}}"
|
||||
Foreground="{DynamicResource Ubisam.Brush.SubTabBar.SelectedForeground}"
|
||||
FontFamily="{StaticResource Ubisam.Font.Heading}"
|
||||
FontWeight="SemiBold"
|
||||
FontSize="17"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
|
||||
<TextBlock Text="{Binding Brand}"
|
||||
FontFamily="{StaticResource Ubisam.Font.Heading}"
|
||||
FontWeight="SemiBold"
|
||||
FontSize="17"
|
||||
VerticalAlignment="Center"
|
||||
Margin="14,0,0,0">
|
||||
<TextBlock.LayoutTransform>
|
||||
<ScaleTransform ScaleX="1" ScaleY="1"/>
|
||||
</TextBlock.LayoutTransform>
|
||||
</TextBlock>
|
||||
|
||||
<Border Width="1" Height="20" Margin="14,0"
|
||||
Background="{DynamicResource Ubisam.Brush.SubTabBar.Border}"/>
|
||||
|
||||
<!-- 장비명: IAppSetup.GetAppName()이 돌려주는 값 -->
|
||||
<TextBlock Text="{Binding Title}"
|
||||
FontSize="14.5"
|
||||
Foreground="{DynamicResource Ubisam.Brush.Sidebar.Foreground}"
|
||||
VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Grid.Column="1" Orientation="Horizontal" VerticalAlignment="Center" Margin="0,0,20,0">
|
||||
<TextBlock Text="{Binding CurrentDate}"
|
||||
FontSize="11"
|
||||
Foreground="{DynamicResource Ubisam.Brush.Muted}"
|
||||
VerticalAlignment="Bottom"
|
||||
Margin="0,0,12,2"/>
|
||||
<TextBlock Text="{Binding CurrentTime}"
|
||||
FontFamily="{StaticResource Ubisam.Font.Heading}"
|
||||
FontWeight="SemiBold"
|
||||
FontSize="25"
|
||||
VerticalAlignment="Bottom"/>
|
||||
</StackPanel>
|
||||
|
||||
<Button Grid.Column="2"
|
||||
Content="{Binding Auth.UserDisplay}"
|
||||
Style="{StaticResource Ubisam.Style.ChromeButton}"
|
||||
VerticalAlignment="Center"
|
||||
Margin="0,0,8,0"
|
||||
Click="Account_Click"/>
|
||||
|
||||
<StackPanel Grid.Column="3" Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<Button Content="바탕화면"
|
||||
Style="{StaticResource Ubisam.Style.ChromeButton}"
|
||||
Click="ShowDesktop_Click"/>
|
||||
<Button Content="종료"
|
||||
Style="{StaticResource Ubisam.Style.ChromeButton}"
|
||||
Margin="8,0,0,0"
|
||||
Click="Exit_Click"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- 네비게이션: 기본은 하단(NavPosition.Bottom). 좌/우 전환은 코드비하인드가 처리 -->
|
||||
<ListBox x:Name="NavList"
|
||||
DockPanel.Dock="Bottom"
|
||||
Style="{StaticResource Ubisam.Style.SidebarBar.Horizontal}"
|
||||
Height="56"
|
||||
ItemsSource="{Binding MainTabs}"
|
||||
SelectedItem="{Binding SelectedMainTab, Mode=TwoWay}">
|
||||
<ListBox.LayoutTransform>
|
||||
<ScaleTransform ScaleX="{Binding UiSettings.ChromeScale}" ScaleY="{Binding UiSettings.ChromeScale}"/>
|
||||
</ListBox.LayoutTransform>
|
||||
</ListBox>
|
||||
|
||||
<!-- 컨텐츠 + 하단 서브탭. 컨텐츠는 여백 없이 영역 전체를 쓴다 —
|
||||
내부 여백은 각 모듈 View가 직접 정한다. -->
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- 모듈이 마운트되기 전에는 사선 해치가 빈 슬롯을 표시 -->
|
||||
<Border Grid.Row="0" Background="{DynamicResource Ubisam.Brush.SlotHatch}">
|
||||
<ContentControl Content="{Binding SelectedMainTab.CurrentContent}"/>
|
||||
</Border>
|
||||
|
||||
<ListBox Grid.Row="1"
|
||||
Style="{StaticResource Ubisam.Style.SubTabBar}"
|
||||
DisplayMemberPath="Title"
|
||||
ItemsSource="{Binding SelectedMainTab.SubTabs}"
|
||||
SelectedItem="{Binding SelectedMainTab.SelectedSubTab, Mode=TwoWay}"
|
||||
Visibility="{Binding SelectedMainTab.HasSubTabs, Converter={StaticResource BoolToVis}}">
|
||||
<ListBox.LayoutTransform>
|
||||
<ScaleTransform ScaleX="{Binding UiSettings.ChromeScale}" ScaleY="{Binding UiSettings.ChromeScale}"/>
|
||||
</ListBox.LayoutTransform>
|
||||
</ListBox>
|
||||
</Grid>
|
||||
</DockPanel>
|
||||
|
||||
<!-- 토스트 오버레이 -->
|
||||
<ItemsControl HorizontalAlignment="Right"
|
||||
VerticalAlignment="Top"
|
||||
Margin="0,80,20,0"
|
||||
IsHitTestVisible="False">
|
||||
<ItemsControl.ItemsSource>
|
||||
<Binding Path="Messages" Source="{x:Static messaging:ToastService.Current}"/>
|
||||
</ItemsControl.ItemsSource>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Border Background="{Binding Type, Converter={StaticResource ToastTypeToBrush}}"
|
||||
Padding="16,12"
|
||||
Margin="0,0,0,8"
|
||||
MaxWidth="360">
|
||||
<TextBlock Text="{Binding Text}" Foreground="White" TextWrapping="Wrap" FontSize="13"/>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</Grid>
|
||||
</Window>
|
||||
```
|
||||
|
||||
### `src/UbisamBase.Core/Shell/ShellViewModel.cs`
|
||||
|
||||
```csharp
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Windows.Threading;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using UbisamBase.Core.Auth;
|
||||
using UbisamBase.Core.Modules;
|
||||
using UbisamBase.Core.Settings;
|
||||
using UbisamBase.Core.Setup;
|
||||
|
||||
namespace UbisamBase.Core.Shell;
|
||||
|
||||
public partial class ShellViewModel : ObservableObject
|
||||
{
|
||||
/// <summary>좌상단에 표기되는 회사명. 장비명(Title)과 구분선으로 나란히 놓인다.</summary>
|
||||
public string Brand { get; } = "UBISAM";
|
||||
|
||||
/// <summary>좌상단 장비명. IAppSetup.GetAppName()이 돌려주는 값.</summary>
|
||||
public string Title { get; }
|
||||
|
||||
public UiSettingsService UiSettings { get; }
|
||||
|
||||
public AuthService Auth { get; }
|
||||
|
||||
public ObservableCollection<MainTabViewModel> MainTabs { get; } = new();
|
||||
|
||||
[ObservableProperty]
|
||||
private MainTabViewModel? selectedMainTab;
|
||||
|
||||
[ObservableProperty]
|
||||
private string currentTime = DateTime.Now.ToString("HH:mm:ss");
|
||||
|
||||
[ObservableProperty]
|
||||
private string currentDate = FormatDate(DateTime.Now);
|
||||
|
||||
private readonly DispatcherTimer clockTimer;
|
||||
|
||||
public ShellViewModel(IServiceProvider serviceProvider, IAppSetup appSetup, TabManager tabManager, UiSettingsService uiSettings, AuthService auth)
|
||||
{
|
||||
Title = appSetup.GetAppName();
|
||||
UiSettings = uiSettings;
|
||||
Auth = auth;
|
||||
|
||||
foreach (var registration in tabManager.Registrations.OrderBy(m => m.Order))
|
||||
{
|
||||
var mainTab = new MainTabViewModel(
|
||||
registration.Title,
|
||||
registration.Icon,
|
||||
() => registration.ContentFactory(serviceProvider),
|
||||
registration.Order);
|
||||
|
||||
foreach (var sub in registration.SubTabs.OrderBy(s => s.Order))
|
||||
{
|
||||
mainTab.AddSubTab(new SubTabViewModel(sub.Title, () => sub.ContentFactory(serviceProvider), sub.Order));
|
||||
}
|
||||
|
||||
MainTabs.Add(mainTab);
|
||||
}
|
||||
|
||||
SelectedMainTab = MainTabs.FirstOrDefault();
|
||||
|
||||
clockTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) };
|
||||
clockTimer.Tick += (_, _) =>
|
||||
{
|
||||
var now = DateTime.Now;
|
||||
CurrentTime = now.ToString("HH:mm:ss");
|
||||
CurrentDate = FormatDate(now);
|
||||
};
|
||||
clockTimer.Start();
|
||||
}
|
||||
|
||||
private static string FormatDate(DateTime value)
|
||||
{
|
||||
var day = value.DayOfWeek switch
|
||||
{
|
||||
DayOfWeek.Sunday => "일",
|
||||
DayOfWeek.Monday => "월",
|
||||
DayOfWeek.Tuesday => "화",
|
||||
DayOfWeek.Wednesday => "수",
|
||||
DayOfWeek.Thursday => "목",
|
||||
DayOfWeek.Friday => "금",
|
||||
_ => "토"
|
||||
};
|
||||
|
||||
return $"{value:yyyy.MM.dd} {day}";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `src/UbisamBase.Core/Converters/IconKeyToGeometryConverter.cs`
|
||||
|
||||
```csharp
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Windows;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace UbisamBase.Core.Converters;
|
||||
|
||||
/// <summary>
|
||||
/// MainTabViewModel.Icon 문자열("Home", "Monitor" 등)을 Themes/Icons.xaml의
|
||||
/// Geometry 리소스("Ubisam.Icon.Home")로 바꿔준다.
|
||||
/// 값이 비어 있거나 없는 키면 null을 돌려주고, 하단 탭은 라벨만 표시한다.
|
||||
/// </summary>
|
||||
public sealed class IconKeyToGeometryConverter : IValueConverter
|
||||
{
|
||||
public object? Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
if (value is not string key || string.IsNullOrWhiteSpace(key))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var resourceKey = key.StartsWith("Ubisam.Icon.", StringComparison.Ordinal)
|
||||
? key
|
||||
: "Ubisam.Icon." + key;
|
||||
|
||||
return Application.Current?.TryFindResource(resourceKey) as Geometry;
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
=> throw new NotSupportedException();
|
||||
}
|
||||
```
|
||||
|
||||
## 3. 직접 수정해야 하는 기존 파일 2곳
|
||||
|
||||
### `src/UbisamBase.Core/Shell/ShellWindow.xaml.cs`
|
||||
|
||||
`ApplyNavPosition`에서 하단 바 높이를 100 → 72로 바꾸세요 (아이콘 + 라벨 두 줄 높이).
|
||||
|
||||
```csharp
|
||||
NavList.Width = isHorizontal ? double.NaN : 140;
|
||||
NavList.Height = isHorizontal ? 72 : double.NaN;
|
||||
```
|
||||
|
||||
### `src/UbisamBase.Core/Settings/UiSettingsService.cs`
|
||||
|
||||
기본 네비게이션 위치를 하단으로 바꾸세요.
|
||||
|
||||
```csharp
|
||||
[ObservableProperty]
|
||||
private NavPosition navPosition = NavPosition.Bottom;
|
||||
```
|
||||
|
||||
## 4. 탭 등록 예시 (`src/UbisamBase.App/AppSetup.cs`)
|
||||
|
||||
`icon` 인자는 이제 아이콘 이름입니다. 기존 등록에 이름을 채워 넣으세요.
|
||||
|
||||
```csharp
|
||||
manager.AddMain<HomeView, HomeViewModel>(ViewIds.Home, "홈", "Home");
|
||||
manager.AddMain<SettingsView, SettingsViewModel>(ViewIds.Settings, "설정", "Settings");
|
||||
manager.AddSub<GeneralSettingsView, GeneralSettingsViewModel>(ViewIds.Settings, "일반");
|
||||
```
|
||||
|
||||
사용 가능한 아이콘 이름 30종: 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)에서 **24×24 그리드 / 스트로크 1.5** 규칙으로 가져와 `Icons.xaml`에 `Geometry` 한 줄로 등록하세요. SVG의 `<circle>`/`<rect>`는 WPF Geometry 문법(호 `A` 두 개 / `M…L…Z`)으로 옮겨야 합니다.
|
||||
|
||||
## 5. 완료 후 확인할 것
|
||||
|
||||
1. 빌드가 통과하는지 (`dotnet build` 또는 Visual Studio)
|
||||
2. `Icons.xaml`이 빌드에 포함되는지 — SDK 스타일 csproj + `UseWPF`라 자동 포함되지만, 리소스 조회 실패 시 `Page`로 명시 추가
|
||||
3. 하단 탭이 창 폭에 균등 분할되고, 선택된 탭에 상단 accent 바가 보이는지
|
||||
4. 설정 화면에서 테마를 Light/Dark로 바꿀 때 상단바·하단 탭·아이콘 색이 함께 전환되는지
|
||||
5. 화면 배율(ChromeScale) 슬라이더를 움직였을 때 아이콘이 깨지지 않고 확대되는지
|
||||
6. 아이콘 이름을 넘기지 않은 탭이 라벨만으로 정상 표시되는지
|
||||
|
||||
## 6. 하지 말 것
|
||||
|
||||
- 모듈 View(`HomeView`, `SettingsView`, `GeneralSettingsView` 등)의 내용은 건드리지 마세요 — 셸만 바꿉니다.
|
||||
- 컨텐츠 영역에 제목/헤더를 다시 넣지 마세요.
|
||||
- 기존 `TabManager`, `EventBus`, `ToastService`, `AuthService`, `LogService`의 구조나 시그니처를 바꾸지 마세요. `ShellViewModel`은 `Brand`/`CurrentDate` 추가 외에는 동일합니다.
|
||||
1069
.design/code/DARK_UI_PROMPT.md
Normal file
80
.design/code/FIX-01.md
Normal file
@ -0,0 +1,80 @@
|
||||
# 패치 01 — 적용 후 발견된 3가지 수정
|
||||
|
||||
첫 적용 후 실행 화면에서 나온 문제를 고치는 패치입니다. 이 문서를 Claude Code에 붙여넣고 "이 패치 적용해줘"라고 요청하세요.
|
||||
`CLAUDE_CODE_PROMPT.md`를 이미 적용한 상태를 전제로 합니다.
|
||||
|
||||
## 문제 1 — 하단 탭 바가 잘린다
|
||||
|
||||
`ShellWindow.xaml`에서 `NavList`에 `Height="56"`이 박혀 있는데, 아이콘(22) + 간격(7) + 라벨 + 상하 패딩을 합치면 56을 넘겨서 라벨이 창 밖으로 밀립니다. **높이 지정을 지우고 내용 높이에 맡깁니다.**
|
||||
|
||||
`ShellWindow.xaml`의 `NavList`에서 이 줄을 삭제:
|
||||
|
||||
```xml
|
||||
Height="56"
|
||||
```
|
||||
|
||||
그리고 `ShellWindow.xaml.cs`의 `ApplyNavPosition`도 하단일 때 높이를 강제하지 않도록:
|
||||
|
||||
```csharp
|
||||
NavList.Width = isHorizontal ? double.NaN : 140;
|
||||
NavList.Height = double.NaN;
|
||||
```
|
||||
|
||||
## 문제 2 — 모듈이 떠 있는데도 사선 해치가 배경에 깔린다
|
||||
|
||||
해치는 **빈 슬롯 표시용**이라 모듈이 마운트되면 사라져야 합니다. `ShellWindow.xaml`의 컨텐츠 `Border`를 아래로 교체하세요 — `CurrentContent`가 null일 때만 해치가 됩니다.
|
||||
|
||||
```xml
|
||||
<Border Grid.Row="0" Padding="26,22">
|
||||
<Border.Style>
|
||||
<Style TargetType="Border">
|
||||
<Setter Property="Background" Value="{DynamicResource Ubisam.Brush.ContentBackground}"/>
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding SelectedMainTab.CurrentContent}" Value="{x:Null}">
|
||||
<Setter Property="Background" Value="{DynamicResource Ubisam.Brush.SlotHatch}"/>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Border.Style>
|
||||
<ContentControl Content="{Binding SelectedMainTab.CurrentContent}"/>
|
||||
</Border>
|
||||
```
|
||||
|
||||
## 문제 3 — 컨텐츠가 화면 왼쪽 끝에 붙는다
|
||||
|
||||
위 `Border`의 `Padding="26,22"`가 이 문제를 함께 해결합니다. 상단 헤더(`Padding="26,14"`)와 세로선이 맞습니다.
|
||||
|
||||
`Margin`이 아니라 `Padding`인 게 중요합니다 — 배경(해치/바탕색)은 영역 전체를 채우고, 안의 모듈 컨텐츠만 26px 안으로 들어옵니다. 모듈 View는 계속 여백 없이 작성해도 됩니다.
|
||||
|
||||
## 문제 4 (덤) — 아이콘 없는 탭도 아이콘 높이를 차지한다
|
||||
|
||||
`ShellStyles.xaml`의 `Ubisam.Style.SidebarItem.Horizontal`에서 `Path`에 `x:Name="Ico"`를 주고, 트리거를 패딩 변경이 아니라 **Path 숨김**으로 바꾸세요.
|
||||
|
||||
```xml
|
||||
<Path x:Name="Ico"
|
||||
Data="{Binding Icon, Converter={StaticResource IconToGeometry}}"
|
||||
... />
|
||||
```
|
||||
|
||||
```xml
|
||||
<DataTrigger Binding="{Binding Icon, Converter={StaticResource IconToGeometry}}" Value="{x:Null}">
|
||||
<Setter TargetName="Ico" Property="Visibility" Value="Collapsed"/>
|
||||
</DataTrigger>
|
||||
```
|
||||
|
||||
## 참고 — 이번 화면에서 아이콘이 안 보인 이유
|
||||
|
||||
`AppSetup.cs`의 `AddMain`에 `icon` 인자를 아직 넘기지 않아서 라벨만 나온 상태입니다. 정상 동작입니다. 아이콘을 켜려면:
|
||||
|
||||
```csharp
|
||||
manager.AddMain<HomeView, HomeViewModel>(ViewIds.Home, "홈", "Home");
|
||||
manager.AddMain<ToolView, ToolViewModel>(ViewIds.Tool, "도구", "Control");
|
||||
manager.AddMain<SettingsView, SettingsViewModel>(ViewIds.Settings, "설정", "Settings");
|
||||
```
|
||||
|
||||
## 확인
|
||||
|
||||
1. 하단 탭 바가 잘리지 않고 라벨(과 아이콘)이 온전히 보이는지
|
||||
2. 설정 화면처럼 모듈이 떠 있을 때 배경이 해치가 아닌 단색인지
|
||||
3. 컨텐츠 좌측이 상단 로고와 세로로 맞는지
|
||||
4. 화면 배율을 1.6까지 올렸을 때 하단 탭 바가 같이 커지고 잘리지 않는지
|
||||
35
.design/code/FIX-02.md
Normal file
@ -0,0 +1,35 @@
|
||||
# 패치 02 — 하단 탭 바가 너무 넓다
|
||||
|
||||
하단 탭이 화면 폭을 균등 분할해서 칸이 과하게 커졌습니다. **가운데에 자기 크기만큼 모이도록** 바꿉니다
|
||||
(디자인 시안 3a와 같은 방식). 바 자체는 폭 전체를 쓰고 상단 경계선도 그대로 이어집니다.
|
||||
|
||||
`src/UbisamBase.Core/Themes/ShellStyles.xaml` 두 곳만 고치면 됩니다.
|
||||
|
||||
## 1. `Ubisam.Style.SidebarBar.Horizontal`
|
||||
|
||||
ItemsPanel을 `UniformGrid`에서 가운데 정렬 `StackPanel`로 바꾸고, ListBox의 `HorizontalContentAlignment="Stretch"` Setter를 삭제하세요.
|
||||
|
||||
```xml
|
||||
<Setter Property="ItemsPanel">
|
||||
<Setter.Value>
|
||||
<ItemsPanelTemplate>
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center"/>
|
||||
</ItemsPanelTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
```
|
||||
|
||||
## 2. `Ubisam.Style.SidebarItem.Horizontal`
|
||||
|
||||
칸 너비를 라벨 길이 + 좌우 패딩으로 정하도록, 패딩을 키우고 아이템의 `HorizontalContentAlignment="Stretch"` Setter를 삭제하세요.
|
||||
|
||||
```xml
|
||||
<Setter Property="Padding" Value="34,12,34,13"/>
|
||||
```
|
||||
|
||||
## 확인
|
||||
|
||||
- 탭 3~5개가 화면 가운데에 붙어 있고, 각 칸이 라벨 길이에 맞게 좁아졌는지
|
||||
- 상단 경계선(1px)은 여전히 화면 폭 끝까지 이어지는지
|
||||
- 탭이 늘어나 폭을 넘기면 좌우로 스크롤되지 않고 잘리는지 — 그런 경우 알려주세요. `ScrollViewer.HorizontalScrollBarVisibility`를 `Auto`로 되돌리면 됩니다.
|
||||
- 여전히 넓게 느껴지면 패딩 34 → 24로 줄이면 됩니다.
|
||||
184
.design/code/FIX-03.md
Normal file
@ -0,0 +1,184 @@
|
||||
# 패치 03 — 하단 탭 모양 정리 + 선택 인디케이터 슬라이드 애니메이션
|
||||
|
||||
첨부하신 첫 번째 사진(아이콘 위 / 라벨 아래 / 라벨 아래 밑줄만) 기준으로 맞추고, 탭을 전환하면 그 밑줄이 선택한 탭으로 **미끄러져 이동**합니다. 화면 컨텐츠는 건드리지 않고 하단 탭 바에만 애니메이션이 들어갑니다.
|
||||
|
||||
이 패치는 패치 01·02를 포함해 대체합니다 — `ShellStyles.xaml`의 하단 탭 관련 두 스타일을 통째로 교체합니다.
|
||||
|
||||
## 1. 모양 (사진 1 기준)
|
||||
|
||||
- 칸 사이 세로 구분선 **제거**
|
||||
- 선택 시 상단 3px 바 + 옅은 accent 판 → **바닥 3px 밑줄만**
|
||||
- 선택 배경 틴트 **제거**, 선택된 탭은 글자·아이콘 색만 accent
|
||||
- 마우스 오버는 선택되지 않은 탭에만 옅은 틴트
|
||||
- 탭은 가운데 모임(균등 분할 아님), 아이템 패딩 `30,13,30,14`
|
||||
|
||||
## 2. `src/UbisamBase.Core/Themes/ShellStyles.xaml`
|
||||
|
||||
`Ubisam.Style.SidebarItem.Horizontal`과 `Ubisam.Style.SidebarBar.Horizontal`을 아래 두 스타일로 교체하세요.
|
||||
`SidebarBar.Horizontal`에는 `ControlTemplate`이 새로 들어갑니다 — 그 안의 `SelectionIndicator` / `SelectionIndicatorShift` 이름을 코드비하인드가 찾습니다. **이름을 바꾸지 마세요.**
|
||||
|
||||
```xml
|
||||
<Style x:Key="Ubisam.Style.SidebarItem.Horizontal" TargetType="ListBoxItem">
|
||||
<Setter Property="Padding" Value="30,13,30,14"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource Ubisam.Brush.Sidebar.Foreground}"/>
|
||||
<Setter Property="FontFamily" Value="{StaticResource Ubisam.Font.Body}"/>
|
||||
<Setter Property="FontSize" Value="13.5"/>
|
||||
<Setter Property="FontWeight" Value="Medium"/>
|
||||
<Setter Property="Cursor" Value="Hand"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="ListBoxItem">
|
||||
<Border x:Name="Bd" Background="Transparent" Padding="{TemplateBinding Padding}">
|
||||
<StackPanel Orientation="Vertical" HorizontalAlignment="Center" VerticalAlignment="Center">
|
||||
<Path x:Name="Ico"
|
||||
Data="{Binding Icon, Converter={StaticResource IconToGeometry}}"
|
||||
Stroke="{Binding Foreground, RelativeSource={RelativeSource TemplatedParent}}"
|
||||
StrokeThickness="1.5"
|
||||
StrokeStartLineCap="Round" StrokeEndLineCap="Round" StrokeLineJoin="Round"
|
||||
Width="22" Height="22" Stretch="Uniform"
|
||||
Margin="0,0,0,7" HorizontalAlignment="Center"/>
|
||||
<TextBlock Text="{Binding Title}" HorizontalAlignment="Center" TextTrimming="CharacterEllipsis"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<DataTrigger Binding="{Binding Icon, Converter={StaticResource IconToGeometry}}" Value="{x:Null}">
|
||||
<Setter TargetName="Ico" Property="Visibility" Value="Collapsed"/>
|
||||
</DataTrigger>
|
||||
<Trigger Property="IsSelected" Value="True">
|
||||
<Setter Property="Foreground" Value="{DynamicResource Ubisam.Brush.Sidebar.SelectedForeground}"/>
|
||||
</Trigger>
|
||||
<MultiTrigger>
|
||||
<MultiTrigger.Conditions>
|
||||
<Condition Property="IsMouseOver" Value="True"/>
|
||||
<Condition Property="IsSelected" Value="False"/>
|
||||
</MultiTrigger.Conditions>
|
||||
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource Ubisam.Brush.Sidebar.HoverBackground}"/>
|
||||
</MultiTrigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style x:Key="Ubisam.Style.SidebarBar.Horizontal" TargetType="ListBox">
|
||||
<Setter Property="Background" Value="{DynamicResource Ubisam.Brush.Sidebar.Background}"/>
|
||||
<Setter Property="BorderThickness" Value="0,1,0,0"/>
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource Ubisam.Brush.SubTabBar.Border}"/>
|
||||
<Setter Property="Padding" Value="0"/>
|
||||
<Setter Property="ItemContainerStyle" Value="{StaticResource Ubisam.Style.SidebarItem.Horizontal}"/>
|
||||
<Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Disabled"/>
|
||||
<Setter Property="ScrollViewer.VerticalScrollBarVisibility" Value="Disabled"/>
|
||||
<Setter Property="ItemsPanel">
|
||||
<Setter.Value>
|
||||
<ItemsPanelTemplate>
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center"/>
|
||||
</ItemsPanelTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="ListBox">
|
||||
<Border Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}">
|
||||
<Grid>
|
||||
<ItemsPresenter/>
|
||||
<Border x:Name="SelectionIndicator"
|
||||
Height="3" Width="0"
|
||||
HorizontalAlignment="Left" VerticalAlignment="Bottom"
|
||||
Background="{DynamicResource Ubisam.Brush.Accent}">
|
||||
<Border.RenderTransform>
|
||||
<TranslateTransform x:Name="SelectionIndicatorShift" X="0"/>
|
||||
</Border.RenderTransform>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
```
|
||||
|
||||
## 3. `src/UbisamBase.Core/Shell/ShellWindow.xaml.cs` — 애니메이션
|
||||
|
||||
using 추가:
|
||||
|
||||
```csharp
|
||||
using System.Windows.Media.Animation;
|
||||
```
|
||||
|
||||
`ShellWindow` 생성자 마지막에 이벤트 두 개를 붙이세요.
|
||||
|
||||
```csharp
|
||||
NavList.SelectionChanged += (_, _) => MoveTabIndicator(animate: true);
|
||||
NavList.Loaded += (_, _) => MoveTabIndicator(animate: false);
|
||||
NavList.SizeChanged += (_, _) => MoveTabIndicator(animate: false);
|
||||
```
|
||||
|
||||
그리고 메서드를 추가하세요.
|
||||
|
||||
```csharp
|
||||
/// <summary>하단 탭의 선택 인디케이터를 선택된 탭 아래로 옮긴다. 좌/우 네비게이션일 때는 인디케이터가 없어 무시된다.</summary>
|
||||
private void MoveTabIndicator(bool animate)
|
||||
{
|
||||
if (NavList.Template?.FindName("SelectionIndicator", NavList) is not Border indicator ||
|
||||
NavList.Template.FindName("SelectionIndicatorShift", NavList) is not TranslateTransform shift)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (NavList.SelectedItem == null ||
|
||||
NavList.ItemContainerGenerator.ContainerFromItem(NavList.SelectedItem) is not ListBoxItem container ||
|
||||
!container.IsArrangeValid)
|
||||
{
|
||||
indicator.Width = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
var origin = container.TranslatePoint(new Point(0, 0), NavList);
|
||||
var targetX = origin.X;
|
||||
var targetWidth = container.ActualWidth;
|
||||
|
||||
if (!animate)
|
||||
{
|
||||
shift.BeginAnimation(TranslateTransform.XProperty, null);
|
||||
indicator.BeginAnimation(FrameworkElement.WidthProperty, null);
|
||||
shift.X = targetX;
|
||||
indicator.Width = targetWidth;
|
||||
return;
|
||||
}
|
||||
|
||||
var duration = new Duration(TimeSpan.FromMilliseconds(260));
|
||||
var ease = new CubicEase { EasingMode = EasingMode.EaseOut };
|
||||
|
||||
shift.BeginAnimation(TranslateTransform.XProperty,
|
||||
new DoubleAnimation(targetX, duration) { EasingFunction = ease, FillBehavior = FillBehavior.HoldEnd });
|
||||
indicator.BeginAnimation(FrameworkElement.WidthProperty,
|
||||
new DoubleAnimation(targetWidth, duration) { EasingFunction = ease, FillBehavior = FillBehavior.HoldEnd });
|
||||
}
|
||||
```
|
||||
|
||||
`ApplyNavPosition`에서 위치를 바꾼 뒤에도 한 번 호출해 주세요 (좌/우 → 하단 전환 시 위치 재계산).
|
||||
|
||||
```csharp
|
||||
NavList.Width = isHorizontal ? double.NaN : 140;
|
||||
NavList.Height = double.NaN;
|
||||
|
||||
if (isHorizontal)
|
||||
{
|
||||
Dispatcher.BeginInvoke(new Action(() => MoveTabIndicator(animate: false)));
|
||||
}
|
||||
```
|
||||
|
||||
## 4. 확인
|
||||
|
||||
1. 탭을 누르면 밑줄이 260ms에 걸쳐 미끄러져 오고, 너비도 새 탭 폭에 맞게 함께 변하는지
|
||||
2. 앱을 처음 켰을 때는 애니메이션 없이 첫 탭 아래에 바로 놓이는지
|
||||
3. 창 크기를 바꾸거나 최대화할 때 밑줄이 어긋나지 않는지
|
||||
4. 설정에서 네비게이션 위치를 좌/우로 바꿨다가 하단으로 되돌렸을 때 밑줄이 정상 위치인지
|
||||
5. 권한에 따라 탭이 필터링(`navView.Refresh()`)된 뒤에도 밑줄이 맞는지 — 어긋나면 `OnAuthChanged` 끝에 `MoveTabIndicator(animate: false)`를 한 줄 추가하세요
|
||||
|
||||
## 참고
|
||||
|
||||
- 260ms가 느리면 `TimeSpan.FromMilliseconds(160)`으로 줄이세요.
|
||||
- 밑줄을 라벨 폭만큼만 짧게 하고 싶으면 `targetWidth`를 `container.ActualWidth - 40` 정도로 줄이고 `targetX`에 `20`을 더하면 됩니다.
|
||||
74
.design/code/FIX-04.md
Normal file
@ -0,0 +1,74 @@
|
||||
# 패치 04 — 입력 컨트롤 · 표 스타일
|
||||
|
||||
TextBox / ComboBox / CheckBox / Button / ListBox / ListView(GridView) 를 셸과 같은 도면 톤으로 맞춥니다.
|
||||
각진 모서리 · 1px 헤어라인 · 포커스는 accent 테두리 + 옅은 글로우. 라이트/다크 모두 토큰으로 전환됩니다.
|
||||
|
||||
전부 **암시적 스타일**(`x:Key` 없음)이라 각 View를 고칠 필요가 없습니다 — 셸 안의 모든 해당 컨트롤에 자동 적용됩니다.
|
||||
예외를 두려면 그 컨트롤에 `Style="{x:Null}"`을 주세요.
|
||||
|
||||
## 1. 새 파일
|
||||
|
||||
`src/UbisamBase.Core/Themes/Controls.xaml` — 이 프로젝트의 `code/UbisamBase.Core/Themes/Controls.xaml`을 그대로 복사하세요.
|
||||
|
||||
## 2. `Themes/ShellStyles.xaml`
|
||||
|
||||
머지 목록에 Controls.xaml을 추가하고, ComboBox 드롭다운 그림자를 정의합니다.
|
||||
|
||||
```xml
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<ResourceDictionary Source="pack://application:,,,/UbisamBase.Core;component/Themes/Icons.xaml"/>
|
||||
<ResourceDictionary Source="pack://application:,,,/UbisamBase.Core;component/Themes/Controls.xaml"/>
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
```
|
||||
|
||||
```xml
|
||||
<DropShadowEffect x:Key="Ubisam.Effect.Popup" BlurRadius="12" ShadowDepth="3" Direction="270" Opacity="0.22" Color="#000000"/>
|
||||
```
|
||||
|
||||
## 3. `Themes/Colors.Light.xaml` / `Colors.Dark.xaml`
|
||||
|
||||
입력·표 토큰을 추가합니다 (`code/`의 두 파일에 이미 들어 있습니다).
|
||||
|
||||
| 키 | 라이트 | 다크 |
|
||||
| --- | --- | --- |
|
||||
| `Ubisam.Brush.Input.Background` | #FFFFFF | #1B1D23 |
|
||||
| `Ubisam.Brush.Input.Border` | #C9C9CC | #33363E |
|
||||
| `Ubisam.Brush.Input.BorderHover` | #A8A8AC | #4A4E57 |
|
||||
| `Ubisam.Brush.Input.DisabledBackground` | #EDEDEF | #191A1F |
|
||||
| `Ubisam.Brush.Input.DisabledBorder` | #DEDEE1 | #26282F |
|
||||
| `Ubisam.Color.AccentGlow` (Color) | #2196F3 | #22D3EE |
|
||||
| `Ubisam.Brush.Table.HeaderBackground` | #EDEDEF | #1B1D23 |
|
||||
| `Ubisam.Brush.Table.RowBorder` | #EDEDEF | #1F2127 |
|
||||
|
||||
## 4. 버튼 종류
|
||||
|
||||
암시적 Button은 **보조(외곽선)** 입니다. 주 동작에는 키를 지정하세요.
|
||||
|
||||
```xml
|
||||
<Button Content="저장" Command="{Binding SaveCommand}" Style="{StaticResource Ubisam.Style.Button.Primary}"/>
|
||||
<Button Content="되돌리기" Style="{StaticResource Ubisam.Style.Button.Ghost}"/>
|
||||
```
|
||||
|
||||
`AllSettingsView.xaml`의 "저장" 버튼에 Primary를 적용하고, 거기 붙어 있는 `Padding="14,6"`은 지우세요(스타일이 정합니다).
|
||||
|
||||
## 5. 확인
|
||||
|
||||
1. `설정 → 설정값` 화면의 TextBox/ComboBox/CheckBox가 각진 모양으로 바뀌고, 클릭 시 테두리가 accent로 변하는지
|
||||
2. ComboBox를 열면 드롭다운이 입력창 아래에 붙고, 선택 항목이 accent 채움인지
|
||||
3. `로그` 화면 표의 헤더가 회색 판 + 1px 경계, 행 hover/선택이 accent 틴트인지
|
||||
4. 다크 테마로 바꿨을 때 입력창 배경이 `#1B1D23`으로 함께 바뀌는지
|
||||
5. 비활성 컨트롤이 흐리게(45%) 보이는지
|
||||
|
||||
## 주의
|
||||
|
||||
- `Controls.xaml`은 폰트 리소스를 `DynamicResource`로 참조합니다. `StaticResource`로 바꾸면 머지 시점 때문에 로드가 실패합니다.
|
||||
- 암시적 `ListBox`/`ListBoxItem` 스타일이 추가되지만, 하단 탭과 서브탭 바는 `Style`/`ItemContainerStyle`을 명시적으로 지정하고 있어 영향받지 않습니다.
|
||||
- `ScrollBar`와 `Slider`는 이번 패치에 없습니다 — 필요하면 알려주세요.
|
||||
|
||||
## 6. 아이콘 세트 갱신 (Icons.xaml 재발행)
|
||||
|
||||
`Themes/Icons.xaml`을 Lucide 원본 경로 기준으로 다시 만들었습니다. 이전 버전은 손으로 근사한 도형이라 실제 Lucide와 미세하게 달랐습니다 — `code/UbisamBase.Core/Themes/Icons.xaml`을 그대로 덮어쓰세요.
|
||||
|
||||
- 좌표계·스트로크 규칙은 동일(24×24, 1.5, 둥근 캡)해서 XAML 다른 곳은 고칠 필요가 없습니다.
|
||||
- 리소스 키(`Ubisam.Icon.Home` 등)와 `AddMain`에 넘기는 이름도 그대로입니다.
|
||||
- SVG 원본은 `icons/` 폴더에 있고, 각 아이콘의 Lucide 이름은 `icons/readme.md` 표에 적어뒀습니다. 새 아이콘은 그 이름으로 lucide.dev에서 찾아 추가하세요.
|
||||
40
.design/code/FIX-05.md
Normal file
@ -0,0 +1,40 @@
|
||||
# 패치 05 — 다크 테마를 검정에서 회색으로
|
||||
|
||||
다크 테마 바탕이 사실상 검정(#121317)이라 무겁게 보였습니다. 차가운 회색 계열로 한 단계씩 올려, 바탕 위에 판이 얹힌 층이 눈에 보이게 했습니다. accent(#22D3EE)는 그대로입니다.
|
||||
|
||||
`src/UbisamBase.Core/Themes/Colors.Dark.xaml` **한 파일만** 교체하면 됩니다 — 이 프로젝트의 `code/UbisamBase.Core/Themes/Colors.Dark.xaml`을 그대로 복사하세요.
|
||||
|
||||
## 바뀐 값
|
||||
|
||||
| 역할 | 기존 | 변경 |
|
||||
| --- | --- | --- |
|
||||
| 컨텐츠 바탕 | #121317 | **#23262B** |
|
||||
| 판 (상단바 · 하단 탭 · 서브탭) | #16171C | **#2A2E34** |
|
||||
| 경계 | #2A2C33 | **#3A3F47** |
|
||||
| 본문 글자 | #E6E6E9 | **#E8EAED** |
|
||||
| 보조 글자 | #B8BCC4 | **#C2C7CE** |
|
||||
| 흐린 글자 | #6A6F79 | **#8A9099** |
|
||||
| accent 위 글자 | #0B0C0F | **#12161A** |
|
||||
| 입력 배경 | #1B1D23 | **#2F343A** |
|
||||
| 입력 경계 | #33363E | **#454B54** |
|
||||
| 입력 경계 hover | #4A4E57 | **#5A616B** |
|
||||
| 입력 비활성 배경 / 경계 | #191A1F / #26282F | **#292D32 / #383D44** |
|
||||
| 표 헤더 배경 | #1B1D23 | **#2F343A** |
|
||||
| 표 행 경계 | #1F2127 | **#383D44** |
|
||||
| hover 틴트 | #22FFFFFF | **#16FFFFFF** (바탕이 밝아진 만큼 약하게) |
|
||||
|
||||
바탕(#23262B) → 판(#2A2E34) → 입력(#2F343A) → 표 헤더(#2F343A) 순으로 밝아지는 3단 층입니다. 층 순서를 지키면 값은 취향대로 조정해도 구조가 유지됩니다.
|
||||
|
||||
## 확인
|
||||
|
||||
1. 다크 테마에서 상단바·하단 탭이 컨텐츠 바탕과 구분되는지(판이 살짝 밝음)
|
||||
2. 입력창이 판보다 한 단계 더 밝아 눌러 넣는 자리로 읽히는지
|
||||
3. 시안 글자(#22D3EE)와 회색 바탕의 대비가 충분한지 — 더 진하게 하고 싶으면 accent를 #67E8F9로 올리면 됩니다
|
||||
4. 로그 표의 행 경계선이 보이는지
|
||||
|
||||
## 더 밝게/어둡게 하려면
|
||||
|
||||
세 값만 같은 방향으로 움직이면 됩니다: `ContentBackground` / `Sidebar.Background`(= `SubTabBar.Background`) / `Input.Background`. 나머지 경계·글자는 그 위에서 자동으로 읽힙니다.
|
||||
|
||||
- 조금 더 밝게: #282C31 / #30343B / #363B42
|
||||
- 조금 더 어둡게: #1D2024 / #24282D / #292E34
|
||||
108
.design/code/FIX-06.md
Normal file
@ -0,0 +1,108 @@
|
||||
# 패치 06 — DataGrid (그리드 컨트롤)
|
||||
|
||||
`ListView` + `GridView`가 읽기 전용 표라면 `DataGrid`는 편집 가능한 격자입니다. 셸과 같은 도면 톤으로 맞췄습니다.
|
||||
패치 04(`Controls.xaml`)를 이미 적용한 상태를 전제로 합니다.
|
||||
|
||||
리포지토리에 아직 `DataGrid`를 쓰는 화면이 없어서 **스타일만** 넣습니다 — 나중에 격자를 붙이면 그대로 적용됩니다.
|
||||
|
||||
## 모양
|
||||
|
||||
- 헤더: 회색 판, 세로 구분선만, 정렬된 열은 accent 글자 + accent 화살표
|
||||
- 행: 짝수 행 옅은 줄무늬, hover 옅은 틴트, 선택은 accent 틴트
|
||||
- 행 머리(폭 26): 선택된 행만 accent 바로 채워져 어느 행인지 왼쪽 끝에서 바로 읽힘
|
||||
- 편집 중인 셀만 accent 테두리 + 내부 여백을 줄여 입력칸처럼 보임
|
||||
- 행 높이 34, 헤더 높이 34, 각진 모서리 · 1px 헤어라인
|
||||
|
||||
## 1. 파일 교체
|
||||
|
||||
| 파일 | 변경 |
|
||||
| --- | --- |
|
||||
| `src/UbisamBase.Core/Themes/Controls.xaml` | DataGrid 스타일 6종 추가 (파일 끝) |
|
||||
| `src/UbisamBase.Core/Themes/Colors.Light.xaml` | 토큰 2개 추가 |
|
||||
| `src/UbisamBase.Core/Themes/Colors.Dark.xaml` | 토큰 2개 추가 |
|
||||
|
||||
`code/UbisamBase.Core/Themes/` 아래 세 파일을 그대로 덮어쓰세요.
|
||||
|
||||
새 토큰:
|
||||
|
||||
| 키 | 라이트 | 다크 |
|
||||
| --- | --- | --- |
|
||||
| `Ubisam.Brush.Table.GridLine` | #DEDEE1 | #383D44 |
|
||||
| `Ubisam.Brush.Table.AltRowBackground` | #F8F8F9 | #333840 |
|
||||
|
||||
## 2. 쓰는 법
|
||||
|
||||
암시적 스타일이라 `<DataGrid>`만 놓으면 적용됩니다. 열은 직접 정의하세요 (`AutoGenerateColumns`는 False가 기본):
|
||||
|
||||
```xml
|
||||
<DataGrid ItemsSource="{Binding Measurements}"
|
||||
SelectedItem="{Binding SelectedMeasurement}">
|
||||
<DataGrid.Columns>
|
||||
<DataGridTextColumn Header="시각" Width="96"
|
||||
Binding="{Binding Timestamp, StringFormat=HH:mm:ss}"
|
||||
IsReadOnly="True"/>
|
||||
<DataGridTextColumn Header="레시피" Width="*"
|
||||
Binding="{Binding RecipeName}"
|
||||
IsReadOnly="True"/>
|
||||
<DataGridTextColumn Header="측정값" Width="118"
|
||||
Binding="{Binding Value, StringFormat=N1}"
|
||||
CellStyle="{StaticResource Ubisam.Style.NumericCell}"/>
|
||||
<DataGridTextColumn Header="편차" Width="96"
|
||||
Binding="{Binding Deviation, StringFormat=P1}"
|
||||
CellStyle="{StaticResource Ubisam.Style.NumericCell}"
|
||||
IsReadOnly="True"/>
|
||||
</DataGrid.Columns>
|
||||
</DataGrid>
|
||||
```
|
||||
|
||||
수치 열에는 `CellStyle="{StaticResource Ubisam.Style.NumericCell}"`을 주세요 — 우측 정렬됩니다.
|
||||
|
||||
편집을 아예 막을 화면은 `IsReadOnly="True"`를 `DataGrid`에 주면 됩니다.
|
||||
|
||||
## 3. 상태 열은 태그로
|
||||
|
||||
판정·레벨 같은 상태 열은 맨 텍스트로 두지 마세요 — 예외 행(재검, Warn)이 정상 행과 구분되지 않습니다. 태그 스타일을 쓰세요.
|
||||
|
||||
```xml
|
||||
<DataGridTemplateColumn Header="판정" Width="96">
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
<DataTemplate>
|
||||
<Border Style="{StaticResource Ubisam.Style.Tag.Accent}">
|
||||
<TextBlock Text="{Binding Verdict}" Style="{StaticResource Ubisam.Style.TagText.Accent}"/>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</DataGridTemplateColumn.CellTemplate>
|
||||
</DataGridTemplateColumn>
|
||||
```
|
||||
|
||||
세 종류입니다:
|
||||
|
||||
| 스타일 쌍 | 쓰는 곳 |
|
||||
| --- | --- |
|
||||
| `Ubisam.Style.Tag.Accent` + `TagText.Accent` | 정상·완료 (양품, Info) |
|
||||
| `Ubisam.Style.Tag.Outline` + `TagText.Outline` | 주의·예외 (재검, Warn) — 외곽선만이라 눈에 걸립니다 |
|
||||
| `Ubisam.Style.Tag` + `TagText` | 중립·대기 (—, Draft) |
|
||||
|
||||
상태값에 따라 스타일을 바꿔야 하면 `DataTrigger`로 `Border.Style`을 전환하거나, 값→스타일 컨버터를 하나 두세요.
|
||||
|
||||
새 토큰 4개(`Ubisam.Brush.Tag.*`)가 두 Colors 파일에 추가됐습니다.
|
||||
|
||||
## 4. 합계행
|
||||
|
||||
시안의 맨 아래 합계행은 `DataGrid`가 기본 제공하지 않습니다. 격자 아래에 `Grid` 한 줄을 따로 놓고 열 폭을 맞추거나, 필요하면 `DataGrid` 안에서 열 폭을 공유하는 방식으로 만들어야 합니다 — 어느 쪽으로 할지 알려주시면 그 부분만 따로 만들어 드립니다.
|
||||
|
||||
## 5. 확인
|
||||
|
||||
1. 헤더를 클릭해 정렬하면 그 열 글자가 accent로 바뀌고 화살표 방향이 뒤집히는지
|
||||
2. 행을 선택하면 왼쪽 끝 행 머리가 accent 바로 채워지는지
|
||||
3. 셀을 더블클릭해 편집에 들어가면 그 셀만 accent 테두리가 생기는지
|
||||
4. 수치 열(측정값·편차)이 모두 우측 정렬인지 — 한쪽만 좌측이면 `CellStyle`이 빠진 것입니다
|
||||
5. 판정 열의 재검 행이 양품 행과 한눈에 구분되는지
|
||||
4. 짝수 행 줄무늬가 다크에서도 구분되는지(#333840)
|
||||
5. 열 경계를 드래그해 폭 조절이 되는지 (그리퍼는 투명하지만 커서가 바뀝니다)
|
||||
|
||||
## 주의
|
||||
|
||||
- `DevExpress GridControl`이 아니라 **WPF 기본 `DataGrid`** 기준입니다. DevExpress를 쓰신다면 알려주세요 — 스타일 구조가 완전히 다릅니다.
|
||||
- 줄무늬가 부담스러우면 `DataGrid`의 `AlternatingRowBackground`를 `Transparent`로 바꾸면 됩니다.
|
||||
- 행을 더 밀집하려면 `RowHeight`를 34 → 28로, 셀 `Padding`을 `11,9` → `9,5`로 줄이세요.
|
||||
50
.design/code/FIX-07.md
Normal file
@ -0,0 +1,50 @@
|
||||
# 패치 07 — 다크모드 accent를 회사 하늘색으로
|
||||
|
||||
다크에서 시안(#22D3EE)이 회사 색과 동떨어져 보이고, 밝은 accent 위에 검은 글자를 얹는 조합이 어색했습니다. 회사 하늘색 계열로 바꾸고 **accent를 두 단계로 나눕니다.**
|
||||
|
||||
| 역할 | 다크 | 라이트 | 쓰는 곳 |
|
||||
| --- | --- | --- | --- |
|
||||
| `Ubisam.Brush.Accent` | **#4DA6F5** | #2196F3 | 선 · 글자 · 탭 밑줄 · 포커스 테두리 — 바탕 위에 얹히는 것 |
|
||||
| `Ubisam.Brush.Accent.Fill` | **#1E7FD4** | #1D82E8 | 글자를 얹는 채움면 — 주 버튼, 로고 타일, 선택된 서브탭·항목, 체크박스 |
|
||||
| `Ubisam.Brush.Accent.OnFill` | **#F2F7FC** | #FFFFFF | 그 채움면 위의 글자 · 체크 표시 |
|
||||
|
||||
밝은 단계(#4DA6F5)에 검은 글자를 얹지 않는 것이 규칙입니다 — 흰 글자와의 대비가 부족하므로, 글자가 올라가는 면은 깊은 단계(#1E7FD4, 흰 글자와 약 4.2:1)를 씁니다.
|
||||
|
||||
라이트에도 같은 두 키를 추가했습니다 — 스타일이 양쪽 테마에서 같은 이름으로 해석됩니다. 라이트의 기존 색은 그대로입니다.
|
||||
|
||||
## 파일 교체
|
||||
|
||||
`code/UbisamBase.Core/Themes/` 아래 세 파일을 덮어쓰세요.
|
||||
|
||||
| 파일 | 변경 |
|
||||
| --- | --- |
|
||||
| `Colors.Dark.xaml` | accent 계열 전체 교체 (틴트 · 글로우 · 해치 · 태그 포함) + 새 키 2개 |
|
||||
| `Colors.Light.xaml` | 새 키 2개 추가 |
|
||||
| `Controls.xaml` | 채움면을 쓰는 4곳이 `Accent.Fill` / `Accent.OnFill`을 참조 |
|
||||
| `Shell/ShellWindow.xaml` | 로고 타일이 `Accent.Fill` / `Accent.OnFill`을 참조 |
|
||||
|
||||
## 로고 — ShellWindow.xaml도 함께 바꿔야 합니다
|
||||
|
||||
상단바 로고 타일이 `Accent`(밝은 단계)에 묶여 있어 다크에서 흰 글자와 대비가 부족했습니다. 두 속성을 채움면 키로 바꾸세요 (`code/UbisamBase.Core/Shell/ShellWindow.xaml`에 이미 반영돼 있습니다).
|
||||
|
||||
```xml
|
||||
<Border Width="30" Height="30" Background="{DynamicResource Ubisam.Brush.Accent.Fill}">
|
||||
<TextBlock Text="{Binding Brand, Converter={StaticResource FirstCharConverter}}"
|
||||
Foreground="{DynamicResource Ubisam.Brush.Accent.OnFill}"
|
||||
... />
|
||||
</Border>
|
||||
```
|
||||
|
||||
`ShellStyles.xaml`의 다른 두 곳(좌측 네비 선택 테두리, 하단 탭 인디케이터 바)은 글자를 얹지 않는 선이라 `Accent`가 맞습니다 — 그대로 두세요.
|
||||
|
||||
## 로고 이미지
|
||||
|
||||
다크 상단바의 로고 타일도 #1E7FD4 + 흰 마크가 됩니다 — 원본 로고에 더 가깝습니다. `logo/ubisam-logo-mono.svg`는 타일이 `currentColor`이므로 부모에 `color: #1E7FD4`만 주면 됩니다.
|
||||
|
||||
## 확인
|
||||
|
||||
1. 다크에서 주 버튼("저장")이 진한 파랑 + 흰 글자인지
|
||||
2. 탭 밑줄 · 시계 · 포커스 테두리는 밝은 파랑(#4DA6F5)인지
|
||||
3. 체크박스 체크 표시가 흰색인지 — 검정이면 `Controls.xaml`이 갱신되지 않은 것입니다
|
||||
4. 선택된 서브탭 글자가 읽히는지
|
||||
5. 라이트 테마가 이전과 동일한지
|
||||
39
.design/code/FIX-08.md
Normal file
@ -0,0 +1,39 @@
|
||||
# 패치 08 — Slider · ScrollBar + 적용 확인
|
||||
|
||||
## 먼저 확인: Controls.xaml이 빌드에 들어갔는지
|
||||
|
||||
다크모드에서 콤보박스가 **둥근 모서리 + 밝은 회색**으로 보이면 WPF 기본 크롬입니다. 패치 04의 스타일이 적용되면 **각진 모서리 + #2F343A 어두운 배경**이 됩니다. 두 가지를 확인하세요.
|
||||
|
||||
1. `src/UbisamBase.Core/Themes/Controls.xaml` 파일이 존재하는지
|
||||
2. `Themes/ShellStyles.xaml` 상단에 머지 항목이 있는지
|
||||
|
||||
```xml
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<ResourceDictionary Source="pack://application:,,,/UbisamBase.Core;component/Themes/Icons.xaml"/>
|
||||
<ResourceDictionary Source="pack://application:,,,/UbisamBase.Core;component/Themes/Controls.xaml"/>
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
```
|
||||
|
||||
머지가 있는데도 기본 크롬이 나오면, 그 `UserControl`이 `ShellWindow.Resources`를 상속받지 못하는 경우입니다 — 그때는 `App.xaml`의 `Application.Resources`에 같은 머지를 넣으세요. 암시적 스타일은 리소스 조회 범위 안에서만 걸립니다.
|
||||
|
||||
## Slider
|
||||
|
||||
UI 설정 화면의 "화면 배율" 슬라이더가 기본 모양이었습니다. 트랙은 얇은 홈, 지나온 구간만 accent, 썸은 각진 10×20 세로 막대(둥근 다이얼이 아니라 계기 슬라이드)로 맞췄습니다.
|
||||
|
||||
## ScrollBar
|
||||
|
||||
폭 10의 얇은 막대. 트랙은 비우고 썸만 회색(#5A616B), hover·드래그 시 accent. 위아래 화살표 버튼은 없앴습니다.
|
||||
|
||||
두 스타일 모두 암시적이라 기존 화면을 고칠 필요는 없습니다.
|
||||
|
||||
## 파일
|
||||
|
||||
`code/UbisamBase.Core/Themes/Controls.xaml` 하나만 덮어쓰면 됩니다 (기존 내용 + Slider·ScrollBar 추가).
|
||||
|
||||
## 확인
|
||||
|
||||
1. 콤보박스가 어두운 배경 + 각진 모서리인지 (아니면 위 머지 문제)
|
||||
2. 화면 배율 슬라이더의 왼쪽 구간만 파랗게 차는지
|
||||
3. 썸을 잡고 끌 때 색이 밝아지는지
|
||||
4. 목록·표를 스크롤할 때 막대가 얇고 어두운지
|
||||
5. 슬라이더를 비활성화하면 45%로 흐려지는지
|
||||
65
.design/code/FIX-09.md
Normal file
@ -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`가 아직 로드되지 않은 시점)에 호출하면 화면 가운데에 뜹니다.
|
||||
@ -0,0 +1,32 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Windows;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace UbisamBase.Core.Converters;
|
||||
|
||||
/// <summary>
|
||||
/// MainTabViewModel.Icon 문자열("Home", "Monitor" 등)을 Themes/Icons.xaml의
|
||||
/// Geometry 리소스("Ubisam.Icon.Home")로 바꿔준다.
|
||||
/// 값이 비어 있거나 없는 키면 null을 돌려주고, 하단 탭은 라벨만 표시한다.
|
||||
/// </summary>
|
||||
public sealed class IconKeyToGeometryConverter : IValueConverter
|
||||
{
|
||||
public object? Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
if (value is not string key || string.IsNullOrWhiteSpace(key))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var resourceKey = key.StartsWith("Ubisam.Icon.", StringComparison.Ordinal)
|
||||
? key
|
||||
: "Ubisam.Icon." + key;
|
||||
|
||||
return Application.Current?.TryFindResource(resourceKey) as Geometry;
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
=> throw new NotSupportedException();
|
||||
}
|
||||
96
.design/code/UbisamBase.Core/Messaging/MessageDialog.xaml
Normal file
@ -0,0 +1,96 @@
|
||||
<Window x:Class="UbisamBase.Core.Messaging.MessageDialog"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
WindowStyle="None"
|
||||
ResizeMode="NoResize"
|
||||
SizeToContent="Height"
|
||||
Width="420"
|
||||
MinHeight="0"
|
||||
ShowInTaskbar="False"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
AllowsTransparency="False"
|
||||
Background="{DynamicResource Ubisam.Brush.ContentBackground}"
|
||||
KeyDown="Dialog_KeyDown">
|
||||
|
||||
<Window.Resources>
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<ResourceDictionary Source="pack://application:,,,/UbisamBase.Core;component/Themes/ShellStyles.xaml"/>
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
</ResourceDictionary>
|
||||
</Window.Resources>
|
||||
|
||||
<!-- OS 타이틀바 대신 1px 헤어라인 프레임. 셸과 같은 각진 도면 톤. -->
|
||||
<Border BorderBrush="{DynamicResource Ubisam.Brush.Input.Border}"
|
||||
BorderThickness="1"
|
||||
TextElement.Foreground="{DynamicResource Ubisam.Brush.ContentForeground}"
|
||||
TextElement.FontFamily="{StaticResource Ubisam.Font.Body}">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- 제목줄: 드래그로 창을 옮길 수 있다 -->
|
||||
<Border Grid.Row="0"
|
||||
Background="{DynamicResource Ubisam.Brush.Sidebar.Background}"
|
||||
BorderBrush="{DynamicResource Ubisam.Brush.SubTabBar.Border}"
|
||||
BorderThickness="0,0,0,1"
|
||||
Padding="20,14"
|
||||
MouseLeftButtonDown="TitleBar_MouseLeftButtonDown">
|
||||
<TextBlock x:Name="TitleText"
|
||||
Text="{Binding Title, RelativeSource={RelativeSource AncestorType=Window}}"
|
||||
FontFamily="{StaticResource Ubisam.Font.Heading}"
|
||||
FontSize="18"
|
||||
FontWeight="SemiBold"/>
|
||||
</Border>
|
||||
|
||||
<!-- 본문: 아이콘 + 메시지 -->
|
||||
<Grid Grid.Row="1" Margin="20,22">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Path x:Name="Icon"
|
||||
Grid.Column="0"
|
||||
Width="24" Height="24"
|
||||
Margin="0,1,15,0"
|
||||
Stretch="Uniform"
|
||||
StrokeThickness="1.5"
|
||||
StrokeStartLineCap="Round"
|
||||
StrokeEndLineCap="Round"
|
||||
StrokeLineJoin="Round"
|
||||
VerticalAlignment="Top"/>
|
||||
<TextBlock x:Name="MessageText"
|
||||
Grid.Column="1"
|
||||
FontSize="13.5"
|
||||
LineHeight="21"
|
||||
TextWrapping="Wrap"
|
||||
VerticalAlignment="Center"/>
|
||||
</Grid>
|
||||
|
||||
<!-- 버튼: 우측 하단. 확인이 주 동작, 취소가 보조. -->
|
||||
<Border Grid.Row="2"
|
||||
Background="{DynamicResource Ubisam.Brush.Sidebar.Background}"
|
||||
BorderBrush="{DynamicResource Ubisam.Brush.SubTabBar.Border}"
|
||||
BorderThickness="0,1,0,0"
|
||||
Padding="20,14">
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
|
||||
<Button x:Name="CancelButton"
|
||||
Content="취소"
|
||||
MinWidth="86"
|
||||
Visibility="Collapsed"
|
||||
Margin="0,0,9,0"
|
||||
Click="Cancel_Click"/>
|
||||
<Button x:Name="OkButton"
|
||||
Content="확인"
|
||||
MinWidth="86"
|
||||
IsDefault="True"
|
||||
Style="{StaticResource Ubisam.Style.Button.Primary}"
|
||||
Click="Ok_Click"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Window>
|
||||
81
.design/code/UbisamBase.Core/Messaging/MessageDialog.xaml.cs
Normal file
@ -0,0 +1,81 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace UbisamBase.Core.Messaging;
|
||||
|
||||
/// <summary>
|
||||
/// OS 기본 MessageBox 대신 쓰는 테마 적용 대화상자.
|
||||
/// 직접 생성하지 말고 <see cref="MessageUtil"/>의 Info / Error / Confirm을 쓴다.
|
||||
/// </summary>
|
||||
public partial class MessageDialog : Window
|
||||
{
|
||||
public enum DialogKind { Info, Error, Confirm }
|
||||
|
||||
private MessageDialog(string title, string message, DialogKind kind)
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
Title = title;
|
||||
MessageText.Text = message;
|
||||
|
||||
// 아이콘과 색은 종류에 따라. 오류만 붉은색을 쓰고 나머지는 accent.
|
||||
var iconKey = kind switch
|
||||
{
|
||||
DialogKind.Error => "Ubisam.Icon.Error",
|
||||
DialogKind.Confirm => "Ubisam.Icon.Question",
|
||||
_ => "Ubisam.Icon.Info"
|
||||
};
|
||||
var brushKey = kind == DialogKind.Error ? "Ubisam.Brush.Danger" : "Ubisam.Brush.Accent";
|
||||
|
||||
Icon.Data = TryFindResource(iconKey) as Geometry;
|
||||
Icon.Stroke = TryFindResource(brushKey) as Brush;
|
||||
|
||||
if (kind == DialogKind.Confirm)
|
||||
{
|
||||
OkButton.Content = "예";
|
||||
CancelButton.Content = "아니오";
|
||||
CancelButton.Visibility = Visibility.Visible;
|
||||
CancelButton.IsCancel = true;
|
||||
}
|
||||
|
||||
// 소유 창 위에 가운데 정렬. 소유자가 없으면 화면 가운데.
|
||||
var owner = Application.Current?.MainWindow;
|
||||
if (owner != null && owner.IsLoaded && !ReferenceEquals(owner, this))
|
||||
{
|
||||
Owner = owner;
|
||||
WindowStartupLocation = WindowStartupLocation.CenterOwner;
|
||||
}
|
||||
else
|
||||
{
|
||||
WindowStartupLocation = WindowStartupLocation.CenterScreen;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>확인 버튼을 눌렀으면 true. Confirm이 아닌 경우 항상 true.</summary>
|
||||
public static bool Show(string title, string message, DialogKind kind)
|
||||
{
|
||||
var dialog = new MessageDialog(title, message, kind);
|
||||
return dialog.ShowDialog() == true;
|
||||
}
|
||||
|
||||
private void TitleBar_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (e.ButtonState == MouseButtonState.Pressed)
|
||||
{
|
||||
DragMove();
|
||||
}
|
||||
}
|
||||
|
||||
private void Dialog_KeyDown(object sender, KeyEventArgs e)
|
||||
{
|
||||
if (e.Key == Key.Escape)
|
||||
{
|
||||
DialogResult = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void Ok_Click(object sender, RoutedEventArgs e) => DialogResult = true;
|
||||
|
||||
private void Cancel_Click(object sender, RoutedEventArgs e) => DialogResult = false;
|
||||
}
|
||||
50
.design/code/UbisamBase.Core/Messaging/MessageUtil.cs
Normal file
@ -0,0 +1,50 @@
|
||||
using UbisamBase.Core.Logging;
|
||||
|
||||
namespace UbisamBase.Core.Messaging;
|
||||
|
||||
/// <summary>
|
||||
/// 어디서나 바로 쓰는 대화상자 / Toast 헬퍼.
|
||||
/// 시그니처는 이전과 완전히 동일하고, 대화상자만 OS 기본 MessageBox에서
|
||||
/// 테마가 적용된 <see cref="MessageDialog"/>로 바뀌었다 — 호출하는 쪽은 고칠 것이 없다.
|
||||
/// </summary>
|
||||
public static class MessageUtil
|
||||
{
|
||||
public static void ShowInfo(string title, string message)
|
||||
=> MessageDialog.Show(title, message, MessageDialog.DialogKind.Info);
|
||||
|
||||
public static void ShowError(string title, string message)
|
||||
=> MessageDialog.Show(title, message, MessageDialog.DialogKind.Error);
|
||||
|
||||
public static bool ShowYesNo(string title, string message)
|
||||
=> MessageDialog.Show(title, message, MessageDialog.DialogKind.Confirm);
|
||||
|
||||
public static void ShowSaveSuccessDialog() => ShowInfo("저장", "저장되었습니다.");
|
||||
|
||||
public static void ShowSaveFailedDialog() => ShowError("저장 실패", "저장하지 못했습니다.");
|
||||
|
||||
public static void ShowToast(string message, Logger? logger = null)
|
||||
{
|
||||
ToastService.Current.Show(message, ToastType.Info);
|
||||
logger?.I(message);
|
||||
}
|
||||
|
||||
public static void ShowSuccessToast(string message = "완료되었습니다.", Logger? logger = null)
|
||||
{
|
||||
ToastService.Current.Show(message, ToastType.Success);
|
||||
logger?.I(message);
|
||||
}
|
||||
|
||||
public static void ShowSavedToast(Logger? logger = null) => ShowSuccessToast("저장되었습니다.", logger);
|
||||
|
||||
public static void ShowWarningToast(string message, Logger? logger = null)
|
||||
{
|
||||
ToastService.Current.Show(message, ToastType.Warning);
|
||||
logger?.W(message);
|
||||
}
|
||||
|
||||
public static void ShowErrorToast(string message, Logger? logger = null)
|
||||
{
|
||||
ToastService.Current.Show(message, ToastType.Error);
|
||||
logger?.E(message);
|
||||
}
|
||||
}
|
||||
87
.design/code/UbisamBase.Core/Shell/ShellViewModel.cs
Normal file
@ -0,0 +1,87 @@
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Windows.Threading;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using UbisamBase.Core.Auth;
|
||||
using UbisamBase.Core.Modules;
|
||||
using UbisamBase.Core.Settings;
|
||||
using UbisamBase.Core.Setup;
|
||||
|
||||
namespace UbisamBase.Core.Shell;
|
||||
|
||||
public partial class ShellViewModel : ObservableObject
|
||||
{
|
||||
/// <summary>좌상단에 표기되는 회사명. 장비명(Title)과 구분선으로 나란히 놓인다.</summary>
|
||||
public string Brand { get; } = "UBISAM";
|
||||
|
||||
/// <summary>좌상단 장비명. IAppSetup.GetAppName()이 돌려주는 값.</summary>
|
||||
public string Title { get; }
|
||||
|
||||
public UiSettingsService UiSettings { get; }
|
||||
|
||||
public AuthService Auth { get; }
|
||||
|
||||
public ObservableCollection<MainTabViewModel> MainTabs { get; } = new();
|
||||
|
||||
[ObservableProperty]
|
||||
private MainTabViewModel? selectedMainTab;
|
||||
|
||||
[ObservableProperty]
|
||||
private string currentTime = DateTime.Now.ToString("HH:mm:ss");
|
||||
|
||||
[ObservableProperty]
|
||||
private string currentDate = FormatDate(DateTime.Now);
|
||||
|
||||
private readonly DispatcherTimer clockTimer;
|
||||
|
||||
public ShellViewModel(IServiceProvider serviceProvider, IAppSetup appSetup, TabManager tabManager, UiSettingsService uiSettings, AuthService auth)
|
||||
{
|
||||
Title = appSetup.GetAppName();
|
||||
UiSettings = uiSettings;
|
||||
Auth = auth;
|
||||
|
||||
foreach (var registration in tabManager.Registrations.OrderBy(m => m.Order))
|
||||
{
|
||||
var mainTab = new MainTabViewModel(
|
||||
registration.Title,
|
||||
registration.Icon,
|
||||
() => registration.ContentFactory(serviceProvider),
|
||||
registration.Order);
|
||||
|
||||
foreach (var sub in registration.SubTabs.OrderBy(s => s.Order))
|
||||
{
|
||||
mainTab.AddSubTab(new SubTabViewModel(sub.Title, () => sub.ContentFactory(serviceProvider), sub.Order));
|
||||
}
|
||||
|
||||
MainTabs.Add(mainTab);
|
||||
}
|
||||
|
||||
SelectedMainTab = MainTabs.FirstOrDefault();
|
||||
|
||||
clockTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) };
|
||||
clockTimer.Tick += (_, _) =>
|
||||
{
|
||||
var now = DateTime.Now;
|
||||
CurrentTime = now.ToString("HH:mm:ss");
|
||||
CurrentDate = FormatDate(now);
|
||||
};
|
||||
clockTimer.Start();
|
||||
}
|
||||
|
||||
private static string FormatDate(DateTime value)
|
||||
{
|
||||
var day = value.DayOfWeek switch
|
||||
{
|
||||
DayOfWeek.Sunday => "일",
|
||||
DayOfWeek.Monday => "월",
|
||||
DayOfWeek.Tuesday => "화",
|
||||
DayOfWeek.Wednesday => "수",
|
||||
DayOfWeek.Thursday => "목",
|
||||
DayOfWeek.Friday => "금",
|
||||
_ => "토"
|
||||
};
|
||||
|
||||
return $"{value:yyyy.MM.dd} {day}";
|
||||
}
|
||||
}
|
||||
172
.design/code/UbisamBase.Core/Shell/ShellWindow.xaml
Normal file
@ -0,0 +1,172 @@
|
||||
<Window x:Class="UbisamBase.Core.Shell.ShellWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:messaging="clr-namespace:UbisamBase.Core.Messaging"
|
||||
Title="{Binding Title}"
|
||||
Height="760" Width="1200"
|
||||
WindowStartupLocation="CenterScreen"
|
||||
WindowState="Maximized"
|
||||
Background="{DynamicResource Ubisam.Brush.ContentBackground}">
|
||||
|
||||
<Window.Resources>
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<ResourceDictionary Source="pack://application:,,,/UbisamBase.Core;component/Themes/ShellStyles.xaml"/>
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
</ResourceDictionary>
|
||||
</Window.Resources>
|
||||
|
||||
<Grid>
|
||||
<DockPanel TextElement.Foreground="{DynamicResource Ubisam.Brush.ContentForeground}"
|
||||
TextElement.FontFamily="{StaticResource Ubisam.Font.Body}">
|
||||
|
||||
<!-- 상단 브랜딩 바: 좌측 = 로고 + 회사명 + 장비명, 우측 = 날짜 + 시계, 그 뒤 유틸리티 버튼.
|
||||
플랫폼 고정 요소 — 모듈이 건드리지 않는다. -->
|
||||
<Border DockPanel.Dock="Top"
|
||||
Background="{DynamicResource Ubisam.Brush.Sidebar.Background}"
|
||||
BorderBrush="{DynamicResource Ubisam.Brush.SubTabBar.Border}"
|
||||
BorderThickness="0,0,0,1"
|
||||
Padding="26,14">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<StackPanel Grid.Column="0" Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<!-- 로고 자리: 이미지 파일이 준비되면 이 Border를 <Image Source="..."/>로 교체 -->
|
||||
<Border Width="30" Height="30" Background="{DynamicResource Ubisam.Brush.Accent.Fill}">
|
||||
<TextBlock Text="{Binding Brand, Converter={StaticResource FirstCharConverter}}"
|
||||
Foreground="{DynamicResource Ubisam.Brush.Accent.OnFill}"
|
||||
FontFamily="{StaticResource Ubisam.Font.Heading}"
|
||||
FontWeight="SemiBold"
|
||||
FontSize="17"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
|
||||
<TextBlock Text="{Binding Brand}"
|
||||
FontFamily="{StaticResource Ubisam.Font.Heading}"
|
||||
FontWeight="SemiBold"
|
||||
FontSize="17"
|
||||
VerticalAlignment="Center"
|
||||
Margin="14,0,0,0">
|
||||
<TextBlock.LayoutTransform>
|
||||
<ScaleTransform ScaleX="1" ScaleY="1"/>
|
||||
</TextBlock.LayoutTransform>
|
||||
</TextBlock>
|
||||
|
||||
<Border Width="1" Height="20" Margin="14,0"
|
||||
Background="{DynamicResource Ubisam.Brush.SubTabBar.Border}"/>
|
||||
|
||||
<!-- 장비명: IAppSetup.GetAppName()이 돌려주는 값 -->
|
||||
<TextBlock Text="{Binding Title}"
|
||||
FontSize="14.5"
|
||||
Foreground="{DynamicResource Ubisam.Brush.Sidebar.Foreground}"
|
||||
VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Grid.Column="1" Orientation="Horizontal" VerticalAlignment="Center" Margin="0,0,20,0">
|
||||
<TextBlock Text="{Binding CurrentDate}"
|
||||
FontSize="11"
|
||||
Foreground="{DynamicResource Ubisam.Brush.Muted}"
|
||||
VerticalAlignment="Bottom"
|
||||
Margin="0,0,12,2"/>
|
||||
<TextBlock Text="{Binding CurrentTime}"
|
||||
FontFamily="{StaticResource Ubisam.Font.Heading}"
|
||||
FontWeight="SemiBold"
|
||||
FontSize="25"
|
||||
VerticalAlignment="Bottom"/>
|
||||
</StackPanel>
|
||||
|
||||
<Button Grid.Column="2"
|
||||
Content="{Binding Auth.UserDisplay}"
|
||||
Style="{StaticResource Ubisam.Style.ChromeButton}"
|
||||
VerticalAlignment="Center"
|
||||
Margin="0,0,8,0"
|
||||
Click="Account_Click"/>
|
||||
|
||||
<StackPanel Grid.Column="3" Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<Button Content="바탕화면"
|
||||
Style="{StaticResource Ubisam.Style.ChromeButton}"
|
||||
Click="ShowDesktop_Click"/>
|
||||
<Button Content="종료"
|
||||
Style="{StaticResource Ubisam.Style.ChromeButton}"
|
||||
Margin="8,0,0,0"
|
||||
Click="Exit_Click"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- 네비게이션: 기본은 하단(NavPosition.Bottom). 좌/우 전환은 코드비하인드가 처리.
|
||||
높이는 고정하지 않는다 — 아이콘 유무와 ChromeScale에 따라 내용 높이로 정해진다. -->
|
||||
<ListBox x:Name="NavList"
|
||||
DockPanel.Dock="Bottom"
|
||||
Style="{StaticResource Ubisam.Style.SidebarBar.Horizontal}"
|
||||
ItemsSource="{Binding MainTabs}"
|
||||
SelectedItem="{Binding SelectedMainTab, Mode=TwoWay}">
|
||||
<ListBox.LayoutTransform>
|
||||
<ScaleTransform ScaleX="{Binding UiSettings.ChromeScale}" ScaleY="{Binding UiSettings.ChromeScale}"/>
|
||||
</ListBox.LayoutTransform>
|
||||
</ListBox>
|
||||
|
||||
<!-- 컨텐츠 + 하단 서브탭. 컨텐츠는 여백 없이 영역 전체를 쓴다 —
|
||||
내부 여백은 각 모듈 View가 직접 정한다. -->
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- 모듈이 없을 때만 사선 해치로 빈 슬롯을 표시하고, 모듈이 마운트되면 바탕색으로 돌아간다.
|
||||
Padding은 모듈 컨텐츠만 안으로 밀어 넣는다 — 상단 헤더·하단 탭과 세로선을 맞춘 26px. -->
|
||||
<Border Grid.Row="0" Padding="26,22">
|
||||
<Border.Style>
|
||||
<Style TargetType="Border">
|
||||
<Setter Property="Background" Value="{DynamicResource Ubisam.Brush.ContentBackground}"/>
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding SelectedMainTab.CurrentContent}" Value="{x:Null}">
|
||||
<Setter Property="Background" Value="{DynamicResource Ubisam.Brush.SlotHatch}"/>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Border.Style>
|
||||
<ContentControl Content="{Binding SelectedMainTab.CurrentContent}"/>
|
||||
</Border>
|
||||
|
||||
<ListBox Grid.Row="1"
|
||||
Style="{StaticResource Ubisam.Style.SubTabBar}"
|
||||
DisplayMemberPath="Title"
|
||||
ItemsSource="{Binding SelectedMainTab.SubTabs}"
|
||||
SelectedItem="{Binding SelectedMainTab.SelectedSubTab, Mode=TwoWay}"
|
||||
Visibility="{Binding SelectedMainTab.HasSubTabs, Converter={StaticResource BoolToVis}}">
|
||||
<ListBox.LayoutTransform>
|
||||
<ScaleTransform ScaleX="{Binding UiSettings.ChromeScale}" ScaleY="{Binding UiSettings.ChromeScale}"/>
|
||||
</ListBox.LayoutTransform>
|
||||
</ListBox>
|
||||
</Grid>
|
||||
</DockPanel>
|
||||
|
||||
<!-- 토스트 오버레이 -->
|
||||
<ItemsControl HorizontalAlignment="Right"
|
||||
VerticalAlignment="Top"
|
||||
Margin="0,80,20,0"
|
||||
IsHitTestVisible="False">
|
||||
<ItemsControl.ItemsSource>
|
||||
<Binding Path="Messages" Source="{x:Static messaging:ToastService.Current}"/>
|
||||
</ItemsControl.ItemsSource>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Border Background="{Binding Type, Converter={StaticResource ToastTypeToBrush}}"
|
||||
Padding="16,12"
|
||||
Margin="0,0,0,8"
|
||||
MaxWidth="360">
|
||||
<TextBlock Text="{Binding Text}" Foreground="White" TextWrapping="Wrap" FontSize="13"/>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</Grid>
|
||||
</Window>
|
||||
54
.design/code/UbisamBase.Core/Themes/Colors.Dark.xaml
Normal file
@ -0,0 +1,54 @@
|
||||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
|
||||
<!-- 다크: 검정이 아니라 차가운 회색 바탕 + 회사 하늘색.
|
||||
판(#2A2E34)이 바탕(#23262B)보다 한 단계 밝아 층이 구분된다.
|
||||
accent는 두 단계로 쓴다 — 선·글자·밑줄은 밝은 #4DA6F5, 글자를 얹는 채움면은
|
||||
깊은 #1E7FD4 + 흰 글자(#F2F7FC). 밝은 단계에 검은 글자를 얹지 않는다. -->
|
||||
<SolidColorBrush x:Key="Ubisam.Brush.ContentBackground" Color="#FF23262B"/>
|
||||
<SolidColorBrush x:Key="Ubisam.Brush.ContentForeground" Color="#FFE8EAED"/>
|
||||
<SolidColorBrush x:Key="Ubisam.Brush.Accent" Color="#FF4DA6F5"/>
|
||||
<SolidColorBrush x:Key="Ubisam.Brush.Accent.Fill" Color="#FF1E7FD4"/>
|
||||
<SolidColorBrush x:Key="Ubisam.Brush.Accent.OnFill" Color="#FFF2F7FC"/>
|
||||
|
||||
<!-- 오류 — 대화상자 아이콘과 실패 태그에만 쓴다. 장식용 색을 늘리지 않는다. -->
|
||||
<SolidColorBrush x:Key="Ubisam.Brush.Danger" Color="#FFF07070"/>
|
||||
<SolidColorBrush x:Key="Ubisam.Brush.Danger.Fill" Color="#FFB33A3A"/>
|
||||
|
||||
<SolidColorBrush x:Key="Ubisam.Brush.Sidebar.Background" Color="#FF2A2E34"/>
|
||||
<SolidColorBrush x:Key="Ubisam.Brush.Sidebar.Foreground" Color="#FFC2C7CE"/>
|
||||
<SolidColorBrush x:Key="Ubisam.Brush.Sidebar.HoverBackground" Color="#16FFFFFF"/>
|
||||
<SolidColorBrush x:Key="Ubisam.Brush.Sidebar.SelectedBackground" Color="#1A4DA6F5"/>
|
||||
<SolidColorBrush x:Key="Ubisam.Brush.Sidebar.SelectedForeground" Color="#FF4DA6F5"/>
|
||||
|
||||
<SolidColorBrush x:Key="Ubisam.Brush.SubTabBar.Background" Color="#FF2A2E34"/>
|
||||
<SolidColorBrush x:Key="Ubisam.Brush.SubTabBar.Border" Color="#FF3A3F47"/>
|
||||
<SolidColorBrush x:Key="Ubisam.Brush.SubTabBar.Foreground" Color="#FFC2C7CE"/>
|
||||
<SolidColorBrush x:Key="Ubisam.Brush.SubTabBar.HoverBackground" Color="#16FFFFFF"/>
|
||||
<SolidColorBrush x:Key="Ubisam.Brush.SubTabBar.SelectedBackground" Color="#FF1E7FD4"/>
|
||||
<SolidColorBrush x:Key="Ubisam.Brush.SubTabBar.SelectedForeground" Color="#FFF2F7FC"/>
|
||||
|
||||
<!-- 입력 컨트롤 -->
|
||||
<SolidColorBrush x:Key="Ubisam.Brush.Input.Background" Color="#FF2F343A"/>
|
||||
<SolidColorBrush x:Key="Ubisam.Brush.Input.Border" Color="#FF454B54"/>
|
||||
<SolidColorBrush x:Key="Ubisam.Brush.Input.BorderHover" Color="#FF5A616B"/>
|
||||
<SolidColorBrush x:Key="Ubisam.Brush.Input.DisabledBackground" Color="#FF292D32"/>
|
||||
<SolidColorBrush x:Key="Ubisam.Brush.Input.DisabledBorder" Color="#FF383D44"/>
|
||||
<Color x:Key="Ubisam.Color.AccentGlow">#FF4DA6F5</Color>
|
||||
|
||||
<!-- 표 -->
|
||||
<SolidColorBrush x:Key="Ubisam.Brush.Table.HeaderBackground" Color="#FF2F343A"/>
|
||||
<SolidColorBrush x:Key="Ubisam.Brush.Table.RowBorder" Color="#FF383D44"/>
|
||||
<SolidColorBrush x:Key="Ubisam.Brush.Table.GridLine" Color="#FF383D44"/>
|
||||
<SolidColorBrush x:Key="Ubisam.Brush.Table.AltRowBackground" Color="#FF333840"/>
|
||||
|
||||
<!-- 상태 태그 -->
|
||||
<SolidColorBrush x:Key="Ubisam.Brush.Tag.AccentBackground" Color="#FF123A5C"/>
|
||||
<SolidColorBrush x:Key="Ubisam.Brush.Tag.AccentForeground" Color="#FFA8D4FA"/>
|
||||
<SolidColorBrush x:Key="Ubisam.Brush.Tag.NeutralBackground" Color="#FF383D44"/>
|
||||
<SolidColorBrush x:Key="Ubisam.Brush.Tag.NeutralForeground" Color="#FFC2C7CE"/>
|
||||
|
||||
<SolidColorBrush x:Key="Ubisam.Brush.Slot.Hatch" Color="#1A4DA6F5"/>
|
||||
<SolidColorBrush x:Key="Ubisam.Brush.Muted" Color="#FF8A9099"/>
|
||||
|
||||
</ResourceDictionary>
|
||||
54
.design/code/UbisamBase.Core/Themes/Colors.Light.xaml
Normal file
@ -0,0 +1,54 @@
|
||||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
|
||||
<!-- 라이트: 기술 도면 톤의 밝은 바탕 + 회사 하늘색 accent -->
|
||||
<SolidColorBrush x:Key="Ubisam.Brush.ContentBackground" Color="#FFF2F2F3"/>
|
||||
<SolidColorBrush x:Key="Ubisam.Brush.ContentForeground" Color="#FF1D1F20"/>
|
||||
<SolidColorBrush x:Key="Ubisam.Brush.Accent" Color="#FF2196F3"/>
|
||||
<!-- 글자를 얹는 채움면용 깊은 단계 + 그 위 글자색 (다크와 키를 공유) -->
|
||||
<SolidColorBrush x:Key="Ubisam.Brush.Accent.Fill" Color="#FF1D82E8"/>
|
||||
<SolidColorBrush x:Key="Ubisam.Brush.Accent.OnFill" Color="#FFFFFFFF"/>
|
||||
|
||||
<!-- 오류 — 대화상자 아이콘과 실패 태그에만 쓴다. 장식용 색을 늘리지 않는다. -->
|
||||
<SolidColorBrush x:Key="Ubisam.Brush.Danger" Color="#FFC93B3B"/>
|
||||
<SolidColorBrush x:Key="Ubisam.Brush.Danger.Fill" Color="#FFB32F2F"/>
|
||||
|
||||
<!-- 상단 브랜딩 바 = 흰 판, 하단 탭 바와 같은 계열 -->
|
||||
<SolidColorBrush x:Key="Ubisam.Brush.Sidebar.Background" Color="#FFFFFFFF"/>
|
||||
<SolidColorBrush x:Key="Ubisam.Brush.Sidebar.Foreground" Color="#FF5D5D60"/>
|
||||
<SolidColorBrush x:Key="Ubisam.Brush.Sidebar.HoverBackground" Color="#14000000"/>
|
||||
<SolidColorBrush x:Key="Ubisam.Brush.Sidebar.SelectedBackground" Color="#122196F3"/>
|
||||
<SolidColorBrush x:Key="Ubisam.Brush.Sidebar.SelectedForeground" Color="#FF2196F3"/>
|
||||
|
||||
<SolidColorBrush x:Key="Ubisam.Brush.SubTabBar.Background" Color="#FFFFFFFF"/>
|
||||
<SolidColorBrush x:Key="Ubisam.Brush.SubTabBar.Border" Color="#FFD6D6D9"/>
|
||||
<SolidColorBrush x:Key="Ubisam.Brush.SubTabBar.Foreground" Color="#FF5D5D60"/>
|
||||
<SolidColorBrush x:Key="Ubisam.Brush.SubTabBar.HoverBackground" Color="#14000000"/>
|
||||
<SolidColorBrush x:Key="Ubisam.Brush.SubTabBar.SelectedBackground" Color="#FF2196F3"/>
|
||||
<SolidColorBrush x:Key="Ubisam.Brush.SubTabBar.SelectedForeground" Color="#FFFFFFFF"/>
|
||||
|
||||
<!-- 입력 컨트롤 -->
|
||||
<SolidColorBrush x:Key="Ubisam.Brush.Input.Background" Color="#FFFFFFFF"/>
|
||||
<SolidColorBrush x:Key="Ubisam.Brush.Input.Border" Color="#FFC9C9CC"/>
|
||||
<SolidColorBrush x:Key="Ubisam.Brush.Input.BorderHover" Color="#FFA8A8AC"/>
|
||||
<SolidColorBrush x:Key="Ubisam.Brush.Input.DisabledBackground" Color="#FFEDEDEF"/>
|
||||
<SolidColorBrush x:Key="Ubisam.Brush.Input.DisabledBorder" Color="#FFDEDEE1"/>
|
||||
<Color x:Key="Ubisam.Color.AccentGlow">#FF2196F3</Color>
|
||||
|
||||
<!-- 표 -->
|
||||
<SolidColorBrush x:Key="Ubisam.Brush.Table.HeaderBackground" Color="#FFEDEDEF"/>
|
||||
<SolidColorBrush x:Key="Ubisam.Brush.Table.RowBorder" Color="#FFEDEDEF"/>
|
||||
<SolidColorBrush x:Key="Ubisam.Brush.Table.GridLine" Color="#FFDEDEE1"/>
|
||||
<SolidColorBrush x:Key="Ubisam.Brush.Table.AltRowBackground" Color="#FFF8F8F9"/>
|
||||
|
||||
<!-- 상태 태그 -->
|
||||
<SolidColorBrush x:Key="Ubisam.Brush.Tag.AccentBackground" Color="#FFDCEBFA"/>
|
||||
<SolidColorBrush x:Key="Ubisam.Brush.Tag.AccentForeground" Color="#FF0B4C7E"/>
|
||||
<SolidColorBrush x:Key="Ubisam.Brush.Tag.NeutralBackground" Color="#FFEDEDEF"/>
|
||||
<SolidColorBrush x:Key="Ubisam.Brush.Tag.NeutralForeground" Color="#FF3F3F42"/>
|
||||
|
||||
<!-- 빈 뷰 슬롯의 사선 해치 색 -->
|
||||
<SolidColorBrush x:Key="Ubisam.Brush.Slot.Hatch" Color="#172196F3"/>
|
||||
<SolidColorBrush x:Key="Ubisam.Brush.Muted" Color="#FF98989B"/>
|
||||
|
||||
</ResourceDictionary>
|
||||
803
.design/code/UbisamBase.Core/Themes/Controls.xaml
Normal file
@ -0,0 +1,803 @@
|
||||
<!-- 입력 컨트롤 · 표 스타일. 전부 암시적 스타일(x:Key 없음)이라
|
||||
각 View를 고치지 않아도 셸 안의 모든 TextBox/ComboBox/CheckBox/Button/ListBox/ListView에 적용된다.
|
||||
특정 위치만 예외로 두고 싶으면 그 컨트롤에 Style="{x:Null}"을 주면 된다. -->
|
||||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
|
||||
<!-- ══ TextBox ══ -->
|
||||
<Style TargetType="TextBox">
|
||||
<Setter Property="MinHeight" Value="34"/>
|
||||
<Setter Property="Padding" Value="10,0"/>
|
||||
<Setter Property="FontFamily" Value="{DynamicResource Ubisam.Font.Body}"/>
|
||||
<Setter Property="FontSize" Value="13.5"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource Ubisam.Brush.ContentForeground}"/>
|
||||
<Setter Property="CaretBrush" Value="{DynamicResource Ubisam.Brush.Accent}"/>
|
||||
<Setter Property="SelectionBrush" Value="{DynamicResource Ubisam.Brush.Accent}"/>
|
||||
<Setter Property="VerticalContentAlignment" Value="Center"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="TextBox">
|
||||
<Border x:Name="Bd"
|
||||
Background="{DynamicResource Ubisam.Brush.Input.Background}"
|
||||
BorderBrush="{DynamicResource Ubisam.Brush.Input.Border}"
|
||||
BorderThickness="1"
|
||||
SnapsToDevicePixels="True">
|
||||
<ScrollViewer x:Name="PART_ContentHost"
|
||||
Margin="{TemplateBinding Padding}"
|
||||
VerticalAlignment="Center"
|
||||
Focusable="False"
|
||||
HorizontalScrollBarVisibility="Hidden"
|
||||
VerticalScrollBarVisibility="Hidden"/>
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="Bd" Property="BorderBrush" Value="{DynamicResource Ubisam.Brush.Input.BorderHover}"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsKeyboardFocusWithin" Value="True">
|
||||
<Setter TargetName="Bd" Property="BorderBrush" Value="{DynamicResource Ubisam.Brush.Accent}"/>
|
||||
<Setter TargetName="Bd" Property="Effect">
|
||||
<Setter.Value>
|
||||
<DropShadowEffect Color="{DynamicResource Ubisam.Color.AccentGlow}"
|
||||
BlurRadius="6" ShadowDepth="0" Opacity="0.55"/>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Trigger>
|
||||
<MultiTrigger>
|
||||
<MultiTrigger.Conditions>
|
||||
<Condition Property="IsEnabled" Value="False"/>
|
||||
</MultiTrigger.Conditions>
|
||||
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource Ubisam.Brush.Input.DisabledBackground}"/>
|
||||
<Setter TargetName="Bd" Property="BorderBrush" Value="{DynamicResource Ubisam.Brush.Input.DisabledBorder}"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource Ubisam.Brush.Muted}"/>
|
||||
</MultiTrigger>
|
||||
<Trigger Property="IsReadOnly" Value="True">
|
||||
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource Ubisam.Brush.Input.DisabledBackground}"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<!-- ══ ComboBox ══ -->
|
||||
<Style x:Key="Ubisam.Style.ComboToggle" TargetType="ToggleButton">
|
||||
<Setter Property="Focusable" Value="False"/>
|
||||
<Setter Property="ClickMode" Value="Press"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="ToggleButton">
|
||||
<Border x:Name="Bd"
|
||||
Background="{DynamicResource Ubisam.Brush.Input.Background}"
|
||||
BorderBrush="{DynamicResource Ubisam.Brush.Input.Border}"
|
||||
BorderThickness="1"
|
||||
SnapsToDevicePixels="True">
|
||||
<Path x:Name="Arrow"
|
||||
Data="M 0,0 L 5,5 L 10,0"
|
||||
Stroke="{DynamicResource Ubisam.Brush.Sidebar.Foreground}"
|
||||
StrokeThickness="1.5"
|
||||
StrokeStartLineCap="Round"
|
||||
StrokeEndLineCap="Round"
|
||||
StrokeLineJoin="Round"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Center"
|
||||
Margin="0,0,11,0"/>
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="Bd" Property="BorderBrush" Value="{DynamicResource Ubisam.Brush.Input.BorderHover}"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsChecked" Value="True">
|
||||
<Setter TargetName="Bd" Property="BorderBrush" Value="{DynamicResource Ubisam.Brush.Accent}"/>
|
||||
<Setter TargetName="Arrow" Property="Stroke" Value="{DynamicResource Ubisam.Brush.Accent}"/>
|
||||
<Setter TargetName="Arrow" Property="RenderTransformOrigin" Value="0.5,0.5"/>
|
||||
<Setter TargetName="Arrow" Property="RenderTransform">
|
||||
<Setter.Value>
|
||||
<ScaleTransform ScaleY="-1"/>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Trigger>
|
||||
<Trigger Property="IsEnabled" Value="False">
|
||||
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource Ubisam.Brush.Input.DisabledBackground}"/>
|
||||
<Setter TargetName="Bd" Property="BorderBrush" Value="{DynamicResource Ubisam.Brush.Input.DisabledBorder}"/>
|
||||
<Setter TargetName="Arrow" Property="Stroke" Value="{DynamicResource Ubisam.Brush.Muted}"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style TargetType="ComboBoxItem">
|
||||
<Setter Property="Padding" Value="11,8"/>
|
||||
<Setter Property="FontFamily" Value="{DynamicResource Ubisam.Font.Body}"/>
|
||||
<Setter Property="FontSize" Value="13.5"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource Ubisam.Brush.ContentForeground}"/>
|
||||
<Setter Property="Cursor" Value="Hand"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="ComboBoxItem">
|
||||
<Border x:Name="Bd" Background="Transparent" Padding="{TemplateBinding Padding}">
|
||||
<ContentPresenter VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource Ubisam.Brush.Sidebar.SelectedBackground}"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource Ubisam.Brush.Sidebar.SelectedForeground}"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsSelected" Value="True">
|
||||
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource Ubisam.Brush.Accent.Fill}"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource Ubisam.Brush.Accent.OnFill}"/>
|
||||
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style TargetType="ComboBox">
|
||||
<Setter Property="MinHeight" Value="34"/>
|
||||
<Setter Property="Padding" Value="10,0,30,0"/>
|
||||
<Setter Property="FontFamily" Value="{DynamicResource Ubisam.Font.Body}"/>
|
||||
<Setter Property="FontSize" Value="13.5"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource Ubisam.Brush.ContentForeground}"/>
|
||||
<Setter Property="Cursor" Value="Hand"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="ComboBox">
|
||||
<Grid>
|
||||
<ToggleButton x:Name="ToggleButton"
|
||||
Style="{StaticResource Ubisam.Style.ComboToggle}"
|
||||
IsChecked="{Binding IsDropDownOpen, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}"/>
|
||||
<ContentPresenter x:Name="ContentSite"
|
||||
Content="{TemplateBinding SelectionBoxItem}"
|
||||
ContentTemplate="{TemplateBinding SelectionBoxItemTemplate}"
|
||||
ContentStringFormat="{TemplateBinding SelectionBoxItemStringFormat}"
|
||||
Margin="{TemplateBinding Padding}"
|
||||
VerticalAlignment="Center"
|
||||
HorizontalAlignment="Left"
|
||||
IsHitTestVisible="False"/>
|
||||
<TextBox x:Name="PART_EditableTextBox"
|
||||
Style="{x:Null}"
|
||||
Visibility="Collapsed"
|
||||
Margin="{TemplateBinding Padding}"
|
||||
Background="Transparent"
|
||||
BorderThickness="0"/>
|
||||
<Popup x:Name="Popup"
|
||||
Placement="Bottom"
|
||||
IsOpen="{TemplateBinding IsDropDownOpen}"
|
||||
AllowsTransparency="True"
|
||||
Focusable="False"
|
||||
PopupAnimation="Fade">
|
||||
<Border MinWidth="{TemplateBinding ActualWidth}"
|
||||
MaxHeight="{TemplateBinding MaxDropDownHeight}"
|
||||
Background="{DynamicResource Ubisam.Brush.Input.Background}"
|
||||
BorderBrush="{DynamicResource Ubisam.Brush.Input.Border}"
|
||||
BorderThickness="1,0,1,1"
|
||||
Effect="{DynamicResource Ubisam.Effect.Popup}">
|
||||
<ScrollViewer>
|
||||
<ItemsPresenter/>
|
||||
</ScrollViewer>
|
||||
</Border>
|
||||
</Popup>
|
||||
</Grid>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsEditable" Value="True">
|
||||
<Setter TargetName="PART_EditableTextBox" Property="Visibility" Value="Visible"/>
|
||||
<Setter TargetName="ContentSite" Property="Visibility" Value="Collapsed"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsEnabled" Value="False">
|
||||
<Setter Property="Foreground" Value="{DynamicResource Ubisam.Brush.Muted}"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<!-- ══ CheckBox ══ -->
|
||||
<Style TargetType="CheckBox">
|
||||
<Setter Property="FontFamily" Value="{DynamicResource Ubisam.Font.Body}"/>
|
||||
<Setter Property="FontSize" Value="13.5"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource Ubisam.Brush.ContentForeground}"/>
|
||||
<Setter Property="Cursor" Value="Hand"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="CheckBox">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<Border x:Name="Box"
|
||||
Width="17" Height="17"
|
||||
Background="{DynamicResource Ubisam.Brush.Input.Background}"
|
||||
BorderBrush="{DynamicResource Ubisam.Brush.Input.Border}"
|
||||
BorderThickness="1"
|
||||
SnapsToDevicePixels="True"
|
||||
VerticalAlignment="Center">
|
||||
<Path x:Name="Check"
|
||||
Data="M 1,6 L 5,10 L 12,2"
|
||||
Stroke="{DynamicResource Ubisam.Brush.Accent.OnFill}"
|
||||
StrokeThickness="2"
|
||||
StrokeStartLineCap="Round"
|
||||
StrokeEndLineCap="Round"
|
||||
StrokeLineJoin="Round"
|
||||
Visibility="Collapsed"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
<ContentPresenter Margin="9,0,0,0" VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="Box" Property="BorderBrush" Value="{DynamicResource Ubisam.Brush.Input.BorderHover}"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsChecked" Value="True">
|
||||
<Setter TargetName="Box" Property="Background" Value="{DynamicResource Ubisam.Brush.Accent.Fill}"/>
|
||||
<Setter TargetName="Box" Property="BorderBrush" Value="{DynamicResource Ubisam.Brush.Accent.Fill}"/>
|
||||
<Setter TargetName="Check" Property="Visibility" Value="Visible"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsEnabled" Value="False">
|
||||
<Setter TargetName="Box" Property="Background" Value="{DynamicResource Ubisam.Brush.Input.DisabledBackground}"/>
|
||||
<Setter TargetName="Box" Property="BorderBrush" Value="{DynamicResource Ubisam.Brush.Input.DisabledBorder}"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource Ubisam.Brush.Muted}"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<!-- ══ Button ══
|
||||
암시적 = 보조(외곽선). 주 동작에는 Style="{StaticResource Ubisam.Style.Button.Primary}" -->
|
||||
<Style TargetType="Button">
|
||||
<Setter Property="MinHeight" Value="34"/>
|
||||
<Setter Property="Padding" Value="20,8"/>
|
||||
<Setter Property="FontFamily" Value="{DynamicResource Ubisam.Font.Body}"/>
|
||||
<Setter Property="FontSize" Value="13.5"/>
|
||||
<Setter Property="FontWeight" Value="Medium"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource Ubisam.Brush.Sidebar.Foreground}"/>
|
||||
<Setter Property="Cursor" Value="Hand"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Border x:Name="Bd"
|
||||
Background="{DynamicResource Ubisam.Brush.Input.Background}"
|
||||
BorderBrush="{DynamicResource Ubisam.Brush.Input.Border}"
|
||||
BorderThickness="1"
|
||||
Padding="{TemplateBinding Padding}"
|
||||
SnapsToDevicePixels="True">
|
||||
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource Ubisam.Brush.Sidebar.SelectedBackground}"/>
|
||||
<Setter TargetName="Bd" Property="BorderBrush" Value="{DynamicResource Ubisam.Brush.Accent}"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource Ubisam.Brush.Sidebar.SelectedForeground}"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsPressed" Value="True">
|
||||
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource Ubisam.Brush.Sidebar.HoverBackground}"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsEnabled" Value="False">
|
||||
<Setter TargetName="Bd" Property="Opacity" Value="0.45"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style x:Key="Ubisam.Style.Button.Primary" TargetType="Button" BasedOn="{StaticResource {x:Type Button}}">
|
||||
<Setter Property="Foreground" Value="{DynamicResource Ubisam.Brush.Accent.OnFill}"/>
|
||||
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Border x:Name="Bd"
|
||||
Background="{DynamicResource Ubisam.Brush.Accent.Fill}"
|
||||
BorderThickness="0"
|
||||
Padding="{TemplateBinding Padding}"
|
||||
SnapsToDevicePixels="True">
|
||||
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="Bd" Property="Opacity" Value="0.88"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsPressed" Value="True">
|
||||
<Setter TargetName="Bd" Property="Opacity" Value="0.75"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsEnabled" Value="False">
|
||||
<Setter TargetName="Bd" Property="Opacity" Value="0.45"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style x:Key="Ubisam.Style.Button.Ghost" TargetType="Button" BasedOn="{StaticResource {x:Type Button}}">
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Border x:Name="Bd" Background="Transparent" Padding="{TemplateBinding Padding}">
|
||||
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource Ubisam.Brush.Sidebar.HoverBackground}"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource Ubisam.Brush.Sidebar.SelectedForeground}"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<!-- ══ 일반 ListBox (설정 키 목록 등) ══ -->
|
||||
<Style TargetType="ListBoxItem">
|
||||
<Setter Property="Padding" Value="11,8"/>
|
||||
<Setter Property="FontFamily" Value="{DynamicResource Ubisam.Font.Body}"/>
|
||||
<Setter Property="FontSize" Value="13.5"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource Ubisam.Brush.ContentForeground}"/>
|
||||
<Setter Property="Cursor" Value="Hand"/>
|
||||
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="ListBoxItem">
|
||||
<Border x:Name="Bd"
|
||||
Background="Transparent"
|
||||
BorderThickness="2,0,0,0"
|
||||
BorderBrush="Transparent"
|
||||
Padding="{TemplateBinding Padding}">
|
||||
<ContentPresenter VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource Ubisam.Brush.Sidebar.HoverBackground}"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsSelected" Value="True">
|
||||
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource Ubisam.Brush.Sidebar.SelectedBackground}"/>
|
||||
<Setter TargetName="Bd" Property="BorderBrush" Value="{DynamicResource Ubisam.Brush.Accent}"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource Ubisam.Brush.Sidebar.SelectedForeground}"/>
|
||||
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style TargetType="ListBox">
|
||||
<Setter Property="Background" Value="{DynamicResource Ubisam.Brush.Input.Background}"/>
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource Ubisam.Brush.Input.Border}"/>
|
||||
<Setter Property="BorderThickness" Value="1"/>
|
||||
<Setter Property="Padding" Value="0,4"/>
|
||||
</Style>
|
||||
|
||||
<!-- ══ ListView + GridView (로그 표) ══ -->
|
||||
<Style TargetType="GridViewColumnHeader">
|
||||
<Setter Property="Padding" Value="11,9"/>
|
||||
<Setter Property="FontFamily" Value="{DynamicResource Ubisam.Font.Body}"/>
|
||||
<Setter Property="FontSize" Value="11"/>
|
||||
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource Ubisam.Brush.Sidebar.Foreground}"/>
|
||||
<Setter Property="HorizontalContentAlignment" Value="Left"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="GridViewColumnHeader">
|
||||
<Border x:Name="Bd"
|
||||
Background="{DynamicResource Ubisam.Brush.Table.HeaderBackground}"
|
||||
BorderBrush="{DynamicResource Ubisam.Brush.Input.Border}"
|
||||
BorderThickness="0,0,1,1"
|
||||
Padding="{TemplateBinding Padding}">
|
||||
<ContentPresenter VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource Ubisam.Brush.Sidebar.HoverBackground}"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style x:Key="Ubisam.Style.TableRow" TargetType="ListViewItem">
|
||||
<Setter Property="Padding" Value="0"/>
|
||||
<Setter Property="FontFamily" Value="{DynamicResource Ubisam.Font.Body}"/>
|
||||
<Setter Property="FontSize" Value="12.5"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource Ubisam.Brush.ContentForeground}"/>
|
||||
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="ListViewItem">
|
||||
<Border x:Name="Bd"
|
||||
Background="Transparent"
|
||||
BorderBrush="{DynamicResource Ubisam.Brush.Table.RowBorder}"
|
||||
BorderThickness="0,0,0,1"
|
||||
Padding="0,4">
|
||||
<GridViewRowPresenter Content="{TemplateBinding Content}"
|
||||
Columns="{TemplateBinding GridView.ColumnCollection}"
|
||||
VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource Ubisam.Brush.Sidebar.HoverBackground}"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsSelected" Value="True">
|
||||
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource Ubisam.Brush.Sidebar.SelectedBackground}"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource Ubisam.Brush.Sidebar.SelectedForeground}"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style TargetType="ListView">
|
||||
<Setter Property="Background" Value="{DynamicResource Ubisam.Brush.Input.Background}"/>
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource Ubisam.Brush.Input.Border}"/>
|
||||
<Setter Property="BorderThickness" Value="1"/>
|
||||
<Setter Property="ItemContainerStyle" Value="{StaticResource Ubisam.Style.TableRow}"/>
|
||||
<Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Auto"/>
|
||||
</Style>
|
||||
|
||||
<!-- ══ DataGrid ══
|
||||
ListView+GridView가 읽기 전용 표라면 DataGrid는 편집 가능한 격자다.
|
||||
셀 구분선은 세로만 그리고(가로는 행 경계가 담당), 선택은 행 머리의 accent 바 + 행 틴트로 알린다. -->
|
||||
|
||||
<!-- 정렬 화살표 -->
|
||||
<Geometry x:Key="Ubisam.Geometry.SortAsc">M1,6 L5.5,1 L10,6</Geometry>
|
||||
<Geometry x:Key="Ubisam.Geometry.SortDesc">M1,1 L5.5,6 L10,1</Geometry>
|
||||
|
||||
<Style TargetType="DataGridColumnHeader">
|
||||
<Setter Property="Padding" Value="11,10"/>
|
||||
<Setter Property="FontFamily" Value="{DynamicResource Ubisam.Font.Body}"/>
|
||||
<Setter Property="FontSize" Value="11"/>
|
||||
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource Ubisam.Brush.Sidebar.Foreground}"/>
|
||||
<Setter Property="HorizontalContentAlignment" Value="Left"/>
|
||||
<Setter Property="VerticalContentAlignment" Value="Center"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="DataGridColumnHeader">
|
||||
<Grid>
|
||||
<Border x:Name="Bd"
|
||||
Background="{DynamicResource Ubisam.Brush.Table.HeaderBackground}"
|
||||
BorderBrush="{DynamicResource Ubisam.Brush.Table.GridLine}"
|
||||
BorderThickness="0,0,1,0"
|
||||
Padding="{TemplateBinding Padding}">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<ContentPresenter Grid.Column="0"
|
||||
VerticalAlignment="Center"
|
||||
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"/>
|
||||
<Path x:Name="SortArrow"
|
||||
Grid.Column="1"
|
||||
Margin="7,0,0,0"
|
||||
Data="{StaticResource Ubisam.Geometry.SortDesc}"
|
||||
Stroke="{DynamicResource Ubisam.Brush.Accent}"
|
||||
StrokeThickness="2"
|
||||
StrokeStartLineCap="Round"
|
||||
StrokeEndLineCap="Round"
|
||||
StrokeLineJoin="Round"
|
||||
VerticalAlignment="Center"
|
||||
Visibility="Collapsed"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
<Thumb x:Name="PART_RightHeaderGripper"
|
||||
HorizontalAlignment="Right"
|
||||
Width="6"
|
||||
Cursor="SizeWE"
|
||||
Opacity="0">
|
||||
<Thumb.Template>
|
||||
<ControlTemplate TargetType="Thumb">
|
||||
<Border Background="Transparent"/>
|
||||
</ControlTemplate>
|
||||
</Thumb.Template>
|
||||
</Thumb>
|
||||
</Grid>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource Ubisam.Brush.Sidebar.HoverBackground}"/>
|
||||
</Trigger>
|
||||
<Trigger Property="SortDirection" Value="Ascending">
|
||||
<Setter TargetName="SortArrow" Property="Visibility" Value="Visible"/>
|
||||
<Setter TargetName="SortArrow" Property="Data" Value="{StaticResource Ubisam.Geometry.SortAsc}"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource Ubisam.Brush.Sidebar.SelectedForeground}"/>
|
||||
</Trigger>
|
||||
<Trigger Property="SortDirection" Value="Descending">
|
||||
<Setter TargetName="SortArrow" Property="Visibility" Value="Visible"/>
|
||||
<Setter TargetName="SortArrow" Property="Data" Value="{StaticResource Ubisam.Geometry.SortDesc}"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource Ubisam.Brush.Sidebar.SelectedForeground}"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<!-- 행 머리: 폭 26, 선택 시 accent 바가 채워진다 -->
|
||||
<Style TargetType="DataGridRowHeader">
|
||||
<Setter Property="Width" Value="26"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="DataGridRowHeader">
|
||||
<Border x:Name="Bd"
|
||||
Background="Transparent"
|
||||
BorderBrush="{DynamicResource Ubisam.Brush.Table.GridLine}"
|
||||
BorderThickness="0,0,1,0"/>
|
||||
<ControlTemplate.Triggers>
|
||||
<DataTrigger Binding="{Binding IsSelected, RelativeSource={RelativeSource AncestorType=DataGridRow}}" Value="True">
|
||||
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource Ubisam.Brush.Accent}"/>
|
||||
</DataTrigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style TargetType="DataGridCell">
|
||||
<Setter Property="Padding" Value="11,9"/>
|
||||
<Setter Property="BorderThickness" Value="0"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource Ubisam.Brush.ContentForeground}"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="DataGridCell">
|
||||
<Border x:Name="Bd"
|
||||
Background="Transparent"
|
||||
BorderBrush="{DynamicResource Ubisam.Brush.Table.RowBorder}"
|
||||
BorderThickness="0,0,1,0"
|
||||
Padding="{TemplateBinding Padding}">
|
||||
<ContentPresenter VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<!-- 편집 중인 셀만 accent 테두리 + 내부 여백을 줄여 입력칸을 보여준다 -->
|
||||
<Trigger Property="IsEditing" Value="True">
|
||||
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource Ubisam.Brush.Input.Background}"/>
|
||||
<Setter TargetName="Bd" Property="BorderBrush" Value="{DynamicResource Ubisam.Brush.Accent}"/>
|
||||
<Setter TargetName="Bd" Property="BorderThickness" Value="1"/>
|
||||
<Setter TargetName="Bd" Property="Padding" Value="6,4"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style TargetType="DataGridRow">
|
||||
<Setter Property="Background" Value="Transparent"/>
|
||||
<Setter Property="FontFamily" Value="{DynamicResource Ubisam.Font.Body}"/>
|
||||
<Setter Property="FontSize" Value="12.5"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource Ubisam.Brush.ContentForeground}"/>
|
||||
<Style.Triggers>
|
||||
<Trigger Property="AlternationIndex" Value="1">
|
||||
<Setter Property="Background" Value="{DynamicResource Ubisam.Brush.Table.AltRowBackground}"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Background" Value="{DynamicResource Ubisam.Brush.Sidebar.HoverBackground}"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsSelected" Value="True">
|
||||
<Setter Property="Background" Value="{DynamicResource Ubisam.Brush.Sidebar.SelectedBackground}"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource Ubisam.Brush.Sidebar.SelectedForeground}"/>
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
|
||||
<Style TargetType="DataGrid">
|
||||
<Setter Property="Background" Value="{DynamicResource Ubisam.Brush.Input.Background}"/>
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource Ubisam.Brush.Input.Border}"/>
|
||||
<Setter Property="BorderThickness" Value="1"/>
|
||||
<Setter Property="RowBackground" Value="Transparent"/>
|
||||
<Setter Property="AlternatingRowBackground" Value="{DynamicResource Ubisam.Brush.Table.AltRowBackground}"/>
|
||||
<Setter Property="RowHeight" Value="34"/>
|
||||
<Setter Property="ColumnHeaderHeight" Value="34"/>
|
||||
<Setter Property="GridLinesVisibility" Value="Horizontal"/>
|
||||
<Setter Property="HorizontalGridLinesBrush" Value="{DynamicResource Ubisam.Brush.Table.RowBorder}"/>
|
||||
<Setter Property="VerticalGridLinesBrush" Value="{DynamicResource Ubisam.Brush.Table.RowBorder}"/>
|
||||
<Setter Property="HeadersVisibility" Value="All"/>
|
||||
<Setter Property="SelectionUnit" Value="FullRow"/>
|
||||
<Setter Property="GridLinesVisibility" Value="All"/>
|
||||
<Setter Property="AutoGenerateColumns" Value="False"/>
|
||||
<Setter Property="CanUserAddRows" Value="False"/>
|
||||
<Setter Property="CanUserDeleteRows" Value="False"/>
|
||||
<Setter Property="CanUserResizeRows" Value="False"/>
|
||||
<Setter Property="EnableRowVirtualization" Value="True"/>
|
||||
</Style>
|
||||
|
||||
<!-- 수치 열에 붙일 셀 스타일: 우측 정렬 + 자릿수 고정 -->
|
||||
<Style x:Key="Ubisam.Style.NumericCell" TargetType="DataGridCell" BasedOn="{StaticResource {x:Type DataGridCell}}">
|
||||
<Setter Property="HorizontalAlignment" Value="Stretch"/>
|
||||
<Setter Property="HorizontalContentAlignment" Value="Right"/>
|
||||
<Setter Property="TextBlock.TextAlignment" Value="Right"/>
|
||||
</Style>
|
||||
|
||||
<!-- ══ 상태 태그 ══
|
||||
표의 판정·레벨 같은 상태 열은 맨 텍스트가 아니라 태그로 그린다 (디자인 시스템 규칙).
|
||||
DataGridTemplateColumn / GridViewColumn의 CellTemplate 안에서 쓴다. -->
|
||||
<Style x:Key="Ubisam.Style.Tag" TargetType="Border">
|
||||
<Setter Property="Background" Value="{DynamicResource Ubisam.Brush.Tag.NeutralBackground}"/>
|
||||
<Setter Property="CornerRadius" Value="3"/>
|
||||
<Setter Property="Padding" Value="10,3"/>
|
||||
<Setter Property="HorizontalAlignment" Value="Left"/>
|
||||
<Setter Property="VerticalAlignment" Value="Center"/>
|
||||
</Style>
|
||||
|
||||
<Style x:Key="Ubisam.Style.TagText" TargetType="TextBlock">
|
||||
<Setter Property="FontFamily" Value="{DynamicResource Ubisam.Font.Body}"/>
|
||||
<Setter Property="FontSize" Value="11"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource Ubisam.Brush.Tag.NeutralForeground}"/>
|
||||
</Style>
|
||||
|
||||
<!-- 정상·완료: accent 옅은 채움 -->
|
||||
<Style x:Key="Ubisam.Style.Tag.Accent" TargetType="Border" BasedOn="{StaticResource Ubisam.Style.Tag}">
|
||||
<Setter Property="Background" Value="{DynamicResource Ubisam.Brush.Tag.AccentBackground}"/>
|
||||
</Style>
|
||||
<Style x:Key="Ubisam.Style.TagText.Accent" TargetType="TextBlock" BasedOn="{StaticResource Ubisam.Style.TagText}">
|
||||
<Setter Property="Foreground" Value="{DynamicResource Ubisam.Brush.Tag.AccentForeground}"/>
|
||||
</Style>
|
||||
|
||||
<!-- 주의·재검: 외곽선만 (예외 행이 한눈에 걸리도록) -->
|
||||
<Style x:Key="Ubisam.Style.Tag.Outline" TargetType="Border" BasedOn="{StaticResource Ubisam.Style.Tag}">
|
||||
<Setter Property="Background" Value="Transparent"/>
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource Ubisam.Brush.Accent}"/>
|
||||
<Setter Property="BorderThickness" Value="1"/>
|
||||
</Style>
|
||||
<Style x:Key="Ubisam.Style.TagText.Outline" TargetType="TextBlock" BasedOn="{StaticResource Ubisam.Style.TagText}">
|
||||
<Setter Property="Foreground" Value="{DynamicResource Ubisam.Brush.Accent}"/>
|
||||
</Style>
|
||||
|
||||
<!-- ══ Slider ══
|
||||
트랙은 얇은 홈, 채워진 구간만 accent. 썸은 각진 세로 막대(다이얼이 아니라 계기 슬라이드). -->
|
||||
<Style x:Key="Ubisam.Style.SliderThumb" TargetType="Thumb">
|
||||
<Setter Property="Focusable" Value="False"/>
|
||||
<Setter Property="Cursor" Value="Hand"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="Thumb">
|
||||
<Border x:Name="Bd"
|
||||
Width="10" Height="20"
|
||||
Background="{DynamicResource Ubisam.Brush.Accent.Fill}"
|
||||
BorderBrush="{DynamicResource Ubisam.Brush.Accent}"
|
||||
BorderThickness="1"
|
||||
SnapsToDevicePixels="True"/>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource Ubisam.Brush.Accent}"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsDragging" Value="True">
|
||||
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource Ubisam.Brush.Accent}"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style TargetType="Slider">
|
||||
<Setter Property="MinHeight" Value="24"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource Ubisam.Brush.Accent}"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="Slider">
|
||||
<Grid VerticalAlignment="Center">
|
||||
<!-- 트랙 홈 -->
|
||||
<Border Height="4"
|
||||
Background="{DynamicResource Ubisam.Brush.Input.DisabledBackground}"
|
||||
BorderBrush="{DynamicResource Ubisam.Brush.Input.Border}"
|
||||
BorderThickness="1"
|
||||
VerticalAlignment="Center"
|
||||
SnapsToDevicePixels="True"/>
|
||||
<Track x:Name="PART_Track">
|
||||
<Track.DecreaseRepeatButton>
|
||||
<RepeatButton Command="Slider.DecreaseLarge"
|
||||
Focusable="False"
|
||||
Height="4"
|
||||
VerticalAlignment="Center">
|
||||
<RepeatButton.Template>
|
||||
<ControlTemplate TargetType="RepeatButton">
|
||||
<!-- 채워진 구간 -->
|
||||
<Border Background="{DynamicResource Ubisam.Brush.Accent}"
|
||||
Height="4"
|
||||
VerticalAlignment="Center"
|
||||
SnapsToDevicePixels="True"/>
|
||||
</ControlTemplate>
|
||||
</RepeatButton.Template>
|
||||
</RepeatButton>
|
||||
</Track.DecreaseRepeatButton>
|
||||
<Track.IncreaseRepeatButton>
|
||||
<RepeatButton Command="Slider.IncreaseLarge" Focusable="False">
|
||||
<RepeatButton.Template>
|
||||
<ControlTemplate TargetType="RepeatButton">
|
||||
<Border Background="Transparent"/>
|
||||
</ControlTemplate>
|
||||
</RepeatButton.Template>
|
||||
</RepeatButton>
|
||||
</Track.IncreaseRepeatButton>
|
||||
<Track.Thumb>
|
||||
<Thumb Style="{StaticResource Ubisam.Style.SliderThumb}"/>
|
||||
</Track.Thumb>
|
||||
</Track>
|
||||
</Grid>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsEnabled" Value="False">
|
||||
<Setter Property="Opacity" Value="0.45"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<!-- ══ ScrollBar ══
|
||||
폭 10의 얇은 막대. 트랙은 비우고 썸만 회색 — hover에서 accent로. 화살표 버튼은 없앤다. -->
|
||||
<Style x:Key="Ubisam.Style.ScrollThumb" TargetType="Thumb">
|
||||
<Setter Property="Focusable" Value="False"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="Thumb">
|
||||
<Border x:Name="Bd"
|
||||
Background="{DynamicResource Ubisam.Brush.Input.BorderHover}"
|
||||
SnapsToDevicePixels="True"/>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource Ubisam.Brush.Accent}"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsDragging" Value="True">
|
||||
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource Ubisam.Brush.Accent}"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style TargetType="ScrollBar">
|
||||
<Setter Property="Background" Value="Transparent"/>
|
||||
<Setter Property="Width" Value="10"/>
|
||||
<Setter Property="MinWidth" Value="10"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="ScrollBar">
|
||||
<Grid Background="{TemplateBinding Background}">
|
||||
<Track x:Name="PART_Track" IsDirectionReversed="True">
|
||||
<Track.DecreaseRepeatButton>
|
||||
<RepeatButton Command="ScrollBar.PageUpCommand" Focusable="False">
|
||||
<RepeatButton.Template>
|
||||
<ControlTemplate TargetType="RepeatButton">
|
||||
<Border Background="Transparent"/>
|
||||
</ControlTemplate>
|
||||
</RepeatButton.Template>
|
||||
</RepeatButton>
|
||||
</Track.DecreaseRepeatButton>
|
||||
<Track.IncreaseRepeatButton>
|
||||
<RepeatButton Command="ScrollBar.PageDownCommand" Focusable="False">
|
||||
<RepeatButton.Template>
|
||||
<ControlTemplate TargetType="RepeatButton">
|
||||
<Border Background="Transparent"/>
|
||||
</ControlTemplate>
|
||||
</RepeatButton.Template>
|
||||
</RepeatButton>
|
||||
</Track.IncreaseRepeatButton>
|
||||
<Track.Thumb>
|
||||
<Thumb Style="{StaticResource Ubisam.Style.ScrollThumb}" Margin="3,0"/>
|
||||
</Track.Thumb>
|
||||
</Track>
|
||||
</Grid>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="Orientation" Value="Horizontal">
|
||||
<Setter Property="Height" Value="10"/>
|
||||
<Setter Property="MinHeight" Value="10"/>
|
||||
<Setter Property="Width" Value="Auto"/>
|
||||
<Setter Property="MinWidth" Value="0"/>
|
||||
<Setter TargetName="PART_Track" Property="IsDirectionReversed" Value="False"/>
|
||||
<Setter TargetName="PART_Track" Property="Orientation" Value="Horizontal"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
</ResourceDictionary>
|
||||
107
.design/code/UbisamBase.Core/Themes/Icons.xaml
Normal file
@ -0,0 +1,107 @@
|
||||
<!-- Ubisam Base 아이콘 세트 — Lucide(https://lucide.dev) 원본 경로를 WPF Geometry 문법으로 옮긴 것.
|
||||
24×24 좌표계, 스트로크 렌더링(StrokeThickness 1.5, 둥근 캡). Path의 Fill은 비우고 Stroke만 쓴다.
|
||||
SVG의 <circle>/<rect>/<ellipse>는 호(A)와 직선으로 변환돼 있다.
|
||||
아이콘을 추가할 때는 Lucide에서 같은 규칙(24px, 1.5)으로 가져와 여기에 Geometry 한 줄로 등록하고,
|
||||
AddMain의 icon 인자에 키 뒷부분("Home" 등)을 넘긴다. -->
|
||||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
|
||||
<!-- 홈 · lucide/house -->
|
||||
<Geometry x:Key="Ubisam.Icon.Home">M15 21v-8a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v8 M3 10a2 2 0 0 1 .709-1.528l7-5.999a2 2 0 0 1 2.582 0l7 5.999A2 2 0 0 1 21 10v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z</Geometry>
|
||||
|
||||
<!-- 모니터링·그래프 · lucide/activity -->
|
||||
<Geometry x:Key="Ubisam.Icon.Monitor">M22 12h-2.48a2 2 0 0 0-1.93 1.46l-2.35 8.36a.25.25 0 0 1-.48 0L9.24 2.18a.25.25 0 0 0-.48 0l-2.35 8.36A2 2 0 0 1 4.49 12H2</Geometry>
|
||||
|
||||
<!-- 통계·리포트 · lucide/chart-column -->
|
||||
<Geometry x:Key="Ubisam.Icon.Chart">M3 3v16a2 2 0 0 0 2 2h16 M18 17V9 M13 17V5 M8 17v-3</Geometry>
|
||||
|
||||
<!-- 유지보수 · lucide/clipboard-check -->
|
||||
<Geometry x:Key="Ubisam.Icon.Maintenance">M9,2 H15 A1,1 0 0 1 16,3 V5 A1,1 0 0 1 15,6 H9 A1,1 0 0 1 8,5 V3 A1,1 0 0 1 9,2 Z M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2 m9 14 2 2 4-4</Geometry>
|
||||
|
||||
<!-- 설정 · lucide/sliders-horizontal -->
|
||||
<Geometry x:Key="Ubisam.Icon.Settings">M21 4h-7 M10 4H3 M21 12h-9 M8 12H3 M21 20h-5 M12 20H3 M14 2v4 M8 10v4 M16 18v4</Geometry>
|
||||
|
||||
<!-- 알람 · lucide/bell -->
|
||||
<Geometry x:Key="Ubisam.Icon.Alarm">M10.268 21a2 2 0 0 0 3.464 0 M3.262 15.326A1 1 0 0 0 4 17h16a1 1 0 0 0 .74-1.673C19.41 13.956 18 12.499 18 8A6 6 0 0 0 6 8c0 4.499-1.411 5.956-2.738 7.326</Geometry>
|
||||
|
||||
<!-- 로그 · lucide/list -->
|
||||
<Geometry x:Key="Ubisam.Icon.Log">M3 5h.01 M3 12h.01 M3 19h.01 M8 5h13 M8 12h13 M8 19h13</Geometry>
|
||||
|
||||
<!-- 문서·리포트 · lucide/file-text -->
|
||||
<Geometry x:Key="Ubisam.Icon.Document">M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7z M14 2v4a2 2 0 0 0 2 2h4 M10 9H8 M16 13H8 M16 17H8</Geometry>
|
||||
|
||||
<!-- 데이터 · lucide/database -->
|
||||
<Geometry x:Key="Ubisam.Icon.Data">M3,5 A9,3 0 0 1 21,5 A9,3 0 0 1 3,5 Z M3 5V19A9 3 0 0 0 21 19V5 M3 12A9 3 0 0 0 21 12</Geometry>
|
||||
|
||||
<!-- 전원 · lucide/power -->
|
||||
<Geometry x:Key="Ubisam.Icon.Power">M12 2v10 M18.4 6.6a9 9 0 1 1-12.77.04</Geometry>
|
||||
|
||||
<!-- 온도 · lucide/thermometer -->
|
||||
<Geometry x:Key="Ubisam.Icon.Temperature">M14 4v10.54a4 4 0 1 1-4 0V4a2 2 0 0 1 4 0z</Geometry>
|
||||
|
||||
<!-- 압력·게이지 · lucide/gauge -->
|
||||
<Geometry x:Key="Ubisam.Icon.Gauge">m12 14 4-4 M3.34 19a10 10 0 1 1 17.32 0</Geometry>
|
||||
|
||||
<!-- 운전 · lucide/play -->
|
||||
<Geometry x:Key="Ubisam.Icon.Run">M6 3l14 9-14 9z</Geometry>
|
||||
|
||||
<!-- 정지 · lucide/square -->
|
||||
<Geometry x:Key="Ubisam.Icon.Stop">M5,3 H19 A2,2 0 0 1 21,5 V19 A2,2 0 0 1 19,21 H5 A2,2 0 0 1 3,19 V5 A2,2 0 0 1 5,3 Z</Geometry>
|
||||
|
||||
<!-- 리셋 · lucide/rotate-cw -->
|
||||
<Geometry x:Key="Ubisam.Icon.Reset">M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8 M21 3v5h-5</Geometry>
|
||||
|
||||
<!-- 계정 · lucide/user -->
|
||||
<Geometry x:Key="Ubisam.Icon.Account">M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2 M8,7 A4,4 0 0 1 16,7 A4,4 0 0 1 8,7 Z</Geometry>
|
||||
|
||||
<!-- 권한·보안 · lucide/lock -->
|
||||
<Geometry x:Key="Ubisam.Icon.Lock">M5,11 H19 A2,2 0 0 1 21,13 V20 A2,2 0 0 1 19,22 H5 A2,2 0 0 1 3,20 V13 A2,2 0 0 1 5,11 Z M7 11V7a5 5 0 0 1 10 0v4</Geometry>
|
||||
|
||||
<!-- 내보내기 · lucide/download -->
|
||||
<Geometry x:Key="Ubisam.Icon.Export">M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4 M7 10l5 5 5-5 M12 15V3</Geometry>
|
||||
|
||||
<!-- 불러오기 · lucide/upload -->
|
||||
<Geometry x:Key="Ubisam.Icon.Import">M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4 M17 8l-5-5-5 5 M12 3v12</Geometry>
|
||||
|
||||
<!-- 검색 · lucide/search -->
|
||||
<Geometry x:Key="Ubisam.Icon.Search">m21 21-4.34-4.34 M3,11 A8,8 0 0 1 19,11 A8,8 0 0 1 3,11 Z</Geometry>
|
||||
|
||||
<!-- 일정·이력 · lucide/calendar -->
|
||||
<Geometry x:Key="Ubisam.Icon.Calendar">M8 2v4 M16 2v4 M5,4 H19 A2,2 0 0 1 21,6 V20 A2,2 0 0 1 19,22 H5 A2,2 0 0 1 3,20 V6 A2,2 0 0 1 5,4 Z M3 10h18</Geometry>
|
||||
|
||||
<!-- 시간·주기 · lucide/clock -->
|
||||
<Geometry x:Key="Ubisam.Icon.Clock">M2,12 A10,10 0 0 1 22,12 A10,10 0 0 1 2,12 Z M12 6v6l4 2</Geometry>
|
||||
|
||||
<!-- 통신·연결 · lucide/link -->
|
||||
<Geometry x:Key="Ubisam.Icon.Link">M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71 M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71</Geometry>
|
||||
|
||||
<!-- 경고 · lucide/triangle-alert -->
|
||||
<Geometry x:Key="Ubisam.Icon.Warning">m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3 M12 9v4 M12 17h.01</Geometry>
|
||||
|
||||
<!-- 검사 결과 · lucide/circle-check -->
|
||||
<Geometry x:Key="Ubisam.Icon.Pass">M2,12 A10,10 0 0 1 22,12 A10,10 0 0 1 2,12 Z m9 12 2 2 4-4</Geometry>
|
||||
|
||||
<!-- 파일·폴더 · lucide/folder -->
|
||||
<Geometry x:Key="Ubisam.Icon.Folder">M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2z</Geometry>
|
||||
|
||||
<!-- 비전·카메라 · lucide/camera -->
|
||||
<Geometry x:Key="Ubisam.Icon.Vision">M14.5 4h-5L7 7H4a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2h-3l-2.5-3z M9,13 A3,3 0 0 1 15,13 A3,3 0 0 1 9,13 Z</Geometry>
|
||||
|
||||
<!-- 제어·I/O · lucide/cpu -->
|
||||
<Geometry x:Key="Ubisam.Icon.Control">M6,4 H18 A2,2 0 0 1 20,6 V18 A2,2 0 0 1 18,20 H6 A2,2 0 0 1 4,18 V6 A2,2 0 0 1 6,4 Z M10,9 H14 A1,1 0 0 1 15,10 V14 A1,1 0 0 1 14,15 H10 A1,1 0 0 1 9,14 V10 A1,1 0 0 1 10,9 Z M15 2v2 M15 20v2 M2 15h2 M2 9h2 M20 15h2 M20 9h2 M9 2v2 M9 20v2</Geometry>
|
||||
|
||||
<!-- 레시피 · lucide/layers -->
|
||||
<Geometry x:Key="Ubisam.Icon.Recipe">M12.83 2.18a2 2 0 0 0-1.66 0L2.6 6.08a1 1 0 0 0 0 1.83l8.58 3.91a2 2 0 0 0 1.66 0l8.58-3.9a1 1 0 0 0 0-1.83z M2 12a1 1 0 0 0 .58.91l8.6 3.91a2 2 0 0 0 1.65 0l8.58-3.9A1 1 0 0 0 22 12 M2 17a1 1 0 0 0 .58.91l8.6 3.91a2 2 0 0 0 1.65 0l8.58-3.9A1 1 0 0 0 22 17</Geometry>
|
||||
|
||||
<!-- 모듈 목록 · lucide/layout-grid -->
|
||||
<Geometry x:Key="Ubisam.Icon.Modules">M4,3 H9 A1,1 0 0 1 10,4 V9 A1,1 0 0 1 9,10 H4 A1,1 0 0 1 3,9 V4 A1,1 0 0 1 4,3 Z M15,3 H20 A1,1 0 0 1 21,4 V9 A1,1 0 0 1 20,10 H15 A1,1 0 0 1 14,9 V4 A1,1 0 0 1 15,3 Z M15,14 H20 A1,1 0 0 1 21,15 V20 A1,1 0 0 1 20,21 H15 A1,1 0 0 1 14,20 V15 A1,1 0 0 1 15,14 Z M4,14 H9 A1,1 0 0 1 10,15 V20 A1,1 0 0 1 9,21 H4 A1,1 0 0 1 3,20 V15 A1,1 0 0 1 4,14 Z</Geometry>
|
||||
|
||||
<!-- 안내 · lucide/circle-info -->
|
||||
<Geometry x:Key="Ubisam.Icon.Info">M2,12 A10,10 0 0 1 22,12 A10,10 0 0 1 2,12 Z M12,16 L12,12 M12,7.99 L12,8.01</Geometry>
|
||||
|
||||
<!-- 확인 질문 · lucide/circle-help -->
|
||||
<Geometry x:Key="Ubisam.Icon.Question">M2,12 A10,10 0 0 1 22,12 A10,10 0 0 1 2,12 Z M9.09,9 A3,3 0 0 1 14.92,10 C14.92,12 11.92,13 11.92,13 M12,16.99 L12,17.01</Geometry>
|
||||
|
||||
<!-- 오류 · lucide/circle-x -->
|
||||
<Geometry x:Key="Ubisam.Icon.Error">M2,12 A10,10 0 0 1 22,12 A10,10 0 0 1 2,12 Z M15,9 L9,15 M9,9 L15,15</Geometry>
|
||||
</ResourceDictionary>
|
||||
41
.design/code/UbisamBase.Core/Themes/Logo.xaml
Normal file
@ -0,0 +1,41 @@
|
||||
<!-- 회사 로고 — 원본 마크(둥근 사각 타일 + 동심 아치 2개 + 점)를 벡터로 재구성.
|
||||
ShellWindow.xaml의 로고 자리에서 <Image Source="{StaticResource Ubisam.Logo}"/>로 쓰거나,
|
||||
단색 배경 위에 마크만 얹으려면 Ubisam.Logo.Mark(Geometry)와 Ubisam.Logo.Dot을 직접 그린다. -->
|
||||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
|
||||
<!-- 아치 2개 (스트로크로 그린다: StrokeThickness 6.2, 64x64 좌표계) -->
|
||||
<Geometry x:Key="Ubisam.Logo.Arcs">M20,32.5 A12.5,12.5 0 0 1 32.5,45 M20,20.5 A24.5,24.5 0 0 1 44.5,45</Geometry>
|
||||
<!-- 점 (채움) -->
|
||||
<Geometry x:Key="Ubisam.Logo.Dot">M15.1,45 A4.9,4.9 0 0 1 24.9,45 A4.9,4.9 0 0 1 15.1,45 Z</Geometry>
|
||||
|
||||
<LinearGradientBrush x:Key="Ubisam.Logo.TileBrush" StartPoint="0,0" EndPoint="1,1">
|
||||
<GradientStop Offset="0" Color="#FF56ADF7"/>
|
||||
<GradientStop Offset="1" Color="#FF1D82E8"/>
|
||||
</LinearGradientBrush>
|
||||
|
||||
<SolidColorBrush x:Key="Ubisam.Logo.MarkBrush" Color="#FFEDF2F4"/>
|
||||
|
||||
<DrawingImage x:Key="Ubisam.Logo">
|
||||
<DrawingImage.Drawing>
|
||||
<DrawingGroup>
|
||||
<!-- 둥근 사각 타일 -->
|
||||
<GeometryDrawing Brush="{StaticResource Ubisam.Logo.TileBrush}">
|
||||
<GeometryDrawing.Geometry>
|
||||
<RectangleGeometry Rect="0,0,64,64" RadiusX="15" RadiusY="15"/>
|
||||
</GeometryDrawing.Geometry>
|
||||
</GeometryDrawing>
|
||||
<!-- 아치 -->
|
||||
<GeometryDrawing Geometry="{StaticResource Ubisam.Logo.Arcs}">
|
||||
<GeometryDrawing.Pen>
|
||||
<Pen Brush="{StaticResource Ubisam.Logo.MarkBrush}" Thickness="6.2" StartLineCap="Flat" EndLineCap="Flat"/>
|
||||
</GeometryDrawing.Pen>
|
||||
</GeometryDrawing>
|
||||
<!-- 점 -->
|
||||
<GeometryDrawing Brush="{StaticResource Ubisam.Logo.MarkBrush}"
|
||||
Geometry="{StaticResource Ubisam.Logo.Dot}"/>
|
||||
</DrawingGroup>
|
||||
</DrawingImage.Drawing>
|
||||
</DrawingImage>
|
||||
|
||||
</ResourceDictionary>
|
||||
273
.design/code/UbisamBase.Core/Themes/ShellStyles.xaml
Normal file
@ -0,0 +1,273 @@
|
||||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:conv="clr-namespace:UbisamBase.Core.Converters"
|
||||
xmlns:sys="clr-namespace:System;assembly=mscorlib">
|
||||
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<ResourceDictionary Source="pack://application:,,,/UbisamBase.Core;component/Themes/Icons.xaml"/>
|
||||
<ResourceDictionary Source="pack://application:,,,/UbisamBase.Core;component/Themes/Controls.xaml"/>
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
|
||||
<conv:BoolToVisibilityConverter x:Key="BoolToVis"/>
|
||||
<conv:FirstCharacterConverter x:Key="FirstCharConverter"/>
|
||||
<conv:ToastTypeToBrushConverter x:Key="ToastTypeToBrush"/>
|
||||
<conv:IconKeyToGeometryConverter x:Key="IconToGeometry"/>
|
||||
|
||||
<!-- ComboBox 드롭다운 그림자 -->
|
||||
<DropShadowEffect x:Key="Ubisam.Effect.Popup" BlurRadius="12" ShadowDepth="3" Direction="270" Opacity="0.22" Color="#000000"/>
|
||||
|
||||
<!-- 타이포: Barlow가 설치돼 있으면 쓰고, 없으면 Segoe UI로 폴백 -->
|
||||
<FontFamily x:Key="Ubisam.Font.Heading">Barlow Condensed, Segoe UI</FontFamily>
|
||||
<FontFamily x:Key="Ubisam.Font.Body">Barlow, Segoe UI</FontFamily>
|
||||
|
||||
<!-- 좌/우측 네비게이션 아이템: 아이콘 위 + 라벨 아래, 선택 시 좌측 accent 바 -->
|
||||
<Style x:Key="Ubisam.Style.SidebarItem" TargetType="ListBoxItem">
|
||||
<Setter Property="Padding" Value="14,20"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource Ubisam.Brush.Sidebar.Foreground}"/>
|
||||
<Setter Property="Cursor" Value="Hand"/>
|
||||
<Setter Property="HorizontalContentAlignment" Value="Center"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="ListBoxItem">
|
||||
<Border x:Name="Bd"
|
||||
Background="Transparent"
|
||||
BorderThickness="3,0,0,0"
|
||||
BorderBrush="Transparent"
|
||||
Padding="{TemplateBinding Padding}">
|
||||
<StackPanel Orientation="Vertical" HorizontalAlignment="Center">
|
||||
<TextBlock Text="{Binding Icon}"
|
||||
FontFamily="Segoe Fluent Icons, Segoe MDL2 Assets"
|
||||
FontSize="26"
|
||||
HorizontalAlignment="Center"/>
|
||||
<TextBlock Text="{Binding Title}"
|
||||
FontSize="13"
|
||||
Margin="0,8,0,0"
|
||||
HorizontalAlignment="Center"
|
||||
TextWrapping="Wrap"
|
||||
TextAlignment="Center"
|
||||
MaxWidth="100"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsSelected" Value="True">
|
||||
<Setter TargetName="Bd" Property="BorderBrush" Value="{DynamicResource Ubisam.Brush.Accent}"/>
|
||||
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource Ubisam.Brush.Sidebar.SelectedBackground}"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource Ubisam.Brush.Sidebar.SelectedForeground}"/>
|
||||
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource Ubisam.Brush.Sidebar.HoverBackground}"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style x:Key="Ubisam.Style.SidebarBar.Vertical" TargetType="ListBox">
|
||||
<Setter Property="Background" Value="{DynamicResource Ubisam.Brush.Sidebar.Background}"/>
|
||||
<Setter Property="BorderThickness" Value="0,0,1,0"/>
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource Ubisam.Brush.SubTabBar.Border}"/>
|
||||
<Setter Property="Padding" Value="0,20"/>
|
||||
<Setter Property="ItemContainerStyle" Value="{StaticResource Ubisam.Style.SidebarItem}"/>
|
||||
<Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Disabled"/>
|
||||
<Setter Property="ScrollViewer.VerticalScrollBarVisibility" Value="Auto"/>
|
||||
<Setter Property="ItemsPanel">
|
||||
<Setter.Value>
|
||||
<ItemsPanelTemplate>
|
||||
<StackPanel Orientation="Vertical"/>
|
||||
</ItemsPanelTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<!-- 하단 네비게이션 아이템: 아이콘 위 + 라벨 아래. 칸 구분선도, 선택 배경 틴트도 없다.
|
||||
선택 표시는 아래 SidebarBar.Horizontal 템플릿의 인디케이터 바가 미끄러져 오면서 담당한다. -->
|
||||
<Style x:Key="Ubisam.Style.SidebarItem.Horizontal" TargetType="ListBoxItem">
|
||||
<Setter Property="Padding" Value="30,13,30,14"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource Ubisam.Brush.Sidebar.Foreground}"/>
|
||||
<Setter Property="FontFamily" Value="{StaticResource Ubisam.Font.Body}"/>
|
||||
<Setter Property="FontSize" Value="13.5"/>
|
||||
<Setter Property="FontWeight" Value="Medium"/>
|
||||
<Setter Property="Cursor" Value="Hand"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="ListBoxItem">
|
||||
<Border x:Name="Bd"
|
||||
Background="Transparent"
|
||||
Padding="{TemplateBinding Padding}">
|
||||
<StackPanel Orientation="Vertical" HorizontalAlignment="Center" VerticalAlignment="Center">
|
||||
<Path x:Name="Ico"
|
||||
Data="{Binding Icon, Converter={StaticResource IconToGeometry}}"
|
||||
Stroke="{Binding Foreground, RelativeSource={RelativeSource TemplatedParent}}"
|
||||
StrokeThickness="1.5"
|
||||
StrokeStartLineCap="Round"
|
||||
StrokeEndLineCap="Round"
|
||||
StrokeLineJoin="Round"
|
||||
Width="22" Height="22"
|
||||
Stretch="Uniform"
|
||||
Margin="0,0,0,7"
|
||||
HorizontalAlignment="Center"/>
|
||||
<TextBlock Text="{Binding Title}"
|
||||
HorizontalAlignment="Center"
|
||||
TextTrimming="CharacterEllipsis"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<!-- icon 인자를 생략한 탭은 아이콘 자리를 아예 접어 라벨만 남긴다 -->
|
||||
<DataTrigger Binding="{Binding Icon, Converter={StaticResource IconToGeometry}}" Value="{x:Null}">
|
||||
<Setter TargetName="Ico" Property="Visibility" Value="Collapsed"/>
|
||||
</DataTrigger>
|
||||
<Trigger Property="IsSelected" Value="True">
|
||||
<Setter Property="Foreground" Value="{DynamicResource Ubisam.Brush.Sidebar.SelectedForeground}"/>
|
||||
</Trigger>
|
||||
<MultiTrigger>
|
||||
<MultiTrigger.Conditions>
|
||||
<Condition Property="IsMouseOver" Value="True"/>
|
||||
<Condition Property="IsSelected" Value="False"/>
|
||||
</MultiTrigger.Conditions>
|
||||
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource Ubisam.Brush.Sidebar.HoverBackground}"/>
|
||||
</MultiTrigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<!-- 하단 탭 바: 탭을 가운데 모으고, 바닥에 accent 인디케이터를 하나 둔다.
|
||||
인디케이터의 위치·너비는 ShellWindow.xaml.cs가 선택 변경 시 애니메이션으로 옮긴다. -->
|
||||
<Style x:Key="Ubisam.Style.SidebarBar.Horizontal" TargetType="ListBox">
|
||||
<Setter Property="Background" Value="{DynamicResource Ubisam.Brush.Sidebar.Background}"/>
|
||||
<Setter Property="BorderThickness" Value="0,1,0,0"/>
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource Ubisam.Brush.SubTabBar.Border}"/>
|
||||
<Setter Property="Padding" Value="0"/>
|
||||
<Setter Property="ItemContainerStyle" Value="{StaticResource Ubisam.Style.SidebarItem.Horizontal}"/>
|
||||
<Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Disabled"/>
|
||||
<Setter Property="ScrollViewer.VerticalScrollBarVisibility" Value="Disabled"/>
|
||||
<Setter Property="ItemsPanel">
|
||||
<Setter.Value>
|
||||
<ItemsPanelTemplate>
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center"/>
|
||||
</ItemsPanelTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="ListBox">
|
||||
<Border Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}">
|
||||
<Grid>
|
||||
<ItemsPresenter/>
|
||||
<Border x:Name="SelectionIndicator"
|
||||
Height="3"
|
||||
Width="0"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Bottom"
|
||||
Background="{DynamicResource Ubisam.Brush.Accent}">
|
||||
<Border.RenderTransform>
|
||||
<TranslateTransform x:Name="SelectionIndicatorShift" X="0"/>
|
||||
</Border.RenderTransform>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<!-- 하단 서브탭: 각진 세그먼트(라운드 제거) -->
|
||||
<Style x:Key="Ubisam.Style.SubTabItem" TargetType="ListBoxItem">
|
||||
<Setter Property="Padding" Value="22,11"/>
|
||||
<Setter Property="FontSize" Value="14"/>
|
||||
<Setter Property="FontFamily" Value="{StaticResource Ubisam.Font.Body}"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource Ubisam.Brush.SubTabBar.Foreground}"/>
|
||||
<Setter Property="Cursor" Value="Hand"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="ListBoxItem">
|
||||
<Border x:Name="Bd"
|
||||
Background="Transparent"
|
||||
BorderThickness="1,0,0,0"
|
||||
BorderBrush="{DynamicResource Ubisam.Brush.SubTabBar.Border}"
|
||||
Padding="{TemplateBinding Padding}">
|
||||
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsSelected" Value="True">
|
||||
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource Ubisam.Brush.SubTabBar.SelectedBackground}"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource Ubisam.Brush.SubTabBar.SelectedForeground}"/>
|
||||
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource Ubisam.Brush.SubTabBar.HoverBackground}"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style x:Key="Ubisam.Style.SubTabBar" TargetType="ListBox">
|
||||
<Setter Property="Background" Value="{DynamicResource Ubisam.Brush.SubTabBar.Background}"/>
|
||||
<Setter Property="BorderThickness" Value="0,1,0,0"/>
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource Ubisam.Brush.SubTabBar.Border}"/>
|
||||
<Setter Property="Padding" Value="0"/>
|
||||
<Setter Property="ItemContainerStyle" Value="{StaticResource Ubisam.Style.SubTabItem}"/>
|
||||
<Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Auto"/>
|
||||
<Setter Property="ScrollViewer.VerticalScrollBarVisibility" Value="Disabled"/>
|
||||
<Setter Property="ItemsPanel">
|
||||
<Setter.Value>
|
||||
<ItemsPanelTemplate>
|
||||
<StackPanel Orientation="Horizontal"/>
|
||||
</ItemsPanelTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<!-- 상단바의 계정/바탕화면/종료 같은 유틸리티 버튼: 각진 헤어라인 박스 -->
|
||||
<Style x:Key="Ubisam.Style.ChromeButton" TargetType="Button">
|
||||
<Setter Property="Padding" Value="14,7"/>
|
||||
<Setter Property="FontSize" Value="13"/>
|
||||
<Setter Property="FontFamily" Value="{StaticResource Ubisam.Font.Body}"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource Ubisam.Brush.Sidebar.Foreground}"/>
|
||||
<Setter Property="Cursor" Value="Hand"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Border x:Name="Bd"
|
||||
Background="Transparent"
|
||||
CornerRadius="0"
|
||||
BorderThickness="1"
|
||||
BorderBrush="{DynamicResource Ubisam.Brush.SubTabBar.Border}"
|
||||
Padding="{TemplateBinding Padding}">
|
||||
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource Ubisam.Brush.Sidebar.HoverBackground}"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<!-- 빈 뷰 슬롯 배경(사선 해치). 모듈이 마운트되기 전 영역 표시용. -->
|
||||
<DrawingBrush x:Key="Ubisam.Brush.SlotHatch"
|
||||
TileMode="Tile"
|
||||
Viewport="0,0,9,9"
|
||||
ViewportUnits="Absolute">
|
||||
<DrawingBrush.Drawing>
|
||||
<GeometryDrawing>
|
||||
<GeometryDrawing.Pen>
|
||||
<Pen Thickness="1" Brush="{DynamicResource Ubisam.Brush.Slot.Hatch}"/>
|
||||
</GeometryDrawing.Pen>
|
||||
<GeometryDrawing.Geometry>
|
||||
<LineGeometry StartPoint="0,9" EndPoint="9,0"/>
|
||||
</GeometryDrawing.Geometry>
|
||||
</GeometryDrawing>
|
||||
</DrawingBrush.Drawing>
|
||||
</DrawingBrush>
|
||||
|
||||
</ResourceDictionary>
|
||||
BIN
.design/logo/Company/logo/UbisamBase.ico
Normal file
|
After Width: | Height: | Size: 77 KiB |
BIN
.design/logo/Company/logo/app-icon-128.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
.design/logo/Company/logo/app-icon-16.png
Normal file
|
After Width: | Height: | Size: 6.3 KiB |
BIN
.design/logo/Company/logo/app-icon-24.png
Normal file
|
After Width: | Height: | Size: 6.7 KiB |
BIN
.design/logo/Company/logo/app-icon-256.png
Normal file
|
After Width: | Height: | Size: 56 KiB |
BIN
.design/logo/Company/logo/app-icon-32.png
Normal file
|
After Width: | Height: | Size: 7.6 KiB |
BIN
.design/logo/Company/logo/app-icon-48.png
Normal file
|
After Width: | Height: | Size: 8.8 KiB |
BIN
.design/logo/Company/logo/app-icon-64.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
.design/logo/Company/logo/candidates/a-128.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
.design/logo/Company/logo/candidates/a-16.png
Normal file
|
After Width: | Height: | Size: 6.3 KiB |
BIN
.design/logo/Company/logo/candidates/a-24.png
Normal file
|
After Width: | Height: | Size: 6.7 KiB |
BIN
.design/logo/Company/logo/candidates/a-256.png
Normal file
|
After Width: | Height: | Size: 56 KiB |
BIN
.design/logo/Company/logo/candidates/a-32.png
Normal file
|
After Width: | Height: | Size: 7.6 KiB |
BIN
.design/logo/Company/logo/candidates/a-48.png
Normal file
|
After Width: | Height: | Size: 8.8 KiB |
BIN
.design/logo/Company/logo/candidates/a-64.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
.design/logo/Company/logo/candidates/b-128.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
.design/logo/Company/logo/candidates/b-16.png
Normal file
|
After Width: | Height: | Size: 6.2 KiB |
BIN
.design/logo/Company/logo/candidates/b-24.png
Normal file
|
After Width: | Height: | Size: 6.4 KiB |
BIN
.design/logo/Company/logo/candidates/b-256.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
.design/logo/Company/logo/candidates/b-32.png
Normal file
|
After Width: | Height: | Size: 6.9 KiB |
BIN
.design/logo/Company/logo/candidates/b-48.png
Normal file
|
After Width: | Height: | Size: 7.4 KiB |
BIN
.design/logo/Company/logo/candidates/b-64.png
Normal file
|
After Width: | Height: | Size: 8.0 KiB |
BIN
.design/logo/Company/logo/candidates/c-128.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
.design/logo/Company/logo/candidates/c-16.png
Normal file
|
After Width: | Height: | Size: 6.2 KiB |
BIN
.design/logo/Company/logo/candidates/c-24.png
Normal file
|
After Width: | Height: | Size: 6.5 KiB |
BIN
.design/logo/Company/logo/candidates/c-256.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
.design/logo/Company/logo/candidates/c-32.png
Normal file
|
After Width: | Height: | Size: 7.0 KiB |
BIN
.design/logo/Company/logo/candidates/c-48.png
Normal file
|
After Width: | Height: | Size: 7.7 KiB |
BIN
.design/logo/Company/logo/candidates/c-64.png
Normal file
|
After Width: | Height: | Size: 8.2 KiB |
BIN
.design/logo/Company/logo/candidates/d-128.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
.design/logo/Company/logo/candidates/d-16.png
Normal file
|
After Width: | Height: | Size: 6.1 KiB |
BIN
.design/logo/Company/logo/candidates/d-24.png
Normal file
|
After Width: | Height: | Size: 6.3 KiB |
BIN
.design/logo/Company/logo/candidates/d-256.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
.design/logo/Company/logo/candidates/d-32.png
Normal file
|
After Width: | Height: | Size: 6.8 KiB |
BIN
.design/logo/Company/logo/candidates/d-48.png
Normal file
|
After Width: | Height: | Size: 7.3 KiB |
BIN
.design/logo/Company/logo/candidates/d-64.png
Normal file
|
After Width: | Height: | Size: 7.8 KiB |
85
.design/logo/Company/logo/readme.md
Normal file
@ -0,0 +1,85 @@
|
||||
# 회사 로고 — 재구성본
|
||||
|
||||
원본(저해상도 PNG)의 구성을 그대로 따라 벡터로 다시 그렸습니다: 둥근 사각 타일 + 같은 중심의 아치 2개 + 점.
|
||||
좌표계는 64×64, 타일 라운드 15, 아치 스트로크 9.75(끝은 평평하게), 마크 중심은 (14.19, 49.81)입니다.
|
||||
마크는 타일의 70%를 차지하고, 좌하단으로 4.5% 광학 보정이 들어가 있습니다 — 아치 질량이 우상단에 몰려 있어 bbox를 그냥 중앙에 놓으면 우상단으로 쏠려 보입니다.
|
||||
|
||||
| 파일 | 용도 |
|
||||
| --- | --- |
|
||||
| `ubisam-logo.svg` | 기본 — 그라디언트 타일(#56ADF7 → #1D82E8) + 오프화이트 마크. 원본에 가장 가깝습니다 |
|
||||
| `ubisam-logo-flat.svg` | 단색 타일(#2196F3) + 흰 마크. 작은 크기(16~24px)나 인쇄용 |
|
||||
| `ubisam-logo-mono.svg` | 타일이 `currentColor` — 다크 배경에서 타일 색만 바꿔 쓸 때 |
|
||||
| `ubisam-mark.svg` | 타일 없이 마크만, `currentColor`. 이미 파란 판 위에 얹을 때 |
|
||||
| `../code/UbisamBase.Core/Themes/Logo.xaml` | WPF용 `DrawingImage` 리소스 (같은 도형) |
|
||||
|
||||
## 워드마크
|
||||
|
||||
첨부해 주신 원본에서 앞의 "Ubi"만 색을 바꿨습니다 — "Sam"과 마크는 손대지 않았습니다.
|
||||
|
||||
원본에는 글자 뒤에 **불투명한 흰 판**이 깔려 있습니다. 색만 바꾸면 어두운 배경에서 그 판이 흰 사각형으로 그대로 보이므로 걷어냈습니다.
|
||||
|
||||
흰 픽셀을 남기는 조건은 하나입니다 — **파란 타일 영역 안 + 파랑에 둘러싸여 있음**. 그게 마크의 아치입니다. 글자 속공간(`a`·`b`·`U`)의 흰색은 파랑에 갇혀 있어도 타일 밖이므로 투명해지고, 타일 라운드 코너 밖의 흰 판도 배경에서 도달하므로 투명해집니다. 글자 경계의 안티에일리어싱은 알파로 보존됩니다.
|
||||
|
||||
| 파일 | 용도 |
|
||||
| --- | --- |
|
||||
| `wordmark-ubi-black.png` | 밝은 배경 — Ubi #1D1F20 |
|
||||
| `wordmark-ubi-white.png` | 어두운 배경 — Ubi #FFFFFF |
|
||||
| `wordmark-ubi-gray.png` | 원본 그대로 (보관용 — 흰 판이 남아 있습니다) |
|
||||
|
||||
3533×829px입니다 — **화면에 쓸 때는 이 파일을 직접 줄이지 마세요.** 30배 축소는 획을 부수므로, `wordmark/` 폴더의 미리 축소한 파일을 쓰세요.
|
||||
|
||||
### 화면용 (미리 축소, 단계 리샘플)
|
||||
|
||||
| 파일 | 크기 |
|
||||
| --- | --- |
|
||||
| `wordmark/{white,black}-180.png` | 180×42 |
|
||||
| `wordmark/{white,black}-240.png` | 240×56 |
|
||||
| `wordmark/{white,black}-360.png` | 360×84 |
|
||||
| `wordmark/{white,black}-480.png` | 480×112 |
|
||||
| `wordmark/{white,black}-720.png` | 720×169 |
|
||||
|
||||
실제 표시 폭보다 한 단계 큰 파일을 쓰고, WPF에서는 `RenderOptions.BitmapScalingMode="HighQuality"`를 지정하세요. 자세한 내용은 `code/FIX-13.md`에 있습니다.
|
||||
|
||||
accent 채움 위에는 쓰지 마세요 — "Sam"이 배경에 묻힙니다. 그 경우 흰 단색 버전이 필요하니 말씀해 주세요.
|
||||
|
||||
## 실행 아이콘 (.ico)
|
||||
|
||||
| 파일 | 용도 |
|
||||
| --- | --- |
|
||||
| `UbisamBase.ico` | 실행 파일·작업 표시줄·바로가기 아이콘. 16 / 24 / 32 / 48 / 64 / 128 / 256px 7종을 담고 있습니다 |
|
||||
| `app-icon-{크기}.png` | 개별 크기 PNG — 설치 관리자나 문서에 쓸 때 |
|
||||
|
||||
크기별로 형태를 다르게 최적화했습니다. 48px 이상은 원본대로 아치 2개, **24px 이하는 아치를 1개로 줄이고 두껍게** 했습니다 — 작은 크기에서 두 아치가 뭉개져 회색 덩어리로 보이는 것을 막기 위해서입니다. 16·24px은 타일이 화면을 꽉 채우고, 큰 크기는 여백을 둡니다.
|
||||
|
||||
### 프로젝트에 등록
|
||||
|
||||
1. `UbisamBase.ico`를 `src/UbisamBase.App/`에 복사
|
||||
2. `.csproj`에 한 줄 추가
|
||||
|
||||
```xml
|
||||
<PropertyGroup>
|
||||
<ApplicationIcon>UbisamBase.ico</ApplicationIcon>
|
||||
</PropertyGroup>
|
||||
```
|
||||
|
||||
창 제목줄 아이콘은 `WindowStyle="None"`이라 보이지 않지만, 작업 표시줄에는 나옵니다. 명시하려면 `ShellWindow.xaml`에 추가하세요.
|
||||
|
||||
```xml
|
||||
Icon="pack://application:,,,/UbisamBase.App;component/UbisamBase.ico"
|
||||
```
|
||||
|
||||
빌드 후 탐색기에서 아이콘이 바뀌지 않으면 아이콘 캐시 때문입니다 — `ie4uinit.exe -show`를 실행하거나 재로그인하세요.
|
||||
|
||||
## WPF에서 쓰기
|
||||
|
||||
`ShellStyles.xaml`의 머지 목록에 `Logo.xaml`을 추가하고, `ShellWindow.xaml`의 로고 자리(accent 사각형 + 첫 글자)를 이미지로 교체하세요.
|
||||
|
||||
```xml
|
||||
<Image Source="{StaticResource Ubisam.Logo}" Width="30" Height="30" VerticalAlignment="Center"/>
|
||||
```
|
||||
|
||||
## 주의
|
||||
|
||||
- 원본이 저해상도라 라운드 반경·스트로크 두께·마크 여백은 눈으로 맞춘 근사값입니다. 원본 벡터(AI/SVG) 파일이 있으면 그걸 쓰는 게 정확합니다.
|
||||
- 작은 크기에서는 그라디언트가 뭉개져 보이므로 `flat` 버전을 쓰세요.
|
||||
- 타일 없이 흰 배경에 마크만 놓으면 대비가 부족합니다 — 마크만 쓸 때는 accent 색을 지정하세요.
|
||||
8
.design/logo/Company/logo/ubisam-logo-flat.svg
Normal file
|
After Width: | Height: | Size: 8.0 KiB |
8
.design/logo/Company/logo/ubisam-logo-mono.svg
Normal file
|
After Width: | Height: | Size: 8.0 KiB |
14
.design/logo/Company/logo/ubisam-logo.svg
Normal file
|
After Width: | Height: | Size: 8.2 KiB |
7
.design/logo/Company/logo/ubisam-mark.svg
Normal file
|
After Width: | Height: | Size: 8.0 KiB |
BIN
.design/logo/Company/logo/ubisam_darkMode.png
Normal file
|
After Width: | Height: | Size: 220 KiB |
BIN
.design/logo/Company/logo/ubisam_lightMode.png
Normal file
|
After Width: | Height: | Size: 222 KiB |
BIN
.design/logo/Company/logo/wordmark-ubi-gray.png
Normal file
|
After Width: | Height: | Size: 230 KiB |
BIN
.design/logo/Company/logo/wordmark/black-180.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
.design/logo/Company/logo/wordmark/black-240.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
.design/logo/Company/logo/wordmark/black-360.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
.design/logo/Company/logo/wordmark/black-480.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
.design/logo/Company/logo/wordmark/black-720.png
Normal file
|
After Width: | Height: | Size: 37 KiB |
BIN
.design/logo/Company/logo/wordmark/white-180.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
.design/logo/Company/logo/wordmark/white-240.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
.design/logo/Company/logo/wordmark/white-360.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
.design/logo/Company/logo/wordmark/white-480.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
.design/logo/Company/logo/wordmark/white-720.png
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
.design/logo/Company/회사 코드 개발 플랫폼 디자인.zip
Normal file
BIN
.design/logo/UbisamBase.ico
Normal file
|
After Width: | Height: | Size: 77 KiB |
BIN
.design/logo/a-256.png
Normal file
|
After Width: | Height: | Size: 56 KiB |