스터디 상태페이지 요청을 처리할 핸들러 생성
package me.weekbelt.studyolle.study;
@RequiredArgsConstructor
@RequestMapping("/study/{path}/settings")
@Controller
public class StudySettingsController {
// ........
@GetMapping("/study")
public String studySettingForm(@CurrentAccount Account account, @PathVariable String path,
Model model) {
Study study = studyService.getStudyToUpdateStatus(account, path);
model.addAttribute(account);
model.addAttribute(study);
return "study/settings/study";
}
}
StudyService에서 getStudyToUpdateStatus작성
package me.weekbelt.studyolle.study;
@RequiredArgsConstructor
@Transactional
@Service
public class StudyService {
// .........
public Study getStudyToUpdateStatus(Account account, String path) {
Study study = studyRepository.findStudyWithManagersByPath(path);
checkIfExistingStudy(path, study);
checkIfManager(account, study);
return study;
}
// .........
}
StudyRepository에서 findStudyWithManagersByPath메소드 작성
package me.weekbelt.studyolle.study;
@Transactional(readOnly = true)
public interface StudyRepository extends JpaRepository<Study, Long> {
// .....
@EntityGraph(value = "Study.withManagers", type = EntityGraph.EntityGraphType.FETCH)
Study findStudyWithManagersByPath(String path);
}
Study엔티티에 NamedQuery 설정
package me.weekbelt.studyolle.domain;
// .......
@NamedEntityGraph(name = "Study.withManagers", attributeNodes = {
@NamedAttributeNode("managers")
})
@Builder @AllArgsConstructor @NoArgsConstructor
@Getter @Setter @EqualsAndHashCode(of = "id")
@Entity
public class Study {
// .......
}
스터디 상태페이지 작성(study/settings/study.html)
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head th:replace="fragments.html :: head"></head>
<body>
<nav th:replace="fragments.html :: main-nav"></nav>
<div th:replace="fragments.html :: study-banner"></div>
<div class="container">
<div th:replace="fragments.html :: study-info"></div>
<div th:replace="fragments.html :: study-menu(studyMenu='settings')"></div>
<div class="row mt-3 justify-content-center">
<div class="col-2">
<div th:replace="fragments.html :: study-settings-menu(currentMenu='study')"></div>
</div>
<div class="col-8">
<div th:replace="fragments.html :: message"></div>
<div class="row">
<h5 class="col-sm-12">스터디 공개 및 종료</h5>
<form th:if="${!study.published && !study.closed}" class="col-sm-12" action="#" th:action="@{'/study/' + ${study.getPath()} + '/settings/study/publish'}" method="post" novalidate>
<div class="alert alert-info" role="alert">
스터디를 다른 사용자에게 공개할 준비가 되었다면 버튼을 클릭하세요.<br/>
소개, 배너 이미지, 스터디 주제 및 활동 지역을 등록했는지 확인하세요.<br/>
스터디를 공개하면 주요 활동 지역과 스터디 주제에 관심있는 다른 사용자에게 알림을 전송합니다.
</div>
<div class="form-group">
<button class="btn btn-outline-primary" type="submit" aria-describedby="submitHelp">스터디 공개</button>
</div>
</form>
<form th:if="${study.published && !study.closed}" class="col-sm-12" action="#" th:action="@{'/study/' + ${study.getPath()} + '/settings/study/close'}" method="post" novalidate>
<div class="alert alert-warning" role="alert">
스터디 활동을 마쳤다면 스터디를 종료하세요.<br/>
스터디를 종료하면 더이상 팀원을 모집하거나 모임을 만들 수 없으며, 스터디 경로와 이름을 수정할 수 없습니다.<br/>
스터디 모임과 참여한 팀원의 기록은 그대로 보관합니다.
</div>
<div class="form-group">
<button class="btn btn-outline-warning" type="submit" aria-describedby="submitHelp">스터디 종료</button>
</div>
</form>
<div th:if="${study.closed}" class="col-sm-12 alert alert-info">
이 스터디는 <span class="date-time" th:text="${study.closedDateTime}"></span>에 종료됐습니다.<br/>
다시 스터디를 진행하고 싶다면 새로운 스터디를 만드세요.<br/>
</div>
</div>
<hr th:if="${!study.closed && !study.recruiting && study.published}"/>
<div class="row" th:if="${!study.closed && !study.recruiting && study.published}">
<h5 class="col-sm-12">팀원 모집</h5>
<form class="col-sm-12" action="#" th:action="@{'/study/' + ${study.getPath()} + '/settings/recruit/start'}" method="post" novalidate>
<div class="alert alert-info" role="alert">
팀원을 모집합니다.<br/>
충분한 스터디 팀원을 모집했다면 모집을 멈출 수 있습니다.<br/>
팀원 모집 정보는 1시간에 한번만 바꿀 수 있습니다.
</div>
<div class="form-group">
<button class="btn btn-outline-primary" type="submit" aria-describedby="submitHelp">팀원 모집 시작</button>
</div>
</form>
</div>
<hr th:if="${!study.closed && study.recruiting && study.published}"/>
<div class="row" th:if="${!study.closed && study.recruiting && study.published}">
<h5 class="col-sm-12">팀원 모집</h5>
<form class="col-sm-12" action="#" th:action="@{'/study/' + ${study.getPath()} + '/settings/recruit/stop'}" method="post" novalidate>
<div class="alert alert-primary" role="alert">
팀원 모집을 중답합니다.<br/>
팀원 충원이 필요할 때 다시 팀원 모집을 시작할 수 있습니다.<br/>
팀원 모집 정보는 1시간에 한번만 바꿀 수 있습니다.
</div>
<div class="form-group">
<button class="btn btn-outline-primary" type="submit" aria-describedby="submitHelp">팀원 모집 중단</button>
</div>
</form>
</div>
<hr th:if="${!study.closed}"/>
<div class="row" th:if="${!study.closed}">
<h5 class="col-sm-12">스터디 경로</h5>
<form class="col-sm-12 needs-validation" th:action="@{'/study/' + ${study.path} + '/settings/study/path'}"
method="post" novalidate>
<div class="alert alert-warning" role="alert">
스터디 경로를 수정하면 이전에 사용하던 경로로 스터디에 접근할 수 없으니 주의하세요. <br/>
</div>
<div class="form-group">
<input id="path" type="text" name="newPath" th:value="${study.path}" class="form-control"
placeholder="예) study-path" aria-describedby="pathHelp" required>
<small id="pathHelp" class="form-text text-muted">
공백없이 문자, 숫자, 대시(-)와 언더바(_)만 3자 이상 20자 이내로 입력하세요. 스터디 홈 주소에 사용합니다. 예) /study/<b>study-path</b>
</small>
<small class="invalid-feedback">스터디 경로를 입력하세요.</small>
<small class="form-text text-danger" th:if="${studyPathError}" th:text="${studyPathError}">Path Error</small>
</div>
<div class="form-group">
<button class="btn btn-outline-warning" type="submit" aria-describedby="submitHelp">경로 수정</button>
</div>
</form>
</div>
<hr th:if="${!study.closed}"/>
<div class="row" th:if="${!study.closed}">
<h5 class="col-12">스터디 이름</h5>
<form class="needs-validation col-12" action="#" th:action="@{'/study/' + ${study.path} + '/settings/study/title'}" method="post" novalidate>
<div class="alert alert-warning" role="alert">
스터디 이름을 수정합니다.<br/>
</div>
<div class="form-group">
<label for="title">스터디 이름</label>
<input id="title" type="text" name="newTitle" th:value="${study.title}" class="form-control"
placeholder="스터디 이름" aria-describedby="titleHelp" required maxlength="50">
<small id="titleHelp" class="form-text text-muted">
스터디 이름을 50자 이내로 입력하세요.
</small>
<small class="invalid-feedback">스터디 이름을 입력하세요.</small>
<small class="form-text text-danger" th:if="${studyTitleError}" th:text="${studyTitleError}">Title Error</small>
</div>
<div class="form-group">
<button class="btn btn-outline-warning" type="submit" aria-describedby="submitHelp">스터디 이름 수정</button>
</div>
</form>
</div>
<hr/>
<div class="row" th:if="${study.isRemovable()}">
<h5 class="col-sm-12 text-danger">스터디 삭제</h5>
<form class="col-sm-12" action="#" th:action="@{'/study/' + ${study.getPath()} + '/settings/study/remove'}" method="post" novalidate>
<div class="alert alert-danger" role="alert">
스터디를 삭제하면 스터디 관련 모든 기록을 삭제하며 복구할 수 없습니다. <br/>
<b>다음에 해당하는 스터디는 자동으로 삭제 됩니다.</b>
<ul>
<li>만든지 1주일이 지난 비공개 스터디</li>
<li>스터디 공개 이후, 한달 동안 모임을 만들지 않은 스터디</li>
<li>스터디 공개 이후, 모임을 만들지 않고 종료한 스터디</li>
</ul>
</div>
<div class="form-group">
<button class="btn btn-outline-danger" type="submit" aria-describedby="submitHelp">스터디 삭제</button>
</div>
</form>
</div>
<div class="row" th:if="${!study.isRemovable()}">
<h5 class="col-sm-12 text-danger">스터디 삭제</h5>
<form class="col-sm-12" action="#" th:action="@{'/study/' + ${study.getPath()} + '/settings/study/remove'}" method="post" novalidate>
<div class="alert alert-danger" role="alert">
공개 중이고 모임을 했던 스터디는 삭제할 수 없습니다.
</div>
<div class="form-group">
<button class="btn btn-outline-danger" type="submit" aria-describedby="submitHelp" disabled>스터디 삭제</button>
</div>
</form>
</div>
</div>
</div>
<div th:replace="fragments.html :: footer"></div>
</div>
<script th:replace="fragments.html :: tooltip"></script>
<script th:replace="fragments.html :: form-validation"></script>
</body>
</html>
스터디 공개/종료 를 처리할 핸들러 작성
package me.weekbelt.studyolle.study;
@RequiredArgsConstructor
@RequestMapping("/study/{path}/settings")
@Controller
public class StudySettingsController {
// .........
@PostMapping("/study/publish")
public String publishStudy(@CurrentAccount Account account, @PathVariable String path,
RedirectAttributes attributes) {
Study study = studyService.getStudyToUpdateStatus(account, path);
studyService.publish(study);
attributes.addFlashAttribute("message", "스터디를 공개했습니다.");
return "redirect:/study/" + getPath(path) + "/settings/study";
}
@PostMapping("/study/close")
public String closeStudy(@CurrentAccount Account account, @PathVariable String path,
RedirectAttributes attributes) {
Study study = studyService.getStudyToUpdateStatus(account, path);
studyService.close(study);
attributes.addFlashAttribute("message", "스터디를 종료했습니다.");
return "redirect:/study/" + getPath(path) + "/settings/study";
}
}
StudyService에서 공개/처리를 위한 메소드 구현
package me.weekbelt.studyolle.study;
@RequiredArgsConstructor
@Transactional
@Service
public class StudyService {
// ........
public void publish(Study study) {
study.publish();
}
public void close(Study study) {
study.close();
}
}
도메인에 위임한 pulish와 close메소드를 Account엔티티에 구현
package me.weekbelt.studyolle.domain;
// .......
@Builder @AllArgsConstructor @NoArgsConstructor
@Getter @Setter @EqualsAndHashCode(of = "id")
@Entity
public class Study {
// .............
public void publish() {
if (this.closed || this.published) {
throw new RuntimeException("스터디를 공개할 수 없는 상태입니다. 스터디를 이미 공개했거나 종료했습니다.");
} else {
this.published = true;
this.publishedDateTime = LocalDateTime.now();
}
}
public void close() {
if (!this.published || this.closed) {
throw new RuntimeException("스터디를 종료할 수 없습니다. 스터디를 공개하지 않았거나 이미 종료한 스터디입니다.");
} else {
this.closed = true;
this.closedDateTime = LocalDateTime.now();
}
}
}
팀원 모집 시작/종료 요청을 처리하기 위한 핸들러 작성
package me.weekbelt.studyolle.study;
@RequiredArgsConstructor
@RequestMapping("/study/{path}/settings")
@Controller
public class StudySettingsController {
// .........
@PostMapping("/recruit/start")
public String startRecruit(@CurrentAccount Account account, @PathVariable String path,
Model model, RedirectAttributes attributes) {
Study study = studyService.getStudyToUpdateStatus(account, path);
if(!study.canUpdateRecruiting()){
attributes.addFlashAttribute("message", "1시간 안에 인원 모집 설정을 여러번 변경할 수 없습니다.");
return "redirect:/study/" + getPath(path) + "/settings/study";
}
studyService.startRecruit(study);
attributes.addFlashAttribute("message", "인원 모집을 시작합니다.");
return "redirect:/study/" + getPath(path) + " /settings/study";
}
@PostMapping("/recruit/stop")
public String stopRecruit(@CurrentAccount Account account, @PathVariable String path,
Model model, RedirectAttributes attributes) {
Study study = studyService.getStudyToUpdateStatus(account, path);
if(!study.canUpdateRecruiting()){
attributes.addFlashAttribute("message", "1시간 안에 인원 모집 설정을 여러번 변경할 수 없습니다.");
return "redirect:/study/" + getPath(path) + "/settings/study";
}
studyService.stopRecruit(study);
attributes.addFlashAttribute("message", "인원 모집을 종료합니다.");
return "redirect:/study/" + getPath(path) + " /settings/study";
}
}
스터디의 모집상태를 업데이트할 수 있는지 확인하기위해 Study엔티티에 canUpdateRecruting메소드 생성
package me.weekbelt.studyolle.domain;
// ......
@Builder @AllArgsConstructor @NoArgsConstructor
@Getter @Setter @EqualsAndHashCode(of = "id")
@Entity
public class Study {
// ........
public boolean canUpdateRecruiting() {
return this.published && this.recruitingUpdatedDateTime == null ||
this.recruitingUpdatedDateTime.isBefore(LocalDateTime.now().minusHours(1));
}
}
스터디 팀원 모집 시작/종료 처리를 위한 메소드 작성
package me.weekbelt.studyolle.study;
@RequiredArgsConstructor
@Transactional
@Service
public class StudyService {
// ........
public void startRecruit(Study study) {
study.startRecruit();
}
public void stopRecruit(Study study) {
study.stopRecruit();
}
}
Study엔티티에 팀원 모집 시작/종료 처리를 위임하기위해 메소드 작성
package me.weekbelt.studyolle.domain;
// ........
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
@EqualsAndHashCode(of = "id")
@Entity
public class Study {
// ........
public void startRecruit() {
if (canUpdateRecruiting()) {
this.recruiting = true;
this.recruitingUpdatedDateTime = LocalDateTime.now();
} else {
throw new RuntimeException("인원 모집을 시작할 수 없습니다. 스터디를 공개하거나 한 시간 뒤 다시 시도하세요.");
}
}
public void stopRecruit() {
if (canUpdateRecruiting()) {
this.recruiting = false;
this.recruitingUpdatedDateTime = LocalDateTime.now();
} else {
throw new RuntimeException("인원 모집을 멈출 수 없습니다. 스터디를 공개하거나 한 시간 뒤 다시 시도하세요.");
}
}
}
참고 :https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-JPA-%EC%9B%B9%EC%95%B1#
'스프링과 JPA 기반 웹 어플리케이션 개발 > 4부 스터디' 카테고리의 다른 글
57. 스터디 설정 - 삭제 (0) | 2020.04.28 |
---|---|
56. 스터디 설정 - 경로 및 이름 수정 (0) | 2020.04.28 |
54. 스터디 설정 - 태그/지역 (0) | 2020.04.24 |
53. 스터디 설정 - 배너 (0) | 2020.04.24 |
52. 스터디 설정 - 소개 수정 (0) | 2020.04.24 |