@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 } = React;

const API = {
    list: '/juil/construction-photos/list',
    store: '/juil/construction-photos',
    show: (id) => `/juil/construction-photos/${id}`,
    update: (id) => `/juil/construction-photos/${id}`,
    destroy: (id) => `/juil/construction-photos/${id}`,
    upload: (id, rowId) => `/juil/construction-photos/${id}/rows/${rowId}/upload`,
    deletePhoto: (id, rowId, type) => `/juil/construction-photos/${id}/rows/${rowId}/photo/${type}`,
    downloadPhoto: (id, rowId, type) => `/juil/construction-photos/${id}/rows/${rowId}/download/${type}`,
    photoUrl: (id, rowId, type) => `/juil/construction-photos/${id}/rows/${rowId}/download/${type}?inline=1`,
    addRow: (id) => `/juil/construction-photos/${id}/rows`,
    deleteRow: (id, rowId) => `/juil/construction-photos/${id}/rows/${rowId}`,
    logSttUsage: '/juil/construction-photos/log-stt-usage',
};

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

const TYPE_LABELS = { before: '작업전', during: '작업중', after: '작업후' };
const TYPE_COLORS = {
    before: { bg: 'bg-blue-50', border: 'border-blue-200', text: 'text-blue-700', badge: 'bg-blue-100 text-blue-800' },
    during: { bg: 'bg-yellow-50', border: 'border-yellow-200', text: 'text-yellow-700', badge: 'bg-yellow-100 text-yellow-800' },
    after: { bg: 'bg-green-50', border: 'border-green-200', text: 'text-green-700', badge: 'bg-green-100 text-green-800' },
};

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();
}

const compressImage = (file, maxWidth = 1920, quality = 0.8) => {
    return new Promise((resolve) => {
        const img = new Image();
        img.onload = () => {
            const canvas = document.createElement('canvas');
            let { width, height } = img;
            if (width > maxWidth) {
                height = Math.round((height * maxWidth) / width);
                width = maxWidth;
            }
            canvas.width = width;
            canvas.height = height;
            canvas.getContext('2d').drawImage(img, 0, 0, width, height);
            canvas.toBlob((blob) => {
                resolve(blob.size < file.size
                    ? new File([blob], file.name.replace(/\.\w+$/, '.jpg'), { type: 'image/jpeg' })
                    : file
                );
            }, 'image/jpeg', quality);
            URL.revokeObjectURL(img.src);
        };
        img.src = URL.createObjectURL(file);
    });
};

