Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -40,23 +40,13 @@ public class VertexAiEmbeddingService {
* @param embeddingType CONCERT, AGENT, SEARCH
*/
@Transactional
@Caching(
cacheable = @Cacheable(
value = "embeddings",
key = "T(com.ticketmate.backend.common.core.util.CommonUtil)"
+ ".normalizeAndRemoveSpecialCharacters(#text)"
+ "+':' + #embeddingType",
condition = "#targetId == null",
unless = "#result == null"
)
)
public Embedding fetchOrGenerateEmbedding(UUID targetId, String text, EmbeddingType embeddingType) {
// 텍스트 정규화
String normalizeText = CommonUtil.normalizeAndRemoveSpecialCharacters(text);

// 검색 모드: 캐시나 DB 조회 후 없으면 생성
return embeddingRepository.findByTextAndEmbeddingType(normalizeText, embeddingType)
.orElseGet(() -> createAndSaveEmbedding(targetId, normalizeText, embeddingType));
.orElseGet(() -> createAndSaveEmbedding(targetId, normalizeText, embeddingType));
}

/**
Expand All @@ -67,11 +57,11 @@ private Embedding createAndSaveEmbedding(UUID targetId, String normalizedText, E
float[] vector = extractVector(generateEmbedding(normalizedText));

return embeddingRepository.save(Embedding.builder()
.targetId(targetId)
.text(normalizedText)
.embeddingVector(vector)
.embeddingType(type)
.build());
.targetId(targetId)
.text(normalizedText)
.embeddingVector(vector)
.embeddingType(type)
.build());
}

/**
Expand All @@ -83,9 +73,9 @@ private Embedding createAndSaveEmbedding(UUID targetId, String normalizedText, E
private EmbedContentResponse generateEmbedding(String text) {
try {
return genAiClient.models.embedContent(
googleGenAIProperties.model(),
text,
EmbedContentConfig.builder().build()
googleGenAIProperties.model(),
text,
EmbedContentConfig.builder().build()
);
} catch (ClientException e) {
log.error("Vertex AI 임베딩 API 호출 실패: {}", e.getMessage());
Expand All @@ -101,9 +91,9 @@ private EmbedContentResponse generateEmbedding(String text) {
*/
private float[] extractVector(EmbedContentResponse response) {
List<Float> embeddingValues = response.embeddings()
.flatMap(list -> list.stream().findFirst())
.flatMap(ContentEmbedding::values)
.orElseThrow(() -> new CustomException(ErrorCode.EMBEDDING_DATA_NOT_FOUND));
.flatMap(list -> list.stream().findFirst())
.flatMap(ContentEmbedding::values)
.orElseThrow(() -> new CustomException(ErrorCode.EMBEDDING_DATA_NOT_FOUND));
int size = embeddingValues.size();
float[] vector = new float[size];
for (int i = 0; i < size; i++) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ public interface EmbeddingRepository extends JpaRepository<Embedding, UUID> {
*/
@Query(value = "SELECT target_id FROM embedding e "
+ "WHERE e.embedding_type = 'CONCERT' "
+ "AND (e.embedding_vector <-> CAST(:vector AS vector)) <= 0.90 "
+ "AND EXISTS "
+ "(SELECT 1 FROM ticket_open_date t "
+ "JOIN concert c ON t.concert_concert_id = c.concert_id "
Expand All @@ -43,6 +44,7 @@ public interface EmbeddingRepository extends JpaRepository<Embedding, UUID> {
*/
@Query(value = "SELECT target_id FROM embedding "
+ "WHERE embedding_type = 'AGENT' "
+ "AND (embedding_vector <-> CAST(:vector AS vector)) <= 0.90 "
+ "ORDER BY embedding_vector <-> CAST(:vector AS vector) LIMIT :limit", nativeQuery = true)
List<UUID> findNearestAgentEmbeddings(@Param("vector") float[] vector, @Param("limit") int limit);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
Expand Down Expand Up @@ -102,13 +103,23 @@ public void acknowledgeRead(ReadAckRequest ack, Member reader, String chatRoomId
String unReadRedisKey = UN_READ_MESSAGE_COUNTER_KEY.formatted(chatRoomId, reader.getMemberId());
redisTemplate.delete(unReadRedisKey);

// 채팅방 리스트에 즉시 갱신하기 위한 코드
ChatMessage lastReadMessage = chatMessageRepository.findById(ack.getLastReadMessageId())
.orElseThrow(() -> new CustomException(ErrorCode.MESSAGE_NOT_FOUND));

String preview = toPreview(lastReadMessage);
String formattedSendDate = formattingSendDate(TimeUtil.toLocalDateTime(lastReadMessage.getSendDate()));

rabbitTemplate.convertAndSend(
"",
chatRabbitMqProperties.unreadRoutingKey() + reader.getMemberId(),
Map.of("chatRoomId", chatRoomId,
"unReadMessageCount", 0,
"lastMessageId", ack.getLastReadMessageId())
buildUnreadTopicPayload(
chatRoomId,
0L,
lastReadMessage.getChatMessageId(),
lastReadMessage.getChatMessageType(),
preview,
formattedSendDate
)
);

ChatRoom chatRoom = chatRoomService.findChatRoomById(chatRoomId);
Expand Down Expand Up @@ -275,14 +286,15 @@ private ChatMessage handleNewChatMessage(Member sender, ChatMessageRequest reque
rabbitTemplate.convertAndSend(
"",
chatRabbitMqProperties.unreadRoutingKey() + chatRoomMemberId,
Map.of(
"chatRoomId", chatRoom.getChatRoomId(),
"unReadMessageCount", count,
"lastMessage", request.toPreview(),
"lastMessageType", request.getType(),
"sendDate", formattedSendDate,
"lastMessageId", message.getChatMessageId()
));
buildUnreadTopicPayload(
chatRoom.getChatRoomId(),
count,
message.getChatMessageId(),
request.getType(),
request.toPreview(),
formattedSendDate
)
);
}
return message;
}
Expand Down Expand Up @@ -429,4 +441,29 @@ private void updateLastMessageInfo(ChatRoom chatRoom, ChatMessage chatMessage, S

chatRoom.updateLastMessageType(chatMessage.getChatMessageType());
}

private Map<String, Object> buildUnreadTopicPayload(String chatRoomId, long unReadMessageCount, String lastMessageId,
ChatMessageType lastMessageType, String lastMessage, String sendDate
) {
Map<String, Object> payload = new LinkedHashMap<>();
payload.put("lastMessageId", lastMessageId);
payload.put("lastMessageType", lastMessageType);
payload.put("unReadMessageCount", unReadMessageCount);
payload.put("chatRoomId", chatRoomId);
payload.put("lastMessage", lastMessage == null ? "" : lastMessage);
payload.put("sendDate", sendDate == null ? "" : sendDate);
return payload;
}


private String toPreview(ChatMessage chatMessage) {
return switch (chatMessage.getChatMessageType()) {
case TEXT -> nvl(chatMessage.getMessage(), "");
case PICTURE -> ChatMessageType.PICTURE.getDescription();
case FULFILLMENT_FORM -> ChatMessageType.FULFILLMENT_FORM.getDescription();
case ACCEPTED_FULFILLMENT_FORM -> ChatMessageType.ACCEPTED_FULFILLMENT_FORM.getDescription();
case REJECTED_FULFILLMENT_FORM -> ChatMessageType.REJECTED_FULFILLMENT_FORM.getDescription();
case UPDATE_FULFILLMENT_FORM -> ChatMessageType.UPDATE_FULFILLMENT_FORM.getDescription();
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,44 +50,44 @@ public List<ConcertSearchInfo> findConcertDetailsByIds(List<UUID> concertIds) {
return Collections.emptyList();
}
return queryFactory
.select(Projections.constructor(ConcertSearchInfo.class,
CONCERT.concertId,
CONCERT.concertName,
CONCERT_HALL.concertHallName,
// 선예매 오픈일
Expressions.dateTimeTemplate(
Instant.class,
"min({0})",
new CaseBuilder()
.when(TICKET_OPEN_DATE.ticketOpenType.eq(TicketOpenType.PRE_OPEN))
.then(TICKET_OPEN_DATE.openDate)
.otherwise((Instant) null)
).as("ticketPreOpenDate"),
// 일반 예매 오픈일
Expressions.dateTimeTemplate(
Instant.class,
"min({0})",
new CaseBuilder()
.when(TICKET_OPEN_DATE.ticketOpenType.eq(TicketOpenType.GENERAL_OPEN))
.then(TICKET_OPEN_DATE.openDate)
.otherwise((Instant) null)
).as("ticketGeneralOpenDate"),
CONCERT_DATE.performanceDate.min().as("startDate"),
CONCERT_DATE.performanceDate.max().as("endDate"),
CONCERT.concertThumbnailStoredPath,
Expressions.constant(0.0)
))
.from(CONCERT)
.leftJoin(CONCERT.concertHall, CONCERT_HALL)
.join(CONCERT_DATE).on(CONCERT.eq(CONCERT_DATE.concert))
.join(TICKET_OPEN_DATE).on(CONCERT.eq(TICKET_OPEN_DATE.concert))
.where(CONCERT.concertId.in(concertIds))
.groupBy(CONCERT.concertId,
CONCERT.concertName,
CONCERT_HALL.concertHallName,
CONCERT.concertThumbnailStoredPath
)
.fetch();
.select(Projections.constructor(ConcertSearchInfo.class,
CONCERT.concertId,
CONCERT.concertName,
CONCERT_HALL.concertHallName,
// 선예매 오픈일
Expressions.dateTimeTemplate(
Instant.class,
"min({0})",
new CaseBuilder()
.when(TICKET_OPEN_DATE.ticketOpenType.eq(TicketOpenType.PRE_OPEN))
.then(TICKET_OPEN_DATE.openDate)
.otherwise((Instant) null)
).as("ticketPreOpenDate"),
// 일반 예매 오픈일
Expressions.dateTimeTemplate(
Instant.class,
"min({0})",
new CaseBuilder()
.when(TICKET_OPEN_DATE.ticketOpenType.eq(TicketOpenType.GENERAL_OPEN))
.then(TICKET_OPEN_DATE.openDate)
.otherwise((Instant) null)
).as("ticketGeneralOpenDate"),
CONCERT_DATE.performanceDate.min().as("startDate"),
CONCERT_DATE.performanceDate.max().as("endDate"),
CONCERT.concertThumbnailStoredPath,
Expressions.constant(0.0)
))
.from(CONCERT)
.leftJoin(CONCERT.concertHall, CONCERT_HALL)
.join(CONCERT_DATE).on(CONCERT.eq(CONCERT_DATE.concert))
.join(TICKET_OPEN_DATE).on(CONCERT.eq(TICKET_OPEN_DATE.concert))
.where(CONCERT.concertId.in(concertIds))
.groupBy(CONCERT.concertId,
CONCERT.concertName,
CONCERT_HALL.concertHallName,
CONCERT.concertThumbnailStoredPath
)
.fetch();
}

/**
Expand All @@ -102,20 +102,20 @@ public List<AgentSearchInfo> findAgentDetailsByIds(List<UUID> agentIds) {
return Collections.emptyList();
}
return queryFactory
.select(Projections.constructor(AgentSearchInfo.class,
MEMBER.memberId,
MEMBER.nickname,
MEMBER.profileImgStoredPath,
PORTFOLIO.portfolioDescription,
AGENT_PERFORMANCE_SUMMARY.averageRating,
AGENT_PERFORMANCE_SUMMARY.reviewCount,
Expressions.constant(0.0)
))
.from(MEMBER)
.leftJoin(PORTFOLIO).on(PORTFOLIO.member.eq(MEMBER))
.innerJoin(AGENT_PERFORMANCE_SUMMARY).on(MEMBER.eq(AGENT_PERFORMANCE_SUMMARY.agent))
.where(MEMBER.memberId.in(agentIds))
.fetch();
.select(Projections.constructor(AgentSearchInfo.class,
MEMBER.memberId,
MEMBER.nickname,
MEMBER.profileImgStoredPath,
PORTFOLIO.portfolioDescription,
AGENT_PERFORMANCE_SUMMARY.averageRating,
AGENT_PERFORMANCE_SUMMARY.reviewCount,
Expressions.constant(0.0)
))
.from(MEMBER)
.leftJoin(PORTFOLIO).on(PORTFOLIO.member.eq(MEMBER))
.innerJoin(AGENT_PERFORMANCE_SUMMARY).on(MEMBER.eq(AGENT_PERFORMANCE_SUMMARY.agent))
.where(MEMBER.memberId.in(agentIds))
.fetch();
}

/**
Expand All @@ -129,21 +129,21 @@ public List<AgentSearchInfo> findAgentDetailsByIds(List<UUID> agentIds) {
public List<UUID> findAgentIdsByKeyword(String keyword, int limit) {
// 동적 WHERE 절 조합
BooleanExpression whereClause = QueryDslUtil.allOf(
MEMBER.memberType.eq(MemberType.AGENT),
QueryDslUtil.anyOf(
QueryDslUtil.likeIgnoreCase(MEMBER.nickname, keyword),
QueryDslUtil.likeIgnoreCase(PORTFOLIO.portfolioDescription, keyword)
)
MEMBER.memberType.eq(MemberType.AGENT),
QueryDslUtil.anyOf(
QueryDslUtil.likeIgnoreCase(MEMBER.nickname, keyword),
QueryDslUtil.likeIgnoreCase(PORTFOLIO.portfolioDescription, keyword)
)
);

return queryFactory
.select(MEMBER.memberId)
.from(MEMBER)
.innerJoin(PORTFOLIO)
.on((PORTFOLIO.member).eq(MEMBER))
.where(whereClause)
.limit(limit)
.fetch();
.select(MEMBER.memberId)
.from(MEMBER)
.innerJoin(PORTFOLIO)
.on((PORTFOLIO.member).eq(MEMBER))
.where(whereClause)
.limit(limit)
.fetch();
}

/**
Expand Down