Conversation
* Article 모델을 추가하고, 관련 DTO 및 리포지토리, 서비스, 컨트롤러를 구현하여 기사 생성, 조회, 업데이트, 삭제 기능을 추가했습니다. * 스크래핑된 콘텐츠 저장 기능과 URL 중복 확인 기능도 포함되었습니다. * 데이터베이스 마이그레이션 파일을 생성하여 Article 테이블을 정의했습니다. * Swagger 문서화를 통해 API 문서화 작업을 진행했습니다.
* 콘텐츠 전처리를 위한 PreHandlerModule 및 PreHandlerService를 추가했습니다. * 다양한 콘텐츠 유형을 처리하기 위한 핸들러(PdfHandler, RssHandler, YoutubeHandler, SocialMediaHandler, NewsSiteHandler, DomainSpecificHandler, ReadabilityHandler)를 구현했습니다. * 각 핸들러는 특정 URL 패턴을 인식하고, 콘텐츠를 적절히 변환하여 처리할 수 있도록 설계되었습니다. * PreHandleResult DTO를 통해 핸들러의 결과를 구조화하여 반환합니다. * 모듈의 확장성을 고려하여 새로운 핸들러 추가가 용이하도록 설계되었습니다.
* ScraperController를 추가하여 웹 콘텐츠 스크래핑 및 저장 기능을 구현했습니다. * FetchContentInput 및 ScrapedContentOutput DTO를 정의하여 스크래핑 요청 및 응답 구조를 명확히 했습니다. * PuppeteerParseService를 통해 Puppeteer를 사용한 콘텐츠 가져오기 및 처리 로직을 추가했습니다. * InvalidUrlException을 정의하여 URL 유효성 검사 실패 시 적절한 예외 처리를 구현했습니다. * 스크래핑된 콘텐츠를 HTML로 렌더링하여 미리보기 기능을 추가했습니다.
* JWT 가드에서 Bearer 토큰 요구 메시지를 수정하고, 로깅 기능을 추가하여 오류 발생 시 로그 기록을 강화했습니다. * Swagger 설정에서 JWT 토큰 유지 기능을 추가하여 브라우저 새로고침 시에도 인증 상태를 유지하도록 개선했습니다. * 스크래퍼 컨트롤러에서 인증된 사용자 정보를 사용하여 콘텐츠 저장 기능을 활성화하고, 관련 DTO를 업데이트했습니다. * 여러 API 엔드포인트에서 AuthRequest 타입을 사용하여 사용자 정보를 명확히 처리하도록 변경했습니다.
* ReadabilityHandler에서 JSDOM 설정을 추가하여 스크립트 실행 및 리소스 로딩을 허용했습니다. * PuppeteerParseService에 Readability를 적용하여 HTML 콘텐츠에서 본문만 추출하는 기능을 추가했습니다. * 콘텐츠 추출 성공 및 실패 시 로깅 기능을 강화하여 디버깅을 용이하게 했습니다.
* Article 모델에 id와 userId를 조합한 복합 고유 제약조건을 추가하여 데이터 무결성을 강화했습니다. * Article 업데이트 로직에서 오류 발생 시 경고 로그를 추가하여 디버깅을 용이하게 했습니다. * Naver 블로그 콘텐츠 추출 기능을 개선하여 타이틀 및 콘텐츠를 보다 정확하게 처리하도록 했습니다.
* 스티비(Stibee) 뉴스레터 플랫폼을 위한 StibeeHandler를 추가하여 스티비 뉴스레터 콘텐츠를 최적화하여 추출하는 기능을 구현했습니다. * 핸들러 목록에 StibeeHandler를 추가하고, 주석을 업데이트하여 핸들러의 순서를 명확히 했습니다.
* Article 엔티티를 ArticleOutput DTO로 변환하는 로직에서 각 필드에 대해 null 체크를 추가하여, undefined 값을 명시적으로 처리하도록 개선했습니다. 이를 통해 데이터의 일관성을 높이고, 클라이언트에서의 오류 발생 가능성을 줄였습니다.
* 메일리(Maily) 뉴스레터 플랫폼을 위한 MailyHandler를 추가하여 메일리 뉴스레터 콘텐츠를 최적화하여 추출하는 기능을 구현했습니다. * 핸들러 목록에 MailyHandler를 추가하고, 주석을 업데이트하여 핸들러의 순서를 명확히 했습니다.
* PreHandlerService를 RefactoredPreHandlerService로 리팩토링하여 핸들러 팩토리 패턴을 적용했습니다. * 핸들러 목록을 HandlerFactory를 통해 관리하도록 변경하여 코드의 가독성과 유지보수성을 향상시켰습니다. * 기존 핸들러를 AbstractContentHandler를 상속받아 리팩토링하여 SOLID 원칙을 준수하도록 개선했습니다. * 각 핸들러의 HTTP 요청 및 DOM 생성 설정을 통일하여 일관성을 높였습니다. * 불필요한 주석을 제거하고, 코드의 명확성을 높였습니다.
* TistoryHandler, MediumHandler, NaverBlogHandler, DisquietHandler를 추가하여 다양한 블로그 플랫폼의 콘텐츠를 처리할 수 있도록 했습니다. * 기존 DomainSpecificHandler를 개선하여 특정 도메인에 대한 URL 변환 로직을 최적화했습니다. * 핸들러의 HTTP 요청 및 DOM 생성 설정을 통일하여 일관성을 높였습니다. * 콘텐츠 추출 과정에서 발생할 수 있는 에러를 명확히 구분하기 위해 커스텀 에러 클래스를 추가했습니다. * 콘텐츠 품질 평가 기능을 추가하여 스크래핑 시 품질 기준을 설정하고, Puppeteer를 통한 대체 로직을 개선했습니다.
* 로깅 인터셉터의 테스트에서 HTTP 응답 로그 형식을 정규 표현식을 사용하여 검증하도록 수정했습니다. * 이를 통해 응답 시간(ms)을 포함한 로그 메시지의 일관성을 높였습니다.
* JwtAuthGuard의 테스트에서 jwtService.verify의 구현 방식을 개선하여 예외 처리 및 토큰 타입 검증 로직을 명확히 했습니다. * authService의 타입을 명시적으로 설정하여 코드의 가독성을 높였습니다.
* 로깅 인터셉터의 getClientIp 테스트에서 입력값의 타입을 Request로 변경하여 타입 안정성을 높였습니다. * 각 테스트 케이스에서 unknown 타입을 명시적으로 처리하여 코드의 가독성을 개선했습니다.
* main.ts에서 bootstrap() 호출을 void bootstrap()으로 변경하여 비동기 함수의 반환값을 무시하도록 수정했습니다. * 코드의 명확성을 높이고, 비동기 처리에 대한 의도를 명확히 했습니다.
* sanitize-html의 IOptions 타입을 사용하여 HTML 정제 로직을 개선했습니다. * 웹페이지의 console.log 출력을 무시하는 기능을 추가하여 불필요한 콘솔 로그를 억제했습니다.
* YouTube 핸들러에서 oEmbed API를 사용하여 동영상 정보를 가져오는 기능을 추가했습니다. * 기존 HTML fetch 방식에서 oEmbed API로의 전환으로 코드의 간결성을 높였습니다. * 자막 추출 기능을 개선하고, oEmbed 데이터를 포함한 HTML 콘텐츠 생성 로직을 추가했습니다. * 핸들러의 URL 매칭 로직을 정규 표현식으로 개선하여 가독성을 향상시켰습니다.
* RefactoredPreHandlerService에 ContentQualityEvaluator를 통합하여 콘텐츠 품질 평가 기능을 추가했습니다. * 핸들러 체인 실행 시 품질 평가를 포함하여 효율적인 콘텐츠 처리를 구현했습니다. * 콘텐츠 요소 찾기 로직에서 최소 텍스트 길이를 80으로 변경하여 품질 기준을 강화했습니다. * ContentQualityEvaluator에서 다양한 품질 메트릭을 계산하도록 개선하여 평가의 정확성을 높였습니다. * PuppeteerParseService에서 품질이 좋은 콘텐츠를 추출하지 못한 경우에만 Puppeteer를 사용하도록 로직을 수정했습니다.
* 애플리케이션의 로그 레벨을 환경 변수로 설정할 수 있도록 변경했습니다. * 부트스트랩 과정에서 현재 로그 레벨을 출력하는 로깅 기능을 추가했습니다. * app.config.ts에 LOG_LEVEL 및 SUPPRESS_BROWSER_LOGS 설정을 추가하여 구성 가능성을 높였습니다.
* JwtAuthGuard 테스트에서 jwtService.verify 및 authService.validateUser 호출 검증을 추가하여 메서드 호출의 정확성을 높였습니다. * RefreshTokenRepository 테스트에서 Prisma 메서드 호출 검증 방식을 개선하여 코드의 가독성을 향상시켰습니다. * GoogleStrategy 테스트에서 사용자 프로필 타입을 명시적으로 설정하여 타입 안정성을 강화했습니다. * UserController 테스트에서 요청 객체의 타입을 AuthRequest로 변경하여 타입 안정성을 높였습니다. * UserRepository 테스트에서 Prisma 메서드 호출 검증 방식을 개선하여 코드의 일관성을 유지했습니다. * 기타 테스트 파일에서 타입 및 메서드 호출 검증을 개선하여 전반적인 테스트 품질을 향상시켰습니다.
* JwtAuthGuard에서 AuthenticatedRequest 인터페이스를 AuthRequest로 변경하여 코드의 일관성을 높였습니다. * 불필요한 import 문을 제거하여 코드의 가독성을 개선했습니다.
* ArticleRepository에서 id와 userId를 조합하여 업데이트 및 삭제 쿼리를 개선했습니다. * TokenService에서 refresh token 실패 시 경고 로그를 추가하여 오류 추적을 용이하게 했습니다. * ScraperController에서 SaveContentInput DTO를 도입하여 요청 본문의 타입 안정성을 높였습니다. * PuppeteerParseService에서 도메인 특화 로직을 제거하고 Readability 적용 로직을 단순화했습니다. * SaveContentInput DTO를 새로 추가하여 스크래핑 시 태그, 북마크, 아카이브 옵션을 지원합니다.
* Google Safe Browsing API를 활용하여 URL의 안전성을 검사하는 SecurityService를 추가했습니다. * ArticleService에서 스크랩한 콘텐츠를 DB에 저장하기 전에 XSS 공격을 방지하기 위해 HTML 콘텐츠를 살균하는 sanitizeHtml 메서드를 구현했습니다. * 새로운 SecurityModule을 추가하여 보안 관련 기능을 모듈화했습니다. * ScraperModule에 SecurityModule을 통합하여 URL 안전성 검사를 적용했습니다.
* RefactoredPreHandlerService를 PreHandlerService로 이름 변경하여 코드 일관성을 높였습니다. * 핸들러 등록 방식을 개선하여 CONTENT_HANDLER_TOKEN을 사용하여 동적으로 핸들러를 주입하도록 변경했습니다. * 핸들러 체인 실행 로직을 단순화하고, 각 핸들러의 후처리 및 URL 가공 로직을 추상화하여 재사용성을 높였습니다. * 여러 핸들러에서 콘텐츠 추출 및 후처리 로직을 개선하여 코드의 가독성을 향상시켰습니다. * 불필요한 주석 및 코드를 정리하여 전체적인 코드 품질을 개선했습니다.
* PreHandlerService에서 핸들러 사용 정보를 result.handlerUsed에 저장하도록 수정했습니다. * PreHandleResult DTO에 handlerUsed 필드를 추가하여 핸들러 정보를 포함하도록 변경했습니다. * ScraperPreviewService를 새로 추가하여 스크래핑 결과를 HTML 형식으로 렌더링하는 기능을 구현했습니다. * 스크래핑 결과 및 사전 처리 결과를 사용자에게 보여줄 수 있는 HTML 템플릿을 작성했습니다.
Summary of ChangesHello @reach0908, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! 이 풀 리퀘스트는 Omnivore 시스템을 벤치마킹하여 웹 콘텐츠 스크래핑 및 관리 시스템을 개발하는 것을 목표로 합니다. 주요 변경 사항으로는 웹 페이지에서 기사 콘텐츠를 추출하고 정제하는 다양한 핸들러(예: 뉴스 사이트, 소셜 미디어, 블로그, PDF, RSS)를 포함하는 사전 처리 모듈과, 추출된 콘텐츠를 저장하고 관리하기 위한 Highlights
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
There was a problem hiding this comment.
Code Review
이 PR은 Omnivore와 유사한 웹 스크래핑 및 저장 시스템을 구현하는 대규모 기능 추가입니다. 전반적으로 코드 품질이 매우 높고, 아키텍처가 잘 설계되었습니다. 특히, 핸들러 체인을 이용한 사전 처리 로직, 콘텐츠 품질 평가를 통한 Puppeteer 폴백 전략, 그리고 Puppeteer 사용 시의 상세한 설정과 최적화는 인상적입니다. 보안(XSS, SSRF, API 키 관리)과 안정성(오류 처리, 재시도 로직)에 대한 고려도 훌륭합니다. 몇 가지 사소한 개선점을 제안하지만, 전체적으로 매우 잘 작성된 코드입니다.
| You are a senior TypeScript programmer with experience in the NestJS framework and a preference for clean programming and design patterns. Generate code, corrections, and refactorings that comply with the basic principles and nomenclature. | ||
|
|
||
| ## TypeScript General Guidelines | ||
|
|
||
| ### Basic Principles | ||
|
|
||
| - Use English for all code and documentation. | ||
| - Always declare the type of each variable and function (parameters and return value). | ||
| - Avoid using any. | ||
| - Create necessary types. | ||
| - Use JSDoc to document public classes and methods. | ||
| - Don't leave blank lines within a function. | ||
| - One export per file. | ||
|
|
||
| ### Nomenclature | ||
|
|
||
| - Use PascalCase for classes. | ||
| - Use camelCase for variables, functions, and methods. | ||
| - Use kebab-case for file and directory names. | ||
| - Use UPPERCASE for environment variables. | ||
| - Avoid magic numbers and define constants. | ||
| - Start each function with a verb. | ||
| - Use verbs for boolean variables. Example: isLoading, hasError, canDelete, etc. | ||
| - Use complete words instead of abbreviations and correct spelling. | ||
| - Except for standard abbreviations like API, URL, etc. | ||
| - Except for well-known abbreviations: | ||
| - i, j for loops | ||
| - err for errors | ||
| - ctx for contexts | ||
| - req, res, next for middleware function parameters | ||
|
|
||
| ### Functions | ||
|
|
||
| - In this context, what is understood as a function will also apply to a method. | ||
| - Write short functions with a single purpose. Less than 20 instructions. | ||
| - Name functions with a verb and something else. | ||
| - If it returns a boolean, use isX or hasX, canX, etc. | ||
| - If it doesn't return anything, use executeX or saveX, etc. | ||
| - Avoid nesting blocks by: | ||
| - Early checks and returns. | ||
| - Extraction to utility functions. | ||
| - Use higher-order functions (map, filter, reduce, etc.) to avoid function nesting. | ||
| - Use arrow functions for simple functions (less than 3 instructions). | ||
| - Use named functions for non-simple functions. | ||
| - Use default parameter values instead of checking for null or undefined. | ||
| - Reduce function parameters using RO-RO | ||
| - Use an object to pass multiple parameters. | ||
| - Use an object to return results. | ||
| - Declare necessary types for input arguments and output. | ||
| - Use a single level of abstraction. | ||
|
|
||
| ### Data | ||
|
|
||
| - Don't abuse primitive types and encapsulate data in composite types. | ||
| - Avoid data validations in functions and use classes with internal validation. | ||
| - Prefer immutability for data. | ||
| - Use readonly for data that doesn't change. | ||
| - Use as const for literals that don't change. | ||
|
|
||
| ### Classes | ||
|
|
||
| - Follow SOLID principles. | ||
| - Prefer composition over inheritance. | ||
| - Declare interfaces to define contracts. | ||
| - Write small classes with a single purpose. | ||
| - Less than 200 instructions. | ||
| - Less than 10 public methods. | ||
| - Less than 10 properties. | ||
|
|
||
| ### Exceptions | ||
|
|
||
| - Use exceptions to handle errors you don't expect. | ||
| - If you catch an exception, it should be to: | ||
| - Fix an expected problem. | ||
| - Add context. | ||
| - Otherwise, use a global handler. | ||
|
|
||
| ### Testing | ||
|
|
||
| - Follow the Arrange-Act-Assert convention for tests. | ||
| - Name test variables clearly. | ||
| - Follow the convention: inputX, mockX, actualX, expectedX, etc. | ||
| - Write unit tests for each public function. | ||
| - Use test doubles to simulate dependencies. | ||
| - Except for third-party dependencies that are not expensive to execute. | ||
| - Write acceptance tests for each module. | ||
| - Follow the Given-When-Then convention. | ||
|
|
||
| ## Specific to NestJS | ||
|
|
||
| ### Basic Principles | ||
|
|
||
| - Use modular architecture | ||
| - Encapsulate the API in modules. | ||
| - One module per main domain/route. | ||
| - One controller for its route. | ||
| - And other controllers for secondary routes. | ||
| - A models folder with data types. | ||
| - DTOs validated with class-validator for inputs. | ||
| - Declare simple types for outputs. | ||
| - A services module with business logic and persistence. | ||
| - Entities with MikroORM for data persistence. | ||
| - One service per entity. | ||
| - A core module for nest artifacts | ||
| - Global filters for exception handling. | ||
| - Global middlewares for request management. | ||
| - Guards for permission management. | ||
| - Interceptors for request management. | ||
| - A shared module for services shared between modules. | ||
| - Utilities | ||
| - Shared business logic | ||
|
|
||
| ### Testing | ||
|
|
||
| - Use the standard Jest framework for testing. | ||
| - Write tests for each controller and service. | ||
| - Write end to end tests for each api module. | ||
| - Add a admin/test method to each controller as a smoke test. |
| "puppeteer-extra-plugin-stealth": "^2.11.2", | ||
| "reflect-metadata": "^0.2.2", | ||
| "rxjs": "^7.8.1", | ||
| "sanitize-html": "^2.17.0", |
| const updated = await this.articleRepository.updateArticle(articleId, userId, { | ||
| isBookmarked: !article.isBookmarked, | ||
| }); | ||
|
|
||
| return this.mapToOutput(updated!); |
There was a problem hiding this comment.
articleRepository.updateArticle 메서드는 null을 반환할 수 있지만, updated!와 같이 non-null assertion을 사용하고 있습니다. 만약 findById와 updateArticle 호출 사이에 해당 Article이 삭제되는 경쟁 조건(race condition)이 발생하면, updateArticle이 null을 반환하여 런타임 에러가 발생할 수 있습니다. null 케이스를 명시적으로 처리하여 안정성을 높이는 것이 좋습니다.
const updated = await this.articleRepository.updateArticle(articleId, userId, {
isBookmarked: !article.isBookmarked,
});
if (!updated) {
throw new NotFoundException('Article을 업데이트하는 중 예기치 않은 오류가 발생했습니다.');
}
return this.mapToOutput(updated);| const updated = await this.articleRepository.updateArticle(articleId, userId, { | ||
| isArchived: !article.isArchived, | ||
| }); | ||
|
|
||
| return this.mapToOutput(updated!); |
There was a problem hiding this comment.
toggleBookmark 메서드와 마찬가지로, articleRepository.updateArticle이 null을 반환할 경우 updated! non-null assertion으로 인해 런타임 에러가 발생할 수 있습니다. null을 반환받는 경우에 대한 예외 처리를 추가하여 코드의 안정성을 높이는 것을 권장합니다.
const updated = await this.articleRepository.updateArticle(articleId, userId, {
isArchived: !article.isArchived,
});
if (!updated) {
throw new NotFoundException('Article을 업데이트하는 중 예기치 않은 오류가 발생했습니다.');
}
return this.mapToOutput(updated);| client.on('Network.requestIntercepted', (e: Protocol.Network.RequestInterceptedEvent) => { | ||
| void (async () => { | ||
| const headers = e.responseHeaders ?? {}; | ||
| const ctype = (headers['content-type'] ?? headers['Content-Type'] ?? '').split(';')[0].toLowerCase(); | ||
| const shouldBlock = | ||
| ctype && !ALLOWED_CONTENT_TYPES.includes(ctype as (typeof ALLOWED_CONTENT_TYPES)[number]); | ||
|
|
||
| await client.send('Network.continueInterceptedRequest', { | ||
| interceptionId: e.interceptionId, | ||
| ...(shouldBlock ? { errorReason: 'BlockedByClient' } : {}), | ||
| }); | ||
| })(); |
No description provided.