검색

2016년 10월 25일

커리어넷 Open API 를 이용한 학교 검색

커리어넷 Open API


학교 정보가 필요한 경우 무료로 제공되는 커리어넷 Open API 를 활용하면 쉽게 서비스 구현이 가능하다. API 사용을 위하여 커리어넷 오픈 API 내용을 확인하고 가) 회원 가입 후  나)오픈 API 인증키 신청 후 발급된 키를 사용하여 이용이 가능하다. 이글에서는 학교정보 API 사용하여 학교 검색을 구현하였다.


한가지 아쉬운 점은 제공되는 Open API 가 jsonp 를 지원하고 있지 않아 자바스크립트에서 직접 호출이 어려워 별도의 서버를 통하여 구현이 가능했다는 점이다.

사용된 주요 기술은 아래와 같다.
  • server framework : spring-webmvc 4.3.2.RELEASE 
  • json framework: jackson 2.8.2  
  • client : jquery 1.12.4 , kendoui 2015.3.930


서버 프로그램

spring mvc 를 사용하여 클라이어트의 요청을 받아 커리어넷 Open API 를 호출하여 결과를 클라이언트가 손쉽게 사용할 수 있는 형식을 테이터로 변경하여 응답하도록 구현하였다.


spring 컨트롤러 : AccountsDataController.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.InputStream;
import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
 
 
@Controller("user-data-controller")
@RequestMapping("/accounts")
public class AccountsDataController {
 
private Logger logger = LoggerFactory.getLogger(getClass());
 
/** Json 데이터 파싱을 위한 매퍼 정의  */ 
private static final ObjectMapper MAPPER = new ObjectMapper();
 
/**
  * 학교검색 (커리어넷 Open API 를 사용 )
  * 
  * 
  * 파라메터 gubun 학교구분 값 [필수]: 
  * <ul>
  *<li>초등학교 : elem_list</li>
  *<li>중학교 :midd_list</li>
  *<li>고등학교 : high_list</li>
  *<li>대학교 : univ_list</li>
  *<li>특수/각종기타학교 : seet_list</li>
  *<li>대안학교 : alte_list</li>
  * </ul>
  * 
  * @param gubun 학교구분
  * @param request
  * @param response
  * @return
  */
 
 @RequestMapping(value="/school/list.json",method={RequestMethod.POST} )
 @ResponseBody
 public ItemList findSchool(@RequestParam(value="gubun", defaultValue="elem_list" ) String gubun,
   @RequestParam(value="name", required = false ) String name,
   @RequestParam(value="region", required = false ) String region,
   @RequestParam(value="page", defaultValue="1" ) Integer page,
   @RequestParam(value="pageSize", defaultValue="15" ) Integer pageSize,
   HttpServletRequest request, HttpServletResponse response){
 
  ItemList items = new ItemList(Collections.EMPTY_LIST, 0);
  StringBuilder sb = new StringBuilder("http://www.career.go.kr/cnet/openapi/getOpenApi.json?apiKey=발급받은키값&svcType=api&svcCode=SCHOOL&contentType=json");
  sb.append("&amp;gubun=").append(gubun);
  if( StringUtils.isNotEmpty(region)){
   sb.append("&amp;region=").append(region);
  }
 
  if( StringUtils.isNotEmpty(name)){
   sb.append("&amp;searchSchulNm=").append(name);
  }  
 
  sb.append("&amp;thisPage=").append(page);
  sb.append("&amp;perPage=").append(pageSize);
 
  try {
   URL url = new URL(sb.toString());
   URLConnection connection = url.openConnection();
   InputStream is = connection.getInputStream();
   JsonNode jn = MAPPER.readTree(is);
   JsonNode jn2 = jn.get("dataSearch").get("content");
   Iterator<jsonnode> iter = jn2.elements();
   List<school> list = new ArrayList<school>();
   while(iter.hasNext()){
    JsonNode jn3 = iter.next();
    if( items.getTotalCount() == 0 )
        items.setTotalCount(jn3.get("totalCount").asInt());    
    list.add(new School(
      jn3.get("seq").asInt(0),
      jn3.get("schoolName").textValue(),
      jn3.get("region").textValue(),
      jn3.get("adres").textValue()
      ));
   } 
 
   items.setItems(list);
 
  } catch (Exception e) {
   logger.error("CAREER API ERROR", e);
  }
  return items;
 }
 
}
 
