Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[DDING-53] Feed 생성 API 구현 #197

Merged
merged 17 commits into from
Jan 6, 2025
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
@@ -0,0 +1,30 @@
package ddingdong.ddingdongBE.domain.feed.api;

import ddingdong.ddingdongBE.auth.PrincipalDetails;
import ddingdong.ddingdongBE.domain.feed.controller.dto.request.CreateFeedRequest;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseStatus;

@Tag(name = "Feed - Club", description = "Feed API")
@RequestMapping("/server")
public interface ClubFeedApi {

@Operation(summary = "동아리 피드 생성 API")
@ApiResponse(responseCode = "201", description = "동아리 피드 생성 성공")
@ResponseStatus(HttpStatus.CREATED)
@SecurityRequirement(name = "AccessToken")
@PostMapping("/central/clubs/feeds")
void createFeed(
@RequestBody @Valid CreateFeedRequest createFeedRequest,
@AuthenticationPrincipal PrincipalDetails principalDetails
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package ddingdong.ddingdongBE.domain.feed.controller;

import ddingdong.ddingdongBE.auth.PrincipalDetails;
import ddingdong.ddingdongBE.domain.feed.api.ClubFeedApi;
import ddingdong.ddingdongBE.domain.feed.controller.dto.request.CreateFeedRequest;
import ddingdong.ddingdongBE.domain.feed.service.FacadeClubFeedService;
import ddingdong.ddingdongBE.domain.user.entity.User;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequiredArgsConstructor
public class ClubFeedController implements ClubFeedApi {

private final FacadeClubFeedService facadeClubFeedService;

@Override
public void createFeed(CreateFeedRequest createFeedRequest, PrincipalDetails principalDetails) {
User user = principalDetails.getUser();
facadeClubFeedService.create(createFeedRequest.toCommand(user));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package ddingdong.ddingdongBE.domain.feed.controller.dto.request;

import ddingdong.ddingdongBE.domain.feed.service.dto.command.CreateFeedCommand;
import ddingdong.ddingdongBE.domain.user.entity.User;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;

public record CreateFeedRequest(
@Schema(description = "활동 내용", example = "저희의 활동 내용은 이것입니다.")
@NotNull(message = "activityContent는 null이 될 수 없습니다.")
String activityContent,
@Schema(description = "이미지/비디오 식별자 id", example = "0192c828-ffce-7ee8-94a8-d9d4c8cdec00")
@NotNull(message = "mediaId는 null이 될 수 없습니다.")
String mediaId,
@Schema(description = "컨텐츠 종류", example = "IMAGE")
@NotNull(message = "contentType은 null이 될 수 없습니다.")
String contentType
) {

public CreateFeedCommand toCommand(User user) {
return CreateFeedCommand.builder()
.activityContent(activityContent)
.mediaId(mediaId)
.contentType(contentType)
.user(user)
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,6 @@ public class Feed extends BaseEntity {
@Column(nullable = false)
private String activityContent;

@Column(nullable = false)
private String thumbnailUrl;

@Column(nullable = false)
private String fileUrl;

@Column(nullable = false)
@Enumerated(EnumType.STRING)
private FeedType feedType;
Expand All @@ -50,11 +44,17 @@ public class Feed extends BaseEntity {
private LocalDateTime deletedAt;

@Builder
private Feed(String activityContent, String thumbnailUrl, Club club, FeedType feedType, String fileUrl) {
private Feed(String activityContent, Club club, FeedType feedType) {
this.activityContent = activityContent;
this.thumbnailUrl = thumbnailUrl;
this.club = club;
this.feedType = feedType;
this.fileUrl = fileUrl;
}

public boolean isImage() {
return feedType == FeedType.IMAGE;
}

public boolean isVideo() {
return feedType == FeedType.VIDEO;
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
package ddingdong.ddingdongBE.domain.feed.entity;

import java.util.Arrays;

public enum FeedType {
IMAGE, VIDEO
IMAGE, VIDEO;

public static FeedType findByContentType(String contentType) {
return Arrays.stream(values())
.filter(feedType -> feedType.name().equalsIgnoreCase(contentType))
.findFirst()
.orElseThrow(() -> new IllegalArgumentException("Feed 내 해당 컨텐츠 종류(" + contentType + ")는 지원하지 않습니다."));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package ddingdong.ddingdongBE.domain.feed.service;

import ddingdong.ddingdongBE.domain.feed.service.dto.command.CreateFeedCommand;

public interface FacadeClubFeedService {

void create(CreateFeedCommand command);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package ddingdong.ddingdongBE.domain.feed.service;

import ddingdong.ddingdongBE.domain.club.entity.Club;
import ddingdong.ddingdongBE.domain.club.service.ClubService;
import ddingdong.ddingdongBE.domain.feed.entity.Feed;
import ddingdong.ddingdongBE.domain.feed.service.dto.command.CreateFeedCommand;
import ddingdong.ddingdongBE.domain.filemetadata.entity.DomainType;
import ddingdong.ddingdongBE.domain.filemetadata.service.FileMetaDataService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class FacadeClubFeedServiceImpl implements FacadeClubFeedService{

private final ClubService clubService;
private final FileMetaDataService fileMetaDataService;
private final FeedService feedService;

@Override
@Transactional
public void create(CreateFeedCommand command) {
Club club = clubService.getByUserId(command.user().getId());
Feed feed = command.toEntity(club);
Long createdId = feedService.create(feed);

if (feed.isImage()) {
fileMetaDataService.updateStatusToCoupled(command.mediaId(), DomainType.FEED_IMAGE, createdId);
}

if (feed.isVideo()) {
fileMetaDataService.updateStatusToCoupled(command.mediaId(), DomainType.FEED_VIDEO, createdId);
}
Comment on lines +29 to +35
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

하나의 피드가 동시에 이미지와 비디오로 처리될 가능성에 대한 고려
feed.isImage()feed.isVideo()가 모두 true가 될 수 있는 상황이라면, fileMetaDataService.updateStatusToCoupled()가 두 번 호출될 소지가 있습니다.
실업무 로직상 ‘이미지 또는 비디오 중 하나’만 가능하다면 else if로 제어하여 중복 처리를 회피하는 방안을 고려해 주세요.

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,25 +19,25 @@
@Transactional(readOnly = true)
public class FacadeFeedService {

private final GeneralFeedService generalFeedService;
private final FeedService feedService;
private final FileInformationService fileInformationService;

public List<FeedListQuery> getAllByClubId(Long clubId) {
List<Feed> feeds = generalFeedService.getAllByClubId(clubId);
List<Feed> feeds = feedService.getAllByClubId(clubId);
return feeds.stream()
.map(FeedListQuery::from)
.toList();
}

public List<FeedListQuery> getNewestAll() {
List<Feed> feeds = generalFeedService.getNewestAll();
List<Feed> feeds = feedService.getNewestAll();
return feeds.stream()
.map(FeedListQuery::from)
.toList();
}

public FeedQuery getById(Long feedId) {
Feed feed = generalFeedService.getById(feedId);
Feed feed = feedService.getById(feedId);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

단일 Feed 조회 시 존재하지 않는 자원에 대한 예외 처리
feedService.getById(feedId)의 결과가 없을 때 예외 처리를 어떻게 할지 확인이 필요합니다. 응답 형태(404 Not Found 등)와 예외 메시지를 클라이언트에 일관성 있게 전달하도록 제안드립니다.

ClubProfileQuery clubProfileQuery = extractClubInfo(feed.getClub());
return FeedQuery.of(feed, clubProfileQuery);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package ddingdong.ddingdongBE.domain.feed.service;

import ddingdong.ddingdongBE.domain.feed.entity.Feed;
import java.util.List;

public interface FeedService {

List<Feed> getAllByClubId(Long clubId);

List<Feed> getNewestAll();

Feed getById(Long feedId);

Long create(Feed feed);
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,29 @@
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class GeneralFeedService {
public class GeneralFeedService implements FeedService {

private final FeedRepository feedRepository;

@Override
public List<Feed> getAllByClubId(Long clubId) {
return feedRepository.findAllByClubIdOrderById(clubId);
}

@Override
public List<Feed> getNewestAll() {
return feedRepository.findNewestAll();
}

@Override
public Feed getById(Long feedId) {
return feedRepository.findById(feedId)
.orElseThrow(() -> new ResourceNotFound("Feed(id: " + feedId + ")를 찾을 수 없습니다."));
}

@Override
public Long create(Feed feed) {
KoSeonJe marked this conversation as resolved.
Show resolved Hide resolved
Feed savedFeed = feedRepository.save(feed);
return savedFeed.getId();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package ddingdong.ddingdongBE.domain.feed.service.dto.command;

import ddingdong.ddingdongBE.domain.club.entity.Club;
import ddingdong.ddingdongBE.domain.feed.entity.Feed;
import ddingdong.ddingdongBE.domain.feed.entity.FeedType;
import ddingdong.ddingdongBE.domain.user.entity.User;
import lombok.Builder;

@Builder
public record CreateFeedCommand(
String activityContent,
String mediaId,
String contentType,
User user
) {

public Feed toEntity(Club club) {
return Feed.builder()
.activityContent(activityContent)
.club(club)
.feedType(FeedType.findByContentType(contentType))
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ public record FeedListQuery(
public static FeedListQuery from(Feed feed) {
return FeedListQuery.builder()
.id(feed.getId())
.thumbnailUrl(feed.getThumbnailUrl())
.thumbnailUrl(null)
.feedType(feed.getFeedType().toString())
.build();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public static FeedQuery of(Feed feed, ClubProfileQuery clubProfileQuery) {
.id(feed.getId())
.clubProfileQuery(clubProfileQuery)
.activityContent(feed.getActivityContent())
.fileUrl(feed.getFileUrl())
.fileUrl(null)
.feedType(feed.getFeedType().toString())
.createdDate(LocalDate.from(feed.getCreatedAt()))
.build();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,6 @@ public enum DomainType {
ACTIVITY_REPORT_IMAGE,
BANNER_WEB_IMAGE,
BANNER_MOBILE_IMAGE,
FEED_IMAGE,
FEED_VIDEO
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ public interface FileMetaDataService {

UUID create(FileMetaData fileMetaData);

FileMetaData getById(String id);

List<FileMetaData> getCoupledAllByDomainTypeAndEntityId(DomainType domainType, Long entityId);

List<FileMetaData> getCoupledAllByEntityId(Long entityId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,13 @@ public UUID create(FileMetaData fileMetaData) {
return savedFileMetaData.getId();
}

@Override
public FileMetaData getById(String id) {
UUID uuid = UUID.fromString(id);
return fileMetaDataRepository.findById(uuid)
.orElseThrow(() -> new ResourceNotFound("해당 FileMetaData(id: " + uuid + ")를 찾을 수 없습니다.)"));
}
Comment on lines +35 to +40
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

오탈자 수정이 필요합니다.
해당 예외 메시지에서 불필요한 ) 문자가 포함되어 있습니다. 아래와 같이 수정하여 문자열을 올바르게 닫아야 합니다.

-            .orElseThrow(() -> new ResourceNotFound("해당 FileMetaData(id: " + uuid + ")를 찾을 수 없습니다.)"));
+            .orElseThrow(() -> new ResourceNotFound("해당 FileMetaData(id: " + uuid + ")를 찾을 수 없습니다."));
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@Override
public FileMetaData getById(String id) {
UUID uuid = UUID.fromString(id);
return fileMetaDataRepository.findById(uuid)
.orElseThrow(() -> new ResourceNotFound("해당 FileMetaData(id: " + uuid + ")를 찾을 수 없습니다.)"));
}
@Override
public FileMetaData getById(String id) {
UUID uuid = UUID.fromString(id);
return fileMetaDataRepository.findById(uuid)
.orElseThrow(() -> new ResourceNotFound("해당 FileMetaData(id: " + uuid + ")를 찾을 수 없습니다."));
}


@Override
public List<FileMetaData> getCoupledAllByDomainTypeAndEntityId(DomainType domainType, Long entityId) {
return fileMetaDataRepository.findAllByDomainTypeAndEntityIdWithFileStatus(domainType, entityId, COUPLED);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
ALTER TABLE feed
DROP COLUMN file_url;

ALTER TABLE feed
DROP COLUMN thumbnail_url;
Original file line number Diff line number Diff line change
Expand Up @@ -39,17 +39,17 @@ void findAllByClubIdOrderById() {

Feed feed1 = fixture.giveMeBuilder(Feed.class)
.set("club", savedClub)
.set("thumbnailUrl", "썸네일1")
.set("activityContent", "내용1")
.set("feedType", FeedType.IMAGE)
.sample();
Feed feed2 = fixture.giveMeBuilder(Feed.class)
.set("club", savedClub)
.set("thumbnailUrl", "썸네일2")
.set("activityContent", "내용2")
.set("feedType", FeedType.VIDEO)
.sample();
Feed feed3 = fixture.giveMeBuilder(Feed.class)
.set("club", savedClub)
.set("thumbnailUrl", "썸네일3")
.set("activityContent", "내용3")
.set("feedType", FeedType.IMAGE)
.sample();
feedRepository.save(feed1);
Expand All @@ -60,11 +60,11 @@ void findAllByClubIdOrderById() {
List<Feed> feeds = feedRepository.findAllByClubIdOrderById(savedClub.getId());

// then
Assertions.assertThat(feeds.get(0).getThumbnailUrl()).isEqualTo("썸네일3");
Assertions.assertThat(feeds.get(0).getActivityContent()).isEqualTo("내용3");
Assertions.assertThat(feeds.get(0).getId()).isEqualTo(3L);
Assertions.assertThat(feeds.get(1).getThumbnailUrl()).isEqualTo("썸네일2");
Assertions.assertThat(feeds.get(1).getActivityContent()).isEqualTo("내용2");
Assertions.assertThat(feeds.get(1).getId()).isEqualTo(2L);
Assertions.assertThat(feeds.get(2).getThumbnailUrl()).isEqualTo("썸네일1");
Assertions.assertThat(feeds.get(2).getActivityContent()).isEqualTo("내용1");
Assertions.assertThat(feeds.get(2).getId()).isEqualTo(1L);
}

Expand Down
Loading
Loading