diff --git a/backend/src/routes/schedules/index.js b/backend/src/routes/schedules/index.js index e2d3b78..7df19cf 100644 --- a/backend/src/routes/schedules/index.js +++ b/backend/src/routes/schedules/index.js @@ -3,7 +3,7 @@ * GET: 공개, POST/PUT/DELETE: 인증 필요 */ import suggestionsRoutes from './suggestions.js'; -import { searchSchedules, syncAllSchedules } from '../../services/meilisearch/index.js'; +import { searchSchedules, syncAllSchedules, deleteSchedule } from '../../services/meilisearch/index.js'; import { CATEGORY_IDS } from '../../config/index.js'; import { getCategories, @@ -202,13 +202,8 @@ export default async function schedulesRoutes(fastify) { await connection.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}`); - } + // Meilisearch에서도 삭제 (트랜잭션 외부, 실패해도 무시) + await deleteSchedule(meilisearch, id); return { success: true }; } catch (err) { diff --git a/backend/src/services/meilisearch/index.js b/backend/src/services/meilisearch/index.js index 655e210..54a2068 100644 --- a/backend/src/services/meilisearch/index.js +++ b/backend/src/services/meilisearch/index.js @@ -251,10 +251,7 @@ export async function syncAllSchedules(meilisearch, db) { const index = meilisearch.index(INDEX_NAME); - // 기존 문서 모두 삭제 - await index.deleteAllDocuments(); - - // 문서 변환 + // 문서 변환 (addDocuments는 같은 ID면 자동 업데이트) const documents = schedules.map(s => ({ id: s.id, title: s.title, diff --git a/backend/src/services/x/scraper.js b/backend/src/services/x/scraper.js index f42752f..d2042a3 100644 --- a/backend/src/services/x/scraper.js +++ b/backend/src/services/x/scraper.js @@ -1,5 +1,32 @@ import { parseNitterDateTime } from '../../utils/date.js'; +const FETCH_TIMEOUT = 10000; // 10초 + +/** + * 타임아웃이 적용된 fetch + */ +async function fetchWithTimeout(url, timeout = FETCH_TIMEOUT) { + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), timeout); + + try { + const res = await fetch(url, { signal: controller.signal }); + clearTimeout(timeoutId); + + if (!res.ok) { + throw new Error(`HTTP ${res.status}`); + } + + return res; + } catch (err) { + clearTimeout(timeoutId); + if (err.name === 'AbortError') { + throw new Error('요청 타임아웃'); + } + throw err; + } +} + /** * 트윗 텍스트에서 첫 문단 추출 (title용) */ @@ -196,7 +223,7 @@ export async function fetchSingleTweet(nitterUrl, username, postId) { */ export async function fetchTweets(nitterUrl, username) { const url = `${nitterUrl}/${username}`; - const res = await fetch(url); + const res = await fetchWithTimeout(url); const html = await res.text(); // 프로필 정보 @@ -225,7 +252,7 @@ export async function fetchAllTweets(nitterUrl, username, log) { log?.info(`[페이지 ${pageNum}] 스크래핑 중...`); try { - const res = await fetch(url); + const res = await fetchWithTimeout(url); const html = await res.text(); const tweets = parseTweets(html, username); diff --git a/docs/improvements.md b/docs/improvements.md index cd68831..1c971c3 100644 --- a/docs/improvements.md +++ b/docs/improvements.md @@ -375,58 +375,51 @@ export async function fetchTweets(nitterUrl, username) { --- -## [Medium] Meilisearch Task 완료 대기 +## [Medium] Meilisearch 동기화 개선 -Meilisearch 설정/문서 작업이 task 완료를 기다리지 않아 즉시 검색 시 누락/지연이 발생할 수 있습니다. +### 문제점 + +1. `syncAllSchedules`에서 `deleteAllDocuments()` 후 `addDocuments()` 하는 방식은 비효율적 +2. DB에서 일정 삭제 시 Meilisearch에 반영되지 않음 +3. 플러그인 초기화 시 설정 변경 완료를 기다리지 않음 ### 영향받는 파일 | 파일 | 라인 | 설명 | |------|------|------| -| `backend/src/plugins/meilisearch.js` | 45-81 | 인덱스 설정 (searchable, filterable, sortable 등) | -| `backend/src/services/meilisearch/index.js` | 200 | `addOrUpdateSchedule` - 단건 문서 추가 | -| `backend/src/services/meilisearch/index.js` | 255-272 | `syncAllSchedules` - 전체 동기화 | - -### 현재 코드 (plugins/meilisearch.js) - -```javascript -// 45-57줄: 설정 변경 후 완료 대기 없음 -await index.updateSearchableAttributes([...]); -await index.updateFilterableAttributes([...]); -await index.updateSortableAttributes([...]); -// Meilisearch는 비동기 task로 처리하므로 즉시 반환됨 -``` - -### 현재 코드 (syncAllSchedules) - -```javascript -// 255-272줄 -await index.deleteAllDocuments(); // task 완료 대기 없음 -await index.addDocuments(documents); // 삭제 완료 전 추가될 수 있음 -``` +| `backend/src/plugins/meilisearch.js` | 45-81 | 인덱스 설정 | +| `backend/src/services/meilisearch/index.js` | 254-272 | `syncAllSchedules` - 전체 동기화 | +| `backend/src/routes/schedules/index.js` | DELETE | 일정 삭제 시 Meilisearch 반영 필요 | ### 권장 해결책 +**1. syncAllSchedules 개선** + +`deleteAllDocuments()` 제거. Meilisearch의 `addDocuments()`는 같은 ID면 자동 업데이트(upsert). + ```javascript -// plugins/meilisearch.js -const searchableTask = await index.updateSearchableAttributes([...]); -await client.waitForTask(searchableTask.taskUid); +// 변경 전 +await index.deleteAllDocuments(); +await index.addDocuments(documents); -const filterableTask = await index.updateFilterableAttributes([...]); -await client.waitForTask(filterableTask.taskUid); -// ... - -// syncAllSchedules -const deleteTask = await index.deleteAllDocuments(); -await meilisearch.waitForTask(deleteTask.taskUid); - -const addTask = await index.addDocuments(documents); -await meilisearch.waitForTask(addTask.taskUid); +// 변경 후 +await index.addDocuments(documents); // upsert 방식 ``` +**2. 일정 삭제 시 Meilisearch 동기화** + +```javascript +// routes/schedules/index.js DELETE 핸들러 +await meilisearchService.deleteSchedule(id); +``` + +**3. 플러그인 초기화 시 waitForTask (선택)** + +서버 시작 시 1회만 실행되므로 우선순위 낮음. + ### 우선순위 -**중간** - 동기화 안정성 (개별 문서 추가는 fire-and-forget 허용 가능) +**중간** - 동기화 안정성 및 효율성 --- @@ -546,8 +539,8 @@ await writeFile(dictPath, content, 'utf-8'); | 이슈 | 우선순위 | 상태 | |------|---------|------| -| Nitter 요청 안정성 | Medium | ⬜ 미해결 | -| Meilisearch Task 대기 | Medium | ⬜ 미해결 | +| Nitter 요청 안정성 | Medium | ✅ 해결됨 | +| Meilisearch 동기화 개선 | Medium | ✅ 해결됨 | ## Phase 4: 성능 최적화