Skip to content

Conversation

@jettieb
Copy link
Contributor

@jettieb jettieb commented Jan 16, 2026

🚘 작업 내용

이번 PR에서 작업한 내용을 간략히 설명해주세요.

  • 서울 제외 교통사고 다발지역 응답 추가

#️⃣ 테스트 내용

어떤 테스트를 수행했는지 명시해주세요.

  • Postman 테스트
  • 단위 테스트 수행
  • 통합 테스트 수행

🔗 관련 이슈


Summary by CodeRabbit

릴리스 노트

  • New Features

    • 타입 시스템 개선으로 데이터 유효성 강화
  • Refactor

    • 하드코딩된 상수를 동적 지역 기반 처리로 전환
    • API 요청 파라미터 최적화
    • 불필요한 기능 제거 및 서비스 정리
    • 검색 시간 범위 업데이트 (최근 3년)

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Jan 16, 2026

Walkthrough

Koroad 핫스팟 동기화 시스템을 서울 특화 구현에서 동적 지역 기반 구현으로 변경합니다. 타입 필드를 String에서 KoroadType 열거형으로 변환하고, 지역 정보를 관리하는 새로운 유틸리티 클래스를 추가하며, 불필요한 KoroadRiskService를 제거합니다.

Changes

Cohort / File(s) 변경 요약
API 클라이언트 업데이트
src/main/java/backend/knowhow/domain/alert/client/KoroadApiClient.java
searchYear 상수를 2015에서 2024로 변경; API 쿼리 파라미터명을 serviceKey에서 ServiceKey로 변경
컨트롤러 리팩토링
src/main/java/backend/knowhow/domain/alert/controller/KoroadHotspotSyncController.java
서울 특화 로직 제거; 엔드포인트를 /sync/seoul에서 /sync로 변경; 동적 지역 기반 반복 처리 추가; 메서드 서명에서 apiType 파라미터 제거
열거형 타입 정의
src/main/java/backend/knowhow/domain/alert/domain/KoroadType.java
새로운 KoroadType 열거형 추가 (OLD_MAN, CHILD, SCHOOL); case-insensitive 문자열 매핑 및 유효성 검증 기능 포함
도메인 엔티티 타입 변경
src/main/java/backend/knowhow/domain/alert/domain/KoroadHotspot.java
type 필드를 String에서 KoroadType 열거형으로 변경
매퍼 업데이트
src/main/java/backend/knowhow/domain/alert/mapper/KoroadHotspotMapper.java
switch 케이스를 문자열 비교에서 열거형 상수 비교로 변경
저장소 인터페이스 수정
src/main/java/backend/knowhow/domain/alert/repository/KoroadHotspotRepository.java
메서드 서명 업데이트: String type 파라미터를 KoroadType으로 변경 (3개 메서드)
서비스 로직 업데이트
src/main/java/backend/knowhow/domain/alert/service/KoroadHotspotSyncService.java
메서드 서명에서 apiType 파라미터 제거; KoroadType 매핑 로직 추가; 저장소 호출 시 열거형 타입 사용
지역 관리 유틸리티
src/main/java/backend/knowhow/domain/alert/util/KoroadRegion.java
새로운 유틸리티 클래스 추가: 전국 행정구역별 지역 코드 매핑을 포함한 SIDO_GUGUN 맵 정의
서비스 제거
src/main/java/backend/knowhow/domain/alert/service/KoroadRiskService.java
기하학적 좌표 기반 핫스팟 판정 로직을 담당하던 클래스 전체 삭제

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Controller as Controller<br/>(Sync)
    participant Region as KoroadRegion
    participant Service as SyncService
    participant ApiClient as ApiClient
    participant Repository as Repository

    rect rgba(100, 150, 255, 0.5)
    Note over Client,Repository: 이전: 서울 전용 동기화
    Client->>Controller: POST /sync/seoul<br/>(apiType, type)
    Controller->>Service: syncHotspots<br/>(apiType, type, sido, gugun)
    Service->>ApiClient: fetchHotspots<br/>(apiType, sido, gugun)
    ApiClient-->>Service: hotspots
    Service->>Repository: deleteByTypeAndSiDoAndGuGun<br/>(type[String], sido, gugun)
    Service->>Repository: save(hotspots)
    end

    rect rgba(150, 200, 100, 0.5)
    Note over Client,Repository: 이후: 동적 지역 기반 동기화
    Client->>Controller: POST /sync(type)
    Controller->>Region: SIDO_GUGUN.entrySet()
    Region-->>Controller: 모든 지역 반복
    loop 각 지역(sido, gugun)
        Controller->>Service: syncHotspots<br/>(type, sido, gugun)
        Service->>ApiClient: fetchHotspots<br/>(type, sido, gugun)
        ApiClient-->>Service: hotspots
        Note over Service: KoroadType.from(type)<br/>타입 변환
        Service->>Repository: deleteByTypeAndSiDoAndGuGun<br/>(koroadType[Enum], sido, gugun)
        Service->>Repository: save(hotspots)
    end
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested labels

