시작하기전에 @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 기반 웹 어플리케이션 개발 > 2부(관심 주제와 지역 정보)' 카테고리의 다른 글
41. 지역 정보 테스트 코드 (0) | 2020.04.23 |
---|---|
39. 지역 도메인 (0) | 2020.04.23 |
38. 관심 주제 테스트 (0) | 2020.04.23 |
37. 관심 주제 자동완성 (0) | 2020.04.22 |
36. 관심 주제 삭제 (0) | 2020.04.22 |