Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[BE] 참여자별 정산 현황 조회 기능 구현 #77

Merged
merged 1 commit into from
Jul 24, 2024
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
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 {
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
public class MemberBillReports {
public class MemberBillReport {

A: 단수형은 어떤가요?


private final Map<String, Long> reports;
Copy link
Contributor

Choose a reason for hiding this comment

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

R: Long을 vo로 분리하면 좋을 것 같아요. Money, MemberName 같이요 :)


private MemberBillReports(Map<String, Long> reports) {
this.reports = reports;
}
Comment on lines +18 to +20
Copy link
Contributor

@kunsanglee kunsanglee Jul 24, 2024

Choose a reason for hiding this comment

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

r: #70 pr에 있는 CurrentMembers 클래스에는 @RequiredArgsConstructor(access = AccessLevel.PRIVATE)를 사용하고, 이 곳에는 생성자를 사용했는데, 이 부분에 대해서 어떻게 생각하시나요?
제 생각에는 일반 객체가 아닌, 생성시 Spring Container에서 Bean 주입이 필요한 Entity, Controller, Service, Component 에만 사용하는 것이 좋아보입니다.

Copy link
Contributor Author

@Arachneee Arachneee Jul 24, 2024

Choose a reason for hiding this comment

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

저도 순수 자바 객체에는 롬복을 쓸 필요가 없어보입니다. POJO를 지향하는 것이 더 읽기 쉽고 유지보수에 좋을 것 같아요.


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());
Comment on lines +63 to +68
Copy link
Contributor

@kunsanglee kunsanglee Jul 24, 2024

Choose a reason for hiding this comment

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

Suggested change
String memberName = memberAction.getMemberName();
if (memberAction.isSameStatus(MemberActionStatus.IN)) {
currentMembers.add(memberName);
return;
}
currentMembers.remove(memberAction.getMemberName());
String memberName = memberAction.getMemberName();
if (memberAction.isSameStatus(MemberActionStatus.IN)) {
currentMembers.add(memberName);
return;
}
currentMembers.remove(memberAction);

r: 만들어 놓은 변수 사용하면 될 것 같아요.

}

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;
Copy link
Contributor

@kunsanglee kunsanglee Jul 24, 2024

Choose a reason for hiding this comment

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

r: memberBillReportsinitReports() 메서드에서 이미 0원으로 모두 초기화 한 상태인데, getOrDefault() 메서드를 사용할 필요가 있을까요?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

initReports() 를 만들고 수정을 안했네요. 수정하겠습니다.

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)));

}
}
Loading