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
2 changes: 2 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ dependencies {
implementation 'org.mariadb.jdbc:mariadb-java-client:3.4.1'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa:3.3.4'
implementation 'org.jboss.logging:jboss-logging:3.5.0.Final' //새로 추가 debug문제로 인해
implementation 'org.hibernate.common:hibernate-commons-annotations:6.0.6.Final' // 새로 추가 debug문제로 인해
implementation 'com.fasterxml:classmate:1.7.0' //새로 추가 debug문제로 인해
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
Expand Down
274 changes: 249 additions & 25 deletions src/main/java/com/example/gp_backend_data/card/service/CardService.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,19 @@

import com.example.gp_backend_data.card.domain.dto.request.CardCreateRequest;
import com.example.gp_backend_data.card.domain.dto.request.CardUpdateRequest;
import com.example.gp_backend_data.graph.domain.dto.response.GraphItemDto;
import com.example.gp_backend_data.card.domain.dto.response.*;
import com.example.gp_backend_data.card.domain.entity.Card;
import com.example.gp_backend_data.card.repository.CardlinkRepository;
import com.example.gp_backend_data.card.repository.CardRepository;
import com.example.gp_backend_data.graph.repository.GraphRepository;
import com.example.gp_backend_data.space.domain.entity.Space;
import com.example.gp_backend_data.space.repository.SpaceRepository;
import com.example.gp_backend_data.utils.UUIDHelper;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import jakarta.persistence.EntityNotFoundException;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
Expand All @@ -34,6 +40,7 @@
public class CardService {
private final SpaceRepository spaceRepository;
private final CardRepository cardRepository;
private final GraphRepository graphRepository;
private final CardlinkRepository cardlinkRepository;
private final UUIDHelper uuidHelper;

Expand All @@ -53,40 +60,124 @@ private Card getCardWithAccessCheck(UUID userId, UUID cardId) {
return card;
}

//새 지식카드 생성 - 사용자가 직접 생성
//새 지식카드 생성 - 사용자가 직접 생성(그래프 반영 되는 버전)
@Transactional
public CardCreateResponse createCard(UUID userId, CardCreateRequest request) {

validateUserAccessToSpace(userId, uuidHelper.convertUUIDToByteArray(request.getSpaceId()));

// 한국 시간으로 LocalDateTime 생성
LocalDateTime nowInKorea = ZonedDateTime.now(ZoneId.of("Asia/Seoul")).toLocalDateTime();

UUID newCardId = UUID.randomUUID();

Card card = Card.builder()
.cardId(uuidHelper.convertUUIDToByteArray(UUID.randomUUID()))
.cardId(uuidHelper.convertUUIDToByteArray(newCardId))
.spaceId(uuidHelper.convertUUIDToByteArray(request.getSpaceId()))
.title(request.getTitle())
//.color(uuidHelper.convertHexToBytes(request.getColor()))
.aiSummary("")
//.userSummary("")
.createdAt(nowInKorea)
.build();

Card savedCard = cardRepository.save(card);

List<CardlinkResponse> cardlinks = List.of();
// 그래프에 추가
addCardToGraph(savedCard);

return CardCreateResponse.builder()
.cardId(uuidHelper.convertByteArrayToUUID(savedCard.getCardId()))
.spaceId(uuidHelper.convertByteArrayToUUID(savedCard.getSpaceId()))
.title(savedCard.getTitle())
//.color(uuidHelper.convertBytesToHex(savedCard.getColor()))
.cardlinks(cardlinks)
.cardlinks(List.of())
.aiSummary(savedCard.getAiSummary())
//.userSummary(savedCard.getUserSummary())
.createdAt(savedCard.getCreatedAt())
.build();
}

//새 카드 Graph.data 최상단에 추가
private void addCardToGraph(Card card) {
graphRepository.findBySpaceId(card.getSpaceId()).ifPresent(graph -> {
try {
ObjectMapper objectMapper = new ObjectMapper();
JsonNode rootNode;

if (graph.getData() == null || graph.getData().isBlank()) {
rootNode = objectMapper.createObjectNode();
((ObjectNode) rootNode).set("items", objectMapper.createArrayNode());
} else {
rootNode = objectMapper.readTree(graph.getData());
if (!rootNode.has("items")) {
((ObjectNode) rootNode).set("items", objectMapper.createArrayNode());
}
}

ArrayNode items = (ArrayNode) rootNode.get("items");

// GraphItemDto를 이용해 새 카드 생성
GraphItemDto graphItem = new GraphItemDto();
graphItem.setType("card");
graphItem.setName(card.getTitle());
graphItem.setCardId(uuidHelper.convertByteArrayToUUID(card.getCardId()));

// DTO → JSON 변환
JsonNode newCardNode = objectMapper.valueToTree(graphItem);

// ObjectNode newCardNode = objectMapper.createObjectNode();
// newCardNode.put("type", "card");
// newCardNode.put("name", card.getTitle());
// newCardNode.put("cardId", card.getCardId());


// 최상단에 삽입 (prepend)
ArrayNode updatedItems = objectMapper.createArrayNode();
updatedItems.add(newCardNode);
items.forEach(updatedItems::add);

((ObjectNode) rootNode).set("items", updatedItems);

graph.setData(objectMapper.writeValueAsString(rootNode));
graphRepository.save(graph);

} catch (Exception e) {
throw new RuntimeException("Graph 업데이트 중 오류 발생", e);
}
});
}


// //새 지식카드 생성 - 사용자가 직접 생성(그래프 반영 안되는 버전)
// public CardCreateResponse createCard(UUID userId, CardCreateRequest request) {
//
// validateUserAccessToSpace(userId, uuidHelper.convertUUIDToByteArray(request.getSpaceId()));
//
// // 한국 시간으로 LocalDateTime 생성
// LocalDateTime nowInKorea = ZonedDateTime.now(ZoneId.of("Asia/Seoul")).toLocalDateTime();
//
// Card card = Card.builder()
// .cardId(uuidHelper.convertUUIDToByteArray(UUID.randomUUID()))
// .spaceId(uuidHelper.convertUUIDToByteArray(request.getSpaceId()))
// .title(request.getTitle())
// //.color(uuidHelper.convertHexToBytes(request.getColor()))
// .aiSummary("")
// //.userSummary("")
// .createdAt(nowInKorea)
// .build();
//
// Card savedCard = cardRepository.save(card);
//
// List<CardlinkResponse> cardlinks = List.of();
//
// return CardCreateResponse.builder()
// .cardId(uuidHelper.convertByteArrayToUUID(savedCard.getCardId()))
// .spaceId(uuidHelper.convertByteArrayToUUID(savedCard.getSpaceId()))
// .title(savedCard.getTitle())
// //.color(uuidHelper.convertBytesToHex(savedCard.getColor()))
// .cardlinks(cardlinks)
// .aiSummary(savedCard.getAiSummary())
// //.userSummary(savedCard.getUserSummary())
// .createdAt(savedCard.getCreatedAt())
// .build();
// }

//지식카드 세부정보 조회
@Transactional(readOnly = true)
public CardCreateResponse getCardDetails(UUID userId, UUID cardId) {
Expand Down Expand Up @@ -213,28 +304,24 @@ public CardResponse updateCard(UUID userId, UUID cardId, CardUpdateRequest reque

Card card = getCardWithAccessCheck(userId, cardId);

// null이 아닌 값만 업데이트
if (request.getTitle() != null) {
card.setTitle(request.getTitle());
}
// if (request.getColor() != null) {
// card.setColor(uuidHelper.convertHexToBytes(request.getColor()));
// }
// if (request.getUserSummary() != null) {
// card.setUserSummary(request.getUserSummary());
// }

card.setUpdatedAt(LocalDateTime.now()); // 실제 엔티티에 반영

card.setUpdatedAt(LocalDateTime.now());
cardRepository.save(card);

List<CardlinkResponse> cardlinkDtos = cardlinkRepository.findByCard_CardId(uuidHelper.convertUUIDToByteArray(cardId)).stream()
//Graph 데이터 업데이트
if (request.getTitle() != null) {
updateGraphCardTitle(card.getSpaceId(), card.getCardId(), request.getTitle());
}

List<CardlinkResponse> cardlinkDtos = cardlinkRepository
.findByCard_CardId(uuidHelper.convertUUIDToByteArray(cardId)).stream()
.map(cardlink -> CardlinkResponse.builder()
.cardlinkId(uuidHelper.convertByteArrayToUUID(cardlink.getCardlinkId()))
.chatId(uuidHelper.convertByteArrayToUUID(cardlink.getChatId()))
.content(cardlink.getContent())
//.positionStart(cardlink.getPositionStart())
//.contentLength(cardlink.getContentLength())
.build())
.toList();

Expand All @@ -243,23 +330,159 @@ public CardResponse updateCard(UUID userId, UUID cardId, CardUpdateRequest reque
.spaceId(uuidHelper.convertByteArrayToUUID(card.getSpaceId()))
.title(card.getTitle())
.cardlinks(cardlinkDtos)
// .color(uuidHelper.convertBytesToHex(card.getColor()))
.aiSummary(card.getAiSummary())
// .userSummary(card.getUserSummary())
.createdAt(card.getCreatedAt())
.updatedAt(card.getUpdatedAt())
.build();
}

//지식카드 삭제
//Graph의 data(JSON)에서 카드 제목 업데이트
private void updateGraphCardTitle(byte[] spaceId, byte[] cardId, String newTitle) {
graphRepository.findBySpaceId(spaceId).ifPresent(graph -> {
try {
ObjectMapper objectMapper = new ObjectMapper();
JsonNode rootNode = objectMapper.readTree(graph.getData());

if (rootNode.has("items")) {
ArrayNode items = (ArrayNode) rootNode.get("items");
updateCardTitleInItems(items, uuidHelper.convertByteArrayToUUID(cardId).toString(), newTitle);
graph.setData(objectMapper.writeValueAsString(rootNode));
graphRepository.save(graph);
}
} catch (Exception e) {
throw new RuntimeException("Graph 업데이트 중 오류 발생", e);
}
});
}

//items 배열을 재귀적으로 탐색하면서 cardId가 일치하면 name(title) 수정
private void updateCardTitleInItems(ArrayNode items, String targetCardId, String newTitle) {
for (JsonNode item : items) {
if (item.has("type") && "card".equals(item.get("type").asText())) {
if (item.has("cardId") && targetCardId.equals(item.get("cardId").asText())) {
((ObjectNode) item).put("name", newTitle);
}
}
if (item.has("items")) {
ArrayNode childItems = (ArrayNode) item.get("items");
updateCardTitleInItems(childItems, targetCardId, newTitle);
}
}
}
// 그래프 업데이트가 없는 카드 수정(제목) 버전
// @Transactional
// public CardResponse updateCard(UUID userId, UUID cardId, CardUpdateRequest request) {
//
// Card card = getCardWithAccessCheck(userId, cardId);
//
// // null이 아닌 값만 업데이트
// if (request.getTitle() != null) {
// card.setTitle(request.getTitle());
// }
//// if (request.getColor() != null) {
//// card.setColor(uuidHelper.convertHexToBytes(request.getColor()));
//// }
//// if (request.getUserSummary() != null) {
//// card.setUserSummary(request.getUserSummary());
//// }
//
// card.setUpdatedAt(LocalDateTime.now()); // 실제 엔티티에 반영
//
// cardRepository.save(card);
//
// List<CardlinkResponse> cardlinkDtos = cardlinkRepository.findByCard_CardId(uuidHelper.convertUUIDToByteArray(cardId)).stream()
// .map(cardlink -> CardlinkResponse.builder()
// .cardlinkId(uuidHelper.convertByteArrayToUUID(cardlink.getCardlinkId()))
// .chatId(uuidHelper.convertByteArrayToUUID(cardlink.getChatId()))
// .content(cardlink.getContent())
// //.positionStart(cardlink.getPositionStart())
// //.contentLength(cardlink.getContentLength())
// .build())
// .toList();
//
// return CardResponse.builder()
// .cardId(uuidHelper.convertByteArrayToUUID(card.getCardId()))
// .spaceId(uuidHelper.convertByteArrayToUUID(card.getSpaceId()))
// .title(card.getTitle())
// .cardlinks(cardlinkDtos)
//// .color(uuidHelper.convertBytesToHex(card.getColor()))
// .aiSummary(card.getAiSummary())
//// .userSummary(card.getUserSummary())
// .createdAt(card.getCreatedAt())
// .updatedAt(card.getUpdatedAt())
// .build();
// }


//지식카드 삭제(그래프에도 삭제 반영)
@Transactional
public void deleteCard(UUID userId, UUID cardId) {

Card card = getCardWithAccessCheck(userId, cardId);

//카드 삭제 전 그래프에서 제거
removeCardFromGraph(card.getSpaceId(), card.getCardId());

//실제 DB 카드 삭제
cardRepository.deleteByCardId(card.getCardId());
}

//Graph JSON에서 해당 cardId를 가진 카드 제거
private void removeCardFromGraph(byte[] spaceId, byte[] cardId) {
graphRepository.findBySpaceId(spaceId).ifPresent(graph -> {
try {
ObjectMapper objectMapper = new ObjectMapper();
JsonNode rootNode = objectMapper.readTree(graph.getData());

if (rootNode.has("items")) {
ArrayNode items = (ArrayNode) rootNode.get("items");
ArrayNode updatedItems = removeCardFromItems(items, uuidHelper.convertByteArrayToUUID(cardId).toString());

((ObjectNode) rootNode).set("items", updatedItems);
graph.setData(objectMapper.writeValueAsString(rootNode));
graphRepository.save(graph);
}
} catch (Exception e) {
throw new RuntimeException("Graph 업데이트 중 오류 발생", e);
}
});
}

//items 배열에서 해당 cardId를 가진 요소를 제거 (재귀적으로 group 내부까지 탐색)
private ArrayNode removeCardFromItems(ArrayNode items, String targetCardId) {
ObjectMapper mapper = new ObjectMapper();
ArrayNode updatedItems = mapper.createArrayNode();

for (JsonNode item : items) {
boolean shouldKeep = true;

if (item.has("type") && "card".equals(item.get("type").asText())) {
if (item.has("cardId") && targetCardId.equals(item.get("cardId").asText())) {
shouldKeep = false; //삭제 대상 카드면 유지하지 않음
}
}

if (item.has("items")) {
ArrayNode childItems = (ArrayNode) item.get("items");
ArrayNode updatedChildItems = removeCardFromItems(childItems, targetCardId);
((ObjectNode) item).set("items", updatedChildItems);
}

if (shouldKeep) {
updatedItems.add(item);
}
}
return updatedItems;
}

// //지식카드 삭제(그래프 반영 안하는 버전)
// @Transactional
// public void deleteCard(UUID userId, UUID cardId) {
//
// Card card = getCardWithAccessCheck(userId, cardId);
//
// cardRepository.deleteByCardId(card.getCardId());
// }

//오늘 생성된 지식카드 목록 조회
// @Transactional
// public List<CardListResponse> getCardsByDate(UUID userId, LocalDate createdAt) {
Expand All @@ -278,6 +501,7 @@ public void deleteCard(UUID userId, UUID cardId) {
// .build()
// ).collect(Collectors.toList());
// }

//오늘 생성된 지식카드 목록 조회
@Transactional
public List<CardListResponse> getCardsByDate(UUID userId, LocalDate createdAt) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.example.gp_backend_data.graph.domain.dto.response;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.UUID;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class GraphItemDto {
private String type; // "card"
private String name; // 카드 제목
private UUID cardId; // 카드 UUID
}
Loading