-
Notifications
You must be signed in to change notification settings - Fork 0
✨ Feat: 체육시설 도메인 및 위치 기반 조회 기능 구현 #28
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -38,3 +38,6 @@ src/main/resources/*.yml | |
|
|
||
| ### VS Code ### | ||
| .vscode/ | ||
|
|
||
| ## local docker compose | ||
| docker/docker-compose.local.yml | ||
| 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; | ||
| } | ||
| } | ||
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
🐛 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
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
| 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) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 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 3Repository: Sportize/be Length of output: 16947 클라이언트에서 전달하는 현재 컨트롤러에서 🤖 Prompt for AI Agents |
||
| ORDER BY distanceM | ||
| LIMIT :limit | ||
| """, nativeQuery = true) | ||
|
Comment on lines
+12
to
+32
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 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/nullRepository: 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/nullRepository: 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/nullRepository: 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 쿼리 구현은 정확합니다. 그러나 🤖 Prompt for AI Agents |
||
| 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(); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
🤖 Prompt for AI Agents