Skip to content

Commit

Permalink
feat: 사용자용 요청 주제(토픽) 생성/조회 API 구현 (#318)
Browse files Browse the repository at this point in the history
* feat: 사용자용 요청 주제(토픽) 생성/조회 API 구현 (#306)

* chore: 충돌 해결

* feat: 사용자용 요청 주제 목록 조회 API URI를 WhiteList에 추가 (#306)

* test: 토픽 생성 테스트 검증 수정 (#306)
  • Loading branch information
kdkdhoho authored Oct 27, 2024
1 parent 982b8fd commit 53b0b08
Show file tree
Hide file tree
Showing 15 changed files with 434 additions and 11 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ public class AuthorizationInterceptor implements HandlerInterceptor {
new UriAndMethod("/categories", GET),
new UriAndMethod("/users/basic-profile-image", GET),
new UriAndMethod("/users/basic-background-image", GET),
new UriAndMethod("/topics", GET)
};

private final JwtManager jwtManager;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import static com.listywave.common.exception.ErrorCode.RESOURCE_NOT_FOUND;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.listywave.common.exception.CustomException;
import java.util.Arrays;
import lombok.AllArgsConstructor;
Expand All @@ -15,31 +14,39 @@ public enum CategoryType {
ENTIRE("0", "전체"),
MUSIC("1", "음악"),
MOVIE_DRAMA("2", "영화&드라마"),
ENTERTAINMENT_ARTS("3","엔터&예술"),
ENTERTAINMENT_ARTS("3", "엔터&예술"),
TRAVEL("4", "여행"),
RESTAURANT_CAFE("5", "맛집&카페"),
FOOD_RECIPES("6", "음식&레시피"),
PLACE("7", "공간"),
DAILYLIFE_THOUGHTS("8", "일상&생각"),
HOBBY_LEISURE("9", "취미&레저"),
ETC("10", "기타")
ETC("10", "기타"),
;

private static final String ERROR_MESSAGE = "해당 카테고리는 존재하지 않습니다. 입력값: ";

private final String code;
private final String viewName;

public static CategoryType codeOf(String code) {
return Arrays.stream(CategoryType.values())
.filter(t -> t.getCode().equals(code))
.findAny()
.orElseThrow(() -> new CustomException(RESOURCE_NOT_FOUND, "해당 카테고리코드는 존재하지 않습니다."));
.orElseThrow(() -> new CustomException(RESOURCE_NOT_FOUND, ERROR_MESSAGE + code));
}

@JsonCreator
public static CategoryType nameOf(String name) {
return Arrays.stream(CategoryType.values())
.filter(categoryType -> categoryType.name().equalsIgnoreCase(name))
.findFirst()
.orElseThrow(() -> new CustomException(RESOURCE_NOT_FOUND, "해당 카테고리는 존재하지 않습니다."));
.orElseThrow(() -> new CustomException(RESOURCE_NOT_FOUND, ERROR_MESSAGE + name));
}

public static CategoryType viewNameOf(String viewName) {
return Arrays.stream(CategoryType.values())
.filter(categoryType -> categoryType.getViewName().equals(viewName))
.findFirst()
.orElseThrow(() -> new CustomException(RESOURCE_NOT_FOUND, ERROR_MESSAGE + viewName));
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package com.listywave.list.application.domain.category;

import jakarta.persistence.AttributeConverter;
import jakarta.persistence.Converter;

@Converter(autoApply = true)
public class CategoryTypeConverter implements AttributeConverter<CategoryType, String> {

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,11 @@
import com.listywave.collaborator.application.domain.Collaborators;
import com.listywave.common.exception.CustomException;
import com.listywave.list.application.domain.category.CategoryType;
import com.listywave.list.application.domain.category.CategoryTypeConverter;
import com.listywave.list.application.domain.item.Item;
import com.listywave.list.application.domain.item.Items;
import com.listywave.list.application.domain.label.Labels;
import com.listywave.user.application.domain.User;
import jakarta.persistence.Column;
import jakarta.persistence.Convert;
import jakarta.persistence.Embedded;
import jakarta.persistence.Entity;
import jakarta.persistence.EntityListeners;
Expand Down Expand Up @@ -56,7 +54,6 @@ public class ListEntity {
private User user;

@Column(name = "category_code", length = 10, nullable = false)
@Convert(converter = CategoryTypeConverter.class)
private CategoryType category;

@Embedded
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,6 @@
import java.util.List;
import java.util.Set;
import lombok.RequiredArgsConstructor;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;
import org.springframework.stereotype.Service;
Expand All @@ -78,7 +77,6 @@ public class ListService {
private final CollaboratorService collaboratorService;
private final HistoryService historyService;
private final ReactionService reactionService;
private final ApplicationEventPublisher applicationEventPublisher;

public ListCreateResponse listCreate(ListCreateRequest request, Long loginUserId) {
User user = userRepository.getById(loginUserId);
Expand Down
46 changes: 46 additions & 0 deletions src/main/java/com/listywave/topic/application/domain/Topic.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package com.listywave.topic.application.domain;

import static lombok.AccessLevel.PROTECTED;

import com.listywave.common.BaseEntity;
import com.listywave.list.application.domain.category.CategoryType;
import com.listywave.list.application.domain.list.ListDescription;
import com.listywave.list.application.domain.list.ListTitle;
import com.listywave.user.application.domain.User;
import jakarta.persistence.Column;
import jakarta.persistence.Embedded;
import jakarta.persistence.Entity;
import jakarta.persistence.ForeignKey;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Entity
@Getter
@Builder
@NoArgsConstructor(access = PROTECTED)
@AllArgsConstructor
public class Topic extends BaseEntity {

@ManyToOne
@JoinColumn(name = "user_id", nullable = false, foreignKey = @ForeignKey(name = "topic_user_fk"))
private User user;

@Column(name = "category_code", length = 10, nullable = false)
private CategoryType category;

@Embedded
private ListTitle title;

@Embedded
private ListDescription description;

@Column(nullable = false)
private boolean isAnonymous;

@Column(nullable = false)
private boolean isExposed;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.listywave.topic.application.service;

import com.listywave.topic.application.domain.Topic;
import com.listywave.topic.application.service.dto.ExposedTopicFindResponse;
import com.listywave.topic.application.service.dto.TopicCreateRequest;
import com.listywave.topic.repository.TopicRepository;
import com.listywave.user.application.domain.User;
import com.listywave.user.repository.user.UserRepository;
import jakarta.annotation.Nullable;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@Transactional
@RequiredArgsConstructor
public class TopicService {

private final UserRepository userRepository;
private final TopicRepository topicRepository;

public void create(TopicCreateRequest request, Long userId) {
User user = userRepository.getById(userId);
Topic topic = request.toEntity(user);
topicRepository.save(topic);
}

public ExposedTopicFindResponse findAllExposed(@Nullable Long cursorId, int size) {
List<Topic> result = topicRepository.findAllExposed(cursorId, size);
return ExposedTopicFindResponse.of(result, size);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package com.listywave.topic.application.service.dto;

import com.listywave.topic.application.domain.Topic;
import java.util.List;
import lombok.Builder;

public record ExposedTopicFindResponse(
Long cursorId,
boolean hasNext,
List<TopicDto> topics
) {

public static ExposedTopicFindResponse of(List<Topic> topics, int size) {
if (topics.isEmpty()) {
return new ExposedTopicFindResponse(null, false, List.of());
}

boolean hasNext = false;
if (topics.size() > size) {
hasNext = true;
topics.remove(topics.size() - 1);
}
Long cursorId = topics.get(topics.size() - 1).getId();
List<TopicDto> topicDtos = TopicDto.toList(topics);

return new ExposedTopicFindResponse(cursorId, hasNext, topicDtos);
}

@Builder
public record TopicDto(
Long id,
String categoryEngName,
String categoryKorName,
String title,
String description,
Long ownerId,
String ownerNickname,
boolean isAnonymous
) {

public static List<TopicDto> toList(List<Topic> topics) {
return topics.stream()
.map(TopicDto::of)
.toList();
}

public static TopicDto of(Topic topic) {
return TopicDto.builder()
.id(topic.getId())
.categoryEngName(topic.getCategory().name())
.categoryKorName(topic.getCategory().getViewName())
.title(topic.getTitle().getValue())
.description(topic.getDescription().getValue())
.ownerId(topic.getUser().getId())
.ownerNickname(topic.getUser().getNickname())
.isAnonymous(topic.isAnonymous())
.build();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.listywave.topic.application.service.dto;

import com.listywave.list.application.domain.category.CategoryType;
import com.listywave.list.application.domain.list.ListDescription;
import com.listywave.list.application.domain.list.ListTitle;
import com.listywave.topic.application.domain.Topic;
import com.listywave.user.application.domain.User;

public record TopicCreateRequest(
String categoryKorName,
String title,
String description,
boolean isAnonymous
) {

public Topic toEntity(User user) {
return Topic.builder()
.user(user)
.category(CategoryType.viewNameOf(categoryKorName))
.title(new ListTitle(title))
.description(new ListDescription(description))
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.listywave.topic.presentation;

import com.listywave.common.auth.Auth;
import com.listywave.topic.application.service.TopicService;
import com.listywave.topic.application.service.dto.ExposedTopicFindResponse;
import com.listywave.topic.application.service.dto.TopicCreateRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequiredArgsConstructor
public class TopicController {

private final TopicService topicService;

@PostMapping("/topics")
ResponseEntity<Void> create(@RequestBody TopicCreateRequest request, @Auth Long userId) {
topicService.create(request, userId);
return ResponseEntity.noContent().build();
}

@GetMapping("/topics")
ResponseEntity<ExposedTopicFindResponse> findAllExposed(
@RequestParam(required = false) Long cursorId,
@RequestParam(defaultValue = "10") int size
) {
ExposedTopicFindResponse result = topicService.findAllExposed(cursorId, size);
return ResponseEntity.ok(result);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.listywave.topic.repository;

import com.listywave.topic.application.domain.Topic;
import java.util.List;

public interface CustomTopicRepository {

List<Topic> findAllExposed(Long cursorId, int size);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.listywave.topic.repository;

import static com.listywave.topic.application.domain.QTopic.topic;
import static com.listywave.user.application.domain.QUser.user;

import com.listywave.topic.application.domain.Topic;
import com.querydsl.core.types.dsl.BooleanExpression;
import com.querydsl.jpa.impl.JPAQueryFactory;
import java.util.List;
import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
public class CustomTopicRepositoryImpl implements CustomTopicRepository {

private final JPAQueryFactory queryFactory;

@Override
public List<Topic> findAllExposed(Long cursorId, int size) {
return queryFactory
.selectFrom(topic)
.join(user).on(topic.user.id.eq(user.id))
.where(
cursorIdLowerThan(cursorId),
topic.isExposed.isTrue()
)
.limit(size + 1)
.orderBy(topic.id.desc())
.fetch();
}

private static BooleanExpression cursorIdLowerThan(Long cursorId) {
return cursorId == null ? null : topic.id.lt(cursorId);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.listywave.topic.repository;

import com.listywave.topic.application.domain.Topic;
import org.springframework.data.jpa.repository.JpaRepository;

public interface TopicRepository extends JpaRepository<Topic, Long>, CustomTopicRepository {
}
7 changes: 7 additions & 0 deletions src/test/java/com/listywave/common/IntegrationTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
import com.listywave.list.repository.list.ListRepository;
import com.listywave.list.repository.reply.ReplyRepository;
import com.listywave.mention.MentionRepository;
import com.listywave.topic.application.service.TopicService;
import com.listywave.topic.repository.TopicRepository;
import com.listywave.user.application.domain.User;
import com.listywave.user.application.service.UserService;
import com.listywave.user.repository.follow.FollowRepository;
Expand Down Expand Up @@ -75,12 +77,17 @@ public abstract class IntegrationTest {
protected FolderService folderService;
@Autowired
protected FollowRepository followRepository;
@Autowired
protected TopicService topicService;
@Autowired
protected TopicRepository topicRepository;

protected User dh, js, ej, sy;
protected ListEntity list;

@BeforeEach
void setUp() {
topicRepository.deleteAllInBatch();
followRepository.deleteAllInBatch();
collectionRepository.deleteAllInBatch();
alarmRepository.deleteAllInBatch();
Expand Down
Loading

0 comments on commit 53b0b08

Please sign in to comment.