본문 바로가기
스프링과 JPA 기반 웹 어플리케이션 개발/2부(관심 주제와 지역 정보)

40. 지역 정보 추가 삭제

by Backchus 2020. 4. 23.

시작하기전에 @CurrentUser인터페이스를 @CurrentAccount로 리팩토링

 

Account엔티티와 Zone의 연관관계 맵핑

package me.weekbelt.studyolle.domain;

@Builder @AllArgsConstructor @NoArgsConstructor
@Getter @Setter @EqualsAndHashCode(of = "id")
@Entity
public class Account {

    // ........
    
    @ManyToMany
    private Set<Zone> zones = new HashSet<>();

    // ........
}

 

Zone엔티티에 도시 정보를 지정된 문자열 형식으로 보여주기 위해 toString() 오버라이딩

package me.weekbelt.studyolle.domain;

@Builder @AllArgsConstructor @NoArgsConstructor
@Getter @Setter @EqualsAndHashCode(of = "id")
@Entity
public class Zone {

    // ........

    @Override
    public String toString() {
        return String.format("%s(%s)/%s", city, localNameOfCity, province);
    }
}

 

지역 정보 추가하는 페이지를 요청하는 핸들러 작성

package me.weekbelt.studyolle.settings;

@RequiredArgsConstructor
@Controller
public class SettingsController {

    // ........
    
    private final ZoneRepository zoneRepository;

    // ........
    
    @GetMapping("/settings/zones")
    public String updateZonesForm(@CurrentAccount Account account, Model model)
            throws JsonProcessingException {
        model.addAttribute(account);

        Set<Zone> zones = accountService.getZones(account);
        model.addAttribute("zones", zones.stream().map(Zone::toString)
                .collect(Collectors.toList()));

        List<String> allZones = zoneRepository.findAll().stream().map(Zone::toString)
                .collect(Collectors.toList());
        model.addAttribute("whitelist", objectMapper.writeValueAsString(allZones));

        return "settings/zones";
    }
}

 

미리 저장된 지역정보들을 불러오는 getZones메소드 구현

package me.weekbelt.studyolle.account;

@Transactional
@RequiredArgsConstructor
@Service
public class AccountService implements UserDetailsService {

    // .........

    public Set<Zone> getZones(Account account) {
        Optional<Account> byId = accountRepository.findById(account.getId());
        return byId.orElseThrow().getZones();
    }
}

 

 

지역 정보 추가페이지인 settings/zones.html 작성과 csrf처리를 위한 스크립트를 fragments.html에 추가

fragments.html

<!DOCTYPE html>
<html lang="en"
      xmlns:th="http://www.thymeleaf.org"
      xmlns:sec="http://www.thymeleaf.org/extras/spring-security">

<head th:fragment="head">

// ...................

<script type="application/javascript" th:inline="javascript" th:fragment="ajax-csrf-header">
    $(function () {
        const csrfToken = /*[[${_csrf.token}]]*/ null;
        const csrfHeader = /*[[${_csrf.headerName}]]*/ null;
        $(document).ajaxSend(function (e, xhr, options) {
            xhr.setRequestHeader(csrfHeader, csrfToken);
        })
    });
</script>
</html>

 

settings/zoens.html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<!--head-->
<head th:replace="fragments.html::head"></head>
<body class="bg-light">
<!--네비게이션 바-->
<div th:replace="fragments.html::main-nav"></div>

<div class="container">
    <div class="row mt-5 justify-content-center">
        <div class="col-2">
            <div th:replace="fragments.html :: settings-menu(currentMenu='zones')"></div>
        </div>
        <div class="col-8">
            <div class="row">
                <h2 class="col-sm-12">주요 활동 지역</h2>
            </div>
            <div class="row">
                <div class="col-12">
                    <div class="alert alert-info" role="alert">
                        주로 스터디를 다닐 수 있는 지역을 등록하세요. 해당 지역에 스터디가 생기면 알림을 받을 수 있습니다.<br/>
                        시스템에 등록된 지역만 선택할 수 있습니다.
                    </div>
                    <div id="whitelist" th:text="${whitelist}" hidden></div>
                    <input id="tags" type="text" name="tags" th:value="${#strings.listJoin(zones, ',')}"
                           class="tagify-outside" aria-describedby="tagHelp"/>
                </div>
            </div>
        </div>
    </div>
    <!-- footer -->
    <div th:replace="fragments.html::footer"></div>
