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

Merged
merged 27 commits into from
Dec 26, 2024
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
443535b
feat(Hashtag): Hashtag와 BoardHashtag 관련 entity와 domain 객체 설계
mingmingmon Dec 14, 2024
2c83f45
feat(HashtagRegister): 해시태그 등록 api 작성
mingmingmon Dec 14, 2024
6480448
feat(BoardRegister): 게시글 생성 시 개발질문 카테고리인 경우 해시태그 저장 로직 추가
mingmingmon Dec 14, 2024
3f32633
refactor(BoardDtoMapper): 게시판 관련 응답 시 해시태그 정보를 포함하도록 수정
mingmingmon Dec 14, 2024
1ca7590
feat(BoardUpdate): 게시글 수정 시 해시태그의 변경 로직 추가
mingmingmon Dec 14, 2024
6b2a81e
feat(BoardsByHashtagRetrievalController): 해시태그로 게시글 조회하는 API 작성
mingmingmon Dec 14, 2024
4c4a70d
fix(BoardsByHashtagRetrievalService): 잘못된 페이지네이션 수정
mingmingmon Dec 14, 2024
8496bd4
refactor(BoardRegisterController): 개발질문 게시판이 아닌 게시판에 해시태그 등록할 경우 예외발생…
mingmingmon Dec 14, 2024
c85f96b
fix(BoardRegisterService, BoardUpdateService): 개발질문 카테고리가 아닌 경우 해시태그 …
mingmingmon Dec 15, 2024
1527242
refactor(BoardHashtagPersistenceAdapter): repository 변수명 앞에 도메인명 삭제
mingmingmon Dec 22, 2024
0a53dd5
refactor(BoardHashtagRepository): SQL문에 개행추가, customRepository에서 JPQL…
mingmingmon Dec 22, 2024
991b9a1
refactor(ExternalBoardHashtag): external 패키지 아래에 위치했던 boardHashtag 관련…
mingmingmon Dec 22, 2024
32fe799
refactor(BoardRegisterService): 게시글 등록 시 해시태그를 표현할 수 있는 카테고리인지 검증하는 로…
mingmingmon Dec 22, 2024
8198678
refactor(BoardHashtagRegisterService): 존재하는 hashtag인지 판단하는 로직 메소드로 표현
mingmingmon Dec 22, 2024
2538b6a
refactor(BoardUpdateService): 복잡한 로직 메소드 추출 및 board update API에서 해시태그…
mingmingmon Dec 22, 2024
b01237d
refactor: 코드 컨벤션 준수하도록 수정
mingmingmon Dec 22, 2024
35f0975
refactor(HashtagRegisterController): 해시태그 등록 권한 수정, 응답 형식 수정 및 도메인 제약…
mingmingmon Dec 22, 2024
7fd88ae
refactor(BoardUpdateService): 사용하지 않는 로깅 코드 삭제
mingmingmon Dec 22, 2024
56b7c5e
feat(HashtagRetrievalController): 존재하는 해시태그 정보 조회하는 API 작성
mingmingmon Dec 22, 2024
b0103cf
refactor(BoardUpdateService): boardHashtag의 변화에 따른 hashtag의 boardUsag…
mingmingmon Dec 22, 2024
d92d201
refactor(HashtagPersistenceAdapter): 이름으로 해시태그를 찾을 때 없는 경우 notfound 예…
mingmingmon Dec 22, 2024
c682599
refactor(HashtagRetrievlaController): 권한 수정
mingmingmon Dec 22, 2024
7d925d3
refactor: 통일된 import문 정렬과 코드 정리 방식을 활용한 정리
mingmingmon Dec 22, 2024
df6ac3a
Merge branch 'develop' into feat/#609
mingmingmon Dec 22, 2024
be6d91d
refactor(HotBoardRetrievalService): 핫 게시글 응답에 HashtagInfo 필드 추가
mingmingmon Dec 22, 2024
fb49e64
refactor(BoardRegisterService): 중복된 해시태그 등록 가능 검사 삭제 및 코드컨벤션 준수
mingmingmon Dec 24, 2024
c33240f
refactor(BoardHashtagRepository): group by와 distinct 동시 사용 대신 group b…
mingmingmon Dec 24, 2024
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;
mingmingmon marked this conversation as resolved.
Show resolved Hide resolved
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(
mingmingmon marked this conversation as resolved.
Show resolved Hide resolved
@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;
limehee marked this conversation as resolved.
Show resolved Hide resolved
@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);
mingmingmon marked this conversation as resolved.
Show resolved Hide resolved
}
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;
limehee marked this conversation as resolved.
Show resolved Hide resolved

@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){
mingmingmon marked this conversation as resolved.
Show resolved Hide resolved
return BoardHashtag.builder()
.boardId(boardId)
.hashtagId(hashtagId)
.isDeleted(false)
.build();
}

public BoardHashtagRequestDto toDto(Long boardId, List<Long> hashtagIdList) {
mingmingmon marked this conversation as resolved.
Show resolved Hide resolved
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