개인프로젝트/기능프로그램_오늘뭐입지

20240510_옷 출력/추가/삭제 기능

일일일코_장민기 2024. 5. 11. 00:38
728x90

어제에 이어서 유저가 본인의 옷을 등록/삭제할 수 있는 기능을 추가했다.

어려운 기능이라기 보다...entity의 관계 설정이 너무 어렵다;;

 

컨트롤러

package org.member.clothController;

import jakarta.servlet.http.HttpSession;
import lombok.extern.slf4j.Slf4j;
import org.member.ClothDTO;
import org.member.ClothVO;
import org.member.MemberDTO;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

@Slf4j
@RestController
@RequestMapping
public class ClothController {

    public final ClothService clothService;

    public ClothController(ClothService clothService){
        this.clothService = clothService;
    }

    //옷 불러오기 메서드
    @PostMapping("/selectAllClothes")
    public List<ClothDTO> selectAllClothes(HttpSession session){
        
        //전체 옷 가져오기
        List<ClothDTO> clothDTOS = clothService.selectAllClothes();

        // 현재 로그인한 유저의 옷만 출력
        MemberDTO memberDTO = (MemberDTO) session.getAttribute("memberDTO");
        List<ClothDTO> memberClothes = new ArrayList<>();
        for (ClothDTO clothDTO : clothDTOS) {
            if (clothDTO.getUserid().equals(memberDTO.getId())) {
                memberClothes.add(clothDTO);
            }
        }
        return memberClothes;
    }

    //옷 추가하기 메서드
    @PostMapping("/updateCloth")
    public void updateCloth(ClothDTO updatedCloth, HttpSession session){
        MemberDTO member = (MemberDTO) session.getAttribute("memberDTO");
            updatedCloth.setUserid(member.getId());
        clothService.updateClothes(updatedCloth);
    }

    //옷 삭제하기 메서드
    @PostMapping("/deleteCloth")
    public void deleteClothes(Integer id) {
        clothService.deleteClothes(id);
    }
}

 

서비스

package org.member.clothController;

import org.member.ClothDTO;
import org.member.ClothVO;
import org.member.MemberVO;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Service
public class ClothService {

    public final ClothRepository clothRepository;

    public ClothService(ClothRepository clothRepository) {
        this.clothRepository = clothRepository;
    }

    //옷 불러오기 메서드
    public List<ClothDTO> selectAllClothes() {
        List<ClothVO> clothVOList = new ArrayList<>(clothRepository.findAll());
        List<ClothDTO> clothDTOList = new ArrayList<>(clothVOList.size());
        for (ClothVO clothVO : clothVOList) {
              ClothDTO clothDTO = new ClothDTO();
                clothDTO.setId(clothVO.getId());
                clothDTO.setUserid(clothVO.getMemberVO().getId());
                clothDTO.setCategory(clothVO.getCategory());
                clothDTO.setClothdata(clothVO.getClothdata());
            clothDTOList.add(clothDTO);
        }
        return clothDTOList;
    }

    //옷 추가하기 메서드
    public void updateClothes(ClothDTO updatedCloth) {
        ClothVO clothVO = new ClothVO();

        MemberVO memberVO = new MemberVO();
        memberVO.setId(updatedCloth.getUserid());
        clothVO.setMemberVO(memberVO);

        clothVO.setCategory(updatedCloth.getCategory());
        clothVO.setClothdata(updatedCloth.getClothdata());

        clothRepository.save(clothVO);
    }

    //옷 삭제하기 메서드
    public void deleteClothes(Integer id) {
        clothRepository.deleteById(id);
    }

}

 

Repository

package org.member.clothController;

import org.member.ClothVO;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface ClothRepository extends JpaRepository<ClothVO, Integer>{

}

 

Entity

package org.member;

import jakarta.persistence.*;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Getter;
import lombok.Setter;
import org.hibernate.annotations.ColumnDefault;

