diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 9e08ffc..4b5a705 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -6,8 +6,6 @@ on: - main - develop - - jobs: CI: name: Continuous Integration @@ -15,19 +13,6 @@ jobs: permissions: contents: read - services: - mysql: - image: mysql:8.0 - options: --network-alias=mysql - ports: - - 3306:3306 - env: - MYSQL_ROOT_PASSWORD: testPW - MYSQL_DATABASE: testDB - MYSQL_USER: test - MYSQL_PASSWORD: testPW - - steps: - name: Checkout with submodules uses: actions/checkout@v4 @@ -49,21 +34,6 @@ jobs: - name: Setup Gradle uses: gradle/actions/setup-gradle@v4 - - name: Wait for MySQL to be ready - run: | - echo "Waiting for MySQL to start..." - for i in {1..30}; do - mysql -h 127.0.0.1 -u test -ptestPW -e "SELECT 1" && echo "MySQL is up!" && exit 0 - sleep 2 - done - echo "MySQL did not start in time!" - exit 1 - - - name: Verify MySQL Connection - run: | - echo "Checking MySQL connection..." - mysql -h 127.0.0.1 -u test -ptestPW -D testDB -e "SHOW TABLES;" - - name: Verify Backend_Config files run: ls -al Backend_Config @@ -72,4 +42,16 @@ jobs: env: SPRING_PROFILES_ACTIVE: test run: | - ./gradlew --no-daemon clean build test --info --stacktrace \ No newline at end of file + echo "▶ Running Gradle build and test..." + ./gradlew --no-daemon clean build test --info --stacktrace + + - name: Show test results summary + if: success() + run: | + echo "All Spring Boot tests passed successfully!" + + - name: Mark failure if tests failed + if: failure() + run: | + echo "Tests failed. Please check the test logs for detail" + exit 1 \ No newline at end of file diff --git a/.github/workflows/CICD.yml b/.github/workflows/CICD.yml index feb9719..f40c194 100644 --- a/.github/workflows/CICD.yml +++ b/.github/workflows/CICD.yml @@ -8,7 +8,6 @@ on: env: DOCKERHUB_REPOSITORY: ${{ secrets.DOCKER_REPOSITORY }} - SPRING_DATASOURCE_URL: ${{ secrets.SPRING_DATASOURCE_URL }} jobs: CI: @@ -17,23 +16,11 @@ jobs: permissions: contents: read - services: - mysql: - image: mysql:8.0 - ports: - - 3306:3306 - env: - MYSQL_ROOT_PASSWORD: testPW - MYSQL_DATABASE: testDB - MYSQL_USER: test - MYSQL_PASSWORD: testPW - steps: - name: Get short SHA id: slug run: echo "sha7=$(echo ${GITHUB_SHA} | cut -c1-7)" >> $GITHUB_OUTPUT - - name: Checkout with submodules uses: actions/checkout@v4 with: @@ -50,32 +37,26 @@ jobs: java-version: '17' distribution: 'temurin' - - name: Wait for MySQL to be ready - run: | - echo "Waiting for MySQL to start..." - for i in {1..30}; do - mysql -h 127.0.0.1 -u test -ptestPW -e "SELECT 1" && echo "MySQL is up!" && exit 0 - sleep 2 - done - echo "MySQL did not start in time!" - exit 1 - - - name: Verify MySQL Connection - run: | - echo "Checking MySQL connection..." - mysql -h 127.0.0.1 -u test -ptestPW -D testDB -e "SHOW TABLES;" - - name: Verify Backend_Config files run: ls -al Backend_Config - name: Build and Test with Gradle Wrapper env: SPRING_PROFILES_ACTIVE: test - SPRING_DATASOURCE_URL: jdbc:mysql://127.0.0.1:3306/testDB?allowPublicKeyRetrieval=true&useSSL=false&characterEncoding=UTF-8&serverTimezone=UTC - run: | + echo "▶ Running Gradle build and test..." ./gradlew --no-daemon clean build test --info --stacktrace + - name: Show test results summary + if: success() + run: echo "All Spring Boot tests passed successfully!" + + - name: Mark failure if tests failed + if: failure() + run: | + echo "Tests failed. Please check the test report/logs for details." + exit 1 + - name: Upload jar file to Artifact uses: actions/upload-artifact@v4 with: diff --git a/.gitignore b/.gitignore index e495b8b..b937a57 100644 --- a/.gitignore +++ b/.gitignore @@ -36,5 +36,4 @@ out/ ### VS Code ### .vscode/ .DS_Store -docker-compose.yml src/main/resources/*.properties diff --git a/Backend_Config b/Backend_Config index 5e6c04c..17c5275 160000 --- a/Backend_Config +++ b/Backend_Config @@ -1 +1 @@ -Subproject commit 5e6c04c54e0bd284742b7b9cac159a6c23b5727e +Subproject commit 17c5275dff935a653efc7d6aee7d012cb3af3d2d diff --git a/build.gradle b/build.gradle index 29ed820..e54d7df 100644 --- a/build.gradle +++ b/build.gradle @@ -34,8 +34,11 @@ dependencies { //webflux implementation 'org.springframework.boot:spring-boot-starter-webflux' //jpa - implementation 'mysql:mysql-connector-java:8.0.33' - implementation 'org.springframework.boot:spring-boot-starter-data-jpa' +// implementation 'mysql:mysql-connector-java:8.0.33' +// implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + //neo4j + implementation 'org.springframework.boot:spring-boot-starter-data-neo4j' + } tasks.named('test') { diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..eaa8dfe --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,23 @@ +version: '3.8' + +services: + neo4j: + image: neo4j:5 + container_name: neo4j-db + restart: always + ports: + - "7474:7474" + - "7687:7687" + environment: + NEO4J_AUTH: neo4j/testPass123 + volumes: + - neo4j-data:/data + networks: + - backend-network + +volumes: + neo4j-data: + +networks: + backend-network: + driver: bridge diff --git a/init.sql b/init.sql deleted file mode 100644 index 897c01e..0000000 --- a/init.sql +++ /dev/null @@ -1,9 +0,0 @@ -CREATE DATABASE IF NOT EXISTS goingDB; -CREATE DATABASE IF NOT EXISTS goingtestDB; - -CREATE USER IF NOT EXISTS 'test'@'%' IDENTIFIED BY 'testPW'; - -GRANT ALL PRIVILEGES ON goingDB.* TO 'test'@'%'; -GRANT ALL PRIVILEGES ON goingtestDB.* TO 'test'@'%'; - -FLUSH PRIVILEGES; \ No newline at end of file diff --git a/src/main/java/com/going/server/domain/cluster/entity/Cluster.java b/src/main/java/com/going/server/domain/cluster/entity/Cluster.java index aa7c854..0cdedd6 100644 --- a/src/main/java/com/going/server/domain/cluster/entity/Cluster.java +++ b/src/main/java/com/going/server/domain/cluster/entity/Cluster.java @@ -1,29 +1,33 @@ package com.going.server.domain.cluster.entity; -import com.going.server.global.common.BaseEntity; -import jakarta.persistence.*; import lombok.*; +import org.springframework.data.neo4j.core.schema.GeneratedValue; +import org.springframework.data.neo4j.core.schema.Id; +import org.springframework.data.neo4j.core.schema.Node; +import org.springframework.data.neo4j.core.schema.Property; -@Entity + +@Node("Cluster") @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor -@Table(name="cluster") -public class Cluster extends BaseEntity { +public class Cluster { @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name="cluster_id") + @GeneratedValue private Long clusterId; - @Column(name="represent_word") + @Property("represent_word") private String representWord; - @Column(name="result_img") + @Property("result_img") private String resultImg; public static Cluster toEntity(String representWord, String resultImg) { - return Cluster.builder().representWord(representWord).resultImg(resultImg).build(); + return Cluster.builder() + .representWord(representWord) + .resultImg(resultImg) + .build(); } } diff --git a/src/main/java/com/going/server/domain/cluster/repository/ClusterRepository.java b/src/main/java/com/going/server/domain/cluster/repository/ClusterRepository.java index ab1c0ac..45f1472 100644 --- a/src/main/java/com/going/server/domain/cluster/repository/ClusterRepository.java +++ b/src/main/java/com/going/server/domain/cluster/repository/ClusterRepository.java @@ -2,16 +2,14 @@ import com.going.server.domain.cluster.entity.Cluster; import com.going.server.domain.cluster.exception.ClusterNotFoundException; -import com.going.server.domain.word.exception.WordNotFoundException; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.repository.CrudRepository; +import org.springframework.data.neo4j.repository.Neo4jRepository; import org.springframework.stereotype.Repository; @Repository -public interface ClusterRepository extends JpaRepository { +public interface ClusterRepository extends Neo4jRepository { Cluster findByRepresentWord(String word); - default Cluster getByCluster(Long ClusterId) { - return findById(ClusterId).orElseThrow(ClusterNotFoundException::new); + default Cluster getByCluster(Long clusterId) { + return findById(clusterId).orElseThrow(ClusterNotFoundException::new); } } diff --git a/src/main/java/com/going/server/domain/history/entity/History.java b/src/main/java/com/going/server/domain/history/entity/History.java index 1931f53..94f8c2a 100644 --- a/src/main/java/com/going/server/domain/history/entity/History.java +++ b/src/main/java/com/going/server/domain/history/entity/History.java @@ -1,33 +1,35 @@ package com.going.server.domain.history.entity; import com.going.server.domain.word.entity.Word; -import jakarta.persistence.*; import lombok.*; +import org.springframework.data.neo4j.core.schema.*; -@Entity +@Node("History") @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor -@Table(name="history") public class History { + @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name="history_id") + @GeneratedValue private Long historyId; - @Column(name="`before`") + @Property("before") private String before; - @Column(name="after") + @Property("after") private String after; - @ManyToOne - @JoinColumn(name="word_id") + @Relationship(type = "RELATED_TO", direction = Relationship.Direction.OUTGOING) private Word word; public static History toEntity(String before, String after, Word word){ - return History.builder().before(before).after(after).word(word).build(); + return History.builder() + .before(before) + .after(after) + .word(word) + .build(); } } diff --git a/src/main/java/com/going/server/domain/history/repository/HistoryRepository.java b/src/main/java/com/going/server/domain/history/repository/HistoryRepository.java index e73727f..3041832 100644 --- a/src/main/java/com/going/server/domain/history/repository/HistoryRepository.java +++ b/src/main/java/com/going/server/domain/history/repository/HistoryRepository.java @@ -1,10 +1,9 @@ package com.going.server.domain.history.repository; import com.going.server.domain.history.entity.History; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.repository.CrudRepository; +import org.springframework.data.neo4j.repository.Neo4jRepository; import org.springframework.stereotype.Repository; @Repository -public interface HistoryRepository extends JpaRepository { +public interface HistoryRepository extends Neo4jRepository { } diff --git a/src/main/java/com/going/server/domain/sentence/controller/SentenceController.java b/src/main/java/com/going/server/domain/sentence/controller/SentenceController.java index 50e3817..a660af8 100644 --- a/src/main/java/com/going/server/domain/sentence/controller/SentenceController.java +++ b/src/main/java/com/going/server/domain/sentence/controller/SentenceController.java @@ -1,7 +1,6 @@ package com.going.server.domain.sentence.controller; import com.going.server.domain.sentence.service.SentenceService; -import com.going.server.domain.sentence.service.SentenceServiceImpl; import com.going.server.global.response.SuccessResponse; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Content; diff --git a/src/main/java/com/going/server/domain/sentence/entity/Sentence.java b/src/main/java/com/going/server/domain/sentence/entity/Sentence.java index d4926a1..517ef3d 100644 --- a/src/main/java/com/going/server/domain/sentence/entity/Sentence.java +++ b/src/main/java/com/going/server/domain/sentence/entity/Sentence.java @@ -1,30 +1,31 @@ package com.going.server.domain.sentence.entity; import com.going.server.domain.word.entity.Word; -import jakarta.persistence.*; import lombok.*; +import org.springframework.data.neo4j.core.schema.*; -@Entity +@Node("Sentence") @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor -@Table(name="sentence") public class Sentence { + @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name="sentence_id") + @GeneratedValue private Long sentenceId; - @Column(name="sentence") + @Property("sentence") private String sentence; - - @ManyToOne - @JoinColumn(name="word_id") + // 문장은 단어를 사용한다. + @Relationship(type = "USES", direction = Relationship.Direction.OUTGOING) private Word word; - public static Sentence toEntity(String sentence,Word word) { - return Sentence.builder().word(word).sentence(sentence).build(); + public static Sentence toEntity(String sentence, Word word) { + return Sentence.builder() + .sentence(sentence) + .word(word) + .build(); } } diff --git a/src/main/java/com/going/server/domain/sentence/repository/SentenceRepository.java b/src/main/java/com/going/server/domain/sentence/repository/SentenceRepository.java index 88f73a5..d51046e 100644 --- a/src/main/java/com/going/server/domain/sentence/repository/SentenceRepository.java +++ b/src/main/java/com/going/server/domain/sentence/repository/SentenceRepository.java @@ -1,14 +1,12 @@ package com.going.server.domain.sentence.repository; import com.going.server.domain.sentence.entity.Sentence; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.repository.CrudRepository; +import org.springframework.data.neo4j.repository.Neo4jRepository; import org.springframework.stereotype.Repository; import java.util.List; -import java.util.Optional; @Repository -public interface SentenceRepository extends JpaRepository { +public interface SentenceRepository extends Neo4jRepository { List findByWord_WordId(Long wordId); } diff --git a/src/main/java/com/going/server/domain/word/controller/WordController.java b/src/main/java/com/going/server/domain/word/controller/WordController.java index a431934..a350afa 100644 --- a/src/main/java/com/going/server/domain/word/controller/WordController.java +++ b/src/main/java/com/going/server/domain/word/controller/WordController.java @@ -4,7 +4,6 @@ import com.going.server.domain.word.dto.ModifyRequestDto; import com.going.server.domain.word.dto.WordResponseDto; import com.going.server.domain.word.service.WordService; -import com.going.server.domain.word.service.WordServiceImpl; import com.going.server.global.response.SuccessResponse; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Content; diff --git a/src/main/java/com/going/server/domain/word/entity/Word.java b/src/main/java/com/going/server/domain/word/entity/Word.java index 53470ad..ab75fd5 100644 --- a/src/main/java/com/going/server/domain/word/entity/Word.java +++ b/src/main/java/com/going/server/domain/word/entity/Word.java @@ -1,31 +1,35 @@ package com.going.server.domain.word.entity; import com.going.server.domain.cluster.entity.Cluster; -import com.going.server.global.common.BaseEntity; -import jakarta.persistence.*; import lombok.*; +import org.springframework.data.neo4j.core.schema.*; -@Entity +@Node("Word") @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor -@Table(name="word") -public class Word extends BaseEntity { +public class Word { + @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name="word_id") + @GeneratedValue private Long wordId; - @Column(name="compose_word") + @Property("compose_word") private String composeWord; - @ManyToOne - @JoinColumn(name="cluster_id") + // Neo4j 관계 설정 + // 하나의 단어는 해당 클러스터에 속한다. Word -> Cluster 라는 의미 + // BELONGS_TO 는 속한다는 관계를 나타내고, OUTGOING은 방향성을 나타냄 + @Relationship(type = "BELONGS_TO", direction = Relationship.Direction.OUTGOING) private Cluster cluster; public static Word toEntity(String composeWord, Cluster cluster) { - return Word.builder().composeWord(composeWord).cluster(cluster).build(); + return Word.builder() + .composeWord(composeWord) + .cluster(cluster) + .build(); } } + diff --git a/src/main/java/com/going/server/domain/word/repository/WordRepository.java b/src/main/java/com/going/server/domain/word/repository/WordRepository.java index 14ffe93..535e426 100644 --- a/src/main/java/com/going/server/domain/word/repository/WordRepository.java +++ b/src/main/java/com/going/server/domain/word/repository/WordRepository.java @@ -2,13 +2,13 @@ import com.going.server.domain.word.entity.Word; import com.going.server.domain.word.exception.WordNotFoundException; -import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.neo4j.repository.Neo4jRepository; import org.springframework.stereotype.Repository; import java.util.List; @Repository -public interface WordRepository extends JpaRepository { +public interface WordRepository extends Neo4jRepository { List findByCluster_ClusterId(Long clusterId); default Word getByWord(Long wordId) { diff --git a/src/main/java/com/going/server/domain/word/service/WordServiceImpl.java b/src/main/java/com/going/server/domain/word/service/WordServiceImpl.java index 1f39827..4844d49 100644 --- a/src/main/java/com/going/server/domain/word/service/WordServiceImpl.java +++ b/src/main/java/com/going/server/domain/word/service/WordServiceImpl.java @@ -4,21 +4,18 @@ import com.going.server.domain.cluster.repository.ClusterRepository; import com.going.server.domain.history.entity.History; import com.going.server.domain.history.repository.HistoryRepository; -import com.going.server.domain.history.service.HistoryServiceImpl; import com.going.server.domain.word.dto.AddRequestDto; import com.going.server.domain.word.dto.ModifyRequestDto; import com.going.server.domain.word.dto.WordDto; import com.going.server.domain.word.dto.WordResponseDto; import com.going.server.domain.word.entity.Word; import com.going.server.domain.word.repository.WordRepository; -import jakarta.persistence.EntityNotFoundException; import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import java.util.ArrayList; import java.util.List; -import java.util.Optional; @Service @RequiredArgsConstructor diff --git a/src/main/java/com/going/server/global/temp/controller/FastApiController.java b/src/main/java/com/going/server/global/temp/controller/FastApiController.java index 84a8c36..c60a711 100644 --- a/src/main/java/com/going/server/global/temp/controller/FastApiController.java +++ b/src/main/java/com/going/server/global/temp/controller/FastApiController.java @@ -1,5 +1,6 @@ package com.going.server.global.temp.controller; +import com.going.server.domain.word.entity.Word; import com.going.server.global.response.SuccessResponse; import com.going.server.global.temp.service.FastApiService; import io.swagger.v3.oas.annotations.Operation; @@ -10,13 +11,15 @@ import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Profile; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @RestController @RequiredArgsConstructor @RequestMapping("/api") -@Tag(name = "FastAPI 통신 테스트", description = "FastAPI 서버와의 통신을 테스트하는 API") +@Profile("!test") +@Tag(name = "통신 테스트", description = "FastAPI 서버 및 neo4j 데이터베이스와의 통신을 테스트하는 API") @Slf4j public class FastApiController { @@ -56,4 +59,11 @@ public SuccessResponse setCluster() { return SuccessResponse.empty(); } + @PostMapping("/test-neo4j") + @Operation(summary = "neo4j 데이터베이스 확인", description = "composeWord를 저장하여 Word 노드를 생성합니다.") + public String saveWord(@RequestParam String word) { + Word savedWord = fastApiService.testWord(word); + return "저장된 테스트용 단어 : " + savedWord.getComposeWord(); + } + } diff --git a/src/main/java/com/going/server/global/temp/service/FastApiService.java b/src/main/java/com/going/server/global/temp/service/FastApiService.java index 20b047e..a948673 100644 --- a/src/main/java/com/going/server/global/temp/service/FastApiService.java +++ b/src/main/java/com/going/server/global/temp/service/FastApiService.java @@ -7,33 +7,36 @@ import com.going.server.domain.word.entity.Word; import com.going.server.domain.word.repository.WordRepository; import jakarta.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Service; -import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.reactive.function.client.WebClient; import java.util.List; import java.util.Map; @Service +@RequiredArgsConstructor @Slf4j +@Profile("!test") public class FastApiService { private final WordRepository wordRepository; private final SentenceRepository sentenceRepository; @Value("${fastapi.base-url}") private String baseUrl; - - private final WebClient webClient; + private final WebClient.Builder webClientBuilder; private final ClusterRepository clusterRepository; + private WebClient webClient; - public FastApiService(WebClient.Builder webClientBuilder, ClusterRepository clusterResultRepository, WordRepository wordRepository, SentenceRepository sentenceRepository) { + @PostConstruct + public void init() { this.webClient = webClientBuilder.build(); - this.clusterRepository = clusterResultRepository; - this.wordRepository = wordRepository; - this.sentenceRepository = sentenceRepository; } + + /** * FastAPI 서버의 기본 상태 확인 (GET 요청) */ @@ -48,6 +51,7 @@ public String callFastApi() { /** * FastAPI에서 클러스터링 결과 가져와 DB에 저장 (POST 요청) */ + @Profile("!test") @PostConstruct public void setCluster() { // FastAPI 요청 데이터 (필요시 변경) @@ -63,7 +67,6 @@ public void setCluster() { //모든 클러스터링 결과 저장 List> clusters = (List>) response.get("clusters"); - //클러스터링 결과 이미지 저장 String imageUrl = response.get("image_url").toString(); @@ -96,4 +99,11 @@ public void setCluster() { } } } + + public Word testWord(String word) { + Word wordEntity = Word.toEntity(word, null); + return wordRepository.save(wordEntity); + + } + }