Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.back.domain.mybar.dto.MyBarListResponseDto;
import com.back.domain.mybar.service.MyBarService;
import com.back.global.rsData.RsData;
import io.swagger.v3.oas.annotations.Operation;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import lombok.RequiredArgsConstructor;
Expand All @@ -18,9 +19,23 @@
@Validated
public class MyBarController {

/**
* 내 바(킵) API 컨트롤러.
* 내가 킵한 칵테일 목록 조회, 킵 추가/복원, 킵 해제를 제공합니다.
*/

private final MyBarService myBarService;

/**
* 내 바 목록 조회(무한스크롤)
* @param userId 인증된 사용자 ID
* @param lastKeptAt 이전 페이지 마지막 keptAt (옵션)
* @param lastId 이전 페이지 마지막 id (옵션)
* @param limit 페이지 크기(1~100)
* @return 킵 아이템 목록과 다음 페이지 커서
*/
@GetMapping
@Operation(summary = "내 바 목록", description = "내가 킵한 칵테일 목록 조회. 무한스크롤 파라미터 지원")
public RsData<MyBarListResponseDto> getMyBarList(
@AuthenticationPrincipal(expression = "id") Long userId,
@RequestParam(required = false)
Expand All @@ -32,8 +47,14 @@ public RsData<MyBarListResponseDto> getMyBarList(
return RsData.successOf(body); // code=200, message="success"
}

/** 킵 추가(생성/복원/재킵) */
/**
* 킵 추가(생성/복원/재킵)
* @param userId 인증된 사용자 ID
* @param cocktailId 칵테일 ID
* @return 201 kept
*/
@PostMapping("/{cocktailId}/keep")
@Operation(summary = "킵 추가/복원", description = "해당 칵테일을 내 바에 킵합니다. 이미 삭제된 경우 복원")
public RsData<Void> keep(
@AuthenticationPrincipal(expression = "id") Long userId,
@PathVariable Long cocktailId
Expand All @@ -42,8 +63,14 @@ public RsData<Void> keep(
return RsData.of(201, "kept"); // Aspect가 HTTP 201로 설정
}

/** 킵 해제(소프트 삭제) — 멱등 */
/**
* 킵 해제(소프트 삭제) — 멱등
* @param userId 인증된 사용자 ID
* @param cocktailId 칵테일 ID
* @return 200 deleted
*/
@DeleteMapping("/{cocktailId}/keep")
@Operation(summary = "킵 해제", description = "내 바에서 해당 칵테일 킵을 해제합니다(소프트 삭제, 멱등)")
public RsData<Void> unkeep(
@AuthenticationPrincipal(expression = "id") Long userId,
@PathVariable Long cocktailId
Expand Down
11 changes: 9 additions & 2 deletions src/main/java/com/back/domain/mybar/service/MyBarService.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ public class MyBarService {
private final UserRepository userRepository;
private final CocktailRepository cocktailRepository;

// 커서: lastKeptAt + lastId를 그대로 파라미터로 사용
// 내 바 목록 조회 (무한스크롤)
// - 커서: lastKeptAt + lastId 조합으로 안정적인 정렬/페이지네이션
// - 첫 페이지: 가장 최근 keptAt 기준으로 최신순
@Transactional(readOnly = true)
public MyBarListResponseDto getMyBar(Long userId, LocalDateTime lastKeptAt, Long lastId, int limit) {
int safeLimit = Math.max(1, Math.min(limit, 100));
Expand All @@ -44,6 +46,7 @@ public MyBarListResponseDto getMyBar(Long userId, LocalDateTime lastKeptAt, Long
rows = myBarRepository.findSliceByCursor(userId, KeepStatus.ACTIVE, lastKeptAt, lastId, pageable);
}

// +1 로우가 있으면 다음 페이지가 존재
boolean hasNext = rows.size() > safeLimit;
if (hasNext) rows = rows.subList(0, safeLimit);

Expand All @@ -61,6 +64,10 @@ public MyBarListResponseDto getMyBar(Long userId, LocalDateTime lastKeptAt, Long
return new MyBarListResponseDto(items, hasNext, nextKeptAt, nextId);
}

// 킵 추가/복원
// - 이미 존재하면 keptAt 갱신 (정렬 최신화)
// - DELETED 상태였다면 ACTIVE로 복원
// - 없으면 새로 생성
@Transactional
public void keep(Long userId, Long cocktailId) {
Optional<MyBar> existingMyBar =
Expand Down Expand Up @@ -90,7 +97,7 @@ public void keep(Long userId, Long cocktailId) {
myBarRepository.save(myBar);
}

/** 킵 해제(소프트 삭제) */
/** 킵 해제(소프트 삭제) — 멱등 처리 */
@Transactional
public void unkeep(Long userId, Long cocktailId) {
myBarRepository.softDeleteByUserAndCocktail(userId, cocktailId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import com.back.domain.myhistory.dto.MyHistoryLikedPostListDto;
import com.back.domain.myhistory.service.MyHistoryService;
import com.back.global.rsData.RsData;
import io.swagger.v3.oas.annotations.Operation;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import lombok.RequiredArgsConstructor;
Expand All @@ -24,7 +25,16 @@ public class MyHistoryController {

private final MyHistoryService myHistoryService;

/**
* 내가 작성한 게시글 목록(무한스크롤)
* @param userId 인증된 사용자 ID
* @param lastCreatedAt 이전 페이지 마지막 createdAt (옵션)
* @param lastId 이전 페이지 마지막 id (옵션)
* @param limit 페이지 크기(1~100)
* @return 게시글 아이템 목록과 다음 페이지 커서
*/
@GetMapping("/posts")
@Operation(summary = "내 게시글 목록", description = "내가 작성한 게시글 최신순 목록. 무한스크롤 파라미터 지원")
public RsData<MyHistoryPostListDto> getMyPosts(
@AuthenticationPrincipal(expression = "id") Long userId,
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime lastCreatedAt,
Expand All @@ -35,7 +45,14 @@ public RsData<MyHistoryPostListDto> getMyPosts(
return RsData.successOf(body);
}

/**
* 내 게시글 이동 링크
* @param userId 인증된 사용자 ID
* @param postId 게시글 ID
* @return 게시글 상세 이동 링크 정보
*/
@GetMapping("/posts/{id}")
@Operation(summary = "내 게시글로 이동", description = "내가 작성한 게시글 상세 링크 정보 반환")
public RsData<com.back.domain.myhistory.dto.MyHistoryPostGoResponseDto> goFromPost(
@AuthenticationPrincipal(expression = "id") Long userId,
@PathVariable("id") Long postId
Expand All @@ -44,7 +61,16 @@ public RsData<com.back.domain.myhistory.dto.MyHistoryPostGoResponseDto> goFromPo
return RsData.successOf(body);
}

/**
* 내가 작성한 댓글 목록(무한스크롤)
* @param userId 인증된 사용자 ID
* @param lastCreatedAt 이전 페이지 마지막 createdAt (옵션)
* @param lastId 이전 페이지 마지막 id (옵션)
* @param limit 페이지 크기(1~100)
* @return 댓글 아이템 목록과 다음 페이지 커서
*/
@GetMapping("/comments")
@Operation(summary = "내 댓글 목록", description = "내가 작성한 댓글 최신순 목록. 무한스크롤 파라미터 지원")
public RsData<MyHistoryCommentListDto> getMyComments(
@AuthenticationPrincipal(expression = "id") Long userId,
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime lastCreatedAt,
Expand All @@ -55,7 +81,16 @@ public RsData<MyHistoryCommentListDto> getMyComments(
return RsData.successOf(body);
}

/**
* 내가 좋아요한 게시글 목록(무한스크롤)
* @param userId 인증된 사용자 ID
* @param lastCreatedAt 이전 페이지 마지막 createdAt (옵션)
* @param lastId 이전 페이지 마지막 id (옵션)
* @param limit 페이지 크기(1~100)
* @return 좋아요 게시글 아이템 목록과 다음 페이지 커서
*/
@GetMapping("/likes")
@Operation(summary = "좋아요한 게시글 목록", description = "좋아요한 게시글 최신순 목록. 무한스크롤 파라미터 지원")
public RsData<MyHistoryLikedPostListDto> getMyLikedPosts(
@AuthenticationPrincipal(expression = "id") Long userId,
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime lastCreatedAt,
Expand All @@ -66,7 +101,14 @@ public RsData<MyHistoryLikedPostListDto> getMyLikedPosts(
return RsData.successOf(body);
}

/**
* 댓글에서 게시글 이동 링크
* @param userId 인증된 사용자 ID
* @param commentId 댓글 ID
* @return 댓글이 달린 게시글 상세 이동 링크 정보
*/
@GetMapping("/comments/{id}")
@Operation(summary = "댓글에서 게시글 이동", description = "내 댓글이 달린 게시글 상세 링크 정보 반환")
public RsData<MyHistoryCommentGoResponseDto> goFromComment(
@AuthenticationPrincipal(expression = "id") Long userId,
@PathVariable("id") Long commentId
Expand All @@ -75,7 +117,14 @@ public RsData<MyHistoryCommentGoResponseDto> goFromComment(
return RsData.successOf(body);
}

/**
* 좋아요 목록에서 게시글 이동 링크
* @param userId 인증된 사용자 ID
* @param postId 게시글 ID
* @return 좋아요한 게시글 상세 이동 링크 정보
*/
@GetMapping("/likes/{id}")
@Operation(summary = "좋아요 목록에서 이동", description = "좋아요한 게시글 상세 링크 정보 반환")
public RsData<com.back.domain.myhistory.dto.MyHistoryPostGoResponseDto> goFromLikedPost(
@AuthenticationPrincipal(expression = "id") Long userId,
@PathVariable("id") Long postId
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ public class MyHistoryService {
private final MyHistoryCommentRepository myHistoryCommentRepository;
private final MyHistoryLikedPostRepository myHistoryLikedPostRepository;

// 내가 작성한 게시글 목록 (무한스크롤)
// - 삭제(DELETED)된 글은 제외, 최신순(createdAt desc, id desc)
@Transactional(readOnly = true)
public MyHistoryPostListDto getMyPosts(Long userId, LocalDateTime lastCreatedAt, Long lastId, int limit) {
int safeLimit = Math.max(1, Math.min(limit, 100));
Expand All @@ -37,6 +39,7 @@ public MyHistoryPostListDto getMyPosts(Long userId, LocalDateTime lastCreatedAt,
rows = myHistoryPostRepository.findMyPostsAfter(userId, PostStatus.DELETED, lastCreatedAt, lastId, PageRequest.of(0, fetchSize));
}

// +1개 초과 여부로 다음 페이지 유무 판단
boolean hasNext = rows.size() > safeLimit;
if (hasNext) rows = rows.subList(0, safeLimit);

Expand All @@ -54,6 +57,8 @@ public MyHistoryPostListDto getMyPosts(Long userId, LocalDateTime lastCreatedAt,
return new MyHistoryPostListDto(items, hasNext, nextCreatedAt, nextId);
}

// 내가 작성한 댓글 목록 (무한스크롤)
// - 댓글과 게시글을 함께 조회(join fetch)하여 N+1 방지
@Transactional(readOnly = true)
public MyHistoryCommentListDto getMyComments(Long userId, LocalDateTime lastCreatedAt, Long lastId, int limit) {
int safeLimit = Math.max(1, Math.min(limit, 100));
Expand Down Expand Up @@ -83,6 +88,9 @@ public MyHistoryCommentListDto getMyComments(Long userId, LocalDateTime lastCrea
return new MyHistoryCommentListDto(items, hasNext, nextCreatedAt, nextId);
}

// 내 댓글에서 게시글로 이동 링크 생성
// - 권한 확인: 해당 댓글이 내 댓글인지 검사
// - 게시글 상태가 삭제면 이동 불가(410)
@Transactional(readOnly = true)
public MyHistoryCommentGoResponseDto getPostLinkFromMyComment(Long userId, Long commentId) {
Comment c = myHistoryCommentRepository.findByIdAndUserId(commentId, userId);
Expand All @@ -98,6 +106,7 @@ public MyHistoryCommentGoResponseDto getPostLinkFromMyComment(Long userId, Long
return new MyHistoryCommentGoResponseDto(postId, apiUrl);
}

// 내가 작성한 게시글에서 이동 링크 생성 (권한/상태 검증 포함)
@Transactional(readOnly = true)
public MyHistoryPostGoResponseDto getPostLinkFromMyPost(Long userId, Long postId) {
Post p = myHistoryPostRepository.findByIdAndUserId(postId, userId);
Expand All @@ -111,6 +120,8 @@ public MyHistoryPostGoResponseDto getPostLinkFromMyPost(Long userId, Long postId
return new MyHistoryPostGoResponseDto(p.getId(), apiUrl);
}

// 내가 좋아요(추천)한 게시글 목록 (무한스크롤)
// - PostLike.createdAt 기준 최신순, 삭제된 게시글 제외
@Transactional(readOnly = true)
public MyHistoryLikedPostListDto getMyLikedPosts(Long userId, LocalDateTime lastCreatedAt, Long lastId, int limit) {
int safeLimit = Math.max(1, Math.min(limit, 100));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import jakarta.validation.Valid;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import io.swagger.v3.oas.annotations.Operation;
import java.time.LocalDateTime;
import lombok.RequiredArgsConstructor;
import org.springframework.format.annotation.DateTimeFormat;
Expand All @@ -31,17 +32,38 @@
@Validated
public class NotificationController {

/**
* 알림 API 컨트롤러.
* 알림 목록 조회, 읽음 처리 후 이동 정보, 알림 설정 조회/변경, SSE 구독을 제공합니다.
*/

private final NotificationService notificationService;
private final NotificationSettingService notificationSettingService;

// SSE 연결
// produces = "text/event-stream": 응답 형식이 SSE임을 명시
/**
* 알림 SSE 구독
*
* @return SSE 스트림 핸들러(SseEmitter)
*/
@GetMapping(value = "/subscribe", produces = "text/event-stream")
@Operation(summary = "알림 SSE 구독", description = "Server-Sent Events로 실시간 알림 스트림 구독")
public SseEmitter subscribe() {
return notificationService.subscribe();
}

/**
* 알림 목록 조회(무한스크롤)
*
* @param userId 인증된 사용자 ID
* @param lastCreatedAt 이전 페이지 마지막 createdAt (옵션)
* @param lastId 이전 페이지 마지막 id (옵션)
* @param limit 페이지 크기(1~100)
* @return 알림 아이템 목록과 다음 페이지 커서
*/
@GetMapping("/notifications")
@Operation(summary = "알림 목록 조회", description = "무한스크롤(nextCreatedAt, nextId) 기반 최신순 조회. limit 1~100")
public RsData<NotificationListResponseDto> getNotifications(
@AuthenticationPrincipal(expression = "id") Long userId,
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime lastCreatedAt,
Expand All @@ -52,15 +74,30 @@ public RsData<NotificationListResponseDto> getNotifications(
return RsData.successOf(body);
}

/**
* 알림 설정 조회
*
* @param userId 인증된 사용자 ID
* @return enabled 상태 (없으면 기본 true)
*/
@GetMapping("/notification-setting")
@Operation(summary = "알림 설정 조회", description = "사용자 알림 on/off 상태 조회. 미생성 시 기본 true 반환")
public RsData<NotificationSettingDto> getMyNotificationSetting(
@AuthenticationPrincipal(expression = "id") Long userId
) {
NotificationSettingDto body = notificationSettingService.getMySetting(userId);
return RsData.successOf(body);
}

/**
* 알림 설정 변경(멱등)
*
* @param userId 인증된 사용자 ID
* @param req enabled true/false
* @return 변경된 enabled 상태
*/
@PatchMapping("/notification-setting")
@Operation(summary = "알림 설정 변경", description = "enabled 값을 true/false로 설정(멱등)")
public RsData<NotificationSettingDto> setMyNotificationSetting(
@AuthenticationPrincipal(expression = "id") Long userId,
@Valid @RequestBody NotificationSettingUpdateRequestDto req
Expand All @@ -69,7 +106,15 @@ public RsData<NotificationSettingDto> setMyNotificationSetting(
return RsData.successOf(body);
}

/**
* 알림 읽음 처리 후 이동 정보 반환
*
* @param userId 인증된 사용자 ID
* @param notificationId 알림 ID
* @return 게시글 ID와 게시글 API URL
*/
@PostMapping("/notifications/{id}")
@Operation(summary = "읽음 처리 후 이동 정보", description = "알림을 읽음 처리하고 해당 게시글 ID와 API URL 반환")
public RsData<NotificationGoResponseDto> goPostLink(
@AuthenticationPrincipal(expression = "id") Long userId,
@PathVariable("id") Long notificationId
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ public SseEmitter subscribe() {
@Transactional(readOnly = true)
public NotificationListResponseDto getNotifications(Long userId, LocalDateTime lastCreatedAt, Long lastId, int limit) {
int safeLimit = Math.max(1, Math.min(limit, 100));
int fetchSize = safeLimit + 1;
int fetchSize = safeLimit + 1; // 다음 페이지가 있는지 판단하기 위해 1건 더 조회

List<Notification> rows;
if (lastCreatedAt == null || lastId == null) {
Expand All @@ -57,7 +57,7 @@ public NotificationListResponseDto getNotifications(Long userId, LocalDateTime l
rows = notificationRepository.findMyNotificationsAfter(userId, lastCreatedAt, lastId, PageRequest.of(0, fetchSize));
}

boolean hasNext = rows.size() > safeLimit;
boolean hasNext = rows.size() > safeLimit; // +1 개가 있으면 다음 페이지 존재
if (hasNext) rows = rows.subList(0, safeLimit);

List<NotificationItemDto> items = new ArrayList<>();
Expand All @@ -74,6 +74,7 @@ public NotificationListResponseDto getNotifications(Long userId, LocalDateTime l
return new NotificationListResponseDto(items, hasNext, nextCreatedAt, nextId);
}

// 읽음 처리 + 게시글 링크 반환
@Transactional
public NotificationGoResponseDto markAsReadAndGetPostLink(Long userId, Long notificationId) {
Notification notification = notificationRepository.findByIdAndUserId(notificationId, userId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ public class NotificationSettingService {
private final NotificationSettingRepository notificationSettingRepository;
private final UserRepository userRepository;

// 알림 설정 조회
// - 아직 생성 전이면 기본값(true)로 동작
@Transactional(readOnly = true)
public NotificationSettingDto getMySetting(Long userId) {
NotificationSetting s = notificationSettingRepository.findByUserId(userId);
Expand All @@ -27,6 +29,8 @@ public NotificationSettingDto getMySetting(Long userId) {
return NotificationSettingDto.from(s);
}

// 알림 설정 저장(멱등)
// - 없으면 생성, 있으면 enabled 값만 갱신
@Transactional
public NotificationSettingDto setMySetting(Long userId, boolean enabled) {
NotificationSetting s = notificationSettingRepository.findByUserId(userId);
Expand Down
Loading