// --- VoiceInputButton (Web Speech API STT) ---
// 규칙: 미확정=이탤릭+회색, 확정=일반체+진한색, 삭제금지, 교정허용, 부드러운 전환
function VoiceInputButton({ onResult, disabled }) {
    const [recording, setRecording] = useState(false);
    const [finalizedSegments, setFinalizedSegments] = useState([]);
    const [interimText, setInterimText] = useState('');
    const recognitionRef = useRef(null);
    const startTimeRef = useRef(null);
    const dismissTimerRef = useRef(null);
    const previewRef = useRef(null);
    const btnRef = useRef(null);

    const isSupported = typeof window !== 'undefined' &&
        (window.SpeechRecognition || window.webkitSpeechRecognition);

    const logUsage = useCallback((startTime) => {
        const duration = Math.max(1, Math.round((Date.now() - startTime) / 1000));
        apiFetch(API.logSttUsage, {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ duration_seconds: duration }),
        }).catch(() => {});
    }, []);

    // 프리뷰 자동 스크롤
    useEffect(() => {
        if (previewRef.current) {
            previewRef.current.scrollTop = previewRef.current.scrollHeight;
        }
    }, [finalizedSegments, interimText]);

    const stopRecording = useCallback(() => {
        recognitionRef.current?.stop();
        recognitionRef.current = null;
        if (startTimeRef.current) {
            logUsage(startTimeRef.current);
            startTimeRef.current = null;
        }
        setRecording(false);
        setInterimText('');
        // 녹음 종료 후 2초 뒤 프리뷰 닫기
        dismissTimerRef.current = setTimeout(() => {
            setFinalizedSegments([]);
        }, 2000);
    }, [logUsage]);

    const startRecording = useCallback(() => {
        // 이전 타이머 정리
        if (dismissTimerRef.current) { clearTimeout(dismissTimerRef.current); dismissTimerRef.current = null; }

        const SR = window.SpeechRecognition || window.webkitSpeechRecognition;
        const recognition = new SR();
        recognition.lang = 'ko-KR';
        recognition.continuous = true;
        recognition.interimResults = true;
        recognition.maxAlternatives = 1;

        recognition.onresult = (event) => {
            if (dismissTimerRef.current) { clearTimeout(dismissTimerRef.current); dismissTimerRef.current = null; }

            let currentInterim = '';
            for (let i = event.resultIndex; i < event.results.length; i++) {
                const transcript = event.results[i][0].transcript;
                if (event.results[i].isFinal) {
                    // 확정: input에 반영 + 로그에 영구 저장
                    onResult(transcript);
                    setFinalizedSegments(prev => [...prev, transcript]);
                    currentInterim = '';
                } else {
                    // 미확정: 교정은 허용하되 이전 확정분은 보존
                    currentInterim = transcript;
                }
            }
            setInterimText(currentInterim);
        };
        recognition.onerror = () => stopRecording();
        recognition.onend = () => {
            if (startTimeRef.current) {
                logUsage(startTimeRef.current);
                startTimeRef.current = null;
            }
            setRecording(false);
            setInterimText('');
            recognitionRef.current = null;
            dismissTimerRef.current = setTimeout(() => {
                setFinalizedSegments([]);
            }, 2000);
        };

        recognitionRef.current = recognition;
        startTimeRef.current = Date.now();
        setFinalizedSegments([]);
        setInterimText('');
        recognition.start();
        setRecording(true);
    }, [onResult, stopRecording, logUsage]);

    const toggle = useCallback((e) => {
        e.preventDefault();
        e.stopPropagation();
        if (disabled || !isSupported) return;
        recording ? stopRecording() : startRecording();
    }, [disabled, isSupported, recording, stopRecording, startRecording]);

    useEffect(() => {
        return () => {
            recognitionRef.current?.stop();
            if (dismissTimerRef.current) clearTimeout(dismissTimerRef.current);
        };
    }, []);

    if (!isSupported) return null;

    const hasContent = finalizedSegments.length > 0 || interimText;

    return (
        <div className="relative flex-shrink-0">
            <button
                ref={btnRef}
                type="button"
                onClick={toggle}
                disabled={disabled}
                title={recording ? '녹음 중지 (클릭)' : '음성으로 입력'}
                className={`inline-flex items-center justify-center w-8 h-8 rounded-full transition-all
                    ${recording
                        ? 'bg-red-500 text-white shadow-lg shadow-red-200'
                        : 'bg-gray-100 text-gray-500 hover:bg-blue-100 hover:text-blue-600'}
                    ${disabled ? 'opacity-30 cursor-not-allowed' : 'cursor-pointer'}`}
            >
                {recording ? (
                    <span className="relative flex items-center justify-center w-4 h-4">
                        <span className="absolute inset-0 rounded-full bg-white/30 animate-ping"></span>
                        <svg className="w-3.5 h-3.5 relative" fill="currentColor" viewBox="0 0 24 24">
                            <rect x="6" y="6" width="12" height="12" rx="2" />
                        </svg>
                    </span>
                ) : (
                    <svg className="w-4 h-4" fill="currentColor" viewBox="0 0 24 24">
                        <path d="M12 14c1.66 0 3-1.34 3-3V5c0-1.66-1.34-3-3-3S9 3.34 9 5v6c0 1.66 1.34 3 3 3z" />
                        <path d="M17 11c0 2.76-2.24 5-5 5s-5-2.24-5-5H5c0 3.53 2.61 6.43 6 6.92V21h2v-3.08c3.39-.49 6-3.39 6-6.92h-2z" />
                    </svg>
                )}
            </button>

            {/* 스트리밍 프리뷰 패널 (fixed로 overflow 클리핑 방지) */}
            {(recording || hasContent) && btnRef.current && (() => {
                const rect = btnRef.current.getBoundingClientRect();
                return (
                <div
                    ref={previewRef}
                    className="fixed bg-gray-900 rounded-lg shadow-xl z-[9999]
                        w-[300px] max-h-[120px] overflow-y-auto px-3 py-2"
                    style={{ lineHeight: '1.6', bottom: `${window.innerHeight - rect.top + 8}px`, right: `${window.innerWidth - rect.right}px` }}
                >
                    {/* 확정 텍스트: 일반체 + 흰색 - 삭제되지 않음 */}
                    {finalizedSegments.map((seg, i) => (
                        <span key={i} className="text-white text-xs font-normal transition-colors duration-300">
                            {seg}
                        </span>
                    ))}

                    {/* 미확정 텍스트: 이탤릭 + 연한 회색 - 교정 가능 */}
                    {interimText && (
                        <span className="text-gray-400 text-xs italic transition-colors duration-200">
                            {interimText}
                        </span>
                    )}

                    {/* 녹음 중 + 텍스트 없음: 대기 표시 */}
                    {recording && !hasContent && (
                        <span className="text-gray-500 text-xs flex items-center gap-1.5">
                            <span className="inline-block w-1.5 h-1.5 bg-red-400 rounded-full animate-pulse"></span>
                            말씀하세요...
                        </span>
                    )}

                    {/* 녹음 종료 후 확정 텍스트만 남아있을 때 */}
                    {!recording && finalizedSegments.length > 0 && !interimText && (
                        <span className="text-green-400 text-xs ml-1">&#10003;</span>
                    )}
                </div>
                );
            })()}
        </div>
    );
}

