Skip to content

Commit 6c9934c

Browse files
authored
Merge pull request #7 from DevKor-github/feature/schedule-and-result
Feature/schedule and result
2 parents 90f8545 + 2ba1d64 commit 6c9934c

32 files changed

Lines changed: 1679 additions & 12 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: 8 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,15 +34,21 @@ 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'
4043
annotationProcessor 'org.projectlombok:lombok'
4144
testImplementation 'org.springframework.boot:spring-boot-starter-test'
4245
testImplementation 'org.springframework.security:spring-security-test'
4346
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
47+
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.8'
48+
implementation "org.springframework.session:spring-session-jdbc"
49+
4450
}
4551

4652
tasks.named('test') {
4753
useJUnitPlatform()
48-
}
54+
}

docs.txt

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# Notes: Spec ↔ Implementation 매핑
2+
#
3+
# page#0 관리자 페이지
4+
# 0.1 홈화면
5+
# GET /votes : 목록 (id, name, code, adminUrl, shareUrl)
6+
투표 블록 터치 → GET /votes/{id} 로 상세 조회
7+
# POST /votes : 새 투표 생성 (res.shareUrl 로 링크 복사)
8+
날짜 2개 선택 → backend에는 startDate,endDate로 저장 (두 날짜 포함 범위)
9+
프론트에서 3번째 날짜 선택 시 프론트 로직으로 2개만 유지 → 서버엔 항상 (start,end)만 전달
10+
# DELETE /votes/{id} : 투표 삭제
11+
# PATCH /votes/{id} : 이름/날짜 범위 수정 (둘 다 vote attr)
12+
13+
#
14+
# 0.2 투표 설정 화면
15+
# 참여자 칩 생성 섹션: POST /votes/{id}/participants {displayName}
16+
생성된 응답에 loginCode 포함(칩/매직링크 구현용)
17+
# 참여자 삭제: DELETE /participants/{participantId}
18+
#
19+
# 기타
20+
# Vote 삭제 시 @OneToMany(cascade=ALL, orphanRemoval=true)로 참가자 함께 삭제
21+
# 코드/링크: app.base-url + "/v/" + code (공유 링크), "/admin/votes/{id}" (관리 URL)
22+
# Validation: endDate >= startDate. name not blank.
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package com.workingdead.config;
2+
3+
import org.springframework.context.annotation.Bean;
4+
import org.springframework.context.annotation.Configuration;
5+
import org.springframework.web.cors.*;
6+
7+
import java.util.List;
8+
9+
@Configuration
10+
public class CorsConfig {
11+
@Bean
12+
public CorsConfigurationSource corsConfigurationSource() {
13+
CorsConfiguration config = new CorsConfiguration();
14+
config.setAllowedOrigins(List.of(
15+
"http://localhost:3000",
16+
"http://localhost:8081",
17+
"http://localhost:8080",
18+
"http://10.0.2.2:8080"
19+
));
20+
config.setAllowedMethods(List.of("GET","POST","PUT","PATCH","DELETE","OPTIONS"));
21+
config.setAllowedHeaders(List.of("*"));
22+
config.setExposedHeaders(List.of("Authorization", "Content-Type"));
23+
config.setAllowCredentials(true);
24+
config.setMaxAge(3600L);
25+
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
26+
source.registerCorsConfiguration("/**", config);
27+
return source;
28+
}
29+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package com.workingdead.config;
2+
3+
import io.swagger.v3.oas.models.Components;
4+
import io.swagger.v3.oas.models.OpenAPI;
5+
import io.swagger.v3.oas.models.info.Info;
6+
import io.swagger.v3.oas.models.security.SecurityRequirement;
7+
import io.swagger.v3.oas.models.security.SecurityScheme;
8+
import org.springframework.context.annotation.Bean;
9+
import org.springframework.context.annotation.Configuration;
10+
11+
/**
12+
* Swagger springdoc-ui 구성 파일
13+
*/
14+
@Configuration
15+
public class OpenApiConfig {
16+
@Bean
17+
public OpenAPI openAPI() {
18+
Info info = new Info()
19+
.title("API Document")
20+
.version("v5.28.1")
21+
.description("UniConnect 백엔드 API 명세");
22+
23+
SecurityScheme securityScheme = new SecurityScheme()
24+
.type(SecurityScheme.Type.HTTP)
25+
.scheme("bearer")
26+
.bearerFormat("JWT")
27+
.in(SecurityScheme.In.HEADER)
28+
.name("Authorization");
29+
30+
SecurityRequirement securityRequirement = new SecurityRequirement().addList("Bearer Authentication");
31+
32+
return new OpenAPI()
33+
.components(new Components().addSecuritySchemes("Bearer Authentication", securityScheme))
34+
.addSecurityItem(securityRequirement)
35+
.info(info);
36+
}
37+
}
38+
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package com.workingdead.config;
2+
3+
import org.springframework.context.annotation.Bean;
4+
import org.springframework.context.annotation.Configuration;
5+
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
6+
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
7+
import org.springframework.security.web.SecurityFilterChain;
8+
9+
@Configuration
10+
@EnableWebSecurity
11+
public class SecurityConfig {
12+
13+
@Bean
14+
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
15+
http
16+
.csrf(csrf -> csrf.disable())
17+
.formLogin(form -> form.disable()) // 기본 로그인 폼 비활성화
18+
.httpBasic(basic -> basic.disable()) // 브라우저 팝업 로그인 비활성화
19+
.authorizeHttpRequests(auth -> auth
20+
.requestMatchers(
21+
"/v3/api-docs/**",
22+
"/swagger-ui/**",
23+
"/swagger-ui.html"
24+
).permitAll()
25+
.anyRequest().permitAll()
26+
);
27+
28+
return http.build();
29+
}
30+
}
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+
}
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
package com.workingdead.meet.controller;
2+
3+
import com.workingdead.meet.dto.*;
4+
import com.workingdead.meet.entity.*;
5+
import com.workingdead.meet.repository.*;
6+
import com.workingdead.meet.service.*;
7+
import com.workingdead.meet.dto.PriorityDtos.*;
8+
import io.swagger.v3.oas.annotations.*;
9+
import io.swagger.v3.oas.annotations.media.*;
10+
import io.swagger.v3.oas.annotations.responses.*;
11+
import io.swagger.v3.oas.annotations.tags.Tag;
12+
import jakarta.validation.Valid;
13+
import jakarta.servlet.http.HttpSession;
14+
import org.springframework.http.ResponseEntity;
15+
import org.springframework.web.bind.annotation.*;
16+
17+
import java.util.List;
18+
import java.util.Map;
19+
import java.lang.reflect.Field;
20+
import org.springframework.util.ReflectionUtils;
21+
22+
@Tag(name = "Participant", description = "참여자 관리 API")
23+
@RestController
24+
@RequestMapping("")
25+
public class ParticipantController {
26+
private final ParticipantService participantService;
27+
private final PriorityService priorityService;
28+
private final ParticipantRepository participantRepository;
29+
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+
}
38+
39+
// 0.2 참여자 추가/삭제
40+
@Operation(
41+
summary = "참여자 추가",
42+
description = "특정 투표에 새로운 참여자를 추가합니다. displayName을 기반으로 참여자 칩이 생성됩니다."
43+
)
44+
@ApiResponses({
45+
@ApiResponse(responseCode = "200", description = "참여자 추가 성공",
46+
content = @Content(schema = @Schema(implementation = ParticipantDtos.ParticipantRes.class))),
47+
@ApiResponse(responseCode = "400", description = "잘못된 요청 (displayName이 비어있는 경우)", content = @Content),
48+
@ApiResponse(responseCode = "404", description = "투표를 찾을 수 없음", content = @Content)
49+
})
50+
@PostMapping("/votes/{voteId}/participants")
51+
public ResponseEntity<ParticipantDtos.ParticipantRes> add(
52+
@PathVariable Long voteId,
53+
@RequestBody @Valid ParticipantDtos.CreateParticipantReq req) {
54+
var res = participantService.add(voteId, req.displayName());
55+
return ResponseEntity.ok(res);
56+
}
57+
58+
@Operation(
59+
summary = "참여자 삭제",
60+
description = "특정 참여자를 삭제합니다."
61+
)
62+
@ApiResponses({
63+
@ApiResponse(responseCode = "204", description = "참여자 삭제 성공", content = @Content),
64+
@ApiResponse(responseCode = "404", description = "참여자를 찾을 수 없음", content = @Content)
65+
})
66+
@DeleteMapping("/participants/{participantId}")
67+
public ResponseEntity<Void> remove(@PathVariable Long participantId) {
68+
participantService.remove(participantId);
69+
return ResponseEntity.noContent().build();
70+
}
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);
121+
}
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+
// }

0 commit comments

Comments
 (0)