@Getter
@Setter
@Entity
@Table(name = "clothvo")
public class ClothVO {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @ColumnDefault("nextval('clothvo_clothid_seq'")
    @Column(name = "clothid", nullable = false)
    private Integer id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "userid")
    private MemberVO memberVO;

    @Size(max = 50)
    @NotNull
    @Column(name = "category", nullable = false, length = 50)
    private String category;

    @Size(max = 100)
    @NotNull
    @Column(name = "clothdata", nullable = false, length = 100)
    private String clothdata;

}

 

DTO

package org.member;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Data;

import java.io.Serializable;

/**
 * DTO for {@link ClothVO}
 */
@Data
public class ClothDTO implements Serializable {

    @NotNull
    Integer userid;

    @Size(max = 50)
    @NotBlank(message = "카테고리가 지정되어야 합니다.")
    String category;

    @Size(max = 100)
    @NotBlank(message = "옷이 입력되어야 합니다.")
    String clothdata;

}

 

MyClothUpdatePage.jsp

<%@ page import="org.member.MemberVO" %>
<%@ page import="org.member.MemberDTO" %>
<%@ page import="org.member.ClothDTO" %>
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/functions" prefix="fn" %>

<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script src="https://code.jquery.com/jquery-3.6.4.min.js"></script>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
    <script type="text/javascript" src="<c:url value="/resources/js/MyClothUpdatePage.js" />"></script>
</head>
<body>

<h1>My Cloth Update Page</h1>
<p>
    <div id="allClothesField"></div>

    <input type="button" id="showClothField" value="옷 수정하기">
    <div id="clothField" style="display: none">
        <form id="clothSaveField">
            <input type="hidden" id="userid" value="${sessionScope.memberDTO.id}">
            <input type="text" id="cloth" name="cloth">
            <select id="categoryForCloth">
                <option value="모자">모자</option>
                <option value="겉옷">겉옷</option>
                <option value="상의">상의</option>
                <option value="하의">하의</option>
                <option value="양말">양말</option>
                <option value="신발">신발</option>
            </select>
            <input type="submit" value="확인">
        </form>
    </div>
</p>

    <form method="post" action="<c:url value="/member/logout"/>">
        <input type="submit" value="로그아웃">
    </form>
</p>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"
        integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p"
        crossorigin="anonymous"></script>
</body>
</html>

 

MyClothUpdatePage.js

$(function(){

    // 현재 옷 출력
    viewMyClothes()

    // 옷 저장 필드 토글
    $("#showClothField").on("click", function(){
        $("#clothField").toggle();
    })// 옷 저장 필드 토글

    // 옷 저장 Ajax
    $("#clothSaveField").on("submit", function(event){
        event.preventDefault();
        $.ajax({
            type: "post",
            url: "/updateCloth",
            data: {
                clothdata : $("#cloth").val(),          //무한재귀가 뜬다면 val() 입력하지 않았는지 확인
                category : $("#categoryForCloth").val()
            },
            datatype: "text",
            success: function(){
                viewMyClothes()
            },
            error: function(){
                console.log("옷 저장 ajax 에러")
            }
        })
    })// 옷 저장 Ajax

    //옷 삭제 ajax
    $(document).on("click", ".deleteEvent", function (){
        var deleteClothID = $(this).data("id");
        console.log(deleteClothID);

        $.ajax({
            type: "post",
            url: "/deleteCloth",
            data: {id: deleteClothID},
            success: function(){
                viewMyClothes()
            },
            error: function(){
                console.log("옷 삭제 ajax 에러")
            }
        })
    })

})//$(function()

function viewMyClothes(){
    var allClothesField = $("#allClothesField")
    $.ajax({
        type: "post",
        url: "/selectAllClothes",
        success: function(response){
            var tableHtml = "<table>";
            tableHtml += "<tr><th>Category</th><th>Cloth</th><th>삭제</th></tr>";
            response.forEach(function(cloth){
                tableHtml += "<tr>";
                tableHtml +=    "<td>" + cloth.category + "</td>";
                tableHtml +=    "<td>" + cloth.clothdata + "</td>";
                tableHtml +=    "<td class='deleteEvent' data-id='" + cloth.id + "'>" + "x" + "</td>";
                tableHtml += "</tr>";
            });
            tableHtml += "</table>";

            // 생성된 표를 화면에 출력
            allClothesField.html(tableHtml);
        },
        error: function(){
            console.log("저장된 옷 불러오기 실패")
        }
    })
}

