diff --git a/.gitmodules b/.gitmodules
deleted file mode 100644
index 70ad252..0000000
--- a/.gitmodules
+++ /dev/null
@@ -1,4 +0,0 @@
-[submodule "android/materialchart"]
- path = android/materialchart
- url = https://github.com/Taewan-P/material-android-chart
- branch = release
diff --git a/android/.gitignore b/android/.gitignore
index f01e7c6..0b6b3bb 100644
--- a/android/.gitignore
+++ b/android/.gitignore
@@ -7,6 +7,7 @@
/.idea/workspace.xml
/.idea/navEditor.xml
/.idea/assetWizardSettings.xml
+/.idea/deploymentTargetDropDown.xml
.DS_Store
/build
/captures
diff --git a/android/.idea/gradle.xml b/android/.idea/gradle.xml
index 266f9cb..0897082 100644
--- a/android/.idea/gradle.xml
+++ b/android/.idea/gradle.xml
@@ -4,17 +4,15 @@
diff --git a/android/.idea/migrations.xml b/android/.idea/migrations.xml
new file mode 100644
index 0000000..f8051a6
--- /dev/null
+++ b/android/.idea/migrations.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts
index edeeff2..3191414 100644
--- a/android/app/build.gradle.kts
+++ b/android/app/build.gradle.kts
@@ -19,10 +19,8 @@ android {
applicationId = "app.priceguard"
minSdk = 29
targetSdk = 34
- versionCode = 2
- versionName = "0.1.1"
- versionCode = 3
- versionName = "0.2.0"
+ versionCode = 4
+ versionName = "0.3.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
@@ -64,6 +62,7 @@ dependencies {
implementation("com.google.firebase:firebase-analytics")
implementation("com.google.firebase:firebase-crashlytics")
implementation("com.google.firebase:firebase-perf")
+ implementation("com.google.firebase:firebase-messaging")
// Android
implementation("androidx.core:core-ktx:1.12.0")
@@ -106,8 +105,13 @@ dependencies {
// Pull to Refresh
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
+ // Worker
+ implementation("androidx.work:work-runtime-ktx:2.9.0")
+ implementation("androidx.hilt:hilt-work:1.1.0")
+ kapt("androidx.hilt:hilt-compiler:1.1.0")
+
// Material chart
- implementation(project(":materialchart"))
+ implementation("app.priceguard:materialchart:0.1.2")
}
kapt {
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index c0c7503..fd71dcf 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -14,6 +14,17 @@
android:supportsRtl="true"
android:theme="@style/Theme.PriceGuard"
tools:targetApi="34">
+
+
+
+
+
@@ -37,16 +48,50 @@
android:exported="false" />
+ android:exported="true">
+
+
+
+
+
+
+
+
+ android:exported="true"
+ android:launchMode="singleTask">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/app/src/main/java/app/priceguard/data/GraphDataConverter.kt b/android/app/src/main/java/app/priceguard/data/GraphDataConverter.kt
new file mode 100644
index 0000000..b7325fa
--- /dev/null
+++ b/android/app/src/main/java/app/priceguard/data/GraphDataConverter.kt
@@ -0,0 +1,97 @@
+package app.priceguard.data
+
+import app.priceguard.data.dto.PriceDataDTO
+import app.priceguard.data.graph.ProductChartData
+import app.priceguard.materialchart.data.GraphMode
+
+class GraphDataConverter {
+
+ fun toDataset(priceData: List?): List {
+ priceData ?: return listOf()
+ if (priceData.isEmpty()) {
+ return listOf()
+ }
+
+ val dataList = mutableListOf()
+ priceData.forEach { dto ->
+ dto.time ?: return@forEach
+ dto.price ?: return@forEach
+ dto.isSoldOut ?: return@forEach
+ dataList.add(
+ ProductChartData(
+ x = dto.time / 1000,
+ y = dto.price.toFloat(),
+ valid = dto.isSoldOut.not()
+ )
+ )
+ }
+
+ return dataList.toList()
+ }
+
+ fun packWithEdgeData(
+ list: List,
+ period: GraphMode = GraphMode.DAY
+ ): List {
+ val currentTime = getCurrentTime()
+ val startTime = getStartTime(period, currentTime)
+ val sortedList = list.sortedBy { it.x }
+ val filteredList = sortedList.filter { it.x in startTime..currentTime }
+ val sievedList = sortedList.filter { it.x !in startTime..currentTime }
+ val startData = if (sievedList.none()) {
+ list.first()
+ } else {
+ sievedList.last()
+ }
+
+ return if (filteredList.isEmpty()) {
+ listOf(
+ ProductChartData(startTime, startData.y, startData.valid),
+ ProductChartData(currentTime, list.last().y, list.last().valid)
+ )
+ } else {
+ listOf(
+ ProductChartData(
+ startTime,
+ startData.y,
+ startData.valid
+ )
+ ) + filteredList + ProductChartData(
+ currentTime,
+ filteredList.last().y,
+ filteredList.last().valid
+ )
+ }
+ }
+
+ private fun getStartTime(period: GraphMode, currentTime: Float = getCurrentTime()): Float {
+ return when (period) {
+ GraphMode.DAY -> {
+ currentTime - DAY
+ }
+
+ GraphMode.WEEK -> {
+ currentTime - WEEK
+ }
+
+ GraphMode.MONTH -> {
+ currentTime - MONTH
+ }
+
+ GraphMode.QUARTER -> {
+ currentTime - QUARTER
+ }
+ }
+ }
+
+ private fun getCurrentTime(): Float {
+ return (System.currentTimeMillis() / 1000).toFloat()
+ }
+
+ companion object {
+ const val DAY = 86400
+ const val WEEK = DAY * 7
+ const val MONTH = DAY * 31
+ const val QUARTER = MONTH * 3
+ }
+}
diff --git a/android/app/src/main/java/app/priceguard/data/datastore/ConfigDataSource.kt b/android/app/src/main/java/app/priceguard/data/datastore/ConfigDataSource.kt
new file mode 100644
index 0000000..e98ef6c
--- /dev/null
+++ b/android/app/src/main/java/app/priceguard/data/datastore/ConfigDataSource.kt
@@ -0,0 +1,8 @@
+package app.priceguard.data.datastore
+
+interface ConfigDataSource {
+ suspend fun saveDynamicMode(mode: Int)
+ suspend fun saveDarkMode(mode: Int)
+ suspend fun getDynamicMode(): Int?
+ suspend fun getDarkMode(): Int?
+}
diff --git a/android/app/src/main/java/app/priceguard/data/datastore/ConfigDataSourceImpl.kt b/android/app/src/main/java/app/priceguard/data/datastore/ConfigDataSourceImpl.kt
new file mode 100644
index 0000000..3de29c4
--- /dev/null
+++ b/android/app/src/main/java/app/priceguard/data/datastore/ConfigDataSourceImpl.kt
@@ -0,0 +1,42 @@
+package app.priceguard.data.datastore
+
+import androidx.datastore.core.DataStore
+import androidx.datastore.preferences.core.Preferences
+import androidx.datastore.preferences.core.edit
+import androidx.datastore.preferences.core.intPreferencesKey
+import app.priceguard.di.ConfigQualifier
+import javax.inject.Inject
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.map
+
+class ConfigDataSourceImpl @Inject constructor(
+ @ConfigQualifier private val dataStore: DataStore
+) : ConfigDataSource {
+
+ private val dynamicMode = intPreferencesKey("dynamic_mode")
+ private val darkMode = intPreferencesKey("dark_mode")
+
+ override suspend fun saveDynamicMode(mode: Int) {
+ dataStore.edit { preferences ->
+ preferences[dynamicMode] = mode
+ }
+ }
+
+ override suspend fun saveDarkMode(mode: Int) {
+ dataStore.edit { preferences ->
+ preferences[darkMode] = mode
+ }
+ }
+
+ override suspend fun getDynamicMode(): Int? {
+ return dataStore.data.map { preferences ->
+ preferences[dynamicMode]
+ }.first()
+ }
+
+ override suspend fun getDarkMode(): Int? {
+ return dataStore.data.map { preferences ->
+ preferences[darkMode]
+ }.first()
+ }
+}
diff --git a/android/app/src/main/java/app/priceguard/data/datastore/TokenDataSourceImpl.kt b/android/app/src/main/java/app/priceguard/data/datastore/TokenDataSourceImpl.kt
index 6912b3d..5a0932b 100644
--- a/android/app/src/main/java/app/priceguard/data/datastore/TokenDataSourceImpl.kt
+++ b/android/app/src/main/java/app/priceguard/data/datastore/TokenDataSourceImpl.kt
@@ -4,11 +4,14 @@ import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.stringPreferencesKey
+import app.priceguard.di.TokensQualifier
import javax.inject.Inject
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
-class TokenDataSourceImpl @Inject constructor(private val dataStore: DataStore) : TokenDataSource {
+class TokenDataSourceImpl @Inject constructor(
+ @TokensQualifier private val dataStore: DataStore
+) : TokenDataSource {
private val accessTokenKey = stringPreferencesKey("access_token")
private val refreshTokenKey = stringPreferencesKey("refresh_token")
diff --git a/android/app/src/main/java/app/priceguard/data/dto/LoginResponse.kt b/android/app/src/main/java/app/priceguard/data/dto/LoginResponse.kt
deleted file mode 100644
index f4e1026..0000000
--- a/android/app/src/main/java/app/priceguard/data/dto/LoginResponse.kt
+++ /dev/null
@@ -1,23 +0,0 @@
-package app.priceguard.data.dto
-
-import kotlinx.serialization.Serializable
-
-@Serializable
-data class LoginResponse(
- val statusCode: Int,
- val message: String,
- val accessToken: String,
- val refreshToken: String
-)
-
-data class LoginResult(
- val loginState: LoginState,
- val accessToken: String?,
- val refreshToken: String?
-)
-
-enum class LoginState {
- SUCCESS,
- INVALID_PARAMETER,
- UNDEFINED_ERROR
-}
diff --git a/android/app/src/main/java/app/priceguard/data/dto/ProductInfo.kt b/android/app/src/main/java/app/priceguard/data/dto/ProductInfo.kt
deleted file mode 100644
index 740d789..0000000
--- a/android/app/src/main/java/app/priceguard/data/dto/ProductInfo.kt
+++ /dev/null
@@ -1,8 +0,0 @@
-package app.priceguard.data.dto
-
-data class ProductInfo(
- val title: String = "",
- val brand: String = "",
- val logo: Int = 0,
- val price: String = ""
-)
diff --git a/android/app/src/main/java/app/priceguard/data/dto/ProductVerifyDTO.kt b/android/app/src/main/java/app/priceguard/data/dto/ProductVerifyDTO.kt
deleted file mode 100644
index 12c02ef..0000000
--- a/android/app/src/main/java/app/priceguard/data/dto/ProductVerifyDTO.kt
+++ /dev/null
@@ -1,12 +0,0 @@
-package app.priceguard.data.dto
-
-import kotlinx.serialization.Serializable
-
-@Serializable
-data class ProductVerifyDTO(
- val productName: String? = null,
- val productCode: String? = null,
- val productPrice: Int? = null,
- val shop: String? = null,
- val imageUrl: String? = null
-)
diff --git a/android/app/src/main/java/app/priceguard/data/dto/SignupResponse.kt b/android/app/src/main/java/app/priceguard/data/dto/SignupResponse.kt
deleted file mode 100644
index 17c51a7..0000000
--- a/android/app/src/main/java/app/priceguard/data/dto/SignupResponse.kt
+++ /dev/null
@@ -1,24 +0,0 @@
-package app.priceguard.data.dto
-
-import kotlinx.serialization.Serializable
-
-@Serializable
-data class SignupResponse(
- val statusCode: Int,
- val message: String,
- val accessToken: String,
- val refreshToken: String
-)
-
-data class SignupResult(
- val signUpState: SignupState,
- val accessToken: String?,
- val refreshToken: String?
-)
-
-enum class SignupState {
- SUCCESS,
- INVALID_PARAMETER,
- DUPLICATE_EMAIL,
- UNDEFINED_ERROR
-}
diff --git a/android/app/src/main/java/app/priceguard/data/dto/ProductAddRequest.kt b/android/app/src/main/java/app/priceguard/data/dto/add/ProductAddRequest.kt
similarity index 80%
rename from android/app/src/main/java/app/priceguard/data/dto/ProductAddRequest.kt
rename to android/app/src/main/java/app/priceguard/data/dto/add/ProductAddRequest.kt
index e9e9dd8..c8d3513 100644
--- a/android/app/src/main/java/app/priceguard/data/dto/ProductAddRequest.kt
+++ b/android/app/src/main/java/app/priceguard/data/dto/add/ProductAddRequest.kt
@@ -1,4 +1,4 @@
-package app.priceguard.data.dto
+package app.priceguard.data.dto.add
import kotlinx.serialization.Serializable
diff --git a/android/app/src/main/java/app/priceguard/data/dto/ProductAddResponse.kt b/android/app/src/main/java/app/priceguard/data/dto/add/ProductAddResponse.kt
similarity index 79%
rename from android/app/src/main/java/app/priceguard/data/dto/ProductAddResponse.kt
rename to android/app/src/main/java/app/priceguard/data/dto/add/ProductAddResponse.kt
index a97b62f..dda1c29 100644
--- a/android/app/src/main/java/app/priceguard/data/dto/ProductAddResponse.kt
+++ b/android/app/src/main/java/app/priceguard/data/dto/add/ProductAddResponse.kt
@@ -1,4 +1,4 @@
-package app.priceguard.data.dto
+package app.priceguard.data.dto.add
import kotlinx.serialization.Serializable
diff --git a/android/app/src/main/java/app/priceguard/data/dto/alert/AlertUpdateResponse.kt b/android/app/src/main/java/app/priceguard/data/dto/alert/AlertUpdateResponse.kt
new file mode 100644
index 0000000..ef273e7
--- /dev/null
+++ b/android/app/src/main/java/app/priceguard/data/dto/alert/AlertUpdateResponse.kt
@@ -0,0 +1,9 @@
+package app.priceguard.data.dto.alert
+
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class AlertUpdateResponse(
+ val statusCode: Int,
+ val message: String
+)
diff --git a/android/app/src/main/java/app/priceguard/data/dto/ProductDeleteResponse.kt b/android/app/src/main/java/app/priceguard/data/dto/delete/ProductDeleteResponse.kt
similarity index 78%
rename from android/app/src/main/java/app/priceguard/data/dto/ProductDeleteResponse.kt
rename to android/app/src/main/java/app/priceguard/data/dto/delete/ProductDeleteResponse.kt
index 89281b8..33e76c2 100644
--- a/android/app/src/main/java/app/priceguard/data/dto/ProductDeleteResponse.kt
+++ b/android/app/src/main/java/app/priceguard/data/dto/delete/ProductDeleteResponse.kt
@@ -1,4 +1,4 @@
-package app.priceguard.data.dto
+package app.priceguard.data.dto.delete
import kotlinx.serialization.Serializable
diff --git a/android/app/src/main/java/app/priceguard/data/dto/ProductResponse.kt b/android/app/src/main/java/app/priceguard/data/dto/detail/ProductResponse.kt
similarity index 55%
rename from android/app/src/main/java/app/priceguard/data/dto/ProductResponse.kt
rename to android/app/src/main/java/app/priceguard/data/dto/detail/ProductResponse.kt
index f46006d..b657c5e 100644
--- a/android/app/src/main/java/app/priceguard/data/dto/ProductResponse.kt
+++ b/android/app/src/main/java/app/priceguard/data/dto/detail/ProductResponse.kt
@@ -1,6 +1,6 @@
-package app.priceguard.data.dto
+package app.priceguard.data.dto.detail
-import app.priceguard.data.graph.ProductChartData
+import app.priceguard.data.dto.PriceDataDTO
import kotlinx.serialization.Serializable
@Serializable
@@ -18,16 +18,3 @@ data class ProductResponse(
val price: Int? = null,
val priceData: List? = null
)
-
-data class ProductDetailResult(
- val productName: String,
- val productCode: String,
- val shop: String,
- val imageUrl: String,
- val rank: Int,
- val shopUrl: String,
- val targetPrice: Int,
- val lowestPrice: Int,
- val price: Int,
- val priceData: List
-)
diff --git a/android/app/src/main/java/app/priceguard/data/dto/firebase/FirebaseTokenUpdateRequest.kt b/android/app/src/main/java/app/priceguard/data/dto/firebase/FirebaseTokenUpdateRequest.kt
new file mode 100644
index 0000000..a10c04c
--- /dev/null
+++ b/android/app/src/main/java/app/priceguard/data/dto/firebase/FirebaseTokenUpdateRequest.kt
@@ -0,0 +1,8 @@
+package app.priceguard.data.dto.firebase
+
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class FirebaseTokenUpdateRequest(
+ val token: String
+)
diff --git a/android/app/src/main/java/app/priceguard/data/dto/firebase/FirebaseTokenUpdateResponse.kt b/android/app/src/main/java/app/priceguard/data/dto/firebase/FirebaseTokenUpdateResponse.kt
new file mode 100644
index 0000000..6f397fc
--- /dev/null
+++ b/android/app/src/main/java/app/priceguard/data/dto/firebase/FirebaseTokenUpdateResponse.kt
@@ -0,0 +1,9 @@
+package app.priceguard.data.dto.firebase
+
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class FirebaseTokenUpdateResponse(
+ val statusCode: Int,
+ val message: String
+)
diff --git a/android/app/src/main/java/app/priceguard/data/dto/ProductDTO.kt b/android/app/src/main/java/app/priceguard/data/dto/list/ProductDTO.kt
similarity index 52%
rename from android/app/src/main/java/app/priceguard/data/dto/ProductDTO.kt
rename to android/app/src/main/java/app/priceguard/data/dto/list/ProductDTO.kt
index 3931a8e..d267903 100644
--- a/android/app/src/main/java/app/priceguard/data/dto/ProductDTO.kt
+++ b/android/app/src/main/java/app/priceguard/data/dto/list/ProductDTO.kt
@@ -1,6 +1,6 @@
-package app.priceguard.data.dto
+package app.priceguard.data.dto.list
-import app.priceguard.data.graph.ProductChartData
+import app.priceguard.data.dto.PriceDataDTO
import kotlinx.serialization.Serializable
@Serializable
@@ -11,15 +11,6 @@ data class ProductDTO(
val imageUrl: String? = null,
val targetPrice: Int? = null,
val price: Int? = null,
+ val isAlert: Boolean? = null,
val priceData: List? = null
)
-
-data class ProductData(
- val productName: String,
- val productCode: String,
- val shop: String,
- val imageUrl: String,
- val targetPrice: Int,
- val price: Int,
- val priceData: List
-)
diff --git a/android/app/src/main/java/app/priceguard/data/dto/ProductListResponse.kt b/android/app/src/main/java/app/priceguard/data/dto/list/ProductListResponse.kt
similarity index 83%
rename from android/app/src/main/java/app/priceguard/data/dto/ProductListResponse.kt
rename to android/app/src/main/java/app/priceguard/data/dto/list/ProductListResponse.kt
index 54be311..7cda3db 100644
--- a/android/app/src/main/java/app/priceguard/data/dto/ProductListResponse.kt
+++ b/android/app/src/main/java/app/priceguard/data/dto/list/ProductListResponse.kt
@@ -1,4 +1,4 @@
-package app.priceguard.data.dto
+package app.priceguard.data.dto.list
import kotlinx.serialization.Serializable
diff --git a/android/app/src/main/java/app/priceguard/data/dto/LoginRequest.kt b/android/app/src/main/java/app/priceguard/data/dto/login/LoginRequest.kt
similarity index 77%
rename from android/app/src/main/java/app/priceguard/data/dto/LoginRequest.kt
rename to android/app/src/main/java/app/priceguard/data/dto/login/LoginRequest.kt
index 2f9cc15..e63ebdd 100644
--- a/android/app/src/main/java/app/priceguard/data/dto/LoginRequest.kt
+++ b/android/app/src/main/java/app/priceguard/data/dto/login/LoginRequest.kt
@@ -1,4 +1,4 @@
-package app.priceguard.data.dto
+package app.priceguard.data.dto.login
import kotlinx.serialization.Serializable
diff --git a/android/app/src/main/java/app/priceguard/data/dto/login/LoginResponse.kt b/android/app/src/main/java/app/priceguard/data/dto/login/LoginResponse.kt
new file mode 100644
index 0000000..c7c7e90
--- /dev/null
+++ b/android/app/src/main/java/app/priceguard/data/dto/login/LoginResponse.kt
@@ -0,0 +1,11 @@
+package app.priceguard.data.dto.login
+
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class LoginResponse(
+ val statusCode: Int,
+ val message: String,
+ val accessToken: String? = null,
+ val refreshToken: String? = null
+)
diff --git a/android/app/src/main/java/app/priceguard/data/dto/PricePatchRequest.kt b/android/app/src/main/java/app/priceguard/data/dto/patch/PricePatchRequest.kt
similarity index 79%
rename from android/app/src/main/java/app/priceguard/data/dto/PricePatchRequest.kt
rename to android/app/src/main/java/app/priceguard/data/dto/patch/PricePatchRequest.kt
index 7765575..0d07b26 100644
--- a/android/app/src/main/java/app/priceguard/data/dto/PricePatchRequest.kt
+++ b/android/app/src/main/java/app/priceguard/data/dto/patch/PricePatchRequest.kt
@@ -1,4 +1,4 @@
-package app.priceguard.data.dto
+package app.priceguard.data.dto.patch
import kotlinx.serialization.Serializable
diff --git a/android/app/src/main/java/app/priceguard/data/dto/PricePatchResponse.kt b/android/app/src/main/java/app/priceguard/data/dto/patch/PricePatchResponse.kt
similarity index 78%
rename from android/app/src/main/java/app/priceguard/data/dto/PricePatchResponse.kt
rename to android/app/src/main/java/app/priceguard/data/dto/patch/PricePatchResponse.kt
index dc162bf..8b95b35 100644
--- a/android/app/src/main/java/app/priceguard/data/dto/PricePatchResponse.kt
+++ b/android/app/src/main/java/app/priceguard/data/dto/patch/PricePatchResponse.kt
@@ -1,4 +1,4 @@
-package app.priceguard.data.dto
+package app.priceguard.data.dto.patch
import kotlinx.serialization.Serializable
diff --git a/android/app/src/main/java/app/priceguard/data/dto/RecommendProductDTO.kt b/android/app/src/main/java/app/priceguard/data/dto/recommend/RecommendProductDTO.kt
similarity index 52%
rename from android/app/src/main/java/app/priceguard/data/dto/RecommendProductDTO.kt
rename to android/app/src/main/java/app/priceguard/data/dto/recommend/RecommendProductDTO.kt
index 9b4ac22..7015641 100644
--- a/android/app/src/main/java/app/priceguard/data/dto/RecommendProductDTO.kt
+++ b/android/app/src/main/java/app/priceguard/data/dto/recommend/RecommendProductDTO.kt
@@ -1,6 +1,6 @@
-package app.priceguard.data.dto
+package app.priceguard.data.dto.recommend
-import app.priceguard.data.graph.ProductChartData
+import app.priceguard.data.dto.PriceDataDTO
import kotlinx.serialization.Serializable
@Serializable
@@ -13,13 +13,3 @@ data class RecommendProductDTO(
val rank: Int? = null,
val priceData: List? = null
)
-
-data class RecommendProductData(
- val productName: String,
- val productCode: String,
- val shop: String,
- val imageUrl: String,
- val price: Int,
- val rank: Int,
- val priceData: List
-)
diff --git a/android/app/src/main/java/app/priceguard/data/dto/RecommendProductResponse.kt b/android/app/src/main/java/app/priceguard/data/dto/recommend/RecommendProductResponse.kt
similarity index 82%
rename from android/app/src/main/java/app/priceguard/data/dto/RecommendProductResponse.kt
rename to android/app/src/main/java/app/priceguard/data/dto/recommend/RecommendProductResponse.kt
index 063af2b..716a0f9 100644
--- a/android/app/src/main/java/app/priceguard/data/dto/RecommendProductResponse.kt
+++ b/android/app/src/main/java/app/priceguard/data/dto/recommend/RecommendProductResponse.kt
@@ -1,4 +1,4 @@
-package app.priceguard.data.dto
+package app.priceguard.data.dto.recommend
import kotlinx.serialization.Serializable
diff --git a/android/app/src/main/java/app/priceguard/data/dto/RenewResponse.kt b/android/app/src/main/java/app/priceguard/data/dto/renew/RenewResponse.kt
similarity index 61%
rename from android/app/src/main/java/app/priceguard/data/dto/RenewResponse.kt
rename to android/app/src/main/java/app/priceguard/data/dto/renew/RenewResponse.kt
index a50f0f7..e501dd9 100644
--- a/android/app/src/main/java/app/priceguard/data/dto/RenewResponse.kt
+++ b/android/app/src/main/java/app/priceguard/data/dto/renew/RenewResponse.kt
@@ -1,4 +1,4 @@
-package app.priceguard.data.dto
+package app.priceguard.data.dto.renew
import kotlinx.serialization.Serializable
@@ -9,10 +9,3 @@ data class RenewResponse(
val accessToken: String,
val refreshToken: String
)
-
-enum class RenewResult {
- SUCCESS,
- EXPIRED,
- UNAUTHORIZED,
- UNKNOWN_ERROR
-}
diff --git a/android/app/src/main/java/app/priceguard/data/dto/SignupRequest.kt b/android/app/src/main/java/app/priceguard/data/dto/signup/SignupRequest.kt
similarity index 80%
rename from android/app/src/main/java/app/priceguard/data/dto/SignupRequest.kt
rename to android/app/src/main/java/app/priceguard/data/dto/signup/SignupRequest.kt
index 16dfdce..6a28e66 100644
--- a/android/app/src/main/java/app/priceguard/data/dto/SignupRequest.kt
+++ b/android/app/src/main/java/app/priceguard/data/dto/signup/SignupRequest.kt
@@ -1,4 +1,4 @@
-package app.priceguard.data.dto
+package app.priceguard.data.dto.signup
import kotlinx.serialization.Serializable
diff --git a/android/app/src/main/java/app/priceguard/data/dto/signup/SignupResponse.kt b/android/app/src/main/java/app/priceguard/data/dto/signup/SignupResponse.kt
new file mode 100644
index 0000000..106c246
--- /dev/null
+++ b/android/app/src/main/java/app/priceguard/data/dto/signup/SignupResponse.kt
@@ -0,0 +1,11 @@
+package app.priceguard.data.dto.signup
+
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class SignupResponse(
+ val statusCode: Int,
+ val message: String,
+ val accessToken: String? = null,
+ val refreshToken: String? = null
+)
diff --git a/android/app/src/main/java/app/priceguard/data/dto/ProductVerifyRequest.kt b/android/app/src/main/java/app/priceguard/data/dto/verify/ProductVerifyRequest.kt
similarity index 75%
rename from android/app/src/main/java/app/priceguard/data/dto/ProductVerifyRequest.kt
rename to android/app/src/main/java/app/priceguard/data/dto/verify/ProductVerifyRequest.kt
index 63cb97f..832565c 100644
--- a/android/app/src/main/java/app/priceguard/data/dto/ProductVerifyRequest.kt
+++ b/android/app/src/main/java/app/priceguard/data/dto/verify/ProductVerifyRequest.kt
@@ -1,4 +1,4 @@
-package app.priceguard.data.dto
+package app.priceguard.data.dto.verify
import kotlinx.serialization.Serializable
diff --git a/android/app/src/main/java/app/priceguard/data/dto/ProductVerifyResponse.kt b/android/app/src/main/java/app/priceguard/data/dto/verify/ProductVerifyResponse.kt
similarity index 89%
rename from android/app/src/main/java/app/priceguard/data/dto/ProductVerifyResponse.kt
rename to android/app/src/main/java/app/priceguard/data/dto/verify/ProductVerifyResponse.kt
index 5eb5e51..230864a 100644
--- a/android/app/src/main/java/app/priceguard/data/dto/ProductVerifyResponse.kt
+++ b/android/app/src/main/java/app/priceguard/data/dto/verify/ProductVerifyResponse.kt
@@ -1,4 +1,4 @@
-package app.priceguard.data.dto
+package app.priceguard.data.dto.verify
import kotlinx.serialization.Serializable
diff --git a/android/app/src/main/java/app/priceguard/data/graph/GraphDataConverter.kt b/android/app/src/main/java/app/priceguard/data/graph/GraphDataConverter.kt
deleted file mode 100644
index 125c39d..0000000
--- a/android/app/src/main/java/app/priceguard/data/graph/GraphDataConverter.kt
+++ /dev/null
@@ -1,24 +0,0 @@
-package app.priceguard.data.graph
-
-import app.priceguard.data.dto.PriceDataDTO
-
-class GraphDataConverter {
-
- // TODO: 기간별로 데이터 필터링을 통해 해당 기간에 발생한 가격 변동만 추적하도록 구조 변경.
- fun toDataset(priceData: List?): List {
- priceData ?: return listOf()
- if (priceData.isEmpty()) {
- return listOf()
- }
- val dataList = mutableListOf()
- priceData.forEach { dto ->
- dto.time ?: return@forEach
- dto.price ?: return@forEach
- dto.isSoldOut ?: return@forEach
- dataList.add(ProductChartData(dto.time / 1000, dto.price.toFloat(), dto.isSoldOut.not()))
- }
- val currentTime = (System.currentTimeMillis() / 1000).toFloat()
- dataList.add(ProductChartData(currentTime, dataList.last().y, dataList.last().valid))
- return dataList.toList().sortedBy { it.x }.filter { it.x <= currentTime }
- }
-}
diff --git a/android/app/src/main/java/app/priceguard/data/graph/ProductChartDataset.kt b/android/app/src/main/java/app/priceguard/data/graph/ProductChartDataset.kt
index b8411f9..79bf41b 100644
--- a/android/app/src/main/java/app/priceguard/data/graph/ProductChartDataset.kt
+++ b/android/app/src/main/java/app/priceguard/data/graph/ProductChartDataset.kt
@@ -1,6 +1,5 @@
package app.priceguard.data.graph
-import app.priceguard.materialchart.data.ChartData
import app.priceguard.materialchart.data.ChartDataset
import app.priceguard.materialchart.data.GraphMode
import app.priceguard.materialchart.data.GridLine
@@ -10,6 +9,8 @@ data class ProductChartDataset(
override val showYAxis: Boolean,
override val isInteractive: Boolean,
override val graphMode: GraphMode,
- override val data: List,
+ override val xLabel: String,
+ override val yLabel: String,
+ override val data: List,
override val gridLines: List
) : ChartDataset
diff --git a/android/app/src/main/java/app/priceguard/data/network/AuthAPI.kt b/android/app/src/main/java/app/priceguard/data/network/AuthAPI.kt
index 49518a9..b65be19 100644
--- a/android/app/src/main/java/app/priceguard/data/network/AuthAPI.kt
+++ b/android/app/src/main/java/app/priceguard/data/network/AuthAPI.kt
@@ -1,6 +1,6 @@
package app.priceguard.data.network
-import app.priceguard.data.dto.RenewResponse
+import app.priceguard.data.dto.renew.RenewResponse
import retrofit2.Response
import retrofit2.http.GET
import retrofit2.http.Header
diff --git a/android/app/src/main/java/app/priceguard/data/network/ProductAPI.kt b/android/app/src/main/java/app/priceguard/data/network/ProductAPI.kt
index 31459ef..1f62101 100644
--- a/android/app/src/main/java/app/priceguard/data/network/ProductAPI.kt
+++ b/android/app/src/main/java/app/priceguard/data/network/ProductAPI.kt
@@ -1,15 +1,16 @@
package app.priceguard.data.network
-import app.priceguard.data.dto.PricePatchRequest
-import app.priceguard.data.dto.PricePatchResponse
-import app.priceguard.data.dto.ProductAddRequest
-import app.priceguard.data.dto.ProductAddResponse
-import app.priceguard.data.dto.ProductDeleteResponse
-import app.priceguard.data.dto.ProductListResponse
-import app.priceguard.data.dto.ProductResponse
-import app.priceguard.data.dto.ProductVerifyRequest
-import app.priceguard.data.dto.ProductVerifyResponse
-import app.priceguard.data.dto.RecommendProductResponse
+import app.priceguard.data.dto.add.ProductAddRequest
+import app.priceguard.data.dto.add.ProductAddResponse
+import app.priceguard.data.dto.alert.AlertUpdateResponse
+import app.priceguard.data.dto.delete.ProductDeleteResponse
+import app.priceguard.data.dto.detail.ProductResponse
+import app.priceguard.data.dto.list.ProductListResponse
+import app.priceguard.data.dto.patch.PricePatchRequest
+import app.priceguard.data.dto.patch.PricePatchResponse
+import app.priceguard.data.dto.recommend.RecommendProductResponse
+import app.priceguard.data.dto.verify.ProductVerifyRequest
+import app.priceguard.data.dto.verify.ProductVerifyResponse
import retrofit2.Response
import retrofit2.http.Body
import retrofit2.http.DELETE
@@ -50,4 +51,9 @@ interface ProductAPI {
suspend fun updateTargetPrice(
@Body pricePatchRequest: PricePatchRequest
): Response
+
+ @PATCH("alert/{productCode}")
+ suspend fun updateAlert(
+ @Path("productCode") productCode: String
+ ): Response
}
diff --git a/android/app/src/main/java/app/priceguard/data/network/ProductRepositoryResult.kt b/android/app/src/main/java/app/priceguard/data/network/ProductRepositoryResult.kt
deleted file mode 100644
index 60710f5..0000000
--- a/android/app/src/main/java/app/priceguard/data/network/ProductRepositoryResult.kt
+++ /dev/null
@@ -1,10 +0,0 @@
-package app.priceguard.data.network
-
-import app.priceguard.data.dto.ProductErrorState
-
-sealed class ProductRepositoryResult {
-
- data class Success(val data: T) : ProductRepositoryResult()
-
- data class Error(val productErrorState: ProductErrorState) : ProductRepositoryResult()
-}
diff --git a/android/app/src/main/java/app/priceguard/data/network/RequestInterceptor.kt b/android/app/src/main/java/app/priceguard/data/network/RequestInterceptor.kt
index 443ec0c..0ab031c 100644
--- a/android/app/src/main/java/app/priceguard/data/network/RequestInterceptor.kt
+++ b/android/app/src/main/java/app/priceguard/data/network/RequestInterceptor.kt
@@ -1,6 +1,6 @@
package app.priceguard.data.network
-import app.priceguard.data.repository.TokenRepository
+import app.priceguard.data.repository.token.TokenRepository
import javax.inject.Inject
import kotlinx.coroutines.runBlocking
import okhttp3.Interceptor
diff --git a/android/app/src/main/java/app/priceguard/data/network/UserAPI.kt b/android/app/src/main/java/app/priceguard/data/network/UserAPI.kt
index 706155f..270b0a4 100644
--- a/android/app/src/main/java/app/priceguard/data/network/UserAPI.kt
+++ b/android/app/src/main/java/app/priceguard/data/network/UserAPI.kt
@@ -1,12 +1,16 @@
package app.priceguard.data.network
-import app.priceguard.data.dto.LoginRequest
-import app.priceguard.data.dto.LoginResponse
-import app.priceguard.data.dto.SignupRequest
-import app.priceguard.data.dto.SignupResponse
+import app.priceguard.data.dto.firebase.FirebaseTokenUpdateRequest
+import app.priceguard.data.dto.firebase.FirebaseTokenUpdateResponse
+import app.priceguard.data.dto.login.LoginRequest
+import app.priceguard.data.dto.login.LoginResponse
+import app.priceguard.data.dto.signup.SignupRequest
+import app.priceguard.data.dto.signup.SignupResponse
import retrofit2.Response
import retrofit2.http.Body
+import retrofit2.http.Header
import retrofit2.http.POST
+import retrofit2.http.PUT
interface UserAPI {
@@ -19,4 +23,10 @@ interface UserAPI {
suspend fun register(
@Body request: SignupRequest
): Response
+
+ @PUT("firebase/token")
+ suspend fun updateFirebaseToken(
+ @Header("Authorization") authToken: String,
+ @Body request: FirebaseTokenUpdateRequest
+ ): Response
}
diff --git a/android/app/src/main/java/app/priceguard/data/network/APIResult.kt b/android/app/src/main/java/app/priceguard/data/repository/APIResult.kt
similarity index 94%
rename from android/app/src/main/java/app/priceguard/data/network/APIResult.kt
rename to android/app/src/main/java/app/priceguard/data/repository/APIResult.kt
index 4055b05..4839379 100644
--- a/android/app/src/main/java/app/priceguard/data/network/APIResult.kt
+++ b/android/app/src/main/java/app/priceguard/data/repository/APIResult.kt
@@ -1,4 +1,4 @@
-package app.priceguard.data.network
+package app.priceguard.data.repository
import retrofit2.Response
diff --git a/android/app/src/main/java/app/priceguard/data/repository/ProductRepository.kt b/android/app/src/main/java/app/priceguard/data/repository/ProductRepository.kt
deleted file mode 100644
index 7078197..0000000
--- a/android/app/src/main/java/app/priceguard/data/repository/ProductRepository.kt
+++ /dev/null
@@ -1,29 +0,0 @@
-package app.priceguard.data.repository
-
-import app.priceguard.data.dto.PricePatchRequest
-import app.priceguard.data.dto.PricePatchResponse
-import app.priceguard.data.dto.ProductAddRequest
-import app.priceguard.data.dto.ProductAddResponse
-import app.priceguard.data.dto.ProductData
-import app.priceguard.data.dto.ProductDetailResult
-import app.priceguard.data.dto.ProductVerifyDTO
-import app.priceguard.data.dto.ProductVerifyRequest
-import app.priceguard.data.dto.RecommendProductData
-import app.priceguard.data.network.ProductRepositoryResult
-
-interface ProductRepository {
-
- suspend fun verifyLink(productUrl: ProductVerifyRequest, isRenewed: Boolean = false): ProductRepositoryResult
-
- suspend fun addProduct(productAddRequest: ProductAddRequest, isRenewed: Boolean = false): ProductRepositoryResult
-
- suspend fun getProductList(isRenewed: Boolean = false): ProductRepositoryResult>
-
- suspend fun getRecommendedProductList(isRenewed: Boolean = false): ProductRepositoryResult>
-
- suspend fun getProductDetail(productCode: String, isRenewed: Boolean = false): ProductRepositoryResult
-
- suspend fun deleteProduct(productCode: String, isRenewed: Boolean = false): ProductRepositoryResult
-
- suspend fun updateTargetPrice(pricePatchRequest: PricePatchRequest, isRenewed: Boolean = false): ProductRepositoryResult
-}
diff --git a/android/app/src/main/java/app/priceguard/data/repository/RepositoryResult.kt b/android/app/src/main/java/app/priceguard/data/repository/RepositoryResult.kt
new file mode 100644
index 0000000..ed0822d
--- /dev/null
+++ b/android/app/src/main/java/app/priceguard/data/repository/RepositoryResult.kt
@@ -0,0 +1,8 @@
+package app.priceguard.data.repository
+
+sealed class RepositoryResult {
+
+ data class Success(val data: T) : RepositoryResult()
+
+ data class Error(val errorState: S) : RepositoryResult()
+}
diff --git a/android/app/src/main/java/app/priceguard/data/repository/TokenRepository.kt b/android/app/src/main/java/app/priceguard/data/repository/TokenRepository.kt
deleted file mode 100644
index 4d34ea0..0000000
--- a/android/app/src/main/java/app/priceguard/data/repository/TokenRepository.kt
+++ /dev/null
@@ -1,13 +0,0 @@
-package app.priceguard.data.repository
-
-import app.priceguard.data.dto.RenewResult
-import app.priceguard.data.dto.UserDataResult
-
-interface TokenRepository {
- suspend fun storeTokens(accessToken: String, refreshToken: String)
- suspend fun getAccessToken(): String?
- suspend fun getRefreshToken(): String?
- suspend fun getUserData(): UserDataResult
- suspend fun renewTokens(refreshToken: String): RenewResult
- suspend fun clearTokens()
-}
diff --git a/android/app/src/main/java/app/priceguard/data/repository/TokenRepositoryImpl.kt b/android/app/src/main/java/app/priceguard/data/repository/TokenRepositoryImpl.kt
deleted file mode 100644
index 4ed4969..0000000
--- a/android/app/src/main/java/app/priceguard/data/repository/TokenRepositoryImpl.kt
+++ /dev/null
@@ -1,75 +0,0 @@
-package app.priceguard.data.repository
-
-import android.util.Log
-import app.priceguard.data.datastore.TokenDataSource
-import app.priceguard.data.dto.RenewResult
-import app.priceguard.data.dto.UserDataDTO
-import app.priceguard.data.dto.UserDataResult
-import app.priceguard.data.network.APIResult
-import app.priceguard.data.network.AuthAPI
-import app.priceguard.data.network.getApiResult
-import java.util.*
-import javax.inject.Inject
-import kotlinx.serialization.json.Json
-
-class TokenRepositoryImpl @Inject constructor(
- private val tokenDataSource: TokenDataSource,
- private val authAPI: AuthAPI
-) : TokenRepository {
-
- override suspend fun storeTokens(accessToken: String, refreshToken: String) {
- tokenDataSource.saveTokens(accessToken, refreshToken)
- }
-
- override suspend fun getAccessToken(): String? {
- return tokenDataSource.getAccessToken()
- }
-
- override suspend fun getRefreshToken(): String? {
- return tokenDataSource.getRefreshToken()
- }
-
- override suspend fun getUserData(): UserDataResult {
- val accessToken = tokenDataSource.getAccessToken() ?: return UserDataResult("", "")
- val parts = accessToken.split(".")
- return try {
- val charset = charset("UTF-8")
- val payload = Json.decodeFromString(
- String(Base64.getUrlDecoder().decode(parts[1].toByteArray(charset)), charset)
- )
- UserDataResult(payload.email, payload.name)
- } catch (e: Exception) {
- Log.e("Data Not Found", "Error parsing JWT: $e")
- UserDataResult("", "")
- }
- }
-
- override suspend fun renewTokens(refreshToken: String): RenewResult {
- when (val response = getApiResult { authAPI.renewTokens("Bearer $refreshToken") }) {
- is APIResult.Success -> {
- storeTokens(response.data.accessToken, response.data.refreshToken)
- return RenewResult.SUCCESS
- }
-
- is APIResult.Error -> {
- return when (response.code) {
- 401 -> {
- RenewResult.UNAUTHORIZED
- }
-
- 410 -> {
- RenewResult.EXPIRED
- }
-
- else -> {
- RenewResult.UNKNOWN_ERROR
- }
- }
- }
- }
- }
-
- override suspend fun clearTokens() {
- tokenDataSource.clearTokens()
- }
-}
diff --git a/android/app/src/main/java/app/priceguard/data/repository/UserRepository.kt b/android/app/src/main/java/app/priceguard/data/repository/UserRepository.kt
deleted file mode 100644
index ec19abd..0000000
--- a/android/app/src/main/java/app/priceguard/data/repository/UserRepository.kt
+++ /dev/null
@@ -1,11 +0,0 @@
-package app.priceguard.data.repository
-
-import app.priceguard.data.dto.LoginResult
-import app.priceguard.data.dto.SignupResult
-
-interface UserRepository {
-
- suspend fun signUp(email: String, userName: String, password: String): SignupResult
-
- suspend fun login(email: String, password: String): LoginResult
-}
diff --git a/android/app/src/main/java/app/priceguard/data/repository/UserRepositoryImpl.kt b/android/app/src/main/java/app/priceguard/data/repository/UserRepositoryImpl.kt
deleted file mode 100644
index 7fd9a41..0000000
--- a/android/app/src/main/java/app/priceguard/data/repository/UserRepositoryImpl.kt
+++ /dev/null
@@ -1,65 +0,0 @@
-package app.priceguard.data.repository
-
-import app.priceguard.data.dto.LoginRequest
-import app.priceguard.data.dto.LoginResult
-import app.priceguard.data.dto.LoginState
-import app.priceguard.data.dto.SignupRequest
-import app.priceguard.data.dto.SignupResult
-import app.priceguard.data.dto.SignupState
-import app.priceguard.data.network.APIResult
-import app.priceguard.data.network.UserAPI
-import app.priceguard.data.network.getApiResult
-import javax.inject.Inject
-
-class UserRepositoryImpl @Inject constructor(private val userAPI: UserAPI) : UserRepository {
-
- override suspend fun signUp(email: String, userName: String, password: String): SignupResult {
- val response = getApiResult {
- userAPI.register(SignupRequest(email, userName, password))
- }
- when (response) {
- is APIResult.Success -> {
- return SignupResult(SignupState.SUCCESS, response.data.accessToken, response.data.refreshToken)
- }
-
- is APIResult.Error -> {
- return when (response.code) {
- 400 -> {
- SignupResult(SignupState.INVALID_PARAMETER, null, null)
- }
-
- 409 -> {
- SignupResult(SignupState.DUPLICATE_EMAIL, null, null)
- }
-
- else -> {
- SignupResult(SignupState.UNDEFINED_ERROR, null, null)
- }
- }
- }
- }
- }
-
- override suspend fun login(email: String, password: String): LoginResult {
- val response = getApiResult {
- userAPI.login(LoginRequest(email, password))
- }
- when (response) {
- is APIResult.Success -> {
- return LoginResult(LoginState.SUCCESS, response.data.accessToken, response.data.refreshToken)
- }
-
- is APIResult.Error -> {
- return when (response.code) {
- 400 -> {
- LoginResult(LoginState.INVALID_PARAMETER, null, null)
- }
-
- else -> {
- LoginResult(LoginState.UNDEFINED_ERROR, null, null)
- }
- }
- }
- }
- }
-}
diff --git a/android/app/src/main/java/app/priceguard/data/repository/auth/AuthErrorState.kt b/android/app/src/main/java/app/priceguard/data/repository/auth/AuthErrorState.kt
new file mode 100644
index 0000000..391b82c
--- /dev/null
+++ b/android/app/src/main/java/app/priceguard/data/repository/auth/AuthErrorState.kt
@@ -0,0 +1,7 @@
+package app.priceguard.data.repository.auth
+
+enum class AuthErrorState {
+ INVALID_REQUEST,
+ DUPLICATED_EMAIL,
+ UNDEFINED_ERROR
+}
diff --git a/android/app/src/main/java/app/priceguard/data/repository/auth/AuthRepository.kt b/android/app/src/main/java/app/priceguard/data/repository/auth/AuthRepository.kt
new file mode 100644
index 0000000..f75b37b
--- /dev/null
+++ b/android/app/src/main/java/app/priceguard/data/repository/auth/AuthRepository.kt
@@ -0,0 +1,12 @@
+package app.priceguard.data.repository.auth
+
+import app.priceguard.data.repository.RepositoryResult
+import app.priceguard.ui.data.LoginResult
+import app.priceguard.ui.data.SignupResult
+
+interface AuthRepository {
+
+ suspend fun signUp(email: String, userName: String, password: String): RepositoryResult
+
+ suspend fun login(email: String, password: String): RepositoryResult
+}
diff --git a/android/app/src/main/java/app/priceguard/data/repository/auth/AuthRepositoryImpl.kt b/android/app/src/main/java/app/priceguard/data/repository/auth/AuthRepositoryImpl.kt
new file mode 100644
index 0000000..d007435
--- /dev/null
+++ b/android/app/src/main/java/app/priceguard/data/repository/auth/AuthRepositoryImpl.kt
@@ -0,0 +1,76 @@
+package app.priceguard.data.repository.auth
+
+import app.priceguard.data.dto.login.LoginRequest
+import app.priceguard.data.dto.signup.SignupRequest
+import app.priceguard.data.network.UserAPI
+import app.priceguard.data.repository.APIResult
+import app.priceguard.data.repository.RepositoryResult
+import app.priceguard.data.repository.getApiResult
+import app.priceguard.ui.data.LoginResult
+import app.priceguard.ui.data.SignupResult
+import javax.inject.Inject
+
+class AuthRepositoryImpl @Inject constructor(private val userAPI: UserAPI) : AuthRepository {
+
+ private fun handleError(
+ code: Int?
+ ): RepositoryResult {
+ return when (code) {
+ 400 -> {
+ RepositoryResult.Error(AuthErrorState.INVALID_REQUEST)
+ }
+
+ 409 -> {
+ RepositoryResult.Error(AuthErrorState.DUPLICATED_EMAIL)
+ }
+
+ else -> {
+ RepositoryResult.Error(AuthErrorState.UNDEFINED_ERROR)
+ }
+ }
+ }
+
+ override suspend fun signUp(
+ email: String,
+ userName: String,
+ password: String
+ ): RepositoryResult {
+ val response = getApiResult {
+ userAPI.register(SignupRequest(email, userName, password))
+ }
+ return when (response) {
+ is APIResult.Success -> {
+ RepositoryResult.Success(
+ SignupResult(
+ response.data.accessToken ?: "",
+ response.data.refreshToken ?: ""
+ )
+ )
+ }
+
+ is APIResult.Error -> {
+ handleError(response.code)
+ }
+ }
+ }
+
+ override suspend fun login(email: String, password: String): RepositoryResult {
+ val response = getApiResult {
+ userAPI.login(LoginRequest(email, password))
+ }
+ return when (response) {
+ is APIResult.Success -> {
+ RepositoryResult.Success(
+ LoginResult(
+ response.data.accessToken ?: "",
+ response.data.refreshToken ?: ""
+ )
+ )
+ }
+
+ is APIResult.Error -> {
+ handleError(response.code)
+ }
+ }
+ }
+}
diff --git a/android/app/src/main/java/app/priceguard/data/dto/ProductErrorState.kt b/android/app/src/main/java/app/priceguard/data/repository/product/ProductErrorState.kt
similarity index 72%
rename from android/app/src/main/java/app/priceguard/data/dto/ProductErrorState.kt
rename to android/app/src/main/java/app/priceguard/data/repository/product/ProductErrorState.kt
index 05d582b..8dd2c74 100644
--- a/android/app/src/main/java/app/priceguard/data/dto/ProductErrorState.kt
+++ b/android/app/src/main/java/app/priceguard/data/repository/product/ProductErrorState.kt
@@ -1,4 +1,4 @@
-package app.priceguard.data.dto
+package app.priceguard.data.repository.product
enum class ProductErrorState {
PERMISSION_DENIED,
diff --git a/android/app/src/main/java/app/priceguard/data/repository/product/ProductRepository.kt b/android/app/src/main/java/app/priceguard/data/repository/product/ProductRepository.kt
new file mode 100644
index 0000000..b0a933a
--- /dev/null
+++ b/android/app/src/main/java/app/priceguard/data/repository/product/ProductRepository.kt
@@ -0,0 +1,28 @@
+package app.priceguard.data.repository.product
+
+import app.priceguard.data.repository.RepositoryResult
+import app.priceguard.ui.data.PricePatchResult
+import app.priceguard.ui.data.ProductAddResult
+import app.priceguard.ui.data.ProductData
+import app.priceguard.ui.data.ProductDetailResult
+import app.priceguard.ui.data.ProductVerifyResult
+import app.priceguard.ui.data.RecommendProductData
+
+interface ProductRepository {
+
+ suspend fun verifyLink(productUrl: String, isRenewed: Boolean = false): RepositoryResult
+
+ suspend fun addProduct(productCode: String, targetPrice: Int, isRenewed: Boolean = false): RepositoryResult
+
+ suspend fun getProductList(isRenewed: Boolean = false): RepositoryResult, ProductErrorState>
+
+ suspend fun getRecommendedProductList(isRenewed: Boolean = false): RepositoryResult, ProductErrorState>
+
+ suspend fun getProductDetail(productCode: String, isRenewed: Boolean = false): RepositoryResult
+
+ suspend fun deleteProduct(productCode: String, isRenewed: Boolean = false): RepositoryResult
+
+ suspend fun updateTargetPrice(productCode: String, targetPrice: Int, isRenewed: Boolean = false): RepositoryResult
+
+ suspend fun switchAlert(productCode: String, isRenewed: Boolean = false): RepositoryResult
+}
diff --git a/android/app/src/main/java/app/priceguard/data/repository/ProductRepositoryImpl.kt b/android/app/src/main/java/app/priceguard/data/repository/product/ProductRepositoryImpl.kt
similarity index 60%
rename from android/app/src/main/java/app/priceguard/data/repository/ProductRepositoryImpl.kt
rename to android/app/src/main/java/app/priceguard/data/repository/product/ProductRepositoryImpl.kt
index 0b269c4..acc3473 100644
--- a/android/app/src/main/java/app/priceguard/data/repository/ProductRepositoryImpl.kt
+++ b/android/app/src/main/java/app/priceguard/data/repository/product/ProductRepositoryImpl.kt
@@ -1,21 +1,20 @@
-package app.priceguard.data.repository
+package app.priceguard.data.repository.product
-import app.priceguard.data.dto.PricePatchRequest
-import app.priceguard.data.dto.PricePatchResponse
-import app.priceguard.data.dto.ProductAddRequest
-import app.priceguard.data.dto.ProductAddResponse
-import app.priceguard.data.dto.ProductData
-import app.priceguard.data.dto.ProductDetailResult
-import app.priceguard.data.dto.ProductErrorState
-import app.priceguard.data.dto.ProductVerifyDTO
-import app.priceguard.data.dto.ProductVerifyRequest
-import app.priceguard.data.dto.RecommendProductData
-import app.priceguard.data.dto.RenewResult
-import app.priceguard.data.graph.GraphDataConverter
-import app.priceguard.data.network.APIResult
+import app.priceguard.data.GraphDataConverter
+import app.priceguard.data.dto.add.ProductAddRequest
+import app.priceguard.data.dto.patch.PricePatchRequest
+import app.priceguard.data.dto.verify.ProductVerifyRequest
import app.priceguard.data.network.ProductAPI
-import app.priceguard.data.network.ProductRepositoryResult
-import app.priceguard.data.network.getApiResult
+import app.priceguard.data.repository.APIResult
+import app.priceguard.data.repository.RepositoryResult
+import app.priceguard.data.repository.getApiResult
+import app.priceguard.data.repository.token.TokenRepository
+import app.priceguard.ui.data.PricePatchResult
+import app.priceguard.ui.data.ProductAddResult
+import app.priceguard.ui.data.ProductData
+import app.priceguard.ui.data.ProductDetailResult
+import app.priceguard.ui.data.ProductVerifyResult
+import app.priceguard.ui.data.RecommendProductData
import javax.inject.Inject
class ProductRepositoryImpl @Inject constructor(
@@ -26,69 +25,73 @@ class ProductRepositoryImpl @Inject constructor(
private suspend fun renew(): Boolean {
val refreshToken = tokenRepository.getRefreshToken() ?: return false
- val renewResult = tokenRepository.renewTokens(refreshToken)
- if (renewResult != RenewResult.SUCCESS) {
- return false
+ return when (tokenRepository.renewTokens(refreshToken)) {
+ is RepositoryResult.Success -> {
+ true
+ }
+
+ is RepositoryResult.Error -> {
+ false
+ }
}
- return true
}
private suspend fun handleError(
code: Int?,
isRenewed: Boolean,
- repoFun: suspend () -> ProductRepositoryResult
- ): ProductRepositoryResult {
+ repoFun: suspend () -> RepositoryResult
+ ): RepositoryResult {
return when (code) {
400 -> {
- ProductRepositoryResult.Error(ProductErrorState.INVALID_REQUEST)
+ RepositoryResult.Error(ProductErrorState.INVALID_REQUEST)
}
401 -> {
- ProductRepositoryResult.Error(ProductErrorState.PERMISSION_DENIED)
+ RepositoryResult.Error(ProductErrorState.PERMISSION_DENIED)
}
404 -> {
- ProductRepositoryResult.Error(ProductErrorState.NOT_FOUND)
+ RepositoryResult.Error(ProductErrorState.NOT_FOUND)
}
409 -> {
- ProductRepositoryResult.Error(ProductErrorState.EXIST)
+ RepositoryResult.Error(ProductErrorState.EXIST)
}
410 -> {
if (isRenewed) {
- ProductRepositoryResult.Error(ProductErrorState.PERMISSION_DENIED)
+ RepositoryResult.Error(ProductErrorState.PERMISSION_DENIED)
} else {
if (renew()) {
repoFun.invoke()
} else {
- ProductRepositoryResult.Error(ProductErrorState.PERMISSION_DENIED)
+ RepositoryResult.Error(ProductErrorState.PERMISSION_DENIED)
}
}
}
else -> {
- ProductRepositoryResult.Error(ProductErrorState.UNDEFINED_ERROR)
+ RepositoryResult.Error(ProductErrorState.UNDEFINED_ERROR)
}
}
}
override suspend fun verifyLink(
- productUrl: ProductVerifyRequest,
+ productUrl: String,
isRenewed: Boolean
- ): ProductRepositoryResult {
+ ): RepositoryResult {
val response = getApiResult {
- productAPI.verifyLink(productUrl)
+ productAPI.verifyLink(ProductVerifyRequest(productUrl))
}
return when (response) {
is APIResult.Success -> {
- ProductRepositoryResult.Success(
- ProductVerifyDTO(
- response.data.productName,
- response.data.productCode,
- response.data.productPrice,
- response.data.shop,
- response.data.imageUrl
+ RepositoryResult.Success(
+ ProductVerifyResult(
+ response.data.productName ?: "",
+ response.data.productCode ?: "",
+ response.data.productPrice ?: 0,
+ response.data.shop ?: "",
+ response.data.imageUrl ?: ""
)
)
}
@@ -102,16 +105,17 @@ class ProductRepositoryImpl @Inject constructor(
}
override suspend fun addProduct(
- productAddRequest: ProductAddRequest,
+ productCode: String,
+ targetPrice: Int,
isRenewed: Boolean
- ): ProductRepositoryResult {
+ ): RepositoryResult {
val response = getApiResult {
- productAPI.addProduct(productAddRequest)
+ productAPI.addProduct(ProductAddRequest(productCode, targetPrice))
}
return when (response) {
is APIResult.Success -> {
- ProductRepositoryResult.Success(
- ProductAddResponse(
+ RepositoryResult.Success(
+ ProductAddResult(
response.data.statusCode,
response.data.message
)
@@ -120,19 +124,19 @@ class ProductRepositoryImpl @Inject constructor(
is APIResult.Error -> {
handleError(response.code, isRenewed) {
- addProduct(productAddRequest, true)
+ addProduct(productCode, targetPrice, true)
}
}
}
}
- override suspend fun getProductList(isRenewed: Boolean): ProductRepositoryResult> {
+ override suspend fun getProductList(isRenewed: Boolean): RepositoryResult, ProductErrorState> {
val response = getApiResult {
productAPI.getProductList()
}
return when (response) {
is APIResult.Success -> {
- ProductRepositoryResult.Success(
+ RepositoryResult.Success(
response.data.trackingList?.map { dto ->
ProductData(
dto.productName ?: "",
@@ -141,6 +145,7 @@ class ProductRepositoryImpl @Inject constructor(
dto.imageUrl ?: "",
dto.targetPrice ?: 0,
dto.price ?: 0,
+ dto.isAlert ?: true,
GraphDataConverter().toDataset(dto.priceData)
)
} ?: listOf()
@@ -155,13 +160,13 @@ class ProductRepositoryImpl @Inject constructor(
}
}
- override suspend fun getRecommendedProductList(isRenewed: Boolean): ProductRepositoryResult> {
+ override suspend fun getRecommendedProductList(isRenewed: Boolean): RepositoryResult, ProductErrorState> {
val response = getApiResult {
productAPI.getRecommendedProductList()
}
return when (response) {
is APIResult.Success -> {
- ProductRepositoryResult.Success(
+ RepositoryResult.Success(
response.data.recommendList?.map { dto ->
RecommendProductData(
dto.productName ?: "",
@@ -187,13 +192,13 @@ class ProductRepositoryImpl @Inject constructor(
override suspend fun getProductDetail(
productCode: String,
isRenewed: Boolean
- ): ProductRepositoryResult {
+ ): RepositoryResult {
val response = getApiResult {
productAPI.getProductDetail(productCode)
}
return when (response) {
is APIResult.Success -> {
- ProductRepositoryResult.Success(
+ RepositoryResult.Success(
ProductDetailResult(
productName = response.data.productName ?: "",
productCode = response.data.productCode ?: "",
@@ -220,10 +225,10 @@ class ProductRepositoryImpl @Inject constructor(
override suspend fun deleteProduct(
productCode: String,
isRenewed: Boolean
- ): ProductRepositoryResult {
+ ): RepositoryResult {
return when (val response = getApiResult { productAPI.deleteProduct(productCode) }) {
is APIResult.Success -> {
- ProductRepositoryResult.Success(true)
+ RepositoryResult.Success(true)
}
is APIResult.Error -> {
@@ -235,16 +240,17 @@ class ProductRepositoryImpl @Inject constructor(
}
override suspend fun updateTargetPrice(
- pricePatchRequest: PricePatchRequest,
+ productCode: String,
+ targetPrice: Int,
isRenewed: Boolean
- ): ProductRepositoryResult {
+ ): RepositoryResult {
val response = getApiResult {
- productAPI.updateTargetPrice(pricePatchRequest)
+ productAPI.updateTargetPrice(PricePatchRequest(productCode, targetPrice))
}
return when (response) {
is APIResult.Success -> {
- ProductRepositoryResult.Success(
- PricePatchResponse(
+ RepositoryResult.Success(
+ PricePatchResult(
response.data.statusCode,
response.data.message
)
@@ -253,7 +259,21 @@ class ProductRepositoryImpl @Inject constructor(
is APIResult.Error -> {
handleError(response.code, isRenewed) {
- updateTargetPrice(pricePatchRequest, true)
+ updateTargetPrice(productCode, targetPrice, true)
+ }
+ }
+ }
+ }
+
+ override suspend fun switchAlert(productCode: String, isRenewed: Boolean): RepositoryResult {
+ return when (val response = getApiResult { productAPI.updateAlert(productCode) }) {
+ is APIResult.Success -> {
+ RepositoryResult.Success(true)
+ }
+
+ is APIResult.Error -> {
+ handleError(response.code, isRenewed) {
+ deleteProduct(productCode, true)
}
}
}
diff --git a/android/app/src/main/java/app/priceguard/data/repository/token/TokenErrorState.kt b/android/app/src/main/java/app/priceguard/data/repository/token/TokenErrorState.kt
new file mode 100644
index 0000000..36506c4
--- /dev/null
+++ b/android/app/src/main/java/app/priceguard/data/repository/token/TokenErrorState.kt
@@ -0,0 +1,7 @@
+package app.priceguard.data.repository.token
+
+enum class TokenErrorState {
+ UNAUTHORIZED,
+ EXPIRED,
+ UNDEFINED_ERROR
+}
diff --git a/android/app/src/main/java/app/priceguard/data/repository/token/TokenRepository.kt b/android/app/src/main/java/app/priceguard/data/repository/token/TokenRepository.kt
new file mode 100644
index 0000000..dff5a21
--- /dev/null
+++ b/android/app/src/main/java/app/priceguard/data/repository/token/TokenRepository.kt
@@ -0,0 +1,15 @@
+package app.priceguard.data.repository.token
+
+import app.priceguard.data.repository.RepositoryResult
+import app.priceguard.ui.data.UserDataResult
+
+interface TokenRepository {
+ suspend fun storeTokens(accessToken: String, refreshToken: String)
+ suspend fun getAccessToken(): String?
+ suspend fun getRefreshToken(): String?
+ suspend fun getFirebaseToken(): String?
+ suspend fun updateFirebaseToken(accessToken: String, firebaseToken: String): RepositoryResult
+ suspend fun getUserData(): UserDataResult
+ suspend fun renewTokens(refreshToken: String): RepositoryResult
+ suspend fun clearTokens()
+}
diff --git a/android/app/src/main/java/app/priceguard/data/repository/token/TokenRepositoryImpl.kt b/android/app/src/main/java/app/priceguard/data/repository/token/TokenRepositoryImpl.kt
new file mode 100644
index 0000000..245322d
--- /dev/null
+++ b/android/app/src/main/java/app/priceguard/data/repository/token/TokenRepositoryImpl.kt
@@ -0,0 +1,111 @@
+package app.priceguard.data.repository.token
+
+import android.util.Log
+import app.priceguard.data.datastore.TokenDataSource
+import app.priceguard.data.dto.firebase.FirebaseTokenUpdateRequest
+import app.priceguard.data.network.AuthAPI
+import app.priceguard.data.network.UserAPI
+import app.priceguard.data.repository.APIResult
+import app.priceguard.data.repository.RepositoryResult
+import app.priceguard.data.repository.getApiResult
+import app.priceguard.ui.data.UserDataResult
+import com.google.firebase.Firebase
+import com.google.firebase.messaging.FirebaseMessaging
+import com.google.firebase.messaging.messaging
+import java.util.*
+import javax.inject.Inject
+import kotlinx.coroutines.tasks.await
+import kotlinx.serialization.json.Json
+
+class TokenRepositoryImpl @Inject constructor(
+ private val tokenDataSource: TokenDataSource,
+ private val authAPI: AuthAPI,
+ private val userAPI: UserAPI
+) : TokenRepository {
+ private fun handleError(
+ code: Int?
+ ): RepositoryResult {
+ return when (code) {
+ 401 -> {
+ RepositoryResult.Error(TokenErrorState.UNAUTHORIZED)
+ }
+
+ 410 -> {
+ RepositoryResult.Error(TokenErrorState.EXPIRED)
+ }
+
+ else -> {
+ RepositoryResult.Error(TokenErrorState.UNDEFINED_ERROR)
+ }
+ }
+ }
+
+ override suspend fun storeTokens(accessToken: String, refreshToken: String) {
+ tokenDataSource.saveTokens(accessToken, refreshToken)
+ }
+
+ override suspend fun getAccessToken(): String? {
+ return tokenDataSource.getAccessToken()
+ }
+
+ override suspend fun getRefreshToken(): String? {
+ return tokenDataSource.getRefreshToken()
+ }
+
+ override suspend fun getFirebaseToken(): String? {
+ return try {
+ FirebaseMessaging.getInstance().token.await()
+ } catch (e: Exception) {
+ Log.e("FCM Token", e.toString())
+ null
+ }
+ }
+
+ override suspend fun updateFirebaseToken(accessToken: String, firebaseToken: String): RepositoryResult {
+ return when (
+ val response =
+ getApiResult { userAPI.updateFirebaseToken("Bearer $accessToken", FirebaseTokenUpdateRequest(firebaseToken)) }
+ ) {
+ is APIResult.Success -> {
+ RepositoryResult.Success(true)
+ }
+
+ is APIResult.Error -> {
+ handleError(response.code)
+ }
+ }
+ }
+
+ override suspend fun getUserData(): UserDataResult {
+ val accessToken = tokenDataSource.getAccessToken() ?: return UserDataResult("", "")
+ val parts = accessToken.split(".")
+ return try {
+ val charset = charset("UTF-8")
+ val payload = Json.decodeFromString(
+ String(Base64.getUrlDecoder().decode(parts[1].toByteArray(charset)), charset)
+ )
+ UserDataResult(payload.email, payload.name)
+ } catch (e: Exception) {
+ Log.e("Data Not Found", "Error parsing JWT: $e")
+ UserDataResult("", "")
+ }
+ }
+
+ override suspend fun renewTokens(refreshToken: String): RepositoryResult {
+ return when (val response = getApiResult { authAPI.renewTokens("Bearer $refreshToken") }) {
+ is APIResult.Success -> {
+ storeTokens(response.data.accessToken, response.data.refreshToken)
+ RepositoryResult.Success(true)
+ }
+
+ is APIResult.Error -> {
+ handleError(response.code)
+ }
+ }
+ }
+
+ override suspend fun clearTokens() {
+ Firebase.messaging.deleteToken()
+ tokenDataSource.clearTokens()
+ }
+}
diff --git a/android/app/src/main/java/app/priceguard/data/dto/UserDataDTO.kt b/android/app/src/main/java/app/priceguard/data/repository/token/TokenUserData.kt
similarity index 55%
rename from android/app/src/main/java/app/priceguard/data/dto/UserDataDTO.kt
rename to android/app/src/main/java/app/priceguard/data/repository/token/TokenUserData.kt
index aae9457..310c9e1 100644
--- a/android/app/src/main/java/app/priceguard/data/dto/UserDataDTO.kt
+++ b/android/app/src/main/java/app/priceguard/data/repository/token/TokenUserData.kt
@@ -1,17 +1,12 @@
-package app.priceguard.data.dto
+package app.priceguard.data.repository.token
import kotlinx.serialization.Serializable
@Serializable
-data class UserDataDTO(
+data class TokenUserData(
val id: String,
val email: String,
val name: String,
val iat: Int,
val exp: Int
)
-
-data class UserDataResult(
- val email: String,
- val name: String
-)
diff --git a/android/app/src/main/java/app/priceguard/di/UserRepositoryModule.kt b/android/app/src/main/java/app/priceguard/di/AuthRepositoryModule.kt
similarity index 55%
rename from android/app/src/main/java/app/priceguard/di/UserRepositoryModule.kt
rename to android/app/src/main/java/app/priceguard/di/AuthRepositoryModule.kt
index efc4b7e..ac878fd 100644
--- a/android/app/src/main/java/app/priceguard/di/UserRepositoryModule.kt
+++ b/android/app/src/main/java/app/priceguard/di/AuthRepositoryModule.kt
@@ -1,8 +1,8 @@
package app.priceguard.di
import app.priceguard.data.network.UserAPI
-import app.priceguard.data.repository.UserRepository
-import app.priceguard.data.repository.UserRepositoryImpl
+import app.priceguard.data.repository.auth.AuthRepository
+import app.priceguard.data.repository.auth.AuthRepositoryImpl
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@@ -11,9 +11,9 @@ import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
-object UserRepositoryModule {
+object AuthRepositoryModule {
@Provides
@Singleton
- fun provideUserRepository(userAPI: UserAPI): UserRepository = UserRepositoryImpl(userAPI)
+ fun provideUserRepository(userAPI: UserAPI): AuthRepository = AuthRepositoryImpl(userAPI)
}
diff --git a/android/app/src/main/java/app/priceguard/di/ConfigDataSourceModule.kt b/android/app/src/main/java/app/priceguard/di/ConfigDataSourceModule.kt
new file mode 100644
index 0000000..c008925
--- /dev/null
+++ b/android/app/src/main/java/app/priceguard/di/ConfigDataSourceModule.kt
@@ -0,0 +1,21 @@
+package app.priceguard.di
+
+import androidx.datastore.core.DataStore
+import androidx.datastore.preferences.core.Preferences
+import app.priceguard.data.datastore.ConfigDataSource
+import app.priceguard.data.datastore.ConfigDataSourceImpl
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+import javax.inject.Singleton
+
+@Module
+@InstallIn(SingletonComponent::class)
+object ConfigDataSourceModule {
+
+ @Provides
+ @Singleton
+ fun provideConfigDataSource(@ConfigQualifier dataStore: DataStore): ConfigDataSource =
+ ConfigDataSourceImpl(dataStore)
+}
diff --git a/android/app/src/main/java/app/priceguard/di/DataStoreModule.kt b/android/app/src/main/java/app/priceguard/di/DataStoreModule.kt
index 841dffe..53a38fb 100644
--- a/android/app/src/main/java/app/priceguard/di/DataStoreModule.kt
+++ b/android/app/src/main/java/app/priceguard/di/DataStoreModule.kt
@@ -12,17 +12,25 @@ import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
+import javax.inject.Qualifier
import javax.inject.Singleton
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.SupervisorJob
private const val TOKEN_FILE = "tokens"
+private const val CONFIG = "config"
+
+@Qualifier
+@Retention(AnnotationRetention.RUNTIME)
+annotation class ConfigQualifier
+
+@Qualifier
+@Retention(AnnotationRetention.RUNTIME)
+annotation class TokensQualifier
@Module
@InstallIn(SingletonComponent::class)
object DataStoreModule {
+ @TokensQualifier
@Provides
@Singleton
fun providePreferencesDataStore(@ApplicationContext appContext: Context): DataStore {
@@ -30,8 +38,19 @@ object DataStoreModule {
corruptionHandler = ReplaceFileCorruptionHandler(
produceNewData = { emptyPreferences() }
),
- scope = CoroutineScope(Dispatchers.IO + SupervisorJob()),
produceFile = { appContext.preferencesDataStoreFile(TOKEN_FILE) }
)
}
+
+ @ConfigQualifier
+ @Provides
+ @Singleton
+ fun provideConfigDataStore(@ApplicationContext appContext: Context): DataStore {
+ return PreferenceDataStoreFactory.create(
+ corruptionHandler = ReplaceFileCorruptionHandler(
+ produceNewData = { emptyPreferences() }
+ ),
+ produceFile = { appContext.preferencesDataStoreFile(CONFIG) }
+ )
+ }
}
diff --git a/android/app/src/main/java/app/priceguard/di/ProductRepositoryModule.kt b/android/app/src/main/java/app/priceguard/di/ProductRepositoryModule.kt
index a835a08..41ce3ac 100644
--- a/android/app/src/main/java/app/priceguard/di/ProductRepositoryModule.kt
+++ b/android/app/src/main/java/app/priceguard/di/ProductRepositoryModule.kt
@@ -1,10 +1,10 @@
package app.priceguard.di
-import app.priceguard.data.graph.GraphDataConverter
+import app.priceguard.data.GraphDataConverter
import app.priceguard.data.network.ProductAPI
-import app.priceguard.data.repository.ProductRepository
-import app.priceguard.data.repository.ProductRepositoryImpl
-import app.priceguard.data.repository.TokenRepository
+import app.priceguard.data.repository.product.ProductRepository
+import app.priceguard.data.repository.product.ProductRepositoryImpl
+import app.priceguard.data.repository.token.TokenRepository
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
diff --git a/android/app/src/main/java/app/priceguard/di/TokenDataSourceModule.kt b/android/app/src/main/java/app/priceguard/di/TokenDataSourceModule.kt
index fc33fae..253f30c 100644
--- a/android/app/src/main/java/app/priceguard/di/TokenDataSourceModule.kt
+++ b/android/app/src/main/java/app/priceguard/di/TokenDataSourceModule.kt
@@ -16,6 +16,6 @@ object TokenDataSourceModule {
@Provides
@Singleton
- fun provideTokenDataSource(dataStore: DataStore): TokenDataSource =
+ fun provideTokenDataSource(@TokensQualifier dataStore: DataStore): TokenDataSource =
TokenDataSourceImpl(dataStore)
}
diff --git a/android/app/src/main/java/app/priceguard/di/TokenRepositoryModule.kt b/android/app/src/main/java/app/priceguard/di/TokenRepositoryModule.kt
index 2169cbe..845aa6f 100644
--- a/android/app/src/main/java/app/priceguard/di/TokenRepositoryModule.kt
+++ b/android/app/src/main/java/app/priceguard/di/TokenRepositoryModule.kt
@@ -2,8 +2,9 @@ package app.priceguard.di
import app.priceguard.data.datastore.TokenDataSource
import app.priceguard.data.network.AuthAPI
-import app.priceguard.data.repository.TokenRepository
-import app.priceguard.data.repository.TokenRepositoryImpl
+import app.priceguard.data.network.UserAPI
+import app.priceguard.data.repository.token.TokenRepository
+import app.priceguard.data.repository.token.TokenRepositoryImpl
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@@ -18,7 +19,8 @@ object TokenRepositoryModule {
@Singleton
fun provideTokenRepository(
tokenDataSource: TokenDataSource,
- authAPI: AuthAPI
+ authAPI: AuthAPI,
+ userAPI: UserAPI
): TokenRepository =
- TokenRepositoryImpl(tokenDataSource, authAPI)
+ TokenRepositoryImpl(tokenDataSource, authAPI, userAPI)
}
diff --git a/android/app/src/main/java/app/priceguard/service/PriceGuardFirebaseMessagingService.kt b/android/app/src/main/java/app/priceguard/service/PriceGuardFirebaseMessagingService.kt
new file mode 100644
index 0000000..e46e26a
--- /dev/null
+++ b/android/app/src/main/java/app/priceguard/service/PriceGuardFirebaseMessagingService.kt
@@ -0,0 +1,11 @@
+package app.priceguard.service
+
+import android.util.Log
+import com.google.firebase.messaging.FirebaseMessagingService
+
+class PriceGuardFirebaseMessagingService : FirebaseMessagingService() {
+ // Init 시에도 호출됨
+ override fun onNewToken(token: String) {
+ Log.d("PriceGuardFirebaseMessagingService", "Refreshed token: $token")
+ }
+}
diff --git a/android/app/src/main/java/app/priceguard/service/UpdateAlarmWorker.kt b/android/app/src/main/java/app/priceguard/service/UpdateAlarmWorker.kt
new file mode 100644
index 0000000..f463b71
--- /dev/null
+++ b/android/app/src/main/java/app/priceguard/service/UpdateAlarmWorker.kt
@@ -0,0 +1,58 @@
+package app.priceguard.service
+
+import android.content.Context
+import android.util.Log
+import androidx.hilt.work.HiltWorker
+import androidx.work.Constraints
+import androidx.work.CoroutineWorker
+import androidx.work.Data
+import androidx.work.OneTimeWorkRequest
+import androidx.work.OneTimeWorkRequestBuilder
+import androidx.work.WorkerParameters
+import app.priceguard.data.repository.RepositoryResult
+import app.priceguard.data.repository.product.ProductRepository
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedInject
+
+@HiltWorker
+class UpdateAlarmWorker @AssistedInject constructor(
+ @Assisted appContext: Context,
+ @Assisted workerParams: WorkerParameters,
+ private val productRepository: ProductRepository
+) : CoroutineWorker(appContext, workerParams) {
+
+ override suspend fun doWork(): Result {
+ val inputData = inputData.getString(ARGUMENT_KEY) ?: return Result.failure()
+
+ return updateAlarm(inputData)
+ }
+
+ private suspend fun updateAlarm(productCode: String): Result {
+ return try {
+ when (productRepository.switchAlert(productCode)) {
+ is RepositoryResult.Error -> {
+ Result.failure()
+ }
+
+ is RepositoryResult.Success -> {
+ Result.success()
+ }
+ }
+ } catch (e: Exception) {
+ Log.e("Update Alarm Error", e.message.toString())
+ Result.failure()
+ }
+ }
+
+ companion object {
+ const val ARGUMENT_KEY = "productCode"
+ fun createWorkRequest(inputString: String): OneTimeWorkRequest {
+ val inputData = Data.Builder().putString(ARGUMENT_KEY, inputString).build()
+ val constraints = Constraints.Builder().build()
+ return OneTimeWorkRequestBuilder()
+ .setInputData(inputData)
+ .setConstraints(constraints)
+ .build()
+ }
+ }
+}
diff --git a/android/app/src/main/java/app/priceguard/service/UpdateTokenWorker.kt b/android/app/src/main/java/app/priceguard/service/UpdateTokenWorker.kt
new file mode 100644
index 0000000..0f4fa3e
--- /dev/null
+++ b/android/app/src/main/java/app/priceguard/service/UpdateTokenWorker.kt
@@ -0,0 +1,46 @@
+package app.priceguard.service
+
+import android.content.Context
+import android.util.Log
+import androidx.hilt.work.HiltWorker
+import androidx.work.CoroutineWorker
+import androidx.work.WorkerParameters
+import app.priceguard.data.repository.RepositoryResult
+import app.priceguard.data.repository.token.TokenRepository
+import com.google.firebase.ktx.Firebase
+import com.google.firebase.messaging.ktx.messaging
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedInject
+import kotlinx.coroutines.tasks.await
+
+@HiltWorker
+class UpdateTokenWorker @AssistedInject constructor(
+ @Assisted appContext: Context,
+ @Assisted workerParams: WorkerParameters,
+ private val tokenRepository: TokenRepository
+) :
+ CoroutineWorker(appContext, workerParams) {
+
+ override suspend fun doWork(): Result {
+ val token = Firebase.messaging.token.await()
+ return storeToken(token)
+ }
+
+ private suspend fun storeToken(token: String): Result {
+ val accessToken = tokenRepository.getAccessToken() ?: return Result.failure()
+ return try {
+ when (tokenRepository.updateFirebaseToken(accessToken, token)) {
+ is RepositoryResult.Error -> {
+ Result.failure()
+ }
+
+ is RepositoryResult.Success -> {
+ Result.success()
+ }
+ }
+ } catch (e: Exception) {
+ Log.e("Update Token Error", e.message.toString())
+ Result.failure()
+ }
+ }
+}
diff --git a/android/app/src/main/java/app/priceguard/ui/PriceGuardApp.kt b/android/app/src/main/java/app/priceguard/ui/PriceGuardApp.kt
index 7743a83..829419b 100644
--- a/android/app/src/main/java/app/priceguard/ui/PriceGuardApp.kt
+++ b/android/app/src/main/java/app/priceguard/ui/PriceGuardApp.kt
@@ -1,7 +1,80 @@
package app.priceguard.ui
import android.app.Application
+import android.app.UiModeManager
+import android.content.Context
+import androidx.appcompat.app.AppCompatDelegate
+import androidx.hilt.work.HiltWorkerFactory
+import androidx.work.Configuration
+import app.priceguard.data.datastore.ConfigDataSource
+import com.google.android.material.color.DynamicColors
import dagger.hilt.android.HiltAndroidApp
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
@HiltAndroidApp
-class PriceGuardApp : Application()
+class PriceGuardApp : Application(), Configuration.Provider {
+
+ @Inject
+ lateinit var configDataSource: ConfigDataSource
+
+ @Inject
+ lateinit var workerFactory: HiltWorkerFactory
+
+ override val workManagerConfiguration: Configuration
+ get() = Configuration.Builder()
+ .setWorkerFactory(workerFactory)
+ .build()
+
+ override fun onCreate() {
+ super.onCreate()
+
+ initAppTheme()
+ }
+
+ private fun initAppTheme() {
+ CoroutineScope(Dispatchers.IO).launch {
+ val dynamicColorMode = configDataSource.getDynamicMode()
+ val darkMode = configDataSource.getDarkMode()
+
+ when (dynamicColorMode) {
+ MODE_DYNAMIC -> {
+ DynamicColors.applyToActivitiesIfAvailable(this@PriceGuardApp)
+ }
+ }
+
+ when (darkMode) {
+ MODE_LIGHT -> {
+ if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) {
+ val uiModeManager =
+ getSystemService(Context.UI_MODE_SERVICE) as UiModeManager
+ uiModeManager.setApplicationNightMode(UiModeManager.MODE_NIGHT_NO)
+ } else {
+ AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
+ }
+ }
+
+ MODE_DARK -> {
+ if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) {
+ val uiModeManager =
+ getSystemService(Context.UI_MODE_SERVICE) as UiModeManager
+ uiModeManager.setApplicationNightMode(UiModeManager.MODE_NIGHT_YES)
+ } else {
+ AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
+ }
+ }
+ }
+ }
+ }
+
+ companion object {
+ const val MODE_SYSTEM = 0
+ const val MODE_LIGHT = 1
+ const val MODE_DARK = 2
+
+ const val MODE_DYNAMIC_NO = 0
+ const val MODE_DYNAMIC = 1
+ }
+}
diff --git a/android/app/src/main/java/app/priceguard/ui/additem/AddItemActivity.kt b/android/app/src/main/java/app/priceguard/ui/additem/AddItemActivity.kt
index fdfba1a..132accb 100644
--- a/android/app/src/main/java/app/priceguard/ui/additem/AddItemActivity.kt
+++ b/android/app/src/main/java/app/priceguard/ui/additem/AddItemActivity.kt
@@ -1,8 +1,12 @@
package app.priceguard.ui.additem
+import android.content.Intent
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
+import androidx.navigation.NavController
+import androidx.navigation.NavDirections
import androidx.navigation.fragment.NavHostFragment
+import app.priceguard.R
import app.priceguard.databinding.ActivityAddItemBinding
import app.priceguard.ui.additem.link.RegisterItemLinkFragmentDirections
import dagger.hilt.android.AndroidEntryPoint
@@ -21,12 +25,19 @@ class AddItemActivity : AppCompatActivity() {
}
private fun setStartDestination() {
- if (intent.hasExtra("productCode") &&
+ val navController = binding.fcvAddItem.getFragment().navController
+
+ if (intent?.action == Intent.ACTION_SEND && intent.type == "text/plain") {
+ intent.getStringExtra(Intent.EXTRA_TEXT)?.let { data ->
+ val bundle = Bundle()
+ bundle.putString("link", data)
+ navController.navigate(R.id.registerItemLinkFragment, bundle)
+ }
+ } else if (intent.hasExtra("productCode") &&
intent.hasExtra("productTitle") &&
intent.hasExtra("productPrice") &&
intent.hasExtra("isAdding")
) {
- val navController = binding.fcvAddItem.getFragment().navController
val action =
RegisterItemLinkFragmentDirections.actionRegisterItemLinkFragmentToSetTargetPriceFragment(
intent.getStringExtra("productCode") ?: "",
@@ -34,7 +45,11 @@ class AddItemActivity : AppCompatActivity() {
intent.getIntExtra("productPrice", 0),
intent.getBooleanExtra("isAdding", true)
)
- navController.navigate(action)
+ navController.safeNavigate(action)
}
}
+
+ private fun NavController.safeNavigate(direction: NavDirections) {
+ currentDestination?.getAction(direction.actionId)?.run { navigate(direction) }
+ }
}
diff --git a/android/app/src/main/java/app/priceguard/ui/additem/confirm/ConfirmItemLinkFragment.kt b/android/app/src/main/java/app/priceguard/ui/additem/confirm/ConfirmItemLinkFragment.kt
index 8af63f0..f8f434a 100644
--- a/android/app/src/main/java/app/priceguard/ui/additem/confirm/ConfirmItemLinkFragment.kt
+++ b/android/app/src/main/java/app/priceguard/ui/additem/confirm/ConfirmItemLinkFragment.kt
@@ -4,20 +4,17 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
+import androidx.activity.viewModels
import androidx.fragment.app.Fragment
+import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
-import app.priceguard.R
-import app.priceguard.data.dto.ProductVerifyDTO
import app.priceguard.databinding.FragmentConfirmItemLinkBinding
-import java.text.NumberFormat
-import kotlinx.serialization.json.Json
class ConfirmItemLinkFragment : Fragment() {
private var _binding: FragmentConfirmItemLinkBinding? = null
private val binding get() = _binding!!
-
- private lateinit var productInfo: ProductVerifyDTO
+ private val confirmItemLinkViewModel: ConfirmItemLinkViewModel by viewModels()
override fun onCreateView(
inflater: LayoutInflater,
@@ -30,23 +27,20 @@ class ConfirmItemLinkFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
-
+ binding.viewModel = confirmItemLinkViewModel
+ binding.lifecycleOwner = viewLifecycleOwner
binding.initListener()
- binding.initView()
+ initView()
}
- private fun FragmentConfirmItemLinkBinding.initView() {
- val productJson = requireArguments().getString("product") ?: return
- productInfo = Json.decodeFromString(productJson)
-
- tvConfirmItemPrice.text =
- String.format(
- resources.getString(R.string.won),
- NumberFormat.getNumberInstance().format(productInfo.productPrice)
- )
- tvConfirmItemBrand.text = productInfo.shop
- tvConfirmItemItemTitle.text = productInfo.productName
- imageUrl = productInfo.imageUrl
+ private fun initView() {
+ val arguments = requireArguments()
+ confirmItemLinkViewModel.setUIState(
+ price = arguments.getInt("productPrice"),
+ brand = arguments.getString("shop") ?: return,
+ name = arguments.getString("productName") ?: return,
+ imageUrl = arguments.getString("imageUrl") ?: return
+ )
}
override fun onDestroyView() {
@@ -55,12 +49,14 @@ class ConfirmItemLinkFragment : Fragment() {
}
private fun FragmentConfirmItemLinkBinding.initListener() {
+ val arguments = requireArguments()
+
btnConfirmItemNext.setOnClickListener {
val action =
ConfirmItemLinkFragmentDirections.actionConfirmItemLinkFragmentToSetTargetPriceFragment(
- productInfo.productCode ?: "",
- productInfo.productName ?: "",
- productInfo.productPrice ?: 0,
+ arguments.getString("productCode") ?: "",
+ arguments.getString("productName") ?: "",
+ arguments.getInt("productPrice"),
true
)
findNavController().navigate(action)
diff --git a/android/app/src/main/java/app/priceguard/ui/additem/confirm/ConfirmItemLinkViewModel.kt b/android/app/src/main/java/app/priceguard/ui/additem/confirm/ConfirmItemLinkViewModel.kt
index 68503f6..cd9db34 100644
--- a/android/app/src/main/java/app/priceguard/ui/additem/confirm/ConfirmItemLinkViewModel.kt
+++ b/android/app/src/main/java/app/priceguard/ui/additem/confirm/ConfirmItemLinkViewModel.kt
@@ -1,16 +1,27 @@
package app.priceguard.ui.additem.confirm
import androidx.lifecycle.ViewModel
-import app.priceguard.data.dto.ProductVerifyDTO
import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.update
class ConfirmItemLinkViewModel : ViewModel() {
- private val _flow = MutableStateFlow(ProductVerifyDTO("", "", 0, "", ""))
- val flow = _flow.asStateFlow()
+ data class ConfirmItemLinkUIState(
+ val price: Int? = null,
+ val brand: String? = null,
+ val name: String? = null,
+ val imageUrl: String? = null
+ )
- fun setProductInfo(productInfo: ProductVerifyDTO) {
- _flow.value = productInfo
+ private var _state: MutableStateFlow =
+ MutableStateFlow(ConfirmItemLinkUIState())
+ val state: StateFlow = _state.asStateFlow()
+
+ fun setUIState(price: Int, brand: String, name: String, imageUrl: String) {
+ _state.update {
+ it.copy(price = price, brand = brand, name = name, imageUrl = imageUrl)
+ }
}
}
diff --git a/android/app/src/main/java/app/priceguard/ui/additem/link/LinkHelperWebViewActivity.kt b/android/app/src/main/java/app/priceguard/ui/additem/link/LinkHelperWebViewActivity.kt
new file mode 100644
index 0000000..23727c6
--- /dev/null
+++ b/android/app/src/main/java/app/priceguard/ui/additem/link/LinkHelperWebViewActivity.kt
@@ -0,0 +1,19 @@
+package app.priceguard.ui.additem.link
+
+import android.os.Bundle
+import androidx.appcompat.app.AppCompatActivity
+import app.priceguard.databinding.ActivityLinkHelperWebViewBinding
+
+class LinkHelperWebViewActivity : AppCompatActivity() {
+
+ private lateinit var binding: ActivityLinkHelperWebViewBinding
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ binding = ActivityLinkHelperWebViewBinding.inflate(layoutInflater)
+ setContentView(binding.root)
+
+ binding.wbLinkHelper.loadUrl("https://info-kr.priceguard.app/")
+ }
+}
diff --git a/android/app/src/main/java/app/priceguard/ui/additem/link/RegisterItemLinkFragment.kt b/android/app/src/main/java/app/priceguard/ui/additem/link/RegisterItemLinkFragment.kt
index e9024e8..e5d3c2f 100644
--- a/android/app/src/main/java/app/priceguard/ui/additem/link/RegisterItemLinkFragment.kt
+++ b/android/app/src/main/java/app/priceguard/ui/additem/link/RegisterItemLinkFragment.kt
@@ -1,24 +1,25 @@
package app.priceguard.ui.additem.link
+import android.content.Intent
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
+import androidx.activity.addCallback
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.navigation.NavController
import androidx.navigation.NavDirections
import androidx.navigation.fragment.findNavController
import app.priceguard.R
-import app.priceguard.data.dto.ProductErrorState
-import app.priceguard.data.repository.TokenRepository
+import app.priceguard.data.repository.product.ProductErrorState
+import app.priceguard.data.repository.token.TokenRepository
import app.priceguard.databinding.FragmentRegisterItemLinkBinding
+import app.priceguard.ui.home.HomeActivity
import app.priceguard.ui.util.lifecycle.repeatOnStarted
import app.priceguard.ui.util.ui.showPermissionDeniedDialog
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
-import kotlinx.serialization.encodeToString
-import kotlinx.serialization.json.Json
@AndroidEntryPoint
class RegisterItemLinkFragment : Fragment() {
@@ -28,7 +29,7 @@ class RegisterItemLinkFragment : Fragment() {
private var _binding: FragmentRegisterItemLinkBinding? = null
private val binding get() = _binding!!
- private val viewModel: RegisterItemLinkViewModel by viewModels()
+ private val registerItemLinkViewModel: RegisterItemLinkViewModel by viewModels()
override fun onCreateView(
inflater: LayoutInflater,
@@ -42,14 +43,46 @@ class RegisterItemLinkFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.lifecycleOwner = viewLifecycleOwner
- binding.viewModel = viewModel
+ binding.viewModel = registerItemLinkViewModel
+
+ setBackPressedCallback()
+ setLinkText()
+
initCollector()
initEvent()
+
+ binding.tvRegisterItemHelper.setOnClickListener {
+ val intent = Intent(requireActivity(), LinkHelperWebViewActivity::class.java)
+ startActivity(intent)
+ }
+ }
+
+ private fun setLinkText() {
+ arguments?.getString("link")?.let { linkText ->
+ binding.etRegisterItemLink.setText(linkText)
+ registerItemLinkViewModel.updateLink(linkText)
+ }
+ }
+
+ private fun setBackPressedCallback() {
+ requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner) {
+ goToHomeActivity()
+ }
+ }
+
+ private fun goToHomeActivity() {
+ val activityIntent = requireActivity().intent
+ if (activityIntent?.action == Intent.ACTION_SEND) {
+ val intent = Intent(requireActivity(), HomeActivity::class.java)
+ intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
+ startActivity(intent)
+ }
+ requireActivity().finish()
}
private fun initCollector() {
repeatOnStarted {
- viewModel.state.collect { state ->
+ registerItemLinkViewModel.state.collect { state ->
if (state.isLinkError) {
updateLinkError(getString(R.string.not_link))
} else {
@@ -61,12 +94,16 @@ class RegisterItemLinkFragment : Fragment() {
private fun initEvent() {
repeatOnStarted {
- viewModel.event.collect { event ->
+ registerItemLinkViewModel.event.collect { event ->
when (event) {
is RegisterItemLinkViewModel.RegisterLinkEvent.SuccessVerification -> {
val action =
RegisterItemLinkFragmentDirections.actionRegisterItemLinkFragmentToConfirmItemLinkFragment(
- Json.encodeToString(event.product)
+ event.product.productCode,
+ event.product.productPrice,
+ event.product.shop,
+ event.product.productName,
+ event.product.imageUrl
)
findNavController().safeNavigate(action)
}
diff --git a/android/app/src/main/java/app/priceguard/ui/additem/link/RegisterItemLinkViewModel.kt b/android/app/src/main/java/app/priceguard/ui/additem/link/RegisterItemLinkViewModel.kt
index e933a86..15c4659 100644
--- a/android/app/src/main/java/app/priceguard/ui/additem/link/RegisterItemLinkViewModel.kt
+++ b/android/app/src/main/java/app/priceguard/ui/additem/link/RegisterItemLinkViewModel.kt
@@ -2,11 +2,10 @@ package app.priceguard.ui.additem.link
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
-import app.priceguard.data.dto.ProductErrorState
-import app.priceguard.data.dto.ProductVerifyDTO
-import app.priceguard.data.dto.ProductVerifyRequest
-import app.priceguard.data.network.ProductRepositoryResult
-import app.priceguard.data.repository.ProductRepository
+import app.priceguard.data.repository.RepositoryResult
+import app.priceguard.data.repository.product.ProductErrorState
+import app.priceguard.data.repository.product.ProductRepository
+import app.priceguard.ui.data.ProductVerifyResult
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
import kotlinx.coroutines.flow.MutableSharedFlow
@@ -21,13 +20,13 @@ class RegisterItemLinkViewModel
data class RegisterLinkUIState(
val link: String = "",
- val product: ProductVerifyDTO? = null,
- val isNextReady: Boolean = true,
+ val product: ProductVerifyResult? = null,
+ val isNextReady: Boolean = false,
val isLinkError: Boolean = false
)
sealed class RegisterLinkEvent {
- data class SuccessVerification(val product: ProductVerifyDTO) : RegisterLinkEvent()
+ data class SuccessVerification(val product: ProductVerifyResult) : RegisterLinkEvent()
data class FailureVerification(val errorType: ProductErrorState) : RegisterLinkEvent()
}
@@ -55,16 +54,15 @@ class RegisterItemLinkViewModel
)
viewModelScope.launch {
- val response = productRepository.verifyLink(ProductVerifyRequest(state.value.link))
- when (response) {
- is ProductRepositoryResult.Success -> {
+ when (val response = productRepository.verifyLink(state.value.link)) {
+ is RepositoryResult.Success -> {
_state.value = state.value.copy(isNextReady = true, product = response.data)
_event.emit(RegisterLinkEvent.SuccessVerification(response.data))
}
- is ProductRepositoryResult.Error -> {
+ is RepositoryResult.Error -> {
_state.value = state.value.copy(isLinkError = true, isNextReady = false)
- _event.emit(RegisterLinkEvent.FailureVerification(response.productErrorState))
+ _event.emit(RegisterLinkEvent.FailureVerification(response.errorState))
}
}
}
diff --git a/android/app/src/main/java/app/priceguard/ui/additem/setprice/SetTargetPriceFragment.kt b/android/app/src/main/java/app/priceguard/ui/additem/setprice/SetTargetPriceFragment.kt
index 51167ba..209d6fd 100644
--- a/android/app/src/main/java/app/priceguard/ui/additem/setprice/SetTargetPriceFragment.kt
+++ b/android/app/src/main/java/app/priceguard/ui/additem/setprice/SetTargetPriceFragment.kt
@@ -1,5 +1,6 @@
package app.priceguard.ui.additem.setprice
+import android.content.Intent
import android.os.Bundle
import android.text.Editable
import android.view.LayoutInflater
@@ -11,10 +12,11 @@ import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
import app.priceguard.R
-import app.priceguard.data.dto.ProductErrorState
-import app.priceguard.data.repository.TokenRepository
+import app.priceguard.data.repository.product.ProductErrorState
+import app.priceguard.data.repository.token.TokenRepository
import app.priceguard.databinding.FragmentSetTargetPriceBinding
import app.priceguard.ui.additem.setprice.SetTargetPriceViewModel.SetTargetPriceEvent
+import app.priceguard.ui.home.HomeActivity
import app.priceguard.ui.util.lifecycle.repeatOnStarted
import app.priceguard.ui.util.ui.showPermissionDeniedDialog
import com.google.android.material.dialog.MaterialAlertDialogBuilder
@@ -32,7 +34,7 @@ class SetTargetPriceFragment : Fragment() {
private var _binding: FragmentSetTargetPriceBinding? = null
private val binding get() = _binding!!
- private val viewModel: SetTargetPriceViewModel by viewModels()
+ private val setTargetPriceViewModel: SetTargetPriceViewModel by viewModels()
override fun onCreateView(
inflater: LayoutInflater,
@@ -46,34 +48,44 @@ class SetTargetPriceFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
- binding.vm = viewModel
+ binding.viewModel = setTargetPriceViewModel
binding.lifecycleOwner = viewLifecycleOwner
- val callback = requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner) {
+ setBackPressedCallback()
+ binding.initView()
+ binding.initListener()
+ handleEvent()
+ }
+
+ private fun setBackPressedCallback() {
+ requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner) {
if (requireActivity().intent.hasExtra("isAdding")) {
requireActivity().finish()
} else {
findNavController().navigateUp()
}
}
+ }
+
+ private fun FragmentSetTargetPriceBinding.initView() {
+ val arguments = requireArguments()
- val productCode = requireArguments().getString("productCode") ?: ""
- val title = requireArguments().getString("productTitle") ?: ""
- val price = requireArguments().getInt("productPrice")
+ val productCode = arguments.getString("productCode") ?: ""
+ val title = arguments.getString("productTitle") ?: ""
+ val price = arguments.getInt("productPrice")
- viewModel.updateTargetPrice((price * 0.8).toInt())
+ setTargetPriceViewModel.updateTargetPrice((price * 0.8).toInt())
- binding.tvSetPriceCurrentPrice.text =
+ tvSetPriceCurrentPrice.text =
String.format(
resources.getString(R.string.won),
NumberFormat.getNumberInstance().format(price)
)
+ tvSetPriceCurrentPrice.contentDescription =
+ getString(R.string.current_price_info, tvSetPriceCurrentPrice.text)
- viewModel.setProductInfo(productCode, title, price)
- binding.etTargetPrice.setText((price * 0.8).toInt().toString())
-
- binding.initListener()
- handleEvent()
+ setTargetPriceViewModel.setProductInfo(productCode, title, price)
+ etTargetPrice.setText((price * 0.8).toInt().toString())
}
private fun FragmentSetTargetPriceBinding.initListener() {
@@ -86,7 +98,7 @@ class SetTargetPriceFragment : Fragment() {
}
btnConfirmItemNext.setOnClickListener {
val isAdding = requireArguments().getBoolean("isAdding")
- if (isAdding) viewModel.addProduct() else viewModel.patchProduct()
+ if (isAdding) setTargetPriceViewModel.addProduct() else setTargetPriceViewModel.patchProduct()
}
slTargetPrice.addOnChangeListener { _, value, _ ->
if (!etTargetPrice.isFocused) {
@@ -115,14 +127,20 @@ class SetTargetPriceFragment : Fragment() {
0F
}
- viewModel.updateTargetPrice(targetPrice.toInt())
+ setTargetPriceViewModel.updateTargetPrice(targetPrice.toInt())
val percent =
- ((targetPrice / viewModel.state.value.productPrice) * MAX_PERCENT).toInt()
+ ((targetPrice / setTargetPriceViewModel.state.value.productPrice) * MAX_PERCENT).toInt()
binding.tvTargetPricePercent.text =
String.format(getString(R.string.current_price_percent), percent)
+ binding.tvTargetPricePercent.contentDescription = getString(
+ R.string.target_price_percent_and_price,
+ binding.tvTargetPricePercent.text,
+ binding.tvSetPriceCurrentPrice.text
+ )
+
binding.updateSlideValueWithPrice(targetPrice, percent.roundAtFirstDigit())
}
}
@@ -132,18 +150,18 @@ class SetTargetPriceFragment : Fragment() {
}
private fun FragmentSetTargetPriceBinding.setTargetPriceAndPercent(value: Float) {
- val targetPrice = ((viewModel.state.value.productPrice) * value.toInt() / 100)
+ val targetPrice = ((setTargetPriceViewModel.state.value.productPrice) * value.toInt() / 100)
tvTargetPricePercent.text =
String.format(getString(R.string.current_price_percent), value.toInt())
etTargetPrice.setText(
targetPrice.toString()
)
- viewModel.updateTargetPrice(targetPrice)
+ setTargetPriceViewModel.updateTargetPrice(targetPrice)
}
private fun handleEvent() {
repeatOnStarted {
- viewModel.event.collect { event ->
+ setTargetPriceViewModel.event.collect { event ->
when (event) {
is SetTargetPriceEvent.SuccessProductAdd -> {
showActivityFinishDialog(
@@ -204,18 +222,28 @@ class SetTargetPriceFragment : Fragment() {
MaterialAlertDialogBuilder(requireActivity(), R.style.ThemeOverlay_App_MaterialAlertDialog)
.setTitle(title)
.setMessage(message)
- .setPositiveButton(R.string.confirm) { _, _ -> requireActivity().finish() }
- .setOnDismissListener { requireActivity().finish() }
+ .setPositiveButton(R.string.confirm) { _, _ -> goToHomeActivity() }
+ .setOnDismissListener { goToHomeActivity() }
.create()
.show()
}
+ private fun goToHomeActivity() {
+ val activityIntent = requireActivity().intent
+ if (activityIntent?.action == Intent.ACTION_SEND) {
+ val intent = Intent(requireActivity(), HomeActivity::class.java)
+ intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
+ startActivity(intent)
+ }
+ requireActivity().finish()
+ }
+
private fun FragmentSetTargetPriceBinding.updateSlideValueWithPrice(
targetPrice: Float,
percent: Int
) {
val pricePercent = percent.coerceIn(MIN_PERCENT, MAX_PERCENT)
- if (targetPrice > viewModel.state.value.productPrice) {
+ if (targetPrice > setTargetPriceViewModel.state.value.productPrice) {
tvTargetPricePercent.text = getString(R.string.over_current_price)
}
slTargetPrice.value = pricePercent.toFloat()
diff --git a/android/app/src/main/java/app/priceguard/ui/additem/setprice/SetTargetPriceViewModel.kt b/android/app/src/main/java/app/priceguard/ui/additem/setprice/SetTargetPriceViewModel.kt
index 7a95341..5120522 100644
--- a/android/app/src/main/java/app/priceguard/ui/additem/setprice/SetTargetPriceViewModel.kt
+++ b/android/app/src/main/java/app/priceguard/ui/additem/setprice/SetTargetPriceViewModel.kt
@@ -2,11 +2,9 @@ package app.priceguard.ui.additem.setprice
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
-import app.priceguard.data.dto.PricePatchRequest
-import app.priceguard.data.dto.ProductAddRequest
-import app.priceguard.data.dto.ProductErrorState
-import app.priceguard.data.network.ProductRepositoryResult
-import app.priceguard.data.repository.ProductRepository
+import app.priceguard.data.repository.RepositoryResult
+import app.priceguard.data.repository.product.ProductErrorState
+import app.priceguard.data.repository.product.ProductRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
import kotlinx.coroutines.flow.MutableSharedFlow
@@ -42,18 +40,16 @@ class SetTargetPriceViewModel @Inject constructor(private val productRepository:
fun addProduct() {
viewModelScope.launch {
val response = productRepository.addProduct(
- ProductAddRequest(
- _state.value.productCode,
- _state.value.targetPrice
- )
+ _state.value.productCode,
+ _state.value.targetPrice
)
when (response) {
- is ProductRepositoryResult.Success -> {
+ is RepositoryResult.Success -> {
_event.emit(SetTargetPriceEvent.SuccessProductAdd)
}
- is ProductRepositoryResult.Error -> {
- _event.emit(SetTargetPriceEvent.FailurePriceAdd(response.productErrorState))
+ is RepositoryResult.Error -> {
+ _event.emit(SetTargetPriceEvent.FailurePriceAdd(response.errorState))
}
}
}
@@ -62,18 +58,16 @@ class SetTargetPriceViewModel @Inject constructor(private val productRepository:
fun patchProduct() {
viewModelScope.launch {
val response = productRepository.updateTargetPrice(
- PricePatchRequest(
- _state.value.productCode,
- _state.value.targetPrice
- )
+ _state.value.productCode,
+ _state.value.targetPrice
)
when (response) {
- is ProductRepositoryResult.Success -> {
+ is RepositoryResult.Success -> {
_event.emit(SetTargetPriceEvent.SuccessPriceUpdate)
}
- is ProductRepositoryResult.Error -> {
- _event.emit(SetTargetPriceEvent.FailurePriceUpdate(response.productErrorState))
+ is RepositoryResult.Error -> {
+ _event.emit(SetTargetPriceEvent.FailurePriceUpdate(response.errorState))
}
}
}
diff --git a/android/app/src/main/java/app/priceguard/ui/data/LoginResult.kt b/android/app/src/main/java/app/priceguard/ui/data/LoginResult.kt
new file mode 100644
index 0000000..e8924ce
--- /dev/null
+++ b/android/app/src/main/java/app/priceguard/ui/data/LoginResult.kt
@@ -0,0 +1,6 @@
+package app.priceguard.ui.data
+
+data class LoginResult(
+ val accessToken: String,
+ val refreshToken: String
+)
diff --git a/android/app/src/main/java/app/priceguard/ui/data/PricePatchResult.kt b/android/app/src/main/java/app/priceguard/ui/data/PricePatchResult.kt
new file mode 100644
index 0000000..8f87999
--- /dev/null
+++ b/android/app/src/main/java/app/priceguard/ui/data/PricePatchResult.kt
@@ -0,0 +1,6 @@
+package app.priceguard.ui.data
+
+data class PricePatchResult(
+ val statusCode: Int,
+ val message: String
+)
diff --git a/android/app/src/main/java/app/priceguard/ui/data/ProductAddResult.kt b/android/app/src/main/java/app/priceguard/ui/data/ProductAddResult.kt
new file mode 100644
index 0000000..f329760
--- /dev/null
+++ b/android/app/src/main/java/app/priceguard/ui/data/ProductAddResult.kt
@@ -0,0 +1,6 @@
+package app.priceguard.ui.data
+
+data class ProductAddResult(
+ val statusCode: Int,
+ val message: String
+)
diff --git a/android/app/src/main/java/app/priceguard/ui/data/ProductData.kt b/android/app/src/main/java/app/priceguard/ui/data/ProductData.kt
new file mode 100644
index 0000000..4313894
--- /dev/null
+++ b/android/app/src/main/java/app/priceguard/ui/data/ProductData.kt
@@ -0,0 +1,14 @@
+package app.priceguard.ui.data
+
+import app.priceguard.data.graph.ProductChartData
+
+data class ProductData(
+ val productName: String,
+ val productCode: String,
+ val shop: String,
+ val imageUrl: String,
+ val targetPrice: Int,
+ val price: Int,
+ val isAlert: Boolean,
+ val priceData: List
+)
diff --git a/android/app/src/main/java/app/priceguard/ui/data/ProductDetailResult.kt b/android/app/src/main/java/app/priceguard/ui/data/ProductDetailResult.kt
new file mode 100644
index 0000000..5d7df31
--- /dev/null
+++ b/android/app/src/main/java/app/priceguard/ui/data/ProductDetailResult.kt
@@ -0,0 +1,16 @@
+package app.priceguard.ui.data
+
+import app.priceguard.data.graph.ProductChartData
+
+data class ProductDetailResult(
+ val productName: String,
+ val productCode: String,
+ val shop: String,
+ val imageUrl: String,
+ val rank: Int,
+ val shopUrl: String,
+ val targetPrice: Int,
+ val lowestPrice: Int,
+ val price: Int,
+ val priceData: List
+)
diff --git a/android/app/src/main/java/app/priceguard/ui/data/ProductVerifyResult.kt b/android/app/src/main/java/app/priceguard/ui/data/ProductVerifyResult.kt
new file mode 100644
index 0000000..1e2bd89
--- /dev/null
+++ b/android/app/src/main/java/app/priceguard/ui/data/ProductVerifyResult.kt
@@ -0,0 +1,9 @@
+package app.priceguard.ui.data
+
+data class ProductVerifyResult(
+ val productName: String,
+ val productCode: String,
+ val productPrice: Int,
+ val shop: String,
+ val imageUrl: String
+)
diff --git a/android/app/src/main/java/app/priceguard/ui/data/RecommendProductData.kt b/android/app/src/main/java/app/priceguard/ui/data/RecommendProductData.kt
new file mode 100644
index 0000000..85e5ce2
--- /dev/null
+++ b/android/app/src/main/java/app/priceguard/ui/data/RecommendProductData.kt
@@ -0,0 +1,13 @@
+package app.priceguard.ui.data
+
+import app.priceguard.data.graph.ProductChartData
+
+data class RecommendProductData(
+ val productName: String,
+ val productCode: String,
+ val shop: String,
+ val imageUrl: String,
+ val price: Int,
+ val rank: Int,
+ val priceData: List
+)
diff --git a/android/app/src/main/java/app/priceguard/ui/data/SignupResult.kt b/android/app/src/main/java/app/priceguard/ui/data/SignupResult.kt
new file mode 100644
index 0000000..b4697a8
--- /dev/null
+++ b/android/app/src/main/java/app/priceguard/ui/data/SignupResult.kt
@@ -0,0 +1,6 @@
+package app.priceguard.ui.data
+
+data class SignupResult(
+ val accessToken: String,
+ val refreshToken: String
+)
diff --git a/android/app/src/main/java/app/priceguard/ui/data/UserDataResult.kt b/android/app/src/main/java/app/priceguard/ui/data/UserDataResult.kt
new file mode 100644
index 0000000..37290ce
--- /dev/null
+++ b/android/app/src/main/java/app/priceguard/ui/data/UserDataResult.kt
@@ -0,0 +1,6 @@
+package app.priceguard.ui.data
+
+data class UserDataResult(
+ val email: String,
+ val name: String
+)
diff --git a/android/app/src/main/java/app/priceguard/ui/detail/DetailActivity.kt b/android/app/src/main/java/app/priceguard/ui/detail/DetailActivity.kt
index 189c358..e34804e 100644
--- a/android/app/src/main/java/app/priceguard/ui/detail/DetailActivity.kt
+++ b/android/app/src/main/java/app/priceguard/ui/detail/DetailActivity.kt
@@ -5,15 +5,18 @@ import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.widget.Toast
+import androidx.activity.addCallback
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import app.priceguard.R
-import app.priceguard.data.dto.ProductErrorState
+import app.priceguard.data.graph.ProductChartDataset
import app.priceguard.data.graph.ProductChartGridLine
-import app.priceguard.data.repository.TokenRepository
+import app.priceguard.data.repository.product.ProductErrorState
+import app.priceguard.data.repository.token.TokenRepository
import app.priceguard.databinding.ActivityDetailBinding
import app.priceguard.materialchart.data.GraphMode
import app.priceguard.ui.additem.AddItemActivity
+import app.priceguard.ui.home.HomeActivity
import app.priceguard.ui.util.lifecycle.repeatOnStarted
import app.priceguard.ui.util.ui.showConfirmationDialog
import app.priceguard.ui.util.ui.showPermissionDeniedDialog
@@ -36,13 +39,19 @@ class DetailActivity : AppCompatActivity() {
binding.viewModel = productDetailViewModel
setContentView(binding.root)
+ setBackPressedCallback()
initListener()
setNavigationButton()
- setGraph()
- checkProductCode()
+ checkProductCode(intent)
observeEvent()
}
+ private fun setBackPressedCallback() {
+ onBackPressedDispatcher.addCallback(this) {
+ goToHomeActivityIfDeepLinked()
+ }
+ }
+
override fun onStart() {
super.onStart()
if (productDetailViewModel.state.value.isReady) {
@@ -90,15 +99,48 @@ class DetailActivity : AppCompatActivity() {
}
}
}
+
+ binding.btnDetailShare.setOnClickListener {
+ val shareLink =
+ getString(R.string.share_link_template, productDetailViewModel.productCode)
+
+ val sendIntent: Intent = Intent().apply {
+ action = Intent.ACTION_SEND
+ putExtra(Intent.EXTRA_TITLE, getString(R.string.share_product))
+ putExtra(Intent.EXTRA_TEXT, getString(R.string.share_message_template, shareLink))
+ type = "text/plain"
+ }
+
+ val shareIntent = Intent.createChooser(sendIntent, null)
+ startActivity(shareIntent)
+ }
+ }
+
+ override fun onNewIntent(intent: Intent?) {
+ super.onNewIntent(intent)
+ if (intent != null) {
+ checkProductCode(intent)
+ }
}
- private fun checkProductCode() {
+ private fun checkProductCode(intent: Intent) {
val productCode = intent.getStringExtra("productCode")
- if (productCode == null) {
- // Invalid access
+ val deepLink = intent.data
+ val productCodeFromDeepLink = deepLink?.getQueryParameter("code")
+
+ if (productCode == null && productCodeFromDeepLink == null) {
showDialogAndExit(getString(R.string.error), getString(R.string.invalid_access))
- } else {
- productDetailViewModel.productCode = productCode
+ return
+ }
+
+ productCode?.let { code ->
+ productDetailViewModel.productCode = code
+ productDetailViewModel.getDetails(false)
+ return
+ }
+
+ productCodeFromDeepLink?.let { code ->
+ productDetailViewModel.productCode = code
productDetailViewModel.getDetails(false)
}
}
@@ -106,17 +148,16 @@ class DetailActivity : AppCompatActivity() {
private fun observeEvent() {
repeatOnStarted {
productDetailViewModel.state.collect { state ->
- binding.chGraphDetail.dataset = state.chartData?.copy(
- gridLines = listOf(
- ProductChartGridLine(
- resources.getString(R.string.target_price),
- state.targetPrice?.toFloat() ?: 0F
- ),
- ProductChartGridLine(
- resources.getString(R.string.lowest_price),
- state.lowestPrice?.toFloat() ?: 0F
- )
- )
+ state.targetPrice ?: return@collect
+ binding.chGraphDetail.dataset = ProductChartDataset(
+ showXAxis = true,
+ showYAxis = true,
+ isInteractive = true,
+ graphMode = state.graphMode,
+ xLabel = getString(R.string.date_text),
+ yLabel = getString(R.string.price_text),
+ data = state.chartData,
+ gridLines = getGridLines(state.targetPrice.toFloat())
)
}
}
@@ -193,14 +234,32 @@ class DetailActivity : AppCompatActivity() {
}
}
+ private fun getGridLines(targetPrice: Float): List {
+ return if (targetPrice < 0) {
+ listOf()
+ } else {
+ listOf(
+ ProductChartGridLine(
+ resources.getString(R.string.target_price),
+ targetPrice
+ )
+ )
+ }
+ }
+
private fun setNavigationButton() {
binding.mtDetailTopbar.setNavigationOnClickListener {
- finish()
+ goToHomeActivityIfDeepLinked()
}
}
- private fun setGraph() {
- binding.chGraphDetail.setXAxisMargin(48F)
+ private fun goToHomeActivityIfDeepLinked() {
+ if (intent.data?.getQueryParameter("code") != null || intent.getBooleanExtra("directed", false)) {
+ val intent = Intent(this@DetailActivity, HomeActivity::class.java)
+ intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
+ startActivity(intent)
+ }
+ finish()
}
private fun showConfirmationDialog(
diff --git a/android/app/src/main/java/app/priceguard/ui/detail/ProductDetailViewModel.kt b/android/app/src/main/java/app/priceguard/ui/detail/ProductDetailViewModel.kt
index d420144..a51de14 100644
--- a/android/app/src/main/java/app/priceguard/ui/detail/ProductDetailViewModel.kt
+++ b/android/app/src/main/java/app/priceguard/ui/detail/ProductDetailViewModel.kt
@@ -2,10 +2,11 @@ package app.priceguard.ui.detail
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
-import app.priceguard.data.dto.ProductErrorState
-import app.priceguard.data.graph.ProductChartDataset
-import app.priceguard.data.network.ProductRepositoryResult
-import app.priceguard.data.repository.ProductRepository
+import app.priceguard.data.GraphDataConverter
+import app.priceguard.data.graph.ProductChartData
+import app.priceguard.data.repository.RepositoryResult
+import app.priceguard.data.repository.product.ProductErrorState
+import app.priceguard.data.repository.product.ProductRepository
import app.priceguard.materialchart.data.GraphMode
import dagger.hilt.android.lifecycle.HiltViewModel
import java.text.NumberFormat
@@ -20,8 +21,10 @@ import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
@HiltViewModel
-class ProductDetailViewModel @Inject constructor(val productRepository: ProductRepository) :
- ViewModel() {
+class ProductDetailViewModel @Inject constructor(
+ private val productRepository: ProductRepository,
+ private val graphDataConverter: GraphDataConverter
+) : ViewModel() {
data class ProductDetailUIState(
val isTracking: Boolean = false,
@@ -38,8 +41,8 @@ class ProductDetailViewModel @Inject constructor(val productRepository: ProductR
val formattedPrice: String = "",
val formattedTargetPrice: String = "",
val formattedLowestPrice: String = "",
- val chartPeriod: GraphMode = GraphMode.DAY,
- val chartData: ProductChartDataset? = null
+ val graphMode: GraphMode = GraphMode.DAY,
+ val chartData: List = listOf()
)
sealed class ProductDetailEvent {
@@ -53,6 +56,7 @@ class ProductDetailViewModel @Inject constructor(val productRepository: ProductR
}
lateinit var productCode: String
+ private var productGraphData: List = listOf()
private var _event: MutableSharedFlow = MutableSharedFlow()
val event: SharedFlow = _event.asSharedFlow()
@@ -64,12 +68,12 @@ class ProductDetailViewModel @Inject constructor(val productRepository: ProductR
fun deleteProductTracking() {
viewModelScope.launch {
when (val result = productRepository.deleteProduct(productCode)) {
- is ProductRepositoryResult.Success -> {
+ is RepositoryResult.Success -> {
_event.emit(ProductDetailEvent.DeleteSuccess)
}
- is ProductRepositoryResult.Error -> {
- _event.emit(ProductDetailEvent.DeleteFailed(result.productErrorState))
+ is RepositoryResult.Error -> {
+ _event.emit(ProductDetailEvent.DeleteFailed(result.errorState))
}
}
}
@@ -90,7 +94,8 @@ class ProductDetailViewModel @Inject constructor(val productRepository: ProductR
_state.value = _state.value.copy(isRefreshing = false)
when (result) {
- is ProductRepositoryResult.Success -> {
+ is RepositoryResult.Success -> {
+ productGraphData = result.data.priceData
_state.update {
it.copy(
isReady = true,
@@ -112,21 +117,16 @@ class ProductDetailViewModel @Inject constructor(val productRepository: ProductR
)
},
formattedLowestPrice = formatPrice(result.data.lowestPrice),
- chartPeriod = GraphMode.DAY,
- chartData = ProductChartDataset(
- showXAxis = true,
- showYAxis = true,
- isInteractive = true,
- graphMode = GraphMode.DAY,
- data = result.data.priceData,
- gridLines = listOf()
+ chartData = graphDataConverter.packWithEdgeData(
+ result.data.priceData,
+ state.value.graphMode
)
)
}
}
- is ProductRepositoryResult.Error -> {
- when (result.productErrorState) {
+ is RepositoryResult.Error -> {
+ when (result.errorState) {
ProductErrorState.PERMISSION_DENIED -> {
_event.emit(ProductDetailEvent.Logout)
}
@@ -145,17 +145,14 @@ class ProductDetailViewModel @Inject constructor(val productRepository: ProductR
}
fun changePeriod(period: GraphMode) {
+ if (productGraphData.isEmpty()) {
+ return
+ }
+
_state.update {
it.copy(
- chartPeriod = period,
- chartData = ProductChartDataset(
- showXAxis = true,
- showYAxis = true,
- isInteractive = true,
- graphMode = period,
- data = it.chartData?.data ?: listOf(),
- gridLines = listOf()
- )
+ graphMode = period,
+ chartData = graphDataConverter.packWithEdgeData(productGraphData, period)
)
}
}
diff --git a/android/app/src/main/java/app/priceguard/ui/home/HomeActivity.kt b/android/app/src/main/java/app/priceguard/ui/home/HomeActivity.kt
index 7ffeec5..e3c789f 100644
--- a/android/app/src/main/java/app/priceguard/ui/home/HomeActivity.kt
+++ b/android/app/src/main/java/app/priceguard/ui/home/HomeActivity.kt
@@ -1,26 +1,138 @@
package app.priceguard.ui.home
+import android.Manifest
+import android.content.pm.PackageManager
+import android.os.Build
import android.os.Bundle
+import android.util.Log
+import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
+import androidx.core.content.ContextCompat
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.ui.setupWithNavController
+import androidx.work.ExistingPeriodicWorkPolicy
+import androidx.work.PeriodicWorkRequestBuilder
+import androidx.work.WorkManager
+import app.priceguard.R
import app.priceguard.databinding.ActivityHomeBinding
+import app.priceguard.service.UpdateTokenWorker
+import app.priceguard.ui.util.ui.openNotificationSettings
+import com.google.android.gms.common.ConnectionResult
+import com.google.android.gms.common.GoogleApiAvailability
+import com.google.android.material.snackbar.Snackbar
import dagger.hilt.android.AndroidEntryPoint
+import java.util.concurrent.TimeUnit
@AndroidEntryPoint
class HomeActivity : AppCompatActivity() {
private lateinit var binding: ActivityHomeBinding
+ private lateinit var snackbar: Snackbar
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityHomeBinding.inflate(layoutInflater)
setContentView(binding.root)
+
+ enqueueWorker()
+ initSnackBar()
+ checkForGooglePlayServices()
setBottomNavigationBar()
+ askNotificationPermission()
+ }
+
+ override fun onResume() {
+ super.onResume()
+ checkForGooglePlayServices()
+
+ if (ContextCompat.checkSelfPermission(
+ this,
+ Manifest.permission.POST_NOTIFICATIONS
+ ) == PackageManager.PERMISSION_GRANTED
+ ) {
+ dismissSnackbar()
+ } else {
+ showNotificationOffSnackbar()
+ }
+ }
+
+ private fun enqueueWorker() {
+ val saveRequest =
+ PeriodicWorkRequestBuilder(730, TimeUnit.HOURS)
+ .build()
+
+ WorkManager.getInstance(this).enqueueUniquePeriodicWork(
+ "saveRequest",
+ ExistingPeriodicWorkPolicy.UPDATE,
+ saveRequest
+ )
+ }
+
+ private fun checkForGooglePlayServices() {
+ val availability = GoogleApiAvailability().isGooglePlayServicesAvailable(this)
+
+ if (availability != ConnectionResult.SUCCESS) {
+ GoogleApiAvailability().makeGooglePlayServicesAvailable(this)
+ }
+ }
+
+ private fun initSnackBar() {
+ snackbar = Snackbar.make(
+ binding.root,
+ getString(R.string.currently_notification_disabled),
+ Snackbar.LENGTH_INDEFINITE
+ ).setAction(getString(R.string.setting)) {
+ openNotificationSettings()
+ }.setAnchorView(binding.bottomNavigation)
}
private fun setBottomNavigationBar() {
val navController = binding.navHostHome.getFragment().navController
binding.bottomNavigation.setupWithNavController(navController)
}
+
+ private val requestPermissionLauncher = registerForActivityResult(
+ ActivityResultContracts.RequestPermission()
+ ) { isGranted: Boolean ->
+ if (isGranted) {
+ Log.d("NOTIFICATION", "PERMISSION GRANTED")
+ } else {
+ showNotificationOffSnackbar()
+ }
+ }
+
+ private fun askNotificationPermission() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ when {
+ ContextCompat.checkSelfPermission(
+ this,
+ Manifest.permission.POST_NOTIFICATIONS
+ ) == PackageManager.PERMISSION_GRANTED -> {
+ // Allowed
+ Log.d("NOTIFICATION", "PERMISSION GRANTED")
+ }
+
+ shouldShowRequestPermissionRationale(Manifest.permission.POST_NOTIFICATIONS) -> {
+ // Explicitly denied
+ showNotificationOffSnackbar()
+ }
+
+ else -> {
+ // Initial cases
+ requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
+ }
+ }
+ }
+ }
+
+ private fun showNotificationOffSnackbar() {
+ if (snackbar.isShown) return
+ snackbar.show()
+ }
+
+ private fun dismissSnackbar() {
+ if (snackbar.isShown) {
+ snackbar.dismiss()
+ }
+ }
}
diff --git a/android/app/src/main/java/app/priceguard/ui/home/ProductSummaryAdapter.kt b/android/app/src/main/java/app/priceguard/ui/home/ProductSummaryAdapter.kt
index ac02f9d..3590c0a 100644
--- a/android/app/src/main/java/app/priceguard/ui/home/ProductSummaryAdapter.kt
+++ b/android/app/src/main/java/app/priceguard/ui/home/ProductSummaryAdapter.kt
@@ -1,6 +1,5 @@
package app.priceguard.ui.home
-import android.content.Intent
import android.util.TypedValue
import android.view.LayoutInflater
import android.view.View
@@ -13,15 +12,22 @@ import app.priceguard.data.graph.ProductChartData
import app.priceguard.data.graph.ProductChartDataset
import app.priceguard.databinding.ItemProductSummaryBinding
import app.priceguard.materialchart.data.GraphMode
-import app.priceguard.ui.detail.DetailActivity
-class ProductSummaryAdapter :
+class ProductSummaryAdapter(private val productSummaryClickListener: ProductSummaryClickListener) :
ListAdapter(diffUtil) {
+ init {
+ setHasStableIds(true)
+ }
+
+ override fun getItemId(position: Int): Long {
+ return getItem(position).productCode.toLong()
+ }
+
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val binding =
ItemProductSummaryBinding.inflate(LayoutInflater.from(parent.context), parent, false)
- return ViewHolder(binding)
+ return ViewHolder(binding, productSummaryClickListener)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
@@ -29,7 +35,10 @@ class ProductSummaryAdapter :
holder.bind(item)
}
- class ViewHolder(private val binding: ItemProductSummaryBinding) :
+ class ViewHolder(
+ private val binding: ItemProductSummaryBinding,
+ private val productSummaryClickListener: ProductSummaryClickListener
+ ) :
RecyclerView.ViewHolder(binding.root) {
fun bind(item: ProductSummary) {
@@ -53,29 +62,30 @@ class ProductSummaryAdapter :
is ProductSummary.UserProductSummary -> {
tvProductRecommendRank.visibility = View.GONE
msProduct.visibility = View.VISIBLE
+ msProduct.isChecked = item.isAlarmOn
tvProductDiscountPercent.visibility = View.VISIBLE
- setDisCount(item.discountPercent)
- setSwitchListener()
+ setDiscount(item.discountPercent)
+ setSwitchListener(item)
}
}
}
- private fun ItemProductSummaryBinding.setSwitchListener() {
+ private fun ItemProductSummaryBinding.setSwitchListener(item: ProductSummary) {
if (msProduct.isChecked.not()) {
msProduct.setThumbIconResource(R.drawable.ic_notifications_off)
}
msProduct.setOnCheckedChangeListener { _, isChecked ->
+ productSummaryClickListener.onToggle(item.productCode, msProduct.isChecked)
if (isChecked) {
- // TODO: 푸쉬 알람 설정 추가
msProduct.setThumbIconResource(R.drawable.ic_notifications_active)
} else {
- // TODO: 푸쉬 알람 설정 제거
msProduct.setThumbIconResource(R.drawable.ic_notifications_off)
}
}
+ msProduct.contentDescription = msProduct.context.getString(R.string.single_product_notification_toggle, item.title)
}
- private fun ItemProductSummaryBinding.setDisCount(discount: Float) {
+ private fun ItemProductSummaryBinding.setDiscount(discount: Float) {
tvProductDiscountPercent.text =
if (discount > 0) {
tvProductDiscountPercent.context.getString(
@@ -95,19 +105,19 @@ class ProductSummaryAdapter :
true
)
tvProductDiscountPercent.setTextColor(value.data)
+ tvProductDiscountPercent.contentDescription = tvProductDiscountPercent.context.getString(R.string.target_price_delta, tvProductDiscountPercent.text)
}
private fun ItemProductSummaryBinding.setRecommendRank(item: ProductSummary.RecommendedProductSummary) {
tvProductRecommendRank.text = tvProductRecommendRank.context.getString(
R.string.recommand_rank, item.recommendRank
)
+ tvProductRecommendRank.contentDescription = tvProductRecommendRank.context.getString(R.string.current_rank_info, item.recommendRank)
}
private fun ItemProductSummaryBinding.setClickListener(code: String) {
cvProduct.setOnClickListener {
- val intent = Intent(binding.root.context, DetailActivity::class.java)
- intent.putExtra("productCode", code)
- binding.root.context.startActivity(intent)
+ productSummaryClickListener.onClick(code)
}
}
@@ -116,7 +126,9 @@ class ProductSummaryAdapter :
showXAxis = false,
showYAxis = false,
isInteractive = false,
- graphMode = GraphMode.DAY,
+ graphMode = GraphMode.WEEK,
+ xLabel = chGraph.context.getString(R.string.date_text),
+ yLabel = chGraph.context.getString(R.string.price_text),
data = data,
gridLines = listOf()
)
@@ -129,7 +141,7 @@ class ProductSummaryAdapter :
oldItem == newItem
override fun areItemsTheSame(oldItem: ProductSummary, newItem: ProductSummary) =
- oldItem.hashCode() == newItem.hashCode()
+ oldItem.productCode == newItem.productCode
}
}
}
diff --git a/android/app/src/main/java/app/priceguard/ui/home/ProductSummaryClickListener.kt b/android/app/src/main/java/app/priceguard/ui/home/ProductSummaryClickListener.kt
new file mode 100644
index 0000000..871d2a5
--- /dev/null
+++ b/android/app/src/main/java/app/priceguard/ui/home/ProductSummaryClickListener.kt
@@ -0,0 +1,7 @@
+package app.priceguard.ui.home
+
+interface ProductSummaryClickListener {
+ fun onClick(productCode: String)
+
+ fun onToggle(productCode: String, checked: Boolean)
+}
diff --git a/android/app/src/main/java/app/priceguard/ui/home/list/ProductListFragment.kt b/android/app/src/main/java/app/priceguard/ui/home/list/ProductListFragment.kt
index f1e0abb..d10b195 100644
--- a/android/app/src/main/java/app/priceguard/ui/home/list/ProductListFragment.kt
+++ b/android/app/src/main/java/app/priceguard/ui/home/list/ProductListFragment.kt
@@ -8,12 +8,17 @@ import android.view.ViewGroup
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
+import androidx.recyclerview.widget.SimpleItemAnimator
+import androidx.work.WorkManager
import app.priceguard.R
-import app.priceguard.data.dto.ProductErrorState
-import app.priceguard.data.repository.TokenRepository
+import app.priceguard.data.repository.product.ProductErrorState
+import app.priceguard.data.repository.token.TokenRepository
import app.priceguard.databinding.FragmentProductListBinding
+import app.priceguard.service.UpdateAlarmWorker
import app.priceguard.ui.additem.AddItemActivity
+import app.priceguard.ui.detail.DetailActivity
import app.priceguard.ui.home.ProductSummaryAdapter
+import app.priceguard.ui.home.ProductSummaryClickListener
import app.priceguard.ui.util.lifecycle.repeatOnStarted
import app.priceguard.ui.util.ui.disableAppBarRecyclerView
import app.priceguard.ui.util.ui.showConfirmationDialog
@@ -31,6 +36,8 @@ class ProductListFragment : Fragment() {
private val binding get() = _binding!!
private val productListViewModel: ProductListViewModel by viewModels()
+ private var workRequestSet: MutableSet = mutableSetOf()
+
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
@@ -59,7 +66,29 @@ class ProductListFragment : Fragment() {
}
private fun FragmentProductListBinding.initSettingAdapter() {
- val adapter = ProductSummaryAdapter()
+ val animator = rvProductList.itemAnimator
+ if (animator is SimpleItemAnimator) {
+ animator.supportsChangeAnimations = false
+ }
+
+ val listener = object : ProductSummaryClickListener {
+ override fun onClick(productCode: String) {
+ val intent = Intent(context, DetailActivity::class.java)
+ intent.putExtra("productCode", productCode)
+ startActivity(intent)
+ }
+
+ override fun onToggle(productCode: String, checked: Boolean) {
+ productListViewModel.updateProductAlarmToggle(productCode, checked)
+ if (workRequestSet.contains(productCode)) {
+ workRequestSet.remove(productCode)
+ } else {
+ workRequestSet.add(productCode)
+ }
+ }
+ }
+
+ val adapter = ProductSummaryAdapter(listener)
rvProductList.adapter = adapter
this@ProductListFragment.repeatOnStarted {
productListViewModel.productList.collect { list ->
@@ -84,7 +113,7 @@ class ProductListFragment : Fragment() {
}
private fun collectEvent() {
- repeatOnStarted {
+ viewLifecycleOwner.repeatOnStarted {
productListViewModel.events.collect { event ->
when (event) {
ProductErrorState.PERMISSION_DENIED -> {
@@ -116,6 +145,15 @@ class ProductListFragment : Fragment() {
}
}
+ override fun onStop() {
+ super.onStop()
+ workRequestSet.forEach { productCode ->
+ WorkManager.getInstance(requireContext())
+ .enqueue(UpdateAlarmWorker.createWorkRequest(productCode))
+ }
+ workRequestSet.clear()
+ }
+
override fun onDestroyView() {
super.onDestroyView()
_binding = null
diff --git a/android/app/src/main/java/app/priceguard/ui/home/list/ProductListViewModel.kt b/android/app/src/main/java/app/priceguard/ui/home/list/ProductListViewModel.kt
index 194e045..5da268c 100644
--- a/android/app/src/main/java/app/priceguard/ui/home/list/ProductListViewModel.kt
+++ b/android/app/src/main/java/app/priceguard/ui/home/list/ProductListViewModel.kt
@@ -2,9 +2,11 @@ package app.priceguard.ui.home.list
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
-import app.priceguard.data.dto.ProductErrorState
-import app.priceguard.data.network.ProductRepositoryResult
-import app.priceguard.data.repository.ProductRepository
+import app.priceguard.data.GraphDataConverter
+import app.priceguard.data.repository.RepositoryResult
+import app.priceguard.data.repository.product.ProductErrorState
+import app.priceguard.data.repository.product.ProductRepository
+import app.priceguard.materialchart.data.GraphMode
import app.priceguard.ui.home.ProductSummary.UserProductSummary
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
@@ -19,7 +21,8 @@ import kotlinx.coroutines.launch
@HiltViewModel
class ProductListViewModel @Inject constructor(
- private val productRepository: ProductRepository
+ private val productRepository: ProductRepository,
+ private val graphDataConverter: GraphDataConverter
) : ViewModel() {
private var _isRefreshing: MutableStateFlow = MutableStateFlow(false)
@@ -42,28 +45,38 @@ class ProductListViewModel @Inject constructor(
_isRefreshing.value = false
when (result) {
- is ProductRepositoryResult.Success -> {
+ is RepositoryResult.Success -> {
_productList.value = result.data.map { data ->
UserProductSummary(
data.shop,
data.productName,
data.price,
data.productCode,
- data.priceData,
+ graphDataConverter.packWithEdgeData(data.priceData, GraphMode.WEEK),
calculateDiscountRate(data.targetPrice, data.price),
- true
+ data.isAlert
)
}
}
- is ProductRepositoryResult.Error -> {
- _events.emit(result.productErrorState)
+ is RepositoryResult.Error -> {
+ _events.emit(result.errorState)
}
}
}
}
+ fun updateProductAlarmToggle(productCode: String, checked: Boolean) {
+ _productList.value = productList.value.mapIndexed { _, product ->
+ if (product.productCode == productCode) {
+ product.copy(isAlarmOn = checked)
+ } else {
+ product
+ }
+ }
+ }
+
private fun calculateDiscountRate(targetPrice: Int, price: Int): Float {
- return round((price - targetPrice).toFloat() / (if (targetPrice == 0) 1 else targetPrice) * 1000) / 10
+ return round((price - targetPrice).toFloat() / (if (price == 0) 1 else price) * 1000) / 10
}
}
diff --git a/android/app/src/main/java/app/priceguard/ui/home/mypage/MyPageFragment.kt b/android/app/src/main/java/app/priceguard/ui/home/mypage/MyPageFragment.kt
index 2392312..7ee3e6d 100644
--- a/android/app/src/main/java/app/priceguard/ui/home/mypage/MyPageFragment.kt
+++ b/android/app/src/main/java/app/priceguard/ui/home/mypage/MyPageFragment.kt
@@ -8,12 +8,14 @@ import android.view.ViewGroup
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
+import androidx.navigation.fragment.findNavController
import app.priceguard.R
-import app.priceguard.data.repository.TokenRepository
+import app.priceguard.data.repository.token.TokenRepository
import app.priceguard.databinding.FragmentMyPageBinding
import app.priceguard.ui.home.mypage.MyPageViewModel.MyPageEvent
import app.priceguard.ui.intro.IntroActivity
import app.priceguard.ui.util.lifecycle.repeatOnStarted
+import app.priceguard.ui.util.ui.openNotificationSettings
import com.google.android.gms.oss.licenses.OssLicensesMenuActivity
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dagger.hilt.android.AndroidEntryPoint
@@ -62,11 +64,11 @@ class MyPageFragment : Fragment() {
override fun onClick(setting: Setting) {
when (setting) {
Setting.NOTIFICATION -> {
- // TODO: 알람 설정
+ requireContext().openNotificationSettings()
}
Setting.THEME -> {
- // TODO: 테마 설정
+ findNavController().navigate(R.id.action_myPageFragment_to_themeDialogFragment)
}
Setting.LICENSE -> {
diff --git a/android/app/src/main/java/app/priceguard/ui/home/mypage/MyPageViewModel.kt b/android/app/src/main/java/app/priceguard/ui/home/mypage/MyPageViewModel.kt
index c553dd2..3ec3342 100644
--- a/android/app/src/main/java/app/priceguard/ui/home/mypage/MyPageViewModel.kt
+++ b/android/app/src/main/java/app/priceguard/ui/home/mypage/MyPageViewModel.kt
@@ -2,7 +2,7 @@ package app.priceguard.ui.home.mypage
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
-import app.priceguard.data.repository.TokenRepository
+import app.priceguard.data.repository.token.TokenRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
import kotlinx.coroutines.flow.MutableSharedFlow
diff --git a/android/app/src/main/java/app/priceguard/ui/home/recommend/RecommendedProductFragment.kt b/android/app/src/main/java/app/priceguard/ui/home/recommend/RecommendedProductFragment.kt
index fbbc963..bee82f2 100644
--- a/android/app/src/main/java/app/priceguard/ui/home/recommend/RecommendedProductFragment.kt
+++ b/android/app/src/main/java/app/priceguard/ui/home/recommend/RecommendedProductFragment.kt
@@ -1,5 +1,6 @@
package app.priceguard.ui.home.recommend
+import android.content.Intent
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
@@ -8,10 +9,12 @@ import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import app.priceguard.R
-import app.priceguard.data.dto.ProductErrorState
-import app.priceguard.data.repository.TokenRepository
+import app.priceguard.data.repository.product.ProductErrorState
+import app.priceguard.data.repository.token.TokenRepository
import app.priceguard.databinding.FragmentRecommendedProductBinding
+import app.priceguard.ui.detail.DetailActivity
import app.priceguard.ui.home.ProductSummaryAdapter
+import app.priceguard.ui.home.ProductSummaryClickListener
import app.priceguard.ui.util.lifecycle.repeatOnStarted
import app.priceguard.ui.util.ui.disableAppBarRecyclerView
import app.priceguard.ui.util.ui.showConfirmationDialog
@@ -57,7 +60,19 @@ class RecommendedProductFragment : Fragment() {
}
private fun FragmentRecommendedProductBinding.initSettingAdapter() {
- val adapter = ProductSummaryAdapter()
+ val listener = object : ProductSummaryClickListener {
+ override fun onClick(productCode: String) {
+ val intent = Intent(context, DetailActivity::class.java)
+ intent.putExtra("productCode", productCode)
+ startActivity(intent)
+ }
+
+ override fun onToggle(productCode: String, checked: Boolean) {
+ return
+ }
+ }
+
+ val adapter = ProductSummaryAdapter(listener)
rvRecommendedProduct.adapter = adapter
this@RecommendedProductFragment.repeatOnStarted {
recommendedProductViewModel.recommendedProductList.collect { list ->
diff --git a/android/app/src/main/java/app/priceguard/ui/home/recommend/RecommendedProductViewModel.kt b/android/app/src/main/java/app/priceguard/ui/home/recommend/RecommendedProductViewModel.kt
index 49d81b5..a3a9d34 100644
--- a/android/app/src/main/java/app/priceguard/ui/home/recommend/RecommendedProductViewModel.kt
+++ b/android/app/src/main/java/app/priceguard/ui/home/recommend/RecommendedProductViewModel.kt
@@ -2,9 +2,11 @@ package app.priceguard.ui.home.recommend
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
-import app.priceguard.data.dto.ProductErrorState
-import app.priceguard.data.network.ProductRepositoryResult
-import app.priceguard.data.repository.ProductRepository
+import app.priceguard.data.GraphDataConverter
+import app.priceguard.data.repository.RepositoryResult
+import app.priceguard.data.repository.product.ProductErrorState
+import app.priceguard.data.repository.product.ProductRepository
+import app.priceguard.materialchart.data.GraphMode
import app.priceguard.ui.home.ProductSummary.RecommendedProductSummary
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
@@ -18,13 +20,10 @@ import kotlinx.coroutines.launch
@HiltViewModel
class RecommendedProductViewModel @Inject constructor(
- private val productRepository: ProductRepository
+ private val productRepository: ProductRepository,
+ private val graphDataConverter: GraphDataConverter
) : ViewModel() {
- sealed class RecommendedProductEvent {
- data object PermissionDenied : RecommendedProductEvent()
- }
-
private var _isRefreshing = MutableStateFlow(false)
val isRefreshing: StateFlow = _isRefreshing.asStateFlow()
@@ -46,21 +45,21 @@ class RecommendedProductViewModel @Inject constructor(
_isRefreshing.value = false
when (result) {
- is ProductRepositoryResult.Success -> {
+ is RepositoryResult.Success -> {
_recommendedProductList.value = result.data.map { data ->
RecommendedProductSummary(
data.shop,
data.productName,
data.price,
data.productCode,
- data.priceData,
+ graphDataConverter.packWithEdgeData(data.priceData, GraphMode.WEEK),
data.rank
)
}
}
- is ProductRepositoryResult.Error -> {
- _events.emit(result.productErrorState)
+ is RepositoryResult.Error -> {
+ _events.emit(result.errorState)
}
}
}
diff --git a/android/app/src/main/java/app/priceguard/ui/home/theme/ThemeDialogFragment.kt b/android/app/src/main/java/app/priceguard/ui/home/theme/ThemeDialogFragment.kt
new file mode 100644
index 0000000..58e8160
--- /dev/null
+++ b/android/app/src/main/java/app/priceguard/ui/home/theme/ThemeDialogFragment.kt
@@ -0,0 +1,124 @@
+package app.priceguard.ui.home.theme
+
+import android.app.Dialog
+import android.os.Bundle
+import androidx.appcompat.app.AppCompatDelegate
+import androidx.fragment.app.DialogFragment
+import androidx.lifecycle.lifecycleScope
+import app.priceguard.R
+import app.priceguard.data.datastore.ConfigDataSource
+import app.priceguard.databinding.FragmentThemeDialogBinding
+import app.priceguard.ui.PriceGuardApp
+import com.google.android.material.color.DynamicColors
+import com.google.android.material.color.DynamicColorsOptions
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import dagger.hilt.android.AndroidEntryPoint
+import javax.inject.Inject
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+
+@AndroidEntryPoint
+class ThemeDialogFragment : DialogFragment() {
+
+ @Inject
+ lateinit var configDataSource: ConfigDataSource
+
+ override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+ val binding: FragmentThemeDialogBinding =
+ FragmentThemeDialogBinding.inflate(requireActivity().layoutInflater)
+ val view = binding.root
+
+ setCheckedButton(binding)
+
+ return MaterialAlertDialogBuilder(
+ requireActivity(),
+ R.style.ThemeOverlay_App_MaterialAlertDialog
+ ).apply {
+ setView(view)
+ setPositiveButton(R.string.confirm) { _, _ ->
+ val dynamicMode = when (binding.rgDynamicColor.checkedRadioButtonId) {
+ R.id.rb_yes -> {
+ DynamicColors.applyToActivitiesIfAvailable(requireActivity().application)
+ requireActivity().recreate()
+ PriceGuardApp.MODE_DYNAMIC
+ }
+
+ else -> {
+ DynamicColors.applyToActivitiesIfAvailable(
+ requireActivity().application,
+ DynamicColorsOptions.Builder()
+ .setThemeOverlay(R.style.Theme_PriceGuard).build()
+ )
+ requireActivity().recreate()
+ PriceGuardApp.MODE_DYNAMIC_NO
+ }
+ }
+
+ val darkMode = when (binding.rgDarkMode.checkedRadioButtonId) {
+ R.id.rb_system -> {
+ AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
+ PriceGuardApp.MODE_SYSTEM
+ }
+
+ R.id.rb_light -> {
+ AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
+ PriceGuardApp.MODE_LIGHT
+ }
+
+ R.id.rb_dark -> {
+ AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
+ PriceGuardApp.MODE_DARK
+ }
+
+ else -> {
+ PriceGuardApp.MODE_SYSTEM
+ }
+ }
+ saveTheme(dynamicMode, darkMode)
+ dismiss()
+ }
+ }.create()
+ }
+
+ private fun saveTheme(dynamicMode: Int, darkMode: Int) {
+ lifecycleScope.launch(Dispatchers.IO) {
+ configDataSource.saveDynamicMode(dynamicMode)
+ configDataSource.saveDarkMode(darkMode)
+ }
+ }
+
+ private fun setCheckedButton(binding: FragmentThemeDialogBinding) {
+ lifecycleScope.launch {
+ val dynamicColorMode = configDataSource.getDynamicMode()
+ val darkMode = configDataSource.getDarkMode()
+
+ binding.rgDynamicColor.check(
+ when (dynamicColorMode) {
+ PriceGuardApp.MODE_DYNAMIC -> {
+ R.id.rb_yes
+ }
+
+ else -> {
+ R.id.rb_no
+ }
+ }
+ )
+
+ binding.rgDarkMode.check(
+ when (darkMode) {
+ PriceGuardApp.MODE_LIGHT -> {
+ R.id.rb_light
+ }
+
+ PriceGuardApp.MODE_DARK -> {
+ R.id.rb_dark
+ }
+
+ else -> {
+ R.id.rb_system
+ }
+ }
+ )
+ }
+ }
+}
diff --git a/android/app/src/main/java/app/priceguard/ui/login/LoginActivity.kt b/android/app/src/main/java/app/priceguard/ui/login/LoginActivity.kt
index b6a62f3..ed65567 100644
--- a/android/app/src/main/java/app/priceguard/ui/login/LoginActivity.kt
+++ b/android/app/src/main/java/app/priceguard/ui/login/LoginActivity.kt
@@ -2,6 +2,7 @@ package app.priceguard.ui.login
import android.content.Intent
import android.os.Bundle
+import android.widget.Toast
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import app.priceguard.R
@@ -45,16 +46,24 @@ class LoginActivity : AppCompatActivity() {
private fun collectEvent() {
repeatOnStarted {
- loginViewModel.event.collect { eventType ->
- when (eventType) {
+ loginViewModel.event.collect { event ->
+ when (event) {
LoginEvent.LoginStart -> {
(binding.btnLoginLogin as MaterialButton).icon =
getCircularProgressIndicatorDrawable(this@LoginActivity)
}
+ LoginEvent.TokenUpdateError, LoginEvent.FirebaseError -> {
+ Toast.makeText(
+ this@LoginActivity,
+ getString(R.string.push_notification_not_working),
+ Toast.LENGTH_LONG
+ ).show()
+ }
+
else -> {
(binding.btnLoginLogin as MaterialButton).icon = null
- setDialogMessageAndShow(eventType)
+ setDialogMessageAndShow(event)
}
}
}
@@ -74,6 +83,10 @@ class LoginActivity : AppCompatActivity() {
showDialog(getString(R.string.login_fail), getString(R.string.login_fail_message))
}
+ is LoginEvent.UndefinedError -> {
+ showDialog(getString(R.string.login_fail), getString(R.string.undefined_error))
+ }
+
is LoginEvent.LoginInfoSaved -> {
gotoHomeActivity()
}
diff --git a/android/app/src/main/java/app/priceguard/ui/login/LoginViewModel.kt b/android/app/src/main/java/app/priceguard/ui/login/LoginViewModel.kt
index 82069f3..737f9d5 100644
--- a/android/app/src/main/java/app/priceguard/ui/login/LoginViewModel.kt
+++ b/android/app/src/main/java/app/priceguard/ui/login/LoginViewModel.kt
@@ -3,9 +3,10 @@ package app.priceguard.ui.login
import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
-import app.priceguard.data.dto.LoginState
-import app.priceguard.data.repository.TokenRepository
-import app.priceguard.data.repository.UserRepository
+import app.priceguard.data.repository.RepositoryResult
+import app.priceguard.data.repository.auth.AuthErrorState
+import app.priceguard.data.repository.auth.AuthRepository
+import app.priceguard.data.repository.token.TokenRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
import kotlinx.coroutines.flow.MutableSharedFlow
@@ -18,7 +19,7 @@ import kotlinx.coroutines.launch
@HiltViewModel
class LoginViewModel @Inject constructor(
- private val userRepository: UserRepository,
+ private val authRepository: AuthRepository,
private val tokenRepository: TokenRepository
) : ViewModel() {
@@ -32,29 +33,31 @@ class LoginViewModel @Inject constructor(
sealed class LoginEvent {
data object LoginStart : LoginEvent()
data object Invalid : LoginEvent()
- data class LoginSuccess(val accessToken: String, val refreshToken: String) : LoginEvent()
- data class LoginFailure(val status: LoginState) : LoginEvent()
+ data object LoginFailure : LoginEvent()
+ data object UndefinedError : LoginEvent()
data object LoginInfoSaved : LoginEvent()
+ data object FirebaseError : LoginEvent()
+ data object TokenUpdateError : LoginEvent()
}
private val emailPattern =
"""^[\w.+-]+@((?!-)[A-Za-z0-9-]{1,63}(?|\-\[\]\\/]*\d)(?=[A-Za-z\d!@#$%^&*()_+={};:'"~`,.?<>|\-\[\]\\/]*[a-z])(?=[A-Za-z\d!@#$%^&*()_+={};:'"~`,.?<>|\-\[\]\\/]*[A-Z])(?=[A-Za-z\d!@#$%^&*()_+={};:'"~`,.?<>|\-\[\]\\/]*[A-Za-z\d!@#$%^&*()_+={};:'"~`,.?<>|\-\[\]\\/])[A-Za-z\d!@#$%^&*()_+={};:'"~`,.?<>|\-\[\]\\/]{8,16}$""".toRegex()
private var _event = MutableSharedFlow()
val event: SharedFlow = _event.asSharedFlow()
private val _state = MutableStateFlow(State())
var state: StateFlow = _state.asStateFlow()
- fun setID(s: CharSequence, start: Int, before: Int, count: Int) {
+ fun setEmail(s: String) {
if (_state.value.isLoading) return
- _state.value = _state.value.copy(email = s.toString())
+ _state.value = _state.value.copy(email = s)
}
- fun setPassword(s: CharSequence, start: Int, before: Int, count: Int) {
+ fun setPassword(s: String) {
if (_state.value.isLoading) return
- _state.value = _state.value.copy(password = s.toString())
+ _state.value = _state.value.copy(password = s)
}
fun login() {
@@ -73,30 +76,52 @@ class LoginViewModel @Inject constructor(
return@launch
}
- val result = userRepository.login(_state.value.email, _state.value.password)
+ when (val result = authRepository.login(_state.value.email, _state.value.password)) {
+ is RepositoryResult.Success -> {
+ if (result.data.accessToken.isEmpty() || result.data.refreshToken.isEmpty()) {
+ sendLoginEvent(LoginEvent.UndefinedError)
+ setLoading(false)
+ return@launch
+ }
- if (result.accessToken == null || result.refreshToken == null) {
- sendLoginEvent(LoginEvent.LoginFailure(result.loginState))
- setLoading(false)
- return@launch
- }
-
- when (result.loginState) {
- LoginState.SUCCESS -> {
+ val firebaseToken = tokenRepository.getFirebaseToken()
setLoginFinished(true)
- sendLoginEvent(LoginEvent.LoginSuccess(result.accessToken, result.refreshToken))
- saveTokens(result.accessToken, result.refreshToken)
+ saveTokens(result.data.accessToken, result.data.refreshToken)
+ updateFirebaseToken(result.data.accessToken, firebaseToken)
sendLoginEvent(LoginEvent.LoginInfoSaved)
}
- else -> {
- sendLoginEvent(LoginEvent.LoginFailure(result.loginState))
+ is RepositoryResult.Error -> {
+ sendLoginEvent(
+ when (result.errorState) {
+ AuthErrorState.INVALID_REQUEST -> {
+ LoginEvent.LoginFailure
+ }
+
+ else -> {
+ LoginEvent.UndefinedError
+ }
+ }
+ )
}
}
setLoading(false)
}
}
+ private suspend fun updateFirebaseToken(accessToken: String, firebaseToken: String?) {
+ if (firebaseToken != null) {
+ when (tokenRepository.updateFirebaseToken(accessToken, firebaseToken)) {
+ is RepositoryResult.Error -> {
+ sendLoginEvent(LoginEvent.TokenUpdateError)
+ }
+ else -> {}
+ }
+ } else {
+ sendLoginEvent(LoginEvent.FirebaseError)
+ }
+ }
+
private suspend fun saveTokens(accessToken: String, refreshToken: String) {
tokenRepository.storeTokens(accessToken, refreshToken)
}
diff --git a/android/app/src/main/java/app/priceguard/ui/signup/SignupActivity.kt b/android/app/src/main/java/app/priceguard/ui/signup/SignupActivity.kt
index c903d68..2591df6 100644
--- a/android/app/src/main/java/app/priceguard/ui/signup/SignupActivity.kt
+++ b/android/app/src/main/java/app/priceguard/ui/signup/SignupActivity.kt
@@ -2,13 +2,13 @@ package app.priceguard.ui.signup
import android.content.Intent
import android.os.Bundle
+import android.widget.Toast
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.widget.NestedScrollView
import androidx.databinding.DataBindingUtil
import app.priceguard.R
-import app.priceguard.data.dto.SignupState
import app.priceguard.databinding.ActivitySignupBinding
import app.priceguard.ui.home.HomeActivity
import app.priceguard.ui.signup.SignupViewModel.SignupEvent
@@ -71,32 +71,36 @@ class SignupActivity : AppCompatActivity() {
(binding.btnSignupSignup as MaterialButton).icon = circularProgressIndicator
}
- is SignupEvent.SignupSuccess -> {
- (binding.btnSignupSignup as MaterialButton).icon = null
- }
-
- is SignupEvent.SignupFailure -> {
+ else -> {
(binding.btnSignupSignup as MaterialButton).icon = null
- when (event.errorState) {
- SignupState.INVALID_PARAMETER -> {
- showDialog(getString(R.string.error), getString(R.string.invalid_parameter))
+ when (event) {
+ SignupEvent.SignupInfoSaved -> {
+ gotoHomeActivity()
}
- SignupState.DUPLICATE_EMAIL -> {
+ SignupEvent.DuplicatedEmail -> {
showDialog(getString(R.string.error), getString(R.string.duplicate_email))
}
- SignupState.UNDEFINED_ERROR -> {
+ SignupEvent.InvalidRequest -> {
+ showDialog(getString(R.string.error), getString(R.string.invalid_parameter))
+ }
+
+ SignupEvent.UndefinedError -> {
showDialog(getString(R.string.error), getString(R.string.undefined_error))
}
+ SignupEvent.TokenUpdateError, SignupEvent.FirebaseError -> {
+ Toast.makeText(
+ this@SignupActivity,
+ getString(R.string.push_notification_not_working),
+ Toast.LENGTH_LONG
+ ).show()
+ }
+
else -> {}
}
}
-
- SignupEvent.SignupInfoSaved -> {
- gotoHomeActivity()
- }
}
}
@@ -142,7 +146,7 @@ class SignupActivity : AppCompatActivity() {
private fun updateNameTextFieldUI(state: SignupUIState) {
when (state.isNameError) {
true -> {
- binding.tilSignupName.error = getString(R.string.name_required)
+ binding.tilSignupName.error = getString(R.string.invalid_name)
}
else -> {
diff --git a/android/app/src/main/java/app/priceguard/ui/signup/SignupViewModel.kt b/android/app/src/main/java/app/priceguard/ui/signup/SignupViewModel.kt
index 0a32e60..b5e5d10 100644
--- a/android/app/src/main/java/app/priceguard/ui/signup/SignupViewModel.kt
+++ b/android/app/src/main/java/app/priceguard/ui/signup/SignupViewModel.kt
@@ -3,9 +3,10 @@ package app.priceguard.ui.signup
import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
-import app.priceguard.data.dto.SignupState
-import app.priceguard.data.repository.TokenRepository
-import app.priceguard.data.repository.UserRepository
+import app.priceguard.data.repository.RepositoryResult
+import app.priceguard.data.repository.auth.AuthErrorState
+import app.priceguard.data.repository.auth.AuthRepository
+import app.priceguard.data.repository.token.TokenRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
import kotlinx.coroutines.flow.MutableSharedFlow
@@ -18,7 +19,7 @@ import kotlinx.coroutines.launch
@HiltViewModel
class SignupViewModel @Inject constructor(
- private val userRepository: UserRepository,
+ private val authRepository: AuthRepository,
private val tokenRepository: TokenRepository
) : ViewModel() {
@@ -38,15 +39,18 @@ class SignupViewModel @Inject constructor(
sealed class SignupEvent {
data object SignupStart : SignupEvent()
- data class SignupSuccess(val accessToken: String, val refreshToken: String) : SignupEvent()
- data class SignupFailure(val errorState: SignupState) : SignupEvent()
+ data object InvalidRequest : SignupEvent()
+ data object DuplicatedEmail : SignupEvent()
+ data object UndefinedError : SignupEvent()
data object SignupInfoSaved : SignupEvent()
+ data object FirebaseError : SignupEvent()
+ data object TokenUpdateError : SignupEvent()
}
private val emailPattern =
"""^[\w.+-]+@((?!-)[A-Za-z0-9-]{1,63}(?|\-\[\]\\/]*\d)(?=[A-Za-z\d!@#$%^&*()_+={};:'"~`,.?<>|\-\[\]\\/]*[a-z])(?=[A-Za-z\d!@#$%^&*()_+={};:'"~`,.?<>|\-\[\]\\/]*[A-Z])(?=[A-Za-z\d!@#$%^&*()_+={};:'"~`,.?<>|\-\[\]\\/]*[A-Za-z\d!@#$%^&*()_+={};:'"~`,.?<>|\-\[\]\\/])[A-Za-z\d!@#$%^&*()_+={};:'"~`,.?<>|\-\[\]\\/]{8,16}$""".toRegex()
private val _state: MutableStateFlow = MutableStateFlow(SignupUIState())
val state: StateFlow = _state.asStateFlow()
@@ -65,30 +69,41 @@ class SignupViewModel @Inject constructor(
Log.d("ViewModel", "Event Start Sent")
updateSignupStarted(true)
val result =
- userRepository.signUp(_state.value.email, _state.value.name, _state.value.password)
+ authRepository.signUp(_state.value.email, _state.value.name, _state.value.password)
- if (result.accessToken == null || result.refreshToken == null) {
- sendSignupEvent(SignupEvent.SignupFailure(result.signUpState))
- updateSignupStarted(false)
- return@launch
- }
+ when (result) {
+ is RepositoryResult.Success -> {
+ if (result.data.accessToken.isEmpty() || result.data.refreshToken.isEmpty()) {
+ sendSignupEvent(SignupEvent.UndefinedError)
+ updateSignupStarted(false)
+ return@launch
+ }
+
+ val firebaseToken = tokenRepository.getFirebaseToken()
- when (result.signUpState) {
- SignupState.SUCCESS -> {
updateSignupFinished(true)
- sendSignupEvent(
- SignupEvent.SignupSuccess(
- result.accessToken,
- result.refreshToken
- )
- )
- saveTokens(result.accessToken, result.refreshToken)
+ saveTokens(result.data.accessToken, result.data.refreshToken)
+ updateFirebaseToken(result.data.accessToken, firebaseToken)
sendSignupEvent(SignupEvent.SignupInfoSaved)
Log.d("ViewModel", "Event Finish Sent")
}
- else -> {
- sendSignupEvent(SignupEvent.SignupFailure(result.signUpState))
+ is RepositoryResult.Error -> {
+ sendSignupEvent(
+ when (result.errorState) {
+ AuthErrorState.INVALID_REQUEST -> {
+ SignupEvent.InvalidRequest
+ }
+
+ AuthErrorState.DUPLICATED_EMAIL -> {
+ SignupEvent.DuplicatedEmail
+ }
+
+ AuthErrorState.UNDEFINED_ERROR -> {
+ SignupEvent.UndefinedError
+ }
+ }
+ )
}
}
updateSignupStarted(false)
@@ -125,7 +140,7 @@ class SignupViewModel @Inject constructor(
}
private fun isValidName(): Boolean {
- return _state.value.name.isNotBlank()
+ return _state.value.name.isNotBlank() && _state.value.name.length <= 16
}
private fun isValidEmail(): Boolean {
@@ -148,6 +163,20 @@ class SignupViewModel @Inject constructor(
_eventFlow.emit(event)
}
+ private suspend fun updateFirebaseToken(accessToken: String, firebaseToken: String?) {
+ if (firebaseToken != null) {
+ when (tokenRepository.updateFirebaseToken(accessToken, firebaseToken)) {
+ is RepositoryResult.Error -> {
+ sendSignupEvent(SignupEvent.TokenUpdateError)
+ }
+
+ else -> {}
+ }
+ } else {
+ sendSignupEvent(SignupEvent.FirebaseError)
+ }
+ }
+
private fun updateIsSignupReady() {
_state.value =
_state.value.copy(isSignupReady = isValidName() && isValidEmail() && isValidPassword() && isValidRetypePassword())
diff --git a/android/app/src/main/java/app/priceguard/ui/splash/SplashScreenActivity.kt b/android/app/src/main/java/app/priceguard/ui/splash/SplashScreenActivity.kt
index eedf191..e5af970 100644
--- a/android/app/src/main/java/app/priceguard/ui/splash/SplashScreenActivity.kt
+++ b/android/app/src/main/java/app/priceguard/ui/splash/SplashScreenActivity.kt
@@ -10,6 +10,7 @@ import android.view.ViewTreeObserver
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import app.priceguard.databinding.ActivitySplashScreenBinding
+import app.priceguard.ui.detail.DetailActivity
import app.priceguard.ui.home.HomeActivity
import app.priceguard.ui.intro.IntroActivity
import app.priceguard.ui.util.lifecycle.repeatOnCreated
@@ -45,10 +46,15 @@ class SplashScreenActivity : AppCompatActivity() {
splashViewModel.event.collect { event ->
when (event) {
SplashScreenViewModel.SplashEvent.OpenHome -> {
- launchActivityAndExit(
- this@SplashScreenActivity,
- HomeActivity::class.java
- )
+ val productCode = intent.getStringExtra("productCode")
+ if (productCode != null) {
+ receivePushAlarm()
+ } else {
+ launchActivityAndExit(
+ this@SplashScreenActivity,
+ HomeActivity::class.java
+ )
+ }
}
SplashScreenViewModel.SplashEvent.OpenIntro -> {
@@ -67,4 +73,12 @@ class SplashScreenActivity : AppCompatActivity() {
val content: View = findViewById(android.R.id.content)
content.viewTreeObserver.addOnPreDrawListener(onPreDrawListener)
}
+
+ private fun receivePushAlarm() {
+ val productCode = intent.getStringExtra("productCode") ?: return
+ val intent = Intent(this, DetailActivity::class.java)
+ intent.putExtra("productCode", productCode)
+ intent.putExtra("directed", true)
+ startActivity(intent)
+ }
}
diff --git a/android/app/src/main/java/app/priceguard/ui/splash/SplashScreenViewModel.kt b/android/app/src/main/java/app/priceguard/ui/splash/SplashScreenViewModel.kt
index 6c96540..b4b222d 100644
--- a/android/app/src/main/java/app/priceguard/ui/splash/SplashScreenViewModel.kt
+++ b/android/app/src/main/java/app/priceguard/ui/splash/SplashScreenViewModel.kt
@@ -2,8 +2,8 @@ package app.priceguard.ui.splash
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
-import app.priceguard.data.dto.RenewResult
-import app.priceguard.data.repository.TokenRepository
+import app.priceguard.data.repository.RepositoryResult
+import app.priceguard.data.repository.token.TokenRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
import kotlinx.coroutines.flow.MutableSharedFlow
@@ -36,17 +36,18 @@ class SplashScreenViewModel @Inject constructor(tokenRepository: TokenRepository
}
// Renew Token
- val renewResult = tokenRepository.renewTokens(refreshToken)
- if (renewResult == RenewResult.SUCCESS) {
- sendSplashEvent(SplashEvent.OpenHome)
- setAsReady()
- return@launch
- }
+ when (tokenRepository.renewTokens(refreshToken)) {
+ is RepositoryResult.Success -> {
+ sendSplashEvent(SplashEvent.OpenHome)
+ setAsReady()
+ }
- // Renew Failed
- tokenRepository.clearTokens()
- sendSplashEvent(SplashEvent.OpenIntro)
- setAsReady()
+ is RepositoryResult.Error -> {
+ tokenRepository.clearTokens()
+ sendSplashEvent(SplashEvent.OpenIntro)
+ setAsReady()
+ }
+ }
}
}
diff --git a/android/app/src/main/java/app/priceguard/ui/util/ui/NetworkDialog.kt b/android/app/src/main/java/app/priceguard/ui/util/ui/NetworkDialog.kt
index f9eaa17..d76f19c 100644
--- a/android/app/src/main/java/app/priceguard/ui/util/ui/NetworkDialog.kt
+++ b/android/app/src/main/java/app/priceguard/ui/util/ui/NetworkDialog.kt
@@ -3,7 +3,7 @@ package app.priceguard.ui.util.ui
import android.app.Activity
import android.content.Intent
import app.priceguard.R
-import app.priceguard.data.repository.TokenRepository
+import app.priceguard.data.repository.token.TokenRepository
import app.priceguard.ui.login.LoginActivity
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.CoroutineScope
diff --git a/android/app/src/main/java/app/priceguard/ui/util/ui/NotificationSetting.kt b/android/app/src/main/java/app/priceguard/ui/util/ui/NotificationSetting.kt
new file mode 100644
index 0000000..428a4fc
--- /dev/null
+++ b/android/app/src/main/java/app/priceguard/ui/util/ui/NotificationSetting.kt
@@ -0,0 +1,11 @@
+package app.priceguard.ui.util.ui
+
+import android.content.Context
+import android.content.Intent
+import android.provider.Settings
+
+fun Context.openNotificationSettings() {
+ val intent = Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS)
+ .putExtra(Settings.EXTRA_APP_PACKAGE, this.packageName)
+ startActivity(intent)
+}
diff --git a/android/app/src/main/res/anim/from_left_enter.xml b/android/app/src/main/res/anim/from_left_enter.xml
new file mode 100644
index 0000000..5e0a2b4
--- /dev/null
+++ b/android/app/src/main/res/anim/from_left_enter.xml
@@ -0,0 +1,9 @@
+
+
+
+
\ No newline at end of file
diff --git a/android/app/src/main/res/anim/from_right_enter.xml b/android/app/src/main/res/anim/from_right_enter.xml
new file mode 100644
index 0000000..5dc2643
--- /dev/null
+++ b/android/app/src/main/res/anim/from_right_enter.xml
@@ -0,0 +1,9 @@
+
+
+
+
\ No newline at end of file
diff --git a/android/app/src/main/res/anim/to_left_exit.xml b/android/app/src/main/res/anim/to_left_exit.xml
new file mode 100644
index 0000000..ce0463c
--- /dev/null
+++ b/android/app/src/main/res/anim/to_left_exit.xml
@@ -0,0 +1,9 @@
+
+
+
+
\ No newline at end of file
diff --git a/android/app/src/main/res/anim/to_right_exit.xml b/android/app/src/main/res/anim/to_right_exit.xml
new file mode 100644
index 0000000..db8f610
--- /dev/null
+++ b/android/app/src/main/res/anim/to_right_exit.xml
@@ -0,0 +1,9 @@
+
+
+
+
\ No newline at end of file
diff --git a/android/app/src/main/res/drawable-anydpi/ic_priceguard_notification.xml b/android/app/src/main/res/drawable-anydpi/ic_priceguard_notification.xml
new file mode 100644
index 0000000..64e86fe
--- /dev/null
+++ b/android/app/src/main/res/drawable-anydpi/ic_priceguard_notification.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
diff --git a/android/app/src/main/res/drawable-hdpi/ic_priceguard_notification.png b/android/app/src/main/res/drawable-hdpi/ic_priceguard_notification.png
new file mode 100644
index 0000000..214b7a4
Binary files /dev/null and b/android/app/src/main/res/drawable-hdpi/ic_priceguard_notification.png differ
diff --git a/android/app/src/main/res/drawable-mdpi/ic_priceguard_notification.png b/android/app/src/main/res/drawable-mdpi/ic_priceguard_notification.png
new file mode 100644
index 0000000..38fae76
Binary files /dev/null and b/android/app/src/main/res/drawable-mdpi/ic_priceguard_notification.png differ
diff --git a/android/app/src/main/res/drawable-xhdpi/ic_priceguard_notification.png b/android/app/src/main/res/drawable-xhdpi/ic_priceguard_notification.png
new file mode 100644
index 0000000..4c12571
Binary files /dev/null and b/android/app/src/main/res/drawable-xhdpi/ic_priceguard_notification.png differ
diff --git a/android/app/src/main/res/drawable-xxhdpi/ic_priceguard_notification.png b/android/app/src/main/res/drawable-xxhdpi/ic_priceguard_notification.png
new file mode 100644
index 0000000..b830f79
Binary files /dev/null and b/android/app/src/main/res/drawable-xxhdpi/ic_priceguard_notification.png differ
diff --git a/android/app/src/main/res/drawable/ic_share.xml b/android/app/src/main/res/drawable/ic_share.xml
new file mode 100644
index 0000000..fd0bdfa
--- /dev/null
+++ b/android/app/src/main/res/drawable/ic_share.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/android/app/src/main/res/layout-land/activity_login.xml b/android/app/src/main/res/layout-land/activity_login.xml
index 603310b..fba714a 100644
--- a/android/app/src/main/res/layout-land/activity_login.xml
+++ b/android/app/src/main/res/layout-land/activity_login.xml
@@ -81,7 +81,7 @@
android:imeOptions="actionNext"
android:inputType="textEmailAddress"
android:maxLines="1"
- android:onTextChanged="@{viewModel::setID}" />
+ android:onTextChanged="@{(content, s, b, c) -> viewModel.setEmail(content.toString())}" />
@@ -106,7 +106,7 @@
android:imeOptions="actionDone"
android:inputType="textPassword"
android:maxLines="1"
- android:onTextChanged="@{viewModel::setPassword}" />
+ android:onTextChanged="@{(content, s, b, c) -> viewModel.setPassword(content.toString())}" />
diff --git a/android/app/src/main/res/layout/activity_add_item.xml b/android/app/src/main/res/layout/activity_add_item.xml
index 454a702..c4aa1ae 100644
--- a/android/app/src/main/res/layout/activity_add_item.xml
+++ b/android/app/src/main/res/layout/activity_add_item.xml
@@ -5,8 +5,8 @@
+ android:fillViewport="true"
+ app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior">
@@ -73,6 +74,7 @@
android:id="@+id/iv_detail_product"
android:layout_width="match_parent"
android:layout_height="256dp"
+ android:contentDescription="@string/product_image"
app:imageFromUrl="@{viewModel.state.imageUrl}"
app:layout_constraintTop_toTopOf="parent" />
@@ -81,20 +83,34 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
+ android:contentDescription="@string/shopping_mall_logo"
android:src="@drawable/ic_11st_logo"
app:layout_constraintStart_toEndOf="@+id/gl_vertical_start_nested"
app:layout_constraintTop_toBottomOf="@id/iv_detail_product" />
+
+
@@ -104,6 +120,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
+ android:contentDescription="@{@string/current_price_info(@string/price(viewModel.state.formattedPrice))}"
android:text="@{@string/price(viewModel.state.formattedPrice)}"
android:textAppearance="?attr/textAppearanceTitleMedium"
app:layout_constraintStart_toEndOf="@id/gl_vertical_start_nested"
@@ -196,6 +213,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
+ android:importantForAccessibility="no"
android:text="@string/lowest_price"
android:textAppearance="?attr/textAppearanceTitleSmall"
app:layout_constraintStart_toEndOf="@id/gl_vertical_start_nested"
@@ -206,6 +224,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
+ android:contentDescription="@{@string/lowest_price_info(@string/price(viewModel.state.formattedLowestPrice))}"
android:text="@{@string/price(viewModel.state.formattedLowestPrice)}"
android:textAppearance="?attr/textAppearanceTitleSmall"
android:textStyle="bold"
@@ -218,6 +237,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
+ android:importantForAccessibility="no"
android:text="@string/target_price"
android:textAppearance="?attr/textAppearanceTitleSmall"
android:visibility="@{viewModel.state.isTracking ? View.VISIBLE : View.GONE}"
@@ -229,6 +249,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
+ android:contentDescription="@{@string/target_price_info(@string/price(viewModel.state.formattedTargetPrice))}"
android:text="@{@string/price(viewModel.state.formattedTargetPrice)}"
android:textAppearance="?attr/textAppearanceTitleSmall"
android:textStyle="bold"
@@ -253,12 +274,13 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
+ android:contentDescription="@string/edit_period"
app:checkedButton="@+id/btn_period_day"
app:layout_constraintEnd_toStartOf="@id/gl_vertical_end_nested"
app:layout_constraintStart_toEndOf="@id/gl_vertical_start_nested"
app:layout_constraintTop_toBottomOf="@id/tv_detail_price_graph"
- app:singleSelection="true"
- app:selectionRequired="true">
+ app:selectionRequired="true"
+ app:singleSelection="true">
diff --git a/android/app/src/main/res/layout/activity_home.xml b/android/app/src/main/res/layout/activity_home.xml
index 4258ec9..d3717a1 100644
--- a/android/app/src/main/res/layout/activity_home.xml
+++ b/android/app/src/main/res/layout/activity_home.xml
@@ -23,9 +23,10 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/app/src/main/res/layout/activity_login.xml b/android/app/src/main/res/layout/activity_login.xml
index 128e98f..f13864e 100644
--- a/android/app/src/main/res/layout/activity_login.xml
+++ b/android/app/src/main/res/layout/activity_login.xml
@@ -1,7 +1,6 @@
+ xmlns:app="http://schemas.android.com/apk/res-auto">
@@ -75,7 +74,7 @@
android:imeOptions="actionNext"
android:inputType="textEmailAddress"
android:maxLines="1"
- android:onTextChanged="@{viewModel::setID}" />
+ android:onTextChanged="@{(content, s, b, c) -> viewModel.setEmail(content.toString())}" />
@@ -100,7 +99,7 @@
android:imeOptions="actionDone"
android:inputType="textPassword"
android:maxLines="1"
- android:onTextChanged="@{viewModel::setPassword}" />
+ android:onTextChanged="@{(content, s, b, c) -> viewModel.setPassword(content.toString())}" />
diff --git a/android/app/src/main/res/layout/activity_signup.xml b/android/app/src/main/res/layout/activity_signup.xml
index a56fdfb..194ea2e 100644
--- a/android/app/src/main/res/layout/activity_signup.xml
+++ b/android/app/src/main/res/layout/activity_signup.xml
@@ -37,6 +37,7 @@
android:fitsSystemWindows="true"
android:minHeight="?attr/actionBarSize"
app:layout_collapseMode="pin"
+ app:navigationContentDescription="@string/go_back"
app:navigationIcon="@drawable/ic_back"
app:title="@string/sign_up" />
diff --git a/android/app/src/main/res/layout/activity_splash_screen.xml b/android/app/src/main/res/layout/activity_splash_screen.xml
index 376bef4..3024924 100644
--- a/android/app/src/main/res/layout/activity_splash_screen.xml
+++ b/android/app/src/main/res/layout/activity_splash_screen.xml
@@ -13,11 +13,11 @@
android:id="@+id/iv_splash_logo"
android:layout_width="160dp"
android:layout_height="160dp"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintTop_toTopOf="parent"
+ android:src="@drawable/ic_priceguard_original"
app:layout_constraintBottom_toBottomOf="parent"
- android:src="@drawable/ic_priceguard_original" />
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent" />
\ No newline at end of file
diff --git a/android/app/src/main/res/layout/fragment_confirm_item_link.xml b/android/app/src/main/res/layout/fragment_confirm_item_link.xml
index 603db39..77e4672 100644
--- a/android/app/src/main/res/layout/fragment_confirm_item_link.xml
+++ b/android/app/src/main/res/layout/fragment_confirm_item_link.xml
@@ -5,8 +5,8 @@
+ name="viewModel"
+ type="app.priceguard.ui.additem.confirm.ConfirmItemLinkViewModel" />
+ app:layout_constraintTop_toTopOf="parent" />
@@ -92,6 +97,8 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="36dp"
+ android:contentDescription="@string/product_11st"
+ android:visibility="gone"
app:layout_constraintStart_toStartOf="@id/tv_confirm_item_brand"
app:layout_constraintTop_toBottomOf="@id/tv_confirm_item_brand" />
@@ -100,6 +107,8 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
+ android:contentDescription="@{@string/current_price_info(@string/won(@string/comma_number(viewModel.state.price)))}"
+ android:text="@{@string/won(@string/comma_number(viewModel.state.price))}"
android:textAppearance="?attr/textAppearanceTitleMedium"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/iv_confirm_item_shop_logo" />
diff --git a/android/app/src/main/res/layout/fragment_my_page.xml b/android/app/src/main/res/layout/fragment_my_page.xml
index f26b28e..cf89cc9 100644
--- a/android/app/src/main/res/layout/fragment_my_page.xml
+++ b/android/app/src/main/res/layout/fragment_my_page.xml
@@ -74,6 +74,7 @@
android:layout_margin="16dp"
android:background="@drawable/bg_circle"
android:gravity="center"
+ android:importantForAccessibility="no"
android:text="@{viewModel.state.firstName}"
android:textAppearance="?attr/textAppearanceTitleMedium"
android:textColor="?attr/colorSurface"
@@ -86,6 +87,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
+ android:contentDescription="@{@string/my_name_info(viewModel.state.name)}"
android:text="@{viewModel.state.name}"
android:textAppearance="?attr/textAppearanceBodyLarge"
app:layout_constraintBottom_toTopOf="@id/tv_my_page_email"
@@ -96,6 +98,7 @@
android:id="@+id/tv_my_page_email"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
+ android:contentDescription="@{@string/my_email_info(viewModel.state.email)}"
android:text="@{viewModel.state.email}"
android:textAppearance="?attr/textAppearanceBodyMedium"
app:layout_constraintBottom_toBottomOf="parent"
@@ -114,7 +117,8 @@
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@id/view_my_page_background" />
+ app:layout_constraintTop_toBottomOf="@id/view_my_page_background"
+ tools:listitem="@layout/item_my_page_list" />
diff --git a/android/app/src/main/res/layout/fragment_product_list.xml b/android/app/src/main/res/layout/fragment_product_list.xml
index 06af66c..e0d16ea 100644
--- a/android/app/src/main/res/layout/fragment_product_list.xml
+++ b/android/app/src/main/res/layout/fragment_product_list.xml
@@ -54,8 +54,8 @@
android:id="@+id/rv_product_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
- app:layout_behavior="@string/appbar_scrolling_view_behavior"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
+ app:layout_behavior="@string/appbar_scrolling_view_behavior"
tools:listitem="@layout/item_product_summary" />
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ android:fillViewport="true">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/app/src/main/res/layout/fragment_theme_dialog.xml b/android/app/src/main/res/layout/fragment_theme_dialog.xml
new file mode 100644
index 0000000..43b7492
--- /dev/null
+++ b/android/app/src/main/res/layout/fragment_theme_dialog.xml
@@ -0,0 +1,94 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/app/src/main/res/layout/item_my_page_list.xml b/android/app/src/main/res/layout/item_my_page_list.xml
index 800f0f8..08cd775 100644
--- a/android/app/src/main/res/layout/item_my_page_list.xml
+++ b/android/app/src/main/res/layout/item_my_page_list.xml
@@ -19,9 +19,9 @@
android:id="@+id/layout_setting"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:paddingHorizontal="8dp"
android:background="?android:attr/selectableItemBackground"
- android:onClick="@{() -> listener.onClick(settingItemInfo.id)}">
+ android:onClick="@{() -> listener.onClick(settingItemInfo.id)}"
+ android:paddingHorizontal="8dp">
@@ -37,6 +37,7 @@
android:layout_width="wrap_content"
android:layout_height="0dp"
android:layout_marginStart="16dp"
+ android:contentDescription="@string/product_11st"
android:src="@drawable/ic_11st_logo"
app:layout_constraintBottom_toBottomOf="@id/tv_my_page_item_title"
app:layout_constraintStart_toStartOf="parent"
@@ -63,6 +64,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
+ android:contentDescription="@{@string/current_price_info(@string/won(@string/comma_number(summary.price)))}"
android:text="@{@string/won(@string/comma_number(summary.price))}"
android:textAppearance="?attr/textAppearanceBodyMedium"
app:layout_constraintStart_toStartOf="@+id/iv_item_icon"
diff --git a/android/app/src/main/res/menu/list_top_app_bar_menu.xml b/android/app/src/main/res/menu/list_top_app_bar_menu.xml
deleted file mode 100644
index 3de7665..0000000
--- a/android/app/src/main/res/menu/list_top_app_bar_menu.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
\ No newline at end of file
diff --git a/android/app/src/main/res/navigation/nav_graph.xml b/android/app/src/main/res/navigation/nav_graph.xml
index d9a80bb..ef15f8c 100644
--- a/android/app/src/main/res/navigation/nav_graph.xml
+++ b/android/app/src/main/res/navigation/nav_graph.xml
@@ -9,12 +9,28 @@
android:name="app.priceguard.ui.additem.confirm.ConfirmItemLinkFragment"
android:label="ConfirmItemLinkFragment"
tools:layout="@layout/fragment_confirm_item_link">
-
+ app:destination="@id/setTargetPriceFragment"
+ app:enterAnim="@anim/from_right_enter"
+ app:exitAnim="@anim/to_left_exit"
+ app:popEnterAnim="@anim/from_left_enter"
+ app:popExitAnim="@anim/to_right_exit" />
+
+
+
+
+
+ app:destination="@id/confirmItemLinkFragment"
+ app:enterAnim="@anim/from_right_enter"
+ app:exitAnim="@anim/to_left_exit"
+ app:popEnterAnim="@anim/from_left_enter"
+ app:popExitAnim="@anim/to_right_exit" />
diff --git a/android/app/src/main/res/navigation/nav_graph_home.xml b/android/app/src/main/res/navigation/nav_graph_home.xml
index 0daa0e2..53e4a9c 100644
--- a/android/app/src/main/res/navigation/nav_graph_home.xml
+++ b/android/app/src/main/res/navigation/nav_graph_home.xml
@@ -19,7 +19,11 @@
android:id="@+id/myPageFragment"
android:name="app.priceguard.ui.home.mypage.MyPageFragment"
android:label="MyPageFragment"
- tools:layout="@layout/fragment_my_page"/>
+ tools:layout="@layout/fragment_my_page">
+
+
@@ -29,4 +33,9 @@
+
\ No newline at end of file
diff --git a/android/app/src/main/res/values-night/themes.xml b/android/app/src/main/res/values-night/themes.xml
index b70d17c..5854369 100644
--- a/android/app/src/main/res/values-night/themes.xml
+++ b/android/app/src/main/res/values-night/themes.xml
@@ -27,7 +27,6 @@
- @color/md_theme_dark_surfaceVariant
- @color/md_theme_dark_onSurfaceVariant
- @color/md_theme_dark_inversePrimary
- - @color/md_theme_dark_surface
- false
\ No newline at end of file
diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml
index 5eedafc..25d621c 100644
--- a/android/app/src/main/res/values/strings.xml
+++ b/android/app/src/main/res/values/strings.xml
@@ -12,10 +12,10 @@
알림 설정
테마 설정
로그아웃
- 이름은 필수 입력 사항입니다.
+ 이름은 16자 이내로 입력해 주세요.
유효하지 않은 이메일입니다.
유효한 이메일입니다.
- 비밀번호는 영문 대소문자와 특수문자가 포함된 8~16자 사이어야 합니다.
+ 비밀번호는 영문 대소문자, 숫자, 특수문자가 포함된 8~16자 사이어야 합니다.
적합한 비밀번호입니다.
비밀번호가 일치하지 않습니다.
비밀번호가 일치합니다.
@@ -34,7 +34,7 @@
인기 상품
상품 목록
refresh
- add product
+ 상품 추가
#%1$d
뒤로
다음
@@ -42,7 +42,7 @@
선택하신 상품을\n확인해 주세요
알림받을 목표가격을\n설정해 주세요
링크를 입력하세요.
- 상품 링크는 어떻게 얻나요?
+ \uD83D\uDD3A상품 링크는 어떻게 얻나요?
에 알람을 받고 싶습니다.
쇼핑몰로 이동
트래킹 목록에 추가
@@ -57,7 +57,7 @@
분기
로그아웃 확인
정말로 로그아웃 하시겠습니까?
- 네
+ 예
아니오
오픈소스 라이선스
현재 가격의 %d%%
@@ -96,4 +96,38 @@
인기 상품 조회 실패
내 상품 목록 조회 실패
트래킹 제거 실패
+ 상품 이미지
+ 쇼핑몰 로고
+ 상품 공유하기
+ https://share.priceguard.app/11st?code=%1$s
+ [PriceGuard]\n이 상품 가격 같이 확인해요!\n\n%1$s
+ 상품 가격 알림
+ 현재 알림 설정이 비활성화되어 있습니다.
+ 설정
+ 다이나믹 컬러 사용
+ 다크 모드 설정
+ 시스템 설정 사용
+ 라이트 모드
+ 다크 모드
+ 가격
+ 시간
+ 푸시 알림이 정상작동하지 않을 수 있습니다.
+ 그래프 주기 설정
+ 뒤로가기
+ 상품 %s 에 대한 개별 알림 토글
+ 현재가격은 설정한 목표가격의 %s
+ 현재가격 %s
+ 인기 순위 %d위
+ 11번가 상품
+ 내 이름, %s
+ 내 E-mail, %s
+ 링크 확인 버튼
+ 선택한 상품의 이미지
+ 상품명 %s
+ 쇼핑몰 이름, %s
+ 목표가격 입력란
+ 목표가격 %1$s, %2$s으로 설정됨
+ 현재 가격의 80%
+ 역대 최저가 %s
+ 목표 가격 %s 으로 설정됨
\ No newline at end of file
diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml
index 4d3fd41..d858f1d 100644
--- a/android/app/src/main/res/values/styles.xml
+++ b/android/app/src/main/res/values/styles.xml
@@ -24,6 +24,10 @@
- ?attr/colorOnSurface
+
+
diff --git a/android/app/src/main/res/values/themes.xml b/android/app/src/main/res/values/themes.xml
index 2f24fb9..6f20f25 100644
--- a/android/app/src/main/res/values/themes.xml
+++ b/android/app/src/main/res/values/themes.xml
@@ -31,7 +31,9 @@
- true
-
+
diff --git a/android/materialchart b/android/materialchart
deleted file mode 160000
index cb75a5f..0000000
--- a/android/materialchart
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit cb75a5f2c8406ce5d38850016433c8d408140522
diff --git a/android/release_notes.txt b/android/release_notes.txt
index ba49687..fce02d7 100644
--- a/android/release_notes.txt
+++ b/android/release_notes.txt
@@ -1,12 +1,14 @@
-December 1, 2023
-v0.2.0
+December 8, 2023
+v0.3.0
🚀 Feature Updates
-- 새로고침 레이아웃 추가
-- 가격 수정 기능 구현
-- 상세페이지 트래킹 목록에 추가 기능 구현
-- 서브모듈 연결
-- 차트 ui 추가
+- 그래프 데이터 변환 버그 수정
+- 다이나믹 테마 지원
+- 상품 정보로까지 딥링크 지원
+- 접근성 개선 (Talkback 전부 지원)
+- 푸시 알림 지원
-차트 라이브러리 주소 : https://github.com/Taewan-P/material-android-chart
+🚧Notice
+
+현재 알림 설정 토글에 버그가 있습니다. 빠른 시일 내에 수정하겠습니다.
diff --git a/android/settings.gradle b/android/settings.gradle
index fb210b7..4f178b0 100644
--- a/android/settings.gradle
+++ b/android/settings.gradle
@@ -13,5 +13,4 @@ dependencyResolutionManagement {
}
}
rootProject.name = "PriceGuard"
-include ':app'
-include ':materialchart'
+include ':app'
\ No newline at end of file
diff --git a/backend/package-lock.json b/backend/package-lock.json
index 139055b..155a2a7 100644
--- a/backend/package-lock.json
+++ b/backend/package-lock.json
@@ -19,12 +19,15 @@
"@nestjs/schedule": "^4.0.0",
"@nestjs/swagger": "^7.1.16",
"@nestjs/typeorm": "^10.0.1",
+ "@songkeys/nestjs-redis": "^10.0.0",
"@types/passport-jwt": "^3.0.13",
"axios": "^1.6.2",
"bcrypt": "^5.1.1",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.0",
+ "firebase-admin": "^11.11.1",
"iconv-lite": "^0.6.3",
+ "ioredis": "^5.3.2",
"mongoose": "^8.0.1",
"mysql2": "^3.6.3",
"nest-winston": "^1.9.4",
@@ -42,13 +45,15 @@
"devDependencies": {
"@nestjs/cli": "^10.0.0",
"@nestjs/schematics": "^10.0.0",
- "@nestjs/testing": "^10.0.0",
+ "@nestjs/testing": "^10.2.10",
"@types/bcrypt": "^5.0.2",
+ "@types/dotenv": "^8.2.0",
"@types/express": "^4.17.17",
"@types/jest": "^29.5.2",
"@types/node": "^20.3.1",
"@types/passport-local": "^1.0.38",
- "@types/supertest": "^2.0.12",
+ "@types/supertest": "^2.0.16",
+ "@types/winston": "^2.4.4",
"@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0",
"cross-env": "^7.0.3",
@@ -115,6 +120,28 @@
}
}
},
+ "node_modules/@angular-devkit/core/node_modules/ajv": {
+ "version": "8.12.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz",
+ "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==",
+ "dev": true,
+ "dependencies": {
+ "fast-deep-equal": "^3.1.1",
+ "json-schema-traverse": "^1.0.0",
+ "require-from-string": "^2.0.2",
+ "uri-js": "^4.2.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/@angular-devkit/core/node_modules/json-schema-traverse": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
+ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
+ "dev": true
+ },
"node_modules/@angular-devkit/schematics": {
"version": "16.2.8",
"resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-16.2.8.tgz",
@@ -199,12 +226,12 @@
}
},
"node_modules/@babel/code-frame": {
- "version": "7.22.13",
- "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz",
- "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==",
+ "version": "7.23.5",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz",
+ "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==",
"dev": true,
"dependencies": {
- "@babel/highlight": "^7.22.13",
+ "@babel/highlight": "^7.23.4",
"chalk": "^2.4.2"
},
"engines": {
@@ -283,30 +310,30 @@
}
},
"node_modules/@babel/compat-data": {
- "version": "7.23.2",
- "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.2.tgz",
- "integrity": "sha512-0S9TQMmDHlqAZ2ITT95irXKfxN9bncq8ZCoJhun3nHL/lLUxd2NKBJYoNGWH7S0hz6fRQwWlAWn/ILM0C70KZQ==",
+ "version": "7.23.5",
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.5.tgz",
+ "integrity": "sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw==",
"dev": true,
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/core": {
- "version": "7.23.2",
- "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.2.tgz",
- "integrity": "sha512-n7s51eWdaWZ3vGT2tD4T7J6eJs3QoBXydv7vkUM06Bf1cbVD2Kc2UrkzhiQwobfV7NwOnQXYL7UBJ5VPU+RGoQ==",
+ "version": "7.23.5",
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.5.tgz",
+ "integrity": "sha512-Cwc2XjUrG4ilcfOw4wBAK+enbdgwAcAJCfGUItPBKR7Mjw4aEfAFYrLxeRp4jWgtNIKn3n2AlBOfwwafl+42/g==",
"dev": true,
"dependencies": {
"@ampproject/remapping": "^2.2.0",
- "@babel/code-frame": "^7.22.13",
- "@babel/generator": "^7.23.0",
+ "@babel/code-frame": "^7.23.5",
+ "@babel/generator": "^7.23.5",
"@babel/helper-compilation-targets": "^7.22.15",
- "@babel/helper-module-transforms": "^7.23.0",
- "@babel/helpers": "^7.23.2",
- "@babel/parser": "^7.23.0",
+ "@babel/helper-module-transforms": "^7.23.3",
+ "@babel/helpers": "^7.23.5",
+ "@babel/parser": "^7.23.5",
"@babel/template": "^7.22.15",
- "@babel/traverse": "^7.23.2",
- "@babel/types": "^7.23.0",
+ "@babel/traverse": "^7.23.5",
+ "@babel/types": "^7.23.5",
"convert-source-map": "^2.0.0",
"debug": "^4.1.0",
"gensync": "^1.0.0-beta.2",
@@ -331,12 +358,12 @@
}
},
"node_modules/@babel/generator": {
- "version": "7.23.0",
- "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz",
- "integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==",
+ "version": "7.23.5",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.5.tgz",
+ "integrity": "sha512-BPssCHrBD+0YrxviOa3QzpqwhNIXKEtOa2jQrm4FlmkC2apYgRnQcmPWiGZDlGxiNtltnUFolMe8497Esry+jA==",
"dev": true,
"dependencies": {
- "@babel/types": "^7.23.0",
+ "@babel/types": "^7.23.5",
"@jridgewell/gen-mapping": "^0.3.2",
"@jridgewell/trace-mapping": "^0.3.17",
"jsesc": "^2.5.1"
@@ -361,6 +388,15 @@
"node": ">=6.9.0"
}
},
+ "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
+ "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
+ "dev": true,
+ "dependencies": {
+ "yallist": "^3.0.2"
+ }
+ },
"node_modules/@babel/helper-compilation-targets/node_modules/semver": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
@@ -370,6 +406,12 @@
"semver": "bin/semver.js"
}
},
+ "node_modules/@babel/helper-compilation-targets/node_modules/yallist": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
+ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
+ "dev": true
+ },
"node_modules/@babel/helper-environment-visitor": {
"version": "7.22.20",
"resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz",
@@ -417,9 +459,9 @@
}
},
"node_modules/@babel/helper-module-transforms": {
- "version": "7.23.0",
- "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.0.tgz",
- "integrity": "sha512-WhDWw1tdrlT0gMgUJSlX0IQvoO1eN279zrAUbVB+KpV2c3Tylz8+GnKOLllCS6Z/iZQEyVYxhZVUdPTqs2YYPw==",
+ "version": "7.23.3",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.3.tgz",
+ "integrity": "sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==",
"dev": true,
"dependencies": {
"@babel/helper-environment-visitor": "^7.22.20",
@@ -469,9 +511,9 @@
}
},
"node_modules/@babel/helper-string-parser": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz",
- "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==",
+ "version": "7.23.4",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz",
+ "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==",
"dev": true,
"engines": {
"node": ">=6.9.0"
@@ -487,32 +529,32 @@
}
},
"node_modules/@babel/helper-validator-option": {
- "version": "7.22.15",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.22.15.tgz",
- "integrity": "sha512-bMn7RmyFjY/mdECUbgn9eoSY4vqvacUnS9i9vGAGttgFWesO6B4CYWA7XlpbWgBt71iv/hfbPlynohStqnu5hA==",
+ "version": "7.23.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz",
+ "integrity": "sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==",
"dev": true,
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helpers": {
- "version": "7.23.2",
- "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.2.tgz",
- "integrity": "sha512-lzchcp8SjTSVe/fPmLwtWVBFC7+Tbn8LGHDVfDp9JGxpAY5opSaEFgt8UQvrnECWOTdji2mOWMz1rOhkHscmGQ==",
+ "version": "7.23.5",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.5.tgz",
+ "integrity": "sha512-oO7us8FzTEsG3U6ag9MfdF1iA/7Z6dz+MtFhifZk8C8o453rGJFFWUP1t+ULM9TUIAzC9uxXEiXjOiVMyd7QPg==",
"dev": true,
"dependencies": {
"@babel/template": "^7.22.15",
- "@babel/traverse": "^7.23.2",
- "@babel/types": "^7.23.0"
+ "@babel/traverse": "^7.23.5",
+ "@babel/types": "^7.23.5"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/highlight": {
- "version": "7.22.20",
- "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz",
- "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==",
+ "version": "7.23.4",
+ "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz",
+ "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==",
"dev": true,
"dependencies": {
"@babel/helper-validator-identifier": "^7.22.20",
@@ -595,10 +637,10 @@
}
},
"node_modules/@babel/parser": {
- "version": "7.23.0",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz",
- "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==",
- "dev": true,
+ "version": "7.23.5",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.5.tgz",
+ "integrity": "sha512-hOOqoiNXrmGdFbhgCzu6GiURxUgM27Xwd/aPuu8RfHEZPBzL1Z54okAHAQjXfcQNwvrlkAmAp4SlRTZ45vlthQ==",
+ "devOptional": true,
"bin": {
"parser": "bin/babel-parser.js"
},
@@ -667,9 +709,9 @@
}
},
"node_modules/@babel/plugin-syntax-jsx": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.22.5.tgz",
- "integrity": "sha512-gvyP4hZrgrs/wWMaocvxZ44Hw0b3W8Pe+cMxc8V1ULQ07oh8VNbIRaoD1LRZVTvD+0nieDKjfgKg89sD7rrKrg==",
+ "version": "7.23.3",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.23.3.tgz",
+ "integrity": "sha512-EB2MELswq55OHUoRZLGg/zC7QWUKfNLpE57m/S2yr1uEneIgsTgrSzXP3NXEsMkVn76OlaVVnzN+ugObuYGwhg==",
"dev": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5"
@@ -769,9 +811,9 @@
}
},
"node_modules/@babel/plugin-syntax-typescript": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.22.5.tgz",
- "integrity": "sha512-1mS2o03i7t1c6VzH6fdQ3OA8tcEIxwG18zIPRp+UY1Ihv6W+XZzBCVxExF9upussPXJ0xE9XRHwMoNs1ep/nRQ==",
+ "version": "7.23.3",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.23.3.tgz",
+ "integrity": "sha512-9EiNjVJOMwCO+43TqoTrgQ8jMwcAd0sWyXi9RPfIsLTj4R2MADDDQXELhffaUx/uJv2AYcxBgPwH6j4TIA4ytQ==",
"dev": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5"
@@ -784,9 +826,9 @@
}
},
"node_modules/@babel/runtime": {
- "version": "7.23.2",
- "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.2.tgz",
- "integrity": "sha512-mM8eg4yl5D6i3lu2QKPuPH4FArvJ8KhTofbE7jwMUv9KX5mBvwPAqnV3MlyBNqdp9RyRKP6Yck8TrfYrPvX3bg==",
+ "version": "7.23.5",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.5.tgz",
+ "integrity": "sha512-NdUTHcPe4C99WxPub+K9l9tK5/lV4UXIoaHSYgzco9BCyjKAAwzdBI+wWtYqHt7LJdbo74ZjRPJgzVweq1sz0w==",
"dependencies": {
"regenerator-runtime": "^0.14.0"
},
@@ -809,19 +851,19 @@
}
},
"node_modules/@babel/traverse": {
- "version": "7.23.2",
- "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz",
- "integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==",
+ "version": "7.23.5",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.5.tgz",
+ "integrity": "sha512-czx7Xy5a6sapWWRx61m1Ke1Ra4vczu1mCTtJam5zRTBOonfdJ+S/B6HYmGYu3fJtr8GGET3si6IhgWVBhJ/m8w==",
"dev": true,
"dependencies": {
- "@babel/code-frame": "^7.22.13",
- "@babel/generator": "^7.23.0",
+ "@babel/code-frame": "^7.23.5",
+ "@babel/generator": "^7.23.5",
"@babel/helper-environment-visitor": "^7.22.20",
"@babel/helper-function-name": "^7.23.0",
"@babel/helper-hoist-variables": "^7.22.5",
"@babel/helper-split-export-declaration": "^7.22.6",
- "@babel/parser": "^7.23.0",
- "@babel/types": "^7.23.0",
+ "@babel/parser": "^7.23.5",
+ "@babel/types": "^7.23.5",
"debug": "^4.1.0",
"globals": "^11.1.0"
},
@@ -839,12 +881,12 @@
}
},
"node_modules/@babel/types": {
- "version": "7.23.0",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz",
- "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==",
+ "version": "7.23.5",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.5.tgz",
+ "integrity": "sha512-ON5kSOJwVO6xXVRTvOI0eOnWe7VdUcIpsovGo9U/Br4Ie4UVFQTboO2cYnDhAGU6Fp+UxSiT+pMft0SMHfuq6w==",
"dev": true,
"dependencies": {
- "@babel/helper-string-parser": "^7.22.5",
+ "@babel/helper-string-parser": "^7.23.4",
"@babel/helper-validator-identifier": "^7.22.20",
"to-fast-properties": "^2.0.0"
},
@@ -925,9 +967,9 @@
}
},
"node_modules/@eslint/eslintrc": {
- "version": "2.1.3",
- "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.3.tgz",
- "integrity": "sha512-yZzuIG+jnVu6hNSzFEN07e8BxF3uAzYtQb6uDkaYZLo6oYZDCq454c5kB8zxnzfCYyP4MIuyBn10L0DqwujTmA==",
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz",
+ "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==",
"dev": true,
"dependencies": {
"ajv": "^6.12.4",
@@ -947,35 +989,209 @@
"url": "https://opencollective.com/eslint"
}
},
- "node_modules/@eslint/eslintrc/node_modules/ajv": {
- "version": "6.12.6",
- "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
- "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+ "node_modules/@eslint/js": {
+ "version": "8.55.0",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.55.0.tgz",
+ "integrity": "sha512-qQfo2mxH5yVom1kacMtZZJFVdW+E70mqHMJvVg6WTLo+VBuQJ4TojZlfWBjK0ve5BdEeNAVxOsl/nvNMpJOaJA==",
"dev": true,
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ }
+ },
+ "node_modules/@fastify/busboy": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-1.2.1.tgz",
+ "integrity": "sha512-7PQA7EH43S0CxcOa9OeAnaeA0oQ+e/DHNPZwSQM9CQHW76jle5+OvLdibRp/Aafs9KXbLhxyjOTkRjWUbQEd3Q==",
+ "dependencies": {
+ "text-decoding": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/@firebase/app-types": {
+ "version": "0.9.0",
+ "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.9.0.tgz",
+ "integrity": "sha512-AeweANOIo0Mb8GiYm3xhTEBVCmPwTYAu9Hcd2qSkLuga/6+j9b1Jskl5bpiSQWy9eJ/j5pavxj6eYogmnuzm+Q=="
+ },
+ "node_modules/@firebase/auth-interop-types": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.2.1.tgz",
+ "integrity": "sha512-VOaGzKp65MY6P5FI84TfYKBXEPi6LmOCSMMzys6o2BN2LOsqy7pCuZCup7NYnfbk5OkkQKzvIfHOzTm0UDpkyg=="
+ },
+ "node_modules/@firebase/component": {
+ "version": "0.6.4",
+ "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.6.4.tgz",
+ "integrity": "sha512-rLMyrXuO9jcAUCaQXCMjCMUsWrba5fzHlNK24xz5j2W6A/SRmK8mZJ/hn7V0fViLbxC0lPMtrK1eYzk6Fg03jA==",
+ "dependencies": {
+ "@firebase/util": "1.9.3",
+ "tslib": "^2.1.0"
+ }
+ },
+ "node_modules/@firebase/database": {
+ "version": "0.14.4",
+ "resolved": "https://registry.npmjs.org/@firebase/database/-/database-0.14.4.tgz",
+ "integrity": "sha512-+Ea/IKGwh42jwdjCyzTmeZeLM3oy1h0mFPsTy6OqCWzcu/KFqRAr5Tt1HRCOBlNOdbh84JPZC47WLU18n2VbxQ==",
+ "dependencies": {
+ "@firebase/auth-interop-types": "0.2.1",
+ "@firebase/component": "0.6.4",
+ "@firebase/logger": "0.4.0",
+ "@firebase/util": "1.9.3",
+ "faye-websocket": "0.11.4",
+ "tslib": "^2.1.0"
+ }
+ },
+ "node_modules/@firebase/database-compat": {
+ "version": "0.3.4",
+ "resolved": "https://registry.npmjs.org/@firebase/database-compat/-/database-compat-0.3.4.tgz",
+ "integrity": "sha512-kuAW+l+sLMUKBThnvxvUZ+Q1ZrF/vFJ58iUY9kAcbX48U03nVzIF6Tmkf0p3WVQwMqiXguSgtOPIB6ZCeF+5Gg==",
+ "dependencies": {
+ "@firebase/component": "0.6.4",
+ "@firebase/database": "0.14.4",
+ "@firebase/database-types": "0.10.4",
+ "@firebase/logger": "0.4.0",
+ "@firebase/util": "1.9.3",
+ "tslib": "^2.1.0"
+ }
+ },
+ "node_modules/@firebase/database-types": {
+ "version": "0.10.4",
+ "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-0.10.4.tgz",
+ "integrity": "sha512-dPySn0vJ/89ZeBac70T+2tWWPiJXWbmRygYv0smT5TfE3hDrQ09eKMF3Y+vMlTdrMWq7mUdYW5REWPSGH4kAZQ==",
+ "dependencies": {
+ "@firebase/app-types": "0.9.0",
+ "@firebase/util": "1.9.3"
+ }
+ },
+ "node_modules/@firebase/logger": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.4.0.tgz",
+ "integrity": "sha512-eRKSeykumZ5+cJPdxxJRgAC3G5NknY2GwEbKfymdnXtnT0Ucm4pspfR6GT4MUQEDuJwRVbVcSx85kgJulMoFFA==",
+ "dependencies": {
+ "tslib": "^2.1.0"
+ }
+ },
+ "node_modules/@firebase/util": {
+ "version": "1.9.3",
+ "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.9.3.tgz",
+ "integrity": "sha512-DY02CRhOZwpzO36fHpuVysz6JZrscPiBXD0fXp6qSrL9oNOx5KWICKdR95C0lSITzxp0TZosVyHqzatE8JbcjA==",
+ "dependencies": {
+ "tslib": "^2.1.0"
+ }
+ },
+ "node_modules/@google-cloud/firestore": {
+ "version": "6.8.0",
+ "resolved": "https://registry.npmjs.org/@google-cloud/firestore/-/firestore-6.8.0.tgz",
+ "integrity": "sha512-JRpk06SmZXLGz0pNx1x7yU3YhkUXheKgH5hbDZ4kMsdhtfV5qPLJLRI4wv69K0cZorIk+zTMOwptue7hizo0eA==",
+ "optional": true,
"dependencies": {
"fast-deep-equal": "^3.1.1",
- "fast-json-stable-stringify": "^2.0.0",
- "json-schema-traverse": "^0.4.1",
- "uri-js": "^4.2.2"
+ "functional-red-black-tree": "^1.0.1",
+ "google-gax": "^3.5.7",
+ "protobufjs": "^7.2.5"
},
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/epoberezkin"
+ "engines": {
+ "node": ">=12.0.0"
}
},
- "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": {
- "version": "0.4.1",
- "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
- "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
- "dev": true
+ "node_modules/@google-cloud/paginator": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/@google-cloud/paginator/-/paginator-3.0.7.tgz",
+ "integrity": "sha512-jJNutk0arIQhmpUUQJPJErsojqo834KcyB6X7a1mxuic8i1tKXxde8E69IZxNZawRIlZdIK2QY4WALvlK5MzYQ==",
+ "optional": true,
+ "dependencies": {
+ "arrify": "^2.0.0",
+ "extend": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=10"
+ }
},
- "node_modules/@eslint/js": {
- "version": "8.53.0",
- "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.53.0.tgz",
- "integrity": "sha512-Kn7K8dx/5U6+cT1yEhpX1w4PCSg0M+XyRILPgvwcEBjerFWCwQj5sbr3/VmxqV0JGHCBCzyd6LxypEuehypY1w==",
- "dev": true,
+ "node_modules/@google-cloud/projectify": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-3.0.0.tgz",
+ "integrity": "sha512-HRkZsNmjScY6Li8/kb70wjGlDDyLkVk3KvoEo9uIoxSjYLJasGiCch9+PqRVDOCGUFvEIqyogl+BeqILL4OJHA==",
+ "optional": true,
"engines": {
- "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ "node": ">=12.0.0"
+ }
+ },
+ "node_modules/@google-cloud/promisify": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-3.0.1.tgz",
+ "integrity": "sha512-z1CjRjtQyBOYL+5Qr9DdYIfrdLBe746jRTYfaYU6MeXkqp7UfYs/jX16lFFVzZ7PGEJvqZNqYUEtb1mvDww4pA==",
+ "optional": true,
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@google-cloud/storage": {
+ "version": "6.12.0",
+ "resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-6.12.0.tgz",
+ "integrity": "sha512-78nNAY7iiZ4O/BouWMWTD/oSF2YtYgYB3GZirn0To6eBOugjXVoK+GXgUXOl+HlqbAOyHxAVXOlsj3snfbQ1dw==",
+ "optional": true,
+ "dependencies": {
+ "@google-cloud/paginator": "^3.0.7",
+ "@google-cloud/projectify": "^3.0.0",
+ "@google-cloud/promisify": "^3.0.0",
+ "abort-controller": "^3.0.0",
+ "async-retry": "^1.3.3",
+ "compressible": "^2.0.12",
+ "duplexify": "^4.0.0",
+ "ent": "^2.2.0",
+ "extend": "^3.0.2",
+ "fast-xml-parser": "^4.2.2",
+ "gaxios": "^5.0.0",
+ "google-auth-library": "^8.0.1",
+ "mime": "^3.0.0",
+ "mime-types": "^2.0.8",
+ "p-limit": "^3.0.1",
+ "retry-request": "^5.0.0",
+ "teeny-request": "^8.0.0",
+ "uuid": "^8.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@google-cloud/storage/node_modules/uuid": {
+ "version": "8.3.2",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
+ "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
+ "optional": true,
+ "bin": {
+ "uuid": "dist/bin/uuid"
+ }
+ },
+ "node_modules/@grpc/grpc-js": {
+ "version": "1.8.21",
+ "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.8.21.tgz",
+ "integrity": "sha512-KeyQeZpxeEBSqFVTi3q2K7PiPXmgBfECc4updA1ejCLjYmoAlvvM3ZMp5ztTDUCUQmoY3CpDxvchjO1+rFkoHg==",
+ "optional": true,
+ "dependencies": {
+ "@grpc/proto-loader": "^0.7.0",
+ "@types/node": ">=12.12.47"
+ },
+ "engines": {
+ "node": "^8.13.0 || >=10.10.0"
+ }
+ },
+ "node_modules/@grpc/proto-loader": {
+ "version": "0.7.10",
+ "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.10.tgz",
+ "integrity": "sha512-CAqDfoaQ8ykFd9zqBDn4k6iWT9loLAlc2ETmDFS9JCD70gDcnA4L3AFEo2iV7KyAtAAHFW9ftq1Fz+Vsgq80RQ==",
+ "optional": true,
+ "dependencies": {
+ "lodash.camelcase": "^4.3.0",
+ "long": "^5.0.0",
+ "protobufjs": "^7.2.4",
+ "yargs": "^17.7.2"
+ },
+ "bin": {
+ "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js"
+ },
+ "engines": {
+ "node": ">=6"
}
},
"node_modules/@humanwhocodes/config-array": {
@@ -1011,6 +1227,11 @@
"integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==",
"dev": true
},
+ "node_modules/@ioredis/commands": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz",
+ "integrity": "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg=="
+ },
"node_modules/@isaacs/cliui": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
@@ -1571,6 +1792,18 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
+ "node_modules/@jsdoc/salty": {
+ "version": "0.2.7",
+ "resolved": "https://registry.npmjs.org/@jsdoc/salty/-/salty-0.2.7.tgz",
+ "integrity": "sha512-mh8LbS9d4Jq84KLw8pzho7XC2q2/IJGiJss3xwRoLD1A+EE16SjN4PfaG4jRCzKegTFLlN0Zd8SdUPE6XdoPFg==",
+ "optional": true,
+ "dependencies": {
+ "lodash": "^4.17.21"
+ },
+ "engines": {
+ "node": ">=v12.0.0"
+ }
+ },
"node_modules/@lukeed/csprng": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@lukeed/csprng/-/csprng-1.1.0.tgz",
@@ -1617,28 +1850,6 @@
"url": "https://github.com/sponsors/isaacs"
}
},
- "node_modules/@mapbox/node-pre-gyp/node_modules/make-dir": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
- "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==",
- "dependencies": {
- "semver": "^6.0.0"
- },
- "engines": {
- "node": ">=8"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/@mapbox/node-pre-gyp/node_modules/make-dir/node_modules/semver": {
- "version": "6.3.1",
- "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
- "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
- "bin": {
- "semver": "bin/semver.js"
- }
- },
"node_modules/@mapbox/node-pre-gyp/node_modules/rimraf": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
@@ -1710,10 +1921,23 @@
}
}
},
+ "node_modules/@nestjs/cli/node_modules/typescript": {
+ "version": "5.2.2",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz",
+ "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==",
+ "dev": true,
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
"node_modules/@nestjs/common": {
- "version": "10.2.8",
- "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-10.2.8.tgz",
- "integrity": "sha512-rmpwcdvq2IWMmsUVP8rsdKub6uDWk7dwCYo0aif50JTwcvcxzaP3iKVFKoSgvp0RKYu8h15+/AEOfaInmPpl0Q==",
+ "version": "10.2.10",
+ "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-10.2.10.tgz",
+ "integrity": "sha512-fwAk931rjW8CNH2Mgwawq/7HWHH1dxkOLdcgs7U52ddLk8CtHXjejm1cbNahewlSbNhvlOl7y1STLHutE6sUqw==",
"dependencies": {
"iterare": "1.2.1",
"tslib": "2.6.2",
@@ -1738,6 +1962,11 @@
}
}
},
+ "node_modules/@nestjs/common/node_modules/tslib": {
+ "version": "2.6.2",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
+ "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="
+ },
"node_modules/@nestjs/config": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/@nestjs/config/-/config-3.1.1.tgz",
@@ -1753,18 +1982,10 @@
"reflect-metadata": "^0.1.13"
}
},
- "node_modules/@nestjs/config/node_modules/uuid": {
- "version": "9.0.0",
- "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz",
- "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==",
- "bin": {
- "uuid": "dist/bin/uuid"
- }
- },
"node_modules/@nestjs/core": {
- "version": "10.2.8",
- "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-10.2.8.tgz",
- "integrity": "sha512-9+MZ2s8ixfY9Bl/M9ofChiyYymcwdK9ZWNH4GDMF7Am7XRAQ1oqde6MYGG05rhQwiVXuTwaYLlXciJKfsrg5qg==",
+ "version": "10.2.10",
+ "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-10.2.10.tgz",
+ "integrity": "sha512-+ckOI6BPi2ZMHikT9MCG4ctHDc4OnjhoIytrn7f2AYMMXI4bnutJhqyQKc30VDka5x3Wq6QAD57pgSP7y+JjJg==",
"hasInstallScript": true,
"dependencies": {
"@nuxtjs/opencollective": "0.3.2",
@@ -1798,6 +2019,11 @@
}
}
},
+ "node_modules/@nestjs/core/node_modules/tslib": {
+ "version": "2.6.2",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
+ "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="
+ },
"node_modules/@nestjs/jwt": {
"version": "10.2.0",
"resolved": "https://registry.npmjs.org/@nestjs/jwt/-/jwt-10.2.0.tgz",
@@ -1842,18 +2068,18 @@
}
},
"node_modules/@nestjs/passport": {
- "version": "10.0.2",
- "resolved": "https://registry.npmjs.org/@nestjs/passport/-/passport-10.0.2.tgz",
- "integrity": "sha512-od31vfB2z3y05IDB5dWSbCGE2+pAf2k2WCBinNuTTOxN0O0+wtO1L3kawj/aCW3YR9uxsTOVbTDwtwgpNNsnjQ==",
+ "version": "10.0.3",
+ "resolved": "https://registry.npmjs.org/@nestjs/passport/-/passport-10.0.3.tgz",
+ "integrity": "sha512-znJ9Y4S8ZDVY+j4doWAJ8EuuVO7SkQN3yOBmzxbGaXbvcSwFDAdGJ+OMCg52NdzIO4tQoN4pYKx8W6M0ArfFRQ==",
"peerDependencies": {
"@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0",
- "passport": "^0.4.0 || ^0.5.0 || ^0.6.0"
+ "passport": "^0.4.0 || ^0.5.0 || ^0.6.0 || ^0.7.0"
}
},
"node_modules/@nestjs/platform-express": {
- "version": "10.2.8",
- "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.2.8.tgz",
- "integrity": "sha512-WoSSVtwIRc5AdGMHWVzWZK4JZLT0f4o2xW8P9gQvcX+omL8W1kXCfY8GQYXNBG84XmBNYH8r0FtC8oMe/lH5NQ==",
+ "version": "10.2.10",
+ "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.2.10.tgz",
+ "integrity": "sha512-U4KDgtMjH8TqEvt0RzC/POP8ABvL9bYoCScvlGtFSKgVGaMLBKkZ4+jHtbQx6qItYSlBBRUuz/dveMZCObfrkQ==",
"dependencies": {
"body-parser": "1.20.2",
"cors": "2.8.5",
@@ -1870,6 +2096,11 @@
"@nestjs/core": "^10.0.0"
}
},
+ "node_modules/@nestjs/platform-express/node_modules/tslib": {
+ "version": "2.6.2",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
+ "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="
+ },
"node_modules/@nestjs/schedule": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/@nestjs/schedule/-/schedule-4.0.0.tgz",
@@ -1884,6 +2115,18 @@
"reflect-metadata": "^0.1.12"
}
},
+ "node_modules/@nestjs/schedule/node_modules/uuid": {
+ "version": "9.0.1",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
+ "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
+ "funding": [
+ "https://github.com/sponsors/broofa",
+ "https://github.com/sponsors/ctavan"
+ ],
+ "bin": {
+ "uuid": "dist/bin/uuid"
+ }
+ },
"node_modules/@nestjs/schematics": {
"version": "10.0.3",
"resolved": "https://registry.npmjs.org/@nestjs/schematics/-/schematics-10.0.3.tgz",
@@ -1932,9 +2175,9 @@
}
},
"node_modules/@nestjs/testing": {
- "version": "10.2.8",
- "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-10.2.8.tgz",
- "integrity": "sha512-9Kj5IQhM67/nj/MT6Wi2OmWr5YQnCMptwKVFrX1TDaikpY12196v7frk0jVjdT7wms7rV07GZle9I2z0aSjqtQ==",
+ "version": "10.2.10",
+ "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-10.2.10.tgz",
+ "integrity": "sha512-IVLUnPz/+fkBtPATYfqTIP+phN9yjkXejmj+JyhmcfPJZpxBmD1i9VSMqa4u54l37j0xkGPscQ0IXpbhqMYUKw==",
"dev": true,
"dependencies": {
"tslib": "2.6.2"
@@ -1958,6 +2201,12 @@
}
}
},
+ "node_modules/@nestjs/testing/node_modules/tslib": {
+ "version": "2.6.2",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
+ "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==",
+ "dev": true
+ },
"node_modules/@nestjs/typeorm": {
"version": "10.0.1",
"resolved": "https://registry.npmjs.org/@nestjs/typeorm/-/typeorm-10.0.1.tgz",
@@ -1973,6 +2222,18 @@
"typeorm": "^0.3.0"
}
},
+ "node_modules/@nestjs/typeorm/node_modules/uuid": {
+ "version": "9.0.1",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
+ "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
+ "funding": [
+ "https://github.com/sponsors/broofa",
+ "https://github.com/sponsors/ctavan"
+ ],
+ "bin": {
+ "uuid": "dist/bin/uuid"
+ }
+ },
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -2055,22 +2316,92 @@
"url": "https://opencollective.com/unts"
}
},
- "node_modules/@sinclair/typebox": {
- "version": "0.27.8",
- "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz",
- "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==",
+ "node_modules/@pkgr/utils/node_modules/tslib": {
+ "version": "2.6.2",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
+ "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==",
"dev": true
},
- "node_modules/@sinonjs/commons": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.0.tgz",
- "integrity": "sha512-jXBtWAF4vmdNmZgD5FoKsVLv3rPgDnLgPbU84LIJ3otV44vJlDRokVng5v8NFJdCf/da9legHcKaRuZs4L7faA==",
- "dev": true,
- "dependencies": {
- "type-detect": "4.0.8"
- }
+ "node_modules/@protobufjs/aspromise": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
+ "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==",
+ "optional": true
},
- "node_modules/@sinonjs/fake-timers": {
+ "node_modules/@protobufjs/base64": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz",
+ "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==",
+ "optional": true
+ },
+ "node_modules/@protobufjs/codegen": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz",
+ "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==",
+ "optional": true
+ },
+ "node_modules/@protobufjs/eventemitter": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz",
+ "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==",
+ "optional": true
+ },
+ "node_modules/@protobufjs/fetch": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz",
+ "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==",
+ "optional": true,
+ "dependencies": {
+ "@protobufjs/aspromise": "^1.1.1",
+ "@protobufjs/inquire": "^1.1.0"
+ }
+ },
+ "node_modules/@protobufjs/float": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz",
+ "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==",
+ "optional": true
+ },
+ "node_modules/@protobufjs/inquire": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz",
+ "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==",
+ "optional": true
+ },
+ "node_modules/@protobufjs/path": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz",
+ "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==",
+ "optional": true
+ },
+ "node_modules/@protobufjs/pool": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz",
+ "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==",
+ "optional": true
+ },
+ "node_modules/@protobufjs/utf8": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
+ "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==",
+ "optional": true
+ },
+ "node_modules/@sinclair/typebox": {
+ "version": "0.27.8",
+ "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz",
+ "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==",
+ "dev": true
+ },
+ "node_modules/@sinonjs/commons": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.0.tgz",
+ "integrity": "sha512-jXBtWAF4vmdNmZgD5FoKsVLv3rPgDnLgPbU84LIJ3otV44vJlDRokVng5v8NFJdCf/da9legHcKaRuZs4L7faA==",
+ "dev": true,
+ "dependencies": {
+ "type-detect": "4.0.8"
+ }
+ },
+ "node_modules/@sinonjs/fake-timers": {
"version": "10.3.0",
"resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz",
"integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==",
@@ -2079,11 +2410,36 @@
"@sinonjs/commons": "^3.0.0"
}
},
+ "node_modules/@songkeys/nestjs-redis": {
+ "version": "10.0.0",
+ "resolved": "https://registry.npmjs.org/@songkeys/nestjs-redis/-/nestjs-redis-10.0.0.tgz",
+ "integrity": "sha512-s56+NECuJXzcaPLYzpvA2xjL0e/1Zy55UE0q6b1UqpbQSKI06TFPFCWCMUadJigiuB26O1hxi+lmDbzahKvcLg==",
+ "dependencies": {
+ "tslib": "2.6.0"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ },
+ "peerDependencies": {
+ "@nestjs/common": "^10.0.0",
+ "@nestjs/core": "^10.0.0",
+ "ioredis": "^5.0.0"
+ }
+ },
"node_modules/@sqltools/formatter": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/@sqltools/formatter/-/formatter-1.2.5.tgz",
"integrity": "sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw=="
},
+ "node_modules/@tootallnate/once": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz",
+ "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==",
+ "optional": true,
+ "engines": {
+ "node": ">= 10"
+ }
+ },
"node_modules/@tsconfig/node10": {
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz",
@@ -2109,9 +2465,9 @@
"devOptional": true
},
"node_modules/@types/babel__core": {
- "version": "7.20.4",
- "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.4.tgz",
- "integrity": "sha512-mLnSC22IC4vcWiuObSRjrLd9XcBTGf59vUSoq2jkQDJ/QQ8PMI9rSuzE+aEV8karUMbskw07bKYoUJCKTUaygg==",
+ "version": "7.20.5",
+ "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
+ "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
"dev": true,
"dependencies": {
"@babel/parser": "^7.20.7",
@@ -2176,15 +2532,25 @@
}
},
"node_modules/@types/cookiejar": {
- "version": "2.1.4",
- "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.4.tgz",
- "integrity": "sha512-b698BLJ6kPVd6uhHsY7wlebZdrWPXYied883PDSzpJZYOP97EOn/oGdLCH3jJf157srkFReIZY5v0H1s8Dozrg==",
+ "version": "2.1.5",
+ "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz",
+ "integrity": "sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==",
"dev": true
},
+ "node_modules/@types/dotenv": {
+ "version": "8.2.0",
+ "resolved": "https://registry.npmjs.org/@types/dotenv/-/dotenv-8.2.0.tgz",
+ "integrity": "sha512-ylSC9GhfRH7m1EUXBXofhgx4lUWmFeQDINW5oLuS+gxWdfUeW4zJdeVTYVkexEW+e2VUvlZR2kGnGGipAWR7kw==",
+ "deprecated": "This is a stub types definition. dotenv provides its own type definitions, so you do not need this installed.",
+ "dev": true,
+ "dependencies": {
+ "dotenv": "*"
+ }
+ },
"node_modules/@types/eslint": {
- "version": "8.44.7",
- "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.44.7.tgz",
- "integrity": "sha512-f5ORu2hcBbKei97U73mf+l9t4zTGl74IqZ0GQk4oVea/VS8tQZYkUveSYojk+frraAVYId0V2WC9O4PTNru2FQ==",
+ "version": "8.44.8",
+ "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.44.8.tgz",
+ "integrity": "sha512-4K8GavROwhrYl2QXDXm0Rv9epkA8GBFu0EI+XrrnnuCl7u8CWBRusX7fXJfanhZTDWSAL24gDI/UqXyUM0Injw==",
"dev": true,
"dependencies": {
"@types/estree": "*",
@@ -2229,6 +2595,16 @@
"@types/send": "*"
}
},
+ "node_modules/@types/glob": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/@types/glob/-/glob-8.1.0.tgz",
+ "integrity": "sha512-IO+MJPVhoqz+28h1qLAcBEH2+xHMK6MTyHJc7MTnnYb6wsoLR29POVGJ7LycmVXIqyy/4/2ShP5sUwTXuOwb/w==",
+ "optional": true,
+ "dependencies": {
+ "@types/minimatch": "^5.1.2",
+ "@types/node": "*"
+ }
+ },
"node_modules/@types/graceful-fs": {
"version": "4.1.9",
"resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz",
@@ -2268,9 +2644,9 @@
}
},
"node_modules/@types/jest": {
- "version": "29.5.8",
- "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.8.tgz",
- "integrity": "sha512-fXEFTxMV2Co8ZF5aYFJv+YeA08RTYJfhtN5c9JSv/mFEMe+xxjufCb+PHL+bJcMs/ebPUsBu+UNTEz+ydXrR6g==",
+ "version": "29.5.11",
+ "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.11.tgz",
+ "integrity": "sha512-S2mHmYIVe13vrm6q4kN6fLYYAka15ALQki/vgDC3mIukEOx8WJlv0kQPM+d4w8Gp6u0uSdKND04IlTXBv0rwnQ==",
"dev": true,
"dependencies": {
"expect": "^29.0.0",
@@ -2291,28 +2667,62 @@
"@types/node": "*"
}
},
+ "node_modules/@types/linkify-it": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-3.0.5.tgz",
+ "integrity": "sha512-yg6E+u0/+Zjva+buc3EIb+29XEg4wltq7cSmd4Uc2EE/1nUVmxyzpX6gUXD0V8jIrG0r7YeOGVIbYRkxeooCtw==",
+ "optional": true
+ },
+ "node_modules/@types/long": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz",
+ "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==",
+ "optional": true
+ },
"node_modules/@types/luxon": {
- "version": "3.3.5",
- "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.3.5.tgz",
- "integrity": "sha512-1cyf6Ge/94zlaWIZA2ei1pE6SZ8xpad2hXaYa5JEFiaUH0YS494CZwyi4MXNpXD9oEuv6ZH0Bmh0e7F9sPhmZA=="
+ "version": "3.3.7",
+ "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.3.7.tgz",
+ "integrity": "sha512-gKc9P2d4g5uYwmy4s/MO/yOVPmvHyvzka1YH6i5dM03UrFofHSmgc0D0ymbDRStFWHusk6cwwF6nhLm/ckBbbQ=="
+ },
+ "node_modules/@types/markdown-it": {
+ "version": "12.2.3",
+ "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-12.2.3.tgz",
+ "integrity": "sha512-GKMHFfv3458yYy+v/N8gjufHO6MSZKCOXpZc5GXIWWy8uldwfmPn98vp81gZ5f9SVw8YYBctgfJ22a2d7AOMeQ==",
+ "optional": true,
+ "dependencies": {
+ "@types/linkify-it": "*",
+ "@types/mdurl": "*"
+ }
+ },
+ "node_modules/@types/mdurl": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-1.0.5.tgz",
+ "integrity": "sha512-6L6VymKTzYSrEf4Nev4Xa1LCHKrlTlYCBMTlQKFuddo1CvQcE52I0mwfOJayueUC7MJuXOeHTcIU683lzd0cUA==",
+ "optional": true
},
"node_modules/@types/mime": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
"integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w=="
},
+ "node_modules/@types/minimatch": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz",
+ "integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==",
+ "optional": true
+ },
"node_modules/@types/node": {
- "version": "20.9.0",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-20.9.0.tgz",
- "integrity": "sha512-nekiGu2NDb1BcVofVcEKMIwzlx4NjHlcjhoxxKBNLtz15Y1z7MYf549DFvkHSId02Ax6kGwWntIBPC3l/JZcmw==",
+ "version": "20.10.3",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.3.tgz",
+ "integrity": "sha512-XJavIpZqiXID5Yxnxv3RUDKTN5b81ddNC3ecsA0SoFXz/QU8OGBwZGMomiq0zw+uuqbL/krztv/DINAQ/EV4gg==",
"dependencies": {
"undici-types": "~5.26.4"
}
},
"node_modules/@types/passport": {
- "version": "1.0.15",
- "resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.15.tgz",
- "integrity": "sha512-oHOgzPBp5eLI1U/7421qYV/ZySQXMYCBSfRkDe1tQ0YrIbLY/M/76qIXE7Bs7lFyvw1x5QqiNQ9imvh0fQHe9Q==",
+ "version": "1.0.16",
+ "resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.16.tgz",
+ "integrity": "sha512-FD0qD5hbPWQzaM0wHUnJ/T0BBCJBxCeemtnCwc/ThhTg3x9jfrAcRUmj5Dopza+MfFS9acTe3wk7rcVnRIp/0A==",
"dependencies": {
"@types/express": "*"
}
@@ -2357,10 +2767,20 @@
"resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz",
"integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ=="
},
+ "node_modules/@types/rimraf": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@types/rimraf/-/rimraf-3.0.2.tgz",
+ "integrity": "sha512-F3OznnSLAUxFrCEu/L5PY8+ny8DtcFRjx7fZZ9bycvXRi3KPTRS9HOitGZwvPg0juRhXFWIeKX58cnX5YqLohQ==",
+ "optional": true,
+ "dependencies": {
+ "@types/glob": "*",
+ "@types/node": "*"
+ }
+ },
"node_modules/@types/semver": {
- "version": "7.5.5",
- "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.5.tgz",
- "integrity": "sha512-+d+WYC1BxJ6yVOgUgzK8gWvp5qF8ssV5r4nsDcZWKRWcDQLQ619tvWAxJQYGgBrO1MnLJC7a5GtiYsAoQ47dJg==",
+ "version": "7.5.6",
+ "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.6.tgz",
+ "integrity": "sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==",
"dev": true
},
"node_modules/@types/send": {
@@ -2389,9 +2809,9 @@
"dev": true
},
"node_modules/@types/superagent": {
- "version": "4.1.21",
- "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-4.1.21.tgz",
- "integrity": "sha512-yrbAccEEY9+BSa1wji3ry8R3/BdW9kyWnjkRKctrtw5ebn/k2a2CsMeaQ7dD4iLfomgHkomBVIVgOFRMV4XYHA==",
+ "version": "4.1.24",
+ "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-4.1.24.tgz",
+ "integrity": "sha512-mEafCgyKiMFin24SDzWN7yAADt4gt6YawFiNMp0QS5ZPboORfyxFt0s3VzJKhTaKg9py/4FUmrHLTNfJKt9Rbw==",
"dev": true,
"dependencies": {
"@types/cookiejar": "*",
@@ -2413,9 +2833,9 @@
"integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw=="
},
"node_modules/@types/validator": {
- "version": "13.11.6",
- "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.11.6.tgz",
- "integrity": "sha512-HUgHujPhKuNzgNXBRZKYexwoG+gHKU+tnfPqjWXFghZAnn73JElicMkuSKJyLGr9JgyA8IgK7fj88IyA9rwYeQ=="
+ "version": "13.11.7",
+ "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.11.7.tgz",
+ "integrity": "sha512-q0JomTsJ2I5Mv7dhHhQLGjMvX0JJm5dyZ1DXQySIUzU1UlwzB8bt+R6+LODUbz0UDIOvEzGc28tk27gBJw2N8Q=="
},
"node_modules/@types/webidl-conversions": {
"version": "7.0.3",
@@ -2431,10 +2851,20 @@
"@types/webidl-conversions": "*"
}
},
+ "node_modules/@types/winston": {
+ "version": "2.4.4",
+ "resolved": "https://registry.npmjs.org/@types/winston/-/winston-2.4.4.tgz",
+ "integrity": "sha512-BVGCztsypW8EYwJ+Hq+QNYiT/MUyCif0ouBH+flrY66O5W+KIXAMML6E/0fJpm7VjIzgangahl5S03bJJQGrZw==",
+ "deprecated": "This is a stub types definition. winston provides its own type definitions, so you do not need this installed.",
+ "dev": true,
+ "dependencies": {
+ "winston": "*"
+ }
+ },
"node_modules/@types/yargs": {
- "version": "17.0.31",
- "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.31.tgz",
- "integrity": "sha512-bocYSx4DI8TmdlvxqGpVNXOgCNR1Jj0gNPhhAY+iz1rgKDAaYrAYdFYnhDV1IFuiuVc9HkOwyDcFxaTElF3/wg==",
+ "version": "17.0.32",
+ "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz",
+ "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==",
"dev": true,
"dependencies": {
"@types/yargs-parser": "*"
@@ -2447,16 +2877,16 @@
"dev": true
},
"node_modules/@typescript-eslint/eslint-plugin": {
- "version": "6.10.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.10.0.tgz",
- "integrity": "sha512-uoLj4g2OTL8rfUQVx2AFO1hp/zja1wABJq77P6IclQs6I/m9GLrm7jCdgzZkvWdDCQf1uEvoa8s8CupsgWQgVg==",
+ "version": "6.13.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.13.2.tgz",
+ "integrity": "sha512-3+9OGAWHhk4O1LlcwLBONbdXsAhLjyCFogJY/cWy2lxdVJ2JrcTF2pTGMaLl2AE7U1l31n8Py4a8bx5DLf/0dQ==",
"dev": true,
"dependencies": {
"@eslint-community/regexpp": "^4.5.1",
- "@typescript-eslint/scope-manager": "6.10.0",
- "@typescript-eslint/type-utils": "6.10.0",
- "@typescript-eslint/utils": "6.10.0",
- "@typescript-eslint/visitor-keys": "6.10.0",
+ "@typescript-eslint/scope-manager": "6.13.2",
+ "@typescript-eslint/type-utils": "6.13.2",
+ "@typescript-eslint/utils": "6.13.2",
+ "@typescript-eslint/visitor-keys": "6.13.2",
"debug": "^4.3.4",
"graphemer": "^1.4.0",
"ignore": "^5.2.4",
@@ -2482,15 +2912,15 @@
}
},
"node_modules/@typescript-eslint/parser": {
- "version": "6.10.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.10.0.tgz",
- "integrity": "sha512-+sZwIj+s+io9ozSxIWbNB5873OSdfeBEH/FR0re14WLI6BaKuSOnnwCJ2foUiu8uXf4dRp1UqHP0vrZ1zXGrog==",
+ "version": "6.13.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.13.2.tgz",
+ "integrity": "sha512-MUkcC+7Wt/QOGeVlM8aGGJZy1XV5YKjTpq9jK6r6/iLsGXhBVaGP5N0UYvFsu9BFlSpwY9kMretzdBH01rkRXg==",
"dev": true,
"dependencies": {
- "@typescript-eslint/scope-manager": "6.10.0",
- "@typescript-eslint/types": "6.10.0",
- "@typescript-eslint/typescript-estree": "6.10.0",
- "@typescript-eslint/visitor-keys": "6.10.0",
+ "@typescript-eslint/scope-manager": "6.13.2",
+ "@typescript-eslint/types": "6.13.2",
+ "@typescript-eslint/typescript-estree": "6.13.2",
+ "@typescript-eslint/visitor-keys": "6.13.2",
"debug": "^4.3.4"
},
"engines": {
@@ -2510,13 +2940,13 @@
}
},
"node_modules/@typescript-eslint/scope-manager": {
- "version": "6.10.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.10.0.tgz",
- "integrity": "sha512-TN/plV7dzqqC2iPNf1KrxozDgZs53Gfgg5ZHyw8erd6jd5Ta/JIEcdCheXFt9b1NYb93a1wmIIVW/2gLkombDg==",
+ "version": "6.13.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.13.2.tgz",
+ "integrity": "sha512-CXQA0xo7z6x13FeDYCgBkjWzNqzBn8RXaE3QVQVIUm74fWJLkJkaHmHdKStrxQllGh6Q4eUGyNpMe0b1hMkXFA==",
"dev": true,
"dependencies": {
- "@typescript-eslint/types": "6.10.0",
- "@typescript-eslint/visitor-keys": "6.10.0"
+ "@typescript-eslint/types": "6.13.2",
+ "@typescript-eslint/visitor-keys": "6.13.2"
},
"engines": {
"node": "^16.0.0 || >=18.0.0"
@@ -2527,13 +2957,13 @@
}
},
"node_modules/@typescript-eslint/type-utils": {
- "version": "6.10.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.10.0.tgz",
- "integrity": "sha512-wYpPs3hgTFblMYwbYWPT3eZtaDOjbLyIYuqpwuLBBqhLiuvJ+9sEp2gNRJEtR5N/c9G1uTtQQL5AhV0fEPJYcg==",
+ "version": "6.13.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.13.2.tgz",
+ "integrity": "sha512-Qr6ssS1GFongzH2qfnWKkAQmMUyZSyOr0W54nZNU1MDfo+U4Mv3XveeLZzadc/yq8iYhQZHYT+eoXJqnACM1tw==",
"dev": true,
"dependencies": {
- "@typescript-eslint/typescript-estree": "6.10.0",
- "@typescript-eslint/utils": "6.10.0",
+ "@typescript-eslint/typescript-estree": "6.13.2",
+ "@typescript-eslint/utils": "6.13.2",
"debug": "^4.3.4",
"ts-api-utils": "^1.0.1"
},
@@ -2554,9 +2984,9 @@
}
},
"node_modules/@typescript-eslint/types": {
- "version": "6.10.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.10.0.tgz",
- "integrity": "sha512-36Fq1PWh9dusgo3vH7qmQAj5/AZqARky1Wi6WpINxB6SkQdY5vQoT2/7rW7uBIsPDcvvGCLi4r10p0OJ7ITAeg==",
+ "version": "6.13.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.13.2.tgz",
+ "integrity": "sha512-7sxbQ+EMRubQc3wTfTsycgYpSujyVbI1xw+3UMRUcrhSy+pN09y/lWzeKDbvhoqcRbHdc+APLs/PWYi/cisLPg==",
"dev": true,
"engines": {
"node": "^16.0.0 || >=18.0.0"
@@ -2567,13 +2997,13 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
- "version": "6.10.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.10.0.tgz",
- "integrity": "sha512-ek0Eyuy6P15LJVeghbWhSrBCj/vJpPXXR+EpaRZqou7achUWL8IdYnMSC5WHAeTWswYQuP2hAZgij/bC9fanBg==",
+ "version": "6.13.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.13.2.tgz",
+ "integrity": "sha512-SuD8YLQv6WHnOEtKv8D6HZUzOub855cfPnPMKvdM/Bh1plv1f7Q/0iFUDLKKlxHcEstQnaUU4QZskgQq74t+3w==",
"dev": true,
"dependencies": {
- "@typescript-eslint/types": "6.10.0",
- "@typescript-eslint/visitor-keys": "6.10.0",
+ "@typescript-eslint/types": "6.13.2",
+ "@typescript-eslint/visitor-keys": "6.13.2",
"debug": "^4.3.4",
"globby": "^11.1.0",
"is-glob": "^4.0.3",
@@ -2594,17 +3024,17 @@
}
},
"node_modules/@typescript-eslint/utils": {
- "version": "6.10.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.10.0.tgz",
- "integrity": "sha512-v+pJ1/RcVyRc0o4wAGux9x42RHmAjIGzPRo538Z8M1tVx6HOnoQBCX/NoadHQlZeC+QO2yr4nNSFWOoraZCAyg==",
+ "version": "6.13.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.13.2.tgz",
+ "integrity": "sha512-b9Ptq4eAZUym4idijCRzl61oPCwwREcfDI8xGk751Vhzig5fFZR9CyzDz4Sp/nxSLBYxUPyh4QdIDqWykFhNmQ==",
"dev": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.4.0",
"@types/json-schema": "^7.0.12",
"@types/semver": "^7.5.0",
- "@typescript-eslint/scope-manager": "6.10.0",
- "@typescript-eslint/types": "6.10.0",
- "@typescript-eslint/typescript-estree": "6.10.0",
+ "@typescript-eslint/scope-manager": "6.13.2",
+ "@typescript-eslint/types": "6.13.2",
+ "@typescript-eslint/typescript-estree": "6.13.2",
"semver": "^7.5.4"
},
"engines": {
@@ -2619,12 +3049,12 @@
}
},
"node_modules/@typescript-eslint/visitor-keys": {
- "version": "6.10.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.10.0.tgz",
- "integrity": "sha512-xMGluxQIEtOM7bqFCo+rCMh5fqI+ZxV5RUUOa29iVPz1OgCZrtc7rFnz5cLUazlkPKYqX+75iuDq7m0HQ48nCg==",
+ "version": "6.13.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.13.2.tgz",
+ "integrity": "sha512-OGznFs0eAQXJsp+xSd6k/O1UbFi/K/L7WjqeRoFE7vadjAF9y0uppXhYNQNEqygjou782maGClOoZwPqF0Drlw==",
"dev": true,
"dependencies": {
- "@typescript-eslint/types": "6.10.0",
+ "@typescript-eslint/types": "6.13.2",
"eslint-visitor-keys": "^3.4.1"
},
"engines": {
@@ -2804,6 +3234,18 @@
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
"integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q=="
},
+ "node_modules/abort-controller": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
+ "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
+ "optional": true,
+ "dependencies": {
+ "event-target-shim": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=6.5"
+ }
+ },
"node_modules/accepts": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
@@ -2841,15 +3283,15 @@
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
"integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
- "dev": true,
+ "devOptional": true,
"peerDependencies": {
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
}
},
"node_modules/acorn-walk": {
- "version": "8.3.0",
- "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.0.tgz",
- "integrity": "sha512-FS7hV565M5l1R08MXqo8odwMTB02C2UqzB17RVgu9EyuYFBqJZ3/ZY97sQD5FewVu1UyDFc1yztUDrAwT0EypA==",
+ "version": "8.3.1",
+ "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.1.tgz",
+ "integrity": "sha512-TgUZgYvqZprrl7YldZNoa9OciCAyZR+Ejm9eXzKCmjsF5IKp/wgQ7Z/ZpjpGTIUPwrHQIcYeI8qDh4PsEwxMbw==",
"devOptional": true,
"engines": {
"node": ">=0.4.0"
@@ -2867,14 +3309,14 @@
}
},
"node_modules/ajv": {
- "version": "8.12.0",
- "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz",
- "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==",
+ "version": "6.12.6",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
+ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"dev": true,
"dependencies": {
"fast-deep-equal": "^3.1.1",
- "json-schema-traverse": "^1.0.0",
- "require-from-string": "^2.0.2",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
"uri-js": "^4.2.2"
},
"funding": {
@@ -2899,6 +3341,37 @@
}
}
},
+ "node_modules/ajv-formats/node_modules/ajv": {
+ "version": "8.12.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz",
+ "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==",
+ "dev": true,
+ "dependencies": {
+ "fast-deep-equal": "^3.1.1",
+ "json-schema-traverse": "^1.0.0",
+ "require-from-string": "^2.0.2",
+ "uri-js": "^4.2.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/ajv-formats/node_modules/json-schema-traverse": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
+ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
+ "dev": true
+ },
+ "node_modules/ajv-keywords": {
+ "version": "3.5.2",
+ "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz",
+ "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==",
+ "dev": true,
+ "peerDependencies": {
+ "ajv": "^6.9.1"
+ }
+ },
"node_modules/ansi-colors": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz",
@@ -3005,19 +3478,6 @@
"node": ">=10"
}
},
- "node_modules/are-we-there-yet/node_modules/readable-stream": {
- "version": "3.6.2",
- "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
- "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
- "dependencies": {
- "inherits": "^2.0.3",
- "string_decoder": "^1.1.1",
- "util-deprecate": "^1.0.1"
- },
- "engines": {
- "node": ">= 6"
- }
- },
"node_modules/arg": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
@@ -3049,6 +3509,15 @@
"node": ">=8"
}
},
+ "node_modules/arrify": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz",
+ "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==",
+ "optional": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/asap": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz",
@@ -3060,6 +3529,15 @@
"resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz",
"integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg=="
},
+ "node_modules/async-retry": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/async-retry/-/async-retry-1.3.3.tgz",
+ "integrity": "sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==",
+ "optional": true,
+ "dependencies": {
+ "retry": "0.13.1"
+ }
+ },
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
@@ -3229,14 +3707,23 @@
}
},
"node_modules/big-integer": {
- "version": "1.6.51",
- "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.51.tgz",
- "integrity": "sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg==",
+ "version": "1.6.52",
+ "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz",
+ "integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==",
"dev": true,
"engines": {
"node": ">=0.6"
}
},
+ "node_modules/bignumber.js": {
+ "version": "9.1.2",
+ "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.2.tgz",
+ "integrity": "sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==",
+ "optional": true,
+ "engines": {
+ "node": "*"
+ }
+ },
"node_modules/binary-extensions": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
@@ -3257,19 +3744,11 @@
"readable-stream": "^3.4.0"
}
},
- "node_modules/bl/node_modules/readable-stream": {
- "version": "3.6.2",
- "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
- "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
- "dev": true,
- "dependencies": {
- "inherits": "^2.0.3",
- "string_decoder": "^1.1.1",
- "util-deprecate": "^1.0.1"
- },
- "engines": {
- "node": ">= 6"
- }
+ "node_modules/bluebird": {
+ "version": "3.7.2",
+ "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz",
+ "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==",
+ "optional": true
},
"node_modules/body-parser": {
"version": "1.20.2",
@@ -3352,9 +3831,9 @@
}
},
"node_modules/browserslist": {
- "version": "4.22.1",
- "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.1.tgz",
- "integrity": "sha512-FEVc202+2iuClEhZhrWy6ZiAcRLvNMyYcxZ8raemul1DYVOVdFsbqckWLdsixQZCpJlwe77Z3UTalE7jsjnKfQ==",
+ "version": "4.22.2",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.2.tgz",
+ "integrity": "sha512-0UgcrvQmBDvZHFGdYUehrCNIazki7/lUP3kkoi/r3YB2amZbFM9J43ZRkJTXBUZK4gmx56+Sqk9+Vs9mwZx9+A==",
"dev": true,
"funding": [
{
@@ -3371,9 +3850,9 @@
}
],
"dependencies": {
- "caniuse-lite": "^1.0.30001541",
- "electron-to-chromium": "^1.4.535",
- "node-releases": "^2.0.13",
+ "caniuse-lite": "^1.0.30001565",
+ "electron-to-chromium": "^1.4.601",
+ "node-releases": "^2.0.14",
"update-browserslist-db": "^1.0.13"
},
"bin": {
@@ -3512,9 +3991,9 @@
}
},
"node_modules/caniuse-lite": {
- "version": "1.0.30001561",
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001561.tgz",
- "integrity": "sha512-NTt0DNoKe958Q0BE0j0c1V9jbUzhBxHIEJy7asmGrpE0yG63KTV7PLHPnK2E1O9RsQrQ081I3NLuXGS6zht3cw==",
+ "version": "1.0.30001566",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001566.tgz",
+ "integrity": "sha512-ggIhCsTxmITBAMmK8yZjEhCO5/47jKXPu6Dha/wuCS4JePVL+3uiDEBuhu2aIoT+bqTOR8L76Ip1ARL9xYsEJA==",
"dev": true,
"funding": [
{
@@ -3531,6 +4010,18 @@
}
]
},
+ "node_modules/catharsis": {
+ "version": "0.9.0",
+ "resolved": "https://registry.npmjs.org/catharsis/-/catharsis-0.9.0.tgz",
+ "integrity": "sha512-prMTQVpcns/tzFgFVkVp6ak6RykZyWb3gu8ckUpd6YkTlacOd3DXGJjIpD4Q6zJirizvaiAjSSHlOsA+6sNh2A==",
+ "optional": true,
+ "dependencies": {
+ "lodash": "^4.17.15"
+ },
+ "engines": {
+ "node": ">= 10"
+ }
+ },
"node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
@@ -3725,9 +4216,9 @@
}
},
"node_modules/cli-spinners": {
- "version": "2.9.1",
- "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.1.tgz",
- "integrity": "sha512-jHgecW0pxkonBJdrKsqxgRX9AcG+u/5k0Q7WPDfi8AogLAdwxEkyYYNWwZ5GvVFoFx2uiY1eNcSK00fh+1+FyQ==",
+ "version": "2.9.2",
+ "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz",
+ "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==",
"dev": true,
"engines": {
"node": ">=6"
@@ -3798,6 +4289,14 @@
"node": ">=0.8"
}
},
+ "node_modules/cluster-key-slot": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz",
+ "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/co": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
@@ -3915,10 +4414,25 @@
}
},
"node_modules/component-emitter": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz",
- "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==",
- "dev": true
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz",
+ "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==",
+ "dev": true,
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/compressible": {
+ "version": "2.0.18",
+ "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz",
+ "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==",
+ "optional": true,
+ "dependencies": {
+ "mime-db": ">= 1.43.0 < 2"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
},
"node_modules/concat-map": {
"version": "0.0.1",
@@ -3939,6 +4453,33 @@
"typedarray": "^0.0.6"
}
},
+ "node_modules/concat-stream/node_modules/readable-stream": {
+ "version": "2.3.8",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
+ "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
+ "dependencies": {
+ "core-util-is": "~1.0.0",
+ "inherits": "~2.0.3",
+ "isarray": "~1.0.0",
+ "process-nextick-args": "~2.0.0",
+ "safe-buffer": "~5.1.1",
+ "string_decoder": "~1.1.1",
+ "util-deprecate": "~1.0.1"
+ }
+ },
+ "node_modules/concat-stream/node_modules/safe-buffer": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
+ },
+ "node_modules/concat-stream/node_modules/string_decoder": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+ "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
+ "dependencies": {
+ "safe-buffer": "~5.1.0"
+ }
+ },
"node_modules/consola": {
"version": "2.15.3",
"resolved": "https://registry.npmjs.org/consola/-/consola-2.15.3.tgz",
@@ -4153,7 +4694,7 @@
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
"integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
- "dev": true
+ "devOptional": true
},
"node_modules/deepmerge": {
"version": "4.3.1",
@@ -4477,6 +5018,18 @@
"node": ">=12"
}
},
+ "node_modules/duplexify": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.2.tgz",
+ "integrity": "sha512-fz3OjcNCHmRP12MJoZMPglx8m4rrFP8rovnk4vT8Fs+aonZoCwGg10dSsQsfP/E62eZcPTMSMP6686fu9Qlqtw==",
+ "optional": true,
+ "dependencies": {
+ "end-of-stream": "^1.4.1",
+ "inherits": "^2.0.3",
+ "readable-stream": "^3.1.1",
+ "stream-shift": "^1.0.0"
+ }
+ },
"node_modules/eastasianwidth": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
@@ -4497,9 +5050,9 @@
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="
},
"node_modules/electron-to-chromium": {
- "version": "1.4.579",
- "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.579.tgz",
- "integrity": "sha512-bJKvA+awBIzYR0xRced7PrQuRIwGQPpo6ZLP62GAShahU9fWpsNN2IP6BSP1BLDDSbxvBVRGAMWlvVVq3npmLA==",
+ "version": "1.4.606",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.606.tgz",
+ "integrity": "sha512-Zdv0XuhfyWZUsQ5Uq59d43ZmZOdoGZNWjeN4WCxxlQaP8crAWdnWcTxfHKcaJl6PW2SWpHx6DsxSx7v6KcGCuw==",
"dev": true
},
"node_modules/emittery": {
@@ -4536,7 +5089,7 @@
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
"integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
- "dev": true,
+ "devOptional": true,
"dependencies": {
"once": "^1.4.0"
}
@@ -4554,6 +5107,21 @@
"node": ">=10.13.0"
}
},
+ "node_modules/ent": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.0.tgz",
+ "integrity": "sha512-GHrMyVZQWvTIdDtpiEXdHZnFQKzeO09apj8Cbl4pKWy4i0Oprcq17usfDt5aO63swf0JOeMWjWQE/LzgSRuWpA==",
+ "optional": true
+ },
+ "node_modules/entities": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz",
+ "integrity": "sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==",
+ "optional": true,
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
"node_modules/error-ex": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
@@ -4564,9 +5132,9 @@
}
},
"node_modules/es-module-lexer": {
- "version": "1.3.1",
- "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.3.1.tgz",
- "integrity": "sha512-JUFAyicQV9mXc3YRxPnDlrfBKpqt6hUYzz9/boprUJHs4e4KVr3XwOF70doO6gwXUor6EWZJAyWAfKki84t20Q==",
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.4.1.tgz",
+ "integrity": "sha512-cXLGjP0c4T3flZJKQSuziYoq7MlT+rnvfZjfp7h+I7K9BNX54kP9nyWvdbwjQ4u1iWbOL4u96fgeZLToQlZC7w==",
"dev": true
},
"node_modules/escalade": {
@@ -4594,24 +5162,115 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/eslint": {
- "version": "8.53.0",
- "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.53.0.tgz",
- "integrity": "sha512-N4VuiPjXDUa4xVeV/GC/RV3hQW9Nw+Y463lkWaKKXKYMvmRiRDAtfpuPFLN+E1/6ZhyR8J2ig+eVREnYgUsiag==",
- "dev": true,
+ "node_modules/escodegen": {
+ "version": "1.14.3",
+ "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz",
+ "integrity": "sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==",
+ "optional": true,
"dependencies": {
- "@eslint-community/eslint-utils": "^4.2.0",
- "@eslint-community/regexpp": "^4.6.1",
- "@eslint/eslintrc": "^2.1.3",
- "@eslint/js": "8.53.0",
- "@humanwhocodes/config-array": "^0.11.13",
- "@humanwhocodes/module-importer": "^1.0.1",
- "@nodelib/fs.walk": "^1.2.8",
- "@ungap/structured-clone": "^1.2.0",
- "ajv": "^6.12.4",
- "chalk": "^4.0.0",
- "cross-spawn": "^7.0.2",
- "debug": "^4.3.2",
+ "esprima": "^4.0.1",
+ "estraverse": "^4.2.0",
+ "esutils": "^2.0.2",
+ "optionator": "^0.8.1"
+ },
+ "bin": {
+ "escodegen": "bin/escodegen.js",
+ "esgenerate": "bin/esgenerate.js"
+ },
+ "engines": {
+ "node": ">=4.0"
+ },
+ "optionalDependencies": {
+ "source-map": "~0.6.1"
+ }
+ },
+ "node_modules/escodegen/node_modules/estraverse": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz",
+ "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==",
+ "optional": true,
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/escodegen/node_modules/levn": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz",
+ "integrity": "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==",
+ "optional": true,
+ "dependencies": {
+ "prelude-ls": "~1.1.2",
+ "type-check": "~0.3.2"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/escodegen/node_modules/optionator": {
+ "version": "0.8.3",
+ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz",
+ "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==",
+ "optional": true,
+ "dependencies": {
+ "deep-is": "~0.1.3",
+ "fast-levenshtein": "~2.0.6",
+ "levn": "~0.3.0",
+ "prelude-ls": "~1.1.2",
+ "type-check": "~0.3.2",
+ "word-wrap": "~1.2.3"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/escodegen/node_modules/prelude-ls": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz",
+ "integrity": "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==",
+ "optional": true,
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/escodegen/node_modules/source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "optional": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/escodegen/node_modules/type-check": {
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz",
+ "integrity": "sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==",
+ "optional": true,
+ "dependencies": {
+ "prelude-ls": "~1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/eslint": {
+ "version": "8.55.0",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.55.0.tgz",
+ "integrity": "sha512-iyUUAM0PCKj5QpwGfmCAG9XXbZCWsqP/eWAWrG/W0umvjuLRBECwSFdt+rCntju0xEH7teIABPwXpahftIaTdA==",
+ "dev": true,
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.2.0",
+ "@eslint-community/regexpp": "^4.6.1",
+ "@eslint/eslintrc": "^2.1.4",
+ "@eslint/js": "8.55.0",
+ "@humanwhocodes/config-array": "^0.11.13",
+ "@humanwhocodes/module-importer": "^1.0.1",
+ "@nodelib/fs.walk": "^1.2.8",
+ "@ungap/structured-clone": "^1.2.0",
+ "ajv": "^6.12.4",
+ "chalk": "^4.0.0",
+ "cross-spawn": "^7.0.2",
+ "debug": "^4.3.2",
"doctrine": "^3.0.0",
"escape-string-regexp": "^4.0.0",
"eslint-scope": "^7.2.2",
@@ -4650,9 +5309,9 @@
}
},
"node_modules/eslint-config-prettier": {
- "version": "9.0.0",
- "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.0.0.tgz",
- "integrity": "sha512-IcJsTkJae2S35pRsRAwoCE+925rJJStOdkKnLVgtE+tEpqU0EVVM7OqrwxqgptKdX29NUwC82I5pXsGFIgSevw==",
+ "version": "9.1.0",
+ "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz",
+ "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==",
"dev": true,
"bin": {
"eslint-config-prettier": "bin/cli.js"
@@ -4710,7 +5369,7 @@
"version": "3.4.3",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
"integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
- "dev": true,
+ "devOptional": true,
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
},
@@ -4718,22 +5377,6 @@
"url": "https://opencollective.com/eslint"
}
},
- "node_modules/eslint/node_modules/ajv": {
- "version": "6.12.6",
- "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
- "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
- "dev": true,
- "dependencies": {
- "fast-deep-equal": "^3.1.1",
- "fast-json-stable-stringify": "^2.0.0",
- "json-schema-traverse": "^0.4.1",
- "uri-js": "^4.2.2"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/epoberezkin"
- }
- },
"node_modules/eslint/node_modules/glob-parent": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
@@ -4746,17 +5389,11 @@
"node": ">=10.13.0"
}
},
- "node_modules/eslint/node_modules/json-schema-traverse": {
- "version": "0.4.1",
- "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
- "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
- "dev": true
- },
"node_modules/espree": {
"version": "9.6.1",
"resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz",
"integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==",
- "dev": true,
+ "devOptional": true,
"dependencies": {
"acorn": "^8.9.0",
"acorn-jsx": "^5.3.2",
@@ -4773,7 +5410,7 @@
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
"integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
- "dev": true,
+ "devOptional": true,
"bin": {
"esparse": "bin/esparse.js",
"esvalidate": "bin/esvalidate.js"
@@ -4810,7 +5447,7 @@
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
"integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
- "dev": true,
+ "devOptional": true,
"engines": {
"node": ">=4.0"
}
@@ -4819,7 +5456,7 @@
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
"integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
- "dev": true,
+ "devOptional": true,
"engines": {
"node": ">=0.10.0"
}
@@ -4832,6 +5469,15 @@
"node": ">= 0.6"
}
},
+ "node_modules/event-target-shim": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
+ "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==",
+ "optional": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/events": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
@@ -5002,6 +5648,12 @@
"node": ">= 0.8"
}
},
+ "node_modules/extend": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
+ "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
+ "optional": true
+ },
"node_modules/external-editor": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz",
@@ -5032,7 +5684,7 @@
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
- "dev": true
+ "devOptional": true
},
"node_modules/fast-diff": {
"version": "1.3.0",
@@ -5066,13 +5718,41 @@
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
"integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
- "dev": true
+ "devOptional": true
},
"node_modules/fast-safe-stringify": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz",
"integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA=="
},
+ "node_modules/fast-text-encoding": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/fast-text-encoding/-/fast-text-encoding-1.0.6.tgz",
+ "integrity": "sha512-VhXlQgj9ioXCqGstD37E/HBeqEGV/qOD/kmbVG8h5xKBYvM1L3lR1Zn4555cQ8GkYbJa8aJSipLPndE1k6zK2w==",
+ "optional": true
+ },
+ "node_modules/fast-xml-parser": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.3.2.tgz",
+ "integrity": "sha512-rmrXUXwbJedoXkStenj1kkljNF7ugn5ZjR9FJcwmCfcCbtOMDghPajbc+Tck6vE6F5XsDmx+Pr2le9fw8+pXBg==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/NaturalIntelligence"
+ },
+ {
+ "type": "paypal",
+ "url": "https://paypal.me/naturalintelligence"
+ }
+ ],
+ "optional": true,
+ "dependencies": {
+ "strnum": "^1.0.5"
+ },
+ "bin": {
+ "fxparser": "src/cli/cli.js"
+ }
+ },
"node_modules/fastq": {
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz",
@@ -5082,6 +5762,17 @@
"reusify": "^1.0.4"
}
},
+ "node_modules/faye-websocket": {
+ "version": "0.11.4",
+ "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz",
+ "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==",
+ "dependencies": {
+ "websocket-driver": ">=0.5.1"
+ },
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
"node_modules/fb-watchman": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz",
@@ -5198,10 +5889,32 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/firebase-admin": {
+ "version": "11.11.1",
+ "resolved": "https://registry.npmjs.org/firebase-admin/-/firebase-admin-11.11.1.tgz",
+ "integrity": "sha512-UyEbq+3u6jWzCYbUntv/HuJiTixwh36G1R9j0v71mSvGAx/YZEWEW7uSGLYxBYE6ckVRQoKMr40PYUEzrm/4dg==",
+ "dependencies": {
+ "@fastify/busboy": "^1.2.1",
+ "@firebase/database-compat": "^0.3.4",
+ "@firebase/database-types": "^0.10.4",
+ "@types/node": ">=12.12.47",
+ "jsonwebtoken": "^9.0.0",
+ "jwks-rsa": "^3.0.1",
+ "node-forge": "^1.3.1",
+ "uuid": "^9.0.0"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "optionalDependencies": {
+ "@google-cloud/firestore": "^6.8.0",
+ "@google-cloud/storage": "^6.9.5"
+ }
+ },
"node_modules/flat-cache": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.1.1.tgz",
- "integrity": "sha512-/qM2b3LUIaIgviBQovTLvijfyOQXPtSRnRK26ksj2J7rzPIecePUIpJsZ4T02Qg+xiAEKIs5K8dsHEd+VaKa/Q==",
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz",
+ "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==",
"dev": true,
"dependencies": {
"flatted": "^3.2.9",
@@ -5209,7 +5922,7 @@
"rimraf": "^3.0.2"
},
"engines": {
- "node": ">=12.0.0"
+ "node": "^10.12.0 || >=12.0.0"
}
},
"node_modules/flat-cache/node_modules/glob": {
@@ -5401,11 +6114,6 @@
"node": ">=8"
}
},
- "node_modules/fs-minipass/node_modules/yallist": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
- "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
- },
"node_modules/fs-monkey": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.5.tgz",
@@ -5439,6 +6147,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/functional-red-black-tree": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz",
+ "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==",
+ "optional": true
+ },
"node_modules/gauge": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz",
@@ -5463,6 +6177,34 @@
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="
},
+ "node_modules/gaxios": {
+ "version": "5.1.3",
+ "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-5.1.3.tgz",
+ "integrity": "sha512-95hVgBRgEIRQQQHIbnxBXeHbW4TqFk4ZDJW7wmVtvYar72FdhRIo1UGOLS2eRAKCPEdPBWu+M7+A33D9CdX9rA==",
+ "optional": true,
+ "dependencies": {
+ "extend": "^3.0.2",
+ "https-proxy-agent": "^5.0.0",
+ "is-stream": "^2.0.0",
+ "node-fetch": "^2.6.9"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/gcp-metadata": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-5.3.0.tgz",
+ "integrity": "sha512-FNTkdNEnBdlqF2oatizolQqNANMrcqJt6AAYt99B3y1aLLC8Hc5IOBb+ZnnzllodEEf6xMBp6wRcBbc16fa65w==",
+ "optional": true,
+ "dependencies": {
+ "gaxios": "^5.0.0",
+ "json-bigint": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/generate-function": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz",
@@ -5622,6 +6364,95 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/google-auth-library": {
+ "version": "8.9.0",
+ "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-8.9.0.tgz",
+ "integrity": "sha512-f7aQCJODJFmYWN6PeNKzgvy9LI2tYmXnzpNDHEjG5sDNPgGb2FXQyTBnXeSH+PAtpKESFD+LmHw3Ox3mN7e1Fg==",
+ "optional": true,
+ "dependencies": {
+ "arrify": "^2.0.0",
+ "base64-js": "^1.3.0",
+ "ecdsa-sig-formatter": "^1.0.11",
+ "fast-text-encoding": "^1.0.0",
+ "gaxios": "^5.0.0",
+ "gcp-metadata": "^5.3.0",
+ "gtoken": "^6.1.0",
+ "jws": "^4.0.0",
+ "lru-cache": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/google-gax": {
+ "version": "3.6.1",
+ "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-3.6.1.tgz",
+ "integrity": "sha512-g/lcUjGcB6DSw2HxgEmCDOrI/CByOwqRvsuUvNalHUK2iPPPlmAIpbMbl62u0YufGMr8zgE3JL7th6dCb1Ry+w==",
+ "optional": true,
+ "dependencies": {
+ "@grpc/grpc-js": "~1.8.0",
+ "@grpc/proto-loader": "^0.7.0",
+ "@types/long": "^4.0.0",
+ "@types/rimraf": "^3.0.2",
+ "abort-controller": "^3.0.0",
+ "duplexify": "^4.0.0",
+ "fast-text-encoding": "^1.0.3",
+ "google-auth-library": "^8.0.2",
+ "is-stream-ended": "^0.1.4",
+ "node-fetch": "^2.6.1",
+ "object-hash": "^3.0.0",
+ "proto3-json-serializer": "^1.0.0",
+ "protobufjs": "7.2.4",
+ "protobufjs-cli": "1.1.1",
+ "retry-request": "^5.0.0"
+ },
+ "bin": {
+ "compileProtos": "build/tools/compileProtos.js",
+ "minifyProtoJson": "build/tools/minify.js"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/google-gax/node_modules/protobufjs": {
+ "version": "7.2.4",
+ "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.2.4.tgz",
+ "integrity": "sha512-AT+RJgD2sH8phPmCf7OUZR8xGdcJRga4+1cOaXJ64hvcSkVhNcRHOwIxUatPH15+nj59WAGTDv3LSGZPEQbJaQ==",
+ "hasInstallScript": true,
+ "optional": true,
+ "dependencies": {
+ "@protobufjs/aspromise": "^1.1.2",
+ "@protobufjs/base64": "^1.1.2",
+ "@protobufjs/codegen": "^2.0.4",
+ "@protobufjs/eventemitter": "^1.1.0",
+ "@protobufjs/fetch": "^1.1.0",
+ "@protobufjs/float": "^1.0.2",
+ "@protobufjs/inquire": "^1.1.0",
+ "@protobufjs/path": "^1.1.2",
+ "@protobufjs/pool": "^1.1.0",
+ "@protobufjs/utf8": "^1.1.0",
+ "@types/node": ">=13.7.0",
+ "long": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
+ "node_modules/google-p12-pem": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-4.0.1.tgz",
+ "integrity": "sha512-WPkN4yGtz05WZ5EhtlxNDWPhC4JIic6G8ePitwUWy4l+XPVYec+a0j0Ts47PDtW59y3RwAhUd9/h9ZZ63px6RQ==",
+ "optional": true,
+ "dependencies": {
+ "node-forge": "^1.3.1"
+ },
+ "bin": {
+ "gp12-pem": "build/src/bin/gp12-pem.js"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
"node_modules/gopd": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
@@ -5637,7 +6468,7 @@
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
- "dev": true
+ "devOptional": true
},
"node_modules/graphemer": {
"version": "1.4.0",
@@ -5645,6 +6476,20 @@
"integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
"dev": true
},
+ "node_modules/gtoken": {
+ "version": "6.1.2",
+ "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-6.1.2.tgz",
+ "integrity": "sha512-4ccGpzz7YAr7lxrT2neugmXQ3hP9ho2gcaityLVkiUecAiwiy60Ii8gRbZeOsXV19fYaRjgBSshs8kXw+NKCPQ==",
+ "optional": true,
+ "dependencies": {
+ "gaxios": "^5.0.1",
+ "google-p12-pem": "^4.0.0",
+ "jws": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
"node_modules/has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
@@ -5749,6 +6594,25 @@
"node": ">= 0.8"
}
},
+ "node_modules/http-parser-js": {
+ "version": "0.5.8",
+ "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.8.tgz",
+ "integrity": "sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q=="
+ },
+ "node_modules/http-proxy-agent": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz",
+ "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==",
+ "optional": true,
+ "dependencies": {
+ "@tootallnate/once": "2",
+ "agent-base": "6",
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
"node_modules/https-proxy-agent": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
@@ -5801,9 +6665,9 @@
]
},
"node_modules/ignore": {
- "version": "5.2.4",
- "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz",
- "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==",
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.0.tgz",
+ "integrity": "sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg==",
"dev": true,
"engines": {
"node": ">= 4"
@@ -5902,6 +6766,29 @@
"node": ">= 0.10"
}
},
+ "node_modules/ioredis": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.3.2.tgz",
+ "integrity": "sha512-1DKMMzlIHM02eBBVOFQ1+AolGjs6+xEcM4PDL7NqOS6szq7H9jSaEkIUH6/a5Hl241LzW6JLSiAbNvTQjUupUA==",
+ "dependencies": {
+ "@ioredis/commands": "^1.1.1",
+ "cluster-key-slot": "^1.1.0",
+ "debug": "^4.3.4",
+ "denque": "^2.1.0",
+ "lodash.defaults": "^4.2.0",
+ "lodash.isarguments": "^3.1.0",
+ "redis-errors": "^1.2.0",
+ "redis-parser": "^3.0.0",
+ "standard-as-callback": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=12.22.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/ioredis"
+ }
+ },
"node_modules/ip": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ip/-/ip-2.0.0.tgz",
@@ -6061,6 +6948,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/is-stream-ended": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/is-stream-ended/-/is-stream-ended-0.1.4.tgz",
+ "integrity": "sha512-xj0XPvmr7bQFTvirqnFr50o0hQIh6ZItDqloxt5aJrR4NQsYeSsyFQERYGCAzfindAcnKjINnwEEgLx4IqVzQw==",
+ "optional": true
+ },
"node_modules/is-unicode-supported": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz",
@@ -6150,6 +7043,21 @@
"node": ">=10"
}
},
+ "node_modules/istanbul-lib-report/node_modules/make-dir": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz",
+ "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==",
+ "dev": true,
+ "dependencies": {
+ "semver": "^7.5.3"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/istanbul-lib-source-maps": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz",
@@ -6826,8 +7734,16 @@
"url": "https://github.com/chalk/supports-color?sponsor=1"
}
},
- "node_modules/js-tokens": {
- "version": "4.0.0",
+ "node_modules/jose": {
+ "version": "4.15.4",
+ "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.4.tgz",
+ "integrity": "sha512-W+oqK4H+r5sITxfxpSU+MMdr/YSWGvgZMQDIsNoBDGGy4i7GBPTtvFKibQzW06n3U3TqHjhvBJsirShsEJ6eeQ==",
+ "funding": {
+ "url": "https://github.com/sponsors/panva"
+ }
+ },
+ "node_modules/js-tokens": {
+ "version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
"dev": true
@@ -6843,6 +7759,65 @@
"js-yaml": "bin/js-yaml.js"
}
},
+ "node_modules/js2xmlparser": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/js2xmlparser/-/js2xmlparser-4.0.2.tgz",
+ "integrity": "sha512-6n4D8gLlLf1n5mNLQPRfViYzu9RATblzPEtm1SthMX1Pjao0r9YI9nw7ZIfRxQMERS87mcswrg+r/OYrPRX6jA==",
+ "optional": true,
+ "dependencies": {
+ "xmlcreate": "^2.0.4"
+ }
+ },
+ "node_modules/jsdoc": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/jsdoc/-/jsdoc-4.0.2.tgz",
+ "integrity": "sha512-e8cIg2z62InH7azBBi3EsSEqrKx+nUtAS5bBcYTSpZFA+vhNPyhv8PTFZ0WsjOPDj04/dOLlm08EDcQJDqaGQg==",
+ "optional": true,
+ "dependencies": {
+ "@babel/parser": "^7.20.15",
+ "@jsdoc/salty": "^0.2.1",
+ "@types/markdown-it": "^12.2.3",
+ "bluebird": "^3.7.2",
+ "catharsis": "^0.9.0",
+ "escape-string-regexp": "^2.0.0",
+ "js2xmlparser": "^4.0.2",
+ "klaw": "^3.0.0",
+ "markdown-it": "^12.3.2",
+ "markdown-it-anchor": "^8.4.1",
+ "marked": "^4.0.10",
+ "mkdirp": "^1.0.4",
+ "requizzle": "^0.2.3",
+ "strip-json-comments": "^3.1.0",
+ "underscore": "~1.13.2"
+ },
+ "bin": {
+ "jsdoc": "jsdoc.js"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
+ "node_modules/jsdoc/node_modules/escape-string-regexp": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz",
+ "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==",
+ "optional": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/jsdoc/node_modules/mkdirp": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
+ "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
+ "optional": true,
+ "bin": {
+ "mkdirp": "bin/cmd.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/jsesc": {
"version": "2.5.2",
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz",
@@ -6855,6 +7830,15 @@
"node": ">=4"
}
},
+ "node_modules/json-bigint": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz",
+ "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==",
+ "optional": true,
+ "dependencies": {
+ "bignumber.js": "^9.0.0"
+ }
+ },
"node_modules/json-buffer": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
@@ -6868,9 +7852,9 @@
"dev": true
},
"node_modules/json-schema-traverse": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
- "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
"dev": true
},
"node_modules/json-stable-stringify-without-jsonify": {
@@ -6930,7 +7914,7 @@
"npm": ">=6"
}
},
- "node_modules/jwa": {
+ "node_modules/jsonwebtoken/node_modules/jwa": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz",
"integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==",
@@ -6940,7 +7924,7 @@
"safe-buffer": "^5.0.1"
}
},
- "node_modules/jws": {
+ "node_modules/jsonwebtoken/node_modules/jws": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz",
"integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==",
@@ -6949,6 +7933,43 @@
"safe-buffer": "^5.0.1"
}
},
+ "node_modules/jwa": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz",
+ "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==",
+ "optional": true,
+ "dependencies": {
+ "buffer-equal-constant-time": "1.0.1",
+ "ecdsa-sig-formatter": "1.0.11",
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "node_modules/jwks-rsa": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/jwks-rsa/-/jwks-rsa-3.1.0.tgz",
+ "integrity": "sha512-v7nqlfezb9YfHHzYII3ef2a2j1XnGeSE/bK3WfumaYCqONAIstJbrEGapz4kadScZzEt7zYCN7bucj8C0Mv/Rg==",
+ "dependencies": {
+ "@types/express": "^4.17.17",
+ "@types/jsonwebtoken": "^9.0.2",
+ "debug": "^4.3.4",
+ "jose": "^4.14.6",
+ "limiter": "^1.1.5",
+ "lru-memoizer": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/jws": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz",
+ "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==",
+ "optional": true,
+ "dependencies": {
+ "jwa": "^2.0.0",
+ "safe-buffer": "^5.0.1"
+ }
+ },
"node_modules/kareem": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/kareem/-/kareem-2.5.1.tgz",
@@ -6966,6 +7987,15 @@
"json-buffer": "3.0.1"
}
},
+ "node_modules/klaw": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/klaw/-/klaw-3.0.0.tgz",
+ "integrity": "sha512-0Fo5oir+O9jnXu5EefYbVK+mHMBeEVEy2cmctR1O1NECcCkPRreJKrS6Qt/j3KC2C148Dfo9i3pCmCMsdqGr0g==",
+ "optional": true,
+ "dependencies": {
+ "graceful-fs": "^4.1.9"
+ }
+ },
"node_modules/kleur": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz",
@@ -7003,9 +8033,14 @@
}
},
"node_modules/libphonenumber-js": {
- "version": "1.10.49",
- "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.10.49.tgz",
- "integrity": "sha512-gvLtyC3tIuqfPzjvYLH9BmVdqzGDiSi4VjtWe2fAgSdBf0yt8yPmbNnRIHNbR5IdtVkm0ayGuzwQKTWmU0hdjQ=="
+ "version": "1.10.51",
+ "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.10.51.tgz",
+ "integrity": "sha512-vY2I+rQwrDQzoPds0JeTEpeWzbUJgqoV0O4v31PauHBb/e+1KCXKylHcDnBMgJZ9fH9mErsEbROJY3Z3JtqEmg=="
+ },
+ "node_modules/limiter": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz",
+ "integrity": "sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA=="
},
"node_modules/lines-and-columns": {
"version": "1.2.4",
@@ -7013,6 +8048,15 @@
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
"dev": true
},
+ "node_modules/linkify-it": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-3.0.3.tgz",
+ "integrity": "sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ==",
+ "optional": true,
+ "dependencies": {
+ "uc.micro": "^1.0.1"
+ }
+ },
"node_modules/loader-runner": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz",
@@ -7042,11 +8086,32 @@
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
},
+ "node_modules/lodash.camelcase": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
+ "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==",
+ "optional": true
+ },
+ "node_modules/lodash.clonedeep": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
+ "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ=="
+ },
+ "node_modules/lodash.defaults": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
+ "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ=="
+ },
"node_modules/lodash.includes": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
"integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w=="
},
+ "node_modules/lodash.isarguments": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz",
+ "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg=="
+ },
"node_modules/lodash.isboolean": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
@@ -7135,14 +8200,39 @@
"integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q=="
},
"node_modules/lru-cache": {
- "version": "5.1.1",
- "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
- "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
- "dev": true,
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
+ "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
"dependencies": {
- "yallist": "^3.0.2"
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/lru-memoizer": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/lru-memoizer/-/lru-memoizer-2.2.0.tgz",
+ "integrity": "sha512-QfOZ6jNkxCcM/BkIPnFsqDhtrazLRsghi9mBwFAzol5GCvj4EkFT899Za3+QwikCg5sRX8JstioBDwOxEyzaNw==",
+ "dependencies": {
+ "lodash.clonedeep": "^4.5.0",
+ "lru-cache": "~4.0.0"
+ }
+ },
+ "node_modules/lru-memoizer/node_modules/lru-cache": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.0.2.tgz",
+ "integrity": "sha512-uQw9OqphAGiZhkuPlpFGmdTU2tEuhxTourM/19qGJrxBPHAr/f8BT1a0i/lOclESnGatdJG/UCkP9kZB/Lh1iw==",
+ "dependencies": {
+ "pseudomap": "^1.0.1",
+ "yallist": "^2.0.0"
}
},
+ "node_modules/lru-memoizer/node_modules/yallist": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz",
+ "integrity": "sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A=="
+ },
"node_modules/luxon": {
"version": "3.4.4",
"resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.4.tgz",
@@ -7176,20 +8266,27 @@
}
},
"node_modules/make-dir": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz",
- "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==",
- "dev": true,
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
+ "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==",
"dependencies": {
- "semver": "^7.5.3"
+ "semver": "^6.0.0"
},
"engines": {
- "node": ">=10"
+ "node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/make-dir/node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
"node_modules/make-error": {
"version": "1.3.6",
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
@@ -7205,6 +8302,50 @@
"tmpl": "1.0.5"
}
},
+ "node_modules/markdown-it": {
+ "version": "12.3.2",
+ "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-12.3.2.tgz",
+ "integrity": "sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg==",
+ "optional": true,
+ "dependencies": {
+ "argparse": "^2.0.1",
+ "entities": "~2.1.0",
+ "linkify-it": "^3.0.1",
+ "mdurl": "^1.0.1",
+ "uc.micro": "^1.0.5"
+ },
+ "bin": {
+ "markdown-it": "bin/markdown-it.js"
+ }
+ },
+ "node_modules/markdown-it-anchor": {
+ "version": "8.6.7",
+ "resolved": "https://registry.npmjs.org/markdown-it-anchor/-/markdown-it-anchor-8.6.7.tgz",
+ "integrity": "sha512-FlCHFwNnutLgVTflOYHPW2pPcl2AACqVzExlkGQNsi4CJgqOHN7YTgDd4LuhgN1BFO3TS0vLAruV1Td6dwWPJA==",
+ "optional": true,
+ "peerDependencies": {
+ "@types/markdown-it": "*",
+ "markdown-it": "*"
+ }
+ },
+ "node_modules/marked": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz",
+ "integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==",
+ "optional": true,
+ "bin": {
+ "marked": "bin/marked.js"
+ },
+ "engines": {
+ "node": ">= 12"
+ }
+ },
+ "node_modules/mdurl": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz",
+ "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==",
+ "optional": true
+ },
"node_modules/media-typer": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
@@ -7272,14 +8413,15 @@
}
},
"node_modules/mime": {
- "version": "1.6.0",
- "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
- "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz",
+ "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==",
+ "optional": true,
"bin": {
"mime": "cli.js"
},
"engines": {
- "node": ">=4"
+ "node": ">=10.0.0"
}
},
"node_modules/mime-db": {
@@ -7361,11 +8503,6 @@
"node": ">=8"
}
},
- "node_modules/minizlib/node_modules/yallist": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
- "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
- },
"node_modules/mkdirp": {
"version": "0.5.6",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
@@ -7386,9 +8523,9 @@
}
},
"node_modules/mongodb": {
- "version": "5.9.1",
- "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-5.9.1.tgz",
- "integrity": "sha512-NBGA8AfJxGPeB12F73xXwozt8ZpeIPmCUeWRwl9xejozTXFes/3zaep9zhzs1B/nKKsw4P3I4iPfXl3K7s6g+Q==",
+ "version": "5.9.2",
+ "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-5.9.2.tgz",
+ "integrity": "sha512-H60HecKO4Bc+7dhOv4sJlgvenK4fQNqqUIlXxZYQNbfEWSALGAwGoyJd/0Qwk4TttFXUOHJ2ZJQe/52ScaUwtQ==",
"optional": true,
"peer": true,
"dependencies": {
@@ -7436,37 +8573,6 @@
"whatwg-url": "^11.0.0"
}
},
- "node_modules/mongodb-connection-string-url/node_modules/tr46": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz",
- "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==",
- "dependencies": {
- "punycode": "^2.1.1"
- },
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/mongodb-connection-string-url/node_modules/webidl-conversions": {
- "version": "7.0.0",
- "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
- "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/mongodb-connection-string-url/node_modules/whatwg-url": {
- "version": "11.0.0",
- "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz",
- "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==",
- "dependencies": {
- "tr46": "^3.0.0",
- "webidl-conversions": "^7.0.0"
- },
- "engines": {
- "node": ">=12"
- }
- },
"node_modules/mongodb/node_modules/bson": {
"version": "5.5.1",
"resolved": "https://registry.npmjs.org/bson/-/bson-5.5.1.tgz",
@@ -7478,9 +8584,9 @@
}
},
"node_modules/mongoose": {
- "version": "8.0.1",
- "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.0.1.tgz",
- "integrity": "sha512-O3TJrtLCt4H1eGf2HoHGcnOcCTWloQkpmIP3hA9olybX3OX2KUjdIIq135HD5paGjZEDJYKn9fw4eH5N477zqQ==",
+ "version": "8.0.2",
+ "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.0.2.tgz",
+ "integrity": "sha512-Vsi9GzTXjdBVzheT1HZOZ2jHNzzR9Xwb5OyLz/FvDEAhlwrRnXnuqJf0QHINUOQSm7aoyvnPks0q85HJkd6yDw==",
"dependencies": {
"bson": "^6.2.0",
"kareem": "2.5.1",
@@ -7596,9 +8702,9 @@
"dev": true
},
"node_modules/mysql2": {
- "version": "3.6.3",
- "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.6.3.tgz",
- "integrity": "sha512-qYd/1CDuW1KYZjD4tzg2O8YS3X/UWuGH8ZMHyMeggMTXL3yOdMisbwZ5SNkHzDGlZXKYLAvV8tMrEH+NUMz3fw==",
+ "version": "3.6.5",
+ "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.6.5.tgz",
+ "integrity": "sha512-pS/KqIb0xlXmtmqEuTvBXTmLoQ5LmAz5NW/r8UyQ1ldvnprNEj3P9GbmuQQ2J0A4LO+ynotGi6TbscPa8OUb+w==",
"dependencies": {
"denque": "^2.1.0",
"generate-function": "^2.3.1",
@@ -7721,6 +8827,33 @@
}
}
},
+ "node_modules/node-fetch/node_modules/tr46": {
+ "version": "0.0.3",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
+ "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
+ },
+ "node_modules/node-fetch/node_modules/webidl-conversions": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
+ "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
+ },
+ "node_modules/node-fetch/node_modules/whatwg-url": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
+ "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
+ "dependencies": {
+ "tr46": "~0.0.3",
+ "webidl-conversions": "^3.0.0"
+ }
+ },
+ "node_modules/node-forge": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz",
+ "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==",
+ "engines": {
+ "node": ">= 6.13.0"
+ }
+ },
"node_modules/node-int64": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz",
@@ -7728,9 +8861,9 @@
"dev": true
},
"node_modules/node-releases": {
- "version": "2.0.13",
- "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz",
- "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==",
+ "version": "2.0.14",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz",
+ "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==",
"dev": true
},
"node_modules/nopt": {
@@ -7788,9 +8921,10 @@
}
},
"node_modules/object-hash": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz",
- "integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==",
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
+ "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
+ "optional": true,
"engines": {
"node": ">= 6"
}
@@ -7932,7 +9066,7 @@
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
"integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
- "dev": true,
+ "devOptional": true,
"dependencies": {
"yocto-queue": "^0.1.0"
},
@@ -8117,9 +9251,9 @@
}
},
"node_modules/path-scurry/node_modules/lru-cache": {
- "version": "10.0.1",
- "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.0.1.tgz",
- "integrity": "sha512-IJ4uwUTi2qCccrioU6g9g/5rvvVl13bsdczUUcqbciD9iLr095yj8DQKdObriEvuNSx325N1rV1O0sJFszx75g==",
+ "version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.1.0.tgz",
+ "integrity": "sha512-/1clY/ui8CzjKFyjdvwPWJUYKiFVXG2I2cY0ssG7h4+hwk+XOIX7ZSG9Q7TW8TW3Kp3BUSqgFWBLgL4PJ+Blag==",
"dev": true,
"engines": {
"node": "14 || >=16.14"
@@ -8254,9 +9388,9 @@
}
},
"node_modules/prettier": {
- "version": "3.0.3",
- "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.3.tgz",
- "integrity": "sha512-L/4pUDMxcNa8R/EthV08Zt42WBO4h1rarVtK0K+QJG0X187OLo7l699jWw0GKuwzkPQ//jMFA/8Xm6Fh3J/DAg==",
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.1.0.tgz",
+ "integrity": "sha512-TQLvXjq5IAibjh8EpBIkNKxO749UEWABoiIZehEPiY4GNpVdhaFKqSTu+QrlU6D2dPAfubRmtJTi4K4YkQ5eXw==",
"dev": true,
"bin": {
"prettier": "bin/prettier.cjs"
@@ -8324,6 +9458,179 @@
"node": ">= 6"
}
},
+ "node_modules/proto3-json-serializer": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/proto3-json-serializer/-/proto3-json-serializer-1.1.1.tgz",
+ "integrity": "sha512-AwAuY4g9nxx0u52DnSMkqqgyLHaW/XaPLtaAo3y/ZCfeaQB/g4YDH4kb8Wc/mWzWvu0YjOznVnfn373MVZZrgw==",
+ "optional": true,
+ "dependencies": {
+ "protobufjs": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
+ "node_modules/protobufjs": {
+ "version": "7.2.5",
+ "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.2.5.tgz",
+ "integrity": "sha512-gGXRSXvxQ7UiPgfw8gevrfRWcTlSbOFg+p/N+JVJEK5VhueL2miT6qTymqAmjr1Q5WbOCyJbyrk6JfWKwlFn6A==",
+ "hasInstallScript": true,
+ "optional": true,
+ "dependencies": {
+ "@protobufjs/aspromise": "^1.1.2",
+ "@protobufjs/base64": "^1.1.2",
+ "@protobufjs/codegen": "^2.0.4",
+ "@protobufjs/eventemitter": "^1.1.0",
+ "@protobufjs/fetch": "^1.1.0",
+ "@protobufjs/float": "^1.0.2",
+ "@protobufjs/inquire": "^1.1.0",
+ "@protobufjs/path": "^1.1.2",
+ "@protobufjs/pool": "^1.1.0",
+ "@protobufjs/utf8": "^1.1.0",
+ "@types/node": ">=13.7.0",
+ "long": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
+ "node_modules/protobufjs-cli": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/protobufjs-cli/-/protobufjs-cli-1.1.1.tgz",
+ "integrity": "sha512-VPWMgIcRNyQwWUv8OLPyGQ/0lQY/QTQAVN5fh+XzfDwsVw1FZ2L3DM/bcBf8WPiRz2tNpaov9lPZfNcmNo6LXA==",
+ "optional": true,
+ "dependencies": {
+ "chalk": "^4.0.0",
+ "escodegen": "^1.13.0",
+ "espree": "^9.0.0",
+ "estraverse": "^5.1.0",
+ "glob": "^8.0.0",
+ "jsdoc": "^4.0.0",
+ "minimist": "^1.2.0",
+ "semver": "^7.1.2",
+ "tmp": "^0.2.1",
+ "uglify-js": "^3.7.7"
+ },
+ "bin": {
+ "pbjs": "bin/pbjs",
+ "pbts": "bin/pbts"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "protobufjs": "^7.0.0"
+ }
+ },
+ "node_modules/protobufjs-cli/node_modules/brace-expansion": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
+ "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
+ "optional": true,
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/protobufjs-cli/node_modules/glob": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz",
+ "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==",
+ "optional": true,
+ "dependencies": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^5.0.1",
+ "once": "^1.3.0"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/protobufjs-cli/node_modules/minimatch": {
+ "version": "5.1.6",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
+ "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
+ "optional": true,
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/protobufjs-cli/node_modules/rimraf": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
+ "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
+ "optional": true,
+ "dependencies": {
+ "glob": "^7.1.3"
+ },
+ "bin": {
+ "rimraf": "bin.js"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/protobufjs-cli/node_modules/rimraf/node_modules/brace-expansion": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "optional": true,
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/protobufjs-cli/node_modules/rimraf/node_modules/glob": {
+ "version": "7.2.3",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
+ "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+ "optional": true,
+ "dependencies": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.1.1",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ },
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/protobufjs-cli/node_modules/rimraf/node_modules/minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "optional": true,
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/protobufjs-cli/node_modules/tmp": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz",
+ "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==",
+ "optional": true,
+ "dependencies": {
+ "rimraf": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8.17.0"
+ }
+ },
"node_modules/proxy-addr": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
@@ -8341,6 +9648,11 @@
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
},
+ "node_modules/pseudomap": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz",
+ "integrity": "sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ=="
+ },
"node_modules/pump": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
@@ -8458,24 +9770,18 @@
"dev": true
},
"node_modules/readable-stream": {
- "version": "2.3.8",
- "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
- "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
- "dependencies": {
- "core-util-is": "~1.0.0",
- "inherits": "~2.0.3",
- "isarray": "~1.0.0",
- "process-nextick-args": "~2.0.0",
- "safe-buffer": "~5.1.1",
- "string_decoder": "~1.1.1",
- "util-deprecate": "~1.0.1"
+ "version": "3.6.2",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
+ "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
+ "dependencies": {
+ "inherits": "^2.0.3",
+ "string_decoder": "^1.1.1",
+ "util-deprecate": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
}
},
- "node_modules/readable-stream/node_modules/safe-buffer": {
- "version": "5.1.2",
- "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
- "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
- },
"node_modules/readdirp": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
@@ -8500,6 +9806,25 @@
"node": ">= 0.10"
}
},
+ "node_modules/redis-errors": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz",
+ "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/redis-parser": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz",
+ "integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==",
+ "dependencies": {
+ "redis-errors": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/reflect-metadata": {
"version": "0.1.13",
"resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz",
@@ -8536,6 +9861,15 @@
"node": ">=0.10.0"
}
},
+ "node_modules/requizzle": {
+ "version": "0.2.4",
+ "resolved": "https://registry.npmjs.org/requizzle/-/requizzle-0.2.4.tgz",
+ "integrity": "sha512-JRrFk1D4OQ4SqovXOgdav+K8EAhSB/LJZqCz8tbX0KObcdeM15Ss59ozWMBWmmINMagCwmqn4ZNryUGpBsl6Jw==",
+ "optional": true,
+ "dependencies": {
+ "lodash": "^4.17.21"
+ }
+ },
"node_modules/resolve": {
"version": "1.22.8",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz",
@@ -8611,6 +9945,28 @@
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
"dev": true
},
+ "node_modules/retry": {
+ "version": "0.13.1",
+ "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz",
+ "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==",
+ "optional": true,
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/retry-request": {
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-5.0.2.tgz",
+ "integrity": "sha512-wfI3pk7EE80lCIXprqh7ym48IHYdwmAAzESdbU8Q9l7pnRCk9LEhpbOTNKjz6FARLm/Bl5m+4F0ABxOkYUujSQ==",
+ "optional": true,
+ "dependencies": {
+ "debug": "^4.1.1",
+ "extend": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/reusify": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
@@ -8800,37 +10156,6 @@
"url": "https://opencollective.com/webpack"
}
},
- "node_modules/schema-utils/node_modules/ajv": {
- "version": "6.12.6",
- "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
- "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
- "dev": true,
- "dependencies": {
- "fast-deep-equal": "^3.1.1",
- "fast-json-stable-stringify": "^2.0.0",
- "json-schema-traverse": "^0.4.1",
- "uri-js": "^4.2.2"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/epoberezkin"
- }
- },
- "node_modules/schema-utils/node_modules/ajv-keywords": {
- "version": "3.5.2",
- "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz",
- "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==",
- "dev": true,
- "peerDependencies": {
- "ajv": "^6.9.1"
- }
- },
- "node_modules/schema-utils/node_modules/json-schema-traverse": {
- "version": "0.4.1",
- "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
- "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
- "dev": true
- },
"node_modules/semver": {
"version": "7.5.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
@@ -8845,22 +10170,6 @@
"node": ">=10"
}
},
- "node_modules/semver/node_modules/lru-cache": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
- "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
- "dependencies": {
- "yallist": "^4.0.0"
- },
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/semver/node_modules/yallist": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
- "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
- },
"node_modules/send": {
"version": "0.18.0",
"resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz",
@@ -8897,6 +10206,17 @@
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
},
+ "node_modules/send/node_modules/mime": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
+ "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
+ "bin": {
+ "mime": "cli.js"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/send/node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@@ -9187,6 +10507,11 @@
"node": ">=8"
}
},
+ "node_modules/standard-as-callback": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz",
+ "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A=="
+ },
"node_modules/statuses": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
@@ -9195,6 +10520,21 @@
"node": ">= 0.8"
}
},
+ "node_modules/stream-events": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/stream-events/-/stream-events-1.0.5.tgz",
+ "integrity": "sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==",
+ "optional": true,
+ "dependencies": {
+ "stubs": "^3.0.0"
+ }
+ },
+ "node_modules/stream-shift": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz",
+ "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==",
+ "optional": true
+ },
"node_modules/streamsearch": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
@@ -9204,18 +10544,13 @@
}
},
"node_modules/string_decoder": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
- "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
+ "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
"dependencies": {
- "safe-buffer": "~5.1.0"
+ "safe-buffer": "~5.2.0"
}
},
- "node_modules/string_decoder/node_modules/safe-buffer": {
- "version": "5.1.2",
- "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
- "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
- },
"node_modules/string-length": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz",
@@ -9303,7 +10638,7 @@
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
"integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
- "dev": true,
+ "devOptional": true,
"engines": {
"node": ">=8"
},
@@ -9311,6 +10646,18 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/strnum": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz",
+ "integrity": "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==",
+ "optional": true
+ },
+ "node_modules/stubs": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz",
+ "integrity": "sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw==",
+ "optional": true
+ },
"node_modules/superagent": {
"version": "8.1.2",
"resolved": "https://registry.npmjs.org/superagent/-/superagent-8.1.2.tgz",
@@ -9409,13 +10756,13 @@
}
},
"node_modules/synckit": {
- "version": "0.8.5",
- "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.5.tgz",
- "integrity": "sha512-L1dapNV6vu2s/4Sputv8xGsCdAVlb5nRDMFU/E27D44l5U6cw1g0dGd45uLc+OXjNMmF4ntiMdCimzcjFKQI8Q==",
+ "version": "0.8.6",
+ "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.6.tgz",
+ "integrity": "sha512-laHF2savN6sMeHCjLRkheIU4wo3Zg9Ln5YOjOo7sZ5dVQW8yF5pPE5SIw1dsPhq3TRp1jisKRCdPhfs/1WMqDA==",
"dev": true,
"dependencies": {
- "@pkgr/utils": "^2.3.1",
- "tslib": "^2.5.0"
+ "@pkgr/utils": "^2.4.2",
+ "tslib": "^2.6.2"
},
"engines": {
"node": "^14.18.0 || >=16.0.0"
@@ -9424,6 +10771,12 @@
"url": "https://opencollective.com/unts"
}
},
+ "node_modules/synckit/node_modules/tslib": {
+ "version": "2.6.2",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
+ "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==",
+ "dev": true
+ },
"node_modules/tapable": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz",
@@ -9468,15 +10821,26 @@
"node": ">=10"
}
},
- "node_modules/tar/node_modules/yallist": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
- "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
+ "node_modules/teeny-request": {
+ "version": "8.0.3",
+ "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-8.0.3.tgz",
+ "integrity": "sha512-jJZpA5He2y52yUhA7pyAGZlgQpcB+xLjcN0eUFxr9c8hP/H7uOXbBNVo/O0C/xVfJLJs680jvkFgVJEEvk9+ww==",
+ "optional": true,
+ "dependencies": {
+ "http-proxy-agent": "^5.0.0",
+ "https-proxy-agent": "^5.0.0",
+ "node-fetch": "^2.6.1",
+ "stream-events": "^1.0.5",
+ "uuid": "^9.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
},
"node_modules/terser": {
- "version": "5.24.0",
- "resolved": "https://registry.npmjs.org/terser/-/terser-5.24.0.tgz",
- "integrity": "sha512-ZpGR4Hy3+wBEzVEnHvstMvqpD/nABNelQn/z2r0fjVWGQsN3bpOLzQlqDxmb4CDZnXq5lpjnQ+mHQLAOpfM5iw==",
+ "version": "5.25.0",
+ "resolved": "https://registry.npmjs.org/terser/-/terser-5.25.0.tgz",
+ "integrity": "sha512-we0I9SIsfvNUMP77zC9HG+MylwYYsGFSBG8qm+13oud2Yh+O104y614FRbyjpxys16jZwot72Fpi827YvGzuqg==",
"dev": true,
"dependencies": {
"@jridgewell/source-map": "^0.3.3",
@@ -9594,6 +10958,11 @@
"url": "https://github.com/sponsors/isaacs"
}
},
+ "node_modules/text-decoding": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/text-decoding/-/text-decoding-1.0.0.tgz",
+ "integrity": "sha512-/0TJD42KDnVwKmDK6jj3xP7E2MG7SHAOG4tyTgyUCRPdHwvkquYNLEQltmdMa3owq3TkddCVcTsoctJI8VQNKA=="
+ },
"node_modules/text-hex": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz",
@@ -9690,9 +11059,15 @@
}
},
"node_modules/tr46": {
- "version": "0.0.3",
- "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
- "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz",
+ "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==",
+ "dependencies": {
+ "punycode": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=12"
+ }
},
"node_modules/tree-kill": {
"version": "1.2.2",
@@ -9767,9 +11142,9 @@
}
},
"node_modules/ts-loader": {
- "version": "9.5.0",
- "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.5.0.tgz",
- "integrity": "sha512-LLlB/pkB4q9mW2yLdFMnK3dEHbrBjeZTYguaaIfusyojBgAGf5kF+O6KcWqiGzWqHk0LBsoolrp4VftEURhybg==",
+ "version": "9.5.1",
+ "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.5.1.tgz",
+ "integrity": "sha512-rNH3sK9kGZcH9dYzC7CewQm4NtxJTjSEVRJ2DyBZR7f8/wcta+iV44UPCXc5+nzDzivKtlzV6c9P4e+oFhDLYg==",
"dev": true,
"dependencies": {
"chalk": "^4.1.0",
@@ -9867,9 +11242,9 @@
}
},
"node_modules/tslib": {
- "version": "2.6.2",
- "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
- "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="
+ "version": "2.6.0",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.0.tgz",
+ "integrity": "sha512-7At1WUettjcSRHXCyYtTselblcHl9PJFFVKiCAy/bY97+BPZXSQ2wbq0P9s8tK2G7dFQfNnlJnPAiArVBVBsfA=="
},
"node_modules/type-check": {
"version": "0.4.0",
@@ -10100,10 +11475,15 @@
"url": "https://github.com/sponsors/isaacs"
}
},
+ "node_modules/typeorm/node_modules/tslib": {
+ "version": "2.6.2",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
+ "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="
+ },
"node_modules/typescript": {
- "version": "5.2.2",
- "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz",
- "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==",
+ "version": "5.3.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz",
+ "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==",
"devOptional": true,
"bin": {
"tsc": "bin/tsc",
@@ -10113,6 +11493,24 @@
"node": ">=14.17"
}
},
+ "node_modules/uc.micro": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz",
+ "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==",
+ "optional": true
+ },
+ "node_modules/uglify-js": {
+ "version": "3.17.4",
+ "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.4.tgz",
+ "integrity": "sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==",
+ "optional": true,
+ "bin": {
+ "uglifyjs": "bin/uglifyjs"
+ },
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
"node_modules/uid": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/uid/-/uid-2.0.2.tgz",
@@ -10124,6 +11522,12 @@
"node": ">=8"
}
},
+ "node_modules/underscore": {
+ "version": "1.13.6",
+ "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.6.tgz",
+ "integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==",
+ "optional": true
+ },
"node_modules/undici-types": {
"version": "5.26.5",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
@@ -10208,13 +11612,9 @@
}
},
"node_modules/uuid": {
- "version": "9.0.1",
- "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
- "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
- "funding": [
- "https://github.com/sponsors/broofa",
- "https://github.com/sponsors/ctavan"
- ],
+ "version": "9.0.0",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz",
+ "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==",
"bin": {
"uuid": "dist/bin/uuid"
}
@@ -10226,9 +11626,9 @@
"devOptional": true
},
"node_modules/v8-to-istanbul": {
- "version": "9.1.3",
- "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.1.3.tgz",
- "integrity": "sha512-9lDD+EVI2fjFsMWXc6dy5JJzBsVTcQ2fVkfBvncZ6xJWG9wtBhOldG+mHkSL0+V1K/xgZz0JDO5UT5hFwHUghg==",
+ "version": "9.2.0",
+ "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.2.0.tgz",
+ "integrity": "sha512-/EH/sDgxU2eGxajKdwLCDmQ4FWq+kpi3uCmBGpw1xJtnAxEjlD8j8PEiGWpCIMIs3ciNAgH0d3TTJiUkYzyZjA==",
"dev": true,
"dependencies": {
"@jridgewell/trace-mapping": "^0.3.12",
@@ -10287,9 +11687,12 @@
}
},
"node_modules/webidl-conversions": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
- "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
+ "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
+ "engines": {
+ "node": ">=12"
+ }
},
"node_modules/webpack": {
"version": "5.89.0",
@@ -10378,13 +11781,37 @@
"node": ">=4.0"
}
},
+ "node_modules/websocket-driver": {
+ "version": "0.7.4",
+ "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz",
+ "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==",
+ "dependencies": {
+ "http-parser-js": ">=0.5.1",
+ "safe-buffer": ">=5.1.0",
+ "websocket-extensions": ">=0.1.1"
+ },
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
+ "node_modules/websocket-extensions": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz",
+ "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==",
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
"node_modules/whatwg-url": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
- "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
+ "version": "11.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz",
+ "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==",
"dependencies": {
- "tr46": "~0.0.3",
- "webidl-conversions": "^3.0.0"
+ "tr46": "^3.0.0",
+ "webidl-conversions": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=12"
}
},
"node_modules/which": {
@@ -10516,6 +11943,14 @@
"winston": "^3"
}
},
+ "node_modules/winston-daily-rotate-file/node_modules/object-hash": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz",
+ "integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
"node_modules/winston-transport": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.6.0.tgz",
@@ -10529,19 +11964,6 @@
"node": ">= 12.0.0"
}
},
- "node_modules/winston-transport/node_modules/readable-stream": {
- "version": "3.6.2",
- "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
- "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
- "dependencies": {
- "inherits": "^2.0.3",
- "string_decoder": "^1.1.1",
- "util-deprecate": "^1.0.1"
- },
- "engines": {
- "node": ">= 6"
- }
- },
"node_modules/winston/node_modules/@colors/colors": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz",
@@ -10550,17 +11972,13 @@
"node": ">=0.1.90"
}
},
- "node_modules/winston/node_modules/readable-stream": {
- "version": "3.6.2",
- "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
- "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
- "dependencies": {
- "inherits": "^2.0.3",
- "string_decoder": "^1.1.1",
- "util-deprecate": "^1.0.1"
- },
+ "node_modules/word-wrap": {
+ "version": "1.2.5",
+ "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
+ "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
+ "optional": true,
"engines": {
- "node": ">= 6"
+ "node": ">=0.10.0"
}
},
"node_modules/wrap-ansi": {
@@ -10630,6 +12048,12 @@
"xml-js": "bin/cli.js"
}
},
+ "node_modules/xmlcreate": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/xmlcreate/-/xmlcreate-2.0.4.tgz",
+ "integrity": "sha512-nquOebG4sngPmGPICTS5EnxqhKbCmz5Ox5hsszI2T6U5qdrJizBc+0ilYSEjTSzU0yZcmvppztXe/5Al5fUwdg==",
+ "optional": true
+ },
"node_modules/xtend": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
@@ -10647,10 +12071,9 @@
}
},
"node_modules/yallist": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
- "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
- "dev": true
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
},
"node_modules/yargs": {
"version": "17.7.2",
@@ -10690,7 +12113,7 @@
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
"integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
- "dev": true,
+ "devOptional": true,
"engines": {
"node": ">=10"
},
diff --git a/backend/package.json b/backend/package.json
index 4d76812..e1b6048 100644
--- a/backend/package.json
+++ b/backend/package.json
@@ -30,12 +30,15 @@
"@nestjs/schedule": "^4.0.0",
"@nestjs/swagger": "^7.1.16",
"@nestjs/typeorm": "^10.0.1",
+ "@songkeys/nestjs-redis": "^10.0.0",
"@types/passport-jwt": "^3.0.13",
"axios": "^1.6.2",
"bcrypt": "^5.1.1",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.0",
+ "firebase-admin": "^11.11.1",
"iconv-lite": "^0.6.3",
+ "ioredis": "^5.3.2",
"mongoose": "^8.0.1",
"mysql2": "^3.6.3",
"nest-winston": "^1.9.4",
@@ -53,13 +56,15 @@
"devDependencies": {
"@nestjs/cli": "^10.0.0",
"@nestjs/schematics": "^10.0.0",
- "@nestjs/testing": "^10.0.0",
+ "@nestjs/testing": "^10.2.10",
"@types/bcrypt": "^5.0.2",
+ "@types/dotenv": "^8.2.0",
"@types/express": "^4.17.17",
"@types/jest": "^29.5.2",
"@types/node": "^20.3.1",
"@types/passport-local": "^1.0.38",
- "@types/supertest": "^2.0.12",
+ "@types/supertest": "^2.0.16",
+ "@types/winston": "^2.4.4",
"@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0",
"cross-env": "^7.0.3",
diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts
index 28ed758..b2e669d 100644
--- a/backend/src/app.module.ts
+++ b/backend/src/app.module.ts
@@ -12,6 +12,8 @@ import { LoggerMiddleware } from './middlewares/logger.middleware';
import { MongooseModule } from '@nestjs/mongoose';
import { MONGODB_URL } from './constants';
import { ScheduleModule } from '@nestjs/schedule';
+import { RedisModule } from '@songkeys/nestjs-redis';
+import { RedisConfig } from './configs/redis.config';
@Module({
imports: [
@@ -22,6 +24,7 @@ import { ScheduleModule } from '@nestjs/schedule';
ProductModule,
MongooseModule.forRoot(MONGODB_URL),
ScheduleModule.forRoot(),
+ RedisModule.forRoot(RedisConfig),
],
controllers: [AppController],
providers: [AppService],
diff --git a/backend/src/auth/auth.module.ts b/backend/src/auth/auth.module.ts
index 1148abf..09143ef 100644
--- a/backend/src/auth/auth.module.ts
+++ b/backend/src/auth/auth.module.ts
@@ -4,15 +4,12 @@ import { UsersModule } from 'src/user/user.module';
import { JwtModule } from '@nestjs/jwt';
import { AccessJwtStrategy, RefreshJwtStrategy } from './jwt/jwt.strategy';
import { AuthController } from './auth.controller';
-import { JWTRepository } from './jwt/jwt.repository';
-import { TypeOrmModule } from '@nestjs/typeorm';
-import { Token } from 'src/entities/token.entity';
import { JWTService } from './jwt/jwt.service';
@Module({
- imports: [TypeOrmModule.forFeature([Token]), forwardRef(() => UsersModule), JwtModule.register({})],
+ imports: [forwardRef(() => UsersModule), JwtModule.register({})],
controllers: [AuthController],
- providers: [AuthService, JWTService, JWTRepository, AccessJwtStrategy, RefreshJwtStrategy],
+ providers: [AuthService, JWTService, AccessJwtStrategy, RefreshJwtStrategy],
exports: [AuthService],
})
export class AuthModule {}
diff --git a/backend/src/auth/auth.service.ts b/backend/src/auth/auth.service.ts
index 3cc5b69..e441998 100644
--- a/backend/src/auth/auth.service.ts
+++ b/backend/src/auth/auth.service.ts
@@ -5,13 +5,15 @@ import { User } from 'src/entities/user.entity';
import { ValidationException } from 'src/exceptions/validation.exception';
import { JwtService } from '@nestjs/jwt';
import { ACCESS_TOKEN_SECRETS, REFRESH_TOKEN_SECRETS } from 'src/constants';
-import { JWTRepository } from './jwt/jwt.repository';
+import { InjectRedis } from '@songkeys/nestjs-redis';
+import Redis from 'ioredis';
+
@Injectable()
export class AuthService {
constructor(
@Inject(forwardRef(() => UsersService)) private usersService: UsersService,
private readonly jwtService: JwtService,
- private jwtRepository: JWTRepository,
+ @InjectRedis() private readonly redis: Redis,
) {}
async getAccessToken(user: User): Promise {
@@ -23,7 +25,8 @@ export class AuthService {
async getRefreshToken(user: User): Promise {
const refreshToken = this.jwtService.sign({ id: user.id }, { secret: REFRESH_TOKEN_SECRETS, expiresIn: '2w' });
- return this.jwtRepository.saveToken(user.id, refreshToken);
+ await this.redis.set(`refreshToken:${user.id}`, refreshToken);
+ return refreshToken;
}
async validateUser(email: string, password: string): Promise> {
@@ -47,4 +50,13 @@ export class AuthService {
refreshToken: await this.getRefreshToken(user),
};
}
+
+ async addFirebaseToken(userId: string, token: string) {
+ try {
+ await this.redis.set(`firebaseToken:${userId}`, token);
+ return token;
+ } catch (e) {
+ throw new HttpException('Firebase 토큰 등록 실패', HttpStatus.INTERNAL_SERVER_ERROR);
+ }
+ }
}
diff --git a/backend/src/auth/jwt/jwt.service.ts b/backend/src/auth/jwt/jwt.service.ts
index 0f8a03d..aec6b10 100644
--- a/backend/src/auth/jwt/jwt.service.ts
+++ b/backend/src/auth/jwt/jwt.service.ts
@@ -1,28 +1,26 @@
-import { InjectRepository } from '@nestjs/typeorm';
-import { JWTRepository } from './jwt.repository';
-import { Token } from 'src/entities/token.entity';
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { INVALIDATED_REFRESHTOKEN, REFRESH_TOKEN_SECRETS } from 'src/constants';
+import Redis from 'ioredis';
+import { InjectRedis } from '@songkeys/nestjs-redis';
@Injectable()
export class JWTService {
constructor(
- @InjectRepository(JWTRepository)
- private jwtRepository: JWTRepository,
private jwtService: JwtService,
+ @InjectRedis() private readonly redis: Redis,
) {}
async validateRefreshToken(userId: string, payload: string) {
if (!(await this.isLatestRefreshToken(userId, payload))) {
- this.jwtRepository.saveToken(userId, INVALIDATED_REFRESHTOKEN);
+ await this.redis.set(`refreshToken:${userId}`, JSON.stringify({ INVALIDATED_REFRESHTOKEN }));
throw new UnauthorizedException('이미 재발급된 refreshToken');
}
}
async isLatestRefreshToken(userId: string, payload: string) {
const tokenInfo = await this.findOne(userId);
- const latestRefreshToken = tokenInfo?.token;
+ const latestRefreshToken = tokenInfo;
const token = this.jwtService.sign(payload, { secret: REFRESH_TOKEN_SECRETS });
if (latestRefreshToken !== token) {
return false;
@@ -30,8 +28,8 @@ export class JWTService {
return true;
}
- async findOne(userId: string): Promise {
- const refreshToken = await this.jwtRepository.findOne({ where: { userId: userId } });
+ async findOne(userId: string): Promise {
+ const refreshToken = await this.redis.get(`refreshToken:${userId}`);
return refreshToken;
}
}
diff --git a/backend/src/configs/redis.config.ts b/backend/src/configs/redis.config.ts
new file mode 100644
index 0000000..79f0b70
--- /dev/null
+++ b/backend/src/configs/redis.config.ts
@@ -0,0 +1,10 @@
+import { RedisModuleOptions } from '@songkeys/nestjs-redis';
+import { REDIS_HOST, REDIS_PASSWORD, REDIS_PORT } from 'src/constants';
+
+export const RedisConfig: RedisModuleOptions = {
+ config: {
+ host: REDIS_HOST,
+ port: REDIS_PORT,
+ password: REDIS_PASSWORD,
+ },
+};
diff --git a/backend/src/constants.ts b/backend/src/constants.ts
index caf4816..01f935f 100644
--- a/backend/src/constants.ts
+++ b/backend/src/constants.ts
@@ -17,7 +17,21 @@ export const BASE_URL_11ST = process.env.BASE_URL_11ST as string;
export const MAX_TRACKING_RANK = parseInt(process.env.MAX_TRACKING_RANK || '50');
export const INVALIDATED_REFRESHTOKEN = 'invalidate refreshToken';
export const MONGODB_URL = process.env.MONGODB_URL as string;
-export const KR_OFFSET = 9 * 60 * 60 * 1000;
export const THIRTY_DAYS = 30;
export const NINETY_DAYS = 90;
export const NO_CACHE = 0;
+export const REDIS_HOST = process.env.REDIS_HOST;
+export const REDIS_PORT = parseInt(process.env.REDIS_PORT || '6379');
+export const REDIS_PASSWORD = process.env.REDIS_PASSWORD;
+export const TYPE = process.env.TYPE;
+export const PROJECT_ID = process.env.PROJECT_ID;
+export const PRIVATE_KEY_ID = process.env.PRIVATE_KEY_ID;
+export const PRIVATE_KEY = process.env.PRIVATE_KEY as string;
+export const CLIENT_EMAIL = process.env.CLIENT_EMAIL;
+export const CLIENT_ID = process.env.CLIENT_ID;
+export const AUTH_URI = process.env.AUTH_URI;
+export const TOKEN_URI = process.env.TOKEN_URI;
+export const AUTH_PROVIDER_X509_CERT_URL = process.env.AUTH_PROVIDER_X509_CERT_URL;
+export const CLIENT_X509_CERT_URL = process.env.CLIENT_X509_CERT_URL;
+export const UNIVERSE_DOMAIN = process.env.UNIVERSE_DOMAIN;
+export const CHANNEL_ID = process.env.CHANNEL_ID;
diff --git a/backend/src/cron/cron.service.ts b/backend/src/cron/cron.service.ts
new file mode 100644
index 0000000..6598dff
--- /dev/null
+++ b/backend/src/cron/cron.service.ts
@@ -0,0 +1,160 @@
+import { Injectable } from '@nestjs/common';
+import { InjectRepository } from '@nestjs/typeorm';
+import { ProductInfoDto } from 'src/dto/product.info.dto';
+import { TrackingProductRepository } from '../product/trackingProduct.repository';
+import { ProductRepository } from '../product/product.repository';
+import { getProductInfo11st } from 'src/utils/openapi.11st';
+import { InjectModel } from '@nestjs/mongoose';
+import { ProductPrice } from 'src/schema/product.schema';
+import { Model } from 'mongoose';
+import { CHANNEL_ID } from 'src/constants';
+import { Cron } from '@nestjs/schedule';
+import { FirebaseService } from '../firebase/firebase.service';
+import { Message } from 'firebase-admin/lib/messaging/messaging-api';
+import { TrackingProduct } from 'src/entities/trackingProduct.entity';
+import Redis from 'ioredis';
+import { InjectRedis } from '@songkeys/nestjs-redis';
+
+@Injectable()
+export class CronService {
+ constructor(
+ @InjectRepository(TrackingProductRepository)
+ private trackingProductRepository: TrackingProductRepository,
+ @InjectRepository(ProductRepository)
+ private productRepository: ProductRepository,
+ @InjectModel(ProductPrice.name)
+ private productPriceModel: Model,
+ @InjectRedis() private readonly redis: Redis,
+ private readonly firebaseService: FirebaseService,
+ ) {}
+
+ private isDefined = (x: T | undefined): x is T => x !== undefined;
+
+ @Cron('* */10 * * * *')
+ async cyclicPriceChecker() {
+ const totalProducts = await this.productRepository.find({ select: { id: true, productCode: true } });
+ const recentProductInfo = await Promise.all(
+ totalProducts.map(({ productCode, id }) => getProductInfo11st(productCode, id)),
+ );
+ const checkProducts = await Promise.all(recentProductInfo.map((data) => this.getUpdatedProduct(data)));
+ const updatedProducts = checkProducts.filter(this.isDefined);
+ if (updatedProducts.length > 0) {
+ await this.productPriceModel.insertMany(
+ updatedProducts.map(({ productId, productPrice, isSoldOut }) => {
+ return { productId, price: productPrice, isSoldOut };
+ }),
+ );
+ await this.pushNotifications(updatedProducts);
+ }
+ }
+
+ async getUpdatedProduct(data: ProductInfoDto) {
+ const { productId, productPrice, isSoldOut } = data;
+ const cacheData = await this.redis.get(`product:${productId}`);
+ const cache = JSON.parse(cacheData as string);
+ if (!cache || cache.isSoldOut !== isSoldOut || cache.price !== productPrice) {
+ const lowestPrice = cache ? Math.min(cache.lowestPrice, productPrice) : productPrice;
+ await this.redis.set(
+ `product:${productId}`,
+ JSON.stringify({
+ isSoldOut,
+ price: productPrice,
+ lowestPrice,
+ }),
+ );
+ return data;
+ }
+ }
+
+ async pushNotifications(updatedProducts: ProductInfoDto[]) {
+ const { messages, products } = await this.getNotifications(updatedProducts);
+ if (messages.length === 0) return;
+ const { responses } = await this.firebaseService.getMessaging().sendEach(messages);
+ const successProducts = products.filter((item, index) => {
+ const { success } = responses[index];
+ item.isFirst = false;
+ return success;
+ });
+ if (successProducts.length > 0) {
+ await this.trackingProductRepository.save(successProducts);
+ }
+ }
+ async getNotifications(
+ productInfo: ProductInfoDto[],
+ ): Promise<{ messages: Message[]; products: TrackingProduct[] }> {
+ const productIds = productInfo.map((p) => p.productId);
+
+ const trackingProducts = await this.trackingProductRepository
+ .createQueryBuilder('tracking_product')
+ .where('tracking_product.productId IN (:...productIds)', { productIds })
+ .getMany();
+
+ const trackingMap = new Map();
+ trackingProducts.forEach((tracking) => {
+ const products = trackingMap.get(tracking.productId) || [];
+ trackingMap.set(tracking.productId, [...products, tracking]);
+ });
+ const results = await Promise.all(
+ productInfo.map(async (product) => {
+ const trackingList = product.productId ? trackingMap.get(product.productId) || [] : [];
+ return await this.findMatchedProducts(trackingList, product);
+ }),
+ );
+
+ const allNotifications = results.flatMap((result) => result.notifications);
+ const allMatchedProducts = results.flatMap((result) => result.matchedProducts);
+
+ return {
+ messages: allNotifications,
+ products: allMatchedProducts,
+ };
+ }
+
+ async findMatchedProducts(trackingList: TrackingProduct[], product: ProductInfoDto) {
+ const { productPrice, productCode, productName, imageUrl } = product;
+ const notifications = [];
+ const matchedProducts = [];
+
+ for (const trackingProduct of trackingList) {
+ const { userId, targetPrice, isFirst, isAlert } = trackingProduct;
+ if (!isFirst && targetPrice < productPrice) {
+ trackingProduct.isFirst = true;
+ await this.trackingProductRepository.save(trackingProduct);
+ } else if (targetPrice >= productPrice && isFirst && isAlert) {
+ const firebaseToken = await this.redis.get(`firebaseToken:${userId}`);
+ if (firebaseToken) {
+ notifications.push(
+ this.getMessage(productCode, productName, productPrice, imageUrl, firebaseToken),
+ );
+ matchedProducts.push(trackingProduct);
+ }
+ }
+ }
+ return { notifications, matchedProducts };
+ }
+
+ private getMessage(
+ productCode: string,
+ productName: string,
+ productPrice: number,
+ imageUrl: string,
+ token: string,
+ ): Message {
+ return {
+ notification: {
+ title: '목표 가격 이하로 내려갔습니다!',
+ body: `${productName}의 현재 가격은 ${productPrice}원 입니다.`,
+ },
+ data: {
+ productCode,
+ },
+ android: {
+ notification: {
+ channelId: CHANNEL_ID,
+ imageUrl,
+ },
+ },
+ token,
+ };
+ }
+}
diff --git a/backend/src/dto/firebase.token.dto.ts b/backend/src/dto/firebase.token.dto.ts
new file mode 100644
index 0000000..37a5a36
--- /dev/null
+++ b/backend/src/dto/firebase.token.dto.ts
@@ -0,0 +1,12 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { IsString } from 'class-validator';
+
+export class FirebaseTokenDto {
+ @ApiProperty({
+ example: 'firebase cloud messaging registeration token',
+ description: 'FCM 등록 토큰',
+ required: true,
+ })
+ @IsString()
+ token: string;
+}
diff --git a/backend/src/dto/product.cache.dto.ts b/backend/src/dto/product.cache.dto.ts
new file mode 100644
index 0000000..ac48185
--- /dev/null
+++ b/backend/src/dto/product.cache.dto.ts
@@ -0,0 +1,7 @@
+export class ProductCacheDto {
+ price: number;
+ isSoldOut: boolean;
+ lowestPrice: number;
+ userCount: number;
+ updateAt: Date;
+}
diff --git a/backend/src/dto/product.info.dto.ts b/backend/src/dto/product.info.dto.ts
index a5d3036..fae2e9b 100644
--- a/backend/src/dto/product.info.dto.ts
+++ b/backend/src/dto/product.info.dto.ts
@@ -4,4 +4,6 @@ export class ProductInfoDto {
productPrice: number;
shop: string;
imageUrl: string;
+ isSoldOut: boolean;
+ productId?: string;
}
diff --git a/backend/src/dto/product.rank.cache.dto.ts b/backend/src/dto/product.rank.cache.dto.ts
new file mode 100644
index 0000000..ce42f7c
--- /dev/null
+++ b/backend/src/dto/product.rank.cache.dto.ts
@@ -0,0 +1,8 @@
+export class ProductRankCacheDto {
+ id: string;
+ productName: string;
+ productCode: string;
+ shop: string;
+ imageUrl: string;
+ userCount: number;
+}
diff --git a/backend/src/dto/product.swagger.dto.ts b/backend/src/dto/product.swagger.dto.ts
index a043679..663dbaf 100644
--- a/backend/src/dto/product.swagger.dto.ts
+++ b/backend/src/dto/product.swagger.dto.ts
@@ -140,6 +140,7 @@ const trackingProductListExample = [
imageUrl: 'https://cdn.011st.com/11dims/strip/false/11src/asin/B091516D2Z/B.jpg?1700527038699',
targetPrice: 30000,
price: 20000,
+ isAlert: true,
priceData: priceDataExample,
},
{
@@ -149,6 +150,7 @@ const trackingProductListExample = [
imageUrl: 'https://cdn.011st.com/11dims/strip/false/11src/asin/B01HZ0YT2C/B.jpg?1700390686058',
targetPrice: 20000,
price: 15000,
+ isAlert: true,
priceData: priceDataExample,
},
];
@@ -354,3 +356,15 @@ export class TrackingProductsNotFound {
})
message: string;
}
+export class ToggleAlertSuccess {
+ @ApiProperty({
+ example: HttpStatus.OK,
+ description: 'Http 상태 코드',
+ })
+ statusCode: number;
+ @ApiProperty({
+ example: '알림 설정 성공',
+ description: '메시지',
+ })
+ message: string;
+}
diff --git a/backend/src/dto/product.tracking.dto.ts b/backend/src/dto/product.tracking.dto.ts
index 2e2ea64..7b28f99 100644
--- a/backend/src/dto/product.tracking.dto.ts
+++ b/backend/src/dto/product.tracking.dto.ts
@@ -7,5 +7,6 @@ export class TrackingProductDto {
imageUrl: string;
targetPrice: number;
price: number;
+ isAlert: boolean;
priceData: PriceDataDto[];
}
diff --git a/backend/src/dto/user.swagger.dto.ts b/backend/src/dto/user.swagger.dto.ts
index 3c33f1d..a3460f0 100644
--- a/backend/src/dto/user.swagger.dto.ts
+++ b/backend/src/dto/user.swagger.dto.ts
@@ -85,3 +85,16 @@ export class LoginFailError {
})
message: string;
}
+
+export class FirebaseTokenSuccess {
+ @ApiProperty({
+ example: HttpStatus.OK,
+ description: 'Http 상태 코드',
+ })
+ statusCode: number;
+ @ApiProperty({
+ example: 'FCM 토큰 등록 성공',
+ description: '메시지',
+ })
+ message: string;
+}
diff --git a/backend/src/entities/trackingProduct.entity.ts b/backend/src/entities/trackingProduct.entity.ts
index 1f3c09d..89e6ba9 100644
--- a/backend/src/entities/trackingProduct.entity.ts
+++ b/backend/src/entities/trackingProduct.entity.ts
@@ -4,15 +4,21 @@ import { Product } from './product.entity';
@Entity('tracking_product')
export class TrackingProduct extends BaseEntity {
- @PrimaryColumn({ type: 'char', length: 36 })
+ @PrimaryColumn({ type: 'char', length: 36, nullable: false })
userId: string;
- @PrimaryColumn({ type: 'varchar', length: 36 })
+ @PrimaryColumn({ type: 'varchar', length: 36, nullable: false })
productId: string;
- @Column({ type: 'int' })
+ @Column({ type: 'int', nullable: false })
targetPrice: number;
+ @Column({ type: 'boolean', nullable: false })
+ isFirst: boolean = true;
+
+ @Column({ type: 'boolean', nullable: false })
+ isAlert: boolean = true;
+
@ManyToOne(() => User, (user) => user.id)
user: User;
diff --git a/backend/src/exceptions/exception.fillter.ts b/backend/src/exceptions/exception.fillter.ts
index 3298646..d6e18df 100644
--- a/backend/src/exceptions/exception.fillter.ts
+++ b/backend/src/exceptions/exception.fillter.ts
@@ -19,7 +19,7 @@ export class UserExceptionFilter implements ExceptionFilter {
this.setResponse(response, exception.getStatus(), exception.getMessage());
return;
}
- this.setResponse(response, HttpStatus.INTERNAL_SERVER_ERROR, '서버 내부 에러');
+ this.setResponse(response, exception.getStatus(), exception.message);
}
private setResponse(response: Response, statusCode: number, msg: string) {
diff --git a/backend/src/firebase/firebase.config.ts b/backend/src/firebase/firebase.config.ts
new file mode 100644
index 0000000..7984d0c
--- /dev/null
+++ b/backend/src/firebase/firebase.config.ts
@@ -0,0 +1,27 @@
+import {
+ AUTH_PROVIDER_X509_CERT_URL,
+ AUTH_URI,
+ CLIENT_EMAIL,
+ CLIENT_ID,
+ CLIENT_X509_CERT_URL,
+ PRIVATE_KEY,
+ PRIVATE_KEY_ID,
+ PROJECT_ID,
+ TOKEN_URI,
+ TYPE,
+ UNIVERSE_DOMAIN,
+} from 'src/constants';
+
+export const serviceAccount = {
+ type: TYPE,
+ project_id: PROJECT_ID,
+ private_key_id: PRIVATE_KEY_ID,
+ private_key: PRIVATE_KEY.replace(/\\n/g, '\n'),
+ client_email: CLIENT_EMAIL,
+ client_id: CLIENT_ID,
+ auth_uri: AUTH_URI,
+ token_uri: TOKEN_URI,
+ auth_provider_x509_cert_url: AUTH_PROVIDER_X509_CERT_URL,
+ client_x509_cert_url: CLIENT_X509_CERT_URL,
+ universe_domain: UNIVERSE_DOMAIN,
+};
diff --git a/backend/src/firebase/firebase.service.ts b/backend/src/firebase/firebase.service.ts
new file mode 100644
index 0000000..8184b6c
--- /dev/null
+++ b/backend/src/firebase/firebase.service.ts
@@ -0,0 +1,16 @@
+import { Injectable } from '@nestjs/common';
+import * as admin from 'firebase-admin';
+import { serviceAccount } from './firebase.config';
+
+@Injectable()
+export class FirebaseService {
+ constructor() {
+ admin.initializeApp({
+ credential: admin.credential.cert(serviceAccount as admin.ServiceAccount),
+ });
+ }
+
+ getMessaging() {
+ return admin.messaging();
+ }
+}
diff --git a/backend/src/product/product.controller.ts b/backend/src/product/product.controller.ts
index 3cdc854..c89c84b 100644
--- a/backend/src/product/product.controller.ts
+++ b/backend/src/product/product.controller.ts
@@ -43,11 +43,11 @@ import {
AddProductConflict,
ProductDetailsSuccess,
AddProductSuccess,
+ ToggleAlertSuccess,
} from 'src/dto/product.swagger.dto';
import { User } from 'src/entities/user.entity';
import { AuthGuard } from '@nestjs/passport';
import { HttpExceptionFilter } from 'src/exceptions/http.exception.filter';
-import { ProductPriceDto } from 'src/dto/product.price.dto';
import { ExpiredTokenError } from 'src/dto/auth.swagger.dto';
@ApiBearerAuth()
@@ -159,9 +159,13 @@ export class ProductController {
return { statusCode: HttpStatus.OK, message: '추적 상품 삭제 성공' };
}
- @Post('/mongoTest')
- async testMongo(@Body() productPriceDto: ProductPriceDto) {
- await this.productService.mongo(productPriceDto);
- return { statusCode: HttpStatus.OK, message: 'mongoDB 연결 테스트 성공' };
+ @ApiOperation({ summary: '추적 상품 알림 설정 API', description: '추적 상품에 대한 알림을 설정한다.' })
+ @ApiOkResponse({ type: ToggleAlertSuccess, description: '알림 설정 성공' })
+ @ApiNotFoundResponse({ type: TrackingProductsNotFound, description: '추적 상품 찾을 수 없음' })
+ @ApiBadRequestResponse({ type: RequestError, description: '잘못된 요청입니다.' })
+ @Patch('/alert/:productCode')
+ async toggleAlert(@Req() req: Request & { user: User }, @Param('productCode') productCode: string) {
+ await this.productService.toggleProductAlert(req.user.id, productCode);
+ return { statusCode: HttpStatus.OK, message: '알림 설정 성공' };
}
}
diff --git a/backend/src/product/product.module.ts b/backend/src/product/product.module.ts
index 2883a10..0887cc0 100644
--- a/backend/src/product/product.module.ts
+++ b/backend/src/product/product.module.ts
@@ -8,6 +8,8 @@ import { ProductRepository } from './product.repository';
import { TrackingProductRepository } from './trackingProduct.repository';
import { MongooseModule } from '@nestjs/mongoose';
import { ProductPrice, ProductPriceSchema } from 'src/schema/product.schema';
+import { FirebaseService } from 'src/firebase/firebase.service';
+import { CronService } from 'src/cron/cron.service';
@Module({
imports: [
@@ -15,6 +17,6 @@ import { ProductPrice, ProductPriceSchema } from 'src/schema/product.schema';
MongooseModule.forFeature([{ name: ProductPrice.name, schema: ProductPriceSchema }]),
],
controllers: [ProductController],
- providers: [ProductService, ProductRepository, TrackingProductRepository],
+ providers: [ProductService, ProductRepository, TrackingProductRepository, FirebaseService, CronService],
})
export class ProductModule {}
diff --git a/backend/src/product/product.repository.ts b/backend/src/product/product.repository.ts
index ddefde8..6955f8e 100644
--- a/backend/src/product/product.repository.ts
+++ b/backend/src/product/product.repository.ts
@@ -5,6 +5,8 @@ import { Product } from 'src/entities/product.entity';
import { createUrl11st } from 'src/utils/openapi.11st';
import { ProductDto } from 'src/dto/product.dto';
import { ProductInfoDto } from 'src/dto/product.info.dto';
+import { MAX_TRACKING_RANK } from 'src/constants';
+import { TrackingProduct } from 'src/entities/trackingProduct.entity';
@Injectable()
export class ProductRepository extends Repository {
@@ -22,4 +24,24 @@ export class ProductRepository extends Repository {
await newProduct.save();
return newProduct;
}
+
+ async getTotalInfoRankingList() {
+ const recommendList = await this.repository
+ .createQueryBuilder('product')
+ .select([
+ 'product.id as id',
+ 'COUNT(tracking_product.userId) as userCount',
+ 'product.productName as productName',
+ 'product.productCode as productCode',
+ 'product.shop as shop',
+ 'product.imageUrl as imageUrl',
+ ])
+ .leftJoin(TrackingProduct, 'tracking_product', 'tracking_product.productId = product.id')
+ .groupBy('product.id')
+ .orderBy('userCount', 'DESC')
+ .addOrderBy('MAX(product.id)', 'DESC')
+ .limit(MAX_TRACKING_RANK)
+ .getRawMany();
+ return recommendList;
+ }
}
diff --git a/backend/src/product/product.service.ts b/backend/src/product/product.service.ts
index 4ae7d09..739f4df 100644
--- a/backend/src/product/product.service.ts
+++ b/backend/src/product/product.service.ts
@@ -11,16 +11,18 @@ import { ProductDetailsDto } from 'src/dto/product.details.dto';
import { InjectModel } from '@nestjs/mongoose';
import { ProductPrice } from 'src/schema/product.schema';
import { Model } from 'mongoose';
-import { ProductPriceDto } from 'src/dto/product.price.dto';
import { PriceDataDto } from 'src/dto/price.data.dto';
-import { KR_OFFSET, NINETY_DAYS, NO_CACHE, THIRTY_DAYS } from 'src/constants';
-import { Cron } from '@nestjs/schedule';
+import { MAX_TRACKING_RANK, NINETY_DAYS, NO_CACHE, THIRTY_DAYS } from 'src/constants';
+import { ProductRankCache } from 'src/utils/cache';
+import { ProductRankCacheDto } from 'src/dto/product.rank.cache.dto';
+import Redis from 'ioredis';
+import { InjectRedis } from '@songkeys/nestjs-redis';
const REGEXP_11ST =
/http[s]?:\/\/(?:www\.|m\.)?11st\.co\.kr\/products\/(?:ma\/|m\/|pa\/)?([1-9]\d*)(?:\?.*)?(?:\/share)?/;
@Injectable()
export class ProductService {
- private productDataCache = new Map();
+ private productRankCache = new ProductRankCache(MAX_TRACKING_RANK);
constructor(
@InjectRepository(TrackingProductRepository)
private trackingProductRepository: TrackingProductRepository,
@@ -28,6 +30,7 @@ export class ProductService {
private productRepository: ProductRepository,
@InjectModel(ProductPrice.name)
private productPriceModel: Model,
+ @InjectRedis() private readonly redis: Redis,
) {
this.initCache();
}
@@ -47,14 +50,31 @@ export class ProductService {
},
},
])
+ .sort('_id')
.exec();
- latestData.forEach((data) => {
- this.productDataCache.set(data._id, {
- price: data.price,
- isSoldOut: data.isSoldOut,
- lowestPrice: data.lowestPrice,
- });
+ const userCountList = await this.trackingProductRepository.getAllUserCount();
+ const rankList = await this.productRepository.getTotalInfoRankingList();
+ const initPromise = latestData.map(async (data) => {
+ const matchProduct = userCountList.find((product) => product.id === data._id);
+ const setUserCount = await this.redis.set(
+ `product:${data._id}`,
+ JSON.stringify({
+ isSoldOut: data.isSoldOut,
+ price: data.price,
+ lowestPrice: data.lowestPrice,
+ }),
+ );
+ const zaddUserCount = await this.redis.zadd(
+ 'userCount',
+ matchProduct ? parseInt(matchProduct.userCount) : 0,
+ data._id,
+ );
+ return Promise.all([setUserCount, zaddUserCount]);
+ });
+ rankList.forEach((product) => {
+ this.productRankCache.put(product.id, { ...product, userCount: parseInt(product.userCount) });
});
+ await Promise.all(initPromise);
}
async verifyUrl(productUrlDto: ProductUrlDto): Promise {
@@ -72,14 +92,27 @@ export class ProductService {
const existProduct = await this.productRepository.findOne({
where: { productCode: productCode },
});
- const productInfo = existProduct ?? (await getProductInfo11st(productCode));
- const product = existProduct ?? (await this.productRepository.saveProduct(productInfo));
+ const product = existProduct ?? (await this.firstAddProduct(productCode));
const trackingProduct = await this.trackingProductRepository.findOne({
where: { productId: product.id, userId: userId },
});
if (trackingProduct) {
throw new HttpException('이미 등록된 상품입니다.', HttpStatus.CONFLICT);
}
+ const cacheData = await this.redis.zscore('userCount', product.id);
+ const userCount = cacheData ? parseInt(cacheData) : NO_CACHE;
+ const productRank = {
+ id: product.id,
+ productName: product.productName,
+ productCode: product.productCode,
+ shop: product.shop,
+ imageUrl: product.imageUrl,
+ userCount: userCount + 1,
+ };
+ this.productRankCache.update(productRank);
+ if (cacheData) {
+ await this.redis.zincrby('userCount', 1, product.id);
+ }
await this.trackingProductRepository.saveTrackingProduct(userId, product.id, targetPrice);
}
@@ -88,12 +121,13 @@ export class ProductService {
where: { userId: userId },
relations: ['product'],
});
- if (trackingProductList.length === 0) {
- throw new HttpException('상품 목록을 찾을 수 없습니다.', HttpStatus.NOT_FOUND);
- }
- const trackingListInfo = trackingProductList.map(async ({ product, targetPrice }) => {
+ if (trackingProductList.length === 0) return [];
+ const trackingListInfo = trackingProductList.map(async ({ product, targetPrice, isAlert }) => {
const { id, productName, productCode, shop, imageUrl } = product;
- const { price } = this.productDataCache.get(id) ?? { price: NO_CACHE };
+ const cacheData = await this.redis.get(`product:${id}`);
+ const { price } = cacheData
+ ? JSON.parse(cacheData)
+ : await this.productRepository.findOne({ where: { id: id } });
const priceData = await this.getPriceData(id, THIRTY_DAYS);
return {
productName,
@@ -102,6 +136,7 @@ export class ProductService {
imageUrl,
targetPrice: targetPrice,
price,
+ isAlert,
priceData,
};
});
@@ -110,10 +145,13 @@ export class ProductService {
}
async getRecommendList() {
- const recommendList = await this.trackingProductRepository.getTotalInfoRankingList();
+ const recommendList = this.productRankCache.getAll();
const recommendListInfo = recommendList.map(async (product, index) => {
const { id, productName, productCode, shop, imageUrl } = product;
- const { price } = this.productDataCache.get(id) ?? { price: NO_CACHE };
+ const cacheData = await this.redis.get(`product:${id}`);
+ const { price } = cacheData
+ ? JSON.parse(cacheData)
+ : await this.productRepository.findOne({ where: { id: id } });
const priceData = await this.getPriceData(id, THIRTY_DAYS);
return {
productName,
@@ -139,11 +177,11 @@ export class ProductService {
const trackingProduct = await this.trackingProductRepository.findOne({
where: { userId: userId, productId: selectProduct.id },
});
- const ranklist = await this.trackingProductRepository.getRankingList();
- const idx = ranklist.findIndex(({ id }) => id === selectProduct.id);
+ await this.trackingProductRepository.getUserCount(selectProduct.id);
+ const idx = this.productRankCache.findIndex(selectProduct.id);
const rank = idx === -1 ? idx : idx + 1;
const priceData = await this.getPriceData(selectProduct.id, NINETY_DAYS);
- const { price, lowestPrice } = this.productDataCache.get(selectProduct.id);
+ const { price, lowestPrice } = await this.getProductCurrentData(selectProduct.id);
return {
productName: selectProduct.productName,
shop: selectProduct.shop,
@@ -160,14 +198,51 @@ export class ProductService {
async updateTargetPrice(userId: string, productAddDto: ProductAddDto) {
const product = await this.findTrackingProductByCode(userId, productAddDto.productCode);
product.targetPrice = productAddDto.targetPrice;
+ product.isFirst = true;
await this.trackingProductRepository.save(product);
}
async deleteProduct(userId: string, productCode: string) {
const product = await this.findTrackingProductByCode(userId, productCode);
+ const prevProduct = this.productRankCache.get(product.productId)?.value;
+ await this.redis.zincrby('userCount', -1, product.productId);
+
+ if (!prevProduct) {
+ throw new HttpException('상품을 찾을 수 없습니다.', HttpStatus.NOT_FOUND);
+ }
+ prevProduct.userCount--;
+ const productCount = await this.redis.zcard('userCount');
+ if (productCount > MAX_TRACKING_RANK) {
+ await this.deleteUpdateCache(prevProduct);
+ } else {
+ this.productRankCache.update(prevProduct);
+ }
await this.trackingProductRepository.remove(product);
}
+ async deleteUpdateCache(prevProduct: ProductRankCacheDto) {
+ const nextDataId = (await this.redis.zrevrange('userCount', MAX_TRACKING_RANK, MAX_TRACKING_RANK))[0];
+ const cacheData = await this.redis.zscore('userCount', nextDataId);
+ const userCount = cacheData ? parseInt(cacheData) : NO_CACHE;
+ if (userCount >= prevProduct.userCount) {
+ const newProduct = await this.productRepository.findOne({
+ where: { id: nextDataId },
+ });
+ if (!newProduct) {
+ throw new HttpException('상품을 찾을 수 없습니다.', HttpStatus.NOT_FOUND);
+ }
+ const newProductRanck = {
+ id: newProduct.id,
+ productName: newProduct.productName,
+ productCode: newProduct.productCode,
+ shop: newProduct.shop,
+ imageUrl: newProduct.imageUrl,
+ userCount: userCount,
+ };
+ this.productRankCache.update(prevProduct, newProductRanck);
+ }
+ }
+
async findTrackingProductByCode(userId: string, productCode: string) {
const existProduct = await this.productRepository.findOne({
where: { productCode: productCode },
@@ -184,13 +259,8 @@ export class ProductService {
return trackingProduct;
}
- async mongo(productPriceDto: ProductPriceDto) {
- const newData = new this.productPriceModel(productPriceDto);
- return newData.save();
- }
-
async getPriceData(productId: string, days: number): Promise {
- const endDate = new Date(+new Date() + KR_OFFSET);
+ const endDate = new Date();
const startDate = new Date(endDate);
startDate.setDate(endDate.getDate() - days);
const dataInfo = await this.productPriceModel
@@ -206,26 +276,58 @@ export class ProductService {
return { time: new Date(time).getTime(), price, isSoldOut };
});
}
- @Cron('* */10 * * * *')
- async cyclicPriceChecker() {
- const productList = await this.productRepository.find({ select: { id: true, productCode: true } });
- const productCodeList = productList.map(({ productCode, id }) => getProductInfo11st(productCode, id));
- const results = (await Promise.all(productCodeList)).map(({ productId, productPrice, isSoldOut }) => {
- return { productId, price: productPrice, isSoldOut };
- });
- const updatedDataInfo = results.filter(({ productId, price, isSoldOut }) => {
- const cache = this.productDataCache.get(productId);
- if (!cache || cache.isSoldOut !== isSoldOut || cache.price !== price) {
- const lowestPrice = cache ? Math.min(cache.lowestPrice, price) : price;
- this.productDataCache.set(productId, {
- isSoldOut,
- price,
- lowestPrice,
- });
- return true;
- }
- return false;
- });
- await this.productPriceModel.insertMany(updatedDataInfo);
+
+ async firstAddProduct(productCode: string) {
+ const productInfo = await getProductInfo11st(productCode);
+ const product = await this.productRepository.saveProduct(productInfo);
+ const updatedDataInfo = {
+ productId: product.id,
+ price: productInfo.productPrice,
+ isSoldOut: productInfo.isSoldOut,
+ };
+ this.redis.set(
+ `product:${product.id}`,
+ JSON.stringify({
+ price: productInfo.productPrice,
+ isSoldOut: productInfo.isSoldOut,
+ lowestPrice: productInfo.productPrice,
+ }),
+ );
+ this.productPriceModel.create(updatedDataInfo);
+ return product;
+ }
+
+ async toggleProductAlert(userId: string, productCode: string) {
+ const product = await this.findTrackingProductByCode(userId, productCode);
+ product.isAlert = !product.isAlert;
+ await this.trackingProductRepository.save(product);
+ }
+
+ async getProductCurrentData(productId: string) {
+ const cacheData = await this.redis.get(`product:${productId}`);
+ if (cacheData) {
+ const { price, lowestPrice } = JSON.parse(cacheData);
+ return { price, lowestPrice };
+ }
+ const latestData = await this.productPriceModel
+ .aggregate([
+ {
+ $match: { productId: productId },
+ },
+ {
+ $sort: { time: -1 },
+ },
+ {
+ $group: {
+ _id: '$productId',
+ price: { $first: '$price' },
+ isSoldOut: { $first: '$isSoldOut' },
+ lowestPrice: { $min: '$price' },
+ },
+ },
+ ])
+ .exec();
+ const { price, lowestPrice } = latestData[0];
+ return { price, lowestPrice };
}
}
diff --git a/backend/src/product/trackingProduct.repository.ts b/backend/src/product/trackingProduct.repository.ts
index 895e5fc..12685b0 100644
--- a/backend/src/product/trackingProduct.repository.ts
+++ b/backend/src/product/trackingProduct.repository.ts
@@ -2,7 +2,6 @@ import { Injectable } from '@nestjs/common';
import { Repository } from 'typeorm';
import { InjectRepository } from '@nestjs/typeorm';
import { TrackingProduct } from 'src/entities/trackingProduct.entity';
-import { Product } from 'src/entities/product.entity';
import { MAX_TRACKING_RANK } from 'src/constants';
@Injectable()
@@ -20,26 +19,6 @@ export class TrackingProductRepository extends Repository {
return newTrackingProduct;
}
- async getTotalInfoRankingList() {
- const recommendList = await this.repository
- .createQueryBuilder('tracking_product')
- .select([
- 'tracking_product.productId as id',
- 'COUNT(tracking_product.userId) as userCount',
- 'product.productName as productName',
- 'product.productCode as productCode',
- 'product.shop as shop',
- 'product.imageUrl as imageUrl',
- ])
- .leftJoin(Product, 'product', 'tracking_product.productId = product.id')
- .groupBy('tracking_product.productId')
- .orderBy('userCount', 'DESC')
- .addOrderBy('MAX(tracking_product.productId)', 'DESC')
- .take(MAX_TRACKING_RANK)
- .getRawMany();
- return recommendList;
- }
-
async getRankingList() {
const rankList = await this.repository
.createQueryBuilder('tracking_product')
@@ -47,8 +26,26 @@ export class TrackingProductRepository extends Repository {
.groupBy('tracking_product.productId')
.orderBy('COUNT(tracking_product.userId)', 'DESC')
.addOrderBy('MAX(tracking_product.productId)', 'DESC')
- .take(MAX_TRACKING_RANK)
+ .limit(MAX_TRACKING_RANK)
.getRawMany();
return rankList;
}
+
+ async getAllUserCount() {
+ const raw = await this.repository
+ .createQueryBuilder('tracking_product')
+ .select(['tracking_product.productId as id', 'COUNT(tracking_product.userId) as userCount'])
+ .groupBy('tracking_product.productId')
+ .getRawMany();
+ return raw;
+ }
+
+ async getUserCount(productId: string) {
+ const raw = await this.repository
+ .createQueryBuilder('tracking_product')
+ .select('COUNT(tracking_product.userId) as userCount')
+ .where('tracking_product.productId = :productId', { productId })
+ .getRawOne();
+ return raw.userCount;
+ }
}
diff --git a/backend/src/schema/product.schema.ts b/backend/src/schema/product.schema.ts
index 4924580..dfbff56 100644
--- a/backend/src/schema/product.schema.ts
+++ b/backend/src/schema/product.schema.ts
@@ -1,11 +1,10 @@
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import mongoose from 'mongoose';
-import { KR_OFFSET } from 'src/constants';
@Schema({
timestamps: {
createdAt: 'time',
- currentTime: () => new Date(+new Date() + KR_OFFSET),
+ currentTime: () => new Date(),
updatedAt: false,
},
})
diff --git a/backend/src/user/user.controller.ts b/backend/src/user/user.controller.ts
index be15b27..36b9adc 100644
--- a/backend/src/user/user.controller.ts
+++ b/backend/src/user/user.controller.ts
@@ -1,4 +1,15 @@
-import { Body, Controller, Post, HttpStatus, UseFilters, forwardRef, Inject } from '@nestjs/common';
+import {
+ Body,
+ Controller,
+ Post,
+ HttpStatus,
+ UseFilters,
+ forwardRef,
+ Inject,
+ Put,
+ UseGuards,
+ Req,
+} from '@nestjs/common';
import { UsersService } from './user.service';
import { UserDto } from '../dto/user.dto';
import { UserExceptionFilter } from 'src/exceptions/exception.fillter';
@@ -6,19 +17,29 @@ import { AuthService } from '../auth/auth.service';
import { LoginDto } from '../dto/login.dto';
import {
ApiBadRequestResponse,
+ ApiBearerAuth,
ApiBody,
ApiConflictResponse,
+ ApiGoneResponse,
+ ApiHeader,
ApiOkResponse,
ApiOperation,
ApiTags,
+ ApiUnauthorizedResponse,
} from '@nestjs/swagger';
import {
BadRequestError,
DupEmailError,
+ FirebaseTokenSuccess,
LoginFailError,
LoginSuccess,
RegisterSuccess,
} from 'src/dto/user.swagger.dto';
+import { FirebaseTokenDto } from 'src/dto/firebase.token.dto';
+import { UnauthorizedRequest } from 'src/dto/product.swagger.dto';
+import { AuthGuard } from '@nestjs/passport';
+import { User } from 'src/entities/user.entity';
+import { ExpiredTokenError } from 'src/dto/auth.swagger.dto';
@ApiTags('사용자 API')
@Controller('user')
@@ -53,4 +74,22 @@ export class UsersController {
const { accessToken, refreshToken } = await this.authService.validateUser(email, password);
return { statusCode: HttpStatus.OK, message: '로그인 성공', accessToken, refreshToken };
}
+ @ApiBearerAuth()
+ @ApiHeader({
+ name: 'Authorization',
+ description: '사용자 인증을 위한 AccessToken이다. ex) Bearer [token]',
+ })
+ @ApiOkResponse({ type: FirebaseTokenSuccess, description: '유저 FCM 토큰 생성 or 갱신' })
+ @ApiUnauthorizedResponse({ type: UnauthorizedRequest, description: '승인되지 않은 요청' })
+ @ApiGoneResponse({ type: ExpiredTokenError, description: 'accessToken 만료' })
+ @UseGuards(AuthGuard('access'))
+ @Put('firebase/token')
+ async registerFirebaseToken(
+ @Req() req: Request & { user: User },
+ @Body() firebaseTokenDto: FirebaseTokenDto,
+ ): Promise {
+ const { token } = firebaseTokenDto;
+ await this.authService.addFirebaseToken(req.user.id, token);
+ return { statusCode: HttpStatus.OK, message: 'FCM 토큰 등록 성공' };
+ }
}
diff --git a/backend/src/user/user.service.spec.ts b/backend/src/user/user.service.spec.ts
new file mode 100644
index 0000000..baabacc
--- /dev/null
+++ b/backend/src/user/user.service.spec.ts
@@ -0,0 +1,89 @@
+import { Test, TestingModule } from '@nestjs/testing';
+import { UsersService } from './user.service';
+import { UsersRepository } from './user.repository';
+import { User } from '../entities/user.entity';
+import { UserDto } from 'src/dto/user.dto';
+import { HttpException } from '@nestjs/common';
+
+describe('UsersService', () => {
+ let usersService: UsersService;
+ let usersRepository: UsersRepository;
+ const mockMemberRepository = {
+ createUser: jest.fn(),
+ findOne: jest.fn(),
+ };
+ beforeEach(async () => {
+ const module: TestingModule = await Test.createTestingModule({
+ providers: [
+ UsersService,
+ {
+ provide: UsersRepository,
+ useValue: mockMemberRepository,
+ },
+ ],
+ }).compile();
+
+ usersService = module.get(UsersService);
+ usersRepository = module.get(UsersRepository);
+ });
+
+ it('should be defined', () => {
+ expect(usersService).toBeDefined();
+ });
+ describe('registerUser', () => {
+ const userDto: UserDto = {
+ email: 'test@example.com',
+ userName: 'testuser',
+ password: 'password123',
+ };
+ it('user 회원가입', async () => {
+ jest.spyOn(usersRepository, 'createUser').mockResolvedValue(new User());
+ await expect(usersService.registerUser(userDto)).resolves.toBeInstanceOf(User);
+ expect(usersRepository.createUser).toHaveBeenCalledWith(userDto);
+ });
+
+ it('email 중복 에러', async () => {
+ jest.spyOn(usersRepository, 'createUser').mockRejectedValueOnce({ code: 'ER_DUP_ENTRY' });
+ await expect(usersService.registerUser(userDto)).rejects.toThrow(HttpException);
+ expect(usersRepository.createUser).toHaveBeenCalledWith(userDto);
+ });
+
+ it('유효 하지 않은 회원가입 입력값', async () => {
+ jest.spyOn(usersRepository, 'createUser').mockRejectedValueOnce({ code: 'ER_NO_DEFAULT_FOR_FIELD' });
+ await expect(usersService.registerUser(userDto)).rejects.toThrow(HttpException);
+ expect(usersRepository.createUser).toHaveBeenCalledWith(userDto);
+ });
+ });
+
+ describe('findOne', () => {
+ it('email로 user 조회', async () => {
+ const email = 'user@test.com';
+ jest.spyOn(usersRepository, 'findOne').mockResolvedValue(new User());
+ await expect(usersService.findOne(email)).resolves.toBeInstanceOf(User);
+ expect(usersRepository.findOne).toHaveBeenCalledWith({ where: { email } });
+ });
+
+ it('해당 email로 회원가입한 유저 없는 경우', async () => {
+ const email = 'user@test.com';
+ jest.spyOn(usersRepository, 'findOne').mockResolvedValue(null);
+ await expect(usersService.findOne(email)).resolves.toEqual(null);
+ expect(usersRepository.findOne).toHaveBeenCalledWith({ where: { email } });
+ });
+ });
+
+ describe('getUserById', () => {
+ it('id로 user 찾기', async () => {
+ const userId = 'testUUID';
+ jest.spyOn(usersRepository, 'findOne').mockResolvedValue(new User());
+ await expect(usersService.getUserById(userId)).resolves.toBeInstanceOf(User);
+ expect(usersRepository.findOne).toHaveBeenCalledWith({ where: { id: userId } });
+ });
+
+ it('해당 id의 유저 없는 경우', async () => {
+ const userId = 'testUUID';
+ jest.spyOn(usersRepository, 'findOne').mockResolvedValue(null);
+ await expect(usersService.findOne(userId)).resolves.toEqual(null);
+ expect(usersRepository.findOne).toHaveBeenCalledWith({ where: { id: userId } });
+ });
+ });
+});
diff --git a/backend/src/user/user.service.ts b/backend/src/user/user.service.ts
index 87c247c..7531115 100644
--- a/backend/src/user/user.service.ts
+++ b/backend/src/user/user.service.ts
@@ -3,7 +3,7 @@ import { UserDto } from '../dto/user.dto';
import { User } from '../entities/user.entity';
import { UsersRepository } from './user.repository';
import { InjectRepository } from '@nestjs/typeorm';
-import { ValidationException } from 'src/exceptions/validation.exception';
+import { ValidationException } from '../exceptions/validation.exception';
@Injectable()
export class UsersService {
diff --git a/backend/src/utils/cache.ts b/backend/src/utils/cache.ts
new file mode 100644
index 0000000..62961ad
--- /dev/null
+++ b/backend/src/utils/cache.ts
@@ -0,0 +1,114 @@
+import { ProductRankCacheDto } from 'src/dto/product.rank.cache.dto';
+
+class CacheNode {
+ key: string;
+ value: ProductRankCacheDto;
+ prev: CacheNode;
+ next: CacheNode;
+ constructor(key: string, value: ProductRankCacheDto) {
+ this.key = key;
+ this.value = value;
+ }
+}
+
+export class ProductRankCache {
+ maxSize: number;
+ count: number;
+ head: CacheNode;
+ tail: CacheNode;
+ hashTable = new Map();
+ constructor(size: number) {
+ this.maxSize = size;
+ this.head = new CacheNode('head', new ProductRankCacheDto());
+ this.tail = new CacheNode('tail', new ProductRankCacheDto());
+ this.head.next = this.tail;
+ this.tail.prev = this.head;
+ this.count = 0;
+ }
+
+ put(key: string, value: ProductRankCacheDto) {
+ const node = new CacheNode(key, value);
+ this.add(node);
+ if (this.count > this.maxSize) {
+ const lowestNode = this.getLowestNode();
+ this.delete(lowestNode);
+ }
+ }
+
+ private add(node: CacheNode) {
+ let prev = this.tail.prev;
+ while (prev.value.userCount <= node.value.userCount) {
+ if (prev.value.userCount === node.value.userCount && prev.value.id > node.value.id) {
+ break;
+ }
+ if (prev === this.head) {
+ break;
+ }
+ prev = prev.prev;
+ }
+ const next = prev.next;
+ prev.next = node;
+ node.next = next;
+ node.prev = prev;
+ next.prev = node;
+ this.hashTable.set(node.key, node);
+ this.count++;
+ }
+
+ private getLowestNode() {
+ let node = this.tail.prev;
+ while (node.value.userCount > node.prev.value.userCount) {
+ node = node.prev;
+ }
+ return node;
+ }
+
+ delete(node: CacheNode) {
+ const { prev, next } = node;
+ prev.next = next;
+ next.prev = prev;
+ this.hashTable.delete(node.key);
+ this.count--;
+ }
+
+ findIndex(key: string) {
+ let node = this.head.next;
+ let idx = 0;
+ while (node.key !== key) {
+ idx++;
+ if (this.count <= idx) {
+ idx = -1;
+ break;
+ }
+ node = node.next;
+ }
+ return idx;
+ }
+
+ update(product: ProductRankCacheDto, newProduct?: ProductRankCacheDto) {
+ const node = this.hashTable.get(product.id);
+ if (node) {
+ this.delete(node);
+ }
+ if (newProduct) {
+ this.put(newProduct.id, newProduct);
+ return;
+ }
+ this.put(product.id, product);
+ }
+
+ get(key: string): CacheNode | null {
+ const node = this.hashTable.get(key);
+ return node ? node : null;
+ }
+
+ getAll() {
+ const nodeList = [];
+ let node = this.head.next;
+ while (node !== this.tail) {
+ nodeList.push(node.value);
+ node = node.next;
+ }
+ return nodeList;
+ }
+}
diff --git a/backend/src/utils/openapi.11st.ts b/backend/src/utils/openapi.11st.ts
index 572b0ad..02c5875 100644
--- a/backend/src/utils/openapi.11st.ts
+++ b/backend/src/utils/openapi.11st.ts
@@ -3,6 +3,7 @@ import * as convert from 'xml-js';
import * as iconv from 'iconv-lite';
import axios from 'axios';
import { HttpException, HttpStatus } from '@nestjs/common';
+import { ProductInfoDto } from 'src/dto/product.info.dto';
function xmlConvert11st(xml: Buffer) {
const xmlUtf8 = iconv.decode(xml, 'EUC-KR').toString();
@@ -25,7 +26,7 @@ function productInfoUrl11st(productCode: string) {
return shopUrl.toString();
}
-export async function getProductInfo11st(productCode: string, productId?: string) {
+export async function getProductInfo11st(productCode: string, productId?: string): Promise {
const openApiUrl = productInfoUrl11st(productCode);
try {
const xml = await axios.get(openApiUrl, { responseType: 'arraybuffer' });