|
11 | 11 | import com.trashheroesbe.feature.trash.dto.response.TrashAnalysisResponseDto; |
12 | 12 | import com.trashheroesbe.feature.trash.domain.Type; |
13 | 13 | import com.trashheroesbe.feature.trash.domain.entity.TrashType; |
| 14 | +import com.trashheroesbe.feature.trash.infrastructure.TrashItemRepository; |
| 15 | +import com.trashheroesbe.feature.trash.infrastructure.TrashTypeRepository; |
14 | 16 | import com.trashheroesbe.global.exception.BusinessException; |
15 | 17 | import com.trashheroesbe.infrastructure.port.gpt.ChatAIClientPort; |
16 | | -import java.util.EnumMap; |
17 | 18 | import java.util.List; |
18 | 19 | import java.util.Map; |
19 | | -import java.util.Optional; |
| 20 | +import java.util.concurrent.ConcurrentHashMap; |
20 | 21 | import java.util.stream.Collectors; |
21 | 22 | import lombok.extern.slf4j.Slf4j; |
22 | 23 | import org.springframework.ai.chat.client.ChatClient; |
23 | 24 | import org.springframework.core.io.ByteArrayResource; |
24 | 25 | import org.springframework.stereotype.Service; |
25 | 26 | import org.springframework.util.MimeType; |
26 | 27 | import org.springframework.util.MimeTypeUtils; |
| 28 | +import com.trashheroesbe.feature.trash.domain.entity.TrashItem; |
27 | 29 |
|
28 | 30 | @Service |
29 | 31 | @Slf4j |
30 | 32 | public class OpenAIChatAdapter implements ChatAIClientPort { |
31 | 33 |
|
32 | 34 | private final ObjectMapper om = new ObjectMapper(); |
33 | 35 | private final ChatClient chatClient; |
| 36 | + private final TrashItemRepository trashItemRepository; |
| 37 | + private final TrashTypeRepository trashTypeRepository; |
34 | 38 |
|
35 | | - public OpenAIChatAdapter(ChatClient.Builder builder) { |
| 39 | + private final Map<Type, List<String>> itemCache = new ConcurrentHashMap<>(); |
| 40 | + |
| 41 | + public OpenAIChatAdapter(ChatClient.Builder builder, final TrashItemRepository trashItemRepository, final TrashTypeRepository trashTypeRepository) { |
36 | 42 | this.chatClient = builder.build(); |
| 43 | + this.trashItemRepository = trashItemRepository; |
| 44 | + this.trashTypeRepository = trashTypeRepository; |
37 | 45 | } |
38 | 46 |
|
39 | 47 | @Override |
@@ -113,8 +121,9 @@ public Type findSimilarTrashItem(String keyword) { |
113 | 121 | } |
114 | 122 |
|
115 | 123 | private String buildSimilarTrashTypePrompt(String keyword) { |
116 | | - String availableTypes = ALLOWED_ITEMS.keySet().stream() |
117 | | - .map(Type::name) |
| 124 | + var types = trashTypeRepository.findAllTypes(); |
| 125 | + String availableTypes = types.stream() |
| 126 | + .map(Enum::name) |
118 | 127 | .collect(Collectors.joining(", ")); |
119 | 128 |
|
120 | 129 | return String.format(""" |
@@ -158,32 +167,34 @@ private String buildTypePrompt() { |
158 | 167 | """.formatted(allowed); |
159 | 168 | } |
160 | 169 |
|
161 | | - private final Map<Type, List<String>> ALLOWED_ITEMS = new EnumMap<>(Type.class) {{ |
162 | | - put(Type.PAPER, List.of("일반 종이류")); |
163 | | - put(Type.PAPER_PACK, List.of("종이팩")); |
164 | | - put(Type.PLASTIC, List.of("플라스틱 병", "음식 용기", "과일용기", "샴푸병")); |
165 | | - put(Type.PET, List.of("PET(투명 페트병)", "PET(유색 페트병)")); |
166 | | - put(Type.VINYL_FILM, List.of("비닐봉투", "뽁뽁이", "아이스팩")); |
167 | | - put(Type.STYROFOAM, List.of("완충 포장재", "라면용기", "식품 포장상자", "아이스박스")); |
168 | | - put(Type.GLASS, List.of("투명 유리병", "유색 유리병")); |
169 | | - put(Type.METAL, List.of("알루미늄 캔", "철 캔")); |
170 | | - put(Type.TEXTILES, List.of("의류", "섬유")); |
171 | | - put(Type.E_WASTE, List.of("냉장고", "TV", "핸드폰", "라디오")); |
172 | | - put(Type.HAZARDOUS_SMALL_WASTE, List.of("폐형광등", "폐건전지", "보조배터리")); |
173 | | - put(Type.FOOD_WASTE, List.of("야채,과일 껍질", "남은음식", "뼈(닭 등의 뼈다귀, 생선뼈)", "껍데기(갑각류, 어패류)")); |
174 | | - put(Type.NON_RECYCLABLE, List.of("일반쓰레기", "기름이 묻은 종이", "카드영수증", "부서진 그릇")); |
175 | | - }}; |
| 170 | + private List<String> loadAllowedItems(Type type) { |
| 171 | + return itemCache.computeIfAbsent(type, t -> |
| 172 | + trashItemRepository.findByTrashType_Type(t).stream() |
| 173 | + .map(TrashItem::getName) |
| 174 | + .filter(s -> s != null && !s.isBlank()) |
| 175 | + .toList() |
| 176 | + ); |
| 177 | + } |
176 | 178 |
|
177 | 179 | private String buildItemPrompt(Type type) { |
178 | | - List<String> items = ALLOWED_ITEMS.getOrDefault(type, List.of()); |
179 | | - String list = items.stream().map(s -> "\"" + s + "\"").collect(Collectors.joining(", ")); |
| 180 | + List<String> items = loadAllowedItems(type); |
| 181 | + if (items.isEmpty()) { |
| 182 | + // 품목 테이블에 데이터가 없는 타입은 목록 제한 없이 간단 규칙만 제공 |
| 183 | + return """ |
| 184 | + 너는 쓰레기 분류 전문가다. 세부 품목(item)만 판단하여 JSON으로 반환하라. |
| 185 | + - JSON(객체)만 반환. 코드블록, 여분 텍스트 금지. |
| 186 | + - type=%s 이다. item은 한국어로 간결히 작성: |
| 187 | + - 출력 형식: {"item":"PET(투명 페트병)"} |
| 188 | + """.formatted(type.name()); |
| 189 | + } |
| 190 | + String list = items.stream().map(s -> "\"" + s + "\"").collect(java.util.stream.Collectors.joining(", ")); |
180 | 191 | return """ |
181 | 192 | 너는 쓰레기 분류 전문가다. 세부 품목(item)만 판단하여 JSON으로 반환하라. |
182 | 193 | - JSON(객체)만 반환. 코드블록, 여분 텍스트 금지. |
183 | 194 | - type=%s 이며, item은 아래 허용 목록 중 정확히 하나(한국어)만 선택: |
184 | 195 | [ %s ] |
185 | | - - 출력 형식: {"item":"PET(투명 페트병)"} |
186 | | - """.formatted(type.name(), list); |
| 196 | + - 출력 형식: {"item":"%s"} |
| 197 | + """.formatted(type.name(), list, items.get(0)); |
187 | 198 | } |
188 | 199 |
|
189 | 200 | private TrashAnalysisResponseDto parseTypeResponse(String content) { |
|
0 commit comments