d3ro-voice/docs/phases/phase-8.md
Yun Chan 28371a9d1a Phase 8: SSOT 리팩토링 + HotkeyRecordModal + Dashboard 재작성
- d3roPalette 11개 토큰 추가 (sidebar, chassis, inactive 등)
- DS 컴포넌트 6개 + 페이지 5개 매직넘버 → 팔레트 참조 (0개 잔여)
- HotkeyRecordModal 신규: 커스텀 핫키 녹화 모달
- SettingsModal 재작성: 음성 모드 3개(받아쓰기/Agent/원터치) + 핫키 변경
- DashboardPage 재작성: Hero + 통계 4카드 + CRT 서비스 상태 + 히스토리 날짜 그룹핑
- recording-tip 색상 수정: #1F5DF2(파란) → #f25b29(앰버)
- 설계 문서: phase-8.md, speakly-settings-ui.md
2026-04-05 09:54:55 +09:00

10 KiB

Phase 8: 핵심 기능 보강 + UI 디자인 근본 재설계

목표

  1. 핫키 변경 UI 구현 (HotkeyRecordModal) — 앱 테스트를 위한 차단 해제
  2. Settings에 음성 모드 토글 3개 추가 (받아쓰기/Agent/원터치)
  3. d3roPalette SSOT 확장 및 매직넘버 ~40곳 제거
  4. DashboardPage 기능 중심 재작성 (통계+히스토리+상태)
  5. HistoryPage 날짜 그룹핑
  6. recording-tip 색상 버그 수정 (파란색 #1F5DF2 → 앰버 #f25b29)

전제 조건

Phase 7.5 완료 (SoundEffect, AutoLaunch, HotkeyService, VoiceModeService 모두 동작)


태스크

8.1 HotkeyRecordModal 구현

  • 파일: src/renderer/components/HotkeyRecordModal.tsx
  • Speakly HotkeyRecordModal 패턴 차용:
    1. Modal 열림 → document.addEventListener('keydown') 리스너 등록
    2. 키 조합 감지 → HotkeyBinding 형태로 Chip/태그 표시
    3. 유효성 검증: 시스템 예약키 블랙리스트 (Ctrl+C, Ctrl+V, Alt+F4, Ctrl+Alt+Del 등)
    4. 중복 핫키 충돌 감지 → 경고 메시지 표시
    5. "저장" 클릭 → IPC hotkey:setDictationShortcut / hotkey:setHandsFreeShortcut 호출
    6. "취소" 또는 ESC → 모달 닫기, 변경 없음
  • 키 표시: platform-specific 라벨 (Ctrl → Windows, 키코드 → 표시 문자열 매핑)
  • HotkeyBinding 구조:
    interface HotkeyBinding {
      keyCode: number;
      ctrl: boolean;
      alt: boolean;
      shift: boolean;
      meta: boolean;
      displayLabel: string; // 예: "Right Alt", "Ctrl+Shift+F5"
    }
    
  • IPC 채널: 기존 hotkey:setDictationShortcut, hotkey:setHandsFreeShortcut, hotkey:setCommandShortcut 활용
  • 시스템 예약키 블랙리스트 (src/shared/constants.ts에 추가):
    export const RESERVED_HOTKEYS: Set<string> = new Set([
      'Ctrl+C', 'Ctrl+V', 'Ctrl+X', 'Ctrl+Z', 'Ctrl+A', 'Ctrl+S',
      'Alt+F4', 'Alt+Tab', 'Ctrl+Alt+Delete', 'Win+L', 'Win+D',
      'Ctrl+Shift+Escape', 'PrintScreen',
    ]);
    

8.2 Settings 핫키 섹션 UI

  • 파일: src/renderer/components/SettingsModal.tsx
  • General 탭에 "Shortcuts" 섹션 추가 (기존 토글 설정 아래):
    • 받아쓰기(Dictation) 핫키: 현재 바인딩 표시 + "변경" 버튼 → HotkeyRecordModal 열기
    • 핸즈프리(Hands-free) 핫키: 동일 패턴
    • Agent Mode 핫키: 더블프레스 기반이므로 "받아쓰기 키 2회 누름" 안내 텍스트만 표시
  • 각 핫키 옆에 현재 바인딩을 Chip 컴포넌트로 표시 (예: Right Alt)
  • 핫키 변경 성공 시 → Chip 갱신 + 토스트 알림
  • IPC 호출: hotkey:getDictationShortcut, hotkey:getHandsFreeShortcut로 초기값 로드

8.3 음성 모드 토글 3개 (Settings 연동)

  • 파일: src/renderer/components/SettingsModal.tsx (General 탭)
  • Speakly Settings의 음성 모드 섹션 패턴:
    • Dictation Mode: Switch 토글 (기본 ON) + 핫키 Chip + "Hold-to-talk" 설명
    • Agent Mode: Switch 토글 (기본 OFF) + "Dictation 키 더블프레스" 설명
      • Dictation이 OFF이면 Agent도 비활성 (의존 관계 표시)
    • Hands-free (One-touch): Switch 토글 (기본 OFF) + 핫키 Chip + "Toggle on/off" 설명
  • IPC / ConfigService 연동:
    • config:set({ key: 'dictationEnabled', value: boolean })
    • config:set({ key: 'agentModeEnabled', value: boolean })
    • config:set({ key: 'handsFreeEnabled', value: boolean })
    • AppConfig에 3개 필드 추가 (src/shared/types.ts)
  • HotkeyService 연동: 모드가 비활성이면 해당 핫키 이벤트 무시

8.4 d3roPalette SSOT 확장

  • 파일: src/renderer/theme.ts
  • 현재 d3roPalette에 누락된 시맨틱 토큰 추가:
    export const d3roPalette = {
      // ...기존 유지...
      interactive: {
        /** 비활성 아이콘/라벨 (현재 '#77797c' 매직넘버로 사용 중) */
        muted: '#77797c',
        /** 호버 시 앰버로 전환할 때 사용 */
        hoverAccent: '#f25b29',
        /** 호버 시 위험 색상 */
        hoverDanger: '#ef4444',
      },
      metadata: {
        /** 보조 정보 텍스트 (현재 '#5c2615' 매직넘버) */
        dim: '#5c2615',
      },
      sidebar: {
        bg: '#1e1f21',
        activeBtn: '#242528',
        activeBtnPressed: '#1a1a1c',
      },
      shadow: {
        /** 물리 버튼 돌출 */
        buttonRaised: '0 2px 4px rgba(0,0,0,0.3), inset 0 1px 1px rgba(255,255,255,0.06)',
        /** 물리 버튼 눌림 */
        buttonPressed: 'inset 0 2px 4px rgba(0,0,0,0.6)',
        /** 인셋 패널 */
        inset: 'inset 0 2px 6px rgba(0,0,0,0.6), 0 1px 1px rgba(255,255,255,0.05)',
      },
    } as const;
    
  • 목표: 모든 컴포넌트에서 '#77797c', '#5c2615', '#1e1f21', '#242528' 등 매직넘버를 d3roPalette.* 참조로 교체

8.5 매직넘버 ~40곳 제거

  • 대상 파일 (grep 결과 기준):
    • src/renderer/components/AppLayout.tsx (~15곳)
    • src/renderer/pages/HistoryPage.tsx (~6곳)
    • src/renderer/pages/DictionaryPage.tsx (~5곳)
    • src/renderer/pages/CommandsPage.tsx (~5곳)
    • src/renderer/components/StatusBar.tsx (~5곳)
    • src/renderer/components/ds/PhysicalButton.tsx (~5곳)
    • src/renderer/components/ds/MetalCard.tsx (~4곳)
    • src/renderer/components/ds/CrtDisplay.tsx (~5곳)
    • src/renderer/components/ds/Led.tsx (~4곳)
  • 규칙: theme.ts에서 d3roPalette / d3roFontMono export한 값만 사용
  • 예외: DS 컴포넌트 내부의 WebGL 셰이더 uniform 값은 매직넘버 허용 (JS→GLSL 변환 복잡)
  • 검증: grep -rn '#[0-9a-fA-F]{6}' src/renderer/ --include='*.tsx' 결과에서 theme.ts 외 0건

8.6 DashboardPage 기능 중심 재작성

  • 파일: src/renderer/pages/DashboardPage.tsx
  • 현재 CRT+LED 인스트루먼트 패널 → 기능 중심 레이아웃으로 교체:
    1. 상단: 통계 카드 4개 (MetalCard 사용)
      • 총 세션 수 / 오늘 세션 수 / 총 녹음 시간 / 연속 사용 일수
      • PhosphorText로 값 표시, 각 카드에 Led 인디케이터
    2. 중단: 최근 히스토리 (5~10건)
      • 날짜 그룹핑 (오늘/어제/이번 주)
      • 원본 텍스트 + 모드 Chip + 녹음 시간
      • 클릭 시 히스토리 상세 or 복사
    3. 하단: 시스템 상태 패널
      • Ollama 연결 상태 (Led green/red)
      • STT 엔진 상태 (모델명 + ready/loading)
      • 현재 핫키 바인딩 표시
  • IPC 호출: stats:getSummary, history:getAll({ page: 0, pageSize: 10 }), llm:getStatus, stt:getStatus
  • CRT 디스플레이는 제거하지 않고 축소하여 시스템 상태 영역에 배치 (옵션)

8.7 HistoryPage 날짜 그룹핑

  • 파일: src/renderer/pages/HistoryPage.tsx
  • 히스토리 목록을 날짜별 섹션으로 그룹핑:
    • "오늘" / "어제" / "이번 주" / "이번 달" / "YYYY년 M월"
    • 각 섹션 헤더: PhosphorText variant="label" + 항목 수
  • 그룹핑 유틸 함수:
    function groupByDate(entries: HistoryEntry[]): Map<string, HistoryEntry[]> {
      // createdAt 기준으로 그룹핑
      // 오늘/어제/이번주/이번달/그외 구분
    }
    
  • 날짜 섹션 사이 시각적 구분선 (Divider + 앰버 강조)
  • 스크롤 시 현재 날짜 섹션 sticky header (옵션)

8.8 recording-tip 색상 버그 수정

  • 파일: src/renderer/popups/recording-tip/style.css
  • 문제: 웨이브 바와 프로그레스 바 색상이 Speakly 기본값 #1F5DF2 (파란색)으로 하드코딩됨
  • 수정: D3RO 앰버 악센트 #f25b29로 변경
    .wave-bar {
      background: #f25b29;  /* was: #1F5DF2 */
    }
    .progress-bar {
      background: #f25b29;  /* was: #1F5DF2 */
    }
    
  • 에러 아이콘: 기존 #D32F2F#ef4444 (d3roPalette.tag.red)로 통일

8.9 AppConfig 타입 확장

  • 파일: src/shared/types.ts
  • 음성 모드 토글 3개 필드 추가:
    export interface AppConfig {
      // ...기존 필드 유지...
      /** 받아쓰기 모드 활성화 (hold-to-talk) */
      dictationEnabled: boolean;
      /** Agent 모드 활성화 (더블프레스, dictation 의존) */
      agentModeEnabled: boolean;
      /** 핸즈프리 모드 활성화 (토글) */
      handsFreeEnabled: boolean;
    }
    
  • ConfigService 기본값 설정: dictationEnabled: true, agentModeEnabled: false, handsFreeEnabled: false

8.10 DS 컴포넌트 d3roPalette 참조 전환

  • 대상 파일:
    • src/renderer/components/ds/Led.tsx: 색상 맵을 d3roPalette.accent.*, d3roPalette.tag.*에서 가져오기
    • src/renderer/components/ds/PhysicalButton.tsx: #242528, #1a1a1c, #f25b29, #77797c → 팔레트 참조
    • src/renderer/components/ds/MetalCard.tsx: #1b1c1e, #242427, #2a2a2d → 팔레트 참조
    • src/renderer/components/ds/CrtDisplay.tsx: #1a1a1c, #050605, #f25b29 → 팔레트 참조 (셰이더 uniform 제외)
  • DS 컴포넌트가 직접 hex 리터럴을 갖지 않고, 반드시 d3roPalette 또는 theme.palette에서 읽도록 변경
  • import { d3roPalette } from '../../theme' 패턴 통일

Speakly RE 참조

  • Settings.js: General 탭에 음성 모드 토글 3개 (Dictation/Agent/Hands-free), 각 모드별 핫키 Chip, 마이크 테스트
  • HotkeyRecordModal.js: keydown 리스너 → 조합키 Chip 표시, 시스템 예약키 블랙리스트, platform-specific 키 라벨
  • CustomInstructionPage.js: 커스텀 명령어별 핫키 바인딩 UI (재활용 패턴)
  • Dashboard.js: 통계 카드 4개, 최근 히스토리 날짜 그룹핑 (오늘/어제/이전)
  • HotkeyConfig.js: VK 코드 → 표시 문자열 매핑 테이블, 시스템 예약키 감지

완료 조건

  • Settings에서 핫키 변경 가능 (HotkeyRecordModal 작동)
  • 변경된 핫키로 녹음 → 전사 → 삽입 사이클 테스트 가능
  • Settings에 음성 모드 토글 3개 표시 + 설정 저장/로드
  • Dictation OFF → Agent 자동 비활성 연동 작동
  • DashboardPage에 통계 + 최근 히스토리 + 시스템 상태 표시
  • HistoryPage 날짜 그룹핑 (오늘/어제/이번 주 등)
  • recording-tip 웨이브 바/프로그레스 바 색상이 앰버(#f25b29)
  • grep '#[0-9a-fA-F]{6}' src/renderer/**/*.tsx 결과에서 theme.ts 외 매직넘버 0건
  • npm run typecheck 통과
  • 모든 DS 컴포넌트가 d3roPalette 참조 (hex 리터럴 직접 사용 없음)