Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,4 @@ out/
application-secret.yml
/src/main/generated/
/src/main/generated_tests/
/src/main/resources/static/docs/
.claude
8 changes: 6 additions & 2 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down Expand Up @@ -71,6 +72,9 @@ dependencies {

// AI
implementation 'org.springframework.ai:spring-ai-starter-model-openai'

// Spatial
implementation 'org.hibernate.orm:hibernate-spatial'
}

dependencyManagement {
Expand Down
4 changes: 4 additions & 0 deletions src/main/java/com/spots/domain/facility/entity/Facility.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import java.util.List;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.locationtech.jts.geom.Point;

@Getter
@Entity
Expand Down Expand Up @@ -54,6 +55,9 @@ public class Facility {

private Double fcltyLo;

Copilot AI Apr 5, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

spring.jpa.hibernate.ddl-auto: none 설정이라 이 필드 추가만으로는 운영 DB에 컬럼이 생성되지 않습니다. facility.location(geography) 컬럼 추가/기존 데이터 backfill 및 GiST/SP-GiST 공간 인덱스 생성이 함께 반영되지 않으면 조회가 누락되거나 성능 이점이 나오기 어렵습니다. 배포 절차(마이그레이션 스크립트 등)를 PR에 포함하거나 명확히 문서화해 주세요.

Suggested change
/**
* PostGIS geography point used for spatial queries.
*
* <p>Production uses {@code spring.jpa.hibernate.ddl-auto: none}, so adding this field does not
* create the database column automatically. The deployment must include a DB migration that:
*
* <ul>
* <li>adds {@code facility.location} as {@code geography(Point, 4326)}</li>
* <li>backfills existing rows from {@code fcltyLo}/{@code fcltyLa}</li>
* <li>creates a GiST or SP-GiST spatial index for query performance</li>
* </ul>
*/

Copilot uses AI. Check for mistakes.
@Column(columnDefinition = "geography(Point, 4326)")
private Point location;

@OneToMany(mappedBy = "facility", fetch = FetchType.LAZY)
private List<Program> programs;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -34,20 +37,11 @@ public Slice<ProgramInfoResponse> searchPrograms(
BooleanBuilder where = new BooleanBuilder();

NumberExpression<Double> 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);
Comment on lines 41 to +44

Copilot AI Apr 5, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

10000.0(meters)가 하드코딩되어 있어 기존 10km 반경 의도가 코드만으로는 드러나지 않습니다. 반경을 상수로 분리하거나 요청 파라미터로 받도록 해서 단위/의도를 명확히 해주세요.

Copilot uses AI. Check for mistakes.
} else {
distance = null;
}
Comment on lines 39 to 47

Copilot AI Apr 5, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

좌표가 없을 때 distance = null로 설정되는데, 아래쪽에서 distance를 select/orderBy/cursor에 사용하면 NPE가 발생할 수 있습니다(예: distance.asc() 호출). 좌표 미제공 시에는 거리 관련 select/orderBy를 제외하고 기본 정렬로 분기 처리해 주세요.

Copilot uses AI. Check for mistakes.
Expand Down Expand Up @@ -133,16 +127,25 @@ public Slice<ProgramInfoResponse> searchPrograms(
return new SliceImpl<>(content, PageRequest.of(0, pageSize.intValue()), hasNext);
}

private NumberExpression<Double> haversineKm(
double srcLat, double srcLon,
NumberPath<Double> latPath,
NumberPath<Double> 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<Double> 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",

Copilot AI Apr 5, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

stDWithin는 JPA 표준 function('...') 형태로 호출하는데, stDistanceKm는 ST_Distance(...)를 JPQL에 직접 삽입하고 있습니다. 현재 프로젝트에 dialect/function 등록 설정이 보이지 않아(hibernate.dialect 미설정) 환경에 따라 쿼리 파싱 오류가 날 수 있으니 function('st_distance', {0}, {1}) 형태로 맞추는 편이 안전합니다.

Suggested change
"ST_Distance({0}, {1}) / 1000.0",
"function('st_distance', {0}, {1}) / 1000.0",

Copilot uses AI. Check for mistakes.
p.facility.location, center
);
}
}
Loading