공공 데이터 포털 API 를 사용하여 국내 여행지, 음식점, 축제 등을 소개하는 프로젝트이다.
팀원: 성훈님(팀장), 윤혜지(나), 은지님, 성윤님, 세연님 (총 5인)
개발 기간 : 2024.09.04 ~ 2024.09.25
사용 기술 : JAVA, SPRINGBOOT, HTML, JAVASCRIPT, JQUERY
1. WF 그리기
본인이 구현한 화면은 백엔드 기능까지 완성하기로 했기 때문에 피그마로 화면 작성부터 하였다.



내가 담당하게 된 페이지는 이렇게 메인 페이지, 여행지, 상세 정보 페이지이다. (상세 정보 페이지는 내가 만든 페이지를 활용하여 공동으로 사용하게 되었다.)
페이지 만들며 작성한 게시글 → https://inblog.ai/hj/html-구성을-위한-투쟁-28606
2. 기능 구현
2-1. 여행지 리스트 구현

제일 어려웠던 페이지는 이 페이지 였는데
리스트를 출력하기 위해
최신순, 거리순, 인기순
, 시, 군, 구, 전체
이렇게 선택하는 항목과 그에 따라 처리해야 하는 데이터가 다 달랐기 때문이다.서비스에 시 클릭, SORTBY 클릭, 군 클릭, 이런식으로 나누어 만들지 않고,
조회를 위해 필요한 매개변수들을 DTO 로 담아 내부에서 동적 쿼리를 사용하여 하나의 서비스로 해결하였다.
해당 포스팅
→ https://inblog.ai/hj/36963 (리스트 출력)
→ https://inblog.ai/hj/36965
(여러 매개변수 추가 하여 작동하도록)
2-2. 상세 페이지 구현

상세 페이지는 공공데이터로 API 요청을 보내서 출력되게 구성되어 있다.
리턴 형식이 JsonData 라서 DTO 로 파싱하여야 했다.
정말 희안하게 단일 데이터 임에도
body → items → item
내부에 배열로 존재했다.해당 포스팅
2-3. 카카오 지도 API 사용
고난에 빠진 팀원분을 도와주며 공부한 덕분에 내 기능 구현에도 유용하게 사용할 수 있었다. 👍
3. 프로젝트 완성
이렇게 프로젝트는 잘 마무리 되었다.
아쉬운 점은 댓글에 사진 첨부 기능이 없다는 점인데 DB 와 여러가지 추가할 사항이 많아져서 시간상 구현하지 못하고 끝나게 되었다.

배운점!
공공API 를 사용할 때 내가 기대한 것 처럼 데이터가 오지 않았다. 이럴때 데이터의 구조를 확인하고 어떻게 꺼내서 프로젝트에 맞게 활용할 수 있는지 알 수 있었다.
여러가지 파라미터가 추가된 검색을 이전에 쇼핑몰 프로젝트를 하며 구현했을때는 이렇게 QueryDSL 을 사용하여 구현했었다. 이번에는 JPQL 을 사용하여 구현할 수 있어서 여러 방법을 경험해서 좋았다.
동적 쿼리를 사용하거나, 타입 안정성을 위해서는 QueryDSL 을 사용하는게 낫다고 하지만 기본적인 쿼리문 공부를 위해서 JPQL을 사용하는 의미가 있었다.
@Override
public Page<Tuple> selectProductsByCate(ProductPageRequestDTO pageRequestDTO, Pageable pageable){
String sort = pageRequestDTO.getSort();
String seletedCate = pageRequestDTO.getCateCode();
OrderSpecifier<?> orderSpecifier = null;
log.info("here1 : " + sort);
if (sort != null && sort.startsWith("prodSold")){
orderSpecifier = qProduct.prodSold.desc();
}else if (sort != null && sort.startsWith("prodLowPrice")) {
orderSpecifier = qProduct.prodPrice.asc();
}else if (sort != null && sort.startsWith("prodHighPrice")) {
orderSpecifier = qProduct.prodPrice.desc();
}else if (sort != null && sort.startsWith("prodScore")) {
orderSpecifier = qProduct.tReviewScore.desc();
}else if (sort != null && sort.startsWith("prodReview")) {
orderSpecifier = qProduct.tReviewCount.desc();
}else if (sort != null && sort.startsWith("prodRdate")) {
orderSpecifier = qProduct.prodRdate.desc();
}else if (sort != null && sort.startsWith("prodHit")) {
orderSpecifier = qProduct.prodHit.desc();
}else if (sort != null && sort.startsWith("prodDiscount")){
orderSpecifier = qProduct.prodDiscount.desc();
}else {
orderSpecifier = qProduct.prodSold.desc();
}
QueryResults<Tuple> results = jpaQueryFactory
.select(qProduct, qProductimg)
.from(qProduct)
.join(qProductimg)
.on(qProduct.prodNo.eq(qProductimg.prodNo))
.where(qProduct.cateCode.like(seletedCate+"%"))
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.orderBy(orderSpecifier)
.fetchResults();
List<Tuple> content = results.getResults();
log.info(content.toString());
long total = results.getTotal();
log.info("total : {}", total);
return new PageImpl<>(content, pageable, total);
}
public List<Content> findByContentTypeId(String contentTypeId,String sortBy, Pageable pageable) {
StringBuilder queryStr = new StringBuilder("select c from Content c where c.contentTypeId = :contentTypeId");
if (sortBy != null && !sortBy.isEmpty()) {
if (sortBy.equals("createdTime")) {
queryStr.append(" order by c.createdTime desc");
} else if (sortBy.equals("viewCount")) {
queryStr.append(" order by c.viewCount desc");
}
}
Query query = em.createQuery(queryStr.toString(), Content.class);
query.setParameter("contentTypeId", contentTypeId);
query.setFirstResult((int) pageable.getOffset()); // 시작 위치
query.setMaxResults(pageable.getPageSize()); // 한 페이지에 표시할 최대 개수
return query.getResultList();
}
public List<Content> findByContentTypeIdAndOption(String contentTypeId, String area, String sigunguCode,String sortBy, Pageable pageable) {
StringBuilder queryStr = new StringBuilder("select c from Content c where c.contentTypeId = :contentTypeId and c.areaCode= :area");
if (sigunguCode != null && !sigunguCode.isEmpty()) {
queryStr.append(" and c.sigunguCode = :sigunguCode");
}
if (sortBy != null && !sortBy.isEmpty()) {
if (sortBy.equals("createdTime")) {
queryStr.append(" order by c.createdTime desc");
} else if (sortBy.equals("viewCount")) {
queryStr.append(" order by c.viewCount desc");
}
}
Query query = em.createQuery(queryStr.toString(), Content.class);
query.setParameter("contentTypeId", contentTypeId);
query.setParameter("area", area);
if (sigunguCode != null && !sigunguCode.isEmpty()) {
query.setParameter("sigunguCode", sigunguCode);
}
query.setFirstResult((int) pageable.getOffset()); // 시작 위치
query.setMaxResults(pageable.getPageSize()); // 한 페이지에 표시할 최대 개수
return query.getResultList();
}
Share article