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
79 changes: 78 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,78 @@
# debate-timer
# Debate Timer Backend

## 프로젝트 소개

디베이트 타이머는 더 쉬운 토론 진행을 위한, 오직 토론을 위한 타이머입니다.

## 기술 스택

### Backend

<div>
<img src="https://img.shields.io/badge/Java-007396?style=for-the-badge&logo=java&logoColor=white">
<img src="https://img.shields.io/badge/Spring_Boot-6DB33F?style=for-the-badge&logo=springboot&logoColor=white">
<img src="https://img.shields.io/badge/Spring_Data_JPA-6DB33F?style=for-the-badge&logo=spring&logoColor=white">
</div>

### Database

<div>
<img src="https://img.shields.io/badge/MySQL-4479A1?style=for-the-badge&logo=mysql&logoColor=white">
<img src="https://img.shields.io/badge/H2-003545?style=for-the-badge&logo=h2&logoColor=white">
<img src="https://img.shields.io/badge/Flyway-CC0200?style=for-the-badge&logo=flyway&logoColor=white">
</div>

### DevOps & Monitoring

<div>
<img src="https://img.shields.io/badge/GitHub_Actions-2088FF?style=for-the-badge&logo=githubactions&logoColor=white">
<img src="https://img.shields.io/badge/AWS-232F3E?style=for-the-badge&logo=amazonaws&logoColor=white">
<img src="https://img.shields.io/badge/Datadog-632CA6?style=for-the-badge&logo=datadog&logoColor=white">
</div>

### Documentation & Testing

<div>
<img src="https://img.shields.io/badge/Swagger-85EA2D?style=for-the-badge&logo=swagger&logoColor=black">
<img src="https://img.shields.io/badge/Spring_REST_Docs-6DB33F?style=for-the-badge&logo=spring&logoColor=white">
<img src="https://img.shields.io/badge/JUnit5-25A162?style=for-the-badge&logo=junit5&logoColor=white">
<img src="https://img.shields.io/badge/Mockito-01FF70?style=for-the-badge">
</div>

## 프로젝트 구조

```
src/
├── main/
│ ├── java/com/debatetimer/
│ │ ├── aop/ # AOP 및 로깅 관련
│ │ ├── client/ # 외부 API 클라이언트
│ │ ├── config/ # 설정 클래스
│ │ ├── controller/ # REST API 컨트롤러
│ │ │ ├── auth/ # 인증 관련
│ │ │ ├── customize/ # 토론 테이블 커스터마이징
│ │ │ ├── member/ # 회원 관리
│ │ │ └── poll/ # 투표 관리
│ │ ├── domain/ # 도메인 모델
│ │ ├── domainrepository/ # 도메인 레포지토리
│ │ ├── dto/ # 데이터 전송 객체
│ │ ├── entity/ # JPA 엔티티
│ │ ├── exception/ # 예외 처리
│ │ ├── repository/ # JPA 레포지토리
│ │ └── service/ # 비즈니스 로직
│ └── resources/
│ ├── application*.yml # 환경별 설정
│ ├── logging/ # 로깅 설정
│ └── db/migration/ # Flyway 마이그레이션
└── test/ # 테스트 코드
```

## Infra & Deployment

![img.png](./docs/debate_timer_infra_v0-1.png)

## BE 팀원 소개

| <img src="https://avatars.githubusercontent.com/u/148152234?v=4" width="100" height="100"/> | <img src="https://avatars.githubusercontent.com/u/44027393?v=4" width="100" height="100"/> | <img src="https://avatars.githubusercontent.com/u/121424793?v=4" width="100" height="100"/> |
|:-------------------------------------------------------------------------------------------:|:------------------------------------------------------------------------------------------:|:-------------------------------------------------------------------------------------------:|
| [콜리](https://github.com/coli-geonwoo) | [커찬](https://github.com/leegwichan) | [비토](https://github.com/unifolio0) |
Binary file added docs/debate_timer_infra_v0-1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/debate_timer_infra_v0.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 5 additions & 3 deletions src/main/java/com/debatetimer/domain/poll/Vote.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.debatetimer.domain.poll;

import java.time.LocalDateTime;
import lombok.Getter;
import lombok.RequiredArgsConstructor;

Expand All @@ -12,12 +13,13 @@ public class Vote {
private final VoteTeam team;
private final ParticipantName name;
private final ParticipateCode code;
private final LocalDateTime createdAt;

public Vote(long pollId, VoteTeam team, String name, String code) {
this(null, pollId, team, name, code);
this(null, pollId, team, name, code, LocalDateTime.now());
}

public Vote(Long id, long pollId, VoteTeam team, String name, String code) {
this(id, pollId, team, new ParticipantName(name), new ParticipateCode(code));
public Vote(Long id, long pollId, VoteTeam team, String name, String code, LocalDateTime createdAt) {
this(id, pollId, team, new ParticipantName(name), new ParticipateCode(code), createdAt);
}
}
26 changes: 22 additions & 4 deletions src/main/java/com/debatetimer/domain/poll/VoteInfo.java
Original file line number Diff line number Diff line change
@@ -1,19 +1,37 @@
package com.debatetimer.domain.poll;

import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import lombok.Getter;

@Getter
public class VoteInfo {

private static final Comparator<Vote> VOTE_COMPARATOR = Comparator.comparing(Vote::getCreatedAt);
private static final long INITIAL_VOTE_COUNT = 0L;

private final long pollId;
private final long totalCount;
private final long prosCount;
private final long consCount;
private final List<ParticipantName> voterNames;

public VoteInfo(long pollId, long prosCount, long consCount) {
public VoteInfo(long pollId, List<Vote> votes) {
Map<VoteTeam, Long> voteCounts = createVoteCounts(votes);
this.pollId = pollId;
this.totalCount = prosCount + consCount;
this.prosCount = prosCount;
this.consCount = consCount;
this.totalCount = votes.size();
this.prosCount = voteCounts.getOrDefault(VoteTeam.PROS, INITIAL_VOTE_COUNT);
this.consCount = voteCounts.getOrDefault(VoteTeam.CONS, INITIAL_VOTE_COUNT);
this.voterNames = votes.stream()
.sorted(VOTE_COMPARATOR)
.map(Vote::getName)
.toList();
}

private Map<VoteTeam, Long> createVoteCounts(List<Vote> votes) {
return votes.stream()
.collect(Collectors.groupingBy(Vote::getTeam, Collectors.counting()));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import com.debatetimer.domain.poll.ParticipateCode;
import com.debatetimer.domain.poll.Vote;
import com.debatetimer.domain.poll.VoteInfo;
import com.debatetimer.domain.poll.VoteTeam;
import com.debatetimer.entity.poll.PollEntity;
import com.debatetimer.entity.poll.VoteEntity;
import com.debatetimer.exception.custom.DTClientErrorException;
Expand All @@ -12,8 +11,6 @@
import com.debatetimer.repository.poll.PollRepository;
import com.debatetimer.repository.poll.VoteRepository;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.stereotype.Repository;
Expand All @@ -27,16 +24,11 @@ public class VoteDomainRepository {
private final RepositoryErrorDecoder errorDecoder;

public VoteInfo findVoteInfoByPollId(long pollId) {
List<VoteEntity> pollVotes = voteRepository.findAllByPollId(pollId);
return countVotes(pollId, pollVotes);
}

private VoteInfo countVotes(long pollId, List<VoteEntity> voteEntities) {
Map<VoteTeam, Long> teamCount = voteEntities.stream()
.collect(Collectors.groupingBy(VoteEntity::getTeam, Collectors.counting()));
long prosCount = teamCount.getOrDefault(VoteTeam.PROS, 0L);
long consCount = teamCount.getOrDefault(VoteTeam.CONS, 0L);
return new VoteInfo(pollId, prosCount, consCount);
List<Vote> pollVotes = voteRepository.findAllByPollId(pollId)
.stream()
.map(VoteEntity::toDomain)
.toList();
return new VoteInfo(pollId, pollVotes);
}

public boolean isExists(long pollId, ParticipateCode code) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package com.debatetimer.dto.poll.response;

import com.debatetimer.domain.poll.ParticipantName;
import com.debatetimer.domain.poll.Poll;
import com.debatetimer.domain.poll.PollStatus;
import com.debatetimer.domain.poll.VoteInfo;
import java.util.List;

public record PollInfoResponse(
long id,
Expand All @@ -11,7 +13,8 @@ public record PollInfoResponse(
String consTeamName,
long totalCount,
long prosCount,
long consCount
long consCount,
List<String> voterNames
) {

public PollInfoResponse(Poll poll, VoteInfo voteInfo) {
Expand All @@ -22,7 +25,10 @@ public PollInfoResponse(Poll poll, VoteInfo voteInfo) {
poll.getConsTeamName().getValue(),
voteInfo.getTotalCount(),
voteInfo.getProsCount(),
voteInfo.getConsCount()
voteInfo.getConsCount(),
voteInfo.getVoterNames().stream()
.map(ParticipantName::getValue)
.toList()
);
}
}
5 changes: 3 additions & 2 deletions src/main/java/com/debatetimer/entity/poll/VoteEntity.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.debatetimer.domain.poll.Vote;
import com.debatetimer.domain.poll.VoteTeam;
import com.debatetimer.entity.BaseTimeEntity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
Expand All @@ -28,7 +29,7 @@
})
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
public class VoteEntity {
public class VoteEntity extends BaseTimeEntity {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
Expand All @@ -55,6 +56,6 @@ public VoteEntity(Vote vote, PollEntity pollEntity) {
}

public Vote toDomain() {
return new Vote(id, poll.getId(), team, name, participateCode);
return new Vote(id, poll.getId(), team, name, participateCode, getCreatedAt());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
ALTER TABLE vote
ADD COLUMN created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP;

ALTER TABLE vote
ADD COLUMN modified_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP;

Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import com.debatetimer.dto.poll.response.PollInfoResponse;
import com.debatetimer.entity.customize.CustomizeTableEntity;
import com.debatetimer.entity.poll.PollEntity;
import com.debatetimer.entity.poll.VoteEntity;
import io.restassured.http.ContentType;
import io.restassured.http.Headers;
import org.junit.jupiter.api.Nested;
Expand Down Expand Up @@ -44,9 +45,9 @@ class GetPollInfo {
Member member = memberGenerator.generate("email@email.com");
CustomizeTableEntity table = customizeTableEntityGenerator.generate(member);
PollEntity pollEntity = pollEntityGenerator.generate(table, PollStatus.PROGRESS);
voteEntityGenerator.generate(pollEntity, VoteTeam.PROS, "콜리");
voteEntityGenerator.generate(pollEntity, VoteTeam.PROS, "비토");
voteEntityGenerator.generate(pollEntity, VoteTeam.CONS, "커찬");
VoteEntity voter1 = voteEntityGenerator.generate(pollEntity, VoteTeam.PROS, "콜리");
VoteEntity voter2 = voteEntityGenerator.generate(pollEntity, VoteTeam.PROS, "비토");
VoteEntity voter3 = voteEntityGenerator.generate(pollEntity, VoteTeam.CONS, "커찬");
Headers headers = headerGenerator.generateAccessTokenHeader(member);

PollInfoResponse response = given()
Expand All @@ -64,7 +65,9 @@ class GetPollInfo {
() -> assertThat(response.status()).isEqualTo(pollEntity.getStatus()),
() -> assertThat(response.totalCount()).isEqualTo(3L),
() -> assertThat(response.prosCount()).isEqualTo(2L),
() -> assertThat(response.consCount()).isEqualTo(1L)
() -> assertThat(response.consCount()).isEqualTo(1L),
() -> assertThat(response.voterNames()).containsExactly(
voter1.getName(), voter2.getName(), voter3.getName())
);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.Mockito.doReturn;
import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName;
import static org.springframework.restdocs.payload.JsonFieldType.ARRAY;
import static org.springframework.restdocs.payload.JsonFieldType.NUMBER;
import static org.springframework.restdocs.payload.JsonFieldType.STRING;
import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
Expand All @@ -18,6 +19,7 @@
import com.debatetimer.dto.poll.response.PollCreateResponse;
import com.debatetimer.dto.poll.response.PollInfoResponse;
import io.restassured.http.ContentType;
import java.util.List;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.http.HttpHeaders;
Expand Down Expand Up @@ -85,7 +87,8 @@ class GetPollInfo {
fieldWithPath("consTeamName").type(STRING).description("반대측 팀 이름"),
fieldWithPath("totalCount").type(NUMBER).description("전체 투표 수"),
fieldWithPath("prosCount").type(NUMBER).description("찬성 투표 수"),
fieldWithPath("consCount").type(NUMBER).description("반대 투표 수")
fieldWithPath("consCount").type(NUMBER).description("반대 투표 수"),
fieldWithPath("voterNames").type(ARRAY).description("투표자 이름 정보")
);

@Test
Expand All @@ -97,7 +100,8 @@ class GetPollInfo {
"반대",
3L,
2L,
1L
1L,
List.of("콜리", "비토", "커찬")
);
doReturn(response).when(pollService).getPollInfo(anyLong(), any(Member.class));

Expand Down Expand Up @@ -136,7 +140,8 @@ class FinishPoll {
fieldWithPath("consTeamName").type(STRING).description("반대측 팀 이름"),
fieldWithPath("totalCount").type(NUMBER).description("전체 투표 수"),
fieldWithPath("prosCount").type(NUMBER).description("찬성 투표 수"),
fieldWithPath("consCount").type(NUMBER).description("반대 투표 수")
fieldWithPath("consCount").type(NUMBER).description("반대 투표 수"),
fieldWithPath("voterNames").type(ARRAY).description("투표자 이름 정보")
);

@Test
Expand All @@ -148,7 +153,9 @@ class FinishPoll {
"반대",
3L,
2L,
1L
1L,
List.of("콜리", "비토", "커찬")

);
doReturn(response).when(pollService).finishPoll(anyLong(), any(Member.class));

Expand Down
39 changes: 39 additions & 0 deletions src/test/java/com/debatetimer/domain/poll/VoteInfoTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.debatetimer.domain.poll;

import static org.assertj.core.api.Assertions.assertThat;

import java.time.LocalDateTime;
import java.util.List;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;

class VoteInfoTest {

@Nested
class getVoterNames {

@Test
void 생성_순으로_투표자_이름을_정렬한다() {
LocalDateTime now = LocalDateTime.now();
LocalDateTime oneMinutesAgo = now.minusMinutes(1);
LocalDateTime twoMinutesAgo = now.minusMinutes(2);
long pollId = 1L;
Vote nowVote = new Vote(1L, pollId, VoteTeam.PROS, "콜리1", "code1", now);
Vote oneMinutesAgoVote = new Vote(2L, pollId, VoteTeam.PROS, "콜리2", "code2", oneMinutesAgo);
Vote twoMinutesAgoVote = new Vote(3L, pollId, VoteTeam.PROS, "콜리3", "code3", twoMinutesAgo);

VoteInfo voteInfo = new VoteInfo(pollId, List.of(nowVote, oneMinutesAgoVote, twoMinutesAgoVote));
List<String> voterNames = voteInfo.getVoterNames()
.stream()
.map(ParticipantName::getValue)
.toList();

assertThat(voterNames)
.containsExactly(
twoMinutesAgoVote.getName().getValue(),
oneMinutesAgoVote.getName().getValue(),
nowVote.getName().getValue()
);
}
}
}
Loading
Loading