inblog logo
|
하쎄의 기술 일기장
    TeamProject

    [Travel] 핫플 리스트 출력

    핫플 리스트 출력
    하세연's avatar
    하세연
    Sep 29, 2024
    [Travel] 핫플 리스트 출력
    Contents
    1. 하위의 시군구 정보 가져오기2. 지역 관광정보 가져오기
     

    1. 하위의 시군구 정보 가져오기

     

    지역 코드 전달

     
    지역 버튼을 눌렀을 때 해당 지역의 하위 지역들을 선택하고 싶다.
    notion image
     
    지역을 나타내는 area code는 지역 버튼을 그리는 div 태그 내부의
    input 태그에 hidden으로 값을 넣어뒀다.
     

    시군구 코드정보 얻기

     
    hotplace.mustache
    notion image
     
    지역을 선택할 때 이 value값으로 해당 지역의 하위 시군구코드를 찾아 올 것이다.
    도시의 경우 시군구 코드는 도시의 구/군 정보를 담고 있고(서울시 - 영등포구)
    도의 경우 시 정보를 담고 있다.(경기도 - 수원시 )
     
     
     
    미리 설정해둔 변수.
    let sigungus= []; let areaCode;
    areaCode에 hidden의 value값을 넣어서 /get-sigungu 라는 주소로 Get요청을 하는데
    쿼리스트링으로 area를 키값. areaCode를 value값으로 전달해서 데이터를 받을 것이다.
    서울을 선택하면 /get-sigungu?area=1 부산을 선택하면 /get-sigungu?area=6 이 된다.
     
     
    js/hot-place.js
    notion image
     
     
    AreaController
    @GetMapping("/get-sigungu") public ResponseEntity<?> getSigungu(@RequestParam("area") String area) { AreaResponse.AreaDTO areaDTO = areaService.시군구리스트가져오기(area); return ResponseEntity.ok(Resp.ok(areaDTO)); }
     
    @GetMapping으로 /get-sigungu 주소에 연결되고 @RequestParam(”area”) String area에서
    /get-sigungu?area=1 에서 ?뒤의 쿼리스트링 area 값을 받아서 string area에 1을 넣어준다.
    Get요청을 받아서 AreaService에서 시군구리스트가져오기 메서드에 area코드를 전달하면서
    실행하고 AreaResponse.AreaDTO를 전달 받는다. 그리고 Return은 우리가 만들어 둔 Resp를 사용.
     
     
    AreaService
    public AreaResponse.AreaDTO 시군구리스트가져오기(String areaCode) { Area area = areaRepository.findByArea(areaCode); AreaResponse.AreaDTO areaDTO = new AreaResponse.AreaDTO(area); return areaDTO; }
    area코드를 전달 받아서 AreaRepository에 만들어 둔 findByArea에 area코드를
    전달하면서 해당 Area 정보를 리턴 받는다.
    Area 정보를 가져 오면 DTO로 만들어서 Controller에 전달한다.
     
     

    DTO로 전달하는 이유

    DTO로 전달하는 이유는 ResponseEntity<> 로 리턴을 하게 되면 JSON으로 파싱해서 전달을 하게 되는데 그냥 Entity를 전달할 경우 Controller에서 ResponseEntity로 응답을 한다고 JSON으로 파싱하면서 엔티티와 연관관계에 있는 다른 Entity를 조회하게 되는데 우리는 OIV (Open In View) = false 이기 때문에 Controller에서는 Connection이 끊긴 상태이고 JSON으로 파싱하면서 다른 엔티티를 조회할 때 Connection이 없기 때문에 LazyInitializationException 이 발생하게 된다. 만약 OIV = true인 상태라면(사용하지는 않지만 그냥 예시를 든다면) Connection이 있기 때문에 다른 엔티티를 조회할 때 엔티티가 서로 참조하는 순환고리가 있다면 계속 참조하다가 stackOverflow가 터진다. 그렇기 때문에 ResponseEntity<>, @ResponseBody, @RestController 로 응답을 할 때는 JSON으로 파싱해서 응답을 하기 때문에 DTO를 만들어서 응답하는 것이 좋다.
     
    AreaResponse의 AreaDTO
    @Data public static class AreaDTO { private String code; private String name; private List<SigunguDTO> sigunguDtos = new ArrayList<>(); public AreaDTO(Area area) { this.code = area.getCode(); this.name = area.getName(); for(Sigungu sigungu : area.getSigungus()) { sigunguDtos.add(new SigunguDTO(sigungu)); } } @Data public class SigunguDTO { private Integer id; private String code; private String name; public SigunguDTO(Sigungu sigungu) { this.id = sigungu.getId(); this.code = sigungu.getCode(); this.name = sigungu.getName(); } } }
    DTO 만드는 것의 핵심은 엔티티 혹은 엔티티 검색결과를 전달해서 필요한 정보만 뽑아서
    DTO에 끼워넣는 것이다.
     
     
     
     
    AreaRepository
    public Area findByArea(String area) { return em.createQuery("select a from Area a join fetch a.sigungus s where a.code =:area", Area.class) .setParameter("area", area) .getSingleResult(); }
     
    area코드를 전달 받아서 Area엔티티 테이블과 그 연관관계에 있는 Singungu 테이블을
    Area의 code로 join fetch를 써서 한 방 쿼리로 가져 온다.
     
    Hibernate의 쿼리문을 확인해보면
    notion image
     
    area_tb와 sigungu_tb를 join하는데 area_tb의 code와 sigungu_tb의 area_code(area_tb의 외래키)
    를 붙여서 area_tb의 코드를 조건으로 해서 가져 온다.
     
    이를 H2 콘솔에서 부산 area코드인 6을 넣어서 검색해보면 잘 나오는 것을 확인할 수 있다.
     
    notion image
     
     
    이렇게 받아온 정보를 Service에서 DTO를 만들어서 컨트롤러로 전달하고
    컨트롤러는 해당 DTO로 JSON으로 파싱해서 응답한다.
     
    다시 /get-sigungu에 요청을 한 자바스크립트로 돌아가자면
    $.ajax({ url: `/get-sigungu?area=${areaCode}`, type: 'GET', dataType: 'json', success: function (response) { sigungus = response.body.sigunguDtos; $(".modal__area").text(response.body.name); generateAreaButtons(); console.log(sigungus); }, error: function (error) { console.error("에러 발생:", error); } });
     
    Controller가 ResponseEntity<>로 응답 해줬고 해당 response의 모습이 궁금하면
    postman을 사용해서 보내보거나 console.log(response.body)로 찍어 보면 된다.
     
     
     
    postman 사용해서 확인
     
    notion image
     
    이렇게 보내보면
     
    notion image
     
    response.body 에 Area의 code, name, sigunguDtos 가 들어 있고
    sigunguDtos에는 하위지역들이 배열로 들어 있다.
     
    console.log("response:" + response.body); 로 콘솔창에서 확인하면
    notion image
     
     
    console도 깔끔한 것 같다.
    하지만 지금은 만들어 놓고 결과를 console로 확인할 수 있기에 가능한 것이고
    그 전에는 개발 과정에서는 postman으로 확인하는 것이 매우 편하다.
     
    postman 사용 방법
     
     
     

    2. 지역 관광정보 가져오기

     
    area코드와 이렇게 가져온 sigungu코드를 사용하면
    어느지역의 어느 시군구에 어떤 content(여행지, 음식점, 축제 정보 등)가 있는지 검색 가능하다.
     
    지역 버튼을 눌렀을 때 modal창에 하위 시군구에 대한 동적 버튼이 생성되게 했다.
     
    notion image
     

    선택창 지역 이름 설정

     
    지역 버튼을 누르면 기본 서울로 되어 있지만
    class가 modal__area인 태그를 응답으로 받은 body의 name으로 해당 지역의 이름을 표시한다.
    $(".modal__area").text(response.body.name);
    notion image
     
     

    같은 이름의 복수 쿼리스트링

     
    그리고 전체 버튼과 시군구 코드를 기반으로 해당 이름의 버튼을 생성하고
    이 버튼들의 태그 속성에 시군구 코드를 추가했다.
     
    전체 버튼을 선택하면 area코드만 전송, 시군구 버튼을 선택하면 area코드와 함께
    sigungu 코드가 sigungu라는 같은 이름의 쿼리스트링에 복수의 값을 배열로 전달한다.
     
     
    notion image
     
     
    이렇게 area의 지역코드는 아까 지역을 선택할 때 value값을 그대로 사용하고
    여러 하위지역들이 선택되면 그 뒤에 / get-hotplace?area=1?sigungu=1?sigungu=2
    이런식으로 같은 이름의 쿼리스트링을 전달한다.
     
    이를 서버에서 List<String>으로 받아 준다.
     
     
    ContentController
    @GetMapping("/hotplace") public String hotPlace( @RequestParam(value = "area", required = false) String area, @RequestParam(value = "sigungu", required = false) List<String> sigungu, HttpServletRequest request) { List<ContentResponse.HotPlaceDTO> hotPlaceDtos = contentService.핫플목록보기(area, sigungu); System.out.println(hotPlaceDtos); request.setAttribute("models", hotPlaceDtos); return "/hotplace/hotplace"; } @GetMapping("/get-hotplace") public ResponseEntity<?> hotPlaceFilter( @RequestParam(value = "area", required = false) String area, @RequestParam(value = "sigungu", required = false) List<String> sigungu ) { List<ContentResponse.HotPlaceDTO> hotPlaceDtos = contentService.핫플목록보기(area, sigungu); return ResponseEntity.ok(Resp.ok(hotPlaceDtos)); }
     
     
    @GetMapping(”/get-hotplace”) 에서 파라미터를 area와 sigungu를 받아주는데 여기서는
    area가 무조건 필요해서 required=false를 안 넣어줘도 되고
    sigungu는 지역 전체를 조회할 때는 area값만 필요하므로 null을 허용할 수 있게
    required=false를 반드시 붙여줘야 된다.
    sigungu는 복수 쿼리 스트링이 올 수도 있으므로 List<String>타입으로 받아준다.
     
    ContentService에 핫플목록보기 메서드에 area와 List<sigungu>를 전달한다.
    Area에서와 마찬가지로 ResponseEntity로 JSON으로 파싱해서 응답을 하므로
    DTO를 만들어서 전달해준다.
     
     
    @GetMapping(”/hoplace”) 에서는 핫플 페이지로 들어올 때이므로
    이때는 area와 sigungu 둘 다 required=false가 필요하다.
    content_tb에서 모든 데이터를 대상으로 지역 상관없이 특정 조건(여기서는 조회수)으로
    정렬할 것이기 때문이다.
     
    그렇기에 ContentService의 같은 메서드를 실행하는데
    전달하는 area와 sigungu 의 null 유무에 따라 조회 방법이 달라진다.
     
     
     
     
    ContentService
    public List<ContentResponse.HotPlaceDTO> 핫플목록보기(String area, List<String> sigungu) { List<Content> contents = new ArrayList<>(); if(area == null && sigungu == null) { contents = contentRepository.findHotPlaceAll(); } else if(area != null && sigungu == null) { contents = contentRepository.findHotPlaceByArea(area); } else if(area != null && sigungu != null) { contents = contentRepository.findHotPlaceByAreaAndSigungu(area, sigungu); } List<ContentResponse.HotPlaceDTO> hotPlaceDtos= new ArrayList<>(); for(Content content : contents) { hotPlaceDtos.add(new ContentResponse.HotPlaceDTO(content)); } return hotPlaceDtos; }
     
    area와 sigungu 둘 다 null이면 데이터를 불러 와서 조회수 기반으로 뿌려주는 메서드 실행
    area는 존재하고 sigungu의 null 유무에 따라 조회 방법이 또 달라진다.
     
    검색 결과가 List<Content>이므로 이를 하나씩 뽑아다가 DTO로 만들어서
    List<ContentResponse.HotPlaceDT>에 넣어주고 리턴한다.
     
     
     
    ContentRepository
    public List<Content> findHotPlaceAll() { return em.createQuery("select c from Content c order by c.viewCount desc", Content.class) .setFirstResult(0) .setMaxResults(20) .getResultList(); } public List<Content> findHotPlaceByArea(String area) { return em.createQuery("select c from Content c where c.areaCode =:area order by viewCount desc", Content.class) .setParameter("area", area) .setFirstResult(0) .setMaxResults(20) .getResultList(); } public List<Content> findHotPlaceByAreaAndSigungu(String area, List<String> sigungu) { return em.createQuery("select c from Content c where c.areaCode =:area and c.sigunguCode in (:sigungu) order by viewCount desc", Content.class) .setParameter("area", area) .setParameter("sigungu", sigungu) .setFirstResult(0) .setMaxResults(20) .getResultList(); }
     
    페이징 할 때 20개씩 출력하려고 .setMaxResult(20)으로 했다.
    .setFirstResult(0)로 시작 인덱스를 0으로 해놨는데 페이징을 할 때
    0번째부터 가져오는 것이므로 페이징 기능 넣을 때 수정이 필요하다.
     
    쿼리가 잘 나가는지 한 번은 확인해볼 필요가 있다.
    부산에서 여러 하위지역을 선택해서 findHotPlaceByAreaAndSigungu()를 호출했다.
     
    notion image
     
    이렇게 선택하고 검색하니 아래의 쿼리를 날렸다.
    where 조건에 area와 sigungu (복수라서 in 쿼리)이 잘 들어갔다.
     
    notion image
    1번 ? 인 area에 부산 지역 코드인 6
    그 다음 ?에 sigungu 코드가 6개 야무지게 들어갔다.
    그 다음은 .setFirstResult(0) .setMaxResults(20) 관련인데 이것도 잘 들어갔다.
    notion image
     
    ? 를 Trace 하는 것은
    application.properties에
    logging.level.org.hibernate.orm.jdbc.bind:trace
    옵션인데 다음 업로드에서 설정해줄 것이다.
    db에서 검색해보면 잘 나온다.
     
    notion image
     
     
     

    조회한 정보 뿌리기

     
    ContentResponse의 HotPlaceDTO
    @Data public static class HotPlaceDTO { public String contentId; public String contentTypeId; public String title; public String addr1; public String firstImage; public String viewCount; public String likeCount; public HotPlaceDTO(Content content) { this.contentId = content.getContentId(); this.contentTypeId = content.getContentTypeId(); this.title = content.getTitle(); this.addr1 = content.getAddr1(); this.firstImage = content.getFirstImage(); if(firstImage == "") { this.firstImage = "/images/hotplace/no-image.jpg"; } this.viewCount = content.getViewCount(); this.likeCount = content.getLikeCount(); } }
     
    viewCount와 likeCount는 따로 테이블을 만들면 뺄 예정이다.
     
     
    이렇게 조회한 데이터로 Controller에서 응답을 해주면
    Contents를 뿌려주던 태그 내부를 비우고
    받은 응답에서 Content를 하나씩 꺼내서 반복문 돌리면서 태그 내부에 붙여준다.
     
    hot-place.js의 loadHotPlace()
    function loadHotPlace(response) { if (response) { $(".hot__place__card__box").empty(); // fetch는 데이터를 받아서 .json()으로 파싱(json -> js object)해줘야 하는데 // ajax의 success 콜백 함수는 json -> js object로 자동 파싱된다. 그냥 쓰면 됨 let placeList = response.body; // 파싱한 데이터를 출력할 박스에 출력 for (place of placeList) { $(".hot__place__card__box").prepend(printHotPlace(place)); } } else { return `<h2>검색 결과가 없습니다.</h2>`; } }
     
     
     
    박스 내부에 붙여줄 때 필요한 정보를 꺼내서 넣어준다.
     
    hot-place.js의 printHotPlace()
    function printHotPlace(place) { return ` <div class="hot__place__card"> <a href="/get-info/${place.contentId}"> <div class="hot__place__img__box"> <img src="${place.firstImage}" class="active" alt="이미지 1"> <img src="${place.firstImage}" alt="이미지 2"> </div> </a> <button class="like__btn" onclick="toggleLike(this)"> <svg id="like-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" xmlns="http://www.w3.org/2000/svg"> <path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" /> </svg> </button> <div class="dots"> <button class="active" onclick="showSlide(0, this)"></button> <button onclick="showSlide(1, this)"></button> </div> <div class="hot__place__description"> <p class="hot__place__name">${place.title}</p> <p class="hot__place__location">${place.addr1}</p> </div> </div>`; }
     
     
     
     
    이제 핫플 페이지로 가면 20개의 관광지, 음식점, 축제 정보가 출력된다.
    지역을 누르고 전체 혹은 하위지역 선택 후 검색하면 해당 지역의 관광 정보가 20개 출력된다.
     
    이제 분류코드를 이용해서 축제를 제외하고
    관광지면 관광지, 음식점이면 음식점만 출력하게 필터를 걸고
    페이징 기능을 추가한 뒤 지역별 아이콘을 바꿔줘야겠다.
    2부에서 계속..
    Share article

    하쎄의 기술 일기장

    RSS·Powered by Inblog