Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ dependencies {
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.8'
implementation "org.springframework.session:spring-session-jdbc"

}

tasks.named('test') {
Expand Down
22 changes: 22 additions & 0 deletions docs.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Notes: Spec โ†” Implementation ๋งคํ•‘
#
# page#0 ๊ด€๋ฆฌ์ž ํŽ˜์ด์ง€
# 0.1 ํ™ˆํ™”๋ฉด
# GET /votes : ๋ชฉ๋ก (id, name, code, adminUrl, shareUrl)
ํˆฌํ‘œ ๋ธ”๋ก ํ„ฐ์น˜ โ†’ GET /votes/{id} ๋กœ ์ƒ์„ธ ์กฐํšŒ
# POST /votes : ์ƒˆ ํˆฌํ‘œ ์ƒ์„ฑ (res.shareUrl ๋กœ ๋งํฌ ๋ณต์‚ฌ)
๋‚ ์งœ 2๊ฐœ ์„ ํƒ โ†’ backend์—๋Š” startDate,endDate๋กœ ์ €์žฅ (๋‘ ๋‚ ์งœ ํฌํ•จ ๋ฒ”์œ„)
ํ”„๋ก ํŠธ์—์„œ 3๋ฒˆ์งธ ๋‚ ์งœ ์„ ํƒ ์‹œ ํ”„๋ก ํŠธ ๋กœ์ง์œผ๋กœ 2๊ฐœ๋งŒ ์œ ์ง€ โ†’ ์„œ๋ฒ„์—” ํ•ญ์ƒ (start,end)๋งŒ ์ „๋‹ฌ
# DELETE /votes/{id} : ํˆฌํ‘œ ์‚ญ์ œ
# PATCH /votes/{id} : ์ด๋ฆ„/๋‚ ์งœ ๋ฒ”์œ„ ์ˆ˜์ • (๋‘˜ ๋‹ค vote attr)

#
# 0.2 ํˆฌํ‘œ ์„ค์ • ํ™”๋ฉด
# ์ฐธ์—ฌ์ž ์นฉ ์ƒ์„ฑ ์„น์…˜: POST /votes/{id}/participants {displayName}
์ƒ์„ฑ๋œ ์‘๋‹ต์— loginCode ํฌํ•จ(์นฉ/๋งค์ง๋งํฌ ๊ตฌํ˜„์šฉ)
# ์ฐธ์—ฌ์ž ์‚ญ์ œ: DELETE /participants/{participantId}
#
# ๊ธฐํƒ€
# Vote ์‚ญ์ œ ์‹œ @OneToMany(cascade=ALL, orphanRemoval=true)๋กœ ์ฐธ๊ฐ€์ž ํ•จ๊ป˜ ์‚ญ์ œ
# ์ฝ”๋“œ/๋งํฌ: app.base-url + "/v/" + code (๊ณต์œ  ๋งํฌ), "/admin/votes/{id}" (๊ด€๋ฆฌ URL)
# Validation: endDate >= startDate. name not blank.
29 changes: 29 additions & 0 deletions src/main/java/com/workingdead/config/CorsConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.workingdead.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.*;

import java.util.List;

@Configuration
public class CorsConfig {
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(List.of(
"http://localhost:3000",
"http://localhost:8081",
"http://localhost:8080",
"http://10.0.2.2:8080"
));
config.setAllowedMethods(List.of("GET","POST","PUT","PATCH","DELETE","OPTIONS"));
config.setAllowedHeaders(List.of("*"));
config.setExposedHeaders(List.of("Authorization", "Content-Type"));
config.setAllowCredentials(true);
config.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return source;
}
}
38 changes: 38 additions & 0 deletions src/main/java/com/workingdead/config/OpenApiConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.workingdead.config;

import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.security.SecurityRequirement;
import io.swagger.v3.oas.models.security.SecurityScheme;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
* Swagger springdoc-ui ๊ตฌ์„ฑ ํŒŒ์ผ
*/
@Configuration
public class OpenApiConfig {
@Bean
public OpenAPI openAPI() {
Info info = new Info()
.title("API Document")
.version("v5.28.1")
.description("UniConnect ๋ฐฑ์—”๋“œ API ๋ช…์„ธ");

SecurityScheme securityScheme = new SecurityScheme()
.type(SecurityScheme.Type.HTTP)
.scheme("bearer")
.bearerFormat("JWT")
.in(SecurityScheme.In.HEADER)
.name("Authorization");

SecurityRequirement securityRequirement = new SecurityRequirement().addList("Bearer Authentication");

return new OpenAPI()
.components(new Components().addSecuritySchemes("Bearer Authentication", securityScheme))
.addSecurityItem(securityRequirement)
.info(info);
}
}

