INTP의 멋대로 개발 세상

[📚상품 구매 사이트 4단계] MySQL DB 연결 없이 관리자 페이지 구현하기 - 4. 관리자 상품 등록하기, 수정하기, 삭제하기 본문

KDT 풀스택 국비 과정/파이널 프로젝트(미니)

[📚상품 구매 사이트 4단계] MySQL DB 연결 없이 관리자 페이지 구현하기 - 4. 관리자 상품 등록하기, 수정하기, 삭제하기

인팁구름 2023. 4. 26. 16:58

 

유저와 관리자로 권한을 나누었다.

상품을 등록, 수정, 삭제하는 기능은 관리자에게만 주어져야 하기 때문에

컨트롤러에서 해당 기능마다 principal의 role을 체크해 주어야 한다.

이전 단계에서 JSP 화면 상으로는 권한 구분을 해 놓았지만

URL이나 포스트맨 등을 이용해서 접근할 수도 있기 때문에

컨트롤러에서도 한 번 더 막아주는 차원!

 

 

판매자의 기능은 1,2단계에 구현되어 있는 걸 가져왔다.

단계를 거쳐오면서 바뀐 데이터의 이름과 권한 체크 코드만 추가되었다.

 

⏬ 구매자 기능 구현 보러가기 ⏬

 

 

GitHub - JungminK1m/Springboot-Product-Study-V1-V2

Contribute to JungminK1m/Springboot-Product-Study-V1-V2 development by creating an account on GitHub.

github.com

 


 

📺 화면 설계📺

 

productSave.jsp
(상품 등록 페이지)

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
    <%@ include file="../layout/header.jsp" %>

        <div class="container">
            <form action="/product/save" method="post">
                <div class="mb-3 mt-3">
                    상품명 : <input id="name" name="productName" type="text" placeholder="상품명을 적어주세요">
                    <button id="CheckproductName" type="button">중복확인</button>

                </div>

                <div class="mb-3 mt-3">
                    상품가격 : <input id="price" name="productPrice" type="text" placeholder="상품 가격을 적어주세요">
                </div>

                <div class="mb-3 mt-3">
                    상품수량 : <input id="qty" name="productQty" type="text" placeholder="상품 수량을 적어주세요">
                </div>

                <button id="submit" type="submit" class="btn btn-primary">상품등록완료</button>
            </form>
        </div>

        <script>

            // 중복체크 여부 = false - 아직 체크 안했으니까
            let sameCheck = false;

            // 상품명 중복체크
            $('#CheckproductName').on('click', function () {

                // 이렇게 데이터를 변수로 만들면 보기가 편하다
                let data = { productName: $('#name').val() }

                $.ajax({
                    url: '/productSave/checkName/',
                    type: 'post',
                    data: data,
                    contentType: "application/x-www-form-urlencoded; charset=utf-8"

                }).done((res) => {
                    alert("등록 가능한 상품입니다")
                    // 콘솔창 확인용
                    console.log(res);
                    console.log("sameCheck : " + sameCheck);
                    // 등록 가능하니까 체크 여부를 true로 변경
                    sameCheck = true;

                }).fail((err) => {
                    alert("이미 등록한 상품입니다")
                    // 콘솔창 확인용
                    console.log(err);
                    console.log("sameCheck : " + sameCheck);
                    // 등록 불가이기 때문에 중복체크를 안한 것으로 설정 (아래에 이벤트 처리를 위해)
                    sameCheck = false;
                });
            });

            // 상품명을 입력하는 input 태그에 값이 변경될 때마다 sameCheck 를 false로 설정하는 이벤트
            // => false가 됐으니 상품명을 다른 걸로 바뀌면 꼭 중복체크를 다시 해야되게 만든다.
            $('#name').on('input', function (e) {
                sameCheck = false;
                console.log(sameCheck);
            });
       
            // 동일 상품명 등록하지 못하게 처리하는 이벤트 (최종 상품 등록 버튼)
            // form이 submit 될 때 실행되는 이벤트
            $('form').on('submit', function(e) {
                // == 주의
                if (sameCheck == false) {
                    alert("상품명 중복확인을 해 주세요.");
                    // e.preventDefault(); = 브라우저가 이벤트를 처리하는 동작을 중단시키는 메서드
                    // submit 이벤트를 중단시키기 위해 사용됨
                    e.preventDefault();
                    console.log(sameCheck);
                }else if (sameCheck == true) {
                    alert("상품이 등록되었습니다.");
                    console.log(sameCheck);
                }
            });
        </script>
        <%@ include file="../layout/footer.jsp" %>

 

