diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 70f0abd4e..fd84fb279 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -115,6 +115,7 @@ dependencies { implementation(project(":feature:review")) implementation(project(":feature:application")) implementation(project(":feature:employment")) + implementation(project(":feature:post-review")) implementation(platform(libs.androidx.compose.bom)) implementation(libs.androidx.navigation.compose) diff --git a/app/src/main/java/team/retum/jobisandroidv2/JobisNavigator.kt b/app/src/main/java/team/retum/jobisandroidv2/JobisNavigator.kt index 96ad93e94..fa6b4282c 100644 --- a/app/src/main/java/team/retum/jobisandroidv2/JobisNavigator.kt +++ b/app/src/main/java/team/retum/jobisandroidv2/JobisNavigator.kt @@ -30,9 +30,15 @@ import team.retum.landing.navigation.navigateToLanding import team.retum.notification.navigation.NAVIGATION_NOTIFICATIONS import team.retum.notification.navigation.navigateToNotification import team.retum.notification.navigation.navigateToNotificationSetting -import team.retum.review.navigation.navigateToPostReview +import team.retum.post.review.model.PostReviewData +import team.retum.post.review.navigation.navigateToPostExpectReview +import team.retum.post.review.navigation.navigateToPostNextReview +import team.retum.post.review.navigation.navigateToPostReview +import team.retum.post.review.navigation.navigateToPostReviewComplete +import team.retum.review.navigation.navigateToReview import team.retum.review.navigation.navigateToReviewDetails -import team.retum.review.navigation.navigateToReviews +import team.retum.review.navigation.navigateToReviewFilter +import team.retum.review.navigation.navigateToSearchReview import team.retum.signin.navigation.navigateToSignIn import team.retum.signup.model.SignUpData import team.retum.signup.navigation.navigateToInputEmail @@ -128,8 +134,20 @@ internal class JobisNavigator( navController.navigateToLanding(popUpRoute = popUpRoute) } - fun navigateToPostReview(companyId: Long) { - navController.navigateToPostReview(companyId = companyId) + fun navigateToPostReview(companyName: String, companyId: Long) { + navController.navigateToPostReview(companyName = companyName, companyId = companyId) + } + + fun navigateToPostNextReview(reviewData: PostReviewData) { + navController.navigateToPostNextReview(reviewData = reviewData) + } + + fun navigateToPostReviewComplete() { + navController.navigateToPostReviewComplete() + } + + fun navigateToPostExpectReview(reviewData: PostReviewData) { + navController.navigateToPostExpectReview(reviewData = reviewData) } fun navigateToRoot(applicationId: Long = 0) { @@ -180,24 +198,23 @@ internal class JobisNavigator( navController.navigateToNoticeDetails(noticeId = noticeId) } - fun navigateToReviewDetails( - reviewId: String, - writer: String, - ) { - navController.navigateToReviewDetails( - reviewId = reviewId, - writer = writer, - ) + fun navigateToReviewDetails(reviewId: Long) { + navController.navigateToReviewDetails(reviewId = reviewId) + } + + fun navigateToSearchReview() { + navController.navigateToSearchReview() } - fun navigateToReviews( + fun navigateToReviewFilter() { + navController.navigateToReviewFilter() + } + + fun navigateToReview( companyId: Long, companyName: String, ) { - navController.navigateToReviews( - companyId = companyId, - companyName = companyName, - ) + navController.navigateToReview() } fun navigatedFromNotifications(): Boolean { diff --git a/app/src/main/java/team/retum/jobisandroidv2/navigation/MainNavigation.kt b/app/src/main/java/team/retum/jobisandroidv2/navigation/MainNavigation.kt index 0fb18cce5..87652cd1a 100644 --- a/app/src/main/java/team/retum/jobisandroidv2/navigation/MainNavigation.kt +++ b/app/src/main/java/team/retum/jobisandroidv2/navigation/MainNavigation.kt @@ -21,9 +21,13 @@ import team.retum.jobisandroidv2.root.NAVIGATION_ROOT import team.retum.jobisandroidv2.root.root import team.retum.notification.navigation.notificationSetting import team.retum.notification.navigation.notifications -import team.retum.review.navigation.postReview +import team.retum.post.review.navigation.postExpectReview +import team.retum.post.review.navigation.postNextReview +import team.retum.post.review.navigation.postReview +import team.retum.post.review.navigation.postReviewComplete import team.retum.review.navigation.reviewDetails -import team.retum.review.navigation.reviews +import team.retum.review.navigation.reviewFilter +import team.retum.review.navigation.searchReview const val NAVIGATION_MAIN = "main" @@ -49,6 +53,9 @@ internal fun NavGraphBuilder.mainNavigation( onNoticeClick = navigator::navigateToNotices, navigateToLanding = { navigator.navigateToLanding(NAVIGATION_ROOT) }, onPostReviewClick = navigator::navigateToPostReview, + onSearchReviewClick = navigator::navigateToSearchReview, + onReviewFilterClick = navigator::navigateToReviewFilter, + onReviewDetailClick = navigator::navigateToReviewDetails, navigateToApplication = navigator::navigateToApplication, navigateToRecruitmentDetails = navigator::navigateToRecruitmentDetails, navigatedFromNotifications = navigator.navigatedFromNotifications(), @@ -102,17 +109,33 @@ internal fun NavGraphBuilder.mainNavigation( onBackPressed = navigator::popBackStackIfNotHome, navigateToDetails = navigator::navigateToNoticeDetails, ) - postReview(onBackPressed = navigator::popBackStackIfNotHome) companyDetails( onBackPressed = navigator::popBackStackIfNotHome, navigateToReviewDetails = navigator::navigateToReviewDetails, - navigateToReviews = navigator::navigateToReviews, + navigateToReviews = navigator::navigateToReview, navigateToRecruitmentDetails = navigator::navigateToRecruitmentDetails, ) + postReview( + onBackPressed = navigator::popBackStackIfNotHome, + navigateToPostNextReview = navigator::navigateToPostNextReview, + ) + postNextReview( + onBackPressed = navigator::popBackStackIfNotHome, + navigateToPostExpectReview = navigator::navigateToPostExpectReview, + ) + postExpectReview( + onBackPressed = navigator::popBackStackIfNotHome, + onPostReviewCompleteClick = navigator::navigateToPostReviewComplete, + ) + postReviewComplete( + onBackPressed = navigator::popBackStackIfNotHome, + navigateToPostReview = navigator::navigateToPostReview, + ) reviewDetails(navigator::popBackStackIfNotHome) - reviews( - navigator::popBackStackIfNotHome, - navigateToReviewDetails = navigator::navigateToReviewDetails, + reviewFilter(onBackPressed = navigator::popBackStackIfNotHome) + searchReview( + onBackPressed = navigator::popBackStackIfNotHome, + onReviewDetailClick = navigator::navigateToReviewDetails, ) } } diff --git a/app/src/main/java/team/retum/jobisandroidv2/root/RootNavigation.kt b/app/src/main/java/team/retum/jobisandroidv2/root/RootNavigation.kt index 89c870730..c69f7cecb 100644 --- a/app/src/main/java/team/retum/jobisandroidv2/root/RootNavigation.kt +++ b/app/src/main/java/team/retum/jobisandroidv2/root/RootNavigation.kt @@ -24,7 +24,10 @@ fun NavGraphBuilder.root( onChangePasswordClick: () -> Unit, onReportBugClick: () -> Unit, navigateToLanding: () -> Unit, - onPostReviewClick: (Long) -> Unit, + onPostReviewClick: (String, Long) -> Unit, + onReviewFilterClick: () -> Unit, + onSearchReviewClick: () -> Unit, + onReviewDetailClick: (Long) -> Unit, navigateToApplication: (ApplicationData) -> Unit, navigateToRecruitmentDetails: (Long) -> Unit, navigatedFromNotifications: Boolean, @@ -48,7 +51,10 @@ fun NavGraphBuilder.root( onChangePasswordClick = onChangePasswordClick, onReportBugClick = onReportBugClick, navigateToLanding = navigateToLanding, + onReviewFilterClick = onReviewFilterClick, + onSearchReviewClick = onSearchReviewClick, onPostReviewClick = onPostReviewClick, + onReviewDetailClick = onReviewDetailClick, navigateToApplication = navigateToApplication, navigateToRecruitmentDetails = navigateToRecruitmentDetails, navigatedFromNotifications = navigatedFromNotifications, diff --git a/app/src/main/java/team/retum/jobisandroidv2/root/RootScreen.kt b/app/src/main/java/team/retum/jobisandroidv2/root/RootScreen.kt index b2c7e3cce..dd5d51896 100644 --- a/app/src/main/java/team/retum/jobisandroidv2/root/RootScreen.kt +++ b/app/src/main/java/team/retum/jobisandroidv2/root/RootScreen.kt @@ -39,6 +39,7 @@ import team.retum.jobisdesignsystemv2.button.JobisButton import team.retum.jobisdesignsystemv2.foundation.JobisTheme import team.retum.jobisdesignsystemv2.foundation.JobisTypography import team.retum.jobisdesignsystemv2.text.JobisText +import team.retum.review.navigation.review @Composable internal fun Root( @@ -55,7 +56,10 @@ internal fun Root( onSelectInterestClick: () -> Unit, onChangePasswordClick: () -> Unit, onReportBugClick: () -> Unit, - onPostReviewClick: (Long) -> Unit, + onPostReviewClick: (String, Long) -> Unit, + onReviewFilterClick: () -> Unit, + onSearchReviewClick: () -> Unit, + onReviewDetailClick: (Long) -> Unit, navigateToLanding: () -> Unit, navigateToApplication: (ApplicationData) -> Unit, navigateToRecruitmentDetails: (Long) -> Unit, @@ -92,6 +96,9 @@ internal fun Root( rejectionReason = applicationData.rejectionReason, navigateToLanding = navigateToLanding, onPostReviewClick = onPostReviewClick, + onReviewFilterClick = onReviewFilterClick, + onSearchReviewClick = onSearchReviewClick, + onReviewDetailClick = onReviewDetailClick, navigateToApplicationByRejectionBottomSheet = { coroutineScope.launch { sheetState.hide() @@ -125,7 +132,10 @@ private fun RootScreen( onReportBugClick: () -> Unit, rejectionReason: String, navigateToLanding: () -> Unit, - onPostReviewClick: (Long) -> Unit, + onPostReviewClick: (String, Long) -> Unit, + onReviewFilterClick: () -> Unit, + onSearchReviewClick: () -> Unit, + onReviewDetailClick: (Long) -> Unit, navigateToApplicationByRejectionBottomSheet: () -> Unit, navigateToApplication: (ApplicationData) -> Unit, navigateToRecruitmentDetails: (Long) -> Unit, @@ -172,6 +182,11 @@ private fun RootScreen( onRecruitmentsClick = navController::navigateToRecruitments, onRecruitmentDetailClick = onRecruitmentDetailsClick, ) + review( + onReviewFilterClick = onReviewFilterClick, + onSearchReviewClick = onSearchReviewClick, + onReviewDetailClick = onReviewDetailClick, + ) myPage( onSelectInterestClick = onSelectInterestClick, onChangePasswordClick = onChangePasswordClick, diff --git a/app/src/main/java/team/retum/jobisandroidv2/ui/BottomMenu.kt b/app/src/main/java/team/retum/jobisandroidv2/ui/BottomMenu.kt index 07272f953..f4c39685a 100644 --- a/app/src/main/java/team/retum/jobisandroidv2/ui/BottomMenu.kt +++ b/app/src/main/java/team/retum/jobisandroidv2/ui/BottomMenu.kt @@ -7,6 +7,7 @@ import team.retum.home.navigation.NAVIGATION_HOME import team.retum.jobis.R import team.retum.jobis.navigation.NAVIGATION_MY_PAGE import team.retum.jobis.recruitment.navigation.NAVIGATION_RECRUITMENTS +import team.retum.review.navigation.NAVIGATION_REVIEW sealed class BottomMenu( val route: String, @@ -25,6 +26,12 @@ sealed class BottomMenu( title = R.string.recruitment, ) + data object Review : BottomMenu( + route = NAVIGATION_REVIEW, + icon = R.drawable.ic_review, + title = R.string.review, + ) + data object Bookmark : BottomMenu( route = NAVIGATION_BOOKMARK, icon = R.drawable.ic_bookmark, diff --git a/app/src/main/java/team/retum/jobisandroidv2/ui/BottomNavigationBar.kt b/app/src/main/java/team/retum/jobisandroidv2/ui/BottomNavigationBar.kt index 8bad96ee3..f3f78f620 100644 --- a/app/src/main/java/team/retum/jobisandroidv2/ui/BottomNavigationBar.kt +++ b/app/src/main/java/team/retum/jobisandroidv2/ui/BottomNavigationBar.kt @@ -25,6 +25,7 @@ import team.retum.jobisdesignsystemv2.foundation.JobisTypography private val bottomMenus = listOf( BottomMenu.Home, BottomMenu.Recruitments, + BottomMenu.Review, BottomMenu.Bookmark, BottomMenu.MyPage, ) diff --git a/app/src/main/res/drawable/ic_review.xml b/app/src/main/res/drawable/ic_review.xml new file mode 100644 index 000000000..55d4a5bf6 --- /dev/null +++ b/app/src/main/res/drawable/ic_review.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0d320cd7e..76923359c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -3,6 +3,7 @@ 모집의뢰서 + 후기 북마크 마이페이지 diff --git a/core/common/src/main/java/team/retum/common/enums/ApplyStatus.kt b/core/common/src/main/java/team/retum/common/enums/ApplyStatus.kt index d5960e4e5..24eedc7e8 100644 --- a/core/common/src/main/java/team/retum/common/enums/ApplyStatus.kt +++ b/core/common/src/main/java/team/retum/common/enums/ApplyStatus.kt @@ -3,10 +3,12 @@ package team.retum.common.enums enum class ApplyStatus(val value: String) { REQUESTED("승인 요청"), APPROVED("승인"), + SEND("전송"), FAILED("탈락"), PASS("합격"), REJECTED("반려"), FIELD_TRAIN("현장실습"), - SEND("지원 중"), ACCEPTANCE("근로계약"), + DOC_FAILED("서류 탈락"), + PROCESSING("진행중"), } diff --git a/core/common/src/main/java/team/retum/common/enums/InterviewLocation.kt b/core/common/src/main/java/team/retum/common/enums/InterviewLocation.kt new file mode 100644 index 000000000..027f97f9d --- /dev/null +++ b/core/common/src/main/java/team/retum/common/enums/InterviewLocation.kt @@ -0,0 +1,8 @@ +package team.retum.common.enums + +enum class InterviewLocation { + DAEJEON, + SEOUL, + GYEONGGI, + OTHER, +} diff --git a/core/common/src/main/java/team/retum/common/enums/InterviewType.kt b/core/common/src/main/java/team/retum/common/enums/InterviewType.kt new file mode 100644 index 000000000..a127f5499 --- /dev/null +++ b/core/common/src/main/java/team/retum/common/enums/InterviewType.kt @@ -0,0 +1,7 @@ +package team.retum.common.enums + +enum class InterviewType { + INDIVIDUAL, + GROUP, + OTHER, +} diff --git a/core/common/src/main/java/team/retum/common/enums/ReviewProcess.kt b/core/common/src/main/java/team/retum/common/enums/ReviewProcess.kt index 70a6926c3..37a3ae14d 100644 --- a/core/common/src/main/java/team/retum/common/enums/ReviewProcess.kt +++ b/core/common/src/main/java/team/retum/common/enums/ReviewProcess.kt @@ -1,7 +1,9 @@ package team.retum.common.enums enum class ReviewProcess { - QUESTION, - TECH, - FINISH, + INTERVIEW_TYPE, + INTERVIEW_LOCATION, + TECH_STACK, + INTERVIEWER_COUNT, + SUMMARY, } diff --git a/core/common/src/main/java/team/retum/common/utils/ResourceKeys.kt b/core/common/src/main/java/team/retum/common/utils/ResourceKeys.kt index cd94f4d58..7e2e55485 100644 --- a/core/common/src/main/java/team/retum/common/utils/ResourceKeys.kt +++ b/core/common/src/main/java/team/retum/common/utils/ResourceKeys.kt @@ -22,4 +22,5 @@ object ResourceKeys { const val EMAIL = "@dsm.hs.kr" const val DATABASE_NAME = "jobis-database" const val CLASS_ID = "classId" + const val REVIEW_DATA = "reviewData" } diff --git a/core/data/src/main/java/team/retum/data/repository/review/ReviewRepository.kt b/core/data/src/main/java/team/retum/data/repository/review/ReviewRepository.kt index 18ea0aa64..17919615c 100644 --- a/core/data/src/main/java/team/retum/data/repository/review/ReviewRepository.kt +++ b/core/data/src/main/java/team/retum/data/repository/review/ReviewRepository.kt @@ -1,13 +1,32 @@ package team.retum.data.repository.review +import team.retum.common.enums.InterviewLocation +import team.retum.common.enums.InterviewType import team.retum.network.model.request.PostReviewRequest +import team.retum.network.model.response.FetchMyReviewResponse +import team.retum.network.model.response.FetchQuestionsResponse import team.retum.network.model.response.FetchReviewDetailResponse +import team.retum.network.model.response.FetchReviewsCountResponse import team.retum.network.model.response.FetchReviewsResponse interface ReviewRepository { suspend fun postReview(reviewRequest: PostReviewRequest) - suspend fun fetchReviews(companyId: Long): FetchReviewsResponse + suspend fun fetchReviews( + page: Int?, + location: InterviewLocation?, + interviewType: InterviewType?, + companyId: Long?, + keyword: String?, + year: Int?, + code: Long?, + ): FetchReviewsResponse suspend fun fetchReviewDetail(reviewId: String): FetchReviewDetailResponse + + suspend fun fetchQuestions(): FetchQuestionsResponse + + suspend fun fetchReviewsCount(): FetchReviewsCountResponse + + suspend fun fetchMyReviews(): FetchMyReviewResponse } diff --git a/core/data/src/main/java/team/retum/data/repository/review/ReviewRepositoryImpl.kt b/core/data/src/main/java/team/retum/data/repository/review/ReviewRepositoryImpl.kt index 192aabfe5..49a594d05 100644 --- a/core/data/src/main/java/team/retum/data/repository/review/ReviewRepositoryImpl.kt +++ b/core/data/src/main/java/team/retum/data/repository/review/ReviewRepositoryImpl.kt @@ -1,8 +1,13 @@ package team.retum.data.repository.review +import team.retum.common.enums.InterviewLocation +import team.retum.common.enums.InterviewType import team.retum.network.datasource.review.ReviewDataSource import team.retum.network.model.request.PostReviewRequest +import team.retum.network.model.response.FetchMyReviewResponse +import team.retum.network.model.response.FetchQuestionsResponse import team.retum.network.model.response.FetchReviewDetailResponse +import team.retum.network.model.response.FetchReviewsCountResponse import team.retum.network.model.response.FetchReviewsResponse import javax.inject.Inject @@ -12,9 +17,34 @@ class ReviewRepositoryImpl @Inject constructor( override suspend fun postReview(reviewRequest: PostReviewRequest) = reviewDataSource.postReview(reviewRequest) - override suspend fun fetchReviews(companyId: Long): FetchReviewsResponse = - reviewDataSource.fetchReviews(companyId) + override suspend fun fetchReviews( + page: Int?, + location: InterviewLocation?, + interviewType: InterviewType?, + companyId: Long?, + keyword: String?, + year: Int?, + code: Long?, + ): FetchReviewsResponse = + reviewDataSource.fetchReviews( + page = page, + location = location, + interviewType = interviewType, + keyword = keyword, + year = year, + companyId = companyId, + code = code, + ) override suspend fun fetchReviewDetail(reviewId: String): FetchReviewDetailResponse = reviewDataSource.fetchReviewDetail(reviewId) + + override suspend fun fetchQuestions(): FetchQuestionsResponse = + reviewDataSource.fetchQuestions() + + override suspend fun fetchReviewsCount(): FetchReviewsCountResponse = + reviewDataSource.fetchReviewsCount() + + override suspend fun fetchMyReviews(): FetchMyReviewResponse = + reviewDataSource.fetchMyReviews() } diff --git a/core/design-system/src/main/java/team/retum/jobisdesignsystemv2/foundation/JobisColor.kt b/core/design-system/src/main/java/team/retum/jobisdesignsystemv2/foundation/JobisColor.kt index d390e66ea..30336d91a 100644 --- a/core/design-system/src/main/java/team/retum/jobisdesignsystemv2/foundation/JobisColor.kt +++ b/core/design-system/src/main/java/team/retum/jobisdesignsystemv2/foundation/JobisColor.kt @@ -32,7 +32,7 @@ internal sealed class JobisColor( ) { data object Light : JobisColor( primary100 = Color(0xFFF3F3FB), - primary200 = Color(0xFF2F4DEF), + primary200 = Color(0xFF2F53FF), primary300 = Color(0xFF263EBF), primary400 = Color(0xFF132BAC), red100 = Color(0xFFFCE9E7), diff --git a/core/design-system/src/main/java/team/retum/jobisdesignsystemv2/foundation/JobisIcon.kt b/core/design-system/src/main/java/team/retum/jobisdesignsystemv2/foundation/JobisIcon.kt index 361a03761..854a66c2a 100644 --- a/core/design-system/src/main/java/team/retum/jobisdesignsystemv2/foundation/JobisIcon.kt +++ b/core/design-system/src/main/java/team/retum/jobisdesignsystemv2/foundation/JobisIcon.kt @@ -37,4 +37,5 @@ object JobisIcon { val Notification = R.drawable.ic_notification val Empty = R.drawable.ic_empty val Person = R.drawable.ic_person + val Asterisk = R.drawable.ic_asterisk } diff --git a/core/design-system/src/main/java/team/retum/jobisdesignsystemv2/review/ReviewContent.kt b/core/design-system/src/main/java/team/retum/jobisdesignsystemv2/review/ReviewContent.kt index 24663ba44..327d01ba2 100644 --- a/core/design-system/src/main/java/team/retum/jobisdesignsystemv2/review/ReviewContent.kt +++ b/core/design-system/src/main/java/team/retum/jobisdesignsystemv2/review/ReviewContent.kt @@ -19,12 +19,12 @@ import team.retum.jobisdesignsystemv2.text.JobisText @Composable fun ReviewContent( - onClick: (String, String) -> Unit, - reviewId: String, + onClick: (Long) -> Unit, + reviewId: Long, writer: String, year: String, ) { - JobisCard(onClick = { onClick(reviewId, writer) }) { + JobisCard(onClick = { onClick(reviewId) }) { Row( modifier = Modifier .fillMaxWidth() diff --git a/core/design-system/src/main/res/drawable/ic_asterisk.xml b/core/design-system/src/main/res/drawable/ic_asterisk.xml new file mode 100644 index 000000000..599f8b50a --- /dev/null +++ b/core/design-system/src/main/res/drawable/ic_asterisk.xml @@ -0,0 +1,9 @@ + + + diff --git a/core/design-system/src/main/res/drawable/ic_success.xml b/core/design-system/src/main/res/drawable/ic_success.xml new file mode 100644 index 000000000..d20756ec2 --- /dev/null +++ b/core/design-system/src/main/res/drawable/ic_success.xml @@ -0,0 +1,9 @@ + + + diff --git a/core/domain/src/main/java/team/retum/usecase/entity/FetchReviewDetailEntity.kt b/core/domain/src/main/java/team/retum/usecase/entity/FetchReviewDetailEntity.kt index 508ef65af..debd0b37a 100644 --- a/core/domain/src/main/java/team/retum/usecase/entity/FetchReviewDetailEntity.kt +++ b/core/domain/src/main/java/team/retum/usecase/entity/FetchReviewDetailEntity.kt @@ -1,23 +1,45 @@ package team.retum.usecase.entity +import team.retum.common.enums.InterviewLocation +import team.retum.common.enums.InterviewType import team.retum.network.model.response.FetchReviewDetailResponse data class FetchReviewDetailEntity( - val qnaResponses: List, + val reviewId: String, + val companyName: String, + val writer: String, + val major: String, + val type: InterviewType, + val location: InterviewLocation, + val interviewerCount: Int, + val year: Int, + val qnaResponse: List, + val question: String, + val answer: String, ) { - data class Detail( + data class QnAs( + val id: Int, val question: String, val answer: String, - val area: String, ) } internal fun FetchReviewDetailResponse.toEntity() = FetchReviewDetailEntity( - qnaResponses = this.qnaResponses.map { it.toEntity() }, + reviewId = this.reviewId, + companyName = this.companyName, + writer = this.writer, + major = this.major, + type = this.type, + location = this.location, + interviewerCount = this.interviewerCount, + year = this.year, + qnaResponse = this.qnaResponse.map { it.toEntity() }, + question = this.question, + answer = this.answer, ) -private fun FetchReviewDetailResponse.Detail.toEntity() = FetchReviewDetailEntity.Detail( +private fun FetchReviewDetailResponse.QnAs.toEntity() = FetchReviewDetailEntity.QnAs( + id = this.id, question = this.question, answer = this.answer, - area = this.area, ) diff --git a/core/domain/src/main/java/team/retum/usecase/entity/FetchReviewsEntity.kt b/core/domain/src/main/java/team/retum/usecase/entity/FetchReviewsEntity.kt index d9f49176b..b2c0a6559 100644 --- a/core/domain/src/main/java/team/retum/usecase/entity/FetchReviewsEntity.kt +++ b/core/domain/src/main/java/team/retum/usecase/entity/FetchReviewsEntity.kt @@ -1,6 +1,7 @@ package team.retum.usecase.entity import androidx.compose.runtime.Immutable +import team.retum.common.utils.ResourceKeys import team.retum.network.model.response.FetchReviewsResponse data class FetchReviewsEntity( @@ -8,10 +9,12 @@ data class FetchReviewsEntity( ) { @Immutable data class Review( - val reviewId: String, + val reviewId: Long, + val companyName: String, + val companyLogoUrl: String, val year: String, val writer: String, - val date: String, + val major: String, ) } @@ -21,7 +24,9 @@ internal fun FetchReviewsResponse.toEntity() = FetchReviewsEntity( private fun FetchReviewsResponse.Review.toEntity() = FetchReviewsEntity.Review( reviewId = this.reviewId, + companyName = this.companyName, + companyLogoUrl = ResourceKeys.IMAGE_URL + this.companyLogoUrl, + major = this.major, year = this.year.toString(), - writer = "${this.writer}님의 후기", - date = this.date, + writer = this.writer, ) diff --git a/core/domain/src/main/java/team/retum/usecase/entity/MyReviews.kt b/core/domain/src/main/java/team/retum/usecase/entity/MyReviews.kt new file mode 100644 index 000000000..33dabdf85 --- /dev/null +++ b/core/domain/src/main/java/team/retum/usecase/entity/MyReviews.kt @@ -0,0 +1,23 @@ +package team.retum.usecase.entity + +import team.retum.network.model.response.FetchMyReviewResponse +import javax.annotation.concurrent.Immutable + +data class MyReviewsEntity( + val reviews: List, +) { + @Immutable + data class MyReview( + val reviewId: Long, + val companyName: String, + ) +} + +internal fun FetchMyReviewResponse.toEntity() = MyReviewsEntity( + reviews = this.reviews.map { it.toEntity() }, +) + +private fun FetchMyReviewResponse.MyReview.toEntity() = MyReviewsEntity.MyReview( + reviewId = this.reviewId, + companyName = this.companyName, +) diff --git a/core/domain/src/main/java/team/retum/usecase/entity/PostReviewEntity.kt b/core/domain/src/main/java/team/retum/usecase/entity/PostReviewEntity.kt index 3e72be2b3..77eedd70c 100644 --- a/core/domain/src/main/java/team/retum/usecase/entity/PostReviewEntity.kt +++ b/core/domain/src/main/java/team/retum/usecase/entity/PostReviewEntity.kt @@ -1,28 +1,38 @@ package team.retum.usecase.entity -import androidx.compose.runtime.Immutable +import team.retum.common.enums.InterviewLocation +import team.retum.common.enums.InterviewType import team.retum.network.model.request.PostReviewRequest data class PostReviewEntity( + val interviewType: InterviewType, + val location: InterviewLocation, val companyId: Long, + val jobCode: Long, + val interviewerCount: Int, val qnaElements: List, + val question: String, + val answer: String, ) { - @Immutable data class PostReviewContentEntity( - val question: String, + val question: Long, val answer: String, - val codeId: Long, ) } fun PostReviewEntity.toPostReviewRequest() = PostReviewRequest( + interviewType = this.interviewType, + location = this.location, + jobCode = this.jobCode, + interviewerCount = this.interviewerCount, companyId = this.companyId, qnaElements = this.qnaElements.map { it.toEntity() }, + question = this.question, + answer = this.answer, ) private fun PostReviewEntity.PostReviewContentEntity.toEntity() = PostReviewRequest.PostReviewContentRequest( question = this.question, answer = this.answer, - codeId = this.codeId, ) diff --git a/core/domain/src/main/java/team/retum/usecase/entity/QuestionsEntity.kt b/core/domain/src/main/java/team/retum/usecase/entity/QuestionsEntity.kt new file mode 100644 index 000000000..670c65bfe --- /dev/null +++ b/core/domain/src/main/java/team/retum/usecase/entity/QuestionsEntity.kt @@ -0,0 +1,23 @@ +package team.retum.usecase.entity + +import androidx.compose.runtime.Immutable +import team.retum.network.model.response.FetchQuestionsResponse + +data class QuestionsEntity( + val questions: List, +) { + @Immutable + data class QuestionEntity( + val id: Long, + val question: String, + ) +} + +internal fun FetchQuestionsResponse.toEntity() = QuestionsEntity( // TODO : 이름 통일 + questions = this.questions.map { it.toEntity() }, +) + +private fun FetchQuestionsResponse.Question.toEntity() = QuestionsEntity.QuestionEntity( + id = this.id, + question = this.question, +) diff --git a/core/domain/src/main/java/team/retum/usecase/entity/ReviewsCountEntity.kt b/core/domain/src/main/java/team/retum/usecase/entity/ReviewsCountEntity.kt new file mode 100644 index 000000000..f0dbd6f69 --- /dev/null +++ b/core/domain/src/main/java/team/retum/usecase/entity/ReviewsCountEntity.kt @@ -0,0 +1,11 @@ +package team.retum.usecase.entity + +import team.retum.network.model.response.FetchReviewsCountResponse + +data class ReviewsCountEntity( + val totalPageCount: Long, +) + +internal fun FetchReviewsCountResponse.toEntity() = ReviewsCountEntity( + totalPageCount = totalPageCount, +) diff --git a/core/domain/src/main/java/team/retum/usecase/usecase/review/FetchMyReviewUseCase.kt b/core/domain/src/main/java/team/retum/usecase/usecase/review/FetchMyReviewUseCase.kt new file mode 100644 index 000000000..728fcca1d --- /dev/null +++ b/core/domain/src/main/java/team/retum/usecase/usecase/review/FetchMyReviewUseCase.kt @@ -0,0 +1,13 @@ +package team.retum.usecase.usecase.review + +import team.retum.data.repository.review.ReviewRepository +import team.retum.usecase.entity.toEntity +import javax.inject.Inject + +class FetchMyReviewUseCase @Inject constructor( + private val reviewRepository: ReviewRepository, +) { + suspend operator fun invoke() = runCatching { + reviewRepository.fetchMyReviews().toEntity() + } +} diff --git a/core/domain/src/main/java/team/retum/usecase/usecase/review/FetchQuestionsUseCase.kt b/core/domain/src/main/java/team/retum/usecase/usecase/review/FetchQuestionsUseCase.kt new file mode 100644 index 000000000..0e798cd63 --- /dev/null +++ b/core/domain/src/main/java/team/retum/usecase/usecase/review/FetchQuestionsUseCase.kt @@ -0,0 +1,13 @@ +package team.retum.usecase.usecase.review + +import team.retum.data.repository.review.ReviewRepository +import team.retum.usecase.entity.toEntity +import javax.inject.Inject + +class FetchQuestionsUseCase @Inject constructor( + private val reviewRepository: ReviewRepository, +) { + suspend operator fun invoke() = runCatching { + reviewRepository.fetchQuestions().toEntity() + } +} diff --git a/core/domain/src/main/java/team/retum/usecase/usecase/review/FetchReviewsCountUseCase.kt b/core/domain/src/main/java/team/retum/usecase/usecase/review/FetchReviewsCountUseCase.kt new file mode 100644 index 000000000..68bb78a28 --- /dev/null +++ b/core/domain/src/main/java/team/retum/usecase/usecase/review/FetchReviewsCountUseCase.kt @@ -0,0 +1,13 @@ +package team.retum.usecase.usecase.review + +import team.retum.data.repository.review.ReviewRepository +import team.retum.usecase.entity.toEntity +import javax.inject.Inject + +class FetchReviewsCountUseCase @Inject constructor( + private val reviewRepository: ReviewRepository, +) { + suspend operator fun invoke() = runCatching { + reviewRepository.fetchReviewsCount().toEntity() + } +} diff --git a/core/domain/src/main/java/team/retum/usecase/usecase/review/FetchReviewsUseCase.kt b/core/domain/src/main/java/team/retum/usecase/usecase/review/FetchReviewsUseCase.kt index d061a2229..69d7d0359 100644 --- a/core/domain/src/main/java/team/retum/usecase/usecase/review/FetchReviewsUseCase.kt +++ b/core/domain/src/main/java/team/retum/usecase/usecase/review/FetchReviewsUseCase.kt @@ -1,5 +1,7 @@ package team.retum.usecase.usecase.review +import team.retum.common.enums.InterviewLocation +import team.retum.common.enums.InterviewType import team.retum.data.repository.review.ReviewRepository import team.retum.usecase.entity.toEntity import javax.inject.Inject @@ -7,7 +9,7 @@ import javax.inject.Inject class FetchReviewsUseCase @Inject constructor( private val reviewRepository: ReviewRepository, ) { - suspend operator fun invoke(companyId: Long) = runCatching { - reviewRepository.fetchReviews(companyId).toEntity() + suspend operator fun invoke(page: Int?, location: InterviewLocation?, interviewType: InterviewType?, keyword: String?, year: Int?, companyId: Long?, code: Long?) = runCatching { + reviewRepository.fetchReviews(page, location, interviewType, companyId, keyword, year, code).toEntity() } } diff --git a/core/network/src/main/java/team/retum/network/api/ReviewApi.kt b/core/network/src/main/java/team/retum/network/api/ReviewApi.kt index 6a11c6def..a748d42ac 100644 --- a/core/network/src/main/java/team/retum/network/api/ReviewApi.kt +++ b/core/network/src/main/java/team/retum/network/api/ReviewApi.kt @@ -4,9 +4,15 @@ import retrofit2.http.Body import retrofit2.http.GET import retrofit2.http.POST import retrofit2.http.Path +import retrofit2.http.Query +import team.retum.common.enums.InterviewLocation +import team.retum.common.enums.InterviewType import team.retum.network.di.RequestUrls import team.retum.network.model.request.PostReviewRequest +import team.retum.network.model.response.FetchMyReviewResponse +import team.retum.network.model.response.FetchQuestionsResponse import team.retum.network.model.response.FetchReviewDetailResponse +import team.retum.network.model.response.FetchReviewsCountResponse import team.retum.network.model.response.FetchReviewsResponse interface ReviewApi { @@ -17,11 +23,26 @@ interface ReviewApi { @GET(RequestUrls.Reviews.reviews) suspend fun fetchReviews( - @Path(RequestUrls.PATH.companyId) companyId: Long, + @Query("page") page: Int?, + @Query("location") location: InterviewLocation?, + @Query("type") interviewType: InterviewType?, + @Query("company_id") companyId: Long?, + @Query("keyword") keyword: String?, + @Query("year") year: Int?, + @Query("code") code: Long?, ): FetchReviewsResponse @GET(RequestUrls.Reviews.details) suspend fun fetchReviewDetail( @Path(RequestUrls.PATH.reviewId) reviewId: String, ): FetchReviewDetailResponse + + @GET(RequestUrls.Reviews.questions) + suspend fun fetchQuestions(): FetchQuestionsResponse + + @GET(RequestUrls.Reviews.count) + suspend fun fetchReviewsCount(): FetchReviewsCountResponse + + @GET(RequestUrls.Reviews.my) + suspend fun fetchMyReviews(): FetchMyReviewResponse } diff --git a/core/network/src/main/java/team/retum/network/datasource/review/ReviewDataSource.kt b/core/network/src/main/java/team/retum/network/datasource/review/ReviewDataSource.kt index 0ad978904..ed858f09e 100644 --- a/core/network/src/main/java/team/retum/network/datasource/review/ReviewDataSource.kt +++ b/core/network/src/main/java/team/retum/network/datasource/review/ReviewDataSource.kt @@ -1,13 +1,32 @@ package team.retum.network.datasource.review +import team.retum.common.enums.InterviewLocation +import team.retum.common.enums.InterviewType import team.retum.network.model.request.PostReviewRequest +import team.retum.network.model.response.FetchMyReviewResponse +import team.retum.network.model.response.FetchQuestionsResponse import team.retum.network.model.response.FetchReviewDetailResponse +import team.retum.network.model.response.FetchReviewsCountResponse import team.retum.network.model.response.FetchReviewsResponse interface ReviewDataSource { suspend fun postReview(postReviewRequest: PostReviewRequest) - suspend fun fetchReviews(companyId: Long): FetchReviewsResponse + suspend fun fetchReviews( + page: Int?, + location: InterviewLocation?, + interviewType: InterviewType?, + companyId: Long?, + keyword: String?, + year: Int?, + code: Long?, + ): FetchReviewsResponse suspend fun fetchReviewDetail(reviewId: String): FetchReviewDetailResponse + + suspend fun fetchQuestions(): FetchQuestionsResponse + + suspend fun fetchReviewsCount(): FetchReviewsCountResponse + + suspend fun fetchMyReviews(): FetchMyReviewResponse } diff --git a/core/network/src/main/java/team/retum/network/datasource/review/ReviewDataSourceImpl.kt b/core/network/src/main/java/team/retum/network/datasource/review/ReviewDataSourceImpl.kt index bf37d4fae..c41e8de11 100644 --- a/core/network/src/main/java/team/retum/network/datasource/review/ReviewDataSourceImpl.kt +++ b/core/network/src/main/java/team/retum/network/datasource/review/ReviewDataSourceImpl.kt @@ -1,8 +1,13 @@ package team.retum.network.datasource.review +import team.retum.common.enums.InterviewLocation +import team.retum.common.enums.InterviewType import team.retum.network.api.ReviewApi import team.retum.network.model.request.PostReviewRequest +import team.retum.network.model.response.FetchMyReviewResponse +import team.retum.network.model.response.FetchQuestionsResponse import team.retum.network.model.response.FetchReviewDetailResponse +import team.retum.network.model.response.FetchReviewsCountResponse import team.retum.network.model.response.FetchReviewsResponse import team.retum.network.util.RequestHandler import javax.inject.Inject @@ -13,9 +18,35 @@ class ReviewDataSourceImpl @Inject constructor( override suspend fun postReview(postReviewRequest: PostReviewRequest) = RequestHandler().request { reviewApi.postReview(postReviewRequest) } - override suspend fun fetchReviews(companyId: Long): FetchReviewsResponse = - RequestHandler().request { reviewApi.fetchReviews(companyId) } + override suspend fun fetchReviews( + page: Int?, + location: InterviewLocation?, + interviewType: InterviewType?, + companyId: Long?, + keyword: String?, + year: Int?, + code: Long?, + ): FetchReviewsResponse = RequestHandler().request { + reviewApi.fetchReviews( + page = page, + location = location, + interviewType = interviewType, + companyId = companyId, + keyword = keyword, + year = year, + code = code, + ) + } override suspend fun fetchReviewDetail(reviewId: String): FetchReviewDetailResponse = RequestHandler().request { reviewApi.fetchReviewDetail(reviewId) } + + override suspend fun fetchQuestions(): FetchQuestionsResponse = + RequestHandler().request { reviewApi.fetchQuestions() } + + override suspend fun fetchReviewsCount(): FetchReviewsCountResponse = + RequestHandler().request { reviewApi.fetchReviewsCount() } + + override suspend fun fetchMyReviews(): FetchMyReviewResponse = + RequestHandler().request { reviewApi.fetchMyReviews() } } diff --git a/core/network/src/main/java/team/retum/network/di/RequestUrls.kt b/core/network/src/main/java/team/retum/network/di/RequestUrls.kt index a78255c70..5633b9375 100644 --- a/core/network/src/main/java/team/retum/network/di/RequestUrls.kt +++ b/core/network/src/main/java/team/retum/network/di/RequestUrls.kt @@ -82,9 +82,12 @@ internal object RequestUrls { data object Reviews { private const val path = "/reviews" - const val details = "$path/details/{${PATH.reviewId}}" - const val reviews = "$path/{${PATH.companyId}}" + const val details = "$path/{${PATH.reviewId}}" + const val reviews = path + const val questions = "$path/questions" + const val count = "$path/count" const val post = path + const val my = "$path/my" } data object Bookmarks { diff --git a/core/network/src/main/java/team/retum/network/model/request/PostReviewRequest.kt b/core/network/src/main/java/team/retum/network/model/request/PostReviewRequest.kt index b05362d95..e5b70b428 100644 --- a/core/network/src/main/java/team/retum/network/model/request/PostReviewRequest.kt +++ b/core/network/src/main/java/team/retum/network/model/request/PostReviewRequest.kt @@ -2,16 +2,23 @@ package team.retum.network.model.request import com.squareup.moshi.Json import com.squareup.moshi.JsonClass +import team.retum.common.enums.InterviewLocation +import team.retum.common.enums.InterviewType @JsonClass(generateAdapter = true) data class PostReviewRequest( + @Json(name = "interview_type") val interviewType: InterviewType, + @Json(name = "location") val location: InterviewLocation, @Json(name = "company_id") val companyId: Long, - @Json(name = "qna_elements") val qnaElements: List, + @Json(name = "job_code") val jobCode: Long, + @Json(name = "interviewer_count") val interviewerCount: Int, + @Json(name = "qnas") val qnaElements: List, + @Json(name = "question") val question: String, + @Json(name = "answer") val answer: String, ) { @JsonClass(generateAdapter = true) data class PostReviewContentRequest( - @Json(name = "question") val question: String, + @Json(name = "question_id") val question: Long, @Json(name = "answer") val answer: String, - @Json(name = "code_id") val codeId: Long, ) } diff --git a/core/network/src/main/java/team/retum/network/model/response/FetchMyReviewResponse.kt b/core/network/src/main/java/team/retum/network/model/response/FetchMyReviewResponse.kt new file mode 100644 index 000000000..42191df19 --- /dev/null +++ b/core/network/src/main/java/team/retum/network/model/response/FetchMyReviewResponse.kt @@ -0,0 +1,15 @@ +package team.retum.network.model.response + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class FetchMyReviewResponse( + @Json(name = "reviews") val reviews: List, +) { + @JsonClass(generateAdapter = true) + data class MyReview( + @Json(name = "review_id") val reviewId: Long, + @Json(name = "company_name") val companyName: String, + ) +} diff --git a/core/network/src/main/java/team/retum/network/model/response/FetchQuestionsResponse.kt b/core/network/src/main/java/team/retum/network/model/response/FetchQuestionsResponse.kt new file mode 100644 index 000000000..2ec6a3aa3 --- /dev/null +++ b/core/network/src/main/java/team/retum/network/model/response/FetchQuestionsResponse.kt @@ -0,0 +1,15 @@ +package team.retum.network.model.response + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class FetchQuestionsResponse( + @Json(name = "questions") val questions: List, +) { + @JsonClass(generateAdapter = true) + data class Question( + @Json(name = "id") val id: Long, + @Json(name = "question") val question: String, + ) +} diff --git a/core/network/src/main/java/team/retum/network/model/response/FetchReviewDetailResponse.kt b/core/network/src/main/java/team/retum/network/model/response/FetchReviewDetailResponse.kt index 01ca3967d..9f8e099ea 100644 --- a/core/network/src/main/java/team/retum/network/model/response/FetchReviewDetailResponse.kt +++ b/core/network/src/main/java/team/retum/network/model/response/FetchReviewDetailResponse.kt @@ -2,15 +2,27 @@ package team.retum.network.model.response import com.squareup.moshi.Json import com.squareup.moshi.JsonClass +import team.retum.common.enums.InterviewLocation +import team.retum.common.enums.InterviewType @JsonClass(generateAdapter = true) data class FetchReviewDetailResponse( - @Json(name = "qna_responses") val qnaResponses: List, + @Json(name = "review_id") val reviewId: String, + @Json(name = "company_name") val companyName: String, + @Json(name = "writer") val writer: String, + @Json(name = "major") val major: String, + @Json(name = "type") val type: InterviewType, + @Json(name = "location") val location: InterviewLocation, + @Json(name = "interviewer_count") val interviewerCount: Int, + @Json(name = "year") val year: Int, + @Json(name = "qn_as") val qnaResponse: List, + @Json(name = "question") val question: String, + @Json(name = "answer") val answer: String, ) { @JsonClass(generateAdapter = true) - data class Detail( + data class QnAs( + @Json(name = "id") val id: Int, @Json(name = "question") val question: String, @Json(name = "answer") val answer: String, - @Json(name = "area") val area: String, ) } diff --git a/core/network/src/main/java/team/retum/network/model/response/FetchReviewsCountResponse.kt b/core/network/src/main/java/team/retum/network/model/response/FetchReviewsCountResponse.kt new file mode 100644 index 000000000..59c125d14 --- /dev/null +++ b/core/network/src/main/java/team/retum/network/model/response/FetchReviewsCountResponse.kt @@ -0,0 +1,9 @@ +package team.retum.network.model.response + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class FetchReviewsCountResponse( + @Json(name = "total_page_count") val totalPageCount: Long, +) diff --git a/core/network/src/main/java/team/retum/network/model/response/FetchReviewsResponse.kt b/core/network/src/main/java/team/retum/network/model/response/FetchReviewsResponse.kt index 1879b69d2..f15083179 100644 --- a/core/network/src/main/java/team/retum/network/model/response/FetchReviewsResponse.kt +++ b/core/network/src/main/java/team/retum/network/model/response/FetchReviewsResponse.kt @@ -9,9 +9,11 @@ data class FetchReviewsResponse( ) { @JsonClass(generateAdapter = true) data class Review( - @Json(name = "review_id") val reviewId: String, + @Json(name = "review_id") val reviewId: Long, + @Json(name = "company_name") val companyName: String, + @Json(name = "company_logo_url") val companyLogoUrl: String, @Json(name = "year") val year: Int, @Json(name = "writer") val writer: String, - @Json(name = "date") val date: String, + @Json(name = "major") val major: String, ) } diff --git a/feature/bookmark/src/main/java/team/retum/bookmark/ui/BookmarkScreen.kt b/feature/bookmark/src/main/java/team/retum/bookmark/ui/BookmarkScreen.kt index 87d9a543d..893ba57b0 100644 --- a/feature/bookmark/src/main/java/team/retum/bookmark/ui/BookmarkScreen.kt +++ b/feature/bookmark/src/main/java/team/retum/bookmark/ui/BookmarkScreen.kt @@ -132,7 +132,7 @@ private fun EmptyBookmarkContent(onRecruitmentsClick: () -> Unit) { ) { Image( modifier = Modifier.size(128.dp), - painter = painterResource(id = R.drawable.ic_empty_bookmark), + painter = painterResource(id = team.retum.design_system.R.drawable.ic_empty), contentDescription = "empty bookmark", ) Spacer(modifier = Modifier.height(16.dp)) diff --git a/feature/bookmark/src/main/res/drawable/ic_empty_bookmark.png b/feature/bookmark/src/main/res/drawable/ic_empty_bookmark.png deleted file mode 100644 index a4a7cf36b..000000000 Binary files a/feature/bookmark/src/main/res/drawable/ic_empty_bookmark.png and /dev/null differ diff --git a/feature/company/src/main/java/team/retum/company/navigation/CompanyDetailsNavigation.kt b/feature/company/src/main/java/team/retum/company/navigation/CompanyDetailsNavigation.kt index d414c6aa7..a73bdc620 100644 --- a/feature/company/src/main/java/team/retum/company/navigation/CompanyDetailsNavigation.kt +++ b/feature/company/src/main/java/team/retum/company/navigation/CompanyDetailsNavigation.kt @@ -12,7 +12,7 @@ const val NAVIGATION_COMPANY_DETAILS = "companyDetails" fun NavGraphBuilder.companyDetails( onBackPressed: () -> Unit, - navigateToReviewDetails: (String, String) -> Unit, + navigateToReviewDetails: (Long) -> Unit, navigateToReviews: (Long, String) -> Unit, navigateToRecruitmentDetails: (Long, Boolean) -> Unit, ) { diff --git a/feature/company/src/main/java/team/retum/company/ui/CompanyDetailsScreen.kt b/feature/company/src/main/java/team/retum/company/ui/CompanyDetailsScreen.kt index ea5c986e1..b0e9e0a33 100644 --- a/feature/company/src/main/java/team/retum/company/ui/CompanyDetailsScreen.kt +++ b/feature/company/src/main/java/team/retum/company/ui/CompanyDetailsScreen.kt @@ -51,7 +51,7 @@ import team.retum.usecase.entity.FetchReviewsEntity internal fun CompanyDetails( companyId: Long, onBackPressed: () -> Unit, - navigateToReviewDetails: (String, String) -> Unit, + navigateToReviewDetails: (Long) -> Unit, navigateToReviews: (Long, String) -> Unit, navigateToRecruitmentDetails: (Long, Boolean) -> Unit, isMovedRecruitmentDetails: Boolean, @@ -115,7 +115,7 @@ internal fun CompanyDetails( private fun CompanyDetailsScreen( companyId: Long, onBackPressed: () -> Unit, - navigateToReviewDetails: (String, String) -> Unit, + navigateToReviewDetails: (Long) -> Unit, navigateToReviews: (Long, String) -> Unit, onMoveToRecruitmentButtonClick: () -> Unit, isMovedRecruitmentDetails: Boolean, @@ -253,7 +253,7 @@ private fun CompanyInformation( @Composable private fun Reviews( reviews: ImmutableList, - navigateToReviewDetails: (String, String) -> Unit, + navigateToReviewDetails: (Long) -> Unit, showMoreReviews: Boolean, onShowMoreReviewClick: () -> Unit, ) { diff --git a/feature/company/src/main/java/team/retum/company/viewmodel/CompanyDetailsViewModel.kt b/feature/company/src/main/java/team/retum/company/viewmodel/CompanyDetailsViewModel.kt index 4ed769788..76d8c1b05 100644 --- a/feature/company/src/main/java/team/retum/company/viewmodel/CompanyDetailsViewModel.kt +++ b/feature/company/src/main/java/team/retum/company/viewmodel/CompanyDetailsViewModel.kt @@ -42,7 +42,15 @@ internal class CompanyDetailsViewModel @Inject constructor( internal fun fetchReviews() { viewModelScope.launch(Dispatchers.IO) { - fetchReviewsUseCase(companyId = state.value.companyId).onSuccess { + fetchReviewsUseCase( + page = null, + location = null, + interviewType = null, + companyId = state.value.companyId, + keyword = null, + year = null, + code = null, + ).onSuccess { val reviews = it.reviews setState { state.value.copy( diff --git a/feature/home/src/main/java/team/retum/home/ui/ApplyCompanyItem.kt b/feature/home/src/main/java/team/retum/home/ui/ApplyCompanyItem.kt index 1d3b435b9..5e2390a65 100644 --- a/feature/home/src/main/java/team/retum/home/ui/ApplyCompanyItem.kt +++ b/feature/home/src/main/java/team/retum/home/ui/ApplyCompanyItem.kt @@ -48,6 +48,7 @@ internal fun ApplyCompanyItem( val color = when (applicationStatus) { ApplyStatus.FAILED, ApplyStatus.REJECTED -> JobisTheme.colors.error ApplyStatus.REQUESTED, ApplyStatus.APPROVED -> JobisTheme.colors.tertiary + ApplyStatus.SEND, ApplyStatus.PROCESSING -> JobisTheme.colors.secondary ApplyStatus.FIELD_TRAIN, ApplyStatus.ACCEPTANCE, ApplyStatus.PASS -> JobisTheme.colors.outlineVariant else -> JobisTheme.colors.onPrimary } diff --git a/feature/home/src/main/java/team/retum/home/ui/HomeScreen.kt b/feature/home/src/main/java/team/retum/home/ui/HomeScreen.kt index ad38a1fe6..cfa3b0bdf 100644 --- a/feature/home/src/main/java/team/retum/home/ui/HomeScreen.kt +++ b/feature/home/src/main/java/team/retum/home/ui/HomeScreen.kt @@ -201,6 +201,7 @@ private fun HomeScreen( onCompaniesClick = onCompaniesClick, onWinterInternClick = onWinterInternClick, ) + // TODO :: 지원 했을 때 홈 진입 시 ui에 바로 반영 ApplyStatus( modifier = Modifier.padding( vertical = 12.dp, diff --git a/feature/mypage/src/main/java/team/retum/jobis/navigation/MyPageNavigation.kt b/feature/mypage/src/main/java/team/retum/jobis/navigation/MyPageNavigation.kt index 44eb27503..ecad1919e 100644 --- a/feature/mypage/src/main/java/team/retum/jobis/navigation/MyPageNavigation.kt +++ b/feature/mypage/src/main/java/team/retum/jobis/navigation/MyPageNavigation.kt @@ -13,7 +13,7 @@ fun NavGraphBuilder.myPage( onReportBugClick: () -> Unit, onNoticeClick: () -> Unit, navigateToLanding: () -> Unit, - onPostReviewClick: (Long) -> Unit, + onPostReviewClick: (String, Long) -> Unit, onNotificationSettingClick: () -> Unit, ) { composable(NAVIGATION_MY_PAGE) { diff --git a/feature/mypage/src/main/java/team/retum/jobis/ui/MyPageScreen.kt b/feature/mypage/src/main/java/team/retum/jobis/ui/MyPageScreen.kt index 244d4b235..1299c8891 100644 --- a/feature/mypage/src/main/java/team/retum/jobis/ui/MyPageScreen.kt +++ b/feature/mypage/src/main/java/team/retum/jobis/ui/MyPageScreen.kt @@ -57,7 +57,7 @@ internal fun MyPage( onNoticeClick: () -> Unit, navigateToLanding: () -> Unit, myPageViewModel: MyPageViewModel = hiltViewModel(), - onPostReviewClick: (Long) -> Unit, + onPostReviewClick: (String, Long) -> Unit, onNotificationSettingClick: () -> Unit, ) { val state by myPageViewModel.state.collectAsStateWithLifecycle() @@ -147,7 +147,7 @@ private fun MyPageScreen( setShowWithdrawalModal: (Boolean) -> Unit, onSignOutClick: () -> Unit, onWithdrawalClick: () -> Unit, - onPostReviewClick: (Long) -> Unit, + onPostReviewClick: (String, Long) -> Unit, onNotificationSettingClick: () -> Unit, showUpdateLaterToast: () -> Unit, showGallery: () -> Unit, @@ -196,7 +196,7 @@ private fun MyPageScreen( state.reviewableCompany?.run { WriteInterviewReview( companyName = state.reviewableCompany.name, - onClick = { onPostReviewClick(state.reviewableCompany.id) }, + onClick = { onPostReviewClick(state.reviewableCompany.name, state.reviewableCompany.id) }, ) } ContentListItem( diff --git a/feature/post-review/.gitignore b/feature/post-review/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/feature/post-review/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/feature/post-review/build.gradle.kts b/feature/post-review/build.gradle.kts new file mode 100644 index 000000000..e6e89bd9b --- /dev/null +++ b/feature/post-review/build.gradle.kts @@ -0,0 +1,25 @@ +@Suppress("DSL_SCOPE_VIOLATION") // TODO: Remove once KTIJ-19369 is fixed +plugins { + id(libs.plugins.android.library.get().pluginId) + id(libs.plugins.kotlin.android.get().pluginId) + id(libs.plugins.kotlin.ksp.get().pluginId) + id(libs.plugins.hilt.android.get().pluginId) + id(libs.plugins.ktlint.gradle.get().pluginId) + id(libs.plugins.kotlinx.serialization.get().pluginId) +} + +apply() +apply() + +android { + namespace = "team.retum.post.review" + + kotlinOptions { + jvmTarget = ProjectProperties.JVM_TARGET + } +} + +dependencies { + implementation(libs.kotlinx.collections.immutable) + implementation(libs.kotlinx.serialization.json) +} diff --git a/feature/post-review/src/main/java/team/retum/post/review/model/PostReviewData.kt b/feature/post-review/src/main/java/team/retum/post/review/model/PostReviewData.kt new file mode 100644 index 000000000..21f3cc726 --- /dev/null +++ b/feature/post-review/src/main/java/team/retum/post/review/model/PostReviewData.kt @@ -0,0 +1,44 @@ +package team.retum.post.review.model + +import kotlinx.serialization.Serializable +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import team.retum.common.enums.InterviewLocation +import team.retum.common.enums.InterviewType +import team.retum.usecase.entity.PostReviewEntity +import java.net.URLDecoder +import java.net.URLEncoder + +@Serializable +data class PostReviewData( + val interviewType: InterviewType = InterviewType.INDIVIDUAL, + val location: InterviewLocation = InterviewLocation.GYEONGGI, + val companyId: Long = 0, + val jobCode: Long = 0, + val interviewerCount: Int = 0, + val qnaElements: List = emptyList(), + val question: String = "", + val answer: String = "", +) { + @Serializable + data class PostReviewContent( + val question: Long = 0, + val answer: String = "", + ) +} + +internal fun PostReviewData.toJsonString(): String { + val json = Json.encodeToString(this) + return URLEncoder.encode(json, "UTF-8") +} + +internal fun String.toReviewData(): PostReviewData { + val decoded = URLDecoder.decode(this, "UTF-8") + return Json.decodeFromString(decoded) +} + +internal fun PostReviewData.PostReviewContent.toEntity(): PostReviewEntity.PostReviewContentEntity = + PostReviewEntity.PostReviewContentEntity( + question = this.question, + answer = this.answer, + ) diff --git a/feature/post-review/src/main/java/team/retum/post/review/navigation/PostExpectReviewNavigation.kt b/feature/post-review/src/main/java/team/retum/post/review/navigation/PostExpectReviewNavigation.kt new file mode 100644 index 000000000..7d4f01a2f --- /dev/null +++ b/feature/post-review/src/main/java/team/retum/post/review/navigation/PostExpectReviewNavigation.kt @@ -0,0 +1,33 @@ +package team.retum.post.review.navigation + +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavType +import androidx.navigation.compose.composable +import androidx.navigation.navArgument +import team.retum.common.utils.ResourceKeys +import team.retum.post.review.model.PostReviewData +import team.retum.post.review.model.toJsonString +import team.retum.post.review.ui.PostExpectReview + +const val NAVIGATION_POST_EXPECT_REVIEW = "postExpectReview" + +fun NavGraphBuilder.postExpectReview( + onBackPressed: () -> Unit, + onPostReviewCompleteClick: () -> Unit, +) { + composable( + route = "$NAVIGATION_POST_EXPECT_REVIEW/{${ResourceKeys.REVIEW_DATA}}", + arguments = listOf(navArgument(ResourceKeys.REVIEW_DATA) { NavType.StringType }), + ) { + PostExpectReview( + reviewData = it.getReviewData(), + onBackPressed = onBackPressed, + onPostReviewCompleteClick = onPostReviewCompleteClick, + ) + } +} + +fun NavController.navigateToPostExpectReview(reviewData: PostReviewData) { + navigate("$NAVIGATION_POST_EXPECT_REVIEW/${reviewData.toJsonString()}") +} diff --git a/feature/post-review/src/main/java/team/retum/post/review/navigation/PostNextReviewNavigation.kt b/feature/post-review/src/main/java/team/retum/post/review/navigation/PostNextReviewNavigation.kt new file mode 100644 index 000000000..661fa61c7 --- /dev/null +++ b/feature/post-review/src/main/java/team/retum/post/review/navigation/PostNextReviewNavigation.kt @@ -0,0 +1,40 @@ +package team.retum.post.review.navigation + +import androidx.navigation.NavBackStackEntry +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavType +import androidx.navigation.compose.composable +import androidx.navigation.navArgument +import team.retum.common.utils.ResourceKeys +import team.retum.post.review.model.PostReviewData +import team.retum.post.review.model.toJsonString +import team.retum.post.review.model.toReviewData +import team.retum.post.review.ui.PostNextReview + +const val NAVIGATION_POST_NEXT_REVIEW = "postNextReview" + +fun NavGraphBuilder.postNextReview( + onBackPressed: () -> Unit, + navigateToPostExpectReview: (PostReviewData) -> Unit, +) { + composable( + route = "$NAVIGATION_POST_NEXT_REVIEW/{${ResourceKeys.REVIEW_DATA}}", + arguments = listOf(navArgument(ResourceKeys.REVIEW_DATA) { NavType.StringType }), + ) { + PostNextReview( + reviewData = it.getReviewData(), + onBackPressed = onBackPressed, + navigateToPostExpectReview = navigateToPostExpectReview, + ) + } +} + +fun NavController.navigateToPostNextReview(reviewData: PostReviewData) { + navigate("$NAVIGATION_POST_NEXT_REVIEW/${reviewData.toJsonString()}") +} + +internal fun NavBackStackEntry.getReviewData(): PostReviewData { + val reviewData = arguments?.getString(ResourceKeys.REVIEW_DATA) ?: throw NullPointerException() + return reviewData.toReviewData() +} diff --git a/feature/post-review/src/main/java/team/retum/post/review/navigation/PostReviewCompleteNavigation.kt b/feature/post-review/src/main/java/team/retum/post/review/navigation/PostReviewCompleteNavigation.kt new file mode 100644 index 000000000..2757bd366 --- /dev/null +++ b/feature/post-review/src/main/java/team/retum/post/review/navigation/PostReviewCompleteNavigation.kt @@ -0,0 +1,26 @@ +package team.retum.post.review.navigation + +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.compose.composable +import team.retum.post.review.ui.PostReviewComplete + +const val NAVIGATION_POST_REVIEW_COMPLETE = "postReviewComplete" + +fun NavGraphBuilder.postReviewComplete( + onBackPressed: () -> Unit, + navigateToPostReview: (String, Long) -> Unit, +) { + composable( + route = NAVIGATION_POST_REVIEW_COMPLETE, + ) { + PostReviewComplete( + onBackPressed = onBackPressed, + navigateToPostReview = navigateToPostReview, + ) + } +} + +fun NavController.navigateToPostReviewComplete() { + navigate(NAVIGATION_POST_REVIEW_COMPLETE) +} diff --git a/feature/post-review/src/main/java/team/retum/post/review/navigation/PostReviewNavigation.kt b/feature/post-review/src/main/java/team/retum/post/review/navigation/PostReviewNavigation.kt new file mode 100644 index 000000000..2cf88b24b --- /dev/null +++ b/feature/post-review/src/main/java/team/retum/post/review/navigation/PostReviewNavigation.kt @@ -0,0 +1,40 @@ +package team.retum.post.review.navigation + +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavType +import androidx.navigation.compose.composable +import androidx.navigation.navArgument +import team.retum.common.utils.ResourceKeys +import team.retum.post.review.model.PostReviewData +import team.retum.post.review.ui.PostReview +import kotlin.text.toLongOrNull + +const val NAVIGATION_POST_REVIEW = "postReview" + +fun NavGraphBuilder.postReview( + onBackPressed: () -> Unit, + navigateToPostNextReview: (PostReviewData) -> Unit, +) { + composable( + route = "$NAVIGATION_POST_REVIEW/{${ResourceKeys.COMPANY_NAME}}/{${ResourceKeys.COMPANY_ID}}", + arguments = listOf( + navArgument(ResourceKeys.COMPANY_NAME) { NavType.StringType }, + navArgument(ResourceKeys.COMPANY_ID) { NavType.StringType }, + ), + ) { + PostReview( + onBackPressed = onBackPressed, + navigateToPostNextReview = navigateToPostNextReview, + companyName = it.arguments?.getString(ResourceKeys.COMPANY_NAME) ?: "", + companyId = it.arguments?.getString(ResourceKeys.COMPANY_ID)?.toLongOrNull() ?: 0L, + ) + } +} + +fun NavController.navigateToPostReview(companyName: String, companyId: Long) { + navigate("$NAVIGATION_POST_REVIEW/$companyName/$companyId") { + popUpTo(NAVIGATION_POST_NEXT_REVIEW) { inclusive = false } + popUpTo(NAVIGATION_POST_EXPECT_REVIEW) { inclusive = false } + } +} diff --git a/feature/post-review/src/main/java/team/retum/post/review/ui/PostExpectReviewScreen.kt b/feature/post-review/src/main/java/team/retum/post/review/ui/PostExpectReviewScreen.kt new file mode 100644 index 000000000..1d33273df --- /dev/null +++ b/feature/post-review/src/main/java/team/retum/post/review/ui/PostExpectReviewScreen.kt @@ -0,0 +1,155 @@ +package team.retum.post.review.ui + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import team.retum.jobisdesignsystemv2.appbar.JobisSmallTopAppBar +import team.retum.jobisdesignsystemv2.button.ButtonColor +import team.retum.jobisdesignsystemv2.button.JobisButton +import team.retum.jobisdesignsystemv2.foundation.JobisTheme +import team.retum.jobisdesignsystemv2.foundation.JobisTypography +import team.retum.jobisdesignsystemv2.text.JobisText +import team.retum.jobisdesignsystemv2.textfield.JobisTextField +import team.retum.post.review.R +import team.retum.post.review.model.PostReviewData +import team.retum.post.review.viewmodel.PostExpectReviewSideEffect +import team.retum.post.review.viewmodel.PostExpectReviewState +import team.retum.post.review.viewmodel.PostExpectReviewViewModel +import team.retum.post.review.viewmodel.PostReviewSideEffect +import team.retum.post.review.viewmodel.PostReviewViewModel + +@Composable +internal fun PostExpectReview( + reviewData: PostReviewData, + onBackPressed: () -> Unit, + onPostReviewCompleteClick: () -> Unit, + postExpectReviewViewModel: PostExpectReviewViewModel = hiltViewModel(), +) { + val postReviewViewModel: PostReviewViewModel = hiltViewModel() + val state by postExpectReviewViewModel.state.collectAsState() + + LaunchedEffect(Unit) { + postExpectReviewViewModel.sideEffect.collect { + when (it) { + is PostExpectReviewSideEffect.PostReview -> { + postReviewViewModel.postReview( + reviewData = reviewData.copy( + question = state.question, + answer = state.answer, + ), + ) + } + } + } + } + + LaunchedEffect(Unit) { + postReviewViewModel.sideEffect.collect { + if (it is PostReviewSideEffect.Success) { + onPostReviewCompleteClick() + } + } + } + + PostExpectReviewScreen( + onBackPressed = onBackPressed, + onReviewFinishClick = postExpectReviewViewModel::setEmpty, + answer = { state.answer }, + onAnswerChange = postExpectReviewViewModel::setAnswer, + question = { state.question }, + onQuestionChange = postExpectReviewViewModel::setQuestion, + state = state, + ) +} + +@Composable +private fun PostExpectReviewScreen( + onBackPressed: () -> Unit, + onReviewFinishClick: () -> Unit, + answer: () -> String, + onAnswerChange: (String) -> Unit, + question: () -> String, + onQuestionChange: (String) -> Unit, + state: PostExpectReviewState, +) { + Column { + JobisSmallTopAppBar( + onBackPressed = onBackPressed, + ) + JobisText( + modifier = Modifier.padding(vertical = 18.dp, horizontal = 24.dp), + text = stringResource(id = R.string.add_interview_question_title), + style = JobisTypography.PageTitle, + ) + Row( + modifier = Modifier.padding(top = 30.dp, start = 24.dp, end = 24.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + JobisText( + text = stringResource(id = R.string.question_label), + style = JobisTypography.Description, + ) + JobisText( + text = stringResource(id = R.string.required_mark), + style = JobisTypography.Description, + color = JobisTheme.colors.onPrimary, + ) + } + JobisTextField( + value = question, + onValueChange = onQuestionChange, + hint = stringResource(id = R.string.hint_write_question), + ) + Row( + modifier = Modifier.padding(top = 30.dp, start = 24.dp, end = 24.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + JobisText( + text = stringResource(R.string.answer), + style = JobisTypography.Description, + ) + JobisText( + text = stringResource(id = R.string.required_mark), + style = JobisTypography.Description, + color = JobisTheme.colors.onPrimary, + ) + } + JobisTextField( + modifier = Modifier.heightIn(min = 120.dp, max = 300.dp), + value = answer, + onValueChange = onAnswerChange, + hint = stringResource(id = R.string.hint_write_answer), + singleLine = false, + ) + Spacer(modifier = Modifier.weight(1f)) + JobisText( + modifier = Modifier + .align(Alignment.CenterHorizontally) + .padding(vertical = 4.dp) + .clickable { onReviewFinishClick() }, + text = stringResource(id = R.string.skip_button), + style = JobisTypography.SubBody, + textDecoration = TextDecoration.Underline, + color = JobisTheme.colors.surfaceTint, + ) + JobisButton( + text = stringResource(id = R.string.complete), + color = ButtonColor.Primary, + onClick = onReviewFinishClick, + enabled = state.buttonEnabled, + ) + } +} diff --git a/feature/post-review/src/main/java/team/retum/post/review/ui/PostNextReviewScreen.kt b/feature/post-review/src/main/java/team/retum/post/review/ui/PostNextReviewScreen.kt new file mode 100644 index 000000000..2f3a915e4 --- /dev/null +++ b/feature/post-review/src/main/java/team/retum/post/review/ui/PostNextReviewScreen.kt @@ -0,0 +1,173 @@ +package team.retum.post.review.ui + +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.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.PagerState +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.snapshots.SnapshotStateList +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import team.retum.jobisdesignsystemv2.appbar.JobisSmallTopAppBar +import team.retum.jobisdesignsystemv2.button.ButtonColor +import team.retum.jobisdesignsystemv2.button.JobisButton +import team.retum.jobisdesignsystemv2.foundation.JobisTheme +import team.retum.jobisdesignsystemv2.foundation.JobisTypography +import team.retum.jobisdesignsystemv2.text.JobisText +import team.retum.jobisdesignsystemv2.textfield.JobisTextField +import team.retum.post.review.R +import team.retum.post.review.model.PostReviewData +import team.retum.post.review.viewmodel.PostNextReviewSideEffect +import team.retum.post.review.viewmodel.PostNextReviewViewModel +import team.retum.usecase.entity.QuestionsEntity.QuestionEntity + +@Composable +internal fun PostNextReview( + reviewData: PostReviewData, + onBackPressed: () -> Unit, + navigateToPostExpectReview: (PostReviewData) -> Unit, + postNextReviewViewModel: PostNextReviewViewModel = hiltViewModel(), +) { + val state by postNextReviewViewModel.state.collectAsStateWithLifecycle() + val pagerState = rememberPagerState(pageCount = { state.questions.size }) + val coroutineScope = rememberCoroutineScope() + val answers = remember { mutableStateListOf("", "", "") } + + LaunchedEffect(Unit) { + postNextReviewViewModel.fetchQuestions() + postNextReviewViewModel.sideEffect.collect { + when (it) { + is PostNextReviewSideEffect.MoveToNext -> { + navigateToPostExpectReview( + reviewData.copy(qnaElements = state.qnaElements), + ) + } + } + } + } + + PostNextReviewScreen( + onBackPressed = onBackPressed, + questions = state.questions, + answers = answers, + coroutineScope = coroutineScope, + pagerState = pagerState, + onPostExpectReviewClick = postNextReviewViewModel::onNextClick, + setAnswer = postNextReviewViewModel::getAnswer, + onAnswerChange = postNextReviewViewModel::setAnswer, + setQuestion = postNextReviewViewModel::setQuestion, + ) +} + +@Composable +private fun PostNextReviewScreen( + onBackPressed: () -> Unit, + questions: List, + coroutineScope: CoroutineScope, + pagerState: PagerState, + answers: SnapshotStateList, + onPostExpectReviewClick: () -> Unit, + setAnswer: (Int) -> String, + onAnswerChange: (String, Int) -> Unit, + setQuestion: () -> Unit, +) { + Column( + modifier = Modifier + .fillMaxWidth(), + ) { + JobisSmallTopAppBar( + onBackPressed = { + if (pagerState.currentPage == 0) { + onBackPressed() + } else { + coroutineScope.launch { + pagerState.animateScrollToPage(pagerState.currentPage - 1) + } + } + }, + ) + HorizontalPager( + state = pagerState, + modifier = Modifier, + userScrollEnabled = false, + ) { page -> + Column { + Column( + modifier = Modifier.padding(horizontal = 24.dp), + ) { + Row( + horizontalArrangement = Arrangement.spacedBy(4.dp), + ) { + repeat(pagerState.pageCount) { + val color = if (pagerState.currentPage == it) JobisTheme.colors.onPrimary else JobisTheme.colors.surfaceVariant + val multiple = if (pagerState.currentPage == it) 1.8f else 1f + Box( + modifier = Modifier + .background(color = color, shape = RoundedCornerShape(200.dp)) + .size(width = 12.dp * multiple, height = 6.dp), + ) + } + } + JobisText( + modifier = Modifier.padding(vertical = 18.dp), + text = stringResource(id = R.string.question_prefix) + questions[page].question, + style = JobisTypography.PageTitle, + ) + Row( + modifier = Modifier.padding(top = 30.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + JobisText( + text = stringResource(R.string.answer), + style = JobisTypography.Description, + ) + JobisText( + text = stringResource(id = R.string.required_mark), + style = JobisTypography.Description, + color = JobisTheme.colors.onPrimary, + ) + } + } + JobisTextField( + modifier = Modifier.heightIn(min = 120.dp, max = 300.dp), + value = { setAnswer(page) }, + onValueChange = { onAnswerChange(answers[page], page) }, + hint = stringResource(R.string.hint_answer_review), + singleLine = false, + ) + Spacer(modifier = Modifier.weight(1f)) + JobisButton( + text = stringResource(id = R.string.next), + color = ButtonColor.Primary, + onClick = { + setQuestion() + coroutineScope.launch { + if (pagerState.currentPage != 2) pagerState.animateScrollToPage(pagerState.currentPage + 1) else onPostExpectReviewClick() + } + }, + ) + } + } + } +} diff --git a/feature/post-review/src/main/java/team/retum/post/review/ui/PostReviewCompleteScreen.kt b/feature/post-review/src/main/java/team/retum/post/review/ui/PostReviewCompleteScreen.kt new file mode 100644 index 000000000..ab8d93bd0 --- /dev/null +++ b/feature/post-review/src/main/java/team/retum/post/review/ui/PostReviewCompleteScreen.kt @@ -0,0 +1,90 @@ +package team.retum.post.review.ui + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import kotlinx.coroutines.delay +import team.retum.post.review.R +import team.retum.jobisdesignsystemv2.foundation.JobisTheme +import team.retum.jobisdesignsystemv2.foundation.JobisTypography +import team.retum.jobisdesignsystemv2.text.JobisText +import team.retum.post.review.viewmodel.PostReviewViewModel + +const val SCREEN_TIME = 1500L + +@Composable +internal fun PostReviewComplete( + onBackPressed: () -> Unit, + navigateToPostReview: (String, Long) -> Unit, + postReviewViewModel: PostReviewViewModel = hiltViewModel(), +) { + val state by postReviewViewModel.state.collectAsStateWithLifecycle() + + LaunchedEffect(Unit) { + // TODO :: 빈번하게 일어나는 api 요청 개선 + delay(SCREEN_TIME) + onBackPressed() + navigateToPostReview("", 0) + } + + PostReviewCompleteScreen( + studentName = state.studentName, + ) +} + +@Composable +private fun PostReviewCompleteScreen( + studentName: String, +) { + Column( + modifier = Modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Column( + modifier = Modifier.fillMaxHeight(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + Image( + painter = painterResource(team.retum.design_system.R.drawable.ic_success), + contentDescription = stringResource(id = R.string.content_description_review_success), + ) + JobisText( + modifier = Modifier.padding( + top = 20.dp, + start = 24.dp, + end = 24.dp, + ), + text = stringResource(R.string.post_complete_review_check_title, studentName), + textAlign = TextAlign.Center, + style = JobisTypography.PageTitle, + color = JobisTheme.colors.onBackground, + ) + JobisText( + modifier = Modifier.padding( + top = 8.dp, + bottom = 20.dp, + start = 24.dp, + end = 24.dp, + ), + text = stringResource(R.string.post_complete_review_good_result), + textAlign = TextAlign.Center, + style = JobisTypography.SubBody, + ) + } + } +} diff --git a/feature/post-review/src/main/java/team/retum/post/review/ui/PostReviewScreen.kt b/feature/post-review/src/main/java/team/retum/post/review/ui/PostReviewScreen.kt new file mode 100644 index 000000000..540db277d --- /dev/null +++ b/feature/post-review/src/main/java/team/retum/post/review/ui/PostReviewScreen.kt @@ -0,0 +1,875 @@ +package team.retum.post.review.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +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.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.rememberModalBottomSheetState +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.setValue +import androidx.compose.runtime.snapshots.SnapshotStateList +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import team.retum.common.enums.InterviewLocation +import team.retum.common.enums.InterviewType +import team.retum.common.enums.ReviewProcess +import team.retum.jobisdesignsystemv2.appbar.JobisLargeTopAppBar +import team.retum.jobisdesignsystemv2.button.ButtonColor +import team.retum.jobisdesignsystemv2.button.JobisButton +import team.retum.jobisdesignsystemv2.button.JobisIconButton +import team.retum.jobisdesignsystemv2.checkbox.JobisCheckBox +import team.retum.jobisdesignsystemv2.foundation.JobisIcon +import team.retum.jobisdesignsystemv2.foundation.JobisTheme +import team.retum.jobisdesignsystemv2.foundation.JobisTypography +import team.retum.jobisdesignsystemv2.text.JobisText +import team.retum.jobisdesignsystemv2.textfield.JobisTextField +import team.retum.jobisdesignsystemv2.toast.JobisToast +import team.retum.jobisdesignsystemv2.utils.clickable +import team.retum.post.review.R +import team.retum.post.review.model.PostReviewData +import team.retum.post.review.viewmodel.PostReviewSideEffect +import team.retum.post.review.viewmodel.PostReviewState +import team.retum.post.review.viewmodel.PostReviewViewModel +import team.retum.usecase.entity.CodesEntity + +const val PAGER_COUNT = 4 + +@Composable +internal fun PostReview( + onBackPressed: () -> Unit, + companyName: String, + companyId: Long, + navigateToPostNextReview: (PostReviewData) -> Unit, + reviewViewModel: PostReviewViewModel = hiltViewModel(), +) { + val state by reviewViewModel.state.collectAsStateWithLifecycle() + val context = LocalContext.current + var currentStep by remember { mutableStateOf(null) } + + LaunchedEffect(Unit) { + reviewViewModel.fetchMyReview() + reviewViewModel.sideEffect.collect { + when(it) { + is PostReviewSideEffect.Success -> { + JobisToast.create( + context = context, + message = context.getString(R.string.post_review_success), + ).show() + } + + is PostReviewSideEffect.MoveToNext -> { + navigateToPostNextReview( + PostReviewData( + companyId = companyId, + interviewType = it.interviewType, + location = it.location, + jobCode = it.jobCode, + interviewerCount = it.interviewerCount, + ) + ) + } + + PostReviewSideEffect.BadRequest -> { + JobisToast.create( + context = context, + message = context.getString(R.string.post_review_bad_request), + drawable = JobisIcon.Error + ).show() + } + } + } + } + + LaunchedEffect(state.keyword) { + reviewViewModel.fetchCodes(state.keyword) + } + + PostReviewScreen( + onBackPressed = onBackPressed, + state = state, + onAddReviewClick = { currentStep = ReviewProcess.INTERVIEW_TYPE }, + currentStep = currentStep, + onDismiss = { currentStep = null }, + onStepChange = { currentStep = it }, + companyName = companyName, + setInterviewerCount = reviewViewModel::setInterviewerCount, + setInterviewType = reviewViewModel::setInterviewType, + setInterviewLocation = reviewViewModel::setInterviewLocation, + setButtonClear = reviewViewModel::setButtonClear, + setChecked = reviewViewModel::setChecked, + setKeyword = reviewViewModel::setKeyword, + setSelectedTech = reviewViewModel::setSelectedTech, + techs = reviewViewModel.techs, + buttonEnabled = state.buttonEnabled, + onPostNextClick = reviewViewModel::onNextClick, + ) +} + +@Composable +private fun PostReviewScreen( + onBackPressed: () -> Unit, + state: PostReviewState, + onAddReviewClick: () -> Unit, + currentStep: ReviewProcess?, + onDismiss: () -> Unit, + onStepChange: (ReviewProcess?) -> Unit, + companyName: String, + setKeyword: (String?) -> Unit, + setSelectedTech: (Long?) -> Unit, + setChecked: (String?) -> Unit, + techs: SnapshotStateList, + setInterviewerCount: (String) -> Unit, + setInterviewType: (InterviewType) -> Unit, + setInterviewLocation: (InterviewLocation) -> Unit, + setButtonClear: () -> Unit, + buttonEnabled: Boolean, + onPostNextClick: () -> Unit, +) { + Column( + modifier = Modifier + .background(JobisTheme.colors.background) + .fillMaxSize(), + ) { + JobisLargeTopAppBar( + onBackPressed = onBackPressed, + title = stringResource(id = R.string.write_review), + ) + LazyColumn( + verticalArrangement = Arrangement.spacedBy(4.dp), + ) { + if (state.myReview.isNotEmpty()) { + items(state.myReview.size) { + val myReview = state.myReview[it] + Box( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 24.dp) + .background(color = JobisTheme.colors.onPrimary, shape = RoundedCornerShape(12.dp)), + ) { + JobisText( + text = stringResource(id = R.string.review_complete_suffix, myReview.companyName), + color = JobisTheme.colors.background, + style = JobisTypography.HeadLine, + textAlign = TextAlign.Center, + modifier = Modifier + .fillMaxWidth() + .padding( + vertical = 16.dp, + horizontal = 16.dp, + ), + maxLines = 1, + ) + } + } + } else { + item { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 24.dp) + .border( + width = 1.dp, + shape = RoundedCornerShape(12.dp), + color = JobisTheme.colors.surfaceVariant, + ), + ) { + JobisText( + text = stringResource(id = R.string.empty), + color = JobisTheme.colors.onSurfaceVariant, + style = JobisTypography.HeadLine, + textAlign = TextAlign.Center, + modifier = Modifier + .fillMaxWidth() + .padding( + vertical = 16.dp, + horizontal = 16.dp, + ), + ) + } + } + } + item { + JobisButton( + text = stringResource(id = R.string.add_review), + onClick = onAddReviewClick, + ) + } + } + } + + // 각 단계별로 별도의 바텀시트 표시 + when (currentStep) { + ReviewProcess.INTERVIEW_TYPE -> { + InterviewTypeBottomSheet( + onDismiss = onDismiss, + onNextClick = { + setButtonClear() + onStepChange(ReviewProcess.INTERVIEW_LOCATION) + }, + setInterviewType = setInterviewType, + interviewType = state.interviewType, + buttonEnabled = buttonEnabled, + ) + } + + ReviewProcess.INTERVIEW_LOCATION -> { + InterviewLocationBottomSheet( + onDismiss = onDismiss, + onBackPressed = { onStepChange(ReviewProcess.INTERVIEW_TYPE) }, + onNextClick = { + setButtonClear() + onStepChange(ReviewProcess.TECH_STACK) + }, + setInterviewLocation = setInterviewLocation, + interviewLocation = state.interviewLocation, + buttonEnabled = buttonEnabled, + ) + } + + ReviewProcess.TECH_STACK -> { + TechStackBottomSheet( + onDismiss = onDismiss, + onBackPressed = { onStepChange(ReviewProcess.INTERVIEW_LOCATION) }, + onNextClick = { + setButtonClear() + onStepChange(ReviewProcess.INTERVIEWER_COUNT) + }, + setKeyword = setKeyword, + setSelectedTech = setSelectedTech, + setChecked = setChecked, + state = state, + techs = techs, + buttonEnabled = buttonEnabled, + ) + } + + ReviewProcess.INTERVIEWER_COUNT -> { + InterviewerCountBottomSheet( + onDismiss = onDismiss, + onBackPressed = { onStepChange(ReviewProcess.TECH_STACK) }, + onNextClick = { + setButtonClear() + onStepChange(ReviewProcess.SUMMARY) + }, + setInterviewerCount = setInterviewerCount, + state = state, + buttonEnabled = buttonEnabled, + ) + } + + ReviewProcess.SUMMARY -> { + SummaryBottomSheet( + onDismiss = onDismiss, + onBackPressed = { onStepChange(ReviewProcess.INTERVIEWER_COUNT) }, + onNextClick = onPostNextClick, + state = state, + companyName = companyName, + ) + } + + null -> { /* 바텀시트 숨김 */ } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun InterviewTypeBottomSheet( + onDismiss: () -> Unit, + onNextClick: () -> Unit, + setInterviewType: (InterviewType) -> Unit, + interviewType: InterviewType, + buttonEnabled: Boolean, +) { + val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + + ModalBottomSheet( + onDismissRequest = onDismiss, + sheetState = sheetState, + containerColor = JobisTheme.colors.inverseSurface, + shape = RoundedCornerShape(topStart = 12.dp, topEnd = 12.dp), + dragHandle = null, + scrimColor = Color.Transparent, + ) { + Column( + modifier = Modifier.padding( + top = 24.dp, + bottom = 12.dp, + ) + ) { + Row( + modifier = Modifier.padding(horizontal = 24.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + JobisIconButton( + drawableResId = JobisIcon.Arrow, + defaultBackgroundColor = JobisTheme.colors.inverseSurface, + contentDescription = stringResource(id = team.retum.design_system.R.string.content_description_arrow), + onClick = onDismiss, + ) + JobisText( + text = stringResource(id = R.string.review_category), + color = JobisTheme.colors.onSurfaceVariant, + style = JobisTypography.SubHeadLine, + ) + } + Row( + modifier = Modifier.padding(top = 10.dp, bottom = 28.dp, start = 24.dp, end = 24.dp), + horizontalArrangement = Arrangement.spacedBy(4.dp) + ) { + repeat(PAGER_COUNT) { + val color = if (it == 0) JobisTheme.colors.onPrimary else JobisTheme.colors.surfaceVariant + val multiple = if (it == 0) 1.8f else 1f + Box( + modifier = Modifier + .background(color = color, shape = RoundedCornerShape(200.dp)) + .size(width = 12.dp * multiple, height = 6.dp) + ) + } + } + + Row( + modifier = Modifier.padding(horizontal = 24.dp), + ) { + JobisText( + text = stringResource(id = R.string.required_answer), + color = JobisTheme.colors.onSurface, + style = JobisTypography.Description + ) + Icon( + painter = painterResource(JobisIcon.Asterisk), + contentDescription = stringResource(id = R.string.content_description_required), + tint = JobisTheme.colors.secondary, + modifier = Modifier.size(12.dp) + ) + } + Column( + modifier = Modifier.padding(start = 24.dp, end = 24.dp, top = 12.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + PostReviewOutlinedStrokeText( + selected = interviewType == InterviewType.INDIVIDUAL, + text = stringResource(id = R.string.individual_review), + onButtonClick = { setInterviewType(InterviewType.INDIVIDUAL) }, + ) + + PostReviewOutlinedStrokeText( + selected = interviewType == InterviewType.GROUP, + text = stringResource(id = R.string.group_review), + onButtonClick = { setInterviewType(InterviewType.GROUP) }, + ) + + PostReviewOutlinedStrokeText( + selected = interviewType == InterviewType.OTHER, + text = stringResource(id = R.string.other_review), + onButtonClick = { setInterviewType(InterviewType.OTHER) }, + ) + } + JobisButton( + modifier = Modifier.padding(top = 12.dp), + text = stringResource(id = R.string.next), + onClick = onNextClick, + color = ButtonColor.Primary, + enabled = buttonEnabled, + ) + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun InterviewLocationBottomSheet( + onDismiss: () -> Unit, + onBackPressed: () -> Unit, + onNextClick: () -> Unit, + setInterviewLocation: (InterviewLocation) -> Unit, + interviewLocation: InterviewLocation, + buttonEnabled: Boolean, +) { + val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + + ModalBottomSheet( + onDismissRequest = onDismiss, + sheetState = sheetState, + containerColor = JobisTheme.colors.inverseSurface, + shape = RoundedCornerShape(topStart = 12.dp, topEnd = 12.dp), + dragHandle = null, + scrimColor = Color.Transparent, + ) { + Column( + modifier = Modifier.padding( + top = 24.dp, + bottom = 12.dp, + ) + ) { + Row( + modifier = Modifier.padding(horizontal = 24.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + JobisIconButton( + drawableResId = JobisIcon.Arrow, + defaultBackgroundColor = JobisTheme.colors.inverseSurface, + contentDescription = stringResource(id = team.retum.design_system.R.string.content_description_arrow), + onClick = onBackPressed, + ) + JobisText( + text = stringResource(id = R.string.review_location), + color = JobisTheme.colors.onSurfaceVariant, + style = JobisTypography.SubHeadLine, + ) + } + Row( + modifier = Modifier.padding(top = 10.dp, bottom = 28.dp, start = 24.dp, end = 24.dp), + horizontalArrangement = Arrangement.spacedBy(4.dp) + ) { + repeat(PAGER_COUNT) { + val color = if (it == 1) JobisTheme.colors.onPrimary else JobisTheme.colors.surfaceVariant + val multiple = if (it == 1) 1.8f else 1f + Box( + modifier = Modifier + .background(color = color, shape = RoundedCornerShape(200.dp)) + .size(width = 12.dp * multiple, height = 6.dp) + ) + } + } + Row( + modifier = Modifier.padding(horizontal = 24.dp), + ) { + JobisText( + text = stringResource(id = R.string.required_answer), + color = JobisTheme.colors.onSurface, + style = JobisTypography.Description + ) + Icon( + painter = painterResource(JobisIcon.Asterisk), + contentDescription = stringResource(id = R.string.content_description_required), + tint = JobisTheme.colors.secondary, + modifier = Modifier.size(12.dp) + ) + } + Column( + modifier = Modifier.padding(start = 24.dp, end = 24.dp, top = 12.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + PostReviewOutlinedStrokeText( + selected = interviewLocation == InterviewLocation.DAEJEON, + text = stringResource(id = R.string.deajeon), + onButtonClick = { setInterviewLocation(InterviewLocation.DAEJEON) }, + ) + PostReviewOutlinedStrokeText( + selected = interviewLocation == InterviewLocation.SEOUL, + text = stringResource(id = R.string.seoul), + onButtonClick = { setInterviewLocation(InterviewLocation.SEOUL) }, + ) + PostReviewOutlinedStrokeText( + selected = interviewLocation == InterviewLocation.GYEONGGI, + text = stringResource(id = R.string.gyeonggi), + onButtonClick = { setInterviewLocation(InterviewLocation.GYEONGGI) }, + ) + + PostReviewOutlinedStrokeText( + selected = interviewLocation == InterviewLocation.OTHER, + text = stringResource(id = R.string.other), + onButtonClick = { setInterviewLocation(InterviewLocation.OTHER) }, + ) + } + JobisButton( + modifier = Modifier.padding(top = 12.dp), + text = stringResource(id = R.string.next), + onClick = onNextClick, + color = ButtonColor.Primary, + enabled = buttonEnabled, + ) + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun TechStackBottomSheet( + onDismiss: () -> Unit, + onBackPressed: () -> Unit, + onNextClick: () -> Unit, + setKeyword: (String?) -> Unit, + setSelectedTech: (Long?) -> Unit, + setChecked: (String?) -> Unit, + state: PostReviewState, + techs: SnapshotStateList, + buttonEnabled: Boolean, +) { + val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + + ModalBottomSheet( + onDismissRequest = onDismiss, + sheetState = sheetState, + containerColor = JobisTheme.colors.inverseSurface, + shape = RoundedCornerShape(topStart = 12.dp, topEnd = 12.dp), + dragHandle = null, + scrimColor = Color.Transparent, + ) { + Column( + modifier = Modifier.padding( + top = 24.dp, + bottom = 12.dp, + ) + ) { + Row( + modifier = Modifier.padding(horizontal = 24.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + JobisIconButton( + drawableResId = JobisIcon.Arrow, + defaultBackgroundColor = JobisTheme.colors.inverseSurface, + contentDescription = stringResource(id = team.retum.design_system.R.string.content_description_arrow), + onClick = onBackPressed, + ) + JobisText( + text = stringResource(id = R.string.support_position), + color = JobisTheme.colors.onSurfaceVariant, + style = JobisTypography.SubHeadLine, + ) + } + + Row( + modifier = Modifier.padding(top = 10.dp, bottom = 28.dp, start = 24.dp, end = 24.dp), + horizontalArrangement = Arrangement.spacedBy(4.dp) + ) { + repeat(PAGER_COUNT) { + val color = if (it == 2) JobisTheme.colors.onPrimary else JobisTheme.colors.surfaceVariant + val multiple = if (it == 2) 1.8f else 1f + Box( + modifier = Modifier + .background(color = color, shape = RoundedCornerShape(200.dp)) + .size(width = 12.dp * multiple, height = 6.dp) + ) + } + } + + JobisTextField( + value = { state.keyword ?: "" }, + hint = stringResource(id = R.string.search), + drawableResId = JobisIcon.Search, + onValueChange = setKeyword, + fieldColor = JobisTheme.colors.background, + ) + Column( + modifier = Modifier + .padding(horizontal = 24.dp) + .heightIn(max = 300.dp) + .verticalScroll(rememberScrollState()), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + techs.forEach { codes -> + Row( + modifier = Modifier.padding(vertical = 12.dp), + ) { + JobisCheckBox( + checked = state.checked == codes.keyword, + onClick = { + if (state.checked == codes.keyword) { + setChecked(null) + setKeyword(null) + setSelectedTech(null) + } else { + setChecked(codes.keyword) + setKeyword(codes.keyword) + setSelectedTech(codes.code) + } + }, + backgroundColor = JobisTheme.colors.background, + ) + Spacer(modifier = Modifier.width(8.dp)) + JobisText( + text = codes.keyword, + style = JobisTypography.Body, + color = JobisTheme.colors.inverseOnSurface, + modifier = Modifier.align(Alignment.CenterVertically), + ) + } + } + } + + JobisButton( + text = stringResource(id = R.string.next), + onClick = onNextClick, + color = ButtonColor.Primary, + enabled = buttonEnabled, + ) + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun InterviewerCountBottomSheet( + onDismiss: () -> Unit, + onBackPressed: () -> Unit, + onNextClick: () -> Unit, + setInterviewerCount: (String) -> Unit, + state: PostReviewState, + buttonEnabled: Boolean, +) { + val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + + ModalBottomSheet( + onDismissRequest = onDismiss, + sheetState = sheetState, + containerColor = JobisTheme.colors.inverseSurface, + shape = RoundedCornerShape(topStart = 12.dp, topEnd = 12.dp), + dragHandle = null, + scrimColor = Color.Transparent, + ) { + Column( + modifier = Modifier.padding( + top = 24.dp, + bottom = 12.dp, + ) + ) { + Row( + modifier = Modifier.padding(horizontal = 24.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + JobisIconButton( + drawableResId = JobisIcon.Arrow, + defaultBackgroundColor = JobisTheme.colors.inverseSurface, + contentDescription = stringResource(id = team.retum.design_system.R.string.content_description_arrow), + onClick = onBackPressed, + ) + JobisText( + text = stringResource(id = R.string.interviewer_count), + color = JobisTheme.colors.onSurfaceVariant, + style = JobisTypography.SubHeadLine, + ) + } + + Row( + modifier = Modifier.padding(top = 10.dp, bottom = 28.dp, start = 24.dp, end = 24.dp), + horizontalArrangement = Arrangement.spacedBy(4.dp) + ) { + repeat(PAGER_COUNT) { + val color = if (it == 3) JobisTheme.colors.onPrimary else JobisTheme.colors.surfaceVariant + val multiple = if (it == 3) 1.8f else 1f + Box( + modifier = Modifier + .background(color = color, shape = RoundedCornerShape(200.dp)) + .size(width = 12.dp * multiple, height = 6.dp) + ) + } + } + + Row( + modifier = Modifier.padding(horizontal = 24.dp), + ) { + JobisText( + text = stringResource(id = R.string.required_answer), + color = JobisTheme.colors.onSurface, + style = JobisTypography.Description + ) + Icon( + painter = painterResource(JobisIcon.Asterisk), + contentDescription = stringResource(id = R.string.content_description_required), + tint = JobisTheme.colors.secondary, + modifier = Modifier.size(12.dp) + ) + } + + JobisTextField( + value = { state.count }, + hint = stringResource(id = R.string.search), + onValueChange = setInterviewerCount, + fieldColor = JobisTheme.colors.background, + keyboardType = KeyboardType.Number, + ) + + JobisButton( + modifier = Modifier.padding(top = 12.dp), + text = stringResource(id = R.string.next), + onClick = onNextClick, + color = ButtonColor.Primary, + enabled = buttonEnabled, + ) + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun SummaryBottomSheet( + onDismiss: () -> Unit, + onBackPressed: () -> Unit, + onNextClick: () -> Unit, + state: PostReviewState, + companyName: String, +) { + val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + + val interviewType = when (state.interviewType) { + InterviewType.INDIVIDUAL -> stringResource(id = R.string.individual_review) + InterviewType.GROUP -> stringResource(id = R.string.group_review) + InterviewType.OTHER -> stringResource(id = R.string.other_review) + } + val interviewLocation = when (state.interviewLocation) { + InterviewLocation.DAEJEON -> stringResource(id = R.string.deajeon) + InterviewLocation.SEOUL -> stringResource(id = R.string.seoul) + InterviewLocation.GYEONGGI -> stringResource(id = R.string.gyeonggi) + InterviewLocation.OTHER -> stringResource(id = R.string.other) + } + + ModalBottomSheet( + onDismissRequest = onDismiss, + sheetState = sheetState, + containerColor = JobisTheme.colors.inverseSurface, + shape = RoundedCornerShape(topStart = 12.dp, topEnd = 12.dp), + dragHandle = null, + scrimColor = Color.Transparent, + ) { + Column( + modifier = Modifier.padding( + top = 24.dp, + bottom = 12.dp, + ) + ) { + Row( + modifier = Modifier.padding(horizontal = 24.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + JobisIconButton( + drawableResId = JobisIcon.Arrow, + defaultBackgroundColor = JobisTheme.colors.inverseSurface, + contentDescription = stringResource(id = team.retum.design_system.R.string.content_description_arrow), + onClick = onBackPressed, + ) + JobisText( + text = stringResource(id = R.string.review_category), + color = JobisTheme.colors.onSurfaceVariant, + style = JobisTypography.SubHeadLine, + ) + } + + Column( + modifier = Modifier.padding(top = 10.dp), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + TopBottomText( + topText = stringResource(id = R.string.interview_type_label), + bottomText = interviewType, + ) + TopBottomText( + topText = stringResource(id = R.string.interview_location_label), + bottomText = interviewLocation + ) + TopBottomText( + topText = stringResource(id = R.string.company_name), + bottomText = companyName.removeSuffix("..."), + ) + TopBottomText( + topText = stringResource(id = R.string.position_label), + bottomText = state.keyword ?: "", + ) + TopBottomText( + topText = stringResource(id = R.string.interviewer_count_label), + bottomText = state.count, + ) + } + + JobisButton( + modifier = Modifier.padding(top = 12.dp), + text = stringResource(id = R.string.next), + onClick = onNextClick, + color = ButtonColor.Primary, + enabled = true, + ) + } + } +} + +@Composable +private fun TopBottomText( + topText: String, + bottomText: String, +) { + Column( + modifier = Modifier.padding(horizontal = 24.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + JobisText( + text = topText, + style = JobisTypography.Description, + color = JobisTheme.colors.onSurfaceVariant, + ) + JobisText( + text = bottomText, + style = JobisTypography.HeadLine, + color = JobisTheme.colors.onPrimary + ) + } +} + +@Composable +private fun PostReviewOutlinedStrokeText( + modifier: Modifier = Modifier, + selected: Boolean, + text: String, + onButtonClick: () -> Unit, +) { + val borderColor = if (selected) { + JobisTheme.colors.onPrimary + } else { + JobisTheme.colors.surfaceVariant + } + val textColor = if (selected) { + JobisTheme.colors.onPrimary + } else { + JobisTheme.colors.onSurfaceVariant + } + + JobisText( + modifier = modifier + .border( + width = 1.dp, + color = borderColor, + shape = RoundedCornerShape(12.dp) + ) + .background(color = Color.White, shape = RoundedCornerShape(12.dp)) + .fillMaxWidth() + .padding( + vertical = 16.dp, + horizontal = 16.dp, + ) + .clickable( + onClick = onButtonClick, + ), + text = text, + color = textColor, + style = JobisTypography.SubHeadLine, + textAlign = TextAlign.Center, + ) +} diff --git a/feature/post-review/src/main/java/team/retum/post/review/viewmodel/PostExpectReviewViewModel.kt b/feature/post-review/src/main/java/team/retum/post/review/viewmodel/PostExpectReviewViewModel.kt new file mode 100644 index 000000000..017b2e3d0 --- /dev/null +++ b/feature/post-review/src/main/java/team/retum/post/review/viewmodel/PostExpectReviewViewModel.kt @@ -0,0 +1,69 @@ +package team.retum.post.review.viewmodel + +import androidx.compose.runtime.Immutable +import dagger.hilt.android.lifecycle.HiltViewModel +import team.retum.common.base.BaseViewModel +import javax.inject.Inject + +@HiltViewModel +internal class PostExpectReviewViewModel @Inject constructor( +) : BaseViewModel(PostExpectReviewState.getInitialState()) { + + internal fun setAnswer(answer: String) = setState { + setButtonEnabled(answer = answer) + state.value.copy(answer = answer) + } + + internal fun setQuestion(question: String) = setState { + setButtonEnabled(question = question) + state.value.copy(question = question) + } + + private fun setButtonEnabled( + answer: String = state.value.answer, + question: String = state.value.question, + ) = setState { + state.value.copy(buttonEnabled = answer.isNotBlank() && question.isNotBlank()) + } + + internal fun setEmpty() { + with(state.value) { + copy( + question = "", + answer = "", + ) + } + onNextClick() + } + + internal fun onNextClick() { + with(state.value) { + postSideEffect(PostExpectReviewSideEffect.PostReview( + question = question, + answer = answer + )) + } + } +} + +@Immutable +data class PostExpectReviewState( + val question: String, + val answer: String, + val buttonEnabled: Boolean, +) { + companion object { + fun getInitialState() = PostExpectReviewState( + question = "", + answer = "", + buttonEnabled = false + ) + } +} + +internal sealed interface PostExpectReviewSideEffect { + data class PostReview( + val question: String, + val answer: String, + ): PostExpectReviewSideEffect +} diff --git a/feature/post-review/src/main/java/team/retum/post/review/viewmodel/PostNextReviewViewModel.kt b/feature/post-review/src/main/java/team/retum/post/review/viewmodel/PostNextReviewViewModel.kt new file mode 100644 index 000000000..e14ed1a35 --- /dev/null +++ b/feature/post-review/src/main/java/team/retum/post/review/viewmodel/PostNextReviewViewModel.kt @@ -0,0 +1,97 @@ +package team.retum.post.review.viewmodel + +import androidx.compose.runtime.Immutable +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import team.retum.common.base.BaseViewModel +import team.retum.post.review.model.PostReviewData.PostReviewContent +import team.retum.usecase.entity.QuestionsEntity.QuestionEntity +import team.retum.usecase.usecase.review.FetchQuestionsUseCase +import javax.inject.Inject + +@HiltViewModel +internal class PostNextReviewViewModel @Inject constructor( + private val fetchQuestionsUseCase: FetchQuestionsUseCase, +) : BaseViewModel(PostNextReviewState.getInitialState()) { + + internal fun fetchQuestions() { + viewModelScope.launch(Dispatchers.IO) { + fetchQuestionsUseCase().onSuccess { + setState { + state.value.copy( + questions = it.questions, + ) + } + } + } + } + + internal fun setAnswer(answer: String, index: Int) { + setState { + with(state.value) { + val updatedAnswers = answers.toMutableList() + updatedAnswers[index] = answer + copy( + answers = updatedAnswers, + ) + } + } + } + + internal fun getAnswer(index: Int): String { + return state.value.answers.getOrNull(index) ?: "" + } + + internal fun setQuestion() { + setState { + with(state.value) { + val updatedQuestions = questions.map { it.id } + copy( + qnaElements = updatedQuestions.zip(answers).map { (q, a) -> + PostReviewContent( + question = q, + answer = a, + ) + }, + ) + } + } + } + + internal fun onNextClick() { + with(state.value) { + postSideEffect( + PostNextReviewSideEffect.MoveToNext( + qnaElements = qnaElements, + ), + ) + } + } +} + +@Immutable +internal data class PostNextReviewState( + val questions: List, + val buttonEnabled: Boolean, + val answer: String, + val answers: List, + val qnaElements: List, +) { + companion object { + fun getInitialState() = PostNextReviewState( + questions = emptyList(), + buttonEnabled = false, + answer = "", + answers = listOf("", "", ""), + qnaElements = emptyList(), + ) + } +} + +internal sealed interface PostNextReviewSideEffect { + data class MoveToNext( + val qnaElements: List, + ) : PostNextReviewSideEffect +} diff --git a/feature/post-review/src/main/java/team/retum/post/review/viewmodel/PostReviewViewModel.kt b/feature/post-review/src/main/java/team/retum/post/review/viewmodel/PostReviewViewModel.kt new file mode 100644 index 000000000..e546439f7 --- /dev/null +++ b/feature/post-review/src/main/java/team/retum/post/review/viewmodel/PostReviewViewModel.kt @@ -0,0 +1,253 @@ +package team.retum.post.review.viewmodel + +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.snapshots.SnapshotStateList +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import team.retum.common.base.BaseViewModel +import team.retum.common.enums.CodeType +import team.retum.common.enums.InterviewLocation +import team.retum.common.enums.InterviewType +import team.retum.common.enums.ReviewProcess +import team.retum.common.exception.BadRequestException +import team.retum.post.review.model.PostReviewData +import team.retum.post.review.model.toEntity +import team.retum.usecase.entity.CodesEntity +import team.retum.usecase.entity.MyReviewsEntity.MyReview +import team.retum.usecase.entity.PostReviewEntity +import team.retum.usecase.entity.PostReviewEntity.PostReviewContentEntity +import team.retum.usecase.usecase.code.FetchCodeUseCase +import team.retum.usecase.usecase.review.FetchMyReviewUseCase +import team.retum.usecase.usecase.review.PostReviewUseCase +import team.retum.usecase.usecase.student.FetchStudentInformationUseCase +import javax.inject.Inject + +@HiltViewModel +internal class PostReviewViewModel @Inject constructor( + private val fetchCodeUseCase: FetchCodeUseCase, + private val fetchMyReviewsUseCase: FetchMyReviewUseCase, + private val fetchStudentInformationUseCase: FetchStudentInformationUseCase, + private val postReviewUseCase: PostReviewUseCase, +) : BaseViewModel(PostReviewState.getInitialState()) { + + init { + fetchStudentInfo() + } + var techs: SnapshotStateList = mutableStateListOf() + private set + + private fun fetchStudentInfo() { + viewModelScope.launch(Dispatchers.IO) { + fetchStudentInformationUseCase().onSuccess { + setState { state.value.copy(studentName = it.studentName) } + } + } + } + + internal fun setQuestion(question: String) { + setState { state.value.copy(question = question) } + setButtonEnabled() + } + + internal fun setAnswer(answer: String) { + setState { state.value.copy(answer = answer) } + setButtonEnabled() + } + + internal fun setKeyword(keyword: String?) { + techs.clear() + setState { + state.value.copy( + keyword = keyword ?: "", + buttonEnabled = true + ) + } + } + + internal fun postReview(reviewData: PostReviewData) { + viewModelScope.launch(Dispatchers.IO) { + postReviewUseCase( + postReviewRequest = PostReviewEntity( + interviewType = reviewData.interviewType, + location = reviewData.location, + companyId = reviewData.companyId, + jobCode = reviewData.jobCode, + interviewerCount = reviewData.interviewerCount, + qnaElements = reviewData.qnaElements.map { it.toEntity() }, + question = reviewData.question, + answer = reviewData.answer, + ) + ).onSuccess { + postSideEffect(PostReviewSideEffect.Success) + }.onFailure { + when (it) { + is BadRequestException -> { + postSideEffect(PostReviewSideEffect.BadRequest) + } + } + } + } + } + +// internal fun addReview() { +// reviews.add( +// PostReviewEntity.PostReviewContentEntity( +// answer = state.value.answer, +// question = state.value.question, +// codeId = state.value.selectedTech!!, +// ), +// ) +// } + + internal fun setSelectedTech(selectedTech: Long?) = + setState { state.value.copy(selectedTech = selectedTech ?: 0) } + + internal fun fetchCodes(keyword: String?) = + viewModelScope.launch(Dispatchers.IO) { + fetchCodeUseCase( + keyword = keyword, + type = CodeType.JOB, + parentCode = null, + ).onSuccess { + techs.addAll(it.codes) + } + } + + internal fun fetchMyReview() { + viewModelScope.launch(Dispatchers.IO) { + fetchMyReviewsUseCase().onSuccess { + setState { + state.value.copy( + myReview = it.reviews + ) + } + } + } + } + + private fun setButtonEnabled() { + when (state.value.reviewProcess) { + ReviewProcess.INTERVIEW_TYPE -> { + setState { state.value.copy(buttonEnabled = state.value.question.isNotEmpty() && state.value.answer.isNotEmpty()) } + } + + else -> { + setState { state.value.copy(buttonEnabled = !state.value.keyword?.isNotEmpty()!!) } + } + } + } + + internal fun setChecked(checked: String?) { + setState { + state.value.copy( + checked = checked ?: "", + buttonEnabled = true, + ) + } + } + + internal fun setInterviewType(interviewType: InterviewType) { + setState { + state.value.copy( + interviewType = interviewType, + buttonEnabled = true, + ) + } + } + + internal fun setInterviewLocation(interviewLocation: InterviewLocation) { + setState { + state.value.copy( + interviewLocation = interviewLocation, + buttonEnabled = true, + ) + } + } + + internal fun setInterviewerCount(count: String) { + //techs.clear() + setState { + state.value.copy( + count = count, + buttonEnabled = true, + ) + } + } + + internal fun setButtonClear() { + setState { + state.value.copy( + buttonEnabled = false, + ) + } + } + + internal fun onNextClick() { + with(state.value) { + postSideEffect(PostReviewSideEffect.MoveToNext( + companyId = companyId, + interviewerCount = count.toInt(), + jobCode = selectedTech ?: 0, + interviewType = interviewType, + location = interviewLocation, + )) + } + } +} + +internal data class PostReviewState( + val studentName: String, + val interviewType: InterviewType, + val interviewLocation: InterviewLocation, + val companyId: Long, + val jobCode: Long, + val interviewerCount: Int, + val qnaElements: List, + val question: String, + val answer: String, + val keyword: String?, + val checked: String?, + val selectedTech: Long?, + val tech: String?, + val buttonEnabled: Boolean, + val reviewProcess: ReviewProcess, + val count: String, + val myReview: List, +) { + companion object { + fun getInitialState() = PostReviewState( + studentName = "", + question = "", + answer = "", + keyword = "", + checked = "", + selectedTech = 0, + tech = null, + buttonEnabled = false, + reviewProcess = ReviewProcess.INTERVIEW_TYPE, + interviewType = InterviewType.INDIVIDUAL, + interviewLocation = InterviewLocation.DAEJEON, // TODO :: 널처리(선택 해제) 고려 + count = "", + companyId = 0, + jobCode = 0, + interviewerCount = 0, + qnaElements = emptyList(), + myReview = emptyList(), + ) + } +} + +internal sealed interface PostReviewSideEffect { + data class MoveToNext( + val interviewType: InterviewType, + val location: InterviewLocation, + val companyId: Long, + val jobCode: Long, + val interviewerCount: Int, + ): PostReviewSideEffect + data object BadRequest : PostReviewSideEffect + + data object Success : PostReviewSideEffect +} diff --git a/feature/post-review/src/main/res/values/strings.xml b/feature/post-review/src/main/res/values/strings.xml new file mode 100644 index 000000000..fa045e78b --- /dev/null +++ b/feature/post-review/src/main/res/values/strings.xml @@ -0,0 +1,58 @@ + + + + 질문 + 답변 + 다른 학생들을 위하여\n면접의 후기를 작성해주세요 + 질문 추가하기 + 후기 추가하기 + 현재 입력 된 질문이 없어요 + 질문이 추가되었어요! + 다음 + 기술 스택 + 작성 완료 + 후기를 작성해주세요 + 검색어를 입력해주세요 + example + 면접 구분 + 면접 지역 + 지원 직무 + 면접관 수 + 개인 면접 + 단체 면접 + 기타 면접 + 대전 + 서울 + 경기 + 기타 + 확인 + 리뷰가 성공적으로 작성 되었어요! + 잘못된 요청이에요 + %s 후기 작성 완료 + Required + 답변 + 면접 구분 + 면접 지역 + 지원 직무 + 면접관 수 + 업체명 + + + %s님의\n후기가 작성이 완료됐어요! + 후기를 작성해 주셔서 감사합니다!\n좋은결과가 있을 거예요! + review_make_success + + + 면접 후기를 성심성의껏 작성해 주세요! + Q. + 다음 + + + 받았던 면접 질문을 추가해주세요! + 질문 + * + 받았던 질문을 작성해 주세요. + 예상 질문 답변을 성심성의껏 작성해 주세요! + 건너뛸래요. + 완료 + \ No newline at end of file diff --git a/feature/review/build.gradle.kts b/feature/review/build.gradle.kts index 41913c179..63ce8404f 100644 --- a/feature/review/build.gradle.kts +++ b/feature/review/build.gradle.kts @@ -5,6 +5,7 @@ plugins { id(libs.plugins.kotlin.ksp.get().pluginId) id(libs.plugins.hilt.android.get().pluginId) id(libs.plugins.ktlint.gradle.get().pluginId) + id(libs.plugins.kotlinx.serialization.get().pluginId) } apply() @@ -29,4 +30,6 @@ dependencies { implementation(libs.androidx.compose.material) implementation(libs.kotlinx.collections.immutable) + implementation(libs.coil.compose) + implementation(libs.kotlinx.serialization.json) } diff --git a/feature/review/src/main/java/team/retum/review/navigation/PostReviewNavigation.kt b/feature/review/src/main/java/team/retum/review/navigation/PostReviewNavigation.kt deleted file mode 100644 index fa639c671..000000000 --- a/feature/review/src/main/java/team/retum/review/navigation/PostReviewNavigation.kt +++ /dev/null @@ -1,28 +0,0 @@ -package team.retum.review.navigation - -import androidx.navigation.NavController -import androidx.navigation.NavGraphBuilder -import androidx.navigation.NavType -import androidx.navigation.compose.composable -import androidx.navigation.navArgument -import team.retum.review.ui.PostReview - -const val NAVIGATION_POST_REVIEW = "postReview" - -private const val COMPANY_ID = "companyId" -fun NavGraphBuilder.postReview(onBackPressed: () -> Unit) { - composable( - route = "$NAVIGATION_POST_REVIEW/{$COMPANY_ID}", - arguments = listOf(navArgument(COMPANY_ID) { NavType.StringType }), - ) { - PostReview( - onBackPressed = onBackPressed, - companyId = it.arguments?.getString(COMPANY_ID)?.toLong() - ?: throw NullPointerException(), - ) - } -} - -fun NavController.navigateToPostReview(companyId: Long) { - navigate("$NAVIGATION_POST_REVIEW/$companyId") -} diff --git a/feature/review/src/main/java/team/retum/review/navigation/ReviewDetailsNavigation.kt b/feature/review/src/main/java/team/retum/review/navigation/ReviewDetailsNavigation.kt index a9d32f338..96e3a57f6 100644 --- a/feature/review/src/main/java/team/retum/review/navigation/ReviewDetailsNavigation.kt +++ b/feature/review/src/main/java/team/retum/review/navigation/ReviewDetailsNavigation.kt @@ -9,31 +9,25 @@ import team.retum.common.utils.ResourceKeys import team.retum.review.ui.ReviewDetails const val NAVIGATION_REVIEW_DETAILS = "reviewDetails" -const val WRITER = "writer" fun NavGraphBuilder.reviewDetails( onBackPressed: () -> Unit, ) { composable( - route = "$NAVIGATION_REVIEW_DETAILS/{${ResourceKeys.REVIEW_ID}}/{$WRITER}", + route = "$NAVIGATION_REVIEW_DETAILS/{${ResourceKeys.REVIEW_ID}}", arguments = listOf( - navArgument(ResourceKeys.REVIEW_ID) { NavType.StringType }, - navArgument(WRITER) { NavType.StringType }, + navArgument(ResourceKeys.REVIEW_ID) { type = NavType.LongType }, ), ) { - val reviewId = it.arguments?.getString(ResourceKeys.REVIEW_ID) ?: "" - val writer = it.arguments?.getString(WRITER) ?: "" + val reviewId = it.arguments?.getLong(ResourceKeys.REVIEW_ID) ?: 0 + ReviewDetails( - onBackPressed = onBackPressed, - writer = writer, reviewId = reviewId, + onBackPressed = onBackPressed, ) } } -fun NavController.navigateToReviewDetails( - reviewId: String, - writer: String, -) { - navigate("$NAVIGATION_REVIEW_DETAILS/$reviewId/$writer") +fun NavController.navigateToReviewDetails(reviewId: Long) { + navigate("$NAVIGATION_REVIEW_DETAILS/$reviewId") } diff --git a/feature/review/src/main/java/team/retum/review/navigation/ReviewFilterNavigation.kt b/feature/review/src/main/java/team/retum/review/navigation/ReviewFilterNavigation.kt new file mode 100644 index 000000000..a73dd5bc2 --- /dev/null +++ b/feature/review/src/main/java/team/retum/review/navigation/ReviewFilterNavigation.kt @@ -0,0 +1,22 @@ +package team.retum.review.navigation + +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.compose.composable +import team.retum.review.ui.ReviewFilter + +const val NAVIGATION_REVIEW_FILTER = "reviewFilter" + +fun NavGraphBuilder.reviewFilter( + onBackPressed: () -> Unit, +) { + composable( + route = NAVIGATION_REVIEW_FILTER, + ) { + ReviewFilter(onBackPressed = onBackPressed) + } +} + +fun NavController.navigateToReviewFilter() { + navigate(NAVIGATION_REVIEW_FILTER) +} diff --git a/feature/review/src/main/java/team/retum/review/navigation/ReviewNavigation.kt b/feature/review/src/main/java/team/retum/review/navigation/ReviewNavigation.kt new file mode 100644 index 000000000..ce3eb5c1b --- /dev/null +++ b/feature/review/src/main/java/team/retum/review/navigation/ReviewNavigation.kt @@ -0,0 +1,28 @@ +package team.retum.review.navigation + +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.compose.composable +import team.retum.review.ui.Review + +const val NAVIGATION_REVIEW = "review" + +fun NavGraphBuilder.review( + onReviewFilterClick: () -> Unit, + onSearchReviewClick: () -> Unit, + onReviewDetailClick: (Long) -> Unit, +) { + composable( + route = NAVIGATION_REVIEW, + ) { + Review( + onReviewFilterClick = onReviewFilterClick, + onSearchReviewClick = onSearchReviewClick, + onReviewDetailClick = onReviewDetailClick, + ) + } +} + +fun NavController.navigateToReview() { + navigate(NAVIGATION_REVIEW) +} diff --git a/feature/review/src/main/java/team/retum/review/navigation/ReviewsNavigation.kt b/feature/review/src/main/java/team/retum/review/navigation/ReviewsNavigation.kt deleted file mode 100644 index d0387696c..000000000 --- a/feature/review/src/main/java/team/retum/review/navigation/ReviewsNavigation.kt +++ /dev/null @@ -1,40 +0,0 @@ -package team.retum.review.navigation - -import androidx.navigation.NavController -import androidx.navigation.NavGraphBuilder -import androidx.navigation.NavType -import androidx.navigation.compose.composable -import androidx.navigation.navArgument -import team.retum.common.utils.ResourceKeys -import team.retum.review.ui.Reviews - -const val NAVIGATION_REVIEWS = "reviews" - -fun NavGraphBuilder.reviews( - onBackPressed: () -> Unit, - navigateToReviewDetails: (String, String) -> Unit, -) { - composable( - route = "$NAVIGATION_REVIEWS/{${ResourceKeys.COMPANY_ID}}/{${ResourceKeys.COMPANY_NAME}}", - arguments = listOf( - navArgument(ResourceKeys.COMPANY_ID) { NavType.StringType }, - navArgument(ResourceKeys.COMPANY_NAME) { NavType.StringType }, - ), - ) { - val companyId = it.arguments?.getString(ResourceKeys.COMPANY_ID) ?: "0" - val companyName = it.arguments?.getString(ResourceKeys.COMPANY_NAME) ?: "" - Reviews( - onBackPressed = onBackPressed, - companyId = companyId.toLong(), - companyName = companyName, - navigateToReviewDetails = navigateToReviewDetails, - ) - } -} - -fun NavController.navigateToReviews( - companyId: Long, - companyName: String, -) { - navigate("$NAVIGATION_REVIEWS/$companyId/$companyName") -} diff --git a/feature/review/src/main/java/team/retum/review/navigation/SearchReviewsNavigation.kt b/feature/review/src/main/java/team/retum/review/navigation/SearchReviewsNavigation.kt new file mode 100644 index 000000000..e64f7c3e9 --- /dev/null +++ b/feature/review/src/main/java/team/retum/review/navigation/SearchReviewsNavigation.kt @@ -0,0 +1,26 @@ +package team.retum.review.navigation + +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.compose.composable +import team.retum.review.ui.SearchReview + +const val NAVIGATION_SEARCH_REVIEW = "searchReview" + +fun NavGraphBuilder.searchReview( + onBackPressed: () -> Unit, + onReviewDetailClick: (Long) -> Unit, +) { + composable( + route = NAVIGATION_SEARCH_REVIEW, + ) { + SearchReview( + onBackPressed = onBackPressed, + onReviewDetailClick = onReviewDetailClick, + ) + } +} + +fun NavController.navigateToSearchReview() { + navigate(NAVIGATION_SEARCH_REVIEW) +} diff --git a/feature/review/src/main/java/team/retum/review/ui/PostReviewScreen.kt b/feature/review/src/main/java/team/retum/review/ui/PostReviewScreen.kt deleted file mode 100644 index db5eeb1d1..000000000 --- a/feature/review/src/main/java/team/retum/review/ui/PostReviewScreen.kt +++ /dev/null @@ -1,420 +0,0 @@ -package team.retum.review.ui - -import android.annotation.SuppressLint -import androidx.compose.foundation.background -import androidx.compose.foundation.border -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.ExperimentalMaterialApi -import androidx.compose.material.ModalBottomSheetLayout -import androidx.compose.material.ModalBottomSheetState -import androidx.compose.material.ModalBottomSheetValue -import androidx.compose.material.rememberModalBottomSheetState -import androidx.compose.material3.Icon -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.snapshots.SnapshotStateList -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.SpanStyle -import androidx.compose.ui.text.buildAnnotatedString -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.text.withStyle -import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import team.retum.common.enums.ReviewProcess -import team.retum.jobis.review.R -import team.retum.jobisdesignsystemv2.appbar.JobisLargeTopAppBar -import team.retum.jobisdesignsystemv2.button.ButtonColor -import team.retum.jobisdesignsystemv2.button.JobisButton -import team.retum.jobisdesignsystemv2.card.JobisCard -import team.retum.jobisdesignsystemv2.checkbox.JobisCheckBox -import team.retum.jobisdesignsystemv2.foundation.JobisIcon -import team.retum.jobisdesignsystemv2.foundation.JobisTheme -import team.retum.jobisdesignsystemv2.foundation.JobisTypography -import team.retum.jobisdesignsystemv2.text.JobisText -import team.retum.jobisdesignsystemv2.textfield.JobisTextField -import team.retum.jobisdesignsystemv2.toast.JobisToast -import team.retum.review.viewmodel.ReviewSideEffect -import team.retum.review.viewmodel.ReviewState -import team.retum.review.viewmodel.ReviewViewModel -import team.retum.usecase.entity.CodesEntity -import team.retum.usecase.entity.PostReviewEntity - -const val SEARCH_DELAY: Long = 200 - -@OptIn(ExperimentalMaterialApi::class) -@Composable -internal fun PostReview( - onBackPressed: () -> Unit, - companyId: Long, - reviewViewModel: ReviewViewModel = hiltViewModel(), -) { - val state by reviewViewModel.state.collectAsStateWithLifecycle() - val sheetState = - rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden) - val sheetScope = rememberCoroutineScope() - val context = LocalContext.current - - LaunchedEffect(Unit) { - reviewViewModel.sideEffect.collect { - if (it is ReviewSideEffect.Success) { - JobisToast.create( - context = context, - message = context.getString(R.string.added_question), - ).show() - } - } - } - - LaunchedEffect(state.keyword) { - delay(SEARCH_DELAY) - reviewViewModel.fetchCodes(state.keyword) - } - - PostReviewScreen( - onBackPressed = onBackPressed, - sheetScope = sheetScope, - hideModalBottomSheet = { sheetScope.launch { sheetState.hide() } }, - onSheetShow = { sheetScope.launch { sheetState.show() } }, - sheetState = sheetState, - addQuestion = { - reviewViewModel.addReview() - reviewViewModel.keywords.add(state.keyword) - }, - state = state, - setQuestion = reviewViewModel::setQuestion, - setAnswer = reviewViewModel::setAnswer, - setKeyword = reviewViewModel::setKeyword, - setSelectedTech = reviewViewModel::setSelectedTech, - setChecked = reviewViewModel::setChecked, - onReviewProcessChange = reviewViewModel::setReviewProcess, - reviewProcess = state.reviewProcess, - techs = reviewViewModel.techs, - fetchQuestion = { reviewViewModel.postReview(companyId) }, - reviews = reviewViewModel.reviews, - setInit = reviewViewModel::setInit, - keywords = reviewViewModel.keywords, - ) -} - -@SuppressLint("CoroutineCreationDuringComposition") -@OptIn(ExperimentalMaterialApi::class) -@Composable -private fun PostReviewScreen( - onBackPressed: () -> Unit, - sheetScope: CoroutineScope, - hideModalBottomSheet: () -> Unit, - onSheetShow: () -> Unit, - sheetState: ModalBottomSheetState, - addQuestion: () -> Unit, - state: ReviewState, - setQuestion: (String) -> Unit, - setAnswer: (String) -> Unit, - setKeyword: (String) -> Unit, - setSelectedTech: (Long) -> Unit, - setChecked: (String) -> Unit, - onReviewProcessChange: (ReviewProcess) -> Unit, - reviewProcess: ReviewProcess, - techs: SnapshotStateList, - fetchQuestion: () -> Unit, - reviews: SnapshotStateList, - setInit: () -> Unit, - keywords: SnapshotStateList, -) { - ModalBottomSheetLayout( - modifier = Modifier - .background(JobisTheme.colors.background) - .fillMaxSize(), - sheetState = sheetState, - sheetContent = { - if (reviewProcess == ReviewProcess.FINISH) { - hideModalBottomSheet() - addQuestion() - } else { - AddQuestionBottomSheet( - onReviewProcess = { onReviewProcessChange(it) }, - state = state, - setQuestion = setQuestion, - setAnswer = setAnswer, - setKeyword = setKeyword, - setSelectedTech = setSelectedTech, - reviewProcess = reviewProcess, - setChecked = setChecked, - techs = techs, - ) - } - }, - sheetShape = RoundedCornerShape( - topStart = 12.dp, - topEnd = 12.dp, - ), - sheetBackgroundColor = JobisTheme.colors.inverseSurface, - ) { - Column( - modifier = Modifier - .background(JobisTheme.colors.background) - .fillMaxSize(), - ) { - JobisLargeTopAppBar( - onBackPressed = onBackPressed, - title = stringResource(id = R.string.write_review), - ) - if (reviews.isEmpty()) { - Box( - modifier = Modifier - .fillMaxWidth() - .padding( - horizontal = 24.dp, - vertical = 4.dp, - ) - .border( - width = 1.dp, - shape = RoundedCornerShape(12.dp), - color = JobisTheme.colors.surfaceVariant, - ), - ) { - JobisText( - text = stringResource(id = R.string.empty), - color = JobisTheme.colors.onSurfaceVariant, - style = JobisTypography.SubBody, - textAlign = TextAlign.Center, - modifier = Modifier - .fillMaxWidth() - .padding(16.dp), - ) - } - } else { - LazyColumn { - items(reviews.size) { - ReviewContent( - review = reviews[it], - keyword = keywords[it], - ) - } - } - } - JobisButton( - text = stringResource(id = R.string.add_question), - onClick = { - onReviewProcessChange(ReviewProcess.QUESTION) - onSheetShow() - setInit() - }, - ) - Spacer(modifier = Modifier.weight(1f)) - JobisButton( - text = if (reviewProcess == ReviewProcess.FINISH) { - stringResource(id = R.string.finish) - } else { - stringResource(id = R.string.please_write_review) - }, - onClick = { - fetchQuestion() - onBackPressed() - }, - enabled = reviews.isNotEmpty(), - color = ButtonColor.Primary, - ) - } - } -} - -@Composable -private fun AddQuestionBottomSheet( - onReviewProcess: (ReviewProcess) -> Unit, - reviewProcess: ReviewProcess, - state: ReviewState, - setQuestion: (String) -> Unit, - setAnswer: (String) -> Unit, - setKeyword: (String) -> Unit, - setSelectedTech: (Long) -> Unit, - setChecked: (String) -> Unit, - techs: SnapshotStateList, -) { - Column { - JobisText( - text = if (reviewProcess == ReviewProcess.QUESTION) { - stringResource(id = R.string.add_question) - } else { - stringResource(id = R.string.tech) - }, - color = JobisTheme.colors.onSurfaceVariant, - style = JobisTypography.SubBody, - modifier = Modifier.padding( - top = 24.dp, - bottom = 16.dp, - start = 24.dp, - end = 24.dp, - ), - ) - if (reviewProcess == ReviewProcess.QUESTION) { - JobisTextField( - value = { state.question }, - hint = stringResource(id = R.string.example), - onValueChange = setQuestion, - title = stringResource(id = R.string.question), - fieldColor = JobisTheme.colors.background, - ) - JobisTextField( - value = { state.answer }, - hint = stringResource(id = R.string.example), - onValueChange = setAnswer, - title = stringResource(id = R.string.answer), - fieldColor = JobisTheme.colors.background, - ) - } else { - JobisTextField( - value = { state.keyword }, - hint = stringResource(id = R.string.search), - drawableResId = JobisIcon.Search, - onValueChange = setKeyword, - fieldColor = JobisTheme.colors.background, - ) - LazyColumn(modifier = Modifier.fillMaxHeight(0.3f)) { - items(techs) { codes -> - Row( - modifier = Modifier.padding( - horizontal = 24.dp, - vertical = 12.dp, - ), - ) { - JobisCheckBox( - checked = state.checked == codes.keyword, - onClick = { - setChecked(codes.keyword) - setKeyword(codes.keyword) - setSelectedTech(codes.code) - }, - backgroundColor = JobisTheme.colors.background, - ) - Spacer(modifier = Modifier.width(8.dp)) - JobisText( - text = codes.keyword, - style = JobisTypography.Body, - color = JobisTheme.colors.inverseOnSurface, - modifier = Modifier.align(Alignment.CenterVertically), - ) - } - } - } - } - JobisButton( - text = stringResource(id = R.string.next), - onClick = { - onReviewProcess( - when (reviewProcess) { - ReviewProcess.QUESTION -> ReviewProcess.TECH - else -> ReviewProcess.FINISH - }, - ) - }, - color = ButtonColor.Primary, - enabled = state.buttonEnabled, - ) - } -} - -@Composable -private fun ReviewContent( - review: PostReviewEntity.PostReviewContentEntity, - keyword: String, -) { - var showQuestionDetail by remember { mutableStateOf(false) } - JobisCard( - modifier = Modifier - .fillMaxWidth() - .padding( - horizontal = 24.dp, - vertical = 4.dp, - ) - .clip(RoundedCornerShape(12.dp)) - .background(JobisTheme.colors.surfaceVariant), - onClick = { showQuestionDetail = !showQuestionDetail }, - ) { - Row( - modifier = Modifier - .padding( - vertical = 12.dp, - horizontal = 16.dp, - ), - ) { - Column { - if (!showQuestionDetail) { - JobisText( - text = review.question, - style = JobisTypography.SubHeadLine, - modifier = Modifier.padding(bottom = 4.dp), - ) - JobisText( - text = keyword, - style = JobisTypography.Description, - color = JobisTheme.colors.onPrimary, - ) - } else { - Text( - text = buildAnnotatedString { - withStyle(style = SpanStyle(color = JobisTheme.colors.onPrimary)) { - append("Q ") - } - withStyle(style = SpanStyle(color = JobisTheme.colors.onBackground)) { - append(review.question) - } - }, - style = JobisTypography.SubHeadLine, - modifier = Modifier.padding(bottom = 4.dp), - ) - JobisText( - text = keyword, - style = JobisTypography.Description, - color = JobisTheme.colors.onPrimary, - ) - Text( - text = buildAnnotatedString { - withStyle(style = SpanStyle(color = JobisTheme.colors.onPrimary)) { - append("A ") - } - withStyle(style = SpanStyle(color = JobisTheme.colors.inverseOnSurface)) { - append(review.answer) - } - }, - style = JobisTypography.Description, - modifier = Modifier - .padding(top = 12.dp) - .fillMaxWidth(0.5f), - ) - } - } - Spacer(modifier = Modifier.weight(1f)) - Icon( - painter = painterResource(id = R.drawable.ic_arrow_down), - contentDescription = "arrow_down", - modifier = Modifier.align(Alignment.CenterVertically), - ) - } - } -} diff --git a/feature/review/src/main/java/team/retum/review/ui/ReviewDetailsScreen.kt b/feature/review/src/main/java/team/retum/review/ui/ReviewDetailsScreen.kt index b0c08327c..a3eac8670 100644 --- a/feature/review/src/main/java/team/retum/review/ui/ReviewDetailsScreen.kt +++ b/feature/review/src/main/java/team/retum/review/ui/ReviewDetailsScreen.kt @@ -1,99 +1,189 @@ package team.retum.review.ui -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.core.animateFloatAsState -import androidx.compose.foundation.ScrollState import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.RowScope -import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Icon +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.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.rotate +import androidx.compose.ui.draw.clip import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toPersistentList +import team.retum.common.enums.InterviewLocation +import team.retum.common.enums.InterviewType import team.retum.jobis.review.R -import team.retum.jobisdesignsystemv2.appbar.JobisLargeTopAppBar +import team.retum.jobisdesignsystemv2.appbar.JobisSmallTopAppBar import team.retum.jobisdesignsystemv2.card.JobisCard +import team.retum.jobisdesignsystemv2.empty.EmptyContent import team.retum.jobisdesignsystemv2.foundation.JobisTheme import team.retum.jobisdesignsystemv2.foundation.JobisTypography +import team.retum.jobisdesignsystemv2.tab.TabBar import team.retum.jobisdesignsystemv2.text.JobisText -import team.retum.review.viewmodel.ReviewDetailsState import team.retum.review.viewmodel.ReviewDetailsViewModel +import team.retum.usecase.entity.FetchReviewDetailEntity @Composable internal fun ReviewDetails( + reviewId: Long, onBackPressed: () -> Unit, - writer: String, - reviewId: String, reviewDetailsViewModel: ReviewDetailsViewModel = hiltViewModel(), ) { - val scrollState = rememberScrollState() + val tabs = listOf( + stringResource(id = R.string.interview_review), + stringResource(id = R.string.reviewed_question), + ) val state by reviewDetailsViewModel.state.collectAsStateWithLifecycle() - LaunchedEffect(Unit) { - reviewDetailsViewModel.setReviewId(reviewId = reviewId) + LaunchedEffect(reviewId) { + // TODO : 실 값 들어왔을 때 UI 호출 + reviewDetailsViewModel.setReviewId(reviewId.toString()) reviewDetailsViewModel.fetchReviewDetails() } ReviewDetailsScreen( + reviewDetail = state.reviewDetail, + tabs = tabs.toPersistentList(), + selectedTabIndex = state.selectedTabIndex, + onSelectTab = { + reviewDetailsViewModel.setTabIndex(it) + }, onBackPressed = onBackPressed, - writer = writer, - scrollState = scrollState, - state = state, ) } @Composable private fun ReviewDetailsScreen( + reviewDetail: FetchReviewDetailEntity, + tabs: ImmutableList, + selectedTabIndex: Int, + onSelectTab: (Int) -> Unit, onBackPressed: () -> Unit, +) { + Column { + JobisSmallTopAppBar( + title = stringResource(id = R.string.review_detail_title), + onBackPressed = onBackPressed, + ) + TabBar( + selectedTabIndex = selectedTabIndex, + tabs = tabs, + onSelectTab = onSelectTab, + ) + StudentInfo( + writer = reviewDetail.writer, + major = reviewDetail.major, + year = reviewDetail.year.toString(), + companyName = reviewDetail.companyName, + location = reviewDetail.location, + type = reviewDetail.type, + interviewerCount = reviewDetail.interviewerCount.toString(), + selectedTabIndex = selectedTabIndex, + ) + when (selectedTabIndex) { + 0 -> InterviewReview(review = reviewDetail.qnaResponse) + 1 -> ExpectedReview(review = reviewDetail) + } + } +} + +@Composable +private fun StudentInfo( writer: String, - scrollState: ScrollState, - state: ReviewDetailsState, + major: String, + year: String, + companyName: String, + location: InterviewLocation, + type: InterviewType, + interviewerCount: String, + selectedTabIndex: Int, ) { Column( modifier = Modifier - .fillMaxSize() - .background(JobisTheme.colors.background), + .fillMaxWidth() + .padding(horizontal = 24.dp, vertical = 20.dp), ) { - JobisLargeTopAppBar( - onBackPressed = onBackPressed, - title = writer, - ) - Column( - modifier = Modifier - .verticalScroll(scrollState) - .padding(horizontal = 24.dp), + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, ) { JobisText( - modifier = Modifier.padding(vertical = 8.dp), - text = stringResource(id = R.string.review_questions), - color = JobisTheme.colors.onSurfaceVariant, - style = JobisTypography.Description, + text = stringResource( + id = R.string.review_writer_title, + writer, + if (selectedTabIndex != 0) stringResource(id = R.string.reviewed_question) else stringResource(id = R.string.interview_review), + ), + style = JobisTypography.PageTitle, ) - Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { - state.questions.forEach { - val (showAnswer, setShowAnswer) = remember { mutableStateOf(false) } - ReviewQuestionContent( - question = it.question, - area = it.area, - answer = it.answer, - showAnswer = showAnswer, - onShowAnswerClick = setShowAnswer, + Row( + horizontalArrangement = Arrangement.spacedBy(12.dp), + ) { + JobisText( + text = major, + style = JobisTypography.Description, + color = JobisTheme.colors.onPrimary, + ) + JobisText( + text = year, + style = JobisTypography.Description, + color = JobisTheme.colors.inverseOnSurface, + ) + } + } + Row( + modifier = Modifier + .fillMaxWidth() + .padding(top = 10.dp), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + val type = when (type) { + InterviewType.INDIVIDUAL -> stringResource(id = R.string.individual_interview) + InterviewType.GROUP -> stringResource(id = R.string.group_interview) + InterviewType.OTHER -> stringResource(id = R.string.other_interview) + } + val location = when (location) { + InterviewLocation.DAEJEON -> stringResource(id = R.string.daejeon) + InterviewLocation.SEOUL -> stringResource(id = R.string.seoul) + InterviewLocation.GYEONGGI -> stringResource(id = R.string.gyeonggi) + InterviewLocation.OTHER -> stringResource(id = R.string.other) + } + val items = listOf(companyName, location, type, stringResource(id = R.string.interviewer_count_format, interviewerCount)) + + items.forEachIndexed { index, item -> + JobisText( + text = item, + style = JobisTypography.SubBody, + color = JobisTheme.colors.inverseOnSurface, + ) + + if (index < items.size - 1) { + JobisText( + text = "•", + style = JobisTypography.SubBody, + color = JobisTheme.colors.inverseOnSurface, ) } } @@ -102,91 +192,166 @@ private fun ReviewDetailsScreen( } @Composable -private fun ReviewQuestionContent( - question: String, - area: String, - answer: String, - showAnswer: Boolean, - onShowAnswerClick: (Boolean) -> Unit, +private fun InterviewReview( + review: List, ) { - val rotate by animateFloatAsState( - targetValue = if (showAnswer) { - 180f - } else { - 0f - }, - label = "", + ReviewContent( + review = review, ) +} - JobisCard(onClick = { onShowAnswerClick(!showAnswer) }) { - Column( - modifier = Modifier.padding( - horizontal = 16.dp, - vertical = 12.dp, - ), - verticalArrangement = Arrangement.spacedBy(12.dp), +@Composable +private fun ExpectedReview( + review: FetchReviewDetailEntity, +) { + if (!review.answer.isBlank() || !review.question.isBlank()) { + JobisCard( + modifier = Modifier + .fillMaxWidth() + .padding( + horizontal = 24.dp, + vertical = 4.dp, + ) + .clip(RoundedCornerShape(12.dp)) + .background(JobisTheme.colors.surfaceVariant), + onClick = {}, ) { Row( - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .padding( + vertical = 12.dp, + horizontal = 16.dp, + ), ) { - Question( - question = question, - area = area, - ) - Icon( - modifier = Modifier.rotate(rotate), - painter = painterResource(id = R.drawable.ic_arrow_down), - contentDescription = "down", - tint = JobisTheme.colors.onSurfaceVariant, - ) - } - AnimatedVisibility(visible = showAnswer) { - Answer(answer = answer) + Column { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = buildAnnotatedString { + withStyle(style = SpanStyle(color = JobisTheme.colors.onPrimary, fontSize = 24.sp)) { + append("Q ") + } + withStyle(style = SpanStyle(color = JobisTheme.colors.onBackground, fontSize = 16.sp)) { + append(review.question) + } + }, + style = JobisTypography.SubHeadLine, + modifier = Modifier.padding(bottom = 4.dp), + ) + } + Text( + text = buildAnnotatedString { + withStyle(style = SpanStyle(color = JobisTheme.colors.onPrimary, fontSize = 24.sp)) { + append("A ") + } + withStyle(style = SpanStyle(color = JobisTheme.colors.inverseOnSurface, fontSize = 14.sp)) { + append(review.answer) + } + }, + style = JobisTypography.Description, + modifier = Modifier + .padding(top = 12.dp) + .fillMaxWidth(0.5f), + maxLines = 3, + ) + } } } + } else { + EmptyContent( + title = stringResource(R.string.empty_content_answer_not_found), + description = stringResource(R.string.empty_content_other_interview_title), + ) } } @Composable -private fun RowScope.Question( - question: String, - area: String, +private fun ReviewContent( + review: List, ) { - Row( - modifier = Modifier.weight(1f), - horizontalArrangement = Arrangement.spacedBy(8.dp), - ) { - JobisText( - text = "Q", - style = JobisTypography.SubHeadLine, - color = JobisTheme.colors.onPrimary, - ) - Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { - JobisText( - text = question, - style = JobisTypography.SubHeadLine, - ) - JobisText( - text = area, - style = JobisTypography.Description, - color = JobisTheme.colors.secondary, - ) + review.forEachIndexed { index, reviewItem -> + var showQuestionDetail by remember { mutableStateOf(false) } + JobisCard( + modifier = Modifier + .fillMaxWidth() + .padding( + horizontal = 24.dp, + vertical = 4.dp, + ) + .clip(RoundedCornerShape(12.dp)) + .background(JobisTheme.colors.surfaceVariant), + onClick = { showQuestionDetail = !showQuestionDetail }, + ) { + Row( + modifier = Modifier + .padding( + vertical = 12.dp, + horizontal = 16.dp, + ), + ) { + Column { + if (!showQuestionDetail) { + Row { + JobisText( + text = reviewItem.question, + textAlign = TextAlign.Center, + style = JobisTypography.SubHeadLine, + modifier = Modifier.padding(bottom = 4.dp), + ) + Spacer(modifier = Modifier.weight(1f)) + Icon( + painter = painterResource(id = R.drawable.ic_arrow_down), + contentDescription = stringResource(id = R.string.content_description_arrow_down), + modifier = Modifier.align(Alignment.CenterVertically), + ) + } + } else { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + ) { + JobisText( + text = stringResource(id = R.string.question_q), + color = JobisTheme.colors.onPrimary, + style = JobisTypography.SubHeadLine.copy(fontSize = 24.sp), + ) + Spacer(modifier = Modifier.width(8.dp)) + JobisText( + text = reviewItem.question, + color = JobisTheme.colors.onBackground, + style = JobisTypography.SubHeadLine, + textAlign = TextAlign.Center, + ) + Spacer(modifier = Modifier.weight(1f)) + Icon( + painter = painterResource(id = R.drawable.ic_arrow_down), + contentDescription = stringResource(id = R.string.content_description_arrow_down), + ) + } + Row( + modifier = Modifier.padding(top = 12.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + JobisText( + text = stringResource(id = R.string.answer_a), + color = JobisTheme.colors.onPrimary, + style = JobisTypography.Description.copy(fontSize = 24.sp), + textAlign = TextAlign.Center, + ) + Spacer(modifier = Modifier.width(8.dp)) + JobisText( + text = reviewItem.answer, + color = JobisTheme.colors.inverseOnSurface, + style = JobisTypography.Description, + textAlign = TextAlign.Center, + maxLines = 3, + ) + } + } + } + } } } } - -@Composable -private fun Answer(answer: String) { - Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { - JobisText( - text = "A", - style = JobisTypography.SubHeadLine, - ) - JobisText( - text = answer, - style = JobisTypography.Description, - color = JobisTheme.colors.inverseOnSurface, - ) - } -} diff --git a/feature/review/src/main/java/team/retum/review/ui/ReviewFilterScreen.kt b/feature/review/src/main/java/team/retum/review/ui/ReviewFilterScreen.kt new file mode 100644 index 000000000..f5cdd822f --- /dev/null +++ b/feature/review/src/main/java/team/retum/review/ui/ReviewFilterScreen.kt @@ -0,0 +1,373 @@ +package team.retum.review.ui + +import androidx.compose.animation.animateColorAsState +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.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import team.retum.common.enums.InterviewLocation +import team.retum.common.enums.InterviewType +import team.retum.jobis.review.R +import team.retum.jobisdesignsystemv2.appbar.JobisSmallTopAppBar +import team.retum.jobisdesignsystemv2.button.ButtonColor +import team.retum.jobisdesignsystemv2.button.JobisButton +import team.retum.jobisdesignsystemv2.checkbox.JobisCheckBox +import team.retum.jobisdesignsystemv2.foundation.JobisTheme +import team.retum.jobisdesignsystemv2.foundation.JobisTypography +import team.retum.jobisdesignsystemv2.text.JobisText +import team.retum.jobisdesignsystemv2.utils.clickable +import team.retum.review.viewmodel.ReviewFilterViewModel +import team.retum.review.viewmodel.ReviewFilterViewModel.Companion.code +import team.retum.review.viewmodel.ReviewFilterViewModel.Companion.interviewType +import team.retum.review.viewmodel.ReviewFilterViewModel.Companion.location +import team.retum.review.viewmodel.ReviewFilterViewModel.Companion.year +import team.retum.review.viewmodel.ReviewsFilterState +import team.retum.usecase.entity.CodesEntity + +@Composable +internal fun ReviewFilter( + onBackPressed: () -> Unit, + reviewFilterViewModel: ReviewFilterViewModel = hiltViewModel(), +) { + val state by reviewFilterViewModel.state.collectAsStateWithLifecycle() + + LaunchedEffect(Unit) { + reviewFilterViewModel.getLocalYears() + } + + ReviewFilterScreen( + state = state, + onBackPressed = onBackPressed, + onMajorSelected = reviewFilterViewModel::setSelectedMajor, + onYearSelected = reviewFilterViewModel::setSelectedYear, + onInterviewTypeSelected = reviewFilterViewModel::setSelectedInterviewType, + onLocationSelected = reviewFilterViewModel::setSelectedLocation, + ) +} + +@Composable +private fun ReviewFilterScreen( + state: ReviewsFilterState, + onBackPressed: () -> Unit, + onMajorSelected: (Long?) -> Unit, + onYearSelected: (Int?) -> Unit, + onInterviewTypeSelected: (InterviewType?) -> Unit, + onLocationSelected: (InterviewLocation?) -> Unit, +) { + Box(modifier = Modifier.fillMaxSize()) { + Column { + JobisSmallTopAppBar( + onBackPressed = onBackPressed, + title = stringResource(id = R.string.filter_setting), + ) + Column( + modifier = Modifier.verticalScroll(rememberScrollState()), + ) { + Skills( + majorList = state.majorList, + selectedMajorCode = state.selectedMajorCode, + onMajorSelected = onMajorSelected, + ) + Years( + years = state.years, + selectedYear = state.selectedYear, + onYearSelected = onYearSelected, + ) + InterviewType( + selectedInterviewType = state.selectedInterviewType, + onInterviewTypeSelected = onInterviewTypeSelected, + ) + Location( + selectedLocation = state.selectedLocation, + onLocationSelected = onLocationSelected, + ) + } + } + JobisButton( + text = stringResource(id = R.string.appliance), + onClick = { + code = state.selectedMajorCode + year = state.selectedYear + location = state.selectedLocation + interviewType = state.selectedInterviewType + onBackPressed() + }, + modifier = Modifier.align(Alignment.BottomCenter), + color = ButtonColor.Primary, + ) + } +} + +@OptIn(ExperimentalLayoutApi::class) +@Composable +private fun Skills( + majorList: List, + selectedMajorCode: Long?, + onMajorSelected: (Long?) -> Unit, +) { + Column( + modifier = Modifier.padding(start = 24.dp, end = 24.dp), + ) { + JobisText( + modifier = Modifier.padding(vertical = 12.dp), + text = stringResource(id = R.string.major), + style = JobisTypography.SubHeadLine, + color = JobisTheme.colors.inverseOnSurface, + ) + FlowRow( + modifier = Modifier.padding(vertical = 20.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + maxItemsInEachRow = 5, + ) { + majorList.forEach { codes -> + MajorContent( + major = codes.keyword, + majorId = codes.code, + selected = selectedMajorCode == codes.code, + onClick = { onMajorSelected(it) }, + ) + } + } + } +} + +@OptIn(ExperimentalLayoutApi::class) +@Composable +private fun Years( + years: List, + selectedYear: Int?, + onYearSelected: (Int?) -> Unit, +) { + Column( + modifier = Modifier.padding(start = 24.dp, end = 24.dp), + ) { + JobisText( + modifier = Modifier.padding(vertical = 12.dp), + text = stringResource(id = R.string.year), + style = JobisTypography.SubHeadLine, + color = JobisTheme.colors.inverseOnSurface, + ) + FlowRow( + modifier = Modifier.padding(vertical = 20.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + maxItemsInEachRow = 5, + ) { + years.forEach { year -> + YearContent( + year = "$year", + selected = selectedYear == year, + onClick = { onYearSelected(it) }, + ) + } + } + } +} + +@Composable +private fun InterviewType( + selectedInterviewType: InterviewType?, + onInterviewTypeSelected: (InterviewType?) -> Unit, +) { + Column( + modifier = Modifier.padding(start = 24.dp, end = 24.dp), + ) { + JobisText( + modifier = Modifier.padding(vertical = 12.dp), + text = stringResource(id = R.string.interview_category), + style = JobisTypography.SubHeadLine, + color = JobisTheme.colors.inverseOnSurface, + ) + ReviewCheckBox( + title = stringResource(id = R.string.individual_interview), + checked = selectedInterviewType == InterviewType.INDIVIDUAL, + onClick = { onInterviewTypeSelected(InterviewType.INDIVIDUAL) }, + ) + ReviewCheckBox( + title = stringResource(id = R.string.group_interview), + checked = selectedInterviewType == InterviewType.GROUP, + onClick = { onInterviewTypeSelected(InterviewType.GROUP) }, + ) + ReviewCheckBox( + title = stringResource(id = R.string.other_interview), + checked = selectedInterviewType == InterviewType.OTHER, + onClick = { onInterviewTypeSelected(InterviewType.OTHER) }, + ) + } +} + +@Composable +private fun Location( + selectedLocation: InterviewLocation?, + onLocationSelected: (InterviewLocation?) -> Unit, +) { + Column( + modifier = Modifier.padding(start = 24.dp, end = 24.dp), + ) { + JobisText( + modifier = Modifier.padding(vertical = 12.dp), + text = stringResource(id = R.string.region), + style = JobisTypography.SubHeadLine, + color = JobisTheme.colors.inverseOnSurface, + ) + ReviewCheckBox( + title = stringResource(id = R.string.daejeon), + checked = selectedLocation == InterviewLocation.DAEJEON, + onClick = { onLocationSelected(InterviewLocation.DAEJEON) }, + ) + ReviewCheckBox( + title = stringResource(id = R.string.seoul), + checked = selectedLocation == InterviewLocation.SEOUL, + onClick = { onLocationSelected(InterviewLocation.SEOUL) }, + ) + ReviewCheckBox( + title = stringResource(id = R.string.gyeonggi), + checked = selectedLocation == InterviewLocation.GYEONGGI, + onClick = { onLocationSelected(InterviewLocation.GYEONGGI) }, + ) + ReviewCheckBox( + title = stringResource(id = R.string.other), + checked = selectedLocation == InterviewLocation.OTHER, + onClick = { onLocationSelected(InterviewLocation.OTHER) }, + ) + } +} + +@Composable +private fun MajorContent( + modifier: Modifier = Modifier, + major: String, + majorId: Long, + selected: Boolean, + onClick: (Long?) -> Unit, +) { + val background by animateColorAsState( + targetValue = if (selected) { + JobisTheme.colors.onPrimary + } else { + JobisTheme.colors.inverseSurface + }, + label = "", + ) + val textColor by animateColorAsState( + targetValue = if (selected) { + JobisTheme.colors.background + } else { + JobisTheme.colors.onPrimaryContainer + }, + label = "", + ) + + Box( + modifier = modifier + .clickable( + enabled = true, + onClick = { onClick(majorId) }, + onPressed = {}, + ) + .clip(RoundedCornerShape(30.dp)) + .background(background), + contentAlignment = Alignment.Center, + ) { + JobisText( + modifier = modifier.padding( + horizontal = 12.dp, + vertical = 4.dp, + ), + text = major, + style = JobisTypography.Body, + color = textColor, + ) + } +} + +@Composable +private fun YearContent( + modifier: Modifier = Modifier, + year: String, + selected: Boolean, + onClick: (Int) -> Unit, +) { + val background by animateColorAsState( + targetValue = if (selected) { + JobisTheme.colors.onPrimary + } else { + JobisTheme.colors.inverseSurface + }, + label = "", + ) + val textColor by animateColorAsState( + targetValue = if (selected) { + JobisTheme.colors.background + } else { + JobisTheme.colors.onPrimaryContainer + }, + label = "", + ) + + Box( + modifier = modifier + .clickable( + enabled = true, + onClick = { onClick(year.toInt()) }, + onPressed = {}, + ) + .clip(RoundedCornerShape(30.dp)) + .background(background), + contentAlignment = Alignment.Center, + ) { + JobisText( + modifier = modifier.padding( + horizontal = 12.dp, + vertical = 4.dp, + ), + text = year, + style = JobisTypography.Body, + color = textColor, + ) + } +} + +@Composable +private fun ReviewCheckBox( + title: String, + checked: Boolean, + onClick: () -> Unit, +) { + Row( + modifier = Modifier.padding(vertical = 12.dp), + ) { + JobisCheckBox( + checked = checked, + onClick = { onClick() }, + ) + Spacer(modifier = Modifier.width(8.dp)) + JobisText( + text = title, + style = JobisTypography.Body, + color = JobisTheme.colors.inverseOnSurface, + modifier = Modifier.align(Alignment.CenterVertically), + ) + } +} diff --git a/feature/review/src/main/java/team/retum/review/ui/ReviewScreen.kt b/feature/review/src/main/java/team/retum/review/ui/ReviewScreen.kt new file mode 100644 index 000000000..717b1cb9e --- /dev/null +++ b/feature/review/src/main/java/team/retum/review/ui/ReviewScreen.kt @@ -0,0 +1,91 @@ +package team.retum.review.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import team.retum.jobis.review.R +import team.retum.jobisdesignsystemv2.appbar.JobisLargeTopAppBar +import team.retum.jobisdesignsystemv2.button.JobisIconButton +import team.retum.jobisdesignsystemv2.foundation.JobisIcon +import team.retum.jobisdesignsystemv2.foundation.JobisTheme +import team.retum.review.ui.component.ReviewItems +import team.retum.review.viewmodel.ReviewFilterViewModel +import team.retum.review.viewmodel.ReviewViewModel +import team.retum.review.viewmodel.ReviewsState + +@Composable +internal fun Review( + onReviewFilterClick: () -> Unit, + onSearchReviewClick: () -> Unit, + onReviewDetailClick: (Long) -> Unit, + reviewViewModel: ReviewViewModel = hiltViewModel(), +) { + val state by reviewViewModel.state.collectAsStateWithLifecycle() + + LaunchedEffect(Unit) { + with(reviewViewModel) { + setYear(ReviewFilterViewModel.year) + setCode(ReviewFilterViewModel.code) + setLocation(ReviewFilterViewModel.location) + setInterviewType(ReviewFilterViewModel.interviewType) + clearReview() + fetchReviews() + } + } + + ReviewScreen( + state = state, + onReviewFilterClick = onReviewFilterClick, + onSearchReviewClick = onSearchReviewClick, + onReviewDetailClick = onReviewDetailClick, + ) +} + +@Composable +private fun ReviewScreen( + state: ReviewsState, + onReviewFilterClick: () -> Unit, + onSearchReviewClick: () -> Unit, + onReviewDetailClick: (Long) -> Unit, +) { + Column( + modifier = Modifier + .fillMaxSize() + .background(JobisTheme.colors.background), + ) { + JobisLargeTopAppBar(title = stringResource(id = R.string.review)) { + JobisIconButton( + drawableResId = JobisIcon.Filter, + contentDescription = stringResource(id = R.string.content_description_filter), + onClick = onReviewFilterClick, + tint = JobisTheme.colors.onPrimary, + ) + JobisIconButton( + drawableResId = JobisIcon.Search, + contentDescription = stringResource(id = R.string.content_description_search), + onClick = onSearchReviewClick, + ) + } + LazyColumn { + items(state.reviews.size) { + val review = state.reviews[it] + ReviewItems( + companyImageUrl = review.companyLogoUrl, + companyName = review.companyName, + reviewId = review.reviewId, + writer = review.writer, + major = review.major, + onReviewDetailClick = onReviewDetailClick, + ) + } + } + } +} diff --git a/feature/review/src/main/java/team/retum/review/ui/ReviewsScreen.kt b/feature/review/src/main/java/team/retum/review/ui/ReviewsScreen.kt deleted file mode 100644 index 006ac5f52..000000000 --- a/feature/review/src/main/java/team/retum/review/ui/ReviewsScreen.kt +++ /dev/null @@ -1,76 +0,0 @@ -package team.retum.review.ui - -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.toPersistentList -import team.retum.jobisdesignsystemv2.appbar.JobisLargeTopAppBar -import team.retum.jobisdesignsystemv2.foundation.JobisTheme -import team.retum.jobisdesignsystemv2.review.ReviewContent -import team.retum.review.viewmodel.ReviewsViewModel -import team.retum.usecase.entity.FetchReviewsEntity - -@Composable -internal fun Reviews( - onBackPressed: () -> Unit, - companyId: Long, - companyName: String, - navigateToReviewDetails: (String, String) -> Unit, - reviewsViewModel: ReviewsViewModel = hiltViewModel(), -) { - val state by reviewsViewModel.state.collectAsStateWithLifecycle() - - LaunchedEffect(Unit) { - reviewsViewModel.setCompanyId(companyId = companyId) - reviewsViewModel.fetchReviews() - } - - ReviewsScreen( - onBackPressed = onBackPressed, - companyName = companyName, - onReviewContentClick = navigateToReviewDetails, - reviews = state.reviews.toPersistentList(), - ) -} - -@Composable -private fun ReviewsScreen( - onBackPressed: () -> Unit, - companyName: String, - onReviewContentClick: (String, String) -> Unit, - reviews: ImmutableList, -) { - Column( - modifier = Modifier - .fillMaxSize() - .background(JobisTheme.colors.background), - ) { - JobisLargeTopAppBar( - onBackPressed = onBackPressed, - title = "${companyName}의 면접 후기", - ) - Column( - modifier = Modifier.padding(horizontal = 24.dp), - verticalArrangement = Arrangement.spacedBy(8.dp), - ) { - reviews.forEach { - ReviewContent( - onClick = onReviewContentClick, - reviewId = it.reviewId, - writer = it.writer, - year = it.year, - ) - } - } - } -} diff --git a/feature/review/src/main/java/team/retum/review/ui/SearchReviewScreen.kt b/feature/review/src/main/java/team/retum/review/ui/SearchReviewScreen.kt new file mode 100644 index 000000000..87f553873 --- /dev/null +++ b/feature/review/src/main/java/team/retum/review/ui/SearchReviewScreen.kt @@ -0,0 +1,73 @@ +package team.retum.review.ui + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.res.stringResource +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import team.retum.jobis.review.R +import team.retum.jobisdesignsystemv2.appbar.JobisSmallTopAppBar +import team.retum.jobisdesignsystemv2.empty.EmptyContent +import team.retum.jobisdesignsystemv2.foundation.JobisIcon +import team.retum.jobisdesignsystemv2.textfield.JobisTextField +import team.retum.review.ui.component.ReviewItems +import team.retum.review.viewmodel.SearchReviewsState +import team.retum.review.viewmodel.SearchReviewsViewModel + +@Composable +internal fun SearchReview( + onBackPressed: () -> Unit, + onReviewDetailClick: (Long) -> Unit, + searchViewModel: SearchReviewsViewModel = hiltViewModel(), +) { + val state by searchViewModel.state.collectAsStateWithLifecycle() + + SearchReviewScreen( + state = state, + onBackPressed = onBackPressed, + onReviewDetailClick = onReviewDetailClick, + onNameChange = searchViewModel::setKeyword, + ) +} + +@Composable +private fun SearchReviewScreen( + state: SearchReviewsState, + onBackPressed: () -> Unit, + onReviewDetailClick: (Long) -> Unit, + onNameChange: (String) -> Unit, +) { + Column { + JobisSmallTopAppBar( + onBackPressed = onBackPressed, + ) + JobisTextField( + value = { state.keyword ?: "" }, + hint = stringResource(R.string.review_hint), + onValueChange = onNameChange, + drawableResId = JobisIcon.Search, + ) + if (!state.showRecruitmentsEmptyContent) { + LazyColumn { + items(state.reviews.size) { + val review = state.reviews[it] + ReviewItems( + companyImageUrl = review.companyLogoUrl, + companyName = review.companyName, + reviewId = review.reviewId, + writer = review.writer, + major = review.major, + onReviewDetailClick = onReviewDetailClick, + ) + } + } + } else { + EmptyContent( + title = stringResource(R.string.search_review_not_find), + description = stringResource(R.string.search_review_expect), + ) + } + } +} diff --git a/feature/review/src/main/java/team/retum/review/ui/component/ReviewItems.kt b/feature/review/src/main/java/team/retum/review/ui/component/ReviewItems.kt new file mode 100644 index 000000000..27883f981 --- /dev/null +++ b/feature/review/src/main/java/team/retum/review/ui/component/ReviewItems.kt @@ -0,0 +1,81 @@ +package team.retum.review.ui.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +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.layout.ContentScale +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import coil.compose.AsyncImage +import team.retum.jobisdesignsystemv2.foundation.JobisTheme +import team.retum.jobisdesignsystemv2.foundation.JobisTypography +import team.retum.jobisdesignsystemv2.text.JobisText +import team.retum.jobisdesignsystemv2.utils.clickable + +@Composable +internal fun ReviewItems( + modifier: Modifier = Modifier, + companyImageUrl: String, + companyName: String, + reviewId: Long, + writer: String, + major: String, + onReviewDetailClick: (Long) -> Unit, +) { + Row( + modifier = modifier + .fillMaxWidth() + .padding( + vertical = 12.dp, + horizontal = 24.dp, + ), + verticalAlignment = Alignment.CenterVertically, + ) { + Row( + modifier = modifier + .weight(1f) + .clickable(onClick = { onReviewDetailClick(reviewId) }), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + AsyncImage( + modifier = Modifier + .size(48.dp) + .clip(RoundedCornerShape(8.dp)) + .background( + color = if (companyImageUrl.isEmpty()) { + JobisTheme.colors.surfaceVariant + } else { + Color.Unspecified + }, + ), + model = companyImageUrl, + contentDescription = "company profile", + contentScale = ContentScale.Crop, + ) + Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { + JobisText( + text = companyName, + style = JobisTypography.SubHeadLine, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + JobisText( + text = "$major • $writer", + style = JobisTypography.Description, + color = JobisTheme.colors.onPrimary, + ) + } + } + } +} diff --git a/feature/review/src/main/java/team/retum/review/viewmodel/PostReviewViewModel.kt b/feature/review/src/main/java/team/retum/review/viewmodel/PostReviewViewModel.kt deleted file mode 100644 index ccf1454b2..000000000 --- a/feature/review/src/main/java/team/retum/review/viewmodel/PostReviewViewModel.kt +++ /dev/null @@ -1,158 +0,0 @@ -package team.retum.review.viewmodel - -import androidx.compose.runtime.mutableStateListOf -import androidx.compose.runtime.snapshots.SnapshotStateList -import androidx.lifecycle.viewModelScope -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import team.retum.common.base.BaseViewModel -import team.retum.common.enums.CodeType -import team.retum.common.enums.ReviewProcess -import team.retum.common.exception.BadRequestException -import team.retum.usecase.entity.CodesEntity -import team.retum.usecase.entity.PostReviewEntity -import team.retum.usecase.usecase.code.FetchCodeUseCase -import team.retum.usecase.usecase.review.PostReviewUseCase -import javax.inject.Inject - -@HiltViewModel -internal class ReviewViewModel @Inject constructor( - private val postReviewUseCase: PostReviewUseCase, - private val fetchCodeUseCase: FetchCodeUseCase, -) : BaseViewModel(ReviewState.getDefaultState()) { - - var techs: SnapshotStateList = mutableStateListOf() - private set - - var reviews: SnapshotStateList = mutableStateListOf() - private set - - var keywords: SnapshotStateList = mutableStateListOf() - private set - - internal fun setInit() = - setState { - state.value.copy( - question = "", - answer = "", - keyword = "", - checked = "", - selectedTech = 0, - tech = null, - buttonEnabled = false, - reviewProcess = ReviewProcess.QUESTION, - ) - } - - internal fun setQuestion(question: String) { - setState { state.value.copy(question = question) } - setButtonEnabled() - } - - internal fun setAnswer(answer: String) { - setState { state.value.copy(answer = answer) } - setButtonEnabled() - } - - internal fun setKeyword(keyword: String) { - techs.clear() - setState { state.value.copy(keyword = keyword) } - setButtonEnabled() - } - - internal fun addReview() { - reviews.add( - PostReviewEntity.PostReviewContentEntity( - answer = state.value.answer, - question = state.value.question, - codeId = state.value.selectedTech, - ), - ) - } - - internal fun setSelectedTech(selectedTech: Long) = - setState { state.value.copy(selectedTech = selectedTech) } - - internal fun postReview(companyId: Long) { - viewModelScope.launch(Dispatchers.IO) { - postReviewUseCase( - postReviewRequest = PostReviewEntity( - companyId = companyId, - qnaElements = reviews, - ), - ).onSuccess { - postSideEffect(ReviewSideEffect.Success) - }.onFailure { - when (it) { - is BadRequestException -> { - postSideEffect(ReviewSideEffect.BadRequest) - } - } - } - } - } - - internal fun fetchCodes(keyword: String?) = - viewModelScope.launch(Dispatchers.IO) { - fetchCodeUseCase( - keyword = keyword, - type = CodeType.TECH, - parentCode = null, - ).onSuccess { - techs.addAll(it.codes) - } - } - - internal fun setReviewProcess(reviewProcess: ReviewProcess) { - setState { state.value.copy(reviewProcess = reviewProcess) } - setButtonEnabled() - } - - private fun setButtonEnabled() { - when (state.value.reviewProcess) { - ReviewProcess.QUESTION -> { - setState { state.value.copy(buttonEnabled = state.value.question.isNotEmpty() && state.value.answer.isNotEmpty()) } - } - - else -> { - setState { state.value.copy(buttonEnabled = state.value.checked.isNotEmpty()) } - } - } - } - - internal fun setChecked(checked: String) { - setState { state.value.copy(checked = checked) } - setButtonEnabled() - } -} - -internal data class ReviewState( - val question: String, - val answer: String, - val keyword: String, - val checked: String, - val selectedTech: Long, - val tech: String?, - val buttonEnabled: Boolean, - val reviewProcess: ReviewProcess, -) { - companion object { - fun getDefaultState() = ReviewState( - question = "", - answer = "", - keyword = "", - checked = "", - selectedTech = 0, - tech = null, - buttonEnabled = false, - reviewProcess = ReviewProcess.QUESTION, - ) - } -} - -internal sealed interface ReviewSideEffect { - data object BadRequest : ReviewSideEffect - - data object Success : ReviewSideEffect -} diff --git a/feature/review/src/main/java/team/retum/review/viewmodel/ReviewDetailsViewModel.kt b/feature/review/src/main/java/team/retum/review/viewmodel/ReviewDetailsViewModel.kt index 6bdd5599c..3fe997889 100644 --- a/feature/review/src/main/java/team/retum/review/viewmodel/ReviewDetailsViewModel.kt +++ b/feature/review/src/main/java/team/retum/review/viewmodel/ReviewDetailsViewModel.kt @@ -1,11 +1,15 @@ package team.retum.review.viewmodel +import android.location.Location +import android.util.Log import androidx.compose.runtime.Immutable import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import team.retum.common.base.BaseViewModel +import team.retum.common.enums.InterviewLocation +import team.retum.common.enums.InterviewType import team.retum.usecase.entity.FetchReviewDetailEntity import team.retum.usecase.usecase.review.FetchReviewDetailUseCase import javax.inject.Inject @@ -22,21 +26,39 @@ internal class ReviewDetailsViewModel @Inject constructor( internal fun fetchReviewDetails() { viewModelScope.launch(Dispatchers.IO) { fetchReviewDetailsUseCase(state.value.reviewId).onSuccess { - setState { state.value.copy(questions = it.qnaResponses) } + setState { state.value.copy(reviewDetail = it) } } } } + + internal fun setTabIndex(tabIndex: Int) { + setState { state.value.copy(selectedTabIndex = tabIndex) } + } } @Immutable internal data class ReviewDetailsState( + val selectedTabIndex: Int, val reviewId: String, - val questions: List, + val reviewDetail: FetchReviewDetailEntity, ) { companion object { fun getInitialState() = ReviewDetailsState( + selectedTabIndex = 0, reviewId = "", - questions = emptyList(), + reviewDetail = FetchReviewDetailEntity( + reviewId = "", + companyName = "", + writer = "", + major = "", + type = InterviewType.INDIVIDUAL, + location = InterviewLocation.GYEONGGI, + interviewerCount = 0, + year = 0, + qnaResponse = emptyList(), + question = "", + answer = "", + ), ) } } diff --git a/feature/review/src/main/java/team/retum/review/viewmodel/ReviewFilterViewModel.kt b/feature/review/src/main/java/team/retum/review/viewmodel/ReviewFilterViewModel.kt new file mode 100644 index 000000000..3e407f42a --- /dev/null +++ b/feature/review/src/main/java/team/retum/review/viewmodel/ReviewFilterViewModel.kt @@ -0,0 +1,113 @@ +package team.retum.review.viewmodel + +import androidx.compose.runtime.Immutable +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import team.retum.common.base.BaseViewModel +import team.retum.common.enums.CodeType +import team.retum.common.enums.InterviewLocation +import team.retum.common.enums.InterviewType +import team.retum.usecase.entity.CodesEntity +import team.retum.usecase.usecase.code.FetchCodeUseCase +import java.time.LocalDate +import javax.inject.Inject + +@HiltViewModel +internal class ReviewFilterViewModel @Inject constructor( + private val fetchCodeUseCase: FetchCodeUseCase, +) : BaseViewModel(initialState = ReviewsFilterState.getDefaultState()) { + + companion object { + var code: Long? = null + var year: Int? = null + var interviewType: InterviewType? = null + var location: InterviewLocation? = null + } + + init { + fetchCodes() + } + + private fun fetchCodes() { + viewModelScope.launch(Dispatchers.IO) { + fetchCodeUseCase( + keyword = null, + type = CodeType.JOB, + parentCode = null, + ).onSuccess { + setState { + state.value.copy( + majorList = it.codes, + ) + } + } + } + } + + internal fun getLocalYears() { + val startYear = 2020 + val endYear = LocalDate.now().year + 1 + viewModelScope.launch(Dispatchers.IO) { + setState { + state.value.copy( + years = (startYear..endYear).toList().reversed(), + ) + } + } + } + + internal fun setSelectedMajor(majorCode: Long?) { + setState { + state.value.copy( + selectedMajorCode = if (state.value.selectedMajorCode == majorCode) null else majorCode, + ) + } + } + + internal fun setSelectedYear(year: Int?) { + setState { + state.value.copy( + selectedYear = if (state.value.selectedYear == year) null else year, + ) + } + } + + internal fun setSelectedInterviewType(type: InterviewType?) { + setState { + state.value.copy( + selectedInterviewType = if (state.value.selectedInterviewType == type) null else type, + ) + } + } + + internal fun setSelectedLocation(location: InterviewLocation?) { + setState { + state.value.copy( + selectedLocation = if (state.value.selectedLocation == location) null else location, + ) + } + } +} + +@Immutable +data class ReviewsFilterState( + val years: List, + val majorList: List, + val selectedMajorCode: Long? = null, + val selectedYear: Int? = null, + val selectedInterviewType: InterviewType? = null, + val selectedLocation: InterviewLocation? = null, +) { + companion object { + fun getDefaultState() = ReviewsFilterState( + years = emptyList(), + majorList = emptyList(), + selectedMajorCode = null, + selectedYear = null, + selectedInterviewType = null, + selectedLocation = null, + ) + } +} diff --git a/feature/review/src/main/java/team/retum/review/viewmodel/ReviewViewModel.kt b/feature/review/src/main/java/team/retum/review/viewmodel/ReviewViewModel.kt new file mode 100644 index 000000000..b7909dbdc --- /dev/null +++ b/feature/review/src/main/java/team/retum/review/viewmodel/ReviewViewModel.kt @@ -0,0 +1,85 @@ +package team.retum.review.viewmodel + +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import team.retum.common.base.BaseViewModel +import team.retum.common.enums.InterviewLocation +import team.retum.common.enums.InterviewType +import team.retum.usecase.entity.FetchReviewsEntity +import team.retum.usecase.usecase.review.FetchReviewsUseCase +import javax.inject.Inject + +@HiltViewModel +internal class ReviewViewModel @Inject constructor( + private val fetchReviewsUseCase: FetchReviewsUseCase, +) : BaseViewModel(ReviewsState.getInitialState()) { + + internal fun setCode(code: Long?) = setState { + state.value.copy(code = code) + } + + internal fun setYear(year: Int?) = setState { + state.value.copy(year = year) + } + + internal fun setInterviewType(interviewType: InterviewType?) = setState { + state.value.copy(interviewType = interviewType) + } + + internal fun setLocation(location: InterviewLocation?) = setState { + state.value.copy(location = location) + } + + internal fun clearReview() { + if (state.value.code != null || state.value.year != null) { + setState { + state.value.copy( + page = 0L, + reviews = emptyList(), + ) + } + } + } + + internal fun fetchReviews() { + with(state.value) { + viewModelScope.launch(Dispatchers.IO) { + fetchReviewsUseCase( + page = null, + location = location, + interviewType = interviewType, + companyId = null, + keyword = null, + year = year, + code = code, + ).onSuccess { + setState { state.value.copy(reviews = it.reviews) } + } + } + } + } +} + +internal data class ReviewsState( + val page: Long, + val companyId: Long, + val code: Long?, + val year: Int?, + val interviewType: InterviewType?, + val location: InterviewLocation?, + val reviews: List, +) { + companion object { + fun getInitialState() = ReviewsState( + page = 0L, + companyId = 0, + code = null, + year = null, + interviewType = null, + location = null, + reviews = emptyList(), + ) + } +} diff --git a/feature/review/src/main/java/team/retum/review/viewmodel/ReviewsViewModel.kt b/feature/review/src/main/java/team/retum/review/viewmodel/ReviewsViewModel.kt deleted file mode 100644 index 06c30ea08..000000000 --- a/feature/review/src/main/java/team/retum/review/viewmodel/ReviewsViewModel.kt +++ /dev/null @@ -1,40 +0,0 @@ -package team.retum.review.viewmodel - -import androidx.lifecycle.viewModelScope -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import team.retum.common.base.BaseViewModel -import team.retum.usecase.entity.FetchReviewsEntity -import team.retum.usecase.usecase.review.FetchReviewsUseCase -import javax.inject.Inject - -@HiltViewModel -internal class ReviewsViewModel @Inject constructor( - private val fetchReviewsUseCase: FetchReviewsUseCase, -) : BaseViewModel(ReviewsState.getInitialState()) { - - internal fun setCompanyId(companyId: Long) = setState { - state.value.copy(companyId = companyId) - } - - internal fun fetchReviews() { - viewModelScope.launch(Dispatchers.IO) { - fetchReviewsUseCase(companyId = state.value.companyId).onSuccess { - setState { state.value.copy(reviews = it.reviews) } - } - } - } -} - -internal data class ReviewsState( - val companyId: Long, - val reviews: List, -) { - companion object { - fun getInitialState() = ReviewsState( - companyId = 0, - reviews = emptyList(), - ) - } -} diff --git a/feature/review/src/main/java/team/retum/review/viewmodel/SearchReviewsViewModel.kt b/feature/review/src/main/java/team/retum/review/viewmodel/SearchReviewsViewModel.kt new file mode 100644 index 000000000..59cb5faac --- /dev/null +++ b/feature/review/src/main/java/team/retum/review/viewmodel/SearchReviewsViewModel.kt @@ -0,0 +1,80 @@ +package team.retum.review.viewmodel + +import androidx.compose.runtime.Immutable +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch +import team.retum.common.base.BaseViewModel +import team.retum.usecase.entity.FetchReviewsEntity +import team.retum.usecase.usecase.review.FetchReviewsUseCase +import javax.inject.Inject + +const val SEARCH_DEBOUNCE_MILLIS = 1000L + +@HiltViewModel +internal class SearchReviewsViewModel @Inject constructor( + private val fetchReviewsUseCase: FetchReviewsUseCase, +) : BaseViewModel(SearchReviewsState.getInitialState()) { + + init { + debounceName() + } + + internal fun setKeyword(keyword: String) = setState { + state.value.copy(keyword = keyword) + } + + @OptIn(FlowPreview::class) + private fun debounceName() { + viewModelScope.launch { + state.map { it.keyword }.distinctUntilChanged().debounce(SEARCH_DEBOUNCE_MILLIS).collect { + if (!it.isNullOrBlank()) { + fetchReviews() + } + } + } + } + + private fun fetchReviews() { + with(state.value) { + viewModelScope.launch(Dispatchers.IO) { + fetchReviewsUseCase( + companyId = null, + page = null, + location = null, + interviewType = null, + keyword = keyword, + year = null, + code = null, + ).onSuccess { + setState { + state.value.copy( + showRecruitmentsEmptyContent = it.reviews.isEmpty(), + reviews = it.reviews, + ) + } + } + } + } + } +} + +@Immutable +data class SearchReviewsState( + val keyword: String?, + val reviews: List, + val showRecruitmentsEmptyContent: Boolean, +) { + companion object { + fun getInitialState() = SearchReviewsState( + keyword = null, + reviews = emptyList(), + showRecruitmentsEmptyContent = false, + ) + } +} diff --git a/feature/review/src/main/res/values/strings.xml b/feature/review/src/main/res/values/strings.xml index 59c9867af..723b90201 100644 --- a/feature/review/src/main/res/values/strings.xml +++ b/feature/review/src/main/res/values/strings.xml @@ -1,18 +1,41 @@ - 질문 - 답변 - 다른 학생들을 위하여\n면접의 후기를 작성해주세요 - 질문 추가하기 - 현재 입력 된 질문이 없어요 - 질문이 추가되었어요! - 다음 - 기술 스택 - 작성 완료 - 후기를 작성해주세요 - 검색어를 입력해주세요 - example - 받은 면접 질문 + 면접 후기 + 받은 질문 + 면접후기 상세보기 + %1$s님의 %2$s + 면접관 수 : %s + 개인 면접 + 단체 면접 + 기타 면접 + 대전 + 서울 + 경기 + 기타 + arrow_down + Q + A + 받은 질문을 작성하지 않았어요! + 다른 학생의 전공 후기를 봐주세요! + + + 후기 + 잘 못 된 요청이에요 + filter + search + + + 적용하기 + 필터 설정 + 전공 + 년도 + 면접 구분 + 지역 + + + 찾고 싶은 면접후기를 입력해주세요 + 검색어와 관련 된 면접 후기를 못찾았어요 + 제대로 입력했는지 다시 한번 확인해주세요 diff --git a/settings.gradle.kts b/settings.gradle.kts index 248f195c6..b5039a214 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -44,3 +44,4 @@ include(":feature:application") include(":feature:splash") include(":core:device") include(":feature:employment") +include(":feature:post-review")