Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;
import lombok.extern.slf4j.Slf4j;

import java.util.List;

@Slf4j
@RestController
@RequiredArgsConstructor
public class CommentController {
Expand All @@ -29,21 +31,24 @@ public ResponseEntity<List<CommentResponse>> getComments(@PathVariable Long post
public ResponseEntity<?> addComment(@PathVariable Long postId,
@RequestBody CommentRequest request,
@AuthenticationPrincipal CustomUserDetails userDetails) {
String username = null;
try {
String username = userDetails.getUsername();
username = userDetails.getUsername();
Users user = usersRepository.findByUsername(username);
System.out.println("🔥 댓글 등록 시작 - postId: " + postId + ", user: " + username);

log.info("댓글 등록 시작 - postId: {}, user: {}", postId, username);

Long commentId = commentService.addComment(postId, request, user);
System.out.println("✅ 댓글 등록 완료 - commentId: " + commentId);

log.info("댓글 등록 완료 - commentId: {}", commentId);

return ResponseEntity.ok().body(commentId);
} catch (Exception e) {
System.err.println("❌ 댓글 등록 실패: " + e.getMessage());
e.printStackTrace();
return ResponseEntity.badRequest().body("댓글 등록에 실패했습니다: " + e.getMessage());
} catch (IllegalArgumentException e) {
log.warn("댓글 등록 실패 - 잘못된 요청: {}", e.getMessage());
return ResponseEntity.badRequest().body("잘못된 요청입니다: " + e.getMessage());
} catch (Exception e) {
log.error("댓글 등록 실패", e);
return ResponseEntity.badRequest().body("댓글 등록에 실패했습니다.");
}
}

Expand All @@ -60,21 +65,20 @@ public ResponseEntity<?> updateComment(@PathVariable Long commentId,
@DeleteMapping("/api/comments/{commentId}")
public ResponseEntity<?> deleteComment(@PathVariable Long commentId,
@AuthenticationPrincipal CustomUserDetails userDetails) {
String username = null;
try {
String username = userDetails.getUsername();
username = userDetails.getUsername();
Users user = usersRepository.findByUsername(username);

System.out.println("🔥 댓글 삭제 시작 - commentId: " + commentId + ", user: " + username);


log.info("댓글 삭제 시작 - commentId: {}, user: {}", commentId, username);
commentService.deleteComment(commentId, user);
System.out.println("✅ 댓글 삭제 완료 - commentId: " + commentId);

log.info("댓글 삭제 완료 - commentId: {}", commentId);

return ResponseEntity.noContent().build();
} catch (Exception e) {
System.err.println("❌ 댓글 삭제 실패: " + e.getMessage());
e.printStackTrace();
return ResponseEntity.badRequest().body("댓글 삭제에 실패했습니다: " + e.getMessage());
log.error("댓글 삭제 실패 - commentId: {}, user: {}", commentId, username, e);
return ResponseEntity.status(500).body("댓글 삭제에 실패했습니다.");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import org.springframework.data.domain.Pageable;
import org.springframework.data.web.PageableDefault;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;

Expand Down Expand Up @@ -99,7 +100,13 @@ public ResponseEntity<?> likePost(@PathVariable Long id,

// 🔥 기존 데이터 업데이트를 위한 임시 엔드포인트 (관리자용)
@PostMapping("/update-counts")
public ResponseEntity<?> updateAllPostCounts() {
@PreAuthorize("hasRole('ADMIN')")
public ResponseEntity<?> updateAllPostCounts(@AuthenticationPrincipal CustomUserDetails userDetails) {
// 관리자 권한 확인
if (!userDetails.getAuthorities().stream()
.anyMatch(a -> a.getAuthority().equals("ROLE_ADMIN"))) {
return ResponseEntity.status(403).body("관리자 권한이 필요합니다.");
}
postService.updateAllPostCounts();
return ResponseEntity.ok(Map.of("message", "모든 게시글의 댓글 수와 좋아요 수가 업데이트되었습니다."));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import lombok.Getter;
import lombok.Setter;

import javax.validation.constraints.Size;
import jakarta.validation.constraints.Size;

@Getter
@Setter
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import lombok.Getter;
import lombok.Setter;

import javax.validation.constraints.Size;
import jakarta.validation.constraints.Size;
import java.util.List;
import io.github.petty.community.dto.PostImageRequest;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;

import java.util.Optional;

Expand All @@ -21,5 +23,32 @@ public interface PostRepository extends JpaRepository<Post, Long> {

@EntityGraph(attributePaths = {"user", "images"})
Optional<Post> findWithUserAndImagesById(Long id);

@Query("""
UPDATE Post p
SET p.commentCount = (SELECT COUNT(c) FROM Comment c WHERE c.post = p),
p.likeCount = (SELECT COUNT(l) FROM PostLike l WHERE l.post = p)
""")
@Modifying
@Transactional
void updateAllPostCounts();

// 또는 더 나은 성능을 위한 네이티브 쿼리
@Query(value = """
UPDATE posts p
SET comment_count = (
SELECT COUNT(*)
FROM comments c
WHERE c.post_id = p.id
),
like_count = (
SELECT COUNT(*)
FROM post_likes pl
WHERE pl.post_id = p.id
)
""", nativeQuery = true)
@Modifying
@Transactional
void updateAllPostCountsNative();
}

Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@
import org.springframework.transaction.annotation.Transactional;

import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;

@Service
@RequiredArgsConstructor
Expand Down Expand Up @@ -70,48 +73,45 @@ public void updateImagesFromRequest(Post post, List<PostImageRequest> imageReque

// 1️⃣ 기존 이미지들 조회
List<PostImage> existingImages = postImageRepository.findByPostIdOrderByOrderingAsc(post.getId());

// 2️⃣ 삭제할 이미지들 처리
for (PostImageRequest dto : imageRequests) {
if (Boolean.TRUE.equals(dto.getIsDeleted()) && dto.getId() != null) {
// 기존 이미지 삭제
existingImages.stream()
.filter(img -> img.getId().equals(dto.getId()))
.findFirst()
.ifPresent(image -> {
postImageRepository.delete(image);
supabaseUploader.delete(image.getImageUrl());
});
}
List<Long> imagesToDelete = imageRequests.stream()
.filter(dto -> Boolean.TRUE.equals(dto.getIsDeleted()) && dto.getId() != null)
.map(PostImageRequest::getId)
.toList();

if (!imagesToDelete.isEmpty()) {
List<PostImage> deletingImages = postImageRepository.findAllById(imagesToDelete);
postImageRepository.deleteAll(deletingImages);
deletingImages.forEach(image -> supabaseUploader.delete(image.getImageUrl()));
}

// 3️⃣ flush로 삭제 작업 완료
postImageRepository.flush();

// 4️⃣ 새로운 이미지들 추가 (id가 없는 것들)
for (PostImageRequest dto : imageRequests) {
if (!Boolean.TRUE.equals(dto.getIsDeleted()) && dto.getId() == null) {
PostImage newImage = PostImage.builder()
.imageUrl(dto.getImageUrl())
.ordering(dto.getOrdering() != null ? dto.getOrdering() : 0)
.post(post)
.build();
.imageUrl(dto.getImageUrl())
.ordering(dto.getOrdering() != null ? dto.getOrdering() : 0)
.post(post)
.build();
postImageRepository.save(newImage);
}
}

// 5️⃣ 기존 이미지 순서 업데이트 (삭제되지 않은 것들만)
Map<Long, PostImage> existingImagesMap = existingImages.stream()
.collect(Collectors.toMap(PostImage::getId, Function.identity()));

for (PostImageRequest dto : imageRequests) {
if (!Boolean.TRUE.equals(dto.getIsDeleted()) && dto.getId() != null) {
postImageRepository.findById(dto.getId())
.ifPresent(image -> {
if (dto.getOrdering() != null) {
image.setOrdering(dto.getOrdering());
}
// save() 호출하지 않음 - @Transactional로 자동 저장
});
if (!Boolean.TRUE.equals(dto.getIsDeleted()) && dto.getId() != null && dto.getOrdering() != null) {
PostImage image = existingImagesMap.get(dto.getId());
if (image != null) {
image.setOrdering(dto.getOrdering());
}
}
}
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -194,21 +194,7 @@ public PostDetailResponse findById(Long id) {
@Override
@Transactional
public void updateAllPostCounts() {
// 🔥 모든 게시글의 댓글 수와 좋아요 수를 올바르게 업데이트
List<Post> allPosts = postRepository.findAll();

for (Post post : allPosts) {
// 댓글 수 업데이트
long commentCount = commentRepository.countByPostId(post.getId());
post.setCommentCount((int) commentCount);

// 좋아요 수 업데이트
long likeCount = postLikeRepository.countByPost(post);
post.setLikeCount((int) likeCount);

postRepository.save(post);
}

postRepository.updateAllPostCountsNative();
System.out.println("✅ 모든 게시글의 댓글 수와 좋아요 수가 업데이트되었습니다.");
}
}
80 changes: 69 additions & 11 deletions src/main/resources/static/js/common/edit-qna.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,34 @@ async function getCurrentUser() {
return null;
}

// 🔥 에러 메시지 표시 함수 추가
function showErrorMessage(message) {
// 기존 알림 제거
removeExistingAlerts();

const alertDiv = document.createElement('div');
alertDiv.className = 'alert alert-error';
alertDiv.innerHTML = `
<span class="alert-icon">⚠️</span>
<span class="alert-message">${message}</span>
<button class="alert-close" onclick="this.parentElement.remove()">×</button>
`;

document.body.insertBefore(alertDiv, document.body.firstChild);

// 5초 후 자동 제거
setTimeout(() => {
if (alertDiv.parentElement) {
alertDiv.remove();
}
}, 5000);
}

function removeExistingAlerts() {
const existingAlerts = document.querySelectorAll('.alert');
existingAlerts.forEach(alert => alert.remove());
}

document.addEventListener("DOMContentLoaded", async () => {
// 🔐 페이지 로드 시 권한 체크
const currentUser = await getCurrentUser();
Expand Down Expand Up @@ -70,14 +98,40 @@ document.addEventListener("DOMContentLoaded", async () => {
return;
}

const payload = {
title: document.getElementById("edit-qna-title").value,
content: document.getElementById("edit-qna-content").value,
petType: getRadioValue("edit-qna-petType") || "OTHER",
postType: postType,
isResolved: getIsResolvedValue(),
images: originalImages
};
const formData = new FormData(form);

// name 속성으로 가져오기 (권장)
const title = formData.get('title')?.trim();
const content = formData.get('content')?.trim();
const petType = formData.get('petType');
const isResolved = formData.has('isResolved');

// 🔐 필수 필드 검증
if (!title) {
showErrorMessage("제목을 입력해주세요.");
form.querySelector('[name="title"]')?.focus(); // name 속성 활용
return;
}

if (!content) {
showErrorMessage("내용을 입력해주세요.");
form.querySelector('[name="content"]')?.focus();
return;
}

if (!petType) {
showErrorMessage("반려동물 종류를 선택해주세요.");
return;
}

const payload = {
title,
content,
petType,
postType: postType,
isResolved,
images: originalImages
};

const res = await fetch(`/api/posts/${postId}`, {
method: "PUT",
Expand Down Expand Up @@ -152,10 +206,14 @@ async function fetchPostForEdit() {
const res = await fetch(`/api/posts/${postId}`);
const post = await res.json();

document.getElementById("edit-qna-title").value = post.title;
document.getElementById("edit-qna-content").value = post.content;
const titleElement = document.querySelector('[name="title"]') || document.getElementById("edit-qna-title");
const contentElement = document.querySelector('[name="content"]') || document.getElementById("edit-qna-content");

if (titleElement) titleElement.value = post.title;
if (contentElement) contentElement.value = post.content;

const petTypeInputs = document.querySelectorAll('input[name="edit-qna-petType"]');
// 🔥 수정: petType name 속성 통일 (edit-qna-petType → petType)
const petTypeInputs = document.querySelectorAll('input[name="petType"]');
petTypeInputs.forEach(input => {
if (input.value === post.petType) input.checked = true;
});
Expand Down
Loading