SpringBoot 프로젝트

Spring Boot - 국립 도서관 Open API를 활용한 사서 추천 도서 목록 구현

orin602 2024. 12. 10. 15:36

이번 글에서는 국립 도서관의 Open API를 활용하여 사서가 추천하는 도서를 가져오고, 웹 페이지에 표시하는 방법을 소개해 볼게요. 부족한 실력 탓에 설명이 미흡합니다... 이해해주세요 :)

 

API에서 반환데는 데이터를 매핑할 DTO 작성

package com.demo.dto;

import jakarta.xml.bind.annotation.XmlElement;
import jakarta.xml.bind.annotation.XmlRootElement;
import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
@XmlRootElement(name = "item")
public class BookRecommendation {

    private String drCodeName;
    private String recomtitle;
    private String recomauthor;
    private String recomcontens;
    private String regdate;
    private String recomfilepath;

    @XmlElement(name = "drCodeName")
    public String getDrCodeName() {
        return drCodeName;
    }

    public void setDrCodeName(String drCodeName) {
        this.drCodeName = drCodeName;
    }

    @XmlElement(name = "recomtitle")
    public String getRecomtitle() {
        return recomtitle;
    }

    public void setRecomtitle(String recomtitle) {
        this.recomtitle = recomtitle;
    }

    @XmlElement(name = "recomauthor")
    public String getRecomauthor() {
        return recomauthor;
    }

    public void setRecomauthor(String recomauthor) {
        this.recomauthor = recomauthor;
    }

    @XmlElement(name = "recomcontens")
    public String getRecomcontens() {
        return recomcontens;
    }

    public void setRecomcontens(String recomcontens) {
        this.recomcontens = recomcontens;
    }

    @XmlElement(name = "regdate")
    public String getRegdate() {
        return regdate;
    }

    public void setRegdate(String regdate) {
        this.regdate = regdate;
    }

    @XmlElement(name = "recomfilepath")
    public String getRecomfilepath() {
        return recomfilepath;
    }

    public void setRecomfilepath(String recomfilepath) {
        this.recomfilepath = recomfilepath;
    }
}
  • 도서 정보의 각 항목을 XML에서 매핑된 자바 개게로 반환.
package com.demo.dto;

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

import jakarta.xml.bind.annotation.XmlElement;
import jakarta.xml.bind.annotation.XmlRootElement;
import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
@XmlRootElement(name = "channel")
public class BookRecommendationResponse {

    private int totalCount;
    private List<BookRecommendationWrapper> lists = new ArrayList<>();

    @XmlElement(name = "totalCount")
    public int getTotalCount() {
        return totalCount;
    }

    public void setTotalCount(int totalCount) {
        this.totalCount = totalCount;
    }

    @XmlElement(name = "list")
    public List<BookRecommendationWrapper> getLists() {
        return lists;
    }

    public void setLists(List<BookRecommendationWrapper> lists) {
        this.lists = lists;
    }
}
package com.demo.dto;

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

import jakarta.xml.bind.annotation.XmlElement;
import jakarta.xml.bind.annotation.XmlRootElement;
import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
@XmlRootElement(name = "list")
public class BookRecommendationWrapper {

    private List<BookRecommendation> items = new ArrayList<>();

    @XmlElement(name = "item")
    public List<BookRecommendation> getItems() {
        return items;
    }

    public void setItems(List<BookRecommendation> items) {
        this.items = items;
    }
}
package com.demo.dto;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
@JsonIgnoreProperties(ignoreUnknown = true) // 알 수 없는 속성을 무시
public class ItemDTO {

    // 국립중앙도서관 api용
    private String kwd;          // 검색어
    private String titleInfo;   // 제목
    private String authorInfo;  // 저자
    private String imageUrl;    // 이미지 URL
    private String kdcName1s;
}

 

Service 클래스 작성

package com.demo.service;

import java.io.StringReader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

import com.demo.dto.BookRecommendation;
import com.demo.dto.BookRecommendationResponse;
import com.demo.dto.BookRecommendationWrapper;

import jakarta.xml.bind.JAXBContext;
import jakarta.xml.bind.JAXBException;
import jakarta.xml.bind.Unmarshaller;

@Service
public class BookRecommendationService {

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

    private final RestTemplate restTemplate;

    public BookRecommendationService(RestTemplate restTemplate) {
        this.restTemplate = restTemplate;
    }

    public List<BookRecommendation> getBookRecommendations(String start_date, String end_date) throws JAXBException {
        String url = String.format("https://nl.go.kr/NL/search/openApi/saseoApi.do?key=%s&start_date=%s&end_date=%s", 
                apiKey, start_date, end_date);
        String response = restTemplate.getForObject(url, String.class);

        // 로그 추가
        System.out.println("Response: " + response);

        JAXBContext jaxbContext = JAXBContext.newInstance(BookRecommendationResponse.class);
        Unmarshaller unmarshaller = jaxbContext.createUnmarshaller();
        BookRecommendationResponse bookRecommendationResponse = 
            (BookRecommendationResponse) unmarshaller.unmarshal(new StringReader(response));

        // 모든 리스트의 아이템들을 하나의 리스트로 합치기
        List<BookRecommendation> recommendations = new ArrayList<>();
        for (BookRecommendationWrapper listWrapper : bookRecommendationResponse.getLists()) {
            recommendations.addAll(listWrapper.getItems());
        }
        // 추천 목록이 비어있는 경우
        if (recommendations.isEmpty()) {
            return Collections.singletonList(new BookRecommendation()); // 빈 객체를 반환
        }
        return recommendations;
    }
}

 

Html

<script>
    $(document).ready(function() {
        var message = /*[[${message}]]*/ '';
        var messageType = /*[[${messageType}]]*/ '';
        var text = /*[[${text}]]*/ '';

        if (message) {
            Swal.fire({
                title: '알림',
                text: message,
                icon: messageType || 'info', // 메시지 타입이 없는 경우 기본값으로 'info' 사용
                confirmButtonText: '확인'
            });
        }

        $('.month-btn').click(function() {
            var start_date = $(this).data('start-date');
            var end_date = $(this).data('end-date');

            console.log('추천을 가져옵니다:', start_date, end_date); // 디버깅 로그

            $.ajax({
                url: '/fetch-recommendations',
                method: 'GET',
                data: { start_date: start_date, end_date: end_date },
                success: function(data) {
                	$('#recommendations-list').empty(); // 이전 데이터 삭제
                    console.log('Response:', data); // 디버깅 로그

                    if (data.error) {
                        Swal.fire({
                            title: 'Error',
                            text: '데이터 오류',
                            icon: 'error',
                            confirmButtonText: '확인'
                        });
                        $('#recommendations-list').html('<li>추천이 없습니다.</li>'); // 추천이 없을 때 표시
                    } else if (data.message) {
                        Swal.fire({
                            title: '알림',
                            text: data.message,
                            icon: 'info',
                            confirmButtonText: '확인'
                        });
                    } else {
                        var recommendations = data.recommendations || []; // 기본값으로 빈 배열 설정
                        var totalCount = data.totalCount || 0; // totalCount 값 설정
                     
                        // totalCount를 사용하여 목록 상단에 추가
                        var listHtml = '<h1>추천 책 : ' + totalCount + '권</h1>'; // 총 추천 책 수

                        if (Array.isArray(recommendations) && recommendations.length > 0) {
                            recommendations.forEach(function(recommendation) {
                                listHtml += '<li class="recommendations-item">' +
                                    '<h2>' + recommendation.recomtitle + '</h2>' +
                                    '<img src="' + recommendation.recomfilepath + '" alt="책 이미지">' +
                                    '<p>' + recommendation.recomauthor.replace(';', '/') + '</p>' +
                                    '<p>' + recommendation.recomcontens + '</p>' +
                                    '<p>날짜: ' + recommendation.regdate + '</p>' +
                                    '</li>';
                            });
                            $('#recommendations-list').html(listHtml);
                        } else {
                            $('#recommendations-list').html('<li>추천이 없습니다.</li>'); // 추천이 없을 때 표시
                        }
                    }
                },
                error: function(xhr, status, error) {
                    console.error('Ajax request failed:', error); // 디버깅 로그
                }
            });
        });
    });
</script>
</head>
<body>
<!-- 헤더 -->
<th:block th:insert="~{include/header}"></th:block>
<div class="main-container">
	<h1>사서 도서 추천</h1>
	<div th:if="${error}">
	    <p th:text="${error}"></p>
	</div>
	<div class="recommendations-btns">
	    <button class="month-btn" data-start-date="20240101" data-end-date="20240131">January</button>
	    <button class="month-btn" data-start-date="20240201" data-end-date="20240229">February</button>
	    <button class="month-btn" data-start-date="20240301" data-end-date="20240331">March</button>
	    <button class="month-btn" data-start-date="20240401" data-end-date="20240430">April</button>
	    <button class="month-btn" data-start-date="20240501" data-end-date="20240531">May</button>
	    <button class="month-btn" data-start-date="20240601" data-end-date="20240630">June</button>
	    <button class="month-btn" data-start-date="20240701" data-end-date="20240731">July</button>
	    <button class="month-btn" data-start-date="20240801" data-end-date="20240831">August</button>
	    <button class="month-btn" data-start-date="20240901" data-end-date="20240930">September</button>
	    <button class="month-btn" data-start-date="20241001" data-end-date="20241031">October</button>
	    <button class="month-btn" data-start-date="20241101" data-end-date="20241130">November</button>
	    <button class="month-btn" data-start-date="20241201" data-end-date="20241231">December</button>
	</div>
	<div id="recommendations-container" class="recommendations-container">
	    <ul id="recommendations-list" class="recommendateions-list">
	        <!-- 추천 목록이 여기에 동적으로 추가됩니다. -->
	    </ul>
	</div>
</div>

  • $('.month-btn').click(function() {...} JavaScript 함수 : <button class="month-btn" 월별 버튼 클릭 시 추천 도서 가져오는 함수.
  • data-start-date="20240101" data-end-date="20240131" : 버튼의 날짜 값을 JavaScript에서 사용할 수 있게 data-*속성을 이용.
  • var start_date = $(this).data('start-date'); , var end_date = $(this).data('end-date'); :전달받은 날짜 값을 저장.
  • $.ajax({... : Ajax를 이용해서 url( url: '/fetch-recommendations' )로 요청, method형태( method: 'GET' ) HTTP GET 요청, data( data: { start_date: start_date, end_date: end_date } ) 요청 시 전송할 데이터.

Controller 작성

// 사서 추천 도서
@GetMapping("/fetch-recommendations")
@ResponseBody
public Map<String, Object> fetchRecommendations(@RequestParam String start_date, @RequestParam String end_date) {
    Map<String, Object> response = new HashMap<>();
    try {
        List<BookRecommendation> recommendations = bookRecoService.getBookRecommendations(start_date, end_date);

        // 총 개수를 response에 추가
        response.put("totalCount", recommendations.size());

        if (recommendations.isEmpty() || (recommendations.size() == 1 && recommendations.get(0).getRecomtitle() == null)) {
            response.put("message", "아직 이달의 사서 도서 추천이 없습니다.");
        } else {
            response.put("recommendations", recommendations);
        }
    } catch (JAXBException e) {
        response.put("error", "Failed to fetch book recommendations.");
    }
    return response;
}
  • List<BookRecommendation> recommendations = bookRecoService.getBookRecommendations(start_date, end_date); : (@RequestParam String start_date, @RequestParam String end_date) 로 전달받은 날짜값으로 Service의 메서드를 사용해서 추천 도서 목록을 List형식으로 recommendations에 저장.
  • response.put("totalCount", recommendations.size()); : put()메서드를 이용해서 "totalCount"에 해당하는 값 recommendations.size()을 추가 또는 변경해서 response에 추가.
    ( totalCount : Map 키 > 총 추천 도서의 개수 / recommendations : 추천 도서 목록을 담고있는 리스트 > List<BookRecommendation> 
// success 콜백 내에서 받은 data는 recommendations 배열.
else {
    var recommendations = data.recommendations || []; // 기본값으로 빈 배열 설정
    var totalCount = data.totalCount || 0; // totalCount 값 설정

    // totalCount를 사용하여 목록 상단에 추가
    var listHtml = '<h1>추천 책 : ' + totalCount + '권</h1>'; // 총 추천 책 수

    if (Array.isArray(recommendations) && recommendations.length > 0) {
        recommendations.forEach(function(recommendation) {
            listHtml += '<li class="recommendations-item">' +
                '<h2>' + recommendation.recomtitle + '</h2>' +
                '<img src="' + recommendation.recomfilepath + '" alt="책 이미지">' +
                '<p>' + recommendation.recomauthor.replace(';', '/') + '</p>' +
                '<p>' + recommendation.recomcontens + '</p>' +
                '<p>날짜: ' + recommendation.regdate + '</p>' +
                '</li>';
        });
        $('#recommendations-list').html(listHtml);
    } else {
        $('#recommendations-list').html('<li>추천이 없습니다.</li>'); // 추천이 없을 때 표시
    }
}
  • $('#recommendations-list').empty(); : 이전에 선택한 날짜의 추천 도서 데이터 삭제.
  • recommendations.forEach(function(recommendation) { : 배열을 순회하면서 각 추천 도서의 정보를 처리.
  • listHtml += ... ; : 변수에 추천 도서의 HTML을 추가.
  • $('#recommendations-list').html(listHtml); : <ul id="recommendations-list" class="recommendateions-list">를 선택해서 listHtml 로 생성된 HTML을 해당 <ul>요소에 삽입.

선택한 달의 추천도서
추천 도서가 없을 때