Skip to content

feat: 병천 날씨 조회 API 추가#2276

Open
dh2906 wants to merge 10 commits into
developfrom
feat/weather-api
Open

feat: 병천 날씨 조회 API 추가#2276
dh2906 wants to merge 10 commits into
developfrom
feat/weather-api

Conversation

@dh2906
Copy link
Copy Markdown
Contributor

@dh2906 dh2906 commented May 27, 2026

🔍 개요

  • 병천 고정 위치의 날씨 정보를 서버가 주기적으로 가져와 /weather에서 제공할 수 있게 했습니다.
  • 유저 요청 시 외부 기상청 API를 직접 호출하지 않고, 매시 정각 스케줄러가 갱신한 Redis 캐시만 응답 경로에서 사용합니다.
  • close 병천 날씨 조회 API 추가 #2274

🚀 주요 변경 내용

  • GET /weather 응답을 기온과 날씨 상태 두 값으로 구성했습니다.
  • 기상청 단기예보 API를 병천면 격자 좌표(nx=66, ny=109)로 호출하고, TMP, SKY, PTY 값을 화면용 응답으로 매핑했습니다.
  • 단기예보 발표시각(02/05/08/11/14/17/20/23시)과 제공 지연을 고려해 조회 기준 시간을 계산하고, 데이터 미제공 시 직전 발표시각으로 한 번 보정합니다.
  • Redis 캐시와 1시간 주기 스케줄러를 추가해 외부 API 호출을 사용자 조회 경로에서 분리했습니다.

💬 참고 사항

  • Redis 캐시 TTL은 2시간입니다. 스케줄러가 1시간마다 갱신하되, 일시적인 기상청 API 실패가 있어도 직전 값을 조금 더 유지하기 위한 설정입니다.
  • 기상청 인증/권한 오류가 발생하면 HTTP 오류 본문을 파싱해 실제 인증 오류 메시지를 남기도록 했습니다.
  • 검증: ./gradlew test --tests 'in.koreatech.koin.unit.domain.weather.*' --tests 'in.koreatech.koin.acceptance.domain.WeatherApiTest' --no-daemon

✅ Checklist (완료 조건)

  • 코드 스타일 가이드 준수
  • 테스트 코드 포함됨
  • Reviewers / Assignees / Labels 지정 완료
  • 보안 및 민감 정보 검증 (API 키, 환경 변수, 개인정보 등)

Compound Engineering
GPT-5 Codex

Summary by CodeRabbit

  • New Features

    • Added a GET /weather endpoint returning temperature and weather condition.
    • Integrated Korea Meteorological Administration short-term forecast and local caching for served data.
    • Added automatic refresh on startup and hourly scheduled cache refresh.
  • Documentation

    • Expanded API docs to include the new weather endpoint and example response.
  • Tests

    • Added unit and acceptance tests covering forecast parsing, request-time fallback, cache behavior, and service refresh.

Review Change Stack

dh2906 added 6 commits May 27, 2026 13:28
- 기상청 단기예보 API를 병천면 고정 좌표로 호출해 화면에 필요한 날씨 정보를 제공한다.
- 응답을 기온과 날씨 상태로 제한해 클라이언트가 디자인에 필요한 값만 의존하도록 했다.
- 발표시각 보정과 SKY/PTY 코드 매핑을 분리해 잘못된 예보시각이나 상태값 노출을 막는다.
- 컨트롤러 인수 테스트와 날씨 도메인 단위 테스트로 기본 조회 흐름을 검증했다.
- 서버 인스턴스가 여러 대여도 같은 예보시각에는 Redis 캐시를 공유하도록 했다.
- 캐시 키를 병천 고정 위치로 제한하고 1시간 TTL을 적용해 기상청 API 중복 호출을 줄인다.
- 캐시된 예보시각이 현재 계산된 예보시각과 다를 때만 새 데이터를 가져오도록 했다.
- Redis 캐시 적중과 미적중 흐름을 단위 테스트로 검증했다.
- 병천 날씨 조회 API가 Swagger UI의 Campus API 그룹에 노출되도록 패키지 스캔 범위에 포함했다.
- API 구현과 캐시 동작은 유지하고 문서 탐색 가능성만 보완했다.
- 기상청 응답을 문자열로 먼저 받아 XML 에러나 비정상 JSON 응답의 원인을 메시지에 남기도록 했다.
- 최신 발표시각 예보가 아직 없거나 예보 항목이 누락된 경우에만 직전 발표시각으로 한 번 재조회한다.
- 인증키 오류 같은 재시도로 해결되지 않는 응답은 그대로 드러내 불필요한 중복 호출을 막는다.
- 직전 발표시각 계산 테스트를 추가해 자정 경계 fallback을 검증했다.
- 기상청 API가 401/403 같은 HTTP 오류를 반환해도 응답 본문을 파싱해 실제 인증 오류 메시지를 남긴다.
- 인증키 오류와 서비스 접근 권한 오류를 구분할 수 있게 해 운영 설정 확인 시간을 줄인다.
- 기존 날씨 도메인 테스트를 다시 실행해 응답 처리 변경의 회귀를 확인했다.
- 사용자 조회 요청에서 기상청 API를 호출하지 않고 Redis 캐시만 읽도록 책임을 분리했다.
- 매시 정각 스케줄러가 병천 날씨를 갱신해 캐시 miss가 사용자 요청 경로에서 외부 호출로 이어지지 않게 했다.
- 스케줄 경계와 일시적인 갱신 실패를 버티도록 캐시 TTL을 조회 주기보다 길게 설정했다.
- API 조회, 캐시 miss, 스케줄러 갱신 흐름을 테스트로 검증했다.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 27, 2026

