Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public class KoroadApiClient {
@Value("${koroad.base-url}")
private String baseUrl;

private static final int searchYear = 2015; // 최근 3년 기준 연도
private static final int searchYear = 2024; // 최근 3년 기준 연도
private static final String PATH_OLDMAN = "/frequentzoneOldman/getRestFrequentzoneOldman";
private static final String PATH_CHILD = "/frequentzoneChild/getRestFrequentzoneChild";
private static final String PATH_SCHOOL = "/frequentzoneChildSchool/getRestFrequentzoneChildSchool";
Expand All @@ -49,7 +49,7 @@ public List<KoroadBaseResponse.Item> fetchHotspots(
String path = resolvePath(type);

String url = baseUrl + path
+ "?serviceKey=" + apiKey
+ "?ServiceKey=" + apiKey
+ "&searchYearCd=" + searchYear
+ "&siDo=" + siDo
+ "&guGun=" + guGun
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package backend.knowhow.domain.alert.controller;

import backend.knowhow.domain.alert.service.KoroadHotspotSyncService;
import backend.knowhow.domain.alert.util.KoroadRegion;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
Expand All @@ -15,63 +16,32 @@ public class KoroadHotspotSyncController {

private final KoroadHotspotSyncService syncService;

// 서울 시도 코드
private static final int SEOUL_SIDO = 11;

// 서울 전체 구/군 코드 목록 (Koroad 기준)
private static final List<Integer> SEOUL_GUGUN_CODES = List.of(
110, // 종로구
140, // 중구
170, // 용산구
200, // 성동구
215, // 광진구
230, // 동대문구
260, // 중랑구
290, // 성북구
305, // 강북구
320, // 도봉구
350, // 노원구
380, // 은평구
410, // 서대문구
440, // 마포구
470, // 양천구
500, // 강서구
530, // 구로구
545, // 금천구
560, // 영등포구
590, // 동작구
620, // 관악구
650, // 서초구
680, // 강남구
710, // 송파구
740 // 강동구
);

/**
* 서울 전체 Koroad 다발지역 동기화
*
* 예)
* - 보행노인 사고 다발지역 전체: apiType=oldman&type=OLD_MAN
* - 보행어린이: apiType=child&type=CHILD
* - 어린이보호구역: apiType=school&type=SCHOOL
* - 보행노인 사고 다발지역 전체: type=oldman
* - 보행어린이: type=child
* - 어린이보호구역: type=school
*/
@PostMapping("/sync/seoul")
@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++;
}
Comment on lines +27 to +40
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.

}

return String.format("Synced Seoul Koroad hotspots for apiType=%s, type=%s, gugunCount=%d",
apiType, type, total);
return String.format("Synced Koroad hotspots for type=%s, gugunCount=%d",
type, total);
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ public class KoroadHotspot {
* - SCHOOL : 어린이보호구역
*/
@Column(nullable = false, length = 20)
private String type;
private KoroadType type;
Comment on lines 25 to +26
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.


@Column(name = "afos_fid", length = 50)
private String afosFid;
Expand Down
30 changes: 30 additions & 0 deletions src/main/java/backend/knowhow/domain/alert/domain/KoroadType.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package backend.knowhow.domain.alert.domain;

import backend.knowhow.global.common.exception.BaseException;
import backend.knowhow.global.common.response.ErrorType;

import java.util.Arrays;

public enum KoroadType {

OLD_MAN("oldman"),
CHILD("child"),
SCHOOL("school");

private final String param;

KoroadType(String param) {
this.param = param;
}

public String param() {
return param;
}

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

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@ public AlertItem toAlert(
int m = (int) Math.round(dist);

String typeLabel = switch (hotspot.getType()) {
case "OLD_MAN" -> "보행 노인 교통사고 다발지역";
case "CHILD" -> "보행 어린이 교통사고 다발지역";
case "SCHOOL" -> "어린이 보호구역 내 교통사고 다발지역";
case OLD_MAN -> "보행 노인 교통사고 다발지역";
case CHILD -> "보행 어린이 교통사고 다발지역";
case SCHOOL -> "어린이 보호구역 내 교통사고 다발지역";
default -> "교통사고 다발지역";
};

Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
package backend.knowhow.domain.alert.repository;

import backend.knowhow.domain.alert.domain.KoroadHotspot;
import backend.knowhow.domain.alert.domain.KoroadType;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.List;

public interface KoroadHotspotRepository extends JpaRepository<KoroadHotspot, Long> {

List<KoroadHotspot> findByType(String type);
List<KoroadHotspot> findByType(KoroadType type);

List<KoroadHotspot> findByTypeAndSiDoAndGuGun(String type, Integer siDo, Integer guGun);
List<KoroadHotspot> findByTypeAndSiDoAndGuGun(KoroadType type, Integer siDo, Integer guGun);

void deleteByTypeAndSiDoAndGuGun(String type, Integer siDo, Integer guGun);
void deleteByTypeAndSiDoAndGuGun(KoroadType type, Integer siDo, Integer guGun);
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import backend.knowhow.domain.alert.client.KoroadApiClient;
import backend.knowhow.domain.alert.domain.KoroadHotspot;
import backend.knowhow.domain.alert.domain.KoroadType;
import backend.knowhow.domain.alert.dto.external.KoroadBaseResponse;
import backend.knowhow.domain.alert.repository.KoroadHotspotRepository;
import lombok.RequiredArgsConstructor;
Expand All @@ -21,17 +22,19 @@ public class KoroadHotspotSyncService {

// Koroad API → MySQL 저장 (해당 type + 시/도 + 구/군 기준으로 전체 갱신)
@Transactional
public void syncHotspots(String apiType, String type, Integer siDo, Integer guGun) {
public void syncHotspots(String type, Integer siDo, Integer guGun) {
// Koroad에서 신규 데이터 가져오기
List<KoroadBaseResponse.Item> items = koroadApiClient.fetchHotspots(apiType, siDo, guGun);
List<KoroadBaseResponse.Item> items = koroadApiClient.fetchHotspots(type, siDo, guGun);

if (items.isEmpty()) {
log.warn("[KoroadHotspotSyncService] No items fetched. Skipping sync for type={}, siDo={}, guGun={}", type, siDo, guGun);
return;
}

KoroadType koroadType = KoroadType.from(type);

// 기존 데이터 삭제 (같은 type + region 기준)
hotspotRepository.deleteByTypeAndSiDoAndGuGun(type, siDo, guGun);
hotspotRepository.deleteByTypeAndSiDoAndGuGun(koroadType, siDo, guGun);

// 3) 새 데이터 저장
List<KoroadHotspot> entities = items.stream()
Expand All @@ -45,12 +48,11 @@ public void syncHotspots(String apiType, String type, Integer siDo, Integer guGu
if (item.getLoCrd() != null)
lon = Double.parseDouble(item.getLoCrd());
} catch (NumberFormatException e) {
log.warn("[KoroadHotspotSyncService] invalid coord la={}, lo={}",
item.getLaCrd(), item.getLoCrd());
log.warn("[KoroadHotspotSyncService] invalid coord la={}, lo={}", item.getLaCrd(), item.getLoCrd());
}

return KoroadHotspot.builder()
.type(type)
.type(koroadType)
.afosFid(item.getAfosId())
.spotName(item.getSpotName())
.sidoSggName(item.getSidoSggName())
Expand Down

This file was deleted.

Loading