Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
4777f9f
Merge pull request #92 from catchtable-clone/develop
jaebeom79 May 26, 2026
17a08c8
Merge pull request #97 from catchtable-clone/develop
jaebeom79 May 27, 2026
e78f6c8
Merge pull request #100 from catchtable-clone/develop
jaebeom79 May 28, 2026
02fe3df
hotfix : KafkaConfig 하드코딩 제거
silkair May 29, 2026
91cf950
hotfix : KafkaConfig 하드코딩 제거
silkair May 29, 2026
200c37b
Merge pull request #106 from catchtable-clone/develop
jaebeom79 May 29, 2026
046d886
Merge pull request #107 from catchtable-clone/develop
jaebeom79 May 30, 2026
c4255b8
Merge pull request #108 from catchtable-clone/develop
jaebeom79 May 30, 2026
2c2187a
Merge pull request #109 from catchtable-clone/develop
jaebeom79 May 30, 2026
2cedecb
Merge pull request #110 from catchtable-clone/develop
jaebeom79 May 30, 2026
f8745f4
Merge pull request #111 from catchtable-clone/develop
jaebeom79 May 30, 2026
9b26106
Merge pull request #112 from catchtable-clone/develop
jaebeom79 May 30, 2026
60a361a
Merge pull request #113 from catchtable-clone/develop
jaebeom79 May 31, 2026
8c646a0
Merge pull request #114 from catchtable-clone/develop
jaebeom79 May 31, 2026
2b56748
Perf: 부하테스트 기반 성능 개선
docodocod Jun 1, 2026
80f53ca
Merge branch 'develop' into feat/k6-test-advancement-after
docodocod Jun 1, 2026
07de346
Fix: Gemini 코드리뷰 반영
docodocod Jun 1, 2026
8abbe95
Refactor: 마이그레이션 파일 통합 및 after-changes 문서 업데이트
docodocod Jun 1, 2026
e2eaebc
Add: PostGIS geography 컬럼 운영 서버용 단일 마이그레이션 스크립트
docodocod Jun 1, 2026
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
32 changes: 32 additions & 0 deletions data/migration_v2_postgis_geography.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
-- PostGIS geography 컬럼 추가 + GIST 인덱스 마이그레이션
-- 실행: docker exec -i catchtable-db psql -U $DB_USER -d $DB_NAME < data/migration_v2_postgis_geography.sql
-- 로컬에서 이미 실행했다면 운영 서버에서만 실행할 것

-- 1. PostGIS 확장 활성화
CREATE EXTENSION IF NOT EXISTS postgis;

-- 2. stores 테이블에 geography 컬럼 추가
-- geometry 대신 geography 사용: ::geography 캐스팅 없이 GIST 인덱스 직접 활용 가능
ALTER TABLE stores
ADD COLUMN IF NOT EXISTS location geography(Point, 4326);

-- 3. 기존 latitude/longitude 데이터를 geography 컬럼으로 복사
UPDATE stores
SET location = ST_SetSRID(ST_MakePoint(longitude, latitude), 4326)::geography
WHERE location IS NULL
AND latitude IS NOT NULL
AND longitude IS NOT NULL;

-- 4. GIST 공간 인덱스 생성
CREATE INDEX IF NOT EXISTS idx_stores_location_gist
ON stores USING GIST (location);

-- 5. nearby 바운딩박스 선필터용 복합 B-tree 인덱스
CREATE INDEX IF NOT EXISTS idx_stores_lat_lng
ON stores (latitude, longitude)
WHERE is_deleted = false;

-- 검증
-- SELECT pg_typeof(location) FROM stores LIMIT 1; -- geography 이어야 정상
-- SELECT COUNT(*) FROM stores WHERE location IS NULL; -- 0 이어야 정상
-- SELECT indexname FROM pg_indexes WHERE tablename = 'stores' AND indexname LIKE 'idx_stores%';
1 change: 1 addition & 0 deletions k6/01-store-browse.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ const rampDuration = new Trend('browse_ramp_duration', true);
const errorRate = new Rate('browse_error_rate');