// --- ToastNotification ---
function ToastNotification({ message, type, onClose }) {
    useEffect(() => {
        const t = setTimeout(onClose, 3000);
        return () => clearTimeout(t);
    }, [onClose]);

    const colors = type === 'error' ? 'bg-red-500' : 'bg-green-500';
    return (
        <div className={`fixed top-4 right-4 z-[9999] px-6 py-3 rounded-lg shadow-lg text-white ${colors} transition-all`}>
            {message}
        </div>
    );
}

// --- PhotoUploadBox ---
function PhotoUploadBox({ type, photoPath, photoId, rowId, onUpload, onDelete, disabled }) {
    const [dragOver, setDragOver] = useState(false);
    const [uploading, setUploading] = useState(false);
    const inputRef = useRef(null);
    const colors = TYPE_COLORS[type];
    const hasPhoto = !!photoPath;

    const handleFiles = async (files) => {
        if (!files || files.length === 0 || !photoId || !rowId) return;
        const file = files[0];
        if (!file.type.startsWith('image/')) {
            alert('이미지 파일만 업로드할 수 있습니다.');
            return;
        }
        setUploading(true);
        try {
            const compressed = await compressImage(file);
            await onUpload(photoId, rowId, type, compressed);
        } finally {
            setUploading(false);
        }
    };

    const onDrop = (e) => {
        e.preventDefault();
        setDragOver(false);
        handleFiles(e.dataTransfer.files);
    };

    return (
        <div className="flex flex-col">
            <span className={`inline-block text-xs font-semibold mb-1.5 px-2 py-0.5 rounded ${colors.badge} w-fit`}>
                {TYPE_LABELS[type]}
            </span>
            <div
                className={`relative border-2 border-dashed rounded-lg overflow-hidden transition-all cursor-pointer aspect-[4/3]
                    ${dragOver ? 'border-blue-400 bg-blue-50' : hasPhoto ? `${colors.border} ${colors.bg}` : 'border-gray-300 bg-gray-50'}
                    ${disabled ? 'opacity-50 pointer-events-none' : 'hover:border-blue-400'}`}
                onDragOver={(e) => { e.preventDefault(); setDragOver(true); }}
                onDragLeave={() => setDragOver(false)}
                onDrop={onDrop}
                onClick={() => !uploading && inputRef.current?.click()}
            >
                {uploading && (
                    <div className="absolute inset-0 bg-black/40 flex items-center justify-center z-10">
                        <div className="w-8 h-8 border-3 border-white border-t-transparent rounded-full animate-spin"></div>
                    </div>
                )}
                {hasPhoto ? (
                    <>
                        <img
                            src={API.photoUrl(photoId, rowId, type)}
                            alt={TYPE_LABELS[type]}
                            className="w-full h-full object-cover"
                            loading="lazy"
                        />
                        <button
                            className="absolute top-1 right-1 bg-red-500 text-white rounded-full w-6 h-6 flex items-center justify-center text-xs hover:bg-red-600 z-10 shadow"
                            onClick={(e) => { e.stopPropagation(); onDelete(photoId, rowId, type); }}
                            title="사진 삭제"
                        >
                            &times;
                        </button>
                    </>
                ) : (
                    <div className="flex flex-col items-center justify-center h-full text-gray-400 p-3">
                        <svg className="w-8 h-8 mb-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                            <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.5" d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
                        </svg>
                        <span className="text-xs text-center">클릭 또는 드래그</span>
                    </div>
                )}
                <input
                    ref={inputRef}
                    type="file"
                    accept="image/jpeg,image/png,image/webp"
                    className="hidden"
                    onChange={(e) => handleFiles(e.target.files)}
                />
            </div>
        </div>
    );
}

