diff --git a/backend/src/docs/asciidoc/docs.adoc b/backend/src/docs/asciidoc/docs.adoc index 44aafefc3..407105045 100644 --- a/backend/src/docs/asciidoc/docs.adoc +++ b/backend/src/docs/asciidoc/docs.adoc @@ -169,13 +169,13 @@ include::{snippets}/city-controller-test/get-cities/http-request.adoc[] ==== 응답 include::{snippets}/city-controller-test/get-cities/http-response.adoc[] -== 경비 API +== 카테고리 API -=== 경비 조회 (/trips/:tripId/expense) +=== 경비 카테고리 조회 (GET /categories) ==== 요청 -include::{snippets}/Expense-controller-test/get-expenses/http-request.adoc[] +include::{snippets}/category-controller-test/get-expense-categories/http-request.adoc[] ==== 응답 -include::{snippets}/Expense-controller-test/get-expenses/http-response.adoc[] -include::{snippets}/Expense-controller-test/get-expenses/response-fields.adoc[] +include::{snippets}/category-controller-test/get-expense-categories/http-response.adoc[] +include::{snippets}/category-controller-test/get-expense-categories/response-fields.adoc[] diff --git a/backend/src/main/java/hanglog/category/Category.java b/backend/src/main/java/hanglog/category/domain/Category.java similarity index 95% rename from backend/src/main/java/hanglog/category/Category.java rename to backend/src/main/java/hanglog/category/domain/Category.java index 3984f12a6..411f4faee 100644 --- a/backend/src/main/java/hanglog/category/Category.java +++ b/backend/src/main/java/hanglog/category/domain/Category.java @@ -1,4 +1,4 @@ -package hanglog.category; +package hanglog.category.domain; import static lombok.AccessLevel.PROTECTED; @@ -24,8 +24,8 @@ public class Category extends BaseEntity { private Long id; @Column(nullable = false, length = 50) - private String korName; + private String engName; @Column(nullable = false, length = 50) - private String engName; + private String korName; } diff --git a/backend/src/main/java/hanglog/category/domain/repository/CategoryRepository.java b/backend/src/main/java/hanglog/category/domain/repository/CategoryRepository.java new file mode 100644 index 000000000..2c4069646 --- /dev/null +++ b/backend/src/main/java/hanglog/category/domain/repository/CategoryRepository.java @@ -0,0 +1,12 @@ +package hanglog.category.domain.repository; + +import hanglog.category.domain.Category; +import java.util.List; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; + +public interface CategoryRepository extends JpaRepository { + + @Query("SELECT c FROM Category c WHERE MOD(c.id, 100) = 0") + List findExpenseCategory(); +} diff --git a/backend/src/main/java/hanglog/category/CategoryResponse.java b/backend/src/main/java/hanglog/category/dto/CategoryResponse.java similarity index 64% rename from backend/src/main/java/hanglog/category/CategoryResponse.java rename to backend/src/main/java/hanglog/category/dto/CategoryResponse.java index 7f35bf766..57c10b60c 100644 --- a/backend/src/main/java/hanglog/category/CategoryResponse.java +++ b/backend/src/main/java/hanglog/category/dto/CategoryResponse.java @@ -1,5 +1,6 @@ -package hanglog.category; +package hanglog.category.dto; +import hanglog.category.domain.Category; import lombok.Getter; import lombok.RequiredArgsConstructor; @@ -8,14 +9,12 @@ public class CategoryResponse { private final Long id; - private final String korName; - private final String engName; + private final String name; public static CategoryResponse of(final Category category) { return new CategoryResponse( category.getId(), - category.getKorName(), - category.getEngName() + category.getKorName() ); } } diff --git a/backend/src/main/java/hanglog/category/presentation/CategoryController.java b/backend/src/main/java/hanglog/category/presentation/CategoryController.java new file mode 100644 index 000000000..f9e373d52 --- /dev/null +++ b/backend/src/main/java/hanglog/category/presentation/CategoryController.java @@ -0,0 +1,24 @@ +package hanglog.category.presentation; + +import hanglog.category.dto.CategoryResponse; +import hanglog.category.service.CategoryService; +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.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/categories") +public class CategoryController { + + private final CategoryService categoryService; + + @GetMapping + public ResponseEntity> getExpenseCategories() { + final List categoryResponses = categoryService.getExpenseCategories(); + return ResponseEntity.ok(categoryResponses); + } +} diff --git a/backend/src/main/java/hanglog/category/repository/CategoryRepository.java b/backend/src/main/java/hanglog/category/repository/CategoryRepository.java deleted file mode 100644 index 64814b93b..000000000 --- a/backend/src/main/java/hanglog/category/repository/CategoryRepository.java +++ /dev/null @@ -1,7 +0,0 @@ -package hanglog.category.repository; - -import hanglog.category.Category; -import org.springframework.data.jpa.repository.JpaRepository; - -public interface CategoryRepository extends JpaRepository { -} diff --git a/backend/src/main/java/hanglog/category/service/CategoryService.java b/backend/src/main/java/hanglog/category/service/CategoryService.java new file mode 100644 index 000000000..e7bdc5568 --- /dev/null +++ b/backend/src/main/java/hanglog/category/service/CategoryService.java @@ -0,0 +1,24 @@ +package hanglog.category.service; + +import hanglog.category.domain.Category; +import hanglog.category.domain.repository.CategoryRepository; +import hanglog.category.dto.CategoryResponse; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class CategoryService { + + private final CategoryRepository categoryRepository; + + public List getExpenseCategories() { + final List expenseCategories = categoryRepository.findExpenseCategory(); + return expenseCategories.stream() + .map(CategoryResponse::of) + .toList(); + } +} diff --git a/backend/src/main/java/hanglog/expense/Expense.java b/backend/src/main/java/hanglog/expense/Expense.java index 1e1db496a..127a6d20b 100644 --- a/backend/src/main/java/hanglog/expense/Expense.java +++ b/backend/src/main/java/hanglog/expense/Expense.java @@ -4,7 +4,7 @@ import static jakarta.persistence.GenerationType.IDENTITY; import static lombok.AccessLevel.PROTECTED; -import hanglog.category.Category; +import hanglog.category.domain.Category; import hanglog.global.BaseEntity; import jakarta.persistence.CascadeType; import jakarta.persistence.Column; diff --git a/backend/src/main/java/hanglog/expense/dto/response/CategoriesInExpenseResponse.java b/backend/src/main/java/hanglog/expense/dto/response/CategoriesInExpenseResponse.java index bb56af6d5..6dc74bf9c 100644 --- a/backend/src/main/java/hanglog/expense/dto/response/CategoriesInExpenseResponse.java +++ b/backend/src/main/java/hanglog/expense/dto/response/CategoriesInExpenseResponse.java @@ -1,6 +1,7 @@ package hanglog.expense.dto.response; -import hanglog.category.Category; + +import hanglog.category.domain.Category; import java.math.BigDecimal; import java.math.RoundingMode; import java.util.List; diff --git a/backend/src/main/java/hanglog/expense/dto/response/CategoryInExpenseResponse.java b/backend/src/main/java/hanglog/expense/dto/response/CategoryInExpenseResponse.java index 8c329320b..282d09905 100644 --- a/backend/src/main/java/hanglog/expense/dto/response/CategoryInExpenseResponse.java +++ b/backend/src/main/java/hanglog/expense/dto/response/CategoryInExpenseResponse.java @@ -1,6 +1,7 @@ package hanglog.expense.dto.response; -import hanglog.category.Category; + +import hanglog.category.domain.Category; import lombok.Getter; import lombok.RequiredArgsConstructor; diff --git a/backend/src/main/java/hanglog/expense/dto/response/DayLogInExpenseResponse.java b/backend/src/main/java/hanglog/expense/dto/response/DayLogInExpenseResponse.java index f89c12a50..387c3e378 100644 --- a/backend/src/main/java/hanglog/expense/dto/response/DayLogInExpenseResponse.java +++ b/backend/src/main/java/hanglog/expense/dto/response/DayLogInExpenseResponse.java @@ -6,8 +6,10 @@ import java.util.Map; import lombok.Getter; import lombok.RequiredArgsConstructor; +import lombok.ToString; @Getter +@ToString @RequiredArgsConstructor public class DayLogInExpenseResponse { diff --git a/backend/src/main/java/hanglog/expense/dto/response/ExpenseGetResponse.java b/backend/src/main/java/hanglog/expense/dto/response/ExpenseGetResponse.java index be686918c..e001a1fac 100644 --- a/backend/src/main/java/hanglog/expense/dto/response/ExpenseGetResponse.java +++ b/backend/src/main/java/hanglog/expense/dto/response/ExpenseGetResponse.java @@ -1,6 +1,7 @@ package hanglog.expense.dto.response; -import hanglog.category.Category; + +import hanglog.category.domain.Category; import hanglog.expense.Currencies; import hanglog.trip.domain.DayLog; import hanglog.trip.domain.Trip; diff --git a/backend/src/main/java/hanglog/expense/service/ExpenseService.java b/backend/src/main/java/hanglog/expense/service/ExpenseService.java index 43a9d1c2e..1e92c3813 100644 --- a/backend/src/main/java/hanglog/expense/service/ExpenseService.java +++ b/backend/src/main/java/hanglog/expense/service/ExpenseService.java @@ -2,7 +2,7 @@ import static hanglog.global.exception.ExceptionCode.NOT_FOUND_TRIP_ID; -import hanglog.category.Category; +import hanglog.category.domain.Category; import hanglog.expense.Currencies; import hanglog.expense.Currency; import hanglog.expense.Expense; diff --git a/backend/src/main/java/hanglog/trip/domain/Place.java b/backend/src/main/java/hanglog/trip/domain/Place.java index 8e6e890a5..e6b3fa1b0 100644 --- a/backend/src/main/java/hanglog/trip/domain/Place.java +++ b/backend/src/main/java/hanglog/trip/domain/Place.java @@ -4,7 +4,7 @@ import static jakarta.persistence.GenerationType.IDENTITY; import static lombok.AccessLevel.PROTECTED; -import hanglog.category.Category; +import hanglog.category.domain.Category; import hanglog.global.BaseEntity; import jakarta.persistence.Column; import jakarta.persistence.Entity; diff --git a/backend/src/main/java/hanglog/trip/dto/response/ExpenseResponse.java b/backend/src/main/java/hanglog/trip/dto/response/ExpenseResponse.java index 181668742..0de2bf494 100644 --- a/backend/src/main/java/hanglog/trip/dto/response/ExpenseResponse.java +++ b/backend/src/main/java/hanglog/trip/dto/response/ExpenseResponse.java @@ -1,6 +1,6 @@ package hanglog.trip.dto.response; -import hanglog.category.CategoryResponse; +import hanglog.category.dto.CategoryResponse; import hanglog.expense.Expense; import lombok.Getter; import lombok.RequiredArgsConstructor; diff --git a/backend/src/main/java/hanglog/trip/dto/response/PlaceResponse.java b/backend/src/main/java/hanglog/trip/dto/response/PlaceResponse.java index 8be411ada..8860528a6 100644 --- a/backend/src/main/java/hanglog/trip/dto/response/PlaceResponse.java +++ b/backend/src/main/java/hanglog/trip/dto/response/PlaceResponse.java @@ -1,6 +1,6 @@ package hanglog.trip.dto.response; -import hanglog.category.CategoryResponse; +import hanglog.category.dto.CategoryResponse; import hanglog.trip.domain.Place; import java.math.BigDecimal; import lombok.Getter; diff --git a/backend/src/main/java/hanglog/trip/service/ItemService.java b/backend/src/main/java/hanglog/trip/service/ItemService.java index 91a4f0c93..e89e13a42 100644 --- a/backend/src/main/java/hanglog/trip/service/ItemService.java +++ b/backend/src/main/java/hanglog/trip/service/ItemService.java @@ -6,8 +6,8 @@ import static hanglog.global.exception.ExceptionCode.NOT_FOUND_TRIP_ITEM_ID; import static hanglog.global.exception.ExceptionCode.NOT_FOUNT_IMAGE_URL; -import hanglog.category.Category; -import hanglog.category.repository.CategoryRepository; +import hanglog.category.domain.Category; +import hanglog.category.domain.repository.CategoryRepository; import hanglog.expense.Expense; import hanglog.global.exception.BadRequestException; import hanglog.global.type.StatusType; diff --git a/backend/src/test/java/hanglog/category/fixture/CategoryFixture.java b/backend/src/test/java/hanglog/category/fixture/CategoryFixture.java index 582f4ad1c..9ce49fc16 100644 --- a/backend/src/test/java/hanglog/category/fixture/CategoryFixture.java +++ b/backend/src/test/java/hanglog/category/fixture/CategoryFixture.java @@ -1,13 +1,16 @@ package hanglog.category.fixture; -import hanglog.category.Category; +import hanglog.category.domain.Category; +import java.util.List; public class CategoryFixture { - public static final Category FOOD = new Category(100L, "음식", "food"); - public static final Category CULTURE = new Category(200L, "문화", "culture"); - public static final Category SHOPPING = new Category(300L, "쇼핑", "shopping"); - public static final Category LODGING = new Category(400L, "숙박", "lodging"); - public static final Category TRANSPORT = new Category(500L, "교통", "transport"); - public static final Category ETC = new Category(600L, "기타", "etc"); + public static final List EXPENSE_CATEGORIES = List.of( + new Category(100L, "food", "음식"), + new Category(200L, "culture", "문화"), + new Category(300L, "shopping", "쇼핑"), + new Category(400L, "accommodation", "숙박"), + new Category(500L, "transportation", "교통"), + new Category(600L, "etc", "기타") + ); } diff --git a/backend/src/test/java/hanglog/category/presentation/CategoryControllerTest.java b/backend/src/test/java/hanglog/category/presentation/CategoryControllerTest.java new file mode 100644 index 000000000..c8158070c --- /dev/null +++ b/backend/src/test/java/hanglog/category/presentation/CategoryControllerTest.java @@ -0,0 +1,76 @@ +package hanglog.category.presentation; + +import static hanglog.category.fixture.CategoryFixture.EXPENSE_CATEGORIES; +import static hanglog.trip.restdocs.RestDocsConfiguration.field; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; +import static org.springframework.http.MediaType.APPLICATION_JSON; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import hanglog.category.dto.CategoryResponse; +import hanglog.category.service.CategoryService; +import hanglog.trip.restdocs.RestDocsTest; +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.restdocs.AutoConfigureRestDocs; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.data.jpa.mapping.JpaMetamodelMappingContext; +import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders; +import org.springframework.restdocs.payload.JsonFieldType; +import org.springframework.test.web.servlet.MvcResult; + +@WebMvcTest(CategoryController.class) +@MockBean(JpaMetamodelMappingContext.class) +@AutoConfigureRestDocs +class CategoryControllerTest extends RestDocsTest { + + @Autowired + private ObjectMapper objectMapper; + + @MockBean + private CategoryService categoryService; + + @DisplayName("경비 구분에 필요한 상위 카테고리 정보를 반환한다.") + @Test + void getExpenseCategories() throws Exception { + // given + final List expectResponses = EXPENSE_CATEGORIES.stream() + .map(CategoryResponse::of) + .toList(); + + when(categoryService.getExpenseCategories()) + .thenReturn(expectResponses); + + // when & then + final MvcResult mvcResult = mockMvc.perform(RestDocumentationRequestBuilders.get("/categories") + .contentType(APPLICATION_JSON)) + .andExpect(status().isOk()) + .andDo(restDocs.document( + responseFields( + fieldWithPath("[].id") + .type(JsonFieldType.NUMBER) + .description("카테고리 ID") + .attributes(field("constraint", "양의 정수")), + fieldWithPath("[].name") + .type(JsonFieldType.STRING) + .description("카테고리 한글명") + .attributes(field("constraint", "50자 이하의 문자열")) + ) + )) + .andReturn(); + + final List actualResponses = objectMapper.readValue( + mvcResult.getResponse().getContentAsString(), + new TypeReference<>() { + } + ); + assertThat(actualResponses).usingRecursiveComparison().isEqualTo(expectResponses); + } +} diff --git a/backend/src/test/java/hanglog/category/service/CategoryServiceTest.java b/backend/src/test/java/hanglog/category/service/CategoryServiceTest.java new file mode 100644 index 000000000..757c48333 --- /dev/null +++ b/backend/src/test/java/hanglog/category/service/CategoryServiceTest.java @@ -0,0 +1,45 @@ +package hanglog.category.service; + +import static hanglog.category.fixture.CategoryFixture.EXPENSE_CATEGORIES; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; + +import hanglog.category.domain.repository.CategoryRepository; +import hanglog.category.dto.CategoryResponse; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.transaction.annotation.Transactional; + +@ExtendWith(MockitoExtension.class) +@Transactional +class CategoryServiceTest { + + @InjectMocks + private CategoryService categoryService; + + @Mock + private CategoryRepository categoryRepository; + + @DisplayName("경비 구분에 필요한 상위 카테고리 정보를 반환한다.") + @Test + void getExpenseCategories() { + // given + final List expectResponses = EXPENSE_CATEGORIES.stream() + .map(CategoryResponse::of) + .toList(); + + given(categoryRepository.findExpenseCategory()) + .willReturn(EXPENSE_CATEGORIES); + + // when + final List actualResponses = categoryService.getExpenseCategories(); + + // then + assertThat(actualResponses).usingRecursiveComparison().isEqualTo(expectResponses); + } +} diff --git a/backend/src/test/java/hanglog/expense/presentation/ExpenseControllerTest.java b/backend/src/test/java/hanglog/expense/presentation/ExpenseControllerTest.java index 3e63418b2..90b3955db 100644 --- a/backend/src/test/java/hanglog/expense/presentation/ExpenseControllerTest.java +++ b/backend/src/test/java/hanglog/expense/presentation/ExpenseControllerTest.java @@ -1,6 +1,6 @@ package hanglog.expense.presentation; -import static hanglog.category.fixture.CategoryFixture.CULTURE; +import static hanglog.category.fixture.CategoryFixture.EXPENSE_CATEGORIES; import static hanglog.expense.fixture.CurrenciesFixture.DEFAULT_CURRENCIES; import static hanglog.trip.fixture.CityFixture.LONDON; import static hanglog.trip.fixture.CityFixture.TOKYO; @@ -44,7 +44,7 @@ void getExpenses() throws Exception { LONDON_TRIP, 10000, List.of(new TripCity(LONDON_TRIP, LONDON), new TripCity(LONDON_TRIP, TOKYO)), - Map.of(CULTURE, 1000), + Map.of(EXPENSE_CATEGORIES.get(1), 1000), DEFAULT_CURRENCIES, Map.of(LONDON_DAY, 1000) ); diff --git a/backend/src/test/java/hanglog/expense/service/ExpenseServiceTest.java b/backend/src/test/java/hanglog/expense/service/ExpenseServiceTest.java index 1e4a6e270..78e77d0e2 100644 --- a/backend/src/test/java/hanglog/expense/service/ExpenseServiceTest.java +++ b/backend/src/test/java/hanglog/expense/service/ExpenseServiceTest.java @@ -1,12 +1,14 @@ package hanglog.expense.service; -import static hanglog.category.fixture.CategoryFixture.CULTURE; -import static hanglog.category.fixture.CategoryFixture.LODGING; + +import static hanglog.category.fixture.CategoryFixture.EXPENSE_CATEGORIES; import static hanglog.expense.fixture.CurrenciesFixture.DEFAULT_CURRENCIES; import static hanglog.trip.fixture.CityFixture.LONDON; import static hanglog.trip.fixture.CityFixture.TOKYO; import static hanglog.trip.fixture.DayLogFixture.EXPENSE_JAPAN_DAYLOG; import static hanglog.trip.fixture.DayLogFixture.EXPENSE_LONDON_DAYLOG; +import static hanglog.trip.fixture.ExpenseFixture.EURO_10000; +import static hanglog.trip.fixture.ExpenseFixture.JPY_10000; import static hanglog.trip.fixture.ItemFixture.AIRPLANE_ITEM; import static hanglog.trip.fixture.ItemFixture.JAPAN_HOTEL; import static hanglog.trip.fixture.ItemFixture.LONDON_EYE_ITEM; @@ -74,6 +76,10 @@ void getAllExpenses() { ItemInDayLogResponse.of(JAPAN_HOTEL), ItemInDayLogResponse.of(AIRPLANE_ITEM) ); + final int japanAmount = (int) (JPY_10000.getAmount() * DEFAULT_CURRENCIES.getUnitRateOfJpy()); + final int londonAmount = (int) (EURO_10000.getAmount() * DEFAULT_CURRENCIES.getEur()); + final int totalAmount = japanAmount + londonAmount * 2; + // when final ExpenseGetResponse actual = expenseService.getAllExpenses(1L); @@ -83,25 +89,28 @@ void getAllExpenses() { .ignoringFields("categories", "dayLogs") .isEqualTo( ExpenseGetResponse.of(LONDON_TO_JAPAN, - 28009000, + totalAmount, List.of(new TripCity(LONDON_TRIP, LONDON), new TripCity(LONDON_TRIP, TOKYO)), - Map.of(LODGING, 9000, CULTURE, 28000000), + Map.of( + EXPENSE_CATEGORIES.get(3), + japanAmount, EXPENSE_CATEGORIES.get(1), + londonAmount * 2), DEFAULT_CURRENCIES, - Map.of(EXPENSE_JAPAN_DAYLOG, 9000, EXPENSE_LONDON_DAYLOG, 28000000) + Map.of(EXPENSE_JAPAN_DAYLOG, japanAmount, EXPENSE_LONDON_DAYLOG, londonAmount * 2) ) ); softly.assertThat(actual.getCategories()) .usingRecursiveFieldByFieldElementComparator() .contains( new CategoriesInExpenseResponse( - CategoryInExpenseResponse.of(LODGING), - 9000, - BigDecimal.valueOf((double) 100 * 9000 / 28009000) + CategoryInExpenseResponse.of(EXPENSE_CATEGORIES.get(3)), + japanAmount, + BigDecimal.valueOf((double) 100 * japanAmount / totalAmount) .setScale(2, RoundingMode.CEILING)), new CategoriesInExpenseResponse( - CategoryInExpenseResponse.of(CULTURE), - 28000000, - BigDecimal.valueOf((double) 100 * 28000000 / 28009000) + CategoryInExpenseResponse.of(EXPENSE_CATEGORIES.get(1)), + londonAmount * 2, + BigDecimal.valueOf((double) 100 * londonAmount * 2 / totalAmount) .setScale(2, RoundingMode.CEILING)) ); softly.assertThat(actual.getDayLogs()).usingRecursiveFieldByFieldElementComparatorIgnoringFields("items") @@ -110,14 +119,14 @@ void getAllExpenses() { 1L, 1, LocalDate.of(2023, 7, 1), - 28000000, + londonAmount * 2, List.of() ), new DayLogInExpenseResponse( 1L, 2, LocalDate.of(2023, 7, 2), - 9000, + japanAmount, List.of() ) ); diff --git a/backend/src/test/java/hanglog/trip/fixture/ExpenseFixture.java b/backend/src/test/java/hanglog/trip/fixture/ExpenseFixture.java index 290e13d31..5fbd96824 100644 --- a/backend/src/test/java/hanglog/trip/fixture/ExpenseFixture.java +++ b/backend/src/test/java/hanglog/trip/fixture/ExpenseFixture.java @@ -1,7 +1,6 @@ package hanglog.trip.fixture; -import static hanglog.category.fixture.CategoryFixture.CULTURE; -import static hanglog.category.fixture.CategoryFixture.LODGING; +import static hanglog.category.fixture.CategoryFixture.EXPENSE_CATEGORIES; import hanglog.expense.Expense; @@ -11,13 +10,13 @@ public final class ExpenseFixture { 1L, "EUR", 10000.0, - CULTURE + EXPENSE_CATEGORIES.get(1) ); - public static final Expense JPY_1000 = new Expense( + public static final Expense JPY_10000 = new Expense( 1L, "JPY", - 1000.0, - LODGING + 10000.0, + EXPENSE_CATEGORIES.get(3) ); } diff --git a/backend/src/test/java/hanglog/trip/fixture/ItemFixture.java b/backend/src/test/java/hanglog/trip/fixture/ItemFixture.java index 879bab655..41bf22b3f 100644 --- a/backend/src/test/java/hanglog/trip/fixture/ItemFixture.java +++ b/backend/src/test/java/hanglog/trip/fixture/ItemFixture.java @@ -56,6 +56,6 @@ public final class ItemFixture { "이동", 1, TripFixture.LONDON_TRIP), - ExpenseFixture.JPY_1000 + ExpenseFixture.JPY_10000 ); } diff --git a/backend/src/test/java/hanglog/trip/fixture/PlaceFixture.java b/backend/src/test/java/hanglog/trip/fixture/PlaceFixture.java index 6984511b0..9bae2f4a8 100644 --- a/backend/src/test/java/hanglog/trip/fixture/PlaceFixture.java +++ b/backend/src/test/java/hanglog/trip/fixture/PlaceFixture.java @@ -1,6 +1,6 @@ package hanglog.trip.fixture; -import hanglog.category.Category; +import hanglog.category.domain.Category; import hanglog.trip.domain.Place; import java.math.BigDecimal; diff --git a/backend/src/test/java/hanglog/trip/service/ItemServiceTest.java b/backend/src/test/java/hanglog/trip/service/ItemServiceTest.java index 30ea0eb2a..31cce7ff1 100644 --- a/backend/src/test/java/hanglog/trip/service/ItemServiceTest.java +++ b/backend/src/test/java/hanglog/trip/service/ItemServiceTest.java @@ -6,8 +6,8 @@ import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.verify; -import hanglog.category.Category; -import hanglog.category.repository.CategoryRepository; +import hanglog.category.domain.Category; +import hanglog.category.domain.repository.CategoryRepository; import hanglog.image.domain.Image; import hanglog.image.domain.repository.ImageRepository; import hanglog.trip.domain.DayLog; diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 3e312a312..4b4320b51 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,5 +1,6 @@ import { Outlet } from 'react-router-dom'; +import { useCityQuery } from '@hooks/api/useCityQuery'; import { useResetError } from '@hooks/common/useResetError'; import Error from '@components/common/Error/Error'; @@ -8,6 +9,7 @@ import Header from '@components/layout/Header/Header'; const App = () => { const { handleErrorReset } = useResetError(); + useCityQuery(); return ( diff --git a/frontend/src/api/trip/putTrip.ts b/frontend/src/api/trip/putTrip.ts new file mode 100644 index 000000000..5381f81f6 --- /dev/null +++ b/frontend/src/api/trip/putTrip.ts @@ -0,0 +1,16 @@ +import { END_POINTS } from '@constants/api'; +import type { TripFormData } from '@type/trip'; + +import { axiosInstance } from '@api/axiosInstance'; + +export interface PutTripParams extends TripFormData { + tripId: number; +} + +export const putTrip = + () => + ({ tripId, ...tripInformation }: PutTripParams) => { + return axiosInstance.put(END_POINTS.TRIP(tripId), { + ...tripInformation, + }); + }; diff --git a/frontend/src/api/trips/newTrip.ts b/frontend/src/api/trips/postNewTrip.ts similarity index 100% rename from frontend/src/api/trips/newTrip.ts rename to frontend/src/api/trips/postNewTrip.ts diff --git a/frontend/src/assets/svg/warning-icon.svg b/frontend/src/assets/svg/warning-icon.svg new file mode 100644 index 000000000..8ec1be71a --- /dev/null +++ b/frontend/src/assets/svg/warning-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/src/components/common/CitySearchBar/CitySearchBar.tsx b/frontend/src/components/common/CitySearchBar/CitySearchBar.tsx index 9e11c258f..ab7b91b29 100644 --- a/frontend/src/components/common/CitySearchBar/CitySearchBar.tsx +++ b/frontend/src/components/common/CitySearchBar/CitySearchBar.tsx @@ -20,19 +20,20 @@ import { import CitySuggestion from '@components/common/CitySuggestion/CitySuggestion'; interface CitySearchBarProps { - initialCityTags?: CityData[]; - setCityData: (cities: CityData[]) => void; + initialCities?: CityData[]; + updateCityInfo: (cities: CityData[]) => void; + required?: boolean; } -const CitySearchBar = ({ initialCityTags, setCityData }: CitySearchBarProps) => { +const CitySearchBar = ({ initialCities, updateCityInfo, required = false }: CitySearchBarProps) => { const [queryWord, setQueryWord] = useState(''); - const { cityTags, addCityTag, deleteCityTag } = useCityTags(initialCityTags ?? []); + const { cityTags, addCityTag, deleteCityTag } = useCityTags(initialCities ?? []); const { isOpen: isSuggestionOpen, open: openSuggestion, close: closeSuggestion } = useOverlay(); const inputRef = useRef(null); const debouncedQueryWord = useDebounce(queryWord, 300); useEffect(() => { - setCityData(cityTags); + updateCityInfo(cityTags); }, [cityTags]); const handleInputChange = (event: FormEvent) => { @@ -74,27 +75,23 @@ const CitySearchBar = ({ initialCityTags, setCityData }: CitySearchBarProps) => } }; - const CityTags = () => { - return cityTags.map((cityTag) => ( - - {cityTag.name} - - - )); - }; - return (
- +
- + {cityTags.map((cityTag) => ( + + {cityTag.name} + + + ))} { }; export const emptyTextStyling = css({ - margin: Theme.spacer.spacing2, + padding: `12px ${Theme.spacer.spacing3}`, color: Theme.color.gray500, }); diff --git a/frontend/src/components/common/DateInput/DateInput.tsx b/frontend/src/components/common/DateInput/DateInput.tsx index 59ed744dc..f341ff3e6 100644 --- a/frontend/src/components/common/DateInput/DateInput.tsx +++ b/frontend/src/components/common/DateInput/DateInput.tsx @@ -13,19 +13,21 @@ import { interface DateInputProps { initialDateRange?: DateRangeData; - setDateData: (dateRange: DateRangeData) => void; + updateDateInfo: (dateRange: DateRangeData) => void; + required?: boolean; } const DateInput = ({ initialDateRange = { start: null, end: null }, - setDateData, + updateDateInfo, + required = false, }: DateInputProps) => { const [inputValue, setInputValue] = useState(dateRangeToString(initialDateRange)); const [selectedDateRange, setSelectedDateRange] = useState(initialDateRange); const { isOpen: isCalendarOpen, close: closeCalendar, toggle: toggleCalendar } = useOverlay(); useEffect(() => { - setDateData(selectedDateRange); + updateDateInfo(selectedDateRange); }, [selectedDateRange]); const handleDateClick = (dateRange: DateRangeData) => { @@ -37,7 +39,7 @@ const DateInput = ({ return ( - + ; const TripInformation = ({ ...information }: TripInformationProps) => { + const { isOpen: isEditModalOpen, close: closeEditModal, open: openEditModal } = useOverlay(); + return ( -
- -
- 여행 대표 이미지 - - - - {information.cities.map(({ id, name }) => ( - {name} - ))} - - - {information.title} - - - {formatDate(information.startDate)} - {formatDate(information.endDate)} - - - {information.description} - - - - {/* 수정 모드일 때만 보인다 */} - - - -
+ <> +
+ +
+ 여행 대표 이미지 + + + + {information.cities.map(({ id, name }) => ( + {name} + ))} + + + {information.title} + + + {formatDate(information.startDate)} - {formatDate(information.endDate)} + + + {information.description} + + + + {/* 수정 모드일 때만 보인다 */} + + + +
+ {isEditModalOpen && ( + + )} + ); }; diff --git a/frontend/src/components/newTrip/NewTripForm/NewTripForm.style.ts b/frontend/src/components/newTrip/NewTripForm/TripCreateForm.style.ts similarity index 100% rename from frontend/src/components/newTrip/NewTripForm/NewTripForm.style.ts rename to frontend/src/components/newTrip/NewTripForm/TripCreateForm.style.ts diff --git a/frontend/src/components/newTrip/NewTripForm/NewTripForm.tsx b/frontend/src/components/newTrip/NewTripForm/TripCreateForm.tsx similarity index 53% rename from frontend/src/components/newTrip/NewTripForm/NewTripForm.tsx rename to frontend/src/components/newTrip/NewTripForm/TripCreateForm.tsx index 225567523..332f7c854 100644 --- a/frontend/src/components/newTrip/NewTripForm/NewTripForm.tsx +++ b/frontend/src/components/newTrip/NewTripForm/TripCreateForm.tsx @@ -1,24 +1,24 @@ +import { formStyling } from '@/components/newTrip/NewTripForm/TripCreateForm.style'; import { PATH } from '@constants/path'; import { Button } from 'hang-log-design-system'; import type { FormEvent } from 'react'; import { useNavigate } from 'react-router-dom'; -import { useNewTripMutation } from '@hooks/api/useNewTripMutation'; -import { useNewTripForm } from '@hooks/newTrip/useNewTripForm'; +import { useCreateTripMutation } from '@hooks/api/useCreateTripMutation'; +import { useCityDateForm } from '@hooks/common/useCityDateForm'; import CitySearchBar from '@components/common/CitySearchBar/CitySearchBar'; import DateInput from '@components/common/DateInput/DateInput'; -import { formStyling } from '@components/newTrip/NewTripForm/NewTripForm.style'; -const NewTripForm = () => { - const { newTripData, setCityData, setDateData, isAllInputFilled } = useNewTripForm(); - const newTripMutation = useNewTripMutation(); +const TripCreateForm = () => { + const { cityDateInfo, updateCityInfo, updateDateInfo, isCityDateValid } = useCityDateForm(); + const createTripMutation = useCreateTripMutation(); const navigate = useNavigate(); const handleSubmit = (e: FormEvent) => { e.preventDefault(); - newTripMutation.mutate(newTripData, { + createTripMutation.mutate(cityDateInfo, { onSuccess: goToTripEditPageWithId, }); }; @@ -30,13 +30,13 @@ const NewTripForm = () => { return (
- - - ); }; -export default NewTripForm; +export default TripCreateForm; diff --git a/frontend/src/components/trip/TripInfoEditModal/TripInfoEditModal.style.ts b/frontend/src/components/trip/TripInfoEditModal/TripInfoEditModal.style.ts new file mode 100644 index 000000000..b8180e969 --- /dev/null +++ b/frontend/src/components/trip/TripInfoEditModal/TripInfoEditModal.style.ts @@ -0,0 +1,27 @@ +import { css } from '@emotion/react'; +import { Theme } from 'hang-log-design-system'; + +export const formStyling = css({ + display: 'flex', + flexDirection: 'column', + gap: Theme.spacer.spacing3, + + '> button': { + width: '400px', + }, +}); + +export const titleStyling = css({ + flexDirection: 'column', + width: '400px', + gap: '4px', + + '> div': { + width: '100%', + }, +}); + +export const textareaStyling = css({ + resize: 'none', + fontFamily: 'none', +}); diff --git a/frontend/src/components/trip/TripInfoEditModal/TripInfoEditModal.tsx b/frontend/src/components/trip/TripInfoEditModal/TripInfoEditModal.tsx new file mode 100644 index 000000000..90ec9736b --- /dev/null +++ b/frontend/src/components/trip/TripInfoEditModal/TripInfoEditModal.tsx @@ -0,0 +1,98 @@ +import WarningIcon from '@assets/svg/warning-icon.svg'; +import type { TripData } from '@type/trip'; +import { + Button, + Flex, + ImageUploadInput, + Input, + Modal, + SupportingText, + Textarea, +} from 'hang-log-design-system'; + +import { useTripEditForm } from '@hooks/trip/useTripEditForm'; + +import CitySearchBar from '@components/common/CitySearchBar/CitySearchBar'; +import DateInput from '@components/common/DateInput/DateInput'; +import { + formStyling, + textareaStyling, + titleStyling, +} from '@components/trip/TripInfoEditModal/TripInfoEditModal.style'; + +interface TripInfoEditModalProps extends Omit { + isOpen: boolean; + onClose: () => void; +} + +const TripInfoEditModal = ({ isOpen, onClose, ...information }: TripInfoEditModalProps) => { + const { + tripInfo, + isCityInputError, + isTitleError, + updateInputValue, + updateCityInfo, + updateDateInfo, + handleSubmit, + } = useTripEditForm(information, onClose); + + return ( + +
+ + + {isCityInputError && ( + + 방문 도시는 최소 한개 이상 선택해야 합니다 + + )} + + + + + + + 방문 기간을 단축하면 마지막 날짜부터 작성한 기록들이 삭제됩니다. + + + + + + {isTitleError && ( + 여행 제목을 입력하세요 + )} + +