30 changes: 30 additions & 0 deletions src/main/java/com/workingdead/config/SecurityConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.workingdead.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.formLogin(form -> form.disable()) // ๊ธฐ๋ณธ ๋กœ๊ทธ์ธ ํผ ๋น„ํ™œ์„ฑํ™”
.httpBasic(basic -> basic.disable()) // ๋ธŒ๋ผ์šฐ์ € ํŒ์—… ๋กœ๊ทธ์ธ ๋น„ํ™œ์„ฑํ™”
.authorizeHttpRequests(auth -> auth
.requestMatchers(
"/v3/api-docs/**",
"/swagger-ui/**",
"/swagger-ui.html"
).permitAll()
.anyRequest().permitAll()
);

return http.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package com.workingdead.meet.controller;

import com.workingdead.meet.dto.*;
import com.workingdead.meet.entity.*;
import com.workingdead.meet.repository.*;
import com.workingdead.meet.service.*;
import io.swagger.v3.oas.annotations.*;
import io.swagger.v3.oas.annotations.media.*;
import io.swagger.v3.oas.annotations.responses.*;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@Tag(name = "Participant", description = "์ฐธ์—ฌ์ž ๊ด€๋ฆฌ API")
@RestController
@RequestMapping("")
public class ParticipantController {
private final ParticipantService participantService;
public ParticipantController(ParticipantService participantService) { this.participantService = participantService; }


// 0.2 ์ฐธ์—ฌ์ž ์ถ”๊ฐ€/์‚ญ์ œ
@Operation(
summary = "์ฐธ์—ฌ์ž ์ถ”๊ฐ€",
description = "ํŠน์ • ํˆฌํ‘œ์— ์ƒˆ๋กœ์šด ์ฐธ์—ฌ์ž๋ฅผ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค. displayName์„ ๊ธฐ๋ฐ˜์œผ๋กœ ์ฐธ์—ฌ์ž ์นฉ์ด ์ƒ์„ฑ๋ฉ๋‹ˆ๋‹ค."
)
@ApiResponses({
@ApiResponse(responseCode = "200", description = "์ฐธ์—ฌ์ž ์ถ”๊ฐ€ ์„ฑ๊ณต",
content = @Content(schema = @Schema(implementation = ParticipantDtos.ParticipantRes.class))),
@ApiResponse(responseCode = "400", description = "์ž˜๋ชป๋œ ์š”์ฒญ (displayName์ด ๋น„์–ด์žˆ๋Š” ๊ฒฝ์šฐ)", content = @Content),
@ApiResponse(responseCode = "404", description = "ํˆฌํ‘œ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Œ", content = @Content)
})
@PostMapping("/votes/{voteId}/participants")
public ResponseEntity<ParticipantDtos.ParticipantRes> add(@PathVariable Long voteId, @RequestBody @Valid ParticipantDtos.CreateParticipantReq req) {
var res = participantService.add(voteId, req.displayName());
return ResponseEntity.ok(res);
}

@Operation(
summary = "์ฐธ์—ฌ์ž ์‚ญ์ œ",
description = "ํŠน์ • ์ฐธ์—ฌ์ž๋ฅผ ์‚ญ์ œํ•ฉ๋‹ˆ๋‹ค."
)
@ApiResponses({
@ApiResponse(responseCode = "204", description = "์ฐธ์—ฌ์ž ์‚ญ์ œ ์„ฑ๊ณต", content = @Content),
@ApiResponse(responseCode = "404", description = "์ฐธ์—ฌ์ž๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Œ", content = @Content)
})
@DeleteMapping("/participants/{participantId}")
public ResponseEntity<Void> remove(@PathVariable Long participantId) {
participantService.remove(participantId);
return ResponseEntity.noContent().build();
}
}
87 changes: 87 additions & 0 deletions src/main/java/com/workingdead/meet/controller/VoteController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package com.workingdead.meet.controller;