productDetail.jsp
(상품 상세 페이지 - 수정/삭제 버튼 부분)

 <%-- ADMIN일 때는 수정하기/삭제하기 버튼 뜨게 하기 --%>
                        <c:if test="${principal.role == 'ADMIN'}">
                            <div class="center" style="margin-top: 20px; text-align: center;">
                                <form action="/product/${product.productId}/updateForm" method="get">
                                    <button
                                        style="width: 240px; height: 50px; margin-right: 20px; background-color: rgb(255, 210, 199);">수정하기</button>
                                </form>
                                <form action="/product/${product.productId}/delete" method="post">
                                    <button
                                        style="width: 240px; height: 50px; margin: auto; background-color: rgb(250, 255, 182);">삭제하기</button>
                                </form>
                            </div>
                        </c:if>

 

productUpdate.jsp
(상품 수정 페이지)

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
    <%@ include file="../layout/header.jsp" %>

        <div class="container">
            <form action="/product/${productId}/update" method="post">
                <div class="mb-3 mt-3">
                    상품명 :
                    <input id="name" name="productName" type="text" value="${product.productName}" placeholder="상품명을 적어주세요">
                </div>
                <div class="mb-3 mt-3">
                    상품가격 :
                    <input id="price" name="productPrice" type="text" value="${product.productPrice}" placeholder="상품 가격을 적어주세요">
                </div>
                <div class="mb-3 mt-3">
                    상품수량 :
                    <input id="qty" name="productQty" type="text" value="${product.productQty}" placeholder="상품 수량을 적어주세요">
                </div>
                <button type="submit" class="btn btn-primary">상품수정완료</button>

            </form>
        </div>

    <%@ include file="../layout/footer.jsp" %>

 

ProductController.java
(상품 컨트롤러)

@Controller
public class ProductController {

    @Autowired
    private ProductRepository productRepository;

    @Autowired
    private HttpSession session;

    // 상품 목록 페이지
    @GetMapping({ "/product", "/" })
    public String productList(Model model) {

        List<Product> productList = productRepository.findAll();
        model.addAttribute("productList", productList);
        return "product/productList";
    }

    // 상품 상세 페이지
    @GetMapping("/product/{productId}")
    public String productDetail(@PathVariable Integer productId, Model model) {

        Product product = productRepository.findById(productId);
        model.addAttribute("product", product);

        return "product/productDetail";
    }

    // 상품 등록 페이지
    @GetMapping("/productSave")
    public String productSave() {

        // 관리자 로그인 한 사람만 구매할 수 있음
        User principal = (User) session.getAttribute("principal");
        if (principal == null || !principal.getRole().equals("ADMIN")) {
            throw new CustomException("관리자 로그인을 먼저 해 주세요.", HttpStatus.FORBIDDEN);
        }
        return "product/productSave";

    }

    // 상품 수정 페이지
    @GetMapping("/productUpdate")
    public String productUpdate() {

        // 관리자 로그인 한 사람만 업데이트 할 수 있음
        User principal = (User) session.getAttribute("principal");
        if (principal == null || !principal.getRole().equals("ADMIN")) {
            throw new CustomException("관리자 로그인을 먼저 해 주세요.", HttpStatus.FORBIDDEN);
        }

        return "product/productUpdate";
    }

    // 상품 등록
    @PostMapping("/product/save")
    public String save(ProductSaveDto productSaveDto) {

        // 관리자 로그인 한 사람만 상품 등록할 수 있음
        User principal = (User) session.getAttribute("principal");
        if (principal == null || !principal.getRole().equals("ADMIN")) {
            throw new CustomException("관리자 로그인을 먼저 해 주세요.", HttpStatus.FORBIDDEN);
        }

        // 새로운 상품 등록(insert)
        int result = productRepository.insert(productSaveDto);

        // result 가 1이 아니면 업데이트 안된 것
        if (result != 1) {
            throw new CustomException("상품 등록을 실패했습니다.", HttpStatus.BAD_REQUEST);
        }
        // result == 1 업데이트 성공
        return "redirect:/product";
    }

