[Java+SpringBoot+JPA] 테이블 조인을 활용한 댓글 기능(1)

2024. 1. 11. 13:24Pratice/CRUD

반응형

 

 

🟢 Constant

훈련기관 enum 열거형으로 설정

public enum StudyRole {

  A("우리인재개발원"),
  B("더조은아카데미"),
  C("그린컴퓨터"),
  D("직업전문학원");

  private String description;

  StudyRole(String description) {
    this.description = description;
  }

  public String getDescription() {
    return description;
  }

}

 

 

🟢 Entity

@Entity
@Getter @Setter
@ToString @Builder
@AllArgsConstructor @NoArgsConstructor
@Table(name = "study")
public class StudyEntity extends BaseEntity{
  
  @Id
  @GeneratedValue(strategy = GenerationType.SEQUENCE)
  @Column(name = "sid", nullable = false)
  private Integer sid;                  //번호

  @Column(name = "subject", length = 50, nullable = false)
  private String subject;          //과목

  @Column(name = "category", nullable = false)
  private Integer category;         //훈련유형

  @Column(name = "agency", length = 20)
  private StudyRole studyRole;           //훈련기관

  @Column(name = "period", nullable = false)
  private String period;           //훈련기간

  @Column(name = "time", nullable = false)
  private String time;            //시간

  @Column(name = "price", nullable = false)
  private Integer price;              //금액

}

 

 

🟢 DTO

  @Size 제약 조건은 주로 컬렉션의 크기(예: 문자열, 리스트)를 검증하는 데 사용


  숫자 유형의 값을 검증하려면 대신 @Min 및 @Max 제약 조건을 사용
  @Size(min = 0, max = 10000000)

@Getter
@Setter
@ToString
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class StudyDTO {

  private Integer sid;                  //번호

  @NotEmpty (message = "과목은 필수입력입니다.")
  private String subject;          //과목

  @NotNull(message = "훈련유형은 필수입력입니다.")
  private Integer category;         //훈련유형

  private StudyRole studyRole;           //훈련기관

  @Pattern(regexp = "\\d{4}-\\d{2}-\\d{2}\\~\\d{4}-\\d{2}-\\d{2}", message = "올바른 날짜 형식은 'yyyy-MM-dd ~ yyyy-MM-dd' 입니다.")
  private String period;           //훈련기간

  @NotEmpty (message = "훈련시간은 필수입력입니다.")
  private String time;            //시간

  @NotNull(message = "금액은 필수입력입니다.")
  @Min(value = 0, message = "금액은 0보다 큰 값이어야 합니다.")
  @Max(value = 10000000, message = "금액은 10000000보다 작거나 같아야 합니다.")
  private Integer price;              //금액

  private LocalDate credate;      //등록날짜

  private LocalDateTime modidate; //수정날짜

}

 

 

🟢 Reppository

@Repository
public interface StudyRepository extends JpaRepository <StudyEntity, Integer> {

  //과목 검색
  @Query("SELECT u FROM StudyEntity u WHERE u.subject like %:keyword%")
  Page<StudyEntity> findBySubject(@Param("keyword") String keyword, Pageable pageable);

  //훈련 기관 검색
  @Query("SELECT u FROM StudyEntity u WHERE u.studyRole = :studyRole")
  Page<StudyEntity> findByStudyRole (@Param("studyRole") StudyRole studyRole, Pageable pageable);

}

 

 

🟢 Service

훈련기관 enum 열거형으로 설정

@Service
@Transactional
@RequiredArgsConstructor
public class StudyService {

  private final StudyRepository studyRepository;
  private final ModelMapper modelMapper = new ModelMapper();

  //삽입
  public StudyEntity insert(StudyDTO studyDTO) throws Exception {

    StudyEntity studyEntity = modelMapper.map(studyDTO, StudyEntity.class);
    studyRepository.save(studyEntity);

    return studyEntity;

  }