📝 Walkthrough

Walkthrough

Adds a weather feature: KMA short-term forecast client with retry and XML/JSON error handling, domain DTOs and enums, Redis caching plus hourly scheduler, a GET /weather REST endpoint with OpenAPI docs, and unit/acceptance tests.

Changes

Weather Forecast Feature

Layer / File(s) Summary
Domain models and contracts
src/main/java/in/koreatech/koin/domain/weather/model/WeatherCondition.java, WeatherForecastRequestTime.java, WeatherForecast.java, WeatherCache.java; src/main/java/in/koreatech/koin/domain/weather/dto/WeatherResponse.java, WeatherApiResponse.java; src/main/java/in/koreatech/koin/domain/weather/exception/WeatherOpenApiException.java
Records and enum for forecast/request time, response DTOs, Redis cache entity, and domain exception type.
External Weather Client
src/main/java/in/koreatech/koin/domain/weather/client/WeatherClient.java
Builds KMA OpenAPI URL, issues HTTP GET via RestTemplate, parses JSON into WeatherApiResponse, extracts TMP/SKY/PTY for requested forecastDate/forecastTime, handles non-JSON XML errors, and retries once with previous base time on specific no-data errors.
Service, repository and scheduler
src/main/java/in/koreatech/koin/domain/weather/service/WeatherService.java, repository/WeatherCacheRepository.java, scheduler/WeatherScheduler.java
Read-through cache and synchronized refresh: WeatherService loads/saves WeatherCache by fixed key, uses Clock to derive requestTime, calls WeatherClient, persists WeatherCache; scheduler triggers refresh on startup and hourly.
REST endpoint and docs
src/main/java/in/koreatech/koin/domain/weather/controller/WeatherApi.java, WeatherController.java; src/main/java/in/koreatech/koin/global/config/SwaggerGroupConfig.java
Defines and implements GET /weather with OpenAPI annotations; Swagger group now includes weather package.
Tests
src/test/java/.../WeatherApiTest.java, WeatherClientTest.java, WeatherServiceTest.java, WeatherForecastRequestTimeTest.java, WeatherConditionTest.java
Acceptance test for endpoint; unit tests for client parsing/fallback, service caching/refresh, request-time computation, and condition mapping.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested reviewers

  • BaeJinho4028
  • Soundbar91
  • taejinn

"I hopped through code with ears held high,
REST and cache beneath the sky.
TMP, SKY, PTY — parsed with care,
Retry once, then cached to share.
Hop, fetch, save — weather done with flair." 🐰

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% 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 The PR title clearly describes the main change: adding a Byeongcheon weather lookup API, which aligns with the primary objective of the changeset.
Linked Issues check ✅ Passed The implementation meets all key requirements: GET /weather endpoint [WeatherApi, WeatherController], Redis caching with hourly refresh [WeatherCache, WeatherScheduler, WeatherService], TMP/SKY/PTY extraction [WeatherForecast, WeatherCondition], base-time calculation with 1-retry fallback [WeatherForecastRequestTime, WeatherClient], and external error detail preservation [WeatherOpenApiException].
Out of Scope Changes check ✅ Passed All changes are directly aligned with weather API implementation: domain models, client, service, controller, DTOs, exceptions, caching, scheduling, and comprehensive tests. SwaggerGroupConfig update appropriately registers the new weather package.

✏️ 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/weather-api

Warning

Review ran into problems

🔥 Problems

Stopped waiting for pipeline failures after 30000ms. One of your pipelines takes longer than our 30000ms fetch window to run, so review may not consider pipeline-failure results for inline comments if any failures occurred after the fetch window. Increase the timeout if you want to wait longer or run a @coderabbit review after the pipeline has finished.


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.

@dh2906 dh2906 added the 기능 새로운 기능을 개발합니다. label May 27, 2026
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 27, 2026

Unit Test Results

674 tests   671 ✔️  1m 16s ⏱️
168 suites      3 💤
168 files        0

