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
33 changes: 33 additions & 0 deletions src/main/java/com/debatetimer/config/ErrorDecoderConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.debatetimer.config;

import com.debatetimer.exception.decoder.H2ErrorDecoder;
import com.debatetimer.exception.decoder.MySqlErrorDecoder;
import com.debatetimer.exception.decoder.RepositoryErrorDecoder;
import lombok.NoArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;

@NoArgsConstructor(access = lombok.AccessLevel.PRIVATE)
public class ErrorDecoderConfig {

@Profile({"dev", "prod"})
@Configuration
public static class MySqlErrorDecoderConfig {

@Bean
public RepositoryErrorDecoder mySqlErrorDecoder() {
return new MySqlErrorDecoder();
}
}

@Profile({"test", "local"})
@Configuration
public static class H2ErrorDecoderConfig {

@Bean
public RepositoryErrorDecoder h2ErrorDecoder() {
return new H2ErrorDecoder();
}
}
}
37 changes: 37 additions & 0 deletions src/main/java/com/debatetimer/controller/poll/VoteController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.debatetimer.controller.poll;

import com.debatetimer.dto.poll.request.VoteRequest;
import com.debatetimer.dto.poll.response.VoteCreateResponse;
import com.debatetimer.dto.poll.response.VoterPollInfoResponse;
import com.debatetimer.service.poll.VoteService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequiredArgsConstructor
public class VoteController {

private final VoteService voteService;

@GetMapping("/api/polls/{pollId}/votes")
@ResponseStatus(HttpStatus.OK)
public VoterPollInfoResponse getVotersPollInfo(@PathVariable long pollId) {
return voteService.getVoterPollInfo(pollId);
}

@PostMapping("/api/polls/{pollId}/votes")
@ResponseStatus(HttpStatus.CREATED)
public VoteCreateResponse votePoll(
@PathVariable long pollId,
@RequestBody @Valid VoteRequest voteRequest
) {
return voteService.vote(pollId, voteRequest);
}
}
12 changes: 4 additions & 8 deletions src/main/java/com/debatetimer/domain/customize/Bell.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,18 @@ public class Bell {

public static final int MAX_BELL_COUNT = 3;

private final BellType type;
private final int time;
private final int count;

public Bell(int time, int count) {
validateTime(time);
public Bell(BellType type, int time, int count) {
type.validateTime(time);
validateCount(count);
this.type = type;
this.time = time;
this.count = count;
}

private void validateTime(int time) {
if (time < 0) {
throw new DTClientErrorException(ClientErrorCode.INVALID_BELL_TIME);
}
}

private void validateCount(int count) {
if (count <= 0 || count > MAX_BELL_COUNT) {
throw new DTClientErrorException(ClientErrorCode.INVALID_BELL_COUNT);
Expand Down
25 changes: 25 additions & 0 deletions src/main/java/com/debatetimer/domain/customize/BellType.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.debatetimer.domain.customize;

import com.debatetimer.exception.custom.DTClientErrorException;
import com.debatetimer.exception.errorcode.ClientErrorCode;
import java.util.function.IntPredicate;

public enum BellType {

AFTER_START(time -> time >= 0),
Copy link
Contributor

@coli-geonwoo coli-geonwoo Jul 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[질문]

이거 타입 + 양수 시간으로 나타내자고 하지 않았나요? 왜 AFTER_END에서는 음수죠? 치코랑 협의가 된 부분인지 궁금해요.

AFTER_END 30 이면 끝난 후 30초 등등으로 해석하기로 했던 것 같은데요?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이건 콜리의 실수였던거로...

BEFORE_END(time -> time >= 0),
AFTER_END(time -> time <= 0),
;

private final IntPredicate timeValidator;

BellType(IntPredicate timeValidator) {
this.timeValidator = timeValidator;
}

public void validateTime(int time) {
if (!timeValidator.test(time)) {
throw new DTClientErrorException(ClientErrorCode.INVALID_BELL_TIME);
}
}
}
2 changes: 1 addition & 1 deletion src/main/java/com/debatetimer/domain/member/Member.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.debatetimer.domain.member;

import com.debatetimer.entity.customize.BaseTimeEntity;
import com.debatetimer.entity.BaseTimeEntity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
Expand Down
4 changes: 4 additions & 0 deletions src/main/java/com/debatetimer/domain/poll/Poll.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,8 @@ public Poll(Long id, long tableId, long memberId, PollStatus status,
String prosTeamName, String consTeamName, String agenda) {
this(id, tableId, memberId, status, new TeamName(prosTeamName), new TeamName(consTeamName), new Agenda(agenda));
}

public boolean isProgress() {
return status.isProgress();
}
}
4 changes: 4 additions & 0 deletions src/main/java/com/debatetimer/domain/poll/PollStatus.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,8 @@ public enum PollStatus {
PROGRESS,
DONE,
;

public boolean isProgress() {
return this == PROGRESS;
}
}
4 changes: 4 additions & 0 deletions src/main/java/com/debatetimer/domain/poll/Vote.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ public class Vote {
private final ParticipantName name;
private final ParticipateCode code;

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

public Vote(Long id, long pollId, VoteTeam team, String name, String code) {
this(id, pollId, team, new ParticipantName(name), new ParticipateCode(code));
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
package com.debatetimer.service.customize;
package com.debatetimer.domainrepository.customize;

import com.debatetimer.domain.customize.CustomizeTable;
import com.debatetimer.domain.customize.CustomizeTimeBox;
import com.debatetimer.domain.member.Member;
import com.debatetimer.dto.customize.request.CustomizeTableCreateRequest;
import com.debatetimer.dto.customize.response.CustomizeTableResponse;
import com.debatetimer.entity.customize.BellEntity;
import com.debatetimer.entity.customize.CustomizeTableEntity;
import com.debatetimer.entity.customize.CustomizeTimeBoxEntities;
Expand All @@ -15,84 +13,77 @@
import java.util.List;
import java.util.stream.IntStream;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;

@Service
@Repository
@RequiredArgsConstructor
public class CustomizeServiceV2 {
public class CustomizeTableDomainRepository {

private final CustomizeTableRepository tableRepository;
private final CustomizeTimeBoxRepository timeBoxRepository;
private final BellRepository bellRepository;

@Transactional
public CustomizeTableResponse save(CustomizeTableCreateRequest tableCreateRequest, Member member) {
CustomizeTable table = tableCreateRequest.toTable(member);
List<CustomizeTimeBox> timeBoxes = tableCreateRequest.toTimeBoxList();

public CustomizeTable save(CustomizeTable table, List<CustomizeTimeBox> timeBoxes) {
CustomizeTableEntity savedTableEntity = tableRepository.save(new CustomizeTableEntity(table));
saveTimeBoxes(savedTableEntity, timeBoxes);
return new CustomizeTableResponse(savedTableEntity.toDomain(), timeBoxes);

return savedTableEntity.toDomain();
}

private void saveTimeBoxes(CustomizeTableEntity tableEntity, List<CustomizeTimeBox> timeBoxes) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[질문]

아마 비토가 작성한 코드는 아닌 것 같은데, 이거 sequence mapping을 왜 domain repository에서 해주나요?

도메인이 sequence 자체를 들고 있으면 더 좋을 것 같아요.
혹은 service에서의 매핑이나 CustomizeTimeBoxes라는 일급컬렉션 도메인 객체도 고려해보면 좋을 것 같아서요..
도메인 레포지토리는 '순차대로 입력받는다' 라는 웹 프론트와 백엔드간의 협의사실을 모르게하는게 맞다고 생각하는데 비토 의견은 어떤지 궁금합니다.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CustomizeTimeBoxes를 고민 안 해봤던 건 아닌데 문제는 그 IntStream을 사용하는 로직을 CustomizeTimeBoxes안에 만드는 방법이 안보였습니다. 그래서 콜리도 이전에 해당 로직을 dto에 뒀던 거로 기억해요.
이번에 4계층을 적용하면서 해당 로직을 dto or domain repository 둘 중 어디에 둬야 될지 선택을 했어야 했는데 저는 dto보단 domain repository가 더 맘에 들어서 이곳에 위치시켰습니다.

  1. 말한대로 '순차대로 입력받는다' 이건 프론트와 백엔드간의 협의 사실인데 저는 이런 규칙은 도메인 규칙이라고 생각합니다. 그런데 dto가 도메인 규칙을 알고 있다는 것보단 domain repository에서 매핑하는게 더 알맞다고 생각했어요.
  2. 지금 코드에서 domain repository가 프론트와 백엔드간의 협의사실을 알고 있다고 생각되지 않았어요. 말한대로 프론트와 백엔드간의 협의 사실은 서비스 단까지만 알아야 된다고 생각합니다. 그런데 지금 파라미터로 넘겨주는 것은 dto가 아닌 domain입니다. 그래서 지금 domain repository는 프론트와 백엔드간의 협의 사실을 안다기보단 서비스에서 순서대로 정렬해서 보냈으니 저장할때 순서를 추가해준다 정도로 봤습니다.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. 만약 순차대로 입력받는다가 도메인 규칙이라면 일전에 회의에서 도메인 규칙을 domain repository는 모르게 하고 domain 매핑 위주의 레포지토리 역할만 수행하게 한다에서 어긋나는 것 같은데 어느정도까지 도메인 규칙을 domain repository에게 위임가능하다고 생각하나요? 제가 궁금한건 메서드 명은 레포지토리처럼 유지하되 도메인 규칙을 넘기는 비일관성에 대한 문제입니다.

또한, 상위 계층에서 dto -> domain을 할 때에 순차적으로 매핑하여 보내주어야 함이라는 것이 도메인 레포지토리 안의 private method를 보아야지만 알 수 있는 것이 바람직하지 않다고 생각합니다.

따라서 백엔드-프론트 간의 API 규약에 대해서 매핑하는 것은 dto가 처리하는게 맞지 않는가? 가 제 입장입니다.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

만약 순차대로 입력받는다가 도메인 규칙이라면 일전에 회의에서 도메인 규칙을 domain repository는 모르게 하고 domain 매핑 위주의 레포지토리 역할만 수행하게 한다에서 어긋나는 것 같은데 어느정도까지 도메인 규칙을 domain repository에게 위임가능하다고 생각하나요? 제가 궁금한건 메서드 명은 레포지토리처럼 유지하되 도메인 규칙을 넘기는 비일관성에 대한 문제입니다.
또한, 상위 계층에서 dto -> domain을 할 때에 순차적으로 매핑하여 보내주어야 함이라는 것이 도메인 레포지토리 안의 private method를 보아야지만 알 수 있는 것이 바람직하지 않다고 생각합니다.
따라서 백엔드-프론트 간의 API 규약에 대해서 매핑하는 것은 dto가 처리하는게 맞지 않는가? 가 제 입장입니다.

제 답변의 2번에서 언급했든이 순차대로 입력받는다는 도메인 규칙을 도메인 레포지토리가 모르는 상태라고 생각하고 있습니다. 그래서 비일관성이라는 점은 제 기준에서는 아니라는 점을 먼저 언급하고 싶네요.
다만 상위 계층에서 dto -> domain을 할 때에 순차적으로 매핑하여 보내주어야 함이라는 것이 도메인 레포지토리 안의 private method를 보아야지만 알 수 있는 것이 바람직하지 않다고 생각합니다. 이 부분은 뼈맞았네요. 좀 더 고민해보겠습니다.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

제가 이것을 보고도 그냥 넘어갔던 이유는... List가 순서가 매겨져 있기 때문이라고 생각해요. 각 타임박스의 순서가 정해져있기 때문에 'List'라는 도메인을 저장한다면 순서에 맞춰 저장할 것이라고 충분히 생각할 수 있다 생각했어요. 제 관점에서 본다면 List 보다 CustomizeTimeBoxes 와 같은 Wrapper 도메인 객체를 만들어 저장했다면, 비토의 현재 로직이 합리적이지 않았을까 생각합니다.

상위 계층에서 dto -> domain을 할 때에 순차적으로 매핑하여 보내주어야 함이라는 것이 도메인 레포지토리 안의 private method를 보아야지만 알 수 있는 것이 바람직하지 않다고 생각합니다.

자료 구조가 Set 도 아니고 List라면... 이걸 매핑하는 개발자가 역순이나 섞어서 매핑하려 하지는 않을 것 같긴 해요. "순차적으로 매핑하여 보내주어야 함"을 얼마나 강조하고 싶은지의 문제일 수는 있겠네요.

제 최종 의견은... 자료 구조가 List 라는 것만으로도 충분히 '순서를 보장해야 한다'라는 이미지를 줄 수 있다 생각해요. (만약에 그렇지 않은 경우가 있을 때, 순서대로 매핑하는 것을 방지할 수 있는 장치가 있어야 한다고 생각합니다.)
콜리의 의견도 납득이 되기는 하지만, 그것에 맞춘 대안이 잘 떠오르지는 않네요. "도메인이 sequence를 가지게 한다"라는 의견을 제시했는데요. 저는 List 내부의 인덱스와 중복되는 느낌을 강하게 받긴 합니다.

IntStream.range(0, timeBoxes.size())
.forEach(i -> saveTimeBox(tableEntity, timeBoxes.get(i), i + 1));
}

private void saveTimeBox(CustomizeTableEntity tableEntity, CustomizeTimeBox timeBox, int sequence) {
CustomizeTimeBoxEntity timeBoxEntity = timeBoxRepository.save(
new CustomizeTimeBoxEntity(tableEntity, timeBox, sequence));
timeBox.getBells()
.forEach(bell -> bellRepository.save(new BellEntity(timeBoxEntity, bell)));
}

@Transactional(readOnly = true)
public CustomizeTableResponse findTable(long tableId, Member member) {
public CustomizeTable getByIdAndMember(long tableId, Member member) {
return tableRepository.getByIdAndMember(tableId, member)
.toDomain();
}

@Transactional(readOnly = true)
public List<CustomizeTimeBox> getCustomizeTimeBoxes(long tableId, Member member) {
CustomizeTableEntity tableEntity = tableRepository.getByIdAndMember(tableId, member);
List<CustomizeTimeBoxEntity> timeBoxEntityList = timeBoxRepository.findAllByCustomizeTable(tableEntity);
List<BellEntity> bellEntityList = bellRepository.findAllByCustomizeTimeBoxIn(timeBoxEntityList);
CustomizeTimeBoxEntities timeBoxEntities = new CustomizeTimeBoxEntities(timeBoxEntityList, bellEntityList);

return new CustomizeTableResponse(tableEntity.toDomain(), timeBoxEntities.toDomain());
return timeBoxEntities.toDomain();
}

@Transactional
public CustomizeTableResponse updateTable(
CustomizeTableCreateRequest tableCreateRequest,
long tableId,
Member member
) {
public CustomizeTable update(CustomizeTable table, long tableId, Member member, List<CustomizeTimeBox> timeBoxes) {
CustomizeTableEntity tableEntity = tableRepository.getByIdAndMember(tableId, member);
tableEntity.updateTable(tableCreateRequest.toTable(member));
tableEntity.updateTable(table);

bellRepository.deleteAllByTable(tableEntity.getId());
timeBoxRepository.deleteAllByTable(tableEntity.getId());
List<CustomizeTimeBox> timeBoxes = tableCreateRequest.toTimeBoxList();

saveTimeBoxes(tableEntity, timeBoxes);
return new CustomizeTableResponse(tableEntity.toDomain(), timeBoxes);
return tableEntity.toDomain();
}

@Transactional
public CustomizeTableResponse updateUsedAt(long tableId, Member member) {
public CustomizeTable updateUsedAt(long tableId, Member member) {
CustomizeTableEntity tableEntity = tableRepository.getByIdAndMember(tableId, member);
List<CustomizeTimeBoxEntity> timeBoxEntityList = timeBoxRepository.findAllByCustomizeTable(tableEntity);
List<BellEntity> bellEntityList = bellRepository.findAllByCustomizeTimeBoxIn(timeBoxEntityList);
CustomizeTimeBoxEntities timeBoxEntities = new CustomizeTimeBoxEntities(timeBoxEntityList, bellEntityList);

tableEntity.updateUsedAt();
CustomizeTable table = tableEntity.toDomain();
List<CustomizeTimeBox> timeBoxes = timeBoxEntities.toDomain();
return new CustomizeTableResponse(table, timeBoxes);
return tableEntity.toDomain();
}

@Transactional
public void deleteTable(long tableId, Member member) {
public void delete(long tableId, Member member) {
CustomizeTableEntity table = tableRepository.getByIdAndMember(tableId, member);

bellRepository.deleteAllByTable(table.getId());
timeBoxRepository.deleteAllByTable(table.getId());
tableRepository.delete(table);
}

private void saveTimeBoxes(CustomizeTableEntity tableEntity, List<CustomizeTimeBox> timeBoxes) {
IntStream.range(0, timeBoxes.size())
.forEach(i -> saveTimeBox(tableEntity, timeBoxes.get(i), i + 1));
}

private void saveTimeBox(CustomizeTableEntity tableEntity, CustomizeTimeBox timeBox, int sequence) {
CustomizeTimeBoxEntity timeBoxEntity = timeBoxRepository.save(
new CustomizeTimeBoxEntity(tableEntity, timeBox, sequence));
timeBox.getBells()
.forEach(bell -> bellRepository.save(new BellEntity(timeBoxEntity, bell)));
}
}

This file was deleted.

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

import com.debatetimer.domain.poll.Poll;
import com.debatetimer.entity.poll.PollEntity;
import com.debatetimer.exception.custom.DTClientErrorException;
import com.debatetimer.exception.errorcode.ClientErrorCode;
import com.debatetimer.repository.poll.PollRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;
Expand All @@ -24,19 +22,20 @@ public Poll create(Poll poll) {

@Transactional(readOnly = true)
public Poll getByIdAndMemberId(long id, long memberId) {
return findPoll(id, memberId)
return pollRepository.getByIdAndMemberId(id, memberId)
.toDomain();
}

@Transactional(readOnly = true)
public Poll getById(long id) {
return pollRepository.getById(id)
.toDomain();
}

@Transactional
public Poll finishPoll(long pollId, long memberId) {
PollEntity pollEntity = findPoll(pollId, memberId);
PollEntity pollEntity = pollRepository.getByIdAndMemberId(pollId, memberId);
pollEntity.updateToDone();
return pollEntity.toDomain();
}

private PollEntity findPoll(long pollId, long memberId) {
return pollRepository.findByIdAndMemberId(pollId, memberId)
.orElseThrow(() -> new DTClientErrorException(ClientErrorCode.POLL_NOT_FOUND));
}
}
Loading