export const options = {
summaryTrendStats: ['avg', 'min', 'med', 'max', 'p(90)', 'p(95)', 'p(99)'],
scenarios: {

// ── SCENARIO A: 이벤트 스파이크 ────────────────────────────────────────
Expand Down
3 changes: 1 addition & 2 deletions k6/02-reservation-concurrency.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,7 @@ const errorRate = new Rate('reservation_error_rate');
const reserveDuration = new Trend('reservation_duration', true);

export const options = {
// stdout 메트릭에 p99 노출 (기본은 p95까지만)
summaryTrendStats: ['avg', 'min', 'med', 'max', 'p(95)', 'p(99)'],
summaryTrendStats: ['avg', 'min', 'med', 'max', 'p(90)', 'p(95)', 'p(99)'],
scenarios: {
// 100명이 동시에 한꺼번에 예약 요청 — 스파이크 시나리오
spike: {
Expand Down
1 change: 1 addition & 0 deletions k6/03-full-flow.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ const stepDurations = {
const errorRate = new Rate('flow_error_rate');

export const options = {
summaryTrendStats: ['avg', 'min', 'med', 'max', 'p(90)', 'p(95)', 'p(99)'],
scenarios: {

// SCENARIO A: 이벤트 스파이크 — 50명 즉시 점프로 이벤트 오픈 순간 재현 (패턴 근거는 01-store-browse.js 헤더)
Expand Down
1 change: 1 addition & 0 deletions k6/05-store-list.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const rampDuration = new Trend('list_ramp_duration', true);
const errorRate = new Rate('store_list_error_rate');

export const options = {
summaryTrendStats: ['avg', 'min', 'med', 'max', 'p(90)', 'p(95)', 'p(99)'],
scenarios: {

// SCENARIO A: 이벤트 스파이크 — 50명 즉시 점프로 이벤트 오픈 순간 재현 (패턴 근거는 01-store-browse.js 헤더)
Expand Down
1 change: 1 addition & 0 deletions k6/06-store-nearby.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ const LOCATIONS = [
];

export const options = {
summaryTrendStats: ['avg', 'min', 'med', 'max', 'p(90)', 'p(95)', 'p(99)'],
scenarios: {

// SCENARIO A: 이벤트 스파이크 — 50명 즉시 점프로 이벤트 오픈 순간 재현 (패턴 근거는 01-store-browse.js 헤더)
Expand Down
9 changes: 5 additions & 4 deletions k6/07-remains.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ const STORE_IDS = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const REMAINS_SLA_MS = Number(__ENV.REMAINS_SLA_MS || 400);

export const options = {
summaryTrendStats: ['avg', 'min', 'med', 'max', 'p(90)', 'p(95)', 'p(99)'],
scenarios: {

// SCENARIO A: 이벤트 스파이크 — 50명 즉시 점프로 이벤트 오픈 순간 재현 (패턴 근거는 01-store-browse.js 헤더)
Expand Down Expand Up @@ -77,11 +78,11 @@ export default function () {
remainsDuration.add(res.timings.duration);
isSpike ? spikeDuration.add(res.timings.duration) : rampDuration.add(res.timings.duration);

const ok = check(res, {
'잔여석 200': (r) => r.status === 200,
[`응답 ${REMAINS_SLA_MS}ms 이내`]: (r) => r.timings.duration < REMAINS_SLA_MS,
});
// HTTP 성공 여부만 errorRate에 반영 — SLA 초과는 에러가 아닌 별도 지표로 관찰
const ok = check(res, { '잔여석 200': (r) => r.status === 200 });
errorRate.add(!ok);
// SLA 체크: threshold 위반 여부는 remains_duration p95/p99로 판단
check(res, { [`응답 ${REMAINS_SLA_MS}ms 이내`]: (r) => r.timings.duration < REMAINS_SLA_MS });
});

sleep(0.5);
Expand Down
13 changes: 7 additions & 6 deletions k6/08-coupon-issue.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
*
* 목적:
* - POST /coupons/{templateId}/issue 에 100명 동시 요청
* - 비관적 락(SELECT FOR UPDATE)으로 초과 발급 방지 검증
* - Redis Lua 원자 발급(Redisson)으로 초과 발급 방지 검증
* - 재고 소진 후 올바른 에러 응답 확인
*
* 기대 결과 (다중 토큰 기준):
Expand Down Expand Up @@ -60,8 +60,7 @@ const issueDuration = new Trend('coupon_issue_duration', true);
const errorRate = new Rate('coupon_error_rate');

export const options = {
// stdout 메트릭에 p99 노출 (기본은 p95까지만 → 결과 출력에서 p99=0 표시 버그 해소)
summaryTrendStats: ['avg', 'min', 'med', 'max', 'p(95)', 'p(99)'],
summaryTrendStats: ['avg', 'min', 'med', 'max', 'p(90)', 'p(95)', 'p(99)'],
scenarios: {
// 선착순 스파이크: 5초 만에 50명 동시 요청
spike: {
Expand All @@ -78,9 +77,11 @@ export const options = {
},
},
thresholds: {
coupon_issue_duration: ['p(95)<2000'], // 비관적 락 대기 포함 2초 기준
coupon_error_rate: ['rate<0.01'], // 5xx 에러만 실패 (재고 소진은 정상)
http_req_failed: ['rate<0.01'],
coupon_issue_duration: ['p(95)<2000'], // Redis Lua 발급 대기 포함 2초 기준
coupon_error_rate: ['rate<0.01'], // 5xx 에러만 실패 (재고 소진/중복은 정상 4xx)
// http_req_failed 제거: 재고 소진(400)/중복 방지(409)가 정상 응답이므로
// k6 빌트인 http_req_failed가 이를 전부 실패로 집계해 오탐 발생
// 실제 장애 판단은 coupon_error_rate(5xx 전용)로만 평가
},
};

Expand Down
1 change: 1 addition & 0 deletions k6/10-soak.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ const START_TIME = Date.now();
const LATE_PHASE_MS = 20 * 60 * 1000;

export const options = {
summaryTrendStats: ['avg', 'min', 'med', 'max', 'p(90)', 'p(95)', 'p(99)'],
scenarios: {
// 3분 워밍업 후 10VU 유지 → 30분 장시간 운영
soak: {
Expand Down
3 changes: 2 additions & 1 deletion k6/run.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,8 @@ Write-Host ""

# k6 v2.0+ 호환 — 기본은 p99만 노출하므로 p50/p90/p95도 명시 push
# 대시보드 Row A/B의 백분위 패널이 작동하려면 필수
$env:K6_PROMETHEUS_RW_TREND_STATS = 'p(50),p(90),p(95),p(99),min,max,avg'
$env:K6_PROMETHEUS_RW_TREND_STATS = 'p(50),p(90),p(95),p(99),min,max,avg'
$env:K6_PROMETHEUS_RW_PUSH_INTERVAL = '2s'

# PrometheusUrl 지정 시에만 remote_write 사용. 미지정 시 콘솔 출력만.
if ($PrometheusUrl) {
Expand Down
8 changes: 7 additions & 1 deletion src/main/java/com/catchtable/bookmark/entity/Bookmark.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,13 @@
import java.time.LocalDateTime;

@Entity
@Table(name = "bookmarks")
@Table(
name = "bookmarks",
indexes = {
@Index(name = "idx_bookmark_folder_deleted", columnList = "folder_id, is_deleted"),
@Index(name = "idx_bookmark_store_folder", columnList = "store_id, folder_id")
}
)
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.catchtable.bookmark.entity.Bookmark;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

Expand All @@ -13,6 +14,11 @@ public interface BookmarkRepository extends JpaRepository<Bookmark, Long> {
@Query("SELECT b FROM Bookmark b JOIN FETCH b.store WHERE b.folder.id = :folderId AND b.isDeleted = false")
List<Bookmark> findByFolderIdAndIsDeletedFalse(@Param("folderId") Long folderId);

// 폴더 삭제 시 벌크 소프트 삭제 — forEach UPDATE N번 → UPDATE 1번
@Modifying(clearAutomatically = true)
@Query("UPDATE Bookmark b SET b.isDeleted = true WHERE b.folder.id = :folderId AND b.isDeleted = false")
void softDeleteAllByFolderId(@Param("folderId") Long folderId);
Comment on lines +17 to +20

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

벌크 연산(@Modifying UPDATE)은 영속성 컨텍스트(1차 캐시)를 거치지 않고 데이터베이스에 직접 쿼리를 실행합니다. 이로 인해 영속성 컨텍스트에 이미 로드되어 있는 엔티티들의 상태와 실제 DB 상태 간의 불일치(Stale Data)가 발생할 수 있습니다.

안전한 처리를 위해 @Modifying(clearAutomatically = true) 옵션을 추가하여 벌크 연산 실행 후 영속성 컨텍스트를 자동으로 비워주도록 설정하는 것을 권장합니다.

Suggested change
// 폴더 삭제 시 벌크 소프트 삭제 — forEach UPDATE N번 → UPDATE 1번
@Modifying
@Query("UPDATE Bookmark b SET b.isDeleted = true WHERE b.folder.id = :folderId AND b.isDeleted = false")
void softDeleteAllByFolderId(@Param("folderId") Long folderId);
// 폴더 삭제 시 벌크 소프트 삭제 — forEach UPDATE N번 → UPDATE 1번
@Modifying(clearAutomatically = true)
@Query("UPDATE Bookmark b SET b.isDeleted = true WHERE b.folder.id = :folderId AND b.isDeleted = false")
void softDeleteAllByFolderId(@Param("folderId") Long folderId);


Optional<Bookmark> findByIdAndIsDeletedFalse(Long id);

boolean existsByFolder_IdAndStore_IdAndIsDeletedFalse(Long folderId, Long storeId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,7 @@ public BookmarkFolderUpdateResponse updateFolder(Long userId, Long folderId, Boo
public BookmarkFolderDeleteResponse deleteFolder(Long userId, Long folderId) {
BookmarkFolder folder = getEditableFolder(userId, folderId);

bookmarkRepository.findByFolderIdAndIsDeletedFalse(folderId)
.forEach(Bookmark::softDelete);
bookmarkRepository.softDeleteAllByFolderId(folderId);

folder.softDelete();

Expand Down
36 changes: 36 additions & 0 deletions src/main/java/com/catchtable/global/config/CacheConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.catchtable.global.config;

import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.time.Duration;
import java.util.Map;

@EnableCaching
@Configuration
public class CacheConfig {

@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {
RedisCacheConfiguration defaults = RedisCacheConfiguration.defaultCacheConfig()
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()))
.disableCachingNullValues();
Comment on lines +21 to +25

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

Jackson2JsonRedisSerializer<Object>(Object.class)를 사용하여 값을 직렬화하면, 캐시 데이터를 역직렬화할 때 구체적인 클래스 타입 정보가 유실되어 LinkedHashMap으로 복원됩니다. 이로 인해 StoreService.getPopularStores 등에서 캐시를 조회할 때 ClassCastException이 발생하게 됩니다.

Spring Cache 환경에서는 클래스 타입 정보를 함께 저장해 주는 GenericJackson2JsonRedisSerializer를 사용하는 것이 안전합니다.

Suggested change
public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {
RedisCacheConfiguration defaults = RedisCacheConfiguration.defaultCacheConfig()
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new Jackson2JsonRedisSerializer<>(Object.class)))
.disableCachingNullValues();
public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {
RedisCacheConfiguration defaults = RedisCacheConfiguration.defaultCacheConfig()
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer()))
.disableCachingNullValues();


Map<String, RedisCacheConfiguration> cacheConfigs = Map.of(
"popularStores", defaults.entryTtl(Duration.ofMinutes(5))
);

return RedisCacheManager.builder(connectionFactory)
.cacheDefaults(defaults.entryTtl(Duration.ofMinutes(10)))
.withInitialCacheConfigurations(cacheConfigs)
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
@Configuration
public class KafkaConfig {

@Value("${spring.kafka.bootstrap-servers:localhost:9092}")
@Value("${spring.kafka.bootstrap-servers}")
private String bootstrapServers;

private final MeterRegistry meterRegistry;
Expand Down
7 changes: 6 additions & 1 deletion src/main/java/com/catchtable/menu/entity/Menu.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,12 @@
import java.time.LocalDateTime;

@Entity
@Table(name = "menu")
@Table(
name = "menu",
indexes = {
@Index(name = "idx_menu_store_deleted", columnList = "store_id, is_deleted")
}
)
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Menu {
Expand Down
11 changes: 11 additions & 0 deletions src/main/java/com/catchtable/reservation/entity/Reservation.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,28 @@
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Index;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.PrePersist;
import jakarta.persistence.PreUpdate;
import jakarta.persistence.Table;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@Entity
@Table(
name = "reservations",
indexes = {
@Index(name = "idx_reservation_user_id", columnList = "user_id"),
@Index(name = "idx_reservation_user_status", columnList = "user_id, status"),
@Index(name = "idx_reservation_remain_id", columnList = "remain_id"),
@Index(name = "idx_reservation_status", columnList = "status")
}
)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Reservation {

Expand Down
8 changes: 7 additions & 1 deletion src/main/java/com/catchtable/review/entity/Review.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,13 @@
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Table(name = "reviews")
@Table(
name = "reviews",
indexes = {
@Index(name = "idx_review_store_deleted", columnList = "store_id, is_deleted"),
@Index(name = "idx_review_user_deleted", columnList = "user_id, is_deleted")
}
)
public class Review {

@Id
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.catchtable.review.repository;

import com.catchtable.review.entity.Review;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
Expand All @@ -9,13 +10,13 @@

public interface ReviewRepository extends JpaRepository<Review, Long> {

// 특정 매장의 삭제되지 않은 모든 리뷰 조회 (가장 최근 작성된 순)
// 특정 매장의 삭제되지 않은 리뷰 조회 — 페이징 적용 (최근 N건)
@Query("SELECT r FROM Review r JOIN FETCH r.user WHERE r.store.id = :storeId AND r.isDeleted = false ORDER BY r.createdAt DESC")
List<Review> findAllByStoreIdAndIsDeletedFalseOrderByCreatedAtDesc(@Param("storeId") Long storeId);
List<Review> findAllByStoreIdAndIsDeletedFalseOrderByCreatedAtDesc(@Param("storeId") Long storeId, Pageable pageable);

// 특정 사용자가 작성한 삭제되지 않은 모든 리뷰 조회
// 특정 사용자가 작성한 삭제되지 않은 리뷰 조회 — 페이징 적용
@Query("SELECT r FROM Review r JOIN FETCH r.store WHERE r.user.id = :userId AND r.isDeleted = false ORDER BY r.createdAt DESC")
List<Review> findAllByUserIdAndIsDeletedFalseOrderByCreatedAtDesc(@Param("userId") Long userId);
List<Review> findAllByUserIdAndIsDeletedFalseOrderByCreatedAtDesc(@Param("userId") Long userId, Pageable pageable);

// 해당 예약을 통해 이미 리뷰를 작성했는지 확인 (중복 리뷰 방지)
boolean existsByReservationIdAndIsDeletedFalse(Long reservationId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import com.catchtable.user.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.data.domain.PageRequest;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

Expand Down Expand Up @@ -81,7 +82,8 @@ public List<ReviewResponseDto> getStoreReviews(Long storeId) {
throw new CustomException(ErrorCode.STORE_NOT_FOUND);
}

List<Review> reviews = reviewRepository.findAllByStoreIdAndIsDeletedFalseOrderByCreatedAtDesc(storeId);
List<Review> reviews = reviewRepository.findAllByStoreIdAndIsDeletedFalseOrderByCreatedAtDesc(
storeId, PageRequest.of(0, 20));
return reviews.stream()
.map(ReviewResponseDto::from)
.toList();
Expand All @@ -93,7 +95,8 @@ public List<ReviewResponseDto> getMyReviews(Long userId) {
throw new CustomException(ErrorCode.USER_NOT_FOUND);
}

List<Review> reviews = reviewRepository.findAllByUserIdAndIsDeletedFalseOrderByCreatedAtDesc(userId);
List<Review> reviews = reviewRepository.findAllByUserIdAndIsDeletedFalseOrderByCreatedAtDesc(
userId, PageRequest.of(0, 20));
return reviews.stream()
.map(ReviewResponseDto::from)
.toList();
Expand Down
15 changes: 14 additions & 1 deletion src/main/java/com/catchtable/store/entity/Store.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,21 @@
import com.catchtable.global.exception.ErrorCode;
import jakarta.persistence.*;
import lombok.*;
import org.locationtech.jts.geom.Point;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;

import java.time.LocalDateTime;

@Entity
@Table(name = "stores")
@Table(
name = "stores",
indexes = {
@Index(name = "idx_store_lat_lng", columnList = "latitude, longitude"),
@Index(name = "idx_store_deleted_category", columnList = "is_deleted, category"),
@Index(name = "idx_store_deleted_district", columnList = "is_deleted, district")
}
)
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
Expand All @@ -37,6 +45,11 @@ public class Store {
@Column(nullable = false)
private Double longitude;

// PostGIS geometry 컬럼 — ST_DWithin/KNN 공간 인덱스 활용
// migration_v2_postgis_geometry.sql 실행 후 GIST 인덱스 적용됨
@Column(columnDefinition = "geography(Point, 4326)", insertable = false, updatable = false)
private Point location;
Comment on lines +48 to +51

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

데이터베이스 마이그레이션 스크립트에서 location 컬럼을 geography(Point, 4326)로 변경함에 따라, JPA 엔티티의 columnDefinition 설정도 동일하게 geography(Point, 4326)로 맞춰주어야 합니다.

Suggested change
// PostGIS geometry 컬럼 — ST_DWithin/KNN 공간 인덱스 활용
// migration_v2_postgis_geometry.sql 실행 후 GIST 인덱스 적용됨
@Column(columnDefinition = "geometry(Point, 4326)", insertable = false, updatable = false)
private Point location;
// PostGIS geography 컬럼 — ST_DWithin/KNN 공간 인덱스 활용
// migration_v2_postgis_geometry.sql 실행 후 GIST 인덱스 적용됨
@Column(columnDefinition = "geography(Point, 4326)", insertable = false, updatable = false)
private Point location;


@Column(nullable = false)
private String address;

Expand Down
Loading
Loading