Skip to content

Feat/sw 38 - DB 마이그레이션 (MySQL → PostgreSQL) #23

Merged
jin2304 merged 18 commits intodevfrom
feat/SW-38
Feb 10, 2026
Merged

Feat/sw 38 - DB 마이그레이션 (MySQL → PostgreSQL) #23
jin2304 merged 18 commits intodevfrom
feat/SW-38

Conversation

@jin2304
Copy link
Member

@jin2304 jin2304 commented Feb 7, 2026

💡 이슈

resolve {#21}

🤩 개요

  • 데이터베이스 마이그레이션 (MySQL → PostgreSQL) 및 그에 따른 변경사항 수정

🧑‍💻 작업 사항

1) 데이터베이스 마이그레이션 (MySQL → PostgreSQL)

  • PostgreSQL용 초기화 스크립트(init_postgres.sql) 및 전체 삭제 스크립트(drop_all_postgres.sql) 작성
  • 기존 MySQL 문법을 PostgreSQL 호환성에 맞춰 쿼리 및 로직 수정

2) JPA & MyBatis 하이브리드 아키텍처 도입

  • BaseEntity 리팩토링: 기존 MyBatis 전용 엔티티를 JPA와 공용으로 사용할 수 있도록 개선
  • JPA 어노테이션(@MappedSuperclass, @EntityListeners) 추가
  • Auditing(@CreatedDate, @LastModifiedDate) 적용 및 Soft Delete 로직 호환성 확보

3) 도메인 리팩토링 및 타입 안정성 강화

  • ID 타입 표준화: Bookmark 및 관련 도메인의 ID 타입을 Integer에서 Long으로 변경 (DB BIGINT 대응)
    DTO, DAO, Service, Controller 전 계층에 걸쳐 타입 불일치 해결



💡 도입 배경 및 목적 (Why?)

1) PostgreSQL 전환: 확장성 및 유연성 확보

  • 단순 데이터 저장을 넘어, JSONB와 같은 고급 데이터 타입을 활용하여 유연한 스키마 구조를 확보하기 위함
  • 향후 구현될 서브폴더, 복잡한 계층 구조, 팀 기능등 정형화되지 않은 데이터를 효율적으로 처리할 수 있는 기반을 마련

2) JPA 도입: 생산성 및 유지보수성 향상

  • 기존의 복잡한 쿼리 처리에 최적화된 MyBatis 틀은 유지하여 안정성 확보
  • 동시에, 팀 협업 기능이나 신규 서비스 개발 시 반복적인 SQL 작업을 줄이고 빠른 개발 생산성과 유지보수성을 확보하기 위해 JPA를 도입

📖 참고 사항

  • 현재 70~80%정도 마이그레이션 완료상태이며, 신규 기능 추가에 따라 후에 리팩토링 예정

Summary by CodeRabbit

  • 새로운 기능

    • 북마크 모델 확장: 링크 연계, 제목·메모·카테고리 등 메타데이터 지원 및 링크 관리 추가
    • 북마크 API 개선: 인증 기반 CRUD, URL 중심 등록/중복체크, 폴더/카테고리 필터링
  • 개선 사항

    • 데이터베이스 전환: PostgreSQL 환경 및 도커 구성 추가
    • 인증/회원 흐름 업데이트: loginId 기반 로그인 통합 및 사용자 정보 확장
    • 공통 엔터티 도입: 감사(auditing)와 soft-delete 지원
  • 기타

    • 게시판/댓글/좋아요 등 ID·매핑 정비 및 템플릿 바인딩 업데이트

- build.gradle: MySQL 의존성을 PostgreSQL로 교체
- docker-compose.yml: 로컬 개발용 PostgreSQL 16 컨테이너 구성 추가
- application.properties: DB 드라이버 설정을 환경변수로 변경
- 프로파일(dev, prod)별 불필요한 드라이버 하드코딩 제거
-DB 초기화: init_postgres.sql 추가
-DB 삭제:  drop_postgres.sql 추가
- JPA & MyBatis 하이브리드 지원
- JPA 자동화 지원
   - @MappedSuperclass 및 @EntityListeners(AuditingEntityListener.class) 적용
   - @CreatedDate, @LastModifiedDate 등을 통한 생성/수정 시간 자동 관리
   - @SQLRestriction("deleted_at IS NULL")을 통해 JPA 조회 시 삭제된 데이터 자동 제외
- MyBatis 호환성 확보
   - MyBatis의 ResultMap 매핑을 위한 Getter/Setter 제공
   - MyBatis에서 수동 업데이트 시 사용할 수 있는 markAsUpdated() 편의 메서드 제공
- 공통 도메인 로직
   - 논리적 삭제(Soft Delete) 정책을 위한 deletedAt, deletedBy 필드 정의 및 softDelete() 메서드 캡슐화
- PostgreSQL 마이그레이션에 맞춰 소셜 Member 도메인 수정
- PostgreSQL 마이그레이션에 맞춰 소셜 Member 도메인 수정
* Note: 일부만 마이그레이션한 상태이며, 추후 폴더 기능 구현 완료 시 추가 리팩토링 예정
* Note: 추후 확장 기능이기 때문에 일부만 마이그레이션
* Note: 추후 확장 기능이기 때문에 일부만 마이그레이션
* Note: 추후 확장 기능이기 때문에 일부만 마이그레이션
* Note: 추후 확장 기능이기 때문에 일부만 마이그레이션
@jin2304 jin2304 requested a review from ekdh0858 February 7, 2026 11:35
@jin2304 jin2304 added ✨ feat 새로운 기능을 추가 ♻️ refactor 코드 리팩토링 labels Feb 7, 2026
@coderabbitai
Copy link

coderabbitai bot commented Feb 7, 2026

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Walkthrough

데이터베이스를 MySQL에서 PostgreSQL로 전환하기 위해 드라이버·프로퍼티·docker-compose를 추가·수정하고, PostgreSQL용 초기화 및 드롭 SQL 스크립트를 도입했습니다. 전역적으로 엔티티 및 API에서 식별자 타입을 int에서 Long으로 확장하고 필드명을 snake_case ↔ camelCase로 정리했습니다. BaseEntity를 추가해 감사(auditing)와 soft-delete를 도입했습니다. 북마크 서브시스템을 링크(link) 중심으로 재설계하고 관련 DTO·DAO·매퍼·서비스·컨트롤러를 대규모로 변경했으며, 인증 식별자를 username에서 loginId로 전환했습니다.

Sequence Diagram(s)

sequenceDiagram
    participant Client as 클라이언트
    participant Controller as BookmarkApiController
    participant Auth as AuthenticationPrincipal
    participant Service as BookmarkServiceImpl
    participant DAO_Link as MybatisBookmarkDao (link)
    participant DAO_Bookmark as MybatisBookmarkDao (member_saved_link)
    participant DB as PostgreSQL (schema)

    Client->>Controller: POST /api/bookmarks?url={url} body: BookmarkDto
    Controller->>Auth: 인증 정보 확인 (`@AuthenticationPrincipal`)
    Auth-->>Controller: memberId (Long)
    Controller->>Service: insertBookmark(bookmarkDto, url, memberId)
    Service->>DAO_Link: selectLinkByCanonicalUrl(canonicalUrl)
    DAO_Link->>DB: SELECT link WHERE canonical_url = ...
    DB-->>DAO_Link: link record or empty
    alt link exists
        DAO_Link-->>Service: existing Link (linkId)
    else link not exist
        Service->>DAO_Link: insertLink(link)
        DAO_Link->>DB: INSERT INTO link RETURNING link_id
        DB-->>DAO_Link: generated linkId
        DAO_Link-->>Service: new Link
    end
    Service->>DAO_Bookmark: checkBookmarkExists(memberId, folderId, linkId)
    DAO_Bookmark->>DB: SELECT count FROM member_saved_link WHERE ...
    DB-->>DAO_Bookmark: 0 or >0
    alt not exists
        Service->>DAO_Bookmark: insertBookmark(map with linkId, displayTitle, ...)
        DAO_Bookmark->>DB: INSERT INTO member_saved_link (...)
        DB-->>DAO_Bookmark: inserted
        DAO_Bookmark-->>Service: success
    else exists
        DAO_Bookmark-->>Service: already exists
    end
    Service-->>Controller: 결과 맵 (success/duplicate)
    Controller-->>Client: HTTP 200 { success: true/false }
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Poem

🐰 새 둥지로 데이터 옮겨왔네,
긴 숫자 손님들 줄 맞춰 앉고,
링크 하나로 북마크 춤추니,
지워진 건 기록해두고,
풀툴에선 더 단단히 뿌리내리네.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목은 메인 변경사항인 데이터베이스 마이그레이션(MySQL → PostgreSQL)을 명확하게 요약하고 있습니다.
Description check ✅ Passed PR 설명은 제공된 템플릿 구조(이슈, 개요, 작업 사항, 참고 사항)를 따르고 있으며, 마이그레이션 내용과 도입 배경을 상세히 기술하고 있습니다.
Docstring Coverage ✅ Passed Docstring coverage is 90.96% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/SW-38

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

Comment @coderabbitai help to get the list of available commands and usage tips.

@jin2304 jin2304 changed the title Feat/sw 38 - Feat/sw 38 - 데이터베이스 마이그레이션 (MySQL → PostgreSQL) Feb 7, 2026
@jin2304 jin2304 changed the title Feat/sw 38 - 데이터베이스 마이그레이션 (MySQL → PostgreSQL) Feat/sw 38 - DB 마이그레이션 (MySQL → PostgreSQL) Feb 7, 2026
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: 58

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (18)
src/main/resources/mapper/main-mapper.xml (1)

24-33: ⚠️ Potential issue | 🟡 Minor

getListByQuery 쿼리에 불필요한 세미콜론이 포함되어 있습니다.

Line 32의 END;에서 세미콜론은 MyBatis XML 매퍼 내에서 일부 드라이버/환경에서 오류를 일으킬 수 있습니다. 제거하는 것이 안전합니다.

🔧 수정 제안
-        ELSE 2  END;
+        ELSE 2  END
src/main/java/com/web/SearchWeb/comment/dao/MybatisCommentDao.java (1)

40-90: 🧹 Nitpick | 🔵 Trivial

변경된 메서드에 @Override 어노테이션이 누락되어 있습니다.

insertComment (Line 31)에는 @Override가 있지만, selectComments, selectCommentsByMemberId, selectComment, updateComment, deleteComment, countComments 등 나머지 메서드에는 빠져 있습니다. 인터페이스 구현 메서드에는 @Override를 붙여 컴파일 타임 검증을 활용하는 것이 좋습니다.

♻️ `@Override` 추가 예시
+    `@Override`
     public List<Comment> selectComments(Long boardId){
         return mapper.selectComments(boardId);
     }

+    `@Override`
     public List<Comment> selectCommentsByMemberId(Long memberId){
         return mapper.selectCommentsByMemberId(memberId);
     }

+    `@Override`
     public Comment selectComment(Long commentId){
         return mapper.selectComment(commentId);
     }

+    `@Override`
     public int updateComment(Long commentId, CommentDto commentDto) {
         return mapper.updateComment(commentId, commentDto);
     }

+    `@Override`
     public int updateCommentUserProfile(UpdateUserProfileCommentDto commentDto) {
         return mapper.updateCommentUserProfile(commentDto);
     }

+    `@Override`
     public int deleteComment(Long commentId){
         return mapper.deleteComment(commentId);
     }

+    `@Override`
     public int countComments(Long boardId) {
         return mapper.countComments(boardId);
     }
src/main/resources/templates/member/login.html (1)

23-27: 🧹 Nitpick | 🔵 Trivial

alert을 통한 메시지 출력 방식에 XSS 주의가 필요합니다.

Thymeleaf의 th:inline="javascript"가 자동 이스케이프를 처리하지만, alert을 사용한 서버 메시지 출력은 사용자 경험 측면에서도 좋지 않습니다. 향후 Toast/Banner UI 컴포넌트로 전환을 고려해 주세요.

src/main/java/com/web/SearchWeb/member/service/MemberServiceImpl.java (2)

24-36: 🧹 Nitpick | 🔵 Trivial

boardDao가 주입되었지만 사용되지 않습니다.

BoardDao가 import, 필드 선언, 생성자 주입까지 되어 있지만, 이 클래스의 어떤 메서드에서도 사용되지 않습니다. 불필요한 의존성을 제거하세요.

♻️ 미사용 의존성 제거 제안
-import com.web.SearchWeb.board.dao.BoardDao;
     private final MemberDao memberDao;
-    private final BoardDao boardDao;
     private final CommentDao commentDao;
     private final BCryptPasswordEncoder bCryptPasswordEncoder;
 
     `@Autowired`
