From 5cc258b0099599d132fe8d0037c753303c979f4f Mon Sep 17 00:00:00 2001 From: caadiq Date: Wed, 21 Jan 2026 15:56:10 +0900 Subject: [PATCH] =?UTF-8?q?refactor(backend):=2016=EB=8B=A8=EA=B3=84=20?= =?UTF-8?q?=EC=97=90=EB=9F=AC=20=EC=B2=98=EB=A6=AC=20=EC=9D=BC=EA=B4=80?= =?UTF-8?q?=EC=84=B1=20-=20schedules=20=EB=9D=BC=EC=9A=B0=ED=8A=B8=20try/c?= =?UTF-8?q?atch=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 모든 핸들러에 try/catch 블록 적용: - GET /categories - GET / (검색/월별/다가오는 일정) - POST /sync-search - GET /:id - DELETE /:id Co-Authored-By: Claude Opus 4.5 --- backend/src/routes/schedules/index.js | 277 ++++++++++++++------------ docs/refactoring.md | 63 ++++++ 2 files changed, 214 insertions(+), 126 deletions(-) diff --git a/backend/src/routes/schedules/index.js b/backend/src/routes/schedules/index.js index e21889a..ece2f73 100644 --- a/backend/src/routes/schedules/index.js +++ b/backend/src/routes/schedules/index.js @@ -33,10 +33,15 @@ export default async function schedulesRoutes(fastify) { }, }, }, async (request, reply) => { - const [categories] = await db.query( - 'SELECT id, name, color, sort_order FROM schedule_categories ORDER BY sort_order ASC, id ASC' - ); - return categories; + try { + const [categories] = await db.query( + 'SELECT id, name, color, sort_order FROM schedule_categories ORDER BY sort_order ASC, id ASC' + ); + return categories; + } catch (err) { + fastify.log.error(err); + return reply.code(500).send({ error: '카테고리 목록 조회 실패' }); + } }); /** @@ -55,24 +60,29 @@ export default async function schedulesRoutes(fastify) { }, }, }, async (request, reply) => { - const { search, year, month, startDate, offset = 0, limit = 100 } = request.query; + try { + const { search, year, month, startDate, offset = 0, limit = 100 } = request.query; - // 검색 모드 - if (search && search.trim()) { - return await handleSearch(fastify, search.trim(), parseInt(offset), parseInt(limit)); + // 검색 모드 + if (search && search.trim()) { + return await handleSearch(fastify, search.trim(), parseInt(offset), parseInt(limit)); + } + + // 다가오는 일정 조회 (startDate부터) + if (startDate) { + return await getUpcomingSchedules(db, startDate, parseInt(limit)); + } + + // 월별 조회 모드 + if (!year || !month) { + return reply.code(400).send({ error: 'search, startDate, 또는 year/month는 필수입니다.' }); + } + + return await getMonthlySchedules(db, parseInt(year), parseInt(month)); + } catch (err) { + fastify.log.error(err); + return reply.code(500).send({ error: '일정 조회 실패' }); } - - // 다가오는 일정 조회 (startDate부터) - if (startDate) { - return await getUpcomingSchedules(db, startDate, parseInt(limit)); - } - - // 월별 조회 모드 - if (!year || !month) { - return reply.code(400).send({ error: 'search, startDate, 또는 year/month는 필수입니다.' }); - } - - return await getMonthlySchedules(db, parseInt(year), parseInt(month)); }); /** @@ -97,8 +107,13 @@ export default async function schedulesRoutes(fastify) { }, preHandler: [fastify.authenticate], }, async (request, reply) => { - const count = await syncAllSchedules(meilisearch, db); - return { success: true, synced: count }; + try { + const count = await syncAllSchedules(meilisearch, db); + return { success: true, synced: count }; + } catch (err) { + fastify.log.error(err); + return reply.code(500).send({ error: '동기화 실패' }); + } }); /** @@ -116,90 +131,95 @@ export default async function schedulesRoutes(fastify) { }, }, }, async (request, reply) => { - const { id } = request.params; + try { + const { id } = request.params; - const [schedules] = await db.query(` - SELECT - s.*, - c.name as category_name, - c.color as category_color, - sy.channel_name as youtube_channel, - sy.video_id as youtube_video_id, - sy.video_type as youtube_video_type, - sx.post_id as x_post_id, - sx.content as x_content, - sx.image_urls as x_image_urls - FROM schedules s - LEFT JOIN schedule_categories c ON s.category_id = c.id - LEFT JOIN schedule_youtube sy ON s.id = sy.schedule_id - LEFT JOIN schedule_x sx ON s.id = sx.schedule_id - WHERE s.id = ? - `, [id]); + const [schedules] = await db.query(` + SELECT + s.*, + c.name as category_name, + c.color as category_color, + sy.channel_name as youtube_channel, + sy.video_id as youtube_video_id, + sy.video_type as youtube_video_type, + sx.post_id as x_post_id, + sx.content as x_content, + sx.image_urls as x_image_urls + FROM schedules s + LEFT JOIN schedule_categories c ON s.category_id = c.id + LEFT JOIN schedule_youtube sy ON s.id = sy.schedule_id + LEFT JOIN schedule_x sx ON s.id = sx.schedule_id + WHERE s.id = ? + `, [id]); - if (schedules.length === 0) { - return reply.code(404).send({ error: '일정을 찾을 수 없습니다.' }); - } - - const s = schedules[0]; - - // 멤버 정보 조회 - const [members] = await db.query(` - SELECT m.id, m.name - FROM schedule_members sm - JOIN members m ON sm.member_id = m.id - WHERE sm.schedule_id = ? - ORDER BY m.id - `, [id]); - - // datetime 생성 (date + time) - const dateStr = s.date instanceof Date ? s.date.toISOString().split('T')[0] : s.date?.split('T')[0]; - const timeStr = s.time ? s.time.slice(0, 5) : null; - const datetime = timeStr ? `${dateStr} ${timeStr}` : dateStr; - - // 공통 필드 - const result = { - id: s.id, - title: s.title, - datetime, - category: { - id: s.category_id, - name: s.category_name, - color: s.category_color, - }, - members, - createdAt: s.created_at, - updatedAt: s.updated_at, - }; - - // 카테고리별 추가 필드 - if (s.category_id === CATEGORY_IDS.YOUTUBE && s.youtube_video_id) { - // YouTube - result.videoId = s.youtube_video_id; - result.videoType = s.youtube_video_type; - result.channelName = s.youtube_channel; - result.videoUrl = s.youtube_video_type === 'shorts' - ? `https://www.youtube.com/shorts/${s.youtube_video_id}` - : `https://www.youtube.com/watch?v=${s.youtube_video_id}`; - } else if (s.category_id === CATEGORY_IDS.X && s.x_post_id) { - // X (Twitter) - const username = config.x.defaultUsername; - result.postId = s.x_post_id; - result.content = s.x_content || null; - result.imageUrls = s.x_image_urls ? JSON.parse(s.x_image_urls) : []; - result.postUrl = `https://x.com/${username}/status/${s.x_post_id}`; - - // 프로필 정보 (Redis 캐시 → DB) - const profile = await fastify.xBot.getProfile(username); - if (profile) { - result.profile = { - username: profile.username, - displayName: profile.displayName, - avatarUrl: profile.avatarUrl, - }; + if (schedules.length === 0) { + return reply.code(404).send({ error: '일정을 찾을 수 없습니다.' }); } - } - return result; + const s = schedules[0]; + + // 멤버 정보 조회 + const [members] = await db.query(` + SELECT m.id, m.name + FROM schedule_members sm + JOIN members m ON sm.member_id = m.id + WHERE sm.schedule_id = ? + ORDER BY m.id + `, [id]); + + // datetime 생성 (date + time) + const dateStr = s.date instanceof Date ? s.date.toISOString().split('T')[0] : s.date?.split('T')[0]; + const timeStr = s.time ? s.time.slice(0, 5) : null; + const datetime = timeStr ? `${dateStr} ${timeStr}` : dateStr; + + // 공통 필드 + const result = { + id: s.id, + title: s.title, + datetime, + category: { + id: s.category_id, + name: s.category_name, + color: s.category_color, + }, + members, + createdAt: s.created_at, + updatedAt: s.updated_at, + }; + + // 카테고리별 추가 필드 + if (s.category_id === CATEGORY_IDS.YOUTUBE && s.youtube_video_id) { + // YouTube + result.videoId = s.youtube_video_id; + result.videoType = s.youtube_video_type; + result.channelName = s.youtube_channel; + result.videoUrl = s.youtube_video_type === 'shorts' + ? `https://www.youtube.com/shorts/${s.youtube_video_id}` + : `https://www.youtube.com/watch?v=${s.youtube_video_id}`; + } else if (s.category_id === CATEGORY_IDS.X && s.x_post_id) { + // X (Twitter) + const username = config.x.defaultUsername; + result.postId = s.x_post_id; + result.content = s.x_content || null; + result.imageUrls = s.x_image_urls ? JSON.parse(s.x_image_urls) : []; + result.postUrl = `https://x.com/${username}/status/${s.x_post_id}`; + + // 프로필 정보 (Redis 캐시 → DB) + const profile = await fastify.xBot.getProfile(username); + if (profile) { + result.profile = { + username: profile.username, + displayName: profile.displayName, + avatarUrl: profile.avatarUrl, + }; + } + } + + return result; + } catch (err) { + fastify.log.error(err); + return reply.code(500).send({ error: '일정 상세 조회 실패' }); + } }); /** @@ -225,32 +245,37 @@ export default async function schedulesRoutes(fastify) { }, preHandler: [fastify.authenticate], }, async (request, reply) => { - const { id } = request.params; - - // 일정 존재 확인 - const [existing] = await db.query('SELECT id FROM schedules WHERE id = ?', [id]); - if (existing.length === 0) { - return reply.code(404).send({ error: '일정을 찾을 수 없습니다.' }); - } - - // 관련 테이블 삭제 (외래 키) - await db.query('DELETE FROM schedule_youtube WHERE schedule_id = ?', [id]); - await db.query('DELETE FROM schedule_x WHERE schedule_id = ?', [id]); - await db.query('DELETE FROM schedule_members WHERE schedule_id = ?', [id]); - await db.query('DELETE FROM schedule_images WHERE schedule_id = ?', [id]); - - // 메인 테이블 삭제 - await db.query('DELETE FROM schedules WHERE id = ?', [id]); - - // Meilisearch에서도 삭제 try { - const { deleteSchedule } = await import('../../services/meilisearch/index.js'); - await deleteSchedule(meilisearch, id); - } catch (err) { - fastify.log.error(`Meilisearch 삭제 오류: ${err.message}`); - } + const { id } = request.params; - return { success: true }; + // 일정 존재 확인 + const [existing] = await db.query('SELECT id FROM schedules WHERE id = ?', [id]); + if (existing.length === 0) { + return reply.code(404).send({ error: '일정을 찾을 수 없습니다.' }); + } + + // 관련 테이블 삭제 (외래 키) + await db.query('DELETE FROM schedule_youtube WHERE schedule_id = ?', [id]); + await db.query('DELETE FROM schedule_x WHERE schedule_id = ?', [id]); + await db.query('DELETE FROM schedule_members WHERE schedule_id = ?', [id]); + await db.query('DELETE FROM schedule_images WHERE schedule_id = ?', [id]); + + // 메인 테이블 삭제 + await db.query('DELETE FROM schedules WHERE id = ?', [id]); + + // Meilisearch에서도 삭제 + try { + const { deleteSchedule } = await import('../../services/meilisearch/index.js'); + await deleteSchedule(meilisearch, id); + } catch (meiliErr) { + fastify.log.error(`Meilisearch 삭제 오류: ${meiliErr.message}`); + } + + return { success: true }; + } catch (err) { + fastify.log.error(err); + return reply.code(500).send({ error: '일정 삭제 실패' }); + } }); } diff --git a/docs/refactoring.md b/docs/refactoring.md index c535680..d17ebe0 100644 --- a/docs/refactoring.md +++ b/docs/refactoring.md @@ -163,6 +163,63 @@ --- +### 16단계: 에러 처리 일관성 ✅ 완료 +- [x] 모든 라우트에 try/catch 적용 +- [x] 에러 응답 패턴 통일 + +**수정된 파일:** +- `src/routes/schedules/index.js` - 모든 핸들러에 try/catch 추가 + +--- + +### 17단계: 중복 코드 제거 (멤버 조회) 🔄 진행 예정 +- [ ] 멤버 조회 로직을 서비스로 분리 +- [ ] 앨범 존재 확인 로직 통합 + +**대상 파일:** +- `src/services/member.js` - 신규 생성 +- `src/routes/members/index.js` - 서비스 호출로 변경 + +--- + +### 18단계: 이미지 처리 최적화 🔄 진행 예정 +- [ ] 이미지 메타데이터 중복 처리 제거 +- [ ] processImage에서 메타데이터 함께 반환 + +**대상 파일:** +- `src/services/image.js` - processImage 함수 개선 + +--- + +### 19단계: Redis 캐시 확대 🔄 진행 예정 +- [ ] 일정 상세 조회 캐싱 +- [ ] 멤버 목록 캐싱 + +**대상 파일:** +- `src/routes/schedules/index.js` - 캐시 적용 +- `src/routes/members/index.js` - 캐시 적용 + +--- + +### 20단계: 서비스 레이어 확대 🔄 진행 예정 +- [ ] schedules 라우트의 DB 쿼리를 서비스로 분리 +- [ ] 일관된 서비스 패턴 적용 + +**대상 파일:** +- `src/services/schedule.js` - 함수 추가 +- `src/routes/schedules/index.js` - 서비스 호출로 변경 + +--- + +### 21단계: 검색 페이징 최적화 🔄 진행 예정 +- [ ] Meilisearch 네이티브 페이징 사용 +- [ ] 클라이언트 slice 제거 + +**대상 파일:** +- `src/services/meilisearch/index.js` - offset/limit 파라미터 전달 + +--- + ## 진행 상황 | 단계 | 작업 | 상태 | @@ -182,6 +239,12 @@ | 13단계 | Swagger/OpenAPI 문서화 | ✅ 완료 | | 14단계 | 입력 검증 강화 (JSON Schema) | ✅ 완료 | | 15단계 | 스키마 파일 분리 | ✅ 완료 | +| 16단계 | 에러 처리 일관성 | 🔄 진행 예정 | +| 17단계 | 중복 코드 제거 (멤버 조회) | 🔄 진행 예정 | +| 18단계 | 이미지 처리 최적화 | 🔄 진행 예정 | +| 19단계 | Redis 캐시 확대 | 🔄 진행 예정 | +| 20단계 | 서비스 레이어 확대 | 🔄 진행 예정 | +| 21단계 | 검색 페이징 최적화 | 🔄 진행 예정 | ---