    // 상품명 중복체크 컨트롤러
    @PostMapping("/productSave/checkName")
    public ResponseEntity<?> checkProductName(@RequestParam String productName) {

        // 관리자 로그인 한 사람만 상품 수정 가능
        User principal = (User) session.getAttribute("principal");
        if (principal == null || !principal.getRole().equals("ADMIN")) {
            throw new CustomException("관리자 로그인을 먼저 해 주세요.", HttpStatus.FORBIDDEN);
        }

        // 디버깅
        System.out.println("productName : " + productName);

        // DB에 중복이 된 값이 있는 지 확인
        Product pn = productRepository.findByName(productName);

        if (pn != null) {
            // pn이 있다면 flase 반환
            return new ResponseEntity<>(false, HttpStatus.BAD_REQUEST);
        }
        // pn == null 기존에 없던 상품이기 때문에 true 반환
        return new ResponseEntity<>(true, HttpStatus.OK);
    }

    // 상품 수정 페이지
    @GetMapping("/product/{productId}/updateForm")
    public String productUpdate(@PathVariable Integer productId, Model model) {

        // 관리자 로그인 한 사람만 상품 수정 가능
        User principal = (User) session.getAttribute("principal");
        if (principal == null || !principal.getRole().equals("ADMIN")) {
            throw new CustomException("관리자 로그인을 먼저 해 주세요.", HttpStatus.FORBIDDEN);
        }

        // Product product = productRepository.findById(id);
        // model.addAttribute("product", product);
        Product product = productRepository.findById(productId);
        model.addAttribute("product", product);

        return "product/productUpdate";
    }

    // 상품 수정
    @PostMapping("/product/{productId}/update")
    public String update(@PathVariable Integer productId, Model model, ProductUpdateDto productUpdateDto) {

        // 관리자 로그인 한 사람만 상품 수정 가능
        User principal = (User) session.getAttribute("principal");
        if (principal == null || !principal.getRole().equals("ADMIN")) {
            throw new CustomException("관리자 로그인을 먼저 해 주세요.", HttpStatus.FORBIDDEN);
        }

        Product p = productRepository.findById(productId);
        model.addAttribute("product", p);

        Product product = new Product();
        product.setProductId(productUpdateDto.getProductId());
        product.setProductName(productUpdateDto.getProductName());
        product.setProductPrice(productUpdateDto.getProductPrice());
        product.setProductQty(productUpdateDto.getProductQty());

        // 업데이트
        int result = productRepository.update(product);

        if (result != 1) {
            throw new CustomException("업데이트 실패", HttpStatus.BAD_REQUEST);
        }

        // 업데이트 완료
        return "redirect:/product/" + productId;
    }

    // 상품 삭제
    @PostMapping("/product/{productId}/delete")
    public String delete(@PathVariable Integer productId) {

        // 관리자 로그인 한 사람만 상품 삭제 가능
        User principal = (User) session.getAttribute("principal");
        if (principal == null || !principal.getRole().equals("ADMIN")) {
            throw new CustomException("관리자 그인을 먼저 해 주세요.", HttpStatus.FORBIDDEN);
        }

        int result = productRepository.deleteById(productId);
        if (result != 1) {
            throw new CustomException("삭제 실패", HttpStatus.BAD_REQUEST);
        }
        return "redirect:/product";
    }

}

 

 


 

🎤 코드 리뷰 🎤

 

1. 상품 등록하기

ProductReqDto.java

@Getter
@Setter
public class ProductReqDto {

    private Integer productId;
    private String productName;
    private Integer productPrice;
    private Integer productQty;

    @Getter
    @Setter
    public static class ProductSaveDto {
        private String productName;
        private Integer productPrice;
        private Integer productQty;
    }

    @Getter
    @Setter
    public static class ProductUpdateDto {
        private Integer productId;
        private String productName;
        private Integer productPrice;
        private Integer productQty;
    }
}

 

우선 상품 등록과 상품 업데이트에 사용될 DTO들을 각각 Request Dto의 내부 클래스로 만들어 주었다.

 

 

 

상품을 등록하는 컨트롤러를 만들어 보자😮

방금 만든 productSaveDto를 매개변수로 받아준다.

우선 제일 먼저 role값이 'ADMIN'이 아닌 사람들을 모두 예외처리 해야한다.

지금 프로젝트에선 서비스를 만들지 않아서 모든 로직이 컨트롤러에 있지만, 서비스가 있다해도 권한체크에 대한 건 컨트롤러 부분에 적어야 좋다.

이해가 쉽게 되는 예시를 들자면

