Pratice/CRUD
[Java+SpringBoot+JPA] 테이블 조인을 활용한 댓글 기능(1)
달해해
2024. 1. 11. 13:24
반응형
🟢 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>
반응형