@extends('layouts.app')

@section('title', '회의록 작성')

@section('content')
<div id="root"></div>
@endsection

@push('scripts')
<script src="https://cdn.tailwindcss.com"></script>
@include('partials.react-cdn')
<script type="text/babel">
@verbatim
const { useState, useEffect, useCallback, useRef, useMemo } = React;

const API = {
    list: '/juil/meeting-minutes/list',
    store: '/juil/meeting-minutes',
    show: (id) => `/juil/meeting-minutes/${id}`,
    update: (id) => `/juil/meeting-minutes/${id}`,
    destroy: (id) => `/juil/meeting-minutes/${id}`,
    saveSegments: (id) => `/juil/meeting-minutes/${id}/segments`,
    uploadAudio: (id) => `/juil/meeting-minutes/${id}/upload-audio`,
    summarize: (id) => `/juil/meeting-minutes/${id}/summarize`,
    downloadAudio: (id) => `/juil/meeting-minutes/${id}/download-audio`,
    diarize: (id) => `/juil/meeting-minutes/${id}/diarize`,
    logSttUsage: '/juil/meeting-minutes/log-stt-usage',
};

const CSRF_TOKEN = document.querySelector('meta[name="csrf-token"]')?.content || '';

const SPEAKER_COLORS = [
    { name: '화자 1', bg: 'bg-blue-50', border: 'border-blue-200', text: 'text-blue-700', badge: 'bg-blue-100 text-blue-800', dot: 'bg-blue-500' },
    { name: '화자 2', bg: 'bg-green-50', border: 'border-green-200', text: 'text-green-700', badge: 'bg-green-100 text-green-800', dot: 'bg-green-500' },
    { name: '화자 3', bg: 'bg-purple-50', border: 'border-purple-200', text: 'text-purple-700', badge: 'bg-purple-100 text-purple-800', dot: 'bg-purple-500' },
    { name: '화자 4', bg: 'bg-orange-50', border: 'border-orange-200', text: 'text-orange-700', badge: 'bg-orange-100 text-orange-800', dot: 'bg-orange-500' },
];

const STATUS_LABELS = {
    DRAFT: { label: '초안', color: 'bg-gray-100 text-gray-800' },
    RECORDING: { label: '녹음중', color: 'bg-red-100 text-red-800' },
    PROCESSING: { label: '처리중', color: 'bg-yellow-100 text-yellow-800' },
    COMPLETED: { label: '완료', color: 'bg-green-100 text-green-800' },
    FAILED: { label: '실패', color: 'bg-red-100 text-red-800' },
};

const LANGUAGES = [
    { code: 'ko-KR', label: '한국어' },
    { code: 'en-US', label: 'English' },
    { code: 'ja-JP', label: '日本語' },
    { code: 'zh-CN', label: '中文' },
];

async function apiFetch(url, options = {}) {
    const { headers: optHeaders, ...restOptions } = options;
    const res = await fetch(url, {
        ...restOptions,
        headers: {
            'X-CSRF-TOKEN': CSRF_TOKEN,
            'Accept': 'application/json',
            ...optHeaders,
        },
    });
    if (!res.ok) {
        const err = await res.json().catch(() => ({ message: '요청 처리 중 오류가 발생했습니다.' }));
        throw new Error(err.message || `HTTP ${res.status}`);
    }
    return res.json();
}

function formatDuration(seconds) {
    const h = Math.floor(seconds / 3600);
    const m = Math.floor((seconds % 3600) / 60);
    const s = seconds % 60;
    if (h > 0) return `${h}:${String(m).padStart(2,'0')}:${String(s).padStart(2,'0')}`;
    return `${String(m).padStart(2,'0')}:${String(s).padStart(2,'0')}`;
}

function formatMs(ms) {
    const totalSec = Math.floor(ms / 1000);
    const m = Math.floor(totalSec / 60);
    const s = totalSec % 60;
    return `${String(m).padStart(2,'0')}:${String(s).padStart(2,'0')}`;
}

// ========== Toast ==========
function ToastNotification({ toast, onClose }) {
    useEffect(() => {
        if (toast) {
            const t = setTimeout(onClose, 3000);
            return () => clearTimeout(t);
        }
    }, [toast]);
    if (!toast) return null;
    const colors = toast.type === 'error' ? 'bg-red-500' : toast.type === 'warning' ? 'bg-yellow-500' : 'bg-green-500';
    return (
        <div className={`fixed top-4 right-4 z-50 ${colors} text-white px-4 py-3 rounded-lg shadow-lg flex items-center gap-2 max-w-sm`}>
            <span className="flex-1 text-sm">{toast.message}</span>
            <button onClick={onClose} className="text-white/80 hover:text-white">&times;</button>
        </div>
    );
}

// ========== AlertModal ==========
function AlertModal({ show, title, message, icon, onClose }) {
    if (!show) return null;
    return (
        <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40" onClick={onClose}>
            <div className="bg-white rounded-xl shadow-2xl max-w-sm w-full mx-4 p-6" onClick={(e) => e.stopPropagation()}>
                <div className="flex flex-col items-center text-center">
                    {icon === 'warning' && (
                        <div className="w-14 h-14 rounded-full bg-yellow-100 flex items-center justify-center mb-4">
                            <svg className="w-7 h-7 text-yellow-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.072 16.5c-.77.833.192 2.5 1.732 2.5z" /></svg>
                        </div>
                    )}
                    {icon === 'info' && (
                        <div className="w-14 h-14 rounded-full bg-blue-100 flex items-center justify-center mb-4">
                            <svg className="w-7 h-7 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
                        </div>
                    )}
                    <h3 className="text-lg font-semibold text-gray-900 mb-2">{title}</h3>
                    <p className="text-sm text-gray-500 leading-relaxed">{message}</p>
                    <button onClick={onClose} className="mt-5 w-full bg-blue-600 text-white py-2.5 rounded-lg hover:bg-blue-700 transition text-sm font-medium">확인</button>
                </div>
            </div>
        </div>
    );
}

// ========== MeetingCard ==========
function MeetingCard({ meeting, onClick }) {
    const st = STATUS_LABELS[meeting.status] || STATUS_LABELS.DRAFT;
    const date = meeting.meeting_date ? new Date(meeting.meeting_date).toLocaleDateString('ko-KR') : '';
    const participants = meeting.participants || [];

    return (
        <div onClick={() => onClick(meeting.id)} className="bg-white rounded-lg border border-gray-200 p-4 hover:shadow-md hover:border-blue-300 transition-all cursor-pointer">
            <div className="flex items-start justify-between mb-2">
                <h3 className="font-medium text-gray-900 truncate flex-1">{meeting.title}</h3>
                <span className={`ml-2 px-2 py-0.5 rounded-full text-xs font-medium ${st.color}`}>{st.label}</span>
            </div>
            <div className="text-sm text-gray-500 space-y-1">
                <div className="flex items-center gap-2">
                    <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" /></svg>
                    <span>{date}</span>
                    {meeting.duration_seconds > 0 && <span className="text-gray-400">| {formatDuration(meeting.duration_seconds)}</span>}
                </div>
                {participants.length > 0 && (
                    <div className="flex flex-wrap gap-1 mt-1">
                        {participants.slice(0, 3).map((p, i) => (
                            <span key={i} className="inline-flex items-center px-2 py-0.5 rounded-full text-xs bg-gray-100 text-gray-600">{p}</span>
                        ))}
                        {participants.length > 3 && <span className="text-xs text-gray-400">+{participants.length - 3}</span>}
                    </div>
                )}
                {meeting.folder && <div className="text-xs text-gray-400 mt-1">📁 {meeting.folder}</div>}
            </div>
        </div>
    );
}