  //전체조회 : 검색+페이지
  public Page<StudyDTO> search(String type, String keyword, String studyRole, Pageable page) throws Exception {

    int currentPage = page.getPageNumber()-1;
    int pageLimit = 3;

    Pageable Pageable = PageRequest.of(currentPage, pageLimit, Sort.by(Sort.Direction.DESC, "sid"));

    Page<StudyEntity> studyEntityPage;

    //s: 과목 / a: 훈련기관
    if(type.equals("s") && keyword != null) {
      studyEntityPage = studyRepository.findBySubject(keyword, Pageable);
    } else if (type.equals("a") && keyword != null) {
      studyEntityPage = studyRepository.findByStudyRole(StudyRole.valueOf(studyRole), Pageable);
    } else {
      studyEntityPage= studyRepository.findAll(Pageable);
    }

    List<StudyDTO> studyDTOS = studyEntityPage.stream()
        .map(studyEntity -> modelMapper.map(studyEntity, StudyDTO.class))
        .collect(Collectors.toList());

    return new PageImpl<>(studyDTOS, Pageable, studyEntityPage.getTotalElements());

  }


  //개별조회
  public StudyDTO findOne(int sid) throws Exception {

    Optional<StudyEntity> optionalStudyEntity = studyRepository.findById(sid);

    StudyDTO studyDTO = modelMapper.map(optionalStudyEntity, StudyDTO.class);

    return studyDTO;
  }


  //수정
  public void update(StudyDTO studyDTO) throws Exception {

    Integer sid = studyDTO.getSid();
    Optional<StudyEntity> studyEntity = studyRepository.findById(sid);

    if(studyEntity.isPresent()) {
      StudyEntity update = studyEntity.get();
      modelMapper.map(studyDTO, update);

      studyRepository.save(update);

    }

  }



  //삭제
  public void delete(int sid) throws Exception {
    studyRepository.deleteById(sid);
  }

}

 

 

🟢 Controller

@Controller
@RequiredArgsConstructor
public class StudyController {
  
  private final StudyService studyService;
  
  //목록 + 검색 + 페이지
  @GetMapping("/studylist")
  public String list(@RequestParam(value = "type", defaultValue = "") String type,
                     @RequestParam(value = "keyword", defaultValue = "") String keyword,
                     @RequestParam(value = "studyRole", defaultValue = "") String studyRole,
                     @PageableDefault(page=1) Pageable pageable,
                     Model model) throws Exception {

    Page<StudyDTO> studyDTOS = studyService.search(type, keyword, studyRole, pageable);

    int blockLimit = 3;
    int startPage = (((int)(Math.ceil((double)pageable.getPageNumber()/blockLimit)))-1) * blockLimit+1;
    int endPage = Math.min(startPage+blockLimit-1, studyDTOS.getTotalPages());
    
    int prevPage = studyDTOS.getNumber();
    int currentPage = studyDTOS.getNumber()+1;
    int nextPage = studyDTOS.getNumber()+2;
    int lastPage = studyDTOS.getTotalPages();

    studyDTOS.getTotalElements();    //전체 게시물 수

    model.addAttribute("studyDTOS", studyDTOS);

    model.addAttribute("startPage", startPage);
    model.addAttribute("endPage", endPage);
    model.addAttribute("prevPage", prevPage);
    model.addAttribute("currentPage", currentPage);
    model.addAttribute("nextPage", nextPage);
    model.addAttribute("lastPage", lastPage);

    model.addAttribute("type", type);
    model.addAttribute("keyword", keyword);

    return "/study/list";
  }


  //삽입 폼
  @GetMapping("/studyinsert")
  public String insertForm(Model model) throws Exception {
    StudyDTO studyDTO = new StudyDTO();
    model.addAttribute("studyDTO", studyDTO);
    model.addAttribute("studyRole", StudyRole.values());
    return "/study/insert";
  }
  
  //삽입 처리
  @PostMapping("/studyinsert")
  public String insertProc(@Valid StudyDTO studyDTO, BindingResult bindingResult, Model model) throws Exception {

    if(bindingResult.hasErrors()) {
      model.addAttribute("studyRole", StudyRole.values());
      return "/study/insert";
    } studyService.insert(studyDTO);

    return "redirect:/studylist";
  }



