feat: 스팟/체크인/한줄평 API 및 로컬(H2) 실행 프로파일
- GET /api/spots, POST /{id}/checkins, GET /{id}/mates, GET·POST /{id}/reviews
- Spot/CheckIn/SpotReview 엔티티·서비스 (체크인 2시간 TTL, 만료 비노출)
- CORS 허용(Vite dev, Capacitor iOS/AOS)
- local 프로파일: Postgres 없이 H2로 기동 + 시드 데이터 (연동 검증용)
- V2 마이그레이션: spots 위경도 컬럼 추가, geom NOT NULL 완화
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+3
-1
@@ -31,9 +31,11 @@ dependencies {
|
||||
|
||||
runtimeOnly 'org.postgresql:postgresql'
|
||||
|
||||
// H2: 로컬 실행 프로파일(local) 및 테스트용 인메모리 DB
|
||||
runtimeOnly 'com.h2database:h2'
|
||||
|
||||
testImplementation 'org.springframework.boot:spring-boot-starter-test'
|
||||
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
|
||||
testRuntimeOnly 'com.h2database:h2'
|
||||
}
|
||||
|
||||
tasks.named('test') {
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
package com.dog.dognation.api;
|
||||
|
||||
import com.dog.dognation.api.dto.SpotDtos.CheckInRequest;
|
||||
import com.dog.dognation.api.dto.SpotDtos.CheckInResponse;
|
||||
import com.dog.dognation.api.dto.SpotDtos.MateResponse;
|
||||
import com.dog.dognation.api.dto.SpotDtos.ReviewCreateRequest;
|
||||
import com.dog.dognation.api.dto.SpotDtos.ReviewResponse;
|
||||
import com.dog.dognation.api.dto.SpotDtos.SpotResponse;
|
||||
import com.dog.dognation.domain.spot.SpotService;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/spots")
|
||||
public class SpotController {
|
||||
|
||||
private final SpotService spotService;
|
||||
|
||||
public SpotController(SpotService spotService) {
|
||||
this.spotService = spotService;
|
||||
}
|
||||
|
||||
/** 스팟 목록 */
|
||||
@GetMapping
|
||||
public List<SpotResponse> list() {
|
||||
return spotService.listSpots().stream().map(SpotResponse::from).toList();
|
||||
}
|
||||
|
||||
/** 스팟 체크인 ("나 지금 여기 도착!") */
|
||||
@PostMapping("/{id}/checkins")
|
||||
public ResponseEntity<CheckInResponse> checkIn(@PathVariable Long id,
|
||||
@Valid @RequestBody CheckInRequest req) {
|
||||
CheckInResponse body = CheckInResponse.from(
|
||||
spotService.checkIn(id, req.userId(), req.dogId()));
|
||||
return ResponseEntity.status(HttpStatus.CREATED).body(body);
|
||||
}
|
||||
|
||||
/** 스팟에 현재 체크인 중인 메이트 목록 */
|
||||
@GetMapping("/{id}/mates")
|
||||
public List<MateResponse> mates(@PathVariable Long id) {
|
||||
return spotService.activeMates(id).stream().map(MateResponse::from).toList();
|
||||
}
|
||||
|
||||
/** 스팟 한줄평 목록 */
|
||||
@GetMapping("/{id}/reviews")
|
||||
public List<ReviewResponse> reviews(@PathVariable Long id) {
|
||||
return spotService.reviews(id).stream().map(ReviewResponse::from).toList();
|
||||
}
|
||||
|
||||
/** 스팟 한줄평 작성 */
|
||||
@PostMapping("/{id}/reviews")
|
||||
public ResponseEntity<ReviewResponse> addReview(@PathVariable Long id,
|
||||
@Valid @RequestBody ReviewCreateRequest req) {
|
||||
ReviewResponse body = ReviewResponse.from(
|
||||
spotService.addReview(id, req.userId(), req.tag(), req.content()));
|
||||
return ResponseEntity.status(HttpStatus.CREATED).body(body);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package com.dog.dognation.api.dto;
|
||||
|
||||
import com.dog.dognation.domain.dog.Dog;
|
||||
import com.dog.dognation.domain.spot.CheckIn;
|
||||
import com.dog.dognation.domain.spot.Spot;
|
||||
import com.dog.dognation.domain.spot.SpotReview;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
|
||||
/** 스팟 관련 요청/응답 DTO 모음. */
|
||||
public final class SpotDtos {
|
||||
|
||||
private SpotDtos() {
|
||||
}
|
||||
|
||||
public record SpotResponse(Long id, String name, String type, Double latitude, Double longitude) {
|
||||
public static SpotResponse from(Spot s) {
|
||||
return new SpotResponse(s.getId(), s.getName(), s.getType(), s.getLatitude(), s.getLongitude());
|
||||
}
|
||||
}
|
||||
|
||||
public record MateResponse(Long dogId, String name, String breed) {
|
||||
public static MateResponse from(Dog d) {
|
||||
return new MateResponse(d.getId(), d.getName(), d.getBreed());
|
||||
}
|
||||
}
|
||||
|
||||
public record ReviewResponse(Long id, String tag, String content, OffsetDateTime createdAt) {
|
||||
public static ReviewResponse from(SpotReview r) {
|
||||
return new ReviewResponse(r.getId(), r.getTag(), r.getContent(), r.getCreatedAt());
|
||||
}
|
||||
}
|
||||
|
||||
public record CheckInResponse(Long id, Long spotId, Long dogId, OffsetDateTime expiresAt) {
|
||||
public static CheckInResponse from(CheckIn c) {
|
||||
return new CheckInResponse(c.getId(), c.getSpotId(), c.getDogId(), c.getExpiresAt());
|
||||
}
|
||||
}
|
||||
|
||||
public record CheckInRequest(
|
||||
@NotNull(message = "userId 는 필수입니다.") Long userId,
|
||||
@NotNull(message = "dogId 는 필수입니다.") Long dogId) {
|
||||
}
|
||||
|
||||
public record ReviewCreateRequest(
|
||||
@NotNull(message = "userId 는 필수입니다.") Long userId,
|
||||
String tag,
|
||||
String content) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package com.dog.dognation.config;
|
||||
|
||||
import com.dog.dognation.domain.spot.SpotService;
|
||||
import org.springframework.boot.CommandLineRunner;
|
||||
import org.springframework.context.annotation.Profile;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* local 프로파일 전용 시드 데이터.
|
||||
* 프론트-백 연동을 곧바로 확인할 수 있도록 유저/강아지/스팟/체크인/한줄평을 채운다.
|
||||
*/
|
||||
@Component
|
||||
@Profile("local")
|
||||
public class LocalSeedData implements CommandLineRunner {
|
||||
|
||||
private final JdbcTemplate jdbc;
|
||||
private final SpotService spotService;
|
||||
|
||||
public LocalSeedData(JdbcTemplate jdbc, SpotService spotService) {
|
||||
this.jdbc = jdbc;
|
||||
this.spotService = spotService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run(String... args) {
|
||||
// 유저 (FREE / PREMIUM)
|
||||
long me = insertUser("me@dog.com", "나", "FREE");
|
||||
long other = insertUser("mate@dog.com", "이웃", "PREMIUM");
|
||||
|
||||
// 강아지
|
||||
long myDog = insertDog(me, "몽이", "푸들");
|
||||
long mate1 = insertDog(other, "콩이", "말티즈");
|
||||
long mate2 = insertDog(other, "보리", "리트리버");
|
||||
|
||||
// 스팟
|
||||
long hangang = insertSpot("한강공원 산책로", "TRAIL", 37.5283, 126.9327);
|
||||
insertSpot("서울숲 반려견 놀이터", "PLAYGROUND", 37.5445, 127.0374);
|
||||
|
||||
// 이웃 강아지들이 한강공원에 체크인 중
|
||||
spotService.checkIn(hangang, other, mate1);
|
||||
spotService.checkIn(hangang, other, mate2);
|
||||
|
||||
// 한줄평
|
||||
spotService.addReview(hangang, other, "진드기 많아요", null);
|
||||
spotService.addReview(hangang, other, "그늘 시원해요", null);
|
||||
spotService.addReview(hangang, me, "물그릇 있음", null);
|
||||
|
||||
System.out.printf("[local seed] me=%d, myDog=%d, spot(한강)=%d 준비 완료%n", me, myDog, hangang);
|
||||
}
|
||||
|
||||
private long insertUser(String email, String nickname, String tier) {
|
||||
jdbc.update("""
|
||||
INSERT INTO users(email, password_hash, nickname, subscription_tier, status, created_at, updated_at)
|
||||
VALUES (?, 'x', ?, ?, 'ACTIVE', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
||||
""", email, nickname, tier);
|
||||
return jdbc.queryForObject("SELECT id FROM users WHERE email = ?", Long.class, email);
|
||||
}
|
||||
|
||||
private long insertDog(long userId, String name, String breed) {
|
||||
jdbc.update("""
|
||||
INSERT INTO dogs(user_id, name, breed, neutered, created_at)
|
||||
VALUES (?, ?, ?, false, CURRENT_TIMESTAMP)
|
||||
""", userId, name, breed);
|
||||
return jdbc.queryForObject(
|
||||
"SELECT id FROM dogs WHERE user_id = ? AND name = ?", Long.class, userId, name);
|
||||
}
|
||||
|
||||
private long insertSpot(String name, String type, double lat, double lng) {
|
||||
jdbc.update("""
|
||||
INSERT INTO spots(name, type, latitude, longitude, created_at)
|
||||
VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP)
|
||||
""", name, type, lat, lng);
|
||||
return jdbc.queryForObject("SELECT id FROM spots WHERE name = ?", Long.class, name);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.dog.dognation.config;
|
||||
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.servlet.config.annotation.CorsRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
/** 프론트엔드(Vite dev / Capacitor 웹뷰)에서의 크로스 오리진 호출 허용. */
|
||||
@Configuration
|
||||
public class WebConfig implements WebMvcConfigurer {
|
||||
|
||||
@Override
|
||||
public void addCorsMappings(CorsRegistry registry) {
|
||||
registry.addMapping("/api/**")
|
||||
.allowedOrigins(
|
||||
"http://localhost:9000", // Vite dev server
|
||||
"http://localhost", // Capacitor(Android)
|
||||
"capacitor://localhost", // Capacitor(iOS)
|
||||
"ionic://localhost"
|
||||
)
|
||||
.allowedMethods("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS")
|
||||
.allowedHeaders("*");
|
||||
}
|
||||
}
|
||||
@@ -60,4 +60,8 @@ public class Dog {
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public String getBreed() {
|
||||
return breed;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
package com.dog.dognation.domain.spot;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Table;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
|
||||
/** 스팟 체크인. 휘발성: expiresAt 이 지나면 만료(비노출). */
|
||||
@Entity
|
||||
@Table(name = "check_ins")
|
||||
public class CheckIn {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(name = "spot_id", nullable = false)
|
||||
private Long spotId;
|
||||
|
||||
@Column(name = "user_id", nullable = false)
|
||||
private Long userId;
|
||||
|
||||
@Column(name = "dog_id", nullable = false)
|
||||
private Long dogId;
|
||||
|
||||
@Column(name = "checked_in_at", nullable = false)
|
||||
private OffsetDateTime checkedInAt = OffsetDateTime.now();
|
||||
|
||||
@Column(name = "expires_at", nullable = false)
|
||||
private OffsetDateTime expiresAt;
|
||||
|
||||
protected CheckIn() {
|
||||
}
|
||||
|
||||
public CheckIn(Long spotId, Long userId, Long dogId, OffsetDateTime expiresAt) {
|
||||
this.spotId = spotId;
|
||||
this.userId = userId;
|
||||
this.dogId = dogId;
|
||||
this.checkedInAt = OffsetDateTime.now();
|
||||
this.expiresAt = expiresAt;
|
||||
}
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public Long getSpotId() {
|
||||
return spotId;
|
||||
}
|
||||
|
||||
public Long getDogId() {
|
||||
return dogId;
|
||||
}
|
||||
|
||||
public OffsetDateTime getExpiresAt() {
|
||||
return expiresAt;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.dog.dognation.domain.spot;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.List;
|
||||
|
||||
public interface CheckInRepository extends JpaRepository<CheckIn, Long> {
|
||||
|
||||
/** 스팟에 현재 유효한(만료 전) 체크인 목록. */
|
||||
List<CheckIn> findBySpotIdAndExpiresAtAfterOrderByCheckedInAtDesc(
|
||||
Long spotId, OffsetDateTime now);
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package com.dog.dognation.domain.spot;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Table;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
|
||||
/**
|
||||
* 가상 스팟(Anchor). 실시간 GPS 동선 대신 고정 스팟 좌표만 저장한다.
|
||||
* (PostGIS geom 컬럼은 공간쿼리용으로 스키마에 유지하되, 엔티티는 lat/lng 만 매핑.)
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "spots")
|
||||
public class Spot {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(nullable = false, length = 150)
|
||||
private String name;
|
||||
|
||||
@Column(nullable = false, length = 30)
|
||||
private String type = "PARK";
|
||||
|
||||
private Double latitude;
|
||||
|
||||
private Double longitude;
|
||||
|
||||
@Column(name = "created_at", nullable = false)
|
||||
private OffsetDateTime createdAt = OffsetDateTime.now();
|
||||
|
||||
protected Spot() {
|
||||
}
|
||||
|
||||
public Spot(String name, String type, Double latitude, Double longitude) {
|
||||
this.name = name;
|
||||
this.type = type;
|
||||
this.latitude = latitude;
|
||||
this.longitude = longitude;
|
||||
this.createdAt = OffsetDateTime.now();
|
||||
}
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public Double getLatitude() {
|
||||
return latitude;
|
||||
}
|
||||
|
||||
public Double getLongitude() {
|
||||
return longitude;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package com.dog.dognation.domain.spot;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface SpotRepository extends JpaRepository<Spot, Long> {
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package com.dog.dognation.domain.spot;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Table;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
|
||||
/** 스팟 태그형 한줄평 (예: "진드기 많아요", "풀숲 무성함"). */
|
||||
@Entity
|
||||
@Table(name = "spot_reviews")
|
||||
public class SpotReview {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(name = "spot_id", nullable = false)
|
||||
private Long spotId;
|
||||
|
||||
@Column(name = "user_id", nullable = false)
|
||||
private Long userId;
|
||||
|
||||
@Column(length = 50)
|
||||
private String tag;
|
||||
|
||||
@Column(length = 200)
|
||||
private String content;
|
||||
|
||||
@Column(name = "created_at", nullable = false)
|
||||
private OffsetDateTime createdAt = OffsetDateTime.now();
|
||||
|
||||
protected SpotReview() {
|
||||
}
|
||||
|
||||
public SpotReview(Long spotId, Long userId, String tag, String content) {
|
||||
this.spotId = spotId;
|
||||
this.userId = userId;
|
||||
this.tag = tag;
|
||||
this.content = content;
|
||||
this.createdAt = OffsetDateTime.now();
|
||||
}
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getTag() {
|
||||
return tag;
|
||||
}
|
||||
|
||||
public String getContent() {
|
||||
return content;
|
||||
}
|
||||
|
||||
public OffsetDateTime getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.dog.dognation.domain.spot;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface SpotReviewRepository extends JpaRepository<SpotReview, Long> {
|
||||
|
||||
List<SpotReview> findBySpotIdOrderByCreatedAtDesc(Long spotId);
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package com.dog.dognation.domain.spot;
|
||||
|
||||
import com.dog.dognation.domain.dog.Dog;
|
||||
import com.dog.dognation.domain.dog.DogRepository;
|
||||
import jakarta.persistence.EntityNotFoundException;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
public class SpotService {
|
||||
|
||||
/** 체크인 유효시간(분). 이 시간이 지나면 스팟에서 자동 비노출. */
|
||||
private static final int CHECKIN_TTL_MINUTES = 120;
|
||||
|
||||
private final SpotRepository spotRepository;
|
||||
private final CheckInRepository checkInRepository;
|
||||
private final SpotReviewRepository reviewRepository;
|
||||
private final DogRepository dogRepository;
|
||||
|
||||
public SpotService(SpotRepository spotRepository,
|
||||
CheckInRepository checkInRepository,
|
||||
SpotReviewRepository reviewRepository,
|
||||
DogRepository dogRepository) {
|
||||
this.spotRepository = spotRepository;
|
||||
this.checkInRepository = checkInRepository;
|
||||
this.reviewRepository = reviewRepository;
|
||||
this.dogRepository = dogRepository;
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<Spot> listSpots() {
|
||||
return spotRepository.findAll();
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public CheckIn checkIn(Long spotId, Long userId, Long dogId) {
|
||||
if (!spotRepository.existsById(spotId)) {
|
||||
throw new EntityNotFoundException("스팟을 찾을 수 없습니다: " + spotId);
|
||||
}
|
||||
if (!dogRepository.existsById(dogId)) {
|
||||
throw new EntityNotFoundException("강아지를 찾을 수 없습니다: " + dogId);
|
||||
}
|
||||
OffsetDateTime expiresAt = OffsetDateTime.now().plusMinutes(CHECKIN_TTL_MINUTES);
|
||||
return checkInRepository.save(new CheckIn(spotId, userId, dogId, expiresAt));
|
||||
}
|
||||
|
||||
/** 스팟에 현재 체크인 중인 강아지(메이트) 목록. */
|
||||
@Transactional(readOnly = true)
|
||||
public List<Dog> activeMates(Long spotId) {
|
||||
List<CheckIn> active = checkInRepository
|
||||
.findBySpotIdAndExpiresAtAfterOrderByCheckedInAtDesc(spotId, OffsetDateTime.now());
|
||||
List<Long> dogIds = active.stream().map(CheckIn::getDogId).distinct().toList();
|
||||
return dogRepository.findAllById(dogIds);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<SpotReview> reviews(Long spotId) {
|
||||
return reviewRepository.findBySpotIdOrderByCreatedAtDesc(spotId);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public SpotReview addReview(Long spotId, Long userId, String tag, String content) {
|
||||
if (!spotRepository.existsById(spotId)) {
|
||||
throw new EntityNotFoundException("스팟을 찾을 수 없습니다: " + spotId);
|
||||
}
|
||||
if ((tag == null || tag.isBlank()) && (content == null || content.isBlank())) {
|
||||
throw new IllegalArgumentException("태그 또는 내용 중 하나는 입력해야 합니다.");
|
||||
}
|
||||
return reviewRepository.save(new SpotReview(spotId, userId, tag, content));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
# 로컬 실행 프로파일 — Postgres/PostGIS 없이 인메모리 H2 로 앱을 띄운다.
|
||||
# 프론트-백 연동을 로컬에서 검증하기 위한 용도. (PostGIS 공간쿼리는 미포함)
|
||||
# 실행: SPRING_PROFILES_ACTIVE=local ./gradlew bootRun
|
||||
spring:
|
||||
datasource:
|
||||
url: jdbc:h2:mem:dognation;MODE=PostgreSQL;DB_CLOSE_DELAY=-1
|
||||
driver-class-name: org.h2.Driver
|
||||
username: sa
|
||||
password:
|
||||
|
||||
jpa:
|
||||
hibernate:
|
||||
ddl-auto: create-drop
|
||||
properties:
|
||||
hibernate:
|
||||
dialect: org.hibernate.dialect.H2Dialect
|
||||
|
||||
flyway:
|
||||
enabled: false
|
||||
|
||||
h2:
|
||||
console:
|
||||
enabled: true # http://localhost:8080/h2-console
|
||||
@@ -0,0 +1,9 @@
|
||||
-- 앱 레벨에서 다루기 쉬운 위경도 컬럼 추가.
|
||||
-- 공간 인덱스/반경검색은 기존 geom(geography) 을 계속 사용하고,
|
||||
-- geom 은 애플리케이션에서 lat/lng 로부터 채운다(또는 트리거로 동기화 예정).
|
||||
ALTER TABLE spots ADD COLUMN latitude DOUBLE PRECISION;
|
||||
ALTER TABLE spots ADD COLUMN longitude DOUBLE PRECISION;
|
||||
|
||||
-- 엔티티가 geom 을 매핑하지 않으므로 JPA 저장 시 null 이 되도록 NOT NULL 완화.
|
||||
-- (lat/lng 로부터 geom 백필은 추후 트리거/배치로 처리)
|
||||
ALTER TABLE spots ALTER COLUMN geom DROP NOT NULL;
|
||||
Reference in New Issue
Block a user