✨ feature

Poem

🐰 호핑하며 온 지역 동기화
서울만 바라보던 눈을 들고,
온 나라의 핫스팟을 한번에 폴폴~
String에서 Enum으로 타입도 쏙!
리팩토링 완료, 귀 쫑긋! 🌟

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 27.27% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed PR 제목은 주요 변경사항인 '서울 제외 교통사고 다발지역 응답 추가'를 명확하게 요약하고 있으며, 변경사항과 일치합니다.

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

✨ Finishing touches
  • 📝 Generate docstrings

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.

Copy link

@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: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/main/java/backend/knowhow/domain/alert/client/KoroadApiClient.java (1)

51-83: API 키가 로그에 노출됩니다.

Line 63, 69, 82에서 url을 그대로 로깅하면 ServiceKey가 외부에 노출됩니다. 민감정보이므로 마스킹/제외가 필요합니다.

🔒️ 개선 예시 (키 마스킹)
-        String url = baseUrl + path
+        String url = baseUrl + path
                 + "?ServiceKey=" + apiKey
                 + "&searchYearCd=" + searchYear
                 + "&siDo=" + siDo
                 + "&guGun=" + guGun
                 + "&type=json"
                 + "&numOfRows=9999"
                 + "&pageNo=1";
+        String safeUrl = url.replace(apiKey, "****");
...
-                log.warn("[KoroadApiClient] Null response. url={}", url);
+                log.warn("[KoroadApiClient] Null response. url={}", safeUrl);
...
-                        url, res.getResultCode(), res.getResultMsg()
+                        safeUrl, res.getResultCode(), res.getResultMsg()
...
-            log.error("[KoroadApiClient] fetchHotspots error. type={}, url={}, message={}", type, url, e.getMessage(), e);
+            log.error("[KoroadApiClient] fetchHotspots error. type={}, url={}, message={}", type, safeUrl, e.getMessage(), e);
🤖 Fix all issues with AI agents
In
`@src/main/java/backend/knowhow/domain/alert/controller/KoroadHotspotSyncController.java`:
- Around line 27-40: Validate the incoming type before entering the nested loop
in syncSeoul: call KoroadType.from(type) once at the top of the method (e.g.,
assign to a variable like KoroadType koroadType = KoroadType.from(type)) so
invalid types immediately trigger the existing
BaseException(ErrorType.BAD_REQUEST) and prevent repeated calls to
syncService.syncHotspots; then use the validated koroadType (or simply ignore
and continue using type) inside the loop over KoroadRegion.SIDO_GUGUN when
invoking syncService.syncHotspots.

In `@src/main/java/backend/knowhow/domain/alert/domain/KoroadHotspot.java`:
- Around line 25-26: The KoroadHotspot.type field is currently using JPA's
default ORDINAL mapping which will mismatch the existing VARCHAR column; add an
explicit enum mapping by annotating the field (KoroadHotspot.type) with
`@Enumerated`(EnumType.STRING) so KoroadType values are persisted as their names;
ensure the corresponding import for javax.persistence.Enumerated and
javax.persistence.EnumType is added/updated and run a quick schema/data check to
confirm compatibility with the existing string column.

In `@src/main/java/backend/knowhow/domain/alert/domain/KoroadType.java`:
- Around line 24-28: KoroadType.from currently calls equalsIgnoreCase on the
incoming value and will NPE for null (and should reject blank inputs); update
KoroadType.from to first validate the input (e.g., if value == null or
value.trim().isEmpty()) and immediately throw new
BaseException(ErrorType.BAD_REQUEST), then proceed with
Arrays.stream(values())...filter(type ->
type.param.equalsIgnoreCase(value))...orElseThrow(...) as before so
equalsIgnoreCase is only invoked on non-null, non-blank input.
🧹 Nitpick comments (1)
src/main/java/backend/knowhow/domain/alert/client/KoroadApiClient.java (1)