// ========== MeetingList ==========
function MeetingList({ onSelect, onNew, showToast }) {
    const [meetings, setMeetings] = useState([]);
    const [loading, setLoading] = useState(true);
    const [search, setSearch] = useState('');
    const [pagination, setPagination] = useState(null);

    const loadMeetings = useCallback(async (page = 1) => {
        setLoading(true);
        try {
            const params = new URLSearchParams({ per_page: 12, page });
            if (search) params.set('search', search);
            const res = await apiFetch(`${API.list}?${params}`);
            setMeetings(res.data.data || []);
            setPagination({ current_page: res.data.current_page, last_page: res.data.last_page, total: res.data.total });
        } catch (e) {
            showToast(e.message, 'error');
        } finally {
            setLoading(false);
        }
    }, [search]);

    useEffect(() => { loadMeetings(); }, [loadMeetings]);

    const handleCreate = async () => {
        try {
            const res = await apiFetch(API.store, {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ title: '무제 회의록', meeting_date: new Date().toISOString().split('T')[0] }),
            });
            showToast('새 회의록이 생성되었습니다.');
            onSelect(res.data.id);
        } catch (e) {
            showToast(e.message, 'error');
        }
    };

    return (
        <div className="p-4">
            <div className="flex items-center justify-between mb-6">
                <div>
                    <h1 className="text-2xl font-bold text-gray-900">회의록</h1>
                    <p className="text-sm text-gray-500 mt-1">AI 음성 인식 + 자동 요약 회의록</p>
                </div>
                <button onClick={handleCreate} className="bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 transition flex items-center gap-2 text-sm font-medium">
                    <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" /></svg>
                    새 회의록
                </button>
            </div>

            <div className="mb-4">
                <input type="text" value={search} onChange={(e) => setSearch(e.target.value)} placeholder="회의록 검색..." className="w-full max-w-md px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-sm" />
            </div>

            {loading ? (
                <div className="flex items-center justify-center py-20">
                    <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
                </div>
            ) : meetings.length === 0 ? (
                <div className="text-center py-20 text-gray-500">
                    <svg className="w-16 h-16 mx-auto mb-4 text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1} d="M19 11a7 7 0 01-7 7m0 0a7 7 0 01-7-7m7 7v4m0 0H8m4 0h4m-4-8a3 3 0 01-3-3V5a3 3 0 116 0v6a3 3 0 01-3 3z" /></svg>
                    <p className="text-lg font-medium">회의록이 없습니다</p>
                    <p className="text-sm mt-1">새 회의록을 만들어 음성 녹음을 시작하세요.</p>
                    <button onClick={handleCreate} className="mt-4 bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 text-sm">새 회의록 만들기</button>
                </div>
            ) : (
                <>
                    <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
                        {meetings.map(m => <MeetingCard key={m.id} meeting={m} onClick={onSelect} />)}
                    </div>
                    {pagination && pagination.last_page > 1 && (
                        <div className="flex justify-center gap-2 mt-6">
                            {Array.from({ length: pagination.last_page }, (_, i) => i + 1).map(p => (
                                <button key={p} onClick={() => loadMeetings(p)} className={`px-3 py-1 rounded text-sm ${p === pagination.current_page ? 'bg-blue-600 text-white' : 'bg-white border text-gray-700 hover:bg-gray-50'}`}>{p}</button>
                            ))}
                        </div>
                    )}
                </>
            )}
        </div>
    );
}

