From 1c2f1b58593ed8cec3c0dc345b1d478d8505e0e4 Mon Sep 17 00:00:00 2001 From: codrin2 Date: Mon, 14 Oct 2024 18:28:43 +0900 Subject: [PATCH 001/203] =?UTF-8?q?[feat]=20MongoDB=20=EC=84=A4=EC=A0=95?= =?UTF-8?q?=20=EB=B0=8F=20Global=20=EC=84=A4=EC=A0=95=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle.kts | 3 + .../pingping/global/common/CommonResponse.kt | 15 +++++ .../com/pingping/global/config/AppConfig.kt | 8 +++ .../pingping/global/config/P6SpyFormatter.kt | 42 ++++++++++++ .../com/pingping/global/config/WebConfig.kt | 15 +++++ .../com/pingping/global/entity/BaseEntity.kt | 14 ++++ .../pingping/global/entity/BaseTimeEntity.kt | 21 ++++++ .../global/exception/CustomException.kt | 5 ++ .../global/exception/ExceptionContent.kt | 10 +++ .../exception/GlobalExceptionHandler.kt | 67 +++++++++++++++++++ 10 files changed, 200 insertions(+) create mode 100644 src/main/kotlin/com/pingping/global/common/CommonResponse.kt create mode 100644 src/main/kotlin/com/pingping/global/config/AppConfig.kt create mode 100644 src/main/kotlin/com/pingping/global/config/P6SpyFormatter.kt create mode 100644 src/main/kotlin/com/pingping/global/config/WebConfig.kt create mode 100644 src/main/kotlin/com/pingping/global/entity/BaseEntity.kt create mode 100644 src/main/kotlin/com/pingping/global/entity/BaseTimeEntity.kt create mode 100644 src/main/kotlin/com/pingping/global/exception/CustomException.kt create mode 100644 src/main/kotlin/com/pingping/global/exception/ExceptionContent.kt create mode 100644 src/main/kotlin/com/pingping/global/exception/GlobalExceptionHandler.kt diff --git a/build.gradle.kts b/build.gradle.kts index 1de7683..fc6c1fd 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -55,6 +55,9 @@ dependencies { // MySQL runtimeOnly("com.mysql:mysql-connector-j") + // MongoDB + implementation("org.springframework.boot:spring-boot-starter-data-mongodb") + // Testing testImplementation("org.springframework.boot:spring-boot-starter-test") testImplementation("org.jetbrains.kotlin:kotlin-test-junit5") diff --git a/src/main/kotlin/com/pingping/global/common/CommonResponse.kt b/src/main/kotlin/com/pingping/global/common/CommonResponse.kt new file mode 100644 index 0000000..9bd8187 --- /dev/null +++ b/src/main/kotlin/com/pingping/global/common/CommonResponse.kt @@ -0,0 +1,15 @@ +package com.pingping.global.common + +import org.springframework.http.HttpStatus + +data class CommonResponse( + val code: Int, + val message: String, + val data: D? = null +) { + companion object { + fun of(status: HttpStatus, message: String, data: D? = null): CommonResponse { + return CommonResponse(status.value(), message, data) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/pingping/global/config/AppConfig.kt b/src/main/kotlin/com/pingping/global/config/AppConfig.kt new file mode 100644 index 0000000..768766c --- /dev/null +++ b/src/main/kotlin/com/pingping/global/config/AppConfig.kt @@ -0,0 +1,8 @@ +package com.pingping.global.config + +import org.springframework.context.annotation.Configuration +import org.springframework.data.jpa.repository.config.EnableJpaAuditing + +@Configuration +@EnableJpaAuditing +class AppConfig \ No newline at end of file diff --git a/src/main/kotlin/com/pingping/global/config/P6SpyFormatter.kt b/src/main/kotlin/com/pingping/global/config/P6SpyFormatter.kt new file mode 100644 index 0000000..04de0b0 --- /dev/null +++ b/src/main/kotlin/com/pingping/global/config/P6SpyFormatter.kt @@ -0,0 +1,42 @@ +package com.pingping.global.config + +import com.p6spy.engine.logging.Category +import com.p6spy.engine.spy.P6SpyOptions +import com.p6spy.engine.spy.appender.MessageFormattingStrategy +import org.hibernate.engine.jdbc.internal.FormatStyle +import org.springframework.context.annotation.Configuration +import jakarta.annotation.PostConstruct +import java.util.Locale + +@Configuration +class P6SpyFormatter : MessageFormattingStrategy { + + @PostConstruct + fun setLogMessageFormat() { + P6SpyOptions.getActiveInstance().logMessageFormat = this::class.java.name + } + + override fun formatMessage( + connectionId: Int, + now: String, + elapsed: Long, + category: String, + prepared: String?, + sql: String?, + url: String? + ): String { + val formattedSql = formatSql(category, sql) + return "[$category] | $elapsed ms | $formattedSql" + } + + private fun formatSql(category: String, sql: String?): String? { + return sql?.takeIf { it.isNotBlank() && category == Category.STATEMENT.name }?.let { + val trimmedSQL = it.lowercase(Locale.getDefault()).trim() + when { + trimmedSQL.startsWith("create") || trimmedSQL.startsWith("alter") || trimmedSQL.startsWith("comment") -> + FormatStyle.DDL.formatter.format(it) + else -> FormatStyle.BASIC.formatter.format(it) + } + } ?: sql + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/pingping/global/config/WebConfig.kt b/src/main/kotlin/com/pingping/global/config/WebConfig.kt new file mode 100644 index 0000000..d8a74c0 --- /dev/null +++ b/src/main/kotlin/com/pingping/global/config/WebConfig.kt @@ -0,0 +1,15 @@ +package com.pingping.global.config + +import org.springframework.context.annotation.Configuration +import org.springframework.web.servlet.config.annotation.CorsRegistry +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer + +@Configuration +class WebConfig : WebMvcConfigurer { + + override fun addCorsMappings(registry: CorsRegistry) { + registry.addMapping("/**") + .allowedOrigins("*") + .allowedMethods("GET", "POST", "PUT", "PATCH", "DELETE") + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/pingping/global/entity/BaseEntity.kt b/src/main/kotlin/com/pingping/global/entity/BaseEntity.kt new file mode 100644 index 0000000..1b2d6b0 --- /dev/null +++ b/src/main/kotlin/com/pingping/global/entity/BaseEntity.kt @@ -0,0 +1,14 @@ +package com.pingping.global.entity + +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType +import jakarta.persistence.Id +import jakarta.persistence.MappedSuperclass + +@MappedSuperclass +open class BaseEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + var id: Long = 0L + protected set +} \ No newline at end of file diff --git a/src/main/kotlin/com/pingping/global/entity/BaseTimeEntity.kt b/src/main/kotlin/com/pingping/global/entity/BaseTimeEntity.kt new file mode 100644 index 0000000..f283614 --- /dev/null +++ b/src/main/kotlin/com/pingping/global/entity/BaseTimeEntity.kt @@ -0,0 +1,21 @@ +package com.pingping.global.entity + +import jakarta.persistence.EntityListeners +import jakarta.persistence.MappedSuperclass +import org.springframework.data.annotation.CreatedDate +import org.springframework.data.annotation.LastModifiedDate +import org.springframework.data.jpa.domain.support.AuditingEntityListener +import java.time.LocalDateTime + +@MappedSuperclass +@EntityListeners(AuditingEntityListener::class) +abstract class BaseTimeEntity: BaseEntity() { + + @CreatedDate + var createdAt: LocalDateTime? = null + protected set + + @LastModifiedDate + var updatedAt: LocalDateTime? = null + protected set +} \ No newline at end of file diff --git a/src/main/kotlin/com/pingping/global/exception/CustomException.kt b/src/main/kotlin/com/pingping/global/exception/CustomException.kt new file mode 100644 index 0000000..23132a6 --- /dev/null +++ b/src/main/kotlin/com/pingping/global/exception/CustomException.kt @@ -0,0 +1,5 @@ +package com.pingping.global.exception + +class CustomException( + val content: ExceptionContent +) : RuntimeException(content.message) \ No newline at end of file diff --git a/src/main/kotlin/com/pingping/global/exception/ExceptionContent.kt b/src/main/kotlin/com/pingping/global/exception/ExceptionContent.kt new file mode 100644 index 0000000..41260f0 --- /dev/null +++ b/src/main/kotlin/com/pingping/global/exception/ExceptionContent.kt @@ -0,0 +1,10 @@ +package com.pingping.global.exception + +import org.springframework.http.HttpStatus + +enum class ExceptionContent(val httpStatus: HttpStatus, val message: String) { + + //user + USER_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 사용자를 찾을 수 없습니다."), + +} \ No newline at end of file diff --git a/src/main/kotlin/com/pingping/global/exception/GlobalExceptionHandler.kt b/src/main/kotlin/com/pingping/global/exception/GlobalExceptionHandler.kt new file mode 100644 index 0000000..39b1c0e --- /dev/null +++ b/src/main/kotlin/com/pingping/global/exception/GlobalExceptionHandler.kt @@ -0,0 +1,67 @@ +package com.pingping.global.exception + +import com.pingping.global.common.CommonResponse +import org.springframework.dao.EmptyResultDataAccessException +import org.springframework.http.HttpStatus +import org.springframework.http.ResponseEntity +import org.springframework.http.converter.HttpMessageNotReadableException +import org.springframework.web.HttpRequestMethodNotSupportedException +import org.springframework.web.bind.annotation.ExceptionHandler +import org.springframework.web.bind.annotation.RestControllerAdvice +import java.util.NoSuchElementException +import io.github.oshai.kotlinlogging.KotlinLogging + +@RestControllerAdvice +class GlobalExceptionHandler { + + private val log = KotlinLogging.logger {} + + // 공통 에러 응답 생성 메서드 + private fun generateErrorResponse(status: HttpStatus, message: String?): ResponseEntity> { + val errorMessage = message ?: "Unexpected error occurred" + val errorResponse = CommonResponse.of(status, errorMessage) + return ResponseEntity(errorResponse, status) + } + + // 예외 발생시 로그 기록 및 응답 처리 + private fun logAndGenerateErrorResponse(e: Exception, status: HttpStatus, message: String? = null): ResponseEntity> { + log.error("${e.javaClass.simpleName} occurred: ${e.message}", e) + return generateErrorResponse(status, message ?: e.message) + } + + // 커스텀 Exception 처리 + @ExceptionHandler(CustomException::class) + fun handleCustomException(exception: CustomException): ResponseEntity> { + return logAndGenerateErrorResponse(exception, exception.content.httpStatus, exception.message) + } + + // 모든 Exception 처리 + @ExceptionHandler(Exception::class) + fun handleAllExceptions(e: Exception): ResponseEntity> { + return logAndGenerateErrorResponse(e, HttpStatus.INTERNAL_SERVER_ERROR, "Internal server error") + } + + // NoSuchElementException 처리 + @ExceptionHandler(NoSuchElementException::class) + fun handleNoSuchElementException(e: NoSuchElementException): ResponseEntity> { + return logAndGenerateErrorResponse(e, HttpStatus.NOT_FOUND, "Resource not found") + } + + // EmptyResultDataAccessException 처리 + @ExceptionHandler(EmptyResultDataAccessException::class) + fun handleEmptyResultDataAccessException(e: EmptyResultDataAccessException): ResponseEntity> { + return logAndGenerateErrorResponse(e, HttpStatus.NOT_FOUND, "Resource not found") + } + + // HttpMessageNotReadableException 처리 + @ExceptionHandler(HttpMessageNotReadableException::class) + fun handleJsonException(e: HttpMessageNotReadableException): ResponseEntity> { + return logAndGenerateErrorResponse(e, HttpStatus.BAD_REQUEST, "Invalid JSON format") + } + + // HttpRequestMethodNotSupportedException 처리 + @ExceptionHandler(HttpRequestMethodNotSupportedException::class) + fun handleRequestMethodException(e: HttpRequestMethodNotSupportedException): ResponseEntity> { + return logAndGenerateErrorResponse(e, HttpStatus.METHOD_NOT_ALLOWED, "API does not support this request method. Please check the endpoint.") + } +} From 99576aa1641e06c4ebd94a8f13eccb5a5b97ca95 Mon Sep 17 00:00:00 2001 From: codrin2 Date: Tue, 15 Oct 2024 17:45:47 +0900 Subject: [PATCH 002/203] =?UTF-8?q?feat:=20redis,=20jwt=20=EC=9D=98?= =?UTF-8?q?=EC=A1=B4=EC=84=B1=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle.kts | 13 +++++++-- docker-compose.yaml | 28 ++++++------------- settings.gradle.kts | 2 +- .../com/pingping/global/entity/BaseEntity.kt | 2 +- 4 files changed, 20 insertions(+), 25 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index fc6c1fd..c91ee78 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -29,15 +29,14 @@ dependencies { implementation("org.springframework.boot:spring-boot-starter-data-jpa") implementation("org.springframework.boot:spring-boot-starter-web") implementation("org.springframework.boot:spring-boot-starter-actuator") + implementation("org.springframework.boot:spring-boot-starter-oauth2-client") + implementation("org.springframework.boot:spring-boot-starter-security") implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.6.0") implementation("com.github.gavlyukovskiy:p6spy-spring-boot-starter:1.9.0") // logger implementation("io.github.oshai:kotlin-logging-jvm:7.0.0") - // Spring Kafka - implementation("org.springframework.kafka:spring-kafka") - // Spring Cloud implementation("org.springframework.cloud:spring-cloud-starter-openfeign") @@ -58,6 +57,14 @@ dependencies { // MongoDB implementation("org.springframework.boot:spring-boot-starter-data-mongodb") + // Redis + implementation("org.springframework.boot:spring-boot-starter-data-redis") + + // JWT + implementation("io.jsonwebtoken:jjwt-api:0.12.5") + implementation("io.jsonwebtoken:jjwt-impl:0.12.5") + implementation("io.jsonwebtoken:jjwt-jackson:0.12.5") + // Testing testImplementation("org.springframework.boot:spring-boot-starter-test") testImplementation("org.jetbrains.kotlin:kotlin-test-junit5") diff --git a/docker-compose.yaml b/docker-compose.yaml index 32cfa3c..c657426 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -15,26 +15,6 @@ services: volumes: - ./mysql/init:/docker-entrypoint-initdb.d - zookeeper: - image: wurstmeister/zookeeper:3.4.6 - ports: - - "2181:2181" - - kafka: - image: wurstmeister/kafka:latest - ports: - - "9092:9092" - expose: - - "9093" - environment: - KAFKA_LISTENERS: INSIDE://0.0.0.0:9093,OUTSIDE://0.0.0.0:9092 - KAFKA_ADVERTISED_LISTENERS: INSIDE://kafka:9093,OUTSIDE://210.109.53.237:9092 - KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: INSIDE:PLAINTEXT,OUTSIDE:PLAINTEXT - KAFKA_INTER_BROKER_LISTENER_NAME: INSIDE - KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181 - volumes: - - /var/run/docker.sock:/var/run/docker.sock - mongo: image: mongo:latest container_name: mappin-mongo @@ -46,5 +26,13 @@ services: volumes: - mongo-data:/data/db + redis: + image: redis:latest + ports: + - "6379:6379" + volumes: + - redis-data:/data + volumes: mongo-data: + redis-data: \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 31269b3..2ab997b 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1 +1 @@ -rootProject.name = "ping-service" +rootProject.name = "pingping-BE" diff --git a/src/main/kotlin/com/pingping/global/entity/BaseEntity.kt b/src/main/kotlin/com/pingping/global/entity/BaseEntity.kt index 1b2d6b0..e6e817f 100644 --- a/src/main/kotlin/com/pingping/global/entity/BaseEntity.kt +++ b/src/main/kotlin/com/pingping/global/entity/BaseEntity.kt @@ -9,6 +9,6 @@ import jakarta.persistence.MappedSuperclass open class BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) - var id: Long = 0L + var id: Long = 0 protected set } \ No newline at end of file From ebab4a5892417842ba68ca85117f7c2e79e8c5a9 Mon Sep 17 00:00:00 2001 From: codrin2 Date: Tue, 15 Oct 2024 17:47:46 +0900 Subject: [PATCH 003/203] =?UTF-8?q?[feat]=20=EB=A6=AC=EB=93=9C=EB=AF=B8=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 313 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 313 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..b769e49 --- /dev/null +++ b/README.md @@ -0,0 +1,313 @@ +# 👍 공통 사항 + +- 단위 테스트 작성(service 메소드 별로) : Kotest 사용 +- 다른 사람이 알아보기 쉽도록 주석처리해야 합니다. (controller, service 메서드마다) + - javadoc 형식 https://jake-seo-dev.tistory.com/59 +- issue 생성 및 PR을 통해 본인이 구현한 부분에 대한 기록을 남겨야 합니다. +- 테스트 및 원할한 서버 운영을 위한 로그를 작성해야 합니다.(에러나 운영에 필요한 로그. 검색시 검색어와 같은 로그) +- 예외처리는 항상 잘 만들어두기 (code, message, data) +- 개발 기간 : 9/30 ~ 11/24 +- 스프린트 (3일간격) 진행 (해올 것을 정해서 해오기) + - 수요일, 토요일 + +
+ +# 🛠️ 기술 스택 + +- #### Language, Framework, Library + ![Kotlin](https://img.shields.io/badge/Kotlin-7F52FF?style=flat-square&logo=Kotlin&logoColor=FFFFFF) + ![Springboot](https://img.shields.io/badge/Springboot-6DB33F?style=flat-square&logo=springboot&logoColor=white) + ![Gradle](https://img.shields.io/badge/Gradle-02303A.svg?style=flat-square&logo=Gradle&logoColor=white) + ![Spring Data JPA](https://img.shields.io/badge/Spring%20Data%20JPA-6DB33F?style=flat-square&logo=spring&logoColor=white) + ![Spring Security](https://img.shields.io/badge/Spring%20Security-6DB33F?style=flat-square&logo=Spring%20Security&logoColor=white) + ![QueryDSL](https://img.shields.io/badge/QueryDSL-4096EE?style=flat-square&logo=QueryDSL&logoColor=white) + - Kotlin은 간결하고 직관적인 문법으로 코드 생산성을 높이며, Null 안정성을 제공하여 오류를 사전에 방지 + - Spring Security는 강력한 인증 및 권한 부여 기능을 제공하며, 다양한 보안 요구사항을 쉽게 적용 가능 + - QueryDSL은 타입 안전한 쿼리 작성이 가능해, SQL 쿼리를 컴파일 시점에 검증하고, 코드 가독성을 높이는 동시에 유지보수성을 향상 + +- #### Test + ![Kotest](https://img.shields.io/badge/Kotest-5D3FD3?style=flat-square&logo=Kotest&logoColor=white) + ![MockK](https://img.shields.io/badge/MockK-FFCA28?style=flat-square&logo=MockK&logoColor=white) + - Kotest는 직관적이고 가독성 높은 테스트 DSL을 제공하여, 테스트 코드를 읽기 쉽게 작성할 수 있으며 다양한 테스트 스타일을 지원. + - MockK는 코틀린에 특화된 모킹 라이브러리로, 코루틴과 같은 코틀린 고유 기능을 쉽게 모킹할 수 있어 비동기 코드 테스트에 강점이 있음. + + +- #### CICD + ![Jenkins](https://img.shields.io/badge/Jenkins-D24939?style=flat-square&logo=Jenkins&logoColor=white) + ![Jacoco](https://img.shields.io/badge/Jacoco-CC6699?style=flat-square&logo=Jacoco&logoColor=white) + ![SonarQube](https://img.shields.io/badge/SonarQube-4E9BCD?style=flat-square&logo=SonarQube&logoColor=white) + ![Trivy](https://img.shields.io/badge/Trivy-0091E2?style=flat-square&logo=Trivy&logoColor=white) + ![ArgoCD](https://img.shields.io/badge/ArgoCD-EF7B4D?style=flat-square&logo=ArgoCD&logoColor=white) + ![Docker](https://img.shields.io/badge/docker-%230db7ed.svg?style=flat-square&logo=docker&logoColor=white) + - Jenkins를 사용한 CI/CD 파이프라인은 자동화된 테스트, 빌드, 배포를 통해 개발 프로세스를 효율적으로 관리 + - Jacoco, SonarQube, TrivyScan은 각각 코드 커버리지, 코드 품질, 보안 취약점을 점검하여 안정적인 코드 배포를 지원 + - ArgoCD는 GitOps 방식으로 애플리케이션을 Kubernetes 환경에 쉽게 배포 및 관리할 수 있어, 전체적인 개발과 운영의 일관성을 보장 + +- #### Infra + ![Kubernetes](https://img.shields.io/badge/Kubernetes-326CE5?style=flat-square&logo=Kubernetes&logoColor=white) + ![Grafana](https://img.shields.io/badge/Grafana-F46800?style=flat-square&logo=Grafana&logoColor=white) + ![Prometheus](https://img.shields.io/badge/Prometheus-E6522C?style=flat-square&logo=Prometheus&logoColor=white) + ![Elasticsearch](https://img.shields.io/badge/Elasticsearch-005571?style=flat-square&logo=Elasticsearch&logoColor=white) + ![Logstash](https://img.shields.io/badge/Logstash-005571?style=flat-square&logo=Logstash&logoColor=white) + ![Kibana](https://img.shields.io/badge/Kibana-005571?style=flat-square&logo=Kibana&logoColor=white) + ![Filebeat](https://img.shields.io/badge/Filebeat-005571?style=flat-square&logo=Filebeat&logoColor=white) + ![Vault](https://img.shields.io/badge/Vault-000000?style=flat-square&logo=Vault&logoColor=white) + ![Kafka](https://img.shields.io/badge/Apache%20Kafka-231F20?style=flat-square&logo=Apache%20Kafka&logoColor=white) + - Kubernetes는 컨테이너화된 애플리케이션의 배포와 확장을 자동화하여, 대규모 인프라 관리가 용이 + - Grafana, Prometheus는 모니터링과 알림 시스템을 구축해 시스템 성능 및 상태를 실시간으로 추적하고 대응 + - Elasticsearch, Logstash, Kibana, Filebeat(ELK 스택)는 로그 수집, 분석, 시각화를 통해 애플리케이션 상태 및 문제를 쉽게 파악하고 대응 + - Kafka는 대용량의 데이터를 실시간으로 처리하고, 분산 환경에서 높은 확장성과 안정성을 제공하는 메시징 플랫폼 + + +- #### Database + ![Elasticsearch](https://img.shields.io/badge/Elasticsearch-005571?style=flat-square&logo=Elasticsearch&logoColor=white) + ![MySQL](https://img.shields.io/badge/mysql-%2300f.svg?style=flat-square&logo=mysql&logoColor=white) + ![Redis](https://img.shields.io/badge/redis-%23DD0031.svg?style=flat-square&logo=redis&logoColor=white) + - Elasticsearch는 대규모 데이터에서 빠른 검색과 분석을 지원하며, 실시간 로그 분석 및 검색에 탁월한 성능을 발휘 + - Redis는 인메모리 데이터 구조 저장소로, 매우 빠른 읽기/쓰기 성능을 제공하여 캐싱, 세션 관리, 실시간 데이터 처리 가능 + +- #### API 테스트, 명세서 + ![Notion](https://img.shields.io/badge/Notion-%23000000.svg?style=flat-square&logo=notion&logoColor=white) + ![Postman](https://img.shields.io/badge/Postman-FF6C37?style=flat-square&logo=postman&logoColor=white) + ![Spring REST Docs](https://img.shields.io/badge/Spring%20REST%20Docs-6DB33F?style=flat-square&logo=spring&logoColor=white) + ![Swagger](https://img.shields.io/badge/Swagger-85EA2D?style=flat-square&logo=swagger&logoColor=white) + - RestDocs를 통해 생성된 문서를 Swagger UI로 시각화하여, 개발자와 비개발자 모두가 실시간으로 API를 테스트 가능 + - 테스트 코드 작성과 함께 API 문서가 자동으로 생성되어, 실제 코드와 문서의 동기화 문제가 발생하지 않음 + - 테스트 시에 문서를 검증할 수 있어 신뢰성을 높임 + +- #### 🙏 협업 툴 + ![Slack](https://img.shields.io/badge/Slack-4A154B.svg?style=flat-square&logo=slack&logoColor=white) + ![Notion](https://img.shields.io/badge/Notion-000000.svg?style=flat-square&logo=notion&logoColor=white) + +
+ +# 🤙 개발규칙 + +### ⭐ Code Convention + +--- + +
+Naming +
+ +- 패키지 : 언더스코어(`_`)나 대문자를 섞지 않고 소문자를 사용하여 작성합니다. +- 클래스 : 클래스 이름은 명사나 명사절로 지으며, 대문자 카멜표기법(Upper camel case)을 사용합니다. +- 메서드 : 메서드 이름은 동사/전치사로 시작하며, 소문자 카멜표기법(Lower camel case)를 사용합니다. 의도가 전달되도록 최대한 간결하게 표현합니다. +- 변수 : 소문자 카멜표기법(Lower camel case)를 사용합니다. +- ENUM, 상수 : 상태를 가지지 않는 자료형이면서 `static final`로 선언되어 있는 필드일 때를 상수로 간주하며, 대문자와 언더스코어(UPPER_SNAKE_CASE)로 구성합니다. +- DB 테이블: 소문자와 언더스코어로(lower_snake_case) 구성합니다. +- 컬렉션(Collection): **복수형**을 사용하거나 **컬렉션을 명시합니다**. (Ex. userList, users, userMap) +- LocalDateTime: 접미사에 *Time**를 붙입니다. + +
+
+
+Comment +
+ +### 1. 한줄 주석은 // 를 사용한다. + +```java +// 하이~ + +``` + +### 2. 한줄 주석 외에 설명을 위한 주석은 JavaDoc을 사용한다. + +```java +/** + * 두 정수를 더합니다. + * + *

이 메소드는 두 개의 정수를 입력받아 그 합계를 반환합니다.

+ * + * @param a 첫 번째 정수 + * @param b 두 번째 정수 + * @return 두 정수의 합 + * @throws ArithmeticException 만약 계산 중 오류가 발생하면 + */ + +``` + +
+
+
+Import +
+ +### 1. 소스파일당 1개의 탑레벨 클래스를 담기 + +> 탑레벨 클래스(Top level class)는 소스 파일에 1개만 존재해야 한다. ( 탑레벨 클래스 선언의 컴파일타임 에러 체크에 대해서는 Java Language Specification 7.6 참조 ) +> + +### 2. static import에만 와일드 카드 허용 + +> 클래스를 import할때는 와일드카드(*) 없이 모든 클래스명을 다 쓴다. static import에서는 와일드카드를 허용한다. +> + +### 3. 애너테이션 선언 후 새줄 사용 + +> 클래스, 인터페이스, 메서드, 생성자에 붙는 애너테이션은 선언 후 새줄을 사용한다. 이 위치에서도 파라미터가 없는 애너테이션 1개는 같은 줄에 선언할 수 있다. +> + +### 4. 배열에서 대괄호는 타입 뒤에 선언 + +> 배열 선언에 오는 대괄호([])는 타입의 바로 뒤에 붙인다. 변수명 뒤에 붙이지 않는다. +> + +### 5. `long`형 값의 마지막에 `L`붙이기 + +> long형의 숫자에는 마지막에 대문자 'L’을 붙인다. 소문자 'l’보다 숫자 '1’과의 차이가 커서 가독성이 높아진다. +> + +
+
+
+URL +
+ +### URL + +URL은 RESTful API 설계 가이드에 따라 작성합니다. + +- HTTP Method로 구분할 수 있는 get, put 등의 행위는 url에 표현하지 않습니다. +- 마지막에 `/` 를 포함하지 않습니다. +- `_` 대신 ``를 사용합니다. +- 소문자를 사용합니다. +- 확장자는 포함하지 않습니다. + +
+
+ +
+ +### ☀️ Commit Convention + +--- + +
+Rules +
+ +### 1. Git Flow + +작업 시작 시 선행되어야 할 작업은 다음과 같습니다. + +> issue를 생성합니다.feature branch를 생성합니다.add → commit → push → pull request 를 진행합니다.pull request를 develop branch로 merge 합니다.이전에 merge된 작업이 있을 경우 다른 branch에서 진행하던 작업에 merge된 작업을 pull 받아옵니다.종료된 issue와 pull request의 label을 관리합니다. +> + +### 2. IntelliJ + +IntelliJ로 작업을 진행하는 경우, 작업 시작 시 선행되어야 할 작업은 다음과 같습니다. + +> 깃허브 프로젝트 저장소에서 issue를 생성합니다.생성한 issue 번호에 맞는 feature branch를 생성함과 동시에 feature branch로 checkout 합니다.feature branch에서 issue 단위 작업을 진행합니다.작업 완료 후, add → commit을 진행합니다.remote develop branch의 변경 사항을 확인하기 위해 pull 받은 이후 push를 진행합니다.만약 코드 충돌이 발생하였다면, IntelliJ에서 코드 충돌을 해결하고 add → commit을 진행합니다.push → pull request (feature branch → develop branch) 를 진행합니다.pull request가 작성되면 작성자 이외의 다른 팀원이 code review를 진행합니다.최소 한 명 이상의 팀원에게 code review와 approve를 받은 경우 pull request 생성자가 merge를 진행합니다.종료된 issue와 pull request의 label과 milestone을 관리합니다. +> + +### 3. Etc + +준수해야 할 규칙은 다음과 같습니다. + +> develop branch에서의 작업은 원칙적으로 금지합니다. 단, README 작성은 develop branch에서 수행합니다.commit, push, merge, pull request 등 모든 작업은 오류 없이 정상적으로 실행되는 지 확인 후 수행합니다. +> + +
+
+ +
+Branch +
+ +### 1. Branch + +branch는 작업 단위 & 기능 단위로 생성된 issue를 기반으로 합니다. + +### 2. Branch Naming Rule + +branch를 생성하기 전 issue를 먼저 작성합니다. issue 작성 후 생성되는 번호와 domain 명을 조합하여 branch의 이름을 결정합니다. `/-` 의 양식을 준수합니다. + +### 3. Prefix + +- `main` : 개발이 완료된 산출물이 저장될 공간입니다. +- `develop`: feature branch에서 구현된 기능들이 merge될 default branch 입니다. +- `feature`: 기능을 개발하는 branch 입니다. 이슈 별 & 작업 별로 branch를 생성 후 기능을 개발하며 naming은 소문자를 사용합니다. + +### 4. Domain + +- `user`, `map`, (`error`, `config`) + +### 5. Etc + +- `feature/7-user`, `feature/5-config` + +
+
+ +
+Issue +
+ +### 1. Issue + +작업 시작 전 issue 생성이 선행되어야 합니다. issue 는 작업 단위 & 기능 단위로 생성하며 생성 후 표시되는 issue number 를 참조하여 branch 이름과 commit message를 작성합니다. + +issue 제목에는 기능의 대표적인 설명을 적고 내용에는 세부적인 내용 및 작업 진행 상황을 작성합니다. + +issue 생성 시 github 오른편의 assignee, label을 적용합니다. assignee는 해당 issue 담당자, label은 작업 내용을 추가합니다. + +### 2. Issue Naming Rule + +`[] ` 의 양식을 준수하되, prefix는 commit message convention을 따릅니다. + +### 3. Etc + + + +--- + +
+
+ +
+Commit +
+ +### 1. Commit Message Convention + +`[] # ` 의 양식을 준수합니다. + +- **feat** : 새로운 기능 구현 `[feat] #11 구글 로그인 API 기능 구현` +- **fix** : 코드 오류 수정 `[fix] #10 회원가입 비즈니스 로직 오류 수정` +- **del** : 쓸모없는 코드 삭제 `[del] #12 불필요한 import 제거` +- **docs** : README나 wiki 등의 문서 개정 `[docs] #14 리드미 수정` +- **refactor** : 내부 로직은 변경 하지 않고 기존의 코드를 개선하는 리팩터링 `[refactor] #15 코드 로직 개선` +- **chore** : 의존성 추가, yml 추가와 수정, 패키지 구조 변경, 파일 이동 `[chore] #21 yml 수정`, `[chore] #22 lombok 의존성 추가` +- **test**: 테스트 코드 작성, 수정 `[test] #20 로그인 API 테스트 코드 작성` +- **style** : 코드에 관련 없는 주석 달기, 줄바꿈 +- **rename** : 파일 및 폴더명 수정 + +
+
+ +
+Pull Request +
+ +### 1. Pull Request + +develop & main branch로 merge할 때에는 pull request가 필요합니다. pull request의 내용에는 변경된 사항에 대한 설명을 명시합니다. + +### 2. Pull Request Naming Rule + +`[] ` 의 양식을 준수하되, prefix는 commit message convention을 따릅니다. + +### 3. Etc + +[feat] 약속 잡기 API 구현 +
[chore] spring data JPA 의존성 추가 + +
+
From 593675543bef1063837a85c3bba8c4b3da436316 Mon Sep 17 00:00:00 2001 From: Hee Sang <118061713+codrin2@users.noreply.github.com> Date: Tue, 15 Oct 2024 17:49:45 +0900 Subject: [PATCH 004/203] Update issue templates --- .github/ISSUE_TEMPLATE/bug_report.md | 22 ++++++++++++++++++++++ .github/ISSUE_TEMPLATE/feature_request.md | 20 ++++++++++++++++++++ 2 files changed, 42 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..a40b6a9 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,22 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + +## 어떤 버그인가요? + +> 어떤 버그인지 간결하게 설명해주세요 + +## 어떤 상황에서 발생한 버그인가요? + +> (가능하면) Given-When-Then 형식으로 서술해주세요 + +## 예상 결과 + +> 예상했던 정상적인 결과가 어떤 것이었는지 설명해주세요 + +## 참고할만한 자료(선택) diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..eaa144a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: '' +assignees: '' + +--- + +## 어떤 기능인가요? + +> 추가하려는 기능에 대해 간결하게 설명해주세요 + +## 작업 상세 내용 + +- [ ] TODO +- [ ] TODO +- [ ] TODO + +## 참고할만한 자료(선택) From b02c8c24b40faf3824ae9ef8a7d72f37a8a9264c Mon Sep 17 00:00:00 2001 From: Hee Sang <118061713+codrin2@users.noreply.github.com> Date: Tue, 15 Oct 2024 17:50:14 +0900 Subject: [PATCH 005/203] Create PULL_REQUEST_TEMPLATE.md --- .github/PULL_REQUEST_TEMPLATE.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 .github/PULL_REQUEST_TEMPLATE.md diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..e6e1ab4 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,13 @@ +## #️⃣연관된 이슈 + +> ex) #이슈번호, #이슈번호 + +## 📝작업 내용 + +> 이번 PR에서 작업한 내용을 간략히 설명해주세요(이미지 첨부 가능) + +### 스크린샷 (선택) + +## 💬리뷰 요구사항(선택) + +> 리뷰어가 특별히 봐주었으면 하는 부분이 있다면 작성해주세요 From 2e772df323d8708d1d95ea0b0d0e8431d5ca2659 Mon Sep 17 00:00:00 2001 From: sominyun Date: Thu, 17 Oct 2024 03:44:54 +0900 Subject: [PATCH 006/203] =?UTF-8?q?feat:=20ddd=20=EB=A9=80=ED=8B=B0?= =?UTF-8?q?=EB=AA=A8=EB=93=88=20=EA=B5=AC=EC=A1=B0=EB=A1=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Ping-Api/build.gradle.kts | 14 ++ .../kotlin/com/ping/api/PingApplication.kt | 23 +++ .../ping/api/nonmember/NonMemberController.kt | 8 + Ping-Application/build.gradle.kts | 15 ++ .../application/nonmember/NonMemberService.kt | 46 ++++++ .../dto/request/NonMemberCreateRequest.kt | 7 + Ping-Common/build.gradle.kts | 23 +++ .../com/ping/common}/config/AppConfig.kt | 2 +- .../com/ping/common}/config/P6SpyFormatter.kt | 2 +- .../com/ping/common}/config/WebConfig.kt | 2 +- .../com/ping/common}/entity/BaseEntity.kt | 2 +- .../com/ping/common}/entity/BaseTimeEntity.kt | 2 +- .../ping/common/exception/CommonResponse.kt | 17 ++ .../ping/common}/exception/CustomException.kt | 2 +- .../ping/common/exception/ExceptionContent.kt | 24 +++ .../exception/GlobalExceptionHandler.kt | 37 ++++- Ping-Domain/build.gradle.kts | 11 ++ .../domain/nonmember/aggregate/NonMember.kt | 18 +++ .../domain/nonmember/aggregate/ShareUrl.kt | 8 + .../repository/NonMemberRepository.kt | 9 ++ .../repository/ShareUrlRepository.kt | 7 + Ping-Infra/build.gradle.kts | 30 ++++ .../infra/nonmember/domain/jpa/JpaConfig.kt | 10 ++ .../domain/jpa/entity/NonMemberEntity.kt | 27 ++++ .../domain/jpa/entity/ShareUrlEntity.kt | 28 ++++ .../jpa/repository/NonMemberJpaRepository.kt | 8 + .../jpa/repository/ShareUrlJpaRepository.kt | 6 + .../domain/mapper/NonMemberMapper.kt | 26 ++++ .../nonmember/domain/mapper/ShareUrlMapper.kt | 24 +++ .../repositoryImpl/NonMemberRepositoryImpl.kt | 23 +++ .../repositoryImpl/ShareUrlRepositoryImpl.kt | 19 +++ build.gradle.kts | 145 ++++++++++-------- settings.gradle.kts | 8 + .../com/pingping/PingServiceApplication.kt | 11 -- .../pingping/global/common/CommonResponse.kt | 15 -- .../global/exception/ExceptionContent.kt | 10 -- .../pingping/PingServiceApplicationTests.kt | 13 -- 37 files changed, 555 insertions(+), 127 deletions(-) create mode 100644 Ping-Api/build.gradle.kts create mode 100644 Ping-Api/src/main/kotlin/com/ping/api/PingApplication.kt create mode 100644 Ping-Api/src/main/kotlin/com/ping/api/nonmember/NonMemberController.kt create mode 100644 Ping-Application/build.gradle.kts create mode 100644 Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberService.kt create mode 100644 Ping-Application/src/main/kotlin/com/ping/application/nonmember/dto/request/NonMemberCreateRequest.kt create mode 100644 Ping-Common/build.gradle.kts rename {src/main/kotlin/com/pingping/global => Ping-Common/src/main/kotlin/com/ping/common}/config/AppConfig.kt (83%) rename {src/main/kotlin/com/pingping/global => Ping-Common/src/main/kotlin/com/ping/common}/config/P6SpyFormatter.kt (97%) rename {src/main/kotlin/com/pingping/global => Ping-Common/src/main/kotlin/com/ping/common}/config/WebConfig.kt (93%) rename {src/main/kotlin/com/pingping/global => Ping-Common/src/main/kotlin/com/ping/common}/entity/BaseEntity.kt (89%) rename {src/main/kotlin/com/pingping/global => Ping-Common/src/main/kotlin/com/ping/common}/entity/BaseTimeEntity.kt (94%) create mode 100644 Ping-Common/src/main/kotlin/com/ping/common/exception/CommonResponse.kt rename {src/main/kotlin/com/pingping/global => Ping-Common/src/main/kotlin/com/ping/common}/exception/CustomException.kt (72%) create mode 100644 Ping-Common/src/main/kotlin/com/ping/common/exception/ExceptionContent.kt rename {src/main/kotlin/com/pingping/global => Ping-Common/src/main/kotlin/com/ping/common}/exception/GlobalExceptionHandler.kt (67%) create mode 100644 Ping-Domain/build.gradle.kts create mode 100644 Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/aggregate/NonMember.kt create mode 100644 Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/aggregate/ShareUrl.kt create mode 100644 Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/repository/NonMemberRepository.kt create mode 100644 Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/repository/ShareUrlRepository.kt create mode 100644 Ping-Infra/build.gradle.kts create mode 100644 Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/jpa/JpaConfig.kt create mode 100644 Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/jpa/entity/NonMemberEntity.kt create mode 100644 Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/jpa/entity/ShareUrlEntity.kt create mode 100644 Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/jpa/repository/NonMemberJpaRepository.kt create mode 100644 Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/jpa/repository/ShareUrlJpaRepository.kt create mode 100644 Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/mapper/NonMemberMapper.kt create mode 100644 Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/mapper/ShareUrlMapper.kt create mode 100644 Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/repositoryImpl/NonMemberRepositoryImpl.kt create mode 100644 Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/repositoryImpl/ShareUrlRepositoryImpl.kt delete mode 100644 src/main/kotlin/com/pingping/PingServiceApplication.kt delete mode 100644 src/main/kotlin/com/pingping/global/common/CommonResponse.kt delete mode 100644 src/main/kotlin/com/pingping/global/exception/ExceptionContent.kt delete mode 100644 src/test/kotlin/com/pingping/PingServiceApplicationTests.kt diff --git a/Ping-Api/build.gradle.kts b/Ping-Api/build.gradle.kts new file mode 100644 index 0000000..c741365 --- /dev/null +++ b/Ping-Api/build.gradle.kts @@ -0,0 +1,14 @@ +dependencies { + implementation(project(":Ping-Application")) + implementation(project(":Ping-Common")) + implementation(project(":Ping-Domain")) + implementation(project(":Ping-Infra")) +} +tasks { + bootJar { + isEnabled = true + } + jar { + isEnabled = true + } +} \ No newline at end of file diff --git a/Ping-Api/src/main/kotlin/com/ping/api/PingApplication.kt b/Ping-Api/src/main/kotlin/com/ping/api/PingApplication.kt new file mode 100644 index 0000000..772b4fb --- /dev/null +++ b/Ping-Api/src/main/kotlin/com/ping/api/PingApplication.kt @@ -0,0 +1,23 @@ +package com.ping.api + +import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.boot.context.properties.ConfigurationPropertiesScan +import org.springframework.boot.runApplication +import org.springframework.scheduling.annotation.EnableScheduling + +@EnableScheduling +@ConfigurationPropertiesScan +@SpringBootApplication( + scanBasePackages = [ + "com.ping.api", + "com.ping.application", + "com.ping.common", + "com.ping.domain", + "com.ping.infra", + ] +) +class PingApplication + +fun main(args: Array) { + runApplication(*args) +} \ No newline at end of file diff --git a/Ping-Api/src/main/kotlin/com/ping/api/nonmember/NonMemberController.kt b/Ping-Api/src/main/kotlin/com/ping/api/nonmember/NonMemberController.kt new file mode 100644 index 0000000..605e112 --- /dev/null +++ b/Ping-Api/src/main/kotlin/com/ping/api/nonmember/NonMemberController.kt @@ -0,0 +1,8 @@ +package com.ping.api.nonmember + +import org.springframework.web.bind.annotation.RestController + +@RestController(value = "/nonmembers") +class NonMemberController { + +} \ No newline at end of file diff --git a/Ping-Application/build.gradle.kts b/Ping-Application/build.gradle.kts new file mode 100644 index 0000000..109e2e8 --- /dev/null +++ b/Ping-Application/build.gradle.kts @@ -0,0 +1,15 @@ +dependencies { + implementation(project(":Ping-Common")) + implementation(project(":Ping-Domain")) + + //jpa + implementation("org.springframework.boot:spring-boot-starter-data-jpa") +} +tasks { + bootJar { + isEnabled = true + } + jar { + isEnabled = true + } +} \ No newline at end of file diff --git a/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberService.kt b/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberService.kt new file mode 100644 index 0000000..81e22ef --- /dev/null +++ b/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberService.kt @@ -0,0 +1,46 @@ +package com.ping.application.nonmember + +import com.ping.application.nonmember.dto.request.NonMemberCreateRequest +import com.ping.common.exception.CustomException +import com.ping.common.exception.ExceptionContent +import com.ping.domain.nonmember.aggregate.NonMember +import com.ping.domain.nonmember.repository.NonMemberRepository +import com.ping.domain.nonmember.repository.ShareUrlRepository +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Service +@Transactional(readOnly = true) +class NonMemberService( + private val nonMemberRepository: NonMemberRepository, + private val shareUrlRepository: ShareUrlRepository, +) { + + @Transactional + fun createNonMember(request: NonMemberCreateRequest): Long { + // 비밀번호 형식 검사 (4자리 숫자) + validatePassword(request.password) + + val shareUrl = shareUrlRepository.findById(request.shareUrlId) + ?: throw CustomException(ExceptionContent.INVALID_SHARE_URL) + + // shareUrlId과 name으로 비회원 존재 여부 확인 + nonMemberRepository.findByShareUrlIdAndName(request.shareUrlId, request.name)?.let { + throw CustomException(ExceptionContent.NON_MEMBER_ALREADY_EXISTS) + } + + // NonMember 엔티티 생성 및 저장 + val nonMember = NonMember.of(request.name, request.password, shareUrl) + val savedNonMember = nonMemberRepository.save(nonMember) + + return savedNonMember.id + } + + // 비밀번호 유효성 검증 로직 + private fun validatePassword(password: String) { + if (!password.matches(Regex("\\d{4}"))) { + throw CustomException(ExceptionContent.INVALID_PASSWORD_FORMAT) + } + } + +} \ No newline at end of file diff --git a/Ping-Application/src/main/kotlin/com/ping/application/nonmember/dto/request/NonMemberCreateRequest.kt b/Ping-Application/src/main/kotlin/com/ping/application/nonmember/dto/request/NonMemberCreateRequest.kt new file mode 100644 index 0000000..690dbd2 --- /dev/null +++ b/Ping-Application/src/main/kotlin/com/ping/application/nonmember/dto/request/NonMemberCreateRequest.kt @@ -0,0 +1,7 @@ +package com.ping.application.nonmember.dto.request + +data class NonMemberCreateRequest( + val shareUrlId: Long, + val name: String, + val password: String +) diff --git a/Ping-Common/build.gradle.kts b/Ping-Common/build.gradle.kts new file mode 100644 index 0000000..8d57d88 --- /dev/null +++ b/Ping-Common/build.gradle.kts @@ -0,0 +1,23 @@ +dependencies { + // JWT + implementation("io.jsonwebtoken:jjwt-api:0.12.5") + implementation("io.jsonwebtoken:jjwt-impl:0.12.5") + implementation("io.jsonwebtoken:jjwt-jackson:0.12.5") + + //jpa + implementation("org.springframework.boot:spring-boot-starter-data-jpa") + + //p6spy + implementation("com.github.gavlyukovskiy:p6spy-spring-boot-starter:1.9.0") + + //kotlin log + implementation("io.github.oshai:kotlin-logging-jvm:7.0.0") +} +tasks { + bootJar { + isEnabled = false + } + jar { + isEnabled = true + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/pingping/global/config/AppConfig.kt b/Ping-Common/src/main/kotlin/com/ping/common/config/AppConfig.kt similarity index 83% rename from src/main/kotlin/com/pingping/global/config/AppConfig.kt rename to Ping-Common/src/main/kotlin/com/ping/common/config/AppConfig.kt index 768766c..d01ed94 100644 --- a/src/main/kotlin/com/pingping/global/config/AppConfig.kt +++ b/Ping-Common/src/main/kotlin/com/ping/common/config/AppConfig.kt @@ -1,4 +1,4 @@ -package com.pingping.global.config +package com.ping.common.config import org.springframework.context.annotation.Configuration import org.springframework.data.jpa.repository.config.EnableJpaAuditing diff --git a/src/main/kotlin/com/pingping/global/config/P6SpyFormatter.kt b/Ping-Common/src/main/kotlin/com/ping/common/config/P6SpyFormatter.kt similarity index 97% rename from src/main/kotlin/com/pingping/global/config/P6SpyFormatter.kt rename to Ping-Common/src/main/kotlin/com/ping/common/config/P6SpyFormatter.kt index 04de0b0..69be10b 100644 --- a/src/main/kotlin/com/pingping/global/config/P6SpyFormatter.kt +++ b/Ping-Common/src/main/kotlin/com/ping/common/config/P6SpyFormatter.kt @@ -1,4 +1,4 @@ -package com.pingping.global.config +package com.ping.common.config import com.p6spy.engine.logging.Category import com.p6spy.engine.spy.P6SpyOptions diff --git a/src/main/kotlin/com/pingping/global/config/WebConfig.kt b/Ping-Common/src/main/kotlin/com/ping/common/config/WebConfig.kt similarity index 93% rename from src/main/kotlin/com/pingping/global/config/WebConfig.kt rename to Ping-Common/src/main/kotlin/com/ping/common/config/WebConfig.kt index d8a74c0..3c9a5ba 100644 --- a/src/main/kotlin/com/pingping/global/config/WebConfig.kt +++ b/Ping-Common/src/main/kotlin/com/ping/common/config/WebConfig.kt @@ -1,4 +1,4 @@ -package com.pingping.global.config +package com.ping.common.config import org.springframework.context.annotation.Configuration import org.springframework.web.servlet.config.annotation.CorsRegistry diff --git a/src/main/kotlin/com/pingping/global/entity/BaseEntity.kt b/Ping-Common/src/main/kotlin/com/ping/common/entity/BaseEntity.kt similarity index 89% rename from src/main/kotlin/com/pingping/global/entity/BaseEntity.kt rename to Ping-Common/src/main/kotlin/com/ping/common/entity/BaseEntity.kt index e6e817f..d8e5236 100644 --- a/src/main/kotlin/com/pingping/global/entity/BaseEntity.kt +++ b/Ping-Common/src/main/kotlin/com/ping/common/entity/BaseEntity.kt @@ -1,4 +1,4 @@ -package com.pingping.global.entity +package com.ping.common.entity import jakarta.persistence.GeneratedValue import jakarta.persistence.GenerationType diff --git a/src/main/kotlin/com/pingping/global/entity/BaseTimeEntity.kt b/Ping-Common/src/main/kotlin/com/ping/common/entity/BaseTimeEntity.kt similarity index 94% rename from src/main/kotlin/com/pingping/global/entity/BaseTimeEntity.kt rename to Ping-Common/src/main/kotlin/com/ping/common/entity/BaseTimeEntity.kt index f283614..c9f733d 100644 --- a/src/main/kotlin/com/pingping/global/entity/BaseTimeEntity.kt +++ b/Ping-Common/src/main/kotlin/com/ping/common/entity/BaseTimeEntity.kt @@ -1,4 +1,4 @@ -package com.pingping.global.entity +package com.ping.common.entity import jakarta.persistence.EntityListeners import jakarta.persistence.MappedSuperclass diff --git a/Ping-Common/src/main/kotlin/com/ping/common/exception/CommonResponse.kt b/Ping-Common/src/main/kotlin/com/ping/common/exception/CommonResponse.kt new file mode 100644 index 0000000..065c9d8 --- /dev/null +++ b/Ping-Common/src/main/kotlin/com/ping/common/exception/CommonResponse.kt @@ -0,0 +1,17 @@ +package com.ping.common.exception + +import org.springframework.http.HttpStatus + +data class CommonResponse( + val code: Int, + val errorCode: String, + val message: String, + val data: D? = null +) { + companion object { + fun of(status: HttpStatus, codePrefix: String, codeNum: Int, message: String, data: D? = null): CommonResponse { + val errorCode="$codePrefix-$codeNum" + return CommonResponse(status.value(), errorCode, message, data) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/pingping/global/exception/CustomException.kt b/Ping-Common/src/main/kotlin/com/ping/common/exception/CustomException.kt similarity index 72% rename from src/main/kotlin/com/pingping/global/exception/CustomException.kt rename to Ping-Common/src/main/kotlin/com/ping/common/exception/CustomException.kt index 23132a6..757d310 100644 --- a/src/main/kotlin/com/pingping/global/exception/CustomException.kt +++ b/Ping-Common/src/main/kotlin/com/ping/common/exception/CustomException.kt @@ -1,4 +1,4 @@ -package com.pingping.global.exception +package com.ping.common.exception class CustomException( val content: ExceptionContent diff --git a/Ping-Common/src/main/kotlin/com/ping/common/exception/ExceptionContent.kt b/Ping-Common/src/main/kotlin/com/ping/common/exception/ExceptionContent.kt new file mode 100644 index 0000000..a10f382 --- /dev/null +++ b/Ping-Common/src/main/kotlin/com/ping/common/exception/ExceptionContent.kt @@ -0,0 +1,24 @@ +package com.ping.common.exception + +import org.springframework.http.HttpStatus + +enum class ExceptionContent( + val httpStatus: HttpStatus, + val errorPrefix: String, + val errorNum: Int, + val message: String +) { + + //user + USER_NOT_FOUND(HttpStatus.NOT_FOUND, "USER", 1, "해당 사용자를 찾을 수 없습니다."), + + // NonMember 관련 예외 + NON_MEMBER_ALREADY_EXISTS(HttpStatus.CONFLICT, "NONMEMBER",1,"비회원 생성 실패: 이미 존재하는 비회원입니다."), + NON_MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "NONMEMBER",2,"비회원 로그인 실패: 해당 비회원 정보를 찾을 수 없습니다."), + NON_MEMBER_LOGIN_FAILED(HttpStatus.UNAUTHORIZED, "NONMEMBER",3,"비회원 로그인 실패: 비밀번호가 일치하지 않습니다."), + INVALID_PASSWORD_FORMAT(HttpStatus.BAD_REQUEST, "NONMEMBER",4,"비밀번호는 4자리 숫자여야 합니다."), + + // ShareUrl 관련 예외 + INVALID_SHARE_URL(HttpStatus.NOT_FOUND, "SHARE URL",1,"유효하지 않은 공유 URL입니다.") + +} \ No newline at end of file diff --git a/src/main/kotlin/com/pingping/global/exception/GlobalExceptionHandler.kt b/Ping-Common/src/main/kotlin/com/ping/common/exception/GlobalExceptionHandler.kt similarity index 67% rename from src/main/kotlin/com/pingping/global/exception/GlobalExceptionHandler.kt rename to Ping-Common/src/main/kotlin/com/ping/common/exception/GlobalExceptionHandler.kt index 39b1c0e..ec6146e 100644 --- a/src/main/kotlin/com/pingping/global/exception/GlobalExceptionHandler.kt +++ b/Ping-Common/src/main/kotlin/com/ping/common/exception/GlobalExceptionHandler.kt @@ -1,6 +1,5 @@ -package com.pingping.global.exception +package com.ping.common.exception -import com.pingping.global.common.CommonResponse import org.springframework.dao.EmptyResultDataAccessException import org.springframework.http.HttpStatus import org.springframework.http.ResponseEntity @@ -15,24 +14,42 @@ import io.github.oshai.kotlinlogging.KotlinLogging class GlobalExceptionHandler { private val log = KotlinLogging.logger {} + val errorPrefix = "UNKNOWN" + val errorNum = 1 // 공통 에러 응답 생성 메서드 private fun generateErrorResponse(status: HttpStatus, message: String?): ResponseEntity> { - val errorMessage = message ?: "Unexpected error occurred" - val errorResponse = CommonResponse.of(status, errorMessage) + val errorMessage = message ?: "알 수 없는 에러가 발생했습니다." + val errorResponse = CommonResponse.of(status, errorPrefix, errorNum, errorMessage) return ResponseEntity(errorResponse, status) } // 예외 발생시 로그 기록 및 응답 처리 - private fun logAndGenerateErrorResponse(e: Exception, status: HttpStatus, message: String? = null): ResponseEntity> { - log.error("${e.javaClass.simpleName} occurred: ${e.message}", e) + private fun logAndGenerateErrorResponse( + e: Exception, + status: HttpStatus, + message: String? = null + ): ResponseEntity> { + log.error { "${e.javaClass.simpleName} occurred: ${e.message}" } return generateErrorResponse(status, message ?: e.message) } + private fun logAndCustomErrorResponse( + e: Exception, + status: HttpStatus, + errorPrefix: String, + errorNum: Int, + message: String + ): ResponseEntity> { + log.error { "${e.javaClass.simpleName} occurred: ${e.message}" } + val errorResponse = CommonResponse.of(status, errorPrefix, errorNum, message) + return ResponseEntity(errorResponse, status) + } + // 커스텀 Exception 처리 @ExceptionHandler(CustomException::class) fun handleCustomException(exception: CustomException): ResponseEntity> { - return logAndGenerateErrorResponse(exception, exception.content.httpStatus, exception.message) + return logAndCustomErrorResponse(exception, exception.content.httpStatus, exception.content.errorPrefix,exception.content.errorNum ,exception.content.message) } // 모든 Exception 처리 @@ -62,6 +79,10 @@ class GlobalExceptionHandler { // HttpRequestMethodNotSupportedException 처리 @ExceptionHandler(HttpRequestMethodNotSupportedException::class) fun handleRequestMethodException(e: HttpRequestMethodNotSupportedException): ResponseEntity> { - return logAndGenerateErrorResponse(e, HttpStatus.METHOD_NOT_ALLOWED, "API does not support this request method. Please check the endpoint.") + return logAndGenerateErrorResponse( + e, + HttpStatus.METHOD_NOT_ALLOWED, + "API does not support this request method. Please check the endpoint." + ) } } diff --git a/Ping-Domain/build.gradle.kts b/Ping-Domain/build.gradle.kts new file mode 100644 index 0000000..5910e61 --- /dev/null +++ b/Ping-Domain/build.gradle.kts @@ -0,0 +1,11 @@ +dependencies { + implementation(project(":Ping-Common")) +} +tasks { + bootJar { + isEnabled = true + } + jar { + isEnabled = true + } +} \ No newline at end of file diff --git a/Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/aggregate/NonMember.kt b/Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/aggregate/NonMember.kt new file mode 100644 index 0000000..d2f621b --- /dev/null +++ b/Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/aggregate/NonMember.kt @@ -0,0 +1,18 @@ +package com.ping.domain.nonmember.aggregate + +data class NonMember( + val id: Long, + val name: String, + val password: String, + val shareUrl: ShareUrl +) { + companion object { + fun of( + name: String, + password: String, + shareUrl: ShareUrl + ): NonMember { + return NonMember(0L, name, password, shareUrl) + } + } +} \ No newline at end of file diff --git a/Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/aggregate/ShareUrl.kt b/Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/aggregate/ShareUrl.kt new file mode 100644 index 0000000..97f2480 --- /dev/null +++ b/Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/aggregate/ShareUrl.kt @@ -0,0 +1,8 @@ +package com.ping.domain.nonmember.aggregate + +data class ShareUrl( + val id: Long, + val url: String, + val eventName: String, + val neighborhood: String +) \ No newline at end of file diff --git a/Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/repository/NonMemberRepository.kt b/Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/repository/NonMemberRepository.kt new file mode 100644 index 0000000..4a7f1c2 --- /dev/null +++ b/Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/repository/NonMemberRepository.kt @@ -0,0 +1,9 @@ +package com.ping.domain.nonmember.repository + +import com.ping.domain.nonmember.aggregate.NonMember + +interface NonMemberRepository { + fun findByShareUrlIdAndName(shareUrlId: Long, name: String) : NonMember? + + fun save(nonMember: NonMember): NonMember +} \ No newline at end of file diff --git a/Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/repository/ShareUrlRepository.kt b/Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/repository/ShareUrlRepository.kt new file mode 100644 index 0000000..c62f751 --- /dev/null +++ b/Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/repository/ShareUrlRepository.kt @@ -0,0 +1,7 @@ +package com.ping.domain.nonmember.repository + +import com.ping.domain.nonmember.aggregate.ShareUrl + +interface ShareUrlRepository { + fun findById(id: Long): ShareUrl? +} \ No newline at end of file diff --git a/Ping-Infra/build.gradle.kts b/Ping-Infra/build.gradle.kts new file mode 100644 index 0000000..0859627 --- /dev/null +++ b/Ping-Infra/build.gradle.kts @@ -0,0 +1,30 @@ +dependencies { + implementation(project(":Ping-Common")) + implementation(project(":Ping-Domain")) + + implementation("org.springframework.boot:spring-boot-starter-data-jpa") + + // MySQL + runtimeOnly("com.mysql:mysql-connector-j") + + // MongoDB + implementation("org.springframework.boot:spring-boot-starter-data-mongodb") + + // Redis + implementation("org.springframework.boot:spring-boot-starter-data-redis") + + // QueryDSL + // 필요 없으면 삭제 +// implementation("com.querydsl:querydsl-jpa:5.0.0:jakarta") +// kapt("com.querydsl:querydsl-apt:5.0.0:jakarta") +// kapt("jakarta.annotation:jakarta.annotation-api") +// kapt("jakarta.persistence:jakarta.persistence-api") +} +tasks { + bootJar { + isEnabled = true + } + jar { + isEnabled = true + } +} \ No newline at end of file diff --git a/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/jpa/JpaConfig.kt b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/jpa/JpaConfig.kt new file mode 100644 index 0000000..6bd2640 --- /dev/null +++ b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/jpa/JpaConfig.kt @@ -0,0 +1,10 @@ +package com.ping.infra.nonmember.domain.jpa + +import org.springframework.boot.autoconfigure.domain.EntityScan +import org.springframework.context.annotation.Configuration +import org.springframework.data.jpa.repository.config.EnableJpaRepositories + +@Configuration +@EntityScan(basePackages = ["com.ping.infra"]) +@EnableJpaRepositories(basePackages = ["com.ping.infra"]) +class JpaConfig \ No newline at end of file diff --git a/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/jpa/entity/NonMemberEntity.kt b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/jpa/entity/NonMemberEntity.kt new file mode 100644 index 0000000..2f06c34 --- /dev/null +++ b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/jpa/entity/NonMemberEntity.kt @@ -0,0 +1,27 @@ +package com.ping.infra.nonmember.domain.jpa.entity +import com.ping.common.entity.BaseTimeEntity +import com.ping.domain.nonmember.aggregate.NonMember +import jakarta.persistence.* + +@Entity(name = "non_member") +@Table +class NonMemberEntity( + @Column(nullable = false) + val name: String, + + @Column(nullable = false) + val password: String, + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "share_url_id", nullable = false) + var shareUrl: ShareUrlEntity +) : BaseTimeEntity(){ + fun toDomain() : NonMember{ + return NonMember( + id = this.id, + name = this.name, + password = this.password, + shareUrl = this.shareUrl.toDomain() + ) + } +} \ No newline at end of file diff --git a/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/jpa/entity/ShareUrlEntity.kt b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/jpa/entity/ShareUrlEntity.kt new file mode 100644 index 0000000..8f401b0 --- /dev/null +++ b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/jpa/entity/ShareUrlEntity.kt @@ -0,0 +1,28 @@ +package com.ping.infra.nonmember.domain.jpa.entity + +import com.ping.common.entity.BaseTimeEntity +import com.ping.domain.nonmember.aggregate.ShareUrl +import jakarta.persistence.* + +@Entity(name = "share_url") +@Table +class ShareUrlEntity( + @Column(name = "url", nullable = false) + val url: String, + + @Column(name = "event_name", nullable = false) + val eventName: String, + + @Column(name = "neighborhood", nullable = false) + val neighborhood: String, + +) : BaseTimeEntity() { + fun toDomain(): ShareUrl { + return ShareUrl( + id = this.id, + url = this.url, + eventName = this.eventName, + neighborhood = this.neighborhood + ) + } +} \ No newline at end of file diff --git a/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/jpa/repository/NonMemberJpaRepository.kt b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/jpa/repository/NonMemberJpaRepository.kt new file mode 100644 index 0000000..62155bb --- /dev/null +++ b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/jpa/repository/NonMemberJpaRepository.kt @@ -0,0 +1,8 @@ +package com.ping.infra.nonmember.domain.jpa.repository + +import com.ping.infra.nonmember.domain.jpa.entity.NonMemberEntity +import org.springframework.data.jpa.repository.JpaRepository + +interface NonMemberJpaRepository : JpaRepository { + fun findByShareUrlIdAndName(urlId: Long, name: String): NonMemberEntity? +} \ No newline at end of file diff --git a/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/jpa/repository/ShareUrlJpaRepository.kt b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/jpa/repository/ShareUrlJpaRepository.kt new file mode 100644 index 0000000..4316f2a --- /dev/null +++ b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/jpa/repository/ShareUrlJpaRepository.kt @@ -0,0 +1,6 @@ +package com.ping.infra.nonmember.domain.jpa.repository + +import com.ping.infra.nonmember.domain.jpa.entity.ShareUrlEntity +import org.springframework.data.jpa.repository.JpaRepository + +interface ShareUrlJpaRepository : JpaRepository \ No newline at end of file diff --git a/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/mapper/NonMemberMapper.kt b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/mapper/NonMemberMapper.kt new file mode 100644 index 0000000..aec994b --- /dev/null +++ b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/mapper/NonMemberMapper.kt @@ -0,0 +1,26 @@ +package com.ping.infra.nonmember.domain.mapper + +import com.ping.domain.nonmember.aggregate.NonMember +import com.ping.infra.nonmember.domain.jpa.entity.NonMemberEntity + +class NonMemberMapper { + companion object { + + fun toDomain(nonMemberEntity: NonMemberEntity): NonMember { + return NonMember( + nonMemberEntity.id, + nonMemberEntity.name, + nonMemberEntity.password, + nonMemberEntity.shareUrl.toDomain() + ) + } + + fun toEntity(nonMember: NonMember): NonMemberEntity { + return NonMemberEntity( + nonMember.name, + nonMember.password, + ShareUrlMapper.toEntity(nonMember.shareUrl) + ) + } + } +} \ No newline at end of file diff --git a/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/mapper/ShareUrlMapper.kt b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/mapper/ShareUrlMapper.kt new file mode 100644 index 0000000..4a8a431 --- /dev/null +++ b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/mapper/ShareUrlMapper.kt @@ -0,0 +1,24 @@ +package com.ping.infra.nonmember.domain.mapper + +import com.ping.domain.nonmember.aggregate.ShareUrl +import com.ping.infra.nonmember.domain.jpa.entity.ShareUrlEntity + +class ShareUrlMapper { + companion object { + fun toEntity(shareUrl: ShareUrl): ShareUrlEntity { + return ShareUrlEntity( + shareUrl.url, + shareUrl.eventName, + shareUrl.neighborhood + ) + } + fun toDomain(shareUrlEntity: ShareUrlEntity): ShareUrl { + return ShareUrl( + shareUrlEntity.id, + shareUrlEntity.url, + shareUrlEntity.eventName, + shareUrlEntity.neighborhood + ) + } + } +} \ No newline at end of file diff --git a/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/repositoryImpl/NonMemberRepositoryImpl.kt b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/repositoryImpl/NonMemberRepositoryImpl.kt new file mode 100644 index 0000000..ab14953 --- /dev/null +++ b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/repositoryImpl/NonMemberRepositoryImpl.kt @@ -0,0 +1,23 @@ +package com.ping.infra.nonmember.domain.repositoryImpl + +import com.ping.domain.nonmember.aggregate.NonMember +import com.ping.domain.nonmember.repository.NonMemberRepository +import com.ping.infra.nonmember.domain.jpa.repository.NonMemberJpaRepository +import com.ping.infra.nonmember.domain.mapper.NonMemberMapper +import org.springframework.stereotype.Repository + +@Repository +class NonMemberRepositoryImpl( + private val nonMemberJpaRepository: NonMemberJpaRepository, +) : NonMemberRepository { + override fun findByShareUrlIdAndName(shareUrlId: Long, name: String): NonMember? { + return nonMemberJpaRepository.findByShareUrlIdAndName(shareUrlId, name)?.let { + NonMemberMapper.toDomain(it) + } + } + + override fun save(nonMember: NonMember): NonMember { + val nonMemberEntity = NonMemberMapper.toEntity(nonMember) + return NonMemberMapper.toDomain(nonMemberEntity) + } +} \ No newline at end of file diff --git a/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/repositoryImpl/ShareUrlRepositoryImpl.kt b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/repositoryImpl/ShareUrlRepositoryImpl.kt new file mode 100644 index 0000000..c83a1ff --- /dev/null +++ b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/repositoryImpl/ShareUrlRepositoryImpl.kt @@ -0,0 +1,19 @@ +package com.ping.infra.nonmember.domain.repositoryImpl + +import com.ping.domain.nonmember.aggregate.ShareUrl +import com.ping.domain.nonmember.repository.ShareUrlRepository +import com.ping.infra.nonmember.domain.jpa.repository.ShareUrlJpaRepository +import com.ping.infra.nonmember.domain.mapper.ShareUrlMapper +import org.springframework.data.repository.findByIdOrNull +import org.springframework.stereotype.Repository + +@Repository +class ShareUrlRepositoryImpl( + private val shareUrlJpaRepository: ShareUrlJpaRepository +) : ShareUrlRepository { + override fun findById(id: Long): ShareUrl? { + return shareUrlJpaRepository.findByIdOrNull(id)?.let { + ShareUrlMapper.toDomain(it) + } + } +} \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index c91ee78..0c93042 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,22 +1,30 @@ +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile +import org.springframework.boot.gradle.tasks.bundling.BootJar + plugins { id("org.springframework.boot") version "3.3.4" id("io.spring.dependency-management") version "1.1.6" id("org.asciidoctor.jvm.convert") version "3.3.2" - kotlin("kapt") version "1.9.25" - kotlin("jvm") version "1.9.25" - kotlin("plugin.spring") version "1.9.25" - kotlin("plugin.jpa") version "1.9.25" + kotlin("kapt") version "2.0.21" + kotlin("jvm") version "2.0.21" + kotlin("plugin.spring") version "2.0.21" + kotlin("plugin.jpa") version "2.0.21" } +allprojects { + group = "com.pingping" + version = "0.0.1-SNAPSHOT" -group = "com.pingping" -version = "0.0.1-SNAPSHOT" + repositories { + mavenCentral() + } -java { - toolchain { - languageVersion = JavaLanguageVersion.of(17) + tasks.withType { + enabled=false } } +group = "com.pingping" +version = "0.0.1-SNAPSHOT" repositories { mavenCentral() @@ -26,61 +34,7 @@ extra["snippetsDir"] = file("build/generated-snippets") extra["springCloudVersion"] = "2023.0.3" dependencies { - implementation("org.springframework.boot:spring-boot-starter-data-jpa") - implementation("org.springframework.boot:spring-boot-starter-web") - implementation("org.springframework.boot:spring-boot-starter-actuator") - implementation("org.springframework.boot:spring-boot-starter-oauth2-client") - implementation("org.springframework.boot:spring-boot-starter-security") - implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.6.0") - implementation("com.github.gavlyukovskiy:p6spy-spring-boot-starter:1.9.0") - - // logger - implementation("io.github.oshai:kotlin-logging-jvm:7.0.0") - - // Spring Cloud - implementation("org.springframework.cloud:spring-cloud-starter-openfeign") - - // Jackson & Kotlin - implementation("com.fasterxml.jackson.module:jackson-module-kotlin") - implementation("org.jetbrains.kotlin:kotlin-reflect") - - // QueryDSL - // 필요 없으면 삭제 - implementation("com.querydsl:querydsl-jpa:5.0.0:jakarta") - kapt("com.querydsl:querydsl-apt:5.0.0:jakarta") - kapt("jakarta.annotation:jakarta.annotation-api") - kapt("jakarta.persistence:jakarta.persistence-api") - - // MySQL - runtimeOnly("com.mysql:mysql-connector-j") - - // MongoDB - implementation("org.springframework.boot:spring-boot-starter-data-mongodb") - - // Redis - implementation("org.springframework.boot:spring-boot-starter-data-redis") - - // JWT - implementation("io.jsonwebtoken:jjwt-api:0.12.5") - implementation("io.jsonwebtoken:jjwt-impl:0.12.5") - implementation("io.jsonwebtoken:jjwt-jackson:0.12.5") - - // Testing - testImplementation("org.springframework.boot:spring-boot-starter-test") - testImplementation("org.jetbrains.kotlin:kotlin-test-junit5") - testImplementation("org.springframework.kafka:spring-kafka-test") - testImplementation("org.springframework.restdocs:spring-restdocs-mockmvc") - testImplementation("org.springframework.security:spring-security-test") - testRuntimeOnly("org.junit.platform:junit-platform-launcher") - - // Kotest - testImplementation("io.kotest:kotest-runner-junit5:5.9.1") - testImplementation("io.kotest:kotest-assertions-core:5.9.1") - testImplementation("io.kotest:kotest-framework-engine:5.9.1") - - // MockK - testImplementation("io.mockk:mockk:1.13.12") - testImplementation("io.mockk:mockk-agent-jvm:1.13.12") + } allOpen { @@ -113,3 +67,66 @@ tasks.asciidoctor { inputs.dir(project.extra["snippetsDir"]!!) dependsOn(tasks.test) } + +subprojects { + apply(plugin = "kotlin") + apply(plugin = "kotlin-kapt") + apply(plugin = "kotlin-spring") + apply(plugin = "org.jetbrains.kotlin.plugin.spring") + apply(plugin = "org.jetbrains.kotlin.plugin.jpa") + apply(plugin = "org.springframework.boot") + apply(plugin = "io.spring.dependency-management") + apply(plugin = "java-test-fixtures") + + + + tasks.withType { + sourceCompatibility = "17" + targetCompatibility = "17" + } + + tasks.withType { + kotlinOptions { + freeCompilerArgs += "-Xjsr305=strict" + jvmTarget = "17" + } + } + + kapt { + keepJavacAnnotationProcessors = true + } + + dependencies { + // kotlin + implementation("com.fasterxml.jackson.module:jackson-module-kotlin") + implementation("org.jetbrains.kotlin:kotlin-reflect") + + //spring + implementation("org.springframework.boot:spring-boot-starter-web") + + //test + testImplementation("org.springframework.boot:spring-boot-starter-test") + + // Testing + testImplementation("org.springframework.boot:spring-boot-starter-test") + testImplementation("org.jetbrains.kotlin:kotlin-test-junit5") + testImplementation("org.springframework.kafka:spring-kafka-test") + testImplementation("org.springframework.restdocs:spring-restdocs-mockmvc") + testImplementation("org.springframework.security:spring-security-test") + testRuntimeOnly("org.junit.platform:junit-platform-launcher") + + // Kotest + testImplementation("io.kotest:kotest-runner-junit5:5.9.1") + testImplementation("io.kotest:kotest-assertions-core:5.9.1") + testImplementation("io.kotest:kotest-framework-engine:5.9.1") + + // MockK + testImplementation("io.mockk:mockk:1.13.12") + testImplementation("io.mockk:mockk-agent-jvm:1.13.12") + } + + tasks.withType { + useJUnitPlatform() + } + +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 2ab997b..8396792 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1 +1,9 @@ +plugins { + id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0" +} rootProject.name = "pingping-BE" +include("Ping-Api") +include("Ping-Application") +include("Ping-Common") +include("Ping-Domain") +include("Ping-Infra") diff --git a/src/main/kotlin/com/pingping/PingServiceApplication.kt b/src/main/kotlin/com/pingping/PingServiceApplication.kt deleted file mode 100644 index 94da816..0000000 --- a/src/main/kotlin/com/pingping/PingServiceApplication.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.pingping - -import org.springframework.boot.autoconfigure.SpringBootApplication -import org.springframework.boot.runApplication - -@SpringBootApplication -class PingServiceApplication - -fun main(args: Array) { - runApplication(*args) -} diff --git a/src/main/kotlin/com/pingping/global/common/CommonResponse.kt b/src/main/kotlin/com/pingping/global/common/CommonResponse.kt deleted file mode 100644 index 9bd8187..0000000 --- a/src/main/kotlin/com/pingping/global/common/CommonResponse.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.pingping.global.common - -import org.springframework.http.HttpStatus - -data class CommonResponse( - val code: Int, - val message: String, - val data: D? = null -) { - companion object { - fun of(status: HttpStatus, message: String, data: D? = null): CommonResponse { - return CommonResponse(status.value(), message, data) - } - } -} \ No newline at end of file diff --git a/src/main/kotlin/com/pingping/global/exception/ExceptionContent.kt b/src/main/kotlin/com/pingping/global/exception/ExceptionContent.kt deleted file mode 100644 index 41260f0..0000000 --- a/src/main/kotlin/com/pingping/global/exception/ExceptionContent.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.pingping.global.exception - -import org.springframework.http.HttpStatus - -enum class ExceptionContent(val httpStatus: HttpStatus, val message: String) { - - //user - USER_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 사용자를 찾을 수 없습니다."), - -} \ No newline at end of file diff --git a/src/test/kotlin/com/pingping/PingServiceApplicationTests.kt b/src/test/kotlin/com/pingping/PingServiceApplicationTests.kt deleted file mode 100644 index 25f77af..0000000 --- a/src/test/kotlin/com/pingping/PingServiceApplicationTests.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.pingping - -import org.junit.jupiter.api.Test -import org.springframework.boot.test.context.SpringBootTest - -@SpringBootTest -class PingServiceApplicationTests { - - @Test - fun contextLoads() { - } - -} From 8cf0729d43fec690844a9f565f6fd56d17558a3c Mon Sep 17 00:00:00 2001 From: sominyun Date: Sun, 20 Oct 2024 18:22:38 +0900 Subject: [PATCH 007/203] =?UTF-8?q?fix:=20base=20entity=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/com/ping/common/entity/BaseEntity.kt | 14 -------------- 1 file changed, 14 deletions(-) delete mode 100644 Ping-Common/src/main/kotlin/com/ping/common/entity/BaseEntity.kt diff --git a/Ping-Common/src/main/kotlin/com/ping/common/entity/BaseEntity.kt b/Ping-Common/src/main/kotlin/com/ping/common/entity/BaseEntity.kt deleted file mode 100644 index d8e5236..0000000 --- a/Ping-Common/src/main/kotlin/com/ping/common/entity/BaseEntity.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.ping.common.entity - -import jakarta.persistence.GeneratedValue -import jakarta.persistence.GenerationType -import jakarta.persistence.Id -import jakarta.persistence.MappedSuperclass - -@MappedSuperclass -open class BaseEntity { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - var id: Long = 0 - protected set -} \ No newline at end of file From ad8e15404a3b7d8bf3f126edaf20f135f33a2c75 Mon Sep 17 00:00:00 2001 From: sominyun Date: Sun, 20 Oct 2024 18:22:51 +0900 Subject: [PATCH 008/203] =?UTF-8?q?fix:=20base=20entity=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ping/infra/nonmember/domain/jpa/common}/BaseTimeEntity.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename {Ping-Common/src/main/kotlin/com/ping/common/entity => Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/jpa/common}/BaseTimeEntity.kt (86%) diff --git a/Ping-Common/src/main/kotlin/com/ping/common/entity/BaseTimeEntity.kt b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/jpa/common/BaseTimeEntity.kt similarity index 86% rename from Ping-Common/src/main/kotlin/com/ping/common/entity/BaseTimeEntity.kt rename to Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/jpa/common/BaseTimeEntity.kt index c9f733d..e15bcd1 100644 --- a/Ping-Common/src/main/kotlin/com/ping/common/entity/BaseTimeEntity.kt +++ b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/jpa/common/BaseTimeEntity.kt @@ -1,4 +1,4 @@ -package com.ping.common.entity +package com.ping.infra.nonmember.domain.jpa.common import jakarta.persistence.EntityListeners import jakarta.persistence.MappedSuperclass @@ -9,7 +9,7 @@ import java.time.LocalDateTime @MappedSuperclass @EntityListeners(AuditingEntityListener::class) -abstract class BaseTimeEntity: BaseEntity() { +abstract class BaseTimeEntity { @CreatedDate var createdAt: LocalDateTime? = null From 27c3710e33c9e6d29207d1ebc474086a2012926c Mon Sep 17 00:00:00 2001 From: sominyun Date: Sun, 20 Oct 2024 18:23:51 +0900 Subject: [PATCH 009/203] =?UTF-8?q?feat:=20url=20=ED=99=95=EC=9E=A5=20util?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/kotlin/com/ping/common/util/UrlUtil.kt | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 Ping-Common/src/main/kotlin/com/ping/common/util/UrlUtil.kt diff --git a/Ping-Common/src/main/kotlin/com/ping/common/util/UrlUtil.kt b/Ping-Common/src/main/kotlin/com/ping/common/util/UrlUtil.kt new file mode 100644 index 0000000..7587c82 --- /dev/null +++ b/Ping-Common/src/main/kotlin/com/ping/common/util/UrlUtil.kt @@ -0,0 +1,16 @@ +package com.ping.common.util + +import java.io.BufferedReader +import java.io.InputStreamReader + +object UrlUtil { + fun expandShortUrl(shortUrl: String): String { + val process = ProcessBuilder("curl", "-L", "-s", "-o", "/dev/null", "-w", "%{url_effective}", shortUrl).start() + + val reader = BufferedReader(InputStreamReader(process.inputStream)) + val expandedUrl = reader.readLine() + + reader.close() + return expandedUrl + } +} \ No newline at end of file From 3d3a3805cf470f29fc0c6f8ceb0c2b53a54e0adc Mon Sep 17 00:00:00 2001 From: sominyun Date: Sun, 20 Oct 2024 18:28:09 +0900 Subject: [PATCH 010/203] =?UTF-8?q?feat:=20=EC=9D=B4=EB=B2=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=A7=84=EC=9E=85=20api=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Ping-Api/build.gradle.kts | 4 + .../kotlin/com/ping/api/PingApplication.kt | 3 + .../ping/api/nonmember/NonMemberController.kt | 16 +++- Ping-Application/build.gradle.kts | 1 + .../application/nonmember/NonMemberService.kt | 73 ++++++++++++++++--- .../dto/request/NonMemberCreateRequest.kt | 8 +- Ping-Client/build.gradle.kts | 17 +++++ Ping-Common/build.gradle.kts | 3 + .../ping/common/exception/ExceptionContent.kt | 6 +- .../nonmember/aggregate/BookmarkDomain.kt | 10 +++ .../domain/nonmember/aggregate/NonMember.kt | 18 ----- .../aggregate/NonMemberBookmarkUrlDomain.kt | 12 +++ .../nonmember/aggregate/NonMemberDomain.kt | 13 ++++ .../aggregate/NonMemberPlaceDomain.kt | 11 +++ .../aggregate/NonMemberStoreUrlDomain.kt | 12 +++ .../{ShareUrl.kt => ShareUrlDomain.kt} | 5 +- .../repository/BookmarkRepository.kt | 7 ++ .../NonMemberBookmarkUrlRepository.kt | 7 ++ .../repository/NonMemberPlaceRepository.kt | 7 ++ .../repository/NonMemberRepository.kt | 6 +- .../repository/NonMemberStoreUrlRepository.kt | 7 ++ .../repository/ShareUrlRepository.kt | 6 +- .../domain/jpa/{ => common}/JpaConfig.kt | 2 +- .../jpa/entity/NonMemberBookmarkUrlEntity.kt | 18 +++++ .../domain/jpa/entity/NonMemberEntity.kt | 25 +++---- .../domain/jpa/entity/NonMemberPlaceEntity.kt | 19 +++++ .../jpa/entity/NonMemberStoreUrlEntity.kt | 18 +++++ .../domain/jpa/entity/ShareUrlEntity.kt | 28 +++---- .../NonMemberBookmarkUrlJpaRepository.kt | 7 ++ .../repository/NonMemberPlaceJpaRepository.kt | 7 ++ .../NonMemberStoreUrlJpaRepository.kt | 7 ++ .../jpa/repository/ShareUrlJpaRepository.kt | 4 +- .../nonmember/domain/mapper/BookmarkMapper.kt | 25 +++++++ .../mapper/NonMemberBookmarkUrlMapper.kt | 19 +++++ .../domain/mapper/NonMemberMapper.kt | 33 ++++----- .../domain/mapper/NonMemberPlaceMapper.kt | 20 +++++ .../domain/mapper/NonMemberStoreUrlMapper.kt | 19 +++++ .../nonmember/domain/mapper/ShareUrlMapper.kt | 37 +++++----- .../mongo/common/BaseTimeMongoEntity.kt | 18 +++++ .../domain/mongo/common/MongoConfig.kt | 8 ++ .../domain/mongo/entity/BookmarkEntity.kt | 18 +++++ .../repository/BookmarkMongoRepository.kt | 7 ++ .../repositoryImpl/BookmarkRepositoryImpl.kt | 19 +++++ .../NonMemberBookmarkUrlRepositoryImpl.kt | 18 +++++ .../NonMemberPlaceRepositoryImpl.kt | 17 +++++ .../repositoryImpl/NonMemberRepositoryImpl.kt | 9 +-- .../NonMemberStoreUrlRepositoryImpl.kt | 17 +++++ .../repositoryImpl/ShareUrlRepositoryImpl.kt | 6 +- settings.gradle.kts | 1 + 49 files changed, 556 insertions(+), 122 deletions(-) create mode 100644 Ping-Client/build.gradle.kts create mode 100644 Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/aggregate/BookmarkDomain.kt delete mode 100644 Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/aggregate/NonMember.kt create mode 100644 Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/aggregate/NonMemberBookmarkUrlDomain.kt create mode 100644 Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/aggregate/NonMemberDomain.kt create mode 100644 Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/aggregate/NonMemberPlaceDomain.kt create mode 100644 Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/aggregate/NonMemberStoreUrlDomain.kt rename Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/aggregate/{ShareUrl.kt => ShareUrlDomain.kt} (58%) create mode 100644 Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/repository/BookmarkRepository.kt create mode 100644 Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/repository/NonMemberBookmarkUrlRepository.kt create mode 100644 Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/repository/NonMemberPlaceRepository.kt create mode 100644 Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/repository/NonMemberStoreUrlRepository.kt rename Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/jpa/{ => common}/JpaConfig.kt (86%) create mode 100644 Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/jpa/entity/NonMemberBookmarkUrlEntity.kt create mode 100644 Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/jpa/entity/NonMemberPlaceEntity.kt create mode 100644 Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/jpa/entity/NonMemberStoreUrlEntity.kt create mode 100644 Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/jpa/repository/NonMemberBookmarkUrlJpaRepository.kt create mode 100644 Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/jpa/repository/NonMemberPlaceJpaRepository.kt create mode 100644 Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/jpa/repository/NonMemberStoreUrlJpaRepository.kt create mode 100644 Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/mapper/BookmarkMapper.kt create mode 100644 Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/mapper/NonMemberBookmarkUrlMapper.kt create mode 100644 Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/mapper/NonMemberPlaceMapper.kt create mode 100644 Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/mapper/NonMemberStoreUrlMapper.kt create mode 100644 Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/mongo/common/BaseTimeMongoEntity.kt create mode 100644 Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/mongo/common/MongoConfig.kt create mode 100644 Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/mongo/entity/BookmarkEntity.kt create mode 100644 Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/mongo/repository/BookmarkMongoRepository.kt create mode 100644 Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/repositoryImpl/BookmarkRepositoryImpl.kt create mode 100644 Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/repositoryImpl/NonMemberBookmarkUrlRepositoryImpl.kt create mode 100644 Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/repositoryImpl/NonMemberPlaceRepositoryImpl.kt create mode 100644 Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/repositoryImpl/NonMemberStoreUrlRepositoryImpl.kt diff --git a/Ping-Api/build.gradle.kts b/Ping-Api/build.gradle.kts index c741365..7406963 100644 --- a/Ping-Api/build.gradle.kts +++ b/Ping-Api/build.gradle.kts @@ -3,6 +3,10 @@ dependencies { implementation(project(":Ping-Common")) implementation(project(":Ping-Domain")) implementation(project(":Ping-Infra")) + implementation(project(":Ping-Client")) + + // MongoDB + implementation("org.springframework.boot:spring-boot-starter-data-mongodb") } tasks { bootJar { diff --git a/Ping-Api/src/main/kotlin/com/ping/api/PingApplication.kt b/Ping-Api/src/main/kotlin/com/ping/api/PingApplication.kt index 772b4fb..595faab 100644 --- a/Ping-Api/src/main/kotlin/com/ping/api/PingApplication.kt +++ b/Ping-Api/src/main/kotlin/com/ping/api/PingApplication.kt @@ -3,10 +3,12 @@ package com.ping.api import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.context.properties.ConfigurationPropertiesScan import org.springframework.boot.runApplication +import org.springframework.data.mongodb.repository.config.EnableMongoRepositories import org.springframework.scheduling.annotation.EnableScheduling @EnableScheduling @ConfigurationPropertiesScan +@EnableMongoRepositories(basePackages = ["com.ping.infra"]) @SpringBootApplication( scanBasePackages = [ "com.ping.api", @@ -14,6 +16,7 @@ import org.springframework.scheduling.annotation.EnableScheduling "com.ping.common", "com.ping.domain", "com.ping.infra", + "com.ping.client" ] ) class PingApplication diff --git a/Ping-Api/src/main/kotlin/com/ping/api/nonmember/NonMemberController.kt b/Ping-Api/src/main/kotlin/com/ping/api/nonmember/NonMemberController.kt index 605e112..c4ae8de 100644 --- a/Ping-Api/src/main/kotlin/com/ping/api/nonmember/NonMemberController.kt +++ b/Ping-Api/src/main/kotlin/com/ping/api/nonmember/NonMemberController.kt @@ -1,8 +1,20 @@ package com.ping.api.nonmember +import com.ping.application.nonmember.NonMemberService +import com.ping.application.nonmember.dto.request.NonMemberCreateRequest +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RestController -@RestController(value = "/nonmembers") -class NonMemberController { +@RestController +@RequestMapping("/nonmembers") +class NonMemberController( + private val nonMemberService: NonMemberService, +) { + @PostMapping("/pings") + fun createNonMemberPings(@RequestBody nonMemberCreateRequest: NonMemberCreateRequest) { + return nonMemberService.createNonMemberPings(nonMemberCreateRequest) + } } \ No newline at end of file diff --git a/Ping-Application/build.gradle.kts b/Ping-Application/build.gradle.kts index 109e2e8..1f16c3a 100644 --- a/Ping-Application/build.gradle.kts +++ b/Ping-Application/build.gradle.kts @@ -1,6 +1,7 @@ dependencies { implementation(project(":Ping-Common")) implementation(project(":Ping-Domain")) + implementation(project(":Ping-Client")) //jpa implementation("org.springframework.boot:spring-boot-starter-data-jpa") diff --git a/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberService.kt b/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberService.kt index 81e22ef..3f2ea58 100644 --- a/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberService.kt +++ b/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberService.kt @@ -1,11 +1,13 @@ package com.ping.application.nonmember import com.ping.application.nonmember.dto.request.NonMemberCreateRequest +import com.ping.client.navermap.NaverMapClient +import com.ping.common.util.UrlUtil import com.ping.common.exception.CustomException import com.ping.common.exception.ExceptionContent -import com.ping.domain.nonmember.aggregate.NonMember -import com.ping.domain.nonmember.repository.NonMemberRepository -import com.ping.domain.nonmember.repository.ShareUrlRepository +import com.ping.domain.nonmember.aggregate.* +import com.ping.domain.nonmember.repository.* +import org.springframework.dao.DataIntegrityViolationException import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional @@ -14,26 +16,78 @@ import org.springframework.transaction.annotation.Transactional class NonMemberService( private val nonMemberRepository: NonMemberRepository, private val shareUrlRepository: ShareUrlRepository, + private val bookmarkRepository: BookmarkRepository, + private val nonMemberPlaceRepository: NonMemberPlaceRepository, + private val nonMemberBookmarkUrlRepository: NonMemberBookmarkUrlRepository, + private val nonMemberStoreUrlRepository: NonMemberStoreUrlRepository, + private val naverMapClient: NaverMapClient ) { @Transactional - fun createNonMember(request: NonMemberCreateRequest): Long { + fun createNonMemberPings(request: NonMemberCreateRequest) { // 비밀번호 형식 검사 (4자리 숫자) validatePassword(request.password) - val shareUrl = shareUrlRepository.findById(request.shareUrlId) + val shareUrl = shareUrlRepository.findByUuid(request.uuid) ?: throw CustomException(ExceptionContent.INVALID_SHARE_URL) // shareUrlId과 name으로 비회원 존재 여부 확인 - nonMemberRepository.findByShareUrlIdAndName(request.shareUrlId, request.name)?.let { + nonMemberRepository.findByShareUrlIdAndName(shareUrl.id, request.name)?.let { throw CustomException(ExceptionContent.NON_MEMBER_ALREADY_EXISTS) } // NonMember 엔티티 생성 및 저장 - val nonMember = NonMember.of(request.name, request.password, shareUrl) - val savedNonMember = nonMemberRepository.save(nonMember) + val nonMemberDomain = NonMemberDomain.of(request.name, request.password, shareUrl) + val nonmember = nonMemberRepository.save(nonMemberDomain) - return savedNonMember.id + //url 저장 + val nonMemberBookmarkUrlDomains = NonMemberBookmarkUrlDomain.of(nonmember, request.bookmarkUrls) + nonMemberBookmarkUrlRepository.saveAll(nonMemberBookmarkUrlDomains) + + val nonMemberStoreUrlDomains = NonMemberStoreUrlDomain.of(nonmember, request.storeUrls) + nonMemberStoreUrlRepository.saveALl(nonMemberStoreUrlDomains) + + val nonMemberPlaces = mutableListOf() + val bookmarks = mutableListOf() + //맵핀 모은 링크 추출 + bookmarks.addAll( + request.bookmarkUrls.flatMap { + val url = UrlUtil.expandShortUrl(it) + naverMapClient.bookmarkUrlToBookmarkLists(url).bookmarkList.map { bookmark -> + //NonMemberPlace 저장 + nonMemberPlaces + .takeIf { nonMemberPlace -> nonMemberPlace.none { place -> place.sid == bookmark.sid } } + ?.add(NonMemberPlaceDomain.of(nonmember, bookmark.sid)) + BookmarkDomain( + name = bookmark.name, + px = bookmark.px, + py = bookmark.py, + sid = bookmark.sid, + address = bookmark.address, + mcidName = bookmark.mcidName + ) + } + }) + //맵핀 가게 링크 추출 + bookmarks.addAll( + request.storeUrls.map { + val url = UrlUtil.expandShortUrl(it) + val bookmark = naverMapClient.storeUrlToBookmark(url) + //NonMemberPlace 저장 + nonMemberPlaces + .takeIf { nonMemberPlace -> nonMemberPlace.none { place -> place.sid == bookmark.sid } } + ?.add(NonMemberPlaceDomain.of(nonmember, bookmark.sid)) + BookmarkDomain( + name = bookmark.name, + px = bookmark.px, + py = bookmark.py, + sid = bookmark.sid, + address = bookmark.address, + mcidName = bookmark.mcidName + ) + }) + bookmarkRepository.saveAll(bookmarks) + nonMemberPlaceRepository.saveAll(nonMemberPlaces) } // 비밀번호 유효성 검증 로직 @@ -42,5 +96,4 @@ class NonMemberService( throw CustomException(ExceptionContent.INVALID_PASSWORD_FORMAT) } } - } \ No newline at end of file diff --git a/Ping-Application/src/main/kotlin/com/ping/application/nonmember/dto/request/NonMemberCreateRequest.kt b/Ping-Application/src/main/kotlin/com/ping/application/nonmember/dto/request/NonMemberCreateRequest.kt index 690dbd2..8eb38c4 100644 --- a/Ping-Application/src/main/kotlin/com/ping/application/nonmember/dto/request/NonMemberCreateRequest.kt +++ b/Ping-Application/src/main/kotlin/com/ping/application/nonmember/dto/request/NonMemberCreateRequest.kt @@ -1,7 +1,9 @@ package com.ping.application.nonmember.dto.request data class NonMemberCreateRequest( - val shareUrlId: Long, + val uuid: String, val name: String, - val password: String -) + val password: String, + val bookmarkUrls: List, + val storeUrls: List +) \ No newline at end of file diff --git a/Ping-Client/build.gradle.kts b/Ping-Client/build.gradle.kts new file mode 100644 index 0000000..5504757 --- /dev/null +++ b/Ping-Client/build.gradle.kts @@ -0,0 +1,17 @@ +dependencies { + implementation(project(":Ping-Common")) + + // Jsoup + implementation("org.jsoup:jsoup:1.15.4") + + // webclient + implementation("org.springframework.boot:spring-boot-starter-webflux") +} +tasks { + bootJar { + isEnabled = false + } + jar { + isEnabled = true + } +} diff --git a/Ping-Common/build.gradle.kts b/Ping-Common/build.gradle.kts index 8d57d88..686a81a 100644 --- a/Ping-Common/build.gradle.kts +++ b/Ping-Common/build.gradle.kts @@ -7,6 +7,9 @@ dependencies { //jpa implementation("org.springframework.boot:spring-boot-starter-data-jpa") + // MongoDB + implementation("org.springframework.boot:spring-boot-starter-data-mongodb") + //p6spy implementation("com.github.gavlyukovskiy:p6spy-spring-boot-starter:1.9.0") diff --git a/Ping-Common/src/main/kotlin/com/ping/common/exception/ExceptionContent.kt b/Ping-Common/src/main/kotlin/com/ping/common/exception/ExceptionContent.kt index a10f382..6116a2e 100644 --- a/Ping-Common/src/main/kotlin/com/ping/common/exception/ExceptionContent.kt +++ b/Ping-Common/src/main/kotlin/com/ping/common/exception/ExceptionContent.kt @@ -8,7 +8,6 @@ enum class ExceptionContent( val errorNum: Int, val message: String ) { - //user USER_NOT_FOUND(HttpStatus.NOT_FOUND, "USER", 1, "해당 사용자를 찾을 수 없습니다."), @@ -19,6 +18,9 @@ enum class ExceptionContent( INVALID_PASSWORD_FORMAT(HttpStatus.BAD_REQUEST, "NONMEMBER",4,"비밀번호는 4자리 숫자여야 합니다."), // ShareUrl 관련 예외 - INVALID_SHARE_URL(HttpStatus.NOT_FOUND, "SHARE URL",1,"유효하지 않은 공유 URL입니다.") + INVALID_SHARE_URL(HttpStatus.NOT_FOUND, "SHARE URL",1,"유효하지 않은 공유 URL입니다."), + //북마크 관련 예외 + INVALID_BOOKMARK_URL(HttpStatus.BAD_REQUEST, "BOOKMARK",1,"북마크를 불러올 수 없습니다"), + INVALID_STORE_URL(HttpStatus.BAD_REQUEST, "BOOKMARK",2,"가게 정보를 불러올 수 없습니다") } \ No newline at end of file diff --git a/Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/aggregate/BookmarkDomain.kt b/Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/aggregate/BookmarkDomain.kt new file mode 100644 index 0000000..c3b90c4 --- /dev/null +++ b/Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/aggregate/BookmarkDomain.kt @@ -0,0 +1,10 @@ +package com.ping.domain.nonmember.aggregate + +data class BookmarkDomain ( + val name: String, + val px: Double, + val py: Double, + val sid: String, + val address: String, + val mcidName: String, +) \ No newline at end of file diff --git a/Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/aggregate/NonMember.kt b/Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/aggregate/NonMember.kt deleted file mode 100644 index d2f621b..0000000 --- a/Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/aggregate/NonMember.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.ping.domain.nonmember.aggregate - -data class NonMember( - val id: Long, - val name: String, - val password: String, - val shareUrl: ShareUrl -) { - companion object { - fun of( - name: String, - password: String, - shareUrl: ShareUrl - ): NonMember { - return NonMember(0L, name, password, shareUrl) - } - } -} \ No newline at end of file diff --git a/Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/aggregate/NonMemberBookmarkUrlDomain.kt b/Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/aggregate/NonMemberBookmarkUrlDomain.kt new file mode 100644 index 0000000..40fafd6 --- /dev/null +++ b/Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/aggregate/NonMemberBookmarkUrlDomain.kt @@ -0,0 +1,12 @@ +package com.ping.domain.nonmember.aggregate + +class NonMemberBookmarkUrlDomain( + val id: Long, + val nonMember: NonMemberDomain, + val bookmarkUrl: String +) { + companion object { + fun of(nonMember: NonMemberDomain, bookmarkUrls: List) = + bookmarkUrls.map { NonMemberBookmarkUrlDomain(0L, nonMember, it) } + } +} \ No newline at end of file diff --git a/Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/aggregate/NonMemberDomain.kt b/Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/aggregate/NonMemberDomain.kt new file mode 100644 index 0000000..485d46e --- /dev/null +++ b/Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/aggregate/NonMemberDomain.kt @@ -0,0 +1,13 @@ +package com.ping.domain.nonmember.aggregate + +data class NonMemberDomain( + val id: Long, + val name: String, + val password: String, + val shareUrlDomain: ShareUrlDomain +) { + companion object { + fun of(name: String, password: String, shareUrlDomain: ShareUrlDomain) = + NonMemberDomain(0L, name, password, shareUrlDomain) + } +} \ No newline at end of file diff --git a/Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/aggregate/NonMemberPlaceDomain.kt b/Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/aggregate/NonMemberPlaceDomain.kt new file mode 100644 index 0000000..82e762a --- /dev/null +++ b/Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/aggregate/NonMemberPlaceDomain.kt @@ -0,0 +1,11 @@ +package com.ping.domain.nonmember.aggregate + +data class NonMemberPlaceDomain( + val id: Long, + val nonMember: NonMemberDomain, + val sid: String +) { + companion object { + fun of(nonMember: NonMemberDomain, sid: String) = NonMemberPlaceDomain(0L, nonMember, sid) + } +} \ No newline at end of file diff --git a/Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/aggregate/NonMemberStoreUrlDomain.kt b/Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/aggregate/NonMemberStoreUrlDomain.kt new file mode 100644 index 0000000..d1263f9 --- /dev/null +++ b/Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/aggregate/NonMemberStoreUrlDomain.kt @@ -0,0 +1,12 @@ +package com.ping.domain.nonmember.aggregate + +data class NonMemberStoreUrlDomain( + val id: Long, + val nonMember: NonMemberDomain, + val storeUrl: String +) { + companion object { + fun of(nonMemberDomain: NonMemberDomain, storeUrl: List) = + storeUrl.map { NonMemberStoreUrlDomain(0L, nonMemberDomain, it) } + } +} \ No newline at end of file diff --git a/Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/aggregate/ShareUrl.kt b/Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/aggregate/ShareUrlDomain.kt similarity index 58% rename from Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/aggregate/ShareUrl.kt rename to Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/aggregate/ShareUrlDomain.kt index 97f2480..daa44cf 100644 --- a/Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/aggregate/ShareUrl.kt +++ b/Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/aggregate/ShareUrlDomain.kt @@ -1,8 +1,9 @@ package com.ping.domain.nonmember.aggregate -data class ShareUrl( +data class ShareUrlDomain( val id: Long, val url: String, val eventName: String, - val neighborhood: String + val neighborhood: String, + val uuid: String ) \ No newline at end of file diff --git a/Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/repository/BookmarkRepository.kt b/Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/repository/BookmarkRepository.kt new file mode 100644 index 0000000..38b4b02 --- /dev/null +++ b/Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/repository/BookmarkRepository.kt @@ -0,0 +1,7 @@ +package com.ping.domain.nonmember.repository + +import com.ping.domain.nonmember.aggregate.BookmarkDomain + +interface BookmarkRepository { + fun saveAll(bookmarkDomains: List) : List +} \ No newline at end of file diff --git a/Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/repository/NonMemberBookmarkUrlRepository.kt b/Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/repository/NonMemberBookmarkUrlRepository.kt new file mode 100644 index 0000000..666d410 --- /dev/null +++ b/Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/repository/NonMemberBookmarkUrlRepository.kt @@ -0,0 +1,7 @@ +package com.ping.domain.nonmember.repository + +import com.ping.domain.nonmember.aggregate.NonMemberBookmarkUrlDomain + +interface NonMemberBookmarkUrlRepository { + fun saveAll(nonMemberBookmarkUrlDomains: List) : List +} \ No newline at end of file diff --git a/Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/repository/NonMemberPlaceRepository.kt b/Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/repository/NonMemberPlaceRepository.kt new file mode 100644 index 0000000..70f1923 --- /dev/null +++ b/Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/repository/NonMemberPlaceRepository.kt @@ -0,0 +1,7 @@ +package com.ping.domain.nonmember.repository + +import com.ping.domain.nonmember.aggregate.NonMemberPlaceDomain + +interface NonMemberPlaceRepository { + fun saveAll(nonMemberPlaceDomains: List): List +} \ No newline at end of file diff --git a/Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/repository/NonMemberRepository.kt b/Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/repository/NonMemberRepository.kt index 4a7f1c2..324a11b 100644 --- a/Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/repository/NonMemberRepository.kt +++ b/Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/repository/NonMemberRepository.kt @@ -1,9 +1,9 @@ package com.ping.domain.nonmember.repository -import com.ping.domain.nonmember.aggregate.NonMember +import com.ping.domain.nonmember.aggregate.NonMemberDomain interface NonMemberRepository { - fun findByShareUrlIdAndName(shareUrlId: Long, name: String) : NonMember? + fun findByShareUrlIdAndName(shareUrlId: Long, name: String) : NonMemberDomain? - fun save(nonMember: NonMember): NonMember + fun save(nonMemberDomain: NonMemberDomain): NonMemberDomain } \ No newline at end of file diff --git a/Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/repository/NonMemberStoreUrlRepository.kt b/Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/repository/NonMemberStoreUrlRepository.kt new file mode 100644 index 0000000..fcfb3a7 --- /dev/null +++ b/Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/repository/NonMemberStoreUrlRepository.kt @@ -0,0 +1,7 @@ +package com.ping.domain.nonmember.repository + +import com.ping.domain.nonmember.aggregate.NonMemberStoreUrlDomain + +interface NonMemberStoreUrlRepository { + fun saveALl(nonMemberStoreUrlDomains: List) : List +} \ No newline at end of file diff --git a/Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/repository/ShareUrlRepository.kt b/Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/repository/ShareUrlRepository.kt index c62f751..518d2cd 100644 --- a/Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/repository/ShareUrlRepository.kt +++ b/Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/repository/ShareUrlRepository.kt @@ -1,7 +1,7 @@ package com.ping.domain.nonmember.repository -import com.ping.domain.nonmember.aggregate.ShareUrl +import com.ping.domain.nonmember.aggregate.ShareUrlDomain interface ShareUrlRepository { - fun findById(id: Long): ShareUrl? -} \ No newline at end of file + fun findByUuid(uuid: String): ShareUrlDomain? +} diff --git a/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/jpa/JpaConfig.kt b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/jpa/common/JpaConfig.kt similarity index 86% rename from Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/jpa/JpaConfig.kt rename to Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/jpa/common/JpaConfig.kt index 6bd2640..9e16b9e 100644 --- a/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/jpa/JpaConfig.kt +++ b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/jpa/common/JpaConfig.kt @@ -1,4 +1,4 @@ -package com.ping.infra.nonmember.domain.jpa +package com.ping.infra.nonmember.domain.jpa.common import org.springframework.boot.autoconfigure.domain.EntityScan import org.springframework.context.annotation.Configuration diff --git a/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/jpa/entity/NonMemberBookmarkUrlEntity.kt b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/jpa/entity/NonMemberBookmarkUrlEntity.kt new file mode 100644 index 0000000..a02de25 --- /dev/null +++ b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/jpa/entity/NonMemberBookmarkUrlEntity.kt @@ -0,0 +1,18 @@ +package com.ping.infra.nonmember.domain.jpa.entity + +import com.ping.infra.nonmember.domain.jpa.common.BaseTimeEntity +import jakarta.persistence.* + +@Entity(name = "non_member_bookmark_link") +class NonMemberBookmarkUrlEntity( + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + val id: Long, + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(nullable = false) + val nonMember: NonMemberEntity, + + @Column(nullable = false) + val bookmarkUrl: String +) : BaseTimeEntity() \ No newline at end of file diff --git a/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/jpa/entity/NonMemberEntity.kt b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/jpa/entity/NonMemberEntity.kt index 2f06c34..1efdae7 100644 --- a/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/jpa/entity/NonMemberEntity.kt +++ b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/jpa/entity/NonMemberEntity.kt @@ -1,11 +1,15 @@ package com.ping.infra.nonmember.domain.jpa.entity -import com.ping.common.entity.BaseTimeEntity -import com.ping.domain.nonmember.aggregate.NonMember + +import com.ping.infra.nonmember.domain.jpa.common.BaseTimeEntity import jakarta.persistence.* @Entity(name = "non_member") -@Table +@Table(uniqueConstraints = [UniqueConstraint(columnNames = ["name", "share_url_id"])]) class NonMemberEntity( + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + val id: Long, + @Column(nullable = false) val name: String, @@ -13,15 +17,6 @@ class NonMemberEntity( val password: String, @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "share_url_id", nullable = false) - var shareUrl: ShareUrlEntity -) : BaseTimeEntity(){ - fun toDomain() : NonMember{ - return NonMember( - id = this.id, - name = this.name, - password = this.password, - shareUrl = this.shareUrl.toDomain() - ) - } -} \ No newline at end of file + @JoinColumn(nullable = false) + val shareUrl: ShareUrlEntity +) : BaseTimeEntity() \ No newline at end of file diff --git a/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/jpa/entity/NonMemberPlaceEntity.kt b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/jpa/entity/NonMemberPlaceEntity.kt new file mode 100644 index 0000000..7f203af --- /dev/null +++ b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/jpa/entity/NonMemberPlaceEntity.kt @@ -0,0 +1,19 @@ +package com.ping.infra.nonmember.domain.jpa.entity + +import com.ping.infra.nonmember.domain.jpa.common.BaseTimeEntity +import jakarta.persistence.* + +@Entity(name = "non_member_place") +@Table(uniqueConstraints = [UniqueConstraint(columnNames = ["non_member_id", "sid"])]) +class NonMemberPlaceEntity( + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + val id: Long, + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(nullable = false) + val nonMember: NonMemberEntity, + + @Column(nullable = false) + val sid: String +) : BaseTimeEntity() \ No newline at end of file diff --git a/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/jpa/entity/NonMemberStoreUrlEntity.kt b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/jpa/entity/NonMemberStoreUrlEntity.kt new file mode 100644 index 0000000..75d4e00 --- /dev/null +++ b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/jpa/entity/NonMemberStoreUrlEntity.kt @@ -0,0 +1,18 @@ +package com.ping.infra.nonmember.domain.jpa.entity + +import com.ping.infra.nonmember.domain.jpa.common.BaseTimeEntity +import jakarta.persistence.* + +@Entity(name = "non_member_store_link") +class NonMemberStoreUrlEntity ( + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + val id: Long, + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(nullable = false) + val nonMember: NonMemberEntity, + + @Column(nullable = false) + val storeUrl: String +) : BaseTimeEntity() \ No newline at end of file diff --git a/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/jpa/entity/ShareUrlEntity.kt b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/jpa/entity/ShareUrlEntity.kt index 8f401b0..9642db9 100644 --- a/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/jpa/entity/ShareUrlEntity.kt +++ b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/jpa/entity/ShareUrlEntity.kt @@ -1,28 +1,24 @@ package com.ping.infra.nonmember.domain.jpa.entity -import com.ping.common.entity.BaseTimeEntity -import com.ping.domain.nonmember.aggregate.ShareUrl +import com.ping.infra.nonmember.domain.jpa.common.BaseTimeEntity import jakarta.persistence.* @Entity(name = "share_url") -@Table class ShareUrlEntity( - @Column(name = "url", nullable = false) + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + val id: Long, + + @Column(nullable = false) val url: String, - @Column(name = "event_name", nullable = false) + @Column(nullable = false) val eventName: String, - @Column(name = "neighborhood", nullable = false) + @Column(nullable = false) val neighborhood: String, -) : BaseTimeEntity() { - fun toDomain(): ShareUrl { - return ShareUrl( - id = this.id, - url = this.url, - eventName = this.eventName, - neighborhood = this.neighborhood - ) - } -} \ No newline at end of file + @Column(nullable = false) + val uuid: String + +) : BaseTimeEntity() \ No newline at end of file diff --git a/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/jpa/repository/NonMemberBookmarkUrlJpaRepository.kt b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/jpa/repository/NonMemberBookmarkUrlJpaRepository.kt new file mode 100644 index 0000000..a795c51 --- /dev/null +++ b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/jpa/repository/NonMemberBookmarkUrlJpaRepository.kt @@ -0,0 +1,7 @@ +package com.ping.infra.nonmember.domain.jpa.repository + +import com.ping.infra.nonmember.domain.jpa.entity.NonMemberBookmarkUrlEntity +import org.springframework.data.jpa.repository.JpaRepository + +interface NonMemberBookmarkUrlJpaRepository: JpaRepository{ +} \ No newline at end of file diff --git a/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/jpa/repository/NonMemberPlaceJpaRepository.kt b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/jpa/repository/NonMemberPlaceJpaRepository.kt new file mode 100644 index 0000000..0f00a26 --- /dev/null +++ b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/jpa/repository/NonMemberPlaceJpaRepository.kt @@ -0,0 +1,7 @@ +package com.ping.infra.nonmember.domain.jpa.repository + +import com.ping.infra.nonmember.domain.jpa.entity.NonMemberPlaceEntity +import org.springframework.data.jpa.repository.JpaRepository + +interface NonMemberPlaceJpaRepository : JpaRepository { +} \ No newline at end of file diff --git a/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/jpa/repository/NonMemberStoreUrlJpaRepository.kt b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/jpa/repository/NonMemberStoreUrlJpaRepository.kt new file mode 100644 index 0000000..d7119a2 --- /dev/null +++ b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/jpa/repository/NonMemberStoreUrlJpaRepository.kt @@ -0,0 +1,7 @@ +package com.ping.infra.nonmember.domain.jpa.repository + +import com.ping.infra.nonmember.domain.jpa.entity.NonMemberStoreUrlEntity +import org.springframework.data.jpa.repository.JpaRepository + +interface NonMemberStoreUrlJpaRepository : JpaRepository { +} \ No newline at end of file diff --git a/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/jpa/repository/ShareUrlJpaRepository.kt b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/jpa/repository/ShareUrlJpaRepository.kt index 4316f2a..6e06093 100644 --- a/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/jpa/repository/ShareUrlJpaRepository.kt +++ b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/jpa/repository/ShareUrlJpaRepository.kt @@ -3,4 +3,6 @@ package com.ping.infra.nonmember.domain.jpa.repository import com.ping.infra.nonmember.domain.jpa.entity.ShareUrlEntity import org.springframework.data.jpa.repository.JpaRepository -interface ShareUrlJpaRepository : JpaRepository \ No newline at end of file +interface ShareUrlJpaRepository : JpaRepository{ + fun findByUuid(uuid: String): ShareUrlEntity? +} \ No newline at end of file diff --git a/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/mapper/BookmarkMapper.kt b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/mapper/BookmarkMapper.kt new file mode 100644 index 0000000..71b2117 --- /dev/null +++ b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/mapper/BookmarkMapper.kt @@ -0,0 +1,25 @@ +package com.ping.infra.nonmember.domain.mapper + +import com.ping.domain.nonmember.aggregate.BookmarkDomain +import com.ping.infra.nonmember.domain.mongo.entity.BookmarkEntity + +object BookmarkMapper { + + fun toDomain(bookmarkEntity: BookmarkEntity) = BookmarkDomain( + bookmarkEntity.name, + bookmarkEntity.px, + bookmarkEntity.py, + bookmarkEntity.sid, + bookmarkEntity.address, + bookmarkEntity.mcidName + ) + + fun toEntity(bookmarkDomain: BookmarkDomain) = BookmarkEntity( + bookmarkDomain.name, + bookmarkDomain.px, + bookmarkDomain.py, + bookmarkDomain.sid, + bookmarkDomain.address, + bookmarkDomain.mcidName + ) +} \ No newline at end of file diff --git a/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/mapper/NonMemberBookmarkUrlMapper.kt b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/mapper/NonMemberBookmarkUrlMapper.kt new file mode 100644 index 0000000..b6ad3da --- /dev/null +++ b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/mapper/NonMemberBookmarkUrlMapper.kt @@ -0,0 +1,19 @@ +package com.ping.infra.nonmember.domain.mapper + +import com.ping.domain.nonmember.aggregate.NonMemberBookmarkUrlDomain +import com.ping.infra.nonmember.domain.jpa.entity.NonMemberBookmarkUrlEntity + +object NonMemberBookmarkUrlMapper { + + fun toDomain(nonMemberBookUrlEntity: NonMemberBookmarkUrlEntity) = NonMemberBookmarkUrlDomain( + nonMemberBookUrlEntity.id, + NonMemberMapper.toDomain(nonMemberBookUrlEntity.nonMember), + nonMemberBookUrlEntity.bookmarkUrl + ) + + fun toEntity(nonMemberBookUrlDomain: NonMemberBookmarkUrlDomain) = NonMemberBookmarkUrlEntity( + nonMemberBookUrlDomain.id, + NonMemberMapper.toEntity(nonMemberBookUrlDomain.nonMember), + nonMemberBookUrlDomain.bookmarkUrl + ) +} \ No newline at end of file diff --git a/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/mapper/NonMemberMapper.kt b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/mapper/NonMemberMapper.kt index aec994b..5dccf33 100644 --- a/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/mapper/NonMemberMapper.kt +++ b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/mapper/NonMemberMapper.kt @@ -1,26 +1,21 @@ package com.ping.infra.nonmember.domain.mapper -import com.ping.domain.nonmember.aggregate.NonMember +import com.ping.domain.nonmember.aggregate.NonMemberDomain import com.ping.infra.nonmember.domain.jpa.entity.NonMemberEntity -class NonMemberMapper { - companion object { +object NonMemberMapper { - fun toDomain(nonMemberEntity: NonMemberEntity): NonMember { - return NonMember( - nonMemberEntity.id, - nonMemberEntity.name, - nonMemberEntity.password, - nonMemberEntity.shareUrl.toDomain() - ) - } + fun toDomain(nonMemberEntity: NonMemberEntity) = NonMemberDomain( + nonMemberEntity.id, + nonMemberEntity.name, + nonMemberEntity.password, + ShareUrlMapper.toDomain(nonMemberEntity.shareUrl) + ) - fun toEntity(nonMember: NonMember): NonMemberEntity { - return NonMemberEntity( - nonMember.name, - nonMember.password, - ShareUrlMapper.toEntity(nonMember.shareUrl) - ) - } - } + fun toEntity(nonMemberDomain: NonMemberDomain) = NonMemberEntity( + nonMemberDomain.id, + nonMemberDomain.name, + nonMemberDomain.password, + ShareUrlMapper.toEntity(nonMemberDomain.shareUrlDomain) + ) } \ No newline at end of file diff --git a/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/mapper/NonMemberPlaceMapper.kt b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/mapper/NonMemberPlaceMapper.kt new file mode 100644 index 0000000..af3a480 --- /dev/null +++ b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/mapper/NonMemberPlaceMapper.kt @@ -0,0 +1,20 @@ +package com.ping.infra.nonmember.domain.mapper + +import com.ping.domain.nonmember.aggregate.NonMemberPlaceDomain +import com.ping.infra.nonmember.domain.jpa.entity.NonMemberPlaceEntity + +object NonMemberPlaceMapper { + + fun toDomain(nonMemberPlaceEntity: NonMemberPlaceEntity) = NonMemberPlaceDomain( + nonMemberPlaceEntity.id, + NonMemberMapper.toDomain(nonMemberPlaceEntity.nonMember), + nonMemberPlaceEntity.sid + ) + + + fun toEntity(nonMemberPlaceDomain: NonMemberPlaceDomain) = NonMemberPlaceEntity( + nonMemberPlaceDomain.id, + NonMemberMapper.toEntity(nonMemberPlaceDomain.nonMember), + nonMemberPlaceDomain.sid + ) +} \ No newline at end of file diff --git a/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/mapper/NonMemberStoreUrlMapper.kt b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/mapper/NonMemberStoreUrlMapper.kt new file mode 100644 index 0000000..fea2bfc --- /dev/null +++ b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/mapper/NonMemberStoreUrlMapper.kt @@ -0,0 +1,19 @@ +package com.ping.infra.nonmember.domain.mapper + +import com.ping.domain.nonmember.aggregate.NonMemberStoreUrlDomain +import com.ping.infra.nonmember.domain.jpa.entity.NonMemberStoreUrlEntity + +object NonMemberStoreUrlMapper { + + fun toDomain(nonMemberStoreUrlEntity: NonMemberStoreUrlEntity) = NonMemberStoreUrlDomain( + nonMemberStoreUrlEntity.id, + NonMemberMapper.toDomain(nonMemberStoreUrlEntity.nonMember), + nonMemberStoreUrlEntity.storeUrl + ) + + fun toEntity(nonMemberStoreUrlDomain: NonMemberStoreUrlDomain) = NonMemberStoreUrlEntity( + nonMemberStoreUrlDomain.id, + NonMemberMapper.toEntity(nonMemberStoreUrlDomain.nonMember), + nonMemberStoreUrlDomain.storeUrl + ) +} \ No newline at end of file diff --git a/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/mapper/ShareUrlMapper.kt b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/mapper/ShareUrlMapper.kt index 4a8a431..51ba854 100644 --- a/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/mapper/ShareUrlMapper.kt +++ b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/mapper/ShareUrlMapper.kt @@ -1,24 +1,23 @@ package com.ping.infra.nonmember.domain.mapper -import com.ping.domain.nonmember.aggregate.ShareUrl +import com.ping.domain.nonmember.aggregate.ShareUrlDomain import com.ping.infra.nonmember.domain.jpa.entity.ShareUrlEntity -class ShareUrlMapper { - companion object { - fun toEntity(shareUrl: ShareUrl): ShareUrlEntity { - return ShareUrlEntity( - shareUrl.url, - shareUrl.eventName, - shareUrl.neighborhood - ) - } - fun toDomain(shareUrlEntity: ShareUrlEntity): ShareUrl { - return ShareUrl( - shareUrlEntity.id, - shareUrlEntity.url, - shareUrlEntity.eventName, - shareUrlEntity.neighborhood - ) - } - } +object ShareUrlMapper { + + fun toEntity(shareUrlDomain: ShareUrlDomain) = ShareUrlEntity( + shareUrlDomain.id, + shareUrlDomain.url, + shareUrlDomain.eventName, + shareUrlDomain.neighborhood, + shareUrlDomain.uuid + ) + + fun toDomain(shareUrlEntity: ShareUrlEntity) = ShareUrlDomain( + shareUrlEntity.id, + shareUrlEntity.url, + shareUrlEntity.eventName, + shareUrlEntity.neighborhood, + shareUrlEntity.uuid + ) } \ No newline at end of file diff --git a/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/mongo/common/BaseTimeMongoEntity.kt b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/mongo/common/BaseTimeMongoEntity.kt new file mode 100644 index 0000000..1ebdbc5 --- /dev/null +++ b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/mongo/common/BaseTimeMongoEntity.kt @@ -0,0 +1,18 @@ +package com.ping.infra.nonmember.domain.mongo.common + +import org.springframework.data.annotation.CreatedDate +import org.springframework.data.annotation.LastModifiedDate +import org.springframework.data.mongodb.core.mapping.Document +import java.time.LocalDateTime + +@Document +abstract class BaseTimeMongoEntity { + + @CreatedDate + var createdAt: LocalDateTime? = null + protected set + + @LastModifiedDate + var updatedAt: LocalDateTime? = null + protected set +} \ No newline at end of file diff --git a/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/mongo/common/MongoConfig.kt b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/mongo/common/MongoConfig.kt new file mode 100644 index 0000000..95199f3 --- /dev/null +++ b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/mongo/common/MongoConfig.kt @@ -0,0 +1,8 @@ +package com.ping.infra.nonmember.domain.mongo.common + +import org.springframework.context.annotation.Configuration +import org.springframework.data.mongodb.config.EnableMongoAuditing + +@Configuration +@EnableMongoAuditing +class MongoConfig \ No newline at end of file diff --git a/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/mongo/entity/BookmarkEntity.kt b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/mongo/entity/BookmarkEntity.kt new file mode 100644 index 0000000..9ff3d3e --- /dev/null +++ b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/mongo/entity/BookmarkEntity.kt @@ -0,0 +1,18 @@ +package com.ping.infra.nonmember.domain.mongo.entity + +import com.ping.infra.nonmember.domain.mongo.common.BaseTimeMongoEntity +import org.springframework.data.annotation.Id +import org.springframework.data.mongodb.core.index.Indexed +import org.springframework.data.mongodb.core.mapping.Document + +@Document(collection = "bookmarks") +data class BookmarkEntity( + val name: String, + val px: Double, + val py: Double, + @Id + @Indexed(unique = true) + val sid: String, + val address: String, + val mcidName: String, +): BaseTimeMongoEntity() \ No newline at end of file diff --git a/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/mongo/repository/BookmarkMongoRepository.kt b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/mongo/repository/BookmarkMongoRepository.kt new file mode 100644 index 0000000..796293b --- /dev/null +++ b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/mongo/repository/BookmarkMongoRepository.kt @@ -0,0 +1,7 @@ +package com.ping.infra.nonmember.domain.mongo.repository + +import com.ping.infra.nonmember.domain.mongo.entity.BookmarkEntity +import org.springframework.data.mongodb.repository.MongoRepository + +interface BookmarkMongoRepository : MongoRepository { +} \ No newline at end of file diff --git a/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/repositoryImpl/BookmarkRepositoryImpl.kt b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/repositoryImpl/BookmarkRepositoryImpl.kt new file mode 100644 index 0000000..c2cc242 --- /dev/null +++ b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/repositoryImpl/BookmarkRepositoryImpl.kt @@ -0,0 +1,19 @@ +package com.ping.infra.nonmember.domain.repositoryImpl + +import com.ping.domain.nonmember.aggregate.BookmarkDomain +import com.ping.domain.nonmember.repository.BookmarkRepository +import com.ping.infra.nonmember.domain.mapper.BookmarkMapper +import com.ping.infra.nonmember.domain.mongo.repository.BookmarkMongoRepository +import org.springframework.stereotype.Repository + +@Repository +class BookmarkRepositoryImpl ( + private val bookmarkMongoRepository: BookmarkMongoRepository +) : BookmarkRepository { + override fun saveAll(bookmarkDomains: List): List { + return bookmarkMongoRepository.saveAll(bookmarkDomains.map { BookmarkMapper.toEntity(it) }).map { + BookmarkMapper.toDomain(it) + } + } + +} \ No newline at end of file diff --git a/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/repositoryImpl/NonMemberBookmarkUrlRepositoryImpl.kt b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/repositoryImpl/NonMemberBookmarkUrlRepositoryImpl.kt new file mode 100644 index 0000000..132ebf7 --- /dev/null +++ b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/repositoryImpl/NonMemberBookmarkUrlRepositoryImpl.kt @@ -0,0 +1,18 @@ +package com.ping.infra.nonmember.domain.repositoryImpl + +import com.ping.domain.nonmember.aggregate.NonMemberBookmarkUrlDomain +import com.ping.domain.nonmember.repository.NonMemberBookmarkUrlRepository +import com.ping.infra.nonmember.domain.jpa.repository.NonMemberBookmarkUrlJpaRepository +import com.ping.infra.nonmember.domain.mapper.NonMemberBookmarkUrlMapper +import org.springframework.stereotype.Repository + +@Repository +class NonMemberBookmarkUrlRepositoryImpl( + private val nonMemberBookmarkUrlJpaRepository: NonMemberBookmarkUrlJpaRepository +) : NonMemberBookmarkUrlRepository { + override fun saveAll(nonMemberBookmarkUrlDomains: List): List { + return nonMemberBookmarkUrlJpaRepository.saveAll(nonMemberBookmarkUrlDomains.map { + NonMemberBookmarkUrlMapper.toEntity(it) + }).map { NonMemberBookmarkUrlMapper.toDomain(it) } + } +} \ No newline at end of file diff --git a/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/repositoryImpl/NonMemberPlaceRepositoryImpl.kt b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/repositoryImpl/NonMemberPlaceRepositoryImpl.kt new file mode 100644 index 0000000..0d3947b --- /dev/null +++ b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/repositoryImpl/NonMemberPlaceRepositoryImpl.kt @@ -0,0 +1,17 @@ +package com.ping.infra.nonmember.domain.repositoryImpl + +import com.ping.domain.nonmember.aggregate.NonMemberPlaceDomain +import com.ping.domain.nonmember.repository.NonMemberPlaceRepository +import com.ping.infra.nonmember.domain.jpa.repository.NonMemberPlaceJpaRepository +import com.ping.infra.nonmember.domain.mapper.NonMemberPlaceMapper +import org.springframework.stereotype.Repository + +@Repository +class NonMemberPlaceRepositoryImpl( + private val nonMemberPlaceJpaRepository: NonMemberPlaceJpaRepository +) : NonMemberPlaceRepository { + override fun saveAll(nonMemberPlaceDomains: List): List { + return nonMemberPlaceJpaRepository.saveAll(nonMemberPlaceDomains.map { NonMemberPlaceMapper.toEntity(it) }) + .map { NonMemberPlaceMapper.toDomain(it) } + } +} \ No newline at end of file diff --git a/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/repositoryImpl/NonMemberRepositoryImpl.kt b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/repositoryImpl/NonMemberRepositoryImpl.kt index ab14953..32e4559 100644 --- a/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/repositoryImpl/NonMemberRepositoryImpl.kt +++ b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/repositoryImpl/NonMemberRepositoryImpl.kt @@ -1,6 +1,6 @@ package com.ping.infra.nonmember.domain.repositoryImpl -import com.ping.domain.nonmember.aggregate.NonMember +import com.ping.domain.nonmember.aggregate.NonMemberDomain import com.ping.domain.nonmember.repository.NonMemberRepository import com.ping.infra.nonmember.domain.jpa.repository.NonMemberJpaRepository import com.ping.infra.nonmember.domain.mapper.NonMemberMapper @@ -10,14 +10,13 @@ import org.springframework.stereotype.Repository class NonMemberRepositoryImpl( private val nonMemberJpaRepository: NonMemberJpaRepository, ) : NonMemberRepository { - override fun findByShareUrlIdAndName(shareUrlId: Long, name: String): NonMember? { + override fun findByShareUrlIdAndName(shareUrlId: Long, name: String): NonMemberDomain? { return nonMemberJpaRepository.findByShareUrlIdAndName(shareUrlId, name)?.let { NonMemberMapper.toDomain(it) } } - override fun save(nonMember: NonMember): NonMember { - val nonMemberEntity = NonMemberMapper.toEntity(nonMember) - return NonMemberMapper.toDomain(nonMemberEntity) + override fun save(nonMemberDomain: NonMemberDomain): NonMemberDomain { + return NonMemberMapper.toDomain(nonMemberJpaRepository.save(NonMemberMapper.toEntity(nonMemberDomain))) } } \ No newline at end of file diff --git a/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/repositoryImpl/NonMemberStoreUrlRepositoryImpl.kt b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/repositoryImpl/NonMemberStoreUrlRepositoryImpl.kt new file mode 100644 index 0000000..dc489bc --- /dev/null +++ b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/repositoryImpl/NonMemberStoreUrlRepositoryImpl.kt @@ -0,0 +1,17 @@ +package com.ping.infra.nonmember.domain.repositoryImpl + +import com.ping.domain.nonmember.aggregate.NonMemberStoreUrlDomain +import com.ping.domain.nonmember.repository.NonMemberStoreUrlRepository +import com.ping.infra.nonmember.domain.jpa.repository.NonMemberStoreUrlJpaRepository +import com.ping.infra.nonmember.domain.mapper.NonMemberStoreUrlMapper +import org.springframework.stereotype.Repository + +@Repository +class NonMemberStoreUrlRepositoryImpl( + private val nonMemberStoreUrlJpaRepository: NonMemberStoreUrlJpaRepository +) : NonMemberStoreUrlRepository { + override fun saveALl(nonMemberStoreUrlDomains: List): List { + return nonMemberStoreUrlJpaRepository.saveAll(nonMemberStoreUrlDomains.map { NonMemberStoreUrlMapper.toEntity(it) }) + .map { NonMemberStoreUrlMapper.toDomain(it) } + } +} \ No newline at end of file diff --git a/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/repositoryImpl/ShareUrlRepositoryImpl.kt b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/repositoryImpl/ShareUrlRepositoryImpl.kt index c83a1ff..417ea07 100644 --- a/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/repositoryImpl/ShareUrlRepositoryImpl.kt +++ b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/repositoryImpl/ShareUrlRepositoryImpl.kt @@ -1,6 +1,6 @@ package com.ping.infra.nonmember.domain.repositoryImpl -import com.ping.domain.nonmember.aggregate.ShareUrl +import com.ping.domain.nonmember.aggregate.ShareUrlDomain import com.ping.domain.nonmember.repository.ShareUrlRepository import com.ping.infra.nonmember.domain.jpa.repository.ShareUrlJpaRepository import com.ping.infra.nonmember.domain.mapper.ShareUrlMapper @@ -11,8 +11,8 @@ import org.springframework.stereotype.Repository class ShareUrlRepositoryImpl( private val shareUrlJpaRepository: ShareUrlJpaRepository ) : ShareUrlRepository { - override fun findById(id: Long): ShareUrl? { - return shareUrlJpaRepository.findByIdOrNull(id)?.let { + override fun findByUuid(uuid: String): ShareUrlDomain? { + return shareUrlJpaRepository.findByUuid(uuid)?.let { ShareUrlMapper.toDomain(it) } } diff --git a/settings.gradle.kts b/settings.gradle.kts index 8396792..948b421 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -7,3 +7,4 @@ include("Ping-Application") include("Ping-Common") include("Ping-Domain") include("Ping-Infra") +include("Ping-Client") From ac3d90cab30d7e1fcf70a882c43f90b6203f1b36 Mon Sep 17 00:00:00 2001 From: sominyun Date: Sun, 20 Oct 2024 18:28:49 +0900 Subject: [PATCH 011/203] fix: unused import --- .../kotlin/com/ping/application/nonmember/NonMemberService.kt | 3 +-- .../nonmember/domain/repositoryImpl/ShareUrlRepositoryImpl.kt | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberService.kt b/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberService.kt index 3f2ea58..d1ec176 100644 --- a/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberService.kt +++ b/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberService.kt @@ -2,12 +2,11 @@ package com.ping.application.nonmember import com.ping.application.nonmember.dto.request.NonMemberCreateRequest import com.ping.client.navermap.NaverMapClient -import com.ping.common.util.UrlUtil import com.ping.common.exception.CustomException import com.ping.common.exception.ExceptionContent +import com.ping.common.util.UrlUtil import com.ping.domain.nonmember.aggregate.* import com.ping.domain.nonmember.repository.* -import org.springframework.dao.DataIntegrityViolationException import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional diff --git a/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/repositoryImpl/ShareUrlRepositoryImpl.kt b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/repositoryImpl/ShareUrlRepositoryImpl.kt index 417ea07..ecf06fc 100644 --- a/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/repositoryImpl/ShareUrlRepositoryImpl.kt +++ b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/repositoryImpl/ShareUrlRepositoryImpl.kt @@ -4,7 +4,6 @@ import com.ping.domain.nonmember.aggregate.ShareUrlDomain import com.ping.domain.nonmember.repository.ShareUrlRepository import com.ping.infra.nonmember.domain.jpa.repository.ShareUrlJpaRepository import com.ping.infra.nonmember.domain.mapper.ShareUrlMapper -import org.springframework.data.repository.findByIdOrNull import org.springframework.stereotype.Repository @Repository From 89812fc733758a584f0e59a6051eac70844b4e78 Mon Sep 17 00:00:00 2001 From: sominyun Date: Sun, 20 Oct 2024 18:29:16 +0900 Subject: [PATCH 012/203] =?UTF-8?q?feat:=20=EB=84=A4=EC=9D=B4=EB=B2=84=20?= =?UTF-8?q?=EC=A7=80=EB=8F=84=20=ED=81=AC=EB=A1=A4=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../client/navermap/NaverBookmarkResponse.kt | 16 +++++ .../ping/client/navermap/NaverMapClient.kt | 65 +++++++++++++++++++ 2 files changed, 81 insertions(+) create mode 100644 Ping-Client/src/main/kotlin/com/ping/client/navermap/NaverBookmarkResponse.kt create mode 100644 Ping-Client/src/main/kotlin/com/ping/client/navermap/NaverMapClient.kt diff --git a/Ping-Client/src/main/kotlin/com/ping/client/navermap/NaverBookmarkResponse.kt b/Ping-Client/src/main/kotlin/com/ping/client/navermap/NaverBookmarkResponse.kt new file mode 100644 index 0000000..7678988 --- /dev/null +++ b/Ping-Client/src/main/kotlin/com/ping/client/navermap/NaverBookmarkResponse.kt @@ -0,0 +1,16 @@ +package com.ping.client.navermap + +class NaverBookmarkResponse { + data class BookmarkLists( + val bookmarkList: List + ) + + data class Bookmark( + val name: String, + val px: Double, + val py: Double, + val sid: String, + val address: String, + val mcidName: String, + ) +} \ No newline at end of file diff --git a/Ping-Client/src/main/kotlin/com/ping/client/navermap/NaverMapClient.kt b/Ping-Client/src/main/kotlin/com/ping/client/navermap/NaverMapClient.kt new file mode 100644 index 0000000..f247659 --- /dev/null +++ b/Ping-Client/src/main/kotlin/com/ping/client/navermap/NaverMapClient.kt @@ -0,0 +1,65 @@ +package com.ping.client.navermap + +import com.fasterxml.jackson.databind.ObjectMapper +import com.ping.common.exception.CustomException +import com.ping.common.exception.ExceptionContent +import org.jsoup.Jsoup +import org.springframework.stereotype.Component +import org.springframework.web.reactive.function.client.WebClient + +@Component +class NaverMapClient( + private val objectMapper: ObjectMapper +) { + + fun bookmarkUrlToBookmarkLists(url: String): NaverBookmarkResponse.BookmarkLists { + try { + val shareId = url.split("/").lastOrNull()?.split("?")?.firstOrNull()?.replace("%", "") + val webclient = WebClient.create("https://pages.map.naver.com") + return webclient.get() + .uri("/save-pages/api/maps-bookmark/v3/shares/${shareId}/bookmarks") + .retrieve() + .bodyToMono(String::class.java) + .map { + objectMapper.readValue(it, NaverBookmarkResponse.BookmarkLists::class.java) + } + .block()!! + } catch (e: Exception) { + throw CustomException(ExceptionContent.INVALID_BOOKMARK_URL) + } + } + + fun storeUrlToBookmark(url: String): NaverBookmarkResponse.Bookmark { + try { + val shareId = url.split("place/").lastOrNull()?.split("/home")?.firstOrNull()?.split("?")?.firstOrNull() + ?.replace("%", "")!! + val crawlingUrl = "https://pcmap.place.naver.com/restaurant/${shareId}/information" + + val randomUserAgent = + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Gecko/20100101 Firefox/85.0"//Random.nextInt(userAgents.size)] + + val document = Jsoup.connect(crawlingUrl) + .userAgent(randomUserAgent) + .referrer("https://www.naver.com") + .get() + + val place = Regex("\"x\":\"([0-9.]+)\",\"y\":\"([0-9.]+)\"").find(document.html())!! + val roadAddress = Regex("\"roadAddress\":\"([^\"]+)\"").find(document.html()) + + val placeName = document.selectFirst(".GHAhO")?.text()!! + val px = place.groupValues[1] + val py = place.groupValues[2] + val address = roadAddress?.groups?.get(1)?.value!! + return NaverBookmarkResponse.Bookmark( + name = placeName, + px = px.toDouble(), + py = py.toDouble(), + sid = shareId, + address = address, + mcidName = "" + ) + } catch (e: Exception) { + throw CustomException(ExceptionContent.INVALID_STORE_URL) + } + } +} \ No newline at end of file From a491b67223646f2871222b930c75e42131a36b3c Mon Sep 17 00:00:00 2001 From: sominyun Date: Mon, 21 Oct 2024 13:49:30 +0900 Subject: [PATCH 013/203] =?UTF-8?q?feat:=20=EC=9D=B4=EB=A6=84=20=EA=B3=B5?= =?UTF-8?q?=EB=B0=B1,=20=ED=8A=B9=EC=88=98=EB=AC=B8=EC=9E=90,=20=EC=88=AB?= =?UTF-8?q?=EC=9E=90=20=EB=B6=88=EA=B0=80=EB=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/ping/application/nonmember/NonMemberService.kt | 9 +++++++++ .../kotlin/com/ping/common/exception/ExceptionContent.kt | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberService.kt b/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberService.kt index d1ec176..cb32cb9 100644 --- a/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberService.kt +++ b/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberService.kt @@ -24,6 +24,8 @@ class NonMemberService( @Transactional fun createNonMemberPings(request: NonMemberCreateRequest) { + //이름 공백, 특수문자, 숫자 불가 + validateName(request.name) // 비밀번호 형식 검사 (4자리 숫자) validatePassword(request.password) @@ -88,6 +90,13 @@ class NonMemberService( bookmarkRepository.saveAll(bookmarks) nonMemberPlaceRepository.saveAll(nonMemberPlaces) } + // 이름 우효성 검증 로직 + private fun validateName(name: String) { + val namePattern = "^[가-힣a-zA-Z]{1,6}\$".toRegex() + if (!namePattern.matches(name)) { + throw CustomException(ExceptionContent.INVALID_NAME_FORMAT) + } + } // 비밀번호 유효성 검증 로직 private fun validatePassword(password: String) { diff --git a/Ping-Common/src/main/kotlin/com/ping/common/exception/ExceptionContent.kt b/Ping-Common/src/main/kotlin/com/ping/common/exception/ExceptionContent.kt index 6116a2e..1c7b178 100644 --- a/Ping-Common/src/main/kotlin/com/ping/common/exception/ExceptionContent.kt +++ b/Ping-Common/src/main/kotlin/com/ping/common/exception/ExceptionContent.kt @@ -15,7 +15,8 @@ enum class ExceptionContent( NON_MEMBER_ALREADY_EXISTS(HttpStatus.CONFLICT, "NONMEMBER",1,"비회원 생성 실패: 이미 존재하는 비회원입니다."), NON_MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "NONMEMBER",2,"비회원 로그인 실패: 해당 비회원 정보를 찾을 수 없습니다."), NON_MEMBER_LOGIN_FAILED(HttpStatus.UNAUTHORIZED, "NONMEMBER",3,"비회원 로그인 실패: 비밀번호가 일치하지 않습니다."), - INVALID_PASSWORD_FORMAT(HttpStatus.BAD_REQUEST, "NONMEMBER",4,"비밀번호는 4자리 숫자여야 합니다."), + INVALID_NAME_FORMAT(HttpStatus.BAD_REQUEST, "NONMEMBER",4,"이름은 공백, 특수문자, 숫자를 포함할 수 없으며, 6글자 이하여야 합니다."), + INVALID_PASSWORD_FORMAT(HttpStatus.BAD_REQUEST, "NONMEMBER",5,"비밀번호는 4자리 숫자여야 합니다."), // ShareUrl 관련 예외 INVALID_SHARE_URL(HttpStatus.NOT_FOUND, "SHARE URL",1,"유효하지 않은 공유 URL입니다."), From 0b4570e15334da44665cf6e41cbaf299e7f99c1b Mon Sep 17 00:00:00 2001 From: sominyun Date: Mon, 21 Oct 2024 16:44:33 +0900 Subject: [PATCH 014/203] =?UTF-8?q?feat:=20dto=20request=20=EC=95=88?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=EA=B5=AC=ED=98=84=ED=95=98=EB=8A=94?= =?UTF-8?q?=EA=B2=83=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ping/application/nonmember/NonMemberService.kt | 5 +++-- .../ping/application/nonmember/dto/CreateNonMember.kt | 11 +++++++++++ .../nonmember/dto/request/NonMemberCreateRequest.kt | 9 --------- 3 files changed, 14 insertions(+), 11 deletions(-) create mode 100644 Ping-Application/src/main/kotlin/com/ping/application/nonmember/dto/CreateNonMember.kt delete mode 100644 Ping-Application/src/main/kotlin/com/ping/application/nonmember/dto/request/NonMemberCreateRequest.kt diff --git a/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberService.kt b/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberService.kt index cb32cb9..24b0c21 100644 --- a/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberService.kt +++ b/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberService.kt @@ -1,6 +1,7 @@ package com.ping.application.nonmember -import com.ping.application.nonmember.dto.request.NonMemberCreateRequest +import com.ping.application.nonmember.dto.CreateNonMember +import com.ping.application.nonmember.dto.GetAllNonMemberPings import com.ping.client.navermap.NaverMapClient import com.ping.common.exception.CustomException import com.ping.common.exception.ExceptionContent @@ -23,7 +24,7 @@ class NonMemberService( ) { @Transactional - fun createNonMemberPings(request: NonMemberCreateRequest) { + fun createNonMemberPings(request: CreateNonMember.Request) { //이름 공백, 특수문자, 숫자 불가 validateName(request.name) // 비밀번호 형식 검사 (4자리 숫자) diff --git a/Ping-Application/src/main/kotlin/com/ping/application/nonmember/dto/CreateNonMember.kt b/Ping-Application/src/main/kotlin/com/ping/application/nonmember/dto/CreateNonMember.kt new file mode 100644 index 0000000..1f5a270 --- /dev/null +++ b/Ping-Application/src/main/kotlin/com/ping/application/nonmember/dto/CreateNonMember.kt @@ -0,0 +1,11 @@ +package com.ping.application.nonmember.dto + +class CreateNonMember { + data class Request( + val uuid: String, + val name: String, + val password: String, + val bookmarkUrls: List, + val storeUrls: List + ) +} \ No newline at end of file diff --git a/Ping-Application/src/main/kotlin/com/ping/application/nonmember/dto/request/NonMemberCreateRequest.kt b/Ping-Application/src/main/kotlin/com/ping/application/nonmember/dto/request/NonMemberCreateRequest.kt deleted file mode 100644 index 8eb38c4..0000000 --- a/Ping-Application/src/main/kotlin/com/ping/application/nonmember/dto/request/NonMemberCreateRequest.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.ping.application.nonmember.dto.request - -data class NonMemberCreateRequest( - val uuid: String, - val name: String, - val password: String, - val bookmarkUrls: List, - val storeUrls: List -) \ No newline at end of file From 721778b612732cc20cc842b7431b35589e82dfec Mon Sep 17 00:00:00 2001 From: sominyun Date: Mon, 21 Oct 2024 16:51:39 +0900 Subject: [PATCH 015/203] =?UTF-8?q?feat:=20bookmark=EC=97=90=20url=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/ping/application/nonmember/NonMemberService.kt | 6 ++++-- .../com/ping/domain/nonmember/aggregate/BookmarkDomain.kt | 1 + .../ping/infra/nonmember/domain/mapper/BookmarkMapper.kt | 6 ++++-- .../infra/nonmember/domain/mongo/entity/BookmarkEntity.kt | 1 + 4 files changed, 10 insertions(+), 4 deletions(-) diff --git a/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberService.kt b/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberService.kt index 24b0c21..1705dea 100644 --- a/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberService.kt +++ b/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberService.kt @@ -66,7 +66,8 @@ class NonMemberService( py = bookmark.py, sid = bookmark.sid, address = bookmark.address, - mcidName = bookmark.mcidName + mcidName = bookmark.mcidName, + url = it ) } }) @@ -85,7 +86,8 @@ class NonMemberService( py = bookmark.py, sid = bookmark.sid, address = bookmark.address, - mcidName = bookmark.mcidName + mcidName = bookmark.mcidName, + url = it ) }) bookmarkRepository.saveAll(bookmarks) diff --git a/Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/aggregate/BookmarkDomain.kt b/Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/aggregate/BookmarkDomain.kt index c3b90c4..b862836 100644 --- a/Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/aggregate/BookmarkDomain.kt +++ b/Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/aggregate/BookmarkDomain.kt @@ -7,4 +7,5 @@ data class BookmarkDomain ( val sid: String, val address: String, val mcidName: String, + val url: String ) \ No newline at end of file diff --git a/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/mapper/BookmarkMapper.kt b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/mapper/BookmarkMapper.kt index 71b2117..1f19acc 100644 --- a/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/mapper/BookmarkMapper.kt +++ b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/mapper/BookmarkMapper.kt @@ -11,7 +11,8 @@ object BookmarkMapper { bookmarkEntity.py, bookmarkEntity.sid, bookmarkEntity.address, - bookmarkEntity.mcidName + bookmarkEntity.mcidName, + bookmarkEntity.url ) fun toEntity(bookmarkDomain: BookmarkDomain) = BookmarkEntity( @@ -20,6 +21,7 @@ object BookmarkMapper { bookmarkDomain.py, bookmarkDomain.sid, bookmarkDomain.address, - bookmarkDomain.mcidName + bookmarkDomain.mcidName, + bookmarkDomain.url ) } \ No newline at end of file diff --git a/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/mongo/entity/BookmarkEntity.kt b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/mongo/entity/BookmarkEntity.kt index 9ff3d3e..7814e74 100644 --- a/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/mongo/entity/BookmarkEntity.kt +++ b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/mongo/entity/BookmarkEntity.kt @@ -15,4 +15,5 @@ data class BookmarkEntity( val sid: String, val address: String, val mcidName: String, + val url: String ): BaseTimeMongoEntity() \ No newline at end of file From 99e17576c94a30c48042ca648bbf5390df206d36 Mon Sep 17 00:00:00 2001 From: sominyun Date: Tue, 22 Oct 2024 00:24:51 +0900 Subject: [PATCH 016/203] =?UTF-8?q?fix:=20=ED=85=8C=EC=9D=B4=EB=B8=94?= =?UTF-8?q?=EB=AA=85=20link->url?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../nonmember/domain/jpa/entity/NonMemberBookmarkUrlEntity.kt | 2 +- .../nonmember/domain/jpa/entity/NonMemberStoreUrlEntity.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/jpa/entity/NonMemberBookmarkUrlEntity.kt b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/jpa/entity/NonMemberBookmarkUrlEntity.kt index a02de25..98cc709 100644 --- a/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/jpa/entity/NonMemberBookmarkUrlEntity.kt +++ b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/jpa/entity/NonMemberBookmarkUrlEntity.kt @@ -3,7 +3,7 @@ package com.ping.infra.nonmember.domain.jpa.entity import com.ping.infra.nonmember.domain.jpa.common.BaseTimeEntity import jakarta.persistence.* -@Entity(name = "non_member_bookmark_link") +@Entity(name = "non_member_bookmark_url") class NonMemberBookmarkUrlEntity( @Id @GeneratedValue(strategy = GenerationType.IDENTITY) diff --git a/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/jpa/entity/NonMemberStoreUrlEntity.kt b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/jpa/entity/NonMemberStoreUrlEntity.kt index 75d4e00..6c94191 100644 --- a/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/jpa/entity/NonMemberStoreUrlEntity.kt +++ b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/jpa/entity/NonMemberStoreUrlEntity.kt @@ -3,7 +3,7 @@ package com.ping.infra.nonmember.domain.jpa.entity import com.ping.infra.nonmember.domain.jpa.common.BaseTimeEntity import jakarta.persistence.* -@Entity(name = "non_member_store_link") +@Entity(name = "non_member_store_url") class NonMemberStoreUrlEntity ( @Id @GeneratedValue(strategy = GenerationType.IDENTITY) From 9b134b5521961eb55fea5532aad249569a56fbaf Mon Sep 17 00:00:00 2001 From: sominyun Date: Tue, 22 Oct 2024 18:49:34 +0900 Subject: [PATCH 017/203] =?UTF-8?q?feat:#6=20ping=20=EC=A0=84=EC=B2=B4=20?= =?UTF-8?q?=EB=B6=88=EB=9F=AC=EC=98=A4=EA=B8=B0=20api?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ping/api/nonmember/NonMemberController.kt | 15 ++-- .../application/nonmember/NonMemberService.kt | 68 +++++++++++++++++++ .../nonmember/dto/GetAllNonMemberPings.kt | 23 +++++++ .../repository/BookmarkRepository.kt | 2 + .../repository/NonMemberPlaceRepository.kt | 2 + .../repository/NonMemberRepository.kt | 4 +- .../jpa/repository/NonMemberJpaRepository.kt | 2 + .../repository/NonMemberPlaceJpaRepository.kt | 1 + .../repository/BookmarkMongoRepository.kt | 3 +- .../repositoryImpl/BookmarkRepositoryImpl.kt | 6 +- .../NonMemberPlaceRepositoryImpl.kt | 4 ++ .../repositoryImpl/NonMemberRepositoryImpl.kt | 4 ++ 12 files changed, 125 insertions(+), 9 deletions(-) create mode 100644 Ping-Application/src/main/kotlin/com/ping/application/nonmember/dto/GetAllNonMemberPings.kt diff --git a/Ping-Api/src/main/kotlin/com/ping/api/nonmember/NonMemberController.kt b/Ping-Api/src/main/kotlin/com/ping/api/nonmember/NonMemberController.kt index c4ae8de..4c28b53 100644 --- a/Ping-Api/src/main/kotlin/com/ping/api/nonmember/NonMemberController.kt +++ b/Ping-Api/src/main/kotlin/com/ping/api/nonmember/NonMemberController.kt @@ -1,11 +1,9 @@ package com.ping.api.nonmember import com.ping.application.nonmember.NonMemberService -import com.ping.application.nonmember.dto.request.NonMemberCreateRequest -import org.springframework.web.bind.annotation.PostMapping -import org.springframework.web.bind.annotation.RequestBody -import org.springframework.web.bind.annotation.RequestMapping -import org.springframework.web.bind.annotation.RestController +import com.ping.application.nonmember.dto.CreateNonMember +import com.ping.application.nonmember.dto.GetAllNonMemberPings +import org.springframework.web.bind.annotation.* @RestController @RequestMapping("/nonmembers") @@ -14,7 +12,12 @@ class NonMemberController( ) { @PostMapping("/pings") - fun createNonMemberPings(@RequestBody nonMemberCreateRequest: NonMemberCreateRequest) { + fun createNonMemberPings(@RequestBody nonMemberCreateRequest: CreateNonMember.Request) { return nonMemberService.createNonMemberPings(nonMemberCreateRequest) } + + @GetMapping("/pings") + fun getNonMemberPings(@RequestParam uuid: String): GetAllNonMemberPings.Response { + return nonMemberService.getAllNonMemberPings(uuid) + } } \ No newline at end of file diff --git a/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberService.kt b/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberService.kt index 1705dea..7fab948 100644 --- a/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberService.kt +++ b/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberService.kt @@ -93,6 +93,7 @@ class NonMemberService( bookmarkRepository.saveAll(bookmarks) nonMemberPlaceRepository.saveAll(nonMemberPlaces) } + // 이름 우효성 검증 로직 private fun validateName(name: String) { val namePattern = "^[가-힣a-zA-Z]{1,6}\$".toRegex() @@ -107,4 +108,71 @@ class NonMemberService( throw CustomException(ExceptionContent.INVALID_PASSWORD_FORMAT) } } + + fun getAllNonMemberPings(uuid: String): GetAllNonMemberPings.Response { + val shareUrl = shareUrlRepository.findByUuid(uuid) + ?: throw CustomException(ExceptionContent.INVALID_SHARE_URL) + val nonMemberList = nonMemberRepository.findAllByShareUrl(shareUrl.id) + + val nonMembers = nonMemberList.map { nonMember -> + GetAllNonMemberPings.NonMember( + nonMemberId = nonMember.id, + name = nonMember.name + ) + } + + //list>> + val allNonMemberPlaces = nonMemberList.flatMap { nonMember -> + nonMemberPlaceRepository.findAllByNonMemberId(nonMember.id).map { place -> + place to nonMember + } + } + val bookmarks = bookmarkRepository.findAllBySidIn(allNonMemberPlaces.map { it.first.sid }.distinct()) + val bookmarkMap = bookmarks.associateBy { it.sid } + + //Map>>> + val nonMemberPlaces = allNonMemberPlaces + .groupBy { it.first.sid } + .mapNotNull { (sid, placeNonMemberPairs) -> + val bookmarkDomain = bookmarkMap[sid] + bookmarkDomain?.let { + it to placeNonMemberPairs.map { placeNonMemberPair -> + placeNonMemberPair.second + } + } + } + .sortedByDescending { it.second.size }.groupBy { it.second.size } + + val pings = nonMemberPlaces.entries.mapIndexed{ index, nonMemberPlace -> + val level = when { + nonMemberPlace.key == 1 -> 0 // 아무도 안겹친 sid (겹친 인원이 1인 경우) + index == 0 -> 4 // 가장 많이 겹친 sid + index == 1 -> 3 // 두 번째로 많이 겹친 sid + index == 2 -> 2 // 세 번째로 많이 겹친 sid + else -> 1 // 그 외의 겹친 sid + } + nonMemberPlace.value.map { bookmarkPair -> + GetAllNonMemberPings.Ping( + iconLevel = level, + nonMembers = bookmarkPair.second.map { + GetAllNonMemberPings.NonMember( + nonMemberId = it.id, + name = it.name + ) + }, + url = bookmarkPair.first.url, + placeName = bookmarkPair.first.name, + px = bookmarkPair.first.px, + py = bookmarkPair.first.py, + ) + } + }.flatten() + + + return GetAllNonMemberPings.Response( + eventName = shareUrl.eventName, + nonMembers = nonMembers, + pings = pings + ) + } } \ No newline at end of file diff --git a/Ping-Application/src/main/kotlin/com/ping/application/nonmember/dto/GetAllNonMemberPings.kt b/Ping-Application/src/main/kotlin/com/ping/application/nonmember/dto/GetAllNonMemberPings.kt new file mode 100644 index 0000000..d199327 --- /dev/null +++ b/Ping-Application/src/main/kotlin/com/ping/application/nonmember/dto/GetAllNonMemberPings.kt @@ -0,0 +1,23 @@ +package com.ping.application.nonmember.dto + +class GetAllNonMemberPings { + data class Response( + val eventName: String, + val nonMembers: List, + val pings: List + + ) + data class NonMember( + val nonMemberId: Long, + val name : String + ) + + data class Ping( + val iconLevel: Int, + val nonMembers: List, + val url: String, + val placeName: String, + val px: Double, + val py: Double + ) +} \ No newline at end of file diff --git a/Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/repository/BookmarkRepository.kt b/Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/repository/BookmarkRepository.kt index 38b4b02..c6af56b 100644 --- a/Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/repository/BookmarkRepository.kt +++ b/Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/repository/BookmarkRepository.kt @@ -4,4 +4,6 @@ import com.ping.domain.nonmember.aggregate.BookmarkDomain interface BookmarkRepository { fun saveAll(bookmarkDomains: List) : List + + fun findAllBySidIn(sids: List) : List } \ No newline at end of file diff --git a/Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/repository/NonMemberPlaceRepository.kt b/Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/repository/NonMemberPlaceRepository.kt index 70f1923..44726da 100644 --- a/Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/repository/NonMemberPlaceRepository.kt +++ b/Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/repository/NonMemberPlaceRepository.kt @@ -4,4 +4,6 @@ import com.ping.domain.nonmember.aggregate.NonMemberPlaceDomain interface NonMemberPlaceRepository { fun saveAll(nonMemberPlaceDomains: List): List + + fun findAllByNonMemberId(nonMemberId: Long): List } \ No newline at end of file diff --git a/Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/repository/NonMemberRepository.kt b/Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/repository/NonMemberRepository.kt index 324a11b..2bc37f1 100644 --- a/Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/repository/NonMemberRepository.kt +++ b/Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/repository/NonMemberRepository.kt @@ -3,7 +3,9 @@ package com.ping.domain.nonmember.repository import com.ping.domain.nonmember.aggregate.NonMemberDomain interface NonMemberRepository { - fun findByShareUrlIdAndName(shareUrlId: Long, name: String) : NonMemberDomain? + fun findByShareUrlIdAndName(shareUrlId: Long, name: String): NonMemberDomain? fun save(nonMemberDomain: NonMemberDomain): NonMemberDomain + + fun findAllByShareUrl(shareUrlId: Long): List } \ No newline at end of file diff --git a/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/jpa/repository/NonMemberJpaRepository.kt b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/jpa/repository/NonMemberJpaRepository.kt index 62155bb..78eff2e 100644 --- a/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/jpa/repository/NonMemberJpaRepository.kt +++ b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/jpa/repository/NonMemberJpaRepository.kt @@ -5,4 +5,6 @@ import org.springframework.data.jpa.repository.JpaRepository interface NonMemberJpaRepository : JpaRepository { fun findByShareUrlIdAndName(urlId: Long, name: String): NonMemberEntity? + + fun findAllByShareUrlId(shareUrlId: Long): List } \ No newline at end of file diff --git a/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/jpa/repository/NonMemberPlaceJpaRepository.kt b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/jpa/repository/NonMemberPlaceJpaRepository.kt index 0f00a26..7fa253a 100644 --- a/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/jpa/repository/NonMemberPlaceJpaRepository.kt +++ b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/jpa/repository/NonMemberPlaceJpaRepository.kt @@ -4,4 +4,5 @@ import com.ping.infra.nonmember.domain.jpa.entity.NonMemberPlaceEntity import org.springframework.data.jpa.repository.JpaRepository interface NonMemberPlaceJpaRepository : JpaRepository { + fun findAllByNonMemberId(nonMemberId: Long): List } \ No newline at end of file diff --git a/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/mongo/repository/BookmarkMongoRepository.kt b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/mongo/repository/BookmarkMongoRepository.kt index 796293b..60997c6 100644 --- a/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/mongo/repository/BookmarkMongoRepository.kt +++ b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/mongo/repository/BookmarkMongoRepository.kt @@ -3,5 +3,6 @@ package com.ping.infra.nonmember.domain.mongo.repository import com.ping.infra.nonmember.domain.mongo.entity.BookmarkEntity import org.springframework.data.mongodb.repository.MongoRepository -interface BookmarkMongoRepository : MongoRepository { +interface BookmarkMongoRepository : MongoRepository { + fun findAllBySidIn(sids: List) : List } \ No newline at end of file diff --git a/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/repositoryImpl/BookmarkRepositoryImpl.kt b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/repositoryImpl/BookmarkRepositoryImpl.kt index c2cc242..8ed1dbf 100644 --- a/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/repositoryImpl/BookmarkRepositoryImpl.kt +++ b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/repositoryImpl/BookmarkRepositoryImpl.kt @@ -7,7 +7,7 @@ import com.ping.infra.nonmember.domain.mongo.repository.BookmarkMongoRepository import org.springframework.stereotype.Repository @Repository -class BookmarkRepositoryImpl ( +class BookmarkRepositoryImpl( private val bookmarkMongoRepository: BookmarkMongoRepository ) : BookmarkRepository { override fun saveAll(bookmarkDomains: List): List { @@ -16,4 +16,8 @@ class BookmarkRepositoryImpl ( } } + override fun findAllBySidIn(sids: List): List { + return bookmarkMongoRepository.findAllBySidIn(sids).map { BookmarkMapper.toDomain(it) } + } + } \ No newline at end of file diff --git a/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/repositoryImpl/NonMemberPlaceRepositoryImpl.kt b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/repositoryImpl/NonMemberPlaceRepositoryImpl.kt index 0d3947b..bc9e860 100644 --- a/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/repositoryImpl/NonMemberPlaceRepositoryImpl.kt +++ b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/repositoryImpl/NonMemberPlaceRepositoryImpl.kt @@ -14,4 +14,8 @@ class NonMemberPlaceRepositoryImpl( return nonMemberPlaceJpaRepository.saveAll(nonMemberPlaceDomains.map { NonMemberPlaceMapper.toEntity(it) }) .map { NonMemberPlaceMapper.toDomain(it) } } + + override fun findAllByNonMemberId(nonMemberId: Long): List { + return nonMemberPlaceJpaRepository.findAllByNonMemberId(nonMemberId).map { NonMemberPlaceMapper.toDomain(it) } + } } \ No newline at end of file diff --git a/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/repositoryImpl/NonMemberRepositoryImpl.kt b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/repositoryImpl/NonMemberRepositoryImpl.kt index 32e4559..593299c 100644 --- a/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/repositoryImpl/NonMemberRepositoryImpl.kt +++ b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/repositoryImpl/NonMemberRepositoryImpl.kt @@ -19,4 +19,8 @@ class NonMemberRepositoryImpl( override fun save(nonMemberDomain: NonMemberDomain): NonMemberDomain { return NonMemberMapper.toDomain(nonMemberJpaRepository.save(NonMemberMapper.toEntity(nonMemberDomain))) } + + override fun findAllByShareUrl(shareUrlId: Long): List { + return nonMemberJpaRepository.findAllByShareUrlId(shareUrlId).map { NonMemberMapper.toDomain(it) } + } } \ No newline at end of file From fbe2ef162e9fe3f930ee77da61ebeedc3b065cbc Mon Sep 17 00:00:00 2001 From: sominyun Date: Tue, 22 Oct 2024 18:49:52 +0900 Subject: [PATCH 018/203] =?UTF-8?q?fix:#6=20uuid=20unique=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ping/infra/nonmember/domain/jpa/entity/ShareUrlEntity.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/jpa/entity/ShareUrlEntity.kt b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/jpa/entity/ShareUrlEntity.kt index 9642db9..5a6122e 100644 --- a/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/jpa/entity/ShareUrlEntity.kt +++ b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/jpa/entity/ShareUrlEntity.kt @@ -18,7 +18,7 @@ class ShareUrlEntity( @Column(nullable = false) val neighborhood: String, - @Column(nullable = false) + @Column(nullable = false, unique = true) val uuid: String ) : BaseTimeEntity() \ No newline at end of file From c4d4c45443891b484fc73545c4601b8f3738e285 Mon Sep 17 00:00:00 2001 From: sominyun Date: Tue, 22 Oct 2024 18:50:13 +0900 Subject: [PATCH 019/203] =?UTF-8?q?fix:#6=20url=20=EC=A3=BC=EC=86=8C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/com/ping/application/nonmember/NonMemberService.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberService.kt b/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberService.kt index 7fab948..7770d31 100644 --- a/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberService.kt +++ b/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberService.kt @@ -67,7 +67,7 @@ class NonMemberService( sid = bookmark.sid, address = bookmark.address, mcidName = bookmark.mcidName, - url = it + url = "https://map.naver.com/p/entry/place/${bookmark.sid}" ) } }) From 169836a13fba03389dd78388f38d03ee33369b56 Mon Sep 17 00:00:00 2001 From: codrin2 Date: Tue, 22 Oct 2024 21:03:57 +0900 Subject: [PATCH 020/203] =?UTF-8?q?feat(PlaceController):=20#5=20=EC=9E=A5?= =?UTF-8?q?=EC=86=8C=20=EA=B2=80=EC=83=89=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ping/api/nonmember/NonMemberController.kt | 6 +++--- .../com/ping/api/place/PlaceController.kt | 19 +++++++++++++++++++ .../application/nonmember/NonMemberService.kt | 1 - 3 files changed, 22 insertions(+), 4 deletions(-) create mode 100644 Ping-Api/src/main/kotlin/com/ping/api/place/PlaceController.kt diff --git a/Ping-Api/src/main/kotlin/com/ping/api/nonmember/NonMemberController.kt b/Ping-Api/src/main/kotlin/com/ping/api/nonmember/NonMemberController.kt index c4ae8de..ca028ec 100644 --- a/Ping-Api/src/main/kotlin/com/ping/api/nonmember/NonMemberController.kt +++ b/Ping-Api/src/main/kotlin/com/ping/api/nonmember/NonMemberController.kt @@ -1,7 +1,7 @@ package com.ping.api.nonmember import com.ping.application.nonmember.NonMemberService -import com.ping.application.nonmember.dto.request.NonMemberCreateRequest +import com.ping.application.nonmember.dto.CreateNonMember import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestMapping @@ -14,7 +14,7 @@ class NonMemberController( ) { @PostMapping("/pings") - fun createNonMemberPings(@RequestBody nonMemberCreateRequest: NonMemberCreateRequest) { - return nonMemberService.createNonMemberPings(nonMemberCreateRequest) + fun createNonMemberPings(@RequestBody request: CreateNonMember.Request) { + return nonMemberService.createNonMemberPings(request) } } \ No newline at end of file diff --git a/Ping-Api/src/main/kotlin/com/ping/api/place/PlaceController.kt b/Ping-Api/src/main/kotlin/com/ping/api/place/PlaceController.kt new file mode 100644 index 0000000..cb1a473 --- /dev/null +++ b/Ping-Api/src/main/kotlin/com/ping/api/place/PlaceController.kt @@ -0,0 +1,19 @@ +package com.ping.api.place + +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +@RestController +@RequestMapping("/places") +class PlaceController( + private val placeService: PlaceService +) { + + // 장소 검색 API + @PostMapping("/search") + fun searchPlace(@RequestBody request: PlaceRequestDto.SearchRequest) = + placeService.searchPlace(request) + +} \ No newline at end of file diff --git a/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberService.kt b/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberService.kt index 1705dea..445da57 100644 --- a/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberService.kt +++ b/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberService.kt @@ -1,7 +1,6 @@ package com.ping.application.nonmember import com.ping.application.nonmember.dto.CreateNonMember -import com.ping.application.nonmember.dto.GetAllNonMemberPings import com.ping.client.navermap.NaverMapClient import com.ping.common.exception.CustomException import com.ping.common.exception.ExceptionContent From 5925d0f148866d3516c0d22c5972b8a4487c699c Mon Sep 17 00:00:00 2001 From: codrin2 Date: Tue, 22 Oct 2024 21:16:43 +0900 Subject: [PATCH 021/203] =?UTF-8?q?feat(PlaceService):=20#5=20=EC=9E=A5?= =?UTF-8?q?=EC=86=8C=20=EA=B2=80=EC=83=89=20=EB=B0=8F=20=EC=A0=80=EC=9E=A5?= =?UTF-8?q?=20=EB=A1=9C=EC=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ping/application/place/PlaceService.kt | 38 +++++++++++++++++++ .../ping/application/place/dto/SavePlace.kt | 10 +++++ .../ping/application/place/dto/SearchPlace.kt | 16 ++++++++ 3 files changed, 64 insertions(+) create mode 100644 Ping-Application/src/main/kotlin/com/ping/application/place/PlaceService.kt create mode 100644 Ping-Application/src/main/kotlin/com/ping/application/place/dto/SavePlace.kt create mode 100644 Ping-Application/src/main/kotlin/com/ping/application/place/dto/SearchPlace.kt diff --git a/Ping-Application/src/main/kotlin/com/ping/application/place/PlaceService.kt b/Ping-Application/src/main/kotlin/com/ping/application/place/PlaceService.kt new file mode 100644 index 0000000..58aade1 --- /dev/null +++ b/Ping-Application/src/main/kotlin/com/ping/application/place/PlaceService.kt @@ -0,0 +1,38 @@ +package com.ping.application.place + +import com.ping.application.place.dto.PlaceRequestDto +import com.ping.application.place.dto.SavePlace +import com.ping.application.place.dto.SearchPlace +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Service +class PlaceService( + private val naverApiClient: NaverApiClient, + private val placeRepository: PlaceRepository +) { + + // 장소 검색 + fun searchPlace(request: SearchPlace.Request): List { + return naverApiClient.searchPlaces(request.keyword).map { + SearchPlace.Response( + name = it.name, + address = it.address, + latitude = it.latitude, + longitude = it.longitude + ) + } + } + + // 장소 저장 + @Transactional + fun savePlace(request: SavePlace.Request) { + val place = PlaceDomain( + name = request.name, + address = request.address, + latitude = request.latitude, + longitude = request.longitude + ) + placeRepository.save(place) + } +} \ No newline at end of file diff --git a/Ping-Application/src/main/kotlin/com/ping/application/place/dto/SavePlace.kt b/Ping-Application/src/main/kotlin/com/ping/application/place/dto/SavePlace.kt new file mode 100644 index 0000000..8a0f570 --- /dev/null +++ b/Ping-Application/src/main/kotlin/com/ping/application/place/dto/SavePlace.kt @@ -0,0 +1,10 @@ +package com.ping.application.place.dto + +class SavePlace { + data class Request ( + val name: String, + val address: String, + val latitude: Double, + val longitude: Double + ) +} \ No newline at end of file diff --git a/Ping-Application/src/main/kotlin/com/ping/application/place/dto/SearchPlace.kt b/Ping-Application/src/main/kotlin/com/ping/application/place/dto/SearchPlace.kt new file mode 100644 index 0000000..071dc85 --- /dev/null +++ b/Ping-Application/src/main/kotlin/com/ping/application/place/dto/SearchPlace.kt @@ -0,0 +1,16 @@ +package com.ping.application.place.dto + +class SearchPlace { + + data class Request( + val keyword: String + ) + + data class Response( + val name: String, + val address: String, + val latitude: Double, + val longitude: Double + ) + +} \ No newline at end of file From bc28af9da57ba9320f0311b2a9a523f4ed2fa165 Mon Sep 17 00:00:00 2001 From: codrin2 Date: Tue, 22 Oct 2024 21:18:30 +0900 Subject: [PATCH 022/203] =?UTF-8?q?feat:=20#5=20Place=20domain=20=EB=B0=8F?= =?UTF-8?q?=20repository?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/ping/domain/nonmember/aggregate/PlaceDomain.kt | 8 ++++++++ .../ping/domain/nonmember/repository/PlaceRepository.kt | 7 +++++++ 2 files changed, 15 insertions(+) create mode 100644 Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/aggregate/PlaceDomain.kt create mode 100644 Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/repository/PlaceRepository.kt diff --git a/Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/aggregate/PlaceDomain.kt b/Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/aggregate/PlaceDomain.kt new file mode 100644 index 0000000..a1fe4d3 --- /dev/null +++ b/Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/aggregate/PlaceDomain.kt @@ -0,0 +1,8 @@ +package com.ping.domain.nonmember.aggregate + +data class PlaceDomain( + val name: String, + val address: String, + val latitude: Double, + val longitude: Double +) \ No newline at end of file diff --git a/Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/repository/PlaceRepository.kt b/Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/repository/PlaceRepository.kt new file mode 100644 index 0000000..7b3170f --- /dev/null +++ b/Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/repository/PlaceRepository.kt @@ -0,0 +1,7 @@ +package com.ping.domain.nonmember.repository + +import com.ping.domain.nonmember.aggregate.PlaceDomain + +interface PlaceRepository { + fun save(place: PlaceDomain) +} \ No newline at end of file From d9aa6509a9253afa2d42cb84a44a56a070373153 Mon Sep 17 00:00:00 2001 From: codrin2 Date: Tue, 22 Oct 2024 21:24:17 +0900 Subject: [PATCH 023/203] =?UTF-8?q?feat:=20#5=20Place=20Entity,=20JpaRepos?= =?UTF-8?q?itory,=20Impl=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/jpa/entity/PlaceEntity.kt | 22 +++++++++++++++++++ .../jpa/repository/PlaceJpaRepository.kt | 6 +++++ .../repositoryImpl/PlaceRepositoryImpl.kt | 20 +++++++++++++++++ 3 files changed, 48 insertions(+) create mode 100644 Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/jpa/entity/PlaceEntity.kt create mode 100644 Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/jpa/repository/PlaceJpaRepository.kt create mode 100644 Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/repositoryImpl/PlaceRepositoryImpl.kt diff --git a/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/jpa/entity/PlaceEntity.kt b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/jpa/entity/PlaceEntity.kt new file mode 100644 index 0000000..e100499 --- /dev/null +++ b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/jpa/entity/PlaceEntity.kt @@ -0,0 +1,22 @@ +package com.ping.infra.nonmember.domain.jpa.entity + +import jakarta.persistence.* + +@Entity +@Table(name = "places") +data class PlaceEntity( + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) + val id: Long = 0, + + @Column(nullable = false) + val name: String, + + @Column(nullable = false) + val address: String, + + @Column(nullable = false) + val latitude: Double, + + @Column(nullable = false) + val longitude: Double +) \ No newline at end of file diff --git a/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/jpa/repository/PlaceJpaRepository.kt b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/jpa/repository/PlaceJpaRepository.kt new file mode 100644 index 0000000..6ac6192 --- /dev/null +++ b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/jpa/repository/PlaceJpaRepository.kt @@ -0,0 +1,6 @@ +package com.ping.infra.nonmember.domain.jpa.repository + +import com.ping.infra.nonmember.domain.jpa.entity.PlaceEntity +import org.springframework.data.jpa.repository.JpaRepository + +interface PlaceJpaRepository : JpaRepository \ No newline at end of file diff --git a/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/repositoryImpl/PlaceRepositoryImpl.kt b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/repositoryImpl/PlaceRepositoryImpl.kt new file mode 100644 index 0000000..d391174 --- /dev/null +++ b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/repositoryImpl/PlaceRepositoryImpl.kt @@ -0,0 +1,20 @@ +package com.ping.infra.nonmember.domain.repositoryImpl + +import com.ping.domain.nonmember.aggregate.PlaceDomain +import com.ping.domain.nonmember.repository.PlaceRepository +import org.springframework.stereotype.Repository + +@Repository +class PlaceRepositoryImpl( + private val placeJpaRepository: PlaceJpaRepository +) : PlaceRepository { + override fun save(place: PlaceDomain) { + val placeEntity = PlaceEntity( + name = place.name, + address = place.address, + latitude = place.latitude, + longitude = place.longitude + ) + placeJpaRepository.save(placeEntity) + } +} \ No newline at end of file From b66893c44564cb516f96d3234c0175a49191739b Mon Sep 17 00:00:00 2001 From: codrin2 Date: Wed, 23 Oct 2024 00:09:25 +0900 Subject: [PATCH 024/203] =?UTF-8?q?feat:=20#5=20Naver=20API=20Client=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/ping/api/place/PlaceController.kt | 7 +++-- .../application/nonmember/NonMemberService.kt | 2 +- .../ping/application/place/PlaceService.kt | 10 ++++--- .../map}/NaverBookmarkResponse.kt | 2 +- .../{navermap => naver/map}/NaverMapClient.kt | 2 +- .../ping/client/naver/place/NaverApiClient.kt | 27 +++++++++++++++++++ .../client/naver/place/NaverApiResponse.kt | 14 ++++++++++ 7 files changed, 55 insertions(+), 9 deletions(-) rename Ping-Client/src/main/kotlin/com/ping/client/{navermap => naver/map}/NaverBookmarkResponse.kt (89%) rename Ping-Client/src/main/kotlin/com/ping/client/{navermap => naver/map}/NaverMapClient.kt (98%) create mode 100644 Ping-Client/src/main/kotlin/com/ping/client/naver/place/NaverApiClient.kt create mode 100644 Ping-Client/src/main/kotlin/com/ping/client/naver/place/NaverApiResponse.kt diff --git a/Ping-Api/src/main/kotlin/com/ping/api/place/PlaceController.kt b/Ping-Api/src/main/kotlin/com/ping/api/place/PlaceController.kt index cb1a473..a5432b1 100644 --- a/Ping-Api/src/main/kotlin/com/ping/api/place/PlaceController.kt +++ b/Ping-Api/src/main/kotlin/com/ping/api/place/PlaceController.kt @@ -1,5 +1,7 @@ package com.ping.api.place +import com.ping.application.place.PlaceService +import com.ping.application.place.dto.SearchPlace import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestMapping @@ -13,7 +15,8 @@ class PlaceController( // 장소 검색 API @PostMapping("/search") - fun searchPlace(@RequestBody request: PlaceRequestDto.SearchRequest) = - placeService.searchPlace(request) + fun searchPlace(@RequestBody request: SearchPlace.Request): List { + return placeService.searchPlace(request); + } } \ No newline at end of file diff --git a/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberService.kt b/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberService.kt index 445da57..621a5c2 100644 --- a/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberService.kt +++ b/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberService.kt @@ -1,7 +1,7 @@ package com.ping.application.nonmember import com.ping.application.nonmember.dto.CreateNonMember -import com.ping.client.navermap.NaverMapClient +import com.ping.client.naver.map.NaverMapClient import com.ping.common.exception.CustomException import com.ping.common.exception.ExceptionContent import com.ping.common.util.UrlUtil diff --git a/Ping-Application/src/main/kotlin/com/ping/application/place/PlaceService.kt b/Ping-Application/src/main/kotlin/com/ping/application/place/PlaceService.kt index 58aade1..36fee1d 100644 --- a/Ping-Application/src/main/kotlin/com/ping/application/place/PlaceService.kt +++ b/Ping-Application/src/main/kotlin/com/ping/application/place/PlaceService.kt @@ -1,8 +1,10 @@ package com.ping.application.place -import com.ping.application.place.dto.PlaceRequestDto import com.ping.application.place.dto.SavePlace import com.ping.application.place.dto.SearchPlace +import com.ping.client.naver.place.NaverApiClient +import com.ping.domain.nonmember.aggregate.PlaceDomain +import com.ping.domain.nonmember.repository.PlaceRepository import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional @@ -16,10 +18,10 @@ class PlaceService( fun searchPlace(request: SearchPlace.Request): List { return naverApiClient.searchPlaces(request.keyword).map { SearchPlace.Response( - name = it.name, + name = it.title, address = it.address, - latitude = it.latitude, - longitude = it.longitude + latitude = it.mapx, + longitude = it.mapy ) } } diff --git a/Ping-Client/src/main/kotlin/com/ping/client/navermap/NaverBookmarkResponse.kt b/Ping-Client/src/main/kotlin/com/ping/client/naver/map/NaverBookmarkResponse.kt similarity index 89% rename from Ping-Client/src/main/kotlin/com/ping/client/navermap/NaverBookmarkResponse.kt rename to Ping-Client/src/main/kotlin/com/ping/client/naver/map/NaverBookmarkResponse.kt index 7678988..b0edc31 100644 --- a/Ping-Client/src/main/kotlin/com/ping/client/navermap/NaverBookmarkResponse.kt +++ b/Ping-Client/src/main/kotlin/com/ping/client/naver/map/NaverBookmarkResponse.kt @@ -1,4 +1,4 @@ -package com.ping.client.navermap +package com.ping.client.naver.map class NaverBookmarkResponse { data class BookmarkLists( diff --git a/Ping-Client/src/main/kotlin/com/ping/client/navermap/NaverMapClient.kt b/Ping-Client/src/main/kotlin/com/ping/client/naver/map/NaverMapClient.kt similarity index 98% rename from Ping-Client/src/main/kotlin/com/ping/client/navermap/NaverMapClient.kt rename to Ping-Client/src/main/kotlin/com/ping/client/naver/map/NaverMapClient.kt index f247659..9f93f4f 100644 --- a/Ping-Client/src/main/kotlin/com/ping/client/navermap/NaverMapClient.kt +++ b/Ping-Client/src/main/kotlin/com/ping/client/naver/map/NaverMapClient.kt @@ -1,4 +1,4 @@ -package com.ping.client.navermap +package com.ping.client.naver.map import com.fasterxml.jackson.databind.ObjectMapper import com.ping.common.exception.CustomException diff --git a/Ping-Client/src/main/kotlin/com/ping/client/naver/place/NaverApiClient.kt b/Ping-Client/src/main/kotlin/com/ping/client/naver/place/NaverApiClient.kt new file mode 100644 index 0000000..c7e6c61 --- /dev/null +++ b/Ping-Client/src/main/kotlin/com/ping/client/naver/place/NaverApiClient.kt @@ -0,0 +1,27 @@ +package com.ping.client.naver.place + +import org.springframework.beans.factory.annotation.Value +import org.springframework.stereotype.Component +import org.springframework.web.reactive.function.client.WebClient + +@Component +class NaverApiClient( + @Value("\${naver.client.id}") private val clientId: String, + @Value("\${naver.client.secret}") private val clientSecret: String +) { + + fun searchPlaces(keyword: String): List { + val client = WebClient.create("https://openapi.naver.com/v1/search/local.json") + val response = client.get() + .uri { uriBuilder -> + uriBuilder.queryParam("query", keyword).queryParam("display", 10).build() + } + .header("X-Naver-Client-Id", clientId) + .header("X-Naver-Client-Secret", clientSecret) + .retrieve() + .bodyToMono(NaverApiResponse.NaverResponse::class.java) + .block() + + return response?.items ?: emptyList() + } +} diff --git a/Ping-Client/src/main/kotlin/com/ping/client/naver/place/NaverApiResponse.kt b/Ping-Client/src/main/kotlin/com/ping/client/naver/place/NaverApiResponse.kt new file mode 100644 index 0000000..b26a531 --- /dev/null +++ b/Ping-Client/src/main/kotlin/com/ping/client/naver/place/NaverApiResponse.kt @@ -0,0 +1,14 @@ +package com.ping.client.naver.place + +class NaverApiResponse { + data class NaverResponse( + val items: List + ) + + data class NaverPlace( + val title: String, + val address: String, + val mapx: Double, + val mapy: Double + ) +} \ No newline at end of file From f897808f68ef1de87bfbb0eb2338b5a89e9755ff Mon Sep 17 00:00:00 2001 From: sominyun Date: Wed, 23 Oct 2024 11:23:55 +0900 Subject: [PATCH 025/203] =?UTF-8?q?fix:#6=20=EA=B8=B0=EB=B3=B8=ED=94=84?= =?UTF-8?q?=EB=A1=9C=ED=95=84=EC=9D=B4=20=EC=95=84=EB=8B=8C=20level1=20ico?= =?UTF-8?q?n=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/com/ping/application/nonmember/NonMemberService.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberService.kt b/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberService.kt index 7770d31..1d9f849 100644 --- a/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberService.kt +++ b/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberService.kt @@ -145,7 +145,7 @@ class NonMemberService( val pings = nonMemberPlaces.entries.mapIndexed{ index, nonMemberPlace -> val level = when { - nonMemberPlace.key == 1 -> 0 // 아무도 안겹친 sid (겹친 인원이 1인 경우) + nonMemberPlace.key == 1 -> 1 // 아무도 안겹친 sid (겹친 인원이 1인 경우) index == 0 -> 4 // 가장 많이 겹친 sid index == 1 -> 3 // 두 번째로 많이 겹친 sid index == 2 -> 2 // 세 번째로 많이 겹친 sid From fb682de232dcf203cf2b73cd0a4a3f97127b6f54 Mon Sep 17 00:00:00 2001 From: sominyun Date: Wed, 23 Oct 2024 13:44:35 +0900 Subject: [PATCH 026/203] hotfix: bootJar isEnabled = false --- Ping-Application/build.gradle.kts | 2 +- Ping-Domain/build.gradle.kts | 2 +- Ping-Infra/build.gradle.kts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Ping-Application/build.gradle.kts b/Ping-Application/build.gradle.kts index 1f16c3a..86ee6b8 100644 --- a/Ping-Application/build.gradle.kts +++ b/Ping-Application/build.gradle.kts @@ -8,7 +8,7 @@ dependencies { } tasks { bootJar { - isEnabled = true + isEnabled = false } jar { isEnabled = true diff --git a/Ping-Domain/build.gradle.kts b/Ping-Domain/build.gradle.kts index 5910e61..843e2da 100644 --- a/Ping-Domain/build.gradle.kts +++ b/Ping-Domain/build.gradle.kts @@ -3,7 +3,7 @@ dependencies { } tasks { bootJar { - isEnabled = true + isEnabled = false } jar { isEnabled = true diff --git a/Ping-Infra/build.gradle.kts b/Ping-Infra/build.gradle.kts index 0859627..eb9530a 100644 --- a/Ping-Infra/build.gradle.kts +++ b/Ping-Infra/build.gradle.kts @@ -22,7 +22,7 @@ dependencies { } tasks { bootJar { - isEnabled = true + isEnabled = false } jar { isEnabled = true From e4e8b69298b4c59498f51ce8012acde9fccd6bd9 Mon Sep 17 00:00:00 2001 From: sominyun Date: Wed, 23 Oct 2024 18:28:04 +0900 Subject: [PATCH 027/203] feat: application.yml --- Ping-Api/src/main/resources/application.yaml | 42 ++++++++++++++++++++ src/main/resources/application-prod.yaml | 0 2 files changed, 42 insertions(+) create mode 100644 Ping-Api/src/main/resources/application.yaml delete mode 100644 src/main/resources/application-prod.yaml diff --git a/Ping-Api/src/main/resources/application.yaml b/Ping-Api/src/main/resources/application.yaml new file mode 100644 index 0000000..fdba4fb --- /dev/null +++ b/Ping-Api/src/main/resources/application.yaml @@ -0,0 +1,42 @@ +server: + port: 8080 + servlet: + context-path: /api/v1 + +spring: + jackson: + time-zone: Asia/Seoul + datasource: + driver-class-name: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://${RDB_HOST}:${RDB_PORT}/${RDB_SCHEMA}?createDatabaseIfNotExist=true + username: ${RDB_USER} + password: ${RDB_PASSWORD} + hikari: + auto-commit: false + connection-test-query: SELECT 1 + minimum-idle: 10 + maximum-pool-size: 20 + + data: + mongodb: + uri: mongodb://${MONGODB_USER}:${MONGODB_PASSWORD}@${MONGODB_HOST}:${MONGODB_PORT}/${MONGODB_SCHEMA}?authSource=admin + + jpa: + database-platform: org.hibernate.dialect.MySQL8Dialect + hibernate: + ddl-auto: update + open-in-view: false + +management: + endpoints: + web: + exposure: + include: health,info + endpoint: + health: + probes: + enabled: true + +logging: + level: + root: info \ No newline at end of file diff --git a/src/main/resources/application-prod.yaml b/src/main/resources/application-prod.yaml deleted file mode 100644 index e69de29..0000000 From 434f90da80d4dbd8b7488a39a920ef8eb899b9e4 Mon Sep 17 00:00:00 2001 From: sominyun Date: Wed, 23 Oct 2024 20:09:29 +0900 Subject: [PATCH 028/203] =?UTF-8?q?hotfix:=20=ED=8F=AC=ED=8A=B8=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Ping-Api/src/main/resources/application.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Ping-Api/src/main/resources/application.yaml b/Ping-Api/src/main/resources/application.yaml index fdba4fb..783e6c2 100644 --- a/Ping-Api/src/main/resources/application.yaml +++ b/Ping-Api/src/main/resources/application.yaml @@ -1,5 +1,5 @@ server: - port: 8080 + port: ${SERVER_PORT} servlet: context-path: /api/v1 From f3a91c3147d922931402f3ca5e79a7ffa5487da7 Mon Sep 17 00:00:00 2001 From: codrin2 Date: Wed, 23 Oct 2024 23:51:44 +0900 Subject: [PATCH 029/203] =?UTF-8?q?remove:=20place=20=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?=ED=95=84=EC=9A=94=EC=97=86=EB=8A=94=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ping/application/place/PlaceService.kt | 3 --- .../nonmember/repository/PlaceRepository.kt | 7 ------ .../domain/jpa/entity/PlaceEntity.kt | 22 ------------------- .../jpa/repository/PlaceJpaRepository.kt | 6 ----- .../repositoryImpl/PlaceRepositoryImpl.kt | 20 ----------------- 5 files changed, 58 deletions(-) delete mode 100644 Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/repository/PlaceRepository.kt delete mode 100644 Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/jpa/entity/PlaceEntity.kt delete mode 100644 Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/jpa/repository/PlaceJpaRepository.kt delete mode 100644 Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/repositoryImpl/PlaceRepositoryImpl.kt diff --git a/Ping-Application/src/main/kotlin/com/ping/application/place/PlaceService.kt b/Ping-Application/src/main/kotlin/com/ping/application/place/PlaceService.kt index 36fee1d..3b2210b 100644 --- a/Ping-Application/src/main/kotlin/com/ping/application/place/PlaceService.kt +++ b/Ping-Application/src/main/kotlin/com/ping/application/place/PlaceService.kt @@ -4,14 +4,12 @@ import com.ping.application.place.dto.SavePlace import com.ping.application.place.dto.SearchPlace import com.ping.client.naver.place.NaverApiClient import com.ping.domain.nonmember.aggregate.PlaceDomain -import com.ping.domain.nonmember.repository.PlaceRepository import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional @Service class PlaceService( private val naverApiClient: NaverApiClient, - private val placeRepository: PlaceRepository ) { // 장소 검색 @@ -35,6 +33,5 @@ class PlaceService( latitude = request.latitude, longitude = request.longitude ) - placeRepository.save(place) } } \ No newline at end of file diff --git a/Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/repository/PlaceRepository.kt b/Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/repository/PlaceRepository.kt deleted file mode 100644 index 7b3170f..0000000 --- a/Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/repository/PlaceRepository.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.ping.domain.nonmember.repository - -import com.ping.domain.nonmember.aggregate.PlaceDomain - -interface PlaceRepository { - fun save(place: PlaceDomain) -} \ No newline at end of file diff --git a/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/jpa/entity/PlaceEntity.kt b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/jpa/entity/PlaceEntity.kt deleted file mode 100644 index e100499..0000000 --- a/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/jpa/entity/PlaceEntity.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.ping.infra.nonmember.domain.jpa.entity - -import jakarta.persistence.* - -@Entity -@Table(name = "places") -data class PlaceEntity( - @Id @GeneratedValue(strategy = GenerationType.IDENTITY) - val id: Long = 0, - - @Column(nullable = false) - val name: String, - - @Column(nullable = false) - val address: String, - - @Column(nullable = false) - val latitude: Double, - - @Column(nullable = false) - val longitude: Double -) \ No newline at end of file diff --git a/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/jpa/repository/PlaceJpaRepository.kt b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/jpa/repository/PlaceJpaRepository.kt deleted file mode 100644 index 6ac6192..0000000 --- a/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/jpa/repository/PlaceJpaRepository.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.ping.infra.nonmember.domain.jpa.repository - -import com.ping.infra.nonmember.domain.jpa.entity.PlaceEntity -import org.springframework.data.jpa.repository.JpaRepository - -interface PlaceJpaRepository : JpaRepository \ No newline at end of file diff --git a/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/repositoryImpl/PlaceRepositoryImpl.kt b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/repositoryImpl/PlaceRepositoryImpl.kt deleted file mode 100644 index d391174..0000000 --- a/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/repositoryImpl/PlaceRepositoryImpl.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.ping.infra.nonmember.domain.repositoryImpl - -import com.ping.domain.nonmember.aggregate.PlaceDomain -import com.ping.domain.nonmember.repository.PlaceRepository -import org.springframework.stereotype.Repository - -@Repository -class PlaceRepositoryImpl( - private val placeJpaRepository: PlaceJpaRepository -) : PlaceRepository { - override fun save(place: PlaceDomain) { - val placeEntity = PlaceEntity( - name = place.name, - address = place.address, - latitude = place.latitude, - longitude = place.longitude - ) - placeJpaRepository.save(placeEntity) - } -} \ No newline at end of file From 0e03e29afd909e4482a056c63a4469fa8b557d41 Mon Sep 17 00:00:00 2001 From: codrin2 Date: Thu, 24 Oct 2024 02:19:41 +0900 Subject: [PATCH 030/203] =?UTF-8?q?feat(EventService):=20=EC=9D=B4?= =?UTF-8?q?=EB=B2=A4=ED=8A=B8=20=EC=83=9D=EC=84=B1=20=EB=A1=9C=EC=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ping/application/event/EventService.kt | 32 +++++++++++++++++++ .../ping/application/event/dto/CreateEvent.kt | 11 +++++++ .../repository/ShareUrlRepository.kt | 1 + .../repositoryImpl/ShareUrlRepositoryImpl.kt | 5 +++ 4 files changed, 49 insertions(+) create mode 100644 Ping-Application/src/main/kotlin/com/ping/application/event/EventService.kt create mode 100644 Ping-Application/src/main/kotlin/com/ping/application/event/dto/CreateEvent.kt diff --git a/Ping-Application/src/main/kotlin/com/ping/application/event/EventService.kt b/Ping-Application/src/main/kotlin/com/ping/application/event/EventService.kt new file mode 100644 index 0000000..1854b7e --- /dev/null +++ b/Ping-Application/src/main/kotlin/com/ping/application/event/EventService.kt @@ -0,0 +1,32 @@ +package com.ping.application.event + +import com.ping.application.event.dto.CreateEvent +import com.ping.domain.nonmember.aggregate.ShareUrlDomain +import com.ping.domain.nonmember.repository.ShareUrlRepository +import org.springframework.beans.factory.annotation.Value +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import java.util.UUID + +@Service +@Transactional(readOnly = true) +class EventService( + private val shareUrlRepository: ShareUrlRepository +) { + @Value("\${pingping.share.base-url}") + private lateinit var baseUrl: String + + @Transactional + fun createShareUrl(request: CreateEvent.Request): CreateEvent.Response { + val uniqueId = UUID.randomUUID().toString().substring(0, 8) + val uniqueUrl = generateUniqueUrl(request, uniqueId) + + val shareUrl = ShareUrlDomain(0,uniqueUrl, request.eventName, request.neighborhood,uniqueId) + val savedShareUrlDomain = shareUrlRepository.save(shareUrl) + + return CreateEvent.Response(savedShareUrlDomain.url) + } + private fun generateUniqueUrl(request: CreateEvent.Request, uniqueId: String): String { + return "$baseUrl/${request.eventName}/${request.neighborhood}/$uniqueId" + } +} \ No newline at end of file diff --git a/Ping-Application/src/main/kotlin/com/ping/application/event/dto/CreateEvent.kt b/Ping-Application/src/main/kotlin/com/ping/application/event/dto/CreateEvent.kt new file mode 100644 index 0000000..3efc83d --- /dev/null +++ b/Ping-Application/src/main/kotlin/com/ping/application/event/dto/CreateEvent.kt @@ -0,0 +1,11 @@ +package com.ping.application.event.dto + +class CreateEvent { + data class Request( + val neighborhood: String, + val eventName: String + ) + data class Response( + val shareUrl: String + ) +} \ No newline at end of file diff --git a/Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/repository/ShareUrlRepository.kt b/Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/repository/ShareUrlRepository.kt index 518d2cd..433ac5d 100644 --- a/Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/repository/ShareUrlRepository.kt +++ b/Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/repository/ShareUrlRepository.kt @@ -4,4 +4,5 @@ import com.ping.domain.nonmember.aggregate.ShareUrlDomain interface ShareUrlRepository { fun findByUuid(uuid: String): ShareUrlDomain? + fun save(shareUrlDomain: ShareUrlDomain): ShareUrlDomain } diff --git a/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/repositoryImpl/ShareUrlRepositoryImpl.kt b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/repositoryImpl/ShareUrlRepositoryImpl.kt index ecf06fc..006f6e9 100644 --- a/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/repositoryImpl/ShareUrlRepositoryImpl.kt +++ b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/repositoryImpl/ShareUrlRepositoryImpl.kt @@ -2,6 +2,7 @@ package com.ping.infra.nonmember.domain.repositoryImpl import com.ping.domain.nonmember.aggregate.ShareUrlDomain import com.ping.domain.nonmember.repository.ShareUrlRepository +import com.ping.infra.nonmember.domain.jpa.entity.ShareUrlEntity import com.ping.infra.nonmember.domain.jpa.repository.ShareUrlJpaRepository import com.ping.infra.nonmember.domain.mapper.ShareUrlMapper import org.springframework.stereotype.Repository @@ -15,4 +16,8 @@ class ShareUrlRepositoryImpl( ShareUrlMapper.toDomain(it) } } + + override fun save(shareUrlDomain: ShareUrlDomain): ShareUrlDomain { + return ShareUrlMapper.toDomain(shareUrlJpaRepository.save(ShareUrlMapper.toEntity(shareUrlDomain))) + } } \ No newline at end of file From 2cd5f66b63c547027258e43a3075eb61fe633015 Mon Sep 17 00:00:00 2001 From: codrin2 Date: Thu, 24 Oct 2024 02:35:28 +0900 Subject: [PATCH 031/203] =?UTF-8?q?fix:=20ShareUrl=EC=97=90=20=EC=9E=A5?= =?UTF-8?q?=EC=86=8C=EC=9D=98=20=EC=9C=84=EB=8F=84=20=EB=B0=8F=20=EA=B2=BD?= =?UTF-8?q?=EB=8F=84=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/kotlin/com/ping/application/event/EventService.kt | 2 +- .../kotlin/com/ping/application/event/dto/CreateEvent.kt | 2 ++ .../com/ping/domain/nonmember/aggregate/ShareUrlDomain.kt | 2 ++ .../infra/nonmember/domain/jpa/entity/ShareUrlEntity.kt | 6 ++++++ .../ping/infra/nonmember/domain/mapper/ShareUrlMapper.kt | 4 ++++ 5 files changed, 15 insertions(+), 1 deletion(-) diff --git a/Ping-Application/src/main/kotlin/com/ping/application/event/EventService.kt b/Ping-Application/src/main/kotlin/com/ping/application/event/EventService.kt index 1854b7e..a73c164 100644 --- a/Ping-Application/src/main/kotlin/com/ping/application/event/EventService.kt +++ b/Ping-Application/src/main/kotlin/com/ping/application/event/EventService.kt @@ -21,7 +21,7 @@ class EventService( val uniqueId = UUID.randomUUID().toString().substring(0, 8) val uniqueUrl = generateUniqueUrl(request, uniqueId) - val shareUrl = ShareUrlDomain(0,uniqueUrl, request.eventName, request.neighborhood,uniqueId) + val shareUrl = ShareUrlDomain(0,uniqueUrl, request.eventName, request.neighborhood,request.mapx, request.mapy, uniqueId) val savedShareUrlDomain = shareUrlRepository.save(shareUrl) return CreateEvent.Response(savedShareUrlDomain.url) diff --git a/Ping-Application/src/main/kotlin/com/ping/application/event/dto/CreateEvent.kt b/Ping-Application/src/main/kotlin/com/ping/application/event/dto/CreateEvent.kt index 3efc83d..b1a9275 100644 --- a/Ping-Application/src/main/kotlin/com/ping/application/event/dto/CreateEvent.kt +++ b/Ping-Application/src/main/kotlin/com/ping/application/event/dto/CreateEvent.kt @@ -3,6 +3,8 @@ package com.ping.application.event.dto class CreateEvent { data class Request( val neighborhood: String, + val mapx: Double, + val mapy: Double, val eventName: String ) data class Response( diff --git a/Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/aggregate/ShareUrlDomain.kt b/Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/aggregate/ShareUrlDomain.kt index daa44cf..5fc67f4 100644 --- a/Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/aggregate/ShareUrlDomain.kt +++ b/Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/aggregate/ShareUrlDomain.kt @@ -5,5 +5,7 @@ data class ShareUrlDomain( val url: String, val eventName: String, val neighborhood: String, + val latitude: Double, + val longtitude: Double, val uuid: String ) \ No newline at end of file diff --git a/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/jpa/entity/ShareUrlEntity.kt b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/jpa/entity/ShareUrlEntity.kt index 5a6122e..8a19214 100644 --- a/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/jpa/entity/ShareUrlEntity.kt +++ b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/jpa/entity/ShareUrlEntity.kt @@ -18,6 +18,12 @@ class ShareUrlEntity( @Column(nullable = false) val neighborhood: String, + @Column(nullable = false) + val latitude: Double, + + @Column(nullable = false) + val longtitude: Double, + @Column(nullable = false, unique = true) val uuid: String diff --git a/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/mapper/ShareUrlMapper.kt b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/mapper/ShareUrlMapper.kt index 51ba854..95b93db 100644 --- a/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/mapper/ShareUrlMapper.kt +++ b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/mapper/ShareUrlMapper.kt @@ -10,6 +10,8 @@ object ShareUrlMapper { shareUrlDomain.url, shareUrlDomain.eventName, shareUrlDomain.neighborhood, + shareUrlDomain.latitude, + shareUrlDomain.longtitude, shareUrlDomain.uuid ) @@ -18,6 +20,8 @@ object ShareUrlMapper { shareUrlEntity.url, shareUrlEntity.eventName, shareUrlEntity.neighborhood, + shareUrlEntity.latitude, + shareUrlEntity.longtitude, shareUrlEntity.uuid ) } \ No newline at end of file From c073a096ce9fe9dd63c38491678c5db9ba071a0d Mon Sep 17 00:00:00 2001 From: codrin2 Date: Thu, 24 Oct 2024 03:09:23 +0900 Subject: [PATCH 032/203] =?UTF-8?q?feat:=20EventController=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/ping/api/event/EventController.kt | 31 +++++++++++++++++++ .../com/ping/api/place/PlaceController.kt | 11 +++++-- .../ping/application/event/EventService.kt | 2 +- .../{CommonResponse.kt => ErrorResponse.kt} | 6 ++-- .../exception/GlobalExceptionHandler.kt | 22 ++++++------- .../ping/common/exception/SuccessResponse.kt | 15 +++++++++ 6 files changed, 70 insertions(+), 17 deletions(-) create mode 100644 Ping-Api/src/main/kotlin/com/ping/api/event/EventController.kt rename Ping-Common/src/main/kotlin/com/ping/common/exception/{CommonResponse.kt => ErrorResponse.kt} (67%) create mode 100644 Ping-Common/src/main/kotlin/com/ping/common/exception/SuccessResponse.kt diff --git a/Ping-Api/src/main/kotlin/com/ping/api/event/EventController.kt b/Ping-Api/src/main/kotlin/com/ping/api/event/EventController.kt new file mode 100644 index 0000000..1b7b567 --- /dev/null +++ b/Ping-Api/src/main/kotlin/com/ping/api/event/EventController.kt @@ -0,0 +1,31 @@ +package com.ping.api.event + +import com.ping.application.event.EventService +import com.ping.application.event.dto.CreateEvent +import com.ping.common.exception.SuccessResponse +import io.swagger.v3.oas.annotations.tags.Tag +import org.springframework.http.HttpStatus +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +@Tag(name = "이벤트") +@RestController +@RequestMapping("/event") +class EventController( + private val eventService: EventService +) { + @PostMapping + fun create( + @RequestBody request: CreateEvent.Request + ): ResponseEntity> { + val response = eventService.create(request) + + return ResponseEntity.ok( + SuccessResponse.of(HttpStatus.OK, "공유 URL이 성공적으로 생성되었습니다.", response) + ) + } + +} \ No newline at end of file diff --git a/Ping-Api/src/main/kotlin/com/ping/api/place/PlaceController.kt b/Ping-Api/src/main/kotlin/com/ping/api/place/PlaceController.kt index 83a5ca5..9f75070 100644 --- a/Ping-Api/src/main/kotlin/com/ping/api/place/PlaceController.kt +++ b/Ping-Api/src/main/kotlin/com/ping/api/place/PlaceController.kt @@ -1,7 +1,11 @@ package com.ping.api.place +import com.ping.application.event.dto.CreateEvent import com.ping.application.place.PlaceService import com.ping.application.place.dto.SearchPlace +import com.ping.common.exception.SuccessResponse +import org.springframework.http.HttpStatus +import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.* @RestController @@ -12,8 +16,11 @@ class PlaceController( // 장소 검색 API @GetMapping("/search/{keyword}") - fun searchPlace(@PathVariable("keyword") keyword: String): List { - return placeService.searchPlace(keyword); + fun searchPlace(@PathVariable("keyword") keyword: String): ResponseEntity>> { + val response = placeService.searchPlace(keyword); + return ResponseEntity.ok( + SuccessResponse.of(HttpStatus.OK, "공유 URL이 성공적으로 생성되었습니다.", response) + ) } } \ No newline at end of file diff --git a/Ping-Application/src/main/kotlin/com/ping/application/event/EventService.kt b/Ping-Application/src/main/kotlin/com/ping/application/event/EventService.kt index a73c164..6e06feb 100644 --- a/Ping-Application/src/main/kotlin/com/ping/application/event/EventService.kt +++ b/Ping-Application/src/main/kotlin/com/ping/application/event/EventService.kt @@ -17,7 +17,7 @@ class EventService( private lateinit var baseUrl: String @Transactional - fun createShareUrl(request: CreateEvent.Request): CreateEvent.Response { + fun create(request: CreateEvent.Request): CreateEvent.Response { val uniqueId = UUID.randomUUID().toString().substring(0, 8) val uniqueUrl = generateUniqueUrl(request, uniqueId) diff --git a/Ping-Common/src/main/kotlin/com/ping/common/exception/CommonResponse.kt b/Ping-Common/src/main/kotlin/com/ping/common/exception/ErrorResponse.kt similarity index 67% rename from Ping-Common/src/main/kotlin/com/ping/common/exception/CommonResponse.kt rename to Ping-Common/src/main/kotlin/com/ping/common/exception/ErrorResponse.kt index 065c9d8..1a9642c 100644 --- a/Ping-Common/src/main/kotlin/com/ping/common/exception/CommonResponse.kt +++ b/Ping-Common/src/main/kotlin/com/ping/common/exception/ErrorResponse.kt @@ -2,16 +2,16 @@ package com.ping.common.exception import org.springframework.http.HttpStatus -data class CommonResponse( +data class ErrorResponse( val code: Int, val errorCode: String, val message: String, val data: D? = null ) { companion object { - fun of(status: HttpStatus, codePrefix: String, codeNum: Int, message: String, data: D? = null): CommonResponse { + fun of(status: HttpStatus, codePrefix: String, codeNum: Int, message: String, data: D? = null): ErrorResponse { val errorCode="$codePrefix-$codeNum" - return CommonResponse(status.value(), errorCode, message, data) + return ErrorResponse(status.value(), errorCode, message, data) } } } \ No newline at end of file diff --git a/Ping-Common/src/main/kotlin/com/ping/common/exception/GlobalExceptionHandler.kt b/Ping-Common/src/main/kotlin/com/ping/common/exception/GlobalExceptionHandler.kt index ec6146e..d4a1390 100644 --- a/Ping-Common/src/main/kotlin/com/ping/common/exception/GlobalExceptionHandler.kt +++ b/Ping-Common/src/main/kotlin/com/ping/common/exception/GlobalExceptionHandler.kt @@ -18,9 +18,9 @@ class GlobalExceptionHandler { val errorNum = 1 // 공통 에러 응답 생성 메서드 - private fun generateErrorResponse(status: HttpStatus, message: String?): ResponseEntity> { + private fun generateErrorResponse(status: HttpStatus, message: String?): ResponseEntity> { val errorMessage = message ?: "알 수 없는 에러가 발생했습니다." - val errorResponse = CommonResponse.of(status, errorPrefix, errorNum, errorMessage) + val errorResponse = ErrorResponse.of(status, errorPrefix, errorNum, errorMessage) return ResponseEntity(errorResponse, status) } @@ -29,7 +29,7 @@ class GlobalExceptionHandler { e: Exception, status: HttpStatus, message: String? = null - ): ResponseEntity> { + ): ResponseEntity> { log.error { "${e.javaClass.simpleName} occurred: ${e.message}" } return generateErrorResponse(status, message ?: e.message) } @@ -40,45 +40,45 @@ class GlobalExceptionHandler { errorPrefix: String, errorNum: Int, message: String - ): ResponseEntity> { + ): ResponseEntity> { log.error { "${e.javaClass.simpleName} occurred: ${e.message}" } - val errorResponse = CommonResponse.of(status, errorPrefix, errorNum, message) + val errorResponse = ErrorResponse.of(status, errorPrefix, errorNum, message) return ResponseEntity(errorResponse, status) } // 커스텀 Exception 처리 @ExceptionHandler(CustomException::class) - fun handleCustomException(exception: CustomException): ResponseEntity> { + fun handleCustomException(exception: CustomException): ResponseEntity> { return logAndCustomErrorResponse(exception, exception.content.httpStatus, exception.content.errorPrefix,exception.content.errorNum ,exception.content.message) } // 모든 Exception 처리 @ExceptionHandler(Exception::class) - fun handleAllExceptions(e: Exception): ResponseEntity> { + fun handleAllExceptions(e: Exception): ResponseEntity> { return logAndGenerateErrorResponse(e, HttpStatus.INTERNAL_SERVER_ERROR, "Internal server error") } // NoSuchElementException 처리 @ExceptionHandler(NoSuchElementException::class) - fun handleNoSuchElementException(e: NoSuchElementException): ResponseEntity> { + fun handleNoSuchElementException(e: NoSuchElementException): ResponseEntity> { return logAndGenerateErrorResponse(e, HttpStatus.NOT_FOUND, "Resource not found") } // EmptyResultDataAccessException 처리 @ExceptionHandler(EmptyResultDataAccessException::class) - fun handleEmptyResultDataAccessException(e: EmptyResultDataAccessException): ResponseEntity> { + fun handleEmptyResultDataAccessException(e: EmptyResultDataAccessException): ResponseEntity> { return logAndGenerateErrorResponse(e, HttpStatus.NOT_FOUND, "Resource not found") } // HttpMessageNotReadableException 처리 @ExceptionHandler(HttpMessageNotReadableException::class) - fun handleJsonException(e: HttpMessageNotReadableException): ResponseEntity> { + fun handleJsonException(e: HttpMessageNotReadableException): ResponseEntity> { return logAndGenerateErrorResponse(e, HttpStatus.BAD_REQUEST, "Invalid JSON format") } // HttpRequestMethodNotSupportedException 처리 @ExceptionHandler(HttpRequestMethodNotSupportedException::class) - fun handleRequestMethodException(e: HttpRequestMethodNotSupportedException): ResponseEntity> { + fun handleRequestMethodException(e: HttpRequestMethodNotSupportedException): ResponseEntity> { return logAndGenerateErrorResponse( e, HttpStatus.METHOD_NOT_ALLOWED, diff --git a/Ping-Common/src/main/kotlin/com/ping/common/exception/SuccessResponse.kt b/Ping-Common/src/main/kotlin/com/ping/common/exception/SuccessResponse.kt new file mode 100644 index 0000000..92df93a --- /dev/null +++ b/Ping-Common/src/main/kotlin/com/ping/common/exception/SuccessResponse.kt @@ -0,0 +1,15 @@ +package com.ping.common.exception + +import org.springframework.http.HttpStatus + +data class SuccessResponse( + val code: Int, + val message: String, + val data: D? = null +) { + companion object { + fun of(status: HttpStatus, message: String, data: D? = null): SuccessResponse { + return SuccessResponse(status.value(), message, data) + } + } +} \ No newline at end of file From edecd2a434a4c974ca28bb5a63209a7d1bb570dd Mon Sep 17 00:00:00 2001 From: codrin2 Date: Thu, 24 Oct 2024 03:53:16 +0900 Subject: [PATCH 033/203] =?UTF-8?q?feat:=20NonMemberLoginService=20?= =?UTF-8?q?=EB=B0=8F=20Validator=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../nonmember/NonMemberLoginService.kt | 31 +++++++++++++++++++ .../application/nonmember/NonMemberService.kt | 23 +++----------- .../nonmember/NonMemberValidator.kt | 24 ++++++++++++++ .../nonmember/dto/LoginNonMember.kt | 8 +++++ .../repository/NonMemberRepository.kt | 2 ++ 5 files changed, 69 insertions(+), 19 deletions(-) create mode 100644 Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberLoginService.kt create mode 100644 Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberValidator.kt create mode 100644 Ping-Application/src/main/kotlin/com/ping/application/nonmember/dto/LoginNonMember.kt diff --git a/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberLoginService.kt b/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberLoginService.kt new file mode 100644 index 0000000..2ea3f34 --- /dev/null +++ b/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberLoginService.kt @@ -0,0 +1,31 @@ +package com.ping.application.nonmember + +import com.ping.application.nonmember.dto.LoginNonMember +import com.ping.common.exception.CustomException +import com.ping.common.exception.ExceptionContent +import com.ping.domain.nonmember.repository.NonMemberRepository +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Service +@Transactional(readOnly = true) +class NonMemberLoginService( + private val nonMemberRepository: NonMemberRepository, + private val validator: NamePasswordValidator +) { + fun loginNonMember(request: LoginNonMember.Request) { + // 비밀번호 형식 검사 (4자리 숫자) + validator.password(request.password) + + val nonMember = nonMemberRepository.findById(request.nonMemberId)?:let { + throw CustomException(ExceptionContent.NON_MEMBER_NOT_FOUND) + } + + // 비밀번호가 일치하는지 비교 + if (request.password != nonMember.password) { + throw CustomException(ExceptionContent.NON_MEMBER_LOGIN_FAILED) + } + } +} + + diff --git a/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberService.kt b/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberService.kt index c6156cf..5d5c7d6 100644 --- a/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberService.kt +++ b/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberService.kt @@ -20,15 +20,15 @@ class NonMemberService( private val nonMemberPlaceRepository: NonMemberPlaceRepository, private val nonMemberBookmarkUrlRepository: NonMemberBookmarkUrlRepository, private val nonMemberStoreUrlRepository: NonMemberStoreUrlRepository, - private val naverMapClient: NaverMapClient + private val naverMapClient: NaverMapClient, + private val validator: NamePasswordValidator ) { - @Transactional fun createNonMemberPings(request: CreateNonMember.Request) { //이름 공백, 특수문자, 숫자 불가 - validateName(request.name) + validator.name(request.name) // 비밀번호 형식 검사 (4자리 숫자) - validatePassword(request.password) + validator.password(request.password) val shareUrl = shareUrlRepository.findByUuid(request.uuid) ?: throw CustomException(ExceptionContent.INVALID_SHARE_URL) @@ -94,21 +94,6 @@ class NonMemberService( nonMemberPlaceRepository.saveAll(nonMemberPlaces) } - // 이름 우효성 검증 로직 - private fun validateName(name: String) { - val namePattern = "^[가-힣a-zA-Z]{1,6}\$".toRegex() - if (!namePattern.matches(name)) { - throw CustomException(ExceptionContent.INVALID_NAME_FORMAT) - } - } - - // 비밀번호 유효성 검증 로직 - private fun validatePassword(password: String) { - if (!password.matches(Regex("\\d{4}"))) { - throw CustomException(ExceptionContent.INVALID_PASSWORD_FORMAT) - } - } - fun getAllNonMemberPings(uuid: String): GetAllNonMemberPings.Response { val shareUrl = shareUrlRepository.findByUuid(uuid) ?: throw CustomException(ExceptionContent.INVALID_SHARE_URL) diff --git a/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberValidator.kt b/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberValidator.kt new file mode 100644 index 0000000..cdb76e2 --- /dev/null +++ b/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberValidator.kt @@ -0,0 +1,24 @@ +package com.ping.application.nonmember + +import com.ping.common.exception.CustomException +import com.ping.common.exception.ExceptionContent +import org.springframework.stereotype.Component + +@Component +class NamePasswordValidator { + + // 이름 유효성 검증 + fun name(name: String) { + val namePattern = "^[가-힣a-zA-Z]{1,6}\$".toRegex() + if (!namePattern.matches(name)) { + throw CustomException(ExceptionContent.INVALID_NAME_FORMAT) + } + } + + // 비밀번호 유효성 검증 + fun password(password: String) { + if (!password.matches(Regex("\\d{4}"))) { + throw CustomException(ExceptionContent.INVALID_PASSWORD_FORMAT) + } + } +} diff --git a/Ping-Application/src/main/kotlin/com/ping/application/nonmember/dto/LoginNonMember.kt b/Ping-Application/src/main/kotlin/com/ping/application/nonmember/dto/LoginNonMember.kt new file mode 100644 index 0000000..0baea8e --- /dev/null +++ b/Ping-Application/src/main/kotlin/com/ping/application/nonmember/dto/LoginNonMember.kt @@ -0,0 +1,8 @@ +package com.ping.application.nonmember.dto + +class LoginNonMember { + data class Request( + val nonMemberId: Long, + val password: String + ) +} \ No newline at end of file diff --git a/Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/repository/NonMemberRepository.kt b/Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/repository/NonMemberRepository.kt index 2bc37f1..f262150 100644 --- a/Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/repository/NonMemberRepository.kt +++ b/Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/repository/NonMemberRepository.kt @@ -8,4 +8,6 @@ interface NonMemberRepository { fun save(nonMemberDomain: NonMemberDomain): NonMemberDomain fun findAllByShareUrl(shareUrlId: Long): List + + fun findById(nonMemberId: Long): NonMemberDomain? } \ No newline at end of file From 7674f872bd0a0d0bfd457a6e979484120b150390 Mon Sep 17 00:00:00 2001 From: codrin2 Date: Thu, 24 Oct 2024 04:06:36 +0900 Subject: [PATCH 034/203] =?UTF-8?q?feat(NonMemberController):=20=EB=B9=84?= =?UTF-8?q?=ED=9A=8C=EC=9B=90=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20API=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ping/api/nonmember/NonMemberController.kt | 16 ++++++++++++++++ .../nonmember/NonMemberLoginService.kt | 2 +- .../repositoryImpl/NonMemberRepositoryImpl.kt | 6 ++++++ 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/Ping-Api/src/main/kotlin/com/ping/api/nonmember/NonMemberController.kt b/Ping-Api/src/main/kotlin/com/ping/api/nonmember/NonMemberController.kt index 26f40b3..6e24961 100644 --- a/Ping-Api/src/main/kotlin/com/ping/api/nonmember/NonMemberController.kt +++ b/Ping-Api/src/main/kotlin/com/ping/api/nonmember/NonMemberController.kt @@ -1,16 +1,32 @@ package com.ping.api.nonmember +import com.ping.application.nonmember.NonMemberLoginService import com.ping.application.nonmember.NonMemberService import com.ping.application.nonmember.dto.CreateNonMember import com.ping.application.nonmember.dto.GetAllNonMemberPings +import com.ping.application.nonmember.dto.LoginNonMember +import com.ping.common.exception.SuccessResponse +import org.springframework.http.HttpStatus +import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.* @RestController @RequestMapping("/nonmembers") class NonMemberController( private val nonMemberService: NonMemberService, + private val nonMemberLoginService: NonMemberLoginService ) { + @PutMapping("/login") + fun loginNonMember( + @RequestBody request: LoginNonMember.Request + ): ResponseEntity> { + nonMemberLoginService.login(request) + + return ResponseEntity.ok( + SuccessResponse.of(HttpStatus.OK, "비회원 로그인 성공") + ) + } @PostMapping("/pings") fun createNonMemberPings(@RequestBody request: CreateNonMember.Request) { return nonMemberService.createNonMemberPings(request) diff --git a/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberLoginService.kt b/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberLoginService.kt index 2ea3f34..a0e6e20 100644 --- a/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberLoginService.kt +++ b/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberLoginService.kt @@ -13,7 +13,7 @@ class NonMemberLoginService( private val nonMemberRepository: NonMemberRepository, private val validator: NamePasswordValidator ) { - fun loginNonMember(request: LoginNonMember.Request) { + fun login(request: LoginNonMember.Request) { // 비밀번호 형식 검사 (4자리 숫자) validator.password(request.password) diff --git a/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/repositoryImpl/NonMemberRepositoryImpl.kt b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/repositoryImpl/NonMemberRepositoryImpl.kt index 593299c..a8195eb 100644 --- a/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/repositoryImpl/NonMemberRepositoryImpl.kt +++ b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/repositoryImpl/NonMemberRepositoryImpl.kt @@ -23,4 +23,10 @@ class NonMemberRepositoryImpl( override fun findAllByShareUrl(shareUrlId: Long): List { return nonMemberJpaRepository.findAllByShareUrlId(shareUrlId).map { NonMemberMapper.toDomain(it) } } + + override fun findById(nonMemberId: Long): NonMemberDomain? { + return nonMemberJpaRepository.findById(nonMemberId).get().let { + NonMemberMapper.toDomain(it) + } + } } \ No newline at end of file From 59a8cbada6ddeb28e298d0b6bedd1730bc4aae9d Mon Sep 17 00:00:00 2001 From: codrin2 Date: Thu, 24 Oct 2024 16:51:45 +0900 Subject: [PATCH 035/203] =?UTF-8?q?feat(yaml):=20base-url=20=EB=B3=80?= =?UTF-8?q?=EC=88=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Ping-Api/src/main/resources/application-prod.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Ping-Api/src/main/resources/application-prod.yaml b/Ping-Api/src/main/resources/application-prod.yaml index 9009fec..46e7c4a 100644 --- a/Ping-Api/src/main/resources/application-prod.yaml +++ b/Ping-Api/src/main/resources/application-prod.yaml @@ -32,6 +32,10 @@ naver: id: ${NAVER_CLIENT_ID} secret: ${NAVER_CLIENT_SECRET} +pingping: + share: + base-url: ${BASE-URL} + management: endpoints: web: From d0d1778a66bc872ffee71d94164f7c47f2f2bd82 Mon Sep 17 00:00:00 2001 From: codrin2 Date: Thu, 24 Oct 2024 17:05:21 +0900 Subject: [PATCH 036/203] =?UTF-8?q?fix(yaml):=20BASE=5FURL=20=EB=B3=80?= =?UTF-8?q?=EC=88=98=20=EC=9D=B4=EB=A6=84=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Ping-Api/src/main/resources/application-prod.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Ping-Api/src/main/resources/application-prod.yaml b/Ping-Api/src/main/resources/application-prod.yaml index 46e7c4a..8f10cc5 100644 --- a/Ping-Api/src/main/resources/application-prod.yaml +++ b/Ping-Api/src/main/resources/application-prod.yaml @@ -34,7 +34,7 @@ naver: pingping: share: - base-url: ${BASE-URL} + base-url: ${BASE_URL} management: endpoints: From 36b0767c0c09ffd76a1d00af1a33bd3b7f74d269 Mon Sep 17 00:00:00 2001 From: codrin2 Date: Thu, 24 Oct 2024 18:46:46 +0900 Subject: [PATCH 037/203] =?UTF-8?q?fix(PlaceSerice):=20=EC=9E=A5=EC=86=8C?= =?UTF-8?q?=20title=EC=97=90=20=20=EC=82=AD=EC=A0=9C=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Ping-Api/src/main/kotlin/com/ping/api/place/PlaceController.kt | 2 +- .../src/main/kotlin/com/ping/application/place/PlaceService.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Ping-Api/src/main/kotlin/com/ping/api/place/PlaceController.kt b/Ping-Api/src/main/kotlin/com/ping/api/place/PlaceController.kt index 9f75070..facfc23 100644 --- a/Ping-Api/src/main/kotlin/com/ping/api/place/PlaceController.kt +++ b/Ping-Api/src/main/kotlin/com/ping/api/place/PlaceController.kt @@ -19,7 +19,7 @@ class PlaceController( fun searchPlace(@PathVariable("keyword") keyword: String): ResponseEntity>> { val response = placeService.searchPlace(keyword); return ResponseEntity.ok( - SuccessResponse.of(HttpStatus.OK, "공유 URL이 성공적으로 생성되었습니다.", response) + SuccessResponse.of(HttpStatus.OK, "장소 검색에 성공하였습니다.", response) ) } diff --git a/Ping-Application/src/main/kotlin/com/ping/application/place/PlaceService.kt b/Ping-Application/src/main/kotlin/com/ping/application/place/PlaceService.kt index 0383f18..7801e8b 100644 --- a/Ping-Application/src/main/kotlin/com/ping/application/place/PlaceService.kt +++ b/Ping-Application/src/main/kotlin/com/ping/application/place/PlaceService.kt @@ -16,7 +16,7 @@ class PlaceService( fun searchPlace(keyword: String): List { return naverApiClient.searchPlaces(keyword).map { SearchPlace.Response( - name = it.title, + name = it.title.replace("", "").replace("", ""), address = it.address, latitude = it.mapx, longitude = it.mapy From c0b8c7c78b4af95a38db823f4b2a2807a33717fe Mon Sep 17 00:00:00 2001 From: sominyun Date: Fri, 25 Oct 2024 22:53:46 +0900 Subject: [PATCH 038/203] =?UTF-8?q?feat:#15=20rest=20docs=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Ping-Api/build.gradle.kts | 43 ++++++++++++++++++ .../com/ping/api/global/BaseRestDocsTest.kt | 45 +++++++++++++++++++ .../com/ping/api/global/RestDocsConfig.kt | 18 ++++++++ gradlew | 0 4 files changed, 106 insertions(+) create mode 100644 Ping-Api/src/test/kotlin/com/ping/api/global/BaseRestDocsTest.kt create mode 100644 Ping-Api/src/test/kotlin/com/ping/api/global/RestDocsConfig.kt mode change 100644 => 100755 gradlew diff --git a/Ping-Api/build.gradle.kts b/Ping-Api/build.gradle.kts index 7406963..879c633 100644 --- a/Ping-Api/build.gradle.kts +++ b/Ping-Api/build.gradle.kts @@ -1,3 +1,16 @@ +plugins { + `java-test-fixtures` + + id("org.asciidoctor.jvm.convert") version "3.3.2" + +} + +val asciidoctorExt = "asciidoctorExt" +configurations.create(asciidoctorExt) { + extendsFrom(configurations.testImplementation.get()) +} + + dependencies { implementation(project(":Ping-Application")) implementation(project(":Ping-Common")) @@ -7,6 +20,11 @@ dependencies { // MongoDB implementation("org.springframework.boot:spring-boot-starter-data-mongodb") + + //RestDocs + testImplementation("org.springframework.restdocs:spring-restdocs-mockmvc") + asciidoctorExt("org.springframework.restdocs:spring-restdocs-asciidoctor") + testFixturesImplementation("org.springframework.restdocs:spring-restdocs-mockmvc") } tasks { bootJar { @@ -15,4 +33,29 @@ tasks { jar { isEnabled = true } +} + +val snippetDir = file("build/generated-snippets") + +tasks.asciidoctor { + inputs.dir(snippetDir) + dependsOn(tasks.test) + configurations(asciidoctorExt) + baseDirFollowsSourceFile() +} +tasks.bootJar{ + dependsOn(tasks.asciidoctor) + from("build/docs/asciidoc"){ + into("static/docs") + } +} + +tasks.register("copyDocs", Copy::class){ + dependsOn(tasks.bootJar) + from("build/docs/asciidoc") + into("src/main/resources/static/docs") +} + +tasks.build { + dependsOn(tasks.getByName("copyDocs")) } \ No newline at end of file diff --git a/Ping-Api/src/test/kotlin/com/ping/api/global/BaseRestDocsTest.kt b/Ping-Api/src/test/kotlin/com/ping/api/global/BaseRestDocsTest.kt new file mode 100644 index 0000000..b99643e --- /dev/null +++ b/Ping-Api/src/test/kotlin/com/ping/api/global/BaseRestDocsTest.kt @@ -0,0 +1,45 @@ +package com.ping.api.global + +import com.fasterxml.jackson.databind.ObjectMapper +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.extension.ExtendWith +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs +import org.springframework.context.annotation.Import +import org.springframework.restdocs.RestDocumentationContextProvider +import org.springframework.restdocs.RestDocumentationExtension +import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation +import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.setup.DefaultMockMvcBuilder +import org.springframework.test.web.servlet.setup.MockMvcBuilders +import org.springframework.web.context.WebApplicationContext +import org.springframework.web.filter.CharacterEncodingFilter +import java.nio.charset.StandardCharsets + +@AutoConfigureRestDocs +@Import(RestDocsConfig::class) +@ExtendWith(RestDocumentationExtension::class) +abstract class BaseRestDocsTest { + @Autowired + lateinit var resultHandler: RestDocumentationResultHandler + + @Autowired + lateinit var objectMapper: ObjectMapper + + lateinit var mockMvc: MockMvc + + @BeforeEach + fun mockMvcSetup( + applicationContext: WebApplicationContext, + contextProvider: RestDocumentationContextProvider + ){ + mockMvc= MockMvcBuilders.webAppContextSetup(applicationContext) + .apply( + MockMvcRestDocumentation.documentationConfiguration(contextProvider).uris() + .withScheme("https") + ).alwaysDo(resultHandler) + .addFilters(CharacterEncodingFilter(StandardCharsets.UTF_8.name(), true)) + .build() + } +} \ No newline at end of file diff --git a/Ping-Api/src/test/kotlin/com/ping/api/global/RestDocsConfig.kt b/Ping-Api/src/test/kotlin/com/ping/api/global/RestDocsConfig.kt new file mode 100644 index 0000000..b495476 --- /dev/null +++ b/Ping-Api/src/test/kotlin/com/ping/api/global/RestDocsConfig.kt @@ -0,0 +1,18 @@ +package com.ping.api.global + +import org.springframework.boot.test.context.TestConfiguration +import org.springframework.context.annotation.Bean +import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation +import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler +import org.springframework.restdocs.operation.preprocess.Preprocessors + +@TestConfiguration +class RestDocsConfig { + + @Bean + fun restDocumentationResultHandler(): RestDocumentationResultHandler = MockMvcRestDocumentation.document( + "{ClassName}/{methodName}", + Preprocessors.preprocessRequest(Preprocessors.prettyPrint()), + Preprocessors.preprocessResponse(Preprocessors.prettyPrint()) + ) +} \ No newline at end of file diff --git a/gradlew b/gradlew old mode 100644 new mode 100755 From eac908adaf7e8f0b317e8eb1f0a3571717d84012 Mon Sep 17 00:00:00 2001 From: sominyun Date: Fri, 25 Oct 2024 22:54:34 +0900 Subject: [PATCH 039/203] feat:#15 nonMemberController test code --- Ping-Api/src/docs/asciidoc/NonMember.adoc | 10 + Ping-Api/src/docs/asciidoc/index.adoc | 9 + .../main/resources/static/docs/NonMember.html | 667 +++++++++++++++++ .../src/main/resources/static/docs/index.html | 691 ++++++++++++++++++ .../api/nonmember/NonMemberControllerTest.kt | 129 ++++ 5 files changed, 1506 insertions(+) create mode 100644 Ping-Api/src/docs/asciidoc/NonMember.adoc create mode 100644 Ping-Api/src/docs/asciidoc/index.adoc create mode 100644 Ping-Api/src/main/resources/static/docs/NonMember.html create mode 100644 Ping-Api/src/main/resources/static/docs/index.html create mode 100644 Ping-Api/src/test/kotlin/com/ping/api/nonmember/NonMemberControllerTest.kt diff --git a/Ping-Api/src/docs/asciidoc/NonMember.adoc b/Ping-Api/src/docs/asciidoc/NonMember.adoc new file mode 100644 index 0000000..9360ef8 --- /dev/null +++ b/Ping-Api/src/docs/asciidoc/NonMember.adoc @@ -0,0 +1,10 @@ +[[NonMembers-API]] +== NonMembers API + +[[Post-NonMemberPings]] +=== 비회원 핑 생성 +operation::NonMemberControllerTest/createNonMemberPings[snippets='HTTP-request,request-fields,HTTP-response'] + +[[Get-NonMemberPings]] +=== 전체 핑 불러오기 +operation::NonMemberControllerTest/getAllNonMemberPings[snippets='HTTP-request,HTTP-response,response-fields'] \ No newline at end of file diff --git a/Ping-Api/src/docs/asciidoc/index.adoc b/Ping-Api/src/docs/asciidoc/index.adoc new file mode 100644 index 0000000..a79117a --- /dev/null +++ b/Ping-Api/src/docs/asciidoc/index.adoc @@ -0,0 +1,9 @@ += Moping-Backend API Docs +:doctype: book +:icons: font +:source-highlighter: highlightjs +:toc: left +:toclevels: 2 +:sectlinks: + +include::NonMember.adoc[] \ No newline at end of file diff --git a/Ping-Api/src/main/resources/static/docs/NonMember.html b/Ping-Api/src/main/resources/static/docs/NonMember.html new file mode 100644 index 0000000..4ee74bb --- /dev/null +++ b/Ping-Api/src/main/resources/static/docs/NonMember.html @@ -0,0 +1,667 @@ + + + + + + + +NonMembers API + + + + + +
+
+

NonMembers API

+
+
+

비회원 핑 생성

+
+

Http request

+
+
+
POST /nonmembers/pings HTTP/1.1
+Content-Type: application/json;charset=UTF-8
+Content-Length: 227
+Host: localhost:8080
+
+{
+  "uuid" : "test",
+  "name" : "윤소민",
+  "password" : "1234",
+  "bookmarkUrls" : [ "https://naver.me/Fqimcb8B", "https://naver.me/xUwGH5c3" ],
+  "storeUrls" : [ "https://naver.me/GuGEom4T", "https://naver.me/FuVzL1bq" ]
+}
+
+
+
+
+

Request fields

+ +++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PathTypeDescription

uuid

String

공유 url 뒤에 붙는 uuid

name

String

비회원 이름

password

String

비회원 비밀번호

bookmarkUrls

Array

네이버 북마크 링크 리스트

storeUrls

Array

네이버 개별지도 링크 리스트

+
+
+

Http response

+
+
+
HTTP/1.1 200 OK
+Vary: Origin
+Vary: Access-Control-Request-Method
+Vary: Access-Control-Request-Headers
+
+
+
+
+
+

전체 핑 불러오기

+
+

Http request

+
+
+
GET /nonmembers/pings?uuid=test HTTP/1.1
+Content-Type: application/json;charset=UTF-8
+Host: localhost:8080
+
+
+
+
+

Http response

+
+
+
HTTP/1.1 200 OK
+Vary: Origin
+Vary: Access-Control-Request-Method
+Vary: Access-Control-Request-Headers
+Content-Type: application/json;charset=UTF-8
+Content-Length: 758
+
+{
+  "eventName" : "핑핑이들 여행",
+  "nonMembers" : [ {
+    "nonMemberId" : 1,
+    "name" : "핑핑이1"
+  }, {
+    "nonMemberId" : 2,
+    "name" : "핑핑이2"
+  } ],
+  "pings" : [ {
+    "iconLevel" : 2,
+    "nonMembers" : [ {
+      "nonMemberId" : 1,
+      "name" : "핑핑이1"
+    }, {
+      "nonMemberId" : 2,
+      "name" : "핑핑이2"
+    } ],
+    "url" : "https://map.naver.com/p/entry/place/1946678040",
+    "placeName" : "호이",
+    "px" : 126.971178,
+    "py" : 37.5302481
+  }, {
+    "iconLevel" : 1,
+    "nonMembers" : [ {
+      "nonMemberId" : 1,
+      "name" : "핑핑이1"
+    } ],
+    "url" : "https://map.naver.com/p/entry/place/1492901893",
+    "placeName" : "퍼즈앤스틸",
+    "px" : 126.9713426,
+    "py" : 37.5303303
+  } ]
+}
+
+
+
+
+

Response fields

+ +++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PathTypeDescription

eventName

String

이벤트 이름

nonMembers[].nonMemberId

Number

비회원의 id

nonMembers[].name

String

비회원 이름

pings[].iconLevel

Number

아이콘 레벨 +4:가장 많이 겹침 +3:그다음 +2:그다음 +1:나머지

pings[].nonMembers[].nonMemberId

Number

비회원 id

pings[].nonMembers[].name

String

비회원 이름

pings[].url

String

장소 url

pings[].placeName

String

장소 이름

pings[].px

Number

위도

pings[].py

Number

경도

+
+
+
+
+
+ + + \ No newline at end of file diff --git a/Ping-Api/src/main/resources/static/docs/index.html b/Ping-Api/src/main/resources/static/docs/index.html new file mode 100644 index 0000000..0d74720 --- /dev/null +++ b/Ping-Api/src/main/resources/static/docs/index.html @@ -0,0 +1,691 @@ + + + + + + + +Moping-Backend API Docs + + + + + + + +
+
+

NonMembers API

+
+
+

비회원 핑 생성

+
+

Http request

+
+
+
POST /nonmembers/pings HTTP/1.1
+Content-Type: application/json;charset=UTF-8
+Content-Length: 227
+Host: localhost:8080
+
+{
+  "uuid" : "test",
+  "name" : "윤소민",
+  "password" : "1234",
+  "bookmarkUrls" : [ "https://naver.me/Fqimcb8B", "https://naver.me/xUwGH5c3" ],
+  "storeUrls" : [ "https://naver.me/GuGEom4T", "https://naver.me/FuVzL1bq" ]
+}
+
+
+
+
+

Request fields

+ +++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PathTypeDescription

uuid

String

공유 url 뒤에 붙는 uuid

name

String

비회원 이름

password

String

비회원 비밀번호

bookmarkUrls

Array

네이버 북마크 링크 리스트

storeUrls

Array

네이버 개별지도 링크 리스트

+
+
+

Http response

+
+
+
HTTP/1.1 200 OK
+Vary: Origin
+Vary: Access-Control-Request-Method
+Vary: Access-Control-Request-Headers
+
+
+
+
+
+

전체 핑 불러오기

+
+

Http request

+
+
+
GET /nonmembers/pings?uuid=test HTTP/1.1
+Content-Type: application/json;charset=UTF-8
+Host: localhost:8080
+
+
+
+
+

Http response

+
+
+
HTTP/1.1 200 OK
+Vary: Origin
+Vary: Access-Control-Request-Method
+Vary: Access-Control-Request-Headers
+Content-Type: application/json;charset=UTF-8
+Content-Length: 758
+
+{
+  "eventName" : "핑핑이들 여행",
+  "nonMembers" : [ {
+    "nonMemberId" : 1,
+    "name" : "핑핑이1"
+  }, {
+    "nonMemberId" : 2,
+    "name" : "핑핑이2"
+  } ],
+  "pings" : [ {
+    "iconLevel" : 2,
+    "nonMembers" : [ {
+      "nonMemberId" : 1,
+      "name" : "핑핑이1"
+    }, {
+      "nonMemberId" : 2,
+      "name" : "핑핑이2"
+    } ],
+    "url" : "https://map.naver.com/p/entry/place/1946678040",
+    "placeName" : "호이",
+    "px" : 126.971178,
+    "py" : 37.5302481
+  }, {
+    "iconLevel" : 1,
+    "nonMembers" : [ {
+      "nonMemberId" : 1,
+      "name" : "핑핑이1"
+    } ],
+    "url" : "https://map.naver.com/p/entry/place/1492901893",
+    "placeName" : "퍼즈앤스틸",
+    "px" : 126.9713426,
+    "py" : 37.5303303
+  } ]
+}
+
+
+
+
+

Response fields

+ +++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PathTypeDescription

eventName

String

이벤트 이름

nonMembers[].nonMemberId

Number

비회원의 id

nonMembers[].name

String

비회원 이름

pings[].iconLevel

Number

아이콘 레벨 +4:가장 많이 겹침 +3:그다음 +2:그다음 +1:나머지

pings[].nonMembers[].nonMemberId

Number

비회원 id

pings[].nonMembers[].name

String

비회원 이름

pings[].url

String

장소 url

pings[].placeName

String

장소 이름

pings[].px

Number

위도

pings[].py

Number

경도

+
+
+
+
+
+ + + + + \ No newline at end of file diff --git a/Ping-Api/src/test/kotlin/com/ping/api/nonmember/NonMemberControllerTest.kt b/Ping-Api/src/test/kotlin/com/ping/api/nonmember/NonMemberControllerTest.kt new file mode 100644 index 0000000..80b91dc --- /dev/null +++ b/Ping-Api/src/test/kotlin/com/ping/api/nonmember/NonMemberControllerTest.kt @@ -0,0 +1,129 @@ +package com.ping.api.nonmember + +import com.ping.api.global.BaseRestDocsTest +import com.ping.application.nonmember.NonMemberLoginService +import com.ping.application.nonmember.NonMemberService +import com.ping.application.nonmember.dto.CreateNonMember +import com.ping.application.nonmember.dto.GetAllNonMemberPings +import com.ping.infra.nonmember.domain.mongo.repository.BookmarkMongoRepository +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.mockito.BDDMockito.given +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest +import org.springframework.boot.test.mock.mockito.MockBean +import org.springframework.http.MediaType +import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders +import org.springframework.restdocs.payload.PayloadDocumentation.* +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status + +@WebMvcTest(NonMemberController::class) +class NonMemberControllerTest : BaseRestDocsTest() { + + @MockBean + private lateinit var nonMemberService: NonMemberService + + @MockBean + private lateinit var nonMemberLoginService: NonMemberLoginService + + @MockBean + private lateinit var bookmarkMongoRepository: BookmarkMongoRepository + + + @Test + @DisplayName("비회원 핑 생성") + fun createNonMemberPings() { + // given + val createNonMemberRequest = CreateNonMember.Request( + uuid = "test", + name = "윤소민", + password = "1234", + bookmarkUrls = listOf("https://naver.me/Fqimcb8B", "https://naver.me/xUwGH5c3"), + storeUrls = listOf("https://naver.me/GuGEom4T", "https://naver.me/FuVzL1bq") + ) + val request = RestDocumentationRequestBuilders.post("/nonmembers/pings") + .contentType(MediaType.APPLICATION_JSON_VALUE) + .content(objectMapper.writeValueAsString(createNonMemberRequest)) + + //when + val result = mockMvc.perform(request) + + // then + result.andExpect(status().isOk) + .andDo( + resultHandler.document( + requestFields( + fieldWithPath("uuid").description("공유 url 뒤에 붙는 uuid"), + fieldWithPath("name").description("비회원 이름"), + fieldWithPath("password").description("비회원 비밀번호"), + fieldWithPath("bookmarkUrls").description("네이버 북마크 링크 리스트"), + fieldWithPath("storeUrls").description("네이버 개별지도 링크 리스트"), + ) + ) + ) + } + + @Test + @DisplayName("전체 핑 불러오기") + fun getAllNonMemberPings() { + // given + val uuid = "test" + val request = RestDocumentationRequestBuilders.get("/nonmembers/pings") + .contentType(MediaType.APPLICATION_JSON_VALUE) + .queryParam("uuid", uuid) + + val getAllNonMemberPings = GetAllNonMemberPings.Response( + eventName = "핑핑이들 여행", + nonMembers = listOf( + GetAllNonMemberPings.NonMember(nonMemberId = 1, name = "핑핑이1"), + GetAllNonMemberPings.NonMember(nonMemberId = 2, name = "핑핑이2") + ), + pings = listOf( + GetAllNonMemberPings.Ping( + iconLevel = 2, + nonMembers = listOf( + GetAllNonMemberPings.NonMember(nonMemberId = 1, name = "핑핑이1"), + GetAllNonMemberPings.NonMember(nonMemberId = 2, name = "핑핑이2") + ), + url = "https://map.naver.com/p/entry/place/1946678040", + placeName = "호이", + px = 126.971178, + py = 37.5302481 + ), + GetAllNonMemberPings.Ping( + iconLevel = 1, + nonMembers = listOf( + GetAllNonMemberPings.NonMember(nonMemberId = 1, name = "핑핑이1"), + ), + url = "https://map.naver.com/p/entry/place/1492901893", + placeName = "퍼즈앤스틸", + px = 126.9713426, + py = 37.5303303 + ) + ) + ) + + given(nonMemberService.getAllNonMemberPings(uuid)).willReturn(getAllNonMemberPings) + + //when + val result = mockMvc.perform(request) + + // then + result.andExpect(status().isOk) + .andDo( + resultHandler.document( + responseFields( + fieldWithPath("eventName").description("이벤트 이름"), + fieldWithPath("nonMembers[].nonMemberId").description("비회원의 id"), + fieldWithPath("nonMembers[].name").description("비회원 이름"), + fieldWithPath("pings[].iconLevel").description("아이콘 레벨\n4:가장 많이 겹침\n3:그다음\n2:그다음\n1:나머지"), + fieldWithPath("pings[].nonMembers[].nonMemberId").description("비회원 id"), + fieldWithPath("pings[].nonMembers[].name").description("비회원 이름"), + fieldWithPath("pings[].url").description("장소 url"), + fieldWithPath("pings[].placeName").description("장소 이름"), + fieldWithPath("pings[].px").description("위도"), + fieldWithPath("pings[].py").description("경도"), + ) + ) + ) + } +} \ No newline at end of file From 6008cf53b7f8fb74b74e339fee116fc70e86e840 Mon Sep 17 00:00:00 2001 From: sominyun Date: Fri, 25 Oct 2024 22:54:45 +0900 Subject: [PATCH 040/203] =?UTF-8?q?fix:#15=20=EC=A4=84=EA=B0=84=EA=B2=A9?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/kotlin/com/ping/api/nonmember/NonMemberController.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/Ping-Api/src/main/kotlin/com/ping/api/nonmember/NonMemberController.kt b/Ping-Api/src/main/kotlin/com/ping/api/nonmember/NonMemberController.kt index 6e24961..37c1d55 100644 --- a/Ping-Api/src/main/kotlin/com/ping/api/nonmember/NonMemberController.kt +++ b/Ping-Api/src/main/kotlin/com/ping/api/nonmember/NonMemberController.kt @@ -30,7 +30,6 @@ class NonMemberController( @PostMapping("/pings") fun createNonMemberPings(@RequestBody request: CreateNonMember.Request) { return nonMemberService.createNonMemberPings(request) - } @GetMapping("/pings") From 426c67ba04b5aab4ac0408e3e1208ae093c4f20a Mon Sep 17 00:00:00 2001 From: sominyun Date: Fri, 25 Oct 2024 22:59:01 +0900 Subject: [PATCH 041/203] feat:#15 git ignore --- .gitignore | 6 +- .../main/resources/static/docs/NonMember.html | 667 ----------------- .../src/main/resources/static/docs/index.html | 691 ------------------ 3 files changed, 5 insertions(+), 1359 deletions(-) delete mode 100644 Ping-Api/src/main/resources/static/docs/NonMember.html delete mode 100644 Ping-Api/src/main/resources/static/docs/index.html diff --git a/.gitignore b/.gitignore index eef43d8..593bd78 100644 --- a/.gitignore +++ b/.gitignore @@ -44,4 +44,8 @@ out/ ### applicaion 제외 ### src/main/resources/application.yaml -Ping-Api/src/main/resources/application.yaml \ No newline at end of file +Ping-Api/src/main/resources/application.yaml + + +### rest docs ### +**/src/main/resources/static/docs/ \ No newline at end of file diff --git a/Ping-Api/src/main/resources/static/docs/NonMember.html b/Ping-Api/src/main/resources/static/docs/NonMember.html deleted file mode 100644 index 4ee74bb..0000000 --- a/Ping-Api/src/main/resources/static/docs/NonMember.html +++ /dev/null @@ -1,667 +0,0 @@ - - - - - - - -NonMembers API - - - - - -
-
-

NonMembers API

-
-
-

비회원 핑 생성

-
-

Http request

-
-
-
POST /nonmembers/pings HTTP/1.1
-Content-Type: application/json;charset=UTF-8
-Content-Length: 227
-Host: localhost:8080
-
-{
-  "uuid" : "test",
-  "name" : "윤소민",
-  "password" : "1234",
-  "bookmarkUrls" : [ "https://naver.me/Fqimcb8B", "https://naver.me/xUwGH5c3" ],
-  "storeUrls" : [ "https://naver.me/GuGEom4T", "https://naver.me/FuVzL1bq" ]
-}
-
-
-
-
-

Request fields

- ----- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
PathTypeDescription

uuid

String

공유 url 뒤에 붙는 uuid

name

String

비회원 이름

password

String

비회원 비밀번호

bookmarkUrls

Array

네이버 북마크 링크 리스트

storeUrls

Array

네이버 개별지도 링크 리스트

-
-
-

Http response

-
-
-
HTTP/1.1 200 OK
-Vary: Origin
-Vary: Access-Control-Request-Method
-Vary: Access-Control-Request-Headers
-
-
-
-
-
-

전체 핑 불러오기

-
-

Http request

-
-
-
GET /nonmembers/pings?uuid=test HTTP/1.1
-Content-Type: application/json;charset=UTF-8
-Host: localhost:8080
-
-
-
-
-

Http response

-
-
-
HTTP/1.1 200 OK
-Vary: Origin
-Vary: Access-Control-Request-Method
-Vary: Access-Control-Request-Headers
-Content-Type: application/json;charset=UTF-8
-Content-Length: 758
-
-{
-  "eventName" : "핑핑이들 여행",
-  "nonMembers" : [ {
-    "nonMemberId" : 1,
-    "name" : "핑핑이1"
-  }, {
-    "nonMemberId" : 2,
-    "name" : "핑핑이2"
-  } ],
-  "pings" : [ {
-    "iconLevel" : 2,
-    "nonMembers" : [ {
-      "nonMemberId" : 1,
-      "name" : "핑핑이1"
-    }, {
-      "nonMemberId" : 2,
-      "name" : "핑핑이2"
-    } ],
-    "url" : "https://map.naver.com/p/entry/place/1946678040",
-    "placeName" : "호이",
-    "px" : 126.971178,
-    "py" : 37.5302481
-  }, {
-    "iconLevel" : 1,
-    "nonMembers" : [ {
-      "nonMemberId" : 1,
-      "name" : "핑핑이1"
-    } ],
-    "url" : "https://map.naver.com/p/entry/place/1492901893",
-    "placeName" : "퍼즈앤스틸",
-    "px" : 126.9713426,
-    "py" : 37.5303303
-  } ]
-}
-
-
-
-
-

Response fields

- ----- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
PathTypeDescription

eventName

String

이벤트 이름

nonMembers[].nonMemberId

Number

비회원의 id

nonMembers[].name

String

비회원 이름

pings[].iconLevel

Number

아이콘 레벨 -4:가장 많이 겹침 -3:그다음 -2:그다음 -1:나머지

pings[].nonMembers[].nonMemberId

Number

비회원 id

pings[].nonMembers[].name

String

비회원 이름

pings[].url

String

장소 url

pings[].placeName

String

장소 이름

pings[].px

Number

위도

pings[].py

Number

경도

-
-
-
-
-
- - - \ No newline at end of file diff --git a/Ping-Api/src/main/resources/static/docs/index.html b/Ping-Api/src/main/resources/static/docs/index.html deleted file mode 100644 index 0d74720..0000000 --- a/Ping-Api/src/main/resources/static/docs/index.html +++ /dev/null @@ -1,691 +0,0 @@ - - - - - - - -Moping-Backend API Docs - - - - - - - -
-
-

NonMembers API

-
-
-

비회원 핑 생성

-
-

Http request

-
-
-
POST /nonmembers/pings HTTP/1.1
-Content-Type: application/json;charset=UTF-8
-Content-Length: 227
-Host: localhost:8080
-
-{
-  "uuid" : "test",
-  "name" : "윤소민",
-  "password" : "1234",
-  "bookmarkUrls" : [ "https://naver.me/Fqimcb8B", "https://naver.me/xUwGH5c3" ],
-  "storeUrls" : [ "https://naver.me/GuGEom4T", "https://naver.me/FuVzL1bq" ]
-}
-
-
-
-
-

Request fields

- ----- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
PathTypeDescription

uuid

String

공유 url 뒤에 붙는 uuid

name

String

비회원 이름

password

String

비회원 비밀번호

bookmarkUrls

Array

네이버 북마크 링크 리스트

storeUrls

Array

네이버 개별지도 링크 리스트

-
-
-

Http response

-
-
-
HTTP/1.1 200 OK
-Vary: Origin
-Vary: Access-Control-Request-Method
-Vary: Access-Control-Request-Headers
-
-
-
-
-
-

전체 핑 불러오기

-
-

Http request

-
-
-
GET /nonmembers/pings?uuid=test HTTP/1.1
-Content-Type: application/json;charset=UTF-8
-Host: localhost:8080
-
-
-
-
-

Http response

-
-
-
HTTP/1.1 200 OK
-Vary: Origin
-Vary: Access-Control-Request-Method
-Vary: Access-Control-Request-Headers
-Content-Type: application/json;charset=UTF-8
-Content-Length: 758
-
-{
-  "eventName" : "핑핑이들 여행",
-  "nonMembers" : [ {
-    "nonMemberId" : 1,
-    "name" : "핑핑이1"
-  }, {
-    "nonMemberId" : 2,
-    "name" : "핑핑이2"
-  } ],
-  "pings" : [ {
-    "iconLevel" : 2,
-    "nonMembers" : [ {
-      "nonMemberId" : 1,
-      "name" : "핑핑이1"
-    }, {
-      "nonMemberId" : 2,
-      "name" : "핑핑이2"
-    } ],
-    "url" : "https://map.naver.com/p/entry/place/1946678040",
-    "placeName" : "호이",
-    "px" : 126.971178,
-    "py" : 37.5302481
-  }, {
-    "iconLevel" : 1,
-    "nonMembers" : [ {
-      "nonMemberId" : 1,
-      "name" : "핑핑이1"
-    } ],
-    "url" : "https://map.naver.com/p/entry/place/1492901893",
-    "placeName" : "퍼즈앤스틸",
-    "px" : 126.9713426,
-    "py" : 37.5303303
-  } ]
-}
-
-
-
-
-

Response fields

- ----- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
PathTypeDescription

eventName

String

이벤트 이름

nonMembers[].nonMemberId

Number

비회원의 id

nonMembers[].name

String

비회원 이름

pings[].iconLevel

Number

아이콘 레벨 -4:가장 많이 겹침 -3:그다음 -2:그다음 -1:나머지

pings[].nonMembers[].nonMemberId

Number

비회원 id

pings[].nonMembers[].name

String

비회원 이름

pings[].url

String

장소 url

pings[].placeName

String

장소 이름

pings[].px

Number

위도

pings[].py

Number

경도

-
-
-
-
-
- - - - - \ No newline at end of file From 21fc34883a779e75595386a91d1f07581ab06758 Mon Sep 17 00:00:00 2001 From: codrin2 Date: Sun, 27 Oct 2024 13:04:17 +0900 Subject: [PATCH 042/203] =?UTF-8?q?fix:=20NonMemberValidator=20=EC=9E=98?= =?UTF-8?q?=EB=AA=BB=EB=90=9C=20=EC=9D=B4=EB=A6=84=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/com/ping/application/nonmember/NonMemberValidator.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberValidator.kt b/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberValidator.kt index cdb76e2..6384113 100644 --- a/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberValidator.kt +++ b/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberValidator.kt @@ -5,7 +5,7 @@ import com.ping.common.exception.ExceptionContent import org.springframework.stereotype.Component @Component -class NamePasswordValidator { +class NonMemberValidator { // 이름 유효성 검증 fun name(name: String) { From 4d52430d96ada02df25d43ef5f5175c58364efad Mon Sep 17 00:00:00 2001 From: codrin2 Date: Sun, 27 Oct 2024 15:37:54 +0900 Subject: [PATCH 043/203] =?UTF-8?q?feat:=20NonMemberUpdateStatusDomain=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/nonmember/NonMemberService.kt | 2 +- .../aggregate/NonMemberUpdateStatusDomain.kt | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/aggregate/NonMemberUpdateStatusDomain.kt diff --git a/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberService.kt b/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberService.kt index 5d5c7d6..45ff9aa 100644 --- a/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberService.kt +++ b/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberService.kt @@ -21,7 +21,7 @@ class NonMemberService( private val nonMemberBookmarkUrlRepository: NonMemberBookmarkUrlRepository, private val nonMemberStoreUrlRepository: NonMemberStoreUrlRepository, private val naverMapClient: NaverMapClient, - private val validator: NamePasswordValidator + private val validator: NonMemberValidator ) { @Transactional fun createNonMemberPings(request: CreateNonMember.Request) { diff --git a/Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/aggregate/NonMemberUpdateStatusDomain.kt b/Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/aggregate/NonMemberUpdateStatusDomain.kt new file mode 100644 index 0000000..da0bca9 --- /dev/null +++ b/Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/aggregate/NonMemberUpdateStatusDomain.kt @@ -0,0 +1,17 @@ +package com.ping.domain.nonmember.aggregate + +data class NonMemberUpdateStatusDomain( + val id: Long, + val nonMemberId: Long, + val friendId: Long, + var isUpdate: Boolean +) { + companion object { + fun of(nonMemberId: Long, friendId: Long, isUpdate: Boolean) = NonMemberUpdateStatusDomain( + id = 0L, + nonMemberId = nonMemberId, + friendId = friendId, + isUpdate = isUpdate + ) + } +} \ No newline at end of file From 9a82219668855f6b993ef29308a604414ad15468 Mon Sep 17 00:00:00 2001 From: codrin2 Date: Sun, 27 Oct 2024 15:51:12 +0900 Subject: [PATCH 044/203] =?UTF-8?q?feat:=20NonMemberUpdateStatusEntity=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../jpa/entity/NonMemberUpdateStatusEntity.kt | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/jpa/entity/NonMemberUpdateStatusEntity.kt diff --git a/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/jpa/entity/NonMemberUpdateStatusEntity.kt b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/jpa/entity/NonMemberUpdateStatusEntity.kt new file mode 100644 index 0000000..db771d1 --- /dev/null +++ b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/jpa/entity/NonMemberUpdateStatusEntity.kt @@ -0,0 +1,23 @@ +package com.ping.infra.nonmember.domain.jpa.entity + +import com.ping.infra.nonmember.domain.jpa.common.BaseTimeEntity +import jakarta.persistence.* + + +@Entity +@Table(name = "non_member_update_status") +data class NonMemberUpdateStatusEntity( + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + val id: Long, + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "non_member_id", nullable = false) + val nonMember: NonMemberEntity, + + @Column(nullable = false) + val friendId: Long, + + @Column(nullable = false) + var isUpdate: Boolean = false +) : BaseTimeEntity() \ No newline at end of file From 5e78d07dbf81d30ca40e3101357fdf02566a683f Mon Sep 17 00:00:00 2001 From: codrin2 Date: Sun, 27 Oct 2024 15:57:19 +0900 Subject: [PATCH 045/203] =?UTF-8?q?feat:=20domain=20=EA=B3=84=EC=B8=B5?= =?UTF-8?q?=EC=9D=98=20NonMemberUpdateStatusRepository=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repository/NonMemberUpdateStatusRepository.kt | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/repository/NonMemberUpdateStatusRepository.kt diff --git a/Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/repository/NonMemberUpdateStatusRepository.kt b/Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/repository/NonMemberUpdateStatusRepository.kt new file mode 100644 index 0000000..95813bd --- /dev/null +++ b/Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/repository/NonMemberUpdateStatusRepository.kt @@ -0,0 +1,8 @@ +package com.ping.domain.nonmember.repository + +import com.ping.domain.nonmember.aggregate.NonMemberUpdateStatusDomain + +interface NonMemberUpdateStatusRepository { + fun findAllByNonMemberId(nonMemberId: Long): List + fun saveAll(statuses: List): List +} \ No newline at end of file From 74e4e9a363deb93dfbbd1c62c547fc7bd21573b3 Mon Sep 17 00:00:00 2001 From: codrin2 Date: Sun, 27 Oct 2024 16:16:36 +0900 Subject: [PATCH 046/203] =?UTF-8?q?feat:=20infra=20=EA=B3=84=EC=B8=B5?= =?UTF-8?q?=EC=9D=98=20NonMemberUpdateStatus=20RepositoryImpl,=20Mapper,?= =?UTF-8?q?=20JpaRepository=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../aggregate/NonMemberUpdateStatusDomain.kt | 6 ++--- .../NonMemberUpdateStatusJpaRepository.kt | 8 +++++++ .../mapper/NonMemberUpdateStatusMapper.kt | 21 ++++++++++++++++++ .../NonMemberUpdateStatusRepositoryImpl.kt | 22 +++++++++++++++++++ 4 files changed, 54 insertions(+), 3 deletions(-) create mode 100644 Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/jpa/repository/NonMemberUpdateStatusJpaRepository.kt create mode 100644 Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/mapper/NonMemberUpdateStatusMapper.kt create mode 100644 Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/repositoryImpl/NonMemberUpdateStatusRepositoryImpl.kt diff --git a/Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/aggregate/NonMemberUpdateStatusDomain.kt b/Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/aggregate/NonMemberUpdateStatusDomain.kt index da0bca9..f1498f7 100644 --- a/Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/aggregate/NonMemberUpdateStatusDomain.kt +++ b/Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/aggregate/NonMemberUpdateStatusDomain.kt @@ -2,14 +2,14 @@ package com.ping.domain.nonmember.aggregate data class NonMemberUpdateStatusDomain( val id: Long, - val nonMemberId: Long, + val nonMemberDomain: NonMemberDomain, val friendId: Long, var isUpdate: Boolean ) { companion object { - fun of(nonMemberId: Long, friendId: Long, isUpdate: Boolean) = NonMemberUpdateStatusDomain( + fun of(nonMemberDomain: NonMemberDomain, friendId: Long, isUpdate: Boolean) = NonMemberUpdateStatusDomain( id = 0L, - nonMemberId = nonMemberId, + nonMemberDomain = nonMemberDomain, friendId = friendId, isUpdate = isUpdate ) diff --git a/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/jpa/repository/NonMemberUpdateStatusJpaRepository.kt b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/jpa/repository/NonMemberUpdateStatusJpaRepository.kt new file mode 100644 index 0000000..a541a06 --- /dev/null +++ b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/jpa/repository/NonMemberUpdateStatusJpaRepository.kt @@ -0,0 +1,8 @@ +package com.ping.infra.nonmember.domain.jpa.repository + +import com.ping.infra.nonmember.domain.jpa.entity.NonMemberUpdateStatusEntity +import org.springframework.data.jpa.repository.JpaRepository + +interface NonMemberUpdateStatusJpaRepository : JpaRepository { + fun findAllByNonMemberId(nonMemberId: Long): List +} \ No newline at end of file diff --git a/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/mapper/NonMemberUpdateStatusMapper.kt b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/mapper/NonMemberUpdateStatusMapper.kt new file mode 100644 index 0000000..ce3dba0 --- /dev/null +++ b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/mapper/NonMemberUpdateStatusMapper.kt @@ -0,0 +1,21 @@ +package com.ping.infra.nonmember.domain.mapper + +import com.ping.domain.nonmember.aggregate.NonMemberUpdateStatusDomain +import com.ping.infra.nonmember.domain.jpa.entity.NonMemberUpdateStatusEntity + +object NonMemberUpdateStatusMapper { + + fun toDomain(entity: NonMemberUpdateStatusEntity) = NonMemberUpdateStatusDomain( + id = entity.id, + nonMemberDomain = NonMemberMapper.toDomain(entity.nonMember), + friendId = entity.friendId, + isUpdate = entity.isUpdate + ) + + fun toEntity(domain: NonMemberUpdateStatusDomain) = NonMemberUpdateStatusEntity( + id = domain.id, + nonMember = NonMemberMapper.toEntity(domain.nonMemberDomain), + friendId = domain.friendId, + isUpdate = domain.isUpdate + ) +} \ No newline at end of file diff --git a/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/repositoryImpl/NonMemberUpdateStatusRepositoryImpl.kt b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/repositoryImpl/NonMemberUpdateStatusRepositoryImpl.kt new file mode 100644 index 0000000..ed5eb3c --- /dev/null +++ b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/repositoryImpl/NonMemberUpdateStatusRepositoryImpl.kt @@ -0,0 +1,22 @@ +package com.ping.infra.nonmember.domain.repositoryImpl + +import com.ping.domain.nonmember.aggregate.NonMemberUpdateStatusDomain +import com.ping.domain.nonmember.repository.NonMemberUpdateStatusRepository +import com.ping.infra.nonmember.domain.jpa.repository.NonMemberUpdateStatusJpaRepository +import com.ping.infra.nonmember.domain.mapper.NonMemberUpdateStatusMapper +import org.springframework.stereotype.Repository + +@Repository +class NonMemberUpdateStatusRepositoryImpl( + private val jpaRepository: NonMemberUpdateStatusJpaRepository +) : NonMemberUpdateStatusRepository { + + override fun findAllByNonMemberId(nonMemberId: Long): List { + return jpaRepository.findAllByNonMemberId(nonMemberId).map { NonMemberUpdateStatusMapper.toDomain(it) } + } + + override fun saveAll(statuses: List): List { + val entities = statuses.map { NonMemberUpdateStatusMapper.toEntity(it) } + return jpaRepository.saveAll(entities).map { NonMemberUpdateStatusMapper.toDomain(it) } + } +} \ No newline at end of file From ce4d740a69cbc928e2060202325b9fa437be9ea4 Mon Sep 17 00:00:00 2001 From: sominyun Date: Sun, 27 Oct 2024 16:59:33 +0900 Subject: [PATCH 047/203] =?UTF-8?q?feat:#15=20uri=20=EC=83=81=EC=88=98?= =?UTF-8?q?=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/kotlin/com/ping/api/nonmember/NonMemberApi.kt | 6 ++++++ .../kotlin/com/ping/api/nonmember/NonMemberController.kt | 9 ++++----- .../com/ping/api/nonmember/NonMemberControllerTest.kt | 4 ++-- 3 files changed, 12 insertions(+), 7 deletions(-) create mode 100644 Ping-Api/src/main/kotlin/com/ping/api/nonmember/NonMemberApi.kt diff --git a/Ping-Api/src/main/kotlin/com/ping/api/nonmember/NonMemberApi.kt b/Ping-Api/src/main/kotlin/com/ping/api/nonmember/NonMemberApi.kt new file mode 100644 index 0000000..29bb6c5 --- /dev/null +++ b/Ping-Api/src/main/kotlin/com/ping/api/nonmember/NonMemberApi.kt @@ -0,0 +1,6 @@ +package com.ping.api.nonmember + +object NonMemberApi { + const val BASE_URL = "/nonmembers" + const val PING = "$BASE_URL/pings" +} \ No newline at end of file diff --git a/Ping-Api/src/main/kotlin/com/ping/api/nonmember/NonMemberController.kt b/Ping-Api/src/main/kotlin/com/ping/api/nonmember/NonMemberController.kt index 37c1d55..ac700ce 100644 --- a/Ping-Api/src/main/kotlin/com/ping/api/nonmember/NonMemberController.kt +++ b/Ping-Api/src/main/kotlin/com/ping/api/nonmember/NonMemberController.kt @@ -11,13 +11,11 @@ import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.* @RestController -@RequestMapping("/nonmembers") class NonMemberController( private val nonMemberService: NonMemberService, private val nonMemberLoginService: NonMemberLoginService ) { - - @PutMapping("/login") + @PutMapping("/nonmembers/login") fun loginNonMember( @RequestBody request: LoginNonMember.Request ): ResponseEntity> { @@ -27,12 +25,13 @@ class NonMemberController( SuccessResponse.of(HttpStatus.OK, "비회원 로그인 성공") ) } - @PostMapping("/pings") + + @PostMapping(NonMemberApi.PING) fun createNonMemberPings(@RequestBody request: CreateNonMember.Request) { return nonMemberService.createNonMemberPings(request) } - @GetMapping("/pings") + @GetMapping(NonMemberApi.PING) fun getNonMemberPings(@RequestParam uuid: String): GetAllNonMemberPings.Response { return nonMemberService.getAllNonMemberPings(uuid) } diff --git a/Ping-Api/src/test/kotlin/com/ping/api/nonmember/NonMemberControllerTest.kt b/Ping-Api/src/test/kotlin/com/ping/api/nonmember/NonMemberControllerTest.kt index 80b91dc..655ba08 100644 --- a/Ping-Api/src/test/kotlin/com/ping/api/nonmember/NonMemberControllerTest.kt +++ b/Ping-Api/src/test/kotlin/com/ping/api/nonmember/NonMemberControllerTest.kt @@ -40,7 +40,7 @@ class NonMemberControllerTest : BaseRestDocsTest() { bookmarkUrls = listOf("https://naver.me/Fqimcb8B", "https://naver.me/xUwGH5c3"), storeUrls = listOf("https://naver.me/GuGEom4T", "https://naver.me/FuVzL1bq") ) - val request = RestDocumentationRequestBuilders.post("/nonmembers/pings") + val request = RestDocumentationRequestBuilders.post(NonMemberApi.PING) .contentType(MediaType.APPLICATION_JSON_VALUE) .content(objectMapper.writeValueAsString(createNonMemberRequest)) @@ -67,7 +67,7 @@ class NonMemberControllerTest : BaseRestDocsTest() { fun getAllNonMemberPings() { // given val uuid = "test" - val request = RestDocumentationRequestBuilders.get("/nonmembers/pings") + val request = RestDocumentationRequestBuilders.get(NonMemberApi.PING) .contentType(MediaType.APPLICATION_JSON_VALUE) .queryParam("uuid", uuid) From 119971303beed94b95db23207c8df4ce8c0011e6 Mon Sep 17 00:00:00 2001 From: sominyun Date: Sun, 27 Oct 2024 17:00:22 +0900 Subject: [PATCH 048/203] =?UTF-8?q?feat:#15=20GetAllNonMemberPings=20respo?= =?UTF-8?q?nse=20px,py=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/ping/api/nonmember/NonMemberControllerTest.kt | 8 ++++++-- .../com/ping/application/nonmember/NonMemberService.kt | 2 ++ .../application/nonmember/dto/GetAllNonMemberPings.kt | 4 +++- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/Ping-Api/src/test/kotlin/com/ping/api/nonmember/NonMemberControllerTest.kt b/Ping-Api/src/test/kotlin/com/ping/api/nonmember/NonMemberControllerTest.kt index 655ba08..5752e3a 100644 --- a/Ping-Api/src/test/kotlin/com/ping/api/nonmember/NonMemberControllerTest.kt +++ b/Ping-Api/src/test/kotlin/com/ping/api/nonmember/NonMemberControllerTest.kt @@ -73,6 +73,8 @@ class NonMemberControllerTest : BaseRestDocsTest() { val getAllNonMemberPings = GetAllNonMemberPings.Response( eventName = "핑핑이들 여행", + px = 127.00001, + py = 37.00001, nonMembers = listOf( GetAllNonMemberPings.NonMember(nonMemberId = 1, name = "핑핑이1"), GetAllNonMemberPings.NonMember(nonMemberId = 2, name = "핑핑이2") @@ -113,6 +115,8 @@ class NonMemberControllerTest : BaseRestDocsTest() { resultHandler.document( responseFields( fieldWithPath("eventName").description("이벤트 이름"), + fieldWithPath("px").description("이벤트 중심 경도"), + fieldWithPath("py").description("이벤트 중심 위도"), fieldWithPath("nonMembers[].nonMemberId").description("비회원의 id"), fieldWithPath("nonMembers[].name").description("비회원 이름"), fieldWithPath("pings[].iconLevel").description("아이콘 레벨\n4:가장 많이 겹침\n3:그다음\n2:그다음\n1:나머지"), @@ -120,8 +124,8 @@ class NonMemberControllerTest : BaseRestDocsTest() { fieldWithPath("pings[].nonMembers[].name").description("비회원 이름"), fieldWithPath("pings[].url").description("장소 url"), fieldWithPath("pings[].placeName").description("장소 이름"), - fieldWithPath("pings[].px").description("위도"), - fieldWithPath("pings[].py").description("경도"), + fieldWithPath("pings[].px").description("경도"), + fieldWithPath("pings[].py").description("위도"), ) ) ) diff --git a/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberService.kt b/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberService.kt index 5d5c7d6..9826a87 100644 --- a/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberService.kt +++ b/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberService.kt @@ -157,6 +157,8 @@ class NonMemberService( return GetAllNonMemberPings.Response( eventName = shareUrl.eventName, nonMembers = nonMembers, + px = shareUrl.latitude, + py = shareUrl.longtitude, pings = pings ) } diff --git a/Ping-Application/src/main/kotlin/com/ping/application/nonmember/dto/GetAllNonMemberPings.kt b/Ping-Application/src/main/kotlin/com/ping/application/nonmember/dto/GetAllNonMemberPings.kt index d199327..532ac9a 100644 --- a/Ping-Application/src/main/kotlin/com/ping/application/nonmember/dto/GetAllNonMemberPings.kt +++ b/Ping-Application/src/main/kotlin/com/ping/application/nonmember/dto/GetAllNonMemberPings.kt @@ -3,10 +3,12 @@ package com.ping.application.nonmember.dto class GetAllNonMemberPings { data class Response( val eventName: String, + val px: Double, + val py: Double, val nonMembers: List, val pings: List - ) + data class NonMember( val nonMemberId: Long, val name : String From 813b318b3e68ecb07fd127918631aee83421a404 Mon Sep 17 00:00:00 2001 From: sominyun Date: Sun, 27 Oct 2024 17:00:40 +0900 Subject: [PATCH 049/203] =?UTF-8?q?fix:#15=20private=20method=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Ping-Api/src/docs/asciidoc/NonMember.adoc | 4 +- .../application/nonmember/NonMemberService.kt | 63 +++++++++++-------- 2 files changed, 38 insertions(+), 29 deletions(-) diff --git a/Ping-Api/src/docs/asciidoc/NonMember.adoc b/Ping-Api/src/docs/asciidoc/NonMember.adoc index 9360ef8..81aa613 100644 --- a/Ping-Api/src/docs/asciidoc/NonMember.adoc +++ b/Ping-Api/src/docs/asciidoc/NonMember.adoc @@ -3,8 +3,8 @@ [[Post-NonMemberPings]] === 비회원 핑 생성 -operation::NonMemberControllerTest/createNonMemberPings[snippets='HTTP-request,request-fields,HTTP-response'] +operation::NonMemberControllerTest/createNonMemberPings[snippets='http-request,request-fields,http-response'] [[Get-NonMemberPings]] === 전체 핑 불러오기 -operation::NonMemberControllerTest/getAllNonMemberPings[snippets='HTTP-request,HTTP-response,response-fields'] \ No newline at end of file +operation::NonMemberControllerTest/getAllNonMemberPings[snippets='http-request,http-response,response-fields'] \ No newline at end of file diff --git a/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberService.kt b/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberService.kt index 9826a87..ffd1bb5 100644 --- a/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberService.kt +++ b/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberService.kt @@ -106,35 +106,20 @@ class NonMemberService( ) } - //list>> - val allNonMemberPlaces = nonMemberList.flatMap { nonMember -> - nonMemberPlaceRepository.findAllByNonMemberId(nonMember.id).map { place -> - place to nonMember - } - } - val bookmarks = bookmarkRepository.findAllBySidIn(allNonMemberPlaces.map { it.first.sid }.distinct()) - val bookmarkMap = bookmarks.associateBy { it.sid } - - //Map>>> - val nonMemberPlaces = allNonMemberPlaces - .groupBy { it.first.sid } - .mapNotNull { (sid, placeNonMemberPairs) -> - val bookmarkDomain = bookmarkMap[sid] - bookmarkDomain?.let { - it to placeNonMemberPairs.map { placeNonMemberPair -> - placeNonMemberPair.second - } - } - } - .sortedByDescending { it.second.size }.groupBy { it.second.size } + val nonMemberPlaces = nonMembersToNonMemberPlacesMap(nonMemberList) val pings = nonMemberPlaces.entries.mapIndexed{ index, nonMemberPlace -> + val mostOverlappedIconLevel = 4 + val secondOverlappedIconLevel = 3 + val thirdOverlappedIconLevel = 2 + val remainderIconLevel = 1 + val level = when { - nonMemberPlace.key == 1 -> 1 // 아무도 안겹친 sid (겹친 인원이 1인 경우) - index == 0 -> 4 // 가장 많이 겹친 sid - index == 1 -> 3 // 두 번째로 많이 겹친 sid - index == 2 -> 2 // 세 번째로 많이 겹친 sid - else -> 1 // 그 외의 겹친 sid + nonMemberPlace.key == 1 -> remainderIconLevel //1명일 때 + index == 0 -> mostOverlappedIconLevel + index == 1 -> secondOverlappedIconLevel + index == 2 -> thirdOverlappedIconLevel + else -> remainderIconLevel } nonMemberPlace.value.map { bookmarkPair -> GetAllNonMemberPings.Ping( @@ -153,7 +138,6 @@ class NonMemberService( } }.flatten() - return GetAllNonMemberPings.Response( eventName = shareUrl.eventName, nonMembers = nonMembers, @@ -162,4 +146,29 @@ class NonMemberService( pings = pings ) } + + private fun nonMembersToNonMemberPlacesMap(nonMembers: List): Map>>> { + //list>> + val allNonMemberPlaces = nonMembers.flatMap { nonMember -> + nonMemberPlaceRepository.findAllByNonMemberId(nonMember.id).map { place -> + place to nonMember + } + } + + val bookmarks = bookmarkRepository.findAllBySidIn(allNonMemberPlaces.map { it.first.sid }.distinct()) + val bookmarkMap = bookmarks.associateBy { it.sid } + + //Map>>> + val nonMemberPlaces = allNonMemberPlaces + .groupBy { it.first.sid } + .mapNotNull { (sid, placeNonMemberPairs) -> + val bookmarkDomain = bookmarkMap[sid] + bookmarkDomain?.let { + it to placeNonMemberPairs.map { placeNonMemberPair -> + placeNonMemberPair.second + } + } + }.sortedByDescending { it.second.size }.groupBy { it.second.size } + return nonMemberPlaces + } } \ No newline at end of file From 54365917b41866c0358ffdce3c66341652aaed2d Mon Sep 17 00:00:00 2001 From: codrin2 Date: Sun, 27 Oct 2024 17:08:09 +0900 Subject: [PATCH 050/203] =?UTF-8?q?feat(NonMemberService):=20=EB=B9=84?= =?UTF-8?q?=ED=9A=8C=EC=9B=90=EC=9D=98=20=EB=A7=B5=ED=95=80=20=EB=AA=A8?= =?UTF-8?q?=EC=9D=8C=20=EB=A7=81=ED=81=AC=20=EB=B0=8F=20=EB=A7=B5=ED=95=80?= =?UTF-8?q?=20=EA=B0=80=EA=B2=8C=20=EB=A7=81=ED=81=AC=20=EC=97=85=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=8A=B8=20=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ping/api/nonmember/NonMemberController.kt | 5 +- .../application/nonmember/NonMemberService.kt | 89 +++++++++++++++++++ .../nonmember/dto/UpdateNonMemberStatus.kt | 9 ++ .../repository/NonMemberPlaceRepository.kt | 1 + .../NonMemberPlaceRepositoryImpl.kt | 5 +- 5 files changed, 104 insertions(+), 5 deletions(-) create mode 100644 Ping-Application/src/main/kotlin/com/ping/application/nonmember/dto/UpdateNonMemberStatus.kt diff --git a/Ping-Api/src/main/kotlin/com/ping/api/nonmember/NonMemberController.kt b/Ping-Api/src/main/kotlin/com/ping/api/nonmember/NonMemberController.kt index 6e24961..ee959ad 100644 --- a/Ping-Api/src/main/kotlin/com/ping/api/nonmember/NonMemberController.kt +++ b/Ping-Api/src/main/kotlin/com/ping/api/nonmember/NonMemberController.kt @@ -23,14 +23,11 @@ class NonMemberController( ): ResponseEntity> { nonMemberLoginService.login(request) - return ResponseEntity.ok( - SuccessResponse.of(HttpStatus.OK, "비회원 로그인 성공") - ) + return ResponseEntity.ok(SuccessResponse.of(HttpStatus.OK, "비회원 로그인 성공")) } @PostMapping("/pings") fun createNonMemberPings(@RequestBody request: CreateNonMember.Request) { return nonMemberService.createNonMemberPings(request) - } @GetMapping("/pings") diff --git a/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberService.kt b/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberService.kt index 45ff9aa..86a66a3 100644 --- a/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberService.kt +++ b/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberService.kt @@ -2,6 +2,7 @@ package com.ping.application.nonmember import com.ping.application.nonmember.dto.CreateNonMember import com.ping.application.nonmember.dto.GetAllNonMemberPings +import com.ping.application.nonmember.dto.UpdateNonMemberStatus import com.ping.client.naver.map.NaverMapClient import com.ping.common.exception.CustomException import com.ping.common.exception.ExceptionContent @@ -20,6 +21,7 @@ class NonMemberService( private val nonMemberPlaceRepository: NonMemberPlaceRepository, private val nonMemberBookmarkUrlRepository: NonMemberBookmarkUrlRepository, private val nonMemberStoreUrlRepository: NonMemberStoreUrlRepository, + private val nonMemberUpdateStatusRepository: NonMemberUpdateStatusRepository, private val naverMapClient: NaverMapClient, private val validator: NonMemberValidator ) { @@ -160,4 +162,91 @@ class NonMemberService( pings = pings ) } + + @Transactional + fun updateNonMemberPings(request: UpdateNonMemberStatus.Request) { + val nonMemberDomain = nonMemberRepository.findById(request.nonMemberId) + ?: throw CustomException(ExceptionContent.NON_MEMBER_NOT_FOUND) + + val existingSids = nonMemberPlaceRepository.findAllByNonMemberId(request.nonMemberId).map { it.sid }.toSet() + val newBookmarkSids = handleBookmarkUrls(request.bookmarkUrls) + val newStoreSids = handleStoreUrls(request.storeUrls) + + val allNewSids = (newBookmarkSids + newStoreSids) + if (existingSids != allNewSids) { + updatePlaceSids(nonMemberDomain, allNewSids) + } + } + + private fun handleBookmarkUrls(bookmarkUrls: List): Set { + val newSids = mutableSetOf() + bookmarkUrls.forEach { url -> + val expandedUrl = UrlUtil.expandShortUrl(url) + val bookmarkList = naverMapClient.bookmarkUrlToBookmarkLists(expandedUrl).bookmarkList + + bookmarkList.forEach { bookmark -> + if (!isBookmarkExists(bookmark.sid)) { + bookmarkRepository.saveAll( + listOf( + BookmarkDomain( + name = bookmark.name, + px = bookmark.px, + py = bookmark.py, + sid = bookmark.sid, + address = bookmark.address, + mcidName = bookmark.mcidName, + url = "https://map.naver.com/p/entry/place/${bookmark.sid}" + ) + ) + ) + } + newSids.add(bookmark.sid) + } + } + return newSids + } + + private fun handleStoreUrls(storeUrls: List): Set { + val newSids = mutableSetOf() + storeUrls.forEach { url -> + val expandedUrl = UrlUtil.expandShortUrl(url) + val store = naverMapClient.storeUrlToBookmark(expandedUrl) + + if (!isBookmarkExists(store.sid)) { + bookmarkRepository.saveAll( + listOf( + BookmarkDomain( + name = store.name, + px = store.px, + py = store.py, + sid = store.sid, + address = store.address, + mcidName = store.mcidName, + url = url + ) + ) + ) + } + newSids.add(store.sid) + } + return newSids + } + + private fun updatePlaceSids(nonMemberDomain: NonMemberDomain, newSids: Set) { + val existingPlaces = nonMemberPlaceRepository.findAllByNonMemberId(nonMemberDomain.id) + val existingSids = existingPlaces.map { it.sid }.toSet() + + val sidsToAdd = newSids - existingSids + val sidsToDelete = existingSids - newSids + + val placesToAdd = sidsToAdd.map { sid -> NonMemberPlaceDomain.of(nonMemberDomain, sid) } + nonMemberPlaceRepository.saveAll(placesToAdd) + + val placesToDelete = existingPlaces.filter { it.sid in sidsToDelete } + nonMemberPlaceRepository.deleteAll(placesToDelete) + } + + private fun isBookmarkExists(sid: String): Boolean { + return bookmarkRepository.findAllBySidIn(listOf(sid)).isNotEmpty() + } } \ No newline at end of file diff --git a/Ping-Application/src/main/kotlin/com/ping/application/nonmember/dto/UpdateNonMemberStatus.kt b/Ping-Application/src/main/kotlin/com/ping/application/nonmember/dto/UpdateNonMemberStatus.kt new file mode 100644 index 0000000..2b361df --- /dev/null +++ b/Ping-Application/src/main/kotlin/com/ping/application/nonmember/dto/UpdateNonMemberStatus.kt @@ -0,0 +1,9 @@ +package com.ping.application.nonmember.dto + +class UpdateNonMemberStatus { + data class Request( + val nonMemberId: Long, + val bookmarkUrls: List, + val storeUrls: List + ) +} \ No newline at end of file diff --git a/Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/repository/NonMemberPlaceRepository.kt b/Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/repository/NonMemberPlaceRepository.kt index 44726da..77d4409 100644 --- a/Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/repository/NonMemberPlaceRepository.kt +++ b/Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/repository/NonMemberPlaceRepository.kt @@ -6,4 +6,5 @@ interface NonMemberPlaceRepository { fun saveAll(nonMemberPlaceDomains: List): List fun findAllByNonMemberId(nonMemberId: Long): List + fun deleteAll(nonMemberPlaceDomains: List) } \ No newline at end of file diff --git a/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/repositoryImpl/NonMemberPlaceRepositoryImpl.kt b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/repositoryImpl/NonMemberPlaceRepositoryImpl.kt index bc9e860..9e2bece 100644 --- a/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/repositoryImpl/NonMemberPlaceRepositoryImpl.kt +++ b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/repositoryImpl/NonMemberPlaceRepositoryImpl.kt @@ -14,8 +14,11 @@ class NonMemberPlaceRepositoryImpl( return nonMemberPlaceJpaRepository.saveAll(nonMemberPlaceDomains.map { NonMemberPlaceMapper.toEntity(it) }) .map { NonMemberPlaceMapper.toDomain(it) } } - override fun findAllByNonMemberId(nonMemberId: Long): List { return nonMemberPlaceJpaRepository.findAllByNonMemberId(nonMemberId).map { NonMemberPlaceMapper.toDomain(it) } } + override fun deleteAll(nonMemberPlaceDomains: List) { + val entities = nonMemberPlaceDomains.map { NonMemberPlaceMapper.toEntity(it) } + nonMemberPlaceJpaRepository.deleteAll(entities) + } } \ No newline at end of file From 3c66839082020ed189dc9eaf9ca785657553413f Mon Sep 17 00:00:00 2001 From: codrin2 Date: Sun, 27 Oct 2024 17:20:35 +0900 Subject: [PATCH 051/203] =?UTF-8?q?feat(NonMemberController=20):=20?= =?UTF-8?q?=EB=B9=84=ED=9A=8C=EC=9B=90=20=ED=95=91=20=EC=97=85=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=8A=B8=20API=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/com/ping/api/nonmember/NonMemberController.kt | 6 ++++++ .../com/ping/application/nonmember/NonMemberService.kt | 4 ++-- .../{UpdateNonMemberStatus.kt => UpdateNonMemberPings.kt} | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) rename Ping-Application/src/main/kotlin/com/ping/application/nonmember/dto/{UpdateNonMemberStatus.kt => UpdateNonMemberPings.kt} (85%) diff --git a/Ping-Api/src/main/kotlin/com/ping/api/nonmember/NonMemberController.kt b/Ping-Api/src/main/kotlin/com/ping/api/nonmember/NonMemberController.kt index ee959ad..29492c4 100644 --- a/Ping-Api/src/main/kotlin/com/ping/api/nonmember/NonMemberController.kt +++ b/Ping-Api/src/main/kotlin/com/ping/api/nonmember/NonMemberController.kt @@ -5,6 +5,7 @@ import com.ping.application.nonmember.NonMemberService import com.ping.application.nonmember.dto.CreateNonMember import com.ping.application.nonmember.dto.GetAllNonMemberPings import com.ping.application.nonmember.dto.LoginNonMember +import com.ping.application.nonmember.dto.UpdateNonMemberPings import com.ping.common.exception.SuccessResponse import org.springframework.http.HttpStatus import org.springframework.http.ResponseEntity @@ -34,4 +35,9 @@ class NonMemberController( fun getNonMemberPings(@RequestParam uuid: String): GetAllNonMemberPings.Response { return nonMemberService.getAllNonMemberPings(uuid) } + + @PutMapping("/pings") + fun updateNonMemberPings(@RequestBody request: UpdateNonMemberPings.Request) { + nonMemberService.updateNonMemberPings(request) + } } \ No newline at end of file diff --git a/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberService.kt b/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberService.kt index 86a66a3..0773c22 100644 --- a/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberService.kt +++ b/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberService.kt @@ -2,7 +2,7 @@ package com.ping.application.nonmember import com.ping.application.nonmember.dto.CreateNonMember import com.ping.application.nonmember.dto.GetAllNonMemberPings -import com.ping.application.nonmember.dto.UpdateNonMemberStatus +import com.ping.application.nonmember.dto.UpdateNonMemberPings import com.ping.client.naver.map.NaverMapClient import com.ping.common.exception.CustomException import com.ping.common.exception.ExceptionContent @@ -164,7 +164,7 @@ class NonMemberService( } @Transactional - fun updateNonMemberPings(request: UpdateNonMemberStatus.Request) { + fun updateNonMemberPings(request: UpdateNonMemberPings.Request) { val nonMemberDomain = nonMemberRepository.findById(request.nonMemberId) ?: throw CustomException(ExceptionContent.NON_MEMBER_NOT_FOUND) diff --git a/Ping-Application/src/main/kotlin/com/ping/application/nonmember/dto/UpdateNonMemberStatus.kt b/Ping-Application/src/main/kotlin/com/ping/application/nonmember/dto/UpdateNonMemberPings.kt similarity index 85% rename from Ping-Application/src/main/kotlin/com/ping/application/nonmember/dto/UpdateNonMemberStatus.kt rename to Ping-Application/src/main/kotlin/com/ping/application/nonmember/dto/UpdateNonMemberPings.kt index 2b361df..f56b09b 100644 --- a/Ping-Application/src/main/kotlin/com/ping/application/nonmember/dto/UpdateNonMemberStatus.kt +++ b/Ping-Application/src/main/kotlin/com/ping/application/nonmember/dto/UpdateNonMemberPings.kt @@ -1,6 +1,6 @@ package com.ping.application.nonmember.dto -class UpdateNonMemberStatus { +class UpdateNonMemberPings { data class Request( val nonMemberId: Long, val bookmarkUrls: List, From 35ee06731773b274248b68a84f84daf9de3cde20 Mon Sep 17 00:00:00 2001 From: sominyun Date: Sun, 27 Oct 2024 17:35:48 +0900 Subject: [PATCH 052/203] =?UTF-8?q?feat:#18=20nonMemberPing=20=EB=B6=88?= =?UTF-8?q?=EB=9F=AC=EC=98=A4=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/ping/api/nonmember/NonMemberApi.kt | 1 + .../ping/api/nonmember/NonMemberController.kt | 6 ++++++ .../application/nonmember/NonMemberService.kt | 16 ++++++++++++++++ .../nonmember/dto/GetNonMemberPing.kt | 14 ++++++++++++++ 4 files changed, 37 insertions(+) create mode 100644 Ping-Application/src/main/kotlin/com/ping/application/nonmember/dto/GetNonMemberPing.kt diff --git a/Ping-Api/src/main/kotlin/com/ping/api/nonmember/NonMemberApi.kt b/Ping-Api/src/main/kotlin/com/ping/api/nonmember/NonMemberApi.kt index 29bb6c5..6a7577e 100644 --- a/Ping-Api/src/main/kotlin/com/ping/api/nonmember/NonMemberApi.kt +++ b/Ping-Api/src/main/kotlin/com/ping/api/nonmember/NonMemberApi.kt @@ -3,4 +3,5 @@ package com.ping.api.nonmember object NonMemberApi { const val BASE_URL = "/nonmembers" const val PING = "$BASE_URL/pings" + const val PING_NONMEMBERID = "$PING/{nonMemberId}" } \ No newline at end of file diff --git a/Ping-Api/src/main/kotlin/com/ping/api/nonmember/NonMemberController.kt b/Ping-Api/src/main/kotlin/com/ping/api/nonmember/NonMemberController.kt index ac700ce..e3f7efc 100644 --- a/Ping-Api/src/main/kotlin/com/ping/api/nonmember/NonMemberController.kt +++ b/Ping-Api/src/main/kotlin/com/ping/api/nonmember/NonMemberController.kt @@ -4,6 +4,7 @@ import com.ping.application.nonmember.NonMemberLoginService import com.ping.application.nonmember.NonMemberService import com.ping.application.nonmember.dto.CreateNonMember import com.ping.application.nonmember.dto.GetAllNonMemberPings +import com.ping.application.nonmember.dto.GetNonMemberPing import com.ping.application.nonmember.dto.LoginNonMember import com.ping.common.exception.SuccessResponse import org.springframework.http.HttpStatus @@ -35,4 +36,9 @@ class NonMemberController( fun getNonMemberPings(@RequestParam uuid: String): GetAllNonMemberPings.Response { return nonMemberService.getAllNonMemberPings(uuid) } + + @GetMapping(NonMemberApi.PING_NONMEMBERID) + fun getNonMemberPing(@PathVariable nonMemberId: Long): GetNonMemberPing.Response { + return nonMemberService.getNonMemberPing(nonMemberId) + } } \ No newline at end of file diff --git a/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberService.kt b/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberService.kt index ffd1bb5..f989f47 100644 --- a/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberService.kt +++ b/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberService.kt @@ -2,6 +2,7 @@ package com.ping.application.nonmember import com.ping.application.nonmember.dto.CreateNonMember import com.ping.application.nonmember.dto.GetAllNonMemberPings +import com.ping.application.nonmember.dto.GetNonMemberPing import com.ping.client.naver.map.NaverMapClient import com.ping.common.exception.CustomException import com.ping.common.exception.ExceptionContent @@ -171,4 +172,19 @@ class NonMemberService( }.sortedByDescending { it.second.size }.groupBy { it.second.size } return nonMemberPlaces } + + fun getNonMemberPing(nonMemberId: Long) : GetNonMemberPing.Response { + val nonMemberPlaces = nonMemberPlaceRepository.findAllByNonMemberId(nonMemberId) + val bookmarks = bookmarkRepository.findAllBySidIn(nonMemberPlaces.map { it.sid }) + return GetNonMemberPing.Response( + pings = bookmarks.map { + GetNonMemberPing.Ping( + url = it.url, + placeName = it.address, + px = it.px, + py = it.py + ) + } + ) + } } \ No newline at end of file diff --git a/Ping-Application/src/main/kotlin/com/ping/application/nonmember/dto/GetNonMemberPing.kt b/Ping-Application/src/main/kotlin/com/ping/application/nonmember/dto/GetNonMemberPing.kt new file mode 100644 index 0000000..2451db2 --- /dev/null +++ b/Ping-Application/src/main/kotlin/com/ping/application/nonmember/dto/GetNonMemberPing.kt @@ -0,0 +1,14 @@ +package com.ping.application.nonmember.dto + +class GetNonMemberPing { + data class Response( + val pings: List + ) + + data class Ping( + val url: String, + val placeName: String, + val px: Double, + val py: Double + ) +} \ No newline at end of file From 768fd96243a9a8c79c2ccd4f1d4c8bca3157a8c6 Mon Sep 17 00:00:00 2001 From: sominyun Date: Sun, 27 Oct 2024 21:47:48 +0900 Subject: [PATCH 053/203] =?UTF-8?q?feat:#18=20=EA=B0=9C=EB=B3=84=20?= =?UTF-8?q?=ED=95=91=20=EB=B6=88=EB=9F=AC=EC=98=A4=EA=B8=B0=20test=20code?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Ping-Api/src/docs/asciidoc/NonMember.adoc | 6 ++- .../api/nonmember/NonMemberControllerTest.kt | 43 +++++++++++++++++++ .../application/nonmember/NonMemberService.kt | 2 +- 3 files changed, 49 insertions(+), 2 deletions(-) diff --git a/Ping-Api/src/docs/asciidoc/NonMember.adoc b/Ping-Api/src/docs/asciidoc/NonMember.adoc index 81aa613..fe464de 100644 --- a/Ping-Api/src/docs/asciidoc/NonMember.adoc +++ b/Ping-Api/src/docs/asciidoc/NonMember.adoc @@ -7,4 +7,8 @@ operation::NonMemberControllerTest/createNonMemberPings[snippets='http-request,r [[Get-NonMemberPings]] === 전체 핑 불러오기 -operation::NonMemberControllerTest/getAllNonMemberPings[snippets='http-request,http-response,response-fields'] \ No newline at end of file +operation::NonMemberControllerTest/getAllNonMemberPings[snippets='http-request,http-response,response-fields'] + +[[Get-NonMemberPing]] +=== 개별 핑 불러오기 +operation::NonMemberControllerTest/getNonMemberPing[snippets='http-request,http-response,response-fields'] \ No newline at end of file diff --git a/Ping-Api/src/test/kotlin/com/ping/api/nonmember/NonMemberControllerTest.kt b/Ping-Api/src/test/kotlin/com/ping/api/nonmember/NonMemberControllerTest.kt index 5752e3a..1e3e42f 100644 --- a/Ping-Api/src/test/kotlin/com/ping/api/nonmember/NonMemberControllerTest.kt +++ b/Ping-Api/src/test/kotlin/com/ping/api/nonmember/NonMemberControllerTest.kt @@ -5,6 +5,7 @@ import com.ping.application.nonmember.NonMemberLoginService import com.ping.application.nonmember.NonMemberService import com.ping.application.nonmember.dto.CreateNonMember import com.ping.application.nonmember.dto.GetAllNonMemberPings +import com.ping.application.nonmember.dto.GetNonMemberPing import com.ping.infra.nonmember.domain.mongo.repository.BookmarkMongoRepository import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test @@ -130,4 +131,46 @@ class NonMemberControllerTest : BaseRestDocsTest() { ) ) } + + @Test + @DisplayName("개별 핑 불러오기") + fun getNonMemberPing() { + //given + val nonMemberId = 1L + val request = RestDocumentationRequestBuilders.get(NonMemberApi.PING_NONMEMBERID,nonMemberId) + .contentType(MediaType.APPLICATION_JSON_VALUE) + val getNonMemberPing = GetNonMemberPing.Response( + pings = listOf(GetNonMemberPing.Ping( + url = "https://map.naver.com/p/entry/place/1072787710", + placeName = "도토리", + px = 126.9727984, + py = 37.5319087 + ), + GetNonMemberPing.Ping( + url = "https://map.naver.com/p/entry/place/1092976589", + placeName = "당케커피", + px = 126.971301, + py = 37.5314638 + )) + ) + + given(nonMemberService.getNonMemberPing(nonMemberId)).willReturn(getNonMemberPing) + + //when + val result = mockMvc.perform(request) + + //then + result.andExpect(status().isOk) + .andDo( + resultHandler.document( + responseFields( + fieldWithPath("pings[].url").description("장소 url"), + fieldWithPath("pings[].placeName").description("장소 이름"), + fieldWithPath("pings[].px").description("경도"), + fieldWithPath("pings[].py").description("위도"), + ) + ) + ) + + } } \ No newline at end of file diff --git a/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberService.kt b/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberService.kt index f989f47..1cf03a2 100644 --- a/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberService.kt +++ b/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberService.kt @@ -180,7 +180,7 @@ class NonMemberService( pings = bookmarks.map { GetNonMemberPing.Ping( url = it.url, - placeName = it.address, + placeName = it.name, px = it.px, py = it.py ) From 76fce2aab4e60e726965d85de245a510d809cfcf Mon Sep 17 00:00:00 2001 From: codrin2 Date: Tue, 29 Oct 2024 17:19:37 +0900 Subject: [PATCH 054/203] =?UTF-8?q?feat:=20createNonMemberPings=20?= =?UTF-8?q?=EB=A9=94=EC=86=8C=EB=93=9C=EC=97=90=20=EC=83=88=EB=A1=9C=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=EB=90=9C=20=EB=B9=84=ED=9A=8C=EC=9B=90?= =?UTF-8?q?=EC=9D=98=20=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8=20=EC=83=81?= =?UTF-8?q?=ED=83=9C=20=EC=B6=94=EA=B0=80=20=EB=A1=9C=EC=A7=81=20=EB=B0=8F?= =?UTF-8?q?=20=EC=A4=91=EB=B3=B5=20=EB=A1=9C=EC=A7=81=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/nonmember/NonMemberService.kt | 175 ++++++++++-------- .../repository/NonMemberStoreUrlRepository.kt | 2 +- 2 files changed, 98 insertions(+), 79 deletions(-) diff --git a/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberService.kt b/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberService.kt index 0773c22..db0cc84 100644 --- a/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberService.kt +++ b/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberService.kt @@ -27,73 +27,37 @@ class NonMemberService( ) { @Transactional fun createNonMemberPings(request: CreateNonMember.Request) { - //이름 공백, 특수문자, 숫자 불가 + // 이름 및 비밀번호 유효성 검증 validator.name(request.name) - // 비밀번호 형식 검사 (4자리 숫자) validator.password(request.password) val shareUrl = shareUrlRepository.findByUuid(request.uuid) ?: throw CustomException(ExceptionContent.INVALID_SHARE_URL) - // shareUrlId과 name으로 비회원 존재 여부 확인 + // 이미 같은 name의 비회원 존재 여부 확인 nonMemberRepository.findByShareUrlIdAndName(shareUrl.id, request.name)?.let { throw CustomException(ExceptionContent.NON_MEMBER_ALREADY_EXISTS) } - // NonMember 엔티티 생성 및 저장 + // NonMember 생성 및 저장 val nonMemberDomain = NonMemberDomain.of(request.name, request.password, shareUrl) val nonmember = nonMemberRepository.save(nonMemberDomain) - //url 저장 + // URL 데이터 저장 val nonMemberBookmarkUrlDomains = NonMemberBookmarkUrlDomain.of(nonmember, request.bookmarkUrls) nonMemberBookmarkUrlRepository.saveAll(nonMemberBookmarkUrlDomains) val nonMemberStoreUrlDomains = NonMemberStoreUrlDomain.of(nonmember, request.storeUrls) - nonMemberStoreUrlRepository.saveALl(nonMemberStoreUrlDomains) + nonMemberStoreUrlRepository.saveAll(nonMemberStoreUrlDomains) - val nonMemberPlaces = mutableListOf() - val bookmarks = mutableListOf() - //맵핀 모은 링크 추출 - bookmarks.addAll( - request.bookmarkUrls.flatMap { - val url = UrlUtil.expandShortUrl(it) - naverMapClient.bookmarkUrlToBookmarkLists(url).bookmarkList.map { bookmark -> - //NonMemberPlace 저장 - nonMemberPlaces - .takeIf { nonMemberPlace -> nonMemberPlace.none { place -> place.sid == bookmark.sid } } - ?.add(NonMemberPlaceDomain.of(nonmember, bookmark.sid)) - BookmarkDomain( - name = bookmark.name, - px = bookmark.px, - py = bookmark.py, - sid = bookmark.sid, - address = bookmark.address, - mcidName = bookmark.mcidName, - url = "https://map.naver.com/p/entry/place/${bookmark.sid}" - ) - } - }) - //맵핀 가게 링크 추출 - bookmarks.addAll( - request.storeUrls.map { - val url = UrlUtil.expandShortUrl(it) - val bookmark = naverMapClient.storeUrlToBookmark(url) - //NonMemberPlace 저장 - nonMemberPlaces - .takeIf { nonMemberPlace -> nonMemberPlace.none { place -> place.sid == bookmark.sid } } - ?.add(NonMemberPlaceDomain.of(nonmember, bookmark.sid)) - BookmarkDomain( - name = bookmark.name, - px = bookmark.px, - py = bookmark.py, - sid = bookmark.sid, - address = bookmark.address, - mcidName = bookmark.mcidName, - url = it - ) - }) - bookmarkRepository.saveAll(bookmarks) - nonMemberPlaceRepository.saveAll(nonMemberPlaces) + // 북마크와 가게 데이터를 개별 리스트로 추출 + val bookmarkPlaces = handleBookmarkUrls(request.bookmarkUrls, nonmember) + val storePlaces = handleStoreUrls(request.storeUrls, nonmember) + + bookmarkRepository.saveAll(bookmarkPlaces.bookmarks + storePlaces.bookmarks) + nonMemberPlaceRepository.saveAll(bookmarkPlaces.places + storePlaces.places) + + createNonMemberUpdateStatus(nonmember, shareUrl.id) } fun getAllNonMemberPings(uuid: String): GetAllNonMemberPings.Response { @@ -168,68 +132,118 @@ class NonMemberService( val nonMemberDomain = nonMemberRepository.findById(request.nonMemberId) ?: throw CustomException(ExceptionContent.NON_MEMBER_NOT_FOUND) + // 현재 비회원의 기존 sid 추출 val existingSids = nonMemberPlaceRepository.findAllByNonMemberId(request.nonMemberId).map { it.sid }.toSet() - val newBookmarkSids = handleBookmarkUrls(request.bookmarkUrls) - val newStoreSids = handleStoreUrls(request.storeUrls) - val allNewSids = (newBookmarkSids + newStoreSids) + // 새로운 북마크 및 가게 데이터 처리 + val bookmarkData = handleBookmarkUrls(request.bookmarkUrls, nonMemberDomain) + val storeData = handleStoreUrls(request.storeUrls, nonMemberDomain) + + // 전체 새로운 sid 집합 + val allNewSids = (bookmarkData.sids + storeData.sids) if (existingSids != allNewSids) { updatePlaceSids(nonMemberDomain, allNewSids) } + + // 새로운 북마크 데이터 저장 + bookmarkRepository.saveAll(bookmarkData.bookmarks + storeData.bookmarks) + nonMemberPlaceRepository.saveAll(bookmarkData.places + storeData.places) + } + + private fun createNonMemberUpdateStatus(newNonMember: NonMemberDomain, shareUrlId: Long) { + // 새로 생성된 비회원을 제외한 기존 비회원 목록 조회 + val existingNonMembers = nonMemberRepository.findAllByShareUrl(shareUrlId) + .filter { it.id != newNonMember.id } + + // 기존 비회원이 없으면 return + if (existingNonMembers.isEmpty()) return + + // 새로 생성된 비회원 기준으로 각 기존 비회원에 대한 NonMemberUpdateStatusDomain 생성 + val updateStatusForNewMember = existingNonMembers.map { existingMember -> + NonMemberUpdateStatusDomain.of( + nonMemberDomain = newNonMember, + friendId = existingMember.id, + isUpdate = false + ) + } + + // 기존 비회원 기준으로 새로 생성된 비회원에 대한 NonMemberUpdateStatusDomain 생성 + val updateStatusForExistingMembers = existingNonMembers.map { existingMember -> + NonMemberUpdateStatusDomain.of( + nonMemberDomain = existingMember, + friendId = newNonMember.id, + isUpdate = false + ) + } + + // 모든 NonMemberUpdateStatusDomain 저장 + nonMemberUpdateStatusRepository.saveAll(updateStatusForNewMember + updateStatusForExistingMembers) } - private fun handleBookmarkUrls(bookmarkUrls: List): Set { + private fun handleBookmarkUrls( + bookmarkUrls: List, nonMember: NonMemberDomain + ): BookmarkData { + val nonMemberPlaces = mutableListOf() + val bookmarks = mutableListOf() val newSids = mutableSetOf() + bookmarkUrls.forEach { url -> val expandedUrl = UrlUtil.expandShortUrl(url) val bookmarkList = naverMapClient.bookmarkUrlToBookmarkLists(expandedUrl).bookmarkList bookmarkList.forEach { bookmark -> if (!isBookmarkExists(bookmark.sid)) { - bookmarkRepository.saveAll( - listOf( - BookmarkDomain( - name = bookmark.name, - px = bookmark.px, - py = bookmark.py, - sid = bookmark.sid, - address = bookmark.address, - mcidName = bookmark.mcidName, - url = "https://map.naver.com/p/entry/place/${bookmark.sid}" - ) + bookmarks.add( + BookmarkDomain( + name = bookmark.name, + px = bookmark.px, + py = bookmark.py, + sid = bookmark.sid, + address = bookmark.address, + mcidName = bookmark.mcidName, + url = "https://map.naver.com/p/entry/place/${bookmark.sid}" ) ) } + if (nonMemberPlaces.none { it.sid == bookmark.sid }) { + nonMemberPlaces.add(NonMemberPlaceDomain.of(nonMember, bookmark.sid)) + } newSids.add(bookmark.sid) } } - return newSids + return BookmarkData(nonMemberPlaces, bookmarks, newSids) } - private fun handleStoreUrls(storeUrls: List): Set { + private fun handleStoreUrls( + storeUrls: List, nonMember: NonMemberDomain + ): BookmarkData { + val nonMemberPlaces = mutableListOf() + val bookmarks = mutableListOf() val newSids = mutableSetOf() + storeUrls.forEach { url -> val expandedUrl = UrlUtil.expandShortUrl(url) val store = naverMapClient.storeUrlToBookmark(expandedUrl) if (!isBookmarkExists(store.sid)) { - bookmarkRepository.saveAll( - listOf( - BookmarkDomain( - name = store.name, - px = store.px, - py = store.py, - sid = store.sid, - address = store.address, - mcidName = store.mcidName, - url = url - ) + bookmarks.add( + BookmarkDomain( + name = store.name, + px = store.px, + py = store.py, + sid = store.sid, + address = store.address, + mcidName = store.mcidName, + url = url ) ) } + if (nonMemberPlaces.none { it.sid == store.sid }) { + nonMemberPlaces.add(NonMemberPlaceDomain.of(nonMember, store.sid)) + } newSids.add(store.sid) } - return newSids + return BookmarkData(nonMemberPlaces, bookmarks, newSids) } private fun updatePlaceSids(nonMemberDomain: NonMemberDomain, newSids: Set) { @@ -249,4 +263,9 @@ class NonMemberService( private fun isBookmarkExists(sid: String): Boolean { return bookmarkRepository.findAllBySidIn(listOf(sid)).isNotEmpty() } + private data class BookmarkData( + val places: List, + val bookmarks: List, + val sids: Set + ) } \ No newline at end of file diff --git a/Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/repository/NonMemberStoreUrlRepository.kt b/Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/repository/NonMemberStoreUrlRepository.kt index fcfb3a7..a52ac97 100644 --- a/Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/repository/NonMemberStoreUrlRepository.kt +++ b/Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/repository/NonMemberStoreUrlRepository.kt @@ -3,5 +3,5 @@ package com.ping.domain.nonmember.repository import com.ping.domain.nonmember.aggregate.NonMemberStoreUrlDomain interface NonMemberStoreUrlRepository { - fun saveALl(nonMemberStoreUrlDomains: List) : List + fun saveAll(nonMemberStoreUrlDomains: List) : List } \ No newline at end of file From 3bc2201f266afbf1772ca4a77019476f324ba59b Mon Sep 17 00:00:00 2001 From: codrin2 Date: Tue, 29 Oct 2024 19:10:13 +0900 Subject: [PATCH 055/203] =?UTF-8?q?fix:=20nonMemberPlaces=20=EC=A4=91?= =?UTF-8?q?=EB=B3=B5=20=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../nonmember/NonMemberLoginService.kt | 2 +- .../application/nonmember/NonMemberService.kt | 80 ++++++++++++++----- .../NonMemberStoreUrlRepositoryImpl.kt | 2 +- 3 files changed, 61 insertions(+), 23 deletions(-) diff --git a/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberLoginService.kt b/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberLoginService.kt index a0e6e20..8a64f45 100644 --- a/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberLoginService.kt +++ b/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberLoginService.kt @@ -11,7 +11,7 @@ import org.springframework.transaction.annotation.Transactional @Transactional(readOnly = true) class NonMemberLoginService( private val nonMemberRepository: NonMemberRepository, - private val validator: NamePasswordValidator + private val validator: NonMemberValidator ) { fun login(request: LoginNonMember.Request) { // 비밀번호 형식 검사 (4자리 숫자) diff --git a/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberService.kt b/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberService.kt index db0cc84..96fd74a 100644 --- a/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberService.kt +++ b/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberService.kt @@ -27,35 +27,73 @@ class NonMemberService( ) { @Transactional fun createNonMemberPings(request: CreateNonMember.Request) { - // 이름 및 비밀번호 유효성 검증 + //이름 공백, 특수문자, 숫자 불가 validator.name(request.name) + // 비밀번호 형식 검사 (4자리 숫자) validator.password(request.password) val shareUrl = shareUrlRepository.findByUuid(request.uuid) ?: throw CustomException(ExceptionContent.INVALID_SHARE_URL) - // 이미 같은 name의 비회원 존재 여부 확인 + // shareUrlId과 name으로 비회원 존재 여부 확인 nonMemberRepository.findByShareUrlIdAndName(shareUrl.id, request.name)?.let { throw CustomException(ExceptionContent.NON_MEMBER_ALREADY_EXISTS) } - // NonMember 생성 및 저장 + // NonMember 엔티티 생성 및 저장 val nonMemberDomain = NonMemberDomain.of(request.name, request.password, shareUrl) val nonmember = nonMemberRepository.save(nonMemberDomain) - // URL 데이터 저장 + //url 저장 val nonMemberBookmarkUrlDomains = NonMemberBookmarkUrlDomain.of(nonmember, request.bookmarkUrls) nonMemberBookmarkUrlRepository.saveAll(nonMemberBookmarkUrlDomains) val nonMemberStoreUrlDomains = NonMemberStoreUrlDomain.of(nonmember, request.storeUrls) nonMemberStoreUrlRepository.saveAll(nonMemberStoreUrlDomains) - // 북마크와 가게 데이터를 개별 리스트로 추출 - val bookmarkPlaces = handleBookmarkUrls(request.bookmarkUrls, nonmember) - val storePlaces = handleStoreUrls(request.storeUrls, nonmember) - - bookmarkRepository.saveAll(bookmarkPlaces.bookmarks + storePlaces.bookmarks) - nonMemberPlaceRepository.saveAll(bookmarkPlaces.places + storePlaces.places) + val nonMemberPlaces = mutableListOf() + val bookmarks = mutableListOf() + //맵핀 모은 링크 추출 + bookmarks.addAll( + request.bookmarkUrls.flatMap { + val url = UrlUtil.expandShortUrl(it) + naverMapClient.bookmarkUrlToBookmarkLists(url).bookmarkList.map { bookmark -> + //NonMemberPlace 저장 + nonMemberPlaces + .takeIf { nonMemberPlace -> nonMemberPlace.none { place -> place.sid == bookmark.sid } } + ?.add(NonMemberPlaceDomain.of(nonmember, bookmark.sid)) + BookmarkDomain( + name = bookmark.name, + px = bookmark.px, + py = bookmark.py, + sid = bookmark.sid, + address = bookmark.address, + mcidName = bookmark.mcidName, + url = "https://map.naver.com/p/entry/place/${bookmark.sid}" + ) + } + }) + //맵핀 가게 링크 추출 + bookmarks.addAll( + request.storeUrls.map { + val url = UrlUtil.expandShortUrl(it) + val bookmark = naverMapClient.storeUrlToBookmark(url) + //NonMemberPlace 저장 + nonMemberPlaces + .takeIf { nonMemberPlace -> nonMemberPlace.none { place -> place.sid == bookmark.sid } } + ?.add(NonMemberPlaceDomain.of(nonmember, bookmark.sid)) + BookmarkDomain( + name = bookmark.name, + px = bookmark.px, + py = bookmark.py, + sid = bookmark.sid, + address = bookmark.address, + mcidName = bookmark.mcidName, + url = it + ) + }) + bookmarkRepository.saveAll(bookmarks) + nonMemberPlaceRepository.saveAll(nonMemberPlaces) createNonMemberUpdateStatus(nonmember, shareUrl.id) } @@ -144,10 +182,6 @@ class NonMemberService( if (existingSids != allNewSids) { updatePlaceSids(nonMemberDomain, allNewSids) } - - // 새로운 북마크 데이터 저장 - bookmarkRepository.saveAll(bookmarkData.bookmarks + storeData.bookmarks) - nonMemberPlaceRepository.saveAll(bookmarkData.places + storeData.places) } private fun createNonMemberUpdateStatus(newNonMember: NonMemberDomain, shareUrlId: Long) { @@ -176,7 +210,6 @@ class NonMemberService( ) } - // 모든 NonMemberUpdateStatusDomain 저장 nonMemberUpdateStatusRepository.saveAll(updateStatusForNewMember + updateStatusForExistingMembers) } @@ -185,13 +218,14 @@ class NonMemberService( ): BookmarkData { val nonMemberPlaces = mutableListOf() val bookmarks = mutableListOf() - val newSids = mutableSetOf() + val newSids = mutableSetOf() // 중복 확인을 위한 sid 집합 bookmarkUrls.forEach { url -> val expandedUrl = UrlUtil.expandShortUrl(url) val bookmarkList = naverMapClient.bookmarkUrlToBookmarkLists(expandedUrl).bookmarkList bookmarkList.forEach { bookmark -> + // MongoDB에 존재하지 않는 경우에만 BookmarkDomain 생성 if (!isBookmarkExists(bookmark.sid)) { bookmarks.add( BookmarkDomain( @@ -205,26 +239,29 @@ class NonMemberService( ) ) } - if (nonMemberPlaces.none { it.sid == bookmark.sid }) { + + // 중복되지 않는 경우에만 NonMemberPlace 추가 + if (newSids.add(bookmark.sid)) { nonMemberPlaces.add(NonMemberPlaceDomain.of(nonMember, bookmark.sid)) } - newSids.add(bookmark.sid) } } return BookmarkData(nonMemberPlaces, bookmarks, newSids) } + // storeUrls에서 중복되지 않는 NonMemberPlaceDomain과 BookmarkDomain을 생성하고 반환 private fun handleStoreUrls( storeUrls: List, nonMember: NonMemberDomain ): BookmarkData { val nonMemberPlaces = mutableListOf() val bookmarks = mutableListOf() - val newSids = mutableSetOf() + val newSids = mutableSetOf() // 중복 확인을 위한 sid 집합 storeUrls.forEach { url -> val expandedUrl = UrlUtil.expandShortUrl(url) val store = naverMapClient.storeUrlToBookmark(expandedUrl) + // MongoDB에 존재하지 않는 경우에만 BookmarkDomain 생성 if (!isBookmarkExists(store.sid)) { bookmarks.add( BookmarkDomain( @@ -238,10 +275,11 @@ class NonMemberService( ) ) } - if (nonMemberPlaces.none { it.sid == store.sid }) { + + // 중복되지 않는 경우에만 NonMemberPlace 추가 + if (newSids.add(store.sid)) { nonMemberPlaces.add(NonMemberPlaceDomain.of(nonMember, store.sid)) } - newSids.add(store.sid) } return BookmarkData(nonMemberPlaces, bookmarks, newSids) } diff --git a/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/repositoryImpl/NonMemberStoreUrlRepositoryImpl.kt b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/repositoryImpl/NonMemberStoreUrlRepositoryImpl.kt index dc489bc..d8b730d 100644 --- a/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/repositoryImpl/NonMemberStoreUrlRepositoryImpl.kt +++ b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/repositoryImpl/NonMemberStoreUrlRepositoryImpl.kt @@ -10,7 +10,7 @@ import org.springframework.stereotype.Repository class NonMemberStoreUrlRepositoryImpl( private val nonMemberStoreUrlJpaRepository: NonMemberStoreUrlJpaRepository ) : NonMemberStoreUrlRepository { - override fun saveALl(nonMemberStoreUrlDomains: List): List { + override fun saveAll(nonMemberStoreUrlDomains: List): List { return nonMemberStoreUrlJpaRepository.saveAll(nonMemberStoreUrlDomains.map { NonMemberStoreUrlMapper.toEntity(it) }) .map { NonMemberStoreUrlMapper.toDomain(it) } } From 27aa496a0ccc181044e3dcc4c41578711ca5d7e0 Mon Sep 17 00:00:00 2001 From: codrin2 Date: Wed, 30 Oct 2024 01:50:15 +0900 Subject: [PATCH 056/203] =?UTF-8?q?feat(NaverApiClient):=20=20=EC=A2=8C?= =?UTF-8?q?=ED=91=9C=20=EB=B0=98=ED=99=98=20=EB=A1=9C=EC=A7=81=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ping/api/nonmember/NonMemberController.kt | 2 +- .../com/ping/api/place/PlaceController.kt | 5 +---- .../com/ping/application/place/PlaceService.kt | 11 ----------- .../ping/application/place/dto/SearchPlace.kt | 2 -- .../ping/client/naver/place/NaverApiClient.kt | 18 ++++++++++++++++++ .../client/naver/place/NaverApiResponse.kt | 11 +++++++++++ 6 files changed, 31 insertions(+), 18 deletions(-) diff --git a/Ping-Api/src/main/kotlin/com/ping/api/nonmember/NonMemberController.kt b/Ping-Api/src/main/kotlin/com/ping/api/nonmember/NonMemberController.kt index 57885b0..2be74f1 100644 --- a/Ping-Api/src/main/kotlin/com/ping/api/nonmember/NonMemberController.kt +++ b/Ping-Api/src/main/kotlin/com/ping/api/nonmember/NonMemberController.kt @@ -44,5 +44,5 @@ class NonMemberController( @PutMapping("/pings") fun updateNonMemberPings(@RequestBody request: UpdateNonMemberPings.Request) { nonMemberService.updateNonMemberPings(request) - + } } \ No newline at end of file diff --git a/Ping-Api/src/main/kotlin/com/ping/api/place/PlaceController.kt b/Ping-Api/src/main/kotlin/com/ping/api/place/PlaceController.kt index facfc23..adcebb8 100644 --- a/Ping-Api/src/main/kotlin/com/ping/api/place/PlaceController.kt +++ b/Ping-Api/src/main/kotlin/com/ping/api/place/PlaceController.kt @@ -14,13 +14,10 @@ class PlaceController( private val placeService: PlaceService ) { - // 장소 검색 API @GetMapping("/search/{keyword}") fun searchPlace(@PathVariable("keyword") keyword: String): ResponseEntity>> { val response = placeService.searchPlace(keyword); - return ResponseEntity.ok( - SuccessResponse.of(HttpStatus.OK, "장소 검색에 성공하였습니다.", response) - ) + return ResponseEntity.ok(SuccessResponse.of(HttpStatus.OK, "장소 검색에 성공하였습니다.", response)) } } \ No newline at end of file diff --git a/Ping-Application/src/main/kotlin/com/ping/application/place/PlaceService.kt b/Ping-Application/src/main/kotlin/com/ping/application/place/PlaceService.kt index 7801e8b..60cc848 100644 --- a/Ping-Application/src/main/kotlin/com/ping/application/place/PlaceService.kt +++ b/Ping-Application/src/main/kotlin/com/ping/application/place/PlaceService.kt @@ -12,7 +12,6 @@ class PlaceService( private val naverApiClient: NaverApiClient, ) { - // 장소 검색 fun searchPlace(keyword: String): List { return naverApiClient.searchPlaces(keyword).map { SearchPlace.Response( @@ -24,14 +23,4 @@ class PlaceService( } } - // 장소 저장 - @Transactional - fun savePlace(request: SavePlace.Request) { - val place = PlaceDomain( - name = request.name, - address = request.address, - latitude = request.latitude, - longitude = request.longitude - ) - } } \ No newline at end of file diff --git a/Ping-Application/src/main/kotlin/com/ping/application/place/dto/SearchPlace.kt b/Ping-Application/src/main/kotlin/com/ping/application/place/dto/SearchPlace.kt index 65868bb..6e2d6f0 100644 --- a/Ping-Application/src/main/kotlin/com/ping/application/place/dto/SearchPlace.kt +++ b/Ping-Application/src/main/kotlin/com/ping/application/place/dto/SearchPlace.kt @@ -1,12 +1,10 @@ package com.ping.application.place.dto class SearchPlace { - data class Response( val name: String, val address: String, val latitude: Double, val longitude: Double ) - } \ No newline at end of file diff --git a/Ping-Client/src/main/kotlin/com/ping/client/naver/place/NaverApiClient.kt b/Ping-Client/src/main/kotlin/com/ping/client/naver/place/NaverApiClient.kt index c7e6c61..b206561 100644 --- a/Ping-Client/src/main/kotlin/com/ping/client/naver/place/NaverApiClient.kt +++ b/Ping-Client/src/main/kotlin/com/ping/client/naver/place/NaverApiClient.kt @@ -24,4 +24,22 @@ class NaverApiClient( return response?.items ?: emptyList() } + + fun getGeocodeAddress(address: String): Pair { + val client = WebClient.create("https://naveropenapi.apigw.ntruss.com/map-geocode/v2/geocode") + val response = client.get() + .uri { uriBuilder -> uriBuilder.queryParam("query", address).build() } + .header("X-NCP-APIGW-API-KEY-ID", clientId) + .header("X-NCP-APIGW-API-KEY", clientSecret) + .retrieve() + .bodyToMono(NaverApiResponse.NaverGeocodeResponse::class.java) + .block() + + val location = response?.addresses?.firstOrNull() + return if (location != null) { + Pair(location.y, location.x) + } else { + Pair(null, null) + } + } } diff --git a/Ping-Client/src/main/kotlin/com/ping/client/naver/place/NaverApiResponse.kt b/Ping-Client/src/main/kotlin/com/ping/client/naver/place/NaverApiResponse.kt index b26a531..f4b5b38 100644 --- a/Ping-Client/src/main/kotlin/com/ping/client/naver/place/NaverApiResponse.kt +++ b/Ping-Client/src/main/kotlin/com/ping/client/naver/place/NaverApiResponse.kt @@ -11,4 +11,15 @@ class NaverApiResponse { val mapx: Double, val mapy: Double ) + + data class Address( + val roadAddress: String, + val jibunAddress: String, + val x: Double, // 경도 + val y: Double // 위도 + ) + + data class NaverGeocodeResponse( + val addresses: List
+ ) } \ No newline at end of file From 2aa9862dac9a2ad389701554a2a264ec6bd5af0e Mon Sep 17 00:00:00 2001 From: codrin2 Date: Wed, 30 Oct 2024 01:57:59 +0900 Subject: [PATCH 057/203] =?UTF-8?q?feat(PlaceService):=20=EC=A2=8C?= =?UTF-8?q?=ED=91=9C=20=EB=B0=98=ED=99=98=20=EB=A1=9C=EC=A7=81=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/ping/application/place/PlaceService.kt | 10 ++++++++++ .../com/ping/application/place/dto/GeocodePlace.kt | 13 +++++++++++++ 2 files changed, 23 insertions(+) create mode 100644 Ping-Application/src/main/kotlin/com/ping/application/place/dto/GeocodePlace.kt diff --git a/Ping-Application/src/main/kotlin/com/ping/application/place/PlaceService.kt b/Ping-Application/src/main/kotlin/com/ping/application/place/PlaceService.kt index 60cc848..973281a 100644 --- a/Ping-Application/src/main/kotlin/com/ping/application/place/PlaceService.kt +++ b/Ping-Application/src/main/kotlin/com/ping/application/place/PlaceService.kt @@ -1,5 +1,6 @@ package com.ping.application.place +import com.ping.application.place.dto.GeocodePlace import com.ping.application.place.dto.SavePlace import com.ping.application.place.dto.SearchPlace import com.ping.client.naver.place.NaverApiClient @@ -23,4 +24,13 @@ class PlaceService( } } + fun getGeocodeAddress(request: GeocodePlace.Request): GeocodePlace.Response { + val (latitude, longitude) = naverApiClient.getGeocodeAddress(request.address) + return GeocodePlace.Response( + address = request.address, + latitude = latitude, + longitude = longitude + ) + } + } \ No newline at end of file diff --git a/Ping-Application/src/main/kotlin/com/ping/application/place/dto/GeocodePlace.kt b/Ping-Application/src/main/kotlin/com/ping/application/place/dto/GeocodePlace.kt new file mode 100644 index 0000000..461a4d7 --- /dev/null +++ b/Ping-Application/src/main/kotlin/com/ping/application/place/dto/GeocodePlace.kt @@ -0,0 +1,13 @@ +package com.ping.application.place.dto + +class GeocodePlace { + data class Request( + val address: String + ) + + data class Response( + val address: String, + val latitude: Double?, + val longitude: Double? + ) +} \ No newline at end of file From 14f5406900f8183a45ce14ca7e2621f4efb42a96 Mon Sep 17 00:00:00 2001 From: codrin2 Date: Wed, 30 Oct 2024 02:05:49 +0900 Subject: [PATCH 058/203] =?UTF-8?q?feat(PlaceController):=20=EC=A2=8C?= =?UTF-8?q?=ED=91=9C=20=EC=83=9D=EC=84=B1=20API=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/kotlin/com/ping/api/place/PlaceController.kt | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Ping-Api/src/main/kotlin/com/ping/api/place/PlaceController.kt b/Ping-Api/src/main/kotlin/com/ping/api/place/PlaceController.kt index adcebb8..b46d803 100644 --- a/Ping-Api/src/main/kotlin/com/ping/api/place/PlaceController.kt +++ b/Ping-Api/src/main/kotlin/com/ping/api/place/PlaceController.kt @@ -2,6 +2,7 @@ package com.ping.api.place import com.ping.application.event.dto.CreateEvent import com.ping.application.place.PlaceService +import com.ping.application.place.dto.GeocodePlace import com.ping.application.place.dto.SearchPlace import com.ping.common.exception.SuccessResponse import org.springframework.http.HttpStatus @@ -20,4 +21,10 @@ class PlaceController( return ResponseEntity.ok(SuccessResponse.of(HttpStatus.OK, "장소 검색에 성공하였습니다.", response)) } + @PostMapping("/geocode") + fun geocodePlace(@RequestBody request: GeocodePlace.Request): ResponseEntity> { + val response = placeService.getGeocodeAddress(request) + return ResponseEntity.ok(SuccessResponse.of(HttpStatus.OK, "좌표 검색에 성공하였습니다.", response)) + } + } \ No newline at end of file From 1072113452d48891001905ac347ca3b7252f0b6b Mon Sep 17 00:00:00 2001 From: codrin2 Date: Wed, 30 Oct 2024 10:09:44 +0900 Subject: [PATCH 059/203] =?UTF-8?q?feat:=20=EB=84=A4=EC=9D=B4=EB=B2=84=20?= =?UTF-8?q?=ED=81=B4=EB=9D=BC=EC=9A=B0=EB=93=9C=20=EC=8B=9C=ED=81=AC?= =?UTF-8?q?=EB=A6=BF=20=ED=82=A4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/kotlin/com/ping/api/event/EventApi.kt | 5 +++++ .../kotlin/com/ping/api/event/EventController.kt | 13 +++---------- .../src/main/kotlin/com/ping/api/place/PlaceApi.kt | 7 +++++++ .../kotlin/com/ping/api/place/PlaceController.kt | 11 +++++------ .../com/ping/application/place/PlaceService.kt | 6 +++--- .../com/ping/client/naver/place/NaverApiClient.kt | 8 +++++--- 6 files changed, 28 insertions(+), 22 deletions(-) create mode 100644 Ping-Api/src/main/kotlin/com/ping/api/event/EventApi.kt create mode 100644 Ping-Api/src/main/kotlin/com/ping/api/place/PlaceApi.kt diff --git a/Ping-Api/src/main/kotlin/com/ping/api/event/EventApi.kt b/Ping-Api/src/main/kotlin/com/ping/api/event/EventApi.kt new file mode 100644 index 0000000..6f2fddf --- /dev/null +++ b/Ping-Api/src/main/kotlin/com/ping/api/event/EventApi.kt @@ -0,0 +1,5 @@ +package com.ping.api.event + +object EventApi { + const val BASE_URL = "/event" +} \ No newline at end of file diff --git a/Ping-Api/src/main/kotlin/com/ping/api/event/EventController.kt b/Ping-Api/src/main/kotlin/com/ping/api/event/EventController.kt index 1b7b567..4af1795 100644 --- a/Ping-Api/src/main/kotlin/com/ping/api/event/EventController.kt +++ b/Ping-Api/src/main/kotlin/com/ping/api/event/EventController.kt @@ -13,19 +13,12 @@ import org.springframework.web.bind.annotation.RestController @Tag(name = "이벤트") @RestController -@RequestMapping("/event") class EventController( private val eventService: EventService ) { - @PostMapping - fun create( - @RequestBody request: CreateEvent.Request - ): ResponseEntity> { + @PostMapping(EventApi.BASE_URL) + fun create(@RequestBody request: CreateEvent.Request): ResponseEntity> { val response = eventService.create(request) - - return ResponseEntity.ok( - SuccessResponse.of(HttpStatus.OK, "공유 URL이 성공적으로 생성되었습니다.", response) - ) + return ResponseEntity.ok(SuccessResponse.of(HttpStatus.OK, "공유 URL이 성공적으로 생성되었습니다.", response)) } - } \ No newline at end of file diff --git a/Ping-Api/src/main/kotlin/com/ping/api/place/PlaceApi.kt b/Ping-Api/src/main/kotlin/com/ping/api/place/PlaceApi.kt new file mode 100644 index 0000000..89658ba --- /dev/null +++ b/Ping-Api/src/main/kotlin/com/ping/api/place/PlaceApi.kt @@ -0,0 +1,7 @@ +package com.ping.api.place + +object PlaceApi { + const val BASE_URL = "/places" + const val SEARCH = "$BASE_URL/search" + const val GEOCODE = "$BASE_URL/geocode" +} diff --git a/Ping-Api/src/main/kotlin/com/ping/api/place/PlaceController.kt b/Ping-Api/src/main/kotlin/com/ping/api/place/PlaceController.kt index b46d803..1495c45 100644 --- a/Ping-Api/src/main/kotlin/com/ping/api/place/PlaceController.kt +++ b/Ping-Api/src/main/kotlin/com/ping/api/place/PlaceController.kt @@ -10,20 +10,19 @@ import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.* @RestController -@RequestMapping("/places") class PlaceController( private val placeService: PlaceService ) { - @GetMapping("/search/{keyword}") - fun searchPlace(@PathVariable("keyword") keyword: String): ResponseEntity>> { + @GetMapping(PlaceApi.SEARCH) + fun searchPlace(@RequestParam("keyword") keyword: String): ResponseEntity>> { val response = placeService.searchPlace(keyword); return ResponseEntity.ok(SuccessResponse.of(HttpStatus.OK, "장소 검색에 성공하였습니다.", response)) } - @PostMapping("/geocode") - fun geocodePlace(@RequestBody request: GeocodePlace.Request): ResponseEntity> { - val response = placeService.getGeocodeAddress(request) + @GetMapping(PlaceApi.GEOCODE) + fun geocodePlace(@RequestParam("address") address: String): ResponseEntity> { + val response = placeService.getGeocodeAddress(address) return ResponseEntity.ok(SuccessResponse.of(HttpStatus.OK, "좌표 검색에 성공하였습니다.", response)) } diff --git a/Ping-Application/src/main/kotlin/com/ping/application/place/PlaceService.kt b/Ping-Application/src/main/kotlin/com/ping/application/place/PlaceService.kt index 973281a..e0e2d9e 100644 --- a/Ping-Application/src/main/kotlin/com/ping/application/place/PlaceService.kt +++ b/Ping-Application/src/main/kotlin/com/ping/application/place/PlaceService.kt @@ -24,10 +24,10 @@ class PlaceService( } } - fun getGeocodeAddress(request: GeocodePlace.Request): GeocodePlace.Response { - val (latitude, longitude) = naverApiClient.getGeocodeAddress(request.address) + fun getGeocodeAddress(address: String): GeocodePlace.Response { + val (latitude, longitude) = naverApiClient.getGeocodeAddress(address) return GeocodePlace.Response( - address = request.address, + address = address, latitude = latitude, longitude = longitude ) diff --git a/Ping-Client/src/main/kotlin/com/ping/client/naver/place/NaverApiClient.kt b/Ping-Client/src/main/kotlin/com/ping/client/naver/place/NaverApiClient.kt index b206561..1287867 100644 --- a/Ping-Client/src/main/kotlin/com/ping/client/naver/place/NaverApiClient.kt +++ b/Ping-Client/src/main/kotlin/com/ping/client/naver/place/NaverApiClient.kt @@ -7,7 +7,9 @@ import org.springframework.web.reactive.function.client.WebClient @Component class NaverApiClient( @Value("\${naver.client.id}") private val clientId: String, - @Value("\${naver.client.secret}") private val clientSecret: String + @Value("\${naver.client.secret}") private val clientSecret: String, + @Value("\${naver.cloud.id}") private val cloudId: String, + @Value("\${naver.cloud.secret}") private val cloudSecret: String ) { fun searchPlaces(keyword: String): List { @@ -29,8 +31,8 @@ class NaverApiClient( val client = WebClient.create("https://naveropenapi.apigw.ntruss.com/map-geocode/v2/geocode") val response = client.get() .uri { uriBuilder -> uriBuilder.queryParam("query", address).build() } - .header("X-NCP-APIGW-API-KEY-ID", clientId) - .header("X-NCP-APIGW-API-KEY", clientSecret) + .header("X-NCP-APIGW-API-KEY-ID", cloudId) + .header("X-NCP-APIGW-API-KEY", cloudSecret) .retrieve() .bodyToMono(NaverApiResponse.NaverGeocodeResponse::class.java) .block() From 320dd1e3e1556213fca7cafaeadc07d45f1d0348 Mon Sep 17 00:00:00 2001 From: codrin2 Date: Wed, 30 Oct 2024 11:06:46 +0900 Subject: [PATCH 060/203] =?UTF-8?q?feat:=20=EB=84=A4=EC=9D=B4=EB=B2=84=20?= =?UTF-8?q?=ED=81=B4=EB=9D=BC=EC=9A=B0=EB=93=9C=20=EC=8B=9C=ED=81=AC?= =?UTF-8?q?=EB=A6=BF=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Ping-Api/src/main/resources/application-prod.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Ping-Api/src/main/resources/application-prod.yaml b/Ping-Api/src/main/resources/application-prod.yaml index 8f10cc5..115aef9 100644 --- a/Ping-Api/src/main/resources/application-prod.yaml +++ b/Ping-Api/src/main/resources/application-prod.yaml @@ -31,6 +31,9 @@ naver: client: id: ${NAVER_CLIENT_ID} secret: ${NAVER_CLIENT_SECRET} + cloud: + id: ${NAVER_CLOUD_ID} + secret: ${NAVER_CLOUD_SECRET} pingping: share: From 667a17947af82ab8fbc4bca8f4af0e3b7aa815bf Mon Sep 17 00:00:00 2001 From: codrin2 Date: Wed, 30 Oct 2024 13:01:10 +0900 Subject: [PATCH 061/203] =?UTF-8?q?test:=20EventController=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EB=B0=8F=20Restdocs?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Ping-Api/src/docs/asciidoc/Event.adoc | 6 ++ Ping-Api/src/docs/asciidoc/index.adoc | 1 + .../com/ping/api/event/EventControllerTest.kt | 65 +++++++++++++++++++ .../ping/application/event/EventService.kt | 2 +- .../ping/application/event/dto/CreateEvent.kt | 4 +- .../ping/application/place/PlaceService.kt | 8 +-- .../application/place/dto/GeocodePlace.kt | 4 +- .../ping/application/place/dto/SearchPlace.kt | 4 +- .../ping/client/naver/place/NaverApiClient.kt | 2 +- 9 files changed, 84 insertions(+), 12 deletions(-) create mode 100644 Ping-Api/src/docs/asciidoc/Event.adoc create mode 100644 Ping-Api/src/test/kotlin/com/ping/api/event/EventControllerTest.kt diff --git a/Ping-Api/src/docs/asciidoc/Event.adoc b/Ping-Api/src/docs/asciidoc/Event.adoc new file mode 100644 index 0000000..5230d1a --- /dev/null +++ b/Ping-Api/src/docs/asciidoc/Event.adoc @@ -0,0 +1,6 @@ +[[Events-API]] +== Events API + +[[Post-Event]] +=== 이벤트 생성 +operation::EventControllerTest/createEvent[snippets='http-request,request-fields,http-response,response-fields'] diff --git a/Ping-Api/src/docs/asciidoc/index.adoc b/Ping-Api/src/docs/asciidoc/index.adoc index a79117a..5d3c79c 100644 --- a/Ping-Api/src/docs/asciidoc/index.adoc +++ b/Ping-Api/src/docs/asciidoc/index.adoc @@ -6,4 +6,5 @@ :toclevels: 2 :sectlinks: +include::event.adoc[] include::NonMember.adoc[] \ No newline at end of file diff --git a/Ping-Api/src/test/kotlin/com/ping/api/event/EventControllerTest.kt b/Ping-Api/src/test/kotlin/com/ping/api/event/EventControllerTest.kt new file mode 100644 index 0000000..8cfa7ea --- /dev/null +++ b/Ping-Api/src/test/kotlin/com/ping/api/event/EventControllerTest.kt @@ -0,0 +1,65 @@ +package com.ping.api.event + +import com.ping.api.global.BaseRestDocsTest +import com.ping.application.event.EventService +import com.ping.application.event.dto.CreateEvent +import com.ping.infra.nonmember.domain.mongo.repository.BookmarkMongoRepository +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.mockito.BDDMockito.given +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest +import org.springframework.boot.test.mock.mockito.MockBean +import org.springframework.http.MediaType +import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders +import org.springframework.restdocs.payload.PayloadDocumentation.* +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status + +@WebMvcTest(EventController::class) +class EventControllerTest : BaseRestDocsTest() { + + @MockBean + private lateinit var eventService: EventService + + @MockBean + private lateinit var bookmarkMongoRepository: BookmarkMongoRepository + + @Test + @DisplayName("이벤트 생성") + fun createEvent() { + // given + val createEventRequest = CreateEvent.Request( + neighborhood = "홍익대학교", + px = 126.9256698, + py = 37.5507353, + eventName = "힙한모임" + ) + val createEventResponse = CreateEvent.Response( + shareUrl = "https://moping.co.kr/share/힙한모임/홍익대학교/12345678" + ) + given(eventService.create(createEventRequest)).willReturn(createEventResponse) + + // when + val request = RestDocumentationRequestBuilders.post(EventApi.BASE_URL) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .content(objectMapper.writeValueAsString(createEventRequest)) + + // then + val result = mockMvc.perform(request) + result.andExpect(status().isOk) + .andDo( + resultHandler.document( + requestFields( + fieldWithPath("neighborhood").description("이벤트 장소"), + fieldWithPath("px").description("이벤트 장소의 위도"), + fieldWithPath("py").description("이벤트 장소의 경도"), + fieldWithPath("eventName").description("이벤트 이름") + ), + responseFields( + fieldWithPath("code").description("응답 코드"), + fieldWithPath("message").description("응답 메시지"), + fieldWithPath("data.shareUrl").description("공유할 이벤트 URL") + ) + ) + ) + } +} diff --git a/Ping-Application/src/main/kotlin/com/ping/application/event/EventService.kt b/Ping-Application/src/main/kotlin/com/ping/application/event/EventService.kt index 6e06feb..7bd83e1 100644 --- a/Ping-Application/src/main/kotlin/com/ping/application/event/EventService.kt +++ b/Ping-Application/src/main/kotlin/com/ping/application/event/EventService.kt @@ -21,7 +21,7 @@ class EventService( val uniqueId = UUID.randomUUID().toString().substring(0, 8) val uniqueUrl = generateUniqueUrl(request, uniqueId) - val shareUrl = ShareUrlDomain(0,uniqueUrl, request.eventName, request.neighborhood,request.mapx, request.mapy, uniqueId) + val shareUrl = ShareUrlDomain(0,uniqueUrl, request.eventName, request.neighborhood,request.px, request.py, uniqueId) val savedShareUrlDomain = shareUrlRepository.save(shareUrl) return CreateEvent.Response(savedShareUrlDomain.url) diff --git a/Ping-Application/src/main/kotlin/com/ping/application/event/dto/CreateEvent.kt b/Ping-Application/src/main/kotlin/com/ping/application/event/dto/CreateEvent.kt index b1a9275..60a15ac 100644 --- a/Ping-Application/src/main/kotlin/com/ping/application/event/dto/CreateEvent.kt +++ b/Ping-Application/src/main/kotlin/com/ping/application/event/dto/CreateEvent.kt @@ -3,8 +3,8 @@ package com.ping.application.event.dto class CreateEvent { data class Request( val neighborhood: String, - val mapx: Double, - val mapy: Double, + val px: Double, + val py: Double, val eventName: String ) data class Response( diff --git a/Ping-Application/src/main/kotlin/com/ping/application/place/PlaceService.kt b/Ping-Application/src/main/kotlin/com/ping/application/place/PlaceService.kt index e0e2d9e..f04b542 100644 --- a/Ping-Application/src/main/kotlin/com/ping/application/place/PlaceService.kt +++ b/Ping-Application/src/main/kotlin/com/ping/application/place/PlaceService.kt @@ -18,8 +18,8 @@ class PlaceService( SearchPlace.Response( name = it.title.replace("", "").replace("", ""), address = it.address, - latitude = it.mapx, - longitude = it.mapy + px = it.mapx, + py = it.mapy ) } } @@ -28,8 +28,8 @@ class PlaceService( val (latitude, longitude) = naverApiClient.getGeocodeAddress(address) return GeocodePlace.Response( address = address, - latitude = latitude, - longitude = longitude + px = latitude, + py = longitude ) } diff --git a/Ping-Application/src/main/kotlin/com/ping/application/place/dto/GeocodePlace.kt b/Ping-Application/src/main/kotlin/com/ping/application/place/dto/GeocodePlace.kt index 461a4d7..4e3a75d 100644 --- a/Ping-Application/src/main/kotlin/com/ping/application/place/dto/GeocodePlace.kt +++ b/Ping-Application/src/main/kotlin/com/ping/application/place/dto/GeocodePlace.kt @@ -7,7 +7,7 @@ class GeocodePlace { data class Response( val address: String, - val latitude: Double?, - val longitude: Double? + val px: Double?, + val py: Double? ) } \ No newline at end of file diff --git a/Ping-Application/src/main/kotlin/com/ping/application/place/dto/SearchPlace.kt b/Ping-Application/src/main/kotlin/com/ping/application/place/dto/SearchPlace.kt index 6e2d6f0..38d51e6 100644 --- a/Ping-Application/src/main/kotlin/com/ping/application/place/dto/SearchPlace.kt +++ b/Ping-Application/src/main/kotlin/com/ping/application/place/dto/SearchPlace.kt @@ -4,7 +4,7 @@ class SearchPlace { data class Response( val name: String, val address: String, - val latitude: Double, - val longitude: Double + val px: Double, + val py: Double ) } \ No newline at end of file diff --git a/Ping-Client/src/main/kotlin/com/ping/client/naver/place/NaverApiClient.kt b/Ping-Client/src/main/kotlin/com/ping/client/naver/place/NaverApiClient.kt index 1287867..9c39ae4 100644 --- a/Ping-Client/src/main/kotlin/com/ping/client/naver/place/NaverApiClient.kt +++ b/Ping-Client/src/main/kotlin/com/ping/client/naver/place/NaverApiClient.kt @@ -39,7 +39,7 @@ class NaverApiClient( val location = response?.addresses?.firstOrNull() return if (location != null) { - Pair(location.y, location.x) + Pair(location.x, location.y) } else { Pair(null, null) } From b4a28f2ca767f5781d4d211904b3fcdf9eb07305 Mon Sep 17 00:00:00 2001 From: codrin2 Date: Wed, 30 Oct 2024 14:07:59 +0900 Subject: [PATCH 062/203] =?UTF-8?q?test:=20PlaceController,=20NonMemberCon?= =?UTF-8?q?troller=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EB=B0=8F=20Restdocs=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Ping-Api/build.gradle.kts | 1 + Ping-Api/src/docs/asciidoc/NonMember.adoc | 10 +- Ping-Api/src/docs/asciidoc/Place.adoc | 10 ++ Ping-Api/src/docs/asciidoc/index.adoc | 3 +- .../com/ping/api/nonmember/NonMemberApi.kt | 1 + .../ping/api/nonmember/NonMemberController.kt | 9 +- .../api/nonmember/NonMemberControllerTest.kt | 120 ++++++++++++++++- .../com/ping/api/place/PlaceControllerTest.kt | 123 ++++++++++++++++++ .../ping/application/place/PlaceService.kt | 3 - 9 files changed, 266 insertions(+), 14 deletions(-) create mode 100644 Ping-Api/src/docs/asciidoc/Place.adoc create mode 100644 Ping-Api/src/test/kotlin/com/ping/api/place/PlaceControllerTest.kt diff --git a/Ping-Api/build.gradle.kts b/Ping-Api/build.gradle.kts index 879c633..253e26e 100644 --- a/Ping-Api/build.gradle.kts +++ b/Ping-Api/build.gradle.kts @@ -22,6 +22,7 @@ dependencies { implementation("org.springframework.boot:spring-boot-starter-data-mongodb") //RestDocs + testImplementation("com.epages:restdocs-api-spec-mockmvc:0.19.2") testImplementation("org.springframework.restdocs:spring-restdocs-mockmvc") asciidoctorExt("org.springframework.restdocs:spring-restdocs-asciidoctor") testFixturesImplementation("org.springframework.restdocs:spring-restdocs-mockmvc") diff --git a/Ping-Api/src/docs/asciidoc/NonMember.adoc b/Ping-Api/src/docs/asciidoc/NonMember.adoc index fe464de..4079a21 100644 --- a/Ping-Api/src/docs/asciidoc/NonMember.adoc +++ b/Ping-Api/src/docs/asciidoc/NonMember.adoc @@ -11,4 +11,12 @@ operation::NonMemberControllerTest/getAllNonMemberPings[snippets='http-request,h [[Get-NonMemberPing]] === 개별 핑 불러오기 -operation::NonMemberControllerTest/getNonMemberPing[snippets='http-request,http-response,response-fields'] \ No newline at end of file +operation::NonMemberControllerTest/getNonMemberPing[snippets='http-request,http-response,response-fields'] + +[[Put-UpdateNonMemberPings]] +=== 비회원 핑 업데이트 +operation::NonMemberControllerTest/updateNonMemberPings[snippets='http-request,request-fields,http-response'] + +[[Post-NonMemberLogin]] +=== 비회원 로그인 +operation::NonMemberControllerTest/loginNonMember[snippets='http-request,request-fields,http-response'] \ No newline at end of file diff --git a/Ping-Api/src/docs/asciidoc/Place.adoc b/Ping-Api/src/docs/asciidoc/Place.adoc new file mode 100644 index 0000000..cfcec45 --- /dev/null +++ b/Ping-Api/src/docs/asciidoc/Place.adoc @@ -0,0 +1,10 @@ +[[Places-API]] +== Places API + +[[Search-Place]] +=== 장소 검색 +operation::PlaceControllerTest/searchPlace[snippets='http-request,http-response,request-parameters,response-fields'] + +[[Geocode-Place]] +=== 주소로 좌표 검색 +operation::PlaceControllerTest/geocodePlace[snippets='http-request,http-response,request-parameters,response-fields'] \ No newline at end of file diff --git a/Ping-Api/src/docs/asciidoc/index.adoc b/Ping-Api/src/docs/asciidoc/index.adoc index 5d3c79c..0aa1d83 100644 --- a/Ping-Api/src/docs/asciidoc/index.adoc +++ b/Ping-Api/src/docs/asciidoc/index.adoc @@ -7,4 +7,5 @@ :sectlinks: include::event.adoc[] -include::NonMember.adoc[] \ No newline at end of file +include::NonMember.adoc[] +include::place.adoc[] \ No newline at end of file diff --git a/Ping-Api/src/main/kotlin/com/ping/api/nonmember/NonMemberApi.kt b/Ping-Api/src/main/kotlin/com/ping/api/nonmember/NonMemberApi.kt index 6a7577e..da36b82 100644 --- a/Ping-Api/src/main/kotlin/com/ping/api/nonmember/NonMemberApi.kt +++ b/Ping-Api/src/main/kotlin/com/ping/api/nonmember/NonMemberApi.kt @@ -2,6 +2,7 @@ package com.ping.api.nonmember object NonMemberApi { const val BASE_URL = "/nonmembers" + const val LOGIN = "$BASE_URL/login" const val PING = "$BASE_URL/pings" const val PING_NONMEMBERID = "$PING/{nonMemberId}" } \ No newline at end of file diff --git a/Ping-Api/src/main/kotlin/com/ping/api/nonmember/NonMemberController.kt b/Ping-Api/src/main/kotlin/com/ping/api/nonmember/NonMemberController.kt index 2be74f1..080ecbd 100644 --- a/Ping-Api/src/main/kotlin/com/ping/api/nonmember/NonMemberController.kt +++ b/Ping-Api/src/main/kotlin/com/ping/api/nonmember/NonMemberController.kt @@ -17,12 +17,9 @@ class NonMemberController( private val nonMemberService: NonMemberService, private val nonMemberLoginService: NonMemberLoginService ) { - @PutMapping("/nonmembers/login") - fun loginNonMember( - @RequestBody request: LoginNonMember.Request - ): ResponseEntity> { + @PutMapping(NonMemberApi.LOGIN) + fun loginNonMember(@RequestBody request: LoginNonMember.Request): ResponseEntity> { nonMemberLoginService.login(request) - return ResponseEntity.ok(SuccessResponse.of(HttpStatus.OK, "비회원 로그인 성공")) } @@ -41,7 +38,7 @@ class NonMemberController( return nonMemberService.getNonMemberPing(nonMemberId) } - @PutMapping("/pings") + @PutMapping(NonMemberApi.PING) fun updateNonMemberPings(@RequestBody request: UpdateNonMemberPings.Request) { nonMemberService.updateNonMemberPings(request) } diff --git a/Ping-Api/src/test/kotlin/com/ping/api/nonmember/NonMemberControllerTest.kt b/Ping-Api/src/test/kotlin/com/ping/api/nonmember/NonMemberControllerTest.kt index 1e3e42f..fa61af8 100644 --- a/Ping-Api/src/test/kotlin/com/ping/api/nonmember/NonMemberControllerTest.kt +++ b/Ping-Api/src/test/kotlin/com/ping/api/nonmember/NonMemberControllerTest.kt @@ -1,22 +1,29 @@ package com.ping.api.nonmember +import com.epages.restdocs.apispec.MockMvcRestDocumentationWrapper +import com.epages.restdocs.apispec.ResourceDocumentation.resource +import com.epages.restdocs.apispec.ResourceSnippetParameters +import com.epages.restdocs.apispec.Schema import com.ping.api.global.BaseRestDocsTest import com.ping.application.nonmember.NonMemberLoginService import com.ping.application.nonmember.NonMemberService -import com.ping.application.nonmember.dto.CreateNonMember -import com.ping.application.nonmember.dto.GetAllNonMemberPings -import com.ping.application.nonmember.dto.GetNonMemberPing +import com.ping.application.nonmember.dto.* import com.ping.infra.nonmember.domain.mongo.repository.BookmarkMongoRepository import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test import org.mockito.BDDMockito.given +import org.mockito.Mockito.doNothing import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest import org.springframework.boot.test.mock.mockito.MockBean import org.springframework.http.MediaType import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders +import org.springframework.restdocs.operation.preprocess.Preprocessors.* import org.springframework.restdocs.payload.PayloadDocumentation.* +import org.springframework.test.web.servlet.ResultActions +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status + @WebMvcTest(NonMemberController::class) class NonMemberControllerTest : BaseRestDocsTest() { @@ -29,6 +36,57 @@ class NonMemberControllerTest : BaseRestDocsTest() { @MockBean private lateinit var bookmarkMongoRepository: BookmarkMongoRepository + @Test + @DisplayName("비회원 로그인") + fun loginNonMember() { + // given + val request = LoginNonMember.Request(nonMemberId = 1L, password = "1234") + + doNothing().`when`(nonMemberLoginService).login(request) + + val jsonRequest = """ + { + "nonMemberId": 1, + "password": "1234" + } + """ + + // when + val result: ResultActions = mockMvc.perform( + RestDocumentationRequestBuilders.put(NonMemberApi.LOGIN) + .content(jsonRequest) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON) + ) + + // then + result.andExpect(status().isOk) + .andExpect(jsonPath("$.code").value(200)) + .andExpect(jsonPath("$.message").value("비회원 로그인 성공")) + .andDo( + MockMvcRestDocumentationWrapper.document( + "nonmember/loginNonMember", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + resource( + ResourceSnippetParameters.builder() + .tag("NonMember") + .description("비회원 로그인") + .requestFields( + fieldWithPath("nonMemberId").description("비회원 ID"), + fieldWithPath("password").description("비회원 비밀번호") + ) + .responseFields( + fieldWithPath("code").description("응답 코드"), + fieldWithPath("message").description("응답 메시지"), + fieldWithPath("data").description("응답 데이터") + ) + .responseSchema(Schema.schema("CommonResponse")) + .build() + ) + ) + ) + } @Test @DisplayName("비회원 핑 생성") @@ -173,4 +231,60 @@ class NonMemberControllerTest : BaseRestDocsTest() { ) } + + @Test + @DisplayName("비회원 핑 업데이트") + fun updateNonMemberPings() { + // given + val request = UpdateNonMemberPings.Request( + nonMemberId = 1L, + bookmarkUrls = listOf("bookmarkUrl1", "bookmarkUrl2"), + storeUrls = listOf("storeUrl1") + ) + + doNothing().`when`(nonMemberService).updateNonMemberPings(request) + + val jsonRequest = """ + { + "nonMemberId": 1, + "bookmarkUrls": ["bookmarkUrl1", "bookmarkUrl2"], + "storeUrls": ["storeUrl1"] + } + """ + + // when + val result: ResultActions = mockMvc.perform( + RestDocumentationRequestBuilders.put(NonMemberApi.PING) + .content(jsonRequest) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON) + ) + + // then + result.andExpect(status().isOk) + .andDo( + MockMvcRestDocumentationWrapper.document( + "nonmember/updateNonMemberPings", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + resource( + ResourceSnippetParameters.builder() + .tag("NonMember") + .description("비회원 핑 업데이트") + .requestFields( + fieldWithPath("nonMemberId").description("비회원 ID"), + fieldWithPath("bookmarkUrls").description("업데이트할 북마크 URL 목록"), + fieldWithPath("storeUrls").description("업데이트할 스토어 URL 목록") + ) +// .responseFields( +// fieldWithPath("code").description("응답 코드"), +// fieldWithPath("message").description("응답 메시지"), +// fieldWithPath("data").description("응답 데이터") +// ) + .responseSchema(Schema.schema("CommonResponse")) + .build() + ) + ) + ) + } } \ No newline at end of file diff --git a/Ping-Api/src/test/kotlin/com/ping/api/place/PlaceControllerTest.kt b/Ping-Api/src/test/kotlin/com/ping/api/place/PlaceControllerTest.kt new file mode 100644 index 0000000..4c7c2cc --- /dev/null +++ b/Ping-Api/src/test/kotlin/com/ping/api/place/PlaceControllerTest.kt @@ -0,0 +1,123 @@ +package com.ping.api.place + +import com.epages.restdocs.apispec.MockMvcRestDocumentationWrapper +import com.epages.restdocs.apispec.ResourceDocumentation.parameterWithName +import com.epages.restdocs.apispec.ResourceDocumentation.resource +import com.epages.restdocs.apispec.ResourceSnippetParameters +import com.epages.restdocs.apispec.Schema +import com.ping.api.global.BaseRestDocsTest +import com.ping.application.place.PlaceService +import com.ping.application.place.dto.GeocodePlace +import com.ping.application.place.dto.SearchPlace +import com.ping.infra.nonmember.domain.mongo.repository.BookmarkMongoRepository +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.mockito.BDDMockito.given +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest +import org.springframework.boot.test.mock.mockito.MockBean +import org.springframework.http.MediaType +import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders +import org.springframework.restdocs.operation.preprocess.Preprocessors.* +import org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath +import org.springframework.test.web.servlet.ResultActions +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status + +@WebMvcTest(PlaceController::class) +class PlaceControllerTest : BaseRestDocsTest() { + + @MockBean + private lateinit var placeService: PlaceService + + @MockBean + private lateinit var bookmarkMongoRepository: BookmarkMongoRepository + + @Test + @DisplayName("장소 검색") + fun searchPlace() { + // given + val keyword = "홍대" + val searchPlaceResponse = listOf( + SearchPlace.Response("홍대입구", "서울 마포구", 126.9784, 37.5665), + SearchPlace.Response("홍익대학교", "서울 마포구", 126.9372, 37.5502) + ) + given(placeService.searchPlace(keyword)).willReturn(searchPlaceResponse) + + // when + val result: ResultActions = mockMvc.perform( + RestDocumentationRequestBuilders.get(PlaceApi.SEARCH) + .param("keyword", keyword) + .contentType(MediaType.APPLICATION_JSON) + ) + + // then + result.andExpect(status().isOk) + .andDo( + MockMvcRestDocumentationWrapper.document( + "place/searchPlace", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + resource( + ResourceSnippetParameters.builder() + .tag("장소") + .description("키워드로 장소 검색") + .queryParameters( + parameterWithName("keyword").description("검색할 장소의 키워드") + ) + .responseFields( + fieldWithPath("code").description("응답 코드"), + fieldWithPath("message").description("응답 메시지"), + fieldWithPath("data[].name").description("장소 이름"), + fieldWithPath("data[].address").description("장소 주소"), + fieldWithPath("data[].px").description("장소 위도"), + fieldWithPath("data[].py").description("장소 경도") + ) + .responseSchema(Schema.schema("SearchPlaceResponse")) + .build() + ) + ) + ) + } + + @Test + @DisplayName("주소로 좌표 검색") + fun geocodePlace() { + // given + val address = "서울시 중구 명동" + val geocodeResponse = GeocodePlace.Response("서울시 중구 명동", 126.9784, 37.5665) + given(placeService.getGeocodeAddress(address)).willReturn(geocodeResponse) + + // when + val result: ResultActions = mockMvc.perform( + RestDocumentationRequestBuilders.get(PlaceApi.GEOCODE) + .param("address", address) + .contentType(MediaType.APPLICATION_JSON) + ) + + // then + result.andExpect(status().isOk) + .andDo( + MockMvcRestDocumentationWrapper.document( + "place/geocodePlace", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + resource( + ResourceSnippetParameters.builder() + .tag("장소") + .description("주소로 좌표 검색") + .queryParameters( + parameterWithName("address").description("좌표를 얻고자 하는 주소") + ) + .responseFields( + fieldWithPath("code").description("응답 코드"), + fieldWithPath("message").description("응답 메시지"), + fieldWithPath("data.address").description("검색된 주소"), + fieldWithPath("data.px").description("주소의 위도"), + fieldWithPath("data.py").description("주소의 경도") + ) + .responseSchema(Schema.schema("GeocodePlaceResponse")) + .build() + ) + ) + ) + } +} diff --git a/Ping-Application/src/main/kotlin/com/ping/application/place/PlaceService.kt b/Ping-Application/src/main/kotlin/com/ping/application/place/PlaceService.kt index f04b542..5b8bc0f 100644 --- a/Ping-Application/src/main/kotlin/com/ping/application/place/PlaceService.kt +++ b/Ping-Application/src/main/kotlin/com/ping/application/place/PlaceService.kt @@ -1,12 +1,9 @@ package com.ping.application.place import com.ping.application.place.dto.GeocodePlace -import com.ping.application.place.dto.SavePlace import com.ping.application.place.dto.SearchPlace import com.ping.client.naver.place.NaverApiClient -import com.ping.domain.nonmember.aggregate.PlaceDomain import org.springframework.stereotype.Service -import org.springframework.transaction.annotation.Transactional @Service class PlaceService( From a2d4cbb7d54e37de6947d42a70780c5ea2b08215 Mon Sep 17 00:00:00 2001 From: codrin2 Date: Wed, 30 Oct 2024 14:33:40 +0900 Subject: [PATCH 063/203] =?UTF-8?q?fix:=20adoc=20=EC=9E=98=EB=AA=BB?= =?UTF-8?q?=EB=90=9C=20=EC=9D=B4=EB=A6=84=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Ping-Api/src/docs/asciidoc/index.adoc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Ping-Api/src/docs/asciidoc/index.adoc b/Ping-Api/src/docs/asciidoc/index.adoc index 0aa1d83..b1068ca 100644 --- a/Ping-Api/src/docs/asciidoc/index.adoc +++ b/Ping-Api/src/docs/asciidoc/index.adoc @@ -6,6 +6,6 @@ :toclevels: 2 :sectlinks: -include::event.adoc[] +include::Event.adoc[] include::NonMember.adoc[] -include::place.adoc[] \ No newline at end of file +include::Place.adoc[] \ No newline at end of file From 6d0b0c8012a9be1d7839d166b1224a00fe9a026a Mon Sep 17 00:00:00 2001 From: codrin2 Date: Wed, 30 Oct 2024 15:02:05 +0900 Subject: [PATCH 064/203] =?UTF-8?q?fix(NonMemberController):=20=EB=B9=84?= =?UTF-8?q?=ED=9A=8C=EC=9B=90=20login=20API=20=EB=B0=98=ED=99=98=EA=B0=92?= =?UTF-8?q?=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ping/api/nonmember/NonMemberController.kt | 4 ++-- .../com/ping/api/event/EventControllerTest.kt | 4 ++-- .../api/nonmember/NonMemberControllerTest.kt | 22 +++++++++---------- .../com/ping/api/place/PlaceControllerTest.kt | 8 +++---- 4 files changed, 19 insertions(+), 19 deletions(-) diff --git a/Ping-Api/src/main/kotlin/com/ping/api/nonmember/NonMemberController.kt b/Ping-Api/src/main/kotlin/com/ping/api/nonmember/NonMemberController.kt index 080ecbd..a2c49fe 100644 --- a/Ping-Api/src/main/kotlin/com/ping/api/nonmember/NonMemberController.kt +++ b/Ping-Api/src/main/kotlin/com/ping/api/nonmember/NonMemberController.kt @@ -18,9 +18,9 @@ class NonMemberController( private val nonMemberLoginService: NonMemberLoginService ) { @PutMapping(NonMemberApi.LOGIN) - fun loginNonMember(@RequestBody request: LoginNonMember.Request): ResponseEntity> { + fun loginNonMember(@RequestBody request: LoginNonMember.Request) { nonMemberLoginService.login(request) - return ResponseEntity.ok(SuccessResponse.of(HttpStatus.OK, "비회원 로그인 성공")) +// return ResponseEntity.ok(SuccessResponse.of(HttpStatus.OK, "비회원 로그인 성공")) } @PostMapping(NonMemberApi.PING) diff --git a/Ping-Api/src/test/kotlin/com/ping/api/event/EventControllerTest.kt b/Ping-Api/src/test/kotlin/com/ping/api/event/EventControllerTest.kt index 8cfa7ea..1438ebd 100644 --- a/Ping-Api/src/test/kotlin/com/ping/api/event/EventControllerTest.kt +++ b/Ping-Api/src/test/kotlin/com/ping/api/event/EventControllerTest.kt @@ -50,8 +50,8 @@ class EventControllerTest : BaseRestDocsTest() { resultHandler.document( requestFields( fieldWithPath("neighborhood").description("이벤트 장소"), - fieldWithPath("px").description("이벤트 장소의 위도"), - fieldWithPath("py").description("이벤트 장소의 경도"), + fieldWithPath("px").description("이벤트 장소의 경도"), + fieldWithPath("py").description("이벤트 장소의 위도"), fieldWithPath("eventName").description("이벤트 이름") ), responseFields( diff --git a/Ping-Api/src/test/kotlin/com/ping/api/nonmember/NonMemberControllerTest.kt b/Ping-Api/src/test/kotlin/com/ping/api/nonmember/NonMemberControllerTest.kt index fa61af8..c72dc60 100644 --- a/Ping-Api/src/test/kotlin/com/ping/api/nonmember/NonMemberControllerTest.kt +++ b/Ping-Api/src/test/kotlin/com/ping/api/nonmember/NonMemberControllerTest.kt @@ -61,8 +61,8 @@ class NonMemberControllerTest : BaseRestDocsTest() { // then result.andExpect(status().isOk) - .andExpect(jsonPath("$.code").value(200)) - .andExpect(jsonPath("$.message").value("비회원 로그인 성공")) +// .andExpect(jsonPath("$.code").value(200)) +// .andExpect(jsonPath("$.message").value("비회원 로그인 성공")) .andDo( MockMvcRestDocumentationWrapper.document( "nonmember/loginNonMember", @@ -76,11 +76,11 @@ class NonMemberControllerTest : BaseRestDocsTest() { fieldWithPath("nonMemberId").description("비회원 ID"), fieldWithPath("password").description("비회원 비밀번호") ) - .responseFields( - fieldWithPath("code").description("응답 코드"), - fieldWithPath("message").description("응답 메시지"), - fieldWithPath("data").description("응답 데이터") - ) +// .responseFields( +// fieldWithPath("code").description("응답 코드"), +// fieldWithPath("message").description("응답 메시지"), +// fieldWithPath("data").description("응답 데이터") +// ) .responseSchema(Schema.schema("CommonResponse")) .build() ) @@ -238,8 +238,8 @@ class NonMemberControllerTest : BaseRestDocsTest() { // given val request = UpdateNonMemberPings.Request( nonMemberId = 1L, - bookmarkUrls = listOf("bookmarkUrl1", "bookmarkUrl2"), - storeUrls = listOf("storeUrl1") + bookmarkUrls = listOf("https://naver.me/Fqimcb8B"), + storeUrls = listOf("https://naver.me/FuVzL1bq") ) doNothing().`when`(nonMemberService).updateNonMemberPings(request) @@ -247,8 +247,8 @@ class NonMemberControllerTest : BaseRestDocsTest() { val jsonRequest = """ { "nonMemberId": 1, - "bookmarkUrls": ["bookmarkUrl1", "bookmarkUrl2"], - "storeUrls": ["storeUrl1"] + "bookmarkUrls": ["https://naver.me/Fqimcb8B"], + "storeUrls": [ "https://naver.me/FuVzL1bq"] } """ diff --git a/Ping-Api/src/test/kotlin/com/ping/api/place/PlaceControllerTest.kt b/Ping-Api/src/test/kotlin/com/ping/api/place/PlaceControllerTest.kt index 4c7c2cc..b78d524 100644 --- a/Ping-Api/src/test/kotlin/com/ping/api/place/PlaceControllerTest.kt +++ b/Ping-Api/src/test/kotlin/com/ping/api/place/PlaceControllerTest.kt @@ -68,8 +68,8 @@ class PlaceControllerTest : BaseRestDocsTest() { fieldWithPath("message").description("응답 메시지"), fieldWithPath("data[].name").description("장소 이름"), fieldWithPath("data[].address").description("장소 주소"), - fieldWithPath("data[].px").description("장소 위도"), - fieldWithPath("data[].py").description("장소 경도") + fieldWithPath("data[].px").description("장소 경도"), + fieldWithPath("data[].py").description("장소 위도") ) .responseSchema(Schema.schema("SearchPlaceResponse")) .build() @@ -111,8 +111,8 @@ class PlaceControllerTest : BaseRestDocsTest() { fieldWithPath("code").description("응답 코드"), fieldWithPath("message").description("응답 메시지"), fieldWithPath("data.address").description("검색된 주소"), - fieldWithPath("data.px").description("주소의 위도"), - fieldWithPath("data.py").description("주소의 경도") + fieldWithPath("data.px").description("주소의 경도"), + fieldWithPath("data.py").description("주소의 위도") ) .responseSchema(Schema.schema("GeocodePlaceResponse")) .build() From 11171f8cec629b74fa4b87e3660b03f03c4a43ab Mon Sep 17 00:00:00 2001 From: codrin2 Date: Thu, 31 Oct 2024 04:44:54 +0900 Subject: [PATCH 065/203] =?UTF-8?q?feat:=20=EB=B9=84=ED=9A=8C=EC=9B=90=20?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EC=8B=9C=20=EB=B9=84=ED=9A=8C?= =?UTF-8?q?=EC=9B=90=EC=9D=B4=20=EC=A0=80=EC=9E=A5=ED=95=9C=20url=20?= =?UTF-8?q?=EB=B0=98=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ping/api/nonmember/NonMemberController.kt | 5 ++--- .../nonmember/NonMemberLoginService.kt | 22 +++++++++++++++---- .../nonmember/dto/LoginNonMember.kt | 7 ++++++ .../NonMemberBookmarkUrlRepository.kt | 1 + .../repository/NonMemberStoreUrlRepository.kt | 1 + .../NonMemberBookmarkUrlJpaRepository.kt | 1 + .../NonMemberStoreUrlJpaRepository.kt | 1 + .../NonMemberBookmarkUrlRepositoryImpl.kt | 5 +++++ .../NonMemberStoreUrlRepositoryImpl.kt | 5 +++++ 9 files changed, 41 insertions(+), 7 deletions(-) diff --git a/Ping-Api/src/main/kotlin/com/ping/api/nonmember/NonMemberController.kt b/Ping-Api/src/main/kotlin/com/ping/api/nonmember/NonMemberController.kt index a2c49fe..12ff8f4 100644 --- a/Ping-Api/src/main/kotlin/com/ping/api/nonmember/NonMemberController.kt +++ b/Ping-Api/src/main/kotlin/com/ping/api/nonmember/NonMemberController.kt @@ -18,9 +18,8 @@ class NonMemberController( private val nonMemberLoginService: NonMemberLoginService ) { @PutMapping(NonMemberApi.LOGIN) - fun loginNonMember(@RequestBody request: LoginNonMember.Request) { - nonMemberLoginService.login(request) -// return ResponseEntity.ok(SuccessResponse.of(HttpStatus.OK, "비회원 로그인 성공")) + fun loginNonMember(@RequestBody request: LoginNonMember.Request): LoginNonMember.Response { + return nonMemberLoginService.login(request) } @PostMapping(NonMemberApi.PING) diff --git a/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberLoginService.kt b/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberLoginService.kt index 8a64f45..8802bef 100644 --- a/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberLoginService.kt +++ b/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberLoginService.kt @@ -3,7 +3,9 @@ package com.ping.application.nonmember import com.ping.application.nonmember.dto.LoginNonMember import com.ping.common.exception.CustomException import com.ping.common.exception.ExceptionContent +import com.ping.domain.nonmember.repository.NonMemberBookmarkUrlRepository import com.ping.domain.nonmember.repository.NonMemberRepository +import com.ping.domain.nonmember.repository.NonMemberStoreUrlRepository import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional @@ -11,20 +13,32 @@ import org.springframework.transaction.annotation.Transactional @Transactional(readOnly = true) class NonMemberLoginService( private val nonMemberRepository: NonMemberRepository, + private val nonMemberBookmarkUrlRepository: NonMemberBookmarkUrlRepository, + private val nonMemberStoreUrlRepository: NonMemberStoreUrlRepository, private val validator: NonMemberValidator ) { - fun login(request: LoginNonMember.Request) { + fun login(request: LoginNonMember.Request): LoginNonMember.Response { // 비밀번호 형식 검사 (4자리 숫자) validator.password(request.password) - val nonMember = nonMemberRepository.findById(request.nonMemberId)?:let { - throw CustomException(ExceptionContent.NON_MEMBER_NOT_FOUND) - } + val nonMember = nonMemberRepository.findById(request.nonMemberId) ?: throw CustomException(ExceptionContent.NON_MEMBER_NOT_FOUND) // 비밀번호가 일치하는지 비교 if (request.password != nonMember.password) { throw CustomException(ExceptionContent.NON_MEMBER_LOGIN_FAILED) } + + // 비회원의 북마크 URL 리스트와 스토어 URL 리스트 조회 + val bookmarkUrls = nonMemberBookmarkUrlRepository.findAllByNonMemberId(request.nonMemberId).map { it.bookmarkUrl } + val storeUrls = nonMemberStoreUrlRepository.findAllByNonMemberId(request.nonMemberId).map { it.storeUrl } + + // 로그인 응답 데이터 반환 + return LoginNonMember.Response( + nonMemberId = nonMember.id, + name = nonMember.name, + bookmarkUrls = bookmarkUrls, + storeUrls = storeUrls + ) } } diff --git a/Ping-Application/src/main/kotlin/com/ping/application/nonmember/dto/LoginNonMember.kt b/Ping-Application/src/main/kotlin/com/ping/application/nonmember/dto/LoginNonMember.kt index 0baea8e..cd493af 100644 --- a/Ping-Application/src/main/kotlin/com/ping/application/nonmember/dto/LoginNonMember.kt +++ b/Ping-Application/src/main/kotlin/com/ping/application/nonmember/dto/LoginNonMember.kt @@ -5,4 +5,11 @@ class LoginNonMember { val nonMemberId: Long, val password: String ) + data class Response( + val nonMemberId: Long, + val name: String, + val bookmarkUrls: List, + val storeUrls: List + ) + } \ No newline at end of file diff --git a/Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/repository/NonMemberBookmarkUrlRepository.kt b/Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/repository/NonMemberBookmarkUrlRepository.kt index 666d410..44d3871 100644 --- a/Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/repository/NonMemberBookmarkUrlRepository.kt +++ b/Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/repository/NonMemberBookmarkUrlRepository.kt @@ -4,4 +4,5 @@ import com.ping.domain.nonmember.aggregate.NonMemberBookmarkUrlDomain interface NonMemberBookmarkUrlRepository { fun saveAll(nonMemberBookmarkUrlDomains: List) : List + fun findAllByNonMemberId(nonMemberId: Long): List } \ No newline at end of file diff --git a/Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/repository/NonMemberStoreUrlRepository.kt b/Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/repository/NonMemberStoreUrlRepository.kt index a52ac97..189bb5b 100644 --- a/Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/repository/NonMemberStoreUrlRepository.kt +++ b/Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/repository/NonMemberStoreUrlRepository.kt @@ -4,4 +4,5 @@ import com.ping.domain.nonmember.aggregate.NonMemberStoreUrlDomain interface NonMemberStoreUrlRepository { fun saveAll(nonMemberStoreUrlDomains: List) : List + fun findAllByNonMemberId(nonMemberId: Long): List } \ No newline at end of file diff --git a/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/jpa/repository/NonMemberBookmarkUrlJpaRepository.kt b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/jpa/repository/NonMemberBookmarkUrlJpaRepository.kt index a795c51..b704d51 100644 --- a/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/jpa/repository/NonMemberBookmarkUrlJpaRepository.kt +++ b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/jpa/repository/NonMemberBookmarkUrlJpaRepository.kt @@ -4,4 +4,5 @@ import com.ping.infra.nonmember.domain.jpa.entity.NonMemberBookmarkUrlEntity import org.springframework.data.jpa.repository.JpaRepository interface NonMemberBookmarkUrlJpaRepository: JpaRepository{ + fun findAllByNonMemberId(nonMemberId: Long): List } \ No newline at end of file diff --git a/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/jpa/repository/NonMemberStoreUrlJpaRepository.kt b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/jpa/repository/NonMemberStoreUrlJpaRepository.kt index d7119a2..7142ea1 100644 --- a/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/jpa/repository/NonMemberStoreUrlJpaRepository.kt +++ b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/jpa/repository/NonMemberStoreUrlJpaRepository.kt @@ -4,4 +4,5 @@ import com.ping.infra.nonmember.domain.jpa.entity.NonMemberStoreUrlEntity import org.springframework.data.jpa.repository.JpaRepository interface NonMemberStoreUrlJpaRepository : JpaRepository { + fun findAllByNonMemberId(nonMemberId: Long): List } \ No newline at end of file diff --git a/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/repositoryImpl/NonMemberBookmarkUrlRepositoryImpl.kt b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/repositoryImpl/NonMemberBookmarkUrlRepositoryImpl.kt index 132ebf7..316def9 100644 --- a/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/repositoryImpl/NonMemberBookmarkUrlRepositoryImpl.kt +++ b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/repositoryImpl/NonMemberBookmarkUrlRepositoryImpl.kt @@ -15,4 +15,9 @@ class NonMemberBookmarkUrlRepositoryImpl( NonMemberBookmarkUrlMapper.toEntity(it) }).map { NonMemberBookmarkUrlMapper.toDomain(it) } } + + override fun findAllByNonMemberId(nonMemberId: Long): List { + return nonMemberBookmarkUrlJpaRepository.findAllByNonMemberId(nonMemberId) + .map { NonMemberBookmarkUrlMapper.toDomain(it) } + } } \ No newline at end of file diff --git a/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/repositoryImpl/NonMemberStoreUrlRepositoryImpl.kt b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/repositoryImpl/NonMemberStoreUrlRepositoryImpl.kt index d8b730d..402ec85 100644 --- a/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/repositoryImpl/NonMemberStoreUrlRepositoryImpl.kt +++ b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/repositoryImpl/NonMemberStoreUrlRepositoryImpl.kt @@ -14,4 +14,9 @@ class NonMemberStoreUrlRepositoryImpl( return nonMemberStoreUrlJpaRepository.saveAll(nonMemberStoreUrlDomains.map { NonMemberStoreUrlMapper.toEntity(it) }) .map { NonMemberStoreUrlMapper.toDomain(it) } } + + override fun findAllByNonMemberId(nonMemberId: Long): List { + return nonMemberStoreUrlJpaRepository.findAllByNonMemberId(nonMemberId) + .map { NonMemberStoreUrlMapper.toDomain(it) } + } } \ No newline at end of file From b91c4b68d049a0c4a2c358dd151e0b8a4e5b1c5d Mon Sep 17 00:00:00 2001 From: codrin2 Date: Thu, 31 Oct 2024 06:09:07 +0900 Subject: [PATCH 066/203] =?UTF-8?q?feat:=20=ED=95=91=20=EC=97=85=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=8A=B8=20=EC=8B=9C=20=EB=B9=84=ED=9A=8C=EC=9B=90?= =?UTF-8?q?=EC=9D=98=20=EB=B6=81=EB=A7=88=ED=81=AC=EC=99=80=20=EC=8A=A4?= =?UTF-8?q?=ED=86=A0=EC=96=B4=20url=20=EB=B3=80=EA=B2=BD=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/nonmember/NonMemberService.kt | 33 +++++++++++++++++++ .../NonMemberBookmarkUrlRepository.kt | 1 + .../repository/NonMemberStoreUrlRepository.kt | 1 + .../NonMemberBookmarkUrlRepositoryImpl.kt | 4 +++ .../NonMemberStoreUrlRepositoryImpl.kt | 4 +++ 5 files changed, 43 insertions(+) diff --git a/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberService.kt b/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberService.kt index 3b13767..148adda 100644 --- a/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberService.kt +++ b/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberService.kt @@ -169,6 +169,33 @@ class NonMemberService( if (existingSids != allNewSids) { updatePlaceSids(nonMemberDomain, allNewSids) } + + // 기존 북마크 URL 및 상점 URL 조회 + val existingBookmarkUrls = nonMemberBookmarkUrlRepository.findAllByNonMemberId(nonMemberDomain.id) + val existingStoreUrls = nonMemberStoreUrlRepository.findAllByNonMemberId(nonMemberDomain.id) + + // URL과 ID를 쉽게 매핑하기 위해 Map 형태로 변환 + val existingBookmarkMap = existingBookmarkUrls.associateBy { it.bookmarkUrl } + val existingStoreMap = existingStoreUrls.associateBy { it.storeUrl } + + // 새 URL과 기존 URL을 비교하여 추가 및 삭제할 URL 식별 + val bookmarkUrlsToAdd = request.bookmarkUrls.filterNot { it in existingBookmarkMap.keys } + val bookmarkUrlsToDeleteIds = existingBookmarkUrls.filter { it.bookmarkUrl !in request.bookmarkUrls }.map { it.id } + + val storeUrlsToAdd = request.storeUrls.filterNot { it in existingStoreMap.keys } + val storeUrlsToDeleteIds = existingStoreUrls.filter { it.storeUrl !in request.storeUrls }.map { it.id } + + // 삭제할 URL을 ID 기반으로 삭제 + nonMemberBookmarkUrlRepository.deleteAllByIds(bookmarkUrlsToDeleteIds) + nonMemberStoreUrlRepository.deleteAllByIds(storeUrlsToDeleteIds) + + // 새 URL 추가 저장 + nonMemberBookmarkUrlRepository.saveAll(bookmarkUrlsToAdd.map { url -> + NonMemberBookmarkUrlDomain.of(nonMemberDomain, listOf(url)).first() + }) + nonMemberStoreUrlRepository.saveAll(storeUrlsToAdd.map { url -> + NonMemberStoreUrlDomain.of(nonMemberDomain, listOf(url)).first() + }) } private fun createNonMemberUpdateStatus(newNonMember: NonMemberDomain, shareUrlId: Long) { @@ -285,6 +312,12 @@ class NonMemberService( nonMemberPlaceRepository.deleteAll(placesToDelete) } + fun findUrlsToUpdate(existingUrls: List, newUrls: List): Pair, List> { + val urlsToAdd = newUrls.filterNot { it in existingUrls } + val urlsToDelete = existingUrls.filterNot { it in newUrls } + return Pair(urlsToAdd, urlsToDelete) + } + private fun isBookmarkExists(sid: String): Boolean { return bookmarkRepository.findAllBySidIn(listOf(sid)).isNotEmpty() } diff --git a/Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/repository/NonMemberBookmarkUrlRepository.kt b/Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/repository/NonMemberBookmarkUrlRepository.kt index 44d3871..b0a5e35 100644 --- a/Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/repository/NonMemberBookmarkUrlRepository.kt +++ b/Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/repository/NonMemberBookmarkUrlRepository.kt @@ -5,4 +5,5 @@ import com.ping.domain.nonmember.aggregate.NonMemberBookmarkUrlDomain interface NonMemberBookmarkUrlRepository { fun saveAll(nonMemberBookmarkUrlDomains: List) : List fun findAllByNonMemberId(nonMemberId: Long): List + fun deleteAllByIds(ids: List) } \ No newline at end of file diff --git a/Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/repository/NonMemberStoreUrlRepository.kt b/Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/repository/NonMemberStoreUrlRepository.kt index 189bb5b..c95859d 100644 --- a/Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/repository/NonMemberStoreUrlRepository.kt +++ b/Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/repository/NonMemberStoreUrlRepository.kt @@ -5,4 +5,5 @@ import com.ping.domain.nonmember.aggregate.NonMemberStoreUrlDomain interface NonMemberStoreUrlRepository { fun saveAll(nonMemberStoreUrlDomains: List) : List fun findAllByNonMemberId(nonMemberId: Long): List + fun deleteAllByIds(ids: List) } \ No newline at end of file diff --git a/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/repositoryImpl/NonMemberBookmarkUrlRepositoryImpl.kt b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/repositoryImpl/NonMemberBookmarkUrlRepositoryImpl.kt index 316def9..56de890 100644 --- a/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/repositoryImpl/NonMemberBookmarkUrlRepositoryImpl.kt +++ b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/repositoryImpl/NonMemberBookmarkUrlRepositoryImpl.kt @@ -20,4 +20,8 @@ class NonMemberBookmarkUrlRepositoryImpl( return nonMemberBookmarkUrlJpaRepository.findAllByNonMemberId(nonMemberId) .map { NonMemberBookmarkUrlMapper.toDomain(it) } } + + override fun deleteAllByIds(ids: List) { + nonMemberBookmarkUrlJpaRepository.deleteAllById(ids) + } } \ No newline at end of file diff --git a/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/repositoryImpl/NonMemberStoreUrlRepositoryImpl.kt b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/repositoryImpl/NonMemberStoreUrlRepositoryImpl.kt index 402ec85..92c0286 100644 --- a/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/repositoryImpl/NonMemberStoreUrlRepositoryImpl.kt +++ b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/repositoryImpl/NonMemberStoreUrlRepositoryImpl.kt @@ -19,4 +19,8 @@ class NonMemberStoreUrlRepositoryImpl( return nonMemberStoreUrlJpaRepository.findAllByNonMemberId(nonMemberId) .map { NonMemberStoreUrlMapper.toDomain(it) } } + + override fun deleteAllByIds(ids: List) { + nonMemberStoreUrlJpaRepository.deleteAllById(ids) + } } \ No newline at end of file From 2966ffb01720b0f613854d66702bb91983abaaba Mon Sep 17 00:00:00 2001 From: codrin2 Date: Thu, 31 Oct 2024 12:16:38 +0900 Subject: [PATCH 067/203] =?UTF-8?q?feat:=20health=20=EC=B2=B4=ED=81=AC?= =?UTF-8?q?=EB=A5=BC=20=EC=9C=84=ED=95=9C=20actuator=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Ping-Infra/build.gradle.kts | 2 +- build.gradle.kts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Ping-Infra/build.gradle.kts b/Ping-Infra/build.gradle.kts index eb9530a..a3504ba 100644 --- a/Ping-Infra/build.gradle.kts +++ b/Ping-Infra/build.gradle.kts @@ -11,7 +11,7 @@ dependencies { implementation("org.springframework.boot:spring-boot-starter-data-mongodb") // Redis - implementation("org.springframework.boot:spring-boot-starter-data-redis") +// implementation("org.springframework.boot:spring-boot-starter-data-redis") // QueryDSL // 필요 없으면 삭제 diff --git a/build.gradle.kts b/build.gradle.kts index dafc4d7..539977f 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -104,6 +104,7 @@ subprojects { //spring implementation("org.springframework.boot:spring-boot-starter-web") implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.6.0") + implementation("org.springframework.boot:spring-boot-starter-actuator") //test testImplementation("org.springframework.boot:spring-boot-starter-test") From 1226f713f3122340f57ea61c88ad3280d97649d8 Mon Sep 17 00:00:00 2001 From: codrin2 Date: Thu, 31 Oct 2024 12:23:52 +0900 Subject: [PATCH 068/203] =?UTF-8?q?test:=20=EB=B9=84=ED=9A=8C=EC=9B=90=20?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EB=A1=9C=EC=A7=81=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=EC=97=90=20=EB=94=B0=EB=A5=B8=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/nonmember/NonMemberControllerTest.kt | 41 ++++++++++++------- 1 file changed, 27 insertions(+), 14 deletions(-) diff --git a/Ping-Api/src/test/kotlin/com/ping/api/nonmember/NonMemberControllerTest.kt b/Ping-Api/src/test/kotlin/com/ping/api/nonmember/NonMemberControllerTest.kt index c72dc60..1e41523 100644 --- a/Ping-Api/src/test/kotlin/com/ping/api/nonmember/NonMemberControllerTest.kt +++ b/Ping-Api/src/test/kotlin/com/ping/api/nonmember/NonMemberControllerTest.kt @@ -12,6 +12,7 @@ import com.ping.infra.nonmember.domain.mongo.repository.BookmarkMongoRepository import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test import org.mockito.BDDMockito.given +import org.mockito.Mockito import org.mockito.Mockito.doNothing import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest import org.springframework.boot.test.mock.mockito.MockBean @@ -41,15 +42,22 @@ class NonMemberControllerTest : BaseRestDocsTest() { fun loginNonMember() { // given val request = LoginNonMember.Request(nonMemberId = 1L, password = "1234") + val response = LoginNonMember.Response( + nonMemberId = 1L, + name = "테스트 사용자", + bookmarkUrls = listOf("bookmarkUrl1", "bookmarkUrl2"), + storeUrls = listOf("storeUrl1") + ) - doNothing().`when`(nonMemberLoginService).login(request) + // 로그인 서비스의 반환 값 설정 + Mockito.`when`(nonMemberLoginService.login(request)).thenReturn(response) val jsonRequest = """ - { - "nonMemberId": 1, - "password": "1234" - } - """ + { + "nonMemberId": 1, + "password": "1234" + } + """ // when val result: ResultActions = mockMvc.perform( @@ -61,8 +69,11 @@ class NonMemberControllerTest : BaseRestDocsTest() { // then result.andExpect(status().isOk) -// .andExpect(jsonPath("$.code").value(200)) -// .andExpect(jsonPath("$.message").value("비회원 로그인 성공")) + .andExpect(jsonPath("$.nonMemberId").value(response.nonMemberId)) + .andExpect(jsonPath("$.name").value(response.name)) + .andExpect(jsonPath("$.bookmarkUrls[0]").value(response.bookmarkUrls[0])) + .andExpect(jsonPath("$.bookmarkUrls[1]").value(response.bookmarkUrls[1])) + .andExpect(jsonPath("$.storeUrls[0]").value(response.storeUrls[0])) .andDo( MockMvcRestDocumentationWrapper.document( "nonmember/loginNonMember", @@ -76,18 +87,20 @@ class NonMemberControllerTest : BaseRestDocsTest() { fieldWithPath("nonMemberId").description("비회원 ID"), fieldWithPath("password").description("비회원 비밀번호") ) -// .responseFields( -// fieldWithPath("code").description("응답 코드"), -// fieldWithPath("message").description("응답 메시지"), -// fieldWithPath("data").description("응답 데이터") -// ) - .responseSchema(Schema.schema("CommonResponse")) + .responseFields( + fieldWithPath("nonMemberId").description("로그인한 비회원의 ID"), + fieldWithPath("name").description("비회원의 이름"), + fieldWithPath("bookmarkUrls").description("비회원의 북마크 URL 목록"), + fieldWithPath("storeUrls").description("비회원의 스토어 URL 목록") + ) + .responseSchema(Schema.schema("LoginNonMemberResponse")) .build() ) ) ) } + @Test @DisplayName("비회원 핑 생성") fun createNonMemberPings() { From f6258f875d1774b70966a47c27717673c6cae8fd Mon Sep 17 00:00:00 2001 From: codrin2 Date: Thu, 31 Oct 2024 14:48:30 +0900 Subject: [PATCH 069/203] =?UTF-8?q?feat(NaverApiClient):=20=EB=84=A4?= =?UTF-8?q?=EC=9D=B4=EB=B2=84=20reverse=20geocode=20API=20=EC=97=B0?= =?UTF-8?q?=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ping/client/naver/place/NaverApiClient.kt | 19 +++++++++++++++++++ .../client/naver/place/NaverApiResponse.kt | 17 +++++++++++++++-- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/Ping-Client/src/main/kotlin/com/ping/client/naver/place/NaverApiClient.kt b/Ping-Client/src/main/kotlin/com/ping/client/naver/place/NaverApiClient.kt index 9c39ae4..99ac03d 100644 --- a/Ping-Client/src/main/kotlin/com/ping/client/naver/place/NaverApiClient.kt +++ b/Ping-Client/src/main/kotlin/com/ping/client/naver/place/NaverApiClient.kt @@ -44,4 +44,23 @@ class NaverApiClient( Pair(null, null) } } + + fun getReverseGeocode(px: Double, py: Double): String? { + val client = WebClient.create("https://naveropenapi.apigw.ntruss.com/map-reversegeocode/v2/gc") + val response = client.get() + .uri { uriBuilder -> + uriBuilder.queryParam("coords", "$px,$py") + .queryParam("output", "json") + .queryParam("orders", "roadaddr") + .build() + } + .header("X-NCP-APIGW-API-KEY-ID", cloudId) + .header("X-NCP-APIGW-API-KEY", cloudSecret) + .retrieve() + .bodyToMono(NaverApiResponse.ReverseGeocodeResponse::class.java) + .block() + + // 응답에서 도로명 주소 추출 + return response?.results?.firstOrNull()?.land?.name + } } diff --git a/Ping-Client/src/main/kotlin/com/ping/client/naver/place/NaverApiResponse.kt b/Ping-Client/src/main/kotlin/com/ping/client/naver/place/NaverApiResponse.kt index f4b5b38..0e6bb65 100644 --- a/Ping-Client/src/main/kotlin/com/ping/client/naver/place/NaverApiResponse.kt +++ b/Ping-Client/src/main/kotlin/com/ping/client/naver/place/NaverApiResponse.kt @@ -15,11 +15,24 @@ class NaverApiResponse { data class Address( val roadAddress: String, val jibunAddress: String, - val x: Double, // 경도 - val y: Double // 위도 + val x: Double, + val y: Double ) data class NaverGeocodeResponse( val addresses: List
) + + data class ReverseGeocodeResponse( + val results: List + ) + + data class Result( + val name: String, + val land: Land + ) + + data class Land( + val name: String // 도로명 주소 + ) } \ No newline at end of file From c6cdf27a108ff9d6ec70186a9f83e9be916adc8a Mon Sep 17 00:00:00 2001 From: codrin2 Date: Thu, 31 Oct 2024 14:50:34 +0900 Subject: [PATCH 070/203] =?UTF-8?q?feat(PlaceService):=20getReverseGeocode?= =?UTF-8?q?=20=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/kotlin/com/ping/application/place/PlaceService.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Ping-Application/src/main/kotlin/com/ping/application/place/PlaceService.kt b/Ping-Application/src/main/kotlin/com/ping/application/place/PlaceService.kt index 5b8bc0f..596de1f 100644 --- a/Ping-Application/src/main/kotlin/com/ping/application/place/PlaceService.kt +++ b/Ping-Application/src/main/kotlin/com/ping/application/place/PlaceService.kt @@ -30,4 +30,8 @@ class PlaceService( ) } + fun getReverseGeocode(px: Double, py: Double): String? { + return naverApiClient.getReverseGeocode(px, py) + } + } \ No newline at end of file From 0ce41d5b36ada67f905c3416373eb515f2d97781 Mon Sep 17 00:00:00 2001 From: codrin2 Date: Thu, 31 Oct 2024 14:55:58 +0900 Subject: [PATCH 071/203] =?UTF-8?q?feat(PlaceController):=20Reverse=20Geoc?= =?UTF-8?q?ode=20API=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/kotlin/com/ping/api/place/PlaceApi.kt | 1 + .../main/kotlin/com/ping/api/place/PlaceController.kt | 11 ++++++++--- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/Ping-Api/src/main/kotlin/com/ping/api/place/PlaceApi.kt b/Ping-Api/src/main/kotlin/com/ping/api/place/PlaceApi.kt index 89658ba..02f688c 100644 --- a/Ping-Api/src/main/kotlin/com/ping/api/place/PlaceApi.kt +++ b/Ping-Api/src/main/kotlin/com/ping/api/place/PlaceApi.kt @@ -4,4 +4,5 @@ object PlaceApi { const val BASE_URL = "/places" const val SEARCH = "$BASE_URL/search" const val GEOCODE = "$BASE_URL/geocode" + const val REVERSE_GEOCODE = "$BASE_URL/reverse-geocode" } diff --git a/Ping-Api/src/main/kotlin/com/ping/api/place/PlaceController.kt b/Ping-Api/src/main/kotlin/com/ping/api/place/PlaceController.kt index 1495c45..7a5ad19 100644 --- a/Ping-Api/src/main/kotlin/com/ping/api/place/PlaceController.kt +++ b/Ping-Api/src/main/kotlin/com/ping/api/place/PlaceController.kt @@ -17,13 +17,18 @@ class PlaceController( @GetMapping(PlaceApi.SEARCH) fun searchPlace(@RequestParam("keyword") keyword: String): ResponseEntity>> { val response = placeService.searchPlace(keyword); - return ResponseEntity.ok(SuccessResponse.of(HttpStatus.OK, "장소 검색에 성공하였습니다.", response)) + return ResponseEntity.ok(SuccessResponse.of(HttpStatus.OK, "장소 검색 성공", response)) } @GetMapping(PlaceApi.GEOCODE) - fun geocodePlace(@RequestParam("address") address: String): ResponseEntity> { + fun getGeocodePlace(@RequestParam("address") address: String): ResponseEntity> { val response = placeService.getGeocodeAddress(address) - return ResponseEntity.ok(SuccessResponse.of(HttpStatus.OK, "좌표 검색에 성공하였습니다.", response)) + return ResponseEntity.ok(SuccessResponse.of(HttpStatus.OK, "좌표 검색 성공", response)) } + @GetMapping(PlaceApi.REVERSE_GEOCODE) + fun getReverseGeocode(@RequestParam("px") px: Double, @RequestParam("py") py: Double): ResponseEntity> { + val response = placeService.getReverseGeocode(px, py) + return ResponseEntity.ok(SuccessResponse.of(HttpStatus.OK, "도로명 주소 조회 성공", response)) + } } \ No newline at end of file From 3a868695dbceaba6c1cd3d06ec0d251fab11967a Mon Sep 17 00:00:00 2001 From: codrin2 Date: Thu, 31 Oct 2024 15:17:11 +0900 Subject: [PATCH 072/203] =?UTF-8?q?feat(NaverApiClient):=20=EB=8F=84?= =?UTF-8?q?=EB=A1=9C=EB=AA=85=20=EC=A3=BC=EC=86=8C=20=EB=B0=98=ED=99=98?= =?UTF-8?q?=EA=B0=92=EC=97=90=20=ED=86=A0=EC=A7=80=20=EB=B3=B8=EB=B2=88?= =?UTF-8?q?=ED=98=B8=EC=99=80=20=EC=83=81=EC=84=B8=20=EC=A3=BC=EC=86=8C=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/com/ping/client/naver/place/NaverApiClient.kt | 9 +++++++-- .../com/ping/client/naver/place/NaverApiResponse.kt | 4 +++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/Ping-Client/src/main/kotlin/com/ping/client/naver/place/NaverApiClient.kt b/Ping-Client/src/main/kotlin/com/ping/client/naver/place/NaverApiClient.kt index 99ac03d..3d75b94 100644 --- a/Ping-Client/src/main/kotlin/com/ping/client/naver/place/NaverApiClient.kt +++ b/Ping-Client/src/main/kotlin/com/ping/client/naver/place/NaverApiClient.kt @@ -60,7 +60,12 @@ class NaverApiClient( .bodyToMono(NaverApiResponse.ReverseGeocodeResponse::class.java) .block() - // 응답에서 도로명 주소 추출 - return response?.results?.firstOrNull()?.land?.name + // 도로명 주소의 모든 구성 요소를 조합하여 반환 + val land = response?.results?.firstOrNull()?.land + return if (land != null) { + "${land.name} ${land.number1}${if (land.number2?.isNotEmpty() == true) "-${land.number2}" else ""}" + } else { + null + } } } diff --git a/Ping-Client/src/main/kotlin/com/ping/client/naver/place/NaverApiResponse.kt b/Ping-Client/src/main/kotlin/com/ping/client/naver/place/NaverApiResponse.kt index 0e6bb65..d25d220 100644 --- a/Ping-Client/src/main/kotlin/com/ping/client/naver/place/NaverApiResponse.kt +++ b/Ping-Client/src/main/kotlin/com/ping/client/naver/place/NaverApiResponse.kt @@ -33,6 +33,8 @@ class NaverApiResponse { ) data class Land( - val name: String // 도로명 주소 + val name: String, // 도로명 + val number1: String, // 본번 + val number2: String? = "" // 부번 (없을 수도 있음) ) } \ No newline at end of file From d1c5b9168a506ad47c54b80774341015d419fe21 Mon Sep 17 00:00:00 2001 From: codrin2 Date: Thu, 31 Oct 2024 15:27:04 +0900 Subject: [PATCH 073/203] =?UTF-8?q?test:=20getReverseGeocode=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/ping/api/place/PlaceControllerTest.kt | 47 ++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/Ping-Api/src/test/kotlin/com/ping/api/place/PlaceControllerTest.kt b/Ping-Api/src/test/kotlin/com/ping/api/place/PlaceControllerTest.kt index b78d524..74e4203 100644 --- a/Ping-Api/src/test/kotlin/com/ping/api/place/PlaceControllerTest.kt +++ b/Ping-Api/src/test/kotlin/com/ping/api/place/PlaceControllerTest.kt @@ -9,6 +9,7 @@ import com.ping.api.global.BaseRestDocsTest import com.ping.application.place.PlaceService import com.ping.application.place.dto.GeocodePlace import com.ping.application.place.dto.SearchPlace +import com.ping.client.naver.place.NaverApiResponse import com.ping.infra.nonmember.domain.mongo.repository.BookmarkMongoRepository import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test @@ -80,7 +81,7 @@ class PlaceControllerTest : BaseRestDocsTest() { @Test @DisplayName("주소로 좌표 검색") - fun geocodePlace() { + fun getGeocodePlace() { // given val address = "서울시 중구 명동" val geocodeResponse = GeocodePlace.Response("서울시 중구 명동", 126.9784, 37.5665) @@ -120,4 +121,48 @@ class PlaceControllerTest : BaseRestDocsTest() { ) ) } + + @Test + @DisplayName("도로명 주소 조회") + fun getReverseGeocode() { + // given + val px = 127.12345 + val py = 37.12345 + val response = "성남대로 123-45" + given(placeService.getReverseGeocode(px, py)).willReturn(response) + + // when + val result: ResultActions = mockMvc.perform( + RestDocumentationRequestBuilders.get(PlaceApi.REVERSE_GEOCODE) + .param("px", px.toString()) + .param("py", py.toString()) + .contentType(MediaType.APPLICATION_JSON) + ) + + // then + result.andExpect(status().isOk) + .andDo( + MockMvcRestDocumentationWrapper.document( + "place/reverseGeocode", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + resource( + ResourceSnippetParameters.builder() + .tag("장소") + .description("경도와 위도를 기반으로 도로명 주소 조회") + .queryParameters( + parameterWithName("px").description("경도"), + parameterWithName("py").description("위도") + ) + .responseFields( + fieldWithPath("code").description("응답 코드"), + fieldWithPath("message").description("응답 메시지"), + fieldWithPath("data").description("도로명 주소") + ) + .responseSchema(Schema.schema("ReverseGeocodeResponse")) + .build() + ) + ) + ) + } } From 7ded432a031d85b5ee0cdad7f53c1380e86df4a4 Mon Sep 17 00:00:00 2001 From: codrin2 Date: Thu, 31 Oct 2024 16:03:23 +0900 Subject: [PATCH 074/203] =?UTF-8?q?feat:=20=EB=8F=84=EB=A1=9C=EB=AA=85=20?= =?UTF-8?q?=EC=A3=BC=EC=86=8C=20=EB=B0=98=ED=99=98=20API=20Restdocs=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Ping-Api/src/docs/asciidoc/Place.adoc | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Ping-Api/src/docs/asciidoc/Place.adoc b/Ping-Api/src/docs/asciidoc/Place.adoc index cfcec45..8245c23 100644 --- a/Ping-Api/src/docs/asciidoc/Place.adoc +++ b/Ping-Api/src/docs/asciidoc/Place.adoc @@ -7,4 +7,8 @@ operation::PlaceControllerTest/searchPlace[snippets='http-request,http-response, [[Geocode-Place]] === 주소로 좌표 검색 -operation::PlaceControllerTest/geocodePlace[snippets='http-request,http-response,request-parameters,response-fields'] \ No newline at end of file +operation::PlaceControllerTest/getGeocodePlace[snippets='http-request,http-response,request-parameters,response-fields'] + +[[Reverse-Geocode-Place]] +=== 도로명 주소 조회 +operation::PlaceControllerTest/getReverseGeocode[snippets='http-request,http-response,request-parameters,response-fields'] \ No newline at end of file From 527ca6f2fca604d1922cb8c48df6be2192580b67 Mon Sep 17 00:00:00 2001 From: codrin2 Date: Thu, 31 Oct 2024 18:09:51 +0900 Subject: [PATCH 075/203] =?UTF-8?q?fix(PlaceController):=20=EB=8F=84?= =?UTF-8?q?=EB=A1=9C=EB=AA=85=20=EC=A3=BC=EC=86=8C=20API=20=EC=9A=94?= =?UTF-8?q?=EC=B2=AD=EA=B0=92=20=EB=B0=98=EC=98=AC=EB=A6=BC=20=EC=B2=98?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/kotlin/com/ping/application/place/PlaceService.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Ping-Application/src/main/kotlin/com/ping/application/place/PlaceService.kt b/Ping-Application/src/main/kotlin/com/ping/application/place/PlaceService.kt index 596de1f..90a8b0a 100644 --- a/Ping-Application/src/main/kotlin/com/ping/application/place/PlaceService.kt +++ b/Ping-Application/src/main/kotlin/com/ping/application/place/PlaceService.kt @@ -31,7 +31,8 @@ class PlaceService( } fun getReverseGeocode(px: Double, py: Double): String? { - return naverApiClient.getReverseGeocode(px, py) + val roundedPx = String.format("%.4f", px).toDouble() + val roundedPy = String.format("%.4f", py).toDouble() + return naverApiClient.getReverseGeocode(roundedPx, roundedPy) } - } \ No newline at end of file From 9d54f1777e7404ccf7f342df80e9ce900e2d9cfc Mon Sep 17 00:00:00 2001 From: codrin2 Date: Thu, 31 Oct 2024 20:33:36 +0900 Subject: [PATCH 076/203] =?UTF-8?q?feat(NonMemberService):=20=EC=A0=84?= =?UTF-8?q?=EC=B2=B4=20=EB=B9=84=ED=9A=8C=EC=9B=90=20=EB=A9=A4=EB=B2=84=20?= =?UTF-8?q?=EB=A6=AC=ED=94=84=EB=A0=88=EC=89=AC=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/nonmember/NonMemberService.kt | 70 +++++++++++++++++-- 1 file changed, 66 insertions(+), 4 deletions(-) diff --git a/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberService.kt b/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberService.kt index 148adda..2cf209b 100644 --- a/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberService.kt +++ b/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberService.kt @@ -198,6 +198,59 @@ class NonMemberService( }) } + fun refreshAllNonMemberPings(uuid: String): GetAllNonMemberPings.Response { + val shareUrl = shareUrlRepository.findByUuid(uuid) + ?: throw CustomException(ExceptionContent.INVALID_SHARE_URL) + + val nonMemberList = nonMemberRepository.findAllByShareUrl(shareUrl.id) + + nonMemberList.forEach { nonMember -> + val updatedBookmarkUrls = nonMemberBookmarkUrlRepository.findAllByNonMemberId(nonMember.id) + val updatedStoreUrls = nonMemberStoreUrlRepository.findAllByNonMemberId(nonMember.id) + + // 새로운 북마크와 스토어 URL에 대한 업데이트 처리 + handleBookmarkUrls(updatedBookmarkUrls.map { it.bookmarkUrl }, nonMember) + handleStoreUrls(updatedStoreUrls.map { it.storeUrl }, nonMember) + } + + val nonMembers = nonMemberList.map { nonMember -> + GetAllNonMemberPings.NonMember( + nonMemberId = nonMember.id, + name = nonMember.name + ) + } + + // 핑 데이터 생성 및 아이콘 레벨 할당 + val nonMemberPlaces = nonMembersToNonMemberPlacesMap(nonMemberList) + val pings = nonMemberPlaces.entries.mapIndexed { index, nonMemberPlace -> + val level = calculateIconLevel(index, nonMemberPlace.key) + + nonMemberPlace.value.map { bookmarkPair -> + GetAllNonMemberPings.Ping( + iconLevel = level, + nonMembers = bookmarkPair.second.map { + GetAllNonMemberPings.NonMember( + nonMemberId = it.id, + name = it.name + ) + }, + url = bookmarkPair.first.url, + placeName = bookmarkPair.first.name, + px = bookmarkPair.first.px, + py = bookmarkPair.first.py, + ) + } + }.flatten() + + return GetAllNonMemberPings.Response( + eventName = shareUrl.eventName, + nonMembers = nonMembers, + px = shareUrl.latitude, + py = shareUrl.longtitude, + pings = pings + ) + } + private fun createNonMemberUpdateStatus(newNonMember: NonMemberDomain, shareUrlId: Long) { // 새로 생성된 비회원을 제외한 기존 비회원 목록 조회 val existingNonMembers = nonMemberRepository.findAllByShareUrl(shareUrlId) @@ -312,10 +365,19 @@ class NonMemberService( nonMemberPlaceRepository.deleteAll(placesToDelete) } - fun findUrlsToUpdate(existingUrls: List, newUrls: List): Pair, List> { - val urlsToAdd = newUrls.filterNot { it in existingUrls } - val urlsToDelete = existingUrls.filterNot { it in newUrls } - return Pair(urlsToAdd, urlsToDelete) + private fun calculateIconLevel(index: Int, overlapCount: Int): Int { + val mostOverlappedIconLevel = 4 + val secondOverlappedIconLevel = 3 + val thirdOverlappedIconLevel = 2 + val remainderIconLevel = 1 + + return when { + overlapCount == 1 -> remainderIconLevel + index == 0 -> mostOverlappedIconLevel + index == 1 -> secondOverlappedIconLevel + index == 2 -> thirdOverlappedIconLevel + else -> remainderIconLevel + } } private fun isBookmarkExists(sid: String): Boolean { From 4b201ccce16faea87c200c3235d22b728beb200f Mon Sep 17 00:00:00 2001 From: codrin2 Date: Thu, 31 Oct 2024 20:44:43 +0900 Subject: [PATCH 077/203] =?UTF-8?q?feat(NonMemberController):=20=EC=A0=84?= =?UTF-8?q?=EC=B2=B4=20=EB=B9=84=ED=9A=8C=EC=9B=90=20=EB=A9=A4=EB=B2=84=20?= =?UTF-8?q?=EB=A6=AC=ED=94=84=EB=A0=88=EC=89=AC=20API=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/kotlin/com/ping/api/nonmember/NonMemberApi.kt | 1 + .../kotlin/com/ping/api/nonmember/NonMemberController.kt | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/Ping-Api/src/main/kotlin/com/ping/api/nonmember/NonMemberApi.kt b/Ping-Api/src/main/kotlin/com/ping/api/nonmember/NonMemberApi.kt index da36b82..7575404 100644 --- a/Ping-Api/src/main/kotlin/com/ping/api/nonmember/NonMemberApi.kt +++ b/Ping-Api/src/main/kotlin/com/ping/api/nonmember/NonMemberApi.kt @@ -5,4 +5,5 @@ object NonMemberApi { const val LOGIN = "$BASE_URL/login" const val PING = "$BASE_URL/pings" const val PING_NONMEMBERID = "$PING/{nonMemberId}" + const val PING_REFRESH_ALL = "$PING/refresh-all" } \ No newline at end of file diff --git a/Ping-Api/src/main/kotlin/com/ping/api/nonmember/NonMemberController.kt b/Ping-Api/src/main/kotlin/com/ping/api/nonmember/NonMemberController.kt index 12ff8f4..c8d47c6 100644 --- a/Ping-Api/src/main/kotlin/com/ping/api/nonmember/NonMemberController.kt +++ b/Ping-Api/src/main/kotlin/com/ping/api/nonmember/NonMemberController.kt @@ -41,4 +41,9 @@ class NonMemberController( fun updateNonMemberPings(@RequestBody request: UpdateNonMemberPings.Request) { nonMemberService.updateNonMemberPings(request) } + + @GetMapping(NonMemberApi.PING_REFRESH_ALL) + fun refreshAllNonMemberPings(@RequestParam uuid: String): GetAllNonMemberPings.Response { + return nonMemberService.refreshAllNonMemberPings(uuid) + } } \ No newline at end of file From 882a8ea593bef15d624527088a6989fdc477dc77 Mon Sep 17 00:00:00 2001 From: codrin2 Date: Thu, 31 Oct 2024 20:51:06 +0900 Subject: [PATCH 078/203] =?UTF-8?q?test(NonMemberControllerTest):=20?= =?UTF-8?q?=EC=A0=84=EC=B2=B4=20=EB=B9=84=ED=9A=8C=EC=9B=90=20=EB=A9=A4?= =?UTF-8?q?=EB=B2=84=20=EB=A6=AC=ED=94=84=EB=A0=88=EC=89=AC=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/nonmember/NonMemberControllerTest.kt | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/Ping-Api/src/test/kotlin/com/ping/api/nonmember/NonMemberControllerTest.kt b/Ping-Api/src/test/kotlin/com/ping/api/nonmember/NonMemberControllerTest.kt index 1e41523..7eceda2 100644 --- a/Ping-Api/src/test/kotlin/com/ping/api/nonmember/NonMemberControllerTest.kt +++ b/Ping-Api/src/test/kotlin/com/ping/api/nonmember/NonMemberControllerTest.kt @@ -1,6 +1,7 @@ package com.ping.api.nonmember import com.epages.restdocs.apispec.MockMvcRestDocumentationWrapper +import com.epages.restdocs.apispec.ResourceDocumentation.parameterWithName import com.epages.restdocs.apispec.ResourceDocumentation.resource import com.epages.restdocs.apispec.ResourceSnippetParameters import com.epages.restdocs.apispec.Schema @@ -300,4 +301,76 @@ class NonMemberControllerTest : BaseRestDocsTest() { ) ) } + + @Test + @DisplayName("비회원 모든 핑 리프레쉬") + fun refreshAllNonMemberPings() { + // given + val uuid = "test-uuid" + val response = GetAllNonMemberPings.Response( + eventName = "Sample Event", + nonMembers = listOf( + GetAllNonMemberPings.NonMember(nonMemberId = 1, name = "핑핑이1"), + GetAllNonMemberPings.NonMember(nonMemberId = 2, name = "핑핑이2") + ), + px = 127.00001, + py = 37.00001, + pings = listOf( + GetAllNonMemberPings.Ping( + iconLevel = 2, + nonMembers = listOf( + GetAllNonMemberPings.NonMember(nonMemberId = 1, name = "핑핑이1"), + GetAllNonMemberPings.NonMember(nonMemberId = 2, name = "핑핑이2") + ), + url = "https://map.naver.com/p/entry/place/1946678040", + placeName = "호이", + px = 126.971178, + py = 37.5302481 + ) + ) + ) + + Mockito.`when`(nonMemberService.refreshAllNonMemberPings(uuid)).thenReturn(response) + + // when + val result: ResultActions = mockMvc.perform( + RestDocumentationRequestBuilders.get(NonMemberApi.PING_REFRESH_ALL) + .param("uuid", uuid) + .contentType(MediaType.APPLICATION_JSON) + ) + + // then + result.andExpect(status().isOk) + .andDo( + MockMvcRestDocumentationWrapper.document( + "nonmember/refreshAllNonMemberPings", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + resource( + ResourceSnippetParameters.builder() + .tag("NonMember") + .description("비회원 모든 핑 리프레쉬") + .queryParameters( + parameterWithName("uuid").description("이벤트 식별자 UUID") + ) + .responseFields( + fieldWithPath("eventName").description("이벤트 이름"), + fieldWithPath("px").description("이벤트 중심 경도"), + fieldWithPath("py").description("이벤트 중심 위도"), + fieldWithPath("nonMembers[].nonMemberId").description("비회원의 id"), + fieldWithPath("nonMembers[].name").description("비회원 이름"), + fieldWithPath("pings[].iconLevel").description("아이콘 레벨\n4:가장 많이 겹침\n3:그다음\n2:그다음\n1:나머지"), + fieldWithPath("pings[].nonMembers[].nonMemberId").description("비회원 id"), + fieldWithPath("pings[].nonMembers[].name").description("비회원 이름"), + fieldWithPath("pings[].url").description("장소 URL"), + fieldWithPath("pings[].placeName").description("장소 이름"), + fieldWithPath("pings[].px").description("경도"), + fieldWithPath("pings[].py").description("위도") + ) + .responseSchema(Schema.schema("GetAllNonMemberPingsResponse")) + .build() + ) + ) + ) + } } \ No newline at end of file From c663401ad5bf9117ecc5e5b52c795b8b66286c32 Mon Sep 17 00:00:00 2001 From: codrin2 Date: Thu, 31 Oct 2024 20:54:37 +0900 Subject: [PATCH 079/203] =?UTF-8?q?docs(NonMember.adoc):=20=EC=A0=84?= =?UTF-8?q?=EC=B2=B4=20=EB=B9=84=ED=9A=8C=EC=9B=90=20=EB=A9=A4=EB=B2=84=20?= =?UTF-8?q?=EB=A6=AC=ED=94=84=EB=A0=88=EC=89=AC=20API=20RestDocs=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Ping-Api/src/docs/asciidoc/NonMember.adoc | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Ping-Api/src/docs/asciidoc/NonMember.adoc b/Ping-Api/src/docs/asciidoc/NonMember.adoc index 4079a21..f3d690f 100644 --- a/Ping-Api/src/docs/asciidoc/NonMember.adoc +++ b/Ping-Api/src/docs/asciidoc/NonMember.adoc @@ -19,4 +19,8 @@ operation::NonMemberControllerTest/updateNonMemberPings[snippets='http-request,r [[Post-NonMemberLogin]] === 비회원 로그인 -operation::NonMemberControllerTest/loginNonMember[snippets='http-request,request-fields,http-response'] \ No newline at end of file +operation::NonMemberControllerTest/loginNonMember[snippets='http-request,request-fields,http-response'] + +[[Get-RefreshAllNonMemberPings]] +=== 비회원 모든 핑 갱신 +operation::NonMemberControllerTest/refreshAllNonMemberPings[snippets='http-request,http-response,query-parameters,response-fields'] \ No newline at end of file From cdf377b3e07398522d079d033e14dc425fa3e1ff Mon Sep 17 00:00:00 2001 From: codrin2 Date: Thu, 31 Oct 2024 21:20:21 +0900 Subject: [PATCH 080/203] =?UTF-8?q?fix(NonMemberService):=20=EB=B9=84?= =?UTF-8?q?=ED=9A=8C=EC=9B=90=20=EB=A9=A4=EB=B2=84=20=EB=A6=AC=ED=94=84?= =?UTF-8?q?=EB=A0=88=EC=89=AC=20=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/nonmember/NonMemberService.kt | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberService.kt b/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberService.kt index 2cf209b..a52a450 100644 --- a/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberService.kt +++ b/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberService.kt @@ -205,12 +205,20 @@ class NonMemberService( val nonMemberList = nonMemberRepository.findAllByShareUrl(shareUrl.id) nonMemberList.forEach { nonMember -> - val updatedBookmarkUrls = nonMemberBookmarkUrlRepository.findAllByNonMemberId(nonMember.id) - val updatedStoreUrls = nonMemberStoreUrlRepository.findAllByNonMemberId(nonMember.id) + // 기존 sid를 추출 + val existingSids = nonMemberPlaceRepository.findAllByNonMemberId(nonMember.id).map { it.sid }.toSet() - // 새로운 북마크와 스토어 URL에 대한 업데이트 처리 - handleBookmarkUrls(updatedBookmarkUrls.map { it.bookmarkUrl }, nonMember) - handleStoreUrls(updatedStoreUrls.map { it.storeUrl }, nonMember) + // 새로운 북마크 및 스토어 URL 처리 후 sid 추출 + val bookmarkData = handleBookmarkUrls(nonMemberBookmarkUrlRepository.findAllByNonMemberId(nonMember.id).map { it.bookmarkUrl }, nonMember) + val storeData = handleStoreUrls(nonMemberStoreUrlRepository.findAllByNonMemberId(nonMember.id).map { it.storeUrl }, nonMember) + + // 새로운 sid 집합 (북마크와 스토어 데이터의 sids 결합) + val allNewSids = (bookmarkData.sids + storeData.sids) + + // 기존 sid와 비교하여 변경 사항이 있으면 업데이트 + if (existingSids != allNewSids) { + updatePlaceSids(nonMember, allNewSids) + } } val nonMembers = nonMemberList.map { nonMember -> From 1ceea61b704b017ad52665db96ec06f27973a3e7 Mon Sep 17 00:00:00 2001 From: codrin2 Date: Thu, 31 Oct 2024 21:29:12 +0900 Subject: [PATCH 081/203] Revert "Merge pull request #31 from Team-pingping/feature/30-refresh-ping" This reverts commit 2bcba4a44272f8314de9708a6dd882257d1b2c86, reversing changes made to 527ca6f2fca604d1922cb8c48df6be2192580b67. --- Ping-Api/src/docs/asciidoc/NonMember.adoc | 6 +- .../com/ping/api/nonmember/NonMemberApi.kt | 1 - .../ping/api/nonmember/NonMemberController.kt | 5 -- .../api/nonmember/NonMemberControllerTest.kt | 73 ------------------- .../application/nonmember/NonMemberService.kt | 70 +----------------- 5 files changed, 5 insertions(+), 150 deletions(-) diff --git a/Ping-Api/src/docs/asciidoc/NonMember.adoc b/Ping-Api/src/docs/asciidoc/NonMember.adoc index f3d690f..4079a21 100644 --- a/Ping-Api/src/docs/asciidoc/NonMember.adoc +++ b/Ping-Api/src/docs/asciidoc/NonMember.adoc @@ -19,8 +19,4 @@ operation::NonMemberControllerTest/updateNonMemberPings[snippets='http-request,r [[Post-NonMemberLogin]] === 비회원 로그인 -operation::NonMemberControllerTest/loginNonMember[snippets='http-request,request-fields,http-response'] - -[[Get-RefreshAllNonMemberPings]] -=== 비회원 모든 핑 갱신 -operation::NonMemberControllerTest/refreshAllNonMemberPings[snippets='http-request,http-response,query-parameters,response-fields'] \ No newline at end of file +operation::NonMemberControllerTest/loginNonMember[snippets='http-request,request-fields,http-response'] \ No newline at end of file diff --git a/Ping-Api/src/main/kotlin/com/ping/api/nonmember/NonMemberApi.kt b/Ping-Api/src/main/kotlin/com/ping/api/nonmember/NonMemberApi.kt index 7575404..da36b82 100644 --- a/Ping-Api/src/main/kotlin/com/ping/api/nonmember/NonMemberApi.kt +++ b/Ping-Api/src/main/kotlin/com/ping/api/nonmember/NonMemberApi.kt @@ -5,5 +5,4 @@ object NonMemberApi { const val LOGIN = "$BASE_URL/login" const val PING = "$BASE_URL/pings" const val PING_NONMEMBERID = "$PING/{nonMemberId}" - const val PING_REFRESH_ALL = "$PING/refresh-all" } \ No newline at end of file diff --git a/Ping-Api/src/main/kotlin/com/ping/api/nonmember/NonMemberController.kt b/Ping-Api/src/main/kotlin/com/ping/api/nonmember/NonMemberController.kt index c8d47c6..12ff8f4 100644 --- a/Ping-Api/src/main/kotlin/com/ping/api/nonmember/NonMemberController.kt +++ b/Ping-Api/src/main/kotlin/com/ping/api/nonmember/NonMemberController.kt @@ -41,9 +41,4 @@ class NonMemberController( fun updateNonMemberPings(@RequestBody request: UpdateNonMemberPings.Request) { nonMemberService.updateNonMemberPings(request) } - - @GetMapping(NonMemberApi.PING_REFRESH_ALL) - fun refreshAllNonMemberPings(@RequestParam uuid: String): GetAllNonMemberPings.Response { - return nonMemberService.refreshAllNonMemberPings(uuid) - } } \ No newline at end of file diff --git a/Ping-Api/src/test/kotlin/com/ping/api/nonmember/NonMemberControllerTest.kt b/Ping-Api/src/test/kotlin/com/ping/api/nonmember/NonMemberControllerTest.kt index 7eceda2..1e41523 100644 --- a/Ping-Api/src/test/kotlin/com/ping/api/nonmember/NonMemberControllerTest.kt +++ b/Ping-Api/src/test/kotlin/com/ping/api/nonmember/NonMemberControllerTest.kt @@ -1,7 +1,6 @@ package com.ping.api.nonmember import com.epages.restdocs.apispec.MockMvcRestDocumentationWrapper -import com.epages.restdocs.apispec.ResourceDocumentation.parameterWithName import com.epages.restdocs.apispec.ResourceDocumentation.resource import com.epages.restdocs.apispec.ResourceSnippetParameters import com.epages.restdocs.apispec.Schema @@ -301,76 +300,4 @@ class NonMemberControllerTest : BaseRestDocsTest() { ) ) } - - @Test - @DisplayName("비회원 모든 핑 리프레쉬") - fun refreshAllNonMemberPings() { - // given - val uuid = "test-uuid" - val response = GetAllNonMemberPings.Response( - eventName = "Sample Event", - nonMembers = listOf( - GetAllNonMemberPings.NonMember(nonMemberId = 1, name = "핑핑이1"), - GetAllNonMemberPings.NonMember(nonMemberId = 2, name = "핑핑이2") - ), - px = 127.00001, - py = 37.00001, - pings = listOf( - GetAllNonMemberPings.Ping( - iconLevel = 2, - nonMembers = listOf( - GetAllNonMemberPings.NonMember(nonMemberId = 1, name = "핑핑이1"), - GetAllNonMemberPings.NonMember(nonMemberId = 2, name = "핑핑이2") - ), - url = "https://map.naver.com/p/entry/place/1946678040", - placeName = "호이", - px = 126.971178, - py = 37.5302481 - ) - ) - ) - - Mockito.`when`(nonMemberService.refreshAllNonMemberPings(uuid)).thenReturn(response) - - // when - val result: ResultActions = mockMvc.perform( - RestDocumentationRequestBuilders.get(NonMemberApi.PING_REFRESH_ALL) - .param("uuid", uuid) - .contentType(MediaType.APPLICATION_JSON) - ) - - // then - result.andExpect(status().isOk) - .andDo( - MockMvcRestDocumentationWrapper.document( - "nonmember/refreshAllNonMemberPings", - preprocessRequest(prettyPrint()), - preprocessResponse(prettyPrint()), - resource( - ResourceSnippetParameters.builder() - .tag("NonMember") - .description("비회원 모든 핑 리프레쉬") - .queryParameters( - parameterWithName("uuid").description("이벤트 식별자 UUID") - ) - .responseFields( - fieldWithPath("eventName").description("이벤트 이름"), - fieldWithPath("px").description("이벤트 중심 경도"), - fieldWithPath("py").description("이벤트 중심 위도"), - fieldWithPath("nonMembers[].nonMemberId").description("비회원의 id"), - fieldWithPath("nonMembers[].name").description("비회원 이름"), - fieldWithPath("pings[].iconLevel").description("아이콘 레벨\n4:가장 많이 겹침\n3:그다음\n2:그다음\n1:나머지"), - fieldWithPath("pings[].nonMembers[].nonMemberId").description("비회원 id"), - fieldWithPath("pings[].nonMembers[].name").description("비회원 이름"), - fieldWithPath("pings[].url").description("장소 URL"), - fieldWithPath("pings[].placeName").description("장소 이름"), - fieldWithPath("pings[].px").description("경도"), - fieldWithPath("pings[].py").description("위도") - ) - .responseSchema(Schema.schema("GetAllNonMemberPingsResponse")) - .build() - ) - ) - ) - } } \ No newline at end of file diff --git a/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberService.kt b/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberService.kt index 2cf209b..148adda 100644 --- a/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberService.kt +++ b/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberService.kt @@ -198,59 +198,6 @@ class NonMemberService( }) } - fun refreshAllNonMemberPings(uuid: String): GetAllNonMemberPings.Response { - val shareUrl = shareUrlRepository.findByUuid(uuid) - ?: throw CustomException(ExceptionContent.INVALID_SHARE_URL) - - val nonMemberList = nonMemberRepository.findAllByShareUrl(shareUrl.id) - - nonMemberList.forEach { nonMember -> - val updatedBookmarkUrls = nonMemberBookmarkUrlRepository.findAllByNonMemberId(nonMember.id) - val updatedStoreUrls = nonMemberStoreUrlRepository.findAllByNonMemberId(nonMember.id) - - // 새로운 북마크와 스토어 URL에 대한 업데이트 처리 - handleBookmarkUrls(updatedBookmarkUrls.map { it.bookmarkUrl }, nonMember) - handleStoreUrls(updatedStoreUrls.map { it.storeUrl }, nonMember) - } - - val nonMembers = nonMemberList.map { nonMember -> - GetAllNonMemberPings.NonMember( - nonMemberId = nonMember.id, - name = nonMember.name - ) - } - - // 핑 데이터 생성 및 아이콘 레벨 할당 - val nonMemberPlaces = nonMembersToNonMemberPlacesMap(nonMemberList) - val pings = nonMemberPlaces.entries.mapIndexed { index, nonMemberPlace -> - val level = calculateIconLevel(index, nonMemberPlace.key) - - nonMemberPlace.value.map { bookmarkPair -> - GetAllNonMemberPings.Ping( - iconLevel = level, - nonMembers = bookmarkPair.second.map { - GetAllNonMemberPings.NonMember( - nonMemberId = it.id, - name = it.name - ) - }, - url = bookmarkPair.first.url, - placeName = bookmarkPair.first.name, - px = bookmarkPair.first.px, - py = bookmarkPair.first.py, - ) - } - }.flatten() - - return GetAllNonMemberPings.Response( - eventName = shareUrl.eventName, - nonMembers = nonMembers, - px = shareUrl.latitude, - py = shareUrl.longtitude, - pings = pings - ) - } - private fun createNonMemberUpdateStatus(newNonMember: NonMemberDomain, shareUrlId: Long) { // 새로 생성된 비회원을 제외한 기존 비회원 목록 조회 val existingNonMembers = nonMemberRepository.findAllByShareUrl(shareUrlId) @@ -365,19 +312,10 @@ class NonMemberService( nonMemberPlaceRepository.deleteAll(placesToDelete) } - private fun calculateIconLevel(index: Int, overlapCount: Int): Int { - val mostOverlappedIconLevel = 4 - val secondOverlappedIconLevel = 3 - val thirdOverlappedIconLevel = 2 - val remainderIconLevel = 1 - - return when { - overlapCount == 1 -> remainderIconLevel - index == 0 -> mostOverlappedIconLevel - index == 1 -> secondOverlappedIconLevel - index == 2 -> thirdOverlappedIconLevel - else -> remainderIconLevel - } + fun findUrlsToUpdate(existingUrls: List, newUrls: List): Pair, List> { + val urlsToAdd = newUrls.filterNot { it in existingUrls } + val urlsToDelete = existingUrls.filterNot { it in newUrls } + return Pair(urlsToAdd, urlsToDelete) } private fun isBookmarkExists(sid: String): Boolean { From a2a788132f3a14ae57f2798d8d7a6e54206305e8 Mon Sep 17 00:00:00 2001 From: codrin2 Date: Thu, 31 Oct 2024 21:50:26 +0900 Subject: [PATCH 082/203] =?UTF-8?q?fix:=20revert=20=ED=96=88=EB=8D=98=20?= =?UTF-8?q?=EC=9D=BC=EB=B6=80=20=EC=BD=94=EB=93=9C=20=EB=B3=B5=EA=B5=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Ping-Api/src/docs/asciidoc/NonMember.adoc | 6 +- .../com/ping/api/nonmember/NonMemberApi.kt | 1 + .../ping/api/nonmember/NonMemberController.kt | 5 ++ .../api/nonmember/NonMemberControllerTest.kt | 70 +++++++++++++++++++ .../application/nonmember/NonMemberService.kt | 16 +++-- 5 files changed, 93 insertions(+), 5 deletions(-) diff --git a/Ping-Api/src/docs/asciidoc/NonMember.adoc b/Ping-Api/src/docs/asciidoc/NonMember.adoc index 4079a21..f3d690f 100644 --- a/Ping-Api/src/docs/asciidoc/NonMember.adoc +++ b/Ping-Api/src/docs/asciidoc/NonMember.adoc @@ -19,4 +19,8 @@ operation::NonMemberControllerTest/updateNonMemberPings[snippets='http-request,r [[Post-NonMemberLogin]] === 비회원 로그인 -operation::NonMemberControllerTest/loginNonMember[snippets='http-request,request-fields,http-response'] \ No newline at end of file +operation::NonMemberControllerTest/loginNonMember[snippets='http-request,request-fields,http-response'] + +[[Get-RefreshAllNonMemberPings]] +=== 비회원 모든 핑 갱신 +operation::NonMemberControllerTest/refreshAllNonMemberPings[snippets='http-request,http-response,query-parameters,response-fields'] \ No newline at end of file diff --git a/Ping-Api/src/main/kotlin/com/ping/api/nonmember/NonMemberApi.kt b/Ping-Api/src/main/kotlin/com/ping/api/nonmember/NonMemberApi.kt index da36b82..7575404 100644 --- a/Ping-Api/src/main/kotlin/com/ping/api/nonmember/NonMemberApi.kt +++ b/Ping-Api/src/main/kotlin/com/ping/api/nonmember/NonMemberApi.kt @@ -5,4 +5,5 @@ object NonMemberApi { const val LOGIN = "$BASE_URL/login" const val PING = "$BASE_URL/pings" const val PING_NONMEMBERID = "$PING/{nonMemberId}" + const val PING_REFRESH_ALL = "$PING/refresh-all" } \ No newline at end of file diff --git a/Ping-Api/src/main/kotlin/com/ping/api/nonmember/NonMemberController.kt b/Ping-Api/src/main/kotlin/com/ping/api/nonmember/NonMemberController.kt index 12ff8f4..c8d47c6 100644 --- a/Ping-Api/src/main/kotlin/com/ping/api/nonmember/NonMemberController.kt +++ b/Ping-Api/src/main/kotlin/com/ping/api/nonmember/NonMemberController.kt @@ -41,4 +41,9 @@ class NonMemberController( fun updateNonMemberPings(@RequestBody request: UpdateNonMemberPings.Request) { nonMemberService.updateNonMemberPings(request) } + + @GetMapping(NonMemberApi.PING_REFRESH_ALL) + fun refreshAllNonMemberPings(@RequestParam uuid: String): GetAllNonMemberPings.Response { + return nonMemberService.refreshAllNonMemberPings(uuid) + } } \ No newline at end of file diff --git a/Ping-Api/src/test/kotlin/com/ping/api/nonmember/NonMemberControllerTest.kt b/Ping-Api/src/test/kotlin/com/ping/api/nonmember/NonMemberControllerTest.kt index 1e41523..159c603 100644 --- a/Ping-Api/src/test/kotlin/com/ping/api/nonmember/NonMemberControllerTest.kt +++ b/Ping-Api/src/test/kotlin/com/ping/api/nonmember/NonMemberControllerTest.kt @@ -1,6 +1,7 @@ package com.ping.api.nonmember import com.epages.restdocs.apispec.MockMvcRestDocumentationWrapper +import com.epages.restdocs.apispec.ResourceDocumentation.parameterWithName import com.epages.restdocs.apispec.ResourceDocumentation.resource import com.epages.restdocs.apispec.ResourceSnippetParameters import com.epages.restdocs.apispec.Schema @@ -300,4 +301,73 @@ class NonMemberControllerTest : BaseRestDocsTest() { ) ) } + + @Test + @DisplayName("비회원 모든 핑 리프레쉬") + fun refreshAllNonMemberPings() { + // given + val uuid = "test-uuid" + val response = GetAllNonMemberPings.Response( + eventName = "Sample Event", + nonMembers = listOf( + GetAllNonMemberPings.NonMember(nonMemberId = 1, name = "핑핑이1"), + GetAllNonMemberPings.NonMember(nonMemberId = 2, name = "핑핑이2") + ), + px = 127.00001, + py = 37.00001, + pings = listOf( + GetAllNonMemberPings.Ping( + iconLevel = 2, + nonMembers = listOf( + GetAllNonMemberPings.NonMember(nonMemberId = 1, name = "핑핑이1"), + GetAllNonMemberPings.NonMember(nonMemberId = 2, name = "핑핑이2") + ), + url = "https://map.naver.com/p/entry/place/1946678040", + placeName = "호이", + px = 126.971178, + py = 37.5302481 + ) + ) + ) + Mockito.`when`(nonMemberService.refreshAllNonMemberPings(uuid)).thenReturn(response) + // when + val result: ResultActions = mockMvc.perform( + RestDocumentationRequestBuilders.get(NonMemberApi.PING_REFRESH_ALL) + .param("uuid", uuid) + .contentType(MediaType.APPLICATION_JSON) + ) + // then + result.andExpect(status().isOk) + .andDo( + MockMvcRestDocumentationWrapper.document( + "nonmember/refreshAllNonMemberPings", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + resource( + ResourceSnippetParameters.builder() + .tag("NonMember") + .description("비회원 모든 핑 리프레쉬") + .queryParameters( + parameterWithName("uuid").description("이벤트 식별자 UUID") + ) + .responseFields( + fieldWithPath("eventName").description("이벤트 이름"), + fieldWithPath("px").description("이벤트 중심 경도"), + fieldWithPath("py").description("이벤트 중심 위도"), + fieldWithPath("nonMembers[].nonMemberId").description("비회원의 id"), + fieldWithPath("nonMembers[].name").description("비회원 이름"), + fieldWithPath("pings[].iconLevel").description("아이콘 레벨\n4:가장 많이 겹침\n3:그다음\n2:그다음\n1:나머지"), + fieldWithPath("pings[].nonMembers[].nonMemberId").description("비회원 id"), + fieldWithPath("pings[].nonMembers[].name").description("비회원 이름"), + fieldWithPath("pings[].url").description("장소 URL"), + fieldWithPath("pings[].placeName").description("장소 이름"), + fieldWithPath("pings[].px").description("경도"), + fieldWithPath("pings[].py").description("위도") + ) + .responseSchema(Schema.schema("GetAllNonMemberPingsResponse")) + .build() + ) + ) + ) + } } \ No newline at end of file diff --git a/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberService.kt b/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberService.kt index 65fe13d..fdbdaeb 100644 --- a/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberService.kt +++ b/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberService.kt @@ -373,10 +373,18 @@ class NonMemberService( nonMemberPlaceRepository.deleteAll(placesToDelete) } - fun findUrlsToUpdate(existingUrls: List, newUrls: List): Pair, List> { - val urlsToAdd = newUrls.filterNot { it in existingUrls } - val urlsToDelete = existingUrls.filterNot { it in newUrls } - return Pair(urlsToAdd, urlsToDelete) + private fun calculateIconLevel(index: Int, overlapCount: Int): Int { + val mostOverlappedIconLevel = 4 + val secondOverlappedIconLevel = 3 + val thirdOverlappedIconLevel = 2 + val remainderIconLevel = 1 + return when { + overlapCount == 1 -> remainderIconLevel + index == 0 -> mostOverlappedIconLevel + index == 1 -> secondOverlappedIconLevel + index == 2 -> thirdOverlappedIconLevel + else -> remainderIconLevel + } } private fun isBookmarkExists(sid: String): Boolean { From 2b2280b3aecfc8dd894e88c7746ad842d52c0b48 Mon Sep 17 00:00:00 2001 From: sominyun Date: Sat, 2 Nov 2024 00:01:36 +0900 Subject: [PATCH 083/203] =?UTF-8?q?refactor:#33=20=EC=95=88=EC=93=B0?= =?UTF-8?q?=EB=8A=94=20docker-compose=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker-compose.yaml | 38 -------------------------------------- 1 file changed, 38 deletions(-) delete mode 100644 docker-compose.yaml diff --git a/docker-compose.yaml b/docker-compose.yaml deleted file mode 100644 index c657426..0000000 --- a/docker-compose.yaml +++ /dev/null @@ -1,38 +0,0 @@ -version: '3.7' -services: - mysql: - container_name: mappin-mysql - image: mysql:8 - command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci --explicit_defaults_for_timestamp=1 - ports: - - 3308:3306 - environment: - - MYSQL_DATABASE=mappin - - MYSQL_USER=admin - - MYSQL_PASSWORD=1234 - - MYSQL_ROOT_PASSWORD=1234 - - TZ=UTC - volumes: - - ./mysql/init:/docker-entrypoint-initdb.d - - mongo: - image: mongo:latest - container_name: mappin-mongo - ports: - - "27017:27017" - environment: - MONGO_INITDB_ROOT_USERNAME: admin - MONGO_INITDB_ROOT_PASSWORD: 1234 - volumes: - - mongo-data:/data/db - - redis: - image: redis:latest - ports: - - "6379:6379" - volumes: - - redis-data:/data - -volumes: - mongo-data: - redis-data: \ No newline at end of file From 99491b0a2f14e2b31416b80b3e887087f4559a6f Mon Sep 17 00:00:00 2001 From: sominyun Date: Sat, 2 Nov 2024 00:21:11 +0900 Subject: [PATCH 084/203] =?UTF-8?q?refactor:#33=20ValidationUtil=20class?= =?UTF-8?q?=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ping/application/nonmember/NonMemberService.kt | 10 ++++------ .../kotlin/com/ping/common/util/ValidationUtil.kt | 14 +++++--------- 2 files changed, 9 insertions(+), 15 deletions(-) rename Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberValidator.kt => Ping-Common/src/main/kotlin/com/ping/common/util/ValidationUtil.kt (67%) diff --git a/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberService.kt b/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberService.kt index fdbdaeb..b99c95b 100644 --- a/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberService.kt +++ b/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberService.kt @@ -1,13 +1,11 @@ package com.ping.application.nonmember -import com.ping.application.nonmember.dto.CreateNonMember -import com.ping.application.nonmember.dto.GetAllNonMemberPings -import com.ping.application.nonmember.dto.UpdateNonMemberPings -import com.ping.application.nonmember.dto.GetNonMemberPing +import com.ping.application.nonmember.dto.* import com.ping.client.naver.map.NaverMapClient import com.ping.common.exception.CustomException import com.ping.common.exception.ExceptionContent import com.ping.common.util.UrlUtil +import com.ping.common.util.ValidationUtil import com.ping.domain.nonmember.aggregate.* import com.ping.domain.nonmember.repository.* import org.springframework.stereotype.Service @@ -29,9 +27,9 @@ class NonMemberService( @Transactional fun createNonMemberPings(request: CreateNonMember.Request) { //이름 공백, 특수문자, 숫자 불가 - validator.name(request.name) + ValidationUtil.validateName(request.name) // 비밀번호 형식 검사 (4자리 숫자) - validator.password(request.password) + ValidationUtil.validatePassword(request.password) val shareUrl = shareUrlRepository.findByUuid(request.uuid) ?: throw CustomException(ExceptionContent.INVALID_SHARE_URL) diff --git a/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberValidator.kt b/Ping-Common/src/main/kotlin/com/ping/common/util/ValidationUtil.kt similarity index 67% rename from Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberValidator.kt rename to Ping-Common/src/main/kotlin/com/ping/common/util/ValidationUtil.kt index 6384113..e275b4b 100644 --- a/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberValidator.kt +++ b/Ping-Common/src/main/kotlin/com/ping/common/util/ValidationUtil.kt @@ -1,14 +1,10 @@ -package com.ping.application.nonmember +package com.ping.common.util import com.ping.common.exception.CustomException import com.ping.common.exception.ExceptionContent -import org.springframework.stereotype.Component -@Component -class NonMemberValidator { - - // 이름 유효성 검증 - fun name(name: String) { +object ValidationUtil { + fun validateName(name: String) { val namePattern = "^[가-힣a-zA-Z]{1,6}\$".toRegex() if (!namePattern.matches(name)) { throw CustomException(ExceptionContent.INVALID_NAME_FORMAT) @@ -16,9 +12,9 @@ class NonMemberValidator { } // 비밀번호 유효성 검증 - fun password(password: String) { + fun validatePassword(password: String) { if (!password.matches(Regex("\\d{4}"))) { throw CustomException(ExceptionContent.INVALID_PASSWORD_FORMAT) } } -} +} \ No newline at end of file From cc214123d959387ab1c85b0dbae13e180e5ae674 Mon Sep 17 00:00:00 2001 From: sominyun Date: Sat, 2 Nov 2024 00:21:55 +0900 Subject: [PATCH 085/203] =?UTF-8?q?refactor:#33=20=EB=B6=84=EB=A6=AC?= =?UTF-8?q?=EB=90=9C=20nonMemberService=20=ED=95=A9=EC=B9=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ping/api/nonmember/NonMemberController.kt | 6 +-- .../api/nonmember/NonMemberControllerTest.kt | 6 +-- .../nonmember/NonMemberLoginService.kt | 45 ------------------- .../application/nonmember/NonMemberService.kt | 27 ++++++++++- 4 files changed, 28 insertions(+), 56 deletions(-) delete mode 100644 Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberLoginService.kt diff --git a/Ping-Api/src/main/kotlin/com/ping/api/nonmember/NonMemberController.kt b/Ping-Api/src/main/kotlin/com/ping/api/nonmember/NonMemberController.kt index c8d47c6..0dc7718 100644 --- a/Ping-Api/src/main/kotlin/com/ping/api/nonmember/NonMemberController.kt +++ b/Ping-Api/src/main/kotlin/com/ping/api/nonmember/NonMemberController.kt @@ -1,6 +1,5 @@ package com.ping.api.nonmember -import com.ping.application.nonmember.NonMemberLoginService import com.ping.application.nonmember.NonMemberService import com.ping.application.nonmember.dto.CreateNonMember import com.ping.application.nonmember.dto.GetAllNonMemberPings @@ -14,12 +13,11 @@ import org.springframework.web.bind.annotation.* @RestController class NonMemberController( - private val nonMemberService: NonMemberService, - private val nonMemberLoginService: NonMemberLoginService + private val nonMemberService: NonMemberService ) { @PutMapping(NonMemberApi.LOGIN) fun loginNonMember(@RequestBody request: LoginNonMember.Request): LoginNonMember.Response { - return nonMemberLoginService.login(request) + return nonMemberService.login(request) } @PostMapping(NonMemberApi.PING) diff --git a/Ping-Api/src/test/kotlin/com/ping/api/nonmember/NonMemberControllerTest.kt b/Ping-Api/src/test/kotlin/com/ping/api/nonmember/NonMemberControllerTest.kt index 159c603..3a2f8f9 100644 --- a/Ping-Api/src/test/kotlin/com/ping/api/nonmember/NonMemberControllerTest.kt +++ b/Ping-Api/src/test/kotlin/com/ping/api/nonmember/NonMemberControllerTest.kt @@ -6,7 +6,6 @@ import com.epages.restdocs.apispec.ResourceDocumentation.resource import com.epages.restdocs.apispec.ResourceSnippetParameters import com.epages.restdocs.apispec.Schema import com.ping.api.global.BaseRestDocsTest -import com.ping.application.nonmember.NonMemberLoginService import com.ping.application.nonmember.NonMemberService import com.ping.application.nonmember.dto.* import com.ping.infra.nonmember.domain.mongo.repository.BookmarkMongoRepository @@ -32,9 +31,6 @@ class NonMemberControllerTest : BaseRestDocsTest() { @MockBean private lateinit var nonMemberService: NonMemberService - @MockBean - private lateinit var nonMemberLoginService: NonMemberLoginService - @MockBean private lateinit var bookmarkMongoRepository: BookmarkMongoRepository @@ -51,7 +47,7 @@ class NonMemberControllerTest : BaseRestDocsTest() { ) // 로그인 서비스의 반환 값 설정 - Mockito.`when`(nonMemberLoginService.login(request)).thenReturn(response) + Mockito.`when`(nonMemberService.login(request)).thenReturn(response) val jsonRequest = """ { diff --git a/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberLoginService.kt b/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberLoginService.kt deleted file mode 100644 index 8802bef..0000000 --- a/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberLoginService.kt +++ /dev/null @@ -1,45 +0,0 @@ -package com.ping.application.nonmember - -import com.ping.application.nonmember.dto.LoginNonMember -import com.ping.common.exception.CustomException -import com.ping.common.exception.ExceptionContent -import com.ping.domain.nonmember.repository.NonMemberBookmarkUrlRepository -import com.ping.domain.nonmember.repository.NonMemberRepository -import com.ping.domain.nonmember.repository.NonMemberStoreUrlRepository -import org.springframework.stereotype.Service -import org.springframework.transaction.annotation.Transactional - -@Service -@Transactional(readOnly = true) -class NonMemberLoginService( - private val nonMemberRepository: NonMemberRepository, - private val nonMemberBookmarkUrlRepository: NonMemberBookmarkUrlRepository, - private val nonMemberStoreUrlRepository: NonMemberStoreUrlRepository, - private val validator: NonMemberValidator -) { - fun login(request: LoginNonMember.Request): LoginNonMember.Response { - // 비밀번호 형식 검사 (4자리 숫자) - validator.password(request.password) - - val nonMember = nonMemberRepository.findById(request.nonMemberId) ?: throw CustomException(ExceptionContent.NON_MEMBER_NOT_FOUND) - - // 비밀번호가 일치하는지 비교 - if (request.password != nonMember.password) { - throw CustomException(ExceptionContent.NON_MEMBER_LOGIN_FAILED) - } - - // 비회원의 북마크 URL 리스트와 스토어 URL 리스트 조회 - val bookmarkUrls = nonMemberBookmarkUrlRepository.findAllByNonMemberId(request.nonMemberId).map { it.bookmarkUrl } - val storeUrls = nonMemberStoreUrlRepository.findAllByNonMemberId(request.nonMemberId).map { it.storeUrl } - - // 로그인 응답 데이터 반환 - return LoginNonMember.Response( - nonMemberId = nonMember.id, - name = nonMember.name, - bookmarkUrls = bookmarkUrls, - storeUrls = storeUrls - ) - } -} - - diff --git a/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberService.kt b/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberService.kt index b99c95b..93d0d4e 100644 --- a/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberService.kt +++ b/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberService.kt @@ -21,9 +21,32 @@ class NonMemberService( private val nonMemberBookmarkUrlRepository: NonMemberBookmarkUrlRepository, private val nonMemberStoreUrlRepository: NonMemberStoreUrlRepository, private val nonMemberUpdateStatusRepository: NonMemberUpdateStatusRepository, - private val naverMapClient: NaverMapClient, - private val validator: NonMemberValidator + private val naverMapClient: NaverMapClient ) { + fun login(request: LoginNonMember.Request): LoginNonMember.Response { + // 비밀번호 형식 검사 (4자리 숫자) + ValidationUtil.validatePassword(request.password) + + val nonMember = nonMemberRepository.findById(request.nonMemberId) ?: throw CustomException(ExceptionContent.NON_MEMBER_NOT_FOUND) + + // 비밀번호가 일치하는지 비교 + if (request.password != nonMember.password) { + throw CustomException(ExceptionContent.NON_MEMBER_LOGIN_FAILED) + } + + // 비회원의 북마크 URL 리스트와 스토어 URL 리스트 조회 + val bookmarkUrls = nonMemberBookmarkUrlRepository.findAllByNonMemberId(request.nonMemberId).map { it.bookmarkUrl } + val storeUrls = nonMemberStoreUrlRepository.findAllByNonMemberId(request.nonMemberId).map { it.storeUrl } + + // 로그인 응답 데이터 반환 + return LoginNonMember.Response( + nonMemberId = nonMember.id, + name = nonMember.name, + bookmarkUrls = bookmarkUrls, + storeUrls = storeUrls + ) + } + @Transactional fun createNonMemberPings(request: CreateNonMember.Request) { //이름 공백, 특수문자, 숫자 불가 From a20493cfe691c488b885c5939e71cf0ddff7fb58 Mon Sep 17 00:00:00 2001 From: codrin2 Date: Sun, 3 Nov 2024 19:33:42 +0900 Subject: [PATCH 086/203] =?UTF-8?q?fix(NonMemberService):=20=ED=95=91=20?= =?UTF-8?q?=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8,=20=EB=A6=AC=ED=94=84?= =?UTF-8?q?=EB=A0=88=EC=89=AC=20=EB=B9=84=ED=9A=8C=EC=9B=90=EC=9D=98=20?= =?UTF-8?q?=ED=95=91=EC=9D=B4=20=EC=A0=9C=EB=8C=80=EB=A1=9C=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=20=EC=95=88=EB=90=98=EB=8A=94=20=EC=98=A4=EB=A5=98=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/nonmember/NonMemberService.kt | 170 ++++++++---------- .../repository/NonMemberPlaceRepository.kt | 2 +- .../NonMemberPlaceRepositoryImpl.kt | 5 +- 3 files changed, 76 insertions(+), 101 deletions(-) diff --git a/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberService.kt b/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberService.kt index 93d0d4e..bc41747 100644 --- a/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberService.kt +++ b/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberService.kt @@ -24,26 +24,22 @@ class NonMemberService( private val naverMapClient: NaverMapClient ) { fun login(request: LoginNonMember.Request): LoginNonMember.Response { - // 비밀번호 형식 검사 (4자리 숫자) ValidationUtil.validatePassword(request.password) val nonMember = nonMemberRepository.findById(request.nonMemberId) ?: throw CustomException(ExceptionContent.NON_MEMBER_NOT_FOUND) - // 비밀번호가 일치하는지 비교 if (request.password != nonMember.password) { throw CustomException(ExceptionContent.NON_MEMBER_LOGIN_FAILED) } - // 비회원의 북마크 URL 리스트와 스토어 URL 리스트 조회 - val bookmarkUrls = nonMemberBookmarkUrlRepository.findAllByNonMemberId(request.nonMemberId).map { it.bookmarkUrl } - val storeUrls = nonMemberStoreUrlRepository.findAllByNonMemberId(request.nonMemberId).map { it.storeUrl } + val savedBookmarkUrls = nonMemberBookmarkUrlRepository.findAllByNonMemberId(request.nonMemberId).map { it.bookmarkUrl } + val savedStoreUrls = nonMemberStoreUrlRepository.findAllByNonMemberId(request.nonMemberId).map { it.storeUrl } - // 로그인 응답 데이터 반환 return LoginNonMember.Response( nonMemberId = nonMember.id, name = nonMember.name, - bookmarkUrls = bookmarkUrls, - storeUrls = storeUrls + bookmarkUrls = savedBookmarkUrls, + storeUrls = savedStoreUrls ) } @@ -175,50 +171,41 @@ class NonMemberService( @Transactional fun updateNonMemberPings(request: UpdateNonMemberPings.Request) { - val nonMemberDomain = nonMemberRepository.findById(request.nonMemberId) + val nonMember = nonMemberRepository.findById(request.nonMemberId) ?: throw CustomException(ExceptionContent.NON_MEMBER_NOT_FOUND) - // 현재 비회원의 기존 sid 추출 - val existingSids = nonMemberPlaceRepository.findAllByNonMemberId(request.nonMemberId).map { it.sid }.toSet() + val nonMemberPlaces = nonMemberPlaceRepository.findAllByNonMemberId(request.nonMemberId) + val existingSids = nonMemberPlaces.map { it.sid }.toSet() - // 새로운 북마크 및 가게 데이터 처리 - val bookmarkData = handleBookmarkUrls(request.bookmarkUrls, nonMemberDomain) - val storeData = handleStoreUrls(request.storeUrls, nonMemberDomain) + val bookmarkData = handleBookmarkUrls(request.bookmarkUrls) + val storeData = handleStoreUrls(request.storeUrls) - // 전체 새로운 sid 집합 - val allNewSids = (bookmarkData.sids + storeData.sids) - if (existingSids != allNewSids) { - updatePlaceSids(nonMemberDomain, allNewSids) + val newSids = (bookmarkData.sids + storeData.sids) + if (existingSids != newSids) { + updateNonMemberPlaces(nonMember,nonMemberPlaces, newSids, existingSids) } - // 기존 북마크 URL 및 상점 URL 조회 - val existingBookmarkUrls = nonMemberBookmarkUrlRepository.findAllByNonMemberId(nonMemberDomain.id) - val existingStoreUrls = nonMemberStoreUrlRepository.findAllByNonMemberId(nonMemberDomain.id) + val existingBookmarkUrls = nonMemberBookmarkUrlRepository.findAllByNonMemberId(nonMember.id) + val existingStoreUrls = nonMemberStoreUrlRepository.findAllByNonMemberId(nonMember.id) - // URL과 ID를 쉽게 매핑하기 위해 Map 형태로 변환 + // URL을 쉽게 매핑하기 위해 Map 형태로 변환 val existingBookmarkMap = existingBookmarkUrls.associateBy { it.bookmarkUrl } val existingStoreMap = existingStoreUrls.associateBy { it.storeUrl } - // 새 URL과 기존 URL을 비교하여 추가 및 삭제할 URL 식별 val bookmarkUrlsToAdd = request.bookmarkUrls.filterNot { it in existingBookmarkMap.keys } val bookmarkUrlsToDeleteIds = existingBookmarkUrls.filter { it.bookmarkUrl !in request.bookmarkUrls }.map { it.id } val storeUrlsToAdd = request.storeUrls.filterNot { it in existingStoreMap.keys } val storeUrlsToDeleteIds = existingStoreUrls.filter { it.storeUrl !in request.storeUrls }.map { it.id } - // 삭제할 URL을 ID 기반으로 삭제 + nonMemberBookmarkUrlRepository.saveAll(NonMemberBookmarkUrlDomain.of(nonMember, bookmarkUrlsToAdd)) + nonMemberStoreUrlRepository.saveAll(NonMemberStoreUrlDomain.of(nonMember, storeUrlsToAdd)) + nonMemberBookmarkUrlRepository.deleteAllByIds(bookmarkUrlsToDeleteIds) nonMemberStoreUrlRepository.deleteAllByIds(storeUrlsToDeleteIds) - - // 새 URL 추가 저장 - nonMemberBookmarkUrlRepository.saveAll(bookmarkUrlsToAdd.map { url -> - NonMemberBookmarkUrlDomain.of(nonMemberDomain, listOf(url)).first() - }) - nonMemberStoreUrlRepository.saveAll(storeUrlsToAdd.map { url -> - NonMemberStoreUrlDomain.of(nonMemberDomain, listOf(url)).first() - }) } + @Transactional fun refreshAllNonMemberPings(uuid: String): GetAllNonMemberPings.Response { val shareUrl = shareUrlRepository.findByUuid(uuid) ?: throw CustomException(ExceptionContent.INVALID_SHARE_URL) @@ -226,19 +213,19 @@ class NonMemberService( val nonMemberList = nonMemberRepository.findAllByShareUrl(shareUrl.id) nonMemberList.forEach { nonMember -> - // 기존 sid를 추출 - val existingSids = nonMemberPlaceRepository.findAllByNonMemberId(nonMember.id).map { it.sid }.toSet() + val nonMemberPlaces = nonMemberPlaceRepository.findAllByNonMemberId(nonMember.id) + val existingSids = nonMemberPlaces.map { it.sid }.toSet() + + val existingBookmarkUrls = nonMemberBookmarkUrlRepository.findAllByNonMemberId(nonMember.id).map { it.bookmarkUrl } + val existingStoreUrls = nonMemberStoreUrlRepository.findAllByNonMemberId(nonMember.id).map { it.storeUrl } - // 새로운 북마크 및 스토어 URL 처리 후 sid 추출 - val bookmarkData = handleBookmarkUrls(nonMemberBookmarkUrlRepository.findAllByNonMemberId(nonMember.id).map { it.bookmarkUrl }, nonMember) - val storeData = handleStoreUrls(nonMemberStoreUrlRepository.findAllByNonMemberId(nonMember.id).map { it.storeUrl }, nonMember) + val bookmarkData = handleBookmarkUrls(existingBookmarkUrls) + val storeData = handleStoreUrls(existingStoreUrls) - // 새로운 sid 집합 (북마크와 스토어 데이터의 sids 결합) - val allNewSids = (bookmarkData.sids + storeData.sids) + val newSids = (bookmarkData.sids + storeData.sids) - // 기존 sid와 비교하여 변경 사항이 있으면 업데이트 - if (existingSids != allNewSids) { - updatePlaceSids(nonMember, allNewSids) + if (existingSids != newSids) { + updateNonMemberPlaces(nonMember, nonMemberPlaces, newSids, existingSids) } } @@ -310,88 +297,81 @@ class NonMemberService( } private fun handleBookmarkUrls( - bookmarkUrls: List, nonMember: NonMemberDomain + bookmarkUrls: List ): BookmarkData { - val nonMemberPlaces = mutableListOf() val bookmarks = mutableListOf() - val newSids = mutableSetOf() // 중복 확인을 위한 sid 집합 + val allSids = mutableSetOf() bookmarkUrls.forEach { url -> val expandedUrl = UrlUtil.expandShortUrl(url) val bookmarkList = naverMapClient.bookmarkUrlToBookmarkLists(expandedUrl).bookmarkList bookmarkList.forEach { bookmark -> - // MongoDB에 존재하지 않는 경우에만 BookmarkDomain 생성 - if (!isBookmarkExists(bookmark.sid)) { - bookmarks.add( - BookmarkDomain( - name = bookmark.name, - px = bookmark.px, - py = bookmark.py, - sid = bookmark.sid, - address = bookmark.address, - mcidName = bookmark.mcidName, - url = "https://map.naver.com/p/entry/place/${bookmark.sid}" - ) + bookmarks.add( + BookmarkDomain( + name = bookmark.name, + px = bookmark.px, + py = bookmark.py, + sid = bookmark.sid, + address = bookmark.address, + mcidName = bookmark.mcidName, + url = "https://map.naver.com/p/entry/place/${bookmark.sid}" ) - } - - // 중복되지 않는 경우에만 NonMemberPlace 추가 - if (newSids.add(bookmark.sid)) { - nonMemberPlaces.add(NonMemberPlaceDomain.of(nonMember, bookmark.sid)) - } + ) + allSids.add(bookmark.sid) } } - return BookmarkData(nonMemberPlaces, bookmarks, newSids) + + val existingSids = bookmarkRepository.findAllBySidIn(allSids.toList()).map { it.sid }.toSet() + val bookmarksToAdd = bookmarks.filterNot { it.sid in existingSids } + bookmarkRepository.saveAll(bookmarksToAdd) + + return BookmarkData(bookmarks, allSids) } - // storeUrls에서 중복되지 않는 NonMemberPlaceDomain과 BookmarkDomain을 생성하고 반환 private fun handleStoreUrls( - storeUrls: List, nonMember: NonMemberDomain + storeUrls: List ): BookmarkData { - val nonMemberPlaces = mutableListOf() val bookmarks = mutableListOf() - val newSids = mutableSetOf() // 중복 확인을 위한 sid 집합 + val allSids = mutableSetOf() storeUrls.forEach { url -> val expandedUrl = UrlUtil.expandShortUrl(url) val store = naverMapClient.storeUrlToBookmark(expandedUrl) - // MongoDB에 존재하지 않는 경우에만 BookmarkDomain 생성 - if (!isBookmarkExists(store.sid)) { - bookmarks.add( - BookmarkDomain( - name = store.name, - px = store.px, - py = store.py, - sid = store.sid, - address = store.address, - mcidName = store.mcidName, - url = url - ) + bookmarks.add( + BookmarkDomain( + name = store.name, + px = store.px, + py = store.py, + sid = store.sid, + address = store.address, + mcidName = store.mcidName, + url = url ) - } + ) - // 중복되지 않는 경우에만 NonMemberPlace 추가 - if (newSids.add(store.sid)) { - nonMemberPlaces.add(NonMemberPlaceDomain.of(nonMember, store.sid)) - } + allSids.add(store.sid) } - return BookmarkData(nonMemberPlaces, bookmarks, newSids) - } - private fun updatePlaceSids(nonMemberDomain: NonMemberDomain, newSids: Set) { - val existingPlaces = nonMemberPlaceRepository.findAllByNonMemberId(nonMemberDomain.id) - val existingSids = existingPlaces.map { it.sid }.toSet() + val existingSids = bookmarkRepository.findAllBySidIn(allSids.toList()).map { it.sid }.toSet() + val bookmarksToAdd = bookmarks.filterNot { it.sid in existingSids } + bookmarkRepository.saveAll(bookmarksToAdd) + + return BookmarkData(bookmarks, allSids) + } + private fun updateNonMemberPlaces(nonMember: NonMemberDomain, nonMemberPlaces: List, newSids: Set, existingSids: Set) { val sidsToAdd = newSids - existingSids val sidsToDelete = existingSids - newSids - val placesToAdd = sidsToAdd.map { sid -> NonMemberPlaceDomain.of(nonMemberDomain, sid) } + val placesToAdd = sidsToAdd.map { sid -> NonMemberPlaceDomain.of(nonMember, sid) } nonMemberPlaceRepository.saveAll(placesToAdd) - val placesToDelete = existingPlaces.filter { it.sid in sidsToDelete } - nonMemberPlaceRepository.deleteAll(placesToDelete) + val placesIdToDelete = nonMemberPlaces + .filter { it.nonMember == nonMember && it.sid in sidsToDelete } + .map { it.id } + nonMemberPlaceRepository.deleteAll(placesIdToDelete) } private fun calculateIconLevel(index: Int, overlapCount: Int): Int { @@ -408,11 +388,7 @@ class NonMemberService( } } - private fun isBookmarkExists(sid: String): Boolean { - return bookmarkRepository.findAllBySidIn(listOf(sid)).isNotEmpty() - } private data class BookmarkData( - val places: List, val bookmarks: List, val sids: Set ) diff --git a/Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/repository/NonMemberPlaceRepository.kt b/Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/repository/NonMemberPlaceRepository.kt index 77d4409..71740d8 100644 --- a/Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/repository/NonMemberPlaceRepository.kt +++ b/Ping-Domain/src/main/kotlin/com/ping/domain/nonmember/repository/NonMemberPlaceRepository.kt @@ -6,5 +6,5 @@ interface NonMemberPlaceRepository { fun saveAll(nonMemberPlaceDomains: List): List fun findAllByNonMemberId(nonMemberId: Long): List - fun deleteAll(nonMemberPlaceDomains: List) + fun deleteAll(ids: List) } \ No newline at end of file diff --git a/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/repositoryImpl/NonMemberPlaceRepositoryImpl.kt b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/repositoryImpl/NonMemberPlaceRepositoryImpl.kt index 9e2bece..7bf2485 100644 --- a/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/repositoryImpl/NonMemberPlaceRepositoryImpl.kt +++ b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/repositoryImpl/NonMemberPlaceRepositoryImpl.kt @@ -17,8 +17,7 @@ class NonMemberPlaceRepositoryImpl( override fun findAllByNonMemberId(nonMemberId: Long): List { return nonMemberPlaceJpaRepository.findAllByNonMemberId(nonMemberId).map { NonMemberPlaceMapper.toDomain(it) } } - override fun deleteAll(nonMemberPlaceDomains: List) { - val entities = nonMemberPlaceDomains.map { NonMemberPlaceMapper.toEntity(it) } - nonMemberPlaceJpaRepository.deleteAll(entities) + override fun deleteAll(ids: List) { + nonMemberPlaceJpaRepository.deleteAllById(ids) } } \ No newline at end of file From e732f050032af66e0f89ef0e8c652b423480b35f Mon Sep 17 00:00:00 2001 From: codrin2 Date: Sun, 3 Nov 2024 20:49:51 +0900 Subject: [PATCH 087/203] =?UTF-8?q?refactor(NonMemberService):=20=EB=B9=84?= =?UTF-8?q?=ED=9A=8C=EC=9B=90=20=ED=95=91=20=EC=97=85=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=8A=B8=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/nonmember/NonMemberService.kt | 116 ++++++++++-------- 1 file changed, 66 insertions(+), 50 deletions(-) diff --git a/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberService.kt b/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberService.kt index bc41747..c2addeb 100644 --- a/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberService.kt +++ b/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberService.kt @@ -177,32 +177,13 @@ class NonMemberService( val nonMemberPlaces = nonMemberPlaceRepository.findAllByNonMemberId(request.nonMemberId) val existingSids = nonMemberPlaces.map { it.sid }.toSet() - val bookmarkData = handleBookmarkUrls(request.bookmarkUrls) - val storeData = handleStoreUrls(request.storeUrls) + val bookmarkUrlSids = handleBookmarkUrls(request.bookmarkUrls) + val storeUrlSids = handleStoreUrls(request.storeUrls) + val newSids = (bookmarkUrlSids + storeUrlSids) - val newSids = (bookmarkData.sids + storeData.sids) - if (existingSids != newSids) { - updateNonMemberPlaces(nonMember,nonMemberPlaces, newSids, existingSids) - } - - val existingBookmarkUrls = nonMemberBookmarkUrlRepository.findAllByNonMemberId(nonMember.id) - val existingStoreUrls = nonMemberStoreUrlRepository.findAllByNonMemberId(nonMember.id) - - // URL을 쉽게 매핑하기 위해 Map 형태로 변환 - val existingBookmarkMap = existingBookmarkUrls.associateBy { it.bookmarkUrl } - val existingStoreMap = existingStoreUrls.associateBy { it.storeUrl } - - val bookmarkUrlsToAdd = request.bookmarkUrls.filterNot { it in existingBookmarkMap.keys } - val bookmarkUrlsToDeleteIds = existingBookmarkUrls.filter { it.bookmarkUrl !in request.bookmarkUrls }.map { it.id } - - val storeUrlsToAdd = request.storeUrls.filterNot { it in existingStoreMap.keys } - val storeUrlsToDeleteIds = existingStoreUrls.filter { it.storeUrl !in request.storeUrls }.map { it.id } - - nonMemberBookmarkUrlRepository.saveAll(NonMemberBookmarkUrlDomain.of(nonMember, bookmarkUrlsToAdd)) - nonMemberStoreUrlRepository.saveAll(NonMemberStoreUrlDomain.of(nonMember, storeUrlsToAdd)) - - nonMemberBookmarkUrlRepository.deleteAllByIds(bookmarkUrlsToDeleteIds) - nonMemberStoreUrlRepository.deleteAllByIds(storeUrlsToDeleteIds) + updateNonMemberPlacesIfNeeded(nonMember, nonMemberPlaces, existingSids, newSids) + updateBookmarkUrls(nonMember, request.bookmarkUrls) + updateStoreUrls(nonMember, request.storeUrls) } @Transactional @@ -219,14 +200,11 @@ class NonMemberService( val existingBookmarkUrls = nonMemberBookmarkUrlRepository.findAllByNonMemberId(nonMember.id).map { it.bookmarkUrl } val existingStoreUrls = nonMemberStoreUrlRepository.findAllByNonMemberId(nonMember.id).map { it.storeUrl } - val bookmarkData = handleBookmarkUrls(existingBookmarkUrls) - val storeData = handleStoreUrls(existingStoreUrls) - - val newSids = (bookmarkData.sids + storeData.sids) + val bookmarkUrlSids = handleBookmarkUrls(existingBookmarkUrls) + val storeUrlSids = handleStoreUrls(existingStoreUrls) + val newSids = (bookmarkUrlSids + storeUrlSids) - if (existingSids != newSids) { - updateNonMemberPlaces(nonMember, nonMemberPlaces, newSids, existingSids) - } + updateNonMemberPlacesIfNeeded(nonMember, nonMemberPlaces, existingSids, newSids) } val nonMembers = nonMemberList.map { nonMember -> @@ -298,7 +276,7 @@ class NonMemberService( private fun handleBookmarkUrls( bookmarkUrls: List - ): BookmarkData { + ): Set { val bookmarks = mutableListOf() val allSids = mutableSetOf() @@ -326,12 +304,12 @@ class NonMemberService( val bookmarksToAdd = bookmarks.filterNot { it.sid in existingSids } bookmarkRepository.saveAll(bookmarksToAdd) - return BookmarkData(bookmarks, allSids) + return allSids } private fun handleStoreUrls( storeUrls: List - ): BookmarkData { + ): Set { val bookmarks = mutableListOf() val allSids = mutableSetOf() @@ -358,20 +336,63 @@ class NonMemberService( val bookmarksToAdd = bookmarks.filterNot { it.sid in existingSids } bookmarkRepository.saveAll(bookmarksToAdd) - return BookmarkData(bookmarks, allSids) + return allSids } - private fun updateNonMemberPlaces(nonMember: NonMemberDomain, nonMemberPlaces: List, newSids: Set, existingSids: Set) { - val sidsToAdd = newSids - existingSids - val sidsToDelete = existingSids - newSids + private fun updateNonMemberPlacesIfNeeded(nonMember: NonMemberDomain, nonMemberPlaces: List, existingSids: Set, newSids: Set) { + if(existingSids != newSids) { + val sidsToAdd = newSids - existingSids + val sidsToDelete = existingSids - newSids - val placesToAdd = sidsToAdd.map { sid -> NonMemberPlaceDomain.of(nonMember, sid) } - nonMemberPlaceRepository.saveAll(placesToAdd) + val placesToAdd = sidsToAdd.map { sid -> NonMemberPlaceDomain.of(nonMember, sid) } + nonMemberPlaceRepository.saveAll(placesToAdd) - val placesIdToDelete = nonMemberPlaces - .filter { it.nonMember == nonMember && it.sid in sidsToDelete } - .map { it.id } - nonMemberPlaceRepository.deleteAll(placesIdToDelete) + val placesIdToDelete = nonMemberPlaces + .filter { it.nonMember == nonMember && it.sid in sidsToDelete } + .map { it.id } + nonMemberPlaceRepository.deleteAll(placesIdToDelete) + } + } + + private fun updateBookmarkUrls(nonMember: NonMemberDomain, bookmarkUrls: List) { + val existingBookmarks = nonMemberBookmarkUrlRepository.findAllByNonMemberId(nonMember.id) + + val (urlsToAdd, idsToDelete) = findUrlsToUpdate( + existingUrls = existingBookmarks, + newUrls = bookmarkUrls, + getUrl = { it.bookmarkUrl }, + getId = { it.id } + ) + + nonMemberBookmarkUrlRepository.saveAll(NonMemberBookmarkUrlDomain.of(nonMember, urlsToAdd)) + nonMemberBookmarkUrlRepository.deleteAllByIds(idsToDelete) + } + + private fun updateStoreUrls(nonMember: NonMemberDomain, storeUrls: List) { + val existingStoreUrls = nonMemberStoreUrlRepository.findAllByNonMemberId(nonMember.id) + + val (urlsToAdd, idsToDelete) = findUrlsToUpdate( + existingUrls = existingStoreUrls, + newUrls = storeUrls, + getUrl = { it.storeUrl }, + getId = { it.id } + ) + + nonMemberStoreUrlRepository.saveAll(NonMemberStoreUrlDomain.of(nonMember, urlsToAdd)) + nonMemberStoreUrlRepository.deleteAllByIds(idsToDelete) + } + + private fun findUrlsToUpdate( + existingUrls: List, + newUrls: List, + getUrl: (T) -> String, + getId: (T) -> Long + ): Pair, List> { + val existingUrlSet = existingUrls.map { getUrl(it) }.toSet() + val urlsToAdd = newUrls.filter { it !in existingUrlSet } + val idsToDelete = existingUrls.filter { getUrl(it) !in newUrls }.map { getId(it) } + + return Pair(urlsToAdd, idsToDelete) } private fun calculateIconLevel(index: Int, overlapCount: Int): Int { @@ -388,11 +409,6 @@ class NonMemberService( } } - private data class BookmarkData( - val bookmarks: List, - val sids: Set - ) - private fun nonMembersToNonMemberPlacesMap(nonMembers: List): Map>>> { //list>> val allNonMemberPlaces = nonMembers.flatMap { nonMember -> From d34627a8464bb66a776023e8d45cd8ab187f54fb Mon Sep 17 00:00:00 2001 From: codrin2 Date: Sun, 3 Nov 2024 21:07:01 +0900 Subject: [PATCH 088/203] =?UTF-8?q?refactor(NonMemberService):=20updateNon?= =?UTF-8?q?MemberPlacesIfNeeded=20=EB=A9=94=EC=86=8C=EB=93=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/nonmember/NonMemberService.kt | 86 +++++++++---------- 1 file changed, 41 insertions(+), 45 deletions(-) diff --git a/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberService.kt b/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberService.kt index c2addeb..7e675e7 100644 --- a/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberService.kt +++ b/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberService.kt @@ -174,14 +174,11 @@ class NonMemberService( val nonMember = nonMemberRepository.findById(request.nonMemberId) ?: throw CustomException(ExceptionContent.NON_MEMBER_NOT_FOUND) - val nonMemberPlaces = nonMemberPlaceRepository.findAllByNonMemberId(request.nonMemberId) - val existingSids = nonMemberPlaces.map { it.sid }.toSet() - val bookmarkUrlSids = handleBookmarkUrls(request.bookmarkUrls) val storeUrlSids = handleStoreUrls(request.storeUrls) val newSids = (bookmarkUrlSids + storeUrlSids) - updateNonMemberPlacesIfNeeded(nonMember, nonMemberPlaces, existingSids, newSids) + updateNonMemberPlacesIfNeeded(nonMember, newSids) updateBookmarkUrls(nonMember, request.bookmarkUrls) updateStoreUrls(nonMember, request.storeUrls) } @@ -194,9 +191,6 @@ class NonMemberService( val nonMemberList = nonMemberRepository.findAllByShareUrl(shareUrl.id) nonMemberList.forEach { nonMember -> - val nonMemberPlaces = nonMemberPlaceRepository.findAllByNonMemberId(nonMember.id) - val existingSids = nonMemberPlaces.map { it.sid }.toSet() - val existingBookmarkUrls = nonMemberBookmarkUrlRepository.findAllByNonMemberId(nonMember.id).map { it.bookmarkUrl } val existingStoreUrls = nonMemberStoreUrlRepository.findAllByNonMemberId(nonMember.id).map { it.storeUrl } @@ -204,45 +198,10 @@ class NonMemberService( val storeUrlSids = handleStoreUrls(existingStoreUrls) val newSids = (bookmarkUrlSids + storeUrlSids) - updateNonMemberPlacesIfNeeded(nonMember, nonMemberPlaces, existingSids, newSids) + updateNonMemberPlacesIfNeeded(nonMember, newSids) } - val nonMembers = nonMemberList.map { nonMember -> - GetAllNonMemberPings.NonMember( - nonMemberId = nonMember.id, - name = nonMember.name - ) - } - - // 핑 데이터 생성 및 아이콘 레벨 할당 - val nonMemberPlaces = nonMembersToNonMemberPlacesMap(nonMemberList) - val pings = nonMemberPlaces.entries.mapIndexed { index, nonMemberPlace -> - val level = calculateIconLevel(index, nonMemberPlace.key) - - nonMemberPlace.value.map { bookmarkPair -> - GetAllNonMemberPings.Ping( - iconLevel = level, - nonMembers = bookmarkPair.second.map { - GetAllNonMemberPings.NonMember( - nonMemberId = it.id, - name = it.name - ) - }, - url = bookmarkPair.first.url, - placeName = bookmarkPair.first.name, - px = bookmarkPair.first.px, - py = bookmarkPair.first.py, - ) - } - }.flatten() - - return GetAllNonMemberPings.Response( - eventName = shareUrl.eventName, - nonMembers = nonMembers, - px = shareUrl.latitude, - py = shareUrl.longtitude, - pings = pings - ) + return createPingResponse(shareUrl, nonMemberList) } private fun createNonMemberUpdateStatus(newNonMember: NonMemberDomain, shareUrlId: Long) { @@ -339,7 +298,10 @@ class NonMemberService( return allSids } - private fun updateNonMemberPlacesIfNeeded(nonMember: NonMemberDomain, nonMemberPlaces: List, existingSids: Set, newSids: Set) { + private fun updateNonMemberPlacesIfNeeded(nonMember: NonMemberDomain, newSids: Set) { + val nonMemberPlaces = nonMemberPlaceRepository.findAllByNonMemberId(nonMember.id) + val existingSids = nonMemberPlaces.map { it.sid }.toSet() + if(existingSids != newSids) { val sidsToAdd = newSids - existingSids val sidsToDelete = existingSids - newSids @@ -395,6 +357,40 @@ class NonMemberService( return Pair(urlsToAdd, idsToDelete) } + private fun createPingResponse( + shareUrl: ShareUrlDomain, + nonMemberList: List + ): GetAllNonMemberPings.Response { + val nonMembers = nonMemberList.map { nonMember -> + GetAllNonMemberPings.NonMember(nonMemberId = nonMember.id, name = nonMember.name) + } + + val nonMemberPlaces = nonMembersToNonMemberPlacesMap(nonMemberList) + val pings = nonMemberPlaces.entries.flatMapIndexed { index, nonMemberPlace -> + val level = calculateIconLevel(index, nonMemberPlace.key) + nonMemberPlace.value.map { bookmarkPair -> + GetAllNonMemberPings.Ping( + iconLevel = level, + nonMembers = bookmarkPair.second.map { + GetAllNonMemberPings.NonMember(nonMemberId = it.id, name = it.name) + }, + url = bookmarkPair.first.url, + placeName = bookmarkPair.first.name, + px = bookmarkPair.first.px, + py = bookmarkPair.first.py, + ) + } + } + + return GetAllNonMemberPings.Response( + eventName = shareUrl.eventName, + nonMembers = nonMembers, + px = shareUrl.latitude, + py = shareUrl.longtitude, + pings = pings + ) + } + private fun calculateIconLevel(index: Int, overlapCount: Int): Int { val mostOverlappedIconLevel = 4 val secondOverlappedIconLevel = 3 From a55fe4fecd916ef2532786146eb5e23614aed39b Mon Sep 17 00:00:00 2001 From: codrin2 Date: Sun, 3 Nov 2024 21:12:02 +0900 Subject: [PATCH 089/203] =?UTF-8?q?refactor(NonMemberService):=20getAllNon?= =?UTF-8?q?MemberPings=EC=9D=98=20=EC=A4=91=EB=B3=B5=EB=90=9C=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EB=B6=80=EB=B6=84=20=EB=B6=84=EB=A6=AC=EB=90=9C=20?= =?UTF-8?q?=EB=A9=94=EC=86=8C=EB=93=9C=EB=A5=BC=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/nonmember/NonMemberService.kt | 47 +------------------ 1 file changed, 1 insertion(+), 46 deletions(-) diff --git a/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberService.kt b/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberService.kt index 7e675e7..c710bf4 100644 --- a/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberService.kt +++ b/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberService.kt @@ -121,52 +121,7 @@ class NonMemberService( ?: throw CustomException(ExceptionContent.INVALID_SHARE_URL) val nonMemberList = nonMemberRepository.findAllByShareUrl(shareUrl.id) - val nonMembers = nonMemberList.map { nonMember -> - GetAllNonMemberPings.NonMember( - nonMemberId = nonMember.id, - name = nonMember.name - ) - } - - val nonMemberPlaces = nonMembersToNonMemberPlacesMap(nonMemberList) - - val pings = nonMemberPlaces.entries.mapIndexed{ index, nonMemberPlace -> - val mostOverlappedIconLevel = 4 - val secondOverlappedIconLevel = 3 - val thirdOverlappedIconLevel = 2 - val remainderIconLevel = 1 - - val level = when { - nonMemberPlace.key == 1 -> remainderIconLevel //1명일 때 - index == 0 -> mostOverlappedIconLevel - index == 1 -> secondOverlappedIconLevel - index == 2 -> thirdOverlappedIconLevel - else -> remainderIconLevel - } - nonMemberPlace.value.map { bookmarkPair -> - GetAllNonMemberPings.Ping( - iconLevel = level, - nonMembers = bookmarkPair.second.map { - GetAllNonMemberPings.NonMember( - nonMemberId = it.id, - name = it.name - ) - }, - url = bookmarkPair.first.url, - placeName = bookmarkPair.first.name, - px = bookmarkPair.first.px, - py = bookmarkPair.first.py, - ) - } - }.flatten() - - return GetAllNonMemberPings.Response( - eventName = shareUrl.eventName, - nonMembers = nonMembers, - px = shareUrl.latitude, - py = shareUrl.longtitude, - pings = pings - ) + return createPingResponse(shareUrl, nonMemberList) } @Transactional From 170c841342a1ba8ed485e493955515ebf0a6021e Mon Sep 17 00:00:00 2001 From: sominyun Date: Sun, 3 Nov 2024 21:33:07 +0900 Subject: [PATCH 090/203] =?UTF-8?q?docs:#36=20readme=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/README.md b/README.md index b769e49..da6a555 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,16 @@ +# API 명세서 +[moping API 명세서 다운로드](https://github.com/user-attachments/files/17610402/Moping-Backend.API.Docs.pdf) + +# ERD +### MySQL +image + +### MongoDB +image + +# 시스템 아키텍처 +![image](https://github.com/user-attachments/assets/45b25ea3-e464-49ed-9c1d-be21ce9d1e63) + # 👍 공통 사항 - 단위 테스트 작성(service 메소드 별로) : Kotest 사용 From 9d2a3a53855cd38916d0a4c0a1a6e823d71a4fe8 Mon Sep 17 00:00:00 2001 From: codrin2 Date: Sun, 3 Nov 2024 21:38:55 +0900 Subject: [PATCH 091/203] =?UTF-8?q?refactor(NonMemberService):=20createNon?= =?UTF-8?q?MemberPings=EC=9D=98=20=EC=A4=91=EB=B3=B5=EB=90=9C=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EB=B6=80=EB=B6=84=20=EB=B6=84=EB=A6=AC=EB=90=9C=20?= =?UTF-8?q?=EB=A9=94=EC=86=8C=EB=93=9C=EB=A5=BC=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/nonmember/NonMemberService.kt | 61 +++---------------- .../com/ping/common/util/ValidationUtil.kt | 3 +- 2 files changed, 10 insertions(+), 54 deletions(-) diff --git a/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberService.kt b/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberService.kt index c710bf4..7feaeba 100644 --- a/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberService.kt +++ b/Ping-Application/src/main/kotlin/com/ping/application/nonmember/NonMemberService.kt @@ -45,75 +45,30 @@ class NonMemberService( @Transactional fun createNonMemberPings(request: CreateNonMember.Request) { - //이름 공백, 특수문자, 숫자 불가 ValidationUtil.validateName(request.name) - // 비밀번호 형식 검사 (4자리 숫자) ValidationUtil.validatePassword(request.password) val shareUrl = shareUrlRepository.findByUuid(request.uuid) ?: throw CustomException(ExceptionContent.INVALID_SHARE_URL) - // shareUrlId과 name으로 비회원 존재 여부 확인 nonMemberRepository.findByShareUrlIdAndName(shareUrl.id, request.name)?.let { throw CustomException(ExceptionContent.NON_MEMBER_ALREADY_EXISTS) } - // NonMember 엔티티 생성 및 저장 val nonMemberDomain = NonMemberDomain.of(request.name, request.password, shareUrl) - val nonmember = nonMemberRepository.save(nonMemberDomain) + val savedNonMember = nonMemberRepository.save(nonMemberDomain) - //url 저장 - val nonMemberBookmarkUrlDomains = NonMemberBookmarkUrlDomain.of(nonmember, request.bookmarkUrls) + val nonMemberBookmarkUrlDomains = NonMemberBookmarkUrlDomain.of(savedNonMember, request.bookmarkUrls) + val nonMemberStoreUrlDomains = NonMemberStoreUrlDomain.of(savedNonMember, request.storeUrls) nonMemberBookmarkUrlRepository.saveAll(nonMemberBookmarkUrlDomains) - - val nonMemberStoreUrlDomains = NonMemberStoreUrlDomain.of(nonmember, request.storeUrls) nonMemberStoreUrlRepository.saveAll(nonMemberStoreUrlDomains) - val nonMemberPlaces = mutableListOf() - val bookmarks = mutableListOf() - //맵핀 모은 링크 추출 - bookmarks.addAll( - request.bookmarkUrls.flatMap { - val url = UrlUtil.expandShortUrl(it) - naverMapClient.bookmarkUrlToBookmarkLists(url).bookmarkList.map { bookmark -> - //NonMemberPlace 저장 - nonMemberPlaces - .takeIf { nonMemberPlace -> nonMemberPlace.none { place -> place.sid == bookmark.sid } } - ?.add(NonMemberPlaceDomain.of(nonmember, bookmark.sid)) - BookmarkDomain( - name = bookmark.name, - px = bookmark.px, - py = bookmark.py, - sid = bookmark.sid, - address = bookmark.address, - mcidName = bookmark.mcidName, - url = "https://map.naver.com/p/entry/place/${bookmark.sid}" - ) - } - }) - //맵핀 가게 링크 추출 - bookmarks.addAll( - request.storeUrls.map { - val url = UrlUtil.expandShortUrl(it) - val bookmark = naverMapClient.storeUrlToBookmark(url) - //NonMemberPlace 저장 - nonMemberPlaces - .takeIf { nonMemberPlace -> nonMemberPlace.none { place -> place.sid == bookmark.sid } } - ?.add(NonMemberPlaceDomain.of(nonmember, bookmark.sid)) - BookmarkDomain( - name = bookmark.name, - px = bookmark.px, - py = bookmark.py, - sid = bookmark.sid, - address = bookmark.address, - mcidName = bookmark.mcidName, - url = it - ) - }) - bookmarkRepository.saveAll(bookmarks) - nonMemberPlaceRepository.saveAll(nonMemberPlaces) + val bookmarkSids = handleBookmarkUrls(request.bookmarkUrls) + val storeSids = handleStoreUrls(request.storeUrls) + val allSids = bookmarkSids + storeSids - createNonMemberUpdateStatus(nonmember, shareUrl.id) + val nonMemberPlaces = allSids.map { sid -> NonMemberPlaceDomain.of(savedNonMember, sid) } + nonMemberPlaceRepository.saveAll(nonMemberPlaces) } fun getAllNonMemberPings(uuid: String): GetAllNonMemberPings.Response { diff --git a/Ping-Common/src/main/kotlin/com/ping/common/util/ValidationUtil.kt b/Ping-Common/src/main/kotlin/com/ping/common/util/ValidationUtil.kt index e275b4b..7b26154 100644 --- a/Ping-Common/src/main/kotlin/com/ping/common/util/ValidationUtil.kt +++ b/Ping-Common/src/main/kotlin/com/ping/common/util/ValidationUtil.kt @@ -4,6 +4,7 @@ import com.ping.common.exception.CustomException import com.ping.common.exception.ExceptionContent object ValidationUtil { + //이름 공백, 특수문자, 숫자 불가 fun validateName(name: String) { val namePattern = "^[가-힣a-zA-Z]{1,6}\$".toRegex() if (!namePattern.matches(name)) { @@ -11,7 +12,7 @@ object ValidationUtil { } } - // 비밀번호 유효성 검증 + // 비밀번호 형식 검사 (4자리 숫자) fun validatePassword(password: String) { if (!password.matches(Regex("\\d{4}"))) { throw CustomException(ExceptionContent.INVALID_PASSWORD_FORMAT) From cb4216f3e1f61987058760dbf05f8ab8b5f6b8b4 Mon Sep 17 00:00:00 2001 From: Hee Sang <118061713+codrin2@users.noreply.github.com> Date: Sun, 3 Nov 2024 22:40:40 +0900 Subject: [PATCH 092/203] =?UTF-8?q?docs(README):=20=EC=8B=9C=EC=8A=A4?= =?UTF-8?q?=ED=85=9C=20=EC=95=84=ED=82=A4=ED=85=8D=EC=B2=98=20=EC=82=AC?= =?UTF-8?q?=EC=A7=84=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index da6a555..d95c1df 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ image # 시스템 아키텍처 -![image](https://github.com/user-attachments/assets/45b25ea3-e464-49ed-9c1d-be21ce9d1e63) +![image](https://github.com/user-attachments/assets/8c64a505-22b4-466b-a527-5d4092854e95) # 👍 공통 사항 From 07fdf58c5e265db7e697d2c5d7fe2441ffff1055 Mon Sep 17 00:00:00 2001 From: sominyun Date: Sun, 3 Nov 2024 22:51:45 +0900 Subject: [PATCH 093/203] =?UTF-8?q?feat:=20mongodb=20atlas=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Ping-Api/src/main/resources/application-prod.yaml | 2 +- .../ping/infra/nonmember/domain/mongo/entity/BookmarkEntity.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Ping-Api/src/main/resources/application-prod.yaml b/Ping-Api/src/main/resources/application-prod.yaml index 115aef9..35b5a2d 100644 --- a/Ping-Api/src/main/resources/application-prod.yaml +++ b/Ping-Api/src/main/resources/application-prod.yaml @@ -19,7 +19,7 @@ spring: data: mongodb: - uri: mongodb://${MONGODB_USER}:${MONGODB_PASSWORD}@${MONGODB_HOST}:${MONGODB_PORT}/${MONGODB_SCHEMA}?authSource=admin + uri: mongodb+srv://${MONGODB_USER}:${MONGODB_PASSWORD}@${MONGODB_HOST}/${MONGODB_SCHEMA}?retryWrites=true&w=majority&appName=${MONGODB_SCHEMA} jpa: database-platform: org.hibernate.dialect.MySQL8Dialect diff --git a/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/mongo/entity/BookmarkEntity.kt b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/mongo/entity/BookmarkEntity.kt index 7814e74..ac11462 100644 --- a/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/mongo/entity/BookmarkEntity.kt +++ b/Ping-Infra/src/main/kotlin/com/ping/infra/nonmember/domain/mongo/entity/BookmarkEntity.kt @@ -5,7 +5,7 @@ import org.springframework.data.annotation.Id import org.springframework.data.mongodb.core.index.Indexed import org.springframework.data.mongodb.core.mapping.Document -@Document(collection = "bookmarks") +@Document(collection = "bookmark") data class BookmarkEntity( val name: String, val px: Double, From 9b3e65f2ea835a090888d79f3a560e803052f08c Mon Sep 17 00:00:00 2001 From: sominyun Date: Mon, 4 Nov 2024 18:17:23 +0900 Subject: [PATCH 094/203] =?UTF-8?q?feat:=20swagger=20ui=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ping/api/global/StaticRoutingConfig.kt | 13 ++++++++++++ .../static/swagger/favicon-16x16.png | Bin 0 -> 665 bytes .../static/swagger/favicon-32x32.png | Bin 0 -> 628 bytes .../main/resources/static/swagger/index.css | 16 ++++++++++++++ .../main/resources/static/swagger/index.html | 19 +++++++++++++++++ .../static/swagger/swagger-initializer.js | 20 ++++++++++++++++++ .../static/swagger/swagger-ui-bundle.js | 2 ++ .../static/swagger/swagger-ui-bundle.js.map | 1 + .../swagger/swagger-ui-standalone-preset.js | 2 ++ .../swagger-ui-standalone-preset.js.map | 1 + .../resources/static/swagger/swagger-ui.css | 3 +++ .../static/swagger/swagger-ui.css.map | 1 + 12 files changed, 78 insertions(+) create mode 100644 Ping-Api/src/main/kotlin/com/ping/api/global/StaticRoutingConfig.kt create mode 100644 Ping-Api/src/main/resources/static/swagger/favicon-16x16.png create mode 100644 Ping-Api/src/main/resources/static/swagger/favicon-32x32.png create mode 100644 Ping-Api/src/main/resources/static/swagger/index.css create mode 100644 Ping-Api/src/main/resources/static/swagger/index.html create mode 100644 Ping-Api/src/main/resources/static/swagger/swagger-initializer.js create mode 100644 Ping-Api/src/main/resources/static/swagger/swagger-ui-bundle.js create mode 100644 Ping-Api/src/main/resources/static/swagger/swagger-ui-bundle.js.map create mode 100644 Ping-Api/src/main/resources/static/swagger/swagger-ui-standalone-preset.js create mode 100644 Ping-Api/src/main/resources/static/swagger/swagger-ui-standalone-preset.js.map create mode 100644 Ping-Api/src/main/resources/static/swagger/swagger-ui.css create mode 100644 Ping-Api/src/main/resources/static/swagger/swagger-ui.css.map diff --git a/Ping-Api/src/main/kotlin/com/ping/api/global/StaticRoutingConfig.kt b/Ping-Api/src/main/kotlin/com/ping/api/global/StaticRoutingConfig.kt new file mode 100644 index 0000000..c564b83 --- /dev/null +++ b/Ping-Api/src/main/kotlin/com/ping/api/global/StaticRoutingConfig.kt @@ -0,0 +1,13 @@ +package com.ping.api.global + +import org.springframework.context.annotation.Configuration +import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer + +@Configuration +class StaticRoutingConfig : WebMvcConfigurer { + override fun addResourceHandlers(registry: ResourceHandlerRegistry) { + registry.addResourceHandler("/static/swagger/**").addResourceLocations("classpath:/static/swagger/") + registry.addResourceHandler("swagger-ui.html").addResourceLocations("classpath:/static/swagger/swagger-ui/") + } +} \ No newline at end of file diff --git a/Ping-Api/src/main/resources/static/swagger/favicon-16x16.png b/Ping-Api/src/main/resources/static/swagger/favicon-16x16.png new file mode 100644 index 0000000000000000000000000000000000000000..8b194e617af1c135e6b37939591d24ac3a5efa18 GIT binary patch literal 665 zcmV;K0%rY*P)}JKSduyL>)s!A4EhTMMEM%Q;aL6%l#xiZiF>S;#Y{N2Zz%pvTGHJduXuC6Lx-)0EGfRy*N{Tv4i8@4oJ41gw zKzThrcRe|7J~(YYIBq{SYCkn-KQm=N8$CrEK1CcqMI1dv9z#VRL_{D)L|`QmF8}}l zJ9JV`Q}p!p_4f7m_U`WQ@apR4;o;!mnU<7}iG_qr zF(e)x9~BG-3IzcG2M4an0002kNkl41`ZiN1i62V%{PM@Ry|IS_+Yc7{bb`MM~xm(7p4|kMHP&!VGuDW4kFixat zXw43VmgwEvB$hXt_u=vZ>+v4i7E}n~eG6;n4Z=zF1n?T*yg<;W6kOfxpC6nao>VR% z?fpr=asSJ&`L*wu^rLJ5Peq*PB0;alL#XazZCBxJLd&giTfw@!hW167F^`7kobi;( ze<<>qNlP|xy7S1zl@lZNIBR7#o9ybJsptO#%}P0hz~sBp00000NkvXXu0mjfUsDF? literal 0 HcmV?d00001 diff --git a/Ping-Api/src/main/resources/static/swagger/favicon-32x32.png b/Ping-Api/src/main/resources/static/swagger/favicon-32x32.png new file mode 100644 index 0000000000000000000000000000000000000000..249737fe44558e679f0b67134e274461d988fa98 GIT binary patch literal 628 zcmV-)0*n2LP)Ma*GM0}OV<074bNCP7P7GVd{iMr*I6y~TMLss@FjvgL~HxU z%Vvj33AwpD(Z4*$Mfx=HaU16axM zt2xG_rloN<$iy9j9I5 + + + + + Swagger UI + + + + + + + +
+ + + + + diff --git a/Ping-Api/src/main/resources/static/swagger/swagger-initializer.js b/Ping-Api/src/main/resources/static/swagger/swagger-initializer.js new file mode 100644 index 0000000..bb5346c --- /dev/null +++ b/Ping-Api/src/main/resources/static/swagger/swagger-initializer.js @@ -0,0 +1,20 @@ +window.onload = function() { + // + + // the following lines will be replaced by docker/configurator, when it runs in a docker-container + window.ui = SwaggerUIBundle({ + url: "openapi3.yaml", + dom_id: '#swagger-ui', + deepLinking: true, + presets: [ + SwaggerUIBundle.presets.apis, + SwaggerUIStandalonePreset + ], + plugins: [ + SwaggerUIBundle.plugins.DownloadUrl + ], + layout: "StandaloneLayout" + }); + + // +}; diff --git a/Ping-Api/src/main/resources/static/swagger/swagger-ui-bundle.js b/Ping-Api/src/main/resources/static/swagger/swagger-ui-bundle.js new file mode 100644 index 0000000..551e172 --- /dev/null +++ b/Ping-Api/src/main/resources/static/swagger/swagger-ui-bundle.js @@ -0,0 +1,2 @@ +/*! For license information please see swagger-ui-bundle.js.LICENSE.txt */ +!function webpackUniversalModuleDefinition(o,s){"object"==typeof exports&&"object"==typeof module?module.exports=s():"function"==typeof define&&define.amd?define([],s):"object"==typeof exports?exports.SwaggerUIBundle=s():o.SwaggerUIBundle=s()}(this,(()=>(()=>{var o,s,i={69119:(o,s)=>{"use strict";Object.defineProperty(s,"__esModule",{value:!0}),s.BLANK_URL=s.relativeFirstCharacters=s.whitespaceEscapeCharsRegex=s.urlSchemeRegex=s.ctrlCharactersRegex=s.htmlCtrlEntityRegex=s.htmlEntitiesRegex=s.invalidProtocolRegex=void 0,s.invalidProtocolRegex=/^([^\w]*)(javascript|data|vbscript)/im,s.htmlEntitiesRegex=/&#(\w+)(^\w|;)?/g,s.htmlCtrlEntityRegex=/&(newline|tab);/gi,s.ctrlCharactersRegex=/[\u0000-\u001F\u007F-\u009F\u2000-\u200D\uFEFF]/gim,s.urlSchemeRegex=/^.+(:|:)/gim,s.whitespaceEscapeCharsRegex=/(\\|%5[cC])((%(6[eE]|72|74))|[nrt])/g,s.relativeFirstCharacters=[".","/"],s.BLANK_URL="about:blank"},16750:(o,s,i)=>{"use strict";s.J=void 0;var u=i(69119);function decodeURI(o){try{return decodeURIComponent(o)}catch(s){return o}}s.J=function sanitizeUrl(o){if(!o)return u.BLANK_URL;var s,i,_=decodeURI(o);do{s=(_=decodeURI(_=(i=_,i.replace(u.ctrlCharactersRegex,"").replace(u.htmlEntitiesRegex,(function(o,s){return String.fromCharCode(s)}))).replace(u.htmlCtrlEntityRegex,"").replace(u.ctrlCharactersRegex,"").replace(u.whitespaceEscapeCharsRegex,"").trim())).match(u.ctrlCharactersRegex)||_.match(u.htmlEntitiesRegex)||_.match(u.htmlCtrlEntityRegex)||_.match(u.whitespaceEscapeCharsRegex)}while(s&&s.length>0);var w=_;if(!w)return u.BLANK_URL;if(function isRelativeUrlWithoutProtocol(o){return u.relativeFirstCharacters.indexOf(o[0])>-1}(w))return w;var x=w.match(u.urlSchemeRegex);if(!x)return w;var C=x[0];return u.invalidProtocolRegex.test(C)?u.BLANK_URL:w}},67526:(o,s)=>{"use strict";s.byteLength=function byteLength(o){var s=getLens(o),i=s[0],u=s[1];return 3*(i+u)/4-u},s.toByteArray=function toByteArray(o){var s,i,w=getLens(o),x=w[0],C=w[1],j=new _(function _byteLength(o,s,i){return 3*(s+i)/4-i}(0,x,C)),L=0,B=C>0?x-4:x;for(i=0;i>16&255,j[L++]=s>>8&255,j[L++]=255&s;2===C&&(s=u[o.charCodeAt(i)]<<2|u[o.charCodeAt(i+1)]>>4,j[L++]=255&s);1===C&&(s=u[o.charCodeAt(i)]<<10|u[o.charCodeAt(i+1)]<<4|u[o.charCodeAt(i+2)]>>2,j[L++]=s>>8&255,j[L++]=255&s);return j},s.fromByteArray=function fromByteArray(o){for(var s,u=o.length,_=u%3,w=[],x=16383,C=0,j=u-_;Cj?j:C+x));1===_?(s=o[u-1],w.push(i[s>>2]+i[s<<4&63]+"==")):2===_&&(s=(o[u-2]<<8)+o[u-1],w.push(i[s>>10]+i[s>>4&63]+i[s<<2&63]+"="));return w.join("")};for(var i=[],u=[],_="undefined"!=typeof Uint8Array?Uint8Array:Array,w="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",x=0;x<64;++x)i[x]=w[x],u[w.charCodeAt(x)]=x;function getLens(o){var s=o.length;if(s%4>0)throw new Error("Invalid string. Length must be a multiple of 4");var i=o.indexOf("=");return-1===i&&(i=s),[i,i===s?0:4-i%4]}function encodeChunk(o,s,u){for(var _,w,x=[],C=s;C>18&63]+i[w>>12&63]+i[w>>6&63]+i[63&w]);return x.join("")}u["-".charCodeAt(0)]=62,u["_".charCodeAt(0)]=63},48287:(o,s,i)=>{"use strict";const u=i(67526),_=i(251),w="function"==typeof Symbol&&"function"==typeof Symbol.for?Symbol.for("nodejs.util.inspect.custom"):null;s.Buffer=Buffer,s.SlowBuffer=function SlowBuffer(o){+o!=o&&(o=0);return Buffer.alloc(+o)},s.INSPECT_MAX_BYTES=50;const x=2147483647;function createBuffer(o){if(o>x)throw new RangeError('The value "'+o+'" is invalid for option "size"');const s=new Uint8Array(o);return Object.setPrototypeOf(s,Buffer.prototype),s}function Buffer(o,s,i){if("number"==typeof o){if("string"==typeof s)throw new TypeError('The "string" argument must be of type string. Received type number');return allocUnsafe(o)}return from(o,s,i)}function from(o,s,i){if("string"==typeof o)return function fromString(o,s){"string"==typeof s&&""!==s||(s="utf8");if(!Buffer.isEncoding(s))throw new TypeError("Unknown encoding: "+s);const i=0|byteLength(o,s);let u=createBuffer(i);const _=u.write(o,s);_!==i&&(u=u.slice(0,_));return u}(o,s);if(ArrayBuffer.isView(o))return function fromArrayView(o){if(isInstance(o,Uint8Array)){const s=new Uint8Array(o);return fromArrayBuffer(s.buffer,s.byteOffset,s.byteLength)}return fromArrayLike(o)}(o);if(null==o)throw new TypeError("The first argument must be one of type string, Buffer, ArrayBuffer, Array, or Array-like Object. Received type "+typeof o);if(isInstance(o,ArrayBuffer)||o&&isInstance(o.buffer,ArrayBuffer))return fromArrayBuffer(o,s,i);if("undefined"!=typeof SharedArrayBuffer&&(isInstance(o,SharedArrayBuffer)||o&&isInstance(o.buffer,SharedArrayBuffer)))return fromArrayBuffer(o,s,i);if("number"==typeof o)throw new TypeError('The "value" argument must not be of type number. Received type number');const u=o.valueOf&&o.valueOf();if(null!=u&&u!==o)return Buffer.from(u,s,i);const _=function fromObject(o){if(Buffer.isBuffer(o)){const s=0|checked(o.length),i=createBuffer(s);return 0===i.length||o.copy(i,0,0,s),i}if(void 0!==o.length)return"number"!=typeof o.length||numberIsNaN(o.length)?createBuffer(0):fromArrayLike(o);if("Buffer"===o.type&&Array.isArray(o.data))return fromArrayLike(o.data)}(o);if(_)return _;if("undefined"!=typeof Symbol&&null!=Symbol.toPrimitive&&"function"==typeof o[Symbol.toPrimitive])return Buffer.from(o[Symbol.toPrimitive]("string"),s,i);throw new TypeError("The first argument must be one of type string, Buffer, ArrayBuffer, Array, or Array-like Object. Received type "+typeof o)}function assertSize(o){if("number"!=typeof o)throw new TypeError('"size" argument must be of type number');if(o<0)throw new RangeError('The value "'+o+'" is invalid for option "size"')}function allocUnsafe(o){return assertSize(o),createBuffer(o<0?0:0|checked(o))}function fromArrayLike(o){const s=o.length<0?0:0|checked(o.length),i=createBuffer(s);for(let u=0;u=x)throw new RangeError("Attempt to allocate Buffer larger than maximum size: 0x"+x.toString(16)+" bytes");return 0|o}function byteLength(o,s){if(Buffer.isBuffer(o))return o.length;if(ArrayBuffer.isView(o)||isInstance(o,ArrayBuffer))return o.byteLength;if("string"!=typeof o)throw new TypeError('The "string" argument must be one of type string, Buffer, or ArrayBuffer. Received type '+typeof o);const i=o.length,u=arguments.length>2&&!0===arguments[2];if(!u&&0===i)return 0;let _=!1;for(;;)switch(s){case"ascii":case"latin1":case"binary":return i;case"utf8":case"utf-8":return utf8ToBytes(o).length;case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return 2*i;case"hex":return i>>>1;case"base64":return base64ToBytes(o).length;default:if(_)return u?-1:utf8ToBytes(o).length;s=(""+s).toLowerCase(),_=!0}}function slowToString(o,s,i){let u=!1;if((void 0===s||s<0)&&(s=0),s>this.length)return"";if((void 0===i||i>this.length)&&(i=this.length),i<=0)return"";if((i>>>=0)<=(s>>>=0))return"";for(o||(o="utf8");;)switch(o){case"hex":return hexSlice(this,s,i);case"utf8":case"utf-8":return utf8Slice(this,s,i);case"ascii":return asciiSlice(this,s,i);case"latin1":case"binary":return latin1Slice(this,s,i);case"base64":return base64Slice(this,s,i);case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return utf16leSlice(this,s,i);default:if(u)throw new TypeError("Unknown encoding: "+o);o=(o+"").toLowerCase(),u=!0}}function swap(o,s,i){const u=o[s];o[s]=o[i],o[i]=u}function bidirectionalIndexOf(o,s,i,u,_){if(0===o.length)return-1;if("string"==typeof i?(u=i,i=0):i>2147483647?i=2147483647:i<-2147483648&&(i=-2147483648),numberIsNaN(i=+i)&&(i=_?0:o.length-1),i<0&&(i=o.length+i),i>=o.length){if(_)return-1;i=o.length-1}else if(i<0){if(!_)return-1;i=0}if("string"==typeof s&&(s=Buffer.from(s,u)),Buffer.isBuffer(s))return 0===s.length?-1:arrayIndexOf(o,s,i,u,_);if("number"==typeof s)return s&=255,"function"==typeof Uint8Array.prototype.indexOf?_?Uint8Array.prototype.indexOf.call(o,s,i):Uint8Array.prototype.lastIndexOf.call(o,s,i):arrayIndexOf(o,[s],i,u,_);throw new TypeError("val must be string, number or Buffer")}function arrayIndexOf(o,s,i,u,_){let w,x=1,C=o.length,j=s.length;if(void 0!==u&&("ucs2"===(u=String(u).toLowerCase())||"ucs-2"===u||"utf16le"===u||"utf-16le"===u)){if(o.length<2||s.length<2)return-1;x=2,C/=2,j/=2,i/=2}function read(o,s){return 1===x?o[s]:o.readUInt16BE(s*x)}if(_){let u=-1;for(w=i;wC&&(i=C-j),w=i;w>=0;w--){let i=!0;for(let u=0;u_&&(u=_):u=_;const w=s.length;let x;for(u>w/2&&(u=w/2),x=0;x>8,_=i%256,w.push(_),w.push(u);return w}(s,o.length-i),o,i,u)}function base64Slice(o,s,i){return 0===s&&i===o.length?u.fromByteArray(o):u.fromByteArray(o.slice(s,i))}function utf8Slice(o,s,i){i=Math.min(o.length,i);const u=[];let _=s;for(;_239?4:s>223?3:s>191?2:1;if(_+x<=i){let i,u,C,j;switch(x){case 1:s<128&&(w=s);break;case 2:i=o[_+1],128==(192&i)&&(j=(31&s)<<6|63&i,j>127&&(w=j));break;case 3:i=o[_+1],u=o[_+2],128==(192&i)&&128==(192&u)&&(j=(15&s)<<12|(63&i)<<6|63&u,j>2047&&(j<55296||j>57343)&&(w=j));break;case 4:i=o[_+1],u=o[_+2],C=o[_+3],128==(192&i)&&128==(192&u)&&128==(192&C)&&(j=(15&s)<<18|(63&i)<<12|(63&u)<<6|63&C,j>65535&&j<1114112&&(w=j))}}null===w?(w=65533,x=1):w>65535&&(w-=65536,u.push(w>>>10&1023|55296),w=56320|1023&w),u.push(w),_+=x}return function decodeCodePointsArray(o){const s=o.length;if(s<=C)return String.fromCharCode.apply(String,o);let i="",u=0;for(;uu.length?(Buffer.isBuffer(s)||(s=Buffer.from(s)),s.copy(u,_)):Uint8Array.prototype.set.call(u,s,_);else{if(!Buffer.isBuffer(s))throw new TypeError('"list" argument must be an Array of Buffers');s.copy(u,_)}_+=s.length}return u},Buffer.byteLength=byteLength,Buffer.prototype._isBuffer=!0,Buffer.prototype.swap16=function swap16(){const o=this.length;if(o%2!=0)throw new RangeError("Buffer size must be a multiple of 16-bits");for(let s=0;si&&(o+=" ... "),""},w&&(Buffer.prototype[w]=Buffer.prototype.inspect),Buffer.prototype.compare=function compare(o,s,i,u,_){if(isInstance(o,Uint8Array)&&(o=Buffer.from(o,o.offset,o.byteLength)),!Buffer.isBuffer(o))throw new TypeError('The "target" argument must be one of type Buffer or Uint8Array. Received type '+typeof o);if(void 0===s&&(s=0),void 0===i&&(i=o?o.length:0),void 0===u&&(u=0),void 0===_&&(_=this.length),s<0||i>o.length||u<0||_>this.length)throw new RangeError("out of range index");if(u>=_&&s>=i)return 0;if(u>=_)return-1;if(s>=i)return 1;if(this===o)return 0;let w=(_>>>=0)-(u>>>=0),x=(i>>>=0)-(s>>>=0);const C=Math.min(w,x),j=this.slice(u,_),L=o.slice(s,i);for(let o=0;o>>=0,isFinite(i)?(i>>>=0,void 0===u&&(u="utf8")):(u=i,i=void 0)}const _=this.length-s;if((void 0===i||i>_)&&(i=_),o.length>0&&(i<0||s<0)||s>this.length)throw new RangeError("Attempt to write outside buffer bounds");u||(u="utf8");let w=!1;for(;;)switch(u){case"hex":return hexWrite(this,o,s,i);case"utf8":case"utf-8":return utf8Write(this,o,s,i);case"ascii":case"latin1":case"binary":return asciiWrite(this,o,s,i);case"base64":return base64Write(this,o,s,i);case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return ucs2Write(this,o,s,i);default:if(w)throw new TypeError("Unknown encoding: "+u);u=(""+u).toLowerCase(),w=!0}},Buffer.prototype.toJSON=function toJSON(){return{type:"Buffer",data:Array.prototype.slice.call(this._arr||this,0)}};const C=4096;function asciiSlice(o,s,i){let u="";i=Math.min(o.length,i);for(let _=s;_u)&&(i=u);let _="";for(let u=s;ui)throw new RangeError("Trying to access beyond buffer length")}function checkInt(o,s,i,u,_,w){if(!Buffer.isBuffer(o))throw new TypeError('"buffer" argument must be a Buffer instance');if(s>_||so.length)throw new RangeError("Index out of range")}function wrtBigUInt64LE(o,s,i,u,_){checkIntBI(s,u,_,o,i,7);let w=Number(s&BigInt(4294967295));o[i++]=w,w>>=8,o[i++]=w,w>>=8,o[i++]=w,w>>=8,o[i++]=w;let x=Number(s>>BigInt(32)&BigInt(4294967295));return o[i++]=x,x>>=8,o[i++]=x,x>>=8,o[i++]=x,x>>=8,o[i++]=x,i}function wrtBigUInt64BE(o,s,i,u,_){checkIntBI(s,u,_,o,i,7);let w=Number(s&BigInt(4294967295));o[i+7]=w,w>>=8,o[i+6]=w,w>>=8,o[i+5]=w,w>>=8,o[i+4]=w;let x=Number(s>>BigInt(32)&BigInt(4294967295));return o[i+3]=x,x>>=8,o[i+2]=x,x>>=8,o[i+1]=x,x>>=8,o[i]=x,i+8}function checkIEEE754(o,s,i,u,_,w){if(i+u>o.length)throw new RangeError("Index out of range");if(i<0)throw new RangeError("Index out of range")}function writeFloat(o,s,i,u,w){return s=+s,i>>>=0,w||checkIEEE754(o,0,i,4),_.write(o,s,i,u,23,4),i+4}function writeDouble(o,s,i,u,w){return s=+s,i>>>=0,w||checkIEEE754(o,0,i,8),_.write(o,s,i,u,52,8),i+8}Buffer.prototype.slice=function slice(o,s){const i=this.length;(o=~~o)<0?(o+=i)<0&&(o=0):o>i&&(o=i),(s=void 0===s?i:~~s)<0?(s+=i)<0&&(s=0):s>i&&(s=i),s>>=0,s>>>=0,i||checkOffset(o,s,this.length);let u=this[o],_=1,w=0;for(;++w>>=0,s>>>=0,i||checkOffset(o,s,this.length);let u=this[o+--s],_=1;for(;s>0&&(_*=256);)u+=this[o+--s]*_;return u},Buffer.prototype.readUint8=Buffer.prototype.readUInt8=function readUInt8(o,s){return o>>>=0,s||checkOffset(o,1,this.length),this[o]},Buffer.prototype.readUint16LE=Buffer.prototype.readUInt16LE=function readUInt16LE(o,s){return o>>>=0,s||checkOffset(o,2,this.length),this[o]|this[o+1]<<8},Buffer.prototype.readUint16BE=Buffer.prototype.readUInt16BE=function readUInt16BE(o,s){return o>>>=0,s||checkOffset(o,2,this.length),this[o]<<8|this[o+1]},Buffer.prototype.readUint32LE=Buffer.prototype.readUInt32LE=function readUInt32LE(o,s){return o>>>=0,s||checkOffset(o,4,this.length),(this[o]|this[o+1]<<8|this[o+2]<<16)+16777216*this[o+3]},Buffer.prototype.readUint32BE=Buffer.prototype.readUInt32BE=function readUInt32BE(o,s){return o>>>=0,s||checkOffset(o,4,this.length),16777216*this[o]+(this[o+1]<<16|this[o+2]<<8|this[o+3])},Buffer.prototype.readBigUInt64LE=defineBigIntMethod((function readBigUInt64LE(o){validateNumber(o>>>=0,"offset");const s=this[o],i=this[o+7];void 0!==s&&void 0!==i||boundsError(o,this.length-8);const u=s+256*this[++o]+65536*this[++o]+this[++o]*2**24,_=this[++o]+256*this[++o]+65536*this[++o]+i*2**24;return BigInt(u)+(BigInt(_)<>>=0,"offset");const s=this[o],i=this[o+7];void 0!==s&&void 0!==i||boundsError(o,this.length-8);const u=s*2**24+65536*this[++o]+256*this[++o]+this[++o],_=this[++o]*2**24+65536*this[++o]+256*this[++o]+i;return(BigInt(u)<>>=0,s>>>=0,i||checkOffset(o,s,this.length);let u=this[o],_=1,w=0;for(;++w=_&&(u-=Math.pow(2,8*s)),u},Buffer.prototype.readIntBE=function readIntBE(o,s,i){o>>>=0,s>>>=0,i||checkOffset(o,s,this.length);let u=s,_=1,w=this[o+--u];for(;u>0&&(_*=256);)w+=this[o+--u]*_;return _*=128,w>=_&&(w-=Math.pow(2,8*s)),w},Buffer.prototype.readInt8=function readInt8(o,s){return o>>>=0,s||checkOffset(o,1,this.length),128&this[o]?-1*(255-this[o]+1):this[o]},Buffer.prototype.readInt16LE=function readInt16LE(o,s){o>>>=0,s||checkOffset(o,2,this.length);const i=this[o]|this[o+1]<<8;return 32768&i?4294901760|i:i},Buffer.prototype.readInt16BE=function readInt16BE(o,s){o>>>=0,s||checkOffset(o,2,this.length);const i=this[o+1]|this[o]<<8;return 32768&i?4294901760|i:i},Buffer.prototype.readInt32LE=function readInt32LE(o,s){return o>>>=0,s||checkOffset(o,4,this.length),this[o]|this[o+1]<<8|this[o+2]<<16|this[o+3]<<24},Buffer.prototype.readInt32BE=function readInt32BE(o,s){return o>>>=0,s||checkOffset(o,4,this.length),this[o]<<24|this[o+1]<<16|this[o+2]<<8|this[o+3]},Buffer.prototype.readBigInt64LE=defineBigIntMethod((function readBigInt64LE(o){validateNumber(o>>>=0,"offset");const s=this[o],i=this[o+7];void 0!==s&&void 0!==i||boundsError(o,this.length-8);const u=this[o+4]+256*this[o+5]+65536*this[o+6]+(i<<24);return(BigInt(u)<>>=0,"offset");const s=this[o],i=this[o+7];void 0!==s&&void 0!==i||boundsError(o,this.length-8);const u=(s<<24)+65536*this[++o]+256*this[++o]+this[++o];return(BigInt(u)<>>=0,s||checkOffset(o,4,this.length),_.read(this,o,!0,23,4)},Buffer.prototype.readFloatBE=function readFloatBE(o,s){return o>>>=0,s||checkOffset(o,4,this.length),_.read(this,o,!1,23,4)},Buffer.prototype.readDoubleLE=function readDoubleLE(o,s){return o>>>=0,s||checkOffset(o,8,this.length),_.read(this,o,!0,52,8)},Buffer.prototype.readDoubleBE=function readDoubleBE(o,s){return o>>>=0,s||checkOffset(o,8,this.length),_.read(this,o,!1,52,8)},Buffer.prototype.writeUintLE=Buffer.prototype.writeUIntLE=function writeUIntLE(o,s,i,u){if(o=+o,s>>>=0,i>>>=0,!u){checkInt(this,o,s,i,Math.pow(2,8*i)-1,0)}let _=1,w=0;for(this[s]=255&o;++w>>=0,i>>>=0,!u){checkInt(this,o,s,i,Math.pow(2,8*i)-1,0)}let _=i-1,w=1;for(this[s+_]=255&o;--_>=0&&(w*=256);)this[s+_]=o/w&255;return s+i},Buffer.prototype.writeUint8=Buffer.prototype.writeUInt8=function writeUInt8(o,s,i){return o=+o,s>>>=0,i||checkInt(this,o,s,1,255,0),this[s]=255&o,s+1},Buffer.prototype.writeUint16LE=Buffer.prototype.writeUInt16LE=function writeUInt16LE(o,s,i){return o=+o,s>>>=0,i||checkInt(this,o,s,2,65535,0),this[s]=255&o,this[s+1]=o>>>8,s+2},Buffer.prototype.writeUint16BE=Buffer.prototype.writeUInt16BE=function writeUInt16BE(o,s,i){return o=+o,s>>>=0,i||checkInt(this,o,s,2,65535,0),this[s]=o>>>8,this[s+1]=255&o,s+2},Buffer.prototype.writeUint32LE=Buffer.prototype.writeUInt32LE=function writeUInt32LE(o,s,i){return o=+o,s>>>=0,i||checkInt(this,o,s,4,4294967295,0),this[s+3]=o>>>24,this[s+2]=o>>>16,this[s+1]=o>>>8,this[s]=255&o,s+4},Buffer.prototype.writeUint32BE=Buffer.prototype.writeUInt32BE=function writeUInt32BE(o,s,i){return o=+o,s>>>=0,i||checkInt(this,o,s,4,4294967295,0),this[s]=o>>>24,this[s+1]=o>>>16,this[s+2]=o>>>8,this[s+3]=255&o,s+4},Buffer.prototype.writeBigUInt64LE=defineBigIntMethod((function writeBigUInt64LE(o,s=0){return wrtBigUInt64LE(this,o,s,BigInt(0),BigInt("0xffffffffffffffff"))})),Buffer.prototype.writeBigUInt64BE=defineBigIntMethod((function writeBigUInt64BE(o,s=0){return wrtBigUInt64BE(this,o,s,BigInt(0),BigInt("0xffffffffffffffff"))})),Buffer.prototype.writeIntLE=function writeIntLE(o,s,i,u){if(o=+o,s>>>=0,!u){const u=Math.pow(2,8*i-1);checkInt(this,o,s,i,u-1,-u)}let _=0,w=1,x=0;for(this[s]=255&o;++_>>=0,!u){const u=Math.pow(2,8*i-1);checkInt(this,o,s,i,u-1,-u)}let _=i-1,w=1,x=0;for(this[s+_]=255&o;--_>=0&&(w*=256);)o<0&&0===x&&0!==this[s+_+1]&&(x=1),this[s+_]=(o/w|0)-x&255;return s+i},Buffer.prototype.writeInt8=function writeInt8(o,s,i){return o=+o,s>>>=0,i||checkInt(this,o,s,1,127,-128),o<0&&(o=255+o+1),this[s]=255&o,s+1},Buffer.prototype.writeInt16LE=function writeInt16LE(o,s,i){return o=+o,s>>>=0,i||checkInt(this,o,s,2,32767,-32768),this[s]=255&o,this[s+1]=o>>>8,s+2},Buffer.prototype.writeInt16BE=function writeInt16BE(o,s,i){return o=+o,s>>>=0,i||checkInt(this,o,s,2,32767,-32768),this[s]=o>>>8,this[s+1]=255&o,s+2},Buffer.prototype.writeInt32LE=function writeInt32LE(o,s,i){return o=+o,s>>>=0,i||checkInt(this,o,s,4,2147483647,-2147483648),this[s]=255&o,this[s+1]=o>>>8,this[s+2]=o>>>16,this[s+3]=o>>>24,s+4},Buffer.prototype.writeInt32BE=function writeInt32BE(o,s,i){return o=+o,s>>>=0,i||checkInt(this,o,s,4,2147483647,-2147483648),o<0&&(o=4294967295+o+1),this[s]=o>>>24,this[s+1]=o>>>16,this[s+2]=o>>>8,this[s+3]=255&o,s+4},Buffer.prototype.writeBigInt64LE=defineBigIntMethod((function writeBigInt64LE(o,s=0){return wrtBigUInt64LE(this,o,s,-BigInt("0x8000000000000000"),BigInt("0x7fffffffffffffff"))})),Buffer.prototype.writeBigInt64BE=defineBigIntMethod((function writeBigInt64BE(o,s=0){return wrtBigUInt64BE(this,o,s,-BigInt("0x8000000000000000"),BigInt("0x7fffffffffffffff"))})),Buffer.prototype.writeFloatLE=function writeFloatLE(o,s,i){return writeFloat(this,o,s,!0,i)},Buffer.prototype.writeFloatBE=function writeFloatBE(o,s,i){return writeFloat(this,o,s,!1,i)},Buffer.prototype.writeDoubleLE=function writeDoubleLE(o,s,i){return writeDouble(this,o,s,!0,i)},Buffer.prototype.writeDoubleBE=function writeDoubleBE(o,s,i){return writeDouble(this,o,s,!1,i)},Buffer.prototype.copy=function copy(o,s,i,u){if(!Buffer.isBuffer(o))throw new TypeError("argument should be a Buffer");if(i||(i=0),u||0===u||(u=this.length),s>=o.length&&(s=o.length),s||(s=0),u>0&&u=this.length)throw new RangeError("Index out of range");if(u<0)throw new RangeError("sourceEnd out of bounds");u>this.length&&(u=this.length),o.length-s>>=0,i=void 0===i?this.length:i>>>0,o||(o=0),"number"==typeof o)for(_=s;_=u+4;i-=3)s=`_${o.slice(i-3,i)}${s}`;return`${o.slice(0,i)}${s}`}function checkIntBI(o,s,i,u,_,w){if(o>i||o3?0===s||s===BigInt(0)?`>= 0${u} and < 2${u} ** ${8*(w+1)}${u}`:`>= -(2${u} ** ${8*(w+1)-1}${u}) and < 2 ** ${8*(w+1)-1}${u}`:`>= ${s}${u} and <= ${i}${u}`,new j.ERR_OUT_OF_RANGE("value",_,o)}!function checkBounds(o,s,i){validateNumber(s,"offset"),void 0!==o[s]&&void 0!==o[s+i]||boundsError(s,o.length-(i+1))}(u,_,w)}function validateNumber(o,s){if("number"!=typeof o)throw new j.ERR_INVALID_ARG_TYPE(s,"number",o)}function boundsError(o,s,i){if(Math.floor(o)!==o)throw validateNumber(o,i),new j.ERR_OUT_OF_RANGE(i||"offset","an integer",o);if(s<0)throw new j.ERR_BUFFER_OUT_OF_BOUNDS;throw new j.ERR_OUT_OF_RANGE(i||"offset",`>= ${i?1:0} and <= ${s}`,o)}E("ERR_BUFFER_OUT_OF_BOUNDS",(function(o){return o?`${o} is outside of buffer bounds`:"Attempt to access memory outside buffer bounds"}),RangeError),E("ERR_INVALID_ARG_TYPE",(function(o,s){return`The "${o}" argument must be of type number. Received type ${typeof s}`}),TypeError),E("ERR_OUT_OF_RANGE",(function(o,s,i){let u=`The value of "${o}" is out of range.`,_=i;return Number.isInteger(i)&&Math.abs(i)>2**32?_=addNumericalSeparator(String(i)):"bigint"==typeof i&&(_=String(i),(i>BigInt(2)**BigInt(32)||i<-(BigInt(2)**BigInt(32)))&&(_=addNumericalSeparator(_)),_+="n"),u+=` It must be ${s}. Received ${_}`,u}),RangeError);const L=/[^+/0-9A-Za-z-_]/g;function utf8ToBytes(o,s){let i;s=s||1/0;const u=o.length;let _=null;const w=[];for(let x=0;x55295&&i<57344){if(!_){if(i>56319){(s-=3)>-1&&w.push(239,191,189);continue}if(x+1===u){(s-=3)>-1&&w.push(239,191,189);continue}_=i;continue}if(i<56320){(s-=3)>-1&&w.push(239,191,189),_=i;continue}i=65536+(_-55296<<10|i-56320)}else _&&(s-=3)>-1&&w.push(239,191,189);if(_=null,i<128){if((s-=1)<0)break;w.push(i)}else if(i<2048){if((s-=2)<0)break;w.push(i>>6|192,63&i|128)}else if(i<65536){if((s-=3)<0)break;w.push(i>>12|224,i>>6&63|128,63&i|128)}else{if(!(i<1114112))throw new Error("Invalid code point");if((s-=4)<0)break;w.push(i>>18|240,i>>12&63|128,i>>6&63|128,63&i|128)}}return w}function base64ToBytes(o){return u.toByteArray(function base64clean(o){if((o=(o=o.split("=")[0]).trim().replace(L,"")).length<2)return"";for(;o.length%4!=0;)o+="=";return o}(o))}function blitBuffer(o,s,i,u){let _;for(_=0;_=s.length||_>=o.length);++_)s[_+i]=o[_];return _}function isInstance(o,s){return o instanceof s||null!=o&&null!=o.constructor&&null!=o.constructor.name&&o.constructor.name===s.name}function numberIsNaN(o){return o!=o}const B=function(){const o="0123456789abcdef",s=new Array(256);for(let i=0;i<16;++i){const u=16*i;for(let _=0;_<16;++_)s[u+_]=o[i]+o[_]}return s}();function defineBigIntMethod(o){return"undefined"==typeof BigInt?BufferBigIntNotDefined:o}function BufferBigIntNotDefined(){throw new Error("BigInt not supported")}},38075:(o,s,i)=>{"use strict";var u=i(70453),_=i(10487),w=_(u("String.prototype.indexOf"));o.exports=function callBoundIntrinsic(o,s){var i=u(o,!!s);return"function"==typeof i&&w(o,".prototype.")>-1?_(i):i}},10487:(o,s,i)=>{"use strict";var u=i(66743),_=i(70453),w=i(96897),x=i(69675),C=_("%Function.prototype.apply%"),j=_("%Function.prototype.call%"),L=_("%Reflect.apply%",!0)||u.call(j,C),B=i(30655),$=_("%Math.max%");o.exports=function callBind(o){if("function"!=typeof o)throw new x("a function is required");var s=L(u,j,arguments);return w(s,1+$(0,o.length-(arguments.length-1)),!0)};var V=function applyBind(){return L(u,C,arguments)};B?B(o.exports,"apply",{value:V}):o.exports.apply=V},57427:(o,s)=>{"use strict";s.parse=function parse(o,s){if("string"!=typeof o)throw new TypeError("argument str must be a string");var i={},u=(s||{}).decode||decode,_=0;for(;_{"use strict";var u=i(16426),_={"text/plain":"Text","text/html":"Url",default:"Text"};o.exports=function copy(o,s){var i,w,x,C,j,L,B=!1;s||(s={}),i=s.debug||!1;try{if(x=u(),C=document.createRange(),j=document.getSelection(),(L=document.createElement("span")).textContent=o,L.ariaHidden="true",L.style.all="unset",L.style.position="fixed",L.style.top=0,L.style.clip="rect(0, 0, 0, 0)",L.style.whiteSpace="pre",L.style.webkitUserSelect="text",L.style.MozUserSelect="text",L.style.msUserSelect="text",L.style.userSelect="text",L.addEventListener("copy",(function(u){if(u.stopPropagation(),s.format)if(u.preventDefault(),void 0===u.clipboardData){i&&console.warn("unable to use e.clipboardData"),i&&console.warn("trying IE specific stuff"),window.clipboardData.clearData();var w=_[s.format]||_.default;window.clipboardData.setData(w,o)}else u.clipboardData.clearData(),u.clipboardData.setData(s.format,o);s.onCopy&&(u.preventDefault(),s.onCopy(u.clipboardData))})),document.body.appendChild(L),C.selectNodeContents(L),j.addRange(C),!document.execCommand("copy"))throw new Error("copy command was unsuccessful");B=!0}catch(u){i&&console.error("unable to copy using execCommand: ",u),i&&console.warn("trying IE specific stuff");try{window.clipboardData.setData(s.format||"text",o),s.onCopy&&s.onCopy(window.clipboardData),B=!0}catch(u){i&&console.error("unable to copy using clipboardData: ",u),i&&console.error("falling back to prompt"),w=function format(o){var s=(/mac os x/i.test(navigator.userAgent)?"⌘":"Ctrl")+"+C";return o.replace(/#{\s*key\s*}/g,s)}("message"in s?s.message:"Copy to clipboard: #{key}, Enter"),window.prompt(w,o)}}finally{j&&("function"==typeof j.removeRange?j.removeRange(C):j.removeAllRanges()),L&&document.body.removeChild(L),x()}return B}},2205:function(o,s,i){var u;u=void 0!==i.g?i.g:this,o.exports=function(o){if(o.CSS&&o.CSS.escape)return o.CSS.escape;var cssEscape=function(o){if(0==arguments.length)throw new TypeError("`CSS.escape` requires an argument.");for(var s,i=String(o),u=i.length,_=-1,w="",x=i.charCodeAt(0);++_=1&&s<=31||127==s||0==_&&s>=48&&s<=57||1==_&&s>=48&&s<=57&&45==x?"\\"+s.toString(16)+" ":0==_&&1==u&&45==s||!(s>=128||45==s||95==s||s>=48&&s<=57||s>=65&&s<=90||s>=97&&s<=122)?"\\"+i.charAt(_):i.charAt(_):w+="�";return w};return o.CSS||(o.CSS={}),o.CSS.escape=cssEscape,cssEscape}(u)},81919:(o,s,i)=>{"use strict";var u=i(48287).Buffer;function isSpecificValue(o){return o instanceof u||o instanceof Date||o instanceof RegExp}function cloneSpecificValue(o){if(o instanceof u){var s=u.alloc?u.alloc(o.length):new u(o.length);return o.copy(s),s}if(o instanceof Date)return new Date(o.getTime());if(o instanceof RegExp)return new RegExp(o);throw new Error("Unexpected situation")}function deepCloneArray(o){var s=[];return o.forEach((function(o,i){"object"==typeof o&&null!==o?Array.isArray(o)?s[i]=deepCloneArray(o):isSpecificValue(o)?s[i]=cloneSpecificValue(o):s[i]=_({},o):s[i]=o})),s}function safeGetProperty(o,s){return"__proto__"===s?void 0:o[s]}var _=o.exports=function(){if(arguments.length<1||"object"!=typeof arguments[0])return!1;if(arguments.length<2)return arguments[0];var o,s,i=arguments[0];return Array.prototype.slice.call(arguments,1).forEach((function(u){"object"!=typeof u||null===u||Array.isArray(u)||Object.keys(u).forEach((function(w){return s=safeGetProperty(i,w),(o=safeGetProperty(u,w))===i?void 0:"object"!=typeof o||null===o?void(i[w]=o):Array.isArray(o)?void(i[w]=deepCloneArray(o)):isSpecificValue(o)?void(i[w]=cloneSpecificValue(o)):"object"!=typeof s||null===s||Array.isArray(s)?void(i[w]=_({},o)):void(i[w]=_(s,o))}))})),i}},14744:o=>{"use strict";var s=function isMergeableObject(o){return function isNonNullObject(o){return!!o&&"object"==typeof o}(o)&&!function isSpecial(o){var s=Object.prototype.toString.call(o);return"[object RegExp]"===s||"[object Date]"===s||function isReactElement(o){return o.$$typeof===i}(o)}(o)};var i="function"==typeof Symbol&&Symbol.for?Symbol.for("react.element"):60103;function cloneUnlessOtherwiseSpecified(o,s){return!1!==s.clone&&s.isMergeableObject(o)?deepmerge(function emptyTarget(o){return Array.isArray(o)?[]:{}}(o),o,s):o}function defaultArrayMerge(o,s,i){return o.concat(s).map((function(o){return cloneUnlessOtherwiseSpecified(o,i)}))}function getKeys(o){return Object.keys(o).concat(function getEnumerableOwnPropertySymbols(o){return Object.getOwnPropertySymbols?Object.getOwnPropertySymbols(o).filter((function(s){return Object.propertyIsEnumerable.call(o,s)})):[]}(o))}function propertyIsOnObject(o,s){try{return s in o}catch(o){return!1}}function mergeObject(o,s,i){var u={};return i.isMergeableObject(o)&&getKeys(o).forEach((function(s){u[s]=cloneUnlessOtherwiseSpecified(o[s],i)})),getKeys(s).forEach((function(_){(function propertyIsUnsafe(o,s){return propertyIsOnObject(o,s)&&!(Object.hasOwnProperty.call(o,s)&&Object.propertyIsEnumerable.call(o,s))})(o,_)||(propertyIsOnObject(o,_)&&i.isMergeableObject(s[_])?u[_]=function getMergeFunction(o,s){if(!s.customMerge)return deepmerge;var i=s.customMerge(o);return"function"==typeof i?i:deepmerge}(_,i)(o[_],s[_],i):u[_]=cloneUnlessOtherwiseSpecified(s[_],i))})),u}function deepmerge(o,i,u){(u=u||{}).arrayMerge=u.arrayMerge||defaultArrayMerge,u.isMergeableObject=u.isMergeableObject||s,u.cloneUnlessOtherwiseSpecified=cloneUnlessOtherwiseSpecified;var _=Array.isArray(i);return _===Array.isArray(o)?_?u.arrayMerge(o,i,u):mergeObject(o,i,u):cloneUnlessOtherwiseSpecified(i,u)}deepmerge.all=function deepmergeAll(o,s){if(!Array.isArray(o))throw new Error("first argument should be an array");return o.reduce((function(o,i){return deepmerge(o,i,s)}),{})};var u=deepmerge;o.exports=u},30041:(o,s,i)=>{"use strict";var u=i(30655),_=i(58068),w=i(69675),x=i(75795);o.exports=function defineDataProperty(o,s,i){if(!o||"object"!=typeof o&&"function"!=typeof o)throw new w("`obj` must be an object or a function`");if("string"!=typeof s&&"symbol"!=typeof s)throw new w("`property` must be a string or a symbol`");if(arguments.length>3&&"boolean"!=typeof arguments[3]&&null!==arguments[3])throw new w("`nonEnumerable`, if provided, must be a boolean or null");if(arguments.length>4&&"boolean"!=typeof arguments[4]&&null!==arguments[4])throw new w("`nonWritable`, if provided, must be a boolean or null");if(arguments.length>5&&"boolean"!=typeof arguments[5]&&null!==arguments[5])throw new w("`nonConfigurable`, if provided, must be a boolean or null");if(arguments.length>6&&"boolean"!=typeof arguments[6])throw new w("`loose`, if provided, must be a boolean");var C=arguments.length>3?arguments[3]:null,j=arguments.length>4?arguments[4]:null,L=arguments.length>5?arguments[5]:null,B=arguments.length>6&&arguments[6],$=!!x&&x(o,s);if(u)u(o,s,{configurable:null===L&&$?$.configurable:!L,enumerable:null===C&&$?$.enumerable:!C,value:i,writable:null===j&&$?$.writable:!j});else{if(!B&&(C||j||L))throw new _("This environment does not support defining a property as non-configurable, non-writable, or non-enumerable.");o[s]=i}}},42838:function(o){o.exports=function(){"use strict";const{entries:o,setPrototypeOf:s,isFrozen:i,getPrototypeOf:u,getOwnPropertyDescriptor:_}=Object;let{freeze:w,seal:x,create:C}=Object,{apply:j,construct:L}="undefined"!=typeof Reflect&&Reflect;w||(w=function freeze(o){return o}),x||(x=function seal(o){return o}),j||(j=function apply(o,s,i){return o.apply(s,i)}),L||(L=function construct(o,s){return new o(...s)});const B=unapply(Array.prototype.forEach),$=unapply(Array.prototype.pop),V=unapply(Array.prototype.push),U=unapply(String.prototype.toLowerCase),z=unapply(String.prototype.toString),Y=unapply(String.prototype.match),Z=unapply(String.prototype.replace),ee=unapply(String.prototype.indexOf),ie=unapply(String.prototype.trim),ae=unapply(Object.prototype.hasOwnProperty),ce=unapply(RegExp.prototype.test),le=unconstruct(TypeError);function numberIsNaN(o){return"number"==typeof o&&isNaN(o)}function unapply(o){return function(s){for(var i=arguments.length,u=new Array(i>1?i-1:0),_=1;_2&&void 0!==arguments[2]?arguments[2]:U;s&&s(o,null);let w=u.length;for(;w--;){let s=u[w];if("string"==typeof s){const o=_(s);o!==s&&(i(u)||(u[w]=o),s=o)}o[s]=!0}return o}function cleanArray(o){for(let s=0;s/gm),$e=x(/\${[\w\W]*}/gm),ze=x(/^data-[\-\w.\u00B7-\uFFFF]/),We=x(/^aria-[\-\w]+$/),He=x(/^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i),Ye=x(/^(?:\w+script|data):/i),Xe=x(/[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g),Qe=x(/^html$/i),et=x(/^[a-z][.\w]*(-[.\w]+)+$/i);var tt=Object.freeze({__proto__:null,MUSTACHE_EXPR:Re,ERB_EXPR:qe,TMPLIT_EXPR:$e,DATA_ATTR:ze,ARIA_ATTR:We,IS_ALLOWED_URI:He,IS_SCRIPT_OR_DATA:Ye,ATTR_WHITESPACE:Xe,DOCTYPE_NAME:Qe,CUSTOM_ELEMENT:et});const rt={element:1,attribute:2,text:3,cdataSection:4,entityReference:5,entityNode:6,progressingInstruction:7,comment:8,document:9,documentType:10,documentFragment:11,notation:12},nt=function getGlobal(){return"undefined"==typeof window?null:window},ot=function _createTrustedTypesPolicy(o,s){if("object"!=typeof o||"function"!=typeof o.createPolicy)return null;let i=null;const u="data-tt-policy-suffix";s&&s.hasAttribute(u)&&(i=s.getAttribute(u));const _="dompurify"+(i?"#"+i:"");try{return o.createPolicy(_,{createHTML:o=>o,createScriptURL:o=>o})}catch(o){return console.warn("TrustedTypes policy "+_+" could not be created."),null}};function createDOMPurify(){let s=arguments.length>0&&void 0!==arguments[0]?arguments[0]:nt();const DOMPurify=o=>createDOMPurify(o);if(DOMPurify.version="3.1.4",DOMPurify.removed=[],!s||!s.document||s.document.nodeType!==rt.document)return DOMPurify.isSupported=!1,DOMPurify;let{document:i}=s;const u=i,_=u.currentScript,{DocumentFragment:x,HTMLTemplateElement:j,Node:L,Element:Re,NodeFilter:qe,NamedNodeMap:$e=s.NamedNodeMap||s.MozNamedAttrMap,HTMLFormElement:ze,DOMParser:We,trustedTypes:Ye}=s,Xe=Re.prototype,et=lookupGetter(Xe,"cloneNode"),st=lookupGetter(Xe,"nextSibling"),it=lookupGetter(Xe,"childNodes"),at=lookupGetter(Xe,"parentNode");if("function"==typeof j){const o=i.createElement("template");o.content&&o.content.ownerDocument&&(i=o.content.ownerDocument)}let ct,lt="";const{implementation:ut,createNodeIterator:pt,createDocumentFragment:ht,getElementsByTagName:dt}=i,{importNode:mt}=u;let gt={};DOMPurify.isSupported="function"==typeof o&&"function"==typeof at&&ut&&void 0!==ut.createHTMLDocument;const{MUSTACHE_EXPR:yt,ERB_EXPR:vt,TMPLIT_EXPR:bt,DATA_ATTR:_t,ARIA_ATTR:Et,IS_SCRIPT_OR_DATA:wt,ATTR_WHITESPACE:St,CUSTOM_ELEMENT:xt}=tt;let{IS_ALLOWED_URI:kt}=tt,Ot=null;const Ct=addToSet({},[...pe,...de,...fe,...be,...we]);let At=null;const jt=addToSet({},[...Se,...xe,...Pe,...Te]);let Pt=Object.seal(C(null,{tagNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},attributeNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},allowCustomizedBuiltInElements:{writable:!0,configurable:!1,enumerable:!0,value:!1}})),It=null,Mt=null,Nt=!0,Tt=!0,Rt=!1,Dt=!0,Lt=!1,Bt=!0,Ft=!1,qt=!1,$t=!1,Vt=!1,Ut=!1,zt=!1,Wt=!0,Kt=!1;const Ht="user-content-";let Jt=!0,Gt=!1,Yt={},Xt=null;const Qt=addToSet({},["annotation-xml","audio","colgroup","desc","foreignobject","head","iframe","math","mi","mn","mo","ms","mtext","noembed","noframes","noscript","plaintext","script","style","svg","template","thead","title","video","xmp"]);let Zt=null;const er=addToSet({},["audio","video","img","source","image","track"]);let tr=null;const rr=addToSet({},["alt","class","for","id","label","name","pattern","placeholder","role","summary","title","value","style","xmlns"]),nr="http://www.w3.org/1998/Math/MathML",sr="http://www.w3.org/2000/svg",ir="http://www.w3.org/1999/xhtml";let ar=ir,cr=!1,lr=null;const ur=addToSet({},[nr,sr,ir],z);let pr=null;const dr=["application/xhtml+xml","text/html"],fr="text/html";let mr=null,gr=null;const yr=255,vr=i.createElement("form"),br=function isRegexOrFunction(o){return o instanceof RegExp||o instanceof Function},_r=function _parseConfig(){let o=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};if(!gr||gr!==o){if(o&&"object"==typeof o||(o={}),o=clone(o),pr=-1===dr.indexOf(o.PARSER_MEDIA_TYPE)?fr:o.PARSER_MEDIA_TYPE,mr="application/xhtml+xml"===pr?z:U,Ot=ae(o,"ALLOWED_TAGS")?addToSet({},o.ALLOWED_TAGS,mr):Ct,At=ae(o,"ALLOWED_ATTR")?addToSet({},o.ALLOWED_ATTR,mr):jt,lr=ae(o,"ALLOWED_NAMESPACES")?addToSet({},o.ALLOWED_NAMESPACES,z):ur,tr=ae(o,"ADD_URI_SAFE_ATTR")?addToSet(clone(rr),o.ADD_URI_SAFE_ATTR,mr):rr,Zt=ae(o,"ADD_DATA_URI_TAGS")?addToSet(clone(er),o.ADD_DATA_URI_TAGS,mr):er,Xt=ae(o,"FORBID_CONTENTS")?addToSet({},o.FORBID_CONTENTS,mr):Qt,It=ae(o,"FORBID_TAGS")?addToSet({},o.FORBID_TAGS,mr):{},Mt=ae(o,"FORBID_ATTR")?addToSet({},o.FORBID_ATTR,mr):{},Yt=!!ae(o,"USE_PROFILES")&&o.USE_PROFILES,Nt=!1!==o.ALLOW_ARIA_ATTR,Tt=!1!==o.ALLOW_DATA_ATTR,Rt=o.ALLOW_UNKNOWN_PROTOCOLS||!1,Dt=!1!==o.ALLOW_SELF_CLOSE_IN_ATTR,Lt=o.SAFE_FOR_TEMPLATES||!1,Bt=!1!==o.SAFE_FOR_XML,Ft=o.WHOLE_DOCUMENT||!1,Vt=o.RETURN_DOM||!1,Ut=o.RETURN_DOM_FRAGMENT||!1,zt=o.RETURN_TRUSTED_TYPE||!1,$t=o.FORCE_BODY||!1,Wt=!1!==o.SANITIZE_DOM,Kt=o.SANITIZE_NAMED_PROPS||!1,Jt=!1!==o.KEEP_CONTENT,Gt=o.IN_PLACE||!1,kt=o.ALLOWED_URI_REGEXP||He,ar=o.NAMESPACE||ir,Pt=o.CUSTOM_ELEMENT_HANDLING||{},o.CUSTOM_ELEMENT_HANDLING&&br(o.CUSTOM_ELEMENT_HANDLING.tagNameCheck)&&(Pt.tagNameCheck=o.CUSTOM_ELEMENT_HANDLING.tagNameCheck),o.CUSTOM_ELEMENT_HANDLING&&br(o.CUSTOM_ELEMENT_HANDLING.attributeNameCheck)&&(Pt.attributeNameCheck=o.CUSTOM_ELEMENT_HANDLING.attributeNameCheck),o.CUSTOM_ELEMENT_HANDLING&&"boolean"==typeof o.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements&&(Pt.allowCustomizedBuiltInElements=o.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements),Lt&&(Tt=!1),Ut&&(Vt=!0),Yt&&(Ot=addToSet({},we),At=[],!0===Yt.html&&(addToSet(Ot,pe),addToSet(At,Se)),!0===Yt.svg&&(addToSet(Ot,de),addToSet(At,xe),addToSet(At,Te)),!0===Yt.svgFilters&&(addToSet(Ot,fe),addToSet(At,xe),addToSet(At,Te)),!0===Yt.mathMl&&(addToSet(Ot,be),addToSet(At,Pe),addToSet(At,Te))),o.ADD_TAGS&&(Ot===Ct&&(Ot=clone(Ot)),addToSet(Ot,o.ADD_TAGS,mr)),o.ADD_ATTR&&(At===jt&&(At=clone(At)),addToSet(At,o.ADD_ATTR,mr)),o.ADD_URI_SAFE_ATTR&&addToSet(tr,o.ADD_URI_SAFE_ATTR,mr),o.FORBID_CONTENTS&&(Xt===Qt&&(Xt=clone(Xt)),addToSet(Xt,o.FORBID_CONTENTS,mr)),Jt&&(Ot["#text"]=!0),Ft&&addToSet(Ot,["html","head","body"]),Ot.table&&(addToSet(Ot,["tbody"]),delete It.tbody),o.TRUSTED_TYPES_POLICY){if("function"!=typeof o.TRUSTED_TYPES_POLICY.createHTML)throw le('TRUSTED_TYPES_POLICY configuration option must provide a "createHTML" hook.');if("function"!=typeof o.TRUSTED_TYPES_POLICY.createScriptURL)throw le('TRUSTED_TYPES_POLICY configuration option must provide a "createScriptURL" hook.');ct=o.TRUSTED_TYPES_POLICY,lt=ct.createHTML("")}else void 0===ct&&(ct=ot(Ye,_)),null!==ct&&"string"==typeof lt&&(lt=ct.createHTML(""));w&&w(o),gr=o}},Er=addToSet({},["mi","mo","mn","ms","mtext"]),wr=addToSet({},["foreignobject","annotation-xml"]),Sr=addToSet({},["title","style","font","a","script"]),xr=addToSet({},[...de,...fe,...ye]),kr=addToSet({},[...be,..._e]),Or=function _checkValidNamespace(o){let s=at(o);s&&s.tagName||(s={namespaceURI:ar,tagName:"template"});const i=U(o.tagName),u=U(s.tagName);return!!lr[o.namespaceURI]&&(o.namespaceURI===sr?s.namespaceURI===ir?"svg"===i:s.namespaceURI===nr?"svg"===i&&("annotation-xml"===u||Er[u]):Boolean(xr[i]):o.namespaceURI===nr?s.namespaceURI===ir?"math"===i:s.namespaceURI===sr?"math"===i&&wr[u]:Boolean(kr[i]):o.namespaceURI===ir?!(s.namespaceURI===sr&&!wr[u])&&!(s.namespaceURI===nr&&!Er[u])&&!kr[i]&&(Sr[i]||!xr[i]):!("application/xhtml+xml"!==pr||!lr[o.namespaceURI]))},Cr=function _forceRemove(o){V(DOMPurify.removed,{element:o});try{o.parentNode.removeChild(o)}catch(s){o.remove()}},Ar=function _removeAttribute(o,s){try{V(DOMPurify.removed,{attribute:s.getAttributeNode(o),from:s})}catch(o){V(DOMPurify.removed,{attribute:null,from:s})}if(s.removeAttribute(o),"is"===o&&!At[o])if(Vt||Ut)try{Cr(s)}catch(o){}else try{s.setAttribute(o,"")}catch(o){}},jr=function _initDocument(o){let s=null,u=null;if($t)o=""+o;else{const s=Y(o,/^[\r\n\t ]+/);u=s&&s[0]}"application/xhtml+xml"===pr&&ar===ir&&(o=''+o+"");const _=ct?ct.createHTML(o):o;if(ar===ir)try{s=(new We).parseFromString(_,pr)}catch(o){}if(!s||!s.documentElement){s=ut.createDocument(ar,"template",null);try{s.documentElement.innerHTML=cr?lt:_}catch(o){}}const w=s.body||s.documentElement;return o&&u&&w.insertBefore(i.createTextNode(u),w.childNodes[0]||null),ar===ir?dt.call(s,Ft?"html":"body")[0]:Ft?s.documentElement:w},Pr=function _createNodeIterator(o){return pt.call(o.ownerDocument||o,o,qe.SHOW_ELEMENT|qe.SHOW_COMMENT|qe.SHOW_TEXT|qe.SHOW_PROCESSING_INSTRUCTION|qe.SHOW_CDATA_SECTION,null)},Ir=function _isClobbered(o){return o instanceof ze&&(void 0!==o.__depth&&"number"!=typeof o.__depth||void 0!==o.__removalCount&&"number"!=typeof o.__removalCount||"string"!=typeof o.nodeName||"string"!=typeof o.textContent||"function"!=typeof o.removeChild||!(o.attributes instanceof $e)||"function"!=typeof o.removeAttribute||"function"!=typeof o.setAttribute||"string"!=typeof o.namespaceURI||"function"!=typeof o.insertBefore||"function"!=typeof o.hasChildNodes)},Mr=function _isNode(o){return"function"==typeof L&&o instanceof L},Nr=function _executeHook(o,s,i){gt[o]&&B(gt[o],(o=>{o.call(DOMPurify,s,i,gr)}))},Tr=function _sanitizeElements(o){let s=null;if(Nr("beforeSanitizeElements",o,null),Ir(o))return Cr(o),!0;const i=mr(o.nodeName);if(Nr("uponSanitizeElement",o,{tagName:i,allowedTags:Ot}),o.hasChildNodes()&&!Mr(o.firstElementChild)&&ce(/<[/\w]/g,o.innerHTML)&&ce(/<[/\w]/g,o.textContent))return Cr(o),!0;if(o.nodeType===rt.progressingInstruction)return Cr(o),!0;if(Bt&&o.nodeType===rt.comment&&ce(/<[/\w]/g,o.data))return Cr(o),!0;if(!Ot[i]||It[i]){if(!It[i]&&Dr(i)){if(Pt.tagNameCheck instanceof RegExp&&ce(Pt.tagNameCheck,i))return!1;if(Pt.tagNameCheck instanceof Function&&Pt.tagNameCheck(i))return!1}if(Jt&&!Xt[i]){const s=at(o)||o.parentNode,i=it(o)||o.childNodes;if(i&&s)for(let u=i.length-1;u>=0;--u){const _=et(i[u],!0);_.__removalCount=(o.__removalCount||0)+1,s.insertBefore(_,st(o))}}return Cr(o),!0}return o instanceof Re&&!Or(o)?(Cr(o),!0):"noscript"!==i&&"noembed"!==i&&"noframes"!==i||!ce(/<\/no(script|embed|frames)/i,o.innerHTML)?(Lt&&o.nodeType===rt.text&&(s=o.textContent,B([yt,vt,bt],(o=>{s=Z(s,o," ")})),o.textContent!==s&&(V(DOMPurify.removed,{element:o.cloneNode()}),o.textContent=s)),Nr("afterSanitizeElements",o,null),!1):(Cr(o),!0)},Rr=function _isValidAttribute(o,s,u){if(Wt&&("id"===s||"name"===s)&&(u in i||u in vr||"__depth"===u||"__removalCount"===u))return!1;if(Tt&&!Mt[s]&&ce(_t,s));else if(Nt&&ce(Et,s));else if(!At[s]||Mt[s]){if(!(Dr(o)&&(Pt.tagNameCheck instanceof RegExp&&ce(Pt.tagNameCheck,o)||Pt.tagNameCheck instanceof Function&&Pt.tagNameCheck(o))&&(Pt.attributeNameCheck instanceof RegExp&&ce(Pt.attributeNameCheck,s)||Pt.attributeNameCheck instanceof Function&&Pt.attributeNameCheck(s))||"is"===s&&Pt.allowCustomizedBuiltInElements&&(Pt.tagNameCheck instanceof RegExp&&ce(Pt.tagNameCheck,u)||Pt.tagNameCheck instanceof Function&&Pt.tagNameCheck(u))))return!1}else if(tr[s]);else if(ce(kt,Z(u,St,"")));else if("src"!==s&&"xlink:href"!==s&&"href"!==s||"script"===o||0!==ee(u,"data:")||!Zt[o])if(Rt&&!ce(wt,Z(u,St,"")));else if(u)return!1;return!0},Dr=function _isBasicCustomElement(o){return"annotation-xml"!==o&&Y(o,xt)},Lr=function _sanitizeAttributes(o){Nr("beforeSanitizeAttributes",o,null);const{attributes:s}=o;if(!s)return;const i={attrName:"",attrValue:"",keepAttr:!0,allowedAttributes:At};let u=s.length;for(;u--;){const _=s[u],{name:w,namespaceURI:x,value:C}=_,j=mr(w);let L="value"===w?C:ie(C);if(i.attrName=j,i.attrValue=L,i.keepAttr=!0,i.forceKeepAttr=void 0,Nr("uponSanitizeAttribute",o,i),L=i.attrValue,i.forceKeepAttr)continue;if(Ar(w,o),!i.keepAttr)continue;if(!Dt&&ce(/\/>/i,L)){Ar(w,o);continue}if(Bt&&ce(/((--!?|])>)|<\/(style|title)/i,L)){Ar(w,o);continue}Lt&&B([yt,vt,bt],(o=>{L=Z(L,o," ")}));const V=mr(o.nodeName);if(Rr(V,j,L)){if(!Kt||"id"!==j&&"name"!==j||(Ar(w,o),L=Ht+L),ct&&"object"==typeof Ye&&"function"==typeof Ye.getAttributeType)if(x);else switch(Ye.getAttributeType(V,j)){case"TrustedHTML":L=ct.createHTML(L);break;case"TrustedScriptURL":L=ct.createScriptURL(L)}try{x?o.setAttributeNS(x,w,L):o.setAttribute(w,L),Ir(o)?Cr(o):$(DOMPurify.removed)}catch(o){}}}Nr("afterSanitizeAttributes",o,null)},Br=function _sanitizeShadowDOM(o){let s=null;const i=Pr(o);for(Nr("beforeSanitizeShadowDOM",o,null);s=i.nextNode();){if(Nr("uponSanitizeShadowNode",s,null),Tr(s))continue;const o=at(s);s.nodeType===rt.element&&(o&&o.__depth?s.__depth=(s.__removalCount||0)+o.__depth+1:s.__depth=1),(s.__depth>=yr||s.__depth<0||numberIsNaN(s.__depth))&&Cr(s),s.content instanceof x&&(s.content.__depth=s.__depth,_sanitizeShadowDOM(s.content)),Lr(s)}Nr("afterSanitizeShadowDOM",o,null)};return DOMPurify.sanitize=function(o){let s=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},i=null,_=null,w=null,C=null;if(cr=!o,cr&&(o="\x3c!--\x3e"),"string"!=typeof o&&!Mr(o)){if("function"!=typeof o.toString)throw le("toString is not a function");if("string"!=typeof(o=o.toString()))throw le("dirty is not a string, aborting")}if(!DOMPurify.isSupported)return o;if(qt||_r(s),DOMPurify.removed=[],"string"==typeof o&&(Gt=!1),Gt){if(o.nodeName){const s=mr(o.nodeName);if(!Ot[s]||It[s])throw le("root node is forbidden and cannot be sanitized in-place")}}else if(o instanceof L)i=jr("\x3c!----\x3e"),_=i.ownerDocument.importNode(o,!0),_.nodeType===rt.element&&"BODY"===_.nodeName||"HTML"===_.nodeName?i=_:i.appendChild(_);else{if(!Vt&&!Lt&&!Ft&&-1===o.indexOf("<"))return ct&&zt?ct.createHTML(o):o;if(i=jr(o),!i)return Vt?null:zt?lt:""}i&&$t&&Cr(i.firstChild);const j=Pr(Gt?o:i);for(;w=j.nextNode();){if(Tr(w))continue;const o=at(w);w.nodeType===rt.element&&(o&&o.__depth?w.__depth=(w.__removalCount||0)+o.__depth+1:w.__depth=1),(w.__depth>=yr||w.__depth<0||numberIsNaN(w.__depth))&&Cr(w),w.content instanceof x&&(w.content.__depth=w.__depth,Br(w.content)),Lr(w)}if(Gt)return o;if(Vt){if(Ut)for(C=ht.call(i.ownerDocument);i.firstChild;)C.appendChild(i.firstChild);else C=i;return(At.shadowroot||At.shadowrootmode)&&(C=mt.call(u,C,!0)),C}let $=Ft?i.outerHTML:i.innerHTML;return Ft&&Ot["!doctype"]&&i.ownerDocument&&i.ownerDocument.doctype&&i.ownerDocument.doctype.name&&ce(Qe,i.ownerDocument.doctype.name)&&($="\n"+$),Lt&&B([yt,vt,bt],(o=>{$=Z($,o," ")})),ct&&zt?ct.createHTML($):$},DOMPurify.setConfig=function(){_r(arguments.length>0&&void 0!==arguments[0]?arguments[0]:{}),qt=!0},DOMPurify.clearConfig=function(){gr=null,qt=!1},DOMPurify.isValidAttribute=function(o,s,i){gr||_r({});const u=mr(o),_=mr(s);return Rr(u,_,i)},DOMPurify.addHook=function(o,s){"function"==typeof s&&(gt[o]=gt[o]||[],V(gt[o],s))},DOMPurify.removeHook=function(o){if(gt[o])return $(gt[o])},DOMPurify.removeHooks=function(o){gt[o]&&(gt[o]=[])},DOMPurify.removeAllHooks=function(){gt={}},DOMPurify}return createDOMPurify()}()},78004:o=>{"use strict";class SubRange{constructor(o,s){this.low=o,this.high=s,this.length=1+s-o}overlaps(o){return!(this.higho.high)}touches(o){return!(this.high+1o.high)}add(o){return new SubRange(Math.min(this.low,o.low),Math.max(this.high,o.high))}subtract(o){return o.low<=this.low&&o.high>=this.high?[]:o.low>this.low&&o.higho+s.length),0)}add(o,s){var _add=o=>{for(var s=0;s{for(var s=0;s{for(var s=0;s{for(var i=s.low;i<=s.high;)o.push(i),i++;return o}),[])}subranges(){return this.ranges.map((o=>({low:o.low,high:o.high,length:1+o.high-o.low})))}}o.exports=DRange},30655:(o,s,i)=>{"use strict";var u=i(70453)("%Object.defineProperty%",!0)||!1;if(u)try{u({},"a",{value:1})}catch(o){u=!1}o.exports=u},41237:o=>{"use strict";o.exports=EvalError},69383:o=>{"use strict";o.exports=Error},79290:o=>{"use strict";o.exports=RangeError},79538:o=>{"use strict";o.exports=ReferenceError},58068:o=>{"use strict";o.exports=SyntaxError},69675:o=>{"use strict";o.exports=TypeError},35345:o=>{"use strict";o.exports=URIError},37007:o=>{"use strict";var s,i="object"==typeof Reflect?Reflect:null,u=i&&"function"==typeof i.apply?i.apply:function ReflectApply(o,s,i){return Function.prototype.apply.call(o,s,i)};s=i&&"function"==typeof i.ownKeys?i.ownKeys:Object.getOwnPropertySymbols?function ReflectOwnKeys(o){return Object.getOwnPropertyNames(o).concat(Object.getOwnPropertySymbols(o))}:function ReflectOwnKeys(o){return Object.getOwnPropertyNames(o)};var _=Number.isNaN||function NumberIsNaN(o){return o!=o};function EventEmitter(){EventEmitter.init.call(this)}o.exports=EventEmitter,o.exports.once=function once(o,s){return new Promise((function(i,u){function errorListener(i){o.removeListener(s,resolver),u(i)}function resolver(){"function"==typeof o.removeListener&&o.removeListener("error",errorListener),i([].slice.call(arguments))}eventTargetAgnosticAddListener(o,s,resolver,{once:!0}),"error"!==s&&function addErrorHandlerIfEventEmitter(o,s,i){"function"==typeof o.on&&eventTargetAgnosticAddListener(o,"error",s,i)}(o,errorListener,{once:!0})}))},EventEmitter.EventEmitter=EventEmitter,EventEmitter.prototype._events=void 0,EventEmitter.prototype._eventsCount=0,EventEmitter.prototype._maxListeners=void 0;var w=10;function checkListener(o){if("function"!=typeof o)throw new TypeError('The "listener" argument must be of type Function. Received type '+typeof o)}function _getMaxListeners(o){return void 0===o._maxListeners?EventEmitter.defaultMaxListeners:o._maxListeners}function _addListener(o,s,i,u){var _,w,x;if(checkListener(i),void 0===(w=o._events)?(w=o._events=Object.create(null),o._eventsCount=0):(void 0!==w.newListener&&(o.emit("newListener",s,i.listener?i.listener:i),w=o._events),x=w[s]),void 0===x)x=w[s]=i,++o._eventsCount;else if("function"==typeof x?x=w[s]=u?[i,x]:[x,i]:u?x.unshift(i):x.push(i),(_=_getMaxListeners(o))>0&&x.length>_&&!x.warned){x.warned=!0;var C=new Error("Possible EventEmitter memory leak detected. "+x.length+" "+String(s)+" listeners added. Use emitter.setMaxListeners() to increase limit");C.name="MaxListenersExceededWarning",C.emitter=o,C.type=s,C.count=x.length,function ProcessEmitWarning(o){console&&console.warn&&console.warn(o)}(C)}return o}function onceWrapper(){if(!this.fired)return this.target.removeListener(this.type,this.wrapFn),this.fired=!0,0===arguments.length?this.listener.call(this.target):this.listener.apply(this.target,arguments)}function _onceWrap(o,s,i){var u={fired:!1,wrapFn:void 0,target:o,type:s,listener:i},_=onceWrapper.bind(u);return _.listener=i,u.wrapFn=_,_}function _listeners(o,s,i){var u=o._events;if(void 0===u)return[];var _=u[s];return void 0===_?[]:"function"==typeof _?i?[_.listener||_]:[_]:i?function unwrapListeners(o){for(var s=new Array(o.length),i=0;i0&&(x=s[0]),x instanceof Error)throw x;var C=new Error("Unhandled error."+(x?" ("+x.message+")":""));throw C.context=x,C}var j=w[o];if(void 0===j)return!1;if("function"==typeof j)u(j,this,s);else{var L=j.length,B=arrayClone(j,L);for(i=0;i=0;w--)if(i[w]===s||i[w].listener===s){x=i[w].listener,_=w;break}if(_<0)return this;0===_?i.shift():function spliceOne(o,s){for(;s+1=0;u--)this.removeListener(o,s[u]);return this},EventEmitter.prototype.listeners=function listeners(o){return _listeners(this,o,!0)},EventEmitter.prototype.rawListeners=function rawListeners(o){return _listeners(this,o,!1)},EventEmitter.listenerCount=function(o,s){return"function"==typeof o.listenerCount?o.listenerCount(s):listenerCount.call(o,s)},EventEmitter.prototype.listenerCount=listenerCount,EventEmitter.prototype.eventNames=function eventNames(){return this._eventsCount>0?s(this._events):[]}},85587:(o,s,i)=>{"use strict";var u=i(26311),_=create(Error);function create(o){return FormattedError.displayName=o.displayName||o.name,FormattedError;function FormattedError(s){return s&&(s=u.apply(null,arguments)),new o(s)}}o.exports=_,_.eval=create(EvalError),_.range=create(RangeError),_.reference=create(ReferenceError),_.syntax=create(SyntaxError),_.type=create(TypeError),_.uri=create(URIError),_.create=create},26311:o=>{!function(){var s;function format(o){for(var s,i,u,_,w=1,x=[].slice.call(arguments),C=0,j=o.length,L="",B=!1,$=!1,nextArg=function(){return x[w++]},slurpNumber=function(){for(var i="";/\d/.test(o[C]);)i+=o[C++],s=o[C];return i.length>0?parseInt(i):null};C{"use strict";var s=Object.prototype.toString,i=Math.max,u=function concatty(o,s){for(var i=[],u=0;u{"use strict";var u=i(89353);o.exports=Function.prototype.bind||u},70453:(o,s,i)=>{"use strict";var u,_=i(69383),w=i(41237),x=i(79290),C=i(79538),j=i(58068),L=i(69675),B=i(35345),$=Function,getEvalledConstructor=function(o){try{return $('"use strict"; return ('+o+").constructor;")()}catch(o){}},V=Object.getOwnPropertyDescriptor;if(V)try{V({},"")}catch(o){V=null}var throwTypeError=function(){throw new L},U=V?function(){try{return throwTypeError}catch(o){try{return V(arguments,"callee").get}catch(o){return throwTypeError}}}():throwTypeError,z=i(64039)(),Y=i(80024)(),Z=Object.getPrototypeOf||(Y?function(o){return o.__proto__}:null),ee={},ie="undefined"!=typeof Uint8Array&&Z?Z(Uint8Array):u,ae={__proto__:null,"%AggregateError%":"undefined"==typeof AggregateError?u:AggregateError,"%Array%":Array,"%ArrayBuffer%":"undefined"==typeof ArrayBuffer?u:ArrayBuffer,"%ArrayIteratorPrototype%":z&&Z?Z([][Symbol.iterator]()):u,"%AsyncFromSyncIteratorPrototype%":u,"%AsyncFunction%":ee,"%AsyncGenerator%":ee,"%AsyncGeneratorFunction%":ee,"%AsyncIteratorPrototype%":ee,"%Atomics%":"undefined"==typeof Atomics?u:Atomics,"%BigInt%":"undefined"==typeof BigInt?u:BigInt,"%BigInt64Array%":"undefined"==typeof BigInt64Array?u:BigInt64Array,"%BigUint64Array%":"undefined"==typeof BigUint64Array?u:BigUint64Array,"%Boolean%":Boolean,"%DataView%":"undefined"==typeof DataView?u:DataView,"%Date%":Date,"%decodeURI%":decodeURI,"%decodeURIComponent%":decodeURIComponent,"%encodeURI%":encodeURI,"%encodeURIComponent%":encodeURIComponent,"%Error%":_,"%eval%":eval,"%EvalError%":w,"%Float32Array%":"undefined"==typeof Float32Array?u:Float32Array,"%Float64Array%":"undefined"==typeof Float64Array?u:Float64Array,"%FinalizationRegistry%":"undefined"==typeof FinalizationRegistry?u:FinalizationRegistry,"%Function%":$,"%GeneratorFunction%":ee,"%Int8Array%":"undefined"==typeof Int8Array?u:Int8Array,"%Int16Array%":"undefined"==typeof Int16Array?u:Int16Array,"%Int32Array%":"undefined"==typeof Int32Array?u:Int32Array,"%isFinite%":isFinite,"%isNaN%":isNaN,"%IteratorPrototype%":z&&Z?Z(Z([][Symbol.iterator]())):u,"%JSON%":"object"==typeof JSON?JSON:u,"%Map%":"undefined"==typeof Map?u:Map,"%MapIteratorPrototype%":"undefined"!=typeof Map&&z&&Z?Z((new Map)[Symbol.iterator]()):u,"%Math%":Math,"%Number%":Number,"%Object%":Object,"%parseFloat%":parseFloat,"%parseInt%":parseInt,"%Promise%":"undefined"==typeof Promise?u:Promise,"%Proxy%":"undefined"==typeof Proxy?u:Proxy,"%RangeError%":x,"%ReferenceError%":C,"%Reflect%":"undefined"==typeof Reflect?u:Reflect,"%RegExp%":RegExp,"%Set%":"undefined"==typeof Set?u:Set,"%SetIteratorPrototype%":"undefined"!=typeof Set&&z&&Z?Z((new Set)[Symbol.iterator]()):u,"%SharedArrayBuffer%":"undefined"==typeof SharedArrayBuffer?u:SharedArrayBuffer,"%String%":String,"%StringIteratorPrototype%":z&&Z?Z(""[Symbol.iterator]()):u,"%Symbol%":z?Symbol:u,"%SyntaxError%":j,"%ThrowTypeError%":U,"%TypedArray%":ie,"%TypeError%":L,"%Uint8Array%":"undefined"==typeof Uint8Array?u:Uint8Array,"%Uint8ClampedArray%":"undefined"==typeof Uint8ClampedArray?u:Uint8ClampedArray,"%Uint16Array%":"undefined"==typeof Uint16Array?u:Uint16Array,"%Uint32Array%":"undefined"==typeof Uint32Array?u:Uint32Array,"%URIError%":B,"%WeakMap%":"undefined"==typeof WeakMap?u:WeakMap,"%WeakRef%":"undefined"==typeof WeakRef?u:WeakRef,"%WeakSet%":"undefined"==typeof WeakSet?u:WeakSet};if(Z)try{null.error}catch(o){var ce=Z(Z(o));ae["%Error.prototype%"]=ce}var le=function doEval(o){var s;if("%AsyncFunction%"===o)s=getEvalledConstructor("async function () {}");else if("%GeneratorFunction%"===o)s=getEvalledConstructor("function* () {}");else if("%AsyncGeneratorFunction%"===o)s=getEvalledConstructor("async function* () {}");else if("%AsyncGenerator%"===o){var i=doEval("%AsyncGeneratorFunction%");i&&(s=i.prototype)}else if("%AsyncIteratorPrototype%"===o){var u=doEval("%AsyncGenerator%");u&&Z&&(s=Z(u.prototype))}return ae[o]=s,s},pe={__proto__:null,"%ArrayBufferPrototype%":["ArrayBuffer","prototype"],"%ArrayPrototype%":["Array","prototype"],"%ArrayProto_entries%":["Array","prototype","entries"],"%ArrayProto_forEach%":["Array","prototype","forEach"],"%ArrayProto_keys%":["Array","prototype","keys"],"%ArrayProto_values%":["Array","prototype","values"],"%AsyncFunctionPrototype%":["AsyncFunction","prototype"],"%AsyncGenerator%":["AsyncGeneratorFunction","prototype"],"%AsyncGeneratorPrototype%":["AsyncGeneratorFunction","prototype","prototype"],"%BooleanPrototype%":["Boolean","prototype"],"%DataViewPrototype%":["DataView","prototype"],"%DatePrototype%":["Date","prototype"],"%ErrorPrototype%":["Error","prototype"],"%EvalErrorPrototype%":["EvalError","prototype"],"%Float32ArrayPrototype%":["Float32Array","prototype"],"%Float64ArrayPrototype%":["Float64Array","prototype"],"%FunctionPrototype%":["Function","prototype"],"%Generator%":["GeneratorFunction","prototype"],"%GeneratorPrototype%":["GeneratorFunction","prototype","prototype"],"%Int8ArrayPrototype%":["Int8Array","prototype"],"%Int16ArrayPrototype%":["Int16Array","prototype"],"%Int32ArrayPrototype%":["Int32Array","prototype"],"%JSONParse%":["JSON","parse"],"%JSONStringify%":["JSON","stringify"],"%MapPrototype%":["Map","prototype"],"%NumberPrototype%":["Number","prototype"],"%ObjectPrototype%":["Object","prototype"],"%ObjProto_toString%":["Object","prototype","toString"],"%ObjProto_valueOf%":["Object","prototype","valueOf"],"%PromisePrototype%":["Promise","prototype"],"%PromiseProto_then%":["Promise","prototype","then"],"%Promise_all%":["Promise","all"],"%Promise_reject%":["Promise","reject"],"%Promise_resolve%":["Promise","resolve"],"%RangeErrorPrototype%":["RangeError","prototype"],"%ReferenceErrorPrototype%":["ReferenceError","prototype"],"%RegExpPrototype%":["RegExp","prototype"],"%SetPrototype%":["Set","prototype"],"%SharedArrayBufferPrototype%":["SharedArrayBuffer","prototype"],"%StringPrototype%":["String","prototype"],"%SymbolPrototype%":["Symbol","prototype"],"%SyntaxErrorPrototype%":["SyntaxError","prototype"],"%TypedArrayPrototype%":["TypedArray","prototype"],"%TypeErrorPrototype%":["TypeError","prototype"],"%Uint8ArrayPrototype%":["Uint8Array","prototype"],"%Uint8ClampedArrayPrototype%":["Uint8ClampedArray","prototype"],"%Uint16ArrayPrototype%":["Uint16Array","prototype"],"%Uint32ArrayPrototype%":["Uint32Array","prototype"],"%URIErrorPrototype%":["URIError","prototype"],"%WeakMapPrototype%":["WeakMap","prototype"],"%WeakSetPrototype%":["WeakSet","prototype"]},de=i(66743),fe=i(9957),ye=de.call(Function.call,Array.prototype.concat),be=de.call(Function.apply,Array.prototype.splice),_e=de.call(Function.call,String.prototype.replace),we=de.call(Function.call,String.prototype.slice),Se=de.call(Function.call,RegExp.prototype.exec),xe=/[^%.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]|(?=(?:\.|\[\])(?:\.|\[\]|%$))/g,Pe=/\\(\\)?/g,Te=function getBaseIntrinsic(o,s){var i,u=o;if(fe(pe,u)&&(u="%"+(i=pe[u])[0]+"%"),fe(ae,u)){var _=ae[u];if(_===ee&&(_=le(u)),void 0===_&&!s)throw new L("intrinsic "+o+" exists, but is not available. Please file an issue!");return{alias:i,name:u,value:_}}throw new j("intrinsic "+o+" does not exist!")};o.exports=function GetIntrinsic(o,s){if("string"!=typeof o||0===o.length)throw new L("intrinsic name must be a non-empty string");if(arguments.length>1&&"boolean"!=typeof s)throw new L('"allowMissing" argument must be a boolean');if(null===Se(/^%?[^%]*%?$/,o))throw new j("`%` may not be present anywhere but at the beginning and end of the intrinsic name");var i=function stringToPath(o){var s=we(o,0,1),i=we(o,-1);if("%"===s&&"%"!==i)throw new j("invalid intrinsic syntax, expected closing `%`");if("%"===i&&"%"!==s)throw new j("invalid intrinsic syntax, expected opening `%`");var u=[];return _e(o,xe,(function(o,s,i,_){u[u.length]=i?_e(_,Pe,"$1"):s||o})),u}(o),u=i.length>0?i[0]:"",_=Te("%"+u+"%",s),w=_.name,x=_.value,C=!1,B=_.alias;B&&(u=B[0],be(i,ye([0,1],B)));for(var $=1,U=!0;$=i.length){var ee=V(x,z);x=(U=!!ee)&&"get"in ee&&!("originalValue"in ee.get)?ee.get:x[z]}else U=fe(x,z),x=x[z];U&&!C&&(ae[w]=x)}}return x}},75795:(o,s,i)=>{"use strict";var u=i(70453)("%Object.getOwnPropertyDescriptor%",!0);if(u)try{u([],"length")}catch(o){u=null}o.exports=u},30592:(o,s,i)=>{"use strict";var u=i(30655),_=function hasPropertyDescriptors(){return!!u};_.hasArrayLengthDefineBug=function hasArrayLengthDefineBug(){if(!u)return null;try{return 1!==u([],"length",{value:1}).length}catch(o){return!0}},o.exports=_},80024:o=>{"use strict";var s={__proto__:null,foo:{}},i=Object;o.exports=function hasProto(){return{__proto__:s}.foo===s.foo&&!(s instanceof i)}},64039:(o,s,i)=>{"use strict";var u="undefined"!=typeof Symbol&&Symbol,_=i(41333);o.exports=function hasNativeSymbols(){return"function"==typeof u&&("function"==typeof Symbol&&("symbol"==typeof u("foo")&&("symbol"==typeof Symbol("bar")&&_())))}},41333:o=>{"use strict";o.exports=function hasSymbols(){if("function"!=typeof Symbol||"function"!=typeof Object.getOwnPropertySymbols)return!1;if("symbol"==typeof Symbol.iterator)return!0;var o={},s=Symbol("test"),i=Object(s);if("string"==typeof s)return!1;if("[object Symbol]"!==Object.prototype.toString.call(s))return!1;if("[object Symbol]"!==Object.prototype.toString.call(i))return!1;for(s in o[s]=42,o)return!1;if("function"==typeof Object.keys&&0!==Object.keys(o).length)return!1;if("function"==typeof Object.getOwnPropertyNames&&0!==Object.getOwnPropertyNames(o).length)return!1;var u=Object.getOwnPropertySymbols(o);if(1!==u.length||u[0]!==s)return!1;if(!Object.prototype.propertyIsEnumerable.call(o,s))return!1;if("function"==typeof Object.getOwnPropertyDescriptor){var _=Object.getOwnPropertyDescriptor(o,s);if(42!==_.value||!0!==_.enumerable)return!1}return!0}},9957:(o,s,i)=>{"use strict";var u=Function.prototype.call,_=Object.prototype.hasOwnProperty,w=i(66743);o.exports=w.call(u,_)},45981:o=>{function deepFreeze(o){return o instanceof Map?o.clear=o.delete=o.set=function(){throw new Error("map is read-only")}:o instanceof Set&&(o.add=o.clear=o.delete=function(){throw new Error("set is read-only")}),Object.freeze(o),Object.getOwnPropertyNames(o).forEach((function(s){var i=o[s];"object"!=typeof i||Object.isFrozen(i)||deepFreeze(i)})),o}var s=deepFreeze,i=deepFreeze;s.default=i;class Response{constructor(o){void 0===o.data&&(o.data={}),this.data=o.data,this.isMatchIgnored=!1}ignoreMatch(){this.isMatchIgnored=!0}}function escapeHTML(o){return o.replace(/&/g,"&").replace(//g,">").replace(/"/g,""").replace(/'/g,"'")}function inherit(o,...s){const i=Object.create(null);for(const s in o)i[s]=o[s];return s.forEach((function(o){for(const s in o)i[s]=o[s]})),i}const emitsWrappingTags=o=>!!o.kind;class HTMLRenderer{constructor(o,s){this.buffer="",this.classPrefix=s.classPrefix,o.walk(this)}addText(o){this.buffer+=escapeHTML(o)}openNode(o){if(!emitsWrappingTags(o))return;let s=o.kind;o.sublanguage||(s=`${this.classPrefix}${s}`),this.span(s)}closeNode(o){emitsWrappingTags(o)&&(this.buffer+="")}value(){return this.buffer}span(o){this.buffer+=``}}class TokenTree{constructor(){this.rootNode={children:[]},this.stack=[this.rootNode]}get top(){return this.stack[this.stack.length-1]}get root(){return this.rootNode}add(o){this.top.children.push(o)}openNode(o){const s={kind:o,children:[]};this.add(s),this.stack.push(s)}closeNode(){if(this.stack.length>1)return this.stack.pop()}closeAllNodes(){for(;this.closeNode(););}toJSON(){return JSON.stringify(this.rootNode,null,4)}walk(o){return this.constructor._walk(o,this.rootNode)}static _walk(o,s){return"string"==typeof s?o.addText(s):s.children&&(o.openNode(s),s.children.forEach((s=>this._walk(o,s))),o.closeNode(s)),o}static _collapse(o){"string"!=typeof o&&o.children&&(o.children.every((o=>"string"==typeof o))?o.children=[o.children.join("")]:o.children.forEach((o=>{TokenTree._collapse(o)})))}}class TokenTreeEmitter extends TokenTree{constructor(o){super(),this.options=o}addKeyword(o,s){""!==o&&(this.openNode(s),this.addText(o),this.closeNode())}addText(o){""!==o&&this.add(o)}addSublanguage(o,s){const i=o.root;i.kind=s,i.sublanguage=!0,this.add(i)}toHTML(){return new HTMLRenderer(this,this.options).value()}finalize(){return!0}}function source(o){return o?"string"==typeof o?o:o.source:null}const u=/\[(?:[^\\\]]|\\.)*\]|\(\??|\\([1-9][0-9]*)|\\./;const _="[a-zA-Z]\\w*",w="[a-zA-Z_]\\w*",x="\\b\\d+(\\.\\d+)?",C="(-?)(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)",j="\\b(0b[01]+)",L={begin:"\\\\[\\s\\S]",relevance:0},B={className:"string",begin:"'",end:"'",illegal:"\\n",contains:[L]},$={className:"string",begin:'"',end:'"',illegal:"\\n",contains:[L]},V={begin:/\b(a|an|the|are|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such|will|you|your|they|like|more)\b/},COMMENT=function(o,s,i={}){const u=inherit({className:"comment",begin:o,end:s,contains:[]},i);return u.contains.push(V),u.contains.push({className:"doctag",begin:"(?:TODO|FIXME|NOTE|BUG|OPTIMIZE|HACK|XXX):",relevance:0}),u},U=COMMENT("//","$"),z=COMMENT("/\\*","\\*/"),Y=COMMENT("#","$"),Z={className:"number",begin:x,relevance:0},ee={className:"number",begin:C,relevance:0},ie={className:"number",begin:j,relevance:0},ae={className:"number",begin:x+"(%|em|ex|ch|rem|vw|vh|vmin|vmax|cm|mm|in|pt|pc|px|deg|grad|rad|turn|s|ms|Hz|kHz|dpi|dpcm|dppx)?",relevance:0},ce={begin:/(?=\/[^/\n]*\/)/,contains:[{className:"regexp",begin:/\//,end:/\/[gimuy]*/,illegal:/\n/,contains:[L,{begin:/\[/,end:/\]/,relevance:0,contains:[L]}]}]},le={className:"title",begin:_,relevance:0},pe={className:"title",begin:w,relevance:0},de={begin:"\\.\\s*"+w,relevance:0};var fe=Object.freeze({__proto__:null,MATCH_NOTHING_RE:/\b\B/,IDENT_RE:_,UNDERSCORE_IDENT_RE:w,NUMBER_RE:x,C_NUMBER_RE:C,BINARY_NUMBER_RE:j,RE_STARTERS_RE:"!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~",SHEBANG:(o={})=>{const s=/^#![ ]*\//;return o.binary&&(o.begin=function concat(...o){return o.map((o=>source(o))).join("")}(s,/.*\b/,o.binary,/\b.*/)),inherit({className:"meta",begin:s,end:/$/,relevance:0,"on:begin":(o,s)=>{0!==o.index&&s.ignoreMatch()}},o)},BACKSLASH_ESCAPE:L,APOS_STRING_MODE:B,QUOTE_STRING_MODE:$,PHRASAL_WORDS_MODE:V,COMMENT,C_LINE_COMMENT_MODE:U,C_BLOCK_COMMENT_MODE:z,HASH_COMMENT_MODE:Y,NUMBER_MODE:Z,C_NUMBER_MODE:ee,BINARY_NUMBER_MODE:ie,CSS_NUMBER_MODE:ae,REGEXP_MODE:ce,TITLE_MODE:le,UNDERSCORE_TITLE_MODE:pe,METHOD_GUARD:de,END_SAME_AS_BEGIN:function(o){return Object.assign(o,{"on:begin":(o,s)=>{s.data._beginMatch=o[1]},"on:end":(o,s)=>{s.data._beginMatch!==o[1]&&s.ignoreMatch()}})}});function skipIfhasPrecedingDot(o,s){"."===o.input[o.index-1]&&s.ignoreMatch()}function beginKeywords(o,s){s&&o.beginKeywords&&(o.begin="\\b("+o.beginKeywords.split(" ").join("|")+")(?!\\.)(?=\\b|\\s)",o.__beforeBegin=skipIfhasPrecedingDot,o.keywords=o.keywords||o.beginKeywords,delete o.beginKeywords,void 0===o.relevance&&(o.relevance=0))}function compileIllegal(o,s){Array.isArray(o.illegal)&&(o.illegal=function either(...o){return"("+o.map((o=>source(o))).join("|")+")"}(...o.illegal))}function compileMatch(o,s){if(o.match){if(o.begin||o.end)throw new Error("begin & end are not supported with match");o.begin=o.match,delete o.match}}function compileRelevance(o,s){void 0===o.relevance&&(o.relevance=1)}const ye=["of","and","for","in","not","or","if","then","parent","list","value"],be="keyword";function compileKeywords(o,s,i=be){const u={};return"string"==typeof o?compileList(i,o.split(" ")):Array.isArray(o)?compileList(i,o):Object.keys(o).forEach((function(i){Object.assign(u,compileKeywords(o[i],s,i))})),u;function compileList(o,i){s&&(i=i.map((o=>o.toLowerCase()))),i.forEach((function(s){const i=s.split("|");u[i[0]]=[o,scoreForKeyword(i[0],i[1])]}))}}function scoreForKeyword(o,s){return s?Number(s):function commonKeyword(o){return ye.includes(o.toLowerCase())}(o)?0:1}function compileLanguage(o,{plugins:s}){function langRe(s,i){return new RegExp(source(s),"m"+(o.case_insensitive?"i":"")+(i?"g":""))}class MultiRegex{constructor(){this.matchIndexes={},this.regexes=[],this.matchAt=1,this.position=0}addRule(o,s){s.position=this.position++,this.matchIndexes[this.matchAt]=s,this.regexes.push([s,o]),this.matchAt+=function countMatchGroups(o){return new RegExp(o.toString()+"|").exec("").length-1}(o)+1}compile(){0===this.regexes.length&&(this.exec=()=>null);const o=this.regexes.map((o=>o[1]));this.matcherRe=langRe(function join(o,s="|"){let i=0;return o.map((o=>{i+=1;const s=i;let _=source(o),w="";for(;_.length>0;){const o=u.exec(_);if(!o){w+=_;break}w+=_.substring(0,o.index),_=_.substring(o.index+o[0].length),"\\"===o[0][0]&&o[1]?w+="\\"+String(Number(o[1])+s):(w+=o[0],"("===o[0]&&i++)}return w})).map((o=>`(${o})`)).join(s)}(o),!0),this.lastIndex=0}exec(o){this.matcherRe.lastIndex=this.lastIndex;const s=this.matcherRe.exec(o);if(!s)return null;const i=s.findIndex(((o,s)=>s>0&&void 0!==o)),u=this.matchIndexes[i];return s.splice(0,i),Object.assign(s,u)}}class ResumableMultiRegex{constructor(){this.rules=[],this.multiRegexes=[],this.count=0,this.lastIndex=0,this.regexIndex=0}getMatcher(o){if(this.multiRegexes[o])return this.multiRegexes[o];const s=new MultiRegex;return this.rules.slice(o).forEach((([o,i])=>s.addRule(o,i))),s.compile(),this.multiRegexes[o]=s,s}resumingScanAtSamePosition(){return 0!==this.regexIndex}considerAll(){this.regexIndex=0}addRule(o,s){this.rules.push([o,s]),"begin"===s.type&&this.count++}exec(o){const s=this.getMatcher(this.regexIndex);s.lastIndex=this.lastIndex;let i=s.exec(o);if(this.resumingScanAtSamePosition())if(i&&i.index===this.lastIndex);else{const s=this.getMatcher(0);s.lastIndex=this.lastIndex+1,i=s.exec(o)}return i&&(this.regexIndex+=i.position+1,this.regexIndex===this.count&&this.considerAll()),i}}if(o.compilerExtensions||(o.compilerExtensions=[]),o.contains&&o.contains.includes("self"))throw new Error("ERR: contains `self` is not supported at the top-level of a language. See documentation.");return o.classNameAliases=inherit(o.classNameAliases||{}),function compileMode(s,i){const u=s;if(s.isCompiled)return u;[compileMatch].forEach((o=>o(s,i))),o.compilerExtensions.forEach((o=>o(s,i))),s.__beforeBegin=null,[beginKeywords,compileIllegal,compileRelevance].forEach((o=>o(s,i))),s.isCompiled=!0;let _=null;if("object"==typeof s.keywords&&(_=s.keywords.$pattern,delete s.keywords.$pattern),s.keywords&&(s.keywords=compileKeywords(s.keywords,o.case_insensitive)),s.lexemes&&_)throw new Error("ERR: Prefer `keywords.$pattern` to `mode.lexemes`, BOTH are not allowed. (see mode reference) ");return _=_||s.lexemes||/\w+/,u.keywordPatternRe=langRe(_,!0),i&&(s.begin||(s.begin=/\B|\b/),u.beginRe=langRe(s.begin),s.endSameAsBegin&&(s.end=s.begin),s.end||s.endsWithParent||(s.end=/\B|\b/),s.end&&(u.endRe=langRe(s.end)),u.terminatorEnd=source(s.end)||"",s.endsWithParent&&i.terminatorEnd&&(u.terminatorEnd+=(s.end?"|":"")+i.terminatorEnd)),s.illegal&&(u.illegalRe=langRe(s.illegal)),s.contains||(s.contains=[]),s.contains=[].concat(...s.contains.map((function(o){return function expandOrCloneMode(o){o.variants&&!o.cachedVariants&&(o.cachedVariants=o.variants.map((function(s){return inherit(o,{variants:null},s)})));if(o.cachedVariants)return o.cachedVariants;if(dependencyOnParent(o))return inherit(o,{starts:o.starts?inherit(o.starts):null});if(Object.isFrozen(o))return inherit(o);return o}("self"===o?s:o)}))),s.contains.forEach((function(o){compileMode(o,u)})),s.starts&&compileMode(s.starts,i),u.matcher=function buildModeRegex(o){const s=new ResumableMultiRegex;return o.contains.forEach((o=>s.addRule(o.begin,{rule:o,type:"begin"}))),o.terminatorEnd&&s.addRule(o.terminatorEnd,{type:"end"}),o.illegal&&s.addRule(o.illegal,{type:"illegal"}),s}(u),u}(o)}function dependencyOnParent(o){return!!o&&(o.endsWithParent||dependencyOnParent(o.starts))}function BuildVuePlugin(o){const s={props:["language","code","autodetect"],data:function(){return{detectedLanguage:"",unknownLanguage:!1}},computed:{className(){return this.unknownLanguage?"":"hljs "+this.detectedLanguage},highlighted(){if(!this.autoDetect&&!o.getLanguage(this.language))return console.warn(`The language "${this.language}" you specified could not be found.`),this.unknownLanguage=!0,escapeHTML(this.code);let s={};return this.autoDetect?(s=o.highlightAuto(this.code),this.detectedLanguage=s.language):(s=o.highlight(this.language,this.code,this.ignoreIllegals),this.detectedLanguage=this.language),s.value},autoDetect(){return!this.language||function hasValueOrEmptyAttribute(o){return Boolean(o||""===o)}(this.autodetect)},ignoreIllegals:()=>!0},render(o){return o("pre",{},[o("code",{class:this.className,domProps:{innerHTML:this.highlighted}})])}};return{Component:s,VuePlugin:{install(o){o.component("highlightjs",s)}}}}const _e={"after:highlightElement":({el:o,result:s,text:i})=>{const u=nodeStream(o);if(!u.length)return;const _=document.createElement("div");_.innerHTML=s.value,s.value=function mergeStreams(o,s,i){let u=0,_="";const w=[];function selectStream(){return o.length&&s.length?o[0].offset!==s[0].offset?o[0].offset"}function close(o){_+=""}function render(o){("start"===o.event?open:close)(o.node)}for(;o.length||s.length;){let s=selectStream();if(_+=escapeHTML(i.substring(u,s[0].offset)),u=s[0].offset,s===o){w.reverse().forEach(close);do{render(s.splice(0,1)[0]),s=selectStream()}while(s===o&&s.length&&s[0].offset===u);w.reverse().forEach(open)}else"start"===s[0].event?w.push(s[0].node):w.pop(),render(s.splice(0,1)[0])}return _+escapeHTML(i.substr(u))}(u,nodeStream(_),i)}};function tag(o){return o.nodeName.toLowerCase()}function nodeStream(o){const s=[];return function _nodeStream(o,i){for(let u=o.firstChild;u;u=u.nextSibling)3===u.nodeType?i+=u.nodeValue.length:1===u.nodeType&&(s.push({event:"start",offset:i,node:u}),i=_nodeStream(u,i),tag(u).match(/br|hr|img|input/)||s.push({event:"stop",offset:i,node:u}));return i}(o,0),s}const we={},error=o=>{console.error(o)},warn=(o,...s)=>{console.log(`WARN: ${o}`,...s)},deprecated=(o,s)=>{we[`${o}/${s}`]||(console.log(`Deprecated as of ${o}. ${s}`),we[`${o}/${s}`]=!0)},Se=escapeHTML,xe=inherit,Pe=Symbol("nomatch");var Te=function(o){const i=Object.create(null),u=Object.create(null),_=[];let w=!0;const x=/(^(<[^>]+>|\t|)+|\n)/gm,C="Could not find the language '{}', did you forget to load/include a language module?",j={disableAutodetect:!0,name:"Plain text",contains:[]};let L={noHighlightRe:/^(no-?highlight)$/i,languageDetectRe:/\blang(?:uage)?-([\w-]+)\b/i,classPrefix:"hljs-",tabReplace:null,useBR:!1,languages:null,__emitter:TokenTreeEmitter};function shouldNotHighlight(o){return L.noHighlightRe.test(o)}function highlight(o,s,i,u){let _="",w="";"object"==typeof s?(_=o,i=s.ignoreIllegals,w=s.language,u=void 0):(deprecated("10.7.0","highlight(lang, code, ...args) has been deprecated."),deprecated("10.7.0","Please use highlight(code, options) instead.\nhttps://github.com/highlightjs/highlight.js/issues/2277"),w=o,_=s);const x={code:_,language:w};fire("before:highlight",x);const C=x.result?x.result:_highlight(x.language,x.code,i,u);return C.code=x.code,fire("after:highlight",C),C}function _highlight(o,s,u,x){function keywordData(o,s){const i=B.case_insensitive?s[0].toLowerCase():s[0];return Object.prototype.hasOwnProperty.call(o.keywords,i)&&o.keywords[i]}function processBuffer(){null!=U.subLanguage?function processSubLanguage(){if(""===Z)return;let o=null;if("string"==typeof U.subLanguage){if(!i[U.subLanguage])return void Y.addText(Z);o=_highlight(U.subLanguage,Z,!0,z[U.subLanguage]),z[U.subLanguage]=o.top}else o=highlightAuto(Z,U.subLanguage.length?U.subLanguage:null);U.relevance>0&&(ee+=o.relevance),Y.addSublanguage(o.emitter,o.language)}():function processKeywords(){if(!U.keywords)return void Y.addText(Z);let o=0;U.keywordPatternRe.lastIndex=0;let s=U.keywordPatternRe.exec(Z),i="";for(;s;){i+=Z.substring(o,s.index);const u=keywordData(U,s);if(u){const[o,_]=u;if(Y.addText(i),i="",ee+=_,o.startsWith("_"))i+=s[0];else{const i=B.classNameAliases[o]||o;Y.addKeyword(s[0],i)}}else i+=s[0];o=U.keywordPatternRe.lastIndex,s=U.keywordPatternRe.exec(Z)}i+=Z.substr(o),Y.addText(i)}(),Z=""}function startNewMode(o){return o.className&&Y.openNode(B.classNameAliases[o.className]||o.className),U=Object.create(o,{parent:{value:U}}),U}function endOfMode(o,s,i){let u=function startsWith(o,s){const i=o&&o.exec(s);return i&&0===i.index}(o.endRe,i);if(u){if(o["on:end"]){const i=new Response(o);o["on:end"](s,i),i.isMatchIgnored&&(u=!1)}if(u){for(;o.endsParent&&o.parent;)o=o.parent;return o}}if(o.endsWithParent)return endOfMode(o.parent,s,i)}function doIgnore(o){return 0===U.matcher.regexIndex?(Z+=o[0],1):(ce=!0,0)}function doBeginMatch(o){const s=o[0],i=o.rule,u=new Response(i),_=[i.__beforeBegin,i["on:begin"]];for(const i of _)if(i&&(i(o,u),u.isMatchIgnored))return doIgnore(s);return i&&i.endSameAsBegin&&(i.endRe=function escape(o){return new RegExp(o.replace(/[-/\\^$*+?.()|[\]{}]/g,"\\$&"),"m")}(s)),i.skip?Z+=s:(i.excludeBegin&&(Z+=s),processBuffer(),i.returnBegin||i.excludeBegin||(Z=s)),startNewMode(i),i.returnBegin?0:s.length}function doEndMatch(o){const i=o[0],u=s.substr(o.index),_=endOfMode(U,o,u);if(!_)return Pe;const w=U;w.skip?Z+=i:(w.returnEnd||w.excludeEnd||(Z+=i),processBuffer(),w.excludeEnd&&(Z=i));do{U.className&&Y.closeNode(),U.skip||U.subLanguage||(ee+=U.relevance),U=U.parent}while(U!==_.parent);return _.starts&&(_.endSameAsBegin&&(_.starts.endRe=_.endRe),startNewMode(_.starts)),w.returnEnd?0:i.length}let j={};function processLexeme(i,_){const x=_&&_[0];if(Z+=i,null==x)return processBuffer(),0;if("begin"===j.type&&"end"===_.type&&j.index===_.index&&""===x){if(Z+=s.slice(_.index,_.index+1),!w){const s=new Error("0 width match regex");throw s.languageName=o,s.badRule=j.rule,s}return 1}if(j=_,"begin"===_.type)return doBeginMatch(_);if("illegal"===_.type&&!u){const o=new Error('Illegal lexeme "'+x+'" for mode "'+(U.className||"")+'"');throw o.mode=U,o}if("end"===_.type){const o=doEndMatch(_);if(o!==Pe)return o}if("illegal"===_.type&&""===x)return 1;if(ae>1e5&&ae>3*_.index){throw new Error("potential infinite loop, way more iterations than matches")}return Z+=x,x.length}const B=getLanguage(o);if(!B)throw error(C.replace("{}",o)),new Error('Unknown language: "'+o+'"');const $=compileLanguage(B,{plugins:_});let V="",U=x||$;const z={},Y=new L.__emitter(L);!function processContinuations(){const o=[];for(let s=U;s!==B;s=s.parent)s.className&&o.unshift(s.className);o.forEach((o=>Y.openNode(o)))}();let Z="",ee=0,ie=0,ae=0,ce=!1;try{for(U.matcher.considerAll();;){ae++,ce?ce=!1:U.matcher.considerAll(),U.matcher.lastIndex=ie;const o=U.matcher.exec(s);if(!o)break;const i=processLexeme(s.substring(ie,o.index),o);ie=o.index+i}return processLexeme(s.substr(ie)),Y.closeAllNodes(),Y.finalize(),V=Y.toHTML(),{relevance:Math.floor(ee),value:V,language:o,illegal:!1,emitter:Y,top:U}}catch(i){if(i.message&&i.message.includes("Illegal"))return{illegal:!0,illegalBy:{msg:i.message,context:s.slice(ie-100,ie+100),mode:i.mode},sofar:V,relevance:0,value:Se(s),emitter:Y};if(w)return{illegal:!1,relevance:0,value:Se(s),emitter:Y,language:o,top:U,errorRaised:i};throw i}}function highlightAuto(o,s){s=s||L.languages||Object.keys(i);const u=function justTextHighlightResult(o){const s={relevance:0,emitter:new L.__emitter(L),value:Se(o),illegal:!1,top:j};return s.emitter.addText(o),s}(o),_=s.filter(getLanguage).filter(autoDetection).map((s=>_highlight(s,o,!1)));_.unshift(u);const w=_.sort(((o,s)=>{if(o.relevance!==s.relevance)return s.relevance-o.relevance;if(o.language&&s.language){if(getLanguage(o.language).supersetOf===s.language)return 1;if(getLanguage(s.language).supersetOf===o.language)return-1}return 0})),[x,C]=w,B=x;return B.second_best=C,B}const B={"before:highlightElement":({el:o})=>{L.useBR&&(o.innerHTML=o.innerHTML.replace(/\n/g,"").replace(//g,"\n"))},"after:highlightElement":({result:o})=>{L.useBR&&(o.value=o.value.replace(/\n/g,"
"))}},$=/^(<[^>]+>|\t)+/gm,V={"after:highlightElement":({result:o})=>{L.tabReplace&&(o.value=o.value.replace($,(o=>o.replace(/\t/g,L.tabReplace))))}};function highlightElement(o){let s=null;const i=function blockLanguage(o){let s=o.className+" ";s+=o.parentNode?o.parentNode.className:"";const i=L.languageDetectRe.exec(s);if(i){const s=getLanguage(i[1]);return s||(warn(C.replace("{}",i[1])),warn("Falling back to no-highlight mode for this block.",o)),s?i[1]:"no-highlight"}return s.split(/\s+/).find((o=>shouldNotHighlight(o)||getLanguage(o)))}(o);if(shouldNotHighlight(i))return;fire("before:highlightElement",{el:o,language:i}),s=o;const _=s.textContent,w=i?highlight(_,{language:i,ignoreIllegals:!0}):highlightAuto(_);fire("after:highlightElement",{el:o,result:w,text:_}),o.innerHTML=w.value,function updateClassName(o,s,i){const _=s?u[s]:i;o.classList.add("hljs"),_&&o.classList.add(_)}(o,i,w.language),o.result={language:w.language,re:w.relevance,relavance:w.relevance},w.second_best&&(o.second_best={language:w.second_best.language,re:w.second_best.relevance,relavance:w.second_best.relevance})}const initHighlighting=()=>{if(initHighlighting.called)return;initHighlighting.called=!0,deprecated("10.6.0","initHighlighting() is deprecated. Use highlightAll() instead.");document.querySelectorAll("pre code").forEach(highlightElement)};let U=!1;function highlightAll(){if("loading"===document.readyState)return void(U=!0);document.querySelectorAll("pre code").forEach(highlightElement)}function getLanguage(o){return o=(o||"").toLowerCase(),i[o]||i[u[o]]}function registerAliases(o,{languageName:s}){"string"==typeof o&&(o=[o]),o.forEach((o=>{u[o.toLowerCase()]=s}))}function autoDetection(o){const s=getLanguage(o);return s&&!s.disableAutodetect}function fire(o,s){const i=o;_.forEach((function(o){o[i]&&o[i](s)}))}"undefined"!=typeof window&&window.addEventListener&&window.addEventListener("DOMContentLoaded",(function boot(){U&&highlightAll()}),!1),Object.assign(o,{highlight,highlightAuto,highlightAll,fixMarkup:function deprecateFixMarkup(o){return deprecated("10.2.0","fixMarkup will be removed entirely in v11.0"),deprecated("10.2.0","Please see https://github.com/highlightjs/highlight.js/issues/2534"),function fixMarkup(o){return L.tabReplace||L.useBR?o.replace(x,(o=>"\n"===o?L.useBR?"
":o:L.tabReplace?o.replace(/\t/g,L.tabReplace):o)):o}(o)},highlightElement,highlightBlock:function deprecateHighlightBlock(o){return deprecated("10.7.0","highlightBlock will be removed entirely in v12.0"),deprecated("10.7.0","Please use highlightElement now."),highlightElement(o)},configure:function configure(o){o.useBR&&(deprecated("10.3.0","'useBR' will be removed entirely in v11.0"),deprecated("10.3.0","Please see https://github.com/highlightjs/highlight.js/issues/2559")),L=xe(L,o)},initHighlighting,initHighlightingOnLoad:function initHighlightingOnLoad(){deprecated("10.6.0","initHighlightingOnLoad() is deprecated. Use highlightAll() instead."),U=!0},registerLanguage:function registerLanguage(s,u){let _=null;try{_=u(o)}catch(o){if(error("Language definition for '{}' could not be registered.".replace("{}",s)),!w)throw o;error(o),_=j}_.name||(_.name=s),i[s]=_,_.rawDefinition=u.bind(null,o),_.aliases&®isterAliases(_.aliases,{languageName:s})},unregisterLanguage:function unregisterLanguage(o){delete i[o];for(const s of Object.keys(u))u[s]===o&&delete u[s]},listLanguages:function listLanguages(){return Object.keys(i)},getLanguage,registerAliases,requireLanguage:function requireLanguage(o){deprecated("10.4.0","requireLanguage will be removed entirely in v11."),deprecated("10.4.0","Please see https://github.com/highlightjs/highlight.js/pull/2844");const s=getLanguage(o);if(s)return s;throw new Error("The '{}' language is required, but not loaded.".replace("{}",o))},autoDetection,inherit:xe,addPlugin:function addPlugin(o){!function upgradePluginAPI(o){o["before:highlightBlock"]&&!o["before:highlightElement"]&&(o["before:highlightElement"]=s=>{o["before:highlightBlock"](Object.assign({block:s.el},s))}),o["after:highlightBlock"]&&!o["after:highlightElement"]&&(o["after:highlightElement"]=s=>{o["after:highlightBlock"](Object.assign({block:s.el},s))})}(o),_.push(o)},vuePlugin:BuildVuePlugin(o).VuePlugin}),o.debugMode=function(){w=!1},o.safeMode=function(){w=!0},o.versionString="10.7.3";for(const o in fe)"object"==typeof fe[o]&&s(fe[o]);return Object.assign(o,fe),o.addPlugin(B),o.addPlugin(_e),o.addPlugin(V),o}({});o.exports=Te},35344:o=>{function concat(...o){return o.map((o=>function source(o){return o?"string"==typeof o?o:o.source:null}(o))).join("")}o.exports=function bash(o){const s={},i={begin:/\$\{/,end:/\}/,contains:["self",{begin:/:-/,contains:[s]}]};Object.assign(s,{className:"variable",variants:[{begin:concat(/\$[\w\d#@][\w\d_]*/,"(?![\\w\\d])(?![$])")},i]});const u={className:"subst",begin:/\$\(/,end:/\)/,contains:[o.BACKSLASH_ESCAPE]},_={begin:/<<-?\s*(?=\w+)/,starts:{contains:[o.END_SAME_AS_BEGIN({begin:/(\w+)/,end:/(\w+)/,className:"string"})]}},w={className:"string",begin:/"/,end:/"/,contains:[o.BACKSLASH_ESCAPE,s,u]};u.contains.push(w);const x={begin:/\$\(\(/,end:/\)\)/,contains:[{begin:/\d+#[0-9a-f]+/,className:"number"},o.NUMBER_MODE,s]},C=o.SHEBANG({binary:`(${["fish","bash","zsh","sh","csh","ksh","tcsh","dash","scsh"].join("|")})`,relevance:10}),j={className:"function",begin:/\w[\w\d_]*\s*\(\s*\)\s*\{/,returnBegin:!0,contains:[o.inherit(o.TITLE_MODE,{begin:/\w[\w\d_]*/})],relevance:0};return{name:"Bash",aliases:["sh","zsh"],keywords:{$pattern:/\b[a-z._-]+\b/,keyword:"if then else elif fi for while in do done case esac function",literal:"true false",built_in:"break cd continue eval exec exit export getopts hash pwd readonly return shift test times trap umask unset alias bind builtin caller command declare echo enable help let local logout mapfile printf read readarray source type typeset ulimit unalias set shopt autoload bg bindkey bye cap chdir clone comparguments compcall compctl compdescribe compfiles compgroups compquote comptags comptry compvalues dirs disable disown echotc echoti emulate fc fg float functions getcap getln history integer jobs kill limit log noglob popd print pushd pushln rehash sched setcap setopt stat suspend ttyctl unfunction unhash unlimit unsetopt vared wait whence where which zcompile zformat zftp zle zmodload zparseopts zprof zpty zregexparse zsocket zstyle ztcp"},contains:[C,o.SHEBANG(),j,x,o.HASH_COMMENT_MODE,_,w,{className:"",begin:/\\"/},{className:"string",begin:/'/,end:/'/},s]}}},73402:o=>{function concat(...o){return o.map((o=>function source(o){return o?"string"==typeof o?o:o.source:null}(o))).join("")}o.exports=function http(o){const s="HTTP/(2|1\\.[01])",i={className:"attribute",begin:concat("^",/[A-Za-z][A-Za-z0-9-]*/,"(?=\\:\\s)"),starts:{contains:[{className:"punctuation",begin:/: /,relevance:0,starts:{end:"$",relevance:0}}]}},u=[i,{begin:"\\n\\n",starts:{subLanguage:[],endsWithParent:!0}}];return{name:"HTTP",aliases:["https"],illegal:/\S/,contains:[{begin:"^(?="+s+" \\d{3})",end:/$/,contains:[{className:"meta",begin:s},{className:"number",begin:"\\b\\d{3}\\b"}],starts:{end:/\b\B/,illegal:/\S/,contains:u}},{begin:"(?=^[A-Z]+ (.*?) "+s+"$)",end:/$/,contains:[{className:"string",begin:" ",end:" ",excludeBegin:!0,excludeEnd:!0},{className:"meta",begin:s},{className:"keyword",begin:"[A-Z]+"}],starts:{end:/\b\B/,illegal:/\S/,contains:u}},o.inherit(i,{relevance:0})]}}},95089:o=>{const s="[A-Za-z$_][0-9A-Za-z$_]*",i=["as","in","of","if","for","while","finally","var","new","function","do","return","void","else","break","catch","instanceof","with","throw","case","default","try","switch","continue","typeof","delete","let","yield","const","class","debugger","async","await","static","import","from","export","extends"],u=["true","false","null","undefined","NaN","Infinity"],_=[].concat(["setInterval","setTimeout","clearInterval","clearTimeout","require","exports","eval","isFinite","isNaN","parseFloat","parseInt","decodeURI","decodeURIComponent","encodeURI","encodeURIComponent","escape","unescape"],["arguments","this","super","console","window","document","localStorage","module","global"],["Intl","DataView","Number","Math","Date","String","RegExp","Object","Function","Boolean","Error","Symbol","Set","Map","WeakSet","WeakMap","Proxy","Reflect","JSON","Promise","Float64Array","Int16Array","Int32Array","Int8Array","Uint16Array","Uint32Array","Float32Array","Array","Uint8Array","Uint8ClampedArray","ArrayBuffer","BigInt64Array","BigUint64Array","BigInt"],["EvalError","InternalError","RangeError","ReferenceError","SyntaxError","TypeError","URIError"]);function lookahead(o){return concat("(?=",o,")")}function concat(...o){return o.map((o=>function source(o){return o?"string"==typeof o?o:o.source:null}(o))).join("")}o.exports=function javascript(o){const w=s,x="<>",C="",j={begin:/<[A-Za-z0-9\\._:-]+/,end:/\/[A-Za-z0-9\\._:-]+>|\/>/,isTrulyOpeningTag:(o,s)=>{const i=o[0].length+o.index,u=o.input[i];"<"!==u?">"===u&&(((o,{after:s})=>{const i="",returnBegin:!0,end:"\\s*=>",contains:[{className:"params",variants:[{begin:o.UNDERSCORE_IDENT_RE,relevance:0},{className:null,begin:/\(\s*\)/,skip:!0},{begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,keywords:L,contains:le}]}]},{begin:/,/,relevance:0},{className:"",begin:/\s/,end:/\s*/,skip:!0},{variants:[{begin:x,end:C},{begin:j.begin,"on:begin":j.isTrulyOpeningTag,end:j.end}],subLanguage:"xml",contains:[{begin:j.begin,end:j.end,skip:!0,contains:["self"]}]}],relevance:0},{className:"function",beginKeywords:"function",end:/[{;]/,excludeEnd:!0,keywords:L,contains:["self",o.inherit(o.TITLE_MODE,{begin:w}),pe],illegal:/%/},{beginKeywords:"while if switch catch for"},{className:"function",begin:o.UNDERSCORE_IDENT_RE+"\\([^()]*(\\([^()]*(\\([^()]*\\)[^()]*)*\\)[^()]*)*\\)\\s*\\{",returnBegin:!0,contains:[pe,o.inherit(o.TITLE_MODE,{begin:w})]},{variants:[{begin:"\\."+w},{begin:"\\$"+w}],relevance:0},{className:"class",beginKeywords:"class",end:/[{;=]/,excludeEnd:!0,illegal:/[:"[\]]/,contains:[{beginKeywords:"extends"},o.UNDERSCORE_TITLE_MODE]},{begin:/\b(?=constructor)/,end:/[{;]/,excludeEnd:!0,contains:[o.inherit(o.TITLE_MODE,{begin:w}),"self",pe]},{begin:"(get|set)\\s+(?="+w+"\\()",end:/\{/,keywords:"get set",contains:[o.inherit(o.TITLE_MODE,{begin:w}),{begin:/\(\)/},pe]},{begin:/\$[(.]/}]}}},65772:o=>{o.exports=function json(o){const s={literal:"true false null"},i=[o.C_LINE_COMMENT_MODE,o.C_BLOCK_COMMENT_MODE],u=[o.QUOTE_STRING_MODE,o.C_NUMBER_MODE],_={end:",",endsWithParent:!0,excludeEnd:!0,contains:u,keywords:s},w={begin:/\{/,end:/\}/,contains:[{className:"attr",begin:/"/,end:/"/,contains:[o.BACKSLASH_ESCAPE],illegal:"\\n"},o.inherit(_,{begin:/:/})].concat(i),illegal:"\\S"},x={begin:"\\[",end:"\\]",contains:[o.inherit(_)],illegal:"\\S"};return u.push(w,x),i.forEach((function(o){u.push(o)})),{name:"JSON",contains:u,keywords:s,illegal:"\\S"}}},26571:o=>{o.exports=function powershell(o){const s={$pattern:/-?[A-z\.\-]+\b/,keyword:"if else foreach return do while until elseif begin for trap data dynamicparam end break throw param continue finally in switch exit filter try process catch hidden static parameter",built_in:"ac asnp cat cd CFS chdir clc clear clhy cli clp cls clv cnsn compare copy cp cpi cpp curl cvpa dbp del diff dir dnsn ebp echo|0 epal epcsv epsn erase etsn exsn fc fhx fl ft fw gal gbp gc gcb gci gcm gcs gdr gerr ghy gi gin gjb gl gm gmo gp gps gpv group gsn gsnp gsv gtz gu gv gwmi h history icm iex ihy ii ipal ipcsv ipmo ipsn irm ise iwmi iwr kill lp ls man md measure mi mount move mp mv nal ndr ni nmo npssc nsn nv ogv oh popd ps pushd pwd r rbp rcjb rcsn rd rdr ren ri rjb rm rmdir rmo rni rnp rp rsn rsnp rujb rv rvpa rwmi sajb sal saps sasv sbp sc scb select set shcm si sl sleep sls sort sp spjb spps spsv start stz sujb sv swmi tee trcm type wget where wjb write"},i={begin:"`[\\s\\S]",relevance:0},u={className:"variable",variants:[{begin:/\$\B/},{className:"keyword",begin:/\$this/},{begin:/\$[\w\d][\w\d_:]*/}]},_={className:"string",variants:[{begin:/"/,end:/"/},{begin:/@"/,end:/^"@/}],contains:[i,u,{className:"variable",begin:/\$[A-z]/,end:/[^A-z]/}]},w={className:"string",variants:[{begin:/'/,end:/'/},{begin:/@'/,end:/^'@/}]},x=o.inherit(o.COMMENT(null,null),{variants:[{begin:/#/,end:/$/},{begin:/<#/,end:/#>/}],contains:[{className:"doctag",variants:[{begin:/\.(synopsis|description|example|inputs|outputs|notes|link|component|role|functionality)/},{begin:/\.(parameter|forwardhelptargetname|forwardhelpcategory|remotehelprunspace|externalhelp)\s+\S+/}]}]}),C={className:"built_in",variants:[{begin:"(".concat("Add|Clear|Close|Copy|Enter|Exit|Find|Format|Get|Hide|Join|Lock|Move|New|Open|Optimize|Pop|Push|Redo|Remove|Rename|Reset|Resize|Search|Select|Set|Show|Skip|Split|Step|Switch|Undo|Unlock|Watch|Backup|Checkpoint|Compare|Compress|Convert|ConvertFrom|ConvertTo|Dismount|Edit|Expand|Export|Group|Import|Initialize|Limit|Merge|Mount|Out|Publish|Restore|Save|Sync|Unpublish|Update|Approve|Assert|Build|Complete|Confirm|Deny|Deploy|Disable|Enable|Install|Invoke|Register|Request|Restart|Resume|Start|Stop|Submit|Suspend|Uninstall|Unregister|Wait|Debug|Measure|Ping|Repair|Resolve|Test|Trace|Connect|Disconnect|Read|Receive|Send|Write|Block|Grant|Protect|Revoke|Unblock|Unprotect|Use|ForEach|Sort|Tee|Where",")+(-)[\\w\\d]+")}]},j={className:"class",beginKeywords:"class enum",end:/\s*[{]/,excludeEnd:!0,relevance:0,contains:[o.TITLE_MODE]},L={className:"function",begin:/function\s+/,end:/\s*\{|$/,excludeEnd:!0,returnBegin:!0,relevance:0,contains:[{begin:"function",relevance:0,className:"keyword"},{className:"title",begin:/\w[\w\d]*((-)[\w\d]+)*/,relevance:0},{begin:/\(/,end:/\)/,className:"params",relevance:0,contains:[u]}]},B={begin:/using\s/,end:/$/,returnBegin:!0,contains:[_,w,{className:"keyword",begin:/(using|assembly|command|module|namespace|type)/}]},$={variants:[{className:"operator",begin:"(".concat("-and|-as|-band|-bnot|-bor|-bxor|-casesensitive|-ccontains|-ceq|-cge|-cgt|-cle|-clike|-clt|-cmatch|-cne|-cnotcontains|-cnotlike|-cnotmatch|-contains|-creplace|-csplit|-eq|-exact|-f|-file|-ge|-gt|-icontains|-ieq|-ige|-igt|-ile|-ilike|-ilt|-imatch|-in|-ine|-inotcontains|-inotlike|-inotmatch|-ireplace|-is|-isnot|-isplit|-join|-le|-like|-lt|-match|-ne|-not|-notcontains|-notin|-notlike|-notmatch|-or|-regex|-replace|-shl|-shr|-split|-wildcard|-xor",")\\b")},{className:"literal",begin:/(-)[\w\d]+/,relevance:0}]},V={className:"function",begin:/\[.*\]\s*[\w]+[ ]??\(/,end:/$/,returnBegin:!0,relevance:0,contains:[{className:"keyword",begin:"(".concat(s.keyword.toString().replace(/\s/g,"|"),")\\b"),endsParent:!0,relevance:0},o.inherit(o.TITLE_MODE,{endsParent:!0})]},U=[V,x,i,o.NUMBER_MODE,_,w,C,u,{className:"literal",begin:/\$(null|true|false)\b/},{className:"selector-tag",begin:/@\B/,relevance:0}],z={begin:/\[/,end:/\]/,excludeBegin:!0,excludeEnd:!0,relevance:0,contains:[].concat("self",U,{begin:"("+["string","char","byte","int","long","bool","decimal","single","double","DateTime","xml","array","hashtable","void"].join("|")+")",className:"built_in",relevance:0},{className:"type",begin:/[\.\w\d]+/,relevance:0})};return V.contains.unshift(z),{name:"PowerShell",aliases:["ps","ps1"],case_insensitive:!0,keywords:s,contains:U.concat(j,L,B,$,z)}}},17285:o=>{function source(o){return o?"string"==typeof o?o:o.source:null}function lookahead(o){return concat("(?=",o,")")}function concat(...o){return o.map((o=>source(o))).join("")}function either(...o){return"("+o.map((o=>source(o))).join("|")+")"}o.exports=function xml(o){const s=concat(/[A-Z_]/,function optional(o){return concat("(",o,")?")}(/[A-Z0-9_.-]*:/),/[A-Z0-9_.-]*/),i={className:"symbol",begin:/&[a-z]+;|&#[0-9]+;|&#x[a-f0-9]+;/},u={begin:/\s/,contains:[{className:"meta-keyword",begin:/#?[a-z_][a-z1-9_-]+/,illegal:/\n/}]},_=o.inherit(u,{begin:/\(/,end:/\)/}),w=o.inherit(o.APOS_STRING_MODE,{className:"meta-string"}),x=o.inherit(o.QUOTE_STRING_MODE,{className:"meta-string"}),C={endsWithParent:!0,illegal:/`]+/}]}]}]};return{name:"HTML, XML",aliases:["html","xhtml","rss","atom","xjb","xsd","xsl","plist","wsf","svg"],case_insensitive:!0,contains:[{className:"meta",begin://,relevance:10,contains:[u,x,w,_,{begin:/\[/,end:/\]/,contains:[{className:"meta",begin://,contains:[u,_,x,w]}]}]},o.COMMENT(//,{relevance:10}),{begin://,relevance:10},i,{className:"meta",begin:/<\?xml/,end:/\?>/,relevance:10},{className:"tag",begin:/)/,end:/>/,keywords:{name:"style"},contains:[C],starts:{end:/<\/style>/,returnEnd:!0,subLanguage:["css","xml"]}},{className:"tag",begin:/)/,end:/>/,keywords:{name:"script"},contains:[C],starts:{end:/<\/script>/,returnEnd:!0,subLanguage:["javascript","handlebars","xml"]}},{className:"tag",begin:/<>|<\/>/},{className:"tag",begin:concat(//,/>/,/\s/)))),end:/\/?>/,contains:[{className:"name",begin:s,relevance:0,starts:C}]},{className:"tag",begin:concat(/<\//,lookahead(concat(s,/>/))),contains:[{className:"name",begin:s,relevance:0},{begin:/>/,relevance:0,endsParent:!0}]}]}}},17533:o=>{o.exports=function yaml(o){var s="true false yes no null",i="[\\w#;/?:@&=+$,.~*'()[\\]]+",u={className:"string",relevance:0,variants:[{begin:/'/,end:/'/},{begin:/"/,end:/"/},{begin:/\S+/}],contains:[o.BACKSLASH_ESCAPE,{className:"template-variable",variants:[{begin:/\{\{/,end:/\}\}/},{begin:/%\{/,end:/\}/}]}]},_=o.inherit(u,{variants:[{begin:/'/,end:/'/},{begin:/"/,end:/"/},{begin:/[^\s,{}[\]]+/}]}),w={className:"number",begin:"\\b[0-9]{4}(-[0-9][0-9]){0,2}([Tt \\t][0-9][0-9]?(:[0-9][0-9]){2})?(\\.[0-9]*)?([ \\t])*(Z|[-+][0-9][0-9]?(:[0-9][0-9])?)?\\b"},x={end:",",endsWithParent:!0,excludeEnd:!0,keywords:s,relevance:0},C={begin:/\{/,end:/\}/,contains:[x],illegal:"\\n",relevance:0},j={begin:"\\[",end:"\\]",contains:[x],illegal:"\\n",relevance:0},L=[{className:"attr",variants:[{begin:"\\w[\\w :\\/.-]*:(?=[ \t]|$)"},{begin:'"\\w[\\w :\\/.-]*":(?=[ \t]|$)'},{begin:"'\\w[\\w :\\/.-]*':(?=[ \t]|$)"}]},{className:"meta",begin:"^---\\s*$",relevance:10},{className:"string",begin:"[\\|>]([1-9]?[+-])?[ ]*\\n( +)[^ ][^\\n]*\\n(\\2[^\\n]+\\n?)*"},{begin:"<%[%=-]?",end:"[%-]?%>",subLanguage:"ruby",excludeBegin:!0,excludeEnd:!0,relevance:0},{className:"type",begin:"!\\w+!"+i},{className:"type",begin:"!<"+i+">"},{className:"type",begin:"!"+i},{className:"type",begin:"!!"+i},{className:"meta",begin:"&"+o.UNDERSCORE_IDENT_RE+"$"},{className:"meta",begin:"\\*"+o.UNDERSCORE_IDENT_RE+"$"},{className:"bullet",begin:"-(?=[ ]|$)",relevance:0},o.HASH_COMMENT_MODE,{beginKeywords:s,keywords:{literal:s}},w,{className:"number",begin:o.C_NUMBER_RE+"\\b",relevance:0},C,j,u],B=[...L];return B.pop(),B.push(_),x.contains=B,{name:"YAML",case_insensitive:!0,aliases:["yml"],contains:L}}},251:(o,s)=>{s.read=function(o,s,i,u,_){var w,x,C=8*_-u-1,j=(1<>1,B=-7,$=i?_-1:0,V=i?-1:1,U=o[s+$];for($+=V,w=U&(1<<-B)-1,U>>=-B,B+=C;B>0;w=256*w+o[s+$],$+=V,B-=8);for(x=w&(1<<-B)-1,w>>=-B,B+=u;B>0;x=256*x+o[s+$],$+=V,B-=8);if(0===w)w=1-L;else{if(w===j)return x?NaN:1/0*(U?-1:1);x+=Math.pow(2,u),w-=L}return(U?-1:1)*x*Math.pow(2,w-u)},s.write=function(o,s,i,u,_,w){var x,C,j,L=8*w-_-1,B=(1<>1,V=23===_?Math.pow(2,-24)-Math.pow(2,-77):0,U=u?0:w-1,z=u?1:-1,Y=s<0||0===s&&1/s<0?1:0;for(s=Math.abs(s),isNaN(s)||s===1/0?(C=isNaN(s)?1:0,x=B):(x=Math.floor(Math.log(s)/Math.LN2),s*(j=Math.pow(2,-x))<1&&(x--,j*=2),(s+=x+$>=1?V/j:V*Math.pow(2,1-$))*j>=2&&(x++,j/=2),x+$>=B?(C=0,x=B):x+$>=1?(C=(s*j-1)*Math.pow(2,_),x+=$):(C=s*Math.pow(2,$-1)*Math.pow(2,_),x=0));_>=8;o[i+U]=255&C,U+=z,C/=256,_-=8);for(x=x<<_|C,L+=_;L>0;o[i+U]=255&x,U+=z,x/=256,L-=8);o[i+U-z]|=128*Y}},9404:function(o){o.exports=function(){"use strict";var o=Array.prototype.slice;function createClass(o,s){s&&(o.prototype=Object.create(s.prototype)),o.prototype.constructor=o}function Iterable(o){return isIterable(o)?o:Seq(o)}function KeyedIterable(o){return isKeyed(o)?o:KeyedSeq(o)}function IndexedIterable(o){return isIndexed(o)?o:IndexedSeq(o)}function SetIterable(o){return isIterable(o)&&!isAssociative(o)?o:SetSeq(o)}function isIterable(o){return!(!o||!o[s])}function isKeyed(o){return!(!o||!o[i])}function isIndexed(o){return!(!o||!o[u])}function isAssociative(o){return isKeyed(o)||isIndexed(o)}function isOrdered(o){return!(!o||!o[_])}createClass(KeyedIterable,Iterable),createClass(IndexedIterable,Iterable),createClass(SetIterable,Iterable),Iterable.isIterable=isIterable,Iterable.isKeyed=isKeyed,Iterable.isIndexed=isIndexed,Iterable.isAssociative=isAssociative,Iterable.isOrdered=isOrdered,Iterable.Keyed=KeyedIterable,Iterable.Indexed=IndexedIterable,Iterable.Set=SetIterable;var s="@@__IMMUTABLE_ITERABLE__@@",i="@@__IMMUTABLE_KEYED__@@",u="@@__IMMUTABLE_INDEXED__@@",_="@@__IMMUTABLE_ORDERED__@@",w="delete",x=5,C=1<>>0;if(""+i!==s||4294967295===i)return NaN;s=i}return s<0?ensureSize(o)+s:s}function returnTrue(){return!0}function wholeSlice(o,s,i){return(0===o||void 0!==i&&o<=-i)&&(void 0===s||void 0!==i&&s>=i)}function resolveBegin(o,s){return resolveIndex(o,s,0)}function resolveEnd(o,s){return resolveIndex(o,s,s)}function resolveIndex(o,s,i){return void 0===o?i:o<0?Math.max(0,s+o):void 0===s?o:Math.min(s,o)}var V=0,U=1,z=2,Y="function"==typeof Symbol&&Symbol.iterator,Z="@@iterator",ee=Y||Z;function Iterator(o){this.next=o}function iteratorValue(o,s,i,u){var _=0===o?s:1===o?i:[s,i];return u?u.value=_:u={value:_,done:!1},u}function iteratorDone(){return{value:void 0,done:!0}}function hasIterator(o){return!!getIteratorFn(o)}function isIterator(o){return o&&"function"==typeof o.next}function getIterator(o){var s=getIteratorFn(o);return s&&s.call(o)}function getIteratorFn(o){var s=o&&(Y&&o[Y]||o[Z]);if("function"==typeof s)return s}function isArrayLike(o){return o&&"number"==typeof o.length}function Seq(o){return null==o?emptySequence():isIterable(o)?o.toSeq():seqFromValue(o)}function KeyedSeq(o){return null==o?emptySequence().toKeyedSeq():isIterable(o)?isKeyed(o)?o.toSeq():o.fromEntrySeq():keyedSeqFromValue(o)}function IndexedSeq(o){return null==o?emptySequence():isIterable(o)?isKeyed(o)?o.entrySeq():o.toIndexedSeq():indexedSeqFromValue(o)}function SetSeq(o){return(null==o?emptySequence():isIterable(o)?isKeyed(o)?o.entrySeq():o:indexedSeqFromValue(o)).toSetSeq()}Iterator.prototype.toString=function(){return"[Iterator]"},Iterator.KEYS=V,Iterator.VALUES=U,Iterator.ENTRIES=z,Iterator.prototype.inspect=Iterator.prototype.toSource=function(){return this.toString()},Iterator.prototype[ee]=function(){return this},createClass(Seq,Iterable),Seq.of=function(){return Seq(arguments)},Seq.prototype.toSeq=function(){return this},Seq.prototype.toString=function(){return this.__toString("Seq {","}")},Seq.prototype.cacheResult=function(){return!this._cache&&this.__iterateUncached&&(this._cache=this.entrySeq().toArray(),this.size=this._cache.length),this},Seq.prototype.__iterate=function(o,s){return seqIterate(this,o,s,!0)},Seq.prototype.__iterator=function(o,s){return seqIterator(this,o,s,!0)},createClass(KeyedSeq,Seq),KeyedSeq.prototype.toKeyedSeq=function(){return this},createClass(IndexedSeq,Seq),IndexedSeq.of=function(){return IndexedSeq(arguments)},IndexedSeq.prototype.toIndexedSeq=function(){return this},IndexedSeq.prototype.toString=function(){return this.__toString("Seq [","]")},IndexedSeq.prototype.__iterate=function(o,s){return seqIterate(this,o,s,!1)},IndexedSeq.prototype.__iterator=function(o,s){return seqIterator(this,o,s,!1)},createClass(SetSeq,Seq),SetSeq.of=function(){return SetSeq(arguments)},SetSeq.prototype.toSetSeq=function(){return this},Seq.isSeq=isSeq,Seq.Keyed=KeyedSeq,Seq.Set=SetSeq,Seq.Indexed=IndexedSeq;var ie,ae,ce,le="@@__IMMUTABLE_SEQ__@@";function ArraySeq(o){this._array=o,this.size=o.length}function ObjectSeq(o){var s=Object.keys(o);this._object=o,this._keys=s,this.size=s.length}function IterableSeq(o){this._iterable=o,this.size=o.length||o.size}function IteratorSeq(o){this._iterator=o,this._iteratorCache=[]}function isSeq(o){return!(!o||!o[le])}function emptySequence(){return ie||(ie=new ArraySeq([]))}function keyedSeqFromValue(o){var s=Array.isArray(o)?new ArraySeq(o).fromEntrySeq():isIterator(o)?new IteratorSeq(o).fromEntrySeq():hasIterator(o)?new IterableSeq(o).fromEntrySeq():"object"==typeof o?new ObjectSeq(o):void 0;if(!s)throw new TypeError("Expected Array or iterable object of [k, v] entries, or keyed object: "+o);return s}function indexedSeqFromValue(o){var s=maybeIndexedSeqFromValue(o);if(!s)throw new TypeError("Expected Array or iterable object of values: "+o);return s}function seqFromValue(o){var s=maybeIndexedSeqFromValue(o)||"object"==typeof o&&new ObjectSeq(o);if(!s)throw new TypeError("Expected Array or iterable object of values, or keyed object: "+o);return s}function maybeIndexedSeqFromValue(o){return isArrayLike(o)?new ArraySeq(o):isIterator(o)?new IteratorSeq(o):hasIterator(o)?new IterableSeq(o):void 0}function seqIterate(o,s,i,u){var _=o._cache;if(_){for(var w=_.length-1,x=0;x<=w;x++){var C=_[i?w-x:x];if(!1===s(C[1],u?C[0]:x,o))return x+1}return x}return o.__iterateUncached(s,i)}function seqIterator(o,s,i,u){var _=o._cache;if(_){var w=_.length-1,x=0;return new Iterator((function(){var o=_[i?w-x:x];return x++>w?iteratorDone():iteratorValue(s,u?o[0]:x-1,o[1])}))}return o.__iteratorUncached(s,i)}function fromJS(o,s){return s?fromJSWith(s,o,"",{"":o}):fromJSDefault(o)}function fromJSWith(o,s,i,u){return Array.isArray(s)?o.call(u,i,IndexedSeq(s).map((function(i,u){return fromJSWith(o,i,u,s)}))):isPlainObj(s)?o.call(u,i,KeyedSeq(s).map((function(i,u){return fromJSWith(o,i,u,s)}))):s}function fromJSDefault(o){return Array.isArray(o)?IndexedSeq(o).map(fromJSDefault).toList():isPlainObj(o)?KeyedSeq(o).map(fromJSDefault).toMap():o}function isPlainObj(o){return o&&(o.constructor===Object||void 0===o.constructor)}function is(o,s){if(o===s||o!=o&&s!=s)return!0;if(!o||!s)return!1;if("function"==typeof o.valueOf&&"function"==typeof s.valueOf){if((o=o.valueOf())===(s=s.valueOf())||o!=o&&s!=s)return!0;if(!o||!s)return!1}return!("function"!=typeof o.equals||"function"!=typeof s.equals||!o.equals(s))}function deepEqual(o,s){if(o===s)return!0;if(!isIterable(s)||void 0!==o.size&&void 0!==s.size&&o.size!==s.size||void 0!==o.__hash&&void 0!==s.__hash&&o.__hash!==s.__hash||isKeyed(o)!==isKeyed(s)||isIndexed(o)!==isIndexed(s)||isOrdered(o)!==isOrdered(s))return!1;if(0===o.size&&0===s.size)return!0;var i=!isAssociative(o);if(isOrdered(o)){var u=o.entries();return s.every((function(o,s){var _=u.next().value;return _&&is(_[1],o)&&(i||is(_[0],s))}))&&u.next().done}var _=!1;if(void 0===o.size)if(void 0===s.size)"function"==typeof o.cacheResult&&o.cacheResult();else{_=!0;var w=o;o=s,s=w}var x=!0,C=s.__iterate((function(s,u){if(i?!o.has(s):_?!is(s,o.get(u,L)):!is(o.get(u,L),s))return x=!1,!1}));return x&&o.size===C}function Repeat(o,s){if(!(this instanceof Repeat))return new Repeat(o,s);if(this._value=o,this.size=void 0===s?1/0:Math.max(0,s),0===this.size){if(ae)return ae;ae=this}}function invariant(o,s){if(!o)throw new Error(s)}function Range(o,s,i){if(!(this instanceof Range))return new Range(o,s,i);if(invariant(0!==i,"Cannot step a Range by 0"),o=o||0,void 0===s&&(s=1/0),i=void 0===i?1:Math.abs(i),su?iteratorDone():iteratorValue(o,_,i[s?u-_++:_++])}))},createClass(ObjectSeq,KeyedSeq),ObjectSeq.prototype.get=function(o,s){return void 0===s||this.has(o)?this._object[o]:s},ObjectSeq.prototype.has=function(o){return this._object.hasOwnProperty(o)},ObjectSeq.prototype.__iterate=function(o,s){for(var i=this._object,u=this._keys,_=u.length-1,w=0;w<=_;w++){var x=u[s?_-w:w];if(!1===o(i[x],x,this))return w+1}return w},ObjectSeq.prototype.__iterator=function(o,s){var i=this._object,u=this._keys,_=u.length-1,w=0;return new Iterator((function(){var x=u[s?_-w:w];return w++>_?iteratorDone():iteratorValue(o,x,i[x])}))},ObjectSeq.prototype[_]=!0,createClass(IterableSeq,IndexedSeq),IterableSeq.prototype.__iterateUncached=function(o,s){if(s)return this.cacheResult().__iterate(o,s);var i=getIterator(this._iterable),u=0;if(isIterator(i))for(var _;!(_=i.next()).done&&!1!==o(_.value,u++,this););return u},IterableSeq.prototype.__iteratorUncached=function(o,s){if(s)return this.cacheResult().__iterator(o,s);var i=getIterator(this._iterable);if(!isIterator(i))return new Iterator(iteratorDone);var u=0;return new Iterator((function(){var s=i.next();return s.done?s:iteratorValue(o,u++,s.value)}))},createClass(IteratorSeq,IndexedSeq),IteratorSeq.prototype.__iterateUncached=function(o,s){if(s)return this.cacheResult().__iterate(o,s);for(var i,u=this._iterator,_=this._iteratorCache,w=0;w<_.length;)if(!1===o(_[w],w++,this))return w;for(;!(i=u.next()).done;){var x=i.value;if(_[w]=x,!1===o(x,w++,this))break}return w},IteratorSeq.prototype.__iteratorUncached=function(o,s){if(s)return this.cacheResult().__iterator(o,s);var i=this._iterator,u=this._iteratorCache,_=0;return new Iterator((function(){if(_>=u.length){var s=i.next();if(s.done)return s;u[_]=s.value}return iteratorValue(o,_,u[_++])}))},createClass(Repeat,IndexedSeq),Repeat.prototype.toString=function(){return 0===this.size?"Repeat []":"Repeat [ "+this._value+" "+this.size+" times ]"},Repeat.prototype.get=function(o,s){return this.has(o)?this._value:s},Repeat.prototype.includes=function(o){return is(this._value,o)},Repeat.prototype.slice=function(o,s){var i=this.size;return wholeSlice(o,s,i)?this:new Repeat(this._value,resolveEnd(s,i)-resolveBegin(o,i))},Repeat.prototype.reverse=function(){return this},Repeat.prototype.indexOf=function(o){return is(this._value,o)?0:-1},Repeat.prototype.lastIndexOf=function(o){return is(this._value,o)?this.size:-1},Repeat.prototype.__iterate=function(o,s){for(var i=0;i=0&&s=0&&ii?iteratorDone():iteratorValue(o,w++,x)}))},Range.prototype.equals=function(o){return o instanceof Range?this._start===o._start&&this._end===o._end&&this._step===o._step:deepEqual(this,o)},createClass(Collection,Iterable),createClass(KeyedCollection,Collection),createClass(IndexedCollection,Collection),createClass(SetCollection,Collection),Collection.Keyed=KeyedCollection,Collection.Indexed=IndexedCollection,Collection.Set=SetCollection;var pe="function"==typeof Math.imul&&-2===Math.imul(4294967295,2)?Math.imul:function imul(o,s){var i=65535&(o|=0),u=65535&(s|=0);return i*u+((o>>>16)*u+i*(s>>>16)<<16>>>0)|0};function smi(o){return o>>>1&1073741824|3221225471&o}function hash(o){if(!1===o||null==o)return 0;if("function"==typeof o.valueOf&&(!1===(o=o.valueOf())||null==o))return 0;if(!0===o)return 1;var s=typeof o;if("number"===s){if(o!=o||o===1/0)return 0;var i=0|o;for(i!==o&&(i^=4294967295*o);o>4294967295;)i^=o/=4294967295;return smi(i)}if("string"===s)return o.length>Se?cachedHashString(o):hashString(o);if("function"==typeof o.hashCode)return o.hashCode();if("object"===s)return hashJSObj(o);if("function"==typeof o.toString)return hashString(o.toString());throw new Error("Value type "+s+" cannot be hashed.")}function cachedHashString(o){var s=Te[o];return void 0===s&&(s=hashString(o),Pe===xe&&(Pe=0,Te={}),Pe++,Te[o]=s),s}function hashString(o){for(var s=0,i=0;i0)switch(o.nodeType){case 1:return o.uniqueID;case 9:return o.documentElement&&o.documentElement.uniqueID}}var ye,be="function"==typeof WeakMap;be&&(ye=new WeakMap);var _e=0,we="__immutablehash__";"function"==typeof Symbol&&(we=Symbol(we));var Se=16,xe=255,Pe=0,Te={};function assertNotInfinite(o){invariant(o!==1/0,"Cannot perform this action with an infinite size.")}function Map(o){return null==o?emptyMap():isMap(o)&&!isOrdered(o)?o:emptyMap().withMutations((function(s){var i=KeyedIterable(o);assertNotInfinite(i.size),i.forEach((function(o,i){return s.set(i,o)}))}))}function isMap(o){return!(!o||!o[qe])}createClass(Map,KeyedCollection),Map.of=function(){var s=o.call(arguments,0);return emptyMap().withMutations((function(o){for(var i=0;i=s.length)throw new Error("Missing value for key: "+s[i]);o.set(s[i],s[i+1])}}))},Map.prototype.toString=function(){return this.__toString("Map {","}")},Map.prototype.get=function(o,s){return this._root?this._root.get(0,void 0,o,s):s},Map.prototype.set=function(o,s){return updateMap(this,o,s)},Map.prototype.setIn=function(o,s){return this.updateIn(o,L,(function(){return s}))},Map.prototype.remove=function(o){return updateMap(this,o,L)},Map.prototype.deleteIn=function(o){return this.updateIn(o,(function(){return L}))},Map.prototype.update=function(o,s,i){return 1===arguments.length?o(this):this.updateIn([o],s,i)},Map.prototype.updateIn=function(o,s,i){i||(i=s,s=void 0);var u=updateInDeepMap(this,forceIterator(o),s,i);return u===L?void 0:u},Map.prototype.clear=function(){return 0===this.size?this:this.__ownerID?(this.size=0,this._root=null,this.__hash=void 0,this.__altered=!0,this):emptyMap()},Map.prototype.merge=function(){return mergeIntoMapWith(this,void 0,arguments)},Map.prototype.mergeWith=function(s){return mergeIntoMapWith(this,s,o.call(arguments,1))},Map.prototype.mergeIn=function(s){var i=o.call(arguments,1);return this.updateIn(s,emptyMap(),(function(o){return"function"==typeof o.merge?o.merge.apply(o,i):i[i.length-1]}))},Map.prototype.mergeDeep=function(){return mergeIntoMapWith(this,deepMerger,arguments)},Map.prototype.mergeDeepWith=function(s){var i=o.call(arguments,1);return mergeIntoMapWith(this,deepMergerWith(s),i)},Map.prototype.mergeDeepIn=function(s){var i=o.call(arguments,1);return this.updateIn(s,emptyMap(),(function(o){return"function"==typeof o.mergeDeep?o.mergeDeep.apply(o,i):i[i.length-1]}))},Map.prototype.sort=function(o){return OrderedMap(sortFactory(this,o))},Map.prototype.sortBy=function(o,s){return OrderedMap(sortFactory(this,s,o))},Map.prototype.withMutations=function(o){var s=this.asMutable();return o(s),s.wasAltered()?s.__ensureOwner(this.__ownerID):this},Map.prototype.asMutable=function(){return this.__ownerID?this:this.__ensureOwner(new OwnerID)},Map.prototype.asImmutable=function(){return this.__ensureOwner()},Map.prototype.wasAltered=function(){return this.__altered},Map.prototype.__iterator=function(o,s){return new MapIterator(this,o,s)},Map.prototype.__iterate=function(o,s){var i=this,u=0;return this._root&&this._root.iterate((function(s){return u++,o(s[1],s[0],i)}),s),u},Map.prototype.__ensureOwner=function(o){return o===this.__ownerID?this:o?makeMap(this.size,this._root,o,this.__hash):(this.__ownerID=o,this.__altered=!1,this)},Map.isMap=isMap;var Re,qe="@@__IMMUTABLE_MAP__@@",$e=Map.prototype;function ArrayMapNode(o,s){this.ownerID=o,this.entries=s}function BitmapIndexedNode(o,s,i){this.ownerID=o,this.bitmap=s,this.nodes=i}function HashArrayMapNode(o,s,i){this.ownerID=o,this.count=s,this.nodes=i}function HashCollisionNode(o,s,i){this.ownerID=o,this.keyHash=s,this.entries=i}function ValueNode(o,s,i){this.ownerID=o,this.keyHash=s,this.entry=i}function MapIterator(o,s,i){this._type=s,this._reverse=i,this._stack=o._root&&mapIteratorFrame(o._root)}function mapIteratorValue(o,s){return iteratorValue(o,s[0],s[1])}function mapIteratorFrame(o,s){return{node:o,index:0,__prev:s}}function makeMap(o,s,i,u){var _=Object.create($e);return _.size=o,_._root=s,_.__ownerID=i,_.__hash=u,_.__altered=!1,_}function emptyMap(){return Re||(Re=makeMap(0))}function updateMap(o,s,i){var u,_;if(o._root){var w=MakeRef(B),x=MakeRef($);if(u=updateNode(o._root,o.__ownerID,0,void 0,s,i,w,x),!x.value)return o;_=o.size+(w.value?i===L?-1:1:0)}else{if(i===L)return o;_=1,u=new ArrayMapNode(o.__ownerID,[[s,i]])}return o.__ownerID?(o.size=_,o._root=u,o.__hash=void 0,o.__altered=!0,o):u?makeMap(_,u):emptyMap()}function updateNode(o,s,i,u,_,w,x,C){return o?o.update(s,i,u,_,w,x,C):w===L?o:(SetRef(C),SetRef(x),new ValueNode(s,u,[_,w]))}function isLeafNode(o){return o.constructor===ValueNode||o.constructor===HashCollisionNode}function mergeIntoNode(o,s,i,u,_){if(o.keyHash===u)return new HashCollisionNode(s,u,[o.entry,_]);var w,C=(0===i?o.keyHash:o.keyHash>>>i)&j,L=(0===i?u:u>>>i)&j;return new BitmapIndexedNode(s,1<>>=1)x[j]=1&i?s[w++]:void 0;return x[u]=_,new HashArrayMapNode(o,w+1,x)}function mergeIntoMapWith(o,s,i){for(var u=[],_=0;_>1&1431655765))+(o>>2&858993459))+(o>>4)&252645135,o+=o>>8,127&(o+=o>>16)}function setIn(o,s,i,u){var _=u?o:arrCopy(o);return _[s]=i,_}function spliceIn(o,s,i,u){var _=o.length+1;if(u&&s+1===_)return o[s]=i,o;for(var w=new Array(_),x=0,C=0;C<_;C++)C===s?(w[C]=i,x=-1):w[C]=o[C+x];return w}function spliceOut(o,s,i){var u=o.length-1;if(i&&s===u)return o.pop(),o;for(var _=new Array(u),w=0,x=0;x=ze)return createNodes(o,j,u,_);var U=o&&o===this.ownerID,z=U?j:arrCopy(j);return V?C?B===$-1?z.pop():z[B]=z.pop():z[B]=[u,_]:z.push([u,_]),U?(this.entries=z,this):new ArrayMapNode(o,z)}},BitmapIndexedNode.prototype.get=function(o,s,i,u){void 0===s&&(s=hash(i));var _=1<<((0===o?s:s>>>o)&j),w=this.bitmap;return w&_?this.nodes[popCount(w&_-1)].get(o+x,s,i,u):u},BitmapIndexedNode.prototype.update=function(o,s,i,u,_,w,C){void 0===i&&(i=hash(u));var B=(0===s?i:i>>>s)&j,$=1<=We)return expandNodes(o,Y,V,B,ee);if(U&&!ee&&2===Y.length&&isLeafNode(Y[1^z]))return Y[1^z];if(U&&ee&&1===Y.length&&isLeafNode(ee))return ee;var ie=o&&o===this.ownerID,ae=U?ee?V:V^$:V|$,ce=U?ee?setIn(Y,z,ee,ie):spliceOut(Y,z,ie):spliceIn(Y,z,ee,ie);return ie?(this.bitmap=ae,this.nodes=ce,this):new BitmapIndexedNode(o,ae,ce)},HashArrayMapNode.prototype.get=function(o,s,i,u){void 0===s&&(s=hash(i));var _=(0===o?s:s>>>o)&j,w=this.nodes[_];return w?w.get(o+x,s,i,u):u},HashArrayMapNode.prototype.update=function(o,s,i,u,_,w,C){void 0===i&&(i=hash(u));var B=(0===s?i:i>>>s)&j,$=_===L,V=this.nodes,U=V[B];if($&&!U)return this;var z=updateNode(U,o,s+x,i,u,_,w,C);if(z===U)return this;var Y=this.count;if(U){if(!z&&--Y0&&u=0&&o>>s&j;if(u>=this.array.length)return new VNode([],o);var _,w=0===u;if(s>0){var C=this.array[u];if((_=C&&C.removeBefore(o,s-x,i))===C&&w)return this}if(w&&!_)return this;var L=editableVNode(this,o);if(!w)for(var B=0;B>>s&j;if(_>=this.array.length)return this;if(s>0){var w=this.array[_];if((u=w&&w.removeAfter(o,s-x,i))===w&&_===this.array.length-1)return this}var C=editableVNode(this,o);return C.array.splice(_+1),u&&(C.array[_]=u),C};var Qe,et,tt={};function iterateList(o,s){var i=o._origin,u=o._capacity,_=getTailOffset(u),w=o._tail;return iterateNodeOrLeaf(o._root,o._level,0);function iterateNodeOrLeaf(o,s,i){return 0===s?iterateLeaf(o,i):iterateNode(o,s,i)}function iterateLeaf(o,x){var j=x===_?w&&w.array:o&&o.array,L=x>i?0:i-x,B=u-x;return B>C&&(B=C),function(){if(L===B)return tt;var o=s?--B:L++;return j&&j[o]}}function iterateNode(o,_,w){var j,L=o&&o.array,B=w>i?0:i-w>>_,$=1+(u-w>>_);return $>C&&($=C),function(){for(;;){if(j){var o=j();if(o!==tt)return o;j=null}if(B===$)return tt;var i=s?--$:B++;j=iterateNodeOrLeaf(L&&L[i],_-x,w+(i<<_))}}}}function makeList(o,s,i,u,_,w,x){var C=Object.create(Xe);return C.size=s-o,C._origin=o,C._capacity=s,C._level=i,C._root=u,C._tail=_,C.__ownerID=w,C.__hash=x,C.__altered=!1,C}function emptyList(){return Qe||(Qe=makeList(0,0,x))}function updateList(o,s,i){if((s=wrapIndex(o,s))!=s)return o;if(s>=o.size||s<0)return o.withMutations((function(o){s<0?setListBounds(o,s).set(0,i):setListBounds(o,0,s+1).set(s,i)}));s+=o._origin;var u=o._tail,_=o._root,w=MakeRef($);return s>=getTailOffset(o._capacity)?u=updateVNode(u,o.__ownerID,0,s,i,w):_=updateVNode(_,o.__ownerID,o._level,s,i,w),w.value?o.__ownerID?(o._root=_,o._tail=u,o.__hash=void 0,o.__altered=!0,o):makeList(o._origin,o._capacity,o._level,_,u):o}function updateVNode(o,s,i,u,_,w){var C,L=u>>>i&j,B=o&&L0){var $=o&&o.array[L],V=updateVNode($,s,i-x,u,_,w);return V===$?o:((C=editableVNode(o,s)).array[L]=V,C)}return B&&o.array[L]===_?o:(SetRef(w),C=editableVNode(o,s),void 0===_&&L===C.array.length-1?C.array.pop():C.array[L]=_,C)}function editableVNode(o,s){return s&&o&&s===o.ownerID?o:new VNode(o?o.array.slice():[],s)}function listNodeFor(o,s){if(s>=getTailOffset(o._capacity))return o._tail;if(s<1<0;)i=i.array[s>>>u&j],u-=x;return i}}function setListBounds(o,s,i){void 0!==s&&(s|=0),void 0!==i&&(i|=0);var u=o.__ownerID||new OwnerID,_=o._origin,w=o._capacity,C=_+s,L=void 0===i?w:i<0?w+i:_+i;if(C===_&&L===w)return o;if(C>=L)return o.clear();for(var B=o._level,$=o._root,V=0;C+V<0;)$=new VNode($&&$.array.length?[void 0,$]:[],u),V+=1<<(B+=x);V&&(C+=V,_+=V,L+=V,w+=V);for(var U=getTailOffset(w),z=getTailOffset(L);z>=1<U?new VNode([],u):Y;if(Y&&z>U&&Cx;ie-=x){var ae=U>>>ie&j;ee=ee.array[ae]=editableVNode(ee.array[ae],u)}ee.array[U>>>x&j]=Y}if(L=z)C-=z,L-=z,B=x,$=null,Z=Z&&Z.removeBefore(u,0,C);else if(C>_||z>>B&j;if(ce!==z>>>B&j)break;ce&&(V+=(1<_&&($=$.removeBefore(u,B,C-V)),$&&z_&&(_=C.size),isIterable(x)||(C=C.map((function(o){return fromJS(o)}))),u.push(C)}return _>o.size&&(o=o.setSize(_)),mergeIntoCollectionWith(o,s,u)}function getTailOffset(o){return o>>x<=C&&x.size>=2*w.size?(u=(_=x.filter((function(o,s){return void 0!==o&&j!==s}))).toKeyedSeq().map((function(o){return o[0]})).flip().toMap(),o.__ownerID&&(u.__ownerID=_.__ownerID=o.__ownerID)):(u=w.remove(s),_=j===x.size-1?x.pop():x.set(j,void 0))}else if(B){if(i===x.get(j)[1])return o;u=w,_=x.set(j,[s,i])}else u=w.set(s,x.size),_=x.set(x.size,[s,i]);return o.__ownerID?(o.size=u.size,o._map=u,o._list=_,o.__hash=void 0,o):makeOrderedMap(u,_)}function ToKeyedSequence(o,s){this._iter=o,this._useKeys=s,this.size=o.size}function ToIndexedSequence(o){this._iter=o,this.size=o.size}function ToSetSequence(o){this._iter=o,this.size=o.size}function FromEntriesSequence(o){this._iter=o,this.size=o.size}function flipFactory(o){var s=makeSequence(o);return s._iter=o,s.size=o.size,s.flip=function(){return o},s.reverse=function(){var s=o.reverse.apply(this);return s.flip=function(){return o.reverse()},s},s.has=function(s){return o.includes(s)},s.includes=function(s){return o.has(s)},s.cacheResult=cacheResultThrough,s.__iterateUncached=function(s,i){var u=this;return o.__iterate((function(o,i){return!1!==s(i,o,u)}),i)},s.__iteratorUncached=function(s,i){if(s===z){var u=o.__iterator(s,i);return new Iterator((function(){var o=u.next();if(!o.done){var s=o.value[0];o.value[0]=o.value[1],o.value[1]=s}return o}))}return o.__iterator(s===U?V:U,i)},s}function mapFactory(o,s,i){var u=makeSequence(o);return u.size=o.size,u.has=function(s){return o.has(s)},u.get=function(u,_){var w=o.get(u,L);return w===L?_:s.call(i,w,u,o)},u.__iterateUncached=function(u,_){var w=this;return o.__iterate((function(o,_,x){return!1!==u(s.call(i,o,_,x),_,w)}),_)},u.__iteratorUncached=function(u,_){var w=o.__iterator(z,_);return new Iterator((function(){var _=w.next();if(_.done)return _;var x=_.value,C=x[0];return iteratorValue(u,C,s.call(i,x[1],C,o),_)}))},u}function reverseFactory(o,s){var i=makeSequence(o);return i._iter=o,i.size=o.size,i.reverse=function(){return o},o.flip&&(i.flip=function(){var s=flipFactory(o);return s.reverse=function(){return o.flip()},s}),i.get=function(i,u){return o.get(s?i:-1-i,u)},i.has=function(i){return o.has(s?i:-1-i)},i.includes=function(s){return o.includes(s)},i.cacheResult=cacheResultThrough,i.__iterate=function(s,i){var u=this;return o.__iterate((function(o,i){return s(o,i,u)}),!i)},i.__iterator=function(s,i){return o.__iterator(s,!i)},i}function filterFactory(o,s,i,u){var _=makeSequence(o);return u&&(_.has=function(u){var _=o.get(u,L);return _!==L&&!!s.call(i,_,u,o)},_.get=function(u,_){var w=o.get(u,L);return w!==L&&s.call(i,w,u,o)?w:_}),_.__iterateUncached=function(_,w){var x=this,C=0;return o.__iterate((function(o,w,j){if(s.call(i,o,w,j))return C++,_(o,u?w:C-1,x)}),w),C},_.__iteratorUncached=function(_,w){var x=o.__iterator(z,w),C=0;return new Iterator((function(){for(;;){var w=x.next();if(w.done)return w;var j=w.value,L=j[0],B=j[1];if(s.call(i,B,L,o))return iteratorValue(_,u?L:C++,B,w)}}))},_}function countByFactory(o,s,i){var u=Map().asMutable();return o.__iterate((function(_,w){u.update(s.call(i,_,w,o),0,(function(o){return o+1}))})),u.asImmutable()}function groupByFactory(o,s,i){var u=isKeyed(o),_=(isOrdered(o)?OrderedMap():Map()).asMutable();o.__iterate((function(w,x){_.update(s.call(i,w,x,o),(function(o){return(o=o||[]).push(u?[x,w]:w),o}))}));var w=iterableClass(o);return _.map((function(s){return reify(o,w(s))}))}function sliceFactory(o,s,i,u){var _=o.size;if(void 0!==s&&(s|=0),void 0!==i&&(i===1/0?i=_:i|=0),wholeSlice(s,i,_))return o;var w=resolveBegin(s,_),x=resolveEnd(i,_);if(w!=w||x!=x)return sliceFactory(o.toSeq().cacheResult(),s,i,u);var C,j=x-w;j==j&&(C=j<0?0:j);var L=makeSequence(o);return L.size=0===C?C:o.size&&C||void 0,!u&&isSeq(o)&&C>=0&&(L.get=function(s,i){return(s=wrapIndex(this,s))>=0&&sC)return iteratorDone();var o=_.next();return u||s===U?o:iteratorValue(s,j-1,s===V?void 0:o.value[1],o)}))},L}function takeWhileFactory(o,s,i){var u=makeSequence(o);return u.__iterateUncached=function(u,_){var w=this;if(_)return this.cacheResult().__iterate(u,_);var x=0;return o.__iterate((function(o,_,C){return s.call(i,o,_,C)&&++x&&u(o,_,w)})),x},u.__iteratorUncached=function(u,_){var w=this;if(_)return this.cacheResult().__iterator(u,_);var x=o.__iterator(z,_),C=!0;return new Iterator((function(){if(!C)return iteratorDone();var o=x.next();if(o.done)return o;var _=o.value,j=_[0],L=_[1];return s.call(i,L,j,w)?u===z?o:iteratorValue(u,j,L,o):(C=!1,iteratorDone())}))},u}function skipWhileFactory(o,s,i,u){var _=makeSequence(o);return _.__iterateUncached=function(_,w){var x=this;if(w)return this.cacheResult().__iterate(_,w);var C=!0,j=0;return o.__iterate((function(o,w,L){if(!C||!(C=s.call(i,o,w,L)))return j++,_(o,u?w:j-1,x)})),j},_.__iteratorUncached=function(_,w){var x=this;if(w)return this.cacheResult().__iterator(_,w);var C=o.__iterator(z,w),j=!0,L=0;return new Iterator((function(){var o,w,B;do{if((o=C.next()).done)return u||_===U?o:iteratorValue(_,L++,_===V?void 0:o.value[1],o);var $=o.value;w=$[0],B=$[1],j&&(j=s.call(i,B,w,x))}while(j);return _===z?o:iteratorValue(_,w,B,o)}))},_}function concatFactory(o,s){var i=isKeyed(o),u=[o].concat(s).map((function(o){return isIterable(o)?i&&(o=KeyedIterable(o)):o=i?keyedSeqFromValue(o):indexedSeqFromValue(Array.isArray(o)?o:[o]),o})).filter((function(o){return 0!==o.size}));if(0===u.length)return o;if(1===u.length){var _=u[0];if(_===o||i&&isKeyed(_)||isIndexed(o)&&isIndexed(_))return _}var w=new ArraySeq(u);return i?w=w.toKeyedSeq():isIndexed(o)||(w=w.toSetSeq()),(w=w.flatten(!0)).size=u.reduce((function(o,s){if(void 0!==o){var i=s.size;if(void 0!==i)return o+i}}),0),w}function flattenFactory(o,s,i){var u=makeSequence(o);return u.__iterateUncached=function(u,_){var w=0,x=!1;function flatDeep(o,C){var j=this;o.__iterate((function(o,_){return(!s||C0}function zipWithFactory(o,s,i){var u=makeSequence(o);return u.size=new ArraySeq(i).map((function(o){return o.size})).min(),u.__iterate=function(o,s){for(var i,u=this.__iterator(U,s),_=0;!(i=u.next()).done&&!1!==o(i.value,_++,this););return _},u.__iteratorUncached=function(o,u){var _=i.map((function(o){return o=Iterable(o),getIterator(u?o.reverse():o)})),w=0,x=!1;return new Iterator((function(){var i;return x||(i=_.map((function(o){return o.next()})),x=i.some((function(o){return o.done}))),x?iteratorDone():iteratorValue(o,w++,s.apply(null,i.map((function(o){return o.value}))))}))},u}function reify(o,s){return isSeq(o)?s:o.constructor(s)}function validateEntry(o){if(o!==Object(o))throw new TypeError("Expected [K, V] tuple: "+o)}function resolveSize(o){return assertNotInfinite(o.size),ensureSize(o)}function iterableClass(o){return isKeyed(o)?KeyedIterable:isIndexed(o)?IndexedIterable:SetIterable}function makeSequence(o){return Object.create((isKeyed(o)?KeyedSeq:isIndexed(o)?IndexedSeq:SetSeq).prototype)}function cacheResultThrough(){return this._iter.cacheResult?(this._iter.cacheResult(),this.size=this._iter.size,this):Seq.prototype.cacheResult.call(this)}function defaultComparator(o,s){return o>s?1:o=0;i--)s={value:arguments[i],next:s};return this.__ownerID?(this.size=o,this._head=s,this.__hash=void 0,this.__altered=!0,this):makeStack(o,s)},Stack.prototype.pushAll=function(o){if(0===(o=IndexedIterable(o)).size)return this;assertNotInfinite(o.size);var s=this.size,i=this._head;return o.reverse().forEach((function(o){s++,i={value:o,next:i}})),this.__ownerID?(this.size=s,this._head=i,this.__hash=void 0,this.__altered=!0,this):makeStack(s,i)},Stack.prototype.pop=function(){return this.slice(1)},Stack.prototype.unshift=function(){return this.push.apply(this,arguments)},Stack.prototype.unshiftAll=function(o){return this.pushAll(o)},Stack.prototype.shift=function(){return this.pop.apply(this,arguments)},Stack.prototype.clear=function(){return 0===this.size?this:this.__ownerID?(this.size=0,this._head=void 0,this.__hash=void 0,this.__altered=!0,this):emptyStack()},Stack.prototype.slice=function(o,s){if(wholeSlice(o,s,this.size))return this;var i=resolveBegin(o,this.size);if(resolveEnd(s,this.size)!==this.size)return IndexedCollection.prototype.slice.call(this,o,s);for(var u=this.size-i,_=this._head;i--;)_=_.next;return this.__ownerID?(this.size=u,this._head=_,this.__hash=void 0,this.__altered=!0,this):makeStack(u,_)},Stack.prototype.__ensureOwner=function(o){return o===this.__ownerID?this:o?makeStack(this.size,this._head,o,this.__hash):(this.__ownerID=o,this.__altered=!1,this)},Stack.prototype.__iterate=function(o,s){if(s)return this.reverse().__iterate(o);for(var i=0,u=this._head;u&&!1!==o(u.value,i++,this);)u=u.next;return i},Stack.prototype.__iterator=function(o,s){if(s)return this.reverse().__iterator(o);var i=0,u=this._head;return new Iterator((function(){if(u){var s=u.value;return u=u.next,iteratorValue(o,i++,s)}return iteratorDone()}))},Stack.isStack=isStack;var ct,lt="@@__IMMUTABLE_STACK__@@",ut=Stack.prototype;function makeStack(o,s,i,u){var _=Object.create(ut);return _.size=o,_._head=s,_.__ownerID=i,_.__hash=u,_.__altered=!1,_}function emptyStack(){return ct||(ct=makeStack(0))}function mixin(o,s){var keyCopier=function(i){o.prototype[i]=s[i]};return Object.keys(s).forEach(keyCopier),Object.getOwnPropertySymbols&&Object.getOwnPropertySymbols(s).forEach(keyCopier),o}ut[lt]=!0,ut.withMutations=$e.withMutations,ut.asMutable=$e.asMutable,ut.asImmutable=$e.asImmutable,ut.wasAltered=$e.wasAltered,Iterable.Iterator=Iterator,mixin(Iterable,{toArray:function(){assertNotInfinite(this.size);var o=new Array(this.size||0);return this.valueSeq().__iterate((function(s,i){o[i]=s})),o},toIndexedSeq:function(){return new ToIndexedSequence(this)},toJS:function(){return this.toSeq().map((function(o){return o&&"function"==typeof o.toJS?o.toJS():o})).__toJS()},toJSON:function(){return this.toSeq().map((function(o){return o&&"function"==typeof o.toJSON?o.toJSON():o})).__toJS()},toKeyedSeq:function(){return new ToKeyedSequence(this,!0)},toMap:function(){return Map(this.toKeyedSeq())},toObject:function(){assertNotInfinite(this.size);var o={};return this.__iterate((function(s,i){o[i]=s})),o},toOrderedMap:function(){return OrderedMap(this.toKeyedSeq())},toOrderedSet:function(){return OrderedSet(isKeyed(this)?this.valueSeq():this)},toSet:function(){return Set(isKeyed(this)?this.valueSeq():this)},toSetSeq:function(){return new ToSetSequence(this)},toSeq:function(){return isIndexed(this)?this.toIndexedSeq():isKeyed(this)?this.toKeyedSeq():this.toSetSeq()},toStack:function(){return Stack(isKeyed(this)?this.valueSeq():this)},toList:function(){return List(isKeyed(this)?this.valueSeq():this)},toString:function(){return"[Iterable]"},__toString:function(o,s){return 0===this.size?o+s:o+" "+this.toSeq().map(this.__toStringMapper).join(", ")+" "+s},concat:function(){return reify(this,concatFactory(this,o.call(arguments,0)))},includes:function(o){return this.some((function(s){return is(s,o)}))},entries:function(){return this.__iterator(z)},every:function(o,s){assertNotInfinite(this.size);var i=!0;return this.__iterate((function(u,_,w){if(!o.call(s,u,_,w))return i=!1,!1})),i},filter:function(o,s){return reify(this,filterFactory(this,o,s,!0))},find:function(o,s,i){var u=this.findEntry(o,s);return u?u[1]:i},forEach:function(o,s){return assertNotInfinite(this.size),this.__iterate(s?o.bind(s):o)},join:function(o){assertNotInfinite(this.size),o=void 0!==o?""+o:",";var s="",i=!0;return this.__iterate((function(u){i?i=!1:s+=o,s+=null!=u?u.toString():""})),s},keys:function(){return this.__iterator(V)},map:function(o,s){return reify(this,mapFactory(this,o,s))},reduce:function(o,s,i){var u,_;return assertNotInfinite(this.size),arguments.length<2?_=!0:u=s,this.__iterate((function(s,w,x){_?(_=!1,u=s):u=o.call(i,u,s,w,x)})),u},reduceRight:function(o,s,i){var u=this.toKeyedSeq().reverse();return u.reduce.apply(u,arguments)},reverse:function(){return reify(this,reverseFactory(this,!0))},slice:function(o,s){return reify(this,sliceFactory(this,o,s,!0))},some:function(o,s){return!this.every(not(o),s)},sort:function(o){return reify(this,sortFactory(this,o))},values:function(){return this.__iterator(U)},butLast:function(){return this.slice(0,-1)},isEmpty:function(){return void 0!==this.size?0===this.size:!this.some((function(){return!0}))},count:function(o,s){return ensureSize(o?this.toSeq().filter(o,s):this)},countBy:function(o,s){return countByFactory(this,o,s)},equals:function(o){return deepEqual(this,o)},entrySeq:function(){var o=this;if(o._cache)return new ArraySeq(o._cache);var s=o.toSeq().map(entryMapper).toIndexedSeq();return s.fromEntrySeq=function(){return o.toSeq()},s},filterNot:function(o,s){return this.filter(not(o),s)},findEntry:function(o,s,i){var u=i;return this.__iterate((function(i,_,w){if(o.call(s,i,_,w))return u=[_,i],!1})),u},findKey:function(o,s){var i=this.findEntry(o,s);return i&&i[0]},findLast:function(o,s,i){return this.toKeyedSeq().reverse().find(o,s,i)},findLastEntry:function(o,s,i){return this.toKeyedSeq().reverse().findEntry(o,s,i)},findLastKey:function(o,s){return this.toKeyedSeq().reverse().findKey(o,s)},first:function(){return this.find(returnTrue)},flatMap:function(o,s){return reify(this,flatMapFactory(this,o,s))},flatten:function(o){return reify(this,flattenFactory(this,o,!0))},fromEntrySeq:function(){return new FromEntriesSequence(this)},get:function(o,s){return this.find((function(s,i){return is(i,o)}),void 0,s)},getIn:function(o,s){for(var i,u=this,_=forceIterator(o);!(i=_.next()).done;){var w=i.value;if((u=u&&u.get?u.get(w,L):L)===L)return s}return u},groupBy:function(o,s){return groupByFactory(this,o,s)},has:function(o){return this.get(o,L)!==L},hasIn:function(o){return this.getIn(o,L)!==L},isSubset:function(o){return o="function"==typeof o.includes?o:Iterable(o),this.every((function(s){return o.includes(s)}))},isSuperset:function(o){return(o="function"==typeof o.isSubset?o:Iterable(o)).isSubset(this)},keyOf:function(o){return this.findKey((function(s){return is(s,o)}))},keySeq:function(){return this.toSeq().map(keyMapper).toIndexedSeq()},last:function(){return this.toSeq().reverse().first()},lastKeyOf:function(o){return this.toKeyedSeq().reverse().keyOf(o)},max:function(o){return maxFactory(this,o)},maxBy:function(o,s){return maxFactory(this,s,o)},min:function(o){return maxFactory(this,o?neg(o):defaultNegComparator)},minBy:function(o,s){return maxFactory(this,s?neg(s):defaultNegComparator,o)},rest:function(){return this.slice(1)},skip:function(o){return this.slice(Math.max(0,o))},skipLast:function(o){return reify(this,this.toSeq().reverse().skip(o).reverse())},skipWhile:function(o,s){return reify(this,skipWhileFactory(this,o,s,!0))},skipUntil:function(o,s){return this.skipWhile(not(o),s)},sortBy:function(o,s){return reify(this,sortFactory(this,s,o))},take:function(o){return this.slice(0,Math.max(0,o))},takeLast:function(o){return reify(this,this.toSeq().reverse().take(o).reverse())},takeWhile:function(o,s){return reify(this,takeWhileFactory(this,o,s))},takeUntil:function(o,s){return this.takeWhile(not(o),s)},valueSeq:function(){return this.toIndexedSeq()},hashCode:function(){return this.__hash||(this.__hash=hashIterable(this))}});var pt=Iterable.prototype;pt[s]=!0,pt[ee]=pt.values,pt.__toJS=pt.toArray,pt.__toStringMapper=quoteString,pt.inspect=pt.toSource=function(){return this.toString()},pt.chain=pt.flatMap,pt.contains=pt.includes,mixin(KeyedIterable,{flip:function(){return reify(this,flipFactory(this))},mapEntries:function(o,s){var i=this,u=0;return reify(this,this.toSeq().map((function(_,w){return o.call(s,[w,_],u++,i)})).fromEntrySeq())},mapKeys:function(o,s){var i=this;return reify(this,this.toSeq().flip().map((function(u,_){return o.call(s,u,_,i)})).flip())}});var ht=KeyedIterable.prototype;function keyMapper(o,s){return s}function entryMapper(o,s){return[s,o]}function not(o){return function(){return!o.apply(this,arguments)}}function neg(o){return function(){return-o.apply(this,arguments)}}function quoteString(o){return"string"==typeof o?JSON.stringify(o):String(o)}function defaultZipper(){return arrCopy(arguments)}function defaultNegComparator(o,s){return os?-1:0}function hashIterable(o){if(o.size===1/0)return 0;var s=isOrdered(o),i=isKeyed(o),u=s?1:0;return murmurHashOfSize(o.__iterate(i?s?function(o,s){u=31*u+hashMerge(hash(o),hash(s))|0}:function(o,s){u=u+hashMerge(hash(o),hash(s))|0}:s?function(o){u=31*u+hash(o)|0}:function(o){u=u+hash(o)|0}),u)}function murmurHashOfSize(o,s){return s=pe(s,3432918353),s=pe(s<<15|s>>>-15,461845907),s=pe(s<<13|s>>>-13,5),s=pe((s=s+3864292196^o)^s>>>16,2246822507),s=smi((s=pe(s^s>>>13,3266489909))^s>>>16)}function hashMerge(o,s){return o^s+2654435769+(o<<6)+(o>>2)}return ht[i]=!0,ht[ee]=pt.entries,ht.__toJS=pt.toObject,ht.__toStringMapper=function(o,s){return JSON.stringify(s)+": "+quoteString(o)},mixin(IndexedIterable,{toKeyedSeq:function(){return new ToKeyedSequence(this,!1)},filter:function(o,s){return reify(this,filterFactory(this,o,s,!1))},findIndex:function(o,s){var i=this.findEntry(o,s);return i?i[0]:-1},indexOf:function(o){var s=this.keyOf(o);return void 0===s?-1:s},lastIndexOf:function(o){var s=this.lastKeyOf(o);return void 0===s?-1:s},reverse:function(){return reify(this,reverseFactory(this,!1))},slice:function(o,s){return reify(this,sliceFactory(this,o,s,!1))},splice:function(o,s){var i=arguments.length;if(s=Math.max(0|s,0),0===i||2===i&&!s)return this;o=resolveBegin(o,o<0?this.count():this.size);var u=this.slice(0,o);return reify(this,1===i?u:u.concat(arrCopy(arguments,2),this.slice(o+s)))},findLastIndex:function(o,s){var i=this.findLastEntry(o,s);return i?i[0]:-1},first:function(){return this.get(0)},flatten:function(o){return reify(this,flattenFactory(this,o,!1))},get:function(o,s){return(o=wrapIndex(this,o))<0||this.size===1/0||void 0!==this.size&&o>this.size?s:this.find((function(s,i){return i===o}),void 0,s)},has:function(o){return(o=wrapIndex(this,o))>=0&&(void 0!==this.size?this.size===1/0||o{"function"==typeof Object.create?o.exports=function inherits(o,s){s&&(o.super_=s,o.prototype=Object.create(s.prototype,{constructor:{value:o,enumerable:!1,writable:!0,configurable:!0}}))}:o.exports=function inherits(o,s){if(s){o.super_=s;var TempCtor=function(){};TempCtor.prototype=s.prototype,o.prototype=new TempCtor,o.prototype.constructor=o}}},5419:o=>{o.exports=function(o,s,i,u){var _=new Blob(void 0!==u?[u,o]:[o],{type:i||"application/octet-stream"});if(void 0!==window.navigator.msSaveBlob)window.navigator.msSaveBlob(_,s);else{var w=window.URL&&window.URL.createObjectURL?window.URL.createObjectURL(_):window.webkitURL.createObjectURL(_),x=document.createElement("a");x.style.display="none",x.href=w,x.setAttribute("download",s),void 0===x.download&&x.setAttribute("target","_blank"),document.body.appendChild(x),x.click(),setTimeout((function(){document.body.removeChild(x),window.URL.revokeObjectURL(w)}),200)}}},20181:(o,s,i)=>{var u=NaN,_="[object Symbol]",w=/^\s+|\s+$/g,x=/^[-+]0x[0-9a-f]+$/i,C=/^0b[01]+$/i,j=/^0o[0-7]+$/i,L=parseInt,B="object"==typeof i.g&&i.g&&i.g.Object===Object&&i.g,$="object"==typeof self&&self&&self.Object===Object&&self,V=B||$||Function("return this")(),U=Object.prototype.toString,z=Math.max,Y=Math.min,now=function(){return V.Date.now()};function isObject(o){var s=typeof o;return!!o&&("object"==s||"function"==s)}function toNumber(o){if("number"==typeof o)return o;if(function isSymbol(o){return"symbol"==typeof o||function isObjectLike(o){return!!o&&"object"==typeof o}(o)&&U.call(o)==_}(o))return u;if(isObject(o)){var s="function"==typeof o.valueOf?o.valueOf():o;o=isObject(s)?s+"":s}if("string"!=typeof o)return 0===o?o:+o;o=o.replace(w,"");var i=C.test(o);return i||j.test(o)?L(o.slice(2),i?2:8):x.test(o)?u:+o}o.exports=function debounce(o,s,i){var u,_,w,x,C,j,L=0,B=!1,$=!1,V=!0;if("function"!=typeof o)throw new TypeError("Expected a function");function invokeFunc(s){var i=u,w=_;return u=_=void 0,L=s,x=o.apply(w,i)}function shouldInvoke(o){var i=o-j;return void 0===j||i>=s||i<0||$&&o-L>=w}function timerExpired(){var o=now();if(shouldInvoke(o))return trailingEdge(o);C=setTimeout(timerExpired,function remainingWait(o){var i=s-(o-j);return $?Y(i,w-(o-L)):i}(o))}function trailingEdge(o){return C=void 0,V&&u?invokeFunc(o):(u=_=void 0,x)}function debounced(){var o=now(),i=shouldInvoke(o);if(u=arguments,_=this,j=o,i){if(void 0===C)return function leadingEdge(o){return L=o,C=setTimeout(timerExpired,s),B?invokeFunc(o):x}(j);if($)return C=setTimeout(timerExpired,s),invokeFunc(j)}return void 0===C&&(C=setTimeout(timerExpired,s)),x}return s=toNumber(s)||0,isObject(i)&&(B=!!i.leading,w=($="maxWait"in i)?z(toNumber(i.maxWait)||0,s):w,V="trailing"in i?!!i.trailing:V),debounced.cancel=function cancel(){void 0!==C&&clearTimeout(C),L=0,u=j=_=C=void 0},debounced.flush=function flush(){return void 0===C?x:trailingEdge(now())},debounced}},55580:(o,s,i)=>{var u=i(56110)(i(9325),"DataView");o.exports=u},21549:(o,s,i)=>{var u=i(22032),_=i(63862),w=i(66721),x=i(12749),C=i(35749);function Hash(o){var s=-1,i=null==o?0:o.length;for(this.clear();++s{var u=i(39344),_=i(94033);function LazyWrapper(o){this.__wrapped__=o,this.__actions__=[],this.__dir__=1,this.__filtered__=!1,this.__iteratees__=[],this.__takeCount__=4294967295,this.__views__=[]}LazyWrapper.prototype=u(_.prototype),LazyWrapper.prototype.constructor=LazyWrapper,o.exports=LazyWrapper},80079:(o,s,i)=>{var u=i(63702),_=i(70080),w=i(24739),x=i(48655),C=i(31175);function ListCache(o){var s=-1,i=null==o?0:o.length;for(this.clear();++s{var u=i(39344),_=i(94033);function LodashWrapper(o,s){this.__wrapped__=o,this.__actions__=[],this.__chain__=!!s,this.__index__=0,this.__values__=void 0}LodashWrapper.prototype=u(_.prototype),LodashWrapper.prototype.constructor=LodashWrapper,o.exports=LodashWrapper},68223:(o,s,i)=>{var u=i(56110)(i(9325),"Map");o.exports=u},53661:(o,s,i)=>{var u=i(63040),_=i(17670),w=i(90289),x=i(4509),C=i(72949);function MapCache(o){var s=-1,i=null==o?0:o.length;for(this.clear();++s{var u=i(56110)(i(9325),"Promise");o.exports=u},76545:(o,s,i)=>{var u=i(56110)(i(9325),"Set");o.exports=u},38859:(o,s,i)=>{var u=i(53661),_=i(31380),w=i(51459);function SetCache(o){var s=-1,i=null==o?0:o.length;for(this.__data__=new u;++s{var u=i(80079),_=i(51420),w=i(90938),x=i(63605),C=i(29817),j=i(80945);function Stack(o){var s=this.__data__=new u(o);this.size=s.size}Stack.prototype.clear=_,Stack.prototype.delete=w,Stack.prototype.get=x,Stack.prototype.has=C,Stack.prototype.set=j,o.exports=Stack},51873:(o,s,i)=>{var u=i(9325).Symbol;o.exports=u},37828:(o,s,i)=>{var u=i(9325).Uint8Array;o.exports=u},28303:(o,s,i)=>{var u=i(56110)(i(9325),"WeakMap");o.exports=u},91033:o=>{o.exports=function apply(o,s,i){switch(i.length){case 0:return o.call(s);case 1:return o.call(s,i[0]);case 2:return o.call(s,i[0],i[1]);case 3:return o.call(s,i[0],i[1],i[2])}return o.apply(s,i)}},83729:o=>{o.exports=function arrayEach(o,s){for(var i=-1,u=null==o?0:o.length;++i{o.exports=function arrayFilter(o,s){for(var i=-1,u=null==o?0:o.length,_=0,w=[];++i{var u=i(96131);o.exports=function arrayIncludes(o,s){return!!(null==o?0:o.length)&&u(o,s,0)>-1}},70695:(o,s,i)=>{var u=i(78096),_=i(72428),w=i(56449),x=i(3656),C=i(30361),j=i(37167),L=Object.prototype.hasOwnProperty;o.exports=function arrayLikeKeys(o,s){var i=w(o),B=!i&&_(o),$=!i&&!B&&x(o),V=!i&&!B&&!$&&j(o),U=i||B||$||V,z=U?u(o.length,String):[],Y=z.length;for(var Z in o)!s&&!L.call(o,Z)||U&&("length"==Z||$&&("offset"==Z||"parent"==Z)||V&&("buffer"==Z||"byteLength"==Z||"byteOffset"==Z)||C(Z,Y))||z.push(Z);return z}},34932:o=>{o.exports=function arrayMap(o,s){for(var i=-1,u=null==o?0:o.length,_=Array(u);++i{o.exports=function arrayPush(o,s){for(var i=-1,u=s.length,_=o.length;++i{o.exports=function arrayReduce(o,s,i,u){var _=-1,w=null==o?0:o.length;for(u&&w&&(i=o[++_]);++_{o.exports=function arraySome(o,s){for(var i=-1,u=null==o?0:o.length;++i{o.exports=function asciiToArray(o){return o.split("")}},1733:o=>{var s=/[^\x00-\x2f\x3a-\x40\x5b-\x60\x7b-\x7f]+/g;o.exports=function asciiWords(o){return o.match(s)||[]}},87805:(o,s,i)=>{var u=i(43360),_=i(75288);o.exports=function assignMergeValue(o,s,i){(void 0!==i&&!_(o[s],i)||void 0===i&&!(s in o))&&u(o,s,i)}},16547:(o,s,i)=>{var u=i(43360),_=i(75288),w=Object.prototype.hasOwnProperty;o.exports=function assignValue(o,s,i){var x=o[s];w.call(o,s)&&_(x,i)&&(void 0!==i||s in o)||u(o,s,i)}},26025:(o,s,i)=>{var u=i(75288);o.exports=function assocIndexOf(o,s){for(var i=o.length;i--;)if(u(o[i][0],s))return i;return-1}},74733:(o,s,i)=>{var u=i(21791),_=i(95950);o.exports=function baseAssign(o,s){return o&&u(s,_(s),o)}},43838:(o,s,i)=>{var u=i(21791),_=i(37241);o.exports=function baseAssignIn(o,s){return o&&u(s,_(s),o)}},43360:(o,s,i)=>{var u=i(93243);o.exports=function baseAssignValue(o,s,i){"__proto__"==s&&u?u(o,s,{configurable:!0,enumerable:!0,value:i,writable:!0}):o[s]=i}},9999:(o,s,i)=>{var u=i(37217),_=i(83729),w=i(16547),x=i(74733),C=i(43838),j=i(93290),L=i(23007),B=i(92271),$=i(48948),V=i(50002),U=i(83349),z=i(5861),Y=i(76189),Z=i(77199),ee=i(35529),ie=i(56449),ae=i(3656),ce=i(87730),le=i(23805),pe=i(38440),de=i(95950),fe=i(37241),ye="[object Arguments]",be="[object Function]",_e="[object Object]",we={};we[ye]=we["[object Array]"]=we["[object ArrayBuffer]"]=we["[object DataView]"]=we["[object Boolean]"]=we["[object Date]"]=we["[object Float32Array]"]=we["[object Float64Array]"]=we["[object Int8Array]"]=we["[object Int16Array]"]=we["[object Int32Array]"]=we["[object Map]"]=we["[object Number]"]=we[_e]=we["[object RegExp]"]=we["[object Set]"]=we["[object String]"]=we["[object Symbol]"]=we["[object Uint8Array]"]=we["[object Uint8ClampedArray]"]=we["[object Uint16Array]"]=we["[object Uint32Array]"]=!0,we["[object Error]"]=we[be]=we["[object WeakMap]"]=!1,o.exports=function baseClone(o,s,i,Se,xe,Pe){var Te,Re=1&s,qe=2&s,$e=4&s;if(i&&(Te=xe?i(o,Se,xe,Pe):i(o)),void 0!==Te)return Te;if(!le(o))return o;var ze=ie(o);if(ze){if(Te=Y(o),!Re)return L(o,Te)}else{var We=z(o),He=We==be||"[object GeneratorFunction]"==We;if(ae(o))return j(o,Re);if(We==_e||We==ye||He&&!xe){if(Te=qe||He?{}:ee(o),!Re)return qe?$(o,C(Te,o)):B(o,x(Te,o))}else{if(!we[We])return xe?o:{};Te=Z(o,We,Re)}}Pe||(Pe=new u);var Ye=Pe.get(o);if(Ye)return Ye;Pe.set(o,Te),pe(o)?o.forEach((function(u){Te.add(baseClone(u,s,i,u,o,Pe))})):ce(o)&&o.forEach((function(u,_){Te.set(_,baseClone(u,s,i,_,o,Pe))}));var Xe=ze?void 0:($e?qe?U:V:qe?fe:de)(o);return _(Xe||o,(function(u,_){Xe&&(u=o[_=u]),w(Te,_,baseClone(u,s,i,_,o,Pe))})),Te}},39344:(o,s,i)=>{var u=i(23805),_=Object.create,w=function(){function object(){}return function(o){if(!u(o))return{};if(_)return _(o);object.prototype=o;var s=new object;return object.prototype=void 0,s}}();o.exports=w},80909:(o,s,i)=>{var u=i(30641),_=i(38329)(u);o.exports=_},2523:o=>{o.exports=function baseFindIndex(o,s,i,u){for(var _=o.length,w=i+(u?1:-1);u?w--:++w<_;)if(s(o[w],w,o))return w;return-1}},83120:(o,s,i)=>{var u=i(14528),_=i(45891);o.exports=function baseFlatten(o,s,i,w,x){var C=-1,j=o.length;for(i||(i=_),x||(x=[]);++C0&&i(L)?s>1?baseFlatten(L,s-1,i,w,x):u(x,L):w||(x[x.length]=L)}return x}},86649:(o,s,i)=>{var u=i(83221)();o.exports=u},30641:(o,s,i)=>{var u=i(86649),_=i(95950);o.exports=function baseForOwn(o,s){return o&&u(o,s,_)}},47422:(o,s,i)=>{var u=i(31769),_=i(77797);o.exports=function baseGet(o,s){for(var i=0,w=(s=u(s,o)).length;null!=o&&i{var u=i(14528),_=i(56449);o.exports=function baseGetAllKeys(o,s,i){var w=s(o);return _(o)?w:u(w,i(o))}},72552:(o,s,i)=>{var u=i(51873),_=i(659),w=i(59350),x=u?u.toStringTag:void 0;o.exports=function baseGetTag(o){return null==o?void 0===o?"[object Undefined]":"[object Null]":x&&x in Object(o)?_(o):w(o)}},20426:o=>{var s=Object.prototype.hasOwnProperty;o.exports=function baseHas(o,i){return null!=o&&s.call(o,i)}},28077:o=>{o.exports=function baseHasIn(o,s){return null!=o&&s in Object(o)}},96131:(o,s,i)=>{var u=i(2523),_=i(85463),w=i(76959);o.exports=function baseIndexOf(o,s,i){return s==s?w(o,s,i):u(o,_,i)}},27534:(o,s,i)=>{var u=i(72552),_=i(40346);o.exports=function baseIsArguments(o){return _(o)&&"[object Arguments]"==u(o)}},60270:(o,s,i)=>{var u=i(87068),_=i(40346);o.exports=function baseIsEqual(o,s,i,w,x){return o===s||(null==o||null==s||!_(o)&&!_(s)?o!=o&&s!=s:u(o,s,i,w,baseIsEqual,x))}},87068:(o,s,i)=>{var u=i(37217),_=i(25911),w=i(21986),x=i(50689),C=i(5861),j=i(56449),L=i(3656),B=i(37167),$="[object Arguments]",V="[object Array]",U="[object Object]",z=Object.prototype.hasOwnProperty;o.exports=function baseIsEqualDeep(o,s,i,Y,Z,ee){var ie=j(o),ae=j(s),ce=ie?V:C(o),le=ae?V:C(s),pe=(ce=ce==$?U:ce)==U,de=(le=le==$?U:le)==U,fe=ce==le;if(fe&&L(o)){if(!L(s))return!1;ie=!0,pe=!1}if(fe&&!pe)return ee||(ee=new u),ie||B(o)?_(o,s,i,Y,Z,ee):w(o,s,ce,i,Y,Z,ee);if(!(1&i)){var ye=pe&&z.call(o,"__wrapped__"),be=de&&z.call(s,"__wrapped__");if(ye||be){var _e=ye?o.value():o,we=be?s.value():s;return ee||(ee=new u),Z(_e,we,i,Y,ee)}}return!!fe&&(ee||(ee=new u),x(o,s,i,Y,Z,ee))}},29172:(o,s,i)=>{var u=i(5861),_=i(40346);o.exports=function baseIsMap(o){return _(o)&&"[object Map]"==u(o)}},41799:(o,s,i)=>{var u=i(37217),_=i(60270);o.exports=function baseIsMatch(o,s,i,w){var x=i.length,C=x,j=!w;if(null==o)return!C;for(o=Object(o);x--;){var L=i[x];if(j&&L[2]?L[1]!==o[L[0]]:!(L[0]in o))return!1}for(;++x{o.exports=function baseIsNaN(o){return o!=o}},45083:(o,s,i)=>{var u=i(1882),_=i(87296),w=i(23805),x=i(47473),C=/^\[object .+?Constructor\]$/,j=Function.prototype,L=Object.prototype,B=j.toString,$=L.hasOwnProperty,V=RegExp("^"+B.call($).replace(/[\\^$.*+?()[\]{}|]/g,"\\$&").replace(/hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g,"$1.*?")+"$");o.exports=function baseIsNative(o){return!(!w(o)||_(o))&&(u(o)?V:C).test(x(o))}},16038:(o,s,i)=>{var u=i(5861),_=i(40346);o.exports=function baseIsSet(o){return _(o)&&"[object Set]"==u(o)}},4901:(o,s,i)=>{var u=i(72552),_=i(30294),w=i(40346),x={};x["[object Float32Array]"]=x["[object Float64Array]"]=x["[object Int8Array]"]=x["[object Int16Array]"]=x["[object Int32Array]"]=x["[object Uint8Array]"]=x["[object Uint8ClampedArray]"]=x["[object Uint16Array]"]=x["[object Uint32Array]"]=!0,x["[object Arguments]"]=x["[object Array]"]=x["[object ArrayBuffer]"]=x["[object Boolean]"]=x["[object DataView]"]=x["[object Date]"]=x["[object Error]"]=x["[object Function]"]=x["[object Map]"]=x["[object Number]"]=x["[object Object]"]=x["[object RegExp]"]=x["[object Set]"]=x["[object String]"]=x["[object WeakMap]"]=!1,o.exports=function baseIsTypedArray(o){return w(o)&&_(o.length)&&!!x[u(o)]}},15389:(o,s,i)=>{var u=i(93663),_=i(87978),w=i(83488),x=i(56449),C=i(50583);o.exports=function baseIteratee(o){return"function"==typeof o?o:null==o?w:"object"==typeof o?x(o)?_(o[0],o[1]):u(o):C(o)}},88984:(o,s,i)=>{var u=i(55527),_=i(3650),w=Object.prototype.hasOwnProperty;o.exports=function baseKeys(o){if(!u(o))return _(o);var s=[];for(var i in Object(o))w.call(o,i)&&"constructor"!=i&&s.push(i);return s}},72903:(o,s,i)=>{var u=i(23805),_=i(55527),w=i(90181),x=Object.prototype.hasOwnProperty;o.exports=function baseKeysIn(o){if(!u(o))return w(o);var s=_(o),i=[];for(var C in o)("constructor"!=C||!s&&x.call(o,C))&&i.push(C);return i}},94033:o=>{o.exports=function baseLodash(){}},93663:(o,s,i)=>{var u=i(41799),_=i(10776),w=i(67197);o.exports=function baseMatches(o){var s=_(o);return 1==s.length&&s[0][2]?w(s[0][0],s[0][1]):function(i){return i===o||u(i,o,s)}}},87978:(o,s,i)=>{var u=i(60270),_=i(58156),w=i(80631),x=i(28586),C=i(30756),j=i(67197),L=i(77797);o.exports=function baseMatchesProperty(o,s){return x(o)&&C(s)?j(L(o),s):function(i){var x=_(i,o);return void 0===x&&x===s?w(i,o):u(s,x,3)}}},85250:(o,s,i)=>{var u=i(37217),_=i(87805),w=i(86649),x=i(42824),C=i(23805),j=i(37241),L=i(14974);o.exports=function baseMerge(o,s,i,B,$){o!==s&&w(s,(function(w,j){if($||($=new u),C(w))x(o,s,j,i,baseMerge,B,$);else{var V=B?B(L(o,j),w,j+"",o,s,$):void 0;void 0===V&&(V=w),_(o,j,V)}}),j)}},42824:(o,s,i)=>{var u=i(87805),_=i(93290),w=i(71961),x=i(23007),C=i(35529),j=i(72428),L=i(56449),B=i(83693),$=i(3656),V=i(1882),U=i(23805),z=i(11331),Y=i(37167),Z=i(14974),ee=i(69884);o.exports=function baseMergeDeep(o,s,i,ie,ae,ce,le){var pe=Z(o,i),de=Z(s,i),fe=le.get(de);if(fe)u(o,i,fe);else{var ye=ce?ce(pe,de,i+"",o,s,le):void 0,be=void 0===ye;if(be){var _e=L(de),we=!_e&&$(de),Se=!_e&&!we&&Y(de);ye=de,_e||we||Se?L(pe)?ye=pe:B(pe)?ye=x(pe):we?(be=!1,ye=_(de,!0)):Se?(be=!1,ye=w(de,!0)):ye=[]:z(de)||j(de)?(ye=pe,j(pe)?ye=ee(pe):U(pe)&&!V(pe)||(ye=C(de))):be=!1}be&&(le.set(de,ye),ae(ye,de,ie,ce,le),le.delete(de)),u(o,i,ye)}}},47237:o=>{o.exports=function baseProperty(o){return function(s){return null==s?void 0:s[o]}}},17255:(o,s,i)=>{var u=i(47422);o.exports=function basePropertyDeep(o){return function(s){return u(s,o)}}},54552:o=>{o.exports=function basePropertyOf(o){return function(s){return null==o?void 0:o[s]}}},85558:o=>{o.exports=function baseReduce(o,s,i,u,_){return _(o,(function(o,_,w){i=u?(u=!1,o):s(i,o,_,w)})),i}},69302:(o,s,i)=>{var u=i(83488),_=i(56757),w=i(32865);o.exports=function baseRest(o,s){return w(_(o,s,u),o+"")}},73170:(o,s,i)=>{var u=i(16547),_=i(31769),w=i(30361),x=i(23805),C=i(77797);o.exports=function baseSet(o,s,i,j){if(!x(o))return o;for(var L=-1,B=(s=_(s,o)).length,$=B-1,V=o;null!=V&&++L{var u=i(83488),_=i(48152),w=_?function(o,s){return _.set(o,s),o}:u;o.exports=w},19570:(o,s,i)=>{var u=i(37334),_=i(93243),w=i(83488),x=_?function(o,s){return _(o,"toString",{configurable:!0,enumerable:!1,value:u(s),writable:!0})}:w;o.exports=x},25160:o=>{o.exports=function baseSlice(o,s,i){var u=-1,_=o.length;s<0&&(s=-s>_?0:_+s),(i=i>_?_:i)<0&&(i+=_),_=s>i?0:i-s>>>0,s>>>=0;for(var w=Array(_);++u<_;)w[u]=o[u+s];return w}},90916:(o,s,i)=>{var u=i(80909);o.exports=function baseSome(o,s){var i;return u(o,(function(o,u,_){return!(i=s(o,u,_))})),!!i}},78096:o=>{o.exports=function baseTimes(o,s){for(var i=-1,u=Array(o);++i{var u=i(51873),_=i(34932),w=i(56449),x=i(44394),C=u?u.prototype:void 0,j=C?C.toString:void 0;o.exports=function baseToString(o){if("string"==typeof o)return o;if(w(o))return _(o,baseToString)+"";if(x(o))return j?j.call(o):"";var s=o+"";return"0"==s&&1/o==-1/0?"-0":s}},54128:(o,s,i)=>{var u=i(31800),_=/^\s+/;o.exports=function baseTrim(o){return o?o.slice(0,u(o)+1).replace(_,""):o}},27301:o=>{o.exports=function baseUnary(o){return function(s){return o(s)}}},19931:(o,s,i)=>{var u=i(31769),_=i(68090),w=i(68969),x=i(77797);o.exports=function baseUnset(o,s){return s=u(s,o),null==(o=w(o,s))||delete o[x(_(s))]}},51234:o=>{o.exports=function baseZipObject(o,s,i){for(var u=-1,_=o.length,w=s.length,x={};++u<_;){var C=u{o.exports=function cacheHas(o,s){return o.has(s)}},31769:(o,s,i)=>{var u=i(56449),_=i(28586),w=i(61802),x=i(13222);o.exports=function castPath(o,s){return u(o)?o:_(o,s)?[o]:w(x(o))}},28754:(o,s,i)=>{var u=i(25160);o.exports=function castSlice(o,s,i){var _=o.length;return i=void 0===i?_:i,!s&&i>=_?o:u(o,s,i)}},49653:(o,s,i)=>{var u=i(37828);o.exports=function cloneArrayBuffer(o){var s=new o.constructor(o.byteLength);return new u(s).set(new u(o)),s}},93290:(o,s,i)=>{o=i.nmd(o);var u=i(9325),_=s&&!s.nodeType&&s,w=_&&o&&!o.nodeType&&o,x=w&&w.exports===_?u.Buffer:void 0,C=x?x.allocUnsafe:void 0;o.exports=function cloneBuffer(o,s){if(s)return o.slice();var i=o.length,u=C?C(i):new o.constructor(i);return o.copy(u),u}},76169:(o,s,i)=>{var u=i(49653);o.exports=function cloneDataView(o,s){var i=s?u(o.buffer):o.buffer;return new o.constructor(i,o.byteOffset,o.byteLength)}},73201:o=>{var s=/\w*$/;o.exports=function cloneRegExp(o){var i=new o.constructor(o.source,s.exec(o));return i.lastIndex=o.lastIndex,i}},93736:(o,s,i)=>{var u=i(51873),_=u?u.prototype:void 0,w=_?_.valueOf:void 0;o.exports=function cloneSymbol(o){return w?Object(w.call(o)):{}}},71961:(o,s,i)=>{var u=i(49653);o.exports=function cloneTypedArray(o,s){var i=s?u(o.buffer):o.buffer;return new o.constructor(i,o.byteOffset,o.length)}},91596:o=>{var s=Math.max;o.exports=function composeArgs(o,i,u,_){for(var w=-1,x=o.length,C=u.length,j=-1,L=i.length,B=s(x-C,0),$=Array(L+B),V=!_;++j{var s=Math.max;o.exports=function composeArgsRight(o,i,u,_){for(var w=-1,x=o.length,C=-1,j=u.length,L=-1,B=i.length,$=s(x-j,0),V=Array($+B),U=!_;++w<$;)V[w]=o[w];for(var z=w;++L{o.exports=function copyArray(o,s){var i=-1,u=o.length;for(s||(s=Array(u));++i{var u=i(16547),_=i(43360);o.exports=function copyObject(o,s,i,w){var x=!i;i||(i={});for(var C=-1,j=s.length;++C{var u=i(21791),_=i(4664);o.exports=function copySymbols(o,s){return u(o,_(o),s)}},48948:(o,s,i)=>{var u=i(21791),_=i(86375);o.exports=function copySymbolsIn(o,s){return u(o,_(o),s)}},55481:(o,s,i)=>{var u=i(9325)["__core-js_shared__"];o.exports=u},58523:o=>{o.exports=function countHolders(o,s){for(var i=o.length,u=0;i--;)o[i]===s&&++u;return u}},20999:(o,s,i)=>{var u=i(69302),_=i(36800);o.exports=function createAssigner(o){return u((function(s,i){var u=-1,w=i.length,x=w>1?i[w-1]:void 0,C=w>2?i[2]:void 0;for(x=o.length>3&&"function"==typeof x?(w--,x):void 0,C&&_(i[0],i[1],C)&&(x=w<3?void 0:x,w=1),s=Object(s);++u{var u=i(64894);o.exports=function createBaseEach(o,s){return function(i,_){if(null==i)return i;if(!u(i))return o(i,_);for(var w=i.length,x=s?w:-1,C=Object(i);(s?x--:++x{o.exports=function createBaseFor(o){return function(s,i,u){for(var _=-1,w=Object(s),x=u(s),C=x.length;C--;){var j=x[o?C:++_];if(!1===i(w[j],j,w))break}return s}}},11842:(o,s,i)=>{var u=i(82819),_=i(9325);o.exports=function createBind(o,s,i){var w=1&s,x=u(o);return function wrapper(){return(this&&this!==_&&this instanceof wrapper?x:o).apply(w?i:this,arguments)}}},12507:(o,s,i)=>{var u=i(28754),_=i(49698),w=i(63912),x=i(13222);o.exports=function createCaseFirst(o){return function(s){s=x(s);var i=_(s)?w(s):void 0,C=i?i[0]:s.charAt(0),j=i?u(i,1).join(""):s.slice(1);return C[o]()+j}}},45539:(o,s,i)=>{var u=i(40882),_=i(50828),w=i(66645),x=RegExp("['’]","g");o.exports=function createCompounder(o){return function(s){return u(w(_(s).replace(x,"")),o,"")}}},82819:(o,s,i)=>{var u=i(39344),_=i(23805);o.exports=function createCtor(o){return function(){var s=arguments;switch(s.length){case 0:return new o;case 1:return new o(s[0]);case 2:return new o(s[0],s[1]);case 3:return new o(s[0],s[1],s[2]);case 4:return new o(s[0],s[1],s[2],s[3]);case 5:return new o(s[0],s[1],s[2],s[3],s[4]);case 6:return new o(s[0],s[1],s[2],s[3],s[4],s[5]);case 7:return new o(s[0],s[1],s[2],s[3],s[4],s[5],s[6])}var i=u(o.prototype),w=o.apply(i,s);return _(w)?w:i}}},77078:(o,s,i)=>{var u=i(91033),_=i(82819),w=i(37471),x=i(18073),C=i(11287),j=i(36306),L=i(9325);o.exports=function createCurry(o,s,i){var B=_(o);return function wrapper(){for(var _=arguments.length,$=Array(_),V=_,U=C(wrapper);V--;)$[V]=arguments[V];var z=_<3&&$[0]!==U&&$[_-1]!==U?[]:j($,U);return(_-=z.length){var u=i(15389),_=i(64894),w=i(95950);o.exports=function createFind(o){return function(s,i,x){var C=Object(s);if(!_(s)){var j=u(i,3);s=w(s),i=function(o){return j(C[o],o,C)}}var L=o(s,i,x);return L>-1?C[j?s[L]:L]:void 0}}},37471:(o,s,i)=>{var u=i(91596),_=i(53320),w=i(58523),x=i(82819),C=i(18073),j=i(11287),L=i(68294),B=i(36306),$=i(9325);o.exports=function createHybrid(o,s,i,V,U,z,Y,Z,ee,ie){var ae=128&s,ce=1&s,le=2&s,pe=24&s,de=512&s,fe=le?void 0:x(o);return function wrapper(){for(var ye=arguments.length,be=Array(ye),_e=ye;_e--;)be[_e]=arguments[_e];if(pe)var we=j(wrapper),Se=w(be,we);if(V&&(be=u(be,V,U,pe)),z&&(be=_(be,z,Y,pe)),ye-=Se,pe&&ye1&&be.reverse(),ae&&ee{var u=i(91033),_=i(82819),w=i(9325);o.exports=function createPartial(o,s,i,x){var C=1&s,j=_(o);return function wrapper(){for(var s=-1,_=arguments.length,L=-1,B=x.length,$=Array(B+_),V=this&&this!==w&&this instanceof wrapper?j:o;++L{var u=i(85087),_=i(54641),w=i(70981);o.exports=function createRecurry(o,s,i,x,C,j,L,B,$,V){var U=8&s;s|=U?32:64,4&(s&=~(U?64:32))||(s&=-4);var z=[o,s,C,U?j:void 0,U?L:void 0,U?void 0:j,U?void 0:L,B,$,V],Y=i.apply(void 0,z);return u(o)&&_(Y,z),Y.placeholder=x,w(Y,o,s)}},66977:(o,s,i)=>{var u=i(68882),_=i(11842),w=i(77078),x=i(37471),C=i(24168),j=i(37381),L=i(3209),B=i(54641),$=i(70981),V=i(61489),U=Math.max;o.exports=function createWrap(o,s,i,z,Y,Z,ee,ie){var ae=2&s;if(!ae&&"function"!=typeof o)throw new TypeError("Expected a function");var ce=z?z.length:0;if(ce||(s&=-97,z=Y=void 0),ee=void 0===ee?ee:U(V(ee),0),ie=void 0===ie?ie:V(ie),ce-=Y?Y.length:0,64&s){var le=z,pe=Y;z=Y=void 0}var de=ae?void 0:j(o),fe=[o,s,i,z,Y,le,pe,Z,ee,ie];if(de&&L(fe,de),o=fe[0],s=fe[1],i=fe[2],z=fe[3],Y=fe[4],!(ie=fe[9]=void 0===fe[9]?ae?0:o.length:U(fe[9]-ce,0))&&24&s&&(s&=-25),s&&1!=s)ye=8==s||16==s?w(o,s,ie):32!=s&&33!=s||Y.length?x.apply(void 0,fe):C(o,s,i,z);else var ye=_(o,s,i);return $((de?u:B)(ye,fe),o,s)}},53138:(o,s,i)=>{var u=i(11331);o.exports=function customOmitClone(o){return u(o)?void 0:o}},24647:(o,s,i)=>{var u=i(54552)({À:"A",Á:"A",Â:"A",Ã:"A",Ä:"A",Å:"A",à:"a",á:"a",â:"a",ã:"a",ä:"a",å:"a",Ç:"C",ç:"c",Ð:"D",ð:"d",È:"E",É:"E",Ê:"E",Ë:"E",è:"e",é:"e",ê:"e",ë:"e",Ì:"I",Í:"I",Î:"I",Ï:"I",ì:"i",í:"i",î:"i",ï:"i",Ñ:"N",ñ:"n",Ò:"O",Ó:"O",Ô:"O",Õ:"O",Ö:"O",Ø:"O",ò:"o",ó:"o",ô:"o",õ:"o",ö:"o",ø:"o",Ù:"U",Ú:"U",Û:"U",Ü:"U",ù:"u",ú:"u",û:"u",ü:"u",Ý:"Y",ý:"y",ÿ:"y",Æ:"Ae",æ:"ae",Þ:"Th",þ:"th",ß:"ss",Ā:"A",Ă:"A",Ą:"A",ā:"a",ă:"a",ą:"a",Ć:"C",Ĉ:"C",Ċ:"C",Č:"C",ć:"c",ĉ:"c",ċ:"c",č:"c",Ď:"D",Đ:"D",ď:"d",đ:"d",Ē:"E",Ĕ:"E",Ė:"E",Ę:"E",Ě:"E",ē:"e",ĕ:"e",ė:"e",ę:"e",ě:"e",Ĝ:"G",Ğ:"G",Ġ:"G",Ģ:"G",ĝ:"g",ğ:"g",ġ:"g",ģ:"g",Ĥ:"H",Ħ:"H",ĥ:"h",ħ:"h",Ĩ:"I",Ī:"I",Ĭ:"I",Į:"I",İ:"I",ĩ:"i",ī:"i",ĭ:"i",į:"i",ı:"i",Ĵ:"J",ĵ:"j",Ķ:"K",ķ:"k",ĸ:"k",Ĺ:"L",Ļ:"L",Ľ:"L",Ŀ:"L",Ł:"L",ĺ:"l",ļ:"l",ľ:"l",ŀ:"l",ł:"l",Ń:"N",Ņ:"N",Ň:"N",Ŋ:"N",ń:"n",ņ:"n",ň:"n",ŋ:"n",Ō:"O",Ŏ:"O",Ő:"O",ō:"o",ŏ:"o",ő:"o",Ŕ:"R",Ŗ:"R",Ř:"R",ŕ:"r",ŗ:"r",ř:"r",Ś:"S",Ŝ:"S",Ş:"S",Š:"S",ś:"s",ŝ:"s",ş:"s",š:"s",Ţ:"T",Ť:"T",Ŧ:"T",ţ:"t",ť:"t",ŧ:"t",Ũ:"U",Ū:"U",Ŭ:"U",Ů:"U",Ű:"U",Ų:"U",ũ:"u",ū:"u",ŭ:"u",ů:"u",ű:"u",ų:"u",Ŵ:"W",ŵ:"w",Ŷ:"Y",ŷ:"y",Ÿ:"Y",Ź:"Z",Ż:"Z",Ž:"Z",ź:"z",ż:"z",ž:"z",IJ:"IJ",ij:"ij",Œ:"Oe",œ:"oe",ʼn:"'n",ſ:"s"});o.exports=u},93243:(o,s,i)=>{var u=i(56110),_=function(){try{var o=u(Object,"defineProperty");return o({},"",{}),o}catch(o){}}();o.exports=_},25911:(o,s,i)=>{var u=i(38859),_=i(14248),w=i(19219);o.exports=function equalArrays(o,s,i,x,C,j){var L=1&i,B=o.length,$=s.length;if(B!=$&&!(L&&$>B))return!1;var V=j.get(o),U=j.get(s);if(V&&U)return V==s&&U==o;var z=-1,Y=!0,Z=2&i?new u:void 0;for(j.set(o,s),j.set(s,o);++z{var u=i(51873),_=i(37828),w=i(75288),x=i(25911),C=i(20317),j=i(84247),L=u?u.prototype:void 0,B=L?L.valueOf:void 0;o.exports=function equalByTag(o,s,i,u,L,$,V){switch(i){case"[object DataView]":if(o.byteLength!=s.byteLength||o.byteOffset!=s.byteOffset)return!1;o=o.buffer,s=s.buffer;case"[object ArrayBuffer]":return!(o.byteLength!=s.byteLength||!$(new _(o),new _(s)));case"[object Boolean]":case"[object Date]":case"[object Number]":return w(+o,+s);case"[object Error]":return o.name==s.name&&o.message==s.message;case"[object RegExp]":case"[object String]":return o==s+"";case"[object Map]":var U=C;case"[object Set]":var z=1&u;if(U||(U=j),o.size!=s.size&&!z)return!1;var Y=V.get(o);if(Y)return Y==s;u|=2,V.set(o,s);var Z=x(U(o),U(s),u,L,$,V);return V.delete(o),Z;case"[object Symbol]":if(B)return B.call(o)==B.call(s)}return!1}},50689:(o,s,i)=>{var u=i(50002),_=Object.prototype.hasOwnProperty;o.exports=function equalObjects(o,s,i,w,x,C){var j=1&i,L=u(o),B=L.length;if(B!=u(s).length&&!j)return!1;for(var $=B;$--;){var V=L[$];if(!(j?V in s:_.call(s,V)))return!1}var U=C.get(o),z=C.get(s);if(U&&z)return U==s&&z==o;var Y=!0;C.set(o,s),C.set(s,o);for(var Z=j;++${var u=i(35970),_=i(56757),w=i(32865);o.exports=function flatRest(o){return w(_(o,void 0,u),o+"")}},34840:(o,s,i)=>{var u="object"==typeof i.g&&i.g&&i.g.Object===Object&&i.g;o.exports=u},50002:(o,s,i)=>{var u=i(82199),_=i(4664),w=i(95950);o.exports=function getAllKeys(o){return u(o,w,_)}},83349:(o,s,i)=>{var u=i(82199),_=i(86375),w=i(37241);o.exports=function getAllKeysIn(o){return u(o,w,_)}},37381:(o,s,i)=>{var u=i(48152),_=i(63950),w=u?function(o){return u.get(o)}:_;o.exports=w},62284:(o,s,i)=>{var u=i(84629),_=Object.prototype.hasOwnProperty;o.exports=function getFuncName(o){for(var s=o.name+"",i=u[s],w=_.call(u,s)?i.length:0;w--;){var x=i[w],C=x.func;if(null==C||C==o)return x.name}return s}},11287:o=>{o.exports=function getHolder(o){return o.placeholder}},12651:(o,s,i)=>{var u=i(74218);o.exports=function getMapData(o,s){var i=o.__data__;return u(s)?i["string"==typeof s?"string":"hash"]:i.map}},10776:(o,s,i)=>{var u=i(30756),_=i(95950);o.exports=function getMatchData(o){for(var s=_(o),i=s.length;i--;){var w=s[i],x=o[w];s[i]=[w,x,u(x)]}return s}},56110:(o,s,i)=>{var u=i(45083),_=i(10392);o.exports=function getNative(o,s){var i=_(o,s);return u(i)?i:void 0}},28879:(o,s,i)=>{var u=i(74335)(Object.getPrototypeOf,Object);o.exports=u},659:(o,s,i)=>{var u=i(51873),_=Object.prototype,w=_.hasOwnProperty,x=_.toString,C=u?u.toStringTag:void 0;o.exports=function getRawTag(o){var s=w.call(o,C),i=o[C];try{o[C]=void 0;var u=!0}catch(o){}var _=x.call(o);return u&&(s?o[C]=i:delete o[C]),_}},4664:(o,s,i)=>{var u=i(79770),_=i(63345),w=Object.prototype.propertyIsEnumerable,x=Object.getOwnPropertySymbols,C=x?function(o){return null==o?[]:(o=Object(o),u(x(o),(function(s){return w.call(o,s)})))}:_;o.exports=C},86375:(o,s,i)=>{var u=i(14528),_=i(28879),w=i(4664),x=i(63345),C=Object.getOwnPropertySymbols?function(o){for(var s=[];o;)u(s,w(o)),o=_(o);return s}:x;o.exports=C},5861:(o,s,i)=>{var u=i(55580),_=i(68223),w=i(32804),x=i(76545),C=i(28303),j=i(72552),L=i(47473),B="[object Map]",$="[object Promise]",V="[object Set]",U="[object WeakMap]",z="[object DataView]",Y=L(u),Z=L(_),ee=L(w),ie=L(x),ae=L(C),ce=j;(u&&ce(new u(new ArrayBuffer(1)))!=z||_&&ce(new _)!=B||w&&ce(w.resolve())!=$||x&&ce(new x)!=V||C&&ce(new C)!=U)&&(ce=function(o){var s=j(o),i="[object Object]"==s?o.constructor:void 0,u=i?L(i):"";if(u)switch(u){case Y:return z;case Z:return B;case ee:return $;case ie:return V;case ae:return U}return s}),o.exports=ce},10392:o=>{o.exports=function getValue(o,s){return null==o?void 0:o[s]}},75251:o=>{var s=/\{\n\/\* \[wrapped with (.+)\] \*/,i=/,? & /;o.exports=function getWrapDetails(o){var u=o.match(s);return u?u[1].split(i):[]}},49326:(o,s,i)=>{var u=i(31769),_=i(72428),w=i(56449),x=i(30361),C=i(30294),j=i(77797);o.exports=function hasPath(o,s,i){for(var L=-1,B=(s=u(s,o)).length,$=!1;++L{var s=RegExp("[\\u200d\\ud800-\\udfff\\u0300-\\u036f\\ufe20-\\ufe2f\\u20d0-\\u20ff\\ufe0e\\ufe0f]");o.exports=function hasUnicode(o){return s.test(o)}},45434:o=>{var s=/[a-z][A-Z]|[A-Z]{2}[a-z]|[0-9][a-zA-Z]|[a-zA-Z][0-9]|[^a-zA-Z0-9 ]/;o.exports=function hasUnicodeWord(o){return s.test(o)}},22032:(o,s,i)=>{var u=i(81042);o.exports=function hashClear(){this.__data__=u?u(null):{},this.size=0}},63862:o=>{o.exports=function hashDelete(o){var s=this.has(o)&&delete this.__data__[o];return this.size-=s?1:0,s}},66721:(o,s,i)=>{var u=i(81042),_=Object.prototype.hasOwnProperty;o.exports=function hashGet(o){var s=this.__data__;if(u){var i=s[o];return"__lodash_hash_undefined__"===i?void 0:i}return _.call(s,o)?s[o]:void 0}},12749:(o,s,i)=>{var u=i(81042),_=Object.prototype.hasOwnProperty;o.exports=function hashHas(o){var s=this.__data__;return u?void 0!==s[o]:_.call(s,o)}},35749:(o,s,i)=>{var u=i(81042);o.exports=function hashSet(o,s){var i=this.__data__;return this.size+=this.has(o)?0:1,i[o]=u&&void 0===s?"__lodash_hash_undefined__":s,this}},76189:o=>{var s=Object.prototype.hasOwnProperty;o.exports=function initCloneArray(o){var i=o.length,u=new o.constructor(i);return i&&"string"==typeof o[0]&&s.call(o,"index")&&(u.index=o.index,u.input=o.input),u}},77199:(o,s,i)=>{var u=i(49653),_=i(76169),w=i(73201),x=i(93736),C=i(71961);o.exports=function initCloneByTag(o,s,i){var j=o.constructor;switch(s){case"[object ArrayBuffer]":return u(o);case"[object Boolean]":case"[object Date]":return new j(+o);case"[object DataView]":return _(o,i);case"[object Float32Array]":case"[object Float64Array]":case"[object Int8Array]":case"[object Int16Array]":case"[object Int32Array]":case"[object Uint8Array]":case"[object Uint8ClampedArray]":case"[object Uint16Array]":case"[object Uint32Array]":return C(o,i);case"[object Map]":case"[object Set]":return new j;case"[object Number]":case"[object String]":return new j(o);case"[object RegExp]":return w(o);case"[object Symbol]":return x(o)}}},35529:(o,s,i)=>{var u=i(39344),_=i(28879),w=i(55527);o.exports=function initCloneObject(o){return"function"!=typeof o.constructor||w(o)?{}:u(_(o))}},62060:o=>{var s=/\{(?:\n\/\* \[wrapped with .+\] \*\/)?\n?/;o.exports=function insertWrapDetails(o,i){var u=i.length;if(!u)return o;var _=u-1;return i[_]=(u>1?"& ":"")+i[_],i=i.join(u>2?", ":" "),o.replace(s,"{\n/* [wrapped with "+i+"] */\n")}},45891:(o,s,i)=>{var u=i(51873),_=i(72428),w=i(56449),x=u?u.isConcatSpreadable:void 0;o.exports=function isFlattenable(o){return w(o)||_(o)||!!(x&&o&&o[x])}},30361:o=>{var s=/^(?:0|[1-9]\d*)$/;o.exports=function isIndex(o,i){var u=typeof o;return!!(i=null==i?9007199254740991:i)&&("number"==u||"symbol"!=u&&s.test(o))&&o>-1&&o%1==0&&o{var u=i(75288),_=i(64894),w=i(30361),x=i(23805);o.exports=function isIterateeCall(o,s,i){if(!x(i))return!1;var C=typeof s;return!!("number"==C?_(i)&&w(s,i.length):"string"==C&&s in i)&&u(i[s],o)}},28586:(o,s,i)=>{var u=i(56449),_=i(44394),w=/\.|\[(?:[^[\]]*|(["'])(?:(?!\1)[^\\]|\\.)*?\1)\]/,x=/^\w*$/;o.exports=function isKey(o,s){if(u(o))return!1;var i=typeof o;return!("number"!=i&&"symbol"!=i&&"boolean"!=i&&null!=o&&!_(o))||(x.test(o)||!w.test(o)||null!=s&&o in Object(s))}},74218:o=>{o.exports=function isKeyable(o){var s=typeof o;return"string"==s||"number"==s||"symbol"==s||"boolean"==s?"__proto__"!==o:null===o}},85087:(o,s,i)=>{var u=i(30980),_=i(37381),w=i(62284),x=i(53758);o.exports=function isLaziable(o){var s=w(o),i=x[s];if("function"!=typeof i||!(s in u.prototype))return!1;if(o===i)return!0;var C=_(i);return!!C&&o===C[0]}},87296:(o,s,i)=>{var u,_=i(55481),w=(u=/[^.]+$/.exec(_&&_.keys&&_.keys.IE_PROTO||""))?"Symbol(src)_1."+u:"";o.exports=function isMasked(o){return!!w&&w in o}},55527:o=>{var s=Object.prototype;o.exports=function isPrototype(o){var i=o&&o.constructor;return o===("function"==typeof i&&i.prototype||s)}},30756:(o,s,i)=>{var u=i(23805);o.exports=function isStrictComparable(o){return o==o&&!u(o)}},63702:o=>{o.exports=function listCacheClear(){this.__data__=[],this.size=0}},70080:(o,s,i)=>{var u=i(26025),_=Array.prototype.splice;o.exports=function listCacheDelete(o){var s=this.__data__,i=u(s,o);return!(i<0)&&(i==s.length-1?s.pop():_.call(s,i,1),--this.size,!0)}},24739:(o,s,i)=>{var u=i(26025);o.exports=function listCacheGet(o){var s=this.__data__,i=u(s,o);return i<0?void 0:s[i][1]}},48655:(o,s,i)=>{var u=i(26025);o.exports=function listCacheHas(o){return u(this.__data__,o)>-1}},31175:(o,s,i)=>{var u=i(26025);o.exports=function listCacheSet(o,s){var i=this.__data__,_=u(i,o);return _<0?(++this.size,i.push([o,s])):i[_][1]=s,this}},63040:(o,s,i)=>{var u=i(21549),_=i(80079),w=i(68223);o.exports=function mapCacheClear(){this.size=0,this.__data__={hash:new u,map:new(w||_),string:new u}}},17670:(o,s,i)=>{var u=i(12651);o.exports=function mapCacheDelete(o){var s=u(this,o).delete(o);return this.size-=s?1:0,s}},90289:(o,s,i)=>{var u=i(12651);o.exports=function mapCacheGet(o){return u(this,o).get(o)}},4509:(o,s,i)=>{var u=i(12651);o.exports=function mapCacheHas(o){return u(this,o).has(o)}},72949:(o,s,i)=>{var u=i(12651);o.exports=function mapCacheSet(o,s){var i=u(this,o),_=i.size;return i.set(o,s),this.size+=i.size==_?0:1,this}},20317:o=>{o.exports=function mapToArray(o){var s=-1,i=Array(o.size);return o.forEach((function(o,u){i[++s]=[u,o]})),i}},67197:o=>{o.exports=function matchesStrictComparable(o,s){return function(i){return null!=i&&(i[o]===s&&(void 0!==s||o in Object(i)))}}},62224:(o,s,i)=>{var u=i(50104);o.exports=function memoizeCapped(o){var s=u(o,(function(o){return 500===i.size&&i.clear(),o})),i=s.cache;return s}},3209:(o,s,i)=>{var u=i(91596),_=i(53320),w=i(36306),x="__lodash_placeholder__",C=128,j=Math.min;o.exports=function mergeData(o,s){var i=o[1],L=s[1],B=i|L,$=B<131,V=L==C&&8==i||L==C&&256==i&&o[7].length<=s[8]||384==L&&s[7].length<=s[8]&&8==i;if(!$&&!V)return o;1&L&&(o[2]=s[2],B|=1&i?0:4);var U=s[3];if(U){var z=o[3];o[3]=z?u(z,U,s[4]):U,o[4]=z?w(o[3],x):s[4]}return(U=s[5])&&(z=o[5],o[5]=z?_(z,U,s[6]):U,o[6]=z?w(o[5],x):s[6]),(U=s[7])&&(o[7]=U),L&C&&(o[8]=null==o[8]?s[8]:j(o[8],s[8])),null==o[9]&&(o[9]=s[9]),o[0]=s[0],o[1]=B,o}},48152:(o,s,i)=>{var u=i(28303),_=u&&new u;o.exports=_},81042:(o,s,i)=>{var u=i(56110)(Object,"create");o.exports=u},3650:(o,s,i)=>{var u=i(74335)(Object.keys,Object);o.exports=u},90181:o=>{o.exports=function nativeKeysIn(o){var s=[];if(null!=o)for(var i in Object(o))s.push(i);return s}},86009:(o,s,i)=>{o=i.nmd(o);var u=i(34840),_=s&&!s.nodeType&&s,w=_&&o&&!o.nodeType&&o,x=w&&w.exports===_&&u.process,C=function(){try{var o=w&&w.require&&w.require("util").types;return o||x&&x.binding&&x.binding("util")}catch(o){}}();o.exports=C},59350:o=>{var s=Object.prototype.toString;o.exports=function objectToString(o){return s.call(o)}},74335:o=>{o.exports=function overArg(o,s){return function(i){return o(s(i))}}},56757:(o,s,i)=>{var u=i(91033),_=Math.max;o.exports=function overRest(o,s,i){return s=_(void 0===s?o.length-1:s,0),function(){for(var w=arguments,x=-1,C=_(w.length-s,0),j=Array(C);++x{var u=i(47422),_=i(25160);o.exports=function parent(o,s){return s.length<2?o:u(o,_(s,0,-1))}},84629:o=>{o.exports={}},68294:(o,s,i)=>{var u=i(23007),_=i(30361),w=Math.min;o.exports=function reorder(o,s){for(var i=o.length,x=w(s.length,i),C=u(o);x--;){var j=s[x];o[x]=_(j,i)?C[j]:void 0}return o}},36306:o=>{var s="__lodash_placeholder__";o.exports=function replaceHolders(o,i){for(var u=-1,_=o.length,w=0,x=[];++u<_;){var C=o[u];C!==i&&C!==s||(o[u]=s,x[w++]=u)}return x}},9325:(o,s,i)=>{var u=i(34840),_="object"==typeof self&&self&&self.Object===Object&&self,w=u||_||Function("return this")();o.exports=w},14974:o=>{o.exports=function safeGet(o,s){if(("constructor"!==s||"function"!=typeof o[s])&&"__proto__"!=s)return o[s]}},31380:o=>{o.exports=function setCacheAdd(o){return this.__data__.set(o,"__lodash_hash_undefined__"),this}},51459:o=>{o.exports=function setCacheHas(o){return this.__data__.has(o)}},54641:(o,s,i)=>{var u=i(68882),_=i(51811)(u);o.exports=_},84247:o=>{o.exports=function setToArray(o){var s=-1,i=Array(o.size);return o.forEach((function(o){i[++s]=o})),i}},32865:(o,s,i)=>{var u=i(19570),_=i(51811)(u);o.exports=_},70981:(o,s,i)=>{var u=i(75251),_=i(62060),w=i(32865),x=i(75948);o.exports=function setWrapToString(o,s,i){var C=s+"";return w(o,_(C,x(u(C),i)))}},51811:o=>{var s=Date.now;o.exports=function shortOut(o){var i=0,u=0;return function(){var _=s(),w=16-(_-u);if(u=_,w>0){if(++i>=800)return arguments[0]}else i=0;return o.apply(void 0,arguments)}}},51420:(o,s,i)=>{var u=i(80079);o.exports=function stackClear(){this.__data__=new u,this.size=0}},90938:o=>{o.exports=function stackDelete(o){var s=this.__data__,i=s.delete(o);return this.size=s.size,i}},63605:o=>{o.exports=function stackGet(o){return this.__data__.get(o)}},29817:o=>{o.exports=function stackHas(o){return this.__data__.has(o)}},80945:(o,s,i)=>{var u=i(80079),_=i(68223),w=i(53661);o.exports=function stackSet(o,s){var i=this.__data__;if(i instanceof u){var x=i.__data__;if(!_||x.length<199)return x.push([o,s]),this.size=++i.size,this;i=this.__data__=new w(x)}return i.set(o,s),this.size=i.size,this}},76959:o=>{o.exports=function strictIndexOf(o,s,i){for(var u=i-1,_=o.length;++u<_;)if(o[u]===s)return u;return-1}},63912:(o,s,i)=>{var u=i(61074),_=i(49698),w=i(42054);o.exports=function stringToArray(o){return _(o)?w(o):u(o)}},61802:(o,s,i)=>{var u=i(62224),_=/[^.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]|(?=(?:\.|\[\])(?:\.|\[\]|$))/g,w=/\\(\\)?/g,x=u((function(o){var s=[];return 46===o.charCodeAt(0)&&s.push(""),o.replace(_,(function(o,i,u,_){s.push(u?_.replace(w,"$1"):i||o)})),s}));o.exports=x},77797:(o,s,i)=>{var u=i(44394);o.exports=function toKey(o){if("string"==typeof o||u(o))return o;var s=o+"";return"0"==s&&1/o==-1/0?"-0":s}},47473:o=>{var s=Function.prototype.toString;o.exports=function toSource(o){if(null!=o){try{return s.call(o)}catch(o){}try{return o+""}catch(o){}}return""}},31800:o=>{var s=/\s/;o.exports=function trimmedEndIndex(o){for(var i=o.length;i--&&s.test(o.charAt(i)););return i}},42054:o=>{var s="\\ud800-\\udfff",i="["+s+"]",u="[\\u0300-\\u036f\\ufe20-\\ufe2f\\u20d0-\\u20ff]",_="\\ud83c[\\udffb-\\udfff]",w="[^"+s+"]",x="(?:\\ud83c[\\udde6-\\uddff]){2}",C="[\\ud800-\\udbff][\\udc00-\\udfff]",j="(?:"+u+"|"+_+")"+"?",L="[\\ufe0e\\ufe0f]?",B=L+j+("(?:\\u200d(?:"+[w,x,C].join("|")+")"+L+j+")*"),$="(?:"+[w+u+"?",u,x,C,i].join("|")+")",V=RegExp(_+"(?="+_+")|"+$+B,"g");o.exports=function unicodeToArray(o){return o.match(V)||[]}},22225:o=>{var s="\\ud800-\\udfff",i="\\u2700-\\u27bf",u="a-z\\xdf-\\xf6\\xf8-\\xff",_="A-Z\\xc0-\\xd6\\xd8-\\xde",w="\\xac\\xb1\\xd7\\xf7\\x00-\\x2f\\x3a-\\x40\\x5b-\\x60\\x7b-\\xbf\\u2000-\\u206f \\t\\x0b\\f\\xa0\\ufeff\\n\\r\\u2028\\u2029\\u1680\\u180e\\u2000\\u2001\\u2002\\u2003\\u2004\\u2005\\u2006\\u2007\\u2008\\u2009\\u200a\\u202f\\u205f\\u3000",x="["+w+"]",C="\\d+",j="["+i+"]",L="["+u+"]",B="[^"+s+w+C+i+u+_+"]",$="(?:\\ud83c[\\udde6-\\uddff]){2}",V="[\\ud800-\\udbff][\\udc00-\\udfff]",U="["+_+"]",z="(?:"+L+"|"+B+")",Y="(?:"+U+"|"+B+")",Z="(?:['’](?:d|ll|m|re|s|t|ve))?",ee="(?:['’](?:D|LL|M|RE|S|T|VE))?",ie="(?:[\\u0300-\\u036f\\ufe20-\\ufe2f\\u20d0-\\u20ff]|\\ud83c[\\udffb-\\udfff])?",ae="[\\ufe0e\\ufe0f]?",ce=ae+ie+("(?:\\u200d(?:"+["[^"+s+"]",$,V].join("|")+")"+ae+ie+")*"),le="(?:"+[j,$,V].join("|")+")"+ce,pe=RegExp([U+"?"+L+"+"+Z+"(?="+[x,U,"$"].join("|")+")",Y+"+"+ee+"(?="+[x,U+z,"$"].join("|")+")",U+"?"+z+"+"+Z,U+"+"+ee,"\\d*(?:1ST|2ND|3RD|(?![123])\\dTH)(?=\\b|[a-z_])","\\d*(?:1st|2nd|3rd|(?![123])\\dth)(?=\\b|[A-Z_])",C,le].join("|"),"g");o.exports=function unicodeWords(o){return o.match(pe)||[]}},75948:(o,s,i)=>{var u=i(83729),_=i(15325),w=[["ary",128],["bind",1],["bindKey",2],["curry",8],["curryRight",16],["flip",512],["partial",32],["partialRight",64],["rearg",256]];o.exports=function updateWrapDetails(o,s){return u(w,(function(i){var u="_."+i[0];s&i[1]&&!_(o,u)&&o.push(u)})),o.sort()}},80257:(o,s,i)=>{var u=i(30980),_=i(56017),w=i(23007);o.exports=function wrapperClone(o){if(o instanceof u)return o.clone();var s=new _(o.__wrapped__,o.__chain__);return s.__actions__=w(o.__actions__),s.__index__=o.__index__,s.__values__=o.__values__,s}},64626:(o,s,i)=>{var u=i(66977);o.exports=function ary(o,s,i){return s=i?void 0:s,s=o&&null==s?o.length:s,u(o,128,void 0,void 0,void 0,void 0,s)}},84058:(o,s,i)=>{var u=i(14792),_=i(45539)((function(o,s,i){return s=s.toLowerCase(),o+(i?u(s):s)}));o.exports=_},14792:(o,s,i)=>{var u=i(13222),_=i(55808);o.exports=function capitalize(o){return _(u(o).toLowerCase())}},32629:(o,s,i)=>{var u=i(9999);o.exports=function clone(o){return u(o,4)}},37334:o=>{o.exports=function constant(o){return function(){return o}}},49747:(o,s,i)=>{var u=i(66977);function curry(o,s,i){var _=u(o,8,void 0,void 0,void 0,void 0,void 0,s=i?void 0:s);return _.placeholder=curry.placeholder,_}curry.placeholder={},o.exports=curry},38221:(o,s,i)=>{var u=i(23805),_=i(10124),w=i(99374),x=Math.max,C=Math.min;o.exports=function debounce(o,s,i){var j,L,B,$,V,U,z=0,Y=!1,Z=!1,ee=!0;if("function"!=typeof o)throw new TypeError("Expected a function");function invokeFunc(s){var i=j,u=L;return j=L=void 0,z=s,$=o.apply(u,i)}function shouldInvoke(o){var i=o-U;return void 0===U||i>=s||i<0||Z&&o-z>=B}function timerExpired(){var o=_();if(shouldInvoke(o))return trailingEdge(o);V=setTimeout(timerExpired,function remainingWait(o){var i=s-(o-U);return Z?C(i,B-(o-z)):i}(o))}function trailingEdge(o){return V=void 0,ee&&j?invokeFunc(o):(j=L=void 0,$)}function debounced(){var o=_(),i=shouldInvoke(o);if(j=arguments,L=this,U=o,i){if(void 0===V)return function leadingEdge(o){return z=o,V=setTimeout(timerExpired,s),Y?invokeFunc(o):$}(U);if(Z)return clearTimeout(V),V=setTimeout(timerExpired,s),invokeFunc(U)}return void 0===V&&(V=setTimeout(timerExpired,s)),$}return s=w(s)||0,u(i)&&(Y=!!i.leading,B=(Z="maxWait"in i)?x(w(i.maxWait)||0,s):B,ee="trailing"in i?!!i.trailing:ee),debounced.cancel=function cancel(){void 0!==V&&clearTimeout(V),z=0,j=U=L=V=void 0},debounced.flush=function flush(){return void 0===V?$:trailingEdge(_())},debounced}},50828:(o,s,i)=>{var u=i(24647),_=i(13222),w=/[\xc0-\xd6\xd8-\xf6\xf8-\xff\u0100-\u017f]/g,x=RegExp("[\\u0300-\\u036f\\ufe20-\\ufe2f\\u20d0-\\u20ff]","g");o.exports=function deburr(o){return(o=_(o))&&o.replace(w,u).replace(x,"")}},75288:o=>{o.exports=function eq(o,s){return o===s||o!=o&&s!=s}},60680:(o,s,i)=>{var u=i(13222),_=/[\\^$.*+?()[\]{}|]/g,w=RegExp(_.source);o.exports=function escapeRegExp(o){return(o=u(o))&&w.test(o)?o.replace(_,"\\$&"):o}},7309:(o,s,i)=>{var u=i(62006)(i(24713));o.exports=u},24713:(o,s,i)=>{var u=i(2523),_=i(15389),w=i(61489),x=Math.max;o.exports=function findIndex(o,s,i){var C=null==o?0:o.length;if(!C)return-1;var j=null==i?0:w(i);return j<0&&(j=x(C+j,0)),u(o,_(s,3),j)}},35970:(o,s,i)=>{var u=i(83120);o.exports=function flatten(o){return(null==o?0:o.length)?u(o,1):[]}},73424:(o,s,i)=>{var u=i(16962),_=i(2874),w=Array.prototype.push;function baseAry(o,s){return 2==s?function(s,i){return o(s,i)}:function(s){return o(s)}}function cloneArray(o){for(var s=o?o.length:0,i=Array(s);s--;)i[s]=o[s];return i}function wrapImmutable(o,s){return function(){var i=arguments.length;if(i){for(var u=Array(i);i--;)u[i]=arguments[i];var _=u[0]=s.apply(void 0,u);return o.apply(void 0,u),_}}}o.exports=function baseConvert(o,s,i,x){var C="function"==typeof s,j=s===Object(s);if(j&&(x=i,i=s,s=void 0),null==i)throw new TypeError;x||(x={});var L={cap:!("cap"in x)||x.cap,curry:!("curry"in x)||x.curry,fixed:!("fixed"in x)||x.fixed,immutable:!("immutable"in x)||x.immutable,rearg:!("rearg"in x)||x.rearg},B=C?i:_,$="curry"in x&&x.curry,V="fixed"in x&&x.fixed,U="rearg"in x&&x.rearg,z=C?i.runInContext():void 0,Y=C?i:{ary:o.ary,assign:o.assign,clone:o.clone,curry:o.curry,forEach:o.forEach,isArray:o.isArray,isError:o.isError,isFunction:o.isFunction,isWeakMap:o.isWeakMap,iteratee:o.iteratee,keys:o.keys,rearg:o.rearg,toInteger:o.toInteger,toPath:o.toPath},Z=Y.ary,ee=Y.assign,ie=Y.clone,ae=Y.curry,ce=Y.forEach,le=Y.isArray,pe=Y.isError,de=Y.isFunction,fe=Y.isWeakMap,ye=Y.keys,be=Y.rearg,_e=Y.toInteger,we=Y.toPath,Se=ye(u.aryMethod),xe={castArray:function(o){return function(){var s=arguments[0];return le(s)?o(cloneArray(s)):o.apply(void 0,arguments)}},iteratee:function(o){return function(){var s=arguments[1],i=o(arguments[0],s),u=i.length;return L.cap&&"number"==typeof s?(s=s>2?s-2:1,u&&u<=s?i:baseAry(i,s)):i}},mixin:function(o){return function(s){var i=this;if(!de(i))return o(i,Object(s));var u=[];return ce(ye(s),(function(o){de(s[o])&&u.push([o,i.prototype[o]])})),o(i,Object(s)),ce(u,(function(o){var s=o[1];de(s)?i.prototype[o[0]]=s:delete i.prototype[o[0]]})),i}},nthArg:function(o){return function(s){var i=s<0?1:_e(s)+1;return ae(o(s),i)}},rearg:function(o){return function(s,i){var u=i?i.length:0;return ae(o(s,i),u)}},runInContext:function(s){return function(i){return baseConvert(o,s(i),x)}}};function castCap(o,s){if(L.cap){var i=u.iterateeRearg[o];if(i)return function iterateeRearg(o,s){return overArg(o,(function(o){var i=s.length;return function baseArity(o,s){return 2==s?function(s,i){return o.apply(void 0,arguments)}:function(s){return o.apply(void 0,arguments)}}(be(baseAry(o,i),s),i)}))}(s,i);var _=!C&&u.iterateeAry[o];if(_)return function iterateeAry(o,s){return overArg(o,(function(o){return"function"==typeof o?baseAry(o,s):o}))}(s,_)}return s}function castFixed(o,s,i){if(L.fixed&&(V||!u.skipFixed[o])){var _=u.methodSpread[o],x=_&&_.start;return void 0===x?Z(s,i):function flatSpread(o,s){return function(){for(var i=arguments.length,u=i-1,_=Array(i);i--;)_[i]=arguments[i];var x=_[s],C=_.slice(0,s);return x&&w.apply(C,x),s!=u&&w.apply(C,_.slice(s+1)),o.apply(this,C)}}(s,x)}return s}function castRearg(o,s,i){return L.rearg&&i>1&&(U||!u.skipRearg[o])?be(s,u.methodRearg[o]||u.aryRearg[i]):s}function cloneByPath(o,s){for(var i=-1,u=(s=we(s)).length,_=u-1,w=ie(Object(o)),x=w;null!=x&&++i1?ae(s,i):s}(0,_=castCap(w,_),o),!1}})),!_})),_||(_=x),_==s&&(_=$?ae(_,1):function(){return s.apply(this,arguments)}),_.convert=createConverter(w,s),_.placeholder=s.placeholder=i,_}if(!j)return wrap(s,i,B);var Pe=i,Te=[];return ce(Se,(function(o){ce(u.aryMethod[o],(function(o){var s=Pe[u.remap[o]||o];s&&Te.push([o,wrap(o,s,Pe)])}))})),ce(ye(Pe),(function(o){var s=Pe[o];if("function"==typeof s){for(var i=Te.length;i--;)if(Te[i][0]==o)return;s.convert=createConverter(o,s),Te.push([o,s])}})),ce(Te,(function(o){Pe[o[0]]=o[1]})),Pe.convert=function convertLib(o){return Pe.runInContext.convert(o)(void 0)},Pe.placeholder=Pe,ce(ye(Pe),(function(o){ce(u.realToAlias[o]||[],(function(s){Pe[s]=Pe[o]}))})),Pe}},16962:(o,s)=>{s.aliasToReal={each:"forEach",eachRight:"forEachRight",entries:"toPairs",entriesIn:"toPairsIn",extend:"assignIn",extendAll:"assignInAll",extendAllWith:"assignInAllWith",extendWith:"assignInWith",first:"head",conforms:"conformsTo",matches:"isMatch",property:"get",__:"placeholder",F:"stubFalse",T:"stubTrue",all:"every",allPass:"overEvery",always:"constant",any:"some",anyPass:"overSome",apply:"spread",assoc:"set",assocPath:"set",complement:"negate",compose:"flowRight",contains:"includes",dissoc:"unset",dissocPath:"unset",dropLast:"dropRight",dropLastWhile:"dropRightWhile",equals:"isEqual",identical:"eq",indexBy:"keyBy",init:"initial",invertObj:"invert",juxt:"over",omitAll:"omit",nAry:"ary",path:"get",pathEq:"matchesProperty",pathOr:"getOr",paths:"at",pickAll:"pick",pipe:"flow",pluck:"map",prop:"get",propEq:"matchesProperty",propOr:"getOr",props:"at",symmetricDifference:"xor",symmetricDifferenceBy:"xorBy",symmetricDifferenceWith:"xorWith",takeLast:"takeRight",takeLastWhile:"takeRightWhile",unapply:"rest",unnest:"flatten",useWith:"overArgs",where:"conformsTo",whereEq:"isMatch",zipObj:"zipObject"},s.aryMethod={1:["assignAll","assignInAll","attempt","castArray","ceil","create","curry","curryRight","defaultsAll","defaultsDeepAll","floor","flow","flowRight","fromPairs","invert","iteratee","memoize","method","mergeAll","methodOf","mixin","nthArg","over","overEvery","overSome","rest","reverse","round","runInContext","spread","template","trim","trimEnd","trimStart","uniqueId","words","zipAll"],2:["add","after","ary","assign","assignAllWith","assignIn","assignInAllWith","at","before","bind","bindAll","bindKey","chunk","cloneDeepWith","cloneWith","concat","conformsTo","countBy","curryN","curryRightN","debounce","defaults","defaultsDeep","defaultTo","delay","difference","divide","drop","dropRight","dropRightWhile","dropWhile","endsWith","eq","every","filter","find","findIndex","findKey","findLast","findLastIndex","findLastKey","flatMap","flatMapDeep","flattenDepth","forEach","forEachRight","forIn","forInRight","forOwn","forOwnRight","get","groupBy","gt","gte","has","hasIn","includes","indexOf","intersection","invertBy","invoke","invokeMap","isEqual","isMatch","join","keyBy","lastIndexOf","lt","lte","map","mapKeys","mapValues","matchesProperty","maxBy","meanBy","merge","mergeAllWith","minBy","multiply","nth","omit","omitBy","overArgs","pad","padEnd","padStart","parseInt","partial","partialRight","partition","pick","pickBy","propertyOf","pull","pullAll","pullAt","random","range","rangeRight","rearg","reject","remove","repeat","restFrom","result","sampleSize","some","sortBy","sortedIndex","sortedIndexOf","sortedLastIndex","sortedLastIndexOf","sortedUniqBy","split","spreadFrom","startsWith","subtract","sumBy","take","takeRight","takeRightWhile","takeWhile","tap","throttle","thru","times","trimChars","trimCharsEnd","trimCharsStart","truncate","union","uniqBy","uniqWith","unset","unzipWith","without","wrap","xor","zip","zipObject","zipObjectDeep"],3:["assignInWith","assignWith","clamp","differenceBy","differenceWith","findFrom","findIndexFrom","findLastFrom","findLastIndexFrom","getOr","includesFrom","indexOfFrom","inRange","intersectionBy","intersectionWith","invokeArgs","invokeArgsMap","isEqualWith","isMatchWith","flatMapDepth","lastIndexOfFrom","mergeWith","orderBy","padChars","padCharsEnd","padCharsStart","pullAllBy","pullAllWith","rangeStep","rangeStepRight","reduce","reduceRight","replace","set","slice","sortedIndexBy","sortedLastIndexBy","transform","unionBy","unionWith","update","xorBy","xorWith","zipWith"],4:["fill","setWith","updateWith"]},s.aryRearg={2:[1,0],3:[2,0,1],4:[3,2,0,1]},s.iterateeAry={dropRightWhile:1,dropWhile:1,every:1,filter:1,find:1,findFrom:1,findIndex:1,findIndexFrom:1,findKey:1,findLast:1,findLastFrom:1,findLastIndex:1,findLastIndexFrom:1,findLastKey:1,flatMap:1,flatMapDeep:1,flatMapDepth:1,forEach:1,forEachRight:1,forIn:1,forInRight:1,forOwn:1,forOwnRight:1,map:1,mapKeys:1,mapValues:1,partition:1,reduce:2,reduceRight:2,reject:1,remove:1,some:1,takeRightWhile:1,takeWhile:1,times:1,transform:2},s.iterateeRearg={mapKeys:[1],reduceRight:[1,0]},s.methodRearg={assignInAllWith:[1,0],assignInWith:[1,2,0],assignAllWith:[1,0],assignWith:[1,2,0],differenceBy:[1,2,0],differenceWith:[1,2,0],getOr:[2,1,0],intersectionBy:[1,2,0],intersectionWith:[1,2,0],isEqualWith:[1,2,0],isMatchWith:[2,1,0],mergeAllWith:[1,0],mergeWith:[1,2,0],padChars:[2,1,0],padCharsEnd:[2,1,0],padCharsStart:[2,1,0],pullAllBy:[2,1,0],pullAllWith:[2,1,0],rangeStep:[1,2,0],rangeStepRight:[1,2,0],setWith:[3,1,2,0],sortedIndexBy:[2,1,0],sortedLastIndexBy:[2,1,0],unionBy:[1,2,0],unionWith:[1,2,0],updateWith:[3,1,2,0],xorBy:[1,2,0],xorWith:[1,2,0],zipWith:[1,2,0]},s.methodSpread={assignAll:{start:0},assignAllWith:{start:0},assignInAll:{start:0},assignInAllWith:{start:0},defaultsAll:{start:0},defaultsDeepAll:{start:0},invokeArgs:{start:2},invokeArgsMap:{start:2},mergeAll:{start:0},mergeAllWith:{start:0},partial:{start:1},partialRight:{start:1},without:{start:1},zipAll:{start:0}},s.mutate={array:{fill:!0,pull:!0,pullAll:!0,pullAllBy:!0,pullAllWith:!0,pullAt:!0,remove:!0,reverse:!0},object:{assign:!0,assignAll:!0,assignAllWith:!0,assignIn:!0,assignInAll:!0,assignInAllWith:!0,assignInWith:!0,assignWith:!0,defaults:!0,defaultsAll:!0,defaultsDeep:!0,defaultsDeepAll:!0,merge:!0,mergeAll:!0,mergeAllWith:!0,mergeWith:!0},set:{set:!0,setWith:!0,unset:!0,update:!0,updateWith:!0}},s.realToAlias=function(){var o=Object.prototype.hasOwnProperty,i=s.aliasToReal,u={};for(var _ in i){var w=i[_];o.call(u,w)?u[w].push(_):u[w]=[_]}return u}(),s.remap={assignAll:"assign",assignAllWith:"assignWith",assignInAll:"assignIn",assignInAllWith:"assignInWith",curryN:"curry",curryRightN:"curryRight",defaultsAll:"defaults",defaultsDeepAll:"defaultsDeep",findFrom:"find",findIndexFrom:"findIndex",findLastFrom:"findLast",findLastIndexFrom:"findLastIndex",getOr:"get",includesFrom:"includes",indexOfFrom:"indexOf",invokeArgs:"invoke",invokeArgsMap:"invokeMap",lastIndexOfFrom:"lastIndexOf",mergeAll:"merge",mergeAllWith:"mergeWith",padChars:"pad",padCharsEnd:"padEnd",padCharsStart:"padStart",propertyOf:"get",rangeStep:"range",rangeStepRight:"rangeRight",restFrom:"rest",spreadFrom:"spread",trimChars:"trim",trimCharsEnd:"trimEnd",trimCharsStart:"trimStart",zipAll:"zip"},s.skipFixed={castArray:!0,flow:!0,flowRight:!0,iteratee:!0,mixin:!0,rearg:!0,runInContext:!0},s.skipRearg={add:!0,assign:!0,assignIn:!0,bind:!0,bindKey:!0,concat:!0,difference:!0,divide:!0,eq:!0,gt:!0,gte:!0,isEqual:!0,lt:!0,lte:!0,matchesProperty:!0,merge:!0,multiply:!0,overArgs:!0,partial:!0,partialRight:!0,propertyOf:!0,random:!0,range:!0,rangeRight:!0,subtract:!0,zip:!0,zipObject:!0,zipObjectDeep:!0}},47934:(o,s,i)=>{o.exports={ary:i(64626),assign:i(74733),clone:i(32629),curry:i(49747),forEach:i(83729),isArray:i(56449),isError:i(23546),isFunction:i(1882),isWeakMap:i(47886),iteratee:i(33855),keys:i(88984),rearg:i(84195),toInteger:i(61489),toPath:i(42072)}},56367:(o,s,i)=>{o.exports=i(77731)},79920:(o,s,i)=>{var u=i(73424),_=i(47934);o.exports=function convert(o,s,i){return u(_,o,s,i)}},2874:o=>{o.exports={}},77731:(o,s,i)=>{var u=i(79920)("set",i(63560));u.placeholder=i(2874),o.exports=u},58156:(o,s,i)=>{var u=i(47422);o.exports=function get(o,s,i){var _=null==o?void 0:u(o,s);return void 0===_?i:_}},61448:(o,s,i)=>{var u=i(20426),_=i(49326);o.exports=function has(o,s){return null!=o&&_(o,s,u)}},80631:(o,s,i)=>{var u=i(28077),_=i(49326);o.exports=function hasIn(o,s){return null!=o&&_(o,s,u)}},83488:o=>{o.exports=function identity(o){return o}},72428:(o,s,i)=>{var u=i(27534),_=i(40346),w=Object.prototype,x=w.hasOwnProperty,C=w.propertyIsEnumerable,j=u(function(){return arguments}())?u:function(o){return _(o)&&x.call(o,"callee")&&!C.call(o,"callee")};o.exports=j},56449:o=>{var s=Array.isArray;o.exports=s},64894:(o,s,i)=>{var u=i(1882),_=i(30294);o.exports=function isArrayLike(o){return null!=o&&_(o.length)&&!u(o)}},83693:(o,s,i)=>{var u=i(64894),_=i(40346);o.exports=function isArrayLikeObject(o){return _(o)&&u(o)}},53812:(o,s,i)=>{var u=i(72552),_=i(40346);o.exports=function isBoolean(o){return!0===o||!1===o||_(o)&&"[object Boolean]"==u(o)}},3656:(o,s,i)=>{o=i.nmd(o);var u=i(9325),_=i(89935),w=s&&!s.nodeType&&s,x=w&&o&&!o.nodeType&&o,C=x&&x.exports===w?u.Buffer:void 0,j=(C?C.isBuffer:void 0)||_;o.exports=j},62193:(o,s,i)=>{var u=i(88984),_=i(5861),w=i(72428),x=i(56449),C=i(64894),j=i(3656),L=i(55527),B=i(37167),$=Object.prototype.hasOwnProperty;o.exports=function isEmpty(o){if(null==o)return!0;if(C(o)&&(x(o)||"string"==typeof o||"function"==typeof o.splice||j(o)||B(o)||w(o)))return!o.length;var s=_(o);if("[object Map]"==s||"[object Set]"==s)return!o.size;if(L(o))return!u(o).length;for(var i in o)if($.call(o,i))return!1;return!0}},2404:(o,s,i)=>{var u=i(60270);o.exports=function isEqual(o,s){return u(o,s)}},23546:(o,s,i)=>{var u=i(72552),_=i(40346),w=i(11331);o.exports=function isError(o){if(!_(o))return!1;var s=u(o);return"[object Error]"==s||"[object DOMException]"==s||"string"==typeof o.message&&"string"==typeof o.name&&!w(o)}},1882:(o,s,i)=>{var u=i(72552),_=i(23805);o.exports=function isFunction(o){if(!_(o))return!1;var s=u(o);return"[object Function]"==s||"[object GeneratorFunction]"==s||"[object AsyncFunction]"==s||"[object Proxy]"==s}},30294:o=>{o.exports=function isLength(o){return"number"==typeof o&&o>-1&&o%1==0&&o<=9007199254740991}},87730:(o,s,i)=>{var u=i(29172),_=i(27301),w=i(86009),x=w&&w.isMap,C=x?_(x):u;o.exports=C},5187:o=>{o.exports=function isNull(o){return null===o}},98023:(o,s,i)=>{var u=i(72552),_=i(40346);o.exports=function isNumber(o){return"number"==typeof o||_(o)&&"[object Number]"==u(o)}},23805:o=>{o.exports=function isObject(o){var s=typeof o;return null!=o&&("object"==s||"function"==s)}},40346:o=>{o.exports=function isObjectLike(o){return null!=o&&"object"==typeof o}},11331:(o,s,i)=>{var u=i(72552),_=i(28879),w=i(40346),x=Function.prototype,C=Object.prototype,j=x.toString,L=C.hasOwnProperty,B=j.call(Object);o.exports=function isPlainObject(o){if(!w(o)||"[object Object]"!=u(o))return!1;var s=_(o);if(null===s)return!0;var i=L.call(s,"constructor")&&s.constructor;return"function"==typeof i&&i instanceof i&&j.call(i)==B}},38440:(o,s,i)=>{var u=i(16038),_=i(27301),w=i(86009),x=w&&w.isSet,C=x?_(x):u;o.exports=C},85015:(o,s,i)=>{var u=i(72552),_=i(56449),w=i(40346);o.exports=function isString(o){return"string"==typeof o||!_(o)&&w(o)&&"[object String]"==u(o)}},44394:(o,s,i)=>{var u=i(72552),_=i(40346);o.exports=function isSymbol(o){return"symbol"==typeof o||_(o)&&"[object Symbol]"==u(o)}},37167:(o,s,i)=>{var u=i(4901),_=i(27301),w=i(86009),x=w&&w.isTypedArray,C=x?_(x):u;o.exports=C},47886:(o,s,i)=>{var u=i(5861),_=i(40346);o.exports=function isWeakMap(o){return _(o)&&"[object WeakMap]"==u(o)}},33855:(o,s,i)=>{var u=i(9999),_=i(15389);o.exports=function iteratee(o){return _("function"==typeof o?o:u(o,1))}},95950:(o,s,i)=>{var u=i(70695),_=i(88984),w=i(64894);o.exports=function keys(o){return w(o)?u(o):_(o)}},37241:(o,s,i)=>{var u=i(70695),_=i(72903),w=i(64894);o.exports=function keysIn(o){return w(o)?u(o,!0):_(o)}},68090:o=>{o.exports=function last(o){var s=null==o?0:o.length;return s?o[s-1]:void 0}},50104:(o,s,i)=>{var u=i(53661);function memoize(o,s){if("function"!=typeof o||null!=s&&"function"!=typeof s)throw new TypeError("Expected a function");var memoized=function(){var i=arguments,u=s?s.apply(this,i):i[0],_=memoized.cache;if(_.has(u))return _.get(u);var w=o.apply(this,i);return memoized.cache=_.set(u,w)||_,w};return memoized.cache=new(memoize.Cache||u),memoized}memoize.Cache=u,o.exports=memoize},55364:(o,s,i)=>{var u=i(85250),_=i(20999)((function(o,s,i){u(o,s,i)}));o.exports=_},6048:o=>{o.exports=function negate(o){if("function"!=typeof o)throw new TypeError("Expected a function");return function(){var s=arguments;switch(s.length){case 0:return!o.call(this);case 1:return!o.call(this,s[0]);case 2:return!o.call(this,s[0],s[1]);case 3:return!o.call(this,s[0],s[1],s[2])}return!o.apply(this,s)}}},63950:o=>{o.exports=function noop(){}},10124:(o,s,i)=>{var u=i(9325);o.exports=function(){return u.Date.now()}},90179:(o,s,i)=>{var u=i(34932),_=i(9999),w=i(19931),x=i(31769),C=i(21791),j=i(53138),L=i(38816),B=i(83349),$=L((function(o,s){var i={};if(null==o)return i;var L=!1;s=u(s,(function(s){return s=x(s,o),L||(L=s.length>1),s})),C(o,B(o),i),L&&(i=_(i,7,j));for(var $=s.length;$--;)w(i,s[$]);return i}));o.exports=$},50583:(o,s,i)=>{var u=i(47237),_=i(17255),w=i(28586),x=i(77797);o.exports=function property(o){return w(o)?u(x(o)):_(o)}},84195:(o,s,i)=>{var u=i(66977),_=i(38816),w=_((function(o,s){return u(o,256,void 0,void 0,void 0,s)}));o.exports=w},40860:(o,s,i)=>{var u=i(40882),_=i(80909),w=i(15389),x=i(85558),C=i(56449);o.exports=function reduce(o,s,i){var j=C(o)?u:x,L=arguments.length<3;return j(o,w(s,4),i,L,_)}},63560:(o,s,i)=>{var u=i(73170);o.exports=function set(o,s,i){return null==o?o:u(o,s,i)}},42426:(o,s,i)=>{var u=i(14248),_=i(15389),w=i(90916),x=i(56449),C=i(36800);o.exports=function some(o,s,i){var j=x(o)?u:w;return i&&C(o,s,i)&&(s=void 0),j(o,_(s,3))}},63345:o=>{o.exports=function stubArray(){return[]}},89935:o=>{o.exports=function stubFalse(){return!1}},17400:(o,s,i)=>{var u=i(99374),_=1/0;o.exports=function toFinite(o){return o?(o=u(o))===_||o===-1/0?17976931348623157e292*(o<0?-1:1):o==o?o:0:0===o?o:0}},61489:(o,s,i)=>{var u=i(17400);o.exports=function toInteger(o){var s=u(o),i=s%1;return s==s?i?s-i:s:0}},80218:(o,s,i)=>{var u=i(13222);o.exports=function toLower(o){return u(o).toLowerCase()}},99374:(o,s,i)=>{var u=i(54128),_=i(23805),w=i(44394),x=/^[-+]0x[0-9a-f]+$/i,C=/^0b[01]+$/i,j=/^0o[0-7]+$/i,L=parseInt;o.exports=function toNumber(o){if("number"==typeof o)return o;if(w(o))return NaN;if(_(o)){var s="function"==typeof o.valueOf?o.valueOf():o;o=_(s)?s+"":s}if("string"!=typeof o)return 0===o?o:+o;o=u(o);var i=C.test(o);return i||j.test(o)?L(o.slice(2),i?2:8):x.test(o)?NaN:+o}},42072:(o,s,i)=>{var u=i(34932),_=i(23007),w=i(56449),x=i(44394),C=i(61802),j=i(77797),L=i(13222);o.exports=function toPath(o){return w(o)?u(o,j):x(o)?[o]:_(C(L(o)))}},69884:(o,s,i)=>{var u=i(21791),_=i(37241);o.exports=function toPlainObject(o){return u(o,_(o))}},13222:(o,s,i)=>{var u=i(77556);o.exports=function toString(o){return null==o?"":u(o)}},55808:(o,s,i)=>{var u=i(12507)("toUpperCase");o.exports=u},66645:(o,s,i)=>{var u=i(1733),_=i(45434),w=i(13222),x=i(22225);o.exports=function words(o,s,i){return o=w(o),void 0===(s=i?void 0:s)?_(o)?x(o):u(o):o.match(s)||[]}},53758:(o,s,i)=>{var u=i(30980),_=i(56017),w=i(94033),x=i(56449),C=i(40346),j=i(80257),L=Object.prototype.hasOwnProperty;function lodash(o){if(C(o)&&!x(o)&&!(o instanceof u)){if(o instanceof _)return o;if(L.call(o,"__wrapped__"))return j(o)}return new _(o)}lodash.prototype=w.prototype,lodash.prototype.constructor=lodash,o.exports=lodash},47248:(o,s,i)=>{var u=i(16547),_=i(51234);o.exports=function zipObject(o,s){return _(o||[],s||[],u)}},43768:(o,s,i)=>{"use strict";var u=i(45981),_=i(85587);s.highlight=highlight,s.highlightAuto=function highlightAuto(o,s){var i,x,C,j,L=s||{},B=L.subset||u.listLanguages(),$=L.prefix,V=B.length,U=-1;null==$&&($=w);if("string"!=typeof o)throw _("Expected `string` for value, got `%s`",o);x={relevance:0,language:null,value:[]},i={relevance:0,language:null,value:[]};for(;++Ux.relevance&&(x=C),C.relevance>i.relevance&&(x=i,i=C));x.language&&(i.secondBest=x);return i},s.registerLanguage=function registerLanguage(o,s){u.registerLanguage(o,s)},s.listLanguages=function listLanguages(){return u.listLanguages()},s.registerAlias=function registerAlias(o,s){var i,_=o;s&&((_={})[o]=s);for(i in _)u.registerAliases(_[i],{languageName:i})},Emitter.prototype.addText=function text(o){var s,i,u=this.stack;if(""===o)return;s=u[u.length-1],(i=s.children[s.children.length-1])&&"text"===i.type?i.value+=o:s.children.push({type:"text",value:o})},Emitter.prototype.addKeyword=function addKeyword(o,s){this.openNode(s),this.addText(o),this.closeNode()},Emitter.prototype.addSublanguage=function addSublanguage(o,s){var i=this.stack,u=i[i.length-1],_=o.rootNode.children,w=s?{type:"element",tagName:"span",properties:{className:[s]},children:_}:_;u.children=u.children.concat(w)},Emitter.prototype.openNode=function open(o){var s=this.stack,i=this.options.classPrefix+o,u=s[s.length-1],_={type:"element",tagName:"span",properties:{className:[i]},children:[]};u.children.push(_),s.push(_)},Emitter.prototype.closeNode=function close(){this.stack.pop()},Emitter.prototype.closeAllNodes=noop,Emitter.prototype.finalize=noop,Emitter.prototype.toHTML=function toHtmlNoop(){return""};var w="hljs-";function highlight(o,s,i){var x,C=u.configure({}),j=(i||{}).prefix;if("string"!=typeof o)throw _("Expected `string` for name, got `%s`",o);if(!u.getLanguage(o))throw _("Unknown language: `%s` is not registered",o);if("string"!=typeof s)throw _("Expected `string` for value, got `%s`",s);if(null==j&&(j=w),u.configure({__emitter:Emitter,classPrefix:j}),x=u.highlight(s,{language:o,ignoreIllegals:!0}),u.configure(C||{}),x.errorRaised)throw x.errorRaised;return{relevance:x.relevance,language:x.language,value:x.emitter.rootNode.children}}function Emitter(o){this.options=o,this.rootNode={children:[]},this.stack=[this.rootNode]}function noop(){}},92340:(o,s,i)=>{const u=i(6048);function coerceElementMatchingCallback(o){return"string"==typeof o?s=>s.element===o:o.constructor&&o.extend?s=>s instanceof o:o}class ArraySlice{constructor(o){this.elements=o||[]}toValue(){return this.elements.map((o=>o.toValue()))}map(o,s){return this.elements.map(o,s)}flatMap(o,s){return this.map(o,s).reduce(((o,s)=>o.concat(s)),[])}compactMap(o,s){const i=[];return this.forEach((u=>{const _=o.bind(s)(u);_&&i.push(_)})),i}filter(o,s){return o=coerceElementMatchingCallback(o),new ArraySlice(this.elements.filter(o,s))}reject(o,s){return o=coerceElementMatchingCallback(o),new ArraySlice(this.elements.filter(u(o),s))}find(o,s){return o=coerceElementMatchingCallback(o),this.elements.find(o,s)}forEach(o,s){this.elements.forEach(o,s)}reduce(o,s){return this.elements.reduce(o,s)}includes(o){return this.elements.some((s=>s.equals(o)))}shift(){return this.elements.shift()}unshift(o){this.elements.unshift(this.refract(o))}push(o){return this.elements.push(this.refract(o)),this}add(o){this.push(o)}get(o){return this.elements[o]}getValue(o){const s=this.elements[o];if(s)return s.toValue()}get length(){return this.elements.length}get isEmpty(){return 0===this.elements.length}get first(){return this.elements[0]}}"undefined"!=typeof Symbol&&(ArraySlice.prototype[Symbol.iterator]=function symbol(){return this.elements[Symbol.iterator]()}),o.exports=ArraySlice},55973:o=>{class KeyValuePair{constructor(o,s){this.key=o,this.value=s}clone(){const o=new KeyValuePair;return this.key&&(o.key=this.key.clone()),this.value&&(o.value=this.value.clone()),o}}o.exports=KeyValuePair},3110:(o,s,i)=>{const u=i(5187),_=i(85015),w=i(98023),x=i(53812),C=i(23805),j=i(85105),L=i(86804);class Namespace{constructor(o){this.elementMap={},this.elementDetection=[],this.Element=L.Element,this.KeyValuePair=L.KeyValuePair,o&&o.noDefault||this.useDefault(),this._attributeElementKeys=[],this._attributeElementArrayKeys=[]}use(o){return o.namespace&&o.namespace({base:this}),o.load&&o.load({base:this}),this}useDefault(){return this.register("null",L.NullElement).register("string",L.StringElement).register("number",L.NumberElement).register("boolean",L.BooleanElement).register("array",L.ArrayElement).register("object",L.ObjectElement).register("member",L.MemberElement).register("ref",L.RefElement).register("link",L.LinkElement),this.detect(u,L.NullElement,!1).detect(_,L.StringElement,!1).detect(w,L.NumberElement,!1).detect(x,L.BooleanElement,!1).detect(Array.isArray,L.ArrayElement,!1).detect(C,L.ObjectElement,!1),this}register(o,s){return this._elements=void 0,this.elementMap[o]=s,this}unregister(o){return this._elements=void 0,delete this.elementMap[o],this}detect(o,s,i){return void 0===i||i?this.elementDetection.unshift([o,s]):this.elementDetection.push([o,s]),this}toElement(o){if(o instanceof this.Element)return o;let s;for(let i=0;i{const s=o[0].toUpperCase()+o.substr(1);this._elements[s]=this.elementMap[o]}))),this._elements}get serialiser(){return new j(this)}}j.prototype.Namespace=Namespace,o.exports=Namespace},10866:(o,s,i)=>{const u=i(6048),_=i(92340);class ObjectSlice extends _{map(o,s){return this.elements.map((i=>o.bind(s)(i.value,i.key,i)))}filter(o,s){return new ObjectSlice(this.elements.filter((i=>o.bind(s)(i.value,i.key,i))))}reject(o,s){return this.filter(u(o.bind(s)))}forEach(o,s){return this.elements.forEach(((i,u)=>{o.bind(s)(i.value,i.key,i,u)}))}keys(){return this.map(((o,s)=>s.toValue()))}values(){return this.map((o=>o.toValue()))}}o.exports=ObjectSlice},86804:(o,s,i)=>{const u=i(10316),_=i(41067),w=i(71167),x=i(40239),C=i(12242),j=i(6233),L=i(87726),B=i(61045),$=i(86303),V=i(14540),U=i(92340),z=i(10866),Y=i(55973);function refract(o){if(o instanceof u)return o;if("string"==typeof o)return new w(o);if("number"==typeof o)return new x(o);if("boolean"==typeof o)return new C(o);if(null===o)return new _;if(Array.isArray(o))return new j(o.map(refract));if("object"==typeof o){return new B(o)}return o}u.prototype.ObjectElement=B,u.prototype.RefElement=V,u.prototype.MemberElement=L,u.prototype.refract=refract,U.prototype.refract=refract,o.exports={Element:u,NullElement:_,StringElement:w,NumberElement:x,BooleanElement:C,ArrayElement:j,MemberElement:L,ObjectElement:B,LinkElement:$,RefElement:V,refract,ArraySlice:U,ObjectSlice:z,KeyValuePair:Y}},86303:(o,s,i)=>{const u=i(10316);o.exports=class LinkElement extends u{constructor(o,s,i){super(o||[],s,i),this.element="link"}get relation(){return this.attributes.get("relation")}set relation(o){this.attributes.set("relation",o)}get href(){return this.attributes.get("href")}set href(o){this.attributes.set("href",o)}}},14540:(o,s,i)=>{const u=i(10316);o.exports=class RefElement extends u{constructor(o,s,i){super(o||[],s,i),this.element="ref",this.path||(this.path="element")}get path(){return this.attributes.get("path")}set path(o){this.attributes.set("path",o)}}},34035:(o,s,i)=>{const u=i(3110),_=i(86804);s.g$=u,s.KeyValuePair=i(55973),s.G6=_.ArraySlice,s.ot=_.ObjectSlice,s.Hg=_.Element,s.Om=_.StringElement,s.kT=_.NumberElement,s.bd=_.BooleanElement,s.Os=_.NullElement,s.wE=_.ArrayElement,s.Sh=_.ObjectElement,s.Pr=_.MemberElement,s.sI=_.RefElement,s.Ft=_.LinkElement,s.e=_.refract,i(85105),i(75147)},6233:(o,s,i)=>{const u=i(6048),_=i(10316),w=i(92340);class ArrayElement extends _{constructor(o,s,i){super(o||[],s,i),this.element="array"}primitive(){return"array"}get(o){return this.content[o]}getValue(o){const s=this.get(o);if(s)return s.toValue()}getIndex(o){return this.content[o]}set(o,s){return this.content[o]=this.refract(s),this}remove(o){const s=this.content.splice(o,1);return s.length?s[0]:null}map(o,s){return this.content.map(o,s)}flatMap(o,s){return this.map(o,s).reduce(((o,s)=>o.concat(s)),[])}compactMap(o,s){const i=[];return this.forEach((u=>{const _=o.bind(s)(u);_&&i.push(_)})),i}filter(o,s){return new w(this.content.filter(o,s))}reject(o,s){return this.filter(u(o),s)}reduce(o,s){let i,u;void 0!==s?(i=0,u=this.refract(s)):(i=1,u="object"===this.primitive()?this.first.value:this.first);for(let s=i;s{o.bind(s)(i,this.refract(u))}))}shift(){return this.content.shift()}unshift(o){this.content.unshift(this.refract(o))}push(o){return this.content.push(this.refract(o)),this}add(o){this.push(o)}findElements(o,s){const i=s||{},u=!!i.recursive,_=void 0===i.results?[]:i.results;return this.forEach(((s,i,w)=>{u&&void 0!==s.findElements&&s.findElements(o,{results:_,recursive:u}),o(s,i,w)&&_.push(s)})),_}find(o){return new w(this.findElements(o,{recursive:!0}))}findByElement(o){return this.find((s=>s.element===o))}findByClass(o){return this.find((s=>s.classes.includes(o)))}getById(o){return this.find((s=>s.id.toValue()===o)).first}includes(o){return this.content.some((s=>s.equals(o)))}contains(o){return this.includes(o)}empty(){return new this.constructor([])}"fantasy-land/empty"(){return this.empty()}concat(o){return new this.constructor(this.content.concat(o.content))}"fantasy-land/concat"(o){return this.concat(o)}"fantasy-land/map"(o){return new this.constructor(this.map(o))}"fantasy-land/chain"(o){return this.map((s=>o(s)),this).reduce(((o,s)=>o.concat(s)),this.empty())}"fantasy-land/filter"(o){return new this.constructor(this.content.filter(o))}"fantasy-land/reduce"(o,s){return this.content.reduce(o,s)}get length(){return this.content.length}get isEmpty(){return 0===this.content.length}get first(){return this.getIndex(0)}get second(){return this.getIndex(1)}get last(){return this.getIndex(this.length-1)}}ArrayElement.empty=function empty(){return new this},ArrayElement["fantasy-land/empty"]=ArrayElement.empty,"undefined"!=typeof Symbol&&(ArrayElement.prototype[Symbol.iterator]=function symbol(){return this.content[Symbol.iterator]()}),o.exports=ArrayElement},12242:(o,s,i)=>{const u=i(10316);o.exports=class BooleanElement extends u{constructor(o,s,i){super(o,s,i),this.element="boolean"}primitive(){return"boolean"}}},10316:(o,s,i)=>{const u=i(2404),_=i(55973),w=i(92340);class Element{constructor(o,s,i){s&&(this.meta=s),i&&(this.attributes=i),this.content=o}freeze(){Object.isFrozen(this)||(this._meta&&(this.meta.parent=this,this.meta.freeze()),this._attributes&&(this.attributes.parent=this,this.attributes.freeze()),this.children.forEach((o=>{o.parent=this,o.freeze()}),this),this.content&&Array.isArray(this.content)&&Object.freeze(this.content),Object.freeze(this))}primitive(){}clone(){const o=new this.constructor;return o.element=this.element,this.meta.length&&(o._meta=this.meta.clone()),this.attributes.length&&(o._attributes=this.attributes.clone()),this.content?this.content.clone?o.content=this.content.clone():Array.isArray(this.content)?o.content=this.content.map((o=>o.clone())):o.content=this.content:o.content=this.content,o}toValue(){return this.content instanceof Element?this.content.toValue():this.content instanceof _?{key:this.content.key.toValue(),value:this.content.value?this.content.value.toValue():void 0}:this.content&&this.content.map?this.content.map((o=>o.toValue()),this):this.content}toRef(o){if(""===this.id.toValue())throw Error("Cannot create reference to an element that does not contain an ID");const s=new this.RefElement(this.id.toValue());return o&&(s.path=o),s}findRecursive(...o){if(arguments.length>1&&!this.isFrozen)throw new Error("Cannot find recursive with multiple element names without first freezing the element. Call `element.freeze()`");const s=o.pop();let i=new w;const append=(o,s)=>(o.push(s),o),checkElement=(o,i)=>{i.element===s&&o.push(i);const u=i.findRecursive(s);return u&&u.reduce(append,o),i.content instanceof _&&(i.content.key&&checkElement(o,i.content.key),i.content.value&&checkElement(o,i.content.value)),o};return this.content&&(this.content.element&&checkElement(i,this.content),Array.isArray(this.content)&&this.content.reduce(checkElement,i)),o.isEmpty||(i=i.filter((s=>{let i=s.parents.map((o=>o.element));for(const s in o){const u=o[s],_=i.indexOf(u);if(-1===_)return!1;i=i.splice(0,_)}return!0}))),i}set(o){return this.content=o,this}equals(o){return u(this.toValue(),o)}getMetaProperty(o,s){if(!this.meta.hasKey(o)){if(this.isFrozen){const o=this.refract(s);return o.freeze(),o}this.meta.set(o,s)}return this.meta.get(o)}setMetaProperty(o,s){this.meta.set(o,s)}get element(){return this._storedElement||"element"}set element(o){this._storedElement=o}get content(){return this._content}set content(o){if(o instanceof Element)this._content=o;else if(o instanceof w)this.content=o.elements;else if("string"==typeof o||"number"==typeof o||"boolean"==typeof o||"null"===o||null==o)this._content=o;else if(o instanceof _)this._content=o;else if(Array.isArray(o))this._content=o.map(this.refract);else{if("object"!=typeof o)throw new Error("Cannot set content to given value");this._content=Object.keys(o).map((s=>new this.MemberElement(s,o[s])))}}get meta(){if(!this._meta){if(this.isFrozen){const o=new this.ObjectElement;return o.freeze(),o}this._meta=new this.ObjectElement}return this._meta}set meta(o){o instanceof this.ObjectElement?this._meta=o:this.meta.set(o||{})}get attributes(){if(!this._attributes){if(this.isFrozen){const o=new this.ObjectElement;return o.freeze(),o}this._attributes=new this.ObjectElement}return this._attributes}set attributes(o){o instanceof this.ObjectElement?this._attributes=o:this.attributes.set(o||{})}get id(){return this.getMetaProperty("id","")}set id(o){this.setMetaProperty("id",o)}get classes(){return this.getMetaProperty("classes",[])}set classes(o){this.setMetaProperty("classes",o)}get title(){return this.getMetaProperty("title","")}set title(o){this.setMetaProperty("title",o)}get description(){return this.getMetaProperty("description","")}set description(o){this.setMetaProperty("description",o)}get links(){return this.getMetaProperty("links",[])}set links(o){this.setMetaProperty("links",o)}get isFrozen(){return Object.isFrozen(this)}get parents(){let{parent:o}=this;const s=new w;for(;o;)s.push(o),o=o.parent;return s}get children(){if(Array.isArray(this.content))return new w(this.content);if(this.content instanceof _){const o=new w([this.content.key]);return this.content.value&&o.push(this.content.value),o}return this.content instanceof Element?new w([this.content]):new w}get recursiveChildren(){const o=new w;return this.children.forEach((s=>{o.push(s),s.recursiveChildren.forEach((s=>{o.push(s)}))})),o}}o.exports=Element},87726:(o,s,i)=>{const u=i(55973),_=i(10316);o.exports=class MemberElement extends _{constructor(o,s,i,_){super(new u,i,_),this.element="member",this.key=o,this.value=s}get key(){return this.content.key}set key(o){this.content.key=this.refract(o)}get value(){return this.content.value}set value(o){this.content.value=this.refract(o)}}},41067:(o,s,i)=>{const u=i(10316);o.exports=class NullElement extends u{constructor(o,s,i){super(o||null,s,i),this.element="null"}primitive(){return"null"}set(){return new Error("Cannot set the value of null")}}},40239:(o,s,i)=>{const u=i(10316);o.exports=class NumberElement extends u{constructor(o,s,i){super(o,s,i),this.element="number"}primitive(){return"number"}}},61045:(o,s,i)=>{const u=i(6048),_=i(23805),w=i(6233),x=i(87726),C=i(10866);o.exports=class ObjectElement extends w{constructor(o,s,i){super(o||[],s,i),this.element="object"}primitive(){return"object"}toValue(){return this.content.reduce(((o,s)=>(o[s.key.toValue()]=s.value?s.value.toValue():void 0,o)),{})}get(o){const s=this.getMember(o);if(s)return s.value}getMember(o){if(void 0!==o)return this.content.find((s=>s.key.toValue()===o))}remove(o){let s=null;return this.content=this.content.filter((i=>i.key.toValue()!==o||(s=i,!1))),s}getKey(o){const s=this.getMember(o);if(s)return s.key}set(o,s){if(_(o))return Object.keys(o).forEach((s=>{this.set(s,o[s])})),this;const i=o,u=this.getMember(i);return u?u.value=s:this.content.push(new x(i,s)),this}keys(){return this.content.map((o=>o.key.toValue()))}values(){return this.content.map((o=>o.value.toValue()))}hasKey(o){return this.content.some((s=>s.key.equals(o)))}items(){return this.content.map((o=>[o.key.toValue(),o.value.toValue()]))}map(o,s){return this.content.map((i=>o.bind(s)(i.value,i.key,i)))}compactMap(o,s){const i=[];return this.forEach(((u,_,w)=>{const x=o.bind(s)(u,_,w);x&&i.push(x)})),i}filter(o,s){return new C(this.content).filter(o,s)}reject(o,s){return this.filter(u(o),s)}forEach(o,s){return this.content.forEach((i=>o.bind(s)(i.value,i.key,i)))}}},71167:(o,s,i)=>{const u=i(10316);o.exports=class StringElement extends u{constructor(o,s,i){super(o,s,i),this.element="string"}primitive(){return"string"}get length(){return this.content.length}}},75147:(o,s,i)=>{const u=i(85105);o.exports=class JSON06Serialiser extends u{serialise(o){if(!(o instanceof this.namespace.elements.Element))throw new TypeError(`Given element \`${o}\` is not an Element instance`);let s;o._attributes&&o.attributes.get("variable")&&(s=o.attributes.get("variable"));const i={element:o.element};o._meta&&o._meta.length>0&&(i.meta=this.serialiseObject(o.meta));const u="enum"===o.element||-1!==o.attributes.keys().indexOf("enumerations");if(u){const s=this.enumSerialiseAttributes(o);s&&(i.attributes=s)}else if(o._attributes&&o._attributes.length>0){let{attributes:u}=o;u.get("metadata")&&(u=u.clone(),u.set("meta",u.get("metadata")),u.remove("metadata")),"member"===o.element&&s&&(u=u.clone(),u.remove("variable")),u.length>0&&(i.attributes=this.serialiseObject(u))}if(u)i.content=this.enumSerialiseContent(o,i);else if(this[`${o.element}SerialiseContent`])i.content=this[`${o.element}SerialiseContent`](o,i);else if(void 0!==o.content){let u;s&&o.content.key?(u=o.content.clone(),u.key.attributes.set("variable",s),u=this.serialiseContent(u)):u=this.serialiseContent(o.content),this.shouldSerialiseContent(o,u)&&(i.content=u)}else this.shouldSerialiseContent(o,o.content)&&o instanceof this.namespace.elements.Array&&(i.content=[]);return i}shouldSerialiseContent(o,s){return"parseResult"===o.element||"httpRequest"===o.element||"httpResponse"===o.element||"category"===o.element||"link"===o.element||void 0!==s&&(!Array.isArray(s)||0!==s.length)}refSerialiseContent(o,s){return delete s.attributes,{href:o.toValue(),path:o.path.toValue()}}sourceMapSerialiseContent(o){return o.toValue()}dataStructureSerialiseContent(o){return[this.serialiseContent(o.content)]}enumSerialiseAttributes(o){const s=o.attributes.clone(),i=s.remove("enumerations")||new this.namespace.elements.Array([]),u=s.get("default");let _=s.get("samples")||new this.namespace.elements.Array([]);if(u&&u.content&&(u.content.attributes&&u.content.attributes.remove("typeAttributes"),s.set("default",new this.namespace.elements.Array([u.content]))),_.forEach((o=>{o.content&&o.content.element&&o.content.attributes.remove("typeAttributes")})),o.content&&0!==i.length&&_.unshift(o.content),_=_.map((o=>o instanceof this.namespace.elements.Array?[o]:new this.namespace.elements.Array([o.content]))),_.length&&s.set("samples",_),s.length>0)return this.serialiseObject(s)}enumSerialiseContent(o){if(o._attributes){const s=o.attributes.get("enumerations");if(s&&s.length>0)return s.content.map((o=>{const s=o.clone();return s.attributes.remove("typeAttributes"),this.serialise(s)}))}if(o.content){const s=o.content.clone();return s.attributes.remove("typeAttributes"),[this.serialise(s)]}return[]}deserialise(o){if("string"==typeof o)return new this.namespace.elements.String(o);if("number"==typeof o)return new this.namespace.elements.Number(o);if("boolean"==typeof o)return new this.namespace.elements.Boolean(o);if(null===o)return new this.namespace.elements.Null;if(Array.isArray(o))return new this.namespace.elements.Array(o.map(this.deserialise,this));const s=this.namespace.getElementClass(o.element),i=new s;i.element!==o.element&&(i.element=o.element),o.meta&&this.deserialiseObject(o.meta,i.meta),o.attributes&&this.deserialiseObject(o.attributes,i.attributes);const u=this.deserialiseContent(o.content);if(void 0===u&&null!==i.content||(i.content=u),"enum"===i.element){i.content&&i.attributes.set("enumerations",i.content);let o=i.attributes.get("samples");if(i.attributes.remove("samples"),o){const u=o;o=new this.namespace.elements.Array,u.forEach((u=>{u.forEach((u=>{const _=new s(u);_.element=i.element,o.push(_)}))}));const _=o.shift();i.content=_?_.content:void 0,i.attributes.set("samples",o)}else i.content=void 0;let u=i.attributes.get("default");if(u&&u.length>0){u=u.get(0);const o=new s(u);o.element=i.element,i.attributes.set("default",o)}}else if("dataStructure"===i.element&&Array.isArray(i.content))[i.content]=i.content;else if("category"===i.element){const o=i.attributes.get("meta");o&&(i.attributes.set("metadata",o),i.attributes.remove("meta"))}else"member"===i.element&&i.key&&i.key._attributes&&i.key._attributes.getValue("variable")&&(i.attributes.set("variable",i.key.attributes.get("variable")),i.key.attributes.remove("variable"));return i}serialiseContent(o){if(o instanceof this.namespace.elements.Element)return this.serialise(o);if(o instanceof this.namespace.KeyValuePair){const s={key:this.serialise(o.key)};return o.value&&(s.value=this.serialise(o.value)),s}return o&&o.map?o.map(this.serialise,this):o}deserialiseContent(o){if(o){if(o.element)return this.deserialise(o);if(o.key){const s=new this.namespace.KeyValuePair(this.deserialise(o.key));return o.value&&(s.value=this.deserialise(o.value)),s}if(o.map)return o.map(this.deserialise,this)}return o}shouldRefract(o){return!!(o._attributes&&o.attributes.keys().length||o._meta&&o.meta.keys().length)||"enum"!==o.element&&(o.element!==o.primitive()||"member"===o.element)}convertKeyToRefract(o,s){return this.shouldRefract(s)?this.serialise(s):"enum"===s.element?this.serialiseEnum(s):"array"===s.element?s.map((s=>this.shouldRefract(s)||"default"===o?this.serialise(s):"array"===s.element||"object"===s.element||"enum"===s.element?s.children.map((o=>this.serialise(o))):s.toValue())):"object"===s.element?(s.content||[]).map(this.serialise,this):s.toValue()}serialiseEnum(o){return o.children.map((o=>this.serialise(o)))}serialiseObject(o){const s={};return o.forEach(((o,i)=>{if(o){const u=i.toValue();s[u]=this.convertKeyToRefract(u,o)}})),s}deserialiseObject(o,s){Object.keys(o).forEach((i=>{s.set(i,this.deserialise(o[i]))}))}}},85105:o=>{o.exports=class JSONSerialiser{constructor(o){this.namespace=o||new this.Namespace}serialise(o){if(!(o instanceof this.namespace.elements.Element))throw new TypeError(`Given element \`${o}\` is not an Element instance`);const s={element:o.element};o._meta&&o._meta.length>0&&(s.meta=this.serialiseObject(o.meta)),o._attributes&&o._attributes.length>0&&(s.attributes=this.serialiseObject(o.attributes));const i=this.serialiseContent(o.content);return void 0!==i&&(s.content=i),s}deserialise(o){if(!o.element)throw new Error("Given value is not an object containing an element name");const s=new(this.namespace.getElementClass(o.element));s.element!==o.element&&(s.element=o.element),o.meta&&this.deserialiseObject(o.meta,s.meta),o.attributes&&this.deserialiseObject(o.attributes,s.attributes);const i=this.deserialiseContent(o.content);return void 0===i&&null!==s.content||(s.content=i),s}serialiseContent(o){if(o instanceof this.namespace.elements.Element)return this.serialise(o);if(o instanceof this.namespace.KeyValuePair){const s={key:this.serialise(o.key)};return o.value&&(s.value=this.serialise(o.value)),s}if(o&&o.map){if(0===o.length)return;return o.map(this.serialise,this)}return o}deserialiseContent(o){if(o){if(o.element)return this.deserialise(o);if(o.key){const s=new this.namespace.KeyValuePair(this.deserialise(o.key));return o.value&&(s.value=this.deserialise(o.value)),s}if(o.map)return o.map(this.deserialise,this)}return o}serialiseObject(o){const s={};if(o.forEach(((o,i)=>{o&&(s[i.toValue()]=this.serialise(o))})),0!==Object.keys(s).length)return s}deserialiseObject(o,s){Object.keys(o).forEach((i=>{s.set(i,this.deserialise(o[i]))}))}}},58859:(o,s,i)=>{var u="function"==typeof Map&&Map.prototype,_=Object.getOwnPropertyDescriptor&&u?Object.getOwnPropertyDescriptor(Map.prototype,"size"):null,w=u&&_&&"function"==typeof _.get?_.get:null,x=u&&Map.prototype.forEach,C="function"==typeof Set&&Set.prototype,j=Object.getOwnPropertyDescriptor&&C?Object.getOwnPropertyDescriptor(Set.prototype,"size"):null,L=C&&j&&"function"==typeof j.get?j.get:null,B=C&&Set.prototype.forEach,$="function"==typeof WeakMap&&WeakMap.prototype?WeakMap.prototype.has:null,V="function"==typeof WeakSet&&WeakSet.prototype?WeakSet.prototype.has:null,U="function"==typeof WeakRef&&WeakRef.prototype?WeakRef.prototype.deref:null,z=Boolean.prototype.valueOf,Y=Object.prototype.toString,Z=Function.prototype.toString,ee=String.prototype.match,ie=String.prototype.slice,ae=String.prototype.replace,ce=String.prototype.toUpperCase,le=String.prototype.toLowerCase,pe=RegExp.prototype.test,de=Array.prototype.concat,fe=Array.prototype.join,ye=Array.prototype.slice,be=Math.floor,_e="function"==typeof BigInt?BigInt.prototype.valueOf:null,we=Object.getOwnPropertySymbols,Se="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?Symbol.prototype.toString:null,xe="function"==typeof Symbol&&"object"==typeof Symbol.iterator,Pe="function"==typeof Symbol&&Symbol.toStringTag&&(typeof Symbol.toStringTag===xe||"symbol")?Symbol.toStringTag:null,Te=Object.prototype.propertyIsEnumerable,Re=("function"==typeof Reflect?Reflect.getPrototypeOf:Object.getPrototypeOf)||([].__proto__===Array.prototype?function(o){return o.__proto__}:null);function addNumericSeparator(o,s){if(o===1/0||o===-1/0||o!=o||o&&o>-1e3&&o<1e3||pe.call(/e/,s))return s;var i=/[0-9](?=(?:[0-9]{3})+(?![0-9]))/g;if("number"==typeof o){var u=o<0?-be(-o):be(o);if(u!==o){var _=String(u),w=ie.call(s,_.length+1);return ae.call(_,i,"$&_")+"."+ae.call(ae.call(w,/([0-9]{3})/g,"$&_"),/_$/,"")}}return ae.call(s,i,"$&_")}var qe=i(42634),$e=qe.custom,ze=isSymbol($e)?$e:null;function wrapQuotes(o,s,i){var u="double"===(i.quoteStyle||s)?'"':"'";return u+o+u}function quote(o){return ae.call(String(o),/"/g,""")}function isArray(o){return!("[object Array]"!==toStr(o)||Pe&&"object"==typeof o&&Pe in o)}function isRegExp(o){return!("[object RegExp]"!==toStr(o)||Pe&&"object"==typeof o&&Pe in o)}function isSymbol(o){if(xe)return o&&"object"==typeof o&&o instanceof Symbol;if("symbol"==typeof o)return!0;if(!o||"object"!=typeof o||!Se)return!1;try{return Se.call(o),!0}catch(o){}return!1}o.exports=function inspect_(o,s,u,_){var C=s||{};if(has(C,"quoteStyle")&&"single"!==C.quoteStyle&&"double"!==C.quoteStyle)throw new TypeError('option "quoteStyle" must be "single" or "double"');if(has(C,"maxStringLength")&&("number"==typeof C.maxStringLength?C.maxStringLength<0&&C.maxStringLength!==1/0:null!==C.maxStringLength))throw new TypeError('option "maxStringLength", if provided, must be a positive integer, Infinity, or `null`');var j=!has(C,"customInspect")||C.customInspect;if("boolean"!=typeof j&&"symbol"!==j)throw new TypeError("option \"customInspect\", if provided, must be `true`, `false`, or `'symbol'`");if(has(C,"indent")&&null!==C.indent&&"\t"!==C.indent&&!(parseInt(C.indent,10)===C.indent&&C.indent>0))throw new TypeError('option "indent" must be "\\t", an integer > 0, or `null`');if(has(C,"numericSeparator")&&"boolean"!=typeof C.numericSeparator)throw new TypeError('option "numericSeparator", if provided, must be `true` or `false`');var Y=C.numericSeparator;if(void 0===o)return"undefined";if(null===o)return"null";if("boolean"==typeof o)return o?"true":"false";if("string"==typeof o)return inspectString(o,C);if("number"==typeof o){if(0===o)return 1/0/o>0?"0":"-0";var ce=String(o);return Y?addNumericSeparator(o,ce):ce}if("bigint"==typeof o){var pe=String(o)+"n";return Y?addNumericSeparator(o,pe):pe}var be=void 0===C.depth?5:C.depth;if(void 0===u&&(u=0),u>=be&&be>0&&"object"==typeof o)return isArray(o)?"[Array]":"[Object]";var we=function getIndent(o,s){var i;if("\t"===o.indent)i="\t";else{if(!("number"==typeof o.indent&&o.indent>0))return null;i=fe.call(Array(o.indent+1)," ")}return{base:i,prev:fe.call(Array(s+1),i)}}(C,u);if(void 0===_)_=[];else if(indexOf(_,o)>=0)return"[Circular]";function inspect(o,s,i){if(s&&(_=ye.call(_)).push(s),i){var w={depth:C.depth};return has(C,"quoteStyle")&&(w.quoteStyle=C.quoteStyle),inspect_(o,w,u+1,_)}return inspect_(o,C,u+1,_)}if("function"==typeof o&&!isRegExp(o)){var $e=function nameOf(o){if(o.name)return o.name;var s=ee.call(Z.call(o),/^function\s*([\w$]+)/);if(s)return s[1];return null}(o),We=arrObjKeys(o,inspect);return"[Function"+($e?": "+$e:" (anonymous)")+"]"+(We.length>0?" { "+fe.call(We,", ")+" }":"")}if(isSymbol(o)){var He=xe?ae.call(String(o),/^(Symbol\(.*\))_[^)]*$/,"$1"):Se.call(o);return"object"!=typeof o||xe?He:markBoxed(He)}if(function isElement(o){if(!o||"object"!=typeof o)return!1;if("undefined"!=typeof HTMLElement&&o instanceof HTMLElement)return!0;return"string"==typeof o.nodeName&&"function"==typeof o.getAttribute}(o)){for(var Ye="<"+le.call(String(o.nodeName)),Xe=o.attributes||[],Qe=0;Qe"}if(isArray(o)){if(0===o.length)return"[]";var et=arrObjKeys(o,inspect);return we&&!function singleLineValues(o){for(var s=0;s=0)return!1;return!0}(et)?"["+indentedJoin(et,we)+"]":"[ "+fe.call(et,", ")+" ]"}if(function isError(o){return!("[object Error]"!==toStr(o)||Pe&&"object"==typeof o&&Pe in o)}(o)){var tt=arrObjKeys(o,inspect);return"cause"in Error.prototype||!("cause"in o)||Te.call(o,"cause")?0===tt.length?"["+String(o)+"]":"{ ["+String(o)+"] "+fe.call(tt,", ")+" }":"{ ["+String(o)+"] "+fe.call(de.call("[cause]: "+inspect(o.cause),tt),", ")+" }"}if("object"==typeof o&&j){if(ze&&"function"==typeof o[ze]&&qe)return qe(o,{depth:be-u});if("symbol"!==j&&"function"==typeof o.inspect)return o.inspect()}if(function isMap(o){if(!w||!o||"object"!=typeof o)return!1;try{w.call(o);try{L.call(o)}catch(o){return!0}return o instanceof Map}catch(o){}return!1}(o)){var rt=[];return x&&x.call(o,(function(s,i){rt.push(inspect(i,o,!0)+" => "+inspect(s,o))})),collectionOf("Map",w.call(o),rt,we)}if(function isSet(o){if(!L||!o||"object"!=typeof o)return!1;try{L.call(o);try{w.call(o)}catch(o){return!0}return o instanceof Set}catch(o){}return!1}(o)){var nt=[];return B&&B.call(o,(function(s){nt.push(inspect(s,o))})),collectionOf("Set",L.call(o),nt,we)}if(function isWeakMap(o){if(!$||!o||"object"!=typeof o)return!1;try{$.call(o,$);try{V.call(o,V)}catch(o){return!0}return o instanceof WeakMap}catch(o){}return!1}(o))return weakCollectionOf("WeakMap");if(function isWeakSet(o){if(!V||!o||"object"!=typeof o)return!1;try{V.call(o,V);try{$.call(o,$)}catch(o){return!0}return o instanceof WeakSet}catch(o){}return!1}(o))return weakCollectionOf("WeakSet");if(function isWeakRef(o){if(!U||!o||"object"!=typeof o)return!1;try{return U.call(o),!0}catch(o){}return!1}(o))return weakCollectionOf("WeakRef");if(function isNumber(o){return!("[object Number]"!==toStr(o)||Pe&&"object"==typeof o&&Pe in o)}(o))return markBoxed(inspect(Number(o)));if(function isBigInt(o){if(!o||"object"!=typeof o||!_e)return!1;try{return _e.call(o),!0}catch(o){}return!1}(o))return markBoxed(inspect(_e.call(o)));if(function isBoolean(o){return!("[object Boolean]"!==toStr(o)||Pe&&"object"==typeof o&&Pe in o)}(o))return markBoxed(z.call(o));if(function isString(o){return!("[object String]"!==toStr(o)||Pe&&"object"==typeof o&&Pe in o)}(o))return markBoxed(inspect(String(o)));if("undefined"!=typeof window&&o===window)return"{ [object Window] }";if(o===i.g)return"{ [object globalThis] }";if(!function isDate(o){return!("[object Date]"!==toStr(o)||Pe&&"object"==typeof o&&Pe in o)}(o)&&!isRegExp(o)){var ot=arrObjKeys(o,inspect),st=Re?Re(o)===Object.prototype:o instanceof Object||o.constructor===Object,it=o instanceof Object?"":"null prototype",at=!st&&Pe&&Object(o)===o&&Pe in o?ie.call(toStr(o),8,-1):it?"Object":"",ct=(st||"function"!=typeof o.constructor?"":o.constructor.name?o.constructor.name+" ":"")+(at||it?"["+fe.call(de.call([],at||[],it||[]),": ")+"] ":"");return 0===ot.length?ct+"{}":we?ct+"{"+indentedJoin(ot,we)+"}":ct+"{ "+fe.call(ot,", ")+" }"}return String(o)};var We=Object.prototype.hasOwnProperty||function(o){return o in this};function has(o,s){return We.call(o,s)}function toStr(o){return Y.call(o)}function indexOf(o,s){if(o.indexOf)return o.indexOf(s);for(var i=0,u=o.length;is.maxStringLength){var i=o.length-s.maxStringLength,u="... "+i+" more character"+(i>1?"s":"");return inspectString(ie.call(o,0,s.maxStringLength),s)+u}return wrapQuotes(ae.call(ae.call(o,/(['\\])/g,"\\$1"),/[\x00-\x1f]/g,lowbyte),"single",s)}function lowbyte(o){var s=o.charCodeAt(0),i={8:"b",9:"t",10:"n",12:"f",13:"r"}[s];return i?"\\"+i:"\\x"+(s<16?"0":"")+ce.call(s.toString(16))}function markBoxed(o){return"Object("+o+")"}function weakCollectionOf(o){return o+" { ? }"}function collectionOf(o,s,i,u){return o+" ("+s+") {"+(u?indentedJoin(i,u):fe.call(i,", "))+"}"}function indentedJoin(o,s){if(0===o.length)return"";var i="\n"+s.prev+s.base;return i+fe.call(o,","+i)+"\n"+s.prev}function arrObjKeys(o,s){var i=isArray(o),u=[];if(i){u.length=o.length;for(var _=0;_{var s,i,u=o.exports={};function defaultSetTimout(){throw new Error("setTimeout has not been defined")}function defaultClearTimeout(){throw new Error("clearTimeout has not been defined")}function runTimeout(o){if(s===setTimeout)return setTimeout(o,0);if((s===defaultSetTimout||!s)&&setTimeout)return s=setTimeout,setTimeout(o,0);try{return s(o,0)}catch(i){try{return s.call(null,o,0)}catch(i){return s.call(this,o,0)}}}!function(){try{s="function"==typeof setTimeout?setTimeout:defaultSetTimout}catch(o){s=defaultSetTimout}try{i="function"==typeof clearTimeout?clearTimeout:defaultClearTimeout}catch(o){i=defaultClearTimeout}}();var _,w=[],x=!1,C=-1;function cleanUpNextTick(){x&&_&&(x=!1,_.length?w=_.concat(w):C=-1,w.length&&drainQueue())}function drainQueue(){if(!x){var o=runTimeout(cleanUpNextTick);x=!0;for(var s=w.length;s;){for(_=w,w=[];++C1)for(var i=1;i{"use strict";var u=i(6925);function emptyFunction(){}function emptyFunctionWithReset(){}emptyFunctionWithReset.resetWarningCache=emptyFunction,o.exports=function(){function shim(o,s,i,_,w,x){if(x!==u){var C=new Error("Calling PropTypes validators directly is not supported by the `prop-types` package. Use PropTypes.checkPropTypes() to call them. Read more at http://fb.me/use-check-prop-types");throw C.name="Invariant Violation",C}}function getShim(){return shim}shim.isRequired=shim;var o={array:shim,bigint:shim,bool:shim,func:shim,number:shim,object:shim,string:shim,symbol:shim,any:shim,arrayOf:getShim,element:shim,elementType:shim,instanceOf:getShim,node:shim,objectOf:getShim,oneOf:getShim,oneOfType:getShim,shape:getShim,exact:getShim,checkPropTypes:emptyFunctionWithReset,resetWarningCache:emptyFunction};return o.PropTypes=o,o}},5556:(o,s,i)=>{o.exports=i(2694)()},6925:o=>{"use strict";o.exports="SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED"},74765:o=>{"use strict";var s=String.prototype.replace,i=/%20/g,u="RFC1738",_="RFC3986";o.exports={default:_,formatters:{RFC1738:function(o){return s.call(o,i,"+")},RFC3986:function(o){return String(o)}},RFC1738:u,RFC3986:_}},55373:(o,s,i)=>{"use strict";var u=i(98636),_=i(62642),w=i(74765);o.exports={formats:w,parse:_,stringify:u}},62642:(o,s,i)=>{"use strict";var u=i(37720),_=Object.prototype.hasOwnProperty,w=Array.isArray,x={allowDots:!1,allowPrototypes:!1,allowSparse:!1,arrayLimit:20,charset:"utf-8",charsetSentinel:!1,comma:!1,decoder:u.decode,delimiter:"&",depth:5,ignoreQueryPrefix:!1,interpretNumericEntities:!1,parameterLimit:1e3,parseArrays:!0,plainObjects:!1,strictNullHandling:!1},interpretNumericEntities=function(o){return o.replace(/&#(\d+);/g,(function(o,s){return String.fromCharCode(parseInt(s,10))}))},parseArrayValue=function(o,s){return o&&"string"==typeof o&&s.comma&&o.indexOf(",")>-1?o.split(","):o},C=function parseQueryStringKeys(o,s,i,u){if(o){var w=i.allowDots?o.replace(/\.([^.[]+)/g,"[$1]"):o,x=/(\[[^[\]]*])/g,C=i.depth>0&&/(\[[^[\]]*])/.exec(w),j=C?w.slice(0,C.index):w,L=[];if(j){if(!i.plainObjects&&_.call(Object.prototype,j)&&!i.allowPrototypes)return;L.push(j)}for(var B=0;i.depth>0&&null!==(C=x.exec(w))&&B=0;--w){var x,C=o[w];if("[]"===C&&i.parseArrays)x=[].concat(_);else{x=i.plainObjects?Object.create(null):{};var j="["===C.charAt(0)&&"]"===C.charAt(C.length-1)?C.slice(1,-1):C,L=parseInt(j,10);i.parseArrays||""!==j?!isNaN(L)&&C!==j&&String(L)===j&&L>=0&&i.parseArrays&&L<=i.arrayLimit?(x=[])[L]=_:"__proto__"!==j&&(x[j]=_):x={0:_}}_=x}return _}(L,s,i,u)}};o.exports=function(o,s){var i=function normalizeParseOptions(o){if(!o)return x;if(null!==o.decoder&&void 0!==o.decoder&&"function"!=typeof o.decoder)throw new TypeError("Decoder has to be a function.");if(void 0!==o.charset&&"utf-8"!==o.charset&&"iso-8859-1"!==o.charset)throw new TypeError("The charset option must be either utf-8, iso-8859-1, or undefined");var s=void 0===o.charset?x.charset:o.charset;return{allowDots:void 0===o.allowDots?x.allowDots:!!o.allowDots,allowPrototypes:"boolean"==typeof o.allowPrototypes?o.allowPrototypes:x.allowPrototypes,allowSparse:"boolean"==typeof o.allowSparse?o.allowSparse:x.allowSparse,arrayLimit:"number"==typeof o.arrayLimit?o.arrayLimit:x.arrayLimit,charset:s,charsetSentinel:"boolean"==typeof o.charsetSentinel?o.charsetSentinel:x.charsetSentinel,comma:"boolean"==typeof o.comma?o.comma:x.comma,decoder:"function"==typeof o.decoder?o.decoder:x.decoder,delimiter:"string"==typeof o.delimiter||u.isRegExp(o.delimiter)?o.delimiter:x.delimiter,depth:"number"==typeof o.depth||!1===o.depth?+o.depth:x.depth,ignoreQueryPrefix:!0===o.ignoreQueryPrefix,interpretNumericEntities:"boolean"==typeof o.interpretNumericEntities?o.interpretNumericEntities:x.interpretNumericEntities,parameterLimit:"number"==typeof o.parameterLimit?o.parameterLimit:x.parameterLimit,parseArrays:!1!==o.parseArrays,plainObjects:"boolean"==typeof o.plainObjects?o.plainObjects:x.plainObjects,strictNullHandling:"boolean"==typeof o.strictNullHandling?o.strictNullHandling:x.strictNullHandling}}(s);if(""===o||null==o)return i.plainObjects?Object.create(null):{};for(var j="string"==typeof o?function parseQueryStringValues(o,s){var i,C={},j=s.ignoreQueryPrefix?o.replace(/^\?/,""):o,L=s.parameterLimit===1/0?void 0:s.parameterLimit,B=j.split(s.delimiter,L),$=-1,V=s.charset;if(s.charsetSentinel)for(i=0;i-1&&(z=w(z)?[z]:z),_.call(C,U)?C[U]=u.combine(C[U],z):C[U]=z}return C}(o,i):o,L=i.plainObjects?Object.create(null):{},B=Object.keys(j),$=0;${"use strict";var u=i(920),_=i(37720),w=i(74765),x=Object.prototype.hasOwnProperty,C={brackets:function brackets(o){return o+"[]"},comma:"comma",indices:function indices(o,s){return o+"["+s+"]"},repeat:function repeat(o){return o}},j=Array.isArray,L=String.prototype.split,B=Array.prototype.push,pushToArray=function(o,s){B.apply(o,j(s)?s:[s])},$=Date.prototype.toISOString,V=w.default,U={addQueryPrefix:!1,allowDots:!1,charset:"utf-8",charsetSentinel:!1,delimiter:"&",encode:!0,encoder:_.encode,encodeValuesOnly:!1,format:V,formatter:w.formatters[V],indices:!1,serializeDate:function serializeDate(o){return $.call(o)},skipNulls:!1,strictNullHandling:!1},z={},Y=function stringify(o,s,i,w,x,C,B,$,V,Y,Z,ee,ie,ae,ce,le){for(var pe=o,de=le,fe=0,ye=!1;void 0!==(de=de.get(z))&&!ye;){var be=de.get(o);if(fe+=1,void 0!==be){if(be===fe)throw new RangeError("Cyclic object value");ye=!0}void 0===de.get(z)&&(fe=0)}if("function"==typeof $?pe=$(s,pe):pe instanceof Date?pe=Z(pe):"comma"===i&&j(pe)&&(pe=_.maybeMap(pe,(function(o){return o instanceof Date?Z(o):o}))),null===pe){if(x)return B&&!ae?B(s,U.encoder,ce,"key",ee):s;pe=""}if(function isNonNullishPrimitive(o){return"string"==typeof o||"number"==typeof o||"boolean"==typeof o||"symbol"==typeof o||"bigint"==typeof o}(pe)||_.isBuffer(pe)){if(B){var _e=ae?s:B(s,U.encoder,ce,"key",ee);if("comma"===i&&ae){for(var we=L.call(String(pe),","),Se="",xe=0;xe0?pe.join(",")||null:void 0}];else if(j($))Pe=$;else{var Re=Object.keys(pe);Pe=V?Re.sort(V):Re}for(var qe=w&&j(pe)&&1===pe.length?s+"[]":s,$e=0;$e0?ce+ae:""}},37720:(o,s,i)=>{"use strict";var u=i(74765),_=Object.prototype.hasOwnProperty,w=Array.isArray,x=function(){for(var o=[],s=0;s<256;++s)o.push("%"+((s<16?"0":"")+s.toString(16)).toUpperCase());return o}(),C=function arrayToObject(o,s){for(var i=s&&s.plainObjects?Object.create(null):{},u=0;u1;){var s=o.pop(),i=s.obj[s.prop];if(w(i)){for(var u=[],_=0;_=48&&B<=57||B>=65&&B<=90||B>=97&&B<=122||w===u.RFC1738&&(40===B||41===B)?j+=C.charAt(L):B<128?j+=x[B]:B<2048?j+=x[192|B>>6]+x[128|63&B]:B<55296||B>=57344?j+=x[224|B>>12]+x[128|B>>6&63]+x[128|63&B]:(L+=1,B=65536+((1023&B)<<10|1023&C.charCodeAt(L)),j+=x[240|B>>18]+x[128|B>>12&63]+x[128|B>>6&63]+x[128|63&B])}return j},isBuffer:function isBuffer(o){return!(!o||"object"!=typeof o)&&!!(o.constructor&&o.constructor.isBuffer&&o.constructor.isBuffer(o))},isRegExp:function isRegExp(o){return"[object RegExp]"===Object.prototype.toString.call(o)},maybeMap:function maybeMap(o,s){if(w(o)){for(var i=[],u=0;u{"use strict";var i=Object.prototype.hasOwnProperty;function decode(o){try{return decodeURIComponent(o.replace(/\+/g," "))}catch(o){return null}}function encode(o){try{return encodeURIComponent(o)}catch(o){return null}}s.stringify=function querystringify(o,s){s=s||"";var u,_,w=[];for(_ in"string"!=typeof s&&(s="?"),o)if(i.call(o,_)){if((u=o[_])||null!=u&&!isNaN(u)||(u=""),_=encode(_),u=encode(u),null===_||null===u)continue;w.push(_+"="+u)}return w.length?s+w.join("&"):""},s.parse=function querystring(o){for(var s,i=/([^=?#&]+)=?([^&]*)/g,u={};s=i.exec(o);){var _=decode(s[1]),w=decode(s[2]);null===_||null===w||_ in u||(u[_]=w)}return u}},41859:(o,s,i)=>{const u=i(27096),_=i(78004),w=u.types;o.exports=class RandExp{constructor(o,s){if(this._setDefaults(o),o instanceof RegExp)this.ignoreCase=o.ignoreCase,this.multiline=o.multiline,o=o.source;else{if("string"!=typeof o)throw new Error("Expected a regexp or string");this.ignoreCase=s&&-1!==s.indexOf("i"),this.multiline=s&&-1!==s.indexOf("m")}this.tokens=u(o)}_setDefaults(o){this.max=null!=o.max?o.max:null!=RandExp.prototype.max?RandExp.prototype.max:100,this.defaultRange=o.defaultRange?o.defaultRange:this.defaultRange.clone(),o.randInt&&(this.randInt=o.randInt)}gen(){return this._gen(this.tokens,[])}_gen(o,s){var i,u,_,x,C;switch(o.type){case w.ROOT:case w.GROUP:if(o.followedBy||o.notFollowedBy)return"";for(o.remember&&void 0===o.groupNumber&&(o.groupNumber=s.push(null)-1),u="",x=0,C=(i=o.options?this._randSelect(o.options):o.stack).length;x{"use strict";var u=i(65606),_=65536,w=4294967295;var x=i(92861).Buffer,C=i.g.crypto||i.g.msCrypto;C&&C.getRandomValues?o.exports=function randomBytes(o,s){if(o>w)throw new RangeError("requested too many random bytes");var i=x.allocUnsafe(o);if(o>0)if(o>_)for(var j=0;j{"use strict";function _typeof(o){return _typeof="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(o){return typeof o}:function(o){return o&&"function"==typeof Symbol&&o.constructor===Symbol&&o!==Symbol.prototype?"symbol":typeof o},_typeof(o)}Object.defineProperty(s,"__esModule",{value:!0}),s.CopyToClipboard=void 0;var u=_interopRequireDefault(i(96540)),_=_interopRequireDefault(i(17965)),w=["text","onCopy","options","children"];function _interopRequireDefault(o){return o&&o.__esModule?o:{default:o}}function ownKeys(o,s){var i=Object.keys(o);if(Object.getOwnPropertySymbols){var u=Object.getOwnPropertySymbols(o);s&&(u=u.filter((function(s){return Object.getOwnPropertyDescriptor(o,s).enumerable}))),i.push.apply(i,u)}return i}function _objectSpread(o){for(var s=1;s=0||(_[i]=o[i]);return _}(o,s);if(Object.getOwnPropertySymbols){var w=Object.getOwnPropertySymbols(o);for(u=0;u=0||Object.prototype.propertyIsEnumerable.call(o,i)&&(_[i]=o[i])}return _}function _defineProperties(o,s){for(var i=0;i{"use strict";var u=i(25264).CopyToClipboard;u.CopyToClipboard=u,o.exports=u},81214:(o,s,i)=>{"use strict";function _typeof(o){return _typeof="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(o){return typeof o}:function(o){return o&&"function"==typeof Symbol&&o.constructor===Symbol&&o!==Symbol.prototype?"symbol":typeof o},_typeof(o)}Object.defineProperty(s,"__esModule",{value:!0}),s.DebounceInput=void 0;var u=_interopRequireDefault(i(96540)),_=_interopRequireDefault(i(20181)),w=["element","onChange","value","minLength","debounceTimeout","forceNotifyByEnter","forceNotifyOnBlur","onKeyDown","onBlur","inputRef"];function _interopRequireDefault(o){return o&&o.__esModule?o:{default:o}}function _objectWithoutProperties(o,s){if(null==o)return{};var i,u,_=function _objectWithoutPropertiesLoose(o,s){if(null==o)return{};var i,u,_={},w=Object.keys(o);for(u=0;u=0||(_[i]=o[i]);return _}(o,s);if(Object.getOwnPropertySymbols){var w=Object.getOwnPropertySymbols(o);for(u=0;u=0||Object.prototype.propertyIsEnumerable.call(o,i)&&(_[i]=o[i])}return _}function ownKeys(o,s){var i=Object.keys(o);if(Object.getOwnPropertySymbols){var u=Object.getOwnPropertySymbols(o);s&&(u=u.filter((function(s){return Object.getOwnPropertyDescriptor(o,s).enumerable}))),i.push.apply(i,u)}return i}function _objectSpread(o){for(var s=1;s=u?i.notify(o):s.length>_.length&&i.notify(_objectSpread(_objectSpread({},o),{},{target:_objectSpread(_objectSpread({},o.target),{},{value:""})}))}))})),_defineProperty(_assertThisInitialized(i),"onKeyDown",(function(o){"Enter"===o.key&&i.forceNotify(o);var s=i.props.onKeyDown;s&&(o.persist(),s(o))})),_defineProperty(_assertThisInitialized(i),"onBlur",(function(o){i.forceNotify(o);var s=i.props.onBlur;s&&(o.persist(),s(o))})),_defineProperty(_assertThisInitialized(i),"createNotifier",(function(o){if(o<0)i.notify=function(){return null};else if(0===o)i.notify=i.doNotify;else{var s=(0,_.default)((function(o){i.isDebouncing=!1,i.doNotify(o)}),o);i.notify=function(o){i.isDebouncing=!0,s(o)},i.flush=function(){return s.flush()},i.cancel=function(){i.isDebouncing=!1,s.cancel()}}})),_defineProperty(_assertThisInitialized(i),"doNotify",(function(){i.props.onChange.apply(void 0,arguments)})),_defineProperty(_assertThisInitialized(i),"forceNotify",(function(o){var s=i.props.debounceTimeout;if(i.isDebouncing||!(s>0)){i.cancel&&i.cancel();var u=i.state.value,_=i.props.minLength;u.length>=_?i.doNotify(o):i.doNotify(_objectSpread(_objectSpread({},o),{},{target:_objectSpread(_objectSpread({},o.target),{},{value:u})}))}})),i.isDebouncing=!1,i.state={value:void 0===o.value||null===o.value?"":o.value};var u=i.props.debounceTimeout;return i.createNotifier(u),i}return function _createClass(o,s,i){return s&&_defineProperties(o.prototype,s),i&&_defineProperties(o,i),Object.defineProperty(o,"prototype",{writable:!1}),o}(DebounceInput,[{key:"componentDidUpdate",value:function componentDidUpdate(o){if(!this.isDebouncing){var s=this.props,i=s.value,u=s.debounceTimeout,_=o.debounceTimeout,w=o.value,x=this.state.value;void 0!==i&&w!==i&&x!==i&&this.setState({value:i}),u!==_&&this.createNotifier(u)}}},{key:"componentWillUnmount",value:function componentWillUnmount(){this.flush&&this.flush()}},{key:"render",value:function render(){var o,s,i=this.props,_=i.element,x=(i.onChange,i.value,i.minLength,i.debounceTimeout,i.forceNotifyByEnter),C=i.forceNotifyOnBlur,j=i.onKeyDown,L=i.onBlur,B=i.inputRef,$=_objectWithoutProperties(i,w),V=this.state.value;o=x?{onKeyDown:this.onKeyDown}:j?{onKeyDown:j}:{},s=C?{onBlur:this.onBlur}:L?{onBlur:L}:{};var U=B?{ref:B}:{};return u.default.createElement(_,_objectSpread(_objectSpread(_objectSpread(_objectSpread({},$),{},{onChange:this.onChange,value:V},o),s),U))}}]),DebounceInput}(u.default.PureComponent);s.DebounceInput=x,_defineProperty(x,"defaultProps",{element:"input",type:"text",onKeyDown:void 0,onBlur:void 0,value:void 0,minLength:0,debounceTimeout:100,forceNotifyByEnter:!0,forceNotifyOnBlur:!0,inputRef:void 0})},24677:(o,s,i)=>{"use strict";var u=i(81214).DebounceInput;u.DebounceInput=u,o.exports=u},22551:(o,s,i)=>{"use strict";var u=i(96540),_=i(69982);function p(o){for(var s="https://reactjs.org/docs/error-decoder.html?invariant="+o,i=1;i