  //상세 폼
  @GetMapping("/studydetail")
  public String detailForm(int sid, Model model) throws Exception {
    StudyDTO studyDTO = studyService.findOne(sid);
    model.addAttribute("studyDTO", studyDTO);
    return "/study/detail";
  }


  //수정 폼
  @GetMapping("/studyupdate")
  public String updateForm(int sid, Model model) throws Exception {
    StudyDTO studyDTO = studyService.findOne(sid);
    model.addAttribute("studyDTO", studyDTO);
    model.addAttribute("studyRole", StudyRole.values());
    return "/study/update";
  }
  
  //수정처리
  @PostMapping("/studyupdate")
  public String updateProc(@Valid StudyDTO studyDTO, BindingResult bindingResult, Model model) throws Exception {

    if(bindingResult.hasErrors()) {
      model.addAttribute("studyRole", StudyRole.values());
      return "/study/update";
    } studyService.update(studyDTO);

    return "redirect:/studylist";
  }


  //삭제 폼
  @GetMapping("/studydelete")
  public String delete(int sid, Model model) throws Exception {
    studyService.delete(sid);
    return "redirect:/studylist";
  }

}

 

 


 

🟢 list.html

훈련기관 enum 열거형으로 설정

<body>
<!-- content 영역 -->
<div layout:fragment="content">
  <div class="row mt-5">
    <div class="col-sm-3"></div>    <!-- 좌측 여백 -->
    <div class="col-sm-6">        <!-- 본문 시작-->
      <h2><span class="pagination justify-content-center mb-5">훈련 과정 소개</span></h2>
      <div class="d-grid gap-2 d-md-flex justify-content-md-end">
        <b>* 총 게시글 수 : <span th:text="${studyDTOS.getTotalElements}"></span> 건</b>
      </div>
      <hr>

      <!-- 반복 영역 -->
      <div th:each="data:${studyDTOS}">
      <div class="container">
        <div class="row g-2 align-items-stretch">
          <div class="col-9" style="cursor: pointer" th:onclick="|location.href='@{/studydetail(sid=${data.sid})}'|"> <!-- 왼쪽 칸 넓히기 -->
            <div class="p-3 border" id="col1"> <!-- 각 칸에 고유한 ID 추가 -->
              <h4><b>[[${data.subject}]]</b></h4>
              <span class="badge bg-primary" th:if="${data.category==1}">내일배움카드</span>
              <span class="badge bg-primary" th:if="${data.category==2}">근로자</span>
              <span class="badge bg-primary" th:if="${data.category==3}">실업자</span>
              <span class="badge bg-primary" th:if="${data.category==4}">재직자</span>
              <br>
              <br>
              <span class="badge rounded bg-dark">훈련기관</span> [[${data.studyRole.description}]]<br>
              <span class="badge rounded bg-dark">훈련기간</span> [[${data.period}]]<br>
              <span class="badge rounded bg-dark">훈련시간</span> [[${data.time}]]<br>
            </div>
          </div>
          <div class="col-3"> <!-- 오른쪽 칸 좁히기 -->
            <div class="p-3 border text-center" id="col2"> <!-- 각 칸에 고유한 ID 추가, 텍스트 가운데 정렬 -->
              <span class="badge bg-danger mb-2">모집중</span> <br>
              <h3>[[${data.price}]]원</h3>
              <button type="button" class="btn btn-primary mb-2">수강신청</button> <br>
              <button type="button" class="btn btn-light">관심등록</button> <br>
