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

[feat/#177-moment-post] 순간포착 게시물 업로드 #183

Merged
merged 7 commits into from
Feb 15, 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
3 changes: 3 additions & 0 deletions src/docs/asciidoc/alarm.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ include::{snippets}/get-alarms/query-parameters.adoc[]
|`TAG_APPROVAL`
|누군가 날 게시물에 태그했을 경우 (수락/거절 후 삭제되는 알람)

|`MOMENT_POST`
|내가 팔로우하고 있는 사람이 순간포착 게시물을 올렸을 경우

|===

include::{snippets}/get-alarms/response-body.adoc[]
Expand Down
6 changes: 6 additions & 0 deletions src/docs/asciidoc/post.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ include::{snippets}/get-search-tab/response-fields.adoc[]

include::{snippets}/upload-post/curl-request.adoc[]

- 순간 포착 게시물 (pinnedHandle 추가)
include::{snippets}/upload-moment-post/curl-request.adoc[]

==== request headers
include::{snippets}/upload-post/request-headers.adoc[]

Expand All @@ -66,6 +69,9 @@ include::{snippets}/upload-post/request-parts.adoc[]
==== query params
include::{snippets}/upload-post/query-parameters.adoc[]

- 순간포착 게시물의 경우, pinnedHandle 을 추가로 전달하면 됩니닷~
include::{snippets}/upload-moment-post/query-parameters.adoc[]

=== Response
==== response body
include::{snippets}/upload-post/response-body.adoc[]
Expand Down
2 changes: 1 addition & 1 deletion src/docs/asciidoc/tag.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ ifndef::snippets[]
:snippets: ./build/generated-snippets
endif::[]

== `GET` Approve Tag API
== `POST` Approve Tag API

게시물 수락 API

Expand Down
4 changes: 3 additions & 1 deletion src/main/java/com/apps/pochak/alarm/domain/AlarmType.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ public enum AlarmType {
FOLLOW("이제 친구를 포착해보세요!", "%s님이 회원님을 팔로우하였습니다.", "%s"),
TAGGED_LIKE("새로운 반응을 확인하세요!", "내가 포착된 게시물에 %s님이 좋아요를 눌렀습니다.", "%s"),
OWNER_LIKE("새로운 반응을 확인하세요!", "내 게시물에 %s님이 좋아요를 눌렀습니다.", "%s"),
TAG_APPROVAL("포착된 사진을 확인해보세요!", "%s님이 회원님을 포착했습니다.", "%s");
TAG_APPROVAL("포착된 사진을 확인해보세요!", "%s님이 회원님을 포착했습니다.", "%s"),
MOMENT_POST("새로운 순간, 포착!", "%s님과 %님이 서로를 순간 포착했습니다.", "%s"),
;

private final String title;
private final String body;
Expand Down
61 changes: 61 additions & 0 deletions src/main/java/com/apps/pochak/alarm/domain/PostAlarm.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package com.apps.pochak.alarm.domain;

import com.apps.pochak.member.domain.Member;
import com.apps.pochak.post.domain.Post;
import jakarta.persistence.Entity;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class PostAlarm extends Alarm {
private Long postId;
private String postImage;

private Long memberId;
private String memberHandle;
private String memberName;
private String memberProfileImage;

public PostAlarm(
final Long id,
final Post post,
final Member receiver,
final AlarmType alarmType
) {
super(id, receiver, alarmType, post.getOwner());
initializeFields(post);
}

public PostAlarm(
final Post post,
final Member receiver,
final AlarmType alarmType
) {
super(receiver, alarmType, post.getOwner());
initializeFields(post);
}

private void initializeFields(final Post post) {
this.postId = post.getId();
this.postImage = post.getPostImage();

Member pinnedMember = post.getPinnedMember();
this.memberId = pinnedMember.getId();
this.memberHandle = pinnedMember.getHandle();
this.memberName = pinnedMember.getName();
this.memberProfileImage = pinnedMember.getProfileImage();
}

@Override
public String getPushNotificationBody() {
return String.format(this.getAlarmType().getBody(), getSender().getName(), memberName);
}

@Override
public String getPushNotificationImage() {
return String.format(this.getAlarmType().getImage(), this.postImage);
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
package com.apps.pochak.alarm.dto.response;

import com.apps.pochak.alarm.domain.*;
import com.apps.pochak.alarm.dto.response.alarm_element.CommentAlarmElement;
import com.apps.pochak.alarm.dto.response.alarm_element.FollowAlarmElement;
import com.apps.pochak.alarm.dto.response.alarm_element.LikeAlarmElement;
import com.apps.pochak.alarm.dto.response.alarm_element.TagApprovalAlarmElement;
import com.apps.pochak.alarm.dto.response.alarm_element.*;
import com.apps.pochak.global.util.PageInfo;
import lombok.AllArgsConstructor;
import lombok.Data;
Expand All @@ -31,6 +28,8 @@ public AlarmElements(final Page<Alarm> alarmPage) {
return new CommentAlarmElement((CommentAlarm) alarm);
} else if (alarm instanceof TagAlarm) {
return new TagApprovalAlarmElement((TagAlarm) alarm);
} else if (alarm instanceof PostAlarm) {
return new PostAlarmElement((PostAlarm) alarm);
} else { // like alarm
return new LikeAlarmElement((LikeAlarm) alarm);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package com.apps.pochak.alarm.dto.response.alarm_element;

import com.apps.pochak.alarm.domain.PostAlarm;
import com.apps.pochak.alarm.dto.response.AlarmElement;
import com.apps.pochak.member.domain.Member;
import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class PostAlarmElement extends AlarmElement {
private Long ownerId;
private String ownerHandle;
private String ownerName;
private String ownerProfileImage;

private Long memberId;
private String memberHandle;
private String memberName;
private String memberProfileImage;

private Long postId;
private String postImage;

public PostAlarmElement(final PostAlarm alarm) {
super(alarm);
Member sender = alarm.getSender();
this.ownerId = sender.getId();
this.ownerHandle = sender.getHandle();
this.ownerName = sender.getName();
this.ownerProfileImage = sender.getProfileImage();

this.memberId = alarm.getMemberId();
this.memberHandle = alarm.getMemberHandle();
this.memberName = alarm.getMemberName();
this.memberProfileImage = alarm.getMemberProfileImage();

this.postId = alarm.getPostId();
this.postImage = alarm.getPostImage();
}
}
45 changes: 45 additions & 0 deletions src/main/java/com/apps/pochak/alarm/service/PostAlarmService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package com.apps.pochak.alarm.service;

import com.apps.pochak.alarm.domain.Alarm;
import com.apps.pochak.alarm.domain.AlarmType;
import com.apps.pochak.alarm.domain.PostAlarm;
import com.apps.pochak.alarm.domain.repository.AlarmRepository;
import com.apps.pochak.fcm.service.FCMService;
import com.apps.pochak.follow.domain.Follow;
import com.apps.pochak.follow.domain.repository.FollowRepository;
import com.apps.pochak.post.domain.Post;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;

import static com.apps.pochak.global.Constant.DEFAULT_DELETION_SIZE;

@Service
@RequiredArgsConstructor
public class PostAlarmService {
private final AlarmRepository alarmRepository;
private final FollowRepository followRepository;
private final FCMService fcmService;

public void saveMomentPostAlarm(final Post post) {
PageRequest pageRequest = PageRequest.of(0, DEFAULT_DELETION_SIZE);
Page<Follow> commonFollowers;
do {
commonFollowers = followRepository.findCommonFollowers(
post.getOwner(),
post.getPinnedMember(),
pageRequest
);
List<PostAlarm> alarmList = commonFollowers.getContent().stream().map(
f -> new PostAlarm(post, f.getSender(), AlarmType.MOMENT_POST)
).toList();
alarmRepository.saveAll(alarmList);
fcmService.sendPushNotification(new ArrayList<Alarm>(alarmList));
pageRequest = pageRequest.next();
} while (commonFollowers.hasNext());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,14 @@
import com.apps.pochak.follow.domain.Follow;
import com.apps.pochak.global.api_payload.exception.GeneralException;
import com.apps.pochak.member.domain.Member;
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.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

import java.util.List;
import java.util.Optional;

import static com.apps.pochak.global.api_payload.code.status.ErrorStatus.NOT_FOLLOW;
Expand All @@ -33,6 +36,20 @@ default Follow findBySenderAndReceiver(final Member sender, final Member receive
return findFollowBySenderAndReceiver(sender, receiver).orElseThrow(() -> new GeneralException(NOT_FOLLOW));
}

@Query("""
select f from Follow f
join fetch f.sender
where f.receiver = :A
and f.sender in (
select f2.sender from Follow f2
where f2.receiver = :B)
""")
Page<Follow> findCommonFollowers(
@Param("A") final Member memberA,
@Param("B") final Member memberB,
final Pageable pageable
);

@Modifying(clearAutomatically = true)
@Query("""
update Follow f
Expand Down
1 change: 1 addition & 0 deletions src/main/java/com/apps/pochak/global/Constant.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ public class Constant {

public static final int DEFAULT_PAGING_SIZE = 30;
public static final int COMMENT_PAGING_SIZE = 10;
public static final int DEFAULT_DELETION_SIZE = 100;
}
20 changes: 20 additions & 0 deletions src/main/java/com/apps/pochak/global/config/AsyncConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.apps.pochak.global.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.AsyncConfigurer;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import java.util.concurrent.Executor;

@EnableAsync
@Configuration
public class AsyncConfig implements AsyncConfigurer {
@Bean
public Executor asyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
return executor;
}
}
8 changes: 7 additions & 1 deletion src/main/java/com/apps/pochak/post/domain/Post.java
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ public class Post extends BaseEntity {
@JoinColumn(name = "owner_id")
private Member owner;

@ManyToOne(fetch = LAZY)
@JoinColumn(name = "pinned_member_id")
private Member pinnedMember;

private String postImage;

private String caption;
Expand All @@ -45,12 +49,14 @@ public class Post extends BaseEntity {
public Post(
final Member owner,
final String postImage,
final String caption
final String caption,
final Member pinnedMember
) {
this.owner = owner;
this.postImage = postImage;
this.caption = caption;
this.postStatus = PRIVATE;
this.pinnedMember = pinnedMember;
}

public boolean isPrivate() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@
import com.apps.pochak.global.api_payload.exception.GeneralException;
import com.apps.pochak.member.domain.Member;
import com.apps.pochak.post.domain.Post;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.AssertTrue;
import jakarta.validation.constraints.Size;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.web.multipart.MultipartFile;

import java.util.ArrayList;
import java.util.List;

import static com.apps.pochak.global.api_payload.code.status.ErrorStatus.TAG_INVALID_MEMBER;
Expand All @@ -27,12 +27,22 @@ public class PostUploadRequest {

private String caption;

@Valid
private String pinnedHandle;

@Size(min = 1, max = 5, message = "유저는 1명 이상, 5명 이하로 태그 가능합니다.")
@NotNull(message = "태그된 유저들의 아이디 리스트는 필수로 전달해야 합니다.")
@ValidDuplicateList
@ValidDuplicateList(message = "유저의 핸들은 중복 전달할 수 없습니다.")
private List<String> taggedMemberHandleList;

public PostUploadRequest(
final MultipartFile postImage,
final String caption,
final List<String> taggedMemberHandleList
) {
this.postImage = postImage;
this.caption = caption;
this.taggedMemberHandleList = taggedMemberHandleList;
}

public Post toEntity(
final String postImage,
final Member owner
Expand All @@ -44,6 +54,32 @@ public Post toEntity(
.build();
}

public Post toEntity(
final String postImage,
final Member owner,
final Member pinnedMember
) {
return Post.builder()
.caption(this.caption)
.postImage(postImage)
.owner(owner)
.pinnedMember(pinnedMember)
.build();
}

@AssertTrue(message = "한 명 이상의 유저를 태그해야 합니다.")
public boolean validateTaggedMember() {
return pinnedHandle != null || (taggedMemberHandleList != null && !taggedMemberHandleList.isEmpty());
}

public List<String> getAllTaggedMember() {
List<String> temp = new ArrayList<>(taggedMemberHandleList);
if (pinnedHandle != null) {
temp.add(pinnedHandle);
}
return temp;
}

public void validateMemberNotTagged(
final Member member
) {
Expand Down
Loading
Loading