</div>
<script th:replace="fragments.html :: ajax-csrf-header"></script>
<script src="/node_modules/@yaireo/tagify/dist/tagify.min.js"></script>
<script type="application/javascript">
    $(function () {
        function tagRequest(url, zoneName) {
            $.ajax({
                dataType: "json",
                autocomplete: {
                    enabled: true,
                    rightKey: true,
                },
                contentType: "application/json; charset=utf-8",
                method: "POST",
                url: "/settings/zones" + url,
                data: JSON.stringify({'zoneName': zoneName})
            }).done(function (data, status) {
                console.log("${data} and status is ${status}");
            });
        }

        function onAdd(e) {
            tagRequest("/add", e.detail.data.value);
        }

        function onRemove(e) {
            tagRequest("/remove", e.detail.data.value);
        }

        const tagInput = document.querySelector("#tags");
        const tagify = new Tagify(tagInput, {
            pattern: /^.{0,20}$/,
            whitelist: JSON.parse(document.querySelector("#whitelist").textContent),
            dropdown: {
                enabled: 1,      // suggest tags after a single character input
            }  // map tags
        });

        tagify.on("add", onAdd);
        tagify.on("remove", onRemove);

        // add a class to Tagify's input element
        tagify.DOM.input.classList.add('form-control');
        // re-place Tagify's input element outside of the element (tagify.DOM.scope), just before it
        tagify.DOM.scope.parentNode.insertBefore(tagify.DOM.input, tagify.DOM.scope);
    });
</script>
</body>
</html>

 

이전에 작성한 settings/tags도 fragments.html의 ajax-csrf-header로 참조

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<!--head-->
<head th:replace="fragments.html::head"></head>
<body class="bg-light">
<!--네비게이션 바-->
<div th:replace="fragments.html::main-nav"></div>

<div class="container">
    
    // ...........
    
    <!-- footer -->
    <div th:replace="fragments.html::footer"></div>
</div>
<script th:replace="fragments.html :: ajax-csrf-header"></script>    // 추가

// ..........

</body>
</html>

 

 

지역 정보를 전달할 ZoneForm DTO 생성

package me.weekbelt.studyolle.settings.form;

@Data
public class ZoneForm {

    // Seoul(서울)/None
    private String zoneName;

    public String getCityName() {
        return zoneName.substring(0, zoneName.indexOf("("));
    }

    public String getProvinceName() {
        return zoneName.substring(zoneName.indexOf("/") + 1);
    }

    public String getLocalNameOfCity() {
        return zoneName.substring(zoneName.indexOf("(") + 1, zoneName.indexOf(")"));
    }
    
    public Zone getZone() {
        return Zone.builder()
                .city(this.getCityName())
                .localNameOfCity(this.getLocalNameOfCity())
                .province(this.getProvinceName())
                .build();
    }

}

 

ZoneRepository에 City와 Province에 따른 Zone 엔티티를 호출하기 위한 findByCityAndProvince메소드 추가

package me.weekbelt.studyolle.zone;

public interface ZoneRepository extends JpaRepository<Zone, Long> {
    Zone findByCityAndProvince(String city, String province);
}

 

지역 정보 추가, 삭제를 처리하는 메소드 추가

package me.weekbelt.studyolle.account;

@Transactional
@RequiredArgsConstructor
@Service
public class AccountService implements UserDetailsService {

    // .............

    public void addZone(Account account, Zone zone) {
        Optional<Account> byId = accountRepository.findById(account.getId());
        byId.ifPresent(a -> a.getZones().add(zone));
    }

    public void removeZone(Account account, Zone zone) {
        Optional<Account> byId = accountRepository.findById(account.getId());
        byId.ifPresent(a -> a.getZones().remove(zone));
    }
}

 

지역 정보 추가, 삭제 핸들러를 추가

package me.weekbelt.studyolle.settings;

@RequiredArgsConstructor
@Controller
public class SettingsController {

    // ........
    
    private final ZoneRepository zoneRepository;

    // ........
   
    @ResponseBody
    @PostMapping("/settings/zones/add")
    public ResponseEntity<?> addZone(@CurrentAccount Account account,
                                     @RequestBody ZoneForm zoneForm) {
        Zone zone = zoneRepository.findByCityAndProvince(zoneForm.getCityName(),
                zoneForm.getProvinceName());
        if (zone == null) {
            return ResponseEntity.badRequest().build();
        }
        
        accountService.addZone(account, zone);
        return ResponseEntity.ok().build();
    }
    
    @PostMapping("/settings/zones/remove")
    public ResponseEntity<?> removeZone(@CurrentAccount Account account,
                                        @RequestBody ZoneForm zoneForm) {
        Zone zone = zoneRepository.findByCityAndProvince(zoneForm.getCityName(), 
                zoneForm.getProvinceName());
        if(zone == null) {
            return ResponseEntity.badRequest().build();
        }
        
        accountService.removeZone(account, zone);
        return ResponseEntity.ok().build();
    }
}

 

 

 

참고: https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-JPA-%EC%9B%B9%EC%95%B1#

 

스프링과 JPA 기반 웹 애플리케이션 개발 - 인프런

이 강좌에서 여러분은 실제로 운영 중인 서비스를 스프링, JPA 그리고 타임리프를 비롯한 여러 자바 기반의 여러 오픈 소스 기술을 사용하여 웹 애플리케이션을 개발하는 과정을 학습할 수 있습니다. 이 강좌를 충분히 학습한다면 여러분 만의 웹 서비스를 만들거나 취직에 도움이 될만한 포트폴리오를 만들 수 있을 겁니다. 활용 웹 개발 프레임워크 및 라이브러리 Java Spring Spring Boot Spring Data JPA Thymeleaf 온라인 강의 스

www.inflearn.com