오픈 API 기상청_단기예보_조회서비스 사용/파싱 (2)

P·2022년 1월 24일
0

1. getWeather() 함수 작성

이전 글에서 DB에서 지역 정보를 조회하고 동적으로 지역이 변경되게 만들었다.
이제 지역 + 날짜 + 시간 값을 가지고 실행을 했을 시, API요청을 실제로 수행하는 getWeather()함수를 작성한다.

function getWeather() {
	var nullCheck = true;
	$('.emptyCheck').each(function (){
		if ('' == $(this).val()){
			alert($(this).attr('title') + "을(를) 확인바람");
			nullCheck = false;
			return false;	// 빈 값에서 멈춤
		}
	});

	if (nullCheck) {
		var time = $('#time').val()+'00';
		if ($('#time').val() < 10){
			time = "0" + time;
		}
		var areacode = "";
		var cityCode = $('#step1 option:selected').val();
		var countyCode = $('#step2 option:selected').val();
		var townCode = $('#step3 option:selected').val();

		if (townCode == '' && countyCode == '') {
			areacode = cityCode;
		}
		else if(townCode == '' && countyCode != '') {
			areacode = countyCode;
		}
		else if(townCode != '') {
			areacode = townCode;
		}

		var date = $('#datepicker').val();
		var data = {"areacode" : areacode, "baseDate" : date, "baseTime" : time};

		$.ajax({
			url: "/board/getWeather.do",
			data: data,
			dataType: "JSON",
			method : "POST",
			success : function(res){
				console.log(res);
				if (res[0].resultCode != null) {
					alert(res[0].resultMsg);
				}
				else {
					var html = "";
					html += "<tbody><tr><th>nx=" + res[0].nx + "</th><th>ny=" + res[0].ny + "</th></tr>";

					$("#resultWeather").empty();
					$.each(res, function(index, item){
						html += "<tr><td>" + item.category + "</td><td>" + item.obsrValue + "</td></tr>";
					});

					html += "</tbody>";
					$("#resultWeather").append(html);
				}
		        },
		        error : function(xhr){
	                alert(xhr.responseText);
		        }
		});
	}
}

javascript에 위 함수를 추가한다.
html 태그에 class=emptyCheck가 설정되어 있는 요소들을 .each를 사용하여 모두 순회하면서, 입력되지 않은 값이 있으면 해당 요소가 비어있다는 alert창과 함께 API요청을 실행하지 않는다.

2. Controller 수정

@Controller
public class WeatherController
{
    @Autowired
    private WeatherService weatherService;

    @GetMapping(value = "/board/weather.do")
    public String openWeatherPage()
    {
        return "/weather/weather";
    }

    @PostMapping(value = "/board/getWeather.do")
    @ResponseBody
    public List<WeatherDTO> getWeatherInfo(@ModelAttribute AreaRequestDTO areaRequestDTO) throws JsonMappingException, JsonProcessingException, UnsupportedEncodingException, URISyntaxException
    {
        AreaRequestDTO coordinate = this.weatherService.getCoordinate(areaRequestDTO.getAreacode());
        areaRequestDTO.setNx(coordinate.getNx());
        areaRequestDTO.setNy(coordinate.getNy());

        List<WeatherDTO> weatherList = this.weatherService.getWeather(areaRequestDTO);
        return weatherList;
    }

    @PostMapping(value = "/board/weatherStep.do")
    @ResponseBody
    public List<AreaRequestDTO> getAreaStep(@RequestParam Map<String, String> params)
    {
        return this.weatherService.getArea(params);
    }
}

파라미터의 AreaRequestDTO는 위의 getWeather()함수의 data(JSON형식)로 설정한 areacode, baseDate, baseTime 값이 설정되어 있는 DTO이다.
받은 값의 areacode(행정구역코드)를 이용하여 getCoordinate()메서드를 실행하면, 해당 areacode와 일치하는 ny, ny값이 coordinate 변수에 저장된다.

nx, ny값 저장 후 getWeather()메서드를 실행하면 areaRequestDTO에 들어있는 변수들을 가지고 API요청을 실행한다.
최종적으로 weatherList에는 API결과값 정보를 담고 있는 WeatherDTO 값들이 담겨져 있을 것이다.

3. Service 수정

기존 Service에 있었던 getArea()메서드는 남겨두고 아래의 코드를 추가한다.
Service

public interface WeatherService
{
    List<WeatherDTO> getWeather(AreaRequestDTO areaRequestDTO) throws UnsupportedEncodingException, URISyntaxException, JsonMappingException, JsonProcessingException;