<!--              <button type="button" class="btn btn-primary mb-2" th:onclick="|location.href='@{/studydetail(sid=${data.sid})}'|">수강신청</button> <br>-->
<!--              <button type="button" class="btn btn-light">관심등록</button> <br>-->
            </div>
          </div>
        </div>
        <hr>
      </div>
      <script>
          // JavaScript 코드로 두 칸의 높이를 조정
          function adjustHeight() {
              var col1 = document.getElementById("col1");
              var col2 = document.getElementById("col2");
              var maxHeight = Math.max(col1.clientHeight, col2.clientHeight);
              col1.style.height = maxHeight + "px";
              col2.style.height = maxHeight + "px";
          }

          // 페이지 로드 시 높이 조정 함수 호출
          window.onload = adjustHeight;

          // 페이지 크기가 변경될 때도 높이를 다시 조정
          window.addEventListener("resize", adjustHeight);
      </script>
      </div>
      <!-- 반복 영역 끝 -->

	  <!-- 검색 영역 -->
      
      <!-- 페이지 영역 -->

    </div>    <!-- 본문 끝-->
    <div class="col-sm-3"></div>    <!-- 우측 여백 -->
  </div>
</div>
</body>
</html>

 

▪️ 검색 영역

더보기
      <div class="row">   <!-- 검색 영역 시작 -->
        <div class="col-sm-2"></div>
        <div class="col-sm-8">
          <form th:action="@{/studylist}" method="get">
            <input type="hidden" name="page" value="1">
            <div class="input-group mb-3 mt-5">
              <select class="form-select" name="type" id="searchType">
                <option value="" th:selected="${type == ''}">== 선택 ==</option>
                <option value="s" th:selected="${type == 's'}">과목</option>
                <option value="a" th:selected="${type == 'a'}">훈련기관</option>
              </select>

              <!-- 중분류 (검색창) -->
              <input type="text" class="form-control" name="keyword" id="keyword" th:value="${keyword}">

              <!-- 중분류 (카테고리 유형) -->
              <select class="form-select" name="studyRole" id="studyRole">
                <option value="" th:selected="${studyRole == ''}"> == 선택 == </option>
                <option value="A" th:selected="${studyRole == 'A'}"> 우리인재개발원 </option>
                <option value="B" th:selected="${studyRole == 'B'}"> 더조은아카데미 </option>
                <option value="C" th:selected="${studyRole == 'C'}"> 그린컴퓨터 </option>
                <option value="D" th:selected="${studyRole == 'D'}"> 직업전문학원 </option>
              </select>

              <button type="submit" class="btn btn-primary" name="searchButton">검색</button>

              <button type="reset" class="btn btn-primary" name="resetButton" th:onclick="|location.href='@{/studylist}'|">다시</button>

            </div>
          </form>

          <!-- JavaScript 코드 -->
          <script>

              // resetSearchForm : 검색 유형 드롭다운이 변경될 때 호출되며, 선택한 검색 유형에 따라 스타일을 업데이트
              document.getElementById('searchType').addEventListener('change', function () {
                  resetSearchForm();
              });

              function resetSearchForm() {
                  // 검색 폼 값 초기화
                  document.getElementById('keyword').value = '';
                  document.getElementById('studyRole').value = '';

                  // localStorage에서 이전 선택 값을 제거
                  localStorage.removeItem('selectedStudyRole');

                  // applyStylesAfterSearch : 선택한 검색 유형 및 카테고리 유형에 따라 스타일을 적용
                  applyStylesAfterSearch();
              }

              function applyStylesAfterSearch() {
                  var selectedSearchType = document.getElementById('searchType').value;

                  //toggleElementDisplay : 조건에 따라 요소의 표시 여부를 전환
                  //'a'가 선택되면 카테고리 유형을 선택하는 드롭다운 출력
                  toggleElementDisplay('studyRole', selectedSearchType == 'a');

                  //'s'가 선택되면 과목을 입력하는 텍스트 필드 출력
                  toggleElementDisplay('keyword', selectedSearchType == 's');

                  //localStorage : 검색 값 유지
                  if (selectedSearchType == 'a') {
                      //초기화
                      var studyRoleElement = document.getElementById('studyRole');

                      if (studyRoleElement.value == '') {
                          // localStorage에서 이전 선택 값을 가져옴
                          studyRoleElement.value = localStorage.getItem('selectedStudyRole') || '';
                      }
                  }
              }

              //검색창 또는 선택창 출력 (elementId에 해당하는 요소의 표시 여부를 condition에 따라 조절)
              function toggleElementDisplay(elementId, condition) {
                  var element = document.getElementById(elementId);
                  element.style.display = condition ? 'block' : 'none';
              }

              // 페이지 로드 시 초기 스타일 적용
              applyStylesAfterSearch();

              // 페이지 로드 시 localStorage에서 이전 선택 값을 가져와 적용
              window.onload = function () {
                  applyStylesAfterSearch();
              }

              // 페이지 언로드 시 localStorage에 현재 선택 값을 저장
              window.onbeforeunload = function () {
                  localStorage.setItem('selectedStudyRole', document.getElementById('studyRole').value);
              }

          </script>
        </div>
        <div class="col-sm-2"></div>
      </div>    <!-- 검색 영역 끝 -->

