Skip to content

Commit fcc76c6

Browse files
authored
Merge pull request #30 from Han-Spoon/refactor/#29-card
[Refactor] 카드 저장 텍스트 스냅샷 단순화 + Flyway 마이그레이션 스쿼시
2 parents aa017db + 9f00846 commit fcc76c6

17 files changed

Lines changed: 242 additions & 430 deletions

File tree

Lines changed: 5 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,57 +1,24 @@
11
package com.hanspoon.backend_api.domain.card.dto;
22

3-
import com.fasterxml.jackson.annotation.JsonIgnore;
43
import com.hanspoon.backend_api.domain.card.entity.CardType;
54
import io.swagger.v3.oas.annotations.media.Schema;
65
import jakarta.validation.Valid;
7-
import jakarta.validation.constraints.AssertTrue;
86
import jakarta.validation.constraints.NotBlank;
97
import jakarta.validation.constraints.NotNull;
108
import jakarta.validation.constraints.Size;
11-
import java.util.List;
129
import java.util.UUID;
1310

1411
/**
15-
* 카드 저장 요청. 문구/재료는 FE 가 분석 결과(hits/ownerCard)로 구성해 전달한다.
12+
* 카드 저장 요청. FE 가 화면에 보이는 문구를 그대로 {@code text} 로 전달한다(언어 스냅샷 — 서버는 저장만).
1613
*
17-
* <p>타입별 페이로드가 다르다:
18-
* <ul>
19-
* <li>order: menuNameKo 만.
20-
* <li>ingredient_check / exclude: menuNameKo + ingredients(hit 코드).
21-
* <li>owner_question: menuNameKo + text(AI 완성 문구) + flag(선택).
22-
* </ul>
23-
*
24-
* @param type 카드 종류 (order | ingredient_check | exclude | owner_question)
14+
* @param type 카드 종류 (order | ingredient_check | exclude)
2515
* @param menuNameKo 한국어 메뉴명(사장님 제시용)
26-
* @param ingredients hit 태그 코드 목록(템플릿형 전용)
27-
* @param text 표시 문구(다국어, owner_question 전용). ko 필수
28-
* @param flag 애매 사유 플래그(owner_question 선택)
16+
* @param text 표시 문구(다국어). ko 필수 + 사용자 언어 1개
2917
* @param scanId 출처 스캔 id(선택). 제공 시 본인 소유 검증
3018
*/
3119
@Schema(description = "카드 저장 요청")
3220
public record SaveCardRequest(
3321
@NotNull CardType type,
3422
@NotBlank @Size(max = 255) String menuNameKo,
35-
List<String> ingredients,
36-
@Valid CardText text,
37-
@Size(max = 50) String flag,
38-
UUID scanId) {
39-
40-
/** ingredient_check / exclude 는 ingredients 가 필수(non-empty). */
41-
@JsonIgnore
42-
@AssertTrue(message = "ingredients is required for ingredient_check/exclude cards.") public boolean isIngredientsValid() {
43-
if (type != CardType.INGREDIENT_CHECK && type != CardType.EXCLUDE) {
44-
return true;
45-
}
46-
return ingredients != null && !ingredients.isEmpty();
47-
}
48-
49-
/** owner_question 은 text(다국어 완성 문구)가 필수. */
50-
@JsonIgnore
51-
@AssertTrue(message = "text is required for owner_question cards.") public boolean isTextValid() {
52-
if (type != CardType.OWNER_QUESTION) {
53-
return true;
54-
}
55-
return text != null;
56-
}
57-
}
23+
@NotNull @Valid CardText text,
24+
UUID scanId) {}

‎src/main/java/com/hanspoon/backend_api/domain/card/dto/SavedCardResponse.java‎

Lines changed: 3 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,39 +4,22 @@
44
import com.hanspoon.backend_api.domain.card.entity.SavedCard;
55
import io.swagger.v3.oas.annotations.media.Schema;
66
import java.time.Instant;
7-
import java.util.List;
87
import java.util.UUID;
98

