diff --git a/backend/routes/admin.js b/backend/routes/admin.js index d55e33f..5068f68 100644 --- a/backend/routes/admin.js +++ b/backend/routes/admin.js @@ -430,7 +430,7 @@ router.get("/albums/:albumId/photos", async (req, res) => { const [photos] = await pool.query( ` SELECT - p.id, p.photo_url, p.photo_type, p.concept_name, + p.id, p.original_url, p.medium_url, p.thumb_url, p.photo_type, p.concept_name, p.sort_order, p.width, p.height, p.file_size, GROUP_CONCAT(pm.member_id) as member_ids FROM album_photos p @@ -442,12 +442,9 @@ router.get("/albums/:albumId/photos", async (req, res) => { [albumId] ); - // URL 변환 및 멤버 배열 파싱 + // 멤버 배열 파싱 const result = photos.map((photo) => ({ ...photo, - photo_url: photo.photo_url, - thumb_url: photo.photo_url.replace("/original/", "/thumb_400/"), - medium_url: photo.photo_url.replace("/original/", "/medium_800/"), members: photo.member_ids ? photo.member_ids.split(",").map(Number) : [], })); @@ -458,6 +455,95 @@ router.get("/albums/:albumId/photos", async (req, res) => { } }); +// 앨범 티저 목록 조회 +router.get("/albums/:albumId/teasers", async (req, res) => { + try { + const { albumId } = req.params; + + // 앨범 존재 확인 + const [albums] = await pool.query( + "SELECT folder_name FROM albums WHERE id = ?", + [albumId] + ); + if (albums.length === 0) { + return res.status(404).json({ error: "앨범을 찾을 수 없습니다." }); + } + + // 티저 조회 + const [teasers] = await pool.query( + `SELECT id, original_url, medium_url, thumb_url, sort_order + FROM album_teasers + WHERE album_id = ? + ORDER BY sort_order ASC`, + [albumId] + ); + + res.json(teasers); + } catch (error) { + console.error("티저 조회 오류:", error); + res.status(500).json({ error: "티저 조회 중 오류가 발생했습니다." }); + } +}); + +// 티저 삭제 +router.delete( + "/albums/:albumId/teasers/:teaserId", + authenticateToken, + async (req, res) => { + const connection = await pool.getConnection(); + + try { + await connection.beginTransaction(); + + const { albumId, teaserId } = req.params; + + // 티저 정보 조회 + const [teasers] = await connection.query( + "SELECT t.*, a.folder_name FROM album_teasers t JOIN albums a ON t.album_id = a.id WHERE t.id = ? AND t.album_id = ?", + [teaserId, albumId] + ); + + if (teasers.length === 0) { + return res.status(404).json({ error: "티저를 찾을 수 없습니다." }); + } + + const teaser = teasers[0]; + const filename = teaser.original_url.split("/").pop(); + const basePath = `album/${teaser.folder_name}/teaser`; + + // RustFS에서 삭제 (3가지 크기 모두) + const sizes = ["original", "medium_800", "thumb_400"]; + for (const size of sizes) { + try { + await s3Client.send( + new DeleteObjectCommand({ + Bucket: BUCKET, + Key: `${basePath}/${size}/${filename}`, + }) + ); + } catch (s3Error) { + console.error(`S3 삭제 오류 (${size}):`, s3Error); + } + } + + // 티저 삭제 + await connection.query("DELETE FROM album_teasers WHERE id = ?", [ + teaserId, + ]); + + await connection.commit(); + + res.json({ message: "티저가 삭제되었습니다." }); + } catch (error) { + await connection.rollback(); + console.error("티저 삭제 오류:", error); + res.status(500).json({ error: "티저 삭제 중 오류가 발생했습니다." }); + } finally { + connection.release(); + } + } +); + // 사진 업로드 (SSE로 실시간 진행률 전송) router.post( "/albums/:albumId/photos", diff --git a/frontend/src/pages/pc/admin/AdminAlbumPhotos.jsx b/frontend/src/pages/pc/admin/AdminAlbumPhotos.jsx index d40dd07..14e8971 100644 --- a/frontend/src/pages/pc/admin/AdminAlbumPhotos.jsx +++ b/frontend/src/pages/pc/admin/AdminAlbumPhotos.jsx @@ -17,6 +17,7 @@ function AdminAlbumPhotos() { const [album, setAlbum] = useState(null); const [photos, setPhotos] = useState([]); + const [teasers, setTeasers] = useState([]); // 티저 이미지 const [loading, setLoading] = useState(true); const [user, setUser] = useState(null); const [toast, setToast] = useState(null); @@ -39,6 +40,8 @@ function AdminAlbumPhotos() { const [processingProgress, setProcessingProgress] = useState({ current: 0, total: 0 }); // 서버 처리 진행률 const [pendingDeleteId, setPendingDeleteId] = useState(null); // 삭제 대기 파일 ID const [uploadConfirmDialog, setUploadConfirmDialog] = useState(false); // 업로드 확인 다이얼로그 + const [activeTab, setActiveTab] = useState('upload'); // 'upload' | 'manage' + const [manageSubTab, setManageSubTab] = useState('concept'); // 'concept' | 'teaser' // 일괄 편집 도구 상태 const [bulkEdit, setBulkEdit] = useState({ @@ -156,6 +159,8 @@ function AdminAlbumPhotos() { const fetchAlbumData = async () => { try { + const token = localStorage.getItem('adminToken'); + // 앨범 정보 로드 const albumRes = await fetch(`/api/albums/${albumId}`); if (!albumRes.ok) throw new Error('앨범을 찾을 수 없습니다'); @@ -169,8 +174,24 @@ function AdminAlbumPhotos() { setMembers(membersData); } - // TODO: 기존 사진 목록 로드 (API 구현 후) - setPhotos([]); + // 기존 컨셉 포토 목록 로드 + const photosRes = await fetch(`/api/admin/albums/${albumId}/photos`, { + headers: { 'Authorization': `Bearer ${token}` } + }); + if (photosRes.ok) { + const photosData = await photosRes.json(); + setPhotos(photosData); + } + + // 티저 이미지 목록 로드 + const teasersRes = await fetch(`/api/admin/albums/${albumId}/teasers`, { + headers: { 'Authorization': `Bearer ${token}` } + }); + if (teasersRes.ok) { + const teasersData = await teasersRes.json(); + setTeasers(teasersData); + } + setLoading(false); } catch (error) { console.error('앨범 로드 오류:', error); @@ -455,18 +476,54 @@ function AdminAlbumPhotos() { } }; - // 삭제 처리 (기존 사진) + // 삭제 처리 (기존 사진/티저) const handleDelete = async () => { setDeleting(true); + const token = localStorage.getItem('adminToken'); - // TODO: 실제 삭제 API 호출 - await new Promise(r => setTimeout(r, 1000)); + try { + // 사진 ID와 티저 ID 분리 + const photoIds = deleteDialog.photos.filter(id => typeof id === 'number' || !String(id).startsWith('teaser-')); + const teaserIds = deleteDialog.photos + .filter(id => String(id).startsWith('teaser-')) + .map(id => parseInt(String(id).replace('teaser-', ''))); - setPhotos(prev => prev.filter(p => !deleteDialog.photos.includes(p.id))); - setSelectedPhotos([]); - setToast({ message: `${deleteDialog.photos.length}개의 사진이 삭제되었습니다.`, type: 'success' }); - setDeleteDialog({ show: false, photos: [] }); - setDeleting(false); + // 사진 삭제 + for (const photoId of photoIds) { + const res = await fetch(`/api/admin/albums/${albumId}/photos/${photoId}`, { + method: 'DELETE', + headers: { 'Authorization': `Bearer ${token}` } + }); + if (!res.ok) { + throw new Error('사진 삭제 실패'); + } + } + + // 티저 삭제 + for (const teaserId of teaserIds) { + const res = await fetch(`/api/admin/albums/${albumId}/teasers/${teaserId}`, { + method: 'DELETE', + headers: { 'Authorization': `Bearer ${token}` } + }); + if (!res.ok) { + throw new Error('티저 삭제 실패'); + } + } + + // UI 상태 업데이트 + setPhotos(prev => prev.filter(p => !photoIds.includes(p.id))); + setTeasers(prev => prev.filter(t => !teaserIds.includes(t.id))); + setSelectedPhotos([]); + + const totalDeleted = photoIds.length + teaserIds.length; + setToast({ message: `${totalDeleted}개 항목이 삭제되었습니다.`, type: 'success' }); + } catch (error) { + console.error('삭제 오류:', error); + setToast({ message: '삭제 중 오류가 발생했습니다.', type: 'error' }); + } finally { + setDeleteDialog({ show: false, photos: [] }); + setDeleting(false); + } }; if (loading) { @@ -666,6 +723,41 @@ function AdminAlbumPhotos() { )} + {/* 탭 UI */} +
컨셉 포토 {photos.length}장 / 티저 {teasers.length}장
+등록된 컨셉 포토가 없습니다
+업로드 탭에서 사진을 추가하세요
+등록된 티저 이미지가 없습니다
+업로드 탭에서 티저를 추가하세요
+