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 @@ -13,6 +13,7 @@
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Min;
import lombok.RequiredArgsConstructor;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
Expand Down Expand Up @@ -428,5 +429,22 @@ public ResponseEntity<ApiResponse<Void>> updateProfileImage(
memberService.updateProfileImage(userDetails.getUsername(), profileImage);
return ApiResponse.success_only(SuccessStatus.UPDATE_PROFILE_IMAGE_SUCCESS);
}

@Operation(
summary = "스토리 보관함 조회 API",
description = "마이페이지 스토리 보관함을 내역을 조회합니다."
)
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "스토리 보관함 조회 성공"),
})
@GetMapping("/my-story")
public ResponseEntity<ApiResponse<MyStoryResponseDTO>> getMyStories(
@AuthenticationPrincipal UserDetails userDetails,
@RequestParam(required = false, defaultValue = "1") @Min(value = 1, message = "페이지는 1 이상이어야 합니다.(1부터 시작)") Integer page,
@RequestParam(required = false, defaultValue = "10") @Min(value = 1, message = "한 페이지당 개수는 1 이상이어야 합니다.") Integer size) {

MyStoryResponseDTO myStoryResponseDTO = memberService.getMyStories(userDetails.getUsername(), page, size);
return ApiResponse.success(SuccessStatus.GET_MY_STORY_SUCCESS, myStoryResponseDTO);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.moongeul.backend.api.member.dto;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.time.LocalDateTime;
import java.util.List;

@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class MyStoryResponseDTO {

private Long total; // 전체 검색 결과 수
private Integer page; // 현재 페이지
private Integer size; // 페이지당 개수
private Integer totalPages; // 전체 페이지 수
private Boolean isLast; // 마지막 페이지 여부
private List<StoryInfo> data; // 기록 리스트

@Getter
@Builder
public static class StoryInfo {
private Long storyId;
private String storyImage;
private LocalDateTime created; // 작성 시간
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
import com.moongeul.backend.api.post.repository.PostRepository;
import com.moongeul.backend.api.post.repository.QuoteRepository;
import com.moongeul.backend.api.book.entity.Book;
import com.moongeul.backend.api.story.entity.Story;
import com.moongeul.backend.api.story.repository.StoryRepository;
import com.moongeul.backend.api.question.repository.AnswerRepository;
import com.moongeul.backend.api.question.repository.QuestionRepository;
import com.moongeul.backend.api.setting.repository.AgreeRepository;
Expand Down Expand Up @@ -57,6 +59,7 @@ public class MemberService {
private final CategoryRepository categoryRepository;
private final PostRepository postRepository;
private final QuoteRepository quoteRepository;
private final StoryRepository storyRepository;
private final LikeRepository likeRepository;
private final WithdrawalRepository withdrawalRepository;
private final NotificationRepository notificationRepository;
Expand Down Expand Up @@ -624,6 +627,32 @@ private void validatePrivacyAccess(Member currentMember, Member targetMember) {
}
}

/* 스토리 보관함 조회 API */
@Transactional(readOnly = true)
public MyStoryResponseDTO getMyStories(String email, Integer page, Integer size) {

Pageable pageable = PageRequest.of(page - 1, size);

Page<Story> myStoryPage = storyRepository.findAllMyStories(email, pageable);

List<MyStoryResponseDTO.StoryInfo> storyInfoList = myStoryPage.getContent().stream()
.map(story -> MyStoryResponseDTO.StoryInfo.builder()
.storyId(story.getId())
.storyImage(story.getStoryImage())
.created(story.getCreatedAt())
.build())
.toList();

return MyStoryResponseDTO.builder()
.total(myStoryPage.getTotalElements())
.page(myStoryPage.getNumber() + 1)
.size(myStoryPage.getSize())
.totalPages(myStoryPage.getTotalPages())
.isLast(myStoryPage.isLast())
.data(storyInfoList)
.build();
}

// 회원 조회 메서드
private Member getMemberByEmail(String email) {
return memberRepository.findByEmail(email)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import com.moongeul.backend.api.member.entity.Member;
import com.moongeul.backend.api.member.entity.Role;
import com.moongeul.backend.api.member.repository.MemberRepository;
import com.moongeul.backend.api.setting.dto.NoticeAllRequestDTO;
import com.moongeul.backend.api.setting.dto.NoticeAllResponseDTO;
import com.moongeul.backend.api.setting.dto.NoticeDTO;
import com.moongeul.backend.api.setting.dto.NoticeRequestDTO;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package com.moongeul.backend.api.story.controller;

import com.moongeul.backend.api.post.entity.PostVisibility;
import com.moongeul.backend.api.story.dto.StoryAllResponseDTO;
import com.moongeul.backend.api.story.dto.StoryIdResponseDTO;
import com.moongeul.backend.api.story.dto.StoryDTO;
import com.moongeul.backend.api.story.service.StoryService;
import com.moongeul.backend.common.response.ApiResponse;
import com.moongeul.backend.common.response.SuccessStatus;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.constraints.Min;
import lombok.RequiredArgsConstructor;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

@Tag(name = "Story", description = "Story(스토리) 관련 API 입니다.")
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/v2/story")
public class StoryController {

private final StoryService storyService;

@Operation(
summary = "스토리 제작 API",
description = "스토리에서 제작한 '이미지' 파일을 저장합니다." +
"<br>- 해당 스토리와 연관된 게시글 ID(postId)값을 넘겨주세요."
)
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "스토리 제작 성공"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "업로드할 이미지가 없거나 이미지 형식이 아닙니다."),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "해당 사용자를 찾을 수 없습니다."),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "500", description = "파일 업로드/삭제에 실패했습니다.")
})
@PostMapping(value = "/create/{postId}", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ResponseEntity<ApiResponse<StoryIdResponseDTO>> createStory(
@AuthenticationPrincipal UserDetails userDetails,
@RequestPart("StoryImage") MultipartFile storyImage,
@PathVariable Long postId) {

StoryIdResponseDTO storyIdResponseDTO = storyService.createStory(userDetails.getUsername(), storyImage, postId);
return ApiResponse.success(SuccessStatus.CREATE_STORY_SUCCESS, storyIdResponseDTO);
}

@Operation(
summary = "스토리 전체 조회 API",
description = "활성화된 스토리를 전체 조회합니다."
)
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "스토리 전체 조회 성공"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "해당 사용자를 찾을 수 없습니다."),
})
@GetMapping
public ResponseEntity<ApiResponse<StoryAllResponseDTO>> getStory(
@AuthenticationPrincipal UserDetails userDetails,
@RequestParam(required = false, defaultValue = "PUBLIC") PostVisibility postVisibility,
@RequestParam(required = false, defaultValue = "1") @Min(value = 1, message = "페이지는 1 이상이어야 합니다.(1부터 시작)") Integer page,
@RequestParam(required = false, defaultValue = "10") @Min(value = 1, message = "한 페이지당 개수는 1 이상이어야 합니다.") Integer size) {

// 비회원인 경우 "anonymousUser", 회원인 경우 email
String username = (userDetails != null) ? userDetails.getUsername() : "anonymousUser";

StoryAllResponseDTO storyAllResponseDTO = storyService.getALLStory(username, postVisibility, page, size);
return ApiResponse.success(SuccessStatus.GET_ALL_STORY_SUCCESS, storyAllResponseDTO);
}