109
/**
1110
* 저장 카드 1건 응답. 목록은 최신 저장순(lastSavedAt desc)으로 정렬된다.
12-
* 타입별로 채워지는 필드가 다르다(템플릿형=ingredients, owner_question=text+flag).
1311
*
1412
* @param cardId 카드 id
1513
* @param type 카드 종류
1614
* @param menuNameKo 한국어 메뉴명
17-
* @param ingredients hit 태그 코드(템플릿형), 그 외 null
18-
* @param text 표시 문구(owner_question), 그 외 null
19-
* @param flag 애매 사유(owner_question), 그 외 null
15+
* @param text 표시 문구(다국어, 저장 시점 스냅샷)
2016
* @param createdAt 최초 저장 시각
2117
*/
2218
@Schema(description = "저장 카드 항목")
23-
public record SavedCardResponse(
24-
UUID cardId,
25-
CardType type,
26-
String menuNameKo,
27-
List<String> ingredients,
28-
CardText text,
29-
String flag,
30-
Instant createdAt) {
19+
public record SavedCardResponse(UUID cardId, CardType type, String menuNameKo, CardText text, Instant createdAt) {
3120

3221
public static SavedCardResponse from(SavedCard card) {
3322
return new SavedCardResponse(
34-
card.getId(),
35-
card.getCardType(),
36-
card.getMenuNameKo(),
37-
card.getIngredients(),
38-
card.getText(),
39-
card.getFlag(),
40-
card.getCreatedAt());
23+
card.getId(), card.getCardType(), card.getMenuNameKo(), card.getText(), card.getCreatedAt());
4124
}
4225
}

