asyouplz_SpeechNote/docs/component-guide.md
asyouplz 7f776af999 Initial commit: Obsidian Speech-to-Text Plugin
- Set up project structure and architecture
- Implement core transcription services with OpenAI Whisper API
- Add comprehensive UI components and settings
- Create documentation in Korean and English
- Establish testing framework and examples

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-25 20:00:43 +09:00

20 KiB

컴포넌트 가이드 (Component Guide)

목차

  1. 개요
  2. UI 컴포넌트
  3. 서비스 컴포넌트
  4. 패턴 구현
  5. 통합 예제
  6. 베스트 프랙티스

개요

이 문서는 Speech-to-Text 플러그인의 주요 컴포넌트들의 구조, 사용법, 통합 방법을 설명합니다.

컴포넌트 아키텍처

graph TD
    A[Main Plugin] --> B[Application Layer]
    A --> C[Infrastructure Layer]
    A --> D[UI Layer]
    
    B --> B1[EditorService]
    B --> B2[TextInsertionHandler]
    B --> B3[EventManager]
    B --> B4[StateManager]
    
    C --> C1[WhisperService]
    C --> C2[SettingsManager]
    C --> C3[FileUploadManager]
    C --> C4[Logger]
    
    D --> D1[FilePickerModal]
    D --> D2[DragDropZone]
    D --> D3[ProgressIndicator]
    D --> D4[RecentFiles]

UI 컴포넌트

DragDropZone

파일 드래그 앤 드롭 기능을 제공하는 컴포넌트입니다.

기본 사용법

import { DragDropZone } from './ui/components/DragDropZone';

const dropZone = new DragDropZone(container, {
    onFileDrop: async (files) => {
        for (const file of files) {
            await processFile(file);
        }
    },
    acceptedFormats: ['.m4a', '.mp3', '.wav'],
    maxFileSize: 25 * 1024 * 1024
});

// 활성화/비활성화
dropZone.setEnabled(true);

// 정리
dropZone.destroy();

주요 기능

  • 파일 검증: 형식과 크기 자동 검증
  • 시각적 피드백: 드래그 오버 시 하이라이트
  • 다중 파일 지원: 여러 파일 동시 처리
  • 에러 처리: 잘못된 파일 형식 알림

FilePickerModal

파일 선택 모달 다이얼로그 컴포넌트입니다.

기본 사용법

import { FilePickerModal } from './ui/modals/FilePickerModal';

class AudioFilePickerModal extends FilePickerModal {
    constructor(app: App, onSelect: (file: TFile) => void) {
        super(app);
        this.onSelect = onSelect;
    }
    
    protected getItems(): TFile[] {
        return this.app.vault.getFiles()
            .filter(f => this.isAudioFile(f));
    }
    
    protected onChooseItem(file: TFile): void {
        this.onSelect(file);
    }
}

// 사용
const modal = new AudioFilePickerModal(app, async (file) => {
    await transcriptionService.transcribe(file);
});
modal.open();

주요 기능

  • 검색 기능: 실시간 파일 검색
  • 최근 파일: 최근 사용한 파일 표시
  • 파일 미리보기: 파일 정보 표시
  • 키보드 단축키: Enter/Esc 지원

ProgressIndicator

진행 상황을 표시하는 컴포넌트입니다.

기본 사용법

import { ProgressIndicator } from './ui/components/ProgressIndicator';

const progress = new ProgressIndicator();

// 진행 상황 업데이트
progress.update({
    percentage: 50,
    message: '파일 처리 중...',
    status: 'processing'
});

// 완료
progress.complete('변환 완료!');

// 에러
progress.error('변환 실패');

// 숨기기
progress.hide();

상태 타입

type ProgressStatus = 
    | 'idle'        // 대기
    | 'preparing'   // 준비 중
    | 'uploading'   // 업로드 중
    | 'processing'  // 처리 중
    | 'completed'   // 완료
    | 'error';      // 에러

RecentFiles

