[COMMUNITY] community 에러 고침 및 사용자 편의성 개선 기여#41
[COMMUNITY] community 에러 고침 및 사용자 편의성 개선 기여#41usn757 merged 3 commits intoPETTY-HUB:mainfrom LimPark996:main
Conversation
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the You can disable this status message by setting the Summary by CodeRabbit
Walkthrough이 변경사항은 PETTY 커뮤니티 시스템의 인증, 권한, 입력 검증, UI 피드백 및 관리 기능을 전반적으로 강화합니다. 쿠키 기반 인증으로 전환하고, 입력 필드 검증을 추가하며, 댓글·게시글 관련 엔티티와 서비스에 유효성 검사와 트랜잭션 처리를 도입했습니다. 관리용 디버그 페이지와 UI 개선도 포함됩니다. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant Browser
participant Server
User->>Browser: 페이지 접속 (예: post-detail.html)
Browser->>Server: /api/users/me (쿠키 포함)
Server-->>Browser: 로그인 사용자 정보 or 401
alt 로그인 성공
Browser->>Server: 게시글/댓글 API 요청 (쿠키 포함)
Server-->>Browser: 데이터 반환
Browser->>User: 소유자 여부/로그인 상태 따라 버튼 노출
else 미로그인
Browser->>User: 로그인 유도 메시지/버튼 비활성화
end
User->>Browser: 댓글/좋아요/수정/삭제 액션
Browser->>Server: 해당 API 요청 (쿠키 포함)
Server-->>Browser: 성공/실패 응답
Browser->>User: 알림(alert) UI로 결과 표시
Possibly related PRs
Poem
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments. CodeRabbit Commands (Invoked using PR comments)
Other keywords and placeholders
CodeRabbit Configuration File (
|
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Actionable comments posted: 26
🔭 Outside diff range comments (5)
src/main/java/io/github/petty/community/controller/CommentController.java (2)
50-58: 🛠️ Refactor suggestionupdateComment 메서드에도 예외 처리 추가 필요
다른 메서드들과 일관성을 위해
updateComment메서드에도 동일한 예외 처리와 로깅을 추가해주세요.@PutMapping("/api/comments/{commentId}") public ResponseEntity<?> updateComment(@PathVariable Long commentId, @RequestBody CommentRequest request, @AuthenticationPrincipal CustomUserDetails userDetails) { - String username = userDetails.getUsername(); - Users user = usersRepository.findByUsername(username); - commentService.updateComment(commentId, request, user); - return ResponseEntity.ok().build(); + try { + String username = userDetails.getUsername(); + Users user = usersRepository.findByUsername(username); + + log.info("댓글 수정 시작 - commentId: {}, user: {}", commentId, username); + + commentService.updateComment(commentId, request, user); + + log.info("댓글 수정 완료 - commentId: {}", commentId); + + return ResponseEntity.ok().build(); + } catch (Exception e) { + log.error("댓글 수정 실패 - commentId: {}, user: {}", commentId, userDetails.getUsername(), e); + return ResponseEntity.badRequest().body("댓글 수정에 실패했습니다."); + } }
50-58: 🛠️ Refactor suggestionupdateComment 메서드에도 일관된 예외 처리를 추가해주세요.
addComment와deleteComment메서드에는 예외 처리가 추가되었지만,updateComment메서드에는 동일한 처리가 누락되어 있습니다. 일관성을 위해 동일한 예외 처리 패턴을 적용해주세요.다음과 같이 예외 처리를 추가해보세요:
@PutMapping("/api/comments/{commentId}") public ResponseEntity<?> updateComment(@PathVariable Long commentId, @RequestBody CommentRequest request, @AuthenticationPrincipal CustomUserDetails userDetails) { + try { String username = userDetails.getUsername(); Users user = usersRepository.findByUsername(username); + + log.info("댓글 수정 시작 - commentId: {}, user: {}", commentId, username); + commentService.updateComment(commentId, request, user); + + log.info("댓글 수정 완료 - commentId: {}", commentId); + return ResponseEntity.ok().build(); + } catch (IllegalArgumentException e) { + log.warn("댓글 수정 실패 - 잘못된 요청: {}", e.getMessage()); + return ResponseEntity.badRequest().body("잘못된 요청입니다: " + e.getMessage()); + } catch (Exception e) { + log.error("댓글 수정 실패", e); + return ResponseEntity.badRequest().body("댓글 수정에 실패했습니다: " + e.getMessage()); + } }src/main/resources/static/js/common/edit-showoff.js (1)
1-249: 🛠️ Refactor suggestion심각한 코드 중복 - 공통 모듈 추출 필요
이 파일은
edit-qna.js와 거의 동일한 코드를 포함하고 있습니다. 특히 다음 함수들이 중복됩니다:
goBack()isLoggedIn()getCurrentUser()checkPostOwnership()handleImageUpload()removeImage()공통 함수를 별도 모듈로 추출하여 재사용하는 것을 강력히 권장합니다:
/js/common/auth-utils.js생성:// 인증 관련 공통 함수들 export async function getCurrentUser() { /* ... */ } export async function checkPostOwnership(postId) { /* ... */ }
/js/common/image-utils.js생성:// 이미지 관련 공통 함수들 export async function handleImageUpload(e, originalImages) { /* ... */ } export function removeImage(url, originalImages) { /* ... */ }
/js/common/navigation-utils.js생성:// 내비게이션 관련 공통 함수들 export function goBack(postId) { /* ... */ }이렇게 하면 유지보수가 훨씬 쉬워지고 버그 수정도 한 곳에서만 하면 됩니다.
src/main/resources/templates/post-detail.html (2)
593-769: 🛠️ Refactor suggestionfetchPostDetail 함수 모듈화 필요
이 함수가 176줄로 너무 길고 여러 책임을 담당하고 있습니다. 가독성과 유지보수성을 위해 기능별로 분리를 권장합니다.
함수를 다음과 같이 분리하세요:
// 1. 게시글 데이터 조회 async function fetchPost(postId) { const res = await fetch(`/api/posts/${postId}`); if (!res.ok) throw new Error("게시글 조회 실패"); return await res.json(); } // 2. 게시글 HTML 렌더링 function renderPost(post, currentUser) { // 게시글 렌더링 로직 } // 3. 좋아요 버튼 이벤트 바인딩 function bindLikeButton(postId, isLoggedIn) { // 좋아요 버튼 이벤트 처리 } // 4. 메인 함수 간소화 async function fetchPostDetail() { try { const post = await fetchPost(postId); const currentUser = await getCurrentUser(); renderPost(post, currentUser); bindLikeButton(postId, !!currentUser); bindCommentForm(!!currentUser); await loadComments(); } catch (err) { console.error(err); alert("게시글을 불러오는 데 실패했습니다."); } }
932-935:⚠️ Potential issueXSS 취약점 주의
editComment함수 호출 시comment.content를 직접 문자열에 삽입하고 있습니다. 이는 XSS 공격에 취약할 수 있습니다.HTML 이스케이프 함수를 추가하고 사용하세요:
function escapeHtml(unsafe) { return unsafe .replace(/&/g, "&") .replace(/</g, "<") .replace(/>/g, ">") .replace(/"/g, """) .replace(/'/g, "'"); } // 사용 예시 const commentActionsHtml = isCommentOwner ? ` <div class="comment-actions"> <button onclick="editComment(${comment.id}, '${escapeHtml(comment.content)}')">수정</button> <button onclick="deleteComment(${comment.id})">삭제</button> </div> ` : '';
🧹 Nitpick comments (18)
src/main/resources/templates/index.html (1)
59-60: 다른 네비게이션 링크에도 일관성 있는 Thymeleaf 속성 적용 권장
현재 홈 링크는<a href="/">홈</a>로 정적 경로를 사용 중이므로, 템플릿 엔진 환경에서 일관된 URL 매핑을 위해<a th:href="@{/}">홈</a>처럼th:href를 적용하는 것을 권장합니다.debug_complete.html (1)
116-117: 하드코딩된 URL을 환경변수로 분리하는 것을 고려해주세요.localhost URL들이 하드코딩되어 있어 다른 개발 환경에서 사용하기 어려울 수 있습니다.
+<script> + const BASE_URL = window.location.origin || 'http://localhost:8080'; +</script>그리고 fetch 호출에서 다음과 같이 사용:
- const res = await fetch('http://localhost:8080/api/users/me', { + const res = await fetch(`${BASE_URL}/api/users/me`, {Also applies to: 147-149, 186-186
src/main/resources/templates/post-review-form.html (1)
39-43: 라디오 그룹의 필수 입력 검증 누락
<label>만 시각적으로 표시되었고,input요소에required가 없어 접근성 및 유효성 검사 누락 우려가 있습니다.최소 하나의 라디오에
required를 추가해 주세요:- <input type="radio" id="review-pet-dog" name="review-petType" value="DOG" checked> + <input type="radio" id="review-pet-dog" name="review-petType" value="DOG" checked required>src/main/resources/templates/post-qna-form.html (1)
29-33: 라디오 필수 선택 검증 추가 권장
<label>에만 시각적 표시를 했고,input요소에required가 없어 접근성·유효성 검사 누락 우려가 있습니다.최소 한 개의 라디오에
required를 지정해 주세요:- <input type="radio" id="pet-dog" name="petType" value="DOG" checked> + <input type="radio" id="pet-dog" name="petType" value="DOG" checked required>src/main/resources/templates/post-showoff-form.html (1)
29-33: 라디오 그룹에 required 속성 추가 권장
첫 번째 라디오에required를 지정해 접근성과 유효성 검사를 보강하세요.- <input type="radio" id="showoff-pet-dog" name="showoff-petType" value="DOG" checked> + <input type="radio" id="showoff-pet-dog" name="showoff-petType" value="DOG" checked required>src/main/java/io/github/petty/community/repository/PostImageRepository.java (1)
19-22: 대량 삭제 메서드 구현이 효율적입니다.게시글 ID로 관련된 모든 이미지를 한 번에 삭제하는 JPQL 쿼리가 잘 구현되었습니다.
@Modifying과@Transactional어노테이션을 사용하여 데이터 변경 작업을 안전하게 처리했습니다.다만, 레포지토리 레벨에서
@Transactional을 사용하는 것보다는 서비스 레벨에서 트랜잭션을 관리하는 것이 일반적입니다:-@Transactional @Modifying @Query("DELETE FROM PostImage p WHERE p.post.id = :postId") void deleteByPostId(Long postId);src/main/resources/static/js/common/edit-qna.js (3)
17-20: 사용되지 않는 함수 제거 권장
isLoggedIn()함수가 항상true를 반환하고 실제로 사용되지 않습니다. 혼란을 방지하기 위해 제거하는 것이 좋습니다.이 함수를 제거하고 대신
getCurrentUser()의 결과를 직접 확인하는 것을 권장합니다.
113-149: 중복된 사용자 정보 조회 개선
checkPostOwnership함수에서getCurrentUser()를 다시 호출하는데, 이미 페이지 로드 시 조회한 사용자 정보를 재사용할 수 있습니다.전역 변수로 사용자 정보를 저장하여 재사용:
+let currentUserInfo = null; // 파일 상단에 추가 -async function checkPostOwnership() { +async function checkPostOwnership(user) { try { - const [postRes, userRes] = await Promise.all([ - fetch(`/api/posts/${postId}`), - getCurrentUser() - ]); + const postRes = await fetch(`/api/posts/${postId}`); if (!postRes.ok) { alert("게시글을 찾을 수 없습니다."); location.href = "/"; return; } const post = await postRes.json(); - const currentUser = userRes; + const currentUser = user || currentUserInfo;그리고 호출 시:
-await checkPostOwnership(); +await checkPostOwnership(currentUser);
40-51: 페이지 이동 방식의 일관성 부족코드 전반에서
window.location.replace()와location.href를 혼용하고 있습니다. 일관된 방식을 사용하는 것이 좋습니다.
window.location.replace(): 히스토리에 남지 않음 (뒤로가기 불가)window.location.href: 히스토리에 남음 (뒤로가기 가능)로그인 페이지로 이동할 때는
replace를, 다른 경우는href를 사용하는 것을 권장합니다.Also applies to: 65-71, 113-149, 206-212
src/main/resources/static/js/common/edit-showoff.js (1)
2-2: 게시글 타입별 설정 분리 제안QNA와 SHOWOFF의 차이는 주로
postType과isResolved필드 유무입니다. 전체 코드를 복사하는 대신 설정 기반 접근을 제안합니다.// edit-post-common.js const POST_CONFIG = { QNA: { postType: 'QNA', fields: ['title', 'content', 'petType', 'isResolved'], redirectPath: '/posts/qna' }, SHOWOFF: { postType: 'SHOWOFF', fields: ['title', 'content', 'petType'], redirectPath: '/posts/showoff' } }; // 공통 초기화 함수 export function initEditPage(postType) { const config = POST_CONFIG[postType]; // 설정에 따라 동적으로 처리 }Also applies to: 73-79
src/main/resources/static/js/common/edit-review.js (3)
65-71: 일관된 리다이렉트 방식을 사용하세요.보안을 위한 재확인은 좋지만, 다른 곳에서는
window.location.replace를 사용하는데 여기서는location.href를 사용합니다.- location.href = "/login"; + window.location.replace("/login");
103-138: 효율적인 권한 확인 구현입니다!Promise.all을 사용한 병렬 처리가 좋습니다. 다만 리다이렉트 방식을 일관되게 사용하면 더 좋겠습니다.
- location.href = "/"; + window.location.replace("/");- location.href = "/login"; + window.location.replace("/login");- location.href = "/"; + window.location.replace("/");
187-193: 이미지 업로드 전 인증 확인이 적절합니다.다만 리다이렉트 방식의 일관성을 위해
window.location.replace를 사용하는 것이 좋겠습니다.- location.href = "/login"; + window.location.replace("/login");src/main/resources/static/js/common/form.js (2)
3-8: 사용되지 않는 함수 제거 권장
isLoggedIn()함수는 항상true를 반환하며 실제 로그인 상태를 확인하지 않습니다. 코드 전체에서 이 함수를 사용하는 곳이 없으므로 제거하거나, 실제 로그인 체크가 필요하다면getCurrentUser()를 활용한 구현으로 변경하세요.-// 🔐 로그인 체크 함수 (쿠키 기반) -function isLoggedIn() { - // HttpOnly 쿠키는 JavaScript로 직접 접근할 수 없으므로 - // 서버에 인증 상태를 확인하는 방식을 사용 - return true; // 일시적으로 true로 설정, 실제 체크는 getCurrentUser()에서 -}
28-49: 미리보기 컨테이너 검색 로직 개선 제안현재 구현은 잘 작동하지만, 하드코딩된 ID 목록은 새로운 페이지 추가 시 유지보수가 어려울 수 있습니다. 데이터 속성이나 공통 클래스를 사용하는 방식을 고려해보세요.
예시:
function findPreviewContainer() { // 공통 클래스나 데이터 속성으로 검색 const element = document.querySelector('[data-image-preview="true"]') || document.querySelector('.image-preview-container'); if (element) { console.log('✅ 미리보기 컨테이너 발견'); return element; } console.error('❌ 미리보기 컨테이너를 찾을 수 없습니다'); return null; }src/main/resources/templates/post-detail.html (2)
771-783: async/await 패턴 일관성 유지다른 함수들은 async/await을 사용하는데, 이 함수만 then 체인을 사용합니다. 일관성을 위해 async/await 패턴을 사용하세요.
-function goToEditPage(postType, postId) { - // 🔐 수정 버튼 클릭 시에도 로그인 체크 - getCurrentUser().then(currentUser => { - if (!currentUser) { - alert("로그인이 필요합니다."); - location.href = "/login"; - return; - } - - const lowerType = postType.toLowerCase(); // QNA → qna - location.href = `/posts/${lowerType}/edit?id=${postId}`; - }); -} +async function goToEditPage(postType, postId) { + // 🔐 수정 버튼 클릭 시에도 로그인 체크 + const currentUser = await getCurrentUser(); + if (!currentUser) { + alert("로그인이 필요합니다."); + location.href = "/login"; + return; + } + + const lowerType = postType.toLowerCase(); // QNA → qna + location.href = `/posts/${lowerType}/edit?id=${postId}`; +}
842-851: 인라인 이벤트 핸들러 대신 addEventListener 사용 권장인라인
onclick대신addEventListener를 사용하여 이벤트를 바인딩하는 것이 더 좋은 패턴입니다.-submitBtn.onclick = (e) => { - e.preventDefault(); - alert("로그인이 필요합니다."); - location.href = "/login"; -}; +submitBtn.addEventListener('click', (e) => { + e.preventDefault(); + alert("로그인이 필요합니다."); + location.href = "/login"; +});src/main/resources/static/css/common-styles.css (1)
415-483: 알림 시스템의 접근성 개선을 고려해보세요.알림 메시지 시스템이 시각적으로 잘 구현되었으나, 접근성 개선을 위해 ARIA 속성 추가를 권장합니다.
다음과 같은 접근성 개선사항을 고려해보세요:
.alert { position: fixed; top: 20px; right: 20px; z-index: 9999; + role: alert; + aria-live: polite; /* ... 기존 스타일 ... */ }또한 키보드 사용자를 위해 닫기 버튼에 포커스 스타일을 추가하는 것도 좋겠습니다:
.alert-close:hover { opacity: 1; } +.alert-close:focus { + opacity: 1; + outline: 2px solid var(--primary); + outline-offset: 2px; +}
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (31)
debug_complete.html(1 hunks)src/main/java/io/github/petty/community/controller/CommentController.java(2 hunks)src/main/java/io/github/petty/community/controller/PostController.java(4 hunks)src/main/java/io/github/petty/community/dto/CommentRequest.java(1 hunks)src/main/java/io/github/petty/community/dto/CommentResponse.java(1 hunks)src/main/java/io/github/petty/community/dto/PostDetailResponse.java(1 hunks)src/main/java/io/github/petty/community/dto/PostRequest.java(1 hunks)src/main/java/io/github/petty/community/entity/PostImage.java(2 hunks)src/main/java/io/github/petty/community/repository/PostImageRepository.java(2 hunks)src/main/java/io/github/petty/community/repository/PostLikeRepository.java(1 hunks)src/main/java/io/github/petty/community/service/CommentService.java(0 hunks)src/main/java/io/github/petty/community/service/CommentServiceImpl.java(2 hunks)src/main/java/io/github/petty/community/service/PostImageServiceImpl.java(1 hunks)src/main/java/io/github/petty/community/service/PostService.java(1 hunks)src/main/java/io/github/petty/community/service/PostServiceImpl.java(6 hunks)src/main/resources/static/css/common-styles.css(2 hunks)src/main/resources/static/js/common/edit-qna.js(6 hunks)src/main/resources/static/js/common/edit-review.js(5 hunks)src/main/resources/static/js/common/edit-showoff.js(5 hunks)src/main/resources/static/js/common/form.js(4 hunks)src/main/resources/templates/edit-qna.html(2 hunks)src/main/resources/templates/edit-review.html(2 hunks)src/main/resources/templates/edit-showoff.html(2 hunks)src/main/resources/templates/index.html(1 hunks)src/main/resources/templates/post-detail.html(11 hunks)src/main/resources/templates/post-qna-form.html(2 hunks)src/main/resources/templates/post-qna-list.html(4 hunks)src/main/resources/templates/post-review-form.html(3 hunks)src/main/resources/templates/post-review-list.html(4 hunks)src/main/resources/templates/post-showoff-form.html(2 hunks)src/main/resources/templates/post-showoff-list.html(4 hunks)
💤 Files with no reviewable changes (1)
- src/main/java/io/github/petty/community/service/CommentService.java
🧰 Additional context used
🧬 Code Graph Analysis (1)
src/main/resources/static/js/common/edit-review.js (3)
src/main/resources/static/js/common/edit-qna.js (11)
postId(4-4)res(25-27)res(152-152)res(231-235)currentUser(42-42)currentUser(128-128)currentUser(207-207)postRes(116-119)post(127-127)post(153-153)isOwner(137-137)src/main/resources/static/js/common/edit-showoff.js (10)
postId(4-4)res(25-27)res(140-140)res(209-213)currentUser(42-42)currentUser(185-185)postRes(105-108)post(116-116)post(141-141)isOwner(125-125)src/main/resources/static/js/common/form.js (6)
res(13-15)res(122-126)res(232-239)currentUser(53-53)currentUser(90-90)currentUser(181-181)
🪛 GitHub Actions: Java CI with Gradle
src/main/java/io/github/petty/community/dto/CommentRequest.java
[error] 3-3: Compilation failed: package 'jakarta.validation.constraints' does not exist. Missing import for NotBlank annotation.
[error] 12-12: Cannot find symbol: validation annotation @notblank is not recognized due to missing package.
src/main/java/io/github/petty/community/dto/PostRequest.java
[error] 4-5: Compilation failed: package 'jakarta.validation.constraints' does not exist. Missing imports for NotBlank and NotNull annotations.
[error] 15-16: Cannot find symbol: validation annotations @NotNull and @notblank are not recognized due to missing package.
src/main/java/io/github/petty/community/entity/PostImage.java
[error] 4-7: Compilation failed: package 'jakarta.validation.constraints' does not exist. Missing import dependencies for validation annotations (Max, Min, NotBlank, Size).
[error] 30-37: Cannot find symbol: validation annotations @notblank, @SiZe, @min, @max are not recognized due to missing package.
src/main/java/io/github/petty/community/controller/PostController.java
[error] 10-10: Compilation failed: package 'jakarta.validation' does not exist. Missing import for @Valid annotation.
[error] 30-50: Cannot find symbol: validation annotation @Valid is not recognized due to missing package in method parameters.
🔇 Additional comments (57)
src/main/resources/templates/index.html (1)
60-60: Thymeleafth:href적용으로 URL 동적 해석 보장
정적 경로 대신th:href="@{/posts/review}"를 사용하여 Thymeleaf 컨텍스트에서 URL을 동적으로 생성하도록 변경한 것은 올바른 접근입니다.src/main/java/io/github/petty/community/repository/PostLikeRepository.java (2)
19-19: 메서드 구현이 올바릅니다.Spring Data JPA의 네이밍 컨벤션을 올바르게 따르고 있으며, 자동으로 구현될 파생 쿼리 메서드입니다.
17-19: 코드 변경사항이 올바르게 구현되었습니다!Spring Data JPA 규약에 따른 메서드명을 사용하여 특정 게시글의 좋아요 수를 카운트하는 기능이 적절히 추가되었습니다.
src/main/resources/static/css/common-styles.css (3)
98-103: 필수 필드 스타일이 잘 구현되었습니다.필수 입력 필드를 시각적으로 명확하게 표시하는 스타일이 적절합니다. 접근성과 사용자 경험을 고려한 좋은 구현입니다.
415-483: 알림 시스템 스타일이 포괄적으로 잘 구현되었습니다.에러와 성공 상태에 대한 알림 스타일이 일관성 있게 구현되어 있고, 애니메이션과 반응형 디자인도 고려되어 있습니다. 사용자 경험을 향상시키는 좋은 구현입니다.
98-103: 필수 필드 스타일링이 잘 구현되었습니다!사용자가 필수 입력 필드를 쉽게 식별할 수 있도록 시각적 표시가 적절히 추가되었습니다.
src/main/java/io/github/petty/community/dto/CommentResponse.java (2)
14-14: DTO 필드 추가가 올바르게 구현되었습니다.
userName필드가 기존 패턴과 일관성 있게 추가되었습니다. Lombok 어노테이션을 통해 자동으로 getter가 생성되어 올바르게 동작할 것입니다.
14-14: DTO에 사용자명 필드가 적절히 추가되었습니다!댓글 응답에 사용자명 정보를 포함하기 위한 필드 추가가 올바르게 구현되었습니다.
src/main/resources/templates/edit-showoff.html (1)
11-11:Details
✅ Verification successful
goBack 함수 정의 확인 필요
onclick="goBack()"으로 백버튼 로직이 중앙화되었으므로, 해당 함수가 스크립트에서 정확히 정의·로드되는지 검증해 주세요.Also applies to: 82-82
🏁 Script executed:
#!/bin/bash # edit-showoff.js에 goBack 함수가 정의되어 있는지 확인 rg -R "function goBack" src/main/resources/static/js/common/edit-showoff.jsLength of output: 482
다음 스크립트를 실행해
static/js디렉토리 전체에서goBack함수 정의 여부를 다시 확인해 주세요:
🏁 Script executed:
#!/bin/bash # edit-showoff.js 및 전체 static/js에서 goBack 함수 정의 검색 rg "function goBack" src/main/resources/static/js rg "goBack\s*=" src/main/resources/static/js rg "window\.goBack" src/main/resources/static/jsLength of output: 360
함수 정의 확인 완료
goBack()함수가src/main/resources/static/js/common/edit-showoff.js를 포함해 공통static/js/common/디렉토리 내 여러 파일(edit-showoff.js, edit-review.js, edit-qna.js)에서 정의되고 있어, 템플릿의onclick="goBack()"호출 시점에 함수가 정상 로드됩니다.src/main/resources/templates/edit-review.html (2)
11-11: 탐색 로직 개선을 위한 좋은 변경사항입니다.
history.back()직접 호출을goBack()함수로 대체하여 탐색 로직을 중앙화한 것은 좋은 접근입니다. 이를 통해 더 안전하고 제어된 뒤로가기 기능을 제공할 수 있습니다.
92-92: 취소 버튼 탐색 로직 일관성 확보취소 버튼의
onclick핸들러도goBack()함수를 사용하도록 변경하여 전체 템플릿에서 일관된 탐색 로직을 사용하고 있습니다.src/main/resources/templates/edit-qna.html (1)
11-11: 템플릿 간 일관된 탐색 로직 적용다른 편집 템플릿들과 일관되게
history.back()직접 호출을goBack()함수로 교체한 것은 훌륭합니다. 이를 통해 모든 편집 페이지에서 통일된 사용자 경험을 제공할 수 있습니다.Also applies to: 93-93
src/main/java/io/github/petty/community/service/PostService.java (2)
15-15: 관리자용 유틸리티 메서드 추가모든 게시글의 댓글 수와 좋아요 수를 업데이트하는
updateAllPostCounts메서드가 추가되었습니다. 데이터 정합성 관리를 위한 유용한 기능입니다.
11-11:Details
✅ Verification successful
삭제 메서드 반환 타입 개선
delete메서드가 삭제된 게시글의 타입을 반환하도록 변경한 것은 좋은 개선입니다. 이를 통해 클라이언트에서 삭제 후 적절한 페이지로 리다이렉트하거나 사용자에게 피드백을 제공할 수 있습니다.메서드 시그니처 변경이 기존 호출 코드에 미치는 영향을 확인해보겠습니다:
🏁 Script executed:
#!/bin/bash # 설명: PostService.delete 메서드 호출 부분을 찾아 반환값 처리 확인 # 예상: 모든 호출에서 새로운 반환 타입을 적절히 처리하고 있는지 확인 # PostService.delete 메서드 호출 검색 rg -A 3 -B 1 "\.delete\(" --type javaLength of output: 3778
PostService.delete 반환값 처리 완료 및 영향 없음 확인
src/main/java/io/github/petty/community/controller/PostController.java에서
String postType = postService.delete(id, user);로 반환값을 적절히 할당하여 응답(Map.of("postType", postType))에 포함하고 있습니다.모든 호출 지점에서 새로운
String반환 타입이 올바르게 처리되어 있으며, 기존 로직에 영향이 없습니다. 변경 사항을 승인합니다.src/main/java/io/github/petty/community/dto/CommentRequest.java (1)
12-14: 댓글 내용 검증 로직 추가
@NotBlank와@Size(max=500)어노테이션을 통해 댓글 내용에 대한 적절한 검증을 추가한 것은 좋습니다. 빈 댓글을 방지하고 최대 길이를 제한하여 데이터 품질을 보장할 수 있습니다.🧰 Tools
🪛 GitHub Actions: Java CI with Gradle
[error] 12-12: Cannot find symbol: validation annotation @notblank is not recognized due to missing package.
src/main/resources/templates/post-qna-list.html (4)
60-62: 호버 효과 구현이 적절합니다.메인으로 돌아가는 링크에 호버 시 기본 브랜드 색상으로 변경되는 효과가 잘 구현되었습니다. 사용자 경험을 향상시키는 좋은 개선사항입니다.
295-304: 헤더 구조 개선이 사용자 네비게이션을 향상시킵니다.홈으로 돌아가는 링크를 아이콘과 함께 추가하여 사용자의 네비게이션 편의성을 크게 개선했습니다. 플렉스 레이아웃을 사용한 구조도 깔끔합니다.
356-361: 카드 전체 클릭 기능이 사용자 경험을 개선합니다.QNA 아이템 전체를 클릭 가능하게 만든 것은 좋은 UX 개선입니다. 포인터 커서 스타일과 클릭 이벤트 처리가 적절합니다.
381-381: 중복 링크 제거로 접근성이 향상되었습니다.아이템 전체가 클릭 가능하므로 제목에서 개별 링크를 제거한 것은 올바른 접근입니다. 중복된 클릭 영역을 방지하여 접근성을 개선했습니다.
src/main/java/io/github/petty/community/service/CommentServiceImpl.java (3)
31-31: 사용자명 필드 추가가 기능을 확장합니다.댓글 응답에
userName필드를 추가하여 프론트엔드에서 사용자 식별이 가능해졌습니다. 이는 권한 체크나 UI 개선에 도움이 될 것입니다.
39-39: 트랜잭션 어노테이션 추가로 데이터 일관성이 보장됩니다.
addComment메서드에@Transactional어노테이션을 추가한 것은 댓글 생성과 게시글 댓글 수 업데이트가 원자적으로 처리되도록 보장합니다. 데이터 무결성을 위한 중요한 개선입니다.
50-52: 명시적 엔티티 저장으로 데이터 변경사항이 확실히 반영됩니다.댓글 수 증가 후 post 엔티티를 명시적으로 저장하는 것은 변경사항이 데이터베이스에 확실히 반영되도록 보장합니다. JPA의 더티 체킹에만 의존하지 않고 명시적으로 저장하는 것은 안전한 접근방식입니다.
src/main/java/io/github/petty/community/repository/PostImageRepository.java (1)
5-8: 필요한 어노테이션 import가 적절합니다.대량 삭제 기능을 위해
@Modifying,@Query,@Transactional어노테이션을 import한 것이 적절합니다.src/main/resources/templates/post-showoff-list.html (4)
60-62: 일관된 호버 효과 적용이 좋습니다.QNA 리스트와 동일한 호버 효과를 적용하여 전체 커뮤니티 페이지의 일관성을 유지했습니다.
319-328: 일관된 네비게이션 패턴 적용이 우수합니다.QNA 리스트와 동일한 "메인으로" 링크를 추가하여 전체 커뮤니티 섹션에서 일관된 네비게이션 경험을 제공합니다. 아이콘과 텍스트를 함께 사용한 것도 직관적입니다.
380-385: 카드 전체 클릭 기능이 사용자 경험을 향상시킵니다.자랑 게시글 카드 전체를 클릭 가능하게 만든 것은 사용자 경험을 크게 개선합니다. 포인터 커서와 클릭 이벤트 처리가 적절하며, QNA 리스트와 일관된 인터랙션 패턴을 제공합니다.
409-410: 중복 링크 제거로 접근성을 개선했습니다.카드 전체가 클릭 가능하므로 제목에서 개별 링크를 제거한 것은 올바른 접근입니다. 중복된 클릭 영역을 방지하여 접근성과 사용자 경험을 모두 개선했습니다.
src/main/resources/templates/post-review-list.html (8)
60-62: hover 효과 스타일 추가 승인메인으로 가기 링크에 hover 효과가 추가되어 사용자 경험이 개선되었습니다.
318-327: 메인 링크 추가로 네비게이션 개선메인 페이지로 돌아가는 링크가 적절한 아이콘과 함께 추가되어 사용자 편의성이 향상되었습니다. flex 레이아웃을 사용한 구성도 깔끔합니다.
379-384: 전체 카드 클릭 기능으로 UX 개선카드 전체를 클릭 가능하게 만들어 사용자 경험이 크게 개선되었습니다. 커서 스타일 변경으로 클릭 가능함을 명확히 표시한 것도 좋습니다.
408-409: 중복 링크 제거로 접근성 개선제목에서 별도 링크를 제거하고 카드 전체 클릭으로 통일한 것이 좋습니다. 중첩된 클릭 가능 요소를 방지하여 접근성이 개선되었습니다.
60-62: UI 개선 승인: 호버 효과 추가홈 링크에 호버 효과를 추가하여 사용자 경험을 개선했습니다. 일관된 브랜딩 색상을 사용하고 있어 좋습니다.
318-327: UI 개선 승인: 메인 링크 추가헤더에 메인 페이지로 돌아가는 링크를 추가하여 네비게이션이 개선되었습니다. 아이콘과 텍스트를 함께 사용하여 직관적입니다.
378-384: UI 개선 승인: 카드 전체 클릭 기능카드 전체를 클릭 가능하게 만들어 사용자 경험을 크게 개선했습니다. 커서 스타일도 적절히 설정되어 있습니다.
408-409: 일관성 개선 승인: 제목 링크 제거카드 전체가 클릭 가능하므로 제목의 개별 링크를 제거한 것이 적절합니다. 중복된 클릭 이벤트를 방지합니다.
src/main/java/io/github/petty/community/entity/PostImage.java (3)
30-40: 적절한 검증 제약 조건 설정이미지 URL과 순서 필드에 대한 검증 제약 조건이 잘 설정되었습니다. URL 길이 제한(500자)과 순서 범위(0-4)가 비즈니스 요구사항에 적합합니다.
의존성 문제가 해결되면 이 검증 로직들이 올바르게 작동할 것입니다.
🧰 Tools
30-33: 검증 로직 승인: 이미지 URL 제약 조건이미지 URL에 대한 적절한 검증 제약 조건이 추가되었습니다. 500자 제한과 필수 필드 검증이 합리적입니다.
🧰 Tools
36-37: 검증 로직 승인: 순서 제약 조건이미지 순서에 대한 0-4 범위 제약이 적절합니다. 비즈니스 로직과 일치합니다.
src/main/java/io/github/petty/community/dto/PostRequest.java (2)
18-27: 필드별 검증 제약 조건 적절히 설정각 필드에 대한
@Size제약 조건이 적절하게 설정되었습니다. 제목(100자), 내용(2000자), 펫 이름(50자), 지역(100자) 제한이 합리적입니다.다만 필수 필드들에 대해서는
@NotBlank검증도 추가하는 것을 고려해보세요:+@NotBlank(message = "제목은 필수입니다") @Size(max=100) private String title; +@NotBlank(message = "내용은 필수입니다") @Size(max=2000) private String content;
18-29: 검증 로직 승인: 필드 수준 크기 제약각 필드에 대한 적절한 크기 제약 조건이 설정되었습니다. 비즈니스 요구사항에 맞는 합리적인 제한입니다.
src/main/java/io/github/petty/community/service/PostImageServiceImpl.java (1)
88-89: flush() 호출의 필요성 검토
@Transactional환경에서는 트랜잭션 종료 시 자동으로 flush가 발생합니다. 명시적인 flush()는 특별한 이유가 없다면 불필요합니다.flush()가 꼭 필요한 특별한 이유가 있나요? 없다면 제거하는 것을 권장합니다.
src/main/resources/static/js/common/edit-review.js (2)
6-15: 안전한 네비게이션 구현이 잘 되었습니다!
replace를 사용하여 브라우저 히스토리를 깔끔하게 관리하는 좋은 접근입니다.
40-51: 페이지 로드 시 인증 체크가 적절합니다!로그인 확인과 게시글 소유권 확인이 순차적으로 잘 구현되었습니다.
src/main/java/io/github/petty/community/controller/PostController.java (2)
30-46: 에러 처리가 개선되었습니다!try-catch 블록과 사용자 null 체크가 적절하게 추가되었습니다. 단,
@Valid어노테이션은 위의 import 문제를 해결한 후에 정상 작동할 것입니다.🧰 Tools
🪛 GitHub Actions: Java CI with Gradle
[error] 30-50: Cannot find symbol: validation annotation @Valid is not recognized due to missing package in method parameters.
59-66: 삭제 후 게시글 타입 반환이 유용합니다!삭제된 게시글의 타입을 반환하여 프론트엔드에서 적절한 리다이렉션을 할 수 있게 되었습니다.
src/main/java/io/github/petty/community/service/PostServiceImpl.java (3)
62-83: update 메서드의 저장 순서가 개선되었습니다!Post 엔티티를 먼저 저장한 후 이미지를 업데이트하는 올바른 순서로 변경되었고, 불필요한 중복 저장이 제거되었습니다.
101-103: 중요한 버그 수정입니다!좋아요 수 변경 후 Post 엔티티를 저장하여 데이터베이스에 반영하는 중요한 수정입니다. 이전에는 메모리에서만 변경되고 DB에 반영되지 않았을 가능성이 있습니다.
108-124: delete 메서드가 트랜잭션으로 보호됩니다!
@Transactional추가로 삭제 작업의 원자성이 보장되고, postType 반환으로 프론트엔드 네비게이션을 지원합니다.src/main/resources/static/js/common/form.js (5)
10-26: 적절한 인증 구현입니다!쿠키 기반 인증을 위한
getCurrentUser()함수가 올바르게 구현되었습니다.credentials: 'include'를 사용하여 쿠키를 포함하고, 401 상태 코드를 적절히 처리하고 있습니다.
51-86: 페이지 초기화 로직이 잘 구현되었습니다!로그인 체크와 이벤트 리스너 등록이 적절히 구현되었습니다.
window.location.replace()를 사용하여 히스토리 관리도 고려한 점이 좋습니다.
88-175: 이미지 업로드 로직이 견고하게 구현되었습니다!로그인 검증, 파일 크기/개수 제한, 중복 체크, 에러 처리가 모두 적절히 구현되었습니다. 사용자 친화적인 에러 메시지도 좋습니다.
177-256: 폼 제출 로직이 안전하게 구현되었습니다!필수 필드 검증, 로그인 체크, 에러 처리가 모두 적절합니다.
trim()을 사용한 공백 처리와window.location.replace()를 통한 히스토리 관리도 좋은 구현입니다.
258-306: 사용자 친화적인 알림 시스템입니다!에러와 성공 메시지를 구분하여 표시하고, 자동 제거 기능과 수동 닫기 버튼을 제공하는 점이 좋습니다. 중복 알림 방지 로직도 적절합니다.
src/main/resources/templates/post-detail.html (4)
785-830: 게시글 삭제 로직이 안전하게 구현되었습니다!로그인 검증, 사용자 확인, 에러 처리가 모두 적절합니다. 삭제 후 게시글 타입에 따른 리다이렉션 로직도 잘 구현되었습니다.
978-1018: 댓글 수정 로직이 안전하게 구현되었습니다!로그인 검증, 입력 검증, 에러 처리가 모두 적절합니다. 수정 후 화면 업데이트도 잘 처리되었습니다.
1020-1052: 댓글 삭제 및 실시간 업데이트가 잘 구현되었습니다!삭제 확인, 권한 검증, 화면 업데이트가 모두 적절합니다. 댓글 수 실시간 업데이트로 사용자 경험이 향상됩니다.
1054-1078: 실시간 통계 업데이트 기능이 훌륭합니다!좋아요와 댓글 수를 실시간으로 업데이트하여 사용자에게 즉각적인 피드백을 제공합니다. 에러 처리도 적절합니다.
| <!DOCTYPE html> | ||
| <html lang="ko"> | ||
| <head> | ||
| <meta charset="UTF-8"> | ||
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | ||
| <title>PETTY 로그인 디버깅</title> | ||
| <style> | ||
| body { | ||
| font-family: Arial, sans-serif; | ||
| max-width: 800px; | ||
| margin: 0 auto; | ||
| padding: 20px; | ||
| background-color: #f5f5f5; | ||
| } | ||
| .debug-section { | ||
| background: white; | ||
| padding: 20px; | ||
| margin-bottom: 20px; | ||
| border-radius: 8px; | ||
| box-shadow: 0 2px 4px rgba(0,0,0,0.1); | ||
| } | ||
| .status { | ||
| padding: 10px; | ||
| border-radius: 4px; | ||
| margin: 10px 0; | ||
| } | ||
| .success { background-color: #d4edda; color: #155724; } | ||
| .error { background-color: #f8d7da; color: #721c24; } | ||
| .warning { background-color: #fff3cd; color: #856404; } | ||
| button { | ||
| background-color: #FF9933; | ||
| color: white; | ||
| border: none; | ||
| padding: 10px 20px; | ||
| border-radius: 4px; | ||
| cursor: pointer; | ||
| margin: 5px; | ||
| } | ||
| button:hover { background-color: #e67e00; } | ||
| .code { | ||
| background-color: #f8f9fa; | ||
| padding: 10px; | ||
| border-radius: 4px; | ||
| font-family: monospace; | ||
| white-space: pre-wrap; | ||
| } | ||
| </style> | ||
| </head> | ||
| <body> | ||
| <h1>🔍 PETTY 로그인 상태 디버깅</h1> | ||
|
|
||
| <div class="debug-section"> | ||
| <h2>1. JWT 토큰 상태</h2> | ||
| <div id="jwtStatus"></div> | ||
| <button onclick="checkJWT()">JWT 확인</button> | ||
| <button onclick="clearJWT()">로그아웃</button> | ||
| </div> | ||
|
|
||
| <div class="debug-section"> | ||
| <h2>2. 사용자 정보 확인</h2> | ||
| <div id="userStatus"></div> | ||
| <button onclick="checkUser()">사용자 정보 확인</button> | ||
| </div> | ||
|
|
||
| <div class="debug-section"> | ||
| <h2>3. API 엔드포인트 테스트</h2> | ||
| <div id="apiStatus"></div> | ||
| <button onclick="testAPI()">API 테스트</button> | ||
| </div> | ||
|
|
||
| <div class="debug-section"> | ||
| <h2>4. 로그인 테스트</h2> | ||
| <div> | ||
| <input type="text" id="username" placeholder="사용자명" style="margin: 5px; padding: 8px;"> | ||
| <input type="password" id="password" placeholder="비밀번호" style="margin: 5px; padding: 8px;"> | ||
| <button onclick="testLogin()">로그인 테스트</button> | ||
| </div> | ||
| <div id="loginStatus"></div> | ||
| </div> | ||
|
|
||
| <script> | ||
| function checkJWT() { | ||
| const statusDiv = document.getElementById("jwtStatus"); | ||
|
|
||
| // HttpOnly 쿠키는 JavaScript로 직접 접근할 수 없음 | ||
| statusDiv.innerHTML = ` | ||
| <div class="status warning">⚠️ HttpOnly 쿠키 방식으로 변경됨</div> | ||
| <div class="code">쿠키는 JavaScript로 직접 접근할 수 없으므로 /api/users/me로 인증 상태를 확인합니다.</div> | ||
| `; | ||
|
|
||
| // 대신 사용자 정보 API로 인증 상태 확인 | ||
| checkUser(); | ||
| } | ||
|
|
||
| function clearJWT() { | ||
| // HttpOnly 쿠키는 JavaScript로 삭제할 수 없으므로 로그아웃 API 호출 | ||
| fetch('/logout', { | ||
| method: 'POST', | ||
| credentials: 'include' | ||
| }).then(() => { | ||
| document.getElementById("jwtStatus").innerHTML = ` | ||
| <div class="status warning">🗑️ 로그아웃 요청을 보냈습니다</div> | ||
| `; | ||
| checkUser(); | ||
| }).catch(err => { | ||
| document.getElementById("jwtStatus").innerHTML = ` | ||
| <div class="status error">❌ 로그아웃 실패: ${err.message}</div> | ||
| `; | ||
| }); | ||
| } | ||
|
|
||
| async function checkUser() { | ||
| const statusDiv = document.getElementById("userStatus"); | ||
|
|
||
| try { | ||
| const res = await fetch('http://localhost:8080/api/users/me', { | ||
| credentials: 'include' // 쿠키 포함 | ||
| }); | ||
|
|
||
| if (res.ok) { | ||
| const user = await res.json(); | ||
| statusDiv.innerHTML = ` | ||
| <div class="status success">✅ 사용자 정보 조회 성공 (로그인됨)</div> | ||
| <div class="code">${JSON.stringify(user, null, 2)}</div> | ||
| `; | ||
| } else if (res.status === 401) { | ||
| statusDiv.innerHTML = ` | ||
| <div class="status error">❌ 로그인되지 않음 (401)</div> | ||
| `; | ||
| } else { | ||
| statusDiv.innerHTML = ` | ||
| <div class="status error">❌ 사용자 정보 조회 실패 (${res.status})</div> | ||
| <div class="code">응답: ${await res.text()}</div> | ||
| `; | ||
| } | ||
| } catch (err) { | ||
| statusDiv.innerHTML = ` | ||
| <div class="status error">❌ 네트워크 오류</div> | ||
| <div class="code">${err.message}</div> | ||
| `; | ||
| } | ||
| } | ||
|
|
||
| async function testAPI() { | ||
| const statusDiv = document.getElementById("apiStatus"); | ||
| const tests = [ | ||
| { name: "메인 페이지", url: "http://localhost:8080/" }, | ||
| { name: "로그인 페이지", url: "http://localhost:8080/login" }, | ||
| { name: "API 상태", url: "http://localhost:8080/api/posts?type=QNA&page=0&size=5" } | ||
| ]; | ||
|
|
||
| statusDiv.innerHTML = "<div>API 테스트 중...</div>"; | ||
|
|
||
| let results = "<h3>API 테스트 결과:</h3>"; | ||
|
|
||
| for (const test of tests) { | ||
| try { | ||
| const res = await fetch(test.url); | ||
| if (res.ok) { | ||
| results += `<div class="status success">✅ ${test.name}: 성공 (${res.status})</div>`; | ||
| } else { | ||
| results += `<div class="status error">❌ ${test.name}: 실패 (${res.status})</div>`; | ||
| } | ||
| } catch (err) { | ||
| results += `<div class="status error">❌ ${test.name}: 네트워크 오류</div>`; | ||
| } | ||
| } | ||
|
|
||
| statusDiv.innerHTML = results; | ||
| } | ||
|
|
||
| async function testLogin() { | ||
| const username = document.getElementById("username").value; | ||
| const password = document.getElementById("password").value; | ||
| const statusDiv = document.getElementById("loginStatus"); | ||
|
|
||
| if (!username || !password) { | ||
| statusDiv.innerHTML = ` | ||
| <div class="status warning">⚠️ 사용자명과 비밀번호를 입력해주세요</div> | ||
| `; | ||
| return; | ||
| } | ||
|
|
||
| try { | ||
| // Spring Security LoginFilter의 기본 경로로 요청 | ||
| const res = await fetch('http://localhost:8080/login', { | ||
| method: 'POST', | ||
| headers: { | ||
| 'Content-Type': 'application/json' | ||
| }, | ||
| credentials: 'include', // 쿠키 포함 | ||
| body: JSON.stringify({ username, password }) | ||
| }); | ||
|
|
||
| if (res.ok) { | ||
| const result = await res.json(); | ||
| statusDiv.innerHTML = ` | ||
| <div class="status success">✅ 로그인 성공!</div> | ||
| <div class="code">쿠키에 토큰이 저장되었습니다</div> | ||
| <div class="code">${JSON.stringify(result, null, 2)}</div> | ||
| `; | ||
|
|
||
| // 로그인 후 사용자 정보 자동 확인 | ||
| setTimeout(() => { | ||
| checkJWT(); | ||
| checkUser(); | ||
| }, 500); | ||
| } else { | ||
| const responseText = await res.text(); | ||
| statusDiv.innerHTML = ` | ||
| <div class="status error">❌ 로그인 실패 (${res.status})</div> | ||
| <div class="code">${responseText}</div> | ||
| `; | ||
| } | ||
| } catch (err) { | ||
| statusDiv.innerHTML = ` | ||
| <div class="status error">❌ 네트워크 오류</div> | ||
| <div class="code">${err.message}</div> | ||
| `; | ||
| } | ||
| } | ||
|
|
||
| // 페이지 로드 시 자동으로 상태 확인 | ||
| window.addEventListener('DOMContentLoaded', () => { | ||
| checkJWT(); | ||
| checkUser(); | ||
| }); | ||
| </script> | ||
| </body> | ||
| </html> No newline at end of file |
There was a problem hiding this comment.
프로덕션 배포 시 보안 위험이 있습니다.
이 디버깅 페이지는 개발용으로는 유용하지만 다음과 같은 보안 우려사항이 있습니다:
- 프로덕션 환경에 배포되면 안 됩니다
- 하드코딩된 localhost URL들이 포함되어 있습니다
- 로그인 테스트 기능이 악용될 수 있습니다
다음과 같은 보안 조치를 권장합니다:
+<!-- 개발 환경에서만 사용하는 디버깅 페이지입니다. 프로덕션 배포 시 제거해야 합니다. -->
+<script>
+ // 프로덕션 환경 체크
+ if (window.location.hostname !== 'localhost' && window.location.hostname !== '127.0.0.1') {
+ document.body.innerHTML = '<h1>이 페이지는 개발 환경에서만 사용 가능합니다.</h1>';
+ }
+</script>또한 프로덕션 빌드 시 이 파일을 제외하는 설정을 추가하거나, /debug/ 경로로 이동시켜 프로덕션에서 접근을 차단하는 것을 고려해주세요.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <!DOCTYPE html> | |
| <html lang="ko"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>PETTY 로그인 디버깅</title> | |
| <style> | |
| body { | |
| font-family: Arial, sans-serif; | |
| max-width: 800px; | |
| margin: 0 auto; | |
| padding: 20px; | |
| background-color: #f5f5f5; | |
| } | |
| .debug-section { | |
| background: white; | |
| padding: 20px; | |
| margin-bottom: 20px; | |
| border-radius: 8px; | |
| box-shadow: 0 2px 4px rgba(0,0,0,0.1); | |
| } | |
| .status { | |
| padding: 10px; | |
| border-radius: 4px; | |
| margin: 10px 0; | |
| } | |
| .success { background-color: #d4edda; color: #155724; } | |
| .error { background-color: #f8d7da; color: #721c24; } | |
| .warning { background-color: #fff3cd; color: #856404; } | |
| button { | |
| background-color: #FF9933; | |
| color: white; | |
| border: none; | |
| padding: 10px 20px; | |
| border-radius: 4px; | |
| cursor: pointer; | |
| margin: 5px; | |
| } | |
| button:hover { background-color: #e67e00; } | |
| .code { | |
| background-color: #f8f9fa; | |
| padding: 10px; | |
| border-radius: 4px; | |
| font-family: monospace; | |
| white-space: pre-wrap; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <h1>🔍 PETTY 로그인 상태 디버깅</h1> | |
| <div class="debug-section"> | |
| <h2>1. JWT 토큰 상태</h2> | |
| <div id="jwtStatus"></div> | |
| <button onclick="checkJWT()">JWT 확인</button> | |
| <button onclick="clearJWT()">로그아웃</button> | |
| </div> | |
| <div class="debug-section"> | |
| <h2>2. 사용자 정보 확인</h2> | |
| <div id="userStatus"></div> | |
| <button onclick="checkUser()">사용자 정보 확인</button> | |
| </div> | |
| <div class="debug-section"> | |
| <h2>3. API 엔드포인트 테스트</h2> | |
| <div id="apiStatus"></div> | |
| <button onclick="testAPI()">API 테스트</button> | |
| </div> | |
| <div class="debug-section"> | |
| <h2>4. 로그인 테스트</h2> | |
| <div> | |
| <input type="text" id="username" placeholder="사용자명" style="margin: 5px; padding: 8px;"> | |
| <input type="password" id="password" placeholder="비밀번호" style="margin: 5px; padding: 8px;"> | |
| <button onclick="testLogin()">로그인 테스트</button> | |
| </div> | |
| <div id="loginStatus"></div> | |
| </div> | |
| <script> | |
| function checkJWT() { | |
| const statusDiv = document.getElementById("jwtStatus"); | |
| // HttpOnly 쿠키는 JavaScript로 직접 접근할 수 없음 | |
| statusDiv.innerHTML = ` | |
| <div class="status warning">⚠️ HttpOnly 쿠키 방식으로 변경됨</div> | |
| <div class="code">쿠키는 JavaScript로 직접 접근할 수 없으므로 /api/users/me로 인증 상태를 확인합니다.</div> | |
| `; | |
| // 대신 사용자 정보 API로 인증 상태 확인 | |
| checkUser(); | |
| } | |
| function clearJWT() { | |
| // HttpOnly 쿠키는 JavaScript로 삭제할 수 없으므로 로그아웃 API 호출 | |
| fetch('/logout', { | |
| method: 'POST', | |
| credentials: 'include' | |
| }).then(() => { | |
| document.getElementById("jwtStatus").innerHTML = ` | |
| <div class="status warning">🗑️ 로그아웃 요청을 보냈습니다</div> | |
| `; | |
| checkUser(); | |
| }).catch(err => { | |
| document.getElementById("jwtStatus").innerHTML = ` | |
| <div class="status error">❌ 로그아웃 실패: ${err.message}</div> | |
| `; | |
| }); | |
| } | |
| async function checkUser() { | |
| const statusDiv = document.getElementById("userStatus"); | |
| try { | |
| const res = await fetch('http://localhost:8080/api/users/me', { | |
| credentials: 'include' // 쿠키 포함 | |
| }); | |
| if (res.ok) { | |
| const user = await res.json(); | |
| statusDiv.innerHTML = ` | |
| <div class="status success">✅ 사용자 정보 조회 성공 (로그인됨)</div> | |
| <div class="code">${JSON.stringify(user, null, 2)}</div> | |
| `; | |
| } else if (res.status === 401) { | |
| statusDiv.innerHTML = ` | |
| <div class="status error">❌ 로그인되지 않음 (401)</div> | |
| `; | |
| } else { | |
| statusDiv.innerHTML = ` | |
| <div class="status error">❌ 사용자 정보 조회 실패 (${res.status})</div> | |
| <div class="code">응답: ${await res.text()}</div> | |
| `; | |
| } | |
| } catch (err) { | |
| statusDiv.innerHTML = ` | |
| <div class="status error">❌ 네트워크 오류</div> | |
| <div class="code">${err.message}</div> | |
| `; | |
| } | |
| } | |
| async function testAPI() { | |
| const statusDiv = document.getElementById("apiStatus"); | |
| const tests = [ | |
| { name: "메인 페이지", url: "http://localhost:8080/" }, | |
| { name: "로그인 페이지", url: "http://localhost:8080/login" }, | |
| { name: "API 상태", url: "http://localhost:8080/api/posts?type=QNA&page=0&size=5" } | |
| ]; | |
| statusDiv.innerHTML = "<div>API 테스트 중...</div>"; | |
| let results = "<h3>API 테스트 결과:</h3>"; | |
| for (const test of tests) { | |
| try { | |
| const res = await fetch(test.url); | |
| if (res.ok) { | |
| results += `<div class="status success">✅ ${test.name}: 성공 (${res.status})</div>`; | |
| } else { | |
| results += `<div class="status error">❌ ${test.name}: 실패 (${res.status})</div>`; | |
| } | |
| } catch (err) { | |
| results += `<div class="status error">❌ ${test.name}: 네트워크 오류</div>`; | |
| } | |
| } | |
| statusDiv.innerHTML = results; | |
| } | |
| async function testLogin() { | |
| const username = document.getElementById("username").value; | |
| const password = document.getElementById("password").value; | |
| const statusDiv = document.getElementById("loginStatus"); | |
| if (!username || !password) { | |
| statusDiv.innerHTML = ` | |
| <div class="status warning">⚠️ 사용자명과 비밀번호를 입력해주세요</div> | |
| `; | |
| return; | |
| } | |
| try { | |
| // Spring Security LoginFilter의 기본 경로로 요청 | |
| const res = await fetch('http://localhost:8080/login', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json' | |
| }, | |
| credentials: 'include', // 쿠키 포함 | |
| body: JSON.stringify({ username, password }) | |
| }); | |
| if (res.ok) { | |
| const result = await res.json(); | |
| statusDiv.innerHTML = ` | |
| <div class="status success">✅ 로그인 성공!</div> | |
| <div class="code">쿠키에 토큰이 저장되었습니다</div> | |
| <div class="code">${JSON.stringify(result, null, 2)}</div> | |
| `; | |
| // 로그인 후 사용자 정보 자동 확인 | |
| setTimeout(() => { | |
| checkJWT(); | |
| checkUser(); | |
| }, 500); | |
| } else { | |
| const responseText = await res.text(); | |
| statusDiv.innerHTML = ` | |
| <div class="status error">❌ 로그인 실패 (${res.status})</div> | |
| <div class="code">${responseText}</div> | |
| `; | |
| } | |
| } catch (err) { | |
| statusDiv.innerHTML = ` | |
| <div class="status error">❌ 네트워크 오류</div> | |
| <div class="code">${err.message}</div> | |
| `; | |
| } | |
| } | |
| // 페이지 로드 시 자동으로 상태 확인 | |
| window.addEventListener('DOMContentLoaded', () => { | |
| checkJWT(); | |
| checkUser(); | |
| }); | |
| </script> | |
| </body> | |
| </html> | |
| <!DOCTYPE html> | |
| <html lang="ko"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>PETTY 로그인 디버깅</title> | |
| <!-- …existing styles… --> | |
| </head> | |
| <body> | |
| <!-- 개발 환경에서만 사용하는 디버깅 페이지입니다. 프로덕션 배포 시 제거해야 합니다. --> | |
| <script> | |
| // 프로덕션 환경 체크 | |
| if (window.location.hostname !== 'localhost' && window.location.hostname !== '127.0.0.1') { | |
| document.body.innerHTML = '<h1>이 페이지는 개발 환경에서만 사용 가능합니다.</h1>'; | |
| } | |
| </script> | |
| <h1>🔍 PETTY 로그인 상태 디버깅</h1> | |
| <div class="debug-section"> | |
| <h2>1. JWT 토큰 상태</h2> | |
| <div id="jwtStatus"></div> | |
| <button onclick="checkJWT()">JWT 확인</button> | |
| <button onclick="clearJWT()">로그아웃</button> | |
| </div> | |
| <!-- …the rest of your debug page… --> | |
| </body> | |
| </html> |
🤖 Prompt for AI Agents
In debug_complete.html lines 1 to 230, the debugging page contains hardcoded
localhost URLs and a login test feature that pose security risks if deployed to
production. To fix this, remove or disable this page from the production build
by excluding it in the build configuration or moving it under a protected path
like /debug/ that is blocked in production. Also, replace hardcoded URLs with
environment-based configurations and restrict or remove the login test
functionality to prevent misuse.
| <label for="review-content">후기 내용 <span class="required">*</span></label> | ||
| <textarea class="input-field" id="review-content" placeholder="여행 경험을 자유롭게 작성해주세요" maxlength="2000" required></textarea> |
There was a problem hiding this comment.
textarea에 name 속성 누락
<textarea>에 name이 없어 서버로 데이터가 전달되지 않습니다.
다음과 같이 name 속성을 추가해 주세요:
- <textarea class="input-field" id="review-content" placeholder="여행 경험을 자유롭게 작성해주세요" maxlength="2000" required></textarea>
+ <textarea name="content" class="input-field" id="review-content" placeholder="여행 경험을 자유롭게 작성해주세요" maxlength="2000" required></textarea>📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <label for="review-content">후기 내용 <span class="required">*</span></label> | |
| <textarea class="input-field" id="review-content" placeholder="여행 경험을 자유롭게 작성해주세요" maxlength="2000" required></textarea> | |
| <label for="review-content">후기 내용 <span class="required">*</span></label> | |
| - <textarea class="input-field" id="review-content" placeholder="여행 경험을 자유롭게 작성해주세요" maxlength="2000" required></textarea> | |
| + <textarea name="content" class="input-field" id="review-content" placeholder="여행 경험을 자유롭게 작성해주세요" maxlength="2000" required></textarea> |
🤖 Prompt for AI Agents
In src/main/resources/templates/post-review-form.html at lines 73 to 74, the
textarea element is missing a name attribute, which prevents its data from being
submitted to the server. Add a name attribute to the textarea tag, for example
name="reviewContent", to ensure the form data is correctly sent.
| <label for="review-title">제목 <span class="required">*</span></label> | ||
| <input type="text" class="input-field" id="review-title" placeholder="후기 제목을 입력해주세요" maxlength="50" required> |
There was a problem hiding this comment.
form 입력에 name 속성 누락
<input> 요소에 required을 추가한 것은 좋지만, name 속성이 없으면 폼 데이터에 포함되지 않습니다.
다음과 같이 name 속성을 추가해 주세요:
- <input type="text" class="input-field" id="review-title" placeholder="후기 제목을 입력해주세요" maxlength="50" required>
+ <input type="text" name="title" class="input-field" id="review-title" placeholder="후기 제목을 입력해주세요" maxlength="50" required>📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <label for="review-title">제목 <span class="required">*</span></label> | |
| <input type="text" class="input-field" id="review-title" placeholder="후기 제목을 입력해주세요" maxlength="50" required> | |
| <label for="review-title">제목 <span class="required">*</span></label> | |
| <input type="text" name="title" class="input-field" id="review-title" placeholder="후기 제목을 입력해주세요" maxlength="50" required> |
🤖 Prompt for AI Agents
In src/main/resources/templates/post-review-form.html around lines 24 to 25, the
input element for the review title is missing a name attribute, which prevents
its value from being included in the form submission. Add a name attribute to
the input tag, for example name="reviewTitle", to ensure the form data is
properly sent.
| <label for="title">제목 <span class="required">*</span></label> | ||
| <input type="text" class="input-field" id="title" placeholder="질문의 제목을 입력해주세요" maxlength="50" required> | ||
| </div> |
There was a problem hiding this comment.
필수 제목 필드 name 속성 누락
<input>에 name 속성이 없어 폼 데이터가 서버에 전달되지 않습니다.
다음과 같이 name 속성을 추가해 주세요:
- <input type="text" class="input-field" id="title" placeholder="질문의 제목을 입력해주세요" maxlength="50" required>
+ <input type="text" name="title" class="input-field" id="title" placeholder="질문의 제목을 입력해주세요" maxlength="50" required>📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <label for="title">제목 <span class="required">*</span></label> | |
| <input type="text" class="input-field" id="title" placeholder="질문의 제목을 입력해주세요" maxlength="50" required> | |
| </div> | |
| <label for="title">제목 <span class="required">*</span></label> | |
| - <input type="text" class="input-field" id="title" placeholder="질문의 제목을 입력해주세요" maxlength="50" required> | |
| + <input type="text" name="title" class="input-field" id="title" placeholder="질문의 제목을 입력해주세요" maxlength="50" required> | |
| </div> |
🤖 Prompt for AI Agents
In src/main/resources/templates/post-qna-form.html around lines 24 to 26, the
input field for the title is missing the required name attribute, which prevents
the form data from being sent to the server. Add a name="title" attribute to the
input element to ensure the title value is included in the form submission.
| <label for="content">질문 내용 <span class="required">*</span></label> | ||
| <textarea class="input-field" id="content" placeholder="질문 내용을 자세히 작성해주세요. 구체적인 내용을 포함하면 더 정확한 답변을 받을 수 있어요." maxlength="2000" required></textarea> | ||
| </div> |
There was a problem hiding this comment.
textarea name 속성 누락
<textarea>에 name이 없어 폼 제출 시 내용이 전송되지 않습니다.
다음과 같이 name을 추가해 주세요:
- <textarea class="input-field" id="content" placeholder="질문 내용을 자세히 작성해주세요. 구체적인 내용을 포함하면 더 정확한 답변을 받을 수 있어요." maxlength="2000" required></textarea>
+ <textarea name="content" class="input-field" id="content" placeholder="질문 내용을 자세히 작성해주세요. 구체적인 내용을 포함하면 더 정확한 답변을 받을 수 있어요." maxlength="2000" required></textarea>📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <label for="content">질문 내용 <span class="required">*</span></label> | |
| <textarea class="input-field" id="content" placeholder="질문 내용을 자세히 작성해주세요. 구체적인 내용을 포함하면 더 정확한 답변을 받을 수 있어요." maxlength="2000" required></textarea> | |
| </div> | |
| <label for="content">질문 내용 <span class="required">*</span></label> | |
| <textarea name="content" class="input-field" id="content" placeholder="질문 내용을 자세히 작성해주세요. 구체적인 내용을 포함하면 더 정확한 답변을 받을 수 있어요." maxlength="2000" required></textarea> | |
| </div> |
🤖 Prompt for AI Agents
In src/main/resources/templates/post-qna-form.html around lines 63 to 65, the
<textarea> element is missing a name attribute, which prevents its content from
being submitted with the form. Add a name attribute to the <textarea> tag, for
example name="content", to ensure the input data is included when the form is
submitted.
| import io.github.petty.users.dto.CustomUserDetails; | ||
| import io.github.petty.users.entity.Users; | ||
| import io.github.petty.users.repository.UsersRepository; | ||
| import jakarta.validation.Valid; |
There was a problem hiding this comment.
컴파일 오류: 잘못된 validation 패키지 import
파이프라인 오류에 따르면 jakarta.validation 패키지가 존재하지 않습니다. Spring Boot 버전을 확인하고 적절한 패키지를 사용하세요.
Spring Boot 2.x를 사용 중이라면:
-import jakarta.validation.Valid;
+import javax.validation.Valid;Spring Boot 3.x를 사용 중이라면 의존성을 확인하세요:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>🧰 Tools
🪛 GitHub Actions: Java CI with Gradle
[error] 10-10: Compilation failed: package 'jakarta.validation' does not exist. Missing import for @Valid annotation.
🤖 Prompt for AI Agents
In src/main/java/io/github/petty/community/controller/PostController.java at
line 10, the import statement uses jakarta.validation.Valid, which causes a
compile error because the package is missing. Check the Spring Boot version: if
using Spring Boot 2.x, replace the import with javax.validation.Valid; if using
Spring Boot 3.x, ensure the spring-boot-starter-validation dependency is added
to your build configuration to provide jakarta.validation. Adjust the import
accordingly based on the Spring Boot version and dependencies.
| // 🔥 기존 데이터 업데이트를 위한 임시 엔드포인트 (관리자용) | ||
| @PostMapping("/update-counts") | ||
| public ResponseEntity<?> updateAllPostCounts() { | ||
| postService.updateAllPostCounts(); | ||
| return ResponseEntity.ok(Map.of("message", "모든 게시글의 댓글 수와 좋아요 수가 업데이트되었습니다.")); | ||
| } |
There was a problem hiding this comment.
보안 취약점: 관리자 권한 체크가 없습니다!
이 엔드포인트는 모든 게시글의 카운트를 업데이트하는 중요한 작업을 수행하지만, 권한 체크가 없어 누구나 호출할 수 있습니다.
관리자 권한 체크를 추가하세요:
@PostMapping("/update-counts")
+@PreAuthorize("hasRole('ADMIN')")
public ResponseEntity<?> updateAllPostCounts() {
postService.updateAllPostCounts();
return ResponseEntity.ok(Map.of("message", "모든 게시글의 댓글 수와 좋아요 수가 업데이트되었습니다."));
}또는 요청자의 권한을 확인하는 로직을 추가하세요:
@PostMapping("/update-counts")
-public ResponseEntity<?> updateAllPostCounts() {
+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", "모든 게시글의 댓글 수와 좋아요 수가 업데이트되었습니다."));
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // 🔥 기존 데이터 업데이트를 위한 임시 엔드포인트 (관리자용) | |
| @PostMapping("/update-counts") | |
| public ResponseEntity<?> updateAllPostCounts() { | |
| postService.updateAllPostCounts(); | |
| return ResponseEntity.ok(Map.of("message", "모든 게시글의 댓글 수와 좋아요 수가 업데이트되었습니다.")); | |
| } | |
| // 🔥 기존 데이터 업데이트를 위한 임시 엔드포인트 (관리자용) | |
| @PostMapping("/update-counts") | |
| @PreAuthorize("hasRole('ADMIN')") | |
| public ResponseEntity<?> updateAllPostCounts() { | |
| postService.updateAllPostCounts(); | |
| return ResponseEntity.ok(Map.of("message", "모든 게시글의 댓글 수와 좋아요 수가 업데이트되었습니다.")); | |
| } |
| // 🔥 기존 데이터 업데이트를 위한 임시 엔드포인트 (관리자용) | |
| @PostMapping("/update-counts") | |
| public ResponseEntity<?> updateAllPostCounts() { | |
| postService.updateAllPostCounts(); | |
| return ResponseEntity.ok(Map.of("message", "모든 게시글의 댓글 수와 좋아요 수가 업데이트되었습니다.")); | |
| } | |
| // 🔥 기존 데이터 업데이트를 위한 임시 엔드포인트 (관리자용) | |
| @PostMapping("/update-counts") | |
| 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", "모든 게시글의 댓글 수와 좋아요 수가 업데이트되었습니다.")); | |
| } |
🤖 Prompt for AI Agents
In src/main/java/io/github/petty/community/controller/PostController.java around
lines 100 to 105, the updateAllPostCounts endpoint lacks any admin authorization
check, allowing unrestricted access. Add a security annotation such as
@PreAuthorize("hasRole('ADMIN')") above the method or implement explicit logic
to verify the requester's admin role before executing the update. This ensures
only authorized administrators can invoke this sensitive operation.
| @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); | ||
| } | ||
|
|
||
| System.out.println("✅ 모든 게시글의 댓글 수와 좋아요 수가 업데이트되었습니다."); | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
대량 데이터 처리 시 성능 문제가 발생할 수 있습니다.
현재 구현은 모든 게시글을 메모리에 로드하고 각각 저장하므로 N+1 쿼리 문제가 발생합니다. 게시글이 많을 경우 성능 문제가 심각할 수 있습니다.
배치 처리나 네이티브 쿼리를 사용한 벌크 업데이트를 고려하세요:
@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();또는 페이징을 사용하여 메모리 사용량을 줄이세요:
@Override
@Transactional
public void updateAllPostCounts() {
int pageSize = 100;
int page = 0;
Page<Post> postPage;
do {
postPage = postRepository.findAll(PageRequest.of(page++, pageSize));
for (Post post : postPage.getContent()) {
long commentCount = commentRepository.countByPostId(post.getId());
post.setCommentCount((int) commentCount);
long likeCount = postLikeRepository.countByPost(post);
post.setLikeCount((int) likeCount);
}
postRepository.saveAll(postPage.getContent());
} while (postPage.hasNext());
System.out.println("✅ 모든 게시글의 댓글 수와 좋아요 수가 업데이트되었습니다.");
}🤖 Prompt for AI Agents
In src/main/java/io/github/petty/community/service/PostServiceImpl.java around
lines 194 to 213, the current updateAllPostCounts method loads all posts into
memory and updates each one individually, causing N+1 query and performance
issues with large data sets. To fix this, refactor the method to either use a
single bulk update query with @Modifying and @Query annotations in the
repository to update commentCount and likeCount in one statement, or implement
paging to process posts in batches (e.g., 100 at a time), updating counts and
saving them in bulk to reduce memory usage and database calls.
| // 🔐 로그인 체크 함수 (쿠키 기반) | ||
| function isLoggedIn() { | ||
| return true; // 실제 체크는 getCurrentUser()에서 | ||
| } | ||
|
|
||
| // 🔐 현재 로그인 사용자 정보 가져오기 (쿠키 기반) | ||
| async function getCurrentUser() { | ||
| try { | ||
| const res = await fetch('/api/users/me', { | ||
| credentials: 'include' // 쿠키를 포함해서 요청 | ||
| }); | ||
|
|
||
| if (res.ok) { | ||
| return await res.json(); | ||
| } else if (res.status === 401) { | ||
| return null; // 로그인 안됨 | ||
| } | ||
| } catch (err) { | ||
| console.error('사용자 정보 조회 실패:', err); | ||
| } | ||
| return null; | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
중복된 인증 함수 리팩토링 필요
isLoggedIn()과 getCurrentUser() 함수가 form.js와 동일하게 중복 정의되어 있습니다. 공통 함수는 별도의 auth.js 파일로 분리하여 재사용하는 것을 권장합니다.
/js/common/auth.js파일 생성:
// 🔐 현재 로그인 사용자 정보 가져오기 (쿠키 기반)
async function getCurrentUser() {
try {
const res = await fetch('/api/users/me', {
credentials: 'include'
});
if (res.ok) {
return await res.json();
} else if (res.status === 401) {
return null;
}
} catch (err) {
console.error('사용자 정보 조회 실패:', err);
}
return null;
}- HTML에서 스크립트 포함:
<script src="/js/common/auth.js"></script>🤖 Prompt for AI Agents
In src/main/resources/templates/post-detail.html around lines 570 to 591, the
authentication functions isLoggedIn() and getCurrentUser() are duplicated from
form.js. To fix this, create a new file /js/common/auth.js containing the
getCurrentUser() function and any related auth logic, then remove these
functions from post-detail.html and include the new auth.js script via a script
tag in the HTML to enable reuse and avoid duplication.
| import jakarta.validation.constraints.NotBlank; | ||
| import jakarta.validation.constraints.NotNull; | ||
| import lombok.Getter; | ||
| import lombok.Setter; | ||
|
|
||
| import javax.validation.constraints.Size; |
There was a problem hiding this comment.
중요한 문제: 혼재된 검증 어노테이션 임포트
Jakarta(jakarta.validation.constraints)와 Java EE(javax.validation.constraints) 검증 어노테이션을 혼재해서 사용하고 있습니다. 이는 런타임 오류를 발생시킬 수 있습니다.
모든 검증 어노테이션을 Jakarta로 통일해야 합니다:
-import jakarta.validation.constraints.NotBlank;
-import jakarta.validation.constraints.NotNull;
-import javax.validation.constraints.Size;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Size;그리고 앞서 언급한 validation 의존성도 추가해야 합니다.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| import jakarta.validation.constraints.NotBlank; | |
| import jakarta.validation.constraints.NotNull; | |
| import lombok.Getter; | |
| import lombok.Setter; | |
| import javax.validation.constraints.Size; | |
| import jakarta.validation.constraints.NotBlank; | |
| import jakarta.validation.constraints.NotNull; | |
| import lombok.Getter; | |
| import lombok.Setter; | |
| import jakarta.validation.constraints.Size; |
🧰 Tools
🪛 GitHub Actions: Java CI with Gradle
[error] 4-5: Compilation failed: package 'jakarta.validation.constraints' does not exist. Missing imports for NotBlank and NotNull annotations.
🤖 Prompt for AI Agents
In src/main/java/io/github/petty/community/dto/PostRequest.java lines 4 to 9,
the validation annotations are imported from both jakarta.validation.constraints
and javax.validation.constraints, which can cause runtime errors. To fix this,
replace all javax.validation.constraints imports with their
jakarta.validation.constraints equivalents so that all validation annotations
consistently use Jakarta imports. Also, ensure the project includes the
necessary Jakarta validation dependency.
📜 PR 내용 요약
커뮤니티 기능의 게시글 편집 및 상세 페이지 개발을 완료했습니다. 사용자가 작성한 게시글을 수정하고, 상세 내용을 확인할 수 있는 기능을 구현했습니다.
주요 기능:
⚒️ 작업 및 변경 내용(상세하게)
🔧 백엔드 개발
1. Controller 계층
CommentController: 댓글 CRUD API 구현
GET /api/posts/{postId}/comments- 댓글 목록 조회POST /api/posts/{postId}/comments- 댓글 작성PUT /api/comments/{commentId}- 댓글 수정DELETE /api/comments/{commentId}- 댓글 삭제PostController: 게시글 관리 API 확장
PUT /api/posts/{id}- 게시글 수정DELETE /api/posts/{id}- 게시글 삭제 (삭제된 게시글 타입 반환)POST /api/posts/{id}/like- 좋아요 토글POST /api/posts/update-counts- 관리자용 통계 업데이트2. Service 계층
CommentService: 댓글 비즈니스 로직
PostService: 게시글 비즈니스 로직 확장
PostImageService: 이미지 관리 로직
3. DTO 설계
🎨 프론트엔드 개발
1. 게시글 수정 페이지
edit-qna.html/js: QNA 게시글 수정
edit-review.html/js: Review 게시글 수정
edit-showoff.html/js: Showoff 게시글 수정
2. 게시글 상세 페이지
3. 댓글 시스템
4. 리스트 페이지 개선
🔐 보안 및 권한 관리
1. 사용자 인증
2. 권한 검증
3. UI 권한 적용
📊 데이터 정합성 관리
실시간 통계 업데이트
📚 기타 참고 사항
🔍 리뷰 포인트
🚀 성능 최적화
@Query와JOIN FETCH를 사용하여 댓글 조회 최적화🛠️ 빌드 및 배포
🔄 후속 작업 예정