import com.workingdead.meet.dto.*;
import com.workingdead.meet.entity.*;
import com.workingdead.meet.repository.*;
import com.workingdead.meet.service.*;
import io.swagger.v3.oas.annotations.*;
import io.swagger.v3.oas.annotations.media.*;
import io.swagger.v3.oas.annotations.responses.*;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;

@Tag(name = "Vote", description = "ํˆฌํ‘œ ๊ด€๋ฆฌ API")
@RestController
@RequestMapping("/votes")
public class VoteController {
private final VoteService voteService;
public VoteController(VoteService voteService) { this.voteService = voteService; }

@Operation(
summary = "ํˆฌํ‘œ ๋ชฉ๋ก ์กฐํšŒ",
description = "๋ชจ๋“  ํˆฌํ‘œ ๋ชฉ๋ก์„ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. ๊ฐ ํˆฌํ‘œ์˜ ๊ธฐ๋ณธ ์ •๋ณด(id, name, code, adminUrl, shareUrl, startDate, endDate)๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค." +
"code+baseUrl=shareUrl"
)
@ApiResponses({
@ApiResponse(responseCode = "200", description = "์กฐํšŒ ์„ฑ๊ณต",
content = @Content(schema = @Schema(implementation = VoteDtos.VoteSummary.class)))
})
// 0.1 ํ™ˆํ™”๋ฉด ๋ฆฌ์ŠคํŠธ & ์ƒ์„ฑ
@GetMapping
public List<VoteDtos.VoteSummary> list() { return voteService.listAll(); }

// 0.2 ํˆฌํ‘œ ์„ค์ • ํ™”๋ฉด ์ฝ๊ธฐ/์ˆ˜์ •/์‚ญ์ œ
@GetMapping("/{id}")
public VoteDtos.VoteDetail get(@PathVariable Long id) { return voteService.get(id); }


@Operation(
summary = "์ƒˆ ํˆฌํ‘œ ์ƒ์„ฑ",
description = "์ƒˆ๋กœ์šด ํˆฌํ‘œ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. ๊ณ ์œ ํ•œ code๊ฐ€ ์ž๋™ ์ƒ์„ฑ๋˜๋ฉฐ, shareUrl์„ ํ†ตํ•ด ์ฐธ์—ฌ์ž์—๊ฒŒ ๊ณต์œ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค."
)
@ApiResponses({
@ApiResponse(responseCode = "200", description = "์ƒ์„ฑ ์„ฑ๊ณต",
content = @Content(schema = @Schema(implementation = VoteDtos.VoteSummary.class))),
@ApiResponse(responseCode = "400", description = "์ž˜๋ชป๋œ ์š”์ฒญ (name์ด ๋น„์–ด์žˆ๋Š” ๊ฒฝ์šฐ)", content = @Content)
})
@PostMapping
public ResponseEntity<VoteDtos.VoteSummary> create(@RequestBody @Valid VoteDtos.CreateVoteReq req) {
var res = voteService.create(req);
// 0.2.3 ๋งํฌ ๋ณต์‚ฌ: res.shareUrl ํฌํ•จ
return ResponseEntity.ok(res);
}


@Operation(
summary = "ํˆฌํ‘œ ์ •๋ณด ์ˆ˜์ •",
description = "ํˆฌํ‘œ์˜ ์ด๋ฆ„ ๋˜๋Š” ๋‚ ์งœ ๋ฒ”์œ„๋ฅผ ์ˆ˜์ •ํ•ฉ๋‹ˆ๋‹ค. endDate๋Š” startDate๋ณด๋‹ค ํฌ๊ฑฐ๋‚˜ ๊ฐ™์•„์•ผ ํ•ฉ๋‹ˆ๋‹ค."
)
@ApiResponses({
@ApiResponse(responseCode = "200", description = "์ˆ˜์ • ์„ฑ๊ณต",
content = @Content(schema = @Schema(implementation = VoteDtos.VoteDetail.class))),
@ApiResponse(responseCode = "400", description = "์ž˜๋ชป๋œ ์š”์ฒญ (endDate < startDate)", content = @Content),
@ApiResponse(responseCode = "404", description = "ํˆฌํ‘œ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Œ", content = @Content)
})
@PatchMapping("/{id}")
public VoteDtos.VoteDetail update(@PathVariable Long id, @RequestBody VoteDtos.UpdateVoteReq req) {
// ๋‚ ์งœ ๋ฒ”์œ„ ๊ฒ€์ฆ์€ ์„œ๋น„์Šค์—์„œ ์ฒ˜๋ฆฌ (end >= start)
return voteService.update(id, req);
}

@Operation(
summary = "ํˆฌํ‘œ ์‚ญ์ œ",
description = "ํˆฌํ‘œ๋ฅผ ์‚ญ์ œํ•ฉ๋‹ˆ๋‹ค. ์—ฐ๊ด€๋œ ๋ชจ๋“  ์ฐธ์—ฌ์ž๋„ ํ•จ๊ป˜ ์‚ญ์ œ๋ฉ๋‹ˆ๋‹ค (cascade)."
)
@ApiResponses({
@ApiResponse(responseCode = "204", description = "์‚ญ์ œ ์„ฑ๊ณต", content = @Content),
@ApiResponse(responseCode = "404", description = "ํˆฌํ‘œ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Œ", content = @Content)
})
@DeleteMapping("/{id}")
public ResponseEntity<Void> delete(@PathVariable Long id) {
voteService.delete(id);
return ResponseEntity.noContent().build();
}
}
9 changes: 9 additions & 0 deletions src/main/java/com/workingdead/meet/dto/ParticipantDtos.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.workingdead.meet.dto;

