diff --git a/src/main/java/com/campus/campus/domain/councilpost/application/dto/response/GetLikedPostResponse.java b/src/main/java/com/campus/campus/domain/councilpost/application/dto/response/GetLikedPostResponse.java new file mode 100644 index 00000000..6bf1bd71 --- /dev/null +++ b/src/main/java/com/campus/campus/domain/councilpost/application/dto/response/GetLikedPostResponse.java @@ -0,0 +1,23 @@ +package com.campus.campus.domain.councilpost.application.dto.response; + +import java.time.LocalDateTime; + +import io.swagger.v3.oas.annotations.media.Schema; + +public record GetLikedPostResponse( + @Schema(description = "게시글 id", example = "1") + Long postId, + + @Schema(description = "게시글 이름", example = "투썸 제휴") + String title, + + @Schema(description = "게시글 장소", example = "투썸 플레이스") + String place, + + @Schema(description = "시간(끝나는 시간 or 행사날짜)", example = "2026-01-10T18:00:00") + LocalDateTime dateTime, + + @Schema(description = "썸네일 image url", example = "https://www.example.com.png") + String thumbnailImageUrl +) { +} diff --git a/src/main/java/com/campus/campus/domain/councilpost/application/dto/response/GetPostForUserResponse.java b/src/main/java/com/campus/campus/domain/councilpost/application/dto/response/GetPostForUserResponse.java new file mode 100644 index 00000000..98b39099 --- /dev/null +++ b/src/main/java/com/campus/campus/domain/councilpost/application/dto/response/GetPostForUserResponse.java @@ -0,0 +1,36 @@ +package com.campus.campus.domain.councilpost.application.dto.response; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; + +import com.campus.campus.domain.councilpost.domain.entity.PostCategory; +import com.campus.campus.domain.councilpost.domain.entity.ThumbnailIcon; +import com.fasterxml.jackson.annotation.JsonInclude; + +import lombok.Builder; + +@Builder +@JsonInclude(JsonInclude.Include.NON_NULL) +public record GetPostForUserResponse( + + Long id, + Long writerId, + String writerName, + + PostCategory category, + String title, + String content, + String place, + LocalDate startDate, + LocalDate endDate, + LocalDateTime startDateTime, + + String thumbnailImageUrl, + ThumbnailIcon thumbnailIcon, + + boolean isLiked, + + List images +) { +} diff --git a/src/main/java/com/campus/campus/domain/councilpost/application/dto/response/PostResponse.java b/src/main/java/com/campus/campus/domain/councilpost/application/dto/response/GetPostResponse.java similarity index 95% rename from src/main/java/com/campus/campus/domain/councilpost/application/dto/response/PostResponse.java rename to src/main/java/com/campus/campus/domain/councilpost/application/dto/response/GetPostResponse.java index fac5ace0..1bb451c8 100644 --- a/src/main/java/com/campus/campus/domain/councilpost/application/dto/response/PostResponse.java +++ b/src/main/java/com/campus/campus/domain/councilpost/application/dto/response/GetPostResponse.java @@ -12,7 +12,7 @@ @Builder @JsonInclude(JsonInclude.Include.NON_NULL) -public record PostResponse( +public record GetPostResponse( Long id, Long writerId, diff --git a/src/main/java/com/campus/campus/domain/councilpost/application/dto/response/LikePostResponse.java b/src/main/java/com/campus/campus/domain/councilpost/application/dto/response/LikePostResponse.java new file mode 100644 index 00000000..989e1403 --- /dev/null +++ b/src/main/java/com/campus/campus/domain/councilpost/application/dto/response/LikePostResponse.java @@ -0,0 +1,15 @@ +package com.campus.campus.domain.councilpost.application.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; + +public record LikePostResponse( + @Schema(description = "좋아요 누른 사용자 id", example = "1") + Long userId, + + @Schema(description = "좋아요 누를 게시글의 id", example = "1") + Long postId, + + @Schema(description = "좋아요 여부", example = "true") + boolean liked +) { +} diff --git a/src/main/java/com/campus/campus/domain/councilpost/application/dto/response/PostListItemResponse.java b/src/main/java/com/campus/campus/domain/councilpost/application/dto/response/PostListItemResponse.java index 7cf83e0a..345f1924 100644 --- a/src/main/java/com/campus/campus/domain/councilpost/application/dto/response/PostListItemResponse.java +++ b/src/main/java/com/campus/campus/domain/councilpost/application/dto/response/PostListItemResponse.java @@ -13,6 +13,6 @@ public record PostListItemResponse( LocalDateTime endDateTime, String thumbnailImageUrl, ThumbnailIcon thumbnailIcon, - Boolean isWriter + boolean liked ) { } diff --git a/src/main/java/com/campus/campus/domain/councilpost/application/mapper/StudentCouncilPostMapper.java b/src/main/java/com/campus/campus/domain/councilpost/application/mapper/StudentCouncilPostMapper.java index 895f99ef..d04cc829 100644 --- a/src/main/java/com/campus/campus/domain/councilpost/application/mapper/StudentCouncilPostMapper.java +++ b/src/main/java/com/campus/campus/domain/councilpost/application/mapper/StudentCouncilPostMapper.java @@ -7,33 +7,35 @@ import org.springframework.stereotype.Component; import com.campus.campus.domain.council.domain.entity.StudentCouncil; +import com.campus.campus.domain.councilpost.application.dto.response.GetLikedPostResponse; +import com.campus.campus.domain.councilpost.application.dto.response.GetPostResponse; +import com.campus.campus.domain.councilpost.application.dto.response.LikePostResponse; import com.campus.campus.domain.councilpost.application.dto.response.GetPostListForCouncilResponse; import com.campus.campus.domain.councilpost.application.dto.response.GetUpcomingEventListForCouncilResponse; import com.campus.campus.domain.councilpost.application.dto.response.GetActivePartnershipListForUserResponse; import com.campus.campus.domain.councilpost.application.dto.response.PostListItemResponse; import com.campus.campus.domain.councilpost.application.dto.request.PostRequest; -import com.campus.campus.domain.councilpost.application.dto.response.PostResponse; +import com.campus.campus.domain.councilpost.application.dto.response.GetPostForUserResponse; +import com.campus.campus.domain.councilpost.domain.entity.LikePost; import com.campus.campus.domain.councilpost.domain.entity.PostImage; import com.campus.campus.domain.councilpost.domain.entity.StudentCouncilPost; +import com.campus.campus.domain.user.domain.entity.User; import lombok.RequiredArgsConstructor; @Component @RequiredArgsConstructor public class StudentCouncilPostMapper { - - public PostListItemResponse toPostListItemResponse(StudentCouncilPost post, Long councilId) { + public PostListItemResponse toPostListItemResponse(StudentCouncilPost post, boolean isLiked) { return new PostListItemResponse( post.getId(), post.getCategory(), post.getTitle(), post.getPlace(), - post.isEvent() - ? post.getStartDateTime() - : post.getEndDateTime(), + post.isEvent() ? post.getStartDateTime() : post.getEndDateTime(), post.getThumbnailImageUrl(), post.getThumbnailIcon(), - post.isWrittenByCouncil(councilId) + isLiked ); } @@ -69,19 +71,45 @@ public GetActivePartnershipListForUserResponse toGetActivePartnershipListForUser ); } - public PostResponse toPostResponse(StudentCouncilPost post, List images, Long currentUserId) { + public GetPostResponse toGetPostResponse(StudentCouncilPost post, List images, Long currentCouncilId) { + var writer = post.getWriter(); + var builder = GetPostResponse.builder() + .id(post.getId()) + .writerId(writer.getId()) + .writerName(writer.getCouncilName()) + .isWriter(post.isWrittenByCouncil(currentCouncilId)) + .category(post.getCategory()) + .title(post.getTitle()) + .content(post.getContent()) + .place(post.getPlace()) + .thumbnailImageUrl(post.getThumbnailImageUrl()) + .thumbnailIcon(post.getThumbnailIcon()) + .images(images != null ? images : Collections.emptyList()); + + if (post.isEvent()) { + builder.startDateTime(post.getStartDateTime()); + } else { + builder.startDate(post.getDisplayStartDate()); + builder.endDate(post.getDisplayEndDate()); + } + + return builder.build(); + } + + public GetPostForUserResponse toGetPostForUserResponse(StudentCouncilPost post, List images, + Long currentUserId, boolean isLiked) { var writer = post.getWriter(); - var builder = PostResponse.builder() + var builder = GetPostForUserResponse.builder() .id(post.getId()) .writerId(writer.getId()) .writerName(writer.getCouncilName()) - .isWriter(post.isWrittenByCouncil(currentUserId)) .category(post.getCategory()) .title(post.getTitle()) .content(post.getContent()) .place(post.getPlace()) .thumbnailImageUrl(post.getThumbnailImageUrl()) .thumbnailIcon(post.getThumbnailIcon()) + .isLiked(isLiked) .images(images != null ? images : Collections.emptyList()); if (post.isEvent()) { @@ -94,9 +122,26 @@ public PostResponse toPostResponse(StudentCouncilPost post, List images, return builder.build(); } + public LikePostResponse toLikePostResponse(User user, StudentCouncilPost post, boolean liked) { + return new LikePostResponse( + user.getId(), + post.getId(), + liked + ); + } + + public GetLikedPostResponse toGetLikedPostResponse(StudentCouncilPost post) { + return new GetLikedPostResponse( + post.getId(), + post.getTitle(), + post.getPlace(), + post.isEvent() ? post.getStartDateTime() : post.getEndDateTime(), + post.getThumbnailImageUrl() + ); + } + public StudentCouncilPost createStudentCouncilPost(StudentCouncil writer, PostRequest dto, - LocalDateTime startDateTime, - LocalDateTime endDateTime) { + LocalDateTime startDateTime, LocalDateTime endDateTime) { return StudentCouncilPost.builder() .writer(writer) .category(dto.category()) @@ -116,4 +161,11 @@ public PostImage createPostImage(StudentCouncilPost post, String imageUrl) { .imageUrl(imageUrl) .build(); } + + public LikePost createLikePost(User user, StudentCouncilPost post) { + return LikePost.builder() + .post(post) + .user(user) + .build(); + } } diff --git a/src/main/java/com/campus/campus/domain/councilpost/application/service/StudentCouncilPostForUserService.java b/src/main/java/com/campus/campus/domain/councilpost/application/service/StudentCouncilPostForUserService.java index f5f788bc..9807c406 100644 --- a/src/main/java/com/campus/campus/domain/councilpost/application/service/StudentCouncilPostForUserService.java +++ b/src/main/java/com/campus/campus/domain/councilpost/application/service/StudentCouncilPostForUserService.java @@ -2,8 +2,12 @@ import java.time.LocalDateTime; import java.time.ZoneId; +import java.util.HashSet; import java.util.List; +import java.util.Optional; +import java.util.Set; +import org.springframework.dao.DataIntegrityViolationException; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; @@ -13,15 +17,19 @@ import com.campus.campus.domain.council.domain.entity.CouncilType; import com.campus.campus.domain.councilpost.application.dto.response.GetActivePartnershipListForUserResponse; +import com.campus.campus.domain.councilpost.application.dto.response.GetLikedPostResponse; +import com.campus.campus.domain.councilpost.application.dto.response.LikePostResponse; import com.campus.campus.domain.councilpost.application.dto.response.PostListItemResponse; -import com.campus.campus.domain.councilpost.application.dto.response.PostResponse; +import com.campus.campus.domain.councilpost.application.dto.response.GetPostForUserResponse; import com.campus.campus.domain.councilpost.application.exception.CollegeNotSetException; import com.campus.campus.domain.councilpost.application.exception.MajorNotSetException; import com.campus.campus.domain.councilpost.application.exception.PostNotFoundException; import com.campus.campus.domain.councilpost.application.mapper.StudentCouncilPostMapper; +import com.campus.campus.domain.councilpost.domain.entity.LikePost; import com.campus.campus.domain.councilpost.domain.entity.PostCategory; import com.campus.campus.domain.councilpost.domain.entity.PostImage; import com.campus.campus.domain.councilpost.domain.entity.StudentCouncilPost; +import com.campus.campus.domain.councilpost.domain.repository.LikePostRepository; import com.campus.campus.domain.councilpost.domain.repository.PostImageRepository; import com.campus.campus.domain.councilpost.domain.repository.StudentCouncilPostRepository; import com.campus.campus.domain.councilpost.policy.PostAccessPolicy; @@ -42,10 +50,64 @@ public class StudentCouncilPostForUserService { private static final ZoneId KST = ZoneId.of("Asia/Seoul"); private final StudentCouncilPostRepository studentCouncilPostRepository; private final PostImageRepository postImageRepository; + private final LikePostRepository likePostRepository; private final StudentCouncilPostMapper studentCouncilPostMapper; private final UserRepository userRepository; private final PostAccessPolicy postAccessPolicy; + @Transactional + public LikePostResponse toggleLikePost(Long userId, Long postId) { + User user = userRepository.findByIdAndDeletedAtIsNull(userId) + .orElseThrow(UserNotFoundException::new); + + StudentCouncilPost post = studentCouncilPostRepository.findById(postId) + .orElseThrow(PostNotFoundException::new); + + int deleted = likePostRepository.deleteByUserIdAndPostId(userId, postId); + if (deleted > 0) { + return studentCouncilPostMapper.toLikePostResponse(user, post, false); + } + + try { + likePostRepository.saveAndFlush(studentCouncilPostMapper.createLikePost(user, post)); + return studentCouncilPostMapper.toLikePostResponse(user, post, true); + } catch (DataIntegrityViolationException e) { + return studentCouncilPostMapper.toLikePostResponse(user, post, true); + } + } + + public Page findLikedPosts(PostCategory category, int page, int size, Long userId) { + userRepository.findByIdAndDeletedAtIsNull(userId) + .orElseThrow(UserNotFoundException::new); + + Pageable pageable = PageRequest.of(Math.max(page - 1, 0), size); + + Page likedPosts = likePostRepository.findLikedPosts(userId, category, pageable); + + return likedPosts.map(likePost -> + studentCouncilPostMapper.toGetLikedPostResponse(likePost.getPost()) + ); + } + + public GetPostForUserResponse findById(Long postId, Long userId) { + User user = userRepository.findByIdWithAcademicInfo(userId).orElseThrow(UserNotFoundException::new); + + StudentCouncilPost post = studentCouncilPostRepository.findByIdWithFullInfo(postId) + .orElseThrow(PostNotFoundException::new); + + // 권한 검증 + postAccessPolicy.validateAccess(user, post.getWriter()); + + List imageUrls = postImageRepository.findAllByPostOrderByIdAsc(post) + .stream() + .map(PostImage::getImageUrl) + .toList(); + + boolean isLiked = likePostRepository.existsByUserIdAndPost_Id(userId, postId); + + return studentCouncilPostMapper.toGetPostForUserResponse(post, imageUrls, userId, isLiked); + } + public Page findSchoolPosts(PostCategory category, int page, int size, Long userId) { User user = userRepository.findByIdWithAcademicInfo(userId).orElseThrow(UserNotFoundException::new); @@ -54,7 +116,7 @@ public Page findSchoolPosts(PostCategory category, int pag Page posts = studentCouncilPostRepository .findBySchoolId(user.getSchool().getSchoolId(), category, CouncilType.SCHOOL_COUNCIL, pageable); - return posts.map(post -> studentCouncilPostMapper.toPostListItemResponse(post, userId)); + return mapPostsWithLikes(posts, userId); } public Page findCollegePosts(PostCategory category, int page, int size, Long userId) { @@ -69,7 +131,7 @@ public Page findCollegePosts(PostCategory category, int pa Page posts = studentCouncilPostRepository .findByCollegeId(user.getCollege().getCollegeId(), category, CouncilType.COLLEGE_COUNCIL, pageable); - return posts.map(post -> studentCouncilPostMapper.toPostListItemResponse(post, userId)); + return mapPostsWithLikes(posts, userId); } public Page findMajorPosts(PostCategory category, int page, int size, Long userId) { @@ -80,7 +142,7 @@ public Page findMajorPosts(PostCategory category, int page Page posts = studentCouncilPostRepository .findByMajorId(user.getMajor().getMajorId(), category, CouncilType.MAJOR_COUNCIL, pageable); - return posts.map(post -> studentCouncilPostMapper.toPostListItemResponse(post, userId)); + return mapPostsWithLikes(posts, userId); } public Page findUpcomingSchoolEvents72h(int page, int size, Long userId) { @@ -100,7 +162,7 @@ public Page findUpcomingSchoolEvents72h(int page, int size pageable ); - return posts.map(post -> studentCouncilPostMapper.toPostListItemResponse(post, userId)); + return mapPostsWithLikes(posts, userId); } public Page findUpcomingCollegeEvents72h(int page, int size, Long userId) { @@ -128,7 +190,7 @@ public Page findUpcomingCollegeEvents72h(int page, int siz pageable ); - return posts.map(post -> studentCouncilPostMapper.toPostListItemResponse(post, userId)); + return mapPostsWithLikes(posts, userId); } public Page findUpcomingMajorEvents72h(int page, int size, Long userId) { @@ -156,24 +218,7 @@ public Page findUpcomingMajorEvents72h(int page, int size, pageable ); - return posts.map(post -> studentCouncilPostMapper.toPostListItemResponse(post, userId)); - } - - public PostResponse findById(Long postId, Long userId) { - User user = userRepository.findByIdWithAcademicInfo(userId).orElseThrow(UserNotFoundException::new); - - StudentCouncilPost post = studentCouncilPostRepository.findByIdWithFullInfo(postId) - .orElseThrow(PostNotFoundException::new); - - // 권한 검증 - postAccessPolicy.validateAccess(user, post.getWriter()); - - List imageUrls = postImageRepository.findAllByPostOrderByIdAsc(post) - .stream() - .map(PostImage::getImageUrl) - .toList(); - - return studentCouncilPostMapper.toPostResponse(post, imageUrls, userId); + return mapPostsWithLikes(posts, userId); } public List findActivePartnershipForUser(CouncilType councilType, @@ -213,4 +258,20 @@ public List findActivePartnershipForUse .map(studentCouncilPostMapper::toGetActivePartnershipListForUserResponse) .toList(); } + + private Page mapPostsWithLikes(Page posts, Long userId) { + List postIds = posts.getContent().stream() + .map(StudentCouncilPost::getId) + .toList(); + + if (postIds.isEmpty()) { + return posts.map(post -> studentCouncilPostMapper.toPostListItemResponse(post, false)); + } + + Set likedPostIds = new HashSet<>(likePostRepository.findLikedPostIds(userId, postIds)); + + return posts.map(post -> + studentCouncilPostMapper.toPostListItemResponse(post, likedPostIds.contains(post.getId())) + ); + } } diff --git a/src/main/java/com/campus/campus/domain/councilpost/application/service/StudentCouncilPostService.java b/src/main/java/com/campus/campus/domain/councilpost/application/service/StudentCouncilPostService.java index 446edee7..189d088a 100644 --- a/src/main/java/com/campus/campus/domain/councilpost/application/service/StudentCouncilPostService.java +++ b/src/main/java/com/campus/campus/domain/councilpost/application/service/StudentCouncilPostService.java @@ -12,14 +12,13 @@ import org.springframework.transaction.annotation.Transactional; import com.campus.campus.domain.council.application.exception.StudentCouncilNotFoundException; -import com.campus.campus.domain.council.domain.entity.CouncilType; import com.campus.campus.domain.council.domain.entity.StudentCouncil; import com.campus.campus.domain.council.domain.repository.StudentCouncilRepository; import com.campus.campus.domain.councilpost.application.dto.response.GetPostListForCouncilResponse; +import com.campus.campus.domain.councilpost.application.dto.response.GetPostResponse; import com.campus.campus.domain.councilpost.application.dto.response.GetUpcomingEventListForCouncilResponse; import com.campus.campus.domain.councilpost.application.dto.response.NormalizedDateTime; import com.campus.campus.domain.councilpost.application.dto.request.PostRequest; -import com.campus.campus.domain.councilpost.application.dto.response.PostResponse; import com.campus.campus.domain.councilpost.application.exception.NotPostWriterException; import com.campus.campus.domain.councilpost.application.exception.PostImageLimitExceededException; import com.campus.campus.domain.councilpost.application.exception.PostNotFoundException; @@ -51,7 +50,7 @@ public class StudentCouncilPostService { private static final long UPCOMING_EVENT_WINDOW_HOURS = 72L; @Transactional - public PostResponse create(Long councilId, PostRequest dto) { + public GetPostResponse create(Long councilId, PostRequest dto) { if (dto.imageUrls() != null && dto.imageUrls().size() > MAX_IMAGE_COUNT) { throw new PostImageLimitExceededException(); } @@ -84,11 +83,11 @@ public PostResponse create(Long councilId, PostRequest dto) { .map(PostImage::getImageUrl) .toList(); - return studentCouncilPostMapper.toPostResponse(post, imageUrls, councilId); + return studentCouncilPostMapper.toGetPostResponse(post, imageUrls, councilId); } @Transactional(readOnly = true) - public PostResponse findById(Long postId, Long currentUserId) { + public GetPostResponse findById(Long postId, Long currentUserId) { StudentCouncilPost post = postRepository.findByIdWithFullInfo(postId) .orElseThrow(PostNotFoundException::new); @@ -98,7 +97,7 @@ public PostResponse findById(Long postId, Long currentUserId) { .map(PostImage::getImageUrl) .toList(); - return studentCouncilPostMapper.toPostResponse(post, imageUrls, currentUserId); + return studentCouncilPostMapper.toGetPostResponse(post, imageUrls, currentUserId); } @Transactional(readOnly = true) @@ -179,7 +178,7 @@ public void delete(Long councilId, Long postId) { } @Transactional - public PostResponse update(Long councilId, Long postId, PostRequest dto) { + public GetPostResponse update(Long councilId, Long postId, PostRequest dto) { studentCouncilRepository.findByIdAndManagerApprovedIsTrueAndDeletedAtIsNull(councilId) .orElseThrow(StudentCouncilNotFoundException::new); @@ -231,7 +230,7 @@ public PostResponse update(Long councilId, Long postId, PostRequest dto) { .map(PostImage::getImageUrl) .toList(); - return studentCouncilPostMapper.toPostResponse(post, imageUrls, councilId); + return studentCouncilPostMapper.toGetPostResponse(post, imageUrls, councilId); } //이미지 삭제 diff --git a/src/main/java/com/campus/campus/domain/councilpost/domain/entity/LikePost.java b/src/main/java/com/campus/campus/domain/councilpost/domain/entity/LikePost.java new file mode 100644 index 00000000..4029ae01 --- /dev/null +++ b/src/main/java/com/campus/campus/domain/councilpost/domain/entity/LikePost.java @@ -0,0 +1,45 @@ +package com.campus.campus.domain.councilpost.domain.entity; + +import static jakarta.persistence.FetchType.*; + +import com.campus.campus.domain.user.domain.entity.User; +import com.campus.campus.global.entity.BaseEntity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Table(name = "like_posts", uniqueConstraints = { + @UniqueConstraint(columnNames = {"user_id", "post_id"}) +}) +@Getter +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class LikePost extends BaseEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "like_post_id") + private Long likePostId; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id") + private User user; + + @ManyToOne(fetch = LAZY) + @JoinColumn(name = "post_id") + private StudentCouncilPost post; +} diff --git a/src/main/java/com/campus/campus/domain/councilpost/domain/repository/LikePostRepository.java b/src/main/java/com/campus/campus/domain/councilpost/domain/repository/LikePostRepository.java new file mode 100644 index 00000000..4c2e9c1e --- /dev/null +++ b/src/main/java/com/campus/campus/domain/councilpost/domain/repository/LikePostRepository.java @@ -0,0 +1,44 @@ +package com.campus.campus.domain.councilpost.domain.repository; + +import java.util.List; +import java.util.Optional; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.EntityGraph; +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 com.campus.campus.domain.councilpost.domain.entity.LikePost; +import com.campus.campus.domain.councilpost.domain.entity.PostCategory; + +public interface LikePostRepository extends JpaRepository { + @Modifying(clearAutomatically = true) + @Query(""" + DELETE FROM LikePost lp + WHERE lp.user.id = :userId AND lp.post.id = :postId + """) + int deleteByUserIdAndPostId(@Param("userId") Long userId, @Param("postId") Long postId); + + boolean existsByUserIdAndPost_Id(Long userId, Long postId); + + @EntityGraph(attributePaths = {"post", "post.writer"}) + @Query(""" + SELECT lp FROM LikePost lp + JOIN lp.post p + WHERE lp.user.id = :userId + AND (:category IS NULL OR p.category = :category) + ORDER BY lp.createdAt DESC + """) + Page findLikedPosts(@Param("userId") Long userId, @Param("category") PostCategory category, + Pageable pageable); + + @Query(""" + SELECT lp.post.id FROM LikePost lp + WHERE lp.user.id = :userId + AND lp.post.id IN :postIds + """) + List findLikedPostIds(@Param("userId") Long userId, @Param("postIds") List postIds); +} diff --git a/src/main/java/com/campus/campus/domain/councilpost/presentation/StudentCouncilPostController.java b/src/main/java/com/campus/campus/domain/councilpost/presentation/StudentCouncilPostController.java index 81099218..81ba6472 100644 --- a/src/main/java/com/campus/campus/domain/councilpost/presentation/StudentCouncilPostController.java +++ b/src/main/java/com/campus/campus/domain/councilpost/presentation/StudentCouncilPostController.java @@ -12,11 +12,10 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; -import com.campus.campus.domain.council.domain.entity.CouncilType; import com.campus.campus.domain.councilpost.application.dto.request.PostRequest; import com.campus.campus.domain.councilpost.application.dto.response.GetPostListForCouncilResponse; +import com.campus.campus.domain.councilpost.application.dto.response.GetPostResponse; import com.campus.campus.domain.councilpost.application.dto.response.GetUpcomingEventListForCouncilResponse; -import com.campus.campus.domain.councilpost.application.dto.response.PostResponse; import com.campus.campus.domain.councilpost.application.service.StudentCouncilPostService; import com.campus.campus.domain.councilpost.domain.entity.PostCategory; import com.campus.campus.global.annotation.CurrentCouncilId; @@ -103,22 +102,22 @@ public class StudentCouncilPostController { ) ) ) - public CommonResponse createPost( + public CommonResponse createPost( @CurrentCouncilId Long councilId, @RequestBody @Valid PostRequest requestDto ) { - PostResponse responseDto = postService.create(councilId, requestDto); + GetPostResponse responseDto = postService.create(councilId, requestDto); return CommonResponse.success(StudentCouncilPostResponseCode.POST_CREATE_SUCCESS, responseDto); } @PatchMapping("/{postId}") @Operation(summary = "학생회 게시글 수정") - public CommonResponse updatePost( + public CommonResponse updatePost( @CurrentCouncilId Long councilId, @PathVariable Long postId, @RequestBody @Valid PostRequest requestDto ) { - PostResponse responseDto = postService.update(councilId, postId, requestDto); + GetPostResponse responseDto = postService.update(councilId, postId, requestDto); return CommonResponse.success(StudentCouncilPostResponseCode.POST_UPDATE_SUCCESS, responseDto); } @@ -133,8 +132,8 @@ public CommonResponse deletePost(@CurrentCouncilId Long councilId, @PathVa @GetMapping("/{postId}") @Operation(summary = "학생회 게시글 상세 조회") - public CommonResponse getPost(@PathVariable Long postId, @CurrentCouncilId Long councilId) { - PostResponse responseDto = postService.findById(postId, councilId); + public CommonResponse getPost(@PathVariable Long postId, @CurrentCouncilId Long councilId) { + GetPostResponse responseDto = postService.findById(postId, councilId); return CommonResponse.success(StudentCouncilPostResponseCode.POST_READ_SUCCESS, responseDto); } diff --git a/src/main/java/com/campus/campus/domain/councilpost/presentation/StudentCouncilPostForUserController.java b/src/main/java/com/campus/campus/domain/councilpost/presentation/StudentCouncilPostForUserController.java index 51e43a60..7d6aa049 100644 --- a/src/main/java/com/campus/campus/domain/councilpost/presentation/StudentCouncilPostForUserController.java +++ b/src/main/java/com/campus/campus/domain/councilpost/presentation/StudentCouncilPostForUserController.java @@ -6,14 +6,17 @@ import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import com.campus.campus.domain.council.domain.entity.CouncilType; import com.campus.campus.domain.councilpost.application.dto.response.GetActivePartnershipListForUserResponse; +import com.campus.campus.domain.councilpost.application.dto.response.GetLikedPostResponse; +import com.campus.campus.domain.councilpost.application.dto.response.LikePostResponse; import com.campus.campus.domain.councilpost.application.dto.response.PostListItemResponse; -import com.campus.campus.domain.councilpost.application.dto.response.PostResponse; +import com.campus.campus.domain.councilpost.application.dto.response.GetPostForUserResponse; import com.campus.campus.domain.councilpost.application.service.StudentCouncilPostForUserService; import com.campus.campus.domain.councilpost.domain.entity.PostCategory; import com.campus.campus.global.annotation.CurrentUserId; @@ -32,6 +35,27 @@ @Slf4j public class StudentCouncilPostForUserController { + @PostMapping("/{postId}/like") + @Operation(summary = "학생회 게시글 좋아요 토글") + public CommonResponse togglePostLike(@PathVariable Long postId, @CurrentUserId Long userId) { + LikePostResponse response = postService.toggleLikePost(userId, postId); + + return CommonResponse.success(StudentCouncilPostResponseCode.POST_LIKE_SUCCESS, response); + } + + @GetMapping("/likes") + @Operation(summary = "관심 학생회 게시글 목록 조회") + public CommonResponse> getLikedPosts( + @RequestParam(required = false) PostCategory category, + @RequestParam(defaultValue = "1") int page, + @RequestParam(defaultValue = "3") int size, + @CurrentUserId Long userId + ) { + Page responses = postService.findLikedPosts(category, page, size, userId); + + return CommonResponse.success(StudentCouncilPostResponseCode.POST_LIST_READ_SUCCESS, responses); + } + private final StudentCouncilPostForUserService postService; @GetMapping("/school") @@ -84,11 +108,11 @@ public CommonResponse> getMajorPosts( @GetMapping("/{postId}") @Operation(summary = "학생회 게시글 상세 조회") - public CommonResponse getPost( + public CommonResponse getPost( @PathVariable Long postId, @CurrentUserId Long userId ) { - PostResponse responseDto = postService.findById(postId, userId); + GetPostForUserResponse responseDto = postService.findById(postId, userId); return CommonResponse.success(StudentCouncilPostResponseCode.POST_READ_SUCCESS, responseDto); } diff --git a/src/main/java/com/campus/campus/domain/councilpost/presentation/StudentCouncilPostResponseCode.java b/src/main/java/com/campus/campus/domain/councilpost/presentation/StudentCouncilPostResponseCode.java index e65bc547..6e56d07e 100644 --- a/src/main/java/com/campus/campus/domain/councilpost/presentation/StudentCouncilPostResponseCode.java +++ b/src/main/java/com/campus/campus/domain/councilpost/presentation/StudentCouncilPostResponseCode.java @@ -16,10 +16,10 @@ public enum StudentCouncilPostResponseCode implements ResponseCodeInterface { POST_LIST_READ_SUCCESS(200, HttpStatus.OK, "게시글 목록 조회에 성공했습니다."), POST_UPDATE_SUCCESS(200, HttpStatus.OK, "게시글 수정에 성공했습니다."), POST_DELETE_SUCCESS(200, HttpStatus.OK, "게시글 삭제에 성공했습니다."), - UPCOMING_SCHOOL_EVENT_LIST_READ_SUCCESS(200, HttpStatus.OK, "학교 72시간 이내 행사 조회에 성공했습니다."), UPCOMING_COLLEGE_EVENT_LIST_READ_SUCCESS(200, HttpStatus.OK, "단과대 72시간 이내 행사 조회에 성공했습니다."), - UPCOMING_MAJOR_EVENT_LIST_READ_SUCCESS(200, HttpStatus.OK, "학과 72시간 이내 행사 조회에 성공했습니다."); + UPCOMING_MAJOR_EVENT_LIST_READ_SUCCESS(200, HttpStatus.OK, "학과 72시간 이내 행사 조회에 성공했습니다."), + POST_LIKE_SUCCESS(200, HttpStatus.OK, "게시글 좋아요 처리에 성공했습니다."); private final int code; private final HttpStatus status;