๐ ๏ธ ๊ฐ๋ฐ ํ๊ฒฝ
๐จ IDE : Intellij Ultimate
๐ Spring : Spring Boot 2.7.x + Spring Security
๐ฅ๏ธ View : Thymeleaf
๐ ๏ธ Java : Amazon corretto 11
Ajax๋?
Ajax๋, Asynchronous JavaScript and XML์ ์ฝ์์ด๋ค.
์ฐ๋ฆฌ๋ ๋ณดํต ์ด ๊ธฐ๋ฅ์ JS๋ฅผ ์ด์ฉํด ์๋ฒ์ ๋น๋๊ธฐ์ ์ผ๋ก ๋ฐ์ดํฐ๋ฅผ ์ฃผ๊ณ ๋ฐ๋๋ฐ ์ฌ์ฉํ๋ค.
์๋น์ ์์๋ก ๋ค์ด๋ณด์!
- ์๋์ด ๋ฐฅ์ ๋จน๊ธฐ ์ํด ์๋น์ ๋ค์ด์๋ค.
- ์๋์ ํ ๋งค๋์ ์๊ฒ ์ ์ก ๋ณถ์์ ์ฃผ๋ฌธํ๋ค.
- ํ ๋งค๋์ ๋ ์ฃผ๋ฐฉ์ฅ์๊ฒ ์ ์ก ๋ณถ์ ์ฃผ๋ฌธ์ ์๋ฆฐ๋ค.
- ์ฃผ๋ฐฉ์ฅ์ด ๋๋ฑ ๋ง๋ค์ด์ ํ ๋งค๋์ ์๊ฒ ์ ๋ฌํ๋ค.
- ํ ๋งค๋์ ๊ฐ ์๋์๊ฒ ์ ๋ฌํ๋ค.
์ฌ๊ธฐ์ ์๋์ ์ฌ์ฉ์, ํ ๋งค๋์ ๋ฅผ ์๋ฒ, ์ฃผ๋ฐฉ์ฅ์ DB๋ผ๊ณ ์๊ฐํ๋ฉด ๋๋ค!
์ฆ, JS๋ฅผ ์ด์ฉํด ์๋ฒ(Spring)๋ก ์์ฒญ์ ๋ณด๋ด๊ณ , ์ํ๋ ๊ฐ์ ๋ฐํํด์ฃผ๋ฉด ๋๋ ๊ฒ์ด๋ค.
HTML
HTML ์ฝ๋๋ ๋จ์ํ๋ค. ๋ก๊ทธ์ธํ์ง ์์ ์ ์ ๊ฐ ์ข์์ ๋ฒํผ์ ๋๋ฅด์ง ๋ชปํ๋๋ก ๊ตฌ์ฑํ์๋ค.
๋ง์ฝ ๋ฒํผ์ ๋๋ฅผ ๊ฒฝ์ฐ JS์ modifyPostLike ํจ์๋ฅผ ํธ์ถํ๋๋ก ํ์๊ณ , ํ์๋ฆฌํ ๋ฆฌํฐ๋ด์ ์ด์ฉํด ํ์ฌ ๊ฒ์๊ธ์ ๋ฒํธ๋ฅผ ๋๊ฒจ์ฃผ์๋ค.
์ด ์ธ์ ๋ถ๋ถ์ ๊ฐ์ฅ ์๋ฐ ์ฝ๋๋ฅผ ๋ณด๋ฉด ์ดํด๊ฐ ๋ ๊ฒ์ด๋ค.
<!-- ๋ก๊ทธ์ธ ๋ ์ฌ์ฉ์๋ง ๋ฒํผ์ ํด๋ฆญํ ์ ์๋๋ก ๊ตฌ์ฑ -->
<th:block sec:authorize="isAuthenticated()">
<div class="post-meta" id="post-ajax-form">
<!-- ํด๋น ๋ฒํผ์ ํด๋ฆญํ ๊ฒฝ์ฐ ํ์ฌ ๊ฒ์๊ธ์ ๋ฒํธ๋ฅผ JS์ addPostLike.modifyPostLike ํจ์๋ก ๋๊ฒจ์ค๋ค. -->
<button class="btn" id="post-react-btn" th:onclick="|addPostLike.modifyPostLike(${post.getId()})|">
<!-- likeFlag๊ฐ ์กด์ฌํ์ง ์์ ๊ฒฝ์ฐ ์ข์์๋ฅผ ๋๋ฅผ ์ ์๋ค. -->
<th:block th:if="${!likeFlag}">
<i class="icofont-thumbs-up"></i>
<span class="mx-2">์ข์์</span>
</th:block>
<!-- likeFlag๊ฐ ์กด์ฌํ ๊ฒฝ์ฐ ์ข์์๋ฅผ ์ทจ์ํ ์ ์๋ค. -->
<th:block th:if="${likeFlag}">
<i class="icofont-thumbs-up text-primary"></i>
<span class="mx-2 text-primary">์ข์์ ์ทจ์</span>
</th:block>
<!-- ํ์ฌ ์ข์์ ์๋ฅผ ํ์ -->
<span class="badge rounded-pill bg-primary text-white" th:text="${post.getPostLike().size()}"></span>
</button>
</div>
</th:block>
JavaScript
์๋ฐ ์คํฌ๋ฆฝํธ ์ฝ๋๋ ๋จ์ํ๋ค.
์ ์ ๊ฐ ๋ฒํผ์ ํด๋ฆญํ๋ฉด์ ๋๊ฒจ์ค postId๋ฅผ ๋ฐ์์จ ๋ค์, ์ด ๋ฐ์ดํฐ๋ฅผ ๋ฐ์ api ์ ๋ณด๋ฅผ ์
๋ ฅํด์ฃผ๋ฉด ๋์ด๋ค.
let addPostLike = {
modifyPostLike: function (postId) {
// ์๋ฒ์ ๋๊ฒจ์ค ๋ฐ์ดํฐ : ๊ฒ์๊ธ ์์ด๋๋ง ์ ๋ฌ
let param = {
postNum: postId
};
$.ajax({
type: "PUT",
url: "/post/modify/like",
data : param,
success: function (flag) {
// ๋ฐ์ดํฐ ์์ฒญ์ ์ฑ๊ณตํ ๊ฒฝ์ฐ
// id๊ฐ post-ajax-form์ธ div๋ง ์๋ก๊ณ ์นจ ํด์ค๋ค.
$('#post-ajax-form').load(location.href+' #post-ajax-form');
},
})
},
}
Java
Entity
์ด๋ค ์ ์ ๊ฐ ์ด๋ค ๊ฒ์๊ธ์ ์ข์์๋ฅผ ๋๋ ๋์ง ์์์ผํ๊ธฐ ๋๋ฌธ์ ์๋์ ๊ฐ์ด ๊ตฌ์ฑํ๋ค.
@Entity
@Getter
@Builder
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Like {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
private Post post;
@ManyToOne(fetch = FetchType.LAZY)
private Member member;
}
๋ฉค๋ฒ๊ฐ ํ์์ ํํดํ๊ฑฐ๋ ๊ฒ์๊ธ์ด ์ง์์ง ๊ฒฝ์ฐ, ๊ทธ์ ํด๋นํ๋ Like๋ ์ง์์ ธ์ผ๊ธฐ ๋๋ฌธ์ Member์ Post์๋ ์ฐ๊ด ๊ด๊ณ ๋งคํ์ ํด์ค๋ค.Set์ ์ฌ์ฉํ๋ ์ด์ ๋ ํ ํ์์ด ์ค๋ณต๋ ๊ฒ์๊ธ์ ์ข์์๋ฅผ ๋จ๊ธธ ์ ์๊ธฐ ๋๋ฌธ์ด๋ค.
๋ก์ง์ ์ ๊ตฌํํ๋ค๋ฉด List๋ก ์์ ํด๋ ์๊ด์๋ค!
public class Member {
@OneToMany(mappedBy = "member", cascade = CascadeType.ALL, orphanRemoval = true)
private Set<Like> postLike;
}
public class Post {
@OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true)
private Set<Like> postLike;
}
Controller
Ajax์์ ์๋์ ๊ฐ์ด ์์ฑํ๊ธฐ ๋๋ฌธ์, ํด๋น ์ ๋ณด์ ๋ง์ถฐ์ ๋ง๋ค์ด์ค๋ค.
type: "PUT",
url: "/post/modify/like",
Security๋ฅผ ์ด์ฉํด User ํด๋์ค๋ฅผ ์์๋ฐ๋ SecurityUser๋ฅผ ๊ตฌํํ๋ค๋ฉด, ์๋์ ๊ฐ์ ์ฝ๋๋ก ๊ตฌํํ๋ฉด ๋๋ค.
@RestController
@RequiredArgsConstructor
public class LikeApiController {
private final LikeApiService likeApiService;
@PutMapping("/post/modify/like")
public boolean modifyPostLike(@AuthenticationPrincipal SecurityUser securityUser,
@RequestParam Map<String, String> params) {
// Post ์ํฐํฐ์ id ํ๋๊ฐ Long์ด๊ธฐ ๋๋ฌธ์ Long์ผ๋ก ๋ณํ
Long postId = Long.valueOf(params.get("postNum"));
// ์ข์์๊ฐ ์๋ก ์๊ฒผ๋ค๋ฉด true, ๊ธฐ์กด์ ์ข์์๊ฐ ์์๋ค๋ฉด false
return likeApiService.modifyLikeStatus(securityUser.getMember(), postId);
}
}
Service
ํด๋น ๊ธฐ๋ฅ์ด ์งํ๋๋ ๋์ค ์๋ฌ๊ฐ ๋ฐ์ํ ๊ฒฝ์ฐ ๋กค๋ฐฑ์ ํด์ผํ๊ธฐ ๋๋ฌธ์ @Transactional์ ํ์ฉํ๋ค.
@Service
@RequiredArgsConstructor
public class LikeApiService {
private final LikeRepository likeRepository;
private final PostService postService;
@Transactional
public boolean modifyLikeStatus(Member member, Long id) {
// postService๋ฅผ ํตํด ์์ด๋์ ๋ํ ์ํฐํฐ ๊ฐ์ฒด๋ฅผ ๋ฐ์์ด
Post currentPost = postService.findById(id);
// DB์์ ํด๋น ํ์์ด ์ข์์๋ฅผ ๋๋ฅธ ์ํ์ธ์ง ํ์ธ
if (existLikeFlag(member, currentPost)) {
// ์ข์์๋ฅผ ๋๋ฅธ ์ํ์ผ ๊ฒฝ์ฐ
// ์ข์์ ์ทจ์๋ฅผ ์ํด DB์์ ์ญ์
Like currentPostLike = likeRepository.findByMemberAndPost(member, currentPost);
likeRepository.delete(currentPostLike);
return false;
} else {
// ์ข์์๋ฅผ ๋๋ฅด์ง ์์์ ๊ฒฝ์ฐ
// DB์ ์ ์ฅ
createNewPostLike(member, currentPost);
return true;
}
}
@Transactional(readOnly = true)
public boolean existLikeFlag(Member member, Post post) {
return likeRepository.existsByMemberAndPost(member, post);
}
@Transactional
public void createNewLike(Member member, Post currentPost) {
Like newPostLike = Like.builder()
.member(member)
.post(currentPost)
.build();
likeRepository.save(newPostLike);
}
}
Repository
public interface LikeRepository extends JpaRepository<Like, Long> {
boolean existsByMemberAndPost(Member member, Post post);
Like findByMemberAndPost(Member member, Post post);
}
๋ง๋ฌด๋ฆฌ
๋จธ๋ฆฌ์์ผ๋ก๋ ์ด๋ป๊ฒ ์ฌ์ฉํ ์ง ์ ๋ฆฌ๊ฐ ๋์์ง๋ง, ๊ธ๋ก ๋จ๊ฒจ๋์ผ๋ ค๋ ์์๊ฐ ๋ค์ฃฝ๋ฐ์ฃฝ ๋ ๊ฒ ๊ฐ๋ค.
๊ทธ๋๋ ์ด๋ฒ ๊ธฐํ์ ๋น๋๊ธฐ ํต์ ์ ๋ํด ๋ฐฐ์ธ ์ ์์ด์ ๋คํ์ด๋ผ๊ณ ์๊ฐํ๋ค.
โก๏ธ ์ฐธ๊ณ ๋ธ๋ก๊ทธ : ๊น๋ฏธ์ธ์ฝ๋ฉ