// --- PhotoCard ---
function PhotoCard({ item, onSelect }) {
    const rows = item.rows || [];
    const firstRow = rows[0];
    const totalPhotos = rows.reduce((sum, row) => {
        let cnt = 0;
        ['before', 'during', 'after'].forEach(t => { if (row[`${t}_photo_path`]) cnt++; });
        return sum + cnt;
    }, 0);
    const totalSlots = rows.length * 3;

    return (
        <div
            className="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden hover:shadow-md transition-shadow cursor-pointer"
            onClick={() => onSelect(item)}
        >
            {/* 사진 썸네일 3칸 (첫 행 기준) */}
            <div className="grid grid-cols-3 gap-0.5 bg-gray-100 p-0.5">
                {['before', 'during', 'after'].map(type => (
                    <div key={type} className="aspect-square bg-gray-200 overflow-hidden relative">
                        {firstRow && firstRow[`${type}_photo_path`] ? (
                            <img
                                src={API.photoUrl(item.id, firstRow.id, type)}
                                alt={TYPE_LABELS[type]}
                                className="w-full h-full object-cover"
                                loading="lazy"
                            />
                        ) : (
                            <div className="w-full h-full flex items-center justify-center text-gray-400">
                                <svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                                    <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.5"
                                        d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
                                </svg>
                            </div>
                        )}
                        <span className={`absolute bottom-0 left-0 right-0 text-center text-[9px] py-0.5 font-medium ${TYPE_COLORS[type].badge}`}>
                            {TYPE_LABELS[type]}
                        </span>
                    </div>
                ))}
            </div>
            {/* 정보 */}
            <div className="p-3">
                <h3 className="font-semibold text-sm text-gray-900 truncate">{item.site_name}</h3>
                <div className="flex items-center justify-between mt-1.5">
                    <span className="text-xs text-gray-500">{item.work_date}</span>
                    <span className="text-xs text-gray-400">
                        {totalPhotos}/{totalSlots}
                        {rows.length > 1 && ` (${rows.length}행)`}
                    </span>
                </div>
                {item.user && (
                    <span className="text-xs text-gray-400 mt-1 block">{item.user.name}</span>
                )}
            </div>
        </div>
    );
}

// --- CreateModal ---
function CreateModal({ show, onClose, onCreate }) {
    const [siteName, setSiteName] = useState('');
    const [workDate, setWorkDate] = useState(new Date().toISOString().split('T')[0]);
    const [description, setDescription] = useState('');
    const [saving, setSaving] = useState(false);

    if (!show) return null;

    const handleSubmit = async (e) => {
        e.preventDefault();
        if (!siteName.trim()) return alert('현장명을 입력해주세요.');
        setSaving(true);
        try {
            await onCreate({ site_name: siteName.trim(), work_date: workDate, description: description.trim() || null });
            setSiteName('');
            setDescription('');
            onClose();
        } catch (err) {
            alert(err.message);
        } finally {
            setSaving(false);
        }
    };

    return (
        <div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4" onClick={onClose}>
            <div className="bg-white rounded-2xl shadow-xl w-full max-w-md" onClick={e => e.stopPropagation()}>
                <div className="p-6">
                    <h2 className="text-lg font-bold text-gray-900 mb-4">새 사진대지 등록</h2>
                    <form onSubmit={handleSubmit} className="space-y-4">
                        <div>
                            <label className="block text-sm font-medium text-gray-700 mb-1">현장명 *</label>
                            <div className="flex items-center gap-2">
                                <input
                                    type="text"
                                    value={siteName}
                                    onChange={e => setSiteName(e.target.value)}
                                    className="flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-sm"
                                    placeholder="공사 현장명을 입력하세요"
                                    autoFocus
                                />
                                <VoiceInputButton onResult={(text) => setSiteName(prev => prev ? prev + ' ' + text : text)} />
                            </div>
                        </div>
                        <div>
                            <label className="block text-sm font-medium text-gray-700 mb-1">작업일자 *</label>
                            <input
                                type="date"
                                value={workDate}
                                onChange={e => setWorkDate(e.target.value)}
                                className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-sm"
                            />
                        </div>
                        <div>
                            <label className="block text-sm font-medium text-gray-700 mb-1">설명</label>
                            <div className="flex items-start gap-2">
                                <textarea
                                    value={description}
                                    onChange={e => setDescription(e.target.value)}
                                    className="flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-sm"
                                    rows={3}
                                    placeholder="작업 내용을 간단히 기록하세요"
                                />
                                <VoiceInputButton onResult={(text) => setDescription(prev => prev ? prev + ' ' + text : text)} />
                            </div>
                        </div>
                        <div className="flex justify-end gap-2 pt-2">
                            <button type="button" onClick={onClose} className="px-4 py-2 text-sm text-gray-700 bg-gray-100 rounded-lg hover:bg-gray-200">
                                취소
                            </button>
                            <button type="submit" disabled={saving} className="px-4 py-2 text-sm text-white bg-blue-600 rounded-lg hover:bg-blue-700 disabled:opacity-50">
                                {saving ? '등록 중...' : '등록'}
                            </button>
                        </div>
                    </form>
                </div>
            </div>
        </div>
    );
}

