Skip to content

Conversation

@NCookies
Copy link
Collaborator

@NCookies NCookies commented Jul 7, 2025

작업 내용

  • 백엔드에서 구현한 알림, 커뮤니티 기능을 프론트에서도 연동되도록 했다.

Summary by CodeRabbit

  • 신규 기능

    • 헤더에 알림 벨 아이콘과 알림 모달 UI가 추가되어 실시간 알림 및 알림 목록 확인이 가능합니다.
    • 알림 페이지가 신설되어 전체 알림 내역과 페이지네이션, 실시간 알림 수신을 지원합니다.
    • 코드 제출 페이지에 탭 UI가 도입되어 '문제 설명'과 '토론글'을 구분해서 볼 수 있습니다.
    • 토론글(Discussion) 및 댓글(Reply)에 대해 작성자인지 여부를 표시하는 기능이 추가되었습니다.
    • 토론글 및 댓글에 대해 투표, 답글, 수정, 삭제 등 다양한 상호작용이 가능합니다.
  • 버그 수정

    • 삭제된 토론글과 댓글이 목록 및 카운트에서 제외되도록 개선되었습니다.
  • 스타일

    • 헤더, 알림, 채팅, 토론 UI 등 전반적인 CSS 스타일이 개선되고 일관성이 향상되었습니다.
  • 문서화

    • 여러 HTML 템플릿에 WebSocket 및 알림 관련 스크립트가 추가되었습니다.
  • 기타

    • 특정 댓글 구조에 대한 유효성 검사가 강화되어, 이미 부모가 있는 댓글에는 추가 답글을 달 수 없도록 처리되었습니다.
    • '/api/languages' 엔드포인트가 인증 없이 접근 가능하도록 변경되었습니다.

@NCookies NCookies self-assigned this Jul 7, 2025
@NCookies NCookies added the enhancement New feature or request label Jul 7, 2025
@coderabbitai
Copy link

coderabbitai bot commented Jul 7, 2025

"""

Walkthrough

이 변경사항은 실시간 알림 기능(웹소켓 기반)과 알림 UI를 헤더 및 별도 페이지에 추가하고, 토론글 및 댓글에 "작성자 여부"(isAuthor) 표시를 지원합니다. 또한, 토론/댓글 목록에서 삭제된 항목을 필터링하고, 댓글의 부모 중복 추가 방지 검증을 도입하였으며, 제출 페이지에 토론 탭과 관련 UI/JS 기능을 대폭 확장하였습니다.

Changes

파일/경로 요약 변경 내용 요약
.../dto/response/DiscussionResponse.java
.../dto/response/ReplyResponse.java
.../dto/DiscussionQueryResult.java
.../dto/ReplyQueryResult.java
토론글 및 댓글 DTO/쿼리결과에 isAuthor(작성자 여부) 필드 추가 및 생성자/팩토리 메서드 수정
.../service/ReplyDomainService.java
.../exception/CommunityExceptionCode.java
댓글의 부모 중복 방지 검증 메서드 및 예외코드 추가
.../repository/community/discussion/DiscussionQueryRepositoryImpl.java
.../repository/community/reply/ReplyQueryRepositoryImpl.java
토론/댓글 쿼리에서 삭제된 항목 제외 및 isAuthor 필드 쿼리 결과에 포함
.../config/SecurityConfig.java /api/languages GET 엔드포인트를 인증 없이 허용하도록 공개 경로 추가
.../view/ViewController.java 알림 페이지(/test/notifications) 뷰 핸들러 추가
.../static/html/header.html 알림 벨 아이콘, 알림 모달, 관련 CSS 및 마크업 추가, 코드 포맷 정리
.../static/js/headerNotification.js 웹소켓 기반 실시간 알림 수신 및 UI 렌더링, 알림 모달 관리 JS 신규 추가
.../static/js/headerUtils.js 헤더 삽입 후 알림 웹소켓 연결 및 이벤트 바인딩 자동 실행 추가
.../templates/search-page.html
.../templates/test-main.html
.../templates/test-mypage.html
.../templates/test-problems.html
.../templates/test-ranking.html
SockJS, STOMP.js(웹소켓용) 및 알림 관련 JS 파일 스크립트 추가, 일부 페이지에 footer 동적 로딩 추가
.../templates/test-notifications.html 실시간 알림 전체 목록 페이지 신규 추가(웹소켓/REST 연동, UI/JS 포함)
.../templates/test-submit.html 제출 페이지 좌측에 토론 탭 및 토론/댓글 UI, JS 기능 대폭 추가(토론 생성, 목록, 정렬, 투표, 댓글/대댓글, 편집/삭제 등)

Sequence Diagram(s)

sequenceDiagram
actor User
participant Header as "Header (알림 벨)"
participant WS as "WebSocket(STOMP)"
participant Server as "Backend"
participant Modal as "알림 모달 UI"

User->>Header: 알림 벨 클릭
Header->>Modal: 알림 모달 표시/숨김

Note over Header,WS: 페이지 로드 시
Header->>WS: WebSocket 연결 및 구독
WS-->>Header: 실시간 알림 수신
Header->>Modal: 알림 목록 갱신/뱃지 표시

User->>Modal: "더보기" 클릭
Modal->>Server: REST로 알림 추가 조회
Server-->>Modal: 알림 데이터 반환
Modal->>Modal: 알림 목록 렌더링
Loading
sequenceDiagram
actor User
participant SubmitPage as "제출 페이지"
participant Server as "Backend"

User->>SubmitPage: 토론 탭 클릭
SubmitPage->>Server: 토론글 목록 조회 API
Server-->>SubmitPage: 토론글+isAuthor 반환
SubmitPage->>User: 토론글 목록 렌더링

User->>SubmitPage: 댓글/대댓글/투표/편집/삭제 등
SubmitPage->>Server: 해당 API 호출
Server-->>SubmitPage: 처리 결과 반환
SubmitPage->>User: UI 갱신
Loading

Possibly related PRs

Suggested labels

documentation

Suggested reviewers

  • minjee2758
  • pokerbearkr
  • Kimminu7
  • chat26666

Poem

🐰
알림 벨이 반짝, 실시간 소식 도착!
토론과 댓글엔 작성자 뱃지 척척
삭제된 글은 안녕,
새 UI로 즐거움이 번쩍
코드 속에 토끼 춤추네,
EZCode 세상에 변화가 활짝!

"""


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.

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.

Actionable comments posted: 11

🧹 Nitpick comments (3)
src/main/resources/static/js/headerNotification.js (1)

3-5: 전역 변수 사용으로 인한 네임스페이스 오염 가능성이 있습니다.

전역 변수들이 다른 스크립트와 충돌할 수 있습니다. 네임스페이스 객체 내에서 관리하는 것을 권장합니다.

-let headerStompClient;
-let notifications = []; // 알림 목록 (최초 1페이지)
-let unreadCount = 0;    // 읽지 않은 알림 개수
+const HeaderNotification = {
+    stompClient: null,
+    notifications: [],
+    unreadCount: 0
+};
src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/community/discussion/DiscussionQueryRepositoryImpl.java (1)

44-46: 사용되지 않는 변수들을 제거하세요.

페이지네이션 관련 변수들(offset, pageNumber, pageSize)이 선언되었지만 실제로는 사용되지 않고 있습니다. 코드의 가독성을 위해 제거하는 것이 좋습니다.

-		long offset = pageable.getOffset();
-		int pageNumber = pageable.getPageNumber();
-		int pageSize = pageable.getPageSize();
-
src/main/resources/templates/test-notifications.html (1)

172-178: DOM 조작 시 메모리 누수 방지를 위한 개선이 필요합니다.

setTimeout을 사용한 애니메이션 클래스 제거 로직에서 요소가 이미 DOM에서 제거된 경우에 대한 체크가 없습니다.

            if (newlyAddedCount > 0) {
                setTimeout(() => {
                    document.querySelectorAll('.page-notification-item.newly-added').forEach(el => {
+                        if (el.parentNode) {
                            el.classList.remove('newly-added');
+                        }
                    });
                }, 1200);
            }
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 113458d and 7fb1cb9.

