diff --git a/src/main/java/com/teamEWSN/gitdeun/common/exception/ErrorCode.java b/src/main/java/com/teamEWSN/gitdeun/common/exception/ErrorCode.java index eba4a9e..3586467 100644 --- a/src/main/java/com/teamEWSN/gitdeun/common/exception/ErrorCode.java +++ b/src/main/java/com/teamEWSN/gitdeun/common/exception/ErrorCode.java @@ -41,6 +41,14 @@ public enum ErrorCode { // 마인드맵 관련 MINDMAP_NOT_FOUND(HttpStatus.NOT_FOUND, "MINDMAP-001", "요청한 마인드맵을 찾을 수 없습니다."), + // 방문기록 관련 + HISTORY_NOT_FOUND(HttpStatus.NOT_FOUND, "VISITHISTORY-001", "방문 기록을 찾을 수 없습니다."), + + // 방문 기록 핀 고정 관련 + USER_NOT_FOUND_FIX_PIN(HttpStatus.NOT_FOUND, "PINNEDHISTORY-001", "핀 고정한 유저를 찾을 수 없습니다."), + PINNEDHISTORY_ALREADY_EXISTS(HttpStatus.CONFLICT, "PINNEDHISTORY-002", "이미 핀 고정한 기록입니다."), + PINNEDHISTORY_NOT_FOUND(HttpStatus.NOT_FOUND, "PINNEDHISTORY-003", "핀 고정 기록을 찾을 수 없습니다."), + // S3 파일 관련 // Client Errors (4xx) FILE_NOT_FOUND(HttpStatus.NOT_FOUND, "FILE-001", "요청한 파일을 찾을 수 없습니다."), diff --git a/src/main/java/com/teamEWSN/gitdeun/mindmap/service/MindmapService.java b/src/main/java/com/teamEWSN/gitdeun/mindmap/service/MindmapService.java index 25408ae..a88f024 100644 --- a/src/main/java/com/teamEWSN/gitdeun/mindmap/service/MindmapService.java +++ b/src/main/java/com/teamEWSN/gitdeun/mindmap/service/MindmapService.java @@ -13,9 +13,9 @@ import com.teamEWSN.gitdeun.mindmap.repository.MindmapRepository; import com.teamEWSN.gitdeun.repo.entity.Repo; import com.teamEWSN.gitdeun.repo.repository.RepoRepository; -import com.teamEWSN.gitdeun.repo.service.RepoService; import com.teamEWSN.gitdeun.user.entity.User; import com.teamEWSN.gitdeun.user.repository.UserRepository; +import com.teamEWSN.gitdeun.visithistory.service.VisitHistoryService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @@ -29,7 +29,7 @@ @Service @RequiredArgsConstructor public class MindmapService { - private final RepoService repoService; + private final VisitHistoryService visitHistoryService; private final MindmapMapper mindmapMapper; private final MindmapRepository mindmapRepository; private final RepoRepository repoRepository; @@ -74,6 +74,9 @@ public MindmapResponseDto createMindmap(MindmapCreateRequest req, Long userId) { mindmapRepository.save(mindmap); + // 방문 기록 생성 + visitHistoryService.createVisitHistory(user, mindmap); + return mindmapMapper.toResponseDto(mindmap); } diff --git a/src/main/java/com/teamEWSN/gitdeun/visithistory/controller/PinnedHistoryController.java b/src/main/java/com/teamEWSN/gitdeun/visithistory/controller/PinnedHistoryController.java new file mode 100644 index 0000000..d21df1d --- /dev/null +++ b/src/main/java/com/teamEWSN/gitdeun/visithistory/controller/PinnedHistoryController.java @@ -0,0 +1,40 @@ +package com.teamEWSN.gitdeun.visithistory.controller; + +import com.teamEWSN.gitdeun.common.jwt.CustomUserDetails; +import com.teamEWSN.gitdeun.visithistory.service.PinnedHistoryService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + +@Slf4j +@RestController +@RequestMapping("/api/history/{historyId}/mindmaps/pinned") +@RequiredArgsConstructor +public class PinnedHistoryController { + + private final PinnedHistoryService pinnedHistoryService; + + // 핀 고정 + @PostMapping + public ResponseEntity fixPinned( + @PathVariable("historyId") Long historyId, + @AuthenticationPrincipal CustomUserDetails customUserDetails + ) { + pinnedHistoryService.fixPinned(historyId, customUserDetails.getId()); + return ResponseEntity.status(HttpStatus.CREATED).build(); + } + + // 핀 해제 + @DeleteMapping + public ResponseEntity removePinned( + @PathVariable("historyId") Long historyId, + @AuthenticationPrincipal CustomUserDetails customUserDetails + ) { + pinnedHistoryService.removePinned(historyId, customUserDetails.getId()); + return ResponseEntity.noContent().build(); + } + +} diff --git a/src/main/java/com/teamEWSN/gitdeun/visithistory/controller/VisitHistoryController.java b/src/main/java/com/teamEWSN/gitdeun/visithistory/controller/VisitHistoryController.java index b4628cd..260ee09 100644 --- a/src/main/java/com/teamEWSN/gitdeun/visithistory/controller/VisitHistoryController.java +++ b/src/main/java/com/teamEWSN/gitdeun/visithistory/controller/VisitHistoryController.java @@ -1,5 +1,50 @@ package com.teamEWSN.gitdeun.visithistory.controller; +import com.teamEWSN.gitdeun.common.jwt.CustomUserDetails; +import com.teamEWSN.gitdeun.visithistory.dto.VisitHistoryResponseDto; +import com.teamEWSN.gitdeun.visithistory.service.VisitHistoryService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@Slf4j +@RestController +@RequestMapping("/api/history") +@RequiredArgsConstructor public class VisitHistoryController { - + + private final VisitHistoryService visitHistoryService; + + // 핀 고정되지 않은 방문 기록 조회 + @GetMapping("/visits") + public ResponseEntity> getVisitHistories( + @AuthenticationPrincipal CustomUserDetails userDetails + ) { + List histories = visitHistoryService.getVisitHistories(userDetails.getId()); + return ResponseEntity.ok(histories); + } + + // 핀 고정된 방문 기록 조회 + @GetMapping("/pins") + public ResponseEntity> getPinnedHistories( + @AuthenticationPrincipal CustomUserDetails userDetails + ) { + List histories = visitHistoryService.getPinnedHistories(userDetails.getId()); + return ResponseEntity.ok(histories); + } + + // 방문 기록 삭제 + @DeleteMapping("/visits/{historyId}") + public ResponseEntity deleteVisitHistory( + @PathVariable Long historyId, + @AuthenticationPrincipal CustomUserDetails userDetails + ) { + visitHistoryService.deleteVisitHistory(historyId, userDetails.getId()); + return ResponseEntity.ok().build(); + } + } \ No newline at end of file diff --git a/src/main/java/com/teamEWSN/gitdeun/visithistory/dto/VisitHistoryDto.java b/src/main/java/com/teamEWSN/gitdeun/visithistory/dto/VisitHistoryDto.java deleted file mode 100644 index d3b8424..0000000 --- a/src/main/java/com/teamEWSN/gitdeun/visithistory/dto/VisitHistoryDto.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.teamEWSN.gitdeun.visithistory.dto; - -public class VisitHistoryDto { - -} \ No newline at end of file diff --git a/src/main/java/com/teamEWSN/gitdeun/visithistory/dto/VisitHistoryResponseDto.java b/src/main/java/com/teamEWSN/gitdeun/visithistory/dto/VisitHistoryResponseDto.java new file mode 100644 index 0000000..33b7796 --- /dev/null +++ b/src/main/java/com/teamEWSN/gitdeun/visithistory/dto/VisitHistoryResponseDto.java @@ -0,0 +1,16 @@ +package com.teamEWSN.gitdeun.visithistory.dto; + +import lombok.Builder; +import lombok.Getter; + +import java.time.LocalDateTime; + +@Getter +@Builder +public class VisitHistoryResponseDto { + private Long visitHistoryId; + private Long mindmapId; + private String mindmapField; // 마인드맵 제목 + private String repoUrl; + private LocalDateTime lastVisitedAt; +} \ No newline at end of file diff --git a/src/main/java/com/teamEWSN/gitdeun/visithistory/entity/PinnedHistory.java b/src/main/java/com/teamEWSN/gitdeun/visithistory/entity/PinnedHistory.java index 1a86b4e..a4c5402 100644 --- a/src/main/java/com/teamEWSN/gitdeun/visithistory/entity/PinnedHistory.java +++ b/src/main/java/com/teamEWSN/gitdeun/visithistory/entity/PinnedHistory.java @@ -2,10 +2,10 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.teamEWSN.gitdeun.common.util.CreatedEntity; -import com.teamEWSN.gitdeun.mindmap.entity.Mindmap; import com.teamEWSN.gitdeun.user.entity.User; import jakarta.persistence.*; import lombok.AccessLevel; +import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; @@ -13,7 +13,10 @@ @Entity @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) -@Table(name = "pinned_history") +@Table(name = "pinned_history", indexes = { + @Index(name = "idx_pinnedHistory_user_visit_history", columnList = "user_id, visit_history_id", unique = true), // 주요 조회 조건 및 중복 방지 + @Index(name = "idx_pinnedHistory_visit_history_id", columnList = "visit_history_id") // 방문 기록 기준 핀 고정 목록 조회 +}) public class PinnedHistory extends CreatedEntity { @Id @@ -28,4 +31,10 @@ public class PinnedHistory extends CreatedEntity { @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "visit_history_id", nullable = false) private VisitHistory visitHistory; + + @Builder + public PinnedHistory(User user, VisitHistory visitHistory) { + this.user = user; + this.visitHistory = visitHistory; + } } diff --git a/src/main/java/com/teamEWSN/gitdeun/visithistory/mapper/VisitHistoryMapper.java b/src/main/java/com/teamEWSN/gitdeun/visithistory/mapper/VisitHistoryMapper.java new file mode 100644 index 0000000..d784911 --- /dev/null +++ b/src/main/java/com/teamEWSN/gitdeun/visithistory/mapper/VisitHistoryMapper.java @@ -0,0 +1,17 @@ +package com.teamEWSN.gitdeun.visithistory.mapper; + +import com.teamEWSN.gitdeun.visithistory.dto.VisitHistoryResponseDto; +import com.teamEWSN.gitdeun.visithistory.entity.VisitHistory; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.ReportingPolicy; + +@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE) +public interface VisitHistoryMapper { + + @Mapping(source = "id", target = "visitHistoryId") + @Mapping(source = "mindmap.id", target = "mindmapId") + @Mapping(source = "mindmap.field", target = "mindmapField") + @Mapping(source = "mindmap.repo.githubRepoUrl", target = "repoUrl") + VisitHistoryResponseDto toResponseDto(VisitHistory visitHistory); +} diff --git a/src/main/java/com/teamEWSN/gitdeun/visithistory/repository/PinnedHistoryRepository.java b/src/main/java/com/teamEWSN/gitdeun/visithistory/repository/PinnedHistoryRepository.java new file mode 100644 index 0000000..ff47d84 --- /dev/null +++ b/src/main/java/com/teamEWSN/gitdeun/visithistory/repository/PinnedHistoryRepository.java @@ -0,0 +1,20 @@ +package com.teamEWSN.gitdeun.visithistory.repository; + +import com.teamEWSN.gitdeun.user.entity.User; +import com.teamEWSN.gitdeun.visithistory.entity.PinnedHistory; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository +public interface PinnedHistoryRepository extends JpaRepository { + + boolean existsByUserIdAndVisitHistoryId(Long userId, Long historyId); + + Optional findByUserIdAndVisitHistoryId(Long userId, Long historyId); + + // 사용자의 핀 고정 기록 최신순 조회 + List findByUserOrderByCreatedAtDesc(User user); +} diff --git a/src/main/java/com/teamEWSN/gitdeun/visithistory/repository/VisitHistoryRepository.java b/src/main/java/com/teamEWSN/gitdeun/visithistory/repository/VisitHistoryRepository.java index a03ff56..ee928c6 100644 --- a/src/main/java/com/teamEWSN/gitdeun/visithistory/repository/VisitHistoryRepository.java +++ b/src/main/java/com/teamEWSN/gitdeun/visithistory/repository/VisitHistoryRepository.java @@ -1,5 +1,20 @@ package com.teamEWSN.gitdeun.visithistory.repository; -public class VisitHistoryRepository { - +import com.teamEWSN.gitdeun.user.entity.User; +import com.teamEWSN.gitdeun.visithistory.entity.VisitHistory; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface VisitHistoryRepository extends JpaRepository { + + // 사용자의 핀 고정되지 않은 방문 기록을 최신순으로 조회 + @Query("SELECT v FROM VisitHistory v LEFT JOIN v.pinnedHistorys p " + + "WHERE v.user = :user AND p IS NULL " + + "ORDER BY v.lastVisitedAt DESC") + List findUnpinnedHistoriesByUser(@Param("user") User user); } \ No newline at end of file diff --git a/src/main/java/com/teamEWSN/gitdeun/visithistory/service/PinnedHistoryService.java b/src/main/java/com/teamEWSN/gitdeun/visithistory/service/PinnedHistoryService.java new file mode 100644 index 0000000..69c9df7 --- /dev/null +++ b/src/main/java/com/teamEWSN/gitdeun/visithistory/service/PinnedHistoryService.java @@ -0,0 +1,57 @@ +package com.teamEWSN.gitdeun.visithistory.service; + +import com.teamEWSN.gitdeun.common.exception.GlobalException; +import com.teamEWSN.gitdeun.user.entity.User; +import com.teamEWSN.gitdeun.user.repository.UserRepository; +import com.teamEWSN.gitdeun.visithistory.entity.PinnedHistory; +import com.teamEWSN.gitdeun.visithistory.entity.VisitHistory; +import com.teamEWSN.gitdeun.visithistory.repository.PinnedHistoryRepository; +import com.teamEWSN.gitdeun.visithistory.repository.VisitHistoryRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import static com.teamEWSN.gitdeun.common.exception.ErrorCode.*; + + +@Slf4j +@Service +@Transactional +@RequiredArgsConstructor +public class PinnedHistoryService { + + private final PinnedHistoryRepository pinnedHistoryRepository; + private final UserRepository userRepository; + private final VisitHistoryRepository visitHistoryRepository; + + public void fixPinned(Long historyId, Long userId) { + User user = userRepository.findById(userId) + .orElseThrow(() -> new GlobalException(USER_NOT_FOUND_FIX_PIN)); + + VisitHistory visitHistory = visitHistoryRepository.findById(historyId) + .orElseThrow(() -> new GlobalException(HISTORY_NOT_FOUND)); + + // 이미 핀 고정이 있는지 확인 + if (pinnedHistoryRepository.existsByUserIdAndVisitHistoryId(userId, historyId)) { + throw new GlobalException(PINNEDHISTORY_ALREADY_EXISTS); + } + + PinnedHistory pin = PinnedHistory.builder() + .user(user) + .visitHistory(visitHistory) + .build(); + + pinnedHistoryRepository.save(pin); + + } + + @Transactional + public void removePinned(Long historyId, Long userId) { + PinnedHistory pin = pinnedHistoryRepository.findByUserIdAndVisitHistoryId(userId, historyId) + .orElseThrow(() -> new GlobalException(PINNEDHISTORY_NOT_FOUND)); + + pinnedHistoryRepository.delete(pin); + + } +} diff --git a/src/main/java/com/teamEWSN/gitdeun/visithistory/service/VisitHistoryService.java b/src/main/java/com/teamEWSN/gitdeun/visithistory/service/VisitHistoryService.java index b655d9f..30a914f 100644 --- a/src/main/java/com/teamEWSN/gitdeun/visithistory/service/VisitHistoryService.java +++ b/src/main/java/com/teamEWSN/gitdeun/visithistory/service/VisitHistoryService.java @@ -1,5 +1,76 @@ package com.teamEWSN.gitdeun.visithistory.service; +import com.teamEWSN.gitdeun.common.exception.ErrorCode; +import com.teamEWSN.gitdeun.common.exception.GlobalException; +import com.teamEWSN.gitdeun.mindmap.entity.Mindmap; +import com.teamEWSN.gitdeun.user.entity.User; +import com.teamEWSN.gitdeun.user.service.UserService; +import com.teamEWSN.gitdeun.visithistory.dto.VisitHistoryResponseDto; +import com.teamEWSN.gitdeun.visithistory.entity.PinnedHistory; +import com.teamEWSN.gitdeun.visithistory.entity.VisitHistory; +import com.teamEWSN.gitdeun.visithistory.mapper.VisitHistoryMapper; +import com.teamEWSN.gitdeun.visithistory.repository.PinnedHistoryRepository; +import com.teamEWSN.gitdeun.visithistory.repository.VisitHistoryRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor public class VisitHistoryService { - + + private final UserService userService; + private final VisitHistoryRepository visitHistoryRepository; + private final PinnedHistoryRepository pinnedHistoryRepository; + private final VisitHistoryMapper visitHistoryMapper; + + // 마인드맵 생성 시 호출되어 방문 기록을 생성 + @Transactional + public void createVisitHistory(User user, Mindmap mindmap) { + VisitHistory visitHistory = VisitHistory.builder() + .user(user) + .mindmap(mindmap) + .lastVisitedAt(LocalDateTime.now()) + .build(); + visitHistoryRepository.save(visitHistory); + } + + // 핀 고정되지 않은 방문 기록 조회 + @Transactional(readOnly = true) + public List getVisitHistories(Long userId) { + User user = userService.findById(userId); + List histories = visitHistoryRepository.findUnpinnedHistoriesByUser(user); + return histories.stream() + .map(visitHistoryMapper::toResponseDto) + .collect(Collectors.toList()); + } + + // 핀 고정된 방문 기록 조회 + @Transactional(readOnly = true) + public List getPinnedHistories(Long userId) { + User user = userService.findById(userId); + List pinnedHistories = pinnedHistoryRepository.findByUserOrderByCreatedAtDesc(user); + return pinnedHistories.stream() + .map(pinned -> visitHistoryMapper.toResponseDto(pinned.getVisitHistory())) + .collect(Collectors.toList()); + } + + // 방문 기록 삭제 + @Transactional + public void deleteVisitHistory(Long visitHistoryId, Long userId) { + VisitHistory visitHistory = visitHistoryRepository.findById(visitHistoryId) + .orElseThrow(() -> new GlobalException(ErrorCode.HISTORY_NOT_FOUND)); + + if (!visitHistory.getUser().getId().equals(userId)) { + throw new GlobalException(ErrorCode.FORBIDDEN_ACCESS); + } + + visitHistoryRepository.delete(visitHistory); + } + + } \ No newline at end of file