// --- DetailModal ---
function DetailModal({ item, onClose, onUpload, onDeletePhoto, onUpdate, onDelete, onRefresh, onAddRow, onDeleteRow, initialEditing }) {
    const [editing, setEditing] = useState(false);
    const [siteName, setSiteName] = useState('');
    const [workDate, setWorkDate] = useState('');
    const [description, setDescription] = useState('');
    const [saving, setSaving] = useState(false);
    const [lightbox, setLightbox] = useState(null);

    useEffect(() => {
        if (item) {
            setSiteName(item.site_name);
            setWorkDate(item.work_date ? item.work_date.substring(0, 10) : '');
            setDescription(item.description || '');
            setEditing(!!initialEditing);
        }
    }, [item]);

    if (!item) return null;

    const rows = item.rows || [];

    const handleSave = async () => {
        if (!siteName.trim()) return alert('현장명을 입력해주세요.');
        setSaving(true);
        try {
            await onUpdate(item.id, { site_name: siteName.trim(), work_date: workDate, description: description.trim() || null });
            setEditing(false);
            onRefresh();
        } catch (err) {
            alert(err.message);
        } finally {
            setSaving(false);
        }
    };

    const handleDelete = async () => {
        if (!confirm('이 사진대지를 삭제하시겠습니까? 모든 사진이 함께 삭제됩니다.')) return;
        try {
            await onDelete(item.id);
            onClose();
        } catch (err) {
            alert(err.message);
        }
    };

    const handleAddRow = async () => {
        try {
            await onAddRow(item.id);
        } catch (err) {
            alert(err.message);
        }
    };

    const handleDeleteRow = async (rowId) => {
        if (!confirm('이 사진 행을 삭제하시겠습니까? 해당 행의 모든 사진이 삭제됩니다.')) return;
        try {
            await onDeleteRow(item.id, rowId);
        } catch (err) {
            alert(err.message);
        }
    };

    return (
        <div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4" onClick={onClose}>
            <div className="bg-white rounded-2xl shadow-xl w-full max-w-3xl max-h-[90vh] overflow-y-auto" onClick={e => e.stopPropagation()}>
                {/* Header */}
                <div className="sticky top-0 bg-white border-b border-gray-200 px-6 py-4 flex items-center justify-between rounded-t-2xl z-10">
                    {editing ? (
                        <div className="flex items-center gap-2 flex-1 mr-3">
                            <input
                                type="text"
                                value={siteName}
                                onChange={e => setSiteName(e.target.value)}
                                className="text-lg font-bold text-gray-900 border border-gray-300 rounded-lg px-3 py-1 flex-1"
                            />
                            <VoiceInputButton onResult={(text) => setSiteName(prev => prev ? prev + ' ' + text : text)} />
                        </div>
                    ) : (
                        <h2 className="text-lg font-bold text-gray-900">{item.site_name}</h2>
                    )}
                    <button onClick={onClose} className="text-gray-400 hover:text-gray-600 text-xl">&times;</button>
                </div>

                <div className="p-6">
                    {/* Info */}
                    <div className="flex flex-wrap gap-4 mb-6">
                        {editing ? (
                            <>
                                <div>
                                    <label className="block text-xs text-gray-500 mb-1">작업일자</label>
                                    <input type="date" value={workDate} onChange={e => setWorkDate(e.target.value)}
                                        className="px-3 py-1.5 border border-gray-300 rounded-lg text-sm" />
                                </div>
                                <div className="flex-1 min-w-[200px]">
                                    <label className="block text-xs text-gray-500 mb-1">설명</label>
                                    <div className="flex items-start gap-2">
                                        <textarea value={description} onChange={e => setDescription(e.target.value)}
                                            className="flex-1 px-3 py-1.5 border border-gray-300 rounded-lg text-sm" rows={2} />
                                        <VoiceInputButton onResult={(text) => setDescription(prev => prev ? prev + ' ' + text : text)} />
                                    </div>
                                </div>
                            </>
                        ) : (
                            <>
                                <div className="flex items-center gap-2 text-sm text-gray-600">
                                    <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>
                                    {item.work_date}
                                </div>
                                {item.user && (
                                    <div className="flex items-center gap-2 text-sm text-gray-600">
                                        <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                                            <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
                                        </svg>
                                        {item.user.name}
                                    </div>
                                )}
                                {item.description && (
                                    <p className="text-sm text-gray-600 w-full">{item.description}</p>
                                )}
                            </>
                        )}
                    </div>

                    {/* Photo Rows */}
                    <div className="space-y-4 mb-6">
                        {rows.map((row, idx) => (
                            <div key={row.id} className="border border-gray-200 rounded-xl p-4 bg-gray-50/50">
                                <div className="flex items-center justify-between mb-3">
                                    <span className="text-sm font-medium text-gray-700">행 {idx + 1}</span>
                                    {editing && rows.length > 1 && (
                                        <button
                                            onClick={() => handleDeleteRow(row.id)}
                                            className="text-xs text-red-500 hover:text-red-700 px-2 py-1 rounded hover:bg-red-50 transition-colors"
                                            title="이 행 삭제"
                                        >
                                            행 삭제
                                        </button>
                                    )}
                                </div>
                                <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
                                    {['before', 'during', 'after'].map(type => (
                                        <PhotoUploadBox
                                            key={type}
                                            type={type}
                                            photoPath={row[`${type}_photo_path`]}
                                            photoId={item.id}
                                            rowId={row.id}
                                            onUpload={onUpload}
                                            onDelete={onDeletePhoto}
                                            disabled={false}
                                        />
                                    ))}
                                </div>
                            </div>
                        ))}

                        {/* 행 추가 버튼 (수정 모드에서만 활성화) */}
                        <button
                            onClick={handleAddRow}
                            disabled={!editing}
                            className={`w-full border-2 border-dashed rounded-xl py-4 transition-all flex items-center justify-center gap-2
                                ${editing
                                    ? 'border-gray-300 text-gray-400 hover:border-blue-400 hover:text-blue-500 hover:bg-blue-50/50 cursor-pointer'
                                    : 'border-gray-200 text-gray-300 cursor-not-allowed'}`}
                        >
                            <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>
                            <span className="text-sm font-medium">사진 행 추가</span>
                        </button>
                    </div>

                    {/* Actions */}
                    <div className="flex justify-between pt-4 border-t border-gray-200">
                        <button onClick={handleDelete}
                            className="px-4 py-2 text-sm text-red-600 bg-red-50 rounded-lg hover:bg-red-100">
                            삭제
                        </button>
                        <div className="flex gap-2">
                            {editing ? (
                                <>
                                    <button onClick={() => setEditing(false)}
                                        className="px-4 py-2 text-sm text-gray-700 bg-gray-100 rounded-lg hover:bg-gray-200">
                                        취소
                                    </button>
                                    <button onClick={handleSave} disabled={saving}
                                        className="px-4 py-2 text-sm text-white bg-blue-600 rounded-lg hover:bg-blue-700 disabled:opacity-50">
                                        {saving ? '저장 중...' : '저장'}
                                    </button>
                                </>
                            ) : (
                                <button onClick={() => setEditing(true)}
                                    className="px-4 py-2 text-sm text-gray-700 bg-gray-100 rounded-lg hover:bg-gray-200">
                                    수정
                                </button>
                            )}
                        </div>
                    </div>
                </div>
            </div>

            {/* Lightbox */}
            {lightbox && (
                <div className="fixed inset-0 bg-black/80 flex items-center justify-center z-[60]"
                    onClick={() => setLightbox(null)}>
                    <img src={lightbox} className="max-w-[90vw] max-h-[90vh] object-contain" />
                </div>
            )}
        </div>
    );
}

