[클론 코딩] 네이버 카페 - (통합게시판) 태그 등록, 수정(삭제)

2024. 6. 4. 21:21Project/Naver Cafe

  네이버 카페 게시글은 작성시에 태그를 입력할 수 있다. 게시글 작성시에 태그를 등록할 수 있으며, 게시글 수정시에 태그를 수정할 수 있다. 태그 수정시에는 태그 등록과 삭제가 동시에 이루어지는 것이 특징이다. 수정 정보와 기존 정보를 비교해 기존에 존재하는 태그는 제외하고 새로 추가된 태그는 등록, 수정 정보에 없는 태그는 삭제하게 될 것이다.


Tag

package CloneCoding.NaverCafe.domain.tag;

@Entity
@Table(name = "TAG")
@Getter
@Builder
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
public class Tag {

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

    @Column(name = "TAG_NAME")
    private String tagName;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "NORMAL_ID")
    private Normal normalId;

    public static List<Tag> createTags(List<String> tags, Normal normal) {
        List<Tag> result = new ArrayList<>();

        for (String t : tags) {
            Tag tag = create(t, normal);
            result.add(tag);
        }

        return result;
    }

    public static Tag create(String tagName, Normal normal) {
        return Tag.builder()
                .tagName(tagName)
                .normalId(normal)
                .build();
    }

}

 

  • create() : Tag 객체를 생성해 반환
  • createTags() : Tag 객체들을 List<Tag>에 담아 해당 리스트 반환

QueryTagRepositoryImpl

package CloneCoding.NaverCafe.domain.tag.repository;

import static CloneCoding.NaverCafe.domain.tag.QTag.tag;

@Repository
@RequiredArgsConstructor
public class QueryTagRepositoryImpl implements QueryTagRepository {

    private final JPAQueryFactory query;

    @Override
    public Tag findByNameAndArticle(String tagName, Normal normal) {
        return Optional.ofNullable(query
                        .selectFrom(tag)
                        .where(tag.tagName.eq(tagName), tag.normalId.eq(normal))
                        .fetchOne())
                .orElseThrow(() -> new NoSuchElementException("태그 정보를 찾을 수 없습니다."));
    }

}

 

  • findByNameAndArticle() : 태그명과 게시판 정보에 해당하는 Tag 엔티티를 조회 후 반환

NormalServiceImpl

package CloneCoding.NaverCafe.domain.article.normal.service;

import static CloneCoding.NaverCafe.message.SystemMessage.WRITE_COMPLETE;

@Service
@RequiredArgsConstructor
public class NormalServiceImpl implements NormalService {

    private final NormalRepository normalRepository;
    private final CafeRepository cafeRepository;
    private final CafeMemberRepository cafeMemberRepository;
    private final IntegrateRepository integrateRepository;
    private final TagRepository tagRepository;
    private final AesUtil aesUtil;

    @Override
    public String createNormal(RequestPostNormal request, String url, String token) {

        CafeMember cafeMember = checkAuth(url, token);

        Normal normal = Normal.create(request, cafeMember);

        List<Tag> tags = createTags(request.getTags(), normal);

        normalRepository.save(normal);
        tagRepository.saveAll(tags);

        return WRITE_COMPLETE.getMessage();

    }

    @Override
    public ResponseNormalForm createUpdateForm(String url, Long id, String token) {

        checkAuth(url, token);

        Normal article = normalRepository.findById(id)
                .orElseThrow(() -> new NoSuchElementException("게시글 정보를 찾을 수 없습니다."));

        Integrate menu = integrateRepository.findById(article.getMenuId())
                .orElseThrow(() -> new NoSuchElementException("통합게시판 정보를 찾을 수 없습니다."));

        return ResponseNormalForm.builder()
                .menu(menu.getName())
                .titleHeader(article.getTitleHeader())
                .title(article.getTitle())
                .body(article.getBody())
                .tags(hasTags(article))
                .notice(article.isNotice())
                .allowComment(article.isAllowComment())
                .build();
    }

    @Override
    public String updateNormal(String url, Long id, RequestPostNormal request, String token) {

        checkAuth(url, token);

        Normal article = normalRepository.findById(id)
                .orElseThrow(() -> new NoSuchElementException("게시글 정보를 찾을 수 없습니다."));

        article.update(request);

        updateTags(request.getTags(), article);
        normalRepository.save(article);

        return WRITE_COMPLETE.getMessage();

    }