// ========== MeetingDetail ==========
function MeetingDetail({ meetingId, onBack, showToast }) {
    const [meeting, setMeeting] = useState(null);
    const [loading, setLoading] = useState(true);
    const [activeTab, setActiveTab] = useState('conversation');
    const [showSummary, setShowSummary] = useState(true);

    // 녹음 상태
    const [isRecording, setIsRecording] = useState(false);
    const [recordingTime, setRecordingTime] = useState(0);
    const [localSegments, setLocalSegments] = useState([]);
    const [interimText, setInterimText] = useState('');
    const [detectedSpeaker, setDetectedSpeaker] = useState('화자 1');
    const [speakers, setSpeakers] = useState([{ name: '화자 1' }]);
    const [sttLanguage, setSttLanguage] = useState('ko-KR');

    // 편집 상태
    const [editingTitle, setEditingTitle] = useState(false);
    const [titleValue, setTitleValue] = useState('');
    const [saving, setSaving] = useState(false);
    const [summarizing, setSummarizing] = useState(false);
    const [diarizing, setDiarizing] = useState(false);
    const [alertModal, setAlertModal] = useState(null);

    // 마이크 감도
    const [micGain, setMicGain] = useState(1.5);

    // 세그먼트 편집 상태
    const [editingSegments, setEditingSegments] = useState(false);
    const [editBackup, setEditBackup] = useState([]);

    // refs
    const mediaRecorderRef = useRef(null);
    const audioChunksRef = useRef([]);
    const rawRecorderRef = useRef(null);       // 원본 오디오 녹음 (화자분리용)
    const rawAudioChunksRef = useRef([]);      // 원본 오디오 청크
    const recognitionRef = useRef(null);
    const streamRef = useRef(null);
    const timerRef = useRef(null);
    const transcriptRef = useRef(null);
    const startTimeRef = useRef(null);
    const audioContextRef = useRef(null);
    const analyserRef = useRef(null);
    const gainNodeRef = useRef(null);
    const rawAnalyserRef = useRef(null);
    const speakerDetectorRef = useRef(null);
    const speakerProfilesRef = useRef([]);
    const recentFeaturesRef = useRef([]);
    const detectedSpeakerRef = useRef('화자 1');

    const loadMeeting = useCallback(async () => {
        try {
            const res = await apiFetch(API.show(meetingId));
            const data = res.data;
            setMeeting(data);
            setTitleValue(data.title);
            setSttLanguage(data.stt_language || 'ko-KR');
            if (data.segments && data.segments.length > 0) {
                setLocalSegments(data.segments.map(s => ({
                    speaker_name: s.speaker_name,
                    text: s.text,
                    start_time_ms: s.start_time_ms,
                    end_time_ms: s.end_time_ms,
                    is_final: true,
                })));
                // 화자 목록 복원
                const uniqueSpeakers = [...new Set(data.segments.map(s => s.speaker_name))];
                if (uniqueSpeakers.length > 0) {
                    setSpeakers(uniqueSpeakers.map(n => ({ name: n })));
                }
            }
        } catch (e) {
            showToast(e.message, 'error');
        } finally {
            setLoading(false);
        }
    }, [meetingId]);

    useEffect(() => { loadMeeting(); }, [loadMeeting]);

    // 타이머
    useEffect(() => {
        if (isRecording) {
            timerRef.current = setInterval(() => setRecordingTime(prev => prev + 1), 1000);
        } else if (timerRef.current) {
            clearInterval(timerRef.current);
        }
        return () => { if (timerRef.current) clearInterval(timerRef.current); };
    }, [isRecording]);

    // 자동 스크롤
    useEffect(() => {
        if (transcriptRef.current) {
            transcriptRef.current.scrollTop = transcriptRef.current.scrollHeight;
        }
    }, [localSegments, interimText]);

    const getSpeakerColor = (name) => {
        const idx = speakers.findIndex(s => s.name === name);
        return SPEAKER_COLORS[idx >= 0 ? idx % SPEAKER_COLORS.length : 0];
    };

    // ===== 화자 분류 (Spectral Centroid 기반) =====
    const classifySpeaker = useCallback((centroid, spread) => {
        const profiles = speakerProfilesRef.current;

        if (profiles.length === 0) {
            profiles.push({ centroid, spread, count: 1, name: '화자 1' });
            return '화자 1';
        }

        const distances = profiles.map(p => Math.abs(p.centroid - centroid) / p.centroid);
        const minDist = Math.min(...distances);
        const minIdx = distances.indexOf(minDist);

        if (minDist < 0.15) {
            const p = profiles[minIdx];
            p.centroid = (p.centroid * p.count + centroid) / (p.count + 1);
            p.spread = (p.spread * p.count + spread) / (p.count + 1);
            p.count++;
            return p.name;
        }

        if (profiles.length < 4) {
            const newName = `화자 ${profiles.length + 1}`;
            profiles.push({ centroid, spread, count: 1, name: newName });
            setSpeakers(prev => {
                if (prev.find(s => s.name === newName)) return prev;
                return [...prev, { name: newName }];
            });
            return newName;
        }

        return profiles[minIdx].name;
    }, []);

    // ===== 화자 감지 (100ms 간격 호출) =====
    const detectSpeaker = useCallback(() => {
        const analyser = rawAnalyserRef.current;
        if (!analyser) return;

        const sampleRate = audioContextRef.current?.sampleRate || 48000;
        const fftSize = analyser.fftSize;
        const binCount = analyser.frequencyBinCount; // fftSize / 2
        const binHz = sampleRate / fftSize; // 각 bin의 Hz 간격

        // 주파수 데이터 (0~255)
        const freqData = new Uint8Array(binCount);
        analyser.getByteFrequencyData(freqData);

        // 시간 도메인 데이터 (VAD용)
        const timeData = new Uint8Array(fftSize);
        analyser.getByteTimeDomainData(timeData);

        // RMS 계산 (VAD)
        let rmsSum = 0;
        for (let i = 0; i < timeData.length; i++) {
            const v = (timeData[i] - 128) / 128;
            rmsSum += v * v;
        }
        const rms = Math.sqrt(rmsSum / timeData.length);

        // VAD: 음성 미감지 시 무시
        if (rms < 0.015) return;

        // 음성 범위 bin (85Hz ~ 1000Hz)
        const minBin = Math.floor(85 / binHz);
        const maxBin = Math.min(Math.ceil(1000 / binHz), binCount - 1);

        // Spectral Centroid 계산
        let weightedSum = 0;
        let amplitudeSum = 0;
        for (let i = minBin; i <= maxBin; i++) {
            const freq = i * binHz;
            const amp = freqData[i];
            weightedSum += freq * amp;
            amplitudeSum += amp;
        }

        if (amplitudeSum < 10) return; // 에너지 부족

        const centroid = weightedSum / amplitudeSum;

        // Spectral Spread 계산
        let spreadSum = 0;
        for (let i = minBin; i <= maxBin; i++) {
            const freq = i * binHz;
            const amp = freqData[i];
            spreadSum += Math.pow(freq - centroid, 2) * amp;
        }
        const spread = Math.sqrt(spreadSum / amplitudeSum);

        // 최근 피처 버퍼에 추가 (500ms 윈도우)
        const now = Date.now();
        const features = recentFeaturesRef.current;
        features.push({ centroid, spread, timestamp: now });

        // 500ms 이전 피처 제거
        const cutoff = now - 500;
        recentFeaturesRef.current = features.filter(f => f.timestamp > cutoff);

        // 다수결: 최근 피처들의 화자 분류
        const votes = recentFeaturesRef.current.map(f => classifySpeaker(f.centroid, f.spread));
        if (votes.length === 0) return;

        // 최빈값 계산
        const counts = {};
        votes.forEach(v => { counts[v] = (counts[v] || 0) + 1; });
        const winner = Object.entries(counts).sort((a, b) => b[1] - a[1])[0][0];

        setDetectedSpeaker(winner);
    }, [classifySpeaker]);

    // ===== 녹음 시작 =====
    const startRecording = async () => {
        try {
            const stream = await navigator.mediaDevices.getUserMedia({
                audio: {
                    echoCancellation: true,
                    noiseSuppression: true,
                    autoGainControl: true,
                    channelCount: 1,
                    sampleRate: 48000,
                }
            });
            streamRef.current = stream;

            // Web Audio API 전처리 체인
            const audioCtx = new (window.AudioContext || window.webkitAudioContext)({ sampleRate: 48000 });
            audioContextRef.current = audioCtx;

            const source = audioCtx.createMediaStreamSource(stream);

            // GainNode: 감도 조절 (기본 1.5x)
            const gainNode = audioCtx.createGain();
            gainNode.gain.value = micGain;
            gainNodeRef.current = gainNode;

            // DynamicsCompressor: 작은 소리 증폭 + 큰 소리 억제
            const compressor = audioCtx.createDynamicsCompressor();
            compressor.threshold.value = -50;
            compressor.knee.value = 40;
            compressor.ratio.value = 12;
            compressor.attack.value = 0;
            compressor.release.value = 0.25;

            // AnalyserNode: VU 미터용
            const analyser = audioCtx.createAnalyser();
            analyser.fftSize = 2048;
            analyserRef.current = analyser;

            // MediaStreamDestination: 처리된 스트림
            const destination = audioCtx.createMediaStreamDestination();

            // rawAnalyser: 화자 감지용 (compressor 이전, 원본 신호 분석)
            const rawAnalyser = audioCtx.createAnalyser();
            rawAnalyser.fftSize = 2048;
            rawAnalyser.smoothingTimeConstant = 0.3;
            rawAnalyserRef.current = rawAnalyser;

            // 체인 연결: source → rawAnalyser(화자감지) → gain → compressor → analyser → destination
            source.connect(rawAnalyser);
            rawAnalyser.connect(gainNode);
            gainNode.connect(compressor);
            compressor.connect(analyser);
            analyser.connect(destination);

            const processedStream = destination.stream;

            const mimeType = MediaRecorder.isTypeSupported('audio/webm;codecs=opus')
                ? 'audio/webm;codecs=opus' : 'audio/webm';

            // MediaRecorder 1: 처리된 스트림 → 실시간 STT 품질 향상용
            const recorder = new MediaRecorder(processedStream, { mimeType, audioBitsPerSecond: 128000 });
            audioChunksRef.current = [];
            recorder.ondataavailable = (e) => { if (e.data.size > 0) audioChunksRef.current.push(e.data); };
            recorder.start();
            mediaRecorderRef.current = recorder;

            // MediaRecorder 2: 원본 스트림 → GCS 업로드 (화자분리용)
            // DynamicsCompressor 없는 원본 음성이 화자 음성 특성을 보존하여 분리 정확도 향상
            const rawRecorder = new MediaRecorder(stream, { mimeType, audioBitsPerSecond: 128000 });
            rawAudioChunksRef.current = [];
            rawRecorder.ondataavailable = (e) => { if (e.data.size > 0) rawAudioChunksRef.current.push(e.data); };
            rawRecorder.start();
            rawRecorderRef.current = rawRecorder;

            // Web Speech API
            const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
            if (SpeechRecognition) {
                const recognition = new SpeechRecognition();
                recognition.lang = sttLanguage;
                recognition.continuous = true;
                recognition.interimResults = true;
                recognition.maxAlternatives = 1;

                recognition.onresult = (event) => {
                    let currentInterim = '';
                    for (let i = event.resultIndex; i < event.results.length; i++) {
                        const text = event.results[i][0].transcript;
                        if (event.results[i].isFinal) {
                            const now = Date.now();
                            const startMs = startTimeRef.current ? now - startTimeRef.current : 0;
                            setLocalSegments(prev => [...prev, {
                                speaker_name: detectedSpeakerRef.current,
                                text: text.trim(),
                                start_time_ms: startMs,
                                end_time_ms: null,
                                is_final: true,
                            }]);
                            currentInterim = '';
                        } else {
                            currentInterim = text;
                        }
                    }
                    setInterimText(currentInterim);
                };

                recognition.onerror = (event) => {
                    if (event.error === 'no-speech' || event.error === 'aborted') return;
                    console.warn('STT error:', event.error);
                };

                recognition.onend = () => {
                    if (isRecordingRef.current && recognitionRef.current) {
                        try { recognitionRef.current.start(); } catch (e) {}
                    }
                };

                recognition.start();
                recognitionRef.current = recognition;
            }

            startTimeRef.current = Date.now();
            setRecordingTime(0);
            setIsRecording(true);

            // 화자 감지 시작
            speakerProfilesRef.current = [];
            recentFeaturesRef.current = [];
            setDetectedSpeaker('화자 1');
            setSpeakers([{ name: '화자 1' }]);
            speakerDetectorRef.current = setInterval(detectSpeaker, 100);
        } catch (e) {
            showToast('마이크 접근 권한이 필요합니다.', 'error');
        }
    };

    // isRecording ref (onend에서 접근)
    const isRecordingRef = useRef(false);
    useEffect(() => { isRecordingRef.current = isRecording; }, [isRecording]);
    useEffect(() => { detectedSpeakerRef.current = detectedSpeaker; }, [detectedSpeaker]);

    // ===== 녹음 중지 =====
    const stopRecording = async () => {
        setIsRecording(false);

        if (recognitionRef.current) {
            recognitionRef.current.onend = null;
            recognitionRef.current.stop();
            recognitionRef.current = null;
        }

        if (streamRef.current) {
            streamRef.current.getTracks().forEach(t => t.stop());
            streamRef.current = null;
        }

        // 화자 감지 중지
        if (speakerDetectorRef.current) {
            clearInterval(speakerDetectorRef.current);
            speakerDetectorRef.current = null;
        }

        // AudioContext 정리
        if (audioContextRef.current) {
            audioContextRef.current.close().catch(() => {});
            audioContextRef.current = null;
            analyserRef.current = null;
            gainNodeRef.current = null;
            rawAnalyserRef.current = null;
        }

        // MediaRecorder 중지 → blob 생성
        const recorder = mediaRecorderRef.current;
        if (recorder && recorder.state !== 'inactive') {
            await new Promise(resolve => {
                recorder.onstop = resolve;
                recorder.stop();
            });
        }

        // 원본 MediaRecorder 중지 (화자분리용)
        const rawRecorder = rawRecorderRef.current;
        if (rawRecorder && rawRecorder.state !== 'inactive') {
            await new Promise(resolve => {
                rawRecorder.onstop = resolve;
                rawRecorder.stop();
            });
        }

        // 화자분리용: 원본(무가공) 오디오 우선, 없으면 처리된 오디오 폴백
        const rawBlob = rawAudioChunksRef.current.length > 0
            ? new Blob(rawAudioChunksRef.current, { type: 'audio/webm' })
            : null;
        const audioBlob = rawBlob || new Blob(audioChunksRef.current, { type: 'audio/webm' });
        const duration = recordingTime;

        setSaving(true);
        try {
            // 1. 세그먼트 임시 저장 (Web Speech API 결과)
            const segmentsToSave = localSegments
                .filter(s => s.is_final && s.text && s.text.trim())
                .map((s, i) => ({
                    speaker_name: s.speaker_name || '화자 1',
                    text: s.text.trim(),
                    start_time_ms: Math.round(s.start_time_ms || 0),
                    end_time_ms: s.end_time_ms ? Math.round(s.end_time_ms) : null,
                    is_manual_speaker: true,
                }));

            if (segmentsToSave.length > 0) {
                await apiFetch(API.saveSegments(meetingId), {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify({ segments: segmentsToSave }),
                });
            }

            // 2. 오디오 업로드
            let audioUploaded = false;
            if (audioBlob.size > 0 && duration > 0) {
                const formData = new FormData();
                formData.append('audio', audioBlob, 'recording.webm');
                formData.append('duration_seconds', duration);
                await apiFetch(API.uploadAudio(meetingId), {
                    method: 'POST',
                    body: formData,
                });
                audioUploaded = true;
            }

            // 3. STT 사용량 로깅
            if (duration > 0) {
                await apiFetch(API.logSttUsage, {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify({ duration_seconds: duration }),
                }).catch(() => {});
            }

            showToast('녹음이 저장되었습니다.');
            setSaving(false);

            // 4. 자동 화자 분리 (오디오 업로드 완료 시)
            if (audioUploaded) {
                setDiarizing(true);
                try {
                    const diarizeRes = await apiFetch(API.diarize(meetingId), {
                        method: 'POST',
                        headers: { 'Content-Type': 'application/json' },
                        body: JSON.stringify({ min_speakers: 2, max_speakers: Math.max(speakers.length, 2) }),
                    });
                    if (diarizeRes.data && diarizeRes.data.segments && diarizeRes.data.segments.length > 0) {
                        setLocalSegments(diarizeRes.data.segments.map(s => ({
                            speaker_name: s.speaker_name,
                            text: s.text,
                            start_time_ms: s.start_time_ms,
                            end_time_ms: s.end_time_ms,
                            is_final: true,
                        })));
                        const uniqueSpeakers = [...new Set(diarizeRes.data.segments.map(s => s.speaker_name))];
                        if (uniqueSpeakers.length > 0) {
                            setSpeakers(uniqueSpeakers.map(n => ({ name: n })));
                        }
                    }
                    showToast(`화자 분리 완료 (${diarizeRes.speaker_count || 1}명 감지)`);
                } catch (e) {
                    showToast('자동 화자 분리 실패: ' + e.message, 'warning');
                } finally {
                    setDiarizing(false);
                }
            }

            // 5. 자동 AI 요약
            setSummarizing(true);
            try {
                await apiFetch(API.summarize(meetingId), { method: 'POST' });
                showToast('AI 요약이 완료되었습니다.');
            } catch (e) {
                showToast('AI 요약 실패: ' + e.message, 'warning');
            } finally {
                setSummarizing(false);
            }

            // 새로고침
            await loadMeeting();
        } catch (e) {
            showToast('저장 실패: ' + e.message, 'error');
            setSaving(false);
        }
    };

    // ===== 제목 인라인 편집 =====
    const saveTitle = async () => {
        if (!titleValue.trim()) return;
        try {
            await apiFetch(API.update(meetingId), {
                method: 'PUT',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ title: titleValue.trim() }),
            });
            setMeeting(prev => ({ ...prev, title: titleValue.trim() }));
            setEditingTitle(false);
        } catch (e) {
            showToast(e.message, 'error');
        }
    };

    // ===== 삭제 =====
    const handleDelete = async () => {
        if (!confirm('이 회의록을 삭제하시겠습니까?')) return;
        try {
            await apiFetch(API.destroy(meetingId), { method: 'DELETE' });
            showToast('회의록이 삭제되었습니다.');
            onBack();
        } catch (e) {
            showToast(e.message, 'error');
        }
    };

    // ===== 노트 복사 =====
    const copyNotes = () => {
        const lines = localSegments.filter(s => s.is_final).map(s => `[${s.speaker_name}] ${s.text}`);
        const text = lines.join('\n');
        navigator.clipboard.writeText(text).then(() => showToast('클립보드에 복사되었습니다.'));
    };

    // ===== 세그먼트 편집 =====
    const cleanSegmentText = (text) => text.replace(/[_\u2581]/g, ' ').replace(/\s{2,}/g, ' ').trim();

    const startEditingSegments = () => {
        setEditBackup(localSegments.map(s => ({ ...s })));
        setLocalSegments(prev => prev.map(s => ({ ...s, text: cleanSegmentText(s.text) })));
        setEditingSegments(true);
    };

    const cancelEditingSegments = () => {
        setLocalSegments(editBackup);
        setEditingSegments(false);
        setEditBackup([]);
    };

    const saveEditedSegments = async () => {
        setSaving(true);
        try {
            const segmentsToSave = localSegments
                .filter(s => s.is_final && s.text && s.text.trim())
                .map(s => ({
                    speaker_name: s.speaker_name || '화자 1',
                    text: s.text.trim(),
                    start_time_ms: Math.round(s.start_time_ms || 0),
                    end_time_ms: s.end_time_ms ? Math.round(s.end_time_ms) : null,
                    is_manual_speaker: true,
                }));
            await apiFetch(API.saveSegments(meetingId), {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ segments: segmentsToSave }),
            });
            showToast('대화 기록이 저장되었습니다.');
            setEditingSegments(false);
            setEditBackup([]);
            await loadMeeting();
        } catch (e) {
            showToast('저장 실패: ' + e.message, 'error');
        } finally {
            setSaving(false);
        }
    };

    const handleEditText = (segIdx, newText) => {
        setLocalSegments(prev => prev.map((s, i) => i === segIdx ? { ...s, text: newText } : s));
    };

    const handleEditSpeaker = (segIdx, newSpeaker) => {
        setLocalSegments(prev => prev.map((s, i) => i === segIdx ? { ...s, speaker_name: newSpeaker } : s));
    };

    const handleDeleteSegment = (segIdx) => {
        setLocalSegments(prev => prev.filter((_, i) => i !== segIdx));
    };

    // ===== 수동 요약 =====
    const handleSummarize = async () => {
        const finalSegments = localSegments.filter(s => s.is_final && s.text && s.text.trim());
        const hasContent = finalSegments.length > 0 || (meeting && meeting.full_transcript);
        if (!hasContent) {
            setAlertModal({
                title: '대화 내용이 없습니다',
                message: '요약을 실행하려면 먼저 녹음을 진행하여 대화 내용을 기록해주세요.',
                icon: 'warning',
            });
            return;
        }
        setSummarizing(true);
        try {
            // 로컬 세그먼트가 있으면 먼저 서버에 저장 (full_transcript 업데이트)
            if (finalSegments.length > 0) {
                const segmentsToSave = finalSegments.map((s, i) => ({
                    speaker_name: s.speaker_name || '화자 1',
                    text: s.text.trim(),
                    start_time_ms: Math.round(s.start_time_ms || 0),
                    end_time_ms: s.end_time_ms ? Math.round(s.end_time_ms) : null,
                    is_manual_speaker: true,
                }));
                await apiFetch(API.saveSegments(meetingId), {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify({ segments: segmentsToSave }),
                });
            }
            await apiFetch(API.summarize(meetingId), { method: 'POST' });
            showToast('AI 요약이 완료되었습니다.');
            await loadMeeting();
        } catch (e) {
            showToast(e.message, 'error');
        } finally {
            setSummarizing(false);
        }
    };

    // ===== 자동 화자 분리 =====
    const handleDiarize = async () => {
        if (!meeting || !meeting.audio_gcs_uri) {
            setAlertModal({
                title: '오디오 파일이 없습니다',
                message: '자동 화자 분리를 실행하려면 먼저 녹음을 진행하여 오디오를 저장해주세요.',
                icon: 'warning',
            });
            return;
        }

        if (!confirm('자동 화자 분리를 실행하면 기존 대화 기록이 새로운 결과로 교체됩니다. 계속하시겠습니까?')) {
            return;
        }

        setDiarizing(true);
        try {
            const res = await apiFetch(API.diarize(meetingId), {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ min_speakers: 2, max_speakers: speakers.length || 4 }),
            });
            showToast(res.message || '자동 화자 분리가 완료되었습니다.');
            await loadMeeting();
        } catch (e) {
            showToast('화자 분리 실패: ' + e.message, 'error');
        } finally {
            setDiarizing(false);
        }
    };

    if (loading) {
        return <div className="flex items-center justify-center h-96"><div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div></div>;
    }

    if (!meeting) {
        return <div className="text-center py-20 text-gray-500">회의록을 찾을 수 없습니다.</div>;
    }

    const segments = localSegments
        .map((s, i) => ({ ...s, __origIdx: i }))
        .filter(s => s.is_final);

    return (
        <div className="flex flex-col" style={{height: 'calc(100vh - 112px)', margin: '-24px'}}>
            {/* Alert Modal */}
            <AlertModal show={!!alertModal} title={alertModal?.title} message={alertModal?.message} icon={alertModal?.icon} onClose={() => setAlertModal(null)} />

            {/* Header */}
            <div className="bg-white border-b px-4 py-3 flex-shrink-0">
                <div className="flex items-center gap-3">
                    <button onClick={onBack} className="text-gray-500 hover:text-gray-700 p-1">
                        <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" /></svg>
                    </button>
                    {editingTitle ? (
                        <input value={titleValue} onChange={(e) => setTitleValue(e.target.value)} onBlur={saveTitle} onKeyDown={(e) => e.key === 'Enter' && saveTitle()} autoFocus className="text-lg font-bold border-b-2 border-blue-500 outline-none bg-transparent flex-1" />
                    ) : (
                        <h1 onClick={() => setEditingTitle(true)} className="text-lg font-bold text-gray-900 cursor-pointer hover:text-blue-600 flex-1 truncate">{meeting.title}</h1>
                    )}
                    <div className="flex items-center gap-2 flex-shrink-0">
                        <button onClick={copyNotes} className="text-gray-500 hover:text-gray-700 text-sm flex items-center gap-1 px-2 py-1 rounded hover:bg-gray-100" title="노트 복사">
                            <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 5H6a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2v-1M8 5a2 2 0 002 2h2a2 2 0 002-2M8 5a2 2 0 012-2h2a2 2 0 012 2m0 0h2a2 2 0 012 2v3m2 4H10m0 0l3-3m-3 3l3 3" /></svg>
                            복사
                        </button>
                        {meeting.audio_file_path && (
                            <a href={API.downloadAudio(meetingId)} className="text-gray-500 hover:text-gray-700 text-sm flex items-center gap-1 px-2 py-1 rounded hover:bg-gray-100" title="음성 다운로드">
                                <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" /></svg>
                            </a>
                        )}
                        <button onClick={handleDelete} className="text-red-400 hover:text-red-600 p-1" title="삭제">
                            <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" /></svg>
                        </button>
                    </div>
                </div>
                <div className="flex items-center gap-3 mt-2 text-sm text-gray-500 flex-wrap">
                    <span className="flex items-center gap-1">
                        <svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" /></svg>
                        {meeting.meeting_date ? new Date(meeting.meeting_date).toLocaleDateString('ko-KR') : ''}
                    </span>
                    {meeting.duration_seconds > 0 && (
                        <span className="flex items-center gap-1">
                            <svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
                            {formatDuration(meeting.duration_seconds)}
                        </span>
                    )}
                    {meeting.participants && meeting.participants.length > 0 && (
                        <div className="flex items-center gap-1">
                            <svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z" /></svg>
                            {meeting.participants.join(', ')}
                        </div>
                    )}
                    {meeting.status && <span className={`px-2 py-0.5 rounded-full text-xs font-medium ${(STATUS_LABELS[meeting.status] || STATUS_LABELS.DRAFT).color}`}>{(STATUS_LABELS[meeting.status] || STATUS_LABELS.DRAFT).label}</span>}
                </div>
            </div>

            {/* Tab Bar */}
            <div className="bg-white border-b px-4 flex items-center gap-4 flex-shrink-0">
                <button onClick={() => setActiveTab('conversation')} className={`py-2.5 text-sm font-medium border-b-2 transition ${activeTab === 'conversation' ? 'border-blue-600 text-blue-600' : 'border-transparent text-gray-500 hover:text-gray-700'}`}>대화 기록</button>
                <button onClick={() => { if (editingSegments) cancelEditingSegments(); setActiveTab('script'); }} className={`py-2.5 text-sm font-medium border-b-2 transition ${activeTab === 'script' ? 'border-blue-600 text-blue-600' : 'border-transparent text-gray-500 hover:text-gray-700'}`}>스크립트</button>
                <div className="flex-1"></div>
                {activeTab === 'conversation' && !isRecording && segments.length > 0 && !editingSegments && (
                    <button onClick={startEditingSegments} className="py-2 px-3 text-sm font-medium text-gray-500 hover:text-blue-600 hover:bg-blue-50 rounded transition flex items-center gap-1">
                        <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" /></svg>
                        편집
                    </button>
                )}
                {editingSegments && (
                    <>
                        <button onClick={cancelEditingSegments} className="py-2 px-3 text-sm font-medium text-gray-500 hover:text-gray-700 hover:bg-gray-100 rounded transition">
                            취소
                        </button>
                        <button onClick={saveEditedSegments} disabled={saving} className="py-2 px-3 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded transition disabled:opacity-50 flex items-center gap-1">
                            {saving ? <div className="animate-spin rounded-full h-3 w-3 border-b-2 border-white"></div> : <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" /></svg>}
                            저장
                        </button>
                    </>
                )}
                <button onClick={() => setShowSummary(!showSummary)} className={`py-2.5 text-sm font-medium flex items-center gap-1 ${showSummary ? 'text-blue-600' : 'text-gray-500'}`}>
                    <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" /></svg>
                    AI 요약
                </button>
            </div>

            {/* Main Content */}
            <div className="flex flex-1 overflow-hidden">
                {/* Left: Transcript */}
                <div className="flex-1 overflow-y-auto p-4" ref={transcriptRef}>
                    {activeTab === 'conversation' ? (
                        <ConversationView segments={segments} interimText={interimText} isRecording={isRecording} detectedSpeaker={detectedSpeaker} getSpeakerColor={getSpeakerColor} editing={editingSegments} onEditText={handleEditText} onEditSpeaker={handleEditSpeaker} onDeleteSegment={handleDeleteSegment} speakers={speakers} />
                    ) : (
                        <ScriptView segments={segments} interimText={interimText} isRecording={isRecording} />
                    )}
                </div>

                {/* Right: Summary Panel */}
                {showSummary && (
                    <div className="w-80 border-l bg-gray-50 overflow-y-auto flex-shrink-0">
                        <SummaryPanel meeting={meeting} onSummarize={handleSummarize} summarizing={summarizing} />
                    </div>
                )}
            </div>

            {/* Bottom: Recording Control Bar */}
            <RecordingControlBar
                isRecording={isRecording}
                recordingTime={recordingTime}
                detectedSpeaker={detectedSpeaker}
                speakerCount={speakers.length}
                sttLanguage={sttLanguage}
                onStart={startRecording}
                onStop={stopRecording}
                onLanguageChange={setSttLanguage}
                onSummarize={handleSummarize}
                onDiarize={handleDiarize}
                saving={saving}
                summarizing={summarizing}
                diarizing={diarizing}
                hasSegments={segments.length > 0}
                hasAudio={!!meeting?.audio_gcs_uri}
                analyserRef={analyserRef}
                micGain={micGain}
                onMicGainChange={setMicGain}
                gainNodeRef={gainNodeRef}
                getSpeakerColor={getSpeakerColor}
            />
        </div>
    );
}