최근 사용한 파일 목록을 관리하는 컴포넌트입니다.

기본 사용법

import { RecentFiles } from './ui/components/RecentFiles';

const recentFiles = new RecentFiles(maxItems = 10);

// 파일 추가
recentFiles.add(file);

// 목록 가져오기
const files = recentFiles.getAll();

// 특정 파일 제거
recentFiles.remove(file.path);

// 전체 초기화
recentFiles.clear();

서비스 컴포넌트

WhisperService

OpenAI Whisper API와 통신하는 핵심 서비스입니다.

초기화 및 설정

import { WhisperService } from './infrastructure/api/WhisperService';
import { Logger } from './infrastructure/logging/Logger';

const logger = new Logger('WhisperService');
const whisperService = new WhisperService(apiKey, logger);

음성 변환

// 기본 변환
const response = await whisperService.transcribe(audioBuffer);

// 옵션과 함께 변환
const response = await whisperService.transcribe(audioBuffer, {
    language: 'ko',
    prompt: '이전 대화 컨텍스트...',
    temperature: 0.2,
    responseFormat: 'verbose_json'
});

// 세그먼트 정보 포함
if (response.segments) {
    response.segments.forEach(segment => {
        console.log(`[${segment.start}-${segment.end}] ${segment.text}`);
    });
}

에러 처리

try {
    const response = await whisperService.transcribe(audioBuffer);
} catch (error) {
    if (error instanceof AuthenticationError) {
        // API 키 문제
        await settingsManager.setApiKey('');
    } else if (error instanceof RateLimitError) {
        // 재시도 로직
        setTimeout(() => retry(), error.retryAfter * 1000);
    } else if (error instanceof FileTooLargeError) {
        // 파일 압축 필요
        const compressed = await compressAudio(audioBuffer);
        await whisperService.transcribe(compressed);
    }
}

SettingsManager

플러그인 설정을 관리하는 서비스입니다.

설정 로드 및 저장

import { SettingsManager } from './infrastructure/storage/SettingsManager';

const settingsManager = new SettingsManager(plugin, logger);

// 설정 로드
const settings = await settingsManager.load();

// 개별 설정 값 가져오기
const language = settingsManager.get('language');
const autoInsert = settingsManager.get('autoInsert');

// 설정 업데이트
await settingsManager.set('language', 'ko');
await settingsManager.set('temperature', 0.3);

// 전체 설정 저장
await settingsManager.save({
    ...settings,
    language: 'en'
});

API 키 관리

// API 키 설정 (자동 암호화)
const success = await settingsManager.setApiKey('sk-...');

// API 키 가져오기 (복호화됨)
const apiKey = settingsManager.getApiKey();

// 마스킹된 API 키 표시
const masked = settingsManager.getMaskedApiKey();
console.log(masked); // "sk-XXXXX...XXXX"

FileUploadManager

대용량 파일 처리 및 업로드를 관리합니다.

파일 처리

import { FileUploadManager } from './infrastructure/api/FileUploadManager';

const uploadManager = new FileUploadManager(vault, logger);

// 진행 상황 콜백과 함께 파일 처리
const processed = await uploadManager.prepareAudioFile(file, (progress) => {
    console.log(`${progress.percentage}% - ${progress.message}`);
    progressIndicator.update(progress);
});

console.log(`원본: ${processed.originalSize}, 처리 후: ${processed.processedSize}`);

청크 업로드

// 대용량 파일을 청크로 나누어 업로드
for await (const chunk of uploadManager.uploadInChunks(buffer)) {
    await sendChunkToServer(chunk);
}

// 취소 기능
uploadManager.cancel();

EditorService

Obsidian 에디터와의 통합을 담당합니다.

텍스트 삽입

import { EditorService } from './application/EditorService';

const editorService = new EditorService(app, eventManager, logger);

// 커서 위치에 삽입
await editorService.insertAtCursor("변환된 텍스트");

// 선택 영역 대체
await editorService.replaceSelection("새로운 텍스트");

