이번 글에서는 관리자 페이지에서의 사이트의 질문(Q&A)관리에 대한 기능설명과 코딩을 만들어 보겠습니다.
부족하더라도 좋게 봐주세요. :)
===== 질문 관리 =====
Qna 클래스 생성
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;
}
+ Repository 작성
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> {
// 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);
}
- interface QnaRepository extends JpaRepository : QnaRepository는 Spring Data JPA의 JpaRepository를 상속받아 기본 CRUD기능을 제공받는다. >> 기본 데이터 처리(생성, 읽기, 업데이트, 삭제)에 필요한 쿼리를 작성할 필요가 없다.
- 나머지 메서드들을 주석에 해당하는 쿼리를 작성하여 만든 메서드입니다.
+ Service 작성
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 Qna createFixQna(Qna qna) {
qna.setQna_date(new Date()); // 작성 날짜 설정
qna.setAnswer_date(new Date()); // 답변 날짜를 현재로 설정
qna.setAnswer_status(1); // 답변 상태를 '답변 완료'로 설정 (1)
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();
}
}
(주의 : 질문 작성에 대해서 회원용과 관리자용이 따로 있음.)
질문 관리 섹션 (adminMain.html)
<div class="admin-section">
<h3 class="clickable" onclick="toggleContent(this)">질문 관리</h3>
<div class="content-list" style="display: none;">
<a th:href="@{/admin-fix-qna}">고정 질문</a><br>
<a th:href="@{/admin-customer-qna}">회원 질문</a><br>
</div>
</div>
- 고정질문, 회원질문에 대한 링크는 Thymeleaf의 th:href를 사용해 URL을 동적으로 구성.
Controller 처리
// 고정질문 페이지
@GetMapping("/admin-fix-qna")
public String fixedQuestions(HttpSession session, Model model) {
Member admin = (Member)session.getAttribute("admin");
if(admin == null) {
model.addAttribute("message", "로그인 페이지로 이동");
model.addAttribute("text", "질문관리를 위해 로그인해주세요.");
model.addAttribute("messageType", "info");
return "admin/admin_login";
}
// 고정 질문 목록을 가져오는 로직 구현
List<Qna> adminQna = qnaService.getFixQna();
model.addAttribute("adminQna", adminQna);
return "admin/section/fixed_questions"; // 뷰 이름
}
// 회원질문 페이지
@GetMapping("/admin-customer-qna")
public String customerQuestions(HttpSession session, Model model) {
Member admin = (Member)session.getAttribute("admin");
if(admin == null) {
model.addAttribute("message", "로그인 페이지로 이동");
model.addAttribute("text", "회원관리를 위해 로그인해주세요.");
model.addAttribute("messageType", "info");
return "admin/admin_login";
}
// 회원 질문 목록을 가져오는 로직 구현
List<Qna> customerQna = qnaService.getCustomerQna();
model.addAttribute("customerQna", customerQna);
return "admin/section/customer_questions"; // 뷰 이름
}
- @GetMapping("...........") : @GetMapping 어노테이션은 " " 안의 URL로 GET 요청을 처리하는 메서드를 연결.
- List<Qna> adminQna = qnaService.getFixQna(); 고정 질문 데이터를 조회 > model.addAttribute("adminQna", adminQna); 조회한 데이터를 Model 객체에 추가해서 전달. return "admin/section/fixed_questions"; 렌더링.
- List<Qna> customerQna = qnaService.getCustomerQna(); 회원 질문 데이터를 조회 > model.addAttribute("customerQna", customerQna); 조회한 데이터를 Model 객체에 추가해서 전달. return "admin/section/customer_questions"; . 렌더링.
고정 질문 페이지( fixed_questions.html )
<div class="all-container">
<h1>고정 질문(관리자)</h1>
<button class="fix-qna-write-btn" type="button" onclick="location.href='/fix-qna-write'">고정 질문 작성</button>
<div class="fix-qna-container">
<div class="item" th:if="${adminQna.isEmpty()}">
<h3>작성한 고정 질문이 없습니다..</h3>
</div>
<div class="item" th:unless="${adminQna.isEmpty()}">
<div class="box" th:each=" qna : ${adminQna}">
<div class="qna-title">
<h4 th:text="${qna.title}"></h4>
</div>
<div class="qna-answer">
<p th:text="${qna.answer}"></p>
</div>
<div class="qna-actions">
<input type="hidden" th:value="${qna.qna_seq}" />
<button class="fix-qna-edit" type="button" th:data-seq="${qna.qna_seq}"
onclick="editFixQna(this)">고정질문 수정</button>
<button class="fix-qna-delete" type="button" th:data-seq="${qna.qna_seq}"
onclick="deleteFixQna(this)">삭제</button>
</div>
</div>
</div>
</div>
</div>
- th:if="${adminQna.isEmpty()}" : Controller에서 Model로 전달받은 adminQna 리스트 비어있는 경우 렌더링.
- <button class="fix-qna-write-btn" type="button" onclick="location.href='/fix-qna-write'">고정 질문 작성</button> : 버튼 클릭 시 /fix-qna-write 경로로 이동.
Controller 처리
// 고정질문 작성 페이지
@GetMapping("/fix-qna-write")
public String fixQnaWriteView(HttpSession session, Model model) {
Member admin = (Member)session.getAttribute("admin");
if(admin == null) {
model.addAttribute("message", "로그인 페이지로 이동");
model.addAttribute("text", "고정질문 작성을 위해 로그인해주세요.");
model.addAttribute("messageType", "info");
return "admin/admin_login";
}
return "admin/section/fix_qna_write";
}
- @GetMapping("/fix-qna-write") url 경로로 GET 요청을 처리.
- if(admin == null) {... : 로그인 여부 확인.
고정 질문 작성 페이지 (fix_qna_write.html)
<div class="all-container">
<h1>고정 질문 작성</h1>
<div class="fix-write-container">
<div class="item">
<form class="fix-qna-form" id="fix-qna-form" method="post">
<label for="title">질문</label>
<input type="text" id="title" placeholder="질문을 입력해주세요." name="title"/>
<label for="answer">답변</label>
<textarea id="answer" placeholder="답변을 입력해주세요." name="answer"></textarea>
</form>
</div>
<button class="before-btn" type="button" onclick="window.location.href='/admin-fix-qna'">이전페이지</button>
<button class="write-btn" type="button" onclick="fix_qna_write()">고정질문 작성</button>
</div>
</div>
+ JavaScript 작성 (고정질문 작성 버튼 클릭 시 호출할 함수)
// 고정질문 작성
function fix_qna_write() {
if($("#title").val() == "") {
swal.fire({
title: '제목을 입력해주세요.',
icon: 'warning',
confirmButtonText: '확인'
});
$("#title").focus();
return false;
} else if($("#answer").val() == "") {
swal.fire({
title: '답변을 입력해주세요.',
icon: 'warning',
confirmButtonText: '확인'
});
$("#answer").focus();
return false;
} else {
swal.fire({
title: '고정질문 작성 성공!',
icon: 'success',
confirmButtonText: '확인'
}).then((result) => {
$("#fix-qna-form").attr("action", "fix-qna-write-form").submit();
})
}
}
- if($("#title").val() == "") { ... } else if($("#answer").val() == "") { ... : 제목과 답변을 입력했는지 확인.
- else { ... 제목과 답변 모두 입력 시 성공 메시지와 함께 확인 버튼 클릭 시 .then((result) => { 처리.
- $("#fix-qna-form").attr("action", "fix-qna-write-form").submit(); controller에 처리 요청.
/fix-qna-write-form url 경로 처리 요청
@PostMapping("/fix-qna-write-form")
public String fixQnaWriteAction(HttpSession session, Model model, Qna fixQna) {
Member admin = (Member)session.getAttribute("admin");
if(admin == null) {
model.addAttribute("message", "로그인 페이지로 이동");
model.addAttribute("text", "고정질문 작성을 위해 로그인해주세요.");
model.addAttribute("messageType", "info");
return "admin/admin_login";
}
fixQna.setMember(admin);
qnaService.createFixQna(fixQna);
return "redirect:/admin-fix-qna";
}
- @PostMapping("/fix-qna-write-form") : url 경로가 /fix-qna-write-form인 HTTP POST요청 처리.
- session.getAttribute("admin"); : 세션에 로그인된 관리자의 정보를 담고있는 객체를 반환.
- fixQna.setMember(admin); : 작성한 고정 질문의 작성자를 로그인한 관리자로 설정.
- qnaService.createFixQna(fixQna); : QnaService의 메서드를 호출하여 새 고정 질문을 DB에 저장.
++ 테스트
+ 고정 질문 수정
<div class="qna-actions">
<input type="hidden" th:value="${qna.qna_seq}" />
<button class="fix-qna-edit" type="button" th:data-seq="${qna.qna_seq}"
onclick="editFixQna(this)">고정질문 수정</button>
<button class="fix-qna-delete" type="button" th:data-seq="${qna.qna_seq}"
onclick="deleteFixQna(this)">삭제</button>
</div>
- <input type="hidden" th:value="${qna.qna_seq}" /> : 각 질문을 특정하기 위해 사용.
- <button class="fix-qna-edit" type="button" th:data-seq="${qna.qna_seq}" onclick="editFixQna(this)">고정질문 수정 </button> : th:data-seq="${qna.qna_seq}" 로 질문의 고유번호 저장.(JavaScript 함수가 사용할 수 있도록) > editFixQna(this) 클릭된 버튼요소 전달.
JavaScript 함수 (editFixQna) 작성
// 고정질문 수정
function editFixQna(button) {
const qna_seq = button.getAttribute('data-seq');
window.location.href = `/fix-qna-edit?qna_seq=${qna_seq}`;
}
- const qna_seq = button.getAttribute('data-seq'); : 버튼의 data-seq 값을 가져온다. (질문 고유 번호)
- window.location.href = `/fix-qna-edit?qna_seq=${qna_seq}`; : 경로를 /fix-qna-edit으로 이동. qna_seq값을 파라미터로 전달.
Controller 처리
// 고정질문 수정 페이지
@GetMapping("/fix-qna-edit")
public String editFixQnaView(@RequestParam("qna_seq")int qna_seq, Model model, HttpSession session) {
Member admin = (Member)session.getAttribute("admin");
if(admin == null) {
model.addAttribute("message", "로그인 페이지로 이동");
model.addAttribute("text", "질문 수정을 위해 로그인해주세요.");
model.addAttribute("messageType", "info");
return "admin/admin_login";
}
Qna qna = qnaService.findQnaBySeq(qna_seq);
model.addAttribute("qna", qna);
return "admin/section/fix_qna_edit";
}
- @GetMapping("/fix-qna-edit") : URL 경로에 대한 GET 요청 처리.
- @RequestParam("qna_seq")int qna_seq : 전달된 qna_seq 값을 파라미터로 매핑.
- Qna qna = qnaService.findQnaBySeq(qna_seq); : 전달받은 qna_seq로 해당 질문 데이터를 검색 > Qna객체로 반환.
- model.addAttribute("qna", qna); : 수정할 질문 데이터를 뷰 템플릿에 Model로 전달.
고정 질문 수정 페이지 (fix_qna_edit.html)
<div class="all-container">
<h1>고정 질문 수정</h1>
<div class="fix-edit-container">
<form class="fix-qna-edit-form" id="fix-qna-edit-form" method="post">
<input type="hidden" name="qna_seq" th:value="${qna.qna_seq}" />
<label>질문</label>
<input type="text" name="title" id="title" th:value="${qna.title}" />
<label>답변</label>
<textarea name="answer" id="answer" th:text="${qna.answer}"></textarea>
</form>
<div class="edit-button">
<button class="before-btn" type="button" onclick="window.location.href='/admin-fix-qna'">목록으로</button>
<button class="edit-btn" type="button" onclick="update_fix_qna()">고정질문 수정</button>
</div>
</div>
</div>
- 질문 또는 답변을 수정 후 버튼을 통해 JavaScript 함수 호출.
function update_fix_qna() {
if($("#title").val() == "") {
swal.fire({
title: '제목을 입력해주세요.',
icon: 'warning',
confirmButtonText: '확인'
});
$("#title").focus();
return false;
} else if($("#answer").val() == "") {
swal.fire({
title: '답변을 입력해주세요.',
icon: 'warning',
confirmButtonText: '확인'
});
$("#answer").focus();
return false;
} else {
swal.fire({
title: '고정질문 수정 성공!',
icon: 'success',
confirmButtonText: '확인'
}).then((result) => {
$("#fix-qna-edit-form").attr("action", "/fix-qna-edit-form").submit();
})
}
}
Controller 처리
// 고정질문 수정 처리
@PostMapping("/fix-qna-edit-form")
public String editFixQnaAction(HttpSession session, Model model, Qna vo) {
Member admin = (Member)session.getAttribute("admin");
if(admin == null) {
model.addAttribute("message", "로그인 페이지로 이동");
model.addAttribute("text", "질문 수정을 위해 로그인해주세요.");
model.addAttribute("messageType", "info");
return "admin/admin_login";
}
qnaService.updateQna(vo);
return "redirect:/admin-fix-qna";
}
++ 테스트
고정 질문 삭제
- Html 버튼
<button class="fix-qna-delete" type="button" th:data-seq="${qna.qna_seq}"
onclick="deleteFixQna(this)">삭제</button>
- 버튼 클릭 시 호출할 JavaScript 함수
// 고정질문 삭제
function deleteFixQna(button) {
const qna_seq = button.getAttribute('data-seq');
swal.fire({
title: '삭제 확인',
text: '이 질문을 삭제하시겠습니까?',
icon: 'warning',
showCancelButton: true,
confirmButtonText: '삭제',
cancelButtonText: '취소'
}).then((result) => {
if (result.isConfirmed) { // 사용자가 확인 버튼을 클릭했는지 확인
$.ajax({
url: '/delete-fix-qna',
type: 'POST',
data: { qna_seq: qna_seq },
success: function() {
swal.fire('삭제 성공', '정상적으로 삭제 처리 되었습니다.').then(() => {
location.reload(); // 페이지 새로고침
});
},
error: function(xhr) {
swal.fire('오류', '질문 삭제 실패...', 'error');
}
});
}
});
}
- $.ajax({ : Ajax 요청
- url: '/delete-fix-qna', type: 'POST' : ' '안의 url로 POST 요청.
- data: { qna_seq: qna_seq } : 삭제할 고정 질문의 고유 번호를 전달.
- 성공 시 swal.fire('삭제 성공', '정상적으로 삭제 처리 되었습니다.').then(() => { 알림 창과 location.reload();페이지 새로고침.
- 실패 시 swal.fire('오류', '질문 삭제 실패...', 'error'); 오류 메시지.
// 고정질문 삭제 처리
@PostMapping("/delete-fix-qna")
public String deleteFixQnaAction(@RequestParam("qna_seq") int qna_seq, HttpSession session,
Model model) {
Member admin = (Member)session.getAttribute("admin");
if(admin == null) {
model.addAttribute("message", "로그인 페이지로 이동");
model.addAttribute("text", "고정질문 삭제를 위해 로그인해주세요.");
model.addAttribute("messageType", "info");
return "admin/admin_login";
}
qnaService.deleteQna(qna_seq);
return "redirect:/admin-fix-qna";
}
++ 테스트
'SpringBoot 프로젝트' 카테고리의 다른 글
Spring Boot - 국립 도서관 Open API를 활용한 사서 추천 도서 목록 구현 (0) | 2024.12.10 |
---|---|
Spring Boot - 관리자 페이지 (5) (0) | 2024.12.07 |
Spring Boot - 관리자 페이지 (3) (0) | 2024.11.30 |
Spring Boot - 관리자 페이지 (2) (0) | 2024.11.25 |
Spring Boot - 관리자 페이지 (1) (0) | 2024.11.25 |