‎src/main/java/com/hanspoon/backend_api/domain/card/entity/CardType.java‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,7 @@
88
public enum CardType {
99
ORDER("order"),
1010
INGREDIENT_CHECK("ingredient_check"),
11-
EXCLUDE("exclude"),
12-
OWNER_QUESTION("owner_question");
11+
EXCLUDE("exclude");
1312

1413
private final String code;
1514

‎src/main/java/com/hanspoon/backend_api/domain/card/entity/SavedCard.java‎

Lines changed: 7 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
import jakarta.persistence.Id;
88
import jakarta.persistence.Table;
99
import java.time.Instant;
10-
import java.util.List;
1110
import java.util.UUID;
1211
import lombok.AccessLevel;
1312
import lombok.Getter;
@@ -18,13 +17,10 @@
1817
/**
1918
* 자주 쓰는 카드(사장님 소통 카드 저장). users 와 N:1 (FK ON DELETE CASCADE).
2019
* 출처 스캔(scanSessionId)은 선택이며, 스캔 삭제 시 ON DELETE SET NULL 로 카드는 보존된다.
21-
* 문구 생성은 FE 책임 — 서버는 저장/목록/삭제만 담당한다.
2220
*
23-
* <p>저장 형태가 타입별로 다르다:
24-
* <ul>
25-
* <li>order/ingredient_check/exclude: {@code ingredients}(hit 코드)만 저장 → FE 가 언어별 문장 재렌더.
26-
* <li>owner_question: AI 가 준 완성 문구 {@code text} + {@code flag} 저장(재생성 불가).
27-
* </ul>
21+
* <p>문구 생성은 FE 책임 — 서버는 화면에 보이는 텍스트({@link CardText})를 그대로 스냅샷 저장한다.
22+
* 사용 언어를 바꿔도 저장된 카드는 변하지 않는다(재렌더 없음). 타입(order/ingredient_check/exclude)별로
23+
* 텍스트 출처만 다를 뿐(재료확인=AI owner_card.question, 주문/빼고요청=FE 템플릿) 저장 형태는 동일하다.
2824
*/
2925
@Entity
3026
@Table(name = "saved_cards")
@@ -45,54 +41,29 @@ public class SavedCard extends BaseEntity {
4541
@Column(name = "menu_name_ko", length = 255, nullable = false)
4642
private String menuNameKo;
4743

48-
// 템플릿형(order/ingredient_check/exclude) — hit 태그 코드. owner_question 에서는 null.
4944
@JdbcTypeCode(SqlTypes.JSON)
50-
@Column(name = "ingredients", columnDefinition = "jsonb")
51-
private List<String> ingredients;
52-
53-
// owner_question 전용 — AI 완성 문구. 템플릿형에서는 null.
54-
@JdbcTypeCode(SqlTypes.JSON)
55-
@Column(name = "text", columnDefinition = "jsonb")
45+
@Column(name = "text", columnDefinition = "jsonb", nullable = false)
5646
private CardText text;
5747

58-
// owner_question 전용 — AI 애매 사유 플래그(예: has_unclear_broth).
59-
@Column(name = "flag", length = 50)
60-
private String flag;
61-
6248
@Column(name = "scan_session_id", columnDefinition = "uuid")
6349
private UUID scanSessionId;
6450

6551
@Column(name = "last_saved_at", nullable = false)
6652
private Instant lastSavedAt;
6753

68-
private SavedCard(
69-
UUID userId,
70-
CardType cardType,
71-
String menuNameKo,
72-
List<String> ingredients,
73-
CardText text,
74-
String flag,
75-
UUID scanSessionId) {
54+
private SavedCard(UUID userId, CardType cardType, String menuNameKo, CardText text, UUID scanSessionId) {
7655
this.id = UUID.randomUUID();
7756
this.userId = userId;
7857
this.cardType = cardType;
7958
this.menuNameKo = menuNameKo;
80-
this.ingredients = ingredients;
8159
this.text = text;
82-
this.flag = flag;
8360
this.scanSessionId = scanSessionId;
8461
this.lastSavedAt = Instant.now();
8562
}
8663

8764
public static SavedCard create(
88-
UUID userId,
89-
CardType cardType,
90-
String menuNameKo,
91-
List<String> ingredients,
92-
CardText text,
93-
String flag,
94-
UUID scanSessionId) {
95-
return new SavedCard(userId, cardType, menuNameKo, ingredients, text, flag, scanSessionId);
65+
UUID userId, CardType cardType, String menuNameKo, CardText text, UUID scanSessionId) {
66+
return new SavedCard(userId, cardType, menuNameKo, text, scanSessionId);
9667
}
9768

9869
/** 동일 카드 재저장 시 호출 — 목록 최상단으로 올라오도록 저장 시각만 갱신한다. */

‎src/main/java/com/hanspoon/backend_api/domain/card/service/CardService.java‎

Lines changed: 6 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
import com.hanspoon.backend_api.global.common.PageResponse;
99
import com.hanspoon.backend_api.global.exception.BusinessException;
1010
import com.hanspoon.backend_api.global.exception.ErrorCode;
11-
import java.util.Objects;
1211
import java.util.UUID;
1312
import org.springframework.data.domain.Pageable;
1413
import org.springframework.stereotype.Service;
@@ -17,8 +16,8 @@
1716
/**
1817
* 자주 쓰는 카드 저장/목록/삭제. 문구·재료는 FE 가 구성하고 서버는 저장만 담당한다.
1918
*
20-
* <p>동일 카드 재저장 시 새 행을 만들지 않고 기존 카드의 저장 시각만 갱신해 목록 최상단으로 올린다.
21-
* 동일 판정은 타입별 페이로드(템플릿형=ingredients, owner_question=text)로 비교한다.
19+
* <p>카드는 화면에 보이는 텍스트를 그대로 스냅샷 저장한다(언어 재렌더 없음). 동일 카드 재저장 시 새 행을 만들지 않고
20+
* 기존 카드의 저장 시각만 갱신해 목록 최상단으로 올린다. 동일 판정은 (user, type, menuNameKo, text)로 한다.
2221
*/
2322
@Service
2423
public class CardService {
@@ -39,22 +38,16 @@ public SavedCardResponse save(UUID userId, SaveCardRequest request) {
3938
savedCardRepository
4039
.findByUserIdAndCardTypeAndMenuNameKo(userId, request.type(), request.menuNameKo())
4140
.stream()
42-
.filter(card -> isSamePayload(card, request))
41+
.filter(card -> card.getText().equals(request.text()))
4342
.findFirst()
4443
.orElse(null);
4544
if (duplicate != null) {
46-
duplicate.resave(); // 중복은 새로 만들지 않고 최상단으로
45+
duplicate.resave(); // 중복(동일 type/menuNameKo/text)은 새로 만들지 않고 최상단으로
4746
return SavedCardResponse.from(duplicate);
4847
}
4948

50-
SavedCard saved = savedCardRepository.save(SavedCard.create(
51-
userId,
52-
request.type(),
53-
request.menuNameKo(),
54-
request.ingredients(),
55-
request.text(),
56-
request.flag(),
57-
scanId));
49+
SavedCard saved = savedCardRepository.save(
50+
SavedCard.create(userId, request.type(), request.menuNameKo(), request.text(), scanId));
5851
return SavedCardResponse.from(saved);
5952
}
6053

@@ -72,15 +65,6 @@ public void delete(UUID userId, UUID cardId) {
7265
savedCardRepository.delete(card);
7366
}
7467

75-
/** 같은 (user, type, menuNameKo) 후보 중 타입별 페이로드까지 동일하면 중복으로 본다. */
76-
private boolean isSamePayload(SavedCard card, SaveCardRequest request) {
77-
return switch (request.type()) {
78-
case ORDER -> true; // 키(user+type+menuNameKo)만으로 동일
79-
case INGREDIENT_CHECK, EXCLUDE -> Objects.equals(card.getIngredients(), request.ingredients());
80-
case OWNER_QUESTION -> Objects.equals(card.getText(), request.text());
81-
};
82-
}
83-
8468
/** scanId 제공 시 본인 소유 스캔인지 검증하고 그대로 반환. 미제공이면 null. */
8569
private UUID resolveScanId(UUID userId, UUID scanId) {
8670
if (scanId == null) {

‎src/main/java/com/hanspoon/backend_api/global/config/DevDataSeeder.java‎

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -182,19 +182,21 @@ private void seedScans(UUID userId) {
182182
}
183183

184184
private void seedCards(UUID userId) {
185+
// 모든 카드는 화면에 보이는 text 를 그대로 저장(언어 스냅샷). 재료확인 text 는 AI owner_card.question 예시.
185186
savedCardRepository.saveAll(List.of(
186-
// 템플릿형: ingredients(hit 코드)만 저장 → FE 가 언어별 문장 재렌더
187-
SavedCard.create(userId, CardType.ORDER, "삼겹살", null, null, null, null),
188-
SavedCard.create(userId, CardType.INGREDIENT_CHECK, "김치찌개", List.of("is_fish"), null, null, null),
189-
SavedCard.create(userId, CardType.EXCLUDE, "비빔밥", List.of("is_egg"), null, null, null),
190-
// owner_question: AI 완성 문구 text + flag 저장
191187
SavedCard.create(
192188
userId,
193-
CardType.OWNER_QUESTION,
189+
CardType.ORDER,
190+
"삼겹살",
191+
new CardText("삼겹살 하나 주세요.", "One pork belly, please.", null),
192+
null),
193+
SavedCard.create(
194+
userId,
195+
CardType.INGREDIENT_CHECK,
194196
"된장찌개",
195-
null,
196-
new CardText("멸치 육수를 사용하나요?", "Do you use anchovy broth?", null),
197-
"has_unclear_broth",
198-
null)));
197+
new CardText("이 메뉴에 멸치육수가 들어가나요?", "Does this dish contain fish broth?", null),
198+
null),
199+
SavedCard.create(
200+
userId, CardType.EXCLUDE, "비빔밥", new CardText("계란 빼주세요.", "No egg, please.", null), null)));
199201
}
200202
}

‎src/main/resources/db/migration/V1__auth_and_profile.sql‎

Lines changed: 0 additions & 60 deletions
This file was deleted.

0 commit comments

Comments
 (0)