반응형
Inversion of Control(제어의 역전)

- 주도권이 스프링에게 있다.
- 클래스: 설계도
- Object: 실체화가 가능한 것
               ex) 롤 챔프 누누(Class)는 게임할때 튀어나오니까 실체화가 가능하다 => Object다.
                     하지만 캐릭터(abstract Class)는 추상적인 의미다. (애쉬가 될수도 있고, 케이틀린이 될수도 있다.)
                     캐릭터는 추상적인 것이기때문에 실체화가 불가능하다, 게임에서 등장 불가능 => Object가 아니다.
- Instance: 실체화가 된것 (누누가 전장에서 돌아다니면, 게임속에서 존재하는것)

주도권이 스프링에게 있다는것은
개발자가 직접 Object를 new해서 heap 공간에 올리게 되면 해당 레퍼런스의 주소를 각각의 메소드가 관리하게된다.
예를 들어보자

Public void make(){
	User user = new User(); 
}

예컨데 user의 주소는 make() 메소드가 실행되는 순간에 메모리에 뜬다.
그래서 다른 메소드에서 user를 사용하고 싶다면 아래와 같이 또 새로 new해야할 텐데 그럼 
heap메모리에 또다른  주소가 생기게 된다. 
또한 make메소드에서의 user와 make2 메소드에서의user는 다른 존재다.(주소가 다르기 때문) 

Public void make2(){
	User user = new User(); 
}

이렇게 되면 다른 매소드에서 같은 user를 공유를 하기 힘들어진다. 

스프링는 수많은 Object들을 스캔해서 그 객체들을 heap 메모리 공간에 올려준다.
이것을 IOC라고한다. 

DI란

스프링이 IOC를 통해 Object들을 메모리에 띄웠기 때문에, 스프링이 관리하기 때문에  
스프링이 관리하는 객체들을 개발자가 원하는 모든곳(메소드)에서 가져와 쓸 수 있는것을 말한다. 
make 메소드의 user나, make2 메소드에서의 user는 같은 user다.
한마디로 싱글톤으로 관리된다. => 스프링이 오브젝트를 스캔하면 user가 딱 한번 힙메모리에 뜨고 그것을 다양한 곳에서 공유해서 쓴다. 

 

한줄 요약

Ioc: 제어의역전, 스프링이 싱글톤으로 빈을 관리하는것
Di: 인스턴스 생성시 IOC 컨터에너에서 싱글톤으로 관리하는 빈들을 변수에 의존성 주입

반응형

공통기능을 필터링처리 하면 코드를 재활용해서 훨씬 코드를 깔끔하게 쓸 수 있다. 
이 공통기능을 AOP처리할거다. 

AOP기능 구현에 앞서 dependency 를 추가해줘야한다. 

// https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-aop
implementation 'org.springframework.boot:spring-boot-starter-aop:2.4.5'

ValidationAdvice
- 유효성 검사를 공통으로 처리하는 곳
- @Aspect가 있어야 AOP를 처리할 수 있는 핸들러가 된다. 
- api쪽 advice와 그냥 advice를 만든다. 

1.
@Before(): 특정 함수 실행직전에 실행
@After(): 특정 함수 실행 직후에 실행
@Around(): 특정 함수 실행직전부터 직후에 관여

2.
-  ....web.api의 모든 컨트롤러의 모든 메소드의 모든 매개변수 가 실행될때 작동된다.
- proceedingJoinPoint : api의 Controller중에 특정 함수가 실행됐으면  proceedingJoinPoint가
그 함수의 매개변수 뿐만 아니라 내부의 모든 정보에 접근할 수 있다.
- 예를들어 profile함수가 실행되는 순간 그 함수의 모든 정보를  proceedingJoinPoint가 담고 profile함수 실행 이전에 apiAdvice가 먼저 실행된다. 
- 그 이후 return proceedingJoinPoint.proceed(); 가 실행되며 profile함수가 실행된다. 

3. 
proceedingJoinPoint.getArgs() 를 통해 함수의 매개변수에 접근해서 리스트에 담는다. 
만약에 매개변수 중에서 BindingResult라는 타입이 있으면 
-> 해당 arg를 BindingResult형으로 다운 캐스팅
-> 만약 bindingResult에 에러가 있다면 -> 유효성 검사 실행

@Component
@Aspect
public class ValidationAdvice {

    @Around("execution(* com.jghan.instaclone.web.api.*Controller.*(..))")
    public Object apiAdvice(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {

//        System.out.println("==================web api 컨트롤러===================");

        Object[] args = proceedingJoinPoint.getArgs();
        for(Object arg: args){
            if(arg instanceof BindingResult){
                BindingResult bindingResult = (BindingResult) arg;
                if (bindingResult.hasErrors()) {
                    Map<String, String> errorMap = new HashMap<>();

                    for (FieldError error : bindingResult.getFieldErrors()) {
                        errorMap.put(error.getField(), error.getDefaultMessage());
                    }
                    throw new CustomValidationApiException("유효성 검사 실패함", errorMap);
                }
            }
        }
        return proceedingJoinPoint.proceed(); //prfofile함수가 실행됨
    }

    @Around("execution(* com.jghan.instaclone.web.*Controller.*(..))")
    public Object advice(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {

        Object[] args = proceedingJoinPoint.getArgs();
        for(Object arg: args){
            if(arg instanceof BindingResult){
                BindingResult bindingResult = (BindingResult) arg;

                if (bindingResult.hasErrors()) {
                    Map<String, String> errorMap = new HashMap<>();

                    for (FieldError error : bindingResult.getFieldErrors()) {
                        errorMap.put(error.getField(), error.getDefaultMessage());

                    }
                    throw new CustomValidationException("유효성 검사 실패함", errorMap);
                }

            }
        }

        return proceedingJoinPoint.proceed();
    }
}

 

반응형

Comment.class

- Comment 와 User는 n:1관계다 (1명의 유저는 여러개의 댓글을 쓸 수 있으므로
- 또한 하나의 이미지에 여러 댓글이 달릴 수 있으므로 Comment 와 Image는 n:1이다.
- ManyToOne에서는 EAGER 가 디폴트
  =>왜냐하면 1개 댓글에는 user나 image 정보가 1개뿐이라서 join해서 가져와도 db에 무리가 없다.

@Builder
@AllArgsConstructor
@NoArgsConstructor
@Data
@Entity
public class Comment {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int id;

    @Column(length = 100, nullable = false)
    private String content;

    @JsonIgnoreProperties("{images}")
    @JoinColumn(name = "userId")
    @ManyToOne(fetch =FetchType.EAGER) 
    private User user;

    @JoinColumn(name = "imageId")
    @ManyToOne(fetch = FetchType.EAGER)
    private Image image;


    private LocalDateTime createDate;

    @PrePersist
    public void createDate(){
        this.createDate = LocalDateTime.now();
    }
}

 

CommentService.class

@RequiredArgsConstructor
@Service
public class CommentService {

    private final CommentRepository commentRepository;
    private final UserRepository userRepository;

    @Transactional
    public Comment commentSave(String content, int imageId, int userId){

        Image image = new Image();
        image.setId(imageId);
        
        User userEntity = userRepository.findById(userId).orElseThrow(()->{
            return new CustomApiException("유저 아이디를 찾을 수 없습니다.");
        });

        Comment comment = new Comment();
        comment.setContent(content);
        comment.setImage(image);
        comment.setUser(userEntity);

        return commentRepository.save(comment);


}

 

CommentApiController.class
- CommentDto commentDto만 쓰는건 x-www-urlencoded 로 받는거기 때문에
    json 데이터 받으려면  @RequestBody  붙여줘야한다.

@RequiredArgsConstructor
@RestController
public class CommentApiController {

    private final CommentService commentService;

    @PostMapping("/api/comment")
    public ResponseEntity<?> commentSave(@RequestBody CommentDto commentDto,
                                         @AuthenticationPrincipal PrincipalDetails principalDetails){

        Comment comment = commentService.commentSave(commentDto.getContent(), commentDto.getImageId(), principalDetails.getUser().getId());
        return new ResponseEntity<>(new CMRespDto<>(1, "댓글쓰기성공", comment), HttpStatus.CREATED);
    }

}

 

story.js
  - 댓글을 달고자하는 이미지의 id를 전달해줘서 클릭시 addComment() 함수가 실행되도록 한다.
- 댓글 작성하고 DB에 댓글이 저장되고, 그 내용이 storyCommentList에 append되야한다 

...

<div id="storyCommentList-${image.id}">`;

                image.comments.forEach((comment)=>{
                    item +=`<div class="sl__item__contents__comment" id="storyCommentItem-${comment.id}">
                    <p>
                        <b> ${image.user} :</b> ${comment.content}
                    </p>

                    <button onclick="deleteComment(${comment.id})">
                        <i class="fas fa-times"></i>
                    </button>
                </div>`;

                });


            item += `
         </div>

         <div class="sl__item__input">
			<input type="text" placeholder="댓글 달기..." id="storyCommentInput-${image.id}" />
             <button type="button" onClick="addComment(${image.id} )">게시</button>
         </div>

     </div>
     </div>`;
function addComment(imageId) {

   let commentInput = $(`#storyCommentInput-${imageId}`);

       let commentList = $(`#storyCommentList-${imageId}`);

       let data = {
           imageId: imageId,
          content: commentInput.val()
    };

   if (data.content === "") {
      alert("댓글을 작성해주세요!");
      return;
   }

   $.ajax({
       type:"post",
       url:"/api/comment",
       data:JSON.stringify(data),
        contentType: "application/json; charset=utf-8",
        dataType: "json"
   }).done(res=>{
       console.log("성공", res);

        let comment = res.data;

        console.log(comment);

        let content = `
          <div class="sl__item__contents__comment" id="storyCommentItem-${comment.id}">
            <p>
              <b>${comment.user.username} :</b>
              ${comment.content}
            </p>
            <button><i class="fas fa-times"></i></button>
          </div>
        `;
        commentList.prepend(content);
   }).fail(error=>{
       console.log("오류", error);
   });

   commentInput.val(""); //인풋필드를 깨끗하게 비워준다.
}

 

CommentDto

- addComment()가 전달하는 값이 imageId와 content 뿐이기에 이 데이터를 받는 dto가 필요하다. 

@Data
public class CommentDto {
    private String content;
    private int imageId;

}

 

ImageApiController의 imageStory 함수가 등록한 댓글을 뿌려줘야 하는데, 

Image.class
현재 Image 객체에는 댓글관련 정보를 들고 있지 않기 때문에 관련 컬럼을 만들어주고, 양방향 매핑을 해줘야한다. 
- 한 이미지에 여러 comments가 붙기때문에 OneToMany 어노테이션을 붙여준다.
-  이미지를 가져올때 Comment의 이미지 정보를 가져올 필요가 없으므로 JsonIgnore해준다. 

@Builder
@AllArgsConstructor
@NoArgsConstructor
@Data
@Entity
public class Image {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int id;

   ...

    //댓글
    @OrderBy("id DESC")
    @JsonIgnoreProperties({"image"})
    @OneToMany(mappedBy = "image")
    private List<Comment>comments;

   ...
}
반응형

profile.html
- profile-img-wrap story-border 를 클릭했을때 popup 메소드가 호출되면서 modal-image 가 팝업된다.
- 이 modal-image의  '사진업로드'를 클릭하면 profileImageUpload() 함수가 호출된다.
- profileImageUpload 함수에 페이지 유저의 아이디와 로그인 유저 아이디를 전달한다. 

...
<div class="profile-left">
  <div class="profile-img-wrap story-border"
       onclick="popup('.modal-image')">

    <form id="userProfileImageForm">
      <input type="file" name="profileImageFile" style="display: none;"
             id="userProfileImageInput" />
    </form>

    <img class="profile-image" src="" th:src="*{profileImageUrl}"
         onerror="this.src='/images/person.jpeg'" id="userProfileImage" />
  </div>
</div>
...


<!--프로필사진 바꾸기 모달-->
<div class="modal-image" onclick="modalImage()">
  <div class="modal">
    <p>프로필 사진 바꾸기</p>
    <button onclick="profileImageUpload()" th:onclick="|profileImageUpload('${dto.user.id}','${#authentication.principal.user.id}')|">사진 업로드</button>
    <button onclick="closePopup('.modal-image')">취소</button>
  </div>
</div>

 

profile.js
- 페이지주인 아이디와 로그인 유저 아이드를 받는다.
- 페이지 유저 아이디랑 로그인 유저가 동일하다면 자동으로 userProfileImageInput 부분을 클릭시킨다.
1. 서버에 이미지 전송
- userProfileImageForm의 0번째 요소를 찾아서  profileImageForm변수에 넣어준다. 
- ajax로 이 데이터를 전송하려면 FormData로 감싸야 한다. 
   =>profileImageForm은 폼테그 그 자체고그것을 FormData에 넣으면 값들만 담긴다고 생각하면 된다.

function profileImageUpload(pageUserId, principalId) {

   if(pageUserId != principalId){
      alert("프로필 사진을 수정할 수 없는 유저입니다.");
      return;
   }

   $("#userProfileImageInput").click();

   $("#userProfileImageInput").on("change", (e) => {
      let f = e.target.files[0];

      if (!f.type.match("image.*")) {
         alert("이미지를 등록해야 합니다.");
         return;
      }

      // 서버에 이미지를 전송
      let profileImageForm = $("#userProfileImageForm")[0];
      console.log(profileImageForm);

      // FormData 객체를 이용하면 form 태그의 필드와 그 값을 나타내는 일련의 key/value 쌍을 담을 수 있다.
      let formData = new FormData(profileImageForm);

      $.ajax({
         type: "put",
         url: `/api/user/${principalId}/profileImageUrl`,
         data: formData,
         contentType: false, // 필수 : x-www-form-urlencoded로 파싱되는 것을 방지
         processData: false,  // 필수: contentType을 false로 줬을 때 QueryString 자동 설정됨. 해제
         enctype: "multipart/form-data",
         dataType: "json"
      }).done(res=>{
         // 사진 전송 성공시 이미지 변경
         let reader = new FileReader();
         reader.onload = (e) => {
            $("#userProfileImage").attr("src", e.target.result);
         }
         reader.readAsDataURL(f); // 이 코드 실행시 reader.onload 실행됨.
      }).fail(error=>{
         console.log("오류", error);
      });


   });
}

 

UserApiController
- 사진데이터도 받기 때문에 MultipartFile도 받는데, html에서의 input 아이디와 똑같은 이름으로 받아야한다. 
- userService에 principalId와 파일정보를 넘긴다.
- 회원사진이 변경되면 세션값이 변경되야하기 때문에 변경된 userEntity를 받아서 세션변경을 해준다.

@RequiredArgsConstructor
@RestController
public class UserApiController {

    private final UserService userService;
    private final FollowService followService;


    @PutMapping("/api/user/{principalId}/profileImageUrl")
    public ResponseEntity<?> profileImageUrlUpdate(@PathVariable int principalId, MultipartFile profileImageFile,
                                                   @AuthenticationPrincipal PrincipalDetails principalDetails){
        User userEntity = userService.profileImageUrlUpdate(principalId, profileImageFile);
        principalDetails.setUser(userEntity); // 세션 변경
        return new ResponseEntity<>(new CMRespDto<>(1, "프로필사진변경 성공", null), HttpStatus.OK);
    }

..


}

 

UserService

@RequiredArgsConstructor
@Service
public class UserService {

    private final UserRepository userRepository;
    private final FollowRepositoy followRepositoy;
    private final BCryptPasswordEncoder bCryptPasswordEncoder;

    @Value("${file.path}") //application.properties에서 가져옴
    private String uploadFolder;

    @Transactional
    public User profileImageUrlUpdate(int principalId, MultipartFile profileImageFile){

        UUID uuid = UUID.randomUUID(); // uuid
        String imageFileName = uuid+"_"+profileImageFile.getOriginalFilename(); // 1.jpg
        System.out.println("이미지 파일이름 : "+imageFileName);

        Path imageFilePath = Paths.get(uploadFolder+imageFileName);

        try {
            Files.write(imageFilePath, profileImageFile.getBytes());
        } catch (Exception e) {
            e.printStackTrace();
        }

        User userEntity = userRepository.findById(principalId).orElseThrow(()->{
            // throw -> return 으로 변경
            return new CustomApiException("유저를 찾을 수 없습니다.");
        });
        userEntity.setProfileImageUrl(imageFileName);

        return userEntity;

    }
반응형

story.js
-image.likeState에 따라 이미지태그의 active 가 보일지를 정해야한다. 

function getStoryItem(image) {
    let item =
    `<div class="story-list__item">
  ...
 <button>`;
      if(image.likeState){
        item += `<i class="fas fa-heart active" id="storyLikeIcon-${image.id}" onclick="toggleLike(${image.id})"></i>`;

      }else{
        item += `<i class="far fa-heart" id="storyLikeIcon-${image.id}" onclick="toggleLike(${image.id})"></i>`;

      }

 item += `
 </button>
 
 <span class="like"><b id="storyLikeCount-${image.id}">${image.likeCount} </b>likes</span>


...

    return item

}

 

그 정보는 storyLoad()함수에서 '/api/image' api에 응답하여 done을 통해 image를 가져올때 담아가야한다. 

function storyLoad() {
    $.ajax({
        url:`/api/image`,
        ataType : "json"
    }).done(res=>{
        console.log(res);
        res.data.forEach((image) => {
            let storyItem = getStoryItem(image)
            $("#storyList").append(storyItem);

        })
    }).fail(error => {
        console.log("오류", error);

    });
}

storyLoad();

 

Image.class
- Image를 가져올때 like정보를 가져오려면 Like과의 연관관계가 필요하다. 
- OneToMany 는 lazy 로딩이 기본이므로 likes의 getter를 호출하면 이미지의 like정보를 가져올 수 있다. 
- boolean타입으로 likeState 만들어준다. 이때 @Transient 어노테이션으로 db에 컬럼생성 막아준다. 
- 또한 좋아요 수 를 구하는 likeCount를  @Transient 어노테이션으로 만들어 준다. 

@Builder
@AllArgsConstructor
@NoArgsConstructor
@Data
@Entity
public class Image {

    ...

    //이미지 좋아요 정보
    @JsonIgnoreProperties({"image"})
    @OneToMany(mappedBy = "image") //lazy가 기본
    private List<Likes>likes;
    
   @Transient //DB에 컬럼이 만들어지지 않는다.
    private boolean likeState;

    @Transient
    private int likeCount;
   ...
    }

ImageService.class
 
예컨데 '2'로 로그인 한다고 하자
-> 2가 팔로우한 계정의 이미지롤 for문으로 모두 가져오기
-> 각 이미지의 좋아요 정보를 모두 가져와서
-> 그 좋아요가 내가 좋아요 한지 판별하기(image의 like정보의 user와 principalId가 같은지 보기)
    해당 이미지에 좋아요한 사람들을 찾아서 현재 로긴한 사람이 좋아요 한것인지 비교
-> 만약에 같다면 likeState에 true값 넣기

- 또한 setLikeCount 로 좋아요 수를 images에 넣어준다.

@RequiredArgsConstructor
@Service
public class ImageService {

    private final ImageRepository imageRepository;

    @Transactional(readOnly = true)
    public List<Image>imageStory(int principalId){
        List<Image> images = imageRepository.mStory(principalId);
     
        images.forEach((image)->{

            image.setLikeCount(image.getLikes().size());

            image.getLikes().forEach((like) -> {
                if(like.getUser().getId() == principalId){ 
                    image.setLikeState(true);
                }
            });
        });

       return images;
    }
   ...
    }
}

  

story.js

// (3) 좋아요, 안좋아요
function toggleLike(imageId) {
   let likeIcon = $(`#storyLikeIcon-${imageId}`);

   if (likeIcon.hasClass("far")) { //빈하트-> LIKE하겠다

       $.ajax({
                 type: "post",
                 url: `/api/image/${imageId}/likes`,
                 dataType: "json"
              }).done(res=>{
				//b태그 내용의 text부분을 가져온다
                 let likeCountStr = $(`#storyLikeCount-${imageId}`).text(); 
                 let likeCount = Number(likeCountStr) + 1;
                 $(`#storyLikeCount-${imageId}`).text(likeCount);

                 likeIcon.addClass("fas");
                 likeIcon.addClass("active");
                 likeIcon.removeClass("far");
              }).fail(error=>{
                 console.log("오류", error);
              });

   } else {  //빨간하트 ->UNLIKE 하겠다.

        $.ajax({
                 type: "delete",
                 url: `/api/image/${imageId}/likes`,
                 dataType: "json"
              }).done(res=>{

                 let likeCountStr = $(`#storyLikeCount-${imageId}`).text();
                 let likeCount = Number(likeCountStr) - 1;
                 $(`#storyLikeCount-${imageId}`).text(likeCount);

                 likeIcon.removeClass("fas");
                 likeIcon.removeClass("active");
                 likeIcon.addClass("far");
              }).fail(error=>{
                 console.log("오류", error);
              });

   }
}

+ Recent posts