    List<AreaRequestDTO> getArea(Map<String, String> params);

    AreaRequestDTO getCoordinate(String areacode);
    
    ResponseEntity<WeatherApiResponseDTO> requestWeatherApi(AreaRequestDTO areaRequestDTO) throws UnsupportedEncodingException, URISyntaxException;
}

ServiceImpl

@Service
@Slf4j
public class WeatherServiceImpl implements WeatherService
{
    @Autowired
    private WeatherMapper weatherMapper;

    @Override
    public List<WeatherDTO> getWeather(AreaRequestDTO areaRequestDTO) throws UnsupportedEncodingException, URISyntaxException, JsonMappingException, JsonProcessingException
    {
        List<WeatherDTO> weatherList = this.weatherMapper.selectSameCoordinateWeatherList(areaRequestDTO); // 날짜, 시간, nx, ny가 requestDTO의 값과 일치하는 데이터가 있는지 검색
        if ( weatherList.isEmpty() )
        {
            ResponseEntity<WeatherApiResponseDTO> response = requestWeatherApi(areaRequestDTO); // 데이터가 하나도 없는 경우 새로 생성
            ObjectMapper objectMapper = new ObjectMapper();
            List<WeatherItemDTO> weatherItemList = response.getBody()
                                                           .getResponse()
                                                           .getBody()
                                                           .getItems()
                                                           .getItem();
            for ( WeatherItemDTO item : weatherItemList )
            {
                weatherList.add(objectMapper.readValue(objectMapper.writeValueAsString(item), WeatherDTO.class));
            }
            this.weatherMapper.insertWeatherList(weatherList); // API요청 후 결과값을 DB에 저장

            return weatherList; 	// 로그를 찍지 않으려면 삭제해도 OK
        }
        return weatherList;	// DB에 기존 저장되어있는 값에서 가져온 List
    }

    @Override
    public List<AreaRequestDTO> getArea(Map<String, String> params)
    {
        return this.weatherMapper.selectArea(params);
    }

    @Override
    public AreaRequestDTO getCoordinate(String areacode)
    {
        return this.weatherMapper.selectCoordinate(areacode);
    }

    @Override
    public ResponseEntity<WeatherApiResponseDTO> requestWeatherApi(AreaRequestDTO areaRequestDTO) throws UnsupportedEncodingException, URISyntaxException
    {
        String url = "http://apis.data.go.kr/1360000/VilageFcstInfoService_2.0/getUltraSrtNcst";
        String serviceKey = "여기에 Decoding Service Key를 입력";
        String encodedServiceKey = URLEncoder.encode(serviceKey, "UTF-8");

        RestTemplate restTemplate = new RestTemplate();
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(new MediaType("application", "JSON", Charset.forName("UTF-8")));

        StringBuilder builder = new StringBuilder(url);
        builder.append("?" + URLEncoder.encode("serviceKey", "UTF-8") + "=" + encodedServiceKey);
        builder.append("&" + URLEncoder.encode("pageNo", "UTF-8") + "=" + URLEncoder.encode("1", "UTF-8"));
        builder.append("&" + URLEncoder.encode("numOfRows", "UTF-8") + "=" + URLEncoder.encode("1000", "UTF-8"));
        builder.append("&" + URLEncoder.encode("dataType", "UTF-8") + "=" + URLEncoder.encode("JSON", "UTF-8"));
        builder.append("&" + URLEncoder.encode("base_date", "UTF-8") + "=" + URLEncoder.encode(areaRequestDTO.getBaseDate(), "UTF-8"));
        builder.append("&" + URLEncoder.encode("base_time", "UTF-8") + "=" + URLEncoder.encode(areaRequestDTO.getBaseTime(), "UTF-8"));
        builder.append("&" + URLEncoder.encode("nx", "UTF-8") + "=" + URLEncoder.encode(areaRequestDTO.getNx(), "UTF-8"));
        builder.append("&" + URLEncoder.encode("ny", "UTF-8") + "=" + URLEncoder.encode(areaRequestDTO.getNy(), "UTF-8"));
        URI uri = new URI(builder.toString());

        ResponseEntity<WeatherApiResponseDTO> response = restTemplate.exchange(uri, HttpMethod.GET, new HttpEntity<String>(headers), WeatherApiResponseDTO.class);
        return response;
    }
}
  • getWeather()메서드는 요청을 위한 정보가 담겨있는 객체인 AreaRequestDTO를 전달받아 API요청 전에 먼저 DB에서 조회하는 메서드 selectSameCoordinateWeatherList()를 실행한다. (좌표값이 있는 엑셀파일을 보면 같은 구, 같은 동인데도 nx, ny값은 같은 위치가 많이 있기 때문에 이 경우에는 API요청을 하지 않고 DB에서만 조회하는 방식으로 한다)
  • 실제 API요청을 수행하는 부분은 requestWeatherApi() 메서드이다.

