diff --git a/.github/workflows/dev_deploy.yml b/.github/workflows/dev_deploy.yml index befe6e91..a7498b00 100644 --- a/.github/workflows/dev_deploy.yml +++ b/.github/workflows/dev_deploy.yml @@ -29,7 +29,6 @@ jobs: spring.jpa.hibernate.ddl-auto=update server.forward-headers-strategy=framework - # PostgreSQL 설정 추가 ✅ spring.datasource.url=\${SPRING_DATASOURCE_URL} spring.datasource.username=\${SPRING_DATASOURCE_USERNAME} spring.datasource.password=\${SPRING_DATASOURCE_PASSWORD} @@ -91,25 +90,98 @@ jobs: cache-from: type=gha cache-to: type=gha,mode=max - deploy: + deploy-dev: + needs: build + runs-on: ubuntu-latest + if: github.ref == 'refs/heads/develop' + environment: development + + steps: + - name: Deploy to Dev Server + uses: appleboy/ssh-action@v1.0.3 + with: + host: ${{ vars.EC2_HOST }} + username: ${{ vars.EC2_USERNAME }} + key: ${{ secrets.EC2_SSH_KEY }} + script: | + cd /home/ubuntu/edison-infra + + cat > .env << 'ENVEOF' + DOCKERHUB_USERNAME=${{ vars.DOCKERHUB_USERNAME }} + + SPRING_TAG=${{ github.sha }} + AI_TAG=latest + + RDS_URL=${{ vars.RDS_URL }} + RDS_USERNAME=${{ vars.RDS_USERNAME }} + RDS_PASSWORD=${{ secrets.RDS_PASSWORD }} + + REDIS_PASSWORD=${{ secrets.REDIS_PASSWORD }} + + JWT_SECRET=${{ secrets.JWT_SECRET }} + JWT_ACCESS_EXPIRATION=${{ vars.JWT_ACCESS_EXPIRATION }} + JWT_REFRESH_EXPIRATION=${{ vars.JWT_REFRESH_EXPIRATION }} + + GOOGLE_CLIENT_ID=${{ vars.GOOGLE_CLIENT_ID }} + GOOGLE_CLIENT_SECRET=${{ secrets.GOOGLE_CLIENT_SECRET }} + + OPENAI_KEY=${{ secrets.OPENAI_KEY }} + + AWS_ACCESS_KEY=${{ secrets.AWS_ACCESS_KEY }} + AWS_SECRET_KEY=${{ secrets.AWS_SECRET_KEY }} + CLOUD_AWS_CREDENTIALS_ACCESS_KEY=${{ secrets.AWS_ACCESS_KEY }} + CLOUD_AWS_CREDENTIALS_SECRET_KEY=${{ secrets.AWS_SECRET_KEY }} + CLOUD_AWS_S3_BUCKET=${{ vars.AWS_S3_BUCKET }} + ENVEOF + + ./deploy.sh spring ${{ github.sha }} dev + + echo "✅ Deployed to DEV server" + + deploy-prod: needs: build runs-on: ubuntu-latest if: github.ref == 'refs/heads/main' + environment: production steps: - - name: Deploy to EC2 + - name: Deploy to Prod Server uses: appleboy/ssh-action@v1.0.3 with: - host: ${{ secrets.EC2_HOST }} - username: ${{ secrets.EC2_USERNAME }} + host: ${{ vars.EC2_HOST }} + username: ${{ vars.EC2_USERNAME }} key: ${{ secrets.EC2_SSH_KEY }} script: | cd /home/ubuntu/edison-infra cat > .env << 'ENVEOF' - ${{ secrets.ENV_FILE }} + DOCKERHUB_USERNAME=${{ vars.DOCKERHUB_USERNAME }} + + SPRING_TAG=${{ github.sha }} + AI_TAG=latest + + RDS_URL=${{ vars.RDS_URL }} + RDS_USERNAME=${{ vars.RDS_USERNAME }} + RDS_PASSWORD=${{ secrets.RDS_PASSWORD }} + + REDIS_PASSWORD=${{ secrets.REDIS_PASSWORD }} + + JWT_SECRET=${{ secrets.JWT_SECRET }} + JWT_ACCESS_EXPIRATION=${{ vars.JWT_ACCESS_EXPIRATION }} + JWT_REFRESH_EXPIRATION=${{ vars.JWT_REFRESH_EXPIRATION }} + + GOOGLE_CLIENT_ID=${{ vars.GOOGLE_CLIENT_ID }} + GOOGLE_CLIENT_SECRET=${{ secrets.GOOGLE_CLIENT_SECRET }} + + OPENAI_KEY=${{ secrets.OPENAI_KEY }} + + AWS_ACCESS_KEY=${{ secrets.AWS_ACCESS_KEY }} + AWS_SECRET_KEY=${{ secrets.AWS_SECRET_KEY }} + CLOUD_AWS_CREDENTIALS_ACCESS_KEY=${{ secrets.AWS_ACCESS_KEY }} + CLOUD_AWS_CREDENTIALS_SECRET_KEY=${{ secrets.AWS_SECRET_KEY }} + CLOUD_AWS_S3_BUCKET=${{ vars.AWS_S3_BUCKET }} ENVEOF - sed -i 's/^SPRING_TAG=.*/SPRING_TAG=${{ github.sha }}/' .env + ./deploy.sh spring ${{ github.sha }} prod - ./deploy.sh spring ${{ github.sha }} \ No newline at end of file + echo "✅ Deployed to PROD server" \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..6829b7ed --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,14 @@ +# local redis용 docker-compose +services: + redis: + image: redis:alpine + container_name: edison-redis + ports: + - "6379:6379" + volumes: + - redis_volume:/data + command: redis-server --appendonly yes + restart: always + +volumes: + redis_volume: \ No newline at end of file diff --git a/nginx/nginx.conf b/nginx/nginx.conf deleted file mode 100644 index 0c420d0f..00000000 --- a/nginx/nginx.conf +++ /dev/null @@ -1,96 +0,0 @@ -# 기본 설정 -user www-data; -worker_processes auto; -pid /run/nginx.pid; -include /etc/nginx/modules-enabled/*.conf; - -# 이벤트 처리 설정 -events { - worker_connections 1024; -} - -# HTTP 설정 -http { - include /etc/nginx/mime.types; - default_type application/octet-stream; - - # 로그 설정 - access_log /var/log/nginx/access.log; - error_log /var/log/nginx/error.log; - - sendfile on; - keepalive_timeout 65; - - # HTTP 요청 → HTTPS로 리디렉션 - server { - listen 80; - listen [::]:80; - server_name api.umcedison.site; - - # Let's Encrypt 인증 요청은 HTTP로 유지 - location ^~ /.well-known/acme-challenge/ { - allow all; - root /var/www/html; - default_type "text/plain"; - try_files $uri =404; - } - - # 헬스 체크 경로 - location /health-check { - return 200 'healthy'; - add_header Content-Type text/plain; - } - - # 나머지 요청은 HTTPS로 리디렉션 - location / { - return 301 https://api.umcedison.site$request_uri; - } - } - - # HTTPS 설정 - server { - listen 443 ssl http2; - listen [::]:443 ssl http2; - server_name api.umcedison.site; - - ssl_certificate /etc/letsencrypt/live/api.umcedison.site/fullchain.pem; - ssl_certificate_key /etc/letsencrypt/live/api.umcedison.site/privkey.pem; - - ssl_protocols TLSv1.2 TLSv1.3; - ssl_ciphers HIGH:!aNULL:!MD5; - ssl_prefer_server_ciphers on; - - # HSTS 설정 - add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; - - # HTTPS에서도 인증서 갱신 허용 - location ^~ /.well-known/acme-challenge/ { - allow all; - root /var/www/html; - default_type "text/plain"; - try_files $uri =404; - } - - # 프록시 설정 (리디렉션 금지) - location / { - proxy_pass http://localhost:8080; - proxy_http_version 1.1; # HTTP/1.1 강제 적용 - proxy_set_header Upgrade $http_upgrade; # WebSocket 지원 - proxy_set_header Connection 'upgrade'; # 연결 유지 - - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto https; # HTTPS로 고정 - proxy_set_header X-Forwarded-Ssl on; # SSL 사용 명시 - - # Authorization 헤더 추가 - proxy_set_header Authorization $http_authorization; - - # 프록시 타임아웃 설정 - proxy_connect_timeout 20s; - proxy_send_timeout 60s; - proxy_read_timeout 60s; - } - } -} diff --git a/project/.gitignore b/project/.gitignore index 5227bb8c..230ff7be 100644 --- a/project/.gitignore +++ b/project/.gitignore @@ -37,4 +37,5 @@ out/ ### VS Code ### .vscode/ .idea/ -.idea/ + +load-test.js diff --git a/project/build.gradle b/project/build.gradle index 106dd41b..a9e8031b 100644 --- a/project/build.gradle +++ b/project/build.gradle @@ -52,13 +52,24 @@ dependencies { implementation 'com.fasterxml.jackson.core:jackson-databind' implementation 'com.squareup.okhttp3:okhttp:4.12.0' + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.3' // Lombok 설정 compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' + runtimeOnly 'com.mysql:mysql-connector-j' runtimeOnly 'org.postgresql:postgresql' + // Vector Database (pgvector) + implementation 'com.pgvector:pgvector:0.1.6' + implementation 'org.hibernate.orm:hibernate-vector:6.6.4.Final' + implementation 'io.hypersistence:hypersistence-utils-hibernate-63:3.7.0' + implementation 'com.github.haifengl:smile-core:3.1.1' + + // 차원 축소 (PCA, t-SNE) + implementation 'org.apache.commons:commons-math3:3.6.1' + // 테스트 관련 testImplementation 'org.springframework.boot:spring-boot-starter-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' diff --git a/project/src/main/java/com/edison/project/domain/artletter/entity/Artletter.java b/project/src/main/java/com/edison/project/domain/artletter/entity/Artletter.java index 6c934e5d..8172971d 100644 --- a/project/src/main/java/com/edison/project/domain/artletter/entity/Artletter.java +++ b/project/src/main/java/com/edison/project/domain/artletter/entity/Artletter.java @@ -11,7 +11,7 @@ @AllArgsConstructor @Table(name = "Artletter", indexes = { @Index(name = "idx_artletter_title", columnList = "title"), - @Index(name = "idx_artletter_writer", columnList = "writer") + @Index(name = "idx_artletter_writer", columnList = "writer_id") }) public class Artletter extends BaseEntity { diff --git a/project/src/main/java/com/edison/project/domain/bubble/controller/BubbleRestController.java b/project/src/main/java/com/edison/project/domain/bubble/controller/BubbleRestController.java index 854dee5b..92b4a81b 100644 --- a/project/src/main/java/com/edison/project/domain/bubble/controller/BubbleRestController.java +++ b/project/src/main/java/com/edison/project/domain/bubble/controller/BubbleRestController.java @@ -147,5 +147,44 @@ public ResponseEntity getAllBubbles( return bubbleService.getAllBubbles(userPrincipal, pageable); } + /** + * 단일 버블 벡터화 + * POST /bubbles/{localIdx}/vectorize + */ + @PostMapping("/{localIdx}/vectorize") + @PreAuthorize("isAuthenticated()") + public ResponseEntity vectorizeBubble( + @AuthenticationPrincipal CustomUserPrincipal userPrincipal, + @PathVariable String localIdx) { + BubbleResponseDto.VectorizeResultDto result = bubbleService.vectorizeBubble(userPrincipal, localIdx); + return ApiResponse.onSuccess(SuccessStatus._OK, result); + } + + /** + * 모든 버블 벡터화 + * POST /bubbles/vectorize-all + */ + @PostMapping("/vectorize-all") + @PreAuthorize("isAuthenticated()") + public ResponseEntity vectorizeAllBubbles( + @AuthenticationPrincipal CustomUserPrincipal userPrincipal) { + return bubbleService.vectorizeAllBubbles(userPrincipal); + } + + /** + * 사용자의 모든 버블 2D 벡터 좌표 조회 + * GET /bubbles/embeddings + */ + @GetMapping("/embeddings") + @PreAuthorize("isAuthenticated()") + public ResponseEntity getAllBubbleEmbeddings( + @AuthenticationPrincipal CustomUserPrincipal userPrincipal, + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "50") int size) { + + Pageable pageable = PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, "createdAt")); + return bubbleService.getAllBubbleEmbeddings(userPrincipal, pageable); + } + } diff --git a/project/src/main/java/com/edison/project/domain/bubble/dto/BubbleResponseDto.java b/project/src/main/java/com/edison/project/domain/bubble/dto/BubbleResponseDto.java index b992e330..3f60c169 100644 --- a/project/src/main/java/com/edison/project/domain/bubble/dto/BubbleResponseDto.java +++ b/project/src/main/java/com/edison/project/domain/bubble/dto/BubbleResponseDto.java @@ -68,4 +68,22 @@ public static class DeleteRestoreResultDto { private boolean isTrashed; } + public record VectorizeResultDto( + String localIdx, + String title, + boolean isVectorized, + Double embedding2dX, + Double embedding2dY, + LocalDateTime vectorizedAt, + String message + ) {} + + public record BubbleEmbeddingDto( + String localIdx, + String title, + Double embedding2dX, + Double embedding2dY, + LocalDateTime createdAt + ) {} + } diff --git a/project/src/main/java/com/edison/project/domain/bubble/entity/Bubble.java b/project/src/main/java/com/edison/project/domain/bubble/entity/Bubble.java index 97ecd4e8..034a4bce 100644 --- a/project/src/main/java/com/edison/project/domain/bubble/entity/Bubble.java +++ b/project/src/main/java/com/edison/project/domain/bubble/entity/Bubble.java @@ -2,8 +2,11 @@ import com.edison.project.domain.label.entity.Label; import com.edison.project.domain.member.entity.Member; +import com.pgvector.PGvector; import jakarta.persistence.*; import lombok.*; +import org.hibernate.annotations.JdbcTypeCode; +import org.hibernate.type.SqlTypes; import java.time.LocalDateTime; import java.util.HashSet; @@ -15,7 +18,7 @@ @Setter @Table(name = "Bubble", indexes = { @Index(name = "idx_bubble_member_id", columnList = "member_id"), - @Index(name = "idx_localIdx", columnList = "local_idx")}) + @Index(name = "idx_bubble_local_idx", columnList = "local_idx")}) public class Bubble { @Id @@ -63,6 +66,16 @@ public class Bubble { @OneToMany(mappedBy = "backlinkBubble", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY) private Set referencingBubbles = new HashSet<>(); + @Column(name = "embedding") + @JdbcTypeCode(SqlTypes.VECTOR) + private float[] embedding; + + @Column(name = "embedding_2d_x") + private Double embedding2dX; + + @Column(name = "embedding_2d_y") + private Double embedding2dY; + @Builder public Bubble(Member member, String localIdx, String title, String content, String mainImg, Set labels, boolean isTrashed, LocalDateTime createdAt, LocalDateTime updatedAt, LocalDateTime deletedAt) { diff --git a/project/src/main/java/com/edison/project/domain/bubble/repository/BubbleEmbeddingProjection.java b/project/src/main/java/com/edison/project/domain/bubble/repository/BubbleEmbeddingProjection.java new file mode 100644 index 00000000..24250de9 --- /dev/null +++ b/project/src/main/java/com/edison/project/domain/bubble/repository/BubbleEmbeddingProjection.java @@ -0,0 +1,11 @@ +package com.edison.project.domain.bubble.repository; + +import java.time.LocalDateTime; + +public interface BubbleEmbeddingProjection { + String getLocalIdx(); + String getTitle(); + Double getEmbedding2dX(); + Double getEmbedding2dY(); + LocalDateTime getCreatedAt(); +} \ No newline at end of file diff --git a/project/src/main/java/com/edison/project/domain/bubble/repository/BubbleRepository.java b/project/src/main/java/com/edison/project/domain/bubble/repository/BubbleRepository.java index 87a6dceb..6c30b2ef 100644 --- a/project/src/main/java/com/edison/project/domain/bubble/repository/BubbleRepository.java +++ b/project/src/main/java/com/edison/project/domain/bubble/repository/BubbleRepository.java @@ -59,9 +59,28 @@ Page findRecentByMember( // ============ 목록 조회 (리스트) ============ List findByMember_MemberIdAndIsTrashedFalse(Long memberId); + // ============ 벡터 임베딩 조회 ============ + Page findByMember_MemberIdAndIsTrashedFalseAndEmbeddingIsNotNull(Long memberId, Pageable pageable); + // ============ 배치 조회 ============ Set findAllByMemberAndLocalIdxIn(Member member, Set localIdxs); // ============ 존재 여부 ============ Boolean existsByMemberAndLocalIdx(Member member, String localIdx); + + @Query("SELECT b.localIdx as localIdx, " + + "b.title as title, " + + "b.embedding2dX as embedding2dX, " + + "b.embedding2dY as embedding2dY, " + + "b.createdAt as createdAt " + + "FROM Bubble b " + + "WHERE b.member.memberId = :memberId " + + "AND b.isTrashed = false " + + "AND b.embedding2dX IS NOT NULL " + + "AND b.embedding2dY IS NOT NULL") + Page findEmbeddingProjectionsByMemberId( + @Param("memberId") Long memberId, + Pageable pageable + ); + } \ No newline at end of file diff --git a/project/src/main/java/com/edison/project/domain/bubble/service/BubbleService.java b/project/src/main/java/com/edison/project/domain/bubble/service/BubbleService.java index 858527bb..0c6b57d9 100644 --- a/project/src/main/java/com/edison/project/domain/bubble/service/BubbleService.java +++ b/project/src/main/java/com/edison/project/domain/bubble/service/BubbleService.java @@ -29,4 +29,19 @@ public interface BubbleService { void hardDeleteBubble(CustomUserPrincipal userPrincipal, String bubbleId); ResponseEntity getAllBubbles(CustomUserPrincipal userPrincipal, Pageable pageable); + + /** + * Bubble을 벡터화하여 데이터베이스에 저장 + */ + BubbleResponseDto.VectorizeResultDto vectorizeBubble(CustomUserPrincipal userPrincipal, String bubbleLocalIdx); + + /** + * 사용자의 모든 Bubble을 벡터화 + */ + ResponseEntity vectorizeAllBubbles(CustomUserPrincipal userPrincipal); + + /** + * 사용자의 모든 Bubble 2D 임베딩 좌표 조회 + */ + ResponseEntity getAllBubbleEmbeddings(CustomUserPrincipal userPrincipal, Pageable pageable); } diff --git a/project/src/main/java/com/edison/project/domain/bubble/service/BubbleServiceImpl.java b/project/src/main/java/com/edison/project/domain/bubble/service/BubbleServiceImpl.java index bc7a1530..e5ff6637 100644 --- a/project/src/main/java/com/edison/project/domain/bubble/service/BubbleServiceImpl.java +++ b/project/src/main/java/com/edison/project/domain/bubble/service/BubbleServiceImpl.java @@ -11,6 +11,7 @@ import com.edison.project.domain.bubble.entity.BubbleBacklink; import com.edison.project.domain.bubble.entity.BubbleLabel; import com.edison.project.domain.bubble.repository.BubbleBacklinkRepository; +import com.edison.project.domain.bubble.repository.BubbleEmbeddingProjection; import com.edison.project.domain.bubble.repository.BubbleLabelRepository; import com.edison.project.domain.bubble.repository.BubbleRepository; import com.edison.project.domain.label.dto.LabelResponseDTO; @@ -19,20 +20,19 @@ import com.edison.project.domain.member.entity.Member; import com.edison.project.domain.member.repository.MemberRepository; import com.edison.project.global.security.CustomUserPrincipal; +import com.pgvector.PGvector; // Must be imported import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; - -import org.springframework.data.domain.Pageable; import org.springframework.transaction.annotation.Transactional; import java.time.LocalDateTime; import java.time.temporal.ChronoUnit; import java.util.*; import java.util.stream.Collectors; -import java.util.List; @Service @RequiredArgsConstructor @@ -45,8 +45,9 @@ public class BubbleServiceImpl implements BubbleService { private final BubbleLabelRepository bubbleLabelRepository; private final MemberRepository memberRepository; private final BubbleBacklinkRepository bubbleBacklinkRepository; + private final EmbeddingService embeddingService; + private final DimensionReductionService dimensionReductionService; - // Bubble → DTO 변환 private BubbleResponseDto.SyncResultDto convertToBubbleResponseDto(Bubble bubble) { return BubbleResponseDto.SyncResultDto.builder() .localIdx(bubble.getLocalIdx()) @@ -69,102 +70,62 @@ private BubbleResponseDto.SyncResultDto convertToBubbleResponseDto(Bubble bubble } @Override - public ResponseEntity getBubblesByMember( - CustomUserPrincipal userPrincipal, - Pageable pageable - ) { - Page bubblePage = bubbleRepository - .findByMember_MemberIdAndIsTrashedFalse(userPrincipal.getMemberId(), pageable); + public ResponseEntity getBubblesByMember(CustomUserPrincipal userPrincipal, Pageable pageable) { + Page bubblePage = bubbleRepository.findByMember_MemberIdAndIsTrashedFalse(userPrincipal.getMemberId(), pageable); List bubbles = bubblePage.getContent().stream() .map(this::convertToBubbleResponseDto) .collect(Collectors.toList()); - PageInfo pageInfo = new PageInfo( - bubblePage.getNumber(), - bubblePage.getSize(), - bubblePage.hasNext(), - bubblePage.getTotalElements(), - bubblePage.getTotalPages() - ); + PageInfo pageInfo = new PageInfo(bubblePage.getNumber(), bubblePage.getSize(), bubblePage.hasNext(), + bubblePage.getTotalElements(), bubblePage.getTotalPages()); return ApiResponse.onSuccess(SuccessStatus._OK, pageInfo, bubbles); } @Override - public ResponseEntity getDeletedBubbles( - CustomUserPrincipal userPrincipal, - Pageable pageable - ) { - Page bubblePage = bubbleRepository - .findByMember_MemberIdAndIsTrashedTrue(userPrincipal.getMemberId(), pageable); - + public ResponseEntity getDeletedBubbles(CustomUserPrincipal userPrincipal, Pageable pageable) { + Page bubblePage = bubbleRepository.findByMember_MemberIdAndIsTrashedTrue(userPrincipal.getMemberId(), pageable); LocalDateTime now = LocalDateTime.now(); List bubbles = bubblePage.getContent().stream() .map(bubble -> convertToTrashedDto(bubble, now)) .collect(Collectors.toList()); - PageInfo pageInfo = new PageInfo( - bubblePage.getNumber(), - bubblePage.getSize(), - bubblePage.hasNext(), - bubblePage.getTotalElements(), - bubblePage.getTotalPages() - ); + PageInfo pageInfo = new PageInfo(bubblePage.getNumber(), bubblePage.getSize(), bubblePage.hasNext(), + bubblePage.getTotalElements(), bubblePage.getTotalPages()); return ApiResponse.onSuccess(SuccessStatus._OK, pageInfo, bubbles); } - /** 최근 7일 내 버블 조회 */ @Override - public ResponseEntity getRecentBubblesByMember( - CustomUserPrincipal userPrincipal, - Pageable pageable - ) { + public ResponseEntity getRecentBubblesByMember(CustomUserPrincipal userPrincipal, Pageable pageable) { LocalDateTime sevenDaysAgo = LocalDateTime.now().minusDays(7); - - Page bubblePage = bubbleRepository - .findRecentByMember(userPrincipal.getMemberId(), sevenDaysAgo, pageable); + Page bubblePage = bubbleRepository.findRecentByMember(userPrincipal.getMemberId(), sevenDaysAgo, pageable); List bubbles = bubblePage.getContent().stream() .map(this::convertToBubbleResponseDto) .collect(Collectors.toList()); - PageInfo pageInfo = new PageInfo( - bubblePage.getNumber(), - bubblePage.getSize(), - bubblePage.hasNext(), - bubblePage.getTotalElements(), - bubblePage.getTotalPages() - ); + PageInfo pageInfo = new PageInfo(bubblePage.getNumber(), bubblePage.getSize(), bubblePage.hasNext(), + bubblePage.getTotalElements(), bubblePage.getTotalPages()); return ApiResponse.onSuccess(SuccessStatus._OK, pageInfo, bubbles); } - /** 버블 상세 조회 - 단건은 EntityGraph 사용 */ @Override - public BubbleResponseDto.SyncResultDto getBubble( - CustomUserPrincipal userPrincipal, - String localIdx - ) { - Bubble bubble = bubbleRepository - .findByMemberAndLocalIdxWithDetails(userPrincipal.getMemberId(), localIdx) + public BubbleResponseDto.SyncResultDto getBubble(CustomUserPrincipal userPrincipal, String localIdx) { + Bubble bubble = bubbleRepository.findByMemberAndLocalIdxWithDetails(userPrincipal.getMemberId(), localIdx) .orElseThrow(() -> new GeneralException(ErrorStatus.BUBBLE_NOT_FOUND)); - return convertToBubbleResponseDto(bubble); } - - /** 버블 SYNC */ @Override @Transactional public BubbleResponseDto.SyncResultDto syncBubble(CustomUserPrincipal userPrincipal, BubbleRequestDto.SyncDto request) { - Member member = memberRepository.findById(userPrincipal.getMemberId()) .orElseThrow(() -> new GeneralException(ErrorStatus.MEMBER_NOT_FOUND)); - // idx -> label Pk Set backlinks = validateBacklinks(request.getBacklinkIds(), member); Set