// 문서 끝에 추가
await editorService.appendToDocument("\n\n## 추가 내용");

// 특정 위치에 삽입
const position = { line: 10, ch: 0 };
await editorService.insertAtPosition("삽입할 텍스트", position);

Undo/Redo 지원

// 작업 실행 (자동으로 히스토리에 기록)
await editorService.insertAtCursor("텍스트", true);

// Undo
await editorService.undo();

// Redo
await editorService.redo();

// 히스토리 초기화
editorService.clearHistory();

TextInsertionHandler

텍스트 포맷팅과 삽입을 처리합니다.

포맷팅 옵션

import { TextInsertionHandler } from './application/TextInsertionHandler';

const insertionHandler = new TextInsertionHandler(
    editorService, 
    eventManager, 
    logger
);

// 인용구 형식으로 삽입
await insertionHandler.insertText("변환된 텍스트", {
    mode: 'cursor',
    format: 'quote',
    quoteAuthor: 'Speaker Name'
});

// 코드 블록으로 삽입
await insertionHandler.insertText("코드 내용", {
    mode: 'cursor',
    format: 'code',
    codeLanguage: 'javascript'
});

// 타임스탬프와 함께 삽입
await insertionHandler.insertText("텍스트", {
    mode: 'cursor',
    format: 'plain',
    addTimestamp: true,
    timestampFormat: 'YYYY-MM-DD HH:mm:ss'
});

템플릿 사용

// 템플릿 적용
await insertionHandler.insertText("내용", {
    mode: 'cursor',
    format: 'plain',
    template: '## {{date}} 회의록\n\n{{content}}\n\n---\n작성: {{time}}'
});

EventManager

애플리케이션 전체의 이벤트를 관리합니다.

이벤트 구독

import { EventManager } from './application/EventManager';

const eventManager = new EventManager(logger);

// 이벤트 리스너 등록
const unsubscribe = eventManager.on('transcription:complete', (data) => {
    console.log(`변환 완료: ${data.text.substring(0, 50)}...`);
    
    if (data.autoInsert) {
        insertionHandler.insertText(data.text, insertOptions);
    }
});

// 일회성 리스너
eventManager.once('file:selected', (data) => {
    console.log(`파일 선택됨: ${data.file.path}`);
});

// 구독 해제
unsubscribe();

고급 기능

// 이벤트 디바운싱 (연속 호출 방지)
eventManager.debounce('settings:changed', 500, (data) => {
    saveSettings(data);
});

// 이벤트 쓰로틀링 (호출 빈도 제한)
eventManager.throttle('transcription:progress', 100, (data) => {
    updateProgressBar(data.progress);
});

// 이벤트 체인
eventManager.chain(
    'transcription:complete',
    'editor:text-inserted',
    (data) => ({ text: data.text, position: 'cursor' })
);

// 이벤트 필터링
eventManager.filter(
    'file:validated',
    (data) => data.valid === true,
    (data) => processValidFile(data.file)
);

패턴 구현

Singleton Pattern

전역적으로 단일 인스턴스를 보장합니다.

import { SingletonDecorator } from './patterns/Singleton';

@SingletonDecorator
class GlobalStateManager {
    private state: AppState;
    
    getState(): AppState {
        return this.state;
    }
}

// 어디서든 동일한 인스턴스
const manager1 = new GlobalStateManager();
const manager2 = new GlobalStateManager();
console.log(manager1 === manager2); // true

Observer Pattern

이벤트 기반 통신을 구현합니다.

import { EventEmitter } from './patterns/Observer';

interface CustomEvents {
    'data:updated': { id: string; value: any };
    'error:occurred': { error: Error };
}

class DataService extends EventEmitter<CustomEvents> {
    updateData(id: string, value: any) {
        // 데이터 업데이트
        this.emit('data:updated', { id, value });
    }
}

Factory Pattern

객체 생성을 추상화합니다.

import { ServiceFactory } from './patterns/Factory';

