From 53a08bb3ea84f3aff3b893fb7f10d6b81cc18842 Mon Sep 17 00:00:00 2001 From: sjinssun Date: Wed, 7 Oct 2026 00:14:14 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EB=B3=80=EB=8F=99=EB=A9=94=EB=89=B4=20?= =?UTF-8?q?=EB=8C=80=ED=91=9C=EB=A9=94=EB=89=B4=20=EB=B2=88=EC=97=AD?= =?UTF-8?q?=EC=9D=84=20menu.name=5Fen=EC=9C=BC=EB=A1=9C=20=EB=8F=99?= =?UTF-8?q?=EA=B8=B0=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ssu/eatssu/domain/menu/entity/Menu.java | 7 ++ .../domain/menu/service/MealService.java | 15 +++ ...kfill_menu_name_en_from_meal_main_menu.sql | 21 ++++ .../domain/menu/service/MealServiceTest.java | 99 +++++++++++++++++++ 4 files changed, 142 insertions(+) create mode 100644 src/main/resources/db/migration/V29__backfill_menu_name_en_from_meal_main_menu.sql diff --git a/src/main/java/ssu/eatssu/domain/menu/entity/Menu.java b/src/main/java/ssu/eatssu/domain/menu/entity/Menu.java index be352d8e..a6e9f50e 100644 --- a/src/main/java/ssu/eatssu/domain/menu/entity/Menu.java +++ b/src/main/java/ssu/eatssu/domain/menu/entity/Menu.java @@ -92,6 +92,13 @@ public void update(String name, Integer price) { this.price = price; } + public void updateNameEn(String nameEn) { + if (nameEn == null || nameEn.isBlank()) { + return; + } + this.nameEn = nameEn; + } + public void changeDiscontinuedStatus() { this.isDiscontinued = !this.isDiscontinued; } diff --git a/src/main/java/ssu/eatssu/domain/menu/service/MealService.java b/src/main/java/ssu/eatssu/domain/menu/service/MealService.java index ee02f6bd..9bbb0f62 100644 --- a/src/main/java/ssu/eatssu/domain/menu/service/MealService.java +++ b/src/main/java/ssu/eatssu/domain/menu/service/MealService.java @@ -222,6 +222,7 @@ private List upsertMainMenus(Long mealId, List menuNames, List menusByName = getMenusByName(meal); List unmatchedMainMenus = new ArrayList<>(); int seq = 0; for (MainMenuRequest mainMenu : mainMenus) { @@ -231,11 +232,25 @@ private List upsertMainMenus(Long mealId, List menuNames, List getMenusByName(Meal meal) { + return meal.getMealMenus().stream() + .map(MealMenu::getMenu) + .collect(Collectors.toMap(Menu::getName, menu -> menu, (existing, duplicate) -> existing)); + } + + private void syncMenuNameEn(Menu menu, String nameEn) { + if (menu == null) { + return; + } + menu.updateNameEn(nameEn); + } + private Optional getExistingMealId(Date date, TimePart timePart, Restaurant restaurant, List menuNames) { List meals = mealRepository.findAllByDateAndTimePartAndRestaurant(date, timePart, restaurant); diff --git a/src/main/resources/db/migration/V29__backfill_menu_name_en_from_meal_main_menu.sql b/src/main/resources/db/migration/V29__backfill_menu_name_en_from_meal_main_menu.sql new file mode 100644 index 00000000..7e493dc0 --- /dev/null +++ b/src/main/resources/db/migration/V29__backfill_menu_name_en_from_meal_main_menu.sql @@ -0,0 +1,21 @@ +ALTER TABLE menu + MODIFY COLUMN name_en VARCHAR(500) NULL; + +UPDATE menu m + JOIN (SELECT ranked.name_ko, ranked.restaurant, ranked.name_en + FROM (SELECT mmm.name_ko, + ml.restaurant, + mmm.name_en, + ROW_NUMBER() OVER ( + PARTITION BY mmm.name_ko, ml.restaurant + ORDER BY ml.date DESC, mmm.meal_main_menu_id DESC + ) AS rn + FROM meal_main_menu mmm + JOIN meal ml ON ml.meal_id = mmm.meal_id + WHERE TRIM(mmm.name_en) <> '') ranked + WHERE ranked.rn = 1) latest + ON CONVERT(latest.name_ko USING utf8mb4) COLLATE utf8mb4_bin = CONVERT(m.name USING utf8mb4) COLLATE utf8mb4_bin + AND CAST(latest.restaurant AS CHAR) = CAST(m.restaurant AS CHAR) +SET m.name_en = latest.name_en +WHERE m.name_en IS NULL + OR TRIM(m.name_en) = ''; diff --git a/src/test/java/ssu/eatssu/domain/menu/service/MealServiceTest.java b/src/test/java/ssu/eatssu/domain/menu/service/MealServiceTest.java index 557a1aea..2a92aa7e 100644 --- a/src/test/java/ssu/eatssu/domain/menu/service/MealServiceTest.java +++ b/src/test/java/ssu/eatssu/domain/menu/service/MealServiceTest.java @@ -7,7 +7,9 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.transaction.annotation.Transactional; +import ssu.eatssu.domain.favorite.persistence.MenuFavoriteQueryRepository; import ssu.eatssu.domain.menu.entity.Meal; +import ssu.eatssu.domain.menu.entity.Menu; import ssu.eatssu.domain.menu.entity.constants.TimePart; import ssu.eatssu.domain.menu.persistence.MealMainMenuRepository; import ssu.eatssu.domain.menu.persistence.MealMenuRepository; @@ -48,6 +50,9 @@ class MealServiceTest { @Autowired private MenuRepository menuRepository; + @Autowired + private MenuFavoriteQueryRepository menuFavoriteQueryRepository; + @BeforeEach void setUp() { cleanUp(); @@ -177,6 +182,100 @@ void createMealWithEmptyMainMenusReturnsEmptyUnmatchedList() { assertThat(result.unmatchedMainMenus()).isEmpty(); } + @Test + void createMealSyncsMainMenuTranslationToMenuNameEn() { + // given + CreateMealRequest request = new CreateMealRequest(List.of("김치찌개", "깍두기"), + List.of(new MainMenuRequest("김치찌개", "Kimchi Stew"))); + + // when + mealService.createMeal(Date.valueOf("2024-01-03"), Restaurant.HAKSIK, LUNCH, request); + + // then + assertThat(menuNameEn("김치찌개")).isEqualTo("Kimchi Stew"); + assertThat(menuNameEn("깍두기")).isNull(); + } + + @Test + void laterMealTranslationOverwritesMenuNameEn() { + // given + mealService.createMeal(Date.valueOf("2024-01-03"), Restaurant.HAKSIK, LUNCH, + new CreateMealRequest(List.of("김치찌개"), List.of(new MainMenuRequest("김치찌개", "Kimchi Stew")))); + + // when + mealService.createMeal(Date.valueOf("2024-01-04"), Restaurant.HAKSIK, LUNCH, + new CreateMealRequest(List.of("김치찌개"), List.of(new MainMenuRequest("김치찌개", "Kimchi Jjigae")))); + + // then + assertThat(menuNameEn("김치찌개")).isEqualTo("Kimchi Jjigae"); + } + + @Test + void menuNameEnIsKeptWhenOmittedFromMainMenusOrBlank() { + // given + mealService.createMeal(Date.valueOf("2024-01-03"), Restaurant.HAKSIK, LUNCH, + new CreateMealRequest(List.of("김치찌개", "돈까스"), + List.of(new MainMenuRequest("김치찌개", "Kimchi Stew"), + new MainMenuRequest("돈까스", "Pork Cutlet")))); + + // when + mealService.createMeal(Date.valueOf("2024-01-04"), Restaurant.HAKSIK, LUNCH, + new CreateMealRequest(List.of("김치찌개", "돈까스"), + List.of(new MainMenuRequest("돈까스", " ")))); + + // then + assertThat(menuNameEn("김치찌개")).isEqualTo("Kimchi Stew"); + assertThat(menuNameEn("돈까스")).isEqualTo("Pork Cutlet"); + } + + @Test + void unmatchedMainMenuDoesNotTouchMenuNameEn() { + // given + CreateMealRequest request = new CreateMealRequest(List.of("돈까스"), + List.of(new MainMenuRequest("김치", "Kimchi"))); + + // when + mealService.createMeal(Date.valueOf("2024-01-03"), Restaurant.HAKSIK, LUNCH, request); + + // then + assertThat(menuRepository.findByNameAndRestaurant("김치", Restaurant.HAKSIK)).isEmpty(); + assertThat(menuNameEn("돈까스")).isNull(); + } + + @Test + void reconcileMealSlotSyncsMainMenuTranslationToMenuNameEn() { + // given + List requests = List.of(new MealCreateWithPriceRequest( + List.of("김치볶음밥"), 5000, List.of(new MainMenuRequest("김치볶음밥", "Kimchi Fried Rice")))); + + // when + mealService.reconcileMealSlot(Date.valueOf("2024-01-03"), Restaurant.HAKSIK, LUNCH, requests); + + // then + assertThat(menuNameEn("김치볶음밥")).isEqualTo("Kimchi Fried Rice"); + } + + @Test + void favoriteSearchMatchesSyncedEnglishName() { + // given + mealService.createMeal(Date.valueOf("2024-01-03"), Restaurant.HAKSIK, LUNCH, + new CreateMealRequest(List.of("김치찌개"), List.of(new MainMenuRequest("김치찌개", "Kimchi Stew")))); + + // when + List englishResults = menuFavoriteQueryRepository.searchMenus("kimchi", Language.EN); + List koreanResults = menuFavoriteQueryRepository.searchMenus("kimchi", Language.KO); + + // then + assertThat(englishResults).extracting(Menu::getName).containsExactly("김치찌개"); + assertThat(koreanResults).isEmpty(); + } + + private String menuNameEn(String name) { + return menuRepository.findByNameAndRestaurant(name, Restaurant.HAKSIK) + .orElseThrow() + .getNameEn(); + } + @Test void 메뉴가_없는_식단을_조회하면_빈_목록을_반환한다() { // given