Conversation
- 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: 추후 확장 기능이기 때문에 일부만 마이그레이션
* Note: 추후 고도화 예정
* Note: 추후 고도화 예정
* Note: 추후 고도화 예정
|
Important Review skippedAuto incremental reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the You can disable this status message by setting the Use the checkbox below for a quick retry:
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 }
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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 ENDsrc/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 | 🟠 MajorFolder 모듈의
int memberId를Long 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 | 🔴 CriticalXSS 취약점: 사용자 입력이 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 | 🔵 TrivialHTTP 메서드
'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
selectBoardCount와selectBoardPage간postType == '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는 조회 키이므로 값이 변경될 수 없어 중복 설정입니다. 또한,updateSocialMembermapper에서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 패턴
incrementBookmarkCount와decrementBookmarkCount가 현재 값을 읽고(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을 반환하는 스텁으로 구현되어 있습니다. 결과적으로:
checkBoardBookmark이 항상0을 반환 →bookmarkExists == 0조건이 항상trueinsertBookmarkForBoard가 항상0을 반환 → 실제 북마크가 생성되지 않음incrementBookmarkCount는 계속 호출되어 게시글의 bookmark_count만 증가- 북마크 해제(unbookmark) 경로는 절대 실행되지 않는 dead code
이 엔드포인트가 현재 사용 중이라면, 요청마다 bookmark_count만 무한히 증가하는 데이터 오염 버그가 됩니다. 사용하지 않을 예정이라면 엔드포인트 자체를 제거하거나
@Deprecated로 명시해야 합니다.
83-94: 🧹 Nitpick | 🔵 Trivial인증 사용자 ID 추출 로직 중복
boardDetail과toggleLike메서드에서currentUserinstanceof 체크를 통한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' |
There was a problem hiding this comment.
🧹 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.
| 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' | |||
There was a problem hiding this comment.
🧹 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 정의에는 영향이 없도록 파일 나머지 구조를 그대로 유지하세요.
| 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를 찾을 수 없습니다."); | ||
| } |
There was a problem hiding this comment.
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.
| 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; // 작성일 |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Board도 BaseEntity를 상속하지 않아 Comment와 동일한 일관성 문제가 있습니다.
Comment와 마찬가지로 Board도 "Legacy"로 표기되어 있으며 BaseEntity를 상속하지 않고 있습니다. createdDate가 String 타입인 점도 동일합니다. 향후 리팩토링 시 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.
| <!-- 게시글 조회(회원번호로 조회) --> | ||
| <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> |
There was a problem hiding this comment.
SELECT *로 조회 시 BoardResultMap의 JOIN 파생 필드(nickname, job, major)가 null로 남습니다.
selectBoardListByMemberId와 selectBoard는 board 테이블만 조회하지만 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.
| <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> |
There was a problem hiding this comment.
🧩 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 2Repository: Searchweb-Dev/Searchweb-Back
Length of output: 3058
updateComment 메서드의 parameterType 오류와 @Param 어노테이션 누락
CommentDao 인터페이스의 updateComment(Long commentId, CommentDto commentDto) 메서드는 두 개의 파라미터를 받지만, XML의 parameterType이 Comment로 지정되어 있습니다. 또한 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.
| <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> |
There was a problem hiding this comment.
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.
| 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 // 기본 카테고리 | ||
| }; |
There was a problem hiding this comment.
DTO 필드명 불일치: member_memberId는 BookmarkDto에 존재하지 않습니다.
BookmarkDto의 관련 코드 스니펫을 보면 createdByMemberId 필드를 사용합니다. member_memberId로 전송하면 서버에서 해당 값이 바인딩되지 않아 createdByMemberId가 null이 됩니다.
🐛 수정 제안
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.
| 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 필드 |
There was a problem hiding this comment.
🧩 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 2Repository: 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 -100Repository: 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.
There was a problem hiding this comment.
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 | 🟡 MinorMyBatis 매퍼 내 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 ENDsrc/main/java/com/web/SearchWeb/config/SecurityConfig.java (1)
79-80: 🧹 Nitpick | 🔵 TrivialCSRF가 비활성화되어 있습니다.
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 | 🔴 CriticalXSS 취약점:
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 | 🔵 TrivialHTTP 메서드
'Put'은 대문자'PUT'으로 통일하는 것이 좋습니다.jQuery가 내부적으로 대소문자를 처리하므로 동작에는 문제가 없지만, Line 776의 프로필 수정에서는
'PUT'을 사용하고 있어 일관성이 떨어집니다. 주석의'Patch'도 정리가 필요합니다.♻️ 제안
- type: 'Put', //type: 'Patch', + type: 'PUT',
359-362: 🧹 Nitpick | 🔵 Trivial
window.onload와window.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에서는mapper가final로 선언되어 있습니다. 생성자에서만 할당되고 이후 변경되지 않으므로 불변성을 보장하는 것이 좋습니다.♻️ 제안
- 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처럼 파라미터 타입이
int→Long으로 변경되는 경우,@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로 주입된Object가String타입"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 경쟁 조건이 있습니다.
selectBoard→getBookmarksCount→updateBookmarkCount(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추출 로직이 중복됩니다.
boardDetail과toggleLike에서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).
| 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 |
There was a problem hiding this comment.
🧹 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.
| 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") |
There was a problem hiding this comment.
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.
| @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); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Find mapper XML files and implementation
find . -name "*board*mapper*.xml" -o -name "*BoardDao*.java" | head -20Repository: 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.xml의 parameterType="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이 발생하지 않도록
수정하세요.
| @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); | ||
| } |
There was a problem hiding this comment.
checkBookmark에서 getMemberId 반환값에 대한 null 체크가 없습니다.
Line 47에서 getMemberId(currentUser)를 호출하지만, 반환값이 null인 경우에 대한 처리가 없습니다. 다른 엔드포인트(insertBookmark, selectBookmarkList 등)에서는 getMemberId 반환 후 null 체크 → 403 응답 패턴을 사용합니다. 여기서는 null이 bookmarkService.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.
| @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.
| @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); | ||
| } |
There was a problem hiding this comment.
updateBookmark과 deleteBookmark에서 인증되지 않은 사용자 체크가 누락되었습니다.
insertBookmark, selectBookmarkList, selectBookmark에서는 currentUser == null || "anonymousUser".equals(currentUser) 검사 후 401을 반환하지만, updateBookmark(Line 135)과 deleteBookmark(Line 158)에서는 이 검사가 빠져 있습니다. 비인증 사용자가 접근하면 getMemberId가 null을 반환하여 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.
| <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> |
There was a problem hiding this comment.
BookmarkResultMap과 LinkResultMap 간 컬럼 이름 충돌 — Link 속성에 잘못된 값이 매핑됩니다.
selectBookmark과 selectBookmarkList 쿼리에서 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>별칭 사용 시 LinkResultMap의 column 속성도 일치시켜야 합니다. 또는 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.
| <!-- 북마크 추가 --> | ||
| <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> |
There was a problem hiding this comment.
🧩 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 fRepository: 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.gradleRepository: Searchweb-Dev/Searchweb-Back
Length of output: 3523
insertBookmark과 updateBookmark의 매퍼 파라미터 참조가 작동하지 않습니다.
BookmarkDao 인터페이스의 메서드들이 @Param 어노테이션 없이 정의되어 있고, build.gradle에 -parameters 컴파일러 플래그가 설정되어 있지 않습니다. 이로 인해 MyBatis는 매퍼 XML의 #{bookmark.memberFolderId}, #{bookmark.displayTitle} 등의 파라미터 참조를 올바르게 해석할 수 없습니다.
다음 중 하나의 방법으로 수정하세요:
@Param어노테이션 추가 (권장) -BookmarkDao메서드 파라미터에 명시적으로@Param("bookmark"),@Param("linkId")등을 선언- 컴파일러 옵션 추가 -
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.
| <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> |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
updateBookmark에서 updated_by_member_id에 createdByMemberId를 사용하고 있습니다.
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.
| <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> |
There was a problem hiding this comment.
🧩 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.xml의CommentResultMap에<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).
| <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> |
There was a problem hiding this comment.
🧹 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).
- User 관련 컬럼명을 Member로 통일 - 모든 테이블에 표준 Audit 컬럼(생성/수정/삭제 정보) 추가 - 누락된 FK 제약조건 추가 및 테이블 생성 순서 오류 수정
💡 이슈
resolve {#21}
🤩 개요
🧑💻 작업 사항
1) 데이터베이스 마이그레이션 (MySQL → PostgreSQL)
2) JPA & MyBatis 하이브리드 아키텍처 도입
3) 도메인 리팩토링 및 타입 안정성 강화
DTO, DAO, Service, Controller 전 계층에 걸쳐 타입 불일치 해결
💡 도입 배경 및 목적 (Why?)
1) PostgreSQL 전환: 확장성 및 유연성 확보
2) JPA 도입: 생산성 및 유지보수성 향상
📖 참고 사항
Summary by CodeRabbit
새로운 기능
개선 사항
기타