Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
55567a2
feat : 투표 결과 내림차순 적용
challonsy Jun 23, 2025
72f2af6
Refactor: 투표 현황 조회 득표수 내림차순 정렬로 수정
SeoyeonPark1223 Jun 23, 2025
33b7c57
Refactor: api에 summary 추가
SeoyeonPark1223 Jun 23, 2025
0317a9f
Merge pull request #15 from challonsy/feature
SeoyeonPark1223 Jun 23, 2025
920143f
Merge pull request #16 from SeoyeonPark1223/feature-team
challonsy Jun 23, 2025
56cd1bb
refactor : 컬럼명 통일
challonsy Jun 27, 2025
41f1052
Fix: reissue 로직 수정
SeoyeonPark1223 Jun 28, 2025
59cc5e7
Merge pull request #17 from SeoyeonPark1223/fix-token
challonsy Jun 28, 2025
69b9016
Fix: 파트장 투표 현황 조회 범위 수정
SeoyeonPark1223 Jun 28, 2025
2a4e07a
Merge pull request #18 from SeoyeonPark1223/fix-part
challonsy Jun 28, 2025
85f9130
Fix: 프론트 cors 수정
SeoyeonPark1223 Jun 29, 2025
75613a4
Fix: 프론트가 다시 경로 수정함
SeoyeonPark1223 Jun 29, 2025
d6799b9
Merge branch 'develop' of https://github.com/Team-Influy/spring-vote-…
challonsy Jun 30, 2025
a2f898b
cors : cors에 배포 주소 추가
challonsy Jun 30, 2025
bf6b30f
fix : 변수명 오류 해결
challonsy Jun 30, 2025
523f622
Merge pull request #19 from challonsy/cors
SeoyeonPark1223 Jun 30, 2025
d650df3
Merge branch 'develop' of https://github.com/Team-Influy/spring-vote-…
SeoyeonPark1223 Jun 30, 2025
5faffdb
Docs: README 내용 추가
SeoyeonPark1223 Jun 30, 2025
5b18ece
Merge pull request #20 from SeoyeonPark1223/fix-part
challonsy Jun 30, 2025
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
40 changes: 39 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,4 +57,42 @@ Example)
![api](readme-src/api.png)

### 배포 사이트
- [vote.influy.com](https://vote.influy.com/swagger-ui/index.html#)
- [vote.influy.com](https://vote.influy.com/swagger-ui/index.html#)

## 참고
### 이번 개발 플젝의 목표: 간단!
- ERD는 최대한 간단하게! (테이블 최소화, 연결 최소화)
### 이니셜라이저
- CommandLineRunner를 통해 스프링 어플리케이션 시작과 동시에 후보자 목록 개수가 0개면 후보자를 등록하도록 설정
- JSON 파일을 읽어 entity로 만든 후 DB에 저장하는 방식
### EOF 따옴표
- CI/CD 과정에서 깃시크릿에 있는 application.yml을 복사하여 생성하는 과정에서 `cat << EOF`를 사용했는데, 여기서 문제 발생
- `cat << EOF` 면 yml 내부에 있는 환경변수들이 먼저 치환되어 들어가게 되는데 그 시점에는 값이 없어서 null로 처리되면서 에러 발생
- 이 부분을 `cat << 'EOF'` 로 바꿔서 문자 그대로 반영되었다가 실행 시점에 `docker-compose.yml`의 환경변수 값으로 치환될 수 있도록 함

```bash
- name: Create application.yml
shell: bash
run: |
mkdir -p src/main/resources
cat <<'EOF' > src/main/resources/application.yml
${{ secrets.APPLICATION_PROD_YML }}
EOF
```
### 백엔드에서 처리한 투표 관련 예외처리
```java
// 유저 관련 에러 응답
USER_ALREADY_EXISTS(HttpStatus.BAD_REQUEST, "USER ALREADY EXISTS", "유저가 이미 존재합니다."),
USER_NOT_FOUND(HttpStatus.NOT_FOUND, "USER NOT FOUND", "유저를 찾을 수 없습니다."),

//파트장 관련 에러 응답
LEADER_NOT_FOUND(HttpStatus.NOT_FOUND, "LEADER NOT FOUND", "파트장 후보를 찾을 수 없습니다."),

//투표 관련 에러 응당
WRONG_PART(HttpStatus.FORBIDDEN, "WRONG PART", "투표자와 투표 대상의 파트가 다릅니다."),
ALREADY_VOTED(HttpStatus.FORBIDDEN, "ALREADY_VOTED", "투표는 한 번만 할 수 있습니다."),

//팀 관련 에러 응답
TEAM_NOT_FOUND(HttpStatus.NOT_FOUND, "TEAM NOT FOUND", "팀 후보를 찾을 수 없습니다."),
NOT_VALID_VOTE(HttpStatus.BAD_REQUEST, "NOT VALID VOTE", "본인 팀에게 투표할 수 없습니다."),
```
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import ceos.study.vote.global.common.PartType;
import ceos.study.vote.global.jwt.CustomUserDetails;
import ceos.study.vote.global.jwt.CustomUserDetailsServiceImpl;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
Expand All @@ -27,6 +28,7 @@ public class LeaderController {
private final CustomUserDetailsServiceImpl customUserDetailsService;

@PostMapping("votes/leaders")
@Operation(summary = "파트장 투표")
public ApiResponse<LeaderResponseDTO.VoteResult> vote(@RequestBody LeaderRequestDTO.Vote request,
@AuthenticationPrincipal CustomUserDetails userDetails) {
User user = customUserDetailsService.findUserByUserDetails(userDetails);
Expand All @@ -38,6 +40,7 @@ public ApiResponse<LeaderResponseDTO.VoteResult> vote(@RequestBody LeaderRequest

//후보자 정보 조회
@GetMapping("candidates/leaders/{part}")
@Operation(summary = "파트장 후보 리스트 조회")
public ApiResponse<List<LeaderResponseDTO.Info>> details(@PathVariable("part") PartType part) {

List<Leader> candidates = leaderService.getCandidates(part);
Expand All @@ -48,9 +51,10 @@ public ApiResponse<List<LeaderResponseDTO.Info>> details(@PathVariable("part") P

//투표 현황
@GetMapping("votes/leaders/{part}/status")
@Operation(summary = "파트장 투표 현황 조회")
public ApiResponse<LeaderResponseDTO.StatsList> voteStats(@PathVariable("part") PartType part) {

List<Leader> candidates = leaderService.getCandidates(part);
List<Leader> candidates = leaderService.getCandidatesOrderByVoteNum(part);
Integer totalVotes = leaderService.getTotalVotes(part);
LeaderResponseDTO.StatsList body = LeaderConverter.toStatsList(candidates,totalVotes);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,6 @@

public interface LeaderRepository extends JpaRepository<Leader, Long> {
List<Leader> findAllByPart(PartType part);

List<Leader> findAllByPartOrderByVoteNumDesc(PartType part);
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,6 @@ public interface LeaderService {
List<Leader> getCandidates(PartType part);

Integer getTotalVotes(PartType part);

List<Leader> getCandidatesOrderByVoteNum(PartType part);
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,20 @@ public Leader vote(LeaderRequestDTO.Vote request, User user) {

@Override
public List<Leader> getCandidates(PartType part) {

return leaderRepository.findAllByPart(part);
}

@Override
public Integer getTotalVotes(PartType part) {

return userRepository.countUserByLeaderVoteTrueAndPart(part);
}

@Override
public List<Leader> getCandidatesOrderByVoteNum(PartType part) {
return leaderRepository.findAllByPartOrderByVoteNumDesc(part);
}


}
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,13 @@ public ApiResponse<TeamResponseDto.VoteResultDto> vote(@AuthenticationPrincipal
}

@GetMapping("votes/teams/status")
@Operation(summary = "데모데이 투표 현황 조회")
@Operation(summary = "데모데이 투표 현황 조회 (득표수 내림차순 정렬)")
public ApiResponse<TeamResponseDto.VoteStatusListDto> status() {
return ApiResponse.onSuccess(teamService.status());
}

@GetMapping("candidates/teams")
@Operation(summary = "데모데이 후보 조회")
public ApiResponse<List<TeamResponseDto.CandidateDto>> candidates() {
return ApiResponse.onSuccess(teamService.candidates());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ public static TeamResponseDto.VoteStatusDto toVoteStatusDto(Team team, Integer t
.id(team.getId())
.team(team.getName())
.description(team.getDescription())
.numVotes(team.getVoteNum())
.ratioVotes(Math.round(((double) team.getVoteNum() / totalVotes) * 10000) / 100.0)
.numVotes(team.getNumVotes())
.ratioVotes(Math.round(((double) team.getNumVotes() / totalVotes) * 10000) / 100.0)
.build();
}

Expand Down
5 changes: 2 additions & 3 deletions src/main/java/ceos/study/vote/domain/team/entity/Team.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import ceos.study.vote.global.common.BaseEntity;
import ceos.study.vote.global.common.TeamType;
import jakarta.annotation.Nullable;
import jakarta.persistence.*;
import jakarta.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
Expand All @@ -29,7 +28,7 @@ public class Team extends BaseEntity {

@Builder.Default
@Column(nullable = false)
private Integer voteNum = 0;
private Integer numVotes = 0;

public void addVote() { voteNum++; }
public void addVote() { numVotes++; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,14 @@
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;

import java.util.List;
import java.util.Optional;

public interface TeamRepository extends JpaRepository<Team, Long> {
Optional<Team> findByName(TeamType team);

@Query("SELECT SUM(t.voteNum) FROM Team t")
@Query("SELECT SUM(t.numVotes) FROM Team t")
Integer getTotalVotes();

List<Team> findAllByOrderByNumVotesDesc();
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ public TeamResponseDto.VoteResultDto vote(User user, TeamRequestDto.VoteDto requ
@Override
@Transactional(readOnly = true)
public TeamResponseDto.VoteStatusListDto status() {
return TeamConverter.toVoteStatusListDto(teamRepository.findAll().stream().toList(), teamRepository.getTotalVotes());
return TeamConverter.toVoteStatusListDto(teamRepository.findAllByOrderByNumVotesDesc(), teamRepository.getTotalVotes());
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ public ApiResponse<UserResponseDto.ResultDto> signOut(HttpServletRequest request
@PostMapping("/reissue")
@Operation(summary = "리프레시 토큰으로 액세스 토큰 재발급 API")
public ApiResponse<UserResponseDto.ReissueResultDto> reissue(@RequestBody @Valid UserRequestDto.ReissueRequestDto request, @AuthenticationPrincipal CustomUserDetails customUserDetails) {
TokenProvider.TokenPair tokenPair = userService.reissue(request, customUserDetails.getUser());
TokenProvider.TokenPair tokenPair = userService.reissue(request);
return ApiResponse.onSuccess(UserConverter.toReissueResultDto(tokenPair));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@ public interface UserService {
User signUp(UserRequestDto.SignUpRequestDto request);
TokenProvider.TokenPair signIn(UserRequestDto.SignInRequestDto request);
void signOut(String accessToken, User user);
TokenProvider.TokenPair reissue(UserRequestDto.ReissueRequestDto request, User user);
TokenProvider.TokenPair reissue(UserRequestDto.ReissueRequestDto request);
}
Original file line number Diff line number Diff line change
Expand Up @@ -69,13 +69,16 @@ public void signOut(String accessToken, User user) {
}

@Override
public TokenProvider.TokenPair reissue(UserRequestDto.ReissueRequestDto request, User user) {
if (user == null) throw new GeneralException(ErrorStatus.USER_NOT_FOUND);
public TokenProvider.TokenPair reissue(UserRequestDto.ReissueRequestDto request) {
String refreshToken = request.getRefreshToken();

// ValidateToken false 반환 -> 사용하려는 refreshToken이 유효하지 않음 (redis에 없음)
if (!tokenProvider.validateToken(refreshToken)) throw new GeneralException(ErrorStatus.INVALID_TOKEN);

String email = tokenProvider.getEmailFromToken(refreshToken);
User user = userRepository.findByEmail(email)
.orElseThrow(() -> new GeneralException(ErrorStatus.USER_NOT_FOUND));

String redisRefreshToken = redisService.getValue(user.getEmail());
if (StringUtils.isEmpty(refreshToken) || StringUtils.isEmpty(redisRefreshToken) || !redisRefreshToken.equals(refreshToken)) {
throw new GeneralException(ErrorStatus.INVALID_TOKEN);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public enum ErrorStatus implements BaseCode {

//투표 관련 에러 응당
WRONG_PART(HttpStatus.FORBIDDEN, "WRONG PART", "투표자와 투표 대상의 파트가 다릅니다."),
ALREADY_VOTED(HttpStatus.FORBIDDEN, "WRONG PART", "투표는 한 번만 할 수 있습니다."),
ALREADY_VOTED(HttpStatus.FORBIDDEN, "ALREADY_VOTED", "투표는 한 번만 할 수 있습니다."),

//팀 관련 에러 응답
TEAM_NOT_FOUND(HttpStatus.NOT_FOUND, "TEAM NOT FOUND", "팀 후보를 찾을 수 없습니다."),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ public static CorsConfigurationSource corsConfigurationSource() {
// 허용할 Origin
corsConfig.addAllowedOrigin("http://localhost:3000");
corsConfig.addAllowedOrigin("https://next-vote-21th-smoky.vercel.app");
corsConfig.addAllowedOrigin("https://vote.influy.com");

// 요청 메서드, 헤더
corsConfig.addAllowedMethod("*");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,11 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti
.formLogin((auth) -> auth.disable())
.sessionManagement(configurer -> configurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests((requests) -> requests
.requestMatchers("/","users/sign-in", "users/sign-up"
.requestMatchers("/","users/sign-in", "users/sign-up", "users/reissue"
,"/user/**"
,"/candidates/**"
,"/votes/teams/status"
,"/votes/leaders/*/status"
,"/swagger-ui/**"
,"/swagger-resources/**"
,"/v3/api-docs/**"
Expand Down
12 changes: 5 additions & 7 deletions src/main/java/ceos/study/vote/global/jwt/TokenProvider.java
Original file line number Diff line number Diff line change
Expand Up @@ -44,16 +44,14 @@ public void afterPropertiesSet() {

public String getEmailFromToken(String token) {
try {
Claims claims = Jwts.parser()
return Jwts.parser()
.setSigningKey(key)
.build()
.parseClaimsJws(token)
.getBody();
log.info("claims.getSubject() = {}", claims.getSubject());
return claims.getSubject(); // subject = email
} catch (Exception ex) {
log.error("JWT parsing failed: {}", ex.getMessage());
return new GeneralException(ErrorStatus.UNSUPPORTED_TOKEN).toString();
.getBody()
.getSubject();
} catch (JwtException | IllegalArgumentException e) {
throw new GeneralException(ErrorStatus.INVALID_TOKEN);
}
}

Expand Down