FureverHomes 프로젝트 - 동물 정보 조회
검색 화면을 처음 들어갔을 때 회원 정보를 등록일 순으로 조회해서 보여주고, 클릭해서 들어가면 상세 정보가 들어가있는 페이지까지 구현을 해보았다.
AnimalResDTO
화면에 정보를 띄워줄 ResponseDTO를 먼저 만들어준다.
@Getter
public class AnimalResDTO {
private Long id; //아이디
private String name; //이름
private Region region; //보호지역
private Sex sex; //성별
private int age; //나이
private String picture; //사진
private Boolean neuter; //중성화여부
private String health_condition; //건강상태
private String shelter_name; //보호소 이름
private String shelter_tel; //보호소 번호
private LocalDate regiDate; //등록날짜
private String personality; //성격
public AnimalResDTO(Long id, String name, Region region, Sex sex, int age, String picture, Boolean neuter, String health_condition, String shelter_name, String shelter_tel, LocalDate regiDate, String personality) {
this.id = id;
this.name = name;
this.region = region;
this.sex = sex;
this.age = age;
this.picture = picture;
this.neuter = neuter;
this.health_condition = health_condition;
this.shelter_name = shelter_name;
this.shelter_tel = shelter_tel;
this.regiDate = regiDate;
this.personality = personality;
}
}
AnimalService
그다음 Service를 생성해준다. 검색창에 뿌려줄 정보는 하나의 동물 정보가 아니라 여러 개의 동물 정보이므로 List로 값을 받아온다.
@Service
@RequiredArgsConstructor
public class AnimalSearchService {
private final AnimalRepository animalRepository;
//등록날짜 기준으로 전체 동물 리스트 조회
public String selectListByRegiDate() {
List<Animal> animals = animalRepository.findAll(Sort.by(Sort.Direction.DESC, "regiDate"));
List<AnimalResDTO> animalResDTO = animals.stream() //리스트 animals를 스트림으로 변환해서
.map(this::converToResDTO) //각 요소에 대해 converToResDTO 메소드를 적용해 (엔티티 -> DTO)
.collect(Collectors.toList()); //변환된 DTO를 다시 리스트로 수집
JSONArray jsonArray = new JSONArray();
JSONObject jsonObject = new JSONObject();
for (AnimalResDTO dto : animalResDTO) {
JSONObject object = new JSONObject();
object.put("id", dto.getId());
object.put("name", dto.getName());
object.put("picture", dto.getPicture());
object.put("region", dto.getRegion().toString());
object.put("sex", dto.getSex().toString());
object.put("age", dto.getAge());
jsonArray.add(object);
}
jsonObject.put("animal_list", jsonArray);
return jsonObject.toJSONString();
}
//resDTO 변환
private AnimalResDTO converToResDTO(final Animal animal) {
AnimalResDTO animalDTO =
new AnimalResDTO(animal.getId(), animal.getName(), animal.getRegion(), animal.getSex(), animal.getAge(), animal.getPicture(),animal.getNeuter(),
animal.getHealth_condition(), animal.getShelter_name(), animal.getShelter_tel(), animal.getRegiDate(), animal.getPersonality());
return animalDTO;
}
//하나의 동물 결과 조회
public String animalDetail(final Long animalId) {
Optional<Animal> animalOptional = animalRepository.findById(animalId);
if (animalOptional.isPresent()) {
System.out.println("DTO 변환 시작");
Animal animal = animalOptional.get();
AnimalResDTO animalResDTO = converToResDTO(animal);
JSONObject object = new JSONObject();
object.put("id", animalResDTO.getId());
object.put("name", animalResDTO.getName());
object.put("picture", animalResDTO.getPicture());
object.put("region", animalResDTO.getRegion().toString());
object.put("sex", animalResDTO.getSex().toString());
object.put("age", animalResDTO.getAge());
object.put("personality", animalResDTO.getPersonality());
object.put("health", animalResDTO.getHealth_condition());
object.put("neuter", animalResDTO.getNeuter());
object.put("shelter_name", animalResDTO.getShelter_name());
object.put("shelter_tel", animalResDTO.getShelter_tel());
return object.toJSONString();
} else {
return null;
}
}
}
- selectListByRegiDate()
findAll을 이용해서 전체 데이터를 조회해오는데, regiDate 필드를 내림차순으로 정렬해서 값들을 받아오고 있다. DTO로 변환하기 위한 작업을 끝낸 후에, JSON 배열에 값들을 하나하나 넣어서 반환해준다.
- converToResDTO()
DTO로 변환하기 위한 메소드이다.
- animalDetail()
selectListByRegiDate와 거의 똑같으며, findById를 이용해 하나의 엔티티만 반환해온다는 점이 다르다. findById는 해당하는 엔티티를 찾아서 Optional 객체로 반환한다고 한다. 따라서 엔티티가 존재하는지 하지 않는지를 파악하고 로직을 구성해줬다.
그리고 서버에서 이넘 클래스를 이용해서 관리하고 있던 데이터들을 바로 getRegion()해서 클라이언트로 넘겨주게 되면 오류가 발생한다. 그냥 넘겨주게 되면
{"sex":M,"name":"예삐","id":1,"region":JEJU,"picture":"1","age":9}
이런식으로 ""가 붙지 않고 전달이 되는데 JSON 속성값은 모두 따옴표로 묶여있어야 유효한 JSON 형식이다. JavaScript에서 문자열로 사용될 수 있도록 toString을 붙여서 값들을 넘겨줬다.
AnimalController
@Controller
@RequiredArgsConstructor
@RequestMapping("/fureverhomes")
public class AnimalController {
private final AnimalSearchService animalService;
//검색 페이지
@GetMapping("/animal")
public String animalSearchPage() {
return "html/furever_search";
}
//상세 페이지
@GetMapping("/detail/{animal_id}")
public String animalDetailPage() {
return "html/furever_detail";
}
@RestController
@RequestMapping("/fureverhomes")
public class AnimalSearchRestController {
//동물 목록 조회
@GetMapping("/animal.list")
public ResponseEntity<String> getAnimalList() {
String animals = animalService.selectListByRegiDate();
if(animals == null) return ResponseEntity.status(HttpStatus.NOT_FOUND).build();
return ResponseEntity.ok(animals);
}
//동물 상세 정보 조회
@GetMapping("/detail/{animal_id}/getDetail")
public ResponseEntity<String> getDetail(@PathVariable("animal_id") Long animalId) {
String animal = animalService.animalDetail(animalId);
if(animal == null) return ResponseEntity.internalServerError().build();
return ResponseEntity.ok(animal);
}
}
}
상세 정보 조회 부분에서 조금 눈여겨볼 부분이 있다면 동물 사진 제목을 아이디값으로 주고, 카드를 클릭하면 /detail/{동물 아이디}로 페이지 매핑을 해준 상태이다. @PathVariable을 이용해서 animal_id 값을 파라미터로 넘겨줘 엔티티를 찾을 수 있도록 만들어주었다. 모든 작업이 무사히 완료가 되면 응답 코드에 json을 담아서 넘겨주도록 만들어줬다.
화면 구성
먼저 검색 페이지 html이다.
<section class="section section-lg">
<div class="container">
<div class="row">
<div class="col d-flex justify-content-start">
<h3 class="h4 mb-5" id="search-info"></h3>
</div>
</div>
<!--시작-->
<div class="row" id="animal-card">
</div>
<!--끝-->
<div class="col mt-3 d-flex justify-content-center">
<nav aria-label="Page navigation example">
<ul class="pagination" id="pagination">
</ul>
</nav>
</div>
</div>
</section>
ajax를 이용해서 동적 페이지를 구성할건데 "animal-card" 아이디를 가지고 있는 부분에 페이지를 구성할 예정이다.
자바 스크립트 코드를 살펴보도록 하겠다.
$(document).ready(function (){
searchGet(null,null,null);
});
function searchGet(species, sex, region){
let apiUrl = "/fureverhomes/animal.list"
$.ajax({
url: apiUrl,
type: "GET",
async: true,
success: function (data, status, xhr) {
let jsonData = JSON.parse(data);
let all_list = jsonData.animal_list;
console.log(all_list.length)
let dataBody = $("#animal-card");
dataBody.empty();
for (let i = 0; i < all_list.length; i++) {
let animal = all_list[i];
let link = "/fureverhomes/detail/";
let imgSrc = "/assets/img/animal/"
let imgId = animal.id;
link = link + imgId;
imgSrc = imgSrc + imgId + ".jpg";
let div1 = $("<div>", {class: "col-12 col-md-6 mb-3"});
let div2 = $("<div>", {class: "card border-light mb-4 animate-up-5"});
let div3 = $("<div>", {class: "row no-gutters align-items-center"});
let div4 = $("<div>", {class: "col-12 col-lg-6 col-xl-4"});
let a1 = $("<a>", {href: link})
let img = $("<img>", {
src: imgSrc,
alt: "animal-picture",
className: "card-img p-2 rounded-xl",
id: imgId,
style: "object-fit: cover; width: 100%; height: 230px;"
});
let div5 = $("<div>", {class: "col-12 col-lg-6 col-xl-8"});
let div6 = $("<div>", {class: "card-body"});
let div7 = $("<div>", {class: "mb-3"});
let span1 = $("<span>", {
class: "h5 mb-3",
text: animal.name
}
)
let heartSpan = $("<span>", {style: "float: right"})
let female = '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="16" fill="gray" class="bi bi-gender-female" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M8 1a4 4 0 1 0 0 8 4 4 0 0 0 0-8zM3 5a5 5 0 1 1 5.5 4.975V12h2a.5.5 0 0 1 0 1h-2v2.5a.5.5 0 0 1-1 0V13h-2a.5.5 0 0 1 0-1h2V9.975A5 5 0 0 1 3 5z"/></svg>';
let male = '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="16" fill="gray" class="bi bi-gender-male" viewBox="0 0 16 16">\n' +
' <path fill-rule="evenodd" d="M9.5 2a.5.5 0 0 1 0-1h5a.5.5 0 0 1 .5.5v5a.5.5 0 0 1-1 0V2.707L9.871 6.836a5 5 0 1 1-.707-.707L13.293 2H9.5zM6 6a4 4 0 1 0 0 8 4 4 0 0 0 0-8z"/>\n' +
'</svg>';
let $female = $(female)
let $male = $(male)
let span2 = $("<span>", {
class: "h6 mb-3",
text: animal.age
}
)
let div8 = $("<div>", {class: "mb-5", style: "color:#424767;"});
let span3 = $("<span>", {class: "fas fa-map-marker-alt mr-2"});
let span4 = $("<span>", {text: animal.region});
dataBody.append(div1);
div1.append(a1);
a1.append(div2);
div2.append(div3);
div3.append(div4);
div4.append(img);
div3.append(div5);
div5.append(div6);
div6.append(div7);
div7.append(span1);
div7.append(heartSpan);
if (animal.sex === "F") {
heartSpan.append($female);
} else if (animal.sex === "M") {
heartSpan.append($male)
}
heartSpan.append(span2);
div6.append(div8);
div8.append(span3);
div8.append(span4);
}
},error: function (xhr) {
alert("페이지를 불러오는데 실패했습니다.")
}
});
}
먼저 페이지에 접근을 하게 되면 바로 searchGet() 메소드가 실행된다. 아직 검색 기능을 구현하지 않아서 파라미터로 넘기는 값들이 존재하지 않지만, searchFoam을 이용해서 검색 내용들을 URL에 추가해서 넘기도록 구현할 예정이다. 서버에서 DTO를 json으로 변경해서 값들을 보내는것까지는 확인이 됐는데, 계속 화면단에서 오류가 떠서 애를 좀 먹었다. id를 일치하지 않게 구성했거나 문법에서 뭔가 놓치는 부분이 있나 싶어서 애를 좀 먹었는데 파싱을 하지 않아서 생기는 오류였다......
상세페이지 역시 유사하게 생성을 해줬으며, 하나의 동물에 대한 정보만 보여주면 되기때문에 검색 페이지의 자바 스크립트보다는 훨씬 간단하게 구성했다.
<html>
<!--html구성-->
<div class="row">
<div class="col-sm-12 mb-3">
<div class="form-group">
<label for="personality">성격
</label>
<input class="form-control" id="personality" type="text" value="" readonly>
</div>
</div>
</div>
</html>
<!--자바스크립트-->
<script>
$("#personality").val(jsonData.personality);
</script>
간단하게 한 부분만 보자면 아까 동물 리스트를 뿌려줄때는 <div> 구성과 <span> 등 하나의 섹션 내부를 자바스크립트를 이용해서 만들어줬었다면 이번에는 큰 레이아웃은 놔두고 데이터가 들어갈 부분들에만 .val() 이나 .append()를 이용해서 데이터를 뿌려줬다.
마무리
오늘 진행했던 부분들은 서버에서 진행할 부분보다는 클라이언트 측에서 진행할 부분이 많았던 것 같다. 사실 어려운 부분은 하나도 없었는데 아무래도 자바스크립트에 익숙하지 않다보니 뭔가 모를 심리적 압박감이 있었던 것 같다. 프로젝트를 진행하면 할수록 정말 부족한 부분이 많구나..를 느낄 수 있다. 내일은 검색 기능과 페이징 기능을 만들어볼 생각이다. 특히 페이징 기능은 원래 약했던 부분인데 Pageable을 이용해서 구현하려니 걱정이 앞선다. 또 머리 깨지게 고민하면서 해봐야 할 것 같다. 🤛