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] 지출로그 추가 기능 구현 #45

Merged
merged 14 commits into from
Jul 21, 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
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,42 @@
package server.haengdong.application;

import java.util.List;
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 BillActionRepository billActionRepository;
private final ActionRepository actionRepository;
private final EventRepository eventRepository;

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

for (BillActionAppRequest request : requests) {
BillAction billAction = request.toBillAction(action);
billActionRepository.save(billAction);
action = action.next();
}
}

private Action createStartAction(Event event) {
return actionRepository.findLastByEvent(event)
.map(Action::next)
.orElse(Action.createFirst(event));
}
}
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);
}
}
15 changes: 15 additions & 0 deletions server/src/main/java/server/haengdong/domain/Action.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
@Entity
public class Action {

private static final long FIRST_SEQUENCE = 1L;

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
Expand All @@ -23,4 +25,17 @@ public class Action {
private Event event;

private Long sequence;

public Action(Event event, Long sequence) {
this.event = event;
this.sequence = sequence;
}

public static Action createFirst(Event event) {
return new Action(event, FIRST_SEQUENCE);
}

public Action next() {
return new Action(event, sequence + 1);
}
}
38 changes: 35 additions & 3 deletions server/src/main/java/server/haengdong/domain/BillAction.java
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
package server.haengdong.domain;

import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
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 @@ -16,14 +17,45 @@
@Entity
public class BillAction {

private static final int MIN_TITLE_LENGTH = 2;
private static final int MAX_TITLE_LENGTH = 30;
private static final long MIN_PRICE = 1L;
private static final long MAX_PRICE = 10_000_000L;

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@OneToOne(fetch = FetchType.LAZY)
@OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true)
private Action action;

@Column(length = MAX_TITLE_LENGTH)
private String title;

private BigDecimal price;
private Long price;

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

private void validateTitle(String title) {
int titleLength = title.trim().length();
if (titleLength < MIN_TITLE_LENGTH || titleLength > MAX_TITLE_LENGTH) {
throw new IllegalArgumentException("앞뒤 공백을 제거한 지출 내역 제목은 2 ~ 30자여야 합니다.");
}
}

private void validatePrice(Long price) {
if (price < MIN_PRICE || price > MAX_PRICE) {
throw new IllegalArgumentException("지출 금액은 10,000,000 이하의 자연수여야 합니다.");
}
}

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,15 @@
package server.haengdong.persistence;

import java.util.List;
import org.springframework.data.jpa.repository.EntityGraph;
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> {

@EntityGraph(attributePaths = {"action"})
List<BillAction> findByAction_Event(Event event);
Arachneee marked this conversation as resolved.
Show resolved Hide resolved
}
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
Arachneee marked this conversation as resolved.
Show resolved Hide resolved
) {
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,14 @@
package server.haengdong.presentation.request;

import jakarta.validation.Valid;
import java.util.List;
import server.haengdong.application.request.BillActionAppRequest;

public record BillActionsSaveRequest(@Valid List<BillActionSaveRequest> actions) {

public List<BillActionAppRequest> toAppRequests() {
Arachneee marked this conversation as resolved.
Show resolved Hide resolved
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
13 changes: 0 additions & 13 deletions server/src/test/java/server/haengdong/ServerApplicationTests.java

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
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.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("지출 내역을 생성한다.")
Arachneee marked this conversation as resolved.
Show resolved Hide resolved
@Test
void saveAllBillAction() {
String token = "TOKEN";
Event event = new Event("감자", token);
Event savedEvent = eventRepository.save(event);

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

billActionService.saveAllBillAction(token, requests);

List<BillAction> actions = billActionRepository.findByAction_Event(savedEvent);

assertThat(actions).extracting(BillAction::getTitle, BillAction::getPrice, BillAction::getSequence)
.containsExactlyInAnyOrder(
tuple("뽕족", 10_000L, 1L),
tuple("인생맥주", 15_000L, 2L)
);
}

@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("존재하지 않는 이벤트 토큰입니다.");
}
}
Loading
Loading