Skip to content

Commit

Permalink
feat: 지출 내역 추가 기능 구현
Browse files Browse the repository at this point in the history
  • Loading branch information
Arachneee committed Jul 18, 2024
1 parent bd1b976 commit 8917865
Show file tree
Hide file tree
Showing 16 changed files with 320 additions and 6 deletions.
1 change: 1 addition & 0 deletions server/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ repositories {
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-validation'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.h2database:h2'
runtimeOnly 'com.mysql:mysql-connector-j'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package server.haengdong.application;

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import server.haengdong.application.request.BillActionAppRequest;
import server.haengdong.domain.Action;
import server.haengdong.domain.BillAction;
import server.haengdong.domain.Event;
import server.haengdong.persistence.ActionRepository;
import server.haengdong.persistence.BillActionRepository;
import server.haengdong.persistence.EventRepository;

@RequiredArgsConstructor
@Transactional(readOnly = true)
@Service
public class BillActionService {

private final EventRepository eventRepository;
private final ActionRepository actionRepository;
private final BillActionRepository billActionRepository;

@Transactional
public void saveAllBillAction(String eventToken, List<BillActionAppRequest> requests) {
Event event = eventRepository.findByToken(eventToken)
.orElseThrow(() -> new IllegalArgumentException("존재하지 않는 이벤트 토큰입니다."));
long lastSequence = getLastSequence(event);

List<BillAction> billActions = new ArrayList<>();
for (BillActionAppRequest request : requests) {
Action action = new Action(event, ++lastSequence);
BillAction billAction = request.toBillAction(action);
billActions.add(billAction);
}
billActionRepository.saveAll(billActions);
}

private long getLastSequence(Event event) {
Optional<Action> lastAction = actionRepository.findLastByEvent(event);
if (lastAction.isPresent()) {
return lastAction.get().getSequence();
}
return 0L;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,6 @@ public EventAppResponse saveEvent(EventAppRequest request) {
Event event = request.toEvent(token);
eventRepository.save(event);

return EventAppResponse.of(event);
return EventAppResponse.of(event);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package server.haengdong.application.request;

import server.haengdong.domain.Action;
import server.haengdong.domain.BillAction;

public record BillActionAppRequest(
String title,
Long price
) {

public BillAction toBillAction(Action action) {
return new BillAction(action, title, price);
}
}
5 changes: 5 additions & 0 deletions server/src/main/java/server/haengdong/domain/Action.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,9 @@ public class Action {
private Event event;

private Long sequence;

public Action(Event event, Long sequence) {
this.event = event;
this.sequence = sequence;
}
}
17 changes: 13 additions & 4 deletions server/src/main/java/server/haengdong/domain/BillAction.java
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
package server.haengdong.domain;

import jakarta.persistence.CascadeType;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.OneToOne;
import java.math.BigDecimal;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
Expand All @@ -20,10 +19,20 @@ public class BillAction {
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@OneToOne(fetch = FetchType.LAZY)
@OneToOne(cascade = CascadeType.PERSIST)
private Action action;

private String title;

private BigDecimal price;
private Long price;

public BillAction(Action action, String title, Long price) {
this.action = action;
this.title = title;
this.price = price;
}

public Long getSequence() {
return action.getSequence();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package server.haengdong.persistence;

import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;
import server.haengdong.domain.Action;
import server.haengdong.domain.Event;

@Repository
public interface ActionRepository extends JpaRepository<Action, Long> {

@Query("""
SELECT a
FROM Action a
WHERE a.event = :event
ORDER BY a.sequence DESC
LIMIT 1
""")
Optional<Action> findLastByEvent(Event event);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package server.haengdong.persistence;

import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import server.haengdong.domain.BillAction;
import server.haengdong.domain.Event;

@Repository
public interface BillActionRepository extends JpaRepository<BillAction, Long> {

List<BillAction> findByAction_Event(Event event);
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
package server.haengdong.persistence;

import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import server.haengdong.domain.Event;

@Repository
public interface EventRepository extends JpaRepository<Event, Long> {

Optional<Event> findByToken(String token);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package server.haengdong.presentation;

import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
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.RestController;
import server.haengdong.application.BillActionService;
import server.haengdong.presentation.request.BillActionsSaveRequest;

@RequiredArgsConstructor
@RestController
public class BillActionController {

private final BillActionService billActionService;

@PostMapping("/api/events/{token}/actions/bills")
public ResponseEntity<Void> saveAllBillAction(
@PathVariable String token,
@RequestBody @Valid BillActionsSaveRequest request
) {
billActionService.saveAllBillAction(token, request.toAppRequests());

return ResponseEntity.ok()
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package server.haengdong.presentation.request;

import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Positive;
import jakarta.validation.constraints.Size;
import server.haengdong.application.request.BillActionAppRequest;

public record BillActionSaveRequest(

@NotNull
@Size(min = 2, max = 30)
String title,

@NotNull
@Positive
Long price
) {

public BillActionAppRequest toAppRequest() {
return new BillActionAppRequest(title, price);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package server.haengdong.presentation.request;

import java.util.List;
import server.haengdong.application.request.BillActionAppRequest;

public record BillActionsSaveRequest(List<BillActionSaveRequest> actions) {

public List<BillActionAppRequest> toAppRequests() {
return actions.stream()
.map(BillActionSaveRequest::toAppRequest)
.toList();
}
}
1 change: 0 additions & 1 deletion server/src/main/resources/application.properties

This file was deleted.

15 changes: 15 additions & 0 deletions server/src/main/resources/application.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
spring:
h2:
console:
enabled: true
path: /h2-console
datasource:
url: jdbc:h2:mem:database
jpa:
defer-datasource-initialization: true
show-sql: true
properties:
hibernate:
format_sql: true
hibernate:
ddl-auto: create-drop
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package server.haengdong.application;

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

import java.util.Comparator;
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.request.BillActionAppRequest;
import server.haengdong.domain.BillAction;
import server.haengdong.domain.Event;
import server.haengdong.persistence.BillActionRepository;
import server.haengdong.persistence.EventRepository;

@SpringBootTest
class BillActionServiceTest {

@Autowired
private BillActionService billActionService;

@Autowired
private EventRepository eventRepository;

@Autowired
private BillActionRepository billActionRepository;

@DisplayName("지출 내역을 생성한다.")
@Test
void saveAllBillAction() {
String token = "TOKEN";
Event event = eventRepository.save(new Event("감자", token));

List<BillActionAppRequest> requests = List.of(
new BillActionAppRequest("뽕족", 10_000L),
new BillActionAppRequest("인생맥주", 15_000L)
);

billActionService.saveAllBillAction(token, requests);

List<BillAction> actions = billActionRepository.findByAction_Event(event)
.stream()
.sorted(Comparator.comparing(BillAction::getSequence).reversed())
.limit(requests.size())
.toList();

assertThat(actions).extracting("title", "price")
.containsExactly(
tuple("인생맥주", 15_000L),
tuple("뽕족", 10_000L)
);
}

@DisplayName("이벤트가 존재하지 않으면 지출 내역을 생성할 수 없다.")
@Test
void saveAllBillAction1() {
List<BillActionAppRequest> requests = List.of(
new BillActionAppRequest("뽕족", 10_000L),
new BillActionAppRequest("인생맥주", 15_000L)
);

assertThatThrownBy(() -> billActionService.saveAllBillAction("token", requests))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("존재하지 않는 이벤트 토큰입니다.");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package server.haengdong.presentation;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doNothing;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import com.fasterxml.jackson.databind.ObjectMapper;
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 server.haengdong.application.BillActionService;
import server.haengdong.presentation.request.BillActionSaveRequest;
import server.haengdong.presentation.request.BillActionsSaveRequest;

@WebMvcTest(BillActionController.class)
class BillActionControllerTest {

@Autowired
private MockMvc mockMvc;

@Autowired
private ObjectMapper objectMapper;

@MockBean
private BillActionService billActionService;

@DisplayName("지출 내역을 생성한다.")
@Test
void saveAllBillAction() throws Exception {
BillActionsSaveRequest request = new BillActionsSaveRequest(
List.of(
new BillActionSaveRequest("뽕족", 10_000L),
new BillActionSaveRequest("인생맥주", 10_000L)
)
);
String requestBody = objectMapper.writeValueAsString(request);
String token = "TOKEN";
doNothing().when(billActionService).saveAllBillAction(any(), any());

mockMvc.perform(post("/api/events/{token}/actions/bills", token)
.contentType(MediaType.APPLICATION_JSON)
.content(requestBody))
.andDo(print())
.andExpect(status().isOk());
}
}

0 comments on commit 8917865

Please sign in to comment.