28-28: searchYear 하드코딩 대신 동적 계산 권장.

“최근 3년 기준”이라면 매년 값이 바뀌어야 하므로 연도 하드코딩은 데이터 최신성에 영향을 줍니다. 시스템 날짜 기반 계산 또는 설정값으로 전환하는 쪽이 안전합니다.

♻️ 개선 예시
-    private static final int searchYear = 2024;  // 최근 3년 기준 연도
+    private static final int searchYear = java.time.Year.now().getValue() - 2;  // 최근 3년 기준 연도
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7f0d07d and adf2be5.

📒 Files selected for processing (9)
  • src/main/java/backend/knowhow/domain/alert/client/KoroadApiClient.java
  • src/main/java/backend/knowhow/domain/alert/controller/KoroadHotspotSyncController.java
  • src/main/java/backend/knowhow/domain/alert/domain/KoroadHotspot.java
  • src/main/java/backend/knowhow/domain/alert/domain/KoroadType.java
  • src/main/java/backend/knowhow/domain/alert/mapper/KoroadHotspotMapper.java
  • src/main/java/backend/knowhow/domain/alert/repository/KoroadHotspotRepository.java
  • src/main/java/backend/knowhow/domain/alert/service/KoroadHotspotSyncService.java
  • src/main/java/backend/knowhow/domain/alert/service/KoroadRiskService.java
  • src/main/java/backend/knowhow/domain/alert/util/KoroadRegion.java
💤 Files with no reviewable changes (1)
  • src/main/java/backend/knowhow/domain/alert/service/KoroadRiskService.java
🧰 Additional context used
🧬 Code graph analysis (1)
src/main/java/backend/knowhow/domain/alert/controller/KoroadHotspotSyncController.java (1)
src/main/java/backend/knowhow/domain/alert/util/KoroadRegion.java (1)
  • KoroadRegion (8-350)
🔇 Additional comments (4)
src/main/java/backend/knowhow/domain/alert/util/KoroadRegion.java (1)

1-350: 구성 명확하고 사용 목적에 부합합니다.

시도/구군 코드 매핑을 중앙화한 점이 좋습니다.

src/main/java/backend/knowhow/domain/alert/mapper/KoroadHotspotMapper.java (1)

27-32: Enum 전환과 문구 매핑이 깔끔합니다.

타입 안정성이 좋아졌습니다.

src/main/java/backend/knowhow/domain/alert/repository/KoroadHotspotRepository.java (1)

11-15: KoroadType 기반 쿼리 메서드 전환 좋습니다.
타입 안정성이 높아지고 상위 레이어와 일관성이 생겼습니다.

src/main/java/backend/knowhow/domain/alert/service/KoroadHotspotSyncService.java (1)