const factory = new ServiceFactory();

// 서비스 등록
factory.register('whisper', () => new WhisperService(apiKey, logger));
factory.register('settings', () => new SettingsManager(plugin));

// 서비스 생성
const whisperService = factory.create('whisper');
const settingsManager = factory.create('settings');

통합 예제

완전한 음성 변환 플로우

class SpeechToTextWorkflow {
    constructor(
        private whisperService: WhisperService,
        private uploadManager: FileUploadManager,
        private editorService: EditorService,
        private insertionHandler: TextInsertionHandler,
        private eventManager: EventManager,
        private logger: Logger
    ) {}
    
    async processAudioFile(file: TFile): Promise<void> {
        try {
            // 1. 시작 이벤트 발생
            this.eventManager.emit('transcription:start', {
                fileName: file.name,
                fileSize: file.stat.size
            });
            
            // 2. 파일 준비 및 검증
            const processed = await this.uploadManager.prepareAudioFile(
                file,
                (progress) => {
                    this.eventManager.emit('transcription:progress', {
                        progress: progress.percentage * 0.3, // 0-30%
                        message: progress.message
                    });
                }
            );
            
            // 3. API 호출
            this.eventManager.emit('transcription:progress', {
                progress: 30,
                message: 'Transcribing audio...'
            });
            
            const response = await this.whisperService.transcribe(
                processed.buffer,
                {
                    language: this.settings.language,
                    responseFormat: 'verbose_json'
                }
            );
            
            // 4. 텍스트 포맷팅 및 삽입
            this.eventManager.emit('transcription:progress', {
                progress: 90,
                message: 'Formatting text...'
            });
            
            await this.insertionHandler.insertText(response.text, {
                mode: this.settings.insertPosition,
                format: 'plain',
                addTimestamp: this.settings.timestampFormat !== 'none',
                language: response.language
            });
            
            // 5. 완료 이벤트
            this.eventManager.emit('transcription:complete', {
                text: response.text,
                duration: processed.metadata.duration || 0
            });
            
            // 6. 성공 알림
            new Notice(`Transcription complete! (${response.language})`);
            
        } catch (error) {
            // 에러 처리
            this.eventManager.emit('transcription:error', {
                error: error as Error,
                fileName: file.name
            });
            
            this.logger.error('Transcription failed', error as Error);
            new Notice(`Failed: ${(error as Error).message}`);
        }
    }
}

설정 UI 통합

class SettingsTab extends PluginSettingTab {
    constructor(
        app: App,
        plugin: Plugin,
        private settingsManager: SettingsManager,
        private whisperService: WhisperService
    ) {
        super(app, plugin);
    }
    
    display(): void {
        const { containerEl } = this;
        containerEl.empty();
        
        // API 키 설정
        new Setting(containerEl)
            .setName('OpenAI API Key')
            .setDesc('Your OpenAI API key for Whisper')
            .addText(text => text
                .setPlaceholder('sk-...')
                .setValue(this.settingsManager.getMaskedApiKey())
                .onChange(async (value) => {
                    if (value && !value.includes('*')) {
                        const isValid = await this.whisperService.validateApiKey(value);
                        if (isValid) {
                            await this.settingsManager.setApiKey(value);
                            new Notice('API key validated and saved');
                        } else {
                            new Notice('Invalid API key');
                        }
                    }
                })
            );
        
        // 언어 설정
        new Setting(containerEl)
            .setName('Language')
            .setDesc('Transcription language')
            .addDropdown(dropdown => dropdown
                .addOptions({
                    'auto': 'Auto-detect',
                    'ko': 'Korean',
                    'en': 'English',
                    'ja': 'Japanese',
                    'zh': 'Chinese'
                })
                .setValue(this.settingsManager.get('language'))
                .onChange(async (value) => {
                    await this.settingsManager.set('language', value);
                })
            );
    }
}

베스트 프랙티스

1. 에러 처리

