Framework/Spring

[Spring] Q&A 페이지 예제

IT수정 2024. 11. 5. 17:46

도메인 생성

Question.java

package login.login_spring.domain;

import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;

import java.time.LocalDateTime;
import java.util.List;

@Entity
@Getter @Setter
public class Question {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "question_id")
    private Long id;

    @Column(length = 200) //제목 최대 길이 200자
    private String subject;

    @Column(columnDefinition = "TEXT")
    //텍스트를 열 데이터로 넣을 수 있음, 여러 줄의 텍스트를 쓸 수 있으며 글자 수 제한을 없애고 싶을 때 사용
    private String content;

    private LocalDateTime createDate;

    private LocalDateTime modifyDate;

    @ManyToOne //Many = author, 주인
    @JoinColumn(name = "member_id") //join할 속성의 이름
    private EzenMember author; //질문 작성자

    @OneToMany(mappedBy = "question", cascade = CascadeType.REMOVE) //질문 삭제 시 답변도 삭제
    private List<Answer> answerList;
}

 

Answer.java

package login.login_spring.domain;

import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;

import java.time.LocalDateTime;

@Entity
@Getter @Setter
public class Answer {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "answer_id")
    private Long id;

    @Column(columnDefinition = "TEXT")
    private String content;

    private LocalDateTime createDate;

    private LocalDateTime modifyDate;

    @ManyToOne
    @JoinColumn(name = "member_id")
    private EzenMember author; //답변 작성자

    @ManyToOne
    @JoinColumn(name = "question_id")
    private Question question;
}

 

EzenMember.java

package login.login_spring.domain;

import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;

import java.util.List;

@Entity
@Getter @Setter
public class EzenMember {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "member_id") //속성 이름 지정
    private Long id;

    private String grade;

    private String loginId;

    private String name;

    private String password;

    @OneToMany(mappedBy = "author", cascade = CascadeType.ALL) //One = questionList, 종
    // mappedBy = "주인"
    private List<Question> questionList;

    @OneToMany(mappedBy = "author", cascade = CascadeType.ALL)
    private List<Answer> answerList;
}

//List 자료형 : EzenMember는 여러 개의 Question과 Answer 객체를 리스트로 관리할 수 있음
//<Question>, <Answer> : 도메인 클래스
//CascadeType.ALL : EzenMember 삭제 시 연관된 객체 자동 삭제

 

생성된 테이블

 

리포지토리 생성

QuestionRepository.java

package login.login_spring.repository;

import login.login_spring.domain.Question;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.List;

public interface QuestionRepository extends JpaRepository<Question, Long> {

    //제목으로 찾기
    Question findBySubject(String subject);

    //제목과 내용으로 찾기
    Question findBySubjectAndContent(String subject, String content);

    //질문엔터티의 subject 열 값들 중에 특정 문자열을 포함하는 데이터를 조회
    List<Question> findBySubjectLike(String subject);

    //findAll 메서드, questionList 페이징 처리
    Page<Question> findAll(Pageable pageable);
}

 

AnswerRepository.java

package login.login_spring.repository;

import login.login_spring.domain.Answer;
import org.springframework.data.jpa.repository.JpaRepository;

public interface AnswerRepository extends JpaRepository<Answer, Long> {

}

 

테스트 실행

QuestionTest.java

package login.login_spring;

import login.login_spring.domain.EzenMember;
import login.login_spring.domain.Question;
import login.login_spring.repository.EzenMemberRepository;
import login.login_spring.repository.QuestionRepository;
import net.bytebuddy.asm.Advice;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import java.time.LocalDateTime;
import java.util.Optional;


@SpringBootTest //SpringBoot 애플리케이션을 테스트할 때 사용
public class QuestionTest {

    @Autowired private QuestionRepository questionRepository;
    //Test는 @RequiredArgsConstructor를 생성 못하기 때문에 @Autowired로 직접 생성해야 함
    //@Autowired 필드, 생성자, 메서드 자동 주입
    @Autowired private EzenMemberRepository ezenMemberRepository;

    @Test //Test 메서드 정의
    void 질문() {
        Question question = new Question();
        question.setSubject("제목입니다");
        question.setContent("내용입니다");

        LocalDateTime now = LocalDateTime.now();
        question.setCreateDate(now);
        //question.setCreateDate(LocalDateTime.mow());

        this.questionRepository.save(question);
    }

