[Spring Boot] 지역별 카페 추천 프로젝트 (3)
여행할 때 방문하면 좋을 카페를 추천하는 웹 프로젝트
📌 지역명을 클릭하면 해당 지역의 추천 카페 리스트가 뜨도록 처리하기
📍 필요한 정보 및 계획
카페 이름, 카페 추천 키워드, 카페 위치, 카페 사이트
📍 카페 추천 키워드
우선 카페 키워드를 만들 것인데 이는 enum을 사용할 것이다.
Keyword 클래스
package yeonjy.cafe.information;
public enum Keyword {
Wide("넓어요"),
View("풍경이_좋아요"),
SeaView("바다가_보여요"),
Traffic("교통이_좋아요"),
Bread("빵이_맛있어요"),
Coffee("커피가_맛있어요"),
;
private String krKeyword;
Keyword(String krKeyword) {
this.krKeyword = krKeyword;
}
public String getKrKeyword() {
return krKeyword;
}
}
📍 카페 추천 리스트 페이지 만들기
📎 header와 footer 만들기
새로운 페이지를 만들 때 헤더 부분은 공용으로 사용해주는 것이 좋을 것 같아 따로 빼준다.
후에 상용화하면 페이지 오류나 의견을 받기 위해 내 정보를 담는 곳도 필요할 것 같아 간단하게 footer.html도 만들었다.
start.html의 헤더부분을 그대로 header.html에 넣는다.
header.html
<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>☕카페 추천☕</title>
</head>
<body>
<div th:fragment="header" class="jumbotron text-center">
<h1>☕여행할 때 가면 좋은 카페 추천☕</h1>
<p>여행할 때 필수 코스인 카페를 손쉽게 정해보세요!</p>
</div>
</body>
</html>
footer.html
<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<div th:fragment="footer" class="card mt-5">
<div class="card-body text-center">
<p class="card-text">
<small class="text-muted">문의 및 오류 신고: yeonjypaper@gmail.com</small>
</p>
</div>
</div>
</html>
basic_layout.html
<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<th:block th:fragment="setContent(content)">
<head>
<meta charset="UTF-8">
<title>Title</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.1/dist/css/bootstrap.min.css">
<script src="https://cdn.jsdelivr.net/npm/jquery@3.6.0/dist/jquery.slim.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.1/dist/umd/popper.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.6.1/dist/js/bootstrap.bundle.min.js"></script>
</head>
<body>
<div class="header">
<th:block th:replace="~{/layout/header :: header}"/>
</div>
<!--Page Content-->
<div class="content">
<th:block th:replace="${content}"></th:block>
</div>
<div class="footer">
<th:block th:replace="~{/layout/footer :: footer}"/>
</div>
</body>
</th:block>
</html>
start.html
<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<th:block th:replace="~{/layout/basic_layout :: setContent(~{this::content} )}">
<th:block th:fragment="content">
<div class="container">
<div class="row">
<div th:each="areas : ${areas}">
<div class="col-sm-11">
<h3>[[${areas.local}]]</h3>
<p>[[${areas.local}]]에는 어떤 카페가 있는지 확인하세요!</p>
</div>
</div>
</div>
</div>
</th:block>
</th:block>
</html>
header를 빼주었으므로 layout을 적용하기 위해 start.html도 수정해주었다.
이제 실행해주면 원래 상태로 나오면서 footer도 추가적으로 나온다.
📎 카페 세부 내용 데이터베이스 만들기
Detail 클래스
package yeonjy.cafe.entity;
import lombok.*;
import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;
@Entity
@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor
@ToString(exclude = "keywords")
public class Detail {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long gno;
@Column(length = 100, nullable = false)
private String name;
@Column(columnDefinition = "TEXT", length = 100, nullable = false)
private String location;
@Column(columnDefinition = "TEXT", length = 200, nullable = false)
private String site;
/* Area와 N : 1 매핑 */
@ManyToOne
@JoinColumn(name = "area_gno")
private Area area;
/* DKeyword 와 1 : N 매핑 */
@OneToMany(mappedBy = "detail", fetch = FetchType.EAGER)
@Builder.Default
private List<DKeyword> keywords = new ArrayList<DKeyword>();
}
@ToString(exclude = "keywords") 를 한 이유는 리포지토리 테스트코드를 짰는데 이때, String.valueOf() 메소드를 사용해서 Keyword Enum을 변수로 넣었다.
하지만, 해당 메소드는 아래와 같이 obj.toString을 호출한다.
public static String valueOf(Object obj) {
return (obj == null) ? "null" : obj.toString();
}
이럴 경우, Detail 엔티티의 toString() 메서드를 호출하게 되는 것이고, Detail 엔티티의 toString()에는 Keyword엔티티가 정의되어있기 때문에 Keyword의 toString() 메소드를 호출하게 된다.
toString() 메소드를 무한 반복하면서 StackOverflowError가 발생한다.
이 때문에 @ToString(exclude = "keywords") 이와 같은 처리를 한 것이다.
Detail 클래스와 Area 클래스를 매핑해주어야 하기 때문에 Area 클래스도 수정한다.
Area 클래스
package yeonjy.cafe.entity;
import lombok.*;
import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;
@Entity
@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor
@ToString(exclude = "details")
public class Area {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long gno;
@Column(length = 100, nullable = false)
private String local;
/* Detail과 1 : N 매핑 */
@OneToMany(mappedBy = "area", fetch = FetchType.EAGER)
@Builder.Default
private List<Detail> details = new ArrayList<Detail>();
}
DKeyword 클래스
package yeonjy.cafe.entity;
import lombok.*;
import javax.persistence.*;
@Entity
@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor
@ToString(exclude = "detail")
public class DKeyword {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long gno;
@Column(length = 50, nullable = false)
private String keyword;
/* Detail과 N : 1 매핑 */
@ManyToOne
@JoinColumn(name = "detail_gno")
private Detail detail;
}
Keyword는 Enum으로 만들었다.
Keyword 클래스
package yeonjy.cafe.information;
public enum Keyword {
Default("업데이트 예정입니다."),
Wide("넓어요"),
View("풍경이_좋아요"),
SeaView("바다가_보여요"),
Traffic("교통이_좋아요"),
Bread("빵이_맛있어요"),
Coffee("커피가_맛있어요"),
;
private String krKeyword;
Keyword(String krKeyword) {
this.krKeyword = krKeyword;
}
public String getKrKeyword() {
return krKeyword;
}
}
📎 리포지토리 만들기
리포지토리는 모두 JpaRepository를 extends 하기 때문에 인터페이스만 만들어도 된다.
DetailRepository 인터페이스
package yeonjy.cafe.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.querydsl.QuerydslPredicateExecutor;
import yeonjy.cafe.entity.Detail;
public interface DetailRepository extends JpaRepository<Detail, Long>, QuerydslPredicateExecutor<Detail> {
}
DKeywordRepository 인터페이스
package yeonjy.cafe.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.querydsl.QuerydslPredicateExecutor;
import yeonjy.cafe.entity.DKeyword;
public interface DKeywordRepository extends JpaRepository<DKeyword, Long>, QuerydslPredicateExecutor<DKeyword> {
}
이제 만든 리포지토리를 이용해서 엔티티를 테스트하는 테스트 코드를 만들어본다.
DetailRepositoryTests 클래스
package yeonjy.cafe.repository;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import yeonjy.cafe.entity.Area;
import yeonjy.cafe.entity.DKeyword;
import yeonjy.cafe.entity.Detail;
import yeonjy.cafe.information.Keyword;
@SpringBootTest
public class DetailRepositoryTests {
@Autowired
private AreaRepository areaRepository;
@Autowired
private DetailRepository detailRepository;
@Autowired
private DKeywordRepository dKeywordRepository;
@Test
public void insertDetails() {
Area area1 = Area.builder()
.gno(1L)
.local("수원")
.build();
System.out.println(areaRepository.save(area1));
Detail detail1 = Detail.builder()
.gno(1L)
.name("이학순 베이커리")
.location("경기 수원시 장안구 경수대로 1252")
.site("https://www.instagram.com/leehaksoon_bakery/")
.area(area1)
.build();
System.out.println(detailRepository.save(detail1));
DKeyword keyword1 = DKeyword.builder()
.gno(1L)
.keyword(String.valueOf(Keyword.Bread))
.detail(detail1)
.build();
System.out.println(dKeywordRepository.save(keyword1));
}
}
[Spring boot] Member 테이블과 product 테이블 엮어보기 (tistory.com)
[Spring boot] Member 테이블과 product 테이블 엮어보기
하나의 유저가 여러개의 물품을 가지고 있는것을 DB에 저장하려고 합니다. 이를 위해서는 다대일 구조로 만들어보는것을 생각했습니다. 일단 Product 테이블을 만들어봅니다. create table product( produ
wonin.tistory.com
Spring Boot 게시판 만들기(RemakeBoard) - 06 ( Service 작성 ) (velog.io)