Skip to content

[COMMUNITY] Community 기능 추가#31

Merged
s0ooo0k merged 2 commits intoPETTY-HUB:mainfrom
LimPark996:main
May 23, 2025
Merged

[COMMUNITY] Community 기능 추가#31
s0ooo0k merged 2 commits intoPETTY-HUB:mainfrom
LimPark996:main

Conversation

@LimPark996
Copy link
Contributor

@LimPark996 LimPark996 commented May 22, 2025

📜 PR 내용 요약

커뮤니티 기능 (좋아요, 댓글 추가, 삭제 시 숫자 변경 확인 필요. 그 외 동작 확인 완료) 추가하였습니다.

⚒️ 작업 및 변경 내용(상세하게)

커뮤니티 기능 대략적인 화면

image

image

image

image

image

image

📚 기타 참고 사항

좋아요, 댓글 추가, 삭제 시 숫자 변경 확인 한번 해주시고, 전반적으로 커뮤니티 잘돌아가는지 테스트 해주세요~

@coderabbitai
Copy link

coderabbitai bot commented May 22, 2025

Summary by CodeRabbit

  • 신규 기능

    • 커뮤니티 게시글(리뷰, 자랑, Q&A) 작성, 수정, 삭제, 상세 조회, 목록, 좋아요, 댓글 기능이 추가되었습니다.
    • 게시글에 여러 이미지를 첨부/삭제/미리보기할 수 있습니다.
    • Q&A, 리뷰, 자랑글 각각의 전용 작성/수정/목록/상세 페이지가 제공됩니다.
    • 댓글 작성, 수정, 삭제, 목록 기능이 제공됩니다.
    • 게시글 및 댓글 작성 시 이미지 업로드(최대 5개, 5MB 제한) 및 미리보기 지원.
    • 커뮤니티 메뉴가 메인 네비게이션에 추가되었습니다.
  • 스타일

    • 공통 디자인 시스템 및 반응형 CSS 스타일시트가 추가되어 일관된 UI 제공.
  • 버그 수정

    • 해당 없음(신규 기능 위주).
  • 문서화

    • 새로운 HTML 템플릿(게시글 작성/수정/목록/상세, 댓글 등) 다수 추가.
  • 기타

    • 불필요한 빈 클래스 및 인터페이스 제거로 코드 정리.

Walkthrough

이 변경 사항은 커뮤니티 게시판 기능을 전면적으로 도입합니다. 게시글, 댓글, 이미지 업로드, 좋아요, 목록 및 상세 조회 등 커뮤니티의 핵심 기능이 백엔드와 프론트엔드 양쪽에 새롭게 구현되었습니다. 기존의 비어 있던 클래스와 인터페이스는 삭제되었으며, 풍부한 엔티티, 서비스, 컨트롤러, DTO, 저장소, 유틸리티, 정적 자원, HTML 템플릿, 자바스크립트, CSS가 추가되었습니다.

Changes

파일/경로 요약 변경 요약
src/main/java/io/github/petty/community/Comments.java
CommunityController.java
CommunityRepository.java
CommunityService.java
PostLike.java
PostViews.java
Posts.java
비어 있던 커뮤니티 관련 클래스 및 인터페이스 7개 삭제
src/main/java/io/github/petty/community/controller/CommentController.java 댓글 CRUD REST 컨트롤러 신설, 인증 사용자 기반의 댓글 생성/수정/삭제/조회 API 제공
src/main/java/io/github/petty/community/controller/PostController.java 게시글 생성, 수정, 삭제, 조회, 좋아요 토글 등 REST API 컨트롤러 신설
src/main/java/io/github/petty/community/controller/PostImageUploadController.java 게시글 이미지 단일/다중 업로드 REST 컨트롤러 신설, 파일 유효성 검사 및 업로드 처리
src/main/java/io/github/petty/community/controller/PostViewController.java 게시글 뷰(리스트, 폼, 수정, 상세)용 GET 엔드포인트 제공하는 MVC 컨트롤러 추가
src/main/java/io/github/petty/community/dto/CommentRequest.java
CommentResponse.java
댓글 요청/응답 DTO 신설
src/main/java/io/github/petty/community/dto/PostDetailResponse.java 게시글 상세 응답 DTO 신설
src/main/java/io/github/petty/community/dto/PostImageRequest.java
PostImageResponse.java
게시글 이미지 요청/응답 DTO 신설
src/main/java/io/github/petty/community/dto/PostQnaListResponse.java
PostReviewListResponse.java
PostShowoffListResponse.java
QNA/리뷰/자랑글 목록 응답 DTO 신설
src/main/java/io/github/petty/community/dto/PostRequest.java 게시글 생성/수정 요청 DTO 신설
src/main/java/io/github/petty/community/entity/Comment.java
Post.java
PostImage.java
PostLike.java
댓글, 게시글, 게시글 이미지, 게시글 좋아요 JPA 엔티티 신설 및 관계 설정
src/main/java/io/github/petty/community/enums/PetType.java 반려동물 타입 enum 신설(라벨 포함)
src/main/java/io/github/petty/community/repository/CommentRepository.java
PostImageRepository.java
PostLikeRepository.java
PostRepository.java
댓글, 게시글, 이미지, 좋아요용 JPA 리포지토리 신설 및 커스텀 쿼리 메서드 추가
src/main/java/io/github/petty/community/service/CommentService.java
CommentServiceImpl.java
댓글 서비스 인터페이스 및 구현체 신설, CRUD 및 권한 체크 로직 포함
src/main/java/io/github/petty/community/service/PostImageService.java
PostImageServiceImpl.java
게시글 이미지 서비스 인터페이스 및 구현체 신설, 업로드/삭제/정렬/수정 로직 구현
src/main/java/io/github/petty/community/service/PostService.java
PostServiceImpl.java
게시글 서비스 인터페이스 및 구현체 신설, 저장/수정/삭제/목록/상세/좋아요 토글 등 구현
src/main/java/io/github/petty/community/util/SupabaseUploader.java Supabase 스토리지 연동 이미지 업로드/삭제 유틸리티 컴포넌트 신설
src/main/java/io/github/petty/config/SupabaseDataSourceConfig.java 커뮤니티 엔티티/리포지토리 JPA 스캔 범위에 추가
src/main/resources/static/css/common-styles.css 커뮤니티 게시판용 디자인 시스템 및 스타일 CSS 신설
src/main/resources/static/js/common/form.js
edit-qna.js
edit-review.js
edit-showoff.js
게시글/댓글 폼, 이미지 업로드, 수정 기능 등 프론트엔드 JS 신설
src/main/resources/templates/edit-qna.html
edit-review.html
edit-showoff.html
QNA/리뷰/자랑글 수정 폼 HTML 템플릿 신설
src/main/resources/templates/post-detail.html 게시글 상세 뷰 HTML 템플릿 및 동적 댓글/좋아요/수정/삭제 JS 포함
src/main/resources/templates/post-qna-form.html
post-review-form.html
post-showoff-form.html
QNA/리뷰/자랑글 작성 폼 HTML 템플릿 신설
src/main/resources/templates/post-qna-list.html
post-review-list.html
post-showoff-list.html
QNA/리뷰/자랑글 목록 HTML 템플릿 및 동적 렌더링 JS 포함
src/main/resources/templates/index.html 네비게이션 바에 "커뮤니티" 링크 추가

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant Frontend(JS)
  participant PostController
  participant PostService
  participant PostRepository
  participant UsersRepository

  User->>Frontend(JS): 게시글 작성/수정/삭제/조회/좋아요 요청
  Frontend(JS)->>PostController: REST API 요청 (JWT 포함)
  PostController->>UsersRepository: 사용자 조회
  PostController->>PostService: 게시글 관련 서비스 호출
  PostService->>PostRepository: 게시글 DB 처리
  PostService-->>PostController: 결과 반환
  PostController-->>Frontend(JS): HTTP 응답 반환
  Frontend(JS)-->>User: 화면 업데이트

  Note over Frontend(JS): 이미지 업로드 시<br>SupabaseUploader 별도 호출
Loading
sequenceDiagram
  participant User
  participant Frontend(JS)
  participant CommentController
  participant CommentService
  participant CommentRepository
  participant PostRepository

  User->>Frontend(JS): 댓글 작성/수정/삭제/조회 요청
  Frontend(JS)->>CommentController: REST API 요청 (JWT 포함)
  CommentController->>CommentService: 댓글 서비스 호출
  CommentService->>CommentRepository: 댓글 DB 처리
  CommentService->>PostRepository: 게시글 댓글수 갱신
  CommentService-->>CommentController: 결과 반환
  CommentController-->>Frontend(JS): HTTP 응답 반환
  Frontend(JS)-->>User: 화면 업데이트
Loading

Poem