// --- App ---
function App() {
    const [items, setItems] = useState([]);
    const [pagination, setPagination] = useState({ current_page: 1, last_page: 1 });
    const [loading, setLoading] = useState(false);
    const [search, setSearch] = useState('');
    const [dateFrom, setDateFrom] = useState('');
    const [dateTo, setDateTo] = useState('');
    const [showCreate, setShowCreate] = useState(false);
    const [selectedItem, setSelectedItem] = useState(null);
    const [detailEditing, setDetailEditing] = useState(false);
    const [toast, setToast] = useState(null);
    const modalDirtyRef = useRef(false);

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

    const fetchList = useCallback(async (page = 1) => {
        setLoading(true);
        try {
            const params = new URLSearchParams({ per_page: '12', page: String(page) });
            if (search) params.set('search', search);
            if (dateFrom) params.set('date_from', dateFrom);
            if (dateTo) params.set('date_to', dateTo);

            const res = await apiFetch(`${API.list}?${params}`);
            setItems(res.data.data || []);
            setPagination({
                current_page: res.data.current_page,
                last_page: res.data.last_page,
                total: res.data.total,
            });
        } catch (err) {
            showToast(err.message, 'error');
        } finally {
            setLoading(false);
        }
    }, [search, dateFrom, dateTo]);

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

    const handleCreate = async (data) => {
        const res = await apiFetch(API.store, {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(data),
        });
        showToast('사진대지가 등록되었습니다.');
        modalDirtyRef.current = true;
        // 생성 후 바로 상세 열기 (수정 모드)
        setDetailEditing(true);
        setSelectedItem(res.data);
    };

    const handleUpload = async (id, rowId, type, file) => {
        const formData = new FormData();
        formData.append('type', type);
        formData.append('photo', file);

        const res = await apiFetch(API.upload(id, rowId), {
            method: 'POST',
            body: formData,
        });
        showToast('사진이 업로드되었습니다.');
        modalDirtyRef.current = true;
        if (selectedItem?.id === id) {
            setSelectedItem(res.data);
        }
    };

    const handleDeletePhoto = async (id, rowId, type) => {
        if (!confirm(`${TYPE_LABELS[type]} 사진을 삭제하시겠습니까?`)) return;
        const res = await apiFetch(API.deletePhoto(id, rowId, type), { method: 'DELETE' });
        showToast('사진이 삭제되었습니다.');
        modalDirtyRef.current = true;
        if (selectedItem?.id === id) {
            setSelectedItem(res.data);
        }
    };

    const handleUpdate = async (id, data) => {
        await apiFetch(API.update(id), {
            method: 'PUT',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(data),
        });
        showToast('사진대지가 수정되었습니다.');
        modalDirtyRef.current = true;
    };

    const handleDelete = async (id) => {
        await apiFetch(API.destroy(id), { method: 'DELETE' });
        showToast('사진대지가 삭제되었습니다.');
        modalDirtyRef.current = true;
    };

    const handleAddRow = async (id) => {
        const res = await apiFetch(API.addRow(id), { method: 'POST' });
        showToast('사진 행이 추가되었습니다.');
        modalDirtyRef.current = true;
        if (selectedItem?.id === id) {
            setSelectedItem(res.data);
        }
    };

    const handleDeleteRow = async (id, rowId) => {
        const res = await apiFetch(API.deleteRow(id, rowId), { method: 'DELETE' });
        showToast('사진 행이 삭제되었습니다.');
        modalDirtyRef.current = true;
        if (selectedItem?.id === id) {
            setSelectedItem(res.data);
        }
    };

    const handleSelectItem = async (item) => {
        modalDirtyRef.current = false;
        setDetailEditing(false);
        try {
            const res = await apiFetch(API.show(item.id));
            setSelectedItem(res.data);
        } catch {
            setSelectedItem(item);
        }
    };

    const handleCloseDetail = useCallback(() => {
        setSelectedItem(null);
        if (modalDirtyRef.current) {
            modalDirtyRef.current = false;
            fetchList();
        }
    }, [fetchList]);

    const refreshSelected = async () => {
        if (!selectedItem) return;
        try {
            const res = await apiFetch(API.show(selectedItem.id));
            setSelectedItem(res.data);
        } catch {}
    };

    return (
        <div className="min-h-screen bg-gray-50">
            {toast && <ToastNotification message={toast.message} type={toast.type} onClose={() => setToast(null)} />}

            {/* Header */}
            <div className="bg-white border-b border-gray-200 px-6 py-4">
                <div className="flex flex-wrap items-center justify-between gap-4">
                    <div>
                        <h1 className="text-xl font-bold text-gray-900">공사현장 사진대지</h1>
                        <p className="text-sm text-gray-500 mt-0.5">작업전/작업중/작업후 현장 사진을 관리합니다</p>
                    </div>
                    <button
                        onClick={() => setShowCreate(true)}
                        className="inline-flex items-center gap-2 px-4 py-2.5 bg-blue-600 text-white text-sm font-medium rounded-lg hover:bg-blue-700 shadow-sm"
                    >
                        <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                            <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 4v16m8-8H4" />
                        </svg>
                        새 사진대지
                    </button>
                </div>

                {/* Filters */}
                <div className="flex flex-wrap gap-3 mt-4">
                    <input
                        type="text"
                        placeholder="현장명, 설명 검색..."
                        value={search}
                        onChange={e => setSearch(e.target.value)}
                        className="px-3 py-2 border border-gray-300 rounded-lg text-sm w-64 focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
                    />
                    <div className="flex items-center gap-2">
                        <input
                            type="date"
                            value={dateFrom}
                            onChange={e => setDateFrom(e.target.value)}
                            className="px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
                        />
                        <span className="text-gray-400">~</span>
                        <input
                            type="date"
                            value={dateTo}
                            onChange={e => setDateTo(e.target.value)}
                            className="px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
                        />
                    </div>
                    {(search || dateFrom || dateTo) && (
                        <button
                            onClick={() => { setSearch(''); setDateFrom(''); setDateTo(''); }}
                            className="px-3 py-2 text-sm text-gray-600 bg-gray-100 rounded-lg hover:bg-gray-200"
                        >
                            초기화
                        </button>
                    )}
                </div>
            </div>

            {/* Content */}
            <div className="p-6">
                {loading ? (
                    <div className="flex items-center justify-center py-20">
                        <div className="w-8 h-8 border-3 border-blue-500 border-t-transparent rounded-full animate-spin"></div>
                    </div>
                ) : items.length === 0 ? (
                    <div className="text-center py-20">
                        <svg className="w-16 h-16 text-gray-300 mx-auto mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                            <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.5"
                                d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
                        </svg>
                        <p className="text-gray-500 text-lg">등록된 사진대지가 없습니다</p>
                        <p className="text-gray-400 text-sm mt-1">새 사진대지를 등록해보세요</p>
                    </div>
                ) : (
                    <>
                        <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
                            {items.map(item => (
                                <PhotoCard
                                    key={item.id}
                                    item={item}
                                    onSelect={handleSelectItem}
                                />
                            ))}
                        </div>

                        {/* Pagination */}
                        {pagination.last_page > 1 && (
                            <div className="flex items-center justify-center gap-2 mt-8">
                                <button
                                    disabled={pagination.current_page === 1}
                                    onClick={() => fetchList(pagination.current_page - 1)}
                                    className="px-3 py-2 text-sm border border-gray-300 rounded-lg hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
                                >
                                    이전
                                </button>
                                <span className="text-sm text-gray-600">
                                    {pagination.current_page} / {pagination.last_page}
                                    {pagination.total !== undefined && ` (총 ${pagination.total}건)`}
                                </span>
                                <button
                                    disabled={pagination.current_page === pagination.last_page}
                                    onClick={() => fetchList(pagination.current_page + 1)}
                                    className="px-3 py-2 text-sm border border-gray-300 rounded-lg hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
                                >
                                    다음
                                </button>
                            </div>
                        )}
                    </>
                )}
            </div>

            {/* Modals */}
            <CreateModal show={showCreate} onClose={() => setShowCreate(false)} onCreate={handleCreate} />
            <DetailModal
                item={selectedItem}
                onClose={handleCloseDetail}
                onUpload={handleUpload}
                onDeletePhoto={handleDeletePhoto}
                onUpdate={handleUpdate}
                onDelete={handleDelete}
                onRefresh={refreshSelected}
                onAddRow={handleAddRow}
                onDeleteRow={handleDeleteRow}
                initialEditing={detailEditing}
            />
        </div>
    );
}

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