minlog
article thumbnail

 

해시 태그 로직 구현하기

 

함께 작업하려는 인원을 해시태그로 입력 받아 공유된 회원은 함께 작업 할 수 있는 로직을 구현하려고 합니다.

우선 앤티티 객체는 필요한 게시판과 태그 그리고 두개의 테이블을 연결 시켜주는 해시 태그 객체를 생성했습니다.

 

 

1. 서버 구성

 

1-1. Entity

📑 Entity  : 게시판(Category), 해시태그 (Hashtag) , 태그 (Tag)

더보기

📑Category.java

package com.example.travel.domain;


import lombok.*;
import org.springframework.data.jpa.repository.Modifying;

import javax.persistence.*;
import java.time.LocalDateTime;
import java.util.List;

@NoArgsConstructor
@AllArgsConstructor
@ToString(exclude = "items")
@Getter
@Builder
@Entity
public class Category extends BaseEntity{
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long categoryNo;
    private Long userTravelNo;  //작성자 No
    private String categoryName; // 카테고리 이름
    private LocalDateTime dateStart;
    private LocalDateTime dateEnd;
    private String categoryArea; // 시/도
    private String categoryAreaDetails; // 군구
    private boolean categorySave; // 임시저장 여부  최종 저장 전에는 false -> true
    private boolean categoryOpen; // 카테고리 외부 공개 여부





}

 

📑Hashtag.java

package com.example.travel.domain;

import lombok.*;

import javax.persistence.*;
import javax.validation.constraints.NotNull;
import java.util.List;

@NoArgsConstructor
@AllArgsConstructor
@ToString(exclude = {"tag"})
@Getter
@Builder
@Entity
public class Hashtag {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long hashId;
    @NotNull
    private Long categoryId;
    @NotNull
    @OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true)
    private List<Tag> tag;

}

 

📑Tag.java

package com.example.travel.domain;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

import javax.persistence.*;

@NoArgsConstructor
@AllArgsConstructor
@Getter
@Builder
@Entity
public class Tag {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "tag_id")
    private Long id;
    private String name;
}

 

 

1-2. Repository

각 앤티티의 저장소 레파지토리를 생성하였습니다.

위와 같이 앤티티를 생성하면 실제 테이블은 4개가 생성이 되는데, hashtag_tag 테이블이 하나가 더 생성됩니다.

테이블을 살펴보면 hashtag 에서는 hesh_id 와 category_id를 받고 있고,

자동으로 생성된 hashtag_tag 테이블을 살펴보면 hashtag_hash_id (hesh_id) 와 tag_tag_id(tag_id)를  저장하고 있습니다.

 

Table : category, hashtag, hashtag_tag, tag

더보기

📑CategoryRepository

package com.example.travel.repository.travel;

import com.example.travel.domain.Category;
import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

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

public interface CategoryRepository extends JpaRepository<Category,Long> {


    @Query(value = "select c from Category c where c.userTravelNo=:no and c.categorySave = false ORDER BY c.createdAt")
    List<Category> getCategoryTemList(@Param(value = "no") Long no);


}

 

📑HashtagRepository

package com.example.travel.repository.travel;

import com.example.travel.domain.Hashtag;
import com.example.travel.domain.Tag;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;

import java.util.List;


@Repository
public interface HashtagRepository extends JpaRepository<Hashtag,Long> {

    @Modifying
    @Query(value = "delete from hashtag_tag h where h.tag_tag_id = :id",nativeQuery = true)
    int deleteByHashIdAndTag(@Param(value="id") Long id);

}

 

📑TagRepository.java

package com.example.travel.repository.travel;

import com.example.travel.domain.Tag;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface TagRepository extends JpaRepository<Tag,Long> {
}

 

 

1-3. Service

게시판을 생성할때 카테고리, 해시태그, 태그 객체 3개를 저장하게되는데, 제거 시에도 함께 delete 시켜주어야 합니다.

이때 주의 할점은 자동으로 생성되었던 hashtag_tag 에서 tag를 제거 해주어야한다는 것입니다.

위에 레파지토리에서 delete 메서드(deleteByHashIdAndTag)를 생성해두어서 해당 메서드를 통해 제거 시켜주었습니다. 

