diff --git a/frontend/src/pages/pc/admin/AdminAlbumPhotos.jsx b/frontend/src/pages/pc/admin/AdminAlbumPhotos.jsx index 9454039..28b3a74 100644 --- a/frontend/src/pages/pc/admin/AdminAlbumPhotos.jsx +++ b/frontend/src/pages/pc/admin/AdminAlbumPhotos.jsx @@ -13,6 +13,7 @@ function AdminAlbumPhotos() { const { albumId } = useParams(); const navigate = useNavigate(); const fileInputRef = useRef(null); + const photoListRef = useRef(null); // 사진 목록 영역 ref const [album, setAlbum] = useState(null); const [photos, setPhotos] = useState([]); @@ -38,6 +39,98 @@ function AdminAlbumPhotos() { const [processingProgress, setProcessingProgress] = useState({ current: 0, total: 0 }); // 서버 처리 진행률 const [pendingDeleteId, setPendingDeleteId] = useState(null); // 삭제 대기 파일 ID + // 일괄 편집 도구 상태 + const [bulkEdit, setBulkEdit] = useState({ + range: '', // 예: "1-5, 8, 10-12" + groupType: '', // 'group' | 'solo' | 'unit' | ''(미적용) + members: [], // 선택된 멤버들 + conceptName: '', // 컨셉명 + }); + + // 범위 문자열 파싱 (표시 번호 기준, 예: "18-22" → [0, 1, 2, 3, 4]) + const parseRange = (rangeStr, baseNumber = 1) => { + if (!rangeStr.trim()) return []; + const indices = new Set(); + const parts = rangeStr.split(',').map(s => s.trim()); + + for (const part of parts) { + if (part.includes('-')) { + const [start, end] = part.split('-').map(n => parseInt(n.trim())); + if (!isNaN(start) && !isNaN(end)) { + for (let i = Math.min(start, end); i <= Math.max(start, end); i++) { + // 표시 번호를 0-indexed로 변환 + const idx = i - baseNumber; + if (idx >= 0) indices.add(idx); + } + } + } else { + const num = parseInt(part); + if (!isNaN(num)) { + const idx = num - baseNumber; + if (idx >= 0) indices.add(idx); + } + } + } + return Array.from(indices).sort((a, b) => a - b); + }; + + // 일괄 편집 적용 + const applyBulkEdit = () => { + const indices = parseRange(bulkEdit.range, startNumber); + if (indices.length === 0) { + setToast({ message: '적용할 번호 범위를 입력하세요.', type: 'warning' }); + return; + } + + const validIndices = indices.filter(i => i < pendingFiles.length); + if (validIndices.length === 0) { + setToast({ message: '유효한 번호가 없습니다.', type: 'error' }); + return; + } + + setPendingFiles(prev => prev.map((file, idx) => { + if (!validIndices.includes(idx)) return file; + + const updates = {}; + if (bulkEdit.groupType) { + updates.groupType = bulkEdit.groupType; + // 단체 선택 시 멤버 비움 + if (bulkEdit.groupType === 'group') { + updates.members = []; + } else if (bulkEdit.members.length > 0) { + // 솔로일 경우 첫 번째만 + updates.members = bulkEdit.groupType === 'solo' + ? [bulkEdit.members[0]] + : [...bulkEdit.members]; + } + } else if (bulkEdit.members.length > 0) { + // 타입 미선택이지만 멤버 선택한 경우 + updates.members = [...bulkEdit.members]; + } + + if (bulkEdit.conceptName) { + updates.conceptName = bulkEdit.conceptName; + } + + return { ...file, ...updates }; + })); + + setToast({ message: `${validIndices.length}개 사진에 일괄 적용되었습니다.`, type: 'success' }); + + // 입력 초기화 + setBulkEdit({ range: '', groupType: '', members: [], conceptName: '' }); + }; + + // 일괄 편집 멤버 토글 + const toggleBulkMember = (memberId) => { + setBulkEdit(prev => ({ + ...prev, + members: prev.members.includes(memberId) + ? prev.members.filter(m => m !== memberId) + : [...prev.members, memberId] + })); + }; + // Toast 자동 숨김 useEffect(() => { if (toast) { @@ -687,21 +780,23 @@ function AdminAlbumPhotos() { )} - {/* 드래그 앤 드롭 영역 + 파일 목록 */} - + {/* 2-column 레이아웃: 사진 목록 + 일괄 편집 패널 */} +
+ {/* 드래그 앤 드롭 영역 + 파일 목록 */} + {/* 드래그 오버레이 */} {dragOver && (
@@ -874,9 +969,123 @@ function AdminAlbumPhotos() {
)} -
+ + {/* 일괄 편집 도구 - CSS sticky (네이티브 성능) */} + {pendingFiles.length > 0 && photoType === 'concept' && ( +
+
+

+ + 일괄 편집 +

+ + {/* 번호 범위 */} +
+ + setBulkEdit(prev => ({ ...prev, range: e.target.value }))} + placeholder={`예: ${startNumber}-${startNumber + 4}, ${startNumber + 7}`} + className="w-full px-3 py-2 text-sm border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary" + /> +

+ {startNumber}~{startNumber + pendingFiles.length - 1}번 중 {parseRange(bulkEdit.range, startNumber).filter(i => i < pendingFiles.length).length}개 선택 +

+
+ {/* 타입 선택 */} +
+ +
+ {[ + { value: 'group', icon: Users, label: '단체' }, + { value: 'solo', icon: User, label: '솔로' }, + { value: 'unit', icon: Users2, label: '유닛' }, + ].map(({ value, icon: Icon, label }) => ( + + ))} +
+
+ + {/* 멤버 선택 */} + {bulkEdit.groupType !== 'group' && ( +
+ +
+ {members.map(member => ( + + ))} +
+
+ )} + + {/* 컨셉명 */} +
+ + setBulkEdit(prev => ({ ...prev, conceptName: e.target.value }))} + placeholder="컨셉명 입력" + className="w-full px-3 py-2 text-sm border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary" + /> +
+ + {/* 적용 버튼 */} + +
+
+ )} +
{/* 삭제 확인 다이얼로그 */}