Results for commit 362882a.

♻️ This comment has been updated with latest results.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/main/java/in/koreatech/koin/domain/weather/client/WeatherClient.java`:
- Line 157: The exception currently throws KoinIllegalStateException with the
full urlBuilder (which contains the serviceKey) causing potential API key
leakage; update the throw in WeatherClient to avoid including the full request
URI—either remove urlBuilder from the message or include only non-sensitive
parts (e.g., the path or a redacted URI that strips or masks the
serviceKey/query param), and ensure the thrown message still provides context
like "Failed to build short-term forecast API URL" while not logging the
sensitive serviceKey.
- Around line 191-193: The null-unsafe comparison of resultCode in WeatherClient
can throw NPE; update the conditional that currently uses resultCode.equals(...)
so it handles nulls (e.g., replace with a null-safe check such as if (resultCode
== null || (!"00".equals(resultCode) && !"0".equals(resultCode))) or use
Objects.equals/Objects.nonNull accordingly) so that missing resultCode still
triggers WeatherOpenApiException.withDetail; reference the resultCode variable
and the WeatherOpenApiException.withDetail call when making the change.

In `@src/main/java/in/koreatech/koin/domain/weather/model/WeatherCache.java`:
- Around line 9-21: The WeatherCache Redis entity is using the wrong Id
annotation: replace the jakarta.persistence.Id import with
org.springframework.data.annotation.Id so the `@RedisHash`("Weather") entity maps
correctly; update the import statement and ensure the private String id field in
class WeatherCache is annotated with org.springframework.data.annotation.Id
instead of jakarta.persistence.Id.

In
`@src/main/java/in/koreatech/koin/domain/weather/model/WeatherForecastRequestTime.java`:
- Around line 30-40: The calculation in
WeatherForecastRequestTime.from(LocalDateTime now) uses
now.toLocalTime().minusMinutes(10) which can wrap across midnight and detach
date context; instead compute a single date-aware timestamp (e.g.,
availableDateTime = now.minusMinutes(10)), then derive baseDate =
availableDateTime.toLocalDate() and availableTime =
availableDateTime.toLocalTime() before iterating BASE_TIMES to find baseTime;
keep the existing fallback that if baseTime == null you decrement baseDate and
pick the last BASE_TIMES entry so the date and time remain consistent across
midnight boundary.

In
`@src/main/java/in/koreatech/koin/domain/weather/scheduler/WeatherScheduler.java`:
- Around line 17-24: Add a startup invocation of the existing refresh logic so
the cache is populated immediately on app start: in WeatherScheduler, keep the
existing `@Scheduled`(cron = "0 0 * * * *") public void refreshWeather() as-is but
also call weatherService.refreshWeather() once during startup (for example from
a method annotated with `@EventListener`(ApplicationReadyEvent.class) or
`@PostConstruct`) and wrap it with the same try/catch/logging used in
refreshWeather() to surface errors; reference the WeatherScheduler class, the
refreshWeather() method, and the weatherService.refreshWeather() call when
making the change.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 73d11a6e-7730-4f67-ad1c-0fb9f8152068

📥 Commits

Reviewing files that changed from the base of the PR and between 872c03b and addb2eb.

📒 Files selected for processing (18)
  • src/main/java/in/koreatech/koin/domain/weather/client/WeatherClient.java
  • src/main/java/in/koreatech/koin/domain/weather/controller/WeatherApi.java
  • src/main/java/in/koreatech/koin/domain/weather/controller/WeatherController.java
  • src/main/java/in/koreatech/koin/domain/weather/dto/WeatherApiResponse.java
  • src/main/java/in/koreatech/koin/domain/weather/dto/WeatherResponse.java
  • src/main/java/in/koreatech/koin/domain/weather/exception/WeatherOpenApiException.java
  • src/main/java/in/koreatech/koin/domain/weather/model/WeatherCache.java
  • src/main/java/in/koreatech/koin/domain/weather/model/WeatherCondition.java
  • src/main/java/in/koreatech/koin/domain/weather/model/WeatherForecast.java
  • src/main/java/in/koreatech/koin/domain/weather/model/WeatherForecastRequestTime.java
  • src/main/java/in/koreatech/koin/domain/weather/repository/WeatherCacheRepository.java
  • src/main/java/in/koreatech/koin/domain/weather/scheduler/WeatherScheduler.java
  • src/main/java/in/koreatech/koin/domain/weather/service/WeatherService.java
  • src/main/java/in/koreatech/koin/global/config/SwaggerGroupConfig.java
  • src/test/java/in/koreatech/koin/acceptance/domain/WeatherApiTest.java
  • src/test/java/in/koreatech/koin/unit/domain/weather/WeatherConditionTest.java
  • src/test/java/in/koreatech/koin/unit/domain/weather/WeatherForecastRequestTimeTest.java
  • src/test/java/in/koreatech/koin/unit/domain/weather/WeatherServiceTest.java

Comment thread src/main/java/in/koreatech/koin/domain/weather/client/WeatherClient.java Outdated
Comment thread src/main/java/in/koreatech/koin/domain/weather/model/WeatherCache.java Outdated
dh2906 and others added 3 commits May 27, 2026 14:26
- 기상청 요청 URL 생성 실패 시 serviceKey가 포함된 URI를 남기지 않도록 민감 정보를 제거

- Redis 캐시 식별자와 기상청 resultCode 검증을 Spring Data 및 null-safe 방식으로 보정

- 자정 직후 발표시각 계산과 애플리케이션 기동 시 날씨 캐시 초기화를 보강
- 기상청 응답에서 TMP, SKY, PTY 중 일부가 누락되면 기본 날씨로 오인하지 않고 비정상 응답으로 처리

- 예보시각 데이터가 아예 없는 경우에만 직전 발표시각 재조회가 동작하도록 실패 원인을 분리

- WeatherClient 단위 테스트로 정상 매핑, 직전 발표시각 fallback, 필수 항목 누락 경로를 고정
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
src/test/java/in/koreatech/koin/unit/domain/weather/WeatherClientTest.java (1)

89-92: ⚡ Quick win

Assert the missing-category detail, not only the default exception message.

Right now this can pass even if a different failure path throws the same default message. Please also assert the detail (e.g., "missing category: SKY") to pin this test to the intended contract on Line 89.

Proposed test tightening
-        assertThatThrownBy(() -> weatherClient.getWeatherForecast(REQUEST_TIME))
-            .isInstanceOf(WeatherOpenApiException.class)
-            .hasMessage("기상청 단기예보 API 응답이 정상적이지 않습니다.");
+        assertThatThrownBy(() -> weatherClient.getWeatherForecast(REQUEST_TIME))
+            .isInstanceOf(WeatherOpenApiException.class)
+            .hasMessage("기상청 단기예보 API 응답이 정상적이지 않습니다.")
+            .satisfies(ex -> assertThat(((WeatherOpenApiException) ex).getFullMessage())
+                .contains("missing category: SKY"));
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/test/java/in/koreatech/koin/unit/domain/weather/WeatherClientTest.java`
around lines 89 - 92, The test currently only asserts the default exception
message for weatherClient.getWeatherForecast but not the specific error detail;
update the assertion for WeatherOpenApiException (thrown by
weatherClient.getWeatherForecast(REQUEST_TIME)) to also assert the exception's
detail field (e.g., call getDetail() on the caught WeatherOpenApiException)
equals the expected string "missing category: SKY", so the test pins the failure
path to the intended contract rather than any exception with the same default
message.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/test/java/in/koreatech/koin/unit/domain/weather/WeatherClientTest.java`:
- Around line 89-92: The test currently only asserts the default exception
message for weatherClient.getWeatherForecast but not the specific error detail;
update the assertion for WeatherOpenApiException (thrown by
weatherClient.getWeatherForecast(REQUEST_TIME)) to also assert the exception's
detail field (e.g., call getDetail() on the caught WeatherOpenApiException)
equals the expected string "missing category: SKY", so the test pins the failure
path to the intended contract rather than any exception with the same default
message.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c3846ec1-fac7-4aaa-9aa8-659ba2b583f8

📥 Commits

Reviewing files that changed from the base of the PR and between addb2eb and acb8c83.

📒 Files selected for processing (6)
  • src/main/java/in/koreatech/koin/domain/weather/client/WeatherClient.java
  • src/main/java/in/koreatech/koin/domain/weather/model/WeatherCache.java
  • src/main/java/in/koreatech/koin/domain/weather/model/WeatherForecastRequestTime.java
  • src/main/java/in/koreatech/koin/domain/weather/scheduler/WeatherScheduler.java
  • src/test/java/in/koreatech/koin/unit/domain/weather/WeatherClientTest.java
  • src/test/java/in/koreatech/koin/unit/domain/weather/WeatherForecastRequestTimeTest.java
🚧 Files skipped from review as they are similar to previous changes (5)
  • src/main/java/in/koreatech/koin/domain/weather/scheduler/WeatherScheduler.java
  • src/main/java/in/koreatech/koin/domain/weather/model/WeatherCache.java
  • src/main/java/in/koreatech/koin/domain/weather/model/WeatherForecastRequestTime.java
  • src/test/java/in/koreatech/koin/unit/domain/weather/WeatherForecastRequestTimeTest.java
  • src/main/java/in/koreatech/koin/domain/weather/client/WeatherClient.java

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

Labels

기능 새로운 기능을 개발합니다.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

병천 날씨 조회 API 추가

1 participant