SpringBoot 프로젝트

Spring Boot - 관리자 페이지 (4)

orin602 2024. 12. 7. 16:24

이번 글에서는 관리자 페이지에서의 사이트의 질문(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에 저장.

++ 테스트

제목, 답변을 입력하지 않았을 때
제목, 답변 모두 입력 시
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";
}

 

++ 테스트

수정된 내용 반영
DB도 수정

고정 질문 삭제

  • 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";		
}

 

++ 테스트

알림 메시지
삭제 후 페이지
삭제 후 DB