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
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public static MentoringDetailDto from(Mentoring mentoring) {
return new MentoringDetailDto(
mentoring.getId(),
mentoring.getTitle(),
mentoring.getTags(),
mentoring.getTagNames(),
mentoring.getBio(),
mentoring.getThumb(),
mentoring.getCreateDate(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public static MentoringWithTagsDto from(Mentoring mentoring) {
return new MentoringWithTagsDto(
mentoring.getId(),
mentoring.getTitle(),
mentoring.getTags(),
mentoring.getTagNames(),
mentoring.getMentor().getId(),
mentoring.getMentor().getMember().getNickname()
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package com.back.domain.mentoring.mentoring.entity;

import com.back.domain.member.mentor.entity.Mentor;
import com.back.global.converter.StringListConverter;
import com.back.global.jpa.BaseEntity;
import jakarta.persistence.*;
import lombok.Builder;
Expand All @@ -25,30 +24,45 @@ public class Mentoring extends BaseEntity {
@Column(columnDefinition = "TEXT")
private String bio;

@Convert(converter = StringListConverter.class)
@Column(columnDefinition = "JSON")
private List<String> tags;
@OneToMany(mappedBy = "mentoring", cascade = CascadeType.ALL, orphanRemoval = true)
private List<MentoringTag> mentoringTags = new ArrayList<>();

@Column(length = 255)
private String thumb;

@Builder
public Mentoring(Mentor mentor, String title, String bio, List<String> tags, String thumb) {
public Mentoring(Mentor mentor, String title, String bio, String thumb) {
this.mentor = mentor;
this.title = title;
this.bio = bio;
this.tags = tags != null ? tags : new ArrayList<>();
this.thumb = thumb;
}

public void update(String title, String bio, List<String> tags, String thumb) {
public void update(String title, String bio, List<Tag> tags, String thumb) {
this.title = title;
this.bio = bio;
this.tags = tags != null ? tags : new ArrayList<>();
this.thumb = thumb;

updateTags(tags);
}

public void updateTags(List<Tag> tags) {
this.mentoringTags.clear();

if (tags != null) {
tags.forEach(tag ->
this.mentoringTags.add(new MentoringTag(this, tag))
);
}
}

public boolean isOwner(Mentor mentor) {
return this.mentor.equals(mentor);
}

public List<String> getTagNames() {
return mentoringTags.stream()
.map(mentoringTag -> mentoringTag.getTag().getName())
.toList();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.back.domain.mentoring.mentoring.entity;

import com.back.global.jpa.BaseEntity;
import jakarta.persistence.*;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Entity
@Getter
@NoArgsConstructor
@Table(name = "mentoring_tag")
public class MentoringTag extends BaseEntity {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "mentoring_id")
private Mentoring mentoring;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "tag_id")
private Tag tag;

@Builder
public MentoringTag(Mentoring mentoring, Tag tag) {
this.mentoring = mentoring;
this.tag = tag;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.back.domain.mentoring.mentoring.entity;

import com.back.global.jpa.BaseEntity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Entity
@Getter
@NoArgsConstructor
public class Tag extends BaseEntity {
@Column(length = 50, nullable = false, unique = true)
private String name;

@Builder
public Tag(String name) {
this.name = name;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@
import com.back.domain.member.mentor.entity.QMentor;
import com.back.domain.mentoring.mentoring.entity.Mentoring;
import com.back.domain.mentoring.mentoring.entity.QMentoring;
import com.back.domain.mentoring.mentoring.entity.QMentoringTag;
import com.back.domain.mentoring.mentoring.entity.QTag;
import com.querydsl.core.BooleanBuilder;
import com.querydsl.core.types.dsl.BooleanExpression;
import com.querydsl.jpa.JPAExpressions;
import com.querydsl.jpa.impl.JPAQueryFactory;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
Expand All @@ -23,40 +27,54 @@ public Page<Mentoring> searchMentorings(String keyword, Pageable pageable) {
QMentoring mentoring = QMentoring.mentoring;
QMentor mentor = QMentor.mentor;
QMember member = QMember.member;
QMentoringTag mentoringTag = QMentoringTag.mentoringTag;
QTag tag = QTag.tag;

BooleanBuilder builder = new BooleanBuilder();

// 제목, 멘토 닉네임 검색 조건
// 제목, 멘토 닉네임, 태그 검색 조건
if (keyword != null && !keyword.isBlank()) {
builder.and(
mentoring.title.containsIgnoreCase(keyword)
.or(mentor.member.nickname.containsIgnoreCase(keyword))
);
// 제목, 멘토 닉네임 검색 조건
BooleanExpression titleOrNickName = mentoring.title.containsIgnoreCase(keyword)
.or(mentor.member.nickname.containsIgnoreCase(keyword));

// 태그 검색 조건 (EXISTS 서브쿼리)
BooleanExpression tagSearch = JPAExpressions
.selectOne()
.from(mentoringTag)
.join(mentoringTag.tag, tag)
.where(
mentoringTag.mentoring.eq(mentoring)
.and(tag.name.containsIgnoreCase(keyword))
)
.exists();

builder.and(titleOrNickName).or(tagSearch);
}

// 1. 조건에 맞는 모든 데이터 조회 (태그 제외)
// 조건에 맞는 모든 데이터 조회
List<Mentoring> content = queryFactory
.selectFrom(mentoring)
.leftJoin(mentoring.mentor, mentor).fetchJoin()
.leftJoin(mentor.member, member).fetchJoin()
.leftJoin(mentoring.mentoringTags, mentoringTag).fetchJoin()
.leftJoin(mentoringTag.tag, tag).fetchJoin()
.where(builder)
.orderBy(mentoring.id.desc())
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.fetch();

// 2. 태그 검색
// TODO: 태그 테이블 분리 후 추가 예정

long total = getTotal(mentoring, builder);
long total = getTotal(mentoring, mentor, builder);

return new PageImpl<>(content, pageable, total);
}

private long getTotal(QMentoring mentoring, BooleanBuilder builder) {
private long getTotal(QMentoring mentoring, QMentor mentor, BooleanBuilder builder) {
Long totalCount = queryFactory
.select(mentoring.count())
.from(mentoring)
.leftJoin(mentoring.mentor, mentor)
.where(builder)
.fetchOne();
return totalCount != null ? totalCount : 0L;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.back.domain.mentoring.mentoring.repository;

import com.back.domain.mentoring.mentoring.entity.Tag;
import org.springframework.data.jpa.repository.JpaRepository;

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

public interface TagRepository extends JpaRepository<Tag, Long> {
Optional<Tag> findByName(String name);
List<Tag> findByNameIn(List<String> names);
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@
import com.back.domain.mentoring.mentoring.dto.request.MentoringRequest;
import com.back.domain.mentoring.mentoring.dto.response.MentoringResponse;
import com.back.domain.mentoring.mentoring.entity.Mentoring;
import com.back.domain.mentoring.mentoring.entity.Tag;
import com.back.domain.mentoring.mentoring.error.MentoringErrorCode;
import com.back.domain.mentoring.mentoring.repository.MentoringRepository;
import com.back.domain.mentoring.mentoring.repository.TagRepository;
import com.back.global.exception.ServiceException;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
Expand All @@ -17,11 +19,17 @@
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

@Service
@RequiredArgsConstructor
public class MentoringService {
private final MentoringRepository mentoringRepository;
private final MentoringStorage mentoringStorage;
private final TagRepository tagRepository;

@Transactional(readOnly = true)
public Page<MentoringWithTagsDto> getMentorings(String keyword, int page, int size) {
Expand Down Expand Up @@ -52,10 +60,12 @@ public MentoringResponse createMentoring(MentoringRequest reqDto, Mentor mentor)
.mentor(mentor)
.title(reqDto.title())
.bio(reqDto.bio())
.tags(reqDto.tags())
.thumb(reqDto.thumb())
.build();

List<Tag> tags = getOrCreateTags(reqDto.tags());
mentoring.updateTags(tags);

mentoringRepository.save(mentoring);

return new MentoringResponse(
Expand All @@ -70,7 +80,9 @@ public MentoringResponse updateMentoring(Long mentoringId, MentoringRequest reqD

validateOwner(mentoring, mentor);

mentoring.update(reqDto.title(), reqDto.bio(), reqDto.tags(), reqDto.thumb());
List<Tag> tags = getOrCreateTags(reqDto.tags());

mentoring.update(reqDto.title(), reqDto.bio(), tags, reqDto.thumb());

return new MentoringResponse(
MentoringDetailDto.from(mentoring),
Expand All @@ -97,6 +109,43 @@ public void deleteMentoring(Long mentoringId, Mentor mentor) {
}


// ==== Tag 관리 =====

private List<Tag> getOrCreateTags(List<String> tagNames) {
if (tagNames == null || tagNames.isEmpty()) {
return new ArrayList<>();
}

// 기존 태그 조회
List<Tag> existingTags = tagRepository.findByNameIn(tagNames);

Set<String> existingNames = existingTags.stream()
.map(Tag::getName)
.collect(Collectors.toSet());

// 신규 태그 생성
List<Tag> newTags = createNewTags(tagNames, existingNames);

// 기존 태그 + 신규 태그
List<Tag> allTags = new ArrayList<>(existingTags);
allTags.addAll(newTags);

return allTags;
}

private List<Tag> createNewTags(List<String> tagNames, Set<String> existingNames) {
List<Tag> newTags = tagNames.stream()
.filter(name -> !existingNames.contains(name))
.map(name -> Tag.builder().name(name).build())
.toList();

if (!newTags.isEmpty()) {
tagRepository.saveAll(newTags);
}
return newTags;
}


// ===== 유효성 검사 =====

private void validateOwner(Mentoring mentoring, Mentor mentor) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -192,9 +192,11 @@ void createMentoringSuccess() throws Exception {
// Mentoring 정보 검증
.andExpect(jsonPath("$.data.mentoring.mentoringId").value(mentoring.getId()))
.andExpect(jsonPath("$.data.mentoring.title").value(mentoring.getTitle()))
.andExpect(jsonPath("$.data.mentoring.tags").value(mentoring.getTags()))
.andExpect(jsonPath("$.data.mentoring.bio").value(mentoring.getBio()))
.andExpect(jsonPath("$.data.mentoring.thumb").value(mentoring.getThumb()))
.andExpect(jsonPath("$.data.mentoring.tags").isArray())
.andExpect(jsonPath("$.data.mentoring.tags[0]").value("Spring"))
.andExpect(jsonPath("$.data.mentoring.tags[1]").value("Java"))

// Mentor 정보 검증
.andExpect(jsonPath("$.data.mentor.mentorId").value(mentorOfMentoring.getId()))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,14 @@
import com.back.domain.mentoring.mentoring.dto.request.MentoringRequest;
import com.back.domain.mentoring.mentoring.dto.response.MentoringResponse;
import com.back.domain.mentoring.mentoring.entity.Mentoring;
import com.back.domain.mentoring.mentoring.entity.Tag;
import com.back.domain.mentoring.mentoring.error.MentoringErrorCode;
import com.back.domain.mentoring.mentoring.repository.MentoringRepository;
import com.back.domain.mentoring.mentoring.repository.TagRepository;
import com.back.fixture.MemberFixture;
import com.back.fixture.MentorFixture;
import com.back.fixture.mentoring.MentoringFixture;
import com.back.fixture.mentoring.TagFixture;
import com.back.global.exception.ServiceException;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
Expand Down Expand Up @@ -41,6 +44,9 @@ class MentoringServiceTest {
@Mock
private MentoringRepository mentoringRepository;

@Mock
private TagRepository tagRepository;

@Mock
private MentoringStorage mentoringStorage;

Expand Down Expand Up @@ -166,6 +172,10 @@ class Describe_createMentoring {
@Test
@DisplayName("생성 성공")
void createMentoring() {
List<Tag> tags = TagFixture.createDefaultTags();

when(tagRepository.findByNameIn(request.tags()))
.thenReturn(tags);
when(mentoringRepository.existsByMentorId(mentor1.getId()))
.thenReturn(false);

Expand All @@ -179,6 +189,7 @@ void createMentoring() {
assertThat(result.mentoring().tags()).isEqualTo(request.tags());
assertThat(result.mentoring().thumb()).isEqualTo(request.thumb());
verify(mentoringRepository).existsByMentorId(mentor1.getId());
verify(tagRepository).findByNameIn(request.tags());
verify(mentoringRepository).save(any(Mentoring.class));
}

Expand Down Expand Up @@ -207,7 +218,10 @@ class Describe_updateMentoring {
void updateMentoring() {
// given
Long mentoringId = 1L;
List<Tag> tags = TagFixture.createDefaultTags();

when(tagRepository.findByNameIn(request.tags()))
.thenReturn(tags);
when(mentoringStorage.findMentoring(mentoringId))
.thenReturn(mentoring1);

Expand All @@ -220,7 +234,9 @@ void updateMentoring() {
assertThat(result.mentoring().bio()).isEqualTo(request.bio());
assertThat(result.mentoring().tags()).isEqualTo(request.tags());
assertThat(result.mentoring().thumb()).isEqualTo(request.thumb());

verify(mentoringStorage).findMentoring(mentoringId);
verify(tagRepository).findByNameIn(request.tags());
}

@Test
Expand Down
Loading