본문 바로가기
스프링과 JPA 기반 웹 어플리케이션 개발/4부 스터디

55. 스터디 설정 - 상태변경

by Backchus 2020. 4. 24.

스터디 상태페이지 요청을 처리할 핸들러 생성

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 기반 웹 애플리케이션 개발 - 인프런

이 강좌에서 여러분은 실제로 운영 중인 서비스를 스프링, JPA 그리고 타임리프를 비롯한 여러 자바 기반의 여러 오픈 소스 기술을 사용하여 웹 애플리케이션을 개발하는 과정을 학습할 수 있습니다. 이 강좌를 충분히 학습한다면 여러분 만의 웹 서비스를 만들거나 취직에 도움이 될만한 포트폴리오를 만들 수 있을 겁니다. 활용 웹 개발 프레임워크 및 라이브러리 Java Spring Spring Boot Spring Data JPA Thymeleaf 온라인 강의 스

www.inflearn.com