Files
tasteby/backend-java/src/main/java/com/tasteby/service/OciGenAiService.java
joungmin cdee37e341 UI/UX 개선: 모바일 네비게이션, 로그인 모달, 지도 기능, 캐치테이블 연동
- 모바일 하단 네비게이션(홈/식당목록/내주변/찜/내정보) 추가
- 로그인 버튼을 모달 방식으로 변경 (소셜 로그인 확장 가능)
- 내위치 기반 정렬 및 영역 필터, 지도 내위치 버튼 추가
- 채널 필터 시 해당 채널만 마커/범례 표시
- 캐치테이블 검색/연동 (단건/벌크), NONE 저장 패턴
- 벌크 트랜스크립트 SSE (Playwright 브라우저 재사용)
- 테이블링/캐치테이블 버튼 UI 차별화
- Google Maps 링크 모바일 호환, 초기화 버튼, 검색 라벨 개선

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 00:49:16 +09:00

183 lines
6.8 KiB
Java

package com.tasteby.service;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.oracle.bmc.ConfigFileReader;
import com.oracle.bmc.auth.ConfigFileAuthenticationDetailsProvider;
import com.oracle.bmc.generativeaiinference.GenerativeAiInferenceClient;
import com.oracle.bmc.generativeaiinference.model.*;
import com.oracle.bmc.generativeaiinference.requests.ChatRequest;
import com.oracle.bmc.generativeaiinference.requests.EmbedTextRequest;
import com.oracle.bmc.generativeaiinference.responses.ChatResponse;
import com.oracle.bmc.generativeaiinference.responses.EmbedTextResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@Service
public class OciGenAiService {
private static final Logger log = LoggerFactory.getLogger(OciGenAiService.class);
private static final int EMBED_BATCH_SIZE = 96;
@Value("${app.oci.compartment-id}")
private String compartmentId;
@Value("${app.oci.chat-endpoint}")
private String chatEndpoint;
@Value("${app.oci.embed-endpoint}")
private String embedEndpoint;
@Value("${app.oci.chat-model-id}")
private String chatModelId;
@Value("${app.oci.embed-model-id}")
private String embedModelId;
private final ObjectMapper mapper;
private ConfigFileAuthenticationDetailsProvider authProvider;
private GenerativeAiInferenceClient chatClient;
private GenerativeAiInferenceClient embedClient;
public OciGenAiService(ObjectMapper mapper) {
this.mapper = mapper;
}
@PostConstruct
public void init() {
try {
ConfigFileReader.ConfigFile configFile = ConfigFileReader.parseDefault();
authProvider = new ConfigFileAuthenticationDetailsProvider(configFile);
chatClient = GenerativeAiInferenceClient.builder()
.endpoint(chatEndpoint).build(authProvider);
embedClient = GenerativeAiInferenceClient.builder()
.endpoint(embedEndpoint).build(authProvider);
log.info("OCI GenAI auth configured (clients initialized)");
} catch (Exception e) {
log.warn("OCI config not found, GenAI features disabled: {}", e.getMessage());
}
}
@PreDestroy
public void destroy() {
if (chatClient != null) chatClient.close();
if (embedClient != null) embedClient.close();
}
/**
* Call OCI GenAI LLM (Chat).
*/
public String chat(String prompt, int maxTokens) {
if (chatClient == null) throw new IllegalStateException("OCI GenAI not configured");
var textContent = TextContent.builder().text(prompt).build();
var userMessage = UserMessage.builder().content(List.of(textContent)).build();
var chatRequest = GenericChatRequest.builder()
.messages(List.of(userMessage))
.maxTokens(maxTokens)
.temperature(0.0)
.build();
var chatDetails = ChatDetails.builder()
.compartmentId(compartmentId)
.servingMode(OnDemandServingMode.builder().modelId(chatModelId).build())
.chatRequest(chatRequest)
.build();
ChatResponse response = chatClient.chat(
ChatRequest.builder().chatDetails(chatDetails).build());
var chatResult = (GenericChatResponse) response.getChatResult().getChatResponse();
var choice = chatResult.getChoices().get(0);
var content = ((TextContent) choice.getMessage().getContent().get(0)).getText();
return content.trim();
}
/**
* Generate embeddings for a list of texts.
*/
public List<List<Double>> embedTexts(List<String> texts) {
if (authProvider == null) throw new IllegalStateException("OCI GenAI not configured");
List<List<Double>> allEmbeddings = new ArrayList<>();
for (int i = 0; i < texts.size(); i += EMBED_BATCH_SIZE) {
List<String> batch = texts.subList(i, Math.min(i + EMBED_BATCH_SIZE, texts.size()));
allEmbeddings.addAll(embedBatch(batch));
}
return allEmbeddings;
}
private List<List<Double>> embedBatch(List<String> texts) {
if (embedClient == null) throw new IllegalStateException("OCI GenAI not configured");
var embedDetails = EmbedTextDetails.builder()
.inputs(texts)
.servingMode(OnDemandServingMode.builder().modelId(embedModelId).build())
.compartmentId(compartmentId)
.inputType(EmbedTextDetails.InputType.SearchDocument)
.build();
EmbedTextResponse response = embedClient.embedText(
EmbedTextRequest.builder().embedTextDetails(embedDetails).build());
return response.getEmbedTextResult().getEmbeddings()
.stream()
.map(emb -> emb.stream().map(Number::doubleValue).toList())
.toList();
}
/**
* Parse LLM response as JSON (handles markdown code blocks, truncated arrays, etc.)
*/
public Object parseJson(String raw) {
// Strip markdown code blocks
raw = raw.replaceAll("(?m)^```(?:json)?\\s*|\\s*```$", "").trim();
// Remove trailing commas
raw = raw.replaceAll(",\\s*([}\\]])", "$1");
try {
return mapper.readValue(raw, Object.class);
} catch (Exception ignored) {}
// Try to recover truncated array
if (raw.trim().startsWith("[")) {
List<Object> items = new ArrayList<>();
int idx = raw.indexOf('[') + 1;
while (idx < raw.length()) {
while (idx < raw.length() && " \t\n\r,".indexOf(raw.charAt(idx)) >= 0) idx++;
if (idx >= raw.length() || raw.charAt(idx) == ']') break;
// Try to parse next object
boolean found = false;
for (int end = idx + 1; end <= raw.length(); end++) {
try {
Object obj = mapper.readValue(raw.substring(idx, end), Object.class);
items.add(obj);
idx = end;
found = true;
break;
} catch (Exception ignored2) {}
}
if (!found) break;
}
if (!items.isEmpty()) {
log.info("Recovered {} items from truncated JSON", items.size());
return items;
}
}
throw new RuntimeException("JSON parse failed: " + raw.substring(0, Math.min(80, raw.length())));
}
}