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")