-    public MemberServiceImpl(MemberDao memberDao, BCryptPasswordEncoder bCryptPasswordEncoder, BoardDao boardDao, CommentDao commentDao) {
+    public MemberServiceImpl(MemberDao memberDao, BCryptPasswordEncoder bCryptPasswordEncoder, CommentDao commentDao) {
         this.memberDao = memberDao;
         this.bCryptPasswordEncoder = bCryptPasswordEncoder;
-        this.boardDao = boardDao;
         this.commentDao = commentDao;
     }

77-98: ⚠️ Potential issue | 🟠 Major

Folder 모듈의 int memberIdLong memberId로 통일 필요

MemberService는 Long memberId를 사용하지만, FolderService, FolderController, FolderDao, FolderSearchRequestDto 등 Folder 관련 모듈 전체에서 여전히 int memberId를 사용하고 있습니다. 도메인 전체에서 memberId 타입을 Long으로 통일하려면 Folder 모듈의 다음 컴포넌트들을 함께 마이그레이션해야 합니다:

  • FolderService 인터페이스 및 FolderServiceImpl의 6개 메서드
  • FolderController의 6개 엔드포인트
  • FolderDao 인터페이스 및 MybatisFolderDao의 구현
  • FolderSearchRequestDto
src/main/java/com/web/SearchWeb/member/service/CustomerUserDetailService.java (2)

31-38: ⚠️ Potential issue | 🟠 Major

loginId 기반 조회로의 변경은 올바르나, 사용자 미발견 시 null 반환은 Spring Security 계약을 위반합니다.

loadUserByUsername에서 사용자를 찾지 못했을 때 null을 반환하면 DaoAuthenticationProvider에서 NullPointerException이 발생할 수 있습니다. Spring Security의 UserDetailsService 계약에 따라 UsernameNotFoundException을 던져야 합니다.

🐛 수정 제안
     `@Override`
     public UserDetails loadUserByUsername(String loginId) throws UsernameNotFoundException {
         Member findUser = mybatisMemberDao.findByLoginId(loginId);
         if(findUser != null){
-            //spring security에 전달해서 검증
             return new CustomUserDetails(findUser);
         }
-        return null;
+        throw new UsernameNotFoundException("사용자를 찾을 수 없습니다: " + loginId);
     }

21-26: 🧹 Nitpick | 🔵 Trivial

MybatisMemberDao를 직접 참조하는 대신 MemberDao 인터페이스에 의존하는 것을 권장합니다.

현재 구현체(MybatisMemberDao)에 직접 의존하고 있어 DIP(의존 역전 원칙)를 위반합니다. 향후 DAO 구현체 교체 시 이 서비스도 수정해야 합니다.

♻️ 인터페이스 의존으로 변경 제안
-import com.web.SearchWeb.member.dao.MybatisMemberDao;
+import com.web.SearchWeb.member.dao.MemberDao;
-    MybatisMemberDao mybatisMemberDao;
+    private final MemberDao memberDao;
 
     `@Autowired`
-    public CustomerUserDetailService(MybatisMemberDao mybatisMemberDao) {
-        this.mybatisMemberDao = mybatisMemberDao;
+    public CustomerUserDetailService(MemberDao memberDao) {
+        this.memberDao = memberDao;
     }
src/main/java/com/web/SearchWeb/member/controller/MemberController.java (1)

26-26: 🧹 Nitpick | 🔵 Trivial

memberService 필드에 접근 제어자 및 final 누락.

생성자 주입을 사용하고 있으므로 private final로 선언하는 것이 Spring 권장 패턴입니다. 불변성을 보장하고 의도치 않은 재할당을 방지합니다.

♻️ 수정 제안
-    MemberService memberService;
+    private final MemberService memberService;
src/main/resources/templates/mypage/myPage.html (2)

536-577: ⚠️ Potential issue | 🔴 Critical

XSS 취약점: 사용자 입력이 HTML에 직접 삽입됩니다.

addBookmarkToUI 함수에서 displayTitle, note, url 값이 이스케이프 없이 템플릿 리터럴로 HTML에 직접 삽입됩니다. 악의적인 사용자가 displayTitle이나 note<script>alert('xss')</script> 같은 값을 저장하면 다른 사용자의 브라우저에서 실행될 수 있습니다. 또한 url 필드에 javascript: 프로토콜을 사용한 XSS도 가능합니다.

textContent/innerText를 사용하거나, DOM API로 엘리먼트를 생성하여 속성을 설정하는 방식으로 변경해야 합니다.

🔒 수정 방향 예시
 function addBookmarkToUI(bookmark) {
     const { bookmarkId, displayTitle, note } = bookmark;
     const url = bookmark.link ? bookmark.link.originalUrl : '';
+
+    // HTML 이스케이프 유틸
+    function escapeHtml(str) {
+        const div = document.createElement('div');
+        div.textContent = str || '';
+        return div.innerHTML;
+    }
+
     const bookmarkElement = document.createElement('div');
     // ... 이후 innerHTML 대신 DOM API 사용 또는 escapeHtml() 적용
+    const safeTitle = escapeHtml(displayTitle);
+    const safeNote = escapeHtml(note);
+    const safeUrl = escapeHtml(url);

688-690: 🧹 Nitpick | 🔵 Trivial

HTTP 메서드 'Put''PUT'으로 대문자 통일 권장.

jQuery는 대소문자 구분 없이 처리하지만, HTTP 표준 및 코드 일관성(Line 715의 'DELETE' 등)을 위해 대문자로 통일하는 것이 좋습니다.

♻️ 수정 제안
-            type: 'Put', //type: 'Patch',
+            type: 'PUT',
src/main/resources/mapper/board-mapper.xml (1)

73-88: ⚠️ Potential issue | 🟠 Major

selectBoardCountselectBoardPagepostType == 'question' 필터 불일치.

selectBoardCount (Line 84-86)에는 postType == 'question' 조건이 남아있지만, selectBoardPage에서는 해당 조건이 제거되었습니다. 이 불일치로 인해 'question' 타입 요청 시 페이징 계산에 사용되는 카운트와 실제 조회되는 결과 수가 달라집니다.

🐛 수정 제안: selectBoardPage에도 조건을 추가하거나, selectBoardCount에서 제거

selectBoardPage에서 의도적으로 제거한 것이라면 selectBoardCount에서도 제거해야 합니다:

             <if test="postType == 'website'">
                 AND url IS NOT NULL
             </if>
-            <if test="postType == 'question'">
-                AND url IS NULL
-            </if>
src/main/java/com/web/SearchWeb/member/dao/MybatisMemberDao.java (1)

32-35: 🧹 Nitpick | 🔵 Trivial

SocialjoinProcess 메서드명이 Java 네이밍 컨벤션을 위반하고 있습니다.

기존 코드이지만, 이번 리팩토링 시 socialJoinProcess로 변경하는 것을 권장합니다. MemberDao 인터페이스와 매퍼 XML도 함께 수정이 필요합니다.

src/main/java/com/web/SearchWeb/member/service/CustomOAuth2UserService.java (1)

100-106: 🧹 Nitpick | 🔵 Trivial

기존 사용자 업데이트 시 loginId 재설정은 불필요

Line 83에서 findByLoginId(loginId)로 조회한 사용자에게 Line 101에서 동일한 loginId를 다시 설정합니다. loginId는 조회 키이므로 값이 변경될 수 없어 중복 설정입니다. 또한, updateSocialMember mapper에서 login_id를 UPDATE SET에 포함하고 있으므로 불필요한 쓰기가 발생합니다.

src/main/java/com/web/SearchWeb/board/service/BoardService.java (2)

106-126: ⚠️ Potential issue | 🔴 Critical

경쟁 조건(Race Condition): 북마크 수 증가/감소의 read-modify-write 패턴

incrementBookmarkCountdecrementBookmarkCount가 현재 값을 읽고(selectBoard) → 계산 → 쓰기(updateBookmarkCount) 패턴을 사용합니다. @Transactional도 없고, 동시 요청 시 두 스레드가 같은 값을 읽어 업데이트를 덮어쓸 수 있습니다.

댓글 수의 incrementCommentCount/decrementCommentCount처럼 SQL 레벨에서 SET bookmarks_count = bookmarks_count + 1로 원자적 업데이트를 수행하는 DAO 메서드로 변경해야 합니다.

추가로, decrementBookmarkCount(Line 122)에서 bookmarksCount가 이미 0인 경우 음수로 내려갈 수 있으므로 하한 검사가 필요합니다.

🐛 제안: 원자적 업데이트 메서드 사용

BoardDao에 원자적 증가/감소 메서드를 추가하고 서비스를 단순화하세요:

-    public void incrementBookmarkCount(Long boardId) {
-        Board board = boardDao.selectBoard(boardId);
-        if (board != null) {
-            boardDao.updateBookmarkCount(boardId, board.getBookmarksCount() + 1);
-        } else {
-            throw new IllegalArgumentException("Invalid board ID");
-        }
-    }
+    public void incrementBookmarkCount(Long boardId) {
+        int updated = boardDao.incrementBookmarkCount(boardId);
+        if (updated == 0) {
+            throw new IllegalArgumentException("Invalid board ID");
+        }
+    }
-    public void decrementBookmarkCount(Long boardId) {
-        Board board = boardDao.selectBoard(boardId);
-        if (board != null) {
-            boardDao.updateBookmarkCount(boardId, board.getBookmarksCount() - 1);
-        } else {
-            throw new IllegalArgumentException("Invalid board ID");
-        }
-    }
+    public void decrementBookmarkCount(Long boardId) {
+        int updated = boardDao.decrementBookmarkCount(boardId);
+        if (updated == 0) {
+            throw new IllegalArgumentException("Invalid board ID");
+        }
+    }

매퍼 SQL 예시:

UPDATE board SET bookmarks_count = bookmarks_count + 1 WHERE board_id = #{boardId}
UPDATE board SET bookmarks_count = GREATEST(bookmarks_count - 1, 0) WHERE board_id = #{boardId}

69-84: ⚠️ Potential issue | 🟠 Major

selectBoard에서 조회수 증가 후 board null 체크 누락

Line 72에서 incrementViewCount를 호출한 후, Line 74에서 selectBoard를 호출하는데 반환값이 null일 가능성에 대한 처리가 없습니다. boardId가 존재하지 않으면 Line 77에서 board.getHashtags() 호출 시 NullPointerException이 발생합니다.

🐛 null 체크 추가 제안
         Board board = boardDao.selectBoard(boardId);  // 단일 Board 객체를 가져옵니다.
+        if (board == null) {
+            throw new IllegalArgumentException("게시글이 존재하지 않습니다.");
+        }
 
         // 해시태그를 분리하여 리스트에 추가합니다.
src/main/java/com/web/SearchWeb/bookmark/dao/MybatisBookmarkDao.java (1)

13-111: ⚠️ Potential issue | 🔴 Critical

다중 파라미터 메서드에 @Param 어노테이션이 필수입니다

BookmarkDao 인터페이스의 checkBookmarkExists, selectBookmark, insertBookmark, updateBookmark, deleteBookmark, deleteBookmarkByLink, checkBookmarkExistsByUrl 등 다중 파라미터를 가진 메서드들에 @Param 어노테이션이 없습니다.

매퍼 XML에서 명시적 파라미터 이름(#{memberId}, #{folderId}, #{linkId}, #{bookmark.*} 등)을 사용하고 있으므로, 각 파라미터에 @Param 어노테이션을 추가하여 MyBatis의 파라미터 바인딩을 올바르게 설정해야 합니다. 현재 상태에서는 이들 메서드 호출 시 파라미터 바인딩 오류가 발생합니다.

예시
// 수정 전
int checkBookmarkExists(Long memberId, Long folderId, Long linkId);

// 수정 후
int checkBookmarkExists(
    `@Param`("memberId") Long memberId, 
    `@Param`("folderId") Long folderId, 
    `@Param`("linkId") Long linkId
);
src/main/java/com/web/SearchWeb/board/controller/BoardController.java (2)

185-216: ⚠️ Potential issue | 🔴 Critical

레거시 board-bookmark 기능이 완전히 동작하지 않음

BookmarkServiceImpl에서 checkBoardBookmark(), insertBookmarkForBoard(), deleteBookmarkBoard() 모두 0을 반환하는 스텁으로 구현되어 있습니다. 결과적으로:

  1. checkBoardBookmark이 항상 0을 반환 → bookmarkExists == 0 조건이 항상 true
  2. insertBookmarkForBoard가 항상 0을 반환 → 실제 북마크가 생성되지 않음
  3. incrementBookmarkCount는 계속 호출되어 게시글의 bookmark_count만 증가
  4. 북마크 해제(unbookmark) 경로는 절대 실행되지 않는 dead code

이 엔드포인트가 현재 사용 중이라면, 요청마다 bookmark_count만 무한히 증가하는 데이터 오염 버그가 됩니다. 사용하지 않을 예정이라면 엔드포인트 자체를 제거하거나 @Deprecated로 명시해야 합니다.


83-94: 🧹 Nitpick | 🔵 Trivial

인증 사용자 ID 추출 로직 중복

boardDetailtoggleLike 메서드에서 currentUser instanceof 체크를 통한 memberId 추출 패턴이 동일하게 반복됩니다. BookmarkApiController에서는 이미 getMemberId() 헬퍼로 추출하고 있으므로, 공통 유틸리티나 베이스 클래스로 통합하는 것이 좋습니다.

🤖 Fix all issues with AI agents
In `@build.gradle`:
- Line 31: The dependency line "implementation
'org.springframework.boot:spring-boot-starter-data-jpa'" in the dependencies
block uses 4 spaces for indentation instead of the tabs used elsewhere; update
the leading whitespace for that line to a single tab so it matches the rest of
the dependencies block formatting (look for the dependencies { ... } block and
the implementation line to correct).

In `@docker-compose.yml`:
- Line 1: docker-compose.yml에서 더 이상 사용되지 않는 `version: '3.8'` 키를 제거하세요; Docker
Compose V2는 `version` 필드를 무시하므로 해당 줄(`version: '3.8'`)을 삭제하면 되고 다른
서비스/volume/network 정의에는 영향이 없도록 파일 나머지 구조를 그대로 유지하세요.
- Around line 7-11: Move plaintext DB credentials out of docker-compose.yml by
creating a .env file (and add it to .gitignore) that defines POSTGRES_USER and
POSTGRES_PASSWORD, update docker-compose.yml to reference that file using
env_file or variable interpolation for POSTGRES_USER/POSTGRES_PASSWORD instead
of hardcoding, and replace the weak password "1111" with a stronger secret;
ensure POSTGRES_DB and TZ remain as needed and verify docker-compose starts
correctly with the new env_file usage.

In `@src/main/java/com/web/SearchWeb/aop/OwnerCheckAspect.java`:
- Around line 69-82: In extractTargetIdFromParams(JoinPoint, String) add a null
check for args[i] before calling toString(): if args[i] is null, log a clear
error including idParam and paramNames and throw IllegalArgumentException (or
return a safe value if desired) instead of letting a NullPointerException
propagate; also guard Long.parseLong with try/catch(NumberFormatException) to
convert bad inputs into a controlled IllegalArgumentException with context so
callers see a descriptive error rather than an NPE or parse crash.

In `@src/main/java/com/web/SearchWeb/board/controller/BoardController.java`:
- Line 114: The `@GetMapping` annotation in BoardController is missing the leading
slash for the route; update the mapping on the method currently annotated with
`@GetMapping`("api/boards") to use `@GetMapping`("/api/boards") so it is consistent
with other endpoints (check the BoardController class and the specific method
handling the boards listing, e.g., the method annotated with
`@GetMapping`("api/boards")).

In `@src/main/java/com/web/SearchWeb/board/domain/Board.java`:
- Around line 14-29: Board is missing BaseEntity inheritance and uses a String
createdDate like Comment; update the Board class to extend BaseEntity (to match
Comment) and change the createdDate field type from String to the project's
standard timestamp type (e.g., Instant/LocalDateTime) to ensure consistent
timestamp handling; modify the Board constructor/getters/setters and any usages
of Board.createdDate (and refer to Board class, createdDate field, and
BaseEntity) to use the unified timestamp type and persist/serialize accordingly.
- Around line 16-20: The Board class currently stores denormalized member fields
(nickname, job, major) alongside memberMemberId, which risks stale data; either
remove these fields and load member data via a join/Member repository when
reading (use the memberMemberId as the FK) or keep them but implement a
synchronization path: add a MemberUpdatedEvent handler (or extend MemberService)
that calls a BoardService method like updateBoardsByMemberId(memberId,
newNickname, newJob, newMajor) to batch-update
Board.nickname/Board.job/Board.major whenever a member profile changes, and
ensure read paths either join to Member or rely on the synchronized denormalized
values consistently.

In `@src/main/java/com/web/SearchWeb/board/dto/BoardDto.java`:
- Around line 11-18: Domain-wide naming is inconsistent between "nickname" and
"nickName"; standardize to a single convention (pick one, e.g., "nickName")
across all DTOs and domain classes to avoid mapping/serialization issues: update
the field name in BoardDto (currently private String nickname) to the chosen
form, and apply the same change to Member, MemberDto, MemberUpdateDto,
UpdateUserProfileCommentDto and any MyBatis mappers/JSON annotations, adjusting
getters/setters, constructors, and mapping references to match the new
identifier.

In
`@src/main/java/com/web/SearchWeb/bookmark/controller/BookmarkApiController.java`:
- Around line 135-152: The updateBookmark method currently relies on getMemberId
to return null for unauthenticated users, causing inconsistent status codes; add
the same explicit authentication check used in
checkBookmark/insertBookmark/selectBookmarkList/selectBookmark by testing if
currentUser == null || "anonymousUser".equals(currentUser) at the start of
updateBookmark and return
ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(response) when true; do the
identical change in deleteBookmark so both methods consistently return 401 for
unauthenticated requests and still use getMemberId and existing logic for
authenticated users.

In `@src/main/java/com/web/SearchWeb/bookmark/dao/BookmarkDao.java`:
- Around line 10-39: The interface currently mixes bookmark operations with
link-specific methods; extract link responsibilities into a new LinkDao by
moving selectLinkByCanonicalUrl and insertLink out of BookmarkDao into a new
LinkDao interface, update all implementing classes (those implementing
BookmarkDao and the new LinkDao) and any callers to use LinkDao (dependency
injection or repository wiring), and remove the two methods from BookmarkDao so
BookmarkDao only exposes bookmark-focused methods like checkBookmarkExists,
insertBookmark, selectBookmark, selectBookmarkList, updateBookmark,
deleteBookmark, deleteBookmarkByLink, and checkBookmarkExistsByUrl; also update
unit/integration tests and wiring accordingly.
- Around line 11-15: Multiple DAO methods that accept two or more parameters
(checkBookmarkExists, insertBookmark, selectBookmark, updateBookmark,
deleteBookmark, deleteBookmarkByLink, checkBookmarkExistsByUrl) are missing
MyBatis `@Param` annotations, causing parameter binding failures; add
org.apache.ibatis.annotations.Param to each of these method signatures and
annotate each parameter with `@Param` using the exact names referenced in the
mapper XML (e.g., `@Param`("memberId"), `@Param`("folderId"), `@Param`("linkId"),
`@Param`("bookmarkId"), etc.) so MyBatis can bind #{memberId}, #{bookmarkId},
#{linkId}, etc., correctly.

In `@src/main/java/com/web/SearchWeb/bookmark/domain/Bookmark.java`:
- Around line 19-31: The Bookmark class inherits BaseEntity which uses Lombok's
`@SuperBuilder/`@NoArgsConstructor/@AllArgsConstructor, but Bookmark itself lacks
these annotations; add Lombok annotations to Bookmark—specifically annotate the
class with `@NoArgsConstructor` and `@AllArgsConstructor` and add `@SuperBuilder` on
Bookmark—so MyBatis can still use the no-arg constructor for mapping and you can
safely use the builder pattern across the BaseEntity/Bookmark hierarchy; ensure
imports for Lombok annotations are present and that the class-level annotations
reference Bookmark (the Bookmark class) to generate the appropriate constructors
and builder.
- Around line 16-19: The Bookmark class uses Lombok's `@ToString` but omits
callSuper, so fields from BaseEntity (e.g., createdAt, updatedAt, deletedAt) are
not included in toString(); update the annotation on the Bookmark class
(`@ToString`) to set callSuper = true so the generated toString() includes
superclass fields (adjust any imports/annotation usage on class Bookmark
accordingly).

In `@src/main/java/com/web/SearchWeb/bookmark/domain/Link.java`:
- Line 33: The field primaryCategoryId in class Link has a type mismatch with
its inline comment; inspect the DB schema for the primary_category_id column and
make the types consistent: if the DB column is BIGINT, update the comment to
BIGINT (and keep Long), otherwise if the DB column is INT change the Java field
primaryCategoryId to Integer (or int) and update any getters/setters,
constructors and usages in Link to use the new type; ensure any JPA
mapping/column definition referencing primaryCategoryId is also updated to
match.
- Line 23: The class declaration uses a fully-qualified name for the superclass;
update Link to import BaseEntity and extend it by simple name: add an import for
com.web.SearchWeb.common.domain.BaseEntity and change the extends clause in
class Link to "extends BaseEntity" (ensure there's no duplicate import and the
package visibility is correct).

In `@src/main/java/com/web/SearchWeb/bookmark/dto/BookmarkCheckDto.java`:
- Around line 12-15: The field naming and type for website_websiteId in
BookmarkCheckDto is inconsistent with the migration (camelCase and Long IDs);
rename website_websiteId to websiteId and change its type from int to Long
within the BookmarkCheckDto class, updating any related getters/setters,
constructors, and usages in methods or other DTOs that reference
BookmarkCheckDto so they use BookmarkCheckDto.websiteId (Long) instead of
website_websiteId (int).

In `@src/main/java/com/web/SearchWeb/bookmark/dto/BookmarkDto.java`:
- Around line 22-27: BookmarkDto is missing Bean Validation annotations for
required fields; add appropriate javax/jakarta.validation annotations (e.g.,
`@NotBlank` on the url field and `@Size/`@NotNull where applicable) to the
BookmarkDto class so incoming requests are validated automatically; update
fields like url, displayTitle, and note (and
createdByMemberId/memberFolderId/primaryCategoryId if business rules require
non-null) with `@NotBlank/`@NotNull/@Size as needed, and ensure controller
endpoints using BookmarkDto are annotated with `@Valid` so the validation is
triggered (refer to the BookmarkDto class and its url/displayTitle/note fields
and the controller methods accepting BookmarkDto).

In `@src/main/java/com/web/SearchWeb/bookmark/service/BookmarkService.java`:
- Around line 33-45: The legacy board-bookmark methods in the BookmarkService
interface (checkBoardBookmark, isBookmarked, insertBookmarkForBoard,
deleteBookmarkBoard) should be removed from this interface and moved into a new
LegacyBoardBookmarkService interface (or deleted together with their callers
such as BoardController.toggleBookmark) so implementing classes like
BookmarkServiceImpl aren't forced to keep stubs; update any callers to depend on
the new interface or remove them. Also reconcile the inconsistent return types:
change isBookmarked to return boolean (to match checkBookmarkExistsByUrl) or
alternatively change checkBookmarkExistsByUrl to int so the semantics align, and
update all usages accordingly. Ensure BookmarkServiceImpl no longer contains
stub 0-return implementations for these legacy methods after the refactor.

In `@src/main/java/com/web/SearchWeb/bookmark/service/BookmarkServiceImpl.java`:
- Around line 168-208: Remove the legacy board-bookmark stubs from
BookmarkServiceImpl (methods checkBoardBookmark, isBookmarked,
insertBookmarkForBoard, deleteBookmarkBoard) and delete the corresponding
endpoint usage in BoardController.toggleBookmark; ensure no callers remain
(search for toggleBookmark and any references to those method names), add a
follow-up issue to track the cleanup and update tests/usage to avoid data
corruption before merging.
- Around line 56-79: getOrCreateLink currently races: concurrent threads can
both see null from bookmarkDao.selectLinkByCanonicalUrl and call
bookmarkDao.insertLink, causing unique constraint violations or duplicates; add
a DB unique constraint on canonical_url and change getOrCreateLink to perform an
atomic upsert (Postgres: INSERT ... ON CONFLICT (canonical_url) DO
UPDATE/NOTHING RETURNING *) via a new DAO method (e.g.,
insertOrGetLinkByCanonicalUrl) or implement a try-catch retry: call insertLink,
on unique-violation exception re-query with selectLinkByCanonicalUrl and return
the existing link; keep normalizeUrl, selectLinkByCanonicalUrl, and insertLink
usages but replace the blind insert with the upsert or insert-with-retry flow to
ensure idempotent creation.
- Around line 139-152: The normalizeUrl method can produce "null://null" when
URI.getScheme() or URI.getHost() is null (e.g., file:, urn:, or relative URIs);
add explicit null checks inside normalizeUrl to avoid constructing strings with
null parts: validate uri.getScheme() and uri.getHost() before concatenation, and
if either is null return the original input (or handle scheme-only URIs by using
only the scheme and path if appropriate). Update the method (normalizeUrl) to
guard against null scheme/host, trim trailing slashes as before, and keep the
existing catch block as a final fallback.
- Around line 86-95: The method checkBookmarkExistsByUrl currently builds a
canonicalUrl but calls bookmarkDao.checkBookmarkExistsByUrl(memberId, url)
causing two bugs: missing mapper SQL and using the unnormalized url. Fix by (1)
updating the DAO call in BookmarkServiceImpl.checkBookmarkExistsByUrl to pass
canonicalUrl instead of url, i.e.,
bookmarkDao.checkBookmarkExistsByUrl(memberId, canonicalUrl), and (2) add the
corresponding SQL mapping in bookmark-mapper.xml for
checkBookmarkExistsByUrl(memberId, canonicalUrl) (or create a new mapper method
like checkBookmarkExistsByCanonicalUrl) that counts bookmarks regardless of
folderId if that behavior is required; ensure method names in BookmarkDao match
the mapper IDs.
- Around line 68-75: The Link builder in BookmarkServiceImpl is using a
hardcoded magic number primaryCategoryId(1L); extract this literal into a named
constant (e.g., DEFAULT_PRIMARY_CATEGORY_ID) or configurable property and
reference that constant instead of 1L; update the Link creation
(Link.builder()...primaryCategoryId(...)) to use the new constant or injected
config value and add the constant declaration (or injection) near the class
scope so intent and maintainability are clear.

In
`@src/main/java/com/web/SearchWeb/comment/controller/CommentApiController.java`:
- Around line 88-93: The mapping for selectComment includes a boardId path
segment but the method doesn't bind it; update the method signature for
selectComment to accept `@PathVariable` Long boardId (in addition to `@PathVariable`
Long commentId) so the {boardId} is explicitly bound, and ensure any usage
needed (e.g., in OwnerCheck or commentService.selectComment) is adjusted
accordingly; alternatively, if boardId is not needed, remove "{boardId}" from
the `@GetMapping` route to keep the mapping and `@OwnerCheck`(idParam="commentId",
service="commentService") consistent.
- Around line 40-41: Update the REST mappings to use the plural resource name
"comments" consistently; change the `@PostMapping` on insertComment (method
insertComment) from "board/{boardId}/comment" to "board/{boardId}/comments" and
likewise adjust the mappings for the methods that handle listing (e.g.,
getComments), retrieving a single comment (getComment), updating
(updateComment/updateCommentById) and deleting (deleteComment/deleteCommentById)
so all paths use "board/{boardId}/comments" for collection endpoints and
"board/{boardId}/comments/{commentId}" for item endpoints; ensure any related
path references or tests are updated to match the new pluralized paths.
- Around line 78-82: The selectComments method in CommentApiController has an
unused Model parameter; remove the Model parameter from the method signature
(public ResponseEntity<List<Comment>> selectComments(`@PathVariable` Long
boardId)) and update any callers if needed, and also remove the unused import
org.springframework.ui.Model from the class imports; keep the call to
commentService.selectComments(boardId) and return ResponseEntity.ok(comments)
unchanged.

In `@src/main/java/com/web/SearchWeb/comment/domain/Comment.java`:
- Around line 15-24: The Comment entity should extend BaseEntity and align its
timestamp/soft-delete fields with the rest of the domain: update class Comment
to extend BaseEntity (so it inherits audit and soft-delete behavior) and remove
or replace the existing createdDate String field with the OffsetDateTime
timestamp provided by BaseEntity (or map it to that type if you need a field
named createdDate). Ensure the identifier and FK fields (commentId,
boardBoardId, memberMemberId) remain, update any constructors/getters/setters to
use the BaseEntity timestamps, and remove the "Legacy" marker once aligned.
- Around line 16-18: The updateComment mapping uses
parameterType="com.web.SearchWeb.comment.domain.Comment" but the SQL references
#{commentDto.content}; change the SQL parameter references in the updateComment
statement to use the Comment entity property names (e.g., #{content} not
#{commentDto.content}), and ensure the updateComment mapping and its
parameterType match the actual object passed from the service; check the
CommentResultMap already handles snake_case/camelCase but keep field names
consistent when converting CommentDto -> Comment in the service layer if
applicable.

In `@src/main/java/com/web/SearchWeb/comment/dto/CommentDto.java`:
- Around line 11-13: CommentDto uses snake_case fields (board_boardId,
member_memberId, member_nickname); rename them to camelCase in the CommentDto
class (boardBoardId, memberMemberId, memberNickname) and update all related
members (constructors, getters/setters, builder methods, JSON property
annotations or mapping code) and any usages elsewhere to match the new names so
the DTO matches the Comment domain naming (class: CommentDto; fields:
board_boardId → boardBoardId, member_memberId → memberMemberId, member_nickname
→ memberNickname).

In `@src/main/java/com/web/SearchWeb/comment/service/CommentService.java`:
- Around line 105-109: The exception message in
CommentService.findMemberIdByCommentId is incorrect for comment lookups: when
commentdao.selectComment(commentId) returns null, replace the
IllegalArgumentException message "게시글이 존재하지 않습니다." with the correct message "댓글이
존재하지 않습니다." (update the throw in findMemberIdByCommentId so it reflects comment
not post).

In `@src/main/java/com/web/SearchWeb/common/domain/BaseEntity.java`:
- Around line 28-37: Class-level `@Setter` in BaseEntity exposes audit fields like
createdAt and createdByMemberId that must be immutable; remove the class-level
`@Setter` on BaseEntity and instead apply Lombok `@Setter` only to mutable fields
(or explicitly add `@Setter`(AccessLevel.NONE) / no setter) for audit fields such
as createdAt, createdByMemberId, updatedAt, updatedByMemberId, leaving getters
(`@Getter`) and JPA auditing to manage those values; update BaseEntity annotations
accordingly and ensure any MyBatis mappings that require setters target only the
fields intended to be writable.
- Around line 70-74: softDelete currently sets deletedAt using
OffsetDateTime.now() which relies on the system default timezone; change
BaseEntity.softDelete to set deletedAt using UTC explicitly (e.g.,
OffsetDateTime.now(ZoneOffset.UTC) or ZoneId.of("UTC")) and keep assigning
deletedByMemberId as before so timestamps are consistent across server/DB
timezones; update imports if needed to include ZoneOffset/ZoneId.
- Around line 76-80: The class provides markAsUpdated(...) for manual audit
setting but lacks a markAsCreated(...) for MyBatis-driven INSERTs; add a public
method markAsCreated(Long memberId, OffsetDateTime now) in BaseEntity that sets
this.createdAt = now and this.createdByMemberId = memberId (optionally also
initialize updatedAt/updatedByMemberId if you want consistent audit values) so
MyBatis can explicitly populate created audit fields when
`@CreatedDate/`@CreatedBy are not applied.
- Around line 54-60: The BaseEntity fields createdByMemberId and
updatedByMemberId use `@CreatedBy/`@LastModifiedBy but auditing is not enabled and
no AuditorAware<Long> bean is provided; add `@EnableJpaAuditing` to your main
application class (SearchWebApplication) or a config class and
implement/register an AuditorAware<Long> bean that returns the current member id
(e.g., from SecurityContextHolder or your authentication token) so the
`@CreatedBy/`@LastModifiedBy annotations on BaseEntity are populated; ensure the
AuditorAware implementation is a `@Component` or declared as a `@Bean` so Spring
Data JPA can use it.

In `@src/main/java/com/web/SearchWeb/main/dao/MainDao.java`:
- Line 10: FolderDao is still using int for IDs while the rest of the project
migrated to Long; update FolderDao and its implementation to use Long
consistently: change method signatures in FolderDao (e.g., selectFolder,
deleteFolder, selectFolderTags and any other ID params) from int to Long, update
MybatisFolderDao method signatures and parameter types accordingly, and adjust
any mapper XML/SQL parameter types or MyBatis parameter bindings that reference
folderId/memberId to use Long to ensure type consistency across the codebase.

In `@src/main/java/com/web/SearchWeb/main/domain/Website.java`:
- Around line 15-24: Website.websiteId was changed to Long but BookmarkCheckDto
still declares website_websiteId as int; update BookmarkCheckDto (field
website_websiteId, its getter/setter and any constructor/mapper that sets or
reads it) to use Long to match Website.websiteId, and search for other DTOs or
comparisons that reference website_websiteId to change types and any
equals/hashCode or comparison logic to Long-aware (avoid int-to-Long autoboxing
or equals mismatches).
- Line 23: The viewCount field in class Website is currently declared as Long;
change it to primitive long if the DB column view_count is NOT NULL to avoid
NullPointerException risks: update the field declaration (viewCount), its
getter/setter methods (getViewCount/setViewCount) and any constructors or
builders that set it to use long, and ensure any ORM/JDBC mapping annotations or
result-set handling reflect non-nullability (e.g., `@Column`(nullable = false) or
using ResultSet.getLong) so no null is propagated.

In `@src/main/java/com/web/SearchWeb/member/dao/MemberDao.java`:
- Line 13: The interface method name SocialjoinProcess violates Java naming
conventions; rename the method to socialJoinProcess in MemberDao and update all
implementing classes and callers to match (preserve the parameter type Member
and method signature), update any `@Override` annotations or method references,
and run a project-wide search/rename to ensure no remaining references to
SocialjoinProcess remain.

In `@src/main/java/com/web/SearchWeb/member/domain/Member.java`:
- Line 18: The Member class uses the field Member.nickName while other types use
"nickname" (Board.nickname, MemberUpdateDto.nickname, Comment.memberNickname);
unify the naming to avoid MyBatis mapping issues by renaming Member.nickName to
nickname everywhere: update the field name in Member, adjust its
getter/setter/constructor/builder/equals/hashCode/toString and any
JSON/serialization annotations, refactor all references (service, repository,
DTOs, mappers, tests) and update corresponding MyBatis XML or `@Column/`@Result
mappings to match the chosen "nickname" property so column ↔ field mapping
remains consistent across the project.

In `@src/main/java/com/web/SearchWeb/member/dto/CustomOAuth2User.java`:
- Around line 84-86: Update CustomOAuth2User.getLoginId() to concatenate the
provider and providerId with a clear delimiter (e.g., ":" or "|") instead of
direct concatenation of oAuth2Response.getProvider() and
oAuth2Response.getProviderId(); change the method to return provider + delimiter
+ providerId (use the chosen delimiter consistently) and ensure any code
referencing login IDs understands the new format, plus verify and handle
compatibility with existing DB-stored login_id values (migrate or map old values
if necessary).

In `@src/main/java/com/web/SearchWeb/member/service/CustomOAuth2UserService.java`:
- Line 97: The method name SocialjoinProcess violates Java naming conventions;
rename it to socialJoinProcess across the codebase: update the DAO interface
method declaration (e.g., MemberDao#SocialjoinProcess -> socialJoinProcess), its
implementation class(es), the MyBatis mapper XML id attribute (change
id="SocialjoinProcess" -> id="socialJoinProcess"), and all call sites such as
the invocation in CustomOAuth2UserService (memberDao.SocialjoinProcess ->
memberDao.socialJoinProcess). After renaming, rebuild to fix imports/usages and
run tests to ensure no remaining references to the old name.
- Line 92: Replace the hardcoded password "1111" in CustomOAuth2UserService
where member.setPasswordHash("1111") is used for social logins with a
cryptographically safe, non-guessable hash: generate a random value (e.g.,
UUID.randomUUID().toString()) and hash it with your password encoder (e.g.,
passwordEncoder.encode(randomValue)) before calling member.setPasswordHash(...).
Ensure you use the existing PasswordEncoder bean or BCryptPasswordEncoder and
reference the member.setPasswordHash call in CustomOAuth2UserService so social
accounts cannot be authenticated via predictable passwords.
- Line 94: The code in CustomOAuth2UserService sets a hardcoded nickname via
member.setNickName("닉네임"), causing every social user to share the same nickname;
replace that hardcoded value with a unique identifier (for example use
oAuth2Response.getName() if available, otherwise derive from the social loginId
or combine provider + loginId to guarantee uniqueness) and ensure the assignment
in the user creation flow (where member is populated) uses this fallback logic
so nicknames are unique per user.

In `@src/main/java/com/web/SearchWeb/mypage/controller/MyPageController.java`:
- Around line 73-83: The POST endpoint insertBookmark in MyPageController
currently mixes `@RequestBody` BookmarkDto and `@RequestParam` String url, forcing
clients to send JSON body plus query param; move url into the request body by
adding a url field to BookmarkDto (or create a new InsertBookmarkRequest DTO)
and update the method signature to accept that DTO (e.g.,
insertBookmark(`@PathVariable` Long memberId, `@RequestBody` BookmarkDto
bookmarkDto)), set createdByMemberId on that DTO, and pass the DTO (or its url)
to bookmarkService.insertBookmark so the API accepts a single JSON payload;
update any validation and tests accordingly.
- Around line 118-125: In updateBookmark (MyPageController.updateBookmark) the
bookmarkDto isn’t populated with the member id so the mapper’s
updated_by_member_id (#{bookmarkDto.createdByMemberId}) becomes null; before
calling bookmarkService.updateBookmark(bookmarkDto, bookmarkId) set
bookmarkDto.setCreatedByMemberId(memberId) so the audit field is populated and
persisted by the updateBookmark flow that uses the bookmark-mapper.xml mapping.

In `@src/main/resources/application.properties`:
- Line 5: 환경 변수 미설정 시 애플리케이션 시작 실패를 방지하려면 현재 사용 중인 프로퍼티 키
SPRING_DATASOURCE_DRIVER_CLASS_NAME에 대해 기본값을 지정하세요; application.properties의
spring.datasource.driver-class-name 항목을 환경변수 치환 구문에 기본값을 포함하도록 변경(예:
spring.datasource.driver-class-name=${SPRING_DATASOURCE_DRIVER_CLASS_NAME:org.postgresql.Driver})하고
커밋하세요 so that the driver falls back to the PostgreSQL driver when the env var is
absent.

In `@src/main/resources/db/init_postgres.sql`:
- Around line 427-541: Several single-column B-tree indexes target very
low-cardinality columns (idx_category_active, idx_fsr_active,
idx_category_level, idx_tfp_permission, idx_team_member_role) which hurt write
performance and provide little planner benefit; drop these standalone indexes
and replace them with composite or partial indexes tailored to actual query
patterns (for example, include deleted_at or other filtering columns, or create
partial indexes like WHERE is_active = true AND deleted_at IS NULL) so the
planner can use them effectively; update or add new indexes using the existing
index symbols (e.g., remove idx_category_active and instead create a
composite/partial index on "category_master" that includes
("parent_category_id", "category_level", "deleted_at") or a partial index
filtering is_active, and similarly replace idx_fsr_active, idx_category_level,
idx_tfp_permission, idx_team_member_role with composite/partial indexes on their
respective tables ("folder_suggestion_rule", "category_master",
"team_folder_permission", "team_member") that reflect real WHERE clauses and
join columns.
- Around line 562-577: The legacy index CREATE statements (idx_website_url,
idx_website_category, idx_board_member, idx_board_title, idx_board_created_date,
idx_comment_board, idx_comment_member, idx_likes_board, idx_likes_member) are
currently executed after the COMMIT, breaking atomicity; move these CREATE INDEX
statements so they run inside the same transaction (i.e., place them before the
existing COMMIT or wrap them with an explicit BEGIN/COMMIT block) so the index
creation is rolled back together with the rest of the transaction if needed.
- Around line 175-182: The FK columns team_saved_link_tag.team_saved_link_id and
member_saved_link_tag.member_saved_link_id are declared as bigint while their
referenced PKs team_saved_link.team_saved_link_id and
member_saved_link.member_saved_link_id are int; make the column types consistent
by either changing the FK columns to int or promoting the referenced PKs to
bigint (choose the project-wide convention), then update the CREATE TABLE
statements for team_saved_link_tag (team_saved_link_id) and
member_saved_link_tag (member_saved_link_id) to use the matching type and re-run
migrations to ensure the foreign key constraints reference identical types.
- Around line 79-108: Add foreign key constraints linking
member_folder.owner_member_id and member_tag.owner_member_id to
member(member_id) to enforce referential integrity; specifically, in the CREATE
TABLE for "member_folder" add a constraint (e.g., CONSTRAINT
fk_member_folder_owner_member_id FOREIGN KEY ("owner_member_id") REFERENCES
"member"("member_id")) and in the CREATE TABLE for "member_tag" add a constraint
(e.g., CONSTRAINT fk_member_tag_owner_member_id FOREIGN KEY ("owner_member_id")
REFERENCES "member"("member_id")); ensure the referenced column types match and
choose the appropriate ON DELETE/ON UPDATE action (RESTRICT/NO ACTION or
CASCADE) per application semantics.
- Around line 110-152: The team tables lack foreign key constraints and use int
for user IDs while the member table uses bigint; update all user-related columns
(team.owner_user_id, team_folder.created_by_user_id,
team_folder_permission.user_id, team_member.user_id, and any
team_saved_link.user_id) to match the canonical user PK type (change to bigint
if member.member_id is bigint or to the external user table's PK type), add FK
constraints referencing the correct users table/column (e.g., REFERENCES
member(member_id) or REFERENCES external_user(id)), and normalize naming (use
*_member_id or *_user_id consistently across tables) so types and FK targets
align with the intended design; if the team system is meant to reference an
external auth table, point the FKs to that table instead and document the
decision.

In `@src/main/resources/mapper/board-mapper.xml`:
- Around line 91-102: selectBoardListByMemberId and selectBoard currently query
only the board table but reuse BoardResultMap which expects JOIN-derived fields
(nickname, job, major); update both select statements to either (A) add an
explicit JOIN to the member table and select the member fields aliased to
nickname, job, major so BoardResultMap is fully populated, or (B) create and use
a simpler ResultMap (e.g., BoardBasicResultMap) that excludes nickname/job/major
if those fields are not needed; modify the SQL in selectBoardListByMemberId and
selectBoard or change their resultMap attribute accordingly to keep mappings
consistent with selected columns.

In `@src/main/resources/mapper/bookmark-mapper.xml`:
- Around line 62-70: The selectBookmark (and selectBookmarkList) queries use
"msl.*" which causes column-name collisions with BookmarkResultMap and
LinkResultMap (shared names like primary_category_id, category_score,
created_at, etc.); fix by explicitly listing member_saved_link columns instead
of "msl.*" or by aliasing link columns (e.g., l.original_url AS
link_original_url, l.primary_category_id AS link_primary_category_id) so
BookmarkResultMap fields map only to msl columns and LinkResultMap fields map to
the aliased link columns; also update the LinkResultMap to reference the new
aliased column names to keep mappings consistent.
- Around line 52-59: The mapper is missing the SQL for
BookmarkServiceImpl.checkBookmarkExistsByUrl ->
bookmarkDao.checkBookmarkExistsByUrl(memberId, url); add a new <select
id="checkBookmarkExistsByUrl" resultType="int"> entry in bookmark-mapper.xml
that accepts parameters memberId and url and returns COUNT(*) from
member_saved_link with WHERE created_by_member_id = #{memberId} AND url = #{url}
AND deleted_at IS NULL so the DAO method can bind correctly and avoid
BindingException at runtime.

In `@src/main/resources/mapper/comment-mapper.xml`:
- Around line 53-57: The XML mapper updateComment uses a single parameterType
Comment but the DAO method updateComment(Long commentId, CommentDto commentDto)
accepts two params and lacks `@Param`, causing binding failures; fix by changing
the mapper to accept a map (parameterType="map") or remove parameterType and
ensure bindings use #{commentId} and #{commentDto.content}, and update the
CommentDao.updateComment signature to annotate parameters with
`@Param`("commentId") and `@Param`("commentDto") so MyBatis can bind commentId and
commentDto.content correctly.

In `@src/main/resources/templates/member/join.html`:
- Around line 40-43: MemberDto is missing server-side validation for the
nickName field so empty POSTs bypass checks; add the
javax.validation.constraints.@NotBlank annotation to the nickName property in
the MemberDto class (and import the annotation) so the server enforces non-empty
nicknames, and ensure any existing validation groups or DTO constructors
accommodate this constraint.

In `@src/main/resources/templates/mypage/myPage.html`:
- Around line 419-426: The front-end is sending bookmarkData.member_memberId
which doesn't match the backend DTO field createdByMemberId; update the
bookmarkData object (in the code that builds bookmarkData) to use
createdByMemberId instead of member_memberId so the server binds the value
correctly, e.g., replace the member_memberId property with createdByMemberId and
keep the rest of the properties (displayTitle, note, tag) unchanged.
- Around line 673-684: The frontend sends a tag property in updatedBookmarkData
but BookmarkDto lacks a corresponding field so Jackson ignores it; fix by adding
a private String tag; field to the server-side BookmarkDto class along with its
standard getter and setter (or annotate appropriately for Jackson), and ensure
the DTO's field name matches "tag" (alternatively remove the tag property from
updatedBookmarkData in the client); refer to updatedBookmarkData on the client
and BookmarkDto on the server when applying the change.

implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-aop'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

들여쓰기 불일치: 탭 대신 공백이 사용되었습니다.

Line 31은 공백 4칸으로 들여쓰기되어 있지만, 나머지 dependencies 블록은 탭을 사용합니다.

🔧 수정 제안
-    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
+	implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
📝 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
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
🤖 Prompt for AI Agents
In `@build.gradle` at line 31, The dependency line "implementation
'org.springframework.boot:spring-boot-starter-data-jpa'" in the dependencies
block uses 4 spaces for indentation instead of the tabs used elsewhere; update
the leading whitespace for that line to a single tab so it matches the rest of
the dependencies block formatting (look for the dependencies { ... } block and
the implementation line to correct).

@@ -0,0 +1,26 @@
version: '3.8'
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

version 키는 최신 Docker Compose에서 더 이상 사용되지 않습니다.

Docker Compose V2부터 version 필드는 무시되며, 향후 경고가 발생할 수 있습니다. 해당 라인을 제거해도 무방합니다.

🤖 Prompt for AI Agents
In `@docker-compose.yml` at line 1, docker-compose.yml에서 더 이상 사용되지 않는 `version:
'3.8'` 키를 제거하세요; Docker Compose V2는 `version` 필드를 무시하므로 해당 줄(`version: '3.8'`)을
삭제하면 되고 다른 서비스/volume/network 정의에는 영향이 없도록 파일 나머지 구조를 그대로 유지하세요.

Comment on lines +69 to 82
private Long extractTargetIdFromParams(JoinPoint joinPoint, String idParam) {
Object[] args = joinPoint.getArgs(); // 메서드 실제 인자 값 배열
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
String[] paramNames = signature.getParameterNames(); // 메서드 파라미터 이름 배열

for (int i = 0; i < paramNames.length; i++) {
if (paramNames[i].equals(idParam)) {
return Integer.parseInt(args[i].toString());
// Integer나 Long 모두 지원하도록 String으로 변환 후 parse
return Long.parseLong(args[i].toString());
}
}
log.error("{}' 파라미터를 찾을 수 없음. 실제 파라미터: {}", idParam, Arrays.toString(paramNames));
throw new IllegalArgumentException("요청 파라미터에서 ID를 찾을 수 없습니다.");
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

args[i]null일 경우 NullPointerException 발생 가능.

Line 77에서 args[i].toString()을 호출하기 전에 null 체크가 없습니다. 컨트롤러 메서드의 파라미터가 null로 전달될 수 있으며 (예: 선택적 @RequestParam), 이 경우 NPE가 발생합니다.

🛡️ 수정 제안
         for (int i = 0; i < paramNames.length; i++) {
             if (paramNames[i].equals(idParam)) {
-                // Integer나 Long 모두 지원하도록 String으로 변환 후 parse
-                return Long.parseLong(args[i].toString());
+                if (args[i] == null) {
+                    throw new IllegalArgumentException("ID 파라미터 '" + idParam + "'가 null입니다.");
+                }
+                return Long.parseLong(args[i].toString());
             }
         }
🤖 Prompt for AI Agents
In `@src/main/java/com/web/SearchWeb/aop/OwnerCheckAspect.java` around lines 69 -
82, In extractTargetIdFromParams(JoinPoint, String) add a null check for args[i]
before calling toString(): if args[i] is null, log a clear error including
idParam and paramNames and throw IllegalArgumentException (or return a safe
value if desired) instead of letting a NullPointerException propagate; also
guard Long.parseLong with try/catch(NumberFormatException) to convert bad inputs
into a controlled IllegalArgumentException with context so callers see a
descriptive error rather than an NPE or parse crash.

Comment on lines 14 to +29
public class Board {
private int boardId;
private int member_memberId;
private String nickname;
private String job;
private String major;
private String url;
private String title;
private String summary;
private String description;
private String hashtags;
private int likes_count;
private int comments_count;
private int bookmarks_count;
private int views_count;
private String created_date;
private Long boardId; // 게시글 ID (PK)
private Long memberMemberId; // 작성자 ID (FK to member - BIGINT)
private String nickname; // 작성자 닉네임
private String job; // 직업
private String major; // 전공
private String url; // 참조 URL (선택)
private String title; // 제목
private String summary; // 요약
private String description; // 본문 내용
private String hashtags; // 해시태그
private int likesCount; // 좋아요 수
private int commentsCount; // 댓글 수
private int bookmarksCount; // 북마크 수
private int viewsCount; // 조회수
private String createdDate; // 작성일
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Board도 BaseEntity를 상속하지 않아 Comment와 동일한 일관성 문제가 있습니다.

Comment와 마찬가지로 Board도 "Legacy"로 표기되어 있으며 BaseEntity를 상속하지 않고 있습니다. createdDateString 타입인 점도 동일합니다. 향후 리팩토링 시 BaseEntity 상속과 함께 타임스탬프 타입 통일을 권장합니다.

🤖 Prompt for AI Agents
In `@src/main/java/com/web/SearchWeb/board/domain/Board.java` around lines 14 -
29, Board is missing BaseEntity inheritance and uses a String createdDate like
Comment; update the Board class to extend BaseEntity (to match Comment) and
change the createdDate field type from String to the project's standard
timestamp type (e.g., Instant/LocalDateTime) to ensure consistent timestamp
handling; modify the Board constructor/getters/setters and any usages of
Board.createdDate (and refer to Board class, createdDate field, and BaseEntity)
to use the unified timestamp type and persist/serialize accordingly.

Comment on lines +91 to 102
<!-- 게시글 조회(회원번호로 조회) -->
<select id="selectBoardListByMemberId" resultMap="BoardResultMap">
SELECT * FROM board
where member_memberId = #{memberId}
WHERE member_member_id = #{memberId}
</select>


<!-- 게시글 단일 조회 -->
<select id="selectBoard" resultType="com.web.SearchWeb.board.domain.Board">
<select id="selectBoard" resultMap="BoardResultMap">
SELECT * FROM board
where boardId = #{boardId}
WHERE board_id = #{boardId}
</select>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

SELECT *로 조회 시 BoardResultMap의 JOIN 파생 필드(nickname, job, major)가 null로 남습니다.

selectBoardListByMemberIdselectBoardboard 테이블만 조회하지만 BoardResultMap을 사용합니다. 이 ResultMap에는 nickname, job, major 매핑이 있는데, 이들은 member 테이블 JOIN 시에만 값이 채워집니다. 해당 필드가 필요한 경우 JOIN을 추가하거나, 필요 없다면 별도의 간단한 ResultMap을 사용하는 것이 명확합니다.

🤖 Prompt for AI Agents
In `@src/main/resources/mapper/board-mapper.xml` around lines 91 - 102,
selectBoardListByMemberId and selectBoard currently query only the board table
but reuse BoardResultMap which expects JOIN-derived fields (nickname, job,
major); update both select statements to either (A) add an explicit JOIN to the
member table and select the member fields aliased to nickname, job, major so
BoardResultMap is fully populated, or (B) create and use a simpler ResultMap
(e.g., BoardBasicResultMap) that excludes nickname/job/major if those fields are
not needed; modify the SQL in selectBoardListByMemberId and selectBoard or
change their resultMap attribute accordingly to keep mappings consistent with
selected columns.

Comment on lines 53 to 57
<update id="updateComment" parameterType="com.web.SearchWeb.comment.domain.Comment">
update comment
set
content = #{commentDto.content}
WHERE
commentId = #{commentId}
UPDATE comment
SET content = #{commentDto.content}
WHERE comment_id = #{commentId}
</update>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

fd "CommentDao.java" -type f --exec cat {}

Repository: Searchweb-Dev/Searchweb-Back

Length of output: 242


🏁 Script executed:

cat -n src/main/resources/mapper/comment-mapper.xml | sed -n '50,65p'

Repository: Searchweb-Dev/Searchweb-Back

Length of output: 679


🏁 Script executed:

fd CommentDao --type f --exec cat {}

Repository: Searchweb-Dev/Searchweb-Back

Length of output: 2943


🏁 Script executed:

rg "updateComment" src/main --type java -A 2

Repository: Searchweb-Dev/Searchweb-Back

Length of output: 3058


updateComment 메서드의 parameterType 오류와 @Param 어노테이션 누락

CommentDao 인터페이스의 updateComment(Long commentId, CommentDto commentDto) 메서드는 두 개의 파라미터를 받지만, XML의 parameterTypeComment로 지정되어 있습니다. 또한 CommentDao 인터페이스에 @Param 어노테이션이 없으므로, MyBatis가 #{commentId}, #{commentDto.content}를 올바르게 바인딩하지 못하여 런타임 오류가 발생합니다.

수정 방법

comment-mapper.xml (라인 53):

-    <update id="updateComment" parameterType="com.web.SearchWeb.comment.domain.Comment">
+    <update id="updateComment">

CommentDao.java:

+import org.apache.ibatis.annotations.Param;
+
-    int updateComment(Long commentId, CommentDto commentDto);
+    int updateComment(`@Param`("commentId") Long commentId, `@Param`("commentDto") CommentDto commentDto);
🤖 Prompt for AI Agents
In `@src/main/resources/mapper/comment-mapper.xml` around lines 53 - 57, The XML
mapper updateComment uses a single parameterType Comment but the DAO method
updateComment(Long commentId, CommentDto commentDto) accepts two params and
lacks `@Param`, causing binding failures; fix by changing the mapper to accept a
map (parameterType="map") or remove parameterType and ensure bindings use
#{commentId} and #{commentDto.content}, and update the CommentDao.updateComment
signature to annotate parameters with `@Param`("commentId") and
`@Param`("commentDto") so MyBatis can bind commentId and commentDto.content
correctly.

Comment on lines +40 to +43
<label class="text-sm font-medium leading-none block mb-1 text-gray-700" for="nickName">닉네임</label>
<input class="flex h-10 text-sm w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-gray-900 shadow-sm placeholder-gray-400
focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500" type="text" th:field="*{nickname}" id="nickname" placeholder="닉네임" required />
<div th:if="${#fields.hasErrors('nickname')}" th:errors="*{nickname}" class="text-red-500 text-xs italic"></div>
focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500" type="text" th:field="*{nickName}" id="nickName" placeholder="닉네임" required />
<div th:if="${#fields.hasErrors('nickName')}" th:errors="*{nickName}" class="text-red-500 text-xs italic"></div>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

nickName 필드에 서버 측 @NotBlank 검증이 누락되어 있습니다.

HTML required 속성으로 클라이언트 측 검증은 되지만, MemberDto.nickName에는 @NotBlank가 없어 직접 POST 요청 시 빈 닉네임이 서버 검증을 통과할 수 있습니다. 회원가입 시 닉네임이 필수라면 MemberDto@NotBlank 어노테이션을 추가해야 합니다.

🤖 Prompt for AI Agents
In `@src/main/resources/templates/member/join.html` around lines 40 - 43,
MemberDto is missing server-side validation for the nickName field so empty
POSTs bypass checks; add the javax.validation.constraints.@NotBlank annotation
to the nickName property in the MemberDto class (and import the annotation) so
the server enforces non-empty nicknames, and ensure any existing validation
groups or DTO constructors accommodate this constraint.

Comment on lines 419 to 426
const bookmarkData = {
member_memberId: memberId,
name: name, // 웹사이트 이름
url: url, // 웹사이트 URL
description: description, // 웹사이트 설명
tag: tag // 단일 태그
displayTitle: name, // 웹사이트 이름 -> displayTitle
note: description, // 웹사이트 설명 -> note
tag: tag, // 단일 태그
// memberFolderId: null, // 백엔드에서 처리하거나, 나중에 폴더 선택 기능 추가 시 설정
// primaryCategoryId: 1 // 기본 카테고리
};
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

DTO 필드명 불일치: member_memberIdBookmarkDto에 존재하지 않습니다.

BookmarkDto의 관련 코드 스니펫을 보면 createdByMemberId 필드를 사용합니다. member_memberId로 전송하면 서버에서 해당 값이 바인딩되지 않아 createdByMemberIdnull이 됩니다.

🐛 수정 제안
         const bookmarkData = {
-            member_memberId: memberId,
+            createdByMemberId: memberId,
             displayTitle: name,
             note: description,
             tag: tag,
         };
🤖 Prompt for AI Agents
In `@src/main/resources/templates/mypage/myPage.html` around lines 419 - 426, The
front-end is sending bookmarkData.member_memberId which doesn't match the
backend DTO field createdByMemberId; update the bookmarkData object (in the code
that builds bookmarkData) to use createdByMemberId instead of member_memberId so
the server binds the value correctly, e.g., replace the member_memberId property
with createdByMemberId and keep the rest of the properties (displayTitle, note,
tag) unchanged.

Comment on lines +673 to +684
const memberFolderId = document.getElementById('updateMemberFolderId').value;
const primaryCategoryId = document.getElementById('updatePrimaryCategoryId').value;

// 수정된 북마크 데이터 생성
const updatedBookmarkData = {
name: name,
url: url,
description: description,
tag: tag
displayTitle: name,
url: url, // DTO has url field
note: description,
tag: tag,
memberFolderId: memberFolderId ? parseInt(memberFolderId) : null,
primaryCategoryId: primaryCategoryId ? parseInt(primaryCategoryId) : 1,
createdByMemberId: memberId // DTO 필드
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find BookmarkDto class definition
fd -t f "BookmarkDto.java" --exec cat -n {} \;

Repository: Searchweb-Dev/Searchweb-Back

Length of output: 1062


🏁 Script executed:

#!/bin/bash
# Search for tag field references in DTO files
rg "class BookmarkDto|private.*tag|public.*tag" -A 5 -B 2

Repository: Searchweb-Dev/Searchweb-Back

Length of output: 5655


🏁 Script executed:

#!/bin/bash
# Search for any mapping or JSON serialization related to BookmarkDto
rg "BookmarkDto" -t java -A 10 -B 2 | head -100

Repository: Searchweb-Dev/Searchweb-Back

Length of output: 11178


BookmarkDto에 tag 필드가 없어 프론트엔드에서 전송한 데이터가 무시됩니다.

JavaScript 코드에서 tag 필드를 updatedBookmarkData 객체에 포함시켜 서버로 전송하고 있으나, BookmarkDto 클래스에는 해당 필드가 없습니다. 결과적으로 Jackson 역직렬화 시 이 필드가 무시되어 데이터 손실이 발생합니다. 다음 중 하나로 처리하세요:

  • BookmarkDto에 private String tag; 필드를 추가
  • 또는 프론트엔드에서 tag 필드 전송 제거

parseInt 사용은 일반적인 ID 범위 내에서는 문제없습니다.

🤖 Prompt for AI Agents
In `@src/main/resources/templates/mypage/myPage.html` around lines 673 - 684, The
frontend sends a tag property in updatedBookmarkData but BookmarkDto lacks a
corresponding field so Jackson ignores it; fix by adding a private String tag;
field to the server-side BookmarkDto class along with its standard getter and
setter (or annotate appropriately for Jackson), and ensure the DTO's field name
matches "tag" (alternatively remove the tag property from updatedBookmarkData in
the client); refer to updatedBookmarkData on the client and BookmarkDto on the
server when applying the change.

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: 36

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (19)
src/main/resources/mapper/main-mapper.xml (1)

24-33: ⚠️ Potential issue | 🟡 Minor

MyBatis 매퍼 내 SQL 끝에 세미콜론(;)을 제거하세요.

Line 32에 세미콜론이 있는데, 같은 파일의 다른 쿼리(selectWebsite, getListByCategory)에는 세미콜론이 없어 일관성이 없습니다. MyBatis 매퍼 XML 내 SQL에 세미콜론을 붙이면, 일부 JDBC 드라이버에서 오류가 발생하거나 동적 SQL 조합 시 문제가 될 수 있습니다.

🔧 수정 제안
         CASE WHEN LOWER(name) LIKE CONCAT('%', LOWER(#{query}), '%') THEN 1
-        ELSE 2  END;
+        ELSE 2  END
src/main/java/com/web/SearchWeb/config/SecurityConfig.java (1)

79-80: 🧹 Nitpick | 🔵 Trivial

CSRF가 비활성화되어 있습니다.

csrf.disable()는 세션 기반 인증 환경에서 CSRF 공격에 취약할 수 있습니다. 현재 PR 범위는 아니지만, 프로덕션 배포 전 CSRF 보호를 활성화하는 것을 권장합니다. Thymeleaf 템플릿 사용 시 _csrf 토큰 자동 삽입이 지원됩니다.

src/main/java/com/web/SearchWeb/bookmark/dto/BookmarkCheckDto.java (1)

8-14: ⚠️ Potential issue | 🟠 Major

@NoArgsConstructor 누락으로 인한 런타임 오류 가능성

@AllArgsConstructor만 선언하면 Java의 기본 no-arg 생성자가 억제됩니다. MyBatis 등 프레임워크에서 리플렉션을 통해 객체를 생성할 때 NoSuchMethodException이 발생할 수 있습니다.

🐛 수정 제안
+import lombok.NoArgsConstructor;
+
 `@Setter`
 `@Getter`
 `@ToString`
+@NoArgsConstructor
 `@AllArgsConstructor`
 public class BookmarkCheckDto {
src/main/java/com/web/SearchWeb/member/service/CustomerUserDetailService.java (1)

31-38: ⚠️ Potential issue | 🟠 Major

loadUserByUsername에서 사용자를 찾지 못할 경우 null 반환은 위험합니다.

Spring Security의 UserDetailsService 계약에 따르면, 사용자를 찾지 못한 경우 UsernameNotFoundException을 던져야 합니다. null을 반환하면 인증 흐름에서 NullPointerException이 발생할 수 있습니다.

🐛 수정 제안
     `@Override`
     public UserDetails loadUserByUsername(String loginId) throws UsernameNotFoundException {
         Member findUser = mybatisMemberDao.findByLoginId(loginId);
         if(findUser != null){
             //spring security에 전달해서 검증
             return new CustomUserDetails(findUser);
         }
-        return null;
+        throw new UsernameNotFoundException("사용자를 찾을 수 없습니다: " + loginId);
     }
src/main/java/com/web/SearchWeb/member/service/MemberServiceImpl.java (1)

83-95: 🧹 Nitpick | 🔵 Trivial

댓글 프로필 업데이트가 N+1 쿼리 패턴으로 되어 있습니다.

모든 댓글을 조회한 후 개별적으로 업데이트하는 대신, memberId 기준으로 일괄 업데이트하는 단일 쿼리를 사용하면 성능을 크게 개선할 수 있습니다.

src/main/java/com/web/SearchWeb/member/dto/CustomOAuth2User.java (1)

62-71: 🧹 Nitpick | 🔵 Trivial

getAuthorities() 익명 클래스를 람다로 간소화할 수 있습니다.

GrantedAuthority는 함수형 인터페이스이므로 람다 표현으로 대체하면 코드가 간결해집니다.

♻️ 제안
     `@Override`
     public Collection<? extends GrantedAuthority> getAuthorities() {
         Collection<GrantedAuthority> collection = new ArrayList<>();
-        collection.add(new GrantedAuthority() {
-            `@Override`
-            public String getAuthority() {
-                return role;
-            }
-        });
+        collection.add(() -> role);
         return collection;
     }
src/main/resources/templates/mypage/myPage.html (3)

536-577: ⚠️ Potential issue | 🔴 Critical

XSS 취약점: innerHTML에 사용자 입력값을 이스케이프 없이 직접 삽입하고 있습니다.

displayTitle, note, url 등 사용자 입력 데이터가 템플릿 리터럴을 통해 innerHTML에 직접 삽입됩니다. 악의적인 사용자가 북마크 이름에 <img src=x onerror=alert(document.cookie)> 같은 값을 저장하면, 해당 북마크를 렌더링할 때 스크립트가 실행됩니다.

textContent를 사용하거나, HTML 이스케이프 함수를 적용해야 합니다.

🛡️ 이스케이프 유틸 함수 추가 제안
+    function escapeHtml(str) {
+        if (!str) return '';
+        const div = document.createElement('div');
+        div.textContent = str;
+        return div.innerHTML;
+    }
+
     function addBookmarkToUI(bookmark) {
         const { bookmarkId, displayTitle, note } = bookmark;
         const url = bookmark.link ? bookmark.link.originalUrl : '';
         // ...
         // 템플릿 리터럴 내에서 사용 시:
-                <h1 class="text-xs font-bold truncate">${displayTitle}</h1>
+                <h1 class="text-xs font-bold truncate">${escapeHtml(displayTitle)}</h1>
         // ...
-            <p class="text-xs text-gray-500 dark:text-gray-400" style="font-size: 0.66rem;">${note || ''}</p>
+            <p class="text-xs text-gray-500 dark:text-gray-400" style="font-size: 0.66rem;">${escapeHtml(note)}</p>

url 값도 href 속성에 삽입되므로 javascript: 프로토콜 주입 방지를 위한 URL 검증이 필요합니다.


690-690: 🧹 Nitpick | 🔵 Trivial

HTTP 메서드 'Put'은 대문자 'PUT'으로 통일하는 것이 좋습니다.

jQuery가 내부적으로 대소문자를 처리하므로 동작에는 문제가 없지만, Line 776의 프로필 수정에서는 'PUT'을 사용하고 있어 일관성이 떨어집니다. 주석의 'Patch'도 정리가 필요합니다.

♻️ 제안
-            type: 'Put', //type: 'Patch',
+            type: 'PUT',

359-362: 🧹 Nitpick | 🔵 Trivial

window.onloadwindow.addEventListener('load', ...)가 중복 실행됩니다.

Line 359의 window.onload에서 loadBookmarks("All", "Oldest")를 호출하고, Line 394의 load 이벤트 리스너에서 loadTags()를 호출합니다. 두 핸들러가 동일 이벤트에 등록되어 있으나, window.onload 할당은 이전에 등록된 onload 핸들러를 덮어쓸 수 있습니다. 하나의 이벤트 리스너로 통합하는 것을 권장합니다.

♻️ 통합 제안
-    window.onload = function() {
-        window.memberId = [[${`#authentication.principal.memberId`}]];
-        loadBookmarks("All", "Oldest");
-    };
     // ...
-    window.addEventListener('load', function() {
-        loadTags();
-    });
+    window.addEventListener('load', function() {
+        window.memberId = [[${`#authentication.principal.memberId`}]];
+        loadTags();
+        loadBookmarks("All", "Oldest");
+    });
src/main/java/com/web/SearchWeb/member/dao/MybatisMemberDao.java (1)

16-16: 🧹 Nitpick | 🔵 Trivial

mapper 필드를 final로 선언하는 것을 권장합니다.

동일 패턴의 MybatisLikesDao에서는 mapperfinal로 선언되어 있습니다. 생성자에서만 할당되고 이후 변경되지 않으므로 불변성을 보장하는 것이 좋습니다.

♻️ 제안
-    private MemberDao mapper;
+    private final MemberDao mapper;
src/main/java/com/web/SearchWeb/comment/dao/MybatisCommentDao.java (1)

40-90: 🛠️ Refactor suggestion | 🟠 Major

인터페이스 메서드에 @Override 어노테이션이 누락되어 있습니다.

insertComment (Line 32)에만 @Override가 있고, selectComments, selectCommentsByMemberId, selectComment, updateComment, deleteComment, countComments 메서드에는 @Override가 빠져 있습니다. 인터페이스 구현 메서드에 @Override를 붙이면 시그니처 불일치를 컴파일 타임에 감지할 수 있습니다.

특히 이번 PR처럼 파라미터 타입이 intLong으로 변경되는 경우, @Override가 없으면 인터페이스 시그니처와 불일치해도 컴파일 에러 없이 새 메서드로 인식될 수 있어 런타임 버그의 원인이 됩니다.

♻️ `@Override` 추가 예시
     /**
      *  게시글 댓글 목록 조회
      */
+    `@Override`
     public List<Comment> selectComments(Long boardId){
         return mapper.selectComments(boardId);
     }

나머지 selectCommentsByMemberId, selectComment, updateComment, deleteComment, countComments 메서드에도 동일하게 적용해 주세요.

src/main/java/com/web/SearchWeb/member/service/CustomOAuth2UserService.java (1)

99-105: 🧹 Nitpick | 🔵 Trivial

업데이트 경로에서 setLoginId(loginId) 호출이 불필요합니다.

Line 83에서 findByLoginId(loginId)로 이미 조회한 멤버이므로 Line 101의 existMember.setLoginId(loginId)는 동일한 값을 재설정하는 것입니다. 불필요한 필드 변경은 MyBatis UPDATE 쿼리에서 불필요한 쓰기를 유발할 수 있습니다.

♻️ 수정 제안
         else{
-            existMember.setLoginId(loginId);
             existMember.setEmail(oAuth2Response.getEmail());
             role = existMember.getRole();
             memberId = existMember.getMemberId();
             memberDao.updateSocialMember(memberId, existMember);
         }
src/main/java/com/web/SearchWeb/comment/controller/CommentApiController.java (1)

46-51: ⚠️ Potential issue | 🟡 Minor

익명 사용자 검사 로직이 취약합니다.

"anonymousUser".equals(currentUser)는 Spring Security의 구현 세부사항에 의존하고 있습니다. @AuthenticationPrincipal로 주입된 ObjectString 타입 "anonymousUser"인 경우에만 작동하며, Security 설정이 변경되면 깨질 수 있습니다.

🛠️ 수정 제안
-        if (currentUser == null || "anonymousUser".equals(currentUser)) {
+        if (currentUser == null || currentUser instanceof String) {

또는 @AuthenticationPrincipal 대신 Authentication 객체를 직접 사용하는 방식이 더 견고합니다:

`@AuthenticationPrincipal` Object currentUser

Authentication authentication

authentication == null || authentication instanceof AnonymousAuthenticationToken으로 검사.

src/main/java/com/web/SearchWeb/comment/service/CommentService.java (2)

37-53: ⚠️ Potential issue | 🟠 Major

memberService.findByLoginId(loginId) 결과가 null일 경우 NPE 발생 가능

Line 39에서 findByLoginId의 반환값에 대한 null 체크 없이 Line 42~45에서 member.getMemberId(), member.getNickName() 등을 바로 호출합니다. 존재하지 않는 loginId가 전달되면 NullPointerException이 발생합니다.

🛡️ 수정 제안
         Member member = memberService.findByLoginId(loginId);
+        if (member == null) {
+            throw new IllegalArgumentException("회원이 존재하지 않습니다: " + loginId);
+        }
         Comment comment = new Comment();

83-91: ⚠️ Potential issue | 🟠 Major

댓글 삭제 실패 시에도 댓글 수가 감소합니다.

commentdao.deleteComment(commentId)의 반환값(result)이 0인 경우(삭제된 행 없음)에도 boardDao.decrementCommentCount(boardId)가 무조건 실행되어, 댓글 수 불일치가 발생할 수 있습니다.

🐛 수정 제안
     public int deleteComment(Long boardId, Long commentId){
         // 댓글 삭제
         int result = commentdao.deleteComment(commentId);
 
-        //게시글 댓글 수 감소
-        boardDao.decrementCommentCount(boardId);
+        //게시글 댓글 수 감소 (삭제 성공 시에만)
+        if (result > 0) {
+            boardDao.decrementCommentCount(boardId);
+        }
         return result;
     }
src/main/java/com/web/SearchWeb/board/service/BoardService.java (2)

106-126: ⚠️ Potential issue | 🟠 Major

incrementBookmarkCount/decrementBookmarkCount에 TOCTOU 경쟁 조건이 있습니다.

selectBoardgetBookmarksCountupdateBookmarkCount(boardId, count ± 1) 패턴은 read-modify-write 경쟁 조건을 가지고 있습니다. 동시 요청 시 카운트 갱신이 유실될 수 있습니다. 같은 파일의 incrementViewCount, incrementLikeCount 등은 SQL에서 원자적으로 증감하는데, 북마크 카운트만 Java 레벨에서 처리하고 있어 일관성이 없습니다.

또한 decrementBookmarkCount에서 카운트가 0 미만이 되는 것에 대한 방어 로직이 없습니다.

♻️ 수정 제안 — SQL 레벨 원자적 증감 사용

BoardDao에 incrementBookmarkCount(Long boardId) / decrementBookmarkCount(Long boardId) 메서드를 추가하고, SQL에서 직접 SET bookmarks_count = bookmarks_count + 1(또는 GREATEST(bookmarks_count - 1, 0))을 수행하도록 변경하는 것을 권장합니다.

     public void incrementBookmarkCount(Long boardId) {
-        Board board = boardDao.selectBoard(boardId);
-        if (board != null) {
-            boardDao.updateBookmarkCount(boardId, board.getBookmarksCount() + 1);
-        } else {
-            throw new IllegalArgumentException("Invalid board ID");
-        }
+        int affected = boardDao.incrementBookmarkCount(boardId);
+        if (affected == 0) {
+            throw new IllegalArgumentException("Invalid board ID");
+        }
     }
 
     public void decrementBookmarkCount(Long boardId) {
-        Board board = boardDao.selectBoard(boardId);
-        if (board != null) {
-            boardDao.updateBookmarkCount(boardId, board.getBookmarksCount() - 1);
-        } else {
-            throw new IllegalArgumentException("Invalid board ID");
-        }
+        int affected = boardDao.decrementBookmarkCount(boardId);
+        if (affected == 0) {
+            throw new IllegalArgumentException("Invalid board ID");
+        }
     }

69-84: ⚠️ Potential issue | 🟠 Major

selectBoard에서 board가 null인 경우 NPE 발생 가능

Line 74에서 boardDao.selectBoard(boardId)가 null을 반환할 수 있으며, Line 77의 board.getHashtags() 호출에서 NullPointerException이 발생합니다. 또한 Line 72의 incrementViewCount가 존재하지 않는 게시글에 대해 먼저 실행됩니다.

🛡️ 수정 제안
     public Map<String, Object> selectBoard(Long boardId) {
-        // 조회수 증가
-        boardDao.incrementViewCount(boardId);
-
         Board board = boardDao.selectBoard(boardId);
+        if (board == null) {
+            throw new IllegalArgumentException("게시글이 존재하지 않습니다: " + boardId);
+        }
+
+        // 조회수 증가
+        boardDao.incrementViewCount(boardId);
 
         String[] hashtagsList = board.getHashtags() != null ? board.getHashtags().split(" ") : new String[0];
src/main/java/com/web/SearchWeb/board/controller/BoardController.java (2)

83-94: 🧹 Nitpick | 🔵 Trivial

인증 사용자에서 memberId 추출 로직이 중복됩니다.

boardDetailtoggleLike에서 currentUser로부터 memberId를 추출하는 동일한 instanceof 체인이 반복됩니다. BookmarkApiController에는 이미 getMemberId(Object currentUser) 헬퍼가 있으므로, 이 패턴을 공통 유틸리티 또는 이 컨트롤러의 private 메서드로 추출하세요.

Also applies to: 163-174


200-206: ⚠️ Potential issue | 🟠 Major

toggleBookmark에서 @AuthenticationPrincipal이 있지만, memberId@PathVariable에서 받고 있습니다.

memberId@PathVariable로부터 받아 BookmarkDto에 사용되는데, 인증된 사용자의 실제 memberId와 일치하는지 검증이 없습니다. 공격자가 다른 사용자의 memberId를 URL에 넣어 요청을 보내면 다른 사용자 명의로 북마크가 생성/삭제될 수 있습니다.

@PathVariable memberId 대신 @AuthenticationPrincipal에서 추출한 memberId를 사용하거나, 최소한 두 값이 일치하는지 검증해야 합니다.

🤖 Fix all issues with AI agents
In `@docker-compose.yml`:
- Around line 3-19: The postgres service (container_name searchweb-db) is
missing a Docker healthcheck; add a healthcheck section under the postgres
service that runs a sensible readiness probe (e.g., pg_isready or equivalent)
using the container's POSTGRES_USER/POSTGRES_DB, and set interval, timeout,
retries and start_period so other services wait until the DB is ready; update
the docker-compose postgres service to include this healthcheck section and
ensure restart: always stays in place so Docker can manage restarts based on
health status.

In `@src/main/java/com/web/SearchWeb/board/controller/BoardController.java`:
- Line 114: The `@GetMapping` annotation in BoardController (the mapping currently
written as "api/boards") is missing the leading slash; update the mapping value
in the BoardController class (the `@GetMapping` on the method handling boards)
from "api/boards" to "/api/boards" so it matches the other endpoints like
"/board" and "/board/{boardId}" and preserves routing consistency.

In `@src/main/java/com/web/SearchWeb/board/dao/BoardDao.java`:
- Line 12: BoardDao 인터페이스의 다중 파라미터 메서드들에서 MyBatis 매퍼의 명시적 이름 바인딩(#{memberId},
#{boardId}, #{job}, #{query}, #{postType} 등)을 사용하고 있으므로 각 파라미터에 `@Param` 어노테이션을
추가하세요; 구체적으로 insertBoard(Long memberId, BoardDto boardDto), updateBoard(...),
updateBoardProfile(...), updateBookmarkCount(...), countBoardList(String query,
String postType)의 모든 파라미터에 `@Param을` 붙이고 각 어노테이션의 값은 매퍼에서 사용하는 이름(예: "memberId",
"boardId", "job", "query", "postType")과 동일하게 지정해 BindingException이 발생하지 않도록
수정하세요.

In
`@src/main/java/com/web/SearchWeb/bookmark/controller/BookmarkApiController.java`:
- Around line 179-186: Extract the duplicated instanceof chain into a shared
utility by creating a static helper like AuthUtils.getMemberId(Object
currentUser) (or add protected getMemberId in a BaseController) that returns the
memberId for CustomUserDetails and CustomOAuth2User and null otherwise; then
replace the existing private Long getMemberId(Object) in BookmarkApiController
and the inline chain in BoardController to call AuthUtils.getMemberId(...) (or
the BaseController method), update imports, and run tests to ensure no
behavioral changes.
- Around line 135-173: The updateBookmark and deleteBookmark handlers are
missing the unauthenticated-user check used elsewhere; add the same pre-check
used in insertBookmark/selectBookmarkList/selectBookmark (if currentUser == null
|| "anonymousUser".equals(currentUser)) and return
ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(response) before calling
getMemberId, keeping the subsequent memberId null guard as a fallback; update
both methods (updateBookmark, deleteBookmark) to perform this check so
unauthenticated requests return 401 instead of 403.
- Around line 40-52: The checkBookmark method calls getMemberId(currentUser)
without validating its return; add the same null check pattern used in
insertBookmark/selectBookmarkList: after Long memberId =
getMemberId(currentUser) verify memberId != null and if null return
ResponseEntity.status(HttpStatus.FORBIDDEN).build(); then proceed to call
bookmarkService.checkBookmarkExistsByUrl(memberId, url) so null is never passed
into that service.

In `@src/main/java/com/web/SearchWeb/bookmark/dao/BookmarkDao.java`:
- Around line 10-39: The mapper is missing two DAO methods and one
parameterType: update bookmark-mapper.xml to (1) add parameterType="map" to the
existing checkBookmarkExists mapping so MyBatis can bind the Long memberId,
folderId and linkId parameters (reference: checkBookmarkExists), and (2) add two
new SQL mappings: an <update> for deleteBookmarkByLink that performs a
soft-delete on member_saved_link by setting deleted_at=now() and
deleted_by_member_id=#{memberId} where link_id=#{linkId} and
created_by_member_id=#{memberId} and deleted_at IS NULL (reference:
deleteBookmarkByLink), and a <select> for checkBookmarkExistsByUrl that joins
member_saved_link and link to COUNT(*) where created_by_member_id=#{memberId}
and link.canonical_url=#{url} and deleted_at IS NULL (reference:
checkBookmarkExistsByUrl); ensure both new statements use parameterType="map"
and resultType="int" for the SELECT.

In `@src/main/java/com/web/SearchWeb/bookmark/dao/MybatisBookmarkDao.java`:
- Around line 107-110: Add a MyBatis mapper entry for the DAO method
deleteBookmarkByLink: in bookmark-mapper.xml add an <update> mapping with
id="deleteBookmarkByLink" and parameterType="map" that performs a soft delete on
member_saved_link by setting deleted_at = now() and deleted_by_member_id =
#{memberId} where link_id = #{linkId} AND created_by_member_id = #{memberId} AND
deleted_at IS NULL so mapper.deleteBookmarkByLink(memberId, linkId) resolves at
runtime without BindingException.
- Around line 36-39: The MyBatis mapping for the DAO method
MybatisBookmarkDao.checkBookmarkExistsByUrl is missing; add a <select> entry
with id "checkBookmarkExistsByUrl" to
src/main/resources/mapper/bookmark-mapper.xml that returns an integer (count)
and accepts the two parameters used by the mapper call (memberId and url) so
mapper.checkBookmarkExistsByUrl(memberId, url) can resolve; the SQL should count
matching rows in the bookmarks table using member_id = #{memberId} and url =
#{url} and set resultType to an integer type to match the method signature.

In `@src/main/java/com/web/SearchWeb/bookmark/domain/Bookmark.java`:
- Around line 19-31: Remove the unused DTO class BookmarkCheckDto: delete the
BookmarkCheckDto class and its file, remove any imports/usages of
BookmarkCheckDto across the codebase (search for BookmarkCheckDto and
website_websiteId), and ensure no remaining code references the non-existent
website_websiteId field; keep BoardBookmarkCheckDto intact and run a full
build/tests to confirm no compile errors or test failures after removal.

In `@src/main/java/com/web/SearchWeb/bookmark/domain/Link.java`:
- Around line 1-37: Add Lombok equality based on linkId so Link behaves
correctly in collections: annotate the class Link with
`@EqualsAndHashCode`(onlyExplicitlyIncluded = true, callSuper = false) and mark
the linkId field with `@EqualsAndHashCode.Include` so equals/hashCode use only
linkId and do not include BaseEntity fields.

In `@src/main/java/com/web/SearchWeb/bookmark/service/BookmarkServiceImpl.java`:
- Around line 101-115: insertBookmark currently calls getOrCreateLink directly
on the same instance so Spring AOP transactional annotations on getOrCreateLink
won't be applied when invoked internally; to fix, call getOrCreateLink via the
Spring proxy instead (e.g., inject the service proxy into BookmarkServiceImpl as
a BookmarkService selfProxy or obtain it via AopContext.currentProxy()) and
replace direct calls to getOrCreateLink(...) with
selfProxy.getOrCreateLink(...), ensuring BookmarkServiceImpl implements
BookmarkService and that getOrCreateLink remains `@Transactional` (or update its
propagation as needed).
- Line 136: 파일의 BookmarkServiceImpl 클래스에서 해당 메서드의 Javadoc 시작 토큰 `/**` 들여쓰기가 다른
메서드들과 맞지 않습니다; `/**` 앞에 공백 하나를 추가해 기존 Javadoc 블록들과 수직 정렬을 맞추세요 (검색 키워드:
BookmarkServiceImpl 클래스의 Javadoc 시작 토큰 `/**`).
- Around line 139-152: normalizeUrl currently assumes URI.getScheme()/getHost()
exist and drops ports; update normalizeUrl to handle missing scheme/host and
preserve ports by: if uri.getScheme() or uri.getHost() is null, attempt
reparsing by prepending "http://" to the input and creating a new URI; build the
normalized string from uri.getScheme(), uri.getHost(), and if uri.getPort() > -1
append ":" + uri.getPort(); keep the path if present and trim trailing slash; if
reparsing still fails or host is absent, return the original url to avoid
producing "null://null".
- Around line 86-95: The method checkBookmarkExistsByUrl currently fetches a
Link via normalizeUrl and bookmarkDao.selectLinkByCanonicalUrl but then ignores
it; update the logic so that if link != null you call the DAO using
link.getLinkId() (e.g., bookmarkDao.checkBookmarkExistsByLinkId(memberId,
link.getLinkId()) or the existing checkBookmarkExistsByUrl overload that accepts
a linkId), otherwise fall back to checking by the original URL/canonicalUrl;
modify the DAO call in BookmarkServiceImpl.checkBookmarkExistsByUrl accordingly
and ensure you reference normalizeUrl, bookmarkDao.selectLinkByCanonicalUrl,
link.getLinkId(), and checkBookmarkExistsByUrl/checkBookmarkExistsByLinkId to
locate and change the call.
- Around line 168-208: The four legacy methods (checkBoardBookmark,
isBookmarked, insertBookmarkForBoard, deleteBookmarkBoard) currently return
defaults and cause bookmark-count drift when controller still calls
boardservice.incrementBookmarkCount(boardId)/decrementBookmarkCount(boardId);
decide one of two fixes: (A) Implement real persistence so
checkBoardBookmark/isBookmarked query the board-bookmark repository and
insertBookmarkForBoard/deleteBookmarkBoard create/delete rows and return success
codes, and only call
boardservice.incrementBookmarkCount(boardId)/decrementBookmarkCount(boardId)
from the controller after those methods confirm success; or (B) if
board-bookmark is intentionally removed, make these methods throw
UnsupportedOperationException (or return a clear failure) and update the
controller to stop calling
boardservice.incrementBookmarkCount/decrementBookmarkCount (or guard those calls
behind the persistence result) to avoid count changes without stored bookmarks;
locate the logic in methods named checkBoardBookmark, isBookmarked,
insertBookmarkForBoard, deleteBookmarkBoard and the matching controller calls to
boardservice.incrementBookmarkCount/decrementBookmarkCount to apply the chosen
fix.
- Around line 56-79: getOrCreateLink can race: between
bookmarkDao.selectLinkByCanonicalUrl and bookmarkDao.insertLink another
transaction may insert the same canonical URL causing a
DataIntegrityViolationException and failing the whole operation, and
normalizeUrl can emit invalid values (e.g., "null://") for inputs missing
scheme/host; fix by catching DataIntegrityViolationException around
bookmarkDao.insertLink in getOrCreateLink and on catch re-query via
bookmarkDao.selectLinkByCanonicalUrl and return the found Link (or rethrow if
still absent), or replace insertLink with an upsert using INSERT ... ON CONFLICT
DO NOTHING/RETURNING at the DAO level; also add validation in normalizeUrl to
require/normalize a scheme and non-null host (or reject invalid inputs) before
building canonicalUrl so getOrCreateLink never uses malformed canonical URLs.

In `@src/main/java/com/web/SearchWeb/comment/dao/CommentDao.java`:
- Around line 9-11: The Javadoc on the CommentDao interface uses an ambiguous
"(Legacy - PostgreSQL)" tag; update the Javadoc for the CommentDao interface
comment to clearly indicate the intended meaning (e.g., replace "(Legacy -
PostgreSQL)" with "(MyBatis-based DAO - PostgreSQL)" or another accurate
descriptor) so readers know this refers to the implementation style rather than
PostgreSQL being legacy.

In `@src/main/java/com/web/SearchWeb/comment/service/CommentService.java`:
- Around line 105-109: The error message in
CommentService.findMemberIdByCommentId is incorrect: when
commentdao.selectComment(commentId) returns null you should throw an
IllegalArgumentException with the message "댓글이 존재하지 않습니다." instead of "게시글이 존재하지
않습니다."; update the exception text in the null-check block of
findMemberIdByCommentId so it correctly references "댓글".

In `@src/main/java/com/web/SearchWeb/common/domain/BaseEntity.java`:
- Line 37: The BaseEntity class currently implements Serializable but lacks a
serialVersionUID; add a private static final long serialVersionUID field to
BaseEntity (e.g., serialVersionUID = 1L or a generated value) to ensure stable
serialization compatibility across JVMs and class changes; update the class
declaration in BaseEntity to include this field and commit the change so
deserialization issues are avoided.
- Around line 36-37: The `@SQLRestriction`("deleted_at IS NULL") on BaseEntity
causes the soft-delete filter to be inherited by every JPA entity; remove this
annotation from BaseEntity and instead apply `@SQLRestriction` only to the
specific entity classes that should always hide deleted rows (e.g., Member,
Bookmark) or create a dedicated `@MappedSuperclass` (e.g., NonDeletedEntity)
annotated with `@SQLRestriction` and have only the non-deleted-default entities
extend it; update any administrative/recovery services to use entities without
that restriction or explicit native queries that include deleted rows as needed
and keep MyBatis mappers unchanged.

In `@src/main/java/com/web/SearchWeb/member/domain/Member.java`:
- Line 22: Replace the String status field in Member with a typesafe enum (e.g.,
create enum MemberStatus { ACTIVE, BLOCKED, ... }) and change the Member.status
field type to MemberStatus; annotate with `@Enumerated`(EnumType.STRING) if JPA
persistence is used, update any getters/setters and constructors to use
MemberStatus, and update all usages (notably
CustomUserDetails.isAccountNonLocked()) to compare against MemberStatus.BLOCKED
(or MemberStatus.ACTIVE) instead of string literals to avoid typos and enable
compile-time checks.
- Around line 9-12: The Member class's Lombok `@ToString` is missing callSuper =
true so BaseEntity fields (e.g., createdAt, updatedAt, deletedAt) are omitted
from toString(); update the class annotation on Member to use
`@ToString`(callSuper = true) so the BaseEntity auditing fields are included in
Member.toString() while keeping the existing `@Getter/`@Setter annotations.

In `@src/main/java/com/web/SearchWeb/member/service/CustomOAuth2UserService.java`:
- Line 94: The current code in CustomOAuth2UserService unconditionally sets
every new member's nickname with member.setNickName("닉네임"); — change this to
derive a sensible default from the OAuth2 provider and user attributes instead:
read the registrationId via
userRequest.getClientRegistration().getRegistrationId() and a unique attribute
(e.g., oAuth2User.getAttribute("name") or "login"/"id" depending on provider)
and set member.setNickName(...) to something like "<provider>_<uniqueAttr>" with
a fallback to a generated value (UUID or timestamp) if the attribute is missing.
- Line 92: Replace the insecure plain-text assignment
member.setPasswordHash("1111") in CustomOAuth2UserService with a BCrypt-encoded
random value: inject a Spring PasswordEncoder into CustomOAuth2UserService
(constructor or `@Autowired`), generate a random UUID string for social-only
accounts and call passwordEncoder.encode(uuidString) and set that result via
member.setPasswordHash(...), ensuring the NOT NULL constraint is satisfied while
storing a proper BCrypt hash rather than a plaintext password.

In `@src/main/java/com/web/SearchWeb/mypage/controller/MyPageController.java`:
- Around line 73-83: The insertBookmark method currently mixes `@RequestBody`
BookmarkDto and `@RequestParam` url; change it to accept the URL as part of the
request body instead: add a url field to BookmarkDto (or create a new wrapper
DTO like BookmarkRequest containing BookmarkDto properties plus url), update the
controller method insertBookmark to take only that DTO from `@RequestBody` (remove
`@RequestParam` String url) and set the createdByMemberId on the DTO, then call
bookmarkService.insertBookmark with the DTO (or pass the extracted url from the
wrapper) so the API follows REST conventions and clients can POST a single JSON
body; update usages of bookmarkService.insertBookmark to the new signature if
needed.
- Around line 118-125: The updateBookmark method is missing a call to set the
DTO's creator ID, so updated_by_member_id ends up null; before calling
bookmarkService.updateBookmark(bookmarkDto, bookmarkId) in
MyPageController.updateBookmark, call bookmarkDto.setCreatedByMemberId(memberId)
(same pattern used in insertBookmark and BookmarkApiController.updateBookmark)
so the mapper's updated_by_member_id = #{bookmarkDto.createdByMemberId} is
populated correctly.

In `@src/main/resources/db/init_postgres.sql`:
- Around line 337-349: Add a foreign key constraint for the nullable column
"member_saved_link_id" on the table "link_enrichment_feedback": create a
constraint (e.g. fk_link_enrichment_feedback_member_saved_link_id) that
references "member_saved_link"("member_saved_link_id") and choose an appropriate
referential action for deletes/updates (suggestion: ON DELETE SET NULL to
preserve nullability); update the CREATE TABLE block for
"link_enrichment_feedback" to include this FK constraint alongside the existing
ones.
- Around line 27-51: Add foreign key constraints that enforce referential
integrity between the various primary/category id columns and the
category_master table: alter the CREATE TABLE definitions for "link" (column
primary_category_id), "team_saved_link" (primary_category_id),
"member_saved_link" (primary_category_id) and "folder_suggestion_rule"
(category_id) to include CONSTRAINTs referencing category_master(category_id)
(e.g., CONSTRAINT fk_link_primary_category FOREIGN KEY (primary_category_id)
REFERENCES category_master(category_id) ON UPDATE CASCADE ON DELETE RESTRICT or
your project's chosen actions); ensure constraint names are unique
(fk_link_primary_category, fk_team_saved_link_primary_category,
fk_member_saved_link_primary_category, fk_folder_suggestion_rule_category) and
that nullable/NOT NULL definitions align with desired behavior.

In `@src/main/resources/mapper/board-mapper.xml`:
- Around line 118-125: The MyBatis update mapped by updateBoardProfile is
attempting to set columns job and major on the board table (which do not exist);
change the SQL to update the member table instead and use the correct identifier
column (member_id) in the WHERE clause, and ensure the parameter map exposes
memberId (or rename the parameter placeholders from #{boardId} to #{memberId})
while keeping the parameter keys #{job} and #{major} intact so the mapper
updates the member.job and member.major fields.
- Around line 36-70: The selectBoardPage query is missing the postType ==
'question' branch so it doesn't filter rows where url IS NULL; mirror the logic
used in selectBoardCount by adding an <if test="postType == 'question">
condition inside the <where> of selectBoardPage to add "AND url IS NULL" when
postType equals 'question' (ensure it coexists correctly with the existing
postType == 'website' and the query text filter).

In `@src/main/resources/mapper/bookmark-mapper.xml`:
- Around line 127-138: The updateBookmark mapper incorrectly uses
bookmarkDto.createdByMemberId for updated_by_member_id; change the mapper to
reference bookmarkDto.updatedByMemberId (in the <update id="updateBookmark"
parameterType="map"> block) and ensure the DTO or service supplies that
value—either add an updatedByMemberId field to the bookmark DTO or populate
updatedByMemberId in the service/audit layer before calling the mapper so the
correct “modifier” is persisted.
- Around line 10-48: The join returns msl.* which causes column-name collisions
between BookmarkResultMap and LinkResultMap (fields like primary_category_id,
category_score, created_at, updated_at, deleted_at), so update the
selectBookmark/selectBookmarkList queries to remove msl.* and instead list
explicit columns for member_saved_link and link, giving link columns an alias or
using a columnPrefix for the Link association; then update
BookmarkResultMap/LinkResultMap mapping columns or set columnPrefix on the
<association property="link"> to match those aliases so Link fields map to the
correct (aliased) link_* columns rather than bookmark columns.
- Around line 119-123: The mapper parameter references like
#{bookmark.memberFolderId} in insertBookmark (and similarly in updateBookmark)
fail because BookmarkDao method parameters lack `@Param` and the compiler flag
-parameters isn't enabled; fix by either adding explicit `@Param` annotations to
the BookmarkDao method signatures (e.g., `@Param`("linkId") and `@Param`("bookmark")
on the corresponding parameters) so MyBatis can resolve #{linkId} and
#{bookmark.*}, or enable the Java compiler -parameters flag in build.gradle (add
tasks.withType(JavaCompile) { options.compilerArgs += ['-parameters'] }) so
parameter names are retained at runtime.

In `@src/main/resources/mapper/comment-mapper.xml`:
- Around line 29-49: The SELECT queries selectComments, selectCommentsByMemberId
and selectComment in comment-mapper.xml are missing the soft-delete filter;
update each query to add "AND deleted_at IS NULL" to exclude soft-deleted rows,
add a deletedAt field to the Comment.java domain class, and update the
CommentResultMap to include <result property="deletedAt" column="deleted_at"/>
so MyBatis maps the column to the new field (follow the member-mapper.xml
pattern for naming and null-filtering).

In `@src/main/resources/mapper/member-mapper.xml`:
- Around line 39-42: Rename the MyBatis mapper id SocialjoinProcess to follow
Java camelCase (socialJoinProcess) and update all call sites: change the insert
id in member-mapper.xml from SocialjoinProcess to socialJoinProcess, then rename
the corresponding DAO/mapper interface method (e.g., SocialjoinProcess() →
socialJoinProcess()) and update any service layer and controller usages that
invoke that method or refer to the mapper id so names stay consistent (ensure
parameterType com.web.SearchWeb.member.domain.Member and keyProperty/keyColumn
remain unchanged).

Comment on lines +3 to +19
services:
postgres:
image: postgres:16
container_name: searchweb-db
environment:
POSTGRES_DB: ${POSTGRES_DB}
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
TZ: Asia/Seoul
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
# - ./src/main/resources/db/init.sql:/docker-entrypoint-initdb.d/init.sql
networks:
- searchweb-network
restart: always
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

PostgreSQL 컨테이너에 healthcheck 설정이 누락되었습니다.

애플리케이션이 이 컨테이너에 의존하는 경우, healthcheck 없이는 DB가 준비되기 전에 연결을 시도할 수 있습니다.

♻️ healthcheck 추가 제안
     restart: always
+    healthcheck:
+      test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
+      interval: 10s
+      timeout: 5s
+      retries: 5
📝 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
services:
postgres:
image: postgres:16
container_name: searchweb-db
environment:
POSTGRES_DB: ${POSTGRES_DB}
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
TZ: Asia/Seoul
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
# - ./src/main/resources/db/init.sql:/docker-entrypoint-initdb.d/init.sql
networks:
- searchweb-network
restart: always
services:
postgres:
image: postgres:16
container_name: searchweb-db
environment:
POSTGRES_DB: ${POSTGRES_DB}
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
TZ: Asia/Seoul
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
# - ./src/main/resources/db/init.sql:/docker-entrypoint-initdb.d/init.sql
networks:
- searchweb-network
restart: always
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
interval: 10s
timeout: 5s
retries: 5
🤖 Prompt for AI Agents
In `@docker-compose.yml` around lines 3 - 19, The postgres service (container_name
searchweb-db) is missing a Docker healthcheck; add a healthcheck section under
the postgres service that runs a sensible readiness probe (e.g., pg_isready or
equivalent) using the container's POSTGRES_USER/POSTGRES_DB, and set interval,
timeout, retries and start_period so other services wait until the DB is ready;
update the docker-compose postgres service to include this healthcheck section
and ensure restart: always stays in place so Docker can manage restarts based on
health status.

* - 클라이언트(JS)에서 스크롤 이벤트 발생 시 요청
*/
@ResponseBody
@GetMapping("api/boards")
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

api/boards 경로에 선행 슬래시(/)가 누락되어 있습니다.

이 컨트롤러의 다른 모든 엔드포인트는 /board, /board/{boardId} 등 선행 슬래시를 사용합니다. 일관성을 위해 /api/boards로 수정하세요.

🔧 수정 제안
-    `@GetMapping`("api/boards")
+    `@GetMapping`("/api/boards")
📝 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
@GetMapping("api/boards")
`@GetMapping`("/api/boards")
🤖 Prompt for AI Agents
In `@src/main/java/com/web/SearchWeb/board/controller/BoardController.java` at
line 114, The `@GetMapping` annotation in BoardController (the mapping currently
written as "api/boards") is missing the leading slash; update the mapping value
in the BoardController class (the `@GetMapping` on the method handling boards)
from "api/boards" to "/api/boards" so it matches the other endpoints like
"/board" and "/board/{boardId}" and preserves routing consistency.

public interface BoardDao {
//게시글 생성
public int insertBoard(int memberId, BoardDto boardDto);
public int insertBoard(Long memberId, BoardDto boardDto);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find mapper XML files and implementation
find . -name "*board*mapper*.xml" -o -name "*BoardDao*.java" | head -20

Repository: Searchweb-Dev/Searchweb-Back

Length of output: 239


🏁 Script executed:

#!/bin/bash
# Search for BoardDao.java to inspect the interface
fd "BoardDao.java" -x cat {}

Repository: Searchweb-Dev/Searchweb-Back

Length of output: 4453


🏁 Script executed:

#!/bin/bash
# Search for board-mapper.xml to check parameter binding
fd "board.*mapper.*xml" -x cat {}

Repository: Searchweb-Dev/Searchweb-Back

Length of output: 5530


다중 파라미터 메서드에 @Param 어노테이션이 필수입니다.

insertBoard(Long memberId, BoardDto boardDto), updateBoard, updateBoardProfile, updateBookmarkCount 메서드는 여러 개의 파라미터를 받습니다. board-mapper.xmlparameterType="map"과 함께 #{memberId}, #{boardId}, #{job} 같은 명시적 이름의 매개변수 바인딩을 사용하려면 각 파라미터에 @Param 어노테이션이 필수입니다. 없으면 런타임에 BindingException이 발생합니다.

수정 제안
-    public int insertBoard(Long memberId, BoardDto boardDto);
+    public int insertBoard(`@Param`("memberId") Long memberId, `@Param`("boardDto") BoardDto boardDto);
-    int updateBoard(Long boardId, BoardDto boardDto);
+    int updateBoard(`@Param`("boardId") Long boardId, `@Param`("boardDto") BoardDto boardDto);
-    int updateBoardProfile(Long boardId, String job, String major);
+    int updateBoardProfile(`@Param`("boardId") Long boardId, `@Param`("job") String job, `@Param`("major") String major);
-    int updateBookmarkCount(Long boardId, int bookmarkCount);
+    int updateBookmarkCount(`@Param`("boardId") Long boardId, `@Param`("bookmarkCount") int bookmarkCount);

또한 countBoardList(String query, String postType) 메서드도 마찬가지로 @Param 어노테이션 추가가 필요합니다:

-    int countBoardList(String query, String postType);
+    int countBoardList(`@Param`("query") String query, `@Param`("postType") String postType);
🤖 Prompt for AI Agents
In `@src/main/java/com/web/SearchWeb/board/dao/BoardDao.java` at line 12, BoardDao
인터페이스의 다중 파라미터 메서드들에서 MyBatis 매퍼의 명시적 이름 바인딩(#{memberId}, #{boardId}, #{job},
#{query}, #{postType} 등)을 사용하고 있으므로 각 파라미터에 `@Param` 어노테이션을 추가하세요; 구체적으로
insertBoard(Long memberId, BoardDto boardDto), updateBoard(...),
updateBoardProfile(...), updateBookmarkCount(...), countBoardList(String query,
String postType)의 모든 파라미터에 `@Param을` 붙이고 각 어노테이션의 값은 매퍼에서 사용하는 이름(예: "memberId",
"boardId", "job", "query", "postType")과 동일하게 지정해 BindingException이 발생하지 않도록
수정하세요.

Comment on lines +40 to +52
@GetMapping("/check")
public ResponseEntity<Boolean> checkBookmark(@AuthenticationPrincipal Object currentUser, @RequestParam String url) {
// 로그인 되지 않은 경우
if (currentUser == null || "anonymousUser".equals(currentUser)) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}

// 현재 로그인된 사용자의 정보 가져오기
int currentUserId = -1;
if (authentication.getPrincipal() instanceof CustomUserDetails) {
// 일반 로그인 사용자 처리
CustomUserDetails userDetails = (CustomUserDetails) authentication.getPrincipal();
currentUserId = userDetails.getMemberId();

} else if (authentication.getPrincipal() instanceof CustomOAuth2User) {
// 소셜 로그인 사용자 처리
CustomOAuth2User oAuth2User = (CustomOAuth2User) authentication.getPrincipal();
currentUserId = oAuth2User.getMemberId();
}
Long memberId = getMemberId(currentUser);

// 북마크 존재 여부 확인
boolean exists = bookmarkService.checkBookmarkExistsByUrl(memberId, url);
return ResponseEntity.ok(exists);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

checkBookmark에서 getMemberId 반환값에 대한 null 체크가 없습니다.

Line 47에서 getMemberId(currentUser)를 호출하지만, 반환값이 null인 경우에 대한 처리가 없습니다. 다른 엔드포인트(insertBookmark, selectBookmarkList 등)에서는 getMemberId 반환 후 null 체크 → 403 응답 패턴을 사용합니다. 여기서는 nullbookmarkService.checkBookmarkExistsByUrl에 전달되어 NPE 또는 예기치 않은 쿼리 결과가 발생할 수 있습니다.

🐛 수정 제안
     Long memberId = getMemberId(currentUser);
+    if (memberId == null) {
+        return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
+    }

     // 북마크 존재 여부 확인
     boolean exists = bookmarkService.checkBookmarkExistsByUrl(memberId, url);
📝 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
@GetMapping("/check")
public ResponseEntity<Boolean> checkBookmark(@AuthenticationPrincipal Object currentUser, @RequestParam String url) {
// 로그인 되지 않은 경우
if (currentUser == null || "anonymousUser".equals(currentUser)) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}
// 현재 로그인된 사용자의 정보 가져오기
int currentUserId = -1;
if (authentication.getPrincipal() instanceof CustomUserDetails) {
// 일반 로그인 사용자 처리
CustomUserDetails userDetails = (CustomUserDetails) authentication.getPrincipal();
currentUserId = userDetails.getMemberId();
} else if (authentication.getPrincipal() instanceof CustomOAuth2User) {
// 소셜 로그인 사용자 처리
CustomOAuth2User oAuth2User = (CustomOAuth2User) authentication.getPrincipal();
currentUserId = oAuth2User.getMemberId();
}
Long memberId = getMemberId(currentUser);
// 북마크 존재 여부 확인
boolean exists = bookmarkService.checkBookmarkExistsByUrl(memberId, url);
return ResponseEntity.ok(exists);
}
`@GetMapping`("/check")
public ResponseEntity<Boolean> checkBookmark(`@AuthenticationPrincipal` Object currentUser, `@RequestParam` String url) {
// 로그인 되지 않은 경우
if (currentUser == null || "anonymousUser".equals(currentUser)) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}
Long memberId = getMemberId(currentUser);
if (memberId == null) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
}
// 북마크 존재 여부 확인
boolean exists = bookmarkService.checkBookmarkExistsByUrl(memberId, url);
return ResponseEntity.ok(exists);
}
🤖 Prompt for AI Agents
In
`@src/main/java/com/web/SearchWeb/bookmark/controller/BookmarkApiController.java`
around lines 40 - 52, The checkBookmark method calls getMemberId(currentUser)
without validating its return; add the same null check pattern used in
insertBookmark/selectBookmarkList: after Long memberId =
getMemberId(currentUser) verify memberId != null and if null return
ResponseEntity.status(HttpStatus.FORBIDDEN).build(); then proceed to call
bookmarkService.checkBookmarkExistsByUrl(memberId, url) so null is never passed
into that service.

Comment on lines +135 to +173
@PutMapping("/{bookmarkId}")
public ResponseEntity<Map<String, Object>> updateBookmark(
@AuthenticationPrincipal Object currentUser,
@PathVariable Long bookmarkId,
@RequestBody BookmarkDto bookmarkDto) {

Map<String, Object> response = new HashMap<>();

Long memberId = getMemberId(currentUser);
if (memberId == null) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).body(response);
}

bookmarkDto.setCreatedByMemberId(memberId);
int result = bookmarkService.updateBookmark(bookmarkDto, bookmarkId);
response.put("success", result > 0);
return ResponseEntity.ok(response);
}


/**
* 웹사이트 북마크 삭제
**/
@DeleteMapping(value = "/{memberId}/bookmark/{websiteId}")
public ResponseEntity<Integer> deleteBookmark(@PathVariable final int memberId, @PathVariable final int websiteId) {
int result = bookmarkService.deleteBookmark(new BookmarkCheckDto(memberId, websiteId));
return ResponseEntity.ok(result);
* 북마크 삭제
*/
@DeleteMapping("/{bookmarkId}")
public ResponseEntity<Map<String, Object>> deleteBookmark(
@AuthenticationPrincipal Object currentUser,
@PathVariable Long bookmarkId) {

Map<String, Object> response = new HashMap<>();

Long memberId = getMemberId(currentUser);
if (memberId == null) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).body(response);
}

int result = bookmarkService.deleteBookmark(memberId, bookmarkId);
response.put("success", result > 0);
return ResponseEntity.ok(response);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

updateBookmarkdeleteBookmark에서 인증되지 않은 사용자 체크가 누락되었습니다.

insertBookmark, selectBookmarkList, selectBookmark에서는 currentUser == null || "anonymousUser".equals(currentUser) 검사 후 401을 반환하지만, updateBookmark(Line 135)과 deleteBookmark(Line 158)에서는 이 검사가 빠져 있습니다. 비인증 사용자가 접근하면 getMemberIdnull을 반환하여 401 대신 403이 반환됩니다. 이는 보안 모범 사례에 부합하지 않으며 클라이언트 측 에러 처리에 혼동을 줄 수 있습니다.

🔧 수정 제안 (updateBookmark 예시, deleteBookmark에도 동일 적용)
     `@PutMapping`("/{bookmarkId}")
     public ResponseEntity<Map<String, Object>> updateBookmark(
             `@AuthenticationPrincipal` Object currentUser,
             `@PathVariable` Long bookmarkId,
             `@RequestBody` BookmarkDto bookmarkDto) {
         
         Map<String, Object> response = new HashMap<>();
         
+        // 로그인 되지 않은 경우
+        if (currentUser == null || "anonymousUser".equals(currentUser)) {
+            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(response);
+        }
+        
         Long memberId = getMemberId(currentUser);
         if (memberId == null) {
             return ResponseEntity.status(HttpStatus.FORBIDDEN).body(response);
         }
🤖 Prompt for AI Agents
In
`@src/main/java/com/web/SearchWeb/bookmark/controller/BookmarkApiController.java`
around lines 135 - 173, The updateBookmark and deleteBookmark handlers are
missing the unauthenticated-user check used elsewhere; add the same pre-check
used in insertBookmark/selectBookmarkList/selectBookmark (if currentUser == null
|| "anonymousUser".equals(currentUser)) and return
ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(response) before calling
getMemberId, keeping the subsequent memberId null guard as a fallback; update
both methods (updateBookmark, deleteBookmark) to perform this check so
unauthenticated requests return 401 instead of 403.

Comment on lines +10 to +48
<resultMap id="BookmarkResultMap" type="com.web.SearchWeb.bookmark.domain.Bookmark">
<id property="bookmarkId" column="member_saved_link_id"/>
<result property="linkId" column="link_id"/>
<result property="linkEnrichmentId" column="link_enrichment_id"/>
<result property="memberFolderId" column="member_folder_id"/>
<result property="displayTitle" column="display_title"/>
<result property="note" column="note"/>
<result property="primaryCategoryId" column="primary_category_id"/>
<result property="categorySource" column="category_source"/>
<result property="categoryScore" column="category_score"/>
<result property="createdAt" column="created_at"/>
<result property="updatedAt" column="updated_at"/>
<result property="deletedAt" column="deleted_at"/>
<result property="createdByMemberId" column="created_by_member_id"/>
<result property="updatedByMemberId" column="updated_by_member_id"/>
<result property="deletedByMemberId" column="deleted_by_member_id"/>
<!-- Link 조인 (Association) -->
<association property="link" resultMap="LinkResultMap"/>
</resultMap>

<!-- ResultMap for Link entity -->
<resultMap id="LinkResultMap" type="com.web.SearchWeb.bookmark.domain.Link">
<id property="linkId" column="link_id"/>
<result property="canonicalUrl" column="canonical_url"/>
<result property="originalUrl" column="original_url"/>
<result property="domain" column="domain"/>
<result property="title" column="title"/>
<result property="description" column="description"/>
<result property="thumbnailUrl" column="thumbnail_url"/>
<result property="faviconUrl" column="favicon_url"/>
<result property="contentType" column="content_type"/>
<result property="primaryCategoryId" column="primary_category_id"/>
<result property="categoryScore" column="category_score"/>
<result property="classifierVersion" column="classifier_version"/>
<result property="categorizedAt" column="categorized_at"/>
<result property="createdAt" column="created_at"/>
<result property="updatedAt" column="updated_at"/>
<result property="deletedAt" column="deleted_at"/>
</resultMap>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

BookmarkResultMapLinkResultMap 간 컬럼 이름 충돌 — Link 속성에 잘못된 값이 매핑됩니다.

selectBookmarkselectBookmarkList 쿼리에서 msl.*을 사용하면 member_saved_link의 모든 컬럼이 확장됩니다. 그런데 LinkResultMap에도 동일한 이름의 컬럼(primary_category_id, category_score, created_at, updated_at, deleted_at)이 매핑되어 있습니다.

MyBatis는 컬럼 이름으로 값을 매핑하므로, Link 객체의 이 필드들에 member_saved_link 테이블의 값이 잘못 할당됩니다. 예를 들어, link.createdAt에 북마크의 created_at이 들어갑니다.

해결 방법: 조인 쿼리에서 msl.* 대신 명시적 컬럼 선택을 사용하고, Link 테이블 컬럼에 별칭(alias)을 붙여서 두 ResultMap이 구분할 수 있게 하세요.

🐛 수정 예시 (selectBookmark)
-    <select id="selectBookmark" parameterType="map" resultMap="BookmarkResultMap">
-        SELECT msl.*, l.original_url, l.canonical_url, l.domain, l.thumbnail_url
-        FROM member_saved_link msl
-        JOIN link l ON msl.link_id = l.link_id
-        WHERE msl.created_by_member_id = #{memberId}
-        AND msl.member_saved_link_id = #{bookmarkId}
-        AND msl.deleted_at IS NULL
-    </select>
+    <select id="selectBookmark" parameterType="map" resultMap="BookmarkResultMap">
+        SELECT msl.member_saved_link_id, msl.link_id, msl.link_enrichment_id,
+               msl.member_folder_id, msl.display_title, msl.note,
+               msl.primary_category_id, msl.category_source, msl.category_score,
+               msl.created_at, msl.updated_at, msl.deleted_at,
+               msl.created_by_member_id, msl.updated_by_member_id, msl.deleted_by_member_id,
+               l.link_id AS l_link_id, l.canonical_url AS l_canonical_url,
+               l.original_url AS l_original_url, l.domain AS l_domain,
+               l.title AS l_title, l.description AS l_description,
+               l.thumbnail_url AS l_thumbnail_url, l.favicon_url AS l_favicon_url,
+               l.content_type AS l_content_type,
+               l.primary_category_id AS l_primary_category_id,
+               l.created_at AS l_created_at, l.updated_at AS l_updated_at
+        FROM member_saved_link msl
+        JOIN link l ON msl.link_id = l.link_id
+        WHERE msl.created_by_member_id = #{memberId}
+        AND msl.member_saved_link_id = #{bookmarkId}
+        AND msl.deleted_at IS NULL
+    </select>

별칭 사용 시 LinkResultMapcolumn 속성도 일치시켜야 합니다. 또는 MyBatis의 columnPrefix를 활용하세요:

     <!-- BookmarkResultMap 내 association -->
-    <association property="link" resultMap="LinkResultMap"/>
+    <association property="link" resultMap="LinkResultMap" columnPrefix="l_"/>

Also applies to: 63-70, 74-101

🤖 Prompt for AI Agents
In `@src/main/resources/mapper/bookmark-mapper.xml` around lines 10 - 48, The join
returns msl.* which causes column-name collisions between BookmarkResultMap and
LinkResultMap (fields like primary_category_id, category_score, created_at,
updated_at, deleted_at), so update the selectBookmark/selectBookmarkList queries
to remove msl.* and instead list explicit columns for member_saved_link and
link, giving link columns an alias or using a columnPrefix for the Link
association; then update BookmarkResultMap/LinkResultMap mapping columns or set
columnPrefix on the <association property="link"> to match those aliases so Link
fields map to the correct (aliased) link_* columns rather than bookmark columns.

Comment on lines +119 to 123
<!-- 북마크 추가 -->
<insert id="insertBookmark" parameterType="map">
INSERT INTO member_saved_link (link_id, member_folder_id, display_title, note, primary_category_id, created_by_member_id)
VALUES (#{linkId}, #{bookmark.memberFolderId}, #{bookmark.displayTitle}, #{bookmark.note}, #{bookmark.primaryCategoryId}, #{bookmark.createdByMemberId})
</insert>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# BookmarkDao 인터페이스 위치 찾기
echo "=== BookmarkDao 찾기 ==="
fd 'BookmarkDao' --type f

echo ""
echo "=== pom.xml 확인 ==="
fd 'pom.xml' --type f

echo ""
echo "=== build.gradle 확인 ==="
fd 'build.gradle' --type f

Repository: Searchweb-Dev/Searchweb-Back

Length of output: 283


🏁 Script executed:

#!/bin/bash
echo "=== BookmarkDao.java 확인 ==="
cat -n src/main/java/com/web/SearchWeb/bookmark/dao/BookmarkDao.java

echo ""
echo "=== build.gradle 확인 ==="
cat -n build.gradle

Repository: Searchweb-Dev/Searchweb-Back

Length of output: 3523


insertBookmarkupdateBookmark의 매퍼 파라미터 참조가 작동하지 않습니다.

BookmarkDao 인터페이스의 메서드들이 @Param 어노테이션 없이 정의되어 있고, build.gradle-parameters 컴파일러 플래그가 설정되어 있지 않습니다. 이로 인해 MyBatis는 매퍼 XML의 #{bookmark.memberFolderId}, #{bookmark.displayTitle} 등의 파라미터 참조를 올바르게 해석할 수 없습니다.

다음 중 하나의 방법으로 수정하세요:

  1. @Param 어노테이션 추가 (권장) - BookmarkDao 메서드 파라미터에 명시적으로 @Param("bookmark"), @Param("linkId") 등을 선언
  2. 컴파일러 옵션 추가 - build.gradle에서 -parameters 플래그 활성화:
    tasks.withType(JavaCompile) {
        options.compilerArgs += ['-parameters']
    }
🤖 Prompt for AI Agents
In `@src/main/resources/mapper/bookmark-mapper.xml` around lines 119 - 123, The
mapper parameter references like #{bookmark.memberFolderId} in insertBookmark
(and similarly in updateBookmark) fail because BookmarkDao method parameters
lack `@Param` and the compiler flag -parameters isn't enabled; fix by either
adding explicit `@Param` annotations to the BookmarkDao method signatures (e.g.,
`@Param`("linkId") and `@Param`("bookmark") on the corresponding parameters) so
MyBatis can resolve #{linkId} and #{bookmark.*}, or enable the Java compiler
-parameters flag in build.gradle (add tasks.withType(JavaCompile) {
options.compilerArgs += ['-parameters'] }) so parameter names are retained at
runtime.

Comment on lines 127 to 138
<update id="updateBookmark" parameterType="map">
update bookmark
UPDATE member_saved_link
SET
name = #{bookmarkDto.name},
description = #{bookmarkDto.description},
url = #{bookmarkDto.url},
tag = #{bookmarkDto.tag},
folder_folderId = #{bookmarkDto.folder_folderId}
WHERE
bookmarkId = #{bookmarkId}
display_title = #{bookmarkDto.displayTitle},
note = #{bookmarkDto.note},
member_folder_id = #{bookmarkDto.memberFolderId},
primary_category_id = #{bookmarkDto.primaryCategoryId},
updated_at = now(),
updated_by_member_id = #{bookmarkDto.createdByMemberId}
WHERE member_saved_link_id = #{bookmarkId}
AND deleted_at IS NULL
</update>
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

updateBookmark에서 updated_by_member_idcreatedByMemberId를 사용하고 있습니다.

Line 135: updated_by_member_id = #{bookmarkDto.createdByMemberId} — 의미상 "수정한 사람"은 updatedByMemberId로 참조하는 것이 맞습니다. 현재는 컨트롤러에서 createdByMemberId에 현재 사용자 ID를 설정하여 우회하고 있지만, 필드 용도가 혼동될 수 있습니다. DTO에 updatedByMemberId 필드를 추가하거나, 서비스 계층에서 감사(audit) 필드를 설정하는 것이 바람직합니다.

🤖 Prompt for AI Agents
In `@src/main/resources/mapper/bookmark-mapper.xml` around lines 127 - 138, The
updateBookmark mapper incorrectly uses bookmarkDto.createdByMemberId for
updated_by_member_id; change the mapper to reference
bookmarkDto.updatedByMemberId (in the <update id="updateBookmark"
parameterType="map"> block) and ensure the DTO or service supplies that
value—either add an updatedByMemberId field to the bookmark DTO or populate
updatedByMemberId in the service/audit layer before calling the mapper so the
correct “modifier” is persisted.

Comment on lines +29 to 49
<select id="selectComments" resultMap="CommentResultMap">
SELECT *
FROM comment
WHERE board_board_id = #{boardId}
</select>


<!-- 회원번호로 게시글 댓글 목록 조회 -->
<select id="selectCommentsByMemberId" resultType="com.web.SearchWeb.comment.domain.Comment">
select *
from comment
WHERE member_memberId = #{memberId}
<select id="selectCommentsByMemberId" resultMap="CommentResultMap">
SELECT *
FROM comment
WHERE member_member_id = #{memberId}
</select>


<!-- 게시글 댓글 단일 조회 -->
<select id="selectComment" resultType="com.web.SearchWeb.comment.domain.Comment">
select *
from comment
WHERE commentId = #{commentId}
<select id="selectComment" resultMap="CommentResultMap">
SELECT *
FROM comment
WHERE comment_id = #{commentId}
</select>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if comment table has deleted_at column in the init SQL
echo "=== Checking comment table schema ==="
fd "init_postgres.sql" --exec grep -A 30 -i "CREATE TABLE.*comment" {}

echo ""
echo "=== Check Comment domain class ==="
fd "Comment.java" --path "*/domain/*" --exec cat {}

Repository: Searchweb-Dev/Searchweb-Back

Length of output: 1686


🏁 Script executed:

#!/bin/bash
# Find Comment.java domain class
echo "=== Finding Comment.java ==="
fd "Comment\.java" | head -20

echo ""
echo "=== Check Comment domain class ==="
fd "Comment\.java" --exec cat {} \;

echo ""
echo "=== Check member-mapper.xml for deleted_at filter ==="
fd "member-mapper\.xml" --exec grep -n "deleted_at" {} \;

echo ""
echo "=== Check comment-mapper.xml ==="
fd "comment-mapper\.xml" --exec cat {} \;

Repository: Searchweb-Dev/Searchweb-Back

Length of output: 3936


Comment SELECT 쿼리에서 soft-delete 필터(deleted_at IS NULL)가 누락되어 있습니다.

comment 테이블에는 deleted_at 컬럼이 존재하고 member-mapper.xml에서는 deleted_at IS NULL 조건을 명시적으로 적용하고 있습니다. 그러나 Comment SELECT 쿼리(selectComments, selectCommentsByMemberId, selectComment)에는 해당 필터가 없어 삭제된 댓글이 조회될 수 있습니다.

추가로 다음 항목들도 수정이 필요합니다:

  • Comment.java 도메인 클래스에 deletedAt 필드 추가
  • comment-mapper.xmlCommentResultMap<result property="deletedAt" column="deleted_at"/> 매핑 추가
  • 각 SELECT 쿼리에 AND deleted_at IS NULL 조건 추가

member-mapper.xml의 구현을 참고하면 deleted_at 컬럼 매핑과 필터 적용 방식을 확인할 수 있습니다.

🤖 Prompt for AI Agents
In `@src/main/resources/mapper/comment-mapper.xml` around lines 29 - 49, The
SELECT queries selectComments, selectCommentsByMemberId and selectComment in
comment-mapper.xml are missing the soft-delete filter; update each query to add
"AND deleted_at IS NULL" to exclude soft-deleted rows, add a deletedAt field to
the Comment.java domain class, and update the CommentResultMap to include
<result property="deletedAt" column="deleted_at"/> so MyBatis maps the column to
the new field (follow the member-mapper.xml pattern for naming and
null-filtering).

Comment on lines +39 to 42
<insert id="SocialjoinProcess" parameterType="com.web.SearchWeb.member.domain.Member" useGeneratedKeys="true" keyProperty="memberId" keyColumn="member_id">
INSERT INTO member (login_id, password_hash, member_name, nick_name, role, email)
VALUES (#{loginId}, #{passwordHash}, #{memberName}, #{nickName}, #{role}, #{email})
</insert>
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

SocialjoinProcess 메서드명이 Java 네이밍 컨벤션을 따르지 않습니다.

메서드 ID가 SocialjoinProcess로 대문자 S로 시작합니다. Java 메서드 네이밍 컨벤션(camelCase)에 따라 socialJoinProcess로 변경하는 것이 좋습니다. DAO 인터페이스와 서비스 레이어의 호출부도 함께 수정해야 합니다.

🤖 Prompt for AI Agents
In `@src/main/resources/mapper/member-mapper.xml` around lines 39 - 42, Rename the
MyBatis mapper id SocialjoinProcess to follow Java camelCase (socialJoinProcess)
and update all call sites: change the insert id in member-mapper.xml from
SocialjoinProcess to socialJoinProcess, then rename the corresponding DAO/mapper
interface method (e.g., SocialjoinProcess() → socialJoinProcess()) and update
any service layer and controller usages that invoke that method or refer to the
mapper id so names stay consistent (ensure parameterType
com.web.SearchWeb.member.domain.Member and keyProperty/keyColumn remain
unchanged).

- Lombok의 @tostring(callSuper = false) 기본 동작은 부모 필드가 출력되지 않음
- 때문에 디버깅 시 출력위해 @tostring(callSuper = true) 설정
- User 관련 컬럼명을 Member로 통일
- 모든 테이블에 표준 Audit 컬럼(생성/수정/삭제 정보) 추가
- 누락된 FK 제약조건 추가 및 테이블 생성 순서 오류 수정
@jin2304 jin2304 merged commit 96c0073 into dev Feb 10, 2026
2 checks passed
@jin2304 jin2304 linked an issue Feb 10, 2026 that may be closed by this pull request
@jin2304 jin2304 self-assigned this Feb 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

✨ feat 새로운 기능을 추가 ♻️ refactor 코드 리팩토링

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[SW-39 DB 마이그레이션] DB 마이그레이션

1 participant