▪️ 페이지네이션 영역

더보기
<!-- 페이지 영역 -->
      <div class="d-flex justify-content-center" th:if="${lastPage > 1}">
        <ul class="pagination">
          <li class="page-item" th:unless="${startPage==1}">
            <a class="page-link" th:href="@{/studylist(type=${type}, keyword=${keyword}, page=1)}">처음</a>
          </li>

          <li class="page-item" th:unless="${currentPage==1}">
            <a class="page-link" th:href="@{/studylist(type=${type}, keyword=${keyword}, page=${prevPage})}">이전</a>
          </li>

          <span th:each="page:${#numbers.sequence(startPage, endPage)}">
             <li class="page-item" th:unless="${currentPage==page}"> <!-- 다른 페이지 -->
                 <a class="page-link" th:href="@{/studylist(type=${type}, keyword=${keyword}, page=${page})}">[[${page}]]</a>
             </li>
             <li class="page-item active"  th:if="${currentPage==page}"> <!-- 활성화(현재 위치) -->
                 <a class="page-link" href="#">[[${page}]]</a>
             </li>
         </span>

          <li class="page-item" th:unless="${currentPage==lastPage}">
            <a class="page-link" th:href="@{/studylist(type=${type}, keyword=${keyword}, page=${nextPage})}">다음</a>
          </li>
          <li class="page-item" th:unless="${endPage==lastPage}">
            <a class="page-link" th:href="@{/studylist(type=${type}, keyword=${keyword}, page=${lastPage})}">끝</a>
          </li>
        </ul>
      </div>

      <!-- 페이지 영역 끝 -->

 

🟢 insert.html

<body>
<!-- content 영역 -->
<div layout:fragment="content">
  <div class="row mt-5">
    <div class="col-sm-3"></div>    <!-- 좌측 여백 -->
    <div class="col-sm-6">        <!-- 본문 시작-->
      <form action="/studyinsert" method="post" th:object="${studyDTO}">
        <div class="card">
          <ul class="list-group list-group-flush">
            <li class="list-group-item bg-light"><b>훈련과정 등록</b></li>
            <li class="list-group-item">

              <div class="input-group mb-3">
                <span class="input-group-text" id="subject">과목</span>
                <input type="text" class="form-control" name="subject">
              </div>
              <p class="text-danger" th:if="${#fields.hasErrors('subject')}" th:errors="*{subject}"></p>

              <div class="input-group mb-3">
                <span class="input-group-text">훈련유형</span>
                <select class="form-select" name="category" th:field="*{category}">
                  <option value="" th:selected="${value==''}">== 선택 ==</option>
                  <option value="1" th:selected="${value==1}">내일배움카드</option>
                  <option value="2" th:selected="${value==2}">근로자</option>
                  <option value="3" th:selected="${value==3}">실업자</option>
                  <option value="4" th:selected="${value==4}">재직자</option>
                </select>
              </div>
              <p class="text-danger" th:if="${#fields.hasErrors('category')}" th:errors="*{category}"></p>

              <div class="input-group mb-3">
                <span class="input-group-text" id="agency">훈련기관</span>
                <select class="form-control" id="studyRole" name="studyRole" required>
                  <option th:each="type:${studyRole}"
                          th:value="${type.name()}"
                          th:text="${type.getDescription()}"
                          th:selected="${type.name() eq data?.studyRole?.name()}">
                  </option>
                </select>
              </div>

              <div class="input-group mb-3">
                <span class="input-group-text" id="period">훈련기간</span>
                <input type="text" class="form-control" name="period"  placeholder="yyyy-MM-dd ~ yyyy-MM-dd">
              </div>
              <p class="text-danger" th:if="${#fields.hasErrors('period')}" th:errors="*{period}"></p>

              <div class="input-group mb-3">
                <span class="input-group-text" id="time">훈련시간</span>
                <input type="text" class="form-control" name="time" placeholder="총 00일(총 00시간)">
              </div>
              <p class="text-danger" th:if="${#fields.hasErrors('time')}" th:errors="*{time}"></p>

              <div class="input-group mb-3">
                <span class="input-group-text">금액</span>
                <input type="number" class="form-control" name="price">
              </div>
            </li>
            <li class="list-group-item">
              <button type="submit" class="btn btn-primary">등록</button>
              <button type="reset" class="btn btn-secondary">다시</button>
              <button type="button" class="btn btn-secondary" onclick="location.href='/studylist'">목록</button>
            </li>
          </ul>
        </div>
      </form>
    </div>    <!-- 본문 끝-->
    <div class="col-sm-3"></div>    <!-- 우측 여백 -->
  </div>
</div>
</body>

 

 

🟢 detail.html

<body>
<!-- content 영역 -->
<div layout:fragment="content">
  <div class="row mt-5">
    <div class="col-sm-3"></div>    <!-- 좌측 여백 -->
    <div class="col-sm-6">        <!-- 본문 시작-->
        <div class="card" th:object="${studyDTO}">
          <ul class="list-group list-group-flush">
            <li class="list-group-item bg-light"><b>상세보기</b></li>
            <li class="list-group-item">

              <input type="hidden" name="sid" th:field="*{sid}">           <!-- hidden 영역 -->

              <div class="input-group mt-3 mb-3">
                <span class="input-group-text" id="subject">과목</span>
                <input type="text" class="form-control" name="subject" th:field="*{subject}" readonly>
              </div>

              <div class="input-group mb-3">
                <span class="input-group-text">훈련유형</span>
                <select class="form-select" name="category" th:field="*{category}" disabled>
                  <option value="" th:selected="${value==''}">== 선택 ==</option>
                  <option value="1" th:selected="${value==1}">내일배움카드</option>
                  <option value="2" th:selected="${value==2}">근로자</option>
                  <option value="3" th:selected="${value==3}">실업자</option>
                  <option value="4" th:selected="${value==4}">재직자</option>
                </select>
              </div>

              <div class="input-group mb-3">
                <span class="input-group-text" id="agency">훈련기관</span>
                <input type="text" class="form-control" name="studyRole" th:field="*{studyRole.description}" readonly>
              </div>

              <div class="input-group mb-3">
                <span class="input-group-text" id="period">훈련기간</span>
                <input type="text" class="form-control" name="period" th:field="*{period}" readonly>
              </div>

              <div class="input-group mb-3">
                <span class="input-group-text" id="time">훈련시간</span>
                <input type="text" class="form-control" name="time" th:field="*{time}" readonly>
              </div>

              <div class="input-group mb-3">
                <span class="input-group-text">금액</span>
                <input type="number" class="form-control" th:field="*{price}" name="price" readonly>
              </div>
            </li>
            <li class="list-group-item">
              <button type="button" class="btn btn-secondary" onclick="location.href='/studylist'">목록</button>
            </li>
          </ul>
        </div>
    </div>    <!-- 본문 끝-->
    <div class="col-sm-3"></div>    <!-- 우측 여백 -->
  </div>
</div>
</body>

 

 

반응형