- 해당 작업을 하는 도중, ajax data 속성의 데이터값의 val() 미표기로 인한 무한재귀 문제가 발생했다.

--> val() 표기를 통해 해결

더보기

무한재귀가 발생한 상황

data: {
                clothdata : $("#cloth").val(),          
                category : $("#categoryForCloth").val()
},
에서 val()이 빠져 있었음

+ 데이터가 DB에 저장되지 않음

--> 출력 이전에 무한재귀에 빠진 상태

 

저장이 실패되는 오류가 뜨는게 아니라 무한재귀가 발생된 이유???

- val() 함수를 사용하지 않으면 jQuery는 해당 요소를 계속해서 참조하려고 시도하여 무한재귀가 발생하기 때문
- $("#cloth")는 jQuery 객체를 반환하며, 이 객체는 DOM 요소를 포함하고 있음

- 그러나 $("#cloth") 자체에 대해 다시 .val()을 호출하지 않고 그냥 $("#cloth")를 보내면 jQuery는 이를 자체적으로 평가하려고 시도

--> jQuery는 여전히 $("#cloth")를 평가하기 위해 자체를 호출하려고 하며, 이는 계속해서 재귀 호출을 발생
==> val() 함수를 사용하여 값을 명시적으로 추출하지 않으면 이러한 무한 재귀가 발생

 

- 또한 동적 생성과 초기 생성으로 인한 문제가 발생했다

--> 현재 표는 동적으로 재생성됨(옷 추가 / 삭제 시 새로 표를 불러옴)

--> 새로 생긴 옷에 대한 삭제 기능 추가가 필요

- 해당 문제는 이전 익명게시판의 댓글/대댓글 기능을 구현할 때 사용했던 방식을 통해 해결했다.

$(".deleteEvent").on("click", function ()
-->
$(document).on("click", ".deleteEvent", function ()

두 코드의 차이

더보기
$(document).on("click", ".deleteEvent", function ()
- 이벤트 위임을 사용하여 상위 요소에 이벤트 핸들러를 연결
- 동적으로 생성된 요소에 대해 일반적으로 사용

$(".deleteEvent").on("click", function ()
- 페이지 로드 시에 .deleteEvent 클래스를 가진 모든 요소에 이벤트 핸들러가 연결
- 동적으로 생성된 요소에는 이벤트가 자동으로 연결되지 않기 때문에 이 방법은 초기에 존재하는 정적인 요소에 대해서만 유효

 

 

# 개선점

- 옷 출력 메서드의 비효율성

--> 현재 전체 옷 목록을 가져온 뒤, 다시 개인의 옷을 출력한다는 비효율적인 코드 구성

==> 가져올 때부터 현재 로그인한 유저의 옷만 가져올 수 있도록 개선이 필요(jpa 코드)

 

- ClothVO가 MemberVO의 id값을 참조하고 있음을 나타내기 위해 MemberVO memberVO가 들어가야 하는듯? 하다

--> jpa 사용에 대해 미숙해서 제대로 왜 사용하는지 파악이 부족함

==> 

MemberVO memberVO = new MemberVO();
        memberVO.setId(updatedCloth.getUserid());
        clothVO.setMemberVO(memberVO);

이런 식으로 사용해서 VO를 구성하는 방법이 타당한가? 에 대한 의문

필요에 따라서 개선 필요

 

- N+1 문제가 발생하는가?

--> 현재 해당 문제 디버깅까지 확인 못함

==> 추가 디버깅 작업을 통해 문제가 발생할 경우 해결 필수(fetch join 사용)