Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: 개발질문 게시판의 해시태그 기능 구현 완료 #629

Open
wants to merge 9 commits into
base: develop
Choose a base branch
from
Open
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 @@ -4,6 +4,7 @@
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.apache.coyote.BadRequestException;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

불필요한 클래스가 import되었어요. 제거 부탁드러요.

import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import page.clab.api.domain.community.board.application.dto.response.BoardCategoryResponseDto;
import page.clab.api.domain.community.board.application.dto.response.BoardOverviewResponseDto;
import page.clab.api.domain.community.board.application.port.in.RetrieveBoardsByCategoryUseCase;
import page.clab.api.domain.community.board.domain.BoardCategory;
import page.clab.api.global.common.dto.ApiResponse;
Expand All @@ -33,15 +33,15 @@ public class BoardsByCategoryRetrievalController {
"DTO의 필드명을 기준으로 정렬 가능하며, 정렬 방향은 오름차순(asc)과 내림차순(desc)이 가능함")
@PreAuthorize("hasRole('GUEST')")
@GetMapping("/category")
public ApiResponse<PagedResponseDto<BoardCategoryResponseDto>> retrieveBoardsByCategory(
public ApiResponse<PagedResponseDto<BoardOverviewResponseDto>> retrieveBoardsByCategory(
@RequestParam(name = "category") BoardCategory category,
@RequestParam(name = "page", defaultValue = "0") int page,
@RequestParam(name = "size", defaultValue = "20") int size,
@RequestParam(name = "sortBy", defaultValue = "createdAt") List<String> sortBy,
@RequestParam(name = "sortDirection", defaultValue = "desc") List<String> sortDirection
) throws SortingArgumentException, InvalidColumnException {
Pageable pageable = pageableUtils.createPageable(page, size, sortBy, sortDirection, BoardCategoryResponseDto.class);
PagedResponseDto<BoardCategoryResponseDto> boards = retrieveBoardsByCategoryUseCase.retrieveBoardsByCategory(category, pageable);
Pageable pageable = pageableUtils.createPageable(page, size, sortBy, sortDirection, BoardOverviewResponseDto.class);
PagedResponseDto<BoardOverviewResponseDto> boards = retrieveBoardsByCategoryUseCase.retrieveBoardsByCategory(category, pageable);
return ApiResponse.success(boards);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package page.clab.api.domain.community.board.adapter.in.web;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Pageable;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import page.clab.api.domain.community.board.application.dto.response.BoardOverviewResponseDto;
import page.clab.api.domain.community.board.application.port.in.RetrieveBoardsByHashtagUseCase;
import page.clab.api.global.common.dto.ApiResponse;
import page.clab.api.global.common.dto.PagedResponseDto;
import page.clab.api.global.exception.InvalidColumnException;
import page.clab.api.global.exception.SortingArgumentException;
import page.clab.api.global.util.PageableUtils;

@RestController
@RequestMapping("/api/v1/boards")
@RequiredArgsConstructor
@Tag(name = "Community - Board", description = "커뮤니티 게시판")
public class BoardsByHashtagRetrievalController {

private final RetrieveBoardsByHashtagUseCase retrieveBoardsByHashtagUseCase;
private final PageableUtils pageableUtils;

@Operation(summary = "[G] 커뮤니티 게시글 해시태그로 조회", description = "ROLE_GUEST 이상의 권한이 필요함<br>" +
"DTO의 필드명을 기준으로 정렬 가능하며, 정렬 방향은 오름차순(asc)과 내림차순(desc)이 가능함<br>" +
"현재는 카테고리가 개발질문인 게시글만 해시태그가 적용되어 있어서 해당 API의 응답으로 개발질문 게시판만 반환됨")
@PreAuthorize("hasRole('GUEST')")
@GetMapping("/hashtag")
public ApiResponse<PagedResponseDto<BoardOverviewResponseDto>> retrieveBoardsByCategory(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이부분 메서드 네임을 retrieveBoardsByHashtag로 변경해도 괜찮을 것 같아요

@RequestParam(name = "hashtags") List<String> hashtags,
@RequestParam(name = "page", defaultValue = "0") int page,
@RequestParam(name = "size", defaultValue = "20") int size,
@RequestParam(name = "sortBy", defaultValue = "createdAt") List<String> sortBy,
@RequestParam(name = "sortDirection", defaultValue = "desc") List<String> sortDirection
) throws SortingArgumentException, InvalidColumnException {
Pageable pageable = pageableUtils.createPageable(page, size, sortBy, sortDirection, BoardOverviewResponseDto.class);
PagedResponseDto<BoardOverviewResponseDto> boards = retrieveBoardsByHashtagUseCase.retrieveBoardsByHashtag(hashtags, pageable);
return ApiResponse.success(boards);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package page.clab.api.domain.community.board.adapter.out.persistence;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.hibernate.annotations.SQLDelete;
import org.hibernate.annotations.SQLRestriction;
import page.clab.api.global.common.domain.BaseEntity;

@Entity
@Getter
@Setter
@Builder
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@SQLDelete(sql = "UPDATE board_hashtag SET is_deleted = true WHERE id = ?")
@SQLRestriction("is_deleted = false")
@Table(name = "board_hashtag")
public class BoardHashtagJpaEntity extends BaseEntity {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(name = "board_id", nullable = false)
private Long boardId;

@Column(name = "hashtag_id", nullable = false)
private Long hashtagId;

@Column(name = "is_deleted", nullable = false)
private Boolean isDeleted;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package page.clab.api.domain.community.board.adapter.out.persistence;

import org.mapstruct.Mapper;
import page.clab.api.domain.community.board.domain.BoardHashtag;

@Mapper(componentModel = "spring")
public interface BoardHashtagMapper {

BoardHashtagJpaEntity toEntity(BoardHashtag boardHashTag);

BoardHashtag toDomain(BoardHashtagJpaEntity entity);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package page.clab.api.domain.community.board.adapter.out.persistence;

import java.util.List;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Component;
import page.clab.api.domain.community.board.application.port.out.RegisterBoardHashtagPort;
import page.clab.api.domain.community.board.application.port.out.RetrieveBoardHashtagPort;
import page.clab.api.domain.community.board.domain.BoardHashtag;

@Component
@RequiredArgsConstructor
public class BoardHashtagPersistenceAdapter implements
RegisterBoardHashtagPort, RetrieveBoardHashtagPort {

private final BoardHashtagRepository boardHashtagRepository;
private final BoardHashtagMapper mapper;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

현재 프로젝트의 PersistenceAdapter 클래스에서 변수명으로 repository, mapper를 사용하는 경우, 도메인 이름을 접두어로 붙이는 방식과 붙이지 않는 방식이 혼재되어 있어요. 우선 해당 클래스 내에서 하나의 방식으로 통일하여 변수명을 변경하고, 이후에 전체적인 변수명 규칙에 대해 논의하는 시간을 가질 필요가 있어보여요.

@Override
public BoardHashtag save(BoardHashtag boardHashtag) {
BoardHashtagJpaEntity entity = mapper.toEntity(boardHashtag);
BoardHashtagJpaEntity savedEntity = boardHashtagRepository.save(entity);
return mapper.toDomain(savedEntity);
}

@Override
public List<BoardHashtag> getAllByBoardId(Long boardId) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이전에 코드베이스 전반 검토할 때, findBy는 예외처리를 하지 않고 조회하는 메서드, getBy는 예외처리까지 포함되어 조회하는 메서드에 적용하기로 했어서, 작업하신 메서드 이름들을 올바른 이름으로 바꾸는 작업이 필요할 것 같아요.

return boardHashtagRepository.findAllByBoardId(boardId).stream()
.map(mapper::toDomain)
.collect(Collectors.toList());
}

@Override
public List<BoardHashtag> getAllIncludingDeletedByBoardId(Long boardId) {
return boardHashtagRepository.findAllIncludingDeletedByBoardId(boardId).stream()
.map(mapper::toDomain)
.collect(Collectors.toList());
}

public List<Long> getBoardIdsByHashTagId(List<Long> hashtagIds, Pageable pageable) {
return boardHashtagRepository.getBoardIdsByHashTagId(hashtagIds, pageable);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package page.clab.api.domain.community.board.adapter.out.persistence;

import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.querydsl.QuerydslPredicateExecutor;
import org.springframework.stereotype.Repository;

@Repository
public interface BoardHashtagRepository extends JpaRepository<BoardHashtagJpaEntity, Long>, BoardHashtagRepositoryCustom, QuerydslPredicateExecutor<BoardHashtagJpaEntity> {

List<BoardHashtagJpaEntity> findAllByBoardId(Long boardId);

@Query(value = "SELECT b.* FROM board_hashtag b WHERE b.board_id = :boardId", nativeQuery = true)
List<BoardHashtagJpaEntity> findAllIncludingDeletedByBoardId(Long boardId);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Query(
    value = "SELECT b.* " +
            "FROM board_hashtag b " +
            "WHERE b.board_id = :boardId", 
    nativeQuery = true
)

위 예시처럼 적절히 개행을 추가해주면 가독성 개선에 도움이 될 것 같아요.

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package page.clab.api.domain.community.board.adapter.out.persistence;

import java.util.List;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;

public interface BoardHashtagRepositoryCustom {
List<Long> getBoardIdsByHashTagId(List<Long> hashtagIds, Pageable pageable);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package page.clab.api.domain.community.board.adapter.out.persistence;

import com.querydsl.jpa.JPQLQuery;
import com.querydsl.jpa.impl.JPAQueryFactory;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Repository;

@Repository
@RequiredArgsConstructor
public class BoardHashtagRepositoryImpl implements BoardHashtagRepositoryCustom {

private final JPAQueryFactory queryFactory;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

기존에 사용하던 QueryDSL을 대신하여 JPQL을 선택하게 된 이유가 궁금해요. 간단히 설명 부탁드려도 될까요?


@Override
public List<Long> getBoardIdsByHashTagId(List<Long> hashtagIds, Pageable pageable) {
QBoardHashtagJpaEntity boardHashtag = QBoardHashtagJpaEntity.boardHashtagJpaEntity;

JPQLQuery<Long> query = queryFactory.selectDistinct(boardHashtag.boardId)
.from(boardHashtag)
.where(boardHashtag.hashtagId.in(hashtagIds)
.and(boardHashtag.isDeleted.eq(false)))
.groupBy(boardHashtag.boardId)
.having(boardHashtag.hashtagId.count().eq((long) hashtagIds.size()));

return query.fetch();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import page.clab.api.domain.community.board.application.dto.request.BoardRequestDto;
import page.clab.api.domain.community.board.application.dto.response.BoardCategoryResponseDto;
import page.clab.api.domain.community.board.application.dto.response.BoardOverviewResponseDto;
import page.clab.api.domain.community.board.application.dto.response.BoardDetailsResponseDto;
import page.clab.api.domain.community.board.application.dto.response.BoardEmojiCountResponseDto;
import page.clab.api.domain.community.board.application.dto.response.BoardHashtagResponseDto;
import page.clab.api.domain.community.board.application.dto.response.BoardListResponseDto;
import page.clab.api.domain.community.board.application.dto.response.BoardMyResponseDto;
import page.clab.api.domain.community.board.application.dto.response.WriterInfo;
Expand Down Expand Up @@ -39,7 +40,7 @@ public Board fromDto(BoardRequestDto requestDto, String memberId, List<UploadedF
.build();
}

public BoardDetailsResponseDto toDto(Board board, MemberDetailedInfoDto memberInfo, boolean isOwner, List<BoardEmojiCountResponseDto> emojiInfos) {
public BoardDetailsResponseDto toDto(Board board, MemberDetailedInfoDto memberInfo, boolean isOwner, List<BoardEmojiCountResponseDto> emojiInfos, List<BoardHashtagResponseDto> boardHashtagInfos) {
WriterInfo writerInfo = createDetail(board, memberInfo);
return BoardDetailsResponseDto.builder()
.id(board.getId())
Expand All @@ -54,17 +55,19 @@ public BoardDetailsResponseDto toDto(Board board, MemberDetailedInfoDto memberIn
.imageUrl(board.getImageUrl())
.isOwner(isOwner)
.emojiInfos(emojiInfos)
.boardHashtagInfos(boardHashtagInfos)
.createdAt(board.getCreatedAt())
.build();
}

public BoardMyResponseDto toDto(Board board, MemberBasicInfoDto memberInfo) {
public BoardMyResponseDto toDto(Board board, MemberBasicInfoDto memberInfo, List<BoardHashtagResponseDto> boardHashtagInfos) {
return BoardMyResponseDto.builder()
.id(board.getId())
.category(board.getCategory().getKey())
.writerName(board.isWantAnonymous() ? board.getNickname() : memberInfo.getMemberName())
.title(board.getTitle())
.imageUrl(board.getImageUrl())
.boardHashtagInfos(boardHashtagInfos)
.createdAt(board.getCreatedAt())
.build();
}
Expand All @@ -78,21 +81,22 @@ public BoardCommentInfoDto toDto(Board board) {
.build();
}

public BoardCategoryResponseDto toCategoryDto(Board board, MemberDetailedInfoDto memberInfo, Long commentCount) {
public BoardOverviewResponseDto toCategoryDto(Board board, MemberDetailedInfoDto memberInfo, Long commentCount, List<BoardHashtagResponseDto> boardHashtagInfos) {
WriterInfo writerInfo = create(board, memberInfo);
return BoardCategoryResponseDto.builder()
return BoardOverviewResponseDto.builder()
.id(board.getId())
.category(board.getCategory().getKey())
.writerId(writerInfo.getId())
.writerName(writerInfo.getName())
.title(board.getTitle())
.commentCount(commentCount)
.imageUrl(board.getImageUrl())
.boardHashtagInfos(boardHashtagInfos)
.createdAt(board.getCreatedAt())
.build();
}

public BoardListResponseDto toListDto(Board board, MemberDetailedInfoDto memberInfo, Long commentCount) {
public BoardListResponseDto toListDto(Board board, MemberDetailedInfoDto memberInfo, Long commentCount, List<BoardHashtagResponseDto> boardHashtagInfos) {
WriterInfo writerInfo = create(board, memberInfo);
return BoardListResponseDto.builder()
.id(board.getId())
Expand All @@ -103,6 +107,7 @@ public BoardListResponseDto toListDto(Board board, MemberDetailedInfoDto memberI
.content(board.getContent())
.commentCount(commentCount)
.imageUrl(board.getImageUrl())
.boardHashtagInfos(boardHashtagInfos)
.createdAt(board.getCreatedAt())
.build();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package page.clab.api.domain.community.board.application.dto.mapper;

import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import page.clab.api.domain.community.board.application.dto.request.BoardHashtagRequestDto;
import page.clab.api.domain.community.board.application.dto.response.BoardHashtagResponseDto;
import page.clab.api.domain.community.board.domain.BoardHashtag;

@Component
@RequiredArgsConstructor
public class BoardHashtagDtoMapper {

public BoardHashtag fromDto(Long boardId, Long hashtagId){
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

){ 사이에 공백이 누락되었어요.

return BoardHashtag.builder()
.boardId(boardId)
.hashtagId(hashtagId)
.isDeleted(false)
.build();
}

public BoardHashtagRequestDto toDto(Long boardId, List<Long> hashtagIdList) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

List<Long> hashtagIdList -> List<Long> hashtagIds
저희 프로젝트에서 리스트를 변수로 표현할 때 뒤에 List를 붙이는 게 아니라 복수형으로 표현하는 것으로 알고있어서 변경 제안 드립니다.

return BoardHashtagRequestDto.builder()
.boardId(boardId)
.hashtagIdList(hashtagIdList)
.build();
}

public BoardHashtagResponseDto toDto(BoardHashtag boardHashtag, String name) {
return BoardHashtagResponseDto.builder()
.id(boardHashtag.getId())
.boardId(boardHashtag.getBoardId())
.name(name)
.hashtagId(boardHashtag.getHashtagId())
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package page.clab.api.domain.community.board.application.dto.request;

import java.util.List;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
@Builder
public class BoardHashtagRequestDto {

private Long boardId;
private List<Long> hashtagIdList;
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,7 @@ public class BoardRequestDto {
@NotNull(message = "{notNull.board.wantAnonymous}")
@Schema(description = "익명 사용 여부", example = "false", required = true)
private boolean wantAnonymous;

@Schema(description = "해시태그 id 리스트", example = "[1, 2]")
private List<Long> hashtagIdList;
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import java.util.List;
import lombok.Getter;
import lombok.Setter;
import page.clab.api.domain.community.board.domain.BoardCategory;
Expand All @@ -25,4 +26,7 @@ public class BoardUpdateRequestDto {
@NotNull(message = "{notNull.board.wantAnonymous}")
@Schema(description = "익명 사용 여부", example = "false")
private boolean wantAnonymous;

@Schema(description = "해시태그 id 리스트", example = "[1, 2]")
private List<Long> hashtagIdList;
}
Loading
Loading