    @Override
    @Transactional
    public ResponseReadNormal readNormal(String url, Long id, String token) {

        CafeMember reader = checkAuth(url, token);

        Normal article = normalRepository.findByIdWithLock(id)
                .orElseThrow(() -> new NoSuchElementException("게시글 정보를 찾을 수 없습니다."));

        Integrate menu = integrateRepository.findById(article.getMenuId())
                .orElseThrow(() -> new NoSuchElementException("게시판 정보를 찾을 수 없습니다."));

        CafeMember writer = cafeMemberRepository.findByAccountId(article.getCafeId(), article.getAccountId());

        article.addViewCount();

        // 동시성 문제 테스트를 위해 필요한 딜레이
        try {
            Thread.sleep(5000L);
        } catch (Exception e) {
            throw new RuntimeException("기다리는 중");
        }

        Normal updateNormal = normalRepository.save(article);

        return ResponseReadNormal.builder()
                .menuId(menu.getId())
                .menuName(menu.getName())
                .title("[" + article.getTitleHeader() + "] " + article.getTitle())
                .profileImage(writer.getProfileImage())
                .accountId(article.getAccountId())
                .nickname(article.getNickname())
                .position(changeNameToPosition(writer.getPosition()))
                .createAt(article.getCreateAt())
                .viewCount(updateNormal.getViewCount())
                .commentCount(article.getCommentCount())
                .articleUrl(DEFAULT_ARTICLE_URL.getValue() +
                        "/" + article.getCafeId().getUrl() +
                        "/" + article.getId())
                .body(article.getBody())
                .favoriteCount(article.getFavoriteCount())
                .commentNickname(reader.getNickname())
                .defaultComment(LEAVE_COMMENT.getValue())
                .tags(hasTags(updateNormal))
                .build();
    }

    private CafeMember checkAuth(String url, String token) {
        Cafe cafe = cafeRepository.findByUrl(url);
        String accountId = aesUtil.aesDecode(token);

        return cafeMemberRepository.findByAccountId(cafe, accountId);
    }

    private List<Tag> createTags(List<String> tags, Normal normal) {
        List<Tag> result = new ArrayList<>();

        if (!tags.isEmpty()) {
            result = Tag.createTags(tags, normal);
        }

        return result;
    }

    private void updateTags(List<String> tags, Normal normal) {

        List<String> delTags = hasTags(normal);
        List<String> addTags = new ArrayList<>();

        for (String t : tags) {
            if (delTags.contains(t)) {
                delTags.remove(t);
            } else {
                addTags.add(t);
            }
        }

        if (!addTags.isEmpty()) {
            List<Tag> newTags = createTags(addTags, normal);
            tagRepository.saveAll(newTags);
        }

        if (!delTags.isEmpty()) {
            for (String n : delTags) {
                Tag findTag = tagRepository.findByNameAndArticle(n, normal);
                tagRepository.delete(findTag);
            }
        }

    }

    private List<String> hasTags(Normal normal) {
        List<String> result = new ArrayList<>();

        for (Tag t : normal.getTags()) {
            result.add(t.getTagName());
        }

        return result;
    }

}

 

  • createNormal() : 요청 정보의 태그 정보로 태그 객체 리스트를 생성해 해당 리스트의 모든 객체를 엔티티로 저장하는 로직을 추가
  • createUpdateForm() : 게시글 정보 반환시, 생성하는 응답 객체 생성에 게시글이 갖는 태그명들을 추가
  • updateNormal() : 수정 정보의 태그 정보를 반영하는 로직 추가
  • readNormal() : 응답 객체 생성시, 게시판이 갖는 태그명들 추가
  • createTags() : 입력 받는 문자열 리스트가 비었다면 빈 리스트를 반환, 반대의 경우 리스트의 문자열을 태그명으로 갖는 태그 객체들을 List<Tag> result 에 추가해 반환
  • updateTags() : 요청의 태그정보를 삭제할 태그와 추가 태그로 분리하여 삭제할 태그는 DB에서 삭제하고, 추가 태그는 DB에 저장하며, 기존에 이미 갖고 있는 태그는 그대로 유지하는 메서드 
  • hasTags() : List<Tag>를 List<String>으로 변환하기 위해 작성한 메서드, Tag 객체 갖는 태그명을 담는 문자열 리스트를 만들어 반환

API TEST

API TEST - 게시글 태그 추가

 

DB - 게시글 태그 추가

 

  기존 게시글 생성 API를 요청시 태그 정보를 입력하면 태그 정보가 저장된다. 요청이 완료됬다는 메시지가 정상적으로 출력되고 DB를 확인하면 입력한 태그들의 정보가 DB에 추가 된 것을 확인할 수 있다.

 

API TEST - 게시글 태그 수정

 

DB - 게시글 태그 수정

 

  게시글 수정 API 요청시 태그 정보를 입력하면 기존 게시글의 태그 정보와 입력 정보의 태그 정보를 비교해 태그 엔티티를 수정하게 된다. 기존에 갖고 있는 태그라면 유지, 없는 태그라면 추가, 입력 정보에 없는 기존 태그는 삭제한다. 요청 수행 완료 후, DB를 확인하면 기존 태그가 {"테스트", "태그", "모음"}에서 {"태그", "수정"}으로 바뀐 것을 볼 수 있다. 공통으로 갖는 "태그" 레코드는 이전과 같은 id 값을 가지며 변화가 없고, 입력 정보에 없는 {"테스트", "모음"} 태그 정보는 삭제 됬으며, 기존에 없던 {"수정"}이라는 태그 정보는 추가 된 것을 확인할 수 있다.