diff --git a/server/src/main/java/server/haengdong/application/ActionService.java b/server/src/main/java/server/haengdong/application/ActionService.java new file mode 100644 index 000000000..22ca43a6b --- /dev/null +++ b/server/src/main/java/server/haengdong/application/ActionService.java @@ -0,0 +1,38 @@ +package server.haengdong.application; + +import java.util.ArrayList; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import server.haengdong.application.response.MemberBillReportAppResponse; +import server.haengdong.domain.action.BillAction; +import server.haengdong.domain.action.BillActionRepository; +import server.haengdong.domain.action.MemberAction; +import server.haengdong.domain.action.MemberActionRepository; +import server.haengdong.domain.action.MemberBillReports; +import server.haengdong.domain.event.Event; +import server.haengdong.domain.event.EventRepository; + +@RequiredArgsConstructor +@Service +public class ActionService { + + private final BillActionRepository billActionRepository; + private final MemberActionRepository memberActionRepository; + private final EventRepository eventRepository; + + public List getMemberBillReports(String token) { + Event event = eventRepository.findByToken(token) + .orElseThrow(() -> new IllegalArgumentException("event not found")); + List billActions = billActionRepository.findByAction_Event(event); + List memberActions = memberActionRepository.findAllByEvent(event); + + MemberBillReports memberBillReports = MemberBillReports.createByActions(billActions, memberActions); + + List memberBillReportResponses = new ArrayList<>(); + memberBillReports.getReports().forEach( + (member, price) -> memberBillReportResponses.add(new MemberBillReportAppResponse(member, price)) + ); + return memberBillReportResponses; + } +} diff --git a/server/src/main/java/server/haengdong/application/response/MemberBillReportAppResponse.java b/server/src/main/java/server/haengdong/application/response/MemberBillReportAppResponse.java new file mode 100644 index 000000000..21b6cef56 --- /dev/null +++ b/server/src/main/java/server/haengdong/application/response/MemberBillReportAppResponse.java @@ -0,0 +1,4 @@ +package server.haengdong.application.response; + +public record MemberBillReportAppResponse(String name, Long price) { +} diff --git a/server/src/main/java/server/haengdong/domain/action/BillAction.java b/server/src/main/java/server/haengdong/domain/action/BillAction.java index 503715fa1..f0e82c409 100644 --- a/server/src/main/java/server/haengdong/domain/action/BillAction.java +++ b/server/src/main/java/server/haengdong/domain/action/BillAction.java @@ -15,7 +15,7 @@ @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) @Entity -public class BillAction { +public class BillAction implements Comparable { private static final int MIN_TITLE_LENGTH = 2; private static final int MAX_TITLE_LENGTH = 30; @@ -58,4 +58,9 @@ private void validatePrice(Long price) { public Long getSequence() { return action.getSequence(); } + + @Override + public int compareTo(BillAction o) { + return Long.compare(this.getSequence(), o.getSequence()); + } } diff --git a/server/src/main/java/server/haengdong/domain/action/MemberAction.java b/server/src/main/java/server/haengdong/domain/action/MemberAction.java index 6e5f0d805..af340de3f 100644 --- a/server/src/main/java/server/haengdong/domain/action/MemberAction.java +++ b/server/src/main/java/server/haengdong/domain/action/MemberAction.java @@ -16,7 +16,7 @@ @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) @Entity -public class MemberAction { +public class MemberAction implements Comparable { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -50,4 +50,9 @@ public boolean isSameStatus(MemberActionStatus memberActionStatus) { public Long getSequence() { return action.getSequence(); } + + @Override + public int compareTo(MemberAction o) { + return Long.compare(this.getSequence(), o.getSequence()); + } } diff --git a/server/src/main/java/server/haengdong/domain/action/MemberBillReports.java b/server/src/main/java/server/haengdong/domain/action/MemberBillReports.java new file mode 100644 index 000000000..aa928d2e4 --- /dev/null +++ b/server/src/main/java/server/haengdong/domain/action/MemberBillReports.java @@ -0,0 +1,83 @@ +package server.haengdong.domain.action; + +import static java.util.stream.Collectors.toMap; + +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.PriorityQueue; +import java.util.Set; +import java.util.function.Function; +import lombok.Getter; + +@Getter +public class MemberBillReports { + + private final Map reports; + + private MemberBillReports(Map reports) { + this.reports = reports; + } + + public static MemberBillReports createByActions(List billActions, List memberActions) { + PriorityQueue sortedBillActions = new PriorityQueue<>(billActions); + PriorityQueue sortedMemberActions = new PriorityQueue<>(memberActions); + + Map memberBillReports = initReports(memberActions); + Set currentMembers = new HashSet<>(); + + while (!sortedBillActions.isEmpty() && !sortedMemberActions.isEmpty()) { + if (isMemberActionTurn(sortedMemberActions, sortedBillActions)) { + addMemberAction(sortedMemberActions, currentMembers); + continue; + } + addBillAction(sortedBillActions, currentMembers, memberBillReports); + } + + while (!sortedBillActions.isEmpty()) { + addBillAction(sortedBillActions, currentMembers, memberBillReports); + } + + return new MemberBillReports(memberBillReports); + } + + private static Map initReports(List memberActions) { + return memberActions.stream() + .map(MemberAction::getMemberName) + .distinct() + .collect(toMap(Function.identity(), i -> 0L)); + } + + private static boolean isMemberActionTurn( + PriorityQueue memberActions, + PriorityQueue billActions + ) { + MemberAction memberAction = memberActions.peek(); + BillAction billAction = billActions.peek(); + + return memberAction.getSequence() < billAction.getSequence(); + } + + private static void addMemberAction(PriorityQueue sortedMemberActions, Set currentMembers) { + MemberAction memberAction = sortedMemberActions.poll(); + String memberName = memberAction.getMemberName(); + if (memberAction.isSameStatus(MemberActionStatus.IN)) { + currentMembers.add(memberName); + return; + } + currentMembers.remove(memberAction.getMemberName()); + } + + private static void addBillAction( + PriorityQueue sortedBillActions, + Set currentMembers, + Map memberBillReports + ) { + BillAction billAction = sortedBillActions.poll(); + Long pricePerMember = billAction.getPrice() / currentMembers.size(); + for (String currentMember : currentMembers) { + Long price = memberBillReports.getOrDefault(currentMember, 0L) + pricePerMember; + memberBillReports.put(currentMember, price); + } + } +} diff --git a/server/src/main/java/server/haengdong/presentation/ActionController.java b/server/src/main/java/server/haengdong/presentation/ActionController.java new file mode 100644 index 000000000..fbde65a24 --- /dev/null +++ b/server/src/main/java/server/haengdong/presentation/ActionController.java @@ -0,0 +1,26 @@ +package server.haengdong.presentation; + +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RestController; +import server.haengdong.application.ActionService; +import server.haengdong.application.response.MemberBillReportAppResponse; +import server.haengdong.presentation.response.MemberBillReportsResponse; + +@RequiredArgsConstructor +@RestController +public class ActionController { + + private final ActionService actionService; + + @GetMapping("/api/events/{token}/actions/reports") + public ResponseEntity getMemberBillReports(@PathVariable("token") String token) { + List memberBillReports = actionService.getMemberBillReports(token); + + return ResponseEntity.ok() + .body(MemberBillReportsResponse.of(memberBillReports)); + } +} diff --git a/server/src/main/java/server/haengdong/presentation/response/MemberBillReportResponse.java b/server/src/main/java/server/haengdong/presentation/response/MemberBillReportResponse.java new file mode 100644 index 000000000..0ea409202 --- /dev/null +++ b/server/src/main/java/server/haengdong/presentation/response/MemberBillReportResponse.java @@ -0,0 +1,10 @@ +package server.haengdong.presentation.response; + +import server.haengdong.application.response.MemberBillReportAppResponse; + +public record MemberBillReportResponse(String name, Long price) { + + public static MemberBillReportResponse of(MemberBillReportAppResponse memberBillReportAppResponse) { + return new MemberBillReportResponse(memberBillReportAppResponse.name(), memberBillReportAppResponse.price()); + } +} diff --git a/server/src/main/java/server/haengdong/presentation/response/MemberBillReportsResponse.java b/server/src/main/java/server/haengdong/presentation/response/MemberBillReportsResponse.java new file mode 100644 index 000000000..d350c4009 --- /dev/null +++ b/server/src/main/java/server/haengdong/presentation/response/MemberBillReportsResponse.java @@ -0,0 +1,15 @@ +package server.haengdong.presentation.response; + +import java.util.List; +import server.haengdong.application.response.MemberBillReportAppResponse; + +public record MemberBillReportsResponse(List reports) { + + public static MemberBillReportsResponse of(List memberBillReports) { + List reports = memberBillReports.stream() + .map(MemberBillReportResponse::of) + .toList(); + + return new MemberBillReportsResponse(reports); + } +} diff --git a/server/src/test/java/server/haengdong/application/ActionServiceTest.java b/server/src/test/java/server/haengdong/application/ActionServiceTest.java new file mode 100644 index 000000000..3680b10d5 --- /dev/null +++ b/server/src/test/java/server/haengdong/application/ActionServiceTest.java @@ -0,0 +1,68 @@ +package server.haengdong.application; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.tuple; +import static server.haengdong.domain.action.MemberActionStatus.IN; +import static server.haengdong.domain.action.MemberActionStatus.OUT; + +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import server.haengdong.application.response.MemberBillReportAppResponse; +import server.haengdong.domain.action.Action; +import server.haengdong.domain.action.BillAction; +import server.haengdong.domain.action.BillActionRepository; +import server.haengdong.domain.action.MemberAction; +import server.haengdong.domain.action.MemberActionRepository; +import server.haengdong.domain.event.Event; +import server.haengdong.domain.event.EventRepository; + +@SpringBootTest +class ActionServiceTest { + + @Autowired + private ActionService actionService; + + @Autowired + private EventRepository eventRepository; + + @Autowired + private BillActionRepository billActionRepository; + + @Autowired + private MemberActionRepository memberActionRepository; + + @DisplayName("참여자별 정산 현황을 조회한다.") + @Test + void getMemberBillReports() { + String token = "tOkEn1"; + Event event = new Event("행동대장", token); + Event savedEvent = eventRepository.save(event); + List memberActions = List.of( + new MemberAction(new Action(savedEvent, 1L), "소하", IN, 1L), + new MemberAction(new Action(savedEvent, 2L), "감자", IN, 1L), + new MemberAction(new Action(savedEvent, 3L), "쿠키", IN, 1L), + new MemberAction(new Action(savedEvent, 5L), "감자", OUT, 2L) + ); + List billActions = List.of( + new BillAction(new Action(savedEvent, 4L), "뽕족", 60_000L), + new BillAction(new Action(savedEvent, 6L), "인생맥주", 40_000L), + new BillAction(new Action(savedEvent, 7L), "인생네컷", 20_000L) + ); + memberActionRepository.saveAll(memberActions); + billActionRepository.saveAll(billActions); + + List responses = actionService.getMemberBillReports(token); + + assertThat(responses) + .hasSize(3) + .extracting(MemberBillReportAppResponse::name, MemberBillReportAppResponse::price) + .containsExactlyInAnyOrder( + tuple("감자", 20_000L), + tuple("쿠키", 50_000L), + tuple("소하", 50_000L) + ); + } +} diff --git a/server/src/test/java/server/haengdong/domain/action/MemberBillReportsTest.java b/server/src/test/java/server/haengdong/domain/action/MemberBillReportsTest.java new file mode 100644 index 000000000..db0e2ff37 --- /dev/null +++ b/server/src/test/java/server/haengdong/domain/action/MemberBillReportsTest.java @@ -0,0 +1,43 @@ +package server.haengdong.domain.action; + +import static org.assertj.core.api.Assertions.assertThat; +import static server.haengdong.domain.action.MemberActionStatus.IN; +import static server.haengdong.domain.action.MemberActionStatus.OUT; + +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import server.haengdong.domain.event.Event; + +class MemberBillReportsTest { + + @DisplayName("액션 목록으로 참가자 정산 리포트를 생성한다.") + @Test + void createByActions() { + String token = "TOK2N"; + Event event = new Event("행동대장", token); + List billActions = List.of( + new BillAction(new Action(event, 4L), "뽕족", 60_000L), + new BillAction(new Action(event, 6L), "인생맥주", 40_000L), + new BillAction(new Action(event, 7L), "인생네컷", 20_000L) + ); + List memberActions = List.of( + new MemberAction(new Action(event, 1L), "소하", IN, 1L), + new MemberAction(new Action(event, 2L), "감자", IN, 1L), + new MemberAction(new Action(event, 3L), "쿠키", IN, 1L), + new MemberAction(new Action(event, 5L), "감자", OUT, 2L) + ); + + MemberBillReports memberBillReports = MemberBillReports.createByActions(billActions, memberActions); + + assertThat(memberBillReports.getReports()) + .containsAllEntriesOf( + Map.of( + "감자", 20_000L, + "쿠키", 50_000L, + "소하", 50_000L + ) + ); + } +} diff --git a/server/src/test/java/server/haengdong/presentation/ActionControllerTest.java b/server/src/test/java/server/haengdong/presentation/ActionControllerTest.java new file mode 100644 index 000000000..739ac4962 --- /dev/null +++ b/server/src/test/java/server/haengdong/presentation/ActionControllerTest.java @@ -0,0 +1,49 @@ +package server.haengdong.presentation; + +import static org.hamcrest.Matchers.equalTo; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.result.MockMvcResultMatchers; +import server.haengdong.application.ActionService; +import server.haengdong.application.response.MemberBillReportAppResponse; + +@WebMvcTest(ActionController.class) +class ActionControllerTest { + + @Autowired + private MockMvc mockMvc; + + @MockBean + private ActionService actionService; + + @DisplayName("참여자별 정산 현황을 조회한다.") + @Test + void getMemberBillReports() throws Exception { + List memberBillReportAppResponses = List.of( + new MemberBillReportAppResponse("소하", 20_000L), new MemberBillReportAppResponse("토다리", 200_000L)); + + given(actionService.getMemberBillReports(any())).willReturn(memberBillReportAppResponses); + + mockMvc.perform(get("/api/events/{token}/actions/reports", "token") + .accept(MediaType.APPLICATION_JSON)) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(MockMvcResultMatchers.jsonPath("$.reports[0].name").value(equalTo("소하"))) + .andExpect(MockMvcResultMatchers.jsonPath("$.reports[0].price").value(equalTo(20_000))) + .andExpect(MockMvcResultMatchers.jsonPath("$.reports[1].name").value(equalTo("토다리"))) + .andExpect(MockMvcResultMatchers.jsonPath("$.reports[1].price").value(equalTo(200_000))); + + } +}