4. Mapper 수정

@Mapper
public interface WeatherMapper
{
    List<AreaRequestDTO> selectArea(Map<String, String> params);

    AreaRequestDTO selectCoordinate(String areacode);

    List<WeatherDTO> selectSameCoordinateWeatherList(AreaRequestDTO areaRequestDTO);

    void insertWeatherList(List<WeatherDTO> weatherList);
}

mybatis

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.my.board.mapper.WeatherMapper">

	<select id="selectArea" resultType="AreaRequestDTO" parameterType="Map">
		<choose>
			<when test="type == 'city'">
				SELECT
					areacode, step1
				FROM
					tb_weather_area
				WHERE
					step2 = "" AND step3 = ""
				ORDER BY
					step1
			</when>

			<when test="type == 'county'">
				SELECT
					areacode, step2
				FROM
					tb_weather_area
				WHERE
					step1 = #{keyword} AND step3 = "" AND step2 != ""
				ORDER BY
					step2
			</when>

			<when test="type == 'town'">
				SELECT
					areacode, step3
				FROM
					tb_weather_area
				WHERE
					step2 = #{keyword} AND step3 != ""
				ORDER BY
					step3
			</when>
		</choose>
	</select>

	<select id="selectCoordinate" parameterType="String" resultType="AreaRequestDTO">
		SELECT
			gridX as nx, gridY as ny
		FROM
			tb_weather_area
		WHERE
			areacode = #{areacode}
	</select>

	<select id="selectSameCoordinateWeatherList" parameterType="AreaRequestDTO" resultType="WeatherDTO">
		SELECT DISTINCT
			baseDate, baseTime, category, nx, ny, obsrValue
		FROM
			tw_weather_response
		WHERE
			baseDate = #{baseDate} AND
			baseTime = #{baseTime} AND
			nx = #{nx} AND
			ny = #{ny}
	</select>

	<insert id="insertWeatherList" parameterType="WeatherDTO">
		INSERT INTO tw_weather_response(
			baseDate
			,baseTime
			,category
			,nx
			,ny
			,obsrValue
		)
		VALUES
			<foreach collection="list" item="item" open="(" separator="),(" close=")">
				#{item.baseDate}
				,#{item.baseTime}
				,#{item.category}
				,#{item.nx}
				,#{item.ny}
				,#{item.obsrValue}
			</foreach>
	</insert>

</mapper>

5. DB 테이블 생성

dbms에서 API응답값들을 저장할 테이블을 생성한다.

CREATE TABLE `tw_weather_response` (
	`baseDate` VARCHAR(50) NOT NULL COMMENT '발표일자' COLLATE 'utf8_general_ci',
	`baseTime` VARCHAR(50) NOT NULL COMMENT '발표시각' COLLATE 'utf8_general_ci',
	`category` VARCHAR(50) NOT NULL COMMENT '자료구분코드' COLLATE 'utf8_general_ci',
	`nx` VARCHAR(50) NOT NULL COMMENT '예보지점X좌표' COLLATE 'utf8_general_ci',
	`ny` VARCHAR(50) NOT NULL COMMENT '예보지점Y좌표' COLLATE 'utf8_general_ci',
	`obsrValue` VARCHAR(50) NOT NULL COMMENT '실황 값' COLLATE 'utf8_general_ci'
)
COMMENT='날씨 API 호출 응답값 저장'
COLLATE='utf8_general_ci'
ENGINE=InnoDB
;

작성하고 나면 아래와 같은 테이블 구성이 만들어진다.

API요청 후 값들은 전부 위의 테이블에 저장된다.

6. DTO 작성

WeatherApiResponseDTO - RestTemplate방식으로 API요청 후 응답값 객체(ResponseEntity)를 받기 위해 만든 DTO, 변수 명 response는 실제 API요청 결과와 일치해야 함.

