SpringBoot 프로젝트

Spring Boot - Q&A 페이지 구현하기 (1)

orin602 2024. 11. 23. 17:07

이번 글에서는 웹 사이트에서 빠질 수 없는 QnA 기능에 대해 다뤄보겠습니다. 일반 회원이 질문을 작성하고, 관리자가 해당 질문에 답변하는 기능을 구현할 예정입니다. 부족한 부분이 있을 수 있지만, 많은 관심과 피드백 부탁드립니다! :)

 

1. Qna 클래스 만들기 (Qna.java)

package com.demo.domain;

import java.util.Date;

import org.hibernate.annotations.ColumnDefault;
import org.hibernate.annotations.DynamicInsert;
import org.hibernate.annotations.DynamicUpdate;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.SequenceGenerator;
import jakarta.persistence.Temporal;
import jakarta.persistence.TemporalType;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@DynamicInsert
@DynamicUpdate
@Builder
@ToString
@Entity
public class Qna {

    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator= "qnaseq")
    @SequenceGenerator(name = "qnaseq", sequenceName= "qnaseq", allocationSize = 1)
    private int qna_seq;

    private String title; // 질문 제목
    private String content; // 질문 내용
    
    @Temporal(value=TemporalType.TIMESTAMP)
    @ColumnDefault("sysdate")
    private Date qna_date; // 질문 작성 날짜

    private String answer; // 답변
    
    @Temporal(value=TemporalType.TIMESTAMP)
    private Date answer_date; // 답변 작성 날짜
    
    // 답변 상태 : 0 = '답변 대기', 1 = '답변 완료'
    @ColumnDefault("0")
    private int answer_status;

    @ManyToOne
    @JoinColumn(name="id")
    private Member member;

}

 

  • @Id : 해당 필드가 엔티티의 기본 키임을 나타냅니다. 즉, 데이터베이스에서 각 QnA 레코드를 구별할 수 있는 고유한 값입니다.
  • @GeneratedValue(strategy = GenerationType.SEQUENCE, generator= "qnaseq") : 기본 키 값을 자동으로 생성하는 전략을 설정합니다. GenerationType.SEQUENCE는 데이터베이스의 시퀀스를 사용하여 기본 키 값을 생성하는 방식입니다. generator= "qnaseq"는 생성자 이름을 지정하는 부분으로, 시퀀스 생성기를 사용해 기본 키 값을 생성합니다.
  • @SequenceGenerator(name = "qnaseq", sequenceName= "qnaseq", allocationSize = 1) : @SequenceGenerator는 qnaseq라는 이름의 시퀀스를 사용하여 기본 키 값을 생성하는 방법을 정의합니다. sequenceName은 데이터베이스에서 실제로 존재하는 시퀀스 이름을 지정하고, allocationSize는 기본 키를 몇 개씩 미리 할당할지 설정하는 값입니다. 여기서는 1씩 할당됩니다.
  • @Temporal(value=TemporalType.TIMESTAMP) : 날짜 및 시간 관련 데이터를 저장할 때 사용되는 어노테이션입니다. TIMESTAMP로 설정되었으므로, 날짜와 시간을 모두 포함한 값을 저장합니다.
  • @ColumnDefault("sysdate") : 이 어노테이션은 데이터베이스 컬럼의 기본값을 설정하는 데 사용됩니다. 여기서 sysdate는 데이터베이스에서 현재 날짜와 시간을 자동으로 삽입하도록 지정하는 SQL 함수입니다. 즉, QnA를 생성할 때 answer_date가 설정되지 않으면, 자동으로 현재 날짜와 시간이 삽입됩니다.
  • @ColumnDefault("0") : private int answer_status; 필드에 대해 기본값을 설정합니다. answer_status 필드는 답변 상태를 나타내며, 기본값을 0으로 설정합니다.(// 답변 상태 : 0 = '답변 대기', 1 = '답변 완료')
  • @ManyToOne : 이 어노테이션은 QnA가 하나의 Member와 다대일 관계를 가짐을 나타냅니다. 즉, 하나의 QnA는 하나의 Member(질문을 작성한 사용자)에 속하게 됩니다. @JoinColumn(name="id") : 이 어노테이션은 QnA 엔티티와 Member 엔티티를 연결할 외래 키 컬럼을 설정합니다. 여기서 name="id"는 외래 키 컬럼이 id라는 이름을 가짐을 의미합니다.

2. Repository 생성 (QnaRepository.java)

package com.demo.persistence;

import java.util.List;

import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

import com.demo.domain.Qna;

public interface QnaRepository extends JpaRepository<Qna, Integer> {

	// JpaRepository는 기본적인 CRUD 작업을 수행하는 메서드를 이미 구현해두었기 때문에, 개발자가 따로 구현하지 않아도 됩니다. 

    // Fix Q&A 조회 (관리자용)
    @Query("SELECT q FROM Qna q WHERE q.member.membercode =1")
    List<Qna> findFixQna();

    // customer Q&A 조회 (회원용)
    @Query("SELECT q FROM Qna q WHERE q.member.membercode =0")
    List<Qna> findCustomerQna();

    // Q&A 특정 조회 (회원용)
    @Query("SELECT q FROM Qna q WHERE q.member.id = :id")
    public List<Qna> getMyQna(@Param("id") String id);

    // qna_seq로 QnA 조회
    @Query("SELECT q FROM Qna q WHERE q.qna_seq =:qna_seq")
    Qna findQnaBySeq(@Param("qna_seq") int qna_seq);
}

 

  • findFixQna(): 관리자 계정에서 작성된 QnA를 조회합니다.
  • findCustomerQna(): 일반 사용자 계정에서 작성된 QnA를 조회합니다.
  • getMyQna(): 특정 사용자가 작성한 QnA 목록을 조회합니다.
  • findQnaBySeq(): 특정 QnA를 QnA 시퀀스 번호(qna_seq)로 조회합니다.

3. Service 생성 (QnaService.java + QnaServiceImpl.java)

Service는 기능을 정의하는 인터페이스로, 실제 서비스가 제공해야 할 메서드를 선언합니다.

package com.demo.service;

import java.util.List;

import com.demo.domain.Qna;

public interface QnaService {

    // QnA를 seq로 찾는 메서드 정의
    Qna findQnaBySeq(int qna_seq); 

    // Q&A 작성 (qna_date로 저장)
    Qna createQna(Qna qna);
    // 고정 질문 작성
    Qna createFixQna(Qna qna);

    // Q&A 삭제
    void deleteQna(int qna_seq);

    // Q&A 수정 (qna_date로 저장)
    void updateQna(Qna qna);

    // Q&A 특정 조회 (회원용)
    List<Qna> getMyQna(String id);

    // 답변 작성 (member_code가 1인 회원만 가능, answer_date로 저장)
    Qna addAnswer(int qna_seq, String answer);

    // qna 조회
    List<Qna> getFixQna(); // 관리자용 고정 질문 조회
    List<Qna> getCustomerQna(); // 고객용 질문 조회

}

 

ServiceImpl은 해당 인터페이스의 구체적인 구현체로, 실제 비즈니스 로직을 구현합니다.

package com.demo.service;

import java.util.Date;
import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;

import com.demo.domain.Qna;
import com.demo.persistence.QnaRepository;

@Service
public class QnaServiceImpl implements QnaService {

    @Autowired
    private QnaRepository qnaRepo;

    @Override
    public Qna createQna(Qna qna) {
        qna.setQna_date(new Date()); // 작성 날짜 설정
        return qnaRepo.save(qna);
    }

    @Override
    public void deleteQna(int qna_seq) {
        Qna qna = qnaRepo.findById(qna_seq).orElseThrow(() ->
        	new IllegalArgumentException("해당 Q&A를 찾을 수 없습니다."));
        qnaRepo.delete(qna);
    }

    @Override
    public void updateQna(Qna qna) {
        Qna update_qna = qnaRepo.findQnaBySeq(qna.getQna_seq());
        qna.setQna_seq(update_qna.getQna_seq());	// 질문 고유번호 유지
        qna.setQna_date(update_qna.getQna_date());	// 질문 작성시간 유지
        qna.setMember(update_qna.getMember());		// 작성자 유지

        qnaRepo.save(qna);        
    }

    @Override
    public Qna addAnswer(int qna_seq, String answer) {
        Qna qna = qnaRepo.findById(qna_seq).orElseThrow(() ->
        	new IllegalArgumentException("해당 Q&A를 찾을 수 없습니다."));

        // 관리자만 답변을 작성할 수 있도록 체크 (member_code == 1)
        if (qna.getMember().getMembercode() != 1) {
        	throw new IllegalArgumentException("관리자만 답변을 작성할 수 있습니다.");
        }

        qna.setAnswer(answer);
        qna.setAnswer_date(new Date()); // 답변 날짜 설정
        qna.setAnswer_status(1);

        return qnaRepo.save(qna);
    }

    @Override
    public List<Qna> getMyQna(String id) {
		return qnaRepo.getMyQna(id);
    }

    @Override
    public Qna findQnaBySeq(int qna_seq) {
		return qnaRepo.findQnaBySeq(qna_seq);
    }

    @Override
    public List<Qna> getFixQna() {
		return qnaRepo.findFixQna();
    }

    @Override
    public List<Qna> getCustomerQna() {
		return qnaRepo.findCustomerQna();
    }

    @Override
    public Qna createFixQna(Qna qna) {
        qna.setQna_date(new Date()); // 작성 날짜 설정
        qna.setAnswer_date(new Date()); // 답변 날짜를 현재로 설정
        qna.setAnswer_status(1); // 답변 상태를 '답변 완료'로 설정 (1)
        return qnaRepo.save(qna);
    }

}
  • public Qna createQna(Qna qna) : 질문 작성 메서드. qna.setQna_date(new Date()); 전달된 Qna 객체에 대해 현재 날짜를 qna_date로 설정한 후, return qnaRepo.save(qna);이를 데이터베이스에 저장합니다.
  • public void deleteQna(int qna_seq) : 질문 삭제 메서드. Qna qna = qnaRepo.findById(qna_seq) 주어진 qna_seq에 해당하는 Q&A를 조회하고, qnaRepo.delete(qna); 삭제. .orElseThrow(() -> new IllegalArgumentException("해당 Q&A를 찾을 수 없습니다.")); 주어진 qna_seq로 Q&A를 찾을 수 없으면 예외를 발생.
  • public void updateQna(Qna qna) : 질문 수정 메서드.Qna update_qna = qnaRepo.findQnaBySeq(qna.getQna_seq()); 수정할 Q&A 조회. 수정할 Q&A 객체의 작성 날짜(qna_date), 작성자(member), 고유번호(qna_seq)를 기존 Q&A로 유지하고, qnaRepo.save(qna); 수정된 Qna 객체를 저장.
  • public Qna addAnswer(int qna_seq, String answer) : 답변 추가 메서드(관리자만 작성할 수 있도록 제한). qnaRepo.findById(qna_seq) 답변을 작성할 질문 조회. if (qna.getMember().getMembercode() != 1) {...} 사용자가 관리자가 아닌 경우 예외를 발생시킴. 이후 qna.setAnswer(answer)답변, qna.setAnswer_date(new Date())날짜, qna.setAnswer_status(1)답변 상태를 "답변 완료"로 설정하고, return qnaRepo.save(qna);저장.
  • public List<Qna> getMyQna(String id) : id에 해당하는 사용자가 작성한 Q&A 목록을 반환.
  • public Qna findQnaBySeq(int qna_seq) : qna_seq로 Q&A를 조회하여 반환.
  • public List<Qna> getFixQna() : member_code == 1인 관리자가 작성한 고정된 Q&A 목록을 반환.
  • public List<Qna> getCustomerQna() : member_code == 0인 고객이 작성한 일반 Q&A 목록을 반환.
  • public Qna createFixQna(Qna qna) : 고정된 Q&A를 생성하는 메서드 (관리자용). qna.setQna_date(new Date()); Q&A 작성 날짜를 현재 날짜로 설정. qna.setAnswer_date(new Date()); 답변 날짜를 현재 날짜로 설정. qna.setAnswer_status(1); 답변 상태를 "답변 완료"로 설정. return qnaRepo.save(qna); 고정된 Q&A를 저장하고 반환.
 

4. Html 작성 및 Controller

 - qna.html 호출 Controller

@GetMapping("/fav_qna")
public String qnaView(HttpSession session, Model model) {
    Member loginUser = (Member)session.getAttribute("loginUser");

    List<Qna> fixQna = qnaService.getFixQna();
    model.addAttribute("fixQna", fixQna);

    return "customer/qna";
}
  • List<Qna> fixQna = qnaService.getFixQna(); : 고정 질문목록을 가져와서 model.addAttribute("fixQna", fixQna); : model.addAttribute를 사용하여 fixQna 목록을 모델에 담고, fixQna라는 이름으로 뷰에 전달합니다. 이후 return "customer/qna"; qna.html 파일을 렌더링합니다.

 - qna.html

<div class="qna-container">
    <h2>자주 묻는 질문</h2>
    <a th:href="@{/qna_write}">질문 작성</a>
    <div class="fix-qnas">
        <div class="qna_table" th:if="${fixQna.isEmpty()}">
        	<h3>자주 묻는 질문이 아직 정리되지 않았어요...</h3>
        </div>
        <div class="qna_table" th:unless="${fixQna.isEmpty()}">
            <div class="box" th:each=" qna : ${fixQna}">
                <div class="qna-title" onclick="toggleAnswer(this)">
                	<h4 th:text="${qna.title}"></h4>
                </div>
                <div class="qna-answer" style="display: none;">
                	<h5 th:text="${qna.answer}"></h5>
                </div>
            </div>
        </div>
    </div>
</div>

<a th:href="@{/qna_write}">질문 작성</a> 링크를 클릭하면 Spring MVC 컨트롤러의 @GetMapping("/qna_write") 메서드가 호출된다.

 - Controller

@GetMapping("/qna_write")
public String qnaWriteView(HttpSession session, Model model) {	//질문 작성 페이지
    Member loginUser = (Member)session.getAttribute("loginUser");

    if(loginUser == null) {
        model.addAttribute("message", "로그인 페이지로 이동");
        model.addAttribute("text", "질문 작성은 로그인 후 이용 가능합니다.");
        model.addAttribute("messageType", "info");

        return "login/login";
    }
    return "customer/qna_write";
}
  • 메서드가 호출되면 session에서 "loginUser" 속성을 가져와 로그인된 사용자 정보를 확인합니다.
  • 사용자가 로그인하지 않았을 때:
    • model.addAttribute를 사용하여 알림 메시지를 설정합니다
    • return "login/login"; 페이지를 반환하여 사용자를 로그인 페이지로 리디렉션합니다.
  • 로그인된 경우:
    • 사용자가 로그인한 상태라면 return "customer/qna_write"; 페이지를 반환합니다. 질문 작성 페이지를 렌더링하여 사용자가 질문을 작성할 수 있도록 합니다.

 - qna_write.html

<div class="qna_write_container">
	<h2>질문 작성</h2>
	<form class="qna_write_form" id="qna-write-form" method="post">
		<input type="text" id="title" name="title" placeholder="제목을 입력해주세요."/>
		<textarea rows="10" cols="20" name="content" id="content" placeholder="내용을 입력해주세요."></textarea>
	</form>
	<button class="before-page-btn" type="button" onclick="window.location.href='/fav_qna'">목록으로</button>
	<button class="write-btn" type="button" onclick="qna_write()">질문 작성</button>
</div>

<button class="write-btn" type="button" onclick="qna_write()">질문 작성</button> 클릭 시 JavaScript의 qna_write() 함수 호출.

 - qna_write()함수 작성

// 질문 작성
function qna_write() {
	if($("#title").val() == "") {
		swal.fire({
			title: '제목을 입력해주세요.',
			text: '제목은 필수 입력 항목입니다.',
			icon: 'warning',
			confirmButtonText: '확인'
		});
		$("#title").focus();
		return false;
	} else if($("#content").val() == "") {
		swal.fire({
			title: '내용을 입력해주세요.',
			text: '내용은 필수 입력 항목 입니다.',
			icon: 'warning',
			confirmButtonText: '확인'
		});
		$("#content").focus();
		return false;
	} else {
		swal.fire({
			title: '질문 작성 성공!',
			text: '질의응답 페이지로 이동합니다.',
			icon: 'success',
			confirmButtonText: '확인'
		}).then((result) => {
			if(result.isConfirmed) {
				$("#qna-write-form").attr("action", "/qna-write-action").submit();
			}
		});
	}
}
  • if($("#title").val() == "") {...}else if($("#content").val() == "") {...} 를 통해 필수 입력 항목을 체크하고, 모든 필수 항목이 입력된 경우 ..then((result) => {...})로 swal.fire에서 확인 버튼을 클릭한 경우의 후속 동작을 지정합니다. if(result.isConfirmed) : 사용자가 "확인" 버튼을 누른 경우 $("#qna-write-form").attr("action", "/qna-write-action").submit(); 폼의 action 속성을 /qna-write-action으로 설정 하고, submit() 메서드를 호출하여 폼을 서버에 제출합니다.

 - ( /qna-write-action ) Controller

@PostMapping("/qna-write-action")
public String qnaWriteAction(HttpSession session, Model model, Qna qna) {
    Member loginUser = (Member) session.getAttribute("loginUser");

    if(loginUser == null) {
        model.addAttribute("message", "로그인 페이지로 이동");
        model.addAttribute("text", "질문 작성은 로그인 후 이용 가능합니다.");
        model.addAttribute("messageType", "info");

        return "login/login";
    }

    qna.setMember(loginUser);
    qnaService.createQna(qna);

    return "mypage/myQna";
}
  • qna.setMember(loginUser); : 현재 로그인한 사용자의 정보를 Qna 객체의 member 필드에 설정합니다.
  • qnaService.createQna(qna); : createQna 메서드를 호출하여 html에서 입력한 정보들을 qna 객체로 데이터베이스에 저장합니다.
  • return "mypage/myQna"; : 질문 작성 작업이 완료되면 내 질문 페이지로 이동합니다.

예외 처리
DB 결과

 - 내 질문 현황 Controller

// 내 질문 현황
@GetMapping("/myqna")
public String myQnaView(HttpSession session, Model model) {
    Member loginUser = (Member)session.getAttribute("loginUser");

    if(loginUser == null) {
        return "login/login";
    }
    model.addAttribute("loginUser", loginUser);
    List<Qna> myQna = qnaService.getMyQna(loginUser.getId());

    model.addAttribute("myQna", myQna);

    return "mypage/myQna";
}

 

  • List<Qna> myQna = qnaService.getMyQna(loginUser.getId()); : 로그인한 회원의 ID로 작성한 질문이 있는지 조회합니다.
  • model.addAttribute("myQna", myQna); : model.addAttribute를 이용해서 질문 목록을 model에 담아 myQna로 전달합니다.
  • return "mypage/myQna"; : myQna.html을 렌더링합니다.

 - myQna.html

<div class="myqna">
    <h1>내 질문 현황</h1>
    <div class="qna-item">
    	<div th:if="${myQna.isEmpty()}">
    		<h3>작성한 질문이 없습니다.</h3>
    		<a th:href="@{/qna_write}">질문 작성</a>
    	</div>
    	<div th:unless="${myQna.isEmpty()}">
    		<a th:href="@{/qna_write}">질문 작성</a>
            <div class="qna-box">
                <table>
                    <thead>
                        <tr>
                            <th>No</th>
                            <th>제목</th>
                            <th>답변 현황</th>
                            <th>작성 날짜</th>
                            <th>작업</th>
                        </tr>
                    </thead>
                    <tbody>
                        <tr th:each="qna : ${myQna}">
                            <td th:text="${qna.qna_seq}"></td>
                            <td th:text="${qna.title}"></td>
                            <td>
                                <span th:text="${qna.answer_status == 1 ? '답변 완료' : ' 답변 대기'}"></span> <!-- 답변 현황 -->
                            </td>
                            <td th:text="${#dates.format(qna.qna_date, 'yyyy-MM-dd HH:mm:ss')}"></td> <!-- 질문 작성 날짜 -->
                            <td>
                                <!-- 수정 버튼 -->
                                <button class="qna-edit-btn" type="button" th:attr="data-qna_seq=${qna.qna_seq},
                                    data-qna_writer=${qna.member.id}, data-answer_status=${qna.answer_status},
                                    data-qna_viewer=${loginUser.id}" onclick="editQna(this)">수정</button>
                                <!-- 삭제 버튼 -->
                                <button class="qna-delete-btn" type="button" th:attr="data-qna_seq=${qna.qna_seq},
                                    data-qna_writer=${qna.member.id}, data-qna_viewer=${loginUser.id}"
                                    onclick="deleteQna(this)">삭제</button>
                            </td>
                        </tr>
                    </tbody>
                </table>
            </div>
        </div>
    </div>
</div>

 

 

 

 

다음 글에서는 질문한 작성 수정 및 삭제에 대해 포스팅 하겠습니다.