diff --git a/src/main/java/agridata/spring/Application.java b/src/main/java/agridata/spring/Application.java index 061aa6b..c5194b7 100644 --- a/src/main/java/agridata/spring/Application.java +++ b/src/main/java/agridata/spring/Application.java @@ -4,7 +4,7 @@ import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.scheduling.annotation.EnableScheduling; -@EnableScheduling +@EnableScheduling // 스케줄링 기능 활성화 @SpringBootApplication public class Application { diff --git a/src/main/java/agridata/spring/controller/NotificationController.java b/src/main/java/agridata/spring/controller/NotificationController.java index af01247..966d277 100644 --- a/src/main/java/agridata/spring/controller/NotificationController.java +++ b/src/main/java/agridata/spring/controller/NotificationController.java @@ -2,6 +2,7 @@ import agridata.spring.config.SecurityUtil; import agridata.spring.domain.NotificationLog; +import agridata.spring.dto.LocationCodeLoader; import agridata.spring.dto.request.NotificationRequestDTO; import agridata.spring.dto.response.NotificationLogDTO; import agridata.spring.global.ApiResponse; @@ -9,10 +10,13 @@ import agridata.spring.repository.NotificationLogRepository; import agridata.spring.service.impl.NotificationServiceImpl; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import java.util.List; +@Slf4j @RestController @RequiredArgsConstructor @RequestMapping("/api/notifications") @@ -21,6 +25,7 @@ public class NotificationController { private final NotificationLogRepository notificationLogRepository; private final NotificationServiceImpl notificationServiceImpl; private SecurityUtil securityUtil; + private final LocationCodeLoader locationCodeLoader; @PostMapping("/notifications") public ApiResponse createNotification(@RequestBody NotificationRequestDTO.CreateRequest dto) { @@ -30,6 +35,7 @@ public ApiResponse createNotification(@Req Long userId = securityUtil.getCurrentMemberId(); notificationServiceImpl.createNotification(userId, dto); + log.info("📥 알림 생성 요청: {}", dto); // ✅ 여기에 전체 DTO 로그 찍기 return ApiResponse.onSuccess(null); } @@ -42,7 +48,7 @@ public ApiResponse> getUserNotifications() { .findByNotification_User_UserIdOrderByTriggeredAtDesc(userId); List result = logs.stream() - .map(NotificationLogDTO::from) + .map(log -> NotificationLogDTO.from(log, locationCodeLoader)) // 💡 지역명 포함 변환 .toList(); return ApiResponse.onSuccess(result); diff --git a/src/main/java/agridata/spring/controller/UserController.java b/src/main/java/agridata/spring/controller/UserController.java index c18fb20..4e3c021 100644 --- a/src/main/java/agridata/spring/controller/UserController.java +++ b/src/main/java/agridata/spring/controller/UserController.java @@ -25,6 +25,13 @@ public ApiResponse registerUser(@RequestBody UserRequ return ApiResponse.onSuccess(result); } + // 닉네임, 이메일 중복 조회 + @GetMapping("/auth/check-duplicate") + public ApiResponse checkDuplicate(@RequestParam("type") String type, @RequestParam("value") String value) { + boolean isDuplicate = userQueryService.isDuplicate(type, value); + return ApiResponse.onSuccess(isDuplicate); + } + // 로그인 API @Operation(summary = "로그인 API", description = "로그인 API입니다.") @PostMapping("/auth/signin") diff --git a/src/main/java/agridata/spring/domain/Notification.java b/src/main/java/agridata/spring/domain/Notification.java index 8aea110..05130f4 100644 --- a/src/main/java/agridata/spring/domain/Notification.java +++ b/src/main/java/agridata/spring/domain/Notification.java @@ -28,6 +28,10 @@ public class Notification extends BaseEntity { @JoinColumn(name = "user_id") private User user; + // 지역 추가 + @Column(length = 10) + private String countyCode; + // @ManyToOne(fetch = FetchType.LAZY) // @JoinColumn(name = "kind_id") // private Kind kind; diff --git a/src/main/java/agridata/spring/domain/User.java b/src/main/java/agridata/spring/domain/User.java index b243ea4..598fec0 100644 --- a/src/main/java/agridata/spring/domain/User.java +++ b/src/main/java/agridata/spring/domain/User.java @@ -10,22 +10,23 @@ @Builder @NoArgsConstructor(access = AccessLevel.PROTECTED) @AllArgsConstructor -@Table(name = "User") +@Table(name = "user") public class User extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long userId; + @Column(name = "nickname", nullable = false) private String nickname; private String password; - @Column(nullable = false) + @Column(name = "email", nullable = false) private String email; - // 사용자 지역 - @Column(length = 20) - @Enumerated(EnumType.STRING) - private Region region; + // 정확한 지역명을 위해 enum(권역별) -> String(지역별) 수정 + @Column(length = 10) + private String countyCode; + // 사용자 관심 품목 private String interestItem; diff --git a/src/main/java/agridata/spring/domain/enums/Region.java b/src/main/java/agridata/spring/domain/enums/Region.java index 6b1db36..94c022c 100644 --- a/src/main/java/agridata/spring/domain/enums/Region.java +++ b/src/main/java/agridata/spring/domain/enums/Region.java @@ -1,5 +1,24 @@ package agridata.spring.domain.enums; +import java.util.List; + + +// 일단 참고 차 냅두기 public enum Region { - 수도권, 관동권, 호서권, 호남권, 영남권, 제주권 -} \ No newline at end of file + 수도권(List.of("1101", "2300", "3111", "3112", "3113", "3138", "3145")), // 서울, 인천, 수원 등 + 관동권(List.of("3211", "3214")), // 춘천, 강릉 + 호서권(List.of("2501", "3311", "3411", "2701")), // 대전, 청주, 천안, 세종 + 호남권(List.of("2401", "3511", "3613")), // 광주, 전주, 순천 + 영남권(List.of("2100", "2200", "2601", "3711", "3714", "3814", "3818")), // 부산, 대구, 울산 등 + 제주권(List.of("3911")); // 제주 + + private final List countyCodes; + + Region(List countyCodes) { + this.countyCodes = countyCodes; + } + + public List getCountyCodes() { + return countyCodes; + } +} diff --git a/src/main/java/agridata/spring/dto/LocationCodeLoader.java b/src/main/java/agridata/spring/dto/LocationCodeLoader.java new file mode 100644 index 0000000..8c94116 --- /dev/null +++ b/src/main/java/agridata/spring/dto/LocationCodeLoader.java @@ -0,0 +1,65 @@ +package agridata.spring.dto; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.annotation.PostConstruct; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.io.InputStream; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Component +@Slf4j +public class LocationCodeLoader { + + private final Map nameToCodeMap = new HashMap<>(); + private final Map codeToNameMap = new HashMap<>(); + + @PostConstruct + public void load() { + try { + ObjectMapper mapper = new ObjectMapper(); + InputStream is = getClass().getClassLoader().getResourceAsStream("location-codes.json"); + + if (is == null) { + log.error("❌ location-codes.json 파일을 찾을 수 없습니다."); + return; + } + + List locations = mapper.readValue(is, new TypeReference<>() {}); + for (LocationCode loc : locations) { + log.info("✅ 지역명 '{}' → '{}'", loc.getName(), loc.getCode()); + nameToCodeMap.put(loc.getName(), loc.getCode()); + codeToNameMap.put(loc.getCode(), loc.getName()); + } + log.info("📌 지역 코드 {}건 로드 완료", nameToCodeMap.size()); + } catch (Exception e) { + log.error("❌ 지역 코드 JSON 로딩 실패", e); + } + } + + public String getCodeByName(String name) { + if (name == null) return null; + name = name.trim().replaceAll("(시|군|구)$", ""); + // 예: "서울" → "1101" + String code = nameToCodeMap.get(name); + log.info("🔍 지역명 '{}' → 코드 '{}'", name, code); + return code; + } + + public String getNameByCode(String code) { + return codeToNameMap.get(code.trim()); + } + + @Getter + @JsonIgnoreProperties(ignoreUnknown = true) + public static class LocationCode { + private String name; + private String code; + } +} diff --git a/src/main/java/agridata/spring/dto/request/NotificationRequestDTO.java b/src/main/java/agridata/spring/dto/request/NotificationRequestDTO.java index a4a507c..313566e 100644 --- a/src/main/java/agridata/spring/dto/request/NotificationRequestDTO.java +++ b/src/main/java/agridata/spring/dto/request/NotificationRequestDTO.java @@ -17,5 +17,6 @@ public static class CreateRequest { private Integer targetPrice; private String type; // 도매 or 소매 private Boolean isActive; + private String countyCode; // 추가 } } diff --git a/src/main/java/agridata/spring/dto/request/UserRequestDTO.java b/src/main/java/agridata/spring/dto/request/UserRequestDTO.java index 2bfc478..01751da 100644 --- a/src/main/java/agridata/spring/dto/request/UserRequestDTO.java +++ b/src/main/java/agridata/spring/dto/request/UserRequestDTO.java @@ -17,7 +17,7 @@ public static class SignupDTO { private String name; private String email; private String password; - private String region; + private String countyCode; private String interestItem; } diff --git a/src/main/java/agridata/spring/dto/response/NotificationLogDTO.java b/src/main/java/agridata/spring/dto/response/NotificationLogDTO.java index 48da631..9a309c0 100644 --- a/src/main/java/agridata/spring/dto/response/NotificationLogDTO.java +++ b/src/main/java/agridata/spring/dto/response/NotificationLogDTO.java @@ -1,6 +1,7 @@ package agridata.spring.dto.response; import agridata.spring.domain.NotificationLog; +import agridata.spring.dto.LocationCodeLoader; import lombok.Builder; import lombok.Data; @@ -15,8 +16,9 @@ public class NotificationLogDTO { private String type; private Integer currentPrice; private Long notificationId; + private String countyName; // 🆕 지역명 추가 - public static NotificationLogDTO from(NotificationLog log) { + public static NotificationLogDTO from(NotificationLog log, LocationCodeLoader codeLoader) { return NotificationLogDTO.builder() .itemName(log.getField()) .message(log.getMessage()) @@ -24,7 +26,9 @@ public static NotificationLogDTO from(NotificationLog log) { .type(log.getType()) .currentPrice(log.getCurrentPrice()) .notificationId(log.getNotification().getNotificationId()) + .countyName(codeLoader.getNameByCode(log.getNotification().getCountyCode())) // ✅ 지역명 .build(); } + } diff --git a/src/main/java/agridata/spring/repository/UserRepository.java b/src/main/java/agridata/spring/repository/UserRepository.java index 0af9d63..72b097c 100644 --- a/src/main/java/agridata/spring/repository/UserRepository.java +++ b/src/main/java/agridata/spring/repository/UserRepository.java @@ -14,6 +14,9 @@ public interface UserRepository extends JpaRepository { // 이메일로 사용자 조회 Optional findByEmail(String email); + // 닉네임 중복 확인 + boolean existsByNickname(String nickname); - + // 이메일 중복 확인 + boolean existsByEmail(String email); } diff --git a/src/main/java/agridata/spring/service/NotificationScheduler.java b/src/main/java/agridata/spring/service/NotificationScheduler.java index 1c5f45b..92fa209 100644 --- a/src/main/java/agridata/spring/service/NotificationScheduler.java +++ b/src/main/java/agridata/spring/service/NotificationScheduler.java @@ -10,8 +10,10 @@ public class NotificationScheduler { private final NotificationServiceImpl notificationServiceImpl; + // 주기적으로 실행할 작업을 정의할 때 사용 +// @Scheduled(cron = "0 0 0 * * *") // 매 시간 정각마다 - @Scheduled(cron = "0 0 0 * * *") // 매 시간 정각마다 + @Scheduled(cron = "0 */1 * * * *") // 매 1분마다 실행 public void runNotificationJob() { notificationServiceImpl.checkAndLogPriceAlerts(); } diff --git a/src/main/java/agridata/spring/service/UserQueryService.java b/src/main/java/agridata/spring/service/UserQueryService.java index 8b6c5b9..715decf 100644 --- a/src/main/java/agridata/spring/service/UserQueryService.java +++ b/src/main/java/agridata/spring/service/UserQueryService.java @@ -4,4 +4,6 @@ public interface UserQueryService { public String getUserPreferItem(); public String getUserRegion(); + + public boolean isDuplicate(String type, String value); } diff --git a/src/main/java/agridata/spring/service/impl/NotificationServiceImpl.java b/src/main/java/agridata/spring/service/impl/NotificationServiceImpl.java index 6400ea9..2cbb997 100644 --- a/src/main/java/agridata/spring/service/impl/NotificationServiceImpl.java +++ b/src/main/java/agridata/spring/service/impl/NotificationServiceImpl.java @@ -9,6 +9,7 @@ import agridata.spring.repository.NotificationLogRepository; import agridata.spring.repository.NotificationRepository; import agridata.spring.repository.UserRepository; +import agridata.spring.service.RetailPriceApiService; import agridata.spring.service.WholesalePriceApiService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -23,12 +24,14 @@ import java.time.format.DateTimeFormatter; import java.util.List; -@Service + @RequiredArgsConstructor @Slf4j +@Service public class NotificationServiceImpl { - private final WholesalePriceApiService priceApiService; + private final WholesalePriceApiService wholsalePriceApiService; + private final RetailPriceApiService retailPriceApiService; private final NotificationRepository notificationRepository; private final NotificationLogRepository notificationLogRepository; private final UserRepository userRepository; @@ -46,15 +49,21 @@ public void checkAndLogPriceAlerts() { continue; } - String responseXml = priceApiService.getPriceData( - code.getItemCode(), - null, - code.getItemCategoryCode(), - null, - "", // 지역 코드 미지정 - getToday(), - getToday() - ); + String responseXml; + if (n.getType() == Type.WHOLESALE) { + responseXml = wholsalePriceApiService.getPriceData( + code.getItemCode(), null, code.getItemCategoryCode(), null, null, + getToday(), getToday() + ); + } else { + String countyCode = n.getCountyCode(); // RETAIL은 지역 필수 + + responseXml = retailPriceApiService.getPriceData( + code.getItemCode(), null, code.getItemCategoryCode(), null, + countyCode, getToday(), getToday() + ); + + } log.info("📥 응답 XML ({}): {}", n.getItemName(), responseXml); @@ -76,17 +85,22 @@ public void checkAndLogPriceAlerts() { String itemName = item.selectFirst("itemname") != null ? item.selectFirst("itemname").text() : "알 수 없음"; // 또는 n.getItemName() + + // price가 사용자가 설정한 가격(getTargetPrice)보다 높아지면 도매, 낮아지면 소매 try { int price = Integer.parseInt(priceText); - // 사용자가 설정한 가격보다 낮아졌을 때 알림 - if (price > n.getTargetPrice()) { + boolean shouldNotify = + (n.getType() == Type.WHOLESALE && price > n.getTargetPrice()) || + (n.getType() == Type.RETAIL && price < n.getTargetPrice()); + if (shouldNotify) { + String direction = (n.getType() == Type.WHOLESALE) ? "상승" : "하락"; NotificationLog logEntity = NotificationLog.builder() .field(itemName) .notification(n) .currentPrice(price) .triggeredAt(LocalDateTime.now()) - .message("가격 상승 감지 (" + county + "): " + price + "원") + .message("가격 " + direction + " 감지 (" + county + "): " + price + "원") .type(n.getType().name()) .build(); @@ -99,6 +113,7 @@ public void checkAndLogPriceAlerts() { } catch (NumberFormatException e) { log.warn("⚠ 가격 파싱 오류: '{}' (지역: {})", priceText, county); } + } } } @@ -126,6 +141,7 @@ public void createNotification(Long userId, NotificationRequestDTO.CreateRequest .type(typeEnum) // 💡 변환된 enum 사용 .targetPrice(dto.getTargetPrice()) .isActive(dto.getIsActive()) + .countyCode(dto.getCountyCode()) // 🆕 지역 추가 .build(); notificationRepository.save(notification); diff --git a/src/main/java/agridata/spring/service/impl/RetailPriceApiServiceImpl.java b/src/main/java/agridata/spring/service/impl/RetailPriceApiServiceImpl.java index 667624f..8f57d0f 100644 --- a/src/main/java/agridata/spring/service/impl/RetailPriceApiServiceImpl.java +++ b/src/main/java/agridata/spring/service/impl/RetailPriceApiServiceImpl.java @@ -38,7 +38,6 @@ public String getPriceData(String itemCode, String kindCode, String itemCategory .append("&p_productclscode=01") // 01: 소매, 02: 도매 .append("&p_itemcategorycode=").append(itemCategoryCode) .append("&p_itemcode=").append(itemCode) - .append("&p_countrycode=").append(countryCode) .append("&p_convert_kg_yn=Y") .append("&p_startday=").append(formatDate(startDate)) .append("&p_endday=").append(formatDate(endDate)); @@ -50,6 +49,10 @@ public String getPriceData(String itemCode, String kindCode, String itemCategory urlBuilder.append("&p_productrankcode=").append(rankCode); } + if (countryCode != null && !countryCode.isBlank()) { + urlBuilder.append("&p_countrycode=").append(countryCode); + } + String url = urlBuilder.toString(); log.info("📡 KAMIS 요청 URL: {}", url); diff --git a/src/main/java/agridata/spring/service/impl/UserCommandServiceImpl.java b/src/main/java/agridata/spring/service/impl/UserCommandServiceImpl.java index 1684ae6..20981f5 100644 --- a/src/main/java/agridata/spring/service/impl/UserCommandServiceImpl.java +++ b/src/main/java/agridata/spring/service/impl/UserCommandServiceImpl.java @@ -1,7 +1,7 @@ package agridata.spring.service.impl; import agridata.spring.domain.User; -import agridata.spring.domain.enums.Region; +import agridata.spring.dto.LocationCodeLoader; import agridata.spring.dto.request.UserRequestDTO; import agridata.spring.dto.response.UserResponseDTO; import agridata.spring.repository.UserRepository; @@ -24,19 +24,33 @@ public class UserCommandServiceImpl implements UserCommandService { private final TokenProvider tokenProvider; private final PasswordEncoder passwordEncoder; // WebSecurityConfig에서 @Bean으로 설정해놓아서, 주입하기만 하면 됨 + private final LocationCodeLoader locationCodeLoader; // 회원가입 @Override public UserResponseDTO.SignupDTO create(UserRequestDTO.SignupDTO dto) { + // 🔒 Null 체크 먼저! + if (dto.getCountyCode() == null || dto.getCountyCode().isBlank()) { + throw new IllegalArgumentException("지역명이 입력되지 않았습니다."); + } + + String countyInput = dto.getCountyCode().trim(); + + // 🔹 지역 코드 조회 + String regionCode = locationCodeLoader.getCodeByName(countyInput); + if (regionCode == null) { + throw new IllegalArgumentException("유효하지 않은 지역명입니다: " + countyInput); + } + User user = User.builder() .nickname(dto.getName()) .email(dto.getEmail()) - .password(passwordEncoder.encode(dto.getPassword())) // 비밀번호를 BCrypt 암호화해서 저장 - .region(Region.valueOf(dto.getRegion())) + .password(passwordEncoder.encode(dto.getPassword())) + .countyCode(regionCode) .interestItem(dto.getInterestItem()) .build(); - User result = userRepository.save(user); + User result = userRepository.save(user); return UserResponseDTO.SignupDTO.builder().id(result.getUserId()).build(); } diff --git a/src/main/java/agridata/spring/service/impl/UserQueryServiceImpl.java b/src/main/java/agridata/spring/service/impl/UserQueryServiceImpl.java index d980256..03fb50c 100644 --- a/src/main/java/agridata/spring/service/impl/UserQueryServiceImpl.java +++ b/src/main/java/agridata/spring/service/impl/UserQueryServiceImpl.java @@ -23,6 +23,21 @@ public String getUserPreferItem() { @Override public String getUserRegion() { Long memberId = SecurityUtil.getCurrentMemberId(); - return userRepository.findById(memberId).get().getRegion().name(); + return userRepository.findById(memberId).get().getCountyCode(); + } + + @Override + public boolean isDuplicate(String type, String value) { + if ("name".equals(type)) { + boolean exists = userRepository.existsByNickname(value); + System.out.println("닉네임 중복 여부: " + value + " → " + exists); + return exists; + } else if ("email".equals(type)) { + boolean exists = userRepository.existsByEmail(value); + System.out.println("이메일 중복 여부: " + value + " → " + exists); + return exists; + } else { + throw new IllegalArgumentException("Invalid type"); + } } } diff --git a/src/main/java/agridata/spring/service/impl/WholesalePriceApiServiceImpl.java b/src/main/java/agridata/spring/service/impl/WholesalePriceApiServiceImpl.java index 09c7348..38caded 100644 --- a/src/main/java/agridata/spring/service/impl/WholesalePriceApiServiceImpl.java +++ b/src/main/java/agridata/spring/service/impl/WholesalePriceApiServiceImpl.java @@ -38,7 +38,6 @@ public String getPriceData(String itemCode, String kindCode, String itemCategory .append("&p_productclscode=02") // 01: 소매, 02: 도매 .append("&p_itemcategorycode=").append(itemCategoryCode) .append("&p_itemcode=").append(itemCode) - .append("&p_countrycode=").append(countryCode) .append("&p_convert_kg_yn=Y") .append("&p_startday=").append(formatDate(startDate)) .append("&p_endday=").append(formatDate(endDate)); @@ -50,6 +49,10 @@ public String getPriceData(String itemCode, String kindCode, String itemCategory urlBuilder.append("&p_productrankcode=").append(rankCode); } + if (countryCode != null && !countryCode.isBlank()) { + urlBuilder.append("&p_countrycode=").append(countryCode); + } + String url = urlBuilder.toString(); log.info("📡 KAMIS 요청 URL: {}", url); diff --git a/src/main/resources/location-codes.json b/src/main/resources/location-codes.json new file mode 100644 index 0000000..90b5865 --- /dev/null +++ b/src/main/resources/location-codes.json @@ -0,0 +1,26 @@ +[ + { "name": "서울", "code": "1101" }, + { "name": "부산", "code": "2100" }, + { "name": "대구", "code": "2200" }, + { "name": "인천", "code": "2300" }, + { "name": "광주", "code": "2401" }, + { "name": "대전", "code": "2501" }, + { "name": "울산", "code": "2601" }, + { "name": "세종", "code": "2701" }, + { "name": "수원", "code": "3111" }, + { "name": "성남", "code": "3112" }, + { "name": "의정부", "code": "3113" }, + { "name": "고양", "code": "3138" }, + { "name": "용인", "code": "3145" }, + { "name": "춘천", "code": "3211" }, + { "name": "강릉", "code": "3214" }, + { "name": "청주", "code": "3311" }, + { "name": "천안", "code": "3411" }, + { "name": "전주", "code": "3511" }, + { "name": "순천", "code": "3613" }, + { "name": "포항", "code": "3711" }, + { "name": "안동", "code": "3714" }, + { "name": "창원", "code": "3814" }, + { "name": "김해", "code": "3818" }, + { "name": "제주", "code": "3911" } +] diff --git a/src/main/resources/location-mark.json b/src/main/resources/location-mark.json index ed06eed..1589e80 100644 --- a/src/main/resources/location-mark.json +++ b/src/main/resources/location-mark.json @@ -86,6 +86,7 @@ {"city": "정선군", "item": "사과", "longitude": 128.6606, "latitude": 37.3798}, {"city": "정선군", "item": "풋고추", "longitude": 128.6606, "latitude": 37.3798}, {"city": "정선군", "item": "건고추", "longitude": 128.6606, "latitude": 37.3798}, + {"city": "정선군", "item": "콩", "longitude": 128.6606, "latitude": 37.3798}, {"city": "철원군", "item": "파프리카", "longitude": 127.3124, "latitude": 38.1461}, {"city": "철원군", "item": "토마토", "longitude": 127.3124, "latitude": 38.1461}, @@ -95,6 +96,7 @@ {"city": "춘천시", "item": "오이", "longitude": 127.7298, "latitude": 37.8813}, {"city": "춘천시", "item": "메론", "longitude": 127.7298, "latitude": 37.8813}, {"city": "춘천시", "item": "복숭아", "longitude": 127.7298, "latitude": 37.8813}, + {"city": "춘천시", "item": "감자", "longitude": 127.7298, "latitude": 37.8813}, {"city": "태백시", "item": "배추", "longitude": 128.9853, "latitude": 37.1641}, {"city": "태백시", "item": "사과", "longitude": 128.9853, "latitude": 37.1641}, @@ -107,19 +109,16 @@ {"city": "평창군", "item": "양배추", "longitude": 128.3903, "latitude": 37.3704}, {"city": "평창군", "item": "브로콜리", "longitude": 128.3903, "latitude": 37.3704}, {"city": "평창군", "item": "대파", "longitude": 128.3903, "latitude": 37.3704}, - {"city": "평창군", "item": "호박", "longitude": 128.3903, "latitude": 37.3704}, - {"city": "평창군", "item": "풋고추", "longitude": 128.3903, "latitude": 37.3704}, - {"city": "평창군", "item": "오이", "longitude": 128.3903, "latitude": 37.3704}, - {"city": "평창군", "item": "사과", "longitude": 128.3903, "latitude": 37.3704}, - {"city": "평창군", "item": "풋고추", "longitude": 128.3903, "latitude": 37.3704}, - {"city": "평창군", "item": "오이", "longitude": 128.3903, "latitude": 37.3704}, - {"city": "평창군", "item": "감자", "longitude": 128.3903, "latitude": 37.3704}, - {"city": "평창군", "item": "배추", "longitude": 128.3903, "latitude": 37.3704}, {"city": "홍천군", "item": "호박", "longitude": 127.888, "latitude": 37.6913}, + {"city": "홍천군", "item": "풋고추", "longitude": 127.888, "latitude": 37.6913}, {"city": "홍천군", "item": "오이", "longitude": 127.888, "latitude": 37.6913}, + {"city": "홍천군", "item": "사과", "longitude": 127.888, "latitude": 37.6913}, {"city": "홍천군", "item": "풋고추", "longitude": 127.888, "latitude": 37.6913}, - {"city": "홍천군", "item": "토마토", "longitude": 127.888, "latitude": 37.6913}, + {"city": "홍천군", "item": "오이", "longitude": 127.888, "latitude": 37.6913}, {"city": "홍천군", "item": "감자", "longitude": 127.888, "latitude": 37.6913}, + {"city": "홍천군", "item": "배추", "longitude": 127.888, "latitude": 37.6913}, + {"city": "홍천군", "item": "호박", "longitude": 127.888, "latitude": 37.6913}, + {"city": "홍천군", "item": "토마토", "longitude": 127.888, "latitude": 37.6913}, {"city": "홍천군", "item": "무", "longitude": 127.888, "latitude": 37.6913}, {"city": "화천군", "item": "토마토", "longitude": 127.7062, "latitude": 38.1057}, diff --git a/src/test/java/agridata/spring/service/impl/NotificationServiceImplTest.java b/src/test/java/agridata/spring/service/impl/NotificationServiceImplTest.java index 0390345..664607b 100644 --- a/src/test/java/agridata/spring/service/impl/NotificationServiceImplTest.java +++ b/src/test/java/agridata/spring/service/impl/NotificationServiceImplTest.java @@ -3,13 +3,13 @@ import agridata.spring.domain.Notification; import agridata.spring.domain.NotificationLog; import agridata.spring.domain.User; -import agridata.spring.domain.enums.Region; import agridata.spring.domain.enums.Type; import agridata.spring.dto.ItemCsvMapper; import agridata.spring.dto.request.NotificationRequestDTO; import agridata.spring.repository.NotificationLogRepository; import agridata.spring.repository.NotificationRepository; import agridata.spring.repository.UserRepository; +import agridata.spring.service.RetailPriceApiService; import agridata.spring.service.WholesalePriceApiService; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -35,6 +35,7 @@ class NotificationServiceImplTest { @Mock private UserRepository userRepository; @Mock private ItemCsvMapper itemCsvMapper; @Mock private WholesalePriceApiService wholesalePriceApiService; + @Mock private RetailPriceApiService retailPriceApiService; @Mock private NotificationLogRepository notificationLogRepository; @InjectMocks @@ -126,7 +127,7 @@ void setUp() { @Test void 가격이_올라가면_알림() { // given - User user = User.builder().userId(1L).region(Region.수도권).build(); + User user = User.builder().userId(1L).countyCode("1101").build(); Notification notification = Notification.builder() .notificationId(1L) .user(user) @@ -154,7 +155,7 @@ void setUp() { // 실제 객체에 mock 주입 notificationService = new NotificationServiceImpl( - wholesalePriceApiService, notificationRepository, + wholesalePriceApiService, retailPriceApiService, notificationRepository, notificationLogRepository, userRepository, itemCsvMapper ); @@ -164,12 +165,13 @@ void setUp() { // then verify(notificationLogRepository, times(1)).save(any()); } + // 도매 @Test - void 가격이_설정값보다_높으면_알림로그_저장된다() { + void 가격이_도매가격_설정값보다_높으면_알림로그_저장된다() { // given User user = User.builder() .userId(1L) - .region(agridata.spring.domain.enums.Region.수도권) + .countyCode("1101") // 예: 서울의 코드값 .build(); Notification notification = Notification.builder() @@ -195,12 +197,13 @@ void setUp() { when(itemCsvMapper.getCode("쌀")).thenReturn(code); when(wholesalePriceApiService.getPriceData( eq("111"), isNull(), eq("100"), isNull(), - eq("수도권"), any(), any() + eq("1101"), any(), any() )).thenReturn(sampleXml); // when notificationService = new NotificationServiceImpl( wholesalePriceApiService, + retailPriceApiService, notificationRepository, notificationLogRepository, userRepository, @@ -227,6 +230,65 @@ void setUp() { assertThat(savedLog.getType()).isEqualTo("WHOLESALE"); assertThat(savedLog.getMessage()).contains("설정한 가격 이상 도달"); } + // 소매 + @Test + void 가격이_소매가격_설정값보다_낮으면_알림로그_저장된다() { + // given + User user = User.builder() + .userId(1L) + .countyCode("1101") // 예: 서울 + .build(); + + Notification notification = Notification.builder() + .notificationId(1L) + .user(user) + .itemName("양배추") + .targetPrice(8000) // 사용자가 설정한 목표 가격 + .type(Type.RETAIL) + .isActive(true) + .build(); + + ItemCsvMapper.ItemCode code = new ItemCsvMapper.ItemCode("200", "222", "양배추"); + + String sampleXml = """ + + + 7000 // ✅ 설정값보다 낮음 + + + """; + + when(notificationRepository.findAllByIsActiveTrue()).thenReturn(List.of(notification)); + when(itemCsvMapper.getCode("양배추")).thenReturn(code); + when(retailPriceApiService.getPriceData( + eq("200"), isNull(), eq("222"), isNull(), + eq("1101"), any(), any() + )).thenReturn(sampleXml); + + notificationService = new NotificationServiceImpl( + wholesalePriceApiService, + retailPriceApiService, + notificationRepository, + notificationLogRepository, + userRepository, + itemCsvMapper + ); + + // when + notificationService.checkAndLogPriceAlerts(); + + // then + ArgumentCaptor captor = ArgumentCaptor.forClass(NotificationLog.class); + verify(notificationLogRepository, times(1)).save(captor.capture()); + + NotificationLog savedLog = captor.getValue(); + + // 검증 + assertThat(savedLog.getCurrentPrice()).isEqualTo(7000); + assertThat(savedLog.getType()).isEqualTo("RETAIL"); + assertThat(savedLog.getMessage()).contains("설정한 가격 이하 도달"); + } + } \ No newline at end of file