프로그래밍

[웹 프로그래밍] 스프링부트JPA 2. 엔티티 심화. 맵핑하고, 타임리프 each문 작성해서 리스트 출력하기.

tt2t2am1118 2023. 1. 2. 01:51
반응형

안녕하세요. 이어서 적어보겠습니다.



현재 이 글을 쓰면서, 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의 경우, 관리자 페이지에서 따로 하는 것이 맞겠죠~. 지금은 테스트로 만들어서 그렇군요.


공부해보세요~.

--
저의 글, 봐 주셔서 감사합니다.

반응형