cs

학교 정보 객체 : School.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
import java.io.Serializable;
 
public class School implements Serializable {
 
    private Integer seq;
    private String region;
    private String name;
    private String address;
 
    public School() {
    }
 
    public School(Integer seq, String name, String region, String address) {
        super();
        this.seq = seq;
        this.name = name;
        this.region = region;
        this.address = address;
    }
 
 
    public Integer getSeq() {
        return seq;
    }
 
    public void setSeq(Integer seq) {
        this.seq = seq;
    }
 
    public String getRegion() {
        return region;
    }
 
    public void setRegion(String region) {
        this.region = region;
    }
 
    public String getName() {
        return name;
    }
 
    public void setName(String name) {
        this.name = name;
    }
 
    public String getAddress() {
        return address;
    }
 
    public void setAddress(String address) {
        this.address = address;
    }        
 
}
 
cs


결과 객체( 배열의 경우 전체 데이터 수 가 없어 구현한 객체 ) : ItemList.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
import java.util.List;
 
public class ItemList {
 
    private List<?> items;
    private int totalCount;
 
    /**
     * @param items
     * @param totalCount
     */
    public ItemList(List<?> items, int totalCount) {
        super();
        this.items = items;
        this.totalCount = totalCount;
    }
 
 
    /**
     * @return items
     */
    public List<?> getItems() {
        return items;
    }
 
 
    /**
     * @param items
     *            설정할 items
     */
 
    public void setItems(List<?> items) {
        this.items = items;
    }
 
 
    /**
     * @return totalCount
     */
    public int getTotalCount() {
        return totalCount;
    }
 
 
    /**
     * @param totalCount
     *            설정할 totalCount
     */
 
    public void setTotalCount(int totalCount) {
        this.totalCount = totalCount;
    }
 
}
 
cs

클라이언트 HTML 소스
학교와 지역 등 코드값들은 OpenApi 매뉴얼 내용을 참고하여 구현하였다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
<script type="text/javascript">  
 
  yepnope([{
   load: [
   '<@spring.url "/js/kendo/2015.3.930/kendo.web.min.js"/>'
   '<@spring.url "/js/kendo.extension/kendo.ko_KR.js"/>',  
   '<@spring.url "/js/bootstrap/3.3.7/bootstrap.min.js"/>',
   '<@spring.url "/js/podo.ui/podo.ui.core.js"/>',
   '<@spring.url "/js/honeycomb/honeycomb.data.js"/>'
   ],
   complete: function() {    
    createSchoolSearch();
   }
  }]);
 
  function createSchoolSearch(){
   var renderTo = $("#school-grid");  
   if(!podo.ui.exists(renderTo)){
    var observable = kendo.observable({
     gubun : "elem_list",
     region : "100260",
     name : "",
     search : function(){
      var $this = this;
      podo.ui.grid(renderTo).dataSource.read({
       name : $this.get('name'),
       gubun: $this.get('gubun'),
       region : $this.get('region')
      });
     }    
    }); 
 
    kendo.bind($('#search-block'), observable);
 
    podo.ui.grid(renderTo, {
     autoBind: false,
     dataSource: { 
      transport: {
             read: {
                 // the remote service url
                 url: "<@spring.url "/accounts/school/list.json"/>",     
                 type: "post",    
                 // the data type of the returned result
                 dataType: "json"
             }
            },
            // describe the result format
            schema: {
            // the data, which the data source will be bound to is in the "list" field of the response
             data : "items",
             total: "totalCount",
             model : {
              id : "seq",
              fields: {
               seq : {type: "number"},
               address:  { type: "string" },
               region:  { type: "string" },
               name:  { type: "string" },
              }
             }
         },
         pageSize: 15,
      serverPaging: true,
      error: podo.ui.handleAjaxError        
     },
     pageable: { refresh:true }, 
     filterable : false,
     height: 300,
     columns: [     
      { field: "name", title: "이름",  width:160}, 
      { field: "region", title: "지역",  width:100 },
      { field: "address", title: "주소" }
     ]   
 
    });
   }    
  }
  </script>
   <div class="form-horizontal" id="search-block">
   <div class="margin-bottom-5">
    <select class="form-control form-group-margin" data-bind="value: gubun" name="gubun" placeholder="구분">
        <option value="elem_list">초등학교</option>
        <option value="midd_list">중학교</option>
        <option value="high_list">고등학교</option>
       </select>  
     </div>
 
