dha-sleep-app-web 수면 오디오 분석 기능 설계
버전: 1.0.0 작성일: 2026-01-20 상태: Draft 대상 앱:
apps/dha-sleep-app-web
목차
1. 개요
1.1 기능 목적
dha-sleep-app-web에 스마트폰 마이크 기반 수면 오디오 분석 기능을 통합합니다.
1.2 핵심 전략: 단일 앱 통합 (Option B)
별도 앱을 만들지 않고, 기존 dha-sleep-app-web에 모든 기능을 통합합니다.
┌─────────────────────────────────────────────────────────────┐
│ dha-sleep-app-web (단일 앱) │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────┐ │
│ │ 웨어러블 착용 사용자 │ │
│ │ • 웨어러블 수면 단계 사용 (정확) │ │
│ │ • 오디오 녹음 → 학습 데이터 수집 │ │
│ │ • 코골이/무호흡 감지 (마이크) │ │
│ └─────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────┐ │
│ │ 웨어러블 미착용 사용자 │ │
│ │ • TFLite 모델로 수면 단계 추정 │ │
│ │ • 코골이/무호흡 감지 (마이크) │ │
│ │ • "추정치" 명시 │ │
│ └─────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
1.3 핵심 기능
| 기능 | 데이터 소스 | 정확도 | 우선순위 |
|---|---|---|---|
| 코골이 감지 | 마이크 | 높음 (90%+) | P0 |
| 무호흡 의심 감지 | 마이크 | 중간 (80%+) | P0 |
| 수면 단계 (웨어러블) | HealthKit/Health Connect | 높음 (85%) | P0 |
| 수면 단계 (ML 추정) | 마이크 + TFLite | 중간 (70%) | P1 |
| 수면 품질 리포트 | 복합 데이터 | - | P1 |
1.4 주요 제약사항
- 배터리: 밤새 녹음 시 배터리 소모 30% 이하
- 저장공간: 오디오 원본 저장 최소화 (스펙트로그램만 저장)
- 프라이버시: GDPR 준수, 오디오 원본 서버 전송 금지
- 오프라인: 기본 분석은 오프라인에서도 동작
2. 시스템 아키텍처
2.1 전체 시스템 구성도
┌─────────────────────────────────────────────────────────────────────────┐
│ 사용자 디바이스 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ ┌─────────────────────────────────────────────┐ │
│ │ Apple Watch │ │ dha-sleep-app-web │ │
│ │ Galaxy Watch│ │ ┌─────────────────────────────────────┐ │ │
│ │ │────▶│ │ Capacitor Native Layer │ │ │
│ │ 심박/움직임 │ │ │ ┌───────────┐ ┌───────────────┐ │ │ │
│ │ 산소포화도 │ │ │ │ Audio │ │ TFLite │ │ │ │
│ └─────────────┘ │ │ │ Recorder │ │ Interpreter │ │ │ │
│ │ │ │ │ Plugin │ │ Plugin │ │ │ │
│ │ │ │ └───────────┘ └───────────────┘ │ │ │
│ │ │ └─────────────────────────────────────┘ │ │
│ │ │ │ │ │
│ │ │ ┌─────────────────┴───────────────────┐ │ │
│ │ │ │ React Application │ │ │
│ │ │ │ ┌─────────┐ ┌─────────────────┐ │ │ │
│ │ │ │ │ Sleep │ │ Sleep Stage │ │ │ │
│ │ │ │ │ Analyzer│ │ Visualizer │ │ │ │
│ │ │ │ │ Service │ │ Component │ │ │ │
│ │ │ │ └─────────┘ └─────────────────┘ │ │ │
│ │ │ └─────────────────────────────────────┘ │ │
│ ▼ └─────────────────────────────────────────────┘ │
│ ┌─────────────┐ │ │
│ │ HealthKit / │ │ │
│ │ Health │◀────────────────────────┘ │
│ │ Connect │ 수면 단계 조회 │
│ └─────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
│
│ 학습 데이터 (동의 시)
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ GCP Backend │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────────┐ │
│ │ Cloud Run │ │ Cloud │ │ Cloud Storage │ │
│ │ dta-wide-api│ │ Firestore │ │ ML Training Data │ │
│ │ │────▶│ 수면 분석 │ │ (익명화된 스펙트로그램) │ │
│ │ │ │ 결과 저장 │ └─────────────────────────┘ │
│ └─────────────┘ └─────────────┘ │ │
│ │ │
│ ┌─────────────────────────────────────────────────┐│ │
│ │ Vertex AI / Cloud Functions ││ │
│ │ ┌─────────────────────────────────────────┐ ││ │
│ │ │ ML Training Pipeline │◀──┘│ │
│ │ │ • 데이터 전처리 │ │ │
│ │ │ • 모델 학습 (TensorFlow) │ │ │
│ │ │ • TFLite 변환 │ │ │
│ │ │ • 모델 버전 관리 │ │ │
│ │ └─────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
2.2 앱 디렉토리 구조 (추가되는 파일들)
apps/dha-sleep-app-web/
├── lib/
│ ├── native/
│ │ ├── audio-recorder.ts # 오디오 녹음 추상화
│ │ ├── sleep-audio-analyzer.ts # 수면 분석 서비스 추상화
│ │ └── tflite-interpreter.ts # TFLite 모델 실행
│ │
│ ├── ml/
│ │ ├── feature-extractor.ts # 오디오 특징 추출
│ │ ├── spectrogram-generator.ts # 스펙트로그램 생성
│ │ ├── sleep-stage-classifier.ts # 수면 단계 분류
│ │ └── training-data-collector.ts # 학습 데이터 수집
│ │
│ └── sleep-analysis/
│ ├── analyzer-factory.ts # 분석기 팩토리
│ ├── wearable-analyzer.ts # 웨어러블 기반 분석
│ ├── ml-analyzer.ts # ML 기반 분석
│ └── snoring-detector.ts # 코골이 감지
│
├── hooks/
│ ├── use-sleep-recorder.ts # 수면 녹음 훅
│ ├── use-sleep-analysis.ts # 수면 분석 훅
│ └── use-wearable-sync.ts # 웨어러블 동기화 훅
│
├── components/
│ └── sleep-analysis/
│ ├── sleep-recorder-control.tsx # 녹음 시작/종료 UI
│ ├── realtime-monitor.tsx # 실시간 모니터링
│ ├── hypnogram-chart.tsx # 수면 단계 차트
│ ├── snoring-events.tsx # 코골이 이벤트 목록
│ ├── sleep-report.tsx # 종합 리포트
│ └── ml-data-consent-sheet.tsx # ML 데이터 동의 시트
│
├── ios/
│ └── App/
│ └── Plugins/
│ ├── SleepAudioRecorder/ # iOS 오디오 녹음 플러그인
│ │ ├── SleepAudioRecorderPlugin.swift
│ │ ├── AudioRecorder.swift
│ │ └── AudioAnalyzer.swift
│ └── TFLiteInterpreter/ # iOS TFLite 플러그인
│ └── TFLiteInterpreterPlugin.swift
│
├── android/
│ └── app/src/main/java/com/welt/dha/sleep/plugins/
│ ├── sleepaudio/
│ │ ├── SleepAudioRecorderPlugin.kt
│ │ ├── AudioRecorderService.kt # Foreground Service
│ │ └── AudioAnalyzer.kt
│ └── tflite/
│ └── TFLiteInterpreterPlugin.kt
│
└── assets/
└── models/
├── snoring_detector.tflite # 코골이 감지 모델
└── sleep_stage_classifier.tflite # 수면 단계 분류 모델
2.3 데이터 흐름
[취침 시작]
│
▼
┌─────────────────────────────────────────┐
│ 오디오 녹음 시작 │
│ • Foreground Service (Android) │
│ • Background Mode (iOS) │
│ • 샘플링: 16kHz, Mono │
└─────────────────────────────────────────┘
│
▼ 30초마다
┌─────────────────────────────────────────┐
│ 실시간 분석 (온디바이스) │
│ 1. 오디오 → 스펙트로그램 변환 │
│ 2. 코골이 감지 모델 실행 (TFLite) │
│ 3. 이벤트 발생 시 앱에 알림 │
└─────────────────────────────────────────┘
│
▼
[기상]
│
├─────────────────────┐
▼ ▼
┌─────────────┐ ┌─────────────┐
│웨어러블 있음 │ │웨어러블 없음 │
│ │ │ │
│HealthKit에서│ │저장된 스펙트 │
│수면단계 조회 │ │로그램+TFLite│
│ │ │로 수면 추정 │
└──────┬──────┘ └──────┬──────┘
│ │
└────────┬─────────┘
▼
┌─────────────────────────────────────────┐
│ 수면 분석 결과 생성 │
│ • 수면 단계 타임라인 (Hypnogram) │
│ • 코골이/무호흡 이벤트 목록 │
│ • 수면 품질 점수 │
└─────────────────────────────────────────┘
3. 데이터 수집 파이프라인
3.1 학습 데이터 스키마
// types/ml-training-data.ts
interface TrainingDataSample {
sampleId: string; // UUID
anonymousUserId: string; // SHA-256 해시된 사용자 ID
sessionDate: string; // ISO date (시간 제외, 프라이버시)
epochIndex: number; // 30초 단위 인덱스
// 특징 데이터 (오디오 원본 대신)
spectrogram: number[][]; // Mel spectrogram (64 x 128)
features: {
mfcc: number[]; // MFCC 13개
spectralCentroid: number;
spectralBandwidth: number;
zeroCrossingRate: number;
rmsEnergy: number;
};
// 라벨 (웨어러블에서 획득)
label: {
sleepStage: 'awake' | 'light' | 'deep' | 'rem';
confidence: number; // 0-1
source: 'apple_watch' | 'galaxy_watch' | 'fitbit';
};
// 품질 지표
quality: {
audioQuality: number; // 0-1
labelConfidence: number; // 0-1
isTransitionPeriod: boolean; // 수면 단계 전환 구간 여부
};
}
interface CollectionSession {
sessionId: string;
anonymousUserId: string;
date: string;
totalEpochs: number;
validEpochs: number;
deviceInfo: {
platform: 'ios' | 'android';
osVersion: string;
appVersion: string;
wearableModel: string;
};
sleepSummary: {
totalDuration: number;
sleepEfficiency: number;
stageDistribution: {
awake: number;
light: number;
deep: number;
rem: number;
};
};
}
3.2 데이터 수집 흐름
[앱 - 수면 중]
1. 오디오 녹음 (30초 버퍼)
2. 스펙트로그램 변환
3. 특징 추출 (MFCC 등)
4. 임시 저장 (기기 내 SQLite)
│
▼
[앱 - 기상 후]
5. HealthKit/Health Connect에서 수면 단계 조회
6. 오디오 epoch와 수면 단계 시간 동기화
7. 라벨 매핑 (각 epoch에 수면 단계 할당)
8. 품질 필터링 (신뢰도 낮은 데이터 제외)
│
▼
[동의 확인]
9. 사용자 동의 확인 (ML 학습 데이터 제공)
10. 동의 시 → 익명화 처리
• userId → SHA-256 해시
• 정확한 시간 제거 (날짜만 유지)
• 기기 정보 일반화
│
▼
[서버]
11. Cloud Run API로 데이터 수신
12. 2차 품질 검증
13. Cloud Storage에 저장
14. 주기적 데이터셋 빌드 (TFRecord 형식)
3.3 데이터 품질 필터링 규칙
// lib/ml/quality-filter.ts
const qualityFilters = {
// 라벨 신뢰도
minLabelConfidence: 0.7, // 70% 이상
// 오디오 품질
minAudioQuality: 0.5, // 50% 이상
maxSilenceRatio: 0.95, // 95% 이상 무음이면 제외
minSnr: 10, // 최소 SNR 10dB
// 수면 단계 전환 구간 제외
excludeTransitionPeriods: true,
transitionBufferSeconds: 30, // 전환 전후 30초 제외
// 수면 단계 최소 지속 시간
minStageDuration: 60, // 1분 미만 수면 단계 제외
// 세션 품질
minSessionDuration: 180, // 최소 3시간 수면
minValidEpochRatio: 0.5, // 50% 이상 유효 데이터
};
3.4 예상 데이터 규모
| Phase | 사용자 | 기간 | 예상 Epochs | 저장 용량 |
|---|---|---|---|---|
| MVP | 50명 | 30일 | 630,000 | ~630MB |
| Beta | 500명 | 90일 | 18.9M | ~19GB |
| Production | 5,000명 | 180일 | 378M | ~378GB |
4. ML 모델 개발
4.1 코골이 감지 모델 (P0)
┌─────────────────────────────────────────────────────────────┐
│ 코골이 감지 모델 아키텍처 │
├─────────────────────────────────────────────────────────────┤
│ │
│ Input: Mel Spectrogram (64 x 64 x 1) - 2초 오디오 │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ DepthwiseSeparableConv2D(32) + BatchNorm + ReLU │ │
│ │ MaxPool(2,2) → 32 x 32 x 32 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ DepthwiseSeparableConv2D(64) + BatchNorm + ReLU │ │
│ │ MaxPool(2,2) → 16 x 16 x 64 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ DepthwiseSeparableConv2D(128) + BatchNorm + ReLU │ │
│ │ GlobalAveragePooling → 128 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Dense(64, ReLU) → Dropout(0.5) → Dense(1, Sigmoid) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ Output: Snoring Probability (0-1) │
│ │
│ • 모델 크기: ~200KB (INT8 양자화 후) │
│ • 추론 시간: ~5ms (모바일) │
│ • 목표 정확도: 90%+ │
│ │
└─────────────────────────────────────────────────────────────┘
4.2 수면 단계 분류 모델 (P1)
┌─────────────────────────────────────────────────────────────┐
│ 수면 단계 분류 모델 아키텍처 │
├─────────────────────────────────────────────────────────────┤
│ │
│ Input: Mel Spectrogram (64 x 128 x 1) - 30초 오디오 │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ CNN Feature Extractor │ │
│ │ Conv2D(32) → Conv2D(64) → Conv2D(128) │ │
│ │ 각 레이어: BatchNorm + ReLU + MaxPool │ │
│ │ Output: 8 x 32 x 128 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Reshape: (8, 32, 128) → (32, 1024) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Bidirectional LSTM(64) → Bidirectional LSTM(32) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Dense(64, ReLU) → Dropout(0.5) → Dense(4, Softmax) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ Output: [P(awake), P(light), P(deep), P(rem)] │
│ │
│ • 모델 크기: ~1.5MB (INT8 양자화 후) │
│ • 추론 시간: ~50ms (모바일) │
│ • 목표 정확도: 70%+ │
│ │
└─────────────────────────────────────────────────────────────┘
4.3 모델 평가 기준
| 모델 | 지표 | 목표 |
|---|---|---|
| 코골이 감지 | Accuracy | ≥ 90% |
| Precision | ≥ 85% | |
| Recall | ≥ 90% | |
| F1 Score | ≥ 87% | |
| 수면 단계 | Overall Accuracy | ≥ 70% |
| Cohen's Kappa | ≥ 0.55 | |
| Sleep/Wake 이진 | ≥ 85% |
4.4 모델 배포 전략
Phase 1: 앱 번들 포함 (권장 - 초기)
- 모델을 앱 바이너리에 포함
- 오프라인 동작 보장
- 앱 업데이트로 모델 갱신
Phase 2: 동적 다운로드 (향후)
- Firebase ML Model Hosting 사용
- 앱 업데이트 없이 모델 갱신
5. 앱 구현 상세
5.1 Capacitor 플러그인 인터페이스
// types/capacitor-sleep-audio.d.ts
declare module '@anthropic/capacitor-sleep-audio' {
export interface SleepAudioPlugin {
// 권한
checkPermissions(): Promise<PermissionStatus>;
requestPermissions(): Promise<PermissionStatus>;
// 녹음 제어
startRecording(options: RecordingOptions): Promise<void>;
stopRecording(): Promise<RecordingResult>;
isRecording(): Promise<{ recording: boolean }>;
// TFLite 모델
loadModel(options: ModelOptions): Promise<void>;
runInference(options: InferenceOptions): Promise<InferenceResult>;
unloadModel(): Promise<void>;
// 이벤트 리스너
addListener(
eventName: 'audioLevel',
callback: (data: AudioLevelEvent) => void
): Promise<PluginListenerHandle>;
addListener(
eventName: 'snoringDetected',
callback: (data: SnoringEvent) => void
): Promise<PluginListenerHandle>;
addListener(
eventName: 'analysisUpdate',
callback: (data: AnalysisUpdate) => void
): Promise<PluginListenerHandle>;
removeAllListeners(): Promise<void>;
}
export interface RecordingOptions {
sampleRate?: number; // default: 16000
channels?: number; // default: 1 (mono)
enableRealTimeAnalysis?: boolean;
analysisIntervalMs?: number; // default: 2000
lowPowerMode?: boolean;
}
export interface RecordingResult {
sessionId: string;
startTime: string;
endTime: string;
durationMs: number;
epochCount: number;
spectrograms: SpectrogramData[];
events: AudioEvent[];
}
export interface SnoringEvent {
timestamp: string;
durationMs: number;
intensity: number; // 0-1
confidence: number;
}
}
5.2 React Hook: 수면 녹음
// hooks/use-sleep-recorder.ts
import { useState, useCallback, useEffect, useRef } from 'react';
import { Capacitor } from '@capacitor/core';
import type { SleepAudioPlugin, RecordingOptions, RecordingResult, SnoringEvent } from '@anthropic/capacitor-sleep-audio';
export function useSleepRecorder() {
const [isRecording, setIsRecording] = useState(false);
const [audioLevel, setAudioLevel] = useState(-60);
const [snoringEvents, setSnoringEvents] = useState<SnoringEvent[]>([]);
const [recordingDuration, setRecordingDuration] = useState(0);
const [hasPermissions, setHasPermissions] = useState(false);
const pluginRef = useRef<SleepAudioPlugin | null>(null);
useEffect(() => {
if (Capacitor.isNativePlatform()) {
pluginRef.current = Capacitor.Plugins.SleepAudioRecorder as SleepAudioPlugin;
checkPermissions();
}
}, []);
const checkPermissions = async () => {
if (!pluginRef.current) return;
const status = await pluginRef.current.checkPermissions();
setHasPermissions(status.microphone === 'granted');
};
const requestPermissions = useCallback(async (): Promise<boolean> => {
if (!pluginRef.current) return false;
const status = await pluginRef.current.requestPermissions();
const granted = status.microphone === 'granted';
setHasPermissions(granted);
return granted;
}, []);
const startRecording = useCallback(async (options?: Partial<RecordingOptions>) => {
if (!pluginRef.current || isRecording) return;
// 리스너 설정
await pluginRef.current.addListener('audioLevel', (data) => {
setAudioLevel(data.level);
});
await pluginRef.current.addListener('snoringDetected', (event) => {
setSnoringEvents(prev => [...prev, event]);
});
// 녹음 시작
await pluginRef.current.startRecording({
sampleRate: 16000,
channels: 1,
enableRealTimeAnalysis: true,
analysisIntervalMs: 2000,
lowPowerMode: true,
...options
});
setIsRecording(true);
setSnoringEvents([]);
}, [isRecording]);
const stopRecording = useCallback(async (): Promise<RecordingResult | null> => {
if (!pluginRef.current || !isRecording) return null;
await pluginRef.current.removeAllListeners();
const result = await pluginRef.current.stopRecording();
setIsRecording(false);
return result;
}, [isRecording]);
return {
isRecording,
audioLevel,
snoringEvents,
recordingDuration,
startRecording,
stopRecording,
requestPermissions,
hasPermissions
};
}
5.3 분석기 팩토리 패턴
// lib/sleep-analysis/analyzer-factory.ts
export interface SleepAnalyzer {
analyze(recordingResult: RecordingResult): Promise<SleepAnalysisResult>;
getSource(): 'wearable' | 'ml_model';
}
// 웨어러블 있는 경우: 정확한 분석 + 학습 데이터 수집
class WearableSleepAnalyzer implements SleepAnalyzer {
async analyze(recordingResult: RecordingResult): Promise<SleepAnalysisResult> {
// 1. 웨어러블에서 수면 단계 가져오기 (정확)
const stages = await getWearableSleepStages(
new Date(recordingResult.startTime),
new Date(recordingResult.endTime)
);
// 2. 학습 데이터 수집 (동의 시)
if (await hasMLDataConsent()) {
await collectTrainingData(recordingResult, stages);
}
return {
stages,
source: 'wearable',
confidence: 0.85
};
}
getSource() { return 'wearable' as const; }
}
// 웨어러블 없는 경우: ML 모델로 추정
class MLSleepAnalyzer implements SleepAnalyzer {
private model: TFLiteModel;
async analyze(recordingResult: RecordingResult): Promise<SleepAnalysisResult> {
// TFLite 모델로 수면 단계 추정
const stages = await this.estimateSleepStages(recordingResult.spectrograms);
return {
stages,
source: 'ml_model',
confidence: 0.65,
isEstimate: true // UI에서 "추정치" 표시용
};
}
getSource() { return 'ml_model' as const; }
}
// 팩토리 함수
export async function createSleepAnalyzer(): Promise<SleepAnalyzer> {
const hasWearable = await checkWearableConnection();
return hasWearable ? new WearableSleepAnalyzer() : new MLSleepAnalyzer();
}
5.4 UI 컴포넌트: 수면 리포트
// components/sleep-analysis/sleep-report.tsx
'use client';
import { SleepAnalysisResult } from '@/lib/sleep-analysis/types';
import { HypnogramChart } from './hypnogram-chart';
import { SnoringTimeline } from './snoring-timeline';
import { Badge } from '@dta-wi-design-system/components/badge';
interface SleepReportProps {
result: SleepAnalysisResult;
}
export function SleepReport({ result }: SleepReportProps) {
return (
<div className="space-y-6">
{/* 데이터 소스 표시 */}
<div className="flex items-center justify-between">
<h2 className="text-xl font-bold">수면 분석 결과</h2>
{result.source === 'wearable' ? (
<Badge variant="success">Apple Watch 측정</Badge>
) : (
<Badge variant="secondary">AI 추정 • 참고용</Badge>
)}
</div>
{/* 수면 단계 차트 */}
<section>
<h3 className="font-semibold mb-2">수면 단계</h3>
<HypnogramChart stages={result.stages} />
</section>
{/* 수면 통계 */}
<section className="grid grid-cols-2 gap-4">
<StatCard
label="총 수면 시간"
value={formatDuration(result.statistics.totalSleepTime)}
/>
<StatCard
label="수면 효율"
value={`${result.statistics.sleepEfficiency}%`}
/>
<StatCard
label="깊은 수면"
value={formatDuration(result.statistics.stageDistribution.deep)}
/>
<StatCard
label="REM 수면"
value={formatDuration(result.statistics.stageDistribution.rem)}
/>
</section>
{/* 코골이 이벤트 */}
{result.snoringEvents.length > 0 && (
<section>
<h3 className="font-semibold mb-2">코골이 감지</h3>
<SnoringTimeline events={result.snoringEvents} />
</section>
)}
{/* ML 추정인 경우 안내 */}
{result.isEstimate && (
<p className="text-sm text-muted-foreground">
* 스마트워치 연동 시 더 정확한 분석이 가능합니다
</p>
)}
</div>
);
}
6. 서버 API
6.1 엔드포인트
# 학습 데이터 수집
POST /api/ml/training-data
Description: 익명화된 학습 데이터 업로드
Auth: Bearer Token (User)
Request:
- segments: TrainingDataSample[]
- metadata: CollectionSession
Response:
- uploadId: string
- acceptedCount: number
- rejectedCount: number
# 수면 분석 결과 저장
POST /api/sleep-analysis/sessions
Description: 수면 분석 세션 결과 저장
Auth: Bearer Token (User)
Request:
- sessionId: string
- startTime: ISO DateTime
- endTime: ISO DateTime
- stages: SleepStageSegment[]
- events: AudioEvent[]
- statistics: SleepStatistics
- source: 'wearable' | 'ml_model'
# 수면 분석 결과 조회
GET /api/sleep-analysis/sessions/:sessionId
GET /api/sleep-analysis/sessions?from=&to=&limit=
# 모델 버전 확인 (동적 모델 배포 시)
GET /api/ml/models/:modelName/version
Response:
- version: string
- downloadUrl: string (signed URL)
- checksum: string
6.2 데이터베이스 스키마
// prisma/schema.prisma (추가)
model SleepAnalysisSession {
id String @id @default(uuid())
userId String
startTime DateTime
endTime DateTime
durationMin Int
source SleepAnalysisSource // WEARABLE, ML_MODEL
confidence Float
// 통계
totalSleepMin Int
sleepEfficiency Float
awakenings Int
awakeMin Int
lightMin Int
deepMin Int
remMin Int
snoringMin Int
snoringPercentage Float
// 관계
user User @relation(fields: [userId], references: [id])
stages SleepStage[]
events SleepEvent[]
createdAt DateTime @default(now())
@@index([userId, startTime])
}
enum SleepAnalysisSource {
WEARABLE
ML_MODEL
}
model SleepStage {
id String @id @default(uuid())
sessionId String
startTime DateTime
endTime DateTime
stage SleepStageType
confidence Float
session SleepAnalysisSession @relation(fields: [sessionId], references: [id], onDelete: Cascade)
@@index([sessionId])
}
enum SleepStageType {
AWAKE
LIGHT
DEEP
REM
}
model SleepEvent {
id String @id @default(uuid())
sessionId String
type SleepEventType
timestamp DateTime
durationMs Int
intensity Float?
confidence Float
session SleepAnalysisSession @relation(fields: [sessionId], references: [id], onDelete: Cascade)
@@index([sessionId])
}
enum SleepEventType {
SNORING
APNEA_SUSPECT
MOVEMENT
TALKING
COUGHING
}
// ML 학습 데이터 (익명화)
model MLTrainingData {
id String @id @default(uuid())
anonymousUserId String // SHA-256 해시
sessionDate DateTime @db.Date
epochIndex Int
spectrogram Json // 64x128 array
features Json // MFCC, spectral features
sleepStage SleepStageType
labelConfidence Float
labelSource String // apple_watch, galaxy_watch
audioQuality Float
isValid Boolean @default(true)
createdAt DateTime @default(now())
@@index([sessionDate])
@@index([sleepStage])
}
7. 개발 로드맵
7.1 Phase 개요
┌─────────────────────────────────────────────────────────────┐
│ 개발 로드맵 │
├─────────────────────────────────────────────────────────────┤
│ │
│ Phase 1: 기반 구축 (4주) │
│ ━━━━━━━━━━━━━━━━━━━━━━ │
│ • Capacitor 오디오 녹음 플러그인 개발 │
│ • 백그라운드 녹음 구현 (iOS/Android) │
│ • 기본 UI 컴포넌트 개발 │
│ • HealthKit/Health Connect 수면 단계 조회 구현 │
│ │
│ Phase 2: 코골이 감지 MVP (4주) │
│ ━━━━━━━━━━━━━━━━━━━━━━━━ │
│ • 공개 데이터셋으로 코골이 감지 모델 학습 │
│ • TFLite 변환 및 앱 통합 │
│ • 실시간 코골이 감지 기능 구현 │
│ • 내부 테스트 및 모델 개선 │
│ │
│ Phase 3: 학습 데이터 수집 (4주) │
│ ━━━━━━━━━━━━━━━━━━━━━━━━━ │
│ • 학습 데이터 수집 파이프라인 구축 │
│ • 사용자 동의 플로우 구현 │
│ • 서버 API 및 데이터베이스 구현 │
│ • 베타 테스터 모집 및 데이터 수집 시작 │
│ │
│ Phase 4: 수면 단계 모델 개발 (6주) │
│ ━━━━━━━━━━━━━━━━━━━━━━━━━━━ │
│ • 수집된 데이터로 수면 단계 모델 학습 │
│ • 모델 최적화 및 양자화 │
│ • 앱 통합 및 A/B 테스트 │
│ • 모델 성능 모니터링 체계 구축 │
│ │
│ Phase 5: 정식 출시 준비 (4주) │
│ ━━━━━━━━━━━━━━━━━━━━━━━━━ │
│ • 종합 수면 리포트 기능 완성 │
│ • 성능 최적화 (배터리, 메모리) │
│ • QA 및 버그 수정 │
│ • 문서화 및 출시 │
│ │
│ 총 예상 기간: 22주 (약 5.5개월) │
│ │
└─────────────────────────────────────────────────────────────┘
7.2 마일스톤
| 마일스톤 | 시점 | 완료 기준 |
|---|---|---|
| M1 | Week 4 | iOS/Android 백그라운드 오디오 녹음 동작 |
| M2 | Week 8 | 코골이 감지 모델 90%+ 정확도 |
| M3 | Week 12 | 100+ 사용자 데이터 수집 시작 |
| M4 | Week 18 | 수면 단계 모델 70%+ 정확도 |
| M5 | Week 22 | App Store / Play Store 출시 |
8. 기술 스택
8.1 앱 (dha-sleep-app-web)
| 영역 | 기술 | 버전 |
|---|---|---|
| Framework | Next.js | 14.x |
| UI | React | 18.x |
| Styling | Tailwind CSS | 3.x |
| Native | Capacitor | 8.x |
| 상태관리 | Zustand | 4.x |
| ML Runtime | TensorFlow Lite | - |
8.2 네이티브 플러그인
| 플랫폼 | 언어 | 주요 라이브러리 |
|---|---|---|
| iOS | Swift 5.9 | AVFoundation, Accelerate, TensorFlowLiteSwift |
| Android | Kotlin 1.9 | AudioRecord, TensorFlow Lite, WorkManager |
8.3 ML 파이프라인
| 단계 | 기술 |
|---|---|
| 데이터 처리 | Python, Pandas, NumPy |
| 오디오 처리 | Librosa |
| 모델 학습 | TensorFlow 2.x |
| 실험 추적 | Weights & Biases |
| 모델 변환 | TFLite Converter (INT8 양자화) |
9. 보안 및 프라이버시
9.1 프라이버시 보호 원칙
-
오디오 원본 최소화
- 오디오 원본은 기기에서만 처리
- 서버에는 스펙트로그램(특징)만 전송
- 스펙트로그램에서 원본 오디오 복원 불가
-
익명화
- 사용자 ID는 SHA-256 해시 처리
- 정확한 시간 정보 제거 (날짜만 유지)
- 기기 정보 일반화 (모델명 → 제조사만)
-
명시적 동의
- 학습 데이터 제공은 별도 동의 필요
- 동의 철회 시 수집 중단
- 동의 없이는 로컬 분석만 수행
-
데이터 보존
- 학습 데이터: 모델 학습 완료 후 2년 보존
- 개인 분석 결과: 사용자 요청 시 즉시 삭제
9.2 GDPR 준수
| 요구사항 | 구현 방법 |
|---|---|
| 동의 (Consent) | 명시적 opt-in, 언제든 철회 가능 |
| 접근권 (Access) | 앱 내에서 수집된 데이터 조회 |
| 삭제권 (Erasure) | 계정 삭제 시 모든 데이터 삭제 |
| 이동권 (Portability) | 데이터 내보내기 기능 |
| 최소화 (Minimization) | 필요한 데이터만 수집 |
9.3 보안 요구사항
const securityRequirements = {
transport: {
tlsVersion: '1.3',
certificatePinning: true,
},
storage: {
localEncryption: true, // iOS Keychain, Android Keystore
serverEncryption: 'AES-256-GCM',
},
dataProcessing: {
audioRetention: 'none', // 서버에 원본 저장 안 함
spectrogramRetention: '2years',
anonymization: 'required',
},
};
변경 이력
| 버전 | 날짜 | 변경 내용 |
|---|---|---|
| 1.0.0 | 2026-01-20 | 초안 작성 |