feat(mcp): Dabit.Mcp 초기 공개 (리네임 + colors 수정 + scroll 검증)
- refactor: DabitChe.Mcp → Dabit.Mcp (namespace, csproj, exe 모두) - fix: display_message colors를 쉼표 구분 문자열로 변경 (ModelContextProtocol 1.3.0 배열 바인딩 버그 우회) - docs: scroll 4방향(left/right/up/down) 실기 검증 완료 (DIBD600T V3.6.1) 20 도구 (읽기 4 + 콘텐츠 2 + 설정 14), stdio 전송, Windows exe. 실기 검증: scroll/fill_screen 8색/display_message 글자별색 모두 정상. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
505
CLAUDE.md
Normal file
505
CLAUDE.md
Normal file
@@ -0,0 +1,505 @@
|
||||
# CLAUDE.md — DabitChe 모노레포 루트
|
||||
|
||||
LED 전광판 콘텐츠 제작 + 스케줄 관리 + 컨트롤러 전송 소프트웨어 (다빛솔루션 DABIT)
|
||||
|
||||
## 프로그램 정체성
|
||||
|
||||
DabitChe는 **LED 전광판 운영 소프트웨어**이다. 핵심 기능은 콘텐츠 제작(텍스트/이미지) → 스케줄 편집(PLA/BGP) → 컨트롤러 전송이다. 통신설정, 제어, 시뮬레이터는 보조 기능이다. dbSimulator(DBPS)는 dabitche의 부분집합으로, 프로토콜 시뮬레이터 기능에 해당한다. 사용자는 현장 설치기사/운영자이며, 프로토콜을 몰라도 사용 가능해야 한다.
|
||||
|
||||
## Naming 규칙 (3층) — 회사 / 제품 / 프로토콜
|
||||
|
||||
이름은 3층으로 나눈다. 새 프로젝트·모듈·MCP 명명 시 이 규칙을 따른다.
|
||||
|
||||
| 층 | 이름 | 적용 대상 | 예 |
|
||||
|----|------|----------|-----|
|
||||
| 회사/브랜드 | **Dabit** (다빛솔루션 DABIT) | 하드웨어 컨트롤러를 다루는 브랜드 레벨 자산 | MCP 서버 `Dabit.Mcp` (등록명 `dabit-led`) |
|
||||
| 제품 | **DabitOne** (레거시명 = DabitChe) | PC 운영 앱 | `DabitOne.Desktop`, `DabitOne.AutoSend` |
|
||||
| 프로토콜 | **DibdProtocol** | 제품 무관 프로토콜/전송 라이브러리 | `DibdProtocol.Core` / `.Transport` |
|
||||
|
||||
- **레거시 잔재 주의**: 폴더·namespace·sln은 아직 `DabitChe.*`(Desktop/AutoSend). 단 출력물(exe `DabitOne.exe`)·창 제목은 이미 `DabitOne`. 내부 `DabitChe`→`DabitOne` 전체 리네임은 **워크트리 다수 동시 진행**이라 충돌 위험 → main 단일 big-bang으로 일괄 처리 예정(보류).
|
||||
- `Dabit.Mcp`가 형제 `DabitOne.*`와 이름이 다른 건 **의도된 계층 구분**(브랜드 vs 제품)이지 실수가 아니다. (2026-06-02 MCP만 선행 리네임: `DabitChe.Mcp`→`Dabit.Mcp`, 등록명 `dabitche-led`→`dabit-led`)
|
||||
|
||||
## UI/UX 원칙
|
||||
|
||||
| 원칙 | 설명 |
|
||||
|------|------|
|
||||
| **가로 우선 레이아웃** | 콘텐츠 편집 + 스케줄 그리드가 핵심 → 가로 공간을 넉넉히 확보 |
|
||||
| **좁은 레이아웃 금지** | dbSimulator(580px 고정)의 좁은 배치를 그대로 가져오지 않음 — 가로 900px+ 기준으로 재배치 |
|
||||
| **GroupBox 가로 병렬** | 통신설정 등에서 Serial/TCP/UDP를 세로로 쌓지 말고 가로 2~3열로 배치 |
|
||||
| **입력 필드 폭 제한** | COM 포트(5글자), IP(15글자), Port(5글자) → 필드 폭을 내용에 맞춤, 우측 여백 과다 금지 |
|
||||
| **전체 통일감** | 한 패널의 배치를 바꾸면 나머지 패널도 동일 원칙 적용 |
|
||||
| **사이드바 5그룹** | 통신 → 설정 → 시뮬 → 편집 → 고급 (사용 순서 = 위→아래) |
|
||||
| **Design Token System** | `Themes/` 폴더에 토큰 + implicit style 집중. **토큰 1곳 수정 → 전체 반영**이 핵심. 정본: `AI_Rules/080_ui-design-system.md` |
|
||||
| **창 크기 기준** | 새 Window/팝업의 크기 요청 시 **메인 창(MainWindow) 대비 비율**로 확인. 예: "메인의 2/3", "메인의 1/2". 비율 미지정 시 사용자에게 "메인 창 대비 몇 분의 몇으로 할까요?" 확인 후 적용 |
|
||||
|
||||
### Design Token 필수 규칙
|
||||
|
||||
| 규칙 | 이유 |
|
||||
|------|------|
|
||||
| 인라인 FontSize/Height/Background/Foreground 금지 | 토큰 파일에서만 관리해야 전역 변경 가능 |
|
||||
| RowDefinition Height="Auto" 필수 | 고정값은 Control.Margin을 찌그러뜨림 |
|
||||
| 모든 입력 컨트롤에 FontWeight="Normal" 명시 | GroupBox SemiBold 상속 차단 |
|
||||
| Window 태그에 `Background="{StaticResource Surface.Background}"` | Window는 implicit style 불가 |
|
||||
| 새 컨트롤 추가 시 Themes/ implicit style 확인 | 토큰 시스템 밖에서 컨트롤이 생성되면 불일치 발생 |
|
||||
|
||||
### Design Token 현재 값 (참고)
|
||||
|
||||
값은 `Themes/_Typography.xaml`, `_Spacing.xaml`, `_Colors.xaml`에서 관리. 변경 시 해당 파일만 수정.
|
||||
|
||||
| 토큰 | 현재 값 | 파일 |
|
||||
|------|--------|------|
|
||||
| `Control.Height` | 26 (MinHeight) | `_Typography.xaml` |
|
||||
| `Control.Margin` | 0,4,0,4 | `_Spacing.xaml` |
|
||||
| `Font.Size.MD` | 12 | `_Typography.xaml` |
|
||||
|
||||
**Button 규칙:**
|
||||
- 전역 스타일이 `MinHeight=26`, `Padding="12,0"`으로 설정됨 — **ComboBox/TextBox와 동일 높이**
|
||||
- **인라인 Padding 금지** — 버튼마다 다른 Padding을 넣으면 높이가 달라져 이질적. 전역 스타일만 사용
|
||||
- 인라인 `FontWeight`, `FontFamily`, `Height` 지정 금지 — 전역 스타일(`Themes/Button.xaml`)에서 관리
|
||||
- 예외적으로 큰 버튼이 필요하면 Named Style(x:Key)로 별도 정의. 인라인 Padding은 여전히 금지
|
||||
|
||||
## 핵심 규칙
|
||||
|
||||
- **한국어 응답**, 간결함 최우선 (3~5줄), 인사말/서론/결론 생략
|
||||
- **PowerShell 7.0+** 필수 (`pwsh.exe`), Bash/Linux 명령어 금지
|
||||
- **C# nullable enable** (WPF), **TypeScript strict** (Electron)
|
||||
- camelCase/PascalCase/SCREAMING_SNAKE_CASE, UTF-8 LF
|
||||
- **Conventional Commits**: `<type>(<scope>): <desc>` (scope: protocol, ui, transport, settings, desktop, electron, test, docs)
|
||||
- 환각 방지: 근거 부족 시 명시, 불확실 시 추측 표기 + 대안 제시
|
||||
|
||||
### AI 코딩 4대 원칙
|
||||
|
||||
1. **Think Before Coding** — 불확실하면 추측하지 말고 먼저 확인
|
||||
2. **Simplicity First** — 요청받은 것만 구현
|
||||
3. **Surgical Changes** — 수정 대상만 변경
|
||||
4. **Goal-Driven Execution** — 명령보다 목표로 지시
|
||||
|
||||
### 모듈화(Modularity) 원칙
|
||||
|
||||
핵심 기능은 나중에 독립 앱/CLI/REST API로 추출할 수 있게 만든다:
|
||||
- **Service 계층에 WPF 타입(Window, Dispatcher, Application 등) 직접 참조 금지** — UI 알림은 Result<T> 반환 후 ViewModel이 처리
|
||||
- **DibdProtocol/은 플랫폼 무관 라이브러리로 유지** — System.Windows 참조 절대 금지
|
||||
- **새 Service 추가 시 `DialogService.Instance` 직접 호출 금지** — IDialogService 주입 또는 ViewModel에서 처리
|
||||
- **`DibdProtocol.Transport/Rpc/`** — DIBD JSON-RPC 2.0 envelope 96 method 본문, namespace `Dabit.LedProtocol` (Blazor WASM 회피 영역이라 Transport 어셈블리 적재, 0517a 트랙)
|
||||
|
||||
### 영속성(Persistence) 필수 규칙
|
||||
|
||||
새 기능/UI 추가 시 반드시 확인:
|
||||
- **사용자가 입력한 모든 설정값은 앱 재시작 후에도 유지**되어야 한다
|
||||
- 새 입력 컨트롤(ComboBox, TextBox, CheckBox 등) 추가 시 → AppSettings + config.properties 저장/복원 구현 필수
|
||||
- 저장 경로: `AppSettings` → `PropertiesConfigRepository` → `config.properties`
|
||||
- 슬롯 구조 데이터는 `SettingService.SetProperties()` 배치 저장 사용
|
||||
- 앱 종료 시 `App.OnExit()`에서 자동 저장됨 — 새 ViewModel 추가 시 여기에 SaveSettings 호출 추가
|
||||
|
||||
## FORBIDDEN (19개)
|
||||
|
||||
위반 시 현장 장비 오동작 또는 아키텍처 붕괴. 예외 없음.
|
||||
|
||||
### 프로토콜/바이너리 (F3–F5, F7–F8, F16–F19)
|
||||
|
||||
| ID | 금지 | 이유 |
|
||||
|----|------|------|
|
||||
| F3 | OPCode 값 변경 금지 | 컨트롤러 펌웨어가 고정 OPCode에 의존 |
|
||||
| F4 | HEX 프레임(DLE-STX) 포맷 변경 금지 | 컨트롤러 파싱 고정 |
|
||||
| F5 | ASCII 프레임(`![...!]`) 포맷 변경 금지 | 레거시 호환 |
|
||||
| F7 | `pendingDle` 상태 제거 금지 | TCP 버퍼 경계 DLE(0x10) 분할 시 패킷 손실 |
|
||||
| F8 | `DLE_OPCODES` Set 변경 금지 | C# `ShouldApplyDle()`과 정확히 일치 |
|
||||
| F16 | DisplaySignal 시그널 코드 변경 금지 | 70+ 모델이 의존 |
|
||||
| F17 | CrcCalculator 수정 금지 | 펌웨어 CRC 테이블과 일치 필수 |
|
||||
| F18 | MSchedule 25바이트 구조 변경 금지 | 펌웨어 `memcpy` 직접 매핑 |
|
||||
| F19 | tsParameter 792바이트 구조/오프셋 변경 금지 | 바이너리 레이아웃 고정 |
|
||||
|
||||
### INI/설정 (F1–F2, F9, F11)
|
||||
|
||||
| ID | 금지 | 이유 |
|
||||
|----|------|------|
|
||||
| F1 | INI 키 오타 수정 금지 (`ColorOder`, `ShowInfomation`, `PorotocolType`) | 현장 장비 하위 호환성 |
|
||||
| F2 | Boolean INI 값을 `true`/`false` 변경 금지 | 레거시 앱·컨트롤러가 `0`/`1`만 읽음 |
|
||||
| F9 | API Key/비밀번호 하드코딩 금지 | 환경변수 사용 필수 |
|
||||
| F11 | AppSettings static 필드 직접 수정 금지 | `IConfigRepository` 경유 필수 |
|
||||
|
||||
### 아키텍처 (F6, F10, F12–F15)
|
||||
|
||||
| ID | 금지 | 이유 |
|
||||
|----|------|------|
|
||||
| F6 | BrowserRouter 사용 금지 | Electron file:// 미지원, HashRouter 필수 (WPF 해당 없음) |
|
||||
| F10 | Bash 명령어 금지 | PowerShell 7.0+ 사용 |
|
||||
| F12 | SerialTransport `portLock`(SemaphoreSlim) 우회 금지 | 동시 접근 시 포트 충돌 |
|
||||
| F13 | DI 외부에서 Service 직접 생성 금지 | DI 컨테이너 일관성 |
|
||||
| F14 | ViewModel에서 직접 Transport 호출 금지 | Service 계층 경유 필수 |
|
||||
| F15 | MQTT 토픽명 변경 금지 (`/db_msg`, `/db_set`, `/db_sch`, `/db_msg_r`, `/db_sch_r`) | 장비 구독 고정 |
|
||||
|
||||
### 읽기 전용
|
||||
|
||||
| 금지 | 이유 |
|
||||
|------|------|
|
||||
| `references/DabitChe-V2.2.1/` 소스 수정 | 읽기 전용 레거시 참조 — 마이그레이션 대조 기준 훼손 |
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
dabitche/
|
||||
├── DabitChe.Desktop/ ← [NEW] C# .NET 9.0 + WPF + CommunityToolkit.Mvvm
|
||||
│ ├── Views/ │ XAML UI (19 FU 매핑)
|
||||
│ ├── ViewModels/ │ MVVM ViewModel
|
||||
│ ├── Services/ │ 비즈니스 로직
|
||||
│ ├── Config/ │ AppSettings, IConfigRepository
|
||||
│ └── Helpers/ │ EventBus, Result<T>
|
||||
├── DabitChe.Desktop.Tests/ ← xUnit 데스크톱 테스트
|
||||
├── DibdProtocol/ ← [SHARED] 프로토콜 라이브러리 (Desktop + Simulator 공유)
|
||||
│ ├── Protocol/ │ HexCommand, AsciiCommand, PacketBuilder, PacketParser
|
||||
│ ├── Codec/ │ CrcCalculator, DleFramer, BccCalculator, EncodingConverter
|
||||
│ ├── Transport/ │ ITransport, Serial/TCP/UDP/MQTT/BT
|
||||
│ ├── Model/ │ DisplayConfig, ConnectionState, MessageSlot
|
||||
│ └── Image/ │ DatBuilder, RgbConverter
|
||||
├── DibdProtocol.Tests/ ← xUnit 프로토콜 테스트
|
||||
├── DabitChe.Electron/ ← [현행] Electron 33 + React 19 + Vite 6 + Zustand 5
|
||||
│ ├── src/main/ │ Electron 메인 (IPC, services, transport)
|
||||
│ ├── src/renderer/ │ React UI (10 pages, 44 components, 8 stores)
|
||||
│ └── src/shared/ │ 공유 타입/프로토콜/i18n
|
||||
├── references/ ← 참조 자료 집중
|
||||
│ ├── DabitChe-V2.2.1/ │ [읽기 전용] C++ Builder 6 레거시
|
||||
│ ├── DabitChe_Java/ │ [로컬 참조] Java 포팅 (git 추적 안 함)
|
||||
│ ├── Font/ │ .fnt + FontConverter (릴리즈에 복사)
|
||||
│ ├── firmware/ │ .bin 펌웨어 (릴리즈에 복사)
|
||||
│ └── DabitChe_UserManual_v9.0.pdf
|
||||
└── docs/ ← 통합 문서
|
||||
```
|
||||
|
||||
### 전환 전략
|
||||
|
||||
- **현행**: DabitChe.Electron 운영 중
|
||||
- **목표**: DabitChe.Desktop(C# WPF)으로 점진적 대체
|
||||
- **공유**: DibdProtocol/ 라이브러리를 Desktop과 dbSimulator가 공유
|
||||
- 프로토콜 엔진은 DibdProtocol/에 집중, 앱별 중복 구현 금지
|
||||
|
||||
## Build & Test
|
||||
|
||||
```powershell
|
||||
# ── C# WPF (새) ──
|
||||
cd DabitChe.Desktop
|
||||
dotnet build
|
||||
dotnet test ../DibdProtocol.Tests
|
||||
dotnet test ../DabitChe.Desktop.Tests
|
||||
|
||||
# ── Electron (현행) ──
|
||||
cd DabitChe.Electron
|
||||
npm test # Vitest 387+개
|
||||
npm run test:e2e # Playwright E2E
|
||||
npm run dev # Vite dev (:5173)
|
||||
npm run dev:electron # 전체 앱
|
||||
npm run package:win # NSIS 인스톨러
|
||||
```
|
||||
|
||||
### DibdProtocol.Core 변경 시 브라우저 안전성 게이트
|
||||
|
||||
`DibdProtocol.Core/` 하위 소스를 수정한 커밋은 CI(`.gitea/workflows/ci.yml`)에서
|
||||
다음을 자동 실행·검증한다.
|
||||
|
||||
1. `dotnet build DabitChe.sln --configuration Release`
|
||||
2. `dotnet test DibdProtocol.Tests` + `dotnet test DibdProtocol.Core.Tests`
|
||||
3. `dotnet publish DibdProtocol.Core.BlazorCheck/... --configuration Release`
|
||||
|
||||
3번이 실패하면 Core에 브라우저 비호환 API(System.IO.Ports / System.Net.Sockets /
|
||||
MQTTnet 등)가 섞인 것. 로컬에서도 Core 수정 후
|
||||
`dotnet publish DibdProtocol.Core.BlazorCheck -c Release` 수동 실행을 권장한다.
|
||||
`dotnet build`만으로는 WASM 링커 회귀를 감지 못 한다.
|
||||
|
||||
**단, publish 성공은 "WASM 링커·트림 통과"까지만 증명하고, 브라우저에서
|
||||
`Pages/Home.razor`의 `OnInitialized()` smoke가 실제 실행됨을 보장하지는 않는다.**
|
||||
런타임 증명이 필요하면 `dotnet run --project DibdProtocol.Core.BlazorCheck`로
|
||||
브라우저에서 5-way smoke(CRC·DLE·EUC-KR·byte-font·ASCII)를 육안 확인하거나,
|
||||
Phase 1+에서 Playwright 1회 접속 step을 CI에 추가한다.
|
||||
|
||||
## Hooks
|
||||
|
||||
DabitChe.Electron에 Claude Code hooks가 설정되어 있다.
|
||||
|
||||
| 패턴 | 동작 |
|
||||
|------|------|
|
||||
| 커밋 전 품질 게이트 | git commit 시 `npm run quality` 자동 실행, 실패 시 차단 |
|
||||
| FORBIDDEN 위반 감지 | 파일 수정 시 FORBIDDEN 패턴 정규식 매칭, 위반 시 경고 |
|
||||
| /careful 안전 모드 | `.claude/.careful` 존재 시 위험 명령 차단 |
|
||||
| Gotchas 자동 수집 | 세션 종료 시 빌드/테스트 실패 패턴 분석 |
|
||||
|
||||
## 로그 작성 원칙
|
||||
|
||||
`LogService`는 4 레벨(Info/Warn/Error/Debug)을 제공한다. UI는 평소 Info/Warn/Error만 표시하고, Debug는 로그 헤더의 "Debug 보기" 토글(`MainWindow.xaml`)을 켜야 보인다. 호출부에서 적절한 레벨을 선택해 노이즈를 줄인다.
|
||||
|
||||
### 레벨 분류 기준
|
||||
|
||||
- **Info** — 사용자 시나리오 흐름. 작업 시작/완료, 핵심 결과, 세션 라이프사이클. **TX/RX 단발 명령은 Info 유지** (업체가 시스템 구현 시 protocol 참고용 — `_logService.Info($"TX: 시간동기화 ASCII ![0030...!]")` 같은 라인은 의도적으로 보임)
|
||||
- **Warn** — 사용자 조치가 필요한 비정상. 파일 누락, 응답 status 비정상, 펌웨어 거부 가능성
|
||||
- **Error** — 작업 실패. 통신 끊김, 예외
|
||||
- **Debug** — 평소 OFF, 트러블슈팅 시 ON. 패킷 단위 hex 덤프(`TX[cmd] NB → RX MB (Tms)`), 반복 호출 진행률(블록 N/M), 분기 결정 trace, 동일값 반복 entry 출력
|
||||
|
||||
### 연속 전송 (펌웨어/폰트/이미지 다블록)
|
||||
|
||||
블록 단위 진행률은 **Debug**. 시작·요약(블록 수)·완료만 **Info**.
|
||||
|
||||
```
|
||||
Info: 펌웨어 전송 시작 219블록
|
||||
Debug: 펌웨어 전송: 1/219 (0%) ← 블록 단위 진행률
|
||||
Debug: 펌웨어 전송: 51/219 (23%)
|
||||
...
|
||||
Info: 펌웨어 전송 완료
|
||||
```
|
||||
|
||||
### 임시 디버그 (`DEBUG-TEMP`)
|
||||
|
||||
특정 버그 추적·신규 기능 검증을 위해 박은 일회성 로그는 다음 중 하나로 마킹한다:
|
||||
|
||||
1. 메시지 접두사: `_logService.Debug($"[DEBUG-TEMP] ...")`
|
||||
2. 코드 주석: `// DEBUG-TEMP: <목적>, <날짜>`
|
||||
|
||||
검증 완료 시점에 두 갈래로 정리:
|
||||
|
||||
- "환경에 따라 또 깨질 수 있나?" (예: TCP fragment, 재부팅 응답) → **영구 Debug로 강등** — `[DEBUG-TEMP]` 접두사 제거하고 `_logService.Debug`만 유지
|
||||
- "한번 검증으로 끝났나?" (예: 특정 byte offset 확인) → **제거**
|
||||
|
||||
월 1회 또는 PR 직전 `git grep "DEBUG-TEMP"`로 잔존 검토.
|
||||
|
||||
### 영구 Debug 후보 — 시스템 경계
|
||||
|
||||
코드 안쪽 깊은 곳의 trace는 임시 디버그(검증 후 제거)지만, **시스템 경계의 객관적 사실**은 영구 Debug로 둔다:
|
||||
|
||||
- 외부 I/O raw byte (TX/RX hex, latency `(Tms)`)
|
||||
- 분기 결정 (어떤 path 탔는지: legacy vs modern, ImageDir fallback 등)
|
||||
- 재시도/fallback 작동 (`ack timeout, retry 2/3`)
|
||||
- 외부 환경 로드 결과 (`config.properties NN키 로드, 인코딩=EUC-KR`)
|
||||
- 변환 입출력 byte count (`ConvertToSplitDat: 3549B → 612B+893B`)
|
||||
|
||||
기준 한 줄: "사용자 환경에서만 재현되는 문제를 원격 진단할 때 결정적 단서인가?"
|
||||
|
||||
## Data Flow Chains (흐름 기반 탐색)
|
||||
|
||||
함수 시그니처는 AI 도구(grep/glob)로 직접 탐색한다.
|
||||
|
||||
### C# WPF (DabitChe.Desktop + DibdProtocol)
|
||||
|
||||
| 시나리오 | 호출 체인 |
|
||||
|---------|----------|
|
||||
| UI → 컨트롤러 명령 | View → ViewModel → Service → DibdProtocol.Protocol → DibdProtocol.Transport |
|
||||
| 프로토콜 수신 파싱 | Transport → PacketParser → Service → ViewModel → View |
|
||||
| INI 설정 로드 | Config → IConfigRepository → fs (EUC-KR → UTF-8) |
|
||||
| 파일 블록 전송 | Service → 블록 분할 (1024B) → Protocol → Transport |
|
||||
| 스케줄 파일 | Service → PLA 직렬화/역직렬화 (리틀엔디언) → fs |
|
||||
|
||||
### Electron (DabitChe.Electron)
|
||||
|
||||
| 시나리오 | 호출 체인 |
|
||||
|---------|----------|
|
||||
| UI → 컨트롤러 명령 | Renderer → preload (contextBridge) → IPC → DeviceCommandService → HexProtocolBuilder → Transport |
|
||||
| 프로토콜 수신 파싱 | Transport → ProtocolParser (상태 머신) → CRC 검증 → DeviceCommandService → IPC → Renderer |
|
||||
| INI 설정 로드 | Renderer → IPC → IniSettingsService → fs.readFileSync → iconv.decode (EUC-KR) |
|
||||
| 파일 블록 전송 | Renderer → IPC → 블록 분할 (1024B) → DeviceCommandService → HexProtocolBuilder → Transport |
|
||||
| 스케줄 파일 | Renderer → IPC → ScheduleFileService → PLA 직렬화/역직렬화 (리틀엔디언) |
|
||||
|
||||
## Symptom → Where to Look
|
||||
|
||||
| 증상 | 확인 위치 (C# WPF) | 확인 위치 (Electron) |
|
||||
|------|--------------------|--------------------|
|
||||
| 프로토콜 바이트 불일치 | `DibdProtocol/Protocol/` | `src/shared/protocol/` |
|
||||
| UI 렌더링 문제 | `DabitChe.Desktop/Views/` | `src/renderer/pages/`, `src/renderer/components/` |
|
||||
| 통신/시리얼 문제 | `DibdProtocol/Transport/` | `src/main/services/transport/` |
|
||||
| INI 설정 문제 | `DabitChe.Desktop/Config/` | `src/main/services/settings/ini-settings-service.ts` |
|
||||
| 빌드/테스트 실패 | `dotnet build` 출력 | `DabitChe.Electron/package.json` 스크립트 |
|
||||
| 레거시 동작 참조 | `references/DabitChe-V2.2.1/CLAUDE.md` | 동일 |
|
||||
|
||||
## Decisions (왜 이렇게 했는지)
|
||||
|
||||
| 결정 | 이유 |
|
||||
|------|------|
|
||||
| C++ Builder → C# → Electron → C# WPF | 레거시 65개 TForm 직접 포팅 불가, 단계적 재작성 |
|
||||
| dbSimulator DibdProtocol 공유 | 프로토콜 엔진 중복 개발 방지 |
|
||||
| INI 오타(`ColorOder` 등) 보존 | 현장 장비 하위 호환성 유지 |
|
||||
| INI 읽기 EUC-KR → 저장 UTF-8 | 점진적 인코딩 전환 |
|
||||
| BCD 시간 인코딩 | 레거시 컨트롤러 RTC 호환 |
|
||||
| 13바이트 밝기 payload | 시간대별 밝기 + CDS 센서 레거시 구조 |
|
||||
| HexProtocolBuilder C# byte-identical | V3.0.0(C#)과 바이트 일치 보장 |
|
||||
| Electron IPC preload.ts 경유 | contextIsolation 보안 모델 준수 |
|
||||
| WPF ViewModel → Service → Transport | MVVM 계층 분리, Transport 직접 호출 금지 (F14) |
|
||||
| DI 컨테이너 필수 | Service 생명주기 관리 일관성 (F13) |
|
||||
|
||||
## Markdown 작성 규칙 (`docs/` 편집 시)
|
||||
|
||||
1. **Frontmatter 필수**: title, tags, last_updated 3필드
|
||||
2. **H1 1개**, 레벨 스킵 금지 (H2→H3 순서)
|
||||
3. **첫 문장 = 문맥 앵커**: H2 직후 첫 문장에 주어+문맥 포함
|
||||
4. **한 문단 = 한 줄** (Git Diff 가독성), 코드블록 독립 (언어 태그 필수)
|
||||
5. **TL;DR**: 문서 상단에 무엇/왜/어떻게 3줄 요약
|
||||
|
||||
## Deep Context (심층 문서)
|
||||
|
||||
심볼 검색은 AI 도구(grep/glob)에 맡기고, 문서에는 코드가 말 못 하는 것만 담는다.
|
||||
|
||||
| 작업 유형 | 읽을 문서 |
|
||||
|----------|----------|
|
||||
| 정본 매트릭스 (SSOT) | `docs/00_AI_가이드/README.md` |
|
||||
| 로드맵 | `docs/00_AI_가이드/10_ROADMAP.md` |
|
||||
| 프로젝트 구조 | `docs/00_AI_가이드/11_프로젝트컨텍스트.md` |
|
||||
| 코딩 패턴 | `docs/00_AI_가이드/20_CODE_PATTERNS.md` |
|
||||
| 모듈/프로토콜 참조 | `docs/00_AI_가이드/30_MODULE_INDEX.md` |
|
||||
| 워크플로우 절차 | `docs/00_AI_가이드/40_WORKFLOW.md` |
|
||||
| 스킬 레퍼런스 | `docs/00_AI_가이드/41_SKILL_REFERENCE.md` |
|
||||
| 빌드 함정/에러 패턴 | `docs/00_AI_가이드/50_BUILD_ERRORS.md` |
|
||||
| 테스트 카탈로그 | `docs/00_AI_가이드/60_TEST_CATALOG.md` |
|
||||
| 자율 루프 | `docs/00_AI_가이드/70_AUTONOMOUS_LOOP.md` |
|
||||
| 시각 검증 | `docs/00_AI_가이드/72_VISUAL_VERIFY.md` |
|
||||
| DeepDive (모듈별) | `docs/00_AI_가이드/Modules/` |
|
||||
| 플랜+회고 | `docs/00_AI_가이드/Decisions/` |
|
||||
| 프로토콜 바이트 명세 | `docs/01_protocols/protocol-reference.md` |
|
||||
| 레거시 참조 | `references/DabitChe-V2.2.1/CLAUDE.md` |
|
||||
| UI 디자인 시스템 | `AI_Rules/080_ui-design-system.md` (토큰 정의, 컴포넌트 스펙, 적용 절차) |
|
||||
|
||||
### PM 시스템 (장기 매니저 운영 + 자동 분업)
|
||||
|
||||
DabitONe의 통합 매니저 역할은 별도 문서·슬래시 커맨드·claude agents로 운영된다. 새 세션에서 PM 직책을 인수하려면 `/pm-start`로 부트스트랩, 종료 직전 `/pm-end`로 인계. 작업 분해 후 PM이 `@worker-isolated`, `@researcher`, `@code-reviewer`, `@dabitone-protocol-expert`를 자동 호출.
|
||||
|
||||
**시간축 (PM 시스템 문서·커맨드):**
|
||||
|
||||
| 산출물 | 용도 |
|
||||
|--------|------|
|
||||
| `PROJECT_BRAIN.md` (루트) | 현재 동적 상태 — 진행·보류·우선순위. 매 세션 갱신 |
|
||||
| `docs/PM/PM_CONTEXT_MAP.md` | 영역 색인 — "어디에 뭐가 있나" |
|
||||
| `docs/PM/PM_PROTOCOL.md` | PM 운영 룰 — 4-Role·검토·팀빌딩·가드레일 |
|
||||
| `docs/PM/templates/spec.md` | Worker brief 표준 템플릿 |
|
||||
| `docs/PM/templates/retrospective.md` | Phase 회고 템플릿 |
|
||||
| `/pm-start` | PM 세션 부트스트랩 슬래시 커맨드 |
|
||||
| `/pm-end` | PM 세션 종료 + `PROJECT_BRAIN.md` 갱신 |
|
||||
|
||||
**공간축 (claude agents 4종 — `.claude/agents/`):**
|
||||
|
||||
| Agent | 역할 | 호출 시점 |
|
||||
|-------|------|---------|
|
||||
| `worker-isolated` | 격리된 worktree에서 spec.md 따라 한 Phase·기능 구현. 빌드·테스트·commit | PM이 Phase 분해 후 호출 |
|
||||
| `researcher` | 외부 자료·라이브러리·기술 비교 조사. WebFetch·WebSearch | 결정 분기 전 호출 |
|
||||
| `code-reviewer` | Worker 완료 코드 독립 검토 (worker 대화 안 봄, 확증편향 회피) | Worker 완료 후 자동 호출 |
|
||||
| `dabitone-protocol-expert` | DIBD 프로토콜 + 콘텐츠 포맷(DAT/PLA/BGP/ANI) + 전송 절차 전문가 | 프로토콜·콘텐츠·전송 영역 변경 시 자동 호출 |
|
||||
|
||||
**시행 강제 (Hook 1개 — `.claude/hooks/`):**
|
||||
|
||||
| 산출물 | 역할 | 호출 시점 |
|
||||
|--------|------|---------|
|
||||
| `r-pm-1-guard.ps1` | **R-PM-1 시행 강제** — PM 직접 grep/glob/read 3회 경고·5회 차단 | PreToolUse 자동 (`.claude/.pm-session-active` 존재 시 발동) |
|
||||
|
||||
**R-PM 직책 한도 (PM 세션에서 매번 준수)**:
|
||||
- **R-PM-1**: 다중 grep/glob/read 직접 금지 — sub-agent(Explore/general-purpose)에 위임 필수. Hook이 3회 경고·5회 차단으로 시행 강제
|
||||
- **R-PM-2**: 단일 핵심 문서 Read·작은 fix·사용자 인터뷰·PROJECT_BRAIN 갱신은 PM 직접 OK
|
||||
- **R-PM-5**: 사용자 이미 답한 내용 재질문 금지·다음 단계 진입 전 명시 동의 받기·"잘못됐다" 지적 시 즉시 중단·결정 항목은 1~3개 선택지로 좁힘
|
||||
- **R-PM-7**: Evidence Tier 강제 — 모든 결론 ✅사실(카메라/실기 확인) / 🟡추정(코드·논리 미검증) / ❌가정(미입증) 3분류 자동 분리. **코드 변경 ≠ 검증.** 사용자가 "근거는?" 묻기 *전에* 자발 분리. 사례: 2026-05-26 retry 정책 — PM이 코드 변경만 보고 "회복 + 견고" 단정했다가 사용자 지적
|
||||
- 상세: `docs/PM/PM_PROTOCOL.md` 2.5 섹션, `AI_Rules/100_pm-system.md` "PM 직책 한도 R-PM-7"
|
||||
|
||||
**🚨 세션 종료 직전 반드시 `/pm-end`** — `PROJECT_BRAIN.md` 갱신 누락은 다음 PM 세션의 인계 단절 = 시스템 무결성 손상.
|
||||
|
||||
정본 가이드: `D:\Note\20_Marketing_Team\23_Resource\AI_Rules\100_pm-system.md` + `101_pm-system-guide.md`
|
||||
|
||||
### 프로토콜 점검 필수 참조
|
||||
|
||||
프로토콜 전송 코드(패킷 빌드, 바이트 매핑, msgType, /P 페이지 등)를 수정할 때 **반드시** 아래 5개 문서를 검토한다:
|
||||
|
||||
| 문서 | 용도 |
|
||||
|------|------|
|
||||
| `docs/01_protocols/ascii-reference.md` | ASCII 명령어 전체 목록 + 속성코드 |
|
||||
| `docs/01_protocols/hex-protocol.md` | HEX 프레임 구조 + DLE 이스케이프 |
|
||||
| `docs/01_protocols/hex-reference.md` | HEX 명령별 실전 예시 |
|
||||
| `docs/01_protocols/protocol-reference.md` | DIBD 완전 레퍼런스 (ASCII+HEX 통합) |
|
||||
| `docs/01_protocols/protocol-schema.json` | AI 파싱용 구조화 스키마 |
|
||||
|
||||
### 콘텐츠 편집 필수 참조 — **🚨 강제 규칙**
|
||||
|
||||
콘텐츠 편집(`TextEditorWindow`, `DatFileService`, `LegacyDatHeader`, `ConvertToSplitDat` 등) 관련 코드를 **수정·추가·리팩토링**할 때, 작업 전에 **반드시** 아래 문서를 읽는다. 호환성 가정을 깨뜨리면 레거시 파일을 읽을 수 없게 되어 마이그레이션이 실패한다.
|
||||
|
||||
| 문서 | 용도 |
|
||||
|------|------|
|
||||
| `docs/00_AI_가이드/85_EDITOR_LEGACY_COMPAT.md` | 레거시 분할 DAT 호환성 현황 + 파싱 누락 지점 + 수정 제안 |
|
||||
| `docs/01_protocols/dat-bgp-pla-textbook.md` | DAT/PLA/BGP 바이트 포맷 전체 정의 (분할 DAT 2-섹션 구조 포함) |
|
||||
| `memory/project_split_block_format.md` | 분할 블록 펌웨어 검증 기록 (Option[0], BFormKind=2, dsrCnt 필수) |
|
||||
|
||||
**원칙**: 바이트 포맷 호환성은 이미 맞춰져 있으므로 깨지 않는다. 파싱 로직은 누락되어 있으므로 수정 시 이 문서의 수정 범위(P0/P1/P2)를 따른다.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- **Gitea 릴리즈 한국어 깨짐 방지** — Git Bash curl로 한국어 JSON 전송 시 인코딩 깨짐. 반드시 `pwsh.exe -NoProfile`에서 `ConvertTo-Json` + `[System.Text.Encoding]::UTF8.GetBytes()` + `Invoke-RestMethod` 조합 사용. curl 금지
|
||||
- **ASCII 페이지 메시지 msgType은 항상 "1"** — 페이지 번호는 `/P{xx}` 속성(0-based)으로 지정. msgType에 페이지 번호를 넣으면 안됨
|
||||
- **분할 DAT Section 2에 TextInfo 넣으면 XY 어긋남** — FCH(32B)만 기록, TextInfo(256B) 금지
|
||||
- **BFormKind=1 대신 2 사용** — 펌웨어가 BFormKind=1을 다르게 처리하여 좌표 계산 오류 발생
|
||||
- **분할 시 Option[0]=1 필수** — 누락 시 Y 5px 어긋남
|
||||
- **분할 시 dsrCnt EDEffect 최소 0x01** — 0이면 컨트롤러가 분할을 인식 못함
|
||||
- **레거시 분할 DAT의 Section1 FCH `RH`는 "메인 블록 폭"** — 전체 폭이 아님. H1F 샘플 파일의 `RH=16`인데 실제 전광판 폭은 96. `DatFileService.Load`가 현재 이를 전체 폭으로 오해하여 레거시 분할 파일을 못 읽음. 수정 전 반드시 `docs/00_AI_가이드/85_EDITOR_LEGACY_COMPAT.md` 참조
|
||||
- **레거시 DAT 열기는 사이드카 기반** — `TextEditorWindow.OnLoadDat`는 `.blocks.json` 사이드카가 없으면 `BackgroundImagePath`에 경로만 할당하고 TextInfo 파싱을 건너뜀. 레거시 파일(사이드카 없음) 호환 작업 시 이 경로 수정 필요
|
||||
- **레거시 BGP/PLA의 dat 참조는 bare 파일명** — V2.x는 `.bgp`(`Data\Playlist\`)와 `.dat`(`Data\Image\`)이 다른 폴더. `ScheduleFileService.ParseLegacyBinary`가 BGP 같은 폴더만 뒤지면 못 찾고 bare name(`"test_r.dat"`)으로 fallback → 전송 단계 `File.Exists` 실패 → 0블록 전송 → 컨트롤러 슬롯 모두 동일 메모리(블록 0) 가리킴 → "1~7번 모두 RED" 증상. 수정 후 해석 우선순위: **`AppPaths.ImageDir`/`VideoDir`(레거시 V2.x DestTextPath와 동일) → BGP 같은 폴더 → bare name**. `ScheduleTransferService.ResolveContentPath`가 전송 단계에서도 한번 더 구제(2026-05-07 fix)
|
||||
- **펌웨어/폰트 청크 전송은 ACK 대기 필수 — Fire-and-forget 금지** — 시리얼/TCP에서 컨트롤러 처리 지연(플래시 쓰기 등)으로 일부 청크가 누락되면 침묵 회귀(전송 성공 같지만 미적용·한글 폰트 비트맵 손상). `FirmwareService` + `FontSettingsViewModel`의 청크 루프는 반드시 `AckPolicy.Required` + 청크별 3회 retry. timeout warning이 가끔 뜨더라도 **무결성 > 속도**. 정본: 67f1fe88(Fire-and-forget 도입)의 회귀 fix (2026-05-09 카메라 검증 — 한글 폰트가 색 채움 형태로 깨졌던 증상이 ACK 대기 복원으로 해결). 향후 속도 개선 시 청크 간 sleep 등 다른 방향으로 (ACK 대기는 유지)
|
||||
- **FNT 폰트 글리프 시작 ASCII는 글리프 수로 분기** — `FntRenderer.RenderEngGlyph`는 `asciiCode * glyphSize`로 lookup하지만 폰트 파일별 시작 ASCII가 다름. 256/128 글리프 폰트(DABIT 표준, Gothic, Roman 8x16 등)는 ASCII 0부터, **95 글리프 폰트(ENG 16x16-ASCII-Roman/RN90, ENG 16x32-굴림체 등)는 ASCII 0x20(space)부터** 저장. 95 글리프는 `glyphIdx = asciiCode - 0x20` 보정 필요. 미리보기에서 "1" 입력 시 "Q"/"R" 같은 다른 글자가 나오면 이 보정 누락. 헤더 byte 12=W, byte 13=H, byte 14=glyphSize(B). 시작 ASCII는 헤더 명시 없어 글리프 수로 추정. 정본: FntRenderer 2026-05-10 fix
|
||||
- **.NET 7+ SerialPort timeout은 IOException** — `SerialPort.ReadAsync` timeout이 .NET 6까지 `TimeoutException`이었으나 .NET 7부터 **`IOException`**으로 throw (GitHub dotnet/runtime#81035). 외부 catch가 `TimeoutException`만 잡으면 .NET 9 환경에서 회귀 — 시리얼 일시 timeout이 외부 retry 발동 안 함. transport retry 정책 작업 시 catch에 `IOException` + `SocketException` 추가 필수. `TransferSessionService.IsRetriable`은 이미 세 가지 다 잡으나, `FirmwareService` + `FontSettingsViewModel`의 청크 외부 retry는 좁아져 있을 수 있음. 정본: memory `reference_dotnet7_serialport_breaking`, 인계 doc `docs/00_AI_가이드/Decisions/2026-05-26-transport-retry-attempt-revert.md`
|
||||
|
||||
### Phase 1b Parser + VirtualControllerCore Gotchas (2026-04-21 v1.2 설계·플랜 반영)
|
||||
|
||||
- **Parser는 wire-faithful — text canonicalization 금지** — `PacketParser.TryParseDisplayMessagePayload`는 decode된 문자열을 **원문 그대로** 반환해야 한다. `\r`(0x0D) 제거 / `\n`→공백 변환 같은 canonicalization을 **parser 내부에서 절대 하지 말 것**. M07(CR/LF 원문 전송) Inv-1 byte stability를 보장하려면 wire-faithful 필수. Canonicalization은 `DisplayMessageSpecWireCanonicalizer.CanonicalizeForRoundTrip`에서만 수행
|
||||
- **`CharColorCodec.Normalize`에 3-bit 마스킹 추가 금지** — Phase 1a `CharColorCodecTests`가 `{1, 2, 9, 9, 9}` 보존을 기대하므로 마스킹 추가 시 breaking. 3-bit (fg·bg ∈ [0, 7]) 정책은 **Phase 1b `DisplayMessageSpecWireCanonicalizer`에만** 적용. Phase 1a 정본 Codec은 건드리지 않는다
|
||||
- **`VirtualControllerCore.ApplyHexFrame`에는 framed packet만 — payload 단독 입력 금지** — `ApplyHexFrame`은 `DleFramer.TryDeframe`을 경유하므로 입력은 `DLE STX DST LEN CMD DATA DLE ETX` 전체. payload(0x94 data)만 넣으면 `ApplyResult.MalformedFrame` 반환. payload만 있으면 `ApplyHexParsed(HexParsedResponse)`로 직접 진입할 것. 테스트 `FrameVsPayloadContractTests`가 계약 오사용을 검증
|
||||
- **Empty-run canonical form 고정** — 빈 텍스트(M18, payload.Length == 16) payload는 `DefaultFg/DefaultBg/CharColors/FontGroup` 복원 불가. Parser와 `DisplayMessageSpecWireCanonicalizer` 모두 **`Text="", CharColors=null, DefaultFg=0, DefaultBg=0, FontGroup=Group0`** 으로 고정. `DisplayMessageSpecStructuralComparer`를 빈 텍스트 특례로 약화시키지 말 것
|
||||
- **`VirtualControllerCore`는 Phase 1b에서 단일 스레드 전용** — 내부 `Dictionary`·`List` 기반이라 `Apply*`/`Snapshot`/`GetSection` concurrent 호출 시 레이스 → 동작 미정의. 호출자가 UI 스레드 or 전용 worker로 직렬화해야 한다. Phase 2+에서 concurrent 필요 시 `lock` 또는 `ConcurrentDictionary` 전환 검토. 구현 시 XML doc에 반드시 "Thread-safety: Phase 1b 단일 스레드 전용" 명시
|
||||
- **`ControllerState.Sections`/`GetSection`은 deep-copy snapshot** — `ImmutableDictionary`/`ImmutableArray`는 컨테이너 불변성만 보장. `DisplayMessageSpec.Run.CharColors.Colors`가 가변 `int[]`이므로 `SectionManager.CloneSpec`에서 `Array.Copy`로 복제해야 내부 상태 aliasing 차단. Snapshot/GetSection 호출자는 반환값의 `NormalizedCharColors.Colors[0] = 999` 변경이 내부에 영향 주지 않음을 가정하고 설계. `SnapshotAliasMutationTests` 2건이 계약을 증명
|
||||
|
||||
## 커밋 시 문서 업데이트 규칙 — **🚨 강제 규칙**
|
||||
|
||||
### MANDATORY: 커밋 전 체크리스트
|
||||
|
||||
**커밋하기 직전에 반드시 다음 4가지를 확인한다. 하나라도 놓치면 커밋 금지.**
|
||||
|
||||
```
|
||||
☐ 1. 프로토콜 바이트 구조 변경/발견이 있으면 → docs/01_protocols/*.md 업데이트
|
||||
☐ 2. 레거시 대비 새 기능이면 → docs/00_AI_가이드/80_LEGACY_ENHANCEMENTS.md 업데이트
|
||||
☐ 3. AI가 반복 실수한 패턴이면 → CLAUDE.md Gotchas 섹션 업데이트
|
||||
☐ 4. 펌웨어 동작 검증 결과면 → memory/project_*.md 추가
|
||||
```
|
||||
|
||||
### 발견 유형별 업데이트 대상
|
||||
|
||||
| 발견 유형 | 업데이트 대상 |
|
||||
|----------|-------------|
|
||||
| 프로토콜 바이트 구조, DAT/PLA/BGP/ANI 포맷 | `docs/01_protocols/dat-bgp-pla-textbook.md` |
|
||||
| HEX/ASCII 명령 응답 포맷 | `docs/01_protocols/hex-reference.md`, `ascii-reference.md` |
|
||||
| 레거시 대비 변경/확장 기능 | `docs/00_AI_가이드/80_LEGACY_ENHANCEMENTS.md` |
|
||||
| AI가 반복한 실수 패턴 | 이 파일의 `Gotchas` 섹션 |
|
||||
| 펌웨어 동작 검증 결과 | `memory/` (project 타입 메모리) |
|
||||
| 새 ViewModel/Service 패턴 | `docs/00_AI_가이드/20_CODE_PATTERNS.md` |
|
||||
|
||||
### 강제 시행 절차
|
||||
|
||||
**커밋 프로세스:**
|
||||
|
||||
1. **코드 변경 완료** → `git diff`로 변경 내용 확인
|
||||
2. **체크리스트 순회** → 4가지 업데이트 대상 중 해당하는 것 식별
|
||||
3. **문서 업데이트** → 발견 사실을 해당 문서에 기록
|
||||
4. **문서도 함께 스테이징** → `git add <code files> <doc files>`
|
||||
5. **커밋 실행** → 코드+문서를 같은 커밋에 포함
|
||||
|
||||
### 위반 시 복구 절차
|
||||
|
||||
**이미 코드만 커밋했고 문서를 빼먹은 경우:**
|
||||
|
||||
```powershell
|
||||
# 직후 커밋으로 문서 추가
|
||||
git add docs/
|
||||
git commit -m "docs: [커밋해시] 후속 — {발견 내용} 반영"
|
||||
```
|
||||
|
||||
### Why 이 규칙이 강력한가
|
||||
|
||||
- **반복 실수 방지**: 세션마다 레거시 바이트를 재분석하면 시간 낭비
|
||||
- **다음 세션 컨텍스트**: 문서가 최신이어야 다른 세션/에이전트가 올바른 정보로 작업
|
||||
- **장기 유지보수**: 6개월 후 이 코드를 볼 때 "왜 이렇게 했지?"에 답해야 함
|
||||
- **검증 이력**: "컨트롤러 검증 완료 (날짜)" 표시로 회귀 방지
|
||||
|
||||
### 구체적 사례 (2026-04-10)
|
||||
|
||||
ANI 파일 포맷 수정 세션에서 놓친 것:
|
||||
- ✗ FCDavHeader 16B (32B가 아님) — 레거시 소스에서 발견
|
||||
- ✗ TextInfo가 프레임 데이터 뒤에 옴 (앞이 아님)
|
||||
- ✗ BlockText[1] Y=16 하드코딩 — 레거시 TFormMessageText.cpp:1936
|
||||
- ✗ 0x9B BGP 비디오 opcode — 문서에 미기재
|
||||
- ✗ HEX SetScreenSize 응답 [0xF1, rows, cols] — 기존 문서 [status] 오기재
|
||||
|
||||
**교훈**: 레거시 소스(`references/DabitChe-V2.2.1/`)를 분석했다면 그 발견을 즉시 텍스트북에 반영해야 다음 세션에서 같은 분석을 반복하지 않는다.
|
||||
Reference in New Issue
Block a user