더보기

📑CategoryService.java

package com.example.travel.service.travel;

import com.example.travel.domain.Category;
import com.example.travel.domain.Tag;
import com.example.travel.dto.travel.CategoryDTO;

import java.text.SimpleDateFormat;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;


public interface CategoryService {
    DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS");

    List<CategoryDTO> getCategoryTemList(Long no);

    CategoryDTO categorySave(CategoryDTO categoryDTO);

    boolean categoryDelete(Long no);
    int categoryDays(String start, String end);



    default CategoryDTO categoryEntityToDto(Category category){
        // 문자열
        String startDate = category.getDateStart().format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));
        String endDate = category.getDateEnd().format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));

        CategoryDTO result = CategoryDTO.builder()
                .userTravelNo(category.getUserTravelNo())
                .categoryNo(category.getCategoryNo())
                .categoryName(category.getCategoryName())
                .dateStart(startDate)
                .dateEnd(endDate)
                .categoryArea(category.getCategoryArea())
                .categoryAreaDetails(category.getCategoryAreaDetails())
                .categorySave(category.isCategorySave())
                .categoryOpen(category.isCategoryOpen())
                .build();

        return result;
    }
    default Category categoryDtoToEntity(CategoryDTO categoryDTO){
        // 문자열
        String start = categoryDTO.getDateStart() + " 00:00:00.000";
        String end = categoryDTO.getDateEnd() + " 00:00:00.000";
        LocalDateTime startDate = LocalDateTime.parse(start, formatter);
        LocalDateTime endDate = LocalDateTime.parse(end, formatter);

        Category result = Category.builder()
                .userTravelNo(categoryDTO.getUserTravelNo())
                .categoryNo(categoryDTO.getCategoryNo())
                .categoryName(categoryDTO.getCategoryName())
                .dateStart(startDate)
                .dateEnd(endDate)
                .categoryArea(categoryDTO.getCategoryArea())
                .categoryAreaDetails(categoryDTO.getCategoryAreaDetails())
                .categorySave(categoryDTO.isCategorySave())
                .categoryOpen(categoryDTO.isCategoryOpen())
                .build();
        return result;
    }


}

 

📑CategoryServiceImpl.java

package com.example.travel.service.travel;

import com.example.travel.domain.Category;
import com.example.travel.domain.Hashtag;
import com.example.travel.domain.Tag;
import com.example.travel.dto.travel.CategoryDTO;
import com.example.travel.repository.travel.CategoryRepository;
import com.example.travel.repository.travel.HashtagRepository;
import com.example.travel.repository.travel.TagRepository;
import groovyjarjarpicocli.CommandLine;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.Period;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

@Log4j2
@RequiredArgsConstructor
@Service
public class CategoryServiceImpl implements CategoryService {
    final CategoryRepository categoryRepository;
    final HashtagRepository hashtagRepository;
    final TagRepository tagRepository;


    @Override
    public List<CategoryDTO> getCategoryTemList(Long no) {
        log.info("임시 저장된 내용 전달");
        List<CategoryDTO> result = new ArrayList<>();
        try{
            List<Category> userTravelNo = categoryRepository.getCategoryTemList(no);
            if (userTravelNo.isEmpty()){
                log.info("없으면 null");
                return result;
            }
            log.info("있으면 리스트 전달");
            log.info(userTravelNo);
            result = userTravelNo.stream().map(item -> categoryEntityToDto(item)).collect(Collectors.toList());            return result;
        }catch (Exception e){
            e.printStackTrace();
        }

        return result;
    }

    @Transactional
    @Override
    public CategoryDTO categorySave(CategoryDTO categoryDTO) {
        log.info("카테고리 임시저장 로직-----------");
        Long userTravelNo = categoryDTO.getUserTravelNo();
        log.info(userTravelNo);
        // 1. 카테고리저장
        // 2. 태그 저장
        // 3. 해쉬태그 저장

        List<CategoryDTO> categoryTemList = getCategoryTemList(categoryDTO.getUserTravelNo());
        int size = categoryTemList.size();
        if (size > 4){
            log.info("카테고리 임시 저장 리스트 5개 이상 있으면");
            return null;
        }
        categoryDTO.setCategorySave(false); //초기 저장 시 임시저장
        Category category = categoryDtoToEntity(categoryDTO);
        log.info("저장된 category : {}",category);

        Category result = categoryRepository.save(category);  // 1. 카테고리저장

        //2. 태그저장
        if (!categoryDTO.getTags().isEmpty()){
            log.info("tag가 있으면 저장");
            List<String> tags = categoryDTO.getTags();
            List<Tag> tagList = tags.stream().map(i -> Tag.builder().name(i).build()).collect(Collectors.toList());

            for(int i=0; i < tagList.size(); i++){
                tagRepository.save(tagList.get(i)); // 2. 태그 저장
            }
            Hashtag hashtag = Hashtag.builder()
                    .categoryId(result.getCategoryNo())
                    .tag(tagList)
                    .build();
            hashtagRepository.save(hashtag); // 3. 해쉬태그 저장
        }



        CategoryDTO dto = categoryEntityToDto(result);
        return dto;
    }

    @Transactional
    @Override
    public boolean categoryDelete(Long no) {
        log.info("카테고리 제거 로직 ---------------");

        Optional<Category> categorys = categoryRepository.findById(no);
        Category category = categorys.get();
        categoryRepository.delete(category); //3. 카테고리제거

        // 1. 태그 제거
        // 2. 해쉬태그 제거
        // 3. 카테고리 제거
        log.info(no); // 카테고리 번호
        Optional<Hashtag> hashtag = hashtagRepository.findById(no);
        log.info("hashtag : {}",hashtag);

        if (hashtag.isPresent()){
            log.info("카테고리 연관 해시태그, 태그 제거 로직 -------------");
            Hashtag hashtagResult = hashtag.get();
            Long categoryId = hashtagResult.getCategoryId(); // 카테고리 번호

            List<Tag> tag = hashtagResult.getTag();
            if (!tag.isEmpty()){ // 리스트가 존재하면 제거 
                log.info("리스트 존재");
                for(int i=0;i<tag.size();i++){
                    Tag tagItem = tag.get(i);
                    int result = hashtagRepository.deleteByHashIdAndTag(tagItem.getId()); // 해쉬태그안에 tag
                    System.out.println(result);
                    tagRepository.delete(tagItem); // 1. tag 제거
                }
                hashtagRepository.delete(hashtagResult); // 2. 해쉬태그 제거
            } else {
                hashtagRepository.delete(hashtagResult); // 3. 해쉬태그 제거
            }
            return true;
        } // 해쉬태그, 태그 제거 end


        log.info("카테고리가 없으면 false 반환");
        return false;
    }

    @Override
    public int categoryDays(String start, String end) {
        log.info("D-day 계산");

        // 문자열
        start = start + " 00:00:00.000";
        end = end + " 00:00:00.000";

        LocalDateTime startDate = LocalDateTime.parse(start, formatter);
        LocalDateTime endDate = LocalDateTime.parse(end, formatter);

        log.info("------------------");
        Period period = Period.between(LocalDate.from(startDate), LocalDate.from(endDate)); // 비교
        int days = period.getDays();
        log.info(days);
        days = days + 1;

        log.info("days : {}",days);
        return days;
    }
}

 

 


 

 

2. 해시태그 리스트 ( 회원 아이디 ) 정보 전달 로직

 

처음 게시판을 생성할 때 화면에서 해시태그 리스트를 Ajax를 활용해  비동기식으로 전달받아와 사용하고 있습니다.

get 방식으로 데이터를 가져와 whielist(해시 태그 리스트) 에 값을 전달하고 있습니다.

 

2-1. 게시판 화면  :  Ajax  해시태그 리스트

 // 회원 아이디 리스트
$(document).ready(function (){
    $.ajax({
        type : 'get',
        url : '/user/tag',
        success : function (data) {
            whitelist = data;
        }
    });
});

 

2-2. 전송을 요청 받은 서버

 

- Controller

서비스 로직에서 List클래스로 값을 받아오고 있습니다.