// ========== AutoResizeTextarea ==========
function AutoResizeTextarea({ value, onChange, className }) {
    const ref = useRef(null);
    useEffect(() => {
        if (ref.current) {
            ref.current.style.height = 'auto';
            ref.current.style.height = ref.current.scrollHeight + 'px';
        }
    }, [value]);
    return (
        <textarea
            ref={ref}
            value={value}
            onChange={onChange}
            rows={1}
            className={className}
            style={{ resize: 'none', overflow: 'hidden' }}
        />
    );
}

// ========== ConversationView ==========
function ConversationView({ segments, interimText, isRecording, detectedSpeaker, getSpeakerColor, editing, onEditText, onEditSpeaker, onDeleteSegment, speakers }) {
    if (segments.length === 0 && !interimText) {
        return (
            <div className="flex flex-col items-center justify-center h-full text-gray-400">
                <svg className="w-16 h-16 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1} d="M19 11a7 7 0 01-7 7m0 0a7 7 0 01-7-7m7 7v4m0 0H8m4 0h4m-4-8a3 3 0 01-3-3V5a3 3 0 116 0v6a3 3 0 01-3 3z" /></svg>
                <p className="text-lg">녹음 버튼을 눌러 시작하세요</p>
                <p className="text-sm mt-1">실시간 음성 인식이 대화를 기록합니다</p>
            </div>
        );
    }

    // 편집 모드: 개별 세그먼트 렌더링
    if (editing) {
        // segments는 filter된 것이므로 원본 인덱스를 찾기 위해 전체 localSegments의 is_final 기준 인덱스 사용
        // 부모에서 segments = localSegments.filter(s => s.is_final)로 전달하므로, segIdx는 filter된 배열이 아닌 원본 인덱스여야 함
        // 하지만 onEditText 등은 localSegments 전체 배열의 인덱스를 사용하므로, 실제 인덱스를 매핑해야 함
        // segments 배열은 is_final만 필터된 것이므로 원본 localSegments에서의 인덱스를 별도로 추적
        return (
            <div className="space-y-2">
                <div className="text-xs text-gray-400 mb-2 flex items-center gap-1">
                    <svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
                    텍스트, 화자를 수정하거나 세그먼트를 삭제할 수 있습니다.
                </div>
                {segments.map((seg, filteredIdx) => {
                    const color = getSpeakerColor(seg.speaker_name);
                    // 원본 localSegments에서의 실제 인덱스 계산
                    // segments는 부모에서 localSegments.filter(s => s.is_final)로 전달됨
                    // filteredIdx → 원본 인덱스 매핑 필요
                    // seg 객체 자체가 localSegments의 참조이므로, 원본에서 찾기
                    const origIdx = seg.__origIdx !== undefined ? seg.__origIdx : filteredIdx;
                    return (
                        <div key={filteredIdx} className={`rounded-lg border-2 p-3 ${color.bg} ${color.border}`}>
                            <div className="flex items-center gap-2 mb-2">
                                <select
                                    value={seg.speaker_name}
                                    onChange={(e) => onEditSpeaker(origIdx, e.target.value)}
                                    className={`text-xs font-medium px-2 py-1 rounded-full border ${color.badge} cursor-pointer`}
                                >
                                    {(speakers || []).map((sp, si) => (
                                        <option key={si} value={sp.name}>{sp.name}</option>
                                    ))}
                                </select>
                                {seg.start_time_ms > 0 && (
                                    <span className="text-xs text-gray-400">{formatMs(seg.start_time_ms)}</span>
                                )}
                                <div className="flex-1"></div>
                                <button
                                    onClick={() => onDeleteSegment(origIdx)}
                                    className="text-red-400 hover:text-red-600 hover:bg-red-50 p-1 rounded transition"
                                    title="세그먼트 삭제"
                                >
                                    <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" /></svg>
                                </button>
                            </div>
                            <AutoResizeTextarea
                                value={seg.text}
                                onChange={(e) => onEditText(origIdx, e.target.value)}
                                className="w-full text-sm text-gray-800 leading-relaxed bg-white/70 border border-gray-200 rounded-md px-3 py-2 focus:ring-2 focus:ring-blue-400 focus:border-blue-400 outline-none"
                            />
                        </div>
                    );
                })}
            </div>
        );
    }

    // 읽기 모드: 연속 같은 화자 세그먼트 그룹화
    const groups = [];
    segments.forEach((seg, i) => {
        if (i === 0 || seg.speaker_name !== segments[i - 1].speaker_name) {
            groups.push({ speaker_name: seg.speaker_name, texts: [seg] });
        } else {
            groups[groups.length - 1].texts.push(seg);
        }
    });

    return (
        <div className="space-y-3">
            {groups.map((group, gi) => {
                const color = getSpeakerColor(group.speaker_name);
                return (
                    <div key={gi} className={`rounded-lg border p-3 ${color.bg} ${color.border}`}>
                        <div className="flex items-center gap-2 mb-1.5">
                            <span className={`inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium ${color.badge}`}>
                                <span className={`w-2 h-2 rounded-full ${color.dot} mr-1`}></span>
                                {group.speaker_name}
                            </span>
                            {group.texts[0].start_time_ms > 0 && (
                                <span className="text-xs text-gray-400">{formatMs(group.texts[0].start_time_ms)}</span>
                            )}
                        </div>
                        <div className="text-sm text-gray-800 leading-relaxed">
                            {group.texts.map((t, ti) => <span key={ti}>{ti > 0 ? ' ' : ''}{t.text.replace(/[_\u2581]/g, ' ').replace(/\s{2,}/g, ' ').trim()}</span>)}
                        </div>
                    </div>
                );
            })}

            {/* Interim (미확정) */}
            {isRecording && interimText && (
                <div className="rounded-lg border-2 border-dashed border-gray-300 p-3 bg-white">
                    <div className="flex items-center gap-2 mb-1.5">
                        <span className={`inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium ${getSpeakerColor(detectedSpeaker).badge}`}>
                            <span className={`w-2 h-2 rounded-full bg-red-500 mr-1 animate-pulse`}></span>
                            {detectedSpeaker}
                        </span>
                        <span className="text-xs text-gray-400">인식 중...</span>
                    </div>
                    <div className="text-sm text-gray-400 italic leading-relaxed">{interimText}</div>
                </div>
            )}
        </div>
    );
}