"애초에 들어오면 안되는 사람을 문 앞에서 막아야지 집 안에 들여보냈다가 내보내나?🤔"

 

'ADMIN' 조건을 통과하면 insert 메서드에 상품 등록 시에 입력받은 Dto 정보를 넣어준다.

 

 

productRepository
product.xml

insert 메서드는 int 타입으로 리턴하게 만들었다.

이렇게 만들면,

 

Dto 값이 잘 들어갔다면 1이라고 리턴될 것이고, 모종의 이유로 값이 제대로 들어가지 않았다면 0을 리턴해 줄 것이다.

result 값이 1이 아니라면 예외처리를 해 준다.

result가 1이라면 값이 잘 들어갔다는 뜻이므로 상품목록 페이지로 리다이렉트 해 준다.

 

productSave.jsp

JSP에서 action, method 속성 값은 컨트롤러와 맞춰주고,

input 태그의 name 값은 ProductSaveDto와 이름을 맞춰주었다.

 


1-1. 상품 등록 시 중복확인 체크 Ajax 요청

 

 

상품명을 적으면 중복체크를 하게 만드는 Ajax 요청을 만들어 보자.

왜 Ajax 요청이냐면! 중복확인 할 때마다 페이지가 새로고침 되면 불편하니까 비동기 통신으로 처리하는 것이다.

(컨트롤러에서 처리하면 매번 새로고침 되기 때문에 자바스크립트 Ajax로 처리하는 것)

 

중복체크에 대한 정리는 아래 게시글에 자세하게 적혀있다😊 2단계에 쓴 것을 고대로 가져왔다

 

 

⏬ Ajax 중복확인 체크 ⏬

 

[연습📚상품 구매 사이트 2단계] 상품명 중복체크, 이벤트 처리 Ajax 수정

GitHub - JungminK1m/Springboot-Product-Study-V1-V2 Contribute to JungminK1m/Springboot-Product-Study-V1-V2 development by creating an account on GitHub. github.com 1단계는 저번 게시글의 CRUD 구현이었다. 나는 자바스크립트 수업할 때

whiteclouds-dev.tistory.com

 

<script>

            // 중복체크 여부 = false - 아직 체크 안했으니까
            let sameCheck = false;

            // 상품명 중복체크
            $('#CheckproductName').on('click', function () {

                // 이렇게 데이터를 변수로 만들면 보기가 편하다
                let data = { productName: $('#name').val() }

                $.ajax({
                    url: '/productSave/checkName/',
                    type: 'post',
                    data: data,
                    contentType: "application/x-www-form-urlencoded; charset=utf-8"

                }).done((res) => {
                    alert("등록 가능한 상품입니다")
                    // 콘솔창 확인용
                    console.log(res);
                    console.log("sameCheck : " + sameCheck);
                    // 등록 가능하니까 체크 여부를 true로 변경
                    sameCheck = true;

                }).fail((err) => {
                    alert("이미 등록한 상품입니다")
                    // 콘솔창 확인용
                    console.log(err);
                    console.log("sameCheck : " + sameCheck);
                    // 등록 불가이기 때문에 중복체크를 안한 것으로 설정 (아래에 이벤트 처리를 위해)
                    sameCheck = false;
                });
            });

            // 상품명을 입력하는 input 태그에 값이 변경될 때마다 sameCheck 를 false로 설정하는 이벤트
            // => false가 됐으니 상품명을 다른 걸로 바뀌면 꼭 중복체크를 다시 해야되게 만든다.
            $('#name').on('input', function (e) {
                sameCheck = false;
                console.log(sameCheck);
            });
       
            // 동일 상품명 등록하지 못하게 처리하는 이벤트 (최종 상품 등록 버튼)
            // form이 submit 될 때 실행되는 이벤트
            $('form').on('submit', function(e) {
                // == 주의
                if (sameCheck == false) {
                    alert("상품명 중복확인을 해 주세요.");
                    // e.preventDefault(); = 브라우저가 이벤트를 처리하는 동작을 중단시키는 메서드
                    // submit 이벤트를 중단시키기 위해 사용됨
                    e.preventDefault();
                    console.log(sameCheck);
                }else if (sameCheck == true) {
                    alert("상품이 등록되었습니다.");
                    console.log(sameCheck);
                }
            });
        </script>

 

