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
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/retail")
@RequestMapping("/kamis") // /retail -> /kamis로 변경 (Kamis 파일들은 OpenAPI 기본 틀로 놔둘 예정)
public class PriceController {

private final KamisApiService kamisApiService;
Expand Down
130 changes: 130 additions & 0 deletions src/main/java/agridata/spring/controller/RetailPriceController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
package agridata.spring.controller;

import agridata.spring.dto.response.RetailPriceResponseDTO;
import agridata.spring.global.ApiResponse;
import agridata.spring.service.RetailPriceApiService;
import agridata.spring.service.util.KamisCodeLoader;
import agridata.spring.service.util.KamisCodeMapper;
import io.swagger.v3.oas.annotations.Operation;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.ArrayList;
import java.util.List;

@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/retail")
public class RetailPriceController {

private final RetailPriceApiService retailPriceApiService;
private final KamisCodeLoader kamisCodeLoader;

@Operation(summary = "소매 가격 불러오기 API(품목 조회하기)", description = "소매 가격 데이터를 조회합니다. 품목, 지역(코드), 시작일, 마지막일을 받아 도매 가격 리스트를 반환합니다.")
@GetMapping("/prices") // 품목별 소매 가격 조회
public ApiResponse<List<RetailPriceResponseDTO.RetailBasicDTO>> getRetailPrice(
@RequestParam String itemName,
@RequestParam(defaultValue = "") String countryCode,
@RequestParam String startDate,
@RequestParam String endDate
)

{
log.info("📥 소매 가격 조회 요청: itemName={}, countryCode={}, startDate={}, endDate={}",
itemName, countryCode, startDate, endDate);

KamisCodeMapper.KamisCode code = kamisCodeLoader.getCode(itemName);
if (code == null) {
log.warn("🥬 지원하지 않는 품목명: '{}'", itemName);
return ApiResponse.onFailure("404", "지원하지 않는 품목명입니다: " + itemName, null);
}

log.info("✅ 매핑된 코드: itemCode={}, kindCode={}, categoryCode={}, rankCode={}",
code.itemCode(), code.kindCode(), code.itemCategoryCode(), code.rankCode());

String xmlResponse = retailPriceApiService.getPriceData(
code.itemCode(), code.kindCode(), code.itemCategoryCode(), code.rankCode(),
countryCode, startDate, endDate
);

// 응답 원문 로그(Xml - ver) 출력
log.debug("📄 응답 원문:\n{}", xmlResponse);

try {
return ApiResponse.onSuccess(parseRetailPrice(xmlResponse));
} catch (Exception e) {
log.error("❌ XML 파싱 실패", e);
return ApiResponse.onFailure("500", "XML 파싱 실패: " + e.getMessage(), null);
}
}

private List<RetailPriceResponseDTO.RetailBasicDTO> parseRetailPrice(String xml){
Document doc = Jsoup.parse(xml, "", org.jsoup.parser.Parser.xmlParser());

String condition = getText(doc, "condition", "N/A");
String message = getText(doc, "error_code", "N/A");
log.info("📡 KAMIS 응답 상태: {}, 메시지: {}", condition, message);

Elements items = doc.getElementsByTag("item");
log.info("📦 파싱된 item 개수: {}", items.size());

List<RetailPriceResponseDTO.RetailBasicDTO> resultList = new ArrayList<>();

for (Element item : items) {
String price = getTagText(item, "price");

if (price == null || price.isBlank()) {
log.debug("⛔️ price 누락 항목: {}", item.outerHtml());
continue;
}

String itemname = getTagText(item, "itemname");
if (itemname == null || itemname.isBlank()) {
log.warn("⚠️ itemname 누락 항목 존재: {}", item.outerHtml());
}

String countyname = getTagText(item, "countyname");
if(countyname == null || countyname.isBlank() || countyname.equals("평년") || countyname.equals("평균")) {
log.debug("지역 누락 항목:\n{}", item.outerHtml());
continue;
}

RetailPriceResponseDTO.RetailBasicDTO dto = RetailPriceResponseDTO.RetailBasicDTO.builder()
.itemname(getTagText(item, "itemname"))
.kindname(getTagText(item, "kindname"))
.countyname(getTagText(item, "countyname"))
.marketname(getTagText(item, "marketname"))
.yyyy(getTagText(item, "yyyy"))
.regday(getTagText(item, "regday"))
.price(price)
.build();
resultList.add(dto);
}
log.info("최종 응답 항목 수: {}", resultList.size());
return resultList;
}


private String getText(Document doc, String tag, String defaultValue) {
Element el = doc.selectFirst(tag);
return el != null ? el.text() : defaultValue;
}

private String getTagText(Element element, String tag) {
Element el = element.selectFirst(tag);
return el != null ? el.text() : null;
}




}
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,9 @@ public ApiResponse<List<WholdesalePriceResponseDTO.BasicDTO>> getWholesalePrice(
@RequestParam(defaultValue = "") String countryCode,
@RequestParam String startDate,
@RequestParam String endDate
) {
)

{
KamisCodeMapper.KamisCode code = kamisCodeLoader.getCode(itemName);
if (code == null) {
log.warn("지원하지 않는 품목명: '{}'", itemName);
Expand Down
1 change: 1 addition & 0 deletions src/main/java/agridata/spring/domain/User.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ public class User extends BaseEntity {
private String nickname;
private String password;


private String email;

// 사용자 지역
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/agridata/spring/domain/enums/Region.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
package agridata.spring.domain.enums;

public enum Region {
수도권, 경상권, 전남권
수도권, 관동권, 호서권, 호남권, 영남권, 제주권
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package agridata.spring.dto.response;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.util.List;

// Retail(소매) 응답 DTO 입니다.
public class RetailPriceResponseDTO {

@Builder
@Getter
@NoArgsConstructor
@AllArgsConstructor
public static class RetailBasicDTO {
private String itemname; // 품목명
private String kindname; // 품종명
private String countyname; // 시군구
private String marketname; // 마켓명
private String yyyy; // 연도
private String regday; // 날짜
private String price; // 가격
}


@Builder
@Getter
@NoArgsConstructor
@AllArgsConstructor
// 응답이 배열 형태라면 리스트로 감싸야 하므로, 다음처럼도 사용 가능
public static class KamisRetailWrapperDTO {
private List<KamisResponseDTO.KamisRetailDTO> data;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package agridata.spring.service;

public interface RetailPriceApiService {

public String getPriceData(String itemCode, String kindCode, String itemCategorycode, String rankCode, String countryCode,
String startDate, String endDate);


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package agridata.spring.service.impl;

import agridata.spring.service.RetailPriceApiService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.client.RestTemplate;

@Slf4j
@Service
@Transactional
@RequiredArgsConstructor
public class RetailPriceApiServiceImpl implements RetailPriceApiService {

private final RestTemplate restTemplate;

@Value("${kamis.api-key}")
private String apiKey;

@Value("${kamis.cert-id}")
private String certId;

@Override
public String getPriceData(String itemCode, String kindCode, String itemCategoryCode, String rankCode,
String countryCode, String startDate, String endDate) {

StringBuilder urlBuilder = new StringBuilder("https://www.kamis.or.kr/service/price/xml.do");
urlBuilder.append("?action=periodProductList")
.append("&p_cert_key=").append(apiKey)
.append("&p_cert_id=").append(certId)
.append("&p_returntype=xml")
.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));

if (kindCode != null && !kindCode.isBlank()) {
urlBuilder.append("&p_kindcode=").append(kindCode);
}
if (rankCode != null && !rankCode.isBlank()) {
urlBuilder.append("&p_productrankcode=").append(rankCode);
}

String url = urlBuilder.toString();
log.info("📡 KAMIS 요청 URL: {}", url);

HttpHeaders headers = new HttpHeaders();
headers.set("User-Agent", "Mozilla/5.0");

HttpEntity<String> entity = new HttpEntity<>(headers);
ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.GET, entity, String.class);

return response.getBody();
}

private String formatDate(String yyyymmdd) {
return yyyymmdd.substring(0, 4) + "-" + yyyymmdd.substring(4, 6) + "-" + yyyymmdd.substring(6, 8);
}
}
Loading