Skip to content

Commit fad9d9b

Browse files
committed
Merge remote-tracking branch 'origin/dev' into feature/kakao-api
2 parents 5f33821 + 4f60205 commit fad9d9b

12 files changed

Lines changed: 771 additions & 0 deletions

File tree

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package com.workingdead.meet.controller;
2+
3+
import com.workingdead.meet.dto.request.TimePollCreateRequest;
4+
import com.workingdead.meet.dto.request.TimePollSubmitRequest;
5+
import com.workingdead.meet.dto.response.TimePollResponse;
6+
import com.workingdead.meet.dto.response.TimePollStatusResponse;
7+
import com.workingdead.meet.dto.response.TimePollStatusResponse;
8+
import com.workingdead.meet.entity.TimePoll;
9+
import com.workingdead.meet.service.TimePollService;
10+
import lombok.RequiredArgsConstructor;
11+
import org.springframework.http.ResponseEntity;
12+
import org.springframework.web.bind.annotation.*;
13+
14+
import java.util.Map;
15+
16+
@RestController
17+
@RequestMapping("/time-polls")
18+
@RequiredArgsConstructor
19+
public class TimePollController {
20+
21+
private final TimePollService timePollService;
22+
23+
/**
24+
* API #1 - 시간 투표 생성
25+
* POST /time-polls
26+
*/
27+
@PostMapping
28+
public ResponseEntity<Map<String, Long>> create(@RequestBody TimePollCreateRequest request) {
29+
TimePoll timePoll = timePollService.create(request);
30+
return ResponseEntity.ok(Map.of("timePollId", timePoll.getId()));
31+
}
32+
33+
/**
34+
* API #2 - 투표 페이지 조회
35+
* GET /time-polls/{pollId}?participantId=123
36+
*/
37+
@GetMapping("/{pollId}")
38+
public ResponseEntity<TimePollResponse> getTimePoll(
39+
@PathVariable Long pollId,
40+
@RequestParam Long participantId) {
41+
return ResponseEntity.ok(timePollService.getTimePoll(pollId, participantId));
42+
}
43+
44+
/**
45+
* API #3 - 투표 제출
46+
* POST /time-polls/{pollId}/submit
47+
*/
48+
@PostMapping("/{pollId}/submit")
49+
public ResponseEntity<TimePollStatusResponse> submit(
50+
@PathVariable Long pollId,
51+
@RequestBody TimePollSubmitRequest request) {
52+
return ResponseEntity.ok(timePollService.submit(pollId, request));
53+
}
54+
55+
/**
56+
* API #4 - 투표 현황 조회
57+
* GET /time-polls/{pollId}/status
58+
*/
59+
@GetMapping("/{pollId}/status")
60+
public ResponseEntity<TimePollStatusResponse> getStatus(@PathVariable Long pollId) {
61+
return ResponseEntity.ok(timePollService.getStatus(pollId));
62+
}
63+
64+
/**
65+
* API #5 - "저도 그때 좋아요" 수락
66+
* POST /time-polls/{pollId}/accept
67+
*/
68+
@PostMapping("/{pollId}/accept")
69+
public ResponseEntity<TimePollStatusResponse> accept(
70+
@PathVariable Long pollId,
71+
@RequestParam Long participantId) {
72+
return ResponseEntity.ok(timePollService.accept(pollId, participantId));
73+
}
74+
75+
/**
76+
* API #6 - 투표 확정 (내부용이지만 수동 트리거 가능)
77+
* POST time-polls/{pollId}/finalize
78+
*/
79+
@PostMapping("/{pollId}/finalize")
80+
public ResponseEntity<Void> finalize(@PathVariable Long pollId) {
81+
timePollService.finalize(pollId);
82+
return ResponseEntity.ok().build();
83+
}
84+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package com.workingdead.meet.dto.request;
2+
3+
import lombok.Getter;
4+
import lombok.NoArgsConstructor;
5+
6+
@Getter
7+
@NoArgsConstructor
8+
public class TimePollCreateRequest {
9+
10+
private Long voteId; // 어떤 날짜 투표에서 파생됐는지
11+
private String confirmedDate; // 확정된 날짜 ("1월 28일 저녁")
12+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package com.workingdead.meet.dto.request;
2+
3+
import lombok.Getter;
4+
import lombok.NoArgsConstructor;
5+
6+
import java.time.LocalTime;
7+
8+
@Getter
9+
@NoArgsConstructor
10+
public class TimePollSubmitRequest {
11+
12+
private Long participantId;
13+
private LocalTime selectedTime; // "18:00", "16:30"
14+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package com.workingdead.meet.dto.response;
2+
3+
import com.workingdead.meet.entity.TimePollStatus;
4+
import lombok.AllArgsConstructor;
5+
import lombok.Builder;
6+
import lombok.Getter;
7+
8+
import java.time.LocalTime;
9+
10+
@Getter
11+
@Builder
12+
@AllArgsConstructor
13+
public class TimePollResponse {
14+
15+
private Long id;
16+
private String confirmedDate;
17+
private TimePollStatus status;
18+
private LocalTime finalizedTime;
19+
private int totalParticipants;
20+
private int submittedCount;
21+
private boolean alreadySubmitted;
22+
private LocalTime mySelection;
23+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package com.workingdead.meet.dto.response;
2+
3+
import com.workingdead.meet.entity.TimePollStatus;
4+
import lombok.AllArgsConstructor;
5+
import lombok.Builder;
6+
import lombok.Getter;
7+
8+
import java.time.LocalTime;
9+
import java.util.List;
10+
11+
@Getter
12+
@Builder
13+
@AllArgsConstructor
14+
public class TimePollStatusResponse {
15+
16+
private Long timePollId;
17+
private String confirmedDate;
18+
private TimePollStatus status;
19+
private LocalTime finalizedTime;
20+
private int totalParticipants;
21+
private int submittedCount;
22+
private boolean allSubmitted;
23+
private List<EntryDto> entries; // 투표 현황 (늦은 시간순)
24+
private List<String> pendingNames; // 미투표자 이름 목록
25+
26+
@Getter
27+
@Builder
28+
@AllArgsConstructor
29+
public static class EntryDto {
30+
private String displayName;
31+
private LocalTime selectedTime;
32+
}
33+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package com.workingdead.meet.entity;
2+
3+
import jakarta.persistence.*;
4+
import lombok.*;
5+
6+
import java.time.Instant;
7+
import java.time.LocalTime;
8+
import java.util.ArrayList;
9+
import java.util.List;
10+
11+
@Getter
12+
@Setter
13+
@NoArgsConstructor
14+
@AllArgsConstructor
15+
@Builder
16+
@Entity
17+
@Table(name = "time_poll")
18+
public class TimePoll {
19+
20+
@Id
21+
@GeneratedValue(strategy = GenerationType.IDENTITY)
22+
private Long id;
23+
24+
// 어떤 Vote(날짜 투표)에서 파생됐는지
25+
@ManyToOne(fetch = FetchType.LAZY)
26+
@JoinColumn(name = "vote_id", nullable = false)
27+
private Vote vote;
28+
29+
// 확정된 날짜 (날짜 투표 결과)
30+
@Column(name = "confirmed_date", nullable = false)
31+
private String confirmedDate; // "1월 28일 저녁" 같은 형태
32+
33+
// 투표 상태
34+
@Enumerated(EnumType.STRING)
35+
@Column(nullable = false)
36+
@Builder.Default
37+
private TimePollStatus status = TimePollStatus.ONGOING;
38+
39+
// 최종 확정된 시간 (투표 완료 후)
40+
@Column(name = "finalized_time")
41+
private LocalTime finalizedTime;
42+
43+
// 최후통첩 발송 시각
44+
@Column(name = "ultimatum_sent_at")
45+
private Instant ultimatumSentAt;
46+
47+
// 마지막 독촉 단계 (0=안함, 1=30분, 2=2시간, 3=6시간, 4=12시간)
48+
@Column(name = "last_reminder_step")
49+
@Builder.Default
50+
private Integer lastReminderStep = 0;
51+
52+
@Column(name = "created_at", nullable = false, updatable = false)
53+
@Builder.Default
54+
private Instant createdAt = Instant.now();
55+
56+
// 개별 투표 응답들
57+
@OneToMany(mappedBy = "timePoll", cascade = CascadeType.ALL, orphanRemoval = true)
58+
@Builder.Default
59+
private List<TimePollEntry> entries = new ArrayList<>();
60+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package com.workingdead.meet.entity;
2+
3+
import jakarta.persistence.*;
4+
import lombok.*;
5+
6+
import java.time.Instant;
7+
import java.time.LocalTime;
8+
9+
@Getter
10+
@Setter
11+
@NoArgsConstructor
12+
@AllArgsConstructor
13+
@Builder
14+
@Entity
15+
@Table(name = "time_poll_entry",
16+
uniqueConstraints = {
17+
@UniqueConstraint(columnNames = {"time_poll_id", "participant_id"})
18+
})
19+
public class TimePollEntry {
20+
21+
@Id
22+
@GeneratedValue(strategy = GenerationType.IDENTITY)
23+
private Long id;
24+
25+
@ManyToOne(fetch = FetchType.LAZY)
26+
@JoinColumn(name = "time_poll_id", nullable = false)
27+
private TimePoll timePoll;
28+
29+
@ManyToOne(fetch = FetchType.LAZY)
30+
@JoinColumn(name = "participant_id", nullable = false)
31+
private Participant participant;
32+
33+
// 선택한 시간 (예: 18:00, 16:30)
34+
@Column(name = "selected_time", nullable = false)
35+
private LocalTime selectedTime;
36+
37+
@Column(name = "submitted_at", nullable = false, updatable = false)
38+
@Builder.Default
39+
private Instant submittedAt = Instant.now();
40+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package com.workingdead.meet.entity;
2+
3+
public enum TimePollStatus {
4+
ONGOING,
5+
ULTIMATUM,
6+
FINALIZED
7+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package com.workingdead.meet.repository;
2+
3+
import com.workingdead.meet.entity.TimePollEntry;
4+
import org.springframework.data.jpa.repository.JpaRepository;
5+
6+
import java.util.List;
7+
import java.util.Optional;
8+
9+
public interface TimePollEntryRepository extends JpaRepository<TimePollEntry, Long> {
10+
11+
// 특정 투표의 모든 응답 조회
12+
List<TimePollEntry> findByTimePollId(Long timePollId);
13+
14+
// 특정 투표 + 특정 참여자의 응답 조회 (중복 제출 체크 / 수정용)
15+
Optional<TimePollEntry> findByTimePollIdAndParticipantId(Long timePollId, Long participantId);
16+
17+
// 특정 투표의 응답 수 (과반 체크용)
18+
long countByTimePollId(Long timePollId);
19+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package com.workingdead.meet.repository;
2+
3+
import com.workingdead.meet.entity.TimePoll;
4+
import com.workingdead.meet.entity.TimePollStatus;
5+
import org.springframework.data.jpa.repository.JpaRepository;
6+
import org.springframework.data.jpa.repository.Query;
7+
import org.springframework.data.repository.query.Param;
8+
9+
import java.time.Instant;
10+
import java.util.List;
11+
import java.util.Optional;
12+
13+
public interface TimePollRepository extends JpaRepository<TimePoll, Long> {
14+
15+
// Vote ID로 시간 투표 조회
16+
Optional<TimePoll> findByVoteId(Long voteId);
17+
18+
// 진행 중인 투표 중 특정 시각 이전에 생성된 것들 (스케줄러용)
19+
List<TimePoll> findByStatusAndCreatedAtBefore(TimePollStatus status, Instant before);
20+
21+
// 최후통첩 상태에서 특정 시각 이전에 발송된 것들 (60분 타임아웃 체크용)
22+
List<TimePoll> findByStatusAndUltimatumSentAtBefore(TimePollStatus status, Instant before);
23+
24+
// 독촉 대상 조회: 진행 중 + 특정 독촉 단계 미만
25+
@Query("SELECT tp FROM TimePoll tp WHERE tp.status = :status AND tp.lastReminderStep < :step AND tp.createdAt < :before")
26+
List<TimePoll> findRemindTargets(@Param("status") TimePollStatus status,
27+
@Param("step") int step,
28+
@Param("before") Instant before);
29+
}

0 commit comments

Comments
 (0)