<div class="margin-bottom-5">
<select class="form-control form-group-margin" data-bind="value: region" name="region" placeholder="지역">
        <option value="">전체</option>
        <option value="100260">서울특별시</option>
        <option value="100267">부산광역시</option>
        <option value="100269">인천광역시</option>
        <option value="100271">대전광역시</option>
        <option value="100272">대구광역시</option>
        <option value="100273">울산광역시</option>
        <option value="100275">광주광역시</option>
        <option value="100276">경기도</option>
        <option value="100278">강원도</option>
        <option value="100280">충청북도</option>
        <option value="100281">충청남도</option>
        <option value="100282">전라북도</option>
        <option value="100283">전라남도</option>
        <option value="100285">경상북도</option>
        <option value="100291">경상남도</option>
        <option value="100292">제주도</option>
       </select>              
     </div>
 
<div class="margin-bottom-5">
<input class="form-control" data-bind="value: name" placeholder="학교이름" type="text">
     </div>
<div class="margin-bottom-5">
<button class="btn btn-default" data-bind="click:search" type="button">검색</button>
     </div>
</div>
<div id="school-grid">
</div>
 
cs


kendoui 사용하니 UI 구현이 아주 간편해졌다. grid기능을 사용하여 쉽게 구현하였다.
검색화면

2016년 10월 5일

Spring 기반 웹 프로그램 개발 Part 1 - Spring Web MVC

1. 소개

Spring Web MVC 에서, DispatcherServlet 클래스는 프론트 컨트롤러로 역할을 한다. (Spring MVC 프로그램의 모든 흐름을 관리하는 책임을 가지고 있다.) Struts 의 ActionServlet 와 같은 역할을 한다고 할 수있다. 또한 디자인 패턴적으로  Struts와 같은 Push 스타일을 따르고 있다.

Spring 3 부터 어노테이션 @Controller 는 해당 클래스가 컨트롤러임을 의미한다. Spring MVC 를 처음 사용해보지만 이부분은 사용이 아주 쉽다. 어노테이션 @RequestMapping 은 요청 URL을 매핑하는데 사용된다. 이부분이 조금 익숙하지 않아 어렵게 생각된다. 매번 구글링을 하는 것도 조금 힘이든다.




  1. 프론트 컨트롤러 Dispatch Servlet 는 모든 요청들을 가로챈다.
  2. Dispatch Servlet 는 xml 파일에서 매핑된 핸들러 정보를 얻어 
  3. 요청을 Controller 로 포워드한다.  
  4. Controller 는 ModelAndView 객체를 리턴하고 
  5. Dispatch Servlet  는  xml 파일에서 Controller 에서 처리된 결과를 보여줄 View 결정 하는 View Resolver 를 채크하여 
  6. 특정 View 콤포넌트를 호출한다.

2. 환경설정


Maven 프로젝트 구성


eclipse 에서 New Maven Project 생성 과정에서 Archetype "maven-archetype-webapp" 를 사용하여 Maven 기반의 웹 프로젝트를 만드는 방법이 일반적인것 같으나, 개인적으로 (직관적으로 보이지 않는것을) 좋아하지 않아 아래와 같은 구조로 구성하여 프로젝트를 구성한다.

  •  소스 : /src
  •  웹 프로그램 루트 : /WebContent
  •  웹 프로그램 설정 : /WebContent/WEB-INF/
  •  maven 설정 : /pom.xml

