Spring Boot + 기상청 예보 OpenAPI + Gemini API

송형근·2024년 9월 13일
0

TIL

목록 보기
37/43
post-thumbnail

기상청 단기예보 ( https://www.data.go.kr/data/15084084/openapi.do ) 와 Gemini를 활용해서 오늘의 일기 예보 요약을 구현

기상청 단기예보 OpenAPI 사용 준비

  • 활용신청

  • API Key 확인

  • Zip 파일 압축 해제 후 격자 위경도 엑셀 파일에서 원하는 지역 격자 X, 격자 Y 값 확인

  • 오픈 API 활용가이드에서 활용할 API 정보 확인

    • API 정보 및 요청 URL 정보
    • RequestParameter
      항목명(영문)항목명(국문)항목크기항목구분샘플데이터항목설명
      serviceKey인증키1001인증키
      (URL Encode)공공데이터포털에서 발급받은 인증키
      numOfRows한 페이지 결과 수4150한 페이지 결과 수
      Default: 10
      pageNo페이지 번호411페이지 번호
      Default: 1
      dataType응답자료형식40XML요청자료형식(XML/JSON)
      Default: XML
      base_date발표일자8120210628‘21년 6월 28일발표
      base_time발표시각41050005시 발표
      * 하단 참고자료 참조
      nx예보지점 X 좌표2155예보지점의 X 좌표값
      *별첨 엑셀 자료 참조
      ny예보지점 Y 좌표21127예보지점의 Y 좌표값
      *별첨 엑셀 자료 참조
  • basetime을 0500으로 하고, JSON형태로 요청 예정

    • basetime 0500기준 numOfRows를 217로 요청하면 0500~2300 의 단기예보 정보를 받아올 수 있음

Gemini API 활용 준비

RestTemplateConfig 작성

  • RestTemplate을 활용해서 API 요청을 할 예정이므로 Config 작성
    @Configuration
    public class RestTemplateConfig {
    
        @Bean
        public RestTemplate geminiRestTemplate(){
            return new RestTemplate();
        }
    
        @Bean
        public RestTemplate weatherRestTemplate(){
            return new RestTemplate();
        }
    }
    

WeatherService

  • WeatherInfoResDto
    • 단기 예보 정보를 받아와서 필요한 정보들만 재 가공하기 위해 ResDto작성
      @Data
      @NoArgsConstructor
      @AllArgsConstructor
      public class WeatherInfoResDto implements Serializable {
      
          private WeatherResponse response;
      
          @Data
          @NoArgsConstructor
          @AllArgsConstructor
          public static class WeatherResponse{
              private WeatherResponseHeader header;
              private WeatherResponseBody body;
          }
      
          @Data
          @NoArgsConstructor
          @AllArgsConstructor
          public static class WeatherResponseHeader{
              private String resultCode;
              private String resultMsg;
          }
      
          @Data
          @NoArgsConstructor
          @AllArgsConstructor
          public static class WeatherResponseBody{
              private String dataType;
              private WeatherResponseItems items;
          }
      
          @Data
          @NoArgsConstructor
          @AllArgsConstructor
          public static class WeatherResponseItems{
              private List<WeatherInfo> item;
          }
      
          @Data
          @NoArgsConstructor
          @AllArgsConstructor
          public static class WeatherInfo{
              private String category;
              private String fcstTime;
              private String fcstValue;
          }
      
          public String getInfos(){
              StringBuilder sb = new StringBuilder();
              for(WeatherInfo info : this.response.body.items.item){
                  sb.append("Category: " + info.category + "\n");
                  sb.append("FcstTime: " + info.fcstTime+ "\n");
                  sb.append("FcstValue: " + info.fcstValue+ ",\n");
              }
              return sb.toString();
          }
      
      }
  • 기상청 단기예보 OpenAPI를 활용해 그날의 단기예보 정보를 가져오는 로직 구현
    @Service
    @RequiredArgsConstructor
    @Slf4j(topic = "WeatherService")
    @Transactional(readOnly = true)
    public class WeatherService {
    
        private final RestTemplate weatherRestTemplate;
    
        @Value("${weather.service.key}")
        String weatherServiceKey;
    
        @Cacheable(cacheNames = "WeatherInfo", key = "args[0]")
        public String getWeatherInfo(String date){
            String requestURL = "https://apis.data.go.kr/1360000/VilageFcstInfoService_2.0/getVilageFcst?" +
                    "serviceKey=" + weatherServiceKey +
                    "&pageNo=1" +
                    "&numOfRows=217" +
                    "&dataType=JSON" +
                    "&base_date="+ date +
                    "&base_time=0500" +
                    "&nx=63" +
                    "&ny=124";
            log.info(requestURL);
            WeatherInfoResDto info = new WeatherInfoResDto();
            try{
                info = weatherRestTemplate.getForObject(requestURL, WeatherInfoResDto.class);
                log.info(info.toString());
                log.info("날씨 정보 호출 성공!");;
    
                return info.getInfos();
            }catch (Exception e){
                log.error(e.getMessage());
    
            }
            return "ERROR";
        }
    
    }
  • 현재 nx와 ny는 하드코딩 해둔 상태지만, 필요시 장소 정보를 받아와서 장소에 대한 nx와 ny를 동적으로 넣어주는 로직으로 변경 가능

GeminiService

  • ReqDto와 ResDto의 경우 GeminiAPI 활용 포스팅 참고
  • Gemini API로 prompt를 전송하는 로직
    @Service
    @RequiredArgsConstructor
    @Slf4j(topic = "GeminiService")
    @Transactional(readOnly = true)
    public class GeminiService {
    
        private final RestTemplate geminiRestTemplate;
    
        @Value("${gemini.api.key}")
        String geminiApiKey;
    
        @Cacheable(cacheNames = "WeatherSummary", key = "args[1]")
        public String getGeminiSummary(String requestPrompt, String date){
            String geminiURL = "https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash-latest:generateContent?key="
                    + geminiApiKey;
    
            GeminiReqDto request = new GeminiReqDto();
    
            request.createGeminiReqDto(requestPrompt);
            String result = "";
    
            try{
                GeminiResDto response = geminiRestTemplate.postForObject(geminiURL, request, GeminiResDto.class);
                log.info("Gemini 요청 성공");
                result = response.getCandidates().get(0).getContent().getParts().get(0).getText();
            }catch (Exception e){
                log.error(e.getMessage());
                log.error("INTERNAL SERVER ERROR");
                throw new CoreApiException(ErrorType.DEFAULT_ERROR);
            }
    
            return result;
        }
    
    }

오늘의 날씨 정보 요약 서비스 로직 구현

  • 앞서 작성한 WeatherService와 GeminiService를 활용해 오늘의 날씨 정보를 요약해서 Slack으로 전송하는 로직 구현
  • Slack 메시지 전송 로직은 Slack 메시지 전송 포스팅 참고
  • sendWeatherInfo 메서드
        @Transactional
        public String sendWeatherInfo(){
    
            DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMdd");
            String today = LocalDate.now().format(formatter);
    
            String requestText = "";
            try{
                requestText = weatherService.getWeatherInfo(today);
            }catch (Exception e){
                log.error(e.getMessage());
                log.error("Internal Server Error");
                throw new CoreApiException(ErrorType.DEFAULT_ERROR);
            }
    
            // 날씨 정보 요약 요청
            String result = geminiService.getGeminiSummary(requestText + " 의 정보를 요약해서 오늘의 날씨를 알려주고 " +
                    "Slack mrkdwn 서식을 적용해서 포맷을 예쁘게 꾸미고 " +
                    "Slack mrkdwn 서식 적용 과정에서 Bold 처리는 반드시 *와 여백 한개로 감싸야 해 " +
                    "(예시)오늘은 *흐리고 비* 가 내릴 예정)" +
                    "그리고 이모티콘도 넣고 줄바꿈도 잘 적용해서 " +
                    "한글로 500자 이내로 작성해줘", today);
    
            try{
                SlackIncomingHookDto slackRequest = new SlackIncomingHookDto("오늘의-날씨", result);
                log.info(slackRestTemplate.postForObject(slackURL, slackRequest, String.class));
                slackRepository.save(Slack.builder().receiverId("오늘의-날씨").message(result).sendTime(LocalDateTime.now()).build());
            }catch (Exception e){
                log.error("Internal Server Error");
                throw new CoreApiException(ErrorType.DEFAULT_ERROR);
            }
    
            return result;
        }

결과


P.S.

  • 두가지 API를 활용해서 오늘의 날씨 요약 정보를 만들고 Slack으로 전송하게 했는데, 하나의 RestTemplate으로 API를 다 요청하려하니 에러가 발생해서 조금 헤매긴 했지만 재밌었음
  • API 요청도 하루에 제한이 있기도하고 결과물을 받아오는 데 시간이 조금 걸리다보니 캐싱을 적용했음
  • 한글은 Slack에서 Bold처리할 경우 * 와 공백 하나로 감싸야 하는데, Gemini로 요청을 해도 항상 적절하게 작성해주진 않는다.... 프롬프트를 어떻게 작성해야 내 말을 잘 들을지 고민이 됨
profile
기록을 남겨보자

0개의 댓글