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
4 changes: 2 additions & 2 deletions .github/workflows/commitlint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name: Commit Message Lint

on:
pull_request:
branches: [ main, develop ]
branches: [ main ]

jobs:
commitlint:
Expand Down Expand Up @@ -32,4 +32,4 @@ jobs:
exit 1
fi

echo "모든 커밋 메시지 검사 통과!"
echo "모든 커밋 메시지 검사 통과!"
63 changes: 0 additions & 63 deletions .github/workflows/deploy-dev.yml

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -20,54 +20,12 @@ public class SectionJpaMapper {
private final ChapterJpaMapper chapterJpaMapper;
private final SectionKeyPointJpaMapper sectionKeyPointJpaMapper;
private final CategoryJpaMapper categoryJpaMapper;
private final SectionJpaRepository sectionJpaRepository;

public SectionJpaEntity toJpaEntity(Section section) {
SectionJpaEntity sectionEntity = new SectionJpaEntity(
section.getId(),
section.getMajor(),
section.getTitle(),
section.getDescription(),
categoryJpaMapper.toJpaEntity(section.getCategory()),
section.getOrderIndex(),
new ArrayList<>(),
new ArrayList<>(),
new HashSet<>(),
section.getCreatedAt(), // createdAt
section.getUpdatedAt() // updatedAt
);

// null 안전성: section.getChapters()가 null이면 빈 리스트로 처리
List<ChapterJpaEntity> chapters = (section.getChapters() != null)
? section.getChapters().stream()
.map(c -> chapterJpaMapper.toEntity(c, sectionEntity)).toList() :
new ArrayList<>();

List<SectionKeyPointJpaEntity> keyPoints = (section.getKeyPoints() != null)
? section.getKeyPoints().stream().map(k -> sectionKeyPointJpaMapper.toJpaEntity(k, sectionEntity)).toList() :
new ArrayList<>();

sectionEntity.getChapters().addAll(chapters);
sectionEntity.getKeyPoints().addAll(keyPoints);

if (section.getPrerequisites() != null && !section.getPrerequisites().isEmpty()) {
// DB에서 managed 엔티티를 가져와서 사용 (transient 엔티티 생성 방지)
List<Long> prerequisiteIds = section.getPrerequisites().stream()
.map(Section::getId)
.toList();
List<SectionJpaEntity> managedPrerequisites = sectionJpaRepository.findAllById(prerequisiteIds);
sectionEntity.getPrerequisites().addAll(managedPrerequisites);
}

return sectionEntity;
}

public SectionJpaEntity toJpaEntity(Section section, Map<Long, CategoryJpaEntity> categoryMap) {
CategoryJpaEntity categoryEntity = categoryMap.get(section.getCategory().getId());
if (categoryEntity == null) {
throw new RuntimeException("Category not found: " + section.getCategory().getId());
}

public SectionJpaEntity toJpaEntity(
Section section,
CategoryJpaEntity categoryEntity,
Set<SectionJpaEntity> prerequisites
) {
SectionJpaEntity sectionEntity = new SectionJpaEntity(
section.getId(),
section.getMajor(),
Expand All @@ -77,7 +35,7 @@ public SectionJpaEntity toJpaEntity(Section section, Map<Long, CategoryJpaEntity
section.getOrderIndex(),
new ArrayList<>(),
new ArrayList<>(),
new HashSet<>(),
new HashSet<>(prerequisites),
section.getCreatedAt(), // createdAt
section.getUpdatedAt() // updatedAt
);
Expand All @@ -95,15 +53,6 @@ public SectionJpaEntity toJpaEntity(Section section, Map<Long, CategoryJpaEntity
sectionEntity.getChapters().addAll(chapters);
sectionEntity.getKeyPoints().addAll(keyPoints);

if (section.getPrerequisites() != null && !section.getPrerequisites().isEmpty()) {
// DB에서 managed 엔티티를 가져와서 사용 (transient 엔티티 생성 방지)
List<Long> prerequisiteIds = section.getPrerequisites().stream()
.map(Section::getId)
.toList();
List<SectionJpaEntity> managedPrerequisites = sectionJpaRepository.findAllById(prerequisiteIds);
sectionEntity.getPrerequisites().addAll(managedPrerequisites);
}

return sectionEntity;
}

Expand Down Expand Up @@ -149,4 +98,4 @@ private Section toPrerequisiteDomain(SectionJpaEntity entity) {
entity.getUpdatedAt()
);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,17 @@
import com.process.clash.domain.common.enums.Major;
import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;

import java.util.Collection;
import java.util.List;
import java.util.Optional;

@Repository
public interface SectionJpaRepository extends JpaRepository<SectionJpaEntity, Long> {

@EntityGraph(attributePaths = {"chapters", "keyPoints", "prerequisites"})
@EntityGraph(attributePaths = {"category", "prerequisites"})
Optional<SectionJpaEntity> findById(Long id);

@EntityGraph(attributePaths = {"chapters", "keyPoints", "prerequisites"})
Expand All @@ -20,6 +22,9 @@ public interface SectionJpaRepository extends JpaRepository<SectionJpaEntity, Lo
@EntityGraph(attributePaths = {"chapters", "keyPoints", "prerequisites"})
List<SectionJpaEntity> findAllById(Iterable<Long> ids);

@Query("SELECT section FROM SectionJpaEntity section WHERE section.id IN :ids")
List<SectionJpaEntity> findAllReferencesById(Collection<Long> ids);

@EntityGraph(attributePaths = {"category"})
List<SectionJpaEntity> findAllByMajorOrderByOrderIndexAsc(Major major);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import com.process.clash.domain.roadmap.entity.Chapter;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;

import java.util.*;
import java.util.stream.Collectors;
Expand All @@ -31,9 +32,12 @@ public Section save(Section section) {
// 카테고리 조회
CategoryJpaEntity categoryEntity = categoryJpaRepository.findById(section.getCategory().getId())
.orElseThrow(CategoryNotFoundException::new);
Map<Long, CategoryJpaEntity> categoryMap = Map.of(section.getCategory().getId(), categoryEntity);

SectionJpaEntity newEntity = sectionJpaMapper.toJpaEntity(section, categoryMap);
SectionJpaEntity newEntity = sectionJpaMapper.toJpaEntity(
section,
categoryEntity,
resolveManagedPrerequisites(section.getPrerequisites())
);
SectionJpaEntity saved = sectionJpaRepository.save(newEntity);
return sectionJpaMapper.toDomain(saved);
}
Expand Down Expand Up @@ -88,7 +92,15 @@ public List<Section> saveAll(List<Section> sections) {
allEntities.add(entity);
} else {
// 신규 객체만 saveAll로 저장
SectionJpaEntity newEntity = sectionJpaMapper.toJpaEntity(domain, categoryMap);
CategoryJpaEntity categoryEntity = categoryMap.get(domain.getCategory().getId());
if (categoryEntity == null) {
throw new CategoryNotFoundException();
}
SectionJpaEntity newEntity = sectionJpaMapper.toJpaEntity(
domain,
categoryEntity,
resolveManagedPrerequisites(domain.getPrerequisites())
);
newEntities.add(newEntity);
allEntities.add(newEntity);
}
Expand All @@ -112,8 +124,11 @@ public Optional<Section> findById(Long id) {
}

@Override
@Transactional(readOnly = true)
public List<Section> findAllById(List<Long> ids) {
return sectionJpaRepository.findAllById(ids).stream().map(sectionJpaMapper::toDomain).toList();
return sectionJpaRepository.findAllReferencesById(ids).stream()
.map(sectionJpaMapper::toDomain)
.toList();
}

@Override
Expand Down Expand Up @@ -194,19 +209,32 @@ private void updateSectionDetails(SectionJpaEntity entity, Section domain, Map<L
// CreateSection은 toJpaEntity()에서 cascade로 처리됨

// 4. Prerequisites (선수 로드맵) 교체
// ManyToMany는 관계 테이블만 관리하므로, 보통 ID 조회 후 Set 교체 방식을 써도 무방함
if (domain.getPrerequisites() != null) {
List<Long> prereqIds = domain.getPrerequisites().stream()
.map(Section::getId)
.toList();

if (!prereqIds.isEmpty()) {
// DB에서 실제 엔티티를 조회하여 영속성 컨텍스트가 관리하는 객체로 세팅
Set<SectionJpaEntity> managedPrereqs = new HashSet<>(sectionJpaRepository.findAllById(prereqIds));
entity.updatePrerequisites(managedPrereqs);
} else {
entity.updatePrerequisites(new HashSet<>());
}
entity.updatePrerequisites(resolveManagedPrerequisites(domain.getPrerequisites()));
}
}

private Set<SectionJpaEntity> resolveManagedPrerequisites(Set<Section> prerequisites) {
if (prerequisites == null || prerequisites.isEmpty()) {
return Set.of();
}

Set<Long> prerequisiteIds = prerequisites.stream()
.map(Section::getId)
.filter(Objects::nonNull)
.collect(Collectors.toSet());

if (prerequisiteIds.size() != prerequisites.size()) {
throw new SectionNotFoundException();
}

List<SectionJpaEntity> managedPrerequisites = sectionJpaRepository
.findAllReferencesById(prerequisiteIds);

if (managedPrerequisites.size() != prerequisiteIds.size()) {
throw new SectionNotFoundException();
}

return new HashSet<>(managedPrerequisites);
}
}
}
2 changes: 0 additions & 2 deletions src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -163,8 +163,6 @@ cors:
- https://api.clash.kr
- ${PRODUCTION_WEB_SERVER:}
- ${PRODUCTION_ELECTRON_CUSTOM_PROTOCOL:}
- ${DEVELOP_ELECTRON_SERVER:}
- ${DEVELOP_WEB_SERVER:}

management:
endpoint:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,14 @@
import com.process.clash.application.roadmap.category.port.in.CreateCategoryUseCase;
import com.process.clash.application.roadmap.section.data.CreateSectionData;
import com.process.clash.application.roadmap.section.data.UpdateSectionData;
import com.process.clash.application.roadmap.section.exception.exception.notfound.SectionNotFoundException;
import com.process.clash.application.roadmap.section.port.in.CreateSectionUseCase;
import com.process.clash.application.roadmap.section.port.in.UpdateSectionUseCase;
import com.process.clash.application.roadmap.section.port.out.SectionKeyPointRepositoryPort;
import com.process.clash.application.roadmap.section.port.out.SectionRepositoryPort;
import com.process.clash.domain.common.enums.Major;
import com.process.clash.domain.roadmap.entity.Category;
import com.process.clash.domain.roadmap.entity.Section;
import com.process.clash.domain.roadmap.entity.SectionKeyPoint;
import com.process.clash.domain.user.user.enums.Role;
import jakarta.persistence.EntityManager;
Expand All @@ -24,8 +28,10 @@
import org.springframework.transaction.annotation.Transactional;

import java.util.List;
import java.util.Set;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

/**
* UpdateSectionService의 keyPoints 업데이트 시 JPA Cascade와의 충돌 여부를 확인하는 통합 테스트
Expand All @@ -52,6 +58,9 @@ public class UpdateSectionServiceIntegrationTest {
@Autowired
private SectionKeyPointRepositoryPort keyPointRepository;

@Autowired
private SectionRepositoryPort sectionRepository;

@PersistenceContext
private EntityManager entityManager;

Expand Down Expand Up @@ -138,6 +147,53 @@ void updateKeyPoints_shouldNotConflictWithOrphanRemoval() {
System.out.println("===========================");
}

@Test
@DisplayName("선수 Section은 영속 엔티티로 연결하여 저장한다")
void saveSection_withPrerequisite_shouldPersistRelationship() {
CreateCategoryData.Result categoryResult = createCategoryUseCase.execute(
new CreateCategoryData.Command(adminActor, "BASIC")
);
CreateSectionData.Result prerequisiteResult = createSectionUseCase.execute(new CreateSectionData.Command(
adminActor, Major.SERVER, "Prerequisite", categoryResult.categoryId(), "", List.of()
));
CreateSectionData.Result targetResult = createSectionUseCase.execute(new CreateSectionData.Command(
adminActor, Major.SERVER, "Target", categoryResult.categoryId(), "", List.of()
));

updateSectionUseCase.execute(new UpdateSectionData.Command(
adminActor, targetResult.sectionId(), null, null, null, null, null,
List.of(prerequisiteResult.sectionId())
));

entityManager.flush();
entityManager.clear();

Section savedTarget = sectionRepository.findById(targetResult.sectionId()).orElseThrow();
assertThat(savedTarget.getPrerequisites())
.extracting(Section::getId)
.containsExactly(prerequisiteResult.sectionId());
}

@Test
@DisplayName("존재하지 않는 선수 Section은 저장하지 않는다")
void saveSection_withMissingPrerequisite_shouldFail() {
CreateCategoryData.Result categoryResult = createCategoryUseCase.execute(
new CreateCategoryData.Command(adminActor, "BASIC")
);
Category category = new Category(categoryResult.categoryId(), "BASIC", null, null, null);
Section missingPrerequisite = new Section(
Long.MAX_VALUE, Major.SERVER, "Missing", "", category, 0,
List.of(), List.of(), Set.of(), null, null
);
Section newSection = new Section(
null, Major.SERVER, "Target", "", category, 0,
List.of(), List.of(), Set.of(missingPrerequisite), null, null
);

assertThatThrownBy(() -> sectionRepository.save(newSection))
.isInstanceOf(SectionNotFoundException.class);
}

@Test
@DisplayName("KeyPoints를 빈 리스트로 업데이트하면 모든 keyPoints가 삭제되는지 확인")
void updateKeyPoints_withEmptyList_shouldDeleteAllKeyPoints() {
Expand Down
Loading