// ========== ScriptView ==========
function ScriptView({ segments, interimText, isRecording }) {
    if (segments.length === 0 && !interimText) {
        return <div className="text-center py-20 text-gray-400">스크립트가 없습니다.</div>;
    }

    return (
        <div className="prose prose-sm max-w-none">
            <div className="text-sm text-gray-800 leading-relaxed whitespace-pre-wrap">
                {segments.map((s, i) => <span key={i}>{s.text.replace(/[_\u2581]/g, ' ').replace(/\s{2,}/g, ' ').trim()}{i < segments.length - 1 ? ' ' : ''}</span>)}
                {isRecording && interimText && <span className="text-gray-400 italic"> {interimText}</span>}
            </div>
        </div>
    );
}

// ========== SummaryPanel ==========
function SummaryPanel({ meeting, onSummarize, summarizing }) {
    const hasSummary = meeting.summary || (meeting.decisions && meeting.decisions.length > 0) || (meeting.action_items && meeting.action_items.length > 0);

    return (
        <div className="p-4">
            <div className="flex items-center justify-between mb-4">
                <h3 className="font-semibold text-gray-900 text-sm">AI 요약</h3>
                <button onClick={onSummarize} disabled={summarizing} className="text-xs text-blue-600 hover:text-blue-700 disabled:text-gray-400 flex items-center gap-1">
                    {summarizing ? (
                        <><div className="animate-spin rounded-full h-3 w-3 border-b-2 border-blue-600"></div> 요약 중...</>
                    ) : (
                        <><svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" /></svg> 요약 실행</>
                    )}
                </button>
            </div>

            {!hasSummary ? (
                <div className="text-center py-8 text-gray-400">
                    <svg className="w-10 h-10 mx-auto mb-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1} d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" /></svg>
                    <p className="text-sm">녹음 완료 후 AI가 자동으로 요약합니다</p>
                </div>
            ) : (
                <div className="space-y-4">
                    {meeting.summary && (
                        <div>
                            <h4 className="text-xs font-semibold text-gray-500 uppercase mb-1">회의 요약</h4>
                            <p className="text-sm text-gray-700 leading-relaxed">{meeting.summary}</p>
                        </div>
                    )}

                    {meeting.decisions && meeting.decisions.length > 0 && (
                        <div>
                            <h4 className="text-xs font-semibold text-gray-500 uppercase mb-1">결정사항</h4>
                            <ul className="space-y-1">
                                {meeting.decisions.map((d, i) => (
                                    <li key={i} className="flex items-start gap-2 text-sm text-gray-700">
                                        <span className="text-green-500 mt-0.5">&#10003;</span>
                                        <span>{d}</span>
                                    </li>
                                ))}
                            </ul>
                        </div>
                    )}

                    {meeting.action_items && meeting.action_items.length > 0 && (
                        <div>
                            <h4 className="text-xs font-semibold text-gray-500 uppercase mb-1">액션 아이템</h4>
                            <ul className="space-y-2">
                                {meeting.action_items.map((item, i) => (
                                    <li key={i} className="bg-white rounded border p-2">
                                        <div className="text-sm text-gray-800">{item.task || item}</div>
                                        <div className="flex items-center gap-2 mt-1 text-xs text-gray-500">
                                            {item.assignee && <span className="bg-blue-50 text-blue-700 px-1.5 py-0.5 rounded">{item.assignee}</span>}
                                            {item.deadline && <span className="bg-orange-50 text-orange-700 px-1.5 py-0.5 rounded">{item.deadline}</span>}
                                        </div>
                                    </li>
                                ))}
                            </ul>
                        </div>
                    )}
                </div>
            )}
        </div>
    );
}

