From 869d917fbcf32396590dc57e1a9d83ccb6ed6be9 Mon Sep 17 00:00:00 2001 From: Yongbum Kim Date: Sat, 4 Apr 2026 18:09:08 +0900 Subject: [PATCH 1/6] =?UTF-8?q?feat:=20PostGIS=20=EC=A0=81=EC=9A=A9?= =?UTF-8?q?=EC=9D=84=20=EC=9C=84=ED=95=9C=20=EC=9D=98=EC=A1=B4=EC=84=B1=20?= =?UTF-8?q?=EB=B0=8F=20=EC=84=A4=EC=A0=95=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Hibernate Spatial 의존성을 추가하여 Java 애플리케이션 단에서 Geometry 타입 및 공간 함수를 활용할 수 있도록 합니다. - 데이터베이스 방언을 PostGIS를 활용할 수 있는 방언으로 지정해줍니다. issue #32 --- .gitignore | 2 +- build.gradle | 3 +++ src/main/resources/application.yml | 1 + 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 6981419..52d62bb 100644 --- a/.gitignore +++ b/.gitignore @@ -43,4 +43,4 @@ out/ application-secret.yml /src/main/generated/ /src/main/generated_tests/ -/src/main/resources/static/docs/ \ No newline at end of file +.claude diff --git a/build.gradle b/build.gradle index 5ebaa17..ddf8d30 100644 --- a/build.gradle +++ b/build.gradle @@ -71,6 +71,9 @@ dependencies { // AI implementation 'org.springframework.ai:spring-ai-starter-model-openai' + + // Spatial + implementation 'org.hibernate.orm:hibernate-spatial' } dependencyManagement { diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 641094a..a27a996 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -14,6 +14,7 @@ spring: properties: hibernate: format_sql: true + dialect: org.hibernate.spatial.dialect.postgis.PostgisPG10Dialect open-in-view: false # Spring AI Config From 27620230d086619a23c47d24094d2d0b072e122f Mon Sep 17 00:00:00 2001 From: Yongbum Kim Date: Sat, 4 Apr 2026 18:21:06 +0900 Subject: [PATCH 2/6] =?UTF-8?q?feat:=20Facility=20Entity=20location=20?= =?UTF-8?q?=ED=95=84=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Facility Entity 데이터에 Point 타입의 location 필드를 추가합니다. issue #32 --- src/main/java/com/spots/domain/facility/entity/Facility.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/java/com/spots/domain/facility/entity/Facility.java b/src/main/java/com/spots/domain/facility/entity/Facility.java index e1a7017..9d42997 100644 --- a/src/main/java/com/spots/domain/facility/entity/Facility.java +++ b/src/main/java/com/spots/domain/facility/entity/Facility.java @@ -13,6 +13,7 @@ import java.util.List; import lombok.Getter; import lombok.NoArgsConstructor; +import org.locationtech.jts.geom.Point; @Getter @Entity @@ -54,6 +55,9 @@ public class Facility { private Double fcltyLo; + @Column(columnDefinition = "geometry(Point, 4326)") + private Point location; // DB의 geometry 컬럼과 매핑 + @OneToMany(mappedBy = "facility", fetch = FetchType.LAZY) private List programs; From b4ab4f66024e3c74b679c3c57b9cac551004fd26 Mon Sep 17 00:00:00 2001 From: Yongbum Kim Date: Sun, 5 Apr 2026 18:21:41 +0900 Subject: [PATCH 3/6] =?UTF-8?q?refactor:=20jpa=20dialect=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Hibernate 6.x 버전에서는 자동으로 dialect를 적용하기 때문에 삭제합니다. issue #32 --- src/main/resources/application.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index a27a996..641094a 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -14,7 +14,6 @@ spring: properties: hibernate: format_sql: true - dialect: org.hibernate.spatial.dialect.postgis.PostgisPG10Dialect open-in-view: false # Spring AI Config From 2b90a098839432b072d9f7bf1bcd35f9aebed8a8 Mon Sep 17 00:00:00 2001 From: Yongbum Kim Date: Sun, 5 Apr 2026 18:22:19 +0900 Subject: [PATCH 4/6] =?UTF-8?q?refactor:=20querydsl=20=EB=B2=84=EC=A0=84?= =?UTF-8?q?=205.x=20->=206.x?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - QueryDsl 버전을 6.x 버전으로 업그레이드 합니다. issue #32 --- build.gradle | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index ddf8d30..80598a6 100644 --- a/build.gradle +++ b/build.gradle @@ -39,8 +39,9 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' // Querydsl - implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta' - annotationProcessor 'com.querydsl:querydsl-apt:5.0.0:jakarta' + implementation 'io.github.openfeign.querydsl:querydsl-jpa:6.12' + implementation 'io.github.openfeign.querydsl:querydsl-spatial:6.12' + annotationProcessor 'io.github.openfeign.querydsl:querydsl-apt:6.12:jpa' annotationProcessor 'jakarta.annotation:jakarta.annotation-api' annotationProcessor 'jakarta.persistence:jakarta.persistence-api' From 71171f3fa516a6371699eee66b897b2399578d1c Mon Sep 17 00:00:00 2001 From: Yongbum Kim Date: Sun, 5 Apr 2026 18:23:29 +0900 Subject: [PATCH 5/6] =?UTF-8?q?fix:=20geometry=20=EA=B3=B5=EA=B0=84=20?= =?UTF-8?q?=ED=83=80=EC=9E=85=EC=97=90=EC=84=9C=20geography=20=EA=B3=B5?= =?UTF-8?q?=EA=B0=84=20=ED=83=80=EC=9E=85=EC=9C=BC=EB=A1=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - meter 단위의 거리 값이 필요하기 때문에 geometry 공간 타입에서 geography 공간 타입으로 변경합니다. issue #32 --- src/main/java/com/spots/domain/facility/entity/Facility.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/spots/domain/facility/entity/Facility.java b/src/main/java/com/spots/domain/facility/entity/Facility.java index 9d42997..f454a2f 100644 --- a/src/main/java/com/spots/domain/facility/entity/Facility.java +++ b/src/main/java/com/spots/domain/facility/entity/Facility.java @@ -55,8 +55,8 @@ public class Facility { private Double fcltyLo; - @Column(columnDefinition = "geometry(Point, 4326)") - private Point location; // DB의 geometry 컬럼과 매핑 + @Column(columnDefinition = "geography(Point, 4326)") + private Point location; @OneToMany(mappedBy = "facility", fetch = FetchType.LAZY) private List programs; From 2e47ce178bf9b0aa6da378ff5fccb771d34f15f5 Mon Sep 17 00:00:00 2001 From: Yongbum Kim Date: Sun, 5 Apr 2026 18:24:52 +0900 Subject: [PATCH 6/6] =?UTF-8?q?fix:=20JTL=20Point=20=EA=B0=9D=EC=B2=B4?= =?UTF-8?q?=EB=A5=BC=20=ED=99=9C=EC=9A=A9=ED=95=98=EC=97=AC=20query=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9=20-=20Hibernate=EC=97=90=EC=84=9C=20ST=5FDwi?= =?UTF-8?q?thin=20=ED=95=A8=EC=88=98=EB=A5=BC=20=EC=9D=B8=EC=8B=9D?= =?UTF-8?q?=ED=95=98=EC=A7=80=20=EB=AA=BB=ED=95=98=EB=8A=94=20=EB=AC=B8?= =?UTF-8?q?=EC=A0=9C=EB=A5=BC=20=EB=B0=9C=EA=B2=AC=ED=96=88=EC=8A=B5?= =?UTF-8?q?=EB=8B=88=EB=8B=A4.=20-=20function()=20=ED=82=A4=EC=9B=8C?= =?UTF-8?q?=EB=93=9C=EB=A5=BC=20=ED=99=9C=EC=9A=A9=ED=95=98=EC=97=AC=20DB?= =?UTF-8?q?=EB=A1=9C=20=EC=A7=81=EC=A0=91=20=EC=BF=BC=EB=A6=AC=EB=A5=BC=20?= =?UTF-8?q?=EB=82=A0=EB=A6=B4=20=EC=88=98=20=EC=9E=88=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=ED=96=88=EC=8A=B5=EB=8B=88=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit issue #32 --- .../repository/ProgramRepositoryImpl.java | 45 ++++++++++--------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/src/main/java/com/spots/domain/program/repository/ProgramRepositoryImpl.java b/src/main/java/com/spots/domain/program/repository/ProgramRepositoryImpl.java index f51ce8c..1203402 100644 --- a/src/main/java/com/spots/domain/program/repository/ProgramRepositoryImpl.java +++ b/src/main/java/com/spots/domain/program/repository/ProgramRepositoryImpl.java @@ -6,13 +6,16 @@ import com.querydsl.core.BooleanBuilder; import com.querydsl.core.types.dsl.Expressions; import com.querydsl.core.types.dsl.NumberExpression; -import com.querydsl.core.types.dsl.NumberPath; import com.querydsl.jpa.impl.JPAQueryFactory; import com.spots.domain.program.dto.request.ProgramInfoServiceRequest; import com.spots.domain.program.dto.response.ProgramInfoResponse; import com.spots.domain.program.entity.QProgram; import java.util.List; import lombok.RequiredArgsConstructor; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.GeometryFactory; +import org.locationtech.jts.geom.Point; +import org.locationtech.jts.geom.PrecisionModel; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Slice; import org.springframework.data.domain.SliceImpl; @@ -34,20 +37,11 @@ public Slice searchPrograms( BooleanBuilder where = new BooleanBuilder(); NumberExpression distance; - double radius = 10.0; if (req.latitude() != null && req.longitude() != null) { - where.and(p.facility.fcltyLa.isNotNull()) - .and(p.facility.fcltyLo.isNotNull()); - - distance = haversineKm( - req.latitude(), - req.longitude(), - p.facility.fcltyLa, - p.facility.fcltyLo - ); - - where.and(distance.loe(radius)); + Point center = createPoint(req.longitude(), req.latitude()); + where.and(stDWithin(p, center, 10000.0)); + distance = stDistanceKm(p, center); } else { distance = null; } @@ -133,16 +127,25 @@ public Slice searchPrograms( return new SliceImpl<>(content, PageRequest.of(0, pageSize.intValue()), hasNext); } - private NumberExpression haversineKm( - double srcLat, double srcLon, - NumberPath latPath, - NumberPath lonPath - ) { + private Point createPoint(double longitude, double latitude) { + return new GeometryFactory(new PrecisionModel(), 4326) + .createPoint(new Coordinate(longitude, latitude)); + } + + private BooleanBuilder stDWithin(QProgram p, Point center, double meters) { + return new BooleanBuilder( + Expressions.booleanTemplate( + "function('st_dwithin', {0}, {1}, {2}) = true", + p.facility.location, center, meters + ) + ); + } + + private NumberExpression stDistanceKm(QProgram p, Point center) { return Expressions.numberTemplate( Double.class, - "6371 * acos( cos(radians({0})) * cos(radians({1})) * cos(radians({2}) - radians({3})) " - + "+ sin(radians({0})) * sin(radians({1})) )", - srcLat, latPath, lonPath, srcLon + "ST_Distance({0}, {1}) / 1000.0", + p.facility.location, center ); } }