// 상품명 중복체크 컨트롤러
    @PostMapping("/productSave/checkName")
    public ResponseEntity<?> checkProductName(@RequestParam String productName) {

        // 디버깅
        System.out.println("productName : " + productName);

        // DB에 중복이 된 값이 있는 지 확인
        Product pn = productRepository.findByName(productName);

        if (pn != null) {
            // pn이 있다면 flase 반환
            return new ResponseEntity<>(false, HttpStatus.BAD_REQUEST);
        }
        // pn == null 기존에 없던 상품이기 때문에 true 반환
        return new ResponseEntity<>(true, HttpStatus.OK);
    }

 


 

2. 상품 수정하기

 

만들 때 가장 애먹었던 업데이트,,..😥😥

1단계 때 만든 거라 코드가 좀 지저분한 느낌이 든다.

선생님께 한 번 검사받은 코드인데 왜 내 눈엔 지저분한지 모르겠다😂 더 깔끔하게 쓸 수 있을 거 같은데..

 

 

상품 수정 페이지는 상품 상세 페이지에서 이어지므로

productId는 상세 페이지로부터 받아온다.

해당 상품의 Id 값으로 findById를 사용하여 정보를 찾는다.

이 과정이 필요 없을? 수도 있는데

 

수정 페이지로 들어왔을 때 해당 상품의 원래 내용이 적혀있으면 좋겠다 싶어서

정보를 다시 불러와 model에 담아주었다.

 

❗❗ 그리고 ❗❗

 

@PostMapping에도 권한 체크를 꼭 해주자!!! 맨 위에!!

// 관리자 로그인 한 사람만 상품 수정 가능
        User principal = (User) session.getAttribute("principal");
        if (principal == null || !principal.getRole().equals("ADMIN")) {
            throw new CustomException("관리자 로그인을 먼저 해 주세요.", HttpStatus.FORBIDDEN);
        }

 

그냥 일반적인 접근으로는 수정이 불가능하겠지만 (@GetMapping에서 이미 권한 체크를 한 차례 했기 때문에)

일반 사용자가 해당 메서드를 호출하게 된다면 DB에 올바르지 않은 값이 저장될 수 있다.

보안 측면에서 @PostMapping 또한 권한 체크를 하는 것이 좋다.

@PostMapping은 클라이언트에서 서버로 데이터를 전송하는 역할을 하므로 이 과정에서 데이터의 무결성과 보안을 유지해야 하기 때문이다.

일반 사용자가 불법적으로 데이터를 조작하는 것을 막아야 한다!!⭐⭐

 

 

새로운 Product 객체를 만들어 주고 수정 페이지에서 입력 받은 Dto를 set 해 준다.

그렇게 정보를 담은 product를 update 메서드에 넣어준다.

(아니 근데 지금보니 이건 정보 업데이트가 아니라 그냥 새 객체 만든거잖아 ㅠㅠ..)

 

productRepository

update 메서드는 Product 객체를 매개변수로 가져서 저렇게 만들었나..?💦💦💦

여튼, update 메서드도 insert와 같이 int 타입을 반환한다.

컨트롤러에서 int result로 받아주었다.

 

product.xml

 

아까와 마찬가지로 업데이트가 잘 되었다면 result의 값이 1일 것이고 아니면 0일 것이다.

1이 아닐 시에 예외처리를 해 준다.

1이 잘 들어갔다면 해당 상품의 상세 페이지로 리다이렉트 해 준다.

 

 

JSP에서 form 태그의 action 속성과 method 속성을 컨트롤러와 동일하게 해 주고,

name 값은 DTO의 값,

value는 아까 미리 입력되어 있으면 좋겠다고 model 객체에 담은 정보이다.

 


 

3. 상품 삭제하기

 

 

상품 삭제의 productId 역시 상품 상세 페이지에서 연결되는 것이므로 상세 페이지에서 가져온 값이다!

해당 Id 값을 deleteById 메서드에 넣어주자.

 

productRepository
product.xml

 

product 테이블에서 해당 id의 정보를 삭제한다.

 

// 관리자 로그인 한 사람만 상품 삭제 가능
        User principal = (User) session.getAttribute("principal");
        if (principal == null || !principal.getRole().equals("ADMIN")) {
            throw new CustomException("관리자 로그인을 먼저 해 주세요.", HttpStatus.FORBIDDEN);
        }

 

권한 체크 꼭 넣기!!!

GetMapping에서 막아줬으니 괜찮다 생각하지 말고 꼼꼼히 다 넣어주자!!

불법적으로 접근할 방법은 내가 아는 것 보다 많을 수도!

Comments