diff --git a/.gitignore b/.gitignore index 71ee9e4..3727504 100644 --- a/.gitignore +++ b/.gitignore @@ -37,4 +37,6 @@ out/ .vscode/ ### application.properties ### -src/main/resources/application-local.yml \ No newline at end of file +src/main/resources/application-local.yml + +src/main/resources/google-service-key.json \ No newline at end of file diff --git a/src/main/java/com/onebridge/ouch/controller/hospital/HospitalController.java b/src/main/java/com/onebridge/ouch/controller/hospital/HospitalController.java new file mode 100644 index 0000000..c22ea80 --- /dev/null +++ b/src/main/java/com/onebridge/ouch/controller/hospital/HospitalController.java @@ -0,0 +1,51 @@ +package com.onebridge.ouch.controller.hospital; + +import java.util.List; + +import org.springframework.web.bind.annotation.*; + +import com.onebridge.ouch.dto.hospital.response.AllDepartmentResponse; +import com.onebridge.ouch.dto.hospital.response.HospitalDetailResponse; +import com.onebridge.ouch.dto.hospital.response.HospitalDistanceResponse; +import com.onebridge.ouch.service.department.DepartmentService; +import com.onebridge.ouch.service.hospital.HospitalDetailService; +import com.onebridge.ouch.service.hospital.HospitalSearchService; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; + +@RestController +@Tag(name = "병원 API", description = "병원 관련 API 입니다.") +@RequestMapping("/hospitals") +@RequiredArgsConstructor +public class HospitalController { + private final HospitalSearchService hospitalSearchService; + private final HospitalDetailService hospitalDetailService; + private final DepartmentService departmentService; + + @Operation(summary = "거리 순 병원 조회 API", description = "입력된 진료과, 위도(lat), 경도(lng)를 기준으로 병원 목록을 거리 순으로 조회합니다. " + + "진료과를 입력하지 않으면 입력된 위도, 경도를 기준으로 모든 병원 목록을 거리 순으로 조회합니다. 위도, 경도는 필수로 입력해야 합니다. ") + @GetMapping("/search") + public List searchHospitals( + @RequestParam(required = false) String department, + @RequestParam Double lat, + @RequestParam Double lng, + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "20") int size + ) { + return hospitalSearchService.searchHospitals(department, lat, lng, page, size); + } + + @Operation(summary = "병원 상세 조회 API", description = "입력된 병원 고유ID를 통해 병원 상세 정보를 조회합니다.") + @GetMapping("/{ykiho}") + public HospitalDetailResponse getHospitalDetail(@PathVariable String ykiho) { + return hospitalDetailService.getHospitalDetail(ykiho); + } + + @Operation(summary = "진료과 목록 조회 API", description = "모든 진료과 목록을 조회합니다.") + @GetMapping("/departments") + public List getDepartments() { + return departmentService.getAllDepartments(); + } +} diff --git a/src/main/java/com/onebridge/ouch/domain/Department.java b/src/main/java/com/onebridge/ouch/domain/Department.java deleted file mode 100644 index b81553f..0000000 --- a/src/main/java/com/onebridge/ouch/domain/Department.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.onebridge.ouch.domain; - -import com.onebridge.ouch.domain.common.BaseEntity; - -import jakarta.persistence.*; -import lombok.*; - -@Entity -@Getter -@Builder -@NoArgsConstructor(access = AccessLevel.PROTECTED) -@AllArgsConstructor -public class Department extends BaseEntity { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @Column(nullable = false, length = 30) - private String name; -} - diff --git a/src/main/java/com/onebridge/ouch/domain/Hospital.java b/src/main/java/com/onebridge/ouch/domain/Hospital.java index 2a5e79d..9dcde89 100644 --- a/src/main/java/com/onebridge/ouch/domain/Hospital.java +++ b/src/main/java/com/onebridge/ouch/domain/Hospital.java @@ -1,31 +1,57 @@ package com.onebridge.ouch.domain; -import java.util.ArrayList; -import java.util.List; - -import com.onebridge.ouch.domain.common.BaseEntity; -import com.onebridge.ouch.domain.mapping.HospitalDepartment; - import jakarta.persistence.*; import lombok.*; @Entity @Getter -@Builder @NoArgsConstructor(access = AccessLevel.PROTECTED) -@AllArgsConstructor -public class Hospital extends BaseEntity { +public class Hospital { @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; + //@Column(name = "암호화요양기호") + private String ykiho; // 암호화요양기호 (PK) + + //@Column(name = "요양기관명") + private String name; // 병원 이름 + + //@Column(name = "종별코드명") + private String type; // 종별코드명(병원/의원/종합병원 등) + + //@Column(name = "시도코드명") + private String sido; // 시/도명 + + //@Column(name = "시군구코드명") + private String sigungu; // 시/군/구명 + + //@Column(name = "읍면동") + private String eupmyeondong; // 읍면동 + + //@Column(name = "우편번호") + private String zipcode; // 우편번호 + + //@Column(name = "주소") + private String address; // 주소 + + //@Column(name = "전화번호") + private String tel; // 전화번호 - @Column(nullable = false, length = 50) - private String name; + //@Column(name = "좌표(X)") + private Double lat; // 좌표(X) = 위도 - @Column(nullable = false, length = 100) - private String address; + //@Column(name = "좌표(Y)") + private Double lng; // 좌표(Y) = 경도 - @OneToMany(mappedBy = "hospital", cascade = CascadeType.ALL, orphanRemoval = true) - private List hospitalDepartmentList = new ArrayList<>(); + // @Id + // @GeneratedValue(strategy = GenerationType.IDENTITY) + // private Long id; + // + // @Column(nullable = false, length = 50) + // private String name; + // + // @Column(nullable = false, length = 100) + // private String address; + // + // @OneToMany(mappedBy = "hospital", cascade = CascadeType.ALL, orphanRemoval = true) + // private List hospitalDepartmentList = new ArrayList<>(); } diff --git a/src/main/java/com/onebridge/ouch/domain/mapping/HospitalDepartment.java b/src/main/java/com/onebridge/ouch/domain/mapping/HospitalDepartment.java index a86eb63..34dbddf 100644 --- a/src/main/java/com/onebridge/ouch/domain/mapping/HospitalDepartment.java +++ b/src/main/java/com/onebridge/ouch/domain/mapping/HospitalDepartment.java @@ -1,28 +1,22 @@ package com.onebridge.ouch.domain.mapping; -import com.onebridge.ouch.domain.Hospital; -import com.onebridge.ouch.domain.Department; -import com.onebridge.ouch.domain.common.BaseEntity; - import jakarta.persistence.*; import lombok.*; @Entity @Getter -@Builder @NoArgsConstructor(access = AccessLevel.PROTECTED) -@AllArgsConstructor -public class HospitalDepartment extends BaseEntity { +public class HospitalDepartment { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "department_id") - private Department department; + private String ykiho; // 암호화요양기호 (병원 식별자) + private Long departmentCode; // 진료과목코드 + private String departmentName; // 진료과목명 + private Integer specialistCount; // 전문의 수 - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "hospital_id") - private Hospital hospital; + // 병원-진료과 unique 인덱스 (ykiho + departmentCode) 추가 권장 + // @Table(uniqueConstraints = {@UniqueConstraint(columnNames = {"ykiho", "departmentCode"})}) } diff --git a/src/main/java/com/onebridge/ouch/domain/tempDomain/Department.txt b/src/main/java/com/onebridge/ouch/domain/tempDomain/Department.txt new file mode 100644 index 0000000..9fe1e12 --- /dev/null +++ b/src/main/java/com/onebridge/ouch/domain/tempDomain/Department.txt @@ -0,0 +1,17 @@ +package com.onebridge.ouch.domain; + + +import jakarta.persistence.*; +import lombok.*; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Department { + + @Id + private Long code; // 진료과목코드(PK, 필요 없으면 생략) + private String nameKr; // 진료과목 한글명 + private String nameEn; // 진료과목 영문명 +} + diff --git a/src/main/java/com/onebridge/ouch/dto/hospital/response/AllDepartmentResponse.java b/src/main/java/com/onebridge/ouch/dto/hospital/response/AllDepartmentResponse.java new file mode 100644 index 0000000..518f4c1 --- /dev/null +++ b/src/main/java/com/onebridge/ouch/dto/hospital/response/AllDepartmentResponse.java @@ -0,0 +1,10 @@ +package com.onebridge.ouch.dto.hospital.response; + +import lombok.Getter; + +@Getter +public class AllDepartmentResponse { + private Long code; + private String nameKr; + private String nameEn; +} diff --git a/src/main/java/com/onebridge/ouch/dto/hospital/response/HospitalDetailResponse.java b/src/main/java/com/onebridge/ouch/dto/hospital/response/HospitalDetailResponse.java new file mode 100644 index 0000000..71184ba --- /dev/null +++ b/src/main/java/com/onebridge/ouch/dto/hospital/response/HospitalDetailResponse.java @@ -0,0 +1,30 @@ +package com.onebridge.ouch.dto.hospital.response; + +import java.util.List; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class HospitalDetailResponse { + private String ykiho; + private String name; + private String type; // 종별코드명 + private String zipcode; + private String address; + private Double lat; + private Double lng; + private String tel; + private List departments; + + // Getter/Setter + @Getter + @Setter + public static class DepartmentInfo { + private String departmentName; + private Integer specialistCount; // 전문의 수 + + // Getter/Setter + } +} diff --git a/src/main/java/com/onebridge/ouch/dto/hospital/response/HospitalDistanceResponse.java b/src/main/java/com/onebridge/ouch/dto/hospital/response/HospitalDistanceResponse.java new file mode 100644 index 0000000..bcd3f31 --- /dev/null +++ b/src/main/java/com/onebridge/ouch/dto/hospital/response/HospitalDistanceResponse.java @@ -0,0 +1,16 @@ +package com.onebridge.ouch.dto.hospital.response; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class HospitalDistanceResponse { + private String ykiho; + private String name; + private String address; + private String tel; + private Double lat; + private Double lng; + private Double distance; //km +} diff --git a/src/main/java/com/onebridge/ouch/repository/hospital/HospitalDepartmentRepository.java b/src/main/java/com/onebridge/ouch/repository/hospital/HospitalDepartmentRepository.java new file mode 100644 index 0000000..a034937 --- /dev/null +++ b/src/main/java/com/onebridge/ouch/repository/hospital/HospitalDepartmentRepository.java @@ -0,0 +1,14 @@ +package com.onebridge.ouch.repository.hospital; + +import java.util.List; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import com.onebridge.ouch.domain.mapping.HospitalDepartment; + +@Repository +public interface HospitalDepartmentRepository extends JpaRepository { + + List findByYkiho(String ykiho); +} diff --git a/src/main/java/com/onebridge/ouch/repository/hospital/HospitalRepository.java b/src/main/java/com/onebridge/ouch/repository/hospital/HospitalRepository.java new file mode 100644 index 0000000..a6fec35 --- /dev/null +++ b/src/main/java/com/onebridge/ouch/repository/hospital/HospitalRepository.java @@ -0,0 +1,49 @@ +package com.onebridge.ouch.repository.hospital; + +import java.util.List; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import com.onebridge.ouch.domain.Hospital; + +@Repository +public interface HospitalRepository extends JpaRepository { + + // 전체 병원 거리순 정렬 (진료과 미입력시) + @Query(value = + "SELECT h.ykiho, h.name, h.address, h.tel, h.lat, h.lng, " + + " (6371 * acos(cos(radians(:lat)) * cos(radians(h.lat)) " + + " * cos(radians(h.lng) - radians(:lng)) + sin(radians(:lat)) * sin(radians(h.lat)))) as distance " + + "FROM hospital h " + + "WHERE h.lat IS NOT NULL AND h.lng IS NOT NULL " + + "ORDER BY distance ASC " + + "LIMIT :limit OFFSET :offset", + nativeQuery = true) + List findAllOrderByDistance( + @Param("lat") double lat, + @Param("lng") double lng, + @Param("limit") int limit, + @Param("offset") int offset + ); + + // 진료과 기반 병원 거리순 정렬 (병원-진료과 조인) + @Query(value = + "SELECT h.ykiho, h.name, h.address, h.tel, h.lat, h.lng, " + + " (6371 * acos(cos(radians(:lat)) * cos(radians(h.lat)) " + + " * cos(radians(h.lng) - radians(:lng)) + sin(radians(:lat)) * sin(radians(h.lat)))) as distance " + + "FROM hospital h " + + "JOIN hospital_department hd ON h.ykiho = hd.ykiho " + + "WHERE hd.department_name = :departmentName AND h.lat IS NOT NULL AND h.lng IS NOT NULL " + + "ORDER BY distance ASC " + + "LIMIT :limit OFFSET :offset", + nativeQuery = true) + List findByDepartmentOrderByDistance( + @Param("departmentName") String departmentName, + @Param("lat") double lat, + @Param("lng") double lng, + @Param("limit") int limit, + @Param("offset") int offset + ); +} diff --git a/src/main/java/com/onebridge/ouch/service/department/DepartmentService.java b/src/main/java/com/onebridge/ouch/service/department/DepartmentService.java new file mode 100644 index 0000000..14eb2ba --- /dev/null +++ b/src/main/java/com/onebridge/ouch/service/department/DepartmentService.java @@ -0,0 +1,29 @@ +package com.onebridge.ouch.service.department; + +import java.io.InputStream; +import java.util.List; + +import org.springframework.core.io.ClassPathResource; +import org.springframework.stereotype.Service; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.onebridge.ouch.dto.hospital.response.AllDepartmentResponse; + +import jakarta.annotation.PostConstruct; + +@Service +public class DepartmentService { + private List departmentList; + + @PostConstruct + public void init() throws Exception { + ObjectMapper mapper = new ObjectMapper(); + InputStream input = new ClassPathResource("data/department.json").getInputStream(); + departmentList = mapper.readValue(input, new TypeReference>() {}); + } + + public List getAllDepartments() { + return departmentList; + } +} diff --git a/src/main/java/com/onebridge/ouch/service/hospital/HospitalDetailService.java b/src/main/java/com/onebridge/ouch/service/hospital/HospitalDetailService.java new file mode 100644 index 0000000..e6af28c --- /dev/null +++ b/src/main/java/com/onebridge/ouch/service/hospital/HospitalDetailService.java @@ -0,0 +1,53 @@ +package com.onebridge.ouch.service.hospital; + +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.stream.Collectors; + +import com.onebridge.ouch.domain.Hospital; +import com.onebridge.ouch.domain.mapping.HospitalDepartment; +import com.onebridge.ouch.dto.hospital.response.HospitalDetailResponse; +import com.onebridge.ouch.repository.hospital.HospitalDepartmentRepository; +import com.onebridge.ouch.repository.hospital.HospitalRepository; + +@Service +public class HospitalDetailService { + private final HospitalRepository hospitalRepository; + private final HospitalDepartmentRepository hospitalDepartmentRepository; + + public HospitalDetailService(HospitalRepository hospitalRepository, HospitalDepartmentRepository hospitalDepartmentRepository) { + this.hospitalRepository = hospitalRepository; + this.hospitalDepartmentRepository = hospitalDepartmentRepository; + } + + public HospitalDetailResponse getHospitalDetail(String ykiho) { + Hospital hospital = hospitalRepository.findById(ykiho) + .orElseThrow(() -> new RuntimeException("병원 정보를 찾을 수 없습니다.")); + + // 해당 병원의 진료과 + 전문의 수 + List deptList = hospitalDepartmentRepository.findByYkiho(ykiho); + + List departments = deptList.stream() + .map(d -> { + HospitalDetailResponse.DepartmentInfo info = new HospitalDetailResponse.DepartmentInfo(); + info.setDepartmentName(d.getDepartmentName()); + info.setSpecialistCount(d.getSpecialistCount()); + return info; + }) + .collect(Collectors.toList()); + + // 상세 응답 구성 + HospitalDetailResponse detail = new HospitalDetailResponse(); + detail.setYkiho(hospital.getYkiho()); + detail.setName(hospital.getName()); + detail.setType(hospital.getType()); + detail.setZipcode(hospital.getZipcode()); + detail.setAddress(hospital.getAddress()); + detail.setLat(hospital.getLat()); + detail.setLng(hospital.getLng()); + detail.setTel(hospital.getTel()); + detail.setDepartments(departments); + return detail; + } +} diff --git a/src/main/java/com/onebridge/ouch/service/hospital/HospitalSearchService.java b/src/main/java/com/onebridge/ouch/service/hospital/HospitalSearchService.java new file mode 100644 index 0000000..facbb98 --- /dev/null +++ b/src/main/java/com/onebridge/ouch/service/hospital/HospitalSearchService.java @@ -0,0 +1,51 @@ +package com.onebridge.ouch.service.hospital; + +import org.springframework.stereotype.Service; +import java.util.ArrayList; +import java.util.List; + +import com.onebridge.ouch.dto.hospital.response.HospitalDistanceResponse; +import com.onebridge.ouch.repository.hospital.HospitalRepository; + +@Service +public class HospitalSearchService { + private final HospitalRepository hospitalRepository; + + public HospitalSearchService(HospitalRepository hospitalRepository) { + this.hospitalRepository = hospitalRepository; + } + + public List searchHospitals( + String department, + Double lat, + Double lng, + int page, + int size + ) { + if (lat == null || lng == null) { + throw new IllegalArgumentException("좌표(lat, lng)는 필수입니다."); + } + int offset = page * size; + List rawList; + if (department != null && !department.isBlank()) { + rawList = hospitalRepository.findByDepartmentOrderByDistance(department, lat, lng, size, offset); + } else { + rawList = hospitalRepository.findAllOrderByDistance(lat, lng, size, offset); + } + + List result = new ArrayList<>(); + for (Object[] row : rawList) { + HospitalDistanceResponse dto = new HospitalDistanceResponse(); + // row 순서는 hospital 테이블 컬럼 순서 + 마지막에 distance + dto.setYkiho((String) row[0]); + dto.setName((String) row[1]); + dto.setAddress((String) row[2]); + dto.setTel((String) row[3]); + dto.setLat(row[4] != null ? ((Number)row[4]).doubleValue() : null); + dto.setLng(row[5] != null ? ((Number)row[5]).doubleValue() : null); + dto.setDistance(row[6] != null ? ((Number)row[6]).doubleValue() : null); + result.add(dto); + } + return result; + } +} diff --git a/src/main/resources/data/department.json b/src/main/resources/data/department.json new file mode 100644 index 0000000..59cafb2 --- /dev/null +++ b/src/main/resources/data/department.json @@ -0,0 +1,21 @@ +[ + { "code": 1, "nameKr": "내과", "nameEn": "Internal Medicine" }, + { "code": 23, "nameKr": "가정의학과", "nameEn": "Family Medicine" }, + { "code": 4, "nameKr": "외과", "nameEn": "General Surgery" }, + { "code": 5, "nameKr": "정형외과", "nameEn": "Orthopedic Surgery" }, + { "code": 6, "nameKr": "신경외과", "nameEn": "Neurosurgery" }, + { "code": 2, "nameKr": "신경과", "nameEn": "Neurology" }, + { "code": 3, "nameKr": "정신건강의학과", "nameEn": "Psychiatry" }, + { "code": 7, "nameKr": "흉부외과", "nameEn": "Thoracic Surgery" }, + { "code": 8, "nameKr": "성형외과", "nameEn": "Plastic Surgery" }, + { "code": 9, "nameKr": "마취통증의학과", "nameEn": "Anesthesiology & Pain Medicine" }, + { "code": 10, "nameKr": "산부인과", "nameEn": "Obstetrics & Gynecology" }, + { "code": 11, "nameKr": "소아청소년과", "nameEn": "Pediatrics" }, + { "code": 12, "nameKr": "안과", "nameEn": "Ophthalmology" }, + { "code": 13, "nameKr": "이비인후과", "nameEn": "Otorhinolaryngology (ENT)" }, + { "code": 14, "nameKr": "피부과", "nameEn": "Dermatology" }, + { "code": 15, "nameKr": "비뇨의학과", "nameEn": "Urology" }, + { "code": 21, "nameKr": "재활의학과", "nameEn": "Rehabilitation Medicine" }, + { "code": 49, "nameKr": "치과", "nameEn": "Dentistry" }, + { "code": 24, "nameKr": "응급의학과", "nameEn": "Emergency Medicine" } +]