Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 31 additions & 8 deletions .github/workflows/CICD.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,23 +20,38 @@ jobs:
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
cache: 'npm' # 이 캐시는 러너의 캐시이며, 빌드 속도 향상에 도움을 줍니다.

- name: 📦 의존성 설치
- name: 📦 의존성 설치 (러너 작업 공간에 설치)
# 이 스텝에서 모든 의존성이 러너의 임시 디렉토리에 설치됩니다.
run: npm ci

- name: ⚙️ 프로젝트 빌드
run: npm run build --if-present

- 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 = {
Expand All @@ -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 = {
Expand All @@ -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
59 changes: 4 additions & 55 deletions controllers/collectionController.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,7 @@ 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) {
try {
const {result} = await ogs({url});
return result.ogImage[0]?.url || null;
} catch (err) {
console.log(`OG Image fetch error (${url}:)`, err.message);
return null;
}
}
import {getOGImage} from "../middlewares/getOGImage.js";

// 컬렉션 생성
const createCollection = async (req, res, next) => {
Expand Down Expand Up @@ -158,49 +148,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)
);
Expand Down Expand Up @@ -495,6 +442,7 @@ const toggleFavorite = async (req, res, next) => {
}
};

// updatedAt 없는 컬렉션, 레퍼런스를 위한 코드 (일회용)
const updateCollectionTime = async (req, res, next) => {
try {
// updatedAt 없는 레퍼런스 업데이트
Expand All @@ -513,11 +461,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 동기화 완료");
Expand Down
64 changes: 64 additions & 0 deletions middlewares/getOGImage.js
Original file line number Diff line number Diff line change
@@ -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;
}
}
*/
29 changes: 29 additions & 0 deletions models/OGImageCache.js
Original file line number Diff line number Diff line change
@@ -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;