@Operation(
summary = "스토리 상세 조회 API",
description = "스토리를 상세 조회합니다."
)
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "스토리 상세 조회 성공"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "해당 스토리를 찾을 수 없습니다."),
})
@GetMapping("/{id}")
public ResponseEntity<ApiResponse<StoryDTO>> getStory(@PathVariable Long id) {

StoryDTO storyDTO = storyService.getStory(id);
return ApiResponse.success(SuccessStatus.GET_STORY_SUCCESS, storyDTO);
}

@Operation(
summary = "스토리 삭제 API",
description = "스토리를 삭제합니다."
)
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "스토리 삭제 성공"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "401", description = "회원의 스토리가 아닙니다."),
})
@DeleteMapping("/{id}")
public ResponseEntity<ApiResponse<Void>> deleteStory(@AuthenticationPrincipal UserDetails userDetails,
@PathVariable Long id) {

storyService.deleteStory(userDetails.getUsername(), id);
return ApiResponse.success_only(SuccessStatus.DELETE_STORY_SUCCESS);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.moongeul.backend.api.story.dto;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.util.List;

@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class StoryAllResponseDTO {

private Long total; // 전체 검색 결과 수
private Integer page; // 현재 페이지
private Integer size; // 페이지당 개수
private Integer totalPages; // 전체 페이지 수
private Boolean isLast; // 마지막 페이지 여부
private List<StoryDTO> data; // 기록 리스트
}
36 changes: 36 additions & 0 deletions src/main/java/com/moongeul/backend/api/story/dto/StoryDTO.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.moongeul.backend.api.story.dto;

import com.moongeul.backend.api.readingTaste.entity.ReadingTasteType;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.time.LocalDateTime;

@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class StoryDTO {

private MemberInfo memberInfo;
private StoryInfo storyInfo;

@Getter
@Builder
public static class MemberInfo {
private Long memberId;
private String nickname; // 닉네임
private String profileImage; // 회원 이미지
private ReadingTasteType readingTasteType; // 독서 취향 유형
}

@Getter
@Builder
public static class StoryInfo {
private Long storyId;
private String storyImage;
private LocalDateTime created; // 작성 시간
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.moongeul.backend.api.story.dto;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class StoryIdResponseDTO {

private Long storyId;
}
33 changes: 33 additions & 0 deletions src/main/java/com/moongeul/backend/api/story/entity/Story.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.moongeul.backend.api.story.entity;

import com.moongeul.backend.api.member.entity.Member;
import com.moongeul.backend.api.post.entity.Post;
import com.moongeul.backend.common.entity.BaseTimeEntity;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Entity
@Getter
@Builder // 빌더 패턴 사용을 위한 롬복 애너테이션
@NoArgsConstructor // 기본 생성자
@AllArgsConstructor // 모든 필드를 포함한 생성자
@Table(name = "STORY") // 데이터베이스 테이블 이름 지정
public class Story extends BaseTimeEntity {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

private String storyImage;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id", nullable = false)
private Member member;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "post_id", nullable = false)
private Post post;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package com.moongeul.backend.api.story.repository;

import com.moongeul.backend.api.story.entity.Story;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

import java.time.LocalDateTime;

public interface StoryRepository extends JpaRepository<Story, Long> {

// 전체 공개 스토리 조회 (비로그인/로그인 공용) (24시간 이내 + 팔로잉 스토리 + 내 스토리 포함)
@Query("SELECT DISTINCT s FROM Story s " +
"JOIN FETCH s.member m " +
"JOIN FETCH s.post p " +
"LEFT JOIN Follow f ON f.following = m AND f.follower.email = :email " +
"WHERE s.createdAt >= :timeLimit " +
"AND (" +
" p.postVisibility = 'PUBLIC' " + // 1. 전체 공개 스토리
" OR m.email = :email " + // 2. 내 스토리
" OR (p.postVisibility = 'FOLLOWERS' AND f.followStatus = 'ACCEPTED')" + // 3. 팔로잉 중인 스토리
")")
Page<Story> findAllPublicStories(
@Param("email") String email,
@Param("timeLimit") LocalDateTime timeLimit,
Pageable pageable
);

// 팔로워 공개 스토리 조회 (24시간 이내 + 팔로잉 스토리 + 내 스토리)
@Query("SELECT s FROM Story s " +
"JOIN FETCH s.member " +
"JOIN FETCH s.post p " +
"LEFT JOIN Follow f ON f.following = s.member AND f.follower.email = :email " +
"WHERE ((s.member.email = :email) " +
"OR (p.postVisibility = 'FOLLOWERS' AND f.followStatus = 'ACCEPTED')) " +
"AND s.createdAt >= :timeLimit")
Page<Story> findAllFollowerStories(
@Param("email") String email,
@Param("timeLimit") LocalDateTime timeLimit,
Pageable pageable
);

// 내 스토리 조회
@Query("SELECT s FROM Story s " +
"WHERE s.member.email = :email " +
"ORDER BY s.createdAt DESC")
Page<Story> findAllMyStories(@Param("email") String email, Pageable pageable);
}
Loading