📑UserApi.java

@GetMapping("/tag")
public List<String> tagList(){
    log.info("회원 아이디 리스트 전달");
    List<String> tagList = userService.userList();
    return tagList;
}

 

- Service

📑UserServiceImpl.java

@Override
public List<String> userList() {
    List<String> userTravelList = userRepository.getUserTravelList();
    return userTravelList;
}

 

- Repository

📑 UserRepository.java

//전체 회원 아이디 리스트
@Query(value = "select m.user_id from user_travel m", nativeQuery = true)
List<String> getUserTravelList();

 

2-3. 정보를 전달 받은 프론트 : Tagify.js 

비동기 방식으로 정보를 다시 전달 받은 화면에서는 해당 정보를

Tagify.js 라이브러리를 사용해서 태그를 입력시 뿌려주고 선택, 취소 가능한 기능을 보여주고 있습니다.

 

See the Pen Tagify - Text Input Example by Yair Even Or (@vsync) on CodePen.

 

Tagify - Tags input Component

In this example, the field is pre-ocupied with 3 tags, and last tag is not included in the whitelist, and will be removed because the enforceWhitelist setting flag is set to true 🚩 This example is very interesting because it shows another layer of compl

yaireo.github.io

 

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
    layout:decorate="~{layout/basic}">