<200,WeatherApiResponseDTO(response=WeatherResponseDTO(header=WeatherHeaderDTO(resultCode=00, resultMsg=NORMAL_SERVICE), body=WeatherBodyDTO(dataType=JSON, items=WeatherItemsDTO(item=[WeatherItemDTO(baseDate=20220124, baseTime=1700, category=PTY, nx=60, ny=120, obsrValue=0.0), WeatherItemDTO(baseDate=20220124, baseTime=1700, category=REH, nx=60, ny=120, obsrValue=43.0), WeatherItemDTO(baseDate=20220124, baseTime=1700, category=RN1, nx=60, ny=120, obsrValue=0.0), WeatherItemDTO(baseDate=20220124, baseTime=1700, category=T1H, nx=60, ny=120, obsrValue=7.6), WeatherItemDTO(baseDate=20220124, baseTime=1700, category=UUU, nx=60, ny=120, obsrValue=-3.3), WeatherItemDTO(baseDate=20220124, baseTime=1700, category=VEC, nx=60, ny=120, obsrValue=100.0), WeatherItemDTO(baseDate=20220124, baseTime=1700, category=VVV, nx=60, ny=120, obsrValue=0.6), WeatherItemDTO(baseDate=20220124, baseTime=1700, category=WSD, nx=60, ny=120, obsrValue=3.5)], numOfRows=0, pageNo=0, totalCount=0)))),[Content-Language:"ko-KR", Set-Cookie:"JSESSIONID=7DMnHdbsSR49wbVAb64iZwZ1kX4vt7RlwRIHymwNqlZqpr9zpkaA2wfDEREDYPr5.amV1c19kb21haW4vbmV3c2t5Mw==; Path=/1360000/VilageFcstInfoService_2.0; HttpOnly; Domain=apis.data.go.kr", Content-Type:"application/json;charset=UTF-8", Content-Length:"909", Date:"Mon, 24 Jan 2022 08:30:25 GMT", Server:"NIA API Server"]>

실제 API요청 후 받는 응답값의 형태가 위의 형태이다.

import lombok.Data;

@Data
public class WeatherApiResponseDTO
{
    private WeatherResponseDTO response;
}

WeatherResponseDTO - 위의 WeatherApiResponseDTO에서 response객체에서 header, body를 분리하기 위해 만드는 DTO

import lombok.Data;

@Data
public class WeatherResponseDTO
{
    private WeatherHeaderDTO header;

    private WeatherBodyDTO   body;
}

WeatherHeaderDTO - 위의 responseDTO에서 header와 매칭되는 DTO

import lombok.Data;

@Data
public class WeatherHeaderDTO
{
    private String resultCode;

    private String resultMsg;
}

WeatherBodyDTO - 위의 responseDTO에서 body와 매칭되는 DTO

import lombok.Data;

@Data
public class WeatherBodyDTO
{
    private String          dataType;

    private WeatherItemsDTO items;
}

WeatherItemsDTO - BodyDTO에서 items의 내용을 가져오기 위한 DTO

import java.util.List;

import lombok.Data;

@Data
public class WeatherItemsDTO
{
    private List<WeatherItemDTO> item;

    private int                  numOfRows;

    private int                  pageNo;

    private int                  totalCount;
}

WeatherItemDTO - ItemsDTO에서 각각의 실제 응답값을 가져오기 위한 DTO

import lombok.Data;

@Data
public class WeatherItemDTO
{
    private String baseDate;

    private String baseTime;

    private String category;

    private String nx;

    private String ny;

    private Double obsrValue;
}

WeatherDTO - View단으로 보내기 위한 DTO

@Data
public class WeatherDTO
{
    // 발표 일자
    private String baseDate;

    // 발표 시각
    private String baseTime;

    // 자료구분코드
    private String category;

    // 예보지점 X좌표
    private String nx;

    // 예보지점 Y좌표
    private String ny;

    // 실황 값
    private Double obsrValue;
}

지금까지 8개의 DTO를 만들었다.

만약 내가 Category가 PTY인 응답값을 위의 WeatherApiResponseDTO에서 알고싶다면

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

System.out.println(response.getBody().getResponse().getBody().getItems().getItem().get(0).getCategory());

와 같이 값을 얻을 수 있다.

다 작성하고 난 후 페이지에서 실행버튼을 누르면 파싱이 되면서 html로 뿌려질 것이다.

추가로 API요청을 주기적으로 하는 스케줄링 기능과 DB로 수집한 값들을 평균내서 보여주는 페이지를 작성할 예정

profile
개인 정리 공간

1개의 댓글

comment-user-thumbnail
2022년 6월 7일

좋은 예제 구현해주셔서 감사합니다. 추가로 작성한다는 스케줄링 기능과 DB 평균값 페이지 작성 예제도 기회된다면 한번 보고싶어요!!

답글 달기