diff --git a/backend/src/main/java/com/openisle/controller/PostController.java b/backend/src/main/java/com/openisle/controller/PostController.java index aa1707ff1..90c892834 100644 --- a/backend/src/main/java/com/openisle/controller/PostController.java +++ b/backend/src/main/java/com/openisle/controller/PostController.java @@ -3,6 +3,7 @@ package com.openisle.controller; import com.openisle.dto.PostDetailDto; import com.openisle.dto.PostRequest; import com.openisle.dto.PostSummaryDto; +import com.openisle.dto.PollDto; import com.openisle.mapper.PostMapper; import com.openisle.model.Post; import com.openisle.service.*; @@ -42,7 +43,8 @@ public class PostController { req.getTitle(), req.getContent(), req.getTagIds(), req.getType(), req.getPrizeDescription(), req.getPrizeIcon(), req.getPrizeCount(), req.getPointCost(), - req.getStartTime(), req.getEndTime()); + req.getStartTime(), req.getEndTime(), + req.getQuestion(), req.getOptions()); draftService.deleteDraft(auth.getName()); PostDetailDto dto = postMapper.toDetailDto(post, auth.getName()); dto.setReward(levelService.awardForPost(auth.getName())); @@ -86,6 +88,17 @@ public class PostController { return ResponseEntity.ok().build(); } + @GetMapping("/{id}/poll/progress") + public ResponseEntity pollProgress(@PathVariable Long id) { + return ResponseEntity.ok(postMapper.toSummaryDto(postService.getPoll(id)).getPoll()); + } + + @PostMapping("/{id}/poll/vote") + public ResponseEntity vote(@PathVariable Long id, @RequestParam("option") int option, Authentication auth) { + postService.votePoll(id, auth.getName(), option); + return ResponseEntity.ok().build(); + } + @GetMapping public List listPosts(@RequestParam(value = "categoryId", required = false) Long categoryId, @RequestParam(value = "categoryIds", required = false) List categoryIds, diff --git a/backend/src/main/java/com/openisle/dto/PollDto.java b/backend/src/main/java/com/openisle/dto/PollDto.java new file mode 100644 index 000000000..3612d60ed --- /dev/null +++ b/backend/src/main/java/com/openisle/dto/PollDto.java @@ -0,0 +1,16 @@ +package com.openisle.dto; + +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; + +@Data +public class PollDto { + private String question; + private List options; + private Map votes; + private LocalDateTime endTime; + private List participants; +} diff --git a/backend/src/main/java/com/openisle/dto/PostRequest.java b/backend/src/main/java/com/openisle/dto/PostRequest.java index 1fe02669a..7e6619e2a 100644 --- a/backend/src/main/java/com/openisle/dto/PostRequest.java +++ b/backend/src/main/java/com/openisle/dto/PostRequest.java @@ -26,5 +26,8 @@ public class PostRequest { private Integer pointCost; private LocalDateTime startTime; private LocalDateTime endTime; + // fields for poll posts + private String question; + private List options; } diff --git a/backend/src/main/java/com/openisle/dto/PostSummaryDto.java b/backend/src/main/java/com/openisle/dto/PostSummaryDto.java index 665c590f7..48205bc0e 100644 --- a/backend/src/main/java/com/openisle/dto/PostSummaryDto.java +++ b/backend/src/main/java/com/openisle/dto/PostSummaryDto.java @@ -31,6 +31,7 @@ public class PostSummaryDto { private int pointReward; private PostType type; private LotteryDto lottery; + private PollDto poll; private boolean rssExcluded; private boolean closed; } diff --git a/backend/src/main/java/com/openisle/mapper/PostMapper.java b/backend/src/main/java/com/openisle/mapper/PostMapper.java index ad1a826da..5577dcdb8 100644 --- a/backend/src/main/java/com/openisle/mapper/PostMapper.java +++ b/backend/src/main/java/com/openisle/mapper/PostMapper.java @@ -5,9 +5,11 @@ import com.openisle.dto.PostDetailDto; import com.openisle.dto.PostSummaryDto; import com.openisle.dto.ReactionDto; import com.openisle.dto.LotteryDto; +import com.openisle.dto.PollDto; import com.openisle.model.CommentSort; import com.openisle.model.Post; import com.openisle.model.LotteryPost; +import com.openisle.model.PollPost; import com.openisle.model.User; import com.openisle.service.CommentService; import com.openisle.service.ReactionService; @@ -93,5 +95,15 @@ public class PostMapper { l.setWinners(lp.getWinners().stream().map(userMapper::toAuthorDto).collect(Collectors.toList())); dto.setLottery(l); } + + if (post instanceof PollPost pp) { + PollDto p = new PollDto(); + p.setQuestion(pp.getQuestion()); + p.setOptions(pp.getOptions()); + p.setVotes(pp.getVotes()); + p.setEndTime(pp.getEndTime()); + p.setParticipants(pp.getParticipants().stream().map(userMapper::toAuthorDto).collect(Collectors.toList())); + dto.setPoll(p); + } } } diff --git a/backend/src/main/java/com/openisle/model/PollPost.java b/backend/src/main/java/com/openisle/model/PollPost.java new file mode 100644 index 000000000..c19978c21 --- /dev/null +++ b/backend/src/main/java/com/openisle/model/PollPost.java @@ -0,0 +1,41 @@ +package com.openisle.model; + +import jakarta.persistence.*; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.time.LocalDateTime; +import java.util.*; + +@Entity +@Table(name = "poll_posts") +@Getter +@Setter +@NoArgsConstructor +@PrimaryKeyJoinColumn(name = "post_id") +public class PollPost extends Post { + + @Column(nullable = false) + private String question; + + @ElementCollection + @CollectionTable(name = "poll_post_options", joinColumns = @JoinColumn(name = "post_id")) + @Column(name = "option_text") + private List options = new ArrayList<>(); + + @ElementCollection + @CollectionTable(name = "poll_post_votes", joinColumns = @JoinColumn(name = "post_id")) + @MapKeyColumn(name = "option_index") + @Column(name = "vote_count") + private Map votes = new HashMap<>(); + + @ManyToMany + @JoinTable(name = "poll_participants", + joinColumns = @JoinColumn(name = "post_id"), + inverseJoinColumns = @JoinColumn(name = "user_id")) + private Set participants = new HashSet<>(); + + @Column + private LocalDateTime endTime; +} diff --git a/backend/src/main/java/com/openisle/model/PostType.java b/backend/src/main/java/com/openisle/model/PostType.java index d14a28701..b8becaca0 100644 --- a/backend/src/main/java/com/openisle/model/PostType.java +++ b/backend/src/main/java/com/openisle/model/PostType.java @@ -2,5 +2,6 @@ package com.openisle.model; public enum PostType { NORMAL, - LOTTERY + LOTTERY, + POLL } diff --git a/backend/src/main/java/com/openisle/repository/PollPostRepository.java b/backend/src/main/java/com/openisle/repository/PollPostRepository.java new file mode 100644 index 000000000..d0985fe4b --- /dev/null +++ b/backend/src/main/java/com/openisle/repository/PollPostRepository.java @@ -0,0 +1,7 @@ +package com.openisle.repository; + +import com.openisle.model.PollPost; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface PollPostRepository extends JpaRepository { +} diff --git a/backend/src/main/java/com/openisle/service/PostService.java b/backend/src/main/java/com/openisle/service/PostService.java index 993c19c20..296133b4c 100644 --- a/backend/src/main/java/com/openisle/service/PostService.java +++ b/backend/src/main/java/com/openisle/service/PostService.java @@ -9,8 +9,10 @@ import com.openisle.model.Category; import com.openisle.model.Comment; import com.openisle.model.NotificationType; import com.openisle.model.LotteryPost; +import com.openisle.model.PollPost; import com.openisle.repository.PostRepository; import com.openisle.repository.LotteryPostRepository; +import com.openisle.repository.PollPostRepository; import com.openisle.repository.UserRepository; import com.openisle.repository.CategoryRepository; import com.openisle.repository.TagRepository; @@ -54,6 +56,7 @@ public class PostService { private final CategoryRepository categoryRepository; private final TagRepository tagRepository; private final LotteryPostRepository lotteryPostRepository; + private final PollPostRepository pollPostRepository; private PublishMode publishMode; private final NotificationService notificationService; private final SubscriptionService subscriptionService; @@ -78,6 +81,7 @@ public class PostService { CategoryRepository categoryRepository, TagRepository tagRepository, LotteryPostRepository lotteryPostRepository, + PollPostRepository pollPostRepository, NotificationService notificationService, SubscriptionService subscriptionService, CommentService commentService, @@ -97,6 +101,7 @@ public class PostService { this.categoryRepository = categoryRepository; this.tagRepository = tagRepository; this.lotteryPostRepository = lotteryPostRepository; + this.pollPostRepository = pollPostRepository; this.notificationService = notificationService; this.subscriptionService = subscriptionService; this.commentService = commentService; @@ -166,7 +171,9 @@ public class PostService { Integer prizeCount, Integer pointCost, LocalDateTime startTime, - LocalDateTime endTime) { + LocalDateTime endTime, + String question, + java.util.List options) { long recent = postRepository.countByAuthorAfter(username, java.time.LocalDateTime.now().minusMinutes(5)); if (recent >= 1) { @@ -200,6 +207,15 @@ public class PostService { lp.setStartTime(startTime); lp.setEndTime(endTime); post = lp; + } else if (actualType == PostType.POLL) { + if (options == null || options.size() < 2) { + throw new IllegalArgumentException("At least two options required"); + } + PollPost pp = new PollPost(); + pp.setQuestion(question); + pp.setOptions(options); + pp.setEndTime(endTime); + post = pp; } else { post = new Post(); } @@ -212,6 +228,8 @@ public class PostService { post.setStatus(publishMode == PublishMode.REVIEW ? PostStatus.PENDING : PostStatus.PUBLISHED); if (post instanceof LotteryPost) { post = lotteryPostRepository.save((LotteryPost) post); + } else if (post instanceof PollPost) { + post = pollPostRepository.save((PollPost) post); } else { post = postRepository.save(post); } @@ -261,6 +279,31 @@ public class PostService { } } + public PollPost getPoll(Long postId) { + return pollPostRepository.findById(postId) + .orElseThrow(() -> new com.openisle.exception.NotFoundException("Post not found")); + } + + @Transactional + public PollPost votePoll(Long postId, String username, int optionIndex) { + PollPost post = pollPostRepository.findById(postId) + .orElseThrow(() -> new com.openisle.exception.NotFoundException("Post not found")); + if (post.getEndTime() != null && post.getEndTime().isBefore(LocalDateTime.now())) { + throw new IllegalStateException("Poll has ended"); + } + User user = userRepository.findByUsername(username) + .orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found")); + if (post.getParticipants().contains(user)) { + throw new IllegalArgumentException("User already voted"); + } + if (optionIndex < 0 || optionIndex >= post.getOptions().size()) { + throw new IllegalArgumentException("Invalid option"); + } + post.getParticipants().add(user); + post.getVotes().merge(optionIndex, 1, Integer::sum); + return pollPostRepository.save(post); + } + @Transactional public void finalizeLottery(Long postId) { log.info("start to finalizeLottery for {}", postId);