diff --git a/build.gradle b/build.gradle index fcfeb7b..e753fc4 100644 --- a/build.gradle +++ b/build.gradle @@ -28,6 +28,7 @@ dependencies { 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' implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.3' implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6' implementation 'org.springframework.boot:spring-boot-starter-validation' @@ -35,7 +36,7 @@ dependencies { implementation 'me.paulschwarz:spring-dotenv:4.0.0' compileOnly 'org.projectlombok:lombok' - runtimeOnly 'com.mysql:mysql-connector-j' + runtimeOnly 'org.postgresql:postgresql' annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter-test:3.0.3' diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..88edc98 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,26 @@ +version: '3.8' + +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 + +networks: + searchweb-network: + driver: bridge + +volumes: + postgres_data: diff --git a/src/main/java/com/web/SearchWeb/aop/OwnerCheckAspect.java b/src/main/java/com/web/SearchWeb/aop/OwnerCheckAspect.java index 119f145..510d941 100644 --- a/src/main/java/com/web/SearchWeb/aop/OwnerCheckAspect.java +++ b/src/main/java/com/web/SearchWeb/aop/OwnerCheckAspect.java @@ -18,6 +18,11 @@ import java.util.Arrays; import java.util.Objects; +/** + * OwnerCheckAspect + * + * 리소스 소유자 검증 AOP + */ @Aspect @Slf4j(topic = "[OwnerCheckAspect]") @Component @@ -34,14 +39,14 @@ public class OwnerCheckAspect { @Before("@annotation(ownerCheck)") public void validateOwner(JoinPoint joinPoint, OwnerCheck ownerCheck) { // 접근 검증 대상 리소스의 ID 추출 - Integer targetId = extractTargetIdFromParams(joinPoint, ownerCheck.idParam()); + Long targetId = extractTargetIdFromParams(joinPoint, ownerCheck.idParam()); // 현재 로그인한 사용자의 memberId 추출 Authentication auth = validateAuthenticatedUser(); - Integer currentUserId = extractMemberId(auth); + Long currentUserId = extractMemberId(auth); // 서비스 이름에 따라 리소스 작성자 memberId 조회 - Integer ownerId = findOwnerIdByServiceName(ownerCheck.service(), targetId); + Long ownerId = findOwnerIdByServiceName(ownerCheck.service(), targetId); // 현재 사용자와 리소스 소유자 검증 if (!Objects.equals(currentUserId, ownerId)) { @@ -54,21 +59,22 @@ public void validateOwner(JoinPoint joinPoint, OwnerCheck ownerCheck) { /** - * 접근 검증 대상이 되는 리소스의 ID를 파라미터 이름(idParam)을 통해 찾아 Integer로 반환 + * 접근 검증 대상이 되는 리소스의 ID를 파라미터 이름(idParam)을 통해 찾아 Long으로 반환 * ex) @OwnerCheck(idParam = "boardId", ...) -> 메서드의 boardId 값을 찾아 사용 * * @param joinPoint 현재 실행된 메서드의 실행 정보 * @param idParam 검증 대상 리소스 ID의 파라미터 이름 (예: "boardId" 문자열) * @return 접근 검증 대상이 되는 리소스의 ID 값 */ - private Integer extractTargetIdFromParams(JoinPoint joinPoint, String idParam) { + 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)); @@ -76,7 +82,7 @@ private Integer extractTargetIdFromParams(JoinPoint joinPoint, String idParam) { } - // SecurityContext 에서 인증된 사용자 반한 + // SecurityContext 에서 인증된 사용자 반환 private Authentication validateAuthenticatedUser() { Authentication auth = SecurityContextHolder.getContext().getAuthentication(); if (auth == null || !auth.isAuthenticated() || "anonymousUser".equals(auth.getPrincipal())) { @@ -87,8 +93,8 @@ private Authentication validateAuthenticatedUser() { } - // 인증 객체에서 현재 로그인한 사용자 memberId 추출 - private Integer extractMemberId(Authentication auth) { + // 인증 객체에서 현재 로그인한 사용자 memberId 추출 (Long 반환) + private Long extractMemberId(Authentication auth) { Object principal = auth.getPrincipal(); if (principal instanceof CustomUserDetails u) return u.getMemberId(); if (principal instanceof CustomOAuth2User u) return u.getMemberId(); @@ -98,11 +104,11 @@ private Integer extractMemberId(Authentication auth) { // 서비스 이름에 따라 해당 리소스의 작성자 조회 - private Integer findOwnerIdByServiceName(String service, Integer targetId) { + private Long findOwnerIdByServiceName(String service, Long targetId) { return switch (service) { case "boardService" -> boardService.findMemberIdByBoardId(targetId); case "commentService" -> commentService.findMemberIdByCommentId(targetId); - case "memberService" -> targetId; + case "memberService" -> targetId; // memberService의 경우 targetId가 곧 memberId default -> { log.error("지원하지 않는 서비스명 '{}'", service); throw new IllegalArgumentException("지원하지 않는 서비스명입니다."); diff --git a/src/main/java/com/web/SearchWeb/board/controller/BoardController.java b/src/main/java/com/web/SearchWeb/board/controller/BoardController.java index 5076a3e..b0ca477 100644 --- a/src/main/java/com/web/SearchWeb/board/controller/BoardController.java +++ b/src/main/java/com/web/SearchWeb/board/controller/BoardController.java @@ -25,18 +25,9 @@ import java.util.Map; /** - * 코드 작성자: - * - 서진영(jin2304) - * - * 코드 설명: - * - BoardController는 게시판 및 게시글 관련 기능을 처리하는 컨트롤러 - * - * 코드 주요 기능: - * - 게시글 등록, 게시글 목록 조회(검색어, 최신순/인기순), 게시글 단일(상세) 조회, 게시글 수정, 게시글 삭제 - * - 게시글 좋아요/북마크 추가 및 취소 - * - * 코드 작성일: - * - 2024.08.24 ~ 2024.09.05 + * BoardController (Legacy - PostgreSQL) + * + * 게시판 및 게시글 관련 기능을 처리하는 컨트롤러 */ @Controller public class BoardController { @@ -55,15 +46,6 @@ public BoardController(BoardService boardservice, MemberService memberservice, L } - /** - * 게시글 생성 - */ - @PostMapping("/board/{memberId}/post") - public String insertBoard(@PathVariable int memberId, BoardDto boardDto){ - int result = boardservice.insertBoard(memberId, boardDto); - return "redirect:/board"; - } - /** @@ -76,19 +58,12 @@ public String boardPage() { /** - * 페이징된 게시글 목록 조회 - * - 검색어, 최신순/인기순, 게시글타입 - * - 스크롤 방식으로 페이징 지원 - * - 클라이언트(JS)에서 스크롤 이벤트 발생 시 요청 + * 게시글 생성 */ - @ResponseBody - @GetMapping("api/boards") - public Map getBoards(@RequestParam(defaultValue = "0") int page, - @RequestParam(defaultValue = "10") int size, - @RequestParam(defaultValue = "newest") String sort, - @RequestParam(required = false) String query, - @RequestParam(defaultValue = "all") String postType) { - return boardservice.selectBoardPage(page, size, sort, query, postType); + @PostMapping("/board/{memberId}/post") + public String insertBoard(@PathVariable Long memberId, BoardDto boardDto){ + int result = boardservice.insertBoard(memberId, boardDto); + return "redirect:/board"; } @@ -96,7 +71,7 @@ public Map getBoards(@RequestParam(defaultValue = "0") int page, * 게시글 단일 조회 */ @GetMapping("/board/{boardId}") - public String boardDetail(@PathVariable int boardId,@AuthenticationPrincipal Object currentUser, Model model){ + public String boardDetail(@PathVariable Long boardId, @AuthenticationPrincipal Object currentUser, Model model){ Map boardData = boardservice.selectBoard(boardId); Board board = (Board) boardData.get("board"); String[] hashtagsList = (String[]) boardData.get("hashtagsList"); @@ -106,7 +81,7 @@ public String boardDetail(@PathVariable int boardId,@AuthenticationPrincipal Obj // 사용자가 로그인된 상태라면, 좋아요 여부를 확인하여 모델에 추가 if (currentUser != null && !"anonymousUser".equals(currentUser)) { - int memberId; + Long memberId; if(currentUser instanceof UserDetails) { // 일반 로그인 사용자 처리 memberId = ((CustomUserDetails) currentUser).getMemberId(); @@ -129,12 +104,29 @@ else if(currentUser instanceof OAuth2User) { } + /** + * 페이징된 게시글 목록 조회 + * - 검색어, 최신순/인기순, 게시글타입 + * - 스크롤 방식으로 페이징 지원 + * - 클라이언트(JS)에서 스크롤 이벤트 발생 시 요청 + */ + @ResponseBody + @GetMapping("api/boards") + public Map getBoards(@RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "10") int size, + @RequestParam(defaultValue = "newest") String sort, + @RequestParam(required = false) String query, + @RequestParam(defaultValue = "all") String postType) { + return boardservice.selectBoardPage(page, size, sort, query, postType); + } + + /** * 게시글 수정 */ @PostMapping("/board/{boardId}/update") @OwnerCheck(idParam = "boardId", service = "boardService") - public String updateBoard(@PathVariable int boardId, BoardDto boardDto){ + public String updateBoard(@PathVariable Long boardId, BoardDto boardDto){ boardservice.updateBoard(boardId, boardDto); return "redirect:/board/{boardId}"; } @@ -145,7 +137,7 @@ public String updateBoard(@PathVariable int boardId, BoardDto boardDto){ */ @PostMapping("/board/{boardId}/delete") @OwnerCheck(idParam = "boardId", service = "boardService") - public String deleteBoard(@PathVariable int boardId) { + public String deleteBoard(@PathVariable Long boardId) { boardservice.deleteBoard(boardId); return "redirect:/board"; } @@ -156,7 +148,7 @@ public String deleteBoard(@PathVariable int boardId) { */ @PostMapping("/board/{boardId}/like") @ResponseBody - public Map toggleLike(@PathVariable int boardId, @AuthenticationPrincipal Object currentUser) { + public Map toggleLike(@PathVariable Long boardId, @AuthenticationPrincipal Object currentUser) { Map response = new HashMap<>(); @@ -168,7 +160,7 @@ public Map toggleLike(@PathVariable int boardId, @Authentication } // 로그인 된 경우 - int memberId; + Long memberId; if(currentUser instanceof UserDetails) { // 일반 로그인 사용자 처리 memberId = ((CustomUserDetails) currentUser).getMemberId(); @@ -192,8 +184,8 @@ else if(currentUser instanceof OAuth2User) { */ @PostMapping(value ="/board/{boardId}/bookmark/{memberId}") public ResponseEntity> toggleBookmark( - @PathVariable final int boardId, - @PathVariable final int memberId, + @PathVariable final Long boardId, + @PathVariable final Long memberId, @RequestBody BookmarkDto bookmarkDto, @AuthenticationPrincipal Object currentUser){ Map response = new HashMap<>(); @@ -211,7 +203,7 @@ public ResponseEntity> toggleBookmark( if (bookmarkExists == 0) { // 북마크가 안 되어 있으면 북마크 추가 - bookmarkService.insertBookmarkForBoard(bookmarkDto); + bookmarkService.insertBookmarkForBoard(boardId, bookmarkDto); boardservice.incrementBookmarkCount(boardId); // 북마크 추가 시 게시글의 북마크 수 증가 response.put("action", "bookmarked"); } else { diff --git a/src/main/java/com/web/SearchWeb/board/dao/BoardDao.java b/src/main/java/com/web/SearchWeb/board/dao/BoardDao.java index 696fc8d..b90cd40 100644 --- a/src/main/java/com/web/SearchWeb/board/dao/BoardDao.java +++ b/src/main/java/com/web/SearchWeb/board/dao/BoardDao.java @@ -9,7 +9,7 @@ public interface BoardDao { //게시글 생성 - public int insertBoard(int memberId, BoardDto boardDto); + public int insertBoard(Long memberId, BoardDto boardDto); // 페이징된 게시글 목록 조회 List selectBoardPage(@Param("offset") int offset, @@ -22,35 +22,35 @@ List selectBoardPage(@Param("offset") int offset, int countBoardList(String query, String postType); //게시글 목록 조회(회원번호로 조회) - List selectBoardListByMemberId(int memberId); + List selectBoardListByMemberId(Long memberId); //게시글 단일 조회 - Board selectBoard(int boardId); + Board selectBoard(Long boardId); //게시글 수정 - int updateBoard(int boardId, BoardDto boardDto); + int updateBoard(Long boardId, BoardDto boardDto); //게시글 수정(회원정보 수정) - int updateBoardProfile(int boardId, String job, String major); + int updateBoardProfile(Long boardId, String job, String major); //게시글 삭제 - int deleteBoard(int boardId); + int deleteBoard(Long boardId); //게시글 북마크 수 수정 - int updateBookmarkCount(int boardId, int bookmarkCount); + int updateBookmarkCount(Long boardId, int bookmarkCount); //게시글 조회수 증가 - int incrementViewCount(int boardId); + int incrementViewCount(Long boardId); //게시글 좋아요 증가 - int incrementLikeCount(int boardId); + int incrementLikeCount(Long boardId); //게시글 좋아요 감소 - int decrementLikeCount(int boardId); + int decrementLikeCount(Long boardId); //게시글 댓글 수 증가 - int incrementCommentCount(int boardId); + int incrementCommentCount(Long boardId); //게시글 댓글 수 감소 - int decrementCommentCount(int boardId); + int decrementCommentCount(Long boardId); } \ No newline at end of file diff --git a/src/main/java/com/web/SearchWeb/board/dao/MybatisBoardDao.java b/src/main/java/com/web/SearchWeb/board/dao/MybatisBoardDao.java index b702b38..d094760 100644 --- a/src/main/java/com/web/SearchWeb/board/dao/MybatisBoardDao.java +++ b/src/main/java/com/web/SearchWeb/board/dao/MybatisBoardDao.java @@ -23,7 +23,7 @@ public MybatisBoardDao(SqlSession sqlSession) { /** * 게시글 생성 */ - public int insertBoard(int memberId, BoardDto boardDto) { + public int insertBoard(Long memberId, BoardDto boardDto) { return mapper.insertBoard(memberId, boardDto); } @@ -48,7 +48,7 @@ public int countBoardList(String query, String postType) { /** * 게시글 목록 조회(회원번호로 조회) */ - public List selectBoardListByMemberId(int memberId) { + public List selectBoardListByMemberId(Long memberId) { return mapper.selectBoardListByMemberId(memberId); } @@ -56,7 +56,7 @@ public List selectBoardListByMemberId(int memberId) { /** * 게시글 단일 조회 */ - public Board selectBoard(int boardId) { + public Board selectBoard(Long boardId) { return mapper.selectBoard(boardId); } @@ -64,7 +64,7 @@ public Board selectBoard(int boardId) { /** * 게시글 수정 */ - public int updateBoard(int boardId, BoardDto boardDto){ + public int updateBoard(Long boardId, BoardDto boardDto){ return mapper.updateBoard(boardId, boardDto); } @@ -72,7 +72,7 @@ public int updateBoard(int boardId, BoardDto boardDto){ /** * 게시글 수정(회원정보 수정) */ - public int updateBoardProfile(int boardId, String job, String major){ + public int updateBoardProfile(Long boardId, String job, String major){ return mapper.updateBoardProfile(boardId, job, major); } @@ -80,7 +80,7 @@ public int updateBoardProfile(int boardId, String job, String major){ /** * 게시글 삭제 */ - public int deleteBoard(int boardId) { + public int deleteBoard(Long boardId) { return mapper.deleteBoard(boardId); } @@ -89,7 +89,7 @@ public int deleteBoard(int boardId) { * 게시글 북마크 수 수정 */ @Override - public int updateBookmarkCount(int boardId, int bookmarkCount) { + public int updateBookmarkCount(Long boardId, int bookmarkCount) { return mapper.updateBookmarkCount(boardId, bookmarkCount); } @@ -97,7 +97,7 @@ public int updateBookmarkCount(int boardId, int bookmarkCount) { /** * 게시글 조회수 증가 */ - public int incrementViewCount(int boardId) { + public int incrementViewCount(Long boardId) { return mapper.incrementViewCount(boardId); } @@ -106,7 +106,7 @@ public int incrementViewCount(int boardId) { * 게시글 좋아요 증가 */ @Override - public int incrementLikeCount(int boardId) { + public int incrementLikeCount(Long boardId) { return mapper.incrementLikeCount(boardId); } @@ -115,7 +115,7 @@ public int incrementLikeCount(int boardId) { * 게시글 좋아요 감소 */ @Override - public int decrementLikeCount(int boardId) { + public int decrementLikeCount(Long boardId) { return mapper.decrementLikeCount(boardId); } @@ -124,7 +124,7 @@ public int decrementLikeCount(int boardId) { * 게시글 댓글 수 증가 */ @Override - public int incrementCommentCount(int boardId) { + public int incrementCommentCount(Long boardId) { return mapper.incrementCommentCount(boardId); } @@ -133,8 +133,7 @@ public int incrementCommentCount(int boardId) { * 게시글 댓글 수 감소 */ @Override - public int decrementCommentCount(int boardId) { + public int decrementCommentCount(Long boardId) { return mapper.decrementCommentCount(boardId); } } - diff --git a/src/main/java/com/web/SearchWeb/board/domain/Board.java b/src/main/java/com/web/SearchWeb/board/domain/Board.java index ab1d139..c536608 100644 --- a/src/main/java/com/web/SearchWeb/board/domain/Board.java +++ b/src/main/java/com/web/SearchWeb/board/domain/Board.java @@ -4,23 +4,27 @@ import lombok.Setter; import lombok.ToString; +/** + * Board 도메인 (Legacy) + * - Member 테이블과 FK 관계 (member_id BIGINT) + */ @Getter @Setter @ToString 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; // 작성일 } diff --git a/src/main/java/com/web/SearchWeb/board/dto/BoardDto.java b/src/main/java/com/web/SearchWeb/board/dto/BoardDto.java index 0e94cb5..a16959b 100644 --- a/src/main/java/com/web/SearchWeb/board/dto/BoardDto.java +++ b/src/main/java/com/web/SearchWeb/board/dto/BoardDto.java @@ -8,12 +8,12 @@ @Setter @ToString public class BoardDto { - 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 String nickname; // 작성자 닉네임 + private String job; // 직업 + private String major; // 전공 + private String url; // 참조 URL + private String title; // 제목 + private String summary; // 요약 + private String description; // 본문 내용 + private String hashtags; // 해시태그 } diff --git a/src/main/java/com/web/SearchWeb/board/service/BoardService.java b/src/main/java/com/web/SearchWeb/board/service/BoardService.java index 2c9e561..8885f01 100644 --- a/src/main/java/com/web/SearchWeb/board/service/BoardService.java +++ b/src/main/java/com/web/SearchWeb/board/service/BoardService.java @@ -34,7 +34,7 @@ public BoardService(BoardDao boardDao, MemberService memberService, LikesService /** * 게시글 생성 */ - public int insertBoard(int memberId, BoardDto boardDto) { + public int insertBoard(Long memberId, BoardDto boardDto) { return boardDao.insertBoard(memberId, boardDto); } @@ -66,7 +66,7 @@ public Map selectBoardPage(int page, int size, String sort, Stri /** * 게시글 단일 조회 */ - public Map selectBoard(int boardId) { + public Map selectBoard(Long boardId) { // 조회수 증가 boardDao.incrementViewCount(boardId); @@ -87,7 +87,7 @@ public Map selectBoard(int boardId) { /** * 게시글 수정 */ - public int updateBoard(int boardId, BoardDto boardDto){ + public int updateBoard(Long boardId, BoardDto boardDto){ return boardDao.updateBoard(boardId, boardDto); } @@ -95,7 +95,7 @@ public int updateBoard(int boardId, BoardDto boardDto){ /** * 게시글 삭제 */ - public int deleteBoard(int boardId) { + public int deleteBoard(Long boardId) { return boardDao.deleteBoard(boardId); } @@ -103,11 +103,10 @@ public int deleteBoard(int boardId) { /** * 게시글 북마크 수 증가 */ - public void incrementBookmarkCount(int boardId) { + public void incrementBookmarkCount(Long boardId) { Board board = boardDao.selectBoard(boardId); if (board != null) { - board.setBookmarks_count(board.getBookmarks_count() + 1); - boardDao.updateBookmarkCount(boardId, board.getBookmarks_count()); + boardDao.updateBookmarkCount(boardId, board.getBookmarksCount() + 1); } else { throw new IllegalArgumentException("Invalid board ID"); } @@ -117,21 +116,22 @@ public void incrementBookmarkCount(int boardId) { /** * 게시글 북마크 수 감소 */ - public void decrementBookmarkCount(int boardId) { + public void decrementBookmarkCount(Long boardId) { Board board = boardDao.selectBoard(boardId); if (board != null) { - board.setBookmarks_count(board.getBookmarks_count() - 1); - boardDao.updateBookmarkCount(boardId, board.getBookmarks_count()); + boardDao.updateBookmarkCount(boardId, board.getBookmarksCount() - 1); } else { throw new IllegalArgumentException("Invalid board ID"); } } - // 게시글 소유자(작성자) 조회 - public int findMemberIdByBoardId(int boardId) { + /** + * 게시글 소유자(작성자) 조회 + */ + public Long findMemberIdByBoardId(Long boardId) { Board board = boardDao.selectBoard(boardId); if (board == null) throw new IllegalArgumentException("게시글이 존재하지 않습니다."); - return board.getMember_memberId(); + return board.getMemberMemberId(); } } diff --git a/src/main/java/com/web/SearchWeb/bookmark/controller/BookmarkApiController.java b/src/main/java/com/web/SearchWeb/bookmark/controller/BookmarkApiController.java index 094374e..1577ea2 100644 --- a/src/main/java/com/web/SearchWeb/bookmark/controller/BookmarkApiController.java +++ b/src/main/java/com/web/SearchWeb/bookmark/controller/BookmarkApiController.java @@ -2,109 +2,186 @@ import com.web.SearchWeb.bookmark.domain.Bookmark; -import com.web.SearchWeb.bookmark.dto.BookmarkCheckDto; import com.web.SearchWeb.bookmark.dto.BookmarkDto; import com.web.SearchWeb.bookmark.service.BookmarkService; -import com.web.SearchWeb.main.service.MainService; import com.web.SearchWeb.member.dto.CustomOAuth2User; import com.web.SearchWeb.member.dto.CustomUserDetails; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.security.authentication.AnonymousAuthenticationToken; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.oauth2.core.user.OAuth2User; import org.springframework.web.bind.annotation.*; +import java.util.HashMap; import java.util.List; +import java.util.Map; /** - * - * 코드 작성자: 서진영(jin2304) - * 코드 설명 :CommentApiController는 맛집의 댓글과 관련된 기능을 담당하며, Rest API방식으로 설계했음. - * 코드 주요 기능: 댓글 등록, 댓글 조회, 댓글 삭제, 댓글 수정 - * 코드 작성일: 2024.07.07 ~ 2024.07.31 - * + * BookmarkApiController + * - 북마크 API (member_saved_link + link 테이블 기반) */ - @RestController -@RequestMapping("/mainList") +@RequestMapping("/api/bookmarks") public class BookmarkApiController { private final BookmarkService bookmarkService; - private final MainService mainService; @Autowired - public BookmarkApiController(BookmarkService bookmarkService, MainService mainService) { + public BookmarkApiController(BookmarkService bookmarkService) { this.bookmarkService = bookmarkService; - this.mainService = mainService; } /** * 북마크 확인 */ - @GetMapping("/bookmark/status/{websiteId}") - public ResponseEntity checkBookmark(@PathVariable final int websiteId){ - // 현재 사용자의 Authentication 객체 가져오기 - Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - - // 사용자가 로그인되어 있는지 확인 - if (authentication instanceof AnonymousAuthenticationToken) { - //사용자가 로그인되지 않은 경우, 로그인 페이지로 리디렉션 - return ResponseEntity - .status(HttpStatus.UNAUTHORIZED) - .body(0); // 로그인되지 않음을 클라이언트에 반환 + @GetMapping("/check") + public ResponseEntity 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); + } - // 해당 유저가 해당 가게를 북마크했는지 여부를 서비스에서 확인하여 반환 - BookmarkCheckDto bookmark = new BookmarkCheckDto(currentUserId, websiteId); - int result = bookmarkService.checkBookmark(bookmark); - return ResponseEntity.ok(result); + /** + * 북마크 추가 + */ + @PostMapping + public ResponseEntity> insertBookmark( + @AuthenticationPrincipal Object currentUser, + @RequestBody BookmarkDto bookmarkDto, + @RequestParam String url) { + + Map 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); + } + + bookmarkDto.setCreatedByMemberId(memberId); + int result = bookmarkService.insertBookmark(bookmarkDto, url); + response.put("success", result > 0); + return ResponseEntity.ok(response); } /** * 북마크 목록 조회 - **/ - @GetMapping("/{memberId}/bookmarks") - public List selectBookmarkList(@PathVariable final int memberId){ - return bookmarkService.selectBookmarkList(memberId, null, "Oldest", null, null); //JSON 형태로 객체 리스트 반환 + */ + @GetMapping + public ResponseEntity> selectBookmarkList( + @AuthenticationPrincipal Object currentUser, + @RequestParam(required = false) Long folderId, + @RequestParam(defaultValue = "Newest") String sort, + @RequestParam(required = false) String query, + @RequestParam(required = false) Long categoryId) { + + // 로그인 되지 않은 경우 + 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(); + } + + List bookmarks = bookmarkService.selectBookmarkList(memberId, folderId, sort, query, categoryId); + return ResponseEntity.ok(bookmarks); + } + + + /** + * 북마크 단일 조회 + */ + @GetMapping("/{bookmarkId}") + public ResponseEntity selectBookmark( + @AuthenticationPrincipal Object currentUser, + @PathVariable Long bookmarkId) { + + // 로그인 되지 않은 경우 + 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(); + } + + Bookmark bookmark = bookmarkService.selectBookmark(memberId, bookmarkId); + return ResponseEntity.ok(bookmark); } /** - * 북마크 추가 (메인리스트에서 추가) - **/ - @PostMapping(value ="/{memberId}/bookmark/{websiteId}") - public ResponseEntity insertBookmark(@PathVariable final int memberId, - @PathVariable final int websiteId, - @RequestBody BookmarkDto bookmarkDto){ - int result = bookmarkService.insertBookmark(bookmarkDto); - return ResponseEntity.ok(result); + * 북마크 수정 + */ + @PutMapping("/{bookmarkId}") + public ResponseEntity> updateBookmark( + @AuthenticationPrincipal Object currentUser, + @PathVariable Long bookmarkId, + @RequestBody BookmarkDto bookmarkDto) { + + Map 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 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> deleteBookmark( + @AuthenticationPrincipal Object currentUser, + @PathVariable Long bookmarkId) { + + Map 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); + } + + + /** + * 현재 사용자의 memberId 추출 + */ + private Long getMemberId(Object currentUser) { + if (currentUser instanceof UserDetails) { + return ((CustomUserDetails) currentUser).getMemberId(); + } else if (currentUser instanceof OAuth2User) { + return ((CustomOAuth2User) currentUser).getMemberId(); + } + return null; } } diff --git a/src/main/java/com/web/SearchWeb/bookmark/dao/BookmarkDao.java b/src/main/java/com/web/SearchWeb/bookmark/dao/BookmarkDao.java index 435d7cd..0456267 100644 --- a/src/main/java/com/web/SearchWeb/bookmark/dao/BookmarkDao.java +++ b/src/main/java/com/web/SearchWeb/bookmark/dao/BookmarkDao.java @@ -1,41 +1,40 @@ package com.web.SearchWeb.bookmark.dao; import com.web.SearchWeb.bookmark.domain.Bookmark; -import com.web.SearchWeb.bookmark.dto.BoardBookmarkCheckDto; -import com.web.SearchWeb.bookmark.dto.BookmarkCheckDto; +import com.web.SearchWeb.bookmark.domain.Link; import com.web.SearchWeb.bookmark.dto.BookmarkDto; -import com.web.SearchWeb.bookmark.domain.BookmarkWebsite; import com.web.SearchWeb.bookmark.dto.request.BookmarkSearchRequestDto; import java.util.List; public interface BookmarkDao { - //북마크 확인 - int checkBookmark(BookmarkCheckDto bookmark); - //게시판 북마크 확인 - int checkBoardBookmark(BoardBookmarkCheckDto checkDto); + //북마크 링크 중복 확인 (동일 폴더에 동일 링크) + int checkBookmarkExists(Long memberId, Long folderId, Long linkId); + + //북마크 추가 + int insertBookmark(BookmarkDto bookmark, Long linkId); + //북마크 단일 조회 - Bookmark selectBookmark(int memberId, int bookmarkId); + Bookmark selectBookmark(Long memberId, Long bookmarkId); + //북마크 목록 조회 List selectBookmarkList(BookmarkSearchRequestDto searchRequest); - //북마크 추가 - int insertBookmark(BookmarkDto bookmark); - //북마크 추가 (사용자 직접 추가) - int insertBookmarkForUser(BookmarkDto bookmarkDto); - //북마크 추가 (게시판에서 추가) - int insertBookmarkBoard(BookmarkDto bookmarkDto); + //북마크 수정 - int updateBookmark(BookmarkDto bookmarkDto, int bookmarkId); + int updateBookmark(BookmarkDto bookmarkDto, Long bookmarkId); + //북마크 삭제 - int deleteBookmark(BookmarkCheckDto bookmark); - //마이페이지 북마크 삭제 - int deleteBookmarkMyPage(int memberId,int bookmarkId); - //게시판 북마크 삭제 - int deleteBookmarkBoard(BoardBookmarkCheckDto bookmark); - //북마크-웹사이트 조회 - List selectBookmarkWebsite(int memberId); - //사용자 태그 목록 조회 - List selectTags(int memberId, Long folderId); - //게시글 북마크 여부 확인 - int isBookmarked(int boardId, int memberId); + int deleteBookmark(Long memberId, Long bookmarkId); + + //북마크 삭제 (Link ID 기반 - soft delete) + int deleteBookmarkByLink(Long memberId, Long linkId); + + //링크 조회 (canonical_url로) + Link selectLinkByCanonicalUrl(String canonicalUrl); + + //링크 추가 (link 테이블) + int insertLink(Link link); + + //URL 기반 북마크 존재 여부 확인 (Board Bridge용) + int checkBookmarkExistsByUrl(Long memberId, String url); } diff --git a/src/main/java/com/web/SearchWeb/bookmark/dao/MybatisBookmarkDao.java b/src/main/java/com/web/SearchWeb/bookmark/dao/MybatisBookmarkDao.java index 6247e63..ab8e678 100644 --- a/src/main/java/com/web/SearchWeb/bookmark/dao/MybatisBookmarkDao.java +++ b/src/main/java/com/web/SearchWeb/bookmark/dao/MybatisBookmarkDao.java @@ -1,10 +1,8 @@ package com.web.SearchWeb.bookmark.dao; import com.web.SearchWeb.bookmark.domain.Bookmark; -import com.web.SearchWeb.bookmark.dto.BoardBookmarkCheckDto; -import com.web.SearchWeb.bookmark.dto.BookmarkCheckDto; +import com.web.SearchWeb.bookmark.domain.Link; import com.web.SearchWeb.bookmark.dto.BookmarkDto; -import com.web.SearchWeb.bookmark.domain.BookmarkWebsite; import com.web.SearchWeb.bookmark.dto.request.BookmarkSearchRequestDto; import org.apache.ibatis.session.SqlSession; import org.springframework.beans.factory.annotation.Autowired; @@ -19,35 +17,25 @@ public class MybatisBookmarkDao implements BookmarkDao { @Autowired public MybatisBookmarkDao(SqlSession sqlSession) { - //세션을 통해 mapper 컨테이너에서 mapper 객체를 꺼내 씀 mapper = sqlSession.getMapper(BookmarkDao.class); } /** - * 북마크 목록 조회 + * 북마크 중복 확인 */ @Override - public List selectBookmarkList(BookmarkSearchRequestDto searchRequest) { - return mapper.selectBookmarkList(searchRequest); + public int checkBookmarkExists(Long memberId, Long folderId, Long linkId) { + return mapper.checkBookmarkExists(memberId, folderId, linkId); } /** - * 북마크 확인 + * URL 기반 북마크 존재 여부 확인 (Board Bridge용) */ @Override - public int checkBookmark(BookmarkCheckDto bookmark) { - return mapper.checkBookmark(bookmark); - } - - - /** - * 게시판 북마크 확인 - */ - @Override - public int checkBoardBookmark(BoardBookmarkCheckDto checkDto) { - return mapper.checkBoardBookmark(checkDto); + public int checkBookmarkExistsByUrl(Long memberId, String url) { + return mapper.checkBookmarkExistsByUrl(memberId, url); } @@ -55,95 +43,69 @@ public int checkBoardBookmark(BoardBookmarkCheckDto checkDto) { * 북마크 단일 조회 */ @Override - public Bookmark selectBookmark(int memberId, int bookmarkId) { + public Bookmark selectBookmark(Long memberId, Long bookmarkId) { return mapper.selectBookmark(memberId, bookmarkId); } /** - * 북마크 추가 + * 북마크 목록 조회 */ @Override - public int insertBookmark(BookmarkDto bookmark) { - return mapper.insertBookmark(bookmark); + public List selectBookmarkList(BookmarkSearchRequestDto searchRequest) { + return mapper.selectBookmarkList(searchRequest); } /** - * 북마크 추가 (사용자 직접 추가) + * 링크 조회 (canonical_url로) */ @Override - public int insertBookmarkForUser(BookmarkDto bookmarkDto) { - return mapper.insertBookmarkForUser(bookmarkDto); + public Link selectLinkByCanonicalUrl(String canonicalUrl) { + return mapper.selectLinkByCanonicalUrl(canonicalUrl); } /** - * 북마크 추가 (게시판에서 추가) + * 링크 추가 */ @Override - public int insertBookmarkBoard(BookmarkDto bookmarkDto) { - return mapper.insertBookmarkBoard(bookmarkDto); + public int insertLink(Link link) { + return mapper.insertLink(link); } /** - * 북마크 수정 - */ - @Override - public int updateBookmark(BookmarkDto bookmarkDto, int bookmarkId) { - return mapper.updateBookmark(bookmarkDto, bookmarkId); - } - - - /** - * 북마크 삭제 + * 북마크 추가 */ @Override - public int deleteBookmark(BookmarkCheckDto bookmark) { - return mapper.deleteBookmark(bookmark); + public int insertBookmark(BookmarkDto bookmark, Long linkId) { + return mapper.insertBookmark(bookmark, linkId); } /** - * 마이페이지 북마크 삭제 + * 북마크 수정 */ @Override - public int deleteBookmarkMyPage(int memberId, int bookmarkId) { - return mapper.deleteBookmarkMyPage(memberId, bookmarkId); + public int updateBookmark(BookmarkDto bookmarkDto, Long bookmarkId) { + return mapper.updateBookmark(bookmarkDto, bookmarkId); } /** - * 게시판 북마크 삭제 - */ - @Override - public int deleteBookmarkBoard(BoardBookmarkCheckDto bookmark) { - return mapper.deleteBookmarkBoard(bookmark); - } - - /** - * 북마크-웹사이트 조회 + * 북마크 삭제 (soft delete) */ @Override - public List selectBookmarkWebsite(int memberId) { - return mapper.selectBookmarkWebsite(memberId); + public int deleteBookmark(Long memberId, Long bookmarkId) { + return mapper.deleteBookmark(memberId, bookmarkId); } - /** - * 사용자 태그 목록 조회 + * 북마크 삭제 (Link ID 기반 - soft delete) */ @Override - public List selectTags(int memberId, Long folderId) { - return mapper.selectTags(memberId, folderId); - } - - - /** - * 게시글 북마크 여부 확인 - */ - public int isBookmarked(int boardId, int memberId) { - return mapper.isBookmarked(boardId, memberId); + public int deleteBookmarkByLink(Long memberId, Long linkId) { + return mapper.deleteBookmarkByLink(memberId, linkId); } } diff --git a/src/main/java/com/web/SearchWeb/bookmark/domain/Bookmark.java b/src/main/java/com/web/SearchWeb/bookmark/domain/Bookmark.java index 7414e93..3d8de31 100644 --- a/src/main/java/com/web/SearchWeb/bookmark/domain/Bookmark.java +++ b/src/main/java/com/web/SearchWeb/bookmark/domain/Bookmark.java @@ -1,22 +1,33 @@ package com.web.SearchWeb.bookmark.domain; +import java.math.BigDecimal; +import java.util.List; +import com.web.SearchWeb.common.domain.BaseEntity; import lombok.Getter; import lombok.Setter; import lombok.ToString; +/** + * Bookmark 도메인 클래스 (MemberSavedLink) + * + * PostgreSQL member_saved_link 테이블과 매핑 + * 사용자가 저장한 링크 정보 + */ @Getter @Setter -@ToString -public class Bookmark { - private int bookmarkId; - private int member_memberId; - private int website_websiteId; - private int board_boardId; - private int folder_folderId; - private String name; - private String description; - private String url; - private String modified_date; - private String tag; +@ToString(callSuper = true) +public class Bookmark extends BaseEntity { + private Long bookmarkId; // 북마크 고유 ID (PK, member_saved_link_id) + private Long linkId; // 링크 ID (FK to link, 필수) + private Long linkEnrichmentId; // 링크 분석 정보 ID (FK to link_enrichment, 선택) + private Long memberFolderId; // 소유 폴더 ID (FK to member_folder, 필수) + private String displayTitle; // 표시 제목 (사용자 지정 또는 원본 제목) + private String note; // 메모 (선택 사항) + private Long primaryCategoryId; // 주 카테고리 ID (선택, AI 분류 또는 사용자 지정) + private String categorySource; // 카테고리 출처 ('system', 'member', default 'system') + private BigDecimal categoryScore; // 카테고리 정확도 점수 (0.0 ~ 1.0) + + // Link 객체 (Association) + private Link link; // Link 테이블과 조인된 객체 } diff --git a/src/main/java/com/web/SearchWeb/bookmark/domain/BookmarkWebsite.java b/src/main/java/com/web/SearchWeb/bookmark/domain/BookmarkWebsite.java deleted file mode 100644 index 830fa34..0000000 --- a/src/main/java/com/web/SearchWeb/bookmark/domain/BookmarkWebsite.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.web.SearchWeb.bookmark.domain; - - -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.Setter; -import lombok.ToString; - -@Getter -@Setter -@ToString -@AllArgsConstructor -public class BookmarkWebsite { - private int member_memberId; - private int websiteId; - private String name; - private String korean_name; - private String description; - private String url; - private String category; - private String subcategory; - private Long viewCount; -} diff --git a/src/main/java/com/web/SearchWeb/bookmark/domain/Link.java b/src/main/java/com/web/SearchWeb/bookmark/domain/Link.java new file mode 100644 index 0000000..97ced8a --- /dev/null +++ b/src/main/java/com/web/SearchWeb/bookmark/domain/Link.java @@ -0,0 +1,38 @@ +package com.web.SearchWeb.bookmark.domain; + +import com.web.SearchWeb.common.domain.BaseEntity; +import java.math.BigDecimal; +import java.time.OffsetDateTime; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; +import lombok.experimental.SuperBuilder; + +/** + * Link 도메인 클래스 + * - URL의 메타데이터를 저장하는 테이블 + */ +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@SuperBuilder +@ToString(callSuper = true) +public class Link extends BaseEntity { + private Long linkId; // 링크 ID + private String canonicalUrl; // 표준 URL (Canonical URL) + private String originalUrl; // 원본 URL + private String domain; // 도메인 + private String title; // 제목 + private String description; // 설명 + private String thumbnailUrl; // 썸네일 URL + private String faviconUrl; // 파비콘 URL + private String contentType; // 콘텐츠 타입 (기본: link) + private Long primaryCategoryId; // 대표 카테고리 ID + private BigDecimal categoryScore; // 카테고리 점수 + private String classifierVersion; // 분류기 버전 + private OffsetDateTime categorizedAt; // 분류 일시 +} diff --git a/src/main/java/com/web/SearchWeb/bookmark/dto/BoardBookmarkCheckDto.java b/src/main/java/com/web/SearchWeb/bookmark/dto/BoardBookmarkCheckDto.java index 02659fb..20b6898 100644 --- a/src/main/java/com/web/SearchWeb/bookmark/dto/BoardBookmarkCheckDto.java +++ b/src/main/java/com/web/SearchWeb/bookmark/dto/BoardBookmarkCheckDto.java @@ -10,6 +10,6 @@ @ToString @AllArgsConstructor public class BoardBookmarkCheckDto { - private int memberId; - private int boardId; + private Long memberId; + private Long bookmarkId; } \ No newline at end of file diff --git a/src/main/java/com/web/SearchWeb/bookmark/dto/BookmarkCheckDto.java b/src/main/java/com/web/SearchWeb/bookmark/dto/BookmarkCheckDto.java index 2f01f0f..b14a85e 100644 --- a/src/main/java/com/web/SearchWeb/bookmark/dto/BookmarkCheckDto.java +++ b/src/main/java/com/web/SearchWeb/bookmark/dto/BookmarkCheckDto.java @@ -10,6 +10,6 @@ @ToString @AllArgsConstructor public class BookmarkCheckDto { - private int member_memberId; + private Long bookmarkId; private int website_websiteId; } diff --git a/src/main/java/com/web/SearchWeb/bookmark/dto/BookmarkDto.java b/src/main/java/com/web/SearchWeb/bookmark/dto/BookmarkDto.java index 4cc7404..baddaa5 100644 --- a/src/main/java/com/web/SearchWeb/bookmark/dto/BookmarkDto.java +++ b/src/main/java/com/web/SearchWeb/bookmark/dto/BookmarkDto.java @@ -1,21 +1,28 @@ package com.web.SearchWeb.bookmark.dto; import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Getter; +import lombok.NoArgsConstructor; import lombok.Setter; import lombok.ToString; +/** + * Bookmark 저장/수정 요청 DTO + * + * member_saved_link + link 테이블에 저장 + */ @Setter @Getter @ToString +@NoArgsConstructor @AllArgsConstructor +@Builder public class BookmarkDto { - private int member_memberId; - private int website_websiteId; - private int board_boardId; - private Long folder_folderId; - private String name; - private String description; - private String url; - private String tag; + private Long memberFolderId; // member_folder_id (저장할 폴더) + private String displayTitle; // display_title (사용자가 지정한 제목) + private String url; // original_url (link 테이블에 저장) + private String note; // note (메모) + private Long primaryCategoryId; // primary_category_id (카테고리) + private Long createdByMemberId; // created_by_member_id (저장한 회원) } diff --git a/src/main/java/com/web/SearchWeb/bookmark/dto/request/BookmarkSearchRequestDto.java b/src/main/java/com/web/SearchWeb/bookmark/dto/request/BookmarkSearchRequestDto.java index 6c3ab77..c4ffbd8 100644 --- a/src/main/java/com/web/SearchWeb/bookmark/dto/request/BookmarkSearchRequestDto.java +++ b/src/main/java/com/web/SearchWeb/bookmark/dto/request/BookmarkSearchRequestDto.java @@ -5,14 +5,17 @@ import lombok.Getter; import lombok.NoArgsConstructor; +/** + * Bookmark 검색 요청 DTO + */ @Getter @NoArgsConstructor @AllArgsConstructor @Builder public class BookmarkSearchRequestDto { - private int memberId; - private String tag; - private String sort; - private String query; - private Long folderId; + private Long memberId; // created_by_member_id로 필터 + private Long folderId; // member_folder_id로 필터 + private String sort; // 정렬 기준 (Newest, Oldest) + private String query; // 검색어 (display_title 검색) + private Long categoryId; // primary_category_id로 필터 (태그 대체) } \ No newline at end of file diff --git a/src/main/java/com/web/SearchWeb/bookmark/service/BookmarkService.java b/src/main/java/com/web/SearchWeb/bookmark/service/BookmarkService.java index c7a98d3..78bd6ee 100644 --- a/src/main/java/com/web/SearchWeb/bookmark/service/BookmarkService.java +++ b/src/main/java/com/web/SearchWeb/bookmark/service/BookmarkService.java @@ -1,42 +1,46 @@ package com.web.SearchWeb.bookmark.service; import com.web.SearchWeb.bookmark.domain.Bookmark; +import com.web.SearchWeb.bookmark.domain.Link; import com.web.SearchWeb.bookmark.dto.BoardBookmarkCheckDto; -import com.web.SearchWeb.bookmark.dto.BookmarkCheckDto; import com.web.SearchWeb.bookmark.dto.BookmarkDto; -import com.web.SearchWeb.bookmark.domain.BookmarkWebsite; - import java.util.List; public interface BookmarkService { - //북마크 확인 - int checkBookmark(BookmarkCheckDto bookmark); - //게시판 북마크 확인 - int checkBoardBookmark(BoardBookmarkCheckDto checkDto); + //북마크 추가 + int insertBookmark(BookmarkDto bookmarkDto, String url); + //북마크 단일 조회 - Bookmark selectBookmark(int memberId, int bookmarkId); + Bookmark selectBookmark(Long memberId, Long bookmarkId); + //북마크 목록 조회 - List selectBookmarkList(int memberId, String tag, String sort, String query, Long folderId); - //북마크 추가 (메인리스트에서 추가) - int insertBookmark(BookmarkDto bookmark); - //북마크 추가 (마이페이지에서 추가) - int insertBookmarkForUser(BookmarkDto bookmarkDto); - //북마크 추가 (게시판에서 추가) - int insertBookmarkForBoard(BookmarkDto bookmarkdto); + List selectBookmarkList(Long memberId, Long folderId, String sort, String query, Long categoryId); + //북마크 수정 - int updateBookmark(BookmarkDto bookmarkDto, int bookmarkId); + int updateBookmark(BookmarkDto bookmarkDto, Long bookmarkId); + //북마크 삭제 - int deleteBookmark(BookmarkCheckDto bookmark); - //마이페이지 북마크 삭제 - int deleteBookmarkMyPage(int memberId,int bookmarkId); - //게시판 북마크 삭제 - int deleteBookmarkBoard(BoardBookmarkCheckDto bookmark); - //북마크-웹사이트 조회 - List selectBookmarkWebsite(int memberId); - //사용자 태그 목록 조회 - List selectTags(int memberId, Long folderId); - //게시글 북마크 여부 확인 - int isBookmarked(int boardId, int memberId); + int deleteBookmark(Long memberId, Long bookmarkId); + + // 링크 조회 또는 생성 (URL 정규화) + Link getOrCreateLink(String url, Long createdByMemberId); + + // 북마크 존재 여부 확인 (URL 기반) + boolean checkBookmarkExistsByUrl(Long memberId, String url); + + // ========== Legacy Board-Bookmark Methods ========== + + //게시글 북마크 확인 + int checkBoardBookmark(BoardBookmarkCheckDto checkDto); + + //게시글 북마크 여부 확인 (for boardDetail) + int isBookmarked(Long boardId, Long memberId); + + //게시글 북마크 추가 + int insertBookmarkForBoard(Long boardId, BookmarkDto bookmarkDto); + + //게시글 북마크 삭제 + int deleteBookmarkBoard(BoardBookmarkCheckDto checkDto); } diff --git a/src/main/java/com/web/SearchWeb/bookmark/service/BookmarkServiceImpl.java b/src/main/java/com/web/SearchWeb/bookmark/service/BookmarkServiceImpl.java index cb5f360..a76737d 100644 --- a/src/main/java/com/web/SearchWeb/bookmark/service/BookmarkServiceImpl.java +++ b/src/main/java/com/web/SearchWeb/bookmark/service/BookmarkServiceImpl.java @@ -2,14 +2,15 @@ import com.web.SearchWeb.bookmark.dao.BookmarkDao; import com.web.SearchWeb.bookmark.domain.Bookmark; +import com.web.SearchWeb.bookmark.domain.Link; import com.web.SearchWeb.bookmark.dto.BoardBookmarkCheckDto; -import com.web.SearchWeb.bookmark.dto.BookmarkCheckDto; import com.web.SearchWeb.bookmark.dto.BookmarkDto; -import com.web.SearchWeb.bookmark.domain.BookmarkWebsite; import com.web.SearchWeb.bookmark.dto.request.BookmarkSearchRequestDto; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import java.net.URI; import java.util.List; @Service @@ -22,29 +23,12 @@ public BookmarkServiceImpl(BookmarkDao bookmarkDao) { this.bookmarkDao = bookmarkDao; } - /** - * 북마크 확인 - */ - @Override - public int checkBookmark(BookmarkCheckDto bookmark) { - return bookmarkDao.checkBookmark(bookmark); - } - - - /** - * 게시판 북마크 확인 - */ - @Override - public int checkBoardBookmark(BoardBookmarkCheckDto checkDto) { - return bookmarkDao.checkBoardBookmark(checkDto); - } - /** * 북마크 단일 조회 */ @Override - public Bookmark selectBookmark(int memberId, int bookmarkId) { + public Bookmark selectBookmark(Long memberId, Long bookmarkId) { return bookmarkDao.selectBookmark(memberId, bookmarkId); } @@ -53,42 +37,81 @@ public Bookmark selectBookmark(int memberId, int bookmarkId) { * 북마크 목록 조회 */ @Override - public List selectBookmarkList(int memberId, String tag, String sort, String query, Long folderId) { + public List selectBookmarkList(Long memberId, Long folderId, String sort, String query, Long categoryId) { BookmarkSearchRequestDto searchRequest = BookmarkSearchRequestDto.builder() .memberId(memberId) - .tag(tag) + .folderId(folderId) .sort(sort) .query(query) - .folderId(folderId) + .categoryId(categoryId) .build(); return bookmarkDao.selectBookmarkList(searchRequest); } /** - * 북마크 추가 (메인리스트에서 추가) + * 링크 조회 또는 생성 (URL 정규화) */ @Override - public int insertBookmark(BookmarkDto bookmark) { - return bookmarkDao.insertBookmark(bookmark); + @Transactional + public Link getOrCreateLink(String url, Long createdByMemberId) { + // URL 정규화 (canonical URL 생성) + String canonicalUrl = normalizeUrl(url); + + // 기존 링크 조회 + Link existingLink = bookmarkDao.selectLinkByCanonicalUrl(canonicalUrl); + if (existingLink != null) { + return existingLink; + } + + // 새 링크 생성 + Link newLink = Link.builder() + .canonicalUrl(canonicalUrl) + .originalUrl(url) + .domain(extractDomain(url)) + .title(url) // 기본값, 나중에 메타데이터 추출로 업데이트 + .primaryCategoryId(1L) // 기본 카테고리 + .createdByMemberId(createdByMemberId) + .build(); + + bookmarkDao.insertLink(newLink); + return newLink; } /** - * 북마크 추가 (마이페이지에서 추가) + * 북마크 존재 여부 확인 (URL 기반) */ @Override - public int insertBookmarkForUser(BookmarkDto bookmarkDto) { - return bookmarkDao.insertBookmarkForUser(bookmarkDto); + public boolean checkBookmarkExistsByUrl(Long memberId, String url) { + String canonicalUrl = normalizeUrl(url); + // Link가 존재하는지 먼저 확인 (최적화) + Link link = bookmarkDao.selectLinkByCanonicalUrl(canonicalUrl); + if (link == null) { + return false; + } + // Link ID로 북마크 테이블 조회 + return bookmarkDao.checkBookmarkExistsByUrl(memberId, url) > 0; } /** - * 북마크 추가 (게시판에서 추가) + * 북마크 추가 */ @Override - public int insertBookmarkForBoard(BookmarkDto bookmarkDto) { - return bookmarkDao.insertBookmarkBoard(bookmarkDto); + @Transactional + public int insertBookmark(BookmarkDto bookmarkDto, String url) { + // 링크 조회 또는 생성 + Link link = getOrCreateLink(url, bookmarkDto.getCreatedByMemberId()); + + // 중복 확인 (기본 폴더 등에서) + int exists = bookmarkDao.checkBookmarkExists(bookmarkDto.getCreatedByMemberId(), bookmarkDto.getMemberFolderId(), link.getLinkId()); + if (exists > 0) { + return 0; // 이미 존재함 + } + + // 북마크 추가 + return bookmarkDao.insertBookmark(bookmarkDto, link.getLinkId()); } @@ -96,61 +119,94 @@ public int insertBookmarkForBoard(BookmarkDto bookmarkDto) { * 북마크 수정 */ @Override - public int updateBookmark(BookmarkDto bookmarkDto, int bookmarkId) { + public int updateBookmark(BookmarkDto bookmarkDto, Long bookmarkId) { return bookmarkDao.updateBookmark(bookmarkDto, bookmarkId); } /** - * 북마크 삭제 + * 북마크 삭제 (soft delete) */ @Override - public int deleteBookmark(BookmarkCheckDto bookmark) { - return bookmarkDao.deleteBookmark(bookmark); + public int deleteBookmark(Long memberId, Long bookmarkId) { + return bookmarkDao.deleteBookmark(memberId, bookmarkId); } - /** - * 마이페이지 북마크 삭제 + /** + * URL 정규화 (canonical URL 생성) */ - @Override - public int deleteBookmarkMyPage(int memberId, int bookmarkId) { - return bookmarkDao.deleteBookmarkMyPage(memberId, bookmarkId); + private String normalizeUrl(String url) { + try { + URI uri = new URI(url); + // 프로토콜 + 호스트 + 경로 (쿼리스트링, 프래그먼트 제거) + String normalized = uri.getScheme() + "://" + uri.getHost(); + if (uri.getPath() != null && !uri.getPath().isEmpty()) { + normalized += uri.getPath(); + } + // 끝의 슬래시 제거 + return normalized.endsWith("/") ? normalized.substring(0, normalized.length() - 1) : normalized; + } catch (Exception e) { + return url; // 정규화 실패 시 원본 반환 + } } /** - * 게시판 북마크 삭제 + * URL에서 도메인 추출 + */ + private String extractDomain(String url) { + try { + URI uri = new URI(url); + return uri.getHost(); + } catch (Exception e) { + return null; + } + } + + + // ========== Legacy Board-Bookmark Methods ========== + + /** + * 게시글 북마크 확인 (Legacy) + * TODO: 새 스키마에서는 board-bookmark 관계가 없음. 현재는 0 반환 */ @Override - public int deleteBookmarkBoard(BoardBookmarkCheckDto bookmark) { - return bookmarkDao.deleteBookmarkBoard(bookmark); + public int checkBoardBookmark(BoardBookmarkCheckDto checkDto) { + // 새 스키마에 board-bookmark 테이블이 없으므로 항상 0 반환 + return 0; } - - - + /** - * 북마크-웹사이트 조회 + * 게시글 북마크 여부 확인 (for boardDetail) + * TODO: 새 스키마에서는 board-bookmark 관계가 없음. 현재는 0 반환 */ @Override - public List selectBookmarkWebsite(int memberId) { - return bookmarkDao.selectBookmarkWebsite(memberId); + public int isBookmarked(Long boardId, Long memberId) { + // 새 스키마에 board-bookmark 테이블이 없으므로 항상 0 반환 + return 0; } - - + /** - * 사용자 태그 목록 조회 + * 게시글 북마크 추가 (Legacy) + * TODO: 새 스키마에서는 board-bookmark 관계가 없음. 현재는 0 반환 */ @Override - public List selectTags(int memberId, Long folderId) { - return bookmarkDao.selectTags(memberId, folderId); + public int insertBookmarkForBoard(Long boardId, BookmarkDto bookmarkDto) { + // 새 스키마에 board-bookmark 테이블이 없으므로 아무 작업 안함 + return 0; } - - + /** - * 게시글 북마크 여부 확인 + * 게시글 북마크 삭제 (Legacy) + * TODO: 새 스키마에서는 board-bookmark 관계가 없음. 현재는 0 반환 */ - public int isBookmarked(int boardId, int memberId) { - return bookmarkDao.isBookmarked(boardId, memberId); + @Override + public int deleteBookmarkBoard(BoardBookmarkCheckDto checkDto) { + // 새 스키마에 board-bookmark 테이블이 없으므로 아무 작업 안함 + return 0; } + + + } diff --git a/src/main/java/com/web/SearchWeb/comment/controller/CommentApiController.java b/src/main/java/com/web/SearchWeb/comment/controller/CommentApiController.java index ec177c1..b209644 100644 --- a/src/main/java/com/web/SearchWeb/comment/controller/CommentApiController.java +++ b/src/main/java/com/web/SearchWeb/comment/controller/CommentApiController.java @@ -6,7 +6,6 @@ import com.web.SearchWeb.comment.service.CommentService; import com.web.SearchWeb.member.dto.CustomOAuth2User; import com.web.SearchWeb.member.dto.CustomUserDetails; -import com.web.SearchWeb.member.service.MemberService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -20,6 +19,9 @@ import java.util.List; import java.util.Map; +/** + * CommentApiController (Legacy - PostgreSQL) + */ @RestController public class CommentApiController { @@ -36,7 +38,7 @@ public CommentApiController(CommentService commentService) { * 게시글 댓글 생성 */ @PostMapping("board/{boardId}/comment") - public ResponseEntity> insertComment(@PathVariable int boardId, + public ResponseEntity> insertComment(@PathVariable Long boardId, @AuthenticationPrincipal Object currentUser, @RequestBody CommentDto commentDto){ Map response = new HashMap<>(); @@ -48,22 +50,22 @@ public ResponseEntity> insertComment(@PathVariable int board .body(response); // 401 Unauthorized 응답 } - // 로그인 된 경우 - String username; + // 로그인 된 경우 (loginId 사용) + String loginId; if(currentUser instanceof UserDetails) { // 일반 로그인 사용자 처리 - username = ((CustomUserDetails) currentUser).getUsername(); + loginId = ((CustomUserDetails) currentUser).getUsername(); } else if(currentUser instanceof OAuth2User) { // 소셜 로그인 사용자 처리 - username = ((CustomOAuth2User) currentUser).getUsername(); + loginId = ((CustomOAuth2User) currentUser).getLoginId(); } else { return ResponseEntity .status(HttpStatus.FORBIDDEN) .body(response); // 403 Forbidden 응답 } - commentService.insertComment(boardId, username, commentDto); + commentService.insertComment(boardId, loginId, commentDto); response.put("success", true); return ResponseEntity.ok(response); // 200 OK 응답 @@ -74,7 +76,7 @@ else if(currentUser instanceof OAuth2User) { * 게시글 댓글 목록 조회 */ @GetMapping("board/{boardId}/comments") - public ResponseEntity> selectComments(@PathVariable int boardId, Model model){ + public ResponseEntity> selectComments(@PathVariable Long boardId, Model model){ List comments = commentService.selectComments(boardId); return ResponseEntity.ok(comments); } @@ -85,7 +87,7 @@ public ResponseEntity> selectComments(@PathVariable int boardId, M */ @GetMapping("board/{boardId}/comment/{commentId}") @OwnerCheck(idParam = "commentId", service = "commentService") - public ResponseEntity selectComment(@PathVariable int commentId){ + public ResponseEntity selectComment(@PathVariable Long commentId){ Comment comment = commentService.selectComment(commentId); return ResponseEntity.ok(comment); } @@ -96,8 +98,8 @@ public ResponseEntity selectComment(@PathVariable int commentId){ */ @PutMapping("board/{boardId}/comments/{commentId}") @OwnerCheck(idParam = "commentId", service = "commentService") - public ResponseEntity> updateComment(@PathVariable int boardId, - @PathVariable int commentId, + public ResponseEntity> updateComment(@PathVariable Long boardId, + @PathVariable Long commentId, @RequestBody CommentDto commentDto){ Map response = new HashMap<>(); commentService.updateComment(commentId, commentDto); @@ -111,8 +113,8 @@ public ResponseEntity> updateComment(@PathVariable int board */ @DeleteMapping("board/{boardId}/comments/{commentId}") @OwnerCheck(idParam = "commentId", service = "commentService") - public ResponseEntity> deleteComment(@PathVariable int boardId, - @PathVariable int commentId){ + public ResponseEntity> deleteComment(@PathVariable Long boardId, + @PathVariable Long commentId){ Map response = new HashMap<>(); commentService.deleteComment(boardId, commentId); response.put("success", true); diff --git a/src/main/java/com/web/SearchWeb/comment/dao/CommentDao.java b/src/main/java/com/web/SearchWeb/comment/dao/CommentDao.java index 2c10d4e..ef5b28f 100644 --- a/src/main/java/com/web/SearchWeb/comment/dao/CommentDao.java +++ b/src/main/java/com/web/SearchWeb/comment/dao/CommentDao.java @@ -6,28 +6,31 @@ import java.util.List; +/** + * CommentDao Interface (Legacy - PostgreSQL) + */ public interface CommentDao { //게시글 댓글 생성 int insertComment(Comment comment); //게시글 댓글 목록 조회 - List selectComments(int boardId); + List selectComments(Long boardId); - //회원번호로 게시글 댓글 목록 조회 - List selectCommentsByMemberId(int memberId); + //회원번호로 게시글 댓글 목록 조회 (Long for member FK) + List selectCommentsByMemberId(Long memberId); //게시글 댓글 단일 조회 - Comment selectComment(int commentId); + Comment selectComment(Long commentId); //게시글 댓글 수정 - int updateComment(int commentId, CommentDto commentDto); + int updateComment(Long commentId, CommentDto commentDto); //게시글 댓글 사용자 프로필 수정 int updateCommentUserProfile(UpdateUserProfileCommentDto commentDto); //게시글 댓글 삭제 - int deleteComment(int commentId); + int deleteComment(Long commentId); //게시글 댓글 수 조회 - int countComments(int boardId); + int countComments(Long boardId); } \ No newline at end of file diff --git a/src/main/java/com/web/SearchWeb/comment/dao/MybatisCommentDao.java b/src/main/java/com/web/SearchWeb/comment/dao/MybatisCommentDao.java index 82ca889..0094552 100644 --- a/src/main/java/com/web/SearchWeb/comment/dao/MybatisCommentDao.java +++ b/src/main/java/com/web/SearchWeb/comment/dao/MybatisCommentDao.java @@ -10,6 +10,9 @@ import java.util.List; +/** + * MybatisCommentDao (Legacy - PostgreSQL) + */ @Repository public class MybatisCommentDao implements CommentDao{ @@ -34,7 +37,7 @@ public int insertComment(Comment comment) { /** * 게시글 댓글 목록 조회 */ - public List selectComments(int boardId){ + public List selectComments(Long boardId){ return mapper.selectComments(boardId); } @@ -42,7 +45,7 @@ public List selectComments(int boardId){ /** * 회원번호로 게시글 댓글 목록 조회 */ - public List selectCommentsByMemberId(int memberId){ + public List selectCommentsByMemberId(Long memberId){ return mapper.selectCommentsByMemberId(memberId); } @@ -50,7 +53,7 @@ public List selectCommentsByMemberId(int memberId){ /** * 게시글 댓글 단일 조회 */ - public Comment selectComment(int commentId){ + public Comment selectComment(Long commentId){ return mapper.selectComment(commentId); } @@ -58,7 +61,7 @@ public Comment selectComment(int commentId){ /** * 게시글 댓글 수정 */ - public int updateComment(int commentId, CommentDto commentDto) { + public int updateComment(Long commentId, CommentDto commentDto) { return mapper.updateComment(commentId, commentDto); } @@ -74,7 +77,7 @@ public int updateCommentUserProfile(UpdateUserProfileCommentDto commentDto) { /** * 게시글 댓글 삭제 */ - public int deleteComment(int commentId){ + public int deleteComment(Long commentId){ return mapper.deleteComment(commentId); } @@ -82,7 +85,7 @@ public int deleteComment(int commentId){ /** * 게시글 댓글 수 조회 */ - public int countComments(int boardId) { + public int countComments(Long boardId) { return mapper.countComments(boardId); } } \ No newline at end of file diff --git a/src/main/java/com/web/SearchWeb/comment/domain/Comment.java b/src/main/java/com/web/SearchWeb/comment/domain/Comment.java index a43dae2..a8a6f8f 100644 --- a/src/main/java/com/web/SearchWeb/comment/domain/Comment.java +++ b/src/main/java/com/web/SearchWeb/comment/domain/Comment.java @@ -4,16 +4,21 @@ import lombok.Setter; import lombok.ToString; +/** + * Comment 도메인 (Legacy) + * - PostgreSQL comment 테이블과 매핑 + * - Member, Board 테이블과 FK 관계 + */ @Getter @Setter @ToString public class Comment { - private int commentId; - private int board_boardId; - private int member_memberId; - private String member_nickname; - private String member_job; - private String member_major; - private String content; - private String created_date; + private Long commentId; // comment_id (BIGINT) + private Long boardBoardId; // board_board_id (FK to board) + private Long memberMemberId; // member_member_id (FK to member - BIGINT) + private String memberNickname; // member_nickname + private String memberJob; // member_job + private String memberMajor; // member_major + private String content; // content + private String createdDate; // created_date } diff --git a/src/main/java/com/web/SearchWeb/comment/dto/CommentDto.java b/src/main/java/com/web/SearchWeb/comment/dto/CommentDto.java index 8631c0e..00e1b73 100644 --- a/src/main/java/com/web/SearchWeb/comment/dto/CommentDto.java +++ b/src/main/java/com/web/SearchWeb/comment/dto/CommentDto.java @@ -8,8 +8,8 @@ @Setter @ToString public class CommentDto { - private int board_boardId; - private int member_memberId; + private Long board_boardId; + private Long member_memberId; private String member_nickname; private String content; } diff --git a/src/main/java/com/web/SearchWeb/comment/dto/UpdateUserProfileCommentDto.java b/src/main/java/com/web/SearchWeb/comment/dto/UpdateUserProfileCommentDto.java index 9983e4d..9e451ec 100644 --- a/src/main/java/com/web/SearchWeb/comment/dto/UpdateUserProfileCommentDto.java +++ b/src/main/java/com/web/SearchWeb/comment/dto/UpdateUserProfileCommentDto.java @@ -1,13 +1,18 @@ package com.web.SearchWeb.comment.dto; +/** + * 사용자 프로필 변경 시 댓글 정보 업데이트용 DTO + * + * PostgreSQL 호환 - Long commentId + */ public record UpdateUserProfileCommentDto( - int commentId, + Long commentId, String nickname, String job, String major ) { - public static UpdateUserProfileCommentDto of(int commentId, String nickname, String job, String major) { + public static UpdateUserProfileCommentDto of(Long commentId, String nickname, String job, String major) { return new UpdateUserProfileCommentDto(commentId, nickname, job, major); } diff --git a/src/main/java/com/web/SearchWeb/comment/service/CommentService.java b/src/main/java/com/web/SearchWeb/comment/service/CommentService.java index bef6813..dc4860d 100644 --- a/src/main/java/com/web/SearchWeb/comment/service/CommentService.java +++ b/src/main/java/com/web/SearchWeb/comment/service/CommentService.java @@ -12,6 +12,9 @@ import java.util.List; +/** + * CommentService (Legacy - PostgreSQL) + */ @Service public class CommentService { @@ -31,15 +34,15 @@ public CommentService(CommentDao commentdao, MemberService memberService, BoardD * 게시글 댓글 생성 */ @Transactional - public int insertComment(int boardId, String username, CommentDto commentDto){ + public int insertComment(Long boardId, String loginId, CommentDto commentDto){ // 댓글 추가 - Member member = memberService.findByUserName(username); + Member member = memberService.findByLoginId(loginId); Comment comment = new Comment(); - comment.setBoard_boardId(boardId); - comment.setMember_memberId(member.getMemberId()); - comment.setMember_nickname(member.getNickname()); - comment.setMember_job(member.getJob()); - comment.setMember_major(member.getMajor()); + comment.setBoardBoardId(boardId); + comment.setMemberMemberId(member.getMemberId()); + comment.setMemberNickname(member.getNickName()); + comment.setMemberJob(member.getJob()); + comment.setMemberMajor(member.getMajor()); comment.setContent(commentDto.getContent()); int result = commentdao.insertComment(comment); @@ -53,7 +56,7 @@ public int insertComment(int boardId, String username, CommentDto commentDto){ /** * 게시글 댓글 목록 조회 */ - public List selectComments(int boardId){ + public List selectComments(Long boardId){ return commentdao.selectComments(boardId); } @@ -61,7 +64,7 @@ public List selectComments(int boardId){ /** * 게시글 댓글 단일 조회 */ - public Comment selectComment(int commentId){ + public Comment selectComment(Long commentId){ return commentdao.selectComment(commentId); } @@ -69,7 +72,7 @@ public Comment selectComment(int commentId){ /** * 게시글 댓글 수정 */ - public int updateComment(int commentId, CommentDto commentDto){ + public int updateComment(Long commentId, CommentDto commentDto){ return commentdao.updateComment(commentId, commentDto); } @@ -78,7 +81,7 @@ public int updateComment(int commentId, CommentDto commentDto){ * 게시글 댓글 삭제 */ @Transactional - public int deleteComment(int boardId, int commentId){ + public int deleteComment(Long boardId, Long commentId){ // 댓글 삭제 int result = commentdao.deleteComment(commentId); @@ -91,15 +94,17 @@ public int deleteComment(int boardId, int commentId){ /** * 게시글 댓글 수 조회 */ - public int getCommentCount(int boardId) { + public int getCommentCount(Long boardId) { return commentdao.countComments(boardId); } - // 댓글 소유자(작성자) 조회 - public int findMemberIdByCommentId(int commentId) { + /** + * 댓글 소유자(작성자) 조회 + */ + public Long findMemberIdByCommentId(Long commentId) { Comment comment = commentdao.selectComment(commentId); if (comment == null) throw new IllegalArgumentException("게시글이 존재하지 않습니다."); - return comment.getMember_memberId(); + return comment.getMemberMemberId(); } } diff --git a/src/main/java/com/web/SearchWeb/common/domain/BaseEntity.java b/src/main/java/com/web/SearchWeb/common/domain/BaseEntity.java new file mode 100644 index 0000000..95f0a66 --- /dev/null +++ b/src/main/java/com/web/SearchWeb/common/domain/BaseEntity.java @@ -0,0 +1,92 @@ +package com.web.SearchWeb.common.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.MappedSuperclass; + +import java.io.Serializable; +import java.time.OffsetDateTime; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; +import lombok.experimental.SuperBuilder; + +import org.hibernate.annotations.SQLRestriction; +import org.springframework.data.annotation.CreatedBy; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedBy; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +/** + * BaseEntity + * - JPA & MyBatis Hybrid Support + */ +@Getter +@Setter +@ToString +@SuperBuilder +@NoArgsConstructor +@AllArgsConstructor +@MappedSuperclass +@EntityListeners(AuditingEntityListener.class) +@SQLRestriction("deleted_at IS NULL") // JPA(Hibernate)에서만 적용 +public abstract class BaseEntity implements Serializable { + + /* ======================= + * Audit Fields + * ======================= */ + + @CreatedDate + @Column(name = "created_at", updatable = false) + private OffsetDateTime createdAt; + + @LastModifiedDate + @Column(name = "updated_at") + private OffsetDateTime updatedAt; + + @Column(name = "deleted_at") + private OffsetDateTime deletedAt; + + @CreatedBy + @Column(name = "created_by_member_id", updatable = false) + private Long createdByMemberId; + + @LastModifiedBy + @Column(name = "updated_by_member_id") + private Long updatedByMemberId; + + @Column(name = "deleted_by_member_id") + private Long deletedByMemberId; + + + /* ======================= + * Domain Behaviors + * ======================= */ + + /** Soft delete (logical delete) */ + public void softDelete(Long deletedByMemberId) { + this.deletedAt = OffsetDateTime.now(); + this.deletedByMemberId = deletedByMemberId; + } + + /** Set creation audit fields manually (MyBatis / 명시적 호출용) */ + public void markAsCreated(Long memberId, OffsetDateTime now) { + this.createdAt = now; + this.createdByMemberId = memberId; + } + + /** Update audit fields manually (MyBatis / 명시적 호출용) */ + public void markAsUpdated(Long memberId, OffsetDateTime now) { + this.updatedAt = now; + this.updatedByMemberId = memberId; + } + + /** Convenience method */ + public boolean isDeleted() { + return this.deletedAt != null; + } +} diff --git a/src/main/java/com/web/SearchWeb/config/SecurityConfig.java b/src/main/java/com/web/SearchWeb/config/SecurityConfig.java index 756f0f1..afd01bc 100644 --- a/src/main/java/com/web/SearchWeb/config/SecurityConfig.java +++ b/src/main/java/com/web/SearchWeb/config/SecurityConfig.java @@ -46,6 +46,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{ .formLogin((auth) -> auth .loginPage("/login") .loginProcessingUrl("/loginProc") //Spring Security가 제공하는 기본 인증 필터(UsernamePasswordAuthenticationFilter)를 통해 자동으로 로그인 요청을 처리 + .usernameParameter("loginId") // 로그인 폼에서 아이디 입력 필드의 name 속성값 (기본: username -> loginId로 변경) .successHandler(customAuthenticationSuccessHandler()) // 커스텀 성공 핸들러 추가 .failureHandler(customAuthenticationFailureHandler()) // 커스텀 실패 핸들러 추가 .permitAll() diff --git a/src/main/java/com/web/SearchWeb/likes/dao/LikesDao.java b/src/main/java/com/web/SearchWeb/likes/dao/LikesDao.java index ded5bc7..43677c8 100644 --- a/src/main/java/com/web/SearchWeb/likes/dao/LikesDao.java +++ b/src/main/java/com/web/SearchWeb/likes/dao/LikesDao.java @@ -1,16 +1,15 @@ package com.web.SearchWeb.likes.dao; - public interface LikesDao { // 게시글 좋아요 상태 확인 - Boolean isLikedByMember( int boardId, int memberId); + Boolean isLikedByMember(Long boardId, Long memberId); // 게시글 좋아요 추가 - int likeBoard(int boardId, int memberId); + int likeBoard(Long boardId, Long memberId); // 게시글 좋아요 취소 - int unlikeBoard(int boardId, int memberId); + int unlikeBoard(Long boardId, Long memberId); // 게시글 좋아요 수 조회 - int countLikes(int boardId); + int countLikes(Long boardId); } diff --git a/src/main/java/com/web/SearchWeb/likes/dao/MybatisLikesDao.java b/src/main/java/com/web/SearchWeb/likes/dao/MybatisLikesDao.java index 666a16a..11bb941 100644 --- a/src/main/java/com/web/SearchWeb/likes/dao/MybatisLikesDao.java +++ b/src/main/java/com/web/SearchWeb/likes/dao/MybatisLikesDao.java @@ -19,7 +19,7 @@ public MybatisLikesDao(SqlSession sqlSession) { * 게시글 좋아요 상태 확인 */ @Override - public Boolean isLikedByMember(int boardId, int memberId) { + public Boolean isLikedByMember(Long boardId, Long memberId) { return mapper.isLikedByMember(boardId, memberId); } @@ -28,7 +28,7 @@ public Boolean isLikedByMember(int boardId, int memberId) { * 게시글 좋아요 추가 */ @Override - public int likeBoard(int boardId, int memberId) { + public int likeBoard(Long boardId, Long memberId) { return mapper.likeBoard(boardId, memberId); } @@ -37,7 +37,7 @@ public int likeBoard(int boardId, int memberId) { * 게시글 좋아요 취소 */ @Override - public int unlikeBoard(int boardId, int memberId) { + public int unlikeBoard(Long boardId, Long memberId) { return mapper.unlikeBoard(boardId, memberId); } @@ -46,7 +46,7 @@ public int unlikeBoard(int boardId, int memberId) { * 게시글 좋아요 수 조회 */ @Override - public int countLikes(int boardId) { + public int countLikes(Long boardId) { return mapper.countLikes(boardId); } } \ No newline at end of file diff --git a/src/main/java/com/web/SearchWeb/likes/service/LikesService.java b/src/main/java/com/web/SearchWeb/likes/service/LikesService.java index 141c47c..bd49209 100644 --- a/src/main/java/com/web/SearchWeb/likes/service/LikesService.java +++ b/src/main/java/com/web/SearchWeb/likes/service/LikesService.java @@ -22,7 +22,7 @@ public LikesService(LikesDao likesDao, BoardDao boardDao) { /** * 게시글 좋아요 상태 확인 */ - public boolean isLiked(int boardId, int memberId) { + public boolean isLiked(Long boardId, Long memberId) { Boolean isLiked = likesDao.isLikedByMember(boardId, memberId); return Boolean.TRUE.equals(isLiked); } @@ -32,7 +32,7 @@ public boolean isLiked(int boardId, int memberId) { * 게시글 좋아요 추가/취소 */ @Transactional - public boolean toggleLike(int boardId, int memberId) { + public boolean toggleLike(Long boardId, Long memberId) { //게시글 좋아요 상태 확인 Boolean isLiked = likesDao.isLikedByMember(boardId, memberId); @@ -53,7 +53,7 @@ public boolean toggleLike(int boardId, int memberId) { /** * 게시글 좋아요 수 조회 */ - public int getLikeCount(int boardId) { + public int getLikeCount(Long boardId) { return likesDao.countLikes(boardId); } diff --git a/src/main/java/com/web/SearchWeb/main/dao/MainDao.java b/src/main/java/com/web/SearchWeb/main/dao/MainDao.java index 5d30a1b..d97812c 100644 --- a/src/main/java/com/web/SearchWeb/main/dao/MainDao.java +++ b/src/main/java/com/web/SearchWeb/main/dao/MainDao.java @@ -7,7 +7,7 @@ public interface MainDao { //웹사이트 조회 - Website selectWebsite(int websiteId); + Website selectWebsite(Long websiteId); //카테고리별 웹사이트 목록 조회 List getListByCategory(String category); diff --git a/src/main/java/com/web/SearchWeb/main/dao/MybatisMainDao.java b/src/main/java/com/web/SearchWeb/main/dao/MybatisMainDao.java index 3dfba79..bc0b831 100644 --- a/src/main/java/com/web/SearchWeb/main/dao/MybatisMainDao.java +++ b/src/main/java/com/web/SearchWeb/main/dao/MybatisMainDao.java @@ -18,7 +18,7 @@ public MybatisMainDao(SqlSession sqlSession) { } @Override - public Website selectWebsite(int websiteId) { + public Website selectWebsite(Long websiteId) { return mapper.selectWebsite(websiteId); } diff --git a/src/main/java/com/web/SearchWeb/main/domain/Website.java b/src/main/java/com/web/SearchWeb/main/domain/Website.java index 6e03a64..73b6be3 100644 --- a/src/main/java/com/web/SearchWeb/main/domain/Website.java +++ b/src/main/java/com/web/SearchWeb/main/domain/Website.java @@ -6,16 +6,19 @@ import lombok.ToString; +/** + * Website 도메인 + */ @Getter @Setter @ToString public class Website { - private int websiteId; - private String name; - private String korean_name; - private String description; - private String url; - private String category; - private String subcategory; - private Long viewCount; + private Long websiteId; // website_id + private String name; // name + private String koreanName; // korean_name + private String description; // description + private String url; // url + private String category; // category + private String subcategory; // subcategory + private Long viewCount; // view_count } diff --git a/src/main/java/com/web/SearchWeb/main/service/MainService.java b/src/main/java/com/web/SearchWeb/main/service/MainService.java index 48d3026..24bb9ac 100644 --- a/src/main/java/com/web/SearchWeb/main/service/MainService.java +++ b/src/main/java/com/web/SearchWeb/main/service/MainService.java @@ -7,7 +7,7 @@ public interface MainService { //웹사이트 조회 - Website selectWebsite(int websiteId); + Website selectWebsite(Long websiteId); //카테고리별 웹사이트 목록 조회 List getListByCategory(String category); diff --git a/src/main/java/com/web/SearchWeb/main/service/MainServiceImpl.java b/src/main/java/com/web/SearchWeb/main/service/MainServiceImpl.java index 6308de6..faa7b7f 100644 --- a/src/main/java/com/web/SearchWeb/main/service/MainServiceImpl.java +++ b/src/main/java/com/web/SearchWeb/main/service/MainServiceImpl.java @@ -21,7 +21,7 @@ public MainServiceImpl(MainDao mainDao) { * 웹사이트 조회 */ @Override - public Website selectWebsite(int websiteId) { + public Website selectWebsite(Long websiteId) { return mainDao.selectWebsite(websiteId); } diff --git a/src/main/java/com/web/SearchWeb/member/controller/MemberController.java b/src/main/java/com/web/SearchWeb/member/controller/MemberController.java index ff53ada..8984805 100644 --- a/src/main/java/com/web/SearchWeb/member/controller/MemberController.java +++ b/src/main/java/com/web/SearchWeb/member/controller/MemberController.java @@ -4,7 +4,6 @@ import com.web.SearchWeb.member.dto.MemberDto; import com.web.SearchWeb.member.service.MemberService; -import jakarta.servlet.http.HttpSession; import jakarta.validation.Valid; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; @@ -19,10 +18,8 @@ * 코드 작성자: 서진영(jin2304) * 코드 설명 :회원가입 및 로그인 기능을 담당하는 컨트롤러 * 코드 주요 기능: 회원가입, 로그인 - * 코드 작성일: 2024.06.30 ~ 2024.07.04 * */ - @Controller public class MemberController { @@ -62,8 +59,8 @@ public String joinProcess(@Valid MemberDto member, BindingResult bindingResult, } // 사용자 아이디 중복 확인 - if (memberService.findByUserName(member.getUsername()) != null) { - bindingResult.rejectValue("username", "error.member", "이미 존재하는 아이디입니다."); + if (memberService.findByLoginId(member.getLoginId()) != null) { + bindingResult.rejectValue("loginId", "error.member", "이미 존재하는 아이디입니다."); redirectAttributes.addFlashAttribute("org.springframework.validation.BindingResult.memberDto", bindingResult); redirectAttributes.addFlashAttribute("memberDto", member); return "redirect:/join"; diff --git a/src/main/java/com/web/SearchWeb/member/dao/MemberDao.java b/src/main/java/com/web/SearchWeb/member/dao/MemberDao.java index 5bff5d7..6a50a63 100644 --- a/src/main/java/com/web/SearchWeb/member/dao/MemberDao.java +++ b/src/main/java/com/web/SearchWeb/member/dao/MemberDao.java @@ -13,14 +13,14 @@ public interface MemberDao { public void SocialjoinProcess(Member member); //회원번호로 찾기 - public Member findByMemberId(int memberId); + public Member findByMemberId(Long memberId); //로그인 아이디로 찾기 - public Member findByUserName(String username); + public Member findByLoginId(String loginId); //회원 수정 - public int updateMember(int memberId, MemberUpdateDto memberUpdateDto); + public int updateMember(Long memberId, MemberUpdateDto memberUpdateDto); //소셜 회원 수정 - public int updateSocialMember(int memberId, Member member); + public int updateSocialMember(Long memberId, Member member); } diff --git a/src/main/java/com/web/SearchWeb/member/dao/MybatisMemberDao.java b/src/main/java/com/web/SearchWeb/member/dao/MybatisMemberDao.java index 9713057..708c696 100644 --- a/src/main/java/com/web/SearchWeb/member/dao/MybatisMemberDao.java +++ b/src/main/java/com/web/SearchWeb/member/dao/MybatisMemberDao.java @@ -34,18 +34,20 @@ public void SocialjoinProcess(Member member) { mapper.SocialjoinProcess(member); } - + /** + * 회원번호로 찾기 + */ @Override - public Member findByMemberId(int memberId) { - Member findUser = mapper.findByMemberId(memberId); - return findUser; + public Member findByMemberId(Long memberId) { + return mapper.findByMemberId(memberId); } - + /** + * 로그인 아이디로 찾기 + */ @Override - public Member findByUserName(String username) { - Member findUser = mapper.findByUserName(username); - return findUser; + public Member findByLoginId(String loginId) { + return mapper.findByLoginId(loginId); } @@ -53,7 +55,7 @@ public Member findByUserName(String username) { * 회원 수정 */ @Override - public int updateMember(int memberId, MemberUpdateDto memberUpdateDto) { + public int updateMember(Long memberId, MemberUpdateDto memberUpdateDto) { return mapper.updateMember(memberId, memberUpdateDto); } @@ -62,7 +64,7 @@ public int updateMember(int memberId, MemberUpdateDto memberUpdateDto) { * 소셜 회원 수정 */ @Override - public int updateSocialMember(int memberId, Member member) { + public int updateSocialMember(Long memberId, Member member) { return mapper.updateSocialMember(memberId, member); } } diff --git a/src/main/java/com/web/SearchWeb/member/domain/Member.java b/src/main/java/com/web/SearchWeb/member/domain/Member.java index 2d859bf..9e48ae4 100644 --- a/src/main/java/com/web/SearchWeb/member/domain/Member.java +++ b/src/main/java/com/web/SearchWeb/member/domain/Member.java @@ -1,21 +1,24 @@ package com.web.SearchWeb.member.domain; +import com.web.SearchWeb.common.domain.BaseEntity; import lombok.Getter; import lombok.Setter; import lombok.ToString; @Getter @Setter -@ToString -public class Member { - private int memberId; - private String username; - private String password; - private String nickname; - private String email; - private String job; - private String major; - private String summary; - private String role; +@ToString(callSuper = true) +public class Member extends BaseEntity { + private Long memberId; // 회원 고유 ID (PK) + private String email; // 이메일 (로그인/알림용, Unique) + private String loginId; // 로그인 ID (구 username, Unique) + private String passwordHash; // 비밀번호 (BCrypt 암호화) + private String memberName; // 사용자 실명 + private String nickName; // 닉네임 (화면 표시용) + private String job; // 직업 + private String major; // 전공/관심분야 + private String summary; // 자기소개 요약 + private String status; // 계정 상태 (active, blocked 등) + private String role; // 권한 (ROLE_USER, ROLE_ADMIN 등) } diff --git a/src/main/java/com/web/SearchWeb/member/dto/CustomOAuth2User.java b/src/main/java/com/web/SearchWeb/member/dto/CustomOAuth2User.java index 2a3a818..8f40500 100644 --- a/src/main/java/com/web/SearchWeb/member/dto/CustomOAuth2User.java +++ b/src/main/java/com/web/SearchWeb/member/dto/CustomOAuth2User.java @@ -27,12 +27,12 @@ * -getAuthorities(): 사용자 권한 반환. * -getName(): 소셜 서비스에서 제공한 사용자 이름 반환. * -getMemberId(): 데이터베이스에 저장된 사용자 고유 ID 반환. - * -getUsername(): 소셜 서비스 이름과 사용자 ID를 조합한 고유 식별값 반환. + * -getLoginId(): 소셜 서비스 이름과 사용자 ID를 조합한 고유 식별값 반환. * * 클래스 주요 필드: * -OAuth2Response oAuth2Response: 소셜 서비스에서 가져온 사용자 정보. * -String role: Searchweb 서비스에 부여한 사용자 권한(Role). - * -int memberId: 데이터베이스에 저장된 사용자 고유 ID. + * -Long memberId: 데이터베이스에 저장된 사용자 고유 ID. * * 소셜 서비스 지원 플랫폼: * -naver, google, kakao @@ -45,9 +45,9 @@ public class CustomOAuth2User implements OAuth2User { private final OAuth2Response oAuth2Response; private final String role; - private final int memberId; + private final Long memberId; - public CustomOAuth2User(OAuth2Response oAuth2Response, String role, int memberId) { + public CustomOAuth2User(OAuth2Response oAuth2Response, String role, Long memberId) { this.oAuth2Response = oAuth2Response; this.role = role; this.memberId = memberId; @@ -76,13 +76,13 @@ public String getName() { } - public int getMemberId() { + public Long getMemberId() { return memberId; } - public String getUsername() { - return oAuth2Response.getProvider()+oAuth2Response.getProviderId(); + public String getLoginId() { + return oAuth2Response.getProvider() + oAuth2Response.getProviderId(); } diff --git a/src/main/java/com/web/SearchWeb/member/dto/CustomUserDetails.java b/src/main/java/com/web/SearchWeb/member/dto/CustomUserDetails.java index 2794350..875e4d5 100644 --- a/src/main/java/com/web/SearchWeb/member/dto/CustomUserDetails.java +++ b/src/main/java/com/web/SearchWeb/member/dto/CustomUserDetails.java @@ -10,7 +10,11 @@ -//로그인 검증 로직 +/** + * CustomUserDetails 클래스 + * + * 로그인 검증 로직 + */ public class CustomUserDetails implements UserDetails { private Member findUser; @@ -38,11 +42,11 @@ public String getAuthority() { } /** - * 아이디 찾기 + * 로그인 아이디 반환 (Spring Security의 username 역할) */ @Override public String getUsername() { - return findUser.getUsername(); + return findUser.getLoginId(); } /** @@ -50,11 +54,14 @@ public String getUsername() { */ @Override public String getPassword() { - return findUser.getPassword(); + return findUser.getPasswordHash(); } - public int getMemberId() { + /** + * 회원 ID 반환 + */ + public Long getMemberId() { return findUser.getMemberId(); } @@ -68,7 +75,7 @@ public boolean isAccountNonExpired() { @Override public boolean isAccountNonLocked() { - return true; + return !"blocked".equals(findUser.getStatus()); } @Override @@ -78,6 +85,6 @@ public boolean isCredentialsNonExpired() { @Override public boolean isEnabled() { - return true; + return findUser.getDeletedAt() == null; } } diff --git a/src/main/java/com/web/SearchWeb/member/dto/MemberDto.java b/src/main/java/com/web/SearchWeb/member/dto/MemberDto.java index 8526d33..f8b2b2a 100644 --- a/src/main/java/com/web/SearchWeb/member/dto/MemberDto.java +++ b/src/main/java/com/web/SearchWeb/member/dto/MemberDto.java @@ -1,19 +1,23 @@ package com.web.SearchWeb.member.dto; +import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Size; import lombok.Getter; import lombok.Setter; import lombok.ToString; +/** + * 회원가입 요청 DTO + */ @Getter @Setter @ToString public class MemberDto { @NotBlank(message = "아이디는 필수 입력 값입니다.") @Size(min = 4, max = 30, message = "아이디는 4자 이상 30자 이하로 입력해주세요.") - private String username; + private String loginId; @NotBlank(message = "비밀번호는 필수 입력 값입니다.") @Size(min = 4, max = 30, message = "비밀번호는 4자 이상 30자 이하로 입력해주세요.") @@ -22,6 +26,14 @@ public class MemberDto { @NotBlank(message = "비밀번호 확인은 필수 입력 값입니다.") private String confirmPassword; - private String nickname; + @Size(max = 20, message = "이름은 20자 이하로 입력해주세요.") + private String memberName; + + @Size(max = 20, message = "닉네임은 20자 이하로 입력해주세요.") + private String nickName; + + @Email(message = "올바른 이메일 형식이어야 합니다.") + private String email; + private String role; } diff --git a/src/main/java/com/web/SearchWeb/member/service/CustomOAuth2UserService.java b/src/main/java/com/web/SearchWeb/member/service/CustomOAuth2UserService.java index 244389a..99f7e60 100644 --- a/src/main/java/com/web/SearchWeb/member/service/CustomOAuth2UserService.java +++ b/src/main/java/com/web/SearchWeb/member/service/CustomOAuth2UserService.java @@ -78,26 +78,27 @@ else if(registrationId.equals("kakao")){ /* 회원가입 및 로그인 로직 */ // 사용자 고유 식별값 생성 (소셜 서비스 이름 + 소셜 서비스 사용자 ID) - String username = oAuth2Response.getProvider()+oAuth2Response.getProviderId(); + String loginId = oAuth2Response.getProvider() + oAuth2Response.getProviderId(); // 기존 사용자 정보 조회 - Member existMember = memberDao.findByUserName(username); + Member existMember = memberDao.findByLoginId(loginId); // 기본 사용자 역할 설정 String role = "ROLE_USER"; - int memberId; + Long memberId; // 사용자가 존재하지않으면 회원가입 if(existMember == null) { Member member = new Member(); - member.setUsername(username); - member.setPassword("1111"); - member.setNickname("닉네임"); //닉네임 임시 설정 + member.setLoginId(loginId); + member.setPasswordHash("1111"); // 소셜 로그인은 비밀번호 불필요하지만 NOT NULL 제약 대응 + member.setMemberName(oAuth2Response.getName() != null ? oAuth2Response.getName() : "Unknown"); // member_name (NOT NULL) + member.setNickName("닉네임"); // 닉네임 임시 설정 member.setEmail(oAuth2Response.getEmail()); member.setRole(role); memberDao.SocialjoinProcess(member); memberId = member.getMemberId(); }// 사용자가 이미 존재한다면 업데이트 else{ - existMember.setUsername(username); + existMember.setLoginId(loginId); existMember.setEmail(oAuth2Response.getEmail()); role = existMember.getRole(); memberId = existMember.getMemberId(); diff --git a/src/main/java/com/web/SearchWeb/member/service/CustomerUserDetailService.java b/src/main/java/com/web/SearchWeb/member/service/CustomerUserDetailService.java index 4d0047d..8a792e6 100644 --- a/src/main/java/com/web/SearchWeb/member/service/CustomerUserDetailService.java +++ b/src/main/java/com/web/SearchWeb/member/service/CustomerUserDetailService.java @@ -10,6 +10,11 @@ import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; +/** + * CustomerUserDetailService + * + * Spring Security 사용자 인증 서비스 + */ @Service public class CustomerUserDetailService implements UserDetailsService { @@ -23,8 +28,8 @@ public CustomerUserDetailService(MybatisMemberDao mybatisMemberDao) { @Override - public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { - Member findUser = mybatisMemberDao.findByUserName(username); + public UserDetails loadUserByUsername(String loginId) throws UsernameNotFoundException { + Member findUser = mybatisMemberDao.findByLoginId(loginId); if(findUser != null){ //spring security에 전달해서 검증 return new CustomUserDetails(findUser); diff --git a/src/main/java/com/web/SearchWeb/member/service/MemberService.java b/src/main/java/com/web/SearchWeb/member/service/MemberService.java index 3706a44..6a8c31c 100644 --- a/src/main/java/com/web/SearchWeb/member/service/MemberService.java +++ b/src/main/java/com/web/SearchWeb/member/service/MemberService.java @@ -10,14 +10,14 @@ public interface MemberService { public void joinProcess(MemberDto member); //회원번호로 찾기 - public Member findByMemberId(int memberId); + public Member findByMemberId(Long memberId); //로그인 아이디로 찾기 - public Member findByUserName(String username); + public Member findByLoginId(String loginId); //비밀번호 확인 public boolean isPasswordMatching(MemberDto memberDto); //회원 수정 - public int updateMember(int memberId, MemberUpdateDto memberUpdateDto); + public int updateMember(Long memberId, MemberUpdateDto memberUpdateDto); } diff --git a/src/main/java/com/web/SearchWeb/member/service/MemberServiceImpl.java b/src/main/java/com/web/SearchWeb/member/service/MemberServiceImpl.java index 3dad379..488a425 100644 --- a/src/main/java/com/web/SearchWeb/member/service/MemberServiceImpl.java +++ b/src/main/java/com/web/SearchWeb/member/service/MemberServiceImpl.java @@ -2,7 +2,6 @@ import com.web.SearchWeb.board.dao.BoardDao; -import com.web.SearchWeb.board.domain.Board; import com.web.SearchWeb.comment.dao.CommentDao; import com.web.SearchWeb.comment.domain.Comment; import com.web.SearchWeb.comment.dto.UpdateUserProfileCommentDto; @@ -18,7 +17,7 @@ import java.util.List; -//서비스 로직, 트랜잭션 처리 + @Service public class MemberServiceImpl implements MemberService{ @@ -51,7 +50,7 @@ public void joinProcess(MemberDto member) { /** * 회원번호로 찾기 */ - public Member findByMemberId(int memberId){ + public Member findByMemberId(Long memberId){ return memberDao.findByMemberId(memberId); } @@ -59,8 +58,8 @@ public Member findByMemberId(int memberId){ /** * 로그인 아이디로 찾기 */ - public Member findByUserName(String username){ - return memberDao.findByUserName(username); + public Member findByLoginId(String loginId){ + return memberDao.findByLoginId(loginId); } @@ -77,7 +76,7 @@ public boolean isPasswordMatching(MemberDto memberDto) { */ @Override @Transactional - public int updateMember(int memberId, MemberUpdateDto memberUpdateDto) { + public int updateMember(Long memberId, MemberUpdateDto memberUpdateDto) { int result = memberDao.updateMember(memberId, memberUpdateDto); // 회원 정보 수정이 성공했을 경우, 게시글 댓글의 회원정보 업데이트 diff --git a/src/main/java/com/web/SearchWeb/mypage/controller/MyPageController.java b/src/main/java/com/web/SearchWeb/mypage/controller/MyPageController.java index 7e02f39..7bd34a4 100644 --- a/src/main/java/com/web/SearchWeb/mypage/controller/MyPageController.java +++ b/src/main/java/com/web/SearchWeb/mypage/controller/MyPageController.java @@ -14,7 +14,9 @@ import org.springframework.web.bind.annotation.*; +import java.util.HashMap; import java.util.List; +import java.util.Map; /** @@ -23,13 +25,11 @@ * * 코드 설명: * - MyPageController는 사용자의 정보 및 사용자가 북마크한 웹사이트를 관리하는 컨트롤러 - * + * * 코드 주요 기능: * - 사용자 정보 조회, 사용자 프로필 수정 * - 마이페이지 북마크 추가(사용자 직접 추가), 북마크 목록 조회, 북마크 단일 조회, 태그 조회, 북마크 수정, 북마크 삭제 * - * 코드 작성일: - * - 2024.07.10 ~ 2024.08.08 */ @Controller public class MyPageController { @@ -49,7 +49,7 @@ public MyPageController(BookmarkService bookmarkService, MemberService memberSer */ @GetMapping("/myPage/{memberId}") @OwnerCheck(idParam = "memberId", service = "memberService") - public String myPage(@PathVariable int memberId, Model model){ + public String myPage(@PathVariable Long memberId, Model model){ Member member = memberService.findByMemberId(memberId); model.addAttribute("member", member); return "mypage/myPage"; @@ -61,7 +61,7 @@ public String myPage(@PathVariable int memberId, Model model){ */ @PutMapping("/myPage/{memberId}/profile") @OwnerCheck(idParam = "memberId", service = "memberService") - public ResponseEntity updateProfile(@PathVariable final int memberId, @RequestBody MemberUpdateDto memberUpdateDto) { + public ResponseEntity updateProfile(@PathVariable final Long memberId, @RequestBody MemberUpdateDto memberUpdateDto) { return ResponseEntity.ok(memberService.updateMember(memberId, memberUpdateDto)); } @@ -72,9 +72,14 @@ public ResponseEntity updateProfile(@PathVariable final int memberId, @ */ @PostMapping(value ="/myPage/{memberId}/bookmark") @OwnerCheck(idParam = "memberId", service = "memberService") - public ResponseEntity insertBookmark(@PathVariable final int memberId, @RequestBody BookmarkDto bookmarkdto){ - bookmarkService.insertBookmarkForUser(bookmarkdto); - return ResponseEntity.ok(bookmarkdto); + public ResponseEntity> insertBookmark(@PathVariable final Long memberId, + @RequestBody BookmarkDto bookmarkDto, + @RequestParam String url){ + Map response = new HashMap<>(); + bookmarkDto.setCreatedByMemberId(memberId); + int result = bookmarkService.insertBookmark(bookmarkDto, url); + response.put("success", result > 0); + return ResponseEntity.ok(response); } @@ -84,34 +89,22 @@ public ResponseEntity insertBookmark(@PathVariable final int member */ @GetMapping(value ="/myPage/{memberId}/bookmarks") @OwnerCheck(idParam = "memberId", service = "memberService") - public ResponseEntity> getBookmarks(@PathVariable final int memberId, + public ResponseEntity> getBookmarks(@PathVariable final Long memberId, @RequestParam(required = false) String query, - @RequestParam(defaultValue = "All") String tag, @RequestParam(required = false) Long folderId, + @RequestParam(required = false) Long categoryId, @RequestParam(defaultValue = "Oldest") String sort) { - List bookmarks = bookmarkService.selectBookmarkList(memberId, tag, sort, query, folderId); + List bookmarks = bookmarkService.selectBookmarkList(memberId, folderId, sort, query, categoryId); return ResponseEntity.ok(bookmarks); } - /** - * 마이페이지 북마크 태그 조회 - */ - @GetMapping("/myPage/{memberId}/tags") - @OwnerCheck(idParam = "memberId", service = "memberService") - public ResponseEntity> getTags(@PathVariable final int memberId, - @RequestParam(required = false) Long folderId) { - List tags = bookmarkService.selectTags(memberId, folderId); - return ResponseEntity.ok(tags); - } - - /** * 마이페이지 북마크 단일 조회 */ @GetMapping("/myPage/{memberId}/bookmark/{bookmarkId}") @OwnerCheck(idParam = "memberId", service = "memberService") - public ResponseEntity getBookmark(@PathVariable final int memberId, @PathVariable final int bookmarkId) { + public ResponseEntity getBookmark(@PathVariable final Long memberId, @PathVariable final Long bookmarkId) { Bookmark bookmark = bookmarkService.selectBookmark(memberId, bookmarkId); return ResponseEntity.ok(bookmark); } @@ -122,11 +115,13 @@ public ResponseEntity getBookmark(@PathVariable final int memberId, @P */ @PutMapping("/myPage/{memberId}/bookmark/{bookmarkId}") @OwnerCheck(idParam = "memberId", service = "memberService") - public ResponseEntity updateBookmark(@PathVariable final int memberId, - @PathVariable final int bookmarkId, + public ResponseEntity> updateBookmark(@PathVariable final Long memberId, + @PathVariable final Long bookmarkId, @RequestBody BookmarkDto bookmarkDto) { + Map response = new HashMap<>(); int result = bookmarkService.updateBookmark(bookmarkDto, bookmarkId); - return ResponseEntity.ok(result); + response.put("success", result > 0); + return ResponseEntity.ok(response); } @@ -135,8 +130,10 @@ public ResponseEntity updateBookmark(@PathVariable final int memberId, */ @DeleteMapping("/myPage/{memberId}/bookmark/{bookmarkId}") @OwnerCheck(idParam = "memberId", service = "memberService") - public ResponseEntity deleteBookmark(@PathVariable final int memberId, @PathVariable final int bookmarkId) { - int result = bookmarkService.deleteBookmarkMyPage(memberId, bookmarkId); - return ResponseEntity.ok(result); + public ResponseEntity> deleteBookmark(@PathVariable final Long memberId, @PathVariable final Long bookmarkId) { + Map response = new HashMap<>(); + int result = bookmarkService.deleteBookmark(memberId, bookmarkId); + response.put("success", result > 0); + return ResponseEntity.ok(response); } } diff --git a/src/main/resources/application-dev.properties b/src/main/resources/application-dev.properties index 8a319fb..8808fd6 100644 --- a/src/main/resources/application-dev.properties +++ b/src/main/resources/application-dev.properties @@ -1,5 +1,4 @@ -# Dev mysql settings -spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver +# Dev PostgreSQL settings spring.datasource.url=${DEV_DB_URL} spring.datasource.username=${DEV_DB_USERNAME} spring.datasource.password=${DEV_DB_PASSWORD} diff --git a/src/main/resources/application-local.properties b/src/main/resources/application-local.properties index bdda5c0..68fc531 100644 --- a/src/main/resources/application-local.properties +++ b/src/main/resources/application-local.properties @@ -1,4 +1,4 @@ -## Local mysql settings +## Local PostgreSQL settings spring.datasource.url=${LOCAL_DB_URL} spring.datasource.username=${LOCAL_DB_USERNAME} spring.datasource.password=${LOCAL_DB_PASSWORD} diff --git a/src/main/resources/application-prod.properties b/src/main/resources/application-prod.properties index c5f5ddf..43fa80f 100644 --- a/src/main/resources/application-prod.properties +++ b/src/main/resources/application-prod.properties @@ -1,5 +1,4 @@ -# Prod mysql settings -spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver +# Prod Postgres settings spring.datasource.url=${PROD_DB_URL} spring.datasource.username=${PROD_DB_USERNAME} spring.datasource.password=${PROD_DB_PASSWORD} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index d62382f..96767a6 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -2,8 +2,8 @@ spring.application.name=SearchWeb # Active profile spring.profiles.active=${SPRING_PROFILES_ACTIVE} +spring.datasource.driver-class-name=${SPRING_DATASOURCE_DRIVER_CLASS_NAME} -spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver # mybatis settings mybatis.mapper-locations=classpath:mapper/*.xml logging.level.org.mybatis=DEBUG diff --git a/src/main/resources/db/drop_postgres.sql b/src/main/resources/db/drop_postgres.sql new file mode 100644 index 0000000..3874702 --- /dev/null +++ b/src/main/resources/db/drop_postgres.sql @@ -0,0 +1,154 @@ +BEGIN; + +-- ========================= +-- DROP TABLES (의존성 고려 → CASCADE) +-- ========================= + +DROP TABLE IF EXISTS + member_saved_link_tag, + link_enrichment_feedback, + member_saved_link, + link_enrichment_keyword, + folder_suggestion_rule, + member_folder_tag, + oauth_member, + link_enrichment, + team_saved_link_tag, + team_saved_link, + team_member, + team_folder_permission, + team_folder, + team_tag, + team, + member_tag, + member_folder, + link, + category_master, + likes, + comment, + board, + website, + member +CASCADE; + +-- ========================= +-- DROP INDEXES (혹시 남아있을 경우 대비) +-- ========================= + +DROP INDEX IF EXISTS + uq_category_child_name, + uq_category_root_name, + idx_category_parent, + idx_category_name, + idx_category_level, + idx_category_active, + idx_category_deleted_at, + + idx_link_domain, + idx_link_title, + idx_link_content_type, + idx_link_primary_category, + idx_link_categorized_at, + idx_link_created_at, + idx_link_deleted_at, + + idx_member_status, + idx_member_created_at, + idx_member_deleted_at, + + idx_member_folder_owner, + idx_member_folder_parent, + uq_member_folder_parent_name, + idx_member_folder_name, + idx_member_folder_created, + idx_member_folder_deleted, + + idx_member_tag_owner, + idx_member_tag_deleted_at, + + idx_team_name, + idx_team_owner, + idx_team_deleted, + + idx_team_folder_team, + idx_team_folder_parent, + uq_team_folder_parent_name, + idx_team_folder_name, + idx_team_folder_created_by, + idx_team_folder_created, + idx_team_folder_deleted, + + idx_tfp_team_folder, + idx_tfp_user, + idx_tfp_permission, + + idx_team_member_team, + idx_team_member_user, + idx_team_member_role, + + idx_team_saved_link_folder, + uq_team_saved_link_folder_link, + idx_team_saved_link_link, + idx_team_saved_link_created_by, + idx_team_saved_link_title, + idx_team_saved_link_primary_category, + idx_team_saved_link_categorized_at, + idx_team_saved_link_folder_sort, + idx_team_saved_link_created_at, + idx_team_saved_link_deleted_at, + + idx_team_saved_link_tag_item, + idx_team_saved_link_tag_tag, + + idx_team_tag_team, + + idx_link_enrichment_deleted_at, + + idx_oauth_member_member_id, + idx_oauth_member_provider, + idx_oauth_member_deleted_at, + + idx_member_folder_tag_item, + idx_member_folder_tag_tag, + + uq_fsr_member_owner_category, + uq_fsr_team_category, + idx_fsr_scope, + uq_fsr_scope_owner_team_category_active, + idx_fsr_owner, + idx_fsr_team, + idx_fsr_category, + idx_fsr_member_folder, + idx_fsr_team_folder, + idx_fsr_priority, + idx_fsr_active, + idx_folder_suggestion_rule_deleted_at, + + idx_link_enrich_kw_enrichment, + idx_link_enrich_kw_keyword, + idx_link_enrich_kw_created_at, + + uq_member_saved_link_folder_link, + idx_member_saved_link_link, + idx_member_saved_link_enrichment, + idx_member_saved_link_folder, + idx_member_saved_link_title, + idx_member_saved_link_primary_category, + idx_member_saved_link_created_at, + idx_member_saved_link_deleted_at, + + idx_member_saved_link_tag_item, + idx_member_saved_link_tag_tag, + + 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 +CASCADE; + +COMMIT; diff --git a/src/main/resources/db/init_postgres.sql b/src/main/resources/db/init_postgres.sql new file mode 100644 index 0000000..d21a2f2 --- /dev/null +++ b/src/main/resources/db/init_postgres.sql @@ -0,0 +1,663 @@ + + +BEGIN; + +-- ========================= +-- TABLES +-- ========================= + +CREATE TABLE IF NOT EXISTS "category_master" ( + "category_id" int GENERATED ALWAYS AS IDENTITY NOT NULL, + "parent_category_id" int, + "category_name" varchar(80) NOT NULL, + "category_level" smallint NOT NULL, + "is_active" boolean NOT NULL, + "created_at" timestamptz DEFAULT now() NOT NULL, + "updated_at" timestamptz DEFAULT now() NOT NULL, + "deleted_at" timestamptz, + "created_by_member_id" bigint, + "updated_by_member_id" bigint, + "deleted_by_member_id" bigint, + CONSTRAINT pk_category_master PRIMARY KEY ("category_id"), + CONSTRAINT ck_category_master_level CHECK (category_level in (1,2)), + CONSTRAINT fk_category_master_parent_category_id + FOREIGN KEY ("parent_category_id") REFERENCES "category_master"("category_id") +); + +CREATE TABLE IF NOT EXISTS "link" ( + "link_id" bigint GENERATED ALWAYS AS IDENTITY NOT NULL, + "canonical_url" text NOT NULL, + "original_url" text NOT NULL, + "domain" varchar(255), + "title" varchar(255), + "description" text, + "thumbnail_url" text, + "favicon_url" text, + "content_type" varchar(30) DEFAULT 'link' NOT NULL, + "primary_category_id" int NOT NULL, + "category_score" numeric(5,4), + "classifier_version" varchar(50), + "categorized_at" timestamptz, + "created_at" timestamptz DEFAULT now() NOT NULL, + "updated_at" timestamptz DEFAULT now() NOT NULL, + "deleted_at" timestamptz, + "created_by_member_id" bigint, + "updated_by_member_id" bigint, + "deleted_by_member_id" bigint, + CONSTRAINT pk_link PRIMARY KEY ("link_id"), + CONSTRAINT uq_link_canonical_url UNIQUE ("canonical_url"), + CONSTRAINT ck_link_content_type CHECK (content_type in ('link','article','video','pdf','etc')), + CONSTRAINT ck_link_category_score_range CHECK (category_score is null or (category_score between 0 and 1)) +); + +CREATE TABLE IF NOT EXISTS "member" ( + "member_id" bigint GENERATED ALWAYS AS IDENTITY NOT NULL, + "email" varchar(255), + "login_id" varchar(50), -- 기존 username 대체 + "password_hash" varchar(255), + "member_name" varchar(20), + "nick_name" varchar(50), + "job" varchar(20), + "major" varchar(20), + "summary" varchar(100) DEFAULT NULL, + "status" varchar(20) DEFAULT 'active' NOT NULL, + "role" varchar(20) DEFAULT 'ROLE_USER' NOT NULL, + "created_at" timestamptz DEFAULT now() NOT NULL, + "updated_at" timestamptz DEFAULT now() NOT NULL, + "deleted_at" timestamptz, + "created_by_member_id" bigint, + "updated_by_member_id" bigint, + "deleted_by_member_id" bigint, + CONSTRAINT pk_member PRIMARY KEY ("member_id"), + CONSTRAINT uq_member_email UNIQUE ("email"), + CONSTRAINT uq_member_login_id UNIQUE ("login_id"), + CONSTRAINT ck_member_login_pw_pair + CHECK ((login_id is null and password_hash is null) OR (login_id is not null and password_hash is not null)), + CONSTRAINT ck_member_status CHECK (status in ('active','blocked')) +); + +CREATE TABLE IF NOT EXISTS "member_folder" ( + "member_folder_id" int GENERATED ALWAYS AS IDENTITY NOT NULL, + "owner_member_id" bigint NOT NULL, + "parent_folder_id" int, + "folder_name" varchar(80) NOT NULL, + "description" text, + "created_at" timestamptz DEFAULT now() NOT NULL, + "updated_at" timestamptz DEFAULT now() NOT NULL, + "deleted_at" timestamptz, + "created_by_member_id" bigint, + "updated_by_member_id" bigint, + "deleted_by_member_id" bigint, + CONSTRAINT pk_member_folder PRIMARY KEY ("member_folder_id"), + CONSTRAINT fk_member_folder_parent_folder_id + FOREIGN KEY ("parent_folder_id") REFERENCES "member_folder"("member_folder_id"), + CONSTRAINT fk_member_folder_owner_member_id + FOREIGN KEY ("owner_member_id") REFERENCES "member"("member_id") +); + +CREATE TABLE IF NOT EXISTS "member_tag" ( + "member_tag_id" bigint GENERATED ALWAYS AS IDENTITY NOT NULL, + "owner_member_id" bigint NOT NULL, + "tag_name" varchar(50) NOT NULL, + "created_at" timestamptz DEFAULT now() NOT NULL, + "updated_at" timestamptz DEFAULT now() NOT NULL, + "deleted_at" timestamptz, + "created_by_member_id" bigint, + "updated_by_member_id" bigint, + "deleted_by_member_id" bigint, + CONSTRAINT pk_member_tag PRIMARY KEY ("member_tag_id"), + CONSTRAINT uq_member_tag_owner_tag UNIQUE ("owner_member_id", "tag_name"), + CONSTRAINT fk_member_tag_owner_member_id FOREIGN KEY ("owner_member_id") REFERENCES "member"("member_id") +); + +CREATE TABLE IF NOT EXISTS "team" ( + "team_id" int GENERATED ALWAYS AS IDENTITY NOT NULL, + "team_name" varchar(80) NOT NULL, + "owner_member_id" int NOT NULL, + "created_at" timestamptz DEFAULT now() NOT NULL, + "updated_at" timestamptz DEFAULT now() NOT NULL, + "deleted_at" timestamptz, + "created_by_member_id" bigint, + "updated_by_member_id" bigint, + "deleted_by_member_id" bigint, + CONSTRAINT pk_team PRIMARY KEY ("team_id"), + CONSTRAINT fk_team_owner_member_id FOREIGN KEY ("owner_member_id") REFERENCES "member"("member_id") +); + +CREATE TABLE IF NOT EXISTS "team_tag" ( + "team_tag_id" bigint GENERATED ALWAYS AS IDENTITY NOT NULL, + "team_id" int NOT NULL, + "tag_name" varchar(50) NOT NULL, + "created_at" timestamptz DEFAULT now() NOT NULL, + "updated_at" timestamptz DEFAULT now() NOT NULL, + "deleted_at" timestamptz, + "created_by_member_id" bigint, + "updated_by_member_id" bigint, + "deleted_by_member_id" bigint, + CONSTRAINT pk_team_tag PRIMARY KEY ("team_tag_id"), + CONSTRAINT uq_team_tag_team_tag UNIQUE ("team_id", "tag_name"), + CONSTRAINT fk_team_tag_team_id FOREIGN KEY ("team_id") REFERENCES "team"("team_id") +); + +CREATE TABLE IF NOT EXISTS "team_folder" ( + "team_folder_id" int GENERATED ALWAYS AS IDENTITY NOT NULL, + "team_id" int NOT NULL, + "parent_folder_id" int, + "folder_name" varchar(80) NOT NULL, + "description" text, + "created_by_member_id" int NOT NULL, + "created_at" timestamptz DEFAULT now() NOT NULL, + "updated_at" timestamptz DEFAULT now() NOT NULL, + "deleted_at" timestamptz, + "updated_by_member_id" bigint, + "deleted_by_member_id" bigint, + CONSTRAINT pk_team_folder PRIMARY KEY ("team_folder_id"), + CONSTRAINT fk_team_folder_team_id FOREIGN KEY ("team_id") REFERENCES "team"("team_id"), + CONSTRAINT fk_team_folder_parent_folder_id FOREIGN KEY ("parent_folder_id") REFERENCES "team_folder"("team_folder_id") +); + +CREATE TABLE IF NOT EXISTS "team_folder_permission" ( + "team_folder_permission_id" int GENERATED ALWAYS AS IDENTITY NOT NULL, + "team_folder_id" int NOT NULL, + "member_id" int NOT NULL, + "permission" varchar(20) DEFAULT 'viewer' NOT NULL, + "created_at" timestamptz DEFAULT now() NOT NULL, + "updated_at" timestamptz DEFAULT now() NOT NULL, + "deleted_at" timestamptz, + "created_by_member_id" bigint, + "updated_by_member_id" bigint, + "deleted_by_member_id" bigint, + CONSTRAINT pk_team_folder_permission PRIMARY KEY ("team_folder_permission_id"), + CONSTRAINT uq_team_folder_permission_folder_member UNIQUE ("team_folder_id", "member_id"), + CONSTRAINT ck_team_folder_permission_permission CHECK (permission in ('viewer','editor')), + CONSTRAINT fk_team_folder_permission_folder_id FOREIGN KEY ("team_folder_id") REFERENCES "team_folder"("team_folder_id"), + CONSTRAINT fk_team_folder_permission_member_id FOREIGN KEY ("member_id") REFERENCES "member"("member_id") +); + +CREATE TABLE IF NOT EXISTS "team_member" ( + "team_member_id" int GENERATED ALWAYS AS IDENTITY NOT NULL, + "team_id" int NOT NULL, + "member_id" int NOT NULL, + "role" varchar(20) DEFAULT 'member' NOT NULL, + "joined_at" timestamptz DEFAULT now() NOT NULL, + "created_at" timestamptz DEFAULT now() NOT NULL, + "updated_at" timestamptz DEFAULT now() NOT NULL, + "deleted_at" timestamptz, + "created_by_member_id" bigint, + "updated_by_member_id" bigint, + "deleted_by_member_id" bigint, + CONSTRAINT pk_team_member PRIMARY KEY ("team_member_id"), + CONSTRAINT uq_team_member_team_member UNIQUE ("team_id", "member_id"), + CONSTRAINT ck_team_member_role CHECK (role in ('owner','admin','member')), + CONSTRAINT fk_team_member_team_id FOREIGN KEY ("team_id") REFERENCES "team"("team_id"), + CONSTRAINT fk_team_member_member_id FOREIGN KEY ("member_id") REFERENCES "member"("member_id") +); + +CREATE TABLE IF NOT EXISTS "team_saved_link" ( + "team_saved_link_id" int GENERATED ALWAYS AS IDENTITY NOT NULL, + "team_folder_id" int NOT NULL, + "link_id" bigint NOT NULL, + "created_by_member_id" int NOT NULL, + "display_title" varchar(255) NOT NULL, + "note" text, + "primary_category_id" int, + "category_source" varchar(10) DEFAULT 'system' NOT NULL, + "category_score" numeric(5,4), + "categorized_at" timestamptz, + "sort_order" int NOT NULL, + "created_at" timestamptz DEFAULT now() NOT NULL, + "updated_at" timestamptz DEFAULT now() NOT NULL, + "deleted_at" timestamptz, + "updated_by_member_id" bigint, + "deleted_by_member_id" bigint, + CONSTRAINT pk_team_saved_link PRIMARY KEY ("team_saved_link_id"), + CONSTRAINT ck_team_saved_link_category_source CHECK (category_source in ('system','member')), + CONSTRAINT ck_team_saved_link_category_score CHECK (category_score is null or (category_score between 0 and 1)), + CONSTRAINT ck_team_saved_link_sort_nonneg CHECK (sort_order >= 0), + CONSTRAINT fk_team_saved_link_folder_id FOREIGN KEY ("team_folder_id") REFERENCES "team_folder"("team_folder_id"), + CONSTRAINT fk_team_saved_link_link_id FOREIGN KEY ("link_id") REFERENCES "link"("link_id") +); + +CREATE TABLE IF NOT EXISTS "team_saved_link_tag" ( + "team_saved_link_tag_id" int GENERATED ALWAYS AS IDENTITY NOT NULL, + "team_saved_link_id" bigint NOT NULL, + "team_tag_id" bigint NOT NULL, + "created_at" timestamptz DEFAULT now() NOT NULL, + "updated_at" timestamptz DEFAULT now() NOT NULL, + "deleted_at" timestamptz, + "created_by_member_id" bigint, + "updated_by_member_id" bigint, + "deleted_by_member_id" bigint, + CONSTRAINT pk_team_saved_link_tag PRIMARY KEY ("team_saved_link_tag_id"), + CONSTRAINT uq_team_saved_link_tag_item_tag UNIQUE ("team_saved_link_id", "team_tag_id"), + CONSTRAINT fk_team_saved_link_tag_link_id FOREIGN KEY ("team_saved_link_id") REFERENCES "team_saved_link"("team_saved_link_id"), + CONSTRAINT fk_team_saved_link_tag_tag_id FOREIGN KEY ("team_tag_id") REFERENCES "team_tag"("team_tag_id") +); + + + +CREATE TABLE IF NOT EXISTS "link_enrichment" ( + "link_enrichment_id" int GENERATED ALWAYS AS IDENTITY NOT NULL, + "link_id" bigint NOT NULL, + "request_url" text NOT NULL, + "final_url" text, + "fetch_status" varchar(20) DEFAULT 'pending' NOT NULL, + "classify_status" varchar(20) DEFAULT 'pending' NOT NULL, + "attempt_count" smallint NOT NULL, + "last_attempt_at" timestamptz, + "error_code" varchar(50), + "error_message" text, + "http_status" int, + "latency_ms" int, + "selected_site_name" varchar(255), + "selected_title" text, + "selected_description" text, + "fetched_at" timestamptz, + "predicted_category_id" int, + "predicted_score" numeric(5,4), + "classifier_version" varchar(50), + "classified_at" timestamptz, + "keyword_extractor_version" varchar(50), + "keyword_source" varchar(30), + "keyword_extracted_at" timestamptz, + "suggested_member_folder_id" int, + "suggested_team_folder_id" int, + "created_at" timestamptz DEFAULT now() NOT NULL, + "updated_at" timestamptz DEFAULT now() NOT NULL, + "deleted_at" timestamptz, + "created_by_member_id" bigint, + "updated_by_member_id" bigint, + "deleted_by_member_id" bigint, + CONSTRAINT pk_link_enrichment PRIMARY KEY ("link_enrichment_id"), + CONSTRAINT ck_link_enrichment_fetch_status CHECK (fetch_status in ('pending','running','success','failed')), + CONSTRAINT ck_link_enrichment_classify_status CHECK (classify_status in ('pending','running','success','failed')), + CONSTRAINT ck_link_enrichment_attempt_nonneg CHECK (attempt_count >= 0), + CONSTRAINT ck_link_enrichment_http_status_range CHECK (http_status is null or (http_status between 100 and 599)), + CONSTRAINT ck_link_enrichment_latency_nonneg CHECK (latency_ms is null or latency_ms >= 0), + CONSTRAINT ck_link_enrichment_score_range CHECK (predicted_score is null or (predicted_score between 0 and 1)), + CONSTRAINT ck_link_enrichment_keyword_source CHECK (keyword_source is null or keyword_source in ('title','description','title_description','other')), + CONSTRAINT ck_link_enrichment_suggested_folder_one + CHECK ((suggested_member_folder_id is null) OR (suggested_team_folder_id is null)), + CONSTRAINT fk_link_enrichment_link_id FOREIGN KEY ("link_id") REFERENCES "link"("link_id") +); + +CREATE TABLE IF NOT EXISTS "oauth_member" ( + "oauth_member_id" int GENERATED ALWAYS AS IDENTITY NOT NULL, + "member_id" bigint NOT NULL, + "provider" varchar(30) NOT NULL, + "provider_member_key" varchar(255) NOT NULL, + "created_at" timestamptz DEFAULT now() NOT NULL, + "updated_at" timestamptz DEFAULT now() NOT NULL, + "deleted_at" timestamptz, + "created_by_member_id" bigint, + "updated_by_member_id" bigint, + "deleted_by_member_id" bigint, + CONSTRAINT pk_oauth_member PRIMARY KEY ("oauth_member_id"), + CONSTRAINT uq_oauth_member_provider_key UNIQUE ("provider", "provider_member_key"), + CONSTRAINT fk_oauth_member_member_id FOREIGN KEY ("member_id") REFERENCES "member"("member_id") +); + +CREATE TABLE IF NOT EXISTS "member_folder_tag" ( + "member_folder_tag_id" bigint GENERATED ALWAYS AS IDENTITY NOT NULL, + "member_folder_id" int NOT NULL, + "member_tag_id" bigint NOT NULL, + "created_at" timestamptz DEFAULT now() NOT NULL, + "updated_at" timestamptz DEFAULT now() NOT NULL, + "deleted_at" timestamptz, + "created_by_member_id" bigint, + "updated_by_member_id" bigint, + "deleted_by_member_id" bigint, + CONSTRAINT pk_member_folder_tag PRIMARY KEY ("member_folder_tag_id"), + CONSTRAINT uq_member_folder_tag_item_tag UNIQUE ("member_folder_id", "member_tag_id"), + CONSTRAINT fk_member_folder_tag_member_folder_id + FOREIGN KEY ("member_folder_id") REFERENCES "member_folder"("member_folder_id"), + CONSTRAINT fk_member_folder_tag_member_tag_id + FOREIGN KEY ("member_tag_id") REFERENCES "member_tag"("member_tag_id") +); + +CREATE TABLE IF NOT EXISTS "folder_suggestion_rule" ( + "folder_suggestion_rule_id" int GENERATED ALWAYS AS IDENTITY NOT NULL, + "scope_type" varchar(10) DEFAULT 'member' NOT NULL, + "owner_member_id" bigint, + "team_id" int, + "category_id" int NOT NULL, + "member_folder_id" int, + "team_folder_id" int, + "priority" int NOT NULL, + "is_active" boolean NOT NULL, + "created_at" timestamptz DEFAULT now() NOT NULL, + "updated_at" timestamptz DEFAULT now() NOT NULL, + "deleted_at" timestamptz, + "created_by_member_id" bigint, + "updated_by_member_id" bigint, + "deleted_by_member_id" bigint, + CONSTRAINT pk_folder_suggestion_rule PRIMARY KEY ("folder_suggestion_rule_id"), + CONSTRAINT ck_folder_suggestion_rule_scope CHECK (scope_type in ('member','team')), + CONSTRAINT ck_folder_suggestion_rule_scope_match CHECK ( + (scope_type='member' and owner_member_id is not null and team_id is null and member_folder_id is not null and team_folder_id is null) + OR + (scope_type='team' and team_id is not null and owner_member_id is null and team_folder_id is not null and member_folder_id is null) + ), + CONSTRAINT ck_folder_suggestion_rule_priority_nonneg CHECK (priority >= 0), + CONSTRAINT fk_folder_suggestion_rule_member_folder_id + FOREIGN KEY ("member_folder_id") REFERENCES "member_folder"("member_folder_id"), + CONSTRAINT fk_folder_suggestion_rule_team_folder_id + FOREIGN KEY ("team_folder_id") REFERENCES "team_folder"("team_folder_id"), + CONSTRAINT fk_folder_suggestion_rule_owner_member_id FOREIGN KEY ("owner_member_id") REFERENCES "member"("member_id"), + CONSTRAINT fk_folder_suggestion_rule_team_id FOREIGN KEY ("team_id") REFERENCES "team"("team_id") +); + +CREATE TABLE IF NOT EXISTS "link_enrichment_keyword" ( + "link_enrichment_keyword_id" bigint GENERATED ALWAYS AS IDENTITY NOT NULL, + "link_enrichment_id" int NOT NULL, + "keyword" varchar(100) NOT NULL, + "score" numeric(5,4), + "rank" smallint NOT NULL, + "source" varchar(30), + "created_at" timestamptz DEFAULT now() NOT NULL, + "updated_at" timestamptz DEFAULT now() NOT NULL, + "deleted_at" timestamptz, + "created_by_member_id" bigint, + "updated_by_member_id" bigint, + "deleted_by_member_id" bigint, + CONSTRAINT pk_link_enrichment_keyword PRIMARY KEY ("link_enrichment_keyword_id"), + CONSTRAINT uq_link_enrichment_keyword_enrich_keyword UNIQUE ("link_enrichment_id", "keyword"), + CONSTRAINT ck_link_enrichment_keyword_score CHECK (score is null or (score between 0 and 1)), + CONSTRAINT ck_link_enrichment_keyword_rank_nonneg CHECK (rank >= 0), + CONSTRAINT ck_link_enrichment_keyword_source CHECK (source is null or source in ('title','description','title_description','other')), + CONSTRAINT fk_link_enrichment_keyword_link_enrichment_id + FOREIGN KEY ("link_enrichment_id") REFERENCES "link_enrichment"("link_enrichment_id") +); + +CREATE TABLE IF NOT EXISTS "member_saved_link" ( + "member_saved_link_id" int GENERATED ALWAYS AS IDENTITY NOT NULL, + "link_id" bigint NOT NULL, + "link_enrichment_id" int, + "member_folder_id" int NOT NULL, + "display_title" varchar(255) NOT NULL, + "note" text, + "primary_category_id" int, + "category_source" varchar(10) DEFAULT 'system' NOT NULL, + "category_score" numeric(5,4), + "created_at" timestamptz DEFAULT now() NOT NULL, + "updated_at" timestamptz DEFAULT now() NOT NULL, + "deleted_at" timestamptz, + "created_by_member_id" bigint, + "updated_by_member_id" bigint, + "deleted_by_member_id" bigint, + CONSTRAINT pk_member_saved_link PRIMARY KEY ("member_saved_link_id"), + CONSTRAINT ck_member_saved_link_category_source CHECK (category_source in ('system','member')), + CONSTRAINT ck_member_saved_link_category_score CHECK (category_score is null or (category_score between 0 and 1)), + CONSTRAINT fk_member_saved_link_link_id FOREIGN KEY ("link_id") REFERENCES "link"("link_id"), + CONSTRAINT fk_member_saved_link_link_enrichment_id FOREIGN KEY ("link_enrichment_id") REFERENCES "link_enrichment"("link_enrichment_id") +); + +CREATE TABLE IF NOT EXISTS "link_enrichment_feedback" ( + "link_enrichment_feedback_id" int GENERATED ALWAYS AS IDENTITY NOT NULL, + "link_enrichment_id" int NOT NULL, + "member_saved_link_id" int, + "action" varchar(20) NOT NULL, + "suggested_member_folder_id" int, + "final_member_folder_id" int, + "created_at" timestamptz DEFAULT now() NOT NULL, + "updated_at" timestamptz DEFAULT now() NOT NULL, + "deleted_at" timestamptz, + "created_by_member_id" bigint, + "updated_by_member_id" bigint, + "deleted_by_member_id" bigint, + CONSTRAINT pk_link_enrichment_feedback PRIMARY KEY ("link_enrichment_feedback_id"), + CONSTRAINT ck_link_enrichment_feedback_action CHECK (action in ('ACCEPT','MOVE','REJECT','IGNORE')), + CONSTRAINT fk_link_enrichment_feedback_link_enrichment_id + FOREIGN KEY ("link_enrichment_id") REFERENCES "link_enrichment"("link_enrichment_id") +); + +CREATE TABLE IF NOT EXISTS "member_saved_link_tag" ( + "member_saved_link_tag_id" bigint GENERATED ALWAYS AS IDENTITY NOT NULL, + "member_saved_link_id" bigint NOT NULL, + "member_tag_id" bigint NOT NULL, + "created_at" timestamptz DEFAULT now() NOT NULL, + "updated_at" timestamptz DEFAULT now() NOT NULL, + "deleted_at" timestamptz, + "created_by_member_id" bigint, + "updated_by_member_id" bigint, + "deleted_by_member_id" bigint, + CONSTRAINT pk_member_saved_link_tag PRIMARY KEY ("member_saved_link_tag_id"), + CONSTRAINT uq_member_saved_link_tag_item_tag UNIQUE ("member_saved_link_id", "member_tag_id"), + CONSTRAINT fk_member_saved_link_tag_member_saved_link_id + FOREIGN KEY ("member_saved_link_id") REFERENCES "member_saved_link"("member_saved_link_id"), + CONSTRAINT fk_member_saved_link_tag_member_tag_id + FOREIGN KEY ("member_tag_id") REFERENCES "member_tag"("member_tag_id") +); + +-- ================================================== +-- LEGACY TABLES (Board, Comment, Likes, Website) +-- ================================================== + +CREATE TABLE IF NOT EXISTS "website" ( + "website_id" bigint GENERATED ALWAYS AS IDENTITY NOT NULL, + "name" varchar(100) NOT NULL, + "korean_name" varchar(100), + "description" text, + "url" text NOT NULL, + "category" varchar(50), + "subcategory" varchar(50), + "view_count" bigint DEFAULT 0 NOT NULL, + "created_at" timestamptz DEFAULT now() NOT NULL, + "updated_at" timestamptz DEFAULT now() NOT NULL, + "deleted_at" timestamptz, + "created_by_member_id" bigint, + "updated_by_member_id" bigint, + "deleted_by_member_id" bigint, + CONSTRAINT pk_website PRIMARY KEY ("website_id") +); + +CREATE TABLE IF NOT EXISTS "board" ( + "board_id" bigint GENERATED ALWAYS AS IDENTITY NOT NULL, + "member_member_id" bigint NOT NULL, + "url" text, + "title" varchar(255) NOT NULL, + "summary" text, + "description" text, + "hashtags" text, + "likes_count" int DEFAULT 0 NOT NULL, + "comments_count" int DEFAULT 0 NOT NULL, + "bookmarks_count" int DEFAULT 0 NOT NULL, + "views_count" int DEFAULT 0 NOT NULL, + "created_date" timestamptz DEFAULT now() NOT NULL, + "created_at" timestamptz DEFAULT now() NOT NULL, + "updated_at" timestamptz DEFAULT now() NOT NULL, + "deleted_at" timestamptz, + "created_by_member_id" bigint, + "updated_by_member_id" bigint, + "deleted_by_member_id" bigint, + CONSTRAINT pk_board PRIMARY KEY ("board_id"), + CONSTRAINT fk_board_member_id FOREIGN KEY ("member_member_id") REFERENCES "member"("member_id") +); + +CREATE TABLE IF NOT EXISTS "comment" ( + "comment_id" bigint GENERATED ALWAYS AS IDENTITY NOT NULL, + "board_board_id" bigint NOT NULL, + "member_member_id" bigint NOT NULL, + "member_nickname" varchar(50), + "member_job" varchar(20), + "member_major" varchar(20), + "content" text NOT NULL, + "created_date" timestamptz DEFAULT now() NOT NULL, + "created_at" timestamptz DEFAULT now() NOT NULL, + "updated_at" timestamptz DEFAULT now() NOT NULL, + "deleted_at" timestamptz, + "created_by_member_id" bigint, + "updated_by_member_id" bigint, + "deleted_by_member_id" bigint, + CONSTRAINT pk_comment PRIMARY KEY ("comment_id"), + CONSTRAINT fk_comment_board_id FOREIGN KEY ("board_board_id") REFERENCES "board"("board_id"), + CONSTRAINT fk_comment_member_id FOREIGN KEY ("member_member_id") REFERENCES "member"("member_id") +); + +CREATE TABLE IF NOT EXISTS "likes" ( + "likes_id" bigint GENERATED ALWAYS AS IDENTITY NOT NULL, + "board_board_id" bigint NOT NULL, + "member_member_id" bigint NOT NULL, + "is_liked" boolean DEFAULT false NOT NULL, + "created_at" timestamptz DEFAULT now() NOT NULL, + "updated_at" timestamptz DEFAULT now() NOT NULL, + "deleted_at" timestamptz, + "created_by_member_id" bigint, + "updated_by_member_id" bigint, + "deleted_by_member_id" bigint, + CONSTRAINT pk_likes PRIMARY KEY ("likes_id"), + CONSTRAINT uq_likes_board_member UNIQUE ("board_board_id", "member_member_id"), + CONSTRAINT fk_likes_board_id FOREIGN KEY ("board_board_id") REFERENCES "board"("board_id"), + CONSTRAINT fk_likes_member_id FOREIGN KEY ("member_member_id") REFERENCES "member"("member_id") +); + +-- ========================= +-- INDEXES (원본 유지 + 위험한 이름만 개선) +-- ========================= + +CREATE UNIQUE INDEX IF NOT EXISTS uq_category_child_name + ON "category_master" ("parent_category_id", "category_name") + WHERE parent_category_id IS NOT NULL AND deleted_at IS NULL; + +CREATE INDEX IF NOT EXISTS idx_category_parent ON "category_master" ("parent_category_id"); + +CREATE UNIQUE INDEX IF NOT EXISTS uq_category_root_name + ON "category_master" ("category_name") + WHERE parent_category_id IS NULL AND deleted_at IS NULL; + +CREATE INDEX IF NOT EXISTS idx_category_name ON "category_master" ("category_name"); +CREATE INDEX IF NOT EXISTS idx_category_level ON "category_master" ("category_level"); +CREATE INDEX IF NOT EXISTS idx_category_active ON "category_master" ("is_active"); +CREATE INDEX IF NOT EXISTS idx_category_deleted_at ON "category_master" ("deleted_at"); + +CREATE INDEX IF NOT EXISTS idx_link_domain ON "link" ("domain"); +CREATE INDEX IF NOT EXISTS idx_link_title ON "link" ("title"); +CREATE INDEX IF NOT EXISTS idx_link_content_type ON "link" ("content_type"); +CREATE INDEX IF NOT EXISTS idx_link_primary_category ON "link" ("primary_category_id"); +CREATE INDEX IF NOT EXISTS idx_link_categorized_at ON "link" ("categorized_at"); +CREATE INDEX IF NOT EXISTS idx_link_created_at ON "link" ("created_at"); +CREATE INDEX IF NOT EXISTS idx_link_deleted_at ON "link" ("deleted_at"); + +CREATE INDEX IF NOT EXISTS idx_member_status ON "member" ("status"); +CREATE INDEX IF NOT EXISTS idx_member_created_at ON "member" ("created_at"); +CREATE INDEX IF NOT EXISTS idx_member_deleted_at ON "member" ("deleted_at"); + +CREATE INDEX IF NOT EXISTS idx_member_folder_owner ON "member_folder" ("owner_member_id"); +CREATE INDEX IF NOT EXISTS idx_member_folder_parent ON "member_folder" ("parent_folder_id"); +CREATE UNIQUE INDEX IF NOT EXISTS uq_member_folder_parent_name + ON "member_folder" ("owner_member_id", "parent_folder_id", "folder_name") + WHERE deleted_at IS NULL; +CREATE INDEX IF NOT EXISTS idx_member_folder_name ON "member_folder" ("folder_name"); +CREATE INDEX IF NOT EXISTS idx_member_folder_created ON "member_folder" ("created_at"); +CREATE INDEX IF NOT EXISTS idx_member_folder_deleted ON "member_folder" ("deleted_at"); + +CREATE INDEX IF NOT EXISTS idx_member_tag_owner ON "member_tag" ("owner_member_id"); +CREATE INDEX IF NOT EXISTS idx_member_tag_deleted_at ON "member_tag" ("deleted_at"); + +CREATE INDEX IF NOT EXISTS idx_team_name ON "team" ("team_name"); +CREATE INDEX IF NOT EXISTS idx_team_owner ON "team" ("owner_member_id"); +CREATE INDEX IF NOT EXISTS idx_team_deleted ON "team" ("deleted_at"); + +CREATE INDEX IF NOT EXISTS idx_team_folder_team ON "team_folder" ("team_id"); +CREATE INDEX IF NOT EXISTS idx_team_folder_parent ON "team_folder" ("parent_folder_id"); +CREATE UNIQUE INDEX IF NOT EXISTS uq_team_folder_parent_name + ON "team_folder" ("team_id", "parent_folder_id", "folder_name") + WHERE deleted_at IS NULL; +CREATE INDEX IF NOT EXISTS idx_team_folder_name ON "team_folder" ("folder_name"); +CREATE INDEX IF NOT EXISTS idx_team_folder_created_by ON "team_folder" ("created_by_member_id"); +CREATE INDEX IF NOT EXISTS idx_team_folder_created ON "team_folder" ("created_at"); +CREATE INDEX IF NOT EXISTS idx_team_folder_deleted ON "team_folder" ("deleted_at"); + +CREATE INDEX IF NOT EXISTS idx_tfp_team_folder ON "team_folder_permission" ("team_folder_id"); +CREATE INDEX IF NOT EXISTS idx_tfp_member ON "team_folder_permission" ("member_id"); +CREATE INDEX IF NOT EXISTS idx_tfp_permission ON "team_folder_permission" ("permission"); + +CREATE INDEX IF NOT EXISTS idx_team_member_team ON "team_member" ("team_id"); +CREATE INDEX IF NOT EXISTS idx_team_member_member ON "team_member" ("member_id"); +CREATE INDEX IF NOT EXISTS idx_team_member_role ON "team_member" ("role"); + +CREATE INDEX IF NOT EXISTS idx_team_saved_link_folder ON "team_saved_link" ("team_folder_id"); +CREATE UNIQUE INDEX IF NOT EXISTS uq_team_saved_link_folder_link + ON "team_saved_link" ("team_folder_id", "link_id") + WHERE deleted_at IS NULL; +CREATE INDEX IF NOT EXISTS idx_team_saved_link_link ON "team_saved_link" ("link_id"); +CREATE INDEX IF NOT EXISTS idx_team_saved_link_created_by ON "team_saved_link" ("created_by_member_id"); +CREATE INDEX IF NOT EXISTS idx_team_saved_link_title ON "team_saved_link" ("display_title"); +CREATE INDEX IF NOT EXISTS idx_team_saved_link_primary_category ON "team_saved_link" ("primary_category_id"); +CREATE INDEX IF NOT EXISTS idx_team_saved_link_categorized_at ON "team_saved_link" ("categorized_at"); +CREATE INDEX IF NOT EXISTS idx_team_saved_link_folder_sort ON "team_saved_link" ("team_folder_id", "sort_order"); +CREATE INDEX IF NOT EXISTS idx_team_saved_link_created_at ON "team_saved_link" ("created_at"); +CREATE INDEX IF NOT EXISTS idx_team_saved_link_deleted_at ON "team_saved_link" ("deleted_at"); + +CREATE INDEX IF NOT EXISTS idx_team_saved_link_tag_item ON "team_saved_link_tag" ("team_saved_link_id"); +CREATE INDEX IF NOT EXISTS idx_team_saved_link_tag_tag ON "team_saved_link_tag" ("team_tag_id"); + +CREATE INDEX IF NOT EXISTS idx_team_tag_team ON "team_tag" ("team_id"); + +CREATE INDEX IF NOT EXISTS idx_link_enrichment_deleted_at ON "link_enrichment" ("deleted_at"); + +CREATE INDEX IF NOT EXISTS idx_oauth_member_member_id ON "oauth_member" ("member_id"); +CREATE INDEX IF NOT EXISTS idx_oauth_member_provider ON "oauth_member" ("provider"); +CREATE INDEX IF NOT EXISTS idx_oauth_member_deleted_at ON "oauth_member" ("deleted_at"); + +CREATE INDEX IF NOT EXISTS idx_member_folder_tag_item ON "member_folder_tag" ("member_folder_id"); +CREATE INDEX IF NOT EXISTS idx_member_folder_tag_tag ON "member_folder_tag" ("member_tag_id"); + +CREATE UNIQUE INDEX IF NOT EXISTS uq_fsr_member_owner_category + ON "folder_suggestion_rule" ("owner_member_id", "category_id") + WHERE scope_type='member' AND is_active=true AND deleted_at IS NULL; + +CREATE UNIQUE INDEX IF NOT EXISTS uq_fsr_team_category + ON "folder_suggestion_rule" ("team_id", "category_id") + WHERE scope_type='team' AND is_active=true AND deleted_at IS NULL; + +CREATE INDEX IF NOT EXISTS idx_fsr_scope ON "folder_suggestion_rule" ("scope_type"); + +CREATE UNIQUE INDEX IF NOT EXISTS uq_fsr_scope_owner_team_category_active + ON "folder_suggestion_rule" ("scope_type", "owner_member_id", "team_id", "category_id") + WHERE is_active = true; + +CREATE INDEX IF NOT EXISTS idx_fsr_owner ON "folder_suggestion_rule" ("owner_member_id"); +CREATE INDEX IF NOT EXISTS idx_fsr_team ON "folder_suggestion_rule" ("team_id"); +CREATE INDEX IF NOT EXISTS idx_fsr_category ON "folder_suggestion_rule" ("category_id"); +CREATE INDEX IF NOT EXISTS idx_fsr_member_folder ON "folder_suggestion_rule" ("member_folder_id"); +CREATE INDEX IF NOT EXISTS idx_fsr_team_folder ON "folder_suggestion_rule" ("team_folder_id"); +CREATE INDEX IF NOT EXISTS idx_fsr_priority ON "folder_suggestion_rule" ("priority"); +CREATE INDEX IF NOT EXISTS idx_fsr_active ON "folder_suggestion_rule" ("is_active"); +CREATE INDEX IF NOT EXISTS idx_folder_suggestion_rule_deleted_at ON "folder_suggestion_rule" ("deleted_at"); + +CREATE INDEX IF NOT EXISTS idx_link_enrich_kw_enrichment ON "link_enrichment_keyword" ("link_enrichment_id"); +CREATE INDEX IF NOT EXISTS idx_link_enrich_kw_keyword ON "link_enrichment_keyword" ("keyword"); +CREATE INDEX IF NOT EXISTS idx_link_enrich_kw_created_at ON "link_enrichment_keyword" ("created_at"); + +CREATE UNIQUE INDEX IF NOT EXISTS uq_member_saved_link_folder_link + ON "member_saved_link" ("member_folder_id", "link_id") + WHERE deleted_at IS NULL; + +CREATE INDEX IF NOT EXISTS idx_member_saved_link_link ON "member_saved_link" ("link_id"); +CREATE INDEX IF NOT EXISTS idx_member_saved_link_enrichment ON "member_saved_link" ("link_enrichment_id"); +CREATE INDEX IF NOT EXISTS idx_member_saved_link_folder ON "member_saved_link" ("member_folder_id"); +CREATE INDEX IF NOT EXISTS idx_member_saved_link_title ON "member_saved_link" ("display_title"); +CREATE INDEX IF NOT EXISTS idx_member_saved_link_primary_category ON "member_saved_link" ("primary_category_id"); +CREATE INDEX IF NOT EXISTS idx_member_saved_link_created_at ON "member_saved_link" ("created_at"); +CREATE INDEX IF NOT EXISTS idx_member_saved_link_deleted_at ON "member_saved_link" ("deleted_at"); + +CREATE INDEX IF NOT EXISTS idx_member_saved_link_tag_item ON "member_saved_link_tag" ("member_saved_link_id"); +CREATE INDEX IF NOT EXISTS idx_member_saved_link_tag_tag ON "member_saved_link_tag" ("member_tag_id"); + +COMMIT; + +-- Indexes for Legacy Tables +CREATE INDEX IF NOT EXISTS idx_website_url ON "website" ("url"); +CREATE INDEX IF NOT EXISTS idx_website_category ON "website" ("category"); + +CREATE INDEX IF NOT EXISTS idx_board_member ON "board" ("member_member_id"); +CREATE INDEX IF NOT EXISTS idx_board_title ON "board" ("title"); +CREATE INDEX IF NOT EXISTS idx_board_created_date ON "board" ("created_date"); + +CREATE INDEX IF NOT EXISTS idx_comment_board ON "comment" ("board_board_id"); +CREATE INDEX IF NOT EXISTS idx_comment_member ON "comment" ("member_member_id"); + +CREATE INDEX IF NOT EXISTS idx_likes_board ON "likes" ("board_board_id"); +CREATE INDEX IF NOT EXISTS idx_likes_member ON "likes" ("member_member_id"); + diff --git a/src/main/resources/mapper/board-mapper.xml b/src/main/resources/mapper/board-mapper.xml index af27909..6ae6ec3 100644 --- a/src/main/resources/mapper/board-mapper.xml +++ b/src/main/resources/mapper/board-mapper.xml @@ -1,41 +1,57 @@ - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + - INSERT INTO board(member_memberId, url, title, summary, description, hashtags) - VALUES (#{memberId}, #{boardDto.url}, #{boardDto.title}, #{boardDto.summary}, #{boardDto.description}, #{boardDto.hashtags}); + INSERT INTO board (member_member_id, url, title, summary, description, hashtags) + VALUES (#{memberId}, #{boardDto.url}, #{boardDto.title}, #{boardDto.summary}, #{boardDto.description}, #{boardDto.hashtags}) - - SELECT - b.boardId, + b.board_id, b.title, b.description, b.created_date, - b.likes_count, b.url, - b.member_memberId, - - m.nickname, + b.member_member_id, + m.nick_name AS nickname, m.job, m.major FROM board b - JOIN member m ON b.member_memberId = m.memberId + JOIN member m ON b.member_member_id = m.member_id - (title LIKE CONCAT('%', #{query}, '%') + (title LIKE '%' || #{query} || '%' OR - description LIKE CONCAT('%', #{query}, '%')) + description LIKE '%' || #{query} || '%') @@ -43,33 +59,24 @@ AND url IS NOT NULL - - - AND url IS NULL - - - - - - ORDER BY likes_count DESC - - - ORDER BY created_date DESC - - - - LIMIT #{size} OFFSET #{offset} + ORDER BY + + created_date DESC + likes_count DESC + created_date DESC + + LIMIT #{limit} OFFSET #{offset} - - SELECT COUNT(*) FROM board - (title LIKE CONCAT('%', #{query}, '%') - OR description LIKE CONCAT('%', #{query}, '%')) + (title LIKE '%' || #{query} || '%' + OR description LIKE '%' || #{query} || '%') AND url IS NOT NULL @@ -81,59 +88,56 @@ - - SELECT * FROM board - where member_memberId = #{memberId} + WHERE member_member_id = #{memberId} - SELECT * FROM board - where boardId = #{boardId} + WHERE board_id = #{boardId} - update board + UPDATE board SET - url = #{boardDto.url}, - title = #{boardDto.title}, - summary = #{boardDto.summary}, - description = #{boardDto.description}, - hashtags = #{boardDto.hashtags} - WHERE - boardId = #{boardId} + url = #{boardDto.url}, + title = #{boardDto.title}, + summary = #{boardDto.summary}, + description = #{boardDto.description}, + hashtags = #{boardDto.hashtags} + WHERE board_id = #{boardId} - + - update board + UPDATE board SET - job = #{job}, - major = #{major} - WHERE boardId = #{boardId} + job = #{job}, + major = #{major} + WHERE board_id = #{boardId} - delete from board - where - boardId = #{boardId} + DELETE FROM board + WHERE board_id = #{boardId} - update board - SET - bookmarks_count = #{bookmarkCount} - WHERE boardId = #{boardId} + UPDATE board + SET bookmarks_count = #{bookmarkCount} + WHERE board_id = #{boardId} @@ -141,7 +145,7 @@ UPDATE board SET views_count = views_count + 1 - WHERE boardId = #{boardId} + WHERE board_id = #{boardId} @@ -149,7 +153,7 @@ UPDATE board SET likes_count = likes_count + 1 - WHERE boardId = #{boardId} + WHERE board_id = #{boardId} @@ -157,7 +161,7 @@ UPDATE board SET likes_count = likes_count - 1 - WHERE boardId = #{boardId} + WHERE board_id = #{boardId} @@ -165,7 +169,7 @@ UPDATE board SET comments_count = comments_count + 1 - WHERE boardId = #{boardId} + WHERE board_id = #{boardId} @@ -173,7 +177,7 @@ UPDATE board SET comments_count = comments_count - 1 - WHERE boardId = #{boardId} + WHERE board_id = #{boardId} diff --git a/src/main/resources/mapper/bookmark-mapper.xml b/src/main/resources/mapper/bookmark-mapper.xml index ce69228..c505a40 100644 --- a/src/main/resources/mapper/bookmark-mapper.xml +++ b/src/main/resources/mapper/bookmark-mapper.xml @@ -4,166 +4,148 @@ "https://mybatis.org/dtd/mybatis-3-mapper.dtd"> - - - - - - - - - + SELECT COUNT(*) + FROM member_saved_link + WHERE created_by_member_id = #{memberId} + AND member_folder_id = #{folderId} + AND link_id = #{linkId} + AND deleted_at IS NULL - - + 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 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.deleted_at IS NULL - AND folder_folderId = #{folderId} + AND msl.member_folder_id = #{folderId} - - AND tag = #{tag} + + AND msl.primary_category_id = #{categoryId} - AND LOWER(name) LIKE CONCAT('%', LOWER(#{query}), '%') + AND LOWER(msl.display_title) LIKE '%' || LOWER(#{query}) || '%' ORDER BY - modified_date DESC + msl.updated_at DESC - modified_date ASC + msl.updated_at ASC - modified_date DESC + msl.updated_at DESC - - - insert - into - bookmark(member_memberId, website_websiteId, name, description, url, folder_folderId) - values(#{member_memberId}, #{website_websiteId}, #{name}, #{description}, #{url}, #{folder_folderId}); - + + - - - insert - into - bookmark(member_memberId, name, description, url, tag, folder_folderId) - values(#{member_memberId},#{name}, #{description}, #{url}, #{tag}, #{folder_folderId}); + + + INSERT INTO link (canonical_url, original_url, domain, title, description, primary_category_id, created_by_member_id) + VALUES (#{canonicalUrl}, #{originalUrl}, #{domain}, #{title}, #{description}, #{primaryCategoryId}, #{createdByMemberId}) - - - insert - into - bookmark(member_memberId, board_boardId, name, description, url, tag, folder_folderId) - values(#{member_memberId},#{board_boardId}, #{name}, #{description}, #{url}, #{tag}, #{folder_folderId}); + + + 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}) - 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 - - - delete - from bookmark - WHERE member_memberId=#{member_memberId} - AND website_websiteId=#{website_websiteId}; - - - - - - delete - from bookmark - WHERE member_memberId=#{memberId} - AND bookmarkId = #{bookmarkId}; - - - - - - delete - from bookmark - WHERE member_memberId=#{memberId} - AND board_boardId=#{boardId} - - - - - - - - - - - - - + + + UPDATE member_saved_link + SET deleted_at = now(), + deleted_by_member_id = #{memberId} + WHERE member_saved_link_id = #{bookmarkId} + AND created_by_member_id = #{memberId} + AND deleted_at IS NULL + - - - diff --git a/src/main/resources/mapper/comment-mapper.xml b/src/main/resources/mapper/comment-mapper.xml index 8f359f2..05760f4 100644 --- a/src/main/resources/mapper/comment-mapper.xml +++ b/src/main/resources/mapper/comment-mapper.xml @@ -1,79 +1,84 @@ - + + + + + + + + + + + + + - - insert INTO comment (board_boardId, member_memberId, member_nickname, member_job, member_major, content) - VALUES (#{board_boardId}, #{member_memberId}, #{member_nickname}, #{member_job,}, #{member_major}, #{content}); + INSERT INTO comment (board_board_id, member_member_id, member_nickname, member_job, member_major, content) + VALUES (#{boardBoardId}, #{memberMemberId}, #{memberNickname}, #{memberJob}, #{memberMajor}, #{content}) - + SELECT * + FROM comment + WHERE board_board_id = #{boardId} - + SELECT * + FROM comment + WHERE member_member_id = #{memberId} - + SELECT * + FROM comment + WHERE comment_id = #{commentId} - update comment - set - content = #{commentDto.content} - WHERE - commentId = #{commentId} + UPDATE comment + SET content = #{commentDto.content} + WHERE comment_id = #{commentId} - update comment - set - member_nickname = #{nickname}, - member_job = #{job}, - member_major = #{major} - WHERE - commentId = #{commentId} + UPDATE comment + SET + member_nickname = #{nickname}, + member_job = #{job}, + member_major = #{major} + WHERE comment_id = #{commentId} - delete - from comment - where commentId = #{commentId}; + DELETE FROM comment + WHERE comment_id = #{commentId} - + SELECT COUNT(*) FROM comment + WHERE board_board_id = #{boardId} - \ No newline at end of file diff --git a/src/main/resources/mapper/likes-mapper.xml b/src/main/resources/mapper/likes-mapper.xml index fc89472..a56519c 100644 --- a/src/main/resources/mapper/likes-mapper.xml +++ b/src/main/resources/mapper/likes-mapper.xml @@ -1,50 +1,40 @@ - - - - + + - insert INTO likes (board_boardId, member_memberId, is_Liked) + INSERT INTO likes (board_board_id, member_member_id, is_liked) VALUES (#{boardId}, #{memberId}, TRUE) - ON DUPLICATE KEY UPDATE is_Liked = TRUE; + ON CONFLICT (board_board_id, member_member_id) DO UPDATE SET is_liked = TRUE - update likes - SET - is_Liked = FALSE - WHERE - board_boardId = #{boardId} - AND - member_memberId = #{memberId}; + UPDATE likes + SET is_liked = FALSE + WHERE board_board_id = #{boardId} + AND member_member_id = #{memberId} - + SELECT COUNT(*) FROM likes + WHERE board_board_id = #{boardId} + AND is_liked = TRUE - \ No newline at end of file diff --git a/src/main/resources/mapper/main-mapper.xml b/src/main/resources/mapper/main-mapper.xml index 9a4268d..0d2ec76 100644 --- a/src/main/resources/mapper/main-mapper.xml +++ b/src/main/resources/mapper/main-mapper.xml @@ -7,7 +7,7 @@ @@ -30,13 +30,5 @@ ORDER BY CASE WHEN LOWER(name) LIKE CONCAT('%', LOWER(#{query}), '%') THEN 1 ELSE 2 END; - - - - \ No newline at end of file diff --git a/src/main/resources/mapper/member-mapper.xml b/src/main/resources/mapper/member-mapper.xml index 3e00492..fc653d4 100644 --- a/src/main/resources/mapper/member-mapper.xml +++ b/src/main/resources/mapper/member-mapper.xml @@ -4,62 +4,85 @@ "https://mybatis.org/dtd/mybatis-3-mapper.dtd"> + + + + + + + + + + + + + + + + + + + + + + - - insert into - member (username, password, nickname, role) - VALUES(#{username}, #{password}, #{nickname}, #{role}) + INSERT INTO member (login_id, password_hash, member_name, nick_name, email, role) + VALUES (#{loginId}, #{password}, #{memberName}, #{nickName}, #{email}, #{role}) - - insert into - member (username, password, nickname, role, email) - VALUES(#{username}, #{password}, #{nickname}, #{role}, #{email}) + + INSERT INTO member (login_id, password_hash, member_name, nick_name, role, email) + VALUES (#{loginId}, #{passwordHash}, #{memberName}, #{nickName}, #{role}, #{email}) - + SELECT * FROM member + WHERE member_id = #{memberId} + AND deleted_at IS NULL - + SELECT * FROM member + WHERE login_id = #{loginId} + AND deleted_at IS NULL - update member + UPDATE member SET - job = #{memberUpdateDto.job}, - major = #{memberUpdateDto.major}, - summary = #{memberUpdateDto.summary} - WHERE - memberId = #{memberId} + job = #{memberUpdateDto.job}, + major = #{memberUpdateDto.major}, + summary = #{memberUpdateDto.summary}, + nick_name = #{memberUpdateDto.nickName}, + updated_at = now() + WHERE member_id = #{memberId} + AND deleted_at IS NULL - update member + UPDATE member SET - username = #{member.username}, - job = #{member.job}, - major = #{member.major}, - summary = #{member.summary}, - email = #{member.email} - WHERE - memberId = #{memberId} + login_id = #{member.loginId}, + job = #{member.job}, + major = #{member.major}, + summary = #{member.summary}, + email = #{member.email}, + updated_at = now() + WHERE member_id = #{memberId} + AND deleted_at IS NULL - - - diff --git a/src/main/resources/templates/member/join.html b/src/main/resources/templates/member/join.html index 4f0209a..19b14aa 100644 --- a/src/main/resources/templates/member/join.html +++ b/src/main/resources/templates/member/join.html @@ -17,11 +17,11 @@

회원가입

- + -
+ focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500" type="text" th:field="*{loginId}" id="loginId" placeholder="아이디" required /> +
@@ -37,10 +37,10 @@

회원가입

- + -
+ focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500" type="text" th:field="*{nickName}" id="nickName" placeholder="닉네임" required /> +
- + + focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500" type="text" name="loginId" id="loginId" placeholder="아이디" required />
diff --git a/src/main/resources/templates/mypage/myPage.html b/src/main/resources/templates/mypage/myPage.html index c8fd09c..5244cef 100644 --- a/src/main/resources/templates/mypage/myPage.html +++ b/src/main/resources/templates/mypage/myPage.html @@ -31,7 +31,7 @@
Profile Picture -

Jane Doe

+

Jane Doe

@@ -79,7 +79,7 @@

프로필 수정

- +
+ + +
-
@@ -415,15 +418,16 @@

북마크 수정

// 폼 데이터로부터 객체 생성 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 // 기본 카테고리 }; // AJAX 요청 보내기 $.ajax({ - url: "/myPage/" + memberId + "/bookmark", + url: "/myPage/" + memberId + "/bookmark?url=" + encodeURIComponent(url), // URL을 쿼리 파라미터로 전달 type: 'POST', contentType: 'application/json', data: JSON.stringify(bookmarkData), // 자바스크립트 객체를 JSON 문자열로 변환 @@ -530,7 +534,8 @@

북마크 수정

// 북마크를 UI에 추가하는 함수 function addBookmarkToUI(bookmark) { - const { bookmarkId, name, url, description } = bookmark; + const { bookmarkId, displayTitle, note } = bookmark; + const url = bookmark.link ? bookmark.link.originalUrl : ''; const bookmarkElement = document.createElement('div'); bookmarkElement.className = 'relative flex flex-col w-full max-w-[200px] max-h-[150px] rounded-lg bg-white border border-gray-200 shadow overflow-visible group'; @@ -560,14 +565,14 @@

북마크 수정

-

${name}

+

${displayTitle}

-

${description}

+

${note || ''}

`; @@ -634,10 +639,15 @@

${name}

success: function(response) { // 불러온 데이터를 폼에 채우기 document.getElementById('updateBookmarkId').value = response.bookmarkId; - document.getElementById('updateName').value = response.name; - document.getElementById('updateUrl').value = response.url; - document.getElementById('updateDescription').value = response.description; - document.getElementById('updateTag').value = response.tag; + document.getElementById('updateName').value = response.displayTitle; + document.getElementById('updateUrl').value = response.link ? response.link.originalUrl : ''; // 히든값 + document.getElementById('displayUpdateUrl').value = response.link ? response.link.originalUrl : ''; // 표시용 + document.getElementById('updateDescription').value = response.note; + document.getElementById('updateTag').value = response.tag || ''; + + // 필수: 업데이트 시 기존 ID 유지 (Folder, Category) + document.getElementById('updateMemberFolderId').value = response.memberFolderId || ''; + document.getElementById('updatePrimaryCategoryId').value = response.primaryCategoryId || 1; // 북마크 수정 폼 표시 toggleUpdateBookmarkForm(); @@ -660,13 +670,18 @@

${name}

const url = document.getElementById('updateUrl').value; const description = document.getElementById('updateDescription').value; const tag = document.getElementById('updateTag').value.trim(); + 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 필드 }; // AJAX 요청 보내기 (북마크 업데이트) @@ -679,7 +694,7 @@

${name}

loadBookmarks(); // 수정된 내용 반영하여 북마크 목록 갱신 loadTags(); // 수정된 태그 반영하여 북마크 목록 갱신 toggleUpdateBookmarkForm(); // 수정 팝업 닫기 - document.getElementById('updateForm').reset(); // 폼 초기화 + document.getElementById('updateBookmarkForm').reset(); }, error: function(error) { console.error('Error:', error);