    @Test
    void 대량삽입() {
        Optional<EzenMember> member = ezenMemberRepository.findById(1L);

        for(int i = 1; i <= 300; i++) {
            Question question = new Question();
            question.setSubject("제목" + i);
            question.setContent("내용입니다");
            question.setAuthor(member.get());
            question.setCreateDate(LocalDateTime.now());
            this.questionRepository.save(question);
        }

    }
}

 

테스트 결과

 

홈 화면 수정

loginHome.html

<html layout:decorate="~{layout}" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">

<div class="container max600 vh-100 d-flex flex-column justify-content-center align-content-center" layout:fragment="content">
    <div class="text-center">
        <h2>홈 화면</h2>
    </div>
    <div class="py-3 row">
        <p th:text="${loginMember.name} + '님, 환영합니다.'" class="mb-3 col-7 text-end"></p>
        <form th:action="@{/logout}" method="post" class="col-5 text-start">
            <button type="submit" class="btn btn-secondary btn-sm">로그아웃</button>
        </form>
    </div>
    <div class="row">
        <div class="col" th:if="${loginMember.grade == 'admin'}">
            <button type="button" th:onclick="|location.href='@{/members}'|" class="w-100 btn btn-dark btn-lg">회원 목록</button>
        </div>
        <div class="col">
            <button type="button" th:onclick="|location.href='@{/question/create}'|" class="w-100 btn btn-dark btn-lg">질문 등록하기</button>
        </div>
        <div class="col">
            <button type="button" th:onclick="|location.href='@{/question/list}'|" class="w-100 btn btn-dark btn-lg">질문 목록</button>
        </div>
    </div>
</div>
</body>
</html>

 

DTO 생성

QuestionForm.java

package login.login_spring.dto;

import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.Size;
import login.login_spring.domain.Answer;
import login.login_spring.domain.EzenMember;
import lombok.Getter;
import lombok.Setter;

import java.time.LocalDateTime;
import java.util.List;

@Getter @Setter
public class QuestionForm {

    private Long id;

    @NotEmpty(message = "제목을 적어주세요")
    @Size(max=200) //해당 필드 최대 200자까지 작성 가능
    private String subject;

    @NotEmpty(message = "내용을 적어주세요")
    private String content;
}

 

AnswerForm.java

package login.login_spring.dto;

import jakarta.validation.constraints.NotEmpty;
import lombok.Getter;
import lombok.Setter;

@Getter @Setter
public class AnswerForm {

    @NotEmpty(message = "내용을 적어주세요")
    private String content;
}

 

Q&A 페이지 생성

questionForm.html

<html layout:decorate="~{layout}" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">

<div class="container vh-100 d-flex flex-column justify-content-center align-content-center" style="width: 800px;" layout:fragment="content">
    <div class="bg-white border rounded-4 p-5">
        <h1 class="pb-3">질문 등록</h1>
        <form th:action th:object="${questionForm}" method="post">
                <div th:if="${#fields.hasAnyErrors()}" class="alert alert-danger" role="alert">
                    <p th:each="err : ${#fields.allErrors()}" th:text="${err}" class="field-error2">전체 오류 메시지</p>
                </div>
            <div class="mb-3">
                <label th:for="subject" class="form-label">제목</label>
                <input type="text" class="form-control" th:field="*{subject}">
            </div>
            <div class="mb-3 pb-3">
                <label th:for="content" class="form-label">내용</label>
                <textarea class="form-control" th:field="*{content}" style="height: 200px;"></textarea>
            </div>
            <div class="row">
                <div class="col">
                    <button type="submit" class="btn btn-primary btn-sm">저장하기</button>
                </div>
            </div>
        </form>
    </div>
</div>
</body>
</html>

 

questionList.html

<html layout:decorate="~{layout}" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">

<div class="container content" style="width: 1000px;" layout:fragment="content">
    <div class="bg-white border rounded-4 p-5">
        <h1 class="pb-4">Q&A</h1>
        <table class="table text-center">
            <colgroup>
                <col style="width: 10%;">
                <col style="width: 50%;">
                <col style="width: 10%;">
                <col style="width: 30%;">
            </colgroup>
            <thead>
            <tr class="table-primary">
                <th scope="col">번호</th>
                <th scope="col">제목</th>
                <th scope="col">글쓴이</th>
                <th scope="col">작성일시</th>
            </tr>
            </thead>
            <tbody>
            <tr class="align-middle" th:each = "question, iterStat : ${paging}">
                <!-- 전체 게시글 번호를 계산 (내림차순):
                     (전체 게시글 수 - 현재 페이지 번호 * 페이지당 글 개수) - (현재 글의 index) -->
                <th scope="row" th:text="${(paging.totalElements - (paging.number * paging.size) - iterStat.index)}"></th>
                <!-- 순차적으로 번호 표시 -->
                <td>
                    <a th:href="@{/questions/detail/{id} (id=${question.id})}" th:text="${question.subject}" class="questionTitle"></a>
                    <!-- 답글이 1개 이상 달리면 제목에 답글 개수 표시 -->
                    <span th:if="${#lists.size(question.answerList) != 0}" th:text="' [' + ${#lists.size(question.answerList)} + ']'"></span>
                    <!-- 오늘 작성된 글에만 'new' 배지 표시 -->
                    <span th:if="${#temporals.format(question.createDate, 'yyyy-MM-dd') == #temporals.format(#temporals.createNow(), 'yyyy-MM-dd')}" class="badge rounded-pill text-bg-danger">New</span>
                </td>
                <td th:text="${question.author != null ? question.author.name : 'Unknown'}"></td>
                <td th:text="${#temporals.format(question.createDate, 'yyyy-MM-dd HH:mm')}"></td>
            </tr>
            </tbody>
        </table>

        <!-- 페이징 처리 시작 -->
            <nav th:if="${!paging.isEmpty()}">
                <ul class="pagination justify-content-center">
                    <li class="page-item"><a class="page-link" th:classappend="${!paging.hasPrevious}?'disabled'" th:href="@{|?page=${paging.number - 1}|}">Previous</a></li>

                    <li class="page-item" th:each="page : ${#numbers.sequence(0, paging.totalPages - 1)}" th:if="${page >= paging.number / 10 * 10 and page < (paging.number / 10 + 1) * 10}"><a class="page-link" th:href="@{|?page=${page}|}" th:text="${page + 1}" th:classappend="${page == paging.number}?'active'"></a></li>

                    <li class="page-item"><a class="page-link" th:classappend="${!paging.hasNext}?'disabled'" th:href="@{|?page=${paging.number+1}|}">Next</a></li>
                </ul>
            </nav>

        <div>
            <a th:href="@{/question/create}" role="button" class="btn btn-sm btn-secondary">질문 등록하기</a>
        </div>
    </div>
</div>
</body>
</html>

<!-- iterStat : Thymeleaf에서 인덱스와 관련된 정보를 제공 -->
<!-- #temporals.format : Thymeleaf에서 날짜 및 시간을 포맷팅할 때 사용하는 기능 -->

 

Q&A 상세보기 생성

questionDetail.html

<html layout:decorate="~{layout}" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">

<div class="container content" style="width: 800px;" layout:fragment="content">
    <div class="bg-white border rounded-4 p-5" th:object="${question}">
        <h1 class="pb-3 text-center">질문 상세페이지</h1>
            <div class="mb-3">
                <h2 th:text="*{subject}"></h2>
            </div>
            <hr>
            <div class="card mb-3">
                <div class="card-body">
                    <p th:text="*{content}"></p>
                    <div class="d-flex" th:classappend="${loginMember.id == question.author.id ? 'justify-content-between' : 'justify-content-end'}">
                        <!-- 질문 수정, 삭제 버튼 -->
                        <div th:if="${loginMember.id == question.author.id}">
                            <a th:href="@{/questions/{id}/edit (id=${question.id})}" class="btn btn-outline-primary me-2">수정</a>
                            <button class="btn btn-outline-danger me-2" data-bs-toggle="modal" data-bs-target="#questionModal">삭제</button>

                            <!-- 질문 삭제 경고창 -->
                            <div class="modal fade" id="questionModal" tabindex="-1" aria-labelledby="exampleModalLabel" aria-hidden="true">
                                <div class="modal-dialog">
                                    <div class="modal-content">
                                        <div class="modal-header">
                                            <h1 class="modal-title fs-5" id="questionModalLabel">경고</h1>
                                            <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
                                        </div>
                                        <div class="modal-body">
                                            질문을 삭제하시겠습니까?
                                        </div>
                                        <div class="modal-footer">
                                            <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">닫기</button>
                                            <a class="btn btn-danger" th:href="@{/questions/{id}/delete (id=${question.id})}">삭제</a>
                                        </div>
                                    </div>
                                </div>
                            </div>
                        </div>

                        <div>
                            <div th:if="${question.modifyDate != null}" class="badge bg-light text-dark p-2 text-start">
                                <div class="mb-2">
                                    <span th:text="${'수정한 날짜'}"></span>
                                </div>
                                <div th:text="${#temporals.format(question.modifyDate, 'yyyy-MM-dd HH:mm')}"></div>
                            </div>
                            <div class="badge bg-light text-dark p-2 text-start">
                                <div class="mb-2">
                                    <span th:text="${question.author != null ? question.author.name : 'Unknown'}"></span>
                                </div>
                                <div th:text="${#temporals.format(question.createDate, 'yyyy-MM-dd HH:mm')}"></div>
                            </div>
                        </div>
                    </div>
                </div>
            </div>

        <div class="mt-5">
            <!-- 답변 개수 표시 -->
            <h5 th:text="|${#lists.size(question.answerList)}개의 답변이 있습니다.|"></h5>
            <hr>

            <!-- 답변 목록 표시 -->
            <div class="card mb-3" th:each = "answer : ${question.answerList}">
                <!-- 답변 등록, 수정 시 답변 위치로 -->
                <a th:id="|answer_${answer.id}|"></a>
                <div class="card-body">
                    <p th:text="${answer.content}">답변</p>
                    <div class="d-flex" th:classappend="${loginMember.id == answer.author.id ? 'justify-content-between' : 'justify-content-end'}">
                        <!-- 답변 수정, 삭제 버튼 -->
                        <div th:if="${loginMember.id == answer.author.id}">
                            <a th:href="@{/answer/{id}/edit (id=${answer.id})}" class="btn btn-outline-primary me-2">수정</a>
                            <button class="btn btn-outline-danger" data-bs-toggle="modal" data-bs-target="#answerModal">삭제</button>

                            <!-- 답변 삭제 경고창 -->
                            <div class="modal fade" id="answerModal" tabindex="-1" aria-labelledby="exampleModalLabel" aria-hidden="true">
                                <div class="modal-dialog">
                                    <div class="modal-content">
                                        <div class="modal-header">
                                            <h1 class="modal-title fs-5" id="answerModalLabel">경고</h1>
                                            <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
                                        </div>
                                        <div class="modal-body">
                                            답변을 삭제하시겠습니까?
                                        </div>
                                        <div class="modal-footer">
                                            <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">닫기</button>
                                            <a class="btn btn-danger" th:href="@{/answer/{id}/delete (id=${answer.id})}">삭제</a>
                                        </div>
                                    </div>
                                </div>
                            </div>
                        </div>

                        <div>
                            <div th:if="${answer.modifyDate != null}" class="badge bg-light text-dark p-2 text-start">
                                <div class="mb-2">
                                    <span th:text="${'수정한 날짜'}"></span>
                                </div>
                                <div th:text="${#temporals.format(answer.modifyDate, 'yyyy-MM-dd HH:mm')}"></div>
                            </div>
                            <div class="badge bg-light text-dark p-2 text-start">
                                <div class="mb-2">
                                    <span th:text="${answer.author != null ? answer.author.name : 'Unknown'}"></span>
                                </div>
                                <div th:text="${#temporals.format(answer.createDate, 'yyyy-MM-dd HH:mm')}"></div>
                            </div>
                        </div>
                    </div>
                </div>
            </div>

            <!-- 답변 등록 폼 -->
            <form th:action="@{|/answer/create/${question.id}|}" th:object="${answerForm}" method="post">
                <div th:if="${#fields.hasAnyErrors()}" class="alert alert-danger" role="alert">
                    <p th:each="err : ${#fields.allErrors()}" th:text="${err}" class="field-error2">전체 오류 메시지</p>
                </div>
                <div class="mb-2 pb-3">
                    <textarea class="form-control" th:field="*{content}" style="height: 200px;"></textarea>
                </div>
                <div>
                    <button type="submit" class="btn btn-primary">답변 등록</button>
                    <a th:onclick="|location.href='@{/question/list}'|" class="btn btn-secondary">닫기</a>
                </div>
            </form>
        </div>
    </div>
</div>
</body>
</html>

 

Q&A 수정 페이지 생성

updateQuestionForm.html

<html layout:decorate="~{layout}" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">

<div class="container vh-100 d-flex flex-column justify-content-center align-content-center" style="width: 800px;" layout:fragment="content">
    <div class="bg-white border rounded-4 p-5">
        <h1 class="pb-3">질문 수정</h1>
        <form th:action th:object="${questionform}" method="post">
            <div class="mb-3">
                <label th:for="subject" class="form-label">제목</label>
                <input type="text" class="form-control" th:errorClass="field-error" th:field="*{subject}">
                <div th:errors="*{subject}" class="field-error"></div>
            </div>
            <div class="mb-3 pb-3">
                <label th:for="content" class="form-label">내용</label>
                <textarea class="form-control" th:errorClass="field-error" th:field="*{content}" style="height: 200px;"></textarea>
                <div th:errors="*{content}" class="field-error"></div>
            </div>
            <div class="row">
                <div class="col">
                    <button type="submit" class="w-100 btn btn-primary btn-lg">수정</button>
                </div>
                <div class="col">
                    <button type="button" th:onclick="|location.href='@{/questions/detail/{id} (id=${question.id})}'|" class="w-100 btn btn-secondary btn-lg">취소</button>
                </div>
            </div>
        </form>
    </div>
</div>
</body>
</html>

 

updateAnswerForm.html

<html layout:decorate="~{layout}" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">

<div class="container vh-100 d-flex flex-column justify-content-center align-content-center" style="width: 800px;" layout:fragment="content">
    <div class="bg-white border rounded-4 p-5">
        <h1 class="pb-3">답변 수정</h1>
        <form th:action th:object="${answerForm}" method="post">
            <div class="mb-3 pb-3">
                <label th:for="content" class="form-label">내용</label>
                <textarea class="form-control" th:errorClass="field-error" th:field="*{content}" style="height: 200px;"></textarea>
                <div th:errors="*{content}" class="field-error"></div>
            </div>
            <div class="row">
                <div class="col">
                    <button type="submit" class="w-100 btn btn-primary btn-lg">수정</button>
                </div>
                <div class="col">
                    <button type="button" th:onclick="|location.href='@{/questions/detail/{id} (id=${question.id})}'|" class="w-100 btn btn-secondary btn-lg">취소</button>
                </div>
            </div>
        </form>
    </div>
</div>
</body>
</html>

 

Q&A 서비스 생성

QuestionService.java

package login.login_spring.service;

import login.login_spring.domain.Question;
import login.login_spring.repository.QuestionRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

@Service
@RequiredArgsConstructor
public class QuestionService {

    private final QuestionRepository questionRepository;

    //질문 등록
    public void createQuestion(Question question) {
        this.questionRepository.save(question);
    }

    //질문 전체 조회
    public Page<Question> getList(int page) {
        List<Sort.Order> sorts = new ArrayList<>();
        sorts.add(Sort.Order.desc("createDate")); //날짜별로 내림차순 정렬
        PageRequest pageable = PageRequest.of(page, 10, Sort.by(sorts));//한 페이지에 10개씩 보여줌
        return questionRepository.findAll(pageable);
    }

    //질문 하나 조회
    public Optional<Question> findOneQuestion(Long id) {
        return questionRepository.findById(id);
    }

    //질문 수정
    public Long updateQuestion(Question question) {
        this.questionRepository.save(question);
        return question.getId();
    }

    //질문 삭제
    public void deleteQuestion(Question question) {
        this.questionRepository.deleteById(question.getId());
    }
}

 

AnswerService.java

package login.login_spring.service;

import login.login_spring.domain.Answer;
import login.login_spring.repository.AnswerRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

import java.util.Optional;

@Service
@RequiredArgsConstructor
public class AnswerService {

    private final AnswerRepository answerRepository;

    //답변 등록
    public void createAnswer(Answer answer){
        this.answerRepository.save(answer);
    }

    //답변 하나 조회
    public Optional<Answer> findOneAnswer(Long id) {
        return answerRepository.findById(id);
    }

    //답변 수정
    public Long updateAnswer(Answer answer) {
        this.answerRepository.save(answer);
        return answer.getId();
    }

    //답변 삭제
    public void deleteAnswer(Answer answer) {
        this.answerRepository.deleteById(answer.getId());
    }

}

 

Q&A 컨트롤러 생성

QuestionController.java

package login.login_spring.controller;

import jakarta.validation.Valid;
import login.login_spring.domain.EzenMember;
import login.login_spring.domain.Question;
import login.login_spring.dto.AnswerForm;
import login.login_spring.dto.QuestionForm;
import login.login_spring.service.QuestionService;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;

import java.time.LocalDateTime;
import java.util.Optional;

@Controller
@RequiredArgsConstructor
public class QuestionController {

    private final QuestionService questionService;

    //질문 등록 폼 열기
    @GetMapping("/question/create")
    public String questionForm(@SessionAttribute(name = SessionConst.LOGIN_MEMBER) EzenMember loginMember, Model model) {

        model.addAttribute("questionForm", new QuestionForm());
        model.addAttribute("loginMember", loginMember);

        return "user/questionForm";
    }

    //질문 등록
    @PostMapping("/question/create")
    public String createQuestion(@Valid @ModelAttribute("questionForm") QuestionForm form, BindingResult bindingResult, @SessionAttribute(name = SessionConst.LOGIN_MEMBER) EzenMember loginMember, Model model) {

        if(bindingResult.hasErrors()){
            model.addAttribute("loginMember", loginMember);
            return "user/questionForm";
        }

        Question question = new Question();
        question.setSubject(form.getSubject());
        question.setContent(form.getContent());
        question.setCreateDate(LocalDateTime.now());
        question.setAuthor(loginMember);
        questionService.createQuestion(question);

        model.addAttribute("loginMember", loginMember);
        return "redirect:/question/list";
    }

    //질문 목록 보기
    @GetMapping("/question/list")
    public String list(@SessionAttribute(name = SessionConst.LOGIN_MEMBER) EzenMember loginMember, @RequestParam(value = "page", defaultValue = "0") int page, Model model){
        Page<Question> paging = questionService.getList(page);
        model.addAttribute("paging", paging);
        model.addAttribute("loginMember", loginMember);
        return "user/questionList";
    }

    //질문 상세 보기
    @GetMapping("/questions/detail/{id}")
    public String detail(@SessionAttribute(name = SessionConst.LOGIN_MEMBER) EzenMember loginMember, @PathVariable("id") Long id, AnswerForm answerForm, Model model){

        Optional<Question> question = questionService.findOneQuestion(id);
        if(question.isPresent()){

            Question questionGet = question.get();

            model.addAttribute("question", questionGet);
            model.addAttribute("loginMember", loginMember);
            model.addAttribute("answerForm", answerForm); //answerForm 가져옴
            return "user/questionDetail";
        }
        model.addAttribute("loginMember", loginMember);
        return "user/questionList";
    }

    //질문 수정을 위해 작성된 질문 가져오기
    @GetMapping("/questions/{id}/edit")
    public String editQuestion(@SessionAttribute(name = SessionConst.LOGIN_MEMBER) EzenMember loginMember, QuestionForm form, @PathVariable("id") Long id, Model model) {
        Optional<Question> findQuestion = questionService.findOneQuestion(id);
        Question findQuestionGet = findQuestion.get();

        form.setSubject(findQuestionGet.getSubject());
        form.setContent(findQuestionGet.getContent());

        model.addAttribute("questionform", form);
        model.addAttribute("question", findQuestionGet);
        model.addAttribute("loginMember", loginMember);
        return "user/updateQuestionForm";
    }

    //질문 수정
    @PostMapping("/questions/{id}/edit")
    public String updateQuestion(@PathVariable("id") Long id, @SessionAttribute(name = SessionConst.LOGIN_MEMBER) EzenMember loginMember, @Valid @ModelAttribute("questionform") QuestionForm form, BindingResult bindingResult, Model model){

        if(bindingResult.hasErrors()){
            model.addAttribute("loginMember", loginMember);
            return "user/updateQuestionForm";
        }

        Optional<Question> findQuestion = questionService.findOneQuestion(id);

        Question question = new Question();
        question.setId(id);
        question.setAuthor(loginMember);
        question.setSubject(form.getSubject());
        question.setContent(form.getContent());

        question.setCreateDate(findQuestion.get().getCreateDate());
        question.setModifyDate(LocalDateTime.now());

        questionService.updateQuestion(question);
        model.addAttribute("loginMember", loginMember);

        return String.format("redirect:/questions/detail/%s", id);
    }

    //질문 삭제
    @GetMapping("/questions/{id}/delete")
    public String deleteQuestion(@PathVariable("id") Long id){
        Optional<Question> findQuestion = questionService.findOneQuestion(id);
        if(findQuestion.isPresent()){
            questionService.deleteQuestion(findQuestion.get());
        }
        return "redirect:/question/list";
    }
}

 

AnwserController.java

package login.login_spring.controller;

import jakarta.validation.Valid;
import login.login_spring.domain.Answer;
import login.login_spring.domain.EzenMember;
import login.login_spring.domain.Question;
import login.login_spring.dto.AnswerForm;
import login.login_spring.service.AnswerService;
import login.login_spring.service.QuestionService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;

import java.time.LocalDateTime;
import java.util.Optional;

@Controller
@RequiredArgsConstructor
public class AnswerController {

    private final AnswerService answerService;
    private final QuestionService questionService;

    //질문에 대한 답변 쓰기
    @PostMapping("/answer/create/{questionId}")
    public String createAnswer(@PathVariable("questionId") Long questionId, @Valid AnswerForm answerForm, BindingResult bindingResult, @SessionAttribute(name = SessionConst.LOGIN_MEMBER) EzenMember loginMember, Model model){

        Optional<Question> question = questionService.findOneQuestion(questionId);
        if(bindingResult.hasErrors()){
            model.addAttribute("question", question.get());
            model.addAttribute("loginMember", loginMember);
            return "user/questionDetail";
        }
        Answer answer = new Answer();

        if(question.isPresent()){
            answer.setContent(answerForm.getContent());
            answer.setCreateDate(LocalDateTime.now());
            answer.setQuestion(question.get());
            answer.setAuthor(loginMember);

            answerService.createAnswer(answer);
        }
        model.addAttribute("loginMember", loginMember);

        return String.format("redirect:/questions/detail/%s#answer_%s", questionId, answer.getId());
    }

    //답변 수정을 위해 답변 가져오기
    @GetMapping("/answer/{id}/edit")
    public String editAnswer(@PathVariable("id") Long id, @SessionAttribute(name = SessionConst.LOGIN_MEMBER) EzenMember loginMember, AnswerForm answerForm, Model model){

        Optional<Answer> findAnswer = answerService.findOneAnswer(id);

        if(findAnswer.isPresent()){
            Answer answer = findAnswer.get();
            answerForm.setContent(answer.getContent());

            model.addAttribute("loginMember", loginMember);
            model.addAttribute("answerForm", answerForm);
            model.addAttribute("question", answer.getQuestion());
            return "user/updateAnswerForm";
        }
        return "redirect:/questions/list";
    }

    //답변 수정
    @PostMapping("/answer/{id}/edit")
    public String updateAnswer(@PathVariable("id") Long id, @Valid AnswerForm answerForm, BindingResult bindingResult, @SessionAttribute(name = SessionConst.LOGIN_MEMBER) EzenMember loginMember, Model model){

        if(bindingResult.hasErrors()) {
            model.addAttribute("loginMember", loginMember);
            return "user/updateAnswerForm";
        }

        //답글 작성자의 아이디 조회
        Optional<Answer> findAnswer = answerService.findOneAnswer(id);
        Answer answer = findAnswer.get();

        //답글에 해당하는 질문 작성자 아이디 조회
        Long questionId = answer.getQuestion().getId();

        answer.setContent(answerForm.getContent());
        answer.setCreateDate(answer.getCreateDate());
        answer.setModifyDate(LocalDateTime.now());
        answerService.updateAnswer(answer);

        model.addAttribute("loginMember", loginMember);
        return String.format("redirect:/questions/detail/%s#answer_%s", questionId, answer.getId());
    }


    //답변 삭제
    @GetMapping("/answer/{id}/delete")
    public String deleteAnswer(@PathVariable("id") Long id) {
        Optional<Answer> findAnswer = answerService.findOneAnswer(id);

        if(findAnswer.isPresent()){
            Answer answer = findAnswer.get();
            Long questionId = answer.getQuestion().getId();
            answerService.deleteAnswer(answer);
            return String.format("redirect:/questions/detail/%s", questionId);
        }
        return "redirect:/question/list";
    }
}

 

경로

 

'Framework > Spring' 카테고리의 다른 글

[Spring] 검색 기능 예제  (0) 2024.11.11
[Spring] Thymeleaf - Paging  (0) 2024.11.06
[Spring] 회원 가입과 로그인 예제  (1) 2024.11.04
[Spring] HTML 구성 요소의 통합  (2) 2024.10.31
[Spring] 회원 관리 예제  (1) 2024.10.31