From d2d8a043b0397e2c229b982f35846c864098e966 Mon Sep 17 00:00:00 2001 From: yeonwoo Date: Mon, 5 Jan 2026 15:52:42 +0900 Subject: [PATCH 01/11] =?UTF-8?q?feat=20:=20recruitingStudy=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../component/button/ImageTextButton.kt | 4 +- .../component/empty/EmptyAlert.kt | 3 +- .../designsystem/component/study/StudyItem.kt | 83 ++- .../study/section/ActivityThemeSection.kt | 2 +- .../java/com/umcspot/spot/model/Global.kt | 18 +- .../com/umcspot/spot/ui/extension/GsonExt.kt | 2 +- .../spot/study/datasource/StudyDataSource.kt | 7 +- .../datasourceimpl/StudyDataSourceImpl.kt | 14 +- .../study/dto/response/StudyResponseDto.kt | 45 +- .../umcspot/spot/study/mapper/StudyMapper.kt | 34 +- .../repositoryimpl/StudyRepositoryImpl.kt | 65 +- .../spot/study/service/StudyService.kt | 23 +- .../umcspot/spot/study/model/StudyResult.kt | 38 +- .../spot/study/repository/StudyRepository.kt | 10 +- .../java/com/umcspot/spot/main/MainNavHost.kt | 16 +- .../com/umcspot/spot/main/MainNavigator.kt | 10 +- .../PreferLocationStudyScreen.kt | 632 +++++++++--------- .../PreferLocationStudyViewModel.kt | 308 ++++----- .../PreferLocationStudyNavigation.kt | 68 +- .../recruiting/RecruitingStudyFilterScreen.kt | 109 ++- .../RecruitingStudyFilterViewmodel.kt | 117 ---- .../study/recruiting/RecruitingStudyScreen.kt | 268 +++++--- .../recruiting/RecruitingStudyViewModel.kt | 144 ++-- .../RecruitingStudyFilterNavigation.kt | 12 + 24 files changed, 1083 insertions(+), 949 deletions(-) delete mode 100644 feature/study/src/main/java/com/umcspot/spot/study/recruiting/RecruitingStudyFilterViewmodel.kt diff --git a/core/designsystem/src/main/java/com/umcspot/spot/designsystem/component/button/ImageTextButton.kt b/core/designsystem/src/main/java/com/umcspot/spot/designsystem/component/button/ImageTextButton.kt index dcf6a66c..4099117c 100644 --- a/core/designsystem/src/main/java/com/umcspot/spot/designsystem/component/button/ImageTextButton.kt +++ b/core/designsystem/src/main/java/com/umcspot/spot/designsystem/component/button/ImageTextButton.kt @@ -105,7 +105,9 @@ fun MultiButton( tintIcon: Boolean = false, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, ) { - val isPressed by interactionSource.collectIsPressedAsState() + val internalSource = interactionSource ?: remember { MutableInteractionSource() } + val isPressed by internalSource.collectIsPressedAsState() + val colors = state.resolveColors( enabled = enabled, isPressed = isPressed, diff --git a/core/designsystem/src/main/java/com/umcspot/spot/designsystem/component/empty/EmptyAlert.kt b/core/designsystem/src/main/java/com/umcspot/spot/designsystem/component/empty/EmptyAlert.kt index 68e70d5d..7ec6b819 100644 --- a/core/designsystem/src/main/java/com/umcspot/spot/designsystem/component/empty/EmptyAlert.kt +++ b/core/designsystem/src/main/java/com/umcspot/spot/designsystem/component/empty/EmptyAlert.kt @@ -55,7 +55,6 @@ fun EmptyAlert( Text( text = alertTitle, style = SpotTheme.typography.h3, - fontSize = 30.sp, color = SpotTheme.colors.B500, textAlign = TextAlign.Center ) @@ -67,7 +66,7 @@ fun EmptyAlert( textAlign = TextAlign.Center ) Spacer(Modifier.height(screenHeightDp(53.dp))) - content() // ✅ 여기로 버튼 등 추가 + content() } } diff --git a/core/designsystem/src/main/java/com/umcspot/spot/designsystem/component/study/StudyItem.kt b/core/designsystem/src/main/java/com/umcspot/spot/designsystem/component/study/StudyItem.kt index d1ea28e4..98d94296 100644 --- a/core/designsystem/src/main/java/com/umcspot/spot/designsystem/component/study/StudyItem.kt +++ b/core/designsystem/src/main/java/com/umcspot/spot/designsystem/component/study/StudyItem.kt @@ -8,8 +8,10 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.Text @@ -26,70 +28,74 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import coil.compose.AsyncImage import com.umcspot.spot.designsystem.R +import com.umcspot.spot.designsystem.component.button.ClickSurface import com.umcspot.spot.designsystem.shapes.SpotShapes import com.umcspot.spot.designsystem.theme.G300 import com.umcspot.spot.designsystem.theme.SpotTheme import com.umcspot.spot.model.ImageRef import com.umcspot.spot.study.model.StudyResult +import com.umcspot.spot.ui.extension.screenHeightDp +import com.umcspot.spot.ui.extension.screenWidthDp @Composable fun StudyListItem( item: StudyResult, modifier: Modifier = Modifier, - onClick: () -> Unit = {}, + onClick: (StudyResult) -> Unit = {}, ) { - Column(modifier = modifier) { + ClickSurface( + onClick = { onClick(item)}, + modifier = modifier + ) { Row( - modifier = modifier.clickable(onClick = onClick), - horizontalArrangement = Arrangement.spacedBy(12.dp), + modifier = Modifier.padding(screenWidthDp(7.dp)), + horizontalArrangement = Arrangement.spacedBy(13.dp), verticalAlignment = Alignment.CenterVertically ) { StudyThumbnail( - imageRef = item.studyImage, + imageRef = item.profileImageUrl, modifier = Modifier - .size(56.dp) + .size(screenWidthDp(73.dp)) .clip(SpotShapes.Hard) ) // 텍스트 + 통계 Column( - modifier = Modifier.weight(1f), - verticalArrangement = Arrangement.spacedBy(6.dp) - ) { + modifier = Modifier + .fillMaxWidth() + .height(screenHeightDp(73.dp)) + .padding(screenHeightDp(4.dp)) + ){ Text( - text = item.title, - style = SpotTheme.typography.medium_500.copy(fontSize = 16.sp), + text = item.name, + style = SpotTheme.typography.h5, maxLines = 1, overflow = TextOverflow.Ellipsis ) Text( - text = item.goal, - style = SpotTheme.typography.small_500.copy(fontSize = 14.sp), + text = item.description, + style = SpotTheme.typography.regular_400, maxLines = 1, overflow = TextOverflow.Ellipsis ) - Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) { + Spacer(modifier = Modifier.weight(1f)) + + Row(horizontalArrangement = Arrangement.spacedBy(screenWidthDp(4.dp))) { Stat( - iconRes = R.drawable.group, count1 = item.member, count2 = item.maxMember + iconRes = R.drawable.group, + count1 = item.currentMembers, + count2 = item.maxMembers ) Stat( - iconRes = R.drawable.like_default, count2 = item.likes + iconRes = R.drawable.eye, count2 = item.hitCount ) Stat( - iconRes = R.drawable.eye, count2 = item.views + iconRes = R.drawable.like_default, count2 = item.likeCount ) } } } - Spacer(Modifier.padding(5.dp)) - - HorizontalDivider( - modifier = Modifier - .fillMaxWidth(), - color = SpotTheme.colors.G300, - thickness = 0.5.dp - ) } } @@ -100,17 +106,20 @@ private fun Stat( count2: Int ) { fun cap(n: Int) = if (n >= 1000) "999+" else n.toString() - val display = if (count1 != 0) "${cap(count1)}/${cap(count2)}" else cap(count2) + val display = if (count1 != 0) "${cap(count1)} / ${cap(count2)}" else cap(count2) - Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp)) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(screenWidthDp(4.dp)) + ) { Icon( painter = painterResource(iconRes), contentDescription = null, tint = Color.Unspecified, - modifier = Modifier.size(14.dp) + modifier = Modifier.size(screenWidthDp(14.dp)) ) - Text(text = display, style = SpotTheme.typography.small_500.copy(fontSize = 12.sp)) + Text(text = display, style = SpotTheme.typography.small_400) } } @@ -171,13 +180,15 @@ private fun StudyListItemPreview() { SpotTheme{ StudyListItem( item = StudyResult( - studyId = "1", - title = "Sample Study", - goal = "Sample Goal", - maxMember = 10, - member = 5, - likes = 400, - views = 1200, + id = 1, + name = "Sample Study", + description = "Sample Goal", + maxMembers = 10, + currentMembers = 5, + likeCount = 400, + isLiked = false, + hitCount = 1200, + profileImageUrl = ImageRef.Name("spot_logo"), ), modifier = Modifier.padding(10.dp), onClick = {} diff --git a/core/designsystem/src/main/java/com/umcspot/spot/designsystem/component/study/section/ActivityThemeSection.kt b/core/designsystem/src/main/java/com/umcspot/spot/designsystem/component/study/section/ActivityThemeSection.kt index 1500e9e1..a822bb2a 100644 --- a/core/designsystem/src/main/java/com/umcspot/spot/designsystem/component/study/section/ActivityThemeSection.kt +++ b/core/designsystem/src/main/java/com/umcspot/spot/designsystem/component/study/section/ActivityThemeSection.kt @@ -33,7 +33,7 @@ fun ActivityThemeSection( @Composable fun ActivityThemeSection( - selectedThemes: ImmutableList, + selectedThemes: List, onSelect: (StudyTheme) -> Unit, modifier: Modifier = Modifier, maxSelection: Int = 3 diff --git a/core/model/src/main/java/com/umcspot/spot/model/Global.kt b/core/model/src/main/java/com/umcspot/spot/model/Global.kt index 81fb9531..246be251 100644 --- a/core/model/src/main/java/com/umcspot/spot/model/Global.kt +++ b/core/model/src/main/java/com/umcspot/spot/model/Global.kt @@ -18,9 +18,9 @@ val PostType.korean: String } enum class RecruitingStudySort(val label: String) { - LATEST("최신 순"), - VIEW("조회수 높은 순"), - LIKE("관심 많은 순") + RECENT("최신 순"), + HITS("조회수 높은 순"), + LIKES("관심 많은 순") } @@ -37,12 +37,12 @@ enum class FeeRange( val label: String ) { NONE("없음"), - UNDER_10K("1만원 미만"), - ABOUT10K("1만원대"), - ABOUT20K("2만원대"), - ABOUT30K("3만원대"), - ABOUT40K("4만원대"), - OVER50K("5만원 이상") + BELOW_10K("1만원 미만"), + FROM_10K_TO_20K("1만원대"), + FROM_20K_TO_30K("2만원대"), + FROM_30K_TO_40K("3만원대"), + FROM_40K_TO_50K("4만원대"), + ABOVE_50K("5만원 이상") } enum class StudyTheme( diff --git a/core/ui/src/main/java/com/umcspot/spot/ui/extension/GsonExt.kt b/core/ui/src/main/java/com/umcspot/spot/ui/extension/GsonExt.kt index de048de4..31a7b184 100644 --- a/core/ui/src/main/java/com/umcspot/spot/ui/extension/GsonExt.kt +++ b/core/ui/src/main/java/com/umcspot/spot/ui/extension/GsonExt.kt @@ -1,10 +1,10 @@ package com.umcspot.spot.ui.extension +import com.google.gson.Gson import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.MultipartBody import okhttp3.RequestBody import okhttp3.RequestBody.Companion.toRequestBody -import com.google.gson.Gson fun Any.toRequestBody(): RequestBody { val jsonString = Gson().toJson(this) diff --git a/data/study/src/main/java/com/umcspot/spot/study/datasource/StudyDataSource.kt b/data/study/src/main/java/com/umcspot/spot/study/datasource/StudyDataSource.kt index eef94bb4..98a40bd3 100644 --- a/data/study/src/main/java/com/umcspot/spot/study/datasource/StudyDataSource.kt +++ b/data/study/src/main/java/com/umcspot/spot/study/datasource/StudyDataSource.kt @@ -13,11 +13,6 @@ import java.io.File interface StudyDataSource { suspend fun getPopularStudies(): BaseResponse suspend fun getRecommendStudies(): BaseResponse - suspend fun getRecruitingStudies( - sortType: RecruitingStudySort, - activityType: ActivityType, - theme: StudyTheme, - feeRange: FeeRange - ): BaseResponse + suspend fun getRecruitingStudies(feeCategory: FeeRange?, categories: List?, isOnline: Boolean?, sortBy: RecruitingStudySort?, cursor: Long?, size: Int): BaseResponse suspend fun createStudy(request: StudyRequestDto, imageFile: File?): BaseResponse } \ No newline at end of file diff --git a/data/study/src/main/java/com/umcspot/spot/study/datasourceimpl/StudyDataSourceImpl.kt b/data/study/src/main/java/com/umcspot/spot/study/datasourceimpl/StudyDataSourceImpl.kt index ab830b4d..bac3fc4f 100644 --- a/data/study/src/main/java/com/umcspot/spot/study/datasourceimpl/StudyDataSourceImpl.kt +++ b/data/study/src/main/java/com/umcspot/spot/study/datasourceimpl/StudyDataSourceImpl.kt @@ -1,9 +1,7 @@ package com.umcspot.spot.study.datasourceimpl -import com.umcspot.spot.model.ActivityType import com.umcspot.spot.model.FeeRange import com.umcspot.spot.model.RecruitingStudySort -import com.umcspot.spot.model.StudyTheme import com.umcspot.spot.network.model.BaseResponse import com.umcspot.spot.study.datasource.StudyDataSource import com.umcspot.spot.study.dto.request.StudyRequestDto @@ -29,12 +27,14 @@ class StudyDataSourceImpl @Inject constructor( studyService.getRecommendStudies() override suspend fun getRecruitingStudies( - sortType: RecruitingStudySort, - activityType: ActivityType, - theme: StudyTheme, - feeRange: FeeRange + feeCategory: FeeRange?, + categories: List?, + isOnline: Boolean?, + sortBy: RecruitingStudySort?, + cursor: Long?, + size: Int ): BaseResponse = - studyService.getRecruitingStudies(sortType, activityType, theme, feeRange) + studyService.getRecruitingStudies(feeCategory, categories, isOnline, sortBy, cursor, size) override suspend fun createStudy( request: StudyRequestDto, diff --git a/data/study/src/main/java/com/umcspot/spot/study/dto/response/StudyResponseDto.kt b/data/study/src/main/java/com/umcspot/spot/study/dto/response/StudyResponseDto.kt index 0e5f86f4..ef5e2b8b 100644 --- a/data/study/src/main/java/com/umcspot/spot/study/dto/response/StudyResponseDto.kt +++ b/data/study/src/main/java/com/umcspot/spot/study/dto/response/StudyResponseDto.kt @@ -8,34 +8,43 @@ import kotlinx.serialization.Serializable @SuppressLint("UnsafeOptInUsageError") @Serializable data class StudyResponseDto( - @SerialName("studyList") - val studyList : List + @SerialName("content") + val content : List, + @SerialName("hasNext") + val hasNext : Boolean, + @SerialName("nextCursor") + val nextCursor : String? = null, + @SerialName("totalElements") + val totalElements : Int, ) @SuppressLint("UnsafeOptInUsageError") @Serializable data class Study ( - @SerialName("StudyId") - val studyId: String, + @SerialName("id") + val id: String, - @SerialName("title") - val title: String, + @SerialName("name") + val name: String, - @SerialName("goal") - val goal: String, + @SerialName("description") + val description: String, - @SerialName("maxMember") - val maxMember : Int, + @SerialName("maxMembers") + val maxMembers : Int, - @SerialName("member") - val member: Int = 0, + @SerialName("currentMembers") + val currentMembers: Int, - @SerialName("likes") - val likes: Int = 0, + @SerialName("likeCount") + val likeCount: Int, - @SerialName("views") - val views: Int = 0, + @SerialName("isLiked") + val isLiked: Boolean, - @SerialName("studyImage") - val studyImage: ImageRef + @SerialName("hitCount") + val hitCount: Int = 0, + + @SerialName("profileImageUrl") + val profileImageUrl: String? ) \ No newline at end of file diff --git a/data/study/src/main/java/com/umcspot/spot/study/mapper/StudyMapper.kt b/data/study/src/main/java/com/umcspot/spot/study/mapper/StudyMapper.kt index 72cde6a4..5e1b90fe 100644 --- a/data/study/src/main/java/com/umcspot/spot/study/mapper/StudyMapper.kt +++ b/data/study/src/main/java/com/umcspot/spot/study/mapper/StudyMapper.kt @@ -1,7 +1,8 @@ package com.umcspot.spot.study.mapper +import com.umcspot.spot.model.toImageRef import com.umcspot.spot.study.dto.request.StudyRequestDto -import com.umcspot.spot.study.dto.response.Study as DTOStudy +import com.umcspot.spot.study.dto.response.Study import com.umcspot.spot.study.dto.response.StudyResponseDto import com.umcspot.spot.study.model.StudyCreateModel import com.umcspot.spot.study.model.StudyResult @@ -18,17 +19,22 @@ fun StudyCreateModel.toData(): StudyRequestDto = StudyRequestDto( regionCodes = this.regionCodes ) -fun DTOStudy.toDomain(): StudyResult = StudyResult( - studyId = this.studyId, - title = this.title, - goal = this.goal, - maxMember = this.maxMember, - member = this.member, - likes = this.likes, - views = this.views, - studyImage = this.studyImage -) +fun Study.toDomain() : StudyResult = + StudyResult ( + id = this.id.toLong(), + name = this.name, + description = this.description, + maxMembers = this.maxMembers, + currentMembers = this.currentMembers, + likeCount = this.likeCount, + isLiked = this.isLiked, + hitCount = this.hitCount, + profileImageUrl = this.profileImageUrl.toImageRef() + ) -fun StudyResponseDto.toDomain(): StudyResultList = StudyResultList( - studyList = this.studyList.map { it.toDomain() } -) \ No newline at end of file +fun StudyResponseDto.toDomainList(): StudyResultList = + StudyResultList( + studyList = this.content.map(Study::toDomain), + hasNext = this.hasNext, + nextCursor = this.nextCursor?.toLong() + ) \ No newline at end of file diff --git a/data/study/src/main/java/com/umcspot/spot/study/repositoryimpl/StudyRepositoryImpl.kt b/data/study/src/main/java/com/umcspot/spot/study/repositoryimpl/StudyRepositoryImpl.kt index 4d8daeab..52232321 100644 --- a/data/study/src/main/java/com/umcspot/spot/study/repositoryimpl/StudyRepositoryImpl.kt +++ b/data/study/src/main/java/com/umcspot/spot/study/repositoryimpl/StudyRepositoryImpl.kt @@ -1,12 +1,13 @@ package com.umcspot.spot.study.repositoryimpl +import android.util.Log import com.umcspot.spot.model.ActivityType import com.umcspot.spot.model.FeeRange import com.umcspot.spot.model.RecruitingStudySort import com.umcspot.spot.model.StudyTheme import com.umcspot.spot.study.datasource.StudyDataSource import com.umcspot.spot.study.mapper.toData -import com.umcspot.spot.study.mapper.toDomain +import com.umcspot.spot.study.mapper.toDomainList import com.umcspot.spot.study.model.StudyCreateModel import com.umcspot.spot.study.model.StudyResultList import com.umcspot.spot.study.repository.StudyRepository @@ -16,33 +17,45 @@ import javax.inject.Inject class StudyRepositoryImpl @Inject constructor( private val studyDataSource: StudyDataSource ) : StudyRepository { - override suspend fun getPopularStudies(): Result = runCatching { val response = studyDataSource.getPopularStudies() - response.result.toDomain() + response.result.toDomainList() + }.recoverCatching { + setPopularDummies() } + private fun setPopularDummies(count: Int = 5): StudyResultList = + StudyResultList(StudyResultList.getPopularDummies(count), hasNext = false, nextCursor = null) + + override suspend fun getRecommendStudies(): Result = runCatching { - val response = studyDataSource.getRecommendStudies() - response.result.toDomain() + val response = studyDataSource.getPopularStudies() + response.result.toDomainList() + }.recoverCatching { + setRecommendDummies() } + private fun setRecommendDummies(count: Int = 5): StudyResultList = + StudyResultList(StudyResultList.getRecommendedDummies(count), hasNext = false, nextCursor = null) + + override suspend fun getRecruitingStudies( - sortType: RecruitingStudySort, - activityType: ActivityType?, - theme: StudyTheme?, - feeRange: FeeRange? + feeCategory: FeeRange?, + categories: List?, + isOnline: Boolean?, + sortBy: RecruitingStudySort?, + cursor: Long?, + size: Int ): Result = runCatching { - val response = studyDataSource.getRecruitingStudies( - sortType = sortType, - activityType = activityType ?: ActivityType.OFFLINE, - theme = theme ?: StudyTheme.OTHER, - feeRange = feeRange ?: FeeRange.NONE - ) - response.result.toDomain() + val response = studyDataSource.getRecruitingStudies(feeCategory = feeCategory, categories = categories, sortBy = sortBy, isOnline = isOnline, cursor = cursor, size = size) + response.result.toDomainList() + }.onFailure { e -> + Log.e("StudyRepository", "getRecruitingStudies failed", e) + }.recoverCatching { + setRecommendDummies(30) } override suspend fun getPreferLocationStudies( @@ -50,16 +63,16 @@ class StudyRepositoryImpl @Inject constructor( activityType: ActivityType?, theme: StudyTheme?, feeRange: FeeRange? - ): Result = - runCatching { - val response = studyDataSource.getRecruitingStudies( - sortType = sortType, - activityType = activityType ?: ActivityType.OFFLINE, - theme = theme ?: StudyTheme.OTHER, - feeRange = feeRange ?: FeeRange.NONE - ) - response.result.toDomain() - } + ): Result = TODO() +// runCatching { +// val response = studyDataSource.getRecruitingStudies( +// sortType = sortType, +// activityType = activityType ?: ActivityType.OFFLINE, +// theme = theme ?: StudyTheme.OTHER, +// feeRange = feeRange ?: FeeRange.NONE +// ) +// response.result.toDomain() +// } override suspend fun createStudy( studyCreateModel: StudyCreateModel, diff --git a/data/study/src/main/java/com/umcspot/spot/study/service/StudyService.kt b/data/study/src/main/java/com/umcspot/spot/study/service/StudyService.kt index f1e48fa5..42605168 100644 --- a/data/study/src/main/java/com/umcspot/spot/study/service/StudyService.kt +++ b/data/study/src/main/java/com/umcspot/spot/study/service/StudyService.kt @@ -25,13 +25,24 @@ interface StudyService { // @Body request: StudyRequestDto ): BaseResponse - - @GET("/api/v1/service") + @GET("/api/studies/recruiting") suspend fun getRecruitingStudies( - @Query("sortType") sortType: RecruitingStudySort, - @Query("activityType") activityType: ActivityType?, - @Query("theme") theme: StudyTheme?, - @Query("feeRange") feeRange: FeeRange? + @Query("feeCategory") feeCategory: FeeRange?, + @Query("categories") categories: List?, + @Query("isOnline") isOnline: Boolean?, + @Query("sortBy") sortBy: RecruitingStudySort?, + @Query("cursor") cursor: Long?, + @Query("size") size: Int + ): BaseResponse + + @GET("/api/studies/preferLocation") + suspend fun getPreferLocationStudies( + @Query("feeCategory") feeCategory: FeeRange, + @Query("categories") categories: List, + @Query("isOnline") isOnline: Boolean, + @Query("sortBy") sortBy: RecruitingStudySort, + @Query("cursor") cursor: Long?, + @Query("size") size: Int ): BaseResponse @Multipart diff --git a/domain/study/src/main/java/com/umcspot/spot/study/model/StudyResult.kt b/domain/study/src/main/java/com/umcspot/spot/study/model/StudyResult.kt index a0ac8a42..a23e174d 100644 --- a/domain/study/src/main/java/com/umcspot/spot/study/model/StudyResult.kt +++ b/domain/study/src/main/java/com/umcspot/spot/study/model/StudyResult.kt @@ -3,7 +3,9 @@ package com.umcspot.spot.study.model import com.umcspot.spot.model.ImageRef data class StudyResultList ( - val studyList: List + val studyList: List, + val hasNext: Boolean, + val nextCursor: Long? ) { companion object { @JvmStatic @@ -18,14 +20,15 @@ data class StudyResultList ( } data class StudyResult( - val studyId: String, - val title: String, - val goal: String, - val maxMember : Int, - val member: Int = 0, - val likes: Int = 0, - val views: Int = 0, - val studyImage: ImageRef = ImageRef.None + val id: Long, + val name: String, + val description: String, + val maxMembers : Int, + val currentMembers: Int = 0, + val likeCount: Int = 0, + val isLiked : Boolean, + val hitCount: Int = 0, + val profileImageUrl: ImageRef ) { companion object { private val titles = listOf( @@ -41,14 +44,15 @@ data class StudyResult( val max = 8 + (index % 5) // 8~12 val mem = (3 + (index % 6)).coerceAtMost(max) return StudyResult( - studyId = "$idPrefix-$index", - title = title, - goal = "주 2회 진행, 코드리뷰", - maxMember = max, - member = mem, - likes = 10 + index * 2, - views = 150 + index * 20, - studyImage = ImageRef.Name("ic_study_default") // 필요시 Url로 교체 + id = index.toLong(), + name = title, + description = "주 2회 진행, 코드리뷰", + maxMembers = max, + currentMembers = mem, + likeCount = 10 + index * 2, + isLiked = index%2 == 0, + hitCount = 150 + index * 20, + profileImageUrl = ImageRef.Name("ic_study_default") ) } } diff --git a/domain/study/src/main/java/com/umcspot/spot/study/repository/StudyRepository.kt b/domain/study/src/main/java/com/umcspot/spot/study/repository/StudyRepository.kt index 4811c49b..5f1c7ec2 100644 --- a/domain/study/src/main/java/com/umcspot/spot/study/repository/StudyRepository.kt +++ b/domain/study/src/main/java/com/umcspot/spot/study/repository/StudyRepository.kt @@ -12,12 +12,14 @@ interface StudyRepository { suspend fun getPopularStudies(): Result suspend fun getRecommendStudies(): Result suspend fun getRecruitingStudies( - sortType: RecruitingStudySort, - activityType: ActivityType?, - theme: StudyTheme?, - feeRange: FeeRange? + feeCategory: FeeRange? = null, + categories: List? = null, + isOnline: Boolean? = null, + sortBy: RecruitingStudySort? = null, + cursor: Long? = null, size: Int ): Result + suspend fun getPreferLocationStudies( sortType: RecruitingStudySort, activityType: ActivityType?, diff --git a/feature/main/src/main/java/com/umcspot/spot/main/MainNavHost.kt b/feature/main/src/main/java/com/umcspot/spot/main/MainNavHost.kt index 8d536f9c..ef85ba4f 100644 --- a/feature/main/src/main/java/com/umcspot/spot/main/MainNavHost.kt +++ b/feature/main/src/main/java/com/umcspot/spot/main/MainNavHost.kt @@ -26,7 +26,6 @@ import com.umcspot.spot.feature.board.post.posting.navigation.postingGraph import com.umcspot.spot.signup.navigation.signupGraph import com.umcspot.spot.study.detail.navigation.studyDetailGraph import com.umcspot.spot.study.my.navigation.myStudyGraph -import com.umcspot.spot.study.preferLocation.navigation.preferLocationStudyGraph import com.umcspot.spot.study.recruiting.navigation.recruitingStudyFilterGraph import com.umcspot.spot.study.recruiting.navigation.recruitingStudyGraph import com.umcspot.spot.study.register.navigation.RegisterStudy @@ -68,7 +67,7 @@ fun MainNavHost( onQuickMenuClick = { type -> when (type) { QuickMenuType.BOARD -> navigator.navigateToBoard() - QuickMenuType.REGION -> navigator.navigateToPreferLocationStudy() + QuickMenuType.REGION -> /*navigator.navigateToPreferLocationStudy()*/{} QuickMenuType.INTERESTS -> { /* TODO */ } @@ -90,15 +89,16 @@ fun MainNavHost( recruitingStudyFilterGraph( contentPadding = contentPadding, + navController = navigator.navController, onAcceptFilterClick = { navigator.popBackStack() } ) - preferLocationStudyGraph( - contentPadding = contentPadding, - onRegisterScrollToTop = onRegisterScrollToTop, - onItemClick = { }, - onFilterClick = { }, - ) +// preferLocationStudyGraph( +// contentPadding = contentPadding, +// onRegisterScrollToTop = onRegisterScrollToTop, +// onItemClick = { }, +// onFilterClick = { }, +// ) boardGraph( contentPadding = contentPadding, diff --git a/feature/main/src/main/java/com/umcspot/spot/main/MainNavigator.kt b/feature/main/src/main/java/com/umcspot/spot/main/MainNavigator.kt index 48a05f6e..2f533fbe 100644 --- a/feature/main/src/main/java/com/umcspot/spot/main/MainNavigator.kt +++ b/feature/main/src/main/java/com/umcspot/spot/main/MainNavigator.kt @@ -33,8 +33,6 @@ import com.umcspot.spot.signup.navigation.navigateToSignUp import com.umcspot.spot.study.detail.navigation.StudyDetail import com.umcspot.spot.study.detail.navigation.navigateToStudyDetail import com.umcspot.spot.study.my.navigation.navigateToMyStudy -import com.umcspot.spot.study.preferLocation.navigation.PreferLocation -import com.umcspot.spot.study.preferLocation.navigation.navigateToPreferLocationStudy import com.umcspot.spot.study.recruiting.navigation.Recruiting import com.umcspot.spot.study.recruiting.navigation.RecruitingFilter import com.umcspot.spot.study.recruiting.navigation.navigateToRecruitingStudy @@ -98,7 +96,7 @@ class MainNavigator( SignUp::class, CheckList::class, Posting::class, BoardList::class) || inAnyGraphRoutes(POST_CONTENT_ROUTE) @Composable - fun showToTopFab(): Boolean = inAnyGraph(Alert::class, AppliedAlert::class, Recruiting::class,PreferLocation::class, BoardList::class) + fun showToTopFab(): Boolean = inAnyGraph(Alert::class, AppliedAlert::class, Recruiting::class,/*PreferLocation::class,*/ BoardList::class) @Composable fun showMultipleFab(): Boolean = inAnyGraph(BoardList::class) @@ -155,9 +153,9 @@ class MainNavigator( navController.navigateToBoard(navOptions) } - fun navigateToPreferLocationStudy(navOptions: NavOptions? = null) { - navController.navigateToPreferLocationStudy(navOptions) - } +// fun navigateToPreferLocationStudy(navOptions: NavOptions? = null) { +// navController.navigateToPreferLocationStudy(navOptions) +// } fun navigateToRecruitingStudy(navOptions: NavOptions? = null) { navController.navigateToRecruitingStudy(navOptions) diff --git a/feature/study/src/main/java/com/umcspot/spot/study/preferLocation/PreferLocationStudyScreen.kt b/feature/study/src/main/java/com/umcspot/spot/study/preferLocation/PreferLocationStudyScreen.kt index 22c49002..d792dde1 100644 --- a/feature/study/src/main/java/com/umcspot/spot/study/preferLocation/PreferLocationStudyScreen.kt +++ b/feature/study/src/main/java/com/umcspot/spot/study/preferLocation/PreferLocationStudyScreen.kt @@ -1,316 +1,316 @@ -package com.umcspot.spot.study.preferLocation - -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.LazyListState -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.OutlinedButton -import androidx.compose.material3.ScrollableTabRow -import androidx.compose.material3.Tab -import androidx.compose.material3.TabRowDefaults -import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset -import androidx.compose.material3.Text -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.drawWithContent -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.geometry.Size -import androidx.compose.ui.graphics.Brush -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import androidx.hilt.navigation.compose.hiltViewModel -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.umcspot.spot.designsystem.R -import com.umcspot.spot.designsystem.component.bottomsheet.LocationBottomSheet -import com.umcspot.spot.designsystem.component.empty.EmptyAlertWithButton -import com.umcspot.spot.designsystem.component.study.StudyListItem -import com.umcspot.spot.designsystem.shapes.SpotShapes -import com.umcspot.spot.designsystem.theme.B500 -import com.umcspot.spot.designsystem.theme.SpotTheme -import com.umcspot.spot.model.RecruitingStudySort -import com.umcspot.spot.study.model.StudyResult -import com.umcspot.spot.ui.state.UiState -import kotlinx.coroutines.launch - -@Composable -fun PreferLocationStudyScreen( - contentPadding: PaddingValues, - viewmodel: PreferLocationStudyViewModel = hiltViewModel(), - onRegisterScrollToTop: ((() -> Unit)?) -> Unit, - onFilterClick: () -> Unit, - onItemClick: (StudyResult) -> Unit -) { - val ui by viewmodel.uiState.collectAsStateWithLifecycle() - val sort by viewmodel.sortType.collectAsStateWithLifecycle() - val query by viewmodel.query.collectAsStateWithLifecycle() - val results by viewmodel.results.collectAsStateWithLifecycle() - val selected by viewmodel.selected.collectAsStateWithLifecycle() - - var showSheet by remember { mutableStateOf(false) } - var selectedTab by remember { mutableStateOf(0) } // 0 = 전체 - val tabs: List = remember(selected) { listOf("전체") + selected } - - val listState = rememberLazyListState() - val scope = rememberCoroutineScope() - - val topPad = contentPadding.calculateTopPadding() - val bottomPad = contentPadding.calculateBottomPadding() - - // 최초 데이터 로드 - LaunchedEffect(ui.studies) { - if (ui.studies is UiState.Empty) viewmodel.load(RecruitingStudySort.LATEST) - } - LaunchedEffect(Unit) { viewmodel.loadLocationData() } - - // 상단으로 스크롤 요청 핸들링 - LaunchedEffect(Unit) { - onRegisterScrollToTop { - scope.launch { listState.animateScrollToItem(0) } - } - } - - // 선택 칩 변화 시 탭 인덱스 보정 - LaunchedEffect(selected.size) { - val maxIdx = (1 + selected.size) - 1 // "전체" 1개 + selected 크기 - 1 - if (selectedTab > maxIdx) selectedTab = maxIdx - } - - // 리스트 데이터 준비 (+ 선택된 탭 기준 필터 적용 지점) - val studiesAll = when (val s = ui.studies) { - is UiState.Success -> s.data.studyList - else -> emptyList() - } - val currentLocation = selected.getOrNull(selectedTab) - // 🔧 여기서 실제 StudyResult의 지역 필드명으로 필터하세요 (예: item.location / item.address 등) - val studiesForUi = if (currentLocation.isNullOrBlank()) { - studiesAll - } else { - studiesAll.filter { item -> - // 예시) item.location?.contains(currentLocation) == true - // 필드명이 다르면 위 라인만 고치면 됨 - false - } - } - - Column( - modifier = Modifier - .fillMaxSize() - .background(SpotTheme.colors.white) - .padding(top = topPad, bottom = bottomPad, start = 16.dp, end = 16.dp) - ) { - // 타이틀 - Text( - text = "내 지역 스터디", - style = SpotTheme.typography.small_400.copy(fontSize = 20.sp) - ) - - Spacer(Modifier.height(8.dp)) - - // 선택 지역 탭 - SelectedLocationTabs( - tabs = tabs, - selectedIndex = selectedTab, - onTabSelected = { selectedTab = it } - ) - - HeaderRow( - size = studiesForUi.size, - sortType = sort, - onOpenSortSheet = { showSheet = true }, - onFilterClick = onFilterClick - ) - - if (ui.studies is UiState.Loading) { - Text("로딩 중...", color = Color.Gray, modifier = Modifier.padding(top = 8.dp)) - } else if (ui.studies is UiState.Failure) { - Text("에러: ${(ui.studies as UiState.Failure).msg}", color = Color.Red, modifier = Modifier.padding(top = 8.dp)) - } else if (studiesForUi.isEmpty()) { - Box(Modifier.fillMaxSize()) { - EmptyAlertWithButton( - modifier = Modifier.fillMaxSize(), - painter = painterResource(R.drawable.location_outline), - alertTitle = "내 지역이 아직 없어요!", - alertDes = "내 지역을 설정하고 스터디를 모아봐요.", - buttonText = "내 지역 설정하기", - onClick = { showSheet = true } - ) - } - } else { - StudyList( - listState = listState, - items = studiesForUi, - onItemClick = onItemClick - ) - } - } - - // 지역 선택 바텀시트 - LocationBottomSheet( - visible = showSheet, - query = query, - onQueryChange = { viewmodel.searchLocation(it) }, - onDismiss = { showSheet = false }, - results = results, - selected = selected, - onAddSelected = viewmodel::add, - onRemoveSelected = viewmodel::remove - ) -} - -@Composable -private fun StudyList( - listState: LazyListState, - items: List, - onItemClick: (StudyResult) -> Unit -) { - LazyColumn( - state = listState, - modifier = Modifier.fillMaxSize() - ) { - items( - items = items, - key = { it.studyId } - ) { item -> - StudyListItem( - item = item, - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 5.dp), - onClick = { onItemClick(item) } - ) - } - } -} - -@Composable -private fun HeaderRow( - size: Int, - sortType: RecruitingStudySort, - onOpenSortSheet: () -> Unit, - onFilterClick: () -> Unit -) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Text( - text = "%02d건".format(size), - style = SpotTheme.typography.medium_500.copy(fontSize = 12.sp), - color = SpotTheme.colors.gray500 - ) - - Row(verticalAlignment = Alignment.CenterVertically) { - OutlinedButton( - onClick = onOpenSortSheet, - shape = SpotShapes.Soft, - contentPadding = PaddingValues(horizontal = 10.dp, vertical = 6.dp), - modifier = Modifier.height(32.dp) - ) { - Text( - text = sortType.label, - color = SpotTheme.colors.black, - style = SpotTheme.typography.medium_500.copy(fontSize = 12.sp) - ) - Spacer(Modifier.width(5.dp)) - Icon( - painter = painterResource(R.drawable.arrow_down), - tint = SpotTheme.colors.B500, - contentDescription = null - ) - } - - IconButton(onClick = onFilterClick) { - Icon( - painter = painterResource(R.drawable.filter), - contentDescription = "필터", - modifier = Modifier.size(22.dp) - ) - } - } - } -} - -@Composable -private fun SelectedLocationTabs( - tabs: List, - selectedIndex: Int, - onTabSelected: (Int) -> Unit -) { - if (tabs.isEmpty()) return - - val horizPad = 12.dp - val scrimWidth = 24.dp - val bg = SpotTheme.colors.white // 배경색(화면 배경과 맞추기) - - Box( - modifier = Modifier - .fillMaxWidth() - // 스크롤 가능 영역의 좌우에 페이드 오버레이를 그리되, 입력은 통과시킴 - .drawWithContent { - drawContent() - - val w = scrimWidth.toPx() - drawRect( - brush = Brush.horizontalGradient( - colors = listOf(Color.Transparent, bg), - // 내부가 투명, 바깥이 흰색이 되도록 방향 지정 - startX = w, // 투명 쪽(내부) - endX = 0f // 흰색 쪽(바깥) - ), - size = Size(w, size.height), - topLeft = Offset(0f, 0f) - ) - // 오른쪽: 내부(투명) → 바깥(흰색) - drawRect( - brush = Brush.horizontalGradient( - colors = listOf(Color.Transparent, bg), - startX = size.width - w, // 투명(내부) - endX = size.width // 흰색(바깥) - ), - size = Size(w, size.height), - topLeft = Offset(size.width - w, 0f) - ) - } - ) { - ScrollableTabRow( - selectedTabIndex = selectedIndex, - edgePadding = 0.dp, - containerColor = Color.Transparent, - divider = {}, - indicator = { tabPositions -> - TabRowDefaults.Indicator( - modifier = Modifier - .tabIndicatorOffset(tabPositions[selectedIndex]) - .padding(horizontal = horizPad) - .height(2.dp), - color = SpotTheme.colors.B500 - ) - } - ) { - tabs.forEachIndexed { index, name -> - Tab( - selected = selectedIndex == index, - onClick = { onTabSelected(index) }, - selectedContentColor = SpotTheme.colors.black, - unselectedContentColor = SpotTheme.colors.black - ) { - Text( - text = name, - style = SpotTheme.typography.medium_500.copy(fontSize = 14.sp), - modifier = Modifier.padding(horizontal = horizPad, vertical = 10.dp) - ) - } - } - } - } -} - - +//package com.umcspot.spot.study.preferLocation +// +//import androidx.compose.foundation.background +//import androidx.compose.foundation.layout.* +//import androidx.compose.foundation.lazy.LazyColumn +//import androidx.compose.foundation.lazy.LazyListState +//import androidx.compose.foundation.lazy.items +//import androidx.compose.foundation.lazy.rememberLazyListState +//import androidx.compose.material3.Icon +//import androidx.compose.material3.IconButton +//import androidx.compose.material3.OutlinedButton +//import androidx.compose.material3.ScrollableTabRow +//import androidx.compose.material3.Tab +//import androidx.compose.material3.TabRowDefaults +//import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset +//import androidx.compose.material3.Text +//import androidx.compose.runtime.* +//import androidx.compose.ui.Alignment +//import androidx.compose.ui.Modifier +//import androidx.compose.ui.draw.drawWithContent +//import androidx.compose.ui.geometry.Offset +//import androidx.compose.ui.geometry.Size +//import androidx.compose.ui.graphics.Brush +//import androidx.compose.ui.graphics.Color +//import androidx.compose.ui.res.painterResource +//import androidx.compose.ui.unit.dp +//import androidx.compose.ui.unit.sp +//import androidx.hilt.navigation.compose.hiltViewModel +//import androidx.lifecycle.compose.collectAsStateWithLifecycle +//import com.umcspot.spot.designsystem.R +//import com.umcspot.spot.designsystem.component.bottomsheet.LocationBottomSheet +//import com.umcspot.spot.designsystem.component.empty.EmptyAlertWithButton +//import com.umcspot.spot.designsystem.component.study.StudyListItem +//import com.umcspot.spot.designsystem.shapes.SpotShapes +//import com.umcspot.spot.designsystem.theme.B500 +//import com.umcspot.spot.designsystem.theme.SpotTheme +//import com.umcspot.spot.model.RecruitingStudySort +//import com.umcspot.spot.study.model.StudyResult +//import com.umcspot.spot.ui.state.UiState +//import kotlinx.coroutines.launch +// +//@Composable +//fun PreferLocationStudyScreen( +// contentPadding: PaddingValues, +// viewmodel: PreferLocationStudyViewModel = hiltViewModel(), +// onRegisterScrollToTop: ((() -> Unit)?) -> Unit, +// onFilterClick: () -> Unit, +// onItemClick: (StudyResult) -> Unit +//) { +// val ui by viewmodel.uiState.collectAsStateWithLifecycle() +// val sort by viewmodel.sortType.collectAsStateWithLifecycle() +// val query by viewmodel.query.collectAsStateWithLifecycle() +// val results by viewmodel.results.collectAsStateWithLifecycle() +// val selected by viewmodel.selected.collectAsStateWithLifecycle() +// +// var showSheet by remember { mutableStateOf(false) } +// var selectedTab by remember { mutableStateOf(0) } // 0 = 전체 +// val tabs: List = remember(selected) { listOf("전체") + selected } +// +// val listState = rememberLazyListState() +// val scope = rememberCoroutineScope() +// +// val topPad = contentPadding.calculateTopPadding() +// val bottomPad = contentPadding.calculateBottomPadding() +// +// // 최초 데이터 로드 +// LaunchedEffect(ui.studies) { +// if (ui.studies is UiState.Empty) viewmodel.load(RecruitingStudySort.RECENT) +// } +// LaunchedEffect(Unit) { viewmodel.loadLocationData() } +// +// // 상단으로 스크롤 요청 핸들링 +// LaunchedEffect(Unit) { +// onRegisterScrollToTop { +// scope.launch { listState.animateScrollToItem(0) } +// } +// } +// +// // 선택 칩 변화 시 탭 인덱스 보정 +// LaunchedEffect(selected.size) { +// val maxIdx = (1 + selected.size) - 1 // "전체" 1개 + selected 크기 - 1 +// if (selectedTab > maxIdx) selectedTab = maxIdx +// } +// +// // 리스트 데이터 준비 (+ 선택된 탭 기준 필터 적용 지점) +// val studiesAll = when (val s = ui.studies) { +// is UiState.Success -> s.data.studyList +// else -> emptyList() +// } +// val currentLocation = selected.getOrNull(selectedTab) +// // 🔧 여기서 실제 StudyResult의 지역 필드명으로 필터하세요 (예: item.location / item.address 등) +// val studiesForUi = if (currentLocation.isNullOrBlank()) { +// studiesAll +// } else { +// studiesAll.filter { item -> +// // 예시) item.location?.contains(currentLocation) == true +// // 필드명이 다르면 위 라인만 고치면 됨 +// false +// } +// } +// +// Column( +// modifier = Modifier +// .fillMaxSize() +// .background(SpotTheme.colors.white) +// .padding(top = topPad, bottom = bottomPad, start = 16.dp, end = 16.dp) +// ) { +// // 타이틀 +// Text( +// text = "내 지역 스터디", +// style = SpotTheme.typography.small_400.copy(fontSize = 20.sp) +// ) +// +// Spacer(Modifier.height(8.dp)) +// +// // 선택 지역 탭 +// SelectedLocationTabs( +// tabs = tabs, +// selectedIndex = selectedTab, +// onTabSelected = { selectedTab = it } +// ) +// +// HeaderRow( +// size = studiesForUi.size, +// sortType = sort, +// onOpenSortSheet = { showSheet = true }, +// onFilterClick = onFilterClick +// ) +// +// if (ui.studies is UiState.Loading) { +// Text("로딩 중...", color = Color.Gray, modifier = Modifier.padding(top = 8.dp)) +// } else if (ui.studies is UiState.Failure) { +// Text("에러: ${(ui.studies as UiState.Failure).msg}", color = Color.Red, modifier = Modifier.padding(top = 8.dp)) +// } else if (studiesForUi.isEmpty()) { +// Box(Modifier.fillMaxSize()) { +// EmptyAlertWithButton( +// modifier = Modifier.fillMaxSize(), +// painter = painterResource(R.drawable.location_outline), +// alertTitle = "내 지역이 아직 없어요!", +// alertDes = "내 지역을 설정하고 스터디를 모아봐요.", +// buttonText = "내 지역 설정하기", +// onClick = { showSheet = true } +// ) +// } +// } else { +// StudyList( +// listState = listState, +// items = studiesForUi, +// onItemClick = onItemClick +// ) +// } +// } +// +// // 지역 선택 바텀시트 +// LocationBottomSheet( +// visible = showSheet, +// query = query, +// onQueryChange = { viewmodel.searchLocation(it) }, +// onDismiss = { showSheet = false }, +// results = results, +// selected = selected, +// onAddSelected = viewmodel::add, +// onRemoveSelected = viewmodel::remove +// ) +//} +// +//@Composable +//private fun StudyList( +// listState: LazyListState, +// items: List, +// onItemClick: (StudyResult) -> Unit +//) { +// LazyColumn( +// state = listState, +// modifier = Modifier.fillMaxSize() +// ) { +// items( +// items = items, +// key = { it.id } +// ) { item -> +// StudyListItem( +// item = item, +// modifier = Modifier +// .fillMaxWidth() +// .padding(vertical = 5.dp), +// onClick = { onItemClick(item) } +// ) +// } +// } +//} +// +//@Composable +//private fun HeaderRow( +// size: Int, +// sortType: RecruitingStudySort, +// onOpenSortSheet: () -> Unit, +// onFilterClick: () -> Unit +//) { +// Row( +// modifier = Modifier.fillMaxWidth(), +// horizontalArrangement = Arrangement.SpaceBetween, +// verticalAlignment = Alignment.CenterVertically +// ) { +// Text( +// text = "%02d건".format(size), +// style = SpotTheme.typography.medium_500.copy(fontSize = 12.sp), +// color = SpotTheme.colors.gray500 +// ) +// +// Row(verticalAlignment = Alignment.CenterVertically) { +// OutlinedButton( +// onClick = onOpenSortSheet, +// shape = SpotShapes.Soft, +// contentPadding = PaddingValues(horizontal = 10.dp, vertical = 6.dp), +// modifier = Modifier.height(32.dp) +// ) { +// Text( +// text = sortType.label, +// color = SpotTheme.colors.black, +// style = SpotTheme.typography.medium_500.copy(fontSize = 12.sp) +// ) +// Spacer(Modifier.width(5.dp)) +// Icon( +// painter = painterResource(R.drawable.arrow_down), +// tint = SpotTheme.colors.B500, +// contentDescription = null +// ) +// } +// +// IconButton(onClick = onFilterClick) { +// Icon( +// painter = painterResource(R.drawable.filter), +// contentDescription = "필터", +// modifier = Modifier.size(22.dp) +// ) +// } +// } +// } +//} +// +//@Composable +//private fun SelectedLocationTabs( +// tabs: List, +// selectedIndex: Int, +// onTabSelected: (Int) -> Unit +//) { +// if (tabs.isEmpty()) return +// +// val horizPad = 12.dp +// val scrimWidth = 24.dp +// val bg = SpotTheme.colors.white // 배경색(화면 배경과 맞추기) +// +// Box( +// modifier = Modifier +// .fillMaxWidth() +// // 스크롤 가능 영역의 좌우에 페이드 오버레이를 그리되, 입력은 통과시킴 +// .drawWithContent { +// drawContent() +// +// val w = scrimWidth.toPx() +// drawRect( +// brush = Brush.horizontalGradient( +// colors = listOf(Color.Transparent, bg), +// // 내부가 투명, 바깥이 흰색이 되도록 방향 지정 +// startX = w, // 투명 쪽(내부) +// endX = 0f // 흰색 쪽(바깥) +// ), +// size = Size(w, size.height), +// topLeft = Offset(0f, 0f) +// ) +// // 오른쪽: 내부(투명) → 바깥(흰색) +// drawRect( +// brush = Brush.horizontalGradient( +// colors = listOf(Color.Transparent, bg), +// startX = size.width - w, // 투명(내부) +// endX = size.width // 흰색(바깥) +// ), +// size = Size(w, size.height), +// topLeft = Offset(size.width - w, 0f) +// ) +// } +// ) { +// ScrollableTabRow( +// selectedTabIndex = selectedIndex, +// edgePadding = 0.dp, +// containerColor = Color.Transparent, +// divider = {}, +// indicator = { tabPositions -> +// TabRowDefaults.Indicator( +// modifier = Modifier +// .tabIndicatorOffset(tabPositions[selectedIndex]) +// .padding(horizontal = horizPad) +// .height(2.dp), +// color = SpotTheme.colors.B500 +// ) +// } +// ) { +// tabs.forEachIndexed { index, name -> +// Tab( +// selected = selectedIndex == index, +// onClick = { onTabSelected(index) }, +// selectedContentColor = SpotTheme.colors.black, +// unselectedContentColor = SpotTheme.colors.black +// ) { +// Text( +// text = name, +// style = SpotTheme.typography.medium_500.copy(fontSize = 14.sp), +// modifier = Modifier.padding(horizontal = horizPad, vertical = 10.dp) +// ) +// } +// } +// } +// } +//} +// +// diff --git a/feature/study/src/main/java/com/umcspot/spot/study/preferLocation/PreferLocationStudyViewModel.kt b/feature/study/src/main/java/com/umcspot/spot/study/preferLocation/PreferLocationStudyViewModel.kt index 77648abf..9f2b26bf 100644 --- a/feature/study/src/main/java/com/umcspot/spot/study/preferLocation/PreferLocationStudyViewModel.kt +++ b/feature/study/src/main/java/com/umcspot/spot/study/preferLocation/PreferLocationStudyViewModel.kt @@ -1,154 +1,154 @@ -package com.umcspot.spot.study.preferLocation - -import android.content.Context -import android.util.Log -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.umcspot.spot.common.location.LocationRow -import com.umcspot.spot.common.location.LocationStore -import com.umcspot.spot.common.location.searchLocations -import com.umcspot.spot.model.ActivityType -import com.umcspot.spot.model.FeeRange -import com.umcspot.spot.model.RecruitingStudySort -import com.umcspot.spot.model.StudyTheme -import com.umcspot.spot.study.model.StudyResultList -import com.umcspot.spot.study.repository.StudyRepository -import com.umcspot.spot.ui.state.UiState -import dagger.hilt.android.lifecycle.HiltViewModel -import dagger.hilt.android.qualifiers.ApplicationContext -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.debounce -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.onStart -import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch -import javax.inject.Inject - -@HiltViewModel -class PreferLocationStudyViewModel @Inject constructor( - private val studyRepository: StudyRepository, - @ApplicationContext private val appContext: Context -) : ViewModel() { - - data class PreferLocationStudyUiState( - val studies: UiState = UiState.Empty - ) - - private val _uiState = MutableStateFlow(PreferLocationStudyUiState()) - val uiState: StateFlow = _uiState.asStateFlow() - - private val _sortType = MutableStateFlow(RecruitingStudySort.LATEST) - val sortType: StateFlow = _sortType.asStateFlow() - - private val _activity = MutableStateFlow(null) - private val _fee = MutableStateFlow(null) - private val _theme = MutableStateFlow(null) - - /** ---------------- 행정구역 관련 ---------------- */ - private var allLocations: List = emptyList() - - private val _query = MutableStateFlow("") - val query = _query.asStateFlow() - - private val _results = MutableStateFlow>(emptyList()) - val results = _results.asStateFlow() - - private val _selected = MutableStateFlow>(emptyList()) - val selected = _selected.asStateFlow() - - - /** ---------------- 초기 네트워크 fetch ---------------- */ - init { - combine(_sortType, _activity, _fee, _theme) { s, a, f, t -> - Params(s, a, f, t) - } - .distinctUntilChanged() - .debounce(200) - .onStart { emit(Params(_sortType.value, _activity.value, _fee.value, _theme.value)) } - .collectLatestIn(viewModelScope) { params -> - fetch(params) - } - } - - private suspend fun fetch(p: Params) { - _uiState.update { it.copy(studies = UiState.Loading) } - val newState: UiState = try { - val res = studyRepository.getPreferLocationStudies( - sortType = p.sort, - activityType = p.activity, - feeRange = p.fee, - theme = p.theme - ) - res.fold( - onSuccess = { data -> UiState.Success(data) }, - onFailure = { e -> UiState.Failure(e.message ?: e.toString()) } - ) - } catch (e: Exception) { - UiState.Failure(e.message ?: e.toString()) - } - _uiState.update { it.copy(studies = newState) } - } - - /** ---------------- 공개 API ---------------- */ - fun load(selected: RecruitingStudySort = _sortType.value) { _sortType.value = selected } - fun selectSort(type: RecruitingStudySort) { _sortType.value = type } - fun setActivityFilter(type: ActivityType?) { _activity.value = type } - fun setFeeFilter(fee: FeeRange?) { _fee.value = fee } - fun setThemeFilter(theme: StudyTheme?) { _theme.value = theme } - fun clearFilters() { - _activity.value = null - _fee.value = null - _theme.value = null - } - - fun add(name: String) = _selected.update { if (name in it || it.size>=10) it else it + name } - fun remove(name: String) = _selected.update { it - name } - fun clear() = _selected.update { emptyList() } - - /** ---------------- 행정구역 검색용 메서드 ---------------- */ - fun loadLocationData() { - viewModelScope.launch(Dispatchers.IO) { - allLocations = LocationStore.load(appContext)} - } - - fun searchLocation(query: String) { - _query.value = query - viewModelScope.launch(Dispatchers.IO) { - if (query.isBlank()) { - _results.value = emptyList() - return@launch - } - - if (allLocations.isEmpty()) { - allLocations = LocationStore.load(appContext) - } - - val filtered = searchLocations(query, allLocations) - - _results.value = filtered - } - } - - - private data class Params( - val sort: RecruitingStudySort, - val activity: ActivityType?, - val fee: FeeRange?, - val theme: StudyTheme? - ) -} - -/** 작은 헬퍼: Flow collectLatest 축약 */ -private inline fun Flow.collectLatestIn( - scope: CoroutineScope, - crossinline block: suspend (T) -> Unit -) = scope.launch { - collectLatest { block(it) } -} +//package com.umcspot.spot.study.preferLocation +// +//import android.content.Context +//import android.util.Log +//import androidx.lifecycle.ViewModel +//import androidx.lifecycle.viewModelScope +//import com.umcspot.spot.common.location.LocationRow +//import com.umcspot.spot.common.location.LocationStore +//import com.umcspot.spot.common.location.searchLocations +//import com.umcspot.spot.model.ActivityType +//import com.umcspot.spot.model.FeeRange +//import com.umcspot.spot.model.RecruitingStudySort +//import com.umcspot.spot.model.StudyTheme +//import com.umcspot.spot.study.model.StudyResultList +//import com.umcspot.spot.study.repository.StudyRepository +//import com.umcspot.spot.ui.state.UiState +//import dagger.hilt.android.lifecycle.HiltViewModel +//import dagger.hilt.android.qualifiers.ApplicationContext +//import kotlinx.coroutines.CoroutineScope +//import kotlinx.coroutines.Dispatchers +//import kotlinx.coroutines.flow.Flow +//import kotlinx.coroutines.flow.MutableStateFlow +//import kotlinx.coroutines.flow.StateFlow +//import kotlinx.coroutines.flow.asStateFlow +//import kotlinx.coroutines.flow.collectLatest +//import kotlinx.coroutines.flow.combine +//import kotlinx.coroutines.flow.debounce +//import kotlinx.coroutines.flow.distinctUntilChanged +//import kotlinx.coroutines.flow.onStart +//import kotlinx.coroutines.flow.update +//import kotlinx.coroutines.launch +//import javax.inject.Inject +// +//@HiltViewModel +//class PreferLocationStudyViewModel @Inject constructor( +// private val studyRepository: StudyRepository, +// @ApplicationContext private val appContext: Context +//) : ViewModel() { +// +// data class PreferLocationStudyUiState( +// val studies: UiState = UiState.Empty +// ) +// +// private val _uiState = MutableStateFlow(PreferLocationStudyUiState()) +// val uiState: StateFlow = _uiState.asStateFlow() +// +// private val _sortType = MutableStateFlow(RecruitingStudySort.RECENT) +// val sortType: StateFlow = _sortType.asStateFlow() +// +// private val _activity = MutableStateFlow(null) +// private val _fee = MutableStateFlow(null) +// private val _theme = MutableStateFlow(null) +// +// /** ---------------- 행정구역 관련 ---------------- */ +// private var allLocations: List = emptyList() +// +// private val _query = MutableStateFlow("") +// val query = _query.asStateFlow() +// +// private val _results = MutableStateFlow>(emptyList()) +// val results = _results.asStateFlow() +// +// private val _selected = MutableStateFlow>(emptyList()) +// val selected = _selected.asStateFlow() +// +// +// /** ---------------- 초기 네트워크 fetch ---------------- */ +// init { +// combine(_sortType, _activity, _fee, _theme) { s, a, f, t -> +// Params(s, a, f, t) +// } +// .distinctUntilChanged() +// .debounce(200) +// .onStart { emit(Params(_sortType.value, _activity.value, _fee.value, _theme.value)) } +// .collectLatestIn(viewModelScope) { params -> +// fetch(params) +// } +// } +// +// private suspend fun fetch(p: Params) { +// _uiState.update { it.copy(studies = UiState.Loading) } +// val newState: UiState = try { +// val res = studyRepository.getRecruitingStudies( +// sortType = p.sort, +// activityType = p.activity, +// feeRange = p.fee, +// theme = p.theme +// ) +// res.fold( +// onSuccess = { data -> UiState.Success(data) }, +// onFailure = { e -> UiState.Failure(e.message ?: e.toString()) } +// ) +// } catch (e: Exception) { +// UiState.Failure(e.message ?: e.toString()) +// } +// _uiState.update { it.copy(studies = newState) } +// } +// +// /** ---------------- 공개 API ---------------- */ +// fun load(selected: RecruitingStudySort = _sortType.value) { _sortType.value = selected } +// fun selectSort(type: RecruitingStudySort) { _sortType.value = type } +// fun setActivityFilter(type: ActivityType?) { _activity.value = type } +// fun setFeeFilter(fee: FeeRange?) { _fee.value = fee } +// fun setThemeFilter(theme: StudyTheme?) { _theme.value = theme } +// fun clearFilters() { +// _activity.value = null +// _fee.value = null +// _theme.value = null +// } +// +// fun add(name: String) = _selected.update { if (name in it || it.size>=10) it else it + name } +// fun remove(name: String) = _selected.update { it - name } +// fun clear() = _selected.update { emptyList() } +// +// /** ---------------- 행정구역 검색용 메서드 ---------------- */ +// fun loadLocationData() { +// viewModelScope.launch(Dispatchers.IO) { +// allLocations = LocationStore.load(appContext)} +// } +// +// fun searchLocation(query: String) { +// _query.value = query +// viewModelScope.launch(Dispatchers.IO) { +// if (query.isBlank()) { +// _results.value = emptyList() +// return@launch +// } +// +// if (allLocations.isEmpty()) { +// allLocations = LocationStore.load(appContext) +// } +// +// val filtered = searchLocations(query, allLocations) +// +// _results.value = filtered +// } +// } +// +// +// private data class Params( +// val sort: RecruitingStudySort, +// val activity: ActivityType?, +// val fee: FeeRange?, +// val theme: StudyTheme? +// ) +//} +// +///** 작은 헬퍼: Flow collectLatest 축약 */ +//private inline fun Flow.collectLatestIn( +// scope: CoroutineScope, +// crossinline block: suspend (T) -> Unit +//) = scope.launch { +// collectLatest { block(it) } +//} diff --git a/feature/study/src/main/java/com/umcspot/spot/study/preferLocation/navigation/PreferLocationStudyNavigation.kt b/feature/study/src/main/java/com/umcspot/spot/study/preferLocation/navigation/PreferLocationStudyNavigation.kt index 1b3522c5..4e6d64f4 100644 --- a/feature/study/src/main/java/com/umcspot/spot/study/preferLocation/navigation/PreferLocationStudyNavigation.kt +++ b/feature/study/src/main/java/com/umcspot/spot/study/preferLocation/navigation/PreferLocationStudyNavigation.kt @@ -1,34 +1,34 @@ -package com.umcspot.spot.study.preferLocation.navigation - -import androidx.compose.foundation.layout.PaddingValues -import androidx.navigation.NavController -import androidx.navigation.NavGraphBuilder -import androidx.navigation.NavOptions -import androidx.navigation.compose.composable -import com.umcspot.spot.navigation.Route -import com.umcspot.spot.study.model.StudyResult -import com.umcspot.spot.study.preferLocation.PreferLocationStudyScreen -import kotlinx.serialization.Serializable - -fun NavController.navigateToPreferLocationStudy(navOptions: NavOptions? = null) { - navigate(PreferLocation, navOptions) -} - -fun NavGraphBuilder.preferLocationStudyGraph( - contentPadding : PaddingValues, - onRegisterScrollToTop: ((() -> Unit)?) -> Unit, - onFilterClick : () -> Unit, - onItemClick : (StudyResult) -> Unit -) { - composable { - PreferLocationStudyScreen( - contentPadding = contentPadding, - onRegisterScrollToTop = onRegisterScrollToTop, - onFilterClick = onFilterClick, - onItemClick = onItemClick - ) - } -} - -@Serializable -data object PreferLocation : Route \ No newline at end of file +//package com.umcspot.spot.study.preferLocation.navigation +// +//import androidx.compose.foundation.layout.PaddingValues +//import androidx.navigation.NavController +//import androidx.navigation.NavGraphBuilder +//import androidx.navigation.NavOptions +//import androidx.navigation.compose.composable +//import com.umcspot.spot.navigation.Route +//import com.umcspot.spot.study.model.StudyResult +//import com.umcspot.spot.study.preferLocation.PreferLocationStudyScreen +//import kotlinx.serialization.Serializable +// +//fun NavController.navigateToPreferLocationStudy(navOptions: NavOptions? = null) { +// navigate(PreferLocation, navOptions) +//} +// +//fun NavGraphBuilder.preferLocationStudyGraph( +// contentPadding : PaddingValues, +// onRegisterScrollToTop: ((() -> Unit)?) -> Unit, +// onFilterClick : () -> Unit, +// onItemClick : (StudyResult) -> Unit +//) { +// composable { +// PreferLocationStudyScreen( +// contentPadding = contentPadding, +// onRegisterScrollToTop = onRegisterScrollToTop, +// onFilterClick = onFilterClick, +// onItemClick = onItemClick +// ) +// } +//} +// +//@Serializable +//data object PreferLocation : Route \ No newline at end of file diff --git a/feature/study/src/main/java/com/umcspot/spot/study/recruiting/RecruitingStudyFilterScreen.kt b/feature/study/src/main/java/com/umcspot/spot/study/recruiting/RecruitingStudyFilterScreen.kt index a37d4928..8381e5c8 100644 --- a/feature/study/src/main/java/com/umcspot/spot/study/recruiting/RecruitingStudyFilterScreen.kt +++ b/feature/study/src/main/java/com/umcspot/spot/study/recruiting/RecruitingStudyFilterScreen.kt @@ -1,5 +1,6 @@ package com.umcspot.spot.study.recruiting +import androidx.activity.compose.BackHandler import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource @@ -24,9 +25,14 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.listSaver +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.role import androidx.compose.ui.semantics.semantics @@ -36,9 +42,12 @@ import androidx.compose.ui.unit.sp import androidx.compose.ui.zIndex import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.umcspot.spot.designsystem.R +import com.umcspot.spot.designsystem.component.button.MultiButton import com.umcspot.spot.designsystem.component.button.TextButton import com.umcspot.spot.designsystem.component.button.TextButtonState import com.umcspot.spot.designsystem.component.study.section.ActivityThemeSection +import com.umcspot.spot.designsystem.shapes.SpotShapes import com.umcspot.spot.designsystem.theme.SpotTheme import com.umcspot.spot.model.ActivityType import com.umcspot.spot.model.FeeRange @@ -51,45 +60,61 @@ import kotlinx.collections.immutable.ImmutableList fun RecruitingStudyFilterScreen( contentPadding: PaddingValues, onAcceptFilterClick: () -> Unit, - vm: RecruitingStudyFilterViewModel = hiltViewModel(), + viewModel: RecruitingStudyViewModel = hiltViewModel(), ) { - val activities by vm.activities.collectAsStateWithLifecycle() - val fees by vm.fees.collectAsStateWithLifecycle() - val themes by vm.themes.collectAsStateWithLifecycle() - val acceptEnabled by vm.notNull.collectAsStateWithLifecycle() + val activity by viewModel.activity.collectAsStateWithLifecycle() + val fee by viewModel.fee.collectAsStateWithLifecycle() + val themes by viewModel.themes.collectAsStateWithLifecycle() + + var draftActivity by rememberSaveable { mutableStateOf(activity) } + var draftFee by rememberSaveable { mutableStateOf(fee) } + + val themeSaver = listSaver, String>( + save = { list -> list.map { it.name } }, + restore = { names -> names.map { StudyTheme.valueOf(it) } } + ) + var draftThemes by rememberSaveable(stateSaver = themeSaver) { mutableStateOf(themes) } + + val acceptEnabled = draftActivity != null || draftFee != null || draftThemes.isNotEmpty() val topPad = contentPadding.calculateTopPadding() val bottomPad = contentPadding.calculateBottomPadding() - LaunchedEffect(Unit) { - vm.events.collect { ev -> - when (ev) { - is RecruitingStudyFilterViewModel.Event.Applied -> onAcceptFilterClick() - } - } + BackHandler { + onAcceptFilterClick() } RecruitingStudyFilterScreenContent( - modifier = Modifier - .padding(top = topPad, bottom = bottomPad), - selectedActivities = activities, - selectedFees = fees, - selectedThemes = themes, + modifier = Modifier.padding(top = topPad, bottom = bottomPad), + selectedActivity = draftActivity, + selectedFee = draftFee, + selectedThemes = draftThemes, buttonEnabled = acceptEnabled, - onToggleActivity = vm::toggleActivity, - onToggleFee = vm::toggleFee, - onToggleTheme = vm::toggleTheme, - onReset = vm::reset, - onApply = vm::apply + onToggleActivity = { type -> draftActivity = if (draftActivity == type) null else type }, + onToggleFee = { fee -> draftFee = if (draftFee == fee) null else fee }, + onToggleTheme = { theme -> draftThemes = if (draftThemes.contains(theme)) draftThemes - theme else draftThemes + theme }, + onReset = { + draftActivity = null + draftFee = null + draftThemes = emptyList() + }, + onApply = { + viewModel.applyFilter( + fee = draftFee, + activity = draftActivity, + themes = draftThemes + ) + onAcceptFilterClick() + } ) } @Composable fun RecruitingStudyFilterScreenContent( - selectedActivities: ImmutableList, - selectedFees: ImmutableList, - selectedThemes: ImmutableList, + selectedActivity: ActivityType?, + selectedFee: FeeRange?, + selectedThemes: List, buttonEnabled: Boolean, onToggleActivity: (ActivityType) -> Unit, onToggleFee: (FeeRange) -> Unit, @@ -99,12 +124,12 @@ fun RecruitingStudyFilterScreenContent( modifier: Modifier = Modifier ) { Box( - modifier = Modifier + modifier = modifier .fillMaxSize() .background(SpotTheme.colors.white) ) { Column( - modifier = modifier + modifier = Modifier .fillMaxSize() .verticalScroll(rememberScrollState()) .padding(horizontal = 16.dp) @@ -119,7 +144,7 @@ fun RecruitingStudyFilterScreenContent( ActivityTypeMultiSection( - selectedTypes = selectedActivities, + selectedTypes = selectedActivity, onToggle = onToggleActivity ) @@ -127,7 +152,7 @@ fun RecruitingStudyFilterScreenContent( ActivityFeeSection( - selectedFees = selectedFees, + selectedFee = selectedFee, onToggle = onToggleFee ) @@ -165,6 +190,9 @@ fun RecruitingStudyFilterScreenContent( .zIndex(1f) ) { TextButton( + modifier = Modifier + .width(screenWidthDp(326.dp)) + .height(screenHeightDp(47.dp)), text = "검색 결과 보기", enabled = buttonEnabled, onClick = onApply @@ -175,19 +203,20 @@ fun RecruitingStudyFilterScreenContent( @Composable fun ActivityTypeMultiSection( - selectedTypes: ImmutableList, + selectedTypes: ActivityType?, onToggle: (ActivityType) -> Unit ) { Row( modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(14.dp) + horizontalArrangement = Arrangement.spacedBy(screenWidthDp(14.dp)) ) { ActivityType.entries.forEach { type -> - TextButton( + MultiButton( modifier = Modifier.weight(1f), text = type.label, - state = TextButtonState.Toggle, - checked = selectedTypes.contains(type), + shape = SpotShapes.Soft, + painter = getIconForType(type), + checked = (selectedTypes == type), onClick = { onToggle(type) } ) } @@ -196,7 +225,7 @@ fun ActivityTypeMultiSection( @Composable fun ActivityFeeSection( - selectedFees: ImmutableList, + selectedFee: FeeRange?, onToggle: (FeeRange) -> Unit ) { Column( @@ -220,10 +249,12 @@ fun ActivityFeeSection( text = fee.label, modifier = Modifier .width(screenWidthDp(71.dp)) - .wrapContentHeight(), + .height(screenHeightDp(35.dp)), state = TextButtonState.Toggle, - checked = selectedFees.contains(fee), + checked = (selectedFee == fee), onClick = { onToggle(fee) }, + shape = SpotShapes.Hard, + style = SpotTheme.typography.medium_500 ) } } @@ -251,4 +282,10 @@ fun ResetFilterText( ) .padding(vertical = 4.dp) ) +} + +@Composable +private fun getIconForType(type: ActivityType) = when (type) { + ActivityType.ONLINE -> painterResource(R.drawable.online) + ActivityType.OFFLINE -> painterResource(R.drawable.offline) } \ No newline at end of file diff --git a/feature/study/src/main/java/com/umcspot/spot/study/recruiting/RecruitingStudyFilterViewmodel.kt b/feature/study/src/main/java/com/umcspot/spot/study/recruiting/RecruitingStudyFilterViewmodel.kt deleted file mode 100644 index 269eff7c..00000000 --- a/feature/study/src/main/java/com/umcspot/spot/study/recruiting/RecruitingStudyFilterViewmodel.kt +++ /dev/null @@ -1,117 +0,0 @@ -package com.umcspot.spot.study.recruiting - -import androidx.lifecycle.SavedStateHandle -import androidx.lifecycle.ViewModel -import com.umcspot.spot.model.ActivityType -import com.umcspot.spot.model.FeeRange -import com.umcspot.spot.model.StudyTheme -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.PersistentList -import kotlinx.collections.immutable.persistentListOf -import kotlinx.collections.immutable.toPersistentList -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharedFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.update -import java.io.Serializable -import javax.inject.Inject - -@HiltViewModel -class RecruitingStudyFilterViewModel @Inject constructor( - private val savedStateHandle: SavedStateHandle -) : ViewModel() { - - private val KEY_ACTIVITIES = "recruit_filter_activities" - private val KEY_FEES = "recruit_filter_fees" - private val KEY_THEMES = "recruit_filter_themes" - - private val _activities = MutableStateFlow>( - savedStateHandle.get>(KEY_ACTIVITIES)?.toPersistentList() ?: persistentListOf() - ) - val activities: StateFlow> = _activities.asStateFlow() - - private val _fees = MutableStateFlow>( - savedStateHandle.get>(KEY_FEES)?.toPersistentList() ?: persistentListOf() - ) - val fees: StateFlow> = _fees.asStateFlow() - - private val _themes = MutableStateFlow>( - savedStateHandle.get>(KEY_THEMES)?.toPersistentList() ?: persistentListOf() - ) - val themes: StateFlow> = _themes.asStateFlow() - - private fun calcNotNull(): Boolean = - _activities.value.isNotEmpty() || _fees.value.isNotEmpty() || _themes.value.isNotEmpty() - - private val _notNull = MutableStateFlow(calcNotNull()) - val notNull: StateFlow = _notNull.asStateFlow() - - sealed interface Event : Serializable { - data class Applied(val filter: RecruitingStudyFilter) : Event - } - private val _events = MutableSharedFlow(extraBufferCapacity = 1) - val events: SharedFlow = _events - - data class RecruitingStudyFilter( - val activities: List, - val fees: List, - val themes: List - ) : Serializable { - fun isEmpty() = activities.isEmpty() && fees.isEmpty() && themes.isEmpty() - } - - fun toggleActivity(type: ActivityType) { - _activities.update { current -> - - val newList = if (current.contains(type)) current.remove(type) else current.add(type) - savedStateHandle[KEY_ACTIVITIES] = ArrayList(newList) - newList - } - _notNull.value = calcNotNull() - } - - fun toggleFee(fee: FeeRange) { - _fees.update { current -> - val newList = if (current.contains(fee)) current.remove(fee) else current.add(fee) - savedStateHandle[KEY_FEES] = ArrayList(newList) - newList - } - _notNull.value = calcNotNull() - } - - fun toggleTheme(theme: StudyTheme) { - _themes.update { current -> - val newList = if (current.contains(theme)) current.remove(theme) else current.add(theme) - savedStateHandle[KEY_THEMES] = ArrayList(newList) - newList - } - _notNull.value = calcNotNull() - } - - fun reset() { - _activities.value = persistentListOf() - _fees.value = persistentListOf() - _themes.value = persistentListOf() - - savedStateHandle[KEY_ACTIVITIES] = null - savedStateHandle[KEY_FEES] = null - savedStateHandle[KEY_THEMES] = null - - _notNull.value = false - } - - fun apply() { - _events.tryEmit( - Event.Applied( - RecruitingStudyFilter( - activities = _activities.value, - fees = _fees.value, - themes = _themes.value - ) - ) - ) - } -} \ No newline at end of file diff --git a/feature/study/src/main/java/com/umcspot/spot/study/recruiting/RecruitingStudyScreen.kt b/feature/study/src/main/java/com/umcspot/spot/study/recruiting/RecruitingStudyScreen.kt index 4090f315..da89e7e3 100644 --- a/feature/study/src/main/java/com/umcspot/spot/study/recruiting/RecruitingStudyScreen.kt +++ b/feature/study/src/main/java/com/umcspot/spot/study/recruiting/RecruitingStudyScreen.kt @@ -1,6 +1,6 @@ package com.umcspot.spot.study.recruiting -import androidx.activity.compose.BackHandler +import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -17,6 +17,8 @@ import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.items @@ -29,10 +31,13 @@ import androidx.compose.material3.ListItem import androidx.compose.material3.ListItemDefaults import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -45,28 +50,34 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.Lifecycle.Event +import androidx.lifecycle.LifecycleEventObserver +import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.umcspot.spot.designsystem.R +import com.umcspot.spot.designsystem.component.SpotSpinner import com.umcspot.spot.designsystem.component.empty.EmptyAlert import com.umcspot.spot.designsystem.component.study.StudyListItem +import com.umcspot.spot.designsystem.shapes.SpotShapes +import com.umcspot.spot.designsystem.theme.B500 +import com.umcspot.spot.designsystem.theme.G200 +import com.umcspot.spot.designsystem.theme.G300 import com.umcspot.spot.designsystem.theme.SpotTheme import com.umcspot.spot.model.RecruitingStudySort import com.umcspot.spot.study.model.StudyResult +import com.umcspot.spot.study.model.StudyResultList +import com.umcspot.spot.ui.extension.screenHeightDp +import com.umcspot.spot.ui.extension.screenWidthDp import com.umcspot.spot.ui.state.UiState import kotlinx.coroutines.launch -import com.umcspot.spot.designsystem.R -import com.umcspot.spot.designsystem.shapes.SpotShapes -import com.umcspot.spot.designsystem.theme.B500 -import com.umcspot.spot.designsystem.theme.G300 -import com.umcspot.spot.study.recruiting.RecruitingStudyViewModel -import timber.log.Timber @Composable fun RecruitingStudyScreen( - contentPadding : PaddingValues, + contentPadding: PaddingValues, viewmodel: RecruitingStudyViewModel = hiltViewModel(), onRegisterScrollToTop: ((() -> Unit)?) -> Unit, - onFilterClick : () -> Unit, - onItemClick : (StudyResult) -> Unit + onFilterClick: () -> Unit, + onItemClick: (StudyResult) -> Unit ) { val state by viewmodel.uiState.collectAsStateWithLifecycle() val sort by viewmodel.sortType.collectAsStateWithLifecycle() @@ -79,14 +90,45 @@ fun RecruitingStudyScreen( val topPad = contentPadding.calculateTopPadding() val bottomPad = contentPadding.calculateBottomPadding() - // 최초 진입 시 한 번 로드 - LaunchedEffect(state.studies) { - if (state.studies is UiState.Empty) { - viewmodel.load(RecruitingStudySort.LATEST) + val lifecycleOwner = LocalLifecycleOwner.current + + val ui = state.studies + val itemList: List = when (ui) { + is UiState.Success -> ui.data.studyList + else -> emptyList() + } + + val shouldLoadMore = remember { + derivedStateOf { + val layoutInfo = listState.layoutInfo + val totalItems = layoutInfo.totalItemsCount + val lastVisibleItemIndex = layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: 0 + + totalItems > 0 && lastVisibleItemIndex >= totalItems - 3 + } + } + + DisposableEffect(lifecycleOwner) { + val observer = LifecycleEventObserver { _, event -> + if (event == Event.ON_RESUME) { + scope.launch { listState.scrollToItem(0) } + } + } + lifecycleOwner.lifecycle.addObserver(observer) + onDispose { lifecycleOwner.lifecycle.removeObserver(observer) } + } + + LaunchedEffect(shouldLoadMore.value) { + if (shouldLoadMore.value) { + val successData = (ui as? UiState.Success)?.data + if (successData?.hasNext == true) { + viewmodel.loadNextPage() + } } } LaunchedEffect(Unit) { + viewmodel.load() onRegisterScrollToTop { scope.launch { listState.animateScrollToItem(0) @@ -94,32 +136,84 @@ fun RecruitingStudyScreen( } } - when (val state = state.studies) { - is UiState.Loading -> Text("로딩 중...", color = Color.Gray) + Column( + modifier = Modifier + .fillMaxSize() + .background(SpotTheme.colors.white) + .padding(top = topPad, bottom = bottomPad, start = screenWidthDp(17.dp), end = screenWidthDp(17.dp)) + ) { + // 타이틀 + Row( + modifier = Modifier + .padding(vertical = screenHeightDp(9.dp)), + ) { + Text( + text = "모집중 스터디", + style = SpotTheme.typography.h4 + ) + } - is UiState.Failure -> Text("에러: ${state.msg}", color = Color.Red) + Spacer(Modifier.height(screenHeightDp(12.dp))) - UiState.Empty -> { - Text(text = "데이터가 없습니다.") - } + HeaderRow( + size = itemList.size, + sortType = sort, + onOpenSortSheet = { showSortSheet = true }, + onFilterClick = onFilterClick + ) + Spacer(Modifier.height(screenHeightDp(10.dp))) - is UiState.Success -> { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { RecruitingStudyScreenContent( modifier = Modifier - .fillMaxSize() - .background(SpotTheme.colors.white) - .padding(top = topPad, bottom = bottomPad, start = 16.dp, end = 16.dp), - studies = state.data.studyList, - sortType = sort, + .fillMaxSize(), + studies = itemList, listState = listState, - onOpenSortSheet = { showSortSheet = true }, - onFilterClick = onFilterClick, onItemClick = onItemClick, ) + + when(ui) { + is UiState.Loading -> { + Surface( + color = SpotTheme.colors.white, + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + ) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + SpotSpinner() + } + } + } + + is UiState.Failure -> { + EmptyAlert( + painter = painterResource(R.drawable.emoji_sad), + alertTitle = "조건에 맞는 스터디가 없어요.", + alertDes = "필터를 재설정하고 스터디를 찾아보세요." + ) + } + + is UiState.Empty -> { + EmptyAlert( + painter = painterResource(R.drawable.emoji_sad), + alertTitle = "조건에 맞는 스터디가 없어요.", + alertDes = "필터를 재설정하고 스터디를 찾아보세요." + ) + } + + is UiState.Success -> Unit + } } + } - // 정렬 바텀시트 if (showSortSheet) { SortTypeBottomSheet( current = sort, @@ -134,66 +228,44 @@ private fun RecruitingStudyScreenContent( modifier: Modifier = Modifier, studies: List, listState: LazyListState, - sortType : RecruitingStudySort, - onOpenSortSheet : () -> Unit, - onFilterClick: () -> Unit, onItemClick: (StudyResult) -> Unit, +) { + LazyColumn( + state = listState, + modifier = modifier, + verticalArrangement = Arrangement.spacedBy(screenHeightDp(0.dp)) ) { - Column( - modifier = modifier - .fillMaxSize() - ) { - // 타이틀 - Text( - text = "모집중 스터디", - style = SpotTheme.typography.small_400.copy(fontSize = 20.sp) - ) + items( + items = studies, + key = { it.id } + ) { item -> + Spacer(Modifier.padding(screenHeightDp(5.dp))) - Spacer(Modifier.height(8.dp)) + StudyListItem( + item = item, + modifier = Modifier + .fillMaxWidth(), + onClick = { onItemClick(item) } + ) - HeaderRow( - size = studies.size, - sortType = sortType, - onOpenSortSheet = onOpenSortSheet, - onFilterClick = onFilterClick - ) + if(studies.indexOf(item) != studies.lastIndex) { + Spacer(Modifier.padding(screenHeightDp(5.dp))) - if (studies.isEmpty()) { - // 빈 상태 - Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - EmptyAlert( - painter = painterResource(R.drawable.alert), - alertTitle = "현재 모집중인 스터디가 없어요", - alertDes = "필터를 변경하거나 나중에 다시 확인해보세요." + HorizontalDivider( + modifier = Modifier + .fillMaxWidth(), + color = SpotTheme.colors.G300, + thickness = 1.dp ) } - } else { - LazyColumn( - state = listState, - modifier = Modifier.fillMaxSize(), - verticalArrangement = Arrangement.spacedBy(0.dp) - ) { - items( - items = studies, - key = { it.studyId } - ) { item -> - StudyListItem( - item = item, - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 5.dp), - onClick = { onItemClick(item) } - ) - } - } } } } @Composable fun HeaderRow( - size : Int, - sortType : RecruitingStudySort, + size: Int, + sortType: RecruitingStudySort, onOpenSortSheet: () -> Unit, onFilterClick: () -> Unit ) { @@ -204,35 +276,44 @@ fun HeaderRow( ) { Text( text = "%02d건".format(size), - style = SpotTheme.typography.medium_500.copy(fontSize = 12.sp), - color = SpotTheme.colors.gray500 + style = SpotTheme.typography.regular_500, + color = SpotTheme.colors.gray400 ) - Row(verticalAlignment = Alignment.CenterVertically) { + Row{ OutlinedButton( onClick = onOpenSortSheet, - shape = SpotShapes.Soft, - contentPadding = PaddingValues(horizontal = 10.dp, vertical = 6.dp), - modifier = Modifier.height(32.dp) + shape = SpotShapes.Hard, + border = BorderStroke(1.dp, SpotTheme.colors.G200), + contentPadding = PaddingValues(start = screenWidthDp(7.dp), end = screenWidthDp(4.dp), top = screenHeightDp(2.dp), bottom = screenHeightDp(4.dp)), + modifier = Modifier + .wrapContentWidth() + .height(screenHeightDp(26.dp)) ) { Text( text = sortType.label, color = SpotTheme.colors.black, style = SpotTheme.typography.medium_500.copy(fontSize = 12.sp) ) - Spacer(Modifier.width(5.dp)) + Spacer(Modifier.width(screenWidthDp(7.dp))) Icon( + modifier = Modifier.size(screenWidthDp(14.dp)), painter = painterResource(R.drawable.arrow_down), tint = SpotTheme.colors.B500, contentDescription = null ) } - IconButton(onClick = onFilterClick) { + Spacer(Modifier.width(screenWidthDp(10.dp))) + + IconButton( + onClick = onFilterClick, + modifier = Modifier.size(screenWidthDp(26.dp)) + ) { Icon( painter = painterResource(R.drawable.filter), contentDescription = "필터", - modifier = Modifier.size(22.dp) + modifier = Modifier.size(screenWidthDp(14.dp)) ) } } @@ -242,13 +323,15 @@ fun HeaderRow( @OptIn(ExperimentalMaterial3Api::class) @Composable fun SortTypeBottomSheet( - current: RecruitingStudySort, + current: RecruitingStudySort?, onSelect: (RecruitingStudySort) -> Unit, onDismiss: () -> Unit ) { val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) ModalBottomSheet( + modifier = Modifier + .fillMaxWidth(), onDismissRequest = onDismiss, sheetState = sheetState, containerColor = SpotTheme.colors.white, @@ -259,7 +342,7 @@ fun SortTypeBottomSheet( Column( modifier = Modifier .navigationBarsPadding() - .padding(vertical = 8.dp) + .padding(vertical = screenHeightDp(14.dp)) ) { RecruitingStudySort.entries.forEachIndexed { index, option -> ListItem( @@ -270,8 +353,10 @@ fun SortTypeBottomSheet( ), headlineContent = { Text( + modifier = Modifier, text = option.label, - style = SpotTheme.typography.medium_500.copy(fontSize = 15.sp) + color = SpotTheme.colors.black, + style = SpotTheme.typography.medium_400 ) }, trailingContent = { @@ -279,23 +364,24 @@ fun SortTypeBottomSheet( Icon( painter = painterResource(R.drawable.success_default), tint = SpotTheme.colors.B500, - modifier = Modifier.size(20.dp), + modifier = Modifier + .size(screenWidthDp(14.dp)), contentDescription = "선택됨", ) } }, modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp) .clickable { onSelect(option) onDismiss() } + .padding(horizontal = screenWidthDp(17.dp)) ) if (index != RecruitingStudySort.entries.lastIndex) { HorizontalDivider( color = SpotTheme.colors.G300, - thickness = 0.6.dp + thickness = 1.dp ) } } diff --git a/feature/study/src/main/java/com/umcspot/spot/study/recruiting/RecruitingStudyViewModel.kt b/feature/study/src/main/java/com/umcspot/spot/study/recruiting/RecruitingStudyViewModel.kt index f158612a..45582897 100644 --- a/feature/study/src/main/java/com/umcspot/spot/study/recruiting/RecruitingStudyViewModel.kt +++ b/feature/study/src/main/java/com/umcspot/spot/study/recruiting/RecruitingStudyViewModel.kt @@ -1,5 +1,6 @@ package com.umcspot.spot.study.recruiting +import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.umcspot.spot.model.ActivityType @@ -13,6 +14,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import javax.inject.Inject @@ -22,77 +24,141 @@ class RecruitingStudyViewModel @Inject constructor( private val studyRepository: StudyRepository ) : ViewModel() { - data class RecruitingStudyUiState( - val studies: UiState = UiState.Empty + data class ScrollPosition( + val index: Int = 0, + val offset: Int = 0 ) + data class RecruitingStudyUiState(val studies: UiState = UiState.Empty) + + var scrollPosition: ScrollPosition = ScrollPosition() + + private val _isLoadingMore = MutableStateFlow(false) + val isLoadingMore: StateFlow = _isLoadingMore.asStateFlow() + private val _uiState = MutableStateFlow(RecruitingStudyUiState()) val uiState: StateFlow = _uiState.asStateFlow() - private val _sortType = MutableStateFlow(RecruitingStudySort.LATEST) + private val _sortType = MutableStateFlow(RecruitingStudySort.RECENT) val sortType: StateFlow = _sortType.asStateFlow() + /** Filter **/ private val _activity = MutableStateFlow(null) val activity: StateFlow = _activity.asStateFlow() private val _fee = MutableStateFlow(null) val fee: StateFlow = _fee.asStateFlow() - private val _theme = MutableStateFlow(null) - val theme: StateFlow = _theme.asStateFlow() + private val _themes = MutableStateFlow>(emptyList()) + val themes: StateFlow> = _themes.asStateFlow() - private fun fetch() { + private fun calcNotNull(): Boolean = + _activity.value != null || _fee.value != null || _themes.value.isNotEmpty() + + private val _notNull = MutableStateFlow(calcNotNull()) + val notNull: StateFlow = _notNull.asStateFlow() + + fun load() { _uiState.update { it.copy(studies = UiState.Loading) } viewModelScope.launch { - val res = studyRepository.getRecruitingStudies( - sortType = _sortType.value, - activityType = _activity.value, - feeRange = _fee.value, - theme = _theme.value - ) - val newState: UiState = res.fold( - onSuccess = { data -> - if (data.studyList.isEmpty()) UiState.Empty else UiState.Success(data) - }, - onFailure = { e -> UiState.Failure(e.message ?: e.toString()) } - ) - _uiState.update { it.copy(studies = newState) } + runCatching { + studyRepository.getRecruitingStudies( + feeCategory = _fee.value, + categories = _themes.value.map { it.name }, + isOnline = _activity.value.toIsOnline(), + sortBy = _sortType.value, + size = 10 + ).getOrThrow() + }.onSuccess { data -> + if (data.studyList.isEmpty()) { + _uiState.update { it.copy(studies = UiState.Empty) } + } else { + _uiState.update { it.copy(studies = UiState.Success(data)) } + } + }.onFailure { e -> + Log.e("RecruitingStudyViewModel", "loadFristError", e) +// UiState.Failure(e.message ?: e.toString()) + } } } - /** 정렬 기준으로 목록 로드 */ - fun load(selected: RecruitingStudySort = _sortType.value) { - _sortType.value = selected - fetch() + fun loadNextPage() { + val currentUi = _uiState.value.studies + val success = currentUi as? UiState.Success ?: return + val currentList = success.data + + if (!currentList.hasNext) return + if (_isLoadingMore.value) return + + viewModelScope.launch { + runCatching { + studyRepository.getRecruitingStudies( + feeCategory = _fee.value, + categories = _themes.value.map { it.name }, + isOnline = _activity.value.toIsOnline(), + sortBy = _sortType.value, + cursor = currentList.nextCursor, + size = 10 + ).getOrThrow() + }.onSuccess { newPage -> + val merged = currentList.copy( + studyList = currentList.studyList + newPage.studyList, + hasNext = newPage.hasNext, + nextCursor = newPage.nextCursor + ) + _uiState.update { it.copy(studies = UiState.Success(merged)) } + }.onFailure { e -> + Log.e("RecruitingStudyViewModel", "loadNextpageError", e) + } + } } /** 정렬 변경 */ fun selectSort(type: RecruitingStudySort) { _sortType.value = type - fetch() + load() } - /** ✅ 각 항목별 필터 변경 */ - fun setActivityFilter(type: ActivityType?, refresh: Boolean = true) { - _activity.value = type - if (refresh) fetch() + + /** Filter 변경 **/ + private fun updateNotNull() { + _notNull.value = calcNotNull() } - fun setFeeFilter(fee: FeeRange?, refresh: Boolean = true) { - _fee.value = fee - if (refresh) fetch() + + fun toggleActivity(type: ActivityType) { + _activity.value = if (_activity.value == type) null else type + updateNotNull() } - fun setThemeFilter(theme: StudyTheme?, refresh: Boolean = true) { - _theme.value = theme - if (refresh) fetch() + fun toggleFee(fee: FeeRange) { + _fee.value = if (_fee.value == fee) null else fee + updateNotNull() } - /** ✅ 전체 초기화 */ - fun clearFilters(refresh: Boolean = true) { - _activity.value = null + fun toggleTheme(theme: StudyTheme) { + val cur = _themes.value + _themes.value = if (cur.contains(theme)) cur - theme else cur + theme + updateNotNull() + } + + fun resetFilter() { _fee.value = null - _theme.value = null - if (refresh) fetch() + _activity.value = null + _themes.value = emptyList() + updateNotNull() + } + + fun applyFilter(fee: FeeRange?, activity: ActivityType?, themes: List) { + _fee.value = fee + _activity.value = activity + _themes.value = themes + load() + } + + private fun ActivityType?.toIsOnline(): Boolean? = when (this) { + ActivityType.ONLINE -> true + ActivityType.OFFLINE -> false + null -> null } } \ No newline at end of file diff --git a/feature/study/src/main/java/com/umcspot/spot/study/recruiting/navigation/RecruitingStudyFilterNavigation.kt b/feature/study/src/main/java/com/umcspot/spot/study/recruiting/navigation/RecruitingStudyFilterNavigation.kt index ecc18efb..38e4db0e 100644 --- a/feature/study/src/main/java/com/umcspot/spot/study/recruiting/navigation/RecruitingStudyFilterNavigation.kt +++ b/feature/study/src/main/java/com/umcspot/spot/study/recruiting/navigation/RecruitingStudyFilterNavigation.kt @@ -1,12 +1,15 @@ package com.umcspot.spot.study.recruiting.navigation import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.runtime.remember +import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptions import androidx.navigation.compose.composable import com.umcspot.spot.navigation.Route import com.umcspot.spot.study.recruiting.RecruitingStudyFilterScreen +import com.umcspot.spot.study.recruiting.RecruitingStudyViewModel import kotlinx.serialization.Serializable fun NavController.navigateToRecruitingStudyFilter(navOptions: NavOptions? = null) { @@ -14,13 +17,22 @@ fun NavController.navigateToRecruitingStudyFilter(navOptions: NavOptions? = null } fun NavGraphBuilder.recruitingStudyFilterGraph( + navController: NavController, contentPadding: PaddingValues, onAcceptFilterClick: () -> Unit, ) { composable { + val parentEntry = remember(navController) { + + navController.getBackStackEntry(Recruiting) + } + + val recruitingVm: RecruitingStudyViewModel = hiltViewModel(parentEntry) + RecruitingStudyFilterScreen( contentPadding = contentPadding, onAcceptFilterClick = onAcceptFilterClick, + viewModel = recruitingVm ) } } From e993d5d68cb86e064074fb8f9c4a783d1b420af7 Mon Sep 17 00:00:00 2001 From: yeonwoo Date: Tue, 6 Jan 2026 18:11:08 +0900 Subject: [PATCH 02/11] =?UTF-8?q?feat=20:=20preferLocationStudy=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/common/src/main/assets/region_data.tsv | 3568 +++++++++++++++++ .../umcspot/spot/common/location/Location.kt | 63 +- .../bottomsheet/LocationBottomSheet.kt | 41 +- .../java/com/umcspot/spot/model/Global.kt | 8 + data/study/build.gradle.kts | 1 + .../spot/study/datasource/StudyDataSource.kt | 12 +- .../datasourceimpl/StudyDataSourceImpl.kt | 13 + .../repositoryimpl/StudyRepositoryImpl.kt | 41 +- .../spot/study/service/StudyService.kt | 15 +- .../spot/user/datasource/UserDataSource.kt | 7 +- .../user/datasourceimpl/UserDataSourceImpl.kt | 14 + .../request/UserPreferredRegionRequestDto.kt | 11 + .../UserPreferredRegionResponseDto.kt | 16 + .../umcspot/spot/user/mapper/UserMapper.kt | 15 +- .../user/repositoryimpl/UserRepositoryImpl.kt | 27 +- .../umcspot/spot/user/service/UserService.kt | 11 + .../spot/study/repository/StudyRepository.kt | 24 +- .../user/model/UserPreferredRegionResult.kt | 7 + .../spot/user/repository/UserRepository.kt | 3 + feature/main/build.gradle.kts | 3 - .../java/com/umcspot/spot/main/MainNavHost.kt | 15 +- .../com/umcspot/spot/main/MainNavigator.kt | 12 +- feature/study/build.gradle.kts | 1 + .../PreferLocationStudyFilterScreen.kt | 559 ++- .../PreferLocationStudyScreen.kt | 695 ++-- .../PreferLocationStudyViewModel.kt | 398 +- .../PreferLocationStudyFilterNavigation.kt | 68 +- .../PreferLocationStudyNavigation.kt | 68 +- .../recruiting/RecruitingStudyFilterScreen.kt | 76 +- .../study/recruiting/RecruitingStudyScreen.kt | 2 +- .../recruiting/RecruitingStudyViewModel.kt | 4 +- .../study/register/RegisterStudyScreen.kt | 5 +- .../study/register/RegisterStudyViewModel.kt | 8 +- .../component/SelectedRegionsSection.kt | 7 +- .../register/model/RegisterStudyState.kt | 2 +- .../study/register/screen/StudyPlaceScreen.kt | 6 +- 36 files changed, 4834 insertions(+), 992 deletions(-) create mode 100644 core/common/src/main/assets/region_data.tsv create mode 100644 data/user/src/main/java/com/umcspot/spot/user/dto/request/UserPreferredRegionRequestDto.kt create mode 100644 data/user/src/main/java/com/umcspot/spot/user/dto/response/UserPreferredRegionResponseDto.kt create mode 100644 domain/user/src/main/java/com/umcspot/spot/user/model/UserPreferredRegionResult.kt diff --git a/core/common/src/main/assets/region_data.tsv b/core/common/src/main/assets/region_data.tsv new file mode 100644 index 00000000..bb12e528 --- /dev/null +++ b/core/common/src/main/assets/region_data.tsv @@ -0,0 +1,3568 @@ +code province district neighborhood +1111051500 서울특별시 종로구 청운효자동 +1111053000 서울특별시 종로구 사직동 +1111054000 서울특별시 종로구 삼청동 +1111055000 서울특별시 종로구 부암동 +1111056000 서울특별시 종로구 평창동 +1111057000 서울특별시 종로구 무악동 +1111058000 서울특별시 종로구 교남동 +1111060000 서울특별시 종로구 가회동 +1111061500 서울특별시 종로구 종로1.2.3.4가동 +1111063000 서울특별시 종로구 종로5.6가동 +1111064000 서울특별시 종로구 이화동 +1111065000 서울특별시 종로구 혜화동 +1111067000 서울특별시 종로구 창신제1동 +1111068000 서울특별시 종로구 창신제2동 +1111069000 서울특별시 종로구 창신제3동 +1111070000 서울특별시 종로구 숭인제1동 +1111071000 서울특별시 종로구 숭인제2동 +1114052000 서울특별시 중구 소공동 +1114054000 서울특별시 중구 회현동 +1114055000 서울특별시 중구 명동 +1114057000 서울특별시 중구 필동 +1114058000 서울특별시 중구 장충동 +1114059000 서울특별시 중구 광희동 +1114060500 서울특별시 중구 을지로동 +1114061500 서울특별시 중구 신당동 +1114062500 서울특별시 중구 다산동 +1114063500 서울특별시 중구 약수동 +1114064500 서울특별시 중구 청구동 +1114065000 서울특별시 중구 신당제5동 +1114066500 서울특별시 중구 동화동 +1114067000 서울특별시 중구 황학동 +1114068000 서울특별시 중구 중림동 +1117051000 서울특별시 용산구 후암동 +1117052000 서울특별시 용산구 용산2가동 +1117053000 서울특별시 용산구 남영동 +1117055500 서울특별시 용산구 청파동 +1117056000 서울특별시 용산구 원효로제1동 +1117057000 서울특별시 용산구 원효로제2동 +1117058000 서울특별시 용산구 효창동 +1117059000 서울특별시 용산구 용문동 +1117062500 서울특별시 용산구 한강로동 +1117063000 서울특별시 용산구 이촌제1동 +1117064000 서울특별시 용산구 이촌제2동 +1117065000 서울특별시 용산구 이태원제1동 +1117066000 서울특별시 용산구 이태원제2동 +1117068500 서울특별시 용산구 한남동 +1117069000 서울특별시 용산구 서빙고동 +1117070000 서울특별시 용산구 보광동 +1120052000 서울특별시 성동구 왕십리제2동 +1120053500 서울특별시 성동구 왕십리도선동 +1120054000 서울특별시 성동구 마장동 +1120055000 서울특별시 성동구 사근동 +1120056000 서울특별시 성동구 행당제1동 +1120057000 서울특별시 성동구 행당제2동 +1120058000 서울특별시 성동구 응봉동 +1120059000 서울특별시 성동구 금호1가동 +1120061500 서울특별시 성동구 금호2.3가동 +1120062000 서울특별시 성동구 금호4가동 +1120064500 서울특별시 성동구 옥수동 +1120065000 서울특별시 성동구 성수1가제1동 +1120066000 서울특별시 성동구 성수1가제2동 +1120067000 서울특별시 성동구 성수2가제1동 +1120069000 서울특별시 성동구 성수2가제3동 +1120072000 서울특별시 성동구 송정동 +1120079000 서울특별시 성동구 용답동 +1121571000 서울특별시 광진구 화양동 +1121573000 서울특별시 광진구 군자동 +1121574000 서울특별시 광진구 중곡제1동 +1121575000 서울특별시 광진구 중곡제2동 +1121576000 서울특별시 광진구 중곡제3동 +1121577000 서울특별시 광진구 중곡제4동 +1121578000 서울특별시 광진구 능동 +1121581000 서울특별시 광진구 광장동 +1121582000 서울특별시 광진구 자양제1동 +1121583000 서울특별시 광진구 자양제2동 +1121584000 서울특별시 광진구 자양제3동 +1121584700 서울특별시 광진구 자양제4동 +1121585000 서울특별시 광진구 구의제1동 +1121586000 서울특별시 광진구 구의제2동 +1121587000 서울특별시 광진구 구의제3동 +1123053600 서울특별시 동대문구 용신동 +1123054500 서울특별시 동대문구 제기동 +1123056000 서울특별시 동대문구 전농제1동 +1123057000 서울특별시 동대문구 전농제2동 +1123060000 서울특별시 동대문구 답십리제1동 +1123061000 서울특별시 동대문구 답십리제2동 +1123065000 서울특별시 동대문구 장안제1동 +1123066000 서울특별시 동대문구 장안제2동 +1123070500 서울특별시 동대문구 청량리동 +1123071000 서울특별시 동대문구 회기동 +1123072000 서울특별시 동대문구 휘경제1동 +1123073000 서울특별시 동대문구 휘경제2동 +1123074000 서울특별시 동대문구 이문제1동 +1123075000 서울특별시 동대문구 이문제2동 +1126052000 서울특별시 중랑구 면목제2동 +1126054000 서울특별시 중랑구 면목제4동 +1126055000 서울특별시 중랑구 면목제5동 +1126056500 서울특별시 중랑구 면목본동 +1126057000 서울특별시 중랑구 면목제7동 +1126057500 서울특별시 중랑구 면목제3.8동 +1126058000 서울특별시 중랑구 상봉제1동 +1126059000 서울특별시 중랑구 상봉제2동 +1126060000 서울특별시 중랑구 중화제1동 +1126061000 서울특별시 중랑구 중화제2동 +1126062000 서울특별시 중랑구 묵제1동 +1126063000 서울특별시 중랑구 묵제2동 +1126065500 서울특별시 중랑구 망우본동 +1126066000 서울특별시 중랑구 망우제3동 +1126068000 서울특별시 중랑구 신내1동 +1126069000 서울특별시 중랑구 신내2동 +1129052500 서울특별시 성북구 성북동 +1129055500 서울특별시 성북구 삼선동 +1129057500 서울특별시 성북구 동선동 +1129058000 서울특별시 성북구 돈암제1동 +1129059000 서울특별시 성북구 돈암제2동 +1129060000 서울특별시 성북구 안암동 +1129061000 서울특별시 성북구 보문동 +1129062000 서울특별시 성북구 정릉제1동 +1129063000 서울특별시 성북구 정릉제2동 +1129064000 서울특별시 성북구 정릉제3동 +1129065000 서울특별시 성북구 정릉제4동 +1129066000 서울특별시 성북구 길음제1동 +1129068500 서울특별시 성북구 길음제2동 +1129070500 서울특별시 성북구 종암동 +1129071500 서울특별시 성북구 월곡제1동 +1129072500 서울특별시 성북구 월곡제2동 +1129076000 서울특별시 성북구 장위제1동 +1129077000 서울특별시 성북구 장위제2동 +1129078000 서울특별시 성북구 장위제3동 +1129081000 서울특별시 성북구 석관동 +1130553400 서울특별시 강북구 삼양동 +1130553500 서울특별시 강북구 미아동 +1130554500 서울특별시 강북구 송중동 +1130555500 서울특별시 강북구 송천동 +1130557500 서울특별시 강북구 삼각산동 +1130559500 서울특별시 강북구 번1동 +1130560300 서울특별시 강북구 번2동 +1130560800 서울특별시 강북구 번3동 +1130561500 서울특별시 강북구 수유1동 +1130562500 서울특별시 강북구 수유2동 +1130563500 서울특별시 강북구 수유3동 +1130564500 서울특별시 강북구 우이동 +1130566000 서울특별시 강북구 인수동 +1132051100 서울특별시 도봉구 창제1동 +1132051200 서울특별시 도봉구 창제2동 +1132051300 서울특별시 도봉구 창제3동 +1132051400 서울특별시 도봉구 창제4동 +1132051500 서울특별시 도봉구 창제5동 +1132052100 서울특별시 도봉구 도봉제1동 +1132052200 서울특별시 도봉구 도봉제2동 +1132066000 서울특별시 도봉구 쌍문제1동 +1132067000 서울특별시 도봉구 쌍문제2동 +1132068000 서울특별시 도봉구 쌍문제3동 +1132068100 서울특별시 도봉구 쌍문제4동 +1132069000 서울특별시 도봉구 방학제1동 +1132070000 서울특별시 도봉구 방학제2동 +1132071000 서울특별시 도봉구 방학제3동 +1135056000 서울특별시 노원구 월계1동 +1135057000 서울특별시 노원구 월계2동 +1135058000 서울특별시 노원구 월계3동 +1135059500 서울특별시 노원구 공릉1동 +1135060000 서울특별시 노원구 공릉2동 +1135061100 서울특별시 노원구 하계1동 +1135061200 서울특별시 노원구 하계2동 +1135061900 서울특별시 노원구 중계본동 +1135062100 서울특별시 노원구 중계1동 +1135062400 서울특별시 노원구 중계4동 +1135062500 서울특별시 노원구 중계2.3동 +1135063000 서울특별시 노원구 상계1동 +1135064000 서울특별시 노원구 상계2동 +1135066500 서울특별시 노원구 상계3.4동 +1135067000 서울특별시 노원구 상계5동 +1135069500 서울특별시 노원구 상계6.7동 +1135070000 서울특별시 노원구 상계8동 +1135071000 서울특별시 노원구 상계9동 +1135072000 서울특별시 노원구 상계10동 +1138051000 서울특별시 은평구 녹번동 +1138052000 서울특별시 은평구 불광제1동 +1138053000 서울특별시 은평구 불광제2동 +1138055100 서울특별시 은평구 갈현제1동 +1138055200 서울특별시 은평구 갈현제2동 +1138056000 서울특별시 은평구 구산동 +1138057000 서울특별시 은평구 대조동 +1138058000 서울특별시 은평구 응암제1동 +1138059000 서울특별시 은평구 응암제2동 +1138060000 서울특별시 은평구 응암제3동 +1138062500 서울특별시 은평구 역촌동 +1138063100 서울특별시 은평구 신사제1동 +1138063200 서울특별시 은평구 신사제2동 +1138064000 서울특별시 은평구 증산동 +1138065000 서울특별시 은평구 수색동 +1138069000 서울특별시 은평구 진관동 +1141052000 서울특별시 서대문구 천연동 +1141055500 서울특별시 서대문구 북아현동 +1141056500 서울특별시 서대문구 충현동 +1141058500 서울특별시 서대문구 신촌동 +1141061500 서울특별시 서대문구 연희동 +1141062000 서울특별시 서대문구 홍제제1동 +1141064000 서울특별시 서대문구 홍제제3동 +1141065500 서울특별시 서대문구 홍제제2동 +1141066000 서울특별시 서대문구 홍은제1동 +1141068500 서울특별시 서대문구 홍은제2동 +1141069000 서울특별시 서대문구 남가좌제1동 +1141070000 서울특별시 서대문구 남가좌제2동 +1141071000 서울특별시 서대문구 북가좌제1동 +1141072000 서울특별시 서대문구 북가좌제2동 +1144055500 서울특별시 마포구 아현동 +1144056500 서울특별시 마포구 공덕동 +1144058500 서울특별시 마포구 도화동 +1144059000 서울특별시 마포구 용강동 +1144060000 서울특별시 마포구 대흥동 +1144061000 서울특별시 마포구 염리동 +1144063000 서울특별시 마포구 신수동 +1144065500 서울특별시 마포구 서강동 +1144066000 서울특별시 마포구 서교동 +1144068000 서울특별시 마포구 합정동 +1144069000 서울특별시 마포구 망원제1동 +1144070000 서울특별시 마포구 망원제2동 +1144071000 서울특별시 마포구 연남동 +1144072000 서울특별시 마포구 성산제1동 +1144073000 서울특별시 마포구 성산제2동 +1144074000 서울특별시 마포구 상암동 +1147051000 서울특별시 양천구 목1동 +1147052000 서울특별시 양천구 목2동 +1147053000 서울특별시 양천구 목3동 +1147054000 서울특별시 양천구 목4동 +1147055000 서울특별시 양천구 목5동 +1147056000 서울특별시 양천구 신월1동 +1147057000 서울특별시 양천구 신월2동 +1147058000 서울특별시 양천구 신월3동 +1147059000 서울특별시 양천구 신월4동 +1147060000 서울특별시 양천구 신월5동 +1147061000 서울특별시 양천구 신월6동 +1147061100 서울특별시 양천구 신월7동 +1147062000 서울특별시 양천구 신정1동 +1147063000 서울특별시 양천구 신정2동 +1147064000 서울특별시 양천구 신정3동 +1147065000 서울특별시 양천구 신정4동 +1147067000 서울특별시 양천구 신정6동 +1147068000 서울특별시 양천구 신정7동 +1150051000 서울특별시 강서구 염창동 +1150052000 서울특별시 강서구 등촌제1동 +1150053000 서울특별시 강서구 등촌제2동 +1150053500 서울특별시 강서구 등촌제3동 +1150054000 서울특별시 강서구 화곡제1동 +1150055000 서울특별시 강서구 화곡제2동 +1150056000 서울특별시 강서구 화곡제3동 +1150057000 서울특별시 강서구 화곡제4동 +1150059000 서울특별시 강서구 화곡본동 +1150059100 서울특별시 강서구 화곡제6동 +1150059300 서울특별시 강서구 화곡제8동 +1150060300 서울특별시 강서구 가양제1동 +1150060400 서울특별시 강서구 가양제2동 +1150060500 서울특별시 강서구 가양제3동 +1150061100 서울특별시 강서구 발산제1동 +1150061500 서울특별시 강서구 우장산동 +1150062000 서울특별시 강서구 공항동 +1150063000 서울특별시 강서구 방화제1동 +1150064000 서울특별시 강서구 방화제2동 +1150064100 서울특별시 강서구 방화제3동 +1153051000 서울특별시 구로구 신도림동 +1153052000 서울특별시 구로구 구로제1동 +1153053000 서울특별시 구로구 구로제2동 +1153054000 서울특별시 구로구 구로제3동 +1153055000 서울특별시 구로구 구로제4동 +1153056000 서울특별시 구로구 구로제5동 +1153059500 서울특별시 구로구 가리봉동 +1153072000 서울특별시 구로구 고척제1동 +1153073000 서울특별시 구로구 고척제2동 +1153074000 서울특별시 구로구 개봉제1동 +1153075000 서울특별시 구로구 개봉제2동 +1153076000 서울특별시 구로구 개봉제3동 +1153077000 서울특별시 구로구 오류제1동 +1153078000 서울특별시 구로구 오류제2동 +1153079000 서울특별시 구로구 수궁동 +1153080000 서울특별시 구로구 항동 +1154551000 서울특별시 금천구 가산동 +1154561000 서울특별시 금천구 독산제1동 +1154562000 서울특별시 금천구 독산제2동 +1154563000 서울특별시 금천구 독산제3동 +1154564000 서울특별시 금천구 독산제4동 +1154567000 서울특별시 금천구 시흥제1동 +1154568000 서울특별시 금천구 시흥제2동 +1154569000 서울특별시 금천구 시흥제3동 +1154570000 서울특별시 금천구 시흥제4동 +1154571000 서울특별시 금천구 시흥제5동 +1156051500 서울특별시 영등포구 영등포본동 +1156053500 서울특별시 영등포구 영등포동 +1156054000 서울특별시 영등포구 여의동 +1156055000 서울특별시 영등포구 당산제1동 +1156056000 서울특별시 영등포구 당산제2동 +1156058500 서울특별시 영등포구 도림동 +1156060500 서울특별시 영등포구 문래동 +1156061000 서울특별시 영등포구 양평제1동 +1156062000 서울특별시 영등포구 양평제2동 +1156063000 서울특별시 영등포구 신길제1동 +1156065000 서울특별시 영등포구 신길제3동 +1156066000 서울특별시 영등포구 신길제4동 +1156067000 서울특별시 영등포구 신길제5동 +1156068000 서울특별시 영등포구 신길제6동 +1156069000 서울특별시 영등포구 신길제7동 +1156070000 서울특별시 영등포구 대림제1동 +1156071000 서울특별시 영등포구 대림제2동 +1156072000 서울특별시 영등포구 대림제3동 +1159051000 서울특별시 동작구 노량진제1동 +1159052000 서울특별시 동작구 노량진제2동 +1159053000 서울특별시 동작구 상도제1동 +1159054000 서울특별시 동작구 상도제2동 +1159055000 서울특별시 동작구 상도제3동 +1159056000 서울특별시 동작구 상도제4동 +1159060500 서울특별시 동작구 흑석동 +1159062000 서울특별시 동작구 사당제1동 +1159063000 서울특별시 동작구 사당제2동 +1159064000 서울특별시 동작구 사당제3동 +1159065000 서울특별시 동작구 사당제4동 +1159065100 서울특별시 동작구 사당제5동 +1159066000 서울특별시 동작구 대방동 +1159067000 서울특별시 동작구 신대방제1동 +1159068000 서울특별시 동작구 신대방제2동 +1162052500 서울특별시 관악구 보라매동 +1162054500 서울특별시 관악구 청림동 +1162056500 서울특별시 관악구 성현동 +1162057500 서울특별시 관악구 행운동 +1162058500 서울특별시 관악구 낙성대동 +1162059500 서울특별시 관악구 청룡동 +1162060500 서울특별시 관악구 은천동 +1162061500 서울특별시 관악구 중앙동 +1162062500 서울특별시 관악구 인헌동 +1162063000 서울특별시 관악구 남현동 +1162064500 서울특별시 관악구 서원동 +1162065500 서울특별시 관악구 신원동 +1162066500 서울특별시 관악구 서림동 +1162068500 서울특별시 관악구 신사동 +1162069500 서울특별시 관악구 신림동 +1162071500 서울특별시 관악구 난향동 +1162072500 서울특별시 관악구 조원동 +1162073500 서울특별시 관악구 대학동 +1162074500 서울특별시 관악구 삼성동 +1162076500 서울특별시 관악구 미성동 +1162077500 서울특별시 관악구 난곡동 +1165051000 서울특별시 서초구 서초1동 +1165052000 서울특별시 서초구 서초2동 +1165053000 서울특별시 서초구 서초3동 +1165053100 서울특별시 서초구 서초4동 +1165054000 서울특별시 서초구 잠원동 +1165055000 서울특별시 서초구 반포본동 +1165056000 서울특별시 서초구 반포1동 +1165057000 서울특별시 서초구 반포2동 +1165058000 서울특별시 서초구 반포3동 +1165058100 서울특별시 서초구 반포4동 +1165059000 서울특별시 서초구 방배본동 +1165060000 서울특별시 서초구 방배1동 +1165061000 서울특별시 서초구 방배2동 +1165062000 서울특별시 서초구 방배3동 +1165062100 서울특별시 서초구 방배4동 +1165065100 서울특별시 서초구 양재1동 +1165065200 서울특별시 서초구 양재2동 +1165066000 서울특별시 서초구 내곡동 +1168051000 서울특별시 강남구 신사동 +1168052100 서울특별시 강남구 논현1동 +1168053100 서울특별시 강남구 논현2동 +1168054500 서울특별시 강남구 압구정동 +1168056500 서울특별시 강남구 청담동 +1168058000 서울특별시 강남구 삼성1동 +1168059000 서울특별시 강남구 삼성2동 +1168060000 서울특별시 강남구 대치1동 +1168061000 서울특별시 강남구 대치2동 +1168063000 서울특별시 강남구 대치4동 +1168064000 서울특별시 강남구 역삼1동 +1168065000 서울특별시 강남구 역삼2동 +1168065500 서울특별시 강남구 도곡1동 +1168065600 서울특별시 강남구 도곡2동 +1168066000 서울특별시 강남구 개포1동 +1168067000 서울특별시 강남구 개포2동 +1168067500 서울특별시 강남구 개포3동 +1168069000 서울특별시 강남구 개포4동 +1168070000 서울특별시 강남구 세곡동 +1168072000 서울특별시 강남구 일원본동 +1168073000 서울특별시 강남구 일원1동 +1168075000 서울특별시 강남구 수서동 +1171051000 서울특별시 송파구 풍납1동 +1171052000 서울특별시 송파구 풍납2동 +1171053100 서울특별시 송파구 거여1동 +1171053200 서울특별시 송파구 거여2동 +1171054000 서울특별시 송파구 마천1동 +1171055000 서울특별시 송파구 마천2동 +1171056100 서울특별시 송파구 방이1동 +1171056200 서울특별시 송파구 방이2동 +1171056600 서울특별시 송파구 오륜동 +1171057000 서울특별시 송파구 오금동 +1171058000 서울특별시 송파구 송파1동 +1171059000 서울특별시 송파구 송파2동 +1171060000 서울특별시 송파구 석촌동 +1171061000 서울특별시 송파구 삼전동 +1171062000 서울특별시 송파구 가락본동 +1171063100 서울특별시 송파구 가락1동 +1171063200 서울특별시 송파구 가락2동 +1171064100 서울특별시 송파구 문정1동 +1171064200 서울특별시 송파구 문정2동 +1171064600 서울특별시 송파구 장지동 +1171064700 서울특별시 송파구 위례동 +1171065000 서울특별시 송파구 잠실본동 +1171067000 서울특별시 송파구 잠실2동 +1171068000 서울특별시 송파구 잠실3동 +1171069000 서울특별시 송파구 잠실4동 +1171071000 서울특별시 송파구 잠실6동 +1171072000 서울특별시 송파구 잠실7동 +1174051500 서울특별시 강동구 강일동 +1174052500 서울특별시 강동구 상일제1동 +1174052600 서울특별시 강동구 상일제2동 +1174053000 서울특별시 강동구 명일제1동 +1174054000 서울특별시 강동구 명일제2동 +1174055000 서울특별시 강동구 고덕제1동 +1174056000 서울특별시 강동구 고덕제2동 +1174057000 서울특별시 강동구 암사제1동 +1174058000 서울특별시 강동구 암사제2동 +1174059000 서울특별시 강동구 암사제3동 +1174060000 서울특별시 강동구 천호제1동 +1174061000 서울특별시 강동구 천호제2동 +1174062000 서울특별시 강동구 천호제3동 +1174064000 서울특별시 강동구 성내제1동 +1174065000 서울특별시 강동구 성내제2동 +1174066000 서울특별시 강동구 성내제3동 +1174068500 서울특별시 강동구 길동 +1174069000 서울특별시 강동구 둔촌제1동 +1174070000 서울특별시 강동구 둔촌제2동 +2611051000 부산광역시 중구 중앙동 +2611052000 부산광역시 중구 동광동 +2611053000 부산광역시 중구 대청동 +2611054500 부산광역시 중구 보수동 +2611056000 부산광역시 중구 부평동 +2611057000 부산광역시 중구 광복동 +2611058000 부산광역시 중구 남포동 +2611059000 부산광역시 중구 영주제1동 +2611060000 부산광역시 중구 영주제2동 +2614051000 부산광역시 서구 동대신제1동 +2614052000 부산광역시 서구 동대신제2동 +2614053000 부산광역시 서구 동대신제3동 +2614054000 부산광역시 서구 서대신제1동 +2614056000 부산광역시 서구 서대신제3동 +2614057000 부산광역시 서구 서대신제4동 +2614059000 부산광역시 서구 부민동 +2614061500 부산광역시 서구 아미동 +2614063000 부산광역시 서구 초장동 +2614064000 부산광역시 서구 충무동 +2614065000 부산광역시 서구 남부민제1동 +2614066000 부산광역시 서구 남부민제2동 +2614068000 부산광역시 서구 암남동 +2617051000 부산광역시 동구 초량제1동 +2617052000 부산광역시 동구 초량제2동 +2617053000 부산광역시 동구 초량제3동 +2617055000 부산광역시 동구 초량제6동 +2617056000 부산광역시 동구 수정제1동 +2617057000 부산광역시 동구 수정제2동 +2617059000 부산광역시 동구 수정제4동 +2617060000 부산광역시 동구 수정제5동 +2617064500 부산광역시 동구 좌천동 +2617065000 부산광역시 동구 범일제1동 +2617066000 부산광역시 동구 범일제2동 +2617068000 부산광역시 동구 범일제5동 +2620053000 부산광역시 영도구 남항동 +2620054000 부산광역시 영도구 영선제1동 +2620055000 부산광역시 영도구 영선제2동 +2620058500 부산광역시 영도구 신선동 +2620059000 부산광역시 영도구 봉래제1동 +2620060500 부산광역시 영도구 봉래제2동 +2620063000 부산광역시 영도구 청학제1동 +2620064000 부산광역시 영도구 청학제2동 +2620065000 부산광역시 영도구 동삼제1동 +2620066000 부산광역시 영도구 동삼제2동 +2620067000 부산광역시 영도구 동삼제3동 +2623051000 부산광역시 부산진구 부전제1동 +2623052000 부산광역시 부산진구 부전제2동 +2623054000 부산광역시 부산진구 연지동 +2623055000 부산광역시 부산진구 초읍동 +2623056000 부산광역시 부산진구 양정제1동 +2623057000 부산광역시 부산진구 양정제2동 +2623060000 부산광역시 부산진구 전포제1동 +2623061000 부산광역시 부산진구 전포제2동 +2623064000 부산광역시 부산진구 부암제1동 +2623066000 부산광역시 부산진구 부암제3동 +2623067000 부산광역시 부산진구 당감제1동 +2623068000 부산광역시 부산진구 당감제2동 +2623070000 부산광역시 부산진구 당감제4동 +2623071000 부산광역시 부산진구 가야제1동 +2623072000 부산광역시 부산진구 가야제2동 +2623074000 부산광역시 부산진구 개금제1동 +2623075000 부산광역시 부산진구 개금제2동 +2623076000 부산광역시 부산진구 개금제3동 +2623077000 부산광역시 부산진구 범천제1동 +2623078000 부산광역시 부산진구 범천제2동 +2626051000 부산광역시 동래구 수민동 +2626052000 부산광역시 동래구 복산동 +2626054500 부산광역시 동래구 명륜동 +2626055000 부산광역시 동래구 온천제1동 +2626056000 부산광역시 동래구 온천제2동 +2626057000 부산광역시 동래구 온천제3동 +2626058000 부산광역시 동래구 사직제1동 +2626059000 부산광역시 동래구 사직제2동 +2626060000 부산광역시 동래구 사직제3동 +2626074000 부산광역시 동래구 안락제1동 +2626075000 부산광역시 동래구 안락제2동 +2626076100 부산광역시 동래구 명장제1동 +2626076200 부산광역시 동래구 명장제2동 +2629051000 부산광역시 남구 대연제1동 +2629053000 부산광역시 남구 대연제3동 +2629054000 부산광역시 남구 대연제4동 +2629055000 부산광역시 남구 대연제5동 +2629056000 부산광역시 남구 대연제6동 +2629057000 부산광역시 남구 용호제1동 +2629058000 부산광역시 남구 용호제2동 +2629059000 부산광역시 남구 용호제3동 +2629060000 부산광역시 남구 용호제4동 +2629061000 부산광역시 남구 용당동 +2629062000 부산광역시 남구 감만제1동 +2629063000 부산광역시 남구 감만제2동 +2629064500 부산광역시 남구 우암동 +2629068000 부산광역시 남구 문현제1동 +2629069000 부산광역시 남구 문현제2동 +2629070000 부산광역시 남구 문현제3동 +2629071000 부산광역시 남구 문현제4동 +2632051000 부산광역시 북구 구포제1동 +2632052000 부산광역시 북구 구포제2동 +2632052100 부산광역시 북구 구포제3동 +2632053000 부산광역시 북구 금곡동 +2632054100 부산광역시 북구 화명제1동 +2632054200 부산광역시 북구 화명제2동 +2632054300 부산광역시 북구 화명제3동 +2632055000 부산광역시 북구 덕천제1동 +2632056000 부산광역시 북구 덕천제2동 +2632056100 부산광역시 북구 덕천제3동 +2632057100 부산광역시 북구 만덕제1동 +2632057200 부산광역시 북구 만덕제2동 +2632057300 부산광역시 북구 만덕제3동 +2635051000 부산광역시 해운대구 우제1동 +2635052000 부산광역시 해운대구 우제2동 +2635052500 부산광역시 해운대구 우제3동 +2635053000 부산광역시 해운대구 중제1동 +2635054000 부산광역시 해운대구 중제2동 +2635055100 부산광역시 해운대구 좌제1동 +2635055200 부산광역시 해운대구 좌제2동 +2635055300 부산광역시 해운대구 좌제3동 +2635055400 부산광역시 해운대구 좌제4동 +2635056000 부산광역시 해운대구 송정동 +2635057000 부산광역시 해운대구 반여제1동 +2635058000 부산광역시 해운대구 반여제2동 +2635059000 부산광역시 해운대구 반여제3동 +2635059500 부산광역시 해운대구 반여제4동 +2635061000 부산광역시 해운대구 반송제1동 +2635062000 부산광역시 해운대구 반송제2동 +2635065000 부산광역시 해운대구 재송제1동 +2635066000 부산광역시 해운대구 재송제2동 +2638051000 부산광역시 사하구 괴정제1동 +2638052000 부산광역시 사하구 괴정제2동 +2638053000 부산광역시 사하구 괴정제3동 +2638054000 부산광역시 사하구 괴정제4동 +2638055000 부산광역시 사하구 당리동 +2638056100 부산광역시 사하구 하단제1동 +2638056200 부산광역시 사하구 하단제2동 +2638057100 부산광역시 사하구 신평제1동 +2638057200 부산광역시 사하구 신평제2동 +2638058000 부산광역시 사하구 장림제1동 +2638059000 부산광역시 사하구 장림제2동 +2638060100 부산광역시 사하구 다대제1동 +2638060200 부산광역시 사하구 다대제2동 +2638061000 부산광역시 사하구 구평동 +2638062000 부산광역시 사하구 감천제1동 +2638063000 부산광역시 사하구 감천제2동 +2641051000 부산광역시 금정구 서제1동 +2641052000 부산광역시 금정구 서제2동 +2641053000 부산광역시 금정구 서제3동 +2641055500 부산광역시 금정구 금사회동동 +2641057000 부산광역시 금정구 부곡제1동 +2641058000 부산광역시 금정구 부곡제2동 +2641059000 부산광역시 금정구 부곡제3동 +2641059100 부산광역시 금정구 부곡제4동 +2641060000 부산광역시 금정구 장전제1동 +2641061000 부산광역시 금정구 장전제2동 +2641063500 부산광역시 금정구 선두구동 +2641066500 부산광역시 금정구 청룡노포동 +2641067000 부산광역시 금정구 남산동 +2641068000 부산광역시 금정구 구서제1동 +2641069000 부산광역시 금정구 구서제2동 +2641070000 부산광역시 금정구 금성동 +2644051000 부산광역시 강서구 대저1동 +2644052000 부산광역시 강서구 대저2동 +2644053000 부산광역시 강서구 강동동 +2644053500 부산광역시 강서구 명지1동 +2644054500 부산광역시 강서구 명지2동 +2644055000 부산광역시 강서구 가락동 +2644056000 부산광역시 강서구 녹산동 +2644058000 부산광역시 강서구 가덕도동 +2647061000 부산광역시 연제구 거제제1동 +2647062000 부산광역시 연제구 거제제2동 +2647063000 부산광역시 연제구 거제제3동 +2647064000 부산광역시 연제구 거제제4동 +2647065000 부산광역시 연제구 연산제1동 +2647066000 부산광역시 연제구 연산제2동 +2647067000 부산광역시 연제구 연산제3동 +2647068000 부산광역시 연제구 연산제4동 +2647069000 부산광역시 연제구 연산제5동 +2647070000 부산광역시 연제구 연산제6동 +2647072000 부산광역시 연제구 연산제8동 +2647073000 부산광역시 연제구 연산제9동 +2650066000 부산광역시 수영구 남천제1동 +2650067000 부산광역시 수영구 남천제2동 +2650073000 부산광역시 수영구 수영동 +2650074000 부산광역시 수영구 망미제1동 +2650075000 부산광역시 수영구 망미제2동 +2650076000 부산광역시 수영구 광안제1동 +2650077000 부산광역시 수영구 광안제2동 +2650078000 부산광역시 수영구 광안제3동 +2650079000 부산광역시 수영구 광안제4동 +2650080000 부산광역시 수영구 민락동 +2653058000 부산광역시 사상구 삼락동 +2653059100 부산광역시 사상구 모라제1동 +2653059300 부산광역시 사상구 모라제3동 +2653060000 부산광역시 사상구 덕포제1동 +2653061000 부산광역시 사상구 덕포제2동 +2653062000 부산광역시 사상구 괘법동 +2653064500 부산광역시 사상구 감전동 +2653065000 부산광역시 사상구 주례제1동 +2653066000 부산광역시 사상구 주례제2동 +2653066100 부산광역시 사상구 주례제3동 +2653067000 부산광역시 사상구 학장동 +2653068000 부산광역시 사상구 엄궁동 +2671025000 부산광역시 기장군 기장읍 +2671025300 부산광역시 기장군 장안읍 +2671025600 부산광역시 기장군 정관읍 +2671025900 부산광역시 기장군 일광읍 +2671033000 부산광역시 기장군 철마면 +2711051700 대구광역시 중구 동인동 +2711054500 대구광역시 중구 삼덕동 +2711056500 대구광역시 중구 성내1동 +2711057500 대구광역시 중구 성내2동 +2711058500 대구광역시 중구 성내3동 +2711059500 대구광역시 중구 대신동 +2711064000 대구광역시 중구 남산1동 +2711065000 대구광역시 중구 남산2동 +2711066000 대구광역시 중구 남산3동 +2711067000 대구광역시 중구 남산4동 +2711068000 대구광역시 중구 대봉1동 +2711069000 대구광역시 중구 대봉2동 +2714051000 대구광역시 동구 신암1동 +2714052000 대구광역시 동구 신암2동 +2714053000 대구광역시 동구 신암3동 +2714054000 대구광역시 동구 신암4동 +2714054100 대구광역시 동구 신암5동 +2714055500 대구광역시 동구 신천1.2동 +2714057000 대구광역시 동구 신천3동 +2714058000 대구광역시 동구 신천4동 +2714059000 대구광역시 동구 효목1동 +2714060000 대구광역시 동구 효목2동 +2714061500 대구광역시 동구 도평동 +2714062000 대구광역시 동구 불로.봉무동 +2714064000 대구광역시 동구 지저동 +2714065500 대구광역시 동구 동촌동 +2714067000 대구광역시 동구 방촌동 +2714068500 대구광역시 동구 해안동 +2714072000 대구광역시 동구 안심1동 +2714073000 대구광역시 동구 안심2동 +2714074200 대구광역시 동구 안심3동 +2714074700 대구광역시 동구 안심4동 +2714075500 대구광역시 동구 혁신동 +2714076000 대구광역시 동구 공산동 +2717051000 대구광역시 서구 내당1동 +2717052500 대구광역시 서구 내당2.3동 +2717054000 대구광역시 서구 내당4동 +2717055000 대구광역시 서구 비산1동 +2717056500 대구광역시 서구 비산2.3동 +2717058000 대구광역시 서구 비산4동 +2717059000 대구광역시 서구 비산5동 +2717060000 대구광역시 서구 비산6동 +2717061000 대구광역시 서구 비산7동 +2717062000 대구광역시 서구 평리1동 +2717063000 대구광역시 서구 평리2동 +2717064000 대구광역시 서구 평리3동 +2717065000 대구광역시 서구 평리4동 +2717066000 대구광역시 서구 평리5동 +2717066100 대구광역시 서구 평리6동 +2717067500 대구광역시 서구 상중이동 +2717069500 대구광역시 서구 원대동 +2720051500 대구광역시 남구 이천동 +2720053000 대구광역시 남구 봉덕1동 +2720054000 대구광역시 남구 봉덕2동 +2720055000 대구광역시 남구 봉덕3동 +2720056000 대구광역시 남구 대명1동 +2720057100 대구광역시 남구 대명2동 +2720058600 대구광역시 남구 대명3동 +2720059000 대구광역시 남구 대명4동 +2720060000 대구광역시 남구 대명5동 +2720061000 대구광역시 남구 대명6동 +2720064000 대구광역시 남구 대명9동 +2720065000 대구광역시 남구 대명10동 +2720066000 대구광역시 남구 대명11동 +2723051000 대구광역시 북구 고성동 +2723052600 대구광역시 북구 칠성동 +2723055000 대구광역시 북구 침산1동 +2723056000 대구광역시 북구 침산2동 +2723057000 대구광역시 북구 침산3동 +2723061000 대구광역시 북구 산격1동 +2723062000 대구광역시 북구 산격2동 +2723063000 대구광역시 북구 산격3동 +2723063100 대구광역시 북구 산격4동 +2723064500 대구광역시 북구 대현동 +2723067100 대구광역시 북구 복현1동 +2723067200 대구광역시 북구 복현2동 +2723068000 대구광역시 북구 검단동 +2723069500 대구광역시 북구 무태조야동 +2723071500 대구광역시 북구 관문동 +2723072500 대구광역시 북구 태전1동 +2723073500 대구광역시 북구 태전2동 +2723074500 대구광역시 북구 구암동 +2723075000 대구광역시 북구 관음동 +2723077000 대구광역시 북구 읍내동 +2723078000 대구광역시 북구 동천동 +2723079000 대구광역시 북구 노원동 +2723080000 대구광역시 북구 국우동 +2726051000 대구광역시 수성구 범어1동 +2726052000 대구광역시 수성구 범어2동 +2726053000 대구광역시 수성구 범어3동 +2726054000 대구광역시 수성구 범어4동 +2726055000 대구광역시 수성구 만촌1동 +2726056000 대구광역시 수성구 만촌2동 +2726056100 대구광역시 수성구 만촌3동 +2726057000 대구광역시 수성구 수성1가동 +2726058000 대구광역시 수성구 수성2.3가동 +2726059000 대구광역시 수성구 수성4가동 +2726060100 대구광역시 수성구 황금1동 +2726060200 대구광역시 수성구 황금2동 +2726061000 대구광역시 수성구 중동 +2726062000 대구광역시 수성구 상동 +2726063000 대구광역시 수성구 파동 +2726064000 대구광역시 수성구 두산동 +2726065100 대구광역시 수성구 지산1동 +2726065200 대구광역시 수성구 지산2동 +2726066100 대구광역시 수성구 범물1동 +2726066200 대구광역시 수성구 범물2동 +2726067000 대구광역시 수성구 고산1동 +2726068000 대구광역시 수성구 고산2동 +2726069000 대구광역시 수성구 고산3동 +2729051500 대구광역시 달서구 성당동 +2729053500 대구광역시 달서구 두류1.2동 +2729055000 대구광역시 달서구 두류3동 +2729055500 대구광역시 달서구 감삼동 +2729056300 대구광역시 달서구 죽전동 +2729056800 대구광역시 달서구 장기동 +2729057100 대구광역시 달서구 용산1동 +2729057200 대구광역시 달서구 용산2동 +2729057600 대구광역시 달서구 이곡1동 +2729057700 대구광역시 달서구 이곡2동 +2729058500 대구광역시 달서구 신당동 +2729059000 대구광역시 달서구 본리동 +2729060100 대구광역시 달서구 월성1동 +2729060200 대구광역시 달서구 월성2동 +2729061500 대구광역시 달서구 진천동 +2729061700 대구광역시 달서구 유천동 +2729062400 대구광역시 달서구 상인1동 +2729062500 대구광역시 달서구 상인2동 +2729062600 대구광역시 달서구 상인3동 +2729062800 대구광역시 달서구 도원동 +2729063000 대구광역시 달서구 송현1동 +2729064000 대구광역시 달서구 송현2동 +2729065000 대구광역시 달서구 본동 +2771025000 대구광역시 달성군 화원읍 +2771025300 대구광역시 달성군 논공읍 +2771025400 대구광역시 달성군 논공읍공단출장소 +2771025600 대구광역시 달성군 다사읍 +2771025700 대구광역시 달성군 다사읍서재출장소 +2771025900 대구광역시 달성군 유가읍 +2771026200 대구광역시 달성군 옥포읍 +2771026500 대구광역시 달성군 현풍읍 +2771031000 대구광역시 달성군 가창면 +2771033000 대구광역시 달성군 하빈면 +2771038000 대구광역시 달성군 구지면 +2772025000 대구광역시 군위군 군위읍 +2772031000 대구광역시 군위군 소보면 +2772032000 대구광역시 군위군 효령면 +2772033000 대구광역시 군위군 부계면 +2772034000 대구광역시 군위군 우보면 +2772035000 대구광역시 군위군 의흥면 +2772036000 대구광역시 군위군 산성면 +2772037000 대구광역시 군위군 삼국유사면 +2811052000 인천광역시 중구 연안동 +2811053000 인천광역시 중구 신포동 +2811054000 인천광역시 중구 신흥동 +2811056000 인천광역시 중구 도원동 +2811057000 인천광역시 중구 율목동 +2811058500 인천광역시 중구 동인천동 +2811061500 인천광역시 중구 개항동 +2811062000 인천광역시 중구 영종동 +2811062200 인천광역시 중구 영종1동 +2811062800 인천광역시 중구 운서동 +2811063000 인천광역시 중구 용유동 +2814051000 인천광역시 동구 만석동 +2814052500 인천광역시 동구 화수1.화평동 +2814053000 인천광역시 동구 화수2동 +2814055500 인천광역시 동구 송현1.2동 +2814057000 인천광역시 동구 송현3동 +2814058000 인천광역시 동구 송림1동 +2814059000 인천광역시 동구 송림2동 +2814060500 인천광역시 동구 송림3.5동 +2814061000 인천광역시 동구 송림4동 +2814063000 인천광역시 동구 송림6동 +2814064000 인천광역시 동구 금창동 +2817751000 인천광역시 미추홀구 숭의2동 +2817752000 인천광역시 미추홀구 숭의1.3동 +2817753000 인천광역시 미추홀구 숭의4동 +2817754000 인천광역시 미추홀구 용현1.4동 +2817755000 인천광역시 미추홀구 용현2동 +2817756000 인천광역시 미추홀구 용현3동 +2817757000 인천광역시 미추홀구 용현5동 +2817758000 인천광역시 미추홀구 학익1동 +2817759000 인천광역시 미추홀구 학익2동 +2817760000 인천광역시 미추홀구 도화1동 +2817761000 인천광역시 미추홀구 도화2.3동 +2817762000 인천광역시 미추홀구 주안1동 +2817763000 인천광역시 미추홀구 주안2동 +2817764000 인천광역시 미추홀구 주안3동 +2817765000 인천광역시 미추홀구 주안4동 +2817766000 인천광역시 미추홀구 주안5동 +2817767000 인천광역시 미추홀구 주안6동 +2817768000 인천광역시 미추홀구 주안7동 +2817769000 인천광역시 미추홀구 주안8동 +2817770000 인천광역시 미추홀구 관교동 +2817771000 인천광역시 미추홀구 문학동 +2818563000 인천광역시 연수구 옥련1동 +2818564000 인천광역시 연수구 옥련2동 +2818575000 인천광역시 연수구 선학동 +2818576100 인천광역시 연수구 연수1동 +2818576200 인천광역시 연수구 연수2동 +2818576300 인천광역시 연수구 연수3동 +2818576600 인천광역시 연수구 청학동 +2818578000 인천광역시 연수구 동춘1동 +2818579000 인천광역시 연수구 동춘2동 +2818579500 인천광역시 연수구 동춘3동 +2818582000 인천광역시 연수구 송도1동 +2818583000 인천광역시 연수구 송도2동 +2818584000 인천광역시 연수구 송도3동 +2818585000 인천광역시 연수구 송도4동 +2818586000 인천광역시 연수구 송도5동 +2820051000 인천광역시 남동구 구월1동 +2820052000 인천광역시 남동구 구월2동 +2820052100 인천광역시 남동구 구월3동 +2820052200 인천광역시 남동구 구월4동 +2820053000 인천광역시 남동구 간석1동 +2820054000 인천광역시 남동구 간석2동 +2820055000 인천광역시 남동구 간석3동 +2820055100 인천광역시 남동구 간석4동 +2820056000 인천광역시 남동구 만수1동 +2820057000 인천광역시 남동구 만수2동 +2820058000 인천광역시 남동구 만수3동 +2820058100 인천광역시 남동구 만수4동 +2820058200 인천광역시 남동구 만수5동 +2820058300 인천광역시 남동구 만수6동 +2820065000 인천광역시 남동구 장수서창동 +2820065500 인천광역시 남동구 서창2동 +2820066000 인천광역시 남동구 남촌도림동 +2820069000 인천광역시 남동구 논현1동 +2820070000 인천광역시 남동구 논현2동 +2820071000 인천광역시 남동구 논현고잔동 +2823751000 인천광역시 부평구 부평1동 +2823752000 인천광역시 부평구 부평2동 +2823753000 인천광역시 부평구 부평3동 +2823754000 인천광역시 부평구 부평4동 +2823755000 인천광역시 부평구 부평5동 +2823756000 인천광역시 부평구 부평6동 +2823757000 인천광역시 부평구 산곡1동 +2823758000 인천광역시 부평구 산곡2동 +2823758100 인천광역시 부평구 산곡3동 +2823758200 인천광역시 부평구 산곡4동 +2823759100 인천광역시 부평구 청천1동 +2823759200 인천광역시 부평구 청천2동 +2823764100 인천광역시 부평구 갈산1동 +2823764200 인천광역시 부평구 갈산2동 +2823764600 인천광역시 부평구 삼산1동 +2823764800 인천광역시 부평구 삼산2동 +2823765000 인천광역시 부평구 부개1동 +2823766000 인천광역시 부평구 부개2동 +2823766100 인천광역시 부평구 부개3동 +2823767000 인천광역시 부평구 일신동 +2823768000 인천광역시 부평구 십정1동 +2823769000 인천광역시 부평구 십정2동 +2824560100 인천광역시 계양구 효성1동 +2824560200 인천광역시 계양구 효성2동 +2824561100 인천광역시 계양구 계산1동 +2824561200 인천광역시 계양구 계산2동 +2824561300 인천광역시 계양구 계산3동 +2824561400 인천광역시 계양구 계산4동 +2824562100 인천광역시 계양구 작전1동 +2824562200 인천광역시 계양구 작전2동 +2824564000 인천광역시 계양구 작전서운동 +2824571000 인천광역시 계양구 계양1동 +2824572000 인천광역시 계양구 계양2동 +2824573000 인천광역시 계양구 계양3동 +2826051500 인천광역시 서구 검암경서동 +2826053000 인천광역시 서구 연희동 +2826053600 인천광역시 서구 청라1동 +2826053700 인천광역시 서구 청라2동 +2826053900 인천광역시 서구 청라3동 +2826054200 인천광역시 서구 가정1동 +2826054300 인천광역시 서구 가정2동 +2826054400 인천광역시 서구 가정3동 +2826055000 인천광역시 서구 석남1동 +2826056000 인천광역시 서구 석남2동 +2826056100 인천광역시 서구 석남3동 +2826057500 인천광역시 서구 신현원창동 +2826058000 인천광역시 서구 가좌1동 +2826059000 인천광역시 서구 가좌2동 +2826060000 인천광역시 서구 가좌3동 +2826061000 인천광역시 서구 가좌4동 +2826068000 인천광역시 서구 검단동 +2826069000 인천광역시 서구 불로대곡동 +2826070000 인천광역시 서구 원당동 +2826071000 인천광역시 서구 당하동 +2826072000 인천광역시 서구 오류왕길동 +2826073000 인천광역시 서구 마전동 +2826074000 인천광역시 서구 아라동 +2871025000 인천광역시 강화군 강화읍 +2871031000 인천광역시 강화군 선원면 +2871032000 인천광역시 강화군 불은면 +2871033000 인천광역시 강화군 길상면 +2871034000 인천광역시 강화군 화도면 +2871035000 인천광역시 강화군 양도면 +2871036000 인천광역시 강화군 내가면 +2871037000 인천광역시 강화군 하점면 +2871038000 인천광역시 강화군 양사면 +2871039000 인천광역시 강화군 송해면 +2871040000 인천광역시 강화군 교동면 +2871041000 인천광역시 강화군 삼산면 +2871042000 인천광역시 강화군 서도면 +2871042500 인천광역시 강화군 서도면볼음출장소 +2872031000 인천광역시 옹진군 북도면 +2872031500 인천광역시 옹진군 북도면장봉출장소 +2872033000 인천광역시 옹진군 백령면 +2872034000 인천광역시 옹진군 대청면 +2872034500 인천광역시 옹진군 대청면소청출장소 +2872035000 인천광역시 옹진군 덕적면 +2872036000 인천광역시 옹진군 영흥면 +2872037000 인천광역시 옹진군 자월면 +2872037500 인천광역시 옹진군 자월면이작출장소 +2872038000 인천광역시 옹진군 연평면 +2911052500 광주광역시 동구 충장동 +2911054500 광주광역시 동구 동명동 +2911056000 광주광역시 동구 계림1동 +2911057000 광주광역시 동구 계림2동 +2911059000 광주광역시 동구 산수1동 +2911060000 광주광역시 동구 산수2동 +2911062000 광주광역시 동구 지산1동 +2911063000 광주광역시 동구 지산2동 +2911065500 광주광역시 동구 서남동 +2911068500 광주광역시 동구 학동 +2911071000 광주광역시 동구 학운동 +2911073000 광주광역시 동구 지원1동 +2911074000 광주광역시 동구 지원2동 +2914057500 광주광역시 서구 양동 +2914059000 광주광역시 서구 양3동 +2914065000 광주광역시 서구 농성1동 +2914066000 광주광역시 서구 농성2동 +2914073000 광주광역시 서구 광천동 +2914074000 광주광역시 서구 유덕동 +2914074500 광주광역시 서구 치평동 +2914075100 광주광역시 서구 상무1동 +2914075200 광주광역시 서구 상무2동 +2914076000 광주광역시 서구 화정1동 +2914077000 광주광역시 서구 화정2동 +2914078000 광주광역시 서구 화정3동 +2914079000 광주광역시 서구 화정4동 +2914080000 광주광역시 서구 서창동 +2914082100 광주광역시 서구 금호1동 +2914082400 광주광역시 서구 금호2동 +2914083000 광주광역시 서구 풍암동 +2914084000 광주광역시 서구 동천동 +2915551000 광주광역시 남구 양림동 +2915552000 광주광역시 남구 방림1동 +2915553000 광주광역시 남구 방림2동 +2915553700 광주광역시 남구 봉선1동 +2915553800 광주광역시 남구 봉선2동 +2915554500 광주광역시 남구 사직동 +2915560500 광주광역시 남구 월산동 +2915563000 광주광역시 남구 월산4동 +2915564000 광주광역시 남구 월산5동 +2915567000 광주광역시 남구 백운1동 +2915568000 광주광역시 남구 백운2동 +2915569000 광주광역시 남구 주월1동 +2915570000 광주광역시 남구 주월2동 +2915570500 광주광역시 남구 진월동 +2915571000 광주광역시 남구 효덕동 +2915572000 광주광역시 남구 송암동 +2915573000 광주광역시 남구 대촌동 +2917051000 광주광역시 북구 중흥1동 +2917052000 광주광역시 북구 중흥2동 +2917053000 광주광역시 북구 중흥3동 +2917055500 광주광역시 북구 중앙동 +2917057000 광주광역시 북구 임동 +2917058000 광주광역시 북구 신안동 +2917059000 광주광역시 북구 용봉동 +2917060100 광주광역시 북구 운암1동 +2917060200 광주광역시 북구 운암2동 +2917060300 광주광역시 북구 운암3동 +2917061500 광주광역시 북구 동림동 +2917062000 광주광역시 북구 우산동 +2917063500 광주광역시 북구 풍향동 +2917065000 광주광역시 북구 문화동 +2917065600 광주광역시 북구 문흥1동 +2917065700 광주광역시 북구 문흥2동 +2917066100 광주광역시 북구 두암1동 +2917066200 광주광역시 북구 두암2동 +2917066300 광주광역시 북구 두암3동 +2917066600 광주광역시 북구 삼각동 +2917066900 광주광역시 북구 일곡동 +2917067300 광주광역시 북구 매곡동 +2917067600 광주광역시 북구 오치1동 +2917067700 광주광역시 북구 오치2동 +2917068500 광주광역시 북구 석곡동 +2917069500 광주광역시 북구 건국동 +2917069600 광주광역시 북구 양산동 +2917069700 광주광역시 북구 신용동 +2920051500 광주광역시 광산구 송정1동 +2920052500 광주광역시 광산구 송정2동 +2920054000 광주광역시 광산구 도산동 +2920055000 광주광역시 광산구 신흥동 +2920056500 광주광역시 광산구 어룡동 +2920058000 광주광역시 광산구 우산동 +2920060000 광주광역시 광산구 월곡1동 +2920061000 광주광역시 광산구 월곡2동 +2920062000 광주광역시 광산구 비아동 +2920062400 광주광역시 광산구 첨단1동 +2920062600 광주광역시 광산구 첨단2동 +2920063000 광주광역시 광산구 신가동 +2920063500 광주광역시 광산구 운남동 +2920063700 광주광역시 광산구 수완동 +2920064000 광주광역시 광산구 하남동 +2920065000 광주광역시 광산구 임곡동 +2920066000 광주광역시 광산구 동곡동 +2920067000 광주광역시 광산구 평동 +2920068000 광주광역시 광산구 삼도동 +2920069000 광주광역시 광산구 본량동 +2920070000 광주광역시 광산구 신창동 +3011051500 대전광역시 동구 중앙동 +3011053000 대전광역시 동구 효동 +3011054500 대전광역시 동구 신인동 +3011055100 대전광역시 동구 판암1동 +3011055200 대전광역시 동구 판암2동 +3011056000 대전광역시 동구 용운동 +3011058500 대전광역시 동구 대동 +3011059000 대전광역시 동구 자양동 +3011062000 대전광역시 동구 가양1동 +3011063000 대전광역시 동구 가양2동 +3011064000 대전광역시 동구 용전동 +3011066500 대전광역시 동구 성남동 +3011067000 대전광역시 동구 홍도동 +3011069500 대전광역시 동구 삼성동 +3011072500 대전광역시 동구 대청동 +3011074000 대전광역시 동구 산내동 +3014053500 대전광역시 중구 은행선화동 +3014055000 대전광역시 중구 목동 +3014056000 대전광역시 중구 중촌동 +3014057500 대전광역시 중구 대흥동 +3014060500 대전광역시 중구 문창동 +3014062000 대전광역시 중구 석교동 +3014063000 대전광역시 중구 대사동 +3014064000 대전광역시 중구 부사동 +3014065500 대전광역시 중구 용두동 +3014067000 대전광역시 중구 오류동 +3014068000 대전광역시 중구 태평1동 +3014069000 대전광역시 중구 태평2동 +3014070000 대전광역시 중구 유천1동 +3014071000 대전광역시 중구 유천2동 +3014072000 대전광역시 중구 문화1동 +3014073000 대전광역시 중구 문화2동 +3014074000 대전광역시 중구 산성동 +3017051000 대전광역시 서구 복수동 +3017052000 대전광역시 서구 도마1동 +3017053000 대전광역시 서구 도마2동 +3017053500 대전광역시 서구 정림동 +3017054000 대전광역시 서구 변동 +3017055000 대전광역시 서구 용문동 +3017055500 대전광역시 서구 탄방동 +3017056000 대전광역시 서구 괴정동 +3017057000 대전광역시 서구 가장동 +3017057500 대전광역시 서구 내동 +3017058100 대전광역시 서구 갈마1동 +3017058200 대전광역시 서구 갈마2동 +3017058600 대전광역시 서구 월평1동 +3017058700 대전광역시 서구 월평2동 +3017058800 대전광역시 서구 월평3동 +3017059000 대전광역시 서구 가수원동 +3017059300 대전광역시 서구 도안동 +3017059600 대전광역시 서구 관저1동 +3017059700 대전광역시 서구 관저2동 +3017060000 대전광역시 서구 기성동 +3017063000 대전광역시 서구 둔산1동 +3017064000 대전광역시 서구 둔산2동 +3017065000 대전광역시 서구 만년동 +3017066000 대전광역시 서구 둔산3동 +3020052000 대전광역시 유성구 진잠동 +3020052600 대전광역시 유성구 학하동 +3020052700 대전광역시 유성구 상대동 +3020053000 대전광역시 유성구 온천1동 +3020054000 대전광역시 유성구 온천2동 +3020054600 대전광역시 유성구 노은1동 +3020054700 대전광역시 유성구 노은2동 +3020054800 대전광역시 유성구 노은3동 +3020055000 대전광역시 유성구 신성동 +3020057000 대전광역시 유성구 전민동 +3020058000 대전광역시 유성구 구즉동 +3020060000 대전광역시 유성구 관평동 +3020061000 대전광역시 유성구 원신흥동 +3023051000 대전광역시 대덕구 오정동 +3023052000 대전광역시 대덕구 대화동 +3023052500 대전광역시 대덕구 회덕동 +3023053300 대전광역시 대덕구 비래동 +3023054300 대전광역시 대덕구 송촌동 +3023054600 대전광역시 대덕구 중리동 +3023055000 대전광역시 대덕구 신탄진동 +3023056000 대전광역시 대덕구 석봉동 +3023057000 대전광역시 대덕구 덕암동 +3023058000 대전광역시 대덕구 목상동 +3023060000 대전광역시 대덕구 법1동 +3023061000 대전광역시 대덕구 법2동 +3111051000 울산광역시 중구 학성동 +3111052000 울산광역시 중구 반구1동 +3111053000 울산광역시 중구 반구2동 +3111054000 울산광역시 중구 복산1동 +3111055000 울산광역시 중구 복산2동 +3111058500 울산광역시 중구 중앙동 +3111059000 울산광역시 중구 우정동 +3111060000 울산광역시 중구 태화동 +3111061000 울산광역시 중구 다운동 +3111062000 울산광역시 중구 병영1동 +3111063000 울산광역시 중구 병영2동 +3111064000 울산광역시 중구 약사동 +3111065000 울산광역시 중구 성안동 +3114051000 울산광역시 남구 신정1동 +3114052000 울산광역시 남구 신정2동 +3114053000 울산광역시 남구 신정3동 +3114054000 울산광역시 남구 신정4동 +3114055000 울산광역시 남구 신정5동 +3114056000 울산광역시 남구 달동 +3114057000 울산광역시 남구 삼산동 +3114058500 울산광역시 남구 삼호동 +3114059500 울산광역시 남구 무거동 +3114060000 울산광역시 남구 옥동 +3114062500 울산광역시 남구 대현동 +3114063500 울산광역시 남구 수암동 +3114064000 울산광역시 남구 선암동 +3114067000 울산광역시 남구 야음장생포동 +3117051000 울산광역시 동구 방어동 +3117052000 울산광역시 동구 일산동 +3117053000 울산광역시 동구 화정동 +3117054000 울산광역시 동구 대송동 +3117055000 울산광역시 동구 전하1동 +3117056000 울산광역시 동구 전하2동 +3117058000 울산광역시 동구 남목1동 +3117059000 울산광역시 동구 남목2동 +3117060000 울산광역시 동구 남목3동 +3120051000 울산광역시 북구 농소1동 +3120052000 울산광역시 북구 농소2동 +3120053000 울산광역시 북구 농소3동 +3120054000 울산광역시 북구 강동동 +3120056000 울산광역시 북구 효문동 +3120057000 울산광역시 북구 송정동 +3120058000 울산광역시 북구 양정동 +3120059000 울산광역시 북구 염포동 +3171025000 울산광역시 울주군 온산읍 +3171025300 울산광역시 울주군 언양읍 +3171025600 울산광역시 울주군 온양읍 +3171025900 울산광역시 울주군 범서읍 +3171026200 울산광역시 울주군 청량읍 +3171026500 울산광역시 울주군 삼남읍 +3171031000 울산광역시 울주군 서생면 +3171034000 울산광역시 울주군 웅촌면 +3171036000 울산광역시 울주군 두동면 +3171037000 울산광역시 울주군 두서면 +3171038000 울산광역시 울주군 상북면 +3171040000 울산광역시 울주군 삼동면 +4111156000 경기도 수원시 장안구 파장동 +4111156600 경기도 수원시 장안구 율천동 +4111157100 경기도 수원시 장안구 정자1동 +4111157200 경기도 수원시 장안구 정자2동 +4111157300 경기도 수원시 장안구 정자3동 +4111158000 경기도 수원시 장안구 영화동 +4111159100 경기도 수원시 장안구 송죽동 +4111159700 경기도 수원시 장안구 조원1동 +4111159800 경기도 수원시 장안구 조원2동 +4111160000 경기도 수원시 장안구 연무동 +4111352000 경기도 수원시 권선구 세류1동 +4111353000 경기도 수원시 권선구 세류2동 +4111354000 경기도 수원시 권선구 세류3동 +4111355000 경기도 수원시 권선구 평동 +4111356000 경기도 수원시 권선구 서둔동 +4111365000 경기도 수원시 권선구 구운동 +4111366200 경기도 수원시 권선구 금곡동 +4111366400 경기도 수원시 권선구 호매실동 +4111367000 경기도 수원시 권선구 권선1동 +4111368000 경기도 수원시 권선구 권선2동 +4111369000 경기도 수원시 권선구 곡선동 +4111370000 경기도 수원시 권선구 입북동 +4111565000 경기도 수원시 팔달구 매교동 +4111566000 경기도 수원시 팔달구 매산동 +4111567000 경기도 수원시 팔달구 고등동 +4111568000 경기도 수원시 팔달구 화서1동 +4111569000 경기도 수원시 팔달구 화서2동 +4111570000 경기도 수원시 팔달구 지동 +4111571000 경기도 수원시 팔달구 우만1동 +4111572000 경기도 수원시 팔달구 우만2동 +4111573000 경기도 수원시 팔달구 인계동 +4111574000 경기도 수원시 팔달구 행궁동 +4111751000 경기도 수원시 영통구 매탄1동 +4111752000 경기도 수원시 영통구 매탄2동 +4111753000 경기도 수원시 영통구 매탄3동 +4111754000 경기도 수원시 영통구 매탄4동 +4111755000 경기도 수원시 영통구 원천동 +4111757000 경기도 수원시 영통구 영통1동 +4111758000 경기도 수원시 영통구 영통2동 +4111758500 경기도 수원시 영통구 영통3동 +4111759300 경기도 수원시 영통구 망포1동 +4111759600 경기도 수원시 영통구 망포2동 +4111760000 경기도 수원시 영통구 광교1동 +4111761000 경기도 수원시 영통구 광교2동 +4113151000 경기도 성남시 수정구 신흥1동 +4113152000 경기도 성남시 수정구 신흥2동 +4113153000 경기도 성남시 수정구 신흥3동 +4113154000 경기도 성남시 수정구 태평1동 +4113155000 경기도 성남시 수정구 태평2동 +4113156000 경기도 성남시 수정구 태평3동 +4113156100 경기도 성남시 수정구 태평4동 +4113157000 경기도 성남시 수정구 수진1동 +4113158000 경기도 성남시 수정구 수진2동 +4113159000 경기도 성남시 수정구 단대동 +4113160000 경기도 성남시 수정구 산성동 +4113161000 경기도 성남시 수정구 양지동 +4113162000 경기도 성남시 수정구 복정동 +4113162500 경기도 성남시 수정구 위례동 +4113163000 경기도 성남시 수정구 신촌동 +4113164000 경기도 성남시 수정구 고등동 +4113165000 경기도 성남시 수정구 시흥동 +4113351000 경기도 성남시 중원구 성남동 +4113352500 경기도 성남시 중원구 중앙동 +4113353000 경기도 성남시 중원구 금광1동 +4113354000 경기도 성남시 중원구 금광2동 +4113355000 경기도 성남시 중원구 은행1동 +4113356000 경기도 성남시 중원구 은행2동 +4113357000 경기도 성남시 중원구 상대원1동 +4113358000 경기도 성남시 중원구 상대원2동 +4113359000 경기도 성남시 중원구 상대원3동 +4113366000 경기도 성남시 중원구 하대원동 +4113367000 경기도 성남시 중원구 도촌동 +4113551000 경기도 성남시 분당구 분당동 +4113552000 경기도 성남시 분당구 수내1동 +4113553000 경기도 성남시 분당구 수내2동 +4113554000 경기도 성남시 분당구 수내3동 +4113554500 경기도 성남시 분당구 정자동 +4113555000 경기도 성남시 분당구 정자1동 +4113556000 경기도 성남시 분당구 정자2동 +4113557000 경기도 성남시 분당구 정자3동 +4113558000 경기도 성남시 분당구 서현1동 +4113559000 경기도 성남시 분당구 서현2동 +4113560000 경기도 성남시 분당구 이매1동 +4113561000 경기도 성남시 분당구 이매2동 +4113562000 경기도 성남시 분당구 야탑1동 +4113563000 경기도 성남시 분당구 야탑2동 +4113564000 경기도 성남시 분당구 야탑3동 +4113565000 경기도 성남시 분당구 판교동 +4113565500 경기도 성남시 분당구 삼평동 +4113565700 경기도 성남시 분당구 백현동 +4113566200 경기도 성남시 분당구 금곡동 +4113566500 경기도 성남시 분당구 구미1동 +4113567000 경기도 성남시 분당구 구미동 +4113568000 경기도 성남시 분당구 운중동 +4115051000 경기도 의정부시 의정부1동 +4115052000 경기도 의정부시 의정부2동 +4115054500 경기도 의정부시 호원1동 +4115055500 경기도 의정부시 호원2동 +4115056100 경기도 의정부시 장암동 +4115056700 경기도 의정부시 신곡1동 +4115056800 경기도 의정부시 신곡2동 +4115057300 경기도 의정부시 송산1동 +4115057600 경기도 의정부시 송산2동 +4115057800 경기도 의정부시 송산3동 +4115058000 경기도 의정부시 자금동 +4115059500 경기도 의정부시 가능동 +4115061500 경기도 의정부시 흥선동 +4115062000 경기도 의정부시 녹양동 +4117151000 경기도 안양시 만안구 안양1동 +4117152000 경기도 안양시 만안구 안양2동 +4117153000 경기도 안양시 만안구 안양3동 +4117154000 경기도 안양시 만안구 안양4동 +4117155000 경기도 안양시 만안구 안양5동 +4117156000 경기도 안양시 만안구 안양6동 +4117157000 경기도 안양시 만안구 안양7동 +4117158000 경기도 안양시 만안구 안양8동 +4117158100 경기도 안양시 만안구 안양9동 +4117159000 경기도 안양시 만안구 석수1동 +4117160000 경기도 안양시 만안구 석수2동 +4117161000 경기도 안양시 만안구 석수3동 +4117162100 경기도 안양시 만안구 박달1동 +4117163000 경기도 안양시 만안구 박달2동 +4117351000 경기도 안양시 동안구 비산1동 +4117352000 경기도 안양시 동안구 비산2동 +4117353000 경기도 안양시 동안구 비산3동 +4117354000 경기도 안양시 동안구 부흥동 +4117354600 경기도 안양시 동안구 달안동 +4117355000 경기도 안양시 동안구 관양1동 +4117356000 경기도 안양시 동안구 관양2동 +4117356600 경기도 안양시 동안구 부림동 +4117357000 경기도 안양시 동안구 평촌동 +4117357600 경기도 안양시 동안구 평안동 +4117357800 경기도 안양시 동안구 귀인동 +4117358000 경기도 안양시 동안구 호계1동 +4117359000 경기도 안양시 동안구 호계2동 +4117360000 경기도 안양시 동안구 호계3동 +4117361000 경기도 안양시 동안구 범계동 +4117362000 경기도 안양시 동안구 신촌동 +4117363000 경기도 안양시 동안구 갈산동 +4119060300 경기도 부천시 심곡동 +4119060600 경기도 부천시 부천동 +4119061000 경기도 부천시 중동 +4119074200 경기도 부천시 신중동 +4119074400 경기도 부천시 상동 +4119074600 경기도 부천시 대산동 +4119075000 경기도 부천시 소사본동 +4119079500 경기도 부천시 범안동 +4119080000 경기도 부천시 성곡동 +4119083000 경기도 부천시 오정동 +4121051000 경기도 광명시 광명1동 +4121052000 경기도 광명시 광명2동 +4121054000 경기도 광명시 광명3동 +4121055000 경기도 광명시 광명4동 +4121056000 경기도 광명시 광명5동 +4121057000 경기도 광명시 광명6동 +4121058000 경기도 광명시 광명7동 +4121059000 경기도 광명시 철산1동 +4121060000 경기도 광명시 철산2동 +4121061000 경기도 광명시 철산3동 +4121062000 경기도 광명시 철산4동 +4121063100 경기도 광명시 하안1동 +4121063200 경기도 광명시 하안2동 +4121063300 경기도 광명시 하안3동 +4121063400 경기도 광명시 하안4동 +4121064000 경기도 광명시 소하1동 +4121065000 경기도 광명시 소하2동 +4121065500 경기도 광명시 일직동 +4121066000 경기도 광명시 학온동 +4122025000 경기도 평택시 팽성읍 +4122025300 경기도 평택시 안중읍 +4122025600 경기도 평택시 포승읍 +4122025900 경기도 평택시 청북읍 +4122031000 경기도 평택시 진위면 +4122032000 경기도 평택시 서탄면 +4122033000 경기도 평택시 고덕면 +4122034000 경기도 평택시 오성면 +4122037000 경기도 평택시 현덕면 +4122051000 경기도 평택시 중앙동 +4122052000 경기도 평택시 서정동 +4122053500 경기도 평택시 송탄동 +4122055000 경기도 평택시 지산동 +4122056000 경기도 평택시 송북동 +4122057000 경기도 평택시 신장1동 +4122058000 경기도 평택시 신장2동 +4122059000 경기도 평택시 신평동 +4122060000 경기도 평택시 원평동 +4122061000 경기도 평택시 통복동 +4122062000 경기도 평택시 비전1동 +4122063000 경기도 평택시 비전2동 +4122063500 경기도 평택시 용이동 +4122064000 경기도 평택시 세교동 +4122065000 경기도 평택시 동삭동 +4122066000 경기도 평택시 고덕동 +4125051000 경기도 동두천시 생연1동 +4125052000 경기도 동두천시 생연2동 +4125053500 경기도 동두천시 중앙동 +4125055000 경기도 동두천시 보산동 +4125056500 경기도 동두천시 불현동 +4125056600 경기도 동두천시 송내동 +4125058000 경기도 동두천시 소요동 +4125060000 경기도 동두천시 상패동 +4127151000 경기도 안산시 상록구 일동 +4127151500 경기도 안산시 상록구 이동 +4127152500 경기도 안산시 상록구 사동 +4127153200 경기도 안산시 상록구 사이동 +4127153700 경기도 안산시 상록구 해양동 +4127154000 경기도 안산시 상록구 본오1동 +4127155000 경기도 안산시 상록구 본오2동 +4127156000 경기도 안산시 상록구 본오3동 +4127157000 경기도 안산시 상록구 부곡동 +4127158000 경기도 안산시 상록구 월피동 +4127159000 경기도 안산시 상록구 성포동 +4127160000 경기도 안산시 상록구 반월동 +4127161000 경기도 안산시 상록구 안산동 +4127351000 경기도 안산시 단원구 와동 +4127352500 경기도 안산시 단원구 고잔동 +4127353200 경기도 안산시 단원구 중앙동 +4127353500 경기도 안산시 단원구 호수동 +4127354500 경기도 안산시 단원구 원곡동 +4127355500 경기도 안산시 단원구 백운동 +4127356500 경기도 안산시 단원구 신길동 +4127357000 경기도 안산시 단원구 초지동 +4127358000 경기도 안산시 단원구 선부1동 +4127359000 경기도 안산시 단원구 선부2동 +4127360000 경기도 안산시 단원구 선부3동 +4127361000 경기도 안산시 단원구 대부동 +4128151000 경기도 고양시 덕양구 주교동 +4128152000 경기도 고양시 덕양구 원신동 +4128153000 경기도 고양시 덕양구 흥도동 +4128154000 경기도 고양시 덕양구 성사1동 +4128155000 경기도 고양시 덕양구 성사2동 +4128156000 경기도 고양시 덕양구 효자동 +4128157600 경기도 고양시 덕양구 삼송1동 +4128157700 경기도 고양시 덕양구 삼송2동 +4128158000 경기도 고양시 덕양구 창릉동 +4128159000 경기도 고양시 덕양구 고양동 +4128160000 경기도 고양시 덕양구 관산동 +4128161000 경기도 고양시 덕양구 능곡동 +4128162100 경기도 고양시 덕양구 화정1동 +4128162200 경기도 고양시 덕양구 화정2동 +4128163000 경기도 고양시 덕양구 행주동 +4128164000 경기도 고양시 덕양구 행신1동 +4128165000 경기도 고양시 덕양구 행신2동 +4128165500 경기도 고양시 덕양구 행신3동 +4128165600 경기도 고양시 덕양구 행신4동 +4128166000 경기도 고양시 덕양구 화전동 +4128167000 경기도 고양시 덕양구 대덕동 +4128551000 경기도 고양시 일산동구 식사동 +4128552500 경기도 고양시 일산동구 중산1동 +4128552600 경기도 고양시 일산동구 중산2동 +4128553000 경기도 고양시 일산동구 정발산동 +4128554000 경기도 고양시 일산동구 풍산동 +4128555100 경기도 고양시 일산동구 백석1동 +4128555200 경기도 고양시 일산동구 백석2동 +4128556000 경기도 고양시 일산동구 마두1동 +4128557000 경기도 고양시 일산동구 마두2동 +4128558000 경기도 고양시 일산동구 장항1동 +4128559000 경기도 고양시 일산동구 장항2동 +4128560000 경기도 고양시 일산동구 고봉동 +4128751000 경기도 고양시 일산서구 일산1동 +4128752000 경기도 고양시 일산서구 일산2동 +4128753000 경기도 고양시 일산서구 일산3동 +4128754500 경기도 고양시 일산서구 탄현1동 +4128754600 경기도 고양시 일산서구 탄현2동 +4128755000 경기도 고양시 일산서구 주엽1동 +4128756000 경기도 고양시 일산서구 주엽2동 +4128757000 경기도 고양시 일산서구 대화동 +4128758000 경기도 고양시 일산서구 송포동 +4128760000 경기도 고양시 일산서구 덕이동 +4128761000 경기도 고양시 일산서구 가좌동 +4129051000 경기도 과천시 중앙동 +4129051500 경기도 과천시 원문동 +4129052000 경기도 과천시 갈현동 +4129053000 경기도 과천시 별양동 +4129054000 경기도 과천시 부림동 +4129055000 경기도 과천시 과천동 +4129056000 경기도 과천시 문원동 +4131051000 경기도 구리시 갈매동 +4131052000 경기도 구리시 동구동 +4131053000 경기도 구리시 인창동 +4131054100 경기도 구리시 교문1동 +4131054200 경기도 구리시 교문2동 +4131057000 경기도 구리시 수택1동 +4131058000 경기도 구리시 수택2동 +4131059000 경기도 구리시 수택3동 +4136025000 경기도 남양주시 와부읍 +4136025300 경기도 남양주시 진접읍 +4136025600 경기도 남양주시 화도읍 +4136025700 경기도 남양주시 화도읍동부출장소 +4136025800 경기도 남양주시 화도읍남부출장소 +4136025900 경기도 남양주시 진건읍 +4136026200 경기도 남양주시 오남읍 +4136026500 경기도 남양주시 퇴계원읍 +4136031000 경기도 남양주시 별내면 +4136034000 경기도 남양주시 수동면 +4136036000 경기도 남양주시 조안면 +4136051000 경기도 남양주시 호평동 +4136052000 경기도 남양주시 평내동 +4136053000 경기도 남양주시 금곡동 +4136054000 경기도 남양주시 양정동 +4136054500 경기도 남양주시 다산1동 +4136056500 경기도 남양주시 다산2동 +4136057000 경기도 남양주시 별내동 +4137051000 경기도 오산시 중앙동 +4137053000 경기도 오산시 남촌동 +4137054000 경기도 오산시 신장동 +4137055000 경기도 오산시 세마동 +4137056000 경기도 오산시 초평동 +4137057000 경기도 오산시 대원동 +4139051000 경기도 시흥시 대야동 +4139052000 경기도 시흥시 신천동 +4139053100 경기도 시흥시 신현동 +4139054000 경기도 시흥시 은행동 +4139055000 경기도 시흥시 매화동 +4139057000 경기도 시흥시 목감동 +4139058100 경기도 시흥시 군자동 +4139058200 경기도 시흥시 월곶동 +4139058900 경기도 시흥시 정왕본동 +4139059100 경기도 시흥시 정왕1동 +4139059200 경기도 시흥시 정왕2동 +4139059300 경기도 시흥시 정왕3동 +4139059400 경기도 시흥시 정왕4동 +4139059600 경기도 시흥시 배곧1동 +4139059700 경기도 시흥시 배곧2동 +4139062100 경기도 시흥시 과림동 +4139063000 경기도 시흥시 연성동 +4139063100 경기도 시흥시 장곡동 +4139064000 경기도 시흥시 능곡동 +4139065000 경기도 시흥시 거북섬동 +4141051000 경기도 군포시 군포1동 +4141052000 경기도 군포시 군포2동 +4141054000 경기도 군포시 산본1동 +4141055000 경기도 군포시 산본2동 +4141056000 경기도 군포시 금정동 +4141057000 경기도 군포시 재궁동 +4141058000 경기도 군포시 오금동 +4141059000 경기도 군포시 수리동 +4141060000 경기도 군포시 궁내동 +4141061000 경기도 군포시 대야동 +4141062000 경기도 군포시 광정동 +4141063000 경기도 군포시 송부동 +4143051000 경기도 의왕시 고천동 +4143052000 경기도 의왕시 부곡동 +4143053000 경기도 의왕시 오전동 +4143054000 경기도 의왕시 내손1동 +4143055000 경기도 의왕시 내손2동 +4143056000 경기도 의왕시 청계동 +4145051000 경기도 하남시 천현동 +4145052000 경기도 하남시 신장1동 +4145053000 경기도 하남시 신장2동 +4145054000 경기도 하남시 덕풍1동 +4145055000 경기도 하남시 덕풍2동 +4145056000 경기도 하남시 덕풍3동 +4145058000 경기도 하남시 감북동 +4145058200 경기도 하남시 감일동 +4145058500 경기도 하남시 위례동 +4145059000 경기도 하남시 춘궁동 +4145060000 경기도 하남시 초이동 +4145061000 경기도 하남시 미사1동 +4145062000 경기도 하남시 미사2동 +4145063000 경기도 하남시 미사3동 +4146125000 경기도 용인시 처인구 포곡읍 +4146125300 경기도 용인시 처인구 모현읍 +4146125600 경기도 용인시 처인구 이동읍 +4146125900 경기도 용인시 처인구 남사읍 +4146134000 경기도 용인시 처인구 원삼면 +4146135000 경기도 용인시 처인구 백암면 +4146136000 경기도 용인시 처인구 양지면 +4146151000 경기도 용인시 처인구 중앙동 +4146152500 경기도 용인시 처인구 역북동 +4146152600 경기도 용인시 처인구 삼가동 +4146153000 경기도 용인시 처인구 유림동 +4146154000 경기도 용인시 처인구 동부동 +4146351000 경기도 용인시 기흥구 신갈동 +4146351600 경기도 용인시 기흥구 영덕1동 +4146351700 경기도 용인시 기흥구 영덕2동 +4146352000 경기도 용인시 기흥구 구갈동 +4146353000 경기도 용인시 기흥구 상갈동 +4146353500 경기도 용인시 기흥구 보라동 +4146354000 경기도 용인시 기흥구 기흥동 +4146355000 경기도 용인시 기흥구 서농동 +4146356000 경기도 용인시 기흥구 구성동 +4146357000 경기도 용인시 기흥구 마북동 +4146357200 경기도 용인시 기흥구 동백1동 +4146357500 경기도 용인시 기흥구 동백2동 +4146357700 경기도 용인시 기흥구 동백3동 +4146358600 경기도 용인시 기흥구 상하동 +4146359000 경기도 용인시 기흥구 보정동 +4146551000 경기도 용인시 수지구 풍덕천1동 +4146552000 경기도 용인시 수지구 풍덕천2동 +4146553000 경기도 용인시 수지구 신봉동 +4146554000 경기도 용인시 수지구 죽전1동 +4146555000 경기도 용인시 수지구 죽전2동 +4146555500 경기도 용인시 수지구 죽전3동 +4146556000 경기도 용인시 수지구 동천동 +4146557000 경기도 용인시 수지구 상현1동 +4146558000 경기도 용인시 수지구 상현2동 +4146558500 경기도 용인시 수지구 상현3동 +4146559000 경기도 용인시 수지구 성복동 +4148025000 경기도 파주시 문산읍 +4148025300 경기도 파주시 파주읍 +4148025600 경기도 파주시 법원읍 +4148026200 경기도 파주시 조리읍 +4148031000 경기도 파주시 월롱면 +4148032000 경기도 파주시 탄현면 +4148035000 경기도 파주시 광탄면 +4148036000 경기도 파주시 파평면 +4148037000 경기도 파주시 적성면 +4148039000 경기도 파주시 장단면 +4148051000 경기도 파주시 금촌1동 +4148052000 경기도 파주시 금촌2동 +4148053000 경기도 파주시 금촌3동 +4148054000 경기도 파주시 교하동 +4148055000 경기도 파주시 운정1동 +4148056000 경기도 파주시 운정2동 +4148057000 경기도 파주시 운정3동 +4148058000 경기도 파주시 운정4동 +4148059000 경기도 파주시 운정5동 +4148060000 경기도 파주시 운정6동 +4150025000 경기도 이천시 장호원읍 +4150025300 경기도 이천시 부발읍 +4150031000 경기도 이천시 신둔면 +4150032000 경기도 이천시 백사면 +4150033000 경기도 이천시 호법면 +4150034000 경기도 이천시 마장면 +4150035000 경기도 이천시 대월면 +4150036000 경기도 이천시 모가면 +4150037000 경기도 이천시 설성면 +4150038000 경기도 이천시 율면 +4150051000 경기도 이천시 창전동 +4150051500 경기도 이천시 증포동 +4150052000 경기도 이천시 중리동 +4150053000 경기도 이천시 관고동 +4155025000 경기도 안성시 공도읍 +4155031000 경기도 안성시 보개면 +4155032000 경기도 안성시 금광면 +4155033000 경기도 안성시 서운면 +4155034000 경기도 안성시 미양면 +4155035000 경기도 안성시 대덕면 +4155036000 경기도 안성시 양성면 +4155038000 경기도 안성시 원곡면 +4155039000 경기도 안성시 일죽면 +4155040000 경기도 안성시 죽산면 +4155041000 경기도 안성시 삼죽면 +4155042000 경기도 안성시 고삼면 +4155051000 경기도 안성시 안성1동 +4155052000 경기도 안성시 안성2동 +4155053000 경기도 안성시 안성3동 +4157025000 경기도 김포시 통진읍 +4157025300 경기도 김포시 고촌읍 +4157025600 경기도 김포시 양촌읍 +4157034000 경기도 김포시 대곶면 +4157035000 경기도 김포시 월곶면 +4157036000 경기도 김포시 하성면 +4157051500 경기도 김포시 김포본동 +4157052500 경기도 김포시 장기본동 +4157054000 경기도 김포시 사우동 +4157055000 경기도 김포시 풍무동 +4157056000 경기도 김포시 장기동 +4157057000 경기도 김포시 구래동 +4157057500 경기도 김포시 마산동 +4157058000 경기도 김포시 운양동 +4159025300 경기도 화성시 봉담읍 +4159025600 경기도 화성시 우정읍 +4159025900 경기도 화성시 향남읍 +4159026200 경기도 화성시 남양읍 +4159031000 경기도 화성시 매송면 +4159032000 경기도 화성시 비봉면 +4159033000 경기도 화성시 마도면 +4159034000 경기도 화성시 송산면 +4159035000 경기도 화성시 서신면 +4159036000 경기도 화성시 팔탄면 +4159037000 경기도 화성시 장안면 +4159040000 경기도 화성시 양감면 +4159041000 경기도 화성시 정남면 +4159051500 경기도 화성시 새솔동 +4159052000 경기도 화성시 진안동 +4159053000 경기도 화성시 병점1동 +4159054000 경기도 화성시 병점2동 +4159055000 경기도 화성시 반월동 +4159056000 경기도 화성시 기배동 +4159057000 경기도 화성시 화산동 +4159058500 경기도 화성시 동탄1동 +4159058600 경기도 화성시 동탄2동 +4159058700 경기도 화성시 동탄3동 +4159058800 경기도 화성시 동탄4동 +4159059000 경기도 화성시 동탄5동 +4159060000 경기도 화성시 동탄6동 +4159061000 경기도 화성시 동탄7동 +4159062000 경기도 화성시 동탄8동 +4159063000 경기도 화성시 동탄9동 +4161025300 경기도 광주시 초월읍 +4161025900 경기도 광주시 곤지암읍 +4161033000 경기도 광주시 도척면 +4161034000 경기도 광주시 퇴촌면 +4161035000 경기도 광주시 남종면 +4161037000 경기도 광주시 남한산성면 +4161051000 경기도 광주시 경안동 +4161052000 경기도 광주시 송정동 +4161054000 경기도 광주시 쌍령동 +4161055000 경기도 광주시 탄벌동 +4161056000 경기도 광주시 광남1동 +4161057000 경기도 광주시 광남2동 +4161058000 경기도 광주시 오포1동 +4161059000 경기도 광주시 오포2동 +4161060000 경기도 광주시 신현동 +4161061000 경기도 광주시 능평동 +4163025000 경기도 양주시 백석읍 +4163031000 경기도 양주시 은현면 +4163032000 경기도 양주시 남면 +4163033000 경기도 양주시 광적면 +4163034000 경기도 양주시 장흥면 +4163051000 경기도 양주시 양주1동 +4163052000 경기도 양주시 양주2동 +4163053000 경기도 양주시 회천1동 +4163054000 경기도 양주시 회천2동 +4163055000 경기도 양주시 회천3동 +4163056000 경기도 양주시 회천4동 +4165025000 경기도 포천시 소흘읍 +4165031000 경기도 포천시 군내면 +4165032000 경기도 포천시 내촌면 +4165033000 경기도 포천시 가산면 +4165034000 경기도 포천시 신북면 +4165035000 경기도 포천시 창수면 +4165036000 경기도 포천시 영중면 +4165037000 경기도 포천시 일동면 +4165038000 경기도 포천시 이동면 +4165039000 경기도 포천시 영북면 +4165040000 경기도 포천시 관인면 +4165041000 경기도 포천시 화현면 +4165051000 경기도 포천시 포천동 +4165052000 경기도 포천시 선단동 +4167025000 경기도 여주시 가남읍 +4167031000 경기도 여주시 점동면 +4167032000 경기도 여주시 흥천면 +4167033000 경기도 여주시 금사면 +4167034500 경기도 여주시 세종대왕면 +4167035000 경기도 여주시 대신면 +4167036000 경기도 여주시 북내면 +4167037000 경기도 여주시 강천면 +4167038000 경기도 여주시 산북면 +4167051000 경기도 여주시 여흥동 +4167052000 경기도 여주시 중앙동 +4167053000 경기도 여주시 오학동 +4180025000 경기도 연천군 연천읍 +4180025300 경기도 연천군 전곡읍 +4180031000 경기도 연천군 군남면 +4180032000 경기도 연천군 청산면 +4180033000 경기도 연천군 백학면 +4180034000 경기도 연천군 미산면 +4180035000 경기도 연천군 왕징면 +4180036000 경기도 연천군 신서면 +4180037000 경기도 연천군 중면 +4180038000 경기도 연천군 장남면 +4182025000 경기도 가평군 가평읍 +4182031000 경기도 가평군 설악면 +4182032500 경기도 가평군 청평면 +4182033000 경기도 가평군 상면 +4182034500 경기도 가평군 조종면 +4182035000 경기도 가평군 북면 +4183025000 경기도 양평군 양평읍 +4183031000 경기도 양평군 강상면 +4183032000 경기도 양평군 강하면 +4183033000 경기도 양평군 양서면 +4183034000 경기도 양평군 옥천면 +4183035000 경기도 양평군 서종면 +4183036000 경기도 양평군 단월면 +4183037000 경기도 양평군 청운면 +4183038000 경기도 양평군 양동면 +4183039500 경기도 양평군 지평면 +4183040000 경기도 양평군 용문면 +4183041000 경기도 양평군 개군면 +4311131000 충청북도 청주시 상당구 낭성면 +4311132000 충청북도 청주시 상당구 미원면 +4311133000 충청북도 청주시 상당구 가덕면 +4311134000 충청북도 청주시 상당구 남일면 +4311135000 충청북도 청주시 상당구 문의면 +4311152500 충청북도 청주시 상당구 중앙동 +4311154500 충청북도 청주시 상당구 성안동 +4311162000 충청북도 청주시 상당구 탑대성동 +4311167000 충청북도 청주시 상당구 영운동 +4311168000 충청북도 청주시 상당구 금천동 +4311169000 충청북도 청주시 상당구 용담.명암.산성동 +4311172000 충청북도 청주시 상당구 용암1동 +4311173000 충청북도 청주시 상당구 용암2동 +4311231000 충청북도 청주시 서원구 남이면 +4311232000 충청북도 청주시 서원구 현도면 +4311251000 충청북도 청주시 서원구 사직1동 +4311252000 충청북도 청주시 서원구 사직2동 +4311253000 충청북도 청주시 서원구 사창동 +4311254000 충청북도 청주시 서원구 모충동 +4311255000 충청북도 청주시 서원구 산남동 +4311256000 충청북도 청주시 서원구 분평동 +4311257000 충청북도 청주시 서원구 수곡1동 +4311258000 충청북도 청주시 서원구 수곡2동 +4311259000 충청북도 청주시 서원구 성화.개신.죽림동 +4311325000 충청북도 청주시 흥덕구 오송읍 +4311331000 충청북도 청주시 흥덕구 강내면 +4311332000 충청북도 청주시 흥덕구 옥산면 +4311370000 충청북도 청주시 흥덕구 운천.신봉동 +4311374100 충청북도 청주시 흥덕구 복대1동 +4311374200 충청북도 청주시 흥덕구 복대2동 +4311374700 충청북도 청주시 흥덕구 가경동 +4311375100 충청북도 청주시 흥덕구 봉명1동 +4311375600 충청북도 청주시 흥덕구 봉명2.송정동 +4311376000 충청북도 청주시 흥덕구 강서제1동 +4311377000 충청북도 청주시 흥덕구 강서제2동 +4311425000 충청북도 청주시 청원구 내수읍 +4311425300 충청북도 청주시 청원구 오창읍 +4311431000 충청북도 청주시 청원구 북이면 +4311451000 충청북도 청주시 청원구 우암동 +4311452000 충청북도 청주시 청원구 내덕1동 +4311453000 충청북도 청주시 청원구 내덕2동 +4311454000 충청북도 청주시 청원구 율량.사천동 +4311455000 충청북도 청주시 청원구 오근장동 +4313025000 충청북도 충주시 주덕읍 +4313031000 충청북도 충주시 살미면 +4313032500 충청북도 충주시 수안보면 +4313033500 충청북도 충주시 대소원면 +4313035000 충청북도 충주시 신니면 +4313036000 충청북도 충주시 노은면 +4313037000 충청북도 충주시 앙성면 +4313038500 충청북도 충주시 중앙탑면 +4313039000 충청북도 충주시 금가면 +4313040000 충청북도 충주시 동량면 +4313041000 충청북도 충주시 산척면 +4313042000 충청북도 충주시 엄정면 +4313043000 충청북도 충주시 소태면 +4313051500 충청북도 충주시 성내.충인동 +4313053500 충청북도 충주시 교현.안림동 +4313054000 충청북도 충주시 교현2동 +4313055000 충청북도 충주시 용산동 +4313056000 충청북도 충주시 지현동 +4313057100 충청북도 충주시 문화동 +4313058000 충청북도 충주시 호암.직동 +4313060500 충청북도 충주시 달천동 +4313061000 충청북도 충주시 봉방동 +4313062500 충청북도 충주시 칠금.금릉동 +4313063000 충청북도 충주시 연수동 +4313064000 충청북도 충주시 목행.용탄동 +4315025000 충청북도 제천시 봉양읍 +4315031000 충청북도 제천시 금성면 +4315032000 충청북도 제천시 청풍면 +4315033000 충청북도 제천시 수산면 +4315034000 충청북도 제천시 덕산면 +4315035000 충청북도 제천시 한수면 +4315036000 충청북도 제천시 백운면 +4315038000 충청북도 제천시 송학면 +4315051000 충청북도 제천시 교동 +4315051800 충청북도 제천시 의림지동 +4315052800 충청북도 제천시 중앙동 +4315053700 충청북도 제천시 남현동 +4315054700 충청북도 제천시 영서동 +4315056000 충청북도 제천시 용두동 +4315057700 충청북도 제천시 신백동 +4315059000 충청북도 제천시 청전동 +4315060500 충청북도 제천시 화산동 +4372025000 충청북도 보은군 보은읍 +4372031500 충청북도 보은군 속리산면 +4372032500 충청북도 보은군 장안면 +4372033000 충청북도 보은군 마로면 +4372034000 충청북도 보은군 탄부면 +4372035000 충청북도 보은군 삼승면 +4372036000 충청북도 보은군 수한면 +4372037000 충청북도 보은군 회남면 +4372038500 충청북도 보은군 회인면 +4372039000 충청북도 보은군 내북면 +4372040000 충청북도 보은군 산외면 +4373025000 충청북도 옥천군 옥천읍 +4373031000 충청북도 옥천군 동이면 +4373032000 충청북도 옥천군 안남면 +4373033000 충청북도 옥천군 안내면 +4373034000 충청북도 옥천군 청성면 +4373035000 충청북도 옥천군 청산면 +4373036000 충청북도 옥천군 이원면 +4373037000 충청북도 옥천군 군서면 +4373038000 충청북도 옥천군 군북면 +4374025000 충청북도 영동군 영동읍 +4374031000 충청북도 영동군 용산면 +4374032000 충청북도 영동군 황간면 +4374033500 충청북도 영동군 추풍령면 +4374034000 충청북도 영동군 매곡면 +4374035000 충청북도 영동군 상촌면 +4374036000 충청북도 영동군 양강면 +4374037000 충청북도 영동군 용화면 +4374038000 충청북도 영동군 학산면 +4374039000 충청북도 영동군 양산면 +4374040000 충청북도 영동군 심천면 +4374525000 충청북도 증평군 증평읍 +4374531000 충청북도 증평군 도안면 +4375025000 충청북도 진천군 진천읍 +4375025300 충청북도 진천군 덕산읍 +4375025400 충청북도 진천군 덕산읍혁신도시출장소 +4375032000 충청북도 진천군 초평면 +4375033000 충청북도 진천군 문백면 +4375034000 충청북도 진천군 백곡면 +4375035000 충청북도 진천군 이월면 +4375037000 충청북도 진천군 광혜원면 +4376025000 충청북도 괴산군 괴산읍 +4376031000 충청북도 괴산군 감물면 +4376032000 충청북도 괴산군 장연면 +4376033000 충청북도 괴산군 연풍면 +4376034000 충청북도 괴산군 칠성면 +4376035000 충청북도 괴산군 문광면 +4376036000 충청북도 괴산군 청천면 +4376037000 충청북도 괴산군 청안면 +4376039000 충청북도 괴산군 사리면 +4376040000 충청북도 괴산군 소수면 +4376041000 충청북도 괴산군 불정면 +4377025000 충청북도 음성군 음성읍 +4377025300 충청북도 음성군 금왕읍 +4377031000 충청북도 음성군 소이면 +4377032000 충청북도 음성군 원남면 +4377033000 충청북도 음성군 맹동면 +4377034000 충청북도 음성군 대소면 +4377035000 충청북도 음성군 삼성면 +4377036000 충청북도 음성군 생극면 +4377037000 충청북도 음성군 감곡면 +4380025000 충청북도 단양군 단양읍 +4380025300 충청북도 단양군 매포읍 +4380031000 충청북도 단양군 대강면 +4380032000 충청북도 단양군 가곡면 +4380033000 충청북도 단양군 영춘면 +4380034000 충청북도 단양군 어상천면 +4380035000 충청북도 단양군 적성면 +4380036000 충청북도 단양군 단성면 +4413125000 충청남도 천안시 동남구 목천읍 +4413131000 충청남도 천안시 동남구 풍세면 +4413132000 충청남도 천안시 동남구 광덕면 +4413133000 충청남도 천안시 동남구 북면 +4413134000 충청남도 천안시 동남구 성남면 +4413135000 충청남도 천안시 동남구 수신면 +4413136000 충청남도 천안시 동남구 병천면 +4413137000 충청남도 천안시 동남구 동면 +4413151000 충청남도 천안시 동남구 중앙동 +4413152000 충청남도 천안시 동남구 문성동 +4413153000 충청남도 천안시 동남구 원성1동 +4413154000 충청남도 천안시 동남구 원성2동 +4413155000 충청남도 천안시 동남구 봉명동 +4413156000 충청남도 천안시 동남구 일봉동 +4413157000 충청남도 천안시 동남구 신방동 +4413158000 충청남도 천안시 동남구 청룡동 +4413159000 충청남도 천안시 동남구 신안동 +4413325000 충청남도 천안시 서북구 성환읍 +4413325300 충청남도 천안시 서북구 성거읍 +4413325600 충청남도 천안시 서북구 직산읍 +4413331000 충청남도 천안시 서북구 입장면 +4413351000 충청남도 천안시 서북구 성정1동 +4413352000 충청남도 천안시 서북구 성정2동 +4413353000 충청남도 천안시 서북구 쌍용1동 +4413354000 충청남도 천안시 서북구 쌍용2동 +4413355000 충청남도 천안시 서북구 쌍용3동 +4413356000 충청남도 천안시 서북구 백석동 +4413356600 충청남도 천안시 서북구 불당1동 +4413356700 충청남도 천안시 서북구 불당2동 +4413358000 충청남도 천안시 서북구 부성1동 +4413359000 충청남도 천안시 서북구 부성2동 +4415025000 충청남도 공주시 유구읍 +4415031000 충청남도 공주시 이인면 +4415032000 충청남도 공주시 탄천면 +4415033000 충청남도 공주시 계룡면 +4415034000 충청남도 공주시 반포면 +4415036000 충청남도 공주시 의당면 +4415037000 충청남도 공주시 정안면 +4415038000 충청남도 공주시 우성면 +4415039000 충청남도 공주시 사곡면 +4415040000 충청남도 공주시 신풍면 +4415051000 충청남도 공주시 중학동 +4415054000 충청남도 공주시 웅진동 +4415055000 충청남도 공주시 금학동 +4415056000 충청남도 공주시 옥룡동 +4415057000 충청남도 공주시 신관동 +4415059000 충청남도 공주시 월송동 +4418025000 충청남도 보령시 웅천읍 +4418031000 충청남도 보령시 주포면 +4418032000 충청남도 보령시 오천면 +4418032500 충청남도 보령시 오천면어항출장소 +4418032900 충청남도 보령시 원산출장소 +4418033000 충청남도 보령시 천북면 +4418034000 충청남도 보령시 청소면 +4418035000 충청남도 보령시 청라면 +4418036000 충청남도 보령시 남포면 +4418038000 충청남도 보령시 주산면 +4418039000 충청남도 보령시 미산면 +4418040000 충청남도 보령시 성주면 +4418041000 충청남도 보령시 주교면 +4418051500 충청남도 보령시 대천1동 +4418052500 충청남도 보령시 대천2동 +4418053500 충청남도 보령시 대천3동 +4418054500 충청남도 보령시 대천4동 +4418056500 충청남도 보령시 대천5동 +4420025000 충청남도 아산시 염치읍 +4420025300 충청남도 아산시 배방읍 +4420031000 충청남도 아산시 송악면 +4420033000 충청남도 아산시 탕정면 +4420035000 충청남도 아산시 음봉면 +4420036000 충청남도 아산시 둔포면 +4420037000 충청남도 아산시 영인면 +4420038000 충청남도 아산시 인주면 +4420039000 충청남도 아산시 선장면 +4420040000 충청남도 아산시 도고면 +4420041000 충청남도 아산시 신창면 +4420057000 충청남도 아산시 온양1동 +4420058000 충청남도 아산시 온양2동 +4420059000 충청남도 아산시 온양3동 +4420060000 충청남도 아산시 온양4동 +4420061000 충청남도 아산시 온양5동 +4420062000 충청남도 아산시 온양6동 +4421025000 충청남도 서산시 대산읍 +4421031000 충청남도 서산시 인지면 +4421032000 충청남도 서산시 부석면 +4421033000 충청남도 서산시 팔봉면 +4421034000 충청남도 서산시 지곡면 +4421036000 충청남도 서산시 성연면 +4421037000 충청남도 서산시 음암면 +4421038000 충청남도 서산시 운산면 +4421039000 충청남도 서산시 해미면 +4421040000 충청남도 서산시 고북면 +4421051000 충청남도 서산시 부춘동 +4421052500 충청남도 서산시 동문1동 +4421053500 충청남도 서산시 동문2동 +4421054000 충청남도 서산시 수석동 +4421055000 충청남도 서산시 석남동 +4423025000 충청남도 논산시 강경읍 +4423025300 충청남도 논산시 연무읍 +4423031000 충청남도 논산시 성동면 +4423032000 충청남도 논산시 광석면 +4423033000 충청남도 논산시 노성면 +4423034000 충청남도 논산시 상월면 +4423035000 충청남도 논산시 부적면 +4423036000 충청남도 논산시 연산면 +4423038000 충청남도 논산시 벌곡면 +4423039000 충청남도 논산시 양촌면 +4423040000 충청남도 논산시 가야곡면 +4423041000 충청남도 논산시 은진면 +4423042000 충청남도 논산시 채운면 +4423051000 충청남도 논산시 취암동 +4423052000 충청남도 논산시 부창동 +4425031000 충청남도 계룡시 두마면 +4425031500 충청남도 계룡시 엄사면 +4425033000 충청남도 계룡시 신도안면 +4425051000 충청남도 계룡시 금암동 +4427025000 충청남도 당진시 합덕읍 +4427025300 충청남도 당진시 송악읍 +4427031000 충청남도 당진시 고대면 +4427032000 충청남도 당진시 석문면 +4427033000 충청남도 당진시 대호지면 +4427034000 충청남도 당진시 정미면 +4427035000 충청남도 당진시 면천면 +4427036000 충청남도 당진시 순성면 +4427037000 충청남도 당진시 우강면 +4427038000 충청남도 당진시 신평면 +4427039000 충청남도 당진시 송산면 +4427051000 충청남도 당진시 당진1동 +4427052000 충청남도 당진시 당진2동 +4427053000 충청남도 당진시 당진3동 +4471025000 충청남도 금산군 금산읍 +4471031000 충청남도 금산군 금성면 +4471032000 충청남도 금산군 제원면 +4471033000 충청남도 금산군 부리면 +4471034000 충청남도 금산군 군북면 +4471035000 충청남도 금산군 남일면 +4471036000 충청남도 금산군 남이면 +4471037000 충청남도 금산군 진산면 +4471038000 충청남도 금산군 복수면 +4471039000 충청남도 금산군 추부면 +4476025000 충청남도 부여군 부여읍 +4476031000 충청남도 부여군 규암면 +4476032000 충청남도 부여군 은산면 +4476033000 충청남도 부여군 외산면 +4476034000 충청남도 부여군 내산면 +4476035000 충청남도 부여군 구룡면 +4476036000 충청남도 부여군 홍산면 +4476037000 충청남도 부여군 옥산면 +4476038000 충청남도 부여군 남면 +4476039000 충청남도 부여군 충화면 +4476040000 충청남도 부여군 양화면 +4476041000 충청남도 부여군 임천면 +4476042000 충청남도 부여군 장암면 +4476043000 충청남도 부여군 세도면 +4476044000 충청남도 부여군 석성면 +4476045000 충청남도 부여군 초촌면 +4477025000 충청남도 서천군 장항읍 +4477025300 충청남도 서천군 서천읍 +4477031000 충청남도 서천군 마서면 +4477032000 충청남도 서천군 화양면 +4477033000 충청남도 서천군 기산면 +4477034000 충청남도 서천군 한산면 +4477035000 충청남도 서천군 마산면 +4477036000 충청남도 서천군 시초면 +4477037000 충청남도 서천군 문산면 +4477038000 충청남도 서천군 판교면 +4477039000 충청남도 서천군 종천면 +4477040000 충청남도 서천군 비인면 +4477041000 충청남도 서천군 서면 +4479025000 충청남도 청양군 청양읍 +4479031000 충청남도 청양군 운곡면 +4479032000 충청남도 청양군 대치면 +4479033000 충청남도 청양군 정산면 +4479034000 충청남도 청양군 목면 +4479035000 충청남도 청양군 청남면 +4479036000 충청남도 청양군 장평면 +4479037000 충청남도 청양군 남양면 +4479038000 충청남도 청양군 화성면 +4479039000 충청남도 청양군 비봉면 +4480025000 충청남도 홍성군 홍성읍 +4480025300 충청남도 홍성군 광천읍 +4480025600 충청남도 홍성군 홍북읍 +4480032000 충청남도 홍성군 금마면 +4480033000 충청남도 홍성군 홍동면 +4480034000 충청남도 홍성군 장곡면 +4480035000 충청남도 홍성군 은하면 +4480036000 충청남도 홍성군 결성면 +4480037000 충청남도 홍성군 서부면 +4480038000 충청남도 홍성군 갈산면 +4480039000 충청남도 홍성군 구항면 +4481025000 충청남도 예산군 예산읍 +4481025300 충청남도 예산군 삽교읍 +4481031000 충청남도 예산군 대술면 +4481032000 충청남도 예산군 신양면 +4481033000 충청남도 예산군 광시면 +4481034000 충청남도 예산군 대흥면 +4481035000 충청남도 예산군 응봉면 +4481036000 충청남도 예산군 덕산면 +4481037000 충청남도 예산군 봉산면 +4481038000 충청남도 예산군 고덕면 +4481039000 충청남도 예산군 신암면 +4481040000 충청남도 예산군 오가면 +4482525000 충청남도 태안군 태안읍 +4482525300 충청남도 태안군 안면읍 +4482531000 충청남도 태안군 고남면 +4482532000 충청남도 태안군 남면 +4482533000 충청남도 태안군 근흥면 +4482534000 충청남도 태안군 소원면 +4482535000 충청남도 태안군 원북면 +4482536000 충청남도 태안군 이원면 +4511151000 전라북도 전주시 완산구 중앙동 +4511153000 전라북도 전주시 완산구 풍남동 +4511160500 전라북도 전주시 완산구 노송동 +4511163500 전라북도 전주시 완산구 완산동 +4511165000 전라북도 전주시 완산구 동서학동 +4511166000 전라북도 전주시 완산구 서서학동 +4511167100 전라북도 전주시 완산구 중화산1동 +4511167200 전라북도 전주시 완산구 중화산2동 +4511168000 전라북도 전주시 완산구 서신동 +4511169100 전라북도 전주시 완산구 평화1동 +4511169200 전라북도 전주시 완산구 평화2동 +4511170100 전라북도 전주시 완산구 삼천1동 +4511170200 전라북도 전주시 완산구 삼천2동 +4511170300 전라북도 전주시 완산구 삼천3동 +4511171100 전라북도 전주시 완산구 효자1동 +4511171200 전라북도 전주시 완산구 효자2동 +4511171300 전라북도 전주시 완산구 효자3동 +4511171400 전라북도 전주시 완산구 효자4동 +4511173000 전라북도 전주시 완산구 효자5동 +4511352500 전라북도 전주시 덕진구 진북동 +4511354000 전라북도 전주시 덕진구 인후1동 +4511355000 전라북도 전주시 덕진구 인후2동 +4511356000 전라북도 전주시 덕진구 인후3동 +4511357000 전라북도 전주시 덕진구 덕진동 +4511358000 전라북도 전주시 덕진구 금암1동 +4511359000 전라북도 전주시 덕진구 금암2동 +4511360000 전라북도 전주시 덕진구 팔복동 +4511361100 전라북도 전주시 덕진구 우아1동 +4511361200 전라북도 전주시 덕진구 우아2동 +4511362000 전라북도 전주시 덕진구 호성동 +4511364100 전라북도 전주시 덕진구 송천1동 +4511364200 전라북도 전주시 덕진구 송천2동 +4511365000 전라북도 전주시 덕진구 조촌동 +4511366500 전라북도 전주시 덕진구 여의동 +4511367000 전라북도 전주시 덕진구 혁신동 +4513025000 전라북도 군산시 옥구읍 +4513031000 전라북도 군산시 옥산면 +4513032000 전라북도 군산시 회현면 +4513033000 전라북도 군산시 임피면 +4513034000 전라북도 군산시 서수면 +4513035000 전라북도 군산시 대야면 +4513036000 전라북도 군산시 개정면 +4513037000 전라북도 군산시 성산면 +4513038000 전라북도 군산시 나포면 +4513039000 전라북도 군산시 옥도면 +4513040000 전라북도 군산시 옥서면 +4513051500 전라북도 군산시 해신동 +4513053000 전라북도 군산시 월명동 +4513055000 전라북도 군산시 신풍동 +4513056000 전라북도 군산시 삼학동 +4513060500 전라북도 군산시 중앙동 +4513064000 전라북도 군산시 흥남동 +4513065000 전라북도 군산시 조촌동 +4513066000 전라북도 군산시 경암동 +4513067000 전라북도 군산시 구암동 +4513068000 전라북도 군산시 개정동 +4513069000 전라북도 군산시 수송동 +4513070100 전라북도 군산시 나운1동 +4513070200 전라북도 군산시 나운2동 +4513070300 전라북도 군산시 나운3동 +4513071000 전라북도 군산시 소룡동 +4513072000 전라북도 군산시 미성동 +4514025000 전라북도 익산시 함열읍 +4514031000 전라북도 익산시 오산면 +4514032000 전라북도 익산시 황등면 +4514033000 전라북도 익산시 함라면 +4514034000 전라북도 익산시 웅포면 +4514035000 전라북도 익산시 성당면 +4514036000 전라북도 익산시 용안면 +4514037000 전라북도 익산시 낭산면 +4514038000 전라북도 익산시 망성면 +4514039000 전라북도 익산시 여산면 +4514040000 전라북도 익산시 금마면 +4514041000 전라북도 익산시 왕궁면 +4514042000 전라북도 익산시 춘포면 +4514043000 전라북도 익산시 삼기면 +4514044000 전라북도 익산시 용동면 +4514052000 전라북도 익산시 중앙동 +4514053000 전라북도 익산시 평화동 +4514056000 전라북도 익산시 인화동 +4514057000 전라북도 익산시 동산동 +4514058000 전라북도 익산시 마동 +4514059500 전라북도 익산시 남중동 +4514061000 전라북도 익산시 모현동 +4514062000 전라북도 익산시 송학동 +4514064600 전라북도 익산시 영등1동 +4514064700 전라북도 익산시 영등2동 +4514065200 전라북도 익산시 어양동 +4514065600 전라북도 익산시 신동 +4514067000 전라북도 익산시 팔봉동 +4514069000 전라북도 익산시 삼성동 +4518025000 전라북도 정읍시 신태인읍 +4518031000 전라북도 정읍시 북면 +4518032000 전라북도 정읍시 입암면 +4518033000 전라북도 정읍시 소성면 +4518034000 전라북도 정읍시 고부면 +4518035000 전라북도 정읍시 영원면 +4518036000 전라북도 정읍시 덕천면 +4518037000 전라북도 정읍시 이평면 +4518038000 전라북도 정읍시 정우면 +4518039000 전라북도 정읍시 태인면 +4518040000 전라북도 정읍시 감곡면 +4518041000 전라북도 정읍시 옹동면 +4518042000 전라북도 정읍시 칠보면 +4518043000 전라북도 정읍시 산내면 +4518044000 전라북도 정읍시 산외면 +4518051000 전라북도 정읍시 수성동 +4518052000 전라북도 정읍시 장명동 +4518053500 전라북도 정읍시 내장상동 +4518054500 전라북도 정읍시 시기동 +4518056500 전라북도 정읍시 초산동 +4518057000 전라북도 정읍시 연지동 +4518058000 전라북도 정읍시 농소동 +4518059500 전라북도 정읍시 상교동 +4519025000 전라북도 남원시 운봉읍 +4519031000 전라북도 남원시 주천면 +4519032000 전라북도 남원시 수지면 +4519033000 전라북도 남원시 송동면 +4519034000 전라북도 남원시 주생면 +4519035000 전라북도 남원시 금지면 +4519036000 전라북도 남원시 대강면 +4519037000 전라북도 남원시 대산면 +4519038000 전라북도 남원시 사매면 +4519039000 전라북도 남원시 덕과면 +4519040000 전라북도 남원시 보절면 +4519041000 전라북도 남원시 산동면 +4519042000 전라북도 남원시 이백면 +4519045000 전라북도 남원시 아영면 +4519046000 전라북도 남원시 산내면 +4519047000 전라북도 남원시 인월면 +4519051000 전라북도 남원시 동충동 +4519052000 전라북도 남원시 죽항동 +4519054000 전라북도 남원시 노암동 +4519055000 전라북도 남원시 금동 +4519056000 전라북도 남원시 왕정동 +4519057000 전라북도 남원시 향교동 +4519059000 전라북도 남원시 도통동 +4521025000 전라북도 김제시 만경읍 +4521032000 전라북도 김제시 죽산면 +4521033000 전라북도 김제시 백산면 +4521034000 전라북도 김제시 용지면 +4521035000 전라북도 김제시 백구면 +4521036000 전라북도 김제시 부량면 +4521038000 전라북도 김제시 공덕면 +4521039000 전라북도 김제시 청하면 +4521040000 전라북도 김제시 성덕면 +4521041000 전라북도 김제시 진봉면 +4521042000 전라북도 김제시 금구면 +4521043000 전라북도 김제시 봉남면 +4521044000 전라북도 김제시 황산면 +4521045000 전라북도 김제시 금산면 +4521046000 전라북도 김제시 광활면 +4521051000 전라북도 김제시 요촌동 +4521052000 전라북도 김제시 신풍동 +4521054000 전라북도 김제시 검산동 +4521058000 전라북도 김제시 교월동 +4571025000 전라북도 완주군 삼례읍 +4571025300 전라북도 완주군 봉동읍 +4571025600 전라북도 완주군 용진읍 +4571032000 전라북도 완주군 상관면 +4571033000 전라북도 완주군 이서면 +4571034000 전라북도 완주군 소양면 +4571035000 전라북도 완주군 구이면 +4571036000 전라북도 완주군 고산면 +4571037000 전라북도 완주군 비봉면 +4571038000 전라북도 완주군 운주면 +4571039000 전라북도 완주군 화산면 +4571040000 전라북도 완주군 동상면 +4571041000 전라북도 완주군 경천면 +4572025000 전라북도 진안군 진안읍 +4572031000 전라북도 진안군 용담면 +4572032000 전라북도 진안군 안천면 +4572033000 전라북도 진안군 동향면 +4572034000 전라북도 진안군 상전면 +4572035000 전라북도 진안군 백운면 +4572036000 전라북도 진안군 성수면 +4572037000 전라북도 진안군 마령면 +4572038000 전라북도 진안군 부귀면 +4572039000 전라북도 진안군 정천면 +4572040000 전라북도 진안군 주천면 +4573025000 전라북도 무주군 무주읍 +4573031000 전라북도 무주군 무풍면 +4573032000 전라북도 무주군 설천면 +4573033000 전라북도 무주군 적상면 +4573034000 전라북도 무주군 안성면 +4573035000 전라북도 무주군 부남면 +4574025000 전라북도 장수군 장수읍 +4574031000 전라북도 장수군 산서면 +4574032000 전라북도 장수군 번암면 +4574033500 전라북도 장수군 장계면 +4574034000 전라북도 장수군 천천면 +4574035000 전라북도 장수군 계남면 +4574036000 전라북도 장수군 계북면 +4575025000 전라북도 임실군 임실읍 +4575031000 전라북도 임실군 청웅면 +4575032000 전라북도 임실군 운암면 +4575033000 전라북도 임실군 신평면 +4575034000 전라북도 임실군 성수면 +4575035500 전라북도 임실군 오수면 +4575036000 전라북도 임실군 신덕면 +4575037000 전라북도 임실군 삼계면 +4575038000 전라북도 임실군 관촌면 +4575039000 전라북도 임실군 강진면 +4575040000 전라북도 임실군 덕치면 +4575041000 전라북도 임실군 지사면 +4577025000 전라북도 순창군 순창읍 +4577031000 전라북도 순창군 인계면 +4577032000 전라북도 순창군 동계면 +4577033000 전라북도 순창군 풍산면 +4577034000 전라북도 순창군 금과면 +4577035000 전라북도 순창군 팔덕면 +4577036000 전라북도 순창군 쌍치면 +4577037000 전라북도 순창군 복흥면 +4577038000 전라북도 순창군 적성면 +4577039000 전라북도 순창군 유등면 +4577040000 전라북도 순창군 구림면 +4579025000 전라북도 고창군 고창읍 +4579031000 전라북도 고창군 고수면 +4579032000 전라북도 고창군 아산면 +4579033000 전라북도 고창군 무장면 +4579034000 전라북도 고창군 공음면 +4579035000 전라북도 고창군 상하면 +4579036000 전라북도 고창군 해리면 +4579037000 전라북도 고창군 성송면 +4579038000 전라북도 고창군 대산면 +4579039000 전라북도 고창군 심원면 +4579040000 전라북도 고창군 흥덕면 +4579041000 전라북도 고창군 성내면 +4579042000 전라북도 고창군 신림면 +4579043000 전라북도 고창군 부안면 +4580025000 전라북도 부안군 부안읍 +4580031000 전라북도 부안군 주산면 +4580032000 전라북도 부안군 동진면 +4580033000 전라북도 부안군 행안면 +4580034000 전라북도 부안군 계화면 +4580035000 전라북도 부안군 보안면 +4580036000 전라북도 부안군 변산면 +4580037000 전라북도 부안군 진서면 +4580038000 전라북도 부안군 백산면 +4580039000 전라북도 부안군 상서면 +4580040000 전라북도 부안군 하서면 +4580041000 전라북도 부안군 줄포면 +4580042000 전라북도 부안군 위도면 +4611051000 전라남도 목포시 용당1동 +4611052000 전라남도 목포시 용당2동 +4611053500 전라남도 목포시 연동 +4611054500 전라남도 목포시 산정동 +4611055400 전라남도 목포시 연산동 +4611055800 전라남도 목포시 원산동 +4611056500 전라남도 목포시 대성동 +4611059500 전라남도 목포시 목원동 +4611064000 전라남도 목포시 동명동 +4611064500 전라남도 목포시 삼학동 +4611065500 전라남도 목포시 만호동 +4611066000 전라남도 목포시 유달동 +4611069500 전라남도 목포시 죽교동 +4611070500 전라남도 목포시 북항동 +4611074500 전라남도 목포시 용해동 +4611075000 전라남도 목포시 이로동 +4611075600 전라남도 목포시 상동 +4611075700 전라남도 목포시 하당동 +4611075800 전라남도 목포시 신흥동 +4611078000 전라남도 목포시 삼향동 +4611079000 전라남도 목포시 옥암동 +4611080000 전라남도 목포시 부흥동 +4611081000 전라남도 목포시 부주동 +4613025000 전라남도 여수시 돌산읍 +4613025100 전라남도 여수시 돌산읍우두출장소 +4613025200 전라남도 여수시 돌산읍죽포출장소 +4613031000 전라남도 여수시 소라면 +4613032000 전라남도 여수시 율촌면 +4613033000 전라남도 여수시 화양면 +4613034000 전라남도 여수시 남면 +4613035000 전라남도 여수시 화정면 +4613035600 전라남도 여수시 화정면개도출장소 +4613036000 전라남도 여수시 삼산면 +4613051500 전라남도 여수시 동문동 +4613053500 전라남도 여수시 한려동 +4613057000 전라남도 여수시 중앙동 +4613060000 전라남도 여수시 충무동 +4613062500 전라남도 여수시 광림동 +4613063500 전라남도 여수시 서강동 +4613065500 전라남도 여수시 대교동 +4613067000 전라남도 여수시 국동 +4613068500 전라남도 여수시 월호동 +4613070000 전라남도 여수시 여서동 +4613071000 전라남도 여수시 문수동 +4613073000 전라남도 여수시 미평동 +4613074000 전라남도 여수시 둔덕동 +4613076500 전라남도 여수시 만덕동 +4613078000 전라남도 여수시 쌍봉동 +4613079000 전라남도 여수시 시전동 +4613080000 전라남도 여수시 여천동 +4613081000 전라남도 여수시 주삼동 +4613082000 전라남도 여수시 삼일동 +4613083000 전라남도 여수시 묘도동 +4615025000 전라남도 순천시 승주읍 +4615031000 전라남도 순천시 해룡면 +4615031500 전라남도 순천시 해룡면상삼출장소 +4615031600 전라남도 순천시 해룡면신대출장소 +4615032000 전라남도 순천시 서면 +4615033000 전라남도 순천시 황전면 +4615034000 전라남도 순천시 월등면 +4615035000 전라남도 순천시 주암면 +4615036000 전라남도 순천시 송광면 +4615037000 전라남도 순천시 외서면 +4615038000 전라남도 순천시 낙안면 +4615039000 전라남도 순천시 별량면 +4615040000 전라남도 순천시 상사면 +4615051500 전라남도 순천시 향동 +4615054000 전라남도 순천시 매곡동 +4615055000 전라남도 순천시 삼산동 +4615056000 전라남도 순천시 조곡동 +4615057000 전라남도 순천시 덕연동 +4615058000 전라남도 순천시 풍덕동 +4615059000 전라남도 순천시 남제동 +4615060000 전라남도 순천시 저전동 +4615061000 전라남도 순천시 장천동 +4615062000 전라남도 순천시 중앙동 +4615063500 전라남도 순천시 도사동 +4615066100 전라남도 순천시 왕조1동 +4615066500 전라남도 순천시 왕조2동 +4617025000 전라남도 나주시 남평읍 +4617031000 전라남도 나주시 세지면 +4617032000 전라남도 나주시 왕곡면 +4617033000 전라남도 나주시 반남면 +4617034000 전라남도 나주시 공산면 +4617035000 전라남도 나주시 동강면 +4617036000 전라남도 나주시 다시면 +4617037000 전라남도 나주시 문평면 +4617038000 전라남도 나주시 노안면 +4617039000 전라남도 나주시 금천면 +4617040000 전라남도 나주시 산포면 +4617042000 전라남도 나주시 다도면 +4617043000 전라남도 나주시 봉황면 +4617051000 전라남도 나주시 송월동 +4617052000 전라남도 나주시 영강동 +4617054000 전라남도 나주시 금남동 +4617055000 전라남도 나주시 성북동 +4617058000 전라남도 나주시 영산동 +4617060000 전라남도 나주시 이창동 +4617062000 전라남도 나주시 빛가람동 +4623025000 전라남도 광양시 광양읍 +4623031000 전라남도 광양시 봉강면 +4623032000 전라남도 광양시 옥룡면 +4623033000 전라남도 광양시 옥곡면 +4623034000 전라남도 광양시 진상면 +4623035000 전라남도 광양시 진월면 +4623036000 전라남도 광양시 다압면 +4623051500 전라남도 광양시 골약동 +4623053000 전라남도 광양시 중마동 +4623054000 전라남도 광양시 광영동 +4623055000 전라남도 광양시 금호동 +4623057000 전라남도 광양시 태인동 +4671025000 전라남도 담양군 담양읍 +4671031000 전라남도 담양군 봉산면 +4671032000 전라남도 담양군 고서면 +4671033500 전라남도 담양군 가사문학면 +4671034000 전라남도 담양군 창평면 +4671035000 전라남도 담양군 대덕면 +4671036000 전라남도 담양군 무정면 +4671037000 전라남도 담양군 금성면 +4671038000 전라남도 담양군 용면 +4671039000 전라남도 담양군 월산면 +4671040000 전라남도 담양군 수북면 +4671041000 전라남도 담양군 대전면 +4672025000 전라남도 곡성군 곡성읍 +4672031000 전라남도 곡성군 오곡면 +4672032000 전라남도 곡성군 삼기면 +4672033000 전라남도 곡성군 석곡면 +4672034000 전라남도 곡성군 목사동면 +4672035000 전라남도 곡성군 죽곡면 +4672036000 전라남도 곡성군 고달면 +4672037000 전라남도 곡성군 옥과면 +4672038000 전라남도 곡성군 입면 +4672039000 전라남도 곡성군 겸면 +4672040000 전라남도 곡성군 오산면 +4673025000 전라남도 구례군 구례읍 +4673031000 전라남도 구례군 문척면 +4673032000 전라남도 구례군 간전면 +4673033000 전라남도 구례군 토지면 +4673034000 전라남도 구례군 마산면 +4673035000 전라남도 구례군 광의면 +4673036000 전라남도 구례군 용방면 +4673037000 전라남도 구례군 산동면 +4677025000 전라남도 고흥군 고흥읍 +4677025300 전라남도 고흥군 도양읍 +4677025400 전라남도 고흥군 도양읍소록출장소 +4677025500 전라남도 고흥군 도양읍시산출장소 +4677031000 전라남도 고흥군 풍양면 +4677032000 전라남도 고흥군 도덕면 +4677033000 전라남도 고흥군 금산면 +4677034000 전라남도 고흥군 도화면 +4677035000 전라남도 고흥군 포두면 +4677036000 전라남도 고흥군 봉래면 +4677037000 전라남도 고흥군 점암면 +4677038000 전라남도 고흥군 과역면 +4677039000 전라남도 고흥군 남양면 +4677040000 전라남도 고흥군 동강면 +4677041000 전라남도 고흥군 대서면 +4677042000 전라남도 고흥군 두원면 +4677044000 전라남도 고흥군 영남면 +4677045000 전라남도 고흥군 동일면 +4678025000 전라남도 보성군 보성읍 +4678025300 전라남도 보성군 벌교읍 +4678031000 전라남도 보성군 노동면 +4678032000 전라남도 보성군 미력면 +4678033000 전라남도 보성군 겸백면 +4678034000 전라남도 보성군 율어면 +4678035000 전라남도 보성군 복내면 +4678036000 전라남도 보성군 문덕면 +4678037000 전라남도 보성군 조성면 +4678038000 전라남도 보성군 득량면 +4678038500 전라남도 보성군 득량면예당출장소 +4678039000 전라남도 보성군 회천면 +4678040000 전라남도 보성군 웅치면 +4679025000 전라남도 화순군 화순읍 +4679025100 전라남도 화순군 화순읍민원출장소 +4679031000 전라남도 화순군 한천면 +4679031500 전라남도 화순군 한천면영외출장소 +4679032000 전라남도 화순군 춘양면 +4679033000 전라남도 화순군 청풍면 +4679034000 전라남도 화순군 이양면 +4679035000 전라남도 화순군 능주면 +4679036000 전라남도 화순군 도곡면 +4679037000 전라남도 화순군 도암면 +4679038000 전라남도 화순군 이서면 +4679039500 전라남도 화순군 백아면 +4679040000 전라남도 화순군 동복면 +4679041500 전라남도 화순군 사평면 +4679042000 전라남도 화순군 동면 +4680025000 전라남도 장흥군 장흥읍 +4680025300 전라남도 장흥군 관산읍 +4680025600 전라남도 장흥군 대덕읍 +4680031000 전라남도 장흥군 용산면 +4680032000 전라남도 장흥군 안양면 +4680033000 전라남도 장흥군 장동면 +4680034000 전라남도 장흥군 장평면 +4680035000 전라남도 장흥군 유치면 +4680036000 전라남도 장흥군 부산면 +4680037000 전라남도 장흥군 회진면 +4681025000 전라남도 강진군 강진읍 +4681031000 전라남도 강진군 군동면 +4681032000 전라남도 강진군 칠량면 +4681033000 전라남도 강진군 대구면 +4681034000 전라남도 강진군 도암면 +4681035000 전라남도 강진군 신전면 +4681036000 전라남도 강진군 성전면 +4681037000 전라남도 강진군 작천면 +4681038000 전라남도 강진군 병영면 +4681039000 전라남도 강진군 옴천면 +4681040000 전라남도 강진군 마량면 +4682025000 전라남도 해남군 해남읍 +4682031000 전라남도 해남군 삼산면 +4682032000 전라남도 해남군 화산면 +4682033000 전라남도 해남군 현산면 +4682034000 전라남도 해남군 송지면 +4682035000 전라남도 해남군 북평면 +4682036000 전라남도 해남군 북일면 +4682037000 전라남도 해남군 옥천면 +4682038000 전라남도 해남군 계곡면 +4682039000 전라남도 해남군 마산면 +4682040000 전라남도 해남군 황산면 +4682041000 전라남도 해남군 산이면 +4682042000 전라남도 해남군 문내면 +4682043000 전라남도 해남군 화원면 +4683025000 전라남도 영암군 영암읍 +4683025300 전라남도 영암군 삼호읍 +4683025400 전라남도 영암군 삼호읍서부출장소 +4683031000 전라남도 영암군 덕진면 +4683032000 전라남도 영암군 금정면 +4683033000 전라남도 영암군 신북면 +4683034000 전라남도 영암군 시종면 +4683035000 전라남도 영암군 도포면 +4683036000 전라남도 영암군 군서면 +4683037000 전라남도 영암군 서호면 +4683038000 전라남도 영암군 학산면 +4683039000 전라남도 영암군 미암면 +4684025000 전라남도 무안군 무안읍 +4684025300 전라남도 무안군 일로읍 +4684025600 전라남도 무안군 삼향읍 +4684025800 전라남도 무안군 삼향읍남악출장소 +4684032000 전라남도 무안군 몽탄면 +4684033000 전라남도 무안군 청계면 +4684034000 전라남도 무안군 현경면 +4684035000 전라남도 무안군 망운면 +4684036000 전라남도 무안군 해제면 +4684037000 전라남도 무안군 운남면 +4686025000 전라남도 함평군 함평읍 +4686031000 전라남도 함평군 손불면 +4686032000 전라남도 함평군 신광면 +4686033000 전라남도 함평군 학교면 +4686034000 전라남도 함평군 엄다면 +4686035000 전라남도 함평군 대동면 +4686036000 전라남도 함평군 나산면 +4686037000 전라남도 함평군 해보면 +4686038000 전라남도 함평군 월야면 +4687025000 전라남도 영광군 영광읍 +4687025300 전라남도 영광군 백수읍 +4687025600 전라남도 영광군 홍농읍 +4687031000 전라남도 영광군 대마면 +4687032000 전라남도 영광군 묘량면 +4687033000 전라남도 영광군 불갑면 +4687034000 전라남도 영광군 군서면 +4687035000 전라남도 영광군 군남면 +4687036000 전라남도 영광군 염산면 +4687037000 전라남도 영광군 법성면 +4687038000 전라남도 영광군 낙월면 +4687038500 전라남도 영광군 낙월면안마출장소 +4688025000 전라남도 장성군 장성읍 +4688031000 전라남도 장성군 진원면 +4688032000 전라남도 장성군 남면 +4688033000 전라남도 장성군 동화면 +4688034000 전라남도 장성군 삼서면 +4688035000 전라남도 장성군 삼계면 +4688036000 전라남도 장성군 황룡면 +4688037000 전라남도 장성군 서삼면 +4688038000 전라남도 장성군 북일면 +4688039000 전라남도 장성군 북이면 +4688040000 전라남도 장성군 북하면 +4689025000 전라남도 완도군 완도읍 +4689025300 전라남도 완도군 금일읍 +4689025600 전라남도 완도군 노화읍 +4689029000 전라남도 완도군 노화읍넙도출장소 +4689031000 전라남도 완도군 군외면 +4689032000 전라남도 완도군 신지면 +4689033000 전라남도 완도군 고금면 +4689034000 전라남도 완도군 약산면 +4689035000 전라남도 완도군 청산면 +4689035500 전라남도 완도군 청산면모도출장소 +4689036000 전라남도 완도군 소안면 +4689037000 전라남도 완도군 금당면 +4689038000 전라남도 완도군 보길면 +4689039000 전라남도 완도군 생일면 +4690025000 전라남도 진도군 진도읍 +4690031000 전라남도 진도군 군내면 +4690032000 전라남도 진도군 고군면 +4690033000 전라남도 진도군 의신면 +4690034000 전라남도 진도군 임회면 +4690035000 전라남도 진도군 지산면 +4690036000 전라남도 진도군 조도면 +4690036500 전라남도 진도군 조도면가사출장소 +4690036600 전라남도 진도군 조도면거차출장소 +4691025000 전라남도 신안군 지도읍 +4691025100 전라남도 신안군 지도읍선도출장소 +4691025300 전라남도 신안군 압해읍 +4691025400 전라남도 신안군 압해읍매화출장소 +4691025500 전라남도 신안군 압해읍고이도출장소 +4691031000 전라남도 신안군 증도면 +4691031500 전라남도 신안군 증도면병풍출장소 +4691032000 전라남도 신안군 임자면 +4691033000 전라남도 신안군 자은면 +4691034000 전라남도 신안군 비금면 +4691035000 전라남도 신안군 도초면 +4691035500 전라남도 신안군 도초면우이도출장소 +4691036000 전라남도 신안군 흑산면 +4691036500 전라남도 신안군 흑산면가거도출장소 +4691036600 전라남도 신안군 흑산면태도출장소 +4691036700 전라남도 신안군 흑산면홍도출장소 +4691036800 전라남도 신안군 흑산면대둔도출장소 +4691037000 전라남도 신안군 하의면 +4691038000 전라남도 신안군 신의면 +4691039000 전라남도 신안군 장산면 +4691040000 전라남도 신안군 안좌면 +4691040500 전라남도 신안군 안좌면자라출장소 +4691041000 전라남도 신안군 팔금면 +4691042000 전라남도 신안군 암태면 +4711125000 경상북도 포항시 남구 구룡포읍 +4711125300 경상북도 포항시 남구 연일읍 +4711125600 경상북도 포항시 남구 오천읍 +4711131000 경상북도 포항시 남구 대송면 +4711132000 경상북도 포항시 남구 동해면 +4711133000 경상북도 포항시 남구 장기면 +4711135000 경상북도 포항시 남구 호미곶면 +4711152500 경상북도 포항시 남구 상대동 +4711154500 경상북도 포항시 남구 해도동 +4711155000 경상북도 포항시 남구 송도동 +4711156000 경상북도 포항시 남구 청림동 +4711157000 경상북도 포항시 남구 제철동 +4711158000 경상북도 포항시 남구 효곡동 +4711159000 경상북도 포항시 남구 대이동 +4711325000 경상북도 포항시 북구 흥해읍 +4711331000 경상북도 포항시 북구 신광면 +4711332000 경상북도 포항시 북구 청하면 +4711333000 경상북도 포항시 북구 송라면 +4711334000 경상북도 포항시 북구 기계면 +4711335000 경상북도 포항시 북구 죽장면 +4711335500 경상북도 포항시 북구 죽장면상옥출장소 +4711336000 경상북도 포항시 북구 기북면 +4711352000 경상북도 포항시 북구 중앙동 +4711363000 경상북도 포항시 북구 양학동 +4711365500 경상북도 포항시 북구 죽도동 +4711366500 경상북도 포항시 북구 용흥동 +4711368000 경상북도 포항시 북구 우창동 +4711369000 경상북도 포항시 북구 두호동 +4711370000 경상북도 포항시 북구 장량동 +4711371000 경상북도 포항시 북구 환여동 +4713025000 경상북도 경주시 감포읍 +4713025300 경상북도 경주시 안강읍 +4713025600 경상북도 경주시 건천읍 +4713025900 경상북도 경주시 외동읍 +4713031500 경상북도 경주시 문무대왕면 +4713032000 경상북도 경주시 양남면 +4713033000 경상북도 경주시 내남면 +4713034000 경상북도 경주시 산내면 +4713035000 경상북도 경주시 서면 +4713036000 경상북도 경주시 현곡면 +4713037000 경상북도 경주시 강동면 +4713038000 경상북도 경주시 천북면 +4713051500 경상북도 경주시 중부동 +4713053000 경상북도 경주시 황오동 +4713055000 경상북도 경주시 성건동 +4713057000 경상북도 경주시 황남동 +4713059000 경상북도 경주시 선도동 +4713060500 경상북도 경주시 월성동 +4713061500 경상북도 경주시 용강동 +4713062100 경상북도 경주시 황성동 +4713063000 경상북도 경주시 동천동 +4713065000 경상북도 경주시 불국동 +4713066000 경상북도 경주시 보덕동 +4715025000 경상북도 김천시 아포읍 +4715031000 경상북도 김천시 농소면 +4715032000 경상북도 김천시 남면 +4715034000 경상북도 김천시 개령면 +4715035000 경상북도 김천시 감문면 +4715036000 경상북도 김천시 어모면 +4715037000 경상북도 김천시 봉산면 +4715038000 경상북도 김천시 대항면 +4715039000 경상북도 김천시 감천면 +4715040000 경상북도 김천시 조마면 +4715041000 경상북도 김천시 구성면 +4715042000 경상북도 김천시 지례면 +4715043000 경상북도 김천시 부항면 +4715044000 경상북도 김천시 대덕면 +4715045000 경상북도 김천시 증산면 +4715051600 경상북도 김천시 자산동 +4715053600 경상북도 김천시 평화남산동 +4715056500 경상북도 김천시 양금동 +4715057500 경상북도 김천시 대신동 +4715059500 경상북도 김천시 대곡동 +4715061000 경상북도 김천시 지좌동 +4715064000 경상북도 김천시 율곡동 +4717025000 경상북도 안동시 풍산읍 +4717031000 경상북도 안동시 와룡면 +4717032000 경상북도 안동시 북후면 +4717033000 경상북도 안동시 서후면 +4717034000 경상북도 안동시 풍천면 +4717035000 경상북도 안동시 일직면 +4717036000 경상북도 안동시 남후면 +4717037000 경상북도 안동시 남선면 +4717038000 경상북도 안동시 임하면 +4717039000 경상북도 안동시 길안면 +4717040000 경상북도 안동시 임동면 +4717041000 경상북도 안동시 예안면 +4717042000 경상북도 안동시 도산면 +4717043000 경상북도 안동시 녹전면 +4717051000 경상북도 안동시 중구동 +4717052000 경상북도 안동시 명륜동 +4717055500 경상북도 안동시 용상동 +4717058500 경상북도 안동시 서구동 +4717060000 경상북도 안동시 태화동 +4717062000 경상북도 안동시 평화동 +4717063000 경상북도 안동시 안기동 +4717065000 경상북도 안동시 옥동 +4717066000 경상북도 안동시 송하동 +4717069000 경상북도 안동시 강남동 +4719025000 경상북도 구미시 선산읍 +4719025300 경상북도 구미시 고아읍 +4719025600 경상북도 구미시 산동읍 +4719031000 경상북도 구미시 무을면 +4719032000 경상북도 구미시 옥성면 +4719033000 경상북도 구미시 도개면 +4719034000 경상북도 구미시 해평면 +4719036000 경상북도 구미시 장천면 +4719051000 경상북도 구미시 송정동 +4719053500 경상북도 구미시 원평동 +4719055100 경상북도 구미시 도량동 +4719055500 경상북도 구미시 지산동 +4719056500 경상북도 구미시 선주원남동 +4719058200 경상북도 구미시 형곡1동 +4719058300 경상북도 구미시 형곡2동 +4719059000 경상북도 구미시 신평1동 +4719060000 경상북도 구미시 신평2동 +4719061000 경상북도 구미시 비산동 +4719063000 경상북도 구미시 광평동 +4719064500 경상북도 구미시 상모사곡동 +4719066000 경상북도 구미시 임오동 +4719067000 경상북도 구미시 인동동 +4719068000 경상북도 구미시 진미동 +4719069000 경상북도 구미시 양포동 +4719070000 경상북도 구미시 공단동 +4721025000 경상북도 영주시 풍기읍 +4721031000 경상북도 영주시 이산면 +4721032000 경상북도 영주시 평은면 +4721033000 경상북도 영주시 문수면 +4721034000 경상북도 영주시 장수면 +4721035000 경상북도 영주시 안정면 +4721036000 경상북도 영주시 봉현면 +4721037000 경상북도 영주시 순흥면 +4721038000 경상북도 영주시 단산면 +4721039000 경상북도 영주시 부석면 +4721051000 경상북도 영주시 상망동 +4721052500 경상북도 영주시 하망동 +4721055000 경상북도 영주시 영주1동 +4721056000 경상북도 영주시 영주2동 +4721059000 경상북도 영주시 휴천1동 +4721060000 경상북도 영주시 휴천2동 +4721061000 경상북도 영주시 휴천3동 +4721062000 경상북도 영주시 가흥1동 +4721063000 경상북도 영주시 가흥2동 +4723025000 경상북도 영천시 금호읍 +4723031000 경상북도 영천시 청통면 +4723032000 경상북도 영천시 신녕면 +4723033000 경상북도 영천시 화산면 +4723034000 경상북도 영천시 화북면 +4723035000 경상북도 영천시 화남면 +4723036000 경상북도 영천시 자양면 +4723037000 경상북도 영천시 임고면 +4723038000 경상북도 영천시 고경면 +4723039000 경상북도 영천시 북안면 +4723040000 경상북도 영천시 대창면 +4723051000 경상북도 영천시 동부동 +4723052000 경상북도 영천시 중앙동 +4723053500 경상북도 영천시 서부동 +4723054000 경상북도 영천시 완산동 +4723055500 경상북도 영천시 남부동 +4725025000 경상북도 상주시 함창읍 +4725031000 경상북도 상주시 중동면 +4725032500 경상북도 상주시 사벌국면 +4725033000 경상북도 상주시 낙동면 +4725033500 경상북도 상주시 낙동면동부출장소 +4725034000 경상북도 상주시 청리면 +4725035000 경상북도 상주시 공성면 +4725036000 경상북도 상주시 외남면 +4725037000 경상북도 상주시 내서면 +4725038000 경상북도 상주시 모동면 +4725039000 경상북도 상주시 모서면 +4725039500 경상북도 상주시 모서면서부출장소 +4725040000 경상북도 상주시 화동면 +4725041000 경상북도 상주시 화서면 +4725042000 경상북도 상주시 화북면 +4725042600 경상북도 상주시 화북면서부출장소 +4725043000 경상북도 상주시 외서면 +4725044000 경상북도 상주시 은척면 +4725044500 경상북도 상주시 은척면북부출장소 +4725045000 경상북도 상주시 공검면 +4725046000 경상북도 상주시 이안면 +4725047000 경상북도 상주시 화남면 +4725052000 경상북도 상주시 남원동 +4725053000 경상북도 상주시 북문동 +4725054000 경상북도 상주시 계림동 +4725055000 경상북도 상주시 동문동 +4725056000 경상북도 상주시 동성동 +4725057000 경상북도 상주시 신흥동 +4728025000 경상북도 문경시 문경읍 +4728025100 경상북도 문경시 문경읍갈평출장소 +4728025300 경상북도 문경시 가은읍 +4728025400 경상북도 문경시 가은읍북부출장소 +4728031000 경상북도 문경시 영순면 +4728032000 경상북도 문경시 산양면 +4728033000 경상북도 문경시 호계면 +4728034000 경상북도 문경시 산북면 +4728035000 경상북도 문경시 동로면 +4728036000 경상북도 문경시 마성면 +4728037000 경상북도 문경시 농암면 +4728057000 경상북도 문경시 점촌1동 +4728058000 경상북도 문경시 점촌2동 +4728059000 경상북도 문경시 점촌3동 +4728060000 경상북도 문경시 점촌4동 +4728061000 경상북도 문경시 점촌5동 +4729025000 경상북도 경산시 하양읍 +4729025300 경상북도 경산시 진량읍 +4729025600 경상북도 경산시 압량읍 +4729031000 경상북도 경산시 와촌면 +4729033000 경상북도 경산시 자인면 +4729034000 경상북도 경산시 용성면 +4729035000 경상북도 경산시 남산면 +4729037000 경상북도 경산시 남천면 +4729051000 경상북도 경산시 중방동 +4729052000 경상북도 경산시 중앙동 +4729053000 경상북도 경산시 남부동 +4729054100 경상북도 경산시 서부1동 +4729054200 경상북도 경산시 서부2동 +4729055000 경상북도 경산시 북부동 +4729056000 경상북도 경산시 동부동 +4773025000 경상북도 의성군 의성읍 +4773031000 경상북도 의성군 단촌면 +4773032000 경상북도 의성군 점곡면 +4773033000 경상북도 의성군 옥산면 +4773034000 경상북도 의성군 사곡면 +4773035000 경상북도 의성군 춘산면 +4773036000 경상북도 의성군 가음면 +4773037000 경상북도 의성군 금성면 +4773038000 경상북도 의성군 봉양면 +4773039000 경상북도 의성군 비안면 +4773040000 경상북도 의성군 구천면 +4773041000 경상북도 의성군 단밀면 +4773042000 경상북도 의성군 단북면 +4773043000 경상북도 의성군 안계면 +4773044000 경상북도 의성군 다인면 +4773045000 경상북도 의성군 신평면 +4773046000 경상북도 의성군 안평면 +4773047000 경상북도 의성군 안사면 +4775025000 경상북도 청송군 청송읍 +4775031500 경상북도 청송군 주왕산면 +4775032000 경상북도 청송군 부남면 +4775033000 경상북도 청송군 현동면 +4775034000 경상북도 청송군 현서면 +4775035000 경상북도 청송군 안덕면 +4775036000 경상북도 청송군 파천면 +4775037000 경상북도 청송군 진보면 +4776025000 경상북도 영양군 영양읍 +4776031000 경상북도 영양군 입암면 +4776032000 경상북도 영양군 청기면 +4776033000 경상북도 영양군 일월면 +4776034000 경상북도 영양군 수비면 +4776035000 경상북도 영양군 석보면 +4777025000 경상북도 영덕군 영덕읍 +4777031000 경상북도 영덕군 강구면 +4777032000 경상북도 영덕군 남정면 +4777033000 경상북도 영덕군 달산면 +4777034000 경상북도 영덕군 지품면 +4777034500 경상북도 영덕군 지품면원전출장소 +4777035000 경상북도 영덕군 축산면 +4777035500 경상북도 영덕군 축산면축산출장소 +4777036000 경상북도 영덕군 영해면 +4777037000 경상북도 영덕군 병곡면 +4777038000 경상북도 영덕군 창수면 +4782025000 경상북도 청도군 화양읍 +4782025100 경상북도 청도군 화양읍남성현출장소 +4782025300 경상북도 청도군 청도읍 +4782025400 경상북도 청도군 청도읍유호출장소 +4782031000 경상북도 청도군 각남면 +4782032000 경상북도 청도군 풍각면 +4782033000 경상북도 청도군 각북면 +4782034000 경상북도 청도군 이서면 +4782035000 경상북도 청도군 운문면 +4782036000 경상북도 청도군 금천면 +4782037000 경상북도 청도군 매전면 +4783025300 경상북도 고령군 대가야읍 +4783031000 경상북도 고령군 덕곡면 +4783032000 경상북도 고령군 운수면 +4783033000 경상북도 고령군 성산면 +4783034000 경상북도 고령군 다산면 +4783035000 경상북도 고령군 개진면 +4783036000 경상북도 고령군 우곡면 +4783037000 경상북도 고령군 쌍림면 +4784025000 경상북도 성주군 성주읍 +4784031000 경상북도 성주군 선남면 +4784032000 경상북도 성주군 용암면 +4784033000 경상북도 성주군 수륜면 +4784034000 경상북도 성주군 가천면 +4784035000 경상북도 성주군 금수면 +4784036000 경상북도 성주군 대가면 +4784037000 경상북도 성주군 벽진면 +4784038000 경상북도 성주군 초전면 +4784039000 경상북도 성주군 월항면 +4785025000 경상북도 칠곡군 왜관읍 +4785025300 경상북도 칠곡군 북삼읍 +4785025600 경상북도 칠곡군 석적읍 +4785031000 경상북도 칠곡군 지천면 +4785032000 경상북도 칠곡군 동명면 +4785033000 경상북도 칠곡군 가산면 +4785036000 경상북도 칠곡군 약목면 +4785037000 경상북도 칠곡군 기산면 +4790025000 경상북도 예천군 예천읍 +4790031000 경상북도 예천군 용문면 +4790034000 경상북도 예천군 감천면 +4790035000 경상북도 예천군 보문면 +4790036000 경상북도 예천군 호명면 +4790037000 경상북도 예천군 유천면 +4790038000 경상북도 예천군 용궁면 +4790039000 경상북도 예천군 개포면 +4790040000 경상북도 예천군 지보면 +4790041000 경상북도 예천군 풍양면 +4790042000 경상북도 예천군 효자면 +4790043000 경상북도 예천군 은풍면 +4792025000 경상북도 봉화군 봉화읍 +4792031000 경상북도 봉화군 물야면 +4792032000 경상북도 봉화군 봉성면 +4792033000 경상북도 봉화군 법전면 +4792034000 경상북도 봉화군 춘양면 +4792035000 경상북도 봉화군 소천면 +4792036000 경상북도 봉화군 재산면 +4792037000 경상북도 봉화군 명호면 +4792038000 경상북도 봉화군 상운면 +4792039000 경상북도 봉화군 석포면 +4793025000 경상북도 울진군 울진읍 +4793025300 경상북도 울진군 평해읍 +4793031000 경상북도 울진군 북면 +4793031500 경상북도 울진군 북면하당출장소 +4793033000 경상북도 울진군 근남면 +4793035000 경상북도 울진군 기성면 +4793036000 경상북도 울진군 온정면 +4793037000 경상북도 울진군 죽변면 +4793038000 경상북도 울진군 후포면 +4793039000 경상북도 울진군 금강송면 +4793040000 경상북도 울진군 매화면 +4794025000 경상북도 울릉군 울릉읍 +4794031000 경상북도 울릉군 서면 +4794031500 경상북도 울릉군 서면태하출장소 +4794032000 경상북도 울릉군 북면 +4812125000 경상남도 창원시 의창구 동읍 +4812131000 경상남도 창원시 의창구 북면 +4812132000 경상남도 창원시 의창구 대산면 +4812151000 경상남도 창원시 의창구 의창동 +4812152000 경상남도 창원시 의창구 팔룡동 +4812153000 경상남도 창원시 의창구 명곡동 +4812154000 경상남도 창원시 의창구 봉림동 +4812351000 경상남도 창원시 성산구 반송동 +4812351500 경상남도 창원시 성산구 용지동 +4812352000 경상남도 창원시 성산구 중앙동 +4812353000 경상남도 창원시 성산구 상남동 +4812354000 경상남도 창원시 성산구 사파동 +4812355000 경상남도 창원시 성산구 가음정동 +4812356000 경상남도 창원시 성산구 성주동 +4812357000 경상남도 창원시 성산구 웅남동 +4812531000 경상남도 창원시 마산합포구 구산면 +4812532000 경상남도 창원시 마산합포구 진동면 +4812533000 경상남도 창원시 마산합포구 진북면 +4812534000 경상남도 창원시 마산합포구 진전면 +4812551000 경상남도 창원시 마산합포구 현동 +4812552000 경상남도 창원시 마산합포구 가포동 +4812553000 경상남도 창원시 마산합포구 월영동 +4812554000 경상남도 창원시 마산합포구 문화동 +4812556500 경상남도 창원시 마산합포구 반월중앙동 +4812557000 경상남도 창원시 마산합포구 완월동 +4812558000 경상남도 창원시 마산합포구 자산동 +4812561000 경상남도 창원시 마산합포구 교방동 +4812563000 경상남도 창원시 마산합포구 오동동 +4812564000 경상남도 창원시 마산합포구 합포동 +4812565000 경상남도 창원시 마산합포구 산호동 +4812725000 경상남도 창원시 마산회원구 내서읍 +4812751000 경상남도 창원시 마산회원구 회원1동 +4812752000 경상남도 창원시 마산회원구 회원2동 +4812754500 경상남도 창원시 마산회원구 석전동 +4812755000 경상남도 창원시 마산회원구 회성동 +4812756000 경상남도 창원시 마산회원구 양덕1동 +4812757000 경상남도 창원시 마산회원구 양덕2동 +4812758000 경상남도 창원시 마산회원구 합성1동 +4812759000 경상남도 창원시 마산회원구 합성2동 +4812760000 경상남도 창원시 마산회원구 구암1동 +4812761000 경상남도 창원시 마산회원구 구암2동 +4812762000 경상남도 창원시 마산회원구 봉암동 +4812953000 경상남도 창원시 진해구 충무동 +4812954000 경상남도 창원시 진해구 여좌동 +4812955000 경상남도 창원시 진해구 태백동 +4812956000 경상남도 창원시 진해구 경화동 +4812957000 경상남도 창원시 진해구 병암동 +4812958000 경상남도 창원시 진해구 석동 +4812959000 경상남도 창원시 진해구 이동 +4812960000 경상남도 창원시 진해구 자은동 +4812961000 경상남도 창원시 진해구 덕산동 +4812962000 경상남도 창원시 진해구 풍호동 +4812963000 경상남도 창원시 진해구 웅천동 +4812964000 경상남도 창원시 진해구 웅동1동 +4812965000 경상남도 창원시 진해구 웅동2동 +4817025000 경상남도 진주시 문산읍 +4817031000 경상남도 진주시 내동면 +4817032000 경상남도 진주시 정촌면 +4817033000 경상남도 진주시 금곡면 +4817035000 경상남도 진주시 진성면 +4817036000 경상남도 진주시 일반성면 +4817037000 경상남도 진주시 이반성면 +4817038000 경상남도 진주시 사봉면 +4817039000 경상남도 진주시 지수면 +4817040000 경상남도 진주시 대곡면 +4817041000 경상남도 진주시 금산면 +4817042000 경상남도 진주시 집현면 +4817043000 경상남도 진주시 미천면 +4817044000 경상남도 진주시 명석면 +4817045000 경상남도 진주시 대평면 +4817046000 경상남도 진주시 수곡면 +4817051500 경상남도 진주시 천전동 +4817055500 경상남도 진주시 성북동 +4817056500 경상남도 진주시 중앙동 +4817059500 경상남도 진주시 상봉동 +4817067300 경상남도 진주시 상대동 +4817067800 경상남도 진주시 하대동 +4817068000 경상남도 진주시 상평동 +4817069500 경상남도 진주시 초장동 +4817071000 경상남도 진주시 평거동 +4817071500 경상남도 진주시 신안동 +4817072000 경상남도 진주시 이현동 +4817073000 경상남도 진주시 판문동 +4817074000 경상남도 진주시 가호동 +4817075000 경상남도 진주시 충무공동 +4822025000 경상남도 통영시 산양읍 +4822031000 경상남도 통영시 용남면 +4822033000 경상남도 통영시 도산면 +4822034000 경상남도 통영시 광도면 +4822035000 경상남도 통영시 욕지면 +4822036000 경상남도 통영시 한산면 +4822037000 경상남도 통영시 사량면 +4822051000 경상남도 통영시 도천동 +4822053000 경상남도 통영시 명정동 +4822055000 경상남도 통영시 중앙동 +4822059000 경상남도 통영시 정량동 +4822060000 경상남도 통영시 북신동 +4822066500 경상남도 통영시 미수동 +4822067000 경상남도 통영시 봉평동 +4822070000 경상남도 통영시 무전동 +4824025000 경상남도 사천시 사천읍 +4824031000 경상남도 사천시 정동면 +4824032000 경상남도 사천시 사남면 +4824033000 경상남도 사천시 용현면 +4824034000 경상남도 사천시 축동면 +4824035000 경상남도 사천시 곤양면 +4824036000 경상남도 사천시 곤명면 +4824037000 경상남도 사천시 서포면 +4824051000 경상남도 사천시 동서동 +4824052000 경상남도 사천시 선구동 +4824053000 경상남도 사천시 동서금동 +4824055000 경상남도 사천시 벌용동 +4824057000 경상남도 사천시 향촌동 +4824059500 경상남도 사천시 남양동 +4824089000 경상남도 사천시 신수출장소 +4825025000 경상남도 김해시 진영읍 +4825032000 경상남도 김해시 주촌면 +4825033000 경상남도 김해시 진례면 +4825034000 경상남도 김해시 한림면 +4825035000 경상남도 김해시 생림면 +4825036000 경상남도 김해시 상동면 +4825037000 경상남도 김해시 대동면 +4825051000 경상남도 김해시 동상동 +4825052000 경상남도 김해시 회현동 +4825053000 경상남도 김해시 부원동 +4825054000 경상남도 김해시 내외동 +4825055000 경상남도 김해시 북부동 +4825056500 경상남도 김해시 칠산서부동 +4825058000 경상남도 김해시 활천동 +4825059000 경상남도 김해시 삼안동 +4825060000 경상남도 김해시 불암동 +4825061000 경상남도 김해시 장유1동 +4825062000 경상남도 김해시 장유2동 +4825063000 경상남도 김해시 장유3동 +4827025000 경상남도 밀양시 삼랑진읍 +4827025100 경상남도 밀양시 삼랑진임천출장소 +4827025300 경상남도 밀양시 하남읍 +4827031000 경상남도 밀양시 부북면 +4827032000 경상남도 밀양시 상동면 +4827033000 경상남도 밀양시 산외면 +4827034000 경상남도 밀양시 산내면 +4827035000 경상남도 밀양시 단장면 +4827036000 경상남도 밀양시 상남면 +4827037000 경상남도 밀양시 초동면 +4827038000 경상남도 밀양시 무안면 +4827039000 경상남도 밀양시 청도면 +4827051000 경상남도 밀양시 내일동 +4827052000 경상남도 밀양시 내이동 +4827053000 경상남도 밀양시 삼문동 +4827054000 경상남도 밀양시 가곡동 +4827055000 경상남도 밀양시 교동 +4831031000 경상남도 거제시 일운면 +4831032000 경상남도 거제시 동부면 +4831033000 경상남도 거제시 남부면 +4831034000 경상남도 거제시 거제면 +4831035000 경상남도 거제시 둔덕면 +4831036000 경상남도 거제시 사등면 +4831036600 경상남도 거제시 사등면가조출장소 +4831037000 경상남도 거제시 연초면 +4831038000 경상남도 거제시 하청면 +4831038500 경상남도 거제시 하청면칠천출장소 +4831039000 경상남도 거제시 장목면 +4831039500 경상남도 거제시 장목면외포출장소 +4831051000 경상남도 거제시 장승포동 +4831053000 경상남도 거제시 능포동 +4831054000 경상남도 거제시 아주동 +4831055000 경상남도 거제시 옥포1동 +4831056000 경상남도 거제시 옥포2동 +4831057000 경상남도 거제시 장평동 +4831058000 경상남도 거제시 고현동 +4831059000 경상남도 거제시 상문동 +4831060000 경상남도 거제시 수양동 +4833025300 경상남도 양산시 물금읍 +4833031000 경상남도 양산시 동면 +4833032000 경상남도 양산시 원동면 +4833033000 경상남도 양산시 상북면 +4833034000 경상남도 양산시 하북면 +4833051000 경상남도 양산시 중앙동 +4833051500 경상남도 양산시 양주동 +4833052000 경상남도 양산시 삼성동 +4833053000 경상남도 양산시 강서동 +4833054000 경상남도 양산시 서창동 +4833055000 경상남도 양산시 소주동 +4833056000 경상남도 양산시 평산동 +4833057000 경상남도 양산시 덕계동 +4872025000 경상남도 의령군 의령읍 +4872031000 경상남도 의령군 가례면 +4872032000 경상남도 의령군 칠곡면 +4872033000 경상남도 의령군 대의면 +4872034000 경상남도 의령군 화정면 +4872035000 경상남도 의령군 용덕면 +4872036000 경상남도 의령군 정곡면 +4872037000 경상남도 의령군 지정면 +4872038000 경상남도 의령군 낙서면 +4872039000 경상남도 의령군 부림면 +4872040000 경상남도 의령군 봉수면 +4872041500 경상남도 의령군 궁류면 +4872042000 경상남도 의령군 유곡면 +4873025000 경상남도 함안군 가야읍 +4873025300 경상남도 함안군 칠원읍 +4873031000 경상남도 함안군 함안면 +4873032000 경상남도 함안군 군북면 +4873033000 경상남도 함안군 법수면 +4873034000 경상남도 함안군 대산면 +4873035000 경상남도 함안군 칠서면 +4873036000 경상남도 함안군 칠북면 +4873038000 경상남도 함안군 산인면 +4873039000 경상남도 함안군 여항면 +4874025000 경상남도 창녕군 창녕읍 +4874025300 경상남도 창녕군 남지읍 +4874031000 경상남도 창녕군 고암면 +4874032000 경상남도 창녕군 성산면 +4874033000 경상남도 창녕군 대합면 +4874034000 경상남도 창녕군 이방면 +4874035000 경상남도 창녕군 유어면 +4874036000 경상남도 창녕군 대지면 +4874037000 경상남도 창녕군 계성면 +4874038000 경상남도 창녕군 영산면 +4874039000 경상남도 창녕군 장마면 +4874040000 경상남도 창녕군 도천면 +4874041000 경상남도 창녕군 길곡면 +4874042000 경상남도 창녕군 부곡면 +4882025000 경상남도 고성군 고성읍 +4882031000 경상남도 고성군 삼산면 +4882032000 경상남도 고성군 하일면 +4882033000 경상남도 고성군 하이면 +4882034000 경상남도 고성군 상리면 +4882035000 경상남도 고성군 대가면 +4882036000 경상남도 고성군 영현면 +4882037000 경상남도 고성군 영오면 +4882038000 경상남도 고성군 개천면 +4882039000 경상남도 고성군 구만면 +4882040000 경상남도 고성군 회화면 +4882041000 경상남도 고성군 마암면 +4882042000 경상남도 고성군 동해면 +4882043000 경상남도 고성군 거류면 +4884025000 경상남도 남해군 남해읍 +4884031000 경상남도 남해군 이동면 +4884032000 경상남도 남해군 상주면 +4884033000 경상남도 남해군 삼동면 +4884034000 경상남도 남해군 미조면 +4884035000 경상남도 남해군 남면 +4884036000 경상남도 남해군 서면 +4884037000 경상남도 남해군 고현면 +4884038000 경상남도 남해군 설천면 +4884039000 경상남도 남해군 창선면 +4885025000 경상남도 하동군 하동읍 +4885031000 경상남도 하동군 화개면 +4885032000 경상남도 하동군 악양면 +4885033000 경상남도 하동군 적량면 +4885034000 경상남도 하동군 횡천면 +4885035000 경상남도 하동군 고전면 +4885036000 경상남도 하동군 금남면 +4885037000 경상남도 하동군 진교면 +4885038000 경상남도 하동군 양보면 +4885039000 경상남도 하동군 북천면 +4885040000 경상남도 하동군 청암면 +4885041000 경상남도 하동군 옥종면 +4885042000 경상남도 하동군 금성면 +4886025000 경상남도 산청군 산청읍 +4886031000 경상남도 산청군 차황면 +4886032000 경상남도 산청군 오부면 +4886033000 경상남도 산청군 생초면 +4886034000 경상남도 산청군 금서면 +4886035000 경상남도 산청군 삼장면 +4886036000 경상남도 산청군 시천면 +4886037000 경상남도 산청군 단성면 +4886038000 경상남도 산청군 신안면 +4886039000 경상남도 산청군 생비량면 +4886040000 경상남도 산청군 신등면 +4887025000 경상남도 함양군 함양읍 +4887031000 경상남도 함양군 마천면 +4887032000 경상남도 함양군 휴천면 +4887033000 경상남도 함양군 유림면 +4887034000 경상남도 함양군 수동면 +4887035000 경상남도 함양군 지곡면 +4887036000 경상남도 함양군 안의면 +4887037000 경상남도 함양군 서하면 +4887038000 경상남도 함양군 서상면 +4887039000 경상남도 함양군 백전면 +4887040000 경상남도 함양군 병곡면 +4888025000 경상남도 거창군 거창읍 +4888031000 경상남도 거창군 주상면 +4888032000 경상남도 거창군 웅양면 +4888033000 경상남도 거창군 고제면 +4888034000 경상남도 거창군 북상면 +4888035000 경상남도 거창군 위천면 +4888036000 경상남도 거창군 마리면 +4888037000 경상남도 거창군 남상면 +4888038000 경상남도 거창군 남하면 +4888039000 경상남도 거창군 신원면 +4888040000 경상남도 거창군 가조면 +4888041000 경상남도 거창군 가북면 +4889025000 경상남도 합천군 합천읍 +4889031000 경상남도 합천군 봉산면 +4889032000 경상남도 합천군 묘산면 +4889033000 경상남도 합천군 가야면 +4889034000 경상남도 합천군 야로면 +4889035000 경상남도 합천군 율곡면 +4889036000 경상남도 합천군 초계면 +4889037000 경상남도 합천군 쌍책면 +4889038000 경상남도 합천군 덕곡면 +4889039000 경상남도 합천군 청덕면 +4889040000 경상남도 합천군 적중면 +4889041000 경상남도 합천군 대양면 +4889042000 경상남도 합천군 쌍백면 +4889043000 경상남도 합천군 삼가면 +4889044000 경상남도 합천군 가회면 +4889045000 경상남도 합천군 대병면 +4889046000 경상남도 합천군 용주면 +5011025000 제주특별자치도 제주시 한림읍 +5011025300 제주특별자치도 제주시 애월읍 +5011025600 제주특별자치도 제주시 구좌읍 +5011025900 제주특별자치도 제주시 조천읍 +5011031000 제주특별자치도 제주시 한경면 +5011032000 제주특별자치도 제주시 추자면 +5011033000 제주특별자치도 제주시 우도면 +5011051000 제주특별자치도 제주시 일도1동 +5011052000 제주특별자치도 제주시 일도2동 +5011053000 제주특별자치도 제주시 이도1동 +5011054000 제주특별자치도 제주시 이도2동 +5011055000 제주특별자치도 제주시 삼도1동 +5011056000 제주특별자치도 제주시 삼도2동 +5011057000 제주특별자치도 제주시 용담1동 +5011058000 제주특별자치도 제주시 용담2동 +5011059000 제주특별자치도 제주시 건입동 +5011060000 제주특별자치도 제주시 화북동 +5011061000 제주특별자치도 제주시 삼양동 +5011062000 제주특별자치도 제주시 봉개동 +5011063000 제주특별자치도 제주시 아라동 +5011064000 제주특별자치도 제주시 오라동 +5011065000 제주특별자치도 제주시 연동 +5011066000 제주특별자치도 제주시 노형동 +5011067000 제주특별자치도 제주시 외도동 +5011068000 제주특별자치도 제주시 이호동 +5011069000 제주특별자치도 제주시 도두동 +5013025000 제주특별자치도 서귀포시 대정읍 +5013025300 제주특별자치도 서귀포시 남원읍 +5013025900 제주특별자치도 서귀포시 성산읍 +5013031000 제주특별자치도 서귀포시 안덕면 +5013032000 제주특별자치도 서귀포시 표선면 +5013051000 제주특별자치도 서귀포시 송산동 +5013052000 제주특별자치도 서귀포시 정방동 +5013053000 제주특별자치도 서귀포시 중앙동 +5013054000 제주특별자치도 서귀포시 천지동 +5013055000 제주특별자치도 서귀포시 효돈동 +5013056000 제주특별자치도 서귀포시 영천동 +5013057000 제주특별자치도 서귀포시 동홍동 +5013058000 제주특별자치도 서귀포시 서홍동 +5013059000 제주특별자치도 서귀포시 대륜동 +5013060000 제주특별자치도 서귀포시 대천동 +5013061000 제주특별자치도 서귀포시 중문동 +5013062000 제주특별자치도 서귀포시 예래동 +5111025000 강원특별자치도 춘천시 신북읍 +5111031000 강원특별자치도 춘천시 동면 +5111032000 강원특별자치도 춘천시 동산면 +5111033000 강원특별자치도 춘천시 신동면 +5111034000 강원특별자치도 춘천시 남면 +5111035000 강원특별자치도 춘천시 서면 +5111036000 강원특별자치도 춘천시 사북면 +5111038000 강원특별자치도 춘천시 북산면 +5111039000 강원특별자치도 춘천시 동내면 +5111040000 강원특별자치도 춘천시 남산면 +5111052000 강원특별자치도 춘천시 교동 +5111053000 강원특별자치도 춘천시 조운동 +5111054500 강원특별자치도 춘천시 약사명동 +5111057000 강원특별자치도 춘천시 근화동 +5111058000 강원특별자치도 춘천시 소양동 +5111060000 강원특별자치도 춘천시 후평1동 +5111061000 강원특별자치도 춘천시 후평2동 +5111061100 강원특별자치도 춘천시 후평3동 +5111062000 강원특별자치도 춘천시 효자1동 +5111063000 강원특별자치도 춘천시 효자2동 +5111064000 강원특별자치도 춘천시 효자3동 +5111065000 강원특별자치도 춘천시 석사동 +5111066000 강원특별자치도 춘천시 퇴계동 +5111067500 강원특별자치도 춘천시 강남동 +5111070500 강원특별자치도 춘천시 신사우동 +5113025000 강원특별자치도 원주시 문막읍 +5113031000 강원특별자치도 원주시 소초면 +5113032000 강원특별자치도 원주시 호저면 +5113033000 강원특별자치도 원주시 지정면 +5113035000 강원특별자치도 원주시 부론면 +5113036000 강원특별자치도 원주시 귀래면 +5113037000 강원특별자치도 원주시 흥업면 +5113038000 강원특별자치도 원주시 판부면 +5113039000 강원특별자치도 원주시 신림면 +5113051500 강원특별자치도 원주시 중앙동 +5113052000 강원특별자치도 원주시 원인동 +5113053000 강원특별자치도 원주시 개운동 +5113054100 강원특별자치도 원주시 명륜1동 +5113054200 강원특별자치도 원주시 명륜2동 +5113055000 강원특별자치도 원주시 단구동 +5113056000 강원특별자치도 원주시 일산동 +5113057500 강원특별자치도 원주시 학성동 +5113059000 강원특별자치도 원주시 단계동 +5113060000 강원특별자치도 원주시 우산동 +5113061000 강원특별자치도 원주시 태장1동 +5113062000 강원특별자치도 원주시 태장2동 +5113063500 강원특별자치도 원주시 봉산동 +5113065000 강원특별자치도 원주시 행구동 +5113066000 강원특별자치도 원주시 무실동 +5113067500 강원특별자치도 원주시 반곡관설동 +5115025000 강원특별자치도 강릉시 주문진읍 +5115031000 강원특별자치도 강릉시 성산면 +5115032000 강원특별자치도 강릉시 왕산면 +5115033000 강원특별자치도 강릉시 구정면 +5115034000 강원특별자치도 강릉시 강동면 +5115035000 강원특별자치도 강릉시 옥계면 +5115036000 강원특별자치도 강릉시 사천면 +5115037000 강원특별자치도 강릉시 연곡면 +5115051000 강원특별자치도 강릉시 홍제동 +5115052000 강원특별자치도 강릉시 중앙동 +5115054000 강원특별자치도 강릉시 옥천동 +5115055000 강원특별자치도 강릉시 교1동 +5115056000 강원특별자치도 강릉시 교2동 +5115057100 강원특별자치도 강릉시 포남1동 +5115057200 강원특별자치도 강릉시 포남2동 +5115058000 강원특별자치도 강릉시 초당동 +5115059000 강원특별자치도 강릉시 송정동 +5115060000 강원특별자치도 강릉시 내곡동 +5115061500 강원특별자치도 강릉시 강남동 +5115064500 강원특별자치도 강릉시 성덕동 +5115066500 강원특별자치도 강릉시 경포동 +5117051000 강원특별자치도 동해시 천곡동 +5117052000 강원특별자치도 동해시 송정동 +5117053000 강원특별자치도 동해시 북삼동 +5117054000 강원특별자치도 동해시 부곡동 +5117055000 강원특별자치도 동해시 동호동 +5117057000 강원특별자치도 동해시 발한동 +5117059000 강원특별자치도 동해시 묵호동 +5117060000 강원특별자치도 동해시 북평동 +5117063000 강원특별자치도 동해시 망상동 +5117065000 강원특별자치도 동해시 삼화동 +5119051500 강원특별자치도 태백시 황지동 +5119052500 강원특별자치도 태백시 황연동 +5119053500 강원특별자치도 태백시 삼수동 +5119054000 강원특별자치도 태백시 상장동 +5119058500 강원특별자치도 태백시 문곡소도동 +5119059500 강원특별자치도 태백시 장성동 +5119060500 강원특별자치도 태백시 구문소동 +5119061500 강원특별자치도 태백시 철암동 +5121051000 강원특별자치도 속초시 영랑동 +5121052000 강원특별자치도 속초시 동명동 +5121054000 강원특별자치도 속초시 금호동 +5121056000 강원특별자치도 속초시 교동 +5121057000 강원특별자치도 속초시 노학동 +5121058000 강원특별자치도 속초시 조양동 +5121059000 강원특별자치도 속초시 청호동 +5121060000 강원특별자치도 속초시 대포동 +5123025000 강원특별자치도 삼척시 도계읍 +5123025300 강원특별자치도 삼척시 원덕읍 +5123025400 강원특별자치도 삼척시 원덕읍임원출장소 +5123031000 강원특별자치도 삼척시 근덕면 +5123032000 강원특별자치도 삼척시 하장면 +5123033000 강원특별자치도 삼척시 노곡면 +5123034000 강원특별자치도 삼척시 미로면 +5123035000 강원특별자치도 삼척시 가곡면 +5123036000 강원특별자치도 삼척시 신기면 +5123051000 강원특별자치도 삼척시 남양동 +5123053000 강원특별자치도 삼척시 교동 +5123054000 강원특별자치도 삼척시 정라동 +5123057000 강원특별자치도 삼척시 성내동 +5172025000 강원특별자치도 홍천군 홍천읍 +5172031000 강원특별자치도 홍천군 화촌면 +5172032000 강원특별자치도 홍천군 두촌면 +5172033000 강원특별자치도 홍천군 내촌면 +5172034000 강원특별자치도 홍천군 서석면 +5172035200 강원특별자치도 홍천군 영귀미면 +5172036000 강원특별자치도 홍천군 남면 +5172037000 강원특별자치도 홍천군 서면 +5172038000 강원특별자치도 홍천군 북방면 +5172039000 강원특별자치도 홍천군 내면 +5173025000 강원특별자치도 횡성군 횡성읍 +5173031000 강원특별자치도 횡성군 우천면 +5173032000 강원특별자치도 횡성군 안흥면 +5173033000 강원특별자치도 횡성군 둔내면 +5173034000 강원특별자치도 횡성군 갑천면 +5173035000 강원특별자치도 횡성군 청일면 +5173036000 강원특별자치도 횡성군 공근면 +5173037000 강원특별자치도 횡성군 서원면 +5173038000 강원특별자치도 횡성군 강림면 +5175025000 강원특별자치도 영월군 영월읍 +5175025300 강원특별자치도 영월군 상동읍 +5175031200 강원특별자치도 영월군 산솔면 +5175032500 강원특별자치도 영월군 김삿갓면 +5175033000 강원특별자치도 영월군 북면 +5175034000 강원특별자치도 영월군 남면 +5175035500 강원특별자치도 영월군 한반도면 +5175035900 강원특별자치도 영월군 한반도면쌍용출장소 +5175036000 강원특별자치도 영월군 주천면 +5175038000 강원특별자치도 영월군 무릉도원면 +5176025000 강원특별자치도 평창군 평창읍 +5176031000 강원특별자치도 평창군 미탄면 +5176032000 강원특별자치도 평창군 방림면 +5176032500 강원특별자치도 평창군 방림면계촌출장소 +5176033000 강원특별자치도 평창군 대화면 +5176034000 강원특별자치도 평창군 봉평면 +5176035000 강원특별자치도 평창군 용평면 +5176036000 강원특별자치도 평창군 진부면 +5176038000 강원특별자치도 평창군 대관령면 +5177025000 강원특별자치도 정선군 정선읍 +5177025300 강원특별자치도 정선군 고한읍 +5177025600 강원특별자치도 정선군 사북읍 +5177025900 강원특별자치도 정선군 신동읍 +5177026000 강원특별자치도 정선군 신동읍함백출장소 +5177032000 강원특별자치도 정선군 남면 +5177034000 강원특별자치도 정선군 북평면 +5177035000 강원특별자치도 정선군 임계면 +5177036000 강원특별자치도 정선군 화암면 +5177037000 강원특별자치도 정선군 여량면 +5178025000 강원특별자치도 철원군 철원읍 +5178025300 강원특별자치도 철원군 김화읍 +5178025600 강원특별자치도 철원군 갈말읍 +5178025900 강원특별자치도 철원군 동송읍 +5178031000 강원특별자치도 철원군 서면 +5178031500 강원특별자치도 철원군 서면와수출장소 +5178032000 강원특별자치도 철원군 근남면 +5178033000 강원특별자치도 철원군 근북면 +5178034000 강원특별자치도 철원군 근동면 +5178035000 강원특별자치도 철원군 원동면 +5178036000 강원특별자치도 철원군 원남면 +5178037000 강원특별자치도 철원군 임남면 +5179025000 강원특별자치도 화천군 화천읍 +5179031000 강원특별자치도 화천군 간동면 +5179032000 강원특별자치도 화천군 하남면 +5179033000 강원특별자치도 화천군 상서면 +5179034000 강원특별자치도 화천군 사내면 +5180025000 강원특별자치도 양구군 양구읍 +5180031500 강원특별자치도 양구군 국토정중앙면 +5180032000 강원특별자치도 양구군 동면 +5180033000 강원특별자치도 양구군 방산면 +5180034000 강원특별자치도 양구군 해안면 +5181025000 강원특별자치도 인제군 인제읍 +5181025100 강원특별자치도 인제군 인제읍귀둔출장소 +5181031000 강원특별자치도 인제군 남면 +5181032000 강원특별자치도 인제군 북면 +5181033000 강원특별자치도 인제군 기린면 +5181034000 강원특별자치도 인제군 서화면 +5181035000 강원특별자치도 인제군 상남면 +5182025000 강원특별자치도 고성군 간성읍 +5182025300 강원특별자치도 고성군 거진읍 +5182031000 강원특별자치도 고성군 현내면 +5182032000 강원특별자치도 고성군 죽왕면 +5182033000 강원특별자치도 고성군 토성면 +5182034000 강원특별자치도 고성군 수동면 +5183025000 강원특별자치도 양양군 양양읍 +5183031000 강원특별자치도 양양군 서면 +5183032000 강원특별자치도 양양군 손양면 +5183033000 강원특별자치도 양양군 현북면 +5183034000 강원특별자치도 양양군 현남면 +5183035000 강원특별자치도 양양군 강현면 \ No newline at end of file diff --git a/core/common/src/main/java/com/umcspot/spot/common/location/Location.kt b/core/common/src/main/java/com/umcspot/spot/common/location/Location.kt index ecb032b2..40838a85 100644 --- a/core/common/src/main/java/com/umcspot/spot/common/location/Location.kt +++ b/core/common/src/main/java/com/umcspot/spot/common/location/Location.kt @@ -8,8 +8,16 @@ import java.nio.charset.Charset data class LocationRow( val code: String, - val name: String, -) + val province: String, + val district: String, + val neighborhood: String, +) { + /** 리스트/검색용 전체 이름 */ + val fullName: String + get() = listOf(province, district, neighborhood) + .filter { it.isNotBlank() } + .joinToString(" ") +} object LocationStore { @Volatile private var cache: List? = null @@ -20,14 +28,12 @@ object LocationStore { return@withContext it } - Log.d("LocationStore", "📂 Loading Location_info.txt from assets...") + Log.d("LocationStore", "📂 Loading region_data.tsv from assets...") val lines = try { - context.assets.open("Location_info.txt") - .bufferedReader(Charset.forName("EUC-KR")) - .use { - it.readLines() - } + context.assets.open("region_data.tsv") + .bufferedReader(Charset.forName("UTF-8")) // 파일 인코딩이 다르면 여기만 변경 + .use { it.readLines() } } catch (e: Exception) { Log.e("LocationStore", "❌ Failed to load asset: ${e.message}", e) emptyList() @@ -35,16 +41,29 @@ object LocationStore { Log.d("LocationStore", "📄 Read ${lines.size} lines") - val parsed = lines.mapNotNull { line -> - val parts = line.split('\t') - if (parts.size < 3) return@mapNotNull null - val code = parts[0].trim() - val name = parts[1].trim() - val status = parts[2].trim() - if (status != "존재") null else LocationRow(code, name) - } + // 첫 줄이 헤더(code province district neighborhood)라고 가정 + val parsed = lines + .drop(1) // 헤더 제거 + .mapNotNull { line -> + val parts = line.split('\t') + if (parts.size < 4) return@mapNotNull null + + val code = parts[0].trim() + val province = parts[1].trim() + val district = parts[2].trim() + val neighborhood = parts[3].trim() + + if (code.isBlank() || neighborhood.isBlank()) return@mapNotNull null - Log.d("LocationStore", "✅ Parsed ${parsed.size} valid rows") + LocationRow( + code = code, + province = province, + district = district, + neighborhood = neighborhood + ) + } + + Log.d("LocationStore", "✅ Parsed ${parsed.size} rows") cache = parsed parsed @@ -55,9 +74,11 @@ fun searchLocations(query: String, list: List, limit: Int = 20): Li if (query.isBlank()) return emptyList() val normalized = query.trim().replace(" ", "").lowercase() - val results = list.filter { - it.name.replace(" ", "").lowercase().contains(normalized) - }.take(limit) - return results + return list + .filter { + it.fullName.replace(" ", "").lowercase().contains(normalized) + } + .take(limit) } + diff --git a/core/designsystem/src/main/java/com/umcspot/spot/designsystem/component/bottomsheet/LocationBottomSheet.kt b/core/designsystem/src/main/java/com/umcspot/spot/designsystem/component/bottomsheet/LocationBottomSheet.kt index 7311819e..99fd2f99 100644 --- a/core/designsystem/src/main/java/com/umcspot/spot/designsystem/component/bottomsheet/LocationBottomSheet.kt +++ b/core/designsystem/src/main/java/com/umcspot/spot/designsystem/component/bottomsheet/LocationBottomSheet.kt @@ -84,9 +84,9 @@ fun LocationBottomSheet( results: List, onQueryChange: (String) -> Unit, onDismiss: () -> Unit, - selected: List, - onAddSelected: (String) -> Unit, - onRemoveSelected: (String) -> Unit + selected: List, + onAddSelected: (LocationRow) -> Unit, + onRemoveSelected: (LocationRow) -> Unit ) { if (!visible) return @@ -97,8 +97,8 @@ fun LocationBottomSheet( val density = LocalDensity.current val screenHeight = LocalConfiguration.current.screenHeightDp.dp - val sheetHeight = screenHeightDp(533.dp) + val scope = rememberCoroutineScope() val sheetOffset = remember { Animatable(with(density) { screenHeight.toPx() }) } val navBarPadding = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() @@ -125,6 +125,7 @@ fun LocationBottomSheet( sheetOffset.animateTo(openY, animationSpec = tween(300)) } } + if (visible) { Dialog( onDismissRequest = { animateAndDismiss() }, @@ -150,7 +151,7 @@ fun LocationBottomSheet( ) Box( - Modifier + modifier = Modifier .fillMaxWidth() .height(sheetHeight) .offset { IntOffset(0, sheetOffset.value.roundToInt()) } @@ -163,7 +164,6 @@ fun LocationBottomSheet( indication = null, interactionSource = remember { MutableInteractionSource() } ) { - blurFocusRequester.requestFocus() keyboard?.hide() } @@ -173,7 +173,6 @@ fun LocationBottomSheet( .fillMaxSize() .padding(horizontal = screenWidthDp(17.dp)) ) { - Row( modifier = Modifier .fillMaxWidth() @@ -191,6 +190,7 @@ fun LocationBottomSheet( Icon( painter = painterResource(R.drawable.dismiss), contentDescription = "닫기", + modifier = Modifier.size(screenWidthDp(13.dp)) ) } } @@ -206,7 +206,7 @@ fun LocationBottomSheet( Spacer(Modifier.height(screenHeightDp(6.dp))) Text( - text = "최대 3개까지 추가할 수 있어요", + text = "최대 10개까지 추가할 수 있어요", style = SpotTheme.typography.h5, color = SpotTheme.colors.gray400 ) @@ -221,7 +221,7 @@ fun LocationBottomSheet( Text( text = "OO시, OO구, OO동", style = SpotTheme.typography.h5, - color = SpotTheme.colors.gray400 + color = SpotTheme.colors.gray300 ) }, trailingIcon = { @@ -238,7 +238,8 @@ fun LocationBottomSheet( Icon( painter = painterResource(R.drawable.search), contentDescription = "검색", - modifier = Modifier.size(18.dp) + modifier = Modifier.size(screenWidthDp(18.dp)), + tint = SpotTheme.colors.gray400 ) } }, @@ -246,7 +247,7 @@ fun LocationBottomSheet( shape = RoundedCornerShape(10.dp), colors = OutlinedTextFieldDefaults.colors( focusedBorderColor = SpotTheme.colors.B500, - unfocusedBorderColor = SpotTheme.colors.gray400, + unfocusedBorderColor = SpotTheme.colors.gray300, cursorColor = SpotTheme.colors.B500 ), modifier = Modifier @@ -264,7 +265,7 @@ fun LocationBottomSheet( ) if (results.isNotEmpty()) { - val isMaxSelected = selected.size >= 3 + val isMaxSelected = selected.size >= 10 HorizontalDivider(thickness = 0.5.dp, color = SpotTheme.colors.G200) LazyColumn( @@ -274,11 +275,11 @@ fun LocationBottomSheet( .background(SpotTheme.colors.white), ) { itemsIndexed(results, key = { _, row -> row.code }) { index, row -> - val isAlreadySelected = selected.contains(row.name) + val isAlreadySelected = selected.any { it.code == row.code } ListItem( headlineContent = { Text( - text = row.name, + text = row.fullName, style = SpotTheme.typography.h5, maxLines = 1, color = if (isMaxSelected && !isAlreadySelected) { @@ -297,7 +298,7 @@ fun LocationBottomSheet( enabled = !isMaxSelected || isAlreadySelected, onClick = { if (!isAlreadySelected) { - onAddSelected(row.name) + onAddSelected(row) } } ) @@ -331,8 +332,8 @@ fun LocationBottomSheet( @OptIn(ExperimentalLayoutApi::class) @Composable private fun SelectedChips( - items: List, - onRemove: (String) -> Unit + items: List, + onRemove: (LocationRow) -> Unit ) { if (items.isEmpty()) return @@ -345,12 +346,12 @@ private fun SelectedChips( .horizontalScroll(scrollState), horizontalArrangement = Arrangement.spacedBy(screenWidthDp(7.dp)) ) { - items.forEach { name -> + items.forEach { it -> AssistChip( onClick = {}, label = { Text( - name, + text = it.neighborhood, style = SpotTheme.typography.small_400 ) }, @@ -361,7 +362,7 @@ private fun SelectedChips( tint = SpotTheme.colors.B500, modifier = Modifier .size(14.dp) - .clickable { onRemove(name) } + .clickable { onRemove(it) } ) }, colors = AssistChipDefaults.assistChipColors( diff --git a/core/model/src/main/java/com/umcspot/spot/model/Global.kt b/core/model/src/main/java/com/umcspot/spot/model/Global.kt index 246be251..792c537d 100644 --- a/core/model/src/main/java/com/umcspot/spot/model/Global.kt +++ b/core/model/src/main/java/com/umcspot/spot/model/Global.kt @@ -73,6 +73,14 @@ enum class StudyStyle { STRUCTURED_AND_PLANNED } +enum class RecruitingStatus( + val value: String +) { + RECRUITING("모집중"), + COMPLETED("모집완료"), +// BEFORE("모집전") +} + enum class SocialLoginType( val title: String ) { diff --git a/data/study/build.gradle.kts b/data/study/build.gradle.kts index a7a83092..6613122b 100644 --- a/data/study/build.gradle.kts +++ b/data/study/build.gradle.kts @@ -8,4 +8,5 @@ android { dependencies { implementation(projects.domain.study) implementation(projects.core.ui) +// implementation(projects.data.user) } \ No newline at end of file diff --git a/data/study/src/main/java/com/umcspot/spot/study/datasource/StudyDataSource.kt b/data/study/src/main/java/com/umcspot/spot/study/datasource/StudyDataSource.kt index 98a40bd3..d215e096 100644 --- a/data/study/src/main/java/com/umcspot/spot/study/datasource/StudyDataSource.kt +++ b/data/study/src/main/java/com/umcspot/spot/study/datasource/StudyDataSource.kt @@ -1,9 +1,8 @@ package com.umcspot.spot.study.datasource -import com.umcspot.spot.model.ActivityType import com.umcspot.spot.model.FeeRange +import com.umcspot.spot.model.RecruitingStatus import com.umcspot.spot.model.RecruitingStudySort -import com.umcspot.spot.model.StudyTheme import com.umcspot.spot.network.model.BaseResponse import com.umcspot.spot.study.dto.request.StudyRequestDto import com.umcspot.spot.study.dto.response.CreateStudyResponseDto @@ -14,5 +13,14 @@ interface StudyDataSource { suspend fun getPopularStudies(): BaseResponse suspend fun getRecommendStudies(): BaseResponse suspend fun getRecruitingStudies(feeCategory: FeeRange?, categories: List?, isOnline: Boolean?, sortBy: RecruitingStudySort?, cursor: Long?, size: Int): BaseResponse + suspend fun getPreferLocationStudies( + recruitingStatus : RecruitingStatus?, + feeCategory: FeeRange?, + categories: List?, + sortType: RecruitingStudySort?, + cursor: Long?, + size: Int, + regionCodes : List? + ): BaseResponse suspend fun createStudy(request: StudyRequestDto, imageFile: File?): BaseResponse } \ No newline at end of file diff --git a/data/study/src/main/java/com/umcspot/spot/study/datasourceimpl/StudyDataSourceImpl.kt b/data/study/src/main/java/com/umcspot/spot/study/datasourceimpl/StudyDataSourceImpl.kt index bac3fc4f..84124157 100644 --- a/data/study/src/main/java/com/umcspot/spot/study/datasourceimpl/StudyDataSourceImpl.kt +++ b/data/study/src/main/java/com/umcspot/spot/study/datasourceimpl/StudyDataSourceImpl.kt @@ -1,6 +1,7 @@ package com.umcspot.spot.study.datasourceimpl import com.umcspot.spot.model.FeeRange +import com.umcspot.spot.model.RecruitingStatus import com.umcspot.spot.model.RecruitingStudySort import com.umcspot.spot.network.model.BaseResponse import com.umcspot.spot.study.datasource.StudyDataSource @@ -36,6 +37,18 @@ class StudyDataSourceImpl @Inject constructor( ): BaseResponse = studyService.getRecruitingStudies(feeCategory, categories, isOnline, sortBy, cursor, size) + override suspend fun getPreferLocationStudies( + recruitingStatus: RecruitingStatus?, + feeCategory: FeeRange?, + categories: List?, + sortType: RecruitingStudySort?, + cursor: Long?, + size: Int, + regionCodes: List? + ): BaseResponse = + studyService.getPreferLocationStudies(recruitingStatus, feeCategory, categories, null, sortType, cursor, size, regionCodes) + + override suspend fun createStudy( request: StudyRequestDto, imageFile: File? diff --git a/data/study/src/main/java/com/umcspot/spot/study/repositoryimpl/StudyRepositoryImpl.kt b/data/study/src/main/java/com/umcspot/spot/study/repositoryimpl/StudyRepositoryImpl.kt index 52232321..bb2987c9 100644 --- a/data/study/src/main/java/com/umcspot/spot/study/repositoryimpl/StudyRepositoryImpl.kt +++ b/data/study/src/main/java/com/umcspot/spot/study/repositoryimpl/StudyRepositoryImpl.kt @@ -1,10 +1,9 @@ package com.umcspot.spot.study.repositoryimpl import android.util.Log -import com.umcspot.spot.model.ActivityType import com.umcspot.spot.model.FeeRange +import com.umcspot.spot.model.RecruitingStatus import com.umcspot.spot.model.RecruitingStudySort -import com.umcspot.spot.model.StudyTheme import com.umcspot.spot.study.datasource.StudyDataSource import com.umcspot.spot.study.mapper.toData import com.umcspot.spot.study.mapper.toDomainList @@ -15,7 +14,7 @@ import java.io.File import javax.inject.Inject class StudyRepositoryImpl @Inject constructor( - private val studyDataSource: StudyDataSource + private val studyDataSource: StudyDataSource, ) : StudyRepository { override suspend fun getPopularStudies(): Result = runCatching { @@ -59,20 +58,28 @@ class StudyRepositoryImpl @Inject constructor( } override suspend fun getPreferLocationStudies( - sortType: RecruitingStudySort, - activityType: ActivityType?, - theme: StudyTheme?, - feeRange: FeeRange? - ): Result = TODO() -// runCatching { -// val response = studyDataSource.getRecruitingStudies( -// sortType = sortType, -// activityType = activityType ?: ActivityType.OFFLINE, -// theme = theme ?: StudyTheme.OTHER, -// feeRange = feeRange ?: FeeRange.NONE -// ) -// response.result.toDomain() -// } + recruitingStatus: RecruitingStatus?, + feeRange: FeeRange?, + categories: List?, + sortBy: RecruitingStudySort?, + cursor: Long?, + size: Int, + regionCodes : List? + ): Result = + runCatching { + val response = studyDataSource.getPreferLocationStudies( + recruitingStatus = recruitingStatus, + feeCategory = feeRange, + categories = categories, + sortType = sortBy, + cursor = cursor, + size = size, + regionCodes = regionCodes + ) + response.result.toDomainList() + }.onFailure { + Log.e("StudyRepository", "getPreferLocationStudies failed", it) + } override suspend fun createStudy( studyCreateModel: StudyCreateModel, diff --git a/data/study/src/main/java/com/umcspot/spot/study/service/StudyService.kt b/data/study/src/main/java/com/umcspot/spot/study/service/StudyService.kt index 42605168..382c38b4 100644 --- a/data/study/src/main/java/com/umcspot/spot/study/service/StudyService.kt +++ b/data/study/src/main/java/com/umcspot/spot/study/service/StudyService.kt @@ -2,6 +2,7 @@ package com.umcspot.spot.study.service import com.umcspot.spot.model.ActivityType import com.umcspot.spot.model.FeeRange +import com.umcspot.spot.model.RecruitingStatus import com.umcspot.spot.model.RecruitingStudySort import com.umcspot.spot.model.StudyTheme import com.umcspot.spot.network.model.BaseResponse @@ -35,14 +36,16 @@ interface StudyService { @Query("size") size: Int ): BaseResponse - @GET("/api/studies/preferLocation") + @GET("/api/studies/by-region") suspend fun getPreferLocationStudies( - @Query("feeCategory") feeCategory: FeeRange, - @Query("categories") categories: List, - @Query("isOnline") isOnline: Boolean, - @Query("sortBy") sortBy: RecruitingStudySort, + @Query("recruitingStatus") recruitingStatus: RecruitingStatus?, + @Query("feeCategory") feeCategory: FeeRange?, + @Query("categories") categories: List?, + @Query("isOnline") isOnline: Boolean?, + @Query("sortBy") sortBy: RecruitingStudySort?, @Query("cursor") cursor: Long?, - @Query("size") size: Int + @Query("size") size: Int, + @Query("regionCodes") regionCodes: List? ): BaseResponse @Multipart diff --git a/data/user/src/main/java/com/umcspot/spot/user/datasource/UserDataSource.kt b/data/user/src/main/java/com/umcspot/spot/user/datasource/UserDataSource.kt index 352abf67..8a603648 100644 --- a/data/user/src/main/java/com/umcspot/spot/user/datasource/UserDataSource.kt +++ b/data/user/src/main/java/com/umcspot/spot/user/datasource/UserDataSource.kt @@ -1,17 +1,16 @@ package com.umcspot.spot.user.datasource -import com.umcspot.spot.model.StudyTheme import com.umcspot.spot.network.model.BaseResponse import com.umcspot.spot.network.model.NullResultResponse import com.umcspot.spot.user.dto.request.UserNameRequestDto import com.umcspot.spot.user.dto.request.UserThemeRequestDto +import com.umcspot.spot.user.dto.response.UserPreferredRegionResponseDto import com.umcspot.spot.user.dto.response.UserResponseDto -import com.umcspot.spot.user.dto.response.UserThemeResponseDto interface UserDataSource { suspend fun getUser(): BaseResponse - suspend fun setUserName(name : UserNameRequestDto) : NullResultResponse suspend fun setUserTheme(themes : UserThemeRequestDto): NullResultResponse - + suspend fun setUserPreferredRegion(regions : List) : NullResultResponse + suspend fun getUserPreferredRegion() : BaseResponse } \ No newline at end of file diff --git a/data/user/src/main/java/com/umcspot/spot/user/datasourceimpl/UserDataSourceImpl.kt b/data/user/src/main/java/com/umcspot/spot/user/datasourceimpl/UserDataSourceImpl.kt index f5abb230..90565c8c 100644 --- a/data/user/src/main/java/com/umcspot/spot/user/datasourceimpl/UserDataSourceImpl.kt +++ b/data/user/src/main/java/com/umcspot/spot/user/datasourceimpl/UserDataSourceImpl.kt @@ -1,13 +1,16 @@ package com.umcspot.spot.user.datasourceimpl +import android.util.Log import com.umcspot.spot.model.StudyTheme import com.umcspot.spot.network.model.BaseResponse import com.umcspot.spot.network.model.NullResultResponse import com.umcspot.spot.user.datasource.UserDataSource import com.umcspot.spot.user.dto.request.UserNameRequestDto import com.umcspot.spot.user.dto.request.UserThemeRequestDto +import com.umcspot.spot.user.dto.response.UserPreferredRegionResponseDto import com.umcspot.spot.user.dto.response.UserResponseDto import com.umcspot.spot.user.dto.response.UserThemeResponseDto +import com.umcspot.spot.user.mapper.toPreferredRegionRequestDto import com.umcspot.spot.user.service.UserService import javax.inject.Inject @@ -31,4 +34,15 @@ class UserDataSourceImpl @Inject constructor( ): NullResultResponse = userService.setUserTheme(themes) + override suspend fun setUserPreferredRegion(regions: List): NullResultResponse { + Log.d("UserDataSource", "setUserPreferredRegion: ${regions.toPreferredRegionRequestDto()}") + + val res = userService.setUserPreferredRegion(regions.toPreferredRegionRequestDto()) + return res + } + + + + override suspend fun getUserPreferredRegion(): BaseResponse = + userService.getUserPreferredRegion() } \ No newline at end of file diff --git a/data/user/src/main/java/com/umcspot/spot/user/dto/request/UserPreferredRegionRequestDto.kt b/data/user/src/main/java/com/umcspot/spot/user/dto/request/UserPreferredRegionRequestDto.kt new file mode 100644 index 00000000..76890662 --- /dev/null +++ b/data/user/src/main/java/com/umcspot/spot/user/dto/request/UserPreferredRegionRequestDto.kt @@ -0,0 +1,11 @@ +package com.umcspot.spot.user.dto.request + +import com.umcspot.spot.model.StudyTheme +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class UserPreferredRegionRequestDto( + @SerialName("regionCodes") + val regionCodes: List +) \ No newline at end of file diff --git a/data/user/src/main/java/com/umcspot/spot/user/dto/response/UserPreferredRegionResponseDto.kt b/data/user/src/main/java/com/umcspot/spot/user/dto/response/UserPreferredRegionResponseDto.kt new file mode 100644 index 00000000..42c283bf --- /dev/null +++ b/data/user/src/main/java/com/umcspot/spot/user/dto/response/UserPreferredRegionResponseDto.kt @@ -0,0 +1,16 @@ +package com.umcspot.spot.user.dto.response + +import android.annotation.SuppressLint +import com.umcspot.spot.model.WeatherType +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@SuppressLint("UnsafeOptInUsageError") +@Serializable +data class UserPreferredRegionResponseDto( + @SerialName("regionCodes") + val regionCodes: List, + + @SerialName("totalCount") + val totalCount: Int +) \ No newline at end of file diff --git a/data/user/src/main/java/com/umcspot/spot/user/mapper/UserMapper.kt b/data/user/src/main/java/com/umcspot/spot/user/mapper/UserMapper.kt index 30882c80..078d47ce 100644 --- a/data/user/src/main/java/com/umcspot/spot/user/mapper/UserMapper.kt +++ b/data/user/src/main/java/com/umcspot/spot/user/mapper/UserMapper.kt @@ -2,10 +2,13 @@ package com.umcspot.spot.user.mapper import com.umcspot.spot.model.StudyTheme import com.umcspot.spot.user.dto.request.UserNameRequestDto +import com.umcspot.spot.user.dto.request.UserPreferredRegionRequestDto import com.umcspot.spot.user.dto.request.UserThemeRequestDto +import com.umcspot.spot.user.dto.response.UserPreferredRegionResponseDto import com.umcspot.spot.user.model.UserResult import com.umcspot.spot.user.dto.response.UserResponseDto import com.umcspot.spot.user.dto.response.UserThemeResponseDto +import com.umcspot.spot.user.model.UserPreferredRegionResult import com.umcspot.spot.user.model.UserTheme import java.time.LocalTime import java.time.format.DateTimeFormatter @@ -24,8 +27,14 @@ fun UserResponseDto.toDomain(): UserResult = name = this.name ) -fun UserThemeResponseDto.toDomain(): UserTheme = - UserTheme( - userThemes = userThemes +fun List.toPreferredRegionRequestDto() : UserPreferredRegionRequestDto = + UserPreferredRegionRequestDto( + regionCodes = this + ) + +fun UserPreferredRegionResponseDto.toDomain(): UserPreferredRegionResult = + UserPreferredRegionResult( + regionCodes = regionCodes, + totalCount = totalCount ) diff --git a/data/user/src/main/java/com/umcspot/spot/user/repositoryimpl/UserRepositoryImpl.kt b/data/user/src/main/java/com/umcspot/spot/user/repositoryimpl/UserRepositoryImpl.kt index a6393a53..6f5c7b44 100644 --- a/data/user/src/main/java/com/umcspot/spot/user/repositoryimpl/UserRepositoryImpl.kt +++ b/data/user/src/main/java/com/umcspot/spot/user/repositoryimpl/UserRepositoryImpl.kt @@ -1,20 +1,21 @@ package com.umcspot.spot.user.repositoryimpl +import android.util.Log import com.umcspot.spot.model.StudyTheme +import com.umcspot.spot.user.datasource.UserDataSource import com.umcspot.spot.user.mapper.toDomain import com.umcspot.spot.user.mapper.toRequestDto +import com.umcspot.spot.user.model.UserPreferredRegionResult import com.umcspot.spot.user.model.UserResult -import com.umcspot.spot.user.model.UserTheme import com.umcspot.spot.user.repository.UserRepository -import com.umcspot.spot.user.service.UserService import javax.inject.Inject class UserRepositoryImpl @Inject constructor( - private val userService: UserService + private val userDataSource: UserDataSource ) : UserRepository { override suspend fun getUserName(): Result = runCatching { - val userName = userService.getUser() + val userName = userDataSource.getUser() userName.result.toDomain() }.recoverCatching { UserResult(name = "123") @@ -22,12 +23,26 @@ class UserRepositoryImpl @Inject constructor( override suspend fun setUserName(name: String): Result = runCatching { - userService.setUserName(name.toRequestDto()) + userDataSource.setUserName(name.toRequestDto()) } override suspend fun setUserTheme(theme: List): Result = runCatching { - userService.setUserTheme(theme.toRequestDto()) + userDataSource.setUserTheme(theme.toRequestDto()) + } + + override suspend fun setUserPreferredRegion(regions: List): Result = + runCatching { + userDataSource.setUserPreferredRegion(regions) + } + + + override suspend fun getUserPreferredRegion(): Result = + runCatching { + val userPreferredRegions = userDataSource.getUserPreferredRegion() + userPreferredRegions.result.toDomain() + }.onFailure { e -> + Log.e("UserRepository", "getUserPreferredRegion failed", e) } } \ No newline at end of file diff --git a/data/user/src/main/java/com/umcspot/spot/user/service/UserService.kt b/data/user/src/main/java/com/umcspot/spot/user/service/UserService.kt index f357bb36..b7f0c2c1 100644 --- a/data/user/src/main/java/com/umcspot/spot/user/service/UserService.kt +++ b/data/user/src/main/java/com/umcspot/spot/user/service/UserService.kt @@ -3,7 +3,9 @@ package com.umcspot.spot.user.service import com.umcspot.spot.network.model.BaseResponse import com.umcspot.spot.network.model.NullResultResponse import com.umcspot.spot.user.dto.request.UserNameRequestDto +import com.umcspot.spot.user.dto.request.UserPreferredRegionRequestDto import com.umcspot.spot.user.dto.request.UserThemeRequestDto +import com.umcspot.spot.user.dto.response.UserPreferredRegionResponseDto import com.umcspot.spot.user.dto.response.UserResponseDto import retrofit2.http.Body import retrofit2.http.GET @@ -23,4 +25,13 @@ interface UserService { suspend fun setUserTheme( @Body request : UserThemeRequestDto ): NullResultResponse + + @POST("/api/members/preferred-regions") + suspend fun setUserPreferredRegion( + @Body request : UserPreferredRegionRequestDto + ): NullResultResponse + + @GET("/api/members/prefer-regions") + suspend fun getUserPreferredRegion( + ): BaseResponse } \ No newline at end of file diff --git a/domain/study/src/main/java/com/umcspot/spot/study/repository/StudyRepository.kt b/domain/study/src/main/java/com/umcspot/spot/study/repository/StudyRepository.kt index 5f1c7ec2..3cada009 100644 --- a/domain/study/src/main/java/com/umcspot/spot/study/repository/StudyRepository.kt +++ b/domain/study/src/main/java/com/umcspot/spot/study/repository/StudyRepository.kt @@ -2,6 +2,7 @@ package com.umcspot.spot.study.repository import com.umcspot.spot.model.ActivityType import com.umcspot.spot.model.FeeRange +import com.umcspot.spot.model.RecruitingStatus import com.umcspot.spot.model.RecruitingStudySort import com.umcspot.spot.model.StudyTheme import com.umcspot.spot.study.model.StudyCreateModel @@ -12,19 +13,22 @@ interface StudyRepository { suspend fun getPopularStudies(): Result suspend fun getRecommendStudies(): Result suspend fun getRecruitingStudies( - feeCategory: FeeRange? = null, - categories: List? = null, - isOnline: Boolean? = null, - sortBy: RecruitingStudySort? = null, - cursor: Long? = null, size: Int + feeCategory: FeeRange?, + categories: List?, + isOnline: Boolean?, + sortBy: RecruitingStudySort?, + cursor: Long?, + size: Int ): Result - suspend fun getPreferLocationStudies( - sortType: RecruitingStudySort, - activityType: ActivityType?, - theme: StudyTheme?, - feeRange: FeeRange? + recruitingStatus : RecruitingStatus?, + feeRange: FeeRange?, + categories: List?, + sortBy: RecruitingStudySort?, + cursor: Long?, + size: Int, + regionCodes : List? ): Result suspend fun createStudy(studyCreateModel: StudyCreateModel, imageFile: File?): Result diff --git a/domain/user/src/main/java/com/umcspot/spot/user/model/UserPreferredRegionResult.kt b/domain/user/src/main/java/com/umcspot/spot/user/model/UserPreferredRegionResult.kt new file mode 100644 index 00000000..98938c97 --- /dev/null +++ b/domain/user/src/main/java/com/umcspot/spot/user/model/UserPreferredRegionResult.kt @@ -0,0 +1,7 @@ +package com.umcspot.spot.user.model + + +data class UserPreferredRegionResult( + val regionCodes : List, + val totalCount: Int +) \ No newline at end of file diff --git a/domain/user/src/main/java/com/umcspot/spot/user/repository/UserRepository.kt b/domain/user/src/main/java/com/umcspot/spot/user/repository/UserRepository.kt index 41016da2..b7d8c2f7 100644 --- a/domain/user/src/main/java/com/umcspot/spot/user/repository/UserRepository.kt +++ b/domain/user/src/main/java/com/umcspot/spot/user/repository/UserRepository.kt @@ -1,6 +1,7 @@ package com.umcspot.spot.user.repository import com.umcspot.spot.model.StudyTheme +import com.umcspot.spot.user.model.UserPreferredRegionResult import com.umcspot.spot.user.model.UserResult import com.umcspot.spot.user.model.UserTheme @@ -8,4 +9,6 @@ interface UserRepository { suspend fun getUserName() : Result suspend fun setUserName(name : String) : Result suspend fun setUserTheme(theme : List) : Result + suspend fun setUserPreferredRegion(regions: List) : Result + suspend fun getUserPreferredRegion() : Result } \ No newline at end of file diff --git a/feature/main/build.gradle.kts b/feature/main/build.gradle.kts index 6ec763aa..b05b1f83 100644 --- a/feature/main/build.gradle.kts +++ b/feature/main/build.gradle.kts @@ -8,15 +8,12 @@ android { dependencies { //start destination implementation(projects.feature.signup) - implementation(projects.feature.home) implementation(projects.feature.category) implementation(projects.feature.jjim) implementation(projects.feature.mypage) implementation(projects.feature.study) - implementation(projects.feature.board) - implementation(projects.feature.alert) implementation(projects.core.model) diff --git a/feature/main/src/main/java/com/umcspot/spot/main/MainNavHost.kt b/feature/main/src/main/java/com/umcspot/spot/main/MainNavHost.kt index ef85ba4f..e673eeb3 100644 --- a/feature/main/src/main/java/com/umcspot/spot/main/MainNavHost.kt +++ b/feature/main/src/main/java/com/umcspot/spot/main/MainNavHost.kt @@ -26,6 +26,7 @@ import com.umcspot.spot.feature.board.post.posting.navigation.postingGraph import com.umcspot.spot.signup.navigation.signupGraph import com.umcspot.spot.study.detail.navigation.studyDetailGraph import com.umcspot.spot.study.my.navigation.myStudyGraph +import com.umcspot.spot.study.preferLocation.navigation.preferLocationStudyGraph import com.umcspot.spot.study.recruiting.navigation.recruitingStudyFilterGraph import com.umcspot.spot.study.recruiting.navigation.recruitingStudyGraph import com.umcspot.spot.study.register.navigation.RegisterStudy @@ -67,7 +68,7 @@ fun MainNavHost( onQuickMenuClick = { type -> when (type) { QuickMenuType.BOARD -> navigator.navigateToBoard() - QuickMenuType.REGION -> /*navigator.navigateToPreferLocationStudy()*/{} + QuickMenuType.REGION -> navigator.navigateToPreferLocationStudy() QuickMenuType.INTERESTS -> { /* TODO */ } @@ -93,12 +94,12 @@ fun MainNavHost( onAcceptFilterClick = { navigator.popBackStack() } ) -// preferLocationStudyGraph( -// contentPadding = contentPadding, -// onRegisterScrollToTop = onRegisterScrollToTop, -// onItemClick = { }, -// onFilterClick = { }, -// ) + preferLocationStudyGraph( + contentPadding = contentPadding, + onRegisterScrollToTop = onRegisterScrollToTop, + onItemClick = { }, + onFilterClick = { navigator.navigateToPreferLocationStudyFilter() }, + ) boardGraph( contentPadding = contentPadding, diff --git a/feature/main/src/main/java/com/umcspot/spot/main/MainNavigator.kt b/feature/main/src/main/java/com/umcspot/spot/main/MainNavigator.kt index 2f533fbe..c2613175 100644 --- a/feature/main/src/main/java/com/umcspot/spot/main/MainNavigator.kt +++ b/feature/main/src/main/java/com/umcspot/spot/main/MainNavigator.kt @@ -33,6 +33,8 @@ import com.umcspot.spot.signup.navigation.navigateToSignUp import com.umcspot.spot.study.detail.navigation.StudyDetail import com.umcspot.spot.study.detail.navigation.navigateToStudyDetail import com.umcspot.spot.study.my.navigation.navigateToMyStudy +import com.umcspot.spot.study.preferLocation.navigation.navigateToPreferLocationStudy +import com.umcspot.spot.study.preferLocation.navigation.navigateToPreferLocationStudyFilter import com.umcspot.spot.study.recruiting.navigation.Recruiting import com.umcspot.spot.study.recruiting.navigation.RecruitingFilter import com.umcspot.spot.study.recruiting.navigation.navigateToRecruitingStudy @@ -153,9 +155,9 @@ class MainNavigator( navController.navigateToBoard(navOptions) } -// fun navigateToPreferLocationStudy(navOptions: NavOptions? = null) { -// navController.navigateToPreferLocationStudy(navOptions) -// } + fun navigateToPreferLocationStudy(navOptions: NavOptions? = null) { + navController.navigateToPreferLocationStudy(navOptions) + } fun navigateToRecruitingStudy(navOptions: NavOptions? = null) { navController.navigateToRecruitingStudy(navOptions) @@ -165,6 +167,10 @@ class MainNavigator( navController.navigateToRecruitingStudyFilter(navOptions) } + fun navigateToPreferLocationStudyFilter(navOptions: NavOptions? = null) { + navController.navigateToPreferLocationStudyFilter(navOptions) + } + fun navigateToAlert(navOptions: NavOptions? = null) { navController.navigateToAlert(navOptions) } diff --git a/feature/study/build.gradle.kts b/feature/study/build.gradle.kts index 5c33bcea..bf92f6c4 100644 --- a/feature/study/build.gradle.kts +++ b/feature/study/build.gradle.kts @@ -8,6 +8,7 @@ android { dependencies { implementation(projects.domain.study) + implementation(projects.domain.user) implementation(projects.core.designsystem) implementation(projects.core.common) diff --git a/feature/study/src/main/java/com/umcspot/spot/study/preferLocation/PreferLocationStudyFilterScreen.kt b/feature/study/src/main/java/com/umcspot/spot/study/preferLocation/PreferLocationStudyFilterScreen.kt index 84ec97c6..ad3b52d7 100644 --- a/feature/study/src/main/java/com/umcspot/spot/study/preferLocation/PreferLocationStudyFilterScreen.kt +++ b/feature/study/src/main/java/com/umcspot/spot/study/preferLocation/PreferLocationStudyFilterScreen.kt @@ -1,300 +1,259 @@ -//package com.umcspot.spot.study.preferLocation -// -//import androidx.compose.foundation.background -//import androidx.compose.foundation.clickable -//import androidx.compose.foundation.interaction.MutableInteractionSource -//import androidx.compose.foundation.layout.Arrangement -//import androidx.compose.foundation.layout.Box -//import androidx.compose.foundation.layout.Column -//import androidx.compose.foundation.layout.FlowRow -//import androidx.compose.foundation.layout.PaddingValues -//import androidx.compose.foundation.layout.Spacer -//import androidx.compose.foundation.layout.fillMaxSize -//import androidx.compose.foundation.layout.fillMaxWidth -//import androidx.compose.foundation.layout.height -//import androidx.compose.foundation.layout.navigationBarsPadding -//import androidx.compose.foundation.layout.padding -//import androidx.compose.foundation.layout.wrapContentSize -//import androidx.compose.foundation.lazy.LazyColumn -//import androidx.compose.foundation.lazy.LazyRow -//import androidx.compose.foundation.lazy.items -//import androidx.compose.foundation.rememberScrollState -//import androidx.compose.foundation.verticalScroll -//import androidx.compose.material3.Text -//import androidx.compose.runtime.Composable -//import androidx.compose.runtime.LaunchedEffect -//import androidx.compose.runtime.getValue -//import androidx.compose.runtime.remember -//import androidx.compose.ui.Alignment -//import androidx.compose.ui.Modifier -//import androidx.compose.ui.res.painterResource -//import androidx.compose.ui.semantics.Role -//import androidx.compose.ui.semantics.role -//import androidx.compose.ui.semantics.semantics -//import androidx.compose.ui.text.style.TextDecoration -//import androidx.compose.ui.tooling.preview.Preview -//import androidx.compose.ui.unit.Dp -//import androidx.compose.ui.unit.dp -//import androidx.compose.ui.unit.sp -//import androidx.compose.ui.zIndex -//import androidx.hilt.navigation.compose.hiltViewModel -//import androidx.lifecycle.compose.collectAsStateWithLifecycle -//import com.umcspot.spot.designsystem.R -//import com.umcspot.spot.designsystem.component.button.MultiButton -//import com.umcspot.spot.designsystem.component.button.MultiButtonM -//import com.umcspot.spot.designsystem.component.button.TextButton -//import com.umcspot.spot.designsystem.component.button.TextButtonM -//import com.umcspot.spot.designsystem.component.button.TextToggleButton -//import com.umcspot.spot.designsystem.theme.SpotTheme -//import com.umcspot.spot.model.ActivityType -//import com.umcspot.spot.model.FeeRange -//import com.umcspot.spot.model.StudyTheme -// -//@Composable -//fun PreferLocationStudyFilterScreen( -// contentPadding : PaddingValues, -// onAcceptFilterClick: () -> Unit, -// vm: PreferLocationStudyFilterViewModel = hiltViewModel(), -//) { -// val activityType by vm.activity.collectAsStateWithLifecycle() -// val fee by vm.fee.collectAsStateWithLifecycle() -// val theme by vm.theme.collectAsStateWithLifecycle() -// val acceptEnabled by vm.notNull.collectAsStateWithLifecycle() -// -// -// val topPad = contentPadding.calculateTopPadding() -// val bottomPad = contentPadding.calculateBottomPadding() -// -// // 적용 이벤트 수신 -// LaunchedEffect(Unit) { -// vm.events.collect { ev -> -// when (ev) { -// is PreferLocationFilterViewModel.Event.Applied -> onAcceptFilterClick() -// } -// } -// } -// -// RecruitingStudyFilterScreenContent( -// modifier = Modifier -// .padding(top = topPad, bottom = bottomPad), -// activityType = activityType, -// fee = fee, -// theme = theme, -// buttonEnabled = acceptEnabled, -// onSetActivity = RecruitingStudyFilterViewModel::setActivity, -// onSetFee = RecruitingStudyFilterViewModel::setFee, -// onSetTheme = RecruitingStudyFilterViewModel::setTheme, -// onReset = RecruitingStudyFilterViewModel::reset, -// onApply = RecruitingStudyFilterViewModel::apply -// ) -//} -// -//@Composable -//fun RecruitingStudyFilterScreenContent( -// activityType: ActivityType?, // ✅ 단일 값 (nullable) -// fee: FeeRange?, // ✅ 단일 값 (nullable) -// theme: StudyTheme?, // ✅ 단일 값 (nullable) -// buttonEnabled : Boolean, -// onSetActivity: (ActivityType) -> Unit, // ✅ set* 로직 (같은 값 다시 누르면 해제는 VM이 처리) -// onSetFee: (FeeRange?) -> Unit, -// onSetTheme: (StudyTheme) -> Unit, -// onReset: () -> Unit, -// onApply: () -> Unit, -// modifier: Modifier = Modifier -//) { -// Box( -// modifier = Modifier -// .fillMaxSize() -// .background(SpotTheme.colors.white) -// ) { -// Column( -// modifier = modifier -// .fillMaxSize() -// .verticalScroll(rememberScrollState()) // ✅ 스크롤 -// .padding(horizontal = 16.dp) -// ) { -// ActivityTypeSection( -// activityType = activityType, -// onSelect = onSetActivity -// ) -// -// Spacer(modifier = Modifier.height(30.dp)) -// -// -// ActivityFeeSection( -// activityFee = fee, -// onSelect = onSetFee -// ) -// -// Spacer(modifier = Modifier.height(30.dp)) -// -// ActivityThemeSection( -// activityTheme = theme, -// onSelect = onSetTheme -// ) -// -// Spacer(modifier = Modifier.height(20.dp)) -// -// ResetFilterText( -// onClick = onReset -// ) -// -// Spacer(Modifier.height(80.dp)) -// } -// -// Box( -// modifier = Modifier -// .align(alignment = Alignment.BottomCenter) -// .fillMaxWidth() -// .navigationBarsPadding() -// .padding(horizontal = 16.dp, vertical = 12.dp) -// .zIndex(1f) // 항상 앞 -// ) { -// -// TextButton( -// text = "검색 결과 보기", -// enabled = buttonEnabled, -// onClick = onApply -// ) -// } -// } -//} -// -//@Composable -//fun ActivityTypeSection( -// activityType: ActivityType?, -// onSelect: (ActivityType) -> Unit -//) { -// Column( -// modifier = Modifier -// .wrapContentSize() -// ) { -// Text( -// text = "활동", -// style = SpotTheme.typography.bodyMedium500.copy(fontSize = 15.sp), -// color = SpotTheme.colors.black -// ) -// -// Spacer(modifier = Modifier.height(10.dp)) -// -// LazyRow(horizontalArrangement = Arrangement.spacedBy(10.dp)) { -// items(ActivityType.entries) { type -> -// val iconRes = when (type) { -// ActivityType.ONLINE -> painterResource( R.drawable.online) -// ActivityType.OFFLINE -> painterResource(R.drawable.offline) -// } -// -// MultiButton( -// text = type.label, -// painter = iconRes, -// width = 140.dp, -// checked = activityType == type, -// onClick = { onSelect(type) }, -// ) -// } -// } -// } -//} -// -//@Composable -//fun ActivityFeeSection( -// activityFee: FeeRange?, -// onSelect: (FeeRange) -> Unit // 누르면 VM의 setActivity 호출 -//) { -// Column( -// modifier = Modifier -// .wrapContentSize() -// .background(SpotTheme.colors.white) -// ) { -// Text( -// text = "활동비", -// style = SpotTheme.typography.bodyMedium500.copy(fontSize = 15.sp), -// color = SpotTheme.colors.black -// ) -// Spacer(modifier = Modifier.height(10.dp)) -// -// FlowRow( -// horizontalArrangement = Arrangement.spacedBy(14.dp), -// verticalArrangement = Arrangement.spacedBy(14.dp) -// ){ -// FeeRange.entries.forEach { fee -> -// TextToggleButton( -// text = fee.label, -// width = 71.dp, -// checked = activityFee == fee, -// onClick = { onSelect(fee) }, -// ) -// } -// } -// } -//} -// -//@Composable -//fun ActivityThemeSection( -// activityTheme: StudyTheme?, -// onSelect : (StudyTheme) -> Unit -//) { -// Column( -// modifier = Modifier -// .fillMaxWidth() -// ) { -// Text( -// text = "스터디 테마", -// style = SpotTheme.typography.bodyMedium500.copy(fontSize = 15.sp), -// color = SpotTheme.colors.black -// ) -// Spacer(modifier = Modifier.height(10.dp)) -// -// FlowRow( -// horizontalArrangement = Arrangement.spacedBy(14.dp), -// verticalArrangement = Arrangement.spacedBy(14.dp) -// ){ -// StudyTheme.entries.forEach { theme -> -// val iconRes = when (theme) { -// StudyTheme.LANGUAGE -> painterResource(R.drawable.language) -// StudyTheme.LICENSE -> painterResource(R.drawable.license) -// StudyTheme.EMPLOYMENT -> painterResource(R.drawable.employment) -// StudyTheme.DISCUSSION -> painterResource(R.drawable.discussion) -// StudyTheme.NEWS -> painterResource(R.drawable.news) -// StudyTheme.SELFSTUDY -> painterResource(R.drawable.self_study) -// StudyTheme.PROJECT -> painterResource(R.drawable.project) -// StudyTheme.CONTEST -> painterResource(R.drawable.contest) -// StudyTheme.MAJOR -> painterResource(R.drawable.major) -// StudyTheme.ETC -> painterResource(R.drawable.resource_else) -// } -// -// MultiButton( -// text = theme.title, -// painter = iconRes, -// checked = activityTheme == theme, -// width = 156.dp, -// onClick = { onSelect(theme) }, -// ) -// } -// } -// } -//} -// -//@Composable -//fun ResetFilterText( -// onClick: () -> Unit, -// modifier: Modifier = Modifier -//) { -// Text( -// text = "필터 초기화", -// color = SpotTheme.colors.gray400, -// style = SpotTheme.typography.bodySmall400.copy( -// fontSize = 13.sp, -// textDecoration = TextDecoration.Underline -// ), -// modifier = modifier -// .semantics { role = Role.Button } -// .clickable( -// interactionSource = remember { MutableInteractionSource() }, -// indication = null, // 리플 없애려면 유지, 리플 원하면 제거 -// onClick = onClick -// ) -// .padding(vertical = 4.dp) // 터치 여유 -// ) -//} -// +package com.umcspot.spot.study.preferLocation + +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.listSaver +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.semantics.role +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.unit.dp +import androidx.compose.ui.zIndex +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.umcspot.spot.designsystem.component.button.TextButton +import com.umcspot.spot.designsystem.component.button.TextButtonState +import com.umcspot.spot.designsystem.component.study.section.ActivityThemeSection +import com.umcspot.spot.designsystem.shapes.SpotShapes +import com.umcspot.spot.designsystem.theme.SpotTheme +import com.umcspot.spot.model.FeeRange +import com.umcspot.spot.model.RecruitingStatus +import com.umcspot.spot.model.StudyTheme +import com.umcspot.spot.ui.extension.screenHeightDp +import com.umcspot.spot.ui.extension.screenWidthDp + +@Composable +fun PreferLocationStudyFilterScreen( + contentPadding : PaddingValues, + onAcceptFilterClick: () -> Unit, + preferLocationVm: PreferLocationStudyViewModel = hiltViewModel(), +) { + val recruitingStatus by preferLocationVm.recruitingStatus.collectAsStateWithLifecycle() + val fee by preferLocationVm.fee.collectAsStateWithLifecycle() + val themes by preferLocationVm.themes.collectAsStateWithLifecycle() + + var draftRecruitingStatus by rememberSaveable { mutableStateOf(recruitingStatus) } + var draftFee by rememberSaveable { mutableStateOf(fee) } + + val themeSaver = listSaver, String>( + save = { list -> list.map { it.name } }, + restore = { names -> names.map { StudyTheme.valueOf(it) } } + ) + var draftThemes by rememberSaveable(stateSaver = themeSaver) { mutableStateOf(themes) } + + val acceptEnabled = true + + val topPad = contentPadding.calculateTopPadding() + val bottomPad = contentPadding.calculateBottomPadding() + + BackHandler { + onAcceptFilterClick() + } + + RecruitingStudyFilterScreenContent( + modifier = Modifier.padding(top = topPad, bottom = bottomPad), + recruitingStatus = recruitingStatus, + fee = draftFee, + themes = draftThemes, + buttonEnabled = acceptEnabled, + onToggleRecruitingStatus = { type -> draftRecruitingStatus = if (draftRecruitingStatus == type) null else type }, + onToggleFee = { fee -> draftFee = if (draftFee == fee) null else fee }, + onToggleTheme = { theme -> draftThemes = if (draftThemes.contains(theme)) draftThemes - theme else draftThemes + theme }, + onReset = { + draftRecruitingStatus = null + draftFee = null + draftThemes = emptyList() + }, + onApply = { + preferLocationVm.applyFilter( + fee = draftFee, + recruitingStatus = draftRecruitingStatus, + themes = draftThemes + ) + onAcceptFilterClick() + } + ) +} + +@Composable +fun RecruitingStudyFilterScreenContent( + recruitingStatus: RecruitingStatus?, + fee: FeeRange?, + themes: List, + buttonEnabled : Boolean, + onToggleRecruitingStatus: (RecruitingStatus) -> Unit, + onToggleFee: (FeeRange) -> Unit, + onToggleTheme: (StudyTheme) -> Unit, + onReset: () -> Unit, + onApply: () -> Unit, + modifier: Modifier = Modifier +) { + Box( + modifier = Modifier + .fillMaxSize() + .background(SpotTheme.colors.white) + ) { + Column( + modifier = modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .padding(horizontal = screenWidthDp(17.dp)) + ) { + + Text( + text = "활동", + style = SpotTheme.typography.h5, + color = SpotTheme.colors.black + ) + + Spacer(modifier = Modifier.height(screenHeightDp(20.dp))) + + RecruitingStatusMultiSection( + recruitingStatus = recruitingStatus, + onSelect = onToggleRecruitingStatus + ) + + Spacer(modifier = Modifier.height(53.dp)) + + Text( + text = "활동비", + style = SpotTheme.typography.h5, + color = SpotTheme.colors.black + ) + + Spacer(modifier = Modifier.height(screenHeightDp(20.dp))) + + ActivityFeeSection( + activityFee = fee, + onSelect = onToggleFee + ) + + Spacer(modifier = Modifier.height(53.dp)) + + ActivityThemeSection( + selectedThemes = themes, + onSelect = onToggleTheme, + maxSelection = 10 + ) + + Spacer(modifier = Modifier.height(33.dp)) + + ResetFilterText( + onClick = onReset + ) + + Spacer(Modifier.height(80.dp)) + } + + Box( + modifier = Modifier + .align(alignment = Alignment.BottomCenter) + .fillMaxWidth() + .navigationBarsPadding() + .padding(horizontal = screenWidthDp(16.dp), vertical = screenHeightDp(12.dp)) + .zIndex(1f) + ) { + + TextButton( + text = "검색 결과 보기", + modifier = Modifier + .width(screenWidthDp(326.dp)) + .height(screenHeightDp(47.dp)), + enabled = buttonEnabled, + onClick = onApply + ) + } + } +} + +@Composable +fun RecruitingStatusMultiSection( + recruitingStatus: RecruitingStatus?, + onSelect: (RecruitingStatus) -> Unit +) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(screenWidthDp(14.dp)) + ) { + RecruitingStatus.entries.forEach { type -> + TextButton( + modifier = Modifier.weight(1f), + text = type.name, + shape = SpotShapes.Soft, + checked = (recruitingStatus == type), + onClick = { onSelect(type) } + ) + } + } +} + +@Composable +fun ActivityFeeSection( + activityFee: FeeRange?, + onSelect: (FeeRange) -> Unit +) { + FlowRow( + horizontalArrangement = Arrangement.spacedBy(screenWidthDp(14.dp)), + verticalArrangement = Arrangement.spacedBy(screenHeightDp(14.dp)) + ){ + FeeRange.entries.forEach { fee -> + TextButton( + text = fee.label, + modifier = Modifier + .width(screenWidthDp(71.dp)) + .height(screenHeightDp(35.dp)), + state = TextButtonState.Toggle, + checked = activityFee == fee, + onClick = { onSelect(fee) }, + shape = SpotShapes.Hard, + style = SpotTheme.typography.medium_500 + ) + } + } +} +@Composable +fun ResetFilterText( + onClick: () -> Unit, + modifier: Modifier = Modifier +) { + Text( + text = "필터 초기화", + color = SpotTheme.colors.gray500, + style = SpotTheme.typography.regular_500, + textDecoration = TextDecoration.Underline, + modifier = modifier + .semantics { role = Role.Button } + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null, + onClick = onClick + ) + ) +} + diff --git a/feature/study/src/main/java/com/umcspot/spot/study/preferLocation/PreferLocationStudyScreen.kt b/feature/study/src/main/java/com/umcspot/spot/study/preferLocation/PreferLocationStudyScreen.kt index d792dde1..8928d947 100644 --- a/feature/study/src/main/java/com/umcspot/spot/study/preferLocation/PreferLocationStudyScreen.kt +++ b/feature/study/src/main/java/com/umcspot/spot/study/preferLocation/PreferLocationStudyScreen.kt @@ -1,316 +1,379 @@ -//package com.umcspot.spot.study.preferLocation -// -//import androidx.compose.foundation.background -//import androidx.compose.foundation.layout.* -//import androidx.compose.foundation.lazy.LazyColumn -//import androidx.compose.foundation.lazy.LazyListState -//import androidx.compose.foundation.lazy.items -//import androidx.compose.foundation.lazy.rememberLazyListState -//import androidx.compose.material3.Icon -//import androidx.compose.material3.IconButton -//import androidx.compose.material3.OutlinedButton -//import androidx.compose.material3.ScrollableTabRow -//import androidx.compose.material3.Tab -//import androidx.compose.material3.TabRowDefaults -//import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset -//import androidx.compose.material3.Text -//import androidx.compose.runtime.* -//import androidx.compose.ui.Alignment -//import androidx.compose.ui.Modifier -//import androidx.compose.ui.draw.drawWithContent -//import androidx.compose.ui.geometry.Offset -//import androidx.compose.ui.geometry.Size -//import androidx.compose.ui.graphics.Brush -//import androidx.compose.ui.graphics.Color -//import androidx.compose.ui.res.painterResource -//import androidx.compose.ui.unit.dp -//import androidx.compose.ui.unit.sp -//import androidx.hilt.navigation.compose.hiltViewModel -//import androidx.lifecycle.compose.collectAsStateWithLifecycle -//import com.umcspot.spot.designsystem.R -//import com.umcspot.spot.designsystem.component.bottomsheet.LocationBottomSheet -//import com.umcspot.spot.designsystem.component.empty.EmptyAlertWithButton -//import com.umcspot.spot.designsystem.component.study.StudyListItem -//import com.umcspot.spot.designsystem.shapes.SpotShapes -//import com.umcspot.spot.designsystem.theme.B500 -//import com.umcspot.spot.designsystem.theme.SpotTheme -//import com.umcspot.spot.model.RecruitingStudySort -//import com.umcspot.spot.study.model.StudyResult -//import com.umcspot.spot.ui.state.UiState -//import kotlinx.coroutines.launch -// -//@Composable -//fun PreferLocationStudyScreen( -// contentPadding: PaddingValues, -// viewmodel: PreferLocationStudyViewModel = hiltViewModel(), -// onRegisterScrollToTop: ((() -> Unit)?) -> Unit, -// onFilterClick: () -> Unit, -// onItemClick: (StudyResult) -> Unit -//) { -// val ui by viewmodel.uiState.collectAsStateWithLifecycle() -// val sort by viewmodel.sortType.collectAsStateWithLifecycle() -// val query by viewmodel.query.collectAsStateWithLifecycle() -// val results by viewmodel.results.collectAsStateWithLifecycle() -// val selected by viewmodel.selected.collectAsStateWithLifecycle() -// -// var showSheet by remember { mutableStateOf(false) } -// var selectedTab by remember { mutableStateOf(0) } // 0 = 전체 -// val tabs: List = remember(selected) { listOf("전체") + selected } -// -// val listState = rememberLazyListState() -// val scope = rememberCoroutineScope() -// -// val topPad = contentPadding.calculateTopPadding() -// val bottomPad = contentPadding.calculateBottomPadding() -// -// // 최초 데이터 로드 -// LaunchedEffect(ui.studies) { -// if (ui.studies is UiState.Empty) viewmodel.load(RecruitingStudySort.RECENT) -// } -// LaunchedEffect(Unit) { viewmodel.loadLocationData() } -// -// // 상단으로 스크롤 요청 핸들링 -// LaunchedEffect(Unit) { -// onRegisterScrollToTop { -// scope.launch { listState.animateScrollToItem(0) } -// } -// } -// -// // 선택 칩 변화 시 탭 인덱스 보정 -// LaunchedEffect(selected.size) { -// val maxIdx = (1 + selected.size) - 1 // "전체" 1개 + selected 크기 - 1 -// if (selectedTab > maxIdx) selectedTab = maxIdx -// } -// -// // 리스트 데이터 준비 (+ 선택된 탭 기준 필터 적용 지점) -// val studiesAll = when (val s = ui.studies) { -// is UiState.Success -> s.data.studyList -// else -> emptyList() -// } -// val currentLocation = selected.getOrNull(selectedTab) -// // 🔧 여기서 실제 StudyResult의 지역 필드명으로 필터하세요 (예: item.location / item.address 등) -// val studiesForUi = if (currentLocation.isNullOrBlank()) { -// studiesAll -// } else { -// studiesAll.filter { item -> -// // 예시) item.location?.contains(currentLocation) == true -// // 필드명이 다르면 위 라인만 고치면 됨 -// false -// } -// } -// -// Column( -// modifier = Modifier -// .fillMaxSize() -// .background(SpotTheme.colors.white) -// .padding(top = topPad, bottom = bottomPad, start = 16.dp, end = 16.dp) -// ) { -// // 타이틀 -// Text( -// text = "내 지역 스터디", -// style = SpotTheme.typography.small_400.copy(fontSize = 20.sp) -// ) -// -// Spacer(Modifier.height(8.dp)) -// -// // 선택 지역 탭 -// SelectedLocationTabs( -// tabs = tabs, -// selectedIndex = selectedTab, -// onTabSelected = { selectedTab = it } -// ) -// -// HeaderRow( -// size = studiesForUi.size, -// sortType = sort, -// onOpenSortSheet = { showSheet = true }, -// onFilterClick = onFilterClick -// ) -// -// if (ui.studies is UiState.Loading) { -// Text("로딩 중...", color = Color.Gray, modifier = Modifier.padding(top = 8.dp)) -// } else if (ui.studies is UiState.Failure) { -// Text("에러: ${(ui.studies as UiState.Failure).msg}", color = Color.Red, modifier = Modifier.padding(top = 8.dp)) -// } else if (studiesForUi.isEmpty()) { -// Box(Modifier.fillMaxSize()) { -// EmptyAlertWithButton( -// modifier = Modifier.fillMaxSize(), -// painter = painterResource(R.drawable.location_outline), -// alertTitle = "내 지역이 아직 없어요!", -// alertDes = "내 지역을 설정하고 스터디를 모아봐요.", -// buttonText = "내 지역 설정하기", -// onClick = { showSheet = true } -// ) -// } -// } else { -// StudyList( -// listState = listState, -// items = studiesForUi, -// onItemClick = onItemClick -// ) -// } -// } -// -// // 지역 선택 바텀시트 -// LocationBottomSheet( -// visible = showSheet, -// query = query, -// onQueryChange = { viewmodel.searchLocation(it) }, -// onDismiss = { showSheet = false }, -// results = results, -// selected = selected, -// onAddSelected = viewmodel::add, -// onRemoveSelected = viewmodel::remove -// ) -//} -// -//@Composable -//private fun StudyList( -// listState: LazyListState, -// items: List, -// onItemClick: (StudyResult) -> Unit -//) { -// LazyColumn( -// state = listState, -// modifier = Modifier.fillMaxSize() -// ) { -// items( -// items = items, -// key = { it.id } -// ) { item -> -// StudyListItem( -// item = item, -// modifier = Modifier -// .fillMaxWidth() -// .padding(vertical = 5.dp), -// onClick = { onItemClick(item) } -// ) -// } -// } -//} -// -//@Composable -//private fun HeaderRow( -// size: Int, -// sortType: RecruitingStudySort, -// onOpenSortSheet: () -> Unit, -// onFilterClick: () -> Unit -//) { -// Row( -// modifier = Modifier.fillMaxWidth(), -// horizontalArrangement = Arrangement.SpaceBetween, -// verticalAlignment = Alignment.CenterVertically -// ) { -// Text( -// text = "%02d건".format(size), -// style = SpotTheme.typography.medium_500.copy(fontSize = 12.sp), -// color = SpotTheme.colors.gray500 -// ) -// -// Row(verticalAlignment = Alignment.CenterVertically) { -// OutlinedButton( -// onClick = onOpenSortSheet, -// shape = SpotShapes.Soft, -// contentPadding = PaddingValues(horizontal = 10.dp, vertical = 6.dp), -// modifier = Modifier.height(32.dp) -// ) { -// Text( -// text = sortType.label, -// color = SpotTheme.colors.black, -// style = SpotTheme.typography.medium_500.copy(fontSize = 12.sp) -// ) -// Spacer(Modifier.width(5.dp)) -// Icon( -// painter = painterResource(R.drawable.arrow_down), -// tint = SpotTheme.colors.B500, -// contentDescription = null -// ) -// } -// -// IconButton(onClick = onFilterClick) { -// Icon( -// painter = painterResource(R.drawable.filter), -// contentDescription = "필터", -// modifier = Modifier.size(22.dp) -// ) -// } -// } -// } -//} -// -//@Composable -//private fun SelectedLocationTabs( -// tabs: List, -// selectedIndex: Int, -// onTabSelected: (Int) -> Unit -//) { -// if (tabs.isEmpty()) return -// -// val horizPad = 12.dp -// val scrimWidth = 24.dp -// val bg = SpotTheme.colors.white // 배경색(화면 배경과 맞추기) -// -// Box( -// modifier = Modifier -// .fillMaxWidth() -// // 스크롤 가능 영역의 좌우에 페이드 오버레이를 그리되, 입력은 통과시킴 -// .drawWithContent { -// drawContent() -// -// val w = scrimWidth.toPx() -// drawRect( -// brush = Brush.horizontalGradient( -// colors = listOf(Color.Transparent, bg), -// // 내부가 투명, 바깥이 흰색이 되도록 방향 지정 -// startX = w, // 투명 쪽(내부) -// endX = 0f // 흰색 쪽(바깥) -// ), -// size = Size(w, size.height), -// topLeft = Offset(0f, 0f) -// ) -// // 오른쪽: 내부(투명) → 바깥(흰색) -// drawRect( -// brush = Brush.horizontalGradient( -// colors = listOf(Color.Transparent, bg), -// startX = size.width - w, // 투명(내부) -// endX = size.width // 흰색(바깥) -// ), -// size = Size(w, size.height), -// topLeft = Offset(size.width - w, 0f) -// ) -// } -// ) { -// ScrollableTabRow( -// selectedTabIndex = selectedIndex, -// edgePadding = 0.dp, -// containerColor = Color.Transparent, -// divider = {}, -// indicator = { tabPositions -> -// TabRowDefaults.Indicator( -// modifier = Modifier -// .tabIndicatorOffset(tabPositions[selectedIndex]) -// .padding(horizontal = horizPad) -// .height(2.dp), -// color = SpotTheme.colors.B500 -// ) -// } -// ) { -// tabs.forEachIndexed { index, name -> -// Tab( -// selected = selectedIndex == index, -// onClick = { onTabSelected(index) }, -// selectedContentColor = SpotTheme.colors.black, -// unselectedContentColor = SpotTheme.colors.black -// ) { -// Text( -// text = name, -// style = SpotTheme.typography.medium_500.copy(fontSize = 14.sp), -// modifier = Modifier.padding(horizontal = horizPad, vertical = 10.dp) -// ) -// } -// } -// } -// } -//} -// -// +package com.umcspot.spot.study.preferLocation + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.ScrollableTabRow +import androidx.compose.material3.Tab +import androidx.compose.material3.TabRowDefaults +import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshotFlow +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawWithContent +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.umcspot.spot.designsystem.R +import com.umcspot.spot.designsystem.component.bottomsheet.LocationBottomSheet +import com.umcspot.spot.designsystem.component.empty.EmptyAlertWithButton +import com.umcspot.spot.designsystem.component.study.StudyListItem +import com.umcspot.spot.designsystem.shapes.SpotShapes +import com.umcspot.spot.designsystem.theme.B500 +import com.umcspot.spot.designsystem.theme.G200 +import com.umcspot.spot.designsystem.theme.SpotTheme +import com.umcspot.spot.model.RecruitingStudySort +import com.umcspot.spot.study.model.StudyResult +import com.umcspot.spot.ui.extension.screenHeightDp +import com.umcspot.spot.ui.extension.screenWidthDp +import com.umcspot.spot.ui.state.UiState +import kotlinx.coroutines.launch + +@Composable +fun PreferLocationStudyScreen( + contentPadding: PaddingValues, + viewmodel: PreferLocationStudyViewModel = hiltViewModel(), + onRegisterScrollToTop: ((() -> Unit)?) -> Unit, + onFilterClick: () -> Unit, + onItemClick: (StudyResult) -> Unit +) { + val ui by viewmodel.uiState.collectAsStateWithLifecycle() + val sort by viewmodel.sortType.collectAsStateWithLifecycle() + val query by viewmodel.query.collectAsStateWithLifecycle() + val results by viewmodel.results.collectAsStateWithLifecycle() + val selected by viewmodel.selected.collectAsStateWithLifecycle() + val isLoadingMore by viewmodel.isLoadingMore.collectAsStateWithLifecycle() + + var showSheet by remember { mutableStateOf(false) } + var selectedTab by remember { mutableStateOf(0) } // 0 = 전체 + val tabs: List = remember(selected) { listOf("전체") + selected.map { it.fullName } } + + val listState = rememberLazyListState() + val scope = rememberCoroutineScope() + + val topPad = contentPadding.calculateTopPadding() + val bottomPad = contentPadding.calculateBottomPadding() + + // 최초 데이터 로드 + LaunchedEffect(Unit) { + viewmodel.load() + viewmodel.loadLocationData() + } + + // 상단으로 스크롤 요청 핸들링 + LaunchedEffect(Unit) { + onRegisterScrollToTop { + scope.launch { listState.animateScrollToItem(0) } + } + } + + // 선택 칩 변화 시 탭 인덱스 보정 + LaunchedEffect(selected.size) { + val maxIdx = (1 + selected.size) - 1 // "전체" 1개 + selected 크기 - 1 + if (selectedTab > maxIdx) selectedTab = maxIdx + } + + LaunchedEffect(selected.size) { + val maxIdx = (1 + selected.size) - 1 + if (selectedTab > maxIdx) { + selectedTab = maxIdx + viewmodel.selectTab(selectedTab) // ✅ 탭 인덱스 바뀌었으니 다시 호출 + } + } + + val studiesForUi = when (val s = ui.data) { + is UiState.Success -> s.data.studyList + else -> emptyList() + } + + LaunchedEffect(listState, studiesForUi.size, isLoadingMore) { + snapshotFlow { listState.layoutInfo.visibleItemsInfo.lastOrNull()?.index } + .collect { lastVisible -> + val total = listState.layoutInfo.totalItemsCount + if (lastVisible != null && total > 0) { + // 마지막에서 3개 전쯤 도달하면 로드 + if (lastVisible >= total - 3) { + viewmodel.loadNextPage() + } + } + } + } + + Column( + modifier = Modifier + .fillMaxSize() + .background(SpotTheme.colors.white) + .padding(top = topPad, bottom = bottomPad, start = screenWidthDp(17.dp), end = screenWidthDp(17.dp)) + ) { + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = screenHeightDp(9.dp)), + verticalAlignment = Alignment.CenterVertically + ) { + // 타이틀 + Text( + text = "내 지역 스터디", + style = SpotTheme.typography.h4 + ) + } + + // 선택 지역 탭 + SelectedLocationTabs( + tabs = tabs, + selectedIndex = selectedTab, + onTabSelected = { + selectedTab = it + viewmodel.selectTab(it) + scope.launch { listState.animateScrollToItem(0) } + } + ) + + if (ui.data is UiState.Loading) { + Text("로딩 중...", color = Color.Gray, modifier = Modifier.padding(top = screenHeightDp(8.dp))) + } else if (ui.data is UiState.Failure) { + Text("에러: ${(ui.data as UiState.Failure).msg}", color = Color.Red, modifier = Modifier.padding(top = screenHeightDp(8.dp))) + } else if (studiesForUi.isEmpty()) { + Box(Modifier.fillMaxSize()) { + EmptyAlertWithButton( + modifier = Modifier.fillMaxSize(), + painter = painterResource(R.drawable.location_outline), + alertTitle = "내 지역이 아직 없어요!", + alertDes = "내 지역을 설정하고 스터디를 모아봐요.", + buttonText = "내 지역 설정하기", + onClick = { showSheet = true } + ) + } + } else { + + Spacer(modifier = Modifier.height(screenHeightDp(12.dp))) + + HeaderRow( + size = studiesForUi.size, + sortType = sort, + onOpenSortSheet = { showSheet = true }, + onFilterClick = onFilterClick + ) + + StudyList( + listState = listState, + items = studiesForUi, + onItemClick = onItemClick + ) + } + } + + // 지역 선택 바텀시트 + LocationBottomSheet( + visible = showSheet, + query = query, + onQueryChange = { viewmodel.searchLocation(it) }, + onDismiss = { + viewmodel.syncPreferredRegions() + showSheet = false + }, + results = results, + selected = selected, + onAddSelected = { viewmodel.addLocation(it) }, + onRemoveSelected = { viewmodel.removeLocation(it) } + ) +} + +@Composable +private fun StudyList( + listState: LazyListState, + items: List, + onItemClick: (StudyResult) -> Unit +) { + LazyColumn( + state = listState, + modifier = Modifier.fillMaxSize() + ) { + items( + items = items, + key = { it.id } + ) { item -> + StudyListItem( + item = item, + modifier = Modifier + .fillMaxWidth() + .padding(vertical = screenWidthDp(5.dp)), + onClick = { onItemClick(item) } + ) + } + } +} + +@Composable +fun HeaderRow( + size: Int, + sortType: RecruitingStudySort, + onOpenSortSheet: () -> Unit, + onFilterClick: () -> Unit +) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "%02d건".format(size), + style = SpotTheme.typography.regular_500, + color = SpotTheme.colors.gray400 + ) + + Row{ + OutlinedButton( + onClick = onOpenSortSheet, + shape = SpotShapes.Hard, + border = BorderStroke(1.dp, SpotTheme.colors.G200), + contentPadding = PaddingValues(start = screenWidthDp(7.dp), end = screenWidthDp(4.dp), top = screenHeightDp(2.dp), bottom = screenHeightDp(4.dp)), + modifier = Modifier + .wrapContentWidth() + .height(screenHeightDp(26.dp)) + ) { + Text( + text = sortType.label, + color = SpotTheme.colors.black, + style = SpotTheme.typography.regular_500 + ) + Spacer(Modifier.width(screenWidthDp(7.dp))) + Icon( + modifier = Modifier.size(screenWidthDp(14.dp)), + painter = painterResource(R.drawable.arrow_down), + tint = SpotTheme.colors.B500, + contentDescription = null + ) + } + + Spacer(Modifier.width(screenWidthDp(10.dp))) + + IconButton( + onClick = onFilterClick, + modifier = Modifier.size(screenWidthDp(26.dp)) + ) { + Icon( + painter = painterResource(R.drawable.filter), + contentDescription = "필터", + modifier = Modifier.size(screenWidthDp(14.dp)) + ) + } + } + } +} + +@Composable +private fun SelectedLocationTabs( + tabs: List, + selectedIndex: Int, + onTabSelected: (Int) -> Unit +) { + if (tabs.isEmpty()) return + + val scrimWidth = screenWidthDp(24.dp) + val bg = SpotTheme.colors.white + + Box( + modifier = Modifier + .fillMaxWidth() + .drawWithContent { + drawContent() + + val w = scrimWidth.toPx() + // 왼쪽: 내부(흰색) → 바깥(투명) + drawRect( + brush = Brush.horizontalGradient( + colors = listOf(Color.Transparent, bg), + startX = w, + endX = 0f + ), + size = Size(w, size.height), + topLeft = Offset(0f, 0f) + ) + // 오른쪽: 내부(투명) → 바깥(흰색) + drawRect( + brush = Brush.horizontalGradient( + colors = listOf(Color.Transparent, bg), + startX = size.width - w, + endX = size.width + ), + size = Size(w, size.height), + topLeft = Offset(size.width - w, 0f) + ) + } + ) { + ScrollableTabRow( + modifier = Modifier.fillMaxWidth(), + selectedTabIndex = selectedIndex, + edgePadding = 0.dp, + containerColor = Color.Transparent, + divider = { + HorizontalDivider( + thickness = 1.dp, + color = SpotTheme.colors.gray200 + ) + }, + indicator = { tabPositions -> + TabRowDefaults.SecondaryIndicator( + modifier = Modifier + .tabIndicatorOffset(tabPositions[selectedIndex]) + .padding(horizontal = screenWidthDp(17.dp)) + .height(1.dp), + color = SpotTheme.colors.B500 + ) + } + ) { + tabs.forEachIndexed { index, name -> + Tab( + modifier = Modifier + .wrapContentWidth(), + selected = selectedIndex == index, + onClick = { onTabSelected(index) }, + selectedContentColor = SpotTheme.colors.black, + unselectedContentColor = SpotTheme.colors.black + ) { + Text( + text = name, + style = SpotTheme.typography.h5, + modifier = Modifier + .padding(horizontal = screenWidthDp(7.dp), vertical = screenHeightDp(4.dp)) + ) + } + } + } + } +} \ No newline at end of file diff --git a/feature/study/src/main/java/com/umcspot/spot/study/preferLocation/PreferLocationStudyViewModel.kt b/feature/study/src/main/java/com/umcspot/spot/study/preferLocation/PreferLocationStudyViewModel.kt index 9f2b26bf..609c5dc8 100644 --- a/feature/study/src/main/java/com/umcspot/spot/study/preferLocation/PreferLocationStudyViewModel.kt +++ b/feature/study/src/main/java/com/umcspot/spot/study/preferLocation/PreferLocationStudyViewModel.kt @@ -1,154 +1,244 @@ -//package com.umcspot.spot.study.preferLocation -// -//import android.content.Context -//import android.util.Log -//import androidx.lifecycle.ViewModel -//import androidx.lifecycle.viewModelScope -//import com.umcspot.spot.common.location.LocationRow -//import com.umcspot.spot.common.location.LocationStore -//import com.umcspot.spot.common.location.searchLocations -//import com.umcspot.spot.model.ActivityType -//import com.umcspot.spot.model.FeeRange -//import com.umcspot.spot.model.RecruitingStudySort -//import com.umcspot.spot.model.StudyTheme -//import com.umcspot.spot.study.model.StudyResultList -//import com.umcspot.spot.study.repository.StudyRepository -//import com.umcspot.spot.ui.state.UiState -//import dagger.hilt.android.lifecycle.HiltViewModel -//import dagger.hilt.android.qualifiers.ApplicationContext -//import kotlinx.coroutines.CoroutineScope -//import kotlinx.coroutines.Dispatchers -//import kotlinx.coroutines.flow.Flow -//import kotlinx.coroutines.flow.MutableStateFlow -//import kotlinx.coroutines.flow.StateFlow -//import kotlinx.coroutines.flow.asStateFlow -//import kotlinx.coroutines.flow.collectLatest -//import kotlinx.coroutines.flow.combine -//import kotlinx.coroutines.flow.debounce -//import kotlinx.coroutines.flow.distinctUntilChanged -//import kotlinx.coroutines.flow.onStart -//import kotlinx.coroutines.flow.update -//import kotlinx.coroutines.launch -//import javax.inject.Inject -// -//@HiltViewModel -//class PreferLocationStudyViewModel @Inject constructor( -// private val studyRepository: StudyRepository, -// @ApplicationContext private val appContext: Context -//) : ViewModel() { -// -// data class PreferLocationStudyUiState( -// val studies: UiState = UiState.Empty -// ) -// -// private val _uiState = MutableStateFlow(PreferLocationStudyUiState()) -// val uiState: StateFlow = _uiState.asStateFlow() -// -// private val _sortType = MutableStateFlow(RecruitingStudySort.RECENT) -// val sortType: StateFlow = _sortType.asStateFlow() -// -// private val _activity = MutableStateFlow(null) -// private val _fee = MutableStateFlow(null) -// private val _theme = MutableStateFlow(null) -// -// /** ---------------- 행정구역 관련 ---------------- */ -// private var allLocations: List = emptyList() -// -// private val _query = MutableStateFlow("") -// val query = _query.asStateFlow() -// -// private val _results = MutableStateFlow>(emptyList()) -// val results = _results.asStateFlow() -// -// private val _selected = MutableStateFlow>(emptyList()) -// val selected = _selected.asStateFlow() -// -// -// /** ---------------- 초기 네트워크 fetch ---------------- */ -// init { -// combine(_sortType, _activity, _fee, _theme) { s, a, f, t -> -// Params(s, a, f, t) -// } -// .distinctUntilChanged() -// .debounce(200) -// .onStart { emit(Params(_sortType.value, _activity.value, _fee.value, _theme.value)) } -// .collectLatestIn(viewModelScope) { params -> -// fetch(params) -// } -// } -// -// private suspend fun fetch(p: Params) { -// _uiState.update { it.copy(studies = UiState.Loading) } -// val newState: UiState = try { -// val res = studyRepository.getRecruitingStudies( -// sortType = p.sort, -// activityType = p.activity, -// feeRange = p.fee, -// theme = p.theme -// ) -// res.fold( -// onSuccess = { data -> UiState.Success(data) }, -// onFailure = { e -> UiState.Failure(e.message ?: e.toString()) } -// ) -// } catch (e: Exception) { -// UiState.Failure(e.message ?: e.toString()) -// } -// _uiState.update { it.copy(studies = newState) } -// } -// -// /** ---------------- 공개 API ---------------- */ -// fun load(selected: RecruitingStudySort = _sortType.value) { _sortType.value = selected } -// fun selectSort(type: RecruitingStudySort) { _sortType.value = type } -// fun setActivityFilter(type: ActivityType?) { _activity.value = type } -// fun setFeeFilter(fee: FeeRange?) { _fee.value = fee } -// fun setThemeFilter(theme: StudyTheme?) { _theme.value = theme } -// fun clearFilters() { -// _activity.value = null -// _fee.value = null -// _theme.value = null -// } -// -// fun add(name: String) = _selected.update { if (name in it || it.size>=10) it else it + name } -// fun remove(name: String) = _selected.update { it - name } -// fun clear() = _selected.update { emptyList() } -// -// /** ---------------- 행정구역 검색용 메서드 ---------------- */ -// fun loadLocationData() { -// viewModelScope.launch(Dispatchers.IO) { -// allLocations = LocationStore.load(appContext)} -// } -// -// fun searchLocation(query: String) { -// _query.value = query -// viewModelScope.launch(Dispatchers.IO) { -// if (query.isBlank()) { -// _results.value = emptyList() -// return@launch -// } -// -// if (allLocations.isEmpty()) { -// allLocations = LocationStore.load(appContext) -// } -// -// val filtered = searchLocations(query, allLocations) -// -// _results.value = filtered -// } -// } -// -// -// private data class Params( -// val sort: RecruitingStudySort, -// val activity: ActivityType?, -// val fee: FeeRange?, -// val theme: StudyTheme? -// ) -//} -// -///** 작은 헬퍼: Flow collectLatest 축약 */ -//private inline fun Flow.collectLatestIn( -// scope: CoroutineScope, -// crossinline block: suspend (T) -> Unit -//) = scope.launch { -// collectLatest { block(it) } -//} +package com.umcspot.spot.study.preferLocation + +import android.content.Context +import android.util.Log +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.umcspot.spot.common.location.LocationRow +import com.umcspot.spot.common.location.LocationStore +import com.umcspot.spot.common.location.searchLocations +import com.umcspot.spot.model.FeeRange +import com.umcspot.spot.model.RecruitingStatus +import com.umcspot.spot.model.RecruitingStudySort +import com.umcspot.spot.model.StudyTheme +import com.umcspot.spot.study.model.StudyResultList +import com.umcspot.spot.study.repository.StudyRepository +import com.umcspot.spot.ui.state.UiState +import com.umcspot.spot.user.repository.UserRepository +import dagger.hilt.android.lifecycle.HiltViewModel +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class PreferLocationStudyViewModel @Inject constructor( + private val studyRepository: StudyRepository, + private val userRepository: UserRepository, + @ApplicationContext private val appContext: Context, +) : ViewModel() { + + data class ScrollPosition( + val index: Int = 0, + val offset: Int = 0 + ) + + data class PreferLocationStudyUiState( + val data: UiState = UiState.Empty + ) + + var scrollPosition: ScrollPosition = ScrollPosition() + + private val _uiState = MutableStateFlow(PreferLocationStudyUiState()) + val uiState: StateFlow = _uiState.asStateFlow() + + /** ---------------- 행정구역 관련 ---------------- */ + private var allLocations: List = emptyList() + + private val _query = MutableStateFlow("") + val query = _query.asStateFlow() + + private val _results = MutableStateFlow>(emptyList()) + val results = _results.asStateFlow() + + private val _selectedRegion = MutableStateFlow>(emptyList()) + val selected = _selectedRegion.asStateFlow() + + /** 탭/페이징 상태 */ + private var currentRegionCode: String? = null + private val _isLoadingMore = MutableStateFlow(false) + val isLoadingMore: StateFlow = _isLoadingMore.asStateFlow() + + private val _sortType = MutableStateFlow(RecruitingStudySort.RECENT) + val sortType: StateFlow = _sortType.asStateFlow() + + /** Filter **/ + private val _recruitingStatus = MutableStateFlow(null) + val recruitingStatus: StateFlow = _recruitingStatus.asStateFlow() + + private val _fee = MutableStateFlow(null) + val fee: StateFlow = _fee.asStateFlow() + + private val _themes = MutableStateFlow>(emptyList()) + val themes: StateFlow> = _themes.asStateFlow() + + /** ========== BoardListViewModel의 load() 역할 ========== */ + fun load(regionCode: String? = null) { + currentRegionCode = regionCode + + viewModelScope.launch { + runCatching { + if (allLocations.isEmpty()) { + allLocations = LocationStore.load(appContext) + } + + // ✅ 현재 선호 지역이 없으면 호출하지 않음 + val preferredCodes = _selectedRegion.value.map { it.code } + if (preferredCodes.isEmpty()) { + _uiState.update { it.copy(data = UiState.Empty) } + return@launch + } + + val regionCodesForRequest = + if (regionCode.isNullOrBlank()) preferredCodes else listOf(regionCode) + + _uiState.update { it.copy(data = UiState.Loading) } + + studyRepository.getPreferLocationStudies( + recruitingStatus = _recruitingStatus.value, + feeRange = _fee.value, + categories = _themes.value.map { it.name }, + sortBy = _sortType.value, + cursor = null, + size = 20, + regionCodes = regionCodesForRequest + ).getOrThrow() + }.onSuccess { firstPage -> + _uiState.update { + it.copy( + data = if (firstPage.studyList.isEmpty()) + UiState.Empty + else + UiState.Success(firstPage) + ) + } + }.onFailure { e -> + Log.e("PreferLocationStudyViewModel", "load error", e) + _uiState.update { it.copy(data = UiState.Empty) } + } + } + } + + + fun loadNextPage() { + val currentUi = _uiState.value.data + val success = currentUi as? UiState.Success ?: return + val currentList = success.data + + // StudyResultList에 hasNext/nextCursor 있다고 가정(없으면 네 모델에 맞게 바꿔야 함) + if (!currentList.hasNext) return + if (_isLoadingMore.value) return + + viewModelScope.launch { + _isLoadingMore.value = true + + runCatching { + // 선호 지역 전체 목록(전체 탭일 때 필요) + val preferredCodes = userRepository.getUserPreferredRegion() + .getOrThrow() + .regionCodes + .map { it.trim() } + + val regionCodesForRequest = + if (currentRegionCode.isNullOrBlank()) preferredCodes else listOf(currentRegionCode!!) + + studyRepository.getPreferLocationStudies( + recruitingStatus = _recruitingStatus.value, + feeRange = _fee.value, + categories = _themes.value.map { it.name }, + sortBy = _sortType.value, + cursor = currentList.nextCursor, + size = 20, + regionCodes = regionCodesForRequest + ).getOrThrow() + }.onSuccess { newPage -> + val merged = currentList.copy( + studyList = currentList.studyList + newPage.studyList, + hasNext = newPage.hasNext, + nextCursor = newPage.nextCursor + ) + _uiState.update { it.copy(data = UiState.Success(merged)) } + }.onFailure { e -> + Log.e("PreferLocationStudyViewModel", "loadNextPage error", e) + } + + _isLoadingMore.value = false + } + } + + fun selectTab(selectedTabIndex: Int) { + // 0 = 전체, 1..n = selectedRegion[index-1] + val code = if (selectedTabIndex == 0) null else _selectedRegion.value.getOrNull(selectedTabIndex - 1)?.code + load(code) + } + + /** Filter 적용 후 현재 탭 기준으로 다시 load */ + fun applyFilter( + recruitingStatus: RecruitingStatus?, + fee: FeeRange?, + themes: List, + ) { + _recruitingStatus.value = recruitingStatus + _fee.value = fee + _themes.value = themes + + load(currentRegionCode) + } + + fun setSort(sort: RecruitingStudySort) { + _sortType.value = sort + load(currentRegionCode) + } + + /** ---------------- 행정구역 검색용 메서드 ---------------- */ + fun loadLocationData() { + viewModelScope.launch(Dispatchers.IO) { + allLocations = LocationStore.load(appContext) + } + } + + fun searchLocation(query: String) { + _query.value = query + viewModelScope.launch(Dispatchers.IO) { + if (query.isBlank()) { + _results.value = emptyList() + return@launch + } + + if (allLocations.isEmpty()) { + allLocations = LocationStore.load(appContext) + } + + _results.value = searchLocations(query, allLocations) + } + } + + fun addLocation(row: LocationRow) { + _selectedRegion.update { list -> + if (list.any { it.code == row.code }) list else list + row + } + } + + fun removeLocation(row: LocationRow) { + _selectedRegion.update { list -> + list.filterNot { it.code == row.code } + } + } + + fun syncPreferredRegions() { + viewModelScope.launch { + runCatching { + val regionCodes = _selectedRegion.value.map { it.code } + userRepository.setUserPreferredRegion(regionCodes).getOrThrow() + }.onSuccess { + load(currentRegionCode) + }.onFailure { e -> + Log.e("PreferLocationStudyViewModel", "setUserPreferredRegion failed", e) + } + } + } +} diff --git a/feature/study/src/main/java/com/umcspot/spot/study/preferLocation/navigation/PreferLocationStudyFilterNavigation.kt b/feature/study/src/main/java/com/umcspot/spot/study/preferLocation/navigation/PreferLocationStudyFilterNavigation.kt index defc0601..7cde2f55 100644 --- a/feature/study/src/main/java/com/umcspot/spot/study/preferLocation/navigation/PreferLocationStudyFilterNavigation.kt +++ b/feature/study/src/main/java/com/umcspot/spot/study/preferLocation/navigation/PreferLocationStudyFilterNavigation.kt @@ -1,28 +1,40 @@ -//package com.umcspot.spot.study.preferLocation.navigation -// -//import androidx.compose.foundation.layout.PaddingValues -//import androidx.navigation.NavController -//import androidx.navigation.NavGraphBuilder -//import androidx.navigation.NavOptions -//import androidx.navigation.compose.composable -//import com.umcspot.spot.navigation.Route -//import com.umcspot.spot.study.preferLocation.PreferLocationStudyFilterScreen -//import kotlinx.serialization.Serializable -// -//fun NavController.navigateToPreferLocationStudyFilter(navOptions: NavOptions? = null) { -// navigate(PreferLocationFilter, navOptions) -//} -// -//fun NavGraphBuilder.preferLocationStudyFilterGraph( -// contentPadding: PaddingValues, -// onAcceptFilterClick: () -> Unit, -//) { -// composable { -// PreferLocationStudyFilterScreen( -// contentPadding = contentPadding, -// onAcceptFilterClick = onAcceptFilterClick, -// ) -// } -//} -//@Serializable -//data object PreferLocationFilter : Route +package com.umcspot.spot.study.preferLocation.navigation + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.runtime.remember +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavOptions +import androidx.navigation.compose.composable +import com.umcspot.spot.navigation.Route +import com.umcspot.spot.study.preferLocation.PreferLocationStudyFilterScreen +import com.umcspot.spot.study.preferLocation.PreferLocationStudyViewModel +import kotlinx.serialization.Serializable + +fun NavController.navigateToPreferLocationStudyFilter(navOptions: NavOptions? = null) { + navigate(PreferLocationFilter, navOptions) +} + +fun NavGraphBuilder.preferLocationStudyFilterGraph( + navController: NavController, + contentPadding: PaddingValues, + onAcceptFilterClick: () -> Unit, +) { + composable { + val parentEntry = remember(navController) { + + navController.getBackStackEntry(PreferLocation) + } + + val preferLocationVm: PreferLocationStudyViewModel = hiltViewModel(parentEntry) + + PreferLocationStudyFilterScreen( + contentPadding = contentPadding, + onAcceptFilterClick = onAcceptFilterClick, + preferLocationVm = preferLocationVm + ) + } +} +@Serializable +data object PreferLocationFilter : Route diff --git a/feature/study/src/main/java/com/umcspot/spot/study/preferLocation/navigation/PreferLocationStudyNavigation.kt b/feature/study/src/main/java/com/umcspot/spot/study/preferLocation/navigation/PreferLocationStudyNavigation.kt index 4e6d64f4..1b3522c5 100644 --- a/feature/study/src/main/java/com/umcspot/spot/study/preferLocation/navigation/PreferLocationStudyNavigation.kt +++ b/feature/study/src/main/java/com/umcspot/spot/study/preferLocation/navigation/PreferLocationStudyNavigation.kt @@ -1,34 +1,34 @@ -//package com.umcspot.spot.study.preferLocation.navigation -// -//import androidx.compose.foundation.layout.PaddingValues -//import androidx.navigation.NavController -//import androidx.navigation.NavGraphBuilder -//import androidx.navigation.NavOptions -//import androidx.navigation.compose.composable -//import com.umcspot.spot.navigation.Route -//import com.umcspot.spot.study.model.StudyResult -//import com.umcspot.spot.study.preferLocation.PreferLocationStudyScreen -//import kotlinx.serialization.Serializable -// -//fun NavController.navigateToPreferLocationStudy(navOptions: NavOptions? = null) { -// navigate(PreferLocation, navOptions) -//} -// -//fun NavGraphBuilder.preferLocationStudyGraph( -// contentPadding : PaddingValues, -// onRegisterScrollToTop: ((() -> Unit)?) -> Unit, -// onFilterClick : () -> Unit, -// onItemClick : (StudyResult) -> Unit -//) { -// composable { -// PreferLocationStudyScreen( -// contentPadding = contentPadding, -// onRegisterScrollToTop = onRegisterScrollToTop, -// onFilterClick = onFilterClick, -// onItemClick = onItemClick -// ) -// } -//} -// -//@Serializable -//data object PreferLocation : Route \ No newline at end of file +package com.umcspot.spot.study.preferLocation.navigation + +import androidx.compose.foundation.layout.PaddingValues +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavOptions +import androidx.navigation.compose.composable +import com.umcspot.spot.navigation.Route +import com.umcspot.spot.study.model.StudyResult +import com.umcspot.spot.study.preferLocation.PreferLocationStudyScreen +import kotlinx.serialization.Serializable + +fun NavController.navigateToPreferLocationStudy(navOptions: NavOptions? = null) { + navigate(PreferLocation, navOptions) +} + +fun NavGraphBuilder.preferLocationStudyGraph( + contentPadding : PaddingValues, + onRegisterScrollToTop: ((() -> Unit)?) -> Unit, + onFilterClick : () -> Unit, + onItemClick : (StudyResult) -> Unit +) { + composable { + PreferLocationStudyScreen( + contentPadding = contentPadding, + onRegisterScrollToTop = onRegisterScrollToTop, + onFilterClick = onFilterClick, + onItemClick = onItemClick + ) + } +} + +@Serializable +data object PreferLocation : Route \ No newline at end of file diff --git a/feature/study/src/main/java/com/umcspot/spot/study/recruiting/RecruitingStudyFilterScreen.kt b/feature/study/src/main/java/com/umcspot/spot/study/recruiting/RecruitingStudyFilterScreen.kt index 8381e5c8..2c512ecc 100644 --- a/feature/study/src/main/java/com/umcspot/spot/study/recruiting/RecruitingStudyFilterScreen.kt +++ b/feature/study/src/main/java/com/umcspot/spot/study/recruiting/RecruitingStudyFilterScreen.kt @@ -76,7 +76,8 @@ fun RecruitingStudyFilterScreen( ) var draftThemes by rememberSaveable(stateSaver = themeSaver) { mutableStateOf(themes) } - val acceptEnabled = draftActivity != null || draftFee != null || draftThemes.isNotEmpty() + val acceptEnabled = true // draftActivity != null || draftFee != null || draftThemes.isNotEmpty() + val topPad = contentPadding.calculateTopPadding() val bottomPad = contentPadding.calculateBottomPadding() @@ -132,15 +133,15 @@ fun RecruitingStudyFilterScreenContent( modifier = Modifier .fillMaxSize() .verticalScroll(rememberScrollState()) - .padding(horizontal = 16.dp) + .padding(horizontal = screenWidthDp(17.dp)) ) { Text( text = "활동", - style = SpotTheme.typography.medium_500.copy(fontSize = 15.sp), + style = SpotTheme.typography.h5, color = SpotTheme.colors.black ) - Spacer(modifier = Modifier.height(screenHeightDp(10.dp))) + Spacer(modifier = Modifier.height(screenHeightDp(20.dp))) ActivityTypeMultiSection( @@ -148,15 +149,22 @@ fun RecruitingStudyFilterScreenContent( onToggle = onToggleActivity ) - Spacer(modifier = Modifier.height(screenHeightDp(30.dp))) + Spacer(modifier = Modifier.height(screenHeightDp(53.dp))) + Text( + text = "활동비", + style = SpotTheme.typography.medium_500.copy(fontSize = 15.sp), + color = SpotTheme.colors.black + ) + + Spacer(modifier = Modifier.height(screenHeightDp(20.dp))) ActivityFeeSection( selectedFee = selectedFee, onToggle = onToggleFee ) - Spacer(modifier = Modifier.height(screenHeightDp(30.dp))) + Spacer(modifier = Modifier.height(screenHeightDp(53.dp))) Text( text = "스터디 테마", @@ -172,7 +180,7 @@ fun RecruitingStudyFilterScreenContent( maxSelection = 10 ) - Spacer(modifier = Modifier.height(screenHeightDp(20.dp))) + Spacer(modifier = Modifier.height(screenHeightDp(33.dp))) ResetFilterText( onClick = onReset @@ -186,7 +194,7 @@ fun RecruitingStudyFilterScreenContent( .align(alignment = Alignment.BottomCenter) .fillMaxWidth() .navigationBarsPadding() - .padding(horizontal = 16.dp, vertical = 12.dp) + .padding(horizontal = screenWidthDp(16.dp), vertical = screenHeightDp(12.dp)) .zIndex(1f) ) { TextButton( @@ -228,35 +236,22 @@ fun ActivityFeeSection( selectedFee: FeeRange?, onToggle: (FeeRange) -> Unit ) { - Column( - modifier = Modifier - .wrapContentSize() - .background(SpotTheme.colors.white) + FlowRow( + horizontalArrangement = Arrangement.spacedBy(screenWidthDp(14.dp)), + verticalArrangement = Arrangement.spacedBy(screenHeightDp(14.dp)) ) { - Text( - text = "활동비", - style = SpotTheme.typography.medium_500.copy(fontSize = 15.sp), - color = SpotTheme.colors.black - ) - Spacer(modifier = Modifier.height(10.dp)) - - FlowRow( - horizontalArrangement = Arrangement.spacedBy(14.dp), - verticalArrangement = Arrangement.spacedBy(14.dp) - ) { - FeeRange.entries.forEach { fee -> - TextButton( - text = fee.label, - modifier = Modifier - .width(screenWidthDp(71.dp)) - .height(screenHeightDp(35.dp)), - state = TextButtonState.Toggle, - checked = (selectedFee == fee), - onClick = { onToggle(fee) }, - shape = SpotShapes.Hard, - style = SpotTheme.typography.medium_500 - ) - } + FeeRange.entries.forEach { fee -> + TextButton( + text = fee.label, + modifier = Modifier + .width(screenWidthDp(71.dp)) + .height(screenHeightDp(35.dp)), + state = TextButtonState.Toggle, + checked = (selectedFee == fee), + onClick = { onToggle(fee) }, + shape = SpotShapes.Hard, + style = SpotTheme.typography.medium_500 + ) } } } @@ -268,11 +263,9 @@ fun ResetFilterText( ) { Text( text = "필터 초기화", - color = SpotTheme.colors.gray400, - style = SpotTheme.typography.small_400.copy( - fontSize = 13.sp, - textDecoration = TextDecoration.Underline - ), + color = SpotTheme.colors.gray500, + style = SpotTheme.typography.regular_500, + textDecoration = TextDecoration.Underline, modifier = modifier .semantics { role = Role.Button } .clickable( @@ -280,7 +273,6 @@ fun ResetFilterText( indication = null, onClick = onClick ) - .padding(vertical = 4.dp) ) } diff --git a/feature/study/src/main/java/com/umcspot/spot/study/recruiting/RecruitingStudyScreen.kt b/feature/study/src/main/java/com/umcspot/spot/study/recruiting/RecruitingStudyScreen.kt index da89e7e3..db8d98a1 100644 --- a/feature/study/src/main/java/com/umcspot/spot/study/recruiting/RecruitingStudyScreen.kt +++ b/feature/study/src/main/java/com/umcspot/spot/study/recruiting/RecruitingStudyScreen.kt @@ -293,7 +293,7 @@ fun HeaderRow( Text( text = sortType.label, color = SpotTheme.colors.black, - style = SpotTheme.typography.medium_500.copy(fontSize = 12.sp) + style = SpotTheme.typography.regular_500 ) Spacer(Modifier.width(screenWidthDp(7.dp))) Icon( diff --git a/feature/study/src/main/java/com/umcspot/spot/study/recruiting/RecruitingStudyViewModel.kt b/feature/study/src/main/java/com/umcspot/spot/study/recruiting/RecruitingStudyViewModel.kt index 45582897..0e819bc0 100644 --- a/feature/study/src/main/java/com/umcspot/spot/study/recruiting/RecruitingStudyViewModel.kt +++ b/feature/study/src/main/java/com/umcspot/spot/study/recruiting/RecruitingStudyViewModel.kt @@ -14,7 +14,6 @@ import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import javax.inject.Inject @@ -67,7 +66,8 @@ class RecruitingStudyViewModel @Inject constructor( categories = _themes.value.map { it.name }, isOnline = _activity.value.toIsOnline(), sortBy = _sortType.value, - size = 10 + size = 10, + cursor = null ).getOrThrow() }.onSuccess { data -> if (data.studyList.isEmpty()) { diff --git a/feature/study/src/main/java/com/umcspot/spot/study/register/RegisterStudyScreen.kt b/feature/study/src/main/java/com/umcspot/spot/study/register/RegisterStudyScreen.kt index b1cdfe69..8fbfbde5 100644 --- a/feature/study/src/main/java/com/umcspot/spot/study/register/RegisterStudyScreen.kt +++ b/feature/study/src/main/java/com/umcspot/spot/study/register/RegisterStudyScreen.kt @@ -23,6 +23,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.umcspot.spot.common.location.LocationRow import com.umcspot.spot.designsystem.component.appBar.BackTopBar import com.umcspot.spot.designsystem.component.button.SpotActivationButton import com.umcspot.spot.designsystem.theme.SpotTheme @@ -147,8 +148,8 @@ private fun RegisterStudyScreen( onQueryChange: (String) -> Unit, onSheetOpen: () -> Unit, onSheetDismiss: () -> Unit, - onAddSelected: (String) -> Unit, - onRemoveSelected: (String) -> Unit, + onAddSelected: (LocationRow) -> Unit, + onRemoveSelected: (LocationRow) -> Unit, onMemberCountChange: (Int) -> Unit, onFeeInfoChange: (Boolean?, String) -> Unit, onPersonalityChange: (Int, Int) -> Unit, diff --git a/feature/study/src/main/java/com/umcspot/spot/study/register/RegisterStudyViewModel.kt b/feature/study/src/main/java/com/umcspot/spot/study/register/RegisterStudyViewModel.kt index 4f4853cc..b8f77432 100644 --- a/feature/study/src/main/java/com/umcspot/spot/study/register/RegisterStudyViewModel.kt +++ b/feature/study/src/main/java/com/umcspot/spot/study/register/RegisterStudyViewModel.kt @@ -81,7 +81,7 @@ class RegisterStudyViewModel @Inject constructor( } } - fun addSelectedRegion(region: String) { + fun addSelectedRegion(region: LocationRow) { if (_uiState.value.selectedRegions.size < 10 && !_uiState.value.selectedRegions.contains( region ) @@ -94,7 +94,7 @@ class RegisterStudyViewModel @Inject constructor( } } - fun removeSelectedRegion(region: String) { + fun removeSelectedRegion(region: LocationRow) { _uiState.update { currentState -> val updatedRegions = currentState.selectedRegions.toMutableList().apply { remove(region) @@ -192,8 +192,8 @@ class RegisterStudyViewModel @Inject constructor( val regionCodes = if (currentState.activityType == ActivityType.ONLINE) { emptyList() } else { - currentState.selectedRegions.mapNotNull { regionName -> - allLocations.find { it.name == regionName }?.code + currentState.selectedRegions.mapNotNull { it -> + allLocations.find { it.fullName == it.fullName }?.code } } diff --git a/feature/study/src/main/java/com/umcspot/spot/study/register/component/SelectedRegionsSection.kt b/feature/study/src/main/java/com/umcspot/spot/study/register/component/SelectedRegionsSection.kt index 1f1cd21c..e6e5db02 100644 --- a/feature/study/src/main/java/com/umcspot/spot/study/register/component/SelectedRegionsSection.kt +++ b/feature/study/src/main/java/com/umcspot/spot/study/register/component/SelectedRegionsSection.kt @@ -20,6 +20,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp +import com.umcspot.spot.common.location.LocationRow import com.umcspot.spot.designsystem.R import com.umcspot.spot.designsystem.theme.B500 import com.umcspot.spot.designsystem.theme.Black @@ -30,8 +31,8 @@ import kotlinx.collections.immutable.ImmutableList @Composable fun SelectedRegionsSection( - selectedRegions: ImmutableList, - onRemoveClick: (String) -> Unit, + selectedRegions: ImmutableList, + onRemoveClick: (LocationRow) -> Unit, onAddClick: () -> Unit, modifier: Modifier = Modifier ) { @@ -41,7 +42,7 @@ fun SelectedRegionsSection( ) { selectedRegions.forEach { region -> RegionItem( - regionName = region, + regionName = region.fullName, onRemoveClick = { onRemoveClick(region) } ) } diff --git a/feature/study/src/main/java/com/umcspot/spot/study/register/model/RegisterStudyState.kt b/feature/study/src/main/java/com/umcspot/spot/study/register/model/RegisterStudyState.kt index a022dec1..2d5de2cf 100644 --- a/feature/study/src/main/java/com/umcspot/spot/study/register/model/RegisterStudyState.kt +++ b/feature/study/src/main/java/com/umcspot/spot/study/register/model/RegisterStudyState.kt @@ -14,7 +14,7 @@ data class RegisterStudyState( val isSheetVisible: Boolean = false, val locationQuery: String = "", val locationResults: List = emptyList(), - val selectedRegions: List = emptyList(), + val selectedRegions: List = emptyList(), val memberCount: Int = 2, val hasFee: Boolean? = null, diff --git a/feature/study/src/main/java/com/umcspot/spot/study/register/screen/StudyPlaceScreen.kt b/feature/study/src/main/java/com/umcspot/spot/study/register/screen/StudyPlaceScreen.kt index 8b073066..ca2f69e2 100644 --- a/feature/study/src/main/java/com/umcspot/spot/study/register/screen/StudyPlaceScreen.kt +++ b/feature/study/src/main/java/com/umcspot/spot/study/register/screen/StudyPlaceScreen.kt @@ -25,13 +25,13 @@ fun StudyPlaceScreen( isSheetVisible: Boolean, query: String, searchResults: List, - selectedRegions: ImmutableList, + selectedRegions: ImmutableList, onActivityTypeSelect: (ActivityType) -> Unit, onQueryChange: (String) -> Unit, onSheetOpen: () -> Unit, onSheetDismiss: () -> Unit, - onAddSelected: (String) -> Unit, - onRemoveSelected: (String) -> Unit, + onAddSelected: (LocationRow) -> Unit, + onRemoveSelected: (LocationRow) -> Unit, modifier: Modifier = Modifier ) { LocationBottomSheet( From de5b82ae58b823f3989c58a2f15ffd3f98ca84a4 Mon Sep 17 00:00:00 2001 From: yeonwoo Date: Wed, 7 Jan 2026 12:16:37 +0900 Subject: [PATCH 03/11] =?UTF-8?q?feat=20:=20recruitingStudy=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../bottomsheet/LocationBottomSheet.kt | 20 +- .../java/com/umcspot/spot/main/MainNavHost.kt | 7 + .../com/umcspot/spot/main/MainNavigator.kt | 7 +- .../java/com/umcspot/spot/main/MainScreen.kt | 2 + .../PreferLocationStudyFilterScreen.kt | 26 +- .../PreferLocationStudyScreen.kt | 256 ++++++++++++++---- .../PreferLocationStudyViewModel.kt | 37 ++- .../study/recruiting/RecruitingStudyScreen.kt | 31 ++- .../recruiting/RecruitingStudyViewModel.kt | 44 +-- 9 files changed, 326 insertions(+), 104 deletions(-) diff --git a/core/designsystem/src/main/java/com/umcspot/spot/designsystem/component/bottomsheet/LocationBottomSheet.kt b/core/designsystem/src/main/java/com/umcspot/spot/designsystem/component/bottomsheet/LocationBottomSheet.kt index 99fd2f99..bbe47a0a 100644 --- a/core/designsystem/src/main/java/com/umcspot/spot/designsystem/component/bottomsheet/LocationBottomSheet.kt +++ b/core/designsystem/src/main/java/com/umcspot/spot/designsystem/component/bottomsheet/LocationBottomSheet.kt @@ -13,6 +13,7 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets @@ -155,7 +156,7 @@ fun LocationBottomSheet( .fillMaxWidth() .height(sheetHeight) .offset { IntOffset(0, sheetOffset.value.roundToInt()) } - .clip(SpotShapes.SoftTop) + .clip(SpotShapes.RoundTop) .background(SpotTheme.colors.white) .imePadding() .focusRequester(blurFocusRequester) @@ -180,17 +181,21 @@ fun LocationBottomSheet( horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically ) { - Spacer(Modifier.width(screenWidthDp(24.dp))) + Spacer(modifier = Modifier.size(screenWidthDp(20.dp))) + Text( text = "스터디 지역", style = SpotTheme.typography.h5, color = SpotTheme.colors.black ) - IconButton(onClick = { animateAndDismiss() }) { + + IconButton( + modifier = Modifier.size(screenWidthDp(20.dp)), + onClick = { animateAndDismiss() }) { Icon( painter = painterResource(R.drawable.dismiss), contentDescription = "닫기", - modifier = Modifier.size(screenWidthDp(13.dp)) + modifier = Modifier.size(screenWidthDp(20.dp)) ) } } @@ -203,7 +208,7 @@ fun LocationBottomSheet( color = SpotTheme.colors.black ) - Spacer(Modifier.height(screenHeightDp(6.dp))) + Spacer(Modifier.height(screenHeightDp(4.dp))) Text( text = "최대 10개까지 추가할 수 있어요", @@ -244,7 +249,7 @@ fun LocationBottomSheet( } }, singleLine = true, - shape = RoundedCornerShape(10.dp), + shape = SpotShapes.Soft, colors = OutlinedTextFieldDefaults.colors( focusedBorderColor = SpotTheme.colors.B500, unfocusedBorderColor = SpotTheme.colors.gray300, @@ -252,11 +257,12 @@ fun LocationBottomSheet( ), modifier = Modifier .fillMaxWidth() + .height(screenHeightDp(51.dp)) .focusRequester(focusRequester) .onFocusChanged { fs -> isFocused = fs.isFocused if (isFocused) keyboard?.show() - } + }, ) SelectedChips( diff --git a/feature/main/src/main/java/com/umcspot/spot/main/MainNavHost.kt b/feature/main/src/main/java/com/umcspot/spot/main/MainNavHost.kt index e673eeb3..11eb84b1 100644 --- a/feature/main/src/main/java/com/umcspot/spot/main/MainNavHost.kt +++ b/feature/main/src/main/java/com/umcspot/spot/main/MainNavHost.kt @@ -26,6 +26,7 @@ import com.umcspot.spot.feature.board.post.posting.navigation.postingGraph import com.umcspot.spot.signup.navigation.signupGraph import com.umcspot.spot.study.detail.navigation.studyDetailGraph import com.umcspot.spot.study.my.navigation.myStudyGraph +import com.umcspot.spot.study.preferLocation.navigation.preferLocationStudyFilterGraph import com.umcspot.spot.study.preferLocation.navigation.preferLocationStudyGraph import com.umcspot.spot.study.recruiting.navigation.recruitingStudyFilterGraph import com.umcspot.spot.study.recruiting.navigation.recruitingStudyGraph @@ -101,6 +102,12 @@ fun MainNavHost( onFilterClick = { navigator.navigateToPreferLocationStudyFilter() }, ) + preferLocationStudyFilterGraph( + contentPadding = contentPadding, + navController = navigator.navController, + onAcceptFilterClick = { navigator.popBackStack() } + ) + boardGraph( contentPadding = contentPadding, navController = navigator.navController, diff --git a/feature/main/src/main/java/com/umcspot/spot/main/MainNavigator.kt b/feature/main/src/main/java/com/umcspot/spot/main/MainNavigator.kt index c2613175..b3f6e199 100644 --- a/feature/main/src/main/java/com/umcspot/spot/main/MainNavigator.kt +++ b/feature/main/src/main/java/com/umcspot/spot/main/MainNavigator.kt @@ -33,6 +33,8 @@ import com.umcspot.spot.signup.navigation.navigateToSignUp import com.umcspot.spot.study.detail.navigation.StudyDetail import com.umcspot.spot.study.detail.navigation.navigateToStudyDetail import com.umcspot.spot.study.my.navigation.navigateToMyStudy +import com.umcspot.spot.study.preferLocation.navigation.PreferLocation +import com.umcspot.spot.study.preferLocation.navigation.PreferLocationFilter import com.umcspot.spot.study.preferLocation.navigation.navigateToPreferLocationStudy import com.umcspot.spot.study.preferLocation.navigation.navigateToPreferLocationStudyFilter import com.umcspot.spot.study.recruiting.navigation.Recruiting @@ -94,11 +96,12 @@ class MainNavigator( fun isInLanding(): Boolean = inAnyGraph(Landing::class, Saving::class) @Composable - fun showBackTopBar(): Boolean = inAnyGraph(Alert::class, AppliedAlert::class, RecruitingFilter::class, + fun showBackTopBar(): Boolean = inAnyGraph(Alert::class, AppliedAlert::class, RecruitingFilter::class, PreferLocationFilter::class, SignUp::class, CheckList::class, Posting::class, BoardList::class) || inAnyGraphRoutes(POST_CONTENT_ROUTE) @Composable - fun showToTopFab(): Boolean = inAnyGraph(Alert::class, AppliedAlert::class, Recruiting::class,/*PreferLocation::class,*/ BoardList::class) + fun showToTopFab(): Boolean = inAnyGraph(Alert::class, AppliedAlert::class, Recruiting::class, + PreferLocation::class, BoardList::class) @Composable fun showMultipleFab(): Boolean = inAnyGraph(BoardList::class) diff --git a/feature/main/src/main/java/com/umcspot/spot/main/MainScreen.kt b/feature/main/src/main/java/com/umcspot/spot/main/MainScreen.kt index 53696997..46084961 100644 --- a/feature/main/src/main/java/com/umcspot/spot/main/MainScreen.kt +++ b/feature/main/src/main/java/com/umcspot/spot/main/MainScreen.kt @@ -37,6 +37,7 @@ import com.umcspot.spot.feature.board.post.posting.navigation.navigateToPostingN import com.umcspot.spot.signup.navigation.CheckList import com.umcspot.spot.signup.navigation.SignUp import com.umcspot.spot.study.detail.navigation.StudyDetail +import com.umcspot.spot.study.preferLocation.navigation.PreferLocationFilter import com.umcspot.spot.study.recruiting.navigation.RecruitingFilter import com.umcspot.spot.study.register.navigation.RegisterStudy import kotlinx.collections.immutable.toImmutableList @@ -64,6 +65,7 @@ fun MainScreen( dest?.hasRoute(Alert::class) == true -> "알림" dest?.hasRoute(AppliedAlert::class) == true -> "신청한 알림" dest?.hasRoute(RecruitingFilter::class) == true -> "모집중인 스터디" + dest?.hasRoute(PreferLocationFilter::class) == true -> "내 지역 스터디" dest?.hasRoute(SignUp::class) == true -> "회원가입" dest?.hasRoute(CheckList::class) == true -> "체크리스트" dest?.hasRoute(Posting::class) == true -> "글쓰기" diff --git a/feature/study/src/main/java/com/umcspot/spot/study/preferLocation/PreferLocationStudyFilterScreen.kt b/feature/study/src/main/java/com/umcspot/spot/study/preferLocation/PreferLocationStudyFilterScreen.kt index ad3b52d7..219f6c53 100644 --- a/feature/study/src/main/java/com/umcspot/spot/study/preferLocation/PreferLocationStudyFilterScreen.kt +++ b/feature/study/src/main/java/com/umcspot/spot/study/preferLocation/PreferLocationStudyFilterScreen.kt @@ -78,7 +78,7 @@ fun PreferLocationStudyFilterScreen( RecruitingStudyFilterScreenContent( modifier = Modifier.padding(top = topPad, bottom = bottomPad), - recruitingStatus = recruitingStatus, + recruitingStatus = draftRecruitingStatus, fee = draftFee, themes = draftThemes, buttonEnabled = acceptEnabled, @@ -115,12 +115,12 @@ fun RecruitingStudyFilterScreenContent( modifier: Modifier = Modifier ) { Box( - modifier = Modifier + modifier = modifier .fillMaxSize() .background(SpotTheme.colors.white) ) { Column( - modifier = modifier + modifier = Modifier .fillMaxSize() .verticalScroll(rememberScrollState()) .padding(horizontal = screenWidthDp(17.dp)) @@ -156,6 +156,14 @@ fun RecruitingStudyFilterScreenContent( Spacer(modifier = Modifier.height(53.dp)) + Text( + text = "스터디 테마", + style = SpotTheme.typography.h5, + color = SpotTheme.colors.black + ) + + Spacer(modifier = Modifier.height(screenHeightDp(20.dp))) + ActivityThemeSection( selectedThemes = themes, onSelect = onToggleTheme, @@ -203,11 +211,15 @@ fun RecruitingStatusMultiSection( ) { RecruitingStatus.entries.forEach { type -> TextButton( - modifier = Modifier.weight(1f), - text = type.name, - shape = SpotShapes.Soft, + modifier = Modifier + .width(screenWidthDp(71.dp)) + .height(screenHeightDp(35.dp)), + text = type.value, + shape = SpotShapes.Hard, + state = TextButtonState.Toggle, checked = (recruitingStatus == type), - onClick = { onSelect(type) } + onClick = { onSelect(type) }, + style = SpotTheme.typography.medium_500 ) } } diff --git a/feature/study/src/main/java/com/umcspot/spot/study/preferLocation/PreferLocationStudyScreen.kt b/feature/study/src/main/java/com/umcspot/spot/study/preferLocation/PreferLocationStudyScreen.kt index 8928d947..9d00e279 100644 --- a/feature/study/src/main/java/com/umcspot/spot/study/preferLocation/PreferLocationStudyScreen.kt +++ b/feature/study/src/main/java/com/umcspot/spot/study/preferLocation/PreferLocationStudyScreen.kt @@ -2,32 +2,42 @@ package com.umcspot.spot.study.preferLocation import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.background +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton +import androidx.compose.material3.ListItem +import androidx.compose.material3.ListItemDefaults +import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.OutlinedButton import androidx.compose.material3.ScrollableTabRow +import androidx.compose.material3.Surface import androidx.compose.material3.Tab import androidx.compose.material3.TabRowDefaults import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset import androidx.compose.material3.Text +import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -38,6 +48,7 @@ import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.drawWithContent import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Size @@ -48,12 +59,16 @@ import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.umcspot.spot.designsystem.R +import com.umcspot.spot.designsystem.component.SpotSpinner import com.umcspot.spot.designsystem.component.bottomsheet.LocationBottomSheet +import com.umcspot.spot.designsystem.component.empty.EmptyAlert import com.umcspot.spot.designsystem.component.empty.EmptyAlertWithButton import com.umcspot.spot.designsystem.component.study.StudyListItem import com.umcspot.spot.designsystem.shapes.SpotShapes +import com.umcspot.spot.designsystem.theme.B100 import com.umcspot.spot.designsystem.theme.B500 import com.umcspot.spot.designsystem.theme.G200 +import com.umcspot.spot.designsystem.theme.G300 import com.umcspot.spot.designsystem.theme.SpotTheme import com.umcspot.spot.model.RecruitingStudySort import com.umcspot.spot.study.model.StudyResult @@ -77,7 +92,8 @@ fun PreferLocationStudyScreen( val selected by viewmodel.selected.collectAsStateWithLifecycle() val isLoadingMore by viewmodel.isLoadingMore.collectAsStateWithLifecycle() - var showSheet by remember { mutableStateOf(false) } + var showLocationSheet by remember { mutableStateOf(false) } + var showSortSheet by remember { mutableStateOf(false) } var selectedTab by remember { mutableStateOf(0) } // 0 = 전체 val tabs: List = remember(selected) { listOf("전체") + selected.map { it.fullName } } @@ -87,6 +103,9 @@ fun PreferLocationStudyScreen( val topPad = contentPadding.calculateTopPadding() val bottomPad = contentPadding.calculateBottomPadding() + val isFiltered by viewmodel.isFiltered.collectAsStateWithLifecycle() + val isNullPreferLocation by viewmodel.isNullPreferLocation.collectAsStateWithLifecycle() + // 최초 데이터 로드 LaunchedEffect(Unit) { viewmodel.load() @@ -114,20 +133,17 @@ fun PreferLocationStudyScreen( } } - val studiesForUi = when (val s = ui.data) { - is UiState.Success -> s.data.studyList - else -> emptyList() - } + val studiesForUi = (ui.data as? UiState.Success)?.data?.studyList.orEmpty() + val isSuccess = ui.data is UiState.Success + + LaunchedEffect(isSuccess, studiesForUi.size, isLoadingMore) { + if (!isSuccess) return@LaunchedEffect - LaunchedEffect(listState, studiesForUi.size, isLoadingMore) { snapshotFlow { listState.layoutInfo.visibleItemsInfo.lastOrNull()?.index } .collect { lastVisible -> val total = listState.layoutInfo.totalItemsCount - if (lastVisible != null && total > 0) { - // 마지막에서 3개 전쯤 도달하면 로드 - if (lastVisible >= total - 3) { - viewmodel.loadNextPage() - } + if (lastVisible != null && total > 0 && lastVisible >= total - 3) { + viewmodel.loadNextPage() } } } @@ -136,13 +152,13 @@ fun PreferLocationStudyScreen( modifier = Modifier .fillMaxSize() .background(SpotTheme.colors.white) - .padding(top = topPad, bottom = bottomPad, start = screenWidthDp(17.dp), end = screenWidthDp(17.dp)) + .padding(top = topPad, bottom = bottomPad) ) { Row( modifier = Modifier .fillMaxWidth() - .padding(vertical = screenHeightDp(9.dp)), + .padding(vertical = screenHeightDp(9.dp), horizontal = screenWidthDp(17.dp)), verticalAlignment = Alignment.CenterVertically ) { // 타이틀 @@ -163,54 +179,101 @@ fun PreferLocationStudyScreen( } ) - if (ui.data is UiState.Loading) { - Text("로딩 중...", color = Color.Gray, modifier = Modifier.padding(top = screenHeightDp(8.dp))) - } else if (ui.data is UiState.Failure) { - Text("에러: ${(ui.data as UiState.Failure).msg}", color = Color.Red, modifier = Modifier.padding(top = screenHeightDp(8.dp))) - } else if (studiesForUi.isEmpty()) { - Box(Modifier.fillMaxSize()) { - EmptyAlertWithButton( - modifier = Modifier.fillMaxSize(), - painter = painterResource(R.drawable.location_outline), - alertTitle = "내 지역이 아직 없어요!", - alertDes = "내 지역을 설정하고 스터디를 모아봐요.", - buttonText = "내 지역 설정하기", - onClick = { showSheet = true } - ) - } - } else { + if(!isNullPreferLocation) { Spacer(modifier = Modifier.height(screenHeightDp(12.dp))) HeaderRow( size = studiesForUi.size, sortType = sort, - onOpenSortSheet = { showSheet = true }, - onFilterClick = onFilterClick + onOpenSortSheet = { showSortSheet = true }, + onFilterClick = onFilterClick, + isFiltered = isFiltered ) + } - StudyList( - listState = listState, - items = studiesForUi, - onItemClick = onItemClick - ) + Box( + modifier = Modifier + .fillMaxSize() + ) { + when (val state = ui.data) { + is UiState.Success -> { + StudyList( + listState = listState, + items = state.data.studyList, + onItemClick = onItemClick + ) + } + + is UiState.Loading -> { + Surface( + color = SpotTheme.colors.white, + modifier = Modifier + .fillMaxSize() + ) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + SpotSpinner() + } + } + } + + is UiState.Empty -> { + if (isNullPreferLocation) { + EmptyAlertWithButton( + modifier = Modifier.fillMaxSize(), + painter = painterResource(R.drawable.location_outline), + alertTitle = "내 지역이 아직 없어요!", + alertDes = "내 지역을 설정하고 스터디를 모아봐요.", + buttonText = "내 지역 설정하기", + onClick = { showLocationSheet = true } + ) + } else { + EmptyAlert( + modifier = Modifier.fillMaxSize(), + painter = painterResource(R.drawable.emoji_sad), + alertTitle = "조건에 맞는 스터디가 없어요.", + alertDes = "필터를 재설정하고 스터디를 찾아보세요.", + ) + } + } + + is UiState.Failure -> { + Text( + "에러: ${state.msg}", + color = Color.Red, + modifier = Modifier + .padding(horizontal = screenWidthDp(17.dp)) + .padding(top = screenHeightDp(8.dp)) + ) + } + } } } // 지역 선택 바텀시트 LocationBottomSheet( - visible = showSheet, + visible = showLocationSheet, query = query, onQueryChange = { viewmodel.searchLocation(it) }, onDismiss = { viewmodel.syncPreferredRegions() - showSheet = false + showLocationSheet = false }, results = results, selected = selected, onAddSelected = { viewmodel.addLocation(it) }, onRemoveSelected = { viewmodel.removeLocation(it) } ) + + SortTypeBottomSheet( + visible = showSortSheet, + current = sort, + onSelect = { viewmodel.setSort(it) }, + onDismiss = { showSortSheet = false } + ) } @Composable @@ -221,19 +284,33 @@ private fun StudyList( ) { LazyColumn( state = listState, - modifier = Modifier.fillMaxSize() + modifier = Modifier + .fillMaxSize() + .padding(horizontal = screenWidthDp(17.dp)) ) { items( items = items, key = { it.id } ) { item -> + Spacer(Modifier.padding(screenHeightDp(5.dp))) + StudyListItem( item = item, modifier = Modifier - .fillMaxWidth() - .padding(vertical = screenWidthDp(5.dp)), + .fillMaxWidth(), onClick = { onItemClick(item) } ) + + if(items.indexOf(item) != items.lastIndex) { + Spacer(Modifier.padding(screenHeightDp(5.dp))) + + HorizontalDivider( + modifier = Modifier + .fillMaxWidth(), + color = SpotTheme.colors.G300, + thickness = 1.dp + ) + } } } } @@ -243,10 +320,13 @@ fun HeaderRow( size: Int, sortType: RecruitingStudySort, onOpenSortSheet: () -> Unit, - onFilterClick: () -> Unit + onFilterClick: () -> Unit, + isFiltered: Boolean ) { Row( - modifier = Modifier.fillMaxWidth(), + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = screenWidthDp(17.dp)), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically ) { @@ -282,14 +362,23 @@ fun HeaderRow( Spacer(Modifier.width(screenWidthDp(10.dp))) - IconButton( - onClick = onFilterClick, - modifier = Modifier.size(screenWidthDp(26.dp)) + Box( + modifier = Modifier + .size(screenWidthDp(26.dp)) + .clip(SpotShapes.Hard) + .background( + color = if (isFiltered) SpotTheme.colors.B100 else SpotTheme.colors.white + ) + .clickable( + onClick = onFilterClick + ), + contentAlignment = Alignment.Center ) { Icon( painter = painterResource(R.drawable.filter), contentDescription = "필터", - modifier = Modifier.size(screenWidthDp(14.dp)) + modifier = Modifier.size(screenWidthDp(14.dp)), + tint = if (isFiltered) SpotTheme.colors.B500 else SpotTheme.colors.black ) } } @@ -376,4 +465,77 @@ private fun SelectedLocationTabs( } } } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SortTypeBottomSheet( + visible: Boolean, + current: RecruitingStudySort?, + onSelect: (RecruitingStudySort) -> Unit, + onDismiss: () -> Unit +) { + if(!visible) return + + val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + + ModalBottomSheet( + modifier = Modifier + .fillMaxWidth(), + onDismissRequest = onDismiss, + sheetState = sheetState, + shape = SpotShapes.RoundTop, + containerColor = SpotTheme.colors.white, + dragHandle = { }, + contentWindowInsets = { WindowInsets(0) }, + tonalElevation = 0.dp + ) { + Column( + modifier = Modifier + .navigationBarsPadding() + .padding(vertical = screenHeightDp(14.dp)) + ) { + RecruitingStudySort.entries.forEachIndexed { index, option -> + ListItem( + colors = ListItemDefaults.colors( + containerColor = SpotTheme.colors.white, + headlineColor = SpotTheme.colors.black, // (선택) 텍스트 색 명시 + trailingIconColor = SpotTheme.colors.B500 // (선택) + ), + headlineContent = { + Text( + modifier = Modifier, + text = option.label, + color = SpotTheme.colors.black, + style = SpotTheme.typography.medium_400 + ) + }, + trailingContent = { + if (option == current) { + Icon( + painter = painterResource(R.drawable.success_default), + tint = SpotTheme.colors.B500, + modifier = Modifier + .size(screenWidthDp(14.dp)), + contentDescription = "선택됨", + ) + } + }, + modifier = Modifier + .fillMaxWidth() + .clickable { + onSelect(option) + onDismiss() + } + .padding(horizontal = screenWidthDp(17.dp)) + ) + if (index != RecruitingStudySort.entries.lastIndex) { + HorizontalDivider( + color = SpotTheme.colors.G300, + thickness = 1.dp + ) + } + } + } + } } \ No newline at end of file diff --git a/feature/study/src/main/java/com/umcspot/spot/study/preferLocation/PreferLocationStudyViewModel.kt b/feature/study/src/main/java/com/umcspot/spot/study/preferLocation/PreferLocationStudyViewModel.kt index 609c5dc8..bc963bd7 100644 --- a/feature/study/src/main/java/com/umcspot/spot/study/preferLocation/PreferLocationStudyViewModel.kt +++ b/feature/study/src/main/java/com/umcspot/spot/study/preferLocation/PreferLocationStudyViewModel.kt @@ -19,8 +19,11 @@ import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import javax.inject.Inject @@ -76,6 +79,18 @@ class PreferLocationStudyViewModel @Inject constructor( private val _themes = MutableStateFlow>(emptyList()) val themes: StateFlow> = _themes.asStateFlow() + val isFiltered: StateFlow = + combine(_recruitingStatus, _fee, _themes) { recruitingStatus, fee, themes -> + recruitingStatus != null || fee != null || themes.isNotEmpty() + }.stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = false + ) + + private val _isNullPreferLocation = MutableStateFlow(false) + val isNullPreferLocation: StateFlow = _isNullPreferLocation.asStateFlow() + /** ========== BoardListViewModel의 load() 역할 ========== */ fun load(regionCode: String? = null) { currentRegionCode = regionCode @@ -87,12 +102,30 @@ class PreferLocationStudyViewModel @Inject constructor( } // ✅ 현재 선호 지역이 없으면 호출하지 않음 - val preferredCodes = _selectedRegion.value.map { it.code } + val preferredCodes = userRepository.getUserPreferredRegion() + .getOrThrow() + .regionCodes + .map { it.trim() } + .filter { it.isNotBlank() } + + val preferredRows = preferredCodes.mapNotNull { code -> + allLocations.find { it.code == code } + } + + _selectedRegion.value = preferredRows + + // 4) 선호지역이 없으면 스터디 호출 안 함 if (preferredCodes.isEmpty()) { - _uiState.update { it.copy(data = UiState.Empty) } + _isNullPreferLocation.value = true + _uiState.update { + it.copy(data = UiState.Empty) + } return@launch } + _isNullPreferLocation.value = false + + // 5) 탭(전체/특정지역)에 따라 요청 regionCodes 결정 val regionCodesForRequest = if (regionCode.isNullOrBlank()) preferredCodes else listOf(regionCode) diff --git a/feature/study/src/main/java/com/umcspot/spot/study/recruiting/RecruitingStudyScreen.kt b/feature/study/src/main/java/com/umcspot/spot/study/recruiting/RecruitingStudyScreen.kt index db8d98a1..626257eb 100644 --- a/feature/study/src/main/java/com/umcspot/spot/study/recruiting/RecruitingStudyScreen.kt +++ b/feature/study/src/main/java/com/umcspot/spot/study/recruiting/RecruitingStudyScreen.kt @@ -1,8 +1,10 @@ package com.umcspot.spot.study.recruiting +import android.R.color.white import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.background import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -23,10 +25,12 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material.ripple.rememberRipple import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton +import androidx.compose.material3.IconButtonDefaults import androidx.compose.material3.ListItem import androidx.compose.material3.ListItemDefaults import androidx.compose.material3.ModalBottomSheet @@ -45,7 +49,9 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @@ -59,6 +65,7 @@ import com.umcspot.spot.designsystem.component.SpotSpinner import com.umcspot.spot.designsystem.component.empty.EmptyAlert import com.umcspot.spot.designsystem.component.study.StudyListItem import com.umcspot.spot.designsystem.shapes.SpotShapes +import com.umcspot.spot.designsystem.theme.B100 import com.umcspot.spot.designsystem.theme.B500 import com.umcspot.spot.designsystem.theme.G200 import com.umcspot.spot.designsystem.theme.G300 @@ -97,6 +104,7 @@ fun RecruitingStudyScreen( is UiState.Success -> ui.data.studyList else -> emptyList() } + val isFiltered by viewmodel.isFiltered.collectAsStateWithLifecycle() val shouldLoadMore = remember { derivedStateOf { @@ -159,7 +167,8 @@ fun RecruitingStudyScreen( size = itemList.size, sortType = sort, onOpenSortSheet = { showSortSheet = true }, - onFilterClick = onFilterClick + onFilterClick = onFilterClick, + isFiltered = isFiltered ) Spacer(Modifier.height(screenHeightDp(10.dp))) @@ -267,7 +276,8 @@ fun HeaderRow( size: Int, sortType: RecruitingStudySort, onOpenSortSheet: () -> Unit, - onFilterClick: () -> Unit + onFilterClick: () -> Unit, + isFiltered: Boolean ) { Row( modifier = Modifier.fillMaxWidth(), @@ -306,14 +316,23 @@ fun HeaderRow( Spacer(Modifier.width(screenWidthDp(10.dp))) - IconButton( - onClick = onFilterClick, - modifier = Modifier.size(screenWidthDp(26.dp)) + Box( + modifier = Modifier + .size(screenWidthDp(26.dp)) + .clip(SpotShapes.Hard) + .background( + color = if (isFiltered) SpotTheme.colors.B100 else SpotTheme.colors.white + ) + .clickable( + onClick = onFilterClick + ), + contentAlignment = Alignment.Center ) { Icon( painter = painterResource(R.drawable.filter), contentDescription = "필터", - modifier = Modifier.size(screenWidthDp(14.dp)) + modifier = Modifier.size(screenWidthDp(14.dp)), + tint = if (isFiltered) SpotTheme.colors.B500 else SpotTheme.colors.black ) } } diff --git a/feature/study/src/main/java/com/umcspot/spot/study/recruiting/RecruitingStudyViewModel.kt b/feature/study/src/main/java/com/umcspot/spot/study/recruiting/RecruitingStudyViewModel.kt index 0e819bc0..83a98b69 100644 --- a/feature/study/src/main/java/com/umcspot/spot/study/recruiting/RecruitingStudyViewModel.kt +++ b/feature/study/src/main/java/com/umcspot/spot/study/recruiting/RecruitingStudyViewModel.kt @@ -12,8 +12,11 @@ import com.umcspot.spot.study.repository.StudyRepository import com.umcspot.spot.ui.state.UiState import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import javax.inject.Inject @@ -51,11 +54,14 @@ class RecruitingStudyViewModel @Inject constructor( private val _themes = MutableStateFlow>(emptyList()) val themes: StateFlow> = _themes.asStateFlow() - private fun calcNotNull(): Boolean = - _activity.value != null || _fee.value != null || _themes.value.isNotEmpty() - - private val _notNull = MutableStateFlow(calcNotNull()) - val notNull: StateFlow = _notNull.asStateFlow() + val isFiltered: StateFlow = + combine(_activity, _fee, _themes) { activity, fee, themes -> + activity != null || fee != null || themes.isNotEmpty() + }.stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = false + ) fun load() { _uiState.update { it.copy(studies = UiState.Loading) } @@ -121,34 +127,6 @@ class RecruitingStudyViewModel @Inject constructor( /** Filter 변경 **/ - private fun updateNotNull() { - _notNull.value = calcNotNull() - } - - - fun toggleActivity(type: ActivityType) { - _activity.value = if (_activity.value == type) null else type - updateNotNull() - } - - fun toggleFee(fee: FeeRange) { - _fee.value = if (_fee.value == fee) null else fee - updateNotNull() - } - - fun toggleTheme(theme: StudyTheme) { - val cur = _themes.value - _themes.value = if (cur.contains(theme)) cur - theme else cur + theme - updateNotNull() - } - - fun resetFilter() { - _fee.value = null - _activity.value = null - _themes.value = emptyList() - updateNotNull() - } - fun applyFilter(fee: FeeRange?, activity: ActivityType?, themes: List) { _fee.value = fee _activity.value = activity From 1dc6c480215ffb5f86f7cdcd132488dfee69493c Mon Sep 17 00:00:00 2001 From: yeonwoo Date: Wed, 7 Jan 2026 14:04:25 +0900 Subject: [PATCH 04/11] =?UTF-8?q?fix=20:=20TopBar=20=EC=82=AC=EC=9D=B4?= =?UTF-8?q?=EC=A6=88=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../designsystem/component/appBar/AppBar.kt | 35 +++++++++++-------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/core/designsystem/src/main/java/com/umcspot/spot/designsystem/component/appBar/AppBar.kt b/core/designsystem/src/main/java/com/umcspot/spot/designsystem/component/appBar/AppBar.kt index 1b7f125c..0df8921e 100644 --- a/core/designsystem/src/main/java/com/umcspot/spot/designsystem/component/appBar/AppBar.kt +++ b/core/designsystem/src/main/java/com/umcspot/spot/designsystem/component/appBar/AppBar.kt @@ -38,9 +38,9 @@ import com.umcspot.spot.designsystem.theme.G300 import com.umcspot.spot.designsystem.theme.G400 import com.umcspot.spot.designsystem.theme.SpotTheme import com.umcspot.spot.designsystem.theme.White +import com.umcspot.spot.ui.extension.screenHeightDp import com.umcspot.spot.ui.extension.screenWidthDp - @Composable fun AppBarHome ( hasAlert: Boolean = false, @@ -51,35 +51,40 @@ fun AppBarHome ( Row( modifier = modifier .fillMaxWidth() + .height(screenHeightDp(53.dp)) .background(SpotTheme.colors.white) - .padding(horizontal = 16.dp, vertical = 8.dp), + .padding(horizontal = screenWidthDp(17.dp)), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween ) { - Image( painter = painterResource(id = R.drawable.spot_logo), contentDescription = "App Logo", - modifier = Modifier.size(32.dp) + modifier = Modifier.size(screenWidthDp(33.dp)) ) Row(verticalAlignment = Alignment.CenterVertically) { - IconButton(onClick = onSearchClick) { + IconButton( + modifier = Modifier.size(screenWidthDp(32.dp)), + onClick = onSearchClick) { Icon( painter = painterResource(id = R.drawable.search), contentDescription = "Search", - modifier = Modifier.size(32.dp) + modifier = Modifier.size(screenWidthDp(24.dp)) ) } - Spacer(modifier = Modifier.width(8.dp)) - IconButton(onClick = onAlertClick) { + Spacer(modifier = Modifier.width(screenWidthDp(8.dp))) + IconButton( + modifier = Modifier.size(screenWidthDp(32.dp)), + onClick = onAlertClick + ) { Icon( painter = painterResource( id = if (hasAlert) R.drawable.alert_noti else R.drawable.alert ), contentDescription = if (hasAlert) "New Notifications" else "Notifications", tint = Color.Unspecified, - modifier = Modifier.size(32.dp) + modifier = Modifier.size(24.dp) ) } } @@ -119,21 +124,24 @@ fun BackTopBar( Row( modifier = modifier .fillMaxWidth() + .height(screenHeightDp(53.dp)) .background(SpotTheme.colors.white) - .padding(start = 5.dp, top = 16.dp, bottom = 16.dp), + .padding(start = screenWidthDp(17.dp)), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Start ) { IconButton( - onClick = onBackClick, + modifier = Modifier.size(screenWidthDp(20.dp)), + onClick = onBackClick ) { Icon( + modifier = Modifier.size(screenWidthDp(20.dp)), painter = painterResource(id = R.drawable.arrow_left), - contentDescription = "Back" + contentDescription = "Back" ) } - Spacer(modifier = Modifier.width((-2).dp)) + Spacer(modifier = Modifier.width(screenWidthDp(10.dp))) Text( text = title, @@ -238,7 +246,6 @@ fun SearchTopBar( @Composable fun PreviewSearchTopBarWithText() { var text by remember { mutableStateOf("안녕하세요") } - SpotTheme{ SearchTopBar( value = text, From 53c968b237e8c34db0382ec951f354f112d97891 Mon Sep 17 00:00:00 2001 From: yeonwoo Date: Wed, 7 Jan 2026 14:25:26 +0900 Subject: [PATCH 05/11] =?UTF-8?q?fix=20:=20locationSheet=20chip=20?= =?UTF-8?q?=EC=82=AC=EC=9D=B4=EC=A6=88=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../bottomsheet/LocationBottomSheet.kt | 63 +++++++++++-------- 1 file changed, 37 insertions(+), 26 deletions(-) diff --git a/core/designsystem/src/main/java/com/umcspot/spot/designsystem/component/bottomsheet/LocationBottomSheet.kt b/core/designsystem/src/main/java/com/umcspot/spot/designsystem/component/bottomsheet/LocationBottomSheet.kt index bbe47a0a..7c3c4fd9 100644 --- a/core/designsystem/src/main/java/com/umcspot/spot/designsystem/component/bottomsheet/LocationBottomSheet.kt +++ b/core/designsystem/src/main/java/com/umcspot/spot/designsystem/component/bottomsheet/LocationBottomSheet.kt @@ -3,7 +3,6 @@ package com.umcspot.spot.designsystem.component.bottomsheet import androidx.activity.compose.BackHandler import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.tween -import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.focusable @@ -13,7 +12,6 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ExperimentalLayoutApi -import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets @@ -26,13 +24,10 @@ import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.AssistChip -import androidx.compose.material3.AssistChipDefaults import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon @@ -56,7 +51,6 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.onFocusChanged -import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalSoftwareKeyboardController @@ -265,11 +259,15 @@ fun LocationBottomSheet( }, ) + Spacer(modifier = Modifier.height(screenHeightDp(13.dp))) + SelectedChips( items = selected, onRemove = onRemoveSelected ) + Spacer(modifier = Modifier.height(screenHeightDp(13.dp))) + if (results.isNotEmpty()) { val isMaxSelected = selected.size >= 10 @@ -348,36 +346,49 @@ private fun SelectedChips( Row( modifier = Modifier .fillMaxWidth() - .padding(top = screenHeightDp(13.dp)) + .height(screenHeightDp(23.dp)) .horizontalScroll(scrollState), horizontalArrangement = Arrangement.spacedBy(screenWidthDp(7.dp)) ) { - items.forEach { it -> - AssistChip( - onClick = {}, - label = { + items.forEach { item -> + Box( + modifier = Modifier + .wrapContentWidth() + .height(screenHeightDp(17.dp)) + .clip(SpotShapes.Hard) + .background(SpotTheme.colors.B100) + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() } + ) { + onRemove(item) + } + .padding( + start = screenWidthDp(7.dp), + end = screenWidthDp(4.dp) + ), + contentAlignment = Alignment.Center + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(screenWidthDp(4.dp)) + ) { Text( - text = it.neighborhood, - style = SpotTheme.typography.small_400 + text = item.neighborhood, + style = SpotTheme.typography.small_400, + color = SpotTheme.colors.B500, + maxLines = 1 ) - }, - trailingIcon = { + Icon( painter = painterResource(R.drawable.dismiss), contentDescription = "삭제", tint = SpotTheme.colors.B500, modifier = Modifier - .size(14.dp) - .clickable { onRemove(it) } + .size(screenWidthDp(14.dp)) ) - }, - colors = AssistChipDefaults.assistChipColors( - containerColor = SpotTheme.colors.B100, - labelColor = SpotTheme.colors.B500 - ), - border = BorderStroke(1.dp, SolidColor(SpotTheme.colors.B100)), - shape = RoundedCornerShape(6.dp) - ) + } + } } } } \ No newline at end of file From f1539c28f6174917c44e7f634cfae2bf3dd123f8 Mon Sep 17 00:00:00 2001 From: yeonwoo Date: Wed, 7 Jan 2026 14:25:47 +0900 Subject: [PATCH 06/11] =?UTF-8?q?feat=20:=20registerStudy=20route=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/java/com/umcspot/spot/main/MainNavigator.kt | 3 ++- .../main/src/main/java/com/umcspot/spot/main/MainScreen.kt | 5 ++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/feature/main/src/main/java/com/umcspot/spot/main/MainNavigator.kt b/feature/main/src/main/java/com/umcspot/spot/main/MainNavigator.kt index b3f6e199..6b8bae60 100644 --- a/feature/main/src/main/java/com/umcspot/spot/main/MainNavigator.kt +++ b/feature/main/src/main/java/com/umcspot/spot/main/MainNavigator.kt @@ -23,6 +23,7 @@ import com.umcspot.spot.jjim.navigation.navigateToJJim import com.umcspot.spot.mypage.navigation.navigateToMypage import com.umcspot.spot.feature.board.post.content.navigation.POST_CONTENT_ROUTE import com.umcspot.spot.feature.board.post.posting.navigation.Posting +import com.umcspot.spot.home.navigation.Home import com.umcspot.spot.signup.navigation.CheckList import com.umcspot.spot.signup.navigation.Landing import com.umcspot.spot.signup.navigation.Saving @@ -104,7 +105,7 @@ class MainNavigator( PreferLocation::class, BoardList::class) @Composable - fun showMultipleFab(): Boolean = inAnyGraph(BoardList::class) + fun showMultipleFab(): Boolean = inAnyGraph(Home::class, BoardList::class) @Composable fun showBottomBar(): Boolean { diff --git a/feature/main/src/main/java/com/umcspot/spot/main/MainScreen.kt b/feature/main/src/main/java/com/umcspot/spot/main/MainScreen.kt index 46084961..9468f0ae 100644 --- a/feature/main/src/main/java/com/umcspot/spot/main/MainScreen.kt +++ b/feature/main/src/main/java/com/umcspot/spot/main/MainScreen.kt @@ -34,6 +34,7 @@ import com.umcspot.spot.main.component.MainBottomBar import com.umcspot.spot.feature.board.post.content.navigation.POST_CONTENT_ROUTE import com.umcspot.spot.feature.board.post.posting.navigation.Posting import com.umcspot.spot.feature.board.post.posting.navigation.navigateToPostingNew +import com.umcspot.spot.home.navigation.Home import com.umcspot.spot.signup.navigation.CheckList import com.umcspot.spot.signup.navigation.SignUp import com.umcspot.spot.study.detail.navigation.StudyDetail @@ -106,7 +107,9 @@ fun MainScreen( dest?.hasRoute(BoardList::class) == true -> { navigator.navController.navigateToPostingNew() } - + dest?.hasRoute(Home::class) == true -> { + navigator.navigateToRegisterStudy() + } } }, spacing = 12.dp, From 22f6f0c13ad5ae46a99ec21ec5764b33a8b75a44 Mon Sep 17 00:00:00 2001 From: yeonwoo Date: Wed, 7 Jan 2026 14:26:28 +0900 Subject: [PATCH 07/11] =?UTF-8?q?feat=20:=20recruiting=20&=20preferlocatio?= =?UTF-8?q?n=20study=20=EA=B5=AC=ED=98=84=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../PreferLocationStudyFilterScreen.kt | 122 +++++++++++------- .../PreferLocationStudyScreen.kt | 13 +- .../PreferLocationStudyViewModel.kt | 12 +- .../recruiting/RecruitingStudyFilterScreen.kt | 9 +- .../study/recruiting/RecruitingStudyScreen.kt | 9 -- .../RecruitingStudyFilterNavigation.kt | 1 - .../navigation/RecruitingStudyNavigation.kt | 5 +- 7 files changed, 89 insertions(+), 82 deletions(-) diff --git a/feature/study/src/main/java/com/umcspot/spot/study/preferLocation/PreferLocationStudyFilterScreen.kt b/feature/study/src/main/java/com/umcspot/spot/study/preferLocation/PreferLocationStudyFilterScreen.kt index 219f6c53..d42c74f4 100644 --- a/feature/study/src/main/java/com/umcspot/spot/study/preferLocation/PreferLocationStudyFilterScreen.kt +++ b/feature/study/src/main/java/com/umcspot/spot/study/preferLocation/PreferLocationStudyFilterScreen.kt @@ -50,7 +50,7 @@ import com.umcspot.spot.ui.extension.screenWidthDp @Composable fun PreferLocationStudyFilterScreen( - contentPadding : PaddingValues, + contentPadding: PaddingValues, onAcceptFilterClick: () -> Unit, preferLocationVm: PreferLocationStudyViewModel = hiltViewModel(), ) { @@ -77,14 +77,21 @@ fun PreferLocationStudyFilterScreen( } RecruitingStudyFilterScreenContent( - modifier = Modifier.padding(top = topPad, bottom = bottomPad), + modifier = Modifier + .background(SpotTheme.colors.white) + .padding(top = topPad, bottom = bottomPad), recruitingStatus = draftRecruitingStatus, fee = draftFee, themes = draftThemes, buttonEnabled = acceptEnabled, - onToggleRecruitingStatus = { type -> draftRecruitingStatus = if (draftRecruitingStatus == type) null else type }, + onToggleRecruitingStatus = { type -> + draftRecruitingStatus = if (draftRecruitingStatus == type) null else type + }, onToggleFee = { fee -> draftFee = if (draftFee == fee) null else fee }, - onToggleTheme = { theme -> draftThemes = if (draftThemes.contains(theme)) draftThemes - theme else draftThemes + theme }, + onToggleTheme = { theme -> + draftThemes = + if (draftThemes.contains(theme)) draftThemes - theme else draftThemes + theme + }, onReset = { draftRecruitingStatus = null draftFee = null @@ -106,7 +113,7 @@ fun RecruitingStudyFilterScreenContent( recruitingStatus: RecruitingStatus?, fee: FeeRange?, themes: List, - buttonEnabled : Boolean, + buttonEnabled: Boolean, onToggleRecruitingStatus: (RecruitingStatus) -> Unit, onToggleFee: (FeeRange) -> Unit, onToggleTheme: (StudyTheme) -> Unit, @@ -117,66 +124,84 @@ fun RecruitingStudyFilterScreenContent( Box( modifier = modifier .fillMaxSize() - .background(SpotTheme.colors.white) ) { Column( modifier = Modifier .fillMaxSize() - .verticalScroll(rememberScrollState()) - .padding(horizontal = screenWidthDp(17.dp)) + .verticalScroll(rememberScrollState()), + verticalArrangement = Arrangement.Top ) { + Row( + modifier = Modifier + .fillMaxWidth() + .height(screenHeightDp(25.dp)) + .background(SpotTheme.colors.gray100), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "검색 결과는 모든 지역에 공통 반영됩니다.", + style = SpotTheme.typography.small_500, + color = SpotTheme.colors.black + ) + } + Column( + modifier = Modifier + .padding(top = screenHeightDp(18.dp)) + .padding(horizontal = screenWidthDp(17.dp)) + ) { + Text( + text = "모집 상태", + style = SpotTheme.typography.h5, + color = SpotTheme.colors.black + ) - Text( - text = "활동", - style = SpotTheme.typography.h5, - color = SpotTheme.colors.black - ) - - Spacer(modifier = Modifier.height(screenHeightDp(20.dp))) + Spacer(modifier = Modifier.height(screenHeightDp(20.dp))) - RecruitingStatusMultiSection( - recruitingStatus = recruitingStatus, - onSelect = onToggleRecruitingStatus - ) + RecruitingStatusMultiSection( + recruitingStatus = recruitingStatus, + onSelect = onToggleRecruitingStatus + ) - Spacer(modifier = Modifier.height(53.dp)) + Spacer(modifier = Modifier.height(53.dp)) - Text( - text = "활동비", - style = SpotTheme.typography.h5, - color = SpotTheme.colors.black - ) + Text( + text = "활동비", + style = SpotTheme.typography.h5, + color = SpotTheme.colors.black + ) - Spacer(modifier = Modifier.height(screenHeightDp(20.dp))) + Spacer(modifier = Modifier.height(screenHeightDp(20.dp))) - ActivityFeeSection( - activityFee = fee, - onSelect = onToggleFee - ) + ActivityFeeSection( + activityFee = fee, + onSelect = onToggleFee + ) - Spacer(modifier = Modifier.height(53.dp)) + Spacer(modifier = Modifier.height(53.dp)) - Text( - text = "스터디 테마", - style = SpotTheme.typography.h5, - color = SpotTheme.colors.black - ) + Text( + text = "스터디 테마", + style = SpotTheme.typography.h5, + color = SpotTheme.colors.black + ) - Spacer(modifier = Modifier.height(screenHeightDp(20.dp))) + Spacer(modifier = Modifier.height(screenHeightDp(20.dp))) - ActivityThemeSection( - selectedThemes = themes, - onSelect = onToggleTheme, - maxSelection = 10 - ) + ActivityThemeSection( + selectedThemes = themes, + onSelect = onToggleTheme, + maxSelection = 10 + ) - Spacer(modifier = Modifier.height(33.dp)) + Spacer(modifier = Modifier.height(33.dp)) - ResetFilterText( - onClick = onReset - ) + ResetFilterText( + onClick = onReset + ) - Spacer(Modifier.height(80.dp)) + Spacer(Modifier.height(80.dp)) + } } Box( @@ -233,7 +258,7 @@ fun ActivityFeeSection( FlowRow( horizontalArrangement = Arrangement.spacedBy(screenWidthDp(14.dp)), verticalArrangement = Arrangement.spacedBy(screenHeightDp(14.dp)) - ){ + ) { FeeRange.entries.forEach { fee -> TextButton( text = fee.label, @@ -249,6 +274,7 @@ fun ActivityFeeSection( } } } + @Composable fun ResetFilterText( onClick: () -> Unit, diff --git a/feature/study/src/main/java/com/umcspot/spot/study/preferLocation/PreferLocationStudyScreen.kt b/feature/study/src/main/java/com/umcspot/spot/study/preferLocation/PreferLocationStudyScreen.kt index 9d00e279..adb891b6 100644 --- a/feature/study/src/main/java/com/umcspot/spot/study/preferLocation/PreferLocationStudyScreen.kt +++ b/feature/study/src/main/java/com/umcspot/spot/study/preferLocation/PreferLocationStudyScreen.kt @@ -17,7 +17,6 @@ import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width -import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListState @@ -26,7 +25,6 @@ import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton import androidx.compose.material3.ListItem import androidx.compose.material3.ListItemDefaults import androidx.compose.material3.ModalBottomSheet @@ -95,7 +93,7 @@ fun PreferLocationStudyScreen( var showLocationSheet by remember { mutableStateOf(false) } var showSortSheet by remember { mutableStateOf(false) } var selectedTab by remember { mutableStateOf(0) } // 0 = 전체 - val tabs: List = remember(selected) { listOf("전체") + selected.map { it.fullName } } + val tabs: List = remember(selected) { listOf("전체") + selected.map { it.neighborhood } } val listState = rememberLazyListState() val scope = rememberCoroutineScope() @@ -121,7 +119,7 @@ fun PreferLocationStudyScreen( // 선택 칩 변화 시 탭 인덱스 보정 LaunchedEffect(selected.size) { - val maxIdx = (1 + selected.size) - 1 // "전체" 1개 + selected 크기 - 1 + val maxIdx = (1 + selected.size) - 1 if (selectedTab > maxIdx) selectedTab = maxIdx } @@ -129,7 +127,7 @@ fun PreferLocationStudyScreen( val maxIdx = (1 + selected.size) - 1 if (selectedTab > maxIdx) { selectedTab = maxIdx - viewmodel.selectTab(selectedTab) // ✅ 탭 인덱스 바뀌었으니 다시 호출 + viewmodel.selectTab(selectedTab) } } @@ -260,6 +258,7 @@ fun PreferLocationStudyScreen( onQueryChange = { viewmodel.searchLocation(it) }, onDismiss = { viewmodel.syncPreferredRegions() + viewmodel.clearLocationSearch() showLocationSheet = false }, results = results, @@ -499,8 +498,8 @@ fun SortTypeBottomSheet( ListItem( colors = ListItemDefaults.colors( containerColor = SpotTheme.colors.white, - headlineColor = SpotTheme.colors.black, // (선택) 텍스트 색 명시 - trailingIconColor = SpotTheme.colors.B500 // (선택) + headlineColor = SpotTheme.colors.black, + trailingIconColor = SpotTheme.colors.B500 ), headlineContent = { Text( diff --git a/feature/study/src/main/java/com/umcspot/spot/study/preferLocation/PreferLocationStudyViewModel.kt b/feature/study/src/main/java/com/umcspot/spot/study/preferLocation/PreferLocationStudyViewModel.kt index bc963bd7..aac7955e 100644 --- a/feature/study/src/main/java/com/umcspot/spot/study/preferLocation/PreferLocationStudyViewModel.kt +++ b/feature/study/src/main/java/com/umcspot/spot/study/preferLocation/PreferLocationStudyViewModel.kt @@ -101,7 +101,6 @@ class PreferLocationStudyViewModel @Inject constructor( allLocations = LocationStore.load(appContext) } - // ✅ 현재 선호 지역이 없으면 호출하지 않음 val preferredCodes = userRepository.getUserPreferredRegion() .getOrThrow() .regionCodes @@ -114,7 +113,6 @@ class PreferLocationStudyViewModel @Inject constructor( _selectedRegion.value = preferredRows - // 4) 선호지역이 없으면 스터디 호출 안 함 if (preferredCodes.isEmpty()) { _isNullPreferLocation.value = true _uiState.update { @@ -125,7 +123,6 @@ class PreferLocationStudyViewModel @Inject constructor( _isNullPreferLocation.value = false - // 5) 탭(전체/특정지역)에 따라 요청 regionCodes 결정 val regionCodesForRequest = if (regionCode.isNullOrBlank()) preferredCodes else listOf(regionCode) @@ -156,13 +153,11 @@ class PreferLocationStudyViewModel @Inject constructor( } } - fun loadNextPage() { val currentUi = _uiState.value.data val success = currentUi as? UiState.Success ?: return val currentList = success.data - // StudyResultList에 hasNext/nextCursor 있다고 가정(없으면 네 모델에 맞게 바꿔야 함) if (!currentList.hasNext) return if (_isLoadingMore.value) return @@ -170,7 +165,6 @@ class PreferLocationStudyViewModel @Inject constructor( _isLoadingMore.value = true runCatching { - // 선호 지역 전체 목록(전체 탭일 때 필요) val preferredCodes = userRepository.getUserPreferredRegion() .getOrThrow() .regionCodes @@ -204,7 +198,6 @@ class PreferLocationStudyViewModel @Inject constructor( } fun selectTab(selectedTabIndex: Int) { - // 0 = 전체, 1..n = selectedRegion[index-1] val code = if (selectedTabIndex == 0) null else _selectedRegion.value.getOrNull(selectedTabIndex - 1)?.code load(code) } @@ -262,6 +255,11 @@ class PreferLocationStudyViewModel @Inject constructor( } } + fun clearLocationSearch() { + _query.value = "" + _results.value = emptyList() + } + fun syncPreferredRegions() { viewModelScope.launch { runCatching { diff --git a/feature/study/src/main/java/com/umcspot/spot/study/recruiting/RecruitingStudyFilterScreen.kt b/feature/study/src/main/java/com/umcspot/spot/study/recruiting/RecruitingStudyFilterScreen.kt index 2c512ecc..5f03f011 100644 --- a/feature/study/src/main/java/com/umcspot/spot/study/recruiting/RecruitingStudyFilterScreen.kt +++ b/feature/study/src/main/java/com/umcspot/spot/study/recruiting/RecruitingStudyFilterScreen.kt @@ -17,13 +17,10 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width -import androidx.compose.foundation.layout.wrapContentHeight -import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -54,7 +51,6 @@ import com.umcspot.spot.model.FeeRange import com.umcspot.spot.model.StudyTheme import com.umcspot.spot.ui.extension.screenHeightDp import com.umcspot.spot.ui.extension.screenWidthDp -import kotlinx.collections.immutable.ImmutableList @Composable fun RecruitingStudyFilterScreen( @@ -87,7 +83,9 @@ fun RecruitingStudyFilterScreen( } RecruitingStudyFilterScreenContent( - modifier = Modifier.padding(top = topPad, bottom = bottomPad), + modifier = Modifier + .background(SpotTheme.colors.white) + .padding(top = topPad, bottom = bottomPad), selectedActivity = draftActivity, selectedFee = draftFee, selectedThemes = draftThemes, @@ -127,7 +125,6 @@ fun RecruitingStudyFilterScreenContent( Box( modifier = modifier .fillMaxSize() - .background(SpotTheme.colors.white) ) { Column( modifier = Modifier diff --git a/feature/study/src/main/java/com/umcspot/spot/study/recruiting/RecruitingStudyScreen.kt b/feature/study/src/main/java/com/umcspot/spot/study/recruiting/RecruitingStudyScreen.kt index 626257eb..35110da5 100644 --- a/feature/study/src/main/java/com/umcspot/spot/study/recruiting/RecruitingStudyScreen.kt +++ b/feature/study/src/main/java/com/umcspot/spot/study/recruiting/RecruitingStudyScreen.kt @@ -1,10 +1,8 @@ package com.umcspot.spot.study.recruiting -import android.R.color.white import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.background import androidx.compose.foundation.clickable -import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -25,12 +23,9 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.material.ripple.rememberRipple import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.IconButtonDefaults import androidx.compose.material3.ListItem import androidx.compose.material3.ListItemDefaults import androidx.compose.material3.ModalBottomSheet @@ -50,11 +45,8 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.Lifecycle.Event import androidx.lifecycle.LifecycleEventObserver @@ -72,7 +64,6 @@ import com.umcspot.spot.designsystem.theme.G300 import com.umcspot.spot.designsystem.theme.SpotTheme import com.umcspot.spot.model.RecruitingStudySort import com.umcspot.spot.study.model.StudyResult -import com.umcspot.spot.study.model.StudyResultList import com.umcspot.spot.ui.extension.screenHeightDp import com.umcspot.spot.ui.extension.screenWidthDp import com.umcspot.spot.ui.state.UiState diff --git a/feature/study/src/main/java/com/umcspot/spot/study/recruiting/navigation/RecruitingStudyFilterNavigation.kt b/feature/study/src/main/java/com/umcspot/spot/study/recruiting/navigation/RecruitingStudyFilterNavigation.kt index 38e4db0e..a6dd0603 100644 --- a/feature/study/src/main/java/com/umcspot/spot/study/recruiting/navigation/RecruitingStudyFilterNavigation.kt +++ b/feature/study/src/main/java/com/umcspot/spot/study/recruiting/navigation/RecruitingStudyFilterNavigation.kt @@ -23,7 +23,6 @@ fun NavGraphBuilder.recruitingStudyFilterGraph( ) { composable { val parentEntry = remember(navController) { - navController.getBackStackEntry(Recruiting) } diff --git a/feature/study/src/main/java/com/umcspot/spot/study/recruiting/navigation/RecruitingStudyNavigation.kt b/feature/study/src/main/java/com/umcspot/spot/study/recruiting/navigation/RecruitingStudyNavigation.kt index 7bb3e8c5..bfbc2343 100644 --- a/feature/study/src/main/java/com/umcspot/spot/study/recruiting/navigation/RecruitingStudyNavigation.kt +++ b/feature/study/src/main/java/com/umcspot/spot/study/recruiting/navigation/RecruitingStudyNavigation.kt @@ -1,16 +1,13 @@ package com.umcspot.spot.study.recruiting.navigation import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.runtime.remember -import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptions import androidx.navigation.compose.composable import com.umcspot.spot.navigation.Route -import com.umcspot.spot.study.recruiting.RecruitingStudyScreen import com.umcspot.spot.study.model.StudyResult +import com.umcspot.spot.study.recruiting.RecruitingStudyScreen import kotlinx.serialization.Serializable fun NavController.navigateToRecruitingStudy(navOptions: NavOptions? = null) { From b02a4efa415f078ec5dc5eeea703b21ccc3f1840 Mon Sep 17 00:00:00 2001 From: yeonwoo Date: Wed, 7 Jan 2026 16:42:52 +0900 Subject: [PATCH 08/11] =?UTF-8?q?fix=20:=20appbar=20height=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/umcspot/spot/designsystem/component/appBar/AppBar.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/designsystem/src/main/java/com/umcspot/spot/designsystem/component/appBar/AppBar.kt b/core/designsystem/src/main/java/com/umcspot/spot/designsystem/component/appBar/AppBar.kt index 0df8921e..610f2630 100644 --- a/core/designsystem/src/main/java/com/umcspot/spot/designsystem/component/appBar/AppBar.kt +++ b/core/designsystem/src/main/java/com/umcspot/spot/designsystem/component/appBar/AppBar.kt @@ -84,7 +84,7 @@ fun AppBarHome ( ), contentDescription = if (hasAlert) "New Notifications" else "Notifications", tint = Color.Unspecified, - modifier = Modifier.size(24.dp) + modifier = Modifier.size(screenWidthDp(24.dp)) ) } } From 1a72ca6246ba13b2cdea2eb48630c966b60022a2 Mon Sep 17 00:00:00 2001 From: yeonwoo Date: Wed, 7 Jan 2026 16:43:17 +0900 Subject: [PATCH 09/11] =?UTF-8?q?fix=20:=20isloading=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../umcspot/spot/study/recruiting/RecruitingStudyViewModel.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/feature/study/src/main/java/com/umcspot/spot/study/recruiting/RecruitingStudyViewModel.kt b/feature/study/src/main/java/com/umcspot/spot/study/recruiting/RecruitingStudyViewModel.kt index 83a98b69..f56974da 100644 --- a/feature/study/src/main/java/com/umcspot/spot/study/recruiting/RecruitingStudyViewModel.kt +++ b/feature/study/src/main/java/com/umcspot/spot/study/recruiting/RecruitingStudyViewModel.kt @@ -97,6 +97,7 @@ class RecruitingStudyViewModel @Inject constructor( if (_isLoadingMore.value) return viewModelScope.launch { + _isLoadingMore.value = true runCatching { studyRepository.getRecruitingStudies( feeCategory = _fee.value, @@ -116,6 +117,7 @@ class RecruitingStudyViewModel @Inject constructor( }.onFailure { e -> Log.e("RecruitingStudyViewModel", "loadNextpageError", e) } + _isLoadingMore.value = false } } From 95d500bb12a1723fe83c07d2d464ad62801e4151 Mon Sep 17 00:00:00 2001 From: yeonwoo Date: Wed, 7 Jan 2026 16:43:34 +0900 Subject: [PATCH 10/11] fix : remove useless import --- .../spot/user/dto/response/UserPreferredRegionResponseDto.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/data/user/src/main/java/com/umcspot/spot/user/dto/response/UserPreferredRegionResponseDto.kt b/data/user/src/main/java/com/umcspot/spot/user/dto/response/UserPreferredRegionResponseDto.kt index 42c283bf..375b0412 100644 --- a/data/user/src/main/java/com/umcspot/spot/user/dto/response/UserPreferredRegionResponseDto.kt +++ b/data/user/src/main/java/com/umcspot/spot/user/dto/response/UserPreferredRegionResponseDto.kt @@ -1,7 +1,6 @@ package com.umcspot.spot.user.dto.response import android.annotation.SuppressLint -import com.umcspot.spot.model.WeatherType import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable From d329be6743bac693fdd5899929b19a33b71dd6d6 Mon Sep 17 00:00:00 2001 From: yeonwoo Date: Wed, 7 Jan 2026 16:46:20 +0900 Subject: [PATCH 11/11] =?UTF-8?q?fix=20:=20registerStudy=20location=20?= =?UTF-8?q?=EB=B9=84=EA=B5=90=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/umcspot/spot/study/register/RegisterStudyViewModel.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/feature/study/src/main/java/com/umcspot/spot/study/register/RegisterStudyViewModel.kt b/feature/study/src/main/java/com/umcspot/spot/study/register/RegisterStudyViewModel.kt index b8f77432..01e48499 100644 --- a/feature/study/src/main/java/com/umcspot/spot/study/register/RegisterStudyViewModel.kt +++ b/feature/study/src/main/java/com/umcspot/spot/study/register/RegisterStudyViewModel.kt @@ -192,8 +192,8 @@ class RegisterStudyViewModel @Inject constructor( val regionCodes = if (currentState.activityType == ActivityType.ONLINE) { emptyList() } else { - currentState.selectedRegions.mapNotNull { it -> - allLocations.find { it.fullName == it.fullName }?.code + currentState.selectedRegions.mapNotNull { region -> + allLocations.find { it.fullName == region.fullName }?.code } }