<th:block layout:fragment="content">
    <div class="content_nav"  th:object="${category}">
        <!-- start : navbar -->
        <th:block th:replace="layout/navBarTravel::navBarTravel"></th:block>
        <div class="content">
            <h2 class="tit text-center mb-4">
                나만의 여행을 만들어보세요.
            </h2>
            <form id="categoryForm" th:action="@{/travel/category}"  th:method="post" class="box travel_add_box content_box">
                <input th:field="*{userTravelNo}" th:id="*{userTravelNo}" th:name="*{userTravelNo}" th:value="*{userTravelNo}" type="hidden">
                <table>
                    <colgroup>
                        <col style="width:100px">
                        <col style="width:calc(100% - 100px)">
                    </colgroup>
                    <tbody>
                        <tr>
                            <th>여행명 <span class="txt_info ck">*</span></th>
                            <td><input th:field="*{categoryName}" th:id="*{categoryName}" th:name="*{categoryName}" class="form-control" type="text" placeholder="가평 가족여행"></td>
                        </tr>
                        <tr>
                            <th>기간 <span class="txt_info ck">*</span></th>
                            <td>
                                <div class="input-group input-daterange">
                                    <input th:field="*{dateStart}" th:id="*{dateStart}" th:name="*{dateStart}" type="text" class="form-control" placeholder="출발 일" readonly>
                                    <input th:field="*{dateEnd}" th:id="*{dateEnd}" th:name="*{dateEnd}" type="text" class="form-control" placeholder="종료 일" readonly>
                                </div>
                            </td>
                        </tr>
                        <tr>
                            <th>지역 <span class="txt_info ck">*</span></th>
                            <td>
                                ...
                            </td>
                        </tr>
                        <tr>
                            <th>함께</th>
                            <td>
                                <!-- 해시 태그 정보를 저장할 input 태그. (textarea도 지원) -->
                                <input th:field="*{tags} "th:id="*{tags}" th:name="*{tags}" class='some_class_name form-control ' data-blacklist='.NET,PHP'  type="text"  placeholder="아이디를 입력하세요" />
                            </td>
                        </tr>
                    </tbody>
                </table>
                <div class="form-check form-switch mt-4 pt-4 border-top">
                    ...
                </div>
                ...
                <button id="btnCategorySave" type="button"  class="w-100 btn btn-primary mt-4">일정 만들기</button>
             </form>

        </div>

    </div>

    <!-- Modal -->
		...
    <!-- end :Modal -->


    <!-- 태그 관련 소스 다운 -->
    <script src="https://unpkg.com/@yaireo/tagify"></script>
    <!-- 폴리필 (구버젼 브라우저 지원) -->
    <script src="https://unpkg.com/@yaireo/tagify/dist/tagify.polyfills.min.js"></script>
    <link href="https://unpkg.com/@yaireo/tagify/dist/tagify.css" rel="stylesheet" type="text/css" />

    <!-- 해쉬 태그 -->
    <script type="text/javascript">

        let inputElm = document.querySelector('input[name=tags]')
        let whitelist = [];

        // 회원 아이디 리스트
        $(document).ready(function (){
            $.ajax({
                type : 'get',
                url : '/user/tag',
                success : function (data) {
                    whitelist = data;
                }
            });
        });




        // initialize Tagify on the above input node reference
        let tagify = new Tagify(inputElm, {
            enforceWhitelist: true,

            // make an array from the initial input value
            whitelist: inputElm.value.trim().split(/\s*,\s*/),
            maxTags: 10, // 최대 허용 태그 갯수
            dropdown: {
                maxItems: 20,           // 드롭다운 메뉴에서 몇개 정도 항목을 보여줄지
                classname: "tags-look", // 드롭다운 메뉴 엘리먼트 클래스 이름. 이걸로 css 선택자로 쓰면 된다.
                enabled: 1,             // 단어 몇글자 입력했을떄 추천 드롭다운 메뉴가 나타날지
                closeOnSelect: false    // 드롭다운 메뉴에서 태그 선택하면 자동으로 꺼지는지 안꺼지는지
            }
        })

        // Chainable event listeners
        tagify.on('add', onAddTag) //태그 추가 될때
            .on('remove', onRemoveTag) //태그가 제거될때
            .on('input', onInput) //태그가 입력되고 있을경우
            .on('edit', onTagEdit) // 입력된 태그 수정
            .on('invalid', onInvalidTag) // 허용되지 않는 태그일경우
            .on('click', onTagClick) // 해시태그 클릭
            .on('focus', onTagifyFocusBlur) // 포커스 될경우
            .on('blur', onTagifyFocusBlur) // 포커스 잃을 경우
            .on('dropdown:hide dropdown:show', e => console.log(e.type))// 드롭다운 메뉴가 사라질경우
            .on('dropdown:select', onDropdownSelect) // 드롭다운 메뉴에서 아이템을 선택할 경우

        let mockAjax = (function mockAjax(){
            var timeout;
            return function(duration){
                clearTimeout(timeout); // abort last request
                return new Promise(function(resolve, reject){
                    timeout = setTimeout(resolve, duration || 700, whitelist)
                })
            }
        })()

        // tag added callback
        function onAddTag(e){
            console.log("onAddTag: ", e.detail);
            console.log("original input value: ", inputElm.value)
            tagify.off('add', onAddTag) // exmaple of removing a custom Tagify event
        }

        // tag remvoed callback
        function onRemoveTag(e){
            console.log("onRemoveTag:", e.detail, "tagify instance value:", tagify.value)
        }



        // on character(s) added/removed (user is typing/deleting)
        function onInput(e){

            console.log("onInput: ", e.detail);
            console.log("onInput value: ", e.detail.value);




            tagify.settings.whitelist.length = 0; // reset current whitelist
            tagify.loading(true).dropdown.hide.call(tagify) // show the loader animation



            // get new whitelist from a delayed mocked request (Promise)
            mockAjax()
                .then(function(result){
                    // replace tagify "whitelist" array values with new values
                    // and add back the ones already choses as Tags
                    tagify.settings.whitelist.push(...result, ...tagify.value)
                    // render the suggestions dropdown.
                    tagify.loading(false).dropdown.show.call(tagify, e.detail.value);
                })
        }

        function onTagEdit(e){
            console.log("onTagEdit: ", e.detail);
        }

        // invalid tag added callback
        function onInvalidTag(e){
            console.log("onInvalidTag: ", e.detail);
        }

        // invalid tag added callback
        function onTagClick(e){
            console.log(e.detail);
            console.log("onTagClick: ", e.detail);
        }

        function onTagifyFocusBlur(e){
            console.log(e.type, "event fired")
        }

        function onDropdownSelect(e){
            console.log("onDropdownSelect: ", e.detail)
        }
    </script>


    <!-- 폼저장 -->
    <script type="text/javascript">
       ...
    </script>

</th:block>
</html>

 

profile

minlog

@jimin-log

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!