From 1c9b30b783bd8a5a28c04eae1ec5d340f5454dcd Mon Sep 17 00:00:00 2001 From: caadiq Date: Fri, 23 Jan 2026 11:24:42 +0900 Subject: [PATCH] =?UTF-8?q?=EC=97=90=EB=9F=AC=20=EC=9C=A0=ED=8B=B8?= =?UTF-8?q?=EB=A6=AC=ED=8B=B0=20=ED=95=A8=EC=88=98=EB=A5=BC=20=EB=AA=A8?= =?UTF-8?q?=EB=93=A0=20=EB=9D=BC=EC=9A=B0=ED=8A=B8=EC=97=90=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit utils/error.js에 정의된 헬퍼 함수들(badRequest, unauthorized, notFound, conflict, serverError)을 전체 라우트 파일에 적용하여 에러 응답 처리 일관성 확보 Co-Authored-By: Claude Opus 4.5 --- backend/src/routes/admin/bots.js | 11 ++-- backend/src/routes/admin/x.js | 9 +-- backend/src/routes/admin/youtube.js | 15 ++--- backend/src/routes/albums/index.js | 19 +++--- backend/src/routes/albums/photos.js | 5 +- backend/src/routes/albums/teasers.js | 5 +- backend/src/routes/auth.js | 9 +-- backend/src/routes/members/index.js | 11 ++-- backend/src/routes/schedules/index.js | 17 +++--- backend/src/routes/schedules/suggestions.js | 5 +- backend/src/routes/stats/index.js | 4 +- docs/code-review.md | 64 ++++++--------------- 12 files changed, 78 insertions(+), 96 deletions(-) diff --git a/backend/src/routes/admin/bots.js b/backend/src/routes/admin/bots.js index 8dbd2b7..514b1d7 100644 --- a/backend/src/routes/admin/bots.js +++ b/backend/src/routes/admin/bots.js @@ -1,6 +1,7 @@ import bots from '../../config/bots.js'; import { errorResponse } from '../../schemas/index.js'; import { syncAllSchedules } from '../../services/meilisearch/index.js'; +import { badRequest, notFound, serverError } from '../../utils/error.js'; // 봇 관련 스키마 const botResponse = { @@ -113,7 +114,7 @@ export default async function botsRoutes(fastify) { await scheduler.startBot(id); return { success: true, message: '봇이 시작되었습니다.' }; } catch (err) { - return reply.code(400).send({ error: err.message }); + return badRequest(reply, err.message); } }); @@ -147,7 +148,7 @@ export default async function botsRoutes(fastify) { await scheduler.stopBot(id); return { success: true, message: '봇이 정지되었습니다.' }; } catch (err) { - return reply.code(400).send({ error: err.message }); + return badRequest(reply, err.message); } }); @@ -182,7 +183,7 @@ export default async function botsRoutes(fastify) { const bot = bots.find(b => b.id === id); if (!bot) { - return reply.code(404).send({ error: '봇을 찾을 수 없습니다.' }); + return notFound(reply, '봇을 찾을 수 없습니다.'); } try { @@ -195,7 +196,7 @@ export default async function botsRoutes(fastify) { const count = await syncAllSchedules(fastify.meilisearch, fastify.db); result = { addedCount: count, total: count }; } else { - return reply.code(400).send({ error: '지원하지 않는 봇 타입입니다.' }); + return badRequest(reply, '지원하지 않는 봇 타입입니다.'); } // 상태 업데이트 @@ -215,7 +216,7 @@ export default async function botsRoutes(fastify) { }; } catch (err) { fastify.log.error(`[${id}] 전체 동기화 오류:`, err); - return reply.code(500).send({ error: err.message }); + return serverError(reply, err.message); } }); diff --git a/backend/src/routes/admin/x.js b/backend/src/routes/admin/x.js index d993e09..5ed88b7 100644 --- a/backend/src/routes/admin/x.js +++ b/backend/src/routes/admin/x.js @@ -7,6 +7,7 @@ import { xPostInfoQuery, xScheduleCreate, } from '../../schemas/index.js'; +import { badRequest, conflict, serverError } from '../../utils/error.js'; const X_CATEGORY_ID = CATEGORY_IDS.X; const NITTER_URL = config.nitter?.url || process.env.NITTER_URL || 'http://nitter:8080'; @@ -61,7 +62,7 @@ export default async function xRoutes(fastify) { // 게시글 ID 유효성 검사 if (!/^\d+$/.test(postId)) { - return reply.code(400).send({ error: '유효하지 않은 게시글 ID입니다.' }); + return badRequest(reply, '유효하지 않은 게시글 ID입니다.'); } try { @@ -80,7 +81,7 @@ export default async function xRoutes(fastify) { }; } catch (err) { fastify.log.error(`X 게시글 조회 오류: ${err.message}`); - return reply.code(500).send({ error: err.message }); + return serverError(reply, err.message); } }); @@ -118,7 +119,7 @@ export default async function xRoutes(fastify) { [postId] ); if (existing.length > 0) { - return reply.code(409).send({ error: '이미 등록된 게시글입니다.' }); + return conflict(reply, '이미 등록된 게시글입니다.'); } // schedules 테이블에 저장 @@ -155,7 +156,7 @@ export default async function xRoutes(fastify) { return { success: true, scheduleId }; } catch (err) { fastify.log.error(`X 일정 저장 오류: ${err.message}`); - return reply.code(500).send({ error: err.message }); + return serverError(reply, err.message); } }); } diff --git a/backend/src/routes/admin/youtube.js b/backend/src/routes/admin/youtube.js index 2cc80f1..4db83ff 100644 --- a/backend/src/routes/admin/youtube.js +++ b/backend/src/routes/admin/youtube.js @@ -8,6 +8,7 @@ import { youtubeScheduleUpdate, idParam, } from '../../schemas/index.js'; +import { badRequest, notFound, conflict, serverError } from '../../utils/error.js'; const YOUTUBE_CATEGORY_ID = CATEGORY_IDS.YOUTUBE; @@ -54,13 +55,13 @@ export default async function youtubeRoutes(fastify) { // YouTube URL에서 video ID 추출 const videoId = extractVideoId(url); if (!videoId) { - return reply.code(400).send({ error: '유효하지 않은 YouTube URL입니다.' }); + return badRequest(reply, '유효하지 않은 YouTube URL입니다.'); } try { const video = await fetchVideoInfo(videoId); if (!video) { - return reply.code(404).send({ error: '영상을 찾을 수 없습니다.' }); + return notFound(reply, '영상을 찾을 수 없습니다.'); } return { @@ -76,7 +77,7 @@ export default async function youtubeRoutes(fastify) { }; } catch (err) { fastify.log.error(`YouTube 영상 조회 오류: ${err.message}`); - return reply.code(500).send({ error: err.message }); + return serverError(reply, err.message); } }); @@ -114,7 +115,7 @@ export default async function youtubeRoutes(fastify) { [videoId] ); if (existing.length > 0) { - return reply.code(409).send({ error: '이미 등록된 영상입니다.' }); + return conflict(reply, '이미 등록된 영상입니다.'); } // schedules 테이블에 저장 @@ -151,7 +152,7 @@ export default async function youtubeRoutes(fastify) { return { success: true, scheduleId }; } catch (err) { fastify.log.error(`YouTube 일정 저장 오류: ${err.message}`); - return reply.code(500).send({ error: err.message }); + return serverError(reply, err.message); } }); @@ -190,7 +191,7 @@ export default async function youtubeRoutes(fastify) { [id, YOUTUBE_CATEGORY_ID] ); if (schedules.length === 0) { - return reply.code(404).send({ error: 'YouTube 일정을 찾을 수 없습니다.' }); + return notFound(reply, 'YouTube 일정을 찾을 수 없습니다.'); } // 영상 유형 수정 @@ -254,7 +255,7 @@ export default async function youtubeRoutes(fastify) { return { success: true }; } catch (err) { fastify.log.error(`YouTube 일정 수정 오류: ${err.message}`); - return reply.code(500).send({ error: err.message }); + return serverError(reply, err.message); } }); } diff --git a/backend/src/routes/albums/index.js b/backend/src/routes/albums/index.js index 0616918..9280f6e 100644 --- a/backend/src/routes/albums/index.js +++ b/backend/src/routes/albums/index.js @@ -11,6 +11,7 @@ import { import photosRoutes from './photos.js'; import teasersRoutes from './teasers.js'; import { errorResponse, successResponse, idParam } from '../../schemas/index.js'; +import { notFound, badRequest } from '../../utils/error.js'; /** * 앨범 라우트 @@ -67,7 +68,7 @@ export default async function albumsRoutes(fastify) { const album = await getAlbumByName(db, albumName); if (!album) { - return reply.code(404).send({ error: '앨범을 찾을 수 없습니다.' }); + return notFound(reply, '앨범을 찾을 수 없습니다.'); } const [tracks] = await db.query( @@ -76,7 +77,7 @@ export default async function albumsRoutes(fastify) { ); if (tracks.length === 0) { - return reply.code(404).send({ error: '트랙을 찾을 수 없습니다.' }); + return notFound(reply, '트랙을 찾을 수 없습니다.'); } const track = tracks[0]; @@ -123,7 +124,7 @@ export default async function albumsRoutes(fastify) { }, async (request, reply) => { const album = await getAlbumByName(db, decodeURIComponent(request.params.name)); if (!album) { - return reply.code(404).send({ error: '앨범을 찾을 수 없습니다.' }); + return notFound(reply, '앨범을 찾을 수 없습니다.'); } return getAlbumDetails(db, album, redis); }); @@ -144,7 +145,7 @@ export default async function albumsRoutes(fastify) { }, async (request, reply) => { const album = await getAlbumById(db, request.params.id); if (!album) { - return reply.code(404).send({ error: '앨범을 찾을 수 없습니다.' }); + return notFound(reply, '앨범을 찾을 수 없습니다.'); } return getAlbumDetails(db, album, redis); }); @@ -187,13 +188,13 @@ export default async function albumsRoutes(fastify) { } if (!data) { - return reply.code(400).send({ error: '데이터가 필요합니다.' }); + return badRequest(reply, '데이터가 필요합니다.'); } const { title, album_type, release_date, folder_name } = data; if (!title || !album_type || !release_date || !folder_name) { - return reply.code(400).send({ error: '필수 필드를 모두 입력해주세요.' }); + return badRequest(reply, '필수 필드를 모두 입력해주세요.'); } const result = await createAlbum(db, data, coverBuffer); @@ -234,12 +235,12 @@ export default async function albumsRoutes(fastify) { } if (!data) { - return reply.code(400).send({ error: '데이터가 필요합니다.' }); + return badRequest(reply, '데이터가 필요합니다.'); } const result = await updateAlbum(db, id, data, coverBuffer); if (!result) { - return reply.code(404).send({ error: '앨범을 찾을 수 없습니다.' }); + return notFound(reply, '앨범을 찾을 수 없습니다.'); } await invalidateAlbumCache(redis, id); return result; @@ -265,7 +266,7 @@ export default async function albumsRoutes(fastify) { const { id } = request.params; const result = await deleteAlbum(db, id); if (!result) { - return reply.code(404).send({ error: '앨범을 찾을 수 없습니다.' }); + return notFound(reply, '앨범을 찾을 수 없습니다.'); } await invalidateAlbumCache(redis, id); return result; diff --git a/backend/src/routes/albums/photos.js b/backend/src/routes/albums/photos.js index 4238f8d..11ce1c3 100644 --- a/backend/src/routes/albums/photos.js +++ b/backend/src/routes/albums/photos.js @@ -4,6 +4,7 @@ import { uploadAlbumVideo, } from '../../services/image.js'; import { withTransaction } from '../../utils/transaction.js'; +import { notFound } from '../../utils/error.js'; /** * 앨범 사진 라우트 @@ -25,7 +26,7 @@ export default async function photosRoutes(fastify) { const [albums] = await db.query('SELECT folder_name FROM albums WHERE id = ?', [albumId]); if (albums.length === 0) { - return reply.code(404).send({ error: '앨범을 찾을 수 없습니다.' }); + return notFound(reply, '앨범을 찾을 수 없습니다.'); } const [photos] = await db.query( @@ -227,7 +228,7 @@ export default async function photosRoutes(fastify) { ); if (photos.length === 0) { - return reply.code(404).send({ error: '사진을 찾을 수 없습니다.' }); + return notFound(reply, '사진을 찾을 수 없습니다.'); } const photo = photos[0]; diff --git a/backend/src/routes/albums/teasers.js b/backend/src/routes/albums/teasers.js index 6bd5f87..28a8e89 100644 --- a/backend/src/routes/albums/teasers.js +++ b/backend/src/routes/albums/teasers.js @@ -3,6 +3,7 @@ import { deleteAlbumVideo, } from '../../services/image.js'; import { withTransaction } from '../../utils/transaction.js'; +import { notFound } from '../../utils/error.js'; /** * 앨범 티저 라우트 @@ -24,7 +25,7 @@ export default async function teasersRoutes(fastify) { const [albums] = await db.query('SELECT folder_name FROM albums WHERE id = ?', [albumId]); if (albums.length === 0) { - return reply.code(404).send({ error: '앨범을 찾을 수 없습니다.' }); + return notFound(reply, '앨범을 찾을 수 없습니다.'); } const [teasers] = await db.query( @@ -61,7 +62,7 @@ export default async function teasersRoutes(fastify) { ); if (teasers.length === 0) { - return reply.code(404).send({ error: '티저를 찾을 수 없습니다.' }); + return notFound(reply, '티저를 찾을 수 없습니다.'); } const teaser = teasers[0]; diff --git a/backend/src/routes/auth.js b/backend/src/routes/auth.js index 2226f3b..8078868 100644 --- a/backend/src/routes/auth.js +++ b/backend/src/routes/auth.js @@ -1,4 +1,5 @@ import bcrypt from 'bcrypt'; +import { badRequest, unauthorized, serverError } from '../utils/error.js'; /** * 인증 라우트 @@ -42,7 +43,7 @@ export default async function authRoutes(fastify, opts) { const { username, password } = request.body || {}; if (!username || !password) { - return reply.code(400).send({ error: '아이디와 비밀번호를 입력해주세요.' }); + return badRequest(reply, '아이디와 비밀번호를 입력해주세요.'); } try { @@ -52,14 +53,14 @@ export default async function authRoutes(fastify, opts) { ); if (users.length === 0) { - return reply.code(401).send({ error: '아이디 또는 비밀번호가 올바르지 않습니다.' }); + return unauthorized(reply, '아이디 또는 비밀번호가 올바르지 않습니다.'); } const user = users[0]; const isValidPassword = await bcrypt.compare(password, user.password_hash); if (!isValidPassword) { - return reply.code(401).send({ error: '아이디 또는 비밀번호가 올바르지 않습니다.' }); + return unauthorized(reply, '아이디 또는 비밀번호가 올바르지 않습니다.'); } // JWT 토큰 생성 @@ -75,7 +76,7 @@ export default async function authRoutes(fastify, opts) { }; } catch (err) { fastify.log.error(err); - return reply.code(500).send({ error: '로그인 처리 중 오류가 발생했습니다.' }); + return serverError(reply, '로그인 처리 중 오류가 발생했습니다.'); } }); diff --git a/backend/src/routes/members/index.js b/backend/src/routes/members/index.js index 0856bfb..7dfec00 100644 --- a/backend/src/routes/members/index.js +++ b/backend/src/routes/members/index.js @@ -1,5 +1,6 @@ import { uploadMemberImage } from '../../services/image.js'; import { getAllMembers, getMemberByName, getMemberBasicByName, invalidateMemberCache } from '../../services/member.js'; +import { notFound, serverError } from '../../utils/error.js'; /** * 멤버 라우트 @@ -22,7 +23,7 @@ export default async function membersRoutes(fastify, opts) { return await getAllMembers(db, redis); } catch (err) { fastify.log.error(err); - return reply.code(500).send({ error: '멤버 목록 조회 실패' }); + return serverError(reply, '멤버 목록 조회 실패'); } }); @@ -45,12 +46,12 @@ export default async function membersRoutes(fastify, opts) { try { const member = await getMemberByName(db, decodeURIComponent(request.params.name)); if (!member) { - return reply.code(404).send({ error: '멤버를 찾을 수 없습니다' }); + return notFound(reply, '멤버를 찾을 수 없습니다'); } return member; } catch (err) { fastify.log.error(err); - return reply.code(500).send({ error: '멤버 조회 실패' }); + return serverError(reply, '멤버 조회 실패'); } }); @@ -80,7 +81,7 @@ export default async function membersRoutes(fastify, opts) { // 기존 멤버 조회 const existing = await getMemberBasicByName(db, decodedName); if (!existing) { - return reply.code(404).send({ error: '멤버를 찾을 수 없습니다' }); + return notFound(reply, '멤버를 찾을 수 없습니다'); } const memberId = existing.id; @@ -161,7 +162,7 @@ export default async function membersRoutes(fastify, opts) { return { message: '멤버 정보가 수정되었습니다', id: memberId }; } catch (err) { fastify.log.error(err); - return reply.code(500).send({ error: '멤버 수정 실패: ' + err.message }); + return serverError(reply, '멤버 수정 실패: ' + err.message); } }); } diff --git a/backend/src/routes/schedules/index.js b/backend/src/routes/schedules/index.js index cfa5487..f499f74 100644 --- a/backend/src/routes/schedules/index.js +++ b/backend/src/routes/schedules/index.js @@ -17,6 +17,7 @@ import { scheduleSearchResponse, idParam, } from '../../schemas/index.js'; +import { badRequest, notFound, serverError } from '../../utils/error.js'; export default async function schedulesRoutes(fastify) { const { db, meilisearch, redis } = fastify; @@ -42,7 +43,7 @@ export default async function schedulesRoutes(fastify) { return await getCategories(db, redis); } catch (err) { fastify.log.error(err); - return reply.code(500).send({ error: '카테고리 목록 조회 실패' }); + return serverError(reply, '카테고리 목록 조회 실패'); } }); @@ -82,13 +83,13 @@ export default async function schedulesRoutes(fastify) { // 월별 조회 모드 if (!year || !month) { - return reply.code(400).send({ error: 'search, startDate, 또는 year/month는 필수입니다.' }); + return badRequest(reply, 'search, startDate, 또는 year/month는 필수입니다.'); } return await getMonthlySchedules(db, parseInt(year), parseInt(month)); } catch (err) { fastify.log.error(err); - return reply.code(500).send({ error: '일정 조회 실패' }); + return serverError(reply, '일정 조회 실패'); } }); @@ -119,7 +120,7 @@ export default async function schedulesRoutes(fastify) { return { success: true, synced: count }; } catch (err) { fastify.log.error(err); - return reply.code(500).send({ error: '동기화 실패' }); + return serverError(reply, '동기화 실패'); } }); @@ -146,13 +147,13 @@ export default async function schedulesRoutes(fastify) { ); if (!result) { - return reply.code(404).send({ error: '일정을 찾을 수 없습니다.' }); + return notFound(reply, '일정을 찾을 수 없습니다.'); } return result; } catch (err) { fastify.log.error(err); - return reply.code(500).send({ error: '일정 상세 조회 실패' }); + return serverError(reply, '일정 상세 조회 실패'); } }); @@ -185,7 +186,7 @@ export default async function schedulesRoutes(fastify) { // 일정 존재 확인 const [existing] = await db.query('SELECT id FROM schedules WHERE id = ?', [id]); if (existing.length === 0) { - return reply.code(404).send({ error: '일정을 찾을 수 없습니다.' }); + return notFound(reply, '일정을 찾을 수 없습니다.'); } // 관련 테이블 삭제 (외래 키) @@ -208,7 +209,7 @@ export default async function schedulesRoutes(fastify) { return { success: true }; } catch (err) { fastify.log.error(err); - return reply.code(500).send({ error: '일정 삭제 실패' }); + return serverError(reply, '일정 삭제 실패'); } }); } diff --git a/backend/src/routes/schedules/suggestions.js b/backend/src/routes/schedules/suggestions.js index 2a089c3..1c9f00d 100644 --- a/backend/src/routes/schedules/suggestions.js +++ b/backend/src/routes/schedules/suggestions.js @@ -4,6 +4,7 @@ import { readFileSync, writeFileSync } from 'fs'; import { SuggestionService } from '../../services/suggestions/index.js'; import { reloadMorpheme, getUserDictPath } from '../../services/suggestions/morpheme.js'; +import { badRequest, serverError } from '../../utils/error.js'; let suggestionService = null; @@ -109,7 +110,7 @@ export default async function suggestionsRoutes(fastify) { const { query } = request.body; if (!query || query.trim().length === 0) { - return reply.code(400).send({ error: '검색어가 필요합니다.' }); + return badRequest(reply, '검색어가 필요합니다.'); } await suggestionService.saveSearchQuery(query); @@ -187,7 +188,7 @@ export default async function suggestionsRoutes(fastify) { return { message: '사전이 저장되었습니다.' }; } catch (error) { fastify.log.error(`[Suggestions] 사전 저장 오류: ${error.message}`); - return reply.code(500).send({ error: '사전 저장 중 오류가 발생했습니다.' }); + return serverError(reply, '사전 저장 중 오류가 발생했습니다.'); } }); } diff --git a/backend/src/routes/stats/index.js b/backend/src/routes/stats/index.js index 318ceaa..e6d840a 100644 --- a/backend/src/routes/stats/index.js +++ b/backend/src/routes/stats/index.js @@ -2,6 +2,8 @@ * 통계 라우트 * 인증 필요 */ +import { serverError } from '../../utils/error.js'; + export default async function statsRoutes(fastify, opts) { const { db } = fastify; @@ -70,7 +72,7 @@ export default async function statsRoutes(fastify, opts) { }; } catch (err) { fastify.log.error(err); - return reply.code(500).send({ error: '통계 조회 실패' }); + return serverError(reply, '통계 조회 실패'); } }); } diff --git a/docs/code-review.md b/docs/code-review.md index 7ed12c6..0d664d2 100644 --- a/docs/code-review.md +++ b/docs/code-review.md @@ -25,20 +25,12 @@ ## 개선 필요 사항 -### 1. 보안 (우선순위: 높음) +### ~~1. 보안 (우선순위: 높음)~~ ✅ 완료 -#### 1.1 JWT Secret 하드코딩 -**파일**: `backend/src/config/index.js:37` +#### ~~1.1 JWT Secret 하드코딩~~ ✅ 완료 +**파일**: `backend/src/config/index.js` -```javascript -// 현재 - 기본값 노출 위험 -secret: process.env.JWT_SECRET || 'fromis9-admin-secret-key-2026', - -// 권장 - 환경변수 필수화 -secret: process.env.JWT_SECRET, -``` - -서버 시작 시 JWT_SECRET 환경변수 존재 여부를 검증하는 로직 추가 필요. +기본값을 제거하고 서버 시작 시 필수 환경변수 검증 로직을 추가함. ### 2. Docker 설정 (우선순위: 중간) @@ -69,17 +61,9 @@ meilisearch: ### 3. 백엔드 코드 (우선순위: 중간) -#### 3.1 에러 유틸리티 미활용 -`src/utils/error.js`에 에러 헬퍼가 있지만 라우트에서 직접 처리. - -```javascript -// 현재 - routes/albums/index.js -return reply.code(404).send({ error: '앨범을 찾을 수 없습니다.' }); - -// 권장 -import { notFound } from '../../utils/error.js'; -return notFound(reply, '앨범을 찾을 수 없습니다.'); -``` +#### ~~3.1 에러 유틸리티 미활용~~ ✅ 완료 +모든 라우트 파일에 에러 유틸리티(`utils/error.js`) 적용 완료. +- `badRequest`, `unauthorized`, `notFound`, `conflict`, `serverError` 함수 사용 #### 3.2 SELECT * 사용 **파일**: `services/album.js:16-19` @@ -92,24 +76,10 @@ return notFound(reply, '앨범을 찾을 수 없습니다.'); 'SELECT id, title, folder_name, album_type, album_type_short, release_date, cover_original_url, cover_medium_url, cover_thumb_url, description FROM albums WHERE folder_name = ? OR title = ?' ``` -#### 3.3 앨범 삭제 시 관련 데이터 누락 -**파일**: `services/album.js:301-312` +#### ~~3.3 앨범 삭제 시 관련 데이터 누락~~ ✅ 완료 +**파일**: `services/album.js` -```javascript -// 현재 -await connection.query('DELETE FROM album_tracks WHERE album_id = ?', [id]); -await connection.query('DELETE FROM albums WHERE id = ?', [id]); - -// 수정 필요 - 관련 테이블 모두 삭제 -await connection.query( - 'DELETE FROM album_photo_members WHERE photo_id IN (SELECT id FROM album_photos WHERE album_id = ?)', - [id] -); -await connection.query('DELETE FROM album_photos WHERE album_id = ?', [id]); -await connection.query('DELETE FROM album_teasers WHERE album_id = ?', [id]); -await connection.query('DELETE FROM album_tracks WHERE album_id = ?', [id]); -await connection.query('DELETE FROM albums WHERE id = ?', [id]); -``` +앨범 삭제 시 관련 테이블(photos, teasers, photo_members) 및 S3 파일도 함께 삭제하도록 개선됨. ### 4. 프론트엔드 코드 (우선순위: 낮음) @@ -175,15 +145,15 @@ export const authDel = authApi.del; ## 개선 우선순위 요약 -| 순위 | 항목 | 파일 | 난이도 | +| 순위 | 항목 | 파일 | 상태 | |:----:|------|------|:------:| -| 1 | JWT Secret 기본값 제거 | `config/index.js` | 낮음 | -| 2 | 앨범 삭제 시 관련 테이블 정리 | `services/album.js` | 중간 | -| 3 | 에러 유틸리티 통일 | 라우트 파일들 | 중간 | -| 4 | App.jsx 라우트 분리 | `App.jsx` | 중간 | -| 5 | 레거시 export 정리 | `api/client.js` | 낮음 | +| 1 | ~~JWT Secret 기본값 제거~~ | `config/index.js` | ✅ 완료 | +| 2 | ~~앨범 삭제 시 관련 테이블 정리~~ | `services/album.js` | ✅ 완료 | +| 3 | ~~에러 유틸리티 통일~~ | 라우트 파일들 | ✅ 완료 | +| 4 | App.jsx 라우트 분리 | `App.jsx` | 미완료 | +| 5 | 레거시 export 정리 | `api/client.js` | 미완료 | | 6 | ~~Meilisearch 버전 업데이트~~ | `docker-compose.yml` | ✅ 완료 | -| 7 | 테스트 코드 작성 | 전체 | 높음 | +| 7 | 테스트 코드 작성 | 전체 | 미완료 | ---