// ========== AudioLevelMeter ==========
function AudioLevelMeter({ analyserRef, isRecording }) {
    const canvasRef = useRef(null);
    const animFrameRef = useRef(null);

    useEffect(() => {
        if (!isRecording || !analyserRef?.current) {
            if (animFrameRef.current) cancelAnimationFrame(animFrameRef.current);
            // 녹음 중지 시 캔버스 클리어
            const canvas = canvasRef.current;
            if (canvas) {
                const ctx = canvas.getContext('2d');
                ctx.clearRect(0, 0, canvas.width, canvas.height);
            }
            return;
        }

        const analyser = analyserRef.current;
        const dataArray = new Uint8Array(analyser.fftSize);

        const draw = () => {
            analyser.getByteTimeDomainData(dataArray);

            // RMS 계산
            let sum = 0;
            for (let i = 0; i < dataArray.length; i++) {
                const v = (dataArray[i] - 128) / 128;
                sum += v * v;
            }
            const rms = Math.sqrt(sum / dataArray.length);
            const level = Math.min(1, rms * 3); // 0~1 정규화

            const canvas = canvasRef.current;
            if (!canvas) return;
            const ctx = canvas.getContext('2d');
            const w = canvas.width;
            const h = canvas.height;
            ctx.clearRect(0, 0, w, h);

            // 바 그리기
            const barWidth = Math.round(w * level);
            const gradient = ctx.createLinearGradient(0, 0, w, 0);
            gradient.addColorStop(0, '#22c55e');
            gradient.addColorStop(0.6, '#eab308');
            gradient.addColorStop(1, '#ef4444');
            ctx.fillStyle = gradient;
            ctx.fillRect(0, 0, barWidth, h);

            // 배경 바
            ctx.fillStyle = 'rgba(0,0,0,0.06)';
            ctx.fillRect(barWidth, 0, w - barWidth, h);

            animFrameRef.current = requestAnimationFrame(draw);
        };

        draw();

        return () => {
            if (animFrameRef.current) cancelAnimationFrame(animFrameRef.current);
        };
    }, [isRecording, analyserRef?.current]);

    return <canvas ref={canvasRef} width={120} height={12} className="rounded-full" style={{ display: 'block' }} />;
}