import jakarta.validation.constraints.*;


public class ParticipantDtos {
public record CreateParticipantReq(@NotBlank String displayName) {}
public record ParticipantRes(Long id, String displayName) {}
}
20 changes: 20 additions & 0 deletions src/main/java/com/workingdead/meet/dto/VoteDtos.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.workingdead.meet.dto;

import jakarta.validation.constraints.*;
import java.time.*;
import java.util.*;


public class VoteDtos {
public record CreateVoteReq(
@NotBlank String name,
LocalDate startDate,
LocalDate endDate,
List<String> participantNames // ์ฐธ์—ฌ์ž ์ด๋ฆ„ ๋ฆฌ์ŠคํŠธ (optional)
) {}
public record UpdateVoteReq(String name, LocalDate startDate, LocalDate endDate) {}

public record VoteSummary(Long id, String name, String code, String adminUrl, String shareUrl, LocalDate startDate, LocalDate endDate) {}

public record VoteDetail(Long id, String name, String code, LocalDate startDate, LocalDate endDate, List<ParticipantDtos.ParticipantRes> participants) {}
}
39 changes: 39 additions & 0 deletions src/main/java/com/workingdead/meet/entity/Participant.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.workingdead.meet.entity;

import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;

@Getter @Setter @AllArgsConstructor @Builder
@Entity
@Table(name = "participant")
public class Participant {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;


@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "vote_id", nullable = false)
private Vote vote; // FK to vote


@Column(nullable = false)
private String displayName; // ์นฉ์— ๋ณด์—ฌ์ค„ ์ด๋ฆ„


// @Column(nullable = false, unique = true)
// private String loginCode; // ๊ฐ„๋‹จ ๋กœ๊ทธ์ธ ํ† ํฐ(์นฉ ์ƒ์„ฑ์šฉ)


public Participant() {}
public Participant(Vote vote, String displayName) {
this.vote = vote;
this.displayName = displayName;
// this.loginCode = loginCode;
}


// getters/setters
}
47 changes: 47 additions & 0 deletions src/main/java/com/workingdead/meet/entity/Vote.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package com.workingdead.meet.entity;

import jakarta.persistence.*;
import lombok.*;

import java.time.*;
import java.util.*;


@Getter @Setter @AllArgsConstructor @Builder
@Entity
@Table(name = "vote")
public class Vote {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;


@Column(nullable = false)
private String name; // ํˆฌํ‘œ(๋ธ”๋ก) ์ด๋ฆ„


@Column(unique = true, nullable = false, updatable = false)
private String code; // ๊ณต์œ ์šฉ ์งง์€ ์ฝ”๋“œ (๋งํฌ)


private LocalDate startDate; // ์„ ํƒ ๋ฒ”์œ„ ์‹œ์ž‘
private LocalDate endDate; // ์„ ํƒ ๋ฒ”์œ„ ๋


@Column(nullable = false, updatable = false)
private Instant createdAt = Instant.now();


@OneToMany(mappedBy = "vote", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Participant> participants = new ArrayList<>();


public Vote() {}
public Vote(String name, String code) {
this.name = name;
this.code = code;
}


// getters/setters
public void setDateRange(LocalDate start, LocalDate end) { this.startDate = start; this.endDate = end; }
}
Loading