diff --git a/build.gradle b/build.gradle index 9c9ed0a..fc60254 100644 --- a/build.gradle +++ b/build.gradle @@ -22,6 +22,7 @@ configurations { repositories { mavenCentral() + maven { url 'https://jitpack.io' } } dependencies { @@ -41,6 +42,7 @@ dependencies { runtimeOnly 'com.mysql:mysql-connector-j' implementation 'org.springframework.boot:spring-boot-starter-data-redis' implementation 'org.springframework.session:spring-session-data-redis' + implementation 'org.redisson:redisson-spring-boot-starter:3.23.1' // Lombok compileOnly 'org.projectlombok:lombok' @@ -60,10 +62,13 @@ dependencies { implementation 'dev.langchain4j:langchain4j:1.0.0-beta1' implementation 'dev.langchain4j:langchain4j-google-ai-gemini:1.0.0-beta1' + // S2 + implementation 'com.github.google:s2-geometry-library-java:2.0.0' + // etc implementation group: 'org.springdoc', name: 'springdoc-openapi-starter-webmvc-ui', version: '2.7.0' implementation 'net.javacrumbs.shedlock:shedlock-spring:5.1.0' - implementation 'net.javacrumbs.shedlock:shedlock-provider-jdbc-template:5.1.0' + implementation 'net.javacrumbs.shedlock:shedlock-provider-redis-spring:5.1.0' } tasks.named('test') { diff --git a/src/main/java/com/knu/ddip/common/config/RedissonConfig.java b/src/main/java/com/knu/ddip/common/config/RedissonConfig.java new file mode 100644 index 0000000..d01e04f --- /dev/null +++ b/src/main/java/com/knu/ddip/common/config/RedissonConfig.java @@ -0,0 +1,36 @@ +package com.knu.ddip.common.config; + +import org.redisson.Redisson; +import org.redisson.api.RedissonClient; +import org.redisson.config.Config; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; + +@Profile("!test") +@Configuration +public class RedissonConfig { + + @Value("${REDIS_HOST}") + private String host; + + @Value("${REDIS_PORT}") + private int port; + + @Value("${REDIS_PASSWORD}") + private String password; + + @Bean + public RedissonClient redissonClient() { + Config config = new Config(); + + config.useSingleServer() + .setAddress("redis://" + host + ":" + port) + .setPassword(password) + .setConnectionPoolSize(10) + .setConnectionMinimumIdleSize(1); + + return Redisson.create(config); + } +} diff --git a/src/main/java/com/knu/ddip/common/config/ScheduleConfig.java b/src/main/java/com/knu/ddip/common/config/ScheduleConfig.java new file mode 100644 index 0000000..1817d58 --- /dev/null +++ b/src/main/java/com/knu/ddip/common/config/ScheduleConfig.java @@ -0,0 +1,19 @@ +package com.knu.ddip.common.config; + +import net.javacrumbs.shedlock.core.LockProvider; +import net.javacrumbs.shedlock.provider.redis.spring.RedisLockProvider; +import net.javacrumbs.shedlock.spring.annotation.EnableSchedulerLock; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.scheduling.annotation.EnableScheduling; + +@EnableScheduling +@Configuration +@EnableSchedulerLock(defaultLockAtLeastFor = "30s", defaultLockAtMostFor = "1m") +public class ScheduleConfig { + @Bean + public LockProvider lockProvider(RedisConnectionFactory redisConnectionFactory) { + return new RedisLockProvider(redisConnectionFactory); + } +} diff --git a/src/main/java/com/knu/ddip/common/exception/GlobalExceptionHandler.java b/src/main/java/com/knu/ddip/common/exception/GlobalExceptionHandler.java index 982d1e4..b32edd8 100644 --- a/src/main/java/com/knu/ddip/common/exception/GlobalExceptionHandler.java +++ b/src/main/java/com/knu/ddip/common/exception/GlobalExceptionHandler.java @@ -1,6 +1,7 @@ package com.knu.ddip.common.exception; import com.knu.ddip.auth.exception.*; +import com.knu.ddip.location.exception.LocationNotFoundException; import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatusCode; import org.springframework.http.ProblemDetail; @@ -67,4 +68,12 @@ public ResponseEntity handleTokenStolenException(TokenStolenExcep return ResponseEntity.status(HttpStatusCode.valueOf(462)).body(problemDetail); } + @ExceptionHandler(LocationNotFoundException.class) + public ResponseEntity handleLocationNotFoundException(LocationNotFoundException e) { + + ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, e.getMessage()); + problemDetail.setTitle("Location Not Found"); + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(problemDetail); + } + } diff --git a/src/main/java/com/knu/ddip/location/application/dto/UpdateMyLocationRequest.java b/src/main/java/com/knu/ddip/location/application/dto/UpdateMyLocationRequest.java new file mode 100644 index 0000000..f0baac9 --- /dev/null +++ b/src/main/java/com/knu/ddip/location/application/dto/UpdateMyLocationRequest.java @@ -0,0 +1,13 @@ +package com.knu.ddip.location.application.dto; + +import lombok.Builder; + +@Builder +public record UpdateMyLocationRequest( + double lat, + double lng +) { + public static UpdateMyLocationRequest of(double lat, double lng) { + return new UpdateMyLocationRequest(lat, lng); + } +} diff --git a/src/main/java/com/knu/ddip/location/application/init/GeoJsonInitializer.java b/src/main/java/com/knu/ddip/location/application/init/GeoJsonInitializer.java new file mode 100644 index 0000000..fc9ff49 --- /dev/null +++ b/src/main/java/com/knu/ddip/location/application/init/GeoJsonInitializer.java @@ -0,0 +1,25 @@ +package com.knu.ddip.location.application.init; + +import com.knu.ddip.location.application.service.LocationService; +import lombok.RequiredArgsConstructor; +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.ApplicationRunner; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class GeoJsonInitializer implements ApplicationRunner { + + public static final String GEOJSON_INIT_LOCK_KEY = "lock:geojson:init"; + + private final OneTimeRunner oneTimeRunner; + private final LocationService locationService; + + @Override + public void run(ApplicationArguments args) { + oneTimeRunner.runOnce( + GEOJSON_INIT_LOCK_KEY, + () -> locationService.loadAndSaveGeoJsonFeatures() + ); + } +} diff --git a/src/main/java/com/knu/ddip/location/application/init/OneTimeRunner.java b/src/main/java/com/knu/ddip/location/application/init/OneTimeRunner.java new file mode 100644 index 0000000..6e6a184 --- /dev/null +++ b/src/main/java/com/knu/ddip/location/application/init/OneTimeRunner.java @@ -0,0 +1,6 @@ +package com.knu.ddip.location.application.init; + +public interface OneTimeRunner { + void runOnce(String lockName, Runnable task); +} + diff --git a/src/main/java/com/knu/ddip/location/application/scheduler/LocationScheduler.java b/src/main/java/com/knu/ddip/location/application/scheduler/LocationScheduler.java new file mode 100644 index 0000000..7faa72f --- /dev/null +++ b/src/main/java/com/knu/ddip/location/application/scheduler/LocationScheduler.java @@ -0,0 +1,26 @@ +package com.knu.ddip.location.application.scheduler; + +import com.knu.ddip.location.application.service.LocationWriter; +import lombok.RequiredArgsConstructor; +import net.javacrumbs.shedlock.spring.annotation.SchedulerLock; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class LocationScheduler { + + private final LocationWriter locationWriter; + + @SchedulerLock( + name = "cleanup_locations_lock", + lockAtLeastFor = "30s", + lockAtMostFor = "5m" + ) + @Scheduled(cron = "0 0 * * * *") // 매 정시 + public void cleanupExpiredUserLocations() { + long now = System.currentTimeMillis(); + locationWriter.cleanupExpiredUserLocations(now); + } + +} \ No newline at end of file diff --git a/src/main/java/com/knu/ddip/location/application/service/LocationReader.java b/src/main/java/com/knu/ddip/location/application/service/LocationReader.java new file mode 100644 index 0000000..ad9b7b8 --- /dev/null +++ b/src/main/java/com/knu/ddip/location/application/service/LocationReader.java @@ -0,0 +1,13 @@ +package com.knu.ddip.location.application.service; + +import java.util.List; + +public interface LocationReader { + void validateLocationByCellId(String cellId); + + List findAllLocationsByCellIdIn(List cellIds); + + List findUserIdsByCellIds(List targetCellIds); + + boolean isCellIdNotInTargetArea(String cellId); +} diff --git a/src/main/java/com/knu/ddip/location/application/service/LocationService.java b/src/main/java/com/knu/ddip/location/application/service/LocationService.java new file mode 100644 index 0000000..0e8978a --- /dev/null +++ b/src/main/java/com/knu/ddip/location/application/service/LocationService.java @@ -0,0 +1,135 @@ +package com.knu.ddip.location.application.service; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.geometry.S2CellId; +import com.knu.ddip.location.application.dto.UpdateMyLocationRequest; +import com.knu.ddip.location.application.util.S2Converter; +import com.knu.ddip.location.application.util.UuidBase64Utils; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.io.ClassPathResource; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +@Slf4j +@Service +@Transactional(readOnly = true) +@RequiredArgsConstructor +public class LocationService { + + private final LocationReader locationReader; + private final LocationWriter locationWriter; + + private final ObjectMapper objectMapper; + + public static final int LEVEL = 17; + public static final String KNU_GEOJSON_FEATURE_FILENAME = "geojson/cells.geojson"; + + // KNU GeoJSON 파일을 읽어서 각 Feature를 DB에 저장 + @Transactional + public void loadAndSaveGeoJsonFeatures() { + // GeoJson 파일 읽기 + String geoJsonContent = getGeoJsonContent(); + + try { + // 기존 데이터 삭제 + locationWriter.deleteAll(); + + // JSON 파싱 + JsonNode rootNode = objectMapper.readTree(geoJsonContent); + JsonNode featuresNode = rootNode.get("features"); + List cellIds = StreamSupport.stream(featuresNode.spliterator(), false) + .map(featureNode -> featureNode.get("properties").get("id").asText()) + .collect(Collectors.toList()); + + locationWriter.saveAll(cellIds); + + log.info("총 {}개의 S2Cell Feature가 저장되었습니다.", cellIds.size()); + } catch (IOException e) { + e.printStackTrace(); + log.error(e.getMessage()); + } + } + + public void saveUserLocationAtomic(UUID userId, UpdateMyLocationRequest request) { + S2CellId cellIdObj = S2Converter.toCellId(request.lat(), request.lng(), LEVEL); + String cellId = cellIdObj.toToken(); + + // 경북대 내부에 위치하는지 확인 + boolean cellIdNotInTargetArea = locationReader.isCellIdNotInTargetArea(cellId); + + String encodedUserId = UuidBase64Utils.uuidToBase64String(userId); + + locationWriter.saveUserIdByCellIdAtomic(cellId, cellIdNotInTargetArea, encodedUserId); + } + + // 요청 전송 시 이웃 userIds 조회 + public List getNeighborRecipientUserIds(UUID myUserId, double lat, double lng) { + S2CellId cellIdObj = S2Converter.toCellId(lat, lng, LEVEL); + String cellId = cellIdObj.toToken(); + + // 경북대 내부에 위치하는지 확인 + locationReader.validateLocationByCellId(cellId); + + // 이웃 cellIds 가져오기 + List neighbors = new ArrayList<>(); + cellIdObj.getAllNeighbors(LEVEL, neighbors); + List neighborCellIds = neighbors.stream() + .map(S2CellId::toToken) + .collect(Collectors.toList()); + + // 경북대 내부에 위치하는 이웃 cellIds만 가져오기 + List targetCellIds = locationReader.findAllLocationsByCellIdIn(neighborCellIds); + targetCellIds.add(cellId); + + // targetCellId의 userIds만 가져오기 + + List userIds = locationReader.findUserIdsByCellIds(targetCellIds).stream() + .map(UuidBase64Utils::base64StringToUuid) + .filter(userId -> !userId.equals(myUserId)) + .collect(Collectors.toList()); + + return userIds; + } + + // 초기 화면에서 인접한 요청 가져오기 (현재는 인접한 cellId 가져오는 것만 구현) + public List getNeighborCellIds(double lat, double lng) { + S2CellId cellIdObj = S2Converter.toCellId(lat, lng, LEVEL); + String cellId = cellIdObj.toToken(); + + // 경북대 내부에 위치하는지 확인 + locationReader.validateLocationByCellId(cellId); + + // 이웃 cellIds 가져오기 + List neighbors = new ArrayList<>(); + cellIdObj.getAllNeighbors(LEVEL, neighbors); + List neighborCellIds = neighbors.stream() + .map(S2CellId::toToken) + .collect(Collectors.toList()); + + // 경북대 내부에 위치하는 이웃 cellIds만 가져오기 + List targetCellIds = locationReader.findAllLocationsByCellIdIn(neighborCellIds); + targetCellIds.add(cellId); + + return targetCellIds; + } + + private String getGeoJsonContent() { + ClassPathResource resource = new ClassPathResource(KNU_GEOJSON_FEATURE_FILENAME); + try (InputStream is = resource.getInputStream()) { + return new String(is.readAllBytes(), StandardCharsets.UTF_8); + } catch (IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/main/java/com/knu/ddip/location/application/service/LocationWriter.java b/src/main/java/com/knu/ddip/location/application/service/LocationWriter.java new file mode 100644 index 0000000..2a5943c --- /dev/null +++ b/src/main/java/com/knu/ddip/location/application/service/LocationWriter.java @@ -0,0 +1,13 @@ +package com.knu.ddip.location.application.service; + +import java.util.List; + +public interface LocationWriter { + void deleteAll(); + + void saveAll(List cellIds); + + void saveUserIdByCellIdAtomic(String newCellId, boolean cellIdNotInTargetArea, String encodedUserId); + + void cleanupExpiredUserLocations(long now); +} diff --git a/src/main/java/com/knu/ddip/location/application/util/LocationKeyFactory.java b/src/main/java/com/knu/ddip/location/application/util/LocationKeyFactory.java new file mode 100644 index 0000000..86a0d9b --- /dev/null +++ b/src/main/java/com/knu/ddip/location/application/util/LocationKeyFactory.java @@ -0,0 +1,15 @@ +package com.knu.ddip.location.application.util; + +public abstract class LocationKeyFactory { + public static String createUserIdKey(String encodedUserId) { + return "user:" + encodedUserId; + } + + public static String createCellIdUsersKey(String cellId) { + return "cell:" + cellId + ":users"; + } + + public static String createCellIdExpiriesKey(String cellId) { + return "cell:" + cellId + ":expiry"; + } +} diff --git a/src/main/java/com/knu/ddip/location/application/util/S2Converter.java b/src/main/java/com/knu/ddip/location/application/util/S2Converter.java new file mode 100644 index 0000000..893f0c0 --- /dev/null +++ b/src/main/java/com/knu/ddip/location/application/util/S2Converter.java @@ -0,0 +1,12 @@ +package com.knu.ddip.location.application.util; + +import com.google.common.geometry.S2CellId; +import com.google.common.geometry.S2LatLng; + +public abstract class S2Converter { + + public static S2CellId toCellId(double lat, double lng, int level) { + S2LatLng latLng = S2LatLng.fromDegrees(lat, lng); + return S2CellId.fromLatLng(latLng).parent(level); + } +} diff --git a/src/main/java/com/knu/ddip/location/application/util/UuidBase64Utils.java b/src/main/java/com/knu/ddip/location/application/util/UuidBase64Utils.java new file mode 100644 index 0000000..d13c6e4 --- /dev/null +++ b/src/main/java/com/knu/ddip/location/application/util/UuidBase64Utils.java @@ -0,0 +1,27 @@ +package com.knu.ddip.location.application.util; + +import java.nio.ByteBuffer; +import java.util.Base64; +import java.util.UUID; + +import static java.util.Base64.getUrlDecoder; +import static java.util.Base64.getUrlEncoder; + +public abstract class UuidBase64Utils { + + private static final Base64.Encoder B64_URL = getUrlEncoder().withoutPadding(); + private static final Base64.Decoder B64_DEC = getUrlDecoder(); + + public static String uuidToBase64String(UUID uuid) { + ByteBuffer bb = ByteBuffer.allocate(16) + .putLong(uuid.getMostSignificantBits()) + .putLong(uuid.getLeastSignificantBits()); + return B64_URL.encodeToString(bb.array()); + } + + public static UUID base64StringToUuid(String string) { + byte[] bytes = B64_DEC.decode(string); + ByteBuffer bb = ByteBuffer.wrap(bytes); + return new UUID(bb.getLong(), bb.getLong()); + } +} diff --git a/src/main/java/com/knu/ddip/location/exception/LocationNotFoundException.java b/src/main/java/com/knu/ddip/location/exception/LocationNotFoundException.java new file mode 100644 index 0000000..401d957 --- /dev/null +++ b/src/main/java/com/knu/ddip/location/exception/LocationNotFoundException.java @@ -0,0 +1,7 @@ +package com.knu.ddip.location.exception; + +public class LocationNotFoundException extends RuntimeException { + public LocationNotFoundException(String message) { + super(message); + } +} diff --git a/src/main/java/com/knu/ddip/location/infrastructure/entity/LocationEntity.java b/src/main/java/com/knu/ddip/location/infrastructure/entity/LocationEntity.java new file mode 100644 index 0000000..c61495b --- /dev/null +++ b/src/main/java/com/knu/ddip/location/infrastructure/entity/LocationEntity.java @@ -0,0 +1,24 @@ +package com.knu.ddip.location.infrastructure.entity; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.*; + +@Entity +@Getter +@Builder +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Table(name = "LOCATIONS") +public class LocationEntity { + + @Id + private String cellId; + + public static LocationEntity create(String cellId) { + return LocationEntity.builder() + .cellId(cellId) + .build(); + } +} diff --git a/src/main/java/com/knu/ddip/location/infrastructure/lock/RedisOneTimeRunner.java b/src/main/java/com/knu/ddip/location/infrastructure/lock/RedisOneTimeRunner.java new file mode 100644 index 0000000..8c97c0c --- /dev/null +++ b/src/main/java/com/knu/ddip/location/infrastructure/lock/RedisOneTimeRunner.java @@ -0,0 +1,37 @@ +package com.knu.ddip.location.infrastructure.lock; + +import com.knu.ddip.location.application.init.OneTimeRunner; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.redisson.api.RLock; +import org.redisson.api.RedissonClient; +import org.springframework.stereotype.Component; + +import java.util.concurrent.TimeUnit; + +@Slf4j +@Component +@RequiredArgsConstructor +public class RedisOneTimeRunner implements OneTimeRunner { + + private final RedissonClient redisson; + + @Override + public void runOnce(String lockName, Runnable task) { + RLock lock = redisson.getLock(lockName); + boolean acquired = false; + try { + acquired = lock.tryLock(0, 30, TimeUnit.MINUTES); + if (!acquired) { + return; + } + task.run(); + } catch (InterruptedException e) { + log.error("Interrupted while waiting for lock {}", lockName, e); + } finally { + if (acquired && lock.isHeldByCurrentThread()) { + lock.unlock(); + } + } + } +} diff --git a/src/main/java/com/knu/ddip/location/infrastructure/repository/LocationJpaRepository.java b/src/main/java/com/knu/ddip/location/infrastructure/repository/LocationJpaRepository.java new file mode 100644 index 0000000..bbc8176 --- /dev/null +++ b/src/main/java/com/knu/ddip/location/infrastructure/repository/LocationJpaRepository.java @@ -0,0 +1,12 @@ +package com.knu.ddip.location.infrastructure.repository; + +import com.knu.ddip.location.infrastructure.entity.LocationEntity; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface LocationJpaRepository extends JpaRepository { + List findAllByCellIdIn(List cellIds); +} diff --git a/src/main/java/com/knu/ddip/location/infrastructure/repository/LocationReaderImpl.java b/src/main/java/com/knu/ddip/location/infrastructure/repository/LocationReaderImpl.java new file mode 100644 index 0000000..a2074c6 --- /dev/null +++ b/src/main/java/com/knu/ddip/location/infrastructure/repository/LocationReaderImpl.java @@ -0,0 +1,73 @@ +package com.knu.ddip.location.infrastructure.repository; + +import com.knu.ddip.location.application.service.LocationReader; +import com.knu.ddip.location.application.util.LocationKeyFactory; +import com.knu.ddip.location.exception.LocationNotFoundException; +import com.knu.ddip.location.infrastructure.entity.LocationEntity; +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.connection.RedisConnection; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Repository; + +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + +@Repository +@RequiredArgsConstructor +public class LocationReaderImpl implements LocationReader { + + private final LocationJpaRepository locationJpaRepository; + private final RedisTemplate redisTemplate; + + @Override + public void validateLocationByCellId(String cellId) { + locationJpaRepository.findById(cellId) + .orElseThrow(() -> new LocationNotFoundException("위치를 찾을 수 없습니다.")); + } + + @Override + public List findAllLocationsByCellIdIn(List cellIds) { + return locationJpaRepository.findAllByCellIdIn(cellIds).stream() + .map(LocationEntity::getCellId) + .collect(Collectors.toList()); + } + + @Override + public List findUserIdsByCellIds(List targetCellIds) { + RedisConnectionFactory connectionFactory = redisTemplate.getConnectionFactory(); + + if (connectionFactory == null) return null; // throw xxx + + try (RedisConnection conn = connectionFactory.getConnection()) { + conn.openPipeline(); + + List keys = targetCellIds.stream() + .map(LocationKeyFactory::createCellIdUsersKey) // "cell:{id}:users" 형태 + .map(k -> k.getBytes(StandardCharsets.UTF_8)) + .collect(Collectors.toList()); + + for (byte[] key : keys) { + conn.sMembers(key); + } + + List rawResults = conn.closePipeline(); + + return rawResults.stream() + .map(result -> (Set) result) + .filter(Objects::nonNull) + .flatMap(set -> set.stream() + .map(bytes -> new String(bytes, StandardCharsets.UTF_8)) + ) + .collect(Collectors.toList()); + } + } + + @Override + public boolean isCellIdNotInTargetArea(String cellId) { + return locationJpaRepository.findById(cellId).isEmpty(); + } +} diff --git a/src/main/java/com/knu/ddip/location/infrastructure/repository/LocationWriterImpl.java b/src/main/java/com/knu/ddip/location/infrastructure/repository/LocationWriterImpl.java new file mode 100644 index 0000000..d3d4a56 --- /dev/null +++ b/src/main/java/com/knu/ddip/location/infrastructure/repository/LocationWriterImpl.java @@ -0,0 +1,129 @@ +package com.knu.ddip.location.infrastructure.repository; + +import com.knu.ddip.location.application.service.LocationWriter; +import jakarta.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.io.ClassPathResource; +import org.springframework.data.redis.connection.RedisConnection; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.Cursor; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.ScanOptions; +import org.springframework.data.redis.core.script.DefaultRedisScript; +import org.springframework.jdbc.core.BatchPreparedStatementSetter; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.scripting.support.ResourceScriptSource; +import org.springframework.stereotype.Repository; + +import java.nio.charset.StandardCharsets; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.Arrays; +import java.util.List; +import java.util.Set; + +import static com.knu.ddip.location.application.util.LocationKeyFactory.*; + +@Slf4j +@Repository +@RequiredArgsConstructor +public class LocationWriterImpl implements LocationWriter { + + private final LocationJpaRepository locationJpaRepository; + private final JdbcTemplate jdbcTemplate; + private final RedisConnectionFactory connectionFactory; + private final RedisTemplate redisTemplate; + + public static final long TTL_SECONDS = 3600L; + + private DefaultRedisScript saveUserLocationScript; + + @PostConstruct + public void init() { + saveUserLocationScript = new DefaultRedisScript<>(); + saveUserLocationScript.setResultType(String.class); + saveUserLocationScript.setScriptSource( + new ResourceScriptSource(new ClassPathResource("luascript/save_user_location.lua")) + ); + } + + @Override + public void deleteAll() { + locationJpaRepository.deleteAllInBatch(); + } + + @Override + public void saveAll(List cellIds) { + String sql = """ + INSERT INTO locations (cell_id) VALUES (?) + """; + + jdbcTemplate.batchUpdate(sql, + new BatchPreparedStatementSetter() { + @Override + public void setValues(PreparedStatement ps, int i) throws SQLException { + String cellId = cellIds.get(i); + ps.setString(1, cellId); + } + + @Override + public int getBatchSize() { + return cellIds.size(); + } + }); + } + + @Override + public void saveUserIdByCellIdAtomic(String newCellId, boolean cellIdNotInTargetArea, String encodedUserId) { + String userIdKey = createUserIdKey(encodedUserId); + String cellIdUsersKey = createCellIdUsersKey(newCellId); + String cellIdExpiriesKey = createCellIdExpiriesKey(newCellId); + + long now = System.currentTimeMillis(); + long expireAt = now + TTL_SECONDS * 1000L; + int cellIdNotInTargetAreaFlag = cellIdNotInTargetArea ? 1 : 0; + + redisTemplate.execute( + saveUserLocationScript, + Arrays.asList(userIdKey, cellIdExpiriesKey, cellIdUsersKey), + newCellId, encodedUserId, String.valueOf(TTL_SECONDS), String.valueOf(expireAt), String.valueOf(cellIdNotInTargetAreaFlag) + ); + } + + @Override + public void cleanupExpiredUserLocations(long now) { + log.info("start cleanupExpiredUserLocations task"); + try ( + RedisConnection conn = connectionFactory.getConnection(); + Cursor cursor = conn.keyCommands() + .scan(ScanOptions.scanOptions() + .match("cell:*:expiry") + .build()) + ) { + while (cursor.hasNext()) { + // 1. key 생성 + byte[] expiryKey = cursor.next(); + String expiryKeyStr = new String(expiryKey, StandardCharsets.UTF_8); + String usersKeyStr = expiryKeyStr.replace(":expiry", ":users"); + byte[] usersKey = usersKeyStr.getBytes(StandardCharsets.UTF_8); + + // 2. 만료된 멤버 수집 (-inf ~ now) + Set expired = conn.zSetCommands() + .zRangeByScore(expiryKey, Double.NEGATIVE_INFINITY, (double) now); + if (expired == null || expired.isEmpty()) continue; + + // 3. 파이프라인으로 SREM + ZREM + conn.openPipeline(); + for (byte[] member : expired) { + conn.sRem(usersKey, member); // SET에서 제거 + conn.zSetCommands().zRem(expiryKey, member); // ZSET에서도 제거 + } + conn.closePipeline(); + } + log.info("finish cleanupExpiredUserLocations task"); + } catch (Exception e) { + log.error("cleanupExpiredUserLocations error", e); + } + } +} diff --git a/src/main/java/com/knu/ddip/location/presentation/api/LocationApi.java b/src/main/java/com/knu/ddip/location/presentation/api/LocationApi.java new file mode 100644 index 0000000..b3cc2c9 --- /dev/null +++ b/src/main/java/com/knu/ddip/location/presentation/api/LocationApi.java @@ -0,0 +1,23 @@ +package com.knu.ddip.location.presentation.api; + +import com.knu.ddip.auth.domain.AuthUser; +import com.knu.ddip.auth.presentation.annotation.Login; +import com.knu.ddip.location.application.dto.UpdateMyLocationRequest; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestMapping; + +@Tag(name = "위치", description = "위치 관련 API") +@RequestMapping("/api/locations") +public interface LocationApi { + + @PutMapping + @Operation(summary = "위치 갱신", + description = "위도와 경도로 현재 내 위치를 갱신한다.") + ResponseEntity updateMyLocation( + @Login AuthUser user, + UpdateMyLocationRequest request + ); +} diff --git a/src/main/java/com/knu/ddip/location/presentation/controller/LocationController.java b/src/main/java/com/knu/ddip/location/presentation/controller/LocationController.java new file mode 100644 index 0000000..35581e7 --- /dev/null +++ b/src/main/java/com/knu/ddip/location/presentation/controller/LocationController.java @@ -0,0 +1,24 @@ +package com.knu.ddip.location.presentation.controller; + +import com.knu.ddip.auth.domain.AuthUser; +import com.knu.ddip.auth.presentation.annotation.RequireAuth; +import com.knu.ddip.location.application.dto.UpdateMyLocationRequest; +import com.knu.ddip.location.application.service.LocationService; +import com.knu.ddip.location.presentation.api.LocationApi; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +public class LocationController implements LocationApi { + + private final LocationService locationService; + + @Override + @RequireAuth + public ResponseEntity updateMyLocation(AuthUser user, UpdateMyLocationRequest request) { + locationService.saveUserLocationAtomic(user.getId(), request); + return ResponseEntity.ok().build(); + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index aeafe64..657f52b 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -2,7 +2,7 @@ spring.application.name=ddip spring.profiles.active=dev # MySQL -spring.datasource.url=jdbc:mysql://${MYSQL_HOST}:${MYSQL_PORT}/${MYSQL_DATABASE}?useSSL=false&serverTimezone=Asia/Seoul&allowPublicKeyRetrieval=true +spring.datasource.url=jdbc:mysql://${MYSQL_HOST}:${MYSQL_PORT}/${MYSQL_DATABASE}?useSSL=false&serverTimezone=Asia/Seoul&allowPublicKeyRetrieval=true&rewriteBatchedStatements=true spring.datasource.username=${MYSQL_USER} spring.datasource.password=${MYSQL_PASSWORD} diff --git a/src/main/resources/geojson/cells.geojson b/src/main/resources/geojson/cells.geojson new file mode 100644 index 0000000..7ba6dbd --- /dev/null +++ b/src/main/resources/geojson/cells.geojson @@ -0,0 +1 @@ +{"type":"FeatureCollection","features":[{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.616273,35.884989],[128.616928,35.88474],[128.616928,35.885321],[128.616273,35.88557],[128.616273,35.884989]]]},"properties":{"id":"3565e19b84","level":17,"str":"1/22230233003031300","uid":"3847729514708729856"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.617584,35.885653],[128.61824,35.885404],[128.61824,35.885985],[128.617584,35.886234],[128.617584,35.885653]]]},"properties":{"id":"3565e19bc4","level":17,"str":"1/22230233003031320","uid":"3847729515782471680"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.603812,35.886809],[128.604468,35.88656],[128.604468,35.887141],[128.603812,35.88739],[128.603812,35.886809]]]},"properties":{"id":"3565e17a74","level":17,"str":"1/22230233002331032","uid":"3847729372706373632"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.605124,35.889217],[128.60578,35.888968],[128.60578,35.889549],[128.605124,35.889798],[128.605124,35.889217]]]},"properties":{"id":"3565e1712c","level":17,"str":"1/22230233002320211","uid":"3847729332843708416"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.607747,35.885317],[128.608403,35.885068],[128.608403,35.885649],[128.607747,35.885898],[128.607747,35.885317]]]},"properties":{"id":"3565e179d4","level":17,"str":"1/22230233002330322","uid":"3847729370022019072"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.616273,35.888475],[128.616928,35.888226],[128.616928,35.888807],[128.616273,35.889056],[128.616273,35.888475]]]},"properties":{"id":"3565e19934","level":17,"str":"1/22230233003030212","uid":"3847729504776617984"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.614305,35.893869],[128.614961,35.89362],[128.614961,35.894201],[128.614305,35.89445],[128.614305,35.893869]]]},"properties":{"id":"3565e1a1bc","level":17,"str":"1/22230233003100313","uid":"3847729541418057728"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.615617,35.892209],[128.616273,35.891961],[128.616273,35.892542],[128.615617,35.89279],[128.615617,35.892209]]]},"properties":{"id":"3565e1a1fc","level":17,"str":"1/22230233003100333","uid":"3847729542491799552"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.603812,35.887971],[128.604468,35.887722],[128.604468,35.888304],[128.603812,35.888552],[128.603812,35.887971]]]},"properties":{"id":"3565e17094","level":17,"str":"1/22230233002320102","uid":"3847729330293571584"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.605124,35.888636],[128.60578,35.888387],[128.60578,35.888968],[128.605124,35.889217],[128.605124,35.888636]]]},"properties":{"id":"3565e170d4","level":17,"str":"1/22230233002320122","uid":"3847729331367313408"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.608403,35.885068],[128.609059,35.884819],[128.609059,35.8854],[128.608403,35.885649],[128.608403,35.885068]]]},"properties":{"id":"3565e1782c","level":17,"str":"1/22230233002330011","uid":"3847729362908479488"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.616273,35.890218],[128.616928,35.889969],[128.616928,35.89055],[128.616273,35.890799],[128.616273,35.890218]]]},"properties":{"id":"3565e198cc","level":17,"str":"1/22230233003030121","uid":"3847729503031787520"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.612338,35.893453],[128.612994,35.893204],[128.612994,35.893785],[128.612338,35.894034],[128.612338,35.893453]]]},"properties":{"id":"3565e1a024","level":17,"str":"1/22230233003100010","uid":"3847729534572953600"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.611682,35.895445],[128.612338,35.895196],[128.612338,35.895777],[128.611682,35.896026],[128.611682,35.895445]]]},"properties":{"id":"3565e1a064","level":17,"str":"1/22230233003100030","uid":"3847729535646695424"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.612994,35.896109],[128.61365,35.895861],[128.61365,35.896441],[128.612994,35.89669],[128.612994,35.896109]]]},"properties":{"id":"3565e1a0e4","level":17,"str":"1/22230233003100130","uid":"3847729537794179072"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.606436,35.888138],[128.607091,35.88789],[128.607091,35.888471],[128.606436,35.88872],[128.606436,35.888138]]]},"properties":{"id":"3565e17724","level":17,"str":"1/22230233002323210","uid":"3847729358479294464"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.607091,35.886147],[128.607747,35.885898],[128.607747,35.886479],[128.607091,35.886728],[128.607091,35.886147]]]},"properties":{"id":"3565e17764","level":17,"str":"1/22230233002323230","uid":"3847729359553036288"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.608403,35.886811],[128.609059,35.886563],[128.609059,35.887144],[128.608403,35.887392],[128.608403,35.886811]]]},"properties":{"id":"3565e177a4","level":17,"str":"1/22230233002323310","uid":"3847729360626778112"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.610371,35.885484],[128.611026,35.885235],[128.611026,35.885816],[128.610371,35.886065],[128.610371,35.885484]]]},"properties":{"id":"3565e177e4","level":17,"str":"1/22230233002323330","uid":"3847729361700519936"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.614305,35.890383],[128.614961,35.890134],[128.614961,35.890715],[128.614305,35.890964],[128.614305,35.890383]]]},"properties":{"id":"3565e19f1c","level":17,"str":"1/22230233003033203","uid":"3847729530143768576"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.614961,35.891877],[128.615617,35.891628],[128.615617,35.892209],[128.614961,35.892458],[128.614961,35.891877]]]},"properties":{"id":"3565e19f5c","level":17,"str":"1/22230233003033223","uid":"3847729531217510400"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.612994,35.892042],[128.61365,35.891794],[128.61365,35.892375],[128.612994,35.892623],[128.612994,35.892042]]]},"properties":{"id":"3565e19f9c","level":17,"str":"1/22230233003033303","uid":"3847729532291252224"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.611026,35.892208],[128.611682,35.891959],[128.611682,35.89254],[128.611026,35.892789],[128.611026,35.892208]]]},"properties":{"id":"3565e19fdc","level":17,"str":"1/22230233003033323","uid":"3847729533364994048"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.610371,35.88897],[128.611026,35.888722],[128.611026,35.889303],[128.610371,35.889551],[128.610371,35.88897]]]},"properties":{"id":"3565e17604","level":17,"str":"1/22230233002323000","uid":"3847729353647456256"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.609059,35.888306],[128.609715,35.888057],[128.609715,35.888638],[128.609059,35.888887],[128.609059,35.888306]]]},"properties":{"id":"3565e17644","level":17,"str":"1/22230233002323020","uid":"3847729354721198080"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.607747,35.889965],[128.608403,35.889716],[128.608403,35.890298],[128.607747,35.890546],[128.607747,35.889965]]]},"properties":{"id":"3565e17684","level":17,"str":"1/22230233002323100","uid":"3847729355794939904"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.606436,35.889301],[128.607091,35.889052],[128.607091,35.889633],[128.606436,35.889882],[128.606436,35.889301]]]},"properties":{"id":"3565e176c4","level":17,"str":"1/22230233002323120","uid":"3847729356868681728"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.611682,35.890216],[128.612338,35.889967],[128.612338,35.890548],[128.611682,35.890797],[128.611682,35.890216]]]},"properties":{"id":"3565e19e3c","level":17,"str":"1/22230233003033013","uid":"3847729526385672192"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.612994,35.888556],[128.61365,35.888308],[128.61365,35.888889],[128.612994,35.889137],[128.612994,35.888556]]]},"properties":{"id":"3565e19e7c","level":17,"str":"1/22230233003033033","uid":"3847729527459414016"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.614961,35.888391],[128.615617,35.888142],[128.615617,35.888723],[128.614961,35.888972],[128.614961,35.888391]]]},"properties":{"id":"3565e19ebc","level":17,"str":"1/22230233003033113","uid":"3847729528533155840"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.61365,35.890051],[128.614305,35.889802],[128.614305,35.890383],[128.61365,35.890632],[128.61365,35.890051]]]},"properties":{"id":"3565e19efc","level":17,"str":"1/22230233003033133","uid":"3847729529606897664"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.609059,35.892954],[128.609715,35.892705],[128.609715,35.893286],[128.609059,35.893535],[128.609059,35.892954]]]},"properties":{"id":"3565e17514","level":17,"str":"1/22230233002322202","uid":"3847729349620924416"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.610371,35.893618],[128.611026,35.89337],[128.611026,35.893951],[128.610371,35.894199],[128.610371,35.893618]]]},"properties":{"id":"3565e17554","level":17,"str":"1/22230233002322222","uid":"3847729350694666240"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.609715,35.890962],[128.610371,35.890713],[128.610371,35.891294],[128.609715,35.891543],[128.609715,35.890962]]]},"properties":{"id":"3565e17594","level":17,"str":"1/22230233002322302","uid":"3847729351768408064"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.608403,35.890298],[128.609059,35.890049],[128.609059,35.89063],[128.608403,35.890879],[128.608403,35.890298]]]},"properties":{"id":"3565e175d4","level":17,"str":"1/22230233002322322","uid":"3847729352842149888"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.612338,35.8859],[128.612994,35.885651],[128.612994,35.886232],[128.612338,35.886481],[128.612338,35.8859]]]},"properties":{"id":"3565e19d0c","level":17,"str":"1/22230233003032201","uid":"3847729521285398528"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.611682,35.884406],[128.612338,35.884157],[128.612338,35.884738],[128.611682,35.884987],[128.611682,35.884406]]]},"properties":{"id":"3565e19d4c","level":17,"str":"1/22230233003032221","uid":"3847729522359140352"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.611026,35.88756],[128.611682,35.887311],[128.611682,35.887892],[128.611026,35.888141],[128.611026,35.88756]]]},"properties":{"id":"3565e19d8c","level":17,"str":"1/22230233003032301","uid":"3847729523432882176"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.612994,35.887394],[128.61365,35.887146],[128.61365,35.887727],[128.612994,35.887975],[128.612994,35.887394]]]},"properties":{"id":"3565e19dcc","level":17,"str":"1/22230233003032321","uid":"3847729524506624000"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.606436,35.891044],[128.607091,35.890795],[128.607091,35.891376],[128.606436,35.891625],[128.606436,35.891044]]]},"properties":{"id":"3565e1741c","level":17,"str":"1/22230233002322003","uid":"3847729345460174848"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.614305,35.887478],[128.614961,35.887229],[128.614961,35.88781],[128.614305,35.888059],[128.614305,35.887478]]]},"properties":{"id":"3565e19c24","level":17,"str":"1/22230233003032010","uid":"3847729517393084416"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.614961,35.885486],[128.615617,35.885237],[128.615617,35.885818],[128.614961,35.886067],[128.614961,35.885486]]]},"properties":{"id":"3565e19c64","level":17,"str":"1/22230233003032030","uid":"3847729518466826240"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.61365,35.884822],[128.614305,35.884573],[128.614305,35.885154],[128.61365,35.885403],[128.61365,35.884822]]]},"properties":{"id":"3565e19ce4","level":17,"str":"1/22230233003032130","uid":"3847729520614309888"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.616273,35.88557],[128.616928,35.885321],[128.616928,35.885902],[128.616273,35.886151],[128.616273,35.88557]]]},"properties":{"id":"3565e19b8c","level":17,"str":"1/22230233003031301","uid":"3847729514842947584"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.605124,35.885731],[128.60578,35.885482],[128.60578,35.886063],[128.605124,35.886312],[128.605124,35.885731]]]},"properties":{"id":"3565e17a1c","level":17,"str":"1/22230233002331003","uid":"3847729371229978624"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.604468,35.890628],[128.605124,35.890379],[128.605124,35.89096],[128.604468,35.891209],[128.604468,35.890628]]]},"properties":{"id":"3565e17144","level":17,"str":"1/22230233002320220","uid":"3847729333246361600"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.607091,35.885566],[128.607747,35.885317],[128.607747,35.885898],[128.607091,35.886147],[128.607091,35.885566]]]},"properties":{"id":"3565e179dc","level":17,"str":"1/22230233002330323","uid":"3847729370156236800"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.616928,35.888226],[128.617584,35.887977],[128.617584,35.888558],[128.616928,35.888807],[128.616928,35.888226]]]},"properties":{"id":"3565e1993c","level":17,"str":"1/22230233003030213","uid":"3847729504910835712"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.61365,35.894118],[128.614305,35.893869],[128.614305,35.89445],[128.61365,35.894699],[128.61365,35.894118]]]},"properties":{"id":"3565e1a1b4","level":17,"str":"1/22230233003100312","uid":"3847729541283840000"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.615617,35.89279],[128.616273,35.892542],[128.616273,35.893123],[128.615617,35.893371],[128.615617,35.89279]]]},"properties":{"id":"3565e1a1f4","level":17,"str":"1/22230233003100332","uid":"3847729542357581824"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.603156,35.88822],[128.603812,35.887971],[128.603812,35.888552],[128.603156,35.888801],[128.603156,35.88822]]]},"properties":{"id":"3565e1708c","level":17,"str":"1/22230233002320101","uid":"3847729330159353856"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.605124,35.888055],[128.60578,35.887806],[128.60578,35.888387],[128.605124,35.888636],[128.605124,35.888055]]]},"properties":{"id":"3565e170cc","level":17,"str":"1/22230233002320121","uid":"3847729331233095680"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.608403,35.884487],[128.609059,35.884238],[128.609059,35.884819],[128.608403,35.885068],[128.608403,35.884487]]]},"properties":{"id":"3565e17834","level":17,"str":"1/22230233002330012","uid":"3847729363042697216"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.616273,35.889637],[128.616928,35.889388],[128.616928,35.889969],[128.616273,35.890218],[128.616273,35.889637]]]},"properties":{"id":"3565e198d4","level":17,"str":"1/22230233003030122","uid":"3847729503166005248"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.611682,35.893702],[128.612338,35.893453],[128.612338,35.894034],[128.611682,35.894283],[128.611682,35.893702]]]},"properties":{"id":"3565e1a01c","level":17,"str":"1/22230233003100003","uid":"3847729534438735872"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.612338,35.895196],[128.612994,35.894947],[128.612994,35.895528],[128.612338,35.895777],[128.612338,35.895196]]]},"properties":{"id":"3565e1a05c","level":17,"str":"1/22230233003100023","uid":"3847729535512477696"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.606436,35.887557],[128.607091,35.887309],[128.607091,35.88789],[128.606436,35.888138],[128.606436,35.887557]]]},"properties":{"id":"3565e1773c","level":17,"str":"1/22230233002323213","uid":"3847729358881947648"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.607747,35.885898],[128.608403,35.885649],[128.608403,35.88623],[128.607747,35.886479],[128.607747,35.885898]]]},"properties":{"id":"3565e1777c","level":17,"str":"1/22230233002323233","uid":"3847729359955689472"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.609059,35.886563],[128.609715,35.886314],[128.609715,35.886895],[128.609059,35.887144],[128.609059,35.886563]]]},"properties":{"id":"3565e177bc","level":17,"str":"1/22230233002323313","uid":"3847729361029431296"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.610371,35.884903],[128.611026,35.884654],[128.611026,35.885235],[128.610371,35.885484],[128.610371,35.884903]]]},"properties":{"id":"3565e177fc","level":17,"str":"1/22230233002323333","uid":"3847729362103173120"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.61365,35.890632],[128.614305,35.890383],[128.614305,35.890964],[128.61365,35.891213],[128.61365,35.890632]]]},"properties":{"id":"3565e19f04","level":17,"str":"1/22230233003033200","uid":"3847729529741115392"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.614961,35.891296],[128.615617,35.891047],[128.615617,35.891628],[128.614961,35.891877],[128.614961,35.891296]]]},"properties":{"id":"3565e19f44","level":17,"str":"1/22230233003033220","uid":"3847729530814857216"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.612994,35.892623],[128.61365,35.892375],[128.61365,35.892956],[128.612994,35.893204],[128.612994,35.892623]]]},"properties":{"id":"3565e19f84","level":17,"str":"1/22230233003033300","uid":"3847729531888599040"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.611682,35.891959],[128.612338,35.89171],[128.612338,35.892291],[128.611682,35.89254],[128.611682,35.891959]]]},"properties":{"id":"3565e19fc4","level":17,"str":"1/22230233003033320","uid":"3847729532962340864"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.609715,35.888057],[128.610371,35.887808],[128.610371,35.888389],[128.609715,35.888638],[128.609715,35.888057]]]},"properties":{"id":"3565e1763c","level":17,"str":"1/22230233002323013","uid":"3847729354586980352"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.608403,35.889716],[128.609059,35.889468],[128.609059,35.890049],[128.608403,35.890298],[128.608403,35.889716]]]},"properties":{"id":"3565e1767c","level":17,"str":"1/22230233002323033","uid":"3847729355660722176"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.606436,35.889882],[128.607091,35.889633],[128.607091,35.890214],[128.606436,35.890463],[128.606436,35.889882]]]},"properties":{"id":"3565e176bc","level":17,"str":"1/22230233002323113","uid":"3847729356734464000"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.607747,35.888222],[128.608403,35.887973],[128.608403,35.888554],[128.607747,35.888803],[128.607747,35.888222]]]},"properties":{"id":"3565e176fc","level":17,"str":"1/22230233002323133","uid":"3847729357808205824"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.611026,35.889303],[128.611682,35.889054],[128.611682,35.889635],[128.611026,35.889884],[128.611026,35.889303]]]},"properties":{"id":"3565e19e04","level":17,"str":"1/22230233003033000","uid":"3847729525446148096"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.612338,35.889967],[128.612994,35.889718],[128.612994,35.890299],[128.612338,35.890548],[128.612338,35.889967]]]},"properties":{"id":"3565e19e44","level":17,"str":"1/22230233003033020","uid":"3847729526519889920"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.61365,35.888308],[128.614305,35.888059],[128.614305,35.88864],[128.61365,35.888889],[128.61365,35.888308]]]},"properties":{"id":"3565e19e84","level":17,"str":"1/22230233003033100","uid":"3847729527593631744"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.614961,35.888972],[128.615617,35.888723],[128.615617,35.889304],[128.614961,35.889553],[128.614961,35.888972]]]},"properties":{"id":"3565e19ec4","level":17,"str":"1/22230233003033120","uid":"3847729528667373568"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.609059,35.892373],[128.609715,35.892124],[128.609715,35.892705],[128.609059,35.892954],[128.609059,35.892373]]]},"properties":{"id":"3565e1750c","level":17,"str":"1/22230233002322201","uid":"3847729349486706688"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.610371,35.890713],[128.611026,35.890465],[128.611026,35.891046],[128.610371,35.891294],[128.610371,35.890713]]]},"properties":{"id":"3565e1758c","level":17,"str":"1/22230233002322301","uid":"3847729351634190336"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.608403,35.890879],[128.609059,35.89063],[128.609059,35.891211],[128.608403,35.89146],[128.608403,35.890879]]]},"properties":{"id":"3565e175cc","level":17,"str":"1/22230233002322321","uid":"3847729352707932160"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.612338,35.885319],[128.612994,35.88507],[128.612994,35.885651],[128.612338,35.8859],[128.612338,35.885319]]]},"properties":{"id":"3565e19d14","level":17,"str":"1/22230233003032202","uid":"3847729521419616256"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.611026,35.884654],[128.611682,35.884406],[128.611682,35.884987],[128.611026,35.885235],[128.611026,35.884654]]]},"properties":{"id":"3565e19d54","level":17,"str":"1/22230233003032222","uid":"3847729522493358080"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.611682,35.887311],[128.612338,35.887062],[128.612338,35.887643],[128.611682,35.887892],[128.611682,35.887311]]]},"properties":{"id":"3565e19d94","level":17,"str":"1/22230233003032302","uid":"3847729523567099904"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.612994,35.887975],[128.61365,35.887727],[128.61365,35.888308],[128.612994,35.888556],[128.612994,35.887975]]]},"properties":{"id":"3565e19dd4","level":17,"str":"1/22230233003032322","uid":"3847729524640841728"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.606436,35.891625],[128.607091,35.891376],[128.607091,35.891957],[128.606436,35.892206],[128.606436,35.891625]]]},"properties":{"id":"3565e17414","level":17,"str":"1/22230233002322002","uid":"3847729345325957120"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.607747,35.892289],[128.608403,35.892041],[128.608403,35.892622],[128.607747,35.89287],[128.607747,35.892289]]]},"properties":{"id":"3565e17454","level":17,"str":"1/22230233002322022","uid":"3847729346399698944"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.615617,35.886399],[128.616273,35.886151],[128.616273,35.886732],[128.615617,35.88698],[128.615617,35.886399]]]},"properties":{"id":"3565e19c0c","level":17,"str":"1/22230233003032001","uid":"3847729516990431232"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.61365,35.886565],[128.614305,35.886316],[128.614305,35.886897],[128.61365,35.887146],[128.61365,35.886565]]]},"properties":{"id":"3565e19c4c","level":17,"str":"1/22230233003032021","uid":"3847729518064173056"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.614961,35.884905],[128.615617,35.884656],[128.615617,35.885237],[128.614961,35.885486],[128.614961,35.884905]]]},"properties":{"id":"3565e19c8c","level":17,"str":"1/22230233003032101","uid":"3847729519137914880"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.614305,35.883411],[128.614961,35.883162],[128.614961,35.883743],[128.614305,35.883992],[128.614305,35.883411]]]},"properties":{"id":"3565e19ccc","level":17,"str":"1/22230233003032121","uid":"3847729520211656704"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.616273,35.886151],[128.616928,35.885902],[128.616928,35.886483],[128.616273,35.886732],[128.616273,35.886151]]]},"properties":{"id":"3565e19bf4","level":17,"str":"1/22230233003031332","uid":"3847729516587778048"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.612338,35.883576],[128.612994,35.883327],[128.612994,35.883908],[128.612338,35.884157],[128.612338,35.883576]]]},"properties":{"id":"3565e182cc","level":17,"str":"1/22230233003001121","uid":"3847729408542507008"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.603812,35.889714],[128.604468,35.889466],[128.604468,35.890047],[128.603812,35.890295],[128.603812,35.889714]]]},"properties":{"id":"3565e1711c","level":17,"str":"1/22230233002320203","uid":"3847729332575272960"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.607091,35.884984],[128.607747,35.884736],[128.607747,35.885317],[128.607091,35.885566],[128.607091,35.884984]]]},"properties":{"id":"3565e179c4","level":17,"str":"1/22230233002330320","uid":"3847729369753583616"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.616928,35.888807],[128.617584,35.888558],[128.617584,35.889139],[128.616928,35.889388],[128.616928,35.888807]]]},"properties":{"id":"3565e19924","level":17,"str":"1/22230233003030210","uid":"3847729504508182528"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.617584,35.886815],[128.61824,35.886566],[128.61824,35.887147],[128.617584,35.887396],[128.617584,35.886815]]]},"properties":{"id":"3565e19964","level":17,"str":"1/22230233003030230","uid":"3847729505581924352"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.61365,35.893537],[128.614305,35.893288],[128.614305,35.893869],[128.61365,35.894118],[128.61365,35.893537]]]},"properties":{"id":"3565e1a1cc","level":17,"str":"1/22230233003100321","uid":"3847729541686493184"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.603156,35.887639],[128.603812,35.88739],[128.603812,35.887971],[128.603156,35.88822],[128.603156,35.887639]]]},"properties":{"id":"3565e17084","level":17,"str":"1/22230233002320100","uid":"3847729330025136128"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.604468,35.888304],[128.605124,35.888055],[128.605124,35.888636],[128.604468,35.888885],[128.604468,35.888304]]]},"properties":{"id":"3565e170c4","level":17,"str":"1/22230233002320120","uid":"3847729331098877952"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.609715,35.884571],[128.610371,35.884322],[128.610371,35.884903],[128.609715,35.885152],[128.609715,35.884571]]]},"properties":{"id":"3565e1781c","level":17,"str":"1/22230233002330003","uid":"3847729362640044032"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.616928,35.89055],[128.617584,35.890301],[128.617584,35.890882],[128.616928,35.891131],[128.616928,35.89055]]]},"properties":{"id":"3565e198bc","level":17,"str":"1/22230233003030113","uid":"3847729502763352064"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.612994,35.893785],[128.61365,35.893537],[128.61365,35.894118],[128.612994,35.894366],[128.612994,35.893785]]]},"properties":{"id":"3565e1a034","level":17,"str":"1/22230233003100012","uid":"3847729534841389056"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.612338,35.895777],[128.612994,35.895528],[128.612994,35.896109],[128.612338,35.896358],[128.612338,35.895777]]]},"properties":{"id":"3565e1a0f4","level":17,"str":"1/22230233003100132","uid":"3847729538062614528"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.607091,35.887309],[128.607747,35.88706],[128.607747,35.887641],[128.607091,35.88789],[128.607091,35.887309]]]},"properties":{"id":"3565e17714","level":17,"str":"1/22230233002323202","uid":"3847729358210859008"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.60578,35.886644],[128.606436,35.886395],[128.606436,35.886976],[128.60578,35.887225],[128.60578,35.886644]]]},"properties":{"id":"3565e17754","level":17,"str":"1/22230233002323222","uid":"3847729359284600832"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.609059,35.885982],[128.609715,35.885733],[128.609715,35.886314],[128.609059,35.886563],[128.609059,35.885982]]]},"properties":{"id":"3565e17794","level":17,"str":"1/22230233002323302","uid":"3847729360358342656"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.610371,35.886646],[128.611026,35.886397],[128.611026,35.886979],[128.610371,35.887227],[128.610371,35.886646]]]},"properties":{"id":"3565e177d4","level":17,"str":"1/22230233002323322","uid":"3847729361432084480"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.61365,35.891213],[128.614305,35.890964],[128.614305,35.891545],[128.61365,35.891794],[128.61365,35.891213]]]},"properties":{"id":"3565e19f0c","level":17,"str":"1/22230233003033201","uid":"3847729529875333120"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.615617,35.891047],[128.616273,35.890799],[128.616273,35.89138],[128.615617,35.891628],[128.615617,35.891047]]]},"properties":{"id":"3565e19f4c","level":17,"str":"1/22230233003033221","uid":"3847729530949074944"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.612338,35.892872],[128.612994,35.892623],[128.612994,35.893204],[128.612338,35.893453],[128.612338,35.892872]]]},"properties":{"id":"3565e19f8c","level":17,"str":"1/22230233003033301","uid":"3847729532022816768"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.611682,35.891378],[128.612338,35.891129],[128.612338,35.89171],[128.611682,35.891959],[128.611682,35.891378]]]},"properties":{"id":"3565e19fcc","level":17,"str":"1/22230233003033321","uid":"3847729533096558592"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.609715,35.887476],[128.610371,35.887227],[128.610371,35.887808],[128.609715,35.888057],[128.609715,35.887476]]]},"properties":{"id":"3565e17634","level":17,"str":"1/22230233002323012","uid":"3847729354452762624"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.609059,35.889468],[128.609715,35.889219],[128.609715,35.8898],[128.609059,35.890049],[128.609059,35.889468]]]},"properties":{"id":"3565e17674","level":17,"str":"1/22230233002323032","uid":"3847729355526504448"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.60578,35.89013],[128.606436,35.889882],[128.606436,35.890463],[128.60578,35.890711],[128.60578,35.89013]]]},"properties":{"id":"3565e176b4","level":17,"str":"1/22230233002323112","uid":"3847729356600246272"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.607747,35.888803],[128.608403,35.888554],[128.608403,35.889135],[128.607747,35.889384],[128.607747,35.888803]]]},"properties":{"id":"3565e176f4","level":17,"str":"1/22230233002323132","uid":"3847729357673988096"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.611026,35.891046],[128.611682,35.890797],[128.611682,35.891378],[128.611026,35.891627],[128.611026,35.891046]]]},"properties":{"id":"3565e19e2c","level":17,"str":"1/22230233003033011","uid":"3847729526117236736"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.612338,35.889386],[128.612994,35.889137],[128.612994,35.889718],[128.612338,35.889967],[128.612338,35.889386]]]},"properties":{"id":"3565e19e6c","level":17,"str":"1/22230233003033031","uid":"3847729527190978560"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.615617,35.887561],[128.616273,35.887313],[128.616273,35.887894],[128.615617,35.888142],[128.615617,35.887561]]]},"properties":{"id":"3565e19eac","level":17,"str":"1/22230233003033111","uid":"3847729528264720384"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.614305,35.889221],[128.614961,35.888972],[128.614961,35.889553],[128.614305,35.889802],[128.614305,35.889221]]]},"properties":{"id":"3565e19eec","level":17,"str":"1/22230233003033131","uid":"3847729529338462208"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.608403,35.892622],[128.609059,35.892373],[128.609059,35.892954],[128.608403,35.893203],[128.608403,35.892622]]]},"properties":{"id":"3565e17504","level":17,"str":"1/22230233002322200","uid":"3847729349352488960"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.609715,35.893286],[128.610371,35.893037],[128.610371,35.893618],[128.609715,35.893867],[128.609715,35.893286]]]},"properties":{"id":"3565e17544","level":17,"str":"1/22230233002322220","uid":"3847729350426230784"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.610371,35.891294],[128.611026,35.891046],[128.611026,35.891627],[128.610371,35.891875],[128.610371,35.891294]]]},"properties":{"id":"3565e17584","level":17,"str":"1/22230233002322300","uid":"3847729351499972608"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.609059,35.89063],[128.609715,35.890381],[128.609715,35.890962],[128.609059,35.891211],[128.609059,35.89063]]]},"properties":{"id":"3565e175c4","level":17,"str":"1/22230233002322320","uid":"3847729352573714432"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.612338,35.884738],[128.612994,35.884489],[128.612994,35.88507],[128.612338,35.885319],[128.612338,35.884738]]]},"properties":{"id":"3565e19d3c","level":17,"str":"1/22230233003032213","uid":"3847729522090704896"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.611026,35.886397],[128.611682,35.886149],[128.611682,35.88673],[128.611026,35.886979],[128.611026,35.886397]]]},"properties":{"id":"3565e19d7c","level":17,"str":"1/22230233003032233","uid":"3847729523164446720"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.612338,35.887062],[128.612994,35.886813],[128.612994,35.887394],[128.612338,35.887643],[128.612338,35.887062]]]},"properties":{"id":"3565e19dbc","level":17,"str":"1/22230233003032313","uid":"3847729524238188544"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.611026,35.888722],[128.611682,35.888473],[128.611682,35.889054],[128.611026,35.889303],[128.611026,35.888722]]]},"properties":{"id":"3565e19dfc","level":17,"str":"1/22230233003032333","uid":"3847729525311930368"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.607747,35.891708],[128.608403,35.89146],[128.608403,35.892041],[128.607747,35.892289],[128.607747,35.891708]]]},"properties":{"id":"3565e1744c","level":17,"str":"1/22230233002322021","uid":"3847729346265481216"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.614961,35.886648],[128.615617,35.886399],[128.615617,35.88698],[128.614961,35.887229],[128.614961,35.886648]]]},"properties":{"id":"3565e19c14","level":17,"str":"1/22230233003032002","uid":"3847729517124648960"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.61365,35.885984],[128.614305,35.885735],[128.614305,35.886316],[128.61365,35.886565],[128.61365,35.885984]]]},"properties":{"id":"3565e19c54","level":17,"str":"1/22230233003032022","uid":"3847729518198390784"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.614961,35.884324],[128.615617,35.884075],[128.615617,35.884656],[128.614961,35.884905],[128.614961,35.884324]]]},"properties":{"id":"3565e19c94","level":17,"str":"1/22230233003032102","uid":"3847729519272132608"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.61365,35.883659],[128.614305,35.883411],[128.614305,35.883992],[128.61365,35.88424],[128.61365,35.883659]]]},"properties":{"id":"3565e19cd4","level":17,"str":"1/22230233003032122","uid":"3847729520345874432"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.616273,35.886732],[128.616928,35.886483],[128.616928,35.887064],[128.616273,35.887313],[128.616273,35.886732]]]},"properties":{"id":"3565e19bfc","level":17,"str":"1/22230233003031333","uid":"3847729516721995776"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.604468,35.88656],[128.605124,35.886312],[128.605124,35.886893],[128.604468,35.887141],[128.604468,35.88656]]]},"properties":{"id":"3565e17a0c","level":17,"str":"1/22230233002331001","uid":"3847729370961543168"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.616273,35.891961],[128.616928,35.891712],[128.616928,35.892293],[128.616273,35.892542],[128.616273,35.891961]]]},"properties":{"id":"3565e1a204","level":17,"str":"1/22230233003101000","uid":"3847729542626017280"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.605124,35.889798],[128.60578,35.889549],[128.60578,35.89013],[128.605124,35.890379],[128.605124,35.889798]]]},"properties":{"id":"3565e17134","level":17,"str":"1/22230233002320212","uid":"3847729332977926144"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.607747,35.884736],[128.608403,35.884487],[128.608403,35.885068],[128.607747,35.885317],[128.607747,35.884736]]]},"properties":{"id":"3565e179cc","level":17,"str":"1/22230233002330321","uid":"3847729369887801344"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.616273,35.889056],[128.616928,35.888807],[128.616928,35.889388],[128.616273,35.889637],[128.616273,35.889056]]]},"properties":{"id":"3565e1992c","level":17,"str":"1/22230233003030211","uid":"3847729504642400256"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.61365,35.89528],[128.614305,35.895031],[128.614305,35.895612],[128.61365,35.895861],[128.61365,35.89528]]]},"properties":{"id":"3565e1a104","level":17,"str":"1/22230233003100200","uid":"3847729538331049984"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.614305,35.893288],[128.614961,35.893039],[128.614961,35.89362],[128.614305,35.893869],[128.614305,35.893288]]]},"properties":{"id":"3565e1a1c4","level":17,"str":"1/22230233003100320","uid":"3847729541552275456"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.604468,35.887722],[128.605124,35.887474],[128.605124,35.888055],[128.604468,35.888304],[128.604468,35.887722]]]},"properties":{"id":"3565e170bc","level":17,"str":"1/22230233002320113","uid":"3847729330964660224"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.603156,35.889382],[128.603812,35.889133],[128.603812,35.889714],[128.603156,35.889963],[128.603156,35.889382]]]},"properties":{"id":"3565e170fc","level":17,"str":"1/22230233002320133","uid":"3847729332038402048"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.609059,35.884819],[128.609715,35.884571],[128.609715,35.885152],[128.609059,35.8854],[128.609059,35.884819]]]},"properties":{"id":"3565e17824","level":17,"str":"1/22230233002330010","uid":"3847729362774261760"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.616928,35.889969],[128.617584,35.88972],[128.617584,35.890301],[128.616928,35.89055],[128.616928,35.889969]]]},"properties":{"id":"3565e198c4","level":17,"str":"1/22230233003030120","uid":"3847729502897569792"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.612994,35.893204],[128.61365,35.892956],[128.61365,35.893537],[128.612994,35.893785],[128.612994,35.893204]]]},"properties":{"id":"3565e1a02c","level":17,"str":"1/22230233003100011","uid":"3847729534707171328"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.611682,35.894864],[128.612338,35.894615],[128.612338,35.895196],[128.611682,35.895445],[128.611682,35.894864]]]},"properties":{"id":"3565e1a06c","level":17,"str":"1/22230233003100031","uid":"3847729535780913152"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.60578,35.888387],[128.606436,35.888138],[128.606436,35.88872],[128.60578,35.888968],[128.60578,35.888387]]]},"properties":{"id":"3565e1772c","level":17,"str":"1/22230233002323211","uid":"3847729358613512192"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.607091,35.886728],[128.607747,35.886479],[128.607747,35.88706],[128.607091,35.887309],[128.607091,35.886728]]]},"properties":{"id":"3565e1776c","level":17,"str":"1/22230233002323231","uid":"3847729359687254016"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.608403,35.887392],[128.609059,35.887144],[128.609059,35.887725],[128.608403,35.887973],[128.608403,35.887392]]]},"properties":{"id":"3565e177ac","level":17,"str":"1/22230233002323311","uid":"3847729360760995840"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.609715,35.885733],[128.610371,35.885484],[128.610371,35.886065],[128.609715,35.886314],[128.609715,35.885733]]]},"properties":{"id":"3565e177ec","level":17,"str":"1/22230233002323331","uid":"3847729361834737664"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.615617,35.890466],[128.616273,35.890218],[128.616273,35.890799],[128.615617,35.891047],[128.615617,35.890466]]]},"properties":{"id":"3565e19f34","level":17,"str":"1/22230233003033212","uid":"3847729530546421760"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.61365,35.891794],[128.614305,35.891545],[128.614305,35.892126],[128.61365,35.892375],[128.61365,35.891794]]]},"properties":{"id":"3565e19f74","level":17,"str":"1/22230233003033232","uid":"3847729531620163584"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.612338,35.891129],[128.612994,35.89088],[128.612994,35.891461],[128.612338,35.89171],[128.612338,35.891129]]]},"properties":{"id":"3565e19fb4","level":17,"str":"1/22230233003033312","uid":"3847729532693905408"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.611682,35.893121],[128.612338,35.892872],[128.612338,35.893453],[128.611682,35.893702],[128.611682,35.893121]]]},"properties":{"id":"3565e19ff4","level":17,"str":"1/22230233003033332","uid":"3847729533767647232"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.610371,35.887227],[128.611026,35.886979],[128.611026,35.88756],[128.610371,35.887808],[128.610371,35.887227]]]},"properties":{"id":"3565e1762c","level":17,"str":"1/22230233002323011","uid":"3847729354318544896"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.609059,35.888887],[128.609715,35.888638],[128.609715,35.889219],[128.609059,35.889468],[128.609059,35.888887]]]},"properties":{"id":"3565e1766c","level":17,"str":"1/22230233002323031","uid":"3847729355392286720"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.60578,35.890711],[128.606436,35.890463],[128.606436,35.891044],[128.60578,35.891292],[128.60578,35.890711]]]},"properties":{"id":"3565e176ac","level":17,"str":"1/22230233002323111","uid":"3847729356466028544"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.607091,35.889052],[128.607747,35.888803],[128.607747,35.889384],[128.607091,35.889633],[128.607091,35.889052]]]},"properties":{"id":"3565e176ec","level":17,"str":"1/22230233002323131","uid":"3847729357539770368"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.611682,35.890797],[128.612338,35.890548],[128.612338,35.891129],[128.611682,35.891378],[128.611682,35.890797]]]},"properties":{"id":"3565e19e34","level":17,"str":"1/22230233003033012","uid":"3847729526251454464"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.612338,35.888805],[128.612994,35.888556],[128.612994,35.889137],[128.612338,35.889386],[128.612338,35.888805]]]},"properties":{"id":"3565e19e74","level":17,"str":"1/22230233003033032","uid":"3847729527325196288"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.615617,35.888142],[128.616273,35.887894],[128.616273,35.888475],[128.615617,35.888723],[128.615617,35.888142]]]},"properties":{"id":"3565e19eb4","level":17,"str":"1/22230233003033112","uid":"3847729528398938112"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.61365,35.88947],[128.614305,35.889221],[128.614305,35.889802],[128.61365,35.890051],[128.61365,35.88947]]]},"properties":{"id":"3565e19ef4","level":17,"str":"1/22230233003033132","uid":"3847729529472679936"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.610371,35.891875],[128.611026,35.891627],[128.611026,35.892208],[128.610371,35.892456],[128.610371,35.891875]]]},"properties":{"id":"3565e1757c","level":17,"str":"1/22230233002322233","uid":"3847729351365754880"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.609059,35.891211],[128.609715,35.890962],[128.609715,35.891543],[128.609059,35.891792],[128.609059,35.891211]]]},"properties":{"id":"3565e175bc","level":17,"str":"1/22230233002322313","uid":"3847729352439496704"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.610371,35.889551],[128.611026,35.889303],[128.611026,35.889884],[128.610371,35.890132],[128.610371,35.889551]]]},"properties":{"id":"3565e175fc","level":17,"str":"1/22230233002322333","uid":"3847729353513238528"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.612994,35.885651],[128.61365,35.885403],[128.61365,35.885984],[128.612994,35.886232],[128.612994,35.885651]]]},"properties":{"id":"3565e19d04","level":17,"str":"1/22230233003032200","uid":"3847729521151180800"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.611682,35.884987],[128.612338,35.884738],[128.612338,35.885319],[128.611682,35.885568],[128.611682,35.884987]]]},"properties":{"id":"3565e19d44","level":17,"str":"1/22230233003032220","uid":"3847729522224922624"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.611026,35.886979],[128.611682,35.88673],[128.611682,35.887311],[128.611026,35.88756],[128.611026,35.886979]]]},"properties":{"id":"3565e19d84","level":17,"str":"1/22230233003032300","uid":"3847729523298664448"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.612338,35.887643],[128.612994,35.887394],[128.612994,35.887975],[128.612338,35.888224],[128.612338,35.887643]]]},"properties":{"id":"3565e19dc4","level":17,"str":"1/22230233003032320","uid":"3847729524372406272"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.60578,35.891292],[128.606436,35.891044],[128.606436,35.891625],[128.60578,35.891873],[128.60578,35.891292]]]},"properties":{"id":"3565e17404","level":17,"str":"1/22230233002322000","uid":"3847729345057521664"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.607091,35.891957],[128.607747,35.891708],[128.607747,35.892289],[128.607091,35.892538],[128.607091,35.891957]]]},"properties":{"id":"3565e17444","level":17,"str":"1/22230233002322020","uid":"3847729346131263488"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.614305,35.886897],[128.614961,35.886648],[128.614961,35.887229],[128.614305,35.887478],[128.614305,35.886897]]]},"properties":{"id":"3565e19c3c","level":17,"str":"1/22230233003032013","uid":"3847729517795737600"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.615617,35.885237],[128.616273,35.884989],[128.616273,35.88557],[128.615617,35.885818],[128.615617,35.885237]]]},"properties":{"id":"3565e19c7c","level":17,"str":"1/22230233003032033","uid":"3847729518869479424"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.614961,35.883743],[128.615617,35.883494],[128.615617,35.884075],[128.614961,35.884324],[128.614961,35.883743]]]},"properties":{"id":"3565e19cbc","level":17,"str":"1/22230233003032113","uid":"3847729519943221248"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.61365,35.885403],[128.614305,35.885154],[128.614305,35.885735],[128.61365,35.885984],[128.61365,35.885403]]]},"properties":{"id":"3565e19cfc","level":17,"str":"1/22230233003032133","uid":"3847729521016963072"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.616928,35.886483],[128.617584,35.886234],[128.617584,35.886815],[128.616928,35.887064],[128.616928,35.886483]]]},"properties":{"id":"3565e19be4","level":17,"str":"1/22230233003031330","uid":"3847729516319342592"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.605124,35.890379],[128.60578,35.89013],[128.60578,35.890711],[128.605124,35.89096],[128.605124,35.890379]]]},"properties":{"id":"3565e1714c","level":17,"str":"1/22230233002320221","uid":"3847729333380579328"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.60578,35.885482],[128.606436,35.885233],[128.606436,35.885814],[128.60578,35.886063],[128.60578,35.885482]]]},"properties":{"id":"3565e179f4","level":17,"str":"1/22230233002330332","uid":"3847729370558889984"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.616273,35.887313],[128.616928,35.887064],[128.616928,35.887645],[128.616273,35.887894],[128.616273,35.887313]]]},"properties":{"id":"3565e19954","level":17,"str":"1/22230233003030222","uid":"3847729505313488896"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.614305,35.892707],[128.614961,35.892458],[128.614961,35.893039],[128.614305,35.893288],[128.614305,35.892707]]]},"properties":{"id":"3565e1a1dc","level":17,"str":"1/22230233003100323","uid":"3847729541954928640"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.605124,35.887474],[128.60578,35.887225],[128.60578,35.887806],[128.605124,35.888055],[128.605124,35.887474]]]},"properties":{"id":"3565e170b4","level":17,"str":"1/22230233002320112","uid":"3847729330830442496"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.603156,35.888801],[128.603812,35.888552],[128.603812,35.889133],[128.603156,35.889382],[128.603156,35.888801]]]},"properties":{"id":"3565e170f4","level":17,"str":"1/22230233002320132","uid":"3847729331904184320"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.616273,35.89138],[128.616928,35.891131],[128.616928,35.891712],[128.616273,35.891961],[128.616273,35.89138]]]},"properties":{"id":"3565e198ac","level":17,"str":"1/22230233003030111","uid":"3847729502494916608"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.611026,35.893951],[128.611682,35.893702],[128.611682,35.894283],[128.611026,35.894532],[128.611026,35.893951]]]},"properties":{"id":"3565e1a004","level":17,"str":"1/22230233003100000","uid":"3847729534036082688"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.612338,35.894615],[128.612994,35.894366],[128.612994,35.894947],[128.612338,35.895196],[128.612338,35.894615]]]},"properties":{"id":"3565e1a044","level":17,"str":"1/22230233003100020","uid":"3847729535109824512"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.607747,35.887641],[128.608403,35.887392],[128.608403,35.887973],[128.607747,35.888222],[128.607747,35.887641]]]},"properties":{"id":"3565e17704","level":17,"str":"1/22230233002323200","uid":"3847729357942423552"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.606436,35.886976],[128.607091,35.886728],[128.607091,35.887309],[128.606436,35.887557],[128.606436,35.886976]]]},"properties":{"id":"3565e17744","level":17,"str":"1/22230233002323220","uid":"3847729359016165376"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.608403,35.885649],[128.609059,35.8854],[128.609059,35.885982],[128.608403,35.88623],[128.608403,35.885649]]]},"properties":{"id":"3565e17784","level":17,"str":"1/22230233002323300","uid":"3847729360089907200"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.609715,35.886314],[128.610371,35.886065],[128.610371,35.886646],[128.609715,35.886895],[128.609715,35.886314]]]},"properties":{"id":"3565e177c4","level":17,"str":"1/22230233002323320","uid":"3847729361163649024"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.614961,35.890715],[128.615617,35.890466],[128.615617,35.891047],[128.614961,35.891296],[128.614961,35.890715]]]},"properties":{"id":"3565e19f3c","level":17,"str":"1/22230233003033213","uid":"3847729530680639488"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.61365,35.892375],[128.614305,35.892126],[128.614305,35.892707],[128.61365,35.892956],[128.61365,35.892375]]]},"properties":{"id":"3565e19f7c","level":17,"str":"1/22230233003033233","uid":"3847729531754381312"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.612338,35.89171],[128.612994,35.891461],[128.612994,35.892042],[128.612338,35.892291],[128.612338,35.89171]]]},"properties":{"id":"3565e19fbc","level":17,"str":"1/22230233003033313","uid":"3847729532828123136"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.611026,35.89337],[128.611682,35.893121],[128.611682,35.893702],[128.611026,35.893951],[128.611026,35.89337]]]},"properties":{"id":"3565e19ffc","level":17,"str":"1/22230233003033333","uid":"3847729533901864960"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.610371,35.887808],[128.611026,35.88756],[128.611026,35.888141],[128.610371,35.888389],[128.610371,35.887808]]]},"properties":{"id":"3565e17624","level":17,"str":"1/22230233002323010","uid":"3847729354184327168"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.608403,35.889135],[128.609059,35.888887],[128.609059,35.889468],[128.608403,35.889716],[128.608403,35.889135]]]},"properties":{"id":"3565e17664","level":17,"str":"1/22230233002323030","uid":"3847729355258068992"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.606436,35.890463],[128.607091,35.890214],[128.607091,35.890795],[128.606436,35.891044],[128.606436,35.890463]]]},"properties":{"id":"3565e176a4","level":17,"str":"1/22230233002323110","uid":"3847729356331810816"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.607091,35.888471],[128.607747,35.888222],[128.607747,35.888803],[128.607091,35.889052],[128.607091,35.888471]]]},"properties":{"id":"3565e176e4","level":17,"str":"1/22230233002323130","uid":"3847729357405552640"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.611026,35.889884],[128.611682,35.889635],[128.611682,35.890216],[128.611026,35.890465],[128.611026,35.889884]]]},"properties":{"id":"3565e19e1c","level":17,"str":"1/22230233003033003","uid":"3847729525848801280"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.612994,35.889718],[128.61365,35.88947],[128.61365,35.890051],[128.612994,35.890299],[128.612994,35.889718]]]},"properties":{"id":"3565e19e5c","level":17,"str":"1/22230233003033023","uid":"3847729526922543104"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.614305,35.888059],[128.614961,35.88781],[128.614961,35.888391],[128.614305,35.88864],[128.614305,35.888059]]]},"properties":{"id":"3565e19e9c","level":17,"str":"1/22230233003033103","uid":"3847729527996284928"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.614961,35.889553],[128.615617,35.889304],[128.615617,35.889885],[128.614961,35.890134],[128.614961,35.889553]]]},"properties":{"id":"3565e19edc","level":17,"str":"1/22230233003033123","uid":"3847729529070026752"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.609715,35.892124],[128.610371,35.891875],[128.610371,35.892456],[128.609715,35.892705],[128.609715,35.892124]]]},"properties":{"id":"3565e17574","level":17,"str":"1/22230233002322232","uid":"3847729351231537152"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.608403,35.89146],[128.609059,35.891211],[128.609059,35.891792],[128.608403,35.892041],[128.608403,35.89146]]]},"properties":{"id":"3565e175b4","level":17,"str":"1/22230233002322312","uid":"3847729352305278976"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.610371,35.890132],[128.611026,35.889884],[128.611026,35.890465],[128.610371,35.890713],[128.610371,35.890132]]]},"properties":{"id":"3565e175f4","level":17,"str":"1/22230233002322332","uid":"3847729353379020800"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.612994,35.883908],[128.61365,35.883659],[128.61365,35.88424],[128.612994,35.884489],[128.612994,35.883908]]]},"properties":{"id":"3565e19d2c","level":17,"str":"1/22230233003032211","uid":"3847729521822269440"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.611682,35.885568],[128.612338,35.885319],[128.612338,35.8859],[128.611682,35.886149],[128.611682,35.885568]]]},"properties":{"id":"3565e19d6c","level":17,"str":"1/22230233003032231","uid":"3847729522896011264"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.612994,35.886232],[128.61365,35.885984],[128.61365,35.886565],[128.612994,35.886813],[128.612994,35.886232]]]},"properties":{"id":"3565e19dac","level":17,"str":"1/22230233003032311","uid":"3847729523969753088"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.611682,35.887892],[128.612338,35.887643],[128.612338,35.888224],[128.611682,35.888473],[128.611682,35.887892]]]},"properties":{"id":"3565e19dec","level":17,"str":"1/22230233003032331","uid":"3847729525043494912"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.607091,35.891376],[128.607747,35.891127],[128.607747,35.891708],[128.607091,35.891957],[128.607091,35.891376]]]},"properties":{"id":"3565e1743c","level":17,"str":"1/22230233002322013","uid":"3847729345997045760"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.615617,35.88698],[128.616273,35.886732],[128.616273,35.887313],[128.615617,35.887561],[128.615617,35.88698]]]},"properties":{"id":"3565e19c04","level":17,"str":"1/22230233003032000","uid":"3847729516856213504"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.614305,35.886316],[128.614961,35.886067],[128.614961,35.886648],[128.614305,35.886897],[128.614305,35.886316]]]},"properties":{"id":"3565e19c44","level":17,"str":"1/22230233003032020","uid":"3847729517929955328"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.615617,35.884656],[128.616273,35.884408],[128.616273,35.884989],[128.615617,35.885237],[128.615617,35.884656]]]},"properties":{"id":"3565e19c84","level":17,"str":"1/22230233003032100","uid":"3847729519003697152"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.614305,35.883992],[128.614961,35.883743],[128.614961,35.884324],[128.614305,35.884573],[128.614305,35.883992]]]},"properties":{"id":"3565e19cc4","level":17,"str":"1/22230233003032120","uid":"3847729520077438976"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.616928,35.885902],[128.617584,35.885653],[128.617584,35.886234],[128.616928,35.886483],[128.616928,35.885902]]]},"properties":{"id":"3565e19bec","level":17,"str":"1/22230233003031331","uid":"3847729516453560320"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.611682,35.883825],[128.612338,35.883576],[128.612338,35.884157],[128.611682,35.884406],[128.611682,35.883825]]]},"properties":{"id":"3565e182b4","level":17,"str":"1/22230233003001112","uid":"3847729408139853824"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.604468,35.889466],[128.605124,35.889217],[128.605124,35.889798],[128.604468,35.890047],[128.604468,35.889466]]]},"properties":{"id":"3565e17124","level":17,"str":"1/22230233002320210","uid":"3847729332709490688"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.60578,35.886063],[128.606436,35.885814],[128.606436,35.886395],[128.60578,35.886644],[128.60578,35.886063]]]},"properties":{"id":"3565e179fc","level":17,"str":"1/22230233002330333","uid":"3847729370693107712"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.616928,35.887064],[128.617584,35.886815],[128.617584,35.887396],[128.616928,35.887645],[128.616928,35.887064]]]},"properties":{"id":"3565e1995c","level":17,"str":"1/22230233003030223","uid":"3847729505447706624"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.614961,35.89362],[128.615617,35.893371],[128.615617,35.893952],[128.614961,35.894201],[128.614961,35.89362]]]},"properties":{"id":"3565e1a194","level":17,"str":"1/22230233003100302","uid":"3847729540746969088"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.61365,35.892956],[128.614305,35.892707],[128.614305,35.893288],[128.61365,35.893537],[128.61365,35.892956]]]},"properties":{"id":"3565e1a1d4","level":17,"str":"1/22230233003100322","uid":"3847729541820710912"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.605124,35.886893],[128.60578,35.886644],[128.60578,35.887225],[128.605124,35.887474],[128.605124,35.886893]]]},"properties":{"id":"3565e170ac","level":17,"str":"1/22230233002320111","uid":"3847729330696224768"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.603812,35.888552],[128.604468,35.888304],[128.604468,35.888885],[128.603812,35.889133],[128.603812,35.888552]]]},"properties":{"id":"3565e170ec","level":17,"str":"1/22230233002320131","uid":"3847729331769966592"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.616273,35.890799],[128.616928,35.89055],[128.616928,35.891131],[128.616273,35.89138],[128.616273,35.890799]]]},"properties":{"id":"3565e198b4","level":17,"str":"1/22230233003030112","uid":"3847729502629134336"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.612338,35.894034],[128.612994,35.893785],[128.612994,35.894366],[128.612338,35.894615],[128.612338,35.894034]]]},"properties":{"id":"3565e1a03c","level":17,"str":"1/22230233003100013","uid":"3847729534975606784"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.612994,35.895528],[128.61365,35.89528],[128.61365,35.895861],[128.612994,35.896109],[128.612994,35.895528]]]},"properties":{"id":"3565e1a0fc","level":17,"str":"1/22230233003100133","uid":"3847729538196832256"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.607091,35.88789],[128.607747,35.887641],[128.607747,35.888222],[128.607091,35.888471],[128.607091,35.88789]]]},"properties":{"id":"3565e1771c","level":17,"str":"1/22230233002323203","uid":"3847729358345076736"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.606436,35.886395],[128.607091,35.886147],[128.607091,35.886728],[128.606436,35.886976],[128.606436,35.886395]]]},"properties":{"id":"3565e1775c","level":17,"str":"1/22230233002323223","uid":"3847729359418818560"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.608403,35.88623],[128.609059,35.885982],[128.609059,35.886563],[128.608403,35.886811],[128.608403,35.88623]]]},"properties":{"id":"3565e1779c","level":17,"str":"1/22230233002323303","uid":"3847729360492560384"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.610371,35.886065],[128.611026,35.885816],[128.611026,35.886397],[128.610371,35.886646],[128.610371,35.886065]]]},"properties":{"id":"3565e177dc","level":17,"str":"1/22230233002323323","uid":"3847729361566302208"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.614961,35.890134],[128.615617,35.889885],[128.615617,35.890466],[128.614961,35.890715],[128.614961,35.890134]]]},"properties":{"id":"3565e19f24","level":17,"str":"1/22230233003033210","uid":"3847729530277986304"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.614305,35.892126],[128.614961,35.891877],[128.614961,35.892458],[128.614305,35.892707],[128.614305,35.892126]]]},"properties":{"id":"3565e19f64","level":17,"str":"1/22230233003033230","uid":"3847729531351728128"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.612994,35.891461],[128.61365,35.891213],[128.61365,35.891794],[128.612994,35.892042],[128.612994,35.891461]]]},"properties":{"id":"3565e19fa4","level":17,"str":"1/22230233003033310","uid":"3847729532425469952"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.611026,35.892789],[128.611682,35.89254],[128.611682,35.893121],[128.611026,35.89337],[128.611026,35.892789]]]},"properties":{"id":"3565e19fe4","level":17,"str":"1/22230233003033330","uid":"3847729533499211776"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.610371,35.888389],[128.611026,35.888141],[128.611026,35.888722],[128.610371,35.88897],[128.610371,35.888389]]]},"properties":{"id":"3565e1761c","level":17,"str":"1/22230233002323003","uid":"3847729354050109440"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.608403,35.888554],[128.609059,35.888306],[128.609059,35.888887],[128.608403,35.889135],[128.608403,35.888554]]]},"properties":{"id":"3565e1765c","level":17,"str":"1/22230233002323023","uid":"3847729355123851264"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.607091,35.890214],[128.607747,35.889965],[128.607747,35.890546],[128.607091,35.890795],[128.607091,35.890214]]]},"properties":{"id":"3565e1769c","level":17,"str":"1/22230233002323103","uid":"3847729356197593088"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.606436,35.88872],[128.607091,35.888471],[128.607091,35.889052],[128.606436,35.889301],[128.606436,35.88872]]]},"properties":{"id":"3565e176dc","level":17,"str":"1/22230233002323123","uid":"3847729357271334912"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.611026,35.890465],[128.611682,35.890216],[128.611682,35.890797],[128.611026,35.891046],[128.611026,35.890465]]]},"properties":{"id":"3565e19e24","level":17,"str":"1/22230233003033010","uid":"3847729525983019008"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.612994,35.889137],[128.61365,35.888889],[128.61365,35.88947],[128.612994,35.889718],[128.612994,35.889137]]]},"properties":{"id":"3565e19e64","level":17,"str":"1/22230233003033030","uid":"3847729527056760832"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.614961,35.88781],[128.615617,35.887561],[128.615617,35.888142],[128.614961,35.888391],[128.614961,35.88781]]]},"properties":{"id":"3565e19ea4","level":17,"str":"1/22230233003033110","uid":"3847729528130502656"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.614305,35.889802],[128.614961,35.889553],[128.614961,35.890134],[128.614305,35.890383],[128.614305,35.889802]]]},"properties":{"id":"3565e19ee4","level":17,"str":"1/22230233003033130","uid":"3847729529204244480"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.609715,35.892705],[128.610371,35.892456],[128.610371,35.893037],[128.609715,35.893286],[128.609715,35.892705]]]},"properties":{"id":"3565e1756c","level":17,"str":"1/22230233002322231","uid":"3847729351097319424"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.608403,35.892041],[128.609059,35.891792],[128.609059,35.892373],[128.608403,35.892622],[128.608403,35.892041]]]},"properties":{"id":"3565e175ac","level":17,"str":"1/22230233002322311","uid":"3847729352171061248"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.609715,35.890381],[128.610371,35.890132],[128.610371,35.890713],[128.609715,35.890962],[128.609715,35.890381]]]},"properties":{"id":"3565e175ec","level":17,"str":"1/22230233002322331","uid":"3847729353244803072"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.612338,35.884157],[128.612994,35.883908],[128.612994,35.884489],[128.612338,35.884738],[128.612338,35.884157]]]},"properties":{"id":"3565e19d34","level":17,"str":"1/22230233003032212","uid":"3847729521956487168"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.611682,35.886149],[128.612338,35.8859],[128.612338,35.886481],[128.611682,35.88673],[128.611682,35.886149]]]},"properties":{"id":"3565e19d74","level":17,"str":"1/22230233003032232","uid":"3847729523030228992"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.612994,35.886813],[128.61365,35.886565],[128.61365,35.887146],[128.612994,35.887394],[128.612994,35.886813]]]},"properties":{"id":"3565e19db4","level":17,"str":"1/22230233003032312","uid":"3847729524103970816"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.611026,35.888141],[128.611682,35.887892],[128.611682,35.888473],[128.611026,35.888722],[128.611026,35.888141]]]},"properties":{"id":"3565e19df4","level":17,"str":"1/22230233003032332","uid":"3847729525177712640"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.607747,35.891127],[128.608403,35.890879],[128.608403,35.89146],[128.607747,35.891708],[128.607747,35.891127]]]},"properties":{"id":"3565e17434","level":17,"str":"1/22230233002322012","uid":"3847729345862828032"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.61365,35.887727],[128.614305,35.887478],[128.614305,35.888059],[128.61365,35.888308],[128.61365,35.887727]]]},"properties":{"id":"3565e19c2c","level":17,"str":"1/22230233003032011","uid":"3847729517527302144"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.614961,35.886067],[128.615617,35.885818],[128.615617,35.886399],[128.614961,35.886648],[128.614961,35.886067]]]},"properties":{"id":"3565e19c6c","level":17,"str":"1/22230233003032031","uid":"3847729518601043968"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.614305,35.884573],[128.614961,35.884324],[128.614961,35.884905],[128.614305,35.885154],[128.614305,35.884573]]]},"properties":{"id":"3565e19cec","level":17,"str":"1/22230233003032131","uid":"3847729520748527616"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.616928,35.885321],[128.617584,35.885072],[128.617584,35.885653],[128.616928,35.885902],[128.616928,35.885321]]]},"properties":{"id":"3565e19b94","level":17,"str":"1/22230233003031302","uid":"3847729514977165312"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.605124,35.886312],[128.60578,35.886063],[128.60578,35.886644],[128.605124,35.886893],[128.605124,35.886312]]]},"properties":{"id":"3565e17a04","level":17,"str":"1/22230233002331000","uid":"3847729370827325440"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.611026,35.884073],[128.611682,35.883825],[128.611682,35.884406],[128.611026,35.884654],[128.611026,35.884073]]]},"properties":{"id":"3565e182ac","level":17,"str":"1/22230233003001111","uid":"3847729408005636096"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.604468,35.890047],[128.605124,35.889798],[128.605124,35.890379],[128.604468,35.890628],[128.604468,35.890047]]]},"properties":{"id":"3565e1713c","level":17,"str":"1/22230233002320213","uid":"3847729333112143872"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.606436,35.885814],[128.607091,35.885566],[128.607091,35.886147],[128.606436,35.886395],[128.606436,35.885814]]]},"properties":{"id":"3565e179e4","level":17,"str":"1/22230233002330330","uid":"3847729370290454528"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.616928,35.887645],[128.617584,35.887396],[128.617584,35.887977],[128.616928,35.888226],[128.616928,35.887645]]]},"properties":{"id":"3565e19944","level":17,"str":"1/22230233003030220","uid":"3847729505045053440"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.61365,35.894699],[128.614305,35.89445],[128.614305,35.895031],[128.61365,35.89528],[128.61365,35.894699]]]},"properties":{"id":"3565e1a1ac","level":17,"str":"1/22230233003100311","uid":"3847729541149622272"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.614961,35.893039],[128.615617,35.89279],[128.615617,35.893371],[128.614961,35.89362],[128.614961,35.893039]]]},"properties":{"id":"3565e1a1ec","level":17,"str":"1/22230233003100331","uid":"3847729542223364096"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.6025,35.888469],[128.603156,35.88822],[128.603156,35.888801],[128.6025,35.88905],[128.6025,35.888469]]]},"properties":{"id":"3565e17064","level":17,"str":"1/22230233002320030","uid":"3847729329488265216"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.604468,35.887141],[128.605124,35.886893],[128.605124,35.887474],[128.604468,35.887722],[128.604468,35.887141]]]},"properties":{"id":"3565e170a4","level":17,"str":"1/22230233002320110","uid":"3847729330562007040"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.603812,35.889133],[128.604468,35.888885],[128.604468,35.889466],[128.603812,35.889714],[128.603812,35.889133]]]},"properties":{"id":"3565e170e4","level":17,"str":"1/22230233002320130","uid":"3847729331635748864"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.616928,35.889388],[128.617584,35.889139],[128.617584,35.88972],[128.616928,35.889969],[128.616928,35.889388]]]},"properties":{"id":"3565e198dc","level":17,"str":"1/22230233003030123","uid":"3847729503300222976"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.611682,35.894283],[128.612338,35.894034],[128.612338,35.894615],[128.611682,35.894864],[128.611682,35.894283]]]},"properties":{"id":"3565e1a014","level":17,"str":"1/22230233003100002","uid":"3847729534304518144"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.612994,35.894947],[128.61365,35.894699],[128.61365,35.89528],[128.612994,35.895528],[128.612994,35.894947]]]},"properties":{"id":"3565e1a054","level":17,"str":"1/22230233003100022","uid":"3847729535378259968"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.60578,35.887806],[128.606436,35.887557],[128.606436,35.888138],[128.60578,35.888387],[128.60578,35.887806]]]},"properties":{"id":"3565e17734","level":17,"str":"1/22230233002323212","uid":"3847729358747729920"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.607747,35.886479],[128.608403,35.88623],[128.608403,35.886811],[128.607747,35.88706],[128.607747,35.886479]]]},"properties":{"id":"3565e17774","level":17,"str":"1/22230233002323232","uid":"3847729359821471744"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.609059,35.887144],[128.609715,35.886895],[128.609715,35.887476],[128.609059,35.887725],[128.609059,35.887144]]]},"properties":{"id":"3565e177b4","level":17,"str":"1/22230233002323312","uid":"3847729360895213568"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.609715,35.885152],[128.610371,35.884903],[128.610371,35.885484],[128.609715,35.885733],[128.609715,35.885152]]]},"properties":{"id":"3565e177f4","level":17,"str":"1/22230233002323332","uid":"3847729361968955392"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.615617,35.889885],[128.616273,35.889637],[128.616273,35.890218],[128.615617,35.890466],[128.615617,35.889885]]]},"properties":{"id":"3565e19f2c","level":17,"str":"1/22230233003033211","uid":"3847729530412204032"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.614305,35.891545],[128.614961,35.891296],[128.614961,35.891877],[128.614305,35.892126],[128.614305,35.891545]]]},"properties":{"id":"3565e19f6c","level":17,"str":"1/22230233003033231","uid":"3847729531485945856"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.612994,35.89088],[128.61365,35.890632],[128.61365,35.891213],[128.612994,35.891461],[128.612994,35.89088]]]},"properties":{"id":"3565e19fac","level":17,"str":"1/22230233003033311","uid":"3847729532559687680"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.611682,35.89254],[128.612338,35.892291],[128.612338,35.892872],[128.611682,35.893121],[128.611682,35.89254]]]},"properties":{"id":"3565e19fec","level":17,"str":"1/22230233003033331","uid":"3847729533633429504"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.609715,35.888638],[128.610371,35.888389],[128.610371,35.88897],[128.609715,35.889219],[128.609715,35.888638]]]},"properties":{"id":"3565e17614","level":17,"str":"1/22230233002323002","uid":"3847729353915891712"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.608403,35.887973],[128.609059,35.887725],[128.609059,35.888306],[128.608403,35.888554],[128.608403,35.887973]]]},"properties":{"id":"3565e17654","level":17,"str":"1/22230233002323022","uid":"3847729354989633536"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.607091,35.889633],[128.607747,35.889384],[128.607747,35.889965],[128.607091,35.890214],[128.607091,35.889633]]]},"properties":{"id":"3565e17694","level":17,"str":"1/22230233002323102","uid":"3847729356063375360"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.60578,35.888968],[128.606436,35.88872],[128.606436,35.889301],[128.60578,35.889549],[128.60578,35.888968]]]},"properties":{"id":"3565e176d4","level":17,"str":"1/22230233002323122","uid":"3847729357137117184"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.611682,35.889054],[128.612338,35.888805],[128.612338,35.889386],[128.611682,35.889635],[128.611682,35.889054]]]},"properties":{"id":"3565e19e0c","level":17,"str":"1/22230233003033001","uid":"3847729525580365824"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.612338,35.890548],[128.612994,35.890299],[128.612994,35.89088],[128.612338,35.891129],[128.612338,35.890548]]]},"properties":{"id":"3565e19e4c","level":17,"str":"1/22230233003033021","uid":"3847729526654107648"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.61365,35.888889],[128.614305,35.88864],[128.614305,35.889221],[128.61365,35.88947],[128.61365,35.888889]]]},"properties":{"id":"3565e19e8c","level":17,"str":"1/22230233003033101","uid":"3847729527727849472"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.615617,35.888723],[128.616273,35.888475],[128.616273,35.889056],[128.615617,35.889304],[128.615617,35.888723]]]},"properties":{"id":"3565e19ecc","level":17,"str":"1/22230233003033121","uid":"3847729528801591296"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.610371,35.892456],[128.611026,35.892208],[128.611026,35.892789],[128.610371,35.893037],[128.610371,35.892456]]]},"properties":{"id":"3565e17564","level":17,"str":"1/22230233002322230","uid":"3847729350963101696"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.609059,35.891792],[128.609715,35.891543],[128.609715,35.892124],[128.609059,35.892373],[128.609059,35.891792]]]},"properties":{"id":"3565e175a4","level":17,"str":"1/22230233002322310","uid":"3847729352036843520"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.609715,35.8898],[128.610371,35.889551],[128.610371,35.890132],[128.609715,35.890381],[128.609715,35.8898]]]},"properties":{"id":"3565e175e4","level":17,"str":"1/22230233002322330","uid":"3847729353110585344"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.612994,35.88507],[128.61365,35.884822],[128.61365,35.885403],[128.612994,35.885651],[128.612994,35.88507]]]},"properties":{"id":"3565e19d1c","level":17,"str":"1/22230233003032203","uid":"3847729521553833984"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.611026,35.885235],[128.611682,35.884987],[128.611682,35.885568],[128.611026,35.885816],[128.611026,35.885235]]]},"properties":{"id":"3565e19d5c","level":17,"str":"1/22230233003032223","uid":"3847729522627575808"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.611682,35.88673],[128.612338,35.886481],[128.612338,35.887062],[128.611682,35.887311],[128.611682,35.88673]]]},"properties":{"id":"3565e19d9c","level":17,"str":"1/22230233003032303","uid":"3847729523701317632"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.612338,35.888224],[128.612994,35.887975],[128.612994,35.888556],[128.612338,35.888805],[128.612338,35.888224]]]},"properties":{"id":"3565e19ddc","level":17,"str":"1/22230233003032323","uid":"3847729524775059456"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.607747,35.890546],[128.608403,35.890298],[128.608403,35.890879],[128.607747,35.891127],[128.607747,35.890546]]]},"properties":{"id":"3565e1742c","level":17,"str":"1/22230233002322011","uid":"3847729345728610304"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.61365,35.887146],[128.614305,35.886897],[128.614305,35.887478],[128.61365,35.887727],[128.61365,35.887146]]]},"properties":{"id":"3565e19c34","level":17,"str":"1/22230233003032012","uid":"3847729517661519872"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.615617,35.885818],[128.616273,35.88557],[128.616273,35.886151],[128.615617,35.886399],[128.615617,35.885818]]]},"properties":{"id":"3565e19c74","level":17,"str":"1/22230233003032032","uid":"3847729518735261696"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.614305,35.885154],[128.614961,35.884905],[128.614961,35.885486],[128.614305,35.885735],[128.614305,35.885154]]]},"properties":{"id":"3565e19cf4","level":17,"str":"1/22230233003032132","uid":"3847729520882745344"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.617584,35.886234],[128.61824,35.885985],[128.61824,35.886566],[128.617584,35.886815],[128.617584,35.886234]]]},"properties":{"id":"3565e19bdc","level":17,"str":"1/22230233003031323","uid":"3847729516185124864"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.603812,35.890295],[128.604468,35.890047],[128.604468,35.890628],[128.603812,35.890876],[128.603812,35.890295]]]},"properties":{"id":"3565e17114","level":17,"str":"1/22230233002320202","uid":"3847729332441055232"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.605124,35.89096],[128.60578,35.890711],[128.60578,35.891292],[128.605124,35.891541],[128.605124,35.89096]]]},"properties":{"id":"3565e17154","level":17,"str":"1/22230233002320222","uid":"3847729333514797056"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.606436,35.885233],[128.607091,35.884984],[128.607091,35.885566],[128.606436,35.885814],[128.606436,35.885233]]]},"properties":{"id":"3565e179ec","level":17,"str":"1/22230233002330331","uid":"3847729370424672256"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.616273,35.887894],[128.616928,35.887645],[128.616928,35.888226],[128.616273,35.888475],[128.616273,35.887894]]]},"properties":{"id":"3565e1994c","level":17,"str":"1/22230233003030221","uid":"3847729505179271168"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.614305,35.89445],[128.614961,35.894201],[128.614961,35.894782],[128.614305,35.895031],[128.614305,35.89445]]]},"properties":{"id":"3565e1a1a4","level":17,"str":"1/22230233003100310","uid":"3847729541015404544"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.614961,35.892458],[128.615617,35.892209],[128.615617,35.89279],[128.614961,35.893039],[128.614961,35.892458]]]},"properties":{"id":"3565e1a1e4","level":17,"str":"1/22230233003100330","uid":"3847729542089146368"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.6025,35.88905],[128.603156,35.888801],[128.603156,35.889382],[128.6025,35.889631],[128.6025,35.88905]]]},"properties":{"id":"3565e1705c","level":17,"str":"1/22230233002320023","uid":"3847729329354047488"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.603812,35.88739],[128.604468,35.887141],[128.604468,35.887722],[128.603812,35.887971],[128.603812,35.88739]]]},"properties":{"id":"3565e1709c","level":17,"str":"1/22230233002320103","uid":"3847729330427789312"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.604468,35.888885],[128.605124,35.888636],[128.605124,35.889217],[128.604468,35.889466],[128.604468,35.888885]]]},"properties":{"id":"3565e170dc","level":17,"str":"1/22230233002320123","uid":"3847729331501531136"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.610371,35.884322],[128.611026,35.884073],[128.611026,35.884654],[128.610371,35.884903],[128.610371,35.884322]]]},"properties":{"id":"3565e17804","level":17,"str":"1/22230233002330000","uid":"3847729362237390848"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.616928,35.891131],[128.617584,35.890882],[128.617584,35.891463],[128.616928,35.891712],[128.616928,35.891131]]]},"properties":{"id":"3565e198a4","level":17,"str":"1/22230233003030110","uid":"3847729502360698880"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.611026,35.894532],[128.611682,35.894283],[128.611682,35.894864],[128.611026,35.895113],[128.611026,35.894532]]]},"properties":{"id":"3565e1a00c","level":17,"str":"1/22230233003100001","uid":"3847729534170300416"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.612994,35.894366],[128.61365,35.894118],[128.61365,35.894699],[128.612994,35.894947],[128.612994,35.894366]]]},"properties":{"id":"3565e1a04c","level":17,"str":"1/22230233003100021","uid":"3847729535244042240"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.607747,35.88706],[128.608403,35.886811],[128.608403,35.887392],[128.607747,35.887641],[128.607747,35.88706]]]},"properties":{"id":"3565e1770c","level":17,"str":"1/22230233002323201","uid":"3847729358076641280"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.60578,35.887225],[128.606436,35.886976],[128.606436,35.887557],[128.60578,35.887806],[128.60578,35.887225]]]},"properties":{"id":"3565e1774c","level":17,"str":"1/22230233002323221","uid":"3847729359150383104"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.609059,35.8854],[128.609715,35.885152],[128.609715,35.885733],[128.609059,35.885982],[128.609059,35.8854]]]},"properties":{"id":"3565e1778c","level":17,"str":"1/22230233002323301","uid":"3847729360224124928"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.609715,35.886895],[128.610371,35.886646],[128.610371,35.887227],[128.609715,35.887476],[128.609715,35.886895]]]},"properties":{"id":"3565e177cc","level":17,"str":"1/22230233002323321","uid":"3847729361297866752"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.614305,35.890964],[128.614961,35.890715],[128.614961,35.891296],[128.614305,35.891545],[128.614305,35.890964]]]},"properties":{"id":"3565e19f14","level":17,"str":"1/22230233003033202","uid":"3847729530009550848"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.615617,35.891628],[128.616273,35.89138],[128.616273,35.891961],[128.615617,35.892209],[128.615617,35.891628]]]},"properties":{"id":"3565e19f54","level":17,"str":"1/22230233003033222","uid":"3847729531083292672"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.612338,35.892291],[128.612994,35.892042],[128.612994,35.892623],[128.612338,35.892872],[128.612338,35.892291]]]},"properties":{"id":"3565e19f94","level":17,"str":"1/22230233003033302","uid":"3847729532157034496"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.611026,35.891627],[128.611682,35.891378],[128.611682,35.891959],[128.611026,35.892208],[128.611026,35.891627]]]},"properties":{"id":"3565e19fd4","level":17,"str":"1/22230233003033322","uid":"3847729533230776320"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.609715,35.889219],[128.610371,35.88897],[128.610371,35.889551],[128.609715,35.8898],[128.609715,35.889219]]]},"properties":{"id":"3565e1760c","level":17,"str":"1/22230233002323001","uid":"3847729353781673984"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.609059,35.887725],[128.609715,35.887476],[128.609715,35.888057],[128.609059,35.888306],[128.609059,35.887725]]]},"properties":{"id":"3565e1764c","level":17,"str":"1/22230233002323021","uid":"3847729354855415808"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.607747,35.889384],[128.608403,35.889135],[128.608403,35.889716],[128.607747,35.889965],[128.607747,35.889384]]]},"properties":{"id":"3565e1768c","level":17,"str":"1/22230233002323101","uid":"3847729355929157632"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.60578,35.889549],[128.606436,35.889301],[128.606436,35.889882],[128.60578,35.89013],[128.60578,35.889549]]]},"properties":{"id":"3565e176cc","level":17,"str":"1/22230233002323121","uid":"3847729357002899456"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.611682,35.889635],[128.612338,35.889386],[128.612338,35.889967],[128.611682,35.890216],[128.611682,35.889635]]]},"properties":{"id":"3565e19e14","level":17,"str":"1/22230233003033002","uid":"3847729525714583552"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.612994,35.890299],[128.61365,35.890051],[128.61365,35.890632],[128.612994,35.89088],[128.612994,35.890299]]]},"properties":{"id":"3565e19e54","level":17,"str":"1/22230233003033022","uid":"3847729526788325376"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.614305,35.88864],[128.614961,35.888391],[128.614961,35.888972],[128.614305,35.889221],[128.614305,35.88864]]]},"properties":{"id":"3565e19e94","level":17,"str":"1/22230233003033102","uid":"3847729527862067200"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.615617,35.889304],[128.616273,35.889056],[128.616273,35.889637],[128.615617,35.889885],[128.615617,35.889304]]]},"properties":{"id":"3565e19ed4","level":17,"str":"1/22230233003033122","uid":"3847729528935809024"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.610371,35.893037],[128.611026,35.892789],[128.611026,35.89337],[128.610371,35.893618],[128.610371,35.893037]]]},"properties":{"id":"3565e1755c","level":17,"str":"1/22230233002322223","uid":"3847729350828883968"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.609715,35.891543],[128.610371,35.891294],[128.610371,35.891875],[128.609715,35.892124],[128.609715,35.891543]]]},"properties":{"id":"3565e1759c","level":17,"str":"1/22230233002322303","uid":"3847729351902625792"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.609059,35.890049],[128.609715,35.8898],[128.609715,35.890381],[128.609059,35.89063],[128.609059,35.890049]]]},"properties":{"id":"3565e175dc","level":17,"str":"1/22230233002322323","uid":"3847729352976367616"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.612994,35.884489],[128.61365,35.88424],[128.61365,35.884822],[128.612994,35.88507],[128.612994,35.884489]]]},"properties":{"id":"3565e19d24","level":17,"str":"1/22230233003032210","uid":"3847729521688051712"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.611026,35.885816],[128.611682,35.885568],[128.611682,35.886149],[128.611026,35.886397],[128.611026,35.885816]]]},"properties":{"id":"3565e19d64","level":17,"str":"1/22230233003032230","uid":"3847729522761793536"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.612338,35.886481],[128.612994,35.886232],[128.612994,35.886813],[128.612338,35.887062],[128.612338,35.886481]]]},"properties":{"id":"3565e19da4","level":17,"str":"1/22230233003032310","uid":"3847729523835535360"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.611682,35.888473],[128.612338,35.888224],[128.612338,35.888805],[128.611682,35.889054],[128.611682,35.888473]]]},"properties":{"id":"3565e19de4","level":17,"str":"1/22230233003032330","uid":"3847729524909277184"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.607091,35.890795],[128.607747,35.890546],[128.607747,35.891127],[128.607091,35.891376],[128.607091,35.890795]]]},"properties":{"id":"3565e17424","level":17,"str":"1/22230233002322010","uid":"3847729345594392576"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.614961,35.887229],[128.615617,35.88698],[128.615617,35.887561],[128.614961,35.88781],[128.614961,35.887229]]]},"properties":{"id":"3565e19c1c","level":17,"str":"1/22230233003032003","uid":"3847729517258866688"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.614305,35.885735],[128.614961,35.885486],[128.614961,35.886067],[128.614305,35.886316],[128.614305,35.885735]]]},"properties":{"id":"3565e19c5c","level":17,"str":"1/22230233003032023","uid":"3847729518332608512"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.615617,35.884075],[128.616273,35.883827],[128.616273,35.884408],[128.615617,35.884656],[128.615617,35.884075]]]},"properties":{"id":"3565e19c9c","level":17,"str":"1/22230233003032103","uid":"3847729519406350336"}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[128.61365,35.88424],[128.614305,35.883992],[128.614305,35.884573],[128.61365,35.884822],[128.61365,35.88424]]]},"properties":{"id":"3565e19cdc","level":17,"str":"1/22230233003032123","uid":"3847729520480092160"}}]} \ No newline at end of file diff --git a/src/main/resources/luascript/save_user_location.lua b/src/main/resources/luascript/save_user_location.lua new file mode 100644 index 0000000..865e3d3 --- /dev/null +++ b/src/main/resources/luascript/save_user_location.lua @@ -0,0 +1,38 @@ +-- KEYS: +-- [1] userIdKey +-- [2] cellIdExpiriesKey +-- [3] cellIdUsersKey + +-- ARGV: +-- [1] newCellId +-- [2] encodedUserId +-- [3] TTL_SECONDS +-- [4] expireAt + +local newCellId = ARGV[1] +local encodedUserId = ARGV[2] +local ttl_seconds = tonumber(ARGV[3]) +local expireAt = tonumber(ARGV[4]) +local cellIdNotInTargetAreaFlag = tonumber(ARGV[5]) + +local prevCellId = redis.call('GET', KEYS[1]) + +if prevCellId then + if prevCellId == newCellId then + redis.call('EXPIRE', KEYS[2], ttl_seconds) + return + end + + local oldUsersKey = "cell:" .. prevCellId .. ":users" + redis.call('SREM', oldUsersKey, encodedUserId) +end + +if cellIdNotInTargetAreaFlag == 1 then + return +end + +redis.call('SET', KEYS[1], newCellId, 'EX', ttl_seconds) + +redis.call('SADD', KEYS[3], encodedUserId) + +redis.call('ZADD', KEYS[2], expireAt, encodedUserId) \ No newline at end of file diff --git a/src/test/java/com/knu/ddip/DdipApplicationTests.java b/src/test/java/com/knu/ddip/DdipApplicationTests.java index 5485b8d..b3f0efa 100644 --- a/src/test/java/com/knu/ddip/DdipApplicationTests.java +++ b/src/test/java/com/knu/ddip/DdipApplicationTests.java @@ -6,7 +6,9 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +@ActiveProfiles("test") @SpringBootTest @ExtendWith({RedisTestContainerConfig.class, MySQLTestContainerConfig.class, TestEnvironmentConfig.class}) class DdipApplicationTests { diff --git a/src/test/java/com/knu/ddip/auth/infrastructure/repository/RedisTokenRepositoryImplIntegrationTest.java b/src/test/java/com/knu/ddip/auth/infrastructure/repository/RedisTokenRepositoryImplIntegrationTest.java index 6cec3d5..66d38ea 100644 --- a/src/test/java/com/knu/ddip/auth/infrastructure/repository/RedisTokenRepositoryImplIntegrationTest.java +++ b/src/test/java/com/knu/ddip/auth/infrastructure/repository/RedisTokenRepositoryImplIntegrationTest.java @@ -14,12 +14,14 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.context.annotation.Import; import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.test.context.ActiveProfiles; import java.util.Optional; import java.util.UUID; import static org.assertj.core.api.Assertions.assertThat; +@ActiveProfiles("test") @SpringBootTest @ExtendWith({RedisTestContainerConfig.class, MySQLTestContainerConfig.class, TestEnvironmentConfig.class}) @Import(IntegrationTestConfig.class) diff --git a/src/test/java/com/knu/ddip/config/IntegrationTestConfig.java b/src/test/java/com/knu/ddip/config/IntegrationTestConfig.java index f9c2fa8..8307ac2 100644 --- a/src/test/java/com/knu/ddip/config/IntegrationTestConfig.java +++ b/src/test/java/com/knu/ddip/config/IntegrationTestConfig.java @@ -2,6 +2,8 @@ import org.springframework.boot.test.context.TestConfiguration; import org.springframework.context.annotation.Import; +import org.springframework.test.context.ActiveProfiles; + @TestConfiguration @Import({ diff --git a/src/test/java/com/knu/ddip/config/RedisTestConfig.java b/src/test/java/com/knu/ddip/config/RedisTestConfig.java index 5685251..9ac8c2f 100644 --- a/src/test/java/com/knu/ddip/config/RedisTestConfig.java +++ b/src/test/java/com/knu/ddip/config/RedisTestConfig.java @@ -1,5 +1,8 @@ package com.knu.ddip.config; +import org.redisson.Redisson; +import org.redisson.api.RedissonClient; +import org.redisson.config.Config; import org.springframework.boot.test.context.TestConfiguration; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Primary; @@ -8,6 +11,7 @@ import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer; +import org.springframework.test.context.ActiveProfiles; @TestConfiguration public class RedisTestConfig { @@ -39,4 +43,19 @@ public RedisTemplate testRedisTemplate(LettuceConnectionFactory template.afterPropertiesSet(); return template; } + + @Bean + @Primary + public RedissonClient testRedissonClient() { + String host = System.getProperty("spring.data.redis.host", "localhost"); + int port = Integer.parseInt(System.getProperty("spring.data.redis.port", "6379")); + String password = System.getProperty("spring.data.redis.password", ""); + + Config config = new Config(); + String address = String.format("redis://%s:%d", host, port); + config.useSingleServer() + .setAddress(address) + .setPassword(password.isEmpty() ? null : password); + return Redisson.create(config); + } } diff --git a/src/test/java/com/knu/ddip/location/application/service/LocationServiceTest.java b/src/test/java/com/knu/ddip/location/application/service/LocationServiceTest.java new file mode 100644 index 0000000..18f1485 --- /dev/null +++ b/src/test/java/com/knu/ddip/location/application/service/LocationServiceTest.java @@ -0,0 +1,339 @@ +package com.knu.ddip.location.application.service; + +import com.knu.ddip.config.IntegrationTestConfig; +import com.knu.ddip.config.MySQLTestContainerConfig; +import com.knu.ddip.config.RedisTestContainerConfig; +import com.knu.ddip.config.TestEnvironmentConfig; +import com.knu.ddip.location.application.dto.UpdateMyLocationRequest; +import com.knu.ddip.location.application.util.S2Converter; +import com.knu.ddip.location.application.util.UuidBase64Utils; +import com.knu.ddip.location.exception.LocationNotFoundException; +import com.knu.ddip.location.infrastructure.repository.LocationJpaRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Import; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.test.context.ActiveProfiles; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import java.util.UUID; +import java.util.concurrent.*; + +import static com.knu.ddip.location.application.util.LocationKeyFactory.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@ActiveProfiles("test") +@SpringBootTest +@ExtendWith({RedisTestContainerConfig.class, MySQLTestContainerConfig.class, TestEnvironmentConfig.class}) +@Import(IntegrationTestConfig.class) +class LocationServiceTest { + + public static final int LEVEL = 17; + @Autowired + LocationService locationService; + @Autowired + RedisTemplate redisTemplate; + @Autowired + LocationJpaRepository locationJpaRepository; + + @BeforeEach + void setUp() { + redisTemplate.getConnectionFactory().getConnection().flushAll(); + } + + @Test + void saveUserLocationTest() { + // given + UUID userId = UUID.randomUUID(); + String encodedUserId = UuidBase64Utils.uuidToBase64String(userId); + + double lat = 35.8889737; + double lng = 128.6099251; + + String cellId = S2Converter.toCellId(lat, lng, LEVEL).toToken(); + + UpdateMyLocationRequest request = UpdateMyLocationRequest.of(lat, lng); + + // when + locationService.saveUserLocationAtomic(userId, request); + + // then + // user:{userId} 확인 + String userIdKey = "user:" + encodedUserId; + String saveCellId = (String) redisTemplate.opsForValue().get(userIdKey); + assertThat(saveCellId).isEqualTo(cellId); + // set 확인 + String cellIdUsersKey = createCellIdUsersKey(cellId); + assertThat(redisTemplate.opsForSet().isMember(cellIdUsersKey, encodedUserId)).isTrue(); + // zset 확인 + String cellIdExpiriesKey = createCellIdExpiriesKey(cellId); + assertThat(redisTemplate.opsForZSet().score(cellIdExpiriesKey, encodedUserId)).isNotNull(); + } + + @DisplayName("이전 유저 위치 정보 삭제 후 저장") + @Test + void saveUserLocationRemovesOldLocationAndSavesNewOne() { + // given + UUID userId = UUID.randomUUID(); + String encodedUserId = UuidBase64Utils.uuidToBase64String(userId); + + double lat = 35.8889737; + double lng = 128.6099251; + + String cellId = S2Converter.toCellId(lat, lng, LEVEL).toToken(); + + UpdateMyLocationRequest request = UpdateMyLocationRequest.of(lat, lng); + + locationService.saveUserLocationAtomic(userId, request); + + double newLat = 35.8929024; + double newLng = 128.6122855; + + UpdateMyLocationRequest newRequest = UpdateMyLocationRequest.of(newLat, newLng); + + // when + locationService.saveUserLocationAtomic(userId, newRequest); + + // then + // user:{userId} 삭제 확인 + String userIdKey = createUserIdKey(encodedUserId); + String saveCellId = (String) redisTemplate.opsForValue().get(userIdKey); + assertThat(saveCellId).isNotEqualTo(cellId); + // set 삭제 확인 + String cellIdUsersKey = createCellIdUsersKey(cellId); + assertThat(redisTemplate.opsForSet().isMember(cellIdUsersKey, encodedUserId)).isFalse(); + } + + @DisplayName("예전 위치가 존재하지만 현재 위치와 같으면 바로 리턴") + @Test + void saveUserLocationWithSameOldLocation() { + // given + UUID userId = UUID.randomUUID(); + String encodedUserId = UuidBase64Utils.uuidToBase64String(userId); + + double lat = 35.8889737; + double lng = 128.6099251; + + String cellId = S2Converter.toCellId(lat, lng, LEVEL).toToken(); + + UpdateMyLocationRequest request = UpdateMyLocationRequest.of(lat, lng); + + locationService.saveUserLocationAtomic(userId, request); + + UpdateMyLocationRequest newRequest = UpdateMyLocationRequest.of(lat, lng); + + // when + locationService.saveUserLocationAtomic(userId, newRequest); + + // then + // user:{userId} 삭제 확인 + String userIdKey = createUserIdKey(encodedUserId); + String saveCellId = (String) redisTemplate.opsForValue().get(userIdKey); + assertThat(saveCellId).isEqualTo(cellId); + // set 삭제 확인 + String cellIdUsersKey = createCellIdUsersKey(cellId); + assertThat(redisTemplate.opsForSet().isMember(cellIdUsersKey, encodedUserId)).isTrue(); + } + + @Test + void getNeighborRecipientUserIdsTest() { + // given + UUID myUserId = UUID.randomUUID(); + + // 일청담 + double requestLat = 35.8886597; + double requestLng = 128.612138; + + UpdateMyLocationRequest request = UpdateMyLocationRequest.of(requestLat, requestLng); + locationService.saveUserLocationAtomic(myUserId, request); + + double[][] latsLngs = { + {35.8891866, 128.6121152}, // 포함 : 시계탑 + {35.8888694, 128.6115197}, // 포함 : 테니스장 우측 + {35.8885521, 128.6126731}, // 포함 : 일청담 왼쪽 갈림길 + {35.8879915, 128.6066381}, // 제외 : 축구장 + {35.8917529, 128.6122976}, // 제외 : 도서관 + }; + + List userIds = new ArrayList<>(); + for (int i = 0; i < latsLngs.length; i++) { + UUID userId = UUID.randomUUID(); + if (i < 3) { + userIds.add(userId); + } + double[] data = latsLngs[i]; + request = UpdateMyLocationRequest.of(data[0], data[1]); + locationService.saveUserLocationAtomic(userId, request); + } + + // when + List neighborRecipientUserIds = locationService.getNeighborRecipientUserIds(myUserId, requestLat, requestLng); + + // then + assertThat(neighborRecipientUserIds).containsAll(userIds); + assertThat(neighborRecipientUserIds).doesNotContain(myUserId); + } + + @Test + void getNeighborCellIdsNotInTargetAreaTest() { + // given + double outsideTargetAreaLat = 1.0; + double outsideTargetAreaLng = 1.0; + + // when // then + assertThatThrownBy(() -> locationService.getNeighborCellIds(outsideTargetAreaLat, outsideTargetAreaLng)) + .isInstanceOf(LocationNotFoundException.class) + .hasMessage("위치를 찾을 수 없습니다."); + } + + @Test + void getNeighborCellIdsTest() { + // given + double lat = 35.8890084; + double lng = 128.6107405; + + // when + List neighborCellIds = locationService.getNeighborCellIds(lat, lng); + + // then + assertThat(neighborCellIds).hasSize(9); + } + + @Test + void getNeighborCellIdsAtEdgeTest() { + // given + double lat = 35.8928546; + double lng = 128.608922; + + // when + List neighborCellIds = locationService.getNeighborCellIds(lat, lng); + + // then + assertThat(neighborCellIds).hasSize(6); + } + + @Test + void saveUserIdByCellIdTest() { + // given + UUID userId = UUID.randomUUID(); + String encodedUserId = UuidBase64Utils.uuidToBase64String(userId); + + String userIdKey = createUserIdKey(encodedUserId); + + double requestLat = 35.8886597; + double requestLng = 128.612138; + + String cellId = S2Converter.toCellId(requestLat, requestLng, LEVEL).toToken(); + String cellIdUsersKey = createCellIdUsersKey(cellId); + String cellIdExpiriesKey = createCellIdExpiriesKey(cellId); + + UpdateMyLocationRequest request = UpdateMyLocationRequest.of(requestLat, requestLng); + + // when + locationService.saveUserLocationAtomic(userId, request); + + // then + assertThat(redisTemplate.opsForValue().get(userIdKey)).isEqualTo(cellId); + assertThat(redisTemplate.opsForSet().isMember(cellIdUsersKey, encodedUserId)).isTrue(); + assertThat(redisTemplate.opsForZSet().score(cellIdExpiriesKey, encodedUserId)).isNotNull(); + } + + @Test + void saveUserIdByCellIdAtOutsideOfTargetAreaTest() { + // given + UUID userId = UUID.randomUUID(); + String encodedUserId = UuidBase64Utils.uuidToBase64String(userId); + + String userIdKey = createUserIdKey(encodedUserId); + + double requestLat = 35.8942626; + double requestLng = 128.6066904; + + String cellId = S2Converter.toCellId(requestLat, requestLng, LEVEL).toToken(); + String cellIdUsersKey = createCellIdUsersKey(cellId); + String cellIdExpiriesKey = createCellIdExpiriesKey(cellId); + + UpdateMyLocationRequest request = UpdateMyLocationRequest.of(requestLat, requestLng); + + // when + locationService.saveUserLocationAtomic(userId, request); + + // then + assertThat(redisTemplate.opsForValue().get(userIdKey)).isNull(); + assertThat(redisTemplate.opsForSet().isMember(cellIdUsersKey, encodedUserId)).isFalse(); + assertThat(redisTemplate.opsForZSet().score(cellIdExpiriesKey, encodedUserId)).isNull(); + } + + @Test + void saveUserLocationAtomicConcurrencyTest() throws InterruptedException { + // given + UUID userId = UUID.randomUUID(); + + double[][] latsLngs = { + {35.8891866, 128.6121152}, // 시계탑 + {35.8888694, 128.6115197}, // 테니스장 우측 + {35.8885521, 128.6126731}, // 일청담 왼쪽 갈림길 + {35.8879915, 128.6066381}, // 축구장 + {35.8917529, 128.6122976}, // 도서관 + {35.8910771, 128.6106976}, // 인문대 + {35.8887477, 128.6137017}, // 박물관 + {35.8880002, 128.6060198}, // 대운동장 + {35.886227, 128.6148282}, // 센트럴파크 + {35.8925547, 128.6143937}, // 간호대 + }; + + Random rand = new Random(); + rand.setSeed(System.currentTimeMillis()); + + int threads = 100; + int poolSize = 32; + + ExecutorService pool = Executors.newFixedThreadPool(poolSize); + CountDownLatch startLatch = new CountDownLatch(1); + CountDownLatch doneLatch = new CountDownLatch(threads); + + List> futures = new ArrayList<>(threads); + for (int i = 0; i < threads; i++) { + int index = rand.nextInt(latsLngs.length); + double[] coord = latsLngs[index]; + UpdateMyLocationRequest request = UpdateMyLocationRequest.of(coord[0], coord[1]); + futures.add(pool.submit(() -> { + try { + startLatch.await(); + locationService.saveUserLocationAtomic(userId, request); + } catch (Exception e) { + throw new RuntimeException(e); + } finally { + doneLatch.countDown(); + } + })); + } + + // when + startLatch.countDown(); // 동시에 출발 + boolean allDone = doneLatch.await(20, TimeUnit.SECONDS); + pool.shutdown(); + boolean terminated = pool.awaitTermination(30, TimeUnit.SECONDS); + + String encodedUserId = UuidBase64Utils.uuidToBase64String(userId); + String userIdKey = createUserIdKey(encodedUserId); + + String cellId = redisTemplate.opsForValue().get(userIdKey); + String cellIdUsersKey = createCellIdUsersKey(cellId); + String cellIdExpiriesKey = createCellIdExpiriesKey(cellId); + + // then + assertThat(allDone).isTrue(); + assertThat(terminated).isTrue(); + assertThat(redisTemplate.opsForSet().isMember(cellIdUsersKey, encodedUserId)).isTrue(); + assertThat(redisTemplate.opsForZSet().score(cellIdExpiriesKey, encodedUserId)).isNotNull(); + } + +} \ No newline at end of file diff --git a/src/test/java/com/knu/ddip/location/application/util/UuidBase64UtilsTest.java b/src/test/java/com/knu/ddip/location/application/util/UuidBase64UtilsTest.java new file mode 100644 index 0000000..f1cea50 --- /dev/null +++ b/src/test/java/com/knu/ddip/location/application/util/UuidBase64UtilsTest.java @@ -0,0 +1,26 @@ +package com.knu.ddip.location.application.util; + +import com.knu.ddip.location.application.util.UuidBase64Utils; +import org.junit.jupiter.api.Test; + +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; + +class UuidBase64UtilsTest { + + @Test + void uuidEncodeAndDecodeTest() { + // given + UUID uuid = UUID.randomUUID(); + + // when + String encodedUuid = UuidBase64Utils.uuidToBase64String(uuid); + + UUID decodedUuid = UuidBase64Utils.base64StringToUuid(encodedUuid); + + // then + assertThat(decodedUuid).isEqualTo(uuid); + } + +} \ No newline at end of file diff --git a/src/test/java/com/knu/ddip/location/infrastructure/repository/LocationReaderImplTest.java b/src/test/java/com/knu/ddip/location/infrastructure/repository/LocationReaderImplTest.java new file mode 100644 index 0000000..3c1b23f --- /dev/null +++ b/src/test/java/com/knu/ddip/location/infrastructure/repository/LocationReaderImplTest.java @@ -0,0 +1,148 @@ +package com.knu.ddip.location.infrastructure.repository; + +import com.knu.ddip.config.IntegrationTestConfig; +import com.knu.ddip.config.MySQLTestContainerConfig; +import com.knu.ddip.config.RedisTestContainerConfig; +import com.knu.ddip.config.TestEnvironmentConfig; +import com.knu.ddip.location.exception.LocationNotFoundException; +import com.knu.ddip.location.infrastructure.entity.LocationEntity; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Import; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +@ActiveProfiles("test") +@Transactional +@SpringBootTest +@ExtendWith({RedisTestContainerConfig.class, MySQLTestContainerConfig.class, TestEnvironmentConfig.class}) +@Import(IntegrationTestConfig.class) +class LocationReaderImplTest { + + @Autowired + LocationReaderImpl locationReader; + @Autowired + LocationWriterImpl locationWriter; + @Autowired + LocationJpaRepository locationJpaRepository; + @Autowired + RedisTemplate redisTemplate; + @Autowired + private LocationReaderImpl locationReaderImpl; + + @BeforeEach + void setUp() { + redisTemplate.getConnectionFactory().getConnection().flushAll(); + } + + @Test + void validateLocationByValidCellIdTest() { + // given + String validCellId = "validCellId"; + locationJpaRepository.save(LocationEntity.create(validCellId)); + + // when // then + assertDoesNotThrow(() -> locationReader.validateLocationByCellId(validCellId)); + } + + @Test + void validateLocationByInvalidCellIdTest() { + // given + String validCellId = "invalidCellId"; + + // when // then + assertThatThrownBy(() -> locationReader.validateLocationByCellId(validCellId)) + .isInstanceOf(LocationNotFoundException.class) + .hasMessage("위치를 찾을 수 없습니다."); + } + + @Test + void findAllLocationsByCellIdInTest() { + // given + List cellIds = List.of( + "findAllLocationsByCellIdInTest1", + "findAllLocationsByCellIdInTest2" + ); + List locations = cellIds.stream() + .map(LocationEntity::create) + .collect(Collectors.toList()); + locationJpaRepository.saveAll(locations); + + // when + List findCellIds = locationReader.findAllLocationsByCellIdIn(cellIds); + + // then + assertThat(findCellIds).hasSize(2) + .containsAll(cellIds); + } + + @Test + void findUserIdsByCellIdsTest() { + // 여 테스트 작성 + // given + List cellIds = List.of( + "findUserIdsByCellIdsTest1", + "findUserIdsByCellIdsTest2" + ); + List locations = cellIds.stream() + .map(LocationEntity::create) + .collect(Collectors.toList()); + locationJpaRepository.saveAll(locations); + + List userIds = List.of( + UUID.randomUUID().toString(), + UUID.randomUUID().toString() + ); + + for (int i = 0; i < 2; i++) { + locationWriter.saveUserIdByCellIdAtomic(cellIds.get(i), false, userIds.get(i)); + } + // 포함되지 않는 셀, 유저 데이터 + locationWriter.saveUserIdByCellIdAtomic("notIncludedCellId", true, UUID.randomUUID().toString()); + + // when + List findCellIds = locationReader.findUserIdsByCellIds(cellIds); + + // then + assertThat(findCellIds).hasSize(2) + .containsAll(userIds); + } + + @Test + void isCellIdNotInTargetAreaWithValidCellIdTest() { + // given + String validCellId = "3565e170b4"; + + // when + boolean cellIdNotInTargetArea = locationReaderImpl.isCellIdNotInTargetArea(validCellId); + + // then + assertThat(cellIdNotInTargetArea).isFalse(); + } + + @Test + void isCellIdNotInTargetAreaWithInvalidCellIdTest() { + // given + String validCellId = "invalidCellId"; + + // when + boolean cellIdNotInTargetArea = locationReaderImpl.isCellIdNotInTargetArea(validCellId); + + // then + assertThat(cellIdNotInTargetArea).isTrue(); + } + +} \ No newline at end of file diff --git a/src/test/java/com/knu/ddip/location/infrastructure/repository/LocationWriterImplTest.java b/src/test/java/com/knu/ddip/location/infrastructure/repository/LocationWriterImplTest.java new file mode 100644 index 0000000..b7531d3 --- /dev/null +++ b/src/test/java/com/knu/ddip/location/infrastructure/repository/LocationWriterImplTest.java @@ -0,0 +1,121 @@ +package com.knu.ddip.location.infrastructure.repository; + +import com.knu.ddip.config.IntegrationTestConfig; +import com.knu.ddip.config.MySQLTestContainerConfig; +import com.knu.ddip.config.RedisTestContainerConfig; +import com.knu.ddip.config.TestEnvironmentConfig; +import com.knu.ddip.location.infrastructure.entity.LocationEntity; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Import; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.stream.Collectors; + +import static com.knu.ddip.location.application.util.LocationKeyFactory.*; +import static org.assertj.core.api.Assertions.assertThat; + +@ActiveProfiles("test") +@Transactional +@SpringBootTest +@ExtendWith({RedisTestContainerConfig.class, MySQLTestContainerConfig.class, TestEnvironmentConfig.class}) +@Import(IntegrationTestConfig.class) +class LocationWriterImplTest { + + @Autowired + LocationReaderImpl locationReader; + @Autowired + LocationWriterImpl locationWriter; + @Autowired + LocationJpaRepository locationJpaRepository; + @Autowired + RedisTemplate redisTemplate; + + @BeforeEach + void setUp() { + redisTemplate.getConnectionFactory().getConnection().flushAll(); + } + + @Test + void saveAllTest() { + // given + List cellIds = List.of("a", "b", "c"); + + // when + locationWriter.saveAll(cellIds); + + List locations = locationJpaRepository.findAll(); + List findCellIds = locations.stream() + .map(LocationEntity::getCellId) + .collect(Collectors.toList()); + + // then + assertThat(findCellIds).containsAll(cellIds); + } + + @Test + void saveUserIdByCellIdAtomicTest() { + // given + String userId = "saveUserIdByCellIdTest"; + String cellId = "saveUserIdByCellIdTest"; + + String cellIdUsersKey = createCellIdUsersKey(cellId); + String cellIdExpiriesKey = createCellIdExpiriesKey(cellId); + + // when + locationWriter.saveUserIdByCellIdAtomic(cellId, false, userId); + + // then + assertThat(redisTemplate.opsForSet().isMember(cellIdUsersKey, cellId)).isTrue(); + assertThat(redisTemplate.opsForZSet().score(cellIdExpiriesKey, cellId)).isNotNull(); + } + + @Test + void saveCellIdByUserIdTest() { + // given + String userId = "saveCellIdByUserIdTest"; + String cellId = "saveCellIdByUserIdTest"; + String userIdKey = createUserIdKey(userId); + + // when + redisTemplate.opsForSet().add(userIdKey, cellId); + + // then + assertThat(redisTemplate.opsForSet().isMember(userIdKey, cellId)).isTrue(); + } + + @Test + void cleanupExpiredUserLocationsTest() { + // given + long now = System.currentTimeMillis(); + + String cellId = "cellId"; + String cellUsersKey = "cell:" + cellId + ":users"; + String cellExpiryKey = "cell:" + cellId + ":expiry"; + + // 만료된 위치 + String userId = "userId"; + long expireAt = now - (2 * 3600 * 1000L); + redisTemplate.opsForSet().add(cellUsersKey, userId); + redisTemplate.opsForZSet().add(cellExpiryKey, userId, (double) expireAt); + + // 만료되지 않은 위치 + String userId2 = "userId2"; + long expireAt2 = now + 3600 * 1000L; + redisTemplate.opsForSet().add(cellUsersKey, userId2); + redisTemplate.opsForZSet().add(cellExpiryKey, userId2, (double) expireAt2); + + // when + locationWriter.cleanupExpiredUserLocations(now); + + // then + assertThat(redisTemplate.opsForSet().isMember(cellUsersKey, userId)).isFalse(); + assertThat(redisTemplate.opsForZSet().score(cellExpiryKey, userId)).isNull(); + } +} \ No newline at end of file