From ac74dc74bd0d9ff75664a8b0e550ec625d5e09af Mon Sep 17 00:00:00 2001 From: Hanmh111 <96728777+Hanmh111@users.noreply.github.com> Date: Fri, 27 Jun 2025 12:05:06 +0900 Subject: [PATCH 1/3] =?UTF-8?q?Refactor:=20getOGImage=20=ED=95=A8=EC=88=98?= =?UTF-8?q?=EC=97=90=20=EC=BA=90=EC=8B=B1=20=EB=A1=9C=EC=A7=81=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=ED=95=98=EC=97=AC=20=EC=84=B1=EB=8A=A5=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- controllers/collectionController.js | 72 +++++++++++------------------ models/OGImageCache.js | 29 ++++++++++++ 2 files changed, 55 insertions(+), 46 deletions(-) create mode 100644 models/OGImageCache.js diff --git a/controllers/collectionController.js b/controllers/collectionController.js index 4f239d6..930b49d 100644 --- a/controllers/collectionController.js +++ b/controllers/collectionController.js @@ -4,6 +4,7 @@ import CollectionShare from "../models/CollectionShare.js"; import Reference from "../models/Reference.js"; import Keyword from "../models/Keyword.js"; import Extension from "../models/Extension.js"; +import OGImageCache from "../models/OGImageCache.js"; import {StatusCodes} from "http-status-codes"; import {deleteFileByUrl} from "../middlewares/fileDelete.js"; @@ -11,12 +12,32 @@ import {deletePreviewByUrl} from "../middlewares/previewDelete.js"; import {MongoError} from "mongodb"; import ogs from "open-graph-scraper"; + async function getOGImage(url) { + // 캐시에서 먼저 확인 + const cachedImage = await OGImageCache.findOne({url}); + + // 캐시된 이미지가 있고, 생성된 지 7일 이내라면 캐시된 이미지 반환 + if (cachedImage && Date.now() - cachedImage.createdAt.getTime() < 7 * 24 * 60 * 60 * 1000) { + return cachedImage.imageUrl; + } + try { const {result} = await ogs({url}); - return result.ogImage[0]?.url || null; + const ogImageUrl = result.ogImage?.[0]?.url || null; + + // 캐시 업데이트 또는 새로 생성 + await OGImageCache.findOneAndUpdate( + {url}, + {imageUrl: ogImageUrl, createdAt: new Date()}, // createdAt을 현재 시간으로 업데이트, TTL 갱신 + {upsert: true, new: true} // 없으면 생성, 있으면 업데이트 + ); + + return ogImageUrl; } catch (err) { - console.log(`OG Image fetch error (${url}:)`, err.message); + console.log(`OG Image fetch error (${url}):`, err.message); + // 에러 발생 시에도 캐시에 null 또는 에러 상태를 저장하여 불필요 요청 방지 + await OGImageCache.findOneAndUpdate({url}, {imageUrl: null, createdAt: new Date()}, {upsert: true, new: true}); return null; } } @@ -158,49 +179,6 @@ const getCollection = async (req, res, next) => { }) ); - // const refList = references.filter((ref) => ref.collectionId.equals(item._id)); - - // // 프리뷰 이미지 - // const relevantReferences = refList.slice(-4).reverse(); - - // const URLs = []; - // for (const ref of relevantReferences) { - // if (Array.isArray(ref.files)) { - // for (const file of ref.files) { - // switch (file.type) { - // case "image": - // URLs.push(...file.previewURLs.map((url) => ({type: file.type, url}))); - // break; - // case "link": - // case "pdf": - // default: - // URLs.push({type: file.type, url: file.previewURL}); - // break; - // } - // if (URLs.length >= 4) break; - // } - // } - // if (URLs.length >= 4) break; - // } - - // let previewImages = await Promise.all( - // URLs.map(async (file) => { - // try { - // switch (file.type) { - // case "link": - // return getOGImage(file.url); - // case "image": - // case "pdf": - // return file.url; - // default: - // return null; - // } - // } catch (err) { - // return null; - // } - // }) - // ); - const sharedEntry = collectionShared.find( (share) => share.collectionId.equals(item._id) && share.userId.equals(userId) ); @@ -495,6 +473,7 @@ const toggleFavorite = async (req, res, next) => { } }; +// updatedAt 없는 컬렉션, 레퍼런스를 위한 코드 (일회용) const updateCollectionTime = async (req, res, next) => { try { // updatedAt 없는 레퍼런스 업데이트 @@ -513,11 +492,12 @@ const updateCollectionTime = async (req, res, next) => { .sort({updatedAt: -1}) // 가장 최신 updatedAt .select("updatedAt"); if (latestRef) { - await Collection.updateOne( + const result = await Collection.updateOne( {_id: doc._id}, {$set: {updatedAt: latestRef.updatedAt}}, {timestamps: false} // 자동 updatedAt 덮어쓰기 방지 ); + console.log(result); } } console.log("✅ 컬렉션 updatedAt 동기화 완료"); diff --git a/models/OGImageCache.js b/models/OGImageCache.js new file mode 100644 index 0000000..552afa8 --- /dev/null +++ b/models/OGImageCache.js @@ -0,0 +1,29 @@ +// models/OGImageCache.js +import mongoose from "mongoose"; + +const OGImageCacheSchema = new mongoose.Schema( + { + url: { + type: String, + required: true, + unique: true, + index: true, // URL로 빠르게 검색하기 위해 인덱스 추가 + }, + imageUrl: { + type: String, + default: null, // OG Image URL이 없는 경우를 대비 + }, + createdAt: { + type: Date, + default: Date.now, // 기준 시작 시점 + expires: 604800, // 7일 후 자동 삭제 + }, + }, + { + timestamps: true, // createdAt, updatedAt 자동 생성 (createdAt은 TTL 인덱스에 사용) + } +); + +const OGImageCache = mongoose.model("OGImageCache", OGImageCacheSchema); + +export default OGImageCache; From 0eb8c80b64f9e7077333c6bf0c20a15a2bf4149e Mon Sep 17 00:00:00 2001 From: Minhee Han <96728777+KwakSsi38@users.noreply.github.com> Date: Fri, 27 Jun 2025 12:27:03 +0900 Subject: [PATCH 2/3] Update CICD.yml --- .github/workflows/CICD.yml | 39 ++++++++++++++++++++++++++++++-------- 1 file changed, 31 insertions(+), 8 deletions(-) diff --git a/.github/workflows/CICD.yml b/.github/workflows/CICD.yml index f1751fc..89d6acb 100644 --- a/.github/workflows/CICD.yml +++ b/.github/workflows/CICD.yml @@ -20,9 +20,10 @@ jobs: uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} - cache: 'npm' + cache: 'npm' # 이 캐시는 러너의 캐시이며, 빌드 속도 향상에 도움을 줍니다. - - name: 📦 의존성 설치 + - name: 📦 의존성 설치 (러너 작업 공간에 설치) + # 이 스텝에서 모든 의존성이 러너의 임시 디렉토리에 설치됩니다. run: npm ci - name: ⚙️ 프로젝트 빌드 @@ -30,13 +31,27 @@ jobs: - name: ✅ 테스트 실행 run: npm test - + - name: 🚀 EC2에 배포 run: | + # EC2의 프로젝트 디렉토리로 이동 cd ~/RefHub_BE + + # 🚨 중요: 기존 node_modules 폴더가 있다면 먼저 삭제하여 공간을 확보하고 중복 설치 방지 + if [ -d "node_modules" ]; then + echo "기존 ~/RefHub_BE/node_modules 삭제 중..." + rm -rf node_modules + fi + + # 최신 코드 pull git pull --no-rebase origin ${{ github.ref_name }} - npm install - + + # 🚨 핵심 변경: EC2 서버에서 npm install을 제거하고, 대신 러너에서 설치된 node_modules를 복사 + # ${{ github.workspace }}는 GitHub Actions 러너의 현재 작업 디렉토리 (즉, npm ci가 실행된 곳) + echo "러너의 node_modules를 EC2 프로젝트로 복사 중..." + cp -r ${{ github.workspace }}/node_modules . # 현재 디렉토리(~/RefHub_BE)로 복사 + + # PM2 ecosystem 파일 생성 및 PM2 명령 실행 (기존과 동일) if [[ "${{ github.ref_name }}" == "main" ]]; then echo "운영용 ecosystem.prod.config.cjs 생성" echo "module.exports = { @@ -63,9 +78,7 @@ jobs: } }] };" > ecosystem.prod.config.cjs - pm2 start ecosystem.prod.config.cjs --only app-prod || pm2 restart app-prod - else echo "개발용 ecosystem.dev.config.cjs 생성" echo "module.exports = { @@ -92,7 +105,17 @@ jobs: } }] };" > ecosystem.dev.config.cjs - pm2 start ecosystem.dev.config.cjs --only app-dev || pm2 restart app-dev fi shell: bash + + - name: 🗑️ 러너 작업 공간 정리 (배포 완료 후) + # 이 스텝은 배포 성공/실패와 무관하게 항상 실행되어 러너의 임시 공간을 비웁니다. + if: always() + run: | + echo "러너 작업 공간의 node_modules 및 캐시 정리 중..." + # 러너의 현재 작업 디렉토리(여기서는 ~/actions-runner/_work/RefHub_BE/RefHub_BE/)에 있는 node_modules 삭제 + rm -rf ${{ github.workspace }}/node_modules + # npm 캐시도 정리하여 혹시 모를 공간 부족에 대비 + npm cache clean --force + shell: bash From 8e6c56643437a8222d98c66c7a397fee83181b26 Mon Sep 17 00:00:00 2001 From: Hanmh111 <96728777+Hanmh111@users.noreply.github.com> Date: Fri, 27 Jun 2025 14:04:16 +0900 Subject: [PATCH 3/3] =?UTF-8?q?Refactor:=20getOGImage=20=ED=95=A8=EC=88=98?= =?UTF-8?q?=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- controllers/collectionController.js | 33 +-------------- middlewares/getOGImage.js | 64 +++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 32 deletions(-) create mode 100644 middlewares/getOGImage.js diff --git a/controllers/collectionController.js b/controllers/collectionController.js index 930b49d..1eb0356 100644 --- a/controllers/collectionController.js +++ b/controllers/collectionController.js @@ -4,43 +4,12 @@ import CollectionShare from "../models/CollectionShare.js"; import Reference from "../models/Reference.js"; import Keyword from "../models/Keyword.js"; import Extension from "../models/Extension.js"; -import OGImageCache from "../models/OGImageCache.js"; import {StatusCodes} from "http-status-codes"; import {deleteFileByUrl} from "../middlewares/fileDelete.js"; import {deletePreviewByUrl} from "../middlewares/previewDelete.js"; import {MongoError} from "mongodb"; -import ogs from "open-graph-scraper"; - - -async function getOGImage(url) { - // 캐시에서 먼저 확인 - const cachedImage = await OGImageCache.findOne({url}); - - // 캐시된 이미지가 있고, 생성된 지 7일 이내라면 캐시된 이미지 반환 - if (cachedImage && Date.now() - cachedImage.createdAt.getTime() < 7 * 24 * 60 * 60 * 1000) { - return cachedImage.imageUrl; - } - - try { - const {result} = await ogs({url}); - const ogImageUrl = result.ogImage?.[0]?.url || null; - - // 캐시 업데이트 또는 새로 생성 - await OGImageCache.findOneAndUpdate( - {url}, - {imageUrl: ogImageUrl, createdAt: new Date()}, // createdAt을 현재 시간으로 업데이트, TTL 갱신 - {upsert: true, new: true} // 없으면 생성, 있으면 업데이트 - ); - - return ogImageUrl; - } catch (err) { - console.log(`OG Image fetch error (${url}):`, err.message); - // 에러 발생 시에도 캐시에 null 또는 에러 상태를 저장하여 불필요 요청 방지 - await OGImageCache.findOneAndUpdate({url}, {imageUrl: null, createdAt: new Date()}, {upsert: true, new: true}); - return null; - } -} +import {getOGImage} from "../middlewares/getOGImage.js"; // 컬렉션 생성 const createCollection = async (req, res, next) => { diff --git a/middlewares/getOGImage.js b/middlewares/getOGImage.js new file mode 100644 index 0000000..8f3b428 --- /dev/null +++ b/middlewares/getOGImage.js @@ -0,0 +1,64 @@ +import OGImageCache from "../models/OGImageCache.js"; +import ogs from "open-graph-scraper"; + +export const getOGImage = async (url) => { + // 캐시에서 먼저 확인 + const cachedImage = await OGImageCache.findOne({url}); + + // 캐시된 이미지가 있고, 생성된 지 7일 이내라면 캐시된 이미지 반환 + if (cachedImage && Date.now() - cachedImage.createdAt.getTime() < 7 * 24 * 60 * 60 * 1000) { + return cachedImage.imageUrl; + } + + try { + const {result} = await ogs({url}); + const ogImageUrl = result.ogImage?.[0]?.url || null; + + // 캐시 업데이트 또는 새로 생성 + await OGImageCache.findOneAndUpdate( + {url}, + {imageUrl: ogImageUrl, createdAt: new Date()}, // createdAt을 현재 시간으로 업데이트, TTL 갱신 + {upsert: true, new: true} // 없으면 생성, 있으면 업데이트 + ); + + return ogImageUrl; + } catch (err) { + console.log(`OG Image fetch error (${url}):`, err.message); + // 에러 발생 시에도 캐시에 null 또는 에러 상태를 저장하여 불필요 요청 방지 + await OGImageCache.findOneAndUpdate({url}, {imageUrl: null, createdAt: new Date()}, {upsert: true, new: true}); + return null; + } +}; + +/* + + +async function getOGImage(url) { + // 캐시에서 먼저 확인 + const cachedImage = await OGImageCache.findOne({url}); + + // 캐시된 이미지가 있고, 생성된 지 7일 이내라면 캐시된 이미지 반환 + if (cachedImage && Date.now() - cachedImage.createdAt.getTime() < 7 * 24 * 60 * 60 * 1000) { + return cachedImage.imageUrl; + } + + try { + const {result} = await ogs({url}); + const ogImageUrl = result.ogImage?.[0]?.url || null; + + // 캐시 업데이트 또는 새로 생성 + await OGImageCache.findOneAndUpdate( + {url}, + {imageUrl: ogImageUrl, createdAt: new Date()}, // createdAt을 현재 시간으로 업데이트, TTL 갱신 + {upsert: true, new: true} // 없으면 생성, 있으면 업데이트 + ); + + return ogImageUrl; + } catch (err) { + console.log(`OG Image fetch error (${url}):`, err.message); + // 에러 발생 시에도 캐시에 null 또는 에러 상태를 저장하여 불필요 요청 방지 + await OGImageCache.findOneAndUpdate({url}, {imageUrl: null, createdAt: new Date()}, {upsert: true, new: true}); + return null; + } +} +*/ \ No newline at end of file