// ========== RecordingControlBar ==========
function RecordingControlBar({ isRecording, recordingTime, detectedSpeaker, speakerCount, sttLanguage, onStart, onStop, onLanguageChange, onSummarize, onDiarize, saving, summarizing, diarizing, hasSegments, hasAudio, analyserRef, micGain, onMicGainChange, gainNodeRef, getSpeakerColor }) {
    const detectedColor = getSpeakerColor ? getSpeakerColor(detectedSpeaker) : SPEAKER_COLORS[0];

    return (
        <div className="bg-white border-t shadow-lg px-4 py-3 flex-shrink-0">
            <div className="flex items-center justify-between gap-3">
                {/* Language */}
                <div className="flex items-center gap-2">
                    <select value={sttLanguage} onChange={(e) => onLanguageChange(e.target.value)} disabled={isRecording} className="text-sm border rounded px-2 py-1.5 bg-white disabled:bg-gray-100 disabled:text-gray-400">
                        {LANGUAGES.map(l => <option key={l.code} value={l.code}>{l.label}</option>)}
                    </select>
                </div>

                {/* Auto Speaker Detection Indicator */}
                {isRecording ? (
                    <div className="flex items-center gap-2">
                        <div className="flex items-center gap-1.5 px-3 py-1.5 rounded-full bg-emerald-50 border border-emerald-200">
                            <svg className="w-3.5 h-3.5 text-emerald-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 11a7 7 0 01-7 7m0 0a7 7 0 01-7-7m7 7v4m0 0H8m4 0h4m-4-8a3 3 0 01-3-3V5a3 3 0 116 0v6a3 3 0 01-3 3z" /></svg>
                            <span className="text-xs font-medium text-emerald-700">화자 자동 감지</span>
                        </div>
                        <span className={`inline-flex items-center px-2.5 py-1 rounded-full text-xs font-medium ${detectedColor.badge} transition-all`}>
                            <span className={`w-2 h-2 rounded-full ${detectedColor.dot} mr-1 animate-pulse`}></span>
                            {detectedSpeaker}
                        </span>
                        <span className="text-xs text-gray-400">{speakerCount}명 감지</span>
                    </div>
                ) : (
                    <div className="flex items-center gap-1.5 px-3 py-1.5 rounded-full bg-gray-50 border border-gray-200">
                        <svg className="w-3.5 h-3.5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 11a7 7 0 01-7 7m0 0a7 7 0 01-7-7m7 7v4m0 0H8m4 0h4m-4-8a3 3 0 01-3-3V5a3 3 0 116 0v6a3 3 0 01-3 3z" /></svg>
                        <span className="text-xs text-gray-500">화자 자동 감지</span>
                    </div>
                )}

                {/* Mic Gain Slider */}
                {isRecording && (
                    <div className="flex items-center gap-1.5">
                        <svg className="w-4 h-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 11a7 7 0 01-7 7m0 0a7 7 0 01-7-7m7 7v4m0 0H8m4 0h4m-4-8a3 3 0 01-3-3V5a3 3 0 116 0v6a3 3 0 01-3 3z" /></svg>
                        <input
                            type="range"
                            min="0.5"
                            max="3.0"
                            step="0.1"
                            value={micGain}
                            onChange={(e) => {
                                const val = parseFloat(e.target.value);
                                onMicGainChange(val);
                                if (gainNodeRef?.current) gainNodeRef.current.gain.value = val;
                            }}
                            className="w-16 h-1 accent-blue-500"
                            title={`감도 ${micGain.toFixed(1)}x`}
                        />
                        <span className="text-xs text-gray-400 min-w-[28px]">{micGain.toFixed(1)}x</span>
                    </div>
                )}

                {/* Record Button + VU Meter */}
                <div className="flex items-center gap-3">
                    {isRecording ? (
                        <button onClick={onStop} disabled={saving} className="bg-red-600 text-white px-5 py-2 rounded-full hover:bg-red-700 transition flex items-center gap-2 text-sm font-medium shadow-lg disabled:opacity-50">
                            <span className="w-3 h-3 bg-white rounded-sm"></span>
                            중지
                        </button>
                    ) : (
                        <button onClick={onStart} disabled={saving || diarizing} className="bg-red-500 text-white px-5 py-2 rounded-full hover:bg-red-600 transition flex items-center gap-2 text-sm font-medium shadow-lg disabled:opacity-50">
                            <span className="w-3 h-3 bg-white rounded-full"></span>
                            녹음
                        </button>
                    )}
                    {isRecording && <AudioLevelMeter analyserRef={analyserRef} isRecording={isRecording} />}
                    <span className="text-sm font-mono text-gray-600 min-w-[60px]">{formatDuration(recordingTime)}</span>
                    {isRecording && <span className="flex items-center gap-1 text-xs text-red-500"><span className="w-2 h-2 bg-red-500 rounded-full animate-pulse"></span>REC</span>}
                </div>

                {/* Status indicators */}
                <div className="flex items-center gap-2">
                    {saving && <span className="text-xs text-blue-500 flex items-center gap-1"><div className="animate-spin rounded-full h-3 w-3 border-b-2 border-blue-500"></div>저장 중...</span>}
                    {summarizing && <span className="text-xs text-purple-500 flex items-center gap-1"><div className="animate-spin rounded-full h-3 w-3 border-b-2 border-purple-500"></div>요약 중...</span>}
                    {diarizing && <span className="text-xs text-indigo-500 flex items-center gap-1"><div className="animate-spin rounded-full h-3 w-3 border-b-2 border-indigo-500"></div>화자 분리 중...</span>}
                </div>

                {/* Action Buttons */}
                <div className="flex items-center gap-2">
                    <button onClick={onDiarize} disabled={diarizing || summarizing || isRecording || !hasAudio} className="bg-indigo-600 text-white px-3 py-1.5 rounded-lg hover:bg-indigo-700 transition text-xs font-medium disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-1" title="업로드된 오디오에서 AI로 자동 화자 구분">
                        <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0z" /></svg>
                        화자 분리
                    </button>
                    <button onClick={onSummarize} disabled={summarizing || diarizing || isRecording || !hasSegments} className="bg-purple-600 text-white px-3 py-1.5 rounded-lg hover:bg-purple-700 transition text-xs font-medium disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-1">
                        <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" /></svg>
                        AI 요약
                    </button>
                </div>
            </div>
        </div>
    );
}

// ========== App ==========
function App() {
    const [currentView, setCurrentView] = useState('list');
    const [selectedId, setSelectedId] = useState(null);
    const [toast, setToast] = useState(null);

    const showToast = useCallback((message, type = 'success') => {
        setToast({ message, type });
    }, []);

    const handleSelect = (id) => {
        setSelectedId(id);
        setCurrentView('detail');
    };

    const handleBack = () => {
        setSelectedId(null);
        setCurrentView('list');
    };

    return (
        <>
            <ToastNotification toast={toast} onClose={() => setToast(null)} />
            {currentView === 'list' ? (
                <MeetingList onSelect={handleSelect} showToast={showToast} />
            ) : (
                <MeetingDetail meetingId={selectedId} onBack={handleBack} showToast={showToast} />
            )}
        </>
    );
}

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);
@endverbatim
</script>
@endpush