25-55: KoroadType 매핑과 저장 흐름 정리가 깔끔합니다.
enum으로 통일되면서 타입 안정성이 좋아졌습니다.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment on lines +27 to +40
@PostMapping("/sync")
public String syncSeoul(
@RequestParam String apiType, // KoroadApiClient 에서 사용하는 구분자 ("oldman", "child", "school" 등)
@RequestParam String type // DB KoroadHotspot.type 에 저장할 값 ("OLD_MAN", "CHILD", "SCHOOL" 등)
@RequestParam String type // KoroadApiClient 에서 사용하는 구분자 ("oldman", "child", "school" 등)
) {

log.info("start");
log.info("sync start");
int total = 0;

for (Integer guGun : SEOUL_GUGUN_CODES) {
syncService.syncHotspots(apiType, type, SEOUL_SIDO, guGun);
log.info("{} finish", guGun);
total++;
for (var entry : KoroadRegion.SIDO_GUGUN.entrySet()) {
int siDo = entry.getKey();
for (int guGun : entry.getValue()) {
syncService.syncHotspots(type, siDo, guGun);
total++;
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's locate the controller file and understand its structure
find . -type f -name "KoroadHotspotSyncController.java"

Repository: Care-Ride/backend

Length of output: 149


🏁 Script executed:

# Search for KoroadType class definition
find . -type f -name "KoroadType.java"

Repository: Care-Ride/backend

Length of output: 128


🏁 Script executed:

# Look for syncService.syncHotspots() method
find . -type f -name "*SyncService*.java" -o -name "*Service*.java" | grep -i sync

Repository: Care-Ride/backend

Length of output: 143


🏁 Script executed:

# Read KoroadType.java
cat ./src/main/java/backend/knowhow/domain/alert/domain/KoroadType.java

Repository: Care-Ride/backend

Length of output: 772


🏁 Script executed:

# Read the full KoroadHotspotSyncController.java
cat ./src/main/java/backend/knowhow/domain/alert/controller/KoroadHotspotSyncController.java

Repository: Care-Ride/backend

Length of output: 1324


🏁 Script executed:

# Read the syncService implementation
cat ./src/main/java/backend/knowhow/domain/alert/service/KoroadHotspotSyncService.java

Repository: Care-Ride/backend

Length of output: 2846


🏁 Script executed:

# Find BaseException definition
find . -type f -name "BaseException.java"

Repository: Care-Ride/backend

Length of output: 135


🏁 Script executed:

# Find ErrorType definition
find . -type f -name "ErrorType.java"

Repository: Care-Ride/backend

Length of output: 130


🏁 Script executed:

# Read BaseException
cat ./src/main/java/backend/knowhow/global/common/exception/BaseException.java

Repository: Care-Ride/backend

Length of output: 561


🏁 Script executed:

# Read ErrorType
cat ./src/main/java/backend/knowhow/global/common/response/ErrorType.java

Repository: Care-Ride/backend

Length of output: 3365


요청 파라미터 type 검증을 루프 시작 전에 추가하기

요청 파라미터 type이 잘못되면 중첩 루프 내에서 syncService.syncHotspots()를 호출할 때마다 외부 API 통신을 시도하다가 결국 KoroadType.from(type)에서 BaseException(ErrorType.BAD_REQUEST)를 던지게 됩니다. 루프 시작 전에 미리 한 번 검증하여 불필요한 외부 API 호출을 방지하고 즉시 400으로 응답하세요.

✅ 제안 수정안
     `@PostMapping`("/sync")
     public String syncSeoul(
             `@RequestParam` String type  // KoroadApiClient 에서 사용하는 구분자 ("oldman", "child", "school" 등)
     ) {
+        KoroadType.from(type);  // 루프 전에 한 번만 검증
 
         log.info("sync start");
         int total = 0;

참고: KoroadType.from(type)이 이미 BaseException(ErrorType.BAD_REQUEST)를 던지므로, 별도의 try-catch 처리 없이 기존 예외 처리 메커니즘이 자동으로 400을 반환합니다.

📝 Committable suggestion

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

Suggested change
@PostMapping("/sync")
public String syncSeoul(
@RequestParam String apiType, // KoroadApiClient 에서 사용하는 구분자 ("oldman", "child", "school" 등)
@RequestParam String type // DB KoroadHotspot.type 에 저장할 값 ("OLD_MAN", "CHILD", "SCHOOL" 등)
@RequestParam String type // KoroadApiClient 에서 사용하는 구분자 ("oldman", "child", "school" 등)
) {
log.info("start");
log.info("sync start");
int total = 0;
for (Integer guGun : SEOUL_GUGUN_CODES) {
syncService.syncHotspots(apiType, type, SEOUL_SIDO, guGun);
log.info("{} finish", guGun);
total++;
for (var entry : KoroadRegion.SIDO_GUGUN.entrySet()) {
int siDo = entry.getKey();
for (int guGun : entry.getValue()) {
syncService.syncHotspots(type, siDo, guGun);
total++;
}
`@PostMapping`("/sync")
public String syncSeoul(
`@RequestParam` String type // KoroadApiClient 에서 사용하는 구분자 ("oldman", "child", "school" 등)
) {
KoroadType.from(type); // 루프 전에 한 번만 검증
log.info("sync start");
int total = 0;
for (var entry : KoroadRegion.SIDO_GUGUN.entrySet()) {
int siDo = entry.getKey();
for (int guGun : entry.getValue()) {
syncService.syncHotspots(type, siDo, guGun);
total++;
}
🤖 Prompt for AI Agents
In
`@src/main/java/backend/knowhow/domain/alert/controller/KoroadHotspotSyncController.java`
around lines 27 - 40, Validate the incoming type before entering the nested loop
in syncSeoul: call KoroadType.from(type) once at the top of the method (e.g.,
assign to a variable like KoroadType koroadType = KoroadType.from(type)) so
invalid types immediately trigger the existing
BaseException(ErrorType.BAD_REQUEST) and prevent repeated calls to
syncService.syncHotspots; then use the validated koroadType (or simply ignore
and continue using type) inside the loop over KoroadRegion.SIDO_GUGUN when
invoking syncService.syncHotspots.

Comment on lines 25 to +26
@Column(nullable = false, length = 20)
private String type;
private KoroadType type;
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Enum 매핑 방식 지정 누락으로 데이터 불일치 위험.

KoroadType을 저장할 때 JPA 기본값이 ORDINAL이라 기존 문자열 컬럼과 호환되지 않습니다. STRING 매핑을 명시해야 합니다.

🐛 수정 제안
-    `@Column`(nullable = false, length = 20)
+    `@Enumerated`(EnumType.STRING)
+    `@Column`(nullable = false, length = 20)
     private KoroadType type;
📝 Committable suggestion

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

Suggested change
@Column(nullable = false, length = 20)
private String type;
private KoroadType type;
`@Enumerated`(EnumType.STRING)
`@Column`(nullable = false, length = 20)
private KoroadType type;
🤖 Prompt for AI Agents
In `@src/main/java/backend/knowhow/domain/alert/domain/KoroadHotspot.java` around
lines 25 - 26, The KoroadHotspot.type field is currently using JPA's default
ORDINAL mapping which will mismatch the existing VARCHAR column; add an explicit
enum mapping by annotating the field (KoroadHotspot.type) with
`@Enumerated`(EnumType.STRING) so KoroadType values are persisted as their names;
ensure the corresponding import for javax.persistence.Enumerated and
javax.persistence.EnumType is added/updated and run a quick schema/data check to
confirm compatibility with the existing string column.

Comment on lines +24 to +28
public static KoroadType from(String value) {
return Arrays.stream(values())
.filter(type -> type.param.equalsIgnoreCase(value))
.findFirst()
.orElseThrow(() -> new BaseException(ErrorType.BAD_REQUEST));
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

null 입력 시 NPE로 500 발생 가능.

from(null) 호출 시 equalsIgnoreCase에서 NPE가 발생합니다. 입력값이 비어 있는 경우도 BAD_REQUEST로 일관되게 처리하도록 방어가 필요합니다.

🐛 수정 제안
     public static KoroadType from(String value) {
+        if (value == null || value.isBlank()) {
+            throw new BaseException(ErrorType.BAD_REQUEST);
+        }
         return Arrays.stream(values())
                 .filter(type -> type.param.equalsIgnoreCase(value))
                 .findFirst()
                 .orElseThrow(() -> new BaseException(ErrorType.BAD_REQUEST));
     }
📝 Committable suggestion

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

Suggested change
public static KoroadType from(String value) {
return Arrays.stream(values())
.filter(type -> type.param.equalsIgnoreCase(value))
.findFirst()
.orElseThrow(() -> new BaseException(ErrorType.BAD_REQUEST));
public static KoroadType from(String value) {
if (value == null || value.isBlank()) {
throw new BaseException(ErrorType.BAD_REQUEST);
}
return Arrays.stream(values())
.filter(type -> type.param.equalsIgnoreCase(value))
.findFirst()
.orElseThrow(() -> new BaseException(ErrorType.BAD_REQUEST));
}
🤖 Prompt for AI Agents
In `@src/main/java/backend/knowhow/domain/alert/domain/KoroadType.java` around
lines 24 - 28, KoroadType.from currently calls equalsIgnoreCase on the incoming
value and will NPE for null (and should reject blank inputs); update
KoroadType.from to first validate the input (e.g., if value == null or
value.trim().isEmpty()) and immediately throw new
BaseException(ErrorType.BAD_REQUEST), then proceed with
Arrays.stream(values())...filter(type ->
type.param.equalsIgnoreCase(value))...orElseThrow(...) as before so
equalsIgnoreCase is only invoked on non-null, non-blank input.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants