본문 바로가기
프로젝트

문화재청 행사일정 API로 달력 만들기 시리즈 (1)

by 이페코장인 2024. 2. 23.

프로젝트 목표

 

1) 문화재청에서 제공하는 API중에서 문화행사 일정 데이터가 있다. 나는 이 데이터를 활용하여 스프링 프로젝트에서 문화행사 일정 달력을 출력하려고 목표를 설정하였다.

 

문화재청 API 링크

문화재청 > Open API 개방목록 > 문화재정보 > 활용정보 (cha.go.kr)

 

문화재청 > Open API 개방목록 > 문화재정보 > 활용정보

Open API 개방목록 문화유산소식 --> 안내사항 오픈API 소개 오픈API란 누구나 사용할 수 있도록 공개된 API를 말합니다. 데이터를 표준화하고 프로그래밍해 외부 소프트웨어 개발자나 사용자들과 공

www.cha.go.kr

 

2) 또한, 데이터를 iCalendar(ics)형태의 파일로 만들어서 구글 캘린더나 네이버 캘린더와 같은 서비스에서 행사일정 목록을 사용할 수 있도록 하는 것 또한 계획하였다. 참고로 ics파일이란, 일정 형태로 저장된 달력 정보로, Outlook, Google Calendar등에 업로드하거나 연동해서 사용할 수 있는 파일 포맷이다.

 

아이캘린더 - 위키백과, 우리 모두의 백과사전 (wikipedia.org)

 

아이캘린더 - 위키백과, 우리 모두의 백과사전

위키백과, 우리 모두의 백과사전. 아이캘린더 구성 요소 및 속성 아이캘린더(iCalendar)는 인터넷 사용자들이 다른 인터넷 사용자들에게 전자 메일을 이용하여 미팅 요청과 할 일을 보내거나 .ics

ko.wikipedia.org

 

상세 계획

 

내가 정한 두 목표(달력 출력, ics파일 생성)를 위해서 어떤 절차가 필요한지 세부적으로 계획해 보았다.

 

 

문화재청 행사일정 프로젝트 (환경: Spring, MySQL)

  1. 문화재청 API에서 2023년도 행사일정 정보를 요청해, MySQL DB로 저장하기
  2. iCal4j를 활용하여 DB에 저장된 행사일정으로 ics 파일을 생성하기
  3. Full Calendar를 사용해서 일정을 페이지에 출력하기
 

 

 

문화재청 API 파싱하고 저장하기

 

우선, 문화재청 API의 문서와 예시 데이터 요청을 통해 API응답구조를 파악하고, 해당 데이터들을 스프링에서 활용할 수 있도록 자바 객체 클래스를 구성한다. 나는 응답데이터를 int나 date와 같은 형태로 바로 변환하기보다 편의상 일단 전부 String 문자열 형태로 받아오는 방식으로 설계했다.

 

행사일정 객체 EventCalendar

package com.multi.mvc.calendar.model.vo;

import lombok.Data;

@Data
public class EventCalendar {

	public String seqNo;	    // 고유 ID
	public String subTitle;	    // 행사 제목
	public String sDate;        // 시작일
	public String eDate;        // 종료일
	public String subPath;      // 관련 링크 URL
	public String subDate;      // 상세 일정
	public String subContent;   // 행사 내용
	public String siteCode;     // 행사 종료 구분
	public String groupName;    // 주최자
	public String contact;      // 전화번호
	public String subDesc;      // 장소
	public String subDesc2;     // 참가대상
	public String subDesc3;     // 참가바
	public String sido;         // 주소(시도)
	public String gugun;        // 주소(구군)
    
}

 

 

이와 비슷하게 MySQL에서 해당 내용을 저장해 줄 수 있도록 테이블을 만들어 준다.

 

MySQL의 Table EVENTCALENDAR

CREATE TABLE EVENTCALENDAR (
    SEQNO VARCHAR(200) PRIMARY KEY,
    SUBTITLE VARCHAR(200),
    SDATE VARCHAR(200),
    EDATE VARCHAR(200),
    SUBPATH VARCHAR(200),
    SUBDATE VARCHAR(200),
    SUBCONTENT VARCHAR(5000),
    SITECODE   VARCHAR(200),
    GROUPNAME  VARCHAR(200),
    CONTACT    VARCHAR(200),
    SUBDESC    VARCHAR(200),
    SUBDESC2   VARCHAR(200),
    SUBDESC3   VARCHAR(200),
    SIDO       VARCHAR(200),
    GUGUN      VARCHAR(200)
);

 

 

이제 스프링과 MySQL을 이어주는 mapper를 작성한다

 

Mapper.xml파일

<?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.multi.mvc.calendar.model.mapper.CalendarMapper">

	<resultMap type="EventCalendar" id="calendarResultMap">
		<result property="seqNo" column="SEQNO"/>
		<result property="subTitle" column="SUBTITLE"/>
		<result property="sDate" column="SDATE"/>
		<result property="eDate" column="EDATE"/>
		<result property="subPath" column="SUBPATH"/>
		<result property="subDate" column="SUBDATE"/>
		<result property="subContent" column="SUBCONTENT"/>
		<result property="siteCode" column="SITECODE"/>
		<result property="groupName" column="GROUPNAME"/>
		<result property="contact" column="CONTACT"/>
		<result property="subDesc" column="SUBDESC"/>
		<result property="subDesc2" column="SUBDESC2"/>
		<result property="subDesc3" column="SUBDESC3"/>
		<result property="sido" column="SIDO"/>
		<result property="gugun" column="GUGUN"/>
	</resultMap>
	
	<!-- 2023전체 행사일정 select문 -->
	<select id="selectAllCalendar" resultMap="calendarResultMap">
		SELECT * FROM EVENTCALENDAR
	</select>
	
	<!-- 중복확인용 select -->
	<select id="selectCalendarByNo" parameterType="String" resultMap="calendarResultMap">
		SELECT * FROM EVENTCALENDAR WHERE SEQNO = #{seqNo}
	</select>
	
	<!-- 일정 저장하는 insert -->
	<insert id="insertCalendar" parameterType="EventCalendar">
		INSERT INTO EVENTCALENDAR (SEQNO, SUBTITLE, SDATE, EDATE, SUBPATH, SUBDATE, SUBCONTENT, SITECODE, 
				GROUPNAME, CONTACT, SUBDESC, SUBDESC2, SUBDESC3, SIDO, GUGUN) 
			VALUES(#{seqNo}, #{subTitle}, #{sDate}, #{eDate}, #{subPath}, #{subDate}, #{subContent}, #{siteCode}, 
				#{groupName}, #{contact}, #{subDesc}, #{subDesc2}, #{subDesc3}, #{sido}, #{gugun})
	</insert>
	
</mapper>

 

 

그리고 마지막으로, API에 데이터를 요청해서 XML파일을 받아 파싱하는 Controller부분의 코드를 완성하여, 데이터를 저장할 준비를 마치면 된다.

public class CalendarController {
	@Autowired
	private CalendarService service;
	
	public final String urlString = "http://www.cha.go.kr/cha/openapi/selectEventListOpenapi.do"; // 기본url

	public void parseXML(String targetData) {
		try {
			// 1. url가공
			StringBuilder urlBuilder = new StringBuilder(urlString);
			urlBuilder.append("?searchYear=2023"); // 2023년 데이터로 한정
			urlBuilder.append("&searchMonth=" + URLEncoder.encode(targetData, "UTF-8"));
			System.out.println(urlBuilder);
			
			// 2. html요청하기
			URL url = new URL(urlBuilder.toString()); 
			HttpURLConnection conn = (HttpURLConnection) url.openConnection();
			conn.setRequestMethod("GET");
			System.out.println("Response code : " + conn.getResponseCode());
			
			// 3. 파싱 시작
			if (conn.getResponseCode() < 200 || conn.getResponseCode() >= 300) {
				System.out.println("페이지를 찾을수 없습니다");
				return;
			}
			
			// 4. xml parsing
			DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
			DocumentBuilder db = dbf.newDocumentBuilder();
			Document doc = db.parse(conn.getInputStream());
			doc.normalizeDocument();
			
			System.out.println("Root element(=tag) : " + doc.getDocumentElement().getNodeName());
	        System.out.println("----------------------------------------------------------------");
	        
	        NodeList eventList = doc.getElementsByTagName("item");
	        for (int i = 0; i < eventList.getLength(); i++) {
	        	Node node = eventList.item(i);
	        	System.out.println("\nCurrent node : " + node.getNodeName());
	        	if (node.getNodeType() == Node.ELEMENT_NODE) {
	        		Element e = (Element)node;
	        		EventCalendar calendar = new EventCalendar();
                    
	        		calendar.setSeqNo(e.getElementsByTagName("seqNo").item(0).getTextContent());
	        		calendar.setSubTitle(e.getElementsByTagName("subTitle").item(0).getTextContent());
	        		calendar.setSDate(e.getElementsByTagName("sDate").item(0).getTextContent());
	        		calendar.setEDate(e.getElementsByTagName("eDate").item(0).getTextContent());
	        		calendar.setSubPath(e.getElementsByTagName("subPath").item(0).getTextContent());
	        		calendar.setSubDate(e.getElementsByTagName("subDate").item(0).getTextContent());
	        		calendar.setSubContent(e.getElementsByTagName("subContent").item(0).getTextContent());
	        		calendar.setSiteCode(e.getElementsByTagName("siteCode").item(0).getTextContent());
	        		calendar.setGroupName(e.getElementsByTagName("groupName").item(0).getTextContent());
	        		calendar.setContact(e.getElementsByTagName("contact").item(0).getTextContent());
	        		calendar.setSubDesc(e.getElementsByTagName("subDesc").item(0).getTextContent());
	        		calendar.setSubDesc2(e.getElementsByTagName("subDesc_2").item(0).getTextContent());
	        		calendar.setSubDesc3(e.getElementsByTagName("subDesc_3").item(0).getTextContent());
	        		calendar.setSido(e.getElementsByTagName("sido").item(0).getTextContent());
	        		calendar.setGugun(e.getElementsByTagName("gugun").item(0).getTextContent());
	        		
	        		System.out.println(calendar.toString());
	        		int result = service.save(calendar);
	        		if (result > 0) {
	        			System.out.println("저장성공");
	        		}
	        	}
	        }
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
    
    // 저장버튼 구현 부분
    @GetMapping("/calendar/save")
	public String view(Model model, @RequestParam(name="no", required = false) String no) {
		parseXML(no);
		log.debug(no + "월 데이터 저장");
		return "calendar/view";
	}
    
}

 

 

프론트 코드

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/functions" prefix="fn"%>
<c:set var="path" value="${pageContext.request.contextPath}"/>


<section id="content">
	<h3>Spring 기반의 MVC2 패턴을 활용한 Web Application 입니다.</h3>
	
	<a href="${path}/calendar/save?no=1"><button>2023년1월</button></a>
	<a href="${path}/calendar/save?no=2"><button>2023년2월</button></a>
	<a href="${path}/calendar/save?no=3"><button>2023년3월</button></a>
	<a href="${path}/calendar/save?no=4"><button>2023년4월</button></a>
	<a href="${path}/calendar/save?no=5"><button>2023년5월</button></a>
	<a href="${path}/calendar/save?no=6"><button>2023년6월</button></a>
	<a href="${path}/calendar/save?no=7"><button>2023년7월</button></a>
	<a href="${path}/calendar/save?no=8"><button>2023년8월</button></a>
	<a href="${path}/calendar/save?no=9"><button>2023년9월</button></a>
	<a href="${path}/calendar/save?no=10"><button>2023년10월</button></a>
	<a href="${path}/calendar/save?no=11"><button>2023년11월</button></a>
	<a href="${path}/calendar/save?no=12"><button>2023년12월</button></a>
</section>

 

 

나는 다음과 같이 생긴 웹페이지를 생성하여, 버튼을 누르면 해당 달의 데이터를 요청하여 DB로 저장하는 방식으로 완성했다. 이런 코드가 보다 직관적으로 이해하기 쉽겠지만, 보다 간결한 코드를 원한다면 Controller에서 반복문을 활용하여 버튼 하나만으로도 2023년 1월~12월 데이터를 모두 요청하여 저장하는 방식도 가능할 것이다.

 

 

 

이제 MySQL을 확인해보면, 데이터가 저장된 것을 확인할 수 있다.

 

 

시리즈 2