항상 try-catch로 에러를 처리하고 사용자에게 적절한 피드백을 제공하세요.

try {
    await riskyOperation();
} catch (error) {
    // 로깅
    logger.error('Operation failed', error);
    
    // 사용자 알림
    new Notice(errorHandler.getUserMessage(error));
    
    // 이벤트 발생
    eventManager.emit('error:occurred', { error });
    
    // 복구 가능한 경우 재시도
    if (errorHandler.isRetryable(error)) {
        await retry(riskyOperation);
    }
}

2. 리소스 정리

컴포넌트 사용 후 항상 정리하세요.

class MyComponent {
    private subscriptions: Unsubscribe[] = [];
    
    initialize() {
        // 이벤트 구독
        this.subscriptions.push(
            eventManager.on('event', this.handleEvent)
        );
    }
    
    destroy() {
        // 모든 구독 해제
        this.subscriptions.forEach(unsub => unsub());
        
        // 리소스 정리
        this.uploadManager?.cleanup();
        this.whisperService?.cancel();
    }
}

3. 비동기 작업 관리

class AsyncOperationManager {
    private operations = new Set<Promise<any>>();
    
    async execute<T>(operation: () => Promise<T>): Promise<T> {
        const promise = operation();
        this.operations.add(promise);
        
        try {
            const result = await promise;
            return result;
        } finally {
            this.operations.delete(promise);
        }
    }
    
    async cancelAll() {
        await Promise.allSettled(this.operations);
        this.operations.clear();
    }
}

4. 타입 안전성

TypeScript의 타입 시스템을 최대한 활용하세요.

// 타입 가드 사용
function isWhisperResponse(obj: any): obj is WhisperResponse {
    return obj && typeof obj.text === 'string';
}

// 제네릭 활용
function createService<T extends IService>(
    ServiceClass: new (...args: any[]) => T,
    ...args: any[]
): T {
    return new ServiceClass(...args);
}

// 유틸리티 타입
type DeepPartial<T> = {
    [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};

5. 성능 최적화

// 디바운싱
const debouncedSave = debounce(saveSettings, 500);

// 메모이제이션
const memoizedExpensiveOperation = memoize(expensiveOperation);

// 레이지 로딩
const LazyService = lazy(() => import('./HeavyService'));

컴포넌트 생명주기

초기화 순서

  1. Logger 초기화: 로깅 시스템 설정
  2. EventManager 생성: 이벤트 시스템 준비
  3. Settings 로드: 설정 불러오기
  4. Services 초기화: 각 서비스 인스턴스 생성
  5. UI 컴포넌트 생성: UI 요소 초기화
  6. 이벤트 리스너 등록: 이벤트 구독 설정

종료 순서

  1. 진행 중인 작업 취소: 모든 비동기 작업 중단
  2. 이벤트 리스너 해제: 구독 정리
  3. UI 컴포넌트 정리: DOM 요소 제거
  4. 서비스 정리: 리소스 해제
  5. 설정 저장: 마지막 상태 저장
  6. Logger 종료: 로그 플러시

디버깅 도구

개발 모드 활성화

// 개발 환경에서만 디버그 모드 활성화
if (process.env.NODE_ENV === 'development') {
    eventManager.setDebugMode(true);
    logger.setLevel(LogLevel.DEBUG);
    
    // 전역 디버그 객체
    window.speechToTextDebug = {
        services: {
            whisper: whisperService,
            settings: settingsManager,
            editor: editorService
        },
        eventManager,
        logger
    };
}

이벤트 모니터링

// 모든 이벤트 로깅
const events = [
    'transcription:start',
    'transcription:progress',
    'transcription:complete',
    'transcription:error'
];

events.forEach(event => {
    eventManager.on(event as any, (data) => {
        console.log(`[Event] ${event}:`, data);
    });
});

// 통계 확인
console.log('Event Stats:', eventManager.getStats());
console.log('Event History:', eventManager.getHistory());

최종 업데이트: 2025-08-22
버전: 1.0.0