diff --git a/.github/workflows/be-autodeploy.yml b/.github/workflows/be-autodeploy.yml
new file mode 100644
index 00000000..80a1c105
--- /dev/null
+++ b/.github/workflows/be-autodeploy.yml
@@ -0,0 +1,71 @@
+name: auto deploy
+
+on:
+ push:
+ branches:
+ - develop/be
+
+
+jobs:
+ push_to_registry:
+ name: Push to ncp container registry
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v3
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v2
+ - name: Login to NCP Container Registry
+ uses: docker/login-action@v2
+ with:
+ registry: ${{ secrets.NCP_CONTAINER_REGISTRY }}
+ username: ${{ secrets.NCP_ACCESS_KEY }}
+ password: ${{ secrets.NCP_SECRET_KEY }}
+ - name: Create config file
+ run: |
+ echo "export const awsConfig = ${OBJECT_STORAGE_CONFIG}" > ./be/objectStorage.config.ts
+ env:
+ OBJECT_STORAGE_CONFIG: ${{ secrets.OBJECT_STORAGE_CONFIG }}
+
+ - name: Create TypeORM config
+ run: |
+ mkdir -p ./be/src/configs
+ echo "import { TypeOrmModuleOptions } from '@nestjs/typeorm';" > ./be/src/configs/typeorm.config.ts
+ echo "export const typeORMConfig: TypeOrmModuleOptions = ${TYPEORM_CONFIG}" >> ./be/src/configs/typeorm.config.ts
+ env:
+ TYPEORM_CONFIG: ${{ secrets.TYPEORM_CONFIG }}
+
+ - name: build and push
+ uses: docker/build-push-action@v3
+ with:
+ context: ./be
+ file: ./be/Dockerfile
+ push: true
+ tags: ${{ secrets.NCP_CONTAINER_REGISTRY }}/nibobnebob:latest
+ cache-from: type=registry,ref=${{ secrets.NCP_CONTAINER_REGISTRY }}/nibobnebob:latest
+ cache-to: type=inline
+
+
+
+
+
+ pull_from_registry:
+ name: Connect server ssh and pull from container registry
+ needs: push_to_registry
+ runs-on: ubuntu-latest
+ steps:
+ - name: connect ssh to Nginx server
+ uses: appleboy/ssh-action@master
+ with:
+ host: ${{ secrets.NGINX_HOST }}
+ username: ${{ secrets.NGINX_USERNAME }}
+ password: ${{ secrets.NGINX_PASSWORD }}
+ port: ${{ secrets.NGINX_PORT }}
+ script: |
+ ssh ${{ secrets.DEV_USERNAME }}@${{ secrets.DEV_HOST }} <<- 'EOSSH'
+ docker pull ${{ secrets.NCP_CONTAINER_REGISTRY }}/nibobnebob
+ docker stop $(docker ps -a -q)
+ docker rm $(docker ps -a -q)
+ docker run -d -p 8000:8000 -e API_KEY=${{ secrets.DEV_APIKEY }} -e NODE_ENV="DEV" ${{ secrets.NCP_CONTAINER_REGISTRY }}/nibobnebob
+ docker image prune -f
+ EOSSH
diff --git a/Aos/app/build.gradle.kts b/Aos/app/build.gradle.kts
index aeee1f9d..5f9c0267 100644
--- a/Aos/app/build.gradle.kts
+++ b/Aos/app/build.gradle.kts
@@ -24,7 +24,7 @@ android {
minSdk = 24
targetSdk = 34
versionCode = 1
- versionName = "0.1.0"
+ versionName = "1.0.3"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
buildConfigField("String", "NAVER_LOGIN_CLIENT_ID", getProperty("naverLoginClientId"))
diff --git a/Aos/app/src/main/AndroidManifest.xml b/Aos/app/src/main/AndroidManifest.xml
index 6287731c..f2460717 100644
--- a/Aos/app/src/main/AndroidManifest.xml
+++ b/Aos/app/src/main/AndroidManifest.xml
@@ -33,7 +33,8 @@
+ android:exported="false"
+ android:windowSoftInputMode="adjustPan">
{
- override fun RecommendFollowListResponse.toDomainModel(): RecommendFollowListData = RecommendFollowListData(
- nickName = nickName,
- region = region,
- isFollow = isFollow,
- profileImage = profileImage
- )
+ override fun RecommendFollowListResponse.toDomainModel(): RecommendFollowListData =
+ RecommendFollowListData(
+ nickName = nickName,
+ region = region,
+ isFollow = isFollow,
+ profileImage = profileImage
+ )
}
}
diff --git a/Aos/app/src/main/java/com/avengers/nibobnebob/data/remote/RestaurantApi.kt b/Aos/app/src/main/java/com/avengers/nibobnebob/data/remote/RestaurantApi.kt
index 7f7a6b53..d6a72907 100644
--- a/Aos/app/src/main/java/com/avengers/nibobnebob/data/remote/RestaurantApi.kt
+++ b/Aos/app/src/main/java/com/avengers/nibobnebob/data/remote/RestaurantApi.kt
@@ -71,6 +71,8 @@ interface RestaurantApi {
// 내 맛집 리스트
@GET("api/user/restaurant")
suspend fun myRestaurantList(
+ @Query("longitude") longitude: String? = null,
+ @Query("latitude") latitude: String? = null,
@Query("limit") limit: Int? = null,
@Query("page") page: Int? = null,
@Query("sort") sort: String? = null,
@@ -119,6 +121,7 @@ interface RestaurantApi {
//위치기반 맛집 리스트
@GET("api/restaurant/all")
suspend fun nearRestaurantList(
+ @Query("limit") limit : Int?,
@Query("radius") radius: String,
@Query("longitude") longitude: String,
@Query("latitude") latitude: String
diff --git a/Aos/app/src/main/java/com/avengers/nibobnebob/data/repository/RestaurantRepositoryImpl.kt b/Aos/app/src/main/java/com/avengers/nibobnebob/data/repository/RestaurantRepositoryImpl.kt
index 9806188f..386ad339 100644
--- a/Aos/app/src/main/java/com/avengers/nibobnebob/data/repository/RestaurantRepositoryImpl.kt
+++ b/Aos/app/src/main/java/com/avengers/nibobnebob/data/repository/RestaurantRepositoryImpl.kt
@@ -11,6 +11,7 @@ import com.avengers.nibobnebob.data.model.response.WishRestaurantResponse.Compan
import com.avengers.nibobnebob.data.model.runRemote
import com.avengers.nibobnebob.data.remote.RestaurantApi
import com.avengers.nibobnebob.domain.model.MyRestaurantData
+import com.avengers.nibobnebob.domain.model.RecommendRestaurantData
import com.avengers.nibobnebob.domain.model.RestaurantDetailData
import com.avengers.nibobnebob.domain.model.RestaurantIsWishData
import com.avengers.nibobnebob.domain.model.RestaurantItemsData
@@ -18,7 +19,6 @@ import com.avengers.nibobnebob.domain.model.ReviewSortData
import com.avengers.nibobnebob.domain.model.SearchRestaurantData
import com.avengers.nibobnebob.domain.model.WishRestaurantData
import com.avengers.nibobnebob.domain.model.base.BaseState
-import com.avengers.nibobnebob.domain.model.RecommendRestaurantData
import com.avengers.nibobnebob.domain.model.base.StatusCode
import com.avengers.nibobnebob.domain.repository.RestaurantRepository
import kotlinx.coroutines.flow.Flow
@@ -140,12 +140,15 @@ class RestaurantRepositoryImpl @Inject constructor(
}
override fun myRestaurantList(
+ longitude: String?,
+ latitude: String?,
limit: Int?,
page: Int?,
sort: String?
): Flow> =
flow {
- when (val result = runRemote { api.myRestaurantList(limit, page, sort) }) {
+ when (val result =
+ runRemote { api.myRestaurantList(longitude, latitude, limit, page, sort) }) {
is BaseState.Success -> {
result.data.body?.let { body ->
emit(BaseState.Success(body.toDomainModel()))
@@ -268,10 +271,11 @@ class RestaurantRepositoryImpl @Inject constructor(
override fun nearRestaurantList(
radius: String,
longitude: String,
- latitude: String
+ latitude: String,
+ limit: Int?
): Flow>> = flow {
-
- when (val result = runRemote { api.nearRestaurantList(radius, longitude, latitude) }) {
+ when (val result =
+ runRemote { api.nearRestaurantList(limit, radius, longitude, latitude) }) {
is BaseState.Success -> {
result.data.body?.let { body ->
emit(BaseState.Success(body.map { it.toDomainModel() }))
diff --git a/Aos/app/src/main/java/com/avengers/nibobnebob/domain/model/MyRestaurantData.kt b/Aos/app/src/main/java/com/avengers/nibobnebob/domain/model/MyRestaurantData.kt
index 928b975e..5916b1d5 100644
--- a/Aos/app/src/main/java/com/avengers/nibobnebob/domain/model/MyRestaurantData.kt
+++ b/Aos/app/src/main/java/com/avengers/nibobnebob/domain/model/MyRestaurantData.kt
@@ -14,7 +14,7 @@ data class MyRestaurantItemData(
val address: String,
val category: String,
val id: Int,
- val createdAt : String,
+ val createdAt: String?,
val location: LocationData,
val name: String,
val phoneNumber: String,
diff --git a/Aos/app/src/main/java/com/avengers/nibobnebob/domain/repository/RestaurantRepository.kt b/Aos/app/src/main/java/com/avengers/nibobnebob/domain/repository/RestaurantRepository.kt
index 075234c4..5ecf683d 100644
--- a/Aos/app/src/main/java/com/avengers/nibobnebob/domain/repository/RestaurantRepository.kt
+++ b/Aos/app/src/main/java/com/avengers/nibobnebob/domain/repository/RestaurantRepository.kt
@@ -42,6 +42,8 @@ interface RestaurantRepository {
): Flow>
fun myRestaurantList(
+ longitude: String? = null,
+ latitude: String? = null,
limit: Int? = null,
page: Int? = null,
sort: String? = null
@@ -82,6 +84,7 @@ interface RestaurantRepository {
radius: String,
longitude: String,
latitude: String,
+ limit: Int?
): Flow>>
fun likeReview(reviewId: Int): Flow>
diff --git a/Aos/app/src/main/java/com/avengers/nibobnebob/domain/usecase/restaurant/GetMyRestaurantListUseCase.kt b/Aos/app/src/main/java/com/avengers/nibobnebob/domain/usecase/restaurant/GetMyRestaurantListUseCase.kt
index e0fac5a1..6ba0b1bb 100644
--- a/Aos/app/src/main/java/com/avengers/nibobnebob/domain/usecase/restaurant/GetMyRestaurantListUseCase.kt
+++ b/Aos/app/src/main/java/com/avengers/nibobnebob/domain/usecase/restaurant/GetMyRestaurantListUseCase.kt
@@ -12,8 +12,11 @@ class GetMyRestaurantListUseCase @Inject constructor(
private val repository: RestaurantRepository
) {
operator fun invoke(
+ longitude: String? = null,
+ latitude: String? = null,
limit: Int? = null,
page: Int? = null,
sort: String? = null,
- ): Flow> = repository.myRestaurantList(limit, page, sort)
+ ): Flow> =
+ repository.myRestaurantList(longitude, latitude, limit, page, sort)
}
\ No newline at end of file
diff --git a/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/base/BaseFragment.kt b/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/base/BaseFragment.kt
index cf1b4518..e39bc178 100644
--- a/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/base/BaseFragment.kt
+++ b/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/base/BaseFragment.kt
@@ -84,7 +84,7 @@ abstract class BaseFragment(
}
fun showToastMessage(message: String) {
- Toast.makeText(context, message, Toast.LENGTH_LONG).show()
+ Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
}
fun showTwoButtonTitleDialog(
diff --git a/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/customview/RecommendRestaurantDialog.kt b/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/customview/RecommendRestaurantDialog.kt
index ecd8998f..1ce128fa 100644
--- a/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/customview/RecommendRestaurantDialog.kt
+++ b/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/customview/RecommendRestaurantDialog.kt
@@ -5,6 +5,7 @@ import android.content.Context
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.os.Bundle
+import android.view.View
import com.avengers.nibobnebob.databinding.DialogRecommendRestaurantBinding
import com.avengers.nibobnebob.presentation.ui.main.home.adapter.HomeRecommendAdapter
import com.avengers.nibobnebob.presentation.ui.main.home.model.UiRecommendRestaurantData
@@ -13,7 +14,6 @@ class RecommendRestaurantDialog(
context: Context,
private val uiRecommendRestaurantDataList: List,
private val restaurantClickListener: (Int) -> Unit
-
) : Dialog(context) {
private lateinit var binding: DialogRecommendRestaurantBinding
@@ -35,6 +35,11 @@ class RecommendRestaurantDialog(
rvRecommendRestaurant.adapter = adapter
adapter.submitList(uiRecommendRestaurantDataList)
+ if(uiRecommendRestaurantDataList.isEmpty()){
+ binding.tvIsEmpty.visibility = View.VISIBLE
+ }
+
+
binding.tvCancel.setOnClickListener {
dismiss()
}
diff --git a/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/ui/NavigationExtensions.kt b/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/ui/NavigationExtensions.kt
index 6480816a..dd69d6e6 100644
--- a/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/ui/NavigationExtensions.kt
+++ b/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/ui/NavigationExtensions.kt
@@ -1,6 +1,10 @@
package com.avengers.nibobnebob.presentation.ui
+import android.app.Activity
+import androidx.activity.OnBackPressedCallback
+import androidx.fragment.app.FragmentActivity
import androidx.navigation.NavController
+import androidx.navigation.fragment.findNavController
import com.avengers.nibobnebob.NavGraphDirections
@@ -31,4 +35,12 @@ internal fun NavController.toMyPage() {
internal fun NavController.toUserDetail(nickName: String) {
val action = NavGraphDirections.globalToUserDetailFragment(nickName)
navigate(action)
+}
+
+internal fun customBack(activity : FragmentActivity, nav : NavController){
+ activity.onBackPressedDispatcher.addCallback(object : OnBackPressedCallback(true) {
+ override fun handleOnBackPressed() {
+ nav.navigateUp()
+ }
+ })
}
\ No newline at end of file
diff --git a/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/ui/bindingadapters/ButtonBindingAdapter.kt b/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/ui/bindingadapters/ButtonBindingAdapter.kt
index c6c5726f..3fbd6088 100644
--- a/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/ui/bindingadapters/ButtonBindingAdapter.kt
+++ b/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/ui/bindingadapters/ButtonBindingAdapter.kt
@@ -12,7 +12,8 @@ fun bindDoneButtonEnable(btn: AppCompatButton, state: EditProfileUiState?) = wit
state ?: return
val allValid = state.nickName.isValid && state.location.isValid && state.birth.isValid
- val isChanged = state.nickName.isChanged || state.location.isChanged || state.birth.isChanged || state.profileImage.isChanged || state.isMale.isChanged
+ val isChanged =
+ state.nickName.isChanged || state.location.isChanged || state.birth.isChanged || state.profileImage.isChanged || state.isMale.isChanged
isEnabled = allValid && isChanged
}
diff --git a/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/ui/bindingadapters/TextViewBindingAdapter.kt b/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/ui/bindingadapters/TextViewBindingAdapter.kt
index 454a17df..84e35f36 100644
--- a/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/ui/bindingadapters/TextViewBindingAdapter.kt
+++ b/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/ui/bindingadapters/TextViewBindingAdapter.kt
@@ -16,7 +16,6 @@ import com.avengers.nibobnebob.presentation.util.Constants.FILTER_NEW
import com.avengers.nibobnebob.presentation.util.Constants.FILTER_OLD
import com.avengers.nibobnebob.presentation.util.Constants.FILTER_WORST
import com.avengers.nibobnebob.presentation.util.LoginType
-import com.google.android.material.textfield.TextInputLayout
// signup
@BindingAdapter("helperMessage")
@@ -38,6 +37,24 @@ fun bindHelpMessage(tv: TextView, inputState: InputState) = with(tv) {
}
}
+//signup
+@BindingAdapter("emailValidation")
+fun bindEmailValidationr(tv: TextView, inputState: InputState) = with(tv) {
+ when (inputState) {
+ is InputState.Success -> {
+ setTextColor(ContextCompat.getColor(context, R.color.nn_dark6))
+ }
+
+ is InputState.Error -> {
+ setTextColor(ContextCompat.getColor(context, R.color.nn_dark6))
+ }
+
+ is InputState.Empty -> {
+ setTextColor(ContextCompat.getColor(context, R.color.nn_dark2))
+ }
+ }
+}
+
@SuppressLint("SetTextI18n")
@BindingAdapter("textLength", "textLimit")
fun bindTextLength(tv: TextView, text: String, limit: Int) {
@@ -87,7 +104,7 @@ fun bindNickHelperText(tv: TextView, state: EditInputState?) = with(tv) {
@BindingAdapter("loginType")
fun bindLoginType(tv: TextView, type: String?) = with(tv) {
type ?: return
- text = if (type == LoginType.NAVER_LOGIN) "네이버 소셜로그인" else ""
+ text = if (type == LoginType.NAVER_LOGIN) "네이버 소셜로그인" else "일반 로그인"
}
@BindingAdapter("filterType")
@@ -100,3 +117,8 @@ fun bindFilterType(tv: TextView, type: String) = with(tv) {
else -> ""
}
}
+
+@BindingAdapter("adjustText")
+fun bindLongText(tv: TextView, value: String) {
+ tv.text = if (value.length > 27) "${value.substring(0, 27)}.." else value
+}
diff --git a/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/ui/intro/IntroActivity.kt b/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/ui/intro/IntroActivity.kt
index 8e5764f6..540fd553 100644
--- a/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/ui/intro/IntroActivity.kt
+++ b/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/ui/intro/IntroActivity.kt
@@ -2,16 +2,20 @@ package com.avengers.nibobnebob.presentation.ui.intro
import android.Manifest
import android.app.Activity
+import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
import android.provider.MediaStore
+import android.view.MotionEvent
+import android.view.inputmethod.InputMethodManager
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import com.avengers.nibobnebob.databinding.ActivityIntroBinding
import com.avengers.nibobnebob.presentation.base.BaseActivity
+import com.avengers.nibobnebob.presentation.ui.adjustKeyboard
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
@@ -100,4 +104,10 @@ class IntroActivity : BaseActivity(ActivityIntroBinding::i
}
}
}
+
+ override fun onTouchEvent(event: MotionEvent?): Boolean {
+ val imm: InputMethodManager = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
+ imm.hideSoftInputFromWindow(currentFocus?.windowToken, 0)
+ return true
+ }
}
\ No newline at end of file
diff --git a/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/ui/intro/login/LoginFragment.kt b/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/ui/intro/login/LoginFragment.kt
index 7438d454..10e22eaf 100644
--- a/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/ui/intro/login/LoginFragment.kt
+++ b/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/ui/intro/login/LoginFragment.kt
@@ -97,6 +97,7 @@ class LoginFragment : BaseFragment(R.layout.fragment_login
private fun NavController.toDetailSignup() {
val action = LoginFragmentDirections.actionLoginFragmentToDetailSignupFragment(
+ provider = "naver",
email = viewModel.naverEmail.value
)
this.navigate(action)
diff --git a/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/ui/intro/signup/BasicSignupViewModel.kt b/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/ui/intro/signup/BasicSignupViewModel.kt
index dc96c8c5..8658cb0d 100644
--- a/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/ui/intro/signup/BasicSignupViewModel.kt
+++ b/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/ui/intro/signup/BasicSignupViewModel.kt
@@ -27,20 +27,21 @@ data class BasicSignupUiState(
val isEmailNotEmpty: Boolean = true
)
-sealed class BasicSignupEvents{
+sealed class BasicSignupEvents {
data object NavigateToBack : BasicSignupEvents()
data class NavigateToDetailSignup(
val provider: String,
val email: String,
val password: String,
) : BasicSignupEvents()
- data class ShowSnackMessage(val msg: String): BasicSignupEvents()
+
+ data class ShowSnackMessage(val msg: String) : BasicSignupEvents()
}
@HiltViewModel
class BasicSignupViewModel @Inject constructor(
private val getEmailValidationUseCase: GetEmailValidationUseCase
-) : ViewModel(){
+) : ViewModel() {
private val _uiState = MutableStateFlow(BasicSignupUiState())
val uiState: StateFlow = _uiState.asStateFlow()
@@ -54,22 +55,23 @@ class BasicSignupViewModel @Inject constructor(
private val emailValidation = MutableStateFlow(false)
private val passwordValidation = MutableStateFlow(false)
- val isDataReady = combine(emailValidation, passwordValidation){ emailValidation, passwordValidation ->
- emailValidation && passwordValidation
- }.stateIn(
- viewModelScope,
- SharingStarted.WhileSubscribed(),
- false
- )
+ val isDataReady =
+ combine(emailValidation, passwordValidation) { emailValidation, passwordValidation ->
+ emailValidation && passwordValidation
+ }.stateIn(
+ viewModelScope,
+ SharingStarted.WhileSubscribed(),
+ false
+ )
- init{
+ init {
observeEmail()
observePasswordCheck()
}
- private fun observeEmail(){
+ private fun observeEmail() {
email.onEach {
- if(ValidationUtil.checkEmail(it)){
+ if (ValidationUtil.checkEmail(it)) {
_uiState.update { state ->
emailValidation.value = false
state.copy(
@@ -77,6 +79,14 @@ class BasicSignupViewModel @Inject constructor(
emailState = InputState.Empty
)
}
+ } else if (it.isEmpty()) {
+ _uiState.update { state ->
+ emailValidation.value = false
+ state.copy(
+ isEmailNotEmpty = false,
+ emailState = InputState.Error("이메일을 입력해주세요.")
+ )
+ }
} else {
_uiState.update { state ->
emailValidation.value = false
@@ -89,10 +99,31 @@ class BasicSignupViewModel @Inject constructor(
}.launchIn(viewModelScope)
}
- private fun observePasswordCheck(){
+ private fun observePasswordCheck() {
+
+ password.onEach {
+ if (it.isNotBlank()) {
+ if (it == passwordCheck.value) {
+ passwordValidation.value = true
+ _uiState.update { state ->
+ state.copy(
+ passwordCheckState = InputState.Success("비밀번호가 일치합니다.")
+ )
+ }
+ } else {
+ passwordValidation.value = false
+ _uiState.update { state ->
+ state.copy(
+ passwordCheckState = InputState.Error("비밀번호가 일치하지 않습니다.")
+ )
+ }
+ }
+ }
+ }.launchIn(viewModelScope)
+
passwordCheck.onEach {
- if(it.isNotBlank()){
- if(it == password.value){
+ if (it.isNotBlank()) {
+ if (it == password.value) {
passwordValidation.value = true
_uiState.update { state ->
state.copy(
@@ -111,11 +142,11 @@ class BasicSignupViewModel @Inject constructor(
}.launchIn(viewModelScope)
}
- fun checkEmail(){
+ fun checkEmail() {
getEmailValidationUseCase(email.value).onEach {
- when(it){
+ when (it) {
is BaseState.Success -> {
- if(it.data.isExist){
+ if (it.data.isExist) {
emailValidation.value = false
_uiState.update { state ->
state.copy(
@@ -131,6 +162,7 @@ class BasicSignupViewModel @Inject constructor(
}
}
}
+
is BaseState.Error -> {
emailValidation.value = false
_events.emit(BasicSignupEvents.ShowSnackMessage(it.message))
@@ -139,19 +171,21 @@ class BasicSignupViewModel @Inject constructor(
}.launchIn(viewModelScope)
}
- fun navigateToBack(){
+ fun navigateToBack() {
viewModelScope.launch {
_events.emit(BasicSignupEvents.NavigateToBack)
}
}
- fun navigateToDetailSignup(){
+ fun navigateToDetailSignup() {
viewModelScope.launch {
- _events.emit(BasicSignupEvents.NavigateToDetailSignup(
- provider = "site",
- email = email.value,
- password = password.value,
- ))
+ _events.emit(
+ BasicSignupEvents.NavigateToDetailSignup(
+ provider = "site",
+ email = email.value,
+ password = password.value,
+ )
+ )
}
}
}
\ No newline at end of file
diff --git a/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/ui/intro/signup/DetailSignupFragment.kt b/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/ui/intro/signup/DetailSignupFragment.kt
index 98be15ab..80d9c904 100644
--- a/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/ui/intro/signup/DetailSignupFragment.kt
+++ b/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/ui/intro/signup/DetailSignupFragment.kt
@@ -4,7 +4,6 @@ import android.content.Intent
import androidx.core.content.ContextCompat
import androidx.fragment.app.activityViewModels
import androidx.fragment.app.viewModels
-import androidx.navigation.NavController
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import com.avengers.nibobnebob.R
@@ -53,7 +52,7 @@ class DetailSignupFragment :
is DetailSignupEvents.ShowLoading -> showLoading(requireContext())
is DetailSignupEvents.DismissLoading -> dismissLoading()
is DetailSignupEvents.GoToMainActivity -> {
- val intent = Intent(requireContext(),MainActivity::class.java)
+ val intent = Intent(requireContext(), MainActivity::class.java)
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
startActivity(intent)
}
@@ -97,10 +96,5 @@ class DetailSignupFragment :
setSimpleItems(resources.getStringArray(R.array.location_list))
}
}
-
- private fun NavController.toLoginFragment() {
- val action = DetailSignupFragmentDirections.actionDetailSignupFragmentToLoginFragment()
- this.navigate(action)
- }
}
diff --git a/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/ui/intro/signup/DetailSignupViewModel.kt b/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/ui/intro/signup/DetailSignupViewModel.kt
index 1035ecff..37dab70c 100644
--- a/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/ui/intro/signup/DetailSignupViewModel.kt
+++ b/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/ui/intro/signup/DetailSignupViewModel.kt
@@ -94,6 +94,7 @@ class DetailSignupViewModel @Inject constructor(
nick.onEach {
_uiState.update { state ->
+ nickValidation.value = false
state.copy(
isNickNotEmpty = it.isNotBlank(),
nickState = InputState.Empty
diff --git a/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/ui/main/MainActivity.kt b/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/ui/main/MainActivity.kt
index f88bdc73..70de496d 100644
--- a/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/ui/main/MainActivity.kt
+++ b/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/ui/main/MainActivity.kt
@@ -2,11 +2,15 @@ package com.avengers.nibobnebob.presentation.ui.main
import android.Manifest
import android.app.Activity
+import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
import android.provider.MediaStore
+import android.view.MotionEvent
import android.view.View
+import android.view.inputmethod.InputMethodManager
+import androidx.activity.OnBackPressedCallback
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.core.app.ActivityCompat
@@ -49,6 +53,7 @@ class MainActivity : BaseActivity(ActivityMainBinding::infl
viewModel.events.collect{
when(it){
is MainEvents.OpenGallery -> onCheckStoragePermissions()
+ is MainEvents.FinishApp -> finish()
}
}
}
@@ -124,4 +129,9 @@ class MainActivity : BaseActivity(ActivityMainBinding::infl
}
}
}
+ override fun onTouchEvent(event: MotionEvent?): Boolean {
+ val imm: InputMethodManager = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
+ imm.hideSoftInputFromWindow(currentFocus?.windowToken, 0)
+ return true
+ }
}
\ No newline at end of file
diff --git a/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/ui/main/MainViewModel.kt b/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/ui/main/MainViewModel.kt
index a6a8620d..dff2ce68 100644
--- a/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/ui/main/MainViewModel.kt
+++ b/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/ui/main/MainViewModel.kt
@@ -18,6 +18,7 @@ import javax.inject.Inject
sealed class MainEvents{
data object OpenGallery: MainEvents()
+ data object FinishApp : MainEvents()
}
@HiltViewModel
@@ -40,6 +41,12 @@ class MainViewModel @Inject constructor(
}
}
+ fun finishApp() {
+ viewModelScope.launch {
+ _events.emit(MainEvents.FinishApp)
+ }
+ }
+
fun setUriString(uri : String){
_image.value = uri
}
diff --git a/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/ui/main/follow/FollowFragment.kt b/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/ui/main/follow/FollowFragment.kt
index c8c95a7e..b1f2367f 100644
--- a/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/ui/main/follow/FollowFragment.kt
+++ b/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/ui/main/follow/FollowFragment.kt
@@ -1,5 +1,6 @@
package com.avengers.nibobnebob.presentation.ui.main.follow
+import androidx.activity.OnBackPressedCallback
import androidx.fragment.app.activityViewModels
import androidx.fragment.app.viewModels
import androidx.navigation.NavController
@@ -24,6 +25,7 @@ class FollowFragment : BaseFragment(R.layout.fragment_fol
binding.rvFollowList.adapter = FollowAdapter()
binding.rvRecommendFriend.adapter = FollowAdapter()
setTabSelectedListener()
+ finishApp()
}
override fun initNetworkView() {
@@ -58,6 +60,20 @@ class FollowFragment : BaseFragment(R.layout.fragment_fol
})
}
+ private fun finishApp(){
+ var backPressTime = 0L
+ requireActivity().onBackPressedDispatcher.addCallback(object : OnBackPressedCallback(true) {
+ override fun handleOnBackPressed() {
+ if(System.currentTimeMillis() - backPressTime <= 2000) {
+ parentViewModel.finishApp()
+ } else{
+ backPressTime = System.currentTimeMillis()
+ showToastMessage("뒤로가기 버튼을 한 번 더 누르면 종료됩니다.")
+ }
+ }
+ })
+ }
+
private fun NavController.toFollowSearch() {
val action = FollowFragmentDirections.actionFollowFragmentToFollowSearchFragment()
navigate(action)
diff --git a/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/ui/main/follow/search/FollowSearchFragment.kt b/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/ui/main/follow/search/FollowSearchFragment.kt
index 47620e8a..88135313 100644
--- a/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/ui/main/follow/search/FollowSearchFragment.kt
+++ b/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/ui/main/follow/search/FollowSearchFragment.kt
@@ -8,6 +8,7 @@ import com.avengers.nibobnebob.databinding.FragmentFollowSearchBinding
import com.avengers.nibobnebob.presentation.base.BaseFragment
import com.avengers.nibobnebob.presentation.customview.SelectRegionDialog
import com.avengers.nibobnebob.presentation.ui.adjustKeyboard
+import com.avengers.nibobnebob.presentation.ui.customBack
import com.avengers.nibobnebob.presentation.ui.main.MainViewModel
import com.avengers.nibobnebob.presentation.ui.main.follow.adapter.FollowSearchAdapter
import com.avengers.nibobnebob.presentation.ui.toUserDetail
@@ -25,6 +26,7 @@ class FollowSearchFragment :
binding.rvFollowSearch.itemAnimator = null
setEditText()
viewModel.observeKeyword()
+ customBack(requireActivity(), findNavController())
}
override fun initNetworkView() {
diff --git a/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/ui/main/global/mapper/UiUserDetailDataMapper.kt b/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/ui/main/global/mapper/UiUserDetailDataMapper.kt
index d1a15d0b..37d81ead 100644
--- a/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/ui/main/global/mapper/UiUserDetailDataMapper.kt
+++ b/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/ui/main/global/mapper/UiUserDetailDataMapper.kt
@@ -21,6 +21,7 @@ internal fun UserDetailData.toUiUserDetailData() = UiUserDetailData(
internal fun UserDetailRestaurantData.toUiUserDetailRestaurantData() = UiUserDetailRestaurantData(
+ id = id,
name = name,
address = address,
phoneNumber = phoneNumber
diff --git a/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/ui/main/global/model/UiUserDetailData.kt b/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/ui/main/global/model/UiUserDetailData.kt
index 18c33e2b..c890f35b 100644
--- a/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/ui/main/global/model/UiUserDetailData.kt
+++ b/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/ui/main/global/model/UiUserDetailData.kt
@@ -11,6 +11,7 @@ data class UiUserDetailData(
data class UiUserDetailRestaurantData(
+ val id : Int,
val name: String,
val address: String,
val phoneNumber: String
diff --git a/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/ui/main/global/restaurantadd/AddMyRestaurantFragment.kt b/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/ui/main/global/restaurantadd/AddMyRestaurantFragment.kt
index 056bcd93..e692017e 100644
--- a/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/ui/main/global/restaurantadd/AddMyRestaurantFragment.kt
+++ b/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/ui/main/global/restaurantadd/AddMyRestaurantFragment.kt
@@ -7,6 +7,7 @@ import androidx.navigation.fragment.navArgs
import com.avengers.nibobnebob.R
import com.avengers.nibobnebob.databinding.FragmentAddMyRestaurantBinding
import com.avengers.nibobnebob.presentation.base.BaseFragment
+import com.avengers.nibobnebob.presentation.ui.customBack
import com.avengers.nibobnebob.presentation.ui.main.MainViewModel
import com.avengers.nibobnebob.presentation.ui.toHome
import com.avengers.nibobnebob.presentation.ui.toMultiPart
@@ -29,6 +30,7 @@ class AddMyRestaurantFragment :
setSliderListener()
setVisitMethodRadioListener()
initImageObserver()
+ customBack(requireActivity(), findNavController())
}
override fun initNetworkView() {
diff --git a/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/ui/main/global/restaurantdetail/RestaurantDetailFragment.kt b/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/ui/main/global/restaurantdetail/RestaurantDetailFragment.kt
index 0bea70ec..df361e4e 100644
--- a/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/ui/main/global/restaurantdetail/RestaurantDetailFragment.kt
+++ b/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/ui/main/global/restaurantdetail/RestaurantDetailFragment.kt
@@ -9,6 +9,7 @@ import com.avengers.nibobnebob.R
import com.avengers.nibobnebob.databinding.FragmentRestaurantDetailBinding
import com.avengers.nibobnebob.presentation.base.BaseFragment
import com.avengers.nibobnebob.presentation.customview.TwoButtonTitleDialog
+import com.avengers.nibobnebob.presentation.ui.customBack
import com.avengers.nibobnebob.presentation.ui.main.MainViewModel
import com.avengers.nibobnebob.presentation.ui.main.global.restaurantdetail.adapter.RestaurantReviewAdapter
import dagger.hilt.android.AndroidEntryPoint
@@ -27,6 +28,7 @@ class RestaurantDetailFragment :
binding.vm = viewModel
viewModel.setRestaurantId(restaurantId)
binding.rvReview.adapter = RestaurantReviewAdapter()
+ customBack(requireActivity(), findNavController())
}
override fun initNetworkView() {
diff --git a/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/ui/main/global/restaurantreview/RestaurantReviewsFragment.kt b/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/ui/main/global/restaurantreview/RestaurantReviewsFragment.kt
index d2557082..b57d72f3 100644
--- a/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/ui/main/global/restaurantreview/RestaurantReviewsFragment.kt
+++ b/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/ui/main/global/restaurantreview/RestaurantReviewsFragment.kt
@@ -1,6 +1,5 @@
package com.avengers.nibobnebob.presentation.ui.main.global.restaurantreview
-import android.util.Log
import androidx.appcompat.widget.PopupMenu
import androidx.fragment.app.activityViewModels
import androidx.fragment.app.viewModels
@@ -10,6 +9,7 @@ import androidx.recyclerview.widget.RecyclerView
import com.avengers.nibobnebob.R
import com.avengers.nibobnebob.databinding.FragmentRestaurantReviewsBinding
import com.avengers.nibobnebob.presentation.base.BaseFragment
+import com.avengers.nibobnebob.presentation.ui.customBack
import com.avengers.nibobnebob.presentation.ui.main.MainViewModel
import com.avengers.nibobnebob.presentation.ui.main.global.restaurantdetail.adapter.RestaurantReviewAdapter
import com.avengers.nibobnebob.presentation.util.Constants
@@ -43,11 +43,11 @@ class RestaurantReviewsFragment :
val isNotLoading = !viewModel.uiState.value.isLoading
if (scrollBottom && hasNextPage && isNotLoading) {
- Log.d("TEST", "end")
viewModel.loadNextPage()
}
}
})
+ customBack(requireActivity(), findNavController())
}
diff --git a/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/ui/main/global/restaurantreview/RestaurantReviewsViewModel.kt b/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/ui/main/global/restaurantreview/RestaurantReviewsViewModel.kt
index 6cab66bb..6c98f1d5 100644
--- a/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/ui/main/global/restaurantreview/RestaurantReviewsViewModel.kt
+++ b/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/ui/main/global/restaurantreview/RestaurantReviewsViewModel.kt
@@ -219,6 +219,6 @@ class RestaurantReviewsViewModel @Inject constructor(
companion object {
const val FIRST_PAGE = 1
- const val ITEM_LIMIT = 3
+ const val ITEM_LIMIT = 5
}
}
\ No newline at end of file
diff --git a/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/ui/main/global/userdetail/UserDetailFragment.kt b/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/ui/main/global/userdetail/UserDetailFragment.kt
index 6f38dcaa..68e31bb0 100644
--- a/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/ui/main/global/userdetail/UserDetailFragment.kt
+++ b/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/ui/main/global/userdetail/UserDetailFragment.kt
@@ -4,12 +4,14 @@ import android.os.Build
import androidx.annotation.RequiresApi
import androidx.fragment.app.activityViewModels
import androidx.fragment.app.viewModels
+import androidx.navigation.NavController
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import com.avengers.nibobnebob.R
import com.avengers.nibobnebob.databinding.FragmentUserDetailBinding
import com.avengers.nibobnebob.presentation.base.BaseFragment
import com.avengers.nibobnebob.presentation.customview.ImageDialog
+import com.avengers.nibobnebob.presentation.ui.customBack
import com.avengers.nibobnebob.presentation.ui.main.MainViewModel
import com.avengers.nibobnebob.presentation.ui.main.global.userdetail.adapter.UserDetailRestaurantAdapter
import dagger.hilt.android.AndroidEntryPoint
@@ -21,11 +23,13 @@ class UserDetailFragment : BaseFragment(R.layout.frag
private val viewModel: UserDetailViewModel by viewModels()
private val args: UserDetailFragmentArgs by navArgs()
private val nickName by lazy { args.nickName }
+ private val adapter = UserDetailRestaurantAdapter { id -> viewModel.restaurantDetail(id) }
- override fun initView() {
- binding.vm = viewModel
+ override fun initView() = with(binding) {
+ vm = viewModel
viewModel.setNick(nickName)
- binding.rvRestaurant.adapter = UserDetailRestaurantAdapter()
+ rvRestaurant.adapter = adapter
+ customBack(requireActivity(), findNavController())
}
@RequiresApi(Build.VERSION_CODES.O)
@@ -37,6 +41,11 @@ class UserDetailFragment : BaseFragment(R.layout.frag
repeatOnStarted {
viewModel.events.collect {
when (it) {
+ is UserDetailEvents.NavigateToRestaurantDetail ->
+ findNavController().toRestaurantDetail(
+ it.id
+ )
+
is UserDetailEvents.NavigateToBack -> findNavController().navigateUp()
is UserDetailEvents.ShowSnackMessage -> showSnackBar(it.msg)
is UserDetailEvents.ShowToastMessage -> showToastMessage(it.msg)
@@ -48,4 +57,11 @@ class UserDetailFragment : BaseFragment(R.layout.frag
}
}
}
+
+
+ private fun NavController.toRestaurantDetail(id: Int) {
+ val action =
+ UserDetailFragmentDirections.actionUserDetailFragmentToRestaurantDetailFragment(id)
+ navigate(action)
+ }
}
\ No newline at end of file
diff --git a/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/ui/main/global/userdetail/UserDetailViewModel.kt b/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/ui/main/global/userdetail/UserDetailViewModel.kt
index 5ea6cb8d..6d8246ce 100644
--- a/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/ui/main/global/userdetail/UserDetailViewModel.kt
+++ b/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/ui/main/global/userdetail/UserDetailViewModel.kt
@@ -34,6 +34,7 @@ sealed class UserDetailEvents {
data class ShowToastMessage(val msg: String) : UserDetailEvents()
data object NavigateToBack : UserDetailEvents()
data class ShowBiggerImageDialog(val img: String) : UserDetailEvents()
+ data class NavigateToRestaurantDetail(val id: Int) : UserDetailEvents()
}
@HiltViewModel
@@ -126,4 +127,10 @@ class UserDetailViewModel @Inject constructor(
_events.emit(UserDetailEvents.NavigateToBack)
}
}
+
+ fun restaurantDetail(id : Int) {
+ viewModelScope.launch {
+ _events.emit(UserDetailEvents.NavigateToRestaurantDetail(id))
+ }
+ }
}
\ No newline at end of file
diff --git a/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/ui/main/global/userdetail/adapter/UserDetailRestaurantAdapter.kt b/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/ui/main/global/userdetail/adapter/UserDetailRestaurantAdapter.kt
index 86415691..39ed4eda 100644
--- a/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/ui/main/global/userdetail/adapter/UserDetailRestaurantAdapter.kt
+++ b/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/ui/main/global/userdetail/adapter/UserDetailRestaurantAdapter.kt
@@ -8,7 +8,9 @@ import com.avengers.nibobnebob.databinding.ItemUserDetailRestaurantBinding
import com.avengers.nibobnebob.presentation.ui.main.global.model.UiUserDetailRestaurantData
import com.avengers.nibobnebob.presentation.util.DefaultDiffUtil
-class UserDetailRestaurantAdapter() :
+class UserDetailRestaurantAdapter(
+ private val restaurantClick: (Int) -> Unit
+) :
ListAdapter(DefaultDiffUtil()) {
override fun onCreateViewHolder(
@@ -24,14 +26,18 @@ class UserDetailRestaurantAdapter() :
}
override fun onBindViewHolder(holder: UserDetailRestaurantViewHolder, position: Int) {
- holder.bind(getItem(position))
+ holder.bind(getItem(position), restaurantClick)
}
}
class UserDetailRestaurantViewHolder(private val binding: ItemUserDetailRestaurantBinding) :
RecyclerView.ViewHolder(binding.root) {
- fun bind(item: UiUserDetailRestaurantData) {
-
+ fun bind(
+ item: UiUserDetailRestaurantData,
+ restaurantClick: (Int) -> Unit
+ ) {
+ binding.item = item
+ binding.root.setOnClickListener { restaurantClick(item.id) }
}
}
\ No newline at end of file
diff --git a/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/ui/main/home/HomeFragment.kt b/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/ui/main/home/HomeFragment.kt
index e54a80b8..62866fbd 100644
--- a/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/ui/main/home/HomeFragment.kt
+++ b/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/ui/main/home/HomeFragment.kt
@@ -1,6 +1,7 @@
package com.avengers.nibobnebob.presentation.ui.main.home
import android.Manifest
+import androidx.activity.OnBackPressedCallback
import androidx.activity.result.contract.ActivityResultContracts
import androidx.fragment.app.activityViewModels
import androidx.fragment.app.viewModels
@@ -34,6 +35,7 @@ import com.naver.maps.map.overlay.OverlayImage
import com.naver.maps.map.util.FusedLocationSource
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.async
+import kotlinx.coroutines.channels.Channel
@AndroidEntryPoint
class HomeFragment : BaseFragment(R.layout.fragment_home), OnMapReadyCallback {
@@ -42,6 +44,7 @@ class HomeFragment : BaseFragment(R.layout.fragment_home),
override val parentViewModel: MainViewModel by activityViewModels()
private val args: HomeFragmentArgs by navArgs()
private val restaurantId by lazy { args.addRestaurantId }
+ private val initCompletedNaverMap = Channel()
companion object {
const val LOCATION_PERMISSION_REQUEST_CODE = 1000
@@ -61,10 +64,15 @@ class HomeFragment : BaseFragment(R.layout.fragment_home),
initMapView()
binding.rvHomeFilter.adapter = HomeFilterAdapter()
viewModel.setAddRestaurantId(restaurantId)
+ finishApp()
}
override fun initNetworkView() {
- viewModel.getFilterList()
+ repeatOnStarted {
+ viewModel.getFilterList()
+ initCompletedNaverMap.receive()
+ viewModel.getMarkerList()
+ }
}
private fun initStateObserver() {
@@ -114,6 +122,9 @@ class HomeFragment : BaseFragment(R.layout.fragment_home),
is HomeEvents.RemoveMarkers -> removeAllMarker()
is HomeEvents.ShowSnackMessage -> showSnackBar(event.msg)
+
+ is HomeEvents.ShowLoading -> showLoading(requireContext())
+ is HomeEvents.DismissLoading -> dismissLoading()
}
}
}
@@ -133,8 +144,7 @@ class HomeFragment : BaseFragment(R.layout.fragment_home),
naverMap.moveCamera(cameraUpdate)
viewModel.uiState.value.markerList.forEach { data ->
- val isNear = viewModel.uiState.value.curFilter == NEAR_RESTAURANT
- setMarker(data, isNear)
+ setMarker(data)
}
}
@@ -148,10 +158,8 @@ class HomeFragment : BaseFragment(R.layout.fragment_home),
locationSource = FusedLocationSource(this, LOCATION_PERMISSION_REQUEST_CODE)
}
- // NaverMap 관련 Setting
override fun onMapReady(nM: NaverMap) {
this.naverMap = nM
- // 맵 ui Settings
with(naverMap.uiSettings) {
isCompassEnabled = false
isZoomControlEnabled = false
@@ -160,7 +168,9 @@ class HomeFragment : BaseFragment(R.layout.fragment_home),
naverMap.locationSource = locationSource
setMapListener()
initStateObserver()
- viewModel.getMarkerList()
+ repeatOnStarted {
+ initCompletedNaverMap.send(Unit)
+ }
}
private fun setMapListener() {
@@ -205,12 +215,14 @@ class HomeFragment : BaseFragment(R.layout.fragment_home),
else viewModel.trackingOff()
}
- private fun setMarker(data: UiRestaurantData, isNear: Boolean) {
+ private fun setMarker(data: UiRestaurantData) {
val marker = Marker()
marker.position = LatLng(data.latitude, data.longitude)
- marker.icon = if (isNear)
+// Log.d("marker test--", "${viewModel.uiState.value.curFilter}..${NEAR_RESTAURANT}.. ")
+
+ marker.icon = if (viewModel.uiState.value.curFilter == NEAR_RESTAURANT)
OverlayImage.fromResource(R.drawable.ic_marker_near)
else
OverlayImage.fromResource(R.drawable.ic_marker)
@@ -258,6 +270,20 @@ class HomeFragment : BaseFragment(R.layout.fragment_home),
markerList.clear()
}
+ private fun finishApp() {
+ var backPressTime = 0L
+ requireActivity().onBackPressedDispatcher.addCallback(object : OnBackPressedCallback(true) {
+ override fun handleOnBackPressed() {
+ if (System.currentTimeMillis() - backPressTime <= 2000) {
+ parentViewModel.finishApp()
+ } else {
+ backPressTime = System.currentTimeMillis()
+ showToastMessage("뒤로가기 버튼을 한 번 더 누르면 종료됩니다.")
+ }
+ }
+ })
+ }
+
private suspend fun addWishTest(id: Int, curState: Boolean): Boolean {
return lifecycleScope.async {
diff --git a/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/ui/main/home/HomeViewModel.kt b/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/ui/main/home/HomeViewModel.kt
index 6ff29f75..103b7ab1 100644
--- a/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/ui/main/home/HomeViewModel.kt
+++ b/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/ui/main/home/HomeViewModel.kt
@@ -20,6 +20,7 @@ import com.naver.maps.geometry.LatLng
import com.naver.maps.map.overlay.Marker
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.async
+import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
@@ -27,7 +28,9 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import java.lang.Math.pow
@@ -43,14 +46,14 @@ data class HomeUiState(
val markerList: List = emptyList(),
val recommendList: List = emptyList(),
val curFilter: String = MY_LIST,
- val cameraLatitude: Double = 0.0,
- val cameraLongitude: Double = 0.0,
- val cameraZoom: Double = 0.0,
+ val cameraLatitude: Double = 37.553836,
+ val cameraLongitude: Double = 126.969652,
+ val cameraZoom: Double = 12.0,
val cameraRadius: Double = 0.0,
val curLatitude: Double = 0.0,
val curLongitude: Double = 0.0,
val curSelectedMarker: Marker? = null,
- val addRestaurantId: Int = -1
+ val addRestaurantId: Int = 0
)
sealed class TrackingState {
@@ -73,6 +76,9 @@ sealed class HomeEvents {
data class ShowSnackMessage(
val msg: String
) : HomeEvents()
+
+ data object ShowLoading : HomeEvents()
+ data object DismissLoading : HomeEvents()
}
@HiltViewModel
@@ -87,17 +93,13 @@ class HomeViewModel @Inject constructor(
private val _uiState = MutableStateFlow(HomeUiState())
val uiState: StateFlow = _uiState.asStateFlow()
- private val _events = MutableSharedFlow()
+ private val _events = MutableSharedFlow(
+ replay = 0,
+ extraBufferCapacity = 1,
+ onBufferOverflow = BufferOverflow.DROP_OLDEST
+ )
val events: SharedFlow = _events.asSharedFlow()
- init {
- _uiState.update { state ->
- state.copy(
- filterList = listOf(UiFilterData(MY_LIST, true, ::onFilterItemClicked))
- )
- }
- }
-
fun updateLocation(latitude: Double, longitude: Double) {
_uiState.update { state ->
state.copy(
@@ -173,13 +175,13 @@ class HomeViewModel @Inject constructor(
fun getFilterList() {
followRepository.getMyFollowing().onEach { it ->
val initialFilterList = listOf(
- UiFilterData(MY_LIST, true, ::onFilterItemClicked),
- UiFilterData(NEAR_RESTAURANT, false, ::onFilterItemClicked)
+ UiFilterData(MY_LIST, isChecked(MY_LIST), ::onFilterItemClicked),
+ UiFilterData(NEAR_RESTAURANT, isChecked(NEAR_RESTAURANT), ::onFilterItemClicked)
)
when (it) {
is BaseState.Success -> {
val filterList = initialFilterList + it.data.map {
- UiFilterData(it.nickName, false, ::onFilterItemClicked)
+ UiFilterData(it.nickName, isChecked(it.nickName), ::onFilterItemClicked)
}
_uiState.update { state ->
state.copy(
@@ -202,42 +204,19 @@ class HomeViewModel @Inject constructor(
}.launchIn(viewModelScope)
}
- fun updateNearRestaurant() {
- val nearRestaurantFilter = uiState.value.filterList.find { it.name == NEAR_RESTAURANT }
- if (uiState.value.curFilter == NEAR_RESTAURANT && nearRestaurantFilter?.isSelected == true) {
- nearRestaurantList()
- } else {
- onFilterItemClicked(NEAR_RESTAURANT)
- }
+ private fun isChecked(filterName: String): Boolean {
+ return filterName == uiState.value.curFilter
}
- private fun resetMarkerList() {
- viewModelScope.launch {
- _uiState.update { state ->
- state.copy(markerList = emptyList())
- }
- _events.emit(HomeEvents.SetNewMarkers)
- }
+ fun updateNearRestaurant() {
+ onFilterItemClicked(NEAR_RESTAURANT)
}
fun getMarkerList() {
- if (uiState.value.filterList.all { !it.isSelected }) {
- resetMarkerList()
- return
- }
-
- when (_uiState.value.curFilter) {
- NEAR_RESTAURANT -> {
- nearRestaurantList()
- }
-
- MY_LIST -> {
- myRestaurantList()
- }
-
- else -> {
- userRestaurantList()
- }
+ when (uiState.value.curFilter) {
+ NEAR_RESTAURANT -> nearRestaurantList()
+ MY_LIST -> myRestaurantList()
+ else -> userRestaurantList()
}
}
@@ -245,12 +224,18 @@ class HomeViewModel @Inject constructor(
restaurantRepository.nearRestaurantList(
radius = uiState.value.cameraRadius.toString(),
longitude = uiState.value.cameraLongitude.toString(),
- latitude = uiState.value.cameraLatitude.toString()
- ).onEach {
+ latitude = uiState.value.cameraLatitude.toString(),
+ limit = if(uiState.value.cameraRadius < 500) 100 else 40
+ //이게 null일때 갯수제한이여야하는데.. 반대로 되어있어 200개 임시로 적음
+ ).onStart {
+ _events.emit(HomeEvents.ShowLoading)
+ }.onEach {
+ _events.emit(HomeEvents.RemoveMarkers)
when (it) {
is BaseState.Success -> {
_uiState.update { state ->
state.copy(
+ curFilter = NEAR_RESTAURANT,
markerList = it.data.map { restaurants ->
restaurants.toUiRestaurantData()
}
@@ -264,12 +249,19 @@ class HomeViewModel @Inject constructor(
}
}
_events.emit(HomeEvents.SetNewMarkers)
+ }.onCompletion {
+ _events.emit(HomeEvents.DismissLoading)
}.launchIn(viewModelScope)
}
private fun myRestaurantList() {
- myRestaurantListUseCase().onEach {
+ myRestaurantListUseCase(
+ longitude = DEFAULT_LONGITUDE,
+ latitude = DEFAULT_LATITUDE
+ ).onStart {
+ _events.emit(HomeEvents.ShowLoading)
+ }.onEach {
_events.emit(HomeEvents.RemoveMarkers)
when (it) {
is BaseState.Success -> {
@@ -279,6 +271,7 @@ class HomeViewModel @Inject constructor(
}
_uiState.update { state ->
state.copy(
+ curFilter = MY_LIST,
markerList = restaurantsList
)
}
@@ -289,15 +282,19 @@ class HomeViewModel @Inject constructor(
is BaseState.Error -> _events.emit(HomeEvents.ShowSnackMessage(ERROR_MSG))
}
_events.emit(HomeEvents.SetNewMarkers)
+ }.onCompletion {
+ _events.emit(HomeEvents.DismissLoading)
}.launchIn(viewModelScope)
}
private fun userRestaurantList() {
restaurantRepository.filterRestaurantList(
- _uiState.value.curFilter,
- "${_uiState.value.curLatitude} ${_uiState.value.curLongitude}",
- 50000
- ).onEach {
+ filter = _uiState.value.curFilter,
+ location = "${_uiState.value.curLatitude} ${_uiState.value.curLongitude}",
+ radius = 50000
+ ).onStart {
+ _events.emit(HomeEvents.ShowLoading)
+ }.onEach {
_events.emit(HomeEvents.RemoveMarkers)
when (it) {
is BaseState.Success -> {
@@ -314,6 +311,8 @@ class HomeViewModel @Inject constructor(
is BaseState.Error -> _events.emit(HomeEvents.ShowSnackMessage(ERROR_MSG))
}
_events.emit(HomeEvents.SetNewMarkers)
+ }.onCompletion {
+ _events.emit(HomeEvents.DismissLoading)
}.launchIn(viewModelScope)
}
@@ -402,7 +401,7 @@ class HomeViewModel @Inject constructor(
private fun moveCamera() {
if (_uiState.value.markerList.isEmpty()) return
- if (uiState.value.addRestaurantId != 0) {
+ if (uiState.value.addRestaurantId > 0) {
val restaurantItem: UiRestaurantData =
uiState.value.markerList.first { it.id == uiState.value.addRestaurantId }
_uiState.update { state ->
@@ -484,7 +483,7 @@ class HomeViewModel @Inject constructor(
}
fun setAddRestaurantId(restaurantId: Int) {
- if (restaurantId != -1) {
+ if (restaurantId <= 0) {
_uiState.update { state ->
state.copy(
addRestaurantId = restaurantId
@@ -493,4 +492,9 @@ class HomeViewModel @Inject constructor(
}
}
+ companion object {
+ const val DEFAULT_LATITUDE = "37.55"
+ const val DEFAULT_LONGITUDE = "126.9"
+ }
+
}
\ No newline at end of file
diff --git a/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/ui/main/home/search/RestaurantSearchFragment.kt b/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/ui/main/home/search/RestaurantSearchFragment.kt
index 4b543af6..3aefb1c6 100644
--- a/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/ui/main/home/search/RestaurantSearchFragment.kt
+++ b/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/ui/main/home/search/RestaurantSearchFragment.kt
@@ -19,6 +19,7 @@ import com.avengers.nibobnebob.R
import com.avengers.nibobnebob.databinding.FragmentRestaurantSearchBinding
import com.avengers.nibobnebob.presentation.base.BaseFragment
import com.avengers.nibobnebob.presentation.ui.adjustKeyboard
+import com.avengers.nibobnebob.presentation.ui.customBack
import com.avengers.nibobnebob.presentation.ui.main.MainActivity
import com.avengers.nibobnebob.presentation.ui.main.MainViewModel
import com.avengers.nibobnebob.presentation.ui.main.home.adapter.HomeSearchAdapter
@@ -43,6 +44,7 @@ class RestaurantSearchFragment :
fetchCurrentLocation()
view?.let { clearFocus(it) }
initStateObserver()
+ customBack(requireActivity(), findNavController())
}
override fun initNetworkView() {
@@ -73,39 +75,33 @@ class RestaurantSearchFragment :
}
}
- private fun fetchCurrentLocation() {
+ private fun checkLocationPermission() : Boolean{
val locationManager =
requireContext().getSystemService(Context.LOCATION_SERVICE) as LocationManager
+ return (ActivityCompat.checkSelfPermission(
+ requireContext(),
+ Manifest.permission.ACCESS_FINE_LOCATION
+ )
+ == PackageManager.PERMISSION_GRANTED &&
+ ActivityCompat.checkSelfPermission(
+ requireContext(),
+ Manifest.permission.ACCESS_COARSE_LOCATION
+ )
+ == PackageManager.PERMISSION_GRANTED &&
+ locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)
+ )
+ }
- if (!locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)) {
- startActivity(Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS))
- } else {
- if (ActivityCompat.checkSelfPermission(
- requireContext(),
- Manifest.permission.ACCESS_FINE_LOCATION
- )
- == PackageManager.PERMISSION_GRANTED &&
- ActivityCompat.checkSelfPermission(
- requireContext(),
- Manifest.permission.ACCESS_COARSE_LOCATION
- )
- == PackageManager.PERMISSION_GRANTED
- ) {
- LocationServices.getFusedLocationProviderClient(activity as MainActivity).apply {
- lastLocation.addOnSuccessListener { location: Location? ->
- viewModel.setCurrentLocation(location?.latitude, location?.longitude)
- }
+ private fun fetchCurrentLocation() {
+
+ if(checkLocationPermission()){
+ LocationServices.getFusedLocationProviderClient(activity as MainActivity).apply {
+ lastLocation.addOnSuccessListener { location: Location? ->
+ viewModel.setCurrentLocation(location?.latitude, location?.longitude)
}
- } else {
- ActivityCompat.requestPermissions(
- requireActivity(),
- arrayOf(
- Manifest.permission.ACCESS_FINE_LOCATION,
- Manifest.permission.ACCESS_COARSE_LOCATION
- ),
- 1000
- )
}
+ } else {
+ showToastMessage("GPS나 위치 권한을 허용해주세요.")
}
}
diff --git a/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/ui/main/home/search/RestaurantSearchMapFragment.kt b/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/ui/main/home/search/RestaurantSearchMapFragment.kt
index 5d233774..f74ac703 100644
--- a/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/ui/main/home/search/RestaurantSearchMapFragment.kt
+++ b/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/ui/main/home/search/RestaurantSearchMapFragment.kt
@@ -16,6 +16,7 @@ import com.avengers.nibobnebob.R
import com.avengers.nibobnebob.databinding.FragmentRestaurantSearchMapBinding
import com.avengers.nibobnebob.presentation.base.BaseFragment
import com.avengers.nibobnebob.presentation.customview.RestaurantBottomSheet
+import com.avengers.nibobnebob.presentation.ui.customBack
import com.avengers.nibobnebob.presentation.ui.main.MainViewModel
import com.avengers.nibobnebob.presentation.ui.main.home.model.UiRestaurantData
import com.avengers.nibobnebob.presentation.ui.toAddRestaurant
@@ -61,6 +62,7 @@ class RestaurantSearchMapFragment :
override fun initView() {
initMapView()
initClickEvent()
+ customBack(requireActivity(), findNavController())
}
override fun initNetworkView() {}
diff --git a/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/ui/main/mypage/MyPageFragment.kt b/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/ui/main/mypage/MyPageFragment.kt
index 08539aa8..46897c86 100644
--- a/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/ui/main/mypage/MyPageFragment.kt
+++ b/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/ui/main/mypage/MyPageFragment.kt
@@ -3,6 +3,7 @@ package com.avengers.nibobnebob.presentation.ui.main.mypage
import android.app.AlertDialog
import android.content.Intent
import android.os.Build
+import androidx.activity.OnBackPressedCallback
import androidx.annotation.RequiresApi
import androidx.fragment.app.activityViewModels
import androidx.fragment.app.viewModels
@@ -27,7 +28,7 @@ class MyPageFragment : BaseFragment(R.layout.fragment_my_
override fun initView() {
binding.svm = sharedViewModel
binding.vm = viewModel
-
+ finishApp()
binding.tvWithdraw.setOnClickListener {
@@ -91,6 +92,20 @@ class MyPageFragment : BaseFragment(R.layout.fragment_my_
}
+ private fun finishApp(){
+ var backPressTime = 0L
+ requireActivity().onBackPressedDispatcher.addCallback(object : OnBackPressedCallback(true) {
+ override fun handleOnBackPressed() {
+ if(System.currentTimeMillis() - backPressTime <= 2000) {
+ parentViewModel.finishApp()
+ } else{
+ backPressTime = System.currentTimeMillis()
+ showToastMessage("뒤로가기 버튼을 한 번 더 누르면 종료됩니다.")
+ }
+ }
+ })
+ }
+
private fun NavController.toEditProfile() {
val action = MyPageFragmentDirections.globalToEditProfileFragment()
diff --git a/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/ui/main/mypage/edit/EditProfileFragment.kt b/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/ui/main/mypage/edit/EditProfileFragment.kt
index 08a45a90..ebfbf6c0 100644
--- a/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/ui/main/mypage/edit/EditProfileFragment.kt
+++ b/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/ui/main/mypage/edit/EditProfileFragment.kt
@@ -1,13 +1,16 @@
package com.avengers.nibobnebob.presentation.ui.main.mypage.edit
+import androidx.activity.OnBackPressedCallback
import androidx.fragment.app.activityViewModels
import androidx.fragment.app.viewModels
import androidx.navigation.NavController
import androidx.navigation.Navigation
+import androidx.navigation.fragment.findNavController
import com.avengers.nibobnebob.R
import com.avengers.nibobnebob.databinding.FragmentEditProfileBinding
import com.avengers.nibobnebob.presentation.base.BaseFragment
import com.avengers.nibobnebob.presentation.customview.CalendarDatePicker
+import com.avengers.nibobnebob.presentation.ui.customBack
import com.avengers.nibobnebob.presentation.ui.main.MainViewModel
import com.avengers.nibobnebob.presentation.ui.main.mypage.share.MyPageSharedUiEvent
import com.avengers.nibobnebob.presentation.ui.main.mypage.share.MyPageSharedViewModel
@@ -29,6 +32,7 @@ class EditProfileFragment :
setDateBtnListener()
initImageObserver()
setGenderRadioListener()
+ customBack(requireActivity(), findNavController())
}
override fun initNetworkView() {
@@ -81,7 +85,6 @@ class EditProfileFragment :
repeatOnStarted {
parentViewModel.image.collect {
viewModel.setImage(it, it.toMultiPart(requireContext(), "profileImage"))
- parentViewModel.uriCollected()
}
}
}
diff --git a/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/ui/main/mypage/edit/EditProfileViewModel.kt b/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/ui/main/mypage/edit/EditProfileViewModel.kt
index 99b20715..682bd2ca 100644
--- a/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/ui/main/mypage/edit/EditProfileViewModel.kt
+++ b/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/ui/main/mypage/edit/EditProfileViewModel.kt
@@ -157,7 +157,7 @@ class EditProfileViewModel @Inject constructor(
nickName = EditInputState(
helperText = Validation.VALID_NICK,
isValid = true,
- isChanged = (originalNickName != nickState.value) && locationState.value != 0
+ isChanged = (originalNickName != nickState.value)
)
)
}
@@ -202,7 +202,7 @@ class EditProfileViewModel @Inject constructor(
birth = EditInputState(
helperText = if (!validData && birth.isNotEmpty()) Validation.INVALID_DATE else Validation.VALID_DATE,
isValid = validData,
- isChanged = originalBirth != birth && locationState.value != 0
+ isChanged = originalBirth != birth
)
)
}
@@ -214,7 +214,7 @@ class EditProfileViewModel @Inject constructor(
_uiState.update { state ->
state.copy(
isMale = EditInputState(
- isChanged = originalIsMale != isMale && locationState.value != 0
+ isChanged = originalIsMale != isMale
)
)
}
@@ -231,7 +231,7 @@ class EditProfileViewModel @Inject constructor(
_uiState.update { state ->
state.copy(
profileImage = EditInputState(
- isChanged = (originalProfileImage != profileImageState.value) && locationState.value != 0
+ isChanged = (originalProfileImage != profileImageState.value)
)
)
}
diff --git a/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/ui/main/mypage/mapper/UiMyListDataMapper.kt b/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/ui/main/mypage/mapper/UiMyListDataMapper.kt
index 3f8b59aa..211b1edc 100644
--- a/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/ui/main/mypage/mapper/UiMyListDataMapper.kt
+++ b/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/ui/main/mypage/mapper/UiMyListDataMapper.kt
@@ -3,7 +3,6 @@ package com.avengers.nibobnebob.presentation.ui.main.mypage.mapper
import android.os.Build
import androidx.annotation.RequiresApi
import com.avengers.nibobnebob.domain.model.MyRestaurantItemData
-import com.avengers.nibobnebob.domain.model.RestaurantItemsData
import com.avengers.nibobnebob.presentation.ui.main.mypage.model.UiMyListData
import com.avengers.nibobnebob.presentation.ui.toCreateDateString
@@ -12,5 +11,5 @@ fun MyRestaurantItemData.toUiMyListData(): UiMyListData = UiMyListData(
id = id,
name = name,
address = address,
- date = createdAt.toCreateDateString()
+ date = createdAt?.toCreateDateString()
)
\ No newline at end of file
diff --git a/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/ui/main/mypage/model/UiMyListData.kt b/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/ui/main/mypage/model/UiMyListData.kt
index c5933281..89c50e4e 100644
--- a/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/ui/main/mypage/model/UiMyListData.kt
+++ b/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/ui/main/mypage/model/UiMyListData.kt
@@ -4,5 +4,5 @@ data class UiMyListData(
val id: Int,
val name: String,
val address: String,
- val date: String
+ val date: String?
)
diff --git a/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/ui/main/mypage/mylist/MyRestaurantListFragment.kt b/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/ui/main/mypage/mylist/MyRestaurantListFragment.kt
index 228878a8..f62dcba5 100644
--- a/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/ui/main/mypage/mylist/MyRestaurantListFragment.kt
+++ b/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/ui/main/mypage/mylist/MyRestaurantListFragment.kt
@@ -9,6 +9,7 @@ import androidx.recyclerview.widget.RecyclerView
import com.avengers.nibobnebob.R
import com.avengers.nibobnebob.databinding.FragmentMyRestaurantListBinding
import com.avengers.nibobnebob.presentation.base.BaseFragment
+import com.avengers.nibobnebob.presentation.ui.customBack
import com.avengers.nibobnebob.presentation.ui.main.MainViewModel
import com.avengers.nibobnebob.presentation.ui.main.mypage.share.MyPageSharedUiEvent
import com.avengers.nibobnebob.presentation.ui.main.mypage.share.MyPageSharedViewModel
@@ -45,6 +46,7 @@ class MyRestaurantListFragment :
}
})
setFilterMenu()
+ customBack(requireActivity(), findNavController())
}
override fun initNetworkView() {
diff --git a/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/ui/main/mypage/wishlist/WishRestaurantListFragment.kt b/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/ui/main/mypage/wishlist/WishRestaurantListFragment.kt
index d7de114e..9efd5915 100644
--- a/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/ui/main/mypage/wishlist/WishRestaurantListFragment.kt
+++ b/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/ui/main/mypage/wishlist/WishRestaurantListFragment.kt
@@ -9,10 +9,10 @@ import androidx.recyclerview.widget.RecyclerView
import com.avengers.nibobnebob.R
import com.avengers.nibobnebob.databinding.FragmentWishRestaurantListBinding
import com.avengers.nibobnebob.presentation.base.BaseFragment
+import com.avengers.nibobnebob.presentation.ui.customBack
import com.avengers.nibobnebob.presentation.ui.main.MainViewModel
import com.avengers.nibobnebob.presentation.ui.main.mypage.share.MyPageSharedUiEvent
import com.avengers.nibobnebob.presentation.ui.main.mypage.share.MyPageSharedViewModel
-import com.avengers.nibobnebob.presentation.ui.toAddRestaurant
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
@@ -47,6 +47,7 @@ class WishRestaurantListFragment :
}
})
setFilterMenu()
+ customBack(requireActivity(), findNavController())
}
override fun initNetworkView() {
@@ -127,4 +128,13 @@ class WishRestaurantListFragment :
navigate(action)
}
+ private fun NavController.toAddRestaurant(restaurantName: String, restaurantId: Int) {
+ val action =
+ WishRestaurantListFragmentDirections.actionWishRestaurantListFragmentToAddMyRestaurantFragment2(
+ restaurantName,
+ restaurantId
+ )
+ navigate(action)
+ }
+
}
\ No newline at end of file
diff --git a/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/util/LocationArray.kt b/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/util/LocationArray.kt
index b6da6611..6222e377 100644
--- a/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/util/LocationArray.kt
+++ b/Aos/app/src/main/java/com/avengers/nibobnebob/presentation/util/LocationArray.kt
@@ -4,27 +4,30 @@ package com.avengers.nibobnebob.presentation.util
object LocationArray {
val LOCATION_ARRAY = listOf(
"지역을 선택하세요.",
- "용산구",
- "은평구",
- "강서구",
"강남구",
- "서초구",
- "동작구",
+ "강동구",
+ "강북구",
+ "강서구",
"관악구",
- "금천구",
- "영등포구",
- "양천구",
- "구로구",
- "송파구",
"광진구",
- "강동구",
- "성동구",
- "중구",
- "서대문구",
+ "구로구",
+ "금천구",
+ "노원구",
+ "도봉구",
+ "동대문구",
+ "동작구",
"마포구",
+ "서대문구",
+ "서초구",
+ "성동구",
+ "성북구",
+ "송파구",
+ "양천구",
+ "영등포구",
+ "용산구",
"은평구",
- "도봉구",
- "노원구",
+ "종로구",
+ "중구",
"중랑구",
)
}
\ No newline at end of file
diff --git a/Aos/app/src/main/res/drawable/ic_add_photo.xml b/Aos/app/src/main/res/drawable/ic_add_photo.xml
index 6517d390..736f48ab 100644
--- a/Aos/app/src/main/res/drawable/ic_add_photo.xml
+++ b/Aos/app/src/main/res/drawable/ic_add_photo.xml
@@ -5,11 +5,11 @@
android:viewportHeight="100">
diff --git a/Aos/app/src/main/res/drawable/ic_add_review_photo.xml b/Aos/app/src/main/res/drawable/ic_add_review_photo.xml
index 1c4efbc4..11a7f631 100644
--- a/Aos/app/src/main/res/drawable/ic_add_review_photo.xml
+++ b/Aos/app/src/main/res/drawable/ic_add_review_photo.xml
@@ -6,12 +6,12 @@
+ android:fillColor="@color/nn_dark7"
+ android:strokeColor="@color/nn_primary2"/>
+ android:fillColor="@color/nn_primary6"/>
+ android:fillColor="@color/nn_primary6"/>
diff --git a/Aos/app/src/main/res/drawable/ic_app_logo.xml b/Aos/app/src/main/res/drawable/ic_app_logo.xml
index ecd5e2d6..a6b24ef6 100644
--- a/Aos/app/src/main/res/drawable/ic_app_logo.xml
+++ b/Aos/app/src/main/res/drawable/ic_app_logo.xml
@@ -5,15 +5,15 @@
android:viewportHeight="332">
+ android:fillColor="@color/nn_primary6"/>
+ android:fillColor="@color/nn_primary3"/>
diff --git a/Aos/app/src/main/res/drawable/ic_back.xml b/Aos/app/src/main/res/drawable/ic_back.xml
index 6167d0fa..adedd99e 100644
--- a/Aos/app/src/main/res/drawable/ic_back.xml
+++ b/Aos/app/src/main/res/drawable/ic_back.xml
@@ -5,5 +5,5 @@
android:viewportHeight="24">
+ android:fillColor="@color/black"/>
diff --git a/Aos/app/src/main/res/drawable/ic_calendar.xml b/Aos/app/src/main/res/drawable/ic_calendar.xml
index 562079ea..82754a8e 100644
--- a/Aos/app/src/main/res/drawable/ic_calendar.xml
+++ b/Aos/app/src/main/res/drawable/ic_calendar.xml
@@ -5,6 +5,6 @@
android:viewportHeight="40">
diff --git a/Aos/app/src/main/res/drawable/ic_check.xml b/Aos/app/src/main/res/drawable/ic_check.xml
index aaf9a3bb..d2d00e68 100644
--- a/Aos/app/src/main/res/drawable/ic_check.xml
+++ b/Aos/app/src/main/res/drawable/ic_check.xml
@@ -5,5 +5,5 @@
android:viewportHeight="11">
+ android:fillColor="@color/black"/>
diff --git a/Aos/app/src/main/res/drawable/ic_close.xml b/Aos/app/src/main/res/drawable/ic_close.xml
index 21d2213e..33c5f8d0 100644
--- a/Aos/app/src/main/res/drawable/ic_close.xml
+++ b/Aos/app/src/main/res/drawable/ic_close.xml
@@ -5,5 +5,5 @@
android:viewportHeight="11">
+ android:fillColor="@color/black"/>
diff --git a/Aos/app/src/main/res/drawable/ic_drop_down.xml b/Aos/app/src/main/res/drawable/ic_drop_down.xml
index 0c0a19be..1eed2eaa 100644
--- a/Aos/app/src/main/res/drawable/ic_drop_down.xml
+++ b/Aos/app/src/main/res/drawable/ic_drop_down.xml
@@ -5,5 +5,5 @@
android:viewportHeight="10">
+ android:fillColor="@color/black"/>
diff --git a/Aos/app/src/main/res/drawable/ic_edit.xml b/Aos/app/src/main/res/drawable/ic_edit.xml
index 015d1c0a..868ca21f 100644
--- a/Aos/app/src/main/res/drawable/ic_edit.xml
+++ b/Aos/app/src/main/res/drawable/ic_edit.xml
@@ -5,5 +5,5 @@
android:viewportHeight="18">
+ android:fillColor="@color/nn_primary6"/>
diff --git a/Aos/app/src/main/res/drawable/ic_enter.xml b/Aos/app/src/main/res/drawable/ic_enter.xml
index 6f805c53..c3408973 100644
--- a/Aos/app/src/main/res/drawable/ic_enter.xml
+++ b/Aos/app/src/main/res/drawable/ic_enter.xml
@@ -7,5 +7,5 @@
android:strokeWidth="1"
android:pathData="M1,1L5.671,6L1,11"
android:fillColor="#00000000"
- android:strokeColor="#262626"/>
+ android:strokeColor="@color/black"/>
diff --git a/Aos/app/src/main/res/drawable/ic_filter.xml b/Aos/app/src/main/res/drawable/ic_filter.xml
index 6c63011d..9cc2f226 100644
--- a/Aos/app/src/main/res/drawable/ic_filter.xml
+++ b/Aos/app/src/main/res/drawable/ic_filter.xml
@@ -5,5 +5,5 @@
android:viewportHeight="10">
+ android:fillColor="@color/nn_dark1"/>
diff --git a/Aos/app/src/main/res/drawable/ic_following.xml b/Aos/app/src/main/res/drawable/ic_following.xml
index 704b1583..5ae9bf7e 100644
--- a/Aos/app/src/main/res/drawable/ic_following.xml
+++ b/Aos/app/src/main/res/drawable/ic_following.xml
@@ -5,8 +5,8 @@
android:viewportHeight="24">
+ android:fillColor="@color/nn_primary1"/>
+ android:fillColor="@color/nn_primary1"/>
diff --git a/Aos/app/src/main/res/drawable/ic_home.xml b/Aos/app/src/main/res/drawable/ic_home.xml
index ca578333..14b6e22b 100644
--- a/Aos/app/src/main/res/drawable/ic_home.xml
+++ b/Aos/app/src/main/res/drawable/ic_home.xml
@@ -8,6 +8,6 @@
android:strokeLineJoin="round"
android:strokeWidth="3"
android:fillColor="#00000000"
- android:strokeColor="#E1D6CC"
+ android:strokeColor="@color/nn_primary1"
android:strokeLineCap="round"/>
diff --git a/Aos/app/src/main/res/drawable/ic_location_circle.xml b/Aos/app/src/main/res/drawable/ic_location_circle.xml
index 05cfc98a..170a0d82 100644
--- a/Aos/app/src/main/res/drawable/ic_location_circle.xml
+++ b/Aos/app/src/main/res/drawable/ic_location_circle.xml
@@ -5,5 +5,5 @@
android:viewportHeight="24">
+ android:fillColor="@color/nn_primary5"/>
diff --git a/Aos/app/src/main/res/drawable/ic_location_off.xml b/Aos/app/src/main/res/drawable/ic_location_off.xml
index 3bd22a8e..76259e5c 100644
--- a/Aos/app/src/main/res/drawable/ic_location_off.xml
+++ b/Aos/app/src/main/res/drawable/ic_location_off.xml
@@ -7,8 +7,8 @@
android:strokeWidth="1"
android:pathData="M17,17m-16.5,0a16.5,16.5 0,1 1,33 0a16.5,16.5 0,1 1,-33 0"
android:fillColor="#ffffff"
- android:strokeColor="#C8CBD0"/>
+ android:strokeColor="@color/nn_dark6"/>
+ android:fillColor="@color/nn_dark6"/>
diff --git a/Aos/app/src/main/res/drawable/ic_location_on.xml b/Aos/app/src/main/res/drawable/ic_location_on.xml
index 0c314356..59d871fc 100644
--- a/Aos/app/src/main/res/drawable/ic_location_on.xml
+++ b/Aos/app/src/main/res/drawable/ic_location_on.xml
@@ -7,8 +7,8 @@
android:strokeWidth="1"
android:pathData="M17,17m-16.5,0a16.5,16.5 0,1 1,33 0a16.5,16.5 0,1 1,-33 0"
android:fillColor="#ffffff"
- android:strokeColor="#643E71"/>
+ android:strokeColor="@color/nn_rainbow_purple"/>
+ android:fillColor="@color/nn_rainbow_purple"/>
diff --git a/Aos/app/src/main/res/drawable/ic_logo.xml b/Aos/app/src/main/res/drawable/ic_logo.xml
index 328c8491..004cd147 100644
--- a/Aos/app/src/main/res/drawable/ic_logo.xml
+++ b/Aos/app/src/main/res/drawable/ic_logo.xml
@@ -5,8 +5,8 @@
android:viewportHeight="150">
+ android:fillColor="@color/nn_primary6"/>
+ android:fillColor="@color/nn_dark6"/>
diff --git a/Aos/app/src/main/res/drawable/ic_logo_big.xml b/Aos/app/src/main/res/drawable/ic_logo_big.xml
index 51acbb67..597fd5b7 100644
--- a/Aos/app/src/main/res/drawable/ic_logo_big.xml
+++ b/Aos/app/src/main/res/drawable/ic_logo_big.xml
@@ -5,8 +5,8 @@
android:viewportHeight="150">
+ android:fillColor="@color/nn_primary6"/>
+ android:fillColor="@color/nn_dark6"/>
\ No newline at end of file
diff --git a/Aos/app/src/main/res/drawable/ic_logo_home.xml b/Aos/app/src/main/res/drawable/ic_logo_home.xml
index 714be889..ebc5503a 100644
--- a/Aos/app/src/main/res/drawable/ic_logo_home.xml
+++ b/Aos/app/src/main/res/drawable/ic_logo_home.xml
@@ -5,8 +5,8 @@
android:viewportHeight="32">
+ android:fillColor="@color/nn_primary6"/>
+ android:fillColor="@color/nn_dark6"/>
diff --git a/Aos/app/src/main/res/drawable/ic_marker.xml b/Aos/app/src/main/res/drawable/ic_marker.xml
index 77185778..bbb57e19 100644
--- a/Aos/app/src/main/res/drawable/ic_marker.xml
+++ b/Aos/app/src/main/res/drawable/ic_marker.xml
@@ -8,5 +8,5 @@
android:fillColor="#000000"/>
+ android:fillColor="@color/nn_primary1"/>
diff --git a/Aos/app/src/main/res/drawable/ic_marker_near.xml b/Aos/app/src/main/res/drawable/ic_marker_near.xml
index 49eef48a..d9f44ac7 100644
--- a/Aos/app/src/main/res/drawable/ic_marker_near.xml
+++ b/Aos/app/src/main/res/drawable/ic_marker_near.xml
@@ -5,10 +5,10 @@
android:viewportWidth="25"
android:viewportHeight="32">
+ android:strokeColor="@color/black" />
diff --git a/Aos/app/src/main/res/drawable/ic_more.xml b/Aos/app/src/main/res/drawable/ic_more.xml
index 2646aae9..5f923b04 100644
--- a/Aos/app/src/main/res/drawable/ic_more.xml
+++ b/Aos/app/src/main/res/drawable/ic_more.xml
@@ -5,6 +5,6 @@
android:viewportHeight="24">
diff --git a/Aos/app/src/main/res/drawable/ic_mypage.xml b/Aos/app/src/main/res/drawable/ic_mypage.xml
index d0a39b59..54b74804 100644
--- a/Aos/app/src/main/res/drawable/ic_mypage.xml
+++ b/Aos/app/src/main/res/drawable/ic_mypage.xml
@@ -8,20 +8,20 @@
android:strokeLineJoin="round"
android:strokeWidth="3"
android:fillColor="#00000000"
- android:strokeColor="#E1D6CC"
+ android:strokeColor="@color/nn_primary1"
android:strokeLineCap="round"/>
diff --git a/Aos/app/src/main/res/drawable/ic_plus.xml b/Aos/app/src/main/res/drawable/ic_plus.xml
index 3d938df1..d0aa877b 100644
--- a/Aos/app/src/main/res/drawable/ic_plus.xml
+++ b/Aos/app/src/main/res/drawable/ic_plus.xml
@@ -5,5 +5,5 @@
android:viewportHeight="24">
+ android:fillColor="@color/black"/>
diff --git a/Aos/app/src/main/res/drawable/ic_refresh.xml b/Aos/app/src/main/res/drawable/ic_refresh.xml
index b4aac64f..31f7cb33 100644
--- a/Aos/app/src/main/res/drawable/ic_refresh.xml
+++ b/Aos/app/src/main/res/drawable/ic_refresh.xml
@@ -5,5 +5,5 @@
android:viewportHeight="16">
+ android:fillColor="@color/nn_dark1"/>
diff --git a/Aos/app/src/main/res/drawable/ic_search.xml b/Aos/app/src/main/res/drawable/ic_search.xml
index 3f1b377b..21a8db29 100644
--- a/Aos/app/src/main/res/drawable/ic_search.xml
+++ b/Aos/app/src/main/res/drawable/ic_search.xml
@@ -5,6 +5,6 @@
android:viewportHeight="24">
diff --git a/Aos/app/src/main/res/drawable/ic_star_border.xml b/Aos/app/src/main/res/drawable/ic_star_border.xml
index 264e6254..83933d08 100644
--- a/Aos/app/src/main/res/drawable/ic_star_border.xml
+++ b/Aos/app/src/main/res/drawable/ic_star_border.xml
@@ -5,5 +5,5 @@
android:viewportHeight="24">
+ android:fillColor="@color/nn_primary6"/>
diff --git a/Aos/app/src/main/res/drawable/ic_star_full.xml b/Aos/app/src/main/res/drawable/ic_star_full.xml
index d690aef7..3c460c06 100644
--- a/Aos/app/src/main/res/drawable/ic_star_full.xml
+++ b/Aos/app/src/main/res/drawable/ic_star_full.xml
@@ -5,5 +5,5 @@
android:viewportHeight="24">
+ android:fillColor="@color/nn_primary6"/>
diff --git a/Aos/app/src/main/res/drawable/ic_thumbs_down_blank.xml b/Aos/app/src/main/res/drawable/ic_thumbs_down_blank.xml
index 2b3c123a..a3ecdcd0 100644
--- a/Aos/app/src/main/res/drawable/ic_thumbs_down_blank.xml
+++ b/Aos/app/src/main/res/drawable/ic_thumbs_down_blank.xml
@@ -8,6 +8,6 @@
android:pathData="M3.931,1.688L1.259,9.61C1.08,10.142 1.497,10.689 2.097,10.707L7.29,10.865C7.609,10.875 7.836,11.157 7.755,11.443L7.502,12.348C7.269,13.177 7.176,14.035 7.227,14.892L7.254,15.358C7.284,15.857 7.502,16.331 7.872,16.696C8.209,17.03 8.675,17.226 9.166,17.241L9.274,17.244C9.571,17.253 9.85,17.112 10.001,16.874L11.265,14.897C11.832,14.01 12.658,13.293 13.648,12.828L16.706,11.391C17.001,11.252 17.19,10.974 17.2,10.666L17.453,2.328C17.467,1.863 17.074,1.474 16.574,1.459L4.82,1.101C4.415,1.089 4.052,1.329 3.931,1.688Z"
android:strokeLineJoin="round"
android:fillColor="#00000000"
- android:strokeColor="#717784"
+ android:strokeColor="@color/nn_dark4"
android:strokeLineCap="round"/>
diff --git a/Aos/app/src/main/res/drawable/ic_thumbs_up_blank.xml b/Aos/app/src/main/res/drawable/ic_thumbs_up_blank.xml
index 907e5b82..401ec0f5 100644
--- a/Aos/app/src/main/res/drawable/ic_thumbs_up_blank.xml
+++ b/Aos/app/src/main/res/drawable/ic_thumbs_up_blank.xml
@@ -8,6 +8,6 @@
android:pathData="M14.535,16.387L16.965,8.387C17.129,7.849 16.695,7.316 16.094,7.316H10.899C10.58,7.316 10.345,7.04 10.417,6.752L10.642,5.84C10.85,5.005 10.916,4.144 10.84,3.289L10.798,2.824C10.754,2.326 10.521,1.859 10.141,1.505C9.793,1.182 9.322,1 8.83,1H8.722C8.425,1 8.151,1.15 8.006,1.392L6.804,3.406C6.264,4.31 5.46,5.052 4.485,5.547L1.472,7.076C1.181,7.224 1,7.508 1,7.816V16.158C1,16.623 1.405,17 1.905,17H13.665C14.07,17 14.425,16.75 14.535,16.387Z"
android:strokeLineJoin="round"
android:fillColor="#00000000"
- android:strokeColor="#717784"
+ android:strokeColor="@color/nn_dark4"
android:strokeLineCap="round"/>
diff --git a/Aos/app/src/main/res/drawable/ic_x.xml b/Aos/app/src/main/res/drawable/ic_x.xml
index f0eda536..6d83a5b1 100644
--- a/Aos/app/src/main/res/drawable/ic_x.xml
+++ b/Aos/app/src/main/res/drawable/ic_x.xml
@@ -5,5 +5,5 @@
android:viewportHeight="11">
+ android:fillColor="@color/black"/>
diff --git a/Aos/app/src/main/res/layout/bottom_sheet_restaurant.xml b/Aos/app/src/main/res/layout/bottom_sheet_restaurant.xml
index 43068218..1fc80baf 100644
--- a/Aos/app/src/main/res/layout/bottom_sheet_restaurant.xml
+++ b/Aos/app/src/main/res/layout/bottom_sheet_restaurant.xml
@@ -139,12 +139,14 @@
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:layout_marginEnd="20dp"
+ android:layout_marginBottom="2dp"
android:background="@drawable/rect_primary3fill_nostroke_radius10"
android:minHeight="0dp"
android:paddingHorizontal="8dp"
android:paddingVertical="4dp"
android:text="@string/bottom_add_btn"
android:visibility="@{item.inMyList ? View.INVISIBLE : View.VISIBLE}"
+ app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/btn_go_review"
app:layout_constraintTop_toBottomOf="@id/tv_review_count_label" />
@@ -153,12 +155,13 @@
style="@style/TextSmallBold"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:layout_marginStart="28dp"
+ android:layout_marginBottom="2dp"
android:background="@drawable/rect_primary3fill_nostroke_radius10"
android:minHeight="0dp"
android:paddingHorizontal="8dp"
android:paddingVertical="4dp"
android:text="@string/bottom_review_btn"
+ app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/btn_add_my_restaurant" />
diff --git a/Aos/app/src/main/res/layout/dialog_loading.xml b/Aos/app/src/main/res/layout/dialog_loading.xml
index a38367df..0f2181f1 100644
--- a/Aos/app/src/main/res/layout/dialog_loading.xml
+++ b/Aos/app/src/main/res/layout/dialog_loading.xml
@@ -28,7 +28,7 @@
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:text="Loading ..."
- android:textColor="@color/white"
+ android:textColor="@color/nn_primary1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/progressBar" />
diff --git a/Aos/app/src/main/res/layout/dialog_recommend_restaurant.xml b/Aos/app/src/main/res/layout/dialog_recommend_restaurant.xml
index 79307862..2d54ce51 100644
--- a/Aos/app/src/main/res/layout/dialog_recommend_restaurant.xml
+++ b/Aos/app/src/main/res/layout/dialog_recommend_restaurant.xml
@@ -23,6 +23,18 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
+
+
@@ -197,7 +199,7 @@
android:text="@string/add_traffic_yes"
android:textColor="@color/nn_dark1"
android:visibility="@{vm.uiState.visitWithCar? View.INVISIBLE : View.VISIBLE}"
- app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/sd_traffic" />
diff --git a/Aos/app/src/main/res/layout/fragment_basic_signup.xml b/Aos/app/src/main/res/layout/fragment_basic_signup.xml
index d77e323d..0dfe3ab0 100644
--- a/Aos/app/src/main/res/layout/fragment_basic_signup.xml
+++ b/Aos/app/src/main/res/layout/fragment_basic_signup.xml
@@ -43,7 +43,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="20dp"
- android:text="기본 정보 입력"
+ android:text="@string/input_basic_information"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@id/btn_back"
app:layout_constraintTop_toTopOf="parent" />
@@ -56,7 +56,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
- android:text="이메일"
+ android:text="@string/email"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/layout_toolbar" />
@@ -76,7 +76,8 @@
style="@style/TextSmallRegular"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:hint="이메일"
+ android:hint="@string/email"
+ android:inputType="textEmailAddress"
android:text="@={vm.email}" />
@@ -95,16 +96,15 @@
@@ -114,7 +114,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="40dp"
- android:text="비밀번호"
+ android:text="@string/password"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_email_helper_text" />
@@ -133,7 +133,7 @@
style="@style/TextSmallRegular"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:hint="비밀번호"
+ android:hint="@string/password"
android:inputType="textPassword"
android:text="@={vm.password}" />
@@ -155,7 +155,7 @@
style="@style/TextSmallRegular"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:hint="비밀번호 확인"
+ android:hint="@string/check_password"
android:inputType="textPassword"
android:text="@={vm.passwordCheck}" />
@@ -183,7 +183,7 @@
android:background="@drawable/selector_next_btn"
android:enabled="@{vm.isDataReady()}"
android:onClick="@{() -> vm.navigateToDetailSignup()}"
- android:text="다음"
+ android:text="@string/next"
android:textColor="@color/selector_next_btn_text"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
diff --git a/Aos/app/src/main/res/layout/fragment_follow.xml b/Aos/app/src/main/res/layout/fragment_follow.xml
index c81c9746..440a130f 100644
--- a/Aos/app/src/main/res/layout/fragment_follow.xml
+++ b/Aos/app/src/main/res/layout/fragment_follow.xml
@@ -14,8 +14,7 @@
+ android:clipToPadding="false">
+ app:layout_constraintGuide_percent="0.9537713" />
diff --git a/Aos/app/src/main/res/layout/fragment_home.xml b/Aos/app/src/main/res/layout/fragment_home.xml
index 0c8267b2..1feaab5d 100644
--- a/Aos/app/src/main/res/layout/fragment_home.xml
+++ b/Aos/app/src/main/res/layout/fragment_home.xml
@@ -56,11 +56,12 @@
+
+
@@ -143,15 +151,18 @@
android:layout_marginTop="50dp"
android:layout_marginEnd="20dp"
android:background="@drawable/rect_naverfill_nostroke_12radius"
- android:drawableStart="@drawable/ic_naver"
- android:paddingStart="110dp"
android:text="@string/start_naver_text"
- android:textAlignment="textStart"
+ android:textAlignment="center"
android:textColor="@color/white"
android:textStyle="bold"
+ android:drawableStart="@drawable/ic_naver"
+ android:paddingStart="80dp"
+ android:paddingEnd="115dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@id/tv_do_signup" />
+ app:layout_constraintTop_toBottomOf="@id/tv_do_signup"
+ tools:ignore="RtlSymmetry" />
+
+
\ No newline at end of file
diff --git a/Aos/app/src/main/res/layout/fragment_restaurant_search.xml b/Aos/app/src/main/res/layout/fragment_restaurant_search.xml
index b43b27ca..2523aff6 100644
--- a/Aos/app/src/main/res/layout/fragment_restaurant_search.xml
+++ b/Aos/app/src/main/res/layout/fragment_restaurant_search.xml
@@ -58,6 +58,7 @@
+
+
@@ -130,6 +132,19 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_region" />
+
+
diff --git a/Aos/app/src/main/res/layout/item_my_list.xml b/Aos/app/src/main/res/layout/item_my_list.xml
index cc8e8c13..b2056a3d 100644
--- a/Aos/app/src/main/res/layout/item_my_list.xml
+++ b/Aos/app/src/main/res/layout/item_my_list.xml
@@ -48,7 +48,7 @@
style="@style/TextSmallBold"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:text="@{item.name}"
+ app:adjustText="@{item.name}"
app:layout_constraintStart_toStartOf="@id/gl_start"
app:layout_constraintTop_toTopOf="@id/gl_top" />
@@ -57,8 +57,8 @@
style="@style/CaptionRegular"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:text="@{item.address}"
android:textColor="@color/nn_primary5"
+ app:adjustText="@{item.address}"
app:layout_constraintBottom_toBottomOf="@id/gl_bottom"
app:layout_constraintStart_toStartOf="@id/gl_start"
app:layout_constraintTop_toTopOf="@id/gl_top" />
diff --git a/Aos/app/src/main/res/layout/item_user_detail_restaurant.xml b/Aos/app/src/main/res/layout/item_user_detail_restaurant.xml
index 989abbb3..17caf46c 100644
--- a/Aos/app/src/main/res/layout/item_user_detail_restaurant.xml
+++ b/Aos/app/src/main/res/layout/item_user_detail_restaurant.xml
@@ -4,6 +4,9 @@
+
@@ -53,6 +57,7 @@
style="@style/CaptionRegular"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
+ android:text="@{item.address}"
android:textColor="@color/nn_primary5"
app:layout_constraintBottom_toBottomOf="@id/gl_bottom"
app:layout_constraintStart_toStartOf="@id/gl_start"
@@ -63,6 +68,7 @@
style="@style/CaptionRegular"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
+ android:text="@{item.phoneNumber.empty? @string/phone_number_empty : item.phoneNumber}"
android:textColor="@color/nn_primary5"
app:layout_constraintBottom_toBottomOf="@id/gl_bottom"
app:layout_constraintStart_toStartOf="@id/gl_start"
diff --git a/Aos/app/src/main/res/layout/item_wish_list.xml b/Aos/app/src/main/res/layout/item_wish_list.xml
index 8b4376ba..f117918f 100644
--- a/Aos/app/src/main/res/layout/item_wish_list.xml
+++ b/Aos/app/src/main/res/layout/item_wish_list.xml
@@ -59,7 +59,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
- android:text="@{item.name}"
+ app:adjustText="@{item.name}"
app:layout_constraintStart_toEndOf="@id/iv_star"
app:layout_constraintTop_toTopOf="@id/gl_top" />
@@ -69,8 +69,8 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
- android:text="@{item.address}"
android:textColor="@color/nn_primary5"
+ app:adjustText="@{item.address}"
app:layout_constraintBottom_toBottomOf="@id/gl_bottom"
app:layout_constraintStart_toEndOf="@id/iv_star" />
diff --git a/Aos/app/src/main/res/navigation/nav_graph.xml b/Aos/app/src/main/res/navigation/nav_graph.xml
index 49f08c87..4a45ba35 100644
--- a/Aos/app/src/main/res/navigation/nav_graph.xml
+++ b/Aos/app/src/main/res/navigation/nav_graph.xml
@@ -46,6 +46,9 @@
android:name="nickName"
app:argType="string"
android:defaultValue=""/>
+
+
+
+
+ #FFEEEFF3
+
+ #FFFFFFFF
+ #FF000000
+
+ #FF333333
+ #69808A
+ #FF666A6B
+ #FF909694
+ #FFB9A6AE
+ #FFD2C4C1
+ #FFE9E3E3
+
+ #FF6E7D80
+ #FF4A5A62
+ #33E1D6CC
+ #66E1D6CC
+ #99E1D6CC
+ #CCE1D6CC
+ #FFE1D6CC
+
+ #FFE1D6CC
+ #FFCCE1D6
+ #FFE8E2E4
+ #FFB3ADA9
+ #FF8D8787
+ #FF625F5Fㅜ
+
+
+
+ #FFCC4141
+ #FFF59A11
+ #FFE0DC88
+ #FF4AA754
+ #FF0D1986
+ #FF222152
+ #FF6437E1
+
+ #FF03C75A
+ #4D909D8F
+
+
\ No newline at end of file
diff --git a/Aos/app/src/main/res/values/strings.xml b/Aos/app/src/main/res/values/strings.xml
index 423f34d3..0759d668 100644
--- a/Aos/app/src/main/res/values/strings.xml
+++ b/Aos/app/src/main/res/values/strings.xml
@@ -118,30 +118,40 @@
오늘의 추천 음식점
리뷰 전체보기
]]>
+ 아직 추가한 맛집이 없습니다.
+ 전화번호 없음
+ \n\n추천드릴 음식점이 존재하지 않습니다.\n\n
+ (선택) 사진 등록
+ 이메일 중복확인
+ 다음
+ 기본 정보 입력
+ 비밀번호 확인
- - 용산구
- - 은평구
- - 강서구
- 강남구
- - 서초구
- - 동작구
+ - 강동구
+ - 강북구
+ - 강서구
- 관악구
- - 금천구
- - 영등포구
- - 양천구
- - 구로구
- - 송파구
- 광진구
- - 강동구
- - 성동구
- - 중구
- - 종로구
- - 서대문구
+ - 구로구
+ - 금천구
+ - 노원구
+ - 도봉구
+ - 동대문구
+ - 동작구
- 마포구
+ - 서대문구
+ - 서초구
+ - 성동구
+ - 성북구
+ - 송파구
+ - 양천구
+ - 영등포구
+ - 용산구
- 은평구
- - 도봉구
- - 노원구
+ - 종로구
+ - 중구
- 중랑구
diff --git a/Aos/app/src/main/res/values/themes.xml b/Aos/app/src/main/res/values/themes.xml
index b3e06e54..f18f1392 100644
--- a/Aos/app/src/main/res/values/themes.xml
+++ b/Aos/app/src/main/res/values/themes.xml
@@ -1,13 +1,20 @@
-
+
+
+
+
+
-
\ No newline at end of file
+
+
+
diff --git a/README.md b/README.md
index 7a6a43aa..69666f15 100644
--- a/README.md
+++ b/README.md
@@ -1,26 +1,110 @@
# 🍚 니밥내밥 ( NIBOB-NEBOB )
-**맛집지도 공유 플랫폼, 니밥내밥**
-내 맛집을 지도에 등록하고,
-다른 유저와 내 맛집 지도를 공유해보세요.
+![Group 2608558](https://github.com/boostcampwm2023/and06-nibobnebob/assets/82799840/31c93c05-b519-4810-b53e-5d6bfe5cfcba)
+
+![Group 2608569 (1)](https://github.com/boostcampwm2023/and06-nibobnebob/assets/82799840/8323d852-f60b-4202-b39f-5eb0595b3e65)
+
[👉🏻 wiki 바로가기](https://github.com/boostcampwm2023/and06-nibobnebob/wiki)
+
+
+
+
+
-## 🔑 기능 소개
-*구현 이후 작성 예정*
+## 🤔 주요 개발 과정과 고민
+
+|주제|설명|
+|--|--|
+|[**앱 flow**](https://github.com/boostcampwm2023/and06-nibobnebob/wiki/%E3%80%B0%EF%B8%8F-App-%ED%99%94%EB%A9%B4-flow)|각 페이지별 깊은 depth 로 인해 발생하는 소통 문제 해결을 위해 flow 작성.|
+|[**권한 허용 여부에 따른 메인화면 flow**](https://github.com/boostcampwm2023/and06-nibobnebob/wiki/%F0%9F%93%8D-%EC%9C%84%EC%B9%98-%EA%B6%8C%ED%95%9C-%ED%97%88%EC%9A%A9%EC%97%90-%EB%94%B0%EB%A5%B8-%EB%A9%94%EC%9D%B8-%ED%99%94%EB%A9%B4-flow)|지도가 핵심 기능인 만큼, 위치 권한과 GPS ON 여부에 따라 메인 화면의 flow 가 달라진다.|
+|[**사용자 경험을 고려한 지도 필터간 전환**](https://github.com/boostcampwm2023/and06-nibobnebob/wiki/%5BK011%5D-%EB%85%B8%EA%B7%A0%EC%9A%B1#%EB%84%A4%EC%9D%B4%EB%B2%84%EC%A7%80%EB%8F%84-%EC%82%AC%EC%9A%A9%EC%9E%90-%EA%B2%BD%ED%97%98-%EA%B0%9C%EC%84%A0%ED%95%98%EA%B8%B0)|필터에 따라 다르게 보여지는 지도와 맛집 마커를 전환하는 과정에서 사용자 경험을 고려했다.|
+|[**왜 공공 API를 선택했나?**](https://github.com/boostcampwm2023/and06-nibobnebob/wiki/%E2%9C%92-%5BBE%5D-%EA%B0%9C%EB%B0%9C%EC%9D%BC%EC%A7%80#%EA%B3%B5%EA%B3%B5-api-%EC%84%A0%ED%83%9D-%EC%9D%B4%EC%9C%A0)|근처의 모든 음식점을 가져올 때, 네이버/카카오 api 가 아닌 공공 api 를 선택한 이유?|
+|[**왜 이미지를 리사이징 하나?**](https://github.com/boostcampwm2023/and06-nibobnebob/wiki/%E2%9C%92-%5BBE%5D-%EA%B0%9C%EB%B0%9C%EC%9D%BC%EC%A7%80#%EC%9D%B4%EB%AF%B8%EC%A7%80-%EB%A6%AC%EC%82%AC%EC%9D%B4%EC%A7%95)|Object Storage에 이미지를 업로드 할 때, 리사이징 하는 이유?|
+|[**어플리케이션 통신 과정**](https://github.com/boostcampwm2023/and06-nibobnebob/wiki/%E2%9C%92-%5BBE%5D-%EA%B0%9C%EB%B0%9C%EC%9D%BC%EC%A7%80#%EC%96%B4%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98-%ED%86%B5%EC%8B%A0-%EA%B3%BC%EC%A0%95)|사용자의 요청부터 서버의 응답까지 어플리케이션 통신 과정 설계 이유|
+
+
+
+📍 [AOS 기술 선택 이유 바로가기](https://github.com/boostcampwm2023/and06-nibobnebob/wiki/%E2%9C%92-%5BAOS%5D-%EA%B0%9C%EB%B0%9C%EC%9D%BC%EC%A7%80#%EA%B8%B0%EC%88%A0%EC%A0%81-%EA%B3%A0%EB%AF%BC)
+
+📍 [BE 기술 선택 이유 바로가기](https://github.com/boostcampwm2023/and06-nibobnebob/wiki/%E2%9C%92-%5BBE%5D-%EA%B0%9C%EB%B0%9C%EC%9D%BC%EC%A7%80#%EA%B8%B0%EC%88%A0%EC%A0%81-%EB%8F%84%EC%A0%84)
+
+
+
+## 📲 주요 기능 동작 화면
+
+### 로그인 / 회원가입
+일반 회원가입|네이버 회원가입|
+|------|---|
+|
+
+
+
+### 홈화면
+홈 지도|위치 트래킹|홈 검색|
+|------|---|---|
+||||
+
+
+
+### 팔로우
+
+유저 추천 및 팔로우|지역으로 유저 검색|유저 검색 및 유저상세|
+|------|---|---|
+||||
+
+
+
+### 맛집 등록하기 및 상세보기
+
+맛집 등록하기|맛집 상세보기|위시리스트 추가하기|
+|------|---|---|
+||||
+
+
+
+### 마이페이지
+프로필 수정|나의 위시리스트|내 맛집 리스트
+|------|---|---|
+||||
## 📚 기술 스택
- Android
+
+| 분류 | 사용기술 |
+| -- | -- |
+|아키텍처|MVVM, Clean Architecture|
+|네트워크 통신|Retrofit, Okhttp|
+|이미지 처리|Glide, MultiPart|
+|비동기|Coroutine, Flow|
+|DI|Hilt|
+|데이터 저장|DataStore|
+
+
+
- Backend
+| 분류 | 사용 기술|
+|-- | --|
+|서버 프레임워크 | NestJS|
+|프로그래밍 언어 | TypeScript|
+|테스트 | Jest, Docker, Apache Jmeter|
+|로깅 | Winston|
+|DB | PostgreSQL, TypeORM|
+|웹 서버 | NginX|
+|클라우드 컴퓨팅 | Naver Cloud Platform|
+|이미지 저장 | Object Storage, multer, sharp|
+|CI/CD | GitHub Action, Docker|
+
+
## 👬 팀 소개
@@ -44,3 +128,4 @@
| [K011_노균욱](https://github.com/BENDENG1) | [K015_박진성](https://github.com/plashdof) | [K024_오세영](https://github.com/yy0ung) | [J123_이태훈](https://github.com/LeeTH916) | [J155_최근혁](https://github.com/GeunH) |
+
diff --git a/be/Dockerfile b/be/Dockerfile
new file mode 100644
index 00000000..2a17d513
--- /dev/null
+++ b/be/Dockerfile
@@ -0,0 +1,23 @@
+# 베이스 이미지 선택
+FROM node:20
+
+# 작업 디렉토리 설정
+WORKDIR /usr/src/app
+
+# 종속성 파일 복사 (package.json 및 package-lock.json)
+COPY package*.json ./
+
+# 종속성 설치
+RUN npm install
+
+# 애플리케이션 소스 코드 복사
+COPY . .
+
+# TypeScript 컴파일
+RUN npm run build
+
+# 애플리케이션 실행 포트 지정
+EXPOSE 8000
+
+# 애플리케이션 실행 명령어
+CMD ["node", "dist/src/main"]
diff --git a/be/dockerignore b/be/dockerignore
new file mode 100644
index 00000000..24cdedf8
--- /dev/null
+++ b/be/dockerignore
@@ -0,0 +1,23 @@
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+
+# dependencies
+/node_modules
+/.pnp
+.pnp.js
+
+# testing
+/coverage
+
+# production
+/build
+
+# misc
+.DS_Store
+.env.local
+.env.development.local
+.env.test.local
+.env.production.local
+
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
\ No newline at end of file
diff --git a/be/package-lock.json b/be/package-lock.json
index 26abfff5..7f28bf65 100644
--- a/be/package-lock.json
+++ b/be/package-lock.json
@@ -14,18 +14,21 @@
"@nestjs/jwt": "^10.2.0",
"@nestjs/passport": "^10.0.2",
"@nestjs/platform-express": "^10.0.0",
+ "@nestjs/schedule": "^4.0.0",
"@nestjs/swagger": "^7.1.15",
"@nestjs/typeorm": "^10.0.1",
"aws-sdk": "^2.348.0",
"axios": "^1.6.2",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.0",
+ "multer": "^1.4.5-lts.1",
"passport": "^0.6.0",
"passport-jwt": "^4.0.1",
"passport-naver": "^1.0.6",
"pg": "^8.11.3",
"reflect-metadata": "^0.1.13",
"rxjs": "^7.8.1",
+ "sharp": "^0.33.0",
"swagger-ui-express": "^5.0.0",
"typeorm": "^0.3.17",
"uuid": "^9.0.1",
@@ -37,6 +40,7 @@
"@nestjs/testing": "^10.0.0",
"@types/express": "^4.17.17",
"@types/jest": "^29.5.2",
+ "@types/multer": "^1.4.11",
"@types/node": "^20.3.1",
"@types/supertest": "^2.0.12",
"@types/winston": "^2.4.4",
@@ -894,6 +898,15 @@
"kuler": "^2.0.0"
}
},
+ "node_modules/@emnapi/runtime": {
+ "version": "0.44.0",
+ "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-0.44.0.tgz",
+ "integrity": "sha512-ZX/etZEZw8DR7zAB1eVQT40lNo0jeqpb6dCgOvctB6FIQ5PoXfMuNY8+ayQfu8tNQbAB8gQWSSJupR8NxeiZXw==",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
"node_modules/@eslint-community/eslint-utils": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz",
@@ -1005,6 +1018,437 @@
"integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==",
"dev": true
},
+ "node_modules/@img/sharp-darwin-arm64": {
+ "version": "0.33.0",
+ "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.0.tgz",
+ "integrity": "sha512-070tEheekI1LJWTGPC9WlQEa5UoKTXzzlORBHMX4TbfUxMiL336YHR8vBEUNsjse0RJCX8dZ4ZXwT595aEF1ug==",
+ "cpu": [
+ "arm64"
+ ],
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "glibc": ">=2.26",
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0",
+ "npm": ">=9.6.5",
+ "pnpm": ">=7.1.0",
+ "yarn": ">=3.2.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-darwin-arm64": "1.0.0"
+ }
+ },
+ "node_modules/@img/sharp-darwin-x64": {
+ "version": "0.33.0",
+ "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.0.tgz",
+ "integrity": "sha512-pu/nvn152F3qbPeUkr+4e9zVvEhD3jhwzF473veQfMPkOYo9aoWXSfdZH/E6F+nYC3qvFjbxbvdDbUtEbghLqw==",
+ "cpu": [
+ "x64"
+ ],
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "glibc": ">=2.26",
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0",
+ "npm": ">=9.6.5",
+ "pnpm": ">=7.1.0",
+ "yarn": ">=3.2.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-darwin-x64": "1.0.0"
+ }
+ },
+ "node_modules/@img/sharp-libvips-darwin-arm64": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.0.tgz",
+ "integrity": "sha512-VzYd6OwnUR81sInf3alj1wiokY50DjsHz5bvfnsFpxs5tqQxESoHtJO6xyksDs3RIkyhMWq2FufXo6GNSU9BMw==",
+ "cpu": [
+ "arm64"
+ ],
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "macos": ">=11",
+ "npm": ">=9.6.5",
+ "pnpm": ">=7.1.0",
+ "yarn": ">=3.2.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-darwin-x64": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.0.tgz",
+ "integrity": "sha512-dD9OznTlHD6aovRswaPNEy8dKtSAmNo4++tO7uuR4o5VxbVAOoEQ1uSmN4iFAdQneTHws1lkTZeiXPrcCkh6IA==",
+ "cpu": [
+ "x64"
+ ],
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "macos": ">=10.13",
+ "npm": ">=9.6.5",
+ "pnpm": ">=7.1.0",
+ "yarn": ">=3.2.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linux-arm": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.0.tgz",
+ "integrity": "sha512-VwgD2eEikDJUk09Mn9Dzi1OW2OJFRQK+XlBTkUNmAWPrtj8Ly0yq05DFgu1VCMx2/DqCGQVi5A1dM9hTmxf3uw==",
+ "cpu": [
+ "arm"
+ ],
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "glibc": ">=2.28",
+ "npm": ">=9.6.5",
+ "pnpm": ">=7.1.0",
+ "yarn": ">=3.2.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linux-arm64": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.0.tgz",
+ "integrity": "sha512-xTYThiqEZEZc0PRU90yVtM3KE7lw1bKdnDQ9kCTHWbqWyHOe4NpPOtMGy27YnN51q0J5dqRrvicfPbALIOeAZA==",
+ "cpu": [
+ "arm64"
+ ],
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "glibc": ">=2.26",
+ "npm": ">=9.6.5",
+ "pnpm": ">=7.1.0",
+ "yarn": ">=3.2.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linux-s390x": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.0.tgz",
+ "integrity": "sha512-o9E46WWBC6JsBlwU4QyU9578G77HBDT1NInd+aERfxeOPbk0qBZHgoDsQmA2v9TbqJRWzoBPx1aLOhprBMgPjw==",
+ "cpu": [
+ "s390x"
+ ],
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "glibc": ">=2.28",
+ "npm": ">=9.6.5",
+ "pnpm": ">=7.1.0",
+ "yarn": ">=3.2.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linux-x64": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.0.tgz",
+ "integrity": "sha512-naldaJy4hSVhWBgEjfdBY85CAa4UO+W1nx6a1sWStHZ7EUfNiuBTTN2KUYT5dH1+p/xij1t2QSXfCiFJoC5S/Q==",
+ "cpu": [
+ "x64"
+ ],
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "glibc": ">=2.26",
+ "npm": ">=9.6.5",
+ "pnpm": ">=7.1.0",
+ "yarn": ">=3.2.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linuxmusl-arm64": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.0.tgz",
+ "integrity": "sha512-OdorplCyvmSAPsoJLldtLh3nLxRrkAAAOHsGWGDYfN0kh730gifK+UZb3dWORRa6EusNqCTjfXV4GxvgJ/nPDQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "musl": ">=1.2.2",
+ "npm": ">=9.6.5",
+ "pnpm": ">=7.1.0",
+ "yarn": ">=3.2.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linuxmusl-x64": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.0.tgz",
+ "integrity": "sha512-FW8iK6rJrg+X2jKD0Ajhjv6y74lToIBEvkZhl42nZt563FfxkCYacrXZtd+q/sRQDypQLzY5WdLkVTbJoPyqNg==",
+ "cpu": [
+ "x64"
+ ],
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "musl": ">=1.2.2",
+ "npm": ">=9.6.5",
+ "pnpm": ">=7.1.0",
+ "yarn": ">=3.2.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-linux-arm": {
+ "version": "0.33.0",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.0.tgz",
+ "integrity": "sha512-4horD3wMFd5a0ddbDY8/dXU9CaOgHjEHALAddXgafoR5oWq5s8X61PDgsSeh4Qupsdo6ycfPPSSNBrfVQnwwrg==",
+ "cpu": [
+ "arm"
+ ],
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "glibc": ">=2.28",
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0",
+ "npm": ">=9.6.5",
+ "pnpm": ">=7.1.0",
+ "yarn": ">=3.2.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linux-arm": "1.0.0"
+ }
+ },
+ "node_modules/@img/sharp-linux-arm64": {
+ "version": "0.33.0",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.0.tgz",
+ "integrity": "sha512-dcomVSrtgF70SyOr8RCOCQ8XGVThXwe71A1d8MGA+mXEVRJ/J6/TrCbBEJh9ddcEIIsrnrkolaEvYSHqVhswQw==",
+ "cpu": [
+ "arm64"
+ ],
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "glibc": ">=2.26",
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0",
+ "npm": ">=9.6.5",
+ "pnpm": ">=7.1.0",
+ "yarn": ">=3.2.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linux-arm64": "1.0.0"
+ }
+ },
+ "node_modules/@img/sharp-linux-s390x": {
+ "version": "0.33.0",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.0.tgz",
+ "integrity": "sha512-TiVJbx38J2rNVfA309ffSOB+3/7wOsZYQEOlKqOUdWD/nqkjNGrX+YQGz7nzcf5oy2lC+d37+w183iNXRZNngQ==",
+ "cpu": [
+ "s390x"
+ ],
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "glibc": ">=2.28",
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0",
+ "npm": ">=9.6.5",
+ "pnpm": ">=7.1.0",
+ "yarn": ">=3.2.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linux-s390x": "1.0.0"
+ }
+ },
+ "node_modules/@img/sharp-linux-x64": {
+ "version": "0.33.0",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.0.tgz",
+ "integrity": "sha512-PaZM4Zi7/Ek71WgTdvR+KzTZpBqrQOFcPe7/8ZoPRlTYYRe43k6TWsf4GVH6XKRLMYeSp8J89RfAhBrSP4itNA==",
+ "cpu": [
+ "x64"
+ ],
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "glibc": ">=2.26",
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0",
+ "npm": ">=9.6.5",
+ "pnpm": ">=7.1.0",
+ "yarn": ">=3.2.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linux-x64": "1.0.0"
+ }
+ },
+ "node_modules/@img/sharp-linuxmusl-arm64": {
+ "version": "0.33.0",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.0.tgz",
+ "integrity": "sha512-1QLbbN0zt+32eVrg7bb1lwtvEaZwlhEsY1OrijroMkwAqlHqFj6R33Y47s2XUv7P6Ie1PwCxK/uFnNqMnkd5kg==",
+ "cpu": [
+ "arm64"
+ ],
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "musl": ">=1.2.2",
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0",
+ "npm": ">=9.6.5",
+ "pnpm": ">=7.1.0",
+ "yarn": ">=3.2.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linuxmusl-arm64": "1.0.0"
+ }
+ },
+ "node_modules/@img/sharp-linuxmusl-x64": {
+ "version": "0.33.0",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.0.tgz",
+ "integrity": "sha512-CecqgB/CnkvCWFhmfN9ZhPGMLXaEBXl4o7WtA6U3Ztrlh/s7FUKX4vNxpMSYLIrWuuzjiaYdfU3+Tdqh1xaHfw==",
+ "cpu": [
+ "x64"
+ ],
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "musl": ">=1.2.2",
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0",
+ "npm": ">=9.6.5",
+ "pnpm": ">=7.1.0",
+ "yarn": ">=3.2.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linuxmusl-x64": "1.0.0"
+ }
+ },
+ "node_modules/@img/sharp-wasm32": {
+ "version": "0.33.0",
+ "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.0.tgz",
+ "integrity": "sha512-Hn4js32gUX9qkISlemZBUPuMs0k/xNJebUNl/L6djnU07B/HAA2KaxRVb3HvbU5fL242hLOcp0+tR+M8dvJUFw==",
+ "cpu": [
+ "wasm32"
+ ],
+ "optional": true,
+ "dependencies": {
+ "@emnapi/runtime": "^0.44.0"
+ },
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0",
+ "npm": ">=9.6.5",
+ "pnpm": ">=7.1.0",
+ "yarn": ">=3.2.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-win32-ia32": {
+ "version": "0.33.0",
+ "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.0.tgz",
+ "integrity": "sha512-5HfcsCZi3l5nPRF2q3bllMVMDXBqEWI3Q8KQONfzl0TferFE5lnsIG0A1YrntMAGqvkzdW6y1Ci1A2uTvxhfzg==",
+ "cpu": [
+ "ia32"
+ ],
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0",
+ "npm": ">=9.6.5",
+ "pnpm": ">=7.1.0",
+ "yarn": ">=3.2.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-win32-x64": {
+ "version": "0.33.0",
+ "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.0.tgz",
+ "integrity": "sha512-i3DtP/2ce1yKFj4OzOnOYltOEL/+dp4dc4dJXJBv6god1AFTcmkaA99H/7SwOmkCOBQkbVvA3lCGm3/5nDtf9Q==",
+ "cpu": [
+ "x64"
+ ],
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0",
+ "npm": ">=9.6.5",
+ "pnpm": ">=7.1.0",
+ "yarn": ">=3.2.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
"node_modules/@isaacs/cliui": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
@@ -2043,6 +2487,37 @@
"@nestjs/core": "^10.0.0"
}
},
+ "node_modules/@nestjs/platform-express/node_modules/multer": {
+ "version": "1.4.4-lts.1",
+ "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.4-lts.1.tgz",
+ "integrity": "sha512-WeSGziVj6+Z2/MwQo3GvqzgR+9Uc+qt8SwHKh3gvNPiISKfsMfG4SvCOFYlxxgkXt7yIV2i1yczehm0EOKIxIg==",
+ "dependencies": {
+ "append-field": "^1.0.0",
+ "busboy": "^1.0.0",
+ "concat-stream": "^1.5.2",
+ "mkdirp": "^0.5.4",
+ "object-assign": "^4.1.1",
+ "type-is": "^1.6.4",
+ "xtend": "^4.0.0"
+ },
+ "engines": {
+ "node": ">= 6.0.0"
+ }
+ },
+ "node_modules/@nestjs/schedule": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/@nestjs/schedule/-/schedule-4.0.0.tgz",
+ "integrity": "sha512-zz4h54m/F/1qyQKvMJCRphmuwGqJltDAkFxUXCVqJBXEs5kbPt93Pza3heCQOcMH22MZNhGlc9DmDMLXVHmgVQ==",
+ "dependencies": {
+ "cron": "3.1.3",
+ "uuid": "9.0.1"
+ },
+ "peerDependencies": {
+ "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0",
+ "@nestjs/core": "^8.0.0 || ^9.0.0 || ^10.0.0",
+ "reflect-metadata": "^0.1.12"
+ }
+ },
"node_modules/@nestjs/schematics": {
"version": "10.0.3",
"resolved": "https://registry.npmjs.org/@nestjs/schematics/-/schematics-10.0.3.tgz",
@@ -2446,12 +2921,26 @@
"@types/node": "*"
}
},
+ "node_modules/@types/luxon": {
+ "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/mime": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
"integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==",
"dev": true
},
+ "node_modules/@types/multer": {
+ "version": "1.4.11",
+ "resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.11.tgz",
+ "integrity": "sha512-svK240gr6LVWvv3YGyhLlA+6LRRWA4mnGIU7RcNmgjBYFl6665wcXrRfxGp5tEPVHUNm5FMcmq7too9bxCwX/w==",
+ "dev": true,
+ "dependencies": {
+ "@types/express": "*"
+ }
+ },
"node_modules/@types/node": {
"version": "20.9.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.9.0.tgz",
@@ -4438,6 +4927,15 @@
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
"devOptional": true
},
+ "node_modules/cron": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/cron/-/cron-3.1.3.tgz",
+ "integrity": "sha512-KVxeKTKYj2eNzN4ElnT6nRSbjbfhyxR92O/Jdp6SH3pc05CDJws59jBrZWEMQlxevCiE6QUTrXy+Im3vC3oD3A==",
+ "dependencies": {
+ "@types/luxon": "~3.3.0",
+ "luxon": "~3.4.0"
+ }
+ },
"node_modules/cross-spawn": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
@@ -4765,7 +5263,6 @@
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.2.tgz",
"integrity": "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==",
- "dev": true,
"engines": {
"node": ">=8"
}
@@ -8281,6 +8778,14 @@
"yallist": "^3.0.2"
}
},
+ "node_modules/luxon": {
+ "version": "3.4.4",
+ "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.4.tgz",
+ "integrity": "sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA==",
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/macos-release": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/macos-release/-/macos-release-2.5.1.tgz",
@@ -8579,9 +9084,9 @@
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"node_modules/multer": {
- "version": "1.4.4-lts.1",
- "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.4-lts.1.tgz",
- "integrity": "sha512-WeSGziVj6+Z2/MwQo3GvqzgR+9Uc+qt8SwHKh3gvNPiISKfsMfG4SvCOFYlxxgkXt7yIV2i1yczehm0EOKIxIg==",
+ "version": "1.4.5-lts.1",
+ "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.1.tgz",
+ "integrity": "sha512-ywPWvcDMeH+z9gQq5qYHCCy+ethsk4goepZ45GLD63fOu0YcNecQxi64nDs3qluZB+murG3/D4dJ7+dGctcCQQ==",
"dependencies": {
"append-field": "^1.0.0",
"busboy": "^1.0.0",
@@ -10509,6 +11014,57 @@
"sha.js": "bin.js"
}
},
+ "node_modules/sharp": {
+ "version": "0.33.0",
+ "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.0.tgz",
+ "integrity": "sha512-99DZKudjm/Rmz+M0/26t4DKpXyywAOJaayGS9boEn7FvgtG0RYBi46uPE2c+obcJRtA3AZa0QwJot63gJQ1F0Q==",
+ "hasInstallScript": true,
+ "dependencies": {
+ "color": "^4.2.3",
+ "detect-libc": "^2.0.2",
+ "semver": "^7.5.4"
+ },
+ "engines": {
+ "libvips": ">=8.15.0",
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-darwin-arm64": "0.33.0",
+ "@img/sharp-darwin-x64": "0.33.0",
+ "@img/sharp-libvips-darwin-arm64": "1.0.0",
+ "@img/sharp-libvips-darwin-x64": "1.0.0",
+ "@img/sharp-libvips-linux-arm": "1.0.0",
+ "@img/sharp-libvips-linux-arm64": "1.0.0",
+ "@img/sharp-libvips-linux-s390x": "1.0.0",
+ "@img/sharp-libvips-linux-x64": "1.0.0",
+ "@img/sharp-libvips-linuxmusl-arm64": "1.0.0",
+ "@img/sharp-libvips-linuxmusl-x64": "1.0.0",
+ "@img/sharp-linux-arm": "0.33.0",
+ "@img/sharp-linux-arm64": "0.33.0",
+ "@img/sharp-linux-s390x": "0.33.0",
+ "@img/sharp-linux-x64": "0.33.0",
+ "@img/sharp-linuxmusl-arm64": "0.33.0",
+ "@img/sharp-linuxmusl-x64": "0.33.0",
+ "@img/sharp-wasm32": "0.33.0",
+ "@img/sharp-win32-ia32": "0.33.0",
+ "@img/sharp-win32-x64": "0.33.0"
+ }
+ },
+ "node_modules/sharp/node_modules/color": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
+ "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==",
+ "dependencies": {
+ "color-convert": "^2.0.1",
+ "color-string": "^1.9.0"
+ },
+ "engines": {
+ "node": ">=12.5.0"
+ }
+ },
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
diff --git a/be/package.json b/be/package.json
index 02fd800a..02bcb026 100644
--- a/be/package.json
+++ b/be/package.json
@@ -26,18 +26,21 @@
"@nestjs/jwt": "^10.2.0",
"@nestjs/passport": "^10.0.2",
"@nestjs/platform-express": "^10.0.0",
+ "@nestjs/schedule": "^4.0.0",
"@nestjs/swagger": "^7.1.15",
"@nestjs/typeorm": "^10.0.1",
"aws-sdk": "^2.348.0",
"axios": "^1.6.2",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.0",
+ "multer": "^1.4.5-lts.1",
"passport": "^0.6.0",
"passport-jwt": "^4.0.1",
"passport-naver": "^1.0.6",
"pg": "^8.11.3",
"reflect-metadata": "^0.1.13",
"rxjs": "^7.8.1",
+ "sharp": "^0.33.0",
"swagger-ui-express": "^5.0.0",
"typeorm": "^0.3.17",
"uuid": "^9.0.1",
@@ -49,6 +52,7 @@
"@nestjs/testing": "^10.0.0",
"@types/express": "^4.17.17",
"@types/jest": "^29.5.2",
+ "@types/multer": "^1.4.11",
"@types/node": "^20.3.1",
"@types/supertest": "^2.0.12",
"@types/winston": "^2.4.4",
@@ -92,4 +96,4 @@
],
"coverageDirectory": "../coverage"
}
-}
\ No newline at end of file
+}
diff --git a/be/src/auth/auth.controller.ts b/be/src/auth/auth.controller.ts
index bbf98afc..3a1c195e 100644
--- a/be/src/auth/auth.controller.ts
+++ b/be/src/auth/auth.controller.ts
@@ -4,6 +4,7 @@ import {
Headers,
Post,
UseGuards,
+ UsePipes,
ValidationPipe,
} from "@nestjs/common";
import { AuthService } from "./auth.service";
@@ -15,11 +16,22 @@ import {
ApiBody,
} from "@nestjs/swagger";
import { RefreshTokenDto } from "./dto/refreshToken.dto";
+import { LoginInfoDto } from "./dto/loginInfo.dto";
@ApiTags("Authentication")
@Controller("auth")
export class AuthController {
- constructor(private authService: AuthService) {}
+ constructor(private authService: AuthService) { }
+
+ @Post("login")
+ @ApiOperation({ summary: "일반 로그인" })
+ @ApiResponse({ status: 200, description: "성공적으로 로그인됨." })
+ @ApiResponse({ status: 401, description: "인증 오류" })
+ @UsePipes(new ValidationPipe)
+ login(@Body() loginInfoDto: LoginInfoDto) {
+ return this.authService.login(loginInfoDto);
+ }
+
@Post("social-login")
@ApiOperation({ summary: "네이버 소셜 로그인" })
diff --git a/be/src/auth/auth.module.ts b/be/src/auth/auth.module.ts
index 7606b236..03a526bd 100644
--- a/be/src/auth/auth.module.ts
+++ b/be/src/auth/auth.module.ts
@@ -5,6 +5,7 @@ import { JwtModule } from "@nestjs/jwt";
import { PassportModule } from "@nestjs/passport";
import { UserModule } from "../user/user.module";
import { JwtStrategy } from "./strategy/jwt.strategy";
+import { AuthRepository } from "./auth.repository";
@Module({
imports: [
@@ -18,7 +19,7 @@ import { JwtStrategy } from "./strategy/jwt.strategy";
forwardRef(() => UserModule),
],
controllers: [AuthController],
- providers: [AuthService, JwtStrategy],
- exports: [PassportModule],
+ providers: [AuthService, JwtStrategy, AuthRepository],
+ exports: [PassportModule, AuthService],
})
-export class AuthModule {}
+export class AuthModule { }
diff --git a/be/src/auth/auth.repository.ts b/be/src/auth/auth.repository.ts
new file mode 100644
index 00000000..f91b8f86
--- /dev/null
+++ b/be/src/auth/auth.repository.ts
@@ -0,0 +1,14 @@
+import { DataSource, IsNull, Repository, Not, In } from "typeorm";
+import {
+ ConflictException,
+ Injectable,
+ BadRequestException,
+} from "@nestjs/common";
+import { AuthRefreshTokenEntity } from "./entity/auth.refreshtoken.entity";
+
+@Injectable()
+export class AuthRepository extends Repository {
+ constructor(private dataSource: DataSource) {
+ super(AuthRefreshTokenEntity, dataSource.createEntityManager());
+ }
+}
\ No newline at end of file
diff --git a/be/src/auth/auth.service.ts b/be/src/auth/auth.service.ts
index 3b718b3b..a5ad685d 100644
--- a/be/src/auth/auth.service.ts
+++ b/be/src/auth/auth.service.ts
@@ -3,17 +3,39 @@ import {
NotFoundException,
HttpException,
HttpStatus,
+ BadRequestException,
+ UnauthorizedException,
+ ForbiddenException,
} from "@nestjs/common";
import { UserRepository } from "../user/user.repository";
import { JwtService } from "@nestjs/jwt";
import axios from "axios";
+import { LoginInfoDto } from "./dto/loginInfo.dto";
+import { comparePasswords } from "../utils/encryption.utils";
+import { AuthRepository } from "./auth.repository";
@Injectable()
export class AuthService {
constructor(
private userRepository: UserRepository,
- private jwtService: JwtService
- ) {}
+ private jwtService: JwtService,
+ private authRepository: AuthRepository
+ ) { }
+ async login(loginInfoDto: LoginInfoDto) {
+ const data = await this.userRepository.findOne({ select: ["password"], where: { email: loginInfoDto.email, provider: "site" } })
+ try {
+ const result = await comparePasswords(loginInfoDto.password, data["password"]);
+ if (result) return this.signin(loginInfoDto);
+ else throw new HttpException("LOGIN FAILED", HttpStatus.FORBIDDEN);
+ } catch (err) {
+ throw new HttpException("LOGIN FAILED", HttpStatus.FORBIDDEN);
+ }
+ }
+
+ async logout(id: number) {
+ await this.authRepository.delete({ id: id });
+ }
+
async NaverAuth(authorization: string) {
if (!authorization) {
throw new HttpException(
@@ -36,21 +58,24 @@ export class AuthService {
}
}
+ async createTokens(id: number) {
+ const payload = { id: id };
+ const accessToken = this.jwtService.sign(payload);
+ const refreshToken = this.jwtService.sign(payload, {
+ secret: "nibobnebob",
+ expiresIn: "7d",
+ });
+ await this.authRepository.upsert({ id: id, accessToken: accessToken, refreshToken: refreshToken }, ["id"]);
+ return { accessToken, refreshToken };
+ }
+
async signin(loginRequestUser: any) {
const user = await this.userRepository.findOneBy({
email: loginRequestUser.email,
});
if (user) {
- const payload = { id: user.id };
- const accessToken = this.jwtService.sign(payload);
-
- const refreshToken = this.jwtService.sign(payload, {
- secret: "nibobnebob",
- expiresIn: "7d",
- });
-
- return { accessToken, refreshToken };
+ return await this.createTokens(user.id);
} else {
throw new NotFoundException(
"사용자가 등록되어 있지 않습니다. 회원가입을 진행해주세요"
@@ -63,9 +88,13 @@ export class AuthService {
const decoded = this.jwtService.verify(refreshToken, {
secret: "nibobnebob",
});
- const payload = { id: decoded.id };
- const accessToken = this.jwtService.sign(payload);
- return { accessToken };
+ if (await this.authRepository.findOne({ where: { id: decoded.id, refreshToken: refreshToken } })) {
+ const payload = { id: decoded.id };
+ const accessToken = this.jwtService.sign(payload);
+ await this.authRepository.update(decoded.id, { accessToken: accessToken });
+ return { accessToken };
+ }
+ throw new HttpException("Invalid refresh token", HttpStatus.UNAUTHORIZED);
} catch (err) {
throw new HttpException("Invalid refresh token", HttpStatus.UNAUTHORIZED);
}
diff --git a/be/src/auth/dto/loginInfo.dto.ts b/be/src/auth/dto/loginInfo.dto.ts
new file mode 100644
index 00000000..c4f68c77
--- /dev/null
+++ b/be/src/auth/dto/loginInfo.dto.ts
@@ -0,0 +1,21 @@
+import { ApiProperty } from "@nestjs/swagger";
+import { IsEmail, IsNotEmpty, IsString, MaxLength } from "class-validator";
+
+export class LoginInfoDto {
+ @ApiProperty({
+ example: "user@example.com",
+ description: "The email of the user",
+ })
+ @IsEmail()
+ @IsNotEmpty()
+ @MaxLength(50)
+ email: string;
+
+ @ApiProperty({
+ description: "The password of the user",
+ })
+ @IsString()
+ @IsNotEmpty()
+ @MaxLength(50)
+ password: string;
+}
diff --git a/be/src/auth/entity/auth.refreshtoken.entity.ts b/be/src/auth/entity/auth.refreshtoken.entity.ts
new file mode 100644
index 00000000..f026e5fc
--- /dev/null
+++ b/be/src/auth/entity/auth.refreshtoken.entity.ts
@@ -0,0 +1,13 @@
+import { Entity, Column, PrimaryColumn } from 'typeorm';
+
+@Entity("auth_token")
+export class AuthRefreshTokenEntity {
+ @PrimaryColumn()
+ id: number;
+
+ @Column({ type: 'varchar', length: 300 })
+ accessToken: string
+
+ @Column({ type: 'varchar', length: 300 })
+ refreshToken: string
+}
diff --git a/be/src/aws/aws.service.ts b/be/src/aws/aws.service.ts
index a4682836..d2745dc8 100644
--- a/be/src/aws/aws.service.ts
+++ b/be/src/aws/aws.service.ts
@@ -1,7 +1,7 @@
import { Injectable } from "@nestjs/common";
import * as AWS from "aws-sdk";
-import { awsConfig } from "objectStorage.config";
-import { v4 } from "uuid";
+import { awsConfig } from "../../objectStorage.config";
+import * as sharp from "sharp";
@Injectable()
export class AwsService {
@@ -18,12 +18,21 @@ export class AwsService {
});
}
- async uploadToS3(path: string, data: Buffer){
- await this.s3.putObject({
- Bucket: awsConfig.bucket,
- Key: path,
- Body: data,
- }).promise();
+ async uploadToS3(path: string, data: Buffer) {
+
+ try {
+ const resizedBuffer = await sharp(data)
+ .resize(256, 256)
+ .toBuffer();
+
+ await this.s3.putObject({
+ Bucket: awsConfig.bucket,
+ Key: path,
+ Body: resizedBuffer,
+ }).promise();
+ } catch (error) {
+ throw error;
+ }
}
getImageURL(path: string) {
diff --git a/be/src/main.ts b/be/src/main.ts
index 9cb01427..f042556e 100644
--- a/be/src/main.ts
+++ b/be/src/main.ts
@@ -3,9 +3,12 @@ import { SwaggerModule, DocumentBuilder } from "@nestjs/swagger";
import { AppModule } from "./app.module";
import { TransformInterceptor } from "./response.interceptor";
import { HttpExceptionFilter } from "./error.filter";
+import * as fs from 'fs';
async function bootstrap() {
+
const app = await NestFactory.create(AppModule);
+
app.setGlobalPrefix("api");
app.useGlobalFilters(new HttpExceptionFilter());
app.useGlobalInterceptors(new TransformInterceptor());
@@ -20,4 +23,4 @@ async function bootstrap() {
SwaggerModule.setup("api", app, document);
await app.listen(8000);
}
-bootstrap();
+bootstrap();
\ No newline at end of file
diff --git a/be/src/restaurant/entities/restaurant.entity.ts b/be/src/restaurant/entities/restaurant.entity.ts
index 9bc236d1..94f58ca6 100644
--- a/be/src/restaurant/entities/restaurant.entity.ts
+++ b/be/src/restaurant/entities/restaurant.entity.ts
@@ -8,9 +8,8 @@ import {
OneToMany,
} from "typeorm";
import { Point } from "geojson";
-import { ReviewInfoEntity } from "src/review/entities/review.entity";
-import { User } from "src/user/entities/user.entity";
-import { UserRestaurantListEntity } from "src/user/entities/user.restaurantlist.entity";
+import { ReviewInfoEntity } from "../../review/entities/review.entity";
+import { UserRestaurantListEntity } from "../../user/entities/user.restaurantlist.entity";
@Unique("unique_name_location", ["name", "location"])
@Entity("restaurant")
diff --git a/be/src/restaurant/restaurant.controller.ts b/be/src/restaurant/restaurant.controller.ts
index c3a918ea..21d74e7e 100644
--- a/be/src/restaurant/restaurant.controller.ts
+++ b/be/src/restaurant/restaurant.controller.ts
@@ -19,13 +19,13 @@ import {
import { RestaurantService } from "./restaurant.service";
import { SearchInfoDto } from "./dto/seachInfo.dto";
import { FilterInfoDto } from "./dto/filterInfo.dto";
-import { GetUser, TokenInfo } from "src/user/user.decorator";
+import { GetUser, TokenInfo } from "../user/user.decorator";
import { LocationDto } from "./dto/location.dto";
@ApiTags("Home")
@Controller("restaurant")
export class RestaurantController {
- constructor(private restaurantService: RestaurantService) {}
+ constructor(private restaurantService: RestaurantService) { }
@Get("autocomplete/:partialRestaurantName")
@UseGuards(AuthGuard("jwt"))
@ApiBearerAuth()
@@ -162,6 +162,12 @@ export class RestaurantController {
type: String,
description: "검색 반경",
})
+ @ApiQuery({
+ name: "limit",
+ required: false,
+ type: String,
+ description: "응답 개수",
+ })
@ApiResponse({
status: 200,
description: "전체 음식점 리스트 요청 성공",
@@ -174,8 +180,9 @@ export class RestaurantController {
@UsePipes(new ValidationPipe())
entireRestaurantList(
@GetUser() tokenInfo: TokenInfo,
- @Query() locationDto: LocationDto
+ @Query() locationDto: LocationDto,
+ @Query("limit") limit: string
) {
- return this.restaurantService.entireRestaurantList(locationDto, tokenInfo);
+ return this.restaurantService.entireRestaurantList(locationDto, tokenInfo, limit);
}
}
diff --git a/be/src/restaurant/restaurant.module.ts b/be/src/restaurant/restaurant.module.ts
index 27c2f1af..a1adc5a5 100644
--- a/be/src/restaurant/restaurant.module.ts
+++ b/be/src/restaurant/restaurant.module.ts
@@ -1,14 +1,15 @@
import { Module } from "@nestjs/common";
import { RestaurantController } from "./restaurant.controller";
-import { AuthModule } from "src/auth/auth.module";
+import { AuthModule } from "../auth/auth.module";
import { RestaurantService } from "./restaurant.service";
import { RestaurantRepository } from "./restaurant.repository";
-import { UserModule } from "src/user/user.module";
-import { ReviewModule } from "src/review/review.module";
+import { UserModule } from "../user/user.module";
+import { ReviewModule } from "../review/review.module";
+import { ScheduleModule } from '@nestjs/schedule';
@Module({
- imports: [AuthModule, UserModule, ReviewModule],
+ imports: [AuthModule, UserModule, ReviewModule, ScheduleModule.forRoot(),],
controllers: [RestaurantController],
providers: [RestaurantService, RestaurantRepository],
})
-export class RestaurantModule {}
+export class RestaurantModule { }
diff --git a/be/src/restaurant/restaurant.repository.ts b/be/src/restaurant/restaurant.repository.ts
index 370a144f..44ef917f 100644
--- a/be/src/restaurant/restaurant.repository.ts
+++ b/be/src/restaurant/restaurant.repository.ts
@@ -2,12 +2,12 @@ import { DataSource, Repository, Like } from "typeorm";
import { Injectable } from "@nestjs/common";
import { RestaurantInfoEntity } from "./entities/restaurant.entity";
import { SearchInfoDto } from "./dto/seachInfo.dto";
-import { TokenInfo } from "src/user/user.decorator";
-import { UserRestaurantListEntity } from "src/user/entities/user.restaurantlist.entity";
+import { TokenInfo } from "../user/user.decorator";
+import { UserRestaurantListEntity } from "../user/entities/user.restaurantlist.entity";
import { FilterInfoDto } from "./dto/filterInfo.dto";
-import { User } from "src/user/entities/user.entity";
+import { User } from "../user/entities/user.entity";
import { LocationDto } from "./dto/location.dto";
-import { UserWishRestaurantListEntity } from "src/user/entities/user.wishrestaurantlist.entity";
+import { UserWishRestaurantListEntity } from "../user/entities/user.wishrestaurantlist.entity";
@Injectable()
export class RestaurantRepository extends Repository {
@@ -178,7 +178,9 @@ export class RestaurantRepository extends Repository {
}
}
- async entireRestaurantList(locationDto: LocationDto, tokenInfo: TokenInfo) {
+ async entireRestaurantList(locationDto: LocationDto, tokenInfo: TokenInfo, limit: string = "40") {
+ const limitNum = parseInt(limit);
+
return this.createQueryBuilder("restaurant")
.leftJoin(
UserRestaurantListEntity,
@@ -208,6 +210,7 @@ export class RestaurantRepository extends Repository {
location,
ST_GeomFromText('POINT(${locationDto.longitude} ${locationDto.latitude})', 4326)) < ${locationDto.radius}`
)
+ .limit(limitNum)
.getRawMany();
}
diff --git a/be/src/restaurant/restaurant.service.ts b/be/src/restaurant/restaurant.service.ts
index cda63ac7..1026ea13 100644
--- a/be/src/restaurant/restaurant.service.ts
+++ b/be/src/restaurant/restaurant.service.ts
@@ -4,29 +4,28 @@ import { SearchInfoDto } from "./dto/seachInfo.dto";
import * as proj4 from "proj4";
import axios from "axios";
import { FilterInfoDto } from "./dto/filterInfo.dto";
-import { TokenInfo } from "src/user/user.decorator";
-import { UserRepository } from "src/user/user.repository";
-import { ReviewRepository } from "src/review/review.repository";
+import { TokenInfo } from "../user/user.decorator";
+import { UserRepository } from "../user/user.repository";
+import { ReviewRepository } from "../review/review.repository";
import { LocationDto } from "./dto/location.dto";
+import { AwsService } from "../aws/aws.service";
+import { Cron } from "@nestjs/schedule";
-const key = "api키 입력하세요";
+const key = process.env.API_KEY;
@Injectable()
-export class RestaurantService implements OnModuleInit {
- onModuleInit() {
- //this.updateRestaurantsFromSeoulData();
- setInterval(
- () => {
- this.updateRestaurantsFromSeoulData();
- },
- 1000 * 60 * 60 * 24 * 3
- );
+export class RestaurantService {
+
+ @Cron('0 0 2 * * *')
+ handleCron() {
+ this.updateRestaurantsFromSeoulData();
}
constructor(
private restaurantRepository: RestaurantRepository,
private userRepository: UserRepository,
- private reviewRepository: ReviewRepository
+ private reviewRepository: ReviewRepository,
+ private awsService: AwsService
) { }
async searchRestaurant(searchInfoDto: SearchInfoDto, tokenInfo: TokenInfo) {
@@ -43,6 +42,22 @@ export class RestaurantService implements OnModuleInit {
})
.getCount();
+ const reviewInfo = await this.reviewRepository
+ .createQueryBuilder("review")
+ .leftJoin("review.reviewLikes", "reviewLike")
+ .select(["review.id", "review.reviewImage"],)
+ .groupBy("review.id")
+ .where("review.restaurant_id = :restaurantId and review.reviewImage is NOT NULL", { restaurantId: restaurant.restaurant_id })
+ .orderBy("COUNT(CASE WHEN reviewLike.isLike = true THEN 1 ELSE NULL END)", "DESC")
+ .getRawOne();
+ if (reviewInfo) {
+ restaurant.restaurant_reviewImage = this.awsService.getImageURL(reviewInfo.review_reviewImage);
+ }
+ else {
+ restaurant.restaurant_reviewImage = this.awsService.getImageURL("review/images/defaultImage.png");
+ }
+
+
restaurant.restaurant_reviewCnt = reviewCount;
}
@@ -58,6 +73,7 @@ export class RestaurantService implements OnModuleInit {
const reviews = await this.reviewRepository
.createQueryBuilder("review")
.leftJoinAndSelect("review.user", "user")
+ .leftJoin("review.reviewLikes", "reviewLike", "reviewLike.userId = :userId", { userId: tokenInfo.id })
.select([
"review.id",
"review.isCarVisit",
@@ -68,17 +84,30 @@ export class RestaurantService implements OnModuleInit {
"review.restroomCleanliness",
"review.overallExperience",
"user.nickName as reviewer",
- "review.createdAt"
+ "user.profileImage",
+ "review.createdAt",
+ "review.reviewImage",
+ "reviewLike.isLike as isLike"
])
+ .addSelect("COUNT(CASE WHEN reviewLike.isLike = true THEN 1 ELSE NULL END)", "likeCount")
+ .addSelect("COUNT(CASE WHEN reviewLike.isLike = false THEN 1 ELSE NULL END)", "dislikeCount")
+ .groupBy("review.id, user.nickName, user.profileImage, review.isCarVisit, review.transportationAccessibility, review.parkingArea, review.taste, review.service, review.restroomCleanliness, review.overallExperience, review.createdAt, review.reviewImage, reviewLike.isLike")
.where("review.restaurant_id = :restaurantId", {
restaurantId: restaurant.restaurant_id,
})
+ .orderBy("COUNT(CASE WHEN reviewLike.isLike = true THEN 1 ELSE NULL END)", "DESC")
.getRawMany();
- restaurant.restaurant_reviewCnt = reviews.length;
- restaurant.reviews = reviews.slice(0, 3);
-
+ restaurant.restaurant_reviewCnt = reviews.length;
+ const reviewList = reviews.slice(0, 3);
+ reviewList.forEach((element) => {
+ if (element.review_reviewImage && element.review_reviewImage != "review/images/defaultImage.png") element.review_reviewImage = this.awsService.getImageURL(element.review_reviewImage);
+ else { element.review_reviewImage = "" }
+ if (element.user_profileImage) element.user_profileImage = this.awsService.getImageURL(element.user_profileImage);
+
+ })
+ restaurant.reviews = reviewList;
return restaurant;
}
@@ -105,16 +134,33 @@ export class RestaurantService implements OnModuleInit {
})
.getCount();
+ const reviewInfo = await this.reviewRepository
+ .createQueryBuilder("review")
+ .leftJoin("review.reviewLikes", "reviewLike")
+ .select(["review.id", "review.reviewImage"],)
+ .groupBy("review.id")
+ .where("review.restaurant_id = :restaurantId and review.reviewImage is NOT NULL", { restaurantId: restaurant.restaurant_id })
+ .orderBy("COUNT(CASE WHEN reviewLike.isLike = true THEN 1 ELSE NULL END)", "DESC")
+ .getRawOne();
+ if (reviewInfo) {
+ restaurant.restaurant_reviewImage = this.awsService.getImageURL(reviewInfo.review_reviewImage);
+ }
+ else {
+ restaurant.restaurant_reviewImage = this.awsService.getImageURL("review/images/defaultImage.png");
+ }
+
+
restaurant.restaurant_reviewCnt = reviewCount;
}
return restaurants;
}
- async entireRestaurantList(locationDto: LocationDto, tokenInfo: TokenInfo) {
+ async entireRestaurantList(locationDto: LocationDto, tokenInfo: TokenInfo, limit: string) {
const restaurants = await this.restaurantRepository.entireRestaurantList(
locationDto,
- tokenInfo
+ tokenInfo,
+ limit
);
for (const restaurant of restaurants) {
@@ -124,7 +170,20 @@ export class RestaurantService implements OnModuleInit {
restaurantId: restaurant.restaurant_id,
})
.getCount();
-
+ const reviewInfo = await this.reviewRepository
+ .createQueryBuilder("review")
+ .leftJoin("review.reviewLikes", "reviewLike")
+ .select(["review.id", "review.reviewImage"],)
+ .groupBy("review.id")
+ .where("review.restaurant_id = :restaurantId and review.reviewImage is NOT NULL", { restaurantId: restaurant.restaurant_id })
+ .orderBy("COUNT(CASE WHEN reviewLike.isLike = true THEN 1 ELSE NULL END)", "DESC")
+ .getRawOne();
+ if (reviewInfo) {
+ restaurant.restaurant_reviewImage = this.awsService.getImageURL(reviewInfo.review_reviewImage);
+ }
+ else {
+ restaurant.restaurant_reviewImage = this.awsService.getImageURL("review/images/defaultImage.png");
+ }
restaurant.restaurant_reviewCnt = reviewCount;
}
diff --git a/be/src/review/dto/reviewInfo.dto.ts b/be/src/review/dto/reviewInfo.dto.ts
index ee47c7bd..3af8774f 100644
--- a/be/src/review/dto/reviewInfo.dto.ts
+++ b/be/src/review/dto/reviewInfo.dto.ts
@@ -10,12 +10,14 @@ import {
Max,
Min,
} from "class-validator";
+import { Transform } from 'class-transformer';
export class ReviewInfoDto {
@ApiProperty({
example: "true",
description: "The transportation for visiting",
})
+ @Transform(({ value }) => value === 'true')
@IsBoolean()
@IsNotEmpty()
isCarVisit: boolean;
@@ -24,8 +26,11 @@ export class ReviewInfoDto {
example: "0",
description: "transportation Accessibility for visiting",
})
- @IsInt()
@IsOptional()
+ @Transform(({ value }) => {
+ return !value ? null : parseInt(value);
+ })
+ @IsInt()
@Min(0)
@Max(4)
transportationAccessibility: number | null;
@@ -34,13 +39,17 @@ export class ReviewInfoDto {
example: "0",
description: "condition of the restaurant's parking area",
})
- @IsInt()
@IsOptional()
+ @Transform(({ value }) => {
+ return !value ? null : parseInt(value);
+ })
+ @IsInt()
@Min(0)
@Max(4)
parkingArea: number | null;
@ApiProperty({ example: "0", description: "The taste of the food" })
+ @Transform(({ value }) => parseInt(value))
@IsInt()
@IsNotEmpty()
@Min(0)
@@ -48,6 +57,7 @@ export class ReviewInfoDto {
taste: number;
@ApiProperty({ example: "0", description: "The service of the restaurant" })
+ @Transform(({ value }) => parseInt(value))
@IsInt()
@IsNotEmpty()
@Min(0)
@@ -58,6 +68,7 @@ export class ReviewInfoDto {
example: "0",
description: "The condition of the restaurant's restroom",
})
+ @Transform(({ value }) => parseInt(value))
@IsInt()
@IsNotEmpty()
@Min(0)
diff --git a/be/src/review/entities/review.entity.ts b/be/src/review/entities/review.entity.ts
index c4b579bb..9b4b507b 100644
--- a/be/src/review/entities/review.entity.ts
+++ b/be/src/review/entities/review.entity.ts
@@ -6,10 +6,12 @@ import {
ManyToOne,
JoinColumn,
OneToOne,
+ OneToMany,
} from "typeorm";
-import { User } from "src/user/entities/user.entity";
-import { RestaurantInfoEntity } from "src/restaurant/entities/restaurant.entity";
-import { UserRestaurantListEntity } from "src/user/entities/user.restaurantlist.entity";
+import { User } from "../../user/entities/user.entity";
+import { RestaurantInfoEntity } from "../../restaurant/entities/restaurant.entity";
+import { UserRestaurantListEntity } from "../../user/entities/user.restaurantlist.entity";
+import { ReviewLikeEntity } from "./review.like.entity";
@Entity("review")
export class ReviewInfoEntity {
@@ -37,9 +39,15 @@ export class ReviewInfoEntity {
@Column({ type: "text" })
overallExperience: string;
+ @Column({ type: "text", nullable: true, default: null })
+ reviewImage: string;
+
@CreateDateColumn({ name: "created_at" })
createdAt: Date;
+ @OneToMany(() => ReviewLikeEntity, reviewLike => reviewLike.review)
+ reviewLikes: ReviewLikeEntity[];
+
@ManyToOne(() => User)
@JoinColumn({ name: "user_id" })
user: User;
diff --git a/be/src/review/entities/review.like.entity.ts b/be/src/review/entities/review.like.entity.ts
new file mode 100644
index 00000000..e502460d
--- /dev/null
+++ b/be/src/review/entities/review.like.entity.ts
@@ -0,0 +1,37 @@
+import {
+ Entity,
+ PrimaryColumn,
+ Column,
+ CreateDateColumn,
+ ManyToOne,
+ JoinColumn,
+ DeleteDateColumn,
+} from "typeorm";
+import { User } from "../../user/entities/user.entity";
+import { ReviewInfoEntity } from "./review.entity";
+
+@Entity("reviewLike")
+export class ReviewLikeEntity {
+ @PrimaryColumn({ name: "review_id" })
+ reviewId: number;
+
+ @PrimaryColumn({ name: "user_id" })
+ userId: number;
+
+ @Column({ name: "is_like" })
+ isLike: boolean;
+
+ @CreateDateColumn({ name: "created_at" })
+ createdAt: Date;
+
+ @DeleteDateColumn({ name: "deleted_at", nullable: true, type: "timestamp" })
+ deletedAt: Date | null;
+
+ @ManyToOne(() => ReviewInfoEntity)
+ @JoinColumn({ name: "review_id", referencedColumnName: "id" })
+ review: ReviewInfoEntity;
+
+ @ManyToOne(() => User)
+ @JoinColumn({ name: "user_id", referencedColumnName: "id" })
+ user: User;
+}
diff --git a/be/src/review/review.controller.spec.ts b/be/src/review/review.controller.spec.ts
new file mode 100644
index 00000000..b1b04586
--- /dev/null
+++ b/be/src/review/review.controller.spec.ts
@@ -0,0 +1,18 @@
+import { Test, TestingModule } from '@nestjs/testing';
+import { ReviewController } from './review.controller';
+
+describe('ReviewController', () => {
+ let controller: ReviewController;
+
+ beforeEach(async () => {
+ const module: TestingModule = await Test.createTestingModule({
+ controllers: [ReviewController],
+ }).compile();
+
+ controller = module.get(ReviewController);
+ });
+
+ it('should be defined', () => {
+ expect(controller).toBeDefined();
+ });
+});
diff --git a/be/src/review/review.controller.ts b/be/src/review/review.controller.ts
new file mode 100644
index 00000000..2e79c27a
--- /dev/null
+++ b/be/src/review/review.controller.ts
@@ -0,0 +1,67 @@
+import { Controller, Get, Param, Post, Query, UseGuards, UsePipes, ValidationPipe } from '@nestjs/common';
+import { ReviewService } from './review.service';
+import { ApiBearerAuth, ApiOperation, ApiParam, ApiQuery, ApiResponse, ApiTags } from '@nestjs/swagger';
+import { AuthGuard } from '@nestjs/passport';
+import { GetUser, TokenInfo } from '../user/user.decorator';
+import { SortInfoDto } from '../utils/sortInfo.dto';
+
+@ApiTags("Review")
+@Controller('review')
+export class ReviewController {
+ constructor(private reviewService: ReviewService) { }
+
+ @Get("/:restaurantId")
+ @UseGuards(AuthGuard("jwt"))
+ @ApiBearerAuth()
+ @ApiQuery({ name: 'sort', required: false, description: '정렬 기준' })
+ @ApiQuery({ name: 'page', required: false, description: '페이지 번호' })
+ @ApiQuery({ name: 'limit', required: false, description: '페이지 당 항목 수' })
+
+ @ApiOperation({ summary: "리뷰 정렬 요청" })
+ @ApiResponse({ status: 200, description: "리뷰 정렬 요청 성공" })
+ @ApiResponse({ status: 401, description: "인증 실패" })
+ @ApiResponse({ status: 400, description: "부적절한 요청" })
+ @UsePipes(new ValidationPipe())
+ async getSortedReviews(
+ @GetUser() tokenInfo: TokenInfo,
+ @Param('restaurantId') restaurantId: string,
+ @Query() getSortedReviewsDto: SortInfoDto
+ ) {
+ const restaurantNumber = parseInt(restaurantId, 10);
+ return await this.reviewService.getSortedReviews(tokenInfo, restaurantNumber, getSortedReviewsDto);
+ }
+
+ @Post("/:reviewId/like")
+ @UseGuards(AuthGuard("jwt"))
+ @ApiBearerAuth()
+ @ApiOperation({ summary: "리뷰 좋아요 요청" })
+ @ApiResponse({ status: 200, description: "리뷰 좋아요 요청 성공" })
+ @ApiResponse({ status: 401, description: "인증 실패" })
+ @ApiResponse({ status: 400, description: "부적절한 요청" })
+ async reviewLike(
+ @GetUser() tokenInfo: TokenInfo,
+ @Param("reviewId") reviewid: number
+ ) {
+ return await this.reviewService.reviewLike(
+ tokenInfo,
+ reviewid
+ );
+ }
+
+ @Post("/:reviewId/unlike")
+ @UseGuards(AuthGuard("jwt"))
+ @ApiBearerAuth()
+ @ApiOperation({ summary: "리뷰 싫어요 요청" })
+ @ApiResponse({ status: 200, description: "리뷰 싫어요 요청 성공" })
+ @ApiResponse({ status: 401, description: "인증 실패" })
+ @ApiResponse({ status: 400, description: "부적절한 요청" })
+ async reviewUnLike(
+ @GetUser() tokenInfo: TokenInfo,
+ @Param("reviewId") reviewid: number
+ ) {
+ return await this.reviewService.reviewUnLike(
+ tokenInfo,
+ reviewid
+ );
+ }
+}
diff --git a/be/src/review/review.like.repository.ts b/be/src/review/review.like.repository.ts
new file mode 100644
index 00000000..d49ad054
--- /dev/null
+++ b/be/src/review/review.like.repository.ts
@@ -0,0 +1,10 @@
+import { DataSource, IsNull, Repository, Not } from "typeorm";
+import { ConflictException, Injectable } from "@nestjs/common";
+import { ReviewLikeEntity } from "./entities/review.like.entity";
+
+@Injectable()
+export class ReviewLikeRepository extends Repository {
+ constructor(private dataSource: DataSource) {
+ super(ReviewLikeEntity, dataSource.createEntityManager());
+ }
+}
diff --git a/be/src/review/review.module.ts b/be/src/review/review.module.ts
index dc114741..d1b2a33a 100644
--- a/be/src/review/review.module.ts
+++ b/be/src/review/review.module.ts
@@ -1,8 +1,12 @@
import { Module } from "@nestjs/common";
import { ReviewRepository } from "./review.repository";
+import { ReviewController } from "./review.controller";
+import { ReviewService } from "./review.service";
+import { ReviewLikeRepository } from "./review.like.repository";
@Module({
- providers: [ReviewRepository],
+ controllers: [ReviewController],
+ providers: [ReviewRepository, ReviewLikeRepository, ReviewService],
exports: [ReviewRepository],
})
-export class ReviewModule {}
+export class ReviewModule { }
diff --git a/be/src/review/review.repository.ts b/be/src/review/review.repository.ts
index f8b6464d..f5fd07dd 100644
--- a/be/src/review/review.repository.ts
+++ b/be/src/review/review.repository.ts
@@ -1,10 +1,139 @@
import { DataSource, IsNull, Repository, Not } from "typeorm";
import { ConflictException, Injectable } from "@nestjs/common";
import { ReviewInfoEntity } from "./entities/review.entity";
+import { TokenInfo } from "../user/user.decorator";
+import { SortInfoDto } from "../utils/sortInfo.dto";
@Injectable()
export class ReviewRepository extends Repository {
constructor(private dataSource: DataSource) {
super(ReviewInfoEntity, dataSource.createEntityManager());
}
+
+ async getReviewIdsWithLikes(sort: string) {
+ if (sort === "ASC") {
+ return await this
+ .createQueryBuilder("review")
+ .leftJoin("review.reviewLikes", "reviewLike")
+ .select("review.id", "reviewId")
+ .addSelect("COUNT(reviewLike.isLike)", "likeCount")
+ .groupBy("review.id")
+ .orderBy("COUNT(CASE WHEN reviewLike.isLike = false THEN 1 ELSE NULL END)", "DESC")
+ .getRawMany();
+ }
+ else {
+ return await this
+ .createQueryBuilder("review")
+ .leftJoin("review.reviewLikes", "reviewLike")
+ .select("review.id", "reviewId")
+ .addSelect("COUNT(reviewLike.isLike)", "likeCount")
+ .groupBy("review.id")
+ .orderBy("COUNT(CASE WHEN reviewLike.isLike = true THEN 1 ELSE NULL END)", "DESC")
+ .getRawMany();
+ }
+ }
+ async getSortedReviews(getSortedReviewsDto: SortInfoDto, restaurantId: number, id: TokenInfo["id"], sortedReviewIds: number[]) {
+ const pageNumber = parseInt(getSortedReviewsDto.page as unknown as string) || 1;
+ const limitNumber = parseInt(getSortedReviewsDto.limit as unknown as string) || 10;
+ const skipNumber = (pageNumber - 1) * limitNumber;
+ if (getSortedReviewsDto && getSortedReviewsDto.sort === "TIME_DESC") {
+ const items = await this.createQueryBuilder("review")
+ .leftJoinAndSelect("review.user", "user")
+ .leftJoin("review.reviewLikes", "reviewLike", "reviewLike.userId = :userId", { userId: id })
+ .select([
+ "review.id",
+ "review.isCarVisit",
+ "review.transportationAccessibility",
+ "review.parkingArea",
+ "review.taste",
+ "review.service",
+ "review.restroomCleanliness",
+ "review.overallExperience",
+ "user.nickName as reviewer",
+ "user.profileImage",
+ "review.createdAt",
+ "review.reviewImage",
+ "reviewLike.isLike as isLike",
+ ])
+ .where("review.restaurant_id = :restaurantId", {
+ restaurantId: restaurantId,
+ })
+ .orderBy("review.createdAt", "DESC")
+ .offset(skipNumber)
+ .limit(limitNumber + 1)
+ .getRawMany();
+
+ const hasNext = items.length > limitNumber;
+ const resultItems = hasNext ? items.slice(0, -1) : items;
+
+ return { hasNext, items: resultItems };
+ }
+ else if (getSortedReviewsDto && getSortedReviewsDto.sort === "TIME_ASC") {
+ const items = await this.createQueryBuilder("review")
+ .leftJoinAndSelect("review.user", "user")
+ .leftJoin("review.reviewLikes", "reviewLike", "reviewLike.userId = :userId", { userId: id })
+ .select([
+ "review.id",
+ "review.isCarVisit",
+ "review.transportationAccessibility",
+ "review.parkingArea",
+ "review.taste",
+ "review.service",
+ "review.restroomCleanliness",
+ "review.overallExperience",
+ "user.nickName as reviewer",
+ "user.profileImage",
+ "review.createdAt",
+ "review.reviewImage",
+ "reviewLike.isLike as isLike",
+ ])
+ .where("review.restaurant_id = :restaurantId", {
+ restaurantId: restaurantId,
+ })
+ .orderBy("review.createdAt", "ASC")
+ .offset(skipNumber)
+ .limit(limitNumber + 1)
+ .getRawMany();
+
+ const hasNext = items.length > limitNumber;
+ const resultItems = hasNext ? items.slice(0, -1) : items;
+
+ return { hasNext, items: resultItems };
+ }
+ else {
+ if (sortedReviewIds.length) {
+ const items = await this.createQueryBuilder("review")
+ .leftJoinAndSelect("review.user", "user")
+ .leftJoin("review.reviewLikes", "reviewLike", "reviewLike.userId = :userId", { userId: id })
+ .select([
+ "review.id",
+ "review.isCarVisit",
+ "review.transportationAccessibility",
+ "review.parkingArea",
+ "review.taste",
+ "review.service",
+ "review.restroomCleanliness",
+ "review.overallExperience",
+ "user.nickName as reviewer",
+ "user.profileImage",
+ "review.createdAt",
+ "review.reviewImage",
+ "reviewLike.isLike as isLike",
+ ])
+ .where("review.id IN (:...sortedReviewIds)", { sortedReviewIds })
+ .andWhere("review.restaurant_id = :restaurantId", { restaurantId: restaurantId })
+ .offset(skipNumber)
+ .limit(limitNumber + 1)
+ .getRawMany();
+ const sortedItems = sortedReviewIds
+ .map(id => items.find(item => item.review_id === id))
+ .filter(item => item !== undefined);
+
+ const hasNext = sortedItems.length > limitNumber;
+ const resultItems = hasNext ? sortedItems.slice(0, -1) : sortedItems;
+
+ return { hasNext, items: resultItems };
+ }
+ }
+ }
}
diff --git a/be/src/review/review.service.spec.ts b/be/src/review/review.service.spec.ts
new file mode 100644
index 00000000..b0fc157f
--- /dev/null
+++ b/be/src/review/review.service.spec.ts
@@ -0,0 +1,18 @@
+import { Test, TestingModule } from '@nestjs/testing';
+import { ReviewService } from './review.service';
+
+describe('ReviewService', () => {
+ let service: ReviewService;
+
+ beforeEach(async () => {
+ const module: TestingModule = await Test.createTestingModule({
+ providers: [ReviewService],
+ }).compile();
+
+ service = module.get(ReviewService);
+ });
+
+ it('should be defined', () => {
+ expect(service).toBeDefined();
+ });
+});
diff --git a/be/src/review/review.service.ts b/be/src/review/review.service.ts
new file mode 100644
index 00000000..38682911
--- /dev/null
+++ b/be/src/review/review.service.ts
@@ -0,0 +1,87 @@
+import { BadRequestException, Injectable } from '@nestjs/common';
+import { ReviewRepository } from './review.repository';
+import { TokenInfo } from '../user/user.decorator';
+import { ReviewLikeRepository } from './review.like.repository';
+import { SortInfoDto } from '../utils/sortInfo.dto';
+import { AwsService } from '../aws/aws.service';
+
+@Injectable()
+export class ReviewService {
+ constructor(
+ private reviewRepository: ReviewRepository,
+ private reviewLikeRepository: ReviewLikeRepository,
+ private awsService: AwsService
+ ) { }
+
+ async getSortedReviews(tokenInfo: TokenInfo, restaurantId: number, getSortedReviewsDto: SortInfoDto) {
+ let sortedReviewIds;
+ if (getSortedReviewsDto.sort === "REVIEW_ASC") {
+ const getReviewIdsWithLikes = await this.reviewRepository.getReviewIdsWithLikes("ASC");
+ sortedReviewIds = getReviewIdsWithLikes.map(rl => rl.reviewId);
+ }
+ else {
+ const getReviewIdsWithLikes = await this.reviewRepository.getReviewIdsWithLikes("DESC");
+ sortedReviewIds = getReviewIdsWithLikes.map(rl => rl.reviewId);
+ }
+
+ const reviews = await this.reviewRepository.getSortedReviews(getSortedReviewsDto, restaurantId, tokenInfo.id, sortedReviewIds);
+ for (const review of reviews.items) {
+ const likeCounts = await this.reviewLikeRepository.createQueryBuilder("reviewLike")
+ .select("reviewLike.isLike", "status")
+ .addSelect("COUNT(*)", "count")
+ .where("reviewLike.reviewId = :reviewId", { reviewId: review.review_id })
+ .groupBy("reviewLike.isLike")
+ .getRawMany();
+ if (review.user_profileImage) review.user_profileImage = this.awsService.getImageURL(review.user_profileImage);
+ if (review.review_reviewImage && review.review_reviewImage != "review/images/defaultImage.png") review.review_reviewImage = this.awsService.getImageURL(review.review_reviewImage);
+ else { review.review_reviewImage = "" }
+ review.likeCount = Number(likeCounts.find(lc => lc.status === true)?.count) || 0;
+ review.dislikeCount = Number(likeCounts.find(lc => lc.status === false)?.count) || 0;
+ }
+
+ return reviews;
+ }
+
+ async reviewLike(tokenInfo: TokenInfo, reviewId: number) {
+ const existingLike = await this.reviewLikeRepository.findOne({
+ where: { userId: tokenInfo.id, reviewId: reviewId }
+ });
+
+ if (existingLike && existingLike.isLike) {
+ await this.reviewLikeRepository.remove(existingLike);
+ } else {
+ const entity = this.reviewLikeRepository.create({
+ userId: tokenInfo.id,
+ reviewId: reviewId,
+ isLike: true,
+ });
+ try {
+ await this.reviewLikeRepository.upsert(entity, ['userId', 'reviewId']);
+ } catch (err) {
+ throw new BadRequestException();
+ }
+ }
+ }
+
+ async reviewUnLike(tokenInfo: TokenInfo, reviewId: number) {
+ const existingLike = await this.reviewLikeRepository.findOne({
+ where: { userId: tokenInfo.id, reviewId: reviewId }
+ });
+
+ if (existingLike && !existingLike.isLike) {
+ await this.reviewLikeRepository.remove(existingLike);
+ } else {
+ const entity = this.reviewLikeRepository.create({
+ userId: tokenInfo.id,
+ reviewId: reviewId,
+ isLike: false,
+ });
+ try {
+ await this.reviewLikeRepository.upsert(entity, ['userId', 'reviewId']);
+ } catch (err) {
+ throw new BadRequestException();
+ }
+ }
+ }
+
+}
diff --git a/be/src/user/dto/userInfo.dto.ts b/be/src/user/dto/userInfo.dto.ts
index f76dff31..2b63c5de 100644
--- a/be/src/user/dto/userInfo.dto.ts
+++ b/be/src/user/dto/userInfo.dto.ts
@@ -9,7 +9,7 @@ import {
IsOptional,
IsInstance,
} from "class-validator";
-import { isArrayBuffer } from "util/types";
+import { Transform } from 'class-transformer';
export class UserInfoDto {
@ApiProperty({
@@ -21,7 +21,7 @@ export class UserInfoDto {
@MaxLength(50)
email: string;
- @ApiProperty({ example: "1234", description: "The password of the user" })
+ @ApiProperty({ example: "1234", description: "The password of the user", required: false })
@IsString()
@IsOptional()
@MaxLength(50)
@@ -55,16 +55,8 @@ export class UserInfoDto {
example: true,
description: "The gender of the user. true is male, false is female",
})
+ @Transform(({ value }) => value === 'true')
@IsBoolean()
@IsNotEmpty()
isMale: boolean;
-
- @ApiProperty({
- example: "",
- description: "The profile image of the user",
- type: 'string',
- format: 'binary'
- })
- @IsOptional()
- profileImage?: Buffer;
}
diff --git a/be/src/user/entities/user.entity.ts b/be/src/user/entities/user.entity.ts
index 90c4b276..d7145d7c 100644
--- a/be/src/user/entities/user.entity.ts
+++ b/be/src/user/entities/user.entity.ts
@@ -9,7 +9,7 @@ import {
} from "typeorm";
import { FollowEntity } from "./user.followList.entity";
import { UserRestaurantListEntity } from "./user.restaurantlist.entity";
-import { ReviewInfoEntity } from "src/review/entities/review.entity";
+import { ReviewInfoEntity } from "../../review/entities/review.entity";
@Entity()
export class User {
@@ -37,7 +37,7 @@ export class User {
@Column({ type: "varchar", length: 20, nullable: true })
provider: string | null;
- @Column({ type: "text", default : "profile/images/defaultprofile.png"})
+ @Column({ type: "text", default: "profile/images/defaultprofile.png" })
profileImage: string;
@CreateDateColumn({ type: "timestamp" })
@@ -55,7 +55,7 @@ export class User {
@OneToMany(() => FollowEntity, (follow) => follow.followedUserId)
follower: FollowEntity[];
- @OneToMany(() => UserRestaurantListEntity, (list) => list.userId)
+ @OneToMany(() => UserRestaurantListEntity, (list) => list.user)
restaurant: UserRestaurantListEntity[];
@OneToMany(() => ReviewInfoEntity, (review) => review.user)
diff --git a/be/src/user/user.controller.ts b/be/src/user/user.controller.ts
index f7a37d44..56316010 100644
--- a/be/src/user/user.controller.ts
+++ b/be/src/user/user.controller.ts
@@ -10,9 +10,14 @@ import {
ValidationPipe,
UseGuards,
Query,
+ UseInterceptors,
+ UploadedFile,
+ BadRequestException,
} from "@nestjs/common";
import {
ApiBearerAuth,
+ ApiBody,
+ ApiConsumes,
ApiOperation,
ApiParam,
ApiQuery,
@@ -24,9 +29,19 @@ import { UserService } from "./user.service";
import { GetUser, TokenInfo } from "./user.decorator";
import { AuthGuard } from "@nestjs/passport";
import { SearchInfoDto } from "../restaurant/dto/seachInfo.dto";
-import { LocationDto } from "src/restaurant/dto/location.dto";
-import { ReviewInfoDto } from "src/review/dto/reviewInfo.dto";
+import { LocationDto } from "../restaurant/dto/location.dto";
+import { ReviewInfoDto } from "../review/dto/reviewInfo.dto";
import { ParseArrayPipe } from "../utils/parsearraypipe";
+import { FileInterceptor } from "@nestjs/platform-express";
+import { memoryStorage } from 'multer';
+import { plainToClass } from "class-transformer";
+import { validate } from "class-validator";
+import { SortInfoDto } from "../utils/sortInfo.dto";
+
+const multerOptions = {
+ storage: memoryStorage(),
+};
+
@Controller("user")
export class UserController {
@@ -155,16 +170,27 @@ export class UserController {
type: String,
description: "검색 반경",
})
+ @ApiQuery({
+ name: "sort",
+ required: false,
+ type: String,
+ description: "선택된 필터",
+ })
+ @ApiQuery({ name: 'page', required: false, description: '페이지 번호' })
+ @ApiQuery({ name: 'limit', required: false, description: '페이지 당 항목 수' })
@ApiResponse({ status: 200, description: "내 맛집 리스트 정보 요청 성공" })
@ApiResponse({ status: 401, description: "인증 실패" })
@ApiResponse({ status: 400, description: "부적절한 요청" })
+ @UsePipes(new ValidationPipe())
async getMyRestaurantListInfo(
@Query() locationDto: LocationDto,
+ @Query() sortInfoDto: SortInfoDto,
@GetUser() tokenInfo: TokenInfo
) {
const searchInfoDto = new SearchInfoDto("", locationDto);
return await this.userService.getMyRestaurantListInfo(
searchInfoDto,
+ sortInfoDto,
tokenInfo
);
}
@@ -174,10 +200,40 @@ export class UserController {
@UseGuards(AuthGuard("jwt"))
@ApiBearerAuth()
@ApiOperation({ summary: "내 위시 맛집 리스트 정보 가져오기" })
+ @ApiQuery({
+ name: "sort",
+ required: false,
+ type: String,
+ description: "선택된 필터",
+ })
+ @ApiQuery({ name: 'page', required: false, description: '페이지 번호' })
+ @ApiQuery({ name: 'limit', required: false, description: '페이지 당 항목 수' })
@ApiResponse({ status: 200, description: "내 맛집 리스트 정보 요청 성공" })
@ApiResponse({ status: 401, description: "인증 실패" })
- async getMyWishRestaurantListInfo(@GetUser() tokenInfo: TokenInfo) {
- return await this.userService.getMyWishRestaurantListInfo(tokenInfo);
+ async getMyWishRestaurantListInfo(@GetUser() tokenInfo: TokenInfo, @Query() sortInfoDto: SortInfoDto) {
+ return await this.userService.getMyWishRestaurantListInfo(tokenInfo, sortInfoDto);
+ }
+
+ @ApiTags("Home")
+ @Get("/state/wish-restaurant")
+ @UseGuards(AuthGuard("jwt"))
+ @ApiBearerAuth()
+ @ApiOperation({ summary: "위시 맛집리스트 포함 여부 정보 가져오기" })
+ @ApiResponse({ status: 200, description: "위시 맛집리스트 포함 여부 정보 요청 성공" })
+ @ApiResponse({ status: 401, description: "인증 실패" })
+ async getStateIsWish(@GetUser() tokenInfo: TokenInfo, @Query("restaurantid") restaurantid: number) {
+ return await this.userService.getStateIsWish(tokenInfo, restaurantid);
+ }
+
+ @ApiTags("Home")
+ @Get("/recommend-food")
+ @UseGuards(AuthGuard("jwt"))
+ @ApiBearerAuth()
+ @ApiOperation({ summary: "추천 음식 정보 가져오기" })
+ @ApiResponse({ status: 200, description: "추천 음식 정보 요청 성공" })
+ @ApiResponse({ status: 401, description: "인증 실패" })
+ async getRecommendFood(@GetUser() tokenInfo: TokenInfo) {
+ return await this.userService.getRecommendFood(tokenInfo);
}
@ApiTags("Follow/Following", "Home")
@@ -189,7 +245,7 @@ export class UserController {
@ApiResponse({ status: 401, description: "인증 실패" })
@ApiResponse({ status: 400, description: "부적절한 요청" })
async getMyFollowListInfo(@GetUser() tokenInfo: TokenInfo) {
- return await this.userService.getMyFollowListInfo(tokenInfo);
+ return this.userService.getMyFollowListInfo(tokenInfo);
}
@ApiTags("Follow/Following")
@@ -212,17 +268,41 @@ export class UserController {
@ApiResponse({ status: 200, description: "추천 사용자 정보 요청 성공" })
@ApiResponse({ status: 401, description: "인증 실패" })
async getRecommendUserListInfo(@GetUser() tokenInfo: TokenInfo) {
- return await this.userService.getRecommendUserListInfo(tokenInfo);
+ return this.userService.getRecommendUserListInfo(tokenInfo);
}
@ApiTags("Signup")
@Post()
+ @UseInterceptors(FileInterceptor('profileImage', multerOptions))
@ApiOperation({ summary: "유저 회원가입" })
@ApiResponse({ status: 200, description: "회원가입 성공" })
@ApiResponse({ status: 400, description: "부적절한 요청" })
- @UsePipes(new ValidationPipe())
- async singup(@Body() userInfoDto: UserInfoDto) {
- return await this.userService.signup(userInfoDto);
+ @ApiBody({
+ schema: {
+ type: 'object',
+ description: "회원가입",
+ required: ['email', 'provider', 'nickName', 'region', 'birthdate', 'isMale'],
+ properties: {
+ email: { type: 'string', example: 'user@example.com', description: 'The email of the user' },
+ password: { type: 'string', example: '1234', description: 'The password of the user' },
+ provider: { type: 'string', example: 'naver', description: 'The provider of the user' },
+ nickName: { type: 'string', example: 'test', description: 'The nickname of the user' },
+ region: { type: 'string', example: '강동구', description: 'The region of the user' },
+ birthdate: { type: 'string', example: '1234/56/78', description: 'The birth of the user' },
+ isMale: { type: 'boolean', example: true, description: 'The gender of the user. true is male, false is female' },
+ profileImage: { type: 'string', format: 'binary', description: 'The profile image of the user' },
+ },
+ },
+ })
+ @ApiConsumes('multipart/form-data')
+ async singup(@Body() body, @UploadedFile() file: Express.Multer.File) {
+ const userInfoDto = plainToClass(UserInfoDto, body);
+ const errors = await validate(userInfoDto);
+ if (errors.length > 0) {
+ console.log(errors);
+ throw new BadRequestException(errors);
+ }
+ return await this.userService.signup(file, userInfoDto);
}
@ApiTags("Follow/Following")
@@ -249,6 +329,7 @@ export class UserController {
@ApiTags("RestaurantList")
@Post("/restaurant/:restaurantid")
+ @UseInterceptors(FileInterceptor('reviewImage', multerOptions))
@UseGuards(AuthGuard("jwt"))
@ApiBearerAuth()
@ApiOperation({ summary: "내 맛집 리스트에 등록하기" })
@@ -258,19 +339,76 @@ export class UserController {
description: "음식점 id",
type: Number,
})
+ @ApiBody({
+ schema: {
+ type: 'object',
+ description: "리뷰 등록하기",
+ required: ['isCarVisit', 'taste', 'service', 'restroomCleanliness', 'overallExperience'],
+ properties: {
+ isCarVisit: { type: 'boolean', example: true, description: 'The transportation for visiting' },
+ transportationAccessibility: {
+ type: 'integer',
+ example: 0,
+ description: 'Transportation accessibility for visiting',
+ minimum: 0,
+ maximum: 4
+ },
+ parkingArea: {
+ type: 'integer',
+ example: 0,
+ description: "Condition of the restaurant's parking area",
+ minimum: 0,
+ maximum: 4
+ },
+ taste: {
+ type: 'integer',
+ example: 0,
+ description: 'The taste of the food',
+ minimum: 0,
+ maximum: 4
+ },
+ service: {
+ type: 'integer',
+ example: 0,
+ description: 'The service of the restaurant',
+ minimum: 0,
+ maximum: 4
+ },
+ restroomCleanliness: {
+ type: 'integer',
+ example: 0,
+ description: "The condition of the restaurant's restroom",
+ minimum: 0,
+ maximum: 4
+ },
+ overallExperience: {
+ type: 'string',
+ example: '20자 이상 작성하기',
+ description: 'The overall experience about the restaurant',
+ minLength: 20
+ },
+ reviewImage: { type: 'string', format: 'binary', description: 'The image of food' },
+ },
+ },
+ })
@ApiResponse({ status: 200, description: "맛집리스트 등록 성공" })
@ApiResponse({ status: 401, description: "인증 실패" })
@ApiResponse({ status: 400, description: "부적절한 요청" })
- @UsePipes(new ValidationPipe())
+ @ApiConsumes('multipart/form-data')
async addRestaurantToNebob(
- @Body() reviewInfoDto: ReviewInfoDto,
+ @Body() body,
@GetUser() tokenInfo: TokenInfo,
- @Param("restaurantid") restaurantid: number
+ @Param("restaurantid") restaurantid: number,
+ @UploadedFile() file: Express.Multer.File
) {
+ const reviewInfoDto = plainToClass(ReviewInfoDto, body);
+ const errors = await validate(reviewInfoDto);
+ if (errors.length > 0) throw new BadRequestException(errors);
return await this.userService.addRestaurantToNebob(
reviewInfoDto,
tokenInfo,
- restaurantid
+ restaurantid,
+ file
);
}
@@ -382,17 +520,49 @@ export class UserController {
@ApiTags("Mypage")
@Put()
+ @UseInterceptors(FileInterceptor('profileImage', multerOptions))
@UseGuards(AuthGuard("jwt"))
@ApiBearerAuth()
+ @ApiBody({
+ schema: {
+ type: 'object',
+ description: "회원정보 수정",
+ required: ['email', 'provider', 'nickName', 'region', 'birthdate', 'isMale'],
+ properties: {
+ email: { type: 'string', example: 'user@example.com', description: 'The email of the user' },
+ password: { type: 'string', example: '1234', description: 'The password of the user' },
+ provider: { type: 'string', example: 'naver', description: 'The provider of the user' },
+ nickName: { type: 'string', example: 'test', description: 'The nickname of the user' },
+ region: { type: 'string', example: '강동구', description: 'The region of the user' },
+ birthdate: { type: 'string', example: '1234/56/78', description: 'The birth of the user' },
+ isMale: { type: 'boolean', example: true, description: 'The gender of the user. true is male, false is female' },
+ profileImage: { type: 'string', format: 'binary', description: 'The profile image of the user' },
+ isImageChanged: { type: 'boolean', example: true, description: 'The Boolean Value of ProfileImageChanged' }
+ },
+ },
+ })
@ApiOperation({ summary: "유저 회원정보 수정" })
@ApiResponse({ status: 200, description: "회원정보 수정 성공" })
@ApiResponse({ status: 401, description: "인증 실패" })
@ApiResponse({ status: 400, description: "부적절한 요청" })
- @UsePipes(new ValidationPipe())
+ @ApiConsumes('multipart/form-data')
async updateMypageUserInfo(
+ @UploadedFile() file: Express.Multer.File,
@GetUser() tokenInfo: TokenInfo,
- @Body() userInfoDto: UserInfoDto
+ @Body() body
) {
- return await this.userService.updateMypageUserInfo(tokenInfo, userInfoDto);
+ const userInfoDto = plainToClass(UserInfoDto, body);
+ if (body.isImageChanged === "true" || body.isImageChanged === true) {
+ body.isImageChanged = true;
+ } else if (body.isImageChanged === "false" || body.isImageChanged === false) {
+ body.isImageChanged = false;
+ } else {
+ throw new BadRequestException();
+ }
+
+
+ const errors = await validate(userInfoDto);
+ if (errors.length > 0) throw new BadRequestException(errors);
+ return await this.userService.updateMypageUserInfo(file, tokenInfo, userInfoDto, body.isImageChanged);
}
}
diff --git a/be/src/user/user.repository.ts b/be/src/user/user.repository.ts
index 47138c31..d6cc13de 100644
--- a/be/src/user/user.repository.ts
+++ b/be/src/user/user.repository.ts
@@ -12,15 +12,8 @@ export class UserRepository extends Repository {
constructor(private dataSource: DataSource) {
super(User, dataSource.createEntityManager());
}
- async createUser(userentity: User): Promise {
- try {
- await this.save(userentity);
- } catch (err) {
- if (err.code === "23505") {
- throw new ConflictException("Duplicated Value");
- }
- }
- return;
+ async createUser(userentity: User) {
+ return await this.save(userentity);
}
async getNickNameAvailability(nickName: UserInfoDto["nickName"]) {
const user = await this.findOne({
@@ -52,7 +45,7 @@ export class UserRepository extends Repository {
}
async getUsersInfo(targetInfoIds: number[]) {
const userInfo = await this.find({
- select: ["nickName", "region"],
+ select: ["nickName", "region", "profileImage"],
where: { id: In(targetInfoIds) },
});
return userInfo;
@@ -72,15 +65,42 @@ export class UserRepository extends Repository {
});
return { userInfo: userInfo };
}
- async getRecommendUserListInfo(idList: number[]) {
+
+ async getRecommendUserListInfo(idList: number[], id: number) {
+ const curUser = await this.findOne({
+ select: ["id", "region"],
+ where: { id: id },
+ });
+
+ const myRestaurants = await this.createQueryBuilder("user")
+ .leftJoinAndSelect("user.restaurant", "userRestaurant")
+ .where("user.id = :id", { id })
+ .select("userRestaurant.restaurantId")
+ .getRawMany();
+
+ const myFavRestaurants = myRestaurants.map(
+ (r) => r.userRestaurant_restaurant_id
+ );
+
const userInfo = await this.createQueryBuilder("user")
- .select(["user.nickName", "user.region"])
+ .leftJoin("user.restaurant", "userRestaurant")
+ .select([
+ "user.nickName",
+ "user.region",
+ "user.profileImage",
+ 'SUM(CASE WHEN userRestaurant.restaurantId IN (:...myFavRestaurants) THEN 1 ELSE 0 END) AS "commonRestaurant"',
+ ])
+ .setParameter("myFavRestaurants", myFavRestaurants)
.where("user.id NOT IN (:...idList)", { idList })
- .orderBy("RANDOM()")
- .limit(2)
- .getMany();
+ .andWhere("user.region = :yourRegion", { yourRegion: curUser.region })
+ .groupBy("user.id")
+ .orderBy("\"commonRestaurant\"", "DESC")
+ .limit(10)
+ .getRawMany();
+
return userInfo;
}
+
async logout(id: number) {
return {};
}
@@ -115,11 +135,11 @@ export class UserRepository extends Repository {
region: userEntity["region"],
provider: userEntity["provider"],
password: userEntity["password"],
- profileImage : userEntity["profileImage"]
+ profileImage: userEntity["profileImage"],
};
if (!isEmailDuplicate) {
- updateObject["email"] =userEntity["email"];
+ updateObject["email"] = userEntity["email"];
}
if (!isNickNameDuplicate) {
updateObject["nickName"] = userEntity["nickName"];
diff --git a/be/src/user/user.restaurantList.repository.ts b/be/src/user/user.restaurantList.repository.ts
index 9d0c134c..1c5ade72 100644
--- a/be/src/user/user.restaurantList.repository.ts
+++ b/be/src/user/user.restaurantList.repository.ts
@@ -2,9 +2,10 @@ import { DataSource, IsNull, Repository, Not } from "typeorm";
import { ConflictException, Injectable } from "@nestjs/common";
import { UserRestaurantListEntity } from "./entities/user.restaurantlist.entity";
import { TokenInfo } from "./user.decorator";
-import { SearchInfoDto } from "src/restaurant/dto/seachInfo.dto";
-import { ReviewInfoEntity } from "src/review/entities/review.entity";
+import { SearchInfoDto } from "../restaurant/dto/seachInfo.dto";
+import { ReviewInfoEntity } from "../review/entities/review.entity";
import { UserWishRestaurantListEntity } from "./entities/user.wishrestaurantlist.entity";
+import { SortInfoDto } from "../utils/sortInfo.dto";
@Injectable()
export class UserRestaurantListRepository extends Repository {
@@ -74,10 +75,11 @@ export class UserRestaurantListRepository extends Repository sortInfoDto.limit;
+ const resultItems = hasNext ? items.slice(0, -1) : items;
+
+ return {
+ hasNext,
+ items: resultItems,
+ }
+
+ }
+ }
+ async getMyFavoriteFoodCategory(id: TokenInfo['id'], region) {
+ const categoryCounts = await this.createQueryBuilder("userRestaurantList")
+ .select("restaurant.category", "category")
+ .addSelect("COUNT(restaurant.category)", "count")
+ .innerJoin("userRestaurantList.restaurant", "restaurant")
+ .where("userRestaurantList.userId = :id", { id })
+ .groupBy("restaurant.category")
+ .getRawMany();
+
+
+ if (categoryCounts.length) {
+ const favoriteCategory = categoryCounts.reduce((a, b) => a.count > b.count ? a : b).category;
+
+ const subQuery = await this.createQueryBuilder()
+ .select("DISTINCT(userRestaurantListSub.restaurantId)", "restaurantId")
+ .from(UserRestaurantListEntity, "userRestaurantListSub")
+ .where("userRestaurantListSub.userId = :id", { id })
.getRawMany();
+
+ const restaurantIds = subQuery.map(item => item.restaurantId);
+
+ const result = await this
+ .createQueryBuilder("userRestaurantList")
+ .leftJoinAndSelect("userRestaurantList.restaurant", "restaurant")
+ .select(["restaurant.id", "restaurant.name", "restaurant.category"])
+ .where("restaurant.category = :category", { category: favoriteCategory })
+ .andWhere("restaurant.address LIKE :region", { region: `%${region.region}%` })
+ .andWhere("userRestaurantList.restaurantId NOT IN (:...restaurantIds)", { restaurantIds: restaurantIds })
+ .groupBy("restaurant.id")
+ .getRawMany();
+
+ if (result.length > 0) {
+ let recommendedRestaurants = [];
+ let usedIndexes = new Set();
+
+ for (let i = 0; i < Math.min(3, result.length); i++) {
+ let randomIndex;
+ do {
+ randomIndex = Math.floor(Math.random() * result.length);
+ } while (usedIndexes.has(randomIndex));
+
+ usedIndexes.add(randomIndex);
+ recommendedRestaurants.push(result[randomIndex]);
+ }
+ return recommendedRestaurants;
+ }
+ }
+ else {
+ const result = await this
+ .createQueryBuilder("userRestaurantList")
+ .leftJoinAndSelect("userRestaurantList.restaurant", "restaurant")
+ .select(["restaurant.id", "restaurant.name", "restaurant.category"])
+ .andWhere("restaurant.address LIKE :region", { region: `%${region.region}%` })
+ .groupBy("restaurant.id")
+ .getRawMany();
+
+ if (result.length > 0) {
+ let recommendedRestaurants = [];
+ let usedIndexes = new Set();
+
+ for (let i = 0; i < Math.min(3, result.length); i++) {
+ let randomIndex;
+ do {
+ randomIndex = Math.floor(Math.random() * result.length);
+ } while (usedIndexes.has(randomIndex));
+
+ usedIndexes.add(randomIndex);
+ recommendedRestaurants.push(result[randomIndex]);
+ }
+ return recommendedRestaurants;
+ }
}
+ return [];
}
}
diff --git a/be/src/user/user.service.ts b/be/src/user/user.service.ts
index d823193a..5ab7881c 100644
--- a/be/src/user/user.service.ts
+++ b/be/src/user/user.service.ts
@@ -1,4 +1,4 @@
-import { Injectable } from "@nestjs/common";
+import { Injectable, UploadedFile } from "@nestjs/common";
import { UserInfoDto } from "./dto/userInfo.dto";
import { InjectRepository } from "@nestjs/typeorm";
import { UserRepository } from "./user.repository";
@@ -8,14 +8,16 @@ import { SearchInfoDto } from "../restaurant/dto/seachInfo.dto";
import { UserRestaurantListRepository } from "./user.restaurantList.repository";
import { UserFollowListRepository } from "./user.followList.repository";
import { Equal, In, Like, Not } from "typeorm";
-import { BadRequestException } from "@nestjs/common/exceptions";
-import { ReviewInfoDto } from "src/review/dto/reviewInfo.dto";
-import { ReviewRepository } from "src/review/review.repository";
+import { BadRequestException, ConflictException } from "@nestjs/common/exceptions";
+import { ReviewInfoDto } from "../review/dto/reviewInfo.dto";
+import { ReviewRepository } from "../review/review.repository";
import { UserWishRestaurantListRepository } from "./user.wishrestaurantList.repository";
-import { AwsService } from "src/aws/aws.service";
+import { AwsService } from "../aws/aws.service";
import { v4 } from "uuid";
import { User } from "./entities/user.entity";
-import { RestaurantInfoEntity } from "src/restaurant/entities/restaurant.entity";
+import { RestaurantInfoEntity } from "../restaurant/entities/restaurant.entity";
+import { AuthService } from "../auth/auth.service";
+import { SortInfoDto } from "../utils/sortInfo.dto";
@Injectable()
export class UserService {
@@ -26,24 +28,39 @@ export class UserService {
private userFollowListRepositoy: UserFollowListRepository,
private reviewRepository: ReviewRepository,
private userWishRestaurantListRepository: UserWishRestaurantListRepository,
- private awsService: AwsService
- ) {}
- async signup(userInfoDto: UserInfoDto) {
- userInfoDto.password = await hashPassword(userInfoDto.password);
+ private awsService: AwsService,
+ private authService: AuthService,
+ ) { }
+ async signup(@UploadedFile() file: Express.Multer.File, userInfoDto: UserInfoDto) {
+ if (userInfoDto.password) userInfoDto.password = await hashPassword(userInfoDto.password);
+ let profileImage;
+
+ if (file) {
+ const uuid = v4();
+ profileImage = `profile/images/${uuid}.png`;
+ } else {
+ profileImage = "profile/images/defaultprofile.png";
+ }
+
const user = {
...userInfoDto,
- profileImage: "profile/images/defaultprofile.png",
+ profileImage: profileImage
};
- if (userInfoDto.profileImage) {
- const uuid = v4();
- user.profileImage = `profile/images/${uuid}.png`;
- }
+ try {
+ const newUser = this.usersRepository.create(user);
+ const result = await this.usersRepository.createUser(newUser);
+ if (file) {
+ await this.awsService.uploadToS3(profileImage, file.buffer);
+ }
+ return this.authService.createTokens(result.id);
+ } catch (error) {
+ if (error.code === "23505") {
+ throw new ConflictException("Duplicated Value");
+ }
+ }
+
- const newUser = this.usersRepository.create(user);
- await this.usersRepository.createUser(newUser);
- if (userInfoDto.profileImage)this.awsService.uploadToS3(user.profileImage, userInfoDto.profileImage);
- return;
}
async getNickNameAvailability(nickName: UserInfoDto["nickName"]) {
return await this.usersRepository.getNickNameAvailability(nickName);
@@ -77,7 +94,8 @@ export class UserService {
targetInfo.id,
tokenInfo.id
);
- if ( restaurantList )result["restaurants"] = restaurantList;
+ if (restaurantList) result["restaurants"] = restaurantList;
+ else result["restaurants"] = [];
result.profileImage = this.awsService.getImageURL(result.profileImage);
return result;
} catch (err) {
@@ -91,15 +109,21 @@ export class UserService {
}
async getMyRestaurantListInfo(
searchInfoDto: SearchInfoDto,
+ sortInfoDto: SortInfoDto,
tokenInfo: TokenInfo
) {
const results =
await this.userRestaurantListRepository.getMyRestaurantListInfo(
searchInfoDto,
+ sortInfoDto,
tokenInfo.id
);
- for (const restaurant of results) {
+ let list
+ if ('items' in results) list = results.items;
+ else list = results;
+
+ for (const restaurant of list) {
const reviewCount = await this.reviewRepository
.createQueryBuilder("review")
.where("review.restaurant_id = :restaurantId", {
@@ -107,30 +131,52 @@ export class UserService {
})
.getCount();
+ const reviewInfo = await this.reviewRepository
+ .createQueryBuilder("review")
+ .leftJoin("review.reviewLikes", "reviewLike")
+ .select(["review.id", "review.reviewImage"],)
+ .groupBy("review.id")
+ .where("review.restaurant_id = :restaurantId and review.reviewImage is NOT NULL", { restaurantId: restaurant.restaurant_id })
+ .orderBy("COUNT(CASE WHEN reviewLike.isLike = true THEN 1 ELSE NULL END)", "DESC")
+ .getRawOne();
+ if (reviewInfo) {
+ restaurant.restaurant_reviewImage = this.awsService.getImageURL(reviewInfo.review_reviewImage);
+ }
+ else {
+ restaurant.restaurant_reviewImage = this.awsService.getImageURL("review/images/defaultImage.png");
+ }
+
restaurant.isMy = true;
restaurant.restaurant_reviewCnt = reviewCount;
}
return results;
}
- async getMyWishRestaurantListInfo(tokenInfo: TokenInfo) {
+ async getMyWishRestaurantListInfo(tokenInfo: TokenInfo, sortInfoDto: SortInfoDto) {
const result =
await this.userWishRestaurantListRepository.getMyWishRestaurantListInfo(
- tokenInfo.id
+ tokenInfo.id,
+ sortInfoDto
);
return result;
}
+ async getStateIsWish(tokenInfo: TokenInfo, restaurantId: number) {
+ const result = await this.userWishRestaurantListRepository.findOne({ where: { restaurantId: restaurantId, userId: tokenInfo["id"] } });
+ if (result) return { isWish: true };
+ else return { isWish: false };
+ }
async getMyFollowListInfo(tokenInfo: TokenInfo) {
const userIds = await this.userFollowListRepositoy.getMyFollowListInfo(
tokenInfo.id
);
const userIdValues = userIds.map((user) => user.followingUserId);
const result = await this.usersRepository.find({
- select: ["nickName", "region"],
+ select: ["nickName", "region", "profileImage"],
where: { id: In(userIdValues) },
});
return result.map((user) => ({
...user,
+ profileImage: this.awsService.getImageURL(user.profileImage),
isFollow: true,
}));
}
@@ -146,7 +192,7 @@ export class UserService {
(user) => user.followingUserId
);
const result = await this.usersRepository.find({
- select: ["id", "nickName", "region"],
+ select: ["id", "nickName", "region", "profileImage"],
where: { id: In(followerUserIdValues) },
});
@@ -154,23 +200,68 @@ export class UserService {
const { id, ...userInfo } = user;
return {
...userInfo,
+ profileImage: this.awsService.getImageURL(userInfo.profileImage),
isFollow: followUserIdValues.includes(id) ? true : false,
};
});
}
+
+
+
async getRecommendUserListInfo(tokenInfo: TokenInfo) {
const userIds = await this.userFollowListRepositoy.getMyFollowListInfo(
tokenInfo.id
);
const userIdValues = userIds.map((user) => user.followingUserId);
userIdValues.push(tokenInfo.id);
- const result =
- await this.usersRepository.getRecommendUserListInfo(userIdValues);
- return result.map((user) => ({
+ const result = await this.usersRepository.getRecommendUserListInfo(userIdValues, tokenInfo.id);
+
+ function getRandomInts(min: number, max: number, count: number): number[] {
+ if (max === -1) {
+ return [];
+ } else if (max === 0) {
+ return [0];
+ }
+
+ const ints = new Set();
+ while (ints.size < count) {
+ const rand = Math.floor(Math.random() * (max - min + 1)) + min;
+ ints.add(rand);
+ }
+ return [...ints].sort((a, b) => a - b);
+ }
+
+ const randomIndexes = getRandomInts(0, result.length - 1, 2);
+ if (randomIndexes.length === 0) return [];
+
+ const selectedUsers = randomIndexes.map(index => result[index]);
+ return selectedUsers.map((user) => ({
...user,
+ user_profileImage: this.awsService.getImageURL(user.user_profileImage),
isFollow: false,
}));
}
+ async getRecommendFood(tokenInfo: TokenInfo) {
+ const region = await this.usersRepository.findOne({ select: ["region"], where: { id: tokenInfo.id } });
+ const restaurants = await this.userRestaurantListRepository.getMyFavoriteFoodCategory(tokenInfo.id, region);
+ for (const restaurant of restaurants) {
+ const reviewInfo = await this.reviewRepository
+ .createQueryBuilder("review")
+ .leftJoin("review.reviewLikes", "reviewLike")
+ .select(["review.id", "review.reviewImage"],)
+ .groupBy("review.id")
+ .where("review.restaurant_id = :restaurantId and review.reviewImage is NOT NULL", { restaurantId: restaurant.restaurant_id })
+ .orderBy("COUNT(CASE WHEN reviewLike.isLike = true THEN 1 ELSE NULL END)", "DESC")
+ .getRawOne();
+ if (reviewInfo) {
+ restaurant.restaurant_reviewImage = this.awsService.getImageURL(reviewInfo.review_reviewImage);
+ }
+ else {
+ restaurant.restaurant_reviewImage = this.awsService.getImageURL("review/images/defaultImage.png");
+ }
+ }
+ return restaurants;
+ }
async searchTargetUser(tokenInfo: TokenInfo, nickName: string, region: string[]) {
const whereCondition: any = {
nickName: Like(`%${nickName}%`),
@@ -195,6 +286,7 @@ export class UserService {
))
? true
: false;
+ result[i]["profileImage"] = this.awsService.getImageURL(result[i]["profileImage"]);
}
return result;
}
@@ -235,9 +327,19 @@ export class UserService {
async addRestaurantToNebob(
reviewInfoDto: ReviewInfoDto,
tokenInfo: TokenInfo,
- restaurantId: number
+ restaurantId: number,
+ file: Express.Multer.File
) {
const reviewEntity = this.reviewRepository.create(reviewInfoDto);
+ let reviewImage;
+ if (file) {
+ const uuid = v4();
+ reviewImage = `review/images/${uuid}.png`;
+ reviewEntity.reviewImage = reviewImage;
+ }
+ else {
+ reviewEntity.reviewImage = `review/images/defaultImage.png`;
+ }
const userEntity = new User();
userEntity.id = tokenInfo["id"];
reviewEntity.user = userEntity;
@@ -252,6 +354,7 @@ export class UserService {
restaurantId,
reviewEntity
);
+ if (file) await this.awsService.uploadToS3(reviewImage, file.buffer);
} catch (err) {
throw new BadRequestException();
}
@@ -290,30 +393,38 @@ export class UserService {
}
async logout(tokenInfo: TokenInfo) {
- return await this.usersRepository.logout(tokenInfo.id);
+ return await this.authService.logout(tokenInfo.id);
}
async deleteUserAccount(tokenInfo: TokenInfo) {
return await this.usersRepository.deleteUserAccount(tokenInfo.id);
}
- async updateMypageUserInfo(tokenInfo: TokenInfo, userInfoDto: UserInfoDto) {
- userInfoDto.password = await hashPassword(userInfoDto.password);
- const user = {
+ async updateMypageUserInfo(file: Express.Multer.File, tokenInfo: TokenInfo, userInfoDto: UserInfoDto, isChanged: Boolean) {
+ const existedInfo = await this.usersRepository.findOne({ select: ["profileImage", "password"], where: { id: tokenInfo.id } })
+
+ if (userInfoDto.password) userInfoDto.password = await hashPassword(userInfoDto.password);
+ else userInfoDto.password = existedInfo.password;
+
+ let profileImage = existedInfo.profileImage;
+ if (isChanged) {
+ if (file) {
+ const uuid = v4();
+ profileImage = `profile/images/${uuid}.png`;
+ } else {
+ profileImage = "profile/images/defaultprofile.png";
+ }
+ }
+
+ let user = {
...userInfoDto,
- profileImage: "profile/images/defaultprofile.png",
+ profileImage
};
- if (userInfoDto.profileImage) {
- const uuid = v4();
- user.profileImage = `profile/images/${uuid}.png`;
- }
-
const newUser = this.usersRepository.create(user);
- const result = await this.usersRepository.updateMypageUserInfo(
- tokenInfo.id,
- newUser
- );
- if (userInfoDto.profileImage)this.awsService.uploadToS3(user.profileImage, userInfoDto.profileImage);
- return result;
+ const updatedUser = await this.usersRepository.updateMypageUserInfo(tokenInfo.id, newUser);
+ if (file && isChanged) {
+ this.awsService.uploadToS3(profileImage, file.buffer);
+ }
+ return updatedUser;
}
}
diff --git a/be/src/user/user.wishrestaurantList.repository.ts b/be/src/user/user.wishrestaurantList.repository.ts
index 8736cd58..819f222a 100644
--- a/be/src/user/user.wishrestaurantList.repository.ts
+++ b/be/src/user/user.wishrestaurantList.repository.ts
@@ -4,6 +4,7 @@ import { UserWishRestaurantListEntity } from "./entities/user.wishrestaurantlist
import { TokenInfo } from "./user.decorator";
import { RestaurantInfoEntity } from "src/restaurant/entities/restaurant.entity";
import { UserRestaurantListEntity } from "./entities/user.restaurantlist.entity";
+import { SortInfoDto } from "src/utils/sortInfo.dto";
@Injectable()
export class UserWishRestaurantListRepository extends Repository {
constructor(private dataSource: DataSource) {
@@ -32,8 +33,8 @@ export class UserWishRestaurantListRepository extends Repository sortInfoDto.limit;
+ const resultItems = hasNext ? items.slice(0, -1) : items;
+
+ return {
+ hasNext,
+ items : resultItems,
+ }
}
}
diff --git a/be/src/utils/sortInfo.dto.ts b/be/src/utils/sortInfo.dto.ts
new file mode 100644
index 00000000..709394a2
--- /dev/null
+++ b/be/src/utils/sortInfo.dto.ts
@@ -0,0 +1,20 @@
+import { IsString, IsInt, IsOptional, Min, IsNotEmpty } from 'class-validator';
+import { Type } from 'class-transformer';
+
+export class SortInfoDto {
+ @IsString()
+ @IsOptional()
+ sort?: string;
+
+ @IsInt()
+ @Min(1)
+ @Type(() => Number)
+ @IsOptional()
+ page?: number;
+
+ @IsInt()
+ @Min(1)
+ @Type(() => Number)
+ @IsOptional()
+ limit?: number;
+}
\ No newline at end of file
diff --git a/be/test/app.e2e-spec.ts b/be/test/app.e2e-spec.ts
index 05db0a79..1163677a 100644
--- a/be/test/app.e2e-spec.ts
+++ b/be/test/app.e2e-spec.ts
@@ -1,7 +1,7 @@
import { Test, TestingModule } from "@nestjs/testing";
import { INestApplication } from "@nestjs/common";
import * as request from "supertest";
-import { AppModule } from "./../src/app.module";
+import { AppModule } from "../src/app.module";
describe("AppController (e2e)", () => {
let app: INestApplication;
@@ -15,10 +15,26 @@ describe("AppController (e2e)", () => {
await app.init();
});
- it("/ (GET)", () => {
- return request(app.getHttpServer())
- .get("/")
- .expect(200)
- .expect("Hello World!");
+ it('/api/user', async () => {
+ const userData = {
+ email: "test@example.com",
+ password: "1234",
+ provider: "site",
+ nickName: "test",
+ region: "강남구",
+ birthdate: "1234/56/78",
+ isMale: true
+ };
+
+ const response = await request(app.getHttpServer())
+ .post('/user')
+ .send(userData)
+ .expect(201);
+
+ expect(response.statusCode).toBe(201);
+ });
+
+ afterAll(async () => {
+ await app.close();
});
});