[Feat/#921] 새벽 나스닥 종목 스케줄러 추가 - #940
Conversation
- 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>
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>
…ockCardGenerator)
- NyseMarketCalendar: 주말 + 2025/2026 NYSE 공휴일 기반 거래일 판별 컴포넌트 추가 - NasdaqDailyStockCardSchedulingUseCase: 미국 동부시간 기준 휴장일이면 실행 건너뜀 - SchedulingController: nasdaq-daily-stock 스케줄 메서드 연결 - NasdaqDailyStockCardSchedulingUseCaseTest: 휴장일 스킵 테스트 케이스 추가 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
WalkthroughKIS API에서 나스닥 관련 종목·가격 데이터를 조회해 Instagram용 1080×1080 카드 이미지를 생성하고 S3에 업로드한 뒤 인스타그램에 게시하는 일일 스케줄링 워크플로우와 이를 구성하는 Feign 클라이언트, DTO, 도메인 모델, 이미지 생성기, 스케줄링 유스케이스/컨트롤러, 거래일 캘린더, 설정 및 단위·통합 테스트들을 추가합니다. Changes
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
Summary of ChangesHello, 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
Changelog
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
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!!) |
| private val appKey: String, | ||
| @Value("\${KIS_APP_SECRET:thisis-kis-app-secret}") | ||
| private val appSecret: String, |
| Request | ||
| .Builder() | ||
| .url(url!!) | ||
| .post(RequestBody.create(null, "")) |
| "[Instagram][SingleMedia] Creation of MediaContainer Failed: ${errorResponse?.error?.message ?: "Unknown error"}", | ||
| ) | ||
| } | ||
| DelayUtil.randomDelay(5, 10) |
| */ | ||
| @Component | ||
| class NasdaqDailyStockCardGenerator { | ||
| private val log = KotlinLogging.logger {} |
| 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 | ||
| ) |
|
|
||
| val date = LocalDate.now() | ||
| val dateStr = date.format(DateTimeFormatter.ofPattern("yyyyMMdd")) | ||
| val outputPath = "gen_images/${dateStr}_nasdaq_daily_stock.png" |
There was a problem hiding this comment.
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
⛔ Files ignored due to path filters (7)
domain/generator/src/main/resources/images/m7/aapl_logo.pngis excluded by!**/*.pngdomain/generator/src/main/resources/images/m7/amzn_logo.pngis excluded by!**/*.pngdomain/generator/src/main/resources/images/m7/googl_logo.pngis excluded by!**/*.pngdomain/generator/src/main/resources/images/m7/meta_logo.pngis excluded by!**/*.pngdomain/generator/src/main/resources/images/m7/msft_logo.pngis excluded by!**/*.pngdomain/generator/src/main/resources/images/m7/nvda_logo.pngis excluded by!**/*.pngdomain/generator/src/main/resources/images/m7/tsla_logo.pngis excluded by!**/*.png
📒 Files selected for processing (21)
domain/generator/src/main/kotlin/com/few/generator/controller/SchedulingController.ktdomain/generator/src/main/kotlin/com/few/generator/core/instagram/InstagramUploader.ktdomain/generator/src/main/kotlin/com/few/generator/core/instagram/NasdaqDailyStockCardGenerator.ktdomain/generator/src/main/kotlin/com/few/generator/core/kis/KisClient.ktdomain/generator/src/main/kotlin/com/few/generator/core/kis/KisStockFetcher.ktdomain/generator/src/main/kotlin/com/few/generator/core/kis/KisTokenClient.ktdomain/generator/src/main/kotlin/com/few/generator/core/kis/OverseaStockConstants.ktdomain/generator/src/main/kotlin/com/few/generator/core/kis/Stock.ktdomain/generator/src/main/kotlin/com/few/generator/core/kis/StockQuote.ktdomain/generator/src/main/kotlin/com/few/generator/core/kis/dto/KisStockPriceResponse.ktdomain/generator/src/main/kotlin/com/few/generator/core/kis/dto/KisTokenRequest.ktdomain/generator/src/main/kotlin/com/few/generator/core/kis/dto/KisTokenResponse.ktdomain/generator/src/main/kotlin/com/few/generator/support/common/NyseMarketCalendar.ktdomain/generator/src/main/kotlin/com/few/generator/usecase/NasdaqDailyStockCardSchedulingUseCase.ktdomain/generator/src/main/resources/application-generator-local.yamldomain/generator/src/main/resources/application-generator-prd.yamldomain/generator/src/test/kotlin/com/few/generator/core/instagram/StockCardGeneratorIntegrationTest.ktdomain/generator/src/test/kotlin/com/few/generator/core/kis/KisStockFetcherIntegrationTest.ktdomain/generator/src/test/kotlin/com/few/generator/core/kis/KisStockFetcherTest.ktdomain/generator/src/test/kotlin/com/few/generator/usecase/NasdaqDailyStockCardSchedulingUseCaseTest.ktdomain/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-token과kis클라이언트는 동일한 기본 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
| @Scheduled(cron = "\${scheduling.cron.nasdaq-daily-stock}", zone = "Asia/Seoul") | ||
| fun nasdaqDailyStockScheduling() { | ||
| nasdaqDailyStockCardSchedulingUseCase.execute() | ||
| } |
There was a problem hiding this comment.
스케줄러 예외 처리 누락
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.
| @SerializedName("rt_cd") | ||
| val rtCd: String, | ||
| @SerializedName("msg1") | ||
| val msg1: String, | ||
| val output: Output?, | ||
| ) { | ||
| data class Output( | ||
| /** 현재가 */ | ||
| val last: String, | ||
| /** 원환산 등락률 (%) */ | ||
| val t_xrat: String, |
There was a problem hiding this comment.
🧩 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.ktRepository: few-letter/few-be
Length of output: 876
🏁 Script executed:
cat -n domain/generator/src/main/kotlin/com/few/generator/core/kis/dto/KisStockPriceResponse.ktRepository: few-letter/few-be
Length of output: 630
🏁 Script executed:
cat -n api/src/main/resources/application.ymlRepository: few-letter/few-be
Length of output: 1555
🏁 Script executed:
grep -r "feign\|gson\|jackson" domain/generator/build.gradle.kts build.gradle.ktsRepository: 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
SpringDecoderrelies on) withspring.http.converters.preferred-json-mapper(e.g.,gsonvsjackson). [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.
| @Value("\${KIS_APP_KEY:thisis-kis-app-key}") | ||
| private val appKey: String, | ||
| @Value("\${KIS_APP_SECRET:thisis-kis-app-secret}") | ||
| private val appSecret: String, |
There was a problem hiding this comment.
🧩 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.yamlRepository: few-letter/few-be
Length of output: 3764
🏁 Script executed:
cat -n domain/generator/src/main/kotlin/com/few/generator/core/kis/KisStockFetcher.ktRepository: 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 2Repository: 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.
| @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().
| 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), | ||
| ) |
There was a problem hiding this comment.
요구사항(상위 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 정의와 관련 주석을 이에 맞게 업데이트하세요.
| 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 | ||
| ) |
There was a problem hiding this comment.
휴장일 데이터가 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.
| log.info { "Instagram 게시 시작 (containerId: $containerId)" } | ||
| instagramUploader.publishMedia(containerId) | ||
| log.info { "Instagram 나스닥 주식 카드 게시 완료" } |
There was a problem hiding this comment.
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.
| val outputFile = File(outputPath) | ||
| assert(outputFile.exists()) { "이미지 파일이 생성되지 않았습니다: ${outputFile.absolutePath}" } | ||
| assert(outputFile.length() > 0L) { "이미지 파일 크기가 0입니다: ${outputFile.absolutePath}" } |
There was a problem hiding this comment.
🧩 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.ktRepository: few-letter/few-be
Length of output: 98
🏁 Script executed:
rg -l 'shouldBe|kotest' domain --glob='**/*.kt' | head -10Repository: 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.ktsRepository: few-letter/few-be
Length of output: 168
🏁 Script executed:
cat domain/generator/build.gradle.ktsRepository: few-letter/few-be
Length of output: 1740
🏁 Script executed:
find . -name '.github' -o -name '.gitlab-ci.yml' -o -name 'gradle.properties' | head -5Repository: few-letter/few-be
Length of output: 70
🏁 Script executed:
rg -A 5 'test {' domain/generator/build.gradle.ktsRepository: 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 -100Repository: 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/nullRepository: 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 -20Repository: 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.
| 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.
| 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") |
There was a problem hiding this comment.
시나리오 설명과 실제 예외 타입이 불일치합니다.
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).
| val bucket = "gen-cards" | ||
|
|
There was a problem hiding this comment.
운영 버킷 하드코딩으로 테스트 오염 위험이 있습니다.
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에 해당하는 변수들)에도
동일하게 적용하세요.
| // 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() | ||
|
|
There was a problem hiding this comment.
로컬 파일 정리가 예외 경로에서 누락됩니다.
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.
There was a problem hiding this comment.
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 | 🟡 MinorKotlin
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
📒 Files selected for processing (7)
domain/generator/src/main/kotlin/com/few/generator/core/instagram/NasdaqDailyStockCardGenerator.ktdomain/generator/src/main/kotlin/com/few/generator/core/kis/OverseaStockConstants.ktdomain/generator/src/main/kotlin/com/few/generator/support/common/NyseMarketCalendar.ktdomain/generator/src/main/kotlin/com/few/generator/usecase/NasdaqDailyStockCardSchedulingUseCase.ktdomain/generator/src/test/kotlin/com/few/generator/core/instagram/StockCardGeneratorIntegrationTest.ktdomain/generator/src/test/kotlin/com/few/generator/usecase/NasdaqDailyStockCardSchedulingUseCaseTest.ktdomain/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
| val etfStocks = stocks[OverseaStockConstants.StockGroup.ETF] ?: emptyList() | ||
| val m7Stocks = stocks[OverseaStockConstants.StockGroup.M7] ?: emptyList() | ||
|
|
||
| drawEtfCards(graphics, etfStocks) | ||
| drawM7Section(graphics, m7Stocks) | ||
| drawFooter(graphics, marketMood) |
There was a problem hiding this comment.
요구사항의 상위 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.
…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>
There was a problem hiding this comment.
🧹 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.shouldBeGreaterThanimport가 필요합니다.🤖 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
📒 Files selected for processing (2)
domain/generator/src/main/kotlin/com/few/generator/core/instagram/NasdaqDailyStockCardGenerator.ktdomain/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"으로 만들 수 있지만, 테스트 목적상 문제되지 않습니다.
🎫 연관 이슈
resolved #921
💁♂️ PR 내용
🙈 PR 참고 사항
🚩 추가된 SQL 운영계 실행계획
Summary by CodeRabbit
새로운 기능
테스트