(\(\
( -.-) 커뮤니티가 활짝 열렸어요!
o_(")(") 토끼는 신나서 춤을 춰요.

게시글, 댓글, 이미지도 척척,
좋아요 누르며 깡충깡충!
새로워진 게시판에 모두 모여
PETTY 세상, 함께 뛰놀아요! 🥕✨

Note

⚡️ AI Code Reviews for VS Code, Cursor, Windsurf

CodeRabbit now has a plugin for VS Code, Cursor and Windsurf. This brings AI code reviews directly in the code editor. Each commit is reviewed immediately, finding bugs before the PR is raised. Seamless context handoff to your AI code agent ensures that you can easily incorporate review feedback.
Learn more here.

✨ Finishing Touches
  • 📝 Generate Docstrings

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.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need 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)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@LimPark996 LimPark996 changed the title feat: Community 기능 추가, build.gradle 수정 (원본은 주석처리함) feat: Community 기능 추가 May 22, 2025
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Inline review comments failed to post. This is likely due to GitHub's limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

🛑 Comments failed to post (76)
src/main/resources/templates/index.html (1)

60-60: 🛠️ Refactor suggestion

Thymeleaf 경로 바인딩 사용 권장
현재 하드코딩된 href="/posts/review" 대신 th:href="@{/posts/review}"를 사용하여 context path 변경에 대응하도록 개선하세요.

🤖 Prompt for AI Agents
In src/main/resources/templates/index.html at line 60, replace the hardcoded
href attribute value "/posts/review" with Thymeleaf's path binding syntax
th:href="@{/posts/review}" to ensure the link correctly adapts to context path
changes.
src/main/java/io/github/petty/community/dto/CommentRequest.java (1)

8-10: 🛠️ Refactor suggestion

요청 DTO에 유효성 검증 추가 추천
content 필드에 @NotBlank, @Size(max=500) 등의 Bean Validation 애노테이션을 추가해 빈 문자열 또는 과도한 길이 입력을 방지하세요.

🤖 Prompt for AI Agents
In src/main/java/io/github/petty/community/dto/CommentRequest.java around lines
8 to 10, the content field lacks validation annotations. Add @NotBlank to ensure
the content is not empty or just whitespace, and @Size(max=500) to limit the
content length to 500 characters. Import the necessary
javax.validation.constraints annotations to enable these validations.
src/main/java/io/github/petty/community/dto/PostRequest.java (2)

12-19: 🛠️ Refactor suggestion

필드 유효성 검증 추가 고려
DTO에 @NotNull, @NotBlank, @Size 등의 Bean Validation 애노테이션이 없어 잘못된 입력이 즉시 처리되지 않을 수 있습니다.
각 필드의 요구사항(제목·내용 길이, 필수 여부 등)에 맞춰 검증 애노테이션을 부여하고, 컨트롤러에서 @Valid를 활용해주세요.

🤖 Prompt for AI Agents
In src/main/java/io/github/petty/community/dto/PostRequest.java around lines 12
to 19, the DTO fields lack Bean Validation annotations, which can allow invalid
input to pass through. Add appropriate validation annotations such as @NotNull,
@NotBlank, and @Size to each field based on its requirements (e.g., title and
content length, mandatory fields). Also, ensure that the controller method
handling this DTO uses the @Valid annotation to trigger validation.

3-8: ⚠️ Potential issue

Import 누락으로 인한 컴파일 에러
PostImageRequest 타입을 참조하고 있으나 import 구문이 없어 컴파일 오류가 발생합니다.
해당 클래스를 import 문에 추가해주세요.

-import java.util.List;
+import java.util.List;
+import io.github.petty.community.dto.PostImageRequest;
📝 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 io.github.petty.community.entity.Post;
import lombok.Getter;
import lombok.Setter;

import java.util.List;
import io.github.petty.community.dto.PostImageRequest;
🤖 Prompt for AI Agents
In src/main/java/io/github/petty/community/dto/PostRequest.java between lines 3
and 8, the PostImageRequest class is referenced but not imported, causing a
compilation error. Add the appropriate import statement for PostImageRequest at
the top of the file along with the existing imports to resolve the error.
src/main/resources/templates/post-showoff-form.html (2)

69-75: 🛠️ Refactor suggestion

CSRF 토큰 누락
Spring Security 사용 시 CSRF 공격을 방지하기 위해 폼에 CSRF 토큰(hidden input)을 반드시 포함해야 합니다.
위 예시처럼 <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>를 추가하세요.

🤖 Prompt for AI Agents
In src/main/resources/templates/post-showoff-form.html around lines 69 to 75,
the form is missing a CSRF token input which is required for Spring Security to
prevent CSRF attacks. Add a hidden input field inside the form with attributes
th:name="${_csrf.parameterName}" and th:value="${_csrf.token}" to include the
CSRF token properly.

22-24: ⚠️ Potential issue

폼 제출 설정 미흡
<form>action·method 속성이 없고, 각 입력 요소에 name 속성이 빠져 있어 서버로 데이터가 전달되지 않습니다.
아래 예시처럼 수정해 주세요.

- <form id="showoffForm">
+ <form id="showoffForm" th:action="@{/posts/showoff}" method="post">
    <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>
- <input type="text" class="input-field" id="showoff-title" placeholder="자랑글 제목을 입력해주세요" maxlength="50">
+ <input type="text" class="input-field" id="showoff-title" name="title" placeholder="자랑글 제목을 입력해주세요" maxlength="50">
...
- <textarea class="input-field" id="showoff-content" placeholder="자랑하고 싶은 내용을 작성해주세요" maxlength="2000"></textarea>
+ <textarea class="input-field" id="showoff-content" name="content" placeholder="자랑하고 싶은 내용을 작성해주세요" maxlength="2000"></textarea>
...
- <input type="file" id="showoff-imageFiles" multiple accept="image/png, image/jpeg, image/gif">
+ <input type="file" id="showoff-imageFiles" name="images" multiple accept="image/png, image/jpeg, image/gif">

Also applies to: 25-27

🤖 Prompt for AI Agents
In src/main/resources/templates/post-showoff-form.html around lines 22 to 27,
the <form> tag lacks the required action and method attributes, and the input
elements inside the form are missing name attributes, preventing data submission
to the server. Add appropriate action and method attributes to the <form> tag,
and ensure each input element has a unique name attribute to enable proper data
transmission upon form submission.
src/main/java/io/github/petty/community/service/CommentService.java (1)

11-12: 🛠️ Refactor suggestion

인터페이스에 @service 어노테이션 불필요

일반적으로 @Service 어노테이션은 구현 클래스에 적용합니다. 인터페이스에는 필요하지 않으므로 제거하는 것이 좋습니다.

-@Service
 public interface CommentService {
📝 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.

public interface CommentService {
🤖 Prompt for AI Agents
In src/main/java/io/github/petty/community/service/CommentService.java at lines
11 to 12, remove the @Service annotation from the CommentService interface
because @Service should be applied only to the implementation class, not the
interface.
src/main/resources/templates/edit-review.html (6)

38-70: 🛠️ Refactor suggestion

반려동물 종류 선택 필수 여부 표시 필요

라디오 버튼 그룹에 기본 선택 값이 설정되어 있지 않고 필수 선택인지 여부가 명확하지 않습니다. 적어도 하나의 옵션을 필수로 선택하도록 설정하고, 사용자에게 필수 필드임을 시각적으로 표시하는 것이 좋습니다.

-                <label>반려동물 종류</label>
+                <label>반려동물 종류 <span class="required">*</span></label>
                 <div class="radio-group">
                     <div class="radio-item">
-                        <input type="radio" id="edit-review-pet-dog" name="edit-review-petType" value="DOG">
+                        <input type="radio" id="edit-review-pet-dog" name="edit-review-petType" value="DOG" required>

또한 CSS에 필수 필드 표시를 위한 스타일을 추가하는 것을 고려하세요:

.required {
  color: red;
  margin-left: 3px;
}
🤖 Prompt for AI Agents
In src/main/resources/templates/edit-review.html around lines 38 to 70, the
radio button group for selecting pet type lacks a default selected option and
does not indicate that the selection is mandatory. To fix this, set the
"checked" attribute on one of the radio inputs to provide a default selection,
add a visual indicator such as a red asterisk next to the label to show the
field is required, and update the label or surrounding markup accordingly.
Additionally, add a CSS class like "required" with styles for the visual
indicator as suggested, and ensure the form validation enforces that one option
must be selected before submission.

72-75: 🛠️ Refactor suggestion

후기 내용 필수 여부 표시 필요

후기 내용 텍스트 영역에 maxlength 속성은 있지만 required 속성이 누락되었습니다. 주요 콘텐츠 필드로 필수 입력으로 표시하는 것이 좋습니다.

-<textarea class="input-field" id="edit-review-content" placeholder="여행 경험을 자유롭게 작성해주세요" maxlength="2000"></textarea>
+<textarea class="input-field" id="edit-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.

            <div class="form-group">
                <label for="edit-review-content">후기 내용</label>
                <textarea class="input-field" id="edit-review-content" placeholder="여행 경험을 자유롭게 작성해주세요" maxlength="2000" required></textarea>
            </div>
🤖 Prompt for AI Agents
In src/main/resources/templates/edit-review.html around lines 72 to 75, the
textarea for the review content has a maxlength attribute but is missing the
required attribute. Add the required attribute to the textarea element to
indicate that this field is mandatory for form submission.

33-36: 🛠️ Refactor suggestion

여행 장소 필드 검증 누락

여행 장소 필드에도 maxlengthrequired 속성이 누락되었습니다. 일관된 유효성 검증을 위해 적절한 제한을 추가해야 합니다.

-<input type="text" class="input-field" id="edit-region" placeholder="여행 장소를 입력해주세요">
+<input type="text" class="input-field" id="edit-region" 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.

            <div class="form-group">
                <label for="edit-region">여행 장소</label>
-               <input type="text" class="input-field" id="edit-region" placeholder="여행 장소를 입력해주세요">
+               <input type="text" class="input-field" id="edit-region" placeholder="여행 장소를 입력해주세요" maxlength="50" required>
            </div>
🤖 Prompt for AI Agents
In src/main/resources/templates/edit-review.html around lines 33 to 36, the
input field for 여행 장소 is missing maxlength and required attributes. Add a
maxlength attribute with an appropriate character limit and include the required
attribute to enforce validation and maintain consistency with other form fields.

28-31: 🛠️ Refactor suggestion

반려동물 이름 필드 검증 누락

반려동물 이름 필드에 maxlengthrequired 속성이 누락되었습니다. 데이터 무결성을 위해 적절한 제한을 추가하는 것이 좋습니다.

-<input type="text" class="input-field" id="edit-petName" placeholder="반려동물 이름을 입력해주세요">
+<input type="text" class="input-field" id="edit-petName" placeholder="반려동물 이름을 입력해주세요" maxlength="30" 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.

            <div class="form-group">
                <label for="edit-petName">반려동물 이름</label>
                <input type="text" class="input-field" id="edit-petName" placeholder="반려동물 이름을 입력해주세요" maxlength="30" required>
            </div>
🤖 Prompt for AI Agents
In src/main/resources/templates/edit-review.html around lines 28 to 31, the
input field for the pet name is missing validation attributes. Add a maxlength
attribute to limit the input length appropriately and a required attribute to
ensure the field is not left empty, thereby enforcing data integrity on the
client side.

22-26: 🛠️ Refactor suggestion

필드 유효성 검증 추가 필요

제목 필드에 maxlength 속성이 설정되어 있지만, 필수 입력 필드에 대한 required 속성이 누락되었습니다. 사용자가 빈 제목으로 폼을 제출할 수 있어 서버 측 검증에 의존하게 됩니다.

-<input type="text" class="input-field" id="edit-review-title" placeholder="후기 제목을 입력해주세요" maxlength="50">
+<input type="text" class="input-field" id="edit-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.

        <form id="editReviewForm">
            <div class="form-group">
                <label for="edit-review-title">제목</label>
                <input type="text" class="input-field" id="edit-review-title"
                       placeholder="후기 제목을 입력해주세요" maxlength="50" required>
            </div>
🤖 Prompt for AI Agents
In src/main/resources/templates/edit-review.html around lines 22 to 26, the
title input field has a maxlength attribute but lacks the required attribute,
allowing empty submissions. Add the required attribute to the input element with
id "edit-review-title" to enforce client-side validation and prevent empty
titles from being submitted.

77-89: 💡 Verification agent

🧩 Analysis chain

이미지 업로드 제한 및 유효성 검증 확인 필요

이미지 업로드 영역에 파일 크기 제한(5MB)과 파일 수 제한(5장)이 명시되어 있지만, JavaScript 코드에서 이러한 제한이 실제로 구현되었는지 확인해야 합니다. 또한 이미지 미리보기 영역의 접근성을 향상시키기 위해 aria-live 속성을 추가하는 것이 좋습니다.

-<div class="image-preview" id="edit-review-imagePreview">
+<div class="image-preview" id="edit-review-imagePreview" aria-live="polite">

🏁 Script executed:

#!/bin/bash
# JavaScript 파일에서 이미지 제한 관련 코드 확인
echo "이미지 크기 제한 검증 코드 확인:"
rg -A 3 "5MB|5000000|5242880" /js/common/edit-review.js || echo "이미지 크기 제한 검증 코드를 찾을 수 없습니다."

echo "\n이미지 수량 제한 검증 코드 확인:"
rg -A 3 "최대 5장|최대 이미지|5장|5개" /js/common/edit-review.js || echo "이미지 수량 제한 검증 코드를 찾을 수 없습니다."

Length of output: 478


아래 스크립트로 edit-review 관련 JavaScript 파일이 실제로 어디에 위치하는지 확인해 주세요.


🏁 Script executed:

#!/bin/bash
echo "edit-review 관련 JS 파일 경로 검색:"
find . -type f -name "*edit-review*.js" -print

Length of output: 166


파일 크기 및 개수 제한 구현 여부를 아래 스크립트로 확인해주세요:


🏁 Script executed:

#!/bin/bash
JS_FILE="src/main/resources/static/js/common/edit-review.js"

echo "=== 파일 크기 제한 검사 (5MB) ==="
rg -n "5242880" "$JS_FILE" || rg -n "5 \\* 1024 \\* 1024" "$JS_FILE" || echo "파일 크기 제한 관련 코드 없음"

echo -e "\n=== 이미지 개수 제한 검사 (5장) ==="
rg -n "MAX_FILES" "$JS_FILE" || rg -n "files?\\.length" -A2 "$JS_FILE" | grep -E "if.*length" || echo "이미지 개수 제한 관련 코드 없음"

Length of output: 583


이미지 업로드 제한 및 접근성 검증 로직 추가 필요

  • src/main/resources/static/js/common/edit-review.js
    • 파일 크기 제한(5MB) 검증 로직이 없습니다.
    • 이미지 개수 제한(5장) 검증 로직이 없습니다.
    → 파일 크기가 5MB를 초과하거나 선택한 이미지 수가 5장을 넘을 경우 업로드를 막고 사용자에게 안내 메시지를 표시하도록 구현하세요.

  • src/main/resources/templates/edit-review.html
    • 이미지 미리보기 영역에 스크린 리더 알림을 위해 aria-live="polite" 속성을 추가하세요.

- <div class="image-preview" id="edit-review-imagePreview">
+ <div class="image-preview" id="edit-review-imagePreview" aria-live="polite">
📝 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.

            <div class="form-group">
                <label for="edit-review-imageFiles">이미지 첨부</label>
                <label for="edit-review-imageFiles" class="upload-area" id="edit-review-uploadArea">
                    <div class="icon">📷</div>
                    <div class="title">이미지를 여기에 드래그하거나</div>
                    <div class="browse">파일 선택하기</div>
                    <div class="formats">PNG, JPG, GIF 최대 5MB (최대 5장)</div>
                    <input type="file" id="edit-review-imageFiles" multiple accept="image/png, image/jpeg, image/gif">
                </label>
+               <div class="image-preview" id="edit-review-imagePreview" aria-live="polite">
                    <!-- 기존 이미지와 새로 업로드한 이미지 미리보기가 여기에 표시됩니다 -->
                </div>
            </div>
🤖 Prompt for AI Agents
In src/main/resources/templates/edit-review.html around lines 77 to 89, add the
attribute aria-live="polite" to the div with id "edit-review-imagePreview" to
improve accessibility by enabling screen readers to announce image preview
updates. In src/main/resources/static/js/common/edit-review.js, implement
validation logic to enforce the 5MB file size limit and the maximum of 5 images
upload limit. When a user selects files, check each file's size and the total
number of files; if any file exceeds 5MB or the total exceeds 5, prevent the
upload and display a user-friendly error message.
src/main/resources/templates/post-review-form.html (6)

38-70: 🛠️ Refactor suggestion

반려동물 종류 선택 필수 여부 표시 필요

라디오 버튼 그룹에 기본 선택 값이 설정되어 있지 않고 필수 선택인지 여부가 명확하지 않습니다. 적어도 하나의 옵션을 필수로 선택하도록 설정하고, 사용자에게 필수 필드임을 시각적으로 표시하는 것이 좋습니다.

-                <label>반려동물 종류</label>
+                <label>반려동물 종류 <span class="required">*</span></label>
                 <div class="radio-group">
                     <div class="radio-item">
-                        <input type="radio" id="review-pet-dog" name="review-petType" value="DOG">
+                        <input type="radio" id="review-pet-dog" name="review-petType" value="DOG" required>

또한 CSS에 필수 필드 표시를 위한 스타일을 추가하는 것을 고려하세요:

.required {
  color: red;
  margin-left: 3px;
}
🤖 Prompt for AI Agents
In src/main/resources/templates/post-review-form.html between lines 38 and 70,
the radio button group for selecting the pet type lacks a default selection and
does not indicate that this is a required field. To fix this, set the first
radio input (e.g., DOG) to be checked by default and add a visual indicator such
as a red asterisk next to the label "반려동물 종류" to show it is mandatory. Also,
update the CSS to include a .required class with red color and margin as
suggested, and apply this class to the asterisk or label to clearly mark the
field as required.

22-26: 🛠️ Refactor suggestion

필드 유효성 검증 추가 필요

제목 필드에 maxlength 속성이 설정되어 있지만, 필수 입력 필드에 대한 required 속성이 누락되었습니다. 사용자가 빈 제목으로 폼을 제출할 수 있어 서버 측 검증에 의존하게 됩니다.

-<input type="text" class="input-field" id="review-title" placeholder="후기 제목을 입력해주세요" maxlength="50">
+<input type="text" 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.

        <form id="reviewForm">
            <div class="form-group">
                <label for="review-title">제목</label>
                <input type="text" class="input-field" id="review-title" placeholder="후기 제목을 입력해주세요" maxlength="50" required>
            </div>
🤖 Prompt for AI Agents
In src/main/resources/templates/post-review-form.html around lines 22 to 26, the
input field for the review title has a maxlength attribute but lacks the
required attribute, allowing empty submissions. Add the required attribute to
the input element to enforce client-side validation and prevent empty titles
from being submitted.

28-31: 🛠️ Refactor suggestion

반려동물 이름 필드 검증 누락

반려동물 이름 필드에 maxlengthrequired 속성이 누락되었습니다. 데이터 무결성을 위해 적절한 제한을 추가하는 것이 좋습니다.

-<input type="text" class="input-field" id="petName" placeholder="반려동물 이름을 입력해주세요">
+<input type="text" class="input-field" id="petName" placeholder="반려동물 이름을 입력해주세요" maxlength="30" 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.

            <div class="form-group">
                <label for="petName">반려동물 이름</label>
                <input type="text" class="input-field" id="petName" placeholder="반려동물 이름을 입력해주세요" maxlength="30" required>
            </div>
🤖 Prompt for AI Agents
In src/main/resources/templates/post-review-form.html around lines 28 to 31, the
input field for the pet name is missing validation attributes. Add a maxlength
attribute to limit the input length appropriately and a required attribute to
ensure the field is not left empty, enhancing data integrity and user input
validation.

72-75: 🛠️ Refactor suggestion

후기 내용 필수 여부 표시 필요

후기 내용 텍스트 영역에 maxlength 속성은 있지만 required 속성이 누락되었습니다. 주요 콘텐츠 필드로 필수 입력으로 표시하는 것이 좋습니다.

-<textarea class="input-field" id="review-content" placeholder="여행 경험을 자유롭게 작성해주세요" maxlength="2000"></textarea>
+<textarea 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.

            <div class="form-group">
                <label for="review-content">후기 내용</label>
                <textarea class="input-field" id="review-content" placeholder="여행 경험을 자유롭게 작성해주세요" maxlength="2000" required></textarea>
            </div>
🤖 Prompt for AI Agents
In src/main/resources/templates/post-review-form.html around lines 72 to 75, the
textarea for the review content lacks the required attribute. Add the required
attribute to the textarea element to make the review content a mandatory field
for form submission.

33-36: 🛠️ Refactor suggestion

여행 장소 필드 검증 누락

여행 장소 필드에도 maxlengthrequired 속성이 누락되었습니다. 일관된 유효성 검증을 위해 적절한 제한을 추가해야 합니다.

-<input type="text" class="input-field" id="region" placeholder="여행 장소를 입력해주세요">
+<input type="text" class="input-field" id="region" 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.

            <div class="form-group">
                <label for="region">여행 장소</label>
-               <input type="text" class="input-field" id="region" placeholder="여행 장소를 입력해주세요">
+               <input type="text" class="input-field" id="region" placeholder="여행 장소를 입력해주세요" maxlength="50" required>
            </div>
🤖 Prompt for AI Agents
In src/main/resources/templates/post-review-form.html around lines 33 to 36, the
input field for "여행 장소" is missing validation attributes. Add the maxlength
attribute with an appropriate character limit and the required attribute to
ensure the field is not left empty, maintaining consistent validation across the
form.

77-89: 💡 Verification agent

🧩 Analysis chain

이미지 업로드 제한 및 유효성 검증 확인 필요

이미지 업로드 영역에 파일 크기 제한(5MB)과 파일 수 제한(5장)이 명시되어 있지만, JavaScript 코드에서 이러한 제한이 실제로 구현되었는지 확인해야 합니다. 또한 이미지 미리보기 영역의 접근성을 향상시키기 위해 aria-live 속성을 추가하는 것이 좋습니다.

-<div class="image-preview" id="review-imagePreview">
+<div class="image-preview" id="review-imagePreview" aria-live="polite">

🏁 Script executed:

#!/bin/bash
# JavaScript 파일에서 이미지 제한 관련 코드 확인
echo "이미지 크기 제한 검증 코드 확인:"
rg -A 3 "5MB|5000000|5242880" /js/common/form.js || echo "이미지 크기 제한 검증 코드를 찾을 수 없습니다."

echo "\n이미지 수량 제한 검증 코드 확인:"
rg -A 3 "최대 5장|최대 이미지|5장|5개" /js/common/form.js || echo "이미지 수량 제한 검증 코드를 찾을 수 없습니다."

Length of output: 450


다음 스크립트를 통해 post-review-form.html에서 실제 연결된 JS 파일 경로를 확인하고, 해당 파일 내에 업로드 제한 로직이 구현되어 있는지 추가로 검증해보겠습니다.


🏁 Script executed:

#!/bin/bash
echo "1) post-review-form.html 내 <script> 태그 확인:"
rg "<script[^>]*src" -n src/main/resources/templates/post-review-form.html || echo "스크립트 태그를 찾을 수 없습니다."

echo -e "\n2) 'review-imageFiles' 참조 검색 (JS 전체):"
rg "review-imageFiles" -n .

echo -e "\n3) 'review-imagePreview' 참조 검색 (JS 전체):"
rg "review-imagePreview" -n .

echo -e "\n4) 프로젝트 내 모든 JS 파일 리스트업:"
find . -type f -name "*.js" | sed 's/^/   - /'

Length of output: 2595


추가로 form.js 내 업로드 제한 로직을 확인합니다.


🏁 Script executed:

#!/bin/bash
echo "=== 'review-imageFiles' 이벤트 핸들러 주변 코드 ==="
rg -n "review-imageFiles" -A30 -B5 src/main/resources/static/js/common/form.js || echo "핸들러 코드 없음"

echo -e "\n=== 파일 크기 검증 (file.size) 검색 ==="
rg -n "file.size" src/main/resources/static/js/common/form.js || echo "file.size 검증 로직 없음"

echo -e "\n=== 파일 갯수 검증 (files.length) 검색 ==="
rg -n "files.length" src/main/resources/static/js/common/form.js || echo "files.length 검증 로직 없음"

Length of output: 1548


이미지 업로드 제한 로직 확인 및 접근성 개선 제안

handleImageUpload 함수에서 이미 MAX_FILE_COUNT(5)와 MAX_FILE_SIZE_MB(5)를 이용한 파일 수·크기 검증이 구현되어 있습니다. 별도 추가 개발은 불필요하며, 접근성 향상을 위해 이미지 미리보기 영역에 aria-live="polite" 속성을 추가해주세요.

• 수정 대상

  • 파일: src/main/resources/templates/post-review-form.html
  • 위치: 이미지 미리보기 <div> 태그
-<div class="image-preview" id="review-imagePreview">
+<div class="image-preview" id="review-imagePreview" aria-live="polite">
📝 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.

            <div class="form-group">
                <label for="review-imageFiles">이미지 첨부</label>
                <label for="review-imageFiles" class="upload-area" id="review-uploadArea">
                    <div class="icon">📷</div>
                    <div class="title">이미지를 여기에 드래그하거나</div>
                    <div class="browse">파일 선택하기</div>
                    <div class="formats">PNG, JPG, GIF 최대 5MB (최대 5장)</div>
                    <input type="file" id="review-imageFiles" multiple accept="image/png, image/jpeg, image/gif">
                </label>
                <div class="image-preview" id="review-imagePreview" aria-live="polite">
                    <!-- 기존 이미지와 새로 업로드한 이미지 미리보기가 여기에 표시됩니다 -->
                </div>
            </div>
🤖 Prompt for AI Agents
In src/main/resources/templates/post-review-form.html around lines 77 to 89, the
image preview <div> with id "review-imagePreview" lacks accessibility support.
To improve accessibility, add the attribute aria-live="polite" to this <div> so
screen readers announce updates to the preview area. No changes to the existing
file count or size validation logic are needed as they are already implemented
in the JavaScript.
src/main/java/io/github/petty/community/dto/PostDetailResponse.java (1)

24-24: 🛠️ Refactor suggestion

컬렉션 필드 보호 필요

images 필드는 컬렉션 타입인데, 빈 컬렉션인 경우와 null인 경우를 구분할 수 있도록 기본값을 빈 리스트로 설정하는 것이 좋습니다. 또한 불변 컬렉션을 사용하여 리스트가 외부에서 수정되지 않도록 보호하는 것이 좋습니다.

-    private List<PostImageResponse> images;
+    @Builder.Default
+    private List<PostImageResponse> images = Collections.emptyList();

필드 선언 위에 다음 import 문을 추가해야 합니다:

import java.util.Collections;

또한 필요하다면 getter 메서드를 오버라이드하여 불변 컬렉션을 반환하도록 할 수 있습니다:

public List<PostImageResponse> getImages() {
    return Collections.unmodifiableList(images);
}
🤖 Prompt for AI Agents
In src/main/java/io/github/petty/community/dto/PostDetailResponse.java at line
24, the images field is a mutable collection that can be null or empty.
Initialize images with an empty list to avoid null and import
java.util.Collections. Override the getter method to return an unmodifiable list
using Collections.unmodifiableList(images) to protect the collection from
external modification.
src/main/java/io/github/petty/community/entity/PostImage.java (4)

26-28: 🛠️ Refactor suggestion

이미지 URL 길이 제한 필요

이미지 URL 필드에 길이 제한이 설정되어 있지 않습니다. 데이터베이스 칼럼 크기를 제한하기 위해 @Column 어노테이션에 length 속성을 추가하는 것이 좋습니다.

-    @Column(name = "image_url", nullable = false)
+    @Column(name = "image_url", nullable = false, length = 512)
📝 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.

    @Column(name = "image_url", nullable = false, length = 512)
    private String imageUrl;
🤖 Prompt for AI Agents
In src/main/java/io/github/petty/community/entity/PostImage.java around lines 26
to 28, the imageUrl field's database column lacks a length restriction. To fix
this, add the length attribute to the @Column annotation for imageUrl,
specifying an appropriate maximum length to limit the size of the stored URL
string in the database.

29-32: 🛠️ Refactor suggestion

이미지 순서 값 범위 제한 필요

이미지 순서(ordering) 필드는 주석에 따르면 0~4 범위를 가져야 하지만, 이 범위를 강제하는 제약 조건이 없습니다. @Min@Max 어노테이션을 사용하여 값 범위를 검증하는 것이 좋습니다.

+import jakarta.validation.constraints.Min;
+import jakarta.validation.constraints.Max;

 // 이미지 순서 (0~4)
 @Builder.Default
 @Column(nullable = false)
+@Min(0)
+@Max(4)
 private Integer ordering = 0;
📝 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.

// Add at the top of the file alongside your other imports:
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.Max;

    // 이미지 순서 (0~4)
    @Builder.Default
    @Column(nullable = false)
    @Min(0)
    @Max(4)
    private Integer ordering = 0;
🤖 Prompt for AI Agents
In src/main/java/io/github/petty/community/entity/PostImage.java around lines 29
to 32, the ordering field should be constrained to values between 0 and 4 as per
the comment. Add @Min(0) and @Max(4) annotations to the ordering field to
enforce this range validation at the entity level.

35-40: ⚠️ Potential issue

양방향 관계 설정 메서드 개선 필요

양방향 관계 설정 메서드는 Post 객체의 images 컬렉션이 null인 경우에 대한 처리가 누락되어 있습니다. NullPointerException을 방지하기 위해 null 체크를 추가하는 것이 좋습니다.

 // 유틸 메서드 (양방향 연관관계 설정)
 public void setPost(Post post) {
     this.post = post;
-    if (!post.getImages().contains(this)) {
+    if (post != null && post.getImages() != null && !post.getImages().contains(this)) {
         post.getImages().add(this);
     }
 }
📝 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.

    public void setPost(Post post) {
        this.post = post;
        if (post != null && post.getImages() != null && !post.getImages().contains(this)) {
            post.getImages().add(this);
        }
    }
🤖 Prompt for AI Agents
In src/main/java/io/github/petty/community/entity/PostImage.java around lines 35
to 40, the setPost method does not check if the images collection in the Post
object is null before accessing it. To fix this, add a null check for
post.getImages() before calling contains or add, and initialize the collection
if it is null to prevent NullPointerException.

1-41: 💡 Verification agent

🧩 Analysis chain

커뮤니티 기능 좋아요 및 댓글 작동 확인 필요

PR 목표에 따르면 좋아요 및 댓글 기능이 추가되었지만, 이 기능들이 올바르게 동작하는지 확인이 필요합니다. 특히 카운트 업데이트가 제대로 이루어지는지 확인해야 합니다.

좋아요와 댓글 카운트 업데이트 로직에 대한 검증이 필요합니다. PR에서 언급된 대로 카운트가 정확하게 업데이트되는지 확인할 수 있는 단위 테스트 또는 통합 테스트를 추가하는 것이 좋습니다. 필요하시면 테스트 코드 작성을 도와드릴 수 있습니다.


🌐 Web query:

What are best practices for implementing like and comment count updates in Spring Boot applications?

💡 Result:

Implementing like and comment count updates in Spring Boot applications involves considerations for performance, consistency, and user experience. Here are some best practices to follow:

  1. Use Atomic Operations for Updates:

    • When incrementing or decrementing counts (likes, comments), use atomic operations to avoid race conditions. This can be achieved using database-level atomic updates (e.g., UPDATE table SET like_count = like_count + 1 WHERE id = ?).
    • Avoid fetching the count, incrementing in code, then persisting, as this can cause inconsistencies under concurrent requests.
  2. Database Design:

    • Store counts as separate columns in your entities (e.g., like_count, comment_count). This reduces the need for expensive counts via joins or aggregate queries.
    • Maintain separate tables for likes and comments with proper indexing to efficiently query for existence and counts.
  3. Caching:

    • Use caching layers (e.g., Redis or in-memory caches) to reduce database load on frequently accessed counts.
    • Update the cache immediately after a like or comment event, and sync back to the database asynchronously if needed.
  4. Event-Driven or Asynchronous Updates:

    • For high traffic applications, consider an event-driven model where like/comment actions publish events to a message broker (Kafka, RabbitMQ).
    • Process and update counts asynchronously to reduce write latency on user actions.
    • This can also allow batch updating counts, reducing the number of database writes.
  5. Optimistic Locking or Versioning:

    • Use optimistic locking if you do in-memory increments on entities before saving to avoid lost updates due to concurrent modifications.
  6. API and Transaction Management:

    • Keep the like/comment update operations transactional to ensure data integrity.
    • Use REST endpoints designed to handle these updates efficiently.
  7. Prevent Duplicate Likes or Comments:

    • For likes, ensure a user can only like once by validating presence before allowing an increment.
    • Implement idempotent operations or checks in your service layer.
  8. Use DTOs and Projections:

    • Use lightweight DTOs or JPA projections to fetch only counts when rendering data to minimize overhead.

Example snippet for atomic update in Spring Data JPA:

@Modifying
@Query("UPDATE Post p SET p.likeCount = p.likeCount + 1 WHERE p.id = :postId")
int incrementLikeCount(@Param("postId") Long postId);

Summary: Use atomic database updates, caching, asynchronous event processing, and proper transaction management to build scalable and reliable like/comment count updating in Spring Boot applications. If you'd like, I can provide a code example for a specific part.


커뮤니티 좋아요·댓글 카운트 업데이트 검증 및 테스트 추가 필요

이번 PR에서 좋아요 및 댓글 기능이 구현되었으나, 다음 사항을 반드시 확인하고 관련 테스트를 추가해주세요:

  • DB 원자성 보장 확인
    @Modifying + SQL UPDATE ... SET like_count = like_count + 1 방식으로 동시성 업데이트 처리
    • 낙관적 잠금(@Version) 적용 여부 검토
  • 중복 좋아요 방지 로직 검증
    • 동일 유저가 다중 요청할 때 한 번만 반영되는지 확인
  • 트랜잭션 관리
    • 서비스 레이어에서 카운트 업데이트가 모두 성공하거나 롤백되는지 확인
  • 캐시 동기화(선택 사항)
    • Redis 등 사용 시 캐시→DB 동기화 로직 검증
  • 테스트 코드 작성
    • 단위 테스트: 순차적 증가/감소 검증
    • 통합 테스트: 동시성 시나리오(멀티스레드) 포함

필요하시면 테스트 코드 예시도 함께 제공해드리겠습니다.

🤖 Prompt for AI Agents
In src/main/java/io/github/petty/community/entity/PostImage.java lines 1 to 41,
the review highlights the need to verify and add tests for like and comment
count updates in the community features. To address this, ensure that the like
and comment count updates use atomic database operations (e.g., @Modifying
queries with increment statements) to handle concurrency, verify that duplicate
likes by the same user are prevented, and that transaction management guarantees
consistency. Additionally, add unit tests to check sequential
increments/decrements and integration tests simulating concurrent updates. If
caching is used, validate cache synchronization with the database. Implement
these checks and tests in the relevant service and repository layers associated
with post likes and comments.
src/main/resources/templates/edit-qna.html (3)

78-90: 🛠️ Refactor suggestion

이미지 업로드 영역에 유효성 검사 피드백이 필요합니다.

사용자가 잘못된 형식의 파일이나 크기 제한을 초과하는 파일을 업로드할 경우 표시할 오류 메시지 영역이 없습니다. 오류 메시지를 표시할 영역을 추가하는 것이 좋습니다.

<div class="image-preview" id="edit-qna-imagePreview">
    <!-- 기존 이미지와 새로 업로드한 이미지 미리보기가 여기에 표시됩니다 -->
</div>
+<div class="error-message" id="edit-qna-imageError" style="display: none; color: red;"></div>
📝 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.

            <div class="form-group">
                <label for="edit-qna-imageFiles">이미지 첨부</label>
                <label for="edit-qna-imageFiles" class="upload-area" id="edit-qna-uploadArea">
                    <div class="icon">📷</div>
                    <div class="title">이미지를 여기에 드래그하거나</div>
                    <div class="browse">파일 선택하기</div>
                    <div class="formats">PNG, JPG, GIF 최대 5MB (최대 5장)</div>
                    <input type="file" id="edit-qna-imageFiles" multiple accept="image/png, image/jpeg, image/gif">
                </label>
                <div class="image-preview" id="edit-qna-imagePreview">
                    <!-- 기존 이미지와 새로 업로드한 이미지 미리보기가 여기에 표시됩니다 -->
                </div>
+               <div class="error-message" id="edit-qna-imageError" style="display: none; color: red;"></div>
            </div>
🤖 Prompt for AI Agents
In src/main/resources/templates/edit-qna.html around lines 78 to 90, the image
upload section lacks an area to display validation error messages for invalid
file types or files exceeding size limits. Add a dedicated element below the
file input or preview area to show these error messages dynamically when
validation fails, ensuring users receive clear feedback on upload issues.

22-26: 🛠️ Refactor suggestion

CSRF 보호 추가가 필요합니다.

Spring Security를 사용한다면, 폼에 CSRF 토큰을 추가하는 것이 보안상 중요합니다.

<form id="editQnaForm">
+   <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}" />
    <div class="form-group">
📝 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.

        <form id="editQnaForm">
            <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}" />
            <div class="form-group">
                <label for="edit-qna-title">제목</label>
                <input type="text" class="input-field" id="edit-qna-title"
                       placeholder="질문의 제목을 입력해주세요" maxlength="50">
            </div>
🤖 Prompt for AI Agents
In src/main/resources/templates/edit-qna.html around lines 22 to 26, the form
with id "editQnaForm" lacks CSRF protection. To fix this, add a hidden input
field containing the CSRF token inside the form. If using Spring Security,
include the CSRF token by adding a hidden input with the name and value set to
the CSRF parameter and token, typically using Thymeleaf syntax or manually
injecting the token into the form.

100-100: 💡 Verification agent

🧩 Analysis chain

외부 JavaScript 파일의 에러 처리 확인이 필요합니다.

edit-qna.js 파일에서 적절한 오류 처리가 구현되어 있는지 확인해주세요. 특히 API 호출 실패 시 사용자에게 적절한 피드백을 제공하는지 검증하는 것이 중요합니다.


🌐 Web query:

자바스크립트에서 axios 또는 fetch API 사용 시 오류 처리 모범 사례는 무엇인가요?

💡 Result:

자바스크립트에서 axios 또는 fetch API를 사용할 때 오류 처리는 네트워크 문제, 서버 에러, 응답값 문제 등 다양한 상황을 다뤄야 하므로 중요한 부분입니다. 다음은 모범 사례들입니다.

1. 공통 모범 사례

  • try-catch 사용: 비동기 함수 내에서 오류를 잡기 위해 try-catch를 사용해 에러를 처리합니다.
  • 응답 상태 코드 확인: 서버가 200~299 범위가 아닌 응답을 반환하는 경우를 반드시 처리합니다.
  • 명확한 오류 메시지 제공: 사용자 또는 개발자가 오류를 쉽게 이해할 수 있도록 의미 있는 메시지를 제공합니다.
  • 재시도 로직 (선택적): 네트워크 오류가 자주 발생하는 경우에는 재시도 로직을 구현할 수 있습니다.
  • 로깅 및 모니터링: 발생한 오류를 로그로 기록하고, 필요 시 모니터링 도구와 연동합니다.

2. axios 오류 처리 모범 사례

import axios from 'axios';

async function fetchData() {
  try {
    const response = await axios.get('https://api.example.com/data');
    // 상태 코드가 2xx가 아닐 경우 에러 발생
    if (response.status < 200 || response.status >= 300) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }
    return response.data;
  } catch (error) {
    if (error.response) {
      // 서버 응답이 있었지만 상태 코드가 2xx가 아닐 때
      console.error('Response error:', error.response.status, error.response.data);
    } else if (error.request) {
      // 요청은 되었으나 응답이 없을 때
      console.error('No response received:', error.request);
    } else {
      // 요청 설정 중 발생한 에러
      console.error('Error setting up request:', error.message);
    }
    throw error;  // 필요 시 사용자에게 재전달
  }
}

3. fetch API 오류 처리 모범 사례

async function fetchData() {
  try {
    const response = await fetch('https://api.example.com/data');
    if (!response.ok) {  // 상태 코드가 200~299가 아니면
      throw new Error(`HTTP error! status: ${response.status}`);
    }
    const data = await response.json();  // 또는 response.text() 등
    return data;
  } catch (error) {
    console.error('Fetch error:', error.message);
    throw error;  // 필요 시 사용자에게 재전달
  }
}

추가 팁

  • 사용자 피드백: 오류 시 사용자에게 친근한 안내 메시지를 제공하세요.
  • 타임아웃 설정: axios에서는 timeout 옵션으로, fetch에서는 AbortController를 사용해 요청 시간 제한을 둡니다.
  • 공통 에러 처리 함수 정의: 여러 API 요청이 있을 경우 재사용 가능한 에러 핸들러를 만들어 중복 코드를 줄입니다.

필요하면 이와 관련해 더 구체적인 예시나 고급 패턴도 안내해 드릴 수 있습니다!


edit-qna.js 파일 내에서 API 호출과 에러 처리(catch) 구현 여부를 확인하기 위해 소스 전체를 검색해 보겠습니다. 아래 스크립트를 실행해주세요.


🏁 Script executed:

#!/bin/bash
set -e
# edit-qna.js 경로 탐색
FILE=$(fd edit-qna.js)
echo "파일 위치: $FILE"
# fetch 또는 axios 호출 탐색
rg -n 'fetch\(' "$FILE"
rg -n 'axios' "$FILE"
# catch 블록 탐색
rg -n 'catch' "$FILE"

Length of output: 501


edit-qna.js의 API 호출에 에러 처리가 누락되어 있습니다

src/main/resources/static/js/common/edit-qna.js 파일에서 fetch를 사용하는 세 곳 모두 오류 처리(try-catch, response.ok 검사 등)가 없습니다. 사용자 피드백을 위해 다음 위치에 에러 핸들링을 추가해주세요:

  • 30행 근처:
    const res = await fetch(`/api/posts/${postId}`, {});
  • 60행 근처:
    const res = await fetch(`/api/posts/${postId}`);
  • 133행 근처:
    const res = await fetch('/api/images/upload/multi', {});

각 호출부를 예시처럼 감싸고,

  1. try { … } catch (error) { … } 로 네트워크/파싱 오류를 처리
  2. if (!res.ok) throw new Error(\HTTP ${res.status}`);` 로 상태 코드 검사
  3. 사용자에게 친절한 오류 메시지(알림창 또는 화면 내 텍스트) 제공
    등을 적용해주세요.
🤖 Prompt for AI Agents
In src/main/resources/static/js/common/edit-qna.js around lines 30, 60, and 133,
the fetch API calls lack proper error handling. Wrap each fetch call in a
try-catch block to handle network or parsing errors, check the response status
with if (!res.ok) to throw an error for non-2xx responses, and provide
user-friendly feedback such as alert messages or UI notifications when errors
occur.
src/main/java/io/github/petty/community/repository/PostRepository.java (1)

19-20: 🛠️ Refactor suggestion

표준 메서드 오버라이딩 주의가 필요합니다.

표준 findById 메서드를 오버라이드하여 EntityGraph를 적용한 것은 기본 동작을 변경합니다. 이로 인해 예상치 못한 동작이나 혼란이 발생할 수 있습니다. 대신 다른 이름의 메서드를 사용하는 것이 더 명확할 수 있습니다.

-@EntityGraph(attributePaths = "images")
-Optional<Post> findById(Long id);
+@EntityGraph(attributePaths = "images")
+Optional<Post> findByIdWithImages(Long id);
🤖 Prompt for AI Agents
In src/main/java/io/github/petty/community/repository/PostRepository.java at
lines 19-20, the standard findById method is overridden with an EntityGraph
annotation, which changes its default behavior and may cause confusion. To fix
this, rename this method to a custom name (e.g., findByIdWithImages) to clearly
distinguish it from the standard findById, preserving the original method's
behavior and avoiding unexpected side effects.
src/main/java/io/github/petty/community/controller/PostViewController.java (5)

6-7: ⚠️ Potential issue

인증 및 권한 검사가 필요합니다.

편집 페이지에 접근하기 전에 사용자 인증 및 권한 검사를 추가하는 것이 중요합니다. 현재는 아무나 편집 페이지에 접근할 수 있어 보안 문제가 발생할 수 있습니다.

@Controller
+@PreAuthorize("isAuthenticated()")
public class PostViewController {

추가로 필요한 import:

import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.PathVariable;
🤖 Prompt for AI Agents
In src/main/java/io/github/petty/community/controller/PostViewController.java
around lines 6 to 7, add user authentication and authorization checks to
restrict access to the edit page. Import the necessary classes: PreAuthorize,
Model, and PathVariable. Then, annotate the relevant controller methods with
@PreAuthorize to enforce that only authorized users can access the edit
functionality, ensuring proper security measures are in place.

9-23: 🛠️ Refactor suggestion

후기 게시판 관련 엔드포인트 개선이 필요합니다.

후기 게시판의 편집 페이지에서 특정 게시물 ID를 경로 변수로 받지 않고 있습니다. RESTful URL 구조를 유지하기 위해 경로 변수를 추가하고 모델에 게시물 ID를 전달하는 것이 좋습니다.

-@GetMapping("/posts/review/edit")
-public String reviewEditPage() {
-    return "edit-review";
+@GetMapping("/posts/review/edit/{id}")
+public String reviewEditPage(@PathVariable Long id, Model model) {
+    model.addAttribute("postId", id);
+    return "edit-review";
📝 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.

    // 📌 후기 게시판
    @GetMapping("/posts/review")
    public String reviewListPage() {
        return "post-review-list";
    }

    @GetMapping("/posts/review/new")
    public String reviewFormPage() {
        return "post-review-form";
    }

-   @GetMapping("/posts/review/edit")
-   public String reviewEditPage() {
-       return "edit-review";
-   }
+   @GetMapping("/posts/review/edit/{id}")
+   public String reviewEditPage(@PathVariable Long id, Model model) {
+       model.addAttribute("postId", id);
+       return "edit-review";
+   }
🤖 Prompt for AI Agents
In src/main/java/io/github/petty/community/controller/PostViewController.java
around lines 9 to 23, the reviewEditPage method's endpoint does not accept a
post ID as a path variable, which breaks RESTful URL conventions. Modify the
@GetMapping annotation to include a path variable for the post ID, update the
method signature to accept this ID as a parameter, and add the post ID to the
model attributes so it can be used in the view for editing the specific post.

57-61: 🛠️ Refactor suggestion

게시물 상세 페이지 엔드포인트 개선이 필요합니다.

상세 페이지에서도 특정 게시물 ID를 경로 변수로 받지 않고 있습니다. RESTful URL 구조를 위해 경로 변수를 추가하고, 게시물 유형도 경로에 포함하는 것이 좋습니다.

-@GetMapping("/posts/detail")
-public String detailPage() {
-    return "post-detail";
+@GetMapping("/posts/{type}/{id}")
+public String detailPage(@PathVariable String type, @PathVariable Long id, Model model) {
+    model.addAttribute("postType", type);
+    model.addAttribute("postId", id);
+    return "post-detail";
📝 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.

    // 📌 상세페이지 (공통)
    @GetMapping("/posts/{type}/{id}")
    public String detailPage(
            @PathVariable String type,
            @PathVariable Long id,
            Model model
    ) {
        model.addAttribute("postType", type);
        model.addAttribute("postId", id);
        return "post-detail";
    }
🤖 Prompt for AI Agents
In src/main/java/io/github/petty/community/controller/PostViewController.java
around lines 57 to 61, the detailPage method does not accept a post ID or post
type as path variables, which is not RESTful. Modify the @GetMapping annotation
to include path variables for post type and post ID, and update the method
signature to accept these variables as parameters. This will allow the URL to
reflect the specific post being requested, improving clarity and RESTful design.

25-39: 🛠️ Refactor suggestion

자랑 게시판 관련 엔드포인트 개선이 필요합니다.

자랑 게시판의 편집 페이지에서도 특정 게시물 ID를 경로 변수로 받지 않고 있습니다. RESTful URL 구조를 위해 경로 변수를 추가하는 것이 좋습니다.

-@GetMapping("/posts/showoff/edit")
-public String showoffEditPage() {
-    return "edit-showoff";
+@GetMapping("/posts/showoff/edit/{id}")
+public String showoffEditPage(@PathVariable Long id, Model model) {
+    model.addAttribute("postId", id);
+    return "edit-showoff";
📝 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.

    // 📌 자랑 게시판
    @GetMapping("/posts/showoff")
    public String showoffListPage() {
        return "post-showoff-list";
    }

    @GetMapping("/posts/showoff/new")
    public String showoffFormPage() {
        return "post-showoff-form";
    }

    @GetMapping("/posts/showoff/edit/{id}")
    public String showoffEditPage(@PathVariable Long id, Model model) {
        model.addAttribute("postId", id);
        return "edit-showoff";
    }
🤖 Prompt for AI Agents
In src/main/java/io/github/petty/community/controller/PostViewController.java
between lines 25 and 39, the showoffEditPage method's @GetMapping does not
include a path variable for the post ID. To follow RESTful URL conventions,
modify the @GetMapping annotation to include a path variable like
"/posts/showoff/edit/{id}" and update the method signature to accept this ID as
a parameter.

41-55: 🛠️ Refactor suggestion

질문 게시판 관련 엔드포인트 개선이 필요합니다.

질문 게시판의 편집 페이지에서도 특정 게시물 ID를 경로 변수로 받지 않고 있습니다. RESTful URL 구조를 위해 경로 변수를 추가하는 것이 좋습니다.

-@GetMapping("/posts/qna/edit")
-public String qnaEditPage() {
-    return "edit-qna";
+@GetMapping("/posts/qna/edit/{id}")
+public String qnaEditPage(@PathVariable Long id, Model model) {
+    model.addAttribute("postId", id);
+    return "edit-qna";
📝 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.

    // 📌 질문 게시판
    @GetMapping("/posts/qna")
    public String qnaListPage() {
        return "post-qna-list";
    }

    @GetMapping("/posts/qna/new")
    public String qnaFormPage() {
        return "post-qna-form";
    }

    @GetMapping("/posts/qna/edit/{id}")
    public String qnaEditPage(@PathVariable Long id, Model model) {
        model.addAttribute("postId", id);
        return "edit-qna";
    }
🤖 Prompt for AI Agents
In src/main/java/io/github/petty/community/controller/PostViewController.java
between lines 41 and 55, the qnaEditPage method's endpoint does not accept a
post ID as a path variable, which is needed for a RESTful URL structure. Modify
the @GetMapping annotation to include a path variable for the post ID (e.g.,
"/posts/qna/edit/{id}"), and update the method signature to accept this ID as a
parameter. This change will allow the edit page to load the specific post based
on its ID.
src/main/resources/templates/post-qna-form.html (3)

22-26: 🛠️ Refactor suggestion

폼 제출 전 필수 필드 검증이 필요합니다.

현재 타이틀 입력 필드에는 maxlength 속성이 설정되어 있지만, required 속성이 없습니다. 빈 제목으로 폼이 제출될 수 있습니다.

- <input type="text" class="input-field" id="title" placeholder="질문의 제목을 입력해주세요" maxlength="50">
+ <input type="text" 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.

        <form id="postForm">
            <div class="form-group">
                <label for="title">제목</label>
-               <input type="text" class="input-field" id="title" placeholder="질문의 제목을 입력해주세요" maxlength="50">
+               <input type="text" 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 22 to 26, the
title input field lacks the required attribute, allowing the form to be
submitted with an empty title. Add the required attribute to the input element
with id "title" to enforce client-side validation that prevents empty
submissions.

81-85: 💡 Verification agent

🧩 Analysis chain

폼 액션과 CSRF 보호가 필요합니다.

폼에 action 속성이 없어 JavaScript로 처리되는 것으로 보이지만, 백엔드에서 CSRF 보호가 활성화되어 있다면 CSRF 토큰을 추가해야 합니다.


🏁 Script executed:

#!/bin/bash
# Spring Security가 CSRF 보호를 사용하는지 확인
grep -r "csrf" src/main/java

Length of output: 131


action 속성 명시 필요 (CSRF는 비활성화됨)
SecurityConfig.java에서 CSRF가 .csrf(csrf -> csrf.disable())로 비활성화되어 있어 CSRF 토큰 추가는 필요하지 않습니다.

  • src/main/resources/templates/post-qna-form.html
    <form> 태그에 적절한 action 속성(e.g. /qna)을 명시하여 요청 엔드포인트를 분명히 지정해주세요.
🤖 Prompt for AI Agents
In src/main/resources/templates/post-qna-form.html around lines 81 to 85, the
form tag lacks an action attribute, which is necessary to clearly specify the
request endpoint. Since CSRF protection is disabled in SecurityConfig.java, you
do not need to add a CSRF token, but you should add an appropriate action
attribute (e.g., action="/qna") to the form tag to explicitly define where the
form data should be submitted.

28-60: 🛠️ Refactor suggestion

펫 타입에 기본값 설정이 필요합니다.

현재 반려동물 타입 선택 라디오 버튼에 기본 선택값이 설정되어 있지 않습니다. 사용자가 선택하지 않고 제출하면 문제가 발생할 수 있습니다.

- <input type="radio" id="pet-dog" name="petType" value="DOG">
+ <input type="radio" id="pet-dog" name="petType" value="DOG" checked>
📝 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.

            <div class="form-group">
                <label>반려동물 종류</label>
                <div class="radio-group">
                    <div class="radio-item">
                        <input type="radio" id="pet-dog" name="petType" value="DOG" checked>
                        <label for="pet-dog">강아지</label>
                    </div>
                    <div class="radio-item">
                        <input type="radio" id="pet-cat" name="petType" value="CAT">
                        <label for="pet-cat">고양이</label>
                    </div>
                    <div class="radio-item">
                        <input type="radio" id="pet-rabbit" name="petType" value="RABBIT">
                        <label for="pet-rabbit">토끼</label>
                    </div>
                    <div class="radio-item">
                        <input type="radio" id="pet-hamster" name="petType" value="HAMSTER">
                        <label for="pet-hamster">햄스터</label>
                    </div>
                    <div class="radio-item">
                        <input type="radio" id="pet-bird" name="petType" value="PARROT">
                        <label for="pet-bird">앵무새</label>
                    </div>
                    <div class="radio-item">
                        <input type="radio" id="pet-reptile" name="petType" value="REPTILE">
                        <label for="pet-reptile">파충류</label>
                    </div>
                    <div class="radio-item">
                        <input type="radio" id="pet-other" name="petType" value="OTHER">
                        <label for="pet-other">기타</label>
                    </div>
                </div>
            </div>
🤖 Prompt for AI Agents
In src/main/resources/templates/post-qna-form.html between lines 28 and 60, the
radio buttons for petType do not have a default selected value, which can cause
issues if the user submits the form without making a selection. To fix this, add
the "checked" attribute to one of the input radio elements (e.g., the first
option "DOG") to set it as the default selected value.
src/main/resources/templates/post-review-list.html (3)

424-430: 💡 Verification agent

🧩 Analysis chain

댓글 기능 확인 필요 - 댓글 카운트 표시가 올바르게 동작하는지 확인하세요.

PR 설명에서 댓글 카운트가 올바르게 업데이트되는지 확인해 달라고 요청하셨습니다. 현재 코드는 댓글 수를 표시하고 있지만, 실시간 업데이트 기능은 보이지 않습니다.


🏁 Script executed:

#!/bin/bash
# 댓글 기능 관련 코드 확인
echo "댓글 카운트 업데이트 관련 코드 검색:"
rg -A 5 "commentCount" src/main/java/io/github/petty/community

echo "댓글 기능 관련 API 엔드포인트 검색:"
rg -A 5 "comment" src/main/java/io/github/petty/community/controller

Length of output: 10351


댓글 카운트 실시간 갱신 로직 검토 필요

백엔드(PostService, CommentService)에서 commentCount가 정상적으로 계산·저장되어 템플릿에 전달되는 것은 확인되었습니다.
하지만 프론트엔드(src/main/resources/templates/post-review-list.html)에서는 댓글 작성·삭제 후 해당 <span> 요소에 담긴 카운트를 다시 조회하거나 갱신하는 로직이 없어, 실시간 반영이 이루어지지 않습니다. 현재는 페이지 새로고침 시에만 최신 댓글 수가 표시됩니다.

점검 및 개선이 필요한 지점:

  • post-review-list.html 내 댓글 카운트 표시(const comments = …) 이후, 댓글 추가·삭제 API 호출 뒤 UI 업데이트 로직
  • 댓글 등록(CommentController.addComment) 및 삭제(deleteComment) 이후 commentCount를 다시 fetch하거나 WebSocket 등 이벤트 기반 갱신 방식 도입

제안:

  • 댓글 등록/삭제 성공 시 해당 포스트의 commentCount만 재조회하여 <span> 내용을 업데이트
  • 또는 WebSocket/Server-Sent Events를 적용해 서버에서 변경된 카운트를 푸시하도록 구현 검토
🤖 Prompt for AI Agents
In src/main/resources/templates/post-review-list.html around lines 424 to 430,
the comment count is displayed but not updated in real time after adding or
deleting comments. To fix this, add logic after the comment add/delete API calls
to fetch the updated commentCount for the affected post and update the innerHTML
of the corresponding <span> element dynamically. Alternatively, consider
implementing a WebSocket or Server-Sent Events mechanism to push comment count
updates from the server to the client for real-time synchronization.

416-422: 💡 Verification agent

🧩 Analysis chain

좋아요 기능 확인 필요 - 좋아요 카운트 표시가 올바르게 동작하는지 확인하세요.

PR 설명에서 좋아요 카운트가 올바르게 업데이트되는지 확인해 달라고 요청하셨습니다. 현재 코드는 좋아요 수를 올바르게 표시하고 있지만, 실시간 업데이트 기능은 보이지 않습니다.


🏁 Script executed:

#!/bin/bash
# 좋아요 기능 관련 코드 확인
echo "좋아요 카운트 업데이트 관련 코드 검색:"
rg -A 5 "likeCount" src/main/java/io/github/petty/community

echo "좋아요 기능 관련 API 엔드포인트 검색:"
rg -A 5 "like" src/main/java/io/github/petty/community/controller

Length of output: 6506


좋아요 기능 동적 업데이트 로직 구현 필요
현재 서버 측에서는 post.likeCount를 반환하고 /posts/{id}/like 엔드포인트가 정상적으로 구현되어 있습니다. 하지만 클라이언트에서 좋아요 클릭 시 이를 호출하고 응답으로 받은 likeCount로 DOM을 갱신하는 로직이 빠져 있습니다.

확인된 위치:

  • 서비스: PostServiceImpl (.likeCount(post.getLikeCount()) 매핑)
  • 컨트롤러: PostController.likePost(@PostMapping("/{id}/like") …)

해야 할 작업:

  • post-review-list.html 내 좋아요 버튼 요소에 클릭 이벤트 핸들러 추가
  • fetch('/posts/${post.id}/like', { method: 'POST' }) 요청 및 응답 처리
  • 응답의 likeCount로 SVG 옆 숫자 갱신
🤖 Prompt for AI Agents
In src/main/resources/templates/post-review-list.html around lines 416 to 422,
the like count is displayed statically without dynamic updates. Add a click
event handler to the like button element that sends a POST request to
/posts/${post.id}/like. Upon receiving the response, update the displayed like
count next to the SVG icon with the returned likeCount value to enable real-time
UI updates.

355-358: 🛠️ Refactor suggestion

데이터 로딩 시 상태 표시 및 에러 처리가 필요합니다.

API 호출 중 로딩 상태를 표시하거나 오류 발생 시 사용자에게 알림을 주는 기능이 없습니다.

async function fetchReviewPosts() {
+  const list = document.getElementById('reviewList');
+  list.innerHTML = '<div class="loading">데이터를 불러오는 중입니다...</div>';
+  
+  try {
    const res = await fetch(`/api/posts?type=REVIEW&page=${currentPage}&size=${pageSize}`);
+    if (!res.ok) {
+      throw new Error('서버 응답 오류: ' + res.status);
+    }
    const page = await res.json();
    const posts = page.content;

    const list = document.getElementById('reviewList');
    list.innerHTML = '';
+    
+    if (posts.length === 0) {
+      list.innerHTML = '<div class="no-posts">게시물이 없습니다.</div>';
+      return;
+    }

    posts.forEach(post => {
      // 기존 코드...
    });

    renderPagination(page.totalPages);
+  } catch (error) {
+    console.error('데이터 로딩 오류:', error);
+    list.innerHTML = '<div class="error">데이터를 불러오는 중 오류가 발생했습니다. 잠시 후 다시 시도해주세요.</div>';
+  }
}
📝 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.

async function fetchReviewPosts() {
  const list = document.getElementById('reviewList');
  list.innerHTML = '<div class="loading">데이터를 불러오는 중입니다...</div>';
  
  try {
    const res = await fetch(`/api/posts?type=REVIEW&page=${currentPage}&size=${pageSize}`);
    if (!res.ok) {
      throw new Error('서버 응답 오류: ' + res.status);
    }
    const page = await res.json();
    const posts = page.content;

    list.innerHTML = '';
    
    if (posts.length === 0) {
      list.innerHTML = '<div class="no-posts">게시물이 없습니다.</div>';
      return;
    }

    posts.forEach(post => {
      // 기존 코드...
    });

    renderPagination(page.totalPages);
  } catch (error) {
    console.error('데이터 로딩 오류:', error);
    list.innerHTML = '<div class="error">데이터를 불러오는 중 오류가 발생했습니다. 잠시 후 다시 시도해주세요.</div>';
  }
}
🤖 Prompt for AI Agents
In src/main/resources/templates/post-review-list.html around lines 355 to 358,
the fetchReviewPosts function lacks loading state indication and error handling.
Add a loading state variable to show when data is being fetched, set it to true
before the fetch call and false after completion. Wrap the fetch call in a
try-catch block to catch errors, and display an error message or alert to the
user if the fetch fails.
src/main/resources/static/js/common/edit-showoff.js (3)

19-36: 🛠️ Refactor suggestion

에러 처리가 필요합니다.

API 호출에 대한 상세한 에러 처리가 부족합니다. 특히 네트워크 오류나 서버 응답 코드에 따른 구체적인 오류 처리가 필요합니다.

const token = localStorage.getItem("jwt");
+if (!token) {
+  alert("로그인이 필요한 기능입니다.");
+  location.href = "/login"; // 로그인 페이지로 리디렉션
+  return;
+}

const payload = {
  title: document.getElementById("edit-showoff-title").value,
  content: document.getElementById("edit-showoff-content").value,
  petType: getRadioValue("edit-showoff-petType") || "OTHER",
  postType: postType,
  images: originalImages
};

-const res = await fetch(`/api/posts/${postId}`, {
-  method: "PUT",
-  headers: {
-    "Content-Type": "application/json",
-    "Authorization": `Bearer ${token}`
-  },
-  body: JSON.stringify(payload)
-});
-
-if (res.ok) {
-  alert("수정 완료!");
-  location.href = `/posts/detail?id=${postId}`;
-} else {
-  alert("수정 실패");
-}
+try {
+  const res = await fetch(`/api/posts/${postId}`, {
+    method: "PUT",
+    headers: {
+      "Content-Type": "application/json",
+      "Authorization": `Bearer ${token}`
+    },
+    body: JSON.stringify(payload)
+  });
+  
+  if (res.ok) {
+    alert("수정 완료!");
+    location.href = `/posts/detail?id=${postId}`;
+  } else {
+    const errorData = await res.json().catch(() => null);
+    if (errorData && errorData.message) {
+      alert(`수정 실패: ${errorData.message}`);
+    } else {
+      alert(`수정 실패: ${res.status} ${res.statusText}`);
+    }
+  }
+} catch (error) {
+  console.error("API 호출 오류:", error);
+  alert("네트워크 오류가 발생했습니다. 연결을 확인하고 다시 시도해주세요.");
+}
📝 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.

      const token = localStorage.getItem("jwt");
      if (!token) {
        alert("로그인이 필요한 기능입니다.");
        location.href = "/login"; // 로그인 페이지로 리디렉션
        return;
      }

      const payload = {
        title: document.getElementById("edit-showoff-title").value,
        content: document.getElementById("edit-showoff-content").value,
        petType: getRadioValue("edit-showoff-petType") || "OTHER",
        postType: postType,
        images: originalImages
      };

      try {
        const res = await fetch(`/api/posts/${postId}`, {
          method: "PUT",
          headers: {
            "Content-Type": "application/json",
            "Authorization": `Bearer ${token}`
          },
          body: JSON.stringify(payload)
        });
        
        if (res.ok) {
          alert("수정 완료!");
          location.href = `/posts/detail?id=${postId}`;
        } else {
          const errorData = await res.json().catch(() => null);
          if (errorData && errorData.message) {
            alert(`수정 실패: ${errorData.message}`);
          } else {
            alert(`수정 실패: ${res.status} ${res.statusText}`);
          }
        }
      } catch (error) {
        console.error("API 호출 오류:", error);
        alert("네트워크 오류가 발생했습니다. 연결을 확인하고 다시 시도해주세요.");
      }
🤖 Prompt for AI Agents
In src/main/resources/static/js/common/edit-showoff.js around lines 19 to 36,
the fetch API call lacks detailed error handling for network failures and server
response status codes. Add try-catch around the fetch call to handle network
errors, and after receiving the response, check if the response status indicates
success; if not, handle or display the error accordingly. This ensures robust
error handling for both network issues and unsuccessful HTTP responses.

146-154: 🛠️ Refactor suggestion

removeImage 함수에 대한 보안 및 스코프 문제 해결 필요

removeImage 함수가 전역 스코프에 정의되어 있어 충돌 가능성이 있으며, onclick 속성을 통해 직접 호출하는 방식은 XSS 취약점을 유발할 수 있습니다.

전역 함수 대신 이벤트 위임 패턴을 사용하세요:

-function removeImage(url) {
+// 전역 함수 대신 모듈 스코프로 변경
+const removeImage = (url) => {
  const img = originalImages.find(img => img.imageUrl === url);
  if (img) {
    img.isDeleted = true;
  }

  const wrapper = document.querySelector(`img[data-url='${url}']`)?.parentElement;
  if (wrapper) wrapper.remove();
}

+// 이미지 미리보기 영역에 이벤트 위임 설정
+document.addEventListener("DOMContentLoaded", () => {
+  const previewBox = document.getElementById("edit-showoff-imagePreview");
+  if (previewBox) {
+    previewBox.addEventListener("click", (e) => {
+      if (e.target.tagName === "BUTTON" && e.target.classList.contains("delete-btn")) {
+        const url = e.target.dataset.url;
+        removeImage(url);
+      }
+    });
+  }
+});

// fetchPostForEdit 및 handleImageUpload 함수에서 이미지 추가 코드 수정
// 아래와 같이 onclick 속성 대신 클래스와 데이터 속성 사용
// <button type="button" class="delete-btn" data-url="${img.imageUrl}">삭제</button>

이 패턴을 적용하려면 이미지 생성 코드도 수정해야 합니다:

// fetchPostForEdit 및 handleImageUpload 함수 내 이미지 생성 코드 수정
imgWrapper.innerHTML = `
  <img src="${img.imageUrl}" data-url="${img.imageUrl}" style="max-width: 100px; border-radius: 6px; object-fit: cover;">
-  <button type="button" onclick="removeImage('${img.imageUrl}')" style="display: block; margin-top: 5px;">삭제</button>
+  <button type="button" class="delete-btn" data-url="${img.imageUrl}" style="display: block; margin-top: 5px;">삭제</button>
`;
📝 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.

// 전역 함수 대신 모듈 스코프로 변경
const removeImage = (url) => {
  const img = originalImages.find(img => img.imageUrl === url);
  if (img) {
    img.isDeleted = true;
  }

  const wrapper = document.querySelector(`img[data-url='${url}']`)?.parentElement;
  if (wrapper) wrapper.remove();
}

// 이미지 미리보기 영역에 이벤트 위임 설정
document.addEventListener("DOMContentLoaded", () => {
  const previewBox = document.getElementById("edit-showoff-imagePreview");
  if (previewBox) {
    previewBox.addEventListener("click", (e) => {
      if (e.target.tagName === "BUTTON" && e.target.classList.contains("delete-btn")) {
        const url = e.target.dataset.url;
        removeImage(url);
      }
    });
  }
});
🤖 Prompt for AI Agents
In src/main/resources/static/js/common/edit-showoff.js around lines 146 to 154,
the removeImage function is defined in the global scope and called via inline
onclick attributes, which risks naming conflicts and XSS vulnerabilities.
Refactor by removing the global removeImage function and instead implement event
delegation on a container element that listens for click events on delete
buttons or images. Update the image rendering code to avoid inline onclick
handlers and use data attributes or classes to identify clickable elements for
deletion. This approach confines event handling within a scoped listener and
mitigates security risks.

48-82: 🛠️ Refactor suggestion

데이터 로딩 시 상태 표시 및 에러 처리가 필요합니다.

포스트 데이터를 가져오는 과정에서 로딩 상태 표시나 에러 처리가 부족합니다.

async function fetchPostForEdit() {
+  try {
+    // 로딩 상태 표시
+    const form = document.getElementById("editShowoffForm");
+    if (form) {
+      form.classList.add("loading");
+      const loadingEl = document.createElement("div");
+      loadingEl.id = "loadingIndicator";
+      loadingEl.className = "loading-indicator";
+      loadingEl.innerHTML = "<span>데이터를 불러오는 중...</span>";
+      form.parentNode.insertBefore(loadingEl, form);
+    }
+    
    const res = await fetch(`/api/posts/${postId}`);
+    
+    if (!res.ok) {
+      throw new Error(`서버 오류: ${res.status} ${res.statusText}`);
+    }
+    
    const post = await res.json();

    document.getElementById("edit-showoff-title").value = post.title;
    document.getElementById("edit-showoff-content").value = post.content;

    const petTypeInputs = document.querySelectorAll('input[name="edit-showoff-petType"]');
    petTypeInputs.forEach(input => {
      if (input.value === post.petType) {
        input.checked = true;
      }
    });

    const previewBox = document.getElementById("edit-showoff-imagePreview");
    if (previewBox) {
      (post.images || []).forEach((img, index) => {
        const imgWrapper = document.createElement("div");
        imgWrapper.style.display = "inline-block";
        imgWrapper.style.margin = "5px";
        imgWrapper.innerHTML = `
          <img src="${img.imageUrl}" data-url="${img.imageUrl}" style="max-width: 100px; border-radius: 6px; object-fit: cover;">
          <button type="button" onclick="removeImage('${img.imageUrl}')" style="display: block; margin-top: 5px;">삭제</button>
        `;
        previewBox.appendChild(imgWrapper);

        originalImages.push({
          id: img.id,
          imageUrl: img.imageUrl,
          ordering: img.ordering,
          isDeleted: false
        });
      });
    }
+  } catch (error) {
+    console.error("포스트 데이터 로딩 오류:", error);
+    alert("포스트 데이터를 불러오는 데 실패했습니다: " + error.message);
+  } finally {
+    // 로딩 상태 제거
+    const form = document.getElementById("editShowoffForm");
+    const loadingEl = document.getElementById("loadingIndicator");
+    if (form) form.classList.remove("loading");
+    if (loadingEl) loadingEl.remove();
+  }
}
📝 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.

async function fetchPostForEdit() {
  try {
    // 로딩 상태 표시
    const form = document.getElementById("editShowoffForm");
    if (form) {
      form.classList.add("loading");
      const loadingEl = document.createElement("div");
      loadingEl.id = "loadingIndicator";
      loadingEl.className = "loading-indicator";
      loadingEl.innerHTML = "<span>데이터를 불러오는 중...</span>";
      form.parentNode.insertBefore(loadingEl, form);
    }

    const res = await fetch(`/api/posts/${postId}`);
    if (!res.ok) {
      throw new Error(`서버 오류: ${res.status} ${res.statusText}`);
    }

    const post = await res.json();

    document.getElementById("edit-showoff-title").value = post.title;
    document.getElementById("edit-showoff-content").value = post.content;

    const petTypeInputs = document.querySelectorAll('input[name="edit-showoff-petType"]');
    petTypeInputs.forEach(input => {
      if (input.value === post.petType) {
        input.checked = true;
      }
    });

    const previewBox = document.getElementById("edit-showoff-imagePreview");
    if (previewBox) {
      (post.images || []).forEach((img) => {
        const imgWrapper = document.createElement("div");
        imgWrapper.style.display = "inline-block";
        imgWrapper.style.margin = "5px";
        imgWrapper.innerHTML = `
          <img src="${img.imageUrl}" data-url="${img.imageUrl}" style="max-width: 100px; border-radius: 6px; object-fit: cover;">
          <button type="button" onclick="removeImage('${img.imageUrl}')" style="display: block; margin-top: 5px;">삭제</button>
        `;
        previewBox.appendChild(imgWrapper);

        originalImages.push({
          id: img.id,
          imageUrl: img.imageUrl,
          ordering: img.ordering,
          isDeleted: false
        });
      });
    }
  } catch (error) {
    console.error("포스트 데이터 로딩 오류:", error);
    alert("포스트 데이터를 불러오는 데 실패했습니다: " + error.message);
  } finally {
    // 로딩 상태 제거
    const form = document.getElementById("editShowoffForm");
    const loadingEl = document.getElementById("loadingIndicator");
    if (form) form.classList.remove("loading");
    if (loadingEl) loadingEl.remove();
  }
}
🤖 Prompt for AI Agents
In src/main/resources/static/js/common/edit-showoff.js around lines 48 to 82,
the fetchPostForEdit function lacks loading state indication and error handling.
Add a loading indicator before starting the fetch request and remove it after
completion. Wrap the fetch call in a try-catch block to catch any errors,
display an appropriate error message to the user, and ensure the loading
indicator is removed in both success and error cases.
src/main/resources/static/js/common/edit-review.js (2)

1-159: 🛠️ Refactor suggestion

코드 중복이 많습니다 - 공통 기능을 추출해야 합니다.

edit-showoff.js와 edit-review.js 사이에 코드 중복이 심각합니다. 두 파일은 거의 동일한 기능을 수행하며, 타입과 필드 이름만 다릅니다. 코드 중복은 유지보수 문제와 버그 발생 가능성을 높입니다.

두 파일의 공통 기능을 추출하여 단일 유틸리티 모듈로 리팩토링하는 것을 강력히 권장합니다:

// edit-utils.js 파일 생성
// 공통 함수 모듈화
export function initPostEditor(options) {
  const {
    postType,
    formId,
    titleId,
    contentId,
    petTypeRadioName,
    imageInputId,
    imagePreviewId,
    additionalFields = [] // 추가 필드 정보 배열 [{id, property}]
  } = options;
  
  let originalImages = [];
  const postId = new URLSearchParams(location.search).get("id");
  
  // 이미지 삭제 함수
  const removeImage = (url) => {
    const img = originalImages.find(img => img.imageUrl === url);
    if (img) {
      img.isDeleted = true;
    }
    
    const wrapper = document.querySelector(`img[data-url='${url}']`)?.parentElement;
    if (wrapper) wrapper.remove();
  };
  
  // 라디오 값 가져오기
  const getRadioValue = (name) => {
    const radios = document.querySelectorAll(`input[name="${name}"]`);
    for (const radio of radios) {
      if (radio.checked) return radio.value;
    }
    return null;
  };
  
  // 이미지 업로드 처리
  const handleImageUpload = async (e) => {
    // 기존 handleImageUpload 코드...
  };
  
  // 포스트 데이터 가져오기
  const fetchPostForEdit = async () => {
    // 기존 fetchPostForEdit 코드...
  };
  
  // 이벤트 리스너 설정
  document.addEventListener("DOMContentLoaded", () => {
    fetchPostForEdit();
    
    const imageInput = document.getElementById(imageInputId);
    if (imageInput) {
      imageInput.addEventListener("change", handleImageUpload);
    }
    
    const form = document.getElementById(formId);
    if (form) {
      form.addEventListener("submit", async (e) => {
        e.preventDefault();
        const token = localStorage.getItem("jwt");
        
        const payload = {
          title: document.getElementById(titleId).value,
          content: document.getElementById(contentId).value,
          petType: getRadioValue(petTypeRadioName) || "OTHER",
          postType: postType,
          images: originalImages
        };
        
        // 추가 필드 처리
        additionalFields.forEach(field => {
          payload[field.property] = document.getElementById(field.id).value;
        });
        
        // 폼 제출 코드...
      });
    }
    
    // 이미지 미리보기 영역에 이벤트 위임 설정
    const previewBox = document.getElementById(imagePreviewId);
    if (previewBox) {
      previewBox.addEventListener("click", (e) => {
        if (e.target.tagName === "BUTTON" && e.target.classList.contains("delete-btn")) {
          const url = e.target.dataset.url;
          removeImage(url);
        }
      });
    }
  });
}

이제 각 파일에서는 다음과 같이 사용할 수 있습니다:

// edit-review.js
import { initPostEditor } from './edit-utils.js';

initPostEditor({
  postType: "REVIEW",
  formId: "editReviewForm",
  titleId: "edit-review-title",
  contentId: "edit-review-content",
  petTypeRadioName: "edit-review-petType",
  imageInputId: "edit-review-imageFiles",
  imagePreviewId: "edit-review-imagePreview",
  additionalFields: [
    { id: "edit-petName", property: "petName" },
    { id: "edit-region", property: "region" }
  ]
});
// edit-showoff.js
import { initPostEditor } from './edit-utils.js';

initPostEditor({
  postType: "SHOWOFF",
  formId: "editShowoffForm",
  titleId: "edit-showoff-title",
  contentId: "edit-showoff-content",
  petTypeRadioName: "edit-showoff-petType",
  imageInputId: "edit-showoff-imageFiles",
  imagePreviewId: "edit-showoff-imagePreview"
});

이러한 리팩토링을 통해 코드 중복을 제거하고, 향후 수정이 필요할 때 한 곳에서만 변경하면 되므로 유지보수성이 크게 향상됩니다.

🤖 Prompt for AI Agents
In src/main/resources/static/js/common/edit-review.js lines 1 to 159, there is
significant code duplication with edit-showoff.js as both files implement nearly
identical functionality with only differences in types and field names. To fix
this, extract the common logic into a single utility module (e.g.,
edit-utils.js) that exports a function like initPostEditor accepting
configuration options for postType, form IDs, input IDs, and additional fields.
Then refactor edit-review.js and edit-showoff.js to import and call this utility
with their specific parameters. This will centralize shared code, reduce
duplication, and improve maintainability.

97-127: 💡 Verification agent

🧩 Analysis chain

PR 요청에 대한 검증: 좋아요 및 댓글 카운트 업데이트 확인 필요

PR 설명에서 "좋아요 또는 댓글이 추가되거나 삭제될 때 카운트 업데이트가 제대로 작동하는지 확인"을 요청했습니다. 현재 이 파일과 관련 파일들에서 좋아요/댓글 숫자가 변경될 때 실시간으로 UI가 업데이트되는 코드가 없습니다.


🏁 Script executed:

#!/bin/bash
# 실시간 업데이트 관련 코드 확인

echo "==== WebSocket 사용 여부 확인 ===="
grep -r "WebSocket" src/main/java/io/github/petty

echo "==== 좋아요 카운트 업데이트 관련 로직 ===="
grep -r "likeCount" src/main/java/io/github/petty/community/service

echo "==== 댓글 카운트 업데이트 관련 로직 ===="
grep -r "commentCount" src/main/java/io/github/petty/community/service

echo "==== 좋아요 버튼 이벤트 핸들러 코드 확인 ===="
grep -r "like-button" --include="*.js" src/main/resources/static/js/

echo "==== 댓글 이벤트 핸들러 코드 확인 ===="
grep -r "comment-form" --include="*.js" src/main/resources/static/js/

Length of output: 1907


실시간 좋아요/댓글 카운트 업데이트 로직 추가 필요

스크립트 검증 결과:

  • 서버(Service) 레이어에서는 likeCount/commentCount를 조회해 반환하는 로직이 존재하나
  • WebSocket/SSE 기반 실시간 푸시(예: WebSocketConfig, 메시지 브로커 설정 등)가 전혀 구현되어 있지 않으며
  • 프론트엔드(static/js)에도 웹소켓 메시지를 수신하거나, 좋아요·댓글 수를 동적으로 갱신하는 이벤트 핸들러가 없음

이에 따라 사용자가 좋아요나 댓글을 추가/삭제해도 페이지 리로드 없이 UI에 반영되지 않습니다. 아래 내용을 검토해 주세요:

  • WebSocket(Stomp) 또는 SSE 기반 메시징 설정 및 서버↔클라이언트 연결 구현
  • 프론트엔드에서 수신된 메시지로 DOM 내 카운트를 업데이트하는 스크립트 추가
  • 필요 시 API 호출 직후 카운트 재조회 또는 페이지 자동 갱신 로직 보완
🤖 Prompt for AI Agents
In src/main/resources/static/js/common/edit-review.js around lines 97 to 127,
there is no implementation for real-time updating of like and comment counts
when they change. To fix this, implement a WebSocket or SSE client connection to
receive server push messages about like/comment count changes, and add event
handlers that update the DOM counts dynamically upon receiving these messages.
Additionally, ensure the server is configured to send these updates, and
consider refreshing counts after API calls if needed.
src/main/resources/templates/post-showoff-list.html (2)

237-237: ⚠️ Potential issue

오타 수정이 필요합니다.

CSS 속성 값 뒤에 의도하지 않은 '0' 문자가 있습니다.

다음과 같이 수정하세요:

-            font-size: 12px;0
+            font-size: 12px;
📝 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.

            font-size: 12px;
🤖 Prompt for AI Agents
In src/main/resources/templates/post-showoff-list.html at line 237, there is a
typo in the CSS where an unintended '0' character appears after the font-size
value. Remove the extraneous '0' so that the line reads correctly as "font-size:
12px;" without any trailing characters.

356-367: 🛠️ Refactor suggestion

API 호출 오류 처리가 필요합니다.

현재 fetch API 호출 시 오류 처리가 없습니다. API 호출 실패 시 사용자에게 알림이 없어 사용성이 저하될 수 있습니다.

try-catch 블록을 추가하여 오류를 처리하고, 사용자에게 적절한 메시지를 표시하세요:

 async function fetchShowoffPosts() {
+    try {
         const res = await fetch(`/api/posts?type=SHOWOFF&page=${currentPage}&size=${pageSize}`);
+        if (!res.ok) {
+            throw new Error(`HTTP error! status: ${res.status}`);
+        }
         const page = await res.json();
         const posts = page.content;

         const list = document.getElementById('showoffList');
         list.innerHTML = '';
+    } catch (error) {
+        console.error("게시글을 불러오는 중 오류가 발생했습니다:", error);
+        const list = document.getElementById('showoffList');
+        list.innerHTML = '<div class="error-message">게시글을 불러오는 중 오류가 발생했습니다. 잠시 후 다시 시도해주세요.</div>';
+    }
📝 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.

    async function fetchShowoffPosts() {
+        try {
            const res = await fetch(`/api/posts?type=SHOWOFF&page=${currentPage}&size=${pageSize}`);
+            if (!res.ok) {
+                throw new Error(`HTTP error! status: ${res.status}`);
+            }
            const page = await res.json();
            const posts = page.content;

            const list = document.getElementById('showoffList');
            list.innerHTML = '';
+        } catch (error) {
+            console.error("게시글을 불러오는 중 오류가 발생했습니다:", error);
+            const list = document.getElementById('showoffList');
+            list.innerHTML = '<div class="error-message">게시글을 불러오는 중 오류가 발생했습니다. 잠시 후 다시 시도해주세요.</div>';
+        }

        posts.forEach(post => {
            const card = document.createElement('div');
            card.className = 'showoff-card';
🤖 Prompt for AI Agents
In src/main/resources/templates/post-showoff-list.html around lines 356 to 367,
the fetchShowoffPosts function lacks error handling for the fetch API call. Wrap
the fetch and subsequent JSON parsing inside a try-catch block to catch any
errors during the API call. In the catch block, display an appropriate user
alert or message indicating the failure to load posts, ensuring better user
experience during API failures.
src/main/java/io/github/petty/community/util/SupabaseUploader.java (2)

78-80: ⚠️ Potential issue

파일 확장자 추출 메서드를 보완해 주세요.

현재 getExtension 메서드는 파일명이 null이거나 확장자가 없는 경우를 처리하지 않습니다. 이로 인해 NullPointerException이나 StringIndexOutOfBoundsException이 발생할 수 있습니다.

다음과 같이 수정하세요:

-private String getExtension(String originalFilename) {
-    return originalFilename.substring(originalFilename.lastIndexOf('.') + 1);
-}
+private String getExtension(String originalFilename) {
+    if (originalFilename == null || !originalFilename.contains(".")) {
+        return "jpg"; // 기본 확장자 설정
+    }
+    return originalFilename.substring(originalFilename.lastIndexOf('.') + 1);
+}
🤖 Prompt for AI Agents
In src/main/java/io/github/petty/community/util/SupabaseUploader.java around
lines 78 to 80, the getExtension method does not handle cases where
originalFilename is null or does not contain a dot, which can cause
NullPointerException or StringIndexOutOfBoundsException. Modify the method to
first check if originalFilename is null or if it lacks a dot character; if so,
return an empty string or a default value. Otherwise, safely extract and return
the substring after the last dot.

33-76: 🛠️ Refactor suggestion

파일 유효성 검사를 추가해 주세요.

현재 업로드 메서드는 파일의 크기나 유형에 대한 검증을 하지 않습니다. 이는 보안 문제를 일으킬 수 있습니다.

파일 크기, 유형 및 null 체크를 추가하세요:

 public String upload(MultipartFile file) throws IOException {
+    if (file == null || file.isEmpty()) {
+        throw new IllegalArgumentException("파일이 비어있거나 null입니다.");
+    }
+    
+    // 파일 크기 제한 (예: 10MB)
+    if (file.getSize() > 10 * 1024 * 1024) {
+        throw new IllegalArgumentException("파일 크기가 10MB를 초과합니다.");
+    }
+    
+    // 파일 타입 검증
+    String contentType = file.getContentType();
+    if (contentType == null || !contentType.startsWith("image/")) {
+        throw new IllegalArgumentException("이미지 파일만 업로드 가능합니다.");
+    }
     
     log.info("Uploading file: {}", file.getOriginalFilename());
     // 나머지 코드...
📝 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.

public String upload(MultipartFile file) throws IOException {
    // 파일 유효성 검사
    if (file == null || file.isEmpty()) {
        throw new IllegalArgumentException("파일이 비어있거나 null입니다.");
    }

    // 파일 크기 제한 (예: 10MB)
    if (file.getSize() > 10 * 1024 * 1024) {
        throw new IllegalArgumentException("파일 크기가 10MB를 초과합니다.");
    }

    // 파일 타입 검증
    String contentType = file.getContentType();
    if (contentType == null || !contentType.startsWith("image/")) {
        throw new IllegalArgumentException("이미지 파일만 업로드 가능합니다.");
    }

    log.info("Uploading file: {}", file.getOriginalFilename());
    log.info("Supabase URL: {}", supabaseUrl);
    log.info("Bucket name: {}", bucketName);

    // 1. 고유한 파일 이름 생성
    String rawFilename = UUID.randomUUID() + "." + getExtension(file.getOriginalFilename());
    String encodedFilename = URLEncoder.encode(rawFilename, StandardCharsets.UTF_8);

    // 실제 업로드 시엔 인코딩된 이름 사용
    String uploadUrl = supabaseUrl + "/storage/v1/object/" + bucketName + "/" + encodedFilename;

    // 업로드 성공 후 접근용 URL - 인코딩된 파일명 그대로 사용해야 함
    String publicUrl = supabaseUrl + "/storage/v1/object/public/" + bucketName + "/" + encodedFilename;

    // 2. 요청 헤더
    HttpHeaders headers = new HttpHeaders();
    headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
    headers.set("Authorization", "Bearer " + supabaseKey);

    // 3. 요청 본문
    HttpEntity<byte[]> requestEntity = new HttpEntity<>(file.getBytes(), headers);

    try {
        // 4. 업로드 요청
        ResponseEntity<String> response = restTemplate.exchange(
                uploadUrl,
                HttpMethod.PUT,
                requestEntity,
                String.class
        );

        if (response.getStatusCode() == HttpStatus.OK || response.getStatusCode() == HttpStatus.CREATED) {
            log.info("Upload successful: {}", publicUrl);
            return publicUrl;
        } else {
            log.error("Upload failed with status: {}, response: {}", response.getStatusCode(), response.getBody());
            throw new RuntimeException("이미지 업로드 실패: " + response.getStatusCode());
        }
    } catch (Exception e) {
        log.error("Error uploading to Supabase: ", e);
        throw new IOException("Supabase 업로드 중 오류 발생: " + e.getMessage());
    }
}
🤖 Prompt for AI Agents
In src/main/java/io/github/petty/community/util/SupabaseUploader.java around
lines 33 to 76, the upload method lacks validation for the input file. Add
checks at the start of the method to ensure the file is not null, its size is
within acceptable limits, and its content type or extension matches allowed
image types. If any validation fails, throw an appropriate exception or return
an error to prevent insecure or invalid file uploads.
src/main/java/io/github/petty/community/service/PostService.java (1)

12-12: 🛠️ Refactor suggestion

와일드카드 제네릭 반환 타입 사용에 주의하세요.

Page<?>는 명확하지 않은 반환 타입을 의미합니다. 이는 타입 안전성을 저하시키고 클라이언트에서 추가적인 타입 캐스팅이 필요할 수 있습니다.

게시물 타입별로 구체적인 반환 타입을 명시하는 것이 좋습니다:

-Page<?> findAllByType(String type, Pageable pageable);
+Page<PostListResponse> findAllByType(String type, Pageable pageable);

만약 타입별로 다른 응답 DTO를 사용해야 한다면, 타입별 메서드를 분리하는 것을 고려하세요:

Page<PostReviewListResponse> findAllReviews(Pageable pageable);
Page<PostQnaListResponse> findAllQnas(Pageable pageable);
Page<PostShowoffListResponse> findAllShowoffs(Pageable pageable);
🤖 Prompt for AI Agents
In src/main/java/io/github/petty/community/service/PostService.java at line 12,
the method returns a Page<?> which is a wildcard generic type causing unclear
return types and potential type safety issues. To fix this, replace the wildcard
with specific return types for each post type, such as
Page<PostReviewListResponse>, Page<PostQnaListResponse>, and
Page<PostShowoffListResponse>. If different DTOs are needed per type, create
separate methods for each post type with explicit return types instead of a
single method with a wildcard return.
src/main/resources/static/js/common/edit-qna.js (3)

113-165: 🛠️ Refactor suggestion

이미지 업로드 유효성 검사 개선 필요

handleImageUpload 함수에서 파일 유형과 크기에 대한 유효성 검사가 없습니다. 이미지 파일만 허용하고 크기 제한을 추가하여 보안 및 성능을 향상시키세요.

async function handleImageUpload(e) {
  const files = Array.from(e.target.files);
  if (!files.length) return;

+  // 파일 타입 및 크기 검증
+  const maxFileSize = 5 * 1024 * 1024; // 5MB
+  const invalidFiles = files.filter(file => 
+    !file.type.startsWith('image/') || file.size > maxFileSize
+  );
+  
+  if (invalidFiles.length > 0) {
+    alert('이미지 파일만 업로드 가능하며, 각 파일은 5MB 이하여야 합니다.');
+    return;
+  }

  const currentCount = originalImages.filter(img => !img.isDeleted).length;
  const maxCount = 5;
  if (currentCount >= maxCount) {
    alert("최대 5개의 이미지를 업로드할 수 있습니다.");
    return;
  }
  
  // 이하 코드 생략...
📝 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.

async function handleImageUpload(e) {
  const files = Array.from(e.target.files);
  if (!files.length) return;

  // 파일 타입 및 크기 검증
  const maxFileSize = 5 * 1024 * 1024; // 5MB
  const invalidFiles = files.filter(file =>
    !file.type.startsWith('image/') || file.size > maxFileSize
  );
  
  if (invalidFiles.length > 0) {
    alert('이미지 파일만 업로드 가능하며, 각 파일은 5MB 이하여야 합니다.');
    return;
  }

  const currentCount = originalImages.filter(img => !img.isDeleted).length;
  const maxCount = 5;
  if (currentCount >= maxCount) {
    alert("최대 5개의 이미지를 업로드할 수 있습니다.");
    return;
  }

  const availableSlots = maxCount - currentCount;
  const filesToUpload = files.slice(0, availableSlots);

  const formData = new FormData();
  for (const file of filesToUpload) {
    formData.append("files", file);
  }

  const token = localStorage.getItem("jwt");
  const res = await fetch('/api/images/upload/multi', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${token}`
    },
    body: formData
  });

  if (!res.ok) {
    alert("이미지 업로드 실패");
    return;
  }

  const json = await res.json();
  const previewBox = document.getElementById("edit-qna-imagePreview");

  if (previewBox && json.images) {
    json.images.forEach((img) => {
      if (originalImages.some(existing => existing.imageUrl === img.imageUrl)) return;

      originalImages.push(img);

      const imgWrapper = document.createElement("div");
      imgWrapper.style.display = "inline-block";
      imgWrapper.style.margin = "5px";
      imgWrapper.innerHTML = `
        <img src="${img.imageUrl}" data-url="${img.imageUrl}" style="max-width: 100px; border-radius: 6px; object-fit: cover;">
        <button type="button" onclick="removeImage('${img.imageUrl}')" style="display: block; margin-top: 5px;">삭제</button>
      `;
      previewBox.appendChild(imgWrapper);
    });
  }
}
🤖 Prompt for AI Agents
In src/main/resources/static/js/common/edit-qna.js between lines 113 and 165,
the handleImageUpload function lacks validation for file type and size. To fix
this, add checks before appending files to formData to ensure each file is an
image (e.g., check file.type starts with "image/") and its size is within an
acceptable limit (e.g., less than 5MB). If a file fails validation, skip it or
alert the user accordingly to improve security and performance.

59-62: 🛠️ Refactor suggestion

API 응답 오류 처리를 추가하세요.

fetchPostForEdit 함수에는 API 호출 실패에 대한 오류 처리가 없습니다. 오류가 발생하면 사용자에게 표시하고 적절한 조치를 취해야 합니다.

async function fetchPostForEdit() {
-  const res = await fetch(`/api/posts/${postId}`);
-  const post = await res.json();
+  try {
+    const res = await fetch(`/api/posts/${postId}`);
+    if (!res.ok) {
+      throw new Error(`게시글을 불러오는데 실패했습니다. (상태: ${res.status})`);
+    }
+    const post = await res.json();
+    
+    // 이하 폼 채우기 코드...
+  } catch (error) {
+    alert(`오류가 발생했습니다: ${error.message}`);
+    console.error('게시글 조회 오류:', error);
+    location.href = '/posts/qna'; // 목록 페이지로 리다이렉트
+  }
📝 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.

async function fetchPostForEdit() {
  try {
    const res = await fetch(`/api/posts/${postId}`);
    if (!res.ok) {
      throw new Error(`게시글을 불러오는데 실패했습니다. (상태: ${res.status})`);
    }
    const post = await res.json();

    // 이하 폼 채우기 코드...
  } catch (error) {
    alert(`오류가 발생했습니다: ${error.message}`);
    console.error('게시글 조회 오류:', error);
    location.href = '/posts/qna'; // 목록 페이지로 리다이렉트
  }
}
🤖 Prompt for AI Agents
In src/main/resources/static/js/common/edit-qna.js around lines 59 to 62, the
fetchPostForEdit function lacks error handling for the API call. Add try-catch
around the fetch and JSON parsing to catch any errors, and handle them by
displaying an error message to the user and implementing any necessary fallback
or recovery logic.

80-96: 💡 Verification agent

🧩 Analysis chain

이미지 순서를 보존하는 방식 개선 필요

기존 이미지를 로드할 때 순서(ordering) 정보를 올바르게 유지하고 있습니다. 그러나 새 이미지를 추가할 때는 ordering 값을 지정하지 않아 순서가 섞일 수 있습니다.

이 코드가 handleImageUpload 함수와 함께 이미지 순서를 올바르게 유지하는지 확인하세요. 새 이미지를 추가할 때 originalImages 배열의 길이 또는 마지막 항목의 ordering 값을 기준으로 순서를 부여하는 로직을 추가하는 것이 좋습니다.


🌐 Web query:

이미지 업로드 시 순서 지정 없이 추가하면 어떤 순서로 저장되나요?

💡 Result:

이미지를 업로드할 때 별도의 순서 지정을 하지 않고 여러 장을 한 번에 추가하면, 저장되는 순서는 사용 환경과 애플리케이션에 따라 다를 수 있습니다.

  • 일반적으로 웹 또는 앱에서 여러 이미지를 한 번에 업로드할 때
    이미지는 사용자가 첨부한 순서(예: 파일 선택 창에서 클릭한 순서나 드래그한 순서)대로 내부 파일 리스트 배열(Array)에 저장됩니다. 즉, 사용자가 업로드한 순서대로 서버나 클라이언트에서 처리하면 해당 순서대로 저장 및 표시가 가능합니다[1][7].

  • 파일 시스템(예: 윈도우 탐색기)에서 여러 이미지를 선택해 복사할 때
    디폴트로는 파일 이름순, 생성일자순 등 정렬 기준에 따라 순서가 결정됩니다. 파일 이름이 IMG_001, IMG_002처럼 규칙적으로 되어 있다면 이름순으로 저장될 수 있습니다. 하지만 단순히 여러 파일을 동시에 선택해 업로드하면, 파일 시스템이나 브라우저, 서버의 구현 방식에 따라 순서가 섞이거나 올바르지 않게 저장될 수도 있습니다[5][9].

  • 스마트폰이나 일부 애플리케이션
    예를 들어 아이폰의 경우, 사진 앱에 저장되는 순서는 파일의 생성 시각(메타데이터의 촬영 시간 기준)이나 앱의 처리 방식에 따라 달라집니다. 사용자가 원하는 순서로 확실하게 정렬하려면, 사진의 파일 정보를 수정하거나, 업로드 후 앱 내에서 순서를 변경해줘야 합니다[5][6].

  • SNS나 블로그 업로드(인스타그램, 네이버 블로그 등)
    업로드 순서가 곧 게시물 내 이미지의 표시 순서가 되는 것이 일반적이지만, 업로드 후 별도의 순서 편집(드래그앤드롭, 우측 이동 등)으로 변경이 가능한 경우도 많습니다[6][8].

정리

  • 이미지 업로드 시 특별히 순서를 지정하지 않으면, 기본적으로는 “선택 혹은 첨부한 순서”나 “파일의 생성/수정 시각”, “파일 이름” 등에 따라 저장됩니다[1][5][7].
  • 일부 환경에서는 동시 업로드 시 순서가 보장되지 않을 수 있으므로, 순서가 매우 중요하다면 업로드 후 순서 조정 기능을 활용해야 합니다[6][8].

즉, 업로드 순서가 중요한 경우 앱이나 웹 서비스에서 이를 명확히 지원하는지 확인하고, 필요하다면 업로드 후 순서 정렬 기능을 이용하는 것이 안전합니다.

Citations:


🏁 Script executed:

#!/bin/bash
# 위치와 구현체 확인을 위해 handleImageUpload 함수 검색
rg -n "handleImageUpload" -n src/main/resources/static/js/common/edit-qna.js

# handleImageUpload 내부 로직 일부를 출력하여 ordering 할당 여부 확인
rg -A 30 "handleImageUpload" -n src/main/resources/static/js/common/edit-qna.js

Length of output: 2224


🏁 Script executed:

#!/bin/bash
sed -n '113,200p' src/main/resources/static/js/common/edit-qna.js

Length of output: 1898


새 이미지에 순서(ordering) 할당 로직 추가 필요

현재 handleImageUpload 함수에서는 서버 응답 json.images를 그대로 originalImages에 추가하여, 클라이언트 측에서 새 이미지의 ordering 값을 부여하지 않고 있습니다. 서버가 ordering을 반환하지 않거나 순서 보장이 필요할 때는 다음과 같이 수정해야 합니다.

수정 대상

  • 파일: src/main/resources/static/js/common/edit-qna.js
  • 위치: handleImageUpload 내부의 json.images.forEach 루프

제안하는 코드 변경 예시:

 json.images.forEach((img) => {
   if (originalImages.some(existing => existing.imageUrl === img.imageUrl)) return;
-  originalImages.push(img);
+  // 마지막 순서를 계산하여 새 이미지에 순서를 부여
+  const lastOrdering = originalImages.reduce(
+    (max, item) => Math.max(max, item.ordering || 0),
+    0
+  );
+  originalImages.push({
+    ...img,
+    ordering: lastOrdering + 1,
+    isDeleted: false
+  });
  • 서버가 이미 ordering을 반환하는지 반드시 확인하세요.
  • 반환하지 않는다면, 위와 같이 클라이언트에서 originalImages.length나 마지막 ordering 값 기반으로 순서를 할당해 주어야 합니다.
  • 이 변경 후에는 페이로드(PUT /api/posts/${postId})에 담기는 이미지 순서가 올바르게 유지됩니다.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/main/resources/static/js/common/edit-qna.js around lines 80 to 96, the
code correctly preserves ordering for existing images but does not assign
ordering values when adding new images in handleImageUpload. To fix this, update
the handleImageUpload function's json.images.forEach loop to assign an ordering
value to each new image based on the current length of originalImages or the
last ordering value. Confirm whether the server returns ordering; if not,
calculate and set ordering on the client side before pushing new images to
originalImages to maintain correct image order.
src/main/java/io/github/petty/community/service/CommentServiceImpl.java (2)

42-54: 🛠️ Refactor suggestion

@transactional 어노테이션 누락

addComment 메서드에도 @Transactional 어노테이션이 필요합니다. 댓글 추가와 게시글의 댓글 수 증가는 하나의 트랜잭션으로 처리되어야 합니다.

+@Transactional
@Override
public Long addComment(Long postId, CommentRequest request, Users user) {
    Post post = postRepository.findById(postId)
            .orElseThrow(() -> new IllegalArgumentException("해당 게시글이 존재하지 않습니다."));

    Comment comment = new Comment();
    comment.setPost(post);
    comment.setUser(user);
    comment.setContent(request.getContent());

    post.setCommentCount(post.getCommentCount() + 1); // 댓글 수 증가

    return commentRepository.save(comment).getId();
}

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/main/java/io/github/petty/community/service/CommentServiceImpl.java
around lines 42 to 54, the addComment method lacks the @Transactional
annotation. Add @Transactional above the method declaration to ensure that
adding a comment and incrementing the post's comment count are executed within a
single transaction.

56-75: 🛠️ Refactor suggestion

댓글 삭제 로직 최적화 필요

댓글 삭제 메서드에서 댓글 수를 계산하기 위해 추가 쿼리를 실행하고 있습니다. 이는 성능에 영향을 줄 수 있습니다. Post 엔티티에서 직접 댓글 수를 감소시키는 것이 더 효율적입니다.

@Transactional
@Override
public void deleteComment(Long commentId, Users user) {
    Comment comment = commentRepository.findById(commentId)
            .orElseThrow(() -> new IllegalArgumentException("해당 댓글이 존재하지 않습니다."));

    if (!comment.getUser().getId().equals(user.getId())) {
        throw new IllegalArgumentException("삭제 권한이 없습니다.");
    }

    Long postId = comment.getPost().getId();
+    Post post = comment.getPost();
    commentRepository.delete(comment);
-    long commentCount = commentRepository.countByPostId(postId);

    // Post를 새로 조회해서 댓글 수 업데이트
-    Post post = postRepository.findById(postId)
-            .orElseThrow(() -> new RuntimeException("게시글을 찾을 수 없습니다."));
-    post.setCommentCount((int) commentCount);
+    post.setCommentCount(post.getCommentCount() - 1);
    postRepository.save(post);
}
📝 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.

    @Transactional
    @Override
    public void deleteComment(Long commentId, Users user) {
        Comment comment = commentRepository.findById(commentId)
                .orElseThrow(() -> new IllegalArgumentException("해당 댓글이 존재하지 않습니다."));

        if (!comment.getUser().getId().equals(user.getId())) {
            throw new IllegalArgumentException("삭제 권한이 없습니다.");
        }

        Long postId = comment.getPost().getId();
        Post post = comment.getPost();
        commentRepository.delete(comment);

        // Post를 새로 조회해서 댓글 수 업데이트
        post.setCommentCount(post.getCommentCount() - 1);
        postRepository.save(post);
    }
🤖 Prompt for AI Agents
In src/main/java/io/github/petty/community/service/CommentServiceImpl.java lines
56 to 75, the deleteComment method currently recalculates the comment count by
querying the database after deleting a comment, which is inefficient. Instead,
modify the method to decrement the comment count directly on the Post entity
after deleting the comment, then save the updated Post. This avoids the extra
count query and improves performance.
src/main/resources/templates/post-qna-list.html (3)

6-286: 🛠️ Refactor suggestion

인라인 CSS를 외부 파일로 분리하세요

인라인 스타일이 HTML 파일 크기를 크게 증가시키고 있습니다. CSS를 외부 파일로 분리하여 코드의 가독성을 향상시키고 브라우저 캐싱을 활용할 수 있습니다.

/static/css/community.css 파일을 생성하고 스타일 코드를 이동한 후, HTML에서 참조하는 것이 좋습니다.

<head>
    <meta charset="UTF-8">
    <title>PETTY - 궁금해요! 질문답변</title>
-    <style>
-        /* 전체 디자인 시스템에서 공통 스타일 가져옴 */
-        :root {
-            /* 컬러 팔레트 */
-            --primary: #FF9933;      /* 주 브랜드 색상: 밝은 오렌지 */
-            /* ... 중략 ... */
-        }
-        /* ... 중략 ... */
-    </style>
+    <link rel="stylesheet" href="/css/community.css">
</head>
📝 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.

<head>
    <meta charset="UTF-8">
    <title>PETTY - 궁금해요! 질문답변</title>
    <link rel="stylesheet" href="/css/community.css">
</head>
🤖 Prompt for AI Agents
In src/main/resources/templates/post-qna-list.html from lines 6 to 286, the CSS
styles are currently embedded inline within the HTML file, which increases file
size and reduces maintainability. To fix this, move all the CSS code inside the
<style> tag to a new external stylesheet file located at
/static/css/community.css. Then, remove the inline <style> block from the HTML
and add a <link> tag in the HTML head to reference the external CSS file,
enabling better readability and browser caching.

328-450: 🛠️ Refactor suggestion

API 오류 처리 및 로딩 상태 추가 필요

데이터 가져오기에 대한 오류 처리가 없고, 로딩 중 상태를 사용자에게 표시하지 않습니다. 또한 데이터가 없는 경우에 대한 처리도 필요합니다.

async function fetchQnaPosts() {
+    const list = document.getElementById('qnaList');
+    list.innerHTML = '<div class="loading-indicator">게시글을 불러오는 중...</div>';
+    
+    try {
        const res = await fetch(`/api/posts?type=QNA&page=${currentPage}&size=${pageSize}`);
+        if (!res.ok) {
+            throw new Error(`서버 오류: ${res.status}`);
+        }
        const page = await res.json();
        const posts = page.content;

-        const list = document.getElementById('qnaList');
        list.innerHTML = '';

+        if (posts.length === 0) {
+            list.innerHTML = '<div class="empty-list">등록된 게시글이 없습니다.</div>';
+            return;
+        }

        posts.forEach(post => {
            // 기존 코드 유지
        });

        renderPagination(page.totalPages);
+    } catch (error) {
+        console.error('게시글 로딩 오류:', error);
+        list.innerHTML = `<div class="error-message">
+            게시글을 불러오는 중 오류가 발생했습니다. 
+            <button onclick="fetchQnaPosts()">다시 시도</button>
+        </div>`;
+    }
}
📝 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.

async function fetchQnaPosts() {
    const list = document.getElementById('qnaList');
    list.innerHTML = '<div class="loading-indicator">게시글을 불러오는 중...</div>';
    
    try {
        const res = await fetch(`/api/posts?type=QNA&page=${currentPage}&size=${pageSize}`);
        if (!res.ok) {
            throw new Error(`서버 오류: ${res.status}`);
        }
        const page = await res.json();
        const posts = page.content;

        // 기존 로딩 인디케이터 제거
        list.innerHTML = '';

        // 데이터가 없을 때
        if (posts.length === 0) {
            list.innerHTML = '<div class="empty-list">등록된 게시글이 없습니다.</div>';
            return;
        }

        // 기존 게시글 렌더링 로직
        posts.forEach(post => {
            const item = document.createElement('div');
            item.className = 'qna-item';

            const header = document.createElement('div');
            header.className = 'qna-header';

            const petType = document.createElement('span');
            petType.className = 'pet-type';
            petType.dataset.type = post.petType;
            petType.textContent = post.petType;
            header.appendChild(petType);

            const statusBadge = document.createElement('span');
            statusBadge.className = post.isResolved ? 'status-badge resolved' : 'status-badge';
            statusBadge.textContent = post.isResolved ? '해결됨' : '미해결';
            header.appendChild(statusBadge);

            item.appendChild(header);

            const title = document.createElement('h3');
            const titleLink = document.createElement('a');
            titleLink.href = `/posts/detail?id=${post.id}`;
            titleLink.textContent = post.title;
            title.appendChild(titleLink);
            item.appendChild(title);

            const content = document.createElement('p');
            content.className = 'content-preview';
            content.textContent = post.content
              ? (post.content.substring(0, 100) + (post.content.length > 100 ? '...' : ''))
              : '질문 내용을 확인하려면 클릭하세요.';
            item.appendChild(content);

            const meta = document.createElement('div');
            meta.className = 'meta';

            const writer = document.createElement('span');
            writer.innerHTML = `
                <svg width="16" height="16" ...>...</svg>
                ${post.writer}
            `;
            meta.appendChild(writer);

            if (post.region) {
                const region = document.createElement('span');
                region.innerHTML = `
                    <svg width="16" height="16" ...>...</svg>
                    ${post.region}
                `;
                meta.appendChild(region);
            }

            const date = document.createElement('span');
            date.innerHTML = `
                <svg width="16" height="16" ...>...</svg>
                ${new Date(post.createdAt).toLocaleDateString()}
            `;
            meta.appendChild(date);

            const comments = document.createElement('span');
            comments.innerHTML = `
                <svg width="16" height="16" ...>...</svg>
                ${post.commentCount || 0}
            `;
            meta.appendChild(comments);

            const likes = document.createElement('span');
            likes.innerHTML = `
                <svg width="16" height="16" ...>...</svg>
                ${post.likeCount || 0}
            `;
            meta.appendChild(likes);

            item.appendChild(meta);
            list.appendChild(item);
        });

        renderPagination(page.totalPages);
    } catch (error) {
        console.error('게시글 로딩 오류:', error);
        list.innerHTML = `
            <div class="error-message">
                게시글을 불러오는 중 오류가 발생했습니다.
                <button onclick="fetchQnaPosts()">다시 시도</button>
            </div>
        `;
    }
}
🤖 Prompt for AI Agents
In src/main/resources/templates/post-qna-list.html between lines 328 and 450,
the fetchQnaPosts function lacks error handling, loading state indication, and
empty data handling. Add a try-catch block around the fetch call to catch and
handle API errors gracefully, such as displaying an error message to the user.
Implement a loading indicator that shows while data is being fetched and hides
after completion. Also, check if the posts array is empty and display a
user-friendly message indicating no posts are available instead of clearing the
list silently.

332-430: ⚠️ Potential issue

XSS 취약점 방지를 위한 콘텐츠 보안 추가 필요

사용자가 입력한 콘텐츠(제목, 내용 등)를 직접 DOM에 삽입하고 있어 XSS 공격에 취약할 수 있습니다. 내용을 안전하게 표시하기 위한 이스케이프 함수가 필요합니다.

HTML 이스케이프 유틸리티 함수를 추가하고 사용자 입력 콘텐츠를 표시할 때 이를 사용하세요:

+ function escapeHTML(str) {
+     return str
+         .replace(/&/g, '&amp;')
+         .replace(/</g, '&lt;')
+         .replace(/>/g, '&gt;')
+         .replace(/"/g, '&quot;')
+         .replace(/'/g, '&#039;');
+ }

async function fetchQnaPosts() {
    // 기존 코드...
    
    posts.forEach(post => {
        // 코드 일부 생략...
        
        const titleLink = document.createElement('a');
        titleLink.href = `/posts/detail?id=${post.id}`;
-       titleLink.textContent = post.title;
+       titleLink.textContent = escapeHTML(post.title);
        title.appendChild(titleLink);
        item.appendChild(title);

        const content = document.createElement('p');
        content.className = 'content-preview';
-       content.textContent = post.content ? (post.content.substring(0, 100) + (post.content.length > 100 ? '...' : '')) : '질문 내용을 확인하려면 클릭하세요.';
+       content.textContent = post.content ? (escapeHTML(post.content).substring(0, 100) + (post.content.length > 100 ? '...' : '')) : '질문 내용을 확인하려면 클릭하세요.';
        item.appendChild(content);
        
        // 코드 일부 생략...
        
        const writer = document.createElement('span');
        writer.innerHTML = `
            <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
                <path d="M12 11C14.2091 11 16 9.20914 16 7C16 4.79086 14.2091 3 12 3C9.79086 3 8 4.79086 8 7C8 9.20914 9.79086 11 12 11Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
                <path d="M5.33788 18.3206C5.99897 16.5269 7.77368 15.3111 9.76328 15.6223L11 15.8093C11.6432 15.9331 12.3568 15.9331 13 15.8093L14.2367 15.6223C16.2263 15.3111 18.001 16.5269 18.6621 18.3206C19.0127 19.2521 19.4593 20.4217 19.6682 21.1328C19.8155 21.6127 19.4317 22 18.9222 22H5.07778C4.56833 22 4.18448 21.6127 4.33181 21.1328C4.54065 20.4217 4.98733 19.2521 5.33788 18.3206Z" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
            </svg>
-           ${post.writer}
+           ${escapeHTML(post.writer)}
        `;
        // 기존 코드...
    });
    // 기존 코드...
}
🤖 Prompt for AI Agents
In src/main/resources/templates/post-qna-list.html between lines 332 and 430,
user-generated content such as post titles and content are directly inserted
into the DOM, which poses an XSS vulnerability. To fix this, create an HTML
escape utility function that converts special characters to their safe HTML
entities, and apply this function to all user input fields like post.title,
post.content, post.writer, and post.region before inserting them into the DOM.
Replace any direct textContent or innerHTML assignments of user content with the
escaped versions to ensure safe rendering.
src/main/java/io/github/petty/community/service/PostImageServiceImpl.java (2)

36-44: ⚠️ Potential issue

Supabase 스토리지에서 이미지 삭제 누락

deleteImagesByPostIddeleteImage 메서드는 데이터베이스에서 이미지 레코드만 삭제하고 Supabase 스토리지에서 실제 이미지 파일을 삭제하지 않습니다. 이로 인해 스토리지 리소스가 낭비될 수 있습니다.

@Override
public void deleteImagesByPostId(Long postId) {
+    List<PostImage> images = postImageRepository.findByPostId(postId);
+    for (PostImage image : images) {
+        try {
+            supabaseUploader.delete(image.getImageUrl());
+        } catch (Exception e) {
+            // 스토리지 삭제 실패 로깅
+            log.error("Supabase에서 이미지 삭제 실패: {}", image.getImageUrl(), e);
+        }
+    }
    postImageRepository.deleteByPostId(postId);
}

@Override
public void deleteImage(Long imageId) {
+    PostImage image = postImageRepository.findById(imageId)
+            .orElseThrow(() -> new IllegalArgumentException("이미지를 찾을 수 없습니다."));
+    try {
+        supabaseUploader.delete(image.getImageUrl());
+    } catch (Exception e) {
+        log.error("Supabase에서 이미지 삭제 실패: {}", image.getImageUrl(), e);
+    }
    postImageRepository.deleteById(imageId);
}
📝 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.

    @Override
    public void deleteImagesByPostId(Long postId) {
        List<PostImage> images = postImageRepository.findByPostId(postId);
        for (PostImage image : images) {
            try {
                supabaseUploader.delete(image.getImageUrl());
            } catch (Exception e) {
                // 스토리지 삭제 실패 로깅
                log.error("Supabase에서 이미지 삭제 실패: {}", image.getImageUrl(), e);
            }
        }
        postImageRepository.deleteByPostId(postId);
    }

    @Override
    public void deleteImage(Long imageId) {
        PostImage image = postImageRepository.findById(imageId)
                .orElseThrow(() -> new IllegalArgumentException("이미지를 찾을 수 없습니다."));
        try {
            supabaseUploader.delete(image.getImageUrl());
        } catch (Exception e) {
            log.error("Supabase에서 이미지 삭제 실패: {}", image.getImageUrl(), e);
        }
        postImageRepository.deleteById(imageId);
    }
🤖 Prompt for AI Agents
In src/main/java/io/github/petty/community/service/PostImageServiceImpl.java
around lines 36 to 44, the methods deleteImagesByPostId and deleteImage only
remove image records from the database but do not delete the actual image files
from Supabase storage. To fix this, integrate calls to the Supabase storage API
to delete the corresponding image files when these methods are invoked, ensuring
both the database records and storage files are removed to prevent resource
waste.

64-88: 🛠️ Refactor suggestion

트랜잭션 내에서 예외 처리 개선 필요

updateImagesFromRequest 메서드는 트랜잭션으로 처리되고 있지만, Supabase 스토리지 연산에서 발생하는 예외에 대한 처리가 없습니다. 외부 서비스 호출에서 예외가 발생하면 전체 트랜잭션이 롤백될 수 있습니다.

@Override
@Transactional
public void updateImagesFromRequest(Post post, List<PostImageRequest> imageRequests) {
+    if (imageRequests == null) return;
+    
    for (PostImageRequest dto : imageRequests) {
        if (Boolean.TRUE.equals(dto.getIsDeleted())) {
            if (dto.getId() != null) {
                PostImage image = postImageRepository.findById(dto.getId())
                        .orElseThrow(() -> new IllegalArgumentException("이미지를 찾을 수 없습니다."));
+                try {
                    postImageRepository.deleteById(dto.getId());
                    supabaseUploader.delete(image.getImageUrl()); // ✅ Supabase에서 삭제
+                } catch (Exception e) {
+                    log.error("이미지 삭제 중 오류 발생: {}", image.getImageUrl(), e);
+                    // 실패하더라도 DB 작업은 계속 진행
+                }
            }
        } else if (dto.getId() != null) {
            PostImage image = postImageRepository.findById(dto.getId())
                    .orElseThrow(() -> new IllegalArgumentException("이미지를 찾을 수 없습니다."));
            image.setImageUrl(dto.getImageUrl());
            image.setOrdering(dto.getOrdering());
        } else {
+            if (dto.getImageUrl() == null || dto.getImageUrl().isEmpty()) {
+                continue; // 이미지 URL이 없는 경우 건너뛰기
+            }
            PostImage newImage = new PostImage();
            newImage.setImageUrl(dto.getImageUrl());
            newImage.setOrdering(dto.getOrdering());
            newImage.setPost(post);
            postImageRepository.save(newImage);
        }
    }
}
📝 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.

    @Override
    @Transactional
    public void updateImagesFromRequest(Post post, List<PostImageRequest> imageRequests) {
        if (imageRequests == null) {
            return;
        }

        for (PostImageRequest dto : imageRequests) {
            if (Boolean.TRUE.equals(dto.getIsDeleted())) {
                if (dto.getId() != null) {
                    PostImage image = postImageRepository.findById(dto.getId())
                            .orElseThrow(() -> new IllegalArgumentException("이미지를 찾을 수 없습니다."));
                    try {
                        postImageRepository.deleteById(dto.getId());
                        supabaseUploader.delete(image.getImageUrl()); // ✅ Supabase에서 삭제
                    } catch (Exception e) {
                        log.error("이미지 삭제 중 오류 발생: {}", image.getImageUrl(), e);
                        // 실패하더라도 DB 작업은 계속 진행
                    }
                }
            } else if (dto.getId() != null) {
                PostImage image = postImageRepository.findById(dto.getId())
                        .orElseThrow(() -> new IllegalArgumentException("이미지를 찾을 수 없습니다."));
                image.setImageUrl(dto.getImageUrl());
                image.setOrdering(dto.getOrdering());
            } else {
                if (dto.getImageUrl() == null || dto.getImageUrl().isEmpty()) {
                    continue; // 이미지 URL이 없는 경우 건너뛰기
                }
                PostImage newImage = new PostImage();
                newImage.setImageUrl(dto.getImageUrl());
                newImage.setOrdering(dto.getOrdering());
                newImage.setPost(post);
                postImageRepository.save(newImage);
            }
        }
    }
🤖 Prompt for AI Agents
In src/main/java/io/github/petty/community/service/PostImageServiceImpl.java
around lines 64 to 88, the updateImagesFromRequest method lacks exception
handling for Supabase storage operations, which can cause the entire transaction
to roll back if an external service call fails. To fix this, wrap the
supabaseUploader.delete call in a try-catch block to handle exceptions
gracefully, log the error, and prevent it from propagating and rolling back the
transaction unnecessarily.
src/main/java/io/github/petty/community/controller/PostImageUploadController.java (2)

100-106: ⚠️ Potential issue

다중 이미지 업로드 시 부분 성공 처리 필요

현재 다중 이미지 업로드에서 한 이미지라도 실패하면 전체 업로드가 실패합니다. 이미 업로드된 이미지들에 대한 처리(롤백 또는 부분 성공 처리)가 필요합니다.

다음과 같이 개선할 수 있습니다:

  1. 각 이미지 업로드 시 성공/실패 여부를 추적
  2. 일부 실패해도 성공한 이미지 정보는 반환
  3. 실패한 이미지에 대한 정보도 응답에 포함
List<Map<String, Object>> imageResponses = new ArrayList<>();
+ List<Map<String, Object>> failedUploads = new ArrayList<>();
+ boolean hasErrors = false;

try {
    int order = 0;
    for (MultipartFile file : files) {
+       try {
            String url = supabaseUploader.upload(file);

            Map<String, Object> imageMap = new HashMap<>();
            imageMap.put("imageUrl", url);
            imageMap.put("ordering", order++);
            imageMap.put("isDeleted", false);

            imageResponses.add(imageMap);
+       } catch (IOException e) {
+           log.error("이미지 업로드 실패: " + file.getOriginalFilename(), e);
+           Map<String, Object> errorMap = new HashMap<>();
+           errorMap.put("filename", file.getOriginalFilename());
+           errorMap.put("error", e.getMessage());
+           failedUploads.add(errorMap);
+           hasErrors = true;
+       }
    }
+   
+   if (hasErrors) {
+       return ResponseEntity.status(HttpStatus.PARTIAL_CONTENT)
+           .body(Map.of(
+               "images", imageResponses,
+               "failed", failedUploads,
+               "message", "일부 이미지 업로드에 실패했습니다."
+           ));
+   }
    
    return ResponseEntity.ok(Map.of("images", imageResponses));
} catch (IOException e) {
    log.error("이미지 업로드 실패", e);
    return ResponseEntity.internalServerError().body(Map.of("error", "이미지 업로드 실패", "message", e.getMessage()));
}
📝 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.

    List<Map<String, Object>> imageResponses = new ArrayList<>();
+   List<Map<String, Object>> failedUploads = new ArrayList<>();
+   boolean hasErrors = false;

    try {
        int order = 0;
        for (MultipartFile file : files) {
+           try {
                String url = supabaseUploader.upload(file);

                Map<String, Object> imageMap = new HashMap<>();
                imageMap.put("imageUrl", url);
                imageMap.put("ordering", order++);
                imageMap.put("isDeleted", false);

                imageResponses.add(imageMap);
+           } catch (IOException e) {
+               log.error("이미지 업로드 실패: " + file.getOriginalFilename(), e);
+               Map<String, Object> errorMap = new HashMap<>();
+               errorMap.put("filename", file.getOriginalFilename());
+               errorMap.put("error", e.getMessage());
+               failedUploads.add(errorMap);
+               hasErrors = true;
+           }
        }

+       if (hasErrors) {
+           return ResponseEntity
+               .status(HttpStatus.PARTIAL_CONTENT)
+               .body(Map.of(
+                   "images", imageResponses,
+                   "failed", failedUploads,
+                   "message", "일부 이미지 업로드에 실패했습니다."
+               ));
+       }

        return ResponseEntity.ok(Map.of("images", imageResponses));
    } catch (IOException e) {
        log.error("이미지 업로드 실패", e);
        return ResponseEntity
            .internalServerError()
            .body(Map.of("error", "이미지 업로드 실패", "message", e.getMessage()));
    }
🤖 Prompt for AI Agents
In
src/main/java/io/github/petty/community/controller/PostImageUploadController.java
around lines 100 to 106, the current multi-image upload fails entirely if any
single image upload fails. Modify the code to track success or failure for each
image individually, collect successful image responses and failure details
separately, and return a combined response that includes both successful uploads
and information about failed images, enabling partial success handling.

108-120: 🛠️ Refactor suggestion

유효성 검사 로직 개선

파일 유효성 검사 메서드에 MIME 타입 검사가 추가되면 보안이 강화될 수 있습니다. 또한, 파일 이름 검증 시 안전하지 않은 문자가 포함되어 있는지 확인하는 것이 좋습니다.

다음과 같이 개선해 보세요:

private boolean isValidImage(MultipartFile file) {
    if (file.isEmpty() || file.getSize() > MAX_FILE_SIZE) {
        return false;
    }

    String originalFilename = file.getOriginalFilename();
    if (originalFilename == null) {
        return false;
    }

+   // MIME 타입 검사 추가
+   String contentType = file.getContentType();
+   if (contentType == null || !contentType.startsWith("image/")) {
+       return false;
+   }
+   
+   // 안전하지 않은 파일명 문자 검사
+   if (originalFilename.contains("..") || originalFilename.contains("/") || originalFilename.contains("\\")) {
+       return false;
+   }

    int lastDotIndex = originalFilename.lastIndexOf('.');
    if (lastDotIndex == -1) {
        return false;
    }
    String extension = originalFilename.substring(lastDotIndex + 1).toLowerCase();
    return ALLOWED_EXTENSIONS.contains(extension);
}
📝 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.

    private boolean isValidImage(MultipartFile file) {
        if (file.isEmpty() || file.getSize() > MAX_FILE_SIZE) {
            return false;
        }

        String originalFilename = file.getOriginalFilename();
        if (originalFilename == null) {
            return false;
        }

        // MIME 타입 검사 추가
        String contentType = file.getContentType();
        if (contentType == null || !contentType.startsWith("image/")) {
            return false;
        }
        
        // 안전하지 않은 파일명 문자 검사
        if (originalFilename.contains("..")
            || originalFilename.contains("/")
            || originalFilename.contains("\\")) {
            return false;
        }

        int lastDotIndex = originalFilename.lastIndexOf('.');
        if (lastDotIndex == -1) {
            return false;
        }

        String extension = originalFilename.substring(lastDotIndex + 1).toLowerCase();
        return ALLOWED_EXTENSIONS.contains(extension);
    }
🤖 Prompt for AI Agents
In
src/main/java/io/github/petty/community/controller/PostImageUploadController.java
around lines 108 to 120, enhance the isValidImage method by adding MIME type
validation to ensure the uploaded file is an actual image, and include a check
for unsafe characters in the original filename to prevent security risks. Update
the method to verify the file's content type against allowed image MIME types
and validate the filename against a safe pattern before proceeding with the
extension check.
src/main/resources/templates/post-detail.html (4)

701-702: ⚠️ Potential issue

XSS 취약점 발견: innerHTML과 사용자 입력 데이터

현재 post.content의 내용을 innerHTML로 직접 삽입하고 있어 XSS 공격에 취약합니다. 사용자가 입력한 HTML 태그가 그대로 실행될 수 있습니다.

다음과 같이 개선하세요:

- <p>${post.content?.replaceAll('\n', '<br>')}</p>
+ <p>${post.content ? escapeHtml(post.content).replaceAll('\n', '<br>') : ''}</p>

그리고 다음 함수를 스크립트에 추가하세요:

function escapeHtml(text) {
    const div = document.createElement('div');
    div.textContent = text;
    return div.innerHTML;
}

또는 DOMPurify 라이브러리를 사용하여 안전한 HTML만 허용하는 방법도 있습니다:

<script src="https://cdnjs.cloudflare.com/ajax/libs/dompurify/2.4.0/purify.min.js"></script>
<p>${post.content ? DOMPurify.sanitize(post.content.replaceAll('\n', '<br>')) : ''}</p>
🤖 Prompt for AI Agents
In src/main/resources/templates/post-detail.html around lines 701 to 702, the
code directly inserts post.content into the HTML using innerHTML, which exposes
an XSS vulnerability by allowing execution of user-supplied HTML. To fix this,
sanitize post.content before insertion by either escaping HTML special
characters using a function like escapeHtml that sets textContent on a temporary
element and returns its innerHTML, or by integrating the DOMPurify library to
sanitize the content safely. Replace the current insertion with a sanitized
version of post.content where newline characters are replaced with <br> tags,
ensuring no malicious scripts can run.

788-789: ⚠️ Potential issue

XSS 취약점 발견: 자바스크립트 이벤트 핸들러에서 문자열 이스케이프 처리 필요

comment.content를 단일 따옴표로 감싸고 있어, 내용에 따옴표가 포함된 경우 JavaScript 구문 오류가 발생하거나 코드 실행이 가능합니다.

다음과 같이 개선하세요:

- <button onclick="editComment(${comment.id}, '${comment.content.replace(/'/g, "\\'")}')">수정</button>
+ <button onclick="editComment(${comment.id}, ${JSON.stringify(comment.content)})">수정</button>

JSON.stringify를 사용하면 문자열을 안전하게 JSON 형식으로 직렬화하여 이스케이핑 문제를 해결할 수 있습니다.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/main/resources/templates/post-detail.html around lines 788 to 789, the
onclick handler for editComment uses comment.content wrapped in single quotes
without proper escaping, causing potential XSS vulnerabilities and JavaScript
syntax errors if the content contains quotes. Fix this by replacing the current
string interpolation with JSON.stringify(comment.content) to safely serialize
and escape the content string for use in the JavaScript context.

858-873: 🛠️ Refactor suggestion

게시글 삭제 기능 개선 필요

현재 게시글 삭제 기능에는 다음과 같은 문제가 있습니다:

  1. 에러 처리가 불충분합니다 (try-catch 없음)
  2. 사용자 확인 다이얼로그가 단순합니다
  3. 실패 시 응답 본문을 검사하지 않습니다

다음과 같이 개선해 보세요:

async function deletePost() {
-   if (!confirm("이 게시글을 삭제하시겠습니까?")) return;
+   if (!confirm("이 게시글을 삭제하시겠습니까?\n삭제된 게시글은 복구할 수 없습니다.")) return;

+   try {
        const res = await fetch(`/api/posts/${postId}`, {
            method: 'DELETE',
            headers: {
                'Authorization': 'Bearer ' + localStorage.getItem("jwt")
            }
        });

        if (res.ok) {
            alert("게시글이 삭제되었습니다.");
            location.href = "/";
        } else {
-           alert("삭제 실패. 권한이 없습니다.");
+           const errorData = await res.json().catch(() => ({}));
+           if (res.status === 403) {
+               alert("삭제 권한이 없습니다. 본인이 작성한 게시글만 삭제할 수 있습니다.");
+           } else if (res.status === 401) {
+               alert("로그인이 필요합니다. 로그인 페이지로 이동합니다.");
+               location.href = "/login?redirect=" + encodeURIComponent(location.href);
+           } else {
+               alert(`삭제 실패: ${errorData.message || '알 수 없는 오류가 발생했습니다.'}`);
+           }
        }
+   } catch (err) {
+       console.error("게시글 삭제 중 오류 발생:", err);
+       alert("네트워크 오류로 삭제에 실패했습니다. 잠시 후 다시 시도해주세요.");
+   }
}
📝 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.

        if (!confirm("이 게시글을 삭제하시겠습니까?\n삭제된 게시글은 복구할 수 없습니다.")) return;

        try {
            const res = await fetch(`/api/posts/${postId}`, {
                method: 'DELETE',
                headers: {
                    'Authorization': 'Bearer ' + localStorage.getItem("jwt")
                }
            });

            if (res.ok) {
                alert("게시글이 삭제되었습니다.");
                location.href = "/";
            } else {
                const errorData = await res.json().catch(() => ({}));
                if (res.status === 403) {
                    alert("삭제 권한이 없습니다. 본인이 작성한 게시글만 삭제할 수 있습니다.");
                } else if (res.status === 401) {
                    alert("로그인이 필요합니다. 로그인 페이지로 이동합니다.");
                    location.href = "/login?redirect=" + encodeURIComponent(location.href);
                } else {
                    alert(`삭제 실패: ${errorData.message || '알 수 없는 오류가 발생했습니다.'}`);
                }
            }
        } catch (err) {
            console.error("게시글 삭제 중 오류 발생:", err);
            alert("네트워크 오류로 삭제에 실패했습니다. 잠시 후 다시 시도해주세요.");
        }
    }
🤖 Prompt for AI Agents
In src/main/resources/templates/post-detail.html around lines 858 to 873,
improve the post deletion function by wrapping the fetch call and related logic
in a try-catch block to handle network or unexpected errors gracefully. Replace
the simple confirm dialog with a more user-friendly confirmation method if
possible. On failure, parse and display the error message from the response body
instead of a generic alert, providing clearer feedback to the user.

793-800: ⚠️ Potential issue

XSS 취약점 발견: 댓글 내용 출력

댓글 내용을 출력할 때 innerHTML을 사용하고 있어 XSS 공격에 취약합니다. 사용자가 입력한 악성 스크립트가 실행될 수 있습니다.

다음과 같이 개선하세요:

div.innerHTML = `
    <div class="comment-header">
        <span class="comment-author">${comment.writer}</span>
        <span class="comment-date">${new Date(comment.createdAt).toLocaleString()}</span>
    </div>
-   <div id="content-${comment.id}" class="comment-content">${comment.content}</div>
+   <div id="content-${comment.id}" class="comment-content"></div>
    ${commentActionsHtml}
`;
+ document.getElementById(`content-${comment.id}`).textContent = comment.content;

또는 이전 제안과 같이 escapeHtml 함수를 사용하세요:

div.innerHTML = `
    <div class="comment-header">
        <span class="comment-author">${comment.writer}</span>
        <span class="comment-date">${new Date(comment.createdAt).toLocaleString()}</span>
    </div>
-   <div id="content-${comment.id}" class="comment-content">${comment.content}</div>
+   <div id="content-${comment.id}" class="comment-content">${escapeHtml(comment.content)}</div>
    ${commentActionsHtml}
`;
📝 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.

                div.innerHTML = `
                    <div class="comment-header">
                        <span class="comment-author">${comment.writer}</span>
                        <span class="comment-date">${new Date(comment.createdAt).toLocaleString()}</span>
                    </div>
                    <div id="content-${comment.id}" class="comment-content"></div>
                    ${commentActionsHtml}
                `;
                document.getElementById(`content-${comment.id}`).textContent = comment.content;
🤖 Prompt for AI Agents
In src/main/resources/templates/post-detail.html around lines 793 to 800, the
code uses innerHTML to insert comment content directly, which exposes an XSS
vulnerability by allowing execution of malicious scripts. To fix this, avoid
using innerHTML for user-generated content and instead set the text content
safely by escaping HTML entities or using a function like escapeHtml to sanitize
comment.content before inserting it into the DOM.
src/main/java/io/github/petty/community/controller/PostController.java (2)

28-35: 🛠️ Refactor suggestion

인증된 사용자 확인 로직 개선 필요

현재 코드는 userDetails가 null인 경우를 고려하지 않고 있습니다. @AuthenticationPrincipal이 null이 될 수 있으며, 이 경우 NullPointerException이 발생할 수 있습니다.

다음과 같이 개선하세요:

@PostMapping
public ResponseEntity<?> create(@RequestBody PostRequest request,
                              @AuthenticationPrincipal CustomUserDetails userDetails) {
+   if (userDetails == null) {
+       return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
+   }
    String username = userDetails.getUsername();
    Users user = usersRepository.findByUsername(username);
+   if (user == null) {
+       return ResponseEntity.status(HttpStatus.NOT_FOUND)
+           .body(Map.of("error", "사용자 정보를 찾을 수 없습니다."));
+   }
    Long id = postService.save(request, user);
    return ResponseEntity.ok(Map.of("id", id));
}
📝 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
    public ResponseEntity<?> create(@RequestBody PostRequest request,
                                    @AuthenticationPrincipal CustomUserDetails userDetails) {
        if (userDetails == null) {
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
        }
        String username = userDetails.getUsername();
        Users user = usersRepository.findByUsername(username);
        if (user == null) {
            return ResponseEntity.status(HttpStatus.NOT_FOUND)
                                 .body(Map.of("error", "사용자 정보를 찾을 수 없습니다."));
        }
        Long id = postService.save(request, user);
        return ResponseEntity.ok(Map.of("id", id));
    }
🤖 Prompt for AI Agents
In src/main/java/io/github/petty/community/controller/PostController.java around
lines 28 to 35, the create method does not check if userDetails is null, which
can cause a NullPointerException. Add a null check for userDetails at the start
of the method and return an appropriate unauthorized response if it is null.
This ensures the method safely handles unauthenticated requests without throwing
exceptions.

70-77: 🛠️ Refactor suggestion

좋아요 토글 시 예외 처리 개선 필요

좋아요 토글 API에서 사용자 정보가 없거나 게시글을 찾을 수 없는 경우에 대한 예외 처리가 부족합니다.

다음과 같이 개선하세요:

@PostMapping("/{id}/like")
public ResponseEntity<?> likePost(@PathVariable Long id,
                                @AuthenticationPrincipal CustomUserDetails userDetails) {
+   if (userDetails == null) {
+       return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
+           .body(Map.of("error", "로그인이 필요합니다."));
+   }
+   
    String username = userDetails.getUsername();
    Users user = usersRepository.findByUsername(username);
+   if (user == null) {
+       return ResponseEntity.status(HttpStatus.NOT_FOUND)
+           .body(Map.of("error", "사용자 정보를 찾을 수 없습니다."));
+   }
+   
+   try {
        int newCount = postService.toggleLike(id, user); // 좋아요 또는 취소
        return ResponseEntity.ok(Map.of("likeCount", newCount));
+   } catch (IllegalArgumentException e) {
+       return ResponseEntity.status(HttpStatus.NOT_FOUND)
+           .body(Map.of("error", "게시글을 찾을 수 없습니다."));
+   } catch (Exception e) {
+       return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
+           .body(Map.of("error", "좋아요 처리 중 오류가 발생했습니다."));
+   }
}
📝 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("/{id}/like")
    public ResponseEntity<?> likePost(@PathVariable Long id,
                                      @AuthenticationPrincipal CustomUserDetails userDetails) {
        if (userDetails == null) {
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
                .body(Map.of("error", "로그인이 필요합니다."));
        }

        String username = userDetails.getUsername();
        Users user = usersRepository.findByUsername(username);
        if (user == null) {
            return ResponseEntity.status(HttpStatus.NOT_FOUND)
                .body(Map.of("error", "사용자 정보를 찾을 수 없습니다."));
        }

        try {
            int newCount = postService.toggleLike(id, user); // 좋아요 또는 취소
            return ResponseEntity.ok(Map.of("likeCount", newCount));
        } catch (IllegalArgumentException e) {
            return ResponseEntity.status(HttpStatus.NOT_FOUND)
                .body(Map.of("error", "게시글을 찾을 수 없습니다."));
        } catch (Exception e) {
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                .body(Map.of("error", "좋아요 처리 중 오류가 발생했습니다."));
        }
    }
🤖 Prompt for AI Agents
In src/main/java/io/github/petty/community/controller/PostController.java around
lines 70 to 77, the likePost method lacks proper exception handling for cases
when the user is not found or the post does not exist. Improve this by adding
checks after retrieving the user and before toggling the like; if the user or
post is missing, throw appropriate exceptions or return meaningful error
responses. Wrap the toggleLike call in a try-catch block to handle potential
exceptions and return suitable HTTP error statuses with descriptive messages.
src/main/java/io/github/petty/community/controller/CommentController.java (4)

23-26: 🛠️ Refactor suggestion

댓글 조회 API 예외 처리 개선 필요

댓글 조회 API에서는 게시글이 존재하지 않는 경우에 대한 예외 처리가 부족합니다.

다음과 같이 개선하세요:

@GetMapping("/api/posts/{postId}/comments")
public ResponseEntity<List<CommentResponse>> getComments(@PathVariable Long postId) {
+   try {
        return ResponseEntity.ok(commentService.getComments(postId));
+   } catch (IllegalArgumentException e) {
+       return ResponseEntity.status(HttpStatus.NOT_FOUND).body(Collections.emptyList());
+   } catch (Exception e) {
+       return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(Collections.emptyList());
+   }
}

이를 위해 필요한 import 문도 추가해야 합니다:

import java.util.Collections;
🤖 Prompt for AI Agents
In src/main/java/io/github/petty/community/controller/CommentController.java
around lines 23 to 26, the getComments method lacks exception handling for the
case when the post with the given postId does not exist. To fix this, add a
check to verify the existence of the post before fetching comments; if the post
does not exist, return an appropriate ResponseEntity such as
ResponseEntity.notFound().build(). Also, import java.util.Collections if needed
for returning empty lists or similar. This will improve robustness by properly
handling missing posts.

38-46: ⚠️ Potential issue

댓글 수정 API 인증 및 예외 처리 개선 필요

댓글 수정 API에서도 인증 및 사용자 정보 확인, 그리고 예외 처리가 개선되어야 합니다.

다음과 같이 개선하세요:

@PutMapping("/api/comments/{commentId}")
public ResponseEntity<?> updateComment(@PathVariable Long commentId,
                                     @RequestBody CommentRequest request,
                                     @AuthenticationPrincipal CustomUserDetails userDetails) {
+   if (userDetails == null) {
+       return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
+           .body(Map.of("error", "로그인이 필요합니다."));
+   }

    String username = userDetails.getUsername();
    Users user = usersRepository.findByUsername(username);
+   if (user == null) {
+       return ResponseEntity.status(HttpStatus.NOT_FOUND)
+           .body(Map.of("error", "사용자 정보를 찾을 수 없습니다."));
+   }
+   
+   try {
        commentService.updateComment(commentId, request, user);
        return ResponseEntity.ok().build();
+   } catch (IllegalArgumentException e) {
+       return ResponseEntity.badRequest().body(Map.of("error", e.getMessage()));
+   } catch (SecurityException e) {
+       return ResponseEntity.status(HttpStatus.FORBIDDEN)
+           .body(Map.of("error", "댓글 수정 권한이 없습니다."));
+   } catch (Exception e) {
+       return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
+           .body(Map.of("error", "댓글 수정 중 오류가 발생했습니다."));
+   }
}
📝 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.

    @PutMapping("/api/comments/{commentId}")
    public ResponseEntity<?> updateComment(@PathVariable Long commentId,
                                           @RequestBody CommentRequest request,
                                           @AuthenticationPrincipal CustomUserDetails userDetails) {
        if (userDetails == null) {
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
                                 .body(Map.of("error", "로그인이 필요합니다."));
        }

        String username = userDetails.getUsername();
        Users user = usersRepository.findByUsername(username);
        if (user == null) {
            return ResponseEntity.status(HttpStatus.NOT_FOUND)
                                 .body(Map.of("error", "사용자 정보를 찾을 수 없습니다."));
        }

        try {
            commentService.updateComment(commentId, request, user);
            return ResponseEntity.ok().build();
        } catch (IllegalArgumentException e) {
            return ResponseEntity.badRequest()
                                 .body(Map.of("error", e.getMessage()));
        } catch (SecurityException e) {
            return ResponseEntity.status(HttpStatus.FORBIDDEN)
                                 .body(Map.of("error", "댓글 수정 권한이 없습니다."));
        } catch (Exception e) {
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                                 .body(Map.of("error", "댓글 수정 중 오류가 발생했습니다."));
        }
    }
🤖 Prompt for AI Agents
In src/main/java/io/github/petty/community/controller/CommentController.java
around lines 38 to 46, the updateComment method lacks proper authentication
validation and exception handling. Improve it by verifying that the user is
authenticated before proceeding, checking if the user exists after fetching from
usersRepository, and adding try-catch blocks or appropriate exception handling
to manage errors from commentService.updateComment. Return meaningful HTTP
status codes and messages for authentication failures or update errors.

28-36: ⚠️ Potential issue

인증 및 예외 처리 개선 필요

댓글 추가 API에서 인증되지 않은 사용자나 사용자 정보를 찾을 수 없는 경우에 대한 처리가 부족합니다. NullPointerException이 발생할 수 있습니다.

다음과 같이 개선하세요:

@PostMapping("/api/posts/{postId}/comments")
public ResponseEntity<?> addComment(@PathVariable Long postId,
                                  @RequestBody CommentRequest request,
                                  @AuthenticationPrincipal CustomUserDetails userDetails) {
+   if (userDetails == null) {
+       return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
+           .body(Map.of("error", "로그인이 필요합니다."));
+   }

    String username = userDetails.getUsername();
    Users user = usersRepository.findByUsername(username);
+   if (user == null) {
+       return ResponseEntity.status(HttpStatus.NOT_FOUND)
+           .body(Map.of("error", "사용자 정보를 찾을 수 없습니다."));
+   }
+   
+   try {
        Long commentId = commentService.addComment(postId, request, user);
        return ResponseEntity.ok().body(commentId);
+   } catch (IllegalArgumentException e) {
+       return ResponseEntity.badRequest().body(Map.of("error", e.getMessage()));
+   } catch (Exception e) {
+       return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
+           .body(Map.of("error", "댓글 추가 중 오류가 발생했습니다."));
+   }
}
📝 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("/api/posts/{postId}/comments")
    public ResponseEntity<?> addComment(@PathVariable Long postId,
                                        @RequestBody CommentRequest request,
                                        @AuthenticationPrincipal CustomUserDetails userDetails) {
        if (userDetails == null) {
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
                                 .body(Map.of("error", "로그인이 필요합니다."));
        }

        String username = userDetails.getUsername();
        Users user = usersRepository.findByUsername(username);
        if (user == null) {
            return ResponseEntity.status(HttpStatus.NOT_FOUND)
                                 .body(Map.of("error", "사용자 정보를 찾을 수 없습니다."));
        }

        try {
            Long commentId = commentService.addComment(postId, request, user);
            return ResponseEntity.ok().body(commentId);
        } catch (IllegalArgumentException e) {
            return ResponseEntity.badRequest()
                                 .body(Map.of("error", e.getMessage()));
        } catch (Exception e) {
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                                 .body(Map.of("error", "댓글 추가 중 오류가 발생했습니다."));
        }
    }
🤖 Prompt for AI Agents
In src/main/java/io/github/petty/community/controller/CommentController.java
around lines 28 to 36, the addComment method lacks handling for unauthenticated
users or when the user is not found in the repository, which can cause
NullPointerExceptions. Add checks to verify that userDetails is not null and
that the user retrieved from usersRepository is present. If either is missing,
return an appropriate error response such as 401 Unauthorized or 404 Not Found
before proceeding to add the comment.

48-55: ⚠️ Potential issue

댓글 삭제 API 개선 필요

댓글 삭제 API에서도 인증 및 사용자 정보 확인, 그리고 예외 처리가 필요합니다.

다음과 같이 개선하세요:

@DeleteMapping("/api/comments/{commentId}")
public ResponseEntity<?> deleteComment(@PathVariable Long commentId,
                                     @AuthenticationPrincipal CustomUserDetails userDetails) {
+   if (userDetails == null) {
+       return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
+           .body(Map.of("error", "로그인이 필요합니다."));
+   }

    String username = userDetails.getUsername();
    Users user = usersRepository.findByUsername(username);
+   if (user == null) {
+       return ResponseEntity.status(HttpStatus.NOT_FOUND)
+           .body(Map.of("error", "사용자 정보를 찾을 수 없습니다."));
+   }
+   
+   try {
        commentService.deleteComment(commentId, user);
        return ResponseEntity.noContent().build();
+   } catch (IllegalArgumentException e) {
+       return ResponseEntity.badRequest().body(Map.of("error", e.getMessage()));
+   } catch (SecurityException e) {
+       return ResponseEntity.status(HttpStatus.FORBIDDEN)
+           .body(Map.of("error", "댓글 삭제 권한이 없습니다."));
+   } catch (Exception e) {
+       return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
+           .body(Map.of("error", "댓글 삭제 중 오류가 발생했습니다."));
+   }
}
📝 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.

    @DeleteMapping("/api/comments/{commentId}")
    public ResponseEntity<?> deleteComment(@PathVariable Long commentId,
                                           @AuthenticationPrincipal CustomUserDetails userDetails) {
        // 인증된 사용자 정보가 없으면 401
        if (userDetails == null) {
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
                                 .body(Map.of("error", "로그인이 필요합니다."));
        }

        String username = userDetails.getUsername();
        Users user = usersRepository.findByUsername(username);
        // 사용자 정보가 없으면 404
        if (user == null) {
            return ResponseEntity.status(HttpStatus.NOT_FOUND)
                                 .body(Map.of("error", "사용자 정보를 찾을 수 없습니다."));
        }

        try {
            commentService.deleteComment(commentId, user);
            return ResponseEntity.noContent().build();
        } catch (IllegalArgumentException e) {
            // 잘못된 입력 파라미터
            return ResponseEntity.badRequest()
                                 .body(Map.of("error", e.getMessage()));
        } catch (SecurityException e) {
            // 삭제 권한 없음
            return ResponseEntity.status(HttpStatus.FORBIDDEN)
                                 .body(Map.of("error", "댓글 삭제 권한이 없습니다."));
        } catch (Exception e) {
            // 그 외 서버 에러
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                                 .body(Map.of("error", "댓글 삭제 중 오류가 발생했습니다."));
        }
    }
🤖 Prompt for AI Agents
In src/main/java/io/github/petty/community/controller/CommentController.java
around lines 48 to 55, the deleteComment method lacks proper authentication
verification and exception handling. Enhance the method by verifying the user's
authentication status, checking if the user is authorized to delete the comment,
and adding try-catch blocks to handle potential exceptions gracefully. Return
appropriate HTTP status codes and messages for unauthorized access or errors
during deletion.
src/main/resources/static/js/common/form.js (3)

101-111: ⚠️ Potential issue

XSS / CSS Injection 가능성

data-url 속성에 업로드 URL을 그대로 삽입하고 querySelector("img[data-url='${url}']")로 검색합니다. URL에 '이나 "]가 포함되면 선택자 파싱이 깨질 수 있습니다.
CSS escape 처리를 하거나 dataset 속성으로 비교하는 방식이 안전합니다.

🤖 Prompt for AI Agents
In src/main/resources/static/js/common/form.js around lines 101 to 111, the code
inserts image URLs directly into the data-url attribute and uses a CSS selector
with that attribute for querying, which can break if the URL contains quotes or
special characters. To fix this, avoid embedding the URL directly in the
attribute selector string; instead, set the data-url attribute using the
element's dataset property and compare using dataset properties in JavaScript
rather than CSS selectors. This prevents selector parsing issues and potential
XSS or CSS injection vulnerabilities.

32-44: 🛠️ Refactor suggestion

업로드 총 개수 제한 누락

MAX_FILE_COUNT는 한 번에 선택한 파일 개수만 검사합니다. 사용자가 5장을 업로드한 뒤 다시 파일 선택 창을 열어 추가로 5장을 올리면 총 10장이 업로드됩니다.
uploadedImages.length + files.length로 전체 개수를 확인해 주세요.

- if (files.length > MAX_FILE_COUNT) {
+ if (uploadedImages.length + files.length > MAX_FILE_COUNT) {

Also applies to: 82-84

🤖 Prompt for AI Agents
In src/main/resources/static/js/common/form.js around lines 32 to 44 and also
lines 82 to 84, the code only checks the number of files selected in the current
upload against MAX_FILE_COUNT, but does not consider the total number of images
already uploaded. Modify the validation to check if the sum of
uploadedImages.length and the newly selected files.length exceeds
MAX_FILE_COUNT, and if so, show the alert and prevent the upload. This ensures
the total uploaded images never exceed the allowed maximum.

57-64: 🛠️ Refactor suggestion

JWT 토큰 미존재 시 예외 처리 필요

localStorage.getItem('jwt')null일 경우 인증 헤더가 Bearer null로 전송되어 401 에러가 발생합니다. 토큰이 없을 때는 로그인 페이지로 리다이렉트하거나 안내 메시지를 표시하는 로직을 추가해 주세요.
예)

if (!token) {
  alert('로그인이 필요합니다.');
  location.href = '/login';
  return;
}
🤖 Prompt for AI Agents
In src/main/resources/static/js/common/form.js around lines 57 to 64, the code
fetches the JWT token from localStorage but does not handle the case when the
token is null, causing an invalid Authorization header and a 401 error. Add a
check after retrieving the token to verify if it exists; if not, display an
alert notifying the user that login is required and redirect them to the login
page, then return early to prevent the fetch call.
src/main/java/io/github/petty/community/service/PostServiceImpl.java (2)

105-107: 🛠️ Refactor suggestion

이미지 레코드 정리 누락

게시글 삭제 시 PostImage가 orphan removal 되지 않으면 불필요한 레코드와 S3 객체가 남습니다.
postImageService.deleteAllFromPost(post) 호출 또는 JPA cascade 설정을 확인해 주세요.

🤖 Prompt for AI Agents
In src/main/java/io/github/petty/community/service/PostServiceImpl.java around
lines 105 to 107, the code deletes post likes and the post itself but does not
handle deletion of associated PostImage records, which can leave orphaned
database entries and S3 objects. To fix this, either call
postImageService.deleteAllFromPost(post) before deleting the post or configure
JPA cascade settings on the Post entity to automatically remove related
PostImage records when the post is deleted.

35-39: ⚠️ Potential issue

PetType 파싱 오류 가능성

PetType.valueOf(request.getPetType())는 잘못된 문자열 입력 시 IllegalArgumentException을 던집니다. 클라이언트 검증을 신뢰하지 말고 try‒catch로 감싸 사용자 친화적인 메시지를 반환하거나 기본값을 지정해 주세요.

Also applies to: 63-67

🤖 Prompt for AI Agents
In src/main/java/io/github/petty/community/service/PostServiceImpl.java around
lines 35 to 39 and similarly at lines 63 to 67, the code uses
PetType.valueOf(request.getPetType()) which throws IllegalArgumentException if
the input string is invalid. To fix this, wrap the valueOf call in a try-catch
block to handle the exception gracefully. In the catch block, either set a
default PetType value or handle the error by returning a user-friendly message
instead of letting the exception propagate.

@s0ooo0k
Copy link
Member

s0ooo0k commented May 23, 2025

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented May 23, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@PETTY-HUB PETTY-HUB deleted a comment from coderabbitai bot May 23, 2025
@PETTY-HUB PETTY-HUB deleted a comment from 23MinL May 23, 2025
@s0ooo0k s0ooo0k changed the title feat: Community 기능 추가 [COMMUNITY] Community 기능 추가 May 23, 2025
@s0ooo0k s0ooo0k merged commit 751c265 into PETTY-HUB:main May 23, 2025
2 checks passed
@23MinL
Copy link
Contributor

23MinL commented May 23, 2025

🎈코드 리뷰

🚨 먼저 해결하면 좋을 것들

1. Import 빼먹은 부분

위치: PostRequest.java

지금 PostImageRequest를 쓰고 있는데 import가 빠져있어요!

// 이것만 추가하면 돼요
import io.github.petty.community.dto.PostImageRequest;

2. 폼이 데이터를 제대로 못 보내고 있어요

위치: 여러 HTML 폼 파일들

지금 폼들을 보니까 이런 문제들이 있어요:

  • 어디로 보낼지 정해지지 않음 (action 없음)
  • 서버에서 어떤 이름으로 받을지 모름 (name 속성 없음)

예시 - 자랑 게시판 폼:

<!-- 지금 상태 -->
<form id="showoffForm">
    <input type="text" id="showoff-title" placeholder="자랑글 제목">

<!-- 이렇게 바꾸면 좋을 것 같아요 -->
<form id="showoffForm" action="/posts/showoff" method="post">
    <input type="text" id="showoff-title" name="title" placeholder="자랑글 제목">

💡 참고: JavaScript로 처리하실 거라면 action은 안 써도 되지만, name 속성은 꼭 필요해요!

🔐 보안 관련 (참고사항)

Spring Security CSRF 토큰

지금은 CSRF가 비활성화되어 있어서 당장 문제없지만, 나중에 보안 강화할 때를 위해 미리 준비해두면 좋을 것 같아요:

<form>
    <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>
    <!-- 폼 내용 -->
</form>

권한 체크

편집 페이지들이 로그인 안 해도 접근 가능해 보여요. 나중에 권한 체크 로직 추가하시면 좋을 것 같아요!

사용자 경험 개선 아이디어

1. 필수 입력 필드 표시

지금은 사용자가 빈 칸으로 제출할 수 있어요. 이렇게 하면 어떨까요?

<!-- required 속성 추가 -->
<input type="text" name="title" required placeholder="제목을 입력해주세요">
<textarea name="content" required placeholder="내용을 입력해주세요"></textarea>

2. 라디오 버튼 기본값

반려동물 종류 선택에서 기본값이 없어서 사용자가 실수로 안 선택할 수 있어요:

<!-- 강아지를 기본 선택으로 -->
<input type="radio" name="petType" value="DOG" checked> 강아지

3. URL 구조 개선

지금 편집 페이지 URL이 이렇게 되어 있어요:

  • /posts/review/edit ← 어떤 게시글인지 모름

이렇게 바꾸면 더 명확할 것 같아요:

  • /posts/review/edit/123 ← 123번 게시글 수정
// PostViewController.java
@GetMapping("/posts/review/edit/{id}")
public String reviewEditPage(@PathVariable Long id, Model model) {
    model.addAttribute("postId", id);
    return "edit-review";
}

🎯 좋아요/댓글 기능 업그레이드

현재 상황

백엔드 API는 잘 만들어져 있는데, 프론트에서 실시간 업데이트가 안 되고 있어요.
지금은 좋아요 누르고 새로고침해야 숫자가 바뀌죠?

개선 아이디어

// 좋아요 버튼 클릭 시
async function handleLike(postId) {
    try {
        const response = await fetch(`/posts/${postId}/like`, {
            method: 'POST'
        });
        
        if (response.ok) {
            const data = await response.json();
            // 화면의 좋아요 수 업데이트
            document.querySelector(`[data-post="${postId}"] .like-count`).textContent = data.likeCount;
        }
    } catch (error) {
        console.error('좋아요 처리 실패:', error);
        // 사용자에게 알림
        alert('좋아요 처리 중 문제가 발생했어요 😅');
    }
}

🛠️ 코드 품질 개선

1. 에러 처리 강화

JavaScript에서 API 호출할 때 에러 처리가 없어서, 네트워크 문제 생기면 사용자가 당황할 수 있어요:

// 기본 패턴
async function apiCall() {
    try {
        const response = await fetch('/api/something');
        
        if (!response.ok) {
            throw new Error('서버 응답 오류');
        }
        
        const data = await response.json();
        // 성공 처리
        
    } catch (error) {
        console.error('API 호출 실패:', error);
        // 사용자 친화적인 메시지
        showErrorMessage('잠시 후 다시 시도해주세요');
    }
}

2. 데이터베이스 관련

엔티티 설계가 전반적으로 좋은데, 몇 가지 추가하면 좋을 것들:

// PostImage.java
@Column(name = "image_url", nullable = false, length = 500) // URL 길이 제한
private String imageUrl;

@Min(0) @Max(4) // 이미지 순서 범위 제한
private Integer ordering;

3. 서비스 계층 정리

인터페이스에는 @service 어노테이션이 필요 없어요:

// CommentService.java - 인터페이스
public interface CommentService {
    // 메서드들
}

// CommentServiceImpl.java - 구현체
@Service
public class CommentServiceImpl implements CommentService {
    // 구현
}

📋 작업 우선순위 제안

  1. Import 누락 수정 (5분이면 끝!)
  2. 폼 name 속성 추가 (기능 동작을 위해 필수)
  3. required 속성 추가 (사용자 경험 개선)
  4. URL 구조 개선
  5. 실시간 좋아요/댓글 업데이트
  6. 에러 처리 강화
  7. 보안 강화 (CSRF, 권한 체크)
  8. 로딩 상태 표시
  9. 테스트 코드 작성

💡 개인적인 생각

전체적으로 기능 구현은 정말 잘 하신 것 같아요! 특히 백엔드 API 설계가 깔끔하네요 👍
저도 프로젝트를 하며 CRUD를 다뤄봤는데 잘 적용하신 것으로 보여집니다!
몇 가지 제안드린 것들은 "꼭 해야 하는 것"이라기보다는 "더 좋게 만들 수 있는 방향"입니다.
급하지 않으니까 천천히 하나씩 적용해보시면 될 것 같아요.

🤝 마무리

리뷰 외에 필요한게 있다면 함께 고민해보면 더 좋은 해결책이 나올 수 있을 거예요 😊

좋은 코드 작성하시느라 고생 많으셨어요! 🎉

@LimPark996 LimPark996 changed the title [COMMUNITY] Community 기능 추가 [COMM] Community 기능 추가 Jun 1, 2025
@LimPark996 LimPark996 changed the title [COMM] Community 기능 추가 [COMMUNITY] Community 기능 추가 Jun 1, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants