SpringBoot 프로젝트

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

orin602 2024. 11. 30. 14:45

지난번 글의 회원관리-회원코드 수정에 이어 이번글에서는 회원탈퇴 처리 및 공지사항에 대한 설명과 기능구현을 해보겠습니다. 부족해도 좋게 봐주세요 :)

 

회원관리 페이지

 

Html (회원 탈퇴 여부)

<td>
    <span th:text="${member.withdrawalRequest == 0 ? 'X' : 'O'}"></span>
    <button class="delete-btn" type="button" th:alt="${member.withdrawalRequest}"
        th:data-memberid="${member.id}" th:data-withdrawalRequest="${member.withdrawalRequest}"
        onclick="deleteMember(this)">회원탈퇴</button>
</td>
  • <span th:text="${member.withdrawalRequest == 0 ? 'X' : 'O'}"></span> : 해당 행의 회원의 탈퇴 요청 상태를 withdrawalRequest  값에 따라 표시.(삼항 연산자를 이용해서 0이면 X , 0이 아니면 O를 출력)
  • 버튼 클릭 시  deleteMember(this) 함수가 호출. th:data-memberid="${member.id}" 회원ID를 동적으로 할당, th:data-withdrawalRequest="${member.withdrawalRequest}" 탈퇴 요청 상태를 전달.

JavaScript

// 회원 탈퇴 처리함수
function deleteMember(button) {
	const memberId = button.getAttribute('data-memberid');
	const withdrawalRequest = button.getAttribute('data-withdrawalRequest');
	
	if(withdrawalRequest === '0') {
		swal.fire({
			title: '탈퇴 처리 불가',
			text: '탈퇴 요청을 하지 않은 회원입니다.',
			icon: 'warning',
			confirmButtonText: '확인'
		});
		return;
	}
	swal.fire({
		title: '정말로 탈퇴 처리 하시겠습니까?',
		text: '이 작업은 되돌릴 수 없습니다.',
		icon: 'warning',
		showCancelButton: true,
		confirmButtonText: '회원 탈퇴',
		cancelButtonText: '취소'
	}).then((result) => {
		if(result.isConfirmed) {
			$.ajax({
				url: '/delete-member-admin',
				type: 'POST',
				data: { id: memberId },
				success: function(response) {
					swal.fire({
						title: '탈퇴 처리 성공',
						text: response,
						icon: 'success'
					}).then(() => {
						location.reload();
					});
				},
				error: function(xhr) {
					console.error('Error : ', xhr.status, xhr.statusText);
					if(xhr.status === 401) {
						swal.fire({
							title: '권한 오류',
							text: '관리자 로그인 후 시도하세요.',
							icon: 'warning'
						}).then(() => {
							window.location.href = '/admin/admin_login';
						});
					} else {
						swal.fire('오류', '회원 탈퇴 처리 실패... 다시 시도해주세요.');
					}
				}
			});
		}
	});
}
  • button.getAttribute(); 를 통해서 html에서 할당한 ""의 값을 가져온다.
  • if(withdrawalRequest === '0') {...} : 탈퇴 요청 확인. '0'이면 경고메시지와 함수를 중. 이후 탈퇴 요청 확인 팝업을 표시하고, confirmButtonText: '회원 탈퇴' 버튼을 클릭 시 result.isConfirmed 가 true. > ajax요청.

Controller

// 회원 탈퇴 처리
@PostMapping("/delete-member-admin")
public ResponseEntity<String> deleteMemberAction (HttpSession session, Model model,
        @RequestParam("id") String memberId) {
    Member admin = (Member)session.getAttribute("admin");

    if(admin == null) {
        return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
                .body("로그인후 가능한 기능입니다."); // 401 Unauthorized 응답
    }

    try {
        Member member = memberService.getMember(memberId);

        if(member == null) {
            return ResponseEntity.status(HttpStatus.NOT_FOUND)
                    .body("회원 정보를 찾을 수 없습니다.");
        }
        memberService.deleteMember(member);

        return ResponseEntity.ok("회원 탈퇴가 성공적으로 처리되었습니다.");
    } catch (Exception e) {
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).
                body("회원 탈퇴 처리 중 오류가 발생했습니다.");
    }
}
  • ResponseEntity<String> : Http 상태 코드, 응답 메시지를 반환.
  • @RequestParam("id") String memberId : 요청한 파라미터에서 "id"값을 읽어와 memberId로 저장.
  • memberService.getMember(memberId); : 탈퇴 요청을 처리할 회원 정보를 가져온다.
  • memberService.deleteMember(member); : 회원이 존재하면 회원정보 삭제(CRUD의 Delete기능). 이후ResponseEntity.ok("회원 탈퇴가 성공적으로 처리되었습니다."); 로 응답 메시지 반환.

+++ 테스트 +++

탈퇴요청을 하지 않은 회원.
탈퇴 처리 테스트용 회원
성공 메시지에서 OK버튼 클릭 후
DB 결과

 

===== 공지사항 관리 =====

Notice 클래스 생성

package com.demo.domain;

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

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

import jakarta.persistence.ElementCollection;
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 Notice {	//공지사항 table

	@Id
	@GeneratedValue(strategy=GenerationType.SEQUENCE, generator = "noticeseq")
	@SequenceGenerator(name="noticeseq", sequenceName="noticeseq", allocationSize = 1)
	private int notice_seq;	//공지사항 seq
	
	private String title;	//공지사항 제목
	private String content;	//공지사항 내용
	
	@ColumnDefault("0")
	private int likeCount;	//공지사항 좋아요수
	@ColumnDefault("0")
	private int viewCount;	//공지사항 조회수
	
	private String noticeImageUrl;	//공지사항 이미지 url
	@ElementCollection
	private List<String> uploadedImages;
	
	
	@Temporal(value=TemporalType.TIMESTAMP)
	@ColumnDefault("sysdate")
	private Date notice_date;	//공지사항 작성 날짜
	
	// 
	@ManyToOne
	@JoinColumn(name="id")
	private Member 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.Notice;
import com.demo.domain.Review;

public interface NoticeRepository extends JpaRepository<Notice, Integer> {

    // Spring Data JPA가 제공하는 기본 메서드로 대부분의 단순한 데이터 작업을 처리할 수 있으므로, 
    // 별도로 JPQL이나 네이티브 SQL을 작성하지 않았습니다.

	// 리뷰 상세 조회
    @Query(value = "SELECT * FROM notice WHERE notice_seq = :notice_seq", nativeQuery = true)
    Notice getNoticeBySeq(@Param("notice_seq") int notice_seq);
	
}

 

Service 작성

package com.demo.service;

import java.io.IOException;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

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

import com.demo.domain.FileUploadUtil;
import com.demo.domain.Notice;
import com.demo.domain.Review;
import com.demo.persistence.NoticeRepository;

@Service
public class NoticeServiceImpl implements NoticeService {

    @Autowired
    private NoticeRepository noticeRepo;

    @Override
    public Notice createNotice(Notice notice) {
    	return noticeRepo.save(notice);
    }

    @Override
    public void deleteNotice(int notice_seq) {
    	noticeRepo.deleteById(notice_seq);
    }

    @Override
    public void updateNotice(Notice vo) {
        Notice update_notice = noticeRepo.getNoticeBySeq(vo.getNotice_seq());

        vo.setMember(update_notice.getMember());
        vo.setNotice_seq(update_notice.getNotice_seq());
        vo.setMember(update_notice.getMember());

        noticeRepo.save(update_notice);
    }

    @Override
    public Notice getNoticeById(int notice_seq) {	// 단일 조회
        return noticeRepo.findById(notice_seq)
        	.orElseThrow(() -> new IllegalArgumentException("공지사항을 찾을 수 없습니다."));
    }


    @Override
    public List<Notice> getAllNotices() {
    	return noticeRepo.findAll();
    }

    @Override
    @Transactional
    public void deleteImage(int notice_seq, int imageIndex) {
        Notice notice = noticeRepo.getNoticeBySeq(notice_seq);
        List<String> uploadImages = notice.getUploadedImages();

        if (imageIndex >= 0 && imageIndex < uploadImages.size()) {
            // 이미지 리스트에서 해당 인덱스의 이미지 삭제
            String imageUrlToRemove = uploadImages.remove(imageIndex);

            try {
                // 파일 시스템에서 이미지 삭제
                FileUploadUtil.deleteFile(imageUrlToRemove);
            } catch (IOException e) {
                // IOException을 RuntimeException으로 전환하여 처리
                throw new RuntimeException("Failed to delete image file: " + imageUrlToRemove, e);
            }
            notice.setUploadedImages(uploadImages);
            noticeRepo.save(notice);
        } else {
        	throw new IllegalArgumentException("Invalid image index: " + imageIndex);
        }

    }
}
  • @Service : 클래스가 서비스 계층임을 명시하고, @Autowired private NoticeRepository noticeRepo; Repository를 주입아 데이터베이스와 작업처리.
  • public Notice createNotice(Notice notice) {... : 공지사항 생성 메서드 noticeRepo.save(notice); save메서드를 통해 새로운 공지사항을 DB에 저장한다. 입력받은 notice 객체를 저장하고, 저장된 객체를 반환.
  • public void deleteNotice(int notice_seq) {... : notice_seq 를 기준으로 해당 공지사항을 삭제하는 메서드. notice_seq 값이 DB에 존재하지 않으면 예외 발생.
  • public void updateNotice(Notice vo) {... : Notice update_notice = noticeRepo.getNoticeBySeq(vo.getNotice_seq()); 수정할 공지사항을 조회한 뒤 update_notice 객체에 저장. 기존 vo.set...(update_notice.get(...)); 객체에 수정한 데이들을 저장. noticeRepo.save(update_notice); 
  •  : 

 

Html (adminMain.html)

<!-- 공지사항 섹션 -->
<div class="admin-section">
    <h3 class="clickable" onclick="toggleContent(this)">공지사항 관리</h3>
    <div class="content-list" style="display: none;">
        <a th:href="@{/admin-notice-list}">전체리스트</a><br>
        <a th:href="@{/admin-notice-write}">작성</a><br>
    </div>
</div>

 

Controller (매핑 url)

 - 전체리스트 ( /admin-notice-list )

// 모든 공지사항
@GetMapping("/admin-notice-list")
public String allNoticeList(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";
    }
    model.addAttribute("admin", admin);
    List<Notice> notices = noticeService.getAllNotices();	

    model.addAttribute("notices", notices);
    return "admin/section/notice_list"; // 뷰 이름
}
  • Member admin = (Member)session.getAttribute("admin"); : 현재 세션에서 admin 정보를 가져와 admin 객체에 저장.
  • model.addAttribute("admin", admin); : 세션에서 가져온 객체 admin을 html에서 활용할 수 있도록 model에 추가.
  • List<Notice> notices = noticeService.getAllNotices(); : 모든 공지사항을 가져온 뒤, model.addAttribute("notices", notices); model로 전달.
  • return "admin/section/notice_list"; : Spring MVC가 이 뷰 이름을 사용해 html 파일을 찾아 렌더링.

 - 작성 ( /admin-notice-write )

// 관리자 공지사항 작성
@GetMapping("/admin-notice-write")
public String writeNotice(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/notice_write"; // 뷰 이름
}

 

공지사항 전체리스트 Html (notice_list.html)

<div class="all-container">
    <h1>공지사항 관리</h1>
    <button class="notice-write-btn" type="button" onclick="location.href='/admin-notice-write'">공지사항 작성</button>
    <div class="items">
        <div class="item" th:if="${notices.isEmpty()}">
        	<h3>아직 작성한 공지사항이 없습니다...</h3>
        </div>
        <div class="item" th:unless="${notices.isEmpty()}">
            <div class="box" th:each="notice : ${notices}">
                <a th:href="@{notice_detail(notice_seq=${notice.notice_seq})}">
                    <th:block th:if="${#lists.isEmpty(notice.uploadedImages)}">
                    	<img th:src="@{/images/no_img.jpg}" alt="이미지 없음" style="width:200px; height:200px;">
                    </th:block>
                    <th:block th:unless="${#lists.isEmpty(notice.uploadedImages)}">
                        <div th:with="firstImage=${notice.uploadedImages[0]}">
                            <img th:src="@{${firstImage}}" th:alt="${notice.title}" style="width:200px; height:200px;">
                        </div>
                    </th:block>
                </a>
                <h4>제목 : <span th:text="${notice.title}"></span></h4>
                <h4>좋아요수 : <span th:text="${notice.likeCount}"></span> / 
                조회수 : <span th:text="${notice.viewCount}"></span></h4>
            </div>
        </div>
    </div>
</div>

 

공지사항 작성 Html (notice_write.html)

<div class="all-container">
    <h1>공지사항 작성</h1>
    <div class="notice-write-container">
        <div class="item">
            <form class="notice-write-form" id="notice-write-form" method="post" enctype="multipart/form-data">
                <label for="title">제목</label>
                <input type="text" id="title" name="title" placeholder="제목을 입력해주세요." class="form-input" />

                <label for="content">내용</label>
                <textarea name="content" id="content" placeholder="내용을 입력해주세요." class="form-textarea"></textarea>
                <label class="custom-file-uploads" for="uploadFile">
                    <input type="file" name="uploadFile" id="uploadFile" multiple>
                    이미지 업로드 (선택 사항)
                </label>
            </form>
        </div>
        <button class="before-btn" type="button" onclick="window.location.href='/admin-notice-list'">이전페이지</button>
        <button class="write-btn" type="button" onclick="notice_write()">공지사항 작성</button>
    </div>
</div>

  • 필수 입력 항목 제목,내용을 입력 후 <button class="write-btn" type="button" onclick="notice_write()">공지사항 작성</button> 버튼을 통해 처리할 함수 notice_write() 호출.

JavaScript notice_write()함수

// 공지사항 작성
function notice_write() {
	if($("#title").val() == "") {
		swal.fire({
			title: '제목을 입력해주세요.',
			icon: 'warning'
		});
		$("#title").focus();
		return false;
	} else if($("#content").val() == "") {
		swal.fire({
			title: '내용을 입력해주세요.',
			icon: 'warning'
		});
		$("#content").focus();
		return false;
	} else {
		swal.fire({
			title: '공지사항 작성 성공!',
			text: '공지사항 목록 페이지로 이동합니다.',
			icon: 'success',
			confirmButtonText: '확인'
		}).then((result) => {
			if(result.isConfirmed) {
				$("#notice-write-form").attr("action", "/notice-write-action").submit();
			}
		});
	}
	
}
  • 필수 입력 항목인 제목(title), 내용(content)이 빈칸이 아닌지 확인한다. => 빈칸이면 경고메시지.
  • 모두 확인이 되면 성공 메시지와 함께 확인 버튼을 클릭result.isConfirmed하면 "/notice-write-action" url로 controller에 요청.

Controller (/notice-write-action)

// 관리자 공지사항 작성 처리
@PostMapping("/notice-write-action")
public String writeNoticeAction(HttpSession session, Model model, Notice notice,
        @RequestParam("uploadFile") MultipartFile[] uploadFile) {
    Member admin = (Member)session.getAttribute("admin");

    if(admin == null) {
        model.addAttribute("message", "로그인 페이지로 이동..");
        model.addAttribute("text", "공지사항 작성은 관리자 로그인 후 이용 가능합니다.");
        model.addAttribute("messageType", "info");

        return "admin/admin_login";
    }
    notice.setMember(admin);

    List<String> fileUrls = new ArrayList<>();
    for (MultipartFile file : uploadFile) {
        if(!file.isEmpty()) {
            // 경로
            String uploadDir = "C:/ThisIsJava/SpringBootWorkspace/Book/uploads2/";
            // 파일이름 수정
            String originalName = file.getOriginalFilename();
            String fileExtension = originalName.substring(originalName.lastIndexOf("."));
            String uuid = UUID.randomUUID().toString();
            String fileName = uuid + fileExtension;

            try {
                // 파일 저자ㅇ
                FileUploadUtil.saveFile(uploadDir, fileName, file);
                // url 생성
                String fileUrl = "/uploads2/" + fileName;
                fileUrls.add(fileUrl);
            } catch (IOException e) {
                e.printStackTrace();
                model.addAttribute("message", "파일 업로드 중 오류 발생");
                model.addAttribute("messageType", "error");
            }
            notice.setUploadedImages(fileUrls);
        }
    }
    noticeService.createNotice(notice);

    return "redirect:/admin-notice-list";
}
  • @PostMapping("/notice-write-action") : /notice-write-action 경로로 들어오는 post 요청 처리. 작성한 공지사항 데이터(Notice notice)와 첨부파일(@RequestParam("uploadFile") MultipartFile[] uploadFile)을 함께 받는다. 
  • Member admin = (Member)session.getAttribute("admin"); : 세션에 저장된 관리자(admin)정보를 확인 및 객체에 저장.
  • notice.setMember(admin); : 공지사항 작성자 정보를 현재 로그인한 관리자로 저장.
  • 파일 업로드 처리 과정은 생략...
  • noticeService.createNotice(notice); : 전달받은 공지사항 객체를 DB에 저장.

++ 테스트

필수 입력항목이 빈칸일 때
제목과 내용 모두 입력.
DB 결과

공지사항 상세보기 ( <a th:href="@{notice_detail(notice_seq=${notice.notice_seq})}"> )

- 매핑 Controller

// 공지사항 상세
@GetMapping("/notice_detail")
public String noticeDetailView(HttpSession session, Model model, @RequestParam("notice_seq") int notice_seq) {
    Member admin = (Member)session.getAttribute("admin");
    if(admin == null) {
        model.addAttribute("message", "로그인 페이지로 이동");
        model.addAttribute("text", "공지사항 수정을 위해 로그인해주세요.");
        model.addAttribute("messageType", "info");

        return "admin/admin_login";
    }
    Notice notice = noticeService.getNoticeById(notice_seq);

    List<String> uploadImages = notice.getUploadedImages();
    model.addAttribute("uploadImages", uploadImages);
    model.addAttribute("notice", notice);
    model.addAttribute("admin", admin);

    return "admin/section/notice_detail";
}
  • @GetMapping("/notice_detail") : /notice_detail 로 들어온 get 요청 처리.
  • @RequestParam("notice_seq") int notice_seq : notice_seq 값 추출.
  • noticeService.getNoticeById(notice_seq); : 추출한 notice_seq 값으로 공지사항을 조회 -> Notice notice 객체로 반환.
  • List<String> uploadImages = notice.getUploadedImages(); , model.addAttribute("uploadImages", uploadImages); : 조회한 notice 에 첨부 이미지 리스트를 가져와 model 로 전달.

공지사항 상세 Html (notice_detail.html)

<div class="all-container">
    <h1>공지사항 상세 보기</h1>
    <div class="notice-detail-container">
        <div class="count-box">
            <h4>
                조회수 : [[${notice.viewCount}]] &nbsp;&nbsp;&nbsp;
                좋아요수 : [[${notice.likeCount}]]
            </h4>
        </div>
        <div class="notice-detail-content">
            <input type="hidden" id="notice-seq" th:value="${notice.notice_seq}">
            <th:block th:if="${uploadImages == null or #lists.isEmpty(uploadImages)}">
            	<img th:src="@{/images/no_img.jpg}" alt="이미지 없음" style="width:200px; height:200px;">
            </th:block>
            <th:block th:unless="${uploadImages == null or #lists.isEmpty(uploadImages)}">
                <div th:each="image : ${uploadImages}">
                    <img th:src="@{${image}}" th:alt="${notice.title}" style="width:200px; height:200px;">
                </div>
            </th:block>
            <h4>작성자 : <span th:text="${notice.member.name}"></span></h4>
            <h4>제목 : <span id="notice-title" th:text="${notice.title}"></span></h4>
            <h4>내용 : <span th:text="${notice.content}"></span></h4>
            <h4>작성일 : <span th:text="${#dates.format(notice.notice_date, 'yyyy-MM-dd HH:mm')}"></span></h4>
        </div>
        <div class="btn-box">
            <button class="edit-btn" type="button" onclick="checkAndEdit()">수정</button>
            <button class="before-btn" type="button" onclick="window.location.href='/admin-notice-list'">목록</button>
            <button class="delete-btn" type="button" onclick="noticedelete()">삭제</button>
        </div>
    </div>
</div>

공지사항 수정 함수 (checkAndEdit())

<script th:inline="javascript">
	var writer = /*[[${notice.member.id}]]*/ '';
	var viewer = /*[[${admin.id}]]*/ '';
	var notice_seq = /*[[${notice.notice_seq}]]*/ 0;
	
	function checkAndEdit() {
		if(writer == viewer) {
			window.location.href = '/admin-notice-ED?notice_seq=' + notice_seq;
		} else {
			swal.fire({
				title: '공지사항 수정 실패',
				text: '작성자만 수정 가능합니다.',
				icon: 'warning',
				confirmButtonText: '확인'
			});
		}
	}
</script>
  • if(writer == viewer) : 작성자와(writer) 상세보기를 하고있는 사용자(viewer)의 id를 비교해서 일치하면, window.location.href = '/admin-notice-ED?notice_seq=' + notice_seq; url주소에 해당 공지사항의 고유번호(notice_seq)와 함께 요청.

Controller

// 공지사항 수정
@GetMapping("/admin-notice-ED")
public String editDeleteNotice(HttpSession session, Model model,
        @RequestParam("notice_seq") int notice_seq) {
    Member admin = (Member)session.getAttribute("admin");
    Notice notice = noticeService.getNoticeById(notice_seq);

    if(admin == null) {
        model.addAttribute("message", "로그인 페이지로 이동");
        model.addAttribute("text", "공지사항 수정을 위해 로그인해주세요.");
        model.addAttribute("messageType", "info");

        return "admin/admin_login";
    }
    if(!notice.getMember().getId().equals(admin.getId())) { // 작성자와 로그인유저 비교
        model.addAttribute("message", "수정 불가");
        model.addAttribute("text", "작성자만 수정할 수 있습니다.");
        model.addAttribute("messageType", "error");

        return "redirect:/admin-notice-list";
    }
    model.addAttribute("notice", notice);

    return "admin/section/notice_edit";
}
  • @RequestParam("notice_seq") int notice_seq : notice_seq 값 추출.
  • Member admin = (Member)session.getAttribute("admin"); : 로그인한 관리자 정보 저장.
  • Notice notice = noticeService.getNoticeById(notice_seq); : 추출한 notice_seq 값으로 공지사항 조회.
  • model.addAttribute("notice", notice); : 조회한 공지사항을 model로 전달.

공지사항 수정 Html (notice_edit.html)

<div class="all-container">
    <h1>공지사항 수정</h1>
    <div class="notice-edit-container">
        <form class="notice-edit-form" id="notice-edit-form" method="post" enctype="multipart/form-data">
            <input type="hidden" name="notice_seq" th:value="${notice.notice_seq}" />
            <!-- 이미지 관리 -->
            <div class="uploaded-images">
                <th:block th:if="${#lists.isEmpty(notice.uploadedImages)}">
                    <div class="uploaded-image">
                    	<img th:src="@{/images/no_img.jpg}" alt="이미지 없음" style="width:200px; height:200px;">
                    </div>
                </th:block>
                <th:block th:unless="${#lists.isEmpty(notice.uploadedImages)}">
                    <th:block th:each="image, iterStat : ${notice.uploadedImages}">
                        <div class="uploaded-image">
                            <img th:src="@{${image}}" th:alt="${notice.title}" style="width:200px; height:200px;">
                            <button type="button" th:data-notice-seq="${notice.notice_seq}"
                                th:data-index="${iterStat.index}" class="edit-imgdelete-btn"
                                onclick="deleteImage(this)">삭제
                            </button>
                        </div>
                    </th:block>
                </th:block>
            </div>
            <label>제목</label>
            <input type="text" name="title" id="title" th:value="${notice.title}" />
            <label>내용</label>
            <textarea name="content" id="content" th:text="${notice.content}"></textarea>
            <label class="custom-file-upload">
                <input type="file" name="uploadFile" id="uploadFile" multiple />
                파일 업로드
            </label>
        </form>
        <div class="edit-button">
            <button class="before-btn" type="button" onclick="window.location.href='/admin-notice-list'">목록으로</button>
            <button class="edit-btn" type="button" onclick="update_notice()">공지사항 수정</button>
    	</div>
    </div>
</div>

 -- 이미지 삭제

<button type="button" th:data-notice-seq="${notice.notice_seq}"
    th:data-index="${iterStat.index}" class="edit-imgdelete-btn"
    onclick="deleteImage(this)">삭제
</button>
  • th:data-notice-seq="${notice.notice_seq}" : 공지사항 고유번호 저장.
  • th:data-index="${iterStat.index}" : 이미지 배열에서 삭제할 이미지의 인덱스 저장.
// 이미지 삭제
function deleteImage(buttonElement) {
	const notice_seq = buttonElement.getAttribute('data-notice-seq');
	const imageIndex = buttonElement.getAttribute('data-index');
	const remainingImages = [];
	$(".uploaded-image").each(function(index, element) {
		if(index !== imageIndex) {
			remainingImages.push($(element).find("img").attr("src"));
		}
	});
	// 삭제된 이미지를 제외한 나머지 이미지를 서버로 전송
    $("#notice-edit-form").append(
        $("<input>", {
            type: "hidden",
            name: "uploadedImages",
            value: remainingImages.join(",")
        })
    );
    $.ajax({
        url: '/delete-image-notice',
        type: 'GET',
        data: { 
            notice_seq: notice_seq,
            imageIndex: imageIndex 
        },
        success: function(response) {
            swal.fire({
                title: '삭제 성공',
                text: '이미지를 성공적으로 삭제하였습니다.',
                icon: 'success',
                confirmButtonText: '확인'
            }).then((result) => {
                if(result.isConfirmed) {
                    location.reload();
                }
            });
        },
        error: function(xhr, status, error) {
            swal.fire({
                title: '삭제 실패',
                text: '이미지 삭제 중 오류가 발생했습니다.',
                icon: 'error',
                confirmButtonText: '확인'
            });
        }
    });
}
  • buttonElement.getAttribute(...); : 를 통해 데이터 추출.
  • $(".uploaded-image") : 로 업로드된 이미지 모두 순회하고, if(index !== imageIndex) {... 삭제 이미지를 제외한 나머지 이미지를 배열에 추가 => remainingImages.push($(element).find("img").attr("src")); 남아있는 이미지의 src경로를 배열에 저장.
  • $("#notice-edit-form").append( : 삭제할 이미지 정보를 서버로 전송. value: remainingImages.join(",") 데이터를 ,로 구분된 문자열로 저장.
// 공지사항 이미지 삭제
@GetMapping("/delete-image-notice")
@ResponseBody
public ResponseEntity<String> deleteImageNotice(@RequestParam("notice_seq") int notice_seq,
        @RequestParam("imageIndex") int imageIndex) {
    try {
        noticeService.deleteImage(notice_seq, imageIndex);
        return ResponseEntity.ok("이미지 삭제 성공!");
    } catch(Exception e) {
        return ResponseEntity.status(500).body("이미지 삭제 실패 : " + e.getMessage());
    }
}
  • @RequestParam("notice_seq") int notice_seq : 공지사항 고유번호
  • @RequestParam("imageIndex") int imageIndex : 삭제할 이미지 index
  • noticeService.deleteImage(notice_seq, imageIndex); : service의 메서드를 통해 해당 이미지 삭제 처리. *( 메서드는 위에 service 작성에 있어요)*

 -- 수정 

<button class="edit-btn" type="button" onclick="update_notice()">공지사항 수정</button>
// 공지사항 수정
function update_notice() {
	if($("#title").val() == "") {
		swal.fire({
			title: '제목을 입력해주세요.',
			icon: 'warning'
		});
		$("#title").focus();
		return false;
	} else if($("#content").val() == "") {
		swal.fire({
			title: '내용을 입력해주세요.',
			icon: 'warning'
		});
		$("#content").focus();
		return false;
	} else {
		swal.fire({
			title: '리뷰 수정 성공',
			icon: 'success',
			confirmButtonText: '확인'
		}).then((result) => {
			if(result.isConfirmed) {
				$("#notice-edit-form").attr("action", "/update-notice").submit();
			}
		});
	}
}

 controller 처리

// 공지사항 수정 처리
@PostMapping("/update-notice")
public String updateNotice(HttpSession session, Model model, Notice vo,
        @RequestParam("uploadFile") MultipartFile[] uploadFile,
        @RequestParam("notice_seq") int notice_seq) {
    Member admin = (Member)session.getAttribute("admin");
    Notice notice = noticeService.getNoticeById(notice_seq);


    if(admin == null) {
        model.addAttribute("message", "로그인 페이지로 이동");
        model.addAttribute("text", "공지사항 수정을 위해 로그인해주세요.");
        model.addAttribute("messageType", "info");

        return "admin/admin_login";
    }
    if(!notice.getMember().getId().equals(admin.getId())) { // 작성자와 로그인유저 비교
        model.addAttribute("message", "수정 불가");
        model.addAttribute("text", "작성자만 수정할 수 있습니다.");
        model.addAttribute("messageType", "error");

        return "redirect:/admin-notice-list";
    }
    notice.setTitle(vo.getTitle());
    notice.setContent(vo.getContent());
    // 기존 이미지 리스트 유지
    List<String> existingImages = notice.getUploadedImages();
    if(existingImages == null) {
        existingImages = new ArrayList<>();
    }
    // 새로 업로드된 이미지 추가
    if(vo.getUploadedImages() != null) {
        existingImages.addAll(vo.getUploadedImages());
    }

    // 수정된 이미지 리스트 설정
    notice.setUploadedImages(existingImages);
    // 파일 업로드
    if(uploadFile.length > 0 || !notice.getUploadedImages().isEmpty()) {
        List<String> fileUrls = new ArrayList<>();

        if(!notice.getUploadedImages().isEmpty()) {
            fileUrls.addAll(notice.getUploadedImages());
        }
        for(MultipartFile file : uploadFile) {
            if(!file.isEmpty()) {
                // 경로
                String uploadDir = "C:/ThisIsJava/SpringBootWorkspace/Book/uploads2/";
                // 파일이름 수정
                String originalName = file.getOriginalFilename();
                String fileExtension = originalName.substring(originalName.lastIndexOf("."));
                String uuid = UUID.randomUUID().toString();
                String fileName = uuid + fileExtension;

                try {
                    // 파일 저자ㅇ
                    FileUploadUtil.saveFile(uploadDir, fileName, file);
                    // url 생성
                    String fileUrl = "/uploads2/" + fileName;
                    fileUrls.add(fileUrl);
                } catch (IOException e) {
                    e.printStackTrace();
                    model.addAttribute("message", "파일 업로드 중 오류 발생");
                    model.addAttribute("messageType", "error");

                    return "redirect:/admin-notice-list";
                }
            }
        }
        notice.setUploadedImages(fileUrls);
    }
    noticeService.updateNotice(notice);
    return "redirect:/admin-notice-list";
}
  • noticeService.getNoticeById(notice_seq); 로 수정할 공지사항을 조회 => notice.setTitle(vo.getTitle()); , notice.setContent(vo.getContent()); 제목과 내용을 Notice vo 로 수정한 데이터로 설정. => noticeService.updateNotice(notice); 공지사항 수정.

++ 테스트

이미지 삭제 후 수정.
삭제 성공 메세지
DB 결과

 -- 삭제

notice_detail.html의 삭제 버튼

<button class="delete-btn" type="button" onclick="noticedelete()">삭제</button>

삭제 버튼 클릭 시 호출 함수

// 공지사항 삭제
function noticedelete() {
    const noticeSeq = document.getElementById("notice-seq").value; // 공지사항의 ID
    const noticeTitle = document.getElementById("notice-title").innerText; // 제목

    Swal.fire({
        title: '삭제 확인',
        text: `${noticeTitle} 공지사항을 삭제하시겠습니까?`,
        icon: 'warning',
        showCancelButton: true,
        confirmButtonText: '삭제',
        cancelButtonText: '취소'
    }).then((result) => {
        if (result.isConfirmed) {
            // AJAX 요청을 통해 공지사항 삭제
            $.ajax({
                url: '/notice/delete', // 삭제 요청 URL
                type: 'DELETE',
                data: { notice_seq: noticeSeq }, // 삭제할 공지사항의 ID
                success: function(response) {
                    // 삭제 성공 시
                    Swal.fire({
                        title: '삭제 완료',
                        text: '공지사항이 삭제되었습니다.',
                        icon: 'success'
                    }).then(() => {
                        location.href = '/admin-notice-list';
                    });
                },
                error: function(xhr, status, error) {
                    // 삭제 실패 시
                    Swal.fire({
                        title: '삭제 실패',
                        text: '공지사항 삭제에 실패했습니다.',
                        icon: 'error'
                    });
                }
            });
        }
    });
}

처리 Controller

// 공지사항 삭제 처리
@DeleteMapping("/notice/delete")
public ResponseEntity<String> deleteNotice(@RequestParam("notice_seq") int notice_seq) {
    try {
        noticeService.deleteNotice(notice_seq);
        return ResponseEntity.ok("공지사항 삭제 완료");
    } catch (Exception e) {
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("삭제 실패: " + e.getMessage());
    }
}
  • @RequestParam("notice_seq") int notice_seq 전달받은 notice_seqnoticeService.deleteNotice(notice_seq); 해당 공지사항을 삭제. return ResponseEntity.ok("공지사항 삭제 완료"); 

 ++ 테스트

삭제 확인 후 성공
DB 결과