Skip to content

Commit 2ba1d64

Browse files
committed
feat: 참가자 일정 제출 및 투표 결과 API 구현
- PATCH /participants/{id}/schedule: 일정 제출 - POST /participants/{id}: 우선순위 설정 - GET /votes/{id}/result: 투표 결과 조회 Closes #6
1 parent 5e5a71e commit 2ba1d64

25 files changed

Lines changed: 1113 additions & 52 deletions

.env

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
DB_PASSWORD=workingdead

build.gradle

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ description = 'Demo project for Spring Boot'
1010

1111
java {
1212
toolchain {
13-
languageVersion = JavaLanguageVersion.of(25)
13+
languageVersion = JavaLanguageVersion.of(21)
1414
}
1515
}
1616

@@ -34,6 +34,9 @@ dependencies {
3434
implementation 'org.flywaydb:flyway-core'
3535
implementation 'org.flywaydb:flyway-database-postgresql'
3636
implementation 'org.springframework.session:spring-session-jdbc'
37+
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
38+
implementation 'org.springframework.boot:spring-boot-starter-data-redis-reactive'
39+
implementation 'org.apache.commons:commons-pool2'
3740
compileOnly 'org.projectlombok:lombok'
3841
runtimeOnly 'com.h2database:h2'
3942
runtimeOnly 'org.postgresql:postgresql'
@@ -48,4 +51,4 @@ dependencies {
4851

4952
tasks.named('test') {
5053
useJUnitPlatform()
51-
}
54+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package com.workingdead.meet.entity;
2+
3+
public enum Period {
4+
LUNCH,
5+
DINNER
6+
}

src/main/java/com/workingdead/meet/controller/ParticipantController.java

Lines changed: 110 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,37 @@
44
import com.workingdead.meet.entity.*;
55
import com.workingdead.meet.repository.*;
66
import com.workingdead.meet.service.*;
7+
import com.workingdead.meet.dto.PriorityDtos.*;
78
import io.swagger.v3.oas.annotations.*;
89
import io.swagger.v3.oas.annotations.media.*;
910
import io.swagger.v3.oas.annotations.responses.*;
1011
import io.swagger.v3.oas.annotations.tags.Tag;
1112
import jakarta.validation.Valid;
13+
import jakarta.servlet.http.HttpSession;
1214
import org.springframework.http.ResponseEntity;
1315
import org.springframework.web.bind.annotation.*;
1416

17+
import java.util.List;
18+
import java.util.Map;
19+
import java.lang.reflect.Field;
20+
import org.springframework.util.ReflectionUtils;
21+
1522
@Tag(name = "Participant", description = "참여자 관리 API")
1623
@RestController
1724
@RequestMapping("")
1825
public class ParticipantController {
1926
private final ParticipantService participantService;
20-
public ParticipantController(ParticipantService participantService) { this.participantService = participantService; }
27+
private final PriorityService priorityService;
28+
private final ParticipantRepository participantRepository;
2129

30+
public ParticipantController(
31+
ParticipantService participantService,
32+
PriorityService priorityService,
33+
ParticipantRepository participantRepository) {
34+
this.participantService = participantService;
35+
this.priorityService = priorityService;
36+
this.participantRepository = participantRepository;
37+
}
2238

2339
// 0.2 참여자 추가/삭제
2440
@Operation(
@@ -32,7 +48,9 @@ public class ParticipantController {
3248
@ApiResponse(responseCode = "404", description = "투표를 찾을 수 없음", content = @Content)
3349
})
3450
@PostMapping("/votes/{voteId}/participants")
35-
public ResponseEntity<ParticipantDtos.ParticipantRes> add(@PathVariable Long voteId, @RequestBody @Valid ParticipantDtos.CreateParticipantReq req) {
51+
public ResponseEntity<ParticipantDtos.ParticipantRes> add(
52+
@PathVariable Long voteId,
53+
@RequestBody @Valid ParticipantDtos.CreateParticipantReq req) {
3654
var res = participantService.add(voteId, req.displayName());
3755
return ResponseEntity.ok(res);
3856
}
@@ -50,4 +68,94 @@ public ResponseEntity<Void> remove(@PathVariable Long participantId) {
5068
participantService.remove(participantId);
5169
return ResponseEntity.noContent().build();
5270
}
71+
72+
@Operation(
73+
summary = "참여자 목록 조회 (로그인 칩)",
74+
description = "어드민이 등록한 참여자 목록을 읽어와 로그인 칩 형태로 제공합니다. " +
75+
"현재 로그인한 참여자는 loggedIn 상태로 표시됩니다."
76+
)
77+
@ApiResponses({
78+
@ApiResponse(responseCode = "200", description = "참여자 목록 조회 성공"),
79+
@ApiResponse(responseCode = "404", description = "투표를 찾을 수 없음")
80+
})
81+
@GetMapping("/votes/{voteId}/participants")
82+
public ResponseEntity<List<ParticipantDtos.ParticipantRes>> getParticipants(
83+
@PathVariable Long voteId,
84+
@RequestParam(required = false) Long currentParticipantId
85+
) {
86+
List<ParticipantDtos.ParticipantRes> participants =
87+
participantService.getParticipantsForVote(voteId, currentParticipantId);
88+
return ResponseEntity.ok(participants);
89+
}
90+
91+
/**
92+
* PATCH /participants/{id}/info
93+
* 참여자 정보 부분 수정 (리플렉션)
94+
*/
95+
@PatchMapping("/participants/{id}/info")
96+
public ResponseEntity<ParticipantDtos.ParticipantRes> updateParticipant(
97+
@PathVariable Long id,
98+
@RequestBody Map<String, Object> updates) {
99+
100+
Participant participant = participantRepository.findById(id)
101+
.orElseThrow(() -> new RuntimeException("Not found"));
102+
103+
updates.forEach((key, value) -> {
104+
Field field = ReflectionUtils.findField(Participant.class, key);
105+
if (field != null) {
106+
field.setAccessible(true);
107+
ReflectionUtils.setField(field, participant, value);
108+
}
109+
});
110+
111+
Participant saved = participantRepository.save(participant);
112+
113+
// DTO로 변환해서 반환!
114+
ParticipantDtos.ParticipantRes response = new ParticipantDtos.ParticipantRes(
115+
saved.getId(),
116+
saved.getDisplayName(),
117+
false // loggedIn 상태
118+
);
119+
120+
return ResponseEntity.ok(response);
53121
}
122+
123+
/**
124+
* POST /participants/{participantId}
125+
* 우선순위 설정 (최대 3개)
126+
*/
127+
@PostMapping("/participants/{participantId}")
128+
public ResponseEntity<PriorityResponse> setPriorities(
129+
@PathVariable Long participantId,
130+
@RequestParam Long voteId,
131+
@Valid @RequestBody PriorityRequest request,
132+
@RequestParam(required = false, defaultValue = "db") String storage,
133+
@RequestParam(required = false, defaultValue = "false") boolean dryRun,
134+
HttpSession session) {
135+
136+
PriorityResponse response = priorityService.setPriorities(
137+
participantId,
138+
voteId,
139+
request,
140+
storage,
141+
dryRun,
142+
session
143+
);
144+
145+
return ResponseEntity.ok(response);
146+
}
147+
148+
/**
149+
* PATCH /participants/{participantId}/schedule
150+
* 일정 제출
151+
*/
152+
@PatchMapping("/participants/{participantId}/schedule")
153+
public ResponseEntity<ParticipantDtos.ParticipantScheduleRes> submitSchedule(
154+
@PathVariable Long participantId,
155+
@Valid @RequestBody ParticipantDtos.SubmitScheduleReq request) {
156+
157+
ParticipantDtos.ParticipantScheduleRes response =
158+
participantService.submitSchedule(participantId, request);
159+
return ResponseEntity.ok(response);
160+
}
161+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// package com.workingdead.meet.controller;
2+
3+
// import com.workingdead.meet.dto.PriorityDtos.*;
4+
// import com.workingdead.meet.service.PriorityService;
5+
// import io.swagger.v3.oas.annotations.Operation;
6+
// import org.springframework.http.ResponseEntity;
7+
// import org.springframework.web.bind.annotation.*;
8+
9+
// import jakarta.servlet.http.HttpSession;
10+
11+
// @RestController
12+
// @RequestMapping("/participants")
13+
// public class PriorityController {
14+
// private final PriorityService priorityService;
15+
16+
// public PriorityController(PriorityService priorityService) {
17+
// this.priorityService = priorityService;
18+
// }
19+
20+
// @Operation(summary = "참여자 우선순위 설정 (dryRun 가능, storage: db/session/redis)")
21+
// @PostMapping("/{participantId}")
22+
// public ResponseEntity<PriorityResponse> setPriorities(
23+
// @PathVariable Long participantId,
24+
// @RequestParam Long voteId,
25+
// @RequestParam(name = "storage", required = false, defaultValue = "db") String storage,
26+
// @RequestParam(name = "dryRun", required = false, defaultValue = "false") boolean dryRun,
27+
// @RequestBody PriorityRequest req,
28+
// HttpSession session
29+
// ) {
30+
// PriorityResponse res = priorityService.setPriorities(participantId, voteId, req, storage, dryRun, session);
31+
// if (dryRun) {
32+
// // 200 OK with diff
33+
// return ResponseEntity.ok(res);
34+
// } else {
35+
// // 200 OK with persisted result
36+
// return ResponseEntity.ok(res);
37+
// }
38+
// }
39+
// }

src/main/java/com/workingdead/meet/controller/VoteController.java

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,15 @@
1717
@RestController
1818
@RequestMapping("/votes")
1919
public class VoteController {
20-
private final VoteService voteService;
21-
public VoteController(VoteService voteService) { this.voteService = voteService; }
20+
private final VoteService voteService;
21+
private final ParticipantRepository participantRepository;
22+
23+
public VoteController(VoteService voteService, ParticipantRepository participantRepository) {
24+
this.voteService = voteService;
25+
this.participantRepository = participantRepository;
26+
}
27+
28+
2229

2330
@Operation(
2431
summary = "투표 목록 조회",
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package com.workingdead.meet.controller;
2+
3+
import com.workingdead.meet.dto.VoteDateRangeDtos.DateSlotDto;
4+
import com.workingdead.meet.service.VoteDateRangeService;
5+
import io.swagger.v3.oas.annotations.Operation;
6+
import io.swagger.v3.oas.annotations.tags.Tag;
7+
import org.springframework.http.ResponseEntity;
8+
import org.springframework.web.bind.annotation.*;
9+
10+
import java.util.List;
11+
12+
@Tag(name = "Vote", description = "투표 관련 API")
13+
@RestController
14+
@RequestMapping("/votes")
15+
public class VoteDateRangeController {
16+
private final VoteDateRangeService voteDateRangeService;
17+
18+
public VoteDateRangeController(VoteDateRangeService voteDateRangeService) {
19+
this.voteDateRangeService = voteDateRangeService;
20+
}
21+
22+
@Operation(summary = "투표 날짜 범위 조회 (날짜별 LUNCH/ DINNER 슬롯 제공)")
23+
@GetMapping("/{voteId}/dateRange")
24+
public ResponseEntity<List<DateSlotDto>> getDateRange(
25+
@PathVariable Long voteId,
26+
@RequestParam(required = false) Long participantId // optional
27+
) {
28+
List<DateSlotDto> slots = voteDateRangeService.getDateRangeSlots(voteId, participantId);
29+
return ResponseEntity.ok(slots);
30+
}
31+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package com.workingdead.meet.controller;
2+
3+
import com.workingdead.meet.dto.VoteResultDtos;
4+
import com.workingdead.meet.service.VoteResultService;
5+
import io.swagger.v3.oas.annotations.Operation;
6+
import lombok.RequiredArgsConstructor;
7+
import org.springframework.http.ResponseEntity;
8+
import org.springframework.web.bind.annotation.*;
9+
10+
@RestController
11+
@RequestMapping("/votes")
12+
@RequiredArgsConstructor
13+
public class VoteResultController {
14+
15+
private final VoteResultService voteResultService;
16+
17+
@Operation(
18+
summary = "투표 결과 조회",
19+
description = "투표 진행 상황을 집계하여 상위 3개 결과를 반환합니다. " +
20+
"정렬 기준: 최다 인원 > 우선순위 가중치 > 빠른 날짜"
21+
)
22+
@GetMapping("/{voteId}/result")
23+
public ResponseEntity<VoteResultDtos.VoteResultRes> getVoteResult(
24+
@PathVariable Long voteId) {
25+
26+
VoteResultDtos.VoteResultRes result = voteResultService.getVoteResult(voteId);
27+
return ResponseEntity.ok(result);
28+
}
29+
}
Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,66 @@
11
package com.workingdead.meet.dto;
22

33
import jakarta.validation.constraints.*;
4+
import java.time.LocalDate;
5+
import java.time.LocalDateTime;
6+
import java.util.List;
47

58

69
public class ParticipantDtos {
710
public record CreateParticipantReq(@NotBlank String displayName) {}
8-
public record ParticipantRes(Long id, String displayName) {}
9-
}
11+
public record ParticipantRes(Long id, String displayName,boolean loggedIn // 로그인 상태
12+
) {}
13+
14+
// 일정 제출 요청
15+
public record SubmitScheduleReq(
16+
@NotEmpty(message = "최소 1개 이상의 날짜를 선택해주세요")
17+
List<DateSlotReq> schedules,
18+
19+
// 우선순위 (선택사항)
20+
List<PriorityReq> priorities
21+
) {}
22+
23+
// 날짜별 슬롯
24+
public record DateSlotReq(
25+
@NotNull LocalDate date,
26+
@NotEmpty List<SlotReq> slots
27+
) {}
28+
29+
// 점심/저녁 선택
30+
public record SlotReq(
31+
@NotNull String period, // "LUNCH" or "DINNER"
32+
boolean selected
33+
) {}
34+
35+
// 우선순위 (선택한 것들 중에서)
36+
public record PriorityReq(
37+
@NotNull LocalDate date,
38+
@NotNull String period,
39+
@Min(1) int priorityIndex, // 1, 2, 3...
40+
double weight // 가중치 (기본값 1.0)
41+
) {}
42+
43+
// 일정 제출 후 상세 응답 (이름 변경!)
44+
public record ParticipantScheduleRes(
45+
Long id,
46+
String displayName,
47+
List<SelectionRes> selections,
48+
List<PriorityRes> priorities,
49+
LocalDateTime submittedAt,
50+
boolean submitted
51+
) {}
52+
53+
public record SelectionRes(
54+
LocalDate date,
55+
String period,
56+
boolean selected
57+
) {}
58+
59+
public record PriorityRes(
60+
LocalDate date,
61+
String period,
62+
int priorityIndex,
63+
double weight
64+
) {}
65+
}
66+

0 commit comments

Comments
 (0)