diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml
index 3c5646bb..df67134c 100644
--- a/.github/workflows/cd.yml
+++ b/.github/workflows/cd.yml
@@ -248,6 +248,7 @@ jobs:
-e EATSSU_SLACK_TOKEN="${{ secrets.EATSSU_SLACK_TOKEN }}" \
-e EATSSU_PROD_DOMAIN="${{ secrets.EATSSU_PROD_DOMAIN }}" \
-e EATSSU_DEEPL_API_KEY="${{ secrets.EATSSU_DEEPL_API_KEY }}" \
+ -e EATSSU_FIREBASE_CREDENTIALS="${{ secrets.EATSSU_FIREBASE_CREDENTIALS }}" \
-e GRAFANA_BASE_URL="${{ secrets.EATSSU_GRAFANA_BASE_URL }}" \
-e GRAFANA_LOKI_DATASOURCE_UID="${{ secrets.EATSSU_GRAFANA_LOKI_DATASOURCE_UID }}" \
-e SWAGGER_USER="${{ secrets.SWAGGER_PROD_USER }}" \
@@ -314,6 +315,7 @@ jobs:
-e EATSSU_SLACK_TOKEN="${{ secrets.EATSSU_SLACK_TOKEN }}" \
-e EATSSU_DEV_DOMAIN="${{ secrets.EATSSU_DEV_DOMAIN }}" \
-e EATSSU_DEEPL_API_KEY="${{ secrets.EATSSU_DEEPL_API_KEY }}" \
+ -e EATSSU_FIREBASE_CREDENTIALS="${{ secrets.EATSSU_FIREBASE_CREDENTIALS }}" \
-e SWAGGER_USER="${{ secrets.SWAGGER_DEV_USER }}" \
-e SWAGGER_PASSWORD="${{ secrets.SWAGGER_DEV_PASSWORD }}" \
${{ secrets.DOCKER_REPO }}/eatssu-dev
diff --git a/build.gradle b/build.gradle
index 8675fd39..3fe0fb4f 100644
--- a/build.gradle
+++ b/build.gradle
@@ -81,6 +81,12 @@ dependencies {
implementation 'net.logstash.logback:logstash-logback-encoder:7.4'
implementation 'org.apache.commons:commons-csv:1.10.0'
+
+ implementation('com.google.firebase:firebase-admin:9.11.0') {
+ exclude group: 'com.google.cloud', module: 'google-cloud-storage'
+ exclude group: 'com.google.cloud', module: 'google-cloud-firestore'
+ }
+ implementation 'com.google.http-client:google-http-client-jackson2:2.2.0'
}
tasks.named('test') {
diff --git a/src/main/java/ssu/eatssu/domain/favorite/dto/response/FavoriteMenuResponse.java b/src/main/java/ssu/eatssu/domain/favorite/dto/response/FavoriteMenuResponse.java
new file mode 100644
index 00000000..6aef7b0e
--- /dev/null
+++ b/src/main/java/ssu/eatssu/domain/favorite/dto/response/FavoriteMenuResponse.java
@@ -0,0 +1,24 @@
+package ssu.eatssu.domain.favorite.dto.response;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import ssu.eatssu.domain.menu.entity.Menu;
+import ssu.eatssu.domain.restaurant.entity.Restaurant;
+import ssu.eatssu.domain.user.entity.Language;
+
+@Schema(title = "찜한 메뉴")
+public record FavoriteMenuResponse(
+ @Schema(description = "메뉴 id", example = "1")
+ Long menuId,
+ @Schema(description = "메뉴명(사용자 언어, 번역이 없으면 한국어)", example = "돈까스")
+ String menuName,
+ @Schema(description = "식당", example = "HAKSIK")
+ Restaurant restaurant,
+ @Schema(description = "단종 여부", example = "false")
+ boolean isDiscontinued
+) {
+
+ public static FavoriteMenuResponse of(Menu menu, Language language) {
+ return new FavoriteMenuResponse(menu.getId(), menu.getNameByLanguage(language), menu.getRestaurant(),
+ menu.isDiscontinued());
+ }
+}
diff --git a/src/main/java/ssu/eatssu/domain/favorite/dto/response/MenuSearchResponse.java b/src/main/java/ssu/eatssu/domain/favorite/dto/response/MenuSearchResponse.java
new file mode 100644
index 00000000..7d738993
--- /dev/null
+++ b/src/main/java/ssu/eatssu/domain/favorite/dto/response/MenuSearchResponse.java
@@ -0,0 +1,24 @@
+package ssu.eatssu.domain.favorite.dto.response;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import ssu.eatssu.domain.menu.entity.Menu;
+import ssu.eatssu.domain.restaurant.entity.Restaurant;
+import ssu.eatssu.domain.user.entity.Language;
+
+@Schema(title = "찜 메뉴 검색 결과")
+public record MenuSearchResponse(
+ @Schema(description = "메뉴 id", example = "1")
+ Long menuId,
+ @Schema(description = "메뉴명(사용자 언어, 번역이 없으면 한국어)", example = "돈까스")
+ String menuName,
+ @Schema(description = "식당", example = "HAKSIK")
+ Restaurant restaurant,
+ @Schema(description = "찜 여부", example = "true")
+ boolean isFavorite
+) {
+
+ public static MenuSearchResponse of(Menu menu, Language language, boolean isFavorite) {
+ return new MenuSearchResponse(menu.getId(), menu.getNameByLanguage(language), menu.getRestaurant(),
+ isFavorite);
+ }
+}
diff --git a/src/main/java/ssu/eatssu/domain/favorite/entity/MenuFavorite.java b/src/main/java/ssu/eatssu/domain/favorite/entity/MenuFavorite.java
new file mode 100644
index 00000000..0b9f3ba1
--- /dev/null
+++ b/src/main/java/ssu/eatssu/domain/favorite/entity/MenuFavorite.java
@@ -0,0 +1,51 @@
+package ssu.eatssu.domain.favorite.entity;
+
+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.JoinColumn;
+import jakarta.persistence.ManyToOne;
+import jakarta.persistence.Table;
+import jakarta.persistence.UniqueConstraint;
+import lombok.AccessLevel;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import org.hibernate.annotations.OnDelete;
+import org.hibernate.annotations.OnDeleteAction;
+import ssu.eatssu.domain.menu.entity.Menu;
+import ssu.eatssu.domain.user.entity.BaseTimeEntity;
+import ssu.eatssu.domain.user.entity.User;
+
+@Entity
+@Getter
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+@Table(uniqueConstraints = @UniqueConstraint(name = "uk_menu_favorite_user_menu", columnNames = {"user_id", "menu_id"}))
+public class MenuFavorite extends BaseTimeEntity {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ @Column(name = "menu_favorite_id")
+ private Long id;
+
+ @ManyToOne(fetch = FetchType.LAZY, optional = false)
+ @JoinColumn(name = "user_id", nullable = false)
+ @OnDelete(action = OnDeleteAction.CASCADE)
+ private User user;
+
+ @ManyToOne(fetch = FetchType.LAZY, optional = false)
+ @JoinColumn(name = "menu_id", nullable = false)
+ @OnDelete(action = OnDeleteAction.CASCADE)
+ private Menu menu;
+
+ private MenuFavorite(User user, Menu menu) {
+ this.user = user;
+ this.menu = menu;
+ }
+
+ public static MenuFavorite of(User user, Menu menu) {
+ return new MenuFavorite(user, menu);
+ }
+}
diff --git a/src/main/java/ssu/eatssu/domain/favorite/persistence/MenuFavoriteQueryRepository.java b/src/main/java/ssu/eatssu/domain/favorite/persistence/MenuFavoriteQueryRepository.java
new file mode 100644
index 00000000..f1395937
--- /dev/null
+++ b/src/main/java/ssu/eatssu/domain/favorite/persistence/MenuFavoriteQueryRepository.java
@@ -0,0 +1,44 @@
+package ssu.eatssu.domain.favorite.persistence;
+
+import com.querydsl.core.types.dsl.BooleanExpression;
+import com.querydsl.core.types.dsl.StringPath;
+import com.querydsl.jpa.impl.JPAQueryFactory;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Component;
+import ssu.eatssu.domain.menu.entity.Menu;
+import ssu.eatssu.domain.menu.entity.QMenu;
+import ssu.eatssu.domain.user.entity.Language;
+
+import java.util.List;
+
+@Component
+@RequiredArgsConstructor
+public class MenuFavoriteQueryRepository {
+
+ private final JPAQueryFactory queryFactory;
+ private final QMenu menu = QMenu.menu;
+
+ public List