프로젝트/2차프로젝트
전문의 목록/검색
HanJW96
2024. 6. 28. 17:34
디자인은 깔끔하게 된거같아서 아주 만족
마우스를 병원쪽으로 hover하면 카카오지도api를 사용해 지도에 병원 사진과 이름으로 위치정보가 뜨게 구현했다.
병원정보를 등록할때 주소를 등록하면 위도와 경도를 계산해 DB에 집어넣는식으로 했다.
검색은 선택된 태그들 모두가지고 있는 의사를 보여준다.
모든 프로필 이미지는 백서버에 url로 요청하는걸로 구현했다.
<div className="w-2/3 ml-4">
{doctorList && doctorList.length > 0 ? (
doctorList.map((doctor) => (
<DoctorCard
key={doctor.userId}
id={doctor.userId}
name={doctor.userName}
comment={doctor.introduce}
address={doctor.hospitalName}
latitude={doctor.latitude}
longitude={doctor.longitude}
hospitalPhoto={doctor.hospitalPhotoUrl}
hospitalName={doctor.hospitalName}
profileUrl={doctor.profileUrl}
showMap={showMap}
closeMap={closeMap}
/>
//DoctorCard Component
function DoctorCard(props) {
const serverImage = process.env.NEXT_PUBLIC_API_URL;
return (
<div className="flex justify-between p-4 mb-4 bg-white border rounded-lg shadow-md">
<div className="flex flex-col justify-between flex-grow p-4">
<div className="text-lg font-bold">{props.name}</div>
<div className="mb-2 text-sm">{props.comment}</div>
<div className="flex items-center mb-2">
<img className="w-4 h-4 mr-2" src="/location.png" alt="Location" />
<div
className="w-1/4"
onMouseEnter={(event) =>
props.showMap(
event,
props.latitude,
props.longitude,
props.hospitalPhoto,
props.hospitalName
)
}
onMouseLeave={props.closeMap}
>
<span className="text-sm">{props.address}</span>
</div>
</div>
<Link
href={`/doctor/${props.id}`}
className="mt-2 text-sm text-blue-500"
>
전문가 프로필 보기 >
</Link>
</div>
<div className="flex items-center justify-center">
<img
className="object-cover w-24 h-24 rounded-lg"
src={
props.profileUrl ? serverImage + props.profileUrl : "/doctor.png"
}
alt="Doctor"
/>
</div>
</div>
);
}
불러온 정보들을 DoctorCard 컴포넌트에 props로 내려준다
const showMap = (event, latitude, longitude, hospitalPhoto, hospitalName) => {
if (mapTimer) {
clearTimeout(mapTimer);
}
const rect = event.target.getBoundingClientRect();
const mapHeight = 130;
const offset = 300;
setMapCoords({ latitude, longitude });
setMapPosition({ top: rect.top - mapHeight, left: rect.left + offset });
setIsMapVisible(true);
sethospitalName(hospitalName);
sethospitalPhoto(hospitalPhoto);
};
const closeMap = () => {
mapTimer = setTimeout(() => {
setIsMapVisible(false);
}, 300); // 마우스가 0.3초간 떠난 후 지도를 숨김
};
지도를 열고 닫는 함수
useEffect(() => {
const fetchData = async () => {
try {
const response =
selectedTags.length === 0 &&
keyword.length === 0 &&
gender.length === 0
? await showList(currentPage)
: await searchList(
currentPage,
selectedTags,
keywordSearch,
gender
);
setPageCount(response.data.pages);
setDoctorList(response.data.doctors);
} catch (error) {
console.error(error);
}
};
fetchData();
}, [currentPage, selectedTags, gender, keywordSearch]);
useEffect를 사용해 선택된 태그 내용이 달라지는게 있으면 재 랜더링하게 설계했다.
검색항목이 있다면 searchList를 , 없다면 기본 showList를
export const showList = (page) => {
return axios.get(baseUrl + "?page=" + page).then((res) => res);
};
export const searchList = (
currentPage,
selectedTags,
keywordSearch,
gender
) => {
const searchUrl = `/doctor/search?currentPage=${currentPage}&selectedTags=${selectedTags}&keyword=${keywordSearch}&gender=${gender}`;
return axios.get(searchUrl).then((res) => res);
};
검색이 없다면 페이지네이션을 위해 현재페이지만 보내고 검색이 있다면 여러 항목들을 보낸다 .
#Spring Boot
#Controller
@GetMapping
public ResponseEntity<Map<String, Object>> getDoctors(@PageableDefault(size = 4, sort = "userId", direction = Sort.Direction.ASC) Pageable pageable) {
log.info("Doctor list request received");
Map<String, Object> result = doctorService.findAll(pageable);
log.info("Returning response with data: {}", result);
return ResponseEntity.ok(result);
}
@GetMapping("/search")
public ResponseEntity<Map<String, Object>> searchDoctors(@PageableDefault(size = 4, sort = "userId", direction = Sort.Direction.ASC) Pageable pageable, @RequestParam(name = "selectedTags", required = false) List<String> tags,
@RequestParam(name = "keyword", required = false) String name,
@RequestParam(name = "gender", required = false) Character gender) {
return ResponseEntity.ok(doctorService.findByAllConditions(name, tags, tagCount, gender, pageable));
}
#Service
단순 리스트뷰는 별로 볼거없으니 바로 search
public Map<String, Object> findByAllConditions(String name, List<String> tags, Character gender, Pageable pageable) {
Specification<Doctor> spec = Specification.where(null);
if (name != null) {
spec = spec.and(DoctorSpecifications.userNameContaining(name));
}
if (tags != null && !tags.isEmpty()) {
spec = spec.and(DoctorSpecifications.tagsIn(tags));
}
if (gender != null) {
spec = spec.and(DoctorSpecifications.genderContaining(gender));
}
return constructResponse(doctorRepository.findAll(spec, pageable));
}
검색조건을 spec 객체를 만들어서 집어넣는다
Specification 객체
package org.ict.atti_boot.doctor.jpa.specification;
import jakarta.persistence.criteria.*;
import org.ict.atti_boot.doctor.jpa.entity.Education;
import org.springframework.data.jpa.domain.Specification;
import org.ict.atti_boot.doctor.jpa.entity.Doctor;
import org.ict.atti_boot.doctor.jpa.entity.DoctorTag;
import java.util.List;
public class DoctorSpecifications {
public static Specification<Doctor> userNameContaining(String name) {
return (root, query, criteriaBuilder) ->
criteriaBuilder.like(root.join("user").get("userName"), "%" + name + "%");
}
public static Specification<Doctor> tagsIn(List<String> tags) {
return (root, query, criteriaBuilder) -> {
if (tags == null || tags.isEmpty()) {
return criteriaBuilder.conjunction();
}
Predicate[] predicates = new Predicate[tags.size()];
for (int i = 0; i < tags.size(); i++) {
Subquery<Long> subquery = query.subquery(Long.class);
Root<Doctor> subRoot = subquery.from(Doctor.class);
Join<Doctor, DoctorTag> subTagJoin = subRoot.join("tags");
subquery.select(subRoot.get("id"))
.where(criteriaBuilder.equal(subTagJoin.get("tag"), tags.get(i)),
criteriaBuilder.equal(subRoot.get("id"), root.get("id")));
predicates[i] = criteriaBuilder.exists(subquery);
}
return criteriaBuilder.and(predicates);
};
}
public static Specification<Doctor> genderContaining(Character gender) {
return (root, query, criteriaBuilder) ->
criteriaBuilder.equal(root.join("user").get("gender"), gender);
}
}
그리고 JPA 기본 메서드 findAll에 spec과 함께 보내면 조건별 검색 완료
검색에만 2~3일정도 걸린거같다. 근데 솔직히 처음에는 5일정도 걸릴줄 알았는데 생각보다 금방끝났다. 저 spec객체의 존재를 몰랐다면 더 걸렸을거같다.