diff --git a/package-lock.json b/package-lock.json index e2e7d1d..65ff801 100644 --- a/package-lock.json +++ b/package-lock.json @@ -40,6 +40,7 @@ "form-data": "^4.0.0", "ioredis": "^5.3.2", "joi": "^17.6.0", + "open-graph-scraper": "^6.8.1", "openai": "^3.3.0", "passport": "^0.6.0", "passport-google-oauth20": "^2.0.0", @@ -9809,6 +9810,36 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/open-graph-scraper": { + "version": "6.8.1", + "resolved": "https://registry.npmjs.org/open-graph-scraper/-/open-graph-scraper-6.8.1.tgz", + "integrity": "sha512-uy9tHm80ir9AnEQclmnlpxU80SBQkwZfWFq0lzRxpNAY1Grcyn7tuOu354XhnfA7C3wNuZ0/j94LNQY5XP7ZRg==", + "dependencies": { + "chardet": "^2.0.0", + "cheerio": "^1.0.0-rc.12", + "iconv-lite": "^0.6.3", + "undici": "^6.19.8" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/open-graph-scraper/node_modules/chardet": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.0.0.tgz", + "integrity": "sha512-xVgPpulCooDjY6zH4m9YW3jbkaBe3FKIAvF5sj5t7aBNsVl2ljIE+xwJ4iNgiDZHFQvNIpjdKdVOQvvk5ZfxbQ==" + }, + "node_modules/open-graph-scraper/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/openai": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/openai/-/openai-3.3.0.tgz", @@ -12228,6 +12259,14 @@ "resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.4.tgz", "integrity": "sha512-IevTus0SbGwQzYh3+fRsAMTVVPOoIVufzacXcHPmdlle1jUpq7BRL+mw3dgeLanvGZdwwbWhRV6XrcFNdBmjWA==" }, + "node_modules/undici": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.19.8.tgz", + "integrity": "sha512-U8uCCl2x9TK3WANvmBavymRzxbfFYG+tAu+fgx3zxQy3qdagQqBLwJVrdyO1TBfUXvfKveMKJZhpvUYoOjM+4g==", + "engines": { + "node": ">=18.17" + } + }, "node_modules/undici-types": { "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", @@ -20690,6 +20729,32 @@ "mimic-fn": "^2.1.0" } }, + "open-graph-scraper": { + "version": "6.8.1", + "resolved": "https://registry.npmjs.org/open-graph-scraper/-/open-graph-scraper-6.8.1.tgz", + "integrity": "sha512-uy9tHm80ir9AnEQclmnlpxU80SBQkwZfWFq0lzRxpNAY1Grcyn7tuOu354XhnfA7C3wNuZ0/j94LNQY5XP7ZRg==", + "requires": { + "chardet": "^2.0.0", + "cheerio": "^1.0.0-rc.12", + "iconv-lite": "^0.6.3", + "undici": "^6.19.8" + }, + "dependencies": { + "chardet": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.0.0.tgz", + "integrity": "sha512-xVgPpulCooDjY6zH4m9YW3jbkaBe3FKIAvF5sj5t7aBNsVl2ljIE+xwJ4iNgiDZHFQvNIpjdKdVOQvvk5ZfxbQ==" + }, + "iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + } + } + } + }, "openai": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/openai/-/openai-3.3.0.tgz", @@ -22433,6 +22498,11 @@ "resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.4.tgz", "integrity": "sha512-IevTus0SbGwQzYh3+fRsAMTVVPOoIVufzacXcHPmdlle1jUpq7BRL+mw3dgeLanvGZdwwbWhRV6XrcFNdBmjWA==" }, + "undici": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.19.8.tgz", + "integrity": "sha512-U8uCCl2x9TK3WANvmBavymRzxbfFYG+tAu+fgx3zxQy3qdagQqBLwJVrdyO1TBfUXvfKveMKJZhpvUYoOjM+4g==" + }, "undici-types": { "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", diff --git a/package.json b/package.json index 5d64b8e..3051a4c 100644 --- a/package.json +++ b/package.json @@ -58,6 +58,7 @@ "form-data": "^4.0.0", "ioredis": "^5.3.2", "joi": "^17.6.0", + "open-graph-scraper": "^6.8.1", "openai": "^3.3.0", "passport": "^0.6.0", "passport-google-oauth20": "^2.0.0", diff --git a/src/common/logger.ts b/src/common/logger.ts index 9549ea4..5e24296 100644 --- a/src/common/logger.ts +++ b/src/common/logger.ts @@ -1,5 +1,5 @@ import * as winston from 'winston'; -import * as DailyRotateFile from 'winston-daily-rotate-file'; +import DailyRotateFile from 'winston-daily-rotate-file'; const { combine, label, printf, colorize } = winston.format; const logFormat = printf(({ level, label, message }) => { diff --git a/src/contents/contents.service.ts b/src/contents/contents.service.ts index 7dca2f7..99d76a7 100644 --- a/src/contents/contents.service.ts +++ b/src/contents/contents.service.ts @@ -28,7 +28,7 @@ import { LoadReminderCountOutput } from './dtos/load-personal-remider-count.dto' import { UserRepository } from '../users/repository/user.repository'; import { ContentRepository } from './repository/content.repository'; import { CategoryRepository } from '../categories/category.repository'; -import { getLinkInfo } from './util/content.util'; +import { getOgData } from './util/content.util'; import { GetLinkInfoResponseDto } from './dtos/get-link.response.dto'; import { checkContentDuplicateAndAddCategorySaveLog } from '../categories/utils/category.util'; import { Transactional } from '../common/aop/transactional'; @@ -69,7 +69,7 @@ export class ContentsService { siteName, description, coverImg, - } = await getLinkInfo(link); + } = await getOgData(link); title = title ? title : linkTitle; let category: Category | undefined = undefined; @@ -132,7 +132,7 @@ export class ContentsService { await Promise.all( contentLinks.map(async (link) => { - const { title, description, coverImg, siteName } = await getLinkInfo( + const { title, description, coverImg, siteName } = await getOgData( link, ); @@ -391,7 +391,7 @@ export class ContentsService { } async getLinkInfo(link: string) { - const data = await getLinkInfo(link); + const data = await getOgData(link); return new GetLinkInfoResponseDto(data); } diff --git a/src/contents/dtos/get-link.response.dto.ts b/src/contents/dtos/get-link.response.dto.ts index a66fc37..45aaecb 100644 --- a/src/contents/dtos/get-link.response.dto.ts +++ b/src/contents/dtos/get-link.response.dto.ts @@ -1,26 +1,26 @@ -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { ApiPropertyOptional } from '@nestjs/swagger'; import { LinkInfo } from '../types/link-info.interface'; export class GetLinkInfoResponseDto { - @ApiProperty({ + @ApiPropertyOptional({ description: '아티클 제목', example: '[Terraform] 테라폼 훑어보기 — 턴태의 밑바닥부터 시작하는 de-vlog', }) - private readonly title: string; + private readonly title?: string; - @ApiProperty({ + @ApiPropertyOptional({ description: '아티클 본문 일부', example: '인프라 구조마저 코드로 조작하고 있는 현재, 가장 많이 쓰이는 도구는 테라폼과 앤서블이 있습니다.', }) - private readonly description: string; + private readonly description?: string; - @ApiProperty({ + @ApiPropertyOptional({ description: '썸네일/커버 이미지', example: 'https://img1.daumcdn.net/thumb/R800x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fz52vz%2FbtsCAOBzgTR%2FhFgGDKkr6iKWfU6eKeKUVk%2Fimg.png', }) - private readonly coverImg: string; + private readonly coverImg?: string; @ApiPropertyOptional({ description: '아티클 사이트 주소', diff --git a/src/contents/types/link-info.interface.ts b/src/contents/types/link-info.interface.ts index a656ff9..434c55f 100644 --- a/src/contents/types/link-info.interface.ts +++ b/src/contents/types/link-info.interface.ts @@ -1,6 +1,6 @@ export interface LinkInfo { - title: string; - description: string; - coverImg: string; + title?: string; + description?: string; + coverImg?: string; siteName?: string; } diff --git a/src/contents/util/content.util.ts b/src/contents/util/content.util.ts index d80f7aa..6acebc1 100644 --- a/src/contents/util/content.util.ts +++ b/src/contents/util/content.util.ts @@ -2,6 +2,30 @@ import { BadRequestException } from '@nestjs/common'; import * as cheerio from 'cheerio'; import axios from 'axios'; +import ogs from 'open-graph-scraper'; + +export async function getOgData(link: string) { + try { + const { result } = await ogs({ + url: link, + }); + + return { + title: result.ogTitle ?? '', + description: result.ogDescription ?? '', + coverImg: result.ogImage ? result.ogImage[0].url : '', + siteName: result.ogSiteName ?? '', + }; + } catch { + return { + title: '', + description: '', + coverImg: '', + siteName: '', + }; + } +} + export const getLinkInfo = async (link: string) => { let title: string | undefined = ''; let coverImg: string | undefined = ''; diff --git a/src/mail/mail.service.ts b/src/mail/mail.service.ts index 0e912b1..7f1c3f3 100644 --- a/src/mail/mail.service.ts +++ b/src/mail/mail.service.ts @@ -1,7 +1,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { EmailVar, MailModuleOptions } from './mail.interface'; import axios from 'axios'; -import * as FormData from 'form-data'; +import FormData from 'form-data'; import { CONFIG_OPTIONS } from '../common/common.constants'; @Injectable() diff --git a/tsconfig.json b/tsconfig.json index 9e7fc36..5b6c086 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -20,6 +20,7 @@ "strictBindCallApply": true, "strictPropertyInitialization": false, "noImplicitThis": true, - "alwaysStrict": true + "alwaysStrict": true, + "esModuleInterop": true } }