안녕하세요. 이어서 적어보겠습니다.
현재 이 글을 쓰면서, Github에 파일로 올렸으니, 아래 코드 내용을 다시 칠 필요는 없을 검니다~.
전 글에서, Github을 연결했다면... 인텔리제이에서, 해당 프로젝트를 열고, Git, Pull... 해당 메뉴를 열면, Github이 업데이트 됨니다~.
https://github.com/infott2t/ex05-springboot-querydsl
GitHub - infott2t/ex05-springboot-querydsl
Contribute to infott2t/ex05-springboot-querydsl development by creating an account on GitHub.
github.com
Spring Boot, QueryDSL. 저도 처음에 알지 못해 헤멨는데요. 한번에 다 알면 물론, 좋겠지만, 학습에 시간이 걸린다고 생각하구요. 또는 실제로 구현하고자 하는 작업이 있다면, 좀더 알기 편해질지도 모르구요. 그려려니 하고 넘어가는 것도 필요하다고 생각해요. 훑어만 보는 것이죠~. QueryDSL을 접해보지 못해보셨다면, 1편의 내용을 이해하고, 지금의 내용은, 필요할 때 참조해 보는 것이 났다고도 생각하는 군요.
실제 온라인 강의를 듣는 것을 추천하네요.
QueryDSL의 경우, 검색조건이 많을때에 코드가 복잡해지지 않습니다. 검색조건이 많을때 Condition을 사용해서... 편하게 사용하기 위해 쓴다고 할 수 있죠.
node.js의 Elastic Search 처럼, 검색조건이 많을 때 사용하면 편하다. 이렇게 기억하시면 되겠습니다~.
전에 작성했던, Workplan의 엔티티를 좀더 자세하게 적을 필요가 있더군요. 맵핑을 해서, 타임리프를 잘 사용할 수 있게 만드는 과정이군요. #맵핑하기
맵핑은, 여러개의 테이블을 연결시킨다. 이렇게 기억하시면 되겠습니다. 쉽게 생각해서, 테이블이 커진다. 이렇게 생각하면 편해요. 한쪽 테이블이 커지는 것이죠. 그리고, 커지는 테이블, 해당 칼럼에 @ManyToOne이라는 어노테이션을 사용하고, 해당 ID를 가져온다. 이렇게 생각하면 되겠죠.
이번편에서 맵핑을 한 이유는, 각 협력사마다, row. 부트스트랩의 row가 달라져서 구분되어야하는데, 타임리프로 그렇게 만들기가 어렵더군요. 그래서, row가 변경되는 이유인, 협력사 이름. A푸드, B 푸드. 이값을 따로 빼주는 작업. 협력사 테이블을 따로 만드는 것을 생각하게 되었군요.
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import lombok.experimental.SuperBuilder;
import javax.persistence.*;
import java.time.LocalDateTime;
@Entity
@Getter
@Setter
@RequiredArgsConstructor
@SuperBuilder
@Table(name="T_WORKPLAN")
public class WorkPlan {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "T_WORKPLAN_ID")
private Long id; //아이디
//private String workPlanCooperation; //협력사
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "T_COPERATION_ID")
private Coperation coperation; // 협력사 (테이블이 커진다. Coperation 엔티티(테이블)를 가져왔다.)
private String workPlanTitle; //제목
private String workPlanTag; //태그
private LocalDateTime workPlanStartDate; //시작일
private LocalDateTime crateDate; // 생성일
private LocalDateTime updateDate; // 수정일
private String workPlanStatus; //상태 N인 경우, 서비스 중단의 경우.
//private ServView servView; //서비스 뷰 보기버튼을 눌렸을 때.
}
협력사 부분을 따로 빼주어서 엔티티로 만듬니다. 엔티티가 바뀌었음으로, 해당 도메인의 내용을 조금씩 수정해줌니다.
WorkPlanRepositoryImpl.java,
package org.example.domain.serv.workplan;
import com.querydsl.core.BooleanBuilder;
import com.querydsl.core.types.Predicate;
import com.querydsl.core.types.Projections;
import com.querydsl.jpa.impl.JPAQueryFactory;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import javax.persistence.EntityManager;
import java.time.LocalDateTime;
import java.time.format.DateTimeParseException;
import java.util.List;
import static org.example.domain.serv.workplan.QWorkPlan.workPlan;
import static org.example.domain.coperation.QCoperation.coperation;
import static org.springframework.util.StringUtils.hasText;
public class WorkPlanRepositoryImpl implements WorkPlanRepositoryCustom {
private final JPAQueryFactory queryFactory;
public WorkPlanRepositoryImpl(EntityManager em) {
this.queryFactory = new JPAQueryFactory(em);
}
@Override
public Page<WorkPlanApiDto> searchAllV2(WorkPlanSearchCondition condition, Pageable pageable) {
List<WorkPlanApiDto> content = queryFactory.
select(Projections.constructor(WorkPlanApiDto.class,
workPlan.id,
workPlan.coperation,
workPlan.workPlanTitle,
workPlan.workPlanTag ,
workPlan.workPlanStatus ,
workPlan.workPlanStartDate ,
workPlan.crateDate ,
workPlan.updateDate
)).from(workPlan)
.join(workPlan.coperation, coperation)
.where(
// searchAllV2Predicate(condition)
)
.orderBy(workPlan.id.desc())
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.fetch();
long total = queryFactory
.select(workPlan.count())
.from(workPlan)
.where(
// searchAllV2Predicate(condition)
)
.fetch().get(0);
return new PageImpl<>(content, pageable, total);
}
/*
private BooleanBuilder searchAllV2Predicate(ProductCategorySearchCondition condition){
return new BooleanBuilder()
.and(condS(condition.getField(), condition.getS()))
.and(condSdate(condition.getSdate()))
.and(condEdate(condition.getEdate()));
}
private Predicate condS(String field, String s){
BooleanBuilder builder = new BooleanBuilder();
if(hasText(field) && hasText(s)) {
if(field.equals("all")){
builder.or(alliance.userTitle.like("%" + s + "%"));
builder.or(alliance.userContent.like("%" + s + "%"));
//builder.or(alliance.isrtDate.between(sdate, edate));
} else if(field.equals("title")) {
builder.or(alliance.userTitle.like("%" + s + "%"));
} else if(field.equals("content")) {
builder.or(alliance.userContent.like("%" + s + "%"));
}
}
return builder;
}
private Predicate condSdate( String sdate){
BooleanBuilder builder = new BooleanBuilder();
if(hasText(sdate)){
try {
LocalDateTime localDateTime = LocalDateTime.parse(sdate + "T00:00:00");
builder.or(alliance.isrtDate.goe(localDateTime)); // isrtDate >= sdate
} catch (DateTimeParseException e) {
}
}
return builder;
}
private Predicate condEdate( String edate){
BooleanBuilder builder = new BooleanBuilder();
if(hasText(edate)) {
try {
LocalDateTime localDateTime = LocalDateTime.parse(edate + "T00:00:00");
builder.or(alliance.isrtDate.loe(localDateTime)); // isrtDate <= edate
} catch (DateTimeParseException e) {
}
}
return builder;
}
*/
@Override
public List<WorkPlanApiDto> searchFindAllDesc() {
List<WorkPlanApiDto> content = queryFactory.
select(Projections.constructor(WorkPlanApiDto.class,
workPlan.id,
workPlan.coperation,
workPlan.workPlanTitle,
workPlan.workPlanTag ,
workPlan.workPlanStatus ,
workPlan.workPlanStartDate ,
workPlan.crateDate ,
workPlan.updateDate
)).from(workPlan)
.join(workPlan.coperation, coperation)
.orderBy(workPlan.id.asc())
.fetch();
return content;
}
}
엔티티 WorkPlan 클래스에서, Coperation클래스를 가져다가 사용하는데요. 그렇기 때문에, join으로 연결시켜줘야합니다.
Coperation 엔티티,
package org.example.domain.coperation;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import lombok.experimental.SuperBuilder;
import javax.persistence.*;
import java.time.LocalDateTime;
@Entity
@Getter
@Setter
@RequiredArgsConstructor
@SuperBuilder
@Table(name="T_COPERATION")
public class Coperation {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "T_COPERATION_ID")
private Long id;
private String coperationName; //협력사명
private LocalDateTime crateDate; // 생성일
}
그리고, 전에 사용했던 QueryDSL 코드 자동생성기로, 위의 Coperation에 해당하는 코드들을 해당 도메인, 패키지인 org.example.domain.coperation에 붙여넣기 해줌니다.
빌더를 다시 만들어줘요. 엔티티 클래스 위에 @SuperBuilder를 통해서, 그냥 내용을 순서대로 쭉적으면 됨니다. 또, 편한점은, 내용, 칼럼이 전부 젹혀지지 않아도, insert된다는 점이 있습니다. Coperation 엔티티의 crateDate가 없어도 만들어졌죠~. #SuperBuilder
package org.example;
import lombok.RequiredArgsConstructor;
import org.example.domain.coperation.Coperation;
import org.example.domain.coperation.CoperationRepository;
import org.example.domain.coperation.CoperationService;
import org.example.domain.member.MemberDto;
import org.example.domain.member.MemberService;
import org.example.domain.serv.workplan.WorkPlan;
import org.example.domain.serv.workplan.WorkPlanRepository;
import org.example.domain.serv.workplan.WorkPlanService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import java.time.LocalDateTime;
@RequiredArgsConstructor
@Controller
public class BaseController {
private final MemberService memberService;
private final WorkPlanService workPlanService;
private final WorkPlanRepository workPlanRepository;
private final CoperationService coperationService;
private final CoperationRepository coperationRepository;
@GetMapping("/")
public String indexDefault(Model model){
LocalDateTime insertDate = LocalDateTime.of(2023,6,1,0,0,0);
LocalDateTime now = LocalDateTime.now();
Coperation coperationA = Coperation.builder()
.coperationName("A 푸드")
.build();
Coperation coperationB = Coperation.builder()
.coperationName("B 푸드")
.build();
WorkPlan workPlan = WorkPlan.builder()
.workPlanTitle("배추 김치 만들기")
.coperation(coperationA)
.workPlanStatus("Y")
.workPlanTag("음식, 요리")
.workPlanStartDate(insertDate)
.crateDate(now)
.updateDate(now)
.build();
WorkPlan workPlan0 = WorkPlan.builder()
.workPlanTitle("제품 운반, 적재하기")
.coperation(coperationA)
.workPlanStatus("Y")
.workPlanTag("음식, 창고")
.workPlanStartDate(insertDate)
.crateDate(now)
.updateDate(now)
.build();
WorkPlan workPlan1 = WorkPlan.builder()
.workPlanTitle("음식 재료 다듬기")
.coperation(coperationB)
.workPlanStatus("Y")
.workPlanTag("음식, 요리")
.workPlanStartDate(insertDate)
.crateDate(now)
.updateDate(now)
.build();
WorkPlan workPlan2 = WorkPlan.builder()
.workPlanTitle("음식 재료 만들기")
.coperation(coperationB)
.workPlanStatus("Y")
.workPlanTag("음식, 요리")
.workPlanStartDate(insertDate)
.crateDate(now)
.updateDate(now)
.build();
coperationRepository.save(coperationA);
coperationRepository.save(coperationB);
workPlanRepository.save(workPlan);
workPlanRepository.save(workPlan0);
workPlanRepository.save(workPlan1);
workPlanRepository.save(workPlan2);
model.addAttribute("coperationList", coperationService.searchFindAllDesc());
model.addAttribute("workPlanList", workPlanService.searchFindAllDesc());
return "index_leaf";
}
@GetMapping("/signup")
public String signupForm(Model model){
model.addAttribute("member", new MemberDto());
return "signupForm";
}
@GetMapping("/test/board")
public String testBoard(){
return "test/board";
}
@PostMapping("/signup")
public String signup( MemberDto memberDto){
memberService.signup(memberDto);
return "redirect:/";
}
@GetMapping("/login")
public String login(){
return "login";
}
}
이렇게 하면, 값이 잘 나올 수 있을거예요.
회사마다 캐치프라이즈를 넣어줌니다~. 다시 엔티티 만들었구요.
package org.example.domain.coperation;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import lombok.experimental.SuperBuilder;
import javax.persistence.*;
import java.time.LocalDateTime;
@Entity
@Getter
@Setter
@RequiredArgsConstructor
@SuperBuilder
@Table(name="T_COPERATION")
public class Coperation {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "T_COPERATION_ID")
private Long id;
private String coperationName; //협력사명
private String catchPrice; //캐치프레이즈 예) 김치를 만들어보세요, 반조리음식. 이 일은 어떤가요.
private LocalDateTime crateDate; // 생성일
}
엔티티를 바꾸면, 관련 도메인의 다른 클래스들도 다 바꿔줘야합니다~.
타임리프. 완성해봤네요. index_leaf.html.
<!doctype html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.5.0/font/bootstrap-icons.css">
<link rel="stylesheet" href="/css/nav.css">
<title>오늘, 일 하시는 것은 어떤가요. 좋은 하루되세요.</title>
<style>
.navbar-brand {
font-size: 1rem;
}
.card {
margin-bottom: 10px;
}
.nav_bottom {
margin-bottom: 40px;
}
</style>
<script th:inline="javascript">
/*<![CDATA[*/
let result = [[${workPlanList}]]
/*]]>*/
</script>
</head>
<body>
<div class="fixed-bottom">
<nav class="navbar navbar-expand-lg nav1">
<div class="container-fluid">
<a class="navbar-brand" href="#">스마트 팩토리</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav"
aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon" style="color:black;margin-top:5px;"><i class="bi bi-justify"></i></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav">
<li class="nav-item">
<a sec:authorize="isAnonymous()" class="nav-link active" aria-current="page" href="#">일 찾아보기</a>
</li>
<li class="nav-item">
<a sec:authorize="isAuthenticated()" class="nav-link" href="#">출근 하기</a>
</li>
<li class="nav-item">
<a sec:authorize="isAuthenticated()" class="nav-link" href="#">마이 페이지</a>
</li>
<li class="nav-item">
<a sec:authorize="isAuthenticated()" class="nav-link" th:href="@{/test/board}">test 게시판</a>
</li>
<li>
<a sec:authorize="isAuthenticated()" class="nav-link">안녕하세요. <span sec:authentication="name"></span>님</a>
</li>
<li>
<a sec:authorize="isAnonymous()" class="nav-link" th:href="@{/login}">로그인</a>
</li>
<li>
<a sec:authorize="isAuthenticated()" class="nav-link" th:href="@{/logout}">로그아웃</a>
</li>
<li>
<a sec:authorize="isAnonymous()" class="nav-link" th:href="@{/signup}">회원가입</a>
</li>
<li class="nav-item" style="display:none" id="email">{{email}}</li>
</ul>
</div>
</div>
</nav>
</div>
<div class="container">
<!-- <h5> A 푸드. 음식. 김치를 만들어보세요.</h5>-->
<br />
<th:block th:each="coList: ${coperationLists}">
<h5><span th:text="${coList.coperationName}"></span>, <span th:text="${coList.catchPrice}"></span></h5>
<br/>
<div class="row justify-content-center" >
<div class="col d-flex justify-content-center" th:each="list : ${workPlanLists}" th:if="${coList.coperationName == list.coperation.coperationName}" >
<div class="card" style="width: 18rem;">
<div class="card-body">
<h5 class="card-title" th:text="${list.coperation.coperationName}"></h5>
<p class="card-text" th:text="${list.workPlanTitle}"></p>
<a href="#" class="btn btn-primary">보기</a>
</div>
<div class="card-footer">
<span th:text="${list.workPlanTag}"></span> | <span th:text="${list.workPlanStartDate}"></span> ~
</div>
</div>
</div>
</div>
<br />
<hr />
<br />
</th:block>
</div>
<br />
<br />
<br />
<br />
<nav class="navbar nav_bottom">
<div class="container-fluid">
<div class="navbar-text" href="#">
<i class="bi bi-emoji-smile"></i>
스마트 팩토리, UI 만들어봤습니다. 2021년 6월 21일 ~ 2021 7월 9일., 2022년 12월 29일 ~ ... <br />한번 만들어보세요~. 일하기가 더 좋아졌으면 좋겠습니다. 좋은 개발되세요~.
감사합니다.<br />작성자: 최현일
|
Github주소 <a href="https://github.com/infott2t/smartFactory-ex">@infott2t</a>
<br/>
</div>
</div>
</nav>
</div>
<!-- Optional JavaScript; choose one of the two! -->
<!-- Option 1: Bootstrap Bundle with Popper -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js"
integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM"
crossorigin="anonymous"></script>
<!-- Option 2: Separate Popper and Bootstrap JS -->
<!--
<script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.9.2/dist/umd/popper.min.js" integrity="sha384-IQsoLXl5PILFhosVNubq5LC7Qb9DXgDA9i+tQ8Zj3iwWAwPtgFTxbJ8NT4GN1R8p" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.min.js" integrity="sha384-cVKIPhGWiC2Al4u+LWgxfKTRIcfu0JTxR+EQDz/bgldoEyl4H0zUF0QKbrJ0EcQF" crossorigin="anonymous"></script>
-->
<script src="/js/jquery-3.6.0.js"></script>
<script>
$(document).ready(function(){
var email = $("#email").text()
setTimeout(function () {
window.ReactNativeWebView.postMessage('{"email" : "'+email+'"}')
}, 2000)
});
</script>
</body>
</html>
완성했군요. 회사별로, 일을 나눠 출력해줌니다~. Coperation 테이블과 WorkPlan 테이블이 서로 맵핑이 되어있어서 가능한 것이겠죠~.

그런데, 현재는 페이지를 새로고침하면, 데이터가 쌓이게 되는데요. 원래 데이터 insert의 경우, 관리자 페이지에서 따로 하는 것이 맞겠죠~. 지금은 테스트로 만들어서 그렇군요.
공부해보세요~.
--
저의 글, 봐 주셔서 감사합니다.
'프로그래밍' 카테고리의 다른 글
[웹 프로그래밍] 스프링부트JPA 5. 회원 기본정보 확장하기. 연락처와 주소. (2) | 2023.01.07 |
---|---|
[웹 프로그래밍] 스프링부트JPA 4. 세션, Role(역할)에 따라, 페이징해주기. - 롤 역할 별 페이지 뷰 만들기. (2) | 2023.01.05 |
[웹 프로그래밍] 스프링부트JPA 3. 스프링 시큐리티와 OAuth2.0. 간편로그인 구현하기. (2) | 2023.01.04 |
[웹 프로그래밍] 스프링부트JPA 1. 데이터를 클래스화 하기. 자동 생성. (2) | 2023.01.01 |
[웹호스팅] 따라해보기 1. 무료 웹호스팅, 클라우드타입. 스프링부트 JPA설치해보자. (0) | 2022.12.30 |