diff --git a/backend/api/src/main/java/com/yat2/episode/episode/EpisodeId.java b/backend/api/src/main/java/com/yat2/episode/episode/EpisodeId.java index 44493c9d3..898ab3b6e 100644 --- a/backend/api/src/main/java/com/yat2/episode/episode/EpisodeId.java +++ b/backend/api/src/main/java/com/yat2/episode/episode/EpisodeId.java @@ -2,41 +2,25 @@ import jakarta.persistence.Column; import jakarta.persistence.Embeddable; +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; import lombok.Getter; -import lombok.Setter; +import lombok.NoArgsConstructor; import java.io.Serializable; -import java.util.Objects; import java.util.UUID; @Getter -@Setter +@EqualsAndHashCode +@AllArgsConstructor +@NoArgsConstructor @Embeddable public class EpisodeId implements Serializable { - @Column(name = "node_id", columnDefinition = "BINARY(16)") + @Column(name = "node_id", columnDefinition = "BINARY(16)", nullable = false) private UUID nodeId; - @Column(name = "user_id") + @Column(name = "user_id", nullable = false) private Long userId; - public EpisodeId() {} - - public EpisodeId(UUID nodeId, Long userId) { - this.nodeId = nodeId; - this.userId = userId; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (!(o instanceof EpisodeId that)) return false; - return Objects.equals(nodeId, that.nodeId) && Objects.equals(userId, that.userId); - } - - @Override - public int hashCode() { - return Objects.hash(nodeId, userId); - } - } diff --git a/backend/api/src/main/java/com/yat2/episode/episode/EpisodeRepository.java b/backend/api/src/main/java/com/yat2/episode/episode/EpisodeRepository.java index 474304f34..04c258125 100644 --- a/backend/api/src/main/java/com/yat2/episode/episode/EpisodeRepository.java +++ b/backend/api/src/main/java/com/yat2/episode/episode/EpisodeRepository.java @@ -13,21 +13,6 @@ @Repository public interface EpisodeRepository extends JpaRepository { - - @Query( - """ - SELECT e - FROM Episode e - JOIN EpisodeStar s ON s.id.nodeId = e.id - WHERE e.mindmapId = :mindmapId - AND s.id.userId = :userId - """ - ) - List findEpisodesByMindmapIdAndUserId( - @Param("mindmapId") UUID mindmapId, - @Param("userId") long userId - ); - @Query("SELECT e.id FROM Episode e WHERE e.mindmapId = :mindmapId") List findNodeIdsByMindmapId( @Param("mindmapId") UUID mindmapId @@ -52,13 +37,18 @@ List findDetailsByMindmapIdAndUserId( ); @Query( - """ - SELECT DISTINCT ctId - FROM Episode e - JOIN EpisodeStar s ON e.id = s.id.nodeId - JOIN s.competencyTypeIds ctId - WHERE e.mindmapId = :mindmapId - """ + value = """ + SELECT DISTINCT jt.ct_id + FROM episodes e + JOIN episode_stars es ON es.node_id = e.node_id + JOIN JSON_TABLE( + es.competency_type_ids, + '$[*]' COLUMNS ( + ct_id INT PATH '$' + ) + ) jt + WHERE e.mindmap_id = :mindmapId + """, nativeQuery = true ) List findCompetencyTypesByMindmapId( @Param("mindmapId") UUID mindmapId diff --git a/backend/api/src/test/java/com/yat2/episode/episode/EpisodeRepositoryTest.java b/backend/api/src/test/java/com/yat2/episode/episode/EpisodeRepositoryTest.java new file mode 100644 index 000000000..45b92dd4d --- /dev/null +++ b/backend/api/src/test/java/com/yat2/episode/episode/EpisodeRepositoryTest.java @@ -0,0 +1,136 @@ +package com.yat2.episode.episode; + +import jakarta.persistence.EntityManager; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.data.jpa.test.autoconfigure.DataJpaTest; +import org.springframework.test.context.ActiveProfiles; + +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; + +import com.yat2.episode.episode.dto.EpisodeDetail; +import com.yat2.episode.mindmap.Mindmap; +import com.yat2.episode.utils.AbstractRepositoryTest; + +import static com.yat2.episode.utils.TestEntityFactory.createEpisode; +import static com.yat2.episode.utils.TestEntityFactory.createEpisodeStar; +import static com.yat2.episode.utils.TestEntityFactory.createMindmap; +import static com.yat2.episode.utils.TestEntityFactory.createUser; +import static org.assertj.core.api.Assertions.assertThat; + +@DataJpaTest +@ActiveProfiles("test") +@DisplayName("EpisodeRepository 통합 테스트") +class EpisodeRepositoryTest extends AbstractRepositoryTest { + + @Autowired + EpisodeRepository episodeRepository; + @Autowired + EntityManager em; + + @Test + @DisplayName("mindmapId로 nodeId 목록 조회") + void findNodeIdsByMindmapId() { + Mindmap m1 = createMindmap("mm-1"); + Mindmap mOther = createMindmap("mm-other"); + em.persist(m1); + em.persist(mOther); + + UUID node1 = UUID.randomUUID(); + UUID node2 = UUID.randomUUID(); + + em.persist(createEpisode(node1, m1.getId(), "c1")); + em.persist(createEpisode(node2, m1.getId(), "c2")); + em.persist(createEpisode(UUID.randomUUID(), mOther.getId(), "other")); + + flushAndClear(); + + List ids = episodeRepository.findNodeIdsByMindmapId(m1.getId()); + + assertThat(ids).containsExactlyInAnyOrder(node1, node2); + } + + @Test + @DisplayName("상세 리스트 조회") + void findDetailsByMindmapIdAndUserId() { + Mindmap m1 = createMindmap("mm-1"); + em.persist(m1); + + long userId = 100L; + em.persist(createUser(userId)); + + UUID node1 = UUID.randomUUID(); + UUID node2 = UUID.randomUUID(); + + em.persist(createEpisode(node1, m1.getId(), "content1")); + em.persist(createEpisode(node2, m1.getId(), "content2")); + + em.persist(createEpisodeStar(node1, userId, Set.of(1, 2))); + em.persist(createEpisodeStar(node2, userId, Set.of(2, 3))); + + flushAndClear(); + + List details = episodeRepository.findDetailsByMindmapIdAndUserId(m1.getId(), userId); + + assertThat(details).hasSize(2); + assertThat(details).extracting(EpisodeDetail::nodeId).containsExactlyInAnyOrder(node1, node2); + } + + @Test + @DisplayName("에피소드 상세 조회") + void findDetail() { + Mindmap m1 = createMindmap("mm-1"); + em.persist(m1); + + long userId = 100L; + em.persist(createUser(userId)); + + UUID node = UUID.randomUUID(); + em.persist(createEpisode(node, m1.getId(), "content")); + em.persist(createEpisodeStar(node, userId, Set.of(7, 8))); + + flushAndClear(); + + Optional opt = episodeRepository.findDetail(node, userId); + + assertThat(opt).isPresent(); + assertThat(opt.get().nodeId()).isEqualTo(node); + assertThat(opt.get().competencyTypeIds()).containsExactlyInAnyOrder(7, 8); + } + + @Test + @DisplayName("Mindmap에 속한 competencyTypeIds 중복 제거 조회") + void findCompetencyTypesByMindmapId() { + Mindmap m1 = createMindmap("mm-1"); + em.persist(m1); + + long userA = 100L; + long userB = 200L; + em.persist(createUser(userA)); + em.persist(createUser(userB)); + + UUID node1 = UUID.randomUUID(); + UUID node2 = UUID.randomUUID(); + + em.persist(createEpisode(node1, m1.getId(), "c1")); + em.persist(createEpisode(node2, m1.getId(), "c2")); + + em.persist(createEpisodeStar(node1, userA, Set.of(1, 2, 3))); + em.persist(createEpisodeStar(node2, userB, Set.of(2, 3, 4))); + + flushAndClear(); + + List ids = episodeRepository.findCompetencyTypesByMindmapId(m1.getId()); + + assertThat(ids).containsExactlyInAnyOrder(1, 2, 3, 4); + } + + private void flushAndClear() { + em.flush(); + em.clear(); + } +} diff --git a/backend/api/src/test/java/com/yat2/episode/mindmap/MindmapRepositoryTest.java b/backend/api/src/test/java/com/yat2/episode/mindmap/MindmapRepositoryTest.java index 54938bbd5..25edd0eb0 100644 --- a/backend/api/src/test/java/com/yat2/episode/mindmap/MindmapRepositoryTest.java +++ b/backend/api/src/test/java/com/yat2/episode/mindmap/MindmapRepositoryTest.java @@ -5,7 +5,6 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.data.jpa.test.autoconfigure.DataJpaTest; import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.util.ReflectionTestUtils; import java.time.LocalDateTime; import java.util.List; @@ -16,36 +15,35 @@ import com.yat2.episode.user.UserRepository; import com.yat2.episode.utils.AbstractRepositoryTest; -import static com.yat2.episode.utils.TestEntityFactory.createEntity; import static com.yat2.episode.utils.TestEntityFactory.createMindmap; +import static com.yat2.episode.utils.TestEntityFactory.createParticipant; +import static com.yat2.episode.utils.TestEntityFactory.createUser; import static org.assertj.core.api.Assertions.assertThat; @DataJpaTest @ActiveProfiles("test") @DisplayName("MindmapRepository 통합 테스트") class MindmapRepositoryTest extends AbstractRepositoryTest { - @Autowired - private MindmapRepository mindmapRepository; @Autowired - private UserRepository userRepository; - + MindmapRepository mindmapRepository; @Autowired - private MindmapParticipantRepository participantRepository; + UserRepository userRepository; + @Autowired + MindmapParticipantRepository participantRepository; @Test @DisplayName("사용자 ID로 참여 중인 마인드맵 목록을 최신순으로 조회한다") void findByUserIdOrderByCreatedDesc_Success() { - User user = User.newUser(12345L, "테스트유저"); + User user = createUser(12345L); userRepository.save(user); Mindmap m1 = createMindmap("먼저 만든 마인드맵", false, LocalDateTime.now().minusSeconds(10)); Mindmap m2 = createMindmap("나중에 만든 마인드맵", false, LocalDateTime.now()); - mindmapRepository.save(m1); - mindmapRepository.save(m2); + mindmapRepository.saveAll(List.of(m1, m2)); - saveParticipant(user, m1); - saveParticipant(user, m2); + participantRepository.save(createParticipant(m1, user)); + participantRepository.save(createParticipant(m2, user)); List result = mindmapRepository.findByUserIdOrderByCreatedDesc(12345L); @@ -56,16 +54,17 @@ void findByUserIdOrderByCreatedDesc_Success() { @Test @DisplayName("특정 이름을 접두사로 가진 사용자의 마인드맵 이름들을 모두 조회한다") void findAllNamesByBaseName_Success() { - User user = User.newUser(1L, "애플"); + User user = createUser(1L); userRepository.save(user); String baseName = "애플의 마인드맵"; Mindmap m1 = createMindmap(baseName, false); Mindmap m2 = createMindmap(baseName + "(1)", false); - mindmapRepository.saveAll(List.of(m1, m2)); - saveParticipant(user, m1); - saveParticipant(user, m2); + + participantRepository.save(createParticipant(m1, user)); + participantRepository.save(createParticipant(m2, user)); + List names = mindmapRepository.findAllNamesByBaseName(baseName, 1L); assertThat(names).hasSize(2); @@ -85,10 +84,4 @@ void findByIdWithLock_Success() { assertThat(result.get().getName()).isEqualTo("잠금 테스트"); } - private void saveParticipant(User user, Mindmap mindmap) { - MindmapParticipant participant = createEntity(MindmapParticipant.class); - ReflectionTestUtils.setField(participant, "user", user); - ReflectionTestUtils.setField(participant, "mindmap", mindmap); - participantRepository.save(participant); - } } diff --git a/backend/api/src/test/java/com/yat2/episode/utils/AbstractRepositoryTest.java b/backend/api/src/test/java/com/yat2/episode/utils/AbstractRepositoryTest.java index eb568dbf8..e4b7fdbe2 100644 --- a/backend/api/src/test/java/com/yat2/episode/utils/AbstractRepositoryTest.java +++ b/backend/api/src/test/java/com/yat2/episode/utils/AbstractRepositoryTest.java @@ -4,14 +4,15 @@ import org.springframework.test.context.DynamicPropertyRegistry; import org.springframework.test.context.DynamicPropertySource; import org.testcontainers.containers.MySQLContainer; -import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.utility.DockerImageName; -@Testcontainers @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) public abstract class AbstractRepositoryTest { + private static final DockerImageName MYSQL_IMAGE = DockerImageName.parse("mysql:8.0.45"); + static final MySQLContainer MYSQL = - new MySQLContainer<>("mysql:8.0").withDatabaseName("testdb").withUsername("test").withPassword("test"); + new MySQLContainer<>(MYSQL_IMAGE).withDatabaseName("testdb").withUsername("test").withPassword("test"); static { MYSQL.start(); @@ -22,6 +23,6 @@ static void configureProperties(DynamicPropertyRegistry registry) { registry.add("spring.datasource.url", MYSQL::getJdbcUrl); registry.add("spring.datasource.username", MYSQL::getUsername); registry.add("spring.datasource.password", MYSQL::getPassword); - registry.add("spring.datasource.driver-class-name", () -> "com.mysql.cj.jdbc.Driver"); + registry.add("spring.datasource.driver-class-name", MYSQL::getDriverClassName); } } diff --git a/backend/api/src/test/java/com/yat2/episode/utils/TestEntityFactory.java b/backend/api/src/test/java/com/yat2/episode/utils/TestEntityFactory.java index 13c1d497c..0ed857314 100644 --- a/backend/api/src/test/java/com/yat2/episode/utils/TestEntityFactory.java +++ b/backend/api/src/test/java/com/yat2/episode/utils/TestEntityFactory.java @@ -4,9 +4,18 @@ import org.springframework.test.util.ReflectionTestUtils; import java.lang.reflect.Constructor; +import java.time.LocalDate; import java.time.LocalDateTime; +import java.util.Set; +import java.util.UUID; +import com.yat2.episode.episode.Episode; +import com.yat2.episode.episode.EpisodeStar; +import com.yat2.episode.episode.dto.EpisodeUpsertContentReq; +import com.yat2.episode.episode.dto.StarUpdateReq; import com.yat2.episode.mindmap.Mindmap; +import com.yat2.episode.mindmap.MindmapParticipant; +import com.yat2.episode.user.User; public class TestEntityFactory { @@ -20,12 +29,12 @@ public static T createEntity(Class clazz) { } } + public static Mindmap createMindmap(String name) { + return createMindmap(name, false, LocalDateTime.now()); + } + public static Mindmap createMindmap(String name, boolean isShared) { - Mindmap mindmap = createEntity(Mindmap.class); - ReflectionTestUtils.setField(mindmap, "id", UuidCreator.getTimeOrderedEpoch()); - ReflectionTestUtils.setField(mindmap, "name", name); - ReflectionTestUtils.setField(mindmap, "shared", isShared); - return mindmap; + return createMindmap(name, isShared, LocalDateTime.now()); } public static Mindmap createMindmap(String name, boolean isShared, LocalDateTime createdAt) { @@ -36,4 +45,34 @@ public static Mindmap createMindmap(String name, boolean isShared, LocalDateTime ReflectionTestUtils.setField(mindmap, "createdAt", createdAt); return mindmap; } + + public static MindmapParticipant createParticipant(Mindmap mindmap, User user) { + MindmapParticipant participant = createEntity(MindmapParticipant.class); + ReflectionTestUtils.setField(participant, "mindmap", mindmap); + ReflectionTestUtils.setField(participant, "user", user); + return participant; + } + + public static User createUser(long kakaoId) { + User user = TestEntityFactory.createEntity(User.class); + ReflectionTestUtils.setField(user, "kakaoId", kakaoId); + ReflectionTestUtils.setField(user, "nickname", "test-user-" + kakaoId); + ReflectionTestUtils.setField(user, "hasWatchedFeatureGuide", false); + return user; + } + + public static Episode createEpisode(UUID nodeId, UUID mindmapId, String content) { + Episode episode = Episode.create(nodeId, mindmapId); + episode.update(new EpisodeUpsertContentReq(content)); + return episode; + } + + public static EpisodeStar createEpisodeStar(UUID nodeId, long userId, Set competencyTypeIds) { + EpisodeStar star = EpisodeStar.create(nodeId, userId); + star.update(new StarUpdateReq(competencyTypeIds, "situation", "task", "action", "result", LocalDate.now(), + LocalDate.now().plusDays(1))); + ReflectionTestUtils.setField(star, "createdAt", LocalDateTime.now()); + return star; + } } +