Skip to content
Merged
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
2 changes: 1 addition & 1 deletion src/main/java/agridata/spring/Application.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;

@EnableScheduling
@EnableScheduling // 스케줄링 기능 활성화
@SpringBootApplication
public class Application {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,21 @@

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;
import agridata.spring.global.error.status.ErrorStatus;
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")
Expand All @@ -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<NotificationRequestDTO.CreateRequest> createNotification(@RequestBody NotificationRequestDTO.CreateRequest dto) {
Expand All @@ -30,6 +35,7 @@ public ApiResponse<NotificationRequestDTO.CreateRequest> createNotification(@Req

Long userId = securityUtil.getCurrentMemberId();
notificationServiceImpl.createNotification(userId, dto);
log.info("📥 알림 생성 요청: {}", dto); // ✅ 여기에 전체 DTO 로그 찍기
return ApiResponse.onSuccess(null);
}

Expand All @@ -42,7 +48,7 @@ public ApiResponse<List<NotificationLogDTO>> getUserNotifications() {
.findByNotification_User_UserIdOrderByTriggeredAtDesc(userId);

List<NotificationLogDTO> result = logs.stream()
.map(NotificationLogDTO::from)
.map(log -> NotificationLogDTO.from(log, locationCodeLoader)) // 💡 지역명 포함 변환
.toList();

return ApiResponse.onSuccess(result);
Expand Down
7 changes: 7 additions & 0 deletions src/main/java/agridata/spring/controller/UserController.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,13 @@ public ApiResponse<UserResponseDTO.SignupDTO> 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")
Expand Down
4 changes: 4 additions & 0 deletions src/main/java/agridata/spring/domain/Notification.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
13 changes: 7 additions & 6 deletions src/main/java/agridata/spring/domain/User.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
23 changes: 21 additions & 2 deletions src/main/java/agridata/spring/domain/enums/Region.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,24 @@
package agridata.spring.domain.enums;

import java.util.List;


// 일단 참고 차 냅두기
public enum Region {
수도권, 관동권, 호서권, 호남권, 영남권, 제주권
}
수도권(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<String> countyCodes;

Region(List<String> countyCodes) {
this.countyCodes = countyCodes;
}

public List<String> getCountyCodes() {
return countyCodes;
}
}
65 changes: 65 additions & 0 deletions src/main/java/agridata/spring/dto/LocationCodeLoader.java
Original file line number Diff line number Diff line change
@@ -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<String, String> nameToCodeMap = new HashMap<>();
private final Map<String, String> 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<LocationCode> 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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,6 @@ public static class CreateRequest {
private Integer targetPrice;
private String type; // 도매 or 소매
private Boolean isActive;
private String countyCode; // 추가
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -15,16 +16,19 @@ 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())
.triggeredAt(log.getTriggeredAt().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")))
.type(log.getType())
.currentPrice(log.getCurrentPrice())
.notificationId(log.getNotification().getNotificationId())
.countyName(codeLoader.getNameByCode(log.getNotification().getCountyCode())) // ✅ 지역명
.build();
}

}

5 changes: 4 additions & 1 deletion src/main/java/agridata/spring/repository/UserRepository.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ public interface UserRepository extends JpaRepository<User, Long> {
// 이메일로 사용자 조회
Optional<User> findByEmail(String email);

// 닉네임 중복 확인
boolean existsByNickname(String nickname);


// 이메일 중복 확인
boolean existsByEmail(String email);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/agridata/spring/service/UserQueryService.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,6 @@ public interface UserQueryService {
public String getUserPreferItem();

public String getUserRegion();

public boolean isDuplicate(String type, String value);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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);

Expand All @@ -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();

Expand All @@ -99,6 +113,7 @@ public void checkAndLogPriceAlerts() {
} catch (NumberFormatException e) {
log.warn("⚠ 가격 파싱 오류: '{}' (지역: {})", priceText, county);
}

}
}
}
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand All @@ -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);

Expand Down
Loading
Loading