Spring Web MVC 를 위해서 다음의 의존성을 pom.xml 파일에 추가한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>example</groupId>
    <artifactId>example</artifactId>
    <version>0.0.1.SNAPSHOT</version>    
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <javac.src.version>1.6</javac.src.version>
        <javac.target.version>1.6</javac.target.version>
        <project.javadoc.docEncoding>UTF-8</project.javadoc.docEncoding>
    </properties> 
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>2.3.2</version>
                <configuration>
                    <source>${javac.src.version}</source>
                    <target>${javac.target.version}</target>
                    <encoding>${project.build.sourceEncoding}</encoding>
                </configuration>
            </plugin> 
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <version>2.3.1</version>
                <configuration>
                    <outputDirectory>${basedir}/WebContent/WEB-INF/lib</outputDirectory>
                </configuration>
            </plugin> 
            <plugin>
                <artifactId>maven-assembly-plugin</artifactId>
                <version>2.3</version>
                <configuration>
                    <descriptorRefs>
                        <descriptorRef>jar-with-dependencies</descriptorRef>
                    </descriptorRefs>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-dependency-plugin</artifactId>
                <version>2.1</version>
                <executions>
                    <execution>
                        <id>copy-dependencies</id>
                        <phase>prepare-package</phase>
                        <goals>
                            <goal>copy-dependencies</goal>
                        </goals>
                        <configuration>
                            <outputDirectory>${basedir}/WebContent/WEB-INF/lib </outputDirectory>
                            <overWriteReleases>false</overWriteReleases>
                            <overWriteSnapshots>false</overWriteSnapshots>
                            <overWriteIfNewer>true</overWriteIfNewer>
                            <excludeGroupIds>
                                javax.crypto,javax.servlet.jsp,javax.transaction,javax.servlet
                            </excludeGroupIds>
 
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
    <dependencies>
        <dependency>
               <groupid>org.springframework</groupid>
               <artifactid>spring-webmvc</artifactid>
               <version>4.3.2.RELEASE</version>
          </dependency>
          <dependency>
               <groupid>javax.servlet</groupid>
               <artifactid>servlet-api</artifactid>
               <version>2.5</version>
               <type>jar</type>   
               <scope>compile</scope>
          </dependency>
          <dependency>
               <groupid>javax.servlet.jsp</groupid>
               <artifactid>jsp-api</artifactid>
               <version>2.0</version>
               <type>jar</type>
               <scope>compile</scope>
          </dependency> 
          <dependency>
               <groupid>javax.inject</groupid>
               <artifactid>javax.inject</artifactid>
               <version>1</version>
          </dependency>    
    </dependencies>
</project>
cs

다음은 아주 중요한 설정으로 ViewResolver 와 뷰 콤포넌트들을 기술한다.

WEB-INF/servelt-config/servlet-context.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<beans:beans xmlns:beans="http://www.springframework.org/schema/beans" 
  xmlns:context="http://www.springframework.org/schema/context" 
  xmlns:mvc="http://www.springframework.org/schema/mvc" 
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
  xsi:schemalocation="http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd
  http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
  http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
 
 <!-- DispatcherServlet Context: 서블릿의 요청 처리를 위한 것들을 정의 -->
 
 <!-- Enables the Spring MVC @Controller programming model --> 
 <mvc:annotation-driven></mvc:annotation-driven>
 
 <context:component-scan base-package="neuro.honeycomb"></context:component-scan>
 
 <mvc:view-controller path="/" view-name="index"></mvc:view-controller>
 
 <!-- 이미지, 자바스클립트, CSS 와 같은 정적 컨텐츠 요청(GET) 을 처리하기 위하여 설정 -->
 <mvc:resources location="/images/" mapping="/images/**"></mvc:resources>
 <mvc:resources location="/js/" mapping="/js/**"></mvc:resources>
 <mvc:resources location="/css/" mapping="/css/**"></mvc:resources>
 
 <!-- 뷰처리 위한 ViewResolver 정의  -->
 <beans:bean class="org.springframework.web.servlet.view.BeanNameViewResolver" id="beanNameViewResolver">
  <beans:property name="order" value="0"></beans:property>
 </beans:bean>
</beans:beans>
cs

component-scan 는 base-package 에 지정된 하위 패키지들에 정의된 @Controller 어노테이션이 기술된 Controller 클래스들을 검색하게 된다. resources 는 특정 URL 패턴에 해당하는 요청들의 자원들이 어디에 있는가를 정의한다. 이들은 ViewResolver 역할을 한다.
위의 설정에서는 정적 콘텐츠들을 resources 태그를 사용하여 매핑하고, bean 이름으로 요청을 매핑하는 ViewResolver 을 정의하고 있다.

예제 프로로그램

MAVEN 기반의 예제 프로그램소스
Eclipse 에서 위의 GIT 레파지토리에서 spring-web-example 을 MAVEN 프로젝트를 임포트한 후 빌드후 WebContent 를 웹 프로그램으로 배포하면 됨.

참조


Spring MVC Tutorial