📒 Files selected for processing (20)
  • src/main/java/org/ezcode/codetest/application/community/dto/response/DiscussionResponse.java (3 hunks)
  • src/main/java/org/ezcode/codetest/application/community/dto/response/ReplyResponse.java (3 hunks)
  • src/main/java/org/ezcode/codetest/common/security/config/SecurityConfig.java (2 hunks)
  • src/main/java/org/ezcode/codetest/domain/community/dto/DiscussionQueryResult.java (3 hunks)
  • src/main/java/org/ezcode/codetest/domain/community/dto/ReplyQueryResult.java (3 hunks)
  • src/main/java/org/ezcode/codetest/domain/community/exception/CommunityExceptionCode.java (1 hunks)
  • src/main/java/org/ezcode/codetest/domain/community/service/ReplyDomainService.java (3 hunks)
  • src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/community/discussion/DiscussionQueryRepositoryImpl.java (5 hunks)
  • src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/community/reply/ReplyQueryRepositoryImpl.java (4 hunks)
  • src/main/java/org/ezcode/codetest/presentation/view/ViewController.java (1 hunks)
  • src/main/resources/static/html/header.html (5 hunks)
  • src/main/resources/static/js/headerNotification.js (1 hunks)
  • src/main/resources/static/js/headerUtils.js (1 hunks)
  • src/main/resources/templates/search-page.html (2 hunks)
  • src/main/resources/templates/test-main.html (2 hunks)
  • src/main/resources/templates/test-mypage.html (2 hunks)
  • src/main/resources/templates/test-notifications.html (1 hunks)
  • src/main/resources/templates/test-problems.html (2 hunks)
  • src/main/resources/templates/test-ranking.html (2 hunks)
  • src/main/resources/templates/test-submit.html (5 hunks)
🧰 Additional context used
🧠 Learnings (8)
📓 Common learnings
Learnt from: chat26666
PR: ezcode-my/backend#37
File: src/main/java/org/ezcode/codetest/infrastructure/elasticsearch/repository/ProblemElasticsearchRepositoryDsl.java:10-10
Timestamp: 2025-06-06T19:56:27.759Z
Learning: When user chat26666 mentions that code was implemented in a previous PR but explanation was missing, they may submit a follow-up PR just to add documentation/explanation without the actual implementation code.
src/main/java/org/ezcode/codetest/common/security/config/SecurityConfig.java (1)
Learnt from: NCookies
PR: ezcode-my/backend#17
File: src/main/java/org/ezcode/codetest/presentation/problemmanagement/ProblemAdminController.java:25-25
Timestamp: 2025-06-02T06:57:37.929Z
Learning: 이 프로젝트에서는 아직 Spring Security가 적용되지 않아서 @PreAuthorize 어노테이션이 주석 처리되어 있음. Spring Security 도입 전까지는 권한 검증 어노테이션들이 비활성화 상태임.
src/main/resources/templates/search-page.html (1)
Learnt from: chat26666
PR: ezcode-my/backend#31
File: src/main/resources/templates/search-page.html:198-233
Timestamp: 2025-06-04T15:14:15.092Z
Learning: src/main/resources/templates/search-page.html은 임시용 뷰로, 곧 폐기될 예정임
src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/community/reply/ReplyQueryRepositoryImpl.java (1)
Learnt from: chat26666
PR: ezcode-my/backend#64
File: src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/game/EncounterChoiceRepositoryImpl.java:0-0
Timestamp: 2025-06-15T04:37:29.231Z
Learning: EncounterChoiceRepositoryImpl in src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/game/EncounterChoiceRepositoryImpl.java is intentionally a skeleton implementation that is work-in-progress and will be completed later.
src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/community/discussion/DiscussionQueryRepositoryImpl.java (1)
Learnt from: chat26666
PR: ezcode-my/backend#64
File: src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/game/EncounterChoiceRepositoryImpl.java:0-0
Timestamp: 2025-06-15T04:37:29.231Z
Learning: EncounterChoiceRepositoryImpl in src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/game/EncounterChoiceRepositoryImpl.java is intentionally a skeleton implementation that is work-in-progress and will be completed later.
src/main/resources/templates/test-problems.html (2)
Learnt from: thezz9
PR: ezcode-my/backend#114
File: src/main/java/org/ezcode/codetest/infrastructure/github/GitHubApiClient.java:159-165
Timestamp: 2025-06-28T02:03:38.693Z
Learning: User thezz9's service (ezcode) targets Korean users exclusively ("찐 한국인밖에 안 옴"), so Korean language hardcoding in commit messages and UI elements is intentional and appropriate for their use case.
Learnt from: thezz9
PR: ezcode-my/backend#73
File: src/main/resources/templates/submit-test.html:135-135
Timestamp: 2025-06-18T11:41:58.457Z
Learning: src/main/resources/templates/submit-test.html 파일은 테스트용 파일로, isCorrect 값의 하드코딩은 의도적이며 사용자가 UI 요소를 통해 정답/오답 상태를 선택할 수 있도록 개선 예정입니다.
src/main/resources/templates/test-notifications.html (1)
Learnt from: thezz9
PR: ezcode-my/backend#73
File: src/main/resources/templates/submit-test.html:135-135
Timestamp: 2025-06-18T11:41:58.457Z
Learning: src/main/resources/templates/submit-test.html 파일은 테스트용 파일로, isCorrect 값의 하드코딩은 의도적이며 사용자가 UI 요소를 통해 정답/오답 상태를 선택할 수 있도록 개선 예정입니다.
src/main/resources/templates/test-submit.html (2)
Learnt from: thezz9
PR: ezcode-my/backend#73
File: src/main/resources/templates/submit-test.html:135-135
Timestamp: 2025-06-18T11:41:58.457Z
Learning: src/main/resources/templates/submit-test.html 파일은 테스트용 파일로, isCorrect 값의 하드코딩은 의도적이며 사용자가 UI 요소를 통해 정답/오답 상태를 선택할 수 있도록 개선 예정입니다.
Learnt from: thezz9
PR: ezcode-my/backend#114
File: src/main/java/org/ezcode/codetest/infrastructure/github/GitHubApiClient.java:159-165
Timestamp: 2025-06-28T02:03:38.693Z
Learning: User thezz9's service (ezcode) targets Korean users exclusively ("찐 한국인밖에 안 옴"), so Korean language hardcoding in commit messages and UI elements is intentional and appropriate for their use case.
🔇 Additional comments (40)
src/main/java/org/ezcode/codetest/domain/community/exception/CommunityExceptionCode.java (1)

18-18: 댓글 계층 구조 검증을 위한 적절한 예외 코드 추가

대댓글의 부모 댓글이 이미 존재하는 경우를 처리하는 예외 코드가 올바르게 추가되었습니다. 메시지가 명확하고 적절한 HTTP 상태 코드를 사용하여 기존 패턴과 일관성을 유지합니다.

src/main/java/org/ezcode/codetest/presentation/view/ViewController.java (1)

53-56: 알림 페이지를 위한 적절한 컨트롤러 메서드 추가

알림 페이지를 제공하는 새로운 엔드포인트가 기존 패턴과 일관성 있게 추가되었습니다. 단순한 뷰 반환 로직으로 적절합니다.

src/main/resources/static/js/headerUtils.js (1)

9-11: 알림 WebSocket 연결 초기화 로직 추가

헤더 로드 후 알림 관련 함수들을 안전하게 호출하는 로직이 추가되었습니다. 함수 존재 여부를 확인하여 오류를 방지하는 좋은 패턴입니다.

src/main/resources/templates/test-ranking.html (2)

9-10: 알림 기능을 위한 WebSocket 라이브러리 추가

실시간 알림 기능을 위한 SockJS와 STOMP 라이브러리가 추가되었습니다. CDN에서 라이브러리를 로드하는 일반적인 방법을 사용합니다.


219-219: 알림 처리 스크립트 추가

헤더 알림 기능을 위한 JavaScript 파일이 추가되었습니다. 다른 페이지들과 일관성 있는 패턴으로 추가되었습니다.

src/main/java/org/ezcode/codetest/common/security/config/SecurityConfig.java (2)

40-40: 코드 정리 및 불필요한 빈 줄 제거

코드 가독성을 위한 적절한 정리 작업입니다.


81-81: 언어 API 공개 엔드포인트 추가

/api/languages 엔드포인트를 공개 GET 요청으로 추가한 것은 적절합니다. 언어 목록은 일반적으로 인증 없이도 접근 가능해야 하는 리소스입니다.

src/main/resources/templates/test-main.html (2)

10-11: WebSocket 라이브러리 추가 승인

실시간 알림 기능을 위한 SockJS와 STOMP.js 라이브러리가 적절히 추가되었습니다. 버전이 고정되어 있어 안정성이 보장됩니다.


32-32: 알림 기능 스크립트 추가 승인

헤더 알림 기능을 위한 스크립트가 적절한 위치에 추가되었습니다. 다른 템플릿들과 일관성을 유지하고 있습니다.

src/main/resources/templates/search-page.html (2)

8-9: WebSocket 라이브러리 추가 승인

실시간 알림 기능을 위한 SockJS와 STOMP.js 라이브러리가 추가되었습니다. 다만 이 파일은 임시용으로 곧 폐기될 예정이므로 우선순위가 낮습니다.


313-314: 알림 및 푸터 기능 스크립트 추가 승인

헤더 알림 기능과 푸터 로드 기능이 적절히 추가되었습니다. 다른 템플릿들과 일관성을 유지하고 있습니다.

Also applies to: 317-317

src/main/java/org/ezcode/codetest/domain/community/service/ReplyDomainService.java (2)

32-32: 댓글 중복 부모 검증 로직 추가 승인

대댓글의 대댓글을 방지하는 검증 로직이 적절히 추가되었습니다. 댓글 계층 구조를 2단계로 제한하는 것은 UX 관점에서 합리적입니다.


110-115: 검증 메서드 구현 승인

checkNoExistingParentForReply 메서드가 명확하고 간결하게 구현되었습니다. 예외 처리도 적절하며 메서드명이 의도를 명확히 나타냅니다.

src/main/resources/templates/test-problems.html (3)

11-12: WebSocket 라이브러리 추가 승인

실시간 알림 기능을 위한 SockJS와 STOMP.js 라이브러리가 적절히 추가되었습니다. 다른 템플릿들과 일관성을 유지하고 있습니다.


481-481: 알림 기능 스크립트 추가 승인

헤더 알림 기능을 위한 스크립트가 적절한 위치에 추가되었습니다. 전체적인 알림 시스템 구현에 일관성을 보여줍니다.


5-5: 코드 포맷팅 개선 승인

메타 태그와 Google Fonts 링크의 포맷팅이 개선되어 가독성이 향상되었습니다.

Also applies to: 9-10

src/main/resources/templates/test-mypage.html (2)

10-11: WebSocket 라이브러리 추가 승인

실시간 알림 기능을 위한 SockJS와 STOMP.js 라이브러리가 적절히 추가되었습니다. 전체 애플리케이션의 알림 시스템 일관성을 유지하고 있습니다.


82-83: 알림 및 푸터 기능 스크립트 추가 승인

헤더 알림 기능과 푸터 로드 기능이 적절히 추가되었습니다. 마이페이지의 사용자 경험이 향상될 것으로 예상됩니다.

Also applies to: 86-86

src/main/java/org/ezcode/codetest/application/community/dto/response/DiscussionResponse.java (3)

42-46: 작성자 여부 필드 추가가 적절하게 구현되었습니다.

isAuthor 필드가 적절한 Swagger 문서화와 함께 추가되었고, 기존 구조와 일관성 있게 구현되었습니다.


49-62: 팩토리 메서드 업데이트가 올바르게 구현되었습니다.

fromEntity 메서드에서 isAuthorfalse로 기본값 설정하는 것이 합리적입니다.


64-77: 쿼리 결과 기반 팩토리 메서드가 올바르게 구현되었습니다.

DiscussionQueryResult에서 isAuthor() 메서드를 통해 값을 가져오는 것이 적절합니다.

src/main/java/org/ezcode/codetest/domain/community/dto/DiscussionQueryResult.java (3)

33-33: 작성자 여부 필드가 올바르게 추가되었습니다.

boolean isAuthor 필드가 적절히 선언되었습니다.


45-45: 생성자 파라미터 추가가 올바르게 구현되었습니다.

isAuthor 파라미터가 기존 파라미터들과 일관성 있게 추가되었습니다.


65-65: 필드 할당이 올바르게 구현되었습니다.

생성자에서 isAuthor 필드에 파라미터 값을 올바르게 할당하고 있습니다.

src/main/java/org/ezcode/codetest/domain/community/dto/ReplyQueryResult.java (3)

35-35: 작성자 여부 필드가 일관성 있게 추가되었습니다.

DiscussionQueryResult와 동일한 패턴으로 isAuthor 필드가 추가되어 일관성이 유지되었습니다.


48-50: 생성자 파라미터 업데이트가 올바르게 구현되었습니다.

isAuthor 파라미터가 적절한 위치에 추가되었습니다.


70-70: 필드 할당이 올바르게 구현되었습니다.

생성자에서 isAuthor 필드 할당이 올바르게 처리되었습니다.

src/main/java/org/ezcode/codetest/application/community/dto/response/ReplyResponse.java (3)

47-48: 작성자 여부 필드가 적절하게 추가되었습니다.

DiscussionResponse와 동일한 패턴으로 isAuthor 필드가 추가되어 일관성이 유지되었습니다.


51-70: 팩토리 메서드 업데이트가 올바르게 구현되었습니다.

fromEntity 메서드에서 isAuthorfalse로 기본값 설정하는 것이 합리적입니다.


72-87: 쿼리 결과 기반 팩토리 메서드가 올바르게 구현되었습니다.

ReplyQueryResult에서 isAuthor() 메서드를 통해 값을 가져오는 방식이 적절합니다.

src/main/resources/static/js/headerNotification.js (2)

65-74: 시간 포맷팅 함수가 올바르게 구현되었습니다.

날짜 파싱과 상대 시간 계산이 적절히 구현되었습니다.


76-92: 이벤트 리스너 바인딩이 적절하게 구현되었습니다.

클릭 이벤트 처리와 모달 토글 로직이 올바르게 구현되었습니다.

src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/community/discussion/DiscussionQueryRepositoryImpl.java (2)

52-52: 논리적 삭제 필터링이 적절히 구현되었습니다.

isDeleted.isFalse() 조건을 통해 삭제된 토론글을 올바르게 필터링하고 있으며, 목록 조회와 카운트 쿼리 모두에 일관되게 적용되어 있습니다.

Also applies to: 126-126


73-73: 작성자 여부 판단 로직이 올바르게 구현되었습니다.

현재 사용자 ID와 토론글 작성자 ID를 비교하여 isAuthor 값을 설정하는 로직이 적절하며, null 체크도 포함되어 있어 안전합니다.

Also applies to: 78-78, 81-81, 99-100

src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/community/reply/ReplyQueryRepositoryImpl.java (2)

69-69: 작성자 여부 판단 로직이 일관되게 잘 구현되었습니다.

토론글 repository와 동일한 패턴으로 isAuthor 로직이 구현되어 있으며, null 체크와 함께 안전하게 처리되고 있습니다. projection에도 올바르게 포함되어 있습니다.

Also applies to: 74-74, 77-77, 96-97


103-103: 논리적 삭제 필터링이 모든 관련 쿼리에 일관되게 적용되었습니다.

댓글 조회, 토론별 댓글 카운트, 부모 댓글별 카운트 쿼리 모두에 isDeleted.isFalse() 조건이 추가되어 삭제된 댓글이 올바르게 제외됩니다.

Also applies to: 116-116, 125-125

src/main/resources/static/html/header.html (2)

75-93: 알림 UI 스타일링이 잘 구현되었습니다.

알림 버튼의 읽지 않은 상태 표시, 모달 스타일링, 호버 효과 등이 일관된 디자인 시스템으로 잘 구현되어 있습니다. 접근성과 사용자 경험을 고려한 스타일입니다.

Also applies to: 95-151


163-170: 알림 기능 HTML 구조가 적절합니다.

SVG 아이콘을 사용한 알림 버튼과 모달 구조가 시맨틱하고 접근 가능하게 구현되어 있습니다. ID와 클래스명도 명확하게 명명되었습니다.

Also applies to: 253-256

src/main/resources/templates/test-submit.html (2)

275-736: CSS 스타일이 포괄적으로 잘 구현되었습니다.

탭 인터페이스, 토론 목록, 투표 버튼, 페이지네이션 등 모든 UI 요소에 대한 스타일이 일관된 디자인 시스템으로 구현되어 있습니다. 호버 효과와 상태 변화도 적절히 처리되었습니다.


744-771: 탭 기반 UI 구조가 적절히 구현되었습니다.

문제 설명과 토론글을 분리한 탭 구조가 사용자 경험을 개선하며, 데이터 속성을 활용한 탭 연결도 깔끔하게 구현되었습니다.

Comment on lines +30 to +35
// 최초 알림 목록 요청 (REST API)
fetch('/api/notifications?page=0', {
headers: { 'Authorization': sessionStorage.getItem('accessToken') }
});
}, err => console.error('[STOMP] 연결 오류:', err));
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

REST API 호출에 오류 처리가 누락되었습니다.

네트워크 오류나 서버 오류에 대한 처리가 없습니다.

-        fetch('/api/notifications?page=0', {
-            headers: { 'Authorization': sessionStorage.getItem('accessToken') }
-        });
+        fetch('/api/notifications?page=0', {
+            headers: { 'Authorization': sessionStorage.getItem('accessToken') }
+        }).catch(error => {
+            console.error('알림 목록 조회 오류:', 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.

Suggested change
// 최초 알림 목록 요청 (REST API)
fetch('/api/notifications?page=0', {
headers: { 'Authorization': sessionStorage.getItem('accessToken') }
});
}, err => console.error('[STOMP] 연결 오류:', err));
}
// 최초 알림 목록 요청 (REST API)
fetch('/api/notifications?page=0', {
headers: { 'Authorization': sessionStorage.getItem('accessToken') }
}).catch(error => {
console.error('알림 목록 조회 오류:', error);
});
}, err => console.error('[STOMP] 연결 오류:', err));
}
🤖 Prompt for AI Agents
In src/main/resources/static/js/headerNotification.js around lines 30 to 35, the
fetch call to the REST API lacks error handling for network or server errors.
Add a .then() block to check the response status and handle non-OK responses
appropriately, and include a .catch() block to catch and log any fetch errors.
This will ensure that errors during the API call are properly detected and
handled.

Comment on lines +23 to +28
headerStompClient.subscribe('/user/queue/notifications', msg => {
const data = JSON.parse(msg.body);
notifications = data.content || [];
unreadCount = notifications.filter(n => !n.isRead).length;
updateNotificationUI();
});
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

알림 목록 구독에서도 오류 처리가 필요합니다.

JSON 파싱 실패와 데이터 구조 검증이 누락되었습니다.

-        headerStompClient.subscribe('/user/queue/notifications', msg => {
-            const data = JSON.parse(msg.body);
-            notifications = data.content || [];
-            unreadCount = notifications.filter(n => !n.isRead).length;
-            updateNotificationUI();
-        });
+        headerStompClient.subscribe('/user/queue/notifications', msg => {
+            try {
+                const data = JSON.parse(msg.body);
+                if (data && Array.isArray(data.content)) {
+                    notifications = data.content;
+                    unreadCount = notifications.filter(n => !n.isRead).length;
+                    updateNotificationUI();
+                }
+            } catch (error) {
+                console.error('알림 목록 파싱 오류:', 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.

Suggested change
headerStompClient.subscribe('/user/queue/notifications', msg => {
const data = JSON.parse(msg.body);
notifications = data.content || [];
unreadCount = notifications.filter(n => !n.isRead).length;
updateNotificationUI();
});
headerStompClient.subscribe('/user/queue/notifications', msg => {
try {
const data = JSON.parse(msg.body);
if (data && Array.isArray(data.content)) {
notifications = data.content;
unreadCount = notifications.filter(n => !n.isRead).length;
updateNotificationUI();
}
} catch (error) {
console.error('알림 목록 파싱 오류:', error);
}
});
🤖 Prompt for AI Agents
In src/main/resources/static/js/headerNotification.js around lines 23 to 28, the
subscription callback lacks error handling for JSON parsing and data structure
validation. Wrap the JSON.parse call in a try-catch block to handle parsing
errors gracefully. After parsing, verify that the data object has the expected
structure before accessing its properties to prevent runtime errors. Add
appropriate fallback or error logging if the data is invalid.

Comment on lines +15 to +20
headerStompClient.subscribe('/user/queue/notification', msg => {
const notif = JSON.parse(msg.body);
notifications.unshift(notif); // 새 알림 맨 앞에 추가
if (!notif.isRead) unreadCount++;
updateNotificationUI();
});
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

메시지 파싱 중 오류 처리가 누락되었습니다.

JSON 파싱 실패나 잘못된 메시지 구조로 인한 오류가 발생할 수 있습니다.

-        headerStompClient.subscribe('/user/queue/notification', msg => {
-            const notif = JSON.parse(msg.body);
-            notifications.unshift(notif); // 새 알림 맨 앞에 추가
-            if (!notif.isRead) unreadCount++;
-            updateNotificationUI();
-        });
+        headerStompClient.subscribe('/user/queue/notification', msg => {
+            try {
+                const notif = JSON.parse(msg.body);
+                if (notif && typeof notif === 'object') {
+                    notifications.unshift(notif);
+                    if (!notif.isRead) unreadCount++;
+                    updateNotificationUI();
+                }
+            } catch (error) {
+                console.error('알림 메시지 파싱 오류:', 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.

Suggested change
headerStompClient.subscribe('/user/queue/notification', msg => {
const notif = JSON.parse(msg.body);
notifications.unshift(notif); // 새 알림 맨 앞에 추가
if (!notif.isRead) unreadCount++;
updateNotificationUI();
});
headerStompClient.subscribe('/user/queue/notification', msg => {
try {
const notif = JSON.parse(msg.body);
if (notif && typeof notif === 'object') {
notifications.unshift(notif); // 새 알림 맨 앞에 추가
if (!notif.isRead) unreadCount++;
updateNotificationUI();
}
} catch (error) {
console.error('알림 메시지 파싱 오류:', error);
}
});
🤖 Prompt for AI Agents
In src/main/resources/static/js/headerNotification.js around lines 15 to 20, the
JSON.parse call on msg.body lacks error handling, which can cause runtime errors
if the message is malformed. Wrap the JSON.parse call in a try-catch block to
catch parsing errors, and handle them gracefully, such as logging the error and
skipping the faulty message to prevent the application from crashing.

Comment on lines +57 to +62
listEl.innerHTML = notifications.map(n => `
<div class="notification-item${n.isRead ? '' : ' unread'}">
<div class="message">${n.message}</div>
<div class="time">${formatTime(n.createdAt)}</div>
</div>
`).join('');
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

XSS 취약점이 존재합니다.

innerHTML을 사용하여 알림 메시지를 직접 삽입하고 있어 XSS 공격에 취약합니다.

-    listEl.innerHTML = notifications.map(n => `
-        <div class="notification-item${n.isRead ? '' : ' unread'}">
-            <div class="message">${n.message}</div>
-            <div class="time">${formatTime(n.createdAt)}</div>
-        </div>
-    `).join('');
+    listEl.innerHTML = notifications.map(n => {
+        const div = document.createElement('div');
+        div.className = `notification-item${n.isRead ? '' : ' unread'}`;
+        
+        const messageDiv = document.createElement('div');
+        messageDiv.className = 'message';
+        messageDiv.textContent = n.message; // XSS 방지
+        
+        const timeDiv = document.createElement('div');
+        timeDiv.className = 'time';
+        timeDiv.textContent = formatTime(n.createdAt);
+        
+        div.appendChild(messageDiv);
+        div.appendChild(timeDiv);
+        return div.outerHTML;
+    }).join('');
📝 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.

Suggested change
listEl.innerHTML = notifications.map(n => `
<div class="notification-item${n.isRead ? '' : ' unread'}">
<div class="message">${n.message}</div>
<div class="time">${formatTime(n.createdAt)}</div>
</div>
`).join('');
listEl.innerHTML = notifications.map(n => {
const div = document.createElement('div');
div.className = `notification-item${n.isRead ? '' : ' unread'}`;
const messageDiv = document.createElement('div');
messageDiv.className = 'message';
messageDiv.textContent = n.message; // XSS 방지
const timeDiv = document.createElement('div');
timeDiv.className = 'time';
timeDiv.textContent = formatTime(n.createdAt);
div.appendChild(messageDiv);
div.appendChild(timeDiv);
return div.outerHTML;
}).join('');
🤖 Prompt for AI Agents
In src/main/resources/static/js/headerNotification.js around lines 57 to 62, the
code uses innerHTML to insert notification messages directly, which exposes an
XSS vulnerability. To fix this, avoid using innerHTML for inserting untrusted
content; instead, create DOM elements programmatically and set text content
using textContent or similar safe methods to ensure messages are properly
escaped and not interpreted as HTML.

Comment on lines +7 to +10
window.connectNotificationWS = function() {
const tokenOnly = (sessionStorage.getItem('accessToken') || '').replace(/^Bearer /, '');
if (!tokenOnly) return;
const socket = new SockJS(`/ws?token=${encodeURIComponent(tokenOnly)}`);
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

토큰 추출 및 검증 로직을 개선해야 합니다.

토큰 존재 여부만 확인하고 있으나, 토큰 형식이나 만료 여부에 대한 검증이 부족합니다.

-    const tokenOnly = (sessionStorage.getItem('accessToken') || '').replace(/^Bearer /, '');
-    if (!tokenOnly) return;
+    const token = sessionStorage.getItem('accessToken');
+    if (!token || token.trim() === '') {
+        console.warn('알림 서비스: 액세스 토큰이 없습니다.');
+        return;
+    }
+    const tokenOnly = token.replace(/^Bearer /, '');
📝 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.

Suggested change
window.connectNotificationWS = function() {
const tokenOnly = (sessionStorage.getItem('accessToken') || '').replace(/^Bearer /, '');
if (!tokenOnly) return;
const socket = new SockJS(`/ws?token=${encodeURIComponent(tokenOnly)}`);
window.connectNotificationWS = function() {
const token = sessionStorage.getItem('accessToken');
if (!token || token.trim() === '') {
console.warn('알림 서비스: 액세스 토큰이 없습니다.');
return;
}
const tokenOnly = token.replace(/^Bearer /, '');
const socket = new SockJS(`/ws?token=${encodeURIComponent(tokenOnly)}`);
🤖 Prompt for AI Agents
In src/main/resources/static/js/headerNotification.js around lines 7 to 10, the
current code only checks if the token exists but does not validate its format or
expiration. Enhance the token validation by verifying the token structure (e.g.,
JWT format) and checking its expiration before proceeding. Implement a function
to decode and validate the token payload and ensure the token is still valid
before establishing the WebSocket connection.

Comment on lines +119 to +156
function connectPageNotificationWS() {
const tokenOnly = (sessionStorage.getItem('accessToken') || '').replace(/^Bearer /, '');
if (!tokenOnly) return;
const socket = new SockJS(`/ws?token=${encodeURIComponent(tokenOnly)}`);
pageStompClient = Stomp.over(socket);

pageStompClient.connect({}, () => {
pageStompClient.subscribe('/user/queue/notifications', msg => {
const data = JSON.parse(msg.body);
pageData = {
page: data.page ?? 0,
size: data.size ?? 10,
totalElements: data.totalElements ?? 0,
totalPages: data.totalPages ?? Math.ceil((data.totalElements ?? 0) / (data.size ?? 10))
};
let newlyAddedCount = 0;
if (pageData.page === 0) {
allNotifications = data.content || [];
newlyAddedCount = allNotifications.length;
} else {
newlyAddedCount = (data.content || []).length;
allNotifications = allNotifications.concat(data.content || []);
}
renderNotificationList(allNotifications, newlyAddedCount);
updateMoreBtn();
isLoading = false;
});
fetchNotifications(0);
});
}

function fetchNotifications(page) {
if (isLoading) return;
isLoading = true;
fetch(`/api/notifications?page=${page}`, {
headers: { 'Authorization': sessionStorage.getItem('accessToken') }
});
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

WebSocket 연결 및 API 호출에서 에러 핸들링이 부족합니다.

WebSocket 연결 실패나 API 호출 실패에 대한 에러 핸들링이 없어서 사용자에게 적절한 피드백을 제공하지 못할 수 있습니다.

에러 핸들링을 추가하세요:

        pageStompClient.connect({}, () => {
            // success callback
+        }, (error) => {
+            console.error('WebSocket 연결 실패:', error);
+            // 사용자에게 알림 표시 로직 추가
        });
        
        function fetchNotifications(page) {
            if (isLoading) return;
            isLoading = true;
-            fetch(`/api/notifications?page=${page}`, {
+            fetch(`/api/notifications?page=${page}`, {
                headers: { 'Authorization': sessionStorage.getItem('accessToken') }
-            });
+            }).catch(error => {
+                console.error('알림 조회 실패:', error);
+                isLoading = false;
+                // 에러 메시지 표시
+            });
        }
📝 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.

Suggested change
function connectPageNotificationWS() {
const tokenOnly = (sessionStorage.getItem('accessToken') || '').replace(/^Bearer /, '');
if (!tokenOnly) return;
const socket = new SockJS(`/ws?token=${encodeURIComponent(tokenOnly)}`);
pageStompClient = Stomp.over(socket);
pageStompClient.connect({}, () => {
pageStompClient.subscribe('/user/queue/notifications', msg => {
const data = JSON.parse(msg.body);
pageData = {
page: data.page ?? 0,
size: data.size ?? 10,
totalElements: data.totalElements ?? 0,
totalPages: data.totalPages ?? Math.ceil((data.totalElements ?? 0) / (data.size ?? 10))
};
let newlyAddedCount = 0;
if (pageData.page === 0) {
allNotifications = data.content || [];
newlyAddedCount = allNotifications.length;
} else {
newlyAddedCount = (data.content || []).length;
allNotifications = allNotifications.concat(data.content || []);
}
renderNotificationList(allNotifications, newlyAddedCount);
updateMoreBtn();
isLoading = false;
});
fetchNotifications(0);
});
}
function fetchNotifications(page) {
if (isLoading) return;
isLoading = true;
fetch(`/api/notifications?page=${page}`, {
headers: { 'Authorization': sessionStorage.getItem('accessToken') }
});
}
function connectPageNotificationWS() {
const tokenOnly = (sessionStorage.getItem('accessToken') || '').replace(/^Bearer /, '');
if (!tokenOnly) return;
const socket = new SockJS(`/ws?token=${encodeURIComponent(tokenOnly)}`);
pageStompClient = Stomp.over(socket);
pageStompClient.connect({}, () => {
pageStompClient.subscribe('/user/queue/notifications', msg => {
const data = JSON.parse(msg.body);
pageData = {
page: data.page ?? 0,
size: data.size ?? 10,
totalElements: data.totalElements ?? 0,
totalPages: data.totalPages ?? Math.ceil((data.totalElements ?? 0) / (data.size ?? 10))
};
let newlyAddedCount = 0;
if (pageData.page === 0) {
allNotifications = data.content || [];
newlyAddedCount = allNotifications.length;
} else {
newlyAddedCount = (data.content || []).length;
allNotifications = allNotifications.concat(data.content || []);
}
renderNotificationList(allNotifications, newlyAddedCount);
updateMoreBtn();
isLoading = false;
});
fetchNotifications(0);
}, (error) => {
console.error('WebSocket 연결 실패:', error);
// 사용자에게 알림 표시 로직 추가
});
}
function fetchNotifications(page) {
if (isLoading) return;
isLoading = true;
fetch(`/api/notifications?page=${page}`, {
headers: { 'Authorization': sessionStorage.getItem('accessToken') }
}).catch(error => {
console.error('알림 조회 실패:', error);
isLoading = false;
// 에러 메시지 표시
});
}
🤖 Prompt for AI Agents
In src/main/resources/templates/test-notifications.html between lines 119 and
156, the WebSocket connection and fetch API calls lack error handling, which can
lead to silent failures and poor user feedback. Add error callbacks to the
WebSocket connection to handle connection failures and subscription errors, and
include catch blocks or then handlers on the fetchNotifications API call to
handle HTTP errors or network issues. Provide appropriate user feedback or
logging in these error handlers to improve robustness.

Comment on lines 1355 to 1389
box.querySelector('.edit-save-btn').onclick = async () => {
const newContent = box.querySelector('.edit-input-text').value.trim();
if (!newContent) return alert('내용을 입력하세요.');
let url, method = 'PUT', payload = { languageId: 1, content: newContent };
if (item.classList.contains('discussion-item')) {
url = `/api/problems/${window.problemId}/discussions/${e.target.dataset.id}`;
} else {
const discussionId = item.closest('.discussion-item').dataset.discussionId;
url = `/api/problems/${window.problemId}/discussions/${discussionId}/replies/${e.target.dataset.id}`;
}
const res = await fetch(url, {
method,
headers: {
'Content-Type': 'application/json',
...tokenHeader()
},
body: JSON.stringify(payload)
});
if (res.ok) {
// 성공: 목록 새로고침
if (item.classList.contains('discussion-item')) {
loadDiscussions(window.problemId, 0, 10, document.getElementById('discussion-sort')?.value || 'best');
} else {
const discussionId = item.closest('.discussion-item').dataset.discussionId;
const container = document.getElementById(`replies-for-${discussionId}`);
if (container) {
container.dataset.loaded = '';
const replies = await fetchReplies(discussionId);
renderReplies(container, replies, discussionId, 0);
}
}
} else {
alert('수정에 실패했습니다.');
}
};
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

수정/삭제 기능에서 에러 핸들링과 사용자 피드백이 부족합니다.

API 호출 실패 시 단순히 alert만 표시하고 있으며, 로딩 상태나 상세한 에러 정보를 제공하지 않습니다.

더 나은 에러 핸들링과 사용자 피드백을 추가하세요:

                const res = await fetch(url, {
                    method,
                    headers: {
                        'Content-Type': 'application/json',
                        ...tokenHeader()
                    },
                    body: JSON.stringify(payload)
                });
-                if (res.ok) {
+                if (res.ok) {
                    // 성공 로직
+                } else {
+                    const errorData = await res.json();
+                    const message = errorData.message || '수정에 실패했습니다.';
+                    alert(message);
                } else {
-                    alert('수정에 실패했습니다.');
                }

Also applies to: 1402-1425

🤖 Prompt for AI Agents
In src/main/resources/templates/test-submit.html around lines 1355 to 1389 and
similarly at 1402 to 1425, the API call error handling only shows a simple alert
without detailed error info or loading state feedback. Enhance the code by
adding a loading indicator before the fetch call and removing it after
completion. Catch fetch errors explicitly and display detailed error messages to
the user. Also, disable the save button during the operation to prevent
duplicate submissions and re-enable it afterward.

Comment on lines 1284 to 1426
document.addEventListener('click', async function(e) {
// 댓글/대댓글 추천/비추천 (reply-item 먼저!)
if (e.target.closest('.upvote-btn') && e.target.closest('.reply-item')) {
const btn = e.target.closest('.upvote-btn');
const replyId = btn.dataset.id;
const discussionId = btn.closest('.discussion-item').dataset.discussionId;
const voteStatus = btn.dataset.votestatus;
await voteReply(discussionId, replyId, voteStatus === 'UP' ? 'NONE' : 'UP');
return;
}
if (e.target.closest('.downvote-btn') && e.target.closest('.reply-item')) {
const btn = e.target.closest('.downvote-btn');
const replyId = btn.dataset.id;
const discussionId = btn.closest('.discussion-item').dataset.discussionId;
const voteStatus = btn.dataset.votestatus;
await voteReply(discussionId, replyId, voteStatus === 'DOWN' ? 'NONE' : 'DOWN');
return;
}
// 토론글 추천/비추천 (discussion-item는 reply-item보다 나중에 체크)
if (e.target.closest('.upvote-btn') && e.target.closest('.discussion-item')) {
const btn = e.target.closest('.upvote-btn');
const discussionId = btn.dataset.id;
const voteStatus = btn.dataset.votestatus;
await voteDiscussion(discussionId, voteStatus === 'UP' ? 'NONE' : 'UP');
return;
}
if (e.target.closest('.downvote-btn') && e.target.closest('.discussion-item')) {
const btn = e.target.closest('.downvote-btn');
const discussionId = btn.dataset.id;
const voteStatus = btn.dataset.votestatus;
await voteDiscussion(discussionId, voteStatus === 'DOWN' ? 'NONE' : 'DOWN');
return;
}
// 댓글 달기
if (e.target.classList.contains('reply-btn') && !e.target.closest('.reply-item')) {
const discussionId = e.target.dataset.id;
showReplyInput(discussionId, null, e.target);
}
// 대댓글 달기
if (e.target.classList.contains('reply-btn') && e.target.closest('.reply-item')) {
const replyId = e.target.dataset.id;
const discussionId = e.target.closest('.discussion-item').dataset.discussionId;
showReplyInput(discussionId, replyId, e.target);
}
// 토론글/댓글/대댓글 수정
if (e.target.classList.contains('edit-btn')) {
const item = e.target.closest('.discussion-item, .reply-item');
const contentEl = item.querySelector('.discussion-content');
const oldContent = contentEl.textContent;
// 이미 수정창이 열려있으면 return
if (item.querySelector('.edit-input-box')) return;
// textarea + 취소/저장 버튼
const box = document.createElement('div');
box.className = 'edit-input-box';
box.innerHTML = `
<textarea class="edit-input-text" style="width:100%;min-height:60px;">${oldContent}</textarea>
<div style="display:flex;gap:8px;justify-content:flex-end;">
<button class="edit-cancel-btn">취소</button>
<button class="edit-save-btn">저장</button>
</div>
`;
contentEl.style.display = 'none';
contentEl.after(box);

// 취소
box.querySelector('.edit-cancel-btn').onclick = () => {
box.remove();
contentEl.style.display = '';
};

// 저장
box.querySelector('.edit-save-btn').onclick = async () => {
const newContent = box.querySelector('.edit-input-text').value.trim();
if (!newContent) return alert('내용을 입력하세요.');
let url, method = 'PUT', payload = { languageId: 1, content: newContent };
if (item.classList.contains('discussion-item')) {
url = `/api/problems/${window.problemId}/discussions/${e.target.dataset.id}`;
} else {
const discussionId = item.closest('.discussion-item').dataset.discussionId;
url = `/api/problems/${window.problemId}/discussions/${discussionId}/replies/${e.target.dataset.id}`;
}
const res = await fetch(url, {
method,
headers: {
'Content-Type': 'application/json',
...tokenHeader()
},
body: JSON.stringify(payload)
});
if (res.ok) {
// 성공: 목록 새로고침
if (item.classList.contains('discussion-item')) {
loadDiscussions(window.problemId, 0, 10, document.getElementById('discussion-sort')?.value || 'best');
} else {
const discussionId = item.closest('.discussion-item').dataset.discussionId;
const container = document.getElementById(`replies-for-${discussionId}`);
if (container) {
container.dataset.loaded = '';
const replies = await fetchReplies(discussionId);
renderReplies(container, replies, discussionId, 0);
}
}
} else {
alert('수정에 실패했습니다.');
}
};
}
// 삭제
if (e.target.classList.contains('delete-btn')) {
if (!confirm('정말 삭제하시겠습니까?')) return;
const item = e.target.closest('.discussion-item, .reply-item');
let url, method = 'DELETE';
if (item.classList.contains('discussion-item')) {
url = `/api/problems/${window.problemId}/discussions/${e.target.dataset.id}`;
} else {
const discussionId = item.closest('.discussion-item').dataset.discussionId;
url = `/api/problems/${window.problemId}/discussions/${discussionId}/replies/${e.target.dataset.id}`;
}
(async () => {
const res = await fetch(url, {
method,
headers: {
...tokenHeader()
}
});
if (res.ok) {
if (item.classList.contains('discussion-item')) {
loadDiscussions(window.problemId, 0, 10, document.getElementById('discussion-sort')?.value || 'best');
} else {
const discussionId = item.closest('.discussion-item').dataset.discussionId;
const container = document.getElementById(`replies-for-${discussionId}`);
if (container) {
container.dataset.loaded = '';
const replies = await fetchReplies(discussionId);
renderReplies(container, replies, discussionId, 0);
}
}
} else {
alert('삭제에 실패했습니다.');
}
})();
}
});
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

이벤트 위임에서 중복 코드와 복잡성이 문제입니다.

단일 이벤트 리스너에서 모든 상호작용을 처리하고 있어 코드가 매우 복잡하고 유지보수가 어렵습니다. 또한 비슷한 로직이 반복되고 있습니다.

기능별로 이벤트 핸들러를 분리하고 공통 로직을 함수로 추출하세요:

    // 별도의 이벤트 핸들러들로 분리
    function handleVoteClick(e) {
        // 투표 관련 로직만 처리
    }
    
    function handleReplyClick(e) {
        // 댓글 관련 로직만 처리
    }
    
    function handleEditDeleteClick(e) {
        // 수정/삭제 관련 로직만 처리
    }
    
    // 각각을 별도로 등록
    document.addEventListener('click', handleVoteClick);
    document.addEventListener('click', handleReplyClick);
    document.addEventListener('click', handleEditDeleteClick);

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

🤖 Prompt for AI Agents
In src/main/resources/templates/test-submit.html between lines 1284 and 1426,
the single event listener handling all click events causes code duplication and
complexity. Refactor by splitting the event handling into separate functions for
each feature (e.g., voting, replying, editing, deleting) and extract common
logic like fetching dataset IDs and updating UI into reusable helper functions.
Then, have the main event listener delegate to these smaller handlers based on
the event target, improving readability and maintainability.

Comment on lines 1079 to 1095
async function loadDiscussions(pid, page = 0, size = 10, sortBy) {
sortBy = sortBy || (window.currentDiscussionSortBy || 'best');
window.currentDiscussionSortBy = sortBy;
const res = await fetch(`/api/problems/${pid}/discussions?page=${page}&size=${size}&sortBy=${sortBy}`, {
headers: {
'Accept': 'application/json',
...tokenHeader()
}
});
const data = await res.json();
if (!data.success) {
document.getElementById('discussion-list').innerHTML = "토론글을 불러오지 못했습니다.";
return;
}
renderDiscussionList(data.result.content);
renderDiscussionPagination(data.result, pid);
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

API 응답 구조 검증이 부족합니다.

API 응답의 구조를 충분히 검증하지 않고 접근하고 있어 런타임 에러가 발생할 수 있습니다.

        const data = await res.json();
-        if (!data.success) {
+        if (!data || !data.success) {
            document.getElementById('discussion-list').innerHTML = "토론글을 불러오지 못했습니다.";
            return;
        }
+        if (!data.result || !Array.isArray(data.result.content)) {
+            document.getElementById('discussion-list').innerHTML = "토론글 데이터 형식이 올바르지 않습니다.";
+            return;
+        }
        renderDiscussionList(data.result.content);
📝 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.

Suggested change
async function loadDiscussions(pid, page = 0, size = 10, sortBy) {
sortBy = sortBy || (window.currentDiscussionSortBy || 'best');
window.currentDiscussionSortBy = sortBy;
const res = await fetch(`/api/problems/${pid}/discussions?page=${page}&size=${size}&sortBy=${sortBy}`, {
headers: {
'Accept': 'application/json',
...tokenHeader()
}
});
const data = await res.json();
if (!data.success) {
document.getElementById('discussion-list').innerHTML = "토론글을 불러오지 못했습니다.";
return;
}
renderDiscussionList(data.result.content);
renderDiscussionPagination(data.result, pid);
}
async function loadDiscussions(pid, page = 0, size = 10, sortBy) {
sortBy = sortBy || (window.currentDiscussionSortBy || 'best');
window.currentDiscussionSortBy = sortBy;
const res = await fetch(`/api/problems/${pid}/discussions?page=${page}&size=${size}&sortBy=${sortBy}`, {
headers: {
'Accept': 'application/json',
...tokenHeader()
}
});
const data = await res.json();
if (!data || !data.success) {
document.getElementById('discussion-list').innerHTML = "토론글을 불러오지 못했습니다.";
return;
}
if (!data.result || !Array.isArray(data.result.content)) {
document.getElementById('discussion-list').innerHTML = "토론글 데이터 형식이 올바르지 않습니다.";
return;
}
renderDiscussionList(data.result.content);
renderDiscussionPagination(data.result, pid);
}
🤖 Prompt for AI Agents
In src/main/resources/templates/test-submit.html around lines 1079 to 1095, the
code assumes the API response has a specific structure without verifying it,
which can cause runtime errors. Add checks to confirm that data, data.success,
data.result, and data.result.content exist and are of expected types before
accessing them. If any check fails, handle the error gracefully by showing an
appropriate message or fallback UI instead of proceeding with rendering.

Comment on lines 1508 to 1581
function showReplyInput(discussionId, parentReplyId, btnEl) {
// 이미 열려있으면 return
const next = btnEl.closest('.comment-actions').nextElementSibling;
if (next && next.classList.contains('reply-input-box')) return;

const box = document.createElement('div');
box.className = 'reply-input-box';
box.innerHTML = `
<textarea class="reply-input-text" rows="3" style="width:100%;resize:vertical;margin-bottom:6px;"></textarea>
<div style="display:flex;gap:8px;">
<button class="reply-cancel-btn">취소</button>
<button class="reply-submit-btn">작성</button>
</div>
`;
btnEl.closest('.comment-actions').after(box);

// 취소
box.querySelector('.reply-cancel-btn').onclick = () => box.remove();

// 작성
box.querySelector('.reply-submit-btn').onclick = async () => {
const content = box.querySelector('.reply-input-text').value.trim();
if (!content) return alert('내용을 입력하세요.');
const payload = parentReplyId ? { parentReplyId: Number(parentReplyId), content } : { content };
const token = sessionStorage.getItem('accessToken');
const res = await fetch(`/api/problems/${window.problemId}/discussions/${discussionId}/replies`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...tokenHeader()
},
body: JSON.stringify(payload)
});
if (res.ok) {
if (parentReplyId) {
// 대댓글: 해당 댓글의 대댓글만 새로고침
const childContainer = document.getElementById(`child-replies-for-${parentReplyId}`);
if (childContainer) {
childContainer.dataset.loaded = 'true';
const discussionId = btnEl.closest('.discussion-item').dataset.discussionId;
const childReplies = await fetchChildReplies(discussionId, parentReplyId);
renderReplies(childContainer, childReplies, discussionId, 1);
// 펼쳐진 상태로 유지
childContainer.style.display = 'block';
// 버튼 텍스트/상태 갱신
const toggleBtn = btnEl.closest('.reply-item').querySelector('.toggle-child-replies-btn');
if (toggleBtn) {
toggleBtn.textContent = '대댓글 접기';
toggleBtn.dataset.open = 'true';
}
}
} else {
// 댓글: 전체 댓글 새로고침
const container = document.getElementById(`replies-for-${discussionId}`);
if (container) {
container.dataset.loaded = 'true';
const replies = await fetchReplies(discussionId);
renderReplies(container, replies, discussionId, 0);
// 펼쳐진 상태로 유지
container.style.display = 'block';
// 버튼 텍스트/상태 갱신
const toggleBtn = btnEl.closest('.discussion-item').querySelector('.toggle-replies-btn');
if (toggleBtn) {
toggleBtn.textContent = '댓글 접기';
toggleBtn.dataset.open = 'true';
}
}
}
box.remove();
} else {
alert('작성에 실패했습니다.');
}
};
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

댓글 입력 기능에서 XSS 공격 가능성이 있습니다.

사용자 입력을 그대로 DOM에 삽입하고 있어 XSS 공격에 취약할 수 있습니다. 또한 입력 검증이 단순합니다.

입력 검증과 XSS 방지를 강화하세요:

        box.querySelector('.reply-submit-btn').onclick = async () => {
            const content = box.querySelector('.reply-input-text').value.trim();
-            if (!content) return alert('내용을 입력하세요.');
+            if (!content) return alert('내용을 입력하세요.');
+            if (content.length > 1000) return alert('내용이 너무 깁니다. (최대 1000자)');
+            
+            // 서버에서 sanitization 처리되어야 하지만, 클라이언트에서도 기본적인 검증
+            const sanitizedContent = content.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '');

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

🤖 Prompt for AI Agents
In src/main/resources/templates/test-submit.html between lines 1508 and 1581,
the showReplyInput function inserts user input directly into the DOM without
proper sanitization, making it vulnerable to XSS attacks. To fix this, implement
input sanitization by escaping or removing potentially dangerous HTML tags or
characters from the user input before rendering it. Additionally, enhance input
validation to reject or sanitize inputs containing scripts or HTML elements that
could lead to XSS, ensuring only safe content is accepted and displayed.

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.

Actionable comments posted: 3

♻️ Duplicate comments (1)
src/main/resources/templates/test-submit.html (1)

1284-1426: 단일 이벤트 위임 블록이 과도하게 비대함

이 범위는 클릭 이벤트를 전부 처리하면서 조건 분기가 많아 가독성과 유지보수가 떨어집니다. 이전 리뷰(1079 라인 등)에서 이미 동일 지적을 드렸습니다.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7fb1cb9 and 51630e3.

📒 Files selected for processing (1)
  • src/main/resources/templates/test-submit.html (5 hunks)
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: chat26666
PR: ezcode-my/backend#37
File: src/main/java/org/ezcode/codetest/infrastructure/elasticsearch/repository/ProblemElasticsearchRepositoryDsl.java:10-10
Timestamp: 2025-06-06T19:56:27.759Z
Learning: When user chat26666 mentions that code was implemented in a previous PR but explanation was missing, they may submit a follow-up PR just to add documentation/explanation without the actual implementation code.
src/main/resources/templates/test-submit.html (2)
Learnt from: thezz9
PR: ezcode-my/backend#73
File: src/main/resources/templates/submit-test.html:135-135
Timestamp: 2025-06-18T11:41:58.457Z
Learning: src/main/resources/templates/submit-test.html 파일은 테스트용 파일로, isCorrect 값의 하드코딩은 의도적이며 사용자가 UI 요소를 통해 정답/오답 상태를 선택할 수 있도록 개선 예정입니다.
Learnt from: thezz9
PR: ezcode-my/backend#114
File: src/main/java/org/ezcode/codetest/infrastructure/github/GitHubApiClient.java:159-165
Timestamp: 2025-06-28T02:03:38.693Z
Learning: User thezz9's service (ezcode) targets Korean users exclusively ("찐 한국인밖에 안 옴"), so Korean language hardcoding in commit messages and UI elements is intentional and appropriate for their use case.
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build

Comment on lines 831 to 871
window.addEventListener('DOMContentLoaded', () => {
const pid = new URLSearchParams(location.search).get('problemId');
if (pid) {
window.problemId = pid;
loadProblem(pid);
}
});

document.addEventListener('DOMContentLoaded', () => {
const tabs = document.querySelectorAll('.tabbar button');
const contents = document.querySelectorAll('.left-content');

tabs.forEach(tab => {
tab.addEventListener('click', () => {
// 1. 모든 탭과 콘텐츠에서 'active' 클래스 제거
tabs.forEach(t => t.classList.remove('active'));
contents.forEach(c => c.classList.remove('active'));

// 2. 클릭된 탭에 'active' 클래스 추가
tab.classList.add('active');

// 3. 클릭된 탭과 연결된 콘텐츠를 찾아 'active' 클래스 추가
const targetContent = document.querySelector(tab.dataset.target);
if (targetContent) {
targetContent.classList.add('active');
}
});
});

document.getElementById('discussion-tab').addEventListener('click', () => {
if (window.problemId) loadDiscussions(window.problemId, 0, 10, document.getElementById('discussion-sort')?.value || 'best');
});

// 정렬 드롭다운 변경 시 토론글 새로고침
const sortEl = document.getElementById('discussion-sort');
if (sortEl) {
sortEl.addEventListener('change', (e) => {
if (window.problemId) loadDiscussions(window.problemId, 0, 10, e.target.value);
});
}
});
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

DOMContentLoaded 리스너가 두 번 등록됨 – 중복 초기화 우려

831838, 839871 두 블록 모두 DOMContentLoaded 에서 탭/문제/정렬 로직을 초기화합니다. 중복 등록으로 같은 코드가 두 번 실행되어 불필요한 API 호출, 이벤트 바인딩 중복, 상태 꼬임이 발생할 수 있습니다.

하나의 초기화 함수로 합치고 한 번만 호출하도록 수정하세요.

🤖 Prompt for AI Agents
In src/main/resources/templates/test-submit.html between lines 831 and 871,
there are two separate DOMContentLoaded event listeners initializing tabs,
problem loading, and sorting logic, causing duplicate event bindings and
redundant API calls. Combine the code from both listeners into a single
DOMContentLoaded event listener function that performs all initialization steps
once, ensuring event handlers and API calls are set up only one time to avoid
conflicts and inefficiencies.

Comment on lines 1219 to 1223
if (isOpen) {
childContainer.style.display = 'none';
this.textContent = '대댓글 접기';
this.dataset.open = 'false';
} else {
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

토글 버튼 텍스트 반대 방향으로 설정되는 버그

닫기 동작일 때도 '대댓글 접기' 를 설정하고 있습니다. 접힘 상태에서는 '대댓글 펼치기' 가 되어야 합니다.

-    childContainer.style.display = 'none';
-    this.textContent = '대댓글 접기';
+    childContainer.style.display = 'none';
+    this.textContent = '대댓글 펼치기';
📝 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.

Suggested change
if (isOpen) {
childContainer.style.display = 'none';
this.textContent = '대댓글 접기';
this.dataset.open = 'false';
} else {
if (isOpen) {
childContainer.style.display = 'none';
this.textContent = '대댓글 펼치기';
this.dataset.open = 'false';
} else {
🤖 Prompt for AI Agents
In src/main/resources/templates/test-submit.html around lines 1219 to 1223, the
toggle button text is incorrectly set to '대댓글 접기' even when closing the comment
thread. Update the code so that when isOpen is true (closing), set the
textContent to '대댓글 펼치기' instead of '대댓글 접기' to reflect the collapsed state
correctly.

Comment on lines +1105 to +1124
<div class="discussion-item" data-discussion-id="${d.discussionId}">
<div class="discussion-header">
<img src="${d.userInfo.profileImageUrl || DEFAULT_PROFILE_IMG}" alt="profile">
<span class="nickname">${d.userInfo.nickname}</span>
<span style="flex:1"></span>
${d.isAuthor ? `
<button class="edit-btn" data-id="${d.discussionId}">수정</button>
<button class="delete-btn" data-id="${d.discussionId}">삭제</button>
` : ''}
</div>
<div class="discussion-content">${d.content}</div>
<div class="comment-actions">
<button class="icon-btn upvote-btn" data-id="${d.discussionId}" data-votestatus="${d.voteStatus}">
${getUpvoteIcon(d.voteStatus === 'UP')}
</button>
<span class="vote-count">${d.upvoteCount}</span>
<button class="icon-btn downvote-btn" data-id="${d.discussionId}" data-votestatus="${d.voteStatus}">
${getDownvoteIcon(d.voteStatus === 'DOWN')}
</button>
<button class="reply-btn" data-id="${d.discussionId}">댓글 달기</button>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

사용자 입력을 innerHTML에 그대로 주입 – XSS 취약점 가능성

d.content 값(서버-측 Markdown 또는 사용자 입력)이 아무 필터 없이 innerHTML로 삽입됩니다. 악의적인 스크립트 태그가 포함될 경우 클라이언트에서 실행될 수 있습니다.

- <div class="discussion-content">${d.content}</div>
+ <div class="discussion-content">${DOMPurify.sanitize(d.content)}</div>

또는 서버에서 HTML escape 후 textContent 로 렌더링하세요.
외부 라이브러리를 쓰지 못한다면 최소한 태그 제거 정규식이라도 적용해야 합니다.

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

🤖 Prompt for AI Agents
In src/main/resources/templates/test-submit.html around lines 1105 to 1124, the
user input d.content is directly injected into the HTML using innerHTML, which
creates an XSS vulnerability. To fix this, sanitize d.content before inserting
it by either escaping HTML characters on the server side and rendering it with
textContent instead of innerHTML, or if server-side changes are not possible,
apply a client-side tag removal or escaping function to strip or encode any HTML
tags or scripts before rendering.

@NCookies NCookies merged commit 2ef0a49 into dev Jul 7, 2025
1 of 2 checks passed
@NCookies NCookies deleted the feature/front-comm-noti branch July 7, 2025 15:47
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.

Actionable comments posted: 1

♻️ Duplicate comments (4)
src/main/resources/templates/test-submit.html (4)

1264-1270: 토글 버튼 텍스트가 접힘·펼침 상태를 혼동합니다

닫을 때도 '대댓글 접기' 를 설정하고 있어 UX 오류가 발생합니다.

- childContainer.style.display = 'none';
- this.textContent = '대댓글 접기';
+ childContainer.style.display = 'none';
+ this.textContent = '대댓글 펼치기';

1331-1473: 단일 전역 클릭 리스너가 너무 많은 책임을 집니다

기능별로 별도 핸들러로 분리하고 공통 로직을 helper 함수로 추출하면 가독성과 유지보수가 크게 향상됩니다. 이전 리뷰와 동일 제안이므로 상세 설명은 생략합니다.


877-918: DOMContentLoaded 리스너가 여전히 두 번 등록되어 초기화 로직이 중복 실행됩니다

기존 리뷰에서도 동일한 문제가 지적되었으나(중복 초기화·이벤트 바인딩·API 호출 위험), 수정되지 않았습니다. 하나의 공용 init() 함수로 합친 뒤 DOMContentLoaded 에서 한 번만 호출하도록 리팩터링하세요.

Also applies to: 1667-1669


1150-1170: 사용자 입력을 그대로 innerHTML에 주입 → XSS 취약점이 계속 존재합니다

d.content / r.content 를 필터링 없이 렌더링하고 있어 악성 스크립트 실행 위험이 큽니다. 이전 리뷰에서도 동일 취약점이 보고되었으나 아직 해결되지 않았습니다.

- <div class="discussion-content">${d.content}</div>
+ <div class="discussion-content">${DOMPurify.sanitize(d.content)}</div>

DOMPurify를 사용할 수 없다면 최소한 서버에서 escape 후 textContent 로 출력하세요.

Also applies to: 1230-1240

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 51630e3 and 926484a.

📒 Files selected for processing (1)
  • src/main/resources/templates/test-submit.html (6 hunks)
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: chat26666
PR: ezcode-my/backend#37
File: src/main/java/org/ezcode/codetest/infrastructure/elasticsearch/repository/ProblemElasticsearchRepositoryDsl.java:10-10
Timestamp: 2025-06-06T19:56:27.759Z
Learning: When user chat26666 mentions that code was implemented in a previous PR but explanation was missing, they may submit a follow-up PR just to add documentation/explanation without the actual implementation code.
src/main/resources/templates/test-submit.html (2)
Learnt from: thezz9
PR: ezcode-my/backend#73
File: src/main/resources/templates/submit-test.html:135-135
Timestamp: 2025-06-18T11:41:58.457Z
Learning: src/main/resources/templates/submit-test.html 파일은 테스트용 파일로, isCorrect 값의 하드코딩은 의도적이며 사용자가 UI 요소를 통해 정답/오답 상태를 선택할 수 있도록 개선 예정입니다.
Learnt from: thezz9
PR: ezcode-my/backend#114
File: src/main/java/org/ezcode/codetest/infrastructure/github/GitHubApiClient.java:159-165
Timestamp: 2025-06-28T02:03:38.693Z
Learning: User thezz9's service (ezcode) targets Korean users exclusively ("찐 한국인밖에 안 옴"), so Korean language hardcoding in commit messages and UI elements is intentional and appropriate for their use case.
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build

Comment on lines +1404 to +1406
if (!newContent) return alert('내용을 입력하세요.');
let url, method = 'PUT', payload = { languageId: 1, content: newContent };
if (item.classList.contains('discussion-item')) {
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

edit 요청 payload 에 불필요한 languageId: 1 하드코딩

수정 API에서 언어ID가 의미가 없거나 서버가 무시한다면 해당 필드를 제거하세요.
만약 필수라면 실제 편집 대상의 언어를 바인딩해야 합니다.

🤖 Prompt for AI Agents
In src/main/resources/templates/test-submit.html around lines 1404 to 1406, the
edit request payload includes a hardcoded languageId: 1 which is unnecessary or
ignored by the server. Remove the languageId field from the payload if it is not
required, or replace it with the actual language ID of the item being edited if
it is mandatory. Adjust the payload construction accordingly to avoid sending
irrelevant or incorrect data.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants