Skip to content

Commit

Permalink
feat: 참여자별 정산 현황 조회 기능 구현
Browse files Browse the repository at this point in the history
  • Loading branch information
Arachneee committed Jul 23, 2024
1 parent e9ceb0f commit 92c5c56
Show file tree
Hide file tree
Showing 11 changed files with 348 additions and 2 deletions.
Original file line number Diff line number Diff line change
@@ -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<MemberBillReportAppResponse> getMemberBillReports(String token) {
Event event = eventRepository.findByToken(token)
.orElseThrow(() -> new IllegalArgumentException("event not found"));
List<BillAction> billActions = billActionRepository.findByAction_Event(event);
List<MemberAction> memberActions = memberActionRepository.findAllByEvent(event);

MemberBillReports memberBillReports = MemberBillReports.createByActions(billActions, memberActions);

List<MemberBillReportAppResponse> memberBillReportResponses = new ArrayList<>();
memberBillReports.getReports().forEach(
(member, price) -> memberBillReportResponses.add(new MemberBillReportAppResponse(member, price))
);
return memberBillReportResponses;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package server.haengdong.application.response;

public record MemberBillReportAppResponse(String name, Long price) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
public class BillAction {
public class BillAction implements Comparable<BillAction> {

private static final int MIN_TITLE_LENGTH = 2;
private static final int MAX_TITLE_LENGTH = 30;
Expand Down Expand Up @@ -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());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
public class MemberAction {
public class MemberAction implements Comparable<MemberAction> {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
Expand Down Expand Up @@ -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());
}
}
Original file line number Diff line number Diff line change
@@ -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<String, Long> reports;

private MemberBillReports(Map<String, Long> reports) {
this.reports = reports;
}

public static MemberBillReports createByActions(List<BillAction> billActions, List<MemberAction> memberActions) {
PriorityQueue<BillAction> sortedBillActions = new PriorityQueue<>(billActions);
PriorityQueue<MemberAction> sortedMemberActions = new PriorityQueue<>(memberActions);

Map<String, Long> memberBillReports = initReports(memberActions);
Set<String> 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<String, Long> initReports(List<MemberAction> memberActions) {
return memberActions.stream()
.map(MemberAction::getMemberName)
.distinct()
.collect(toMap(Function.identity(), i -> 0L));
}

private static boolean isMemberActionTurn(
PriorityQueue<MemberAction> memberActions,
PriorityQueue<BillAction> billActions
) {
MemberAction memberAction = memberActions.peek();
BillAction billAction = billActions.peek();

return memberAction.getSequence() < billAction.getSequence();
}

private static void addMemberAction(PriorityQueue<MemberAction> sortedMemberActions, Set<String> 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<BillAction> sortedBillActions,
Set<String> currentMembers,
Map<String, Long> 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);
}
}
}
Original file line number Diff line number Diff line change
@@ -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<MemberBillReportsResponse> getMemberBillReports(@PathVariable("token") String token) {
List<MemberBillReportAppResponse> memberBillReports = actionService.getMemberBillReports(token);

return ResponseEntity.ok()
.body(MemberBillReportsResponse.of(memberBillReports));
}
}
Original file line number Diff line number Diff line change
@@ -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());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package server.haengdong.presentation.response;

import java.util.List;
import server.haengdong.application.response.MemberBillReportAppResponse;

public record MemberBillReportsResponse(List<MemberBillReportResponse> reports) {

public static MemberBillReportsResponse of(List<MemberBillReportAppResponse> memberBillReports) {
List<MemberBillReportResponse> reports = memberBillReports.stream()
.map(MemberBillReportResponse::of)
.toList();

return new MemberBillReportsResponse(reports);
}
}
Original file line number Diff line number Diff line change
@@ -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<MemberAction> 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<BillAction> 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<MemberBillReportAppResponse> responses = actionService.getMemberBillReports(token);

assertThat(responses)
.hasSize(3)
.extracting(MemberBillReportAppResponse::name, MemberBillReportAppResponse::price)
.containsExactlyInAnyOrder(
tuple("감자", 20_000L),
tuple("쿠키", 50_000L),
tuple("소하", 50_000L)
);
}
}
Original file line number Diff line number Diff line change
@@ -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<BillAction> 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<MemberAction> 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
)
);
}
}
Original file line number Diff line number Diff line change
@@ -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<MemberBillReportAppResponse> 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)));

}
}

0 comments on commit 92c5c56

Please sign in to comment.