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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,6 @@ src/main/resources/*.yml

### VS Code ###
.vscode/

## local docker compose
docker/docker-compose.local.yml
7 changes: 7 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,13 @@ dependencies {

// AWS S3
implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE'

// PostGIS 공간 타입 <-> JTS(Point 등) 매핑 지원
implementation "org.hibernate.orm:hibernate-spatial"

// JTS
implementation 'org.locationtech.jts:jts-core:1.19.0'

}

tasks.named('test') {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.be.sportizebe.domain.facility.controller;

import com.be.sportizebe.domain.facility.dto.FacilityNearResponse;
import com.be.sportizebe.domain.facility.service.SportsFacilityService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/facilities")
public class SportsFacilityController {

private final SportsFacilityService sportsFacilityService;

@GetMapping("/near")
public List<FacilityNearResponse> near(
@RequestParam double lat,
@RequestParam double lng,
@RequestParam(defaultValue = "3000") int radiusM,
@RequestParam(defaultValue = "50") int limit,
@RequestParam(required = false) String type

) {
return sportsFacilityService.getNear(lat, lng, radiusM, limit, type);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.be.sportizebe.domain.facility.dto;

import lombok.Builder;
import lombok.Getter;

@Getter
@Builder
public class FacilityNearResponse {
private Long id;
private String facilityName;
private String introduce;
private String thumbnailUrl;
private String facilityType;
private int distanceM; // 프론트에서 보기 쉽게 미터 int로
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.be.sportizebe.domain.facility.entity;

public enum FacilityType {
BASKETBALL,
SOCCER,
BADMINTON,
TENNIS,
BOWLING,
ETC
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package com.be.sportizebe.domain.facility.entity;

import com.be.sportizebe.global.common.BaseTimeEntity;
import jakarta.persistence.*;
import lombok.*;
import org.locationtech.jts.geom.Point;

@Entity
@Table(name = "sports_facilities")
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@Builder
public class SportsFacility extends BaseTimeEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(nullable = false, length = 100)
private String facilityName;

@Column(columnDefinition = "text")
private String introduce;

// 리소스 부담 생각해서 일단 썸네일 이미지 1장으로만 구현하는 방식
private String thumbnailUrl;

@Enumerated(EnumType.STRING)
@Column(nullable = false, length = 30)
private FacilityType facilityType;

@Column(columnDefinition = "geography(Point, 4326)", nullable = false)
private Point location;

// 변경 메서드(Dirty Checking용)
public void changeInfo(String facilityName, String introduce, String thumbnailUrl, FacilityType facilityType) {
if (facilityName != null) this.facilityName = facilityName;
if (introduce != null) this.introduce = introduce;
if (thumbnailUrl != null) this.thumbnailUrl = thumbnailUrl;
if (facilityType != null) this.facilityType = facilityType;
}

public void changeLocation(Point location) {
this.location = location;
}
Comment on lines +43 to +45
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

changeLocation 메서드에 null 검증 누락

location 컬럼은 nullable = false로 정의되어 있지만(line 32), changeLocation 메서드는 null 체크 없이 값을 할당합니다. null이 전달되면 JPA flush 시점에 DB 제약 조건 위반 예외가 발생합니다.

🐛 null 검증 추가 제안
     public void changeLocation(Point location) {
+        if (location == null) {
+            throw new IllegalArgumentException("Location cannot be null");
+        }
         this.location = location;
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public void changeLocation(Point location) {
this.location = location;
}
public void changeLocation(Point location) {
if (location == null) {
throw new IllegalArgumentException("Location cannot be null");
}
this.location = location;
}
🤖 Prompt for AI Agents
In `@src/main/java/com/be/sportizebe/domain/facility/entity/SportsFacility.java`
around lines 43 - 45, The changeLocation(Point location) method assigns a value
to the non-nullable field location without validation; add a null check at the
start of changeLocation (or use Objects.requireNonNull) and throw a clear
exception (e.g., IllegalArgumentException or NullPointerException) if location
is null so callers receive an immediate, descriptive error instead of a DB
constraint failure when JPA flushes.

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.be.sportizebe.domain.facility.mapper;

import com.be.sportizebe.domain.facility.dto.FacilityNearResponse;
import com.be.sportizebe.domain.facility.repository.FacilityNearProjection;

public interface FacilityMapper {

public static FacilityNearResponse toNearResponse(FacilityNearProjection p){
return FacilityNearResponse.builder()
.id(p.getId())
.facilityName(p.getFacilityName())
.introduce(p.getIntroduce())
.thumbnailUrl(p.getThumbnailUrl())
.facilityType(p.getFacilityType())
.distanceM((int) Math.round(p.getDistanceM()))
.build();
}
Comment on lines +8 to +17
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

distanceM가 null일 경우 NPE 발생 가능성

FacilityNearProjection.getDistanceM()Double 타입을 반환하므로 null이 될 수 있습니다. Math.round(null)은 NPE를 발생시킵니다.

🐛 null 처리 추가 제안
     public static FacilityNearResponse toNearResponse(FacilityNearProjection p){
         return FacilityNearResponse.builder()
                 .id(p.getId())
                 .facilityName(p.getFacilityName())
                 .introduce(p.getIntroduce())
                 .thumbnailUrl(p.getThumbnailUrl())
                 .facilityType(p.getFacilityType())
-                .distanceM((int) Math.round(p.getDistanceM()))
+                .distanceM(p.getDistanceM() != null ? (int) Math.round(p.getDistanceM()) : 0)
                 .build();
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public static FacilityNearResponse toNearResponse(FacilityNearProjection p){
return FacilityNearResponse.builder()
.id(p.getId())
.facilityName(p.getFacilityName())
.introduce(p.getIntroduce())
.thumbnailUrl(p.getThumbnailUrl())
.facilityType(p.getFacilityType())
.distanceM((int) Math.round(p.getDistanceM()))
.build();
}
public static FacilityNearResponse toNearResponse(FacilityNearProjection p){
return FacilityNearResponse.builder()
.id(p.getId())
.facilityName(p.getFacilityName())
.introduce(p.getIntroduce())
.thumbnailUrl(p.getThumbnailUrl())
.facilityType(p.getFacilityType())
.distanceM(p.getDistanceM() != null ? (int) Math.round(p.getDistanceM()) : 0)
.build();
}
🤖 Prompt for AI Agents
In `@src/main/java/com/be/sportizebe/domain/facility/mapper/FacilityMapper.java`
around lines 8 - 17, Handle potential null from
FacilityNearProjection.getDistanceM() in toNearResponse: replace the direct
Math.round(p.getDistanceM()) call with a null-safe conditional so that
FacilityNearResponse.builder().distanceM(...) receives null when getDistanceM()
is null, otherwise cast the rounded double to int; update the toNearResponse
method to use p.getDistanceM() == null ? null : (int)
Math.round(p.getDistanceM()) (or equivalent) to avoid NPE.

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.be.sportizebe.domain.facility.repository;

public interface FacilityNearProjection {
Long getId();
String getFacilityName();
String getIntroduce();
String getFacilityType();
String getThumbnailUrl();
Double getDistanceM();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.be.sportizebe.domain.facility.repository;

import com.be.sportizebe.domain.facility.entity.SportsFacility;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

import java.util.List;

public interface SportsFacilityRepository extends JpaRepository<SportsFacility, Long> {

@Query(value = """
SELECT
sf.id AS id,
sf.facility_name AS facilityName,
sf.introduce AS introduce,
sf.thumbnail_url AS thumbnailUrl,
sf.facility_type AS facilityType,
ST_Distance(
sf.location,
ST_SetSRID(ST_MakePoint(:lng, :lat), 4326)::geography
) AS distanceM
FROM sports_facilities sf
WHERE ST_DWithin(
sf.location,
ST_SetSRID(ST_MakePoint(:lng, :lat), 4326)::geography,
:radiusM
)
AND (:type IS NULL OR sf.facility_type = :type)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find FacilityType enum definition
echo "=== Finding FacilityType enum ==="
fd "FacilityType.java" --exec cat {}

echo -e "\n=== Checking facility type enum storage ==="
find . -type f -name "*.java" | xargs grep -l "FacilityType" | head -5

echo -e "\n=== Checking repository query handling ==="
fd "SportsFacilityRepository.java" --exec cat {}

Repository: Sportize/be

Length of output: 2017


🏁 Script executed:

#!/bin/bash
# Search for calls to findNear method
echo "=== Searching for findNear method calls ==="
rg "findNear" --type java -B 2 -A 5

# Check SportsFacility entity and facility_type field
echo -e "\n=== Checking SportsFacility entity ==="
fd "SportsFacility.java" --exec cat {}

# Check for controller handling of type parameter
echo -e "\n=== Searching for facility type parameter in controllers ==="
rg "@RequestParam|@Param|type.*String" --type java --glob "*Controller.java" -B 3 -A 3

Repository: Sportize/be

Length of output: 16947


클라이언트에서 전달하는 type 값은 반드시 대문자(예: SOCCER)로 변환해야 함

현재 컨트롤러에서 type 파라미터를 그대로 리포지토리에 전달하는데, 데이터베이스는 @Enumerated(EnumType.STRING) 설정으로 인해 대문자 enum 값(SOCCER, BASKETBALL 등)만 저장되어 있습니다. SQL의 facility_type = :type 비교는 대소문자를 구분하므로, 클라이언트가 소문자나 혼합 케이스로 입력하면 쿼리 결과가 없을 것입니다. 서비스 또는 컨트롤러에서 type.toUpperCase() 처리를 추가하거나, API 문서에서 대문자만 허용되는 것을 명시하세요.

🤖 Prompt for AI Agents
In
`@src/main/java/com/be/sportizebe/domain/facility/repository/SportsFacilityRepository.java`
at line 29, The query in SportsFacilityRepository uses facility_type = :type
which is case-sensitive; ensure the incoming type parameter is normalized to
uppercase before reaching the repository (e.g., call type = type.toUpperCase()
in the controller method that accepts the request or in the service method that
delegates to SportsFacilityRepository), or validate/convert the incoming enum
string and reject invalid values; update the controller/service method that
calls the repository (the request handler that forwards the type param) to
perform the uppercase conversion so the repository comparison matches the
EnumType.STRING values like SOCCER/BASKETBALL.

ORDER BY distanceM
LIMIT :limit
""", nativeQuery = true)
Comment on lines +12 to +32
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check for spatial index on sports_facilities table location column

echo "=== Searching for GIST index creation ==="
fd -e sql | xargs grep -i "create.*index.*gist" 2>/dev/null | head -20

echo ""
echo "=== Searching for index on location column ==="
fd -e sql | xargs grep -i "location" 2>/dev/null | grep -i "index" | head -20

echo ""
echo "=== Searching for sports_facilities table migration ==="
fd -e sql | xargs grep -i "sports_facilities" 2>/dev/null

echo ""
echo "=== Checking Java migration files ==="
fd -e java | xargs grep -l -i "sports_facilities\|location.*index" 2>/dev/null

Repository: Sportize/be

Length of output: 404


🏁 Script executed:

#!/bin/bash
# Check for different migration approaches

echo "=== Looking for migration directories ==="
find . -type d \( -name migration -o -name migrations -o -name flyway -o -name db \) 2>/dev/null

echo ""
echo "=== Checking for Flyway V*.sql files ==="
find . -name "V*.sql" -o -name "V*.sql" 2>/dev/null

echo ""
echo "=== Checking for Liquibase files ==="
find . -name "*changelog*" -o -name "*.xml" -path "*/db/*" 2>/dev/null | head -20

echo ""
echo "=== Checking SportsFacility entity for index annotations ==="
cat src/main/java/com/be/sportizebe/domain/facility/entity/SportsFacility.java | head -100

echo ""
echo "=== Checking application properties for migration ==="
find . -name "application*.properties" -o -name "application*.yml" 2>/dev/null | xargs grep -i "flyway\|liquibase" 2>/dev/null

Repository: Sportize/be

Length of output: 1743


🏁 Script executed:

#!/bin/bash
# Check for index creation and schema initialization

echo "=== Searching for `@Index` annotations ==="
rg "@Index" --type java

echo ""
echo "=== Searching for schema.sql or database.sql ==="
find . -name "schema.sql" -o -name "database.sql" -o -name "init.sql" 2>/dev/null

echo ""
echo "=== Checking for Hibernate index creation in entity files ==="
rg "indexes\s*=|@Index" --type java -A 3

echo ""
echo "=== Checking all Java entity files for index configuration ==="
find . -path "*/entity/*.java" -type f | xargs grep -l "Index\|index" 2>/dev/null

Repository: Sportize/be

Length of output: 1022


🏁 Script executed:

#!/bin/bash
# Final verification - check SportsFacility entity for any index definitions we might have missed

echo "=== Full SportsFacility entity check ==="
cat src/main/java/com/be/sportizebe/domain/facility/entity/SportsFacility.java | grep -A 2 -B 2 "Table\|Index\|location"

Repository: Sportize/be

Length of output: 525


공간 인덱스 생성 필수입니다.

PostGIS 쿼리 구현은 정확합니다. ST_MakePoint(:lng, :lat) 순서와 geography 타입 캐스팅이 올바르게 적용되어 있습니다.

그러나 location 컬럼에 공간 인덱스(GIST)가 존재하지 않습니다. ST_DWithin 쿼리는 인덱스 없이 전체 행을 스캔하게 되어 데이터가 증가하면 심각한 성능 저하를 초래합니다. 프로젝트에서 다른 엔티티(ChatMessage)에서 @Index 애너테이션을 사용하고 있으므로, SportsFacility 엔티티의 location 컬럼에도 공간 인덱스를 추가해야 합니다.

`@Table`(name = "sports_facilities", indexes = {
    `@Index`(name = "idx_sports_facilities_location", columnList = "location", columnDefinition = "USING GIST")
})
🤖 Prompt for AI Agents
In
`@src/main/java/com/be/sportizebe/domain/facility/repository/SportsFacilityRepository.java`
around lines 12 - 32, The SportsFacility entity is missing a spatial GIST index
on the location column, causing ST_DWithin queries (used in
SportsFacilityRepository) to do full table scans; add a table-level index
annotation to the SportsFacility entity such as declaring `@Table`(... indexes = {
`@Index`(name = "idx_sports_facilities_location", columnList = "location",
columnDefinition = "USING GIST") }) so the location column gets a PostGIS GIST
index, and also ensure the corresponding DB migration (CREATE INDEX CONCURRENTLY
... USING GIST (location)) is applied in your migration scripts if you use
schema migrations.

List<FacilityNearProjection> findNear(
@Param("lat") double lat,
@Param("lng") double lng,
@Param("radiusM") long radiusM,
@Param("limit") int limit,
@Param("type") String type
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.be.sportizebe.domain.facility.service;

import com.be.sportizebe.domain.facility.dto.FacilityNearResponse;
import com.be.sportizebe.domain.facility.mapper.FacilityMapper;
import com.be.sportizebe.domain.facility.repository.FacilityNearProjection;
import com.be.sportizebe.domain.facility.repository.SportsFacilityRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class SportsFacilityService {

private final SportsFacilityRepository sportsFacilityRepository;

public List<FacilityNearResponse> getNear(
double lat,
double lng,
long radiusM,
int limit,
String type
) {
if (radiusM <= 0) radiusM = 1000;
if (radiusM > 20000) radiusM = 20000;
if (limit <= 0) limit = 50;
if (limit > 200) limit = 200;

List<FacilityNearProjection> rows =
sportsFacilityRepository.findNear(lat, lng, radiusM, limit, type);

return rows.stream()
.map(FacilityMapper::toNearResponse)
.toList();
}
}