Skip to content

[Feat/#921] 새벽 나스닥 종목 스케줄러 추가 - #940

Open
MinguKang98 wants to merge 14 commits into
devfrom
feat/#921
Open

[Feat/#921] 새벽 나스닥 종목 스케줄러 추가#940
MinguKang98 wants to merge 14 commits into
devfrom
feat/#921

Conversation

@MinguKang98

@MinguKang98 MinguKang98 commented Mar 16, 2026

Copy link
Copy Markdown
Collaborator

🎫 연관 이슈

resolved #921

💁‍♂️ PR 내용

🙈 PR 참고 사항

🚩 추가된 SQL 운영계 실행계획

Summary by CodeRabbit

  • 새로운 기능

    • 일일 나스닥 주식 카드 이미지 생성기 추가(1080×1080), S3 업로드 및 Instagram 단일 미디어 게시 워크플로 통합
    • KIS 토큰/시세 Feign 클라이언트 및 주식 조회(ETF/M7 그룹) 구현
    • NYSE 영업일 검사 및 스케줄링(크론)으로 자동 실행 제어
  • 테스트

    • 단위 및 통합 테스트 대거 추가(이미지 생성, KIS 연동, S3 업로드, 스케줄링/Instagram 흐름)

MinguKang98 and others added 9 commits March 1, 2026 23:16
- KisClient/KisTokenClient Feign 클라이언트 구현
- NasdaqStockConstants에 M7 + S&P500/나스닥100 ETF 종목 정의
- NasdaqStockData에 changeRate 부호 기반 isRise computed property 적용
- local/prd 환경 Feign 설정에 KIS API 엔드포인트 추가
- KisStockFetcherTest(단위) 및 KisStockFetcherIntegrationTest(통합) 작성

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- 주식 카드뉴스 생성 기능 StockCardGenerator 추가
- NasdaqStockConstants 내 SCHD 추가
- NasdaqStockData -> StockQuote
- NasdaqStockConstants -> OverseaStockConstants
- Stock 클래스 추가
- 캔버스 800×950 → 1080×1080 (1:1 인스타그램 비율)
- 헤더: 딥네이비(#1F2333) 배경, "NASDAQ DAILY" + 날짜 아쿠아/화이트 텍스트
- ETF 섹션: 라운드 화이트 카드 3개 수평 배치, FontMetrics 기반 세로 중앙 정렬
- M7 섹션: 아쿠아 타이틀+밑줄, 로고·종목명·가격·등락률 열 구성, 세로 중앙 정렬
- 푸터: few_logo.png 중앙 배치, optional Market Mood 텍스트 지원
- M7 기업 로고 PNG 7종 추가 (애플·MS·구글·아마존·엔비디아·메타·테슬라)
- 로고 비율 유지 리사이즈 및 바운딩박스 중앙 정렬 적용

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- NasdaqDailyStockCardSchedulingUseCase: KIS 시세 조회 → 이미지 생성 → S3 업로드 → Instagram 단일 이미지 게시 전체 흐름 구현
- InstagramUploader: 단일 이미지용 createSingleMediaContainer 메서드 추가
- OverseaStockConstants: SCHD 한국명 '미국배당다우존스' → '다우존스' 축약
- 스케줄러 크론키 nasdaq-stock → nasdaq-daily-stock 변경 (local/prd)
- NasdaqDailyStockCardSchedulingUseCaseTest: 단위 테스트 추가 (성공/실패 케이스)
- NasdaqStockCardS3UploadIntegrationTest: 이미지 생성 후 S3 업로드 통합 테스트 추가

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- NyseMarketCalendar: 주말 + 2025/2026 NYSE 공휴일 기반 거래일 판별 컴포넌트 추가
- NasdaqDailyStockCardSchedulingUseCase: 미국 동부시간 기준 휴장일이면 실행 건너뜀
- SchedulingController: nasdaq-daily-stock 스케줄 메서드 연결
- NasdaqDailyStockCardSchedulingUseCaseTest: 휴장일 스킵 테스트 케이스 추가

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Mar 16, 2026

Copy link
Copy Markdown

Walkthrough

KIS API에서 나스닥 관련 종목·가격 데이터를 조회해 Instagram용 1080×1080 카드 이미지를 생성하고 S3에 업로드한 뒤 인스타그램에 게시하는 일일 스케줄링 워크플로우와 이를 구성하는 Feign 클라이언트, DTO, 도메인 모델, 이미지 생성기, 스케줄링 유스케이스/컨트롤러, 거래일 캘린더, 설정 및 단위·통합 테스트들을 추가합니다.

Changes

Cohort / File(s) Summary
KIS 클라이언트 · 토큰 클라이언트 · DTOs
domain/generator/src/main/kotlin/com/few/generator/core/kis/KisClient.kt, domain/generator/src/main/kotlin/com/few/generator/core/kis/KisTokenClient.kt, domain/generator/src/main/kotlin/com/few/generator/core/kis/dto/KisTokenRequest.kt, domain/generator/src/main/kotlin/com/few/generator/core/kis/dto/KisTokenResponse.kt, domain/generator/src/main/kotlin/com/few/generator/core/kis/dto/KisStockPriceResponse.kt
Feign 클라이언트 인터페이스 및 토큰/응답 DTO를 추가(엔드포인트 매핑, Gson 직렬화 어노테이션 포함).
도메인 모델 · 상수
domain/generator/src/main/kotlin/com/few/generator/core/kis/Stock.kt, .../StockQuote.kt, .../OverseaStockConstants.kt
Stock/StockQuote 데이터 클래스와 M7/ETF 등 미리 정의된 종목 상수 및 그룹 맵을 추가.
주식 수집 로직
domain/generator/src/main/kotlin/com/few/generator/core/kis/KisStockFetcher.kt
KIS 토큰 발급, 가격 조회, 응답 검증 및 StockQuote 매핑을 수행하는 Fetcher 컴포넌트 추가(에러 핸들링 포함).
이미지 생성기
domain/generator/src/main/kotlin/com/few/generator/core/instagram/NasdaqDailyStockCardGenerator.kt
1080×1080 Instagram 카드 이미지를 렌더링하는 신규 컴포넌트 추가(헤더, ETF/M7 섹션, 로고 등 렌더링 로직 포함).
Instagram 업로드 확장
domain/generator/src/main/kotlin/com/few/generator/core/instagram/InstagramUploader.kt
단일 미디어 컨테이너 생성 메서드 createSingleMediaContainer(imageUrl, caption) 추가 및 기존 패턴에 따른 에러 핸들링 유지.
스케줄링 사용례 및 컨트롤러
domain/generator/src/main/kotlin/com/few/generator/usecase/NasdaqDailyStockCardSchedulingUseCase.kt, domain/generator/src/main/kotlin/com/few/generator/controller/SchedulingController.kt
NYSE 거래일 검증 후 데이터 수집 → 카드 생성 → S3 업로드 → 인스타 게시를 실행하는 스케줄드 유스케이스 및 컨트롤러(스케줄 엔드포인트 추가).
거래일 체크 유틸리티
domain/generator/src/main/kotlin/com/few/generator/support/common/NyseMarketCalendar.kt
주말과 NYSE 휴장일을 기준으로 거래일 여부를 판단하는 컴포넌트 추가(2025~2026년 휴일 목록 포함).
설정
domain/generator/src/main/resources/application-generator-local.yaml, domain/generator/src/main/resources/application-generator-prd.yaml
openfeign 클라이언트(kis-token, kis) 구성 및 scheduling.cron.nasdaq-daily-stock 스케줄 항목 추가.
테스트(단위/통합)
domain/generator/src/test/kotlin/.../KisStockFetcherTest.kt, KisStockFetcherIntegrationTest.kt, NasdaqDailyStockCardSchedulingUseCaseTest.kt, NasdaqStockCardS3UploadIntegrationTest.kt, core/instagram/StockCardGeneratorIntegrationTest.kt, core/instagram/NasdaqDailyStockCardGeneratorTest.kt
KisStockFetcher 단위/통합 테스트, usecase 흐름 테스트, 이미지 생성기 테스트 및 E2E 통합 테스트(이미지 생성·S3 업로드·인스타 연계) 추가.

Sequence Diagram(s)

sequenceDiagram
    participant Scheduler as 스케줄러
    participant Controller as SchedulingController
    participant UseCase as NasdaqDailyStockCardSchedulingUseCase
    participant Calendar as NyseMarketCalendar
    participant Fetcher as KisStockFetcher
    participant Generator as NasdaqDailyStockCardGenerator
    participant S3 as S3Provider
    participant Instagram as InstagramUploader

    Scheduler->>Controller: nasdaqDailyStockScheduling()
    Controller->>UseCase: execute()
    UseCase->>Calendar: isTradingDay(date)
    alt trading day
        Calendar-->>UseCase: true
        UseCase->>Fetcher: fetchAll()
        Fetcher-->>UseCase: Map<StockGroup,List<StockQuote>>
        UseCase->>Generator: generateImage(stocks, outputPath)
        Generator-->>UseCase: success:Boolean
        alt image success
            UseCase->>S3: uploadImages([outputPath])
            S3-->>UseCase: List<UploadedResult>
            alt upload success
                UseCase->>Instagram: createSingleMediaContainer(s3Url, caption)
                Instagram-->>UseCase: containerId
                alt containerId present
                    UseCase->>Instagram: publishMedia(containerId)
                    Instagram-->>UseCase: published
                else
                    UseCase-->>UseCase: RuntimeException
                end
            else
                UseCase-->>UseCase: RuntimeException
            end
        else
            UseCase-->>UseCase: RuntimeException
        end
    else
        Calendar-->>UseCase: false
        UseCase-->>UseCase: 종료(휴장일)
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 13.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed PR 제목은 새벽 나스닥 종목 스케줄러 추가를 명확하게 설명하며, 변경사항의 주요 내용을 정확히 반영합니다.
Linked Issues check ✅ Passed Issue #921의 요구사항(나스닥 상위 5개 종목 크롤링 및 인스타 업로드)이 구현에 포함되어 있습니다.
Out of Scope Changes check ✅ Passed 모든 변경사항이 나스닥 종목 스케줄러 구현과 관련된 범위 내에 있습니다.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/#921

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@gemini-code-assist

Copy link
Copy Markdown

Summary of Changes

Hello, 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!

이 PR은 나스닥 주요 종목의 일간 주식 정보를 자동으로 생성하고 인스타그램에 게시하는 기능을 도입합니다. KIS API를 통해 실시간 주식 데이터를 조회하고, 이를 바탕으로 시각적으로 매력적인 주식 카드 이미지를 생성합니다. 생성된 이미지는 S3에 업로드된 후 인스타그램에 자동으로 게시되며, NYSE 휴장일에는 스케줄러가 실행되지 않도록 시장 개장일을 확인하는 로직도 포함되어 있습니다.

Highlights

  • 나스닥 일간 주식 정보 스케줄러 추가: 나스닥 주요 종목의 일간 주식 정보를 자동으로 생성하고 인스타그램에 게시하는 새로운 스케줄러가 추가되었습니다.
  • 주식 카드 이미지 생성기 구현: 인스타그램에 최적화된 나스닥 주식 카드 이미지를 생성하는 NasdaqDailyStockCardGenerator가 구현되었습니다.
  • KIS API 연동: KIS API를 통해 실시간 주식 시세 및 인증 토큰을 가져오는 기능이 통합되었습니다.
  • NYSE 개장일 확인 로직 추가: 미국 증권 거래소(NYSE)의 개장일을 확인하여 휴장일에는 스케줄러가 실행되지 않도록 하는 NyseMarketCalendar 유틸리티가 추가되었습니다.
  • Instagram 단일 이미지 게시 기능 확장: InstagramUploader에 단일 이미지 게시를 위한 createSingleMediaContainer 메서드가 추가되었습니다.
  • 통합 및 단위 테스트 추가: 새로운 KIS API 데이터 가져오기, 이미지 생성, S3 업로드 및 스케줄링 기능에 대한 포괄적인 통합 및 단위 테스트가 추가되었습니다.
Changelog
  • domain/generator/src/main/kotlin/com/few/generator/controller/SchedulingController.kt
    • 새로운 NasdaqDailyStockCardSchedulingUseCase를 주입하고, @scheduled 어노테이션을 사용하여 나스닥 일간 주식 스케줄링 메서드를 추가했습니다.
  • domain/generator/src/main/kotlin/com/few/generator/core/instagram/InstagramUploader.kt
    • 단일 이미지 게시를 위한 createSingleMediaContainer 메서드를 추가하여 인스타그램 업로드 기능을 확장했습니다.
  • domain/generator/src/main/kotlin/com/few/generator/core/instagram/NasdaqDailyStockCardGenerator.kt
    • 나스닥 주요 종목의 주식 카드 이미지를 생성하는 로직을 담은 새로운 클래스를 추가했습니다.
  • domain/generator/src/main/kotlin/com/few/generator/core/kis/KisClient.kt
    • KIS API에서 주식 시세 정보를 가져오기 위한 Feign 클라이언트 인터페이스를 정의했습니다.
  • domain/generator/src/main/kotlin/com/few/generator/core/kis/KisStockFetcher.kt
    • KIS API를 통해 주식 데이터를 조회하고 처리하는 로직을 구현한 클래스를 추가했습니다.
  • domain/generator/src/main/kotlin/com/few/generator/core/kis/KisTokenClient.kt
    • KIS API 인증 토큰을 발급받기 위한 Feign 클라이언트 인터페이스를 정의했습니다.
  • domain/generator/src/main/kotlin/com/few/generator/core/kis/OverseaStockConstants.kt
    • KIS API 관련 상수와 M7, ETF 등 나스닥 주요 종목 그룹 정보를 정의한 객체를 추가했습니다.
  • domain/generator/src/main/kotlin/com/few/generator/core/kis/Stock.kt
    • 주식 티커, 한글 이름, 거래소 코드를 포함하는 Stock 데이터 클래스를 추가했습니다.
  • domain/generator/src/main/kotlin/com/few/generator/core/kis/StockQuote.kt
    • 주식 시세(현재가, 등락률) 및 상승/하락 여부를 포함하는 StockQuote 데이터 클래스를 추가했습니다.
  • domain/generator/src/main/kotlin/com/few/generator/core/kis/dto/KisStockPriceResponse.kt
    • KIS 주식 시세 API 응답을 위한 DTO를 정의했습니다.
  • domain/generator/src/main/kotlin/com/few/generator/core/kis/dto/KisTokenRequest.kt
    • KIS 토큰 발급 요청을 위한 DTO를 정의했습니다.
  • domain/generator/src/main/kotlin/com/few/generator/core/kis/dto/KisTokenResponse.kt
    • KIS 토큰 발급 응답을 위한 DTO를 정의했습니다.
  • domain/generator/src/main/kotlin/com/few/generator/support/common/NyseMarketCalendar.kt
    • NYSE의 휴장일을 관리하고 특정 날짜가 거래일인지 확인하는 유틸리티 클래스를 추가했습니다.
  • domain/generator/src/main/kotlin/com/few/generator/usecase/NasdaqDailyStockCardSchedulingUseCase.kt
    • 나스닥 일간 주식 카드 생성 및 게시의 전체 워크플로우를 관리하는 유스케이스를 추가했습니다.
  • domain/generator/src/main/resources/application-generator-local.yaml
    • KIS API 연동을 위한 Feign 클라이언트 설정과 나스닥 일간 주식 스케줄러의 cron 표현식을 추가했습니다.
  • domain/generator/src/main/resources/application-generator-prd.yaml
    • KIS API 연동을 위한 Feign 클라이언트 설정과 나스닥 일간 주식 스케줄러의 cron 표현식을 추가했습니다.
  • domain/generator/src/test/kotlin/com/few/generator/core/instagram/StockCardGeneratorIntegrationTest.kt
    • 실제 KIS API 데이터를 사용하여 주식 카드 이미지 생성 기능을 검증하는 통합 테스트를 추가했습니다.
  • domain/generator/src/test/kotlin/com/few/generator/core/kis/KisStockFetcherIntegrationTest.kt
    • 실제 KIS API를 호출하여 주식 데이터를 가져오는 기능을 검증하는 통합 테스트를 추가했습니다.
  • domain/generator/src/test/kotlin/com/few/generator/core/kis/KisStockFetcherTest.kt
    • KIS 주식 데이터 조회 로직에 대한 단위 테스트를 추가했습니다.
  • domain/generator/src/test/kotlin/com/few/generator/usecase/NasdaqDailyStockCardSchedulingUseCaseTest.kt
    • 나스닥 일간 주식 카드 스케줄링 유스케이스의 동작을 검증하는 단위 테스트를 추가했습니다.
  • domain/generator/src/test/kotlin/com/few/generator/usecase/NasdaqStockCardS3UploadIntegrationTest.kt
    • 나스닥 주식 카드 이미지 생성 후 S3 업로드까지의 전체 과정을 검증하는 통합 테스트를 추가했습니다.
Using Gemini Code Assist

The 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 /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

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 .gemini/ folder in the base of the repository. Detailed instructions can be found here.

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

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a new feature to automatically generate and publish daily NASDAQ stock cards to Instagram. Key changes include adding a scheduled task in SchedulingController to trigger the process, implementing a NasdaqDailyStockCardGenerator for image creation, and integrating with the KIS API via KisStockFetcher to retrieve stock data. A new InstagramUploader function handles single image posts, and a NyseMarketCalendar ensures scheduling only occurs on trading days. Review comments highlight several improvement opportunities: handling potential KotlinNullPointerException from toHttpUrlOrNull(), updating a deprecated RequestBody.create() method, replacing magic numbers in DelayUtil.randomDelay() with named constants, addressing hardcoded font names for consistent image rendering, managing hardcoded NYSE holidays more dynamically, and externalizing the image output path from NasdaqDailyStockCardSchedulingUseCase for better flexibility. Additionally, a high-severity concern was raised regarding the placeholder default values for appKey and appSecret in KisStockFetcher, which could pose a security risk if deployed to production without proper configuration.

val request =
Request
.Builder()
.url(url!!)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

toHttpUrlOrNull() 메서드는 null을 반환할 수 있으므로, url!!을 사용하여 강제로 non-null로 처리하는 것은 KotlinNullPointerException을 발생시킬 수 있습니다. null 가능성을 명시적으로 처리하는 것이 좋습니다.

Suggested change
.url(url!!)
.url(url ?: throw RuntimeException("Invalid Instagram media URL constructed"))

Comment on lines +13 to +15
private val appKey: String,
@Value("\${KIS_APP_SECRET:thisis-kis-app-secret}")
private val appSecret: String,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

appKeyappSecret에 "thisis-kis-app-key"와 같은 플레이스홀더 기본값이 설정되어 있습니다. 이는 개발 환경에서 편리할 수 있지만, 실수로 프로덕션 환경에 배포될 경우 보안 위험을 초래할 수 있습니다. 실제 키가 설정되지 않은 경우 애플리케이션이 시작되지 않도록 하거나, 더 안전한 기본값을 사용하는 것을 고려해야 합니다.

Request
.Builder()
.url(url!!)
.post(RequestBody.create(null, ""))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

RequestBody.create(null, "")는 OkHttp 4.x에서 더 이상 사용되지 않는(deprecated) 메서드입니다. RequestBody.create("", null) 또는 "".toRequestBody(null)을 사용하는 것이 좋습니다.

Suggested change
.post(RequestBody.create(null, ""))
.post(RequestBody.create("", null))

"[Instagram][SingleMedia] Creation of MediaContainer Failed: ${errorResponse?.error?.message ?: "Unknown error"}",
)
}
DelayUtil.randomDelay(5, 10)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

DelayUtil.randomDelay(5, 10)에 사용된 5와 10은 매직 넘버입니다. 이 값들이 무엇을 의미하는지 명확히 하거나, 상수로 정의하여 가독성과 유지보수성을 높이는 것이 좋습니다.

*/
@Component
class NasdaqDailyStockCardGenerator {
private val log = KotlinLogging.logger {}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

NasdaqDailyStockCardGeneratorloadKoreanFont 함수에서 "Noto Sans CJK KR" 폰트 이름을 하드코딩하고 있습니다. 이 폰트가 실행 환경에 설치되어 있지 않으면 대체 폰트가 사용되어 이미지 렌더링이 일관되지 않을 수 있습니다. 폰트 경로를 설정 파일에서 관리하거나, 폰트 파일을 애플리케이션 리소스에 포함하여 항상 사용할 수 있도록 하는 것이 좋습니다.

Comment on lines +18 to +41
setOf(
// 2025
LocalDate.of(2025, 1, 1), // New Year's Day
LocalDate.of(2025, 1, 20), // MLK Jr. Day
LocalDate.of(2025, 2, 17), // Presidents' Day
LocalDate.of(2025, 4, 18), // Good Friday
LocalDate.of(2025, 5, 26), // Memorial Day
LocalDate.of(2025, 6, 19), // Juneteenth
LocalDate.of(2025, 7, 4), // Independence Day
LocalDate.of(2025, 9, 1), // Labor Day
LocalDate.of(2025, 11, 27), // Thanksgiving Day
LocalDate.of(2025, 12, 25), // Christmas Day
// 2026
LocalDate.of(2026, 1, 1), // New Year's Day
LocalDate.of(2026, 1, 19), // MLK Jr. Day
LocalDate.of(2026, 2, 16), // Presidents' Day
LocalDate.of(2026, 4, 3), // Good Friday
LocalDate.of(2026, 5, 25), // Memorial Day
LocalDate.of(2026, 6, 19), // Juneteenth
LocalDate.of(2026, 7, 3), // Independence Day (observed, 7/4 is Saturday)
LocalDate.of(2026, 9, 7), // Labor Day
LocalDate.of(2026, 11, 26), // Thanksgiving Day
LocalDate.of(2026, 12, 25), // Christmas Day
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

NYSE_HOLIDAYS가 2025년과 2026년의 휴일로 하드코딩되어 있습니다. 이 목록은 매년 업데이트해야 하므로 유지보수 부담이 큽니다. 외부 API를 통해 휴일 정보를 가져오거나, 동적으로 휴일을 계산하는 로직을 도입하여 유지보수성을 개선하는 것을 고려해볼 수 있습니다.


val date = LocalDate.now()
val dateStr = date.format(DateTimeFormatter.ofPattern("yyyyMMdd"))
val outputPath = "gen_images/${dateStr}_nasdaq_daily_stock.png"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

이미지 저장 경로인 gen_images/가 하드코딩되어 있습니다. 이 경로는 환경에 따라 달라질 수 있으므로, 설정 파일에서 관리하도록 변경하여 유연성을 높이는 것이 좋습니다.

Suggested change
val outputPath = "gen_images/${dateStr}_nasdaq_daily_stock.png"
val outputPath = "${imageOutputPath}/${dateStr}_nasdaq_daily_stock.png"

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 12

🧹 Nitpick comments (4)
domain/generator/src/main/kotlin/com/few/generator/core/instagram/InstagramUploader.kt (1)

83-96: url!! 강제 언래핑 패턴 개선 권장

toHttpUrlOrNull()null을 반환할 경우 url!!에서 NullPointerException이 발생합니다. 기존 코드와 일관성이 있으나, 더 명확한 에러 메시지를 위해 requireNotNull 또는 엘비스 연산자 사용을 고려해 주세요.

♻️ 제안하는 개선안
         val url =
             "https://graph.instagram.com/$accountId/media"
                 .toHttpUrlOrNull()
                 ?.newBuilder()
                 ?.addQueryParameter("access_token", instagramTokenService.getLatestAccessToken())
                 ?.addQueryParameter("image_url", imageUrl)
                 ?.addQueryParameter("caption", caption)
                 ?.build()
+                ?: throw IllegalArgumentException("Failed to build Instagram media URL")

         val request =
             Request
                 .Builder()
-                .url(url!!)
+                .url(url)
                 .post(RequestBody.create(null, ""))
                 .build()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@domain/generator/src/main/kotlin/com/few/generator/core/instagram/InstagramUploader.kt`
around lines 83 - 96, The code force-unwraps the constructed HttpUrl with url!!
in InstagramUploader.kt which can throw an NPE if toHttpUrlOrNull() returns
null; replace the force-unwrap with a null-check using requireNotNull(url) with
a clear error message (e.g., including accountId and imageUrl) or use the Elvis
operator to throw an IllegalArgumentException, and pass that checked value into
Request.Builder().url(...) so the failure is explicit and debuggable.
domain/generator/src/test/kotlin/com/few/generator/core/instagram/StockCardGeneratorIntegrationTest.kt (1)

35-76: 통합테스트 간 Feign 구성 코드가 중복됩니다.

KIS 클라이언트 빌더를 공용 헬퍼로 추출하면 설정 변경 시 한 곳만 수정하면 되어 테스트 안정성이 올라갑니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@domain/generator/src/test/kotlin/com/few/generator/core/instagram/StockCardGeneratorIntegrationTest.kt`
around lines 35 - 76, The Feign client setup is duplicated in buildFetcher();
extract the common configuration (Gson encoder/decoder, SpringMvcContract, and
shared headers) into a single helper used to create both KisTokenClient and
KisClient so updates apply in one place; e.g., add a private helper function
that returns a configured Feign.Builder (or a typed client factory) configured
with contract = SpringMvcContract(), encoder = gsonEncoder, decoder =
gsonDecoder and a parameter to apply requestInterceptor headers
(appKey/appSecret/Content-Type) when building KisClient, then use that helper to
create tokenClient and stockClient passed into KisStockFetcher.
domain/generator/src/test/kotlin/com/few/generator/core/kis/KisStockFetcherIntegrationTest.kt (1)

29-70: Feign 테스트 클라이언트 구성 코드가 중복되어 유지보수 포인트가 분산됩니다.

StockCardGeneratorIntegrationTest와 동일한 빌더 로직을 공용 팩토리/헬퍼로 추출해 두 테스트가 같은 설정을 재사용하도록 정리하는 것을 권장합니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@domain/generator/src/test/kotlin/com/few/generator/core/kis/KisStockFetcherIntegrationTest.kt`
around lines 29 - 70, The Feign client setup duplicated in buildFetcher() should
be extracted into a shared test factory/helper so both
KisStockFetcherIntegrationTest and StockCardGeneratorIntegrationTest reuse the
same configuration; create a test utility (e.g., FeignTestClientFactory or
TestFeignBuilders) that initializes common values (gson, contract, gsonEncoder,
gsonDecoder) and exposes methods to produce KisTokenClient and KisClient or a
preconfigured Feign.Builder, then update buildFetcher() to call that factory
(and update the other test to use it) so the KisStockFetcher construction only
passes kisTokenClient, kisClient, appKey and appSecret.
domain/generator/src/main/kotlin/com/few/generator/usecase/NasdaqDailyStockCardSchedulingUseCase.kt (1)

25-25: 긴 파이프라인 특성상 비동기 진입점 제공을 고려해도 좋습니다.

현재 execute()는 동기 실행입니다. 수동 트리거나 외부 호출 지점이 생길 경우를 대비해 fire-and-forget 비동기 래퍼(executeAsync)를 두면 호출자 블로킹을 줄일 수 있습니다.

Based on learnings AbstractGenSchedulingUseCase의 executeAsync() 메서드는 긴 실행 시간을 고려한 fire-and-forget 패턴으로 의도적으로 Async 구현.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@domain/generator/src/main/kotlin/com/few/generator/usecase/NasdaqDailyStockCardSchedulingUseCase.kt`
at line 25, Add a non-blocking fire-and-forget async wrapper for the
long-running execute() to avoid caller blocking: in
NasdaqDailyStockCardSchedulingUseCase, implement an executeAsync() that launches
execute() on a background thread/coroutine (matching the app's concurrency
model) and immediately returns, handling/logging any thrown exceptions inside
the launched task; follow the pattern used by
AbstractGenSchedulingUseCase.executeAsync() for error handling and lifecycle
behavior so callers can trigger scheduling without being blocked by the long
pipeline.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@domain/generator/src/main/kotlin/com/few/generator/controller/SchedulingController.kt`:
- Around line 53-56: Wrap the body of nasdaqDailyStockScheduling() in a
try-catch similar to refreshInstagramToken(): call
nasdaqDailyStockCardSchedulingUseCase.execute() inside try, catch
RuntimeException (or Exception) and log the error with context (include
exception message/stack) so failures (image generation, S3 upload, Instagram
container creation) are recorded and do not bubble up to break the scheduler;
optionally rethrow only if needed but prefer swallowing after logging to keep
the scheduler running.

In
`@domain/generator/src/main/kotlin/com/few/generator/core/kis/dto/KisStockPriceResponse.kt`:
- Around line 6-16: KisStockPriceResponse and its nested Output rely on Gson
`@SerializedName` which Jackson ignores; add Jackson annotations to ensure proper
deserialization: annotate the top-level property rtCd with
`@JsonProperty`("rt_cd") and the Output property t_xrat (and any other snake_case
fields) with `@JsonProperty`("t_xrat"), import
com.fasterxml.jackson.annotation.JsonProperty, and keep the existing
`@SerializedName` if you need backward compatibility with Gson.

In
`@domain/generator/src/main/kotlin/com/few/generator/core/kis/KisStockFetcher.kt`:
- Around line 12-15: The current KisStockFetcher injects dummy default
credentials via `@Value`("\${KIS_APP_KEY:thisis-kis-app-key}") and
`@Value`("\${KIS_APP_SECRET:thisis-kis-app-secret}"), allowing the app to start
and only failing later in issueToken(); remove those dummy defaults so missing
env vars are detected at startup and add a fail-fast validation in
KisStockFetcher (e.g., init block or constructor) that checks appKey and
appSecret and throws an IllegalStateException with a clear message if either is
blank or equals the placeholder, ensuring the service fails on boot rather than
at runtime in issueToken().

In
`@domain/generator/src/main/kotlin/com/few/generator/core/kis/OverseaStockConstants.kt`:
- Around line 13-30: 현재 DAILY_NASDAQ_STOCK_GROUP_MAP가 M7 그룹에 7개(AAPL, MSFT,
GOOGL, AMZN, NVDA, META, TSLA)를 포함해 총 10개 종목을 고정하고 있는데 요구사항은 “나스닥 상위 5개”입니다;
StockGroup.M7의 리스트를 나스닥 상위 5개 종목만 포함하도록 축소(예: AAPL, MSFT, GOOGL, AMZN, NVDA 또는
최신 요구에 따른 정확한 5종목)하고 불필요한 상수(META, TSLA 등)를 제거하거나 사용처가 있으면 다른 그룹으로 옮기며
DAILY_NASDAQ_STOCK_GROUP_MAP 정의와 관련 주석을 이에 맞게 업데이트하세요.

In
`@domain/generator/src/main/kotlin/com/few/generator/support/common/NyseMarketCalendar.kt`:
- Around line 17-41: NYSE_HOLIDAYS is hard-coded only through 2026 so dates
>=2027 can be treated as trading days; update NyseMarketCalendar to prevent
silent failures by either extending holiday data or adding a year-bounds check.
Modify the code paths that consult NYSE_HOLIDAYS (e.g., isHoliday(date),
isTradingDay(date), nextTradingDay(...) or any method using NYSE_HOLIDAYS) to
validate the date.year is within the supported range and: either throw an
IllegalStateException with a clear message asking to update NYSE_HOLIDAYS for
newer years, or replace the static set with a dynamic holiday generator that
computes observed NYSE holidays for arbitrary years. Ensure the check references
NYSE_HOLIDAYS (or the new generator) so callers fail-fast instead of
misclassifying holidays.

In
`@domain/generator/src/main/kotlin/com/few/generator/usecase/NasdaqDailyStockCardSchedulingUseCase.kt`:
- Around line 74-76: The call instagramUploader.publishMedia(containerId)
currently ignores its result so failures are logged as successes; update
NasdaqDailyStockCardSchedulingUseCase to inspect the return value or caught
exception from instagramUploader.publishMedia(containerId) and branch
accordingly — if publishMedia indicates failure (or throws), log an error and
rethrow or handle the failure path instead of logging "Instagram 나스닥 주식 카드 게시
완료"; ensure success logging only occurs when publishMedia reports success and
add appropriate exception handling around the call to avoid false-positive
completion logs.
- Around line 50-60: The upload call in NasdaqDailyStockCardSchedulingUseCase
currently calls s3Provider.uploadImages(listOf(outputPath)) without guaranteeing
local cleanup if an exception occurs; wrap the uploadImages call in a
try/finally so that the File(outputPath) deletion logic always runs (move the
existing File(outputPath).takeIf { it.exists() }?.let { ... } block into the
finally), keeping the log.info before the try and preserving uploadResult
assignment inside the try so exceptions propagate but the local file is deleted
regardless.
- Around line 26-34: The code uses usDate
(LocalDate.now(ZoneId.of("America/New_York"))) to check NYSE trading day but
then uses LocalDate.now() for filename/caption, causing date drift; change the
filename/caption logic to derive date/dateStr/outputPath from usDate (replace
LocalDate.now() usage) and pass that same usDate-based value into
buildCaption(date) so outputPath, dateStr and buildCaption all use the same
usDate reference.

In
`@domain/generator/src/test/kotlin/com/few/generator/core/instagram/StockCardGeneratorIntegrationTest.kt`:
- Around line 120-122: Replace the Kotlin JVM assert() checks with Kotest
matchers to avoid silent skips when -ea isn’t enabled: in
StockCardGeneratorIntegrationTest.kt, replace the two assertions around
outputFile (the variable created from outputPath) with Kotest assertions such as
outputFile.exists() shouldBe true and outputFile.length() shouldBeGreaterThan
0L, and add the necessary Kotest matcher imports (e.g.,
io.kotest.matchers.shouldBe and
io.kotest.matchers.comparables.shouldBeGreaterThan) so the test consistently
fails when the file is missing or empty.

In
`@domain/generator/src/test/kotlin/com/few/generator/core/kis/KisStockFetcherTest.kt`:
- Around line 165-176: The test description in KisStockFetcherTest ("일부 종목 API
호출이 FeignException으로 실패할 때") does not match the mocked exception
(RuntimeException("simulated feign error")) thrown from the
kisClient.getStockPrice stub; either change the Given text to reference
RuntimeException or update the stub to throw a FeignException instance instead
(e.g., replace throws RuntimeException(...) with throws FeignException (created
via FeignException.errorStatus or an appropriate FeignException constructor) for
the kisClient.getStockPrice mock).

In
`@domain/generator/src/test/kotlin/com/few/generator/usecase/NasdaqStockCardS3UploadIntegrationTest.kt`:
- Around line 50-51: 테스트에 하드코딩된 S3 버킷/리전 값(bucket, region)을 환경변수로 분리하고, 해당 env
vars가 없으면 테스트를 건너뛰도록 변경하세요: NasdaqStockCardS3UploadIntegrationTest 내에 사용 중인
변수(예: bucket, region 또는 테스트 메서드들)를 S3_TEST_BUCKET/S3_TEST_REGION 같은 환경변수로 읽어오게
바꾸고, 값이 비어있으면 JUnit의 assumeTrue/assumeFalse 또는 테스트 스킵 로직으로 즉시 반환하여 실제 운영 스토리지로
업로드되지 않게 하며 같은 패턴을 파일의 다른 하드코딩 위치들(언급된 다른 블록들: 103-104, 128-131에 해당하는 변수들)에도
동일하게 적용하세요.
- Around line 166-176: The local cleanup is skipped if uploadImages throws; wrap
the S3 upload and its logging in a try/finally so that File(outputPath).takeIf {
it.exists() }?.delete() always runs; specifically, keep using buildS3Provider()
and call s3Provider.uploadImages(...) inside the try, perform println of
uploadResult inside the try, and move the single-file deletion
(File(outputPath)...delete()) into the finally block to guarantee deletion
regardless of exceptions from uploadImages.

---

Nitpick comments:
In
`@domain/generator/src/main/kotlin/com/few/generator/core/instagram/InstagramUploader.kt`:
- Around line 83-96: The code force-unwraps the constructed HttpUrl with url!!
in InstagramUploader.kt which can throw an NPE if toHttpUrlOrNull() returns
null; replace the force-unwrap with a null-check using requireNotNull(url) with
a clear error message (e.g., including accountId and imageUrl) or use the Elvis
operator to throw an IllegalArgumentException, and pass that checked value into
Request.Builder().url(...) so the failure is explicit and debuggable.

In
`@domain/generator/src/main/kotlin/com/few/generator/usecase/NasdaqDailyStockCardSchedulingUseCase.kt`:
- Line 25: Add a non-blocking fire-and-forget async wrapper for the long-running
execute() to avoid caller blocking: in NasdaqDailyStockCardSchedulingUseCase,
implement an executeAsync() that launches execute() on a background
thread/coroutine (matching the app's concurrency model) and immediately returns,
handling/logging any thrown exceptions inside the launched task; follow the
pattern used by AbstractGenSchedulingUseCase.executeAsync() for error handling
and lifecycle behavior so callers can trigger scheduling without being blocked
by the long pipeline.

In
`@domain/generator/src/test/kotlin/com/few/generator/core/instagram/StockCardGeneratorIntegrationTest.kt`:
- Around line 35-76: The Feign client setup is duplicated in buildFetcher();
extract the common configuration (Gson encoder/decoder, SpringMvcContract, and
shared headers) into a single helper used to create both KisTokenClient and
KisClient so updates apply in one place; e.g., add a private helper function
that returns a configured Feign.Builder (or a typed client factory) configured
with contract = SpringMvcContract(), encoder = gsonEncoder, decoder =
gsonDecoder and a parameter to apply requestInterceptor headers
(appKey/appSecret/Content-Type) when building KisClient, then use that helper to
create tokenClient and stockClient passed into KisStockFetcher.

In
`@domain/generator/src/test/kotlin/com/few/generator/core/kis/KisStockFetcherIntegrationTest.kt`:
- Around line 29-70: The Feign client setup duplicated in buildFetcher() should
be extracted into a shared test factory/helper so both
KisStockFetcherIntegrationTest and StockCardGeneratorIntegrationTest reuse the
same configuration; create a test utility (e.g., FeignTestClientFactory or
TestFeignBuilders) that initializes common values (gson, contract, gsonEncoder,
gsonDecoder) and exposes methods to produce KisTokenClient and KisClient or a
preconfigured Feign.Builder, then update buildFetcher() to call that factory
(and update the other test to use it) so the KisStockFetcher construction only
passes kisTokenClient, kisClient, appKey and appSecret.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 79953d81-c787-4b12-95c4-354a2f7738db

📥 Commits

Reviewing files that changed from the base of the PR and between 521a8f1 and 7a5be56.

⛔ Files ignored due to path filters (7)
  • domain/generator/src/main/resources/images/m7/aapl_logo.png is excluded by !**/*.png
  • domain/generator/src/main/resources/images/m7/amzn_logo.png is excluded by !**/*.png
  • domain/generator/src/main/resources/images/m7/googl_logo.png is excluded by !**/*.png
  • domain/generator/src/main/resources/images/m7/meta_logo.png is excluded by !**/*.png
  • domain/generator/src/main/resources/images/m7/msft_logo.png is excluded by !**/*.png
  • domain/generator/src/main/resources/images/m7/nvda_logo.png is excluded by !**/*.png
  • domain/generator/src/main/resources/images/m7/tsla_logo.png is excluded by !**/*.png
📒 Files selected for processing (21)
  • domain/generator/src/main/kotlin/com/few/generator/controller/SchedulingController.kt
  • domain/generator/src/main/kotlin/com/few/generator/core/instagram/InstagramUploader.kt
  • domain/generator/src/main/kotlin/com/few/generator/core/instagram/NasdaqDailyStockCardGenerator.kt
  • domain/generator/src/main/kotlin/com/few/generator/core/kis/KisClient.kt
  • domain/generator/src/main/kotlin/com/few/generator/core/kis/KisStockFetcher.kt
  • domain/generator/src/main/kotlin/com/few/generator/core/kis/KisTokenClient.kt
  • domain/generator/src/main/kotlin/com/few/generator/core/kis/OverseaStockConstants.kt
  • domain/generator/src/main/kotlin/com/few/generator/core/kis/Stock.kt
  • domain/generator/src/main/kotlin/com/few/generator/core/kis/StockQuote.kt
  • domain/generator/src/main/kotlin/com/few/generator/core/kis/dto/KisStockPriceResponse.kt
  • domain/generator/src/main/kotlin/com/few/generator/core/kis/dto/KisTokenRequest.kt
  • domain/generator/src/main/kotlin/com/few/generator/core/kis/dto/KisTokenResponse.kt
  • domain/generator/src/main/kotlin/com/few/generator/support/common/NyseMarketCalendar.kt
  • domain/generator/src/main/kotlin/com/few/generator/usecase/NasdaqDailyStockCardSchedulingUseCase.kt
  • domain/generator/src/main/resources/application-generator-local.yaml
  • domain/generator/src/main/resources/application-generator-prd.yaml
  • domain/generator/src/test/kotlin/com/few/generator/core/instagram/StockCardGeneratorIntegrationTest.kt
  • domain/generator/src/test/kotlin/com/few/generator/core/kis/KisStockFetcherIntegrationTest.kt
  • domain/generator/src/test/kotlin/com/few/generator/core/kis/KisStockFetcherTest.kt
  • domain/generator/src/test/kotlin/com/few/generator/usecase/NasdaqDailyStockCardSchedulingUseCaseTest.kt
  • domain/generator/src/test/kotlin/com/few/generator/usecase/NasdaqStockCardS3UploadIntegrationTest.kt
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: api-test
  • GitHub Check: lint
  • GitHub Check: Build and analyze
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2026-01-17T01:53:02.072Z
Learnt from: hun-ca
Repo: few-letter/few-be PR: 883
File: domain/generator/src/main/kotlin/com/few/generator/usecase/UploadGenCardNewsS3UseCase.kt:28-30
Timestamp: 2026-01-17T01:53:02.072Z
Learning: `UploadGenCardNewsS3UseCase`에서 S3 업로드 후 성공/실패 여부와 관계없이 모든 로컬 이미지 파일(`event.imagePaths`)을 삭제하는 것이 의도된 동작임. 실패한 파일도 포함하여 임시 파일을 정리하는 것이 설계 의도.

Applied to files:

  • domain/generator/src/test/kotlin/com/few/generator/usecase/NasdaqStockCardS3UploadIntegrationTest.kt
📚 Learning: 2026-01-22T09:30:57.808Z
Learnt from: hun-ca
Repo: few-letter/few-be PR: 906
File: domain/generator/src/main/kotlin/com/few/generator/usecase/AbstractGenSchedulingUseCase.kt:35-36
Timestamp: 2026-01-22T09:30:57.808Z
Learning: AbstractGenSchedulingUseCase의 executeAsync() 메서드는 컨텐츠 생성 작업의 긴 실행 시간으로 인해 의도적으로 Async로 구현되어 있으며, API 호출자에게 즉시 응답을 반환하는 fire-and-forget 패턴을 사용합니다.

Applied to files:

  • domain/generator/src/main/kotlin/com/few/generator/usecase/NasdaqDailyStockCardSchedulingUseCase.kt
🔇 Additional comments (12)
domain/generator/src/main/resources/application-generator-prd.yaml (2)

64-64: LGTM!

Cron 표현식 "0 0 6 * * *"이 매일 오전 6시에 실행되도록 올바르게 설정되어 있습니다.


22-33: 설정 구조가 올바르게 설계됨 - 추가 조치 불필요

kis-tokenkis 클라이언트는 동일한 기본 URL과 헤더를 공유하지만, 이는 의도된 설계입니다. 두 클라이언트는 서로 다른 목적을 수행합니다:

  • kis-token: 토큰 획득 (POST /oauth2/tokenP)
  • kis: 주식 가격 조회 (GET /uapi/overseas-price/v1/quotations/price-detail)

kis 클라이언트는 authorization 헤더를 메서드 매개변수로 요구하며, 이는 런타임에 동적으로 주입됩니다. 기본 설정에 Authorization 헤더가 없는 것은 정상입니다.

domain/generator/src/main/kotlin/com/few/generator/core/instagram/InstagramUploader.kt (1)

78-112: LGTM!

createSingleMediaContainer 메서드가 기존 패턴을 일관되게 따르며, 에러 처리와 응답 파싱이 적절하게 구현되어 있습니다.

domain/generator/src/main/resources/application-generator-local.yaml (1)

22-33: LGTM!

로컬 환경 설정이 운영 환경 설정과 일관되게 구성되어 있습니다.

Also applies to: 66-66

domain/generator/src/main/kotlin/com/few/generator/core/kis/Stock.kt (1)

1-7: LGTM!

간결하고 명확한 데이터 클래스입니다. 주석이 각 필드의 용도를 잘 설명하고 있습니다.

domain/generator/src/main/kotlin/com/few/generator/core/kis/dto/KisTokenResponse.kt (1)

1-12: LGTM!

KIS API 토큰 응답을 위한 적절한 DTO 구조입니다.

domain/generator/src/main/kotlin/com/few/generator/core/kis/StockQuote.kt (1)

3-19: LGTM!

isRise 계산 속성이 상승(+), 하락(-), 보합(null) 세 가지 상태를 명확하게 처리합니다. NasdaqDailyStockCardGenerator에서 when 표현식으로 모든 경우를 적절히 처리하고 있어 안전합니다.

domain/generator/src/main/kotlin/com/few/generator/core/kis/dto/KisTokenRequest.kt (1)

1-12: LGTM!

KIS API 토큰 요청을 위한 적절한 DTO 구조입니다. grantType의 기본값 설정이 OAuth2 표준을 따릅니다.

domain/generator/src/main/kotlin/com/few/generator/core/kis/KisTokenClient.kt (1)

10-17: 토큰 발급 Feign 계약이 명확하게 정의되어 있습니다.

엔드포인트/요청 바디/응답 모델 연결이 깔끔하고 테스트 코드와도 일관됩니다.

domain/generator/src/main/kotlin/com/few/generator/core/kis/KisClient.kt (1)

10-20: KIS 시세 조회 계약이 간결하고 명확합니다.

요청 헤더와 파라미터가 시그니처에 잘 드러나 있어 호출부 추적성이 좋습니다.

domain/generator/src/test/kotlin/com/few/generator/usecase/NasdaqDailyStockCardSchedulingUseCaseTest.kt (1)

35-244: 성공/실패/휴장일 분기 커버리지가 좋습니다.

핵심 파이프라인의 정상 경로와 주요 실패 경로를 모두 검증해 회귀 방지에 효과적입니다.

domain/generator/src/main/kotlin/com/few/generator/core/instagram/NasdaqDailyStockCardGenerator.kt (1)

97-126: 레이아웃 분리와 그래픽 리소스 정리가 잘 되어 있습니다.

섹션별 메서드 분리로 가독성이 좋고, finally에서 graphics.dispose()를 보장한 점이 안정적입니다.

Also applies to: 128-348

Comment on lines +53 to +56
@Scheduled(cron = "\${scheduling.cron.nasdaq-daily-stock}", zone = "Asia/Seoul")
fun nasdaqDailyStockScheduling() {
nasdaqDailyStockCardSchedulingUseCase.execute()
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

스케줄러 예외 처리 누락

nasdaqDailyStockScheduling()에 예외 처리가 없습니다. NasdaqDailyStockCardSchedulingUseCase.execute()는 이미지 생성 실패, S3 업로드 실패, Instagram 컨테이너 생성 실패 시 RuntimeException을 발생시킵니다.

refreshInstagramToken()(Line 44-51)과 달리 try-catch가 없어, 예외 발생 시 로깅이 누락되고 스케줄러 동작에 영향을 줄 수 있습니다.

🛡️ 예외 처리 추가 제안
     `@Scheduled`(cron = "\${scheduling.cron.nasdaq-daily-stock}", zone = "Asia/Seoul")
     fun nasdaqDailyStockScheduling() {
-        nasdaqDailyStockCardSchedulingUseCase.execute()
+        try {
+            nasdaqDailyStockCardSchedulingUseCase.execute()
+        } catch (e: Exception) {
+            log.error(e) { "나스닥 주식 카드 스케줄 실행 중 오류 발생: ${e.message}" }
+        }
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@domain/generator/src/main/kotlin/com/few/generator/controller/SchedulingController.kt`
around lines 53 - 56, Wrap the body of nasdaqDailyStockScheduling() in a
try-catch similar to refreshInstagramToken(): call
nasdaqDailyStockCardSchedulingUseCase.execute() inside try, catch
RuntimeException (or Exception) and log the error with context (include
exception message/stack) so failures (image generation, S3 upload, Instagram
container creation) are recorded and do not bubble up to break the scheduler;
optionally rethrow only if needed but prefer swallowing after logging to keep
the scheduler running.

Comment on lines +6 to +16
@SerializedName("rt_cd")
val rtCd: String,
@SerializedName("msg1")
val msg1: String,
val output: Output?,
) {
data class Output(
/** 현재가 */
val last: String,
/** 원환산 등락률 (%) */
val t_xrat: String,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
fd -HI 'build.gradle.kts|build.gradle|pom.xml|application*.yml|application*.yaml'
rg -n -C2 'spring\\.cloud\\.openfeign|Feign|GsonDecoder|feign-gson|GsonHttpMessageConverter|preferred-json-mapper|jackson'

Repository: few-letter/few-be

Length of output: 21446


🏁 Script executed:

cat -n domain/generator/src/main/kotlin/com/few/generator/config/GeneratorFeignConfig.kt

Repository: few-letter/few-be

Length of output: 876


🏁 Script executed:

cat -n domain/generator/src/main/kotlin/com/few/generator/core/kis/dto/KisStockPriceResponse.kt

Repository: few-letter/few-be

Length of output: 630


🏁 Script executed:

cat -n api/src/main/resources/application.yml

Repository: few-letter/few-be

Length of output: 1555


🏁 Script executed:

grep -r "feign\|gson\|jackson" domain/generator/build.gradle.kts build.gradle.kts

Repository: few-letter/few-be

Length of output: 426


🏁 Script executed:

cat -n build.gradle.kts | grep -A 20 "spring.cloud.starter.openfeign\|dependencyManagement\|springCloud"

Repository: few-letter/few-be

Length of output: 997


🏁 Script executed:

grep -r "GsonDecoder\|JsonDecoder\|custom.*Decoder\|feign.*decoder" domain/generator/src/main/kotlin/

Repository: few-letter/few-be

Length of output: 43


🌐 Web query:

Spring Cloud OpenFeign 2023.0.4 default decoder Jackson vs Gson

💡 Result:

In Spring Cloud OpenFeign 2023.0.4, the default feign.Decoder is not “Jackson vs Gson” directly. It’s:

OptionalDecoder -> ResponseEntityDecoder -> SpringDecoder(messageConverters, …) as defined in FeignClientsConfiguration#feignDecoder(...) (so it delegates JSON decoding to Spring Boot’s HttpMessageConverters). [1]

So the effective JSON library is whichever JSON HttpMessageConverter Spring Boot configures:

  • Default is Jackson (Spring Boot prefers Jackson and auto-configures it via spring-boot-starter-json). [2]
  • You can switch the JSON mapper Spring MVC / imperative HTTP clients use (which includes the converters OpenFeign’s SpringDecoder relies on) with spring.http.converters.preferred-json-mapper (e.g., gson vs jackson). [2]

Bottom line: with the usual Spring Boot setup, OpenFeign’s default decoding will use Jackson; it will use Gson only if Spring Boot’s configured JSON converters are Gson (e.g., via the preferred-json-mapper setting and having Gson available). [1][2]

Sources:
[1] Spring Cloud OpenFeign FeignClientsConfiguration (default decoder wiring) https://raw.githubusercontent.com/spring-cloud/spring-cloud-openfeign/a00941b5/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignClientsConfiguration.java
[2] Spring Boot reference docs – JSON (Jackson default; preferred-json-mapper property) https://docs.spring.io/spring-boot/reference/features/json.html


운영 환경에서 Jackson 디코더 사용으로 인해 필드 매핑이 실패할 수 있습니다.

rt_cd/t_xrat 필드는 Gson @SerializedName 어노테이션에만 의존 중입니다. 그러나 Spring Cloud OpenFeign 2023.0.4는 기본으로 Jackson을 JSON 디코더로 사용하므로, Gson 어노테이션이 무시되어 응답 역직렬화가 실패할 수 있습니다. Jackson은 필드명의 정확한 매칭(또는 케이싱 규칙)을 요구하므로, rt_cd 같은 스네이크케이스 필드명이 rtCd 필드로 자동 매핑되지 않습니다.

🔧 제안 수정안 (Jackson 호환)
+import com.fasterxml.jackson.annotation.JsonProperty
 import com.google.gson.annotations.SerializedName

 data class KisStockPriceResponse(
-    `@SerializedName`("rt_cd")
+    `@JsonProperty`("rt_cd")
     val rtCd: String,
-    `@SerializedName`("msg1")
+    `@JsonProperty`("msg1")
     val msg1: String,
     val output: Output?,
 ) {
     data class Output(
         /** 현재가 */
         val last: String,
         /** 원환산 등락률 (%) */
-        val t_xrat: String,
+        `@JsonProperty`("t_xrat")
+        val tXrat: String,
     )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@domain/generator/src/main/kotlin/com/few/generator/core/kis/dto/KisStockPriceResponse.kt`
around lines 6 - 16, KisStockPriceResponse and its nested Output rely on Gson
`@SerializedName` which Jackson ignores; add Jackson annotations to ensure proper
deserialization: annotate the top-level property rtCd with
`@JsonProperty`("rt_cd") and the Output property t_xrat (and any other snake_case
fields) with `@JsonProperty`("t_xrat"), import
com.fasterxml.jackson.annotation.JsonProperty, and keep the existing
`@SerializedName` if you need backward compatibility with Gson.

Comment on lines +12 to +15
@Value("\${KIS_APP_KEY:thisis-kis-app-key}")
private val appKey: String,
@Value("\${KIS_APP_SECRET:thisis-kis-app-secret}")
private val appSecret: String,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n -C2 '\$\{KIS_APP_KEY:|\$\{KIS_APP_SECRET:' domain/generator/src/main/kotlin
rg -n -C2 'KIS_APP_KEY|KIS_APP_SECRET' domain/generator/src/main/resources/application-generator-local.yaml domain/generator/src/main/resources/application-generator-prd.yaml

Repository: few-letter/few-be

Length of output: 3764


🏁 Script executed:

cat -n domain/generator/src/main/kotlin/com/few/generator/core/kis/KisStockFetcher.kt

Repository: few-letter/few-be

Length of output: 2828


🏁 Script executed:

rg -n 'appKey|appSecret' domain/generator/src/main/kotlin/com/few/generator/core/kis/KisStockFetcher.kt -A 2

Repository: few-letter/few-be

Length of output: 375


KIS 자격증명에 더미 기본값을 설정하면 운영 장애를 늦게 드러냅니다.

환경변수가 설정되지 않으면 더미값(thisis-kis-app-key, thisis-kis-app-secret)이 주입되어 애플리케이션이 정상적으로 시작됩니다. 실제 실패는 issueToken() 메서드가 실행되는 런타임 단계에서 KIS API 토큰 발급 시도 시 발생합니다.

설정 누락을 시작 시점에 즉시 감지하도록 변경하세요:

🔒 제안 수정안 (fail-fast)
 class KisStockFetcher(
     private val kisTokenClient: KisTokenClient,
     private val kisClient: KisClient,
-    `@Value`("\${KIS_APP_KEY:thisis-kis-app-key}")
+    `@Value`("\${KIS_APP_KEY}")
     private val appKey: String,
-    `@Value`("\${KIS_APP_SECRET:thisis-kis-app-secret}")
+    `@Value`("\${KIS_APP_SECRET}")
     private val appSecret: String,
 ) {
     private val log = KotlinLogging.logger {}
+
+    init {
+        require(appKey.isNotBlank()) { "KIS_APP_KEY must not be blank" }
+        require(appSecret.isNotBlank()) { "KIS_APP_SECRET must not be blank" }
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@Value("\${KIS_APP_KEY:thisis-kis-app-key}")
private val appKey: String,
@Value("\${KIS_APP_SECRET:thisis-kis-app-secret}")
private val appSecret: String,
class KisStockFetcher(
private val kisTokenClient: KisTokenClient,
private val kisClient: KisClient,
`@Value`("\${KIS_APP_KEY}")
private val appKey: String,
`@Value`("\${KIS_APP_SECRET}")
private val appSecret: String,
) {
private val log = KotlinLogging.logger {}
init {
require(appKey.isNotBlank()) { "KIS_APP_KEY must not be blank" }
require(appSecret.isNotBlank()) { "KIS_APP_SECRET must not be blank" }
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@domain/generator/src/main/kotlin/com/few/generator/core/kis/KisStockFetcher.kt`
around lines 12 - 15, The current KisStockFetcher injects dummy default
credentials via `@Value`("\${KIS_APP_KEY:thisis-kis-app-key}") and
`@Value`("\${KIS_APP_SECRET:thisis-kis-app-secret}"), allowing the app to start
and only failing later in issueToken(); remove those dummy defaults so missing
env vars are detected at startup and add a fail-fast validation in
KisStockFetcher (e.g., init block or constructor) that checks appKey and
appSecret and throws an IllegalStateException with a clear message if either is
blank or equals the placeholder, ensuring the service fails on boot rather than
at runtime in issueToken().

Comment on lines +13 to +30
val AAPL = Stock("AAPL", "애플", EXCD_NAS)
val MSFT = Stock("MSFT", "마이크로소프트", EXCD_NAS)
val GOOGL = Stock("GOOGL", "알파벳", EXCD_NAS)
val AMZN = Stock("AMZN", "아마존", EXCD_NAS)
val NVDA = Stock("NVDA", "엔비디아", EXCD_NAS)
val META = Stock("META", "메타", EXCD_NAS)
val TSLA = Stock("TSLA", "테슬라", EXCD_NAS)

/** ETF 개별 종목 */
val SPY = Stock("SPY", "S&P500", EXCD_AMS)
val QQQ = Stock("QQQ", "나스닥100", EXCD_NAS)
val SCHD = Stock("SCHD", "다우존스", EXCD_AMS)

val DAILY_NASDAQ_STOCK_GROUP_MAP: Map<StockGroup, List<Stock>> =
mapOf(
StockGroup.ETF to listOf(SPY, QQQ, SCHD),
StockGroup.M7 to listOf(AAPL, MSFT, GOOGL, AMZN, NVDA, META, TSLA),
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

요구사항(상위 5개)과 현재 고정 종목 구성(10개)이 불일치합니다.

Issue #921 기준으로는 “나스닥 상위 5개”가 목표인데, 현재는 M7+ETF 10개를 고정 조회합니다. 실제 요구가 바뀐 게 아니라면 기능 스코프를 재정렬해야 합니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@domain/generator/src/main/kotlin/com/few/generator/core/kis/OverseaStockConstants.kt`
around lines 13 - 30, 현재 DAILY_NASDAQ_STOCK_GROUP_MAP가 M7 그룹에 7개(AAPL, MSFT,
GOOGL, AMZN, NVDA, META, TSLA)를 포함해 총 10개 종목을 고정하고 있는데 요구사항은 “나스닥 상위 5개”입니다;
StockGroup.M7의 리스트를 나스닥 상위 5개 종목만 포함하도록 축소(예: AAPL, MSFT, GOOGL, AMZN, NVDA 또는
최신 요구에 따른 정확한 5종목)하고 불필요한 상수(META, TSLA 등)를 제거하거나 사용처가 있으면 다른 그룹으로 옮기며
DAILY_NASDAQ_STOCK_GROUP_MAP 정의와 관련 주석을 이에 맞게 업데이트하세요.

Comment on lines +17 to +41
private val NYSE_HOLIDAYS =
setOf(
// 2025
LocalDate.of(2025, 1, 1), // New Year's Day
LocalDate.of(2025, 1, 20), // MLK Jr. Day
LocalDate.of(2025, 2, 17), // Presidents' Day
LocalDate.of(2025, 4, 18), // Good Friday
LocalDate.of(2025, 5, 26), // Memorial Day
LocalDate.of(2025, 6, 19), // Juneteenth
LocalDate.of(2025, 7, 4), // Independence Day
LocalDate.of(2025, 9, 1), // Labor Day
LocalDate.of(2025, 11, 27), // Thanksgiving Day
LocalDate.of(2025, 12, 25), // Christmas Day
// 2026
LocalDate.of(2026, 1, 1), // New Year's Day
LocalDate.of(2026, 1, 19), // MLK Jr. Day
LocalDate.of(2026, 2, 16), // Presidents' Day
LocalDate.of(2026, 4, 3), // Good Friday
LocalDate.of(2026, 5, 25), // Memorial Day
LocalDate.of(2026, 6, 19), // Juneteenth
LocalDate.of(2026, 7, 3), // Independence Day (observed, 7/4 is Saturday)
LocalDate.of(2026, 9, 7), // Labor Day
LocalDate.of(2026, 11, 26), // Thanksgiving Day
LocalDate.of(2026, 12, 25), // Christmas Day
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

휴장일 데이터가 2026년까지만 있어 2027년부터 스케줄 오동작 위험이 큽니다.

현재 구현은 2027년 이후 NYSE 휴장일을 거래일로 처리할 수 있어, 휴장일 게시가 발생할 수 있습니다.

🔧 제안 수정안 (지원 연도 외 fail-fast 또는 안전 차단)
 class NyseMarketCalendar {
     fun isTradingDay(date: LocalDate): Boolean {
+        require(date.year in SUPPORTED_YEARS) {
+            "NYSE 휴장일 데이터가 ${date.year}년에 대해 정의되어 있지 않습니다."
+        }
         if (date.dayOfWeek == DayOfWeek.SATURDAY || date.dayOfWeek == DayOfWeek.SUNDAY) {
             return false
         }
         return date !in NYSE_HOLIDAYS
     }

     companion object {
+        private val SUPPORTED_YEARS = setOf(2025, 2026)
         private val NYSE_HOLIDAYS =
             setOf(
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@domain/generator/src/main/kotlin/com/few/generator/support/common/NyseMarketCalendar.kt`
around lines 17 - 41, NYSE_HOLIDAYS is hard-coded only through 2026 so dates
>=2027 can be treated as trading days; update NyseMarketCalendar to prevent
silent failures by either extending holiday data or adding a year-bounds check.
Modify the code paths that consult NYSE_HOLIDAYS (e.g., isHoliday(date),
isTradingDay(date), nextTradingDay(...) or any method using NYSE_HOLIDAYS) to
validate the date.year is within the supported range and: either throw an
IllegalStateException with a clear message asking to update NYSE_HOLIDAYS for
newer years, or replace the static set with a dynamic holiday generator that
computes observed NYSE holidays for arbitrary years. Ensure the check references
NYSE_HOLIDAYS (or the new generator) so callers fail-fast instead of
misclassifying holidays.

Comment on lines +74 to +76
log.info { "Instagram 게시 시작 (containerId: $containerId)" }
instagramUploader.publishMedia(containerId)
log.info { "Instagram 나스닥 주식 카드 게시 완료" }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Instagram 게시 결과를 확인하지 않아 실패를 성공으로 기록할 수 있습니다.

Line 75의 반환값을 체크하지 않아 게시 실패 시에도 Line 76 완료 로그가 찍힙니다. 실패 시 예외 처리로 명확히 분기해 주세요.

✅ 제안 수정안
         log.info { "Instagram 게시 시작 (containerId: $containerId)" }
-        instagramUploader.publishMedia(containerId)
+        val published = instagramUploader.publishMedia(containerId)
+        if (!published) {
+            throw RuntimeException("Instagram 게시 실패: containerId=$containerId")
+        }
         log.info { "Instagram 나스닥 주식 카드 게시 완료" }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@domain/generator/src/main/kotlin/com/few/generator/usecase/NasdaqDailyStockCardSchedulingUseCase.kt`
around lines 74 - 76, The call instagramUploader.publishMedia(containerId)
currently ignores its result so failures are logged as successes; update
NasdaqDailyStockCardSchedulingUseCase to inspect the return value or caught
exception from instagramUploader.publishMedia(containerId) and branch
accordingly — if publishMedia indicates failure (or throws), log an error and
rethrow or handle the failure path instead of logging "Instagram 나스닥 주식 카드 게시
완료"; ensure success logging only occurs when publishMedia reports success and
add appropriate exception handling around the call to avoid false-positive
completion logs.

Comment on lines +120 to +122
val outputFile = File(outputPath)
assert(outputFile.exists()) { "이미지 파일이 생성되지 않았습니다: ${outputFile.absolutePath}" }
assert(outputFile.length() > 0L) { "이미지 파일 크기가 0입니다: ${outputFile.absolutePath}" }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
fd -HI 'build.gradle.kts|build.gradle|gradle.properties'
rg -n -C2 'enableAssertions|jvmArgs\s*\(.*-ea|allJvmArgs|kotest' --glob '**/*.gradle*' --glob 'gradle.properties'

Repository: few-letter/few-be

Length of output: 1332


🏁 Script executed:

cat -n domain/generator/src/test/kotlin/com/few/generator/core/instagram/StockCardGeneratorIntegrationTest.kt | sed -n '110,130p'

Repository: few-letter/few-be

Length of output: 829


🏁 Script executed:

rg -n 'import.*assert|import.*shouldBe' domain/generator/src/test/kotlin/com/few/generator/core/instagram/StockCardGeneratorIntegrationTest.kt

Repository: few-letter/few-be

Length of output: 98


🏁 Script executed:

rg -l 'shouldBe|kotest' domain --glob='**/*.kt' | head -10

Repository: few-letter/few-be

Length of output: 1016


🏁 Script executed:

cat build.gradle.kts | grep -A 20 'tasks.test\|tasks\.register.*test\|test {'

Repository: few-letter/few-be

Length of output: 43


🏁 Script executed:

rg -n 'test\s*{|Test\s*{' domain/generator/build.gradle.kts

Repository: few-letter/few-be

Length of output: 168


🏁 Script executed:

cat domain/generator/build.gradle.kts

Repository: few-letter/few-be

Length of output: 1740


🏁 Script executed:

find . -name '.github' -o -name '.gitlab-ci.yml' -o -name 'gradle.properties' | head -5

Repository: few-letter/few-be

Length of output: 70


🏁 Script executed:

rg -A 5 'test {' domain/generator/build.gradle.kts

Repository: few-letter/few-be

Length of output: 136


🏁 Script executed:

find .github -type f -name '*.yml' -o -name '*.yaml' | xargs cat 2>/dev/null | head -100

Repository: few-letter/few-be

Length of output: 2635


🏁 Script executed:

grep -r "jvmArgs\|enableAssertions\|-ea" domain/generator/build.gradle.kts build.gradle.kts 2>/dev/null

Repository: few-letter/few-be

Length of output: 43


🏁 Script executed:

cat gradle.properties 2>/dev/null || echo "No gradle.properties found"

Repository: few-letter/few-be

Length of output: 87


🏁 Script executed:

rg 'assert\(' domain/generator/src/test/kotlin/ --type kotlin | head -20

Repository: few-letter/few-be

Length of output: 454


Kotest 매처로 변경하여 테스트 검증 일관성 개선

이 파일은 라인 118에서 shouldBe로 Kotest 매처를 사용하면서 라인 121-122에서는 Kotlin assert()를 혼용하고 있습니다. JVM 시작 옵션에 -ea 플래그가 없으면 assert()는 실행되지 않아 테스트가 거짓 양성으로 통과할 수 있습니다.

프로젝트에서 Kotest를 표준 테스트 라이브러리로 사용하고 있으므로, 일관성을 위해 다음과 같이 변경하세요:

            val outputFile = File(outputPath)
-           assert(outputFile.exists()) { "이미지 파일이 생성되지 않았습니다: ${outputFile.absolutePath}" }
-           assert(outputFile.length() > 0L) { "이미지 파일 크기가 0입니다: ${outputFile.absolutePath}" }
+           outputFile.exists() shouldBe true
+           outputFile.length() shouldBeGreaterThan 0L
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
val outputFile = File(outputPath)
assert(outputFile.exists()) { "이미지 파일이 생성되지 않았습니다: ${outputFile.absolutePath}" }
assert(outputFile.length() > 0L) { "이미지 파일 크기가 0입니다: ${outputFile.absolutePath}" }
val outputFile = File(outputPath)
outputFile.exists() shouldBe true
outputFile.length() shouldBeGreaterThan 0L
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@domain/generator/src/test/kotlin/com/few/generator/core/instagram/StockCardGeneratorIntegrationTest.kt`
around lines 120 - 122, Replace the Kotlin JVM assert() checks with Kotest
matchers to avoid silent skips when -ea isn’t enabled: in
StockCardGeneratorIntegrationTest.kt, replace the two assertions around
outputFile (the variable created from outputPath) with Kotest assertions such as
outputFile.exists() shouldBe true and outputFile.length() shouldBeGreaterThan
0L, and add the necessary Kotest matcher imports (e.g.,
io.kotest.matchers.shouldBe and
io.kotest.matchers.comparables.shouldBeGreaterThan) so the test consistently
fails when the file is missing or empty.

Comment on lines +165 to +176
Given("일부 종목 API 호출이 FeignException으로 실패할 때") {
beforeEach {
every { kisTokenClient.getToken(any()) } returns mockTokenResponse
OverseaStockConstants.DAILY_NASDAQ_STOCK_GROUP_MAP.values.flatten().forEachIndexed { index, stock ->
if (index % 2 == 0) {
every {
kisClient.getStockPrice(any(), any(), stock.excd, stock.symbol)
} returns mockStockResponse()
} else {
every {
kisClient.getStockPrice(any(), any(), stock.excd, stock.symbol)
} throws RuntimeException("simulated feign error")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

시나리오 설명과 실제 예외 타입이 불일치합니다.

Given 문구는 “FeignException으로 실패”인데 실제로는 RuntimeException("simulated feign error")를 던집니다. 테스트 명세 해석이 흔들리므로 문구 또는 예외 타입을 맞춰주세요.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@domain/generator/src/test/kotlin/com/few/generator/core/kis/KisStockFetcherTest.kt`
around lines 165 - 176, The test description in KisStockFetcherTest ("일부 종목 API
호출이 FeignException으로 실패할 때") does not match the mocked exception
(RuntimeException("simulated feign error")) thrown from the
kisClient.getStockPrice stub; either change the Given text to reference
RuntimeException or update the stub to throw a FeignException instance instead
(e.g., replace throws RuntimeException(...) with throws FeignException (created
via FeignException.errorStatus or an appropriate FeignException constructor) for
the kisClient.getStockPrice mock).

Comment on lines +50 to +51
val bucket = "gen-cards"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

운영 버킷 하드코딩으로 테스트 오염 위험이 있습니다.

Line 50의 버킷/리전 고정값 때문에 실자격증명 환경에서 테스트가 운영 스토리지에 직접 업로드할 수 있습니다. 버킷/리전을 환경변수로 분리하고 미설정 시 테스트를 건너뛰는 게 안전합니다.

🔧 제안 수정안
-        val bucket = "gen-cards"
+        val bucket = System.getenv("STORAGE_BUCKET") ?: ""
+        val storageRegion = System.getenv("STORAGE_REGION") ?: "ap-northeast-2"
...
+            if (bucket.isBlank()) {
+                println("⚠️  STORAGE_BUCKET 환경변수가 설정되지 않아 테스트를 건너뜁니다.")
+                return@test
+            }
...
-                    ).region(Region.AP_NORTHEAST_2)
+                    ).region(Region.of(storageRegion))
...
-                    ).region(Region.AP_NORTHEAST_2)
+                    ).region(Region.of(storageRegion))

Also applies to: 103-104, 128-131

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@domain/generator/src/test/kotlin/com/few/generator/usecase/NasdaqStockCardS3UploadIntegrationTest.kt`
around lines 50 - 51, 테스트에 하드코딩된 S3 버킷/리전 값(bucket, region)을 환경변수로 분리하고, 해당 env
vars가 없으면 테스트를 건너뛰도록 변경하세요: NasdaqStockCardS3UploadIntegrationTest 내에 사용 중인
변수(예: bucket, region 또는 테스트 메서드들)를 S3_TEST_BUCKET/S3_TEST_REGION 같은 환경변수로 읽어오게
바꾸고, 값이 비어있으면 JUnit의 assumeTrue/assumeFalse 또는 테스트 스킵 로직으로 즉시 반환하여 실제 운영 스토리지로
업로드되지 않게 하며 같은 패턴을 파일의 다른 하드코딩 위치들(언급된 다른 블록들: 103-104, 128-131에 해당하는 변수들)에도
동일하게 적용하세요.

Comment on lines +166 to +176
// Step 3: S3 업로드
val s3Provider = buildS3Provider()
val uploadResult = s3Provider.uploadImages(listOf(outputPath))
println("=== S3 업로드 결과 ===")
println("성공: ${uploadResult.uploadedCount}개 / 실패: ${uploadResult.failedCount}개")
uploadResult.successfulUploads.forEach { println("URL: ${it.url}") }
uploadResult.failedUploads.forEach { println("실패: ${it.path} - ${it.errorMessage}") }

// Step 4: 로컬 파일 정리
File(outputPath).takeIf { it.exists() }?.delete()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

로컬 파일 정리가 예외 경로에서 누락됩니다.

Line 168 업로드에서 예외가 발생하면 Line 175 정리가 실행되지 않아 임시 이미지가 남을 수 있습니다. try/finally로 정리를 보장해 주세요.

🧹 제안 수정안
-            val uploadResult = s3Provider.uploadImages(listOf(outputPath))
-            println("=== S3 업로드 결과 ===")
-            println("성공: ${uploadResult.uploadedCount}개 / 실패: ${uploadResult.failedCount}개")
-            uploadResult.successfulUploads.forEach { println("URL: ${it.url}") }
-            uploadResult.failedUploads.forEach { println("실패: ${it.path} - ${it.errorMessage}") }
-
-            // Step 4: 로컬 파일 정리
-            File(outputPath).takeIf { it.exists() }?.delete()
+            val uploadResult =
+                try {
+                    s3Provider.uploadImages(listOf(outputPath))
+                } finally {
+                    // Step 4: 로컬 파일 정리 (성공/실패 무관)
+                    File(outputPath).takeIf { it.exists() }?.delete()
+                }
+            println("=== S3 업로드 결과 ===")
+            println("성공: ${uploadResult.uploadedCount}개 / 실패: ${uploadResult.failedCount}개")
+            uploadResult.successfulUploads.forEach { println("URL: ${it.url}") }
+            uploadResult.failedUploads.forEach { println("실패: ${it.path} - ${it.errorMessage}") }

Based on learnings UploadGenCardNewsS3UseCase에서 S3 업로드 후 성공/실패 여부와 관계없이 모든 로컬 이미지 파일을 삭제하는 것이 의도된 동작임.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@domain/generator/src/test/kotlin/com/few/generator/usecase/NasdaqStockCardS3UploadIntegrationTest.kt`
around lines 166 - 176, The local cleanup is skipped if uploadImages throws;
wrap the S3 upload and its logging in a try/finally so that
File(outputPath).takeIf { it.exists() }?.delete() always runs; specifically,
keep using buildS3Provider() and call s3Provider.uploadImages(...) inside the
try, perform println of uploadResult inside the try, and move the single-file
deletion (File(outputPath)...delete()) into the finally block to guarantee
deletion regardless of exceptions from uploadImages.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (3)
domain/generator/src/test/kotlin/com/few/generator/usecase/NasdaqStockCardS3UploadIntegrationTest.kt (2)

175-184: ⚠️ Potential issue | 🟡 Minor

업로드 예외 경로에서도 로컬 파일 정리를 보장해 주세요.

Line 177에서 uploadImages()가 예외를 던지면 Line 184 삭제가 실행되지 않아 gen_images/에 임시 파일이 남습니다. 업로드와 로그 출력을 try에 두고, 삭제를 finally로 옮겨 주세요.

🧹 제안 수정안
             // Step 3: S3 업로드
             val s3Provider = buildS3Provider()
-            val uploadResult = s3Provider.uploadImages(listOf(outputPath))
-            println("=== S3 업로드 결과 ===")
-            println("성공: ${uploadResult.uploadedCount}개 / 실패: ${uploadResult.failedCount}개")
-            uploadResult.successfulUploads.forEach { println("URL: ${it.url}") }
-            uploadResult.failedUploads.forEach { println("실패: ${it.path} - ${it.errorMessage}") }
-
-            // Step 4: 로컬 파일 정리
-            File(outputPath).takeIf { it.exists() }?.delete()
+            val uploadResult =
+                try {
+                    s3Provider.uploadImages(listOf(outputPath)).also {
+                        println("=== S3 업로드 결과 ===")
+                        println("성공: ${it.uploadedCount}개 / 실패: ${it.failedCount}개")
+                        it.successfulUploads.forEach { upload -> println("URL: ${upload.url}") }
+                        it.failedUploads.forEach { failed -> println("실패: ${failed.path} - ${failed.errorMessage}") }
+                    }
+                } finally {
+                    File(outputPath).takeIf { it.exists() }?.delete()
+                }

Based on learnings UploadGenCardNewsS3UseCase에서 S3 업로드 후 성공/실패 여부와 관계없이 모든 로컬 이미지 파일(event.imagePaths)을 삭제하는 것이 의도된 동작임.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@domain/generator/src/test/kotlin/com/few/generator/usecase/NasdaqStockCardS3UploadIntegrationTest.kt`
around lines 175 - 184, The current test calls
s3Provider.uploadImages(listOf(outputPath)) and prints results, but if
uploadImages throws an exception the local cleanup File(outputPath).delete() is
skipped; wrap the upload and all printlns in a try block and move the
File(outputPath).takeIf { it.exists() }?.delete() into a finally block so the
temp gen_images file is removed regardless of upload success or exceptions;
reference the s3Provider.uploadImages call, the println result processing, and
the File(outputPath) deletion when making the change and ensure the finally
still safely checks for file existence before deleting.

45-50: ⚠️ Potential issue | 🟠 Major

테스트가 실제 버킷으로 업로드할 여지가 있습니다.

Line 50, 103-104, 128-129에서 버킷/리전을 고정하면 실제 자격증명이 들어온 환경에서 gen-cards로 바로 업로드됩니다. 테스트용 버킷/리전을 환경변수로 분리하고, 값이 없으면 테스트를 건너뛰는 쪽이 안전합니다.

🔒 제안 수정안
         val storageAccessKey = System.getenv("STORAGE_ACCESS_KEY") ?: ""
         val storageSecretKey = System.getenv("STORAGE_SECRET_KEY") ?: ""
-        val bucket = "gen-cards"
+        val bucket = System.getenv("S3_TEST_BUCKET") ?: ""
+        val region = System.getenv("S3_TEST_REGION") ?: "ap-northeast-2"
 ...
             if (storageAccessKey.isBlank() || storageSecretKey.isBlank()) {
                 println("⚠️  STORAGE_ACCESS_KEY 또는 STORAGE_SECRET_KEY 환경변수가 설정되지 않아 테스트를 건너뜁니다.")
                 return@test
             }
+            if (bucket.isBlank()) {
+                println("⚠️  S3_TEST_BUCKET 환경변수가 설정되지 않아 테스트를 건너뜁니다.")
+                return@test
+            }
 ...
-                    ).region(Region.AP_NORTHEAST_2)
+                    ).region(Region.of(region))
 ...
-                    ).region(Region.AP_NORTHEAST_2)
+                    ).region(Region.of(region))

Also applies to: 95-131

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@domain/generator/src/test/kotlin/com/few/generator/usecase/NasdaqStockCardS3UploadIntegrationTest.kt`
around lines 45 - 50, The test currently hardcodes bucket and region variables
(e.g., bucket = "gen-cards", and reads storageAccessKey/storageSecretKey) which
can cause real uploads when credentials are present; change the test to read
test-specific environment variables (e.g., TEST_BUCKET, TEST_REGION) for bucket
and region and only proceed if both TEST_BUCKET and storage credentials
(STORAGE_ACCESS_KEY/SECRET) are present, otherwise skip the test (use the test
framework's assume/ignore/skip mechanism). Update references to bucket and
region in NasdaqStockCardS3UploadIntegrationTest (and any helper methods used by
the test) to use these env vars so real production bucket values are never used
by default.
domain/generator/src/test/kotlin/com/few/generator/core/instagram/StockCardGeneratorIntegrationTest.kt (1)

120-122: ⚠️ Potential issue | 🟡 Minor

Kotlin assert() 대신 Kotest 매처로 고정해 주세요.

Line 121-122의 assert()는 JVM이 -ea 없이 실행되면 검사 자체가 생략됩니다. 지금처럼 Kotest 검증과 섞여 있으면 파일이 없거나 비어 있어도 테스트가 녹색으로 지나갈 수 있습니다.

🧪 제안 수정안
 import io.kotest.matchers.shouldBe
+import io.kotest.matchers.comparables.shouldBeGreaterThan
 ...
             val outputFile = File(outputPath)
-            assert(outputFile.exists()) { "이미지 파일이 생성되지 않았습니다: ${outputFile.absolutePath}" }
-            assert(outputFile.length() > 0L) { "이미지 파일 크기가 0입니다: ${outputFile.absolutePath}" }
+            outputFile.exists() shouldBe true
+            outputFile.length() shouldBeGreaterThan 0L
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@domain/generator/src/test/kotlin/com/few/generator/core/instagram/StockCardGeneratorIntegrationTest.kt`
around lines 120 - 122, Replace the JVM-only Kotlin assert() checks used on
outputFile (created from outputPath) with Kotest matchers so the test always
verifies failures; specifically, change the two assertions around
outputFile.exists() and outputFile.length() to Kotest assertions such as using
the File matcher shouldExist() for existence and a Kotest numeric matcher (e.g.,
outputFile.length() shouldBeGreaterThan 0L) to ensure file size > 0 — update the
assertions near the outputFile variable in StockCardGeneratorIntegrationTest.kt
accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@domain/generator/src/main/kotlin/com/few/generator/core/instagram/NasdaqDailyStockCardGenerator.kt`:
- Around line 115-120: The generator currently pulls ETF and M7 fixed groups
from the stocks map and renders fixed sections (via drawEtfCards and
drawM7Section), which hardcodes ETF 3 + M7 7 layout; change
NasdaqDailyStockCardGenerator to accept a single sorted List of top stocks (the
"상위 5개 나스닥 종목") instead of using stocks[OverseaStockConstants.StockGroup],
remove the dependency on OverseaStockConstants.StockGroup for layout, and
replace the hardcoded drawEtfCards/drawM7Section calls with a generic rendering
flow that computes rows and sections from the input list (e.g., a new
renderTopStocks(graphics, List<Stock>) or adapt draw* methods to accept the list
and derive number of rows/columns dynamically); ensure drawFooter still receives
marketMood and that any helper methods (drawEtfCards, drawM7Section, layout
logic) are updated to use the data-driven row/section calculations so changes in
collection logic produce correct card layouts.

---

Duplicate comments:
In
`@domain/generator/src/test/kotlin/com/few/generator/core/instagram/StockCardGeneratorIntegrationTest.kt`:
- Around line 120-122: Replace the JVM-only Kotlin assert() checks used on
outputFile (created from outputPath) with Kotest matchers so the test always
verifies failures; specifically, change the two assertions around
outputFile.exists() and outputFile.length() to Kotest assertions such as using
the File matcher shouldExist() for existence and a Kotest numeric matcher (e.g.,
outputFile.length() shouldBeGreaterThan 0L) to ensure file size > 0 — update the
assertions near the outputFile variable in StockCardGeneratorIntegrationTest.kt
accordingly.

In
`@domain/generator/src/test/kotlin/com/few/generator/usecase/NasdaqStockCardS3UploadIntegrationTest.kt`:
- Around line 175-184: The current test calls
s3Provider.uploadImages(listOf(outputPath)) and prints results, but if
uploadImages throws an exception the local cleanup File(outputPath).delete() is
skipped; wrap the upload and all printlns in a try block and move the
File(outputPath).takeIf { it.exists() }?.delete() into a finally block so the
temp gen_images file is removed regardless of upload success or exceptions;
reference the s3Provider.uploadImages call, the println result processing, and
the File(outputPath) deletion when making the change and ensure the finally
still safely checks for file existence before deleting.
- Around line 45-50: The test currently hardcodes bucket and region variables
(e.g., bucket = "gen-cards", and reads storageAccessKey/storageSecretKey) which
can cause real uploads when credentials are present; change the test to read
test-specific environment variables (e.g., TEST_BUCKET, TEST_REGION) for bucket
and region and only proceed if both TEST_BUCKET and storage credentials
(STORAGE_ACCESS_KEY/SECRET) are present, otherwise skip the test (use the test
framework's assume/ignore/skip mechanism). Update references to bucket and
region in NasdaqStockCardS3UploadIntegrationTest (and any helper methods used by
the test) to use these env vars so real production bucket values are never used
by default.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 85f0be4d-5b92-40bb-bba2-d909e9b71fec

📥 Commits

Reviewing files that changed from the base of the PR and between 7a5be56 and d43beeb.

📒 Files selected for processing (7)
  • domain/generator/src/main/kotlin/com/few/generator/core/instagram/NasdaqDailyStockCardGenerator.kt
  • domain/generator/src/main/kotlin/com/few/generator/core/kis/OverseaStockConstants.kt
  • domain/generator/src/main/kotlin/com/few/generator/support/common/NyseMarketCalendar.kt
  • domain/generator/src/main/kotlin/com/few/generator/usecase/NasdaqDailyStockCardSchedulingUseCase.kt
  • domain/generator/src/test/kotlin/com/few/generator/core/instagram/StockCardGeneratorIntegrationTest.kt
  • domain/generator/src/test/kotlin/com/few/generator/usecase/NasdaqDailyStockCardSchedulingUseCaseTest.kt
  • domain/generator/src/test/kotlin/com/few/generator/usecase/NasdaqStockCardS3UploadIntegrationTest.kt
✅ Files skipped from review due to trivial changes (2)
  • domain/generator/src/main/kotlin/com/few/generator/support/common/NyseMarketCalendar.kt
  • domain/generator/src/main/kotlin/com/few/generator/core/kis/OverseaStockConstants.kt
🚧 Files skipped from review as they are similar to previous changes (2)
  • domain/generator/src/test/kotlin/com/few/generator/usecase/NasdaqDailyStockCardSchedulingUseCaseTest.kt
  • domain/generator/src/main/kotlin/com/few/generator/usecase/NasdaqDailyStockCardSchedulingUseCase.kt
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: api-test
  • GitHub Check: Build and analyze
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2026-01-17T01:53:02.072Z
Learnt from: hun-ca
Repo: few-letter/few-be PR: 883
File: domain/generator/src/main/kotlin/com/few/generator/usecase/UploadGenCardNewsS3UseCase.kt:28-30
Timestamp: 2026-01-17T01:53:02.072Z
Learning: `UploadGenCardNewsS3UseCase`에서 S3 업로드 후 성공/실패 여부와 관계없이 모든 로컬 이미지 파일(`event.imagePaths`)을 삭제하는 것이 의도된 동작임. 실패한 파일도 포함하여 임시 파일을 정리하는 것이 설계 의도.

Applied to files:

  • domain/generator/src/test/kotlin/com/few/generator/usecase/NasdaqStockCardS3UploadIntegrationTest.kt
📚 Learning: 2026-01-11T03:48:00.270Z
Learnt from: hun-ca
Repo: few-letter/few-be PR: 875
File: domain/generator/src/main/kotlin/com/few/generator/service/instagram/ImageGeneratorUtils.kt:60-87
Timestamp: 2026-01-11T03:48:00.270Z
Learning: Amazon Linux 2023에서는 한글 폰트가 기본 포함되지 않으므로 `dnf install -y google-noto-sans-cjk-kr-fonts` 명령어로 설치가 필요하며, Java AWT에서 "Noto Sans CJK KR" 또는 "Noto Sans KR" 폰트명으로 접근 가능합니다.

Applied to files:

  • domain/generator/src/main/kotlin/com/few/generator/core/instagram/NasdaqDailyStockCardGenerator.kt

Comment on lines +115 to +120
val etfStocks = stocks[OverseaStockConstants.StockGroup.ETF] ?: emptyList()
val m7Stocks = stocks[OverseaStockConstants.StockGroup.M7] ?: emptyList()

drawEtfCards(graphics, etfStocks)
drawM7Section(graphics, m7Stocks)
drawFooter(graphics, marketMood)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

요구사항의 상위 5개 나스닥 종목과 렌더링 계약이 다릅니다.

Issue #921는 네이버 기준 상위 5개 나스닥 종목 게시인데, 여기서는 ETF/M7 두 그룹만 꺼내서 고정 섹션으로 그립니다. OverseaStockConstants.StockGroup도 두 값만 가지기 때문에 지금 구조로는 수집 로직을 바꿔도 카드가 계속 ETF 3개 + M7 7개 형식으로 생성됩니다. 입력을 “정렬된 상위 5개 종목 리스트” 하나로 받고, 행 수와 섹션을 데이터 기반으로 계산하는 형태로 바꿔야 요구사항과 맞습니다.

Also applies to: 150-258

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@domain/generator/src/main/kotlin/com/few/generator/core/instagram/NasdaqDailyStockCardGenerator.kt`
around lines 115 - 120, The generator currently pulls ETF and M7 fixed groups
from the stocks map and renders fixed sections (via drawEtfCards and
drawM7Section), which hardcodes ETF 3 + M7 7 layout; change
NasdaqDailyStockCardGenerator to accept a single sorted List of top stocks (the
"상위 5개 나스닥 종목") instead of using stocks[OverseaStockConstants.StockGroup],
remove the dependency on OverseaStockConstants.StockGroup for layout, and
replace the hardcoded drawEtfCards/drawM7Section calls with a generic rendering
flow that computes rows and sections from the input list (e.g., a new
renderTopStocks(graphics, List<Stock>) or adapt draw* methods to accept the list
and derive number of rows/columns dynamically); ensure drawFooter still receives
marketMood and that any helper methods (drawEtfCards, drawM7Section, layout
logic) are updated to use the data-driven row/section calculations so changes in
collection logic produce correct card layouts.

hun-ca and others added 3 commits March 31, 2026 22:06
…GeneratorUtils로 수정

존재하지 않는 ImageGeneratorUtils를 참조하던 컴파일 오류를 실제 유틸리티 클래스인 CardImageGeneratorUtils로 교체

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
더미 데이터 기반으로 기본 생성, marketMood 유무, 특정 날짜, ETF/M7 누락, 전종목 상승/하락 등 8가지 케이스 검증

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
domain/generator/src/test/kotlin/com/few/generator/core/instagram/NasdaqDailyStockCardGeneratorTest.kt (2)

46-48: 어색한 assertion 패턴입니다.

Line 48의 assertion이 file.length()를 자기 자신과 비교하고 있어 shouldBe는 항상 통과합니다. 실제 검증은 also 블록 내 assert()에서 수행되는데, Kotest matcher를 사용하는 것이 더 명확합니다.

♻️ 권장 수정
-            file.length() shouldBe (file.length().also { assert(it > 0L) { "이미지 파일 크기가 0입니다" } })
+            file.length() shouldBeGreaterThan 0L

io.kotest.matchers.longs.shouldBeGreaterThan import가 필요합니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@domain/generator/src/test/kotlin/com/few/generator/core/instagram/NasdaqDailyStockCardGeneratorTest.kt`
around lines 46 - 48, The assertion currently compares file.length() to itself
and hides the real check inside an assert; update the test in
NasdaqDailyStockCardGeneratorTest so instead of the self-comparison you assert
the file size is > 0 using Kotest matcher (e.g., call shouldBeGreaterThan on
file.length()), and add the import io.kotest.matchers.longs.shouldBeGreaterThan
to the test file so the matcher resolves.

13-14: 테스트 아티팩트 정리를 고려하세요.

테스트가 gen_images 디렉토리에 이미지 파일을 생성하지만, 테스트 후 정리(cleanup)가 없습니다. .gitignore에 해당 경로가 포함되어 있는지 확인하고, 가능하다면 임시 디렉토리를 사용하거나 afterSpec/afterTest에서 생성된 파일을 삭제하는 것이 좋습니다.

🧹 임시 디렉토리 사용 예시
import kotlin.io.path.createTempDirectory

class NasdaqDailyStockCardGeneratorTest :
    FunSpec({

        val generator = NasdaqDailyStockCardGenerator()
        val outputDir = createTempDirectory("gen_images").toFile()

        afterSpec {
            outputDir.deleteRecursively()
        }
        // ... tests
    })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@domain/generator/src/test/kotlin/com/few/generator/core/instagram/NasdaqDailyStockCardGeneratorTest.kt`
around lines 13 - 14, The test currently writes images to a hardcoded
"gen_images" directory without cleanup; update NasdaqDailyStockCardGeneratorTest
to use a temporary directory for output (e.g.,
createTempDirectory(...).toFile()) instead of the string "gen_images", assign
that to outputDir used by generator, and add an afterSpec or afterTest block
that calls outputDir.deleteRecursively() to remove generated files;
alternatively ensure the "gen_images" path is listed in .gitignore if you keep
the static path.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In
`@domain/generator/src/test/kotlin/com/few/generator/core/instagram/NasdaqDailyStockCardGeneratorTest.kt`:
- Around line 46-48: The assertion currently compares file.length() to itself
and hides the real check inside an assert; update the test in
NasdaqDailyStockCardGeneratorTest so instead of the self-comparison you assert
the file size is > 0 using Kotest matcher (e.g., call shouldBeGreaterThan on
file.length()), and add the import io.kotest.matchers.longs.shouldBeGreaterThan
to the test file so the matcher resolves.
- Around line 13-14: The test currently writes images to a hardcoded
"gen_images" directory without cleanup; update NasdaqDailyStockCardGeneratorTest
to use a temporary directory for output (e.g.,
createTempDirectory(...).toFile()) instead of the string "gen_images", assign
that to outputDir used by generator, and add an afterSpec or afterTest block
that calls outputDir.deleteRecursively() to remove generated files;
alternatively ensure the "gen_images" path is listed in .gitignore if you keep
the static path.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 8c0abd21-3ef1-4761-ad66-b600129f5901

📥 Commits

Reviewing files that changed from the base of the PR and between d43beeb and 11d543a.

📒 Files selected for processing (2)
  • domain/generator/src/main/kotlin/com/few/generator/core/instagram/NasdaqDailyStockCardGenerator.kt
  • domain/generator/src/test/kotlin/com/few/generator/core/instagram/NasdaqDailyStockCardGeneratorTest.kt
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: api-test
  • GitHub Check: Build and analyze
  • GitHub Check: lint
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2026-01-11T03:48:00.270Z
Learnt from: hun-ca
Repo: few-letter/few-be PR: 875
File: domain/generator/src/main/kotlin/com/few/generator/service/instagram/ImageGeneratorUtils.kt:60-87
Timestamp: 2026-01-11T03:48:00.270Z
Learning: Amazon Linux 2023에서는 한글 폰트가 기본 포함되지 않으므로 `dnf install -y google-noto-sans-cjk-kr-fonts` 명령어로 설치가 필요하며, Java AWT에서 "Noto Sans CJK KR" 또는 "Noto Sans KR" 폰트명으로 접근 가능합니다.

Applied to files:

  • domain/generator/src/main/kotlin/com/few/generator/core/instagram/NasdaqDailyStockCardGenerator.kt
🔇 Additional comments (12)
domain/generator/src/main/kotlin/com/few/generator/core/instagram/NasdaqDailyStockCardGenerator.kt (10)

1-19: LGTM!

패키지 및 import 구성이 적절합니다. CardImageGeneratorUtils의 유틸리티 함수들을 static import로 깔끔하게 가져오고 있습니다.


35-95: 상수 정의가 잘 구조화되어 있습니다.

레이아웃 영역별로 상수가 명확하게 분리되어 있고, 매직 넘버 없이 의미 있는 이름으로 정의되어 유지보수성이 좋습니다. DATE_FORMATTER도 한국어 로케일로 적절히 설정되어 있습니다.


97-126: 리소스 관리가 적절합니다.

try/finally 블록을 사용하여 Graphics2D 리소스를 확실히 dispose하는 패턴이 좋습니다.

다만, saveImage의 반환값이 ImageIO.write의 실제 결과가 아닌 예외 발생 여부만 반영한다는 점을 인지하시기 바랍니다 (관련 코드 스니펫 참조). 현재 구현에서는 문제가 되지 않지만, 향후 더 정확한 저장 검증이 필요할 경우 ImageIO.write의 반환값도 확인하는 것이 좋습니다.


128-144: LGTM!

AlphaComposite 사용 후 원래 composite를 복원하는 패턴이 올바르게 구현되어 있습니다. 한글 폰트 로딩도 적절합니다.


150-202: 동적 레이아웃 계산이 잘 구현되어 있습니다.

빈 리스트 early return, 동적 카드 너비 계산, 그리고 AlphaComposite 복원 패턴 모두 적절합니다. 텍스트 블록의 세로 중앙 정렬 계산도 정확합니다.


208-258: LGTM!

행 구분선이 마지막 행에서 생략되는 로직(idx < stocks.size - 1)이 올바르게 구현되어 있고, 사용 가능한 공간 내에서 행들을 세로 중앙 정렬하는 계산도 적절합니다.


260-294: LGTM!

marketMood 존재 여부에 따른 조건부 렌더링과 로고 위치 조정이 적절합니다. 로고 이미지 null 체크와 aspect ratio 기반 리사이징도 올바르게 구현되어 있습니다.


296-319: LGTM!

로고 이미지 로드 실패 시 drawCompanyBadge 폴백 패턴이 좋습니다. aspect ratio 유지와 중앙 정렬 계산도 올바릅니다.


321-340: LGTM!

알 수 없는 심볼에 대한 AQUA_BLUE 폴백과 원형 배지 내 글자 중앙 정렬이 적절합니다.


342-348: LGTM!

Nullable Boolean에 대한 when 표현식이 모든 케이스(상승, 하락, 보합)를 명확하게 처리합니다.

domain/generator/src/test/kotlin/com/few/generator/core/instagram/NasdaqDailyStockCardGeneratorTest.kt (2)

51-103: LGTM!

다양한 시나리오(marketMood 유무, 특정 날짜, ETF/M7 데이터 누락)에 대한 테스트 커버리지가 좋습니다. 이미지 생성 테스트에서 파일 존재 여부 확인은 적절한 수준입니다.


105-143: LGTM!

모든 상승/하락 시나리오 테스트가 잘 구현되어 있습니다. changeRate 변환 로직이 "0.00""+0.00" 또는 "-0.00"으로 만들 수 있지만, 테스트 목적상 문제되지 않습니다.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

새벽 나스닥 종목 크롤링

2 participants