diff --git a/build.gradle b/build.gradle index 4155f82..8bf9eec 100644 --- a/build.gradle +++ b/build.gradle @@ -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' diff --git a/src/main/java/com/example/gp_backend_data/card/service/CardService.java b/src/main/java/com/example/gp_backend_data/card/service/CardService.java index 734d7eb..9b2a620 100644 --- a/src/main/java/com/example/gp_backend_data/card/service/CardService.java +++ b/src/main/java/com/example/gp_backend_data/card/service/CardService.java @@ -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; @@ -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; @@ -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 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 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) { @@ -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 cardlinkDtos = cardlinkRepository.findByCard_CardId(uuidHelper.convertUUIDToByteArray(cardId)).stream() + //Graph 데이터 업데이트 + if (request.getTitle() != null) { + updateGraphCardTitle(card.getSpaceId(), card.getCardId(), request.getTitle()); + } + + List 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(); @@ -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 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 getCardsByDate(UUID userId, LocalDate createdAt) { @@ -278,6 +501,7 @@ public void deleteCard(UUID userId, UUID cardId) { // .build() // ).collect(Collectors.toList()); // } + //오늘 생성된 지식카드 목록 조회 @Transactional public List getCardsByDate(UUID userId, LocalDate createdAt) { diff --git a/src/main/java/com/example/gp_backend_data/graph/domain/dto/response/GraphItemDto.java b/src/main/java/com/example/gp_backend_data/graph/domain/dto/response/GraphItemDto.java new file mode 100644 index 0000000..429aea5 --- /dev/null +++ b/src/main/java/com/example/